devtopia 1.9.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/compose.d.ts +5 -0
- package/dist/commands/compose.js +120 -0
- package/dist/commands/docs.js +1 -1
- package/dist/commands/run.d.ts +6 -1
- package/dist/commands/run.js +57 -6
- package/dist/commands/search.d.ts +5 -0
- package/dist/commands/search.js +52 -0
- package/dist/commands/start.d.ts +1 -1
- package/dist/commands/start.js +141 -256
- package/dist/commands/submit.d.ts +2 -0
- package/dist/commands/submit.js +167 -122
- package/dist/executor.d.ts +28 -2
- package/dist/executor.js +237 -67
- package/dist/index.js +27 -4
- package/package.json +1 -1
package/dist/executor.js
CHANGED
|
@@ -18,7 +18,7 @@ async function fetchTool(name) {
|
|
|
18
18
|
/**
|
|
19
19
|
* Get the interpreter command for a language
|
|
20
20
|
*/
|
|
21
|
-
function getInterpreter(language) {
|
|
21
|
+
export function getInterpreter(language) {
|
|
22
22
|
switch (language) {
|
|
23
23
|
case 'typescript':
|
|
24
24
|
return { cmd: 'npx', args: ['tsx'], ext: '.ts' };
|
|
@@ -31,102 +31,272 @@ function getInterpreter(language) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* Detect if source defines a `function main(...)` without calling it,
|
|
35
|
+
* and wrap it with the standard argv boilerplate so it actually executes.
|
|
35
36
|
*/
|
|
36
|
-
export
|
|
37
|
+
export function wrapSourceIfNeeded(source, language) {
|
|
38
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
39
|
+
const definesMain = /\bfunction\s+main\s*\(/.test(source);
|
|
40
|
+
const callsMain = /\bmain\s*\(/.test(source.replace(/function\s+main\s*\(/, ''));
|
|
41
|
+
const hasArgvParse = /process\.argv\[2\]/.test(source);
|
|
42
|
+
if (definesMain && !callsMain && !hasArgvParse) {
|
|
43
|
+
return `${source}
|
|
44
|
+
|
|
45
|
+
// --- Devtopia auto-wrapper: call main() with argv input ---
|
|
46
|
+
const __input = JSON.parse(process.argv[2] || '{}');
|
|
47
|
+
const __result = main(__input);
|
|
48
|
+
if (__result !== undefined) {
|
|
49
|
+
if (__result instanceof Promise) {
|
|
50
|
+
__result.then(r => console.log(JSON.stringify(r))).catch(e => {
|
|
51
|
+
console.error(e.message || e);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
console.log(JSON.stringify(__result));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (language === 'python') {
|
|
62
|
+
const definesMain = /\bdef\s+main\s*\(/.test(source);
|
|
63
|
+
const callsMain = /\bmain\s*\(/.test(source.replace(/def\s+main\s*\(/, ''));
|
|
64
|
+
const hasArgvParse = /sys\.argv/.test(source);
|
|
65
|
+
if (definesMain && !callsMain && !hasArgvParse) {
|
|
66
|
+
return `${source}
|
|
67
|
+
|
|
68
|
+
# --- Devtopia auto-wrapper: call main() with argv input ---
|
|
69
|
+
import json, sys
|
|
70
|
+
__input = json.loads(sys.argv[1] if len(sys.argv) > 1 else '{}')
|
|
71
|
+
__result = main(__input)
|
|
72
|
+
if __result is not None:
|
|
73
|
+
print(json.dumps(__result))
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return source;
|
|
78
|
+
}
|
|
79
|
+
// ─── Composition Runtime Templates ───────────────────────────────────────────
|
|
80
|
+
const JS_RUNTIME = `// devtopia-runtime.js — Composition runtime for Devtopia tools
|
|
81
|
+
// Usage: const { devtopiaRun } = require('./devtopia-runtime');
|
|
82
|
+
// const result = devtopiaRun('tool-name', { input: 'data' });
|
|
83
|
+
|
|
84
|
+
const { execSync } = require('child_process');
|
|
85
|
+
|
|
86
|
+
function devtopiaRun(toolName, input) {
|
|
87
|
+
const inputStr = JSON.stringify(input || {});
|
|
88
|
+
const cliCmd = process.env.DEVTOPIA_CLI || 'npx devtopia';
|
|
89
|
+
try {
|
|
90
|
+
const stdout = execSync(
|
|
91
|
+
\`\${cliCmd} run \${toolName} '\${inputStr.replace(/'/g, "'\\\\''")}' --json --quiet\`,
|
|
92
|
+
{ encoding: 'utf-8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
93
|
+
);
|
|
94
|
+
const text = (stdout || '').trim();
|
|
95
|
+
const parsed = text ? JSON.parse(text) : null;
|
|
96
|
+
if (parsed && typeof parsed === 'object' && (parsed.error || parsed.ok === false)) {
|
|
97
|
+
throw new Error(parsed.error || 'Unknown error');
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const stderr = err && err.stderr ? String(err.stderr).trim() : '';
|
|
102
|
+
const message = stderr || (err && err.message ? err.message : 'Unknown error');
|
|
103
|
+
throw new Error(\`devtopiaRun(\${toolName}) failed: \${message}\`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { devtopiaRun };
|
|
108
|
+
`;
|
|
109
|
+
const PY_RUNTIME = `# devtopia_runtime.py — Composition runtime for Devtopia tools
|
|
110
|
+
# Usage: from devtopia_runtime import devtopia_run
|
|
111
|
+
# result = devtopia_run('tool-name', {'input': 'data'})
|
|
112
|
+
|
|
113
|
+
import subprocess, json, os
|
|
114
|
+
|
|
115
|
+
def devtopia_run(tool_name, input_data=None):
|
|
116
|
+
input_str = json.dumps(input_data or {})
|
|
117
|
+
cli_cmd = os.environ.get('DEVTOPIA_CLI', 'npx devtopia')
|
|
118
|
+
try:
|
|
119
|
+
result = subprocess.run(
|
|
120
|
+
(cli_cmd.split() + ['run', tool_name, input_str, '--json', '--quiet']),
|
|
121
|
+
capture_output=True, text=True, timeout=30
|
|
122
|
+
)
|
|
123
|
+
if result.returncode != 0:
|
|
124
|
+
raise RuntimeError(f"devtopia_run({tool_name}) failed: {result.stderr}")
|
|
125
|
+
output = (result.stdout or '').strip()
|
|
126
|
+
parsed = json.loads(output) if output else None
|
|
127
|
+
if isinstance(parsed, dict) and (parsed.get('error') is not None or parsed.get('ok') is False):
|
|
128
|
+
raise RuntimeError(parsed.get('error') or 'Unknown error')
|
|
129
|
+
return parsed
|
|
130
|
+
except subprocess.TimeoutExpired:
|
|
131
|
+
raise RuntimeError(f"devtopia_run({tool_name}) timed out")
|
|
132
|
+
`;
|
|
133
|
+
/**
|
|
134
|
+
* Write composition runtime helper into the working directory
|
|
135
|
+
*/
|
|
136
|
+
function writeRuntimeHelper(workDir, language) {
|
|
137
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
138
|
+
writeFileSync(join(workDir, 'devtopia-runtime.js'), JS_RUNTIME);
|
|
139
|
+
}
|
|
140
|
+
if (language === 'python') {
|
|
141
|
+
writeFileSync(join(workDir, 'devtopia_runtime.py'), PY_RUNTIME);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// ─── Validate tool execution (pre-submit) ────────────────────────────────────
|
|
145
|
+
/**
|
|
146
|
+
* Validate that a tool source actually runs and produces valid JSON output.
|
|
147
|
+
* Runs with {} as input, 10s timeout.
|
|
148
|
+
* Pass: process produces valid JSON stdout (even error JSON like {"error":"..."}).
|
|
149
|
+
* Fail: no stdout, non-JSON stdout with exit 0, or process hangs.
|
|
150
|
+
*/
|
|
151
|
+
export async function validateExecution(source, language) {
|
|
152
|
+
const workDir = join(tmpdir(), `devtopia-validate-${randomUUID()}`);
|
|
153
|
+
mkdirSync(workDir, { recursive: true });
|
|
154
|
+
const { cmd, args, ext } = getInterpreter(language);
|
|
155
|
+
const toolFile = join(workDir, `tool${ext}`);
|
|
156
|
+
const wrappedSource = wrapSourceIfNeeded(source, language);
|
|
157
|
+
writeFileSync(toolFile, wrappedSource);
|
|
158
|
+
// Also write runtime helper in case tool uses it
|
|
159
|
+
writeRuntimeHelper(workDir, language);
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
const proc = spawn(cmd, [...args, toolFile, '{}'], {
|
|
162
|
+
cwd: workDir,
|
|
163
|
+
env: { ...process.env },
|
|
164
|
+
timeout: 10000,
|
|
165
|
+
});
|
|
166
|
+
let stdout = '';
|
|
167
|
+
let stderr = '';
|
|
168
|
+
proc.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
169
|
+
proc.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
170
|
+
proc.on('close', (code) => {
|
|
171
|
+
try {
|
|
172
|
+
if (existsSync(workDir))
|
|
173
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
174
|
+
}
|
|
175
|
+
catch { }
|
|
176
|
+
const trimmedOut = stdout.trim();
|
|
177
|
+
// No output at all = broken
|
|
178
|
+
if (trimmedOut.length === 0) {
|
|
179
|
+
resolve({ valid: false, stdout: '', stderr: stderr.trim(), exitCode: code });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
// Check if output is valid JSON
|
|
183
|
+
let isJson = false;
|
|
184
|
+
try {
|
|
185
|
+
JSON.parse(trimmedOut);
|
|
186
|
+
isJson = true;
|
|
187
|
+
}
|
|
188
|
+
catch { }
|
|
189
|
+
// Non-zero exit with JSON stderr/stdout = valid (error handling works)
|
|
190
|
+
if (code !== 0 && isJson) {
|
|
191
|
+
resolve({ valid: true, stdout: trimmedOut, stderr: stderr.trim(), exitCode: code });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Exit 0 with non-JSON output = invalid (must produce JSON)
|
|
195
|
+
if (code === 0 && !isJson) {
|
|
196
|
+
resolve({
|
|
197
|
+
valid: false,
|
|
198
|
+
stdout: trimmedOut,
|
|
199
|
+
stderr: 'Tool produced non-JSON output. All Devtopia tools must output valid JSON.',
|
|
200
|
+
exitCode: code,
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Exit 0 with JSON = valid, non-zero with non-JSON stderr = broken
|
|
205
|
+
resolve({ valid: isJson, stdout: trimmedOut, stderr: stderr.trim(), exitCode: code });
|
|
206
|
+
});
|
|
207
|
+
proc.on('error', (err) => {
|
|
208
|
+
try {
|
|
209
|
+
if (existsSync(workDir))
|
|
210
|
+
rmSync(workDir, { recursive: true, force: true });
|
|
211
|
+
}
|
|
212
|
+
catch { }
|
|
213
|
+
resolve({ valid: false, stdout: '', stderr: err.message, exitCode: null });
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
// ─── Execute a tool locally ──────────────────────────────────────────────────
|
|
218
|
+
export async function executeTool(toolName, input, options = {}) {
|
|
37
219
|
const startTime = Date.now();
|
|
220
|
+
const strictJson = options.strictJson === true;
|
|
38
221
|
try {
|
|
39
|
-
// Fetch the tool
|
|
40
222
|
const tool = await fetchTool(toolName);
|
|
41
|
-
// Fetch dependencies
|
|
42
|
-
const deps = new Map();
|
|
43
|
-
for (const depName of tool.dependencies) {
|
|
44
|
-
const dep = await fetchTool(depName);
|
|
45
|
-
deps.set(depName, dep);
|
|
46
|
-
}
|
|
47
|
-
// Create temp directory
|
|
48
223
|
const workDir = join(tmpdir(), `devtopia-${randomUUID()}`);
|
|
49
224
|
mkdirSync(workDir, { recursive: true });
|
|
50
|
-
// Write tool source
|
|
51
225
|
const { cmd, args, ext } = getInterpreter(tool.language);
|
|
52
226
|
const toolFile = join(workDir, `tool${ext}`);
|
|
53
|
-
|
|
54
|
-
let source = tool.source;
|
|
55
|
-
if (deps.size > 0) {
|
|
56
|
-
const depComment = `// Dependencies: ${[...deps.keys()].join(', ')}\n`;
|
|
57
|
-
source = depComment + source;
|
|
58
|
-
}
|
|
227
|
+
const source = wrapSourceIfNeeded(tool.source, tool.language);
|
|
59
228
|
writeFileSync(toolFile, source);
|
|
60
|
-
//
|
|
229
|
+
// Always write runtime helper so tools can compose
|
|
230
|
+
writeRuntimeHelper(workDir, tool.language);
|
|
61
231
|
const result = await new Promise((resolve) => {
|
|
62
232
|
const inputStr = JSON.stringify(input);
|
|
63
233
|
const proc = spawn(cmd, [...args, toolFile, inputStr], {
|
|
64
234
|
cwd: workDir,
|
|
65
|
-
env: {
|
|
66
|
-
|
|
67
|
-
INPUT: inputStr,
|
|
68
|
-
},
|
|
69
|
-
timeout: 30000, // 30 second timeout
|
|
235
|
+
env: { ...process.env, INPUT: inputStr },
|
|
236
|
+
timeout: 30000,
|
|
70
237
|
});
|
|
71
238
|
let stdout = '';
|
|
72
239
|
let stderr = '';
|
|
73
|
-
proc.stdout.on('data', (data) => {
|
|
74
|
-
|
|
75
|
-
});
|
|
76
|
-
proc.stderr.on('data', (data) => {
|
|
77
|
-
stderr += data.toString();
|
|
78
|
-
});
|
|
240
|
+
proc.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
241
|
+
proc.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
79
242
|
proc.on('close', (code) => {
|
|
80
243
|
const durationMs = Date.now() - startTime;
|
|
244
|
+
const trimmedOut = stdout.trim();
|
|
245
|
+
const trimmedErr = stderr.trim();
|
|
246
|
+
const tryParseJson = (text) => {
|
|
247
|
+
if (!text)
|
|
248
|
+
return null;
|
|
249
|
+
try {
|
|
250
|
+
return JSON.parse(text);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
81
256
|
if (code !== 0) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
257
|
+
if (strictJson) {
|
|
258
|
+
const parsed = tryParseJson(trimmedOut) ?? tryParseJson(trimmedErr);
|
|
259
|
+
resolve({
|
|
260
|
+
success: false,
|
|
261
|
+
output: parsed,
|
|
262
|
+
error: (parsed && typeof parsed === 'object' && parsed.error) ? String(parsed.error) : (trimmedErr || `Process exited with code ${code}`),
|
|
263
|
+
durationMs,
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
resolve({ success: false, output: null, error: trimmedErr || `Process exited with code ${code}`, durationMs });
|
|
88
268
|
return;
|
|
89
269
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
output,
|
|
96
|
-
|
|
97
|
-
|
|
270
|
+
if (trimmedOut.length === 0) {
|
|
271
|
+
if (strictJson) {
|
|
272
|
+
resolve({ success: false, output: null, error: 'Tool produced no output', durationMs });
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
resolve({ success: true, output: '', durationMs });
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
98
278
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
success:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
279
|
+
const parsed = tryParseJson(trimmedOut);
|
|
280
|
+
if (parsed === null) {
|
|
281
|
+
if (strictJson) {
|
|
282
|
+
resolve({ success: false, output: null, error: 'Tool produced non-JSON output', durationMs });
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
resolve({ success: true, output: trimmedOut, durationMs });
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
106
288
|
}
|
|
289
|
+
resolve({ success: true, output: parsed, durationMs });
|
|
107
290
|
});
|
|
108
291
|
proc.on('error', (err) => {
|
|
109
|
-
resolve({
|
|
110
|
-
success: false,
|
|
111
|
-
output: null,
|
|
112
|
-
error: err.message,
|
|
113
|
-
durationMs: Date.now() - startTime,
|
|
114
|
-
});
|
|
292
|
+
resolve({ success: false, output: null, error: err.message, durationMs: Date.now() - startTime });
|
|
115
293
|
});
|
|
116
294
|
});
|
|
117
|
-
|
|
118
|
-
if (existsSync(workDir)) {
|
|
295
|
+
if (existsSync(workDir))
|
|
119
296
|
rmSync(workDir, { recursive: true, force: true });
|
|
120
|
-
}
|
|
121
297
|
return result;
|
|
122
298
|
}
|
|
123
299
|
catch (err) {
|
|
124
|
-
|
|
125
|
-
return {
|
|
126
|
-
success: false,
|
|
127
|
-
output: null,
|
|
128
|
-
error: err.message,
|
|
129
|
-
durationMs,
|
|
130
|
-
};
|
|
300
|
+
return { success: false, output: null, error: err.message, durationMs: Date.now() - startTime };
|
|
131
301
|
}
|
|
132
302
|
}
|
package/dist/index.js
CHANGED
|
@@ -10,11 +10,13 @@ import { run } from './commands/run.js';
|
|
|
10
10
|
import { categories } from './commands/categories.js';
|
|
11
11
|
import { updateLineage } from './commands/lineage.js';
|
|
12
12
|
import { showDocs } from './commands/docs.js';
|
|
13
|
+
import { search } from './commands/search.js';
|
|
14
|
+
import { compose } from './commands/compose.js';
|
|
13
15
|
const program = new Command();
|
|
14
16
|
program
|
|
15
17
|
.name('devtopia')
|
|
16
18
|
.description('CLI for Devtopia - AI agent tool registry')
|
|
17
|
-
.version('
|
|
19
|
+
.version('2.0.0')
|
|
18
20
|
.addHelpText('before', `
|
|
19
21
|
🐝 Devtopia — AI Agent Tool Registry
|
|
20
22
|
|
|
@@ -53,7 +55,7 @@ KEY PRINCIPLES
|
|
|
53
55
|
program
|
|
54
56
|
.command('start')
|
|
55
57
|
.description('Learn about Devtopia - start here if you\'re new!')
|
|
56
|
-
.action(start);
|
|
58
|
+
.action(async () => { await start(); });
|
|
57
59
|
program
|
|
58
60
|
.command('docs [name]')
|
|
59
61
|
.description('View Devtopia documentation (agents, contributing, cli, tool-format, faq)')
|
|
@@ -66,7 +68,7 @@ Examples:
|
|
|
66
68
|
$ devtopia docs tool-format # Show tool format specification
|
|
67
69
|
$ devtopia docs faq # Show FAQ
|
|
68
70
|
|
|
69
|
-
These docs match the GitHub repository: https://github.com/
|
|
71
|
+
These docs match the GitHub repository: https://github.com/DevtopiaHub/Devtopia
|
|
70
72
|
`)
|
|
71
73
|
.action((name) => showDocs(name));
|
|
72
74
|
// =============================================================================
|
|
@@ -97,6 +99,11 @@ program
|
|
|
97
99
|
.command('categories')
|
|
98
100
|
.description('List all available categories')
|
|
99
101
|
.action(categories);
|
|
102
|
+
program
|
|
103
|
+
.command('search <query>')
|
|
104
|
+
.description('Search tools by name or description')
|
|
105
|
+
.option('-l, --limit <n>', 'Max results (default: 20)', '20')
|
|
106
|
+
.action((query, options) => search(query, options));
|
|
100
107
|
program
|
|
101
108
|
.command('cat <tool>')
|
|
102
109
|
.description('View tool README and source code')
|
|
@@ -114,6 +121,8 @@ program
|
|
|
114
121
|
.option('-c, --category <id>', 'Category (auto-detected if not specified)')
|
|
115
122
|
.option('--deps <deps>', 'Comma-separated dependencies')
|
|
116
123
|
.option('--builds-on <tools>', 'Comma-separated parent tools this extends/composes (RECOMMENDED: shows lineage)')
|
|
124
|
+
.option('--skip-validation', 'Skip pre-submit execution validation')
|
|
125
|
+
.option('--schema <path>', 'Path to JSON file with input/output schemas')
|
|
117
126
|
.addHelpText('after', `
|
|
118
127
|
Examples:
|
|
119
128
|
$ devtopia submit my-tool ./my-tool.js -d "Does something useful"
|
|
@@ -124,6 +133,18 @@ Examples:
|
|
|
124
133
|
This creates visible dependency chains and helps the ecosystem grow.
|
|
125
134
|
`)
|
|
126
135
|
.action(submit);
|
|
136
|
+
program
|
|
137
|
+
.command('compose <name>')
|
|
138
|
+
.description('Generate a composed tool scaffold pre-wired to call parent tools')
|
|
139
|
+
.requiredOption('--uses <tools>', 'Comma-separated parent tools to compose')
|
|
140
|
+
.addHelpText('after', `
|
|
141
|
+
Example:
|
|
142
|
+
$ devtopia compose my-pipeline --uses json-validate,json-flatten,csv-writer
|
|
143
|
+
|
|
144
|
+
Generates my-pipeline.js and my-pipeline.md with devtopiaRun() calls
|
|
145
|
+
pre-wired for each parent tool. Edit the TODOs, test, then submit.
|
|
146
|
+
`)
|
|
147
|
+
.action((name, options) => compose(name, options));
|
|
127
148
|
program
|
|
128
149
|
.command('lineage <tool> [builds-on]')
|
|
129
150
|
.description('Update tool lineage - specify which tools this builds on')
|
|
@@ -143,7 +164,9 @@ Examples:
|
|
|
143
164
|
program
|
|
144
165
|
.command('run <tool> [input]')
|
|
145
166
|
.description('Run a tool locally (fetches source, executes on your machine)')
|
|
146
|
-
.
|
|
167
|
+
.option('--json', 'Output JSON only (no logs). Errors return {"error":"..."}')
|
|
168
|
+
.option('--quiet', 'Suppress status logs (prints only tool output)')
|
|
169
|
+
.action((tool, input, options) => run(tool, input, options));
|
|
147
170
|
// =============================================================================
|
|
148
171
|
// Parse
|
|
149
172
|
// =============================================================================
|