@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/bin.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  exitCodeFor,
4
4
  runCli
5
- } from "./chunk-TOSMNGGQ.js";
5
+ } from "./chunk-I4XURPUK.js";
6
6
 
7
7
  // src/bin.ts
8
8
  runCli().catch((err) => {
@@ -5942,11 +5942,12 @@ Usage:
5942
5942
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
5943
5943
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
5944
5944
  odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
5945
+ odla-ai runbook lint [--app <id>] [--all] [--json]
5945
5946
  odla-ai runbook list [--app <id>] [--all] [--q <text>] [--json]
5946
5947
  odla-ai runbook comment <slug> --body "..." [--app <id>]
5947
5948
  odla-ai runbook get <slug> [--app <id>]
5948
- odla-ai runbook new <slug> --title <t> --file <path|-> [--summary <s>] [--app <id>]
5949
- odla-ai runbook edit <slug> [--file <path|->|--body "..."] [--note <why>] [--app <id>]
5949
+ odla-ai runbook new <slug> --title <t> --file <path|-> [--summary <s>] [--requires <specs>] [--app <id>]
5950
+ odla-ai runbook edit <slug> [--file <path|->|--body "..."] [--note <why>] [--requires <specs>] [--app <id>]
5950
5951
  odla-ai runbook import <dir> [--visibility operator|admin] [--dry-run] [--app <id>]
5951
5952
  odla-ai runbook publish <slug> [--app <id>]
5952
5953
  odla-ai runbook visibility <slug> <operator|admin> [--app <id>]
@@ -5988,7 +5989,10 @@ Commands:
5988
5989
  "search" the passages behind it; "get" the whole document.
5989
5990
  "impact" diffs your working tree against a base ref and names the
5990
5991
  runbooks that describe what you changed \u2014 run it after touching a
5991
- package's exported API or JSDoc, or a Studio surface. A wrong step
5992
+ package's exported API or JSDoc, or a Studio surface. "lint"
5993
+ checks the corpus the other way: every odla-ai command the
5994
+ runbooks name is held against this CLI's real command surface,
5995
+ and against the minimum versions each runbook declares. A wrong step
5992
5996
  is fixed with "edit", which takes effect immediately: a runbook is
5993
5997
  a row, not a release. Runbooks describe PROCEDURE; what an export
5994
5998
  does and what it guarantees is JSDoc, rendered per package at
@@ -6732,6 +6736,124 @@ ${env}: credentials are already saved; retry "odla-ai secrets push --env ${env}$
6732
6736
  }
6733
6737
  }
6734
6738
 
