@odla-ai/cli 0.18.0 → 0.19.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,6 +6494,7 @@ 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>]
@@ -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,6 +7968,8 @@ 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
7975
  async function runbookNew(ctx, slug, title, body, summary) {
@@ -8329,6 +8389,179 @@ async function runbookImpact(ctx, options, deps = {}) {
8329
8389
  report2(ctx, impacts);
8330
8390
  }
8331
8391
 
8392
+ // src/surface.ts
8393
+ var PM_ACTIONS = {
8394
+ list: {},
8395
+ add: {},
8396
+ create: {},
8397
+ get: {},
8398
+ set: {},
8399
+ update: {},
8400
+ status: {},
8401
+ move: {},
8402
+ done: {},
8403
+ comment: {},
8404
+ comments: {},
8405
+ rm: {},
8406
+ delete: {}
8407
+ };
8408
+ var PM_ENTITIES = Object.fromEntries(
8409
+ ["goal", "conformance", "task", "kanban", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
8410
+ );
8411
+ var COMMAND_SURFACE = {
8412
+ admin: {
8413
+ ai: {
8414
+ show: {},
8415
+ set: {},
8416
+ credentials: {},
8417
+ models: {},
8418
+ usage: {},
8419
+ audit: {},
8420
+ credential: { set: {} }
8421
+ }
8422
+ },
8423
+ app: {
8424
+ archive: {},
8425
+ restore: {},
8426
+ export: {},
8427
+ import: {},
8428
+ rename: {},
8429
+ "refresh-sandbox": {},
8430
+ "go-live": {},
8431
+ promote: {},
8432
+ owners: { list: {}, add: {}, remove: {} }
8433
+ },
8434
+ calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
8435
+ capabilities: {},
8436
+ code: { connect: {} },
8437
+ doctor: {},
8438
+ help: {},
8439
+ init: {},
8440
+ pm: PM_ENTITIES,
8441
+ provision: {},
8442
+ runbook: {
8443
+ ask: {},
8444
+ search: {},
8445
+ impact: {},
8446
+ list: {},
8447
+ get: {},
8448
+ cat: {},
8449
+ new: {},
8450
+ edit: {},
8451
+ comment: {},
8452
+ import: {},
8453
+ visibility: {},
8454
+ publish: {},
8455
+ archive: {},
8456
+ history: {},
8457
+ revert: {},
8458
+ rm: {},
8459
+ lint: {}
8460
+ },
8461
+ secrets: { push: {}, set: {}, "set-clerk-key": {} },
8462
+ security: {
8463
+ plan: {},
8464
+ sources: {},
8465
+ run: {},
8466
+ status: {},
8467
+ report: {},
8468
+ github: { connect: {}, disconnect: {} }
8469
+ },
8470
+ setup: {},
8471
+ skill: { install: {} },
8472
+ smoke: {},
8473
+ version: {},
8474
+ whoami: {}
8475
+ };
8476
+ function acceptedAfter(path) {
8477
+ let node = COMMAND_SURFACE;
8478
+ for (const word of path) {
8479
+ node = node?.[word];
8480
+ if (!node) return [];
8481
+ }
8482
+ return Object.keys(node).sort();
8483
+ }
8484
+ function validateInvocation(words2) {
8485
+ let node = COMMAND_SURFACE;
8486
+ const walked = [];
8487
+ for (const word of words2) {
8488
+ if (Object.keys(node).length === 0) return null;
8489
+ const next = node[word];
8490
+ if (!next) return { validPrefix: walked.join(" "), word, accepted: Object.keys(node).sort() };
8491
+ walked.push(word);
8492
+ node = next;
8493
+ }
8494
+ return null;
8495
+ }
8496
+ function describeProblem(problem) {
8497
+ const where = problem.validPrefix ? `after "${problem.validPrefix}"` : "as a command";
8498
+ return `"${problem.word}" is not accepted ${where} \u2014 try: ${problem.accepted.join(", ")}`;
8499
+ }
8500
+ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
8501
+ const paths = [];
8502
+ for (const [word, child] of Object.entries(node)) {
8503
+ const path = [...prefix, word];
8504
+ paths.push(path);
8505
+ paths.push(...surfacePaths(child, path));
8506
+ }
8507
+ return paths;
8508
+ }
8509
+
8510
+ // src/runbook-lint.ts
8511
+ function invocationsIn(body) {
8512
+ const re = /(`|npx[^\S\n]+)?(?:@odla-ai\/cli(?:@[\w.-]+)?|odla-ai)((?:[^\S\n]+[a-z][\w-]*)+)/g;
8513
+ const found = [];
8514
+ for (const match of body.matchAll(re)) {
8515
+ if (!match[1]) continue;
8516
+ const words2 = match[2].trim().split(/[^\S\n]+/);
8517
+ if (words2.length) found.push(words2);
8518
+ }
8519
+ return found;
8520
+ }
8521
+ function lintRunbook(runbook, installed) {
8522
+ const findings = [];
8523
+ const at = { slug: runbook.slug, appId: runbook.appId };
8524
+ const declared = parseRequires(runbook.requires);
8525
+ const seen = /* @__PURE__ */ new Set();
8526
+ for (const words2 of invocationsIn(runbook.body)) {
8527
+ const problem = validateInvocation(words2);
8528
+ if (!problem) continue;
8529
+ const key = `${problem.validPrefix}|${problem.word}`;
8530
+ if (seen.has(key)) continue;
8531
+ seen.add(key);
8532
+ findings.push({
8533
+ ...at,
8534
+ kind: declared.length ? "command" : "undeclared",
8535
+ 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.`
8536
+ });
8537
+ }
8538
+ const unmet = unmetRequirements(runbook.requires, installed);
8539
+ if (unmet.length) findings.push({ ...at, kind: "requires", detail: describeUnmet(runbook.slug, unmet) });
8540
+ return findings;
8541
+ }
8542
+ async function runbookLint(ctx, all) {
8543
+ const params = new URLSearchParams();
8544
+ if (!all) params.set("app", ctx.appId);
8545
+ const page = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
8546
+ const installed = { "@odla-ai/cli": cliVersion() };
8547
+ const findings = page.records.flatMap((runbook) => lintRunbook(runbook, installed));
8548
+ if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page.records.length, findings }, null, 2));
8549
+ if (!page.records.length) return ctx.out.log("(no runbooks in scope)");
8550
+ if (!findings.length) {
8551
+ return ctx.out.log(
8552
+ `${page.records.length} runbook${page.records.length === 1 ? "" : "s"} checked; every command they name is real for @odla-ai/cli ${cliVersion()}.`
8553
+ );
8554
+ }
8555
+ ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${page.records.length} runbooks:`);
8556
+ for (const finding of findings) {
8557
+ const scope = finding.appId === PLATFORM_SCOPE ? "" : ` --app ${finding.appId}`;
8558
+ ctx.out.log("");
8559
+ ctx.out.log(`${finding.slug} [${finding.kind}]`);
8560
+ ctx.out.log(` ${finding.detail}`);
8561
+ ctx.out.log(` odla-ai runbook get ${finding.slug}${scope}`);
8562
+ }
8563
+ }
8564
+
8332
8565
  // src/runbook-search-command.ts
8333
8566
  async function runbookSearch(ctx, query, all, limit) {
8334
8567
  const params = new URLSearchParams({ q: query });
@@ -8528,7 +8761,7 @@ function requireSlug(slug, action) {
8528
8761
  return slug;
8529
8762
  }
8530
8763
  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"]);
8764
+ var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact", "lint"]);
8532
8765
  async function loadOrDefaultConfig(configPath, action, appId) {
8533
8766
  const projectScoped = appId !== void 0;
8534
8767
  if (!projectScoped && CONFIG_FREE.has(action) && !(0, import_node_fs17.existsSync)((0, import_node_path14.resolve)(configPath))) {
@@ -8610,6 +8843,8 @@ async function runbookCommand(parsed, deps = {}) {
8610
8843
  if (!question) throw new Error('"runbook ask" needs a question, e.g. odla-ai runbook ask "how do I roll back a publish?"');
8611
8844
  return runbookAsk(ctx, question, parsed.options.all === true);
8612
8845
  }
8846
+ case "lint":
8847
+ return runbookLint(ctx, parsed.options.all === true);
8613
8848
  case "impact":
8614
8849
  return runbookImpact(
8615
8850
  ctx,
@@ -8678,9 +8913,7 @@ async function runbookCommand(parsed, deps = {}) {
8678
8913
  case "rm":
8679
8914
  return runbookRemove(ctx, requireSlug(slug, "rm"));
8680
8915
  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
- );
8916
+ throw new Error(`unknown runbook action "${action}". Try ${acceptedAfter(["runbook"]).join(", ")}.`);
8684
8917
  }
8685
8918
  }
8686
8919
 
@@ -9505,8 +9738,10 @@ async function calendarCommand(parsed, dependencies) {
9505
9738
  CAPABILITIES,
9506
9739
  CODE_BUILD_RECIPES,
9507
9740
  CODE_PI_IMAGE,
9741
+ COMMAND_SURFACE,
9508
9742
  GOOGLE_CALENDAR_EVENTS_SCOPE,
9509
9743
  SYSTEM_AI_PURPOSES,
9744
+ acceptedAfter,
9510
9745
  adminAi,
9511
9746
  calendarBookingPageUrl,
9512
9747
  calendarCalendars,
@@ -9516,6 +9751,7 @@ async function calendarCommand(parsed, dependencies) {
9516
9751
  calendarStatus,
9517
9752
  codeConnect,
9518
9753
  connectGitHubSecuritySource,
9754
+ describeProblem,
9519
9755
  disconnectGitHubSecuritySource,
9520
9756
  doctor,
9521
9757
  exitCodeFor,
@@ -9543,6 +9779,8 @@ async function calendarCommand(parsed, dependencies) {
9543
9779
  secretsSet,
9544
9780
  secretsSetClerkKey,
9545
9781
  smoke,
9546
- startHostedSecurityJob
9782
+ startHostedSecurityJob,
9783
+ surfacePaths,
9784
+ validateInvocation
9547
9785
  });
9548
9786
  //# sourceMappingURL=index.cjs.map