yaml-flow 4.0.0 → 5.1.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.
Files changed (96) hide show
  1. package/browser/board-livegraph-runtime.js +1453 -0
  2. package/browser/board-livegraph-runtime.js.map +1 -0
  3. package/browser/card-compute.js +36 -17
  4. package/browser/live-cards.js +848 -109
  5. package/browser/live-cards.schema.json +46 -21
  6. package/dist/board-livegraph-runtime/index.cjs +1448 -0
  7. package/dist/board-livegraph-runtime/index.cjs.map +1 -0
  8. package/dist/board-livegraph-runtime/index.d.cts +101 -0
  9. package/dist/board-livegraph-runtime/index.d.ts +101 -0
  10. package/dist/board-livegraph-runtime/index.js +1441 -0
  11. package/dist/board-livegraph-runtime/index.js.map +1 -0
  12. package/dist/card-compute/index.cjs +159 -44
  13. package/dist/card-compute/index.cjs.map +1 -1
  14. package/dist/card-compute/index.d.cts +36 -11
  15. package/dist/card-compute/index.d.ts +36 -11
  16. package/dist/card-compute/index.js +156 -44
  17. package/dist/card-compute/index.js.map +1 -1
  18. package/dist/cli/board-live-cards-cli.cjs +476 -105
  19. package/dist/cli/board-live-cards-cli.cjs.map +1 -1
  20. package/dist/cli/board-live-cards-cli.d.cts +8 -16
  21. package/dist/cli/board-live-cards-cli.d.ts +8 -16
  22. package/dist/cli/board-live-cards-cli.js +476 -106
  23. package/dist/cli/board-live-cards-cli.js.map +1 -1
  24. package/dist/continuous-event-graph/index.cjs +74 -33
  25. package/dist/continuous-event-graph/index.cjs.map +1 -1
  26. package/dist/continuous-event-graph/index.d.cts +7 -23
  27. package/dist/continuous-event-graph/index.d.ts +7 -23
  28. package/dist/continuous-event-graph/index.js +73 -32
  29. package/dist/continuous-event-graph/index.js.map +1 -1
  30. package/dist/index.cjs +1440 -56
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +21 -3
  33. package/dist/index.d.ts +21 -3
  34. package/dist/index.js +1434 -56
  35. package/dist/index.js.map +1 -1
  36. package/dist/journal-DRfJiheM.d.cts +28 -0
  37. package/dist/journal-NLYuqege.d.ts +28 -0
  38. package/dist/{journal-B_2JnBMF.d.ts → live-cards-bridge-Or7fdEJV.d.ts} +5 -32
  39. package/dist/{journal-BJDjWb5Q.d.cts → live-cards-bridge-vGJ6tMzN.d.cts} +5 -32
  40. package/dist/schedule-CMcZe5Ny.d.ts +21 -0
  41. package/dist/schedule-CiucyCan.d.cts +21 -0
  42. package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +1 -1
  43. package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +3 -3
  44. package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +1 -1
  45. package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +3 -3
  46. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
  47. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +33 -5
  48. package/examples/browser/livecards-browser/index.html +37 -684
  49. package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
  50. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +3 -3
  51. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
  52. package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +3 -3
  53. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +2 -2
  54. package/examples/example-board/board.yaml +23 -0
  55. package/examples/example-board/bootstrap_payload.json +1 -0
  56. package/examples/example-board/cards/card-chain-region-alert.json +39 -0
  57. package/examples/example-board/cards/card-chain-region-totals.json +26 -0
  58. package/examples/example-board/cards/card-chain-top-region.json +24 -0
  59. package/examples/example-board/cards/card-ex-actions.json +32 -0
  60. package/examples/example-board/cards/card-ex-chart.json +30 -0
  61. package/examples/example-board/cards/card-ex-filter.json +36 -0
  62. package/examples/example-board/cards/card-ex-filtered-by-preference.json +59 -0
  63. package/examples/example-board/cards/card-ex-form.json +91 -0
  64. package/examples/example-board/cards/card-ex-list.json +22 -0
  65. package/examples/example-board/cards/card-ex-markdown.json +17 -0
  66. package/examples/example-board/cards/card-ex-metric.json +19 -0
  67. package/examples/example-board/cards/card-ex-narrative.json +36 -0
  68. package/examples/example-board/cards/card-ex-source-http.json +28 -0
  69. package/examples/example-board/cards/card-ex-source.json +21 -0
  70. package/examples/example-board/cards/card-ex-status.json +35 -0
  71. package/examples/example-board/cards/card-ex-table.json +30 -0
  72. package/examples/example-board/cards/card-ex-todo.json +29 -0
  73. package/examples/example-board/demo-chat-handler.js +69 -0
  74. package/examples/example-board/demo-server-config.json +7 -0
  75. package/examples/example-board/demo-server.js +124 -0
  76. package/examples/example-board/demo-shell-browser.html +806 -0
  77. package/examples/example-board/demo-shell-with-server.html +280 -0
  78. package/examples/example-board/demo-shell.html +62 -0
  79. package/examples/example-board/demo-task-executor.js +255 -0
  80. package/examples/example-board/mock.db +15 -0
  81. package/examples/example-board/reusable-board-runtime-client.js +265 -0
  82. package/examples/example-board/reusable-runtime-artifacts-adapter.js +233 -0
  83. package/examples/example-board/reusable-server-runtime.js +1341 -0
  84. package/examples/index.html +16 -9
  85. package/examples/npm-libs/continuous-event-graph/live-cards-board.ts +17 -17
  86. package/examples/npm-libs/continuous-event-graph/live-portfolio-dashboard.ts +23 -23
  87. package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
  88. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +3 -3
  89. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
  90. package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +1 -1
  91. package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
  92. package/package.json +16 -2
  93. package/schema/card-runtime.schema.json +25 -0
  94. package/schema/live-cards.schema.json +46 -21
  95. package/browser/ingest-board.js +0 -296
  96. package/examples/ingest.js +0 -733
@@ -1,12 +1,12 @@
1
1
  import * as fs from 'fs';
2
2
  import * as os from 'os';
3
3
  import * as path from 'path';
4
- import { randomUUID, createHash } from 'crypto';
4
+ import { randomUUID } from 'crypto';
5
5
  import { execFileSync, spawn, execFile } from 'child_process';
6
6
  import { fileURLToPath } from 'url';
7
7
  import fg from 'fast-glob';
8
8
  import { lockSync } from 'proper-lockfile';
9
- import jsonata from 'jsonata';
9
+ import jsonata2 from 'jsonata';
10
10
  import 'ajv-formats';
11
11
 
12
12
  // src/cli/board-live-cards-cli.ts
@@ -638,9 +638,11 @@ var MemoryJournal = class {
638
638
  return this.buffer.length;
639
639
  }
640
640
  };
641
+
642
+ // src/continuous-event-graph/reactive.ts
641
643
  function computeDataHash(data) {
642
644
  const json = stableStringify(data);
643
- return createHash("sha256").update(json).digest("hex").slice(0, 16);
645
+ return fnv1a64Hex(json);
644
646
  }
