@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/README.md +30 -14
- package/dist/bin.cjs +224 -5
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-TOSMNGGQ.js → chunk-QMA5OGKQ.js} +239 -6
- package/dist/chunk-QMA5OGKQ.js.map +1 -0
- package/dist/index.cjs +245 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -1
- package/dist/index.d.ts +41 -1
- package/dist/index.js +13 -3
- package/package.json +1 -1
- package/skills/odla/SKILL.md +14 -0
- package/dist/chunk-TOSMNGGQ.js.map +0 -1
package/README.md
CHANGED
|
@@ -66,6 +66,23 @@ Platform runbook edits need admin or the `platform:runbook:write` capability,
|
|
|
66
66
|
which the CLI requests for you; a plain handshake token is never admin by
|
|
67
67
|
design, and `odla-ai whoami` reports what this terminal actually holds.
|
|
68
68
|
|
|
69
|
+
**Is the corpus itself still true?**
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx odla-ai runbook lint
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Because a runbook is edited without a release, its text can name a command that
|
|
76
|
+
only a newer CLI has — and no gate in any repository can see that, since the
|
|
77
|
+
text is a row in a database rather than a file. `lint` closes that: it holds
|
|
78
|
+
every `odla-ai …` command the runbooks name against this CLI's actual command
|
|
79
|
+
surface, and against the minimum versions each runbook declares in `requires`.
|
|
80
|
+
|
|
81
|
+
A runbook may record the versions its steps assume, as `name@version` specs read
|
|
82
|
+
as minimums (`@odla-ai/cli@0.18.0`). When yours is older, `runbook get` says so
|
|
83
|
+
on **stderr** — never stdout, so piping the procedure into an agent still yields
|
|
84
|
+
the procedure and nothing else.
|
|
85
|
+
|
|
69
86
|
## Install
|
|
70
87
|
|
|
71
88
|
No install needed — the package ships a single `odla-ai` binary, so npx can run
|
|
@@ -109,6 +126,7 @@ npx odla-ai runbook search "restore a tenant" --limit 5
|
|
|
109
126
|
npx odla-ai runbook get release
|
|
110
127
|
npx odla-ai runbook list
|
|
111
128
|
npx odla-ai runbook impact --base origin/main
|
|
129
|
+
npx odla-ai runbook lint
|
|
112
130
|
npx odla-ai runbook edit db --note "what changed"
|
|
113
131
|
npx odla-ai runbook comment release --body "step 4 no longer applies"
|
|
114
132
|
npx odla-ai whoami
|
|
@@ -118,7 +136,6 @@ npx odla-ai doctor
|
|
|
118
136
|
npx odla-ai calendar status --env dev
|
|
119
137
|
npx odla-ai calendar calendars --env dev
|
|
120
138
|
npx odla-ai calendar connect --env dev
|
|
121
|
-
npx odla-ai calendar resync --env dev
|
|
122
139
|
npx odla-ai calendar disconnect --env dev --yes
|
|
123
140
|
npx odla-ai capabilities --json
|
|
124
141
|
npx odla-ai code connect --platform https://odla.ai --app-id my-app --env dev --email owner@example.com
|
|
@@ -318,19 +335,18 @@ agent automation. Once connected, `calendar calendars` lists ids visible to the
|
|
|
318
335
|
Google identity so the checked-in selection can be refined. `calendar connect`
|
|
319
336
|
applies the checked-in booking-page setting and starts consent when the
|
|
320
337
|
connection is absent, failed, disconnected, or degraded (so an agent can
|
|
321
|
-
repair revoked credentials); a healthy connection is reused. A
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
Google authorization code, client secret, access token, or refresh token.
|
|
338
|
+
repair revoked credentials); a healthy connection is reused. A connection that
|
|
339
|
+
already holds provider credentials is reused without requesting Google consent
|
|
340
|
+
again, and an in-flight authorization is awaited rather than duplicated.
|
|
341
|
+
|
|
342
|
+
Nothing syncs: Google Calendar is the source of truth and odla stores no events
|
|
343
|
+
and no attendees, so there is no resync to ask for. `calendar disconnect --yes`
|
|
344
|
+
deletes this app/environment connection's encrypted platform token. It does not
|
|
345
|
+
revoke the shared user-to-Google-OAuth-project grant, because that could
|
|
346
|
+
invalidate other odla connections for the same user. A future explicitly global
|
|
347
|
+
revoke command would need cross-connection accounting and warning. A direct
|
|
348
|
+
production connect requires `--yes`. None of these commands accepts a Google
|
|
349
|
+
authorization code, client secret, access token, or refresh token.
|
|
334
350
|
|
|
335
351
|
The SDK uses the existing server-side db values (`ODLA_ENDPOINT`,
|
|
336
352
|
`ODLA_TENANT`, `ODLA_API_KEY`), so calendar adds no Worker secret. Never expose
|
package/dist/bin.cjs
CHANGED
|
@@ -6435,6 +6435,7 @@ Usage:
|
|
|
6435
6435
|
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
6436
6436
|
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
6437
6437
|
odla-ai runbook impact [--base origin/main] [--app <id>] [--all] [--limit <n>] [--json]
|
|
6438
|
+
odla-ai runbook lint [--app <id>] [--all] [--json]
|
|
6438
6439
|
odla-ai runbook list [--app <id>] [--all] [--q <text>] [--json]
|
|
6439
6440
|
odla-ai runbook comment <slug> --body "..." [--app <id>]
|
|
6440
6441
|
odla-ai runbook get <slug> [--app <id>]
|
|
@@ -6481,7 +6482,10 @@ Commands:
|
|
|
6481
6482
|
"search" the passages behind it; "get" the whole document.
|
|
6482
6483
|
"impact" diffs your working tree against a base ref and names the
|
|
6483
6484
|
runbooks that describe what you changed \u2014 run it after touching a
|
|
6484
|
-
package's exported API or JSDoc, or a Studio surface.
|
|
6485
|
+
package's exported API or JSDoc, or a Studio surface. "lint"
|
|
6486
|
+
checks the corpus the other way: every odla-ai command the
|
|
6487
|
+
runbooks name is held against this CLI's real command surface,
|
|
6488
|
+
and against the minimum versions each runbook declares. A wrong step
|
|
6485
6489
|
is fixed with "edit", which takes effect immediately: a runbook is
|
|
6486
6490
|
a row, not a release. Runbooks describe PROCEDURE; what an export
|
|
6487
6491
|
does and what it guarantees is JSDoc, rendered per package at
|
|
@@ -7802,6 +7806,55 @@ var import_node_process9 = __toESM(require("process"), 1);
|
|
|
7802
7806
|
|
|
7803
7807
|
// src/runbook-actions.ts
|
|
7804
7808
|
var import_node_fs13 = require("fs");
|
|
7809
|
+
|
|
7810
|
+
// src/runbook-requires.ts
|
|
7811
|
+
var SPEC = /^(@?[\w./-]+?)@(\d+\.\d+\.\d+(?:[\w.-]*)?)$/;
|
|
7812
|
+
function parseRequires(value) {
|
|
7813
|
+
if (!value) return [];
|
|
7814
|
+
const out = [];
|
|
7815
|
+
for (const token of value.split(/[\s,]+/).filter(Boolean)) {
|
|
7816
|
+
const match = SPEC.exec(token);
|
|
7817
|
+
if (match?.[1] && match[2]) out.push({ name: match[1], min: match[2] });
|
|
7818
|
+
}
|
|
7819
|
+
return out;
|
|
7820
|
+
}
|
|
7821
|
+
function compareVersions(a, b) {
|
|
7822
|
+
const parts = (v) => {
|
|
7823
|
+
const [core = "", pre = ""] = v.split("-", 2);
|
|
7824
|
+
return { nums: core.split(".").map((n) => Number.parseInt(n, 10) || 0), pre };
|
|
7825
|
+
};
|
|
7826
|
+
const left = parts(a);
|
|
7827
|
+
const right = parts(b);
|
|
7828
|
+
for (let i = 0; i < Math.max(left.nums.length, right.nums.length); i++) {
|
|
7829
|
+
const diff = (left.nums[i] ?? 0) - (right.nums[i] ?? 0);
|
|
7830
|
+
if (diff) return diff < 0 ? -1 : 1;
|
|
7831
|
+
}
|
|
7832
|
+
if (left.pre === right.pre) return 0;
|
|
7833
|
+
if (!left.pre) return 1;
|
|
7834
|
+
if (!right.pre) return -1;
|
|
7835
|
+
return left.pre < right.pre ? -1 : 1;
|
|
7836
|
+
}
|
|
7837
|
+
function satisfies(installed, min) {
|
|
7838
|
+
return compareVersions(installed, min) >= 0;
|
|
7839
|
+
}
|
|
7840
|
+
function unmetRequirements(requires, installedVersions) {
|
|
7841
|
+
const unmet = [];
|
|
7842
|
+
for (const requirement of parseRequires(requires)) {
|
|
7843
|
+
const installed = installedVersions[requirement.name];
|
|
7844
|
+
if (installed === void 0) {
|
|
7845
|
+
unmet.push({ ...requirement, installed: null });
|
|
7846
|
+
} else if (!satisfies(installed, requirement.min)) {
|
|
7847
|
+
unmet.push({ ...requirement, installed });
|
|
7848
|
+
}
|
|
7849
|
+
}
|
|
7850
|
+
return unmet;
|
|
7851
|
+
}
|
|
7852
|
+
function describeUnmet(slug, unmet) {
|
|
7853
|
+
const detail = unmet.map((u) => `${u.name}@${u.min}${u.installed ? ` (you have ${u.installed})` : " (not installed here)"}`).join(", ");
|
|
7854
|
+
return `note: runbook "${slug}" expects ${detail} \u2014 some steps may name commands your version does not have.`;
|
|
7855
|
+
}
|
|
7856
|
+
|
|
7857
|
+
// src/runbook-actions.ts
|
|
7805
7858
|
var PLATFORM_SCOPE = "$platform";
|
|
7806
7859
|
async function call(ctx, method, path, body) {
|
|
7807
7860
|
const res = await ctx.doFetch(`${ctx.platformUrl.replace(/\/$/, "")}/registry/pm${path}`, {
|
|
@@ -7856,6 +7909,8 @@ async function runbookList(ctx, all, query) {
|
|
|
7856
7909
|
async function runbookGet(ctx, slug) {
|
|
7857
7910
|
const runbook = await bySlug(ctx, slug);
|
|
7858
7911
|
if (ctx.json) return ctx.out.log(JSON.stringify(runbook, null, 2));
|
|
7912
|
+
const unmet = unmetRequirements(runbook.requires, { "@odla-ai/cli": cliVersion() });
|
|
7913
|
+
if (unmet.length) ctx.out.error(describeUnmet(slug, unmet));
|
|
7859
7914
|
ctx.out.log(runbook.body);
|
|
7860
7915
|
}
|
|
7861
7916
|
async function runbookNew(ctx, slug, title, body, summary) {
|
|
@@ -8275,6 +8330,170 @@ async function runbookImpact(ctx, options, deps = {}) {
|
|
|
8275
8330
|
report2(ctx, impacts);
|
|
8276
8331
|
}
|
|
8277
8332
|
|
|
8333
|
+
// src/surface.ts
|
|
8334
|
+
var PM_ACTIONS = {
|
|
8335
|
+
list: {},
|
|
8336
|
+
add: {},
|
|
8337
|
+
create: {},
|
|
8338
|
+
get: {},
|
|
8339
|
+
set: {},
|
|
8340
|
+
update: {},
|
|
8341
|
+
status: {},
|
|
8342
|
+
move: {},
|
|
8343
|
+
done: {},
|
|
8344
|
+
comment: {},
|
|
8345
|
+
comments: {},
|
|
8346
|
+
rm: {},
|
|
8347
|
+
delete: {}
|
|
8348
|
+
};
|
|
8349
|
+
var PM_ENTITIES = Object.fromEntries(
|
|
8350
|
+
["goal", "conformance", "task", "kanban", "decision", "bug"].map((entity) => [entity, PM_ACTIONS])
|
|
8351
|
+
);
|
|
8352
|
+
var COMMAND_SURFACE = {
|
|
8353
|
+
admin: {
|
|
8354
|
+
ai: {
|
|
8355
|
+
show: {},
|
|
8356
|
+
set: {},
|
|
8357
|
+
credentials: {},
|
|
8358
|
+
models: {},
|
|
8359
|
+
usage: {},
|
|
8360
|
+
audit: {},
|
|
8361
|
+
credential: { set: {} }
|
|
8362
|
+
}
|
|
8363
|
+
},
|
|
8364
|
+
app: {
|
|
8365
|
+
archive: {},
|
|
8366
|
+
restore: {},
|
|
8367
|
+
export: {},
|
|
8368
|
+
import: {},
|
|
8369
|
+
rename: {},
|
|
8370
|
+
"refresh-sandbox": {},
|
|
8371
|
+
"go-live": {},
|
|
8372
|
+
promote: {},
|
|
8373
|
+
owners: { list: {}, add: {}, remove: {} }
|
|
8374
|
+
},
|
|
8375
|
+
calendar: { status: {}, calendars: {}, connect: {}, disconnect: {} },
|
|
8376
|
+
capabilities: {},
|
|
8377
|
+
code: { connect: {} },
|
|
8378
|
+
doctor: {},
|
|
8379
|
+
help: {},
|
|
8380
|
+
init: {},
|
|
8381
|
+
pm: PM_ENTITIES,
|
|
8382
|
+
provision: {},
|
|
8383
|
+
runbook: {
|
|
8384
|
+
ask: {},
|
|
8385
|
+
search: {},
|
|
8386
|
+
impact: {},
|
|
8387
|
+
list: {},
|
|
8388
|
+
get: {},
|
|
8389
|
+
cat: {},
|
|
8390
|
+
new: {},
|
|
8391
|
+
edit: {},
|
|
8392
|
+
comment: {},
|
|
8393
|
+
import: {},
|
|
8394
|
+
visibility: {},
|
|
8395
|
+
publish: {},
|
|
8396
|
+
archive: {},
|
|
8397
|
+
history: {},
|
|
8398
|
+
revert: {},
|
|
8399
|
+
rm: {},
|
|
8400
|
+
lint: {}
|
|
8401
|
+
},
|
|
8402
|
+
secrets: { push: {}, set: {}, "set-clerk-key": {} },
|
|
8403
|
+
security: {
|
|
8404
|
+
plan: {},
|
|
8405
|
+
sources: {},
|
|
8406
|
+
run: {},
|
|
8407
|
+
status: {},
|
|
8408
|
+
report: {},
|
|
8409
|
+
github: { connect: {}, disconnect: {} }
|
|
8410
|
+
},
|
|
8411
|
+
setup: {},
|
|
8412
|
+
skill: { install: {} },
|
|
8413
|
+
smoke: {},
|
|
8414
|
+
version: {},
|
|
8415
|
+
whoami: {}
|
|
8416
|
+
};
|
|
8417
|
+
function acceptedAfter(path) {
|
|
8418
|
+
let node = COMMAND_SURFACE;
|
|
8419
|
+
for (const word of path) {
|
|
8420
|
+
node = node?.[word];
|
|
8421
|
+
if (!node) return [];
|
|
8422
|
+
}
|
|
8423
|
+
return Object.keys(node).sort();
|
|
8424
|
+
}
|
|
8425
|
+
function validateInvocation(words2) {
|
|
8426
|
+
let node = COMMAND_SURFACE;
|
|
8427
|
+
const walked = [];
|
|
8428
|
+
for (const word of words2) {
|
|
8429
|
+
if (Object.keys(node).length === 0) return null;
|
|
8430
|
+
const next = node[word];
|
|
8431
|
+
if (!next) return { validPrefix: walked.join(" "), word, accepted: Object.keys(node).sort() };
|
|
8432
|
+
walked.push(word);
|
|
8433
|
+
node = next;
|
|
8434
|
+
}
|
|
8435
|
+
return null;
|
|
8436
|
+
}
|
|
8437
|
+
function describeProblem(problem) {
|
|
8438
|
+
const where = problem.validPrefix ? `after "${problem.validPrefix}"` : "as a command";
|
|
8439
|
+
return `"${problem.word}" is not accepted ${where} \u2014 try: ${problem.accepted.join(", ")}`;
|
|
8440
|
+
}
|
|
8441
|
+
|
|
8442
|
+
// src/runbook-lint.ts
|
|
8443
|
+
function invocationsIn(body) {
|
|
8444
|
+
const re = /(`|npx[^\S\n]+)?(?:@odla-ai\/cli(?:@[\w.-]+)?|odla-ai)((?:[^\S\n]+[a-z][\w-]*)+)/g;
|
|
8445
|
+
const found = [];
|
|
8446
|
+
for (const match of body.matchAll(re)) {
|
|
8447
|
+
if (!match[1]) continue;
|
|
8448
|
+
const words2 = match[2].trim().split(/[^\S\n]+/);
|
|
8449
|
+
if (words2.length) found.push(words2);
|
|
8450
|
+
}
|
|
8451
|
+
return found;
|
|
8452
|
+
}
|
|
8453
|
+
function lintRunbook(runbook, installed) {
|
|
8454
|
+
const findings = [];
|
|
8455
|
+
const at = { slug: runbook.slug, appId: runbook.appId };
|
|
8456
|
+
const declared = parseRequires(runbook.requires);
|
|
8457
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8458
|
+
for (const words2 of invocationsIn(runbook.body)) {
|
|
8459
|
+
const problem = validateInvocation(words2);
|
|
8460
|
+
if (!problem) continue;
|
|
8461
|
+
const key = `${problem.validPrefix}|${problem.word}`;
|
|
8462
|
+
if (seen.has(key)) continue;
|
|
8463
|
+
seen.add(key);
|
|
8464
|
+
findings.push({
|
|
8465
|
+
...at,
|
|
8466
|
+
kind: declared.length ? "command" : "undeclared",
|
|
8467
|
+
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.`
|
|
8468
|
+
});
|
|
8469
|
+
}
|
|
8470
|
+
const unmet = unmetRequirements(runbook.requires, installed);
|
|
8471
|
+
if (unmet.length) findings.push({ ...at, kind: "requires", detail: describeUnmet(runbook.slug, unmet) });
|
|
8472
|
+
return findings;
|
|
8473
|
+
}
|
|
8474
|
+
async function runbookLint(ctx, all) {
|
|
8475
|
+
const params = new URLSearchParams();
|
|
8476
|
+
if (!all) params.set("app", ctx.appId);
|
|
8477
|
+
const page = await call(ctx, "GET", `/runbook${params.size ? `?${params}` : ""}`);
|
|
8478
|
+
const installed = { "@odla-ai/cli": cliVersion() };
|
|
8479
|
+
const findings = page.records.flatMap((runbook) => lintRunbook(runbook, installed));
|
|
8480
|
+
if (ctx.json) return ctx.out.log(JSON.stringify({ checked: page.records.length, findings }, null, 2));
|
|
8481
|
+
if (!page.records.length) return ctx.out.log("(no runbooks in scope)");
|
|
8482
|
+
if (!findings.length) {
|
|
8483
|
+
return ctx.out.log(
|
|
8484
|
+
`${page.records.length} runbook${page.records.length === 1 ? "" : "s"} checked; every command they name is real for @odla-ai/cli ${cliVersion()}.`
|
|
8485
|
+
);
|
|
8486
|
+
}
|
|
8487
|
+
ctx.out.log(`${findings.length} finding${findings.length === 1 ? "" : "s"} across ${page.records.length} runbooks:`);
|
|
8488
|
+
for (const finding of findings) {
|
|
8489
|
+
const scope = finding.appId === PLATFORM_SCOPE ? "" : ` --app ${finding.appId}`;
|
|
8490
|
+
ctx.out.log("");
|
|
8491
|
+
ctx.out.log(`${finding.slug} [${finding.kind}]`);
|
|
8492
|
+
ctx.out.log(` ${finding.detail}`);
|
|
8493
|
+
ctx.out.log(` odla-ai runbook get ${finding.slug}${scope}`);
|
|
8494
|
+
}
|
|
8495
|
+
}
|
|
8496
|
+
|
|
8278
8497
|
// src/runbook-search-command.ts
|
|
8279
8498
|
async function runbookSearch(ctx, query, all, limit) {
|
|
8280
8499
|
const params = new URLSearchParams({ q: query });
|
|
@@ -8474,7 +8693,7 @@ function requireSlug(slug, action) {
|
|
|
8474
8693
|
return slug;
|
|
8475
8694
|
}
|
|
8476
8695
|
var WRITES = /* @__PURE__ */ new Set(["new", "edit", "publish", "archive", "visibility", "revert", "rm", "import"]);
|
|
8477
|
-
var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact"]);
|
|
8696
|
+
var CONFIG_FREE = /* @__PURE__ */ new Set(["list", "search", "ask", "get", "cat", "history", "impact", "lint"]);
|
|
8478
8697
|
async function loadOrDefaultConfig(configPath, action, appId) {
|
|
8479
8698
|
const projectScoped = appId !== void 0;
|
|
8480
8699
|
if (!projectScoped && CONFIG_FREE.has(action) && !(0, import_node_fs17.existsSync)((0, import_node_path14.resolve)(configPath))) {
|
|
@@ -8556,6 +8775,8 @@ async function runbookCommand(parsed, deps = {}) {
|
|
|
8556
8775
|
if (!question) throw new Error('"runbook ask" needs a question, e.g. odla-ai runbook ask "how do I roll back a publish?"');
|
|
8557
8776
|
return runbookAsk(ctx, question, parsed.options.all === true);
|
|
8558
8777
|
}
|
|
8778
|
+
case "lint":
|
|
8779
|
+
return runbookLint(ctx, parsed.options.all === true);
|
|
8559
8780
|
case "impact":
|
|
8560
8781
|
return runbookImpact(
|
|
8561
8782
|
ctx,
|
|
@@ -8624,9 +8845,7 @@ async function runbookCommand(parsed, deps = {}) {
|
|
|
8624
8845
|
case "rm":
|
|
8625
8846
|
return runbookRemove(ctx, requireSlug(slug, "rm"));
|
|
8626
8847
|
default:
|
|
8627
|
-
throw new Error(
|
|
8628
|
-
`unknown runbook action "${action}". Try ask, search, impact, list, get, new, edit, comment, import, visibility, publish, archive, history, revert, rm.`
|
|
8629
|
-
);
|
|
8848
|
+
throw new Error(`unknown runbook action "${action}". Try ${acceptedAfter(["runbook"]).join(", ")}.`);
|
|
8630
8849
|
}
|
|
8631
8850
|
}
|
|
8632
8851
|
|