@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/README.md +8 -0
- package/dist/bin.cjs +100 -16
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-DINDRUDA.js → chunk-7P7MI4WX.js} +101 -17
- package/dist/chunk-7P7MI4WX.js.map +1 -0
- package/dist/index.cjs +100 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-DINDRUDA.js.map +0 -1
package/README.md
CHANGED
|
@@ -43,12 +43,20 @@ app is registered, start each session by reading its doing tasks, open bugs and
|
|
|
43
43
|
goals, and recent decisions:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
+
npx odla-ai pm handoff --app <appId>
|
|
46
47
|
npx odla-ai pm task list --app <appId> --column doing
|
|
47
48
|
npx odla-ai pm bug list --app <appId> --status open
|
|
48
49
|
npx odla-ai pm goal list --app <appId> --status open
|
|
49
50
|
npx odla-ai pm decision list --app <appId> --limit 20
|
|
50
51
|
```
|
|
51
52
|
|
|
53
|
+
`pm handoff` is the authoritative unresolved-state view: unmet goals, every
|
|
54
|
+
non-done task, and every open or triaged bug. File bugs with actionable context
|
|
55
|
+
(`pm bug add ... --description "reproduction/evidence"`); a title-only bug is
|
|
56
|
+
rejected. When an accepted decision resolves or explicitly retains a bug, link
|
|
57
|
+
it with `pm bug done <id> --decision <decision-id>` or
|
|
58
|
+
`pm bug set <id> --status triaged --decision <decision-id>`.
|
|
59
|
+
|
|
52
60
|
Read the active task, linked goal, and comments before changing code. Without
|
|
53
61
|
`--app`, list commands span every project the account co-owns, which is the
|
|
54
62
|
right first view when coordinating several efforts; scope every write to the
|
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)
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 };
|
|
@@ -6424,13 +6426,14 @@ Usage:
|
|
|
6424
6426
|
odla-ai app owners add <email> [--email <odla-account>] [--json]
|
|
6425
6427
|
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
6426
6428
|
odla-ai pm <goal|task|decision|bug> list [--app <id>] [--status <s>] [--severity <s>] [--column <c>] [--q <text>] [--json]
|
|
6427
|
-
odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--body <text>|--proof <text>]
|
|
6429
|
+
odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>]
|
|
6428
6430
|
odla-ai pm <goal|task|decision|bug> get <id> [--json]
|
|
6429
|
-
odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>]
|
|
6430
|
-
odla-ai pm <goal|task|decision|bug> done <id>
|
|
6431
|
+
odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>]
|
|
6432
|
+
odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>]
|
|
6431
6433
|
odla-ai pm <goal|task|decision|bug> comment <id> --body "..."
|
|
6432
6434
|
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
6433
6435
|
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
6436
|
+
odla-ai pm handoff --app <id> [--json]
|
|
6434
6437
|
odla-ai whoami [--json]
|
|
6435
6438
|
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
6436
6439
|
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
@@ -7189,6 +7192,7 @@ var FIELD_MAP = {
|
|
|
7189
7192
|
column: { key: "column" },
|
|
7190
7193
|
rank: { key: "rank", num: true },
|
|
7191
7194
|
goal: { key: "goalId" },
|
|
7195
|
+
decision: { key: "decisionId" },
|
|
7192
7196
|
assignee: { key: "assigneeId" },
|
|
7193
7197
|
due: { key: "dueAt", num: true },
|
|
7194
7198
|
proof: { key: "proof" },
|
|
@@ -7227,6 +7231,14 @@ function collectFields(parsed, allowClear) {
|
|
|
7227
7231
|
}
|
|
7228
7232
|
return out;
|
|
7229
7233
|
}
|
|
7234
|
+
function collectEntityFields(entity, parsed, allowClear) {
|
|
7235
|
+
const fields = collectFields(parsed, allowClear);
|
|
7236
|
+
if ((entity === "task" || entity === "bug") && fields.body !== void 0) {
|
|
7237
|
+
if (fields.description === void 0) fields.description = fields.body;
|
|
7238
|
+
delete fields.body;
|
|
7239
|
+
}
|
|
7240
|
+
return fields;
|
|
7241
|
+
}
|
|
7230
7242
|
function statusCol(entity, r) {
|
|
7231
7243
|
if (entity === "bug") return `${r.status ?? ""}/${r.severity ?? ""}`;
|
|
7232
7244
|
if (entity === "task") return String(r.column ?? "");
|
|
@@ -7241,7 +7253,14 @@ function emit(ctx, value, human) {
|
|
|
7241
7253
|
}
|
|
7242
7254
|
async function pmList(ctx, entity, parsed) {
|
|
7243
7255
|
const q = new URLSearchParams();
|
|
7244
|
-
const filters = {
|
|
7256
|
+
const filters = {
|
|
7257
|
+
status: "status",
|
|
7258
|
+
severity: "severity",
|
|
7259
|
+
column: "column",
|
|
7260
|
+
goal: "goalId",
|
|
7261
|
+
assignee: "assigneeId",
|
|
7262
|
+
decision: "decisionId"
|
|
7263
|
+
};
|
|
7245
7264
|
const app = stringOpt(parsed.options.app);
|
|
7246
7265
|
if (app) q.set("app", app);
|
|
7247
7266
|
for (const [flag, param] of Object.entries(filters)) {
|
|
@@ -7263,8 +7282,10 @@ async function pmList(ctx, entity, parsed) {
|
|
|
7263
7282
|
async function pmAdd(ctx, entity, parsed) {
|
|
7264
7283
|
const appId = stringOpt(parsed.options.app);
|
|
7265
7284
|
if (!appId) throw new Error("pm add needs --app <appId>");
|
|
7266
|
-
const input =
|
|
7285
|
+
const input = collectEntityFields(entity, parsed, false);
|
|
7267
7286
|
if (!input.title) throw new Error("pm add needs --title <title>");
|
|
7287
|
+
if (entity === "bug" && !input.description)
|
|
7288
|
+
throw new Error("pm bug add needs --description <context> (or --body <context>)");
|
|
7268
7289
|
const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
|
|
7269
7290
|
emit(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
|
|
7270
7291
|
}
|
|
@@ -7273,16 +7294,71 @@ async function pmGet(ctx, entity, id) {
|
|
|
7273
7294
|
emit(ctx, record5, () => printRecord(ctx, entity, record5));
|
|
7274
7295
|
}
|
|
7275
7296
|
async function pmSet(ctx, entity, id, parsed) {
|
|
7276
|
-
const patch2 =
|
|
7297
|
+
const patch2 = collectEntityFields(entity, parsed, true);
|
|
7277
7298
|
if (Object.keys(patch2).length === 0)
|
|
7278
7299
|
throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
|
|
7279
7300
|
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
7280
7301
|
emit(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
|
|
7281
7302
|
}
|
|
7282
|
-
async function pmDone(ctx, entity, id) {
|
|
7283
|
-
const
|
|
7303
|
+
async function pmDone(ctx, entity, id, parsed) {
|
|
7304
|
+
const decisionId = stringOpt(parsed.options.decision);
|
|
7305
|
+
if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
|
|
7306
|
+
const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
|
|
7307
|
+
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
7284
7308
|
emit(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
|
|
7285
7309
|
}
|
|
7310
|
+
async function allRecords(ctx, entity, appId) {
|
|
7311
|
+
const records = [];
|
|
7312
|
+
for (; ; ) {
|
|
7313
|
+
const q = new URLSearchParams({
|
|
7314
|
+
app: appId,
|
|
7315
|
+
limit: "100",
|
|
7316
|
+
offset: String(records.length)
|
|
7317
|
+
});
|
|
7318
|
+
const page = await pmRequest(ctx, "GET", `/${entity}?${q}`);
|
|
7319
|
+
records.push(...page.records);
|
|
7320
|
+
if (records.length >= page.total || page.records.length === 0) return records;
|
|
7321
|
+
}
|
|
7322
|
+
}
|
|
7323
|
+
async function pmHandoff(ctx, parsed) {
|
|
7324
|
+
const appId = stringOpt(parsed.options.app);
|
|
7325
|
+
if (!appId) throw new Error("pm handoff needs --app <appId>");
|
|
7326
|
+
const [goals, tasks, bugs] = await Promise.all([
|
|
7327
|
+
allRecords(ctx, "goal", appId),
|
|
7328
|
+
allRecords(ctx, "task", appId),
|
|
7329
|
+
allRecords(ctx, "bug", appId)
|
|
7330
|
+
]);
|
|
7331
|
+
const handoff = {
|
|
7332
|
+
appId,
|
|
7333
|
+
unmetGoals: goals.filter((record5) => record5.status !== "met"),
|
|
7334
|
+
activeTasks: tasks.filter((record5) => record5.column !== "done"),
|
|
7335
|
+
openBugs: bugs.filter((record5) => record5.status !== "fixed" && record5.status !== "wontfix")
|
|
7336
|
+
};
|
|
7337
|
+
const result = {
|
|
7338
|
+
...handoff,
|
|
7339
|
+
clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
|
|
7340
|
+
};
|
|
7341
|
+
emit(ctx, result, () => {
|
|
7342
|
+
if (result.clean) {
|
|
7343
|
+
ctx.out.log(`${appId}: no unresolved PM work`);
|
|
7344
|
+
return;
|
|
7345
|
+
}
|
|
7346
|
+
ctx.out.log(`${appId}: authoritative PM handoff`);
|
|
7347
|
+
for (const [label, records] of [
|
|
7348
|
+
["unmet goals", result.unmetGoals],
|
|
7349
|
+
["active tasks", result.activeTasks],
|
|
7350
|
+
["open bugs", result.openBugs]
|
|
7351
|
+
]) {
|
|
7352
|
+
ctx.out.log(`${label}:`);
|
|
7353
|
+
if (!records.length) ctx.out.log("- (none)");
|
|
7354
|
+
else for (const record5 of records) printRecord(
|
|
7355
|
+
ctx,
|
|
7356
|
+
label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
|
|
7357
|
+
record5
|
|
7358
|
+
);
|
|
7359
|
+
}
|
|
7360
|
+
});
|
|
7361
|
+
}
|
|
7286
7362
|
async function pmComment(ctx, entity, id, parsed) {
|
|
7287
7363
|
const body = stringOpt(parsed.options.body);
|
|
7288
7364
|
if (!body) throw new Error('pm comment needs --body "..."');
|
|
@@ -7333,6 +7409,7 @@ var ALLOWED = [
|
|
|
7333
7409
|
"target",
|
|
7334
7410
|
"description",
|
|
7335
7411
|
"desc",
|
|
7412
|
+
"decision",
|
|
7336
7413
|
"limit",
|
|
7337
7414
|
"offset",
|
|
7338
7415
|
"q"
|
|
@@ -7355,6 +7432,10 @@ async function buildContext(parsed, deps) {
|
|
|
7355
7432
|
}
|
|
7356
7433
|
async function pmCommand(parsed, deps = {}) {
|
|
7357
7434
|
const word = parsed.positionals[1] ?? "";
|
|
7435
|
+
if (word === "handoff") {
|
|
7436
|
+
assertArgs(parsed, ALLOWED, 2);
|
|
7437
|
+
return pmHandoff(await buildContext(parsed, deps), parsed);
|
|
7438
|
+
}
|
|
7358
7439
|
const entity = ALIASES[word];
|
|
7359
7440
|
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
7360
7441
|
const action = parsed.positionals[2] ?? "list";
|
|
@@ -7375,7 +7456,7 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
7375
7456
|
case "move":
|
|
7376
7457
|
return pmSet(ctx, entity, requireId(id, action), parsed);
|
|
7377
7458
|
case "done":
|
|
7378
|
-
return pmDone(ctx, entity, requireId(id, action));
|
|
7459
|
+
return pmDone(ctx, entity, requireId(id, action), parsed);
|
|
7379
7460
|
case "comment":
|
|
7380
7461
|
return pmComment(ctx, entity, requireId(id, action), parsed);
|
|
7381
7462
|
case "comments":
|
|
@@ -7851,7 +7932,10 @@ var COMMAND_SURFACE = {
|
|
|
7851
7932
|
doctor: {},
|
|
7852
7933
|
help: {},
|
|
7853
7934
|
init: {},
|
|
7854
|
-
pm:
|
|
7935
|
+
pm: {
|
|
7936
|
+
...PM_ENTITIES,
|
|
7937
|
+
handoff: {}
|
|
7938
|
+
},
|
|
7855
7939
|
provision: {},
|
|
7856
7940
|
runbook: {
|
|
7857
7941
|
ask: {},
|