@odla-ai/cli 0.22.1 → 0.24.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-DINDRUDA.js";
5
+ } from "./chunk-7P7MI4WX.js";
6
6
 
7
7
  // src/bin.ts
8
8
  runCli().catch((err) => {
@@ -5931,13 +5931,14 @@ Usage:
5931
5931
  odla-ai app owners add <email> [--email <odla-account>] [--json]
5932
5932
  odla-ai app owners remove <email> [--email <odla-account>] [--json]
5933
5933
  odla-ai pm <goal|task|decision|bug> list [--app <id>] [--status <s>] [--severity <s>] [--column <c>] [--q <text>] [--json]
5934
- odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--body <text>|--proof <text>]
5934
+ odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>]
5935
5935
  odla-ai pm <goal|task|decision|bug> get <id> [--json]
5936
- odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>]
5937
- odla-ai pm <goal|task|decision|bug> done <id>
5936
+ odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>]
5937
+ odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>]
5938
5938
  odla-ai pm <goal|task|decision|bug> comment <id> --body "..."
5939
5939
  odla-ai pm <goal|task|decision|bug> comments <id> [--json]
5940
5940
  odla-ai pm <goal|task|decision|bug> rm <id>
5941
+ odla-ai pm handoff --app <id> [--json]
5941
5942
  odla-ai whoami [--json]
5942
5943
  odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
5943
5944
  odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
