UNPKG

12.5 kBJavaScriptView Raw
1"use strict";
2
3
4const diff = require('mout/array/difference');
5const startsWith = require('mout/string/startsWith');
6
7const parsefunc = require('reflection-js/parsefunc');
8const splitArgs = require('nyks/process/splitArgs');
9const sprintf = require('nyks/string/format');
10const repeat = require('nyks/string/repeat');
11const rreplaces = require('nyks/string/rreplaces');
12const box = require('./box');
13const readline = require('../pty/readline');
14
15
16const RUNNER_NS = 'runner';
17const OUTPUT_JSON = 'json';
18const OUTPUT_RAW = 'raw';
19
20
21class Cnyks {
22
23 constructor(dict, name) {
24 if(!dict)
25 dict = {};
26
27 this.output = null;
28 if(dict['ir://json'])
29 this.output = OUTPUT_JSON;
30 if(dict['ir://raw'])
31 this.output = OUTPUT_RAW;
32
33 let prompt; //might be undefined
34 let output = Function.prototype;
35 let trace = Function.prototype;
36 let cols = 76;
37
38 if(dict["ir://stream"]) {
39 let stream = dict["ir://stream"];
40 output = stream.write.bind(stream);
41 trace = stream.stderr.write.bind(stream.stderr);
42 prompt = readline.bind({stdin : stream, stderr : stream.stderr});
43
44 stream.on("resize", () => {
45 this.cols = Math.min(stream.columns - 2, 76);
46 });
47 cols = Math.min(stream.columns - 2, cols);
48 }
49
50 this._prompt = dict["ir://prompt"] || prompt;
51 this._stdout = dict["ir://stdout"] || output;
52 this._stderr = dict["ir://stderr"] || trace;
53 this.cols = dict["ir://cols"] || cols;
54
55
56
57 this.commands_list = {};
58 this.module_name = dict["ir://name"] || name;
59 this.scan(this, RUNNER_NS);
60 this._box = (...blocs) => {
61 return box({cols : this.cols}, ...blocs);
62 };
63 }
64
65
66 completer(line) /**
67 * Autocomplete helper
68 * @interactive_runner hide
69 */ {
70
71 var completions = [];
72 for(let [, command] of Object.entries(this.commands_list)) {
73 if(command['command_ns'] == RUNNER_NS)
74 continue;
75 if(!command['usage']['hide'])
76 completions.push(command['command_key']);
77 for(let [alias] of Object.entries(command['aliases'])) {
78 if([command['command_hash'], command['command_key']].indexOf(alias) == -1)
79 completions.push(alias);
80 }
81 }
82
83 var hits = completions.filter(function(c) { return c.indexOf(line) == 0; });
84 // show all completions if none found
85 let results = [hits.length ? hits : (line ? [] : completions), line];
86
87 return results;
88 }
89
90
91 help_cmd(command) /**
92 * @interactive_runner hide
93 */ {
94 var str = command['command_key'];
95 var aliases = Object.keys(command['aliases']).filter(entry => !command['aliases'][entry].length);
96
97 aliases = diff(aliases, [command['command_key'], command['command_hash']]);
98
99 if(aliases.length)
100 str += " (" + aliases.join(', ') + ")";
101
102 if(Object.keys(command['usage']['params']).length) {
103 var tmp_trailing_optionnal = 0; var tmp_str = [];
104
105 for(let [param_name, param_infos] of Object.entries(command['usage']['params'])) {
106 if(param_infos['optional'])
107 tmp_trailing_optionnal++;
108 tmp_str.push((param_infos['optional'] ? '[' : '') + (param_infos['rest'] ? '...' : '') + "$" + param_name);
109 }
110
111 str += " " + tmp_str.join(', ') + repeat("]", tmp_trailing_optionnal);
112 }
113
114 let lines = [];
115
116 let doc = command['usage']['doc'];
117 if(doc)
118 str = str + repeat(" ", Math.max(1, this.cols - str.length - doc.length - 2)) + doc;
119
120 if(!command['usage']['hide'])
121 lines.push(str);
122
123 for(let entry of Object.keys(command['aliases'])) {
124 let aliases = command['aliases'][entry];
125 if(aliases.length)
126 lines.push(`${entry} (=${command['command_key']} ${aliases.join(' ')}) `);
127 }
128
129 return lines;
130 }
131
132
133 list_commands() /**
134 * Display all available commands
135 * @alias ?
136 */ {
137
138 var msgs = {};
139 var rbx_msgs = [];
140
141 for(let [, command] of Object.entries(this.commands_list)) {
142
143 if(!msgs[command['command_ns']])
144 msgs[command['command_ns']] = [];
145
146 var lines = this.help_cmd(command);
147
148 msgs[command['command_ns']].push(...lines);
149 }
150
151 for(let [command_ns, msgss] of Object.entries(msgs)) {
152 rbx_msgs.push(`\`${command_ns}\` commands list`);
153 rbx_msgs.push(msgss.join("\n"));
154 }
155
156 this._stderr(this._box(...rbx_msgs));
157 }
158
159 _respond(response) {
160 if(response === undefined)
161 return;
162
163 if(!this.output)
164 return this._stderr(this._box("Response", response));
165
166 if(Buffer.isBuffer(response))
167 return this._stdout(response);
168
169 if(this.output == OUTPUT_RAW)
170 return this._stdout(String(response));
171
172 if(this.output == OUTPUT_JSON)
173 return this._stdout(JSON.stringify(response) + "\n");
174 }
175
176 generate_command_hash(command_ns, command_key) /**
177 * @interactive_runner hide
178 */ {
179 return sprintf("%s:%s", command_ns, command_key);
180 }
181
182 lookup(command_prompt) /**
183 * @interactive_runner hide
184 */ {
185 var command_resolve = [];
186 for(let [, command_infos] of Object.entries(this.commands_list)) {
187 if(command_prompt in command_infos['aliases'])
188 command_resolve.push(command_infos);
189 }
190
191 if(command_resolve.length > 1)
192 throw Error(sprintf("Too many results for command '%s', call explicitly [ns]:[cmd]", command_prompt));
193
194 return command_resolve[0];
195 }
196
197 command_alias(command_ns, command_key, alias, args = []) /**
198 * @interactive_runner hide
199 */ {
200 var command_hash = this.generate_command_hash(command_ns, command_key);
201 if(!this.commands_list[command_hash])
202 return false;
203
204 this.commands_list[command_hash]['aliases'][alias] = args;
205 }
206
207
208 command_register(command_ns, command_key, callback, usage) /**
209 * @interactive_runner hide
210 */ {
211 var command_hash = this.generate_command_hash(command_ns, command_key);
212 this.commands_list[command_hash] = {
213 'command_hash' : command_hash,
214 'command_ns' : command_ns,
215 'command_key' : command_key,
216 'usage' : usage,
217 'aliases' : {},
218
219 'apply' : function (argv) {
220 return callback.obj[callback.method_name].apply(callback.obj, argv);
221 },
222 };
223
224 if(!usage.hide)
225 this.command_alias(command_ns, command_key, command_key);
226
227 this.command_alias(command_ns, command_key, command_hash);
228 }
229
230
231 async command_parse(command_prompt, command_args, command_dict) /**
232 * @interactive_runner hide
233 */ {
234
235 if(!command_prompt)
236 return;
237
238 var command_infos = this.lookup(command_prompt);
239
240 if(!command_infos)
241 throw new Error(sprintf("Invalid command key '%s'", command_prompt));
242
243 var alias_args = command_infos['aliases'][command_prompt];
244 if(alias_args)
245 command_args = alias_args.concat(command_args);
246
247 var command_args_mask = command_infos['usage']['params'];
248
249 var mandatory_arg_index = 0;
250 var current_args = {};
251 //var mandatory_arg_len = Object.keys(command_args_mask).length;
252
253 for(let [param_name, param_infos] of Object.entries(command_args_mask)) {
254 var param_in;
255
256 if(command_args[mandatory_arg_index] !== undefined) {
257 param_in = command_args[mandatory_arg_index];
258 } else if(command_dict && command_dict[param_name] !== undefined) {
259 param_in = command_dict[param_name];
260 } else if(param_infos.rest) {
261 break;
262 } else if(param_infos.optional) {
263 param_in = param_infos['value'];
264 } else {
265 if(!this._prompt)
266 throw `Missing parameter --${param_name}`;
267
268 param_in = await this._prompt({
269 prompt : sprintf("$%s[%s] ", this.module_name, param_name)
270 });
271 }
272
273 if(typeof param_in === "string" && param_in !== "" && isFinite(param_in))
274 param_in = parseFloat(param_in);
275
276 current_args[param_name] = param_in;
277 mandatory_arg_index++;
278 }
279
280 for(let i = mandatory_arg_index; i < command_args.length; i++)
281 current_args[`+${i}`] = command_args[i];
282
283 return {
284 ...command_infos,
285 args : current_args,
286 argv : Object.values(current_args),
287 };
288
289 }
290
291
292 quit() /**
293 * @alias q
294 */ {
295 this._running = false;
296 }
297
298 async _run(opts) /**
299 * @interactive_runner hide
300 */ {
301 var run = [];
302 var start = [];
303
304 if(opts["ir://run"])
305 run = opts["ir://run"] === true ? "run" : opts["ir://run"];
306 if(opts["ir://start"])
307 start = opts["ir://start"] === true ? "start" : opts["ir://start"];
308
309 if(typeof run === "string")
310 run = [run];
311
312 if(typeof start === "string")
313 start = [start];
314
315 var operations = [].concat(start, run);
316
317 for(var cmd of operations) {
318 var foo = await this.command_parse(cmd, [], opts);
319
320 var response = await foo.apply(foo.argv);
321 this._respond(response);
322 }
323
324 if(run.length || !this._prompt)
325 return;
326
327 await this.command_loop();
328 }
329
330
331
332 async command_loop() /**
333 * @interactive_runner hide
334 */ {
335
336
337 this._running = true;
338
339 var opts = {
340 prompt : "$" + this.module_name + " : ",
341 completer : this.completer.bind(this),
342 };
343
344 var data_str;
345 var command_split;
346 var command_prompt;
347 var command;
348 do {
349 try {
350 data_str = await this._prompt(opts);
351 } catch(e) {
352 this._stderr(e + "\r\n");//very improbable
353 break;
354 }
355
356 command_split = splitArgs(data_str);
357 command_prompt = command_split.shift();
358
359 try {
360 command = await this.command_parse(command_prompt, command_split);
361 } catch(e) {
362 this._stderr(e + "\r\n");
363 command = null;
364 }
365
366 if(!command)
367 continue;
368
369 try {
370 var response = await command.apply(command.argv);
371 this._respond(response);
372 } catch(err) {
373 var trace = rreplaces(err.stack || '(none)', { [process.cwd()] : '.' });
374 this._stderr("\r\n" + this._box("!! Uncatched exception !!", "" + err, "Trace", trace));
375 }
376
377
378 } while(this._running);
379
380 }
381
382
383
384 scan(obj, command_ns) /**
385 * @interactive_runner hide
386 */ {
387 var proto = typeof obj == "function" ? obj.prototype : Object.getPrototypeOf(obj);
388
389 var level = obj, keys = [];
390
391 while(level && level != Function.prototype && level != Object.prototype) {
392 keys.push(...Object.getOwnPropertyNames(level));
393 level = Object.getPrototypeOf(level);
394 }
395
396 for(let method_name of keys) {
397 var section = command_ns;
398 if(typeof obj[method_name] != "function")
399 continue;
400
401 if(method_name == "initialize"
402 || method_name == "constructor"
403 || startsWith(method_name, "_"))
404 continue;
405
406
407 var command_key = method_name;
408 var callback = { obj, method_name};
409
410 var {blocs, params, doc} = parsefunc(proto[method_name] || obj[method_name]);
411 var ir = blocs['interactive_runner'];
412
413 var tmp = ir ? ir['computed'] : [];
414
415 if(blocs['section'])
416 section = blocs['section'].computed[0];
417
418 var usage = {
419 'doc' : doc[0] || "",
420 'params' : params,
421 'hide' : tmp.indexOf('hide') != -1,
422 };
423
424 this.command_register(section, command_key, callback, usage);
425
426
427 if(blocs.alias) {
428 for(let args of blocs.alias['values']) {
429 var alias_name = args.shift();
430 if(!(alias_name && command_key))
431 continue;
432 this.command_alias(command_ns, command_key, alias_name, args);
433 }
434 }
435 }
436
437 }
438
439
440
441 static start(module, opts, args, chain) {
442 if(!opts)
443 opts = {};
444 if(!args)
445 args = [];
446 if(!chain)
447 chain = Function.prototype;
448
449 var runner = new Cnyks(opts, module.name || module.constructor && module.constructor.name);
450
451 if(typeof module == "function") {
452 if(isConstructor(module) && isClass(module)) {
453 runner.scan(module, runner.module_name); //static scan
454 //new module(args...)
455 if(!opts['ir://static'])
456 module = new (Function.prototype.bind.apply(module, [null].concat(args)));
457 } else {
458 module = {[module.name || 'run'] : module};
459 }
460 }
461
462 runner.scan(module, runner.module_name);
463
464 if(!runner.output)
465 runner.list_commands();
466
467 runner._run(opts).then(function(body) { chain(null, body); }, chain);
468
469 return runner;
470 }
471
472}
473
474function isConstructor(obj) {
475 return typeof obj === "function" && !!obj.prototype && (obj.prototype.constructor === obj);
476}
477
478function isClass(v) {
479 return typeof v === 'function' && /^\s*class(?:\s+|{)/.test(v.toString());
480}
481
482module.exports = Cnyks;