@odla-ai/cli 0.18.0 → 0.20.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
@@ -35,8 +35,10 @@ __export(index_exports, {
35
35
  CAPABILITIES: () => CAPABILITIES,
36
36
  CODE_BUILD_RECIPES: () => CODE_BUILD_RECIPES,
37
37
  CODE_PI_IMAGE: () => CODE_PI_IMAGE,
38
+ COMMAND_SURFACE: () => COMMAND_SURFACE,
38
39
  GOOGLE_CALENDAR_EVENTS_SCOPE: () => GOOGLE_CALENDAR_EVENTS_SCOPE,
39
40
  SYSTEM_AI_PURPOSES: () => SYSTEM_AI_PURPOSES,
41
+ acceptedAfter: () => acceptedAfter,
40
42
  adminAi: () => adminAi,
41
43
  calendarBookingPageUrl: () => calendarBookingPageUrl,
42
44
  calendarCalendars: () => calendarCalendars,
@@ -46,6 +48,7 @@ __export(index_exports, {
46
48
  calendarStatus: () => calendarStatus,
47
49
  codeConnect: () => codeConnect,
48
50
  connectGitHubSecuritySource: () => connectGitHubSecuritySource,
51
+ describeProblem: () => describeProblem,
49
52
  disconnectGitHubSecuritySource: () => disconnectGitHubSecuritySource,
50
53
  doctor: () => doctor,
51
54
  exitCodeFor: () => exitCodeFor,
@@ -73,7 +76,9 @@ __export(index_exports, {
73
76
  secretsSet: () => secretsSet,
74
77
  secretsSetClerkKey: () => secretsSetClerkKey,
75
78
  smoke: () => smoke,
76
- startHostedSecurityJob: () => startHostedSecurityJob
79
+ startHostedSecurityJob: () => startHostedSecurityJob,
80
+ surfacePaths: () => surfacePaths,
81
+ validateInvocation: () => validateInvocation
77
82
  });
78
83
  module.exports = __toCommonJS(index_exports);
79
84
 
@@ -6489,11 +6494,12 @@ Usage:
6489
6494
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
6490
6495
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
6491
6496
  odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
6497
+ odla-ai runbook lint [--app <id>] [--all] [--json]
6492
6498
  odla-ai runbook list [--app <id>] [--all] [--q <text>] [--json]
6493
6499
  odla-ai runbook comment <slug> --body "..." [--app <id>]
6494
6500
  odla-ai runbook get <slug> [--app <id>]
6495
- odla-ai runbook new <slug> --title <t> --file <path|-> [--summary <s>] [--app <id>]
6496
- odla-ai runbook edit <slug> [--file <path|->|--body "..."] [--note <why>] [--app <id>]
6501
+ odla-ai runbook new <slug> --title <t> --file <path|-> [--summary <s>] [--requires <specs>] [--app <id>]
6502
+ odla-ai runbook edit <slug> [--file <path|->|--body "..."] [--note <why>] [--requires <specs>] [--app <id>]
6497
6503
  odla-ai runbook import <dir> [--visibility operator|admin] [--dry-run] [--app <id>]
6498
6504
  odla-ai runbook publish <slug> [--app <id>]
6499
6505
  odla-ai runbook visibility <slug> <operator|admin> [--app <id>]
@@ -6535,7 +6541,10 @@ Commands:
6535
6541
  "search" the passages behind it; "get" the whole document.
6536
6542
  "impact" diffs your working tree against a base ref and names the
6537
6543
  runbooks that describe what you changed \u2014 run it after touching a
6538
- package's exported API or JSDoc, or a Studio surface. A wrong step
6544
+ package's exported API or JSDoc, or a Studio surface. "lint"
6545
+ checks the corpus the other way: every odla-ai command the
6546
+ runbooks name is held against this CLI's real command surface,
6547
+ and against the minimum versions each runbook declares. A wrong step
6539
6548
  is fixed with "edit", which takes effect immediately: a runbook is
6540
6549
  a row, not a release. Runbooks describe PROCEDURE; what an export
6541
6550
  does and what it guarantees is JSDoc, rendered per package at
@@ -7856,6 +7865,55 @@ var import_node_process9 = __toESM(require("process"), 1);
7856
7865
 
7857
7866
  // src/runbook-actions.ts
7858
7867
  var import_node_fs13 = require("fs");
7868
+
7869
+ // src/runbook-requires.ts
7870
+ var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
7871
+ function parseRequires(value) {
7872
+ if (!value) return [];
7873
+ const out = [];
7874
+ for (const token of value.split(/[\s,]+/).filter(Boolean)) {
7875
+ const match = SPEC.exec(token);
7876
+ if (match?.[1] && match[2]) out.push({ name: match[1], min: match[2] });
7877
+ }
7878
+ return out;
7879
+ }
7880
+ function compareVersions(a, b) {
7881
+ const parts = (v) => {
7882
+ const [core = "", pre = ""] = v.split("-", 2);
7883
+ return { nums: core.split(".").map((n) => Number.parseInt(n, 10) || 0), pre };
7884
+ };
7885
+ const left = parts(a);
7886
+ const right = parts(b);
7887
+ for (let i = 0; i < Math.max(left.nums.length, right.nums.length); i++) {
7888
+ const diff = (left.nums[i] ?? 0) - (right.nums[i] ?? 0);
7889
+ if (diff) return diff < 0 ? -1 : 1;
7890
+ }
7891
+ if (left.pre === right.pre) return 0;
7892
+ if (!left.pre) return 1;
7893
+ if (!right.pre) return -1;
7894
+ return left.pre < right.pre ? -1 : 1;
7895
+ }
7896
+ function satisfies(installed, min) {
7897
+ return compareVersions(installed, min) >= 0;
7898
+ }
7899
+ function unmetRequirements(requires, installedVersions) {
7900
+ const unmet = [];
7901
+ for (const requirement of parseRequires(requires)) {
7902
+ const installed = installedVersions[requirement.name];
7903
+ if (installed === void 0) {
7904
+ unmet.push({ ...requirement, installed: null });
7905
+ } else if (!satisfies(installed, requirement.min)) {
7906
+ unmet.push({ ...requirement, installed });
7907
+ }
7908
+ }
7909
+ return unmet;
7910
+ }
7911
+ function describeUnmet(slug, unmet) {
7912
+ const detail = unmet.map((u) => `${u.name}@${u.min}${u.installed ? ` (you have ${u.installed})` : " (not installed here)"}`).join(", ");
7913
+ return `note: runbook "${slug}" expects ${detail} \u2014 some steps may name commands your version does not have.`;
7914
+ }
7915
+
7916
+ // src/runbook-actions.ts
7859
7917
  var PLATFORM_SCOPE = "$platform";
7860
7918
  async function call(ctx, method, path, body) {
7861
7919
  const res = await ctx.doFetch(`${ctx.platformUrl.replace(/\/$/, "")}/registry/pm${path}`, {
@@ -7910,19 +7968,23 @@ async function runbookList(ctx, all, query) {
7910
7968
  async function runbookGet(ctx, slug) {
7911
7969
  const runbook = await bySlug(ctx, slug);
7912
7970
  if (ctx.json) return ctx.out.log(JSON.stringify(runbook, null, 2));
7971
+ const unmet = unmetRequirements(runbook.requires, { "@odla-ai/cli": cliVersion() });
7972
+ if (unmet.length) ctx.out.error(describeUnmet(slug, unmet));
7913
7973
  ctx.out.log(runbook.body);
7914
7974
  }
7915
- async function runbookNew(ctx, slug, title, body, summary) {
7975
+ async function runbookNew(ctx, slug, title, body, summary, requires) {
7916
7976
  const created = await call(ctx, "POST", "/runbook", {
7917
7977
  appId: ctx.appId,
7918
- input: { slug, title, body, ...summary ? { summary } : {} }
7978
+ input: { slug, title, body, ...summary ? { summary } : {}, ...requires ? { requires } : {} }
7919
7979
  });
7920
7980
  ctx.out.log(ctx.json ? JSON.stringify(created, null, 2) : `created ${slug} (${created.id})`);
7921
7981
  }
7922
- async function runbookEdit(ctx, slug, body, note) {
7982
+ async function runbookEdit(ctx, slug, body, note, requires) {
7923
7983
  const runbook = await bySlug(ctx, slug);
7924
7984
  const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
7925
- patch: { body, ...note ? { note } : {} }
7985
+ // An empty --requires clears the declaration; omitting the flag leaves
7986
+ // whatever is there, so an ordinary body edit never drops it.
7987
+ patch: { body, ...note ? { note } : {}, ...requires === void 0 ? {} : { requires: requires || null } }
7926
7988
  });
7927
7989
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
7928
7990
  ctx.out.log(`${slug} \u2192 v${result.record?.version ?? runbook.version + 1}`);
@@ -8329,6 +8391,179 @@ async function runbookImpact(ctx, options, deps = {}) {
8329
8391
  report2(ctx, impacts);
8330
8392
  }
8331
8393
 
8394
+ // src/surface.ts
8395
+ var PM_ACTIONS = {
8396
+ list: {},
8397
+ add: {},
8398
+ create: {},
8399
+ get: {},
8400
+ set: {},
8401
+ update: {},
8402
+ status: {},
8403
+ move: {},
8404
+ done: {},
8405
+ comment: {},
8406
+ comments: {},
8407
+ rm: {},
8408
+ delete: {}
8409
+ };
8410
+ var PM_ENTITIES = Object.fromEntries(
8411
+ ["goal", "conformance", "task", "kanban", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
8412
+ );
8413
+ var COMMAND_SURFACE = {
8414
+ admin: {
8415
+ ai: {
8416
+ show: {},
8417
+ set: {},
8418
+ credentials: {},
8419
+ models: {},
8420
+ usage: {},
8421
+ audit: {},
8422
+ credential: { set: {} }
8423
+ }
8424
+ },
8425
+ app: {
8426
+ archive: {},
8427
+ restore: {},
8428
+ export: {},
8429
+ import: {},
8430
+ rename: {},
8431
+ "refresh-sandbox": {},
8432
+ "go-live": {},
8433
+ promote: {},
8434
+ owners: { list: {}, add: {}, remove: {} }
8435
+ },
8436
+ calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
8437
+ capabilities: {},
8438
+ code: { connect: {} },
8439
+ doctor: {},
8440
+ help: {},
8441
+ init: {},
8442
+ pm: PM_ENTITIES,
8443
+ provision: {},
8444
+ runbook: {
8445
+ ask: {},
8446
+ search: {},
8447
+ impact: {},
8448
+ list: {},
8449
+ get: {},
8450
+ cat: {},
8451
+ new: {},
8452
+ edit: {},
8453
+ comment: {},
8454
+ import: {},
8455
+ visibility: {},
8456
+ publish: {},
8457
+ archive: {},
8458
+ history: {},
8459
+ revert: {},
8460
+ rm: {},
8461
+ lint: {}
8462
+ },
8463
+ secrets: { push: {}, set: {}, "set-clerk-key": {} },
8464
+ security: {
8465
+ plan: {},
8466
+ sources: {},
8467
+ run: {},
8468
+ status: {},
8469
+ report: {},
8470
+ github: { connect: {}, disconnect: {} }
8471
+ },
8472
+ setup: {},
8473
+ skill: { install: {} },
8474
+ smoke: {},
8475
+ version: {},
8476
+ whoami: {}
8477
+ };
8478
+ function acceptedAfter(path) {
8479
+ let node = COMMAND_SURFACE;
8480
+ for (const word of path) {
8481
+ node = node?.[word];
8482
+ if (!node) return [];
8483
+ }
8484
+ return Object.keys(node).sort();
8485
+ }
8486
+ function validateInvocation(words2) {
8487
+ let node = COMMAND_SURFACE;
8488
+ const walked = [];
8489
+ for (const word of words2) {
8490
+ if (Object.keys(node).length === 0) return null;
8491
+ const next = node[word];
8492
+ if (!next) return { validPrefix: walked.join(" "), word, accepted: Object.keys(node).sort() };
8493
+ walked.push(word);
8494
+ node = next;
8495
+ }
8496
+ return null;
8497
+ }
8498
+ function describeProblem(problem) {
8499
+ const where = problem.validPrefix ? `after "${problem.validPrefix}"` : "as a command";
8500
+ return `"${problem.word}" is not accepted ${where} \u2014 try: ${problem.accepted.join(", ")}`;
8501
+ }
8502
+ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
8503
+ const paths = [];
8504
+ for (const [word, child] of Object.entries(node)) {
8505
+ const path = [...prefix, word];
8506
+ paths.push(path);
8507
+ paths.push(...surfacePaths(child, path));
8508
+ }
8509
+ return paths;
8510
+ }
8511
+
8512
+ // src/runbook-lint.ts
8513
+ function invocationsIn(body) {
8514
+ const re = /(`|npx[^\S\n]+)?(?:@odla-ai\/cli(?:@[\w.-]+)?|odla-ai)((?:[^\S\n]+[a-z][\w-]*)+)/g;
8515
+ const found = [];
8516
+ for (const match of body.matchAll(re)) {
8517
+ if (!match[1]) continue;
8518
+ const words2 = match[2].trim().split(/[^\S\n]+/);
8519
+ if (words2.length) found.push(words2);
8520
+ }
8521
+ return found;
8522
+ }
8523
+ function lintRunbook(runbook, installed) {
8524
+ const findings = [];
8525
+ const at = { slug: runbook.slug, appId: runbook.appId };
8526
+ const declared = parseRequires(runbook.requires);
8527
+ const seen = /* @__PURE__ */ new Set();
8528
+ for (const words2 of invocationsIn(runbook.body)) {
8529
+ const problem = validateInvocation(words2);
8530
+ if (!problem) continue;
8531
+ const key = `${problem.validPrefix}|${problem.word}`;
8532
+ if (seen.has(key)) continue;
8533
+ seen.add(key);
8534
+ findings.push({
8535
+ ...at,
8536
+ kind: declared.length ? "command" : "undeclared",
8537
+ detail: declared.length ? `\`odla-ai ${words2.join(" ")}\`: ${describeProblem(problem)}` : `\`odla-ai ${words2.join(" ")}\`: ${describeProblem(problem)}. If a newer package added it, declare that with requires.`
8538
+ });
8539
+ }
8540
+ const unmet = unmetRequirements(runbook.requires, installed);
8541
+ if (unmet.length) findings.push({ ...at, kind: "requires", detail: describeUnmet(runbook.slug, unmet) });
8542
+ return findings;
8543
+ }
8544
+ async function runbookLint(ctx, all) {
8545
+ const params = new URLSearchParams();
8546
+ if (!all) params.set("app", ctx.appId);
8547
+ const page = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
8548
+ const installed = { "@odla-ai/cli": cliVersion() };
8549
+ const findings = page.records.flatMap((runbook) => lintRunbook(runbook, installed));
8550
+ if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page.records.length, findings }, null, 2));
8551
+ if (!page.records.length) return ctx.out.log("(no runbooks in scope)");
8552
+ if (!findings.length) {
8553
+ return ctx.out.log(
8554
+ `${page.records.length} runbook${page.records.length === 1 ? "" : "s"} checked; every command they name is real for @odla-ai/cli ${cliVersion()}.`
8555
+ );
8556
+ }
8557
+ ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${page.records.length} runbooks:`);
8558
+ for (const finding of findings) {
8559
+ const scope = finding.appId === PLATFORM_SCOPE ? "" : ` --app ${finding.appId}`;
8560
+ ctx.out.log("");
8561
+ ctx.out.log(`${finding.slug} [${finding.kind}]`);
8562
+ ctx.out.log(` ${finding.detail}`);
8563
+ ctx.out.log(` odla-ai runbook get ${finding.slug}${scope}`);
8564
+ }
8565
+ }
8566
+
8332
8567
  // src/runbook-search-command.ts
8333
8568
  async function runbookSearch(ctx, query, all, limit) {
8334
8569
  const params = new URLSearchParams({ q: query });
@@ -8521,14 +8756,15 @@ var ALLOWED2 = [
8521
8756
  "visibility",
8522
8757
  "dry-run",
8523
8758
  "limit",
8524
- "base"
8759
+ "base",
8760
+ "requires"
8525
8761
  ];
8526
8762
  function requireSlug(slug, action) {
8527
8763
  if (!slug) throw new Error(`"runbook ${action}" needs a slug, e.g. "odla-ai runbook ${action} release"`);
8528
8764
  return slug;
8529
8765
  }
8530
8766
  var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
8531
- var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact"]);
8767
+ var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact", "lint"]);
8532
8768
  async function loadOrDefaultConfig(configPath, action, appId) {
8533
8769
  const projectScoped = appId !== void 0;
8534
8770
  if (!projectScoped && CONFIG_FREE.has(action) && !(0, import_node_fs17.existsSync)((0, import_node_path14.resolve)(configPath))) {
@@ -8610,6 +8846,8 @@ async function runbookCommand(parsed, deps = {}) {
8610
8846
  if (!question) throw new Error('"runbook ask" needs a question, e.g. odla-ai runbook ask "how do I roll back a publish?"');
8611
8847
  return runbookAsk(ctx, question, parsed.options.all === true);
8612
8848
  }
8849
+ case "lint":
8850
+ return runbookLint(ctx, parsed.options.all === true);
8613
8851
  case "impact":
8614
8852
  return runbookImpact(
8615
8853
  ctx,
@@ -8640,7 +8878,8 @@ async function runbookCommand(parsed, deps = {}) {
8640
8878
  name,
8641
8879
  stringOpt(parsed.options.title) ?? name,
8642
8880
  readBody(stringOpt(parsed.options.file), stringOpt(parsed.options.body)),
8643
- stringOpt(parsed.options.summary)
8881
+ stringOpt(parsed.options.summary),
8882
+ stringOpt(parsed.options.requires)
8644
8883
  );
8645
8884
  }
8646
8885
  case "edit": {
@@ -8649,7 +8888,13 @@ async function runbookCommand(parsed, deps = {}) {
8649
8888
  const inline = stringOpt(parsed.options.body);
8650
8889
  const body = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
8651
8890
  if (body === null) return ctx.out.log(`${name} unchanged; nothing written`);
8652
- return runbookEdit(ctx, name, body, stringOpt(parsed.options.note));
8891
+ return runbookEdit(
8892
+ ctx,
8893
+ name,
8894
+ body,
8895
+ stringOpt(parsed.options.note),
8896
+ parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? ""
8897
+ );
8653
8898
  }
8654
8899
  case "import": {
8655
8900
  const dir = parsed.positionals[2];
@@ -8678,9 +8923,7 @@ async function runbookCommand(parsed, deps = {}) {
8678
8923
  case "rm":
8679
8924
  return runbookRemove(ctx, requireSlug(slug, "rm"));
8680
8925
  default:
8681
- throw new Error(
8682
- `unknown runbook action "${action}". Try ask, search, impact, list, get, new, edit, comment, import, visibility, publish, archive, history, revert, rm.`
8683
- );
8926
+ throw new Error(`unknown runbook action "${action}". Try ${acceptedAfter(["runbook"]).join(", ")}.`);
8684
8927
  }
8685
8928
  }
8686
8929
 
@@ -9505,8 +9748,10 @@ async function calendarCommand(parsed, dependencies) {
9505
9748
  CAPABILITIES,
9506
9749
  CODE_BUILD_RECIPES,
9507
9750
  CODE_PI_IMAGE,
9751
+ COMMAND_SURFACE,
9508
9752
  GOOGLE_CALENDAR_EVENTS_SCOPE,
9509
9753
  SYSTEM_AI_PURPOSES,
9754
+ acceptedAfter,
9510
9755
  adminAi,
9511
9756
  calendarBookingPageUrl,
9512
9757
  calendarCalendars,
@@ -9516,6 +9761,7 @@ async function calendarCommand(parsed, dependencies) {
9516
9761
  calendarStatus,
9517
9762
  codeConnect,
9518
9763
  connectGitHubSecuritySource,
9764
+ describeProblem,
9519
9765
  disconnectGitHubSecuritySource,
9520
9766
  doctor,
9521
9767
  exitCodeFor,
@@ -9543,6 +9789,8 @@ async function calendarCommand(parsed, dependencies) {
9543
9789
  secretsSet,
9544
9790
  secretsSetClerkKey,
9545
9791
  smoke,
9546
- startHostedSecurityJob
9792
+ startHostedSecurityJob,
9793
+ surfacePaths,
9794
+ validateInvocation
9547
9795
  });
9548
9796
  //# sourceMappingURL=index.cjs.map