yaml-flow 3.1.1 → 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.
- package/README.md +81 -20
- package/board-live-cards-cli.js +37 -0
- package/browser/board-livegraph-runtime.js +1453 -0
- package/browser/board-livegraph-runtime.js.map +1 -0
- package/browser/card-compute.js +153 -433
- package/browser/live-cards.js +868 -115
- package/browser/live-cards.schema.json +90 -83
- package/dist/board-livegraph-runtime/index.cjs +1448 -0
- package/dist/board-livegraph-runtime/index.cjs.map +1 -0
- package/dist/board-livegraph-runtime/index.d.cts +101 -0
- package/dist/board-livegraph-runtime/index.d.ts +101 -0
- package/dist/board-livegraph-runtime/index.js +1441 -0
- package/dist/board-livegraph-runtime/index.js.map +1 -0
- package/dist/card-compute/index.cjs +266 -431
- package/dist/card-compute/index.cjs.map +1 -1
- package/dist/card-compute/index.d.cts +77 -49
- package/dist/card-compute/index.d.ts +77 -49
- package/dist/card-compute/index.js +263 -432
- package/dist/card-compute/index.js.map +1 -1
- package/dist/cli/board-live-cards-cli.cjs +2750 -0
- package/dist/cli/board-live-cards-cli.cjs.map +1 -0
- package/dist/cli/board-live-cards-cli.d.cts +205 -0
- package/dist/cli/board-live-cards-cli.d.ts +205 -0
- package/dist/cli/board-live-cards-cli.js +2702 -0
- package/dist/cli/board-live-cards-cli.js.map +1 -0
- package/dist/{constants-B2zqu10b.d.ts → constants-DuzE5n03.d.ts} +2 -2
- package/dist/{constants-DJZU1pwJ.d.cts → constants-ozjf1Ejw.d.cts} +2 -2
- package/dist/continuous-event-graph/index.cjs +258 -464
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.d.cts +18 -358
- package/dist/continuous-event-graph/index.d.ts +18 -358
- package/dist/continuous-event-graph/index.js +255 -464
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/event-graph/index.cjs +4 -4
- package/dist/event-graph/index.cjs.map +1 -1
- package/dist/event-graph/index.d.cts +5 -5
- package/dist/event-graph/index.d.ts +5 -5
- package/dist/event-graph/index.js +4 -4
- package/dist/event-graph/index.js.map +1 -1
- package/dist/index.cjs +1684 -555
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -7
- package/dist/index.d.ts +26 -7
- package/dist/index.js +1678 -555
- package/dist/index.js.map +1 -1
- package/dist/inference/index.cjs +138 -19
- package/dist/inference/index.cjs.map +1 -1
- package/dist/inference/index.d.cts +2 -2
- package/dist/inference/index.d.ts +2 -2
- package/dist/inference/index.js +138 -19
- package/dist/inference/index.js.map +1 -1
- package/dist/journal-DRfJiheM.d.cts +28 -0
- package/dist/journal-NLYuqege.d.ts +28 -0
- package/dist/live-cards-bridge-Or7fdEJV.d.ts +316 -0
- package/dist/live-cards-bridge-vGJ6tMzN.d.cts +316 -0
- package/dist/schedule-CMcZe5Ny.d.ts +21 -0
- package/dist/schedule-CiucyCan.d.cts +21 -0
- package/dist/step-machine/index.cjs +18 -1
- package/dist/step-machine/index.cjs.map +1 -1
- package/dist/step-machine/index.d.cts +2 -2
- package/dist/step-machine/index.d.ts +2 -2
- package/dist/step-machine/index.js +18 -1
- package/dist/step-machine/index.js.map +1 -1
- package/dist/stores/file.d.cts +1 -1
- package/dist/stores/file.d.ts +1 -1
- package/dist/stores/index.d.cts +1 -1
- package/dist/stores/index.d.ts +1 -1
- package/dist/stores/localStorage.d.cts +1 -1
- package/dist/stores/localStorage.d.ts +1 -1
- package/dist/stores/memory.d.cts +1 -1
- package/dist/stores/memory.d.ts +1 -1
- package/dist/{types-BwvgvlOO.d.cts → types-BzLD8bjb.d.cts} +1 -1
- package/dist/{types-ClRA8hzC.d.ts → types-C2eJ7DAV.d.ts} +1 -1
- package/dist/{types-DEj7OakX.d.cts → types-CMFSIjpc.d.cts} +39 -4
- package/dist/{types-DEj7OakX.d.ts → types-CMFSIjpc.d.ts} +39 -4
- package/dist/{types-FZ_eyErS.d.cts → types-ycun84cq.d.cts} +1 -0
- package/dist/{types-FZ_eyErS.d.ts → types-ycun84cq.d.ts} +1 -0
- package/dist/{validate-DEZ2Ymdb.d.ts → validate-DJQTQ6bP.d.ts} +1 -1
- package/dist/{validate-DqKTZg_o.d.cts → validate-ke92Cleg.d.cts} +1 -1
- package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +22 -0
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +16 -0
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +15 -0
- package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +15 -0
- package/examples/browser/boards/portfolio-tracker/fetch-prices.js +43 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.bat +7 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +217 -0
- package/examples/browser/livecards-browser/index.html +41 -0
- package/examples/browser/{index.html → step-machine-browser/index.html} +53 -53
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +22 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +43 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +15 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +15 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/fetch-prices.js +48 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +58 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +27 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +25 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +29 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +27 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +25 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +37 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +53 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +35 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +227 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +38 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +29 -0
- package/examples/cli/step-machine-demo/jsonata-init-board-cli.js +36 -0
- package/examples/cli/step-machine-demo/jsonata-init-board.flow.yaml +30 -0
- package/examples/cli/step-machine-demo/one-step-cli-only.flow.yaml +19 -0
- package/examples/cli/step-machine-demo/step-cli-echo-y.js +15 -0
- package/examples/cli/step-machine-demo/step2-double-cli.js +39 -0
- package/examples/cli/step-machine-demo/two-step-math-handlers.js +32 -0
- package/examples/cli/step-machine-demo/two-step-math.flow.yaml +31 -0
- package/examples/cli/step-machine-demo/two-step-mixed-handlers.js +24 -0
- package/examples/cli/step-machine-demo/two-step-mixed.flow.yaml +35 -0
- package/examples/example-board/board.yaml +23 -0
- package/examples/example-board/bootstrap_payload.json +1 -0
- package/examples/example-board/cards/card-chain-region-alert.json +39 -0
- package/examples/example-board/cards/card-chain-region-totals.json +26 -0
- package/examples/example-board/cards/card-chain-top-region.json +24 -0
- package/examples/example-board/cards/card-ex-actions.json +32 -0
- package/examples/example-board/cards/card-ex-chart.json +30 -0
- package/examples/example-board/cards/card-ex-filter.json +36 -0
- package/examples/example-board/cards/card-ex-filtered-by-preference.json +59 -0
- package/examples/example-board/cards/card-ex-form.json +91 -0
- package/examples/example-board/cards/card-ex-list.json +22 -0
- package/examples/example-board/cards/card-ex-markdown.json +17 -0
- package/examples/example-board/cards/card-ex-metric.json +19 -0
- package/examples/example-board/cards/card-ex-narrative.json +36 -0
- package/examples/example-board/cards/card-ex-source-http.json +28 -0
- package/examples/example-board/cards/card-ex-source.json +21 -0
- package/examples/example-board/cards/card-ex-status.json +35 -0
- package/examples/example-board/cards/card-ex-table.json +30 -0
- package/examples/example-board/cards/card-ex-todo.json +29 -0
- package/examples/example-board/demo-chat-handler.js +69 -0
- package/examples/example-board/demo-server.js +87 -0
- package/examples/example-board/demo-shell-browser.html +806 -0
- package/examples/example-board/demo-shell-with-server.html +280 -0
- package/examples/example-board/demo-shell.html +62 -0
- package/examples/example-board/demo-task-executor.js +255 -0
- package/examples/example-board/mock.db +15 -0
- package/examples/example-board/reusable-board-runtime-client.js +265 -0
- package/examples/example-board/reusable-runtime-artifacts-adapter.js +233 -0
- package/examples/example-board/reusable-server-runtime.js +1284 -0
- package/examples/index.html +799 -0
- package/examples/{batch → npm-libs/batch}/batch-step-machine.ts +1 -1
- package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/live-cards-board.ts +18 -18
- package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/live-portfolio-dashboard.ts +24 -24
- package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/portfolio-tracker.ts +1 -1
- package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/reactive-monitoring.ts +1 -1
- package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/reactive-pipeline.ts +1 -1
- package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/soc-incident-board.ts +1 -1
- package/examples/{continuous-event-graph → npm-libs/continuous-event-graph}/stock-dashboard.ts +1 -1
- package/examples/{event-graph → npm-libs/event-graph}/ci-cd-pipeline.ts +1 -1
- package/examples/{event-graph → npm-libs/event-graph}/executor-diamond.ts +1 -1
- package/examples/{event-graph → npm-libs/event-graph}/executor-pipeline.ts +1 -1
- package/examples/{event-graph → npm-libs/event-graph}/research-pipeline.ts +1 -1
- package/examples/{graph-of-graphs → npm-libs/graph-of-graphs}/multi-stage-etl.ts +1 -1
- package/examples/{graph-of-graphs → npm-libs/graph-of-graphs}/url-processing-pipeline.ts +1 -1
- package/examples/{inference → npm-libs/inference}/azure-deployment.ts +1 -1
- package/examples/{inference → npm-libs/inference}/copilot-cli.ts +1 -1
- package/examples/{inference → npm-libs/inference}/data-pipeline.ts +1 -1
- package/examples/{inference → npm-libs/inference}/pluggable-adapters.ts +1 -1
- package/examples/{node → npm-libs/node}/ai-conversation.ts +1 -1
- package/examples/{node → npm-libs/node}/simple-greeting.ts +2 -2
- package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +22 -0
- package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +43 -0
- package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +15 -0
- package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +15 -0
- package/examples/step-machine-cli/portfolio-tracker/fetch-prices.js +48 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +58 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +27 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +25 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +29 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +27 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/status-cli.js +25 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +37 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +53 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +35 -0
- package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
- package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +227 -0
- package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +38 -0
- package/examples/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +29 -0
- package/package.json +27 -2
- package/schema/board-status.schema.json +118 -0
- package/schema/card-runtime.schema.json +25 -0
- package/schema/flow.schema.json +5 -0
- package/schema/live-cards.schema.json +90 -83
- package/step-machine-cli.js +674 -0
- package/browser/ingest-board.js +0 -296
- package/examples/ingest.js +0 -733
- /package/examples/{flows → npm-libs/flows}/ai-conversation.yaml +0 -0
- /package/examples/{flows → npm-libs/flows}/order-processing.yaml +0 -0
- /package/examples/{flows → npm-libs/flows}/simple-greeting.yaml +0 -0
|
@@ -0,0 +1,2702 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { execFileSync, spawn, execFile } from 'child_process';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import fg from 'fast-glob';
|
|
8
|
+
import { lockSync } from 'proper-lockfile';
|
|
9
|
+
import jsonata2 from 'jsonata';
|
|
10
|
+
import 'ajv-formats';
|
|
11
|
+
|
|
12
|
+
// src/cli/board-live-cards-cli.ts
|
|
13
|
+
|
|
14
|
+
// src/event-graph/constants.ts
|
|
15
|
+
var TASK_STATUS = {
|
|
16
|
+
RUNNING: "running",
|
|
17
|
+
COMPLETED: "completed",
|
|
18
|
+
FAILED: "failed",
|
|
19
|
+
INACTIVATED: "inactivated"
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/event-graph/graph-helpers.ts
|
|
23
|
+
function getProvides(task) {
|
|
24
|
+
if (!task) return [];
|
|
25
|
+
if (Array.isArray(task.provides)) return task.provides;
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
function getRequires(task) {
|
|
29
|
+
if (!task) return [];
|
|
30
|
+
if (Array.isArray(task.requires)) return task.requires;
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
function getAllTasks(graph) {
|
|
34
|
+
return graph.tasks ?? {};
|
|
35
|
+
}
|
|
36
|
+
function isNonActiveTask(taskState) {
|
|
37
|
+
if (!taskState) return false;
|
|
38
|
+
return taskState.status === TASK_STATUS.FAILED || taskState.status === TASK_STATUS.INACTIVATED;
|
|
39
|
+
}
|
|
40
|
+
function getRefreshStrategy(taskConfig, graphSettings) {
|
|
41
|
+
return taskConfig.refreshStrategy ?? graphSettings?.refreshStrategy ?? "data-changed";
|
|
42
|
+
}
|
|
43
|
+
function getMaxExecutions(taskConfig) {
|
|
44
|
+
return taskConfig.maxExecutions;
|
|
45
|
+
}
|
|
46
|
+
function computeAvailableOutputs(graph, taskStates) {
|
|
47
|
+
const outputs = /* @__PURE__ */ new Set();
|
|
48
|
+
for (const [taskName, taskState] of Object.entries(taskStates)) {
|
|
49
|
+
if (taskState.status === TASK_STATUS.COMPLETED) {
|
|
50
|
+
const taskConfig = graph.tasks[taskName];
|
|
51
|
+
if (taskConfig) {
|
|
52
|
+
const provides = getProvides(taskConfig);
|
|
53
|
+
provides.forEach((output) => outputs.add(output));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return Array.from(outputs);
|
|
58
|
+
}
|
|
59
|
+
function groupTasksByProvides(candidateTaskNames, tasks) {
|
|
60
|
+
const outputGroups = {};
|
|
61
|
+
candidateTaskNames.forEach((taskName) => {
|
|
62
|
+
const task = tasks[taskName];
|
|
63
|
+
if (!task) return;
|
|
64
|
+
const provides = getProvides(task);
|
|
65
|
+
provides.forEach((output) => {
|
|
66
|
+
if (!outputGroups[output]) {
|
|
67
|
+
outputGroups[output] = [];
|
|
68
|
+
}
|
|
69
|
+
outputGroups[output].push(taskName);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
return outputGroups;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/event-graph/task-transitions.ts
|
|
76
|
+
function applyTaskStart(state, taskName) {
|
|
77
|
+
const existingTask = state.tasks[taskName] ?? createDefaultGraphEngineStore();
|
|
78
|
+
const updatedTask = {
|
|
79
|
+
...existingTask,
|
|
80
|
+
status: "running",
|
|
81
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
82
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
83
|
+
progress: 0,
|
|
84
|
+
error: void 0
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
...state,
|
|
88
|
+
tasks: { ...state.tasks, [taskName]: updatedTask },
|
|
89
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function applyTaskCompletion(state, graph, taskName, result, dataHash, data) {
|
|
93
|
+
const existingTask = state.tasks[taskName] ?? createDefaultGraphEngineStore();
|
|
94
|
+
const taskConfig = graph.tasks[taskName];
|
|
95
|
+
if (!taskConfig) {
|
|
96
|
+
throw new Error(`Task "${taskName}" not found in graph`);
|
|
97
|
+
}
|
|
98
|
+
let outputTokens;
|
|
99
|
+
if (result && taskConfig.on && taskConfig.on[result]) {
|
|
100
|
+
outputTokens = taskConfig.on[result];
|
|
101
|
+
} else {
|
|
102
|
+
outputTokens = getProvides(taskConfig);
|
|
103
|
+
}
|
|
104
|
+
const lastConsumedHashes = { ...existingTask.lastConsumedHashes };
|
|
105
|
+
const requires = taskConfig.requires ?? [];
|
|
106
|
+
for (const token of requires) {
|
|
107
|
+
for (const [otherName, otherConfig] of Object.entries(graph.tasks)) {
|
|
108
|
+
if (getProvides(otherConfig).includes(token)) {
|
|
109
|
+
const otherState = state.tasks[otherName];
|
|
110
|
+
if (otherState?.lastDataHash) {
|
|
111
|
+
lastConsumedHashes[token] = otherState.lastDataHash;
|
|
112
|
+
}
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const updatedTask = {
|
|
118
|
+
...existingTask,
|
|
119
|
+
status: "completed",
|
|
120
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
121
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
122
|
+
executionCount: existingTask.executionCount + 1,
|
|
123
|
+
lastEpoch: existingTask.executionCount + 1,
|
|
124
|
+
lastDataHash: dataHash,
|
|
125
|
+
data,
|
|
126
|
+
lastConsumedHashes,
|
|
127
|
+
error: void 0
|
|
128
|
+
};
|
|
129
|
+
const newOutputs = [.../* @__PURE__ */ new Set([...state.availableOutputs, ...outputTokens])];
|
|
130
|
+
return {
|
|
131
|
+
...state,
|
|
132
|
+
tasks: { ...state.tasks, [taskName]: updatedTask },
|
|
133
|
+
availableOutputs: newOutputs,
|
|
134
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function applyTaskFailure(state, graph, taskName, error) {
|
|
138
|
+
const existingTask = state.tasks[taskName] ?? createDefaultGraphEngineStore();
|
|
139
|
+
const taskConfig = graph.tasks[taskName];
|
|
140
|
+
if (taskConfig?.retry) {
|
|
141
|
+
const retryCount = existingTask.retryCount + 1;
|
|
142
|
+
if (retryCount <= taskConfig.retry.max_attempts) {
|
|
143
|
+
const updatedTask2 = {
|
|
144
|
+
...existingTask,
|
|
145
|
+
status: "not-started",
|
|
146
|
+
retryCount,
|
|
147
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
148
|
+
error
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
...state,
|
|
152
|
+
tasks: { ...state.tasks, [taskName]: updatedTask2 },
|
|
153
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const updatedTask = {
|
|
158
|
+
...existingTask,
|
|
159
|
+
status: "failed",
|
|
160
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
161
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
162
|
+
error,
|
|
163
|
+
executionCount: existingTask.executionCount + 1
|
|
164
|
+
};
|
|
165
|
+
let newOutputs = state.availableOutputs;
|
|
166
|
+
if (taskConfig?.on_failure && taskConfig.on_failure.length > 0) {
|
|
167
|
+
newOutputs = [.../* @__PURE__ */ new Set([...state.availableOutputs, ...taskConfig.on_failure])];
|
|
168
|
+
}
|
|
169
|
+
if (taskConfig?.circuit_breaker && updatedTask.executionCount >= taskConfig.circuit_breaker.max_executions) {
|
|
170
|
+
const breakTokens = taskConfig.circuit_breaker.on_break;
|
|
171
|
+
newOutputs = [.../* @__PURE__ */ new Set([...newOutputs, ...breakTokens])];
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
...state,
|
|
175
|
+
tasks: { ...state.tasks, [taskName]: updatedTask },
|
|
176
|
+
availableOutputs: newOutputs,
|
|
177
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function applyTaskProgress(state, taskName, message, progress) {
|
|
181
|
+
const existingTask = state.tasks[taskName] ?? createDefaultGraphEngineStore();
|
|
182
|
+
const updatedTask = {
|
|
183
|
+
...existingTask,
|
|
184
|
+
progress: typeof progress === "number" ? progress : existingTask.progress,
|
|
185
|
+
messages: [
|
|
186
|
+
...existingTask.messages ?? [],
|
|
187
|
+
...message ? [{ message, timestamp: (/* @__PURE__ */ new Date()).toISOString(), status: existingTask.status }] : []
|
|
188
|
+
],
|
|
189
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
190
|
+
};
|
|
191
|
+
return {
|
|
192
|
+
...state,
|
|
193
|
+
tasks: { ...state.tasks, [taskName]: updatedTask },
|
|
194
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function applyTaskRestart(state, taskName) {
|
|
198
|
+
const existingTask = state.tasks[taskName];
|
|
199
|
+
if (!existingTask) return state;
|
|
200
|
+
const updatedTask = {
|
|
201
|
+
...existingTask,
|
|
202
|
+
status: "not-started",
|
|
203
|
+
startedAt: void 0,
|
|
204
|
+
completedAt: void 0,
|
|
205
|
+
failedAt: void 0,
|
|
206
|
+
error: void 0,
|
|
207
|
+
data: void 0,
|
|
208
|
+
progress: null,
|
|
209
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
...state,
|
|
213
|
+
tasks: { ...state.tasks, [taskName]: updatedTask },
|
|
214
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
function createDefaultGraphEngineStore() {
|
|
218
|
+
return {
|
|
219
|
+
status: "not-started",
|
|
220
|
+
executionCount: 0,
|
|
221
|
+
retryCount: 0,
|
|
222
|
+
lastEpoch: 0,
|
|
223
|
+
messages: [],
|
|
224
|
+
progress: null
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/continuous-event-graph/core.ts
|
|
229
|
+
function createLiveGraph(config, executionId) {
|
|
230
|
+
const id = `live-${Date.now()}`;
|
|
231
|
+
const tasks = {};
|
|
232
|
+
for (const taskName of Object.keys(config.tasks)) {
|
|
233
|
+
tasks[taskName] = createDefaultGraphEngineStore2();
|
|
234
|
+
}
|
|
235
|
+
const state = {
|
|
236
|
+
status: "running",
|
|
237
|
+
tasks,
|
|
238
|
+
availableOutputs: [],
|
|
239
|
+
stuckDetection: { is_stuck: false, stuck_description: null, outputs_unresolvable: [], tasks_blocked: [] },
|
|
240
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
241
|
+
executionId: id,
|
|
242
|
+
executionConfig: {
|
|
243
|
+
executionMode: config.settings.execution_mode ?? "eligibility-mode",
|
|
244
|
+
conflictStrategy: config.settings.conflict_strategy ?? "alphabetical",
|
|
245
|
+
completionStrategy: config.settings.completion
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
return { config, state };
|
|
249
|
+
}
|
|
250
|
+
function applyEvent(live, event) {
|
|
251
|
+
const { config, state } = live;
|
|
252
|
+
if ("executionId" in event && event.executionId && event.executionId !== state.executionId) {
|
|
253
|
+
return live;
|
|
254
|
+
}
|
|
255
|
+
switch (event.type) {
|
|
256
|
+
// --- Execution state transitions ---
|
|
257
|
+
case "task-started":
|
|
258
|
+
return { config, state: applyTaskStart(state, event.taskName) };
|
|
259
|
+
case "task-completed":
|
|
260
|
+
return { config, state: applyTaskCompletion(state, config, event.taskName, event.result, event.dataHash, event.data) };
|
|
261
|
+
case "task-failed":
|
|
262
|
+
return { config, state: applyTaskFailure(state, config, event.taskName, event.error) };
|
|
263
|
+
case "task-progress":
|
|
264
|
+
return { config, state: applyTaskProgress(state, event.taskName, event.message, event.progress) };
|
|
265
|
+
case "task-restart":
|
|
266
|
+
return { config, state: applyTaskRestart(state, event.taskName) };
|
|
267
|
+
case "inject-tokens":
|
|
268
|
+
return {
|
|
269
|
+
config,
|
|
270
|
+
state: {
|
|
271
|
+
...state,
|
|
272
|
+
availableOutputs: [.../* @__PURE__ */ new Set([...state.availableOutputs, ...event.tokens])],
|
|
273
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
case "agent-action":
|
|
277
|
+
return { config, state: applyAgentAction(state, event.action) };
|
|
278
|
+
// --- Structural mutations ---
|
|
279
|
+
case "task-upsert":
|
|
280
|
+
return addNode(live, event.taskName, event.taskConfig);
|
|
281
|
+
case "task-removal":
|
|
282
|
+
return removeNode(live, event.taskName);
|
|
283
|
+
case "node-requires-add":
|
|
284
|
+
return addRequires(live, event.nodeName, event.tokens);
|
|
285
|
+
case "node-requires-remove":
|
|
286
|
+
return removeRequires(live, event.nodeName, event.tokens);
|
|
287
|
+
case "node-provides-add":
|
|
288
|
+
return addProvides(live, event.nodeName, event.tokens);
|
|
289
|
+
case "node-provides-remove":
|
|
290
|
+
return removeProvides(live, event.nodeName, event.tokens);
|
|
291
|
+
default:
|
|
292
|
+
return live;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function applyEvents(live, events) {
|
|
296
|
+
return events.reduce((current, event) => applyEvent(current, event), live);
|
|
297
|
+
}
|
|
298
|
+
function addNode(live, name, taskConfig) {
|
|
299
|
+
const exists = !!live.config.tasks[name];
|
|
300
|
+
return {
|
|
301
|
+
config: {
|
|
302
|
+
...live.config,
|
|
303
|
+
tasks: { ...live.config.tasks, [name]: taskConfig }
|
|
304
|
+
},
|
|
305
|
+
state: {
|
|
306
|
+
...live.state,
|
|
307
|
+
tasks: {
|
|
308
|
+
...live.state.tasks,
|
|
309
|
+
[name]: exists ? live.state.tasks[name] : createDefaultGraphEngineStore2()
|
|
310
|
+
},
|
|
311
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function removeNode(live, name) {
|
|
316
|
+
if (!live.config.tasks[name]) return live;
|
|
317
|
+
const { [name]: _removedConfig, ...remainingTasks } = live.config.tasks;
|
|
318
|
+
const { [name]: _removedState, ...remainingStates } = live.state.tasks;
|
|
319
|
+
return {
|
|
320
|
+
config: {
|
|
321
|
+
...live.config,
|
|
322
|
+
tasks: remainingTasks
|
|
323
|
+
},
|
|
324
|
+
state: {
|
|
325
|
+
...live.state,
|
|
326
|
+
tasks: remainingStates,
|
|
327
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function addRequires(live, nodeName, tokens) {
|
|
332
|
+
const task = live.config.tasks[nodeName];
|
|
333
|
+
if (!task) return live;
|
|
334
|
+
const current = getRequires(task);
|
|
335
|
+
const toAdd = tokens.filter((t) => !current.includes(t));
|
|
336
|
+
if (toAdd.length === 0) return live;
|
|
337
|
+
return {
|
|
338
|
+
config: {
|
|
339
|
+
...live.config,
|
|
340
|
+
tasks: {
|
|
341
|
+
...live.config.tasks,
|
|
342
|
+
[nodeName]: { ...task, requires: [...current, ...toAdd] }
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
state: live.state
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function removeRequires(live, nodeName, tokens) {
|
|
349
|
+
const task = live.config.tasks[nodeName];
|
|
350
|
+
if (!task) return live;
|
|
351
|
+
const current = getRequires(task);
|
|
352
|
+
const remaining = current.filter((t) => !tokens.includes(t));
|
|
353
|
+
if (remaining.length === current.length) return live;
|
|
354
|
+
return {
|
|
355
|
+
config: {
|
|
356
|
+
...live.config,
|
|
357
|
+
tasks: {
|
|
358
|
+
...live.config.tasks,
|
|
359
|
+
[nodeName]: { ...task, requires: remaining }
|
|
360
|
+
}
|
|
361
|
+
},
|
|
362
|
+
state: live.state
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
function addProvides(live, nodeName, tokens) {
|
|
366
|
+
const task = live.config.tasks[nodeName];
|
|
367
|
+
if (!task) return live;
|
|
368
|
+
const current = getProvides(task);
|
|
369
|
+
const toAdd = tokens.filter((t) => !current.includes(t));
|
|
370
|
+
if (toAdd.length === 0) return live;
|
|
371
|
+
return {
|
|
372
|
+
config: {
|
|
373
|
+
...live.config,
|
|
374
|
+
tasks: {
|
|
375
|
+
...live.config.tasks,
|
|
376
|
+
[nodeName]: { ...task, provides: [...current, ...toAdd] }
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
state: live.state
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function removeProvides(live, nodeName, tokens) {
|
|
383
|
+
const task = live.config.tasks[nodeName];
|
|
384
|
+
if (!task) return live;
|
|
385
|
+
const current = getProvides(task);
|
|
386
|
+
const remaining = current.filter((t) => !tokens.includes(t));
|
|
387
|
+
if (remaining.length === current.length) return live;
|
|
388
|
+
return {
|
|
389
|
+
config: {
|
|
390
|
+
...live.config,
|
|
391
|
+
tasks: {
|
|
392
|
+
...live.config.tasks,
|
|
393
|
+
[nodeName]: { ...task, provides: remaining }
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
state: live.state
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function snapshot(live) {
|
|
400
|
+
return {
|
|
401
|
+
version: 1,
|
|
402
|
+
config: live.config,
|
|
403
|
+
state: live.state,
|
|
404
|
+
snapshotAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
function restore(data) {
|
|
408
|
+
if (!data || typeof data !== "object") {
|
|
409
|
+
throw new Error("Invalid snapshot: expected an object");
|
|
410
|
+
}
|
|
411
|
+
const snap = data;
|
|
412
|
+
if (!snap.config || typeof snap.config !== "object") {
|
|
413
|
+
throw new Error('Invalid snapshot: missing or invalid "config"');
|
|
414
|
+
}
|
|
415
|
+
if (!snap.state || typeof snap.state !== "object") {
|
|
416
|
+
throw new Error('Invalid snapshot: missing or invalid "state"');
|
|
417
|
+
}
|
|
418
|
+
const config = snap.config;
|
|
419
|
+
const state = snap.state;
|
|
420
|
+
if (!config.settings || typeof config.settings !== "object") {
|
|
421
|
+
throw new Error("Invalid snapshot: config.settings missing");
|
|
422
|
+
}
|
|
423
|
+
if (!config.tasks || typeof config.tasks !== "object") {
|
|
424
|
+
throw new Error("Invalid snapshot: config.tasks missing");
|
|
425
|
+
}
|
|
426
|
+
if (!state.tasks || typeof state.tasks !== "object") {
|
|
427
|
+
throw new Error("Invalid snapshot: state.tasks missing");
|
|
428
|
+
}
|
|
429
|
+
if (!Array.isArray(state.availableOutputs)) {
|
|
430
|
+
throw new Error("Invalid snapshot: state.availableOutputs must be an array");
|
|
431
|
+
}
|
|
432
|
+
return { config, state };
|
|
433
|
+
}
|
|
434
|
+
function createDefaultGraphEngineStore2() {
|
|
435
|
+
return {
|
|
436
|
+
status: "not-started",
|
|
437
|
+
executionCount: 0,
|
|
438
|
+
retryCount: 0,
|
|
439
|
+
lastEpoch: 0,
|
|
440
|
+
messages: [],
|
|
441
|
+
progress: null
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function applyAgentAction(state, action) {
|
|
445
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
446
|
+
switch (action) {
|
|
447
|
+
case "stop":
|
|
448
|
+
return { ...state, status: "stopped", lastUpdated: now };
|
|
449
|
+
case "pause":
|
|
450
|
+
return { ...state, status: "paused", lastUpdated: now };
|
|
451
|
+
case "resume":
|
|
452
|
+
return { ...state, status: "running", lastUpdated: now };
|
|
453
|
+
default:
|
|
454
|
+
return state;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/continuous-event-graph/schedule.ts
|
|
459
|
+
function schedule(live) {
|
|
460
|
+
const { config, state } = live;
|
|
461
|
+
const graphTasks = getAllTasks(config);
|
|
462
|
+
const taskNames = Object.keys(graphTasks);
|
|
463
|
+
if (taskNames.length === 0) {
|
|
464
|
+
return { eligible: [], pending: [], unresolved: [], blocked: [], conflicts: {} };
|
|
465
|
+
}
|
|
466
|
+
const producerMap = buildProducerMap(graphTasks);
|
|
467
|
+
const computedOutputs = computeAvailableOutputs(config, state.tasks);
|
|
468
|
+
const availableOutputs = /* @__PURE__ */ new Set([...computedOutputs, ...state.availableOutputs]);
|
|
469
|
+
const eligible = [];
|
|
470
|
+
const pending = [];
|
|
471
|
+
const unresolved = [];
|
|
472
|
+
const blocked = [];
|
|
473
|
+
for (const [taskName, taskConfig] of Object.entries(graphTasks)) {
|
|
474
|
+
const taskState = state.tasks[taskName];
|
|
475
|
+
const strategy = getRefreshStrategy(taskConfig, config.settings);
|
|
476
|
+
const rerunnable = strategy !== "once";
|
|
477
|
+
if (taskState?.status === TASK_STATUS.RUNNING || isNonActiveTask(taskState)) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
const maxExec = getMaxExecutions(taskConfig);
|
|
481
|
+
if (maxExec !== void 0 && taskState && taskState.executionCount >= maxExec) {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (taskConfig.circuit_breaker && taskState && taskState.executionCount >= taskConfig.circuit_breaker.max_executions) {
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (!rerunnable && taskState?.status === TASK_STATUS.COMPLETED) {
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
if (rerunnable && taskState?.status === TASK_STATUS.COMPLETED) {
|
|
491
|
+
const requires2 = getRequires(taskConfig);
|
|
492
|
+
let shouldSkip = false;
|
|
493
|
+
switch (strategy) {
|
|
494
|
+
case "data-changed": {
|
|
495
|
+
if (requires2.length > 0) {
|
|
496
|
+
const hasChangedData = requires2.some((req) => {
|
|
497
|
+
for (const [otherName, otherConfig] of Object.entries(graphTasks)) {
|
|
498
|
+
if (getProvides(otherConfig).includes(req)) {
|
|
499
|
+
const otherState = state.tasks[otherName];
|
|
500
|
+
if (!otherState) continue;
|
|
501
|
+
const consumed = taskState.lastConsumedHashes?.[req];
|
|
502
|
+
if (otherState.lastDataHash == null) {
|
|
503
|
+
return otherState.executionCount > taskState.lastEpoch;
|
|
504
|
+
}
|
|
505
|
+
return otherState.lastDataHash !== consumed;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return false;
|
|
509
|
+
});
|
|
510
|
+
if (!hasChangedData) shouldSkip = true;
|
|
511
|
+
} else {
|
|
512
|
+
shouldSkip = true;
|
|
513
|
+
}
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
case "epoch-changed": {
|
|
517
|
+
if (requires2.length > 0) {
|
|
518
|
+
const hasRefreshed = requires2.some((req) => {
|
|
519
|
+
for (const [otherName, otherConfig] of Object.entries(graphTasks)) {
|
|
520
|
+
if (getProvides(otherConfig).includes(req)) {
|
|
521
|
+
const otherState = state.tasks[otherName];
|
|
522
|
+
if (otherState && otherState.executionCount > taskState.lastEpoch) return true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
return false;
|
|
526
|
+
});
|
|
527
|
+
if (!hasRefreshed) shouldSkip = true;
|
|
528
|
+
} else {
|
|
529
|
+
shouldSkip = true;
|
|
530
|
+
}
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
case "time-based": {
|
|
534
|
+
const interval = taskConfig.refreshInterval ?? 0;
|
|
535
|
+
if (interval <= 0) {
|
|
536
|
+
shouldSkip = true;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
const completedAt = taskState.completedAt;
|
|
540
|
+
if (!completedAt) {
|
|
541
|
+
shouldSkip = true;
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
const elapsedSec = (Date.now() - Date.parse(completedAt)) / 1e3;
|
|
545
|
+
if (elapsedSec < interval) shouldSkip = true;
|
|
546
|
+
break;
|
|
547
|
+
}
|
|
548
|
+
case "manual":
|
|
549
|
+
shouldSkip = true;
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
if (shouldSkip) continue;
|
|
553
|
+
}
|
|
554
|
+
const requires = getRequires(taskConfig);
|
|
555
|
+
if (requires.length === 0) {
|
|
556
|
+
eligible.push(taskName);
|
|
557
|
+
continue;
|
|
558
|
+
}
|
|
559
|
+
const missingTokens = [];
|
|
560
|
+
const pendingTokens = [];
|
|
561
|
+
const failedTokenInfo = [];
|
|
562
|
+
for (const token of requires) {
|
|
563
|
+
if (availableOutputs.has(token)) continue;
|
|
564
|
+
const producers = producerMap[token] || [];
|
|
565
|
+
if (producers.length === 0) {
|
|
566
|
+
missingTokens.push(token);
|
|
567
|
+
} else {
|
|
568
|
+
const allFailed = producers.every((p) => isNonActiveTask(state.tasks[p]));
|
|
569
|
+
if (allFailed) {
|
|
570
|
+
failedTokenInfo.push({ token, failedProducer: producers[0] });
|
|
571
|
+
} else {
|
|
572
|
+
pendingTokens.push(token);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (missingTokens.length > 0) {
|
|
577
|
+
unresolved.push({ taskName, missingTokens });
|
|
578
|
+
} else if (failedTokenInfo.length > 0) {
|
|
579
|
+
blocked.push({
|
|
580
|
+
taskName,
|
|
581
|
+
failedTokens: failedTokenInfo.map((f) => f.token),
|
|
582
|
+
failedProducers: [...new Set(failedTokenInfo.map((f) => f.failedProducer))]
|
|
583
|
+
});
|
|
584
|
+
} else if (pendingTokens.length > 0) {
|
|
585
|
+
pending.push({ taskName, waitingOn: pendingTokens });
|
|
586
|
+
} else {
|
|
587
|
+
eligible.push(taskName);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const conflicts = {};
|
|
591
|
+
if (eligible.length > 1) {
|
|
592
|
+
const outputGroups = groupTasksByProvides(eligible, graphTasks);
|
|
593
|
+
for (const [outputKey, groupTasks] of Object.entries(outputGroups)) {
|
|
594
|
+
if (groupTasks.length > 1) {
|
|
595
|
+
conflicts[outputKey] = groupTasks;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return { eligible, pending, unresolved, blocked, conflicts };
|
|
600
|
+
}
|
|
601
|
+
function buildProducerMap(tasks) {
|
|
602
|
+
const map = {};
|
|
603
|
+
for (const [name, config] of Object.entries(tasks)) {
|
|
604
|
+
for (const token of getProvides(config)) {
|
|
605
|
+
if (!map[token]) map[token] = [];
|
|
606
|
+
map[token].push(name);
|
|
607
|
+
}
|
|
608
|
+
if (config.on) {
|
|
609
|
+
for (const tokens of Object.values(config.on)) {
|
|
610
|
+
for (const token of tokens) {
|
|
611
|
+
if (!map[token]) map[token] = [];
|
|
612
|
+
if (!map[token].includes(name)) map[token].push(name);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
if (config.on_failure) {
|
|
617
|
+
for (const token of config.on_failure) {
|
|
618
|
+
if (!map[token]) map[token] = [];
|
|
619
|
+
if (!map[token].includes(name)) map[token].push(name);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return map;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/continuous-event-graph/journal.ts
|
|
627
|
+
var MemoryJournal = class {
|
|
628
|
+
buffer = [];
|
|
629
|
+
append(event) {
|
|
630
|
+
this.buffer.push(event);
|
|
631
|
+
}
|
|
632
|
+
drain() {
|
|
633
|
+
const events = this.buffer;
|
|
634
|
+
this.buffer = [];
|
|
635
|
+
return events;
|
|
636
|
+
}
|
|
637
|
+
get size() {
|
|
638
|
+
return this.buffer.length;
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// src/continuous-event-graph/reactive.ts
|
|
643
|
+
function computeDataHash(data) {
|
|
644
|
+
const json = stableStringify(data);
|
|
645
|
+
return fnv1a64Hex(json);
|
|
646
|
+
}
|
|
647
|
+
function stableStringify(value) {
|
|
648
|
+
if (value === null || value === void 0 || typeof value !== "object") {
|
|
649
|
+
return JSON.stringify(value);
|
|
650
|
+
}
|
|
651
|
+
if (Array.isArray(value)) {
|
|
652
|
+
return "[" + value.map(stableStringify).join(",") + "]";
|
|
653
|
+
}
|
|
654
|
+
const obj = value;
|
|
655
|
+
const keys = Object.keys(obj).sort();
|
|
656
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + stableStringify(obj[k])).join(",") + "}";
|
|
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
|
+
}
|
|
694
|
+
function encodeCallbackToken(taskName) {
|
|
695
|
+
const payload = JSON.stringify({ t: taskName, n: Date.now().toString(36) + Math.random().toString(36).slice(2, 6) });
|
|
696
|
+
return base64UrlEncode(payload);
|
|
697
|
+
}
|
|
698
|
+
function decodeCallbackToken(token) {
|
|
699
|
+
try {
|
|
700
|
+
const payload = JSON.parse(base64UrlDecode(token));
|
|
701
|
+
if (typeof payload?.t === "string") return { taskName: payload.t };
|
|
702
|
+
return null;
|
|
703
|
+
} catch {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
function createReactiveGraph(configOrLive, options, executionId) {
|
|
708
|
+
const {
|
|
709
|
+
handlers: initialHandlers,
|
|
710
|
+
onDrain
|
|
711
|
+
} = options;
|
|
712
|
+
const inputQueue = new MemoryJournal();
|
|
713
|
+
let live = "state" in configOrLive && "config" in configOrLive ? configOrLive : createLiveGraph(configOrLive);
|
|
714
|
+
let disposed = false;
|
|
715
|
+
const handlers = new Map(Object.entries(initialHandlers));
|
|
716
|
+
const internalJournal = new MemoryJournal();
|
|
717
|
+
let draining = false;
|
|
718
|
+
let drainQueued = false;
|
|
719
|
+
function drain() {
|
|
720
|
+
if (disposed) return;
|
|
721
|
+
if (draining) {
|
|
722
|
+
drainQueued = true;
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
draining = true;
|
|
726
|
+
try {
|
|
727
|
+
do {
|
|
728
|
+
drainQueued = false;
|
|
729
|
+
drainOnce();
|
|
730
|
+
} while (drainQueued);
|
|
731
|
+
} finally {
|
|
732
|
+
draining = false;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
function drainOnce() {
|
|
736
|
+
const internalEvents = internalJournal.drain();
|
|
737
|
+
const inputEvents = inputQueue.drain();
|
|
738
|
+
const events = [...internalEvents, ...inputEvents];
|
|
739
|
+
if (events.length > 0) {
|
|
740
|
+
live = applyEvents(live, events);
|
|
741
|
+
}
|
|
742
|
+
const result = schedule(live);
|
|
743
|
+
if (events.length > 0) {
|
|
744
|
+
onDrain?.(events, live, result);
|
|
745
|
+
}
|
|
746
|
+
for (const taskName of result.eligible) {
|
|
747
|
+
dispatchTask(taskName);
|
|
748
|
+
}
|
|
749
|
+
for (const event of events) {
|
|
750
|
+
if (event.type === "task-progress") {
|
|
751
|
+
const { taskName, update } = event;
|
|
752
|
+
const taskConfig = live.config.tasks[taskName];
|
|
753
|
+
if (!taskConfig) continue;
|
|
754
|
+
const taskState = live.state.tasks[taskName];
|
|
755
|
+
if (!taskState || taskState.status !== "running") continue;
|
|
756
|
+
const callbackToken = encodeCallbackToken(taskName);
|
|
757
|
+
runPipeline(taskName, callbackToken, update).catch((error) => {
|
|
758
|
+
if (disposed) return;
|
|
759
|
+
internalJournal.append({
|
|
760
|
+
type: "task-failed",
|
|
761
|
+
taskName,
|
|
762
|
+
error: error.message ?? String(error),
|
|
763
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
764
|
+
});
|
|
765
|
+
drain();
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function resolveUpstreamState(taskName) {
|
|
771
|
+
const taskConfig = live.config.tasks[taskName];
|
|
772
|
+
const requires = taskConfig.requires ?? [];
|
|
773
|
+
const tokenToTask = /* @__PURE__ */ new Map();
|
|
774
|
+
for (const [name, cfg] of Object.entries(live.config.tasks)) {
|
|
775
|
+
for (const token of cfg.provides ?? []) {
|
|
776
|
+
tokenToTask.set(token, name);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const state = {};
|
|
780
|
+
for (const token of requires) {
|
|
781
|
+
const producerTask = tokenToTask.get(token);
|
|
782
|
+
if (producerTask) {
|
|
783
|
+
state[token] = live.state.tasks[producerTask]?.data;
|
|
784
|
+
} else {
|
|
785
|
+
state[token] = void 0;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return state;
|
|
789
|
+
}
|
|
790
|
+
async function runPipeline(taskName, callbackToken, update) {
|
|
791
|
+
const taskConfig = live.config.tasks[taskName];
|
|
792
|
+
const handlerNames = taskConfig.taskHandlers ?? [];
|
|
793
|
+
const upstreamState = resolveUpstreamState(taskName);
|
|
794
|
+
for (const handlerName of handlerNames) {
|
|
795
|
+
const handler = handlers.get(handlerName);
|
|
796
|
+
if (!handler) {
|
|
797
|
+
throw new Error(`Handler '${handlerName}' not found in registry (task '${taskName}')`);
|
|
798
|
+
}
|
|
799
|
+
const input = {
|
|
800
|
+
nodeId: taskName,
|
|
801
|
+
state: upstreamState,
|
|
802
|
+
taskState: live.state.tasks[taskName],
|
|
803
|
+
config: taskConfig,
|
|
804
|
+
callbackToken,
|
|
805
|
+
update
|
|
806
|
+
};
|
|
807
|
+
const status = await handler(input);
|
|
808
|
+
if (status === "task-initiate-failure") {
|
|
809
|
+
throw new Error(`Handler '${handlerName}' returned task-initiate-failure (task '${taskName}')`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function dispatchTask(taskName) {
|
|
814
|
+
const taskConfig = live.config.tasks[taskName];
|
|
815
|
+
const handlerNames = taskConfig?.taskHandlers;
|
|
816
|
+
if (!handlerNames || handlerNames.length === 0) {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
internalJournal.append({
|
|
820
|
+
type: "task-started",
|
|
821
|
+
taskName,
|
|
822
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
823
|
+
});
|
|
824
|
+
drain();
|
|
825
|
+
const callbackToken = encodeCallbackToken(taskName);
|
|
826
|
+
runPipeline(taskName, callbackToken).catch((error) => {
|
|
827
|
+
if (disposed) return;
|
|
828
|
+
internalJournal.append({
|
|
829
|
+
type: "task-failed",
|
|
830
|
+
taskName,
|
|
831
|
+
error: error.message ?? String(error),
|
|
832
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
833
|
+
});
|
|
834
|
+
drain();
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
return {
|
|
838
|
+
push(event) {
|
|
839
|
+
if (disposed) return;
|
|
840
|
+
if (event.type === "task-completed" && event.data && !event.dataHash) {
|
|
841
|
+
event = { ...event, dataHash: computeDataHash(event.data) };
|
|
842
|
+
}
|
|
843
|
+
inputQueue.append(event);
|
|
844
|
+
drain();
|
|
845
|
+
},
|
|
846
|
+
pushAll(events) {
|
|
847
|
+
if (disposed) return;
|
|
848
|
+
for (const event of events) {
|
|
849
|
+
if (event.type === "task-completed" && event.data && !event.dataHash) {
|
|
850
|
+
inputQueue.append({ ...event, dataHash: computeDataHash(event.data) });
|
|
851
|
+
} else {
|
|
852
|
+
inputQueue.append(event);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
drain();
|
|
856
|
+
},
|
|
857
|
+
resolveCallback(callbackToken, data, errors) {
|
|
858
|
+
if (disposed) return;
|
|
859
|
+
const decoded = decodeCallbackToken(callbackToken);
|
|
860
|
+
if (!decoded) return;
|
|
861
|
+
const { taskName } = decoded;
|
|
862
|
+
if (!live.config.tasks[taskName]) return;
|
|
863
|
+
if (errors && errors.length > 0) {
|
|
864
|
+
inputQueue.append({
|
|
865
|
+
type: "task-failed",
|
|
866
|
+
taskName,
|
|
867
|
+
error: errors.join("; "),
|
|
868
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
869
|
+
});
|
|
870
|
+
} else {
|
|
871
|
+
const dataHash = data && Object.keys(data).length > 0 ? computeDataHash(data) : void 0;
|
|
872
|
+
inputQueue.append({
|
|
873
|
+
type: "task-completed",
|
|
874
|
+
taskName,
|
|
875
|
+
data,
|
|
876
|
+
dataHash,
|
|
877
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
drain();
|
|
881
|
+
},
|
|
882
|
+
addNode(name, taskConfig) {
|
|
883
|
+
if (disposed) return;
|
|
884
|
+
inputQueue.append({ type: "task-upsert", taskName: name, taskConfig, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
885
|
+
drain();
|
|
886
|
+
},
|
|
887
|
+
removeNode(name) {
|
|
888
|
+
if (disposed) return;
|
|
889
|
+
inputQueue.append({ type: "task-removal", taskName: name, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
890
|
+
drain();
|
|
891
|
+
},
|
|
892
|
+
addRequires(nodeName, tokens) {
|
|
893
|
+
if (disposed) return;
|
|
894
|
+
inputQueue.append({ type: "node-requires-add", nodeName, tokens, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
895
|
+
drain();
|
|
896
|
+
},
|
|
897
|
+
removeRequires(nodeName, tokens) {
|
|
898
|
+
if (disposed) return;
|
|
899
|
+
inputQueue.append({ type: "node-requires-remove", nodeName, tokens, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
900
|
+
drain();
|
|
901
|
+
},
|
|
902
|
+
addProvides(nodeName, tokens) {
|
|
903
|
+
if (disposed) return;
|
|
904
|
+
inputQueue.append({ type: "node-provides-add", nodeName, tokens, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
905
|
+
drain();
|
|
906
|
+
},
|
|
907
|
+
removeProvides(nodeName, tokens) {
|
|
908
|
+
if (disposed) return;
|
|
909
|
+
inputQueue.append({ type: "node-provides-remove", nodeName, tokens, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
910
|
+
drain();
|
|
911
|
+
},
|
|
912
|
+
registerHandler(name, fn) {
|
|
913
|
+
handlers.set(name, fn);
|
|
914
|
+
},
|
|
915
|
+
unregisterHandler(name) {
|
|
916
|
+
handlers.delete(name);
|
|
917
|
+
},
|
|
918
|
+
retrigger(taskName) {
|
|
919
|
+
if (disposed) return;
|
|
920
|
+
if (!live.config.tasks[taskName]) return;
|
|
921
|
+
inputQueue.append({
|
|
922
|
+
type: "task-restart",
|
|
923
|
+
taskName,
|
|
924
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
925
|
+
});
|
|
926
|
+
drain();
|
|
927
|
+
},
|
|
928
|
+
retriggerAll(taskNames) {
|
|
929
|
+
if (disposed) return;
|
|
930
|
+
for (const name of taskNames) {
|
|
931
|
+
if (!live.config.tasks[name]) continue;
|
|
932
|
+
inputQueue.append({
|
|
933
|
+
type: "task-restart",
|
|
934
|
+
taskName: name,
|
|
935
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
drain();
|
|
939
|
+
},
|
|
940
|
+
snapshot() {
|
|
941
|
+
return snapshot(live);
|
|
942
|
+
},
|
|
943
|
+
getState() {
|
|
944
|
+
return live;
|
|
945
|
+
},
|
|
946
|
+
getSchedule() {
|
|
947
|
+
return schedule(live);
|
|
948
|
+
},
|
|
949
|
+
dispose() {
|
|
950
|
+
disposed = true;
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// src/card-compute/index.ts
|
|
956
|
+
function deepGet(obj, path2) {
|
|
957
|
+
if (!path2 || !obj) return void 0;
|
|
958
|
+
const parts = path2.split(".");
|
|
959
|
+
let cur = obj;
|
|
960
|
+
for (let i = 0; i < parts.length; i++) {
|
|
961
|
+
if (cur == null) return void 0;
|
|
962
|
+
cur = cur[parts[i]];
|
|
963
|
+
}
|
|
964
|
+
return cur;
|
|
965
|
+
}
|
|
966
|
+
function deepSet(obj, path2, value) {
|
|
967
|
+
const parts = path2.split(".");
|
|
968
|
+
let cur = obj;
|
|
969
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
970
|
+
if (cur[parts[i]] == null || typeof cur[parts[i]] !== "object") cur[parts[i]] = {};
|
|
971
|
+
cur = cur[parts[i]];
|
|
972
|
+
}
|
|
973
|
+
cur[parts[parts.length - 1]] = value;
|
|
974
|
+
}
|
|
975
|
+
async function run(node, options) {
|
|
976
|
+
if (!node?.compute?.length) return node;
|
|
977
|
+
if (!node.card_data) node.card_data = {};
|
|
978
|
+
node.computed_values = {};
|
|
979
|
+
node._sourcesData = options?.sourcesData ?? {};
|
|
980
|
+
const ctx = {
|
|
981
|
+
card_data: node.card_data,
|
|
982
|
+
requires: node.requires ?? {},
|
|
983
|
+
fetched_sources: node._sourcesData,
|
|
984
|
+
computed_values: node.computed_values
|
|
985
|
+
};
|
|
986
|
+
for (const step of node.compute) {
|
|
987
|
+
try {
|
|
988
|
+
const val = await jsonata2(step.expr).evaluate(ctx);
|
|
989
|
+
deepSet(node.computed_values, step.bindTo, val);
|
|
990
|
+
ctx.computed_values = node.computed_values;
|
|
991
|
+
} catch (err) {
|
|
992
|
+
console.error(`CardCompute.run error on "${node.id ?? "?"}.${step.bindTo}":`, err);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return node;
|
|
996
|
+
}
|
|
997
|
+
async function evalExpr(expr, node) {
|
|
998
|
+
const ctx = {
|
|
999
|
+
card_data: node.card_data ?? {},
|
|
1000
|
+
requires: node.requires ?? {},
|
|
1001
|
+
fetched_sources: node._sourcesData ?? {},
|
|
1002
|
+
computed_values: node.computed_values ?? {}
|
|
1003
|
+
};
|
|
1004
|
+
return jsonata2(expr).evaluate(ctx);
|
|
1005
|
+
}
|
|
1006
|
+
function resolve(node, path2) {
|
|
1007
|
+
if (path2.startsWith("fetched_sources.")) {
|
|
1008
|
+
return deepGet(node._sourcesData ?? {}, path2.slice("fetched_sources.".length));
|
|
1009
|
+
}
|
|
1010
|
+
return deepGet(node, path2);
|
|
1011
|
+
}
|
|
1012
|
+
var VALID_ELEMENT_KINDS = /* @__PURE__ */ new Set([
|
|
1013
|
+
"metric",
|
|
1014
|
+
"table",
|
|
1015
|
+
"chart",
|
|
1016
|
+
"form",
|
|
1017
|
+
"filter",
|
|
1018
|
+
"list",
|
|
1019
|
+
"notes",
|
|
1020
|
+
"todo",
|
|
1021
|
+
"alert",
|
|
1022
|
+
"narrative",
|
|
1023
|
+
"badge",
|
|
1024
|
+
"text",
|
|
1025
|
+
"markdown",
|
|
1026
|
+
"custom"
|
|
1027
|
+
]);
|
|
1028
|
+
var ALLOWED_KEYS = /* @__PURE__ */ new Set(["id", "meta", "requires", "provides", "view", "card_data", "compute", "sources"]);
|
|
1029
|
+
function validateNode(node) {
|
|
1030
|
+
const errors = [];
|
|
1031
|
+
if (!node || typeof node !== "object" || Array.isArray(node)) {
|
|
1032
|
+
return { ok: false, errors: ["Node must be a non-null object"] };
|
|
1033
|
+
}
|
|
1034
|
+
const n = node;
|
|
1035
|
+
if (typeof n.id !== "string" || !n.id) errors.push("id: required, must be a non-empty string");
|
|
1036
|
+
for (const key of Object.keys(n)) {
|
|
1037
|
+
if (!ALLOWED_KEYS.has(key)) errors.push(`Unknown top-level key: "${key}"`);
|
|
1038
|
+
}
|
|
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");
|
|
1041
|
+
}
|
|
1042
|
+
if (n.meta != null) {
|
|
1043
|
+
if (typeof n.meta !== "object" || Array.isArray(n.meta)) {
|
|
1044
|
+
errors.push("meta: must be an object");
|
|
1045
|
+
} else {
|
|
1046
|
+
const meta = n.meta;
|
|
1047
|
+
if (meta.title != null && typeof meta.title !== "string") errors.push("meta.title: must be a string");
|
|
1048
|
+
if (meta.tags != null && !Array.isArray(meta.tags)) errors.push("meta.tags: must be an array");
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (n.requires != null && !Array.isArray(n.requires)) errors.push("requires: must be an array of strings");
|
|
1052
|
+
if (n.provides != null) {
|
|
1053
|
+
if (!Array.isArray(n.provides)) {
|
|
1054
|
+
errors.push("provides: must be an array of { bindTo, src } bindings");
|
|
1055
|
+
} else {
|
|
1056
|
+
n.provides.forEach((p, i) => {
|
|
1057
|
+
if (!p || typeof p !== "object" || Array.isArray(p)) {
|
|
1058
|
+
errors.push(`provides[${i}]: must be an object with bindTo and src`);
|
|
1059
|
+
} else {
|
|
1060
|
+
const b = p;
|
|
1061
|
+
if (typeof b.bindTo !== "string" || !b.bindTo) errors.push(`provides[${i}]: missing required "bindTo" string`);
|
|
1062
|
+
if (typeof b.src !== "string" || !b.src) errors.push(`provides[${i}]: missing required "src" string`);
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (n.compute != null) {
|
|
1068
|
+
if (!Array.isArray(n.compute)) {
|
|
1069
|
+
errors.push("compute: must be an array of compute steps");
|
|
1070
|
+
} else {
|
|
1071
|
+
n.compute.forEach((step, i) => {
|
|
1072
|
+
if (!step || typeof step !== "object" || Array.isArray(step)) {
|
|
1073
|
+
errors.push(`compute[${i}]: must be a compute step object`);
|
|
1074
|
+
} else {
|
|
1075
|
+
const s = step;
|
|
1076
|
+
if (typeof s.bindTo !== "string" || !s.bindTo) errors.push(`compute[${i}]: missing required "bindTo" property`);
|
|
1077
|
+
if (typeof s.expr !== "string" || !s.expr) errors.push(`compute[${i}]: missing required "expr" string (JSONata expression)`);
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (n.sources != null) {
|
|
1083
|
+
if (!Array.isArray(n.sources)) {
|
|
1084
|
+
errors.push("sources: must be an array");
|
|
1085
|
+
} else {
|
|
1086
|
+
n.sources.forEach((src, i) => {
|
|
1087
|
+
if (!src || typeof src !== "object" || Array.isArray(src)) {
|
|
1088
|
+
errors.push(`sources[${i}]: must be an object`);
|
|
1089
|
+
} else {
|
|
1090
|
+
const s = src;
|
|
1091
|
+
if (typeof s.bindTo !== "string" || !s.bindTo) errors.push(`sources[${i}]: missing required "bindTo" property`);
|
|
1092
|
+
if (s.outputFile != null && typeof s.outputFile !== "string") errors.push(`sources[${i}]: outputFile must be a string`);
|
|
1093
|
+
if (s.optionalForCompletionGating != null && typeof s.optionalForCompletionGating !== "boolean") {
|
|
1094
|
+
errors.push(`sources[${i}]: optionalForCompletionGating must be a boolean`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
if (n.view != null) {
|
|
1101
|
+
if (typeof n.view !== "object" || Array.isArray(n.view)) {
|
|
1102
|
+
errors.push("view: must be an object");
|
|
1103
|
+
} else {
|
|
1104
|
+
const view = n.view;
|
|
1105
|
+
if (!Array.isArray(view.elements) || view.elements.length === 0) {
|
|
1106
|
+
errors.push("view.elements: required, must be a non-empty array");
|
|
1107
|
+
} else {
|
|
1108
|
+
view.elements.forEach((elem, i) => {
|
|
1109
|
+
if (!elem || typeof elem !== "object") {
|
|
1110
|
+
errors.push(`view.elements[${i}]: must be an object`);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
if (!elem.kind || typeof elem.kind !== "string") {
|
|
1114
|
+
errors.push(`view.elements[${i}].kind: required, must be a string`);
|
|
1115
|
+
} else if (!VALID_ELEMENT_KINDS.has(elem.kind)) {
|
|
1116
|
+
errors.push(`view.elements[${i}].kind: unknown kind "${elem.kind}". Valid: ${[...VALID_ELEMENT_KINDS].join(", ")}`);
|
|
1117
|
+
}
|
|
1118
|
+
if (elem.data != null && (typeof elem.data !== "object" || Array.isArray(elem.data))) {
|
|
1119
|
+
errors.push(`view.elements[${i}].data: must be an object`);
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
if (view.layout != null && (typeof view.layout !== "object" || Array.isArray(view.layout))) errors.push("view.layout: must be an object");
|
|
1124
|
+
if (view.features != null && (typeof view.features !== "object" || Array.isArray(view.features))) errors.push("view.features: must be an object");
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
return { ok: errors.length === 0, errors };
|
|
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
|
+
}
|
|
1138
|
+
var CardCompute = {
|
|
1139
|
+
run,
|
|
1140
|
+
eval: evalExpr,
|
|
1141
|
+
resolve,
|
|
1142
|
+
validate: validateNode,
|
|
1143
|
+
enrichSources
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
// src/cli/board-live-cards-cli.ts
|
|
1147
|
+
var BOARD_FILE = "board-graph.json";
|
|
1148
|
+
var JOURNAL_FILE = "board-journal.jsonl";
|
|
1149
|
+
var TASK_EXECUTOR_LOG_FILE = "task-executor.jsonl";
|
|
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";
|
|
1156
|
+
var EMPTY_CONFIG = { settings: { completion: "manual", refreshStrategy: "data-changed" }, tasks: {} };
|
|
1157
|
+
var BoardJournal = class {
|
|
1158
|
+
journalPath;
|
|
1159
|
+
lastDrainedId;
|
|
1160
|
+
constructor(journalPath, lastDrainedJournalId) {
|
|
1161
|
+
this.journalPath = journalPath;
|
|
1162
|
+
this.lastDrainedId = lastDrainedJournalId;
|
|
1163
|
+
}
|
|
1164
|
+
append(event) {
|
|
1165
|
+
const entry = { id: randomUUID(), event };
|
|
1166
|
+
fs.appendFileSync(this.journalPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
1167
|
+
}
|
|
1168
|
+
drain() {
|
|
1169
|
+
if (!fs.existsSync(this.journalPath)) return [];
|
|
1170
|
+
const content = fs.readFileSync(this.journalPath, "utf-8").trim();
|
|
1171
|
+
if (!content) return [];
|
|
1172
|
+
const entries = content.split("\n").map((l) => JSON.parse(l));
|
|
1173
|
+
let startIdx = 0;
|
|
1174
|
+
if (this.lastDrainedId) {
|
|
1175
|
+
const drainedIdx = entries.findIndex((e) => e.id === this.lastDrainedId);
|
|
1176
|
+
if (drainedIdx !== -1) startIdx = drainedIdx + 1;
|
|
1177
|
+
}
|
|
1178
|
+
const undrained = entries.slice(startIdx);
|
|
1179
|
+
if (undrained.length > 0) {
|
|
1180
|
+
this.lastDrainedId = undrained[undrained.length - 1].id;
|
|
1181
|
+
}
|
|
1182
|
+
return undrained.map((e) => e.event);
|
|
1183
|
+
}
|
|
1184
|
+
get size() {
|
|
1185
|
+
if (!fs.existsSync(this.journalPath)) return 0;
|
|
1186
|
+
const content = fs.readFileSync(this.journalPath, "utf-8").trim();
|
|
1187
|
+
if (!content) return 0;
|
|
1188
|
+
const entries = content.split("\n").map((l) => JSON.parse(l));
|
|
1189
|
+
if (!this.lastDrainedId) return entries.length;
|
|
1190
|
+
const drainedIdx = entries.findIndex((e) => e.id === this.lastDrainedId);
|
|
1191
|
+
return drainedIdx === -1 ? entries.length : entries.length - drainedIdx - 1;
|
|
1192
|
+
}
|
|
1193
|
+
get lastDrainedJournalId() {
|
|
1194
|
+
return this.lastDrainedId;
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
function readCardInventory(boardDir) {
|
|
1198
|
+
const inventoryPath = path.join(boardDir, INVENTORY_FILE);
|
|
1199
|
+
if (!fs.existsSync(inventoryPath)) return [];
|
|
1200
|
+
const lines = fs.readFileSync(inventoryPath, "utf-8").split("\n").filter((l) => l.trim());
|
|
1201
|
+
return lines.map((l) => JSON.parse(l));
|
|
1202
|
+
}
|
|
1203
|
+
function lookupCardPath(boardDir, cardId) {
|
|
1204
|
+
const entries = readCardInventory(boardDir);
|
|
1205
|
+
const entry = entries.find((e) => e.cardId === cardId);
|
|
1206
|
+
return entry?.cardFilePath ?? null;
|
|
1207
|
+
}
|
|
1208
|
+
function appendCardInventory(boardDir, entry) {
|
|
1209
|
+
const inventoryPath = path.join(boardDir, INVENTORY_FILE);
|
|
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 };
|
|
1238
|
+
}
|
|
1239
|
+
function initBoard(dir) {
|
|
1240
|
+
const boardPath = path.join(dir, BOARD_FILE);
|
|
1241
|
+
if (fs.existsSync(boardPath)) {
|
|
1242
|
+
const envelope2 = JSON.parse(fs.readFileSync(boardPath, "utf-8"));
|
|
1243
|
+
restore(envelope2.graph);
|
|
1244
|
+
return "exists";
|
|
1245
|
+
}
|
|
1246
|
+
if (fs.existsSync(dir)) {
|
|
1247
|
+
const entries = fs.readdirSync(dir);
|
|
1248
|
+
if (entries.length > 0) {
|
|
1249
|
+
throw new Error(`Directory "${dir}" is not empty and has no valid ${BOARD_FILE}`);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1253
|
+
const live = createLiveGraph(EMPTY_CONFIG);
|
|
1254
|
+
const snap = snapshot(live);
|
|
1255
|
+
const envelope = { lastDrainedJournalId: "", graph: snap };
|
|
1256
|
+
fs.writeFileSync(boardPath, JSON.stringify(envelope, null, 2));
|
|
1257
|
+
return "created";
|
|
1258
|
+
}
|
|
1259
|
+
function loadBoardEnvelope(dir) {
|
|
1260
|
+
const raw = fs.readFileSync(path.join(dir, BOARD_FILE), "utf-8");
|
|
1261
|
+
return JSON.parse(raw);
|
|
1262
|
+
}
|
|
1263
|
+
function loadBoard(dir) {
|
|
1264
|
+
const envelope = loadBoardEnvelope(dir);
|
|
1265
|
+
return restore(envelope.graph);
|
|
1266
|
+
}
|
|
1267
|
+
function saveBoard(dir, rg, journal) {
|
|
1268
|
+
const snap = rg.snapshot();
|
|
1269
|
+
const envelope = {
|
|
1270
|
+
lastDrainedJournalId: journal.lastDrainedJournalId,
|
|
1271
|
+
graph: snap
|
|
1272
|
+
};
|
|
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);
|
|
1330
|
+
}
|
|
1331
|
+
function withBoardLock(boardDir, fn) {
|
|
1332
|
+
const boardPath = path.join(boardDir, BOARD_FILE);
|
|
1333
|
+
const release = lockSync(boardPath, { retries: { retries: 5, minTimeout: 100 } });
|
|
1334
|
+
try {
|
|
1335
|
+
return fn();
|
|
1336
|
+
} finally {
|
|
1337
|
+
release();
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
function decodeCallbackToken2(token) {
|
|
1341
|
+
try {
|
|
1342
|
+
const payload = JSON.parse(Buffer.from(token, "base64url").toString());
|
|
1343
|
+
if (typeof payload?.t === "string") return { taskName: payload.t };
|
|
1344
|
+
return null;
|
|
1345
|
+
} catch {
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
function encodeSourceToken(payload) {
|
|
1350
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
1351
|
+
}
|
|
1352
|
+
function decodeSourceToken(token) {
|
|
1353
|
+
try {
|
|
1354
|
+
const p = JSON.parse(Buffer.from(token, "base64url").toString());
|
|
1355
|
+
if (typeof p?.cbk === "string" && typeof p?.cid === "string" && typeof p?.b === "string" && typeof p?.d === "string") {
|
|
1356
|
+
return p;
|
|
1357
|
+
}
|
|
1358
|
+
return null;
|
|
1359
|
+
} catch {
|
|
1360
|
+
return null;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
function runtimePath(boardDir, cardId) {
|
|
1364
|
+
return path.join(boardDir, `${cardId}.runtime.json`);
|
|
1365
|
+
}
|
|
1366
|
+
function readRuntimeState(boardDir, cardId) {
|
|
1367
|
+
const p = runtimePath(boardDir, cardId);
|
|
1368
|
+
if (!fs.existsSync(p)) return { _sources: {} };
|
|
1369
|
+
try {
|
|
1370
|
+
return JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
1371
|
+
} catch {
|
|
1372
|
+
return { _sources: {} };
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
function writeRuntimeState(boardDir, cardId, state) {
|
|
1376
|
+
fs.writeFileSync(runtimePath(boardDir, cardId), JSON.stringify(state, null, 2));
|
|
1377
|
+
}
|
|
1378
|
+
function appendEventToJournal(boardDir, event) {
|
|
1379
|
+
const journalPath = path.join(boardDir, JOURNAL_FILE);
|
|
1380
|
+
const entry = { id: randomUUID(), event };
|
|
1381
|
+
fs.appendFileSync(journalPath, JSON.stringify(entry) + "\n", "utf-8");
|
|
1382
|
+
}
|
|
1383
|
+
function getUndrainedEntries(boardDir, lastDrainedId) {
|
|
1384
|
+
const journalPath = path.join(boardDir, JOURNAL_FILE);
|
|
1385
|
+
if (!fs.existsSync(journalPath)) return [];
|
|
1386
|
+
const content = fs.readFileSync(journalPath, "utf-8").trim();
|
|
1387
|
+
if (!content) return [];
|
|
1388
|
+
const entries = content.split("\n").map((l) => JSON.parse(l));
|
|
1389
|
+
if (!lastDrainedId) return entries;
|
|
1390
|
+
const idx = entries.findIndex((e) => e.id === lastDrainedId);
|
|
1391
|
+
return idx === -1 ? entries : entries.slice(idx + 1);
|
|
1392
|
+
}
|
|
1393
|
+
function determineLatestPendingAccumulated(boardDir) {
|
|
1394
|
+
const boardPath = path.join(boardDir, BOARD_FILE);
|
|
1395
|
+
if (!fs.existsSync(boardPath)) return 0;
|
|
1396
|
+
try {
|
|
1397
|
+
const envelope = loadBoardEnvelope(boardDir);
|
|
1398
|
+
return getUndrainedEntries(boardDir, envelope.lastDrainedJournalId).length;
|
|
1399
|
+
} catch {
|
|
1400
|
+
return 0;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
function shouldUseShellForCommand(cmd, forceShell) {
|
|
1404
|
+
if (typeof forceShell === "boolean") return forceShell;
|
|
1405
|
+
return process.platform === "win32" && /\.(cmd|bat)$/i.test(cmd);
|
|
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
|
+
}
|
|
1446
|
+
function spawnDetachedCommand(cmd, args) {
|
|
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" });
|
|
1464
|
+
child.unref();
|
|
1465
|
+
}
|
|
1466
|
+
function execCommandSync(cmd, args, options) {
|
|
1467
|
+
const output = execFileSync(cmd, args, {
|
|
1468
|
+
shell: shouldUseShellForCommand(cmd, options?.shell),
|
|
1469
|
+
timeout: options?.timeout,
|
|
1470
|
+
encoding: options?.encoding,
|
|
1471
|
+
cwd: options?.cwd,
|
|
1472
|
+
windowsHide: true,
|
|
1473
|
+
env: options?.env
|
|
1474
|
+
});
|
|
1475
|
+
return typeof output === "string" ? output : output.toString("utf-8");
|
|
1476
|
+
}
|
|
1477
|
+
function execCommandAsync(cmd, args, callback) {
|
|
1478
|
+
execFile(
|
|
1479
|
+
cmd,
|
|
1480
|
+
args,
|
|
1481
|
+
{ shell: shouldUseShellForCommand(cmd), encoding: "utf8", windowsHide: true },
|
|
1482
|
+
(err, stdout, stderr) => callback(err ?? null, stdout, stderr)
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
function splitCommandLine(command) {
|
|
1486
|
+
const tokens = [];
|
|
1487
|
+
let current = "";
|
|
1488
|
+
let quote = null;
|
|
1489
|
+
for (const ch of command.trim()) {
|
|
1490
|
+
if (quote) {
|
|
1491
|
+
if (ch === quote) {
|
|
1492
|
+
quote = null;
|
|
1493
|
+
} else {
|
|
1494
|
+
current += ch;
|
|
1495
|
+
}
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
if (ch === '"' || ch === "'") {
|
|
1499
|
+
quote = ch;
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
if (/\s/.test(ch)) {
|
|
1503
|
+
if (current) {
|
|
1504
|
+
tokens.push(current);
|
|
1505
|
+
current = "";
|
|
1506
|
+
}
|
|
1507
|
+
continue;
|
|
1508
|
+
}
|
|
1509
|
+
current += ch;
|
|
1510
|
+
}
|
|
1511
|
+
if (quote) {
|
|
1512
|
+
throw new Error(`Unterminated quote in command: ${command}`);
|
|
1513
|
+
}
|
|
1514
|
+
if (current) tokens.push(current);
|
|
1515
|
+
return tokens;
|
|
1516
|
+
}
|
|
1517
|
+
function spawnDetachedProcessAccumulatedWorker(boardDir) {
|
|
1518
|
+
const { cmd, args: cliArgs } = getCliInvocation("process-accumulated-events", ["--rg", boardDir, "--inline-loop"]);
|
|
1519
|
+
try {
|
|
1520
|
+
spawnDetachedCommand(cmd, cliArgs);
|
|
1521
|
+
return true;
|
|
1522
|
+
} catch {
|
|
1523
|
+
return false;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
async function processAccumulatedEventsInlineLoop(boardDir, settleDelayMs = 50) {
|
|
1527
|
+
while (determineLatestPendingAccumulated(boardDir) > 0) {
|
|
1528
|
+
const ran = processAccumulatedEvents(boardDir);
|
|
1529
|
+
if (!ran) return false;
|
|
1530
|
+
await new Promise((resolve3) => setTimeout(resolve3, settleDelayMs));
|
|
1531
|
+
}
|
|
1532
|
+
return true;
|
|
1533
|
+
}
|
|
1534
|
+
function shouldAvoidDetachedProcessSpawn() {
|
|
1535
|
+
return process.env.BOARD_LIVE_CARDS_NO_SPAWN === "1";
|
|
1536
|
+
}
|
|
1537
|
+
function processAccumulatedEvents(boardDir) {
|
|
1538
|
+
const boardPath = path.join(boardDir, BOARD_FILE);
|
|
1539
|
+
let release;
|
|
1540
|
+
try {
|
|
1541
|
+
release = lockSync(boardPath, { retries: 0 });
|
|
1542
|
+
} catch {
|
|
1543
|
+
return false;
|
|
1544
|
+
}
|
|
1545
|
+
try {
|
|
1546
|
+
const { rg, journal } = createBoardReactiveGraph(boardDir);
|
|
1547
|
+
const undrained = journal.drain();
|
|
1548
|
+
rg.pushAll(undrained);
|
|
1549
|
+
saveBoard(boardDir, rg, journal);
|
|
1550
|
+
rg.dispose();
|
|
1551
|
+
return true;
|
|
1552
|
+
} finally {
|
|
1553
|
+
release();
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
async function processAccumulatedEventsInfinitePass(boardDir, settleDelayMs = 50, options) {
|
|
1557
|
+
if (options?.inlineLoop || shouldAvoidDetachedProcessSpawn()) {
|
|
1558
|
+
return processAccumulatedEventsInlineLoop(boardDir, settleDelayMs);
|
|
1559
|
+
}
|
|
1560
|
+
return spawnDetachedProcessAccumulatedWorker(boardDir);
|
|
1561
|
+
}
|
|
1562
|
+
async function processAccumulatedEventsForced(boardDir, options) {
|
|
1563
|
+
processAccumulatedEvents(boardDir);
|
|
1564
|
+
await processAccumulatedEventsInfinitePass(boardDir, 50, options);
|
|
1565
|
+
}
|
|
1566
|
+
function liveCardToTaskConfig(card) {
|
|
1567
|
+
const requires = card.requires;
|
|
1568
|
+
const provides = card.provides?.map((p) => p.bindTo) ?? [];
|
|
1569
|
+
return {
|
|
1570
|
+
requires: requires && requires.length > 0 ? requires : void 0,
|
|
1571
|
+
provides,
|
|
1572
|
+
taskHandlers: ["card-handler"],
|
|
1573
|
+
description: card.meta?.title ?? card.id
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
var __dirname$1 = path.dirname(fileURLToPath(import.meta.url));
|
|
1577
|
+
var REPO_ROOT = path.resolve(__dirname$1, "..", "..");
|
|
1578
|
+
var LOCAL_TSX_CLI = path.join(REPO_ROOT, "node_modules", "tsx", "dist", "cli.mjs");
|
|
1579
|
+
function getCliInvocation(command, args) {
|
|
1580
|
+
const jsPath = path.join(__dirname$1, "board-live-cards-cli.js");
|
|
1581
|
+
if (fs.existsSync(jsPath)) {
|
|
1582
|
+
return { cmd: process.execPath, args: [jsPath, command, ...args] };
|
|
1583
|
+
}
|
|
1584
|
+
const tsPath = path.join(__dirname$1, "board-live-cards-cli.ts");
|
|
1585
|
+
if (fs.existsSync(tsPath) && fs.existsSync(LOCAL_TSX_CLI)) {
|
|
1586
|
+
return { cmd: process.execPath, args: [LOCAL_TSX_CLI, tsPath, command, ...args] };
|
|
1587
|
+
}
|
|
1588
|
+
const npxCmd = process.platform === "win32" ? "npx.cmd" : "npx";
|
|
1589
|
+
return { cmd: npxCmd, args: ["tsx", tsPath, command, ...args] };
|
|
1590
|
+
}
|
|
1591
|
+
function invokeRunSources(boardDir, cardPath, callbackToken, callback) {
|
|
1592
|
+
const { cmd, args } = getCliInvocation("run-sources-internal", ["--card", cardPath, "--token", callbackToken, "--rg", boardDir]);
|
|
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
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
function invokeSourceDataFetched(sourceToken, tmpFile, callback) {
|
|
1612
|
+
const { cmd, args } = getCliInvocation("source-data-fetched", ["--tmp", tmpFile, "--token", sourceToken]);
|
|
1613
|
+
execCommandAsync(cmd, args, (err, stdout, stderr) => {
|
|
1614
|
+
if (err) console.error(`[source-data-fetched] call failed:`, err.message);
|
|
1615
|
+
if (stdout) console.log(stdout.trim());
|
|
1616
|
+
if (stderr) console.error(stderr.trim());
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
function invokeSourceDataFetchFailure(sourceToken, reason, callback) {
|
|
1620
|
+
const { cmd, args } = getCliInvocation("source-data-fetch-failure", ["--token", sourceToken, "--reason", reason]);
|
|
1621
|
+
execCommandAsync(cmd, args, (err) => callback(err));
|
|
1622
|
+
}
|
|
1623
|
+
function createBoardReactiveGraph(boardDir) {
|
|
1624
|
+
const envelope = loadBoardEnvelope(boardDir);
|
|
1625
|
+
const live = restore(envelope.graph);
|
|
1626
|
+
const journalPath = path.join(boardDir, JOURNAL_FILE);
|
|
1627
|
+
const journal = new BoardJournal(journalPath, envelope.lastDrainedJournalId);
|
|
1628
|
+
const handlers = {
|
|
1629
|
+
"card-handler": async (input) => {
|
|
1630
|
+
const cardPath = lookupCardPath(boardDir, input.nodeId);
|
|
1631
|
+
if (!cardPath) return "task-initiate-failure";
|
|
1632
|
+
const card = JSON.parse(fs.readFileSync(cardPath, "utf-8"));
|
|
1633
|
+
const cardId = card.id;
|
|
1634
|
+
const cardState = card.card_data ?? {};
|
|
1635
|
+
const allSources = card.sources ?? [];
|
|
1636
|
+
const requiredSources = allSources.filter((s) => s.optionalForCompletionGating !== true);
|
|
1637
|
+
const runtime = readRuntimeState(boardDir, cardId);
|
|
1638
|
+
let runtimeDirty = false;
|
|
1639
|
+
if (input.update) {
|
|
1640
|
+
const u = input.update;
|
|
1641
|
+
const bindTo = u.bindTo;
|
|
1642
|
+
if (!runtime._sources[bindTo]) runtime._sources[bindTo] = {};
|
|
1643
|
+
if (u.failure) {
|
|
1644
|
+
runtime._sources[bindTo].lastError = u.reason ?? "unknown";
|
|
1645
|
+
delete runtime._sources[bindTo].lastFetchedAt;
|
|
1646
|
+
runtimeDirty = true;
|
|
1647
|
+
console.log(`[card-handler] source "${bindTo}" fetch failed: ${runtime._sources[bindTo].lastError}`);
|
|
1648
|
+
} else {
|
|
1649
|
+
runtime._sources[bindTo].lastFetchedAt = u.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1650
|
+
delete runtime._sources[bindTo].lastError;
|
|
1651
|
+
runtimeDirty = true;
|
|
1652
|
+
console.log(`[card-handler] source "${bindTo}" delivered \u2192 ${u.dest}`);
|
|
1653
|
+
}
|
|
1654
|
+
if (runtimeDirty) writeRuntimeState(boardDir, cardId, runtime);
|
|
1655
|
+
}
|
|
1656
|
+
const sourcesData = {};
|
|
1657
|
+
for (const src of allSources) {
|
|
1658
|
+
if (src.outputFile) {
|
|
1659
|
+
const filePath = path.join(boardDir, src.outputFile);
|
|
1660
|
+
if (fs.existsSync(filePath)) {
|
|
1661
|
+
const raw = fs.readFileSync(filePath, "utf-8").trim();
|
|
1662
|
+
try {
|
|
1663
|
+
sourcesData[src.bindTo] = JSON.parse(raw);
|
|
1664
|
+
} catch {
|
|
1665
|
+
sourcesData[src.bindTo] = raw;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
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
|
+
}
|
|
1679
|
+
const computeNode = {
|
|
1680
|
+
id: cardId,
|
|
1681
|
+
card_data: { ...cardState },
|
|
1682
|
+
requires,
|
|
1683
|
+
sources: allSources,
|
|
1684
|
+
compute: card.compute
|
|
1685
|
+
};
|
|
1686
|
+
computeNode._sourcesData = sourcesData;
|
|
1687
|
+
if (card.compute) {
|
|
1688
|
+
await CardCompute.run(computeNode, { sourcesData });
|
|
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
|
+
});
|
|
1696
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1697
|
+
const undeliveredRequired = requiredSources.filter((s) => {
|
|
1698
|
+
if (!s.outputFile) return false;
|
|
1699
|
+
const entry = runtime._sources[s.bindTo];
|
|
1700
|
+
if (!entry?.lastRequestedAt) return true;
|
|
1701
|
+
if (!entry.lastFetchedAt) return true;
|
|
1702
|
+
return entry.lastFetchedAt <= entry.lastRequestedAt;
|
|
1703
|
+
});
|
|
1704
|
+
if (undeliveredRequired.length > 0) {
|
|
1705
|
+
let stampedAny = false;
|
|
1706
|
+
for (const src of undeliveredRequired) {
|
|
1707
|
+
const entry = runtime._sources[src.bindTo] ?? {};
|
|
1708
|
+
if (!entry.lastRequestedAt || entry.lastFetchedAt && entry.lastFetchedAt >= entry.lastRequestedAt) {
|
|
1709
|
+
entry.lastRequestedAt = now;
|
|
1710
|
+
runtime._sources[src.bindTo] = entry;
|
|
1711
|
+
stampedAny = true;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
if (stampedAny) writeRuntimeState(boardDir, cardId, runtime);
|
|
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
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
return "task-initiated";
|
|
1742
|
+
}
|
|
1743
|
+
const providesBindings = card.provides ?? [];
|
|
1744
|
+
const data = {};
|
|
1745
|
+
for (const { bindTo, src } of providesBindings) {
|
|
1746
|
+
data[bindTo] = CardCompute.resolve(computeNode, src);
|
|
1747
|
+
}
|
|
1748
|
+
writeRuntimeDataObjects(boardDir, data);
|
|
1749
|
+
const undeliveredOptional = allSources.filter((s) => {
|
|
1750
|
+
if (s.optionalForCompletionGating !== true || !s.outputFile) return false;
|
|
1751
|
+
const entry = runtime._sources[s.bindTo];
|
|
1752
|
+
if (!entry?.lastRequestedAt) return true;
|
|
1753
|
+
if (!entry.lastFetchedAt) return true;
|
|
1754
|
+
return entry.lastFetchedAt <= entry.lastRequestedAt;
|
|
1755
|
+
});
|
|
1756
|
+
if (undeliveredOptional.length > 0) {
|
|
1757
|
+
invokeRunSources(boardDir, cardPath, input.callbackToken, (err) => {
|
|
1758
|
+
if (err) console.error(`[card-handler] ${input.nodeId}:`, err.message);
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
appendEventToJournal(boardDir, {
|
|
1762
|
+
type: "task-completed",
|
|
1763
|
+
taskName: input.nodeId,
|
|
1764
|
+
data,
|
|
1765
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1766
|
+
});
|
|
1767
|
+
return "task-initiated";
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
const rg = createReactiveGraph(live, { handlers });
|
|
1771
|
+
return { rg, journal };
|
|
1772
|
+
}
|
|
1773
|
+
function addSingleCardFromFile(dir, cardFile) {
|
|
1774
|
+
const absCardPath = path.resolve(cardFile);
|
|
1775
|
+
if (!fs.existsSync(absCardPath)) {
|
|
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)}`);
|
|
1783
|
+
}
|
|
1784
|
+
if (!card.id) {
|
|
1785
|
+
throw new Error('Card JSON must have an "id" field');
|
|
1786
|
+
}
|
|
1787
|
+
const existing = readCardInventory(dir);
|
|
1788
|
+
if (existing.some((e) => e.cardId === card.id)) {
|
|
1789
|
+
throw new Error(`Card "${card.id}" already exists in inventory`);
|
|
1790
|
+
}
|
|
1791
|
+
appendCardInventory(dir, {
|
|
1792
|
+
cardId: card.id,
|
|
1793
|
+
cardFilePath: absCardPath,
|
|
1794
|
+
addedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1795
|
+
});
|
|
1796
|
+
const taskConfig = liveCardToTaskConfig(card);
|
|
1797
|
+
appendEventToJournal(dir, {
|
|
1798
|
+
type: "task-upsert",
|
|
1799
|
+
taskName: card.id,
|
|
1800
|
+
taskConfig,
|
|
1801
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1802
|
+
});
|
|
1803
|
+
console.log(`Card "${card.id}" added to board at ${path.resolve(dir)} (drain scheduled)`);
|
|
1804
|
+
console.log(` taskHandlers: [${taskConfig.taskHandlers?.join(", ") ?? ""}]`);
|
|
1805
|
+
console.log(` provides: [${taskConfig.provides.join(", ")}]`);
|
|
1806
|
+
if (taskConfig.requires) console.log(` requires: [${taskConfig.requires.join(", ")}]`);
|
|
1807
|
+
}
|
|
1808
|
+
function resolveCardGlobMatches(cardGlob) {
|
|
1809
|
+
const patterns = cardGlob.split(",").map((s) => s.trim()).filter(Boolean).map((p) => p.replace(/\\/g, "/"));
|
|
1810
|
+
const matches = fg.sync(patterns, {
|
|
1811
|
+
absolute: true,
|
|
1812
|
+
onlyFiles: true,
|
|
1813
|
+
unique: true,
|
|
1814
|
+
dot: false
|
|
1815
|
+
});
|
|
1816
|
+
return [...matches].map((m) => path.resolve(m)).sort((a, b) => a.localeCompare(b));
|
|
1817
|
+
}
|
|
1818
|
+
function cmdAddCards(args) {
|
|
1819
|
+
const rgIdx = args.indexOf("--rg");
|
|
1820
|
+
const cardIdx = args.indexOf("--card");
|
|
1821
|
+
const globIdx = args.indexOf("--card-glob");
|
|
1822
|
+
const dir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
|
|
1823
|
+
const cardFile = cardIdx !== -1 ? args[cardIdx + 1] : void 0;
|
|
1824
|
+
const cardGlob = globIdx !== -1 ? args[globIdx + 1] : void 0;
|
|
1825
|
+
if (!dir || !cardFile && !cardGlob || cardFile && cardGlob) {
|
|
1826
|
+
throw new Error("Usage: board-live-cards add-cards --rg <dir> (--card <card.json> | --card-glob <glob>)");
|
|
1827
|
+
}
|
|
1828
|
+
if (cardFile) {
|
|
1829
|
+
addSingleCardFromFile(dir, cardFile);
|
|
1830
|
+
} else {
|
|
1831
|
+
const matches = resolveCardGlobMatches(cardGlob);
|
|
1832
|
+
if (matches.length === 0) {
|
|
1833
|
+
throw new Error(`No card files matched glob: ${cardGlob}`);
|
|
1834
|
+
}
|
|
1835
|
+
for (const match of matches) {
|
|
1836
|
+
addSingleCardFromFile(dir, match);
|
|
1837
|
+
}
|
|
1838
|
+
console.log(`Added ${matches.length} cards from glob: ${cardGlob}`);
|
|
1839
|
+
}
|
|
1840
|
+
void processAccumulatedEventsInfinitePass(dir);
|
|
1841
|
+
}
|
|
1842
|
+
function cmdInit(args) {
|
|
1843
|
+
const dir = args[0];
|
|
1844
|
+
if (!dir) {
|
|
1845
|
+
throw new Error("Usage: board-live-cards init <dir> [--task-executor <script>] [--chat-handler <script>] [--runtime-out <dir>]");
|
|
1846
|
+
}
|
|
1847
|
+
const teIdx = args.indexOf("--task-executor");
|
|
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
|
+
}
|
|
1856
|
+
const result = initBoard(dir);
|
|
1857
|
+
if (taskExecutor) {
|
|
1858
|
+
fs.writeFileSync(path.join(dir, ".task-executor"), taskExecutor, "utf-8");
|
|
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));
|
|
1866
|
+
if (result === "exists") {
|
|
1867
|
+
console.log(`Board already initialized at ${path.resolve(dir)}${taskExecutor ? ` (task-executor updated: ${taskExecutor})` : ""} (runtime-out: ${runtimeOutDir})`);
|
|
1868
|
+
} else {
|
|
1869
|
+
console.log(`Board initialized at ${path.resolve(dir)}${taskExecutor ? ` (task-executor: ${taskExecutor})` : ""} (runtime-out: ${runtimeOutDir})`);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
function buildBoardStatusObject(dir, live) {
|
|
1873
|
+
const taskState = live.state.tasks;
|
|
1874
|
+
const taskConfig = live.config.tasks;
|
|
1875
|
+
const cardNames = Object.keys(taskState);
|
|
1876
|
+
const sched = schedule(live);
|
|
1877
|
+
const statusCounts = {
|
|
1878
|
+
completed: 0,
|
|
1879
|
+
failed: 0,
|
|
1880
|
+
in_progress: 0,
|
|
1881
|
+
pending: 0,
|
|
1882
|
+
blocked: 0,
|
|
1883
|
+
unresolved: 0
|
|
1884
|
+
};
|
|
1885
|
+
const waitingByCard = /* @__PURE__ */ new Map();
|
|
1886
|
+
for (const p of sched.pending) waitingByCard.set(p.taskName, p.waitingOn);
|
|
1887
|
+
for (const u of sched.unresolved) waitingByCard.set(u.taskName, u.missingTokens);
|
|
1888
|
+
for (const b of sched.blocked) waitingByCard.set(b.taskName, b.failedTokens);
|
|
1889
|
+
const dependentsByToken = /* @__PURE__ */ new Map();
|
|
1890
|
+
for (const [name, cfg] of Object.entries(taskConfig)) {
|
|
1891
|
+
for (const token of cfg.requires ?? []) {
|
|
1892
|
+
const dependents = dependentsByToken.get(token) ?? [];
|
|
1893
|
+
dependents.push(name);
|
|
1894
|
+
dependentsByToken.set(token, dependents);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
const cards = cardNames.sort().map((name) => {
|
|
1898
|
+
const state = taskState[name];
|
|
1899
|
+
const cfg = taskConfig[name] ?? { requires: [], provides: [] };
|
|
1900
|
+
if (state.status === "completed") statusCounts.completed += 1;
|
|
1901
|
+
else if (state.status === "failed") statusCounts.failed += 1;
|
|
1902
|
+
else if (state.status === "in-progress") statusCounts.in_progress += 1;
|
|
1903
|
+
const requires = cfg.requires ?? [];
|
|
1904
|
+
const provides = cfg.provides ?? [];
|
|
1905
|
+
const runtimeKeys = Object.keys(state.data ?? {}).sort();
|
|
1906
|
+
const requiresSatisfied = requires.filter((token) => live.state.availableOutputs.includes(token));
|
|
1907
|
+
const requiresMissing = requires.filter((token) => !live.state.availableOutputs.includes(token));
|
|
1908
|
+
const blockedBy = waitingByCard.get(name) ?? requiresMissing;
|
|
1909
|
+
const unblocks = /* @__PURE__ */ new Set();
|
|
1910
|
+
for (const token of provides) {
|
|
1911
|
+
for (const dependent of dependentsByToken.get(token) ?? []) {
|
|
1912
|
+
if (dependent !== name) unblocks.add(dependent);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
const lastFailureAt = state.failedAt;
|
|
1916
|
+
const error = state.error ? {
|
|
1917
|
+
message: state.error,
|
|
1918
|
+
code: "TASK_FAILED",
|
|
1919
|
+
at: lastFailureAt,
|
|
1920
|
+
source: "task-runtime"
|
|
1921
|
+
} : void 0;
|
|
1922
|
+
return {
|
|
1923
|
+
name,
|
|
1924
|
+
status: state.status,
|
|
1925
|
+
error,
|
|
1926
|
+
requires,
|
|
1927
|
+
requires_satisfied: requiresSatisfied,
|
|
1928
|
+
requires_missing: requiresMissing,
|
|
1929
|
+
provides_declared: provides,
|
|
1930
|
+
provides_runtime: runtimeKeys,
|
|
1931
|
+
blocked_by: blockedBy,
|
|
1932
|
+
unblocks: Array.from(unblocks).sort(),
|
|
1933
|
+
runtime: {
|
|
1934
|
+
attempt_count: state.executionCount ?? 0,
|
|
1935
|
+
restart_count: state.retryCount ?? 0,
|
|
1936
|
+
in_progress_since: state.status === "in-progress" ? state.startedAt ?? null : null,
|
|
1937
|
+
last_transition_at: state.lastUpdated ?? null,
|
|
1938
|
+
last_completed_at: state.completedAt ?? null,
|
|
1939
|
+
last_restarted_at: state.startedAt ?? null,
|
|
1940
|
+
status_age_ms: state.lastUpdated ? Math.max(0, Date.now() - Date.parse(state.lastUpdated)) : null
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
});
|
|
1944
|
+
statusCounts.pending = sched.pending.length;
|
|
1945
|
+
statusCounts.blocked = sched.blocked.length;
|
|
1946
|
+
statusCounts.unresolved = sched.unresolved.length;
|
|
1947
|
+
const fanOut = cards.map((c) => ({ name: c.name, fanOut: c.unblocks.length })).sort((a, b) => b.fanOut - a.fanOut || a.name.localeCompare(b.name));
|
|
1948
|
+
const maxFanOut = fanOut.length > 0 ? fanOut[0] : { name: null, fanOut: 0 };
|
|
1949
|
+
const allRequires = /* @__PURE__ */ new Set();
|
|
1950
|
+
for (const cfg of Object.values(taskConfig)) {
|
|
1951
|
+
for (const r of cfg.requires ?? []) allRequires.add(r);
|
|
1952
|
+
}
|
|
1953
|
+
let orphanCards = 0;
|
|
1954
|
+
for (const [name, cfg] of Object.entries(taskConfig)) {
|
|
1955
|
+
const requiresNone = (cfg.requires ?? []).length === 0;
|
|
1956
|
+
const provides = cfg.provides ?? [];
|
|
1957
|
+
const feedsAny = provides.some((p) => (dependentsByToken.get(p) ?? []).some((d) => d !== name));
|
|
1958
|
+
if (requiresNone && !feedsAny) orphanCards += 1;
|
|
1959
|
+
}
|
|
1960
|
+
return {
|
|
1961
|
+
schema_version: "v1",
|
|
1962
|
+
meta: {
|
|
1963
|
+
board: {
|
|
1964
|
+
path: path.resolve(dir)
|
|
1965
|
+
}
|
|
1966
|
+
},
|
|
1967
|
+
summary: {
|
|
1968
|
+
card_count: cardNames.length,
|
|
1969
|
+
completed: statusCounts.completed,
|
|
1970
|
+
eligible: sched.eligible.length,
|
|
1971
|
+
pending: statusCounts.pending,
|
|
1972
|
+
blocked: statusCounts.blocked,
|
|
1973
|
+
unresolved: statusCounts.unresolved,
|
|
1974
|
+
failed: statusCounts.failed,
|
|
1975
|
+
in_progress: statusCounts.in_progress,
|
|
1976
|
+
orphan_cards: orphanCards,
|
|
1977
|
+
topology: {
|
|
1978
|
+
edge_count: Array.from(allRequires).length,
|
|
1979
|
+
max_fan_out_card: maxFanOut.name,
|
|
1980
|
+
max_fan_out: maxFanOut.fanOut
|
|
1981
|
+
}
|
|
1982
|
+
},
|
|
1983
|
+
cards
|
|
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
|
+
}
|
|
2002
|
+
if (asJson) {
|
|
2003
|
+
console.log(JSON.stringify(statusObject, null, 2));
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
console.log(`Board: ${statusObject.meta.board.path}`);
|
|
2007
|
+
console.log(`Tasks: ${statusObject.summary.card_count}`);
|
|
2008
|
+
console.log("");
|
|
2009
|
+
for (const card of statusObject.cards) {
|
|
2010
|
+
const dataKeys = card.provides_runtime.join(", ");
|
|
2011
|
+
console.log(` ${card.status.padEnd(12)} ${card.name}${dataKeys ? ` \u2014 [${dataKeys}]` : ""}`);
|
|
2012
|
+
}
|
|
2013
|
+
console.log("");
|
|
2014
|
+
console.log(`Schedule: ${statusObject.summary.eligible} eligible, ${statusObject.summary.pending} pending, ${statusObject.summary.blocked} blocked, ${statusObject.summary.unresolved} unresolved`);
|
|
2015
|
+
}
|
|
2016
|
+
function cmdTaskCompleted(args) {
|
|
2017
|
+
const rgIdx = args.indexOf("--rg");
|
|
2018
|
+
const tokenIdx = args.indexOf("--token");
|
|
2019
|
+
const dataIdx = args.indexOf("--data");
|
|
2020
|
+
const dir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
|
|
2021
|
+
const token = tokenIdx !== -1 ? args[tokenIdx + 1] : void 0;
|
|
2022
|
+
if (!dir || !token) {
|
|
2023
|
+
console.error("Usage: board-live-cards task-completed --rg <dir> --token <token> [--data <json>]");
|
|
2024
|
+
process.exit(1);
|
|
2025
|
+
}
|
|
2026
|
+
const decoded = decodeCallbackToken2(token);
|
|
2027
|
+
if (!decoded) {
|
|
2028
|
+
console.error("Invalid callback token");
|
|
2029
|
+
process.exit(1);
|
|
2030
|
+
}
|
|
2031
|
+
const data = dataIdx !== -1 ? JSON.parse(args[dataIdx + 1]) : {};
|
|
2032
|
+
writeRuntimeDataObjects(dir, data);
|
|
2033
|
+
appendEventToJournal(dir, {
|
|
2034
|
+
type: "task-completed",
|
|
2035
|
+
taskName: decoded.taskName,
|
|
2036
|
+
data,
|
|
2037
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2038
|
+
});
|
|
2039
|
+
void processAccumulatedEventsForced(dir);
|
|
2040
|
+
console.log("Task completed.");
|
|
2041
|
+
}
|
|
2042
|
+
function cmdTaskFailed(args) {
|
|
2043
|
+
const rgIdx = args.indexOf("--rg");
|
|
2044
|
+
const tokenIdx = args.indexOf("--token");
|
|
2045
|
+
const errorIdx = args.indexOf("--error");
|
|
2046
|
+
const dir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
|
|
2047
|
+
const token = tokenIdx !== -1 ? args[tokenIdx + 1] : void 0;
|
|
2048
|
+
const errorMsg = errorIdx !== -1 ? args[errorIdx + 1] : "unknown error";
|
|
2049
|
+
if (!dir || !token) {
|
|
2050
|
+
console.error("Usage: board-live-cards task-failed --rg <dir> --token <token> [--error <message>]");
|
|
2051
|
+
process.exit(1);
|
|
2052
|
+
}
|
|
2053
|
+
const decoded = decodeCallbackToken2(token);
|
|
2054
|
+
if (!decoded) {
|
|
2055
|
+
console.error("Invalid callback token");
|
|
2056
|
+
process.exit(1);
|
|
2057
|
+
}
|
|
2058
|
+
appendEventToJournal(dir, {
|
|
2059
|
+
type: "task-failed",
|
|
2060
|
+
taskName: decoded.taskName,
|
|
2061
|
+
error: errorMsg,
|
|
2062
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2063
|
+
});
|
|
2064
|
+
void processAccumulatedEventsForced(dir);
|
|
2065
|
+
console.log("Task failed.");
|
|
2066
|
+
}
|
|
2067
|
+
function cmdRemoveCard(args) {
|
|
2068
|
+
const rgIdx = args.indexOf("--rg");
|
|
2069
|
+
const idIdx = args.indexOf("--id");
|
|
2070
|
+
const dir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
|
|
2071
|
+
const cardId = idIdx !== -1 ? args[idIdx + 1] : void 0;
|
|
2072
|
+
if (!dir || !cardId) {
|
|
2073
|
+
console.error("Usage: board-live-cards remove-card --rg <dir> --id <card-id>");
|
|
2074
|
+
process.exit(1);
|
|
2075
|
+
}
|
|
2076
|
+
appendEventToJournal(dir, {
|
|
2077
|
+
type: "task-removal",
|
|
2078
|
+
taskName: cardId,
|
|
2079
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2080
|
+
});
|
|
2081
|
+
void processAccumulatedEventsInfinitePass(dir);
|
|
2082
|
+
console.log(`Card "${cardId}" removed.`);
|
|
2083
|
+
}
|
|
2084
|
+
function cmdSourceDataFetched(args) {
|
|
2085
|
+
const tmpIdx = args.indexOf("--tmp");
|
|
2086
|
+
const tokenIdx = args.indexOf("--token");
|
|
2087
|
+
const tmpFile = tmpIdx !== -1 ? args[tmpIdx + 1] : void 0;
|
|
2088
|
+
const token = tokenIdx !== -1 ? args[tokenIdx + 1] : void 0;
|
|
2089
|
+
if (!tmpFile || !token) {
|
|
2090
|
+
console.error("Usage: board-live-cards source-data-fetched --tmp <tmp-file> --token <sourceToken>");
|
|
2091
|
+
process.exit(1);
|
|
2092
|
+
}
|
|
2093
|
+
const payload = decodeSourceToken(token);
|
|
2094
|
+
if (!payload) {
|
|
2095
|
+
console.error("Invalid source token");
|
|
2096
|
+
process.exit(1);
|
|
2097
|
+
}
|
|
2098
|
+
const { cbk, rg, cid, b, d } = payload;
|
|
2099
|
+
const destPath = path.join(rg, d);
|
|
2100
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
2101
|
+
fs.renameSync(tmpFile, destPath);
|
|
2102
|
+
console.log(`[source-data-fetched] ${cid}.${b} \u2192 ${d}`);
|
|
2103
|
+
const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2104
|
+
const cbkDecoded = decodeCallbackToken2(cbk);
|
|
2105
|
+
if (!cbkDecoded) {
|
|
2106
|
+
console.error("Invalid callback token embedded in source token");
|
|
2107
|
+
process.exit(1);
|
|
2108
|
+
}
|
|
2109
|
+
appendEventToJournal(rg, {
|
|
2110
|
+
type: "task-progress",
|
|
2111
|
+
taskName: cbkDecoded.taskName,
|
|
2112
|
+
update: { bindTo: b, fetchedAt, dest: d },
|
|
2113
|
+
timestamp: fetchedAt
|
|
2114
|
+
});
|
|
2115
|
+
void processAccumulatedEventsInfinitePass(rg);
|
|
2116
|
+
}
|
|
2117
|
+
function cmdSourceDataFetchFailure(args) {
|
|
2118
|
+
const tokenIdx = args.indexOf("--token");
|
|
2119
|
+
const reasonIdx = args.indexOf("--reason");
|
|
2120
|
+
const token = tokenIdx !== -1 ? args[tokenIdx + 1] : void 0;
|
|
2121
|
+
const reason = reasonIdx !== -1 ? args[reasonIdx + 1] : "unknown";
|
|
2122
|
+
if (!token) {
|
|
2123
|
+
console.error("Usage: board-live-cards source-data-fetch-failure --token <sourceToken> [--reason <msg>]");
|
|
2124
|
+
process.exit(1);
|
|
2125
|
+
}
|
|
2126
|
+
const payload = decodeSourceToken(token);
|
|
2127
|
+
if (!payload) {
|
|
2128
|
+
console.error("Invalid source token");
|
|
2129
|
+
process.exit(1);
|
|
2130
|
+
}
|
|
2131
|
+
const { cbk, rg, cid, b } = payload;
|
|
2132
|
+
console.log(`[source-data-fetch-failure] ${cid}.${b}: ${reason}`);
|
|
2133
|
+
const cbkDecoded = decodeCallbackToken2(cbk);
|
|
2134
|
+
if (!cbkDecoded) {
|
|
2135
|
+
console.error("Invalid callback token embedded in source token");
|
|
2136
|
+
process.exit(1);
|
|
2137
|
+
}
|
|
2138
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2139
|
+
appendEventToJournal(rg, {
|
|
2140
|
+
type: "task-progress",
|
|
2141
|
+
taskName: cbkDecoded.taskName,
|
|
2142
|
+
update: { bindTo: b, failure: true, reason },
|
|
2143
|
+
timestamp
|
|
2144
|
+
});
|
|
2145
|
+
void processAccumulatedEventsInfinitePass(rg);
|
|
2146
|
+
}
|
|
2147
|
+
function cmdRunSources(args) {
|
|
2148
|
+
const cardIdx = args.indexOf("--card");
|
|
2149
|
+
const tokenIdx = args.indexOf("--token");
|
|
2150
|
+
const rgIdx = args.indexOf("--rg");
|
|
2151
|
+
const cardFilePath = cardIdx !== -1 ? args[cardIdx + 1] : void 0;
|
|
2152
|
+
const callbackToken = tokenIdx !== -1 ? args[tokenIdx + 1] : void 0;
|
|
2153
|
+
const boardDir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
|
|
2154
|
+
if (!cardFilePath || !callbackToken || !boardDir) {
|
|
2155
|
+
console.error("Usage: board-live-cards run-sources-internal --card <path> --token <token> --rg <dir>");
|
|
2156
|
+
process.exit(1);
|
|
2157
|
+
}
|
|
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
|
+
}
|
|
2165
|
+
console.log(`[run-sources-internal] Processing card "${card.id}"`);
|
|
2166
|
+
const executorFile = path.join(boardDir, ".task-executor");
|
|
2167
|
+
const taskExecutor = fs.existsSync(executorFile) ? fs.readFileSync(executorFile, "utf-8").trim() : void 0;
|
|
2168
|
+
function runSource(src) {
|
|
2169
|
+
const sourceToken = encodeSourceToken({
|
|
2170
|
+
cbk: callbackToken,
|
|
2171
|
+
rg: boardDir,
|
|
2172
|
+
cid: card.id,
|
|
2173
|
+
b: src.bindTo,
|
|
2174
|
+
d: src.outputFile ?? ""
|
|
2175
|
+
});
|
|
2176
|
+
function reportFailure(reason) {
|
|
2177
|
+
invokeSourceDataFetchFailure(sourceToken, reason, (err) => {
|
|
2178
|
+
if (err) console.error(`[run-sources-internal] source-data-fetch-failure call failed:`, err.message);
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
function reportFetched(outFile2) {
|
|
2182
|
+
invokeSourceDataFetched(sourceToken, outFile2);
|
|
2183
|
+
}
|
|
2184
|
+
if (taskExecutor) {
|
|
2185
|
+
if (!src.outputFile) {
|
|
2186
|
+
console.warn(`[run-sources-internal] source "${src.bindTo}" has no outputFile configured \u2014 cannot deliver`);
|
|
2187
|
+
reportFailure("no outputFile configured");
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
const inFile = path.join(os.tmpdir(), `card-source-in-${src.bindTo}-${Date.now()}.json`);
|
|
2191
|
+
const outFile2 = path.join(os.tmpdir(), `card-source-out-${src.bindTo}-${Date.now()}.json`);
|
|
2192
|
+
const errFile = path.join(os.tmpdir(), `card-source-err-${src.bindTo}-${Date.now()}.txt`);
|
|
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);
|
|
2199
|
+
fs.writeFileSync(inFile, JSON.stringify(sourceForExecutor, null, 2), "utf-8");
|
|
2200
|
+
console.log(`[run-sources-internal] task-executor: ${taskExecutor} run-source-fetch --in ${inFile} --out ${outFile2} --err ${errFile}`);
|
|
2201
|
+
try {
|
|
2202
|
+
execCommandSync(taskExecutor, ["run-source-fetch", "--in", inFile, "--out", outFile2, "--err", errFile], {
|
|
2203
|
+
shell: true,
|
|
2204
|
+
timeout: src.timeout ?? 12e4
|
|
2205
|
+
});
|
|
2206
|
+
} catch (err) {
|
|
2207
|
+
const reason = err.message ?? String(err);
|
|
2208
|
+
console.error(`[run-sources-internal] task-executor failed for source "${src.bindTo}":`, reason);
|
|
2209
|
+
reportFailure(reason);
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
if (fs.existsSync(outFile2)) {
|
|
2213
|
+
reportFetched(outFile2);
|
|
2214
|
+
} else {
|
|
2215
|
+
const errMsg = fs.existsSync(errFile) ? fs.readFileSync(errFile, "utf-8").trim() : "executor produced no output file";
|
|
2216
|
+
console.warn(`[run-sources-internal] source "${src.bindTo}": ${errMsg}`);
|
|
2217
|
+
reportFailure(errMsg);
|
|
2218
|
+
}
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
if (!src.outputFile) {
|
|
2222
|
+
console.warn(`[run-sources-internal] source "${src.bindTo}" has no outputFile configured \u2014 cannot deliver`);
|
|
2223
|
+
reportFailure("no outputFile configured");
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
const outFile = path.join(os.tmpdir(), `card-source-out-${src.bindTo}-${Date.now()}.json`);
|
|
2227
|
+
if (!src.cli) {
|
|
2228
|
+
const errMsg = "source.cli is required for built-in source execution";
|
|
2229
|
+
console.warn(`[run-sources-internal] source "${src.bindTo}": ${errMsg}`);
|
|
2230
|
+
reportFailure(errMsg);
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
const timeout = src.timeout ?? 12e4;
|
|
2234
|
+
const sourceCwd = typeof src.cwd === "string" ? src.cwd : path.dirname(cardFilePath || "");
|
|
2235
|
+
const sourceBoardDir = typeof src.boardDir === "string" ? src.boardDir : boardDir;
|
|
2236
|
+
const cmdParts = splitCommandLine(src.cli);
|
|
2237
|
+
if (cmdParts.length === 0) {
|
|
2238
|
+
const errMsg = "source.cli command is empty";
|
|
2239
|
+
console.warn(`[run-sources-internal] source "${src.bindTo}": ${errMsg}`);
|
|
2240
|
+
reportFailure(errMsg);
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
const rawCmd = cmdParts[0];
|
|
2244
|
+
const cmd = /^(node|node\.exe)$/i.test(rawCmd) ? process.execPath : rawCmd;
|
|
2245
|
+
const cliArgs = cmdParts.slice(1);
|
|
2246
|
+
let stdout;
|
|
2247
|
+
try {
|
|
2248
|
+
stdout = execCommandSync(cmd, cliArgs, {
|
|
2249
|
+
shell: false,
|
|
2250
|
+
encoding: "utf-8",
|
|
2251
|
+
timeout,
|
|
2252
|
+
cwd: sourceCwd,
|
|
2253
|
+
env: {
|
|
2254
|
+
...process.env,
|
|
2255
|
+
...sourceBoardDir ? { BOARD_DIR: sourceBoardDir } : {}
|
|
2256
|
+
}
|
|
2257
|
+
});
|
|
2258
|
+
} catch (err) {
|
|
2259
|
+
const reason = err.message ?? String(err);
|
|
2260
|
+
console.error(`[run-sources-internal] source fetch failed for source "${src.bindTo}":`, reason);
|
|
2261
|
+
reportFailure(reason);
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
fs.writeFileSync(outFile, stdout.trim(), "utf-8");
|
|
2265
|
+
reportFetched(outFile);
|
|
2266
|
+
}
|
|
2267
|
+
const sources = card.sources ?? [];
|
|
2268
|
+
for (const src of sources) {
|
|
2269
|
+
runSource(src);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
function cmdRunSourceFetch(args) {
|
|
2273
|
+
const inIdx = args.indexOf("--in");
|
|
2274
|
+
const outIdx = args.indexOf("--out");
|
|
2275
|
+
const errIdx = args.indexOf("--err");
|
|
2276
|
+
const inFile = inIdx !== -1 ? args[inIdx + 1] : void 0;
|
|
2277
|
+
const outFile = outIdx !== -1 ? args[outIdx + 1] : void 0;
|
|
2278
|
+
const errFile = errIdx !== -1 ? args[errIdx + 1] : void 0;
|
|
2279
|
+
if (!inFile || !outFile) {
|
|
2280
|
+
console.error("Usage: board-live-cards run-source-fetch --in <source.json> --out <result.json> [--err <error.txt>]");
|
|
2281
|
+
process.exit(1);
|
|
2282
|
+
}
|
|
2283
|
+
if (!fs.existsSync(inFile)) {
|
|
2284
|
+
const msg = `Input file not found: ${inFile}`;
|
|
2285
|
+
if (errFile) fs.writeFileSync(errFile, msg);
|
|
2286
|
+
console.error(`[run-source-fetch] ${msg}`);
|
|
2287
|
+
process.exit(1);
|
|
2288
|
+
}
|
|
2289
|
+
let source;
|
|
2290
|
+
try {
|
|
2291
|
+
const raw = fs.readFileSync(inFile, "utf-8");
|
|
2292
|
+
source = JSON.parse(raw);
|
|
2293
|
+
} catch (err) {
|
|
2294
|
+
const msg = `Failed to parse input file: ${err.message}`;
|
|
2295
|
+
if (errFile) fs.writeFileSync(errFile, msg);
|
|
2296
|
+
console.error(`[run-source-fetch] ${msg}`);
|
|
2297
|
+
process.exit(1);
|
|
2298
|
+
}
|
|
2299
|
+
if (!source.cli) {
|
|
2300
|
+
const msg = "Source definition missing cli field (board-live-cards built-in executor only understands source.cli)";
|
|
2301
|
+
if (errFile) fs.writeFileSync(errFile, msg);
|
|
2302
|
+
console.error(`[run-source-fetch] ${msg}`);
|
|
2303
|
+
process.exit(1);
|
|
2304
|
+
}
|
|
2305
|
+
console.log(`[run-source-fetch] executing: ${source.cli}`);
|
|
2306
|
+
const timeout = source.timeout ?? 12e4;
|
|
2307
|
+
const sourceCwd = typeof source.cwd === "string" ? source.cwd : process.cwd();
|
|
2308
|
+
const sourceBoardDir = typeof source.boardDir === "string" ? source.boardDir : void 0;
|
|
2309
|
+
const cmdParts = splitCommandLine(source.cli);
|
|
2310
|
+
if (cmdParts.length === 0) {
|
|
2311
|
+
const msg = "Source cli command is empty";
|
|
2312
|
+
if (errFile) fs.writeFileSync(errFile, msg);
|
|
2313
|
+
console.error(`[run-source-fetch] ${msg}`);
|
|
2314
|
+
process.exit(1);
|
|
2315
|
+
}
|
|
2316
|
+
const rawCmd = cmdParts[0];
|
|
2317
|
+
const cmd = /^(node|node\.exe)$/i.test(rawCmd) ? process.execPath : rawCmd;
|
|
2318
|
+
const cliArgs = cmdParts.slice(1);
|
|
2319
|
+
let stdout;
|
|
2320
|
+
try {
|
|
2321
|
+
stdout = execCommandSync(cmd, cliArgs, {
|
|
2322
|
+
shell: false,
|
|
2323
|
+
encoding: "utf-8",
|
|
2324
|
+
timeout,
|
|
2325
|
+
cwd: sourceCwd,
|
|
2326
|
+
env: {
|
|
2327
|
+
...process.env,
|
|
2328
|
+
...sourceBoardDir ? { BOARD_DIR: sourceBoardDir } : {}
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
} catch (err) {
|
|
2332
|
+
const msg = err.message ?? String(err);
|
|
2333
|
+
console.error(`[run-source-fetch] cli failed: ${msg}`);
|
|
2334
|
+
if (errFile) fs.writeFileSync(errFile, msg);
|
|
2335
|
+
process.exit(1);
|
|
2336
|
+
}
|
|
2337
|
+
const result = stdout.trim();
|
|
2338
|
+
try {
|
|
2339
|
+
fs.writeFileSync(outFile, result);
|
|
2340
|
+
console.log(`[run-source-fetch] result written to ${outFile}`);
|
|
2341
|
+
} catch (err) {
|
|
2342
|
+
const msg = `Failed to write output file: ${err.message}`;
|
|
2343
|
+
console.error(`[run-source-fetch] ${msg}`);
|
|
2344
|
+
if (errFile) fs.writeFileSync(errFile, msg);
|
|
2345
|
+
process.exit(1);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
function cmdUpdateCard(args) {
|
|
2349
|
+
const rgIdx = args.indexOf("--rg");
|
|
2350
|
+
const idIdx = args.indexOf("--card-id");
|
|
2351
|
+
const restart = args.includes("--restart");
|
|
2352
|
+
const dir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
|
|
2353
|
+
const cardId = idIdx !== -1 ? args[idIdx + 1] : void 0;
|
|
2354
|
+
if (!dir || !cardId) {
|
|
2355
|
+
throw new Error("Usage: board-live-cards update-card --rg <dir> --card-id <card-id> [--restart]");
|
|
2356
|
+
}
|
|
2357
|
+
const cardPath = lookupCardPath(dir, cardId);
|
|
2358
|
+
if (!cardPath) {
|
|
2359
|
+
throw new Error(`Card "${cardId}" not found in inventory`);
|
|
2360
|
+
}
|
|
2361
|
+
if (!fs.existsSync(cardPath)) {
|
|
2362
|
+
throw new Error(`Card file not found: ${cardPath}`);
|
|
2363
|
+
}
|
|
2364
|
+
const card = JSON.parse(fs.readFileSync(cardPath, "utf-8"));
|
|
2365
|
+
const taskConfig = liveCardToTaskConfig(card);
|
|
2366
|
+
appendEventToJournal(dir, {
|
|
2367
|
+
type: "task-upsert",
|
|
2368
|
+
taskName: cardId,
|
|
2369
|
+
taskConfig,
|
|
2370
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2371
|
+
});
|
|
2372
|
+
if (restart) {
|
|
2373
|
+
appendEventToJournal(dir, {
|
|
2374
|
+
type: "task-restart",
|
|
2375
|
+
taskName: cardId,
|
|
2376
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
void processAccumulatedEventsInfinitePass(dir);
|
|
2380
|
+
console.log(`Card "${cardId}" updated${restart ? " (restarted)" : ""}.`);
|
|
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
|
+
}
|
|
2497
|
+
async function cmdTryDrain(args) {
|
|
2498
|
+
const rgIdx = args.indexOf("--rg");
|
|
2499
|
+
const inlineLoop = args.includes("--inline-loop");
|
|
2500
|
+
const boardDir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
|
|
2501
|
+
if (!boardDir) {
|
|
2502
|
+
console.error("Usage: board-live-cards process-accumulated-events --rg <dir>");
|
|
2503
|
+
process.exit(1);
|
|
2504
|
+
}
|
|
2505
|
+
await processAccumulatedEventsForced(boardDir, { inlineLoop });
|
|
2506
|
+
}
|
|
2507
|
+
function cmdRetrigger(args) {
|
|
2508
|
+
const rgIdx = args.indexOf("--rg");
|
|
2509
|
+
const taskIdx = args.indexOf("--task");
|
|
2510
|
+
const dir = rgIdx !== -1 ? args[rgIdx + 1] : void 0;
|
|
2511
|
+
const taskName = taskIdx !== -1 ? args[taskIdx + 1] : void 0;
|
|
2512
|
+
if (!dir || !taskName) {
|
|
2513
|
+
console.error("Usage: board-live-cards retrigger --rg <dir> --task <task-name>");
|
|
2514
|
+
process.exit(1);
|
|
2515
|
+
}
|
|
2516
|
+
appendEventToJournal(dir, {
|
|
2517
|
+
type: "task-restart",
|
|
2518
|
+
taskName,
|
|
2519
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2520
|
+
});
|
|
2521
|
+
void processAccumulatedEventsInfinitePass(dir);
|
|
2522
|
+
console.log(`Task "${taskName}" retriggered.`);
|
|
2523
|
+
}
|
|
2524
|
+
async function cli(argv) {
|
|
2525
|
+
const cmd = argv[0];
|
|
2526
|
+
const rest = argv.slice(1);
|
|
2527
|
+
switch (cmd) {
|
|
2528
|
+
case "help":
|
|
2529
|
+
case "--help":
|
|
2530
|
+
case "-h":
|
|
2531
|
+
return cmdHelp();
|
|
2532
|
+
case "init":
|
|
2533
|
+
return cmdInit(rest);
|
|
2534
|
+
case "status":
|
|
2535
|
+
return cmdStatus(rest);
|
|
2536
|
+
case "add-cards":
|
|
2537
|
+
return cmdAddCards(rest);
|
|
2538
|
+
case "update-card":
|
|
2539
|
+
return cmdUpdateCard(rest);
|
|
2540
|
+
case "upsert-card":
|
|
2541
|
+
return cmdUpsertCard(rest);
|
|
2542
|
+
case "remove-card":
|
|
2543
|
+
return cmdRemoveCard(rest);
|
|
2544
|
+
case "retrigger":
|
|
2545
|
+
return cmdRetrigger(rest);
|
|
2546
|
+
case "task-completed":
|
|
2547
|
+
return cmdTaskCompleted(rest);
|
|
2548
|
+
case "task-failed":
|
|
2549
|
+
return cmdTaskFailed(rest);
|
|
2550
|
+
case "source-data-fetched":
|
|
2551
|
+
return cmdSourceDataFetched(rest);
|
|
2552
|
+
case "source-data-fetch-failure":
|
|
2553
|
+
return cmdSourceDataFetchFailure(rest);
|
|
2554
|
+
case "run-sources-internal":
|
|
2555
|
+
return cmdRunSources(rest);
|
|
2556
|
+
case "run-source-fetch":
|
|
2557
|
+
return cmdRunSourceFetch(rest);
|
|
2558
|
+
case "process-accumulated-events":
|
|
2559
|
+
return await cmdTryDrain(rest);
|
|
2560
|
+
default:
|
|
2561
|
+
throw new Error(`Unknown command: ${cmd ?? "(none)"}`);
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
function cmdHelp() {
|
|
2565
|
+
console.log(`
|
|
2566
|
+
board-live-cards-cli \u2014 LiveCards board CLI
|
|
2567
|
+
|
|
2568
|
+
USAGE
|
|
2569
|
+
board-live-cards-cli <command> [options]
|
|
2570
|
+
|
|
2571
|
+
BOARD MANAGEMENT
|
|
2572
|
+
init <dir> [--task-executor <script>] [--runtime-out <dir>]
|
|
2573
|
+
Create a new board in <dir>.
|
|
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
|
|
2579
|
+
Re-running init on an existing board is safe; --task-executor updates the registration.
|
|
2580
|
+
|
|
2581
|
+
status --rg <dir> [--json]
|
|
2582
|
+
Read and print the published status snapshot from <runtime-out>/board-livegraph-status.json.
|
|
2583
|
+
--json emits the stable machine-readable status object.
|
|
2584
|
+
|
|
2585
|
+
CARD MANAGEMENT
|
|
2586
|
+
add-cards --rg <dir> (--card <card.json> | --card-glob <glob>)
|
|
2587
|
+
Add one card or many cards from a glob and trigger processing.
|
|
2588
|
+
--card adds one JSON file.
|
|
2589
|
+
--card-glob adds all matching files in deterministic order.
|
|
2590
|
+
Example glob: "examples/browser/boards/portfolio-tracker/cards/*.json"
|
|
2591
|
+
|
|
2592
|
+
update-card --rg <dir> --card-id <card-id> [--restart]
|
|
2593
|
+
Re-read the card JSON from disk and patch the board.
|
|
2594
|
+
--restart clears the task so it re-triggers from scratch.
|
|
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
|
+
|
|
2606
|
+
remove-card --rg <dir> --id <card-id>
|
|
2607
|
+
Remove a card and its task from the board.
|
|
2608
|
+
|
|
2609
|
+
retrigger --rg <dir> --task <task-name>
|
|
2610
|
+
Mark a task not-started and drain to re-trigger it.
|
|
2611
|
+
|
|
2612
|
+
TASK CALLBACKS (called by task executor scripts)
|
|
2613
|
+
task-completed --token <callbackToken> [--data <json>]
|
|
2614
|
+
Signal successful task completion with optional JSON result data.
|
|
2615
|
+
|
|
2616
|
+
task-failed --token <callbackToken> [--error <message>]
|
|
2617
|
+
Signal task failure with an optional error message.
|
|
2618
|
+
|
|
2619
|
+
SOURCE CALLBACKS (called internally by run-sources-internal)
|
|
2620
|
+
source-data-fetched --tmp <file> --token <sourceToken>
|
|
2621
|
+
Atomically rename <file> into the outputFile destination and record delivery
|
|
2622
|
+
in runtime.json. Appends a task-progress event to re-invoke the card handler.
|
|
2623
|
+
|
|
2624
|
+
source-data-fetch-failure --token <sourceToken> [--reason <message>]
|
|
2625
|
+
Record a source fetch failure in runtime.json and append a task-progress event.
|
|
2626
|
+
|
|
2627
|
+
INTERNAL COMMANDS
|
|
2628
|
+
process-accumulated-events --rg <dir>
|
|
2629
|
+
Executes forced drain for this board.
|
|
2630
|
+
This command is also used as the background relay worker.
|
|
2631
|
+
By default it schedules a detached worker and returns quickly.
|
|
2632
|
+
Internal workers run with --inline-loop to perform the settle loop.
|
|
2633
|
+
|
|
2634
|
+
Eventual-progress guarantee is relay-based (not per-call blocking guarantee):
|
|
2635
|
+
1) at least one runner continues processing,
|
|
2636
|
+
2) no crash/forced exit in relay window,
|
|
2637
|
+
3) lock stays healthy,
|
|
2638
|
+
4) event production eventually quiesces.
|
|
2639
|
+
|
|
2640
|
+
run-sources-internal-internal --card <card.json> --token <callbackToken> --rg <dir>
|
|
2641
|
+
Execute all source[] entries for a card, then report delivery or failure.
|
|
2642
|
+
(Internal command \u2014 invoked by the card-handler. Not intended for direct use.)
|
|
2643
|
+
|
|
2644
|
+
If <dir>/.task-executor exists, invokes it with run-source-fetch subcommand:
|
|
2645
|
+
<executor> run-source-fetch --in <source_json> --out <outfile> --err <errfile>
|
|
2646
|
+
|
|
2647
|
+
If no .task-executor is registered, uses board-live-cards built-in run-source-fetch.
|
|
2648
|
+
|
|
2649
|
+
run-source-fetch --in <source.json> --out <result.json> [--err <error.txt>]
|
|
2650
|
+
Execute a source definition. Board-live-cards reads source.cli and executes it.
|
|
2651
|
+
Writes result to --out. Presence of --out after exit indicates success.
|
|
2652
|
+
|
|
2653
|
+
RUN-SOURCE-FETCH PROTOCOL
|
|
2654
|
+
External task-executors implement:
|
|
2655
|
+
<executor> run-source-fetch --in <source.json> --out <result.json> [--err <error.txt>]
|
|
2656
|
+
|
|
2657
|
+
INPUT: --in file contains the full sources[x] definition object
|
|
2658
|
+
OUTPUT: --out file is written with the result to signal success.
|
|
2659
|
+
--err file may be written to explain failure.
|
|
2660
|
+
|
|
2661
|
+
Exit code and --out presence determine success:
|
|
2662
|
+
Exit 0 + --out file present \u2192 source delivery recorded, card re-evaluated.
|
|
2663
|
+
Exit non-zero OR --out absent \u2192 source-data-fetch-failure recorded.
|
|
2664
|
+
|
|
2665
|
+
BOARD-LIVE-CARDS BUILT-IN EXECUTOR
|
|
2666
|
+
Understands source.cli field only:
|
|
2667
|
+
"sources": [{ "cli": "node ../fetch-prices.js", "bindTo": "prices", "outputFile": "prices.json" }]
|
|
2668
|
+
|
|
2669
|
+
The source.cli command is executed with:
|
|
2670
|
+
- Direct command invocation (no shell; quote-aware argument parsing)
|
|
2671
|
+
- Stdout is captured and delivered to the card as-is
|
|
2672
|
+
- Timeout from source.timeout (default 120s)
|
|
2673
|
+
|
|
2674
|
+
The source.cli command must:
|
|
2675
|
+
- Execute successfully (exit 0)
|
|
2676
|
+
- Write output to stdout
|
|
2677
|
+
- Complete within the timeout
|
|
2678
|
+
|
|
2679
|
+
The output format is the concern of the card's compute function to interpret.
|
|
2680
|
+
|
|
2681
|
+
External task-executors can interpret source definitions however they want.
|
|
2682
|
+
|
|
2683
|
+
EXAMPLES
|
|
2684
|
+
board-live-cards-cli init ./my-board
|
|
2685
|
+
board-live-cards-cli init ./my-board --task-executor ./executors/my-runner.py
|
|
2686
|
+
board-live-cards-cli add-cards --rg ./my-board --card cards/prices.json
|
|
2687
|
+
board-live-cards-cli status --rg ./my-board
|
|
2688
|
+
board-live-cards-cli retrigger --rg ./my-board --task price-fetch
|
|
2689
|
+
`.trimStart());
|
|
2690
|
+
}
|
|
2691
|
+
var isMain = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"));
|
|
2692
|
+
if (isMain) {
|
|
2693
|
+
cli(process.argv.slice(2)).catch((err) => {
|
|
2694
|
+
const msg = err instanceof Error ? err.stack ?? err.message : String(err);
|
|
2695
|
+
console.error(msg);
|
|
2696
|
+
process.exit(1);
|
|
2697
|
+
});
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
export { BoardJournal, appendCardInventory, appendEventToJournal, buildCardInventoryIndex, cli, createBoardReactiveGraph, decodeSourceToken, encodeSourceToken, getUndrainedEntries, initBoard, liveCardToTaskConfig, loadBoard, loadBoardEnvelope, lookupCardPath, processAccumulatedEvents, processAccumulatedEventsForced, processAccumulatedEventsInfinitePass, readCardInventory, saveBoard, withBoardLock };
|
|
2701
|
+
//# sourceMappingURL=board-live-cards-cli.js.map
|
|
2702
|
+
//# sourceMappingURL=board-live-cards-cli.js.map
|