@@ -6784,7 +6785,10 @@ var COMMAND_SURFACE = {
6784
6785
  doctor: {},
6785
6786
  help: {},
6786
6787
  init: {},
6787
- pm: PM_ENTITIES,
6788
+ pm: {
6789
+ ...PM_ENTITIES,
6790
+ handoff: {}
6791
+ },
6788
6792
  provision: {},
6789
6793
  runbook: {
6790
6794
  ask: {},
@@ -7367,6 +7371,7 @@ function chooseIdMode(options, rows) {
7367
7371
  async function appImport(options) {
7368
7372
  const cfg = await loadProjectConfig(options.configPath);
7369
7373
  const out = options.stdout ?? console;
7374
+ const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
7370
7375
  const doFetch = options.fetch ?? fetch;
7371
7376
  const { tenant } = resolveTenant(cfg, options.env);
7372
7377
  const text = options.file === "-" ? (options.readStdin ?? (() => readFileSync7(0, "utf8")))() : readFileSync7(options.file, "utf8");
@@ -7388,10 +7393,10 @@ ${detail}${more}`);
7388
7393
  }
7389
7394
  const chunks = planImportChunks(ops);
7390
7395
  const result = { tenant, rows: ops.length, chunks: chunks.length, committed: 0, duplicate: 0, txIds: [], ignored };
7391
- if (ignored.length > 0) out.log(`ignoring server-maintained field(s): ${ignored.join(", ")} \u2014 odla-db stamps these on write`);
7396
+ if (ignored.length > 0) say(`ignoring server-maintained field(s): ${ignored.join(", ")} \u2014 odla-db stamps these on write`);
7392
7397
  if (options.dryRun || options.yes !== true) {
7393
7398
  const why = options.dryRun ? "--dry-run" : "no --yes";
7394
- out.log(`${tenant}: would upsert ${ops.length} row(s) across ${chunks.length} transaction(s) \u2014 nothing written (${why})`);
7399
+ say(`${tenant}: would upsert ${ops.length} row(s) across ${chunks.length} transaction(s) \u2014 nothing written (${why})`);
7395
7400
  if (options.json) out.log(JSON.stringify(result, null, 2));
7396
7401
  return result;
7397
7402
  }
@@ -7600,6 +7605,7 @@ function printPlan(out, verb, pre, opts) {
7600
7605
  async function appTransfer(options) {
7601
7606
  const cfg = await loadProjectConfig(options.configPath);
7602
7607
  const out = options.stdout ?? console;
7608
+ const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
7603
7609
  const doFetch = options.fetch ?? fetch;
7604
7610
  const tenants = bothTenants(cfg);
7605
7611
  const route2 = endpointsFor(options.verb, tenants);
@@ -7620,7 +7626,7 @@ async function appTransfer(options) {
7620
7626
  throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
7621
7627
  }
7622
7628
  const plan = pre.body;
7623
- printPlan(out, options.verb, plan, options);
7629
+ printPlan({ log: say }, options.verb, plan, options);
7624
7630
  if (options.verb === "go-live" && !plan.targetEmpty) {
7625
7631
  throw new Error(
7626
7632
  `${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.`
@@ -7628,7 +7634,7 @@ async function appTransfer(options) {
7628
7634
  }
7629
7635
  if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
7630
7636
  if (options.dryRun || options.yes !== true) {
7631
- out.log(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
7637
+ say(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
7632
7638
  if (options.json) out.log(JSON.stringify(plan, null, 2));
7633
7639
  return { ok: true, plan };
7634
7640
  }
@@ -7642,7 +7648,7 @@ async function appTransfer(options) {
7642
7648
  );
7643
7649
  if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
7644
7650
  if (options.json) out.log(JSON.stringify(res2.body, null, 2));
7645
- else out.log(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
7651
+ else say(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
7646
7652
  return { ok: true, plan, result: res2.body };
7647
7653
  }
7648
7654
  const res = await api(
@@ -7661,7 +7667,7 @@ async function appTransfer(options) {
7661
7667
  doFetch
7662
7668
  );
7663
7669
  if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
7664
- out.log(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
7670
+ say(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
7665
7671
  if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
7666
7672
  if (options.json) out.log(JSON.stringify(res.body, null, 2));
7667
7673
  return { ok: true, plan, result: res.body };
@@ -7980,6 +7986,7 @@ var FIELD_MAP = {
7980
7986
  column: { key: "column" },
7981
7987
  rank: { key: "rank", num: true },
7982
7988
  goal: { key: "goalId" },
7989
+ decision: { key: "decisionId" },
7983
7990
  assignee: { key: "assigneeId" },
7984
7991
  due: { key: "dueAt", num: true },
7985
7992
  proof: { key: "proof" },
@@ -8018,6 +8025,14 @@ function collectFields(parsed, allowClear) {
8018
8025
  }
8019
8026
  return out;
8020
8027
  }
8028
+ function collectEntityFields(entity, parsed, allowClear) {
8029
+ const fields = collectFields(parsed, allowClear);
8030
+ if ((entity === "task" || entity === "bug") && fields.body !== void 0) {
8031
+ if (fields.description === void 0) fields.description = fields.body;
8032
+ delete fields.body;
8033
+ }
8034
+ return fields;
8035
+ }
8021
8036
  function statusCol(entity, r) {
8022
8037
  if (entity === "bug") return `${r.status ?? ""}/${r.severity ?? ""}`;
8023
8038
  if (entity === "task") return String(r.column ?? "");
@@ -8032,7 +8047,14 @@ function emit(ctx, value, human) {
8032
8047
  }
8033
8048
  async function pmList(ctx, entity, parsed) {
8034
8049
  const q = new URLSearchParams();
8035
- const filters = { status: "status", severity: "severity", column: "column", goal: "goalId", assignee: "assigneeId" };
8050
+ const filters = {
8051
+ status: "status",
8052
+ severity: "severity",
8053
+ column: "column",
8054
+ goal: "goalId",
8055
+ assignee: "assigneeId",
8056
+ decision: "decisionId"
8057
+ };
8036
8058
  const app = stringOpt(parsed.options.app);
8037
8059
  if (app) q.set("app", app);
8038
8060
  for (const [flag, param] of Object.entries(filters)) {
@@ -8054,8 +8076,10 @@ async function pmList(ctx, entity, parsed) {
8054
8076
  async function pmAdd(ctx, entity, parsed) {
8055
8077
  const appId = stringOpt(parsed.options.app);
8056
8078
  if (!appId) throw new Error("pm add needs --app <appId>");
8057
- const input = collectFields(parsed, false);
8079
+ const input = collectEntityFields(entity, parsed, false);
8058
8080
  if (!input.title) throw new Error("pm add needs --title <title>");
8081
+ if (entity === "bug" && !input.description)
8082
+ throw new Error("pm bug add needs --description <context> (or --body <context>)");
8059
8083
  const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
8060
8084
  emit(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
8061
8085
  }
@@ -8064,16 +8088,71 @@ async function pmGet(ctx, entity, id) {
8064
8088
  emit(ctx, record5, () => printRecord(ctx, entity, record5));
8065
8089
  }
8066
8090
  async function pmSet(ctx, entity, id, parsed) {
8067
- const patch2 = collectFields(parsed, true);
8091
+ const patch2 = collectEntityFields(entity, parsed, true);
8068
8092
  if (Object.keys(patch2).length === 0)
8069
8093
  throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
8070
8094
  const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
8071
8095
  emit(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
8072
8096
  }
8073
- async function pmDone(ctx, entity, id) {
8074
- const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: DONE[entity] });
8097
+ async function pmDone(ctx, entity, id, parsed) {
8098
+ const decisionId = stringOpt(parsed.options.decision);
8099
+ if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
8100
+ const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
8101
+ const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
8075
8102
  emit(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
8076
8103
  }
8104
+ async function allRecords(ctx, entity, appId) {
8105
+ const records = [];
8106
+ for (; ; ) {
8107
+ const q = new URLSearchParams({
8108
+ app: appId,
8109
+ limit: "100",
8110
+ offset: String(records.length)
8111
+ });
8112
+ const page = await pmRequest(ctx, "GET", `/${entity}?${q}`);
8113
+ records.push(...page.records);
8114
+ if (records.length >= page.total || page.records.length === 0) return records;
8115
+ }
8116
+ }
8117
+ async function pmHandoff(ctx, parsed) {
8118
+ const appId = stringOpt(parsed.options.app);
8119
+ if (!appId) throw new Error("pm handoff needs --app <appId>");
8120
+ const [goals, tasks, bugs] = await Promise.all([
8121
+ allRecords(ctx, "goal", appId),
8122
+ allRecords(ctx, "task", appId),
8123
+ allRecords(ctx, "bug", appId)
8124
+ ]);
8125
+ const handoff = {
8126
+ appId,
8127
+ unmetGoals: goals.filter((record5) => record5.status !== "met"),
8128
+ activeTasks: tasks.filter((record5) => record5.column !== "done"),
8129
+ openBugs: bugs.filter((record5) => record5.status !== "fixed" && record5.status !== "wontfix")
8130
+ };
8131
+ const result = {
8132
+ ...handoff,
8133
+ clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
8134
+ };
8135
+ emit(ctx, result, () => {
8136
+ if (result.clean) {
8137
+ ctx.out.log(`${appId}: no unresolved PM work`);
8138
+ return;
8139
+ }
8140
+ ctx.out.log(`${appId}: authoritative PM handoff`);
8141
+ for (const [label, records] of [
8142
+ ["unmet goals", result.unmetGoals],
8143
+ ["active tasks", result.activeTasks],
8144
+ ["open bugs", result.openBugs]
8145
+ ]) {
8146
+ ctx.out.log(`${label}:`);
8147
+ if (!records.length) ctx.out.log("- (none)");
8148
+ else for (const record5 of records) printRecord(
8149
+ ctx,
8150
+ label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
8151
+ record5
8152
+ );
8153
+ }
8154
+ });
8155
+ }
8077
8156
  async function pmComment(ctx, entity, id, parsed) {
8078
8157
  const body = stringOpt(parsed.options.body);
8079
8158
  if (!body) throw new Error('pm comment needs --body "..."');
@@ -8124,6 +8203,7 @@ var ALLOWED = [
8124
8203
  "target",
8125
8204
  "description",
8126
8205
  "desc",
8206
+ "decision",
8127
8207
  "limit",
8128
8208
  "offset",
8129
8209
  "q"
@@ -8146,6 +8226,10 @@ async function buildContext(parsed, deps) {
8146
8226
  }
8147
8227
  async function pmCommand(parsed, deps = {}) {
8148
8228
  const word = parsed.positionals[1] ?? "";
8229
+ if (word === "handoff") {
8230
+ assertArgs(parsed, ALLOWED, 2);
8231
+ return pmHandoff(await buildContext(parsed, deps), parsed);
8232
+ }
8149
8233
  const entity = ALIASES[word];
8150
8234
  if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
8151
8235
  const action = parsed.positionals[2] ?? "list";
@@ -8166,7 +8250,7 @@ async function pmCommand(parsed, deps = {}) {
8166
8250
  case "move":
8167
8251
  return pmSet(ctx, entity, requireId(id, action), parsed);
8168
8252
  case "done":
8169
- return pmDone(ctx, entity, requireId(id, action));
8253
+ return pmDone(ctx, entity, requireId(id, action), parsed);
8170
8254
  case "comment":
8171
8255
  return pmComment(ctx, entity, requireId(id, action), parsed);
8172
8256
  case "comments":
@@ -9747,4 +9831,4 @@ export {
9747
9831
  exitCodeFor,
9748
9832
  runCli
9749
9833
  };
9750
- //# sourceMappingURL=chunk-DINDRUDA.js.map
9834
+ //# sourceMappingURL=chunk-7P7MI4WX.js.map