@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/bin.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  exitCodeFor,
4
4
  runCli
5
- } from "./chunk-TOSMNGGQ.js";
5
+ } from "./chunk-QMA5OGKQ.js";
6
6
 
7
7
  // src/bin.ts
8
8
  runCli().catch((err) => {
@@ -5942,6 +5942,7 @@ 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>]
@@ -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,6 +8277,8 @@ 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
8284
  async function runbookNew(ctx, slug, title, body, summary) {
@@ -8525,6 +8698,61 @@ async function runbookImpact(ctx, options, deps = {}) {
8525
8698
  report2(ctx, impacts);
8526
8699
  }
8527
8700
 
8701
+ // src/runbook-lint.ts
8702
+ function invocationsIn(body) {
8703
+ const re = /(`|npx[^\S\n]+)?(?:@odla-ai\/cli(?:@[\w.-]+)?|odla-ai)((?:[^\S\n]+[a-z][\w-]*)+)/g;
8704
+ const found = [];
8705
+ for (const match of body.matchAll(re)) {
8706
+ if (!match[1]) continue;
8707
+ const words2 = match[2].trim().split(/[^\S\n]+/);
8708
+ if (words2.length) found.push(words2);
8709
+ }
8710
+ return found;
8711
+ }
8712
+ function lintRunbook(runbook, installed) {
8713
+ const findings = [];
8714
+ const at = { slug: runbook.slug, appId: runbook.appId };
8715
+ const declared = parseRequires(runbook.requires);
8716
+ const seen = /* @__PURE__ */ new Set();
8717
+ for (const words2 of invocationsIn(runbook.body)) {
8718
+ const problem = validateInvocation(words2);
8719
+ if (!problem) continue;
8720
+ const key = `${problem.validPrefix}|${problem.word}`;
8721
+ if (seen.has(key)) continue;
8722
+ seen.add(key);
8723
+ findings.push({
8724
+ ...at,
8725
+ kind: declared.length ? "command" : "undeclared",
8726
+ 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.`
8727
+ });
8728
+ }
8729
+ const unmet = unmetRequirements(runbook.requires, installed);
8730
+ if (unmet.length) findings.push({ ...at, kind: "requires", detail: describeUnmet(runbook.slug, unmet) });
8731
+ return findings;
8732
+ }
8733
+ async function runbookLint(ctx, all) {
8734
+ const params = new URLSearchParams();
8735
+ if (!all) params.set("app", ctx.appId);
8736
+ const page = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
8737
+ const installed = { "@odla-ai/cli": cliVersion() };
8738
+ const findings = page.records.flatMap((runbook) => lintRunbook(runbook, installed));
8739
+ if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page.records.length, findings }, null, 2));
8740
+ if (!page.records.length) return ctx.out.log("(no runbooks in scope)");
8741
+ if (!findings.length) {
8742
+ return ctx.out.log(
8743
+ `${page.records.length} runbook${page.records.length === 1 ? "" : "s"} checked; every command they name is real for @odla-ai/cli ${cliVersion()}.`
8744
+ );
8745
+ }
8746
+ ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${page.records.length} runbooks:`);
8747
+ for (const finding of findings) {
8748
+ const scope = finding.appId === PLATFORM_SCOPE ? "" : ` --app ${finding.appId}`;
8749
+ ctx.out.log("");
8750
+ ctx.out.log(`${finding.slug} [${finding.kind}]`);
8751
+ ctx.out.log(` ${finding.detail}`);
8752
+ ctx.out.log(` odla-ai runbook get ${finding.slug}${scope}`);
8753
+ }
8754
+ }
8755
+
8528
8756
  // src/runbook-search-command.ts
8529
8757
  async function runbookSearch(ctx, query, all, limit) {
8530
8758
  const params = new URLSearchParams({ q: query });
@@ -8724,7 +8952,7 @@ function requireSlug(slug, action) {
8724
8952
  return slug;
8725
8953
  }
8726
8954
  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"]);
8955
+ var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact", "lint"]);
8728
8956
  async function loadOrDefaultConfig(configPath, action, appId) {
8729
8957
  const projectScoped = appId !== void 0;
8730
8958
  if (!projectScoped && CONFIG_FREE.has(action) && !existsSync10(resolve10(configPath))) {
@@ -8806,6 +9034,8 @@ async function runbookCommand(parsed, deps = {}) {
8806
9034
  if (!question) throw new Error('"runbook ask" needs a question, e.g. odla-ai runbook ask "how do I roll back a publish?"');
8807
9035
  return runbookAsk(ctx, question, parsed.options.all === true);
8808
9036
  }
9037
+ case "lint":
9038
+ return runbookLint(ctx, parsed.options.all === true);
8809
9039
  case "impact":
8810
9040
  return runbookImpact(
8811
9041
  ctx,
@@ -8874,9 +9104,7 @@ async function runbookCommand(parsed, deps = {}) {
8874
9104
  case "rm":
8875
9105
  return runbookRemove(ctx, requireSlug(slug, "rm"));
8876
9106
  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
- );
9107
+ throw new Error(`unknown runbook action "${action}". Try ${acceptedAfter(["runbook"]).join(", ")}.`);
8880
9108
  }
8881
9109
  }
8882
9110
 
@@ -9460,6 +9688,11 @@ export {
9460
9688
  codeConnect,
9461
9689
  runCodeRuntime,
9462
9690
  provision,
9691
+ COMMAND_SURFACE,
9692
+ acceptedAfter,
9693
+ validateInvocation,
9694
+ describeProblem,
9695
+ surfacePaths,
9463
9696
  runHostedSecurity,
9464
9697
  getHostedSecurityIntent,
9465
9698
  getHostedSecurityPlan,
@@ -9472,4 +9705,4 @@ export {
9472
9705
  exitCodeFor,
9473
9706
  runCli
9474
9707
  };
9475
- //# sourceMappingURL=chunk-TOSMNGGQ.js.map
9708
+ //# sourceMappingURL=chunk-QMA5OGKQ.js.map