@0xgeorgemathew/zcode 0.3.2 → 0.4.1
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/README.md +184 -138
- package/bin/zcode.js +181 -140
- package/lib/appserver.js +190 -135
- package/lib/bootstrap.js +39 -60
- package/lib/checks.js +103 -0
- package/lib/commands/doctor.js +43 -64
- package/lib/commands/exec.js +327 -0
- package/lib/commands/login.js +2 -9
- package/lib/commands/mcp-list.js +6 -14
- package/lib/commands/sessions.js +181 -0
- package/lib/contract.js +301 -0
- package/lib/executor.js +617 -0
- package/lib/install-skills.js +54 -0
- package/lib/isolation.js +233 -223
- package/lib/key.js +1 -10
- package/lib/store.js +276 -0
- package/lib/ui.js +200 -71
- package/lib/workspace.js +336 -0
- package/package.json +22 -7
- package/skills/README.md +34 -0
- package/skills/claude-code/zcode/SKILL.md +112 -0
- package/skills/codex/zcode/SKILL.md +114 -0
- package/skills/codex/zcode/agents/openai.yaml +4 -0
- package/lib/commands/agent.js +0 -203
- package/lib/commands/run.js +0 -126
- package/lib/overlay.js +0 -138
package/bin/zcode.js
CHANGED
|
@@ -1,108 +1,130 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
// zcode — CLI entry: argv parsing + command dispatch
|
|
3
|
+
// zcode — CLI entry: argv parsing + command dispatch. Parsing rejects bad
|
|
4
|
+
// input (exit 2) before any engine spawn or session record exists.
|
|
4
5
|
const { normalizeModel, unknownModelMessage, normalizeEffort, EFFORT_LEVELS, DEFAULT_MODEL } = require('../lib/models.js');
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
--
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
--
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
--
|
|
34
|
-
--
|
|
35
|
-
--
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
--
|
|
39
|
-
--
|
|
40
|
-
--
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
--
|
|
44
|
-
--
|
|
45
|
-
-
|
|
46
|
-
--
|
|
7
|
+
const USAGE = `usage:
|
|
8
|
+
zcode task [options] <objective…> execute a task contract, verify it, hand back
|
|
9
|
+
zcode [run] [options] <prompt…> one-shot turn (prompt via stdin when piped)
|
|
10
|
+
zcode agent [options] <instructions…> turn that MUST dispatch a subagent
|
|
11
|
+
zcode resume <id> [message…] continue a session (answer, follow-up, retry)
|
|
12
|
+
zcode status [<id>] [--json] one session in detail, or all sessions
|
|
13
|
+
zcode wait <id…> [--json] block until sessions finish; print handbacks
|
|
14
|
+
zcode cancel <id> [--json] stop a running session
|
|
15
|
+
zcode login read API key from stdin → credentials
|
|
16
|
+
zcode doctor [--json] engine + isolation + key report
|
|
17
|
+
|
|
18
|
+
session:
|
|
19
|
+
--id <id> name the session (letters, digits, . _ -); default zc-<random>.
|
|
20
|
+
An existing id is never re-run: use resume.
|
|
21
|
+
--label <text> free-form label shown in status
|
|
22
|
+
--detach start in the background, print the id, exit 0
|
|
23
|
+
--cwd <dir> workspace (default: current dir)
|
|
24
|
+
--owns <path|glob> repeatable: paths this session may change (relative to
|
|
25
|
+
--cwd). Default: the whole workspace. Overlapping
|
|
26
|
+
ownership with a running session is refused (exit 7)
|
|
27
|
+
--allow-overlap start despite overlapping ownership
|
|
28
|
+
|
|
29
|
+
task contract (task only):
|
|
30
|
+
--task <file|-> JSON contract or markdown brief (- = stdin)
|
|
31
|
+
--accept <text> repeatable acceptance criterion
|
|
32
|
+
--check <command> repeatable shell check zcode runs after the work
|
|
33
|
+
--file <path> repeatable relevant file to read first
|
|
34
|
+
--context <text|@f> repeatable extra context (design decisions, references)
|
|
35
|
+
--max-repairs <n> repair turns after failing checks (default 2)
|
|
36
|
+
--check-timeout-ms <n> per-check timeout (default 300000)
|
|
37
|
+
|
|
38
|
+
execution:
|
|
39
|
+
--model <id> glm-5.3 | glm-5.3-flash (+aliases). default: glm-5.3-flash
|
|
40
|
+
--effort <level> low | medium | high | xhigh | max. default: max
|
|
41
|
+
--timeout-ms <n> whole invocation incl. checks/repairs (default 600000;
|
|
42
|
+
task default 1800000)
|
|
43
|
+
--profile <name> subagent profile (agent command; default general-purpose)
|
|
44
|
+
--mcp <json|@f> add MCP servers (repeatable); hermetic by default
|
|
45
|
+
--mcp-inherit also inherit the desktop's enabled MCP servers
|
|
46
|
+
--mcp-list print the effective MCP server set as JSON and exit
|
|
47
|
+
--skills <dir> add skills (repeatable): one skill dir or a root of them
|
|
48
|
+
--no-skills empty skill set
|
|
49
|
+
|
|
50
|
+
output:
|
|
51
|
+
--json NDJSON events on stdout; final line {"type":"result",…}
|
|
52
|
+
--quiet no progress: no stderr feed; with --json only the result line
|
|
53
|
+
--thinking / --no-thinking mirror reasoning to stderr (run: on, task: off)
|
|
54
|
+
--stream task --json: include text/reasoning deltas in the events
|
|
55
|
+
--include-reasoning full reasoning text in the result
|
|
56
|
+
-p, --prompt <t> prompt text (alias for positional prompt)
|
|
57
|
+
--session <id> run: continue a session (zcode id or engine sess_ id)
|
|
58
|
+
--version, --help
|
|
59
|
+
|
|
60
|
+
exit codes: 0 completed · 2 usage · 3 no key · 4 no engine · 5 failed ·
|
|
61
|
+
6 agent: no subagent · 7 workspace conflict / session busy · 8 partial or
|
|
62
|
+
checks failing · 9 unverified · 10 blocked · 124 timeout · 130 cancelled`;
|
|
47
63
|
|
|
48
64
|
function fail(code, message) {
|
|
49
65
|
process.stderr.write(`zcode: ${message}\n`);
|
|
50
66
|
process.exit(code);
|
|
51
67
|
}
|
|
52
68
|
|
|
53
|
-
function
|
|
69
|
+
function readFileOrFail(file, flag) {
|
|
54
70
|
try {
|
|
55
|
-
return require('node:fs').
|
|
71
|
+
return require('node:fs').readFileSync(file, 'utf8');
|
|
56
72
|
} catch {
|
|
57
|
-
return
|
|
73
|
+
return fail(2, `${flag} file "${file}" could not be read.`);
|
|
58
74
|
}
|
|
59
75
|
}
|
|
60
76
|
|
|
61
|
-
// --mcp accepts inline JSON or @file
|
|
62
|
-
// definition objects ({type:"stdio"|"http"|"sse", ...} per the engine's
|
|
63
|
-
// config schema). Fails (exit 2) before any engine spawn.
|
|
77
|
+
// --mcp accepts inline JSON or @file mapping server names to definitions.
|
|
64
78
|
function parseMcpSpec(value) {
|
|
65
|
-
|
|
66
|
-
if (value.startsWith('@')) {
|
|
67
|
-
try {
|
|
68
|
-
text = require('node:fs').readFileSync(value.slice(1), 'utf8');
|
|
69
|
-
} catch {
|
|
70
|
-
fail(EXIT.USAGE, `--mcp file "${value.slice(1)}" could not be read.`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
79
|
+
const text = value.startsWith('@') ? readFileOrFail(value.slice(1), '--mcp') : value;
|
|
73
80
|
let parsed;
|
|
74
81
|
try {
|
|
75
82
|
parsed = JSON.parse(text);
|
|
76
83
|
} catch (err) {
|
|
77
|
-
fail(
|
|
84
|
+
fail(2, `--mcp spec must be valid JSON mapping server names to definitions: ${err.message}`);
|
|
78
85
|
}
|
|
79
86
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
80
|
-
fail(
|
|
87
|
+
fail(2, `--mcp spec must be valid JSON mapping server names to definitions, got ${Array.isArray(parsed) ? 'an array' : `a ${typeof parsed}`}.`);
|
|
81
88
|
}
|
|
82
89
|
for (const [name, def] of Object.entries(parsed)) {
|
|
83
90
|
if (!def || typeof def !== 'object' || Array.isArray(def)) {
|
|
84
|
-
fail(
|
|
91
|
+
fail(2, `--mcp server "${name}" must be an object (e.g. {"type":"stdio","command":...,"args":[...]}).`);
|
|
85
92
|
}
|
|
86
93
|
}
|
|
87
94
|
return parsed;
|
|
88
95
|
}
|
|
89
96
|
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
const COMMANDS = new Set(['run', 'task', 'agent', 'resume', 'status', 'list', 'wait', 'cancel', 'login', 'doctor', '__exec']);
|
|
98
|
+
const TASK_ONLY = new Set(['task', 'accept', 'check', 'file', 'context', 'maxRepairs', 'checkTimeoutMs']);
|
|
99
|
+
const START_ONLY = new Set(['id', 'label', 'owns', 'allowOverlap', 'mcpSpecs', 'mcpInherit', 'skillsDirs', 'noSkills', 'profile', 'cwd']);
|
|
100
|
+
|
|
92
101
|
function parseArgv(argv) {
|
|
93
102
|
const out = {
|
|
94
103
|
command: 'run',
|
|
95
104
|
prompts: [],
|
|
105
|
+
given: new Set(),
|
|
96
106
|
flags: {
|
|
97
107
|
model: DEFAULT_MODEL,
|
|
98
108
|
effort: 'max',
|
|
99
109
|
cwd: process.cwd(),
|
|
100
110
|
json: false,
|
|
101
111
|
fresh: false,
|
|
102
|
-
timeoutMs:
|
|
112
|
+
timeoutMs: undefined,
|
|
103
113
|
profile: 'general-purpose',
|
|
104
114
|
prompt: undefined,
|
|
105
115
|
session: undefined,
|
|
116
|
+
id: undefined,
|
|
117
|
+
label: undefined,
|
|
118
|
+
detach: false,
|
|
119
|
+
task: undefined,
|
|
120
|
+
accept: [],
|
|
121
|
+
check: [],
|
|
122
|
+
file: [],
|
|
123
|
+
context: [],
|
|
124
|
+
owns: [],
|
|
125
|
+
allowOverlap: false,
|
|
126
|
+
maxRepairs: undefined,
|
|
127
|
+
checkTimeoutMs: undefined,
|
|
106
128
|
mcpSpecs: [],
|
|
107
129
|
mcpInherit: false,
|
|
108
130
|
mcpList: false,
|
|
@@ -110,130 +132,149 @@ function parseArgv(argv) {
|
|
|
110
132
|
noSkills: false,
|
|
111
133
|
includeReasoning: false,
|
|
112
134
|
quiet: false,
|
|
135
|
+
thinking: undefined,
|
|
136
|
+
stream: false,
|
|
113
137
|
},
|
|
114
138
|
};
|
|
115
139
|
const valueFlags = new Map([
|
|
116
|
-
['--model', 'model'],
|
|
117
|
-
['--
|
|
118
|
-
['--
|
|
119
|
-
['--
|
|
120
|
-
['--timeout-ms', 'timeoutMs'],
|
|
121
|
-
['-p', 'prompt'],
|
|
122
|
-
['--prompt', 'prompt'],
|
|
123
|
-
['--session', 'session'],
|
|
140
|
+
['--model', 'model'], ['--effort', 'effort'], ['--cwd', 'cwd'], ['--profile', 'profile'],
|
|
141
|
+
['--timeout-ms', 'timeoutMs'], ['-p', 'prompt'], ['--prompt', 'prompt'], ['--session', 'session'],
|
|
142
|
+
['--id', 'id'], ['--label', 'label'], ['--task', 'task'], ['--max-repairs', 'maxRepairs'],
|
|
143
|
+
['--check-timeout-ms', 'checkTimeoutMs'],
|
|
124
144
|
]);
|
|
125
145
|
const listFlags = new Map([
|
|
126
|
-
['--mcp', 'mcpSpecs'],
|
|
127
|
-
['--
|
|
146
|
+
['--mcp', 'mcpSpecs'], ['--skills', 'skillsDirs'], ['--accept', 'accept'], ['--check', 'check'],
|
|
147
|
+
['--file', 'file'], ['--context', 'context'], ['--owns', 'owns'],
|
|
128
148
|
]);
|
|
129
149
|
const boolFlags = new Map([
|
|
130
|
-
['--json', 'json'],
|
|
131
|
-
['--
|
|
132
|
-
['--
|
|
133
|
-
['--
|
|
134
|
-
['--no-
|
|
135
|
-
['--include-reasoning', 'includeReasoning'],
|
|
136
|
-
['--quiet', 'quiet'],
|
|
150
|
+
['--json', ['json', true]], ['--fresh', ['fresh', true]], ['--mcp-inherit', ['mcpInherit', true]],
|
|
151
|
+
['--mcp-list', ['mcpList', true]], ['--no-skills', ['noSkills', true]],
|
|
152
|
+
['--include-reasoning', ['includeReasoning', true]], ['--quiet', ['quiet', true]],
|
|
153
|
+
['--detach', ['detach', true]], ['--allow-overlap', ['allowOverlap', true]],
|
|
154
|
+
['--thinking', ['thinking', true]], ['--no-thinking', ['thinking', false]], ['--stream', ['stream', true]],
|
|
137
155
|
]);
|
|
138
|
-
const
|
|
156
|
+
const numeric = (flag, value, { min = 1 } = {}) => {
|
|
157
|
+
const n = Number(value);
|
|
158
|
+
if (!Number.isFinite(n) || n < min) fail(2, `${flag} must be a number >= ${min}, got "${value}".`);
|
|
159
|
+
return n;
|
|
160
|
+
};
|
|
139
161
|
|
|
140
162
|
for (let i = 0; i < argv.length; i++) {
|
|
141
163
|
const arg = argv[i];
|
|
142
|
-
if (valueFlags.has(arg) ||
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
} else {
|
|
165
|
-
if (!isDirectory(value)) fail(EXIT.USAGE, `--skills requires an existing directory, got "${value}".`);
|
|
166
|
-
out.flags.skillsDirs.push(value);
|
|
167
|
-
}
|
|
168
|
-
} else {
|
|
169
|
-
key = boolFlags.get(arg);
|
|
170
|
-
out.flags[key] = true;
|
|
171
|
-
}
|
|
164
|
+
if (valueFlags.has(arg) || listFlags.has(arg)) {
|
|
165
|
+
const key = valueFlags.get(arg) || listFlags.get(arg);
|
|
166
|
+
const value = argv[++i];
|
|
167
|
+
if (value === undefined) fail(2, `option ${arg} requires a value.\n${USAGE}`);
|
|
168
|
+
out.given.add(key);
|
|
169
|
+
if (key === 'timeoutMs' || key === 'checkTimeoutMs') out.flags[key] = numeric(arg, value);
|
|
170
|
+
else if (key === 'maxRepairs') out.flags[key] = numeric(arg, value, { min: 0 });
|
|
171
|
+
else if (key === 'mcpSpecs') out.flags.mcpSpecs.push(parseMcpSpec(value));
|
|
172
|
+
else if (key === 'skillsDirs') {
|
|
173
|
+
if (!isDirectory(value)) fail(2, `--skills requires an existing directory, got "${value}".`);
|
|
174
|
+
out.flags.skillsDirs.push(value);
|
|
175
|
+
} else if (key === 'context') {
|
|
176
|
+
out.flags.context.push(value.startsWith('@') ? readFileOrFail(value.slice(1), '--context') : value);
|
|
177
|
+
} else if (listFlags.has(arg)) out.flags[key].push(value);
|
|
178
|
+
else if (!value.trim()) fail(2, `${arg} requires a non-empty value.`);
|
|
179
|
+
else out.flags[key] = value.trim();
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (boolFlags.has(arg)) {
|
|
183
|
+
const [key, v] = boolFlags.get(arg);
|
|
184
|
+
out.given.add(key);
|
|
185
|
+
out.flags[key] = v;
|
|
172
186
|
continue;
|
|
173
187
|
}
|
|
174
188
|
if (arg === '--help' || arg === '-h') {
|
|
175
189
|
process.stdout.write(USAGE + '\n');
|
|
176
|
-
process.exit(
|
|
190
|
+
process.exit(0);
|
|
177
191
|
}
|
|
178
192
|
if (arg === '--version') {
|
|
179
193
|
process.stdout.write(require('../package.json').version + '\n');
|
|
180
|
-
process.exit(
|
|
194
|
+
process.exit(0);
|
|
181
195
|
}
|
|
182
196
|
if (arg === '--') {
|
|
183
197
|
out.prompts.push(...argv.slice(i + 1));
|
|
184
198
|
break;
|
|
185
199
|
}
|
|
186
|
-
if (arg.startsWith('--') && arg.length > 2) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
out.command = arg;
|
|
200
|
+
if (arg.startsWith('--') && arg.length > 2) fail(2, `unknown option "${arg}".\n${USAGE}`);
|
|
201
|
+
if (out.prompts.length === 0 && out.command === 'run' && !out.commandSet && COMMANDS.has(arg)) {
|
|
202
|
+
out.command = arg === 'list' ? 'status' : arg;
|
|
203
|
+
out.commandSet = true;
|
|
191
204
|
continue;
|
|
192
205
|
}
|
|
193
206
|
out.prompts.push(arg);
|
|
194
207
|
}
|
|
195
208
|
|
|
196
|
-
|
|
197
|
-
|
|
209
|
+
const cmd = out.command;
|
|
210
|
+
if (out.flags.prompt !== undefined) out.prompts.unshift(out.flags.prompt);
|
|
211
|
+
if (cmd !== 'task') {
|
|
212
|
+
for (const key of TASK_ONLY) {
|
|
213
|
+
if (out.given.has(key) && !(cmd === 'resume' && (key === 'maxRepairs' || key === 'checkTimeoutMs'))) {
|
|
214
|
+
fail(2, `--${flagName(key)} is only valid with "zcode task".`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
198
217
|
}
|
|
218
|
+
if (cmd === 'resume' || cmd === '__exec') {
|
|
219
|
+
for (const key of START_ONLY) {
|
|
220
|
+
if (out.given.has(key)) fail(2, `--${flagName(key)} cannot change on resume; it is fixed when the session starts.`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (out.given.has('session') && cmd !== 'run') fail(2, '--session is only valid with "zcode run"; use "zcode resume <id>".');
|
|
224
|
+
if (out.given.has('id') && !require('../lib/store.js').validId(out.flags.id)) {
|
|
225
|
+
fail(2, `--id "${out.flags.id}" must be 1-64 chars of letters, digits, ".", "_" or "-", starting with a letter or digit.`);
|
|
226
|
+
}
|
|
227
|
+
if (!isDirectory(out.flags.cwd)) fail(2, `--cwd must be an existing directory, got "${out.flags.cwd}".`);
|
|
199
228
|
|
|
200
229
|
const model = normalizeModel(out.flags.model);
|
|
201
|
-
if (!model) fail(
|
|
230
|
+
if (!model) fail(2, unknownModelMessage(out.flags.model));
|
|
202
231
|
out.flags.model = model;
|
|
203
|
-
|
|
204
232
|
const effort = normalizeEffort(out.flags.effort);
|
|
205
|
-
if (!effort) {
|
|
206
|
-
fail(
|
|
207
|
-
EXIT.USAGE,
|
|
208
|
-
`unknown effort "${out.flags.effort}". --effort accepts ${EFFORT_LEVELS.join(' | ')}.`
|
|
209
|
-
);
|
|
210
|
-
}
|
|
233
|
+
if (!effort) fail(2, `unknown effort "${out.flags.effort}". --effort accepts ${EFFORT_LEVELS.join(' | ')}.`);
|
|
211
234
|
out.flags.effort = effort;
|
|
212
235
|
return out;
|
|
213
236
|
}
|
|
214
237
|
|
|
238
|
+
function flagName(key) {
|
|
239
|
+
return key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`).replace(/^mcp-specs$/, 'mcp').replace(/^skills-dirs$/, 'skills');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isDirectory(p) {
|
|
243
|
+
try {
|
|
244
|
+
return require('node:fs').statSync(p).isDirectory();
|
|
245
|
+
} catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
215
250
|
async function main() {
|
|
216
251
|
const parsed = parseArgv(process.argv.slice(2));
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return require('../lib/commands/mcp-list.js')(parsed);
|
|
220
|
-
}
|
|
221
|
-
const run = require('../lib/commands/run.js');
|
|
222
|
-
switch (command) {
|
|
252
|
+
if (parsed.flags.mcpList) return require('../lib/commands/mcp-list.js')(parsed);
|
|
253
|
+
switch (parsed.command) {
|
|
223
254
|
case 'run':
|
|
224
|
-
|
|
255
|
+
case 'task':
|
|
225
256
|
case 'agent':
|
|
226
|
-
|
|
257
|
+
case 'resume':
|
|
258
|
+
case '__exec':
|
|
259
|
+
return require('../lib/commands/exec.js')(parsed);
|
|
260
|
+
case 'status':
|
|
261
|
+
case 'wait':
|
|
262
|
+
case 'cancel':
|
|
263
|
+
return require('../lib/commands/sessions.js')(parsed);
|
|
227
264
|
case 'login':
|
|
228
265
|
return require('../lib/commands/login.js')(parsed);
|
|
229
266
|
case 'doctor':
|
|
230
267
|
return require('../lib/commands/doctor.js')(parsed);
|
|
231
268
|
default:
|
|
232
|
-
fail(
|
|
269
|
+
return fail(2, `unknown command "${parsed.command}".\n${USAGE}`);
|
|
233
270
|
}
|
|
234
271
|
}
|
|
235
272
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
273
|
+
if (require.main === module) {
|
|
274
|
+
main().catch((err) => {
|
|
275
|
+
process.stderr.write(`zcode: internal error: ${err && err.stack ? err.stack : err}\n`);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = { parseArgv, USAGE };
|