@flrande/browserctl 0.3.0 → 0.4.0-dev.15.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.
|
@@ -55,6 +55,33 @@ describe("cli", () => {
|
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
it("parses --help as a global help request", () => {
|
|
59
|
+
expect(parseArgs(["--help"])).toEqual({
|
|
60
|
+
command: null,
|
|
61
|
+
commandArgs: [],
|
|
62
|
+
json: false,
|
|
63
|
+
helpTopic: "all"
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("parses -h as a global help request", () => {
|
|
68
|
+
expect(parseArgs(["-h"])).toEqual({
|
|
69
|
+
command: null,
|
|
70
|
+
commandArgs: [],
|
|
71
|
+
json: false,
|
|
72
|
+
helpTopic: "all"
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("parses command --help as command help request", () => {
|
|
77
|
+
expect(parseArgs(["status", "--help"])).toEqual({
|
|
78
|
+
command: null,
|
|
79
|
+
commandArgs: [],
|
|
80
|
+
json: false,
|
|
81
|
+
helpTopic: "status"
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
58
85
|
it("writes JSON output on successful run", async () => {
|
|
59
86
|
vi.spyOn(statusCommand, "runStatusCommand").mockResolvedValue({
|
|
60
87
|
kind: "browserd",
|
|
@@ -86,6 +113,68 @@ describe("cli", () => {
|
|
|
86
113
|
expect(state.stderr).toContain("Unknown command: unknown");
|
|
87
114
|
});
|
|
88
115
|
|
|
116
|
+
it("writes general help output for --help", async () => {
|
|
117
|
+
const { io, state } = createIoCapture();
|
|
118
|
+
|
|
119
|
+
const exitCode = await runCli(["--help"], io);
|
|
120
|
+
|
|
121
|
+
expect(exitCode).toBe(EXIT_CODES.OK);
|
|
122
|
+
expect(state.stderr).toBe("");
|
|
123
|
+
expect(state.stdout).toContain("Usage:");
|
|
124
|
+
expect(state.stdout).toContain("browserctl help [command]");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("writes JSON help output when --json is provided", async () => {
|
|
128
|
+
const { io, state } = createIoCapture();
|
|
129
|
+
|
|
130
|
+
const exitCode = await runCli(["--json", "--help"], io);
|
|
131
|
+
|
|
132
|
+
expect(exitCode).toBe(EXIT_CODES.OK);
|
|
133
|
+
expect(state.stderr).toBe("");
|
|
134
|
+
expect(JSON.parse(state.stdout)).toEqual(
|
|
135
|
+
expect.objectContaining({
|
|
136
|
+
ok: true,
|
|
137
|
+
command: "help",
|
|
138
|
+
data: expect.objectContaining({
|
|
139
|
+
topic: "all",
|
|
140
|
+
text: expect.stringContaining("Usage:")
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("writes command help output for help <command>", async () => {
|
|
147
|
+
const { io, state } = createIoCapture();
|
|
148
|
+
|
|
149
|
+
const exitCode = await runCli(["help", "status"], io);
|
|
150
|
+
|
|
151
|
+
expect(exitCode).toBe(EXIT_CODES.OK);
|
|
152
|
+
expect(state.stderr).toBe("");
|
|
153
|
+
expect(state.stdout).toContain("Usage:");
|
|
154
|
+
expect(state.stdout).toContain("browserctl status");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("writes command help output for <command> --help", async () => {
|
|
158
|
+
const { io, state } = createIoCapture();
|
|
159
|
+
|
|
160
|
+
const exitCode = await runCli(["tab-open", "--help"], io);
|
|
161
|
+
|
|
162
|
+
expect(exitCode).toBe(EXIT_CODES.OK);
|
|
163
|
+
expect(state.stderr).toBe("");
|
|
164
|
+
expect(state.stdout).toContain("Usage:");
|
|
165
|
+
expect(state.stdout).toContain("browserctl tab-open <url>");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("returns INVALID_ARGS for unknown help topic", async () => {
|
|
169
|
+
const { io, state } = createIoCapture();
|
|
170
|
+
|
|
171
|
+
const exitCode = await runCli(["help", "unknown"], io);
|
|
172
|
+
|
|
173
|
+
expect(exitCode).toBe(EXIT_CODES.INVALID_ARGS);
|
|
174
|
+
expect(state.stdout).toBe("");
|
|
175
|
+
expect(state.stderr).toContain("Unknown help topic: unknown");
|
|
176
|
+
});
|
|
177
|
+
|
|
89
178
|
it("writes non-JSON output when --json is not provided", async () => {
|
|
90
179
|
vi.spyOn(tabsCommand, "runTabsCommand").mockResolvedValue({
|
|
91
180
|
driver: "managed",
|
|
@@ -74,9 +74,12 @@ export interface ParsedArgs {
|
|
|
74
74
|
command: CommandName | null;
|
|
75
75
|
commandArgs: string[];
|
|
76
76
|
json: boolean;
|
|
77
|
+
helpTopic?: HelpTopic;
|
|
77
78
|
error?: string;
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
export type HelpTopic = "all" | CommandName;
|
|
82
|
+
|
|
80
83
|
const VALID_COMMANDS: ReadonlySet<CommandName> = new Set([
|
|
81
84
|
"status",
|
|
82
85
|
"tabs",
|
|
@@ -111,6 +114,177 @@ const VALID_COMMANDS: ReadonlySet<CommandName> = new Set([
|
|
|
111
114
|
"daemon-stop"
|
|
112
115
|
]);
|
|
113
116
|
|
|
117
|
+
interface CommandHelpEntry {
|
|
118
|
+
usage: string;
|
|
119
|
+
description: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const COMMAND_HELP: Readonly<Record<CommandName, CommandHelpEntry>> = {
|
|
123
|
+
status: {
|
|
124
|
+
usage: "status",
|
|
125
|
+
description: "Query daemon and active profile readiness."
|
|
126
|
+
},
|
|
127
|
+
tabs: {
|
|
128
|
+
usage: "tabs",
|
|
129
|
+
description: "List tabs for the current session."
|
|
130
|
+
},
|
|
131
|
+
"profile-list": {
|
|
132
|
+
usage: "profile-list",
|
|
133
|
+
description: "List available driver profiles."
|
|
134
|
+
},
|
|
135
|
+
"profile-use": {
|
|
136
|
+
usage: "profile-use <driverKey>",
|
|
137
|
+
description: "Bind session to a driver profile."
|
|
138
|
+
},
|
|
139
|
+
"tab-open": {
|
|
140
|
+
usage: "tab-open <url>",
|
|
141
|
+
description: "Open a new tab with the given URL."
|
|
142
|
+
},
|
|
143
|
+
"tab-focus": {
|
|
144
|
+
usage: "tab-focus <targetId>",
|
|
145
|
+
description: "Activate an existing tab target."
|
|
146
|
+
},
|
|
147
|
+
"tab-close": {
|
|
148
|
+
usage: "tab-close <targetId>",
|
|
149
|
+
description: "Close a tab target."
|
|
150
|
+
},
|
|
151
|
+
snapshot: {
|
|
152
|
+
usage: "snapshot <targetId>",
|
|
153
|
+
description: "Get structured DOM snapshot for a tab."
|
|
154
|
+
},
|
|
155
|
+
screenshot: {
|
|
156
|
+
usage: "screenshot <targetId>",
|
|
157
|
+
description: "Capture full-page screenshot for a tab."
|
|
158
|
+
},
|
|
159
|
+
"dom-query": {
|
|
160
|
+
usage: "dom-query <targetId> <selector>",
|
|
161
|
+
description: "Get first matching element details."
|
|
162
|
+
},
|
|
163
|
+
"dom-query-all": {
|
|
164
|
+
usage: "dom-query-all <targetId> <selector>",
|
|
165
|
+
description: "List all matching elements."
|
|
166
|
+
},
|
|
167
|
+
"element-screenshot": {
|
|
168
|
+
usage: "element-screenshot <targetId> <selector>",
|
|
169
|
+
description: "Capture screenshot for one element."
|
|
170
|
+
},
|
|
171
|
+
"a11y-snapshot": {
|
|
172
|
+
usage: "a11y-snapshot <targetId> [selector]",
|
|
173
|
+
description: "Get accessibility tree snapshot."
|
|
174
|
+
},
|
|
175
|
+
"network-wait-for": {
|
|
176
|
+
usage:
|
|
177
|
+
"network-wait-for <targetId> <urlPattern> [--method <METHOD>] [--status <CODE>] [--timeout-ms <ms>] [--poll-ms <ms>]",
|
|
178
|
+
description: "Wait until a matching network response appears."
|
|
179
|
+
},
|
|
180
|
+
"cookie-get": {
|
|
181
|
+
usage: "cookie-get <targetId> [name]",
|
|
182
|
+
description: "Read cookie(s) from page context."
|
|
183
|
+
},
|
|
184
|
+
"cookie-set": {
|
|
185
|
+
usage: "cookie-set <targetId> <name> <value> [url]",
|
|
186
|
+
description: "Set one cookie in page context."
|
|
187
|
+
},
|
|
188
|
+
"cookie-clear": {
|
|
189
|
+
usage: "cookie-clear <targetId> [name]",
|
|
190
|
+
description: "Clear one or all cookies."
|
|
191
|
+
},
|
|
192
|
+
"storage-get": {
|
|
193
|
+
usage: "storage-get <targetId> <local|session> <key>",
|
|
194
|
+
description: "Read a storage value."
|
|
195
|
+
},
|
|
196
|
+
"storage-set": {
|
|
197
|
+
usage: "storage-set <targetId> <local|session> <key> <value>",
|
|
198
|
+
description: "Write a storage value."
|
|
199
|
+
},
|
|
200
|
+
"frame-list": {
|
|
201
|
+
usage: "frame-list <targetId>",
|
|
202
|
+
description: "List frames for a tab."
|
|
203
|
+
},
|
|
204
|
+
"frame-snapshot": {
|
|
205
|
+
usage: "frame-snapshot <targetId> <frameId>",
|
|
206
|
+
description: "Snapshot one frame subtree."
|
|
207
|
+
},
|
|
208
|
+
act: {
|
|
209
|
+
usage: "act <actionType> <targetId>",
|
|
210
|
+
description: "Execute high-level action against a tab."
|
|
211
|
+
},
|
|
212
|
+
"upload-arm": {
|
|
213
|
+
usage: "upload-arm <targetId> <file1> [file2 ...]",
|
|
214
|
+
description: "Prepare file chooser upload paths."
|
|
215
|
+
},
|
|
216
|
+
"dialog-arm": {
|
|
217
|
+
usage: "dialog-arm <targetId>",
|
|
218
|
+
description: "Prepare next JS dialog interception."
|
|
219
|
+
},
|
|
220
|
+
"download-trigger": {
|
|
221
|
+
usage: "download-trigger <targetId>",
|
|
222
|
+
description: "Trigger download capture mode."
|
|
223
|
+
},
|
|
224
|
+
"download-wait": {
|
|
225
|
+
usage: "download-wait <targetId> [path]",
|
|
226
|
+
description: "Wait for next download and persist file."
|
|
227
|
+
},
|
|
228
|
+
"console-list": {
|
|
229
|
+
usage: "console-list <targetId>",
|
|
230
|
+
description: "List collected console events."
|
|
231
|
+
},
|
|
232
|
+
"response-body": {
|
|
233
|
+
usage: "response-body <targetId> <requestId>",
|
|
234
|
+
description: "Read response body for a captured request."
|
|
235
|
+
},
|
|
236
|
+
"daemon-status": {
|
|
237
|
+
usage: "daemon-status",
|
|
238
|
+
description: "Inspect local daemon process status."
|
|
239
|
+
},
|
|
240
|
+
"daemon-start": {
|
|
241
|
+
usage: "daemon-start [--browser <chromium|chrome|edge>]",
|
|
242
|
+
description: "Start daemon if needed and report runtime."
|
|
243
|
+
},
|
|
244
|
+
"daemon-stop": {
|
|
245
|
+
usage: "daemon-stop",
|
|
246
|
+
description: "Stop local daemon process."
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const COMMAND_GROUPS: ReadonlyArray<{ title: string; commands: readonly CommandName[] }> = [
|
|
251
|
+
{
|
|
252
|
+
title: "Daemon lifecycle",
|
|
253
|
+
commands: ["daemon-start", "daemon-status", "daemon-stop"]
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
title: "Driver/session",
|
|
257
|
+
commands: ["status", "profile-list", "profile-use", "tabs"]
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
title: "Tab/page",
|
|
261
|
+
commands: ["tab-open", "tab-focus", "tab-close", "snapshot", "screenshot"]
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
title: "Structured reads",
|
|
265
|
+
commands: [
|
|
266
|
+
"dom-query",
|
|
267
|
+
"dom-query-all",
|
|
268
|
+
"element-screenshot",
|
|
269
|
+
"a11y-snapshot",
|
|
270
|
+
"network-wait-for",
|
|
271
|
+
"cookie-get",
|
|
272
|
+
"cookie-set",
|
|
273
|
+
"cookie-clear",
|
|
274
|
+
"storage-get",
|
|
275
|
+
"storage-set",
|
|
276
|
+
"frame-list",
|
|
277
|
+
"frame-snapshot",
|
|
278
|
+
"console-list",
|
|
279
|
+
"response-body"
|
|
280
|
+
]
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
title: "Action/file flow",
|
|
284
|
+
commands: ["act", "upload-arm", "dialog-arm", "download-trigger", "download-wait"]
|
|
285
|
+
}
|
|
286
|
+
];
|
|
287
|
+
|
|
114
288
|
const CHROME_RELAY_FAILURE_GUIDANCE = [
|
|
115
289
|
"chrome-relay 连接失败,请先完成以下检查:",
|
|
116
290
|
"1) 扩展 relay 方案:设置 BROWSERD_CHROME_RELAY_MODE=extension。",
|
|
@@ -120,9 +294,89 @@ const CHROME_RELAY_FAILURE_GUIDANCE = [
|
|
|
120
294
|
"5) 确认 BROWSERD_CHROME_RELAY_URL 指向可访问地址(默认 http://127.0.0.1:9223)。"
|
|
121
295
|
].join("\n");
|
|
122
296
|
|
|
297
|
+
function isHelpFlag(value: string): boolean {
|
|
298
|
+
return value === "--help" || value === "-h";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function formatGeneralHelp(): string {
|
|
302
|
+
const lines = [
|
|
303
|
+
"browserctl - BrowserCtl command line client",
|
|
304
|
+
"",
|
|
305
|
+
"Usage:",
|
|
306
|
+
" browserctl [--json] <command> [command-options] [positionals]",
|
|
307
|
+
" browserctl help [command]",
|
|
308
|
+
" browserctl <command> --help",
|
|
309
|
+
"",
|
|
310
|
+
"Global options:",
|
|
311
|
+
" --json Print success/error envelopes as JSON.",
|
|
312
|
+
" --help, -h Show general help or command help.",
|
|
313
|
+
"",
|
|
314
|
+
"Context options (browser commands):",
|
|
315
|
+
" --session <id> Session namespace (default: cli:local).",
|
|
316
|
+
" --profile <driverKey> Driver override.",
|
|
317
|
+
" --token <authToken> Request auth token override.",
|
|
318
|
+
" --browser <preset> Daemon startup browser preset (daemon-start only).",
|
|
319
|
+
"",
|
|
320
|
+
"Commands:"
|
|
321
|
+
];
|
|
322
|
+
|
|
323
|
+
for (const group of COMMAND_GROUPS) {
|
|
324
|
+
lines.push(` ${group.title}:`);
|
|
325
|
+
for (const command of group.commands) {
|
|
326
|
+
const entry = COMMAND_HELP[command];
|
|
327
|
+
lines.push(` ${entry.usage}`);
|
|
328
|
+
lines.push(` ${entry.description}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
lines.push("");
|
|
333
|
+
lines.push("Run \"browserctl help <command>\" for command-specific usage.");
|
|
334
|
+
|
|
335
|
+
return `${lines.join("\n")}\n`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function formatCommandHelp(command: CommandName): string {
|
|
339
|
+
const entry = COMMAND_HELP[command];
|
|
340
|
+
const lines = [`Command: ${command}`, "", "Usage:", ` browserctl ${entry.usage}`, "", entry.description];
|
|
341
|
+
const isDaemonCommand = command.startsWith("daemon-");
|
|
342
|
+
|
|
343
|
+
if (!isDaemonCommand) {
|
|
344
|
+
lines.push("");
|
|
345
|
+
lines.push("Context options:");
|
|
346
|
+
lines.push(" --session <id> Session namespace (default: cli:local).");
|
|
347
|
+
lines.push(" --profile <driverKey> Driver override.");
|
|
348
|
+
lines.push(" --token <authToken> Request auth token override.");
|
|
349
|
+
} else if (command === "daemon-start") {
|
|
350
|
+
lines.push("");
|
|
351
|
+
lines.push("Command options:");
|
|
352
|
+
lines.push(" --browser <chromium|chrome|edge> managed-local browser preset.");
|
|
353
|
+
lines.push(" --token <authToken> Request auth token override.");
|
|
354
|
+
} else if (command === "daemon-status") {
|
|
355
|
+
lines.push("");
|
|
356
|
+
lines.push("Command options:");
|
|
357
|
+
lines.push(" --token <authToken> Request auth token override.");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
lines.push("");
|
|
361
|
+
lines.push("Global options:");
|
|
362
|
+
lines.push(" --json");
|
|
363
|
+
lines.push(" --help, -h");
|
|
364
|
+
|
|
365
|
+
return `${lines.join("\n")}\n`;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function formatHelp(topic: HelpTopic): string {
|
|
369
|
+
if (topic === "all") {
|
|
370
|
+
return formatGeneralHelp();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return formatCommandHelp(topic);
|
|
374
|
+
}
|
|
375
|
+
|
|
123
376
|
export function parseArgs(argv: string[]): ParsedArgs {
|
|
124
|
-
const positional = [];
|
|
377
|
+
const positional: string[] = [];
|
|
125
378
|
let json = false;
|
|
379
|
+
let helpRequested = false;
|
|
126
380
|
|
|
127
381
|
for (const value of argv) {
|
|
128
382
|
if (value === "--json") {
|
|
@@ -130,11 +384,25 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
130
384
|
continue;
|
|
131
385
|
}
|
|
132
386
|
|
|
387
|
+
if (isHelpFlag(value)) {
|
|
388
|
+
helpRequested = true;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
|
|
133
392
|
positional.push(value);
|
|
134
393
|
}
|
|
135
394
|
|
|
136
395
|
const [commandToken, ...commandArgs] = positional;
|
|
137
396
|
if (commandToken === undefined) {
|
|
397
|
+
if (helpRequested) {
|
|
398
|
+
return {
|
|
399
|
+
command: null,
|
|
400
|
+
commandArgs: [],
|
|
401
|
+
json,
|
|
402
|
+
helpTopic: "all"
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
138
406
|
return {
|
|
139
407
|
command: null,
|
|
140
408
|
commandArgs: [],
|
|
@@ -143,6 +411,61 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
143
411
|
};
|
|
144
412
|
}
|
|
145
413
|
|
|
414
|
+
if (commandToken === "help") {
|
|
415
|
+
if (commandArgs.length === 0) {
|
|
416
|
+
return {
|
|
417
|
+
command: null,
|
|
418
|
+
commandArgs: [],
|
|
419
|
+
json,
|
|
420
|
+
helpTopic: "all"
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (commandArgs.length > 1) {
|
|
425
|
+
return {
|
|
426
|
+
command: null,
|
|
427
|
+
commandArgs: [],
|
|
428
|
+
json,
|
|
429
|
+
error: "Too many arguments for help."
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const topic = commandArgs[0];
|
|
434
|
+
if (!VALID_COMMANDS.has(topic as CommandName)) {
|
|
435
|
+
return {
|
|
436
|
+
command: null,
|
|
437
|
+
commandArgs: [],
|
|
438
|
+
json,
|
|
439
|
+
error: `Unknown help topic: ${topic}`
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
command: null,
|
|
445
|
+
commandArgs: [],
|
|
446
|
+
json,
|
|
447
|
+
helpTopic: topic as CommandName
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (helpRequested) {
|
|
452
|
+
if (!VALID_COMMANDS.has(commandToken as CommandName)) {
|
|
453
|
+
return {
|
|
454
|
+
command: null,
|
|
455
|
+
commandArgs: [],
|
|
456
|
+
json,
|
|
457
|
+
error: `Unknown command: ${commandToken}`
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
command: null,
|
|
463
|
+
commandArgs: [],
|
|
464
|
+
json,
|
|
465
|
+
helpTopic: commandToken as CommandName
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
146
469
|
if (!VALID_COMMANDS.has(commandToken as CommandName)) {
|
|
147
470
|
return {
|
|
148
471
|
command: null,
|
|
@@ -327,6 +650,26 @@ export async function runCli(
|
|
|
327
650
|
return EXIT_CODES.INVALID_ARGS;
|
|
328
651
|
}
|
|
329
652
|
|
|
653
|
+
if (parsedArgs.helpTopic !== undefined) {
|
|
654
|
+
const helpText = formatHelp(parsedArgs.helpTopic);
|
|
655
|
+
if (parsedArgs.json) {
|
|
656
|
+
io.stdout.write(
|
|
657
|
+
`${JSON.stringify({
|
|
658
|
+
ok: true,
|
|
659
|
+
command: "help",
|
|
660
|
+
data: {
|
|
661
|
+
topic: parsedArgs.helpTopic,
|
|
662
|
+
text: helpText.trimEnd()
|
|
663
|
+
}
|
|
664
|
+
})}\n`
|
|
665
|
+
);
|
|
666
|
+
} else {
|
|
667
|
+
io.stdout.write(helpText);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return EXIT_CODES.OK;
|
|
671
|
+
}
|
|
672
|
+
|
|
330
673
|
try {
|
|
331
674
|
const data = await executeCommand(parsedArgs.command, parsedArgs.commandArgs);
|
|
332
675
|
|