@ansible-flow/node 0.0.0-stage → 0.86.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/LICENSE +21 -0
- package/README.md +9 -2
- package/dist/ai/agents.d.ts +34 -0
- package/dist/ai/agents.js +16 -0
- package/dist/ai/agents.js.map +7 -0
- package/dist/ai/hostConfig.d.ts +44 -0
- package/dist/ai/hostConfig.js +120 -0
- package/dist/ai/hostConfig.js.map +7 -0
- package/dist/ai/hosts.d.ts +28 -0
- package/dist/ai/hosts.js +9 -0
- package/dist/ai/hosts.js.map +7 -0
- package/dist/ai/writers.d.ts +124 -0
- package/dist/ai/writers.js +75 -0
- package/dist/ai/writers.js.map +7 -0
- package/dist/autoLayout.d.ts +38 -0
- package/dist/autoLayout.js +145 -0
- package/dist/autoLayout.js.map +7 -0
- package/dist/awx/awxWorkflow.d.ts +47 -0
- package/dist/awx/awxWorkflow.js +17 -0
- package/dist/awx/awxWorkflow.js.map +7 -0
- package/dist/awx/client.d.ts +77 -0
- package/dist/awx/client.js +22 -0
- package/dist/awx/client.js.map +7 -0
- package/dist/catalog/bindingCheck.d.ts +4 -0
- package/dist/catalog/bindingCheck.js +69 -0
- package/dist/catalog/bindingCheck.js.map +7 -0
- package/dist/catalog/catalogIndex.d.ts +52 -0
- package/dist/catalog/catalogIndex.js +13 -0
- package/dist/catalog/catalogIndex.js.map +7 -0
- package/dist/catalog/catalogWorker.d.ts +17 -0
- package/dist/catalog/catalogWorker.js +38 -0
- package/dist/catalog/catalogWorker.js.map +7 -0
- package/dist/catalog/dumpFiles.d.ts +3 -0
- package/dist/catalog/dumpFiles.js +19 -0
- package/dist/catalog/dumpFiles.js.map +7 -0
- package/dist/chunks/chunk-2MZQHIXP.js +614 -0
- package/dist/chunks/chunk-2MZQHIXP.js.map +7 -0
- package/dist/chunks/chunk-4FHPTGI4.js +52 -0
- package/dist/chunks/chunk-4FHPTGI4.js.map +7 -0
- package/dist/chunks/chunk-A2NMQGQE.js +189 -0
- package/dist/chunks/chunk-A2NMQGQE.js.map +7 -0
- package/dist/chunks/chunk-EPJKJBNW.js +181 -0
- package/dist/chunks/chunk-EPJKJBNW.js.map +7 -0
- package/dist/chunks/chunk-FG53MLUX.js +136 -0
- package/dist/chunks/chunk-FG53MLUX.js.map +7 -0
- package/dist/chunks/chunk-GAL5LW6Y.js +269 -0
- package/dist/chunks/chunk-GAL5LW6Y.js.map +7 -0
- package/dist/chunks/chunk-HPVT6I36.js +371 -0
- package/dist/chunks/chunk-HPVT6I36.js.map +7 -0
- package/dist/chunks/chunk-I6FIOP7G.js +48 -0
- package/dist/chunks/chunk-I6FIOP7G.js.map +7 -0
- package/dist/chunks/chunk-IIFP6UQR.js +44 -0
- package/dist/chunks/chunk-IIFP6UQR.js.map +7 -0
- package/dist/chunks/chunk-JDPN3LTT.js +18 -0
- package/dist/chunks/chunk-JDPN3LTT.js.map +7 -0
- package/dist/chunks/chunk-LGUG2Q7Z.js +191 -0
- package/dist/chunks/chunk-LGUG2Q7Z.js.map +7 -0
- package/dist/chunks/chunk-NTZRNEZM.js +25 -0
- package/dist/chunks/chunk-NTZRNEZM.js.map +7 -0
- package/dist/chunks/chunk-RETJPSKL.js +123 -0
- package/dist/chunks/chunk-RETJPSKL.js.map +7 -0
- package/dist/chunks/chunk-SSZNBJZQ.js +52 -0
- package/dist/chunks/chunk-SSZNBJZQ.js.map +7 -0
- package/dist/chunks/chunk-SUV3VHKR.js +52 -0
- package/dist/chunks/chunk-SUV3VHKR.js.map +7 -0
- package/dist/chunks/chunk-THNSGAKN.js +133 -0
- package/dist/chunks/chunk-THNSGAKN.js.map +7 -0
- package/dist/chunks/chunk-UNCR5JOZ.js +85 -0
- package/dist/chunks/chunk-UNCR5JOZ.js.map +7 -0
- package/dist/env/probeClient.d.ts +77 -0
- package/dist/env/probeClient.js +244 -0
- package/dist/env/probeClient.js.map +7 -0
- package/dist/env/userSettings.d.ts +33 -0
- package/dist/env/userSettings.js +101 -0
- package/dist/env/userSettings.js.map +7 -0
- package/dist/flowMerge.d.ts +35 -0
- package/dist/flowMerge.js +74 -0
- package/dist/flowMerge.js.map +7 -0
- package/dist/run/audit.d.ts +2 -0
- package/dist/run/audit.js +10 -0
- package/dist/run/audit.js.map +7 -0
- package/dist/run/exec.d.ts +14 -0
- package/dist/run/exec.js +7 -0
- package/dist/run/exec.js.map +7 -0
- package/dist/run/facts.d.ts +39 -0
- package/dist/run/facts.js +96 -0
- package/dist/run/facts.js.map +7 -0
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +8 -0
- package/dist/run/ids.js.map +7 -0
- package/dist/run/pastRuns.d.ts +9 -0
- package/dist/run/pastRuns.js +13 -0
- package/dist/run/pastRuns.js.map +7 -0
- package/dist/run/runFlow.d.ts +78 -0
- package/dist/run/runFlow.js +20 -0
- package/dist/run/runFlow.js.map +7 -0
- package/dist/run/segments.d.ts +43 -0
- package/dist/run/segments.js +157 -0
- package/dist/run/segments.js.map +7 -0
- package/dist/run/stop.d.ts +3 -0
- package/dist/run/stop.js +7 -0
- package/dist/run/stop.js.map +7 -0
- package/dist/run/storage.d.ts +16 -0
- package/dist/run/storage.js +15 -0
- package/dist/run/storage.js.map +7 -0
- package/dist/run/tail.d.ts +5 -0
- package/dist/run/tail.js +7 -0
- package/dist/run/tail.js.map +7 -0
- package/dist/runner/client.d.ts +149 -0
- package/dist/runner/client.js +168 -0
- package/dist/runner/client.js.map +7 -0
- package/dist/runner/devBundle.d.ts +14 -0
- package/dist/runner/devBundle.js +78 -0
- package/dist/runner/devBundle.js.map +7 -0
- package/dist/scenario/cli.d.ts +39 -0
- package/dist/scenario/cli.js +125 -0
- package/dist/scenario/cli.js.map +7 -0
- package/dist/scenario/runner.d.ts +50 -0
- package/dist/scenario/runner.js +185 -0
- package/dist/scenario/runner.js.map +7 -0
- package/dist/sharedRefs.d.ts +22 -0
- package/dist/sharedRefs.js +114 -0
- package/dist/sharedRefs.js.map +7 -0
- package/dist/ssh/exec.d.ts +39 -0
- package/dist/ssh/exec.js +13 -0
- package/dist/ssh/exec.js.map +7 -0
- package/dist/ssh/remote.d.ts +40 -0
- package/dist/ssh/remote.js +25 -0
- package/dist/ssh/remote.js.map +7 -0
- package/dist/ssh/sshRun.d.ts +1 -0
- package/dist/ssh/sshRun.js +24 -0
- package/dist/ssh/sshRun.js.map +7 -0
- package/dist/ssh/sync.d.ts +22 -0
- package/dist/ssh/sync.js +12 -0
- package/dist/ssh/sync.js.map +7 -0
- package/dist/timeouts.d.ts +38 -0
- package/dist/timeouts.js +7 -0
- package/dist/timeouts.js.map +7 -0
- package/package.json +35 -4
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
followRunFile,
|
|
3
|
+
putRunFile,
|
|
4
|
+
removeRunDir,
|
|
5
|
+
syncWorkspace
|
|
6
|
+
} from "./chunk-RETJPSKL.js";
|
|
7
|
+
import {
|
|
8
|
+
keepRun,
|
|
9
|
+
runsDir
|
|
10
|
+
} from "./chunk-4FHPTGI4.js";
|
|
11
|
+
import {
|
|
12
|
+
tailFile
|
|
13
|
+
} from "./chunk-SSZNBJZQ.js";
|
|
14
|
+
import {
|
|
15
|
+
stopStarted
|
|
16
|
+
} from "./chunk-JDPN3LTT.js";
|
|
17
|
+
import {
|
|
18
|
+
appendAudit
|
|
19
|
+
} from "./chunk-NTZRNEZM.js";
|
|
20
|
+
import {
|
|
21
|
+
KEEP_RUNS,
|
|
22
|
+
dropVars,
|
|
23
|
+
pruneRuns
|
|
24
|
+
} from "./chunk-SUV3VHKR.js";
|
|
25
|
+
import {
|
|
26
|
+
startProcess
|
|
27
|
+
} from "./chunk-I6FIOP7G.js";
|
|
28
|
+
|
|
29
|
+
// src/node/run/runFlow.ts
|
|
30
|
+
import { existsSync } from "node:fs";
|
|
31
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { nodeDesigns } from "@ansible-flow/core/flow/semantic";
|
|
34
|
+
import { runProcess, splitInputs } from "@ansible-flow/core/run/command";
|
|
35
|
+
import { createSummarizer, summarizeRun } from "@ansible-flow/core/run/events";
|
|
36
|
+
import { redactLine, redactText, secretValues } from "@ansible-flow/core/run/redact";
|
|
37
|
+
var LIVE_MS = 500;
|
|
38
|
+
var STALL_MS = 2e4;
|
|
39
|
+
async function runFlow(request, hooks) {
|
|
40
|
+
const { id, mode, doc, code, startedAt, vaultPassword, partial } = request;
|
|
41
|
+
const fail = (error) => {
|
|
42
|
+
const run = { id, mode, state: "error", startedAt, error };
|
|
43
|
+
hooks.report(run);
|
|
44
|
+
return run;
|
|
45
|
+
};
|
|
46
|
+
const runDir = join(runsDir(request.storage, doc.flow.id), id);
|
|
47
|
+
await mkdir(runDir, { recursive: true });
|
|
48
|
+
const { plain, secret } = splitInputs(request.inputs);
|
|
49
|
+
const secrets = Object.keys(secret).length > 0;
|
|
50
|
+
await writeFile(join(runDir, "vars.json"), JSON.stringify(plain, null, 2), "utf8");
|
|
51
|
+
await pruneRuns(request.storage, doc.flow.id, request.keepRuns ?? KEEP_RUNS).catch(
|
|
52
|
+
() => void 0
|
|
53
|
+
);
|
|
54
|
+
const remote = request.env.kind === "ssh" ? request.env : void 0;
|
|
55
|
+
if (remote) {
|
|
56
|
+
const failed = await syncWorkspace(
|
|
57
|
+
remote,
|
|
58
|
+
{ root: request.root, probe: request.probeDir },
|
|
59
|
+
request.syncAlso ?? []
|
|
60
|
+
) ?? await putRunFile(remote, runDir, "vars.json", JSON.stringify(plain, null, 2));
|
|
61
|
+
if (failed) return fail(failed);
|
|
62
|
+
}
|
|
63
|
+
const spec = runProcess(
|
|
64
|
+
request.env,
|
|
65
|
+
{ root: request.root, probe: request.probeDir },
|
|
66
|
+
{
|
|
67
|
+
playbook: request.playbook,
|
|
68
|
+
inventory: request.inventory,
|
|
69
|
+
check: mode === "check",
|
|
70
|
+
runDir,
|
|
71
|
+
vault: vaultPassword !== void 0,
|
|
72
|
+
secrets,
|
|
73
|
+
hasConfig: existsSync(join(request.root, "ansible.cfg")),
|
|
74
|
+
callbacks: request.callbacks
|
|
75
|
+
},
|
|
76
|
+
`afd-run-${id.toLowerCase()}`
|
|
77
|
+
);
|
|
78
|
+
if (request.audit)
|
|
79
|
+
await appendAudit(request.storage, { event: "launch", run: id, ...request.audit });
|
|
80
|
+
const running = {
|
|
81
|
+
id,
|
|
82
|
+
mode,
|
|
83
|
+
state: "running",
|
|
84
|
+
startedAt,
|
|
85
|
+
environment: request.environment,
|
|
86
|
+
source: "local",
|
|
87
|
+
flowVersion: doc.flow.version,
|
|
88
|
+
design: nodeDesigns(doc),
|
|
89
|
+
...partial ? { partial } : {}
|
|
90
|
+
};
|
|
91
|
+
hooks.report(running);
|
|
92
|
+
const env = { ...spec.env };
|
|
93
|
+
if (spec.env.WSLENV && process.env.WSLENV)
|
|
94
|
+
env.WSLENV = `${process.env.WSLENV}:${spec.env.WSLENV}`;
|
|
95
|
+
if (vaultPassword !== void 0) env.AFD_VAULT_PASSWORD = vaultPassword;
|
|
96
|
+
if (secrets) env.AFD_SECRET_VARS = JSON.stringify(secret);
|
|
97
|
+
const t0 = Date.now();
|
|
98
|
+
const hidden = secretValues(vaultPassword ? { ...secret, "": vaultPassword } : secret);
|
|
99
|
+
const live = createSummarizer(code.routes);
|
|
100
|
+
const eventsFile = join(runDir, "events.jsonl");
|
|
101
|
+
const following = remote ? followRunFile(remote, runDir, "events.jsonl", eventsFile) : void 0;
|
|
102
|
+
let lastEvent = Date.now();
|
|
103
|
+
let changed = false;
|
|
104
|
+
let quiet = false;
|
|
105
|
+
const tail = tailFile(eventsFile, (lines) => {
|
|
106
|
+
for (const line of lines) live.push(redactLine(line, hidden));
|
|
107
|
+
lastEvent = Date.now();
|
|
108
|
+
changed = true;
|
|
109
|
+
quiet = false;
|
|
110
|
+
});
|
|
111
|
+
const ticker = setInterval(() => {
|
|
112
|
+
if (hooks.watchQuiet && !quiet && Date.now() - lastEvent > STALL_MS) {
|
|
113
|
+
quiet = true;
|
|
114
|
+
changed = true;
|
|
115
|
+
hooks.quiet?.();
|
|
116
|
+
}
|
|
117
|
+
if (!changed) return;
|
|
118
|
+
changed = false;
|
|
119
|
+
hooks.report({
|
|
120
|
+
...running,
|
|
121
|
+
summary: live.summary({ partial: true }),
|
|
122
|
+
...quiet ? { quiet } : {}
|
|
123
|
+
});
|
|
124
|
+
}, LIVE_MS);
|
|
125
|
+
let stopped = false;
|
|
126
|
+
const execute = hooks.execute ?? ((s, e) => startProcess(s, e, hooks.log ?? (() => {
|
|
127
|
+
})));
|
|
128
|
+
let execution;
|
|
129
|
+
try {
|
|
130
|
+
execution = await execute(spec, env, () => stopped);
|
|
131
|
+
} catch {
|
|
132
|
+
execution = { exit: Promise.resolve(void 0), terminate: () => void 0 };
|
|
133
|
+
}
|
|
134
|
+
const stop = async () => {
|
|
135
|
+
stopped = true;
|
|
136
|
+
await stopStarted(spec);
|
|
137
|
+
execution.terminate();
|
|
138
|
+
};
|
|
139
|
+
const onAbort = () => void stop();
|
|
140
|
+
if (request.signal?.aborted) onAbort();
|
|
141
|
+
else request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
142
|
+
const release = hooks.started?.(stop);
|
|
143
|
+
const rc = await execution.exit;
|
|
144
|
+
release?.();
|
|
145
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
146
|
+
clearInterval(ticker);
|
|
147
|
+
await following?.stop();
|
|
148
|
+
await tail.stop();
|
|
149
|
+
if (remote) await removeRunDir(remote, runDir);
|
|
150
|
+
await dropVars(runDir).catch(() => void 0);
|
|
151
|
+
await hooks.ended?.();
|
|
152
|
+
let text = "";
|
|
153
|
+
try {
|
|
154
|
+
text = await readFile(eventsFile, "utf8");
|
|
155
|
+
if (hidden.length > 0) {
|
|
156
|
+
text = redactText(text, hidden);
|
|
157
|
+
await writeFile(eventsFile, text, "utf8");
|
|
158
|
+
}
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
const summary = summarizeRun(text, code.routes);
|
|
162
|
+
hooks.finished?.({ stopped, rc, events: !!text.trim(), summary });
|
|
163
|
+
const done = {
|
|
164
|
+
...running,
|
|
165
|
+
state: rc === void 0 || stopped ? "error" : "done",
|
|
166
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
167
|
+
rc,
|
|
168
|
+
summary,
|
|
169
|
+
error: stopped ? "Stopped by the user" : rc === void 0 ? "The run could not be started" : text ? void 0 : "The run produced no events",
|
|
170
|
+
...stopped ? { errorCode: "cancelled" } : {}
|
|
171
|
+
};
|
|
172
|
+
if (request.audit)
|
|
173
|
+
await appendAudit(request.storage, {
|
|
174
|
+
event: "finish",
|
|
175
|
+
run: id,
|
|
176
|
+
rc,
|
|
177
|
+
ms: Date.now() - t0,
|
|
178
|
+
hosts: summary.hosts.length,
|
|
179
|
+
failed: Object.keys(summary.outcome).length + summary.failures.length
|
|
180
|
+
});
|
|
181
|
+
await keepRun(runDir, done);
|
|
182
|
+
hooks.report(done);
|
|
183
|
+
return done;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export {
|
|
187
|
+
LIVE_MS,
|
|
188
|
+
STALL_MS,
|
|
189
|
+
runFlow
|
|
190
|
+
};
|
|
191
|
+
//# sourceMappingURL=chunk-LGUG2Q7Z.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/node/run/runFlow.ts"],
|
|
4
|
+
"sourcesContent": ["// One run of a flow's generated code (AFD-271): what the extension's local run and a server's run\n// have in common. The caller has generated the code, resolved the environment, collected the\n// inputs and asked whatever it asks; from there this writes the run's folder, starts\n// ansible-playbook, follows its events into a view of the run, redacts and keeps the result.\n//\n// What differs between hosts is given as hooks: how the process is started (a VS Code terminal\n// task, or a plain child process), who is told that the run went quiet, what is cleaned up.\nimport { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { GenerateResult } from '../../core/engine/types';\nimport { nodeDesigns } from '../../core/flow/semantic';\nimport type { FlowDocument } from '../../core/flow/types';\nimport type { RunView } from '../../core/protocol';\nimport { type ProcessSpec, runProcess, splitInputs } from '../../core/run/command';\nimport type { Environment } from '../../core/run/environment';\nimport { createSummarizer, type RunSummary, summarizeRun } from '../../core/run/events';\nimport { redactLine, redactText, secretValues } from '../../core/run/redact';\nimport { followRunFile, putRunFile, removeRunDir, syncWorkspace } from '../ssh/remote';\nimport { appendAudit } from './audit';\nimport { type Execution, startProcess } from './exec';\nimport { keepRun, runsDir } from './pastRuns';\nimport { stopStarted } from './stop';\nimport { dropVars, KEEP_RUNS, pruneRuns } from './storage';\nimport { tailFile } from './tail';\n\n/** How often a live run updates its view, at most (AFD-81). */\nexport const LIVE_MS = 500;\n/** No new events for this long: the run may wait for input. */\nexport const STALL_MS = 20_000;\n\nexport interface RunFlowRequest {\n /** The run's id (`stamp()`), which names its folder. */\n id: string;\n startedAt: string;\n mode: 'check' | 'real';\n doc: FlowDocument;\n /** The code that runs: its routes map the events back to nodes. */\n code: GenerateResult;\n env: Environment;\n /** The environment's name for people (\"afd-ee:2.19 (docker)\"). */\n environment: string;\n /** Repository root on this machine. */\n root: string;\n /** Folder with afd_probe/ and callback_plugins/afd_events.py. */\n probeDir: string;\n /** The playbook to run, relative to `root`, with forward slashes. */\n playbook: string;\n inventory?: string;\n /** Where runs are kept: `<storage>/runs/<flow id>/<run id>/`, and the audit log. */\n storage: string;\n /** How many runs of the flow to keep (default `KEEP_RUNS`). */\n keepRuns?: number;\n /** Flow input values. Secret ones never reach a file. */\n inputs: Record<string, unknown>;\n vaultPassword?: string;\n /** The repository's own callbacks, kept running beside the run's (`repositoryCallbacks`). */\n callbacks?: { enabled: string[]; paths: string[] };\n /** AFD-244: the run is of a part of the flow; its code is in a folder of its own. */\n partial?: RunView['partial'];\n /** AFD-247: paths the SSH copy needs that Git does not list (a partial run's folder). */\n syncAlso?: string[];\n /** What the audit log says about the launch, besides the run's id. Never input values. */\n audit?: Record<string, unknown>;\n /** Stops the run when it aborts. */\n signal?: AbortSignal;\n}\n\nexport interface RunFlowHooks {\n /** The run's view: when it starts, as it goes (at most every `LIVE_MS`), and when it ends. */\n report(run: RunView): void;\n /**\n * Starts the process. Default: a child process whose output goes to `log`. `stopping` says the\n * run is being stopped, for a host whose process may end without an exit code then.\n */\n execute?(\n spec: ProcessSpec,\n env: Record<string, string>,\n stopping: () => boolean,\n ): Execution | Promise<Execution>;\n /** The default process's output, line by line. */\n log?(line: string): void;\n /** The run started; `stop` ends it. Returns what to call when the run has ended. */\n started?(stop: () => Promise<void>): (() => void) | undefined;\n /** Whether to watch for a run gone quiet (no event for `STALL_MS`). */\n watchQuiet?: boolean;\n /** The run went quiet. */\n quiet?(): void;\n /** The process ended; before its events are read. */\n ended?(): Promise<void>;\n /** How the run ended, before it is kept and reported. */\n finished?(end: {\n stopped: boolean;\n rc: number | undefined;\n events: boolean;\n summary: RunSummary;\n }): void;\n}\n\n/** Runs the flow's code and resolves with the finished run (also reported). Never rejects. */\nexport async function runFlow(request: RunFlowRequest, hooks: RunFlowHooks): Promise<RunView> {\n const { id, mode, doc, code, startedAt, vaultPassword, partial } = request;\n const fail = (error: string): RunView => {\n const run: RunView = { id, mode, state: 'error', startedAt, error };\n hooks.report(run);\n return run;\n };\n\n const runDir = join(runsDir(request.storage, doc.flow.id), id);\n await mkdir(runDir, { recursive: true });\n // Secret inputs travel in an environment variable only; vars.json goes when the run ends.\n const { plain, secret } = splitInputs(request.inputs);\n const secrets = Object.keys(secret).length > 0;\n await writeFile(join(runDir, 'vars.json'), JSON.stringify(plain, null, 2), 'utf8');\n await pruneRuns(request.storage, doc.flow.id, request.keepRuns ?? KEEP_RUNS).catch(\n () => undefined,\n );\n // AFD-247: an SSH host runs a copy of the repository: brought up to date now, with the code just\n // generated, and the inputs.\n const remote = request.env.kind === 'ssh' ? request.env : undefined;\n if (remote) {\n const failed =\n (await syncWorkspace(\n remote,\n { root: request.root, probe: request.probeDir },\n request.syncAlso ?? [],\n )) ?? (await putRunFile(remote, runDir, 'vars.json', JSON.stringify(plain, null, 2)));\n if (failed) return fail(failed);\n }\n const spec = runProcess(\n request.env,\n { root: request.root, probe: request.probeDir },\n {\n playbook: request.playbook,\n inventory: request.inventory,\n check: mode === 'check',\n runDir,\n vault: vaultPassword !== undefined,\n secrets,\n hasConfig: existsSync(join(request.root, 'ansible.cfg')),\n callbacks: request.callbacks,\n },\n `afd-run-${id.toLowerCase()}`,\n );\n\n if (request.audit)\n await appendAudit(request.storage, { event: 'launch', run: id, ...request.audit });\n const running: RunView = {\n id,\n mode,\n state: 'running',\n startedAt,\n environment: request.environment,\n source: 'local',\n flowVersion: doc.flow.version,\n design: nodeDesigns(doc),\n ...(partial ? { partial } : {}),\n };\n hooks.report(running);\n\n const env: Record<string, string> = { ...spec.env };\n // AFD-75: names WSL should take from Windows join the ones already set, if any.\n if (spec.env.WSLENV && process.env.WSLENV)\n env.WSLENV = `${process.env.WSLENV}:${spec.env.WSLENV}`;\n if (vaultPassword !== undefined) env.AFD_VAULT_PASSWORD = vaultPassword;\n if (secrets) env.AFD_SECRET_VARS = JSON.stringify(secret);\n\n const t0 = Date.now();\n // The events as they come: a partial summary at most every LIVE_MS, secrets out.\n // AFD-138: the vault password too, if a task prints it.\n const hidden = secretValues(vaultPassword ? { ...secret, '': vaultPassword } : secret);\n const live = createSummarizer(code.routes);\n const eventsFile = join(runDir, 'events.jsonl');\n // AFD-247: the events are written on the host; they are followed into the file here.\n const following = remote ? followRunFile(remote, runDir, 'events.jsonl', eventsFile) : undefined;\n let lastEvent = Date.now();\n let changed = false;\n let quiet = false;\n const tail = tailFile(eventsFile, (lines) => {\n for (const line of lines) live.push(redactLine(line, hidden));\n lastEvent = Date.now();\n changed = true;\n quiet = false;\n });\n const ticker = setInterval(() => {\n if (hooks.watchQuiet && !quiet && Date.now() - lastEvent > STALL_MS) {\n quiet = true;\n changed = true;\n hooks.quiet?.();\n }\n if (!changed) return;\n changed = false;\n hooks.report({\n ...running,\n summary: live.summary({ partial: true }),\n ...(quiet ? { quiet } : {}),\n });\n }, LIVE_MS);\n\n // Stopping ends the process; the run then ends as stopped, not as an error.\n let stopped = false;\n const execute = hooks.execute ?? ((s, e) => startProcess(s, e, hooks.log ?? (() => {})));\n let execution: Execution;\n try {\n execution = await execute(spec, env, () => stopped);\n } catch {\n execution = { exit: Promise.resolve(undefined), terminate: () => undefined };\n }\n const stop = async () => {\n stopped = true;\n // AFD-131: the container first, so the playbook stops changing hosts and writing events\n // before the redaction pass below reads them; the process then ends by itself.\n await stopStarted(spec);\n execution.terminate();\n };\n const onAbort = () => void stop();\n if (request.signal?.aborted) onAbort();\n else request.signal?.addEventListener('abort', onAbort, { once: true });\n const release = hooks.started?.(stop);\n const rc = await execution.exit;\n release?.();\n request.signal?.removeEventListener('abort', onAbort);\n clearInterval(ticker);\n await following?.stop();\n await tail.stop();\n if (remote) await removeRunDir(remote, runDir);\n\n await dropVars(runDir).catch(() => undefined);\n await hooks.ended?.();\n let text = '';\n try {\n text = await readFile(eventsFile, 'utf8');\n // The kept events hold no secret input value either (the output now comes with them).\n if (hidden.length > 0) {\n text = redactText(text, hidden);\n await writeFile(eventsFile, text, 'utf8');\n }\n } catch {}\n const summary = summarizeRun(text, code.routes);\n hooks.finished?.({ stopped, rc, events: !!text.trim(), summary });\n const done: RunView = {\n ...running,\n state: rc === undefined || stopped ? 'error' : 'done',\n finishedAt: new Date().toISOString(),\n rc,\n summary,\n error: stopped\n ? 'Stopped by the user'\n : rc === undefined\n ? 'The run could not be started'\n : text\n ? undefined\n : 'The run produced no events',\n ...(stopped ? { errorCode: 'cancelled' as const } : {}),\n };\n if (request.audit)\n await appendAudit(request.storage, {\n event: 'finish',\n run: id,\n rc,\n ms: Date.now() - t0,\n hosts: summary.hosts.length,\n failed: Object.keys(summary.outcome).length + summary.failures.length,\n });\n // AFD-245: kept with its events, so the run can be shown again later.\n await keepRun(runDir, done);\n hooks.report(done);\n return done;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
rotateLog
|
|
3
|
+
} from "./chunk-SUV3VHKR.js";
|
|
4
|
+
|
|
5
|
+
// src/node/run/audit.ts
|
|
6
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
var AUDIT_ROTATE_BYTES = 1024 * 1024;
|
|
9
|
+
var AUDIT_OLD_FILES = 2;
|
|
10
|
+
function auditFile(storage) {
|
|
11
|
+
return join(storage, "audit.jsonl");
|
|
12
|
+
}
|
|
13
|
+
async function appendAudit(storage, entry) {
|
|
14
|
+
await mkdir(storage, { recursive: true });
|
|
15
|
+
await rotateLog(auditFile(storage), AUDIT_ROTATE_BYTES, AUDIT_OLD_FILES);
|
|
16
|
+
const line = `${JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), ...entry })}
|
|
17
|
+
`;
|
|
18
|
+
await appendFile(auditFile(storage), line, "utf8");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
auditFile,
|
|
23
|
+
appendAudit
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=chunk-NTZRNEZM.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/node/run/audit.ts"],
|
|
4
|
+
"sourcesContent": ["// Local audit log of run launches (decision 18): one JSON object per line in a storage folder. No\n// secrets: input values and passwords are never written here. The log rotates at 1 MB and keeps\n// two older files (AFD-29).\nimport { appendFile, mkdir } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { rotateLog } from './storage';\n\n/** Size at which audit.jsonl moves to audit.1.jsonl; audit.1 and audit.2 are kept. */\nconst AUDIT_ROTATE_BYTES = 1024 * 1024;\nconst AUDIT_OLD_FILES = 2;\n\nexport function auditFile(storage: string): string {\n return join(storage, 'audit.jsonl');\n}\n\nexport async function appendAudit(storage: string, entry: Record<string, unknown>): Promise<void> {\n await mkdir(storage, { recursive: true });\n await rotateLog(auditFile(storage), AUDIT_ROTATE_BYTES, AUDIT_OLD_FILES);\n const line = `${JSON.stringify({ ts: new Date().toISOString(), ...entry })}\\n`;\n await appendFile(auditFile(storage), line, 'utf8');\n}\n"],
|
|
5
|
+
"mappings": ";;;;;AAGA,SAAS,YAAY,aAAa;AAClC,SAAS,YAAY;AAIrB,IAAM,qBAAqB,OAAO;AAClC,IAAM,kBAAkB;AAEjB,SAAS,UAAU,SAAyB;AACjD,SAAO,KAAK,SAAS,aAAa;AACpC;AAEA,eAAsB,YAAY,SAAiB,OAA+C;AAChG,QAAM,MAAM,SAAS,EAAE,WAAW,KAAK,CAAC;AACxC,QAAM,UAAU,UAAU,OAAO,GAAG,oBAAoB,eAAe;AACvE,QAAM,OAAO,GAAG,KAAK,UAAU,EAAE,KAAI,oBAAI,KAAK,GAAE,YAAY,GAAG,GAAG,MAAM,CAAC,CAAC;AAAA;AAC1E,QAAM,WAAW,UAAU,OAAO,GAAG,MAAM,MAAM;AACnD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {
|
|
2
|
+
syncFolder
|
|
3
|
+
} from "./chunk-THNSGAKN.js";
|
|
4
|
+
import {
|
|
5
|
+
pipe,
|
|
6
|
+
run,
|
|
7
|
+
sshWhy,
|
|
8
|
+
tarExe
|
|
9
|
+
} from "./chunk-FG53MLUX.js";
|
|
10
|
+
|
|
11
|
+
// src/node/ssh/remote.ts
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname } from "node:path";
|
|
15
|
+
import {
|
|
16
|
+
checkCommand,
|
|
17
|
+
fetchDirCommand,
|
|
18
|
+
followCommand,
|
|
19
|
+
openDirCommand,
|
|
20
|
+
parseHostFacts,
|
|
21
|
+
readFileCommand,
|
|
22
|
+
remotePaths,
|
|
23
|
+
removeCommand,
|
|
24
|
+
writeFileCommand
|
|
25
|
+
} from "@ansible-flow/core/run/ssh";
|
|
26
|
+
async function syncWorkspace(env, paths, include = []) {
|
|
27
|
+
for (const [from, into] of [
|
|
28
|
+
[paths.probe, "probe"],
|
|
29
|
+
[paths.root, "repo"]
|
|
30
|
+
]) {
|
|
31
|
+
const r = await syncFolder(env, from, into, into === "repo" ? include : []);
|
|
32
|
+
if (!r.ok) return r.error;
|
|
33
|
+
}
|
|
34
|
+
return void 0;
|
|
35
|
+
}
|
|
36
|
+
async function putRunFile(env, runDir, name, text) {
|
|
37
|
+
const there = remotePaths(env, runDir).run;
|
|
38
|
+
const r = await run(writeFileCommand(env, `${there}/${name}`, !!env.container), { input: text });
|
|
39
|
+
return r.code === 0 ? void 0 : `Could not write ${name} on ${env.host}: ${sshWhy(r.stderr)}`;
|
|
40
|
+
}
|
|
41
|
+
function followRunFile(env, runDir, name, local) {
|
|
42
|
+
const there = `${remotePaths(env, runDir).run}/${name}`;
|
|
43
|
+
mkdirSync(dirname(local), { recursive: true });
|
|
44
|
+
writeFileSync(local, "");
|
|
45
|
+
const c = followCommand(env, there);
|
|
46
|
+
let proc;
|
|
47
|
+
try {
|
|
48
|
+
proc = spawn(c.command, c.args, { stdio: ["ignore", "pipe", "ignore"], windowsHide: true });
|
|
49
|
+
proc.stdout?.on("data", (d) => {
|
|
50
|
+
try {
|
|
51
|
+
appendFileSync(local, d);
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
proc.on("error", () => void 0);
|
|
56
|
+
} catch {
|
|
57
|
+
proc = void 0;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
async stop() {
|
|
61
|
+
proc?.kill();
|
|
62
|
+
const whole = await run(readFileCommand(env, there), { binary: true, timeoutMs: 6e4 });
|
|
63
|
+
if (whole.code === 0 && whole.out.length > 0) {
|
|
64
|
+
try {
|
|
65
|
+
writeFileSync(local, whole.out);
|
|
66
|
+
} catch {
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function putRunFolder(env, runDir, events) {
|
|
73
|
+
const there = remotePaths(env, runDir).run;
|
|
74
|
+
const r = await syncFolder(env, runDir, there.slice(env.dir.length + 1));
|
|
75
|
+
if (!r.ok) return r.error;
|
|
76
|
+
if (!events) return void 0;
|
|
77
|
+
const made = await run(openDirCommand(env, `${remotePaths(env, runDir).run}/${events}`));
|
|
78
|
+
return made.code === 0 ? void 0 : `Could not prepare the run on ${env.host}: ${sshWhy(made.stderr)}`;
|
|
79
|
+
}
|
|
80
|
+
async function fetchRunFile(env, runDir, name, local) {
|
|
81
|
+
const whole = await run(readFileCommand(env, `${remotePaths(env, runDir).run}/${name}`), {
|
|
82
|
+
binary: true,
|
|
83
|
+
timeoutMs: 6e4
|
|
84
|
+
});
|
|
85
|
+
if (whole.code !== 0) return;
|
|
86
|
+
mkdirSync(dirname(local), { recursive: true });
|
|
87
|
+
writeFileSync(local, whole.out);
|
|
88
|
+
}
|
|
89
|
+
async function removeRunDir(env, runDir) {
|
|
90
|
+
const there = remotePaths(env, runDir).run;
|
|
91
|
+
if (there) await run(removeCommand(env, there), { timeoutMs: 3e4 });
|
|
92
|
+
}
|
|
93
|
+
async function fetchCache(env, localCache) {
|
|
94
|
+
mkdirSync(localCache, { recursive: true });
|
|
95
|
+
const r = await pipe(
|
|
96
|
+
fetchDirCommand(env, remotePaths(env).cache),
|
|
97
|
+
{ command: tarExe(), args: ["-xf", "-", "-C", localCache] },
|
|
98
|
+
{ timeoutMs: 6e5 }
|
|
99
|
+
);
|
|
100
|
+
return r.from === 0 && r.to === 0 ? void 0 : `Could not fetch the catalog from ${env.host}: ${sshWhy(r.stderr)}`;
|
|
101
|
+
}
|
|
102
|
+
async function hostFacts(host) {
|
|
103
|
+
const r = await run(checkCommand(host), { timeoutMs: 3e4 });
|
|
104
|
+
if (r.code !== 0) return { error: `Could not connect to ${host}: ${sshWhy(r.stderr)}` };
|
|
105
|
+
return { facts: parseHostFacts(r.stdout) };
|
|
106
|
+
}
|
|
107
|
+
async function copyFile(env, content, remotePath) {
|
|
108
|
+
const r = await run(writeFileCommand(env, remotePath), { input: content });
|
|
109
|
+
return r.code === 0 ? void 0 : `Could not copy the file to ${env.host}: ${sshWhy(r.stderr)}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
syncWorkspace,
|
|
114
|
+
putRunFile,
|
|
115
|
+
followRunFile,
|
|
116
|
+
putRunFolder,
|
|
117
|
+
fetchRunFile,
|
|
118
|
+
removeRunDir,
|
|
119
|
+
fetchCache,
|
|
120
|
+
hostFacts,
|
|
121
|
+
copyFile
|
|
122
|
+
};
|
|
123
|
+
//# sourceMappingURL=chunk-RETJPSKL.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/node/ssh/remote.ts"],
|
|
4
|
+
"sourcesContent": ["// AFD-247: what a run on an SSH host needs around the run itself: its inputs put there, its\n// events followed as they are written, the folder removed after, the catalog's dumps fetched, and\n// a look at what the host has.\nimport { type ChildProcess, spawn } from 'node:child_process';\nimport { appendFileSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport type { Environment } from '../../core/run/environment';\nimport {\n checkCommand,\n fetchDirCommand,\n followCommand,\n type HostFacts,\n openDirCommand,\n parseHostFacts,\n readFileCommand,\n remotePaths,\n removeCommand,\n writeFileCommand,\n} from '../../core/run/ssh';\nimport { pipe, run, sshWhy, tarExe } from './exec';\nimport { syncFolder } from './sync';\n\nexport type SshEnv = Extract<Environment, { kind: 'ssh' }>;\n\n/** The repository and the probe on the host, as they are here now. */\nexport async function syncWorkspace(\n env: SshEnv,\n paths: { root: string; probe: string },\n /** Folders of the repository copied whatever .gitignore says. */\n include: readonly string[] = [],\n): Promise<string | undefined> {\n for (const [from, into] of [\n [paths.probe, 'probe'],\n [paths.root, 'repo'],\n ] as const) {\n const r = await syncFolder(env, from, into, into === 'repo' ? include : []);\n if (!r.ok) return r.error;\n }\n return undefined;\n}\n\n/** Puts a file of the run folder on the host (its inputs, a generated config). */\nexport async function putRunFile(\n env: SshEnv,\n runDir: string,\n name: string,\n text: string,\n): Promise<string | undefined> {\n const there = remotePaths(env, runDir).run;\n // A container's own user writes the events beside it.\n const r = await run(writeFileCommand(env, `${there}/${name}`, !!env.container), { input: text });\n return r.code === 0 ? undefined : `Could not write ${name} on ${env.host}: ${sshWhy(r.stderr)}`;\n}\n\n/**\n * Follows a file of the run folder on the host into the same file here, so what reads the local\n * one (the live summary on the canvas) sees the run as it goes. `stop` ends the following and\n * then writes the whole file once more: nothing the last moments wrote is lost.\n */\nexport function followRunFile(\n env: SshEnv,\n runDir: string,\n name: string,\n local: string,\n): { stop(): Promise<void> } {\n const there = `${remotePaths(env, runDir).run}/${name}`;\n mkdirSync(dirname(local), { recursive: true });\n writeFileSync(local, '');\n const c = followCommand(env, there);\n let proc: ChildProcess | undefined;\n try {\n proc = spawn(c.command, c.args, { stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });\n proc.stdout?.on('data', (d: Buffer) => {\n try {\n appendFileSync(local, d);\n } catch {\n // The run folder went away (pruned): the final read below has nowhere to write either.\n }\n });\n proc.on('error', () => undefined);\n } catch {\n proc = undefined;\n }\n return {\n async stop() {\n proc?.kill();\n const whole = await run(readFileCommand(env, there), { binary: true, timeoutMs: 60_000 });\n if (whole.code === 0 && whole.out.length > 0) {\n try {\n writeFileSync(local, whole.out);\n } catch {\n // As above.\n }\n }\n },\n };\n}\n\n/**\n * AFD-247: a scenario's folder (its build of the flow, inventory and vars) on the host, as the\n * run folder `runDir`. `events`: a folder in it for an image's own user to write the events to.\n */\nexport async function putRunFolder(\n env: SshEnv,\n runDir: string,\n events?: string,\n): Promise<string | undefined> {\n const there = remotePaths(env, runDir).run as string;\n const r = await syncFolder(env, runDir, there.slice(env.dir.length + 1));\n if (!r.ok) return r.error;\n if (!events) return undefined;\n const made = await run(openDirCommand(env, `${remotePaths(env, runDir).run}/${events}`));\n return made.code === 0\n ? undefined\n : `Could not prepare the run on ${env.host}: ${sshWhy(made.stderr)}`;\n}\n\n/** A file of the run folder on the host, read whole into the same file here. */\nexport async function fetchRunFile(\n env: SshEnv,\n runDir: string,\n name: string,\n local: string,\n): Promise<void> {\n const whole = await run(readFileCommand(env, `${remotePaths(env, runDir).run}/${name}`), {\n binary: true,\n timeoutMs: 60_000,\n });\n if (whole.code !== 0) return;\n mkdirSync(dirname(local), { recursive: true });\n writeFileSync(local, whole.out);\n}\n\n/** Removes the run's folder on the host (its inputs are not kept there). */\nexport async function removeRunDir(env: SshEnv, runDir: string): Promise<void> {\n const there = remotePaths(env, runDir).run;\n if (there) await run(removeCommand(env, there), { timeoutMs: 30_000 });\n}\n\n/** Brings the catalog dumps the probe wrote on the host into the cache here. */\nexport async function fetchCache(env: SshEnv, localCache: string): Promise<string | undefined> {\n mkdirSync(localCache, { recursive: true });\n const r = await pipe(\n fetchDirCommand(env, remotePaths(env).cache),\n { command: tarExe(), args: ['-xf', '-', '-C', localCache] },\n { timeoutMs: 600_000 },\n );\n return r.from === 0 && r.to === 0\n ? undefined\n : `Could not fetch the catalog from ${env.host}: ${sshWhy(r.stderr)}`;\n}\n\n/** What the host has; an error when it cannot be reached. */\nexport async function hostFacts(host: string): Promise<{ facts?: HostFacts; error?: string }> {\n const r = await run(checkCommand(host), { timeoutMs: 30_000 });\n if (r.code !== 0) return { error: `Could not connect to ${host}: ${sshWhy(r.stderr)}` };\n return { facts: parseHostFacts(r.stdout) };\n}\n\n/** Copies one file to the host, readable by the account alone (a vault password file). */\nexport async function copyFile(\n env: Pick<SshEnv, 'host' | 'dir'>,\n content: Buffer,\n remotePath: string,\n): Promise<string | undefined> {\n const r = await run(writeFileCommand(env, remotePath), { input: content });\n return r.code === 0 ? undefined : `Could not copy the file to ${env.host}: ${sshWhy(r.stderr)}`;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/node/run/tail.ts
|
|
2
|
+
import { open, stat } from "node:fs/promises";
|
|
3
|
+
import { splitLines } from "@ansible-flow/core/run/events";
|
|
4
|
+
function tailFile(path, onLines, intervalMs = 250) {
|
|
5
|
+
let offset = 0;
|
|
6
|
+
let carry = "";
|
|
7
|
+
let decoder = new TextDecoder();
|
|
8
|
+
let busy = Promise.resolve();
|
|
9
|
+
const read = async () => {
|
|
10
|
+
let size;
|
|
11
|
+
try {
|
|
12
|
+
size = (await stat(path)).size;
|
|
13
|
+
} catch {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (size < offset) {
|
|
17
|
+
offset = 0;
|
|
18
|
+
carry = "";
|
|
19
|
+
decoder = new TextDecoder();
|
|
20
|
+
}
|
|
21
|
+
if (size === offset) return;
|
|
22
|
+
const fh = await open(path, "r");
|
|
23
|
+
try {
|
|
24
|
+
const buf = Buffer.alloc(size - offset);
|
|
25
|
+
const { bytesRead } = await fh.read(buf, 0, buf.length, offset);
|
|
26
|
+
offset += bytesRead;
|
|
27
|
+
const r = splitLines(carry, decoder.decode(buf.subarray(0, bytesRead), { stream: true }));
|
|
28
|
+
carry = r.carry;
|
|
29
|
+
if (r.lines.length > 0) onLines(r.lines);
|
|
30
|
+
} finally {
|
|
31
|
+
await fh.close();
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const tick = () => {
|
|
35
|
+
busy = busy.then(read).catch(() => void 0);
|
|
36
|
+
return busy;
|
|
37
|
+
};
|
|
38
|
+
const timer = setInterval(tick, intervalMs);
|
|
39
|
+
return {
|
|
40
|
+
async stop() {
|
|
41
|
+
clearInterval(timer);
|
|
42
|
+
await tick();
|
|
43
|
+
if (carry.trim()) onLines([carry]);
|
|
44
|
+
carry = "";
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export {
|
|
50
|
+
tailFile
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=chunk-SSZNBJZQ.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/node/run/tail.ts"],
|
|
4
|
+
"sourcesContent": ["// AFD-81: events.jsonl read while the run writes it. Polling, not fs.watch: change events do not\n// reach a file on a Docker bind mount or under /mnt in WSL reliably on Windows.\nimport { open, stat } from 'node:fs/promises';\nimport { splitLines } from '../../core/run/events';\n\nexport interface Tail {\n /** One last read; a final line without a line break counts too. */\n stop(): Promise<void>;\n}\n\nexport function tailFile(path: string, onLines: (lines: string[]) => void, intervalMs = 250): Tail {\n let offset = 0;\n let carry = '';\n // A UTF-8 character cut between two reads waits for its other bytes.\n let decoder = new TextDecoder();\n let busy: Promise<void> = Promise.resolve();\n const read = async () => {\n let size: number;\n try {\n size = (await stat(path)).size;\n } catch {\n return; // not written yet\n }\n if (size < offset) {\n // Written again from the start.\n offset = 0;\n carry = '';\n decoder = new TextDecoder();\n }\n if (size === offset) return;\n const fh = await open(path, 'r');\n try {\n const buf = Buffer.alloc(size - offset);\n const { bytesRead } = await fh.read(buf, 0, buf.length, offset);\n offset += bytesRead;\n const r = splitLines(carry, decoder.decode(buf.subarray(0, bytesRead), { stream: true }));\n carry = r.carry;\n if (r.lines.length > 0) onLines(r.lines);\n } finally {\n await fh.close();\n }\n };\n const tick = () => {\n busy = busy.then(read).catch(() => undefined);\n return busy;\n };\n const timer = setInterval(tick, intervalMs);\n return {\n async stop() {\n clearInterval(timer);\n await tick();\n if (carry.trim()) onLines([carry]);\n carry = '';\n },\n };\n}\n"],
|
|
5
|
+
"mappings": ";AAEA,SAAS,MAAM,YAAY;AAC3B,SAAS,kBAAkB;AAOpB,SAAS,SAAS,MAAc,SAAoC,aAAa,KAAW;AACjG,MAAI,SAAS;AACb,MAAI,QAAQ;AAEZ,MAAI,UAAU,IAAI,YAAY;AAC9B,MAAI,OAAsB,QAAQ,QAAQ;AAC1C,QAAM,OAAO,YAAY;AACvB,QAAI;AACJ,QAAI;AACF,cAAQ,MAAM,KAAK,IAAI,GAAG;AAAA,IAC5B,QAAQ;AACN;AAAA,IACF;AACA,QAAI,OAAO,QAAQ;AAEjB,eAAS;AACT,cAAQ;AACR,gBAAU,IAAI,YAAY;AAAA,IAC5B;AACA,QAAI,SAAS,OAAQ;AACrB,UAAM,KAAK,MAAM,KAAK,MAAM,GAAG;AAC/B,QAAI;AACF,YAAM,MAAM,OAAO,MAAM,OAAO,MAAM;AACtC,YAAM,EAAE,UAAU,IAAI,MAAM,GAAG,KAAK,KAAK,GAAG,IAAI,QAAQ,MAAM;AAC9D,gBAAU;AACV,YAAM,IAAI,WAAW,OAAO,QAAQ,OAAO,IAAI,SAAS,GAAG,SAAS,GAAG,EAAE,QAAQ,KAAK,CAAC,CAAC;AACxF,cAAQ,EAAE;AACV,UAAI,EAAE,MAAM,SAAS,EAAG,SAAQ,EAAE,KAAK;AAAA,IACzC,UAAE;AACA,YAAM,GAAG,MAAM;AAAA,IACjB;AAAA,EACF;AACA,QAAM,OAAO,MAAM;AACjB,WAAO,KAAK,KAAK,IAAI,EAAE,MAAM,MAAM,MAAS;AAC5C,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,YAAY,MAAM,UAAU;AAC1C,SAAO;AAAA,IACL,MAAM,OAAO;AACX,oBAAc,KAAK;AACnB,YAAM,KAAK;AACX,UAAI,MAAM,KAAK,EAAG,SAAQ,CAAC,KAAK,CAAC;AACjC,cAAQ;AAAA,IACV;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/node/run/storage.ts
|
|
2
|
+
import { readdir, rename, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { isFileId } from "@ansible-flow/core/flow/ids";
|
|
5
|
+
var KEEP_RUNS = 30;
|
|
6
|
+
async function children(dir) {
|
|
7
|
+
try {
|
|
8
|
+
return (await readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
9
|
+
} catch {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function dropVars(runDir) {
|
|
14
|
+
await rm(join(runDir, "vars.json"), { force: true });
|
|
15
|
+
}
|
|
16
|
+
async function pruneRuns(storage, flowId, keep) {
|
|
17
|
+
if (!isFileId(flowId)) return;
|
|
18
|
+
const n = Number.isInteger(keep) && keep >= 1 ? keep : KEEP_RUNS;
|
|
19
|
+
const dir = join(storage, "runs", flowId);
|
|
20
|
+
const old = (await children(dir)).sort().slice(0, -n);
|
|
21
|
+
for (const run of old) await rm(join(dir, run), { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
async function sweepRuns(storage, keep) {
|
|
24
|
+
const root = join(storage, "runs");
|
|
25
|
+
for (const flow of await children(root)) {
|
|
26
|
+
for (const run of await children(join(root, flow)))
|
|
27
|
+
await rm(join(root, flow, run, "vars.json"), { force: true });
|
|
28
|
+
await pruneRuns(storage, flow, keep);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function rotateLog(file, limit, old) {
|
|
32
|
+
let size = 0;
|
|
33
|
+
try {
|
|
34
|
+
size = (await stat(file)).size;
|
|
35
|
+
} catch {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (size < limit) return;
|
|
39
|
+
const nth = (n) => file.replace(/\.jsonl$/, `.${n}.jsonl`);
|
|
40
|
+
await rm(nth(old), { force: true });
|
|
41
|
+
for (let n = old - 1; n >= 1; n--) await rename(nth(n), nth(n + 1)).catch(() => void 0);
|
|
42
|
+
await rename(file, nth(1));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
KEEP_RUNS,
|
|
47
|
+
dropVars,
|
|
48
|
+
pruneRuns,
|
|
49
|
+
sweepRuns,
|
|
50
|
+
rotateLog
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=chunk-SUV3VHKR.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/node/run/storage.ts"],
|
|
4
|
+
"sourcesContent": ["// What runs leave in global storage (AFD-29): `runs/<flow>/<run>/` keeps the events of the newest\n// runs only, and vars.json never outlives its run. Secret inputs never reach this folder at all.\n// No vscode import: paths are plain folders, so unit tests use a temporary one.\nimport { readdir, rename, rm, stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { isFileId } from '../../core/flow/ids';\n\n/** Run folders kept per flow when `afd.runs.keep` is not set. */\nexport const KEEP_RUNS = 30;\n\nasync function children(dir: string): Promise<string[]> {\n try {\n return (await readdir(dir, { withFileTypes: true }))\n .filter((e) => e.isDirectory())\n .map((e) => e.name);\n } catch {\n return [];\n }\n}\n\n/** Removes a run's vars.json once ansible-playbook is done with it. */\nexport async function dropVars(runDir: string): Promise<void> {\n await rm(join(runDir, 'vars.json'), { force: true });\n}\n\n/** Keeps the newest `keep` run folders of a flow (run ids are sortable time stamps). */\nexport async function pruneRuns(storage: string, flowId: string, keep: number): Promise<void> {\n // A flow ID that is not a ULID never names a folder here (AFD-133): the delete below is recursive.\n if (!isFileId(flowId)) return;\n const n = Number.isInteger(keep) && keep >= 1 ? keep : KEEP_RUNS;\n const dir = join(storage, 'runs', flowId);\n const old = (await children(dir)).sort().slice(0, -n);\n for (const run of old) await rm(join(dir, run), { recursive: true, force: true });\n}\n\n/**\n * At startup, when no run is going: removes the vars.json files earlier versions left behind (they\n * held every input, secrets included) and applies the retention to every flow.\n */\nexport async function sweepRuns(storage: string, keep: number): Promise<void> {\n const root = join(storage, 'runs');\n for (const flow of await children(root)) {\n for (const run of await children(join(root, flow)))\n await rm(join(root, flow, run, 'vars.json'), { force: true });\n await pruneRuns(storage, flow, keep);\n }\n}\n\n/**\n * Rotates `<name>.jsonl` once it reaches `limit` bytes: it becomes `<name>.1.jsonl`, the older ones\n * move up, and only `old` of them are kept.\n */\nexport async function rotateLog(file: string, limit: number, old: number): Promise<void> {\n let size = 0;\n try {\n size = (await stat(file)).size;\n } catch {\n return;\n }\n if (size < limit) return;\n const nth = (n: number) => file.replace(/\\.jsonl$/, `.${n}.jsonl`);\n await rm(nth(old), { force: true });\n for (let n = old - 1; n >= 1; n--) await rename(nth(n), nth(n + 1)).catch(() => undefined);\n await rename(file, nth(1));\n}\n"],
|
|
5
|
+
"mappings": ";AAGA,SAAS,SAAS,QAAQ,IAAI,YAAY;AAC1C,SAAS,YAAY;AACrB,SAAS,gBAAgB;AAGlB,IAAM,YAAY;AAEzB,eAAe,SAAS,KAAgC;AACtD,MAAI;AACF,YAAQ,MAAM,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC,GAC/C,OAAO,CAAC,MAAM,EAAE,YAAY,CAAC,EAC7B,IAAI,CAAC,MAAM,EAAE,IAAI;AAAA,EACtB,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACF;AAGA,eAAsB,SAAS,QAA+B;AAC5D,QAAM,GAAG,KAAK,QAAQ,WAAW,GAAG,EAAE,OAAO,KAAK,CAAC;AACrD;AAGA,eAAsB,UAAU,SAAiB,QAAgB,MAA6B;AAE5F,MAAI,CAAC,SAAS,MAAM,EAAG;AACvB,QAAM,IAAI,OAAO,UAAU,IAAI,KAAK,QAAQ,IAAI,OAAO;AACvD,QAAM,MAAM,KAAK,SAAS,QAAQ,MAAM;AACxC,QAAM,OAAO,MAAM,SAAS,GAAG,GAAG,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC;AACpD,aAAW,OAAO,IAAK,OAAM,GAAG,KAAK,KAAK,GAAG,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAClF;AAMA,eAAsB,UAAU,SAAiB,MAA6B;AAC5E,QAAM,OAAO,KAAK,SAAS,MAAM;AACjC,aAAW,QAAQ,MAAM,SAAS,IAAI,GAAG;AACvC,eAAW,OAAO,MAAM,SAAS,KAAK,MAAM,IAAI,CAAC;AAC/C,YAAM,GAAG,KAAK,MAAM,MAAM,KAAK,WAAW,GAAG,EAAE,OAAO,KAAK,CAAC;AAC9D,UAAM,UAAU,SAAS,MAAM,IAAI;AAAA,EACrC;AACF;AAMA,eAAsB,UAAU,MAAc,OAAe,KAA4B;AACvF,MAAI,OAAO;AACX,MAAI;AACF,YAAQ,MAAM,KAAK,IAAI,GAAG;AAAA,EAC5B,QAAQ;AACN;AAAA,EACF;AACA,MAAI,OAAO,MAAO;AAClB,QAAM,MAAM,CAAC,MAAc,KAAK,QAAQ,YAAY,IAAI,CAAC,QAAQ;AACjE,QAAM,GAAG,IAAI,GAAG,GAAG,EAAE,OAAO,KAAK,CAAC;AAClC,WAAS,IAAI,MAAM,GAAG,KAAK,GAAG,IAAK,OAAM,OAAO,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,EAAE,MAAM,MAAM,MAAS;AACzF,QAAM,OAAO,MAAM,IAAI,CAAC,CAAC;AAC3B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|