1 | "use strict";
|
2 |
|
3 |
|
4 | const diff = require('mout/array/difference');
|
5 | const startsWith = require('mout/string/startsWith');
|
6 |
|
7 | const parsefunc = require('reflection-js/parsefunc');
|
8 | const splitArgs = require('nyks/process/splitArgs');
|
9 | const sprintf = require('nyks/string/format');
|
10 | const repeat = require('nyks/string/repeat');
|
11 | const rreplaces = require('nyks/string/rreplaces');
|
12 | const box = require('./box');
|
13 | const readline = require('../pty/readline');
|
14 |
|
15 |
|
16 | const RUNNER_NS = 'runner';
|
17 | const OUTPUT_JSON = 'json';
|
18 | const OUTPUT_RAW = 'raw';
|
19 |
|
20 |
|
21 | class 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;
|
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 |
|
68 |
|
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 |
|
85 | let results = [hits.length ? hits : (line ? [] : completions), line];
|
86 |
|
87 | return results;
|
88 | }
|
89 |
|
90 |
|
91 | help_cmd(command) |
92 |
|
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 |
|
135 |
|
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 |
|
178 | {
|
179 | return sprintf("%s:%s", command_ns, command_key);
|
180 | }
|
181 |
|
182 | lookup(command_prompt) |
183 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
294 | {
|
295 | this._running = false;
|
296 | }
|
297 |
|
298 | async _run(opts) |
299 |
|
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 |
|
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");
|
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 |
|
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);
|
454 |
|
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 |
|
474 | function isConstructor(obj) {
|
475 | return typeof obj === "function" && !!obj.prototype && (obj.prototype.constructor === obj);
|
476 | }
|
477 |
|
478 | function isClass(v) {
|
479 | return typeof v === 'function' && /^\s*class(?:\s+|{)/.test(v.toString());
|
480 | }
|
481 |
|
482 | module.exports = Cnyks;
|