@odla-ai/cli 0.43.1 → 0.44.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/index.cjs CHANGED
@@ -1234,10 +1234,17 @@ function parseArgv(argv) {
1234
1234
  function assertArgs(parsed, allowedOptions2, maxPositionals) {
1235
1235
  const allowed = new Set(allowedOptions2);
1236
1236
  for (const name of Object.keys(parsed.options)) {
1237
- if (!allowed.has(name)) throw new Error(`unknown option "--${name}"; run "odla-ai help" for supported options`);
1237
+ if (allowed.has(name)) continue;
1238
+ const accepts = [...allowed].sort().map((option) => `--${option}`).join(" ");
1239
+ throw new Error(
1240
+ `unknown option "--${name}"` + (accepts ? ` \u2014 this command accepts: ${accepts}` : " \u2014 this command takes no options")
1241
+ );
1238
1242
  }
1239
1243
  if (parsed.positionals.length > maxPositionals) {
1240
- throw new Error(`unexpected argument "${parsed.positionals[maxPositionals]}"; run "odla-ai help"`);
1244
+ const taken = parsed.positionals.slice(0, maxPositionals).join(" ");
1245
+ throw new Error(
1246
+ `unexpected argument "${parsed.positionals[maxPositionals]}" \u2014 ` + (maxPositionals ? `"odla-ai ${taken}" takes no further arguments` : "this command takes no arguments")
1247
+ );
1241
1248
  }
1242
1249
  }
1243
1250
  function requiredString(value2, name) {
@@ -1275,6 +1282,198 @@ function addOption(options, name, value2) {
1275
1282
  else options[name] = [String(current), String(value2)];
1276
1283
  }
1277
1284
 
1285
+ // src/surface.ts
1286
+ var PM_ACTIONS = {
1287
+ list: {},
1288
+ add: {},
1289
+ create: {},
1290
+ get: {},
1291
+ set: {},
1292
+ update: {},
1293
+ status: {},
1294
+ move: {},
1295
+ done: {},
1296
+ comment: {},
1297
+ comments: {},
1298
+ history: {},
1299
+ link: {},
1300
+ ref: {},
1301
+ rm: {},
1302
+ delete: {}
1303
+ };
1304
+ var PM_TASK_ACTIONS = {
1305
+ ...PM_ACTIONS,
1306
+ ready: {},
1307
+ claim: {},
1308
+ release: {}
1309
+ };
1310
+ var PM_ENTITIES = {
1311
+ ...Object.fromEntries(
1312
+ ["goal", "conformance", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
1313
+ ),
1314
+ task: PM_TASK_ACTIONS,
1315
+ kanban: PM_TASK_ACTIONS
1316
+ };
1317
+ var COMMAND_SURFACE = {
1318
+ agent: { jobs: {}, retry: {} },
1319
+ ai: { models: {} },
1320
+ admin: {
1321
+ ai: {
1322
+ show: {},
1323
+ set: {},
1324
+ credentials: {},
1325
+ models: {},
1326
+ usage: {},
1327
+ audit: {},
1328
+ credential: { set: {} }
1329
+ },
1330
+ spend: { show: {}, reset: {} }
1331
+ },
1332
+ app: {
1333
+ archive: {},
1334
+ restore: {},
1335
+ export: {},
1336
+ import: {},
1337
+ rename: {},
1338
+ "refresh-sandbox": {},
1339
+ "go-live": {},
1340
+ promote: {},
1341
+ owners: { list: {}, add: {}, remove: {} }
1342
+ },
1343
+ auth: { login: {} },
1344
+ brand: { design: { unpack: {} } },
1345
+ bug: { create: {}, list: {}, report: {} },
1346
+ calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
1347
+ capabilities: {},
1348
+ code: {
1349
+ connect: {},
1350
+ grant: { request: {}, list: {}, approve: {}, revoke: {} },
1351
+ repository: { show: {}, list: {}, bind: {} }
1352
+ },
1353
+ config: { diff: {}, plan: {}, apply: {} },
1354
+ context: { show: {}, list: {}, save: {}, remove: {} },
1355
+ credentials: { list: {}, revoke: {} },
1356
+ device: { enroll: {}, list: {}, revoke: {} },
1357
+ // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
1358
+ discuss: {
1359
+ groups: {},
1360
+ list: {},
1361
+ topics: {},
1362
+ read: {},
1363
+ post: {},
1364
+ reply: {},
1365
+ resolve: {},
1366
+ who: {},
1367
+ watch: {}
1368
+ },
1369
+ doctor: {},
1370
+ help: {},
1371
+ init: {},
1372
+ monitor: { plan: {}, apply: {}, run: {}, status: {}, incidents: {}, report: {} },
1373
+ o11y: { status: {} },
1374
+ operations: { get: {}, wait: {} },
1375
+ platform: {
1376
+ status: {}
1377
+ },
1378
+ pm: {
1379
+ ...PM_ENTITIES,
1380
+ project: { list: {}, add: {}, create: {}, use: {} },
1381
+ handoff: {},
1382
+ next: {},
1383
+ start: {},
1384
+ watch: {}
1385
+ },
1386
+ provision: {},
1387
+ runbook: {
1388
+ ask: {},
1389
+ search: {},
1390
+ impact: {},
1391
+ list: {},
1392
+ get: {},
1393
+ cat: {},
1394
+ new: {},
1395
+ edit: {},
1396
+ comment: {},
1397
+ import: {},
1398
+ visibility: {},
1399
+ publish: {},
1400
+ archive: {},
1401
+ history: {},
1402
+ revert: {},
1403
+ rm: {},
1404
+ lint: {}
1405
+ },
1406
+ secrets: { push: {}, status: {}, set: {}, "set-clerk-key": {} },
1407
+ security: {
1408
+ plan: {},
1409
+ sources: {},
1410
+ run: {},
1411
+ status: {},
1412
+ report: {},
1413
+ github: { connect: {}, disconnect: {} }
1414
+ },
1415
+ setup: {},
1416
+ skill: { install: {} },
1417
+ smoke: {},
1418
+ version: {},
1419
+ whoami: {}
1420
+ };
1421
+ function acceptedAfter(path) {
1422
+ let node = COMMAND_SURFACE;
1423
+ for (const word of path) {
1424
+ node = node?.[word];
1425
+ if (!node) return [];
1426
+ }
1427
+ return Object.keys(node).sort();
1428
+ }
1429
+ function validateInvocation(words2) {
1430
+ let node = COMMAND_SURFACE;
1431
+ const walked = [];
1432
+ for (const word of words2) {
1433
+ if (Object.keys(node).length === 0) return null;
1434
+ const next = node[word];
1435
+ if (!next) return { validPrefix: walked.join(" "), word, accepted: Object.keys(node).sort() };
1436
+ walked.push(word);
1437
+ node = next;
1438
+ }
1439
+ return null;
1440
+ }
1441
+ function describeProblem(problem) {
1442
+ const where = problem.validPrefix ? `after "${problem.validPrefix}"` : "as a command";
1443
+ if (!problem.accepted.length) return `"${problem.word}" is not accepted ${where}. Run "odla-ai help"`;
1444
+ if (!problem.word) return `"odla-ai ${problem.validPrefix}" needs one of: ${problem.accepted.join(", ")}`;
1445
+ return `"${problem.word}" is not accepted ${where} \u2014 try: ${problem.accepted.join(", ")}`;
1446
+ }
1447
+ function rejectWord(path, word, note) {
1448
+ const sentence = describeProblem({
1449
+ validPrefix: path.join(" "),
1450
+ word: word ?? "",
1451
+ accepted: acceptedAfter(path)
1452
+ });
1453
+ throw new Error(note ? `${sentence}. ${note}` : sentence);
1454
+ }
1455
+ function invocationPath(words2) {
1456
+ let node = COMMAND_SURFACE;
1457
+ const path = [];
1458
+ for (const word of words2) {
1459
+ const next = node[word];
1460
+ if (!next) break;
1461
+ path.push(word);
1462
+ node = next;
1463
+ if (Object.keys(node).length === 0) break;
1464
+ }
1465
+ return path;
1466
+ }
1467
+ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
1468
+ const paths2 = [];
1469
+ for (const [word, child] of Object.entries(node)) {
1470
+ const path = [...prefix, word];
1471
+ paths2.push(path);
1472
+ paths2.push(...surfacePaths(child, path));
1473
+ }
1474
+ return paths2;
1475
+ }
1476
+
1278
1477
  // src/admin-spend.ts
1279
1478
  async function call(ctx, method, scope) {
1280
1479
  const url = `${ctx.platformUrl.replace(/\/$/, "")}/registry/platform/spend?scope=${encodeURIComponent(scope)}`;
@@ -1324,9 +1523,7 @@ async function spendReset(ctx, scope) {
1324
1523
  async function adminSpend(parsed, ctx) {
1325
1524
  const action2 = parsed.positionals[2];
1326
1525
  const scope = parsed.positionals[3] ?? stringOpt(parsed.options.scope);
1327
- if (action2 !== "show" && action2 !== "reset") {
1328
- throw new Error('unknown spend command. Try "odla-ai admin spend show <scope>".');
1329
- }
1526
+ if (action2 !== "show" && action2 !== "reset") rejectWord(["admin", "spend"], action2);
1330
1527
  if (!scope) {
1331
1528
  throw new Error(
1332
1529
  `"admin spend ${action2}" needs a scope, e.g. odla-ai admin spend ${action2} app:my-app:<incarnation>`
@@ -2125,6 +2322,11 @@ var SET_OPTIONS = [
2125
2322
  async function adminCommand(parsed, deps = {}) {
2126
2323
  const area = parsed.positionals[1];
2127
2324
  const action2 = parsed.positionals[2];
2325
+ if (area !== "ai" && area !== "spend") rejectWord(["admin"], area);
2326
+ if (!acceptedAfter(["admin", area]).includes(action2 ?? "")) rejectWord(["admin", area], action2);
2327
+ if (action2 === "credential" && !acceptedAfter(["admin", "ai", "credential"]).includes(parsed.positionals[3] ?? "")) {
2328
+ rejectWord(["admin", "ai", "credential"], parsed.positionals[3]);
2329
+ }
2128
2330
  if (area === "spend") {
2129
2331
  assertArgs(parsed, JSON_OPTIONS, 4);
2130
2332
  const context2 = await resolveOperatorContext(parsed, { allowMissingConfig: true });
@@ -2150,14 +2352,11 @@ async function adminCommand(parsed, deps = {}) {
2150
2352
  out
2151
2353
  });
2152
2354
  }
2153
- const credentialSet = action2 === "credential" && parsed.positionals[3] === "set";
2355
+ const credentialSet = action2 === "credential";
2154
2356
  const credentials = action2 === "credentials";
2155
2357
  const models = action2 === "models";
2156
2358
  const usage = action2 === "usage";
2157
2359
  const audit = action2 === "audit";
2158
- if (area !== "ai" || action2 !== "show" && action2 !== "set" && !credentialSet && !credentials && !models && !usage && !audit) {
2159
- throw new Error('unknown admin command. Try "odla-ai admin ai show".');
2160
- }
2161
2360
  const allowed = credentialSet ? [...CONTEXT_OPTIONS, "from-env", "stdin"] : action2 === "set" ? SET_OPTIONS : models ? [...JSON_OPTIONS, "provider"] : usage ? [...JSON_OPTIONS, "app-id", "env", "run-id", "limit"] : audit ? [...JSON_OPTIONS, "limit"] : JSON_OPTIONS;
2162
2361
  assertArgs(parsed, allowed, credentialSet ? 5 : action2 === "set" ? 4 : 3);
2163
2362
  const context = await resolveOperatorContext(parsed, { allowMissingConfig: true });
@@ -2371,9 +2570,7 @@ async function authCommand(parsed, deps = {}) {
2371
2570
  "json"
2372
2571
  ], 2);
2373
2572
  const action2 = parsed.positionals[1] ?? "login";
2374
- if (action2 !== "login") {
2375
- throw new Error(`unknown auth action "${action2}". Try "odla-ai auth login --app <id> --email <odla-account>".`);
2376
- }
2573
+ if (action2 !== "login") rejectWord(["auth"], action2);
2377
2574
  const context = await resolveOperatorContext(parsed, {
2378
2575
  allowMissingConfig: true,
2379
2576
  requireApp: true
@@ -2446,9 +2643,7 @@ function bothTenants(cfg) {
2446
2643
  // src/agent-command.ts
2447
2644
  async function agentCommand(parsed, deps = {}) {
2448
2645
  const action2 = parsed.positionals[1];
2449
- if (action2 !== "jobs" && action2 !== "retry") {
2450
- throw new Error(`unknown agent action "${action2 ?? ""}". Try "odla-ai agent jobs --json".`);
2451
- }
2646
+ if (action2 !== "jobs" && action2 !== "retry") rejectWord(["agent"], action2);
2452
2647
  assertArgs(parsed, ["config", "env", "state", "limit", "json", "token"], action2 === "jobs" ? 2 : 3);
2453
2648
  if (action2 === "retry" && (parsed.options.state !== void 0 || parsed.options.limit !== void 0)) {
2454
2649
  throw new Error('--state and --limit are supported only by "agent jobs"');
@@ -2619,9 +2814,7 @@ async function appOwnersCommand(parsed, dependencies = {}) {
2619
2814
  await (sub === "add" ? ownersAdd(email, options) : ownersRemove(email, options));
2620
2815
  return;
2621
2816
  }
2622
- throw new Error(
2623
- `unknown app owners subcommand "${sub}". Try "odla-ai app owners list", "odla-ai app owners add <email>", or "odla-ai app owners remove <email>".`
2624
- );
2817
+ rejectWord(["app", "owners"], sub);
2625
2818
  }
2626
2819
 
2627
2820
  // src/app-rename.ts
@@ -2727,8 +2920,10 @@ async function appCommand(parsed, dependencies = {}) {
2727
2920
  return;
2728
2921
  }
2729
2922
  if (sub !== "archive" && sub !== "restore") {
2730
- throw new Error(
2731
- `unknown app subcommand "${sub ?? ""}". Try "odla-ai app archive --yes", "odla-ai app restore", "odla-ai app export", "odla-ai app import <file>", "odla-ai app refresh-sandbox", "odla-ai app go-live", "odla-ai app promote", "odla-ai app rename <name>", or "odla-ai app owners <list|add|remove>". (Permanent deletion has no CLI: it requires a signed-in owner in Studio.)`
2923
+ rejectWord(
2924
+ ["app"],
2925
+ sub,
2926
+ "Permanent deletion has no CLI: it requires a signed-in owner in Studio."
2732
2927
  );
2733
2928
  }
2734
2929
  assertArgs(parsed, ["config", "token", "email", "yes", "json"], 2);
@@ -2889,7 +3084,8 @@ async function brandCommand(parsed, deps) {
2889
3084
  await designUnpack(parsed, deps);
2890
3085
  return;
2891
3086
  }
2892
- throw new Error(USAGE);
3087
+ if (subject !== "design") rejectWord(["brand"], subject);
3088
+ rejectWord(["brand", "design"], action2, USAGE);
2893
3089
  }
2894
3090
 
2895
3091
  // src/calendar-errors.ts
@@ -5750,9 +5946,7 @@ async function secretsCommand(parsed, deps) {
5750
5946
  return;
5751
5947
  }
5752
5948
  if (sub !== "push") {
5753
- throw new Error(
5754
- `unknown secrets subcommand "${sub ?? ""}". Try "odla-ai secrets push --env dev", "odla-ai secrets status --env dev", "odla-ai secrets set <name> --env dev --stdin", or "odla-ai secrets set-clerk-key --env dev --stdin".`
5755
- );
5949
+ rejectWord(["secrets"], sub);
5756
5950
  }
5757
5951
  assertArgs(parsed, ["config", "env", "dry-run", "yes"], 2);
5758
5952
  await secretsPush({
@@ -5765,7 +5959,7 @@ async function secretsCommand(parsed, deps) {
5765
5959
  async function projectCommand(command, parsed, deps) {
5766
5960
  if (command === "ai") {
5767
5961
  const sub = parsed.positionals[1];
5768
- if (sub !== "models") throw new Error(`unknown ai subcommand "${sub ?? ""}". Try "odla-ai ai models --env dev".`);
5962
+ if (sub !== "models") rejectWord(["ai"], sub);
5769
5963
  assertArgs(parsed, ["config", "env", "provider", "json"], 2);
5770
5964
  await aiModels({
5771
5965
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
@@ -5779,9 +5973,7 @@ async function projectCommand(command, parsed, deps) {
5779
5973
  }
5780
5974
  if (command === "config") {
5781
5975
  const sub = parsed.positionals[1];
5782
- if (sub !== "diff" && sub !== "plan" && sub !== "apply") {
5783
- throw new Error(`unknown config subcommand "${sub ?? ""}". Try "odla-ai config diff --json".`);
5784
- }
5976
+ if (sub !== "diff" && sub !== "plan" && sub !== "apply") rejectWord(["config"], sub);
5785
5977
  assertArgs(
5786
5978
  parsed,
5787
5979
  sub === "apply" ? ["config", "plan", "idempotency-key", "token", "email", "open", "json"] : ["config", "token", "email", "open", "json"],
@@ -5809,7 +6001,7 @@ async function projectCommand(command, parsed, deps) {
5809
6001
  if (command === "operations") {
5810
6002
  const sub = parsed.positionals[1];
5811
6003
  if (sub !== "get" && sub !== "wait") {
5812
- throw new Error(`unknown operations subcommand "${sub ?? ""}". Try "odla-ai operations get <operation-id> --json".`);
6004
+ rejectWord(["operations"], sub);
5813
6005
  }
5814
6006
  assertArgs(
5815
6007
  parsed,
@@ -5883,7 +6075,7 @@ async function projectCommand(command, parsed, deps) {
5883
6075
  }
5884
6076
  if (command === "skill") {
5885
6077
  const sub = parsed.positionals[1];
5886
- if (sub !== "install") throw new Error(`unknown skill subcommand "${sub ?? ""}". Try "odla-ai skill install".`);
6078
+ if (sub !== "install") rejectWord(["skill"], sub);
5887
6079
  install(parsed, 2, deps);
5888
6080
  return true;
5889
6081
  }
@@ -10441,9 +10633,7 @@ function grantsUrl(cfg, suffix = "") {
10441
10633
  async function codeGrantCommand(parsed, deps = {}) {
10442
10634
  const action2 = parsed.positionals[2];
10443
10635
  if (!isAction(action2)) {
10444
- throw new Error(
10445
- `unknown code grant action "${action2 ?? ""}". Try "odla-ai code grant list --env dev".`
10446
- );
10636
+ rejectWord(["code", "grant"], action2);
10447
10637
  }
10448
10638
  const decides = action2 === "approve" || action2 === "revoke";
10449
10639
  assertArgs(parsed, ["config", "env", "json", "token", "email", "open"], decides ? 4 : 3);
@@ -10563,9 +10753,7 @@ function repositoryUrl(cfg, suffix = "") {
10563
10753
  async function codeRepositoryCommand(parsed, deps = {}) {
10564
10754
  const action2 = parsed.positionals[2];
10565
10755
  if (!isAction2(action2)) {
10566
- throw new Error(
10567
- `unknown code repository action "${action2 ?? ""}". Try "odla-ai code repository show --env dev".`
10568
- );
10756
+ rejectWord(["code", "repository"], action2);
10569
10757
  }
10570
10758
  assertArgs(parsed, ["config", "env", "repo", "json", "token", "email", "open"], 3);
10571
10759
  const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
@@ -10640,9 +10828,7 @@ async function codeCommand(parsed, dependencies) {
10640
10828
  });
10641
10829
  }
10642
10830
  if (sub !== "connect") {
10643
- throw new Error(
10644
- `unknown code subcommand "${sub ?? ""}". Try "odla-ai code connect --env dev" or "odla-ai code grant list --env dev".`
10645
- );
10831
+ rejectWord(["code"], sub);
10646
10832
  }
10647
10833
  assertArgs(parsed, [
10648
10834
  "config",
@@ -10779,9 +10965,7 @@ async function contextCommand(parsed, deps = {}) {
10779
10965
  return;
10780
10966
  }
10781
10967
  if (action2 !== "show") {
10782
- throw new Error(
10783
- `unknown context action "${action2 ?? ""}". Try show|list|save|remove.`
10784
- );
10968
+ rejectWord(["context"], action2);
10785
10969
  }
10786
10970
  const context = await resolveOperatorContext(parsed, {
10787
10971
  allowMissingConfig: true,
@@ -10837,9 +11021,7 @@ async function responseError(response2) {
10837
11021
  }
10838
11022
  async function credentialCommand(parsed, deps = {}) {
10839
11023
  const action2 = parsed.positionals[1] ?? "list";
10840
- if (action2 !== "list" && action2 !== "revoke") {
10841
- throw new Error(`unknown credentials action "${action2}". Try "odla-ai credentials list".`);
10842
- }
11024
+ if (action2 !== "list" && action2 !== "revoke") rejectWord(["credentials"], action2);
10843
11025
  assertArgs(parsed, ["config", "env", "all", "json", "token", "email", "open"], action2 === "revoke" ? 3 : 2);
10844
11026
  const cfg = await loadProjectConfig(stringOpt(parsed.options.config) ?? "odla.config.mjs");
10845
11027
  const doFetch = deps.fetch ?? fetch;
@@ -11841,7 +12023,7 @@ async function discussCommand(parsed, deps = {}) {
11841
12023
  assertArgs(parsed, ALLOWED, 3);
11842
12024
  const action2 = parsed.positionals[1];
11843
12025
  const id2 = parsed.positionals[2];
11844
- if (!action2) throw new Error('"discuss" needs an action. Run "odla-ai help".');
12026
+ if (!acceptedAfter(["discuss"]).includes(action2 ?? "")) rejectWord(["discuss"], action2);
11845
12027
  const ctx = await buildContext(parsed, deps);
11846
12028
  switch (action2) {
11847
12029
  case "groups":
@@ -11865,7 +12047,7 @@ async function discussCommand(parsed, deps = {}) {
11865
12047
  return;
11866
12048
  }
11867
12049
  default:
11868
- throw new Error(`unknown discuss action "${action2}". Run "odla-ai help".`);
12050
+ rejectWord(["discuss"], action2);
11869
12051
  }
11870
12052
  }
11871
12053
 
@@ -12775,7 +12957,7 @@ async function pmCommand(parsed, deps = {}) {
12775
12957
  assertArgs(parsed, COMMON_OPTIONS, 4);
12776
12958
  return pmProjectUse(await buildContext2(parsed, deps), requireId2(parsed.positionals[3], action3));
12777
12959
  }
12778
- throw new Error(`unknown pm project action "${action3}". Try list|add|use.`);
12960
+ rejectWord(["pm", "project"], action3);
12779
12961
  }
12780
12962
  if (word === "next") {
12781
12963
  assertArgs(parsed, [...COMMON_OPTIONS, "app", "project", "verbose"], 2);
@@ -12806,10 +12988,10 @@ async function pmCommand(parsed, deps = {}) {
12806
12988
  return pmHandoff(await buildContext2(parsed, deps), parsed);
12807
12989
  }
12808
12990
  const entity = ALIASES[word];
12809
- if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm start" to claim Ready work, or "odla-ai pm bug list" (goal|task|decision|bug).`);
12991
+ if (!entity) rejectWord(["pm"], word);
12810
12992
  const requestedAction = parsed.positionals[2] ?? "list";
12811
12993
  const action2 = canonicalAction(requestedAction);
12812
- if (!action2) throw new Error(`unknown pm action "${requestedAction}". Try list|add|get|set|done|link|ref|comment|comments|history|rm.`);
12994
+ if (!action2) rejectWord(["pm", word], requestedAction);
12813
12995
  assertArgs(parsed, allowedOptions(entity, action2), 4);
12814
12996
  if ((action2 === "ready" || action2 === "claim" || action2 === "release") && entity !== "task") {
12815
12997
  throw new Error(`pm ${action2} is only valid for tasks`);
@@ -12895,12 +13077,7 @@ async function platformCommand(parsed, deps = {}) {
12895
13077
  if (action2 === "status") {
12896
13078
  return platformStatus(parsed, deps);
12897
13079
  }
12898
- throw new Error(
12899
- `unknown platform action "${[
12900
- action2,
12901
- parsed.positionals[2]
12902
- ].filter(Boolean).join(" ")}". Try "odla-ai platform status --json".`
12903
- );
13080
+ rejectWord(["platform"], action2);
12904
13081
  }
12905
13082
  async function platformStatus(parsed, deps) {
12906
13083
  assertArgs(
@@ -13237,9 +13414,7 @@ async function o11yCommand(parsed, deps = {}) {
13237
13414
  );
13238
13415
  const action2 = parsed.positionals[1];
13239
13416
  if (action2 !== "status") {
13240
- throw new Error(
13241
- `unknown o11y action "${action2 ?? ""}". Try "odla-ai o11y status --json".`
13242
- );
13417
+ rejectWord(["o11y"], action2);
13243
13418
  }
13244
13419
  const minutes = statusMinutes(
13245
13420
  numberOpt(parsed.options.minutes, "--minutes") ?? 60
@@ -13492,9 +13667,7 @@ var OPTIONS = [
13492
13667
  async function monitorCommand(parsed, deps = {}) {
13493
13668
  assertArgs(parsed, OPTIONS, 3);
13494
13669
  const action2 = parsed.positionals[1] ?? "status";
13495
- if (!["plan", "apply", "run", "status", "incidents", "report"].includes(action2)) {
13496
- throw new Error(`unknown monitor action "${action2}". Try "odla-ai monitor status --json".`);
13497
- }
13670
+ if (!acceptedAfter(["monitor"]).includes(action2)) rejectWord(["monitor"], action2);
13498
13671
  const context = await resolveOperatorContext(parsed, {
13499
13672
  allowMissingConfig: action2 !== "plan" && action2 !== "apply",
13500
13673
  requireApp: true
@@ -14124,187 +14297,6 @@ async function provision(options) {
14124
14297
  // src/record.ts
14125
14298
  var import_node_fs18 = require("fs");
14126
14299
  var import_node_process18 = __toESM(require("process"), 1);
14127
-
14128
- // src/surface.ts
14129
- var PM_ACTIONS = {
14130
- list: {},
14131
- add: {},
14132
- create: {},
14133
- get: {},
14134
- set: {},
14135
- update: {},
14136
- status: {},
14137
- move: {},
14138
- done: {},
14139
- comment: {},
14140
- comments: {},
14141
- ref: {},
14142
- rm: {},
14143
- delete: {}
14144
- };
14145
- var PM_TASK_ACTIONS = {
14146
- ...PM_ACTIONS,
14147
- ready: {},
14148
- claim: {},
14149
- release: {}
14150
- };
14151
- var PM_ENTITIES = {
14152
- ...Object.fromEntries(
14153
- ["goal", "conformance", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
14154
- ),
14155
- task: PM_TASK_ACTIONS,
14156
- kanban: PM_TASK_ACTIONS
14157
- };
14158
- var COMMAND_SURFACE = {
14159
- agent: { jobs: {}, retry: {} },
14160
- ai: { models: {} },
14161
- admin: {
14162
- ai: {
14163
- show: {},
14164
- set: {},
14165
- credentials: {},
14166
- models: {},
14167
- usage: {},
14168
- audit: {},
14169
- credential: { set: {} }
14170
- }
14171
- },
14172
- app: {
14173
- archive: {},
14174
- restore: {},
14175
- export: {},
14176
- import: {},
14177
- rename: {},
14178
- "refresh-sandbox": {},
14179
- "go-live": {},
14180
- promote: {},
14181
- owners: { list: {}, add: {}, remove: {} }
14182
- },
14183
- auth: { login: {} },
14184
- brand: { design: { unpack: {} } },
14185
- bug: { create: {}, list: {}, report: {} },
14186
- calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
14187
- capabilities: {},
14188
- code: {
14189
- connect: {},
14190
- grant: { request: {}, list: {}, approve: {}, revoke: {} },
14191
- repository: { show: {}, list: {}, bind: {} }
14192
- },
14193
- config: { diff: {}, plan: {}, apply: {} },
14194
- context: { show: {}, list: {}, save: {}, remove: {} },
14195
- credentials: { list: {}, revoke: {} },
14196
- device: { enroll: {}, list: {}, revoke: {} },
14197
- // `watch`, `read`, `reply`, and `resolve` take a topic id from there on.
14198
- discuss: {
14199
- groups: {},
14200
- list: {},
14201
- topics: {},
14202
- read: {},
14203
- post: {},
14204
- reply: {},
14205
- resolve: {},
14206
- who: {},
14207
- watch: {}
14208
- },
14209
- doctor: {},
14210
- help: {},
14211
- init: {},
14212
- monitor: { plan: {}, apply: {}, run: {}, status: {}, incidents: {}, report: {} },
14213
- o11y: { status: {} },
14214
- operations: { get: {}, wait: {} },
14215
- platform: {
14216
- status: {}
14217
- },
14218
- pm: {
14219
- ...PM_ENTITIES,
14220
- project: { list: {}, add: {}, create: {}, use: {} },
14221
- handoff: {},
14222
- next: {},
14223
- start: {},
14224
- watch: {}
14225
- },
14226
- provision: {},
14227
- runbook: {
14228
- ask: {},
14229
- search: {},
14230
- impact: {},
14231
- list: {},
14232
- get: {},
14233
- cat: {},
14234
- new: {},
14235
- edit: {},
14236
- comment: {},
14237
- import: {},
14238
- visibility: {},
14239
- publish: {},
14240
- archive: {},
14241
- history: {},
14242
- revert: {},
14243
- rm: {},
14244
- lint: {}
14245
- },
14246
- secrets: { push: {}, status: {}, set: {}, "set-clerk-key": {} },
14247
- security: {
14248
- plan: {},
14249
- sources: {},
14250
- run: {},
14251
- status: {},
14252
- report: {},
14253
- github: { connect: {}, disconnect: {} }
14254
- },
14255
- setup: {},
14256
- skill: { install: {} },
14257
- smoke: {},
14258
- version: {},
14259
- whoami: {}
14260
- };
14261
- function acceptedAfter(path) {
14262
- let node = COMMAND_SURFACE;
14263
- for (const word of path) {
14264
- node = node?.[word];
14265
- if (!node) return [];
14266
- }
14267
- return Object.keys(node).sort();
14268
- }
14269
- function validateInvocation(words2) {
14270
- let node = COMMAND_SURFACE;
14271
- const walked = [];
14272
- for (const word of words2) {
14273
- if (Object.keys(node).length === 0) return null;
14274
- const next = node[word];
14275
- if (!next) return { validPrefix: walked.join(" "), word, accepted: Object.keys(node).sort() };
14276
- walked.push(word);
14277
- node = next;
14278
- }
14279
- return null;
14280
- }
14281
- function describeProblem(problem) {
14282
- const where = problem.validPrefix ? `after "${problem.validPrefix}"` : "as a command";
14283
- return `"${problem.word}" is not accepted ${where} \u2014 try: ${problem.accepted.join(", ")}`;
14284
- }
14285
- function invocationPath(words2) {
14286
- let node = COMMAND_SURFACE;
14287
- const path = [];
14288
- for (const word of words2) {
14289
- const next = node[word];
14290
- if (!next) break;
14291
- path.push(word);
14292
- node = next;
14293
- if (Object.keys(node).length === 0) break;
14294
- }
14295
- return path;
14296
- }
14297
- function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
14298
- const paths2 = [];
14299
- for (const [word, child] of Object.entries(node)) {
14300
- const path = [...prefix, word];
14301
- paths2.push(path);
14302
- paths2.push(...surfacePaths(child, path));
14303
- }
14304
- return paths2;
14305
- }
14306
-
14307
- // src/record.ts
14308
14300
  function recordInvocation(parsed) {
14309
14301
  const file = import_node_process18.default.env.ODLA_CLI_RECORD;
14310
14302
  if (!file) return;
@@ -14393,6 +14385,7 @@ async function deviceCommand(parsed, deps) {
14393
14385
  "wait"
14394
14386
  ], 3);
14395
14387
  const action2 = parsed.positionals[1] ?? "";
14388
+ if (!acceptedAfter(["device"]).includes(action2)) rejectWord(["device"], action2);
14396
14389
  const out = deps.stdout ?? console;
14397
14390
  const doFetch = deps.fetch ?? fetch;
14398
14391
  const cfg = await loadProjectConfig(stringOpt(parsed.options.config));
@@ -14400,7 +14393,7 @@ async function deviceCommand(parsed, deps) {
14400
14393
  if (action2 === "enroll") return enroll(parsed, deps, cfg, doFetch, out, json);
14401
14394
  if (action2 === "list") return list2(parsed, deps, cfg, doFetch, out, json);
14402
14395
  if (action2 === "revoke") return revoke(parsed, deps, cfg, doFetch, out, json);
14403
- throw new Error('odla-ai device expects "enroll", "list", or "revoke"');
14396
+ rejectWord(["device"], action2);
14404
14397
  }
14405
14398
  async function enroll(parsed, deps, cfg, doFetch, out, json) {
14406
14399
  const name = stringOpt(parsed.options.name) ?? defaultDeviceName();
@@ -15347,6 +15340,7 @@ async function buildContext3(parsed, deps, action2) {
15347
15340
  async function runbookCommand(parsed, deps = {}) {
15348
15341
  const action2 = parsed.positionals[1] ?? "list";
15349
15342
  assertArgs(parsed, ALLOWED2, action2 === "ask" || action2 === "search" ? 64 : 4);
15343
+ if (!acceptedAfter(["runbook"]).includes(action2)) rejectWord(["runbook"], action2);
15350
15344
  const ctx = await buildContext3(parsed, deps, action2);
15351
15345
  const slug = parsed.positionals[2];
15352
15346
  switch (action2) {
@@ -15436,7 +15430,7 @@ async function runbookCommand(parsed, deps = {}) {
15436
15430
  case "rm":
15437
15431
  return runbookRemove(ctx, requireSlug(slug, "rm"));
15438
15432
  default:
15439
- throw new Error(`unknown runbook action "${action2}". Try ${acceptedAfter(["runbook"]).join(", ")}.`);
15433
+ rejectWord(["runbook"], action2);
15440
15434
  }
15441
15435
  }
15442
15436
 
@@ -16050,9 +16044,7 @@ async function securityCommand(parsed, dependencies) {
16050
16044
  else printHostedReport(context.stdout, report5);
16051
16045
  return;
16052
16046
  }
16053
- if (sub !== "run") {
16054
- throw new Error('unknown security command. Try "odla-ai security plan", "security sources", or "security run".');
16055
- }
16047
+ if (sub !== "run") rejectWord(["security"], sub);
16056
16048
  const sourceId = stringOpt(parsed.options.source);
16057
16049
  if (sourceId) await runSourceSecurityCommand(parsed, dependencies, sourceId);
16058
16050
  else await runLocalSecurityCommand(parsed, dependencies);
@@ -16069,9 +16061,7 @@ async function githubSecurityCommand(parsed, dependencies) {
16069
16061
  stringOpt(parsed.options.env)
16070
16062
  );
16071
16063
  }
16072
- if (action2 !== "connect") {
16073
- throw new Error('unknown security github command. Try "odla-ai security github connect".');
16074
- }
16064
+ if (action2 !== "connect") rejectWord(["security", "github"], action2);
16075
16065
  assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
16076
16066
  await requireStudioHuman(
16077
16067
  stringOpt(parsed.options.config) ?? "odla.config.mjs",
@@ -16225,6 +16215,7 @@ async function dispatchCli(argv, dependencies) {
16225
16215
  }
16226
16216
  if (command === "bug") {
16227
16217
  const action2 = parsed.positionals[1] ?? "list";
16218
+ if (!acceptedAfter(["bug"]).includes(action2)) rejectWord(["bug"], action2);
16228
16219
  const canonical2 = action2 === "report" || action2 === "create" ? "add" : action2;
16229
16220
  await pmCommand({
16230
16221
  ...parsed,
@@ -16253,7 +16244,7 @@ async function dispatchCli(argv, dependencies) {
16253
16244
  return;
16254
16245
  }
16255
16246
  if (await projectCommand(command, parsed, runtime)) return;
16256
- throw new Error(`unknown command "${command}". Run "odla-ai help".`);
16247
+ rejectWord([], command);
16257
16248
  }
16258
16249
  async function provisionCommand(parsed, dependencies) {
16259
16250
  assertArgs(parsed, [
@@ -16297,7 +16288,7 @@ async function provisionCommand(parsed, dependencies) {
16297
16288
  async function calendarCommand(parsed, dependencies) {
16298
16289
  const sub = parsed.positionals[1];
16299
16290
  if (sub !== "status" && sub !== "calendars" && sub !== "connect" && sub !== "disconnect") {
16300
- throw new Error(`unknown calendar subcommand "${sub ?? ""}". Try "odla-ai calendar status --env dev".`);
16291
+ rejectWord(["calendar"], sub);
16301
16292
  }
16302
16293
  assertArgs(parsed, ["config", "env", "json", "token", "email", "open", "yes"], 2);
16303
16294
  if (sub !== "status" && sub !== "calendars" && parsed.options.json !== void 0) throw new Error(`--json is supported only by calendar status/calendars`);