645
647
  function stableStringify(value) {
646
648
  if (value === null || value === void 0 || typeof value !== "object") {
@@ -653,13 +655,49 @@ function stableStringify(value) {
653
655
  const keys = Object.keys(obj).sort();
654
656
  return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
655
657
  }
658
+ function fnv1a64Hex(input) {
659
+ let hash = 0xcbf29ce484222325n;
660
+ const prime = 0x100000001b3n;
661
+ const mod = 0xffffffffffffffffn;
662
+ for (let i = 0; i < input.length; i++) {
663
+ hash ^= BigInt(input.charCodeAt(i));
664
+ hash = hash * prime & mod;
665
+ }
666
+ return hash.toString(16).padStart(16, "0");
667
+ }
668
+ function base64UrlEncode(input) {
669
+ if (typeof Buffer !== "undefined") {
670
+ return Buffer.from(input, "utf8").toString("base64url");
671
+ }
672
+ if (typeof btoa === "function") {
673
+ const bytes = new TextEncoder().encode(input);
674
+ let binary = "";
675
+ for (const b of bytes) binary += String.fromCharCode(b);
676
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
677
+ }
678
+ throw new Error("No base64 encoder available in this runtime");
679
+ }
680
+ function base64UrlDecode(input) {
681
+ if (typeof Buffer !== "undefined") {
682
+ return Buffer.from(input, "base64url").toString("utf8");
683
+ }
684
+ if (typeof atob === "function") {
685
+ const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
686
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
687
+ const binary = atob(padded);
688
+ const bytes = new Uint8Array(binary.length);
689
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
690
+ return new TextDecoder().decode(bytes);
691
+ }
692
+ throw new Error("No base64 decoder available in this runtime");
693
+ }
656
694
  function encodeCallbackToken(taskName) {
657
695
  const payload = JSON.stringify({ t: taskName, n: Date.now().toString(36) + Math.random().toString(36).slice(2, 6) });
658
- return Buffer.from(payload).toString("base64url");
696
+ return base64UrlEncode(payload);
659
697
  }
660
698
  function decodeCallbackToken(token) {
661
699
  try {
662
- const payload = JSON.parse(Buffer.from(token, "base64url").toString());
700
+ const payload = JSON.parse(base64UrlDecode(token));
663
701
  if (typeof payload?.t === "string") return { taskName: payload.t };
664
702
  return null;
665
703
  } catch {
@@ -936,18 +974,18 @@ function deepSet(obj, path2, value) {
936
974
  }
937
975
  async function run(node, options) {
938
976
  if (!node?.compute?.length) return node;
939
- if (!node.state) node.state = {};
977
+ if (!node.card_data) node.card_data = {};
940
978
  node.computed_values = {};
941
979
  node._sourcesData = options?.sourcesData ?? {};
942
980
  const ctx = {
943
- state: node.state,
981
+ card_data: node.card_data,
944
982
  requires: node.requires ?? {},
945
- sources: node._sourcesData,
983
+ fetched_sources: node._sourcesData,
946
984
  computed_values: node.computed_values
947
985
  };
948
986
  for (const step of node.compute) {
949
987
  try {
950
- const val = await jsonata(step.expr).evaluate(ctx);
988
+ const val = await jsonata2(step.expr).evaluate(ctx);
951
989
  deepSet(node.computed_values, step.bindTo, val);
952
990
  ctx.computed_values = node.computed_values;
953
991
  } catch (err) {
@@ -958,16 +996,16 @@ async function run(node, options) {
958
996
  }
959
997
  async function evalExpr(expr, node) {
960
998
  const ctx = {
961
- state: node.state ?? {},
999
+ card_data: node.card_data ?? {},
962
1000
  requires: node.requires ?? {},
963
- sources: node._sourcesData ?? {},
1001
+ fetched_sources: node._sourcesData ?? {},
964
1002
  computed_values: node.computed_values ?? {}
965
1003
  };
966
- return jsonata(expr).evaluate(ctx);
1004
+ return jsonata2(expr).evaluate(ctx);
967
1005
  }
968
1006
  function resolve(node, path2) {
969
- if (path2.startsWith("sources.")) {
970
- return deepGet(node._sourcesData ?? {}, path2.slice("sources.".length));
1007
+ if (path2.startsWith("fetched_sources.")) {
1008
+ return deepGet(node._sourcesData ?? {}, path2.slice("fetched_sources.".length));
971
1009
  }
972
1010
  return deepGet(node, path2);
973
1011
  }
@@ -987,8 +1025,7 @@ var VALID_ELEMENT_KINDS = /* @__PURE__ */ new Set([
987
1025
  "markdown",
988
1026
  "custom"
989
1027
  ]);
990
- var VALID_STATUSES = /* @__PURE__ */ new Set(["fresh", "stale", "loading", "error"]);
991
- var ALLOWED_KEYS = /* @__PURE__ */ new Set(["id", "meta", "requires", "provides", "view", "state", "compute", "sources"]);
1028
+ var ALLOWED_KEYS = /* @__PURE__ */ new Set(["id", "meta", "requires", "provides", "view", "card_data", "compute", "sources"]);
992
1029
  function validateNode(node) {
993
1030
  const errors = [];
994
1031
  if (!node || typeof node !== "object" || Array.isArray(node)) {
@@ -999,13 +1036,8 @@ function validateNode(node) {
999
1036
  for (const key of Object.keys(n)) {
1000
1037
  if (!ALLOWED_KEYS.has(key)) errors.push(`Unknown top-level key: "${key}"`);
1001
1038
  }
1002
- if (n.state == null || typeof n.state !== "object" || Array.isArray(n.state)) {
1003
- errors.push("state: required, must be an object");
1004
- } else {
1005
- const state = n.state;
1006
- if (state.status != null && !VALID_STATUSES.has(state.status)) {
1007
- errors.push(`state.status: must be one of: ${[...VALID_STATUSES].join(", ")}`);
1008
- }
1039
+ if (n.card_data == null || typeof n.card_data !== "object" || Array.isArray(n.card_data)) {
1040
+ errors.push("card_data: required, must be an object");
1009
1041
  }
1010
1042
  if (n.meta != null) {
1011
1043
  if (typeof n.meta !== "object" || Array.isArray(n.meta)) {
@@ -1094,17 +1126,33 @@ function validateNode(node) {
1094
1126
  }
1095
1127
  return { ok: errors.length === 0, errors };
1096
1128
  }
1129
+ function enrichSources(sources, context) {
1130
+ if (!sources || sources.length === 0) return [];
1131
+ return sources.map((src) => ({
1132
+ ...src,
1133
+ _requires: context.requires ?? {},
1134
+ _sourcesData: context.sourcesData ?? {},
1135
+ _computed_values: context.computed_values ?? {}
1136
+ }));
1137
+ }
1097
1138
  var CardCompute = {
1098
1139
  run,
1099
1140
  eval: evalExpr,
1100
1141
  resolve,
1101
- validate: validateNode
1142
+ validate: validateNode,
1143
+ enrichSources
1102
1144
  };
1103
1145
 
1104
1146
  // src/cli/board-live-cards-cli.ts
1105
1147
  var BOARD_FILE = "board-graph.json";
1106
1148
  var JOURNAL_FILE = "board-journal.jsonl";
1149
+ var TASK_EXECUTOR_LOG_FILE = "task-executor.jsonl";
1107
1150
  var INVENTORY_FILE = "cards-inventory.jsonl";
1151
+ var RUNTIME_OUT_FILE = ".runtime-out";
1152
+ var DEFAULT_RUNTIME_OUT_DIR = "runtime-out";
1153
+ var RUNTIME_STATUS_FILE = "board-livegraph-status.json";
1154
+ var RUNTIME_CARDS_DIR = "cards";
1155
+ var RUNTIME_DATA_OBJECTS_DIR = "data-objects";
1108
1156
  var EMPTY_CONFIG = { settings: { completion: "manual", refreshStrategy: "data-changed" }, tasks: {} };
1109
1157
  var BoardJournal = class {
1110
1158
  journalPath;
@@ -1159,7 +1207,34 @@ function lookupCardPath(boardDir, cardId) {
1159
1207
  }
1160
1208
  function appendCardInventory(boardDir, entry) {
1161
1209
  const inventoryPath = path.join(boardDir, INVENTORY_FILE);
1162
- fs.appendFileSync(inventoryPath, JSON.stringify(entry) + "\n");
1210
+ const normalized = { ...entry, cardFilePath: path.resolve(entry.cardFilePath) };
1211
+ fs.appendFileSync(inventoryPath, JSON.stringify(normalized) + "\n");
1212
+ }
1213
+ function buildCardInventoryIndex(boardDir) {
1214
+ const byCardId = /* @__PURE__ */ new Map();
1215
+ const byCardPath = /* @__PURE__ */ new Map();
1216
+ for (const entry of readCardInventory(boardDir)) {
1217
+ const normalizedPath = path.resolve(entry.cardFilePath);
1218
+ const normalizedEntry = {
1219
+ ...entry,
1220
+ cardFilePath: normalizedPath
1221
+ };
1222
+ const existingById = byCardId.get(entry.cardId);
1223
+ if (existingById && existingById.cardFilePath !== normalizedPath) {
1224
+ throw new Error(
1225
+ `Inventory invariant violation: card id "${entry.cardId}" maps to multiple files: "${existingById.cardFilePath}" and "${normalizedPath}"`
1226
+ );
1227
+ }
1228
+ const existingByPath = byCardPath.get(normalizedPath);
1229
+ if (existingByPath && existingByPath.cardId !== entry.cardId) {
1230
+ throw new Error(
1231
+ `Inventory invariant violation: file "${normalizedPath}" maps to multiple ids: "${existingByPath.cardId}" and "${entry.cardId}"`
1232
+ );
1233
+ }
1234
+ byCardId.set(entry.cardId, normalizedEntry);
1235
+ byCardPath.set(normalizedPath, normalizedEntry);
1236
+ }
1237
+ return { byCardId, byCardPath };
1163
1238
  }
1164
1239
  function initBoard(dir) {
1165
1240
  const boardPath = path.join(dir, BOARD_FILE);
@@ -1195,7 +1270,63 @@ function saveBoard(dir, rg, journal) {
1195
1270
  lastDrainedJournalId: journal.lastDrainedJournalId,
1196
1271
  graph: snap
1197
1272
  };
1198
- fs.writeFileSync(path.join(dir, BOARD_FILE), JSON.stringify(envelope, null, 2));
1273
+ writeJsonAtomic(path.join(dir, BOARD_FILE), envelope);
1274
+ const live = restore(snap);
1275
+ const statusObject = buildBoardStatusObject(dir, live);
1276
+ writeJsonAtomic(resolveStatusSnapshotPath(dir), statusObject);
1277
+ }
1278
+ function runtimeOutConfigPath(boardDir) {
1279
+ return path.join(boardDir, RUNTIME_OUT_FILE);
1280
+ }
1281
+ function resolveConfiguredRuntimeOutDir(boardDir) {
1282
+ const cfgPath = runtimeOutConfigPath(boardDir);
1283
+ if (fs.existsSync(cfgPath)) {
1284
+ const configured = fs.readFileSync(cfgPath, "utf-8").trim();
1285
+ if (configured) {
1286
+ return path.isAbsolute(configured) ? configured : path.resolve(boardDir, configured);
1287
+ }
1288
+ }
1289
+ const defaultDir = path.join(boardDir, DEFAULT_RUNTIME_OUT_DIR);
1290
+ fs.writeFileSync(cfgPath, defaultDir, "utf-8");
1291
+ return defaultDir;
1292
+ }
1293
+ function configureRuntimeOutDir(boardDir, runtimeOut) {
1294
+ let resolved;
1295
+ if (runtimeOut) {
1296
+ resolved = path.isAbsolute(runtimeOut) ? runtimeOut : path.resolve(boardDir, runtimeOut);
1297
+ } else {
1298
+ resolved = path.join(boardDir, DEFAULT_RUNTIME_OUT_DIR);
1299
+ }
1300
+ fs.mkdirSync(resolved, { recursive: true });
1301
+ fs.writeFileSync(runtimeOutConfigPath(boardDir), resolved, "utf-8");
1302
+ return resolved;
1303
+ }
1304
+ function resolveStatusSnapshotPath(boardDir) {
1305
+ return path.join(resolveConfiguredRuntimeOutDir(boardDir), RUNTIME_STATUS_FILE);
1306
+ }
1307
+ function resolveComputedValuesPath(boardDir, cardId) {
1308
+ return path.join(resolveConfiguredRuntimeOutDir(boardDir), RUNTIME_CARDS_DIR, `${cardId}.computed.json`);
1309
+ }
1310
+ function resolveDataObjectsDirPath(boardDir) {
1311
+ return path.join(resolveConfiguredRuntimeOutDir(boardDir), RUNTIME_DATA_OBJECTS_DIR);
1312
+ }
1313
+ function toDataObjectFileName(token) {
1314
+ return token.replace(/[\\/]/g, "__");
1315
+ }
1316
+ function writeRuntimeDataObjects(boardDir, data) {
1317
+ for (const [token, payload] of Object.entries(data)) {
1318
+ if (!token) continue;
1319
+ const fileName = toDataObjectFileName(token);
1320
+ if (!fileName) continue;
1321
+ const filePath = path.join(resolveDataObjectsDirPath(boardDir), fileName);
1322
+ writeJsonAtomic(filePath, payload);
1323
+ }
1324
+ }
1325
+ function writeJsonAtomic(filePath, payload) {
1326
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
1327
+ const tmpPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
1328
+ fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), "utf-8");
1329
+ fs.renameSync(tmpPath, filePath);
1199
1330
  }
1200
1331
  function withBoardLock(boardDir, fn) {
1201
1332
  const boardPath = path.join(boardDir, BOARD_FILE);
@@ -1273,15 +1404,63 @@ function shouldUseShellForCommand(cmd, forceShell) {
1273
1404
  if (typeof forceShell === "boolean") return forceShell;
1274
1405
  return process.platform === "win32" && /\.(cmd|bat)$/i.test(cmd);
1275
1406
  }
1407
+ var _gitBashPath;
1408
+ var GIT_BASH_CACHE_FILE = path.join(os.tmpdir(), ".board-live-cards-git-bash-cache.json");
1409
+ function findGitBash() {
1410
+ if (_gitBashPath !== void 0) return _gitBashPath;
1411
+ if (process.platform !== "win32") return _gitBashPath = false;
1412
+ try {
1413
+ const cached = JSON.parse(fs.readFileSync(GIT_BASH_CACHE_FILE, "utf8"));
1414
+ if (cached.path === false || typeof cached.path === "string" && fs.existsSync(cached.path)) {
1415
+ return _gitBashPath = cached.path;
1416
+ }
1417
+ } catch {
1418
+ }
1419
+ const candidates = [
1420
+ process.env.SHELL,
1421
+ process.env.PROGRAMFILES && path.join(process.env.PROGRAMFILES, "Git", "usr", "bin", "bash.exe"),
1422
+ process.env.PROGRAMFILES && path.join(process.env.PROGRAMFILES, "Git", "bin", "bash.exe"),
1423
+ process.env["PROGRAMFILES(X86)"] && path.join(process.env["PROGRAMFILES(X86)"], "Git", "bin", "bash.exe"),
1424
+ process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, "Programs", "Git", "bin", "bash.exe")
1425
+ ];
1426
+ for (const c of candidates) {
1427
+ if (c && /bash(\.exe)?$/i.test(c) && fs.existsSync(c)) {
1428
+ _gitBashPath = c;
1429
+ try {
1430
+ fs.writeFileSync(GIT_BASH_CACHE_FILE, JSON.stringify({ path: c }));
1431
+ } catch {
1432
+ }
1433
+ return _gitBashPath;
1434
+ }
1435
+ }
1436
+ _gitBashPath = false;
1437
+ try {
1438
+ fs.writeFileSync(GIT_BASH_CACHE_FILE, JSON.stringify({ path: false }));
1439
+ } catch {
1440
+ }
1441
+ return _gitBashPath;
1442
+ }
1443
+ function shellQuote(s) {
1444
+ return "'" + s.replace(/'/g, "'\\''") + "'";
1445
+ }
1276
1446
  function spawnDetachedCommand(cmd, args) {
1277
- const child = process.platform === "win32" ? spawn("cmd", ["/c", "start", "/b", "", cmd, ...args], {
1278
- stdio: "ignore",
1279
- windowsHide: true
1280
- }) : spawn(cmd, args, {
1281
- shell: false,
1282
- detached: true,
1283
- stdio: "ignore"
1284
- });
1447
+ if (process.platform === "win32") {
1448
+ const bash = findGitBash();
1449
+ if (bash) {
1450
+ const shellCmd = [cmd, ...args].map((a) => shellQuote(a.replace(/\\/g, "/"))).join(" ");
1451
+ const child3 = spawn(bash, ["-c", shellCmd], { detached: true, stdio: "ignore", windowsHide: true });
1452
+ child3.unref();
1453
+ return;
1454
+ }
1455
+ const child2 = spawn("cmd", ["/c", "start", "/b", "", cmd, ...args], {
1456
+ detached: true,
1457
+ stdio: "ignore",
1458
+ windowsHide: true
1459
+ });
1460
+ child2.unref();
1461
+ return;
1462
+ }
1463
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
1285
1464
  child.unref();
1286
1465
  }
1287
1466
  function execCommandSync(cmd, args, options) {
@@ -1386,7 +1565,7 @@ async function processAccumulatedEventsForced(boardDir, options) {
1386
1565
  }
1387
1566
  function liveCardToTaskConfig(card) {
1388
1567
  const requires = card.requires;
1389
- const provides = card.provides ? card.provides.map((p) => p.bindTo) : [card.id];
1568
+ const provides = card.provides?.map((p) => p.bindTo) ?? [];
1390
1569
  return {
1391
1570
  requires: requires && requires.length > 0 ? requires : void 0,
1392
1571
  provides,
@@ -1411,23 +1590,23 @@ function getCliInvocation(command, args) {
1411
1590
  }
1412
1591
  function invokeRunSources(boardDir, cardPath, callbackToken, callback) {
1413
1592
  const { cmd, args } = getCliInvocation("run-sources-internal", ["--card", cardPath, "--token", callbackToken, "--rg", boardDir]);
1414
- const child = process.platform === "win32" ? spawn("cmd", ["/c", "start", "/b", "", cmd, ...args], {
1415
- stdio: "ignore",
1416
- windowsHide: true
1417
- }) : spawn(cmd, args, {
1418
- shell: false,
1419
- detached: true,
1420
- stdio: "ignore"
1421
- });
1422
- let finished = false;
1423
- const done = (err) => {
1424
- if (finished) return;
1425
- finished = true;
1426
- callback(err);
1427
- };
1428
- child.on("error", (err) => done(err));
1429
- child.unref();
1430
- done(null);
1593
+ try {
1594
+ spawnDetachedCommand(cmd, args);
1595
+ callback(null);
1596
+ } catch (err) {
1597
+ callback(err instanceof Error ? err : new Error(String(err)));
1598
+ }
1599
+ }
1600
+ function appendTaskExecutorLog(boardDir, hydratedSource) {
1601
+ try {
1602
+ const entry = {
1603
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1604
+ hydratedSource
1605
+ };
1606
+ fs.appendFileSync(path.join(boardDir, TASK_EXECUTOR_LOG_FILE), JSON.stringify(entry) + "\n", "utf-8");
1607
+ } catch (logErr) {
1608
+ console.error(`[task-executor-log] append failed: ${logErr instanceof Error ? logErr.message : String(logErr)}`);
1609
+ }
1431
1610
  }
1432
1611
  function invokeSourceDataFetched(sourceToken, tmpFile, callback) {
1433
1612
  const { cmd, args } = getCliInvocation("source-data-fetched", ["--tmp", tmpFile, "--token", sourceToken]);
@@ -1452,7 +1631,7 @@ function createBoardReactiveGraph(boardDir) {
1452
1631
  if (!cardPath) return "task-initiate-failure";
1453
1632
  const card = JSON.parse(fs.readFileSync(cardPath, "utf-8"));
1454
1633
  const cardId = card.id;
1455
- const cardState = card.state ?? {};
1634
+ const cardState = card.card_data ?? {};
1456
1635
  const allSources = card.sources ?? [];
1457
1636
  const requiredSources = allSources.filter((s) => s.optionalForCompletionGating !== true);
1458
1637
  const runtime = readRuntimeState(boardDir, cardId);
@@ -1488,18 +1667,32 @@ function createBoardReactiveGraph(boardDir) {
1488
1667
  }
1489
1668
  }
1490
1669
  }
1670
+ const requires = {};
1671
+ for (const [token, taskData] of Object.entries(input.state ?? {})) {
1672
+ if (taskData !== null && typeof taskData === "object" && !Array.isArray(taskData)) {
1673
+ const unwrapped = taskData[token];
1674
+ requires[token] = unwrapped !== void 0 ? unwrapped : taskData;
1675
+ } else {
1676
+ requires[token] = taskData;
1677
+ }
1678
+ }
1491
1679
  const computeNode = {
1492
1680
  id: cardId,
1493
- state: { ...cardState },
1494
- requires: input.state ?? {},
1681
+ card_data: { ...cardState },
1682
+ requires,
1495
1683
  sources: allSources,
1496
1684
  compute: card.compute
1497
1685
  };
1686
+ computeNode._sourcesData = sourcesData;
1498
1687
  if (card.compute) {
1499
1688
  await CardCompute.run(computeNode, { sourcesData });
1500
- const cvPath = path.join(boardDir, `${cardId}.computed_values.json`);
1501
- fs.writeFileSync(cvPath, JSON.stringify(computeNode.computed_values ?? {}, null, 2));
1502
1689
  }
1690
+ const cvPath = resolveComputedValuesPath(boardDir, cardId);
1691
+ writeJsonAtomic(cvPath, {
1692
+ schema_version: "v1",
1693
+ card_id: cardId,
1694
+ computed_values: computeNode.computed_values ?? {}
1695
+ });
1503
1696
  const now = (/* @__PURE__ */ new Date()).toISOString();
1504
1697
  const undeliveredRequired = requiredSources.filter((s) => {
1505
1698
  if (!s.outputFile) return false;
@@ -1519,16 +1712,40 @@ function createBoardReactiveGraph(boardDir) {
1519
1712
  }
1520
1713
  }
1521
1714
  if (stampedAny) writeRuntimeState(boardDir, cardId, runtime);
1522
- invokeRunSources(boardDir, cardPath, input.callbackToken, (err) => {
1523
- if (err) console.error(`[card-handler] ${input.nodeId}:`, err.message);
1715
+ const enrichedCard = { ...card };
1716
+ const enrichedSources = CardCompute.enrichSources(
1717
+ Array.isArray(card.sources) ? card.sources : void 0,
1718
+ {
1719
+ requires,
1720
+ sourcesData,
1721
+ computed_values: computeNode.computed_values
1722
+ }
1723
+ );
1724
+ const sourceCwd = path.dirname(cardPath);
1725
+ enrichedCard.sources = Array.isArray(enrichedSources) ? enrichedSources.map((src) => ({
1726
+ ...src,
1727
+ cwd: typeof src.cwd === "string" && src.cwd ? src.cwd : sourceCwd,
1728
+ boardDir: typeof src.boardDir === "string" && src.boardDir ? src.boardDir : boardDir
1729
+ })) : enrichedSources;
1730
+ const enrichedCardPath = path.join(os.tmpdir(), `card-enriched-${cardId}-${Date.now()}.json`);
1731
+ fs.writeFileSync(enrichedCardPath, JSON.stringify(enrichedCard, null, 2), "utf-8");
1732
+ invokeRunSources(boardDir, enrichedCardPath, input.callbackToken, (err) => {
1733
+ if (err) {
1734
+ console.error(`[card-handler] ${input.nodeId}:`, err.message);
1735
+ try {
1736
+ fs.unlinkSync(enrichedCardPath);
1737
+ } catch {
1738
+ }
1739
+ }
1524
1740
  });
1525
1741
  return "task-initiated";
1526
1742
  }
1527
- const providesBindings = card.provides ?? [{ bindTo: cardId, src: `state.${cardId}` }];
1743
+ const providesBindings = card.provides ?? [];
1528
1744
  const data = {};
1529
1745
  for (const { bindTo, src } of providesBindings) {
1530
1746
  data[bindTo] = CardCompute.resolve(computeNode, src);
1531
1747
  }
1748
+ writeRuntimeDataObjects(boardDir, data);
1532
1749
  const undeliveredOptional = allSources.filter((s) => {
1533
1750
  if (s.optionalForCompletionGating !== true || !s.outputFile) return false;
1534
1751
  const entry = runtime._sources[s.bindTo];
@@ -1556,18 +1773,20 @@ function createBoardReactiveGraph(boardDir) {
1556
1773
  function addSingleCardFromFile(dir, cardFile) {
1557
1774
  const absCardPath = path.resolve(cardFile);
1558
1775
  if (!fs.existsSync(absCardPath)) {
1559
- console.error(`Card file not found: ${absCardPath}`);
1560
- process.exit(1);
1776
+ throw new Error(`Card file not found: ${absCardPath}`);
1777
+ }
1778
+ let card;
1779
+ try {
1780
+ card = JSON.parse(fs.readFileSync(absCardPath, "utf-8"));
1781
+ } catch (err) {
1782
+ throw new Error(`Failed to parse card file: ${absCardPath} - ${err instanceof Error ? err.message : String(err)}`);
1561
1783
  }
1562
- const card = JSON.parse(fs.readFileSync(absCardPath, "utf-8"));
1563
1784
  if (!card.id) {
1564
- console.error('Card JSON must have an "id" field');
1565
- process.exit(1);
1785
+ throw new Error('Card JSON must have an "id" field');
1566
1786
  }
1567
1787
  const existing = readCardInventory(dir);
1568
1788
  if (existing.some((e) => e.cardId === card.id)) {
1569
- console.error(`Card "${card.id}" already exists in inventory`);
1570
- process.exit(1);
1789
+ throw new Error(`Card "${card.id}" already exists in inventory`);
1571
1790
  }
1572
1791
  appendCardInventory(dir, {
1573
1792
  cardId: card.id,
@@ -1594,7 +1813,7 @@ function resolveCardGlobMatches(cardGlob) {
1594
1813
  unique: true,
1595
1814
  dot: false
1596
1815
  });
1597
- return [...matches].sort((a, b) => a.localeCompare(b));
1816
+ return [...matches].map((m) => path.resolve(m)).sort((a, b) => a.localeCompare(b));
1598
1817
  }
1599
1818
  function cmdAddCards(args) {
1600
1819
  const rgIdx = args.indexOf("--rg");
@@ -1604,16 +1823,14 @@ function cmdAddCards(args) {
1604
1823
  const cardFile = cardIdx !== -1 ? args[cardIdx + 1] : void 0;
1605
1824
  const cardGlob = globIdx !== -1 ? args[globIdx + 1] : void 0;
1606
1825
  if (!dir || !cardFile && !cardGlob || cardFile && cardGlob) {
1607
- console.error("Usage: board-live-cards add-cards --rg <dir> (--card <card.json> | --card-glob <glob>)");
1608
- process.exit(1);
1826
+ throw new Error("Usage: board-live-cards add-cards --rg <dir> (--card <card.json> | --card-glob <glob>)");
1609
1827
  }
1610
1828
  if (cardFile) {
1611
1829
  addSingleCardFromFile(dir, cardFile);
1612
1830
  } else {
1613
1831
  const matches = resolveCardGlobMatches(cardGlob);
1614
1832
  if (matches.length === 0) {
1615
- console.error(`No card files matched glob: ${cardGlob}`);
1616
- process.exit(1);
1833
+ throw new Error(`No card files matched glob: ${cardGlob}`);
1617
1834
  }
1618
1835
  for (const match of matches) {
1619
1836
  addSingleCardFromFile(dir, match);
@@ -1625,30 +1842,34 @@ function cmdAddCards(args) {
1625
1842
  function cmdInit(args) {
1626
1843
  const dir = args[0];
1627
1844
  if (!dir) {
1628
- console.error("Usage: board-live-cards init <dir> [--task-executor <script>]");
1629
- process.exit(1);
1845
+ throw new Error("Usage: board-live-cards init <dir> [--task-executor <script>] [--chat-handler <script>] [--runtime-out <dir>]");
1630
1846
  }
1631
1847
  const teIdx = args.indexOf("--task-executor");
1632
1848
  const taskExecutor = teIdx !== -1 ? args[teIdx + 1] : void 0;
1849
+ const chIdx = args.indexOf("--chat-handler");
1850
+ const chatHandler = chIdx !== -1 ? args[chIdx + 1] : void 0;
1851
+ const roIdx = args.indexOf("--runtime-out");
1852
+ const runtimeOut = roIdx !== -1 ? args[roIdx + 1] : void 0;
1853
+ if (roIdx !== -1 && !runtimeOut) {
1854
+ throw new Error("Usage: board-live-cards init <dir> [--task-executor <script>] [--chat-handler <script>] [--runtime-out <dir>]");
1855
+ }
1633
1856
  const result = initBoard(dir);
1634
1857
  if (taskExecutor) {
1635
1858
  fs.writeFileSync(path.join(dir, ".task-executor"), taskExecutor, "utf-8");
1636
1859
  }
1860
+ if (chatHandler) {
1861
+ fs.writeFileSync(path.join(dir, ".chat-handler"), chatHandler, "utf-8");
1862
+ }
1863
+ const runtimeOutDir = configureRuntimeOutDir(dir, runtimeOut);
1864
+ const live = loadBoard(dir);
1865
+ writeJsonAtomic(resolveStatusSnapshotPath(dir), buildBoardStatusObject(dir, live));
1637
1866
  if (result === "exists") {
1638
- console.log(`Board already initialized at ${path.resolve(dir)}${taskExecutor ? ` (task-executor updated: ${taskExecutor})` : ""}`);
1867
+ console.log(`Board already initialized at ${path.resolve(dir)}${taskExecutor ? ` (task-executor updated: ${taskExecutor})` : ""} (runtime-out: ${runtimeOutDir})`);
1639
1868
  } else {
1640
- console.log(`Board initialized at ${path.resolve(dir)}${taskExecutor ? ` (task-executor: ${taskExecutor})` : ""}`);
1869
+ console.log(`Board initialized at ${path.resolve(dir)}${taskExecutor ? ` (task-executor: ${taskExecutor})` : ""} (runtime-out: ${runtimeOutDir})`);
1641
1870
  }
1642
1871
  }
1643
- function cmdStatus(args) {
1644
- const rgIdx = args.indexOf("--rg");
1645
- const asJson = args.includes("--json");
1646
- const dir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
1647
- if (!dir) {
1648
- console.error("Usage: board-live-cards status --rg <dir>");
1649
- process.exit(1);
1650
- }
1651
- const live = loadBoard(dir);
1872
+ function buildBoardStatusObject(dir, live) {
1652
1873
  const taskState = live.state.tasks;
1653
1874
  const taskConfig = live.config.tasks;
1654
1875
  const cardNames = Object.keys(taskState);
@@ -1665,14 +1886,8 @@ function cmdStatus(args) {
1665
1886
  for (const p of sched.pending) waitingByCard.set(p.taskName, p.waitingOn);
1666
1887
  for (const u of sched.unresolved) waitingByCard.set(u.taskName, u.missingTokens);
1667
1888
  for (const b of sched.blocked) waitingByCard.set(b.taskName, b.failedTokens);
1668
- const providersByToken = /* @__PURE__ */ new Map();
1669
1889
  const dependentsByToken = /* @__PURE__ */ new Map();
1670
1890
  for (const [name, cfg] of Object.entries(taskConfig)) {
1671
- for (const token of cfg.provides ?? []) {
1672
- const providers = providersByToken.get(token) ?? [];
1673
- providers.push(name);
1674
- providersByToken.set(token, providers);
1675
- }
1676
1891
  for (const token of cfg.requires ?? []) {
1677
1892
  const dependents = dependentsByToken.get(token) ?? [];
1678
1893
  dependents.push(name);
@@ -1742,7 +1957,7 @@ function cmdStatus(args) {
1742
1957
  const feedsAny = provides.some((p) => (dependentsByToken.get(p) ?? []).some((d) => d !== name));
1743
1958
  if (requiresNone && !feedsAny) orphanCards += 1;
1744
1959
  }
1745
- const statusObject = {
1960
+ return {
1746
1961
  schema_version: "v1",
1747
1962
  meta: {
1748
1963
  board: {
@@ -1767,6 +1982,23 @@ function cmdStatus(args) {
1767
1982
  },
1768
1983
  cards
1769
1984
  };
1985
+ }
1986
+ function cmdStatus(args) {
1987
+ const rgIdx = args.indexOf("--rg");
1988
+ const asJson = args.includes("--json");
1989
+ const dir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
1990
+ if (!dir) {
1991
+ console.error("Usage: board-live-cards status --rg <dir>");
1992
+ process.exit(1);
1993
+ }
1994
+ const statusOutPath = resolveStatusSnapshotPath(dir);
1995
+ let statusObject;
1996
+ if (fs.existsSync(statusOutPath)) {
1997
+ statusObject = JSON.parse(fs.readFileSync(statusOutPath, "utf-8"));
1998
+ } else {
1999
+ statusObject = buildBoardStatusObject(dir, loadBoard(dir));
2000
+ writeJsonAtomic(statusOutPath, statusObject);
2001
+ }
1770
2002
  if (asJson) {
1771
2003
  console.log(JSON.stringify(statusObject, null, 2));
1772
2004
  return;
@@ -1797,6 +2029,7 @@ function cmdTaskCompleted(args) {
1797
2029
  process.exit(1);
1798
2030
  }
1799
2031
  const data = dataIdx !== -1 ? JSON.parse(args[dataIdx + 1]) : {};
2032
+ writeRuntimeDataObjects(dir, data);
1800
2033
  appendEventToJournal(dir, {
1801
2034
  type: "task-completed",
1802
2035
  taskName: decoded.taskName,
@@ -1923,6 +2156,12 @@ function cmdRunSources(args) {
1923
2156
  process.exit(1);
1924
2157
  }
1925
2158
  const card = JSON.parse(fs.readFileSync(cardFilePath, "utf-8"));
2159
+ if (path.basename(cardFilePath).startsWith("card-enriched-")) {
2160
+ try {
2161
+ fs.unlinkSync(cardFilePath);
2162
+ } catch {
2163
+ }
2164
+ }
1926
2165
  console.log(`[run-sources-internal] Processing card "${card.id}"`);
1927
2166
  const executorFile = path.join(boardDir, ".task-executor");
1928
2167
  const taskExecutor = fs.existsSync(executorFile) ? fs.readFileSync(executorFile, "utf-8").trim() : void 0;
@@ -1951,7 +2190,12 @@ function cmdRunSources(args) {
1951
2190
  const inFile = path.join(os.tmpdir(), `card-source-in-${src.bindTo}-${Date.now()}.json`);
1952
2191
  const outFile2 = path.join(os.tmpdir(), `card-source-out-${src.bindTo}-${Date.now()}.json`);
1953
2192
  const errFile = path.join(os.tmpdir(), `card-source-err-${src.bindTo}-${Date.now()}.txt`);
1954
- const sourceForExecutor = { ...src, cwd: path.dirname(cardFilePath || ""), boardDir };
2193
+ const sourceForExecutor = {
2194
+ ...src,
2195
+ cwd: typeof src.cwd === "string" && src.cwd ? src.cwd : path.dirname(cardFilePath || ""),
2196
+ boardDir: typeof src.boardDir === "string" && src.boardDir ? src.boardDir : boardDir
2197
+ };
2198
+ appendTaskExecutorLog(boardDir, sourceForExecutor);
1955
2199
  fs.writeFileSync(inFile, JSON.stringify(sourceForExecutor, null, 2), "utf-8");
1956
2200
  console.log(`[run-sources-internal] task-executor: ${taskExecutor} run-source-fetch --in ${inFile} --out ${outFile2} --err ${errFile}`);
1957
2201
  try {
@@ -2108,17 +2352,14 @@ function cmdUpdateCard(args) {
2108
2352
  const dir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
2109
2353
  const cardId = idIdx !== -1 ? args[idIdx + 1] : void 0;
2110
2354
  if (!dir || !cardId) {
2111
- console.error("Usage: board-live-cards update-card --rg <dir> --card-id <card-id> [--restart]");
2112
- process.exit(1);
2355
+ throw new Error("Usage: board-live-cards update-card --rg <dir> --card-id <card-id> [--restart]");
2113
2356
  }
2114
2357
  const cardPath = lookupCardPath(dir, cardId);
2115
2358
  if (!cardPath) {
2116
- console.error(`Card "${cardId}" not found in inventory`);
2117
- process.exit(1);
2359
+ throw new Error(`Card "${cardId}" not found in inventory`);
2118
2360
  }
2119
2361
  if (!fs.existsSync(cardPath)) {
2120
- console.error(`Card file not found: ${cardPath}`);
2121
- process.exit(1);
2362
+ throw new Error(`Card file not found: ${cardPath}`);
2122
2363
  }
2123
2364
  const card = JSON.parse(fs.readFileSync(cardPath, "utf-8"));
2124
2365
  const taskConfig = liveCardToTaskConfig(card);
@@ -2138,6 +2379,121 @@ function cmdUpdateCard(args) {
2138
2379
  void processAccumulatedEventsInfinitePass(dir);
2139
2380
  console.log(`Card "${cardId}" updated${restart ? " (restarted)" : ""}.`);
2140
2381
  }
2382
+ function cmdUpsertCard(args) {
2383
+ const rgIdx = args.indexOf("--rg");
2384
+ const cardIdx = args.indexOf("--card");
2385
+ const globIdx = args.indexOf("--card-glob");
2386
+ const cardIdIdx = args.indexOf("--card-id");
2387
+ const restart = args.includes("--restart");
2388
+ const dir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
2389
+ const cardFile = cardIdx !== -1 ? args[cardIdx + 1] : void 0;
2390
+ const cardGlob = globIdx !== -1 ? args[globIdx + 1] : void 0;
2391
+ const requestedCardId = cardIdIdx !== -1 ? args[cardIdIdx + 1] : void 0;
2392
+ if (!dir || !cardFile && !cardGlob || cardFile && cardGlob) {
2393
+ console.error("Usage: board-live-cards upsert-card --rg <dir> (--card <card.json> | --card-glob <glob>) [--card-id <card-id>] [--restart]");
2394
+ process.exit(1);
2395
+ }
2396
+ if (cardGlob && requestedCardId) {
2397
+ console.error("Usage: --card-id may be used only with --card (single file), not with --card-glob");
2398
+ process.exit(1);
2399
+ }
2400
+ const cardFiles = cardFile ? [path.resolve(cardFile)] : resolveCardGlobMatches(cardGlob);
2401
+ if (!cardFile && cardFiles.length === 0) {
2402
+ console.error(`No card files matched glob: ${cardGlob}`);
2403
+ process.exit(1);
2404
+ }
2405
+ const idx = buildCardInventoryIndex(dir);
2406
+ const batchByCardId = /* @__PURE__ */ new Map();
2407
+ const batchByCardPath = /* @__PURE__ */ new Map();
2408
+ const plans = [];
2409
+ const logs = [];
2410
+ for (const absCardPath of cardFiles) {
2411
+ if (!fs.existsSync(absCardPath)) {
2412
+ console.error(`Card file not found: ${absCardPath}`);
2413
+ process.exit(1);
2414
+ }
2415
+ const card = JSON.parse(fs.readFileSync(absCardPath, "utf-8"));
2416
+ if (!card.id) {
2417
+ console.error(`Card JSON must have an "id" field (${absCardPath})`);
2418
+ process.exit(1);
2419
+ }
2420
+ if (requestedCardId && requestedCardId !== card.id) {
2421
+ console.error(
2422
+ `Card id mismatch: --card-id "${requestedCardId}" does not match file id "${card.id}" (${absCardPath})`
2423
+ );
2424
+ process.exit(1);
2425
+ }
2426
+ const seenPathCardId = batchByCardPath.get(absCardPath);
2427
+ if (seenPathCardId && seenPathCardId !== card.id) {
2428
+ console.error(
2429
+ `Upsert rejected: file "${absCardPath}" appears multiple times in batch with conflicting ids ("${seenPathCardId}" vs "${card.id}")`
2430
+ );
2431
+ process.exit(1);
2432
+ }
2433
+ const seenCardPath = batchByCardId.get(card.id);
2434
+ if (seenCardPath && seenCardPath !== absCardPath) {
2435
+ console.error(
2436
+ `Upsert rejected: card id "${card.id}" appears multiple times in batch with conflicting files ("${seenCardPath}" vs "${absCardPath}")`
2437
+ );
2438
+ process.exit(1);
2439
+ }
2440
+ const existingById = idx.byCardId.get(card.id);
2441
+ const existingByPath = idx.byCardPath.get(absCardPath);
2442
+ if (existingByPath && existingByPath.cardId !== card.id) {
2443
+ console.error(
2444
+ `Upsert rejected: file "${absCardPath}" is already mapped to card id "${existingByPath.cardId}", cannot remap to "${card.id}"`
2445
+ );
2446
+ process.exit(1);
2447
+ }
2448
+ if (existingById && existingById.cardFilePath !== absCardPath) {
2449
+ console.error(
2450
+ `Upsert rejected: card id "${card.id}" is already mapped to file "${existingById.cardFilePath}", cannot remap to "${absCardPath}"`
2451
+ );
2452
+ process.exit(1);
2453
+ }
2454
+ batchByCardPath.set(absCardPath, card.id);
2455
+ batchByCardId.set(card.id, absCardPath);
2456
+ plans.push({
2457
+ card,
2458
+ absCardPath,
2459
+ isInsert: !existingById
2460
+ });
2461
+ }
2462
+ for (const plan of plans) {
2463
+ const { card, absCardPath, isInsert } = plan;
2464
+ if (isInsert) {
2465
+ const newEntry = {
2466
+ cardId: card.id,
2467
+ cardFilePath: absCardPath,
2468
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
2469
+ };
2470
+ appendCardInventory(dir, newEntry);
2471
+ idx.byCardId.set(card.id, newEntry);
2472
+ idx.byCardPath.set(absCardPath, newEntry);
2473
+ }
2474
+ const taskConfig = liveCardToTaskConfig(card);
2475
+ appendEventToJournal(dir, {
2476
+ type: "task-upsert",
2477
+ taskName: card.id,
2478
+ taskConfig,
2479
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2480
+ });
2481
+ if (restart) {
2482
+ appendEventToJournal(dir, {
2483
+ type: "task-restart",
2484
+ taskName: card.id,
2485
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2486
+ });
2487
+ }
2488
+ logs.push(`Card "${card.id}" ${isInsert ? "upserted (inserted)" : "upserted (updated)"}${restart ? " (restarted)" : ""}.`);
2489
+ }
2490
+ void processAccumulatedEventsInfinitePass(dir);
2491
+ if (cardGlob) {
2492
+ console.log(`Upserted ${cardFiles.length} cards from glob: ${cardGlob}${restart ? " (restarted)" : ""}`);
2493
+ } else {
2494
+ console.log(logs[0]);
2495
+ }
2496
+ }
2141
2497
  async function cmdTryDrain(args) {
2142
2498
  const rgIdx = args.indexOf("--rg");
2143
2499
  const inlineLoop = args.includes("--inline-loop");
@@ -2181,6 +2537,8 @@ async function cli(argv) {
2181
2537
  return cmdAddCards(rest);
2182
2538
  case "update-card":
2183
2539
  return cmdUpdateCard(rest);
2540
+ case "upsert-card":
2541
+ return cmdUpsertCard(rest);
2184
2542
  case "remove-card":
2185
2543
  return cmdRemoveCard(rest);
2186
2544
  case "retrigger":
@@ -2200,9 +2558,7 @@ async function cli(argv) {
2200
2558
  case "process-accumulated-events":
2201
2559
  return await cmdTryDrain(rest);
2202
2560
  default:
2203
- console.error(`Unknown command: ${cmd ?? "(none)"}`);
2204
- console.error("Run: board-live-cards help");
2205
- process.exit(1);
2561
+ throw new Error(`Unknown command: ${cmd ?? "(none)"}`);
2206
2562
  }
2207
2563
  }
2208
2564
  function cmdHelp() {
@@ -2213,14 +2569,18 @@ USAGE
2213
2569
  board-live-cards-cli <command> [options]
2214
2570
 
2215
2571
  BOARD MANAGEMENT
2216
- init <dir> [--task-executor <script>]
2572
+ init <dir> [--task-executor <script>] [--runtime-out <dir>]
2217
2573
  Create a new board in <dir>.
2218
2574
  If --task-executor is given, writes <dir>/.task-executor with the script path.
2575
+ Writes <dir>/.runtime-out (default: <dir>/runtime-out).
2576
+ Published runtime files:
2577
+ <runtime-out>/board-livegraph-status.json
2578
+ <runtime-out>/cards/<card-id>.computed.json
2219
2579
  Re-running init on an existing board is safe; --task-executor updates the registration.
2220
2580
 
2221
2581
  status --rg <dir> [--json]
2222
- Print the current task status of every card in the board.
2223
- --json emits a stable machine-readable status object.
2582
+ Read and print the published status snapshot from <runtime-out>/board-livegraph-status.json.
2583
+ --json emits the stable machine-readable status object.
2224
2584
 
2225
2585
  CARD MANAGEMENT
2226
2586
  add-cards --rg <dir> (--card <card.json> | --card-glob <glob>)
@@ -2233,6 +2593,16 @@ CARD MANAGEMENT
2233
2593
  Re-read the card JSON from disk and patch the board.
2234
2594
  --restart clears the task so it re-triggers from scratch.
2235
2595
 
2596
+ upsert-card --rg <dir> (--card <card.json> | --card-glob <glob>) [--card-id <card-id>] [--restart]
2597
+ Insert or update one or many cards.
2598
+ Enforces strict one-to-one mapping between card id and file path:
2599
+ - same id + same file path: update
2600
+ - new id + new file path: insert
2601
+ - id remap or file remap: rejected
2602
+ If --card-id is provided, it must match the id inside the file.
2603
+ --card-id is valid only with --card (single file), not with --card-glob.
2604
+ --restart clears the task so it re-triggers from scratch.
2605
+
2236
2606
  remove-card --rg <dir> --id <card-id>
2237
2607
  Remove a card and its task from the board.
2238
2608
 
@@ -2327,6 +2697,6 @@ if (isMain) {
2327
2697
  });
2328
2698
  }
2329
2699
 
2330
- export { BoardJournal, appendCardInventory, appendEventToJournal, cli, createBoardReactiveGraph, decodeSourceToken, encodeSourceToken, getUndrainedEntries, initBoard, liveCardToTaskConfig, loadBoard, loadBoardEnvelope, lookupCardPath, processAccumulatedEvents, processAccumulatedEventsForced, processAccumulatedEventsInfinitePass, readCardInventory, saveBoard, withBoardLock };
2700
+ export { BoardJournal, appendCardInventory, appendEventToJournal, buildCardInventoryIndex, cli, createBoardReactiveGraph, decodeSourceToken, encodeSourceToken, getUndrainedEntries, initBoard, liveCardToTaskConfig, loadBoard, loadBoardEnvelope, lookupCardPath, processAccumulatedEvents, processAccumulatedEventsForced, processAccumulatedEventsInfinitePass, readCardInventory, saveBoard, withBoardLock };
2331
2701
  //# sourceMappingURL=board-live-cards-cli.js.map
2332
2702
  //# sourceMappingURL=board-live-cards-cli.js.map