not-manage 0.2.2 → 0.2.3

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/src/cli.js CHANGED
@@ -3,20 +3,111 @@ const {
3
3
  authRevoke,
4
4
  authSetup,
5
5
  authStatus,
6
- maybeRunSetupOnFirstUse,
7
6
  setupWizard,
8
7
  whoAmI,
9
8
  } = require("./commands-auth");
10
- const { hasFlag, parseOptions, readBooleanOption, readCommandOptions } = require("./cli-options");
9
+ const { readJsonInput } = require("./agent-input");
10
+ const { UsageError } = require("./cli-errors");
11
+ const {
12
+ hasFlag,
13
+ parseOptions,
14
+ readBooleanOption,
15
+ readCommandOptions,
16
+ readStringOption,
17
+ } = require("./cli-options");
18
+ const { agentContext } = require("./commands-agent-context");
19
+ const { doctor } = require("./commands-doctor");
20
+ const { rawRequest } = require("./commands-request");
11
21
  const { getResourceHandler } = require("./resource-handlers");
12
22
  const {
13
23
  RESOURCE_ORDER,
24
+ findMissingRequiredOptions,
14
25
  getResourceMetadata,
15
26
  listRequiredOptionFlags,
16
27
  normalizeResourceCommand,
17
28
  } = require("./resource-metadata");
18
29
  const { version } = require("../package.json");
19
30
 
