@love-moon/conductor-cli 0.2.42 → 0.3.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/bin/conductor-fire.js +21 -3
- package/bin/conductor-issue.js +357 -0
- package/bin/conductor-project.js +436 -0
- package/bin/conductor-task.js +285 -0
- package/bin/conductor.js +25 -1
- package/package.json +9 -4
- package/src/ai-manager-handlers.js +17 -1
- package/src/daemon.js +795 -35
- package/src/entity-helpers.js +345 -0
- package/src/fire/resume.js +113 -870
- package/src/runtime-backends.js +48 -8
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* conductor project — entity-oriented project management.
|
|
5
|
+
*
|
|
6
|
+
* Subcommands:
|
|
7
|
+
* list [--include-hidden]
|
|
8
|
+
* show [<id|name>]
|
|
9
|
+
* current
|
|
10
|
+
* create [--name <n>] [--workspace-path <p>] [--daemon-host <h>] [--default] [--client-request-id <key>]
|
|
11
|
+
* set-default <id|name>
|
|
12
|
+
* hide <id|name>
|
|
13
|
+
* unhide <id|name>
|
|
14
|
+
*
|
|
15
|
+
* Global flags supported on every write subcommand:
|
|
16
|
+
* --json, --dry-run, --project, --config-file
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import process from "node:process";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
|
|
23
|
+
import yargs from "yargs/yargs";
|
|
24
|
+
import { hideBin } from "yargs/helpers";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
EXIT,
|
|
28
|
+
buildApis,
|
|
29
|
+
buildAuditMetadata,
|
|
30
|
+
emitDryRun,
|
|
31
|
+
exitCodeForError,
|
|
32
|
+
makeDryRunPayload,
|
|
33
|
+
printJson,
|
|
34
|
+
printPretty,
|
|
35
|
+
pad,
|
|
36
|
+
reportError,
|
|
37
|
+
resolveProject,
|
|
38
|
+
} from "../src/entity-helpers.js";
|
|
39
|
+
|
|
40
|
+
const isMainModule = (() => {
|
|
41
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
42
|
+
const entryFile = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
43
|
+
return entryFile === currentFile;
|
|
44
|
+
})();
|
|
45
|
+
|
|
46
|
+
function buildBaseUrl(config) {
|
|
47
|
+
const raw = (config?.backendUrl || "").replace(/\/+$/, "");
|
|
48
|
+
return raw || "http://localhost";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function resolveProjectSelector(apis, selector, options = {}) {
|
|
52
|
+
if (!selector) {
|
|
53
|
+
return resolveProject(apis, options);
|
|
54
|
+
}
|
|
55
|
+
const daemonHostFilter = options.daemonHost
|
|
56
|
+
? String(options.daemonHost).trim()
|
|
57
|
+
: null;
|
|
58
|
+
|
|
59
|
+
// Pull the project list once and do all matching client-side. We
|
|
60
|
+
// deliberately do NOT route through `ProjectsApi.getProject` because the
|
|
61
|
+
// SDK's variant transparently falls back from "id miss" to "unique name
|
|
62
|
+
// match" — which collapses two semantically distinct cases (id miss vs.
|
|
63
|
+
// ambiguous name) into one error, and prevents us from surfacing the
|
|
64
|
+
// candidate list. Doing it locally keeps that distinction.
|
|
65
|
+
const list = await apis.projects.listProjects({ includeHidden: true });
|
|
66
|
+
|
|
67
|
+
const byId = list.find((entry) => entry.id === selector);
|
|
68
|
+
if (byId) {
|
|
69
|
+
if (daemonHostFilter && byId.daemonHost && byId.daemonHost !== daemonHostFilter) {
|
|
70
|
+
const err = new Error(
|
|
71
|
+
`Project ${byId.id} is on daemon '${byId.daemonHost}', not '${daemonHostFilter}'`,
|
|
72
|
+
);
|
|
73
|
+
err.code = "ARGS";
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
return byId;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const nameMatches = list.filter((entry) => entry.name === selector);
|
|
80
|
+
const matches = daemonHostFilter
|
|
81
|
+
? nameMatches.filter((entry) => entry.daemonHost === daemonHostFilter)
|
|
82
|
+
: nameMatches;
|
|
83
|
+
if (matches.length === 0) {
|
|
84
|
+
const hint = daemonHostFilter
|
|
85
|
+
? ` (no match on daemon '${daemonHostFilter}')`
|
|
86
|
+
: "";
|
|
87
|
+
const err = new Error(`No project found matching '${selector}'${hint}`);
|
|
88
|
+
err.statusCode = 404;
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
if (matches.length > 1) {
|
|
92
|
+
// Surface candidates inline so the caller can copy-paste the right id
|
|
93
|
+
// without a second `conductor project list` round-trip (multi-daemon UX
|
|
94
|
+
// follow-up to RFC 0025).
|
|
95
|
+
const candidates = nameMatches
|
|
96
|
+
.map((entry) => ` ${entry.id} daemon=${entry.daemonHost ?? "(none)"} ${entry.workspacePath ?? ""}`.trimEnd())
|
|
97
|
+
.join("\n");
|
|
98
|
+
const err = new Error(
|
|
99
|
+
`Project name '${selector}' is ambiguous (${matches.length} matches). Pass --project <id> or --daemon-host <host> to disambiguate:\n${candidates}`,
|
|
100
|
+
);
|
|
101
|
+
err.code = "ARGS";
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
return matches[0];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function projectAsObject(p) {
|
|
108
|
+
if (!p) return null;
|
|
109
|
+
if (typeof p.asObject === "function") return p.asObject();
|
|
110
|
+
return {
|
|
111
|
+
id: p.id,
|
|
112
|
+
name: p.name,
|
|
113
|
+
daemonHost: p.daemonHost,
|
|
114
|
+
workspacePath: p.workspacePath,
|
|
115
|
+
isDefault: p.isDefault,
|
|
116
|
+
hidden: p.hidden ?? Boolean(p.hiddenAt),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function handleList(argv, deps) {
|
|
121
|
+
const apis = await buildApis(deps);
|
|
122
|
+
const list = await apis.projects.listProjects({ includeHidden: Boolean(argv.includeHidden) });
|
|
123
|
+
const objects = list.map(projectAsObject);
|
|
124
|
+
if (argv.json) {
|
|
125
|
+
printJson(deps.stdout, objects);
|
|
126
|
+
return EXIT.OK;
|
|
127
|
+
}
|
|
128
|
+
if (objects.length === 0) {
|
|
129
|
+
printPretty(deps.stdout, "(no projects)");
|
|
130
|
+
return EXIT.OK;
|
|
131
|
+
}
|
|
132
|
+
// DAEMON column width sized for typical hostnames (e.g. "4090", "m1",
|
|
133
|
+
// "macbook-pro"). Longer hosts overflow rather than truncate — readability
|
|
134
|
+
// for the common case beats column alignment for outliers.
|
|
135
|
+
const daemonColWidth = 14;
|
|
136
|
+
printPretty(
|
|
137
|
+
deps.stdout,
|
|
138
|
+
`${pad("ID", 24)} ${pad("DEFAULT", 8)} ${pad("HIDDEN", 7)} ${pad("DAEMON", daemonColWidth)} NAME`,
|
|
139
|
+
);
|
|
140
|
+
for (const p of objects) {
|
|
141
|
+
printPretty(
|
|
142
|
+
deps.stdout,
|
|
143
|
+
`${pad(p.id, 24)} ${pad(p.isDefault ? "yes" : "", 8)} ${pad(p.hidden ? "yes" : "", 7)} ${pad(p.daemonHost ?? "", daemonColWidth)} ${p.name ?? ""}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return EXIT.OK;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function handleShow(argv, deps) {
|
|
150
|
+
const apis = await buildApis(deps);
|
|
151
|
+
const selector = argv.idOrName ?? argv.project;
|
|
152
|
+
const project = await resolveProjectSelector(apis, selector, {
|
|
153
|
+
env: deps.env,
|
|
154
|
+
cwd: deps.cwd,
|
|
155
|
+
daemonHost: argv.daemonHost,
|
|
156
|
+
});
|
|
157
|
+
const obj = projectAsObject(project);
|
|
158
|
+
if (argv.json) {
|
|
159
|
+
printJson(deps.stdout, obj);
|
|
160
|
+
return EXIT.OK;
|
|
161
|
+
}
|
|
162
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
163
|
+
if (value === undefined || value === null) continue;
|
|
164
|
+
printPretty(deps.stdout, `${key}: ${value}`);
|
|
165
|
+
}
|
|
166
|
+
return EXIT.OK;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function handleCurrent(argv, deps) {
|
|
170
|
+
const apis = await buildApis(deps);
|
|
171
|
+
const project = await resolveProject(apis, { env: deps.env, cwd: deps.cwd, project: argv.project });
|
|
172
|
+
const obj = projectAsObject(project);
|
|
173
|
+
if (argv.json) {
|
|
174
|
+
printJson(deps.stdout, obj);
|
|
175
|
+
return EXIT.OK;
|
|
176
|
+
}
|
|
177
|
+
// Just the id, suitable for shell substitution.
|
|
178
|
+
deps.stdout.write(`${obj.id}\n`);
|
|
179
|
+
return EXIT.OK;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function handleCreate(argv, deps) {
|
|
183
|
+
const apis = await buildApis(deps);
|
|
184
|
+
const config = apis.config;
|
|
185
|
+
const env = deps.env;
|
|
186
|
+
|
|
187
|
+
const cwd = deps.cwd;
|
|
188
|
+
const workspacePath = argv.workspacePath
|
|
189
|
+
? path.resolve(argv.workspacePath)
|
|
190
|
+
: cwd;
|
|
191
|
+
const defaultName = argv.name && String(argv.name).trim()
|
|
192
|
+
? String(argv.name).trim()
|
|
193
|
+
: (path.basename(workspacePath) || undefined);
|
|
194
|
+
const daemonHost = argv.daemonHost ? String(argv.daemonHost) : undefined;
|
|
195
|
+
const isDefault = Boolean(argv.default);
|
|
196
|
+
const metadata = buildAuditMetadata(env);
|
|
197
|
+
if (argv.clientRequestId) {
|
|
198
|
+
metadata.clientRequestId = String(argv.clientRequestId);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const body = isDefault
|
|
202
|
+
? { isDefault: true, metadata }
|
|
203
|
+
: {
|
|
204
|
+
...(defaultName ? { name: defaultName } : {}),
|
|
205
|
+
...(workspacePath ? { workspacePath } : {}),
|
|
206
|
+
...(daemonHost ? { daemonHost } : {}),
|
|
207
|
+
metadata,
|
|
208
|
+
...(argv.clientRequestId ? { clientRequestId: String(argv.clientRequestId) } : {}),
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
if (argv.dryRun) {
|
|
212
|
+
emitDryRun(
|
|
213
|
+
deps.stdout,
|
|
214
|
+
argv.json,
|
|
215
|
+
makeDryRunPayload("POST", `${buildBaseUrl(config)}/api/projects`, body),
|
|
216
|
+
);
|
|
217
|
+
return EXIT.OK;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!isDefault) {
|
|
221
|
+
if (!daemonHost && !config?.daemonName) {
|
|
222
|
+
// Service-side will error when daemon is not reachable; we surface a
|
|
223
|
+
// clearer hint here per "Decisions on leftover RFC questions" §1.
|
|
224
|
+
// We don't proactively probe — but we *do* improve the wording when
|
|
225
|
+
// the server returns "daemon not reachable".
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let created;
|
|
230
|
+
try {
|
|
231
|
+
created = await apis.projects.createProject(body);
|
|
232
|
+
} catch (err) {
|
|
233
|
+
if (isDaemonUnreachableError(err)) {
|
|
234
|
+
const host = daemonHost || "(local daemon)";
|
|
235
|
+
const friendly = new Error(
|
|
236
|
+
`Daemon at ${host} not reachable. Start one with \`conductor daemon\` or pass --daemon-host <h> to use an existing one.`,
|
|
237
|
+
);
|
|
238
|
+
friendly.statusCode = err.statusCode;
|
|
239
|
+
throw friendly;
|
|
240
|
+
}
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
const obj = projectAsObject(created);
|
|
244
|
+
if (argv.json) {
|
|
245
|
+
printJson(deps.stdout, obj);
|
|
246
|
+
return EXIT.OK;
|
|
247
|
+
}
|
|
248
|
+
printPretty(deps.stdout, `Created project ${obj.name ?? "(unnamed)"} (${obj.id})`);
|
|
249
|
+
return EXIT.OK;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function isDaemonUnreachableError(err) {
|
|
253
|
+
if (!err) return false;
|
|
254
|
+
const message = err.message || "";
|
|
255
|
+
if (/daemon/i.test(message) && /(not reachable|unreachable|cannot reach|connection refused|ECONN)/i.test(message)) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
const details = err.details;
|
|
259
|
+
if (details && typeof details === "object") {
|
|
260
|
+
const detailMessage = String(details.error || details.message || "");
|
|
261
|
+
if (/daemon/i.test(detailMessage) && /(not reachable|unreachable|cannot reach)/i.test(detailMessage)) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function handleSetDefault(argv, deps) {
|
|
269
|
+
const apis = await buildApis(deps);
|
|
270
|
+
const project = await resolveProjectSelector(apis, argv.idOrName, {
|
|
271
|
+
env: deps.env,
|
|
272
|
+
cwd: deps.cwd,
|
|
273
|
+
daemonHost: argv.daemonHost,
|
|
274
|
+
});
|
|
275
|
+
// The matching server endpoint is `POST /api/projects/default` with body
|
|
276
|
+
// `{ projectId, metadata }` (see web/src/app/api/projects/default/route.ts).
|
|
277
|
+
// Earlier the dry-run preview pointed at the PATCH-by-query route, which
|
|
278
|
+
// would have misled an AI agent inspecting the dry-run (review B3).
|
|
279
|
+
const url = `${buildBaseUrl(apis.config)}/api/projects/default`;
|
|
280
|
+
const metadata = buildAuditMetadata(deps.env);
|
|
281
|
+
const body = { projectId: project.id, metadata };
|
|
282
|
+
if (argv.dryRun) {
|
|
283
|
+
emitDryRun(deps.stdout, argv.json, makeDryRunPayload("POST", url, body));
|
|
284
|
+
return EXIT.OK;
|
|
285
|
+
}
|
|
286
|
+
const updated = await apis.projects.setDefaultProject(project.id, { metadata });
|
|
287
|
+
if (argv.json) {
|
|
288
|
+
printJson(deps.stdout, projectAsObject(updated));
|
|
289
|
+
return EXIT.OK;
|
|
290
|
+
}
|
|
291
|
+
const display = updated?.name ?? project.name ?? project.id;
|
|
292
|
+
printPretty(deps.stdout, `Default project set to ${display}`);
|
|
293
|
+
return EXIT.OK;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function handleSetHidden(argv, deps, hidden) {
|
|
297
|
+
const apis = await buildApis(deps);
|
|
298
|
+
const project = await resolveProjectSelector(apis, argv.idOrName, {
|
|
299
|
+
env: deps.env,
|
|
300
|
+
cwd: deps.cwd,
|
|
301
|
+
daemonHost: argv.daemonHost,
|
|
302
|
+
});
|
|
303
|
+
const url = `${buildBaseUrl(apis.config)}/api/projects?projectId=${encodeURIComponent(project.id)}`;
|
|
304
|
+
const metadata = buildAuditMetadata(deps.env);
|
|
305
|
+
const body = { hidden, metadata };
|
|
306
|
+
if (argv.dryRun) {
|
|
307
|
+
emitDryRun(deps.stdout, argv.json, makeDryRunPayload("PATCH", url, body));
|
|
308
|
+
return EXIT.OK;
|
|
309
|
+
}
|
|
310
|
+
// SDK signature: `setProjectHidden(idOrName, hidden, options?)`. Earlier we
|
|
311
|
+
// were passing the whole body as the third arg, which the SDK silently
|
|
312
|
+
// dropped — audit metadata never reached the server (review H2a).
|
|
313
|
+
const updated = await apis.projects.setProjectHidden(project.id, hidden, { metadata });
|
|
314
|
+
if (argv.json) {
|
|
315
|
+
printJson(deps.stdout, projectAsObject(updated));
|
|
316
|
+
return EXIT.OK;
|
|
317
|
+
}
|
|
318
|
+
printPretty(
|
|
319
|
+
deps.stdout,
|
|
320
|
+
hidden
|
|
321
|
+
? `Hid project ${project.name ?? project.id}`
|
|
322
|
+
: `Unhid project ${project.name ?? project.id}`,
|
|
323
|
+
);
|
|
324
|
+
return EXIT.OK;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function main(argvInput = hideBin(process.argv), deps = {}) {
|
|
328
|
+
const stdout = deps.stdout || process.stdout;
|
|
329
|
+
const stderr = deps.stderr || process.stderr;
|
|
330
|
+
const env = deps.env || process.env;
|
|
331
|
+
const cwd = deps.cwd || process.cwd();
|
|
332
|
+
const consoleErr = { error: (msg) => stderr.write(`${msg}\n`) };
|
|
333
|
+
const handlerDeps = { ...deps, stdout, stderr, env, cwd };
|
|
334
|
+
|
|
335
|
+
let exitCode = EXIT.OK;
|
|
336
|
+
try {
|
|
337
|
+
await yargs(argvInput)
|
|
338
|
+
.scriptName("conductor project")
|
|
339
|
+
.strict()
|
|
340
|
+
.help()
|
|
341
|
+
.option("json", { type: "boolean", default: false, describe: "Print machine-readable JSON" })
|
|
342
|
+
.option("dry-run", { type: "boolean", default: false, describe: "Print the would-be request, don't send" })
|
|
343
|
+
.option("project", { type: "string", describe: "Project id or name override" })
|
|
344
|
+
.option("config-file", { type: "string", describe: "Path to Conductor config file" })
|
|
345
|
+
.command(
|
|
346
|
+
"list",
|
|
347
|
+
"List projects",
|
|
348
|
+
(cmd) => cmd.option("include-hidden", { type: "boolean", default: false }),
|
|
349
|
+
async (argv) => {
|
|
350
|
+
exitCode = await handleList(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
351
|
+
},
|
|
352
|
+
)
|
|
353
|
+
.command(
|
|
354
|
+
"show [idOrName]",
|
|
355
|
+
"Show a project (defaults to the resolved current project)",
|
|
356
|
+
(cmd) => cmd
|
|
357
|
+
.positional("idOrName", { type: "string" })
|
|
358
|
+
.option("daemon-host", { type: "string", describe: "Disambiguate same-name projects across daemons" }),
|
|
359
|
+
async (argv) => {
|
|
360
|
+
exitCode = await handleShow(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
361
|
+
},
|
|
362
|
+
)
|
|
363
|
+
.command(
|
|
364
|
+
"current",
|
|
365
|
+
"Print the current project's id (or full JSON with --json)",
|
|
366
|
+
(cmd) => cmd,
|
|
367
|
+
async (argv) => {
|
|
368
|
+
exitCode = await handleCurrent(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
369
|
+
},
|
|
370
|
+
)
|
|
371
|
+
.command(
|
|
372
|
+
"create",
|
|
373
|
+
"Create a new project",
|
|
374
|
+
(cmd) => cmd
|
|
375
|
+
.option("name", { type: "string", describe: "Project name (defaults to basename of workspace path)" })
|
|
376
|
+
.option("workspace-path", { type: "string", describe: "Workspace path (defaults to cwd)" })
|
|
377
|
+
.option("daemon-host", { type: "string", describe: "Daemon hostname for binding" })
|
|
378
|
+
.option("default", { type: "boolean", default: false, describe: "Create the user's default project" })
|
|
379
|
+
.option("client-request-id", { type: "string", describe: "Idempotency key" }),
|
|
380
|
+
async (argv) => {
|
|
381
|
+
exitCode = await handleCreate(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
382
|
+
},
|
|
383
|
+
)
|
|
384
|
+
.command(
|
|
385
|
+
"set-default <idOrName>",
|
|
386
|
+
"Set the user's default project",
|
|
387
|
+
(cmd) => cmd
|
|
388
|
+
.positional("idOrName", { type: "string", demandOption: true })
|
|
389
|
+
.option("daemon-host", { type: "string", describe: "Disambiguate same-name projects across daemons" }),
|
|
390
|
+
async (argv) => {
|
|
391
|
+
exitCode = await handleSetDefault(argv, { ...handlerDeps, configFile: argv.configFile });
|
|
392
|
+
},
|
|
393
|
+
)
|
|
394
|
+
.command(
|
|
395
|
+
"hide <idOrName>",
|
|
396
|
+
"Hide a project from default listings",
|
|
397
|
+
(cmd) => cmd
|
|
398
|
+
.positional("idOrName", { type: "string", demandOption: true })
|
|
399
|
+
.option("daemon-host", { type: "string", describe: "Disambiguate same-name projects across daemons" }),
|
|
400
|
+
async (argv) => {
|
|
401
|
+
exitCode = await handleSetHidden(argv, { ...handlerDeps, configFile: argv.configFile }, true);
|
|
402
|
+
},
|
|
403
|
+
)
|
|
404
|
+
.command(
|
|
405
|
+
"unhide <idOrName>",
|
|
406
|
+
"Unhide a previously hidden project",
|
|
407
|
+
(cmd) => cmd
|
|
408
|
+
.positional("idOrName", { type: "string", demandOption: true })
|
|
409
|
+
.option("daemon-host", { type: "string", describe: "Disambiguate same-name projects across daemons" }),
|
|
410
|
+
async (argv) => {
|
|
411
|
+
exitCode = await handleSetHidden(argv, { ...handlerDeps, configFile: argv.configFile }, false);
|
|
412
|
+
},
|
|
413
|
+
)
|
|
414
|
+
.demandCommand(1)
|
|
415
|
+
.fail((msg, err) => {
|
|
416
|
+
if (err) {
|
|
417
|
+
throw err;
|
|
418
|
+
}
|
|
419
|
+
stderr.write(`${msg}\n`);
|
|
420
|
+
exitCode = EXIT.ARGS;
|
|
421
|
+
})
|
|
422
|
+
.parseAsync();
|
|
423
|
+
} catch (err) {
|
|
424
|
+
exitCode = reportError(consoleErr, err);
|
|
425
|
+
}
|
|
426
|
+
return exitCode;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (isMainModule) {
|
|
430
|
+
main().then((code) => {
|
|
431
|
+
if (code !== 0) process.exit(code);
|
|
432
|
+
}).catch((err) => {
|
|
433
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
434
|
+
process.exit(exitCodeForError(err));
|
|
435
|
+
});
|
|
436
|
+
}
|