yaml-flow 4.0.0 → 5.0.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 (95) 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.js +87 -0
  75. package/examples/example-board/demo-shell-browser.html +806 -0
  76. package/examples/example-board/demo-shell-with-server.html +280 -0
  77. package/examples/example-board/demo-shell.html +62 -0
  78. package/examples/example-board/demo-task-executor.js +255 -0
  79. package/examples/example-board/mock.db +15 -0
  80. package/examples/example-board/reusable-board-runtime-client.js +265 -0
  81. package/examples/example-board/reusable-runtime-artifacts-adapter.js +233 -0
  82. package/examples/example-board/reusable-server-runtime.js +1284 -0
  83. package/examples/index.html +16 -9
  84. package/examples/npm-libs/continuous-event-graph/live-cards-board.ts +17 -17
  85. package/examples/npm-libs/continuous-event-graph/live-portfolio-dashboard.ts +23 -23
  86. package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
  87. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +3 -3
  88. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
  89. package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +1 -1
  90. package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
  91. package/package.json +16 -2
  92. package/schema/card-runtime.schema.json +25 -0
  93. package/schema/live-cards.schema.json +46 -21
  94. package/browser/ingest-board.js +0 -296
  95. package/examples/ingest.js +0 -733
@@ -8,7 +8,7 @@ var child_process = require('child_process');
8
8
  var url = require('url');
9
9
  var fg = require('fast-glob');
10
10
  var properLockfile = require('proper-lockfile');
11
- var jsonata = require('jsonata');
11
+ var jsonata2 = require('jsonata');
12
12
  require('ajv-formats');
13
13
 
14
14
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
@@ -36,7 +36,7 @@ var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
36
36
  var os__namespace = /*#__PURE__*/_interopNamespace(os);
37
37
  var path__namespace = /*#__PURE__*/_interopNamespace(path);
38
38
  var fg__default = /*#__PURE__*/_interopDefault(fg);
39
- var jsonata__default = /*#__PURE__*/_interopDefault(jsonata);
39
+ var jsonata2__default = /*#__PURE__*/_interopDefault(jsonata2);
40
40
 
41
41
  // src/cli/board-live-cards-cli.ts
42
42
 
@@ -667,9 +667,11 @@ var MemoryJournal = class {
667
667
  return this.buffer.length;
668
668
  }
669
669
  };
670
+
671
+ // src/continuous-event-graph/reactive.ts
670
672
  function computeDataHash(data) {
671
673
  const json = stableStringify(data);
672
- return crypto.createHash("sha256").update(json).digest("hex").slice(0, 16);
674
+ return fnv1a64Hex(json);
673
675
  }