31
+ const HELP_FLAGS = new Set(["-h", "--help"]);
32
+ const VERSION_FLAGS = new Set(["-v", "--version"]);
33
+ const GLOBAL_BOOLEAN_FLAGS = new Set([
34
+ "agent",
35
+ "compact",
36
+ "json",
37
+ "no-color",
38
+ "no-input",
39
+ "yes",
40
+ "force",
41
+ ]);
42
+
43
+ function parseGlobalBoolean(value, flagName) {
44
+ if (value === undefined || value === "") {
45
+ return true;
46
+ }
47
+
48
+ const normalized = String(value).trim().toLowerCase();
49
+ if (normalized === "true") {
50
+ return true;
51
+ }
52
+ if (normalized === "false") {
53
+ return false;
54
+ }
55
+
56
+ throw new UsageError(`\`--${flagName}\` must be \`true\` or \`false\`.`, {
57
+ code: "invalid_global_flag",
58
+ });
59
+ }
60
+
61
+ function readGlobalFlags(args) {
62
+ const values = {
63
+ agent: false,
64
+ compact: false,
65
+ force: false,
66
+ json: false,
67
+ noColor: false,
68
+ noInput: false,
69
+ yes: false,
70
+ };
71
+
72
+ for (let index = 0; index < args.length; index += 1) {
73
+ const token = args[index];
74
+ if (!token.startsWith("--")) {
75
+ continue;
76
+ }
77
+
78
+ const [flagName, inlineValue] = token.slice(2).split("=", 2);
79
+ if (!GLOBAL_BOOLEAN_FLAGS.has(flagName)) {
80
+ continue;
81
+ }
82
+
83
+ let value = inlineValue;
84
+ const next = args[index + 1];
85
+ if (
86
+ value === undefined &&
87
+ (next === "true" || next === "false")
88
+ ) {
89
+ value = next;
90
+ index += 1;
91
+ }
92
+
93
+ values[flagName.replace(/-([a-z])/g, (_match, char) => char.toUpperCase())] =
94
+ parseGlobalBoolean(value, flagName);
95
+ }
96
+
97
+ if (values.agent) {
98
+ values.compact = true;
99
+ values.json = true;
100
+ values.noColor = true;
101
+ values.noInput = true;
102
+ values.yes = true;
103
+ }
104
+ if (values.force) {
105
+ values.yes = true;
106
+ }
107
+
108
+ return values;
109
+ }
110
+
20
111
  function maybePrintDefaultFields(command, sub, optionValues) {
21
112
  if (optionValues.fields !== true) {
22
113
  return false;
@@ -24,8 +115,12 @@ function maybePrintDefaultFields(command, sub, optionValues) {
24
115
 
25
116
  const defaults = getResourceMetadata(command)?.defaultFields?.[sub];
26
117
  if (!defaults) {
27
- throw new Error(
28
- "`--fields` requires a comma-separated value for this command. Example: --fields id,name"
118
+ throw new UsageError(
119
+ "`--fields` requires a comma-separated value for this command. Example: --fields id,name",
120
+ {
121
+ code: "missing_fields_value",
122
+ hint: "--fields id,name",
123
+ }
29
124
  );
30
125
  }
31
126
 
@@ -74,85 +169,693 @@ function warnAboutRedaction(resourceMetadata, sub, optionValues, redacted) {
74
169
  }
75
170
  }
76
171
 
77
- function printHelp() {
172
+ function printOptionLines(entries = []) {
173
+ if (entries.length === 0) {
174
+ return;
175
+ }
176
+
177
+ console.log("Options:");
178
+ entries.forEach((entry) => {
179
+ if (typeof entry === "string") {
180
+ console.log(` ${entry}`);
181
+ return;
182
+ }
183
+
184
+ console.log(` ${entry.usage.padEnd(28, " ")} ${entry.description}`);
185
+ });
186
+ }
187
+
188
+ function printExamples(examples = []) {
189
+ if (examples.length === 0) {
190
+ return;
191
+ }
192
+
193
+ console.log("Examples:");
194
+ examples.forEach((example) => {
195
+ console.log(` ${example}`);
196
+ });
197
+ }
198
+
199
+ function printCommandHelp(title, details = {}) {
200
+ const {
201
+ description,
202
+ examples = [],
203
+ notes = [],
204
+ options = [],
205
+ usage = [],
206
+ } = details;
207
+
208
+ console.log(title);
209
+ console.log("");
210
+
211
+ if (usage.length > 0) {
212
+ console.log("Usage:");
213
+ usage.forEach((line) => {
214
+ console.log(` ${line}`);
215
+ });
216
+ console.log("");
217
+ }
218
+
219
+ if (description) {
220
+ console.log("Description:");
221
+ console.log(` ${description}`);
222
+ console.log("");
223
+ }
224
+
225
+ if (notes.length > 0) {
226
+ console.log("Notes:");
227
+ notes.forEach((line) => {
228
+ console.log(` ${line}`);
229
+ });
230
+ console.log("");
231
+ }
232
+
233
+ printOptionLines(options);
234
+ if (options.length > 0 && examples.length > 0) {
235
+ console.log("");
236
+ }
237
+ printExamples(examples);
238
+ }
239
+
240
+ function optionPlaceholder(optionDef) {
241
+ switch (optionDef.kind) {
242
+ case "boolean":
243
+ return " <true|false>";
244
+ case "object":
245
+ return " <json|key=value>";
246
+ case "string-array":
247
+ return " <value[,value]>";
248
+ default:
249
+ return " <value>";
250
+ }
251
+ }
252
+
253
+ function formatResourceOption(optionDef) {
254
+ if (optionDef.positional !== undefined) {
255
+ return null;
256
+ }
257
+
258
+ return `--${optionDef.option}${optionDef.kind === "flag" ? "" : optionPlaceholder(optionDef)}`;
259
+ }
260
+
261
+ function buildResourceOptionLines(resourceMetadata, sub) {
262
+ const optionLines = Object.values(resourceMetadata.optionSchema?.[sub] || {})
263
+ .map((optionDef) => formatResourceOption(optionDef))
264
+ .filter(Boolean);
265
+
266
+ if (sub === "list" || sub === "get") {
267
+ optionLines.push("--options-file <path|->");
268
+ optionLines.push("--json");
269
+ optionLines.push("--compact");
270
+ optionLines.push("--redacted");
271
+ optionLines.push("--unredacted");
272
+ }
273
+
274
+ if (sub === "get") {
275
+ optionLines.push("--ids-file <path|->");
276
+ optionLines.push("--stdin");
277
+ }
278
+
279
+ optionLines.push("-h, --help");
280
+ return optionLines;
281
+ }
282
+
283
+ function buildResourceExamples(command, sub, requiredFlags) {
284
+ if (sub === "get") {
285
+ return [
286
+ `not-manage ${command} get 123 --json`,
287
+ `not-manage ${command} get 123 --fields id,name --json`,
288
+ `printf "123\\n456\\n" | not-manage ${command} get --stdin --json`,
289
+ ];
290
+ }
291
+
292
+ const exampleValues = {
293
+ "--conversation-id": "123",
294
+ "--matter-id": "123",
295
+ "--type": "Matter",
296
+ "--user-id": "123",
297
+ };
298
+ const requiredExample =
299
+ requiredFlags.length > 0
300
+ ? `${requiredFlags.map((flag) => `${flag} <value>`).join(" ")} `
301
+ : "";
302
+ const realisticRequiredExample =
303
+ requiredFlags.length > 0
304
+ ? `${requiredFlags.map((flag) => `${flag} ${exampleValues[flag] || "123"}`).join(" ")} `
305
+ : "";
306
+
307
+ return [
308
+ `not-manage ${command} list ${realisticRequiredExample}--json`,
309
+ `not-manage ${command} list ${requiredExample}--fields id,name`,
310
+ `not-manage ${command} list --options-file filters.json --json`,
311
+ ];
312
+ }
313
+
314
+ function printGlobalHelp() {
78
315
  console.log("not-manage");
79
316
  console.log("");
80
317
  console.log("Usage:");
81
318
  console.log(" not-manage <command> [options]");
82
319
  console.log("");
83
320
  console.log("Commands:");
84
- console.log(" setup Run guided setup and OAuth login");
85
- console.log(" auth setup Configure client credentials in OS keychain");
86
- console.log(" auth login Run local OAuth login flow");
87
- console.log(" auth status Show auth status and connected user");
88
- console.log(" auth revoke Revoke token and clear local token storage");
89
-
90
- RESOURCE_ORDER.forEach((command) => {
91
- const resourceMetadata = getResourceMetadata(command);
92
- ["list", "get"].forEach((sub) => {
93
- if (!resourceMetadata.capabilities[sub].enabled) {
94
- return;
95
- }
96
-
97
- const usage = `${command} ${sub}`.padEnd(18, " ");
98
- const requiredFlags = listRequiredOptionFlags(resourceMetadata, sub);
99
- const requirementNote =
100
- requiredFlags.length > 0 ? ` (requires ${requiredFlags.join(", ")})` : "";
101
- console.log(` ${usage} ${resourceMetadata.help[sub]}${requirementNote}`);
102
- });
103
- });
104
-
321
+ console.log(" setup Guided setup plus OAuth login");
322
+ console.log(" doctor Diagnose config, token, and Clio connectivity");
323
+ console.log(" agent-context Machine-readable command/resource catalog");
324
+ console.log(" auth Manage app credentials and OAuth tokens");
105
325
  console.log(" whoami Call /api/v4/users/who_am_i");
326
+ console.log(" request Raw API escape hatch with write guard");
327
+ console.log(" <resource> Read Clio resources with list/get subcommands");
106
328
  console.log("");
107
- console.log("Aliases:");
108
- console.log(" Singular aliases are accepted, for example:");
109
- console.log(" contact get, matter get, bill get, invoice get, task get, user get");
329
+ console.log("Resources:");
330
+ console.log(" Run `not-manage agent-context` for the full machine-readable catalog.");
331
+ console.log(" Run `not-manage <resource> --help` for resource subcommands.");
332
+ console.log(" Singular aliases are accepted, for example `contact get` and `matter get`.");
110
333
  console.log("");
111
334
  console.log("Options:");
335
+ console.log(" --agent Agent defaults: --json --compact --no-input --no-color --yes");
336
+ console.log(" --compact Return reduced JSON fields for lower-token output");
112
337
  console.log(" --fields <list> Override returned fields; pass `--fields` alone to print defaults");
113
338
  console.log(" --json Print machine-readable JSON for supported commands");
339
+ console.log(" --no-input Disable interactive prompts where supported");
340
+ console.log(" --no-color Disable colored output");
341
+ console.log(" --force Alias for --yes");
114
342
  console.log(" --redacted Kept for compatibility; data commands are redacted by default");
115
343
  console.log(" --unredacted Show raw output without default redaction");
116
344
  console.log(" -h, --help Show help");
117
345
  console.log(" -v, --version Show version");
346
+ console.log("");
347
+ console.log("Examples:");
348
+ console.log(" not-manage --agent agent-context");
349
+ console.log(" not-manage --agent doctor");
350
+ console.log(" not-manage --agent contacts list --limit 5");
351
+ console.log("");
352
+ console.log("Run `not-manage <command> --help` for command-specific flags and examples.");
118
353
  }
119
354
 
120
- function buildResourceOptions(resourceMetadata, sub, optionValues, positional, json, redacted) {
355
+ function printSetupHelp() {
356
+ printCommandHelp("not-manage setup", {
357
+ description: "Run guided credential setup, then continue directly into OAuth login.",
358
+ usage: ["not-manage setup [auth-setup-options]"],
359
+ notes: [
360
+ "This command accepts the same setup flags as `not-manage auth setup`.",
361
+ "If required values are missing, prompts are used only in an interactive terminal.",
362
+ ],
363
+ options: [
364
+ { usage: "--confirm-confidentiality", description: "Acknowledge the confidentiality warning without a prompt" },
365
+ { usage: "--region <code>", description: "Clio region code: us, ca, eu, or au" },
366
+ { usage: "--client-id <value>", description: "App Key / Client ID" },
367
+ { usage: "--client-secret <value>", description: "App Secret / Client Secret" },
368
+ { usage: "--redirect-uri <url>", description: "Override the loopback OAuth redirect URI" },
369
+ { usage: "--open-browser <true|false>", description: "Open the regional developer portal during setup" },
370
+ { usage: "-h, --help", description: "Show help" },
371
+ ],
372
+ examples: [
373
+ "not-manage setup",
374
+ "not-manage setup --confirm-confidentiality --region us --client-id <key> --client-secret <secret>",
375
+ ],
376
+ });
377
+ }
378
+
379
+ function printAuthHelp() {
380
+ printCommandHelp("not-manage auth", {
381
+ description: "Manage Clio app credentials and OAuth tokens.",
382
+ usage: ["not-manage auth <subcommand> [options]"],
383
+ notes: [
384
+ "Subcommands: setup, login, status, revoke",
385
+ "Run `not-manage auth <subcommand> --help` for flags and examples.",
386
+ ],
387
+ examples: [
388
+ "not-manage auth setup --help",
389
+ "not-manage auth status --json",
390
+ "not-manage auth revoke --dry-run",
391
+ ],
392
+ });
393
+ }
394
+
395
+ function printAuthSetupHelp() {
396
+ printCommandHelp("not-manage auth setup", {
397
+ description: "Configure client credentials in the OS keychain.",
398
+ usage: ["not-manage auth setup [options]"],
399
+ notes: [
400
+ "If required values are missing, prompts are used only in an interactive terminal.",
401
+ "Outside a TTY, pass `--confirm-confidentiality`, `--client-id`, and `--client-secret`.",
402
+ ],
403
+ options: [
404
+ { usage: "--confirm-confidentiality", description: "Acknowledge the confidentiality warning without a prompt" },
405
+ { usage: "--region <code>", description: "Clio region code: us, ca, eu, or au" },
406
+ { usage: "--client-id <value>", description: "App Key / Client ID" },
407
+ { usage: "--client-secret <value>", description: "App Secret / Client Secret" },
408
+ { usage: "--redirect-uri <url>", description: "Override the loopback OAuth redirect URI" },
409
+ { usage: "--open-browser <true|false>", description: "Open the regional developer portal during setup" },
410
+ { usage: "-h, --help", description: "Show help" },
411
+ ],
412
+ examples: [
413
+ "not-manage auth setup",
414
+ "not-manage auth setup --confirm-confidentiality --region us --client-id <key> --client-secret <secret>",
415
+ ],
416
+ });
417
+ }
418
+
419
+ function printAuthLoginHelp() {
420
+ printCommandHelp("not-manage auth login", {
421
+ description: "Run the local OAuth login flow using the saved client credentials.",
422
+ usage: ["not-manage auth login"],
423
+ options: [{ usage: "-h, --help", description: "Show help" }],
424
+ examples: ["not-manage auth login"],
425
+ });
426
+ }
427
+
428
+ function printAuthStatusHelp() {
429
+ printCommandHelp("not-manage auth status", {
430
+ description: "Show the configured region, token source, and connected user.",
431
+ usage: ["not-manage auth status [options]"],
432
+ options: [
433
+ { usage: "--json", description: "Print machine-readable JSON" },
434
+ { usage: "--unredacted", description: "Show raw connected-user details instead of masked values" },
435
+ { usage: "-h, --help", description: "Show help" },
436
+ ],
437
+ examples: ["not-manage auth status", "not-manage auth status --json"],
438
+ });
439
+ }
440
+
441
+ function printDoctorHelp() {
442
+ printCommandHelp("not-manage doctor", {
443
+ description:
444
+ "Report CLI version, config source, region/host, and token/auth state. Works without auth so agents can diagnose missing setup without triggering other errors.",
445
+ usage: ["not-manage doctor [options]"],
446
+ options: [
447
+ { usage: "--json", description: "Print machine-readable JSON" },
448
+ { usage: "--compact", description: "Print compact JSON when combined with --json" },
449
+ { usage: "--fail-on <warn|error>", description: "Exit non-zero when the report reaches this severity" },
450
+ { usage: "-h, --help", description: "Show help" },
451
+ ],
452
+ examples: ["not-manage doctor", "not-manage doctor --json", "not-manage doctor --fail-on warn"],
453
+ });
454
+ }
455
+
456
+ function printAgentContextHelp() {
457
+ printCommandHelp("not-manage agent-context", {
458
+ description: "Emit a machine-readable command and option map for agents.",
459
+ usage: ["not-manage agent-context [options]"],
460
+ options: [
461
+ { usage: "--compact", description: "Print compact JSON" },
462
+ { usage: "-h, --help", description: "Show help" },
463
+ ],
464
+ examples: ["not-manage agent-context", "not-manage --agent agent-context"],
465
+ });
466
+ }
467
+
468
+ function printRequestHelp() {
469
+ printCommandHelp("not-manage request", {
470
+ description:
471
+ "Raw Clio API escape hatch. Reuses saved auth, base URL trust checks, redaction, and JSON envelope.",
472
+ usage: ["not-manage request <method> <path> [options]"],
473
+ notes: [
474
+ "Reads are safe by default: only GET and HEAD run without an extra confirmation flag.",
475
+ "Non-idempotent methods (POST/PUT/PATCH/DELETE) require `--write` and are treated as live writes.",
476
+ "`--dry-run` previews the validated request plan without sending it.",
477
+ "Paths are validated against the configured region host before the bearer token is sent.",
478
+ "Response bodies are redacted by default; pass `--unredacted` to inspect raw output.",
479
+ ],
480
+ options: [
481
+ { usage: "--query <k=v[,k=v]>", description: "Appended as URL query parameters" },
482
+ { usage: "--body-file <path|->", description: "JSON body file, or `-` to read from stdin" },
483
+ { usage: "--write", description: "Required for POST, PUT, PATCH, DELETE" },
484
+ { usage: "--dry-run", description: "Print the validated request plan without sending it" },
485
+ { usage: "--json", description: "Print machine-readable JSON (default for this command)" },
486
+ { usage: "--unredacted", description: "Show raw output without redaction" },
487
+ { usage: "-h, --help", description: "Show help" },
488
+ ],
489
+ examples: [
490
+ "not-manage request get /api/v4/users/who_am_i --json",
491
+ "not-manage request get /api/v4/matters.json --query status=open,limit=5 --json",
492
+ "not-manage request post /api/v4/activities.json --body-file body.json --dry-run --json",
493
+ "cat body.json | not-manage request post /api/v4/activities.json --body-file - --write --json",
494
+ ],
495
+ });
496
+ }
497
+
498
+ function printAuthRevokeHelp() {
499
+ printCommandHelp("not-manage auth revoke", {
500
+ description: "Revoke the current Clio token and clear the local keychain token.",
501
+ usage: ["not-manage auth revoke [options]"],
502
+ notes: ["Use `--dry-run` to inspect the action without changing remote or local state."],
503
+ options: [
504
+ { usage: "--yes", description: "Skip the confirmation prompt and revoke immediately" },
505
+ { usage: "--force", description: "Alias for --yes" },
506
+ { usage: "--dry-run", description: "Print the revoke plan without revoking or clearing tokens" },
507
+ { usage: "-h, --help", description: "Show help" },
508
+ ],
509
+ examples: ["not-manage auth revoke --dry-run", "not-manage auth revoke --yes"],
510
+ });
511
+ }
512
+
513
+ function printWhoAmIHelp() {
514
+ printCommandHelp("not-manage whoami", {
515
+ description: "Call `/api/v4/users/who_am_i` for the authenticated user.",
516
+ usage: ["not-manage whoami [options]"],
517
+ options: [
518
+ { usage: "--json", description: "Print machine-readable JSON" },
519
+ { usage: "-h, --help", description: "Show help" },
520
+ ],
521
+ examples: ["not-manage whoami", "not-manage whoami --json"],
522
+ });
523
+ }
524
+
525
+ function printResourceOverview(command, resourceMetadata) {
526
+ const subcommands = ["list", "get"].filter((sub) => resourceMetadata.capabilities[sub].enabled);
527
+
528
+ printCommandHelp(`not-manage ${command}`, {
529
+ description: `Explore the ${command} command surface.`,
530
+ usage: [`not-manage ${command} <subcommand> [options]`],
531
+ notes: [
532
+ `Subcommands: ${subcommands.join(", ")}`,
533
+ resourceMetadata.aliases.length > 0
534
+ ? `Aliases: ${resourceMetadata.aliases.join(", ")}`
535
+ : null,
536
+ `Run \`not-manage ${command} <subcommand> --help\` for flags and examples.`,
537
+ ].filter(Boolean),
538
+ examples: [
539
+ `not-manage ${command} list --help`,
540
+ resourceMetadata.capabilities.list.enabled ? `not-manage ${command} list --json` : null,
541
+ resourceMetadata.capabilities.get.enabled ? `not-manage ${command} get <id> --json` : null,
542
+ ].filter(Boolean),
543
+ });
544
+ }
545
+
546
+ function printResourceCommandHelp(command, sub, resourceMetadata) {
547
+ const requiredFlags = listRequiredOptionFlags(resourceMetadata, sub);
548
+ const usage =
549
+ sub === "get"
550
+ ? [`not-manage ${command} get <id> [options]`]
551
+ : [`not-manage ${command} list [options]`];
552
+ const notes = [];
553
+
554
+ if (requiredFlags.length > 0) {
555
+ notes.push(`Required filters: ${requiredFlags.join(", ")}`);
556
+ }
557
+
558
+ if (resourceMetadata.aliases.length > 0) {
559
+ notes.push(`Aliases: ${resourceMetadata.aliases.join(", ")}`);
560
+ }
561
+
562
+ printCommandHelp(`not-manage ${command} ${sub}`, {
563
+ description: resourceMetadata.help[sub],
564
+ usage,
565
+ notes,
566
+ options: buildResourceOptionLines(resourceMetadata, sub),
567
+ examples: buildResourceExamples(command, sub, requiredFlags),
568
+ });
569
+ }
570
+
571
+ function printCommandSpecificHelp(command, sub) {
572
+ if (!command) {
573
+ printGlobalHelp();
574
+ return;
575
+ }
576
+
577
+ if (command === "setup") {
578
+ printSetupHelp();
579
+ return;
580
+ }
581
+
582
+ if (command === "auth") {
583
+ if (!sub) {
584
+ printAuthHelp();
585
+ return;
586
+ }
587
+
588
+ if (sub === "setup") {
589
+ printAuthSetupHelp();
590
+ return;
591
+ }
592
+
593
+ if (sub === "login") {
594
+ printAuthLoginHelp();
595
+ return;
596
+ }
597
+
598
+ if (sub === "status") {
599
+ printAuthStatusHelp();
600
+ return;
601
+ }
602
+
603
+ if (sub === "revoke") {
604
+ printAuthRevokeHelp();
605
+ return;
606
+ }
607
+
608
+ throw new UsageError(
609
+ `Unknown subcommand for auth: ${sub}\nRun \`not-manage auth --help\`.`,
610
+ { code: "unknown_subcommand", hint: "not-manage auth --help" }
611
+ );
612
+ }
613
+
614
+ if (command === "whoami") {
615
+ printWhoAmIHelp();
616
+ return;
617
+ }
618
+
619
+ if (command === "doctor") {
620
+ printDoctorHelp();
621
+ return;
622
+ }
623
+
624
+ if (command === "agent-context") {
625
+ printAgentContextHelp();
626
+ return;
627
+ }
628
+
629
+ if (command === "request") {
630
+ printRequestHelp();
631
+ return;
632
+ }
633
+
634
+ const resourceMetadata = getResourceMetadata(command);
635
+ if (!resourceMetadata) {
636
+ throw new UsageError(
637
+ `Unknown command: ${command}\nRun \`not-manage --help\`.`,
638
+ { code: "unknown_command", hint: "not-manage --help" }
639
+ );
640
+ }
641
+
642
+ if (!sub) {
643
+ printResourceOverview(command, resourceMetadata);
644
+ return;
645
+ }
646
+
647
+ if (!resourceMetadata.capabilities[sub]?.enabled) {
648
+ throw new UsageError(
649
+ `Unknown subcommand for ${command}: ${sub}\nRun \`not-manage ${command} --help\`.`,
650
+ { code: "unknown_subcommand", hint: `not-manage ${command} --help` }
651
+ );
652
+ }
653
+
654
+ printResourceCommandHelp(command, sub, resourceMetadata);
655
+ }
656
+
657
+ function stripRoutingFlags(args) {
658
+ const routingArgs = [];
659
+
660
+ for (let index = 0; index < args.length; index += 1) {
661
+ const arg = args[index];
662
+ if (HELP_FLAGS.has(arg) || VERSION_FLAGS.has(arg)) {
663
+ continue;
664
+ }
665
+
666
+ if (arg.startsWith("--")) {
667
+ const [flagName, inlineValue] = arg.slice(2).split("=", 2);
668
+ if (GLOBAL_BOOLEAN_FLAGS.has(flagName)) {
669
+ const next = args[index + 1];
670
+ if (inlineValue === undefined && (next === "true" || next === "false")) {
671
+ index += 1;
672
+ }
673
+ continue;
674
+ }
675
+ }
676
+
677
+ routingArgs.push(arg);
678
+ }
679
+
680
+ return routingArgs;
681
+ }
682
+
683
+ function buildResourceOptions(
684
+ resourceMetadata,
685
+ sub,
686
+ optionValues,
687
+ positional,
688
+ json,
689
+ redacted,
690
+ compact
691
+ ) {
692
+ const baseOptions = {
693
+ json,
694
+ redacted,
695
+ };
696
+ if (compact) {
697
+ baseOptions.compact = true;
698
+ }
699
+
121
700
  return readCommandOptions(
122
701
  optionValues,
123
702
  resourceMetadata.optionSchema[sub],
124
703
  positional,
125
- {
126
- json,
127
- redacted,
128
- },
704
+ baseOptions,
129
705
  resourceMetadata.fixedOptions?.[sub]
130
706
  );
131
707
  }
132
708
 
133
- async function run(args) {
134
- if (!args.length) {
135
- const startedOnboarding = await maybeRunSetupOnFirstUse();
136
- if (!startedOnboarding) {
137
- printHelp();
138
- }
709
+ function assertRequiredResourceOptions(command, sub, resourceMetadata, options) {
710
+ const missing = findMissingRequiredOptions(resourceMetadata, sub, options);
711
+ if (missing.length === 0) {
139
712
  return;
140
713
  }
141
714
 
142
- if (hasFlag(args, "-h", "--help")) {
143
- printHelp();
715
+ const missingList = missing.join(", ");
716
+ const exampleFlags = missing.map((flag) => `${flag} <value>`).join(" ");
717
+ const message = [
718
+ `Missing required ${missing.length === 1 ? "flag" : "flags"} for \`not-manage ${command} ${sub}\`: ${missingList}.`,
719
+ `Usage: not-manage ${command} ${sub} ${exampleFlags}`.trim(),
720
+ `Run \`not-manage ${command} ${sub} --help\` for the full flag list.`,
721
+ ].join("\n");
722
+
723
+ throw new UsageError(message, {
724
+ code: "missing_required_flags",
725
+ hint: `not-manage ${command} ${sub} --help`,
726
+ details: { command, subcommand: sub, missing_flags: missing },
727
+ });
728
+ }
729
+
730
+ function assertResourceIdOption(command, sub, options) {
731
+ if (sub !== "get" || options.id || options.idsFile) {
144
732
  return;
145
733
  }
146
734
 
147
- if (hasFlag(args, "-v", "--version")) {
148
- console.log(`not-manage v${version}`);
735
+ const usage = `Usage: not-manage ${command} get <id>`;
736
+ throw new UsageError(usage, {
737
+ code: "missing_resource_id",
738
+ hint: `not-manage ${command} get <id>`,
739
+ details: { missing: ["id"] },
740
+ });
741
+ }
742
+
743
+ function isPlainObject(value) {
744
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
745
+ }
746
+
747
+ function hasOwn(object, key) {
748
+ return Object.prototype.hasOwnProperty.call(object, key);
749
+ }
750
+
751
+ async function mergeOptionsFile(optionValues) {
752
+ const optionsFile = readStringOption(optionValues, "options-file");
753
+ if (!optionsFile) {
754
+ return optionValues;
755
+ }
756
+
757
+ const fileOptions = await readJsonInput(optionsFile, "`--options-file` input");
758
+ if (!isPlainObject(fileOptions)) {
759
+ throw new UsageError("`--options-file` must contain a JSON object.", {
760
+ code: "invalid_options_file",
761
+ hint: "Use a JSON object like `{ \"limit\": 5 }`.",
762
+ });
763
+ }
764
+
765
+ return {
766
+ ...fileOptions,
767
+ ...optionValues,
768
+ };
769
+ }
770
+
771
+ function assertSingleStdinConsumer(optionValues) {
772
+ const optionsFile = readStringOption(optionValues, "options-file");
773
+ const idsFile = readStringOption(optionValues, "ids-file");
774
+ const stdinIds = readBooleanOption(optionValues, "stdin") === true;
775
+ const stdinConsumers = [optionsFile === "-", idsFile === "-", stdinIds].filter(Boolean);
776
+
777
+ if (stdinConsumers.length > 1) {
778
+ throw new UsageError("Only one stdin-consuming flag can be used at a time.", {
779
+ code: "conflicting_stdin_inputs",
780
+ hint: "Use either `--options-file -`, `--ids-file -`, or `--stdin`.",
781
+ });
782
+ }
783
+ }
784
+
785
+ function applyAgentInputOptions(sub, optionValues, options) {
786
+ if (sub !== "get") {
787
+ return options;
788
+ }
789
+
790
+ const idsFile = readStringOption(optionValues, "ids-file");
791
+ const stdinIds = readBooleanOption(optionValues, "stdin") === true;
792
+ if (!idsFile && !stdinIds) {
793
+ return options;
794
+ }
795
+
796
+ if (hasOwn(optionValues, "stdin") && readBooleanOption(optionValues, "stdin") !== true) {
797
+ return options;
798
+ }
799
+
800
+ return {
801
+ ...options,
802
+ idsFile: stdinIds ? "-" : idsFile,
803
+ };
804
+ }
805
+
806
+ function buildAuthSetupOptions(optionValues, globalFlags = {}) {
807
+ const options = {
808
+ clientId: readStringOption(optionValues, "client-id"),
809
+ clientSecret: readStringOption(optionValues, "client-secret"),
810
+ confirmConfidentiality: readBooleanOption(optionValues, "confirm-confidentiality") === true,
811
+ openBrowser: readBooleanOption(optionValues, "open-browser"),
812
+ redirectUri: readStringOption(optionValues, "redirect-uri"),
813
+ region: readStringOption(optionValues, "region"),
814
+ };
815
+ if (globalFlags.noInput) {
816
+ options.noInput = true;
817
+ }
818
+ return options;
819
+ }
820
+
821
+ function buildAuthRevokeOptions(optionValues, globalFlags = {}) {
822
+ return {
823
+ dryRun: readBooleanOption(optionValues, "dry-run") === true,
824
+ yes:
825
+ globalFlags.yes === true ||
826
+ readBooleanOption(optionValues, "yes") === true ||
827
+ readBooleanOption(optionValues, "force") === true,
828
+ };
829
+ }
830
+
831
+ async function run(args) {
832
+ const helpRequested = hasFlag(args, "-h", "--help");
833
+ const versionRequested = hasFlag(args, "-v", "--version");
834
+ const globalFlags = readGlobalFlags(args);
835
+ const routingArgs = stripRoutingFlags(args);
836
+
837
+ if (routingArgs.length === 0) {
838
+ if (versionRequested && !helpRequested) {
839
+ console.log(`not-manage v${version}`);
840
+ return;
841
+ }
842
+
843
+ printGlobalHelp();
844
+ return;
845
+ }
846
+
847
+ const command = normalizeResourceCommand(routingArgs[0]);
848
+ const sub = routingArgs[1];
849
+
850
+ if (helpRequested) {
851
+ printCommandSpecificHelp(command, sub);
149
852
  return;
150
853
  }
151
854
 
152
- const json = hasFlag(args, "--json");
153
- const command = normalizeResourceCommand(args[0]);
154
- const sub = args[1];
155
- const { parsed: optionValues, positional } = parseOptions(args.slice(2));
855
+ const json = globalFlags.json;
856
+ const { parsed, positional } = parseOptions(routingArgs.slice(2));
857
+ assertSingleStdinConsumer(parsed);
858
+ const optionValues = await mergeOptionsFile(parsed);
156
859
  const redacted = resolveRedactionPreference(optionValues);
157
860
 
158
861
  if (maybePrintDefaultFields(command, sub, optionValues)) {
@@ -160,47 +863,115 @@ async function run(args) {
160
863
  }
161
864
 
162
865
  if (command === "setup") {
163
- await setupWizard();
866
+ await setupWizard(buildAuthSetupOptions(optionValues, globalFlags));
164
867
  return;
165
868
  }
166
869
 
167
- if (command === "auth" && sub === "setup") {
168
- await authSetup();
169
- return;
870
+ if (command === "auth") {
871
+ if (!sub) {
872
+ printAuthHelp();
873
+ return;
874
+ }
875
+
876
+ if (sub === "setup") {
877
+ await authSetup(buildAuthSetupOptions(optionValues, globalFlags));
878
+ return;
879
+ }
880
+
881
+ if (sub === "login") {
882
+ await authLogin({ json });
883
+ return;
884
+ }
885
+
886
+ if (sub === "status") {
887
+ await authStatus({ json, redacted });
888
+ return;
889
+ }
890
+
891
+ if (sub === "revoke") {
892
+ await authRevoke(buildAuthRevokeOptions(optionValues, globalFlags));
893
+ return;
894
+ }
895
+
896
+ throw new UsageError(
897
+ `Unknown subcommand for auth: ${sub}\nRun \`not-manage auth --help\`.`,
898
+ { code: "unknown_subcommand", hint: "not-manage auth --help" }
899
+ );
170
900
  }
171
901
 
172
- if (command === "auth" && sub === "login") {
173
- await authLogin();
902
+ if (command === "whoami") {
903
+ await whoAmI({ json, redacted });
174
904
  return;
175
905
  }
176
906
 
177
- if (command === "auth" && sub === "status") {
178
- await authStatus({ json });
907
+ if (command === "doctor") {
908
+ await doctor({
909
+ compact: globalFlags.compact,
910
+ failOn: readStringOption(optionValues, "fail-on"),
911
+ json,
912
+ });
179
913
  return;
180
914
  }
181
915
 
182
- if (command === "auth" && sub === "revoke") {
183
- await authRevoke();
916
+ if (command === "agent-context") {
917
+ await agentContext({
918
+ compact: globalFlags.compact,
919
+ });
184
920
  return;
185
921
  }
186
922
 
187
- if (command === "whoami") {
188
- await whoAmI({ json });
923
+ if (command === "request") {
924
+ await rawRequest({
925
+ method: routingArgs[1],
926
+ path: routingArgs[2],
927
+ query: readStringOption(optionValues, "query"),
928
+ bodyFile: readStringOption(optionValues, "body-file"),
929
+ dryRun: readBooleanOption(optionValues, "dry-run") === true,
930
+ write: readBooleanOption(optionValues, "write") === true,
931
+ compact: globalFlags.compact,
932
+ json,
933
+ redacted,
934
+ });
189
935
  return;
190
936
  }
191
937
 
192
938
  const resourceMetadata = getResourceMetadata(command);
939
+ if (resourceMetadata && !sub) {
940
+ printResourceOverview(command, resourceMetadata);
941
+ return;
942
+ }
943
+
193
944
  const handler = getResourceHandler(resourceMetadata, sub);
194
945
 
195
946
  if (resourceMetadata && handler) {
196
- warnAboutRedaction(resourceMetadata, sub, optionValues, redacted);
197
- await handler(
198
- buildResourceOptions(resourceMetadata, sub, optionValues, positional, json, redacted)
947
+ const resourceOptions = buildResourceOptions(
948
+ resourceMetadata,
949
+ sub,
950
+ optionValues,
951
+ positional,
952
+ json,
953
+ redacted,
954
+ globalFlags.compact
199
955
  );
956
+ const resourceOptionsWithAgentInput = applyAgentInputOptions(sub, optionValues, resourceOptions);
957
+ assertRequiredResourceOptions(command, sub, resourceMetadata, resourceOptionsWithAgentInput);
958
+ assertResourceIdOption(command, sub, resourceOptionsWithAgentInput);
959
+ warnAboutRedaction(resourceMetadata, sub, optionValues, redacted);
960
+ await handler(resourceOptionsWithAgentInput);
200
961
  return;
201
962
  }
202
963
 
203
- throw new Error(`Unknown command: ${args.join(" ")}`);
964
+ if (resourceMetadata) {
965
+ throw new UsageError(
966
+ `Unknown subcommand for ${command}: ${sub}\nRun \`not-manage ${command} --help\`.`,
967
+ { code: "unknown_subcommand", hint: `not-manage ${command} --help` }
968
+ );
969
+ }
970
+
971
+ throw new UsageError(
972
+ `Unknown command: ${routingArgs.join(" ")}\nRun \`not-manage --help\`.`,
973
+ { code: "unknown_command", hint: "not-manage --help" }
974
+ );
204
975
  }
205
976
 
206
977
  module.exports = {