@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/dist/index.cjs
CHANGED
|
@@ -1450,6 +1450,7 @@ function chooseIdMode(options, rows) {
|
|
|
1450
1450
|
async function appImport(options) {
|
|
1451
1451
|
const cfg = await loadProjectConfig(options.configPath);
|
|
1452
1452
|
const out = options.stdout ?? console;
|
|
1453
|
+
const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
|
|
1453
1454
|
const doFetch = options.fetch ?? fetch;
|
|
1454
1455
|
const { tenant } = resolveTenant(cfg, options.env);
|
|
1455
1456
|
const text = options.file === "-" ? (options.readStdin ?? (() => (0, import_node_fs6.readFileSync)(0, "utf8")))() : (0, import_node_fs6.readFileSync)(options.file, "utf8");
|
|
@@ -1471,10 +1472,10 @@ ${detail}${more}`);
|
|
|
1471
1472
|
}
|
|
1472
1473
|
const chunks = (0, import_import.planImportChunks)(ops);
|
|
1473
1474
|
const result = { tenant, rows: ops.length, chunks: chunks.length, committed: 0, duplicate: 0, txIds: [], ignored };
|
|
1474
|
-
if (ignored.length > 0)
|
|
1475
|
+
if (ignored.length > 0) say(`ignoring server-maintained field(s): ${ignored.join(", ")} \u2014 odla-db stamps these on write`);
|
|
1475
1476
|
if (options.dryRun || options.yes !== true) {
|
|
1476
1477
|
const why = options.dryRun ? "--dry-run" : "no --yes";
|
|
1477
|
-
|
|
1478
|
+
say(`${tenant}: would upsert ${ops.length} row(s) across ${chunks.length} transaction(s) \u2014 nothing written (${why})`);
|
|
1478
1479
|
if (options.json) out.log(JSON.stringify(result, null, 2));
|
|
1479
1480
|
return result;
|
|
1480
1481
|
}
|
|
@@ -1683,6 +1684,7 @@ function printPlan(out, verb, pre, opts) {
|
|
|
1683
1684
|
async function appTransfer(options) {
|
|
1684
1685
|
const cfg = await loadProjectConfig(options.configPath);
|
|
1685
1686
|
const out = options.stdout ?? console;
|
|
1687
|
+
const say = options.json ? (line) => out.error(line) : (line) => out.log(line);
|
|
1686
1688
|
const doFetch = options.fetch ?? fetch;
|
|
1687
1689
|
const tenants = bothTenants(cfg);
|
|
1688
1690
|
const route2 = endpointsFor(options.verb, tenants);
|
|
@@ -1703,7 +1705,7 @@ async function appTransfer(options) {
|
|
|
1703
1705
|
throw new Error(`pre-flight failed${pre.body.error?.code ? ` (${pre.body.error.code})` : ""}: ${pre.body.error?.message ?? pre.status}`);
|
|
1704
1706
|
}
|
|
1705
1707
|
const plan = pre.body;
|
|
1706
|
-
printPlan(
|
|
1708
|
+
printPlan({ log: say }, options.verb, plan, options);
|
|
1707
1709
|
if (options.verb === "go-live" && !plan.targetEmpty) {
|
|
1708
1710
|
throw new Error(
|
|
1709
1711
|
`${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.`
|
|
@@ -1711,7 +1713,7 @@ async function appTransfer(options) {
|
|
|
1711
1713
|
}
|
|
1712
1714
|
if (plan.blockers.length > 0) throw new Error(`cannot ${options.verb}: ${plan.blockers.map((b) => b.message).join("; ")}`);
|
|
1713
1715
|
if (options.dryRun || options.yes !== true) {
|
|
1714
|
-
|
|
1716
|
+
say(`nothing written (${options.dryRun ? "--dry-run" : "no --yes"})`);
|
|
1715
1717
|
if (options.json) out.log(JSON.stringify(plan, null, 2));
|
|
1716
1718
|
return { ok: true, plan };
|
|
1717
1719
|
}
|
|
@@ -1725,7 +1727,7 @@ async function appTransfer(options) {
|
|
|
1725
1727
|
);
|
|
1726
1728
|
if (res2.status !== 200) throw new Error(`promote failed${res2.body.error?.code ? ` (${res2.body.error.code})` : ""}: ${res2.body.error?.message ?? res2.status}`);
|
|
1727
1729
|
if (options.json) out.log(JSON.stringify(res2.body, null, 2));
|
|
1728
|
-
else
|
|
1730
|
+
else say(`${route2.target}: promoted ${(res2.body.applied ?? []).join(", ")}`);
|
|
1729
1731
|
return { ok: true, plan, result: res2.body };
|
|
1730
1732
|
}
|
|
1731
1733
|
const res = await api(
|
|
@@ -1744,7 +1746,7 @@ async function appTransfer(options) {
|
|
|
1744
1746
|
doFetch
|
|
1745
1747
|
);
|
|
1746
1748
|
if (res.status !== 200) throw new Error(`${options.verb} failed${res.body.error?.code ? ` (${res.body.error.code})` : ""}: ${res.body.error?.message ?? res.status}`);
|
|
1747
|
-
|
|
1749
|
+
say(`${route2.target}: replaced from ${route2.source} (tx ${res.body.destination?.maxTx}, epoch ${res.body.destination?.epoch}) \u2014 connected clients resync automatically`);
|
|
1748
1750
|
if (options.includeFiles) await copyFiles(cfg, token, route2, doFetch, out);
|
|
1749
1751
|
if (options.json) out.log(JSON.stringify(res.body, null, 2));
|
|
1750
1752
|
return { ok: true, plan, result: res.body };
|
|
@@ -6484,13 +6486,14 @@ Usage:
|
|
|
6484
6486
|
odla-ai app owners add <email> [--email <odla-account>] [--json]
|
|
6485
6487
|
odla-ai app owners remove <email> [--email <odla-account>] [--json]
|
|
6486
6488
|
odla-ai pm <goal|task|decision|bug> list [--app <id>] [--status <s>] [--severity <s>] [--column <c>] [--q <text>] [--json]
|
|
6487
|
-
odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--body <text>|--proof <text>]
|
|
6489
|
+
odla-ai pm <goal|task|decision|bug> add --app <id> --title <t> [--severity high|--column doing|--goal <id>|--description <text>|--body <text>|--proof <text>]
|
|
6488
6490
|
odla-ai pm <goal|task|decision|bug> get <id> [--json]
|
|
6489
|
-
odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>]
|
|
6490
|
-
odla-ai pm <goal|task|decision|bug> done <id>
|
|
6491
|
+
odla-ai pm <goal|task|decision|bug> set <id> [--status <s>|--column <c>|--assignee <id>|--no-assignee|--goal <id>|--decision <id>]
|
|
6492
|
+
odla-ai pm <goal|task|decision|bug> done <id> [--decision <accepted-decision-id>]
|
|
6491
6493
|
odla-ai pm <goal|task|decision|bug> comment <id> --body "..."
|
|
6492
6494
|
odla-ai pm <goal|task|decision|bug> comments <id> [--json]
|
|
6493
6495
|
odla-ai pm <goal|task|decision|bug> rm <id>
|
|
6496
|
+
odla-ai pm handoff --app <id> [--json]
|
|
6494
6497
|
odla-ai whoami [--json]
|
|
6495
6498
|
odla-ai runbook ask "<question>" [--app <id>] [--all] [--json]
|
|
6496
6499
|
odla-ai runbook search "<question>" [--app <id>] [--all] [--limit <n>] [--json]
|
|
@@ -7249,6 +7252,7 @@ var FIELD_MAP = {
|
|
|
7249
7252
|
column: { key: "column" },
|
|
7250
7253
|
rank: { key: "rank", num: true },
|
|
7251
7254
|
goal: { key: "goalId" },
|
|
7255
|
+
decision: { key: "decisionId" },
|
|
7252
7256
|
assignee: { key: "assigneeId" },
|
|
7253
7257
|
due: { key: "dueAt", num: true },
|
|
7254
7258
|
proof: { key: "proof" },
|
|
@@ -7287,6 +7291,14 @@ function collectFields(parsed, allowClear) {
|
|
|
7287
7291
|
}
|
|
7288
7292
|
return out;
|
|
7289
7293
|
}
|
|
7294
|
+
function collectEntityFields(entity, parsed, allowClear) {
|
|
7295
|
+
const fields = collectFields(parsed, allowClear);
|
|
7296
|
+
if ((entity === "task" || entity === "bug") && fields.body !== void 0) {
|
|
7297
|
+
if (fields.description === void 0) fields.description = fields.body;
|
|
7298
|
+
delete fields.body;
|
|
7299
|
+
}
|
|
7300
|
+
return fields;
|
|
7301
|
+
}
|
|
7290
7302
|
function statusCol(entity, r) {
|
|
7291
7303
|
if (entity === "bug") return `${r.status ?? ""}/${r.severity ?? ""}`;
|
|
7292
7304
|
if (entity === "task") return String(r.column ?? "");
|
|
@@ -7301,7 +7313,14 @@ function emit(ctx, value, human) {
|
|
|
7301
7313
|
}
|
|
7302
7314
|
async function pmList(ctx, entity, parsed) {
|
|
7303
7315
|
const q = new URLSearchParams();
|
|
7304
|
-
const filters = {
|
|
7316
|
+
const filters = {
|
|
7317
|
+
status: "status",
|
|
7318
|
+
severity: "severity",
|
|
7319
|
+
column: "column",
|
|
7320
|
+
goal: "goalId",
|
|
7321
|
+
assignee: "assigneeId",
|
|
7322
|
+
decision: "decisionId"
|
|
7323
|
+
};
|
|
7305
7324
|
const app = stringOpt(parsed.options.app);
|
|
7306
7325
|
if (app) q.set("app", app);
|
|
7307
7326
|
for (const [flag, param] of Object.entries(filters)) {
|
|
@@ -7323,8 +7342,10 @@ async function pmList(ctx, entity, parsed) {
|
|
|
7323
7342
|
async function pmAdd(ctx, entity, parsed) {
|
|
7324
7343
|
const appId = stringOpt(parsed.options.app);
|
|
7325
7344
|
if (!appId) throw new Error("pm add needs --app <appId>");
|
|
7326
|
-
const input =
|
|
7345
|
+
const input = collectEntityFields(entity, parsed, false);
|
|
7327
7346
|
if (!input.title) throw new Error("pm add needs --title <title>");
|
|
7347
|
+
if (entity === "bug" && !input.description)
|
|
7348
|
+
throw new Error("pm bug add needs --description <context> (or --body <context>)");
|
|
7328
7349
|
const res = await pmRequest(ctx, "POST", `/${entity}`, { appId, input });
|
|
7329
7350
|
emit(ctx, res, () => ctx.out.log(`created ${entity} ${res.id}`));
|
|
7330
7351
|
}
|
|
@@ -7333,16 +7354,71 @@ async function pmGet(ctx, entity, id) {
|
|
|
7333
7354
|
emit(ctx, record5, () => printRecord(ctx, entity, record5));
|
|
7334
7355
|
}
|
|
7335
7356
|
async function pmSet(ctx, entity, id, parsed) {
|
|
7336
|
-
const patch2 =
|
|
7357
|
+
const patch2 = collectEntityFields(entity, parsed, true);
|
|
7337
7358
|
if (Object.keys(patch2).length === 0)
|
|
7338
7359
|
throw new Error("pm set needs at least one field flag (e.g. --status doing, --assignee me, --no-assignee)");
|
|
7339
7360
|
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
7340
7361
|
emit(ctx, res, () => ctx.out.log(res.record ? `${res.record.id} [${statusCol(entity, res.record)}] ${res.record.appId} ${res.record.title ?? ""}` : `updated ${entity} ${id}`));
|
|
7341
7362
|
}
|
|
7342
|
-
async function pmDone(ctx, entity, id) {
|
|
7343
|
-
const
|
|
7363
|
+
async function pmDone(ctx, entity, id, parsed) {
|
|
7364
|
+
const decisionId = stringOpt(parsed.options.decision);
|
|
7365
|
+
if (decisionId && entity !== "bug") throw new Error("--decision is only valid when completing a bug");
|
|
7366
|
+
const patch2 = { ...DONE[entity], ...decisionId ? { decisionId } : {} };
|
|
7367
|
+
const res = await pmRequest(ctx, "PATCH", `/${entity}/${encodeURIComponent(id)}`, { patch: patch2 });
|
|
7344
7368
|
emit(ctx, res, () => ctx.out.log(`${entity} ${id} \u2192 done`));
|
|
7345
7369
|
}
|
|
7370
|
+
async function allRecords(ctx, entity, appId) {
|
|
7371
|
+
const records = [];
|
|
7372
|
+
for (; ; ) {
|
|
7373
|
+
const q = new URLSearchParams({
|
|
7374
|
+
app: appId,
|
|
7375
|
+
limit: "100",
|
|
7376
|
+
offset: String(records.length)
|
|
7377
|
+
});
|
|
7378
|
+
const page = await pmRequest(ctx, "GET", `/${entity}?${q}`);
|
|
7379
|
+
records.push(...page.records);
|
|
7380
|
+
if (records.length >= page.total || page.records.length === 0) return records;
|
|
7381
|
+
}
|
|
7382
|
+
}
|
|
7383
|
+
async function pmHandoff(ctx, parsed) {
|
|
7384
|
+
const appId = stringOpt(parsed.options.app);
|
|
7385
|
+
if (!appId) throw new Error("pm handoff needs --app <appId>");
|
|
7386
|
+
const [goals, tasks, bugs] = await Promise.all([
|
|
7387
|
+
allRecords(ctx, "goal", appId),
|
|
7388
|
+
allRecords(ctx, "task", appId),
|
|
7389
|
+
allRecords(ctx, "bug", appId)
|
|
7390
|
+
]);
|
|
7391
|
+
const handoff = {
|
|
7392
|
+
appId,
|
|
7393
|
+
unmetGoals: goals.filter((record5) => record5.status !== "met"),
|
|
7394
|
+
activeTasks: tasks.filter((record5) => record5.column !== "done"),
|
|
7395
|
+
openBugs: bugs.filter((record5) => record5.status !== "fixed" && record5.status !== "wontfix")
|
|
7396
|
+
};
|
|
7397
|
+
const result = {
|
|
7398
|
+
...handoff,
|
|
7399
|
+
clean: !handoff.unmetGoals.length && !handoff.activeTasks.length && !handoff.openBugs.length
|
|
7400
|
+
};
|
|
7401
|
+
emit(ctx, result, () => {
|
|
7402
|
+
if (result.clean) {
|
|
7403
|
+
ctx.out.log(`${appId}: no unresolved PM work`);
|
|
7404
|
+
return;
|
|
7405
|
+
}
|
|
7406
|
+
ctx.out.log(`${appId}: authoritative PM handoff`);
|
|
7407
|
+
for (const [label, records] of [
|
|
7408
|
+
["unmet goals", result.unmetGoals],
|
|
7409
|
+
["active tasks", result.activeTasks],
|
|
7410
|
+
["open bugs", result.openBugs]
|
|
7411
|
+
]) {
|
|
7412
|
+
ctx.out.log(`${label}:`);
|
|
7413
|
+
if (!records.length) ctx.out.log("- (none)");
|
|
7414
|
+
else for (const record5 of records) printRecord(
|
|
7415
|
+
ctx,
|
|
7416
|
+
label === "unmet goals" ? "goal" : label === "active tasks" ? "task" : "bug",
|
|
7417
|
+
record5
|
|
7418
|
+
);
|
|
7419
|
+
}
|
|
7420
|
+
});
|
|
7421
|
+
}
|
|
7346
7422
|
async function pmComment(ctx, entity, id, parsed) {
|
|
7347
7423
|
const body = stringOpt(parsed.options.body);
|
|
7348
7424
|
if (!body) throw new Error('pm comment needs --body "..."');
|
|
@@ -7393,6 +7469,7 @@ var ALLOWED = [
|
|
|
7393
7469
|
"target",
|
|
7394
7470
|
"description",
|
|
7395
7471
|
"desc",
|
|
7472
|
+
"decision",
|
|
7396
7473
|
"limit",
|
|
7397
7474
|
"offset",
|
|
7398
7475
|
"q"
|
|
@@ -7415,6 +7492,10 @@ async function buildContext(parsed, deps) {
|
|
|
7415
7492
|
}
|
|
7416
7493
|
async function pmCommand(parsed, deps = {}) {
|
|
7417
7494
|
const word = parsed.positionals[1] ?? "";
|
|
7495
|
+
if (word === "handoff") {
|
|
7496
|
+
assertArgs(parsed, ALLOWED, 2);
|
|
7497
|
+
return pmHandoff(await buildContext(parsed, deps), parsed);
|
|
7498
|
+
}
|
|
7418
7499
|
const entity = ALIASES[word];
|
|
7419
7500
|
if (!entity) throw new Error(`unknown pm entity "${word}". Try "odla-ai pm bug list" (goal|task|decision|bug).`);
|
|
7420
7501
|
const action = parsed.positionals[2] ?? "list";
|
|
@@ -7435,7 +7516,7 @@ async function pmCommand(parsed, deps = {}) {
|
|
|
7435
7516
|
case "move":
|
|
7436
7517
|
return pmSet(ctx, entity, requireId(id, action), parsed);
|
|
7437
7518
|
case "done":
|
|
7438
|
-
return pmDone(ctx, entity, requireId(id, action));
|
|
7519
|
+
return pmDone(ctx, entity, requireId(id, action), parsed);
|
|
7439
7520
|
case "comment":
|
|
7440
7521
|
return pmComment(ctx, entity, requireId(id, action), parsed);
|
|
7441
7522
|
case "comments":
|
|
@@ -7911,7 +7992,10 @@ var COMMAND_SURFACE = {
|
|
|
7911
7992
|
doctor: {},
|
|
7912
7993
|
help: {},
|
|
7913
7994
|
init: {},
|
|
7914
|
-
pm:
|
|
7995
|
+
pm: {
|
|
7996
|
+
...PM_ENTITIES,
|
|
7997
|
+
handoff: {}
|
|
7998
|
+
},
|
|
7915
7999
|
provision: {},
|
|
7916
8000
|
runbook: {
|
|
7917
8001
|
ask: {},
|