674
676
  function stableStringify(value) {
675
677
  if (value === null || value === void 0 || typeof value !== "object") {
@@ -682,13 +684,49 @@ function stableStringify(value) {
682
684
  const keys = Object.keys(obj).sort();
683
685
  return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
684
686
  }
687
+ function fnv1a64Hex(input) {
688
+ let hash = 0xcbf29ce484222325n;
689
+ const prime = 0x100000001b3n;
690
+ const mod = 0xffffffffffffffffn;
691
+ for (let i = 0; i < input.length; i++) {
692
+ hash ^= BigInt(input.charCodeAt(i));
693
+ hash = hash * prime & mod;
694
+ }
695
+ return hash.toString(16).padStart(16, "0");
696
+ }
697
+ function base64UrlEncode(input) {
698
+ if (typeof Buffer !== "undefined") {
699
+ return Buffer.from(input, "utf8").toString("base64url");
700
+ }
701
+ if (typeof btoa === "function") {
702
+ const bytes = new TextEncoder().encode(input);
703
+ let binary = "";
704
+ for (const b of bytes) binary += String.fromCharCode(b);
705
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
706
+ }
707
+ throw new Error("No base64 encoder available in this runtime");
708
+ }
709
+ function base64UrlDecode(input) {
710
+ if (typeof Buffer !== "undefined") {
711
+ return Buffer.from(input, "base64url").toString("utf8");
712
+ }
713
+ if (typeof atob === "function") {
714
+ const base64 = input.replace(/-/g, "+").replace(/_/g, "/");
715
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
716
+ const binary = atob(padded);
717
+ const bytes = new Uint8Array(binary.length);
718
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
719
+ return new TextDecoder().decode(bytes);
720
+ }
721
+ throw new Error("No base64 decoder available in this runtime");
722
+ }
685
723
  function encodeCallbackToken(taskName) {
686
724
  const payload = JSON.stringify({ t: taskName, n: Date.now().toString(36) + Math.random().toString(36).slice(2, 6) });
687
- return Buffer.from(payload).toString("base64url");
725
+ return base64UrlEncode(payload);
688
726
  }
689
727
  function decodeCallbackToken(token) {
690
728
  try {
691
- const payload = JSON.parse(Buffer.from(token, "base64url").toString());
729
+ const payload = JSON.parse(base64UrlDecode(token));
692
730
  if (typeof payload?.t === "string") return { taskName: payload.t };
693
731
  return null;
694
732
  } catch {
@@ -965,18 +1003,18 @@ function deepSet(obj, path2, value) {
965
1003
  }
966
1004
  async function run(node, options) {
967
1005
  if (!node?.compute?.length) return node;
968
- if (!node.state) node.state = {};
1006
+ if (!node.card_data) node.card_data = {};
969
1007
  node.computed_values = {};
970
1008
  node._sourcesData = options?.sourcesData ?? {};
971
1009
  const ctx = {
972
- state: node.state,
1010
+ card_data: node.card_data,
973
1011
  requires: node.requires ?? {},
974
- sources: node._sourcesData,
1012
+ fetched_sources: node._sourcesData,
975
1013
  computed_values: node.computed_values
976
1014
  };
977
1015
  for (const step of node.compute) {
978
1016
  try {
979
- const val = await jsonata__default.default(step.expr).evaluate(ctx);
1017
+ const val = await jsonata2__default.default(step.expr).evaluate(ctx);
980
1018
  deepSet(node.computed_values, step.bindTo, val);
981
1019
  ctx.computed_values = node.computed_values;
982
1020
  } catch (err) {
@@ -987,16 +1025,16 @@ async function run(node, options) {
987
1025
  }
988
1026
  async function evalExpr(expr, node) {
989
1027
  const ctx = {
990
- state: node.state ?? {},
1028
+ card_data: node.card_data ?? {},
991
1029
  requires: node.requires ?? {},
992
- sources: node._sourcesData ?? {},
1030
+ fetched_sources: node._sourcesData ?? {},
993
1031
  computed_values: node.computed_values ?? {}
994
1032
  };
995
- return jsonata__default.default(expr).evaluate(ctx);
1033
+ return jsonata2__default.default(expr).evaluate(ctx);
996
1034
  }
997
1035
  function resolve(node, path2) {
998
- if (path2.startsWith("sources.")) {
999
- return deepGet(node._sourcesData ?? {}, path2.slice("sources.".length));
1036
+ if (path2.startsWith("fetched_sources.")) {
1037
+ return deepGet(node._sourcesData ?? {}, path2.slice("fetched_sources.".length));
1000
1038
  }
1001
1039
  return deepGet(node, path2);
1002
1040
  }
@@ -1016,8 +1054,7 @@ var VALID_ELEMENT_KINDS = /* @__PURE__ */ new Set([
1016
1054
  "markdown",
1017
1055
  "custom"
1018
1056
  ]);
1019
- var VALID_STATUSES = /* @__PURE__ */ new Set(["fresh", "stale", "loading", "error"]);
1020
- var ALLOWED_KEYS = /* @__PURE__ */ new Set(["id", "meta", "requires", "provides", "view", "state", "compute", "sources"]);
1057
+ var ALLOWED_KEYS = /* @__PURE__ */ new Set(["id", "meta", "requires", "provides", "view", "card_data", "compute", "sources"]);
1021
1058
  function validateNode(node) {
1022
1059
  const errors = [];
1023
1060
  if (!node || typeof node !== "object" || Array.isArray(node)) {
@@ -1028,13 +1065,8 @@ function validateNode(node) {
1028
1065
  for (const key of Object.keys(n)) {
1029
1066
  if (!ALLOWED_KEYS.has(key)) errors.push(`Unknown top-level key: "${key}"`);
1030
1067
  }
1031
- if (n.state == null || typeof n.state !== "object" || Array.isArray(n.state)) {
1032
- errors.push("state: required, must be an object");
1033
- } else {
1034
- const state = n.state;
1035
- if (state.status != null && !VALID_STATUSES.has(state.status)) {
1036
- errors.push(`state.status: must be one of: ${[...VALID_STATUSES].join(", ")}`);
1037
- }
1068
+ if (n.card_data == null || typeof n.card_data !== "object" || Array.isArray(n.card_data)) {
1069
+ errors.push("card_data: required, must be an object");
1038
1070
  }
1039
1071
  if (n.meta != null) {
1040
1072
  if (typeof n.meta !== "object" || Array.isArray(n.meta)) {
@@ -1123,17 +1155,33 @@ function validateNode(node) {
1123
1155
  }
1124
1156
  return { ok: errors.length === 0, errors };
1125
1157
  }
1158
+ function enrichSources(sources, context) {
1159
+ if (!sources || sources.length === 0) return [];
1160
+ return sources.map((src) => ({
1161
+ ...src,
1162
+ _requires: context.requires ?? {},
1163
+ _sourcesData: context.sourcesData ?? {},
1164
+ _computed_values: context.computed_values ?? {}
1165
+ }));
1166
+ }
1126
1167
  var CardCompute = {
1127
1168
  run,
1128
1169
  eval: evalExpr,
1129
1170
  resolve,
1130
- validate: validateNode
1171
+ validate: validateNode,
1172
+ enrichSources
1131
1173
  };
1132
1174
 
1133
1175
  // src/cli/board-live-cards-cli.ts
1134
1176
  var BOARD_FILE = "board-graph.json";
1135
1177
  var JOURNAL_FILE = "board-journal.jsonl";
1178
+ var TASK_EXECUTOR_LOG_FILE = "task-executor.jsonl";
1136
1179
  var INVENTORY_FILE = "cards-inventory.jsonl";
1180
+ var RUNTIME_OUT_FILE = ".runtime-out";
1181
+ var DEFAULT_RUNTIME_OUT_DIR = "runtime-out";
1182
+ var RUNTIME_STATUS_FILE = "board-livegraph-status.json";
1183
+ var RUNTIME_CARDS_DIR = "cards";
1184
+ var RUNTIME_DATA_OBJECTS_DIR = "data-objects";
1137
1185
  var EMPTY_CONFIG = { settings: { completion: "manual", refreshStrategy: "data-changed" }, tasks: {} };
1138
1186
  var BoardJournal = class {
1139
1187
  journalPath;
@@ -1188,7 +1236,34 @@ function lookupCardPath(boardDir, cardId) {
1188
1236
  }
1189
1237
  function appendCardInventory(boardDir, entry) {
1190
1238
  const inventoryPath = path__namespace.join(boardDir, INVENTORY_FILE);
1191
- fs__namespace.appendFileSync(inventoryPath, JSON.stringify(entry) + "\n");
1239
+ const normalized = { ...entry, cardFilePath: path__namespace.resolve(entry.cardFilePath) };
1240
+ fs__namespace.appendFileSync(inventoryPath, JSON.stringify(normalized) + "\n");
1241
+ }
1242
+ function buildCardInventoryIndex(boardDir) {
1243
+ const byCardId = /* @__PURE__ */ new Map();
1244
+ const byCardPath = /* @__PURE__ */ new Map();
1245
+ for (const entry of readCardInventory(boardDir)) {
1246
+ const normalizedPath = path__namespace.resolve(entry.cardFilePath);
1247
+ const normalizedEntry = {
1248
+ ...entry,
1249
+ cardFilePath: normalizedPath
1250
+ };
1251
+ const existingById = byCardId.get(entry.cardId);
1252
+ if (existingById && existingById.cardFilePath !== normalizedPath) {
1253
+ throw new Error(
1254
+ `Inventory invariant violation: card id "${entry.cardId}" maps to multiple files: "${existingById.cardFilePath}" and "${normalizedPath}"`
1255
+ );
1256
+ }
1257
+ const existingByPath = byCardPath.get(normalizedPath);
1258
+ if (existingByPath && existingByPath.cardId !== entry.cardId) {
1259
+ throw new Error(
1260
+ `Inventory invariant violation: file "${normalizedPath}" maps to multiple ids: "${existingByPath.cardId}" and "${entry.cardId}"`
1261
+ );
1262
+ }
1263
+ byCardId.set(entry.cardId, normalizedEntry);
1264
+ byCardPath.set(normalizedPath, normalizedEntry);
1265
+ }
1266
+ return { byCardId, byCardPath };
1192
1267
  }
1193
1268
  function initBoard(dir) {
1194
1269
  const boardPath = path__namespace.join(dir, BOARD_FILE);
@@ -1224,7 +1299,63 @@ function saveBoard(dir, rg, journal) {
1224
1299
  lastDrainedJournalId: journal.lastDrainedJournalId,
1225
1300
  graph: snap
1226
1301
  };
1227
- fs__namespace.writeFileSync(path__namespace.join(dir, BOARD_FILE), JSON.stringify(envelope, null, 2));
1302
+ writeJsonAtomic(path__namespace.join(dir, BOARD_FILE), envelope);
1303
+ const live = restore(snap);
1304
+ const statusObject = buildBoardStatusObject(dir, live);
1305
+ writeJsonAtomic(resolveStatusSnapshotPath(dir), statusObject);
1306
+ }
1307
+ function runtimeOutConfigPath(boardDir) {
1308
+ return path__namespace.join(boardDir, RUNTIME_OUT_FILE);
1309
+ }
1310
+ function resolveConfiguredRuntimeOutDir(boardDir) {
1311
+ const cfgPath = runtimeOutConfigPath(boardDir);
1312
+ if (fs__namespace.existsSync(cfgPath)) {
1313
+ const configured = fs__namespace.readFileSync(cfgPath, "utf-8").trim();
1314
+ if (configured) {
1315
+ return path__namespace.isAbsolute(configured) ? configured : path__namespace.resolve(boardDir, configured);
1316
+ }
1317
+ }
1318
+ const defaultDir = path__namespace.join(boardDir, DEFAULT_RUNTIME_OUT_DIR);
1319
+ fs__namespace.writeFileSync(cfgPath, defaultDir, "utf-8");
1320
+ return defaultDir;
1321
+ }
1322
+ function configureRuntimeOutDir(boardDir, runtimeOut) {
1323
+ let resolved;
1324
+ if (runtimeOut) {
1325
+ resolved = path__namespace.isAbsolute(runtimeOut) ? runtimeOut : path__namespace.resolve(boardDir, runtimeOut);
1326
+ } else {
1327
+ resolved = path__namespace.join(boardDir, DEFAULT_RUNTIME_OUT_DIR);
1328
+ }
1329
+ fs__namespace.mkdirSync(resolved, { recursive: true });
1330
+ fs__namespace.writeFileSync(runtimeOutConfigPath(boardDir), resolved, "utf-8");
1331
+ return resolved;
1332
+ }
1333
+ function resolveStatusSnapshotPath(boardDir) {
1334
+ return path__namespace.join(resolveConfiguredRuntimeOutDir(boardDir), RUNTIME_STATUS_FILE);
1335
+ }
1336
+ function resolveComputedValuesPath(boardDir, cardId) {
1337
+ return path__namespace.join(resolveConfiguredRuntimeOutDir(boardDir), RUNTIME_CARDS_DIR, `${cardId}.computed.json`);
1338
+ }
1339
+ function resolveDataObjectsDirPath(boardDir) {
1340
+ return path__namespace.join(resolveConfiguredRuntimeOutDir(boardDir), RUNTIME_DATA_OBJECTS_DIR);
1341
+ }
1342
+ function toDataObjectFileName(token) {
1343
+ return token.replace(/[\\/]/g, "__");
1344
+ }
1345
+ function writeRuntimeDataObjects(boardDir, data) {
1346
+ for (const [token, payload] of Object.entries(data)) {
1347
+ if (!token) continue;
1348
+ const fileName = toDataObjectFileName(token);
1349
+ if (!fileName) continue;
1350
+ const filePath = path__namespace.join(resolveDataObjectsDirPath(boardDir), fileName);
1351
+ writeJsonAtomic(filePath, payload);
1352
+ }
1353
+ }
1354
+ function writeJsonAtomic(filePath, payload) {
1355
+ fs__namespace.mkdirSync(path__namespace.dirname(filePath), { recursive: true });
1356
+ const tmpPath = `${filePath}.${process.pid}.${crypto.randomUUID()}.tmp`;
1357
+ fs__namespace.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), "utf-8");
1358
+ fs__namespace.renameSync(tmpPath, filePath);
1228
1359
  }
1229
1360
  function withBoardLock(boardDir, fn) {
1230
1361
  const boardPath = path__namespace.join(boardDir, BOARD_FILE);
@@ -1302,15 +1433,63 @@ function shouldUseShellForCommand(cmd, forceShell) {
1302
1433
  if (typeof forceShell === "boolean") return forceShell;
1303
1434
  return process.platform === "win32" && /\.(cmd|bat)$/i.test(cmd);
1304
1435
  }
1436
+ var _gitBashPath;
1437
+ var GIT_BASH_CACHE_FILE = path__namespace.join(os__namespace.tmpdir(), ".board-live-cards-git-bash-cache.json");
1438
+ function findGitBash() {
1439
+ if (_gitBashPath !== void 0) return _gitBashPath;
1440
+ if (process.platform !== "win32") return _gitBashPath = false;
1441
+ try {
1442
+ const cached = JSON.parse(fs__namespace.readFileSync(GIT_BASH_CACHE_FILE, "utf8"));
1443
+ if (cached.path === false || typeof cached.path === "string" && fs__namespace.existsSync(cached.path)) {
1444
+ return _gitBashPath = cached.path;
1445
+ }
1446
+ } catch {
1447
+ }
1448
+ const candidates = [
1449
+ process.env.SHELL,
1450
+ process.env.PROGRAMFILES && path__namespace.join(process.env.PROGRAMFILES, "Git", "usr", "bin", "bash.exe"),
1451
+ process.env.PROGRAMFILES && path__namespace.join(process.env.PROGRAMFILES, "Git", "bin", "bash.exe"),
1452
+ process.env["PROGRAMFILES(X86)"] && path__namespace.join(process.env["PROGRAMFILES(X86)"], "Git", "bin", "bash.exe"),
1453
+ process.env.LOCALAPPDATA && path__namespace.join(process.env.LOCALAPPDATA, "Programs", "Git", "bin", "bash.exe")
1454
+ ];
1455
+ for (const c of candidates) {
1456
+ if (c && /bash(\.exe)?$/i.test(c) && fs__namespace.existsSync(c)) {
1457
+ _gitBashPath = c;
1458
+ try {
1459
+ fs__namespace.writeFileSync(GIT_BASH_CACHE_FILE, JSON.stringify({ path: c }));
1460
+ } catch {
1461
+ }
1462
+ return _gitBashPath;
1463
+ }
1464
+ }
1465
+ _gitBashPath = false;
1466
+ try {
1467
+ fs__namespace.writeFileSync(GIT_BASH_CACHE_FILE, JSON.stringify({ path: false }));
1468
+ } catch {
1469
+ }
1470
+ return _gitBashPath;
1471
+ }
1472
+ function shellQuote(s) {
1473
+ return "'" + s.replace(/'/g, "'\\''") + "'";
1474
+ }
1305
1475
  function spawnDetachedCommand(cmd, args) {
1306
- const child = process.platform === "win32" ? child_process.spawn("cmd", ["/c", "start", "/b", "", cmd, ...args], {
1307
- stdio: "ignore",
1308
- windowsHide: true
1309
- }) : child_process.spawn(cmd, args, {
1310
- shell: false,
1311
- detached: true,
1312
- stdio: "ignore"
1313
- });
1476
+ if (process.platform === "win32") {
1477
+ const bash = findGitBash();
1478
+ if (bash) {
1479
+ const shellCmd = [cmd, ...args].map((a) => shellQuote(a.replace(/\\/g, "/"))).join(" ");
1480
+ const child3 = child_process.spawn(bash, ["-c", shellCmd], { detached: true, stdio: "ignore", windowsHide: true });
1481
+ child3.unref();
1482
+ return;
1483
+ }
1484
+ const child2 = child_process.spawn("cmd", ["/c", "start", "/b", "", cmd, ...args], {
1485
+ detached: true,
1486
+ stdio: "ignore",
1487
+ windowsHide: true
1488
+ });
1489
+ child2.unref();
1490
+ return;
1491
+ }
1492
+ const child = child_process.spawn(cmd, args, { detached: true, stdio: "ignore" });
1314
1493
  child.unref();
1315
1494
  }
1316
1495
  function execCommandSync(cmd, args, options) {
@@ -1415,7 +1594,7 @@ async function processAccumulatedEventsForced(boardDir, options) {
1415
1594
  }
1416
1595
  function liveCardToTaskConfig(card) {
1417
1596
  const requires = card.requires;
1418
- const provides = card.provides ? card.provides.map((p) => p.bindTo) : [card.id];
1597
+ const provides = card.provides?.map((p) => p.bindTo) ?? [];
1419
1598
  return {
1420
1599
  requires: requires && requires.length > 0 ? requires : void 0,
1421
1600
  provides,
@@ -1440,23 +1619,23 @@ function getCliInvocation(command, args) {
1440
1619
  }
1441
1620
  function invokeRunSources(boardDir, cardPath, callbackToken, callback) {
1442
1621
  const { cmd, args } = getCliInvocation("run-sources-internal", ["--card", cardPath, "--token", callbackToken, "--rg", boardDir]);
1443
- const child = process.platform === "win32" ? child_process.spawn("cmd", ["/c", "start", "/b", "", cmd, ...args], {
1444
- stdio: "ignore",
1445
- windowsHide: true
1446
- }) : child_process.spawn(cmd, args, {
1447
- shell: false,
1448
- detached: true,
1449
- stdio: "ignore"
1450
- });
1451
- let finished = false;
1452
- const done = (err) => {
1453
- if (finished) return;
1454
- finished = true;
1455
- callback(err);
1456
- };
1457
- child.on("error", (err) => done(err));
1458
- child.unref();
1459
- done(null);
1622
+ try {
1623
+ spawnDetachedCommand(cmd, args);
1624
+ callback(null);
1625
+ } catch (err) {
1626
+ callback(err instanceof Error ? err : new Error(String(err)));
1627
+ }
1628
+ }
1629
+ function appendTaskExecutorLog(boardDir, hydratedSource) {
1630
+ try {
1631
+ const entry = {
1632
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1633
+ hydratedSource
1634
+ };
1635
+ fs__namespace.appendFileSync(path__namespace.join(boardDir, TASK_EXECUTOR_LOG_FILE), JSON.stringify(entry) + "\n", "utf-8");
1636
+ } catch (logErr) {
1637
+ console.error(`[task-executor-log] append failed: ${logErr instanceof Error ? logErr.message : String(logErr)}`);
1638
+ }
1460
1639
  }
1461
1640
  function invokeSourceDataFetched(sourceToken, tmpFile, callback) {
1462
1641
  const { cmd, args } = getCliInvocation("source-data-fetched", ["--tmp", tmpFile, "--token", sourceToken]);
@@ -1481,7 +1660,7 @@ function createBoardReactiveGraph(boardDir) {
1481
1660
  if (!cardPath) return "task-initiate-failure";
1482
1661
  const card = JSON.parse(fs__namespace.readFileSync(cardPath, "utf-8"));
1483
1662
  const cardId = card.id;
1484
- const cardState = card.state ?? {};
1663
+ const cardState = card.card_data ?? {};
1485
1664
  const allSources = card.sources ?? [];
1486
1665
  const requiredSources = allSources.filter((s) => s.optionalForCompletionGating !== true);
1487
1666
  const runtime = readRuntimeState(boardDir, cardId);
@@ -1517,18 +1696,32 @@ function createBoardReactiveGraph(boardDir) {
1517
1696
  }
1518
1697
  }
1519
1698
  }
1699
+ const requires = {};
1700
+ for (const [token, taskData] of Object.entries(input.state ?? {})) {
1701
+ if (taskData !== null && typeof taskData === "object" && !Array.isArray(taskData)) {
1702
+ const unwrapped = taskData[token];
1703
+ requires[token] = unwrapped !== void 0 ? unwrapped : taskData;
1704
+ } else {
1705
+ requires[token] = taskData;
1706
+ }
1707
+ }
1520
1708
  const computeNode = {
1521
1709
  id: cardId,
1522
- state: { ...cardState },
1523
- requires: input.state ?? {},
1710
+ card_data: { ...cardState },
1711
+ requires,
1524
1712
  sources: allSources,
1525
1713
  compute: card.compute
1526
1714
  };
1715
+ computeNode._sourcesData = sourcesData;
1527
1716
  if (card.compute) {
1528
1717
  await CardCompute.run(computeNode, { sourcesData });
1529
- const cvPath = path__namespace.join(boardDir, `${cardId}.computed_values.json`);
1530
- fs__namespace.writeFileSync(cvPath, JSON.stringify(computeNode.computed_values ?? {}, null, 2));
1531
1718
  }
1719
+ const cvPath = resolveComputedValuesPath(boardDir, cardId);
1720
+ writeJsonAtomic(cvPath, {
1721
+ schema_version: "v1",
1722
+ card_id: cardId,
1723
+ computed_values: computeNode.computed_values ?? {}
1724
+ });
1532
1725
  const now = (/* @__PURE__ */ new Date()).toISOString();
1533
1726
  const undeliveredRequired = requiredSources.filter((s) => {
1534
1727
  if (!s.outputFile) return false;
@@ -1548,16 +1741,40 @@ function createBoardReactiveGraph(boardDir) {
1548
1741
  }
1549
1742
  }
1550
1743
  if (stampedAny) writeRuntimeState(boardDir, cardId, runtime);
1551
- invokeRunSources(boardDir, cardPath, input.callbackToken, (err) => {
1552
- if (err) console.error(`[card-handler] ${input.nodeId}:`, err.message);
1744
+ const enrichedCard = { ...card };
1745
+ const enrichedSources = CardCompute.enrichSources(
1746
+ Array.isArray(card.sources) ? card.sources : void 0,
1747
+ {
1748
+ requires,
1749
+ sourcesData,
1750
+ computed_values: computeNode.computed_values
1751
+ }
1752
+ );
1753
+ const sourceCwd = path__namespace.dirname(cardPath);
1754
+ enrichedCard.sources = Array.isArray(enrichedSources) ? enrichedSources.map((src) => ({
1755
+ ...src,
1756
+ cwd: typeof src.cwd === "string" && src.cwd ? src.cwd : sourceCwd,
1757
+ boardDir: typeof src.boardDir === "string" && src.boardDir ? src.boardDir : boardDir
1758
+ })) : enrichedSources;
1759
+ const enrichedCardPath = path__namespace.join(os__namespace.tmpdir(), `card-enriched-${cardId}-${Date.now()}.json`);
1760
+ fs__namespace.writeFileSync(enrichedCardPath, JSON.stringify(enrichedCard, null, 2), "utf-8");
1761
+ invokeRunSources(boardDir, enrichedCardPath, input.callbackToken, (err) => {
1762
+ if (err) {
1763
+ console.error(`[card-handler] ${input.nodeId}:`, err.message);
1764
+ try {
1765
+ fs__namespace.unlinkSync(enrichedCardPath);
1766
+ } catch {
1767
+ }
1768
+ }
1553
1769
  });
1554
1770
  return "task-initiated";
1555
1771
  }
1556
- const providesBindings = card.provides ?? [{ bindTo: cardId, src: `state.${cardId}` }];
1772
+ const providesBindings = card.provides ?? [];
1557
1773
  const data = {};
1558
1774
  for (const { bindTo, src } of providesBindings) {
1559
1775
  data[bindTo] = CardCompute.resolve(computeNode, src);
1560
1776
  }
1777
+ writeRuntimeDataObjects(boardDir, data);
1561
1778
  const undeliveredOptional = allSources.filter((s) => {
1562
1779
  if (s.optionalForCompletionGating !== true || !s.outputFile) return false;
1563
1780
  const entry = runtime._sources[s.bindTo];
@@ -1585,18 +1802,20 @@ function createBoardReactiveGraph(boardDir) {
1585
1802
  function addSingleCardFromFile(dir, cardFile) {
1586
1803
  const absCardPath = path__namespace.resolve(cardFile);
1587
1804
  if (!fs__namespace.existsSync(absCardPath)) {
1588
- console.error(`Card file not found: ${absCardPath}`);
1589
- process.exit(1);
1805
+ throw new Error(`Card file not found: ${absCardPath}`);
1806
+ }
1807
+ let card;
1808
+ try {
1809
+ card = JSON.parse(fs__namespace.readFileSync(absCardPath, "utf-8"));
1810
+ } catch (err) {
1811
+ throw new Error(`Failed to parse card file: ${absCardPath} - ${err instanceof Error ? err.message : String(err)}`);
1590
1812
  }
1591
- const card = JSON.parse(fs__namespace.readFileSync(absCardPath, "utf-8"));
1592
1813
  if (!card.id) {
1593
- console.error('Card JSON must have an "id" field');
1594
- process.exit(1);
1814
+ throw new Error('Card JSON must have an "id" field');
1595
1815
  }
1596
1816
  const existing = readCardInventory(dir);
1597
1817
  if (existing.some((e) => e.cardId === card.id)) {
1598
- console.error(`Card "${card.id}" already exists in inventory`);
1599
- process.exit(1);
1818
+ throw new Error(`Card "${card.id}" already exists in inventory`);
1600
1819
  }
1601
1820
  appendCardInventory(dir, {
1602
1821
  cardId: card.id,
@@ -1623,7 +1842,7 @@ function resolveCardGlobMatches(cardGlob) {
1623
1842
  unique: true,
1624
1843
  dot: false
1625
1844
  });
1626
- return [...matches].sort((a, b) => a.localeCompare(b));
1845
+ return [...matches].map((m) => path__namespace.resolve(m)).sort((a, b) => a.localeCompare(b));
1627
1846
  }
1628
1847
  function cmdAddCards(args) {
1629
1848
  const rgIdx = args.indexOf("--rg");
@@ -1633,16 +1852,14 @@ function cmdAddCards(args) {
1633
1852
  const cardFile = cardIdx !== -1 ? args[cardIdx + 1] : void 0;
1634
1853
  const cardGlob = globIdx !== -1 ? args[globIdx + 1] : void 0;
1635
1854
  if (!dir || !cardFile && !cardGlob || cardFile && cardGlob) {
1636
- console.error("Usage: board-live-cards add-cards --rg <dir> (--card <card.json> | --card-glob <glob>)");
1637
- process.exit(1);
1855
+ throw new Error("Usage: board-live-cards add-cards --rg <dir> (--card <card.json> | --card-glob <glob>)");
1638
1856
  }
1639
1857
  if (cardFile) {
1640
1858
  addSingleCardFromFile(dir, cardFile);
1641
1859
  } else {
1642
1860
  const matches = resolveCardGlobMatches(cardGlob);
1643
1861
  if (matches.length === 0) {
1644
- console.error(`No card files matched glob: ${cardGlob}`);
1645
- process.exit(1);
1862
+ throw new Error(`No card files matched glob: ${cardGlob}`);
1646
1863
  }
1647
1864
  for (const match of matches) {
1648
1865
  addSingleCardFromFile(dir, match);
@@ -1654,30 +1871,34 @@ function cmdAddCards(args) {
1654
1871
  function cmdInit(args) {
1655
1872
  const dir = args[0];
1656
1873
  if (!dir) {
1657
- console.error("Usage: board-live-cards init <dir> [--task-executor <script>]");
1658
- process.exit(1);
1874
+ throw new Error("Usage: board-live-cards init <dir> [--task-executor <script>] [--chat-handler <script>] [--runtime-out <dir>]");
1659
1875
  }
1660
1876
  const teIdx = args.indexOf("--task-executor");
1661
1877
  const taskExecutor = teIdx !== -1 ? args[teIdx + 1] : void 0;
1878
+ const chIdx = args.indexOf("--chat-handler");
1879
+ const chatHandler = chIdx !== -1 ? args[chIdx + 1] : void 0;
1880
+ const roIdx = args.indexOf("--runtime-out");
1881
+ const runtimeOut = roIdx !== -1 ? args[roIdx + 1] : void 0;
1882
+ if (roIdx !== -1 && !runtimeOut) {
1883
+ throw new Error("Usage: board-live-cards init <dir> [--task-executor <script>] [--chat-handler <script>] [--runtime-out <dir>]");
1884
+ }
1662
1885
  const result = initBoard(dir);
1663
1886
  if (taskExecutor) {
1664
1887
  fs__namespace.writeFileSync(path__namespace.join(dir, ".task-executor"), taskExecutor, "utf-8");
1665
1888
  }
1889
+ if (chatHandler) {
1890
+ fs__namespace.writeFileSync(path__namespace.join(dir, ".chat-handler"), chatHandler, "utf-8");
1891
+ }
1892
+ const runtimeOutDir = configureRuntimeOutDir(dir, runtimeOut);
1893
+ const live = loadBoard(dir);
1894
+ writeJsonAtomic(resolveStatusSnapshotPath(dir), buildBoardStatusObject(dir, live));
1666
1895
  if (result === "exists") {
1667
- console.log(`Board already initialized at ${path__namespace.resolve(dir)}${taskExecutor ? ` (task-executor updated: ${taskExecutor})` : ""}`);
1896
+ console.log(`Board already initialized at ${path__namespace.resolve(dir)}${taskExecutor ? ` (task-executor updated: ${taskExecutor})` : ""} (runtime-out: ${runtimeOutDir})`);
1668
1897
  } else {
1669
- console.log(`Board initialized at ${path__namespace.resolve(dir)}${taskExecutor ? ` (task-executor: ${taskExecutor})` : ""}`);
1898
+ console.log(`Board initialized at ${path__namespace.resolve(dir)}${taskExecutor ? ` (task-executor: ${taskExecutor})` : ""} (runtime-out: ${runtimeOutDir})`);
1670
1899
  }
1671
1900
  }
1672
- function cmdStatus(args) {
1673
- const rgIdx = args.indexOf("--rg");
1674
- const asJson = args.includes("--json");
1675
- const dir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
1676
- if (!dir) {
1677
- console.error("Usage: board-live-cards status --rg <dir>");
1678
- process.exit(1);
1679
- }
1680
- const live = loadBoard(dir);
1901
+ function buildBoardStatusObject(dir, live) {
1681
1902
  const taskState = live.state.tasks;
1682
1903
  const taskConfig = live.config.tasks;
1683
1904
  const cardNames = Object.keys(taskState);
@@ -1694,14 +1915,8 @@ function cmdStatus(args) {
1694
1915
  for (const p of sched.pending) waitingByCard.set(p.taskName, p.waitingOn);
1695
1916
  for (const u of sched.unresolved) waitingByCard.set(u.taskName, u.missingTokens);
1696
1917
  for (const b of sched.blocked) waitingByCard.set(b.taskName, b.failedTokens);
1697
- const providersByToken = /* @__PURE__ */ new Map();
1698
1918
  const dependentsByToken = /* @__PURE__ */ new Map();
1699
1919
  for (const [name, cfg] of Object.entries(taskConfig)) {
1700
- for (const token of cfg.provides ?? []) {
1701
- const providers = providersByToken.get(token) ?? [];
1702
- providers.push(name);
1703
- providersByToken.set(token, providers);
1704
- }
1705
1920
  for (const token of cfg.requires ?? []) {
1706
1921
  const dependents = dependentsByToken.get(token) ?? [];
1707
1922
  dependents.push(name);
@@ -1771,7 +1986,7 @@ function cmdStatus(args) {
1771
1986
  const feedsAny = provides.some((p) => (dependentsByToken.get(p) ?? []).some((d) => d !== name));
1772
1987
  if (requiresNone && !feedsAny) orphanCards += 1;
1773
1988
  }
1774
- const statusObject = {
1989
+ return {
1775
1990
  schema_version: "v1",
1776
1991
  meta: {
1777
1992
  board: {
@@ -1796,6 +2011,23 @@ function cmdStatus(args) {
1796
2011
  },
1797
2012
  cards
1798
2013
  };
2014
+ }
2015
+ function cmdStatus(args) {
2016
+ const rgIdx = args.indexOf("--rg");
2017
+ const asJson = args.includes("--json");
2018
+ const dir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
2019
+ if (!dir) {
2020
+ console.error("Usage: board-live-cards status --rg <dir>");
2021
+ process.exit(1);
2022
+ }
2023
+ const statusOutPath = resolveStatusSnapshotPath(dir);
2024
+ let statusObject;
2025
+ if (fs__namespace.existsSync(statusOutPath)) {
2026
+ statusObject = JSON.parse(fs__namespace.readFileSync(statusOutPath, "utf-8"));
2027
+ } else {
2028
+ statusObject = buildBoardStatusObject(dir, loadBoard(dir));
2029
+ writeJsonAtomic(statusOutPath, statusObject);
2030
+ }
1799
2031
  if (asJson) {
1800
2032
  console.log(JSON.stringify(statusObject, null, 2));
1801
2033
  return;
@@ -1826,6 +2058,7 @@ function cmdTaskCompleted(args) {
1826
2058
  process.exit(1);
1827
2059
  }
1828
2060
  const data = dataIdx !== -1 ? JSON.parse(args[dataIdx + 1]) : {};
2061
+ writeRuntimeDataObjects(dir, data);
1829
2062
  appendEventToJournal(dir, {
1830
2063
  type: "task-completed",
1831
2064
  taskName: decoded.taskName,
@@ -1952,6 +2185,12 @@ function cmdRunSources(args) {
1952
2185
  process.exit(1);
1953
2186
  }
1954
2187
  const card = JSON.parse(fs__namespace.readFileSync(cardFilePath, "utf-8"));
2188
+ if (path__namespace.basename(cardFilePath).startsWith("card-enriched-")) {
2189
+ try {
2190
+ fs__namespace.unlinkSync(cardFilePath);
2191
+ } catch {
2192
+ }
2193
+ }
1955
2194
  console.log(`[run-sources-internal] Processing card "${card.id}"`);
1956
2195
  const executorFile = path__namespace.join(boardDir, ".task-executor");
1957
2196
  const taskExecutor = fs__namespace.existsSync(executorFile) ? fs__namespace.readFileSync(executorFile, "utf-8").trim() : void 0;
@@ -1980,7 +2219,12 @@ function cmdRunSources(args) {
1980
2219
  const inFile = path__namespace.join(os__namespace.tmpdir(), `card-source-in-${src.bindTo}-${Date.now()}.json`);
1981
2220
  const outFile2 = path__namespace.join(os__namespace.tmpdir(), `card-source-out-${src.bindTo}-${Date.now()}.json`);
1982
2221
  const errFile = path__namespace.join(os__namespace.tmpdir(), `card-source-err-${src.bindTo}-${Date.now()}.txt`);
1983
- const sourceForExecutor = { ...src, cwd: path__namespace.dirname(cardFilePath || ""), boardDir };
2222
+ const sourceForExecutor = {
2223
+ ...src,
2224
+ cwd: typeof src.cwd === "string" && src.cwd ? src.cwd : path__namespace.dirname(cardFilePath || ""),
2225
+ boardDir: typeof src.boardDir === "string" && src.boardDir ? src.boardDir : boardDir
2226
+ };
2227
+ appendTaskExecutorLog(boardDir, sourceForExecutor);
1984
2228
  fs__namespace.writeFileSync(inFile, JSON.stringify(sourceForExecutor, null, 2), "utf-8");
1985
2229
  console.log(`[run-sources-internal] task-executor: ${taskExecutor} run-source-fetch --in ${inFile} --out ${outFile2} --err ${errFile}`);
1986
2230
  try {
@@ -2137,17 +2381,14 @@ function cmdUpdateCard(args) {
2137
2381
  const dir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
2138
2382
  const cardId = idIdx !== -1 ? args[idIdx + 1] : void 0;
2139
2383
  if (!dir || !cardId) {
2140
- console.error("Usage: board-live-cards update-card --rg <dir> --card-id <card-id> [--restart]");
2141
- process.exit(1);
2384
+ throw new Error("Usage: board-live-cards update-card --rg <dir> --card-id <card-id> [--restart]");
2142
2385
  }
2143
2386
  const cardPath = lookupCardPath(dir, cardId);
2144
2387
  if (!cardPath) {
2145
- console.error(`Card "${cardId}" not found in inventory`);
2146
- process.exit(1);
2388
+ throw new Error(`Card "${cardId}" not found in inventory`);
2147
2389
  }
2148
2390
  if (!fs__namespace.existsSync(cardPath)) {
2149
- console.error(`Card file not found: ${cardPath}`);
2150
- process.exit(1);
2391
+ throw new Error(`Card file not found: ${cardPath}`);
2151
2392
  }
2152
2393
  const card = JSON.parse(fs__namespace.readFileSync(cardPath, "utf-8"));
2153
2394
  const taskConfig = liveCardToTaskConfig(card);
@@ -2167,6 +2408,121 @@ function cmdUpdateCard(args) {
2167
2408
  void processAccumulatedEventsInfinitePass(dir);
2168
2409
  console.log(`Card "${cardId}" updated${restart ? " (restarted)" : ""}.`);
2169
2410
  }
2411
+ function cmdUpsertCard(args) {
2412
+ const rgIdx = args.indexOf("--rg");
2413
+ const cardIdx = args.indexOf("--card");
2414
+ const globIdx = args.indexOf("--card-glob");
2415
+ const cardIdIdx = args.indexOf("--card-id");
2416
+ const restart = args.includes("--restart");
2417
+ const dir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
2418
+ const cardFile = cardIdx !== -1 ? args[cardIdx + 1] : void 0;
2419
+ const cardGlob = globIdx !== -1 ? args[globIdx + 1] : void 0;
2420
+ const requestedCardId = cardIdIdx !== -1 ? args[cardIdIdx + 1] : void 0;
2421
+ if (!dir || !cardFile && !cardGlob || cardFile && cardGlob) {
2422
+ console.error("Usage: board-live-cards upsert-card --rg <dir> (--card <card.json> | --card-glob <glob>) [--card-id <card-id>] [--restart]");
2423
+ process.exit(1);
2424
+ }
2425
+ if (cardGlob && requestedCardId) {
2426
+ console.error("Usage: --card-id may be used only with --card (single file), not with --card-glob");
2427
+ process.exit(1);
2428
+ }
2429
+ const cardFiles = cardFile ? [path__namespace.resolve(cardFile)] : resolveCardGlobMatches(cardGlob);
2430
+ if (!cardFile && cardFiles.length === 0) {
2431
+ console.error(`No card files matched glob: ${cardGlob}`);
2432
+ process.exit(1);
2433
+ }
2434
+ const idx = buildCardInventoryIndex(dir);
2435
+ const batchByCardId = /* @__PURE__ */ new Map();
2436
+ const batchByCardPath = /* @__PURE__ */ new Map();
2437
+ const plans = [];
2438
+ const logs = [];
2439
+ for (const absCardPath of cardFiles) {
2440
+ if (!fs__namespace.existsSync(absCardPath)) {
2441
+ console.error(`Card file not found: ${absCardPath}`);
2442
+ process.exit(1);
2443
+ }
2444
+ const card = JSON.parse(fs__namespace.readFileSync(absCardPath, "utf-8"));
2445
+ if (!card.id) {
2446
+ console.error(`Card JSON must have an "id" field (${absCardPath})`);
2447
+ process.exit(1);
2448
+ }
2449
+ if (requestedCardId && requestedCardId !== card.id) {
2450
+ console.error(
2451
+ `Card id mismatch: --card-id "${requestedCardId}" does not match file id "${card.id}" (${absCardPath})`
2452
+ );
2453
+ process.exit(1);
2454
+ }
2455
+ const seenPathCardId = batchByCardPath.get(absCardPath);
2456
+ if (seenPathCardId && seenPathCardId !== card.id) {
2457
+ console.error(
2458
+ `Upsert rejected: file "${absCardPath}" appears multiple times in batch with conflicting ids ("${seenPathCardId}" vs "${card.id}")`
2459
+ );
2460
+ process.exit(1);
2461
+ }
2462
+ const seenCardPath = batchByCardId.get(card.id);
2463
+ if (seenCardPath && seenCardPath !== absCardPath) {
2464
+ console.error(
2465
+ `Upsert rejected: card id "${card.id}" appears multiple times in batch with conflicting files ("${seenCardPath}" vs "${absCardPath}")`
2466
+ );
2467
+ process.exit(1);
2468
+ }
2469
+ const existingById = idx.byCardId.get(card.id);
2470
+ const existingByPath = idx.byCardPath.get(absCardPath);
2471
+ if (existingByPath && existingByPath.cardId !== card.id) {
2472
+ console.error(
2473
+ `Upsert rejected: file "${absCardPath}" is already mapped to card id "${existingByPath.cardId}", cannot remap to "${card.id}"`
2474
+ );
2475
+ process.exit(1);
2476
+ }
2477
+ if (existingById && existingById.cardFilePath !== absCardPath) {
2478
+ console.error(
2479
+ `Upsert rejected: card id "${card.id}" is already mapped to file "${existingById.cardFilePath}", cannot remap to "${absCardPath}"`
2480
+ );
2481
+ process.exit(1);
2482
+ }
2483
+ batchByCardPath.set(absCardPath, card.id);
2484
+ batchByCardId.set(card.id, absCardPath);
2485
+ plans.push({
2486
+ card,
2487
+ absCardPath,
2488
+ isInsert: !existingById
2489
+ });
2490
+ }
2491
+ for (const plan of plans) {
2492
+ const { card, absCardPath, isInsert } = plan;
2493
+ if (isInsert) {
2494
+ const newEntry = {
2495
+ cardId: card.id,
2496
+ cardFilePath: absCardPath,
2497
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
2498
+ };
2499
+ appendCardInventory(dir, newEntry);
2500
+ idx.byCardId.set(card.id, newEntry);
2501
+ idx.byCardPath.set(absCardPath, newEntry);
2502
+ }
2503
+ const taskConfig = liveCardToTaskConfig(card);
2504
+ appendEventToJournal(dir, {
2505
+ type: "task-upsert",
2506
+ taskName: card.id,
2507
+ taskConfig,
2508
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2509
+ });
2510
+ if (restart) {
2511
+ appendEventToJournal(dir, {
2512
+ type: "task-restart",
2513
+ taskName: card.id,
2514
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2515
+ });
2516
+ }
2517
+ logs.push(`Card "${card.id}" ${isInsert ? "upserted (inserted)" : "upserted (updated)"}${restart ? " (restarted)" : ""}.`);
2518
+ }
2519
+ void processAccumulatedEventsInfinitePass(dir);
2520
+ if (cardGlob) {
2521
+ console.log(`Upserted ${cardFiles.length} cards from glob: ${cardGlob}${restart ? " (restarted)" : ""}`);
2522
+ } else {
2523
+ console.log(logs[0]);
2524
+ }
2525
+ }
2170
2526
  async function cmdTryDrain(args) {
2171
2527
  const rgIdx = args.indexOf("--rg");
2172
2528
  const inlineLoop = args.includes("--inline-loop");
@@ -2210,6 +2566,8 @@ async function cli(argv) {
2210
2566
  return cmdAddCards(rest);
2211
2567
  case "update-card":
2212
2568
  return cmdUpdateCard(rest);
2569
+ case "upsert-card":
2570
+ return cmdUpsertCard(rest);
2213
2571
  case "remove-card":
2214
2572
  return cmdRemoveCard(rest);
2215
2573
  case "retrigger":
@@ -2229,9 +2587,7 @@ async function cli(argv) {
2229
2587
  case "process-accumulated-events":
2230
2588
  return await cmdTryDrain(rest);
2231
2589
  default:
2232
- console.error(`Unknown command: ${cmd ?? "(none)"}`);
2233
- console.error("Run: board-live-cards help");
2234
- process.exit(1);
2590
+ throw new Error(`Unknown command: ${cmd ?? "(none)"}`);
2235
2591
  }
2236
2592
  }
2237
2593
  function cmdHelp() {
@@ -2242,14 +2598,18 @@ USAGE
2242
2598
  board-live-cards-cli <command> [options]
2243
2599
 
2244
2600
  BOARD MANAGEMENT
2245
- init <dir> [--task-executor <script>]
2601
+ init <dir> [--task-executor <script>] [--runtime-out <dir>]
2246
2602
  Create a new board in <dir>.
2247
2603
  If --task-executor is given, writes <dir>/.task-executor with the script path.
2604
+ Writes <dir>/.runtime-out (default: <dir>/runtime-out).
2605
+ Published runtime files:
2606
+ <runtime-out>/board-livegraph-status.json
2607
+ <runtime-out>/cards/<card-id>.computed.json
2248
2608
  Re-running init on an existing board is safe; --task-executor updates the registration.
2249
2609
 
2250
2610
  status --rg <dir> [--json]
2251
- Print the current task status of every card in the board.
2252
- --json emits a stable machine-readable status object.
2611
+ Read and print the published status snapshot from <runtime-out>/board-livegraph-status.json.
2612
+ --json emits the stable machine-readable status object.
2253
2613
 
2254
2614
  CARD MANAGEMENT
2255
2615
  add-cards --rg <dir> (--card <card.json> | --card-glob <glob>)
@@ -2262,6 +2622,16 @@ CARD MANAGEMENT
2262
2622
  Re-read the card JSON from disk and patch the board.
2263
2623
  --restart clears the task so it re-triggers from scratch.
2264
2624
 
2625
+ upsert-card --rg <dir> (--card <card.json> | --card-glob <glob>) [--card-id <card-id>] [--restart]
2626
+ Insert or update one or many cards.
2627
+ Enforces strict one-to-one mapping between card id and file path:
2628
+ - same id + same file path: update
2629
+ - new id + new file path: insert
2630
+ - id remap or file remap: rejected
2631
+ If --card-id is provided, it must match the id inside the file.
2632
+ --card-id is valid only with --card (single file), not with --card-glob.
2633
+ --restart clears the task so it re-triggers from scratch.
2634
+
2265
2635
  remove-card --rg <dir> --id <card-id>
2266
2636
  Remove a card and its task from the board.
2267
2637
 
@@ -2359,6 +2729,7 @@ if (isMain) {
2359
2729
  exports.BoardJournal = BoardJournal;
2360
2730
  exports.appendCardInventory = appendCardInventory;
2361
2731
  exports.appendEventToJournal = appendEventToJournal;
2732
+ exports.buildCardInventoryIndex = buildCardInventoryIndex;
2362
2733
  exports.cli = cli;
2363
2734
  exports.createBoardReactiveGraph = createBoardReactiveGraph;
2364
2735
  exports.decodeSourceToken = decodeSourceToken;