6739
+ // src/surface.ts
6740
+ var PM_ACTIONS = {
6741
+ list: {},
6742
+ add: {},
6743
+ create: {},
6744
+ get: {},
6745
+ set: {},
6746
+ update: {},
6747
+ status: {},
6748
+ move: {},
6749
+ done: {},
6750
+ comment: {},
6751
+ comments: {},
6752
+ rm: {},
6753
+ delete: {}
6754
+ };
6755
+ var PM_ENTITIES = Object.fromEntries(
6756
+ ["goal", "conformance", "task", "kanban", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
6757
+ );
6758
+ var COMMAND_SURFACE = {
6759
+ admin: {
6760
+ ai: {
6761
+ show: {},
6762
+ set: {},
6763
+ credentials: {},
6764
+ models: {},
6765
+ usage: {},
6766
+ audit: {},
6767
+ credential: { set: {} }
6768
+ }
6769
+ },
6770
+ app: {
6771
+ archive: {},
6772
+ restore: {},
6773
+ export: {},
6774
+ import: {},
6775
+ rename: {},
6776
+ "refresh-sandbox": {},
6777
+ "go-live": {},
6778
+ promote: {},
6779
+ owners: { list: {}, add: {}, remove: {} }
6780
+ },
6781
+ calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
6782
+ capabilities: {},
6783
+ code: { connect: {} },
6784
+ doctor: {},
6785
+ help: {},
6786
+ init: {},
6787
+ pm: PM_ENTITIES,
6788
+ provision: {},
6789
+ runbook: {
6790
+ ask: {},
6791
+ search: {},
6792
+ impact: {},
6793
+ list: {},
6794
+ get: {},
6795
+ cat: {},
6796
+ new: {},
6797
+ edit: {},
6798
+ comment: {},
6799
+ import: {},
6800
+ visibility: {},
6801
+ publish: {},
6802
+ archive: {},
6803
+ history: {},
6804
+ revert: {},
6805
+ rm: {},
6806
+ lint: {}
6807
+ },
6808
+ secrets: { push: {}, set: {}, "set-clerk-key": {} },
6809
+ security: {
6810
+ plan: {},
6811
+ sources: {},
6812
+ run: {},
6813
+ status: {},
6814
+ report: {},
6815
+ github: { connect: {}, disconnect: {} }
6816
+ },
6817
+ setup: {},
6818
+ skill: { install: {} },
6819
+ smoke: {},
6820
+ version: {},
6821
+ whoami: {}
6822
+ };
6823
+ function acceptedAfter(path) {
6824
+ let node = COMMAND_SURFACE;
6825
+ for (const word of path) {
6826
+ node = node?.[word];
6827
+ if (!node) return [];
6828
+ }
6829
+ return Object.keys(node).sort();
6830
+ }
6831
+ function validateInvocation(words2) {
6832
+ let node = COMMAND_SURFACE;
6833
+ const walked = [];
6834
+ for (const word of words2) {
6835
+ if (Object.keys(node).length === 0) return null;
6836
+ const next = node[word];
6837
+ if (!next) return { validPrefix: walked.join(" "), word, accepted: Object.keys(node).sort() };
6838
+ walked.push(word);
6839
+ node = next;
6840
+ }
6841
+ return null;
6842
+ }
6843
+ function describeProblem(problem) {
6844
+ const where = problem.validPrefix ? `after "${problem.validPrefix}"` : "as a command";
6845
+ return `"${problem.word}" is not accepted ${where} \u2014 try: ${problem.accepted.join(", ")}`;
6846
+ }
6847
+ function surfacePaths(node = COMMAND_SURFACE, prefix = []) {
6848
+ const paths = [];
6849
+ for (const [word, child] of Object.entries(node)) {
6850
+ const path = [...prefix, word];
6851
+ paths.push(path);
6852
+ paths.push(...surfacePaths(child, path));
6853
+ }
6854
+ return paths;
6855
+ }
6856
+
6735
6857
  // src/security.ts
6736
6858
  import { isAbsolute as isAbsolute5, relative as relative5, resolve as resolve9, sep as sep4 } from "path";
6737
6859
  import {
@@ -8052,6 +8174,55 @@ import process10 from "process";
8052
8174
 
8053
8175
  // src/runbook-actions.ts
8054
8176
  import { readFileSync as readFileSync8 } from "fs";
8177
+
8178
+ // src/runbook-requires.ts
8179
+ var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
8180
+ function parseRequires(value) {
8181
+ if (!value) return [];
8182
+ const out = [];
8183
+ for (const token of value.split(/[\s,]+/).filter(Boolean)) {
8184
+ const match = SPEC.exec(token);
8185
+ if (match?.[1] && match[2]) out.push({ name: match[1], min: match[2] });
8186
+ }
8187
+ return out;
8188
+ }
8189
+ function compareVersions(a, b) {
8190
+ const parts = (v) => {
8191
+ const [core = "", pre = ""] = v.split("-", 2);
8192
+ return { nums: core.split(".").map((n) => Number.parseInt(n, 10) || 0), pre };
8193
+ };
8194
+ const left = parts(a);
8195
+ const right = parts(b);
8196
+ for (let i = 0; i < Math.max(left.nums.length, right.nums.length); i++) {
8197
+ const diff = (left.nums[i] ?? 0) - (right.nums[i] ?? 0);
8198
+ if (diff) return diff < 0 ? -1 : 1;
8199
+ }
8200
+ if (left.pre === right.pre) return 0;
8201
+ if (!left.pre) return 1;
8202
+ if (!right.pre) return -1;
8203
+ return left.pre < right.pre ? -1 : 1;
8204
+ }
8205
+ function satisfies(installed, min) {
8206
+ return compareVersions(installed, min) >= 0;
8207
+ }
8208
+ function unmetRequirements(requires, installedVersions) {
8209
+ const unmet = [];
8210
+ for (const requirement of parseRequires(requires)) {
8211
+ const installed = installedVersions[requirement.name];
8212
+ if (installed === void 0) {
8213
+ unmet.push({ ...requirement, installed: null });
8214
+ } else if (!satisfies(installed, requirement.min)) {
8215
+ unmet.push({ ...requirement, installed });
8216
+ }
8217
+ }
8218
+ return unmet;
8219
+ }
8220
+ function describeUnmet(slug, unmet) {
8221
+ const detail = unmet.map((u) => `${u.name}@${u.min}${u.installed ? ` (you have ${u.installed})` : " (not installed here)"}`).join(", ");
8222
+ return `note: runbook "${slug}" expects ${detail} \u2014 some steps may name commands your version does not have.`;
8223
+ }
8224
+
8225
+ // src/runbook-actions.ts
8055
8226
  var PLATFORM_SCOPE = "$platform";
8056
8227
  async function call(ctx, method, path, body) {
8057
8228
  const res = await ctx.doFetch(`${ctx.platformUrl.replace(/\/$/, "")}/registry/pm${path}`, {
@@ -8106,19 +8277,23 @@ async function runbookList(ctx, all, query) {
8106
8277
  async function runbookGet(ctx, slug) {
8107
8278
  const runbook = await bySlug(ctx, slug);
8108
8279
  if (ctx.json) return ctx.out.log(JSON.stringify(runbook, null, 2));
8280
+ const unmet = unmetRequirements(runbook.requires, { "@odla-ai/cli": cliVersion() });
8281
+ if (unmet.length) ctx.out.error(describeUnmet(slug, unmet));
8109
8282
  ctx.out.log(runbook.body);
8110
8283
  }
8111
- async function runbookNew(ctx, slug, title, body, summary) {
8284
+ async function runbookNew(ctx, slug, title, body, summary, requires) {
8112
8285
  const created = await call(ctx, "POST", "/runbook", {
8113
8286
  appId: ctx.appId,
8114
- input: { slug, title, body, ...summary ? { summary } : {} }
8287
+ input: { slug, title, body, ...summary ? { summary } : {}, ...requires ? { requires } : {} }
8115
8288
  });
8116
8289
  ctx.out.log(ctx.json ? JSON.stringify(created, null, 2) : `created ${slug} (${created.id})`);
8117
8290
  }
8118
- async function runbookEdit(ctx, slug, body, note) {
8291
+ async function runbookEdit(ctx, slug, body, note, requires) {
8119
8292
  const runbook = await bySlug(ctx, slug);
8120
8293
  const result = await call(ctx, "PATCH", `/runbook/${encodeURIComponent(runbook.id)}`, {
8121
- patch: { body, ...note ? { note } : {} }
8294
+ // An empty --requires clears the declaration; omitting the flag leaves
8295
+ // whatever is there, so an ordinary body edit never drops it.
8296
+ patch: { body, ...note ? { note } : {}, ...requires === void 0 ? {} : { requires: requires || null } }
8122
8297
  });
8123
8298
  if (ctx.json) return ctx.out.log(JSON.stringify(result, null, 2));
8124
8299
  ctx.out.log(`${slug} \u2192 v${result.record?.version ?? runbook.version + 1}`);
@@ -8525,6 +8700,61 @@ async function runbookImpact(ctx, options, deps = {}) {
8525
8700
  report2(ctx, impacts);
8526
8701
  }
8527
8702
 
8703
+ // src/runbook-lint.ts
8704
+ function invocationsIn(body) {
8705
+ const re = /(`|npx[^\S\n]+)?(?:@odla-ai\/cli(?:@[\w.-]+)?|odla-ai)((?:[^\S\n]+[a-z][\w-]*)+)/g;
8706
+ const found = [];
8707
+ for (const match of body.matchAll(re)) {
8708
+ if (!match[1]) continue;
8709
+ const words2 = match[2].trim().split(/[^\S\n]+/);
8710
+ if (words2.length) found.push(words2);
8711
+ }
8712
+ return found;
8713
+ }
8714
+ function lintRunbook(runbook, installed) {
8715
+ const findings = [];
8716
+ const at = { slug: runbook.slug, appId: runbook.appId };
8717
+ const declared = parseRequires(runbook.requires);
8718
+ const seen = /* @__PURE__ */ new Set();
8719
+ for (const words2 of invocationsIn(runbook.body)) {
8720
+ const problem = validateInvocation(words2);
8721
+ if (!problem) continue;
8722
+ const key = `${problem.validPrefix}|${problem.word}`;
8723
+ if (seen.has(key)) continue;
8724
+ seen.add(key);
8725
+ findings.push({
8726
+ ...at,
8727
+ kind: declared.length ? "command" : "undeclared",
8728
+ 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.`
8729
+ });
8730
+ }
8731
+ const unmet = unmetRequirements(runbook.requires, installed);
8732
+ if (unmet.length) findings.push({ ...at, kind: "requires", detail: describeUnmet(runbook.slug, unmet) });
8733
+ return findings;
8734
+ }
8735
+ async function runbookLint(ctx, all) {
8736
+ const params = new URLSearchParams();
8737
+ if (!all) params.set("app", ctx.appId);
8738
+ const page = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
8739
+ const installed = { "@odla-ai/cli": cliVersion() };
8740
+ const findings = page.records.flatMap((runbook) => lintRunbook(runbook, installed));
8741
+ if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page.records.length, findings }, null, 2));
8742
+ if (!page.records.length) return ctx.out.log("(no runbooks in scope)");
8743
+ if (!findings.length) {
8744
+ return ctx.out.log(
8745
+ `${page.records.length} runbook${page.records.length === 1 ? "" : "s"} checked; every command they name is real for @odla-ai/cli ${cliVersion()}.`
8746
+ );
8747
+ }
8748
+ ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${page.records.length} runbooks:`);
8749
+ for (const finding of findings) {
8750
+ const scope = finding.appId === PLATFORM_SCOPE ? "" : ` --app ${finding.appId}`;
8751
+ ctx.out.log("");
8752
+ ctx.out.log(`${finding.slug} [${finding.kind}]`);
8753
+ ctx.out.log(` ${finding.detail}`);
8754
+ ctx.out.log(` odla-ai runbook get ${finding.slug}${scope}`);
8755
+ }
8756
+ }
8757
+
8528
8758
  // src/runbook-search-command.ts
8529
8759
  async function runbookSearch(ctx, query, all, limit) {
8530
8760
  const params = new URLSearchParams({ q: query });
@@ -8717,14 +8947,15 @@ var ALLOWED2 = [
8717
8947
  "visibility",
8718
8948
  "dry-run",
8719
8949
  "limit",
8720
- "base"
8950
+ "base",
8951
+ "requires"
8721
8952
  ];
8722
8953
  function requireSlug(slug, action) {
8723
8954
  if (!slug) throw new Error(`"runbook ${action}" needs a slug, e.g. "odla-ai runbook ${action} release"`);
8724
8955
  return slug;
8725
8956
  }
8726
8957
  var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
8727
- var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact"]);
8958
+ var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact", "lint"]);
8728
8959
  async function loadOrDefaultConfig(configPath, action, appId) {
8729
8960
  const projectScoped = appId !== void 0;
8730
8961
  if (!projectScoped && CONFIG_FREE.has(action) && !existsSync10(resolve10(configPath))) {
@@ -8806,6 +9037,8 @@ async function runbookCommand(parsed, deps = {}) {
8806
9037
  if (!question) throw new Error('"runbook ask" needs a question, e.g. odla-ai runbook ask "how do I roll back a publish?"');
8807
9038
  return runbookAsk(ctx, question, parsed.options.all === true);
8808
9039
  }
9040
+ case "lint":
9041
+ return runbookLint(ctx, parsed.options.all === true);
8809
9042
  case "impact":
8810
9043
  return runbookImpact(
8811
9044
  ctx,
@@ -8836,7 +9069,8 @@ async function runbookCommand(parsed, deps = {}) {
8836
9069
  name,
8837
9070
  stringOpt(parsed.options.title) ?? name,
8838
9071
  readBody(stringOpt(parsed.options.file), stringOpt(parsed.options.body)),
8839
- stringOpt(parsed.options.summary)
9072
+ stringOpt(parsed.options.summary),
9073
+ stringOpt(parsed.options.requires)
8840
9074
  );
8841
9075
  }
8842
9076
  case "edit": {
@@ -8845,7 +9079,13 @@ async function runbookCommand(parsed, deps = {}) {
8845
9079
  const inline = stringOpt(parsed.options.body);
8846
9080
  const body = file === void 0 && inline === void 0 ? await editRunbook(ctx, name) : readBody(file, inline);
8847
9081
  if (body === null) return ctx.out.log(`${name} unchanged; nothing written`);
8848
- return runbookEdit(ctx, name, body, stringOpt(parsed.options.note));
9082
+ return runbookEdit(
9083
+ ctx,
9084
+ name,
9085
+ body,
9086
+ stringOpt(parsed.options.note),
9087
+ parsed.options.requires === void 0 ? void 0 : stringOpt(parsed.options.requires) ?? ""
9088
+ );
8849
9089
  }
8850
9090
  case "import": {
8851
9091
  const dir = parsed.positionals[2];
@@ -8874,9 +9114,7 @@ async function runbookCommand(parsed, deps = {}) {
8874
9114
  case "rm":
8875
9115
  return runbookRemove(ctx, requireSlug(slug, "rm"));
8876
9116
  default:
8877
- throw new Error(
8878
- `unknown runbook action "${action}". Try ask, search, impact, list, get, new, edit, comment, import, visibility, publish, archive, history, revert, rm.`
8879
- );
9117
+ throw new Error(`unknown runbook action "${action}". Try ${acceptedAfter(["runbook"]).join(", ")}.`);
8880
9118
  }
8881
9119
  }
8882
9120
 
@@ -9460,6 +9698,11 @@ export {
9460
9698
  codeConnect,
9461
9699
  runCodeRuntime,
9462
9700
  provision,
9701
+ COMMAND_SURFACE,
9702
+ acceptedAfter,
9703
+ validateInvocation,
9704
+ describeProblem,
9705
+ surfacePaths,
9463
9706
  runHostedSecurity,
9464
9707
  getHostedSecurityIntent,
9465
9708
  getHostedSecurityPlan,
@@ -9472,4 +9715,4 @@ export {
9472
9715
  exitCodeFor,
9473
9716
  runCli
9474
9717
  };
9475
- //# sourceMappingURL=chunk-TOSMNGGQ.js.map
9718
+ //# sourceMappingURL=chunk-I4XURPUK.js.map