@odla-ai/cli 0.22.0 → 0.23.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 CHANGED
@@ -37,6 +37,26 @@ npx @odla-ai/cli runbook list # what exists
37
37
  Reading works for any signed-in developer and needs no `odla.config.mjs` — the
38
38
  advice is most useful before there is a project.
39
39
 
40
+ Runbooks tell an agent how to operate; PM tells every agent what this project is
41
+ trying to prove, what is active, what was decided, and what is broken. Once the
42
+ app is registered, start each session by reading its doing tasks, open bugs and
43
+ goals, and recent decisions:
44
+
45
+ ```bash
46
+ npx odla-ai pm task list --app <appId> --column doing
47
+ npx odla-ai pm bug list --app <appId> --status open
48
+ npx odla-ai pm goal list --app <appId> --status open
49
+ npx odla-ai pm decision list --app <appId> --limit 20
50
+ ```
51
+
52
+ Read the active task, linked goal, and comments before changing code. Without
53
+ `--app`, list commands span every project the account co-owns, which is the
54
+ right first view when coordinating several efforts; scope every write to the
55
+ exact `appId` from that project's config. PM requires a registered app. Before
56
+ the first provision, use a focused commit/checkpoint, then initialize PM and
57
+ backfill that evidence immediately after registration. Never use a parallel
58
+ status diary or put a secret in PM.
59
+
40
60
  **Runbooks are procedure; JSDoc is API.** What a function takes, returns, and
41
61
  guarantees is documented on the export itself, ships in the installed
42
62
  `.d.ts`, and renders per package at <https://odla.ai/docs>. A runbook will tell
@@ -93,11 +113,14 @@ npx @odla-ai/cli init --app-id my-app --name "My App"
93
113
  ```
94
114
 
95
115
  For a project you keep working in, install it as a dev dependency so the
96
- shorter `npx odla-ai` form works and the version is pinned by your lockfile:
116
+ shorter `npx odla-ai` form works. ODLA is under active development: use a normal
117
+ dependency declaration, commit the lockfile, and let `npm ci` reproduce the
118
+ resolved graph:
97
119
 
98
120
  ```bash
99
- npm view @odla-ai/cli@0.16.6 version
100
- npm i -D --save-exact @odla-ai/cli@0.16.6
121
+ npm view @odla-ai/cli version
122
+ npm i -D @odla-ai/cli
123
+ npm ls @odla-ai/cli
101
124
  ```
102
125
 
103
126
  ## Prerequisites
@@ -360,8 +383,8 @@ rolling-window/age-out, quota-backoff, retention/purge, or provider-derived
360
383
  mutation-id control. Public health remains aggregate rather than per calendar.
361
384
 
362
385
  The bundled build/migration skills add a passive pre-ship gate with
363
- `@odla-ai/security`: after the exact-version availability check, run
364
- `npm i -D --save-exact @odla-ai/security@0.3.1`, then
386
+ `@odla-ai/security`: install it with a normal development range, commit the
387
+ lockfile, then run `npm i -D @odla-ai/security`, followed by
365
388
  `npx odla-security scan . --profile odla --out .odla/security/pre-ship --fail-on high --fail-on-candidates critical`.
366
389
  The passive scan remains a separate binary: it makes no model calls and does
367
390
  not execute target code. After explicit redacted-source approval,
@@ -385,12 +408,11 @@ a conservative `Retry-After`. A provider-side `429` uses the sanitized
385
408
  carries a bounded `Retry-After` only when the upstream supplied one. The CLI
386
409
  does not echo arbitrary response text or silently change the admin-selected
387
410
  model.
388
- Before installing the exact release, require
389
- `npm view @odla-ai/security@0.3.1 version` to succeed before installation. An
390
- exact-version `E404` means the release is unavailable, not that the security
391
- preflight passed, and does not prove the package name is absent. odla's own
392
- engineering environment uses an equivalent internal gate; customer projects
393
- should use the published `odla-security` command above.
411
+ Confirm `npm view @odla-ai/security version` succeeds and record the installed
412
+ version from `npm ls @odla-ai/security` with the scan evidence. A registry
413
+ failure blocks the preflight; it is not a clean scan. odla's own engineering
414
+ environment uses an equivalent internal gate; customer projects should use the
415
+ published `odla-security` command above.
394
416
 
395
417
  For a server-side, commit-pinned review, connect one repository through the
396
418
  source-read-only odla GitHub App. The CLI infers `owner/name` from a safe GitHub HTTPS
package/dist/bin.cjs CHANGED
@@ -1390,6 +1390,7 @@ function chooseIdMode(options, rows) {
1390
1390
  async function appImport(options) {
1391
1391
  const cfg = await loadProjectConfig(options.configPath);
1392
1392
  const out = options.stdout ?? console;
1393
+ const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
1393
1394
  const doFetch = options.fetch ?? fetch;
1394
1395
  const { tenant } = resolveTenant(cfg, options.env);
1395
1396
  const text = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs6.readFileSync)(0, "utf8")))() : (0, import_node_fs6.readFileSync)(options.file, "utf8");
@@ -1411,10 +1412,10 @@ ${detail}${more}`);
1411
1412
  }
1412
1413
  const chunks = (0, import_import.planImportChunks)(ops);
1413
1414
  const result = { tenant, rows: ops.length, chunks: chunks.length, committed: 0, duplicate: 0, txIds: [], ignored };
1414
- if (ignored.length > 0) out.log(`ignoring server-maintained field(s): ${ignored.join(", ")} \u2014 odla-db stamps these on write`);
1415
+ if (ignored.length > 0) say(`ignoring server-maintained field(s): ${ignored.join(", ")} \u2014 odla-db stamps these on write`);
1415
1416
  if (options.dryRun || options.yes !== true) {
1416
1417
  const why = options.dryRun ? "--dry-run" : "no --yes";
1417
- out.log(`${tenant}: would upsert ${ops.length} row(s) across ${chunks.length} transaction(s) \u2014 nothing written (${why})`);
1418
+ say(`${tenant}: would upsert ${ops.length} row(s) across ${chunks.length} transaction(s) \u2014 nothing written (${why})`);
1418
1419
  if (options.json) out.log(JSON.stringify(result, null, 2));
1419
1420
  return result;
1420
1421
  }
@@ -1623,6 +1624,7 @@ function printPlan(out, verb, pre, opts) {
1623
1624
  async function appTransfer(options) {
1624
1625
  const cfg = await loadProjectConfig(options.configPath);
1625
1626
  const out = options.stdout ?? console;
1627
+ const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
1626
1628
  const doFetch = options.fetch ?? fetch;
1627
1629
  const tenants = bothTenants(cfg);
1628
1630
  const route2 = endpointsFor(options.verb, tenants);
@@ -1643,7 +1645,7 @@ async function appTransfer(options) {
1643
1645
  throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
1644
1646
  }
1645
1647
  const plan = pre.body;
1646
- printPlan(out, options.verb, plan, options);
1648
+ printPlan({ log: say }, options.verb, plan, options);
1647
1649
  if (options.verb === "go-live" && !plan.targetEmpty) {
1648
1650
  throw new Error(
1649
1651
  `${route2.target} already has data \u2014 go-live has already happened. Use \`odla-ai app promote --yes\` to push schema and rules, or \`odla-ai app refresh-sandbox --yes\` to pull live back down.`
@@ -1651,7 +1653,7 @@ async function appTransfer(options) {
1651
1653
  }
1652
1654
  if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
1653
1655
  if (options.dryRun || options.yes !== true) {
1654
- out.log(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
1656
+ say(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
1655
1657
  if (options.json) out.log(JSON.stringify(plan, null, 2));
1656
1658
  return { ok: true, plan };
1657
1659
  }
@@ -1665,7 +1667,7 @@ async function appTransfer(options) {
1665
1667
  );
1666
1668
  if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
1667
1669
  if (options.json) out.log(JSON.stringify(res2.body, null, 2));
1668
- else out.log(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
1670
+ else say(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
1669
1671
  return { ok: true, plan, result: res2.body };
1670
1672
  }
1671
1673
  const res = await api(
@@ -1684,7 +1686,7 @@ async function appTransfer(options) {
1684
1686
  doFetch
1685
1687
  );
1686
1688
  if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
1687
- out.log(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
1689
+ say(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
1688
1690
  if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
1689
1691
  if (options.json) out.log(JSON.stringify(res.body, null, 2));
1690
1692
  return { ok: true, plan, result: res.body };