@ansible-flow/node 0.86.0 → 0.88.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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Ansible Flow Designer for Node hosts: layout, the probe client, runs, the AWX client, the module catalog and SSH.
4
4
 
5
- Built from [Ansible Flow Designer](https://github.com/ChistokhinSV/ansible-flow-designer) 0.86.0
5
+ Built from [Ansible Flow Designer](https://github.com/ChistokhinSV/ansible-flow-designer) 0.88.0
6
6
  and released with it. Modules are imported by their path in the source tree, for example
7
7
  `@ansible-flow/node/ai/agents`.
8
8
 
@@ -1,16 +1,19 @@
1
+ import {
2
+ tailFile
3
+ } from "./chunk-SSZNBJZQ.js";
1
4
  import {
2
5
  followRunFile,
3
6
  putRunFile,
4
7
  removeRunDir,
5
8
  syncWorkspace
6
9
  } from "./chunk-RETJPSKL.js";
10
+ import {
11
+ startProcess
12
+ } from "./chunk-I6FIOP7G.js";
7
13
  import {
8
14
  keepRun,
9
15
  runsDir
10
16
  } from "./chunk-4FHPTGI4.js";
11
- import {
12
- tailFile
13
- } from "./chunk-SSZNBJZQ.js";
14
17
  import {
15
18
  stopStarted
16
19
  } from "./chunk-JDPN3LTT.js";
@@ -22,16 +25,16 @@ import {
22
25
  dropVars,
23
26
  pruneRuns
24
27
  } from "./chunk-SUV3VHKR.js";
25
- import {
26
- startProcess
27
- } from "./chunk-I6FIOP7G.js";
28
28
 
29
29
  // src/node/run/runFlow.ts
30
30
  import { existsSync } from "node:fs";
31
31
  import { mkdir, readFile, writeFile } from "node:fs/promises";
32
32
  import { join } from "node:path";
33
33
  import { nodeDesigns } from "@ansible-flow/core/flow/semantic";
34
- import { runProcess, splitInputs } from "@ansible-flow/core/run/command";
34
+ import {
35
+ runProcess,
36
+ splitInputs
37
+ } from "@ansible-flow/core/run/command";
35
38
  import { createSummarizer, summarizeRun } from "@ansible-flow/core/run/events";
36
39
  import { redactLine, redactText, secretValues } from "@ansible-flow/core/run/redact";
37
40
  var LIVE_MS = 500;
@@ -188,4 +191,4 @@ export {
188
191
  STALL_MS,
189
192
  runFlow
190
193
  };
191
- //# sourceMappingURL=chunk-LGUG2Q7Z.js.map
194
+ //# sourceMappingURL=chunk-N5T2ZJ72.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 {\n type ProcessSpec,\n type RepositoryConfig,\n runProcess,\n splitInputs,\n} 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?: RepositoryConfig;\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,4 @@
1
+ import type { FlowDocument } from '@ansible-flow/core/flow/types';
2
+ import type { Finding } from '@ansible-flow/core/flow/validate';
3
+ /** Warnings for steps whose repository file is missing; none without a repository root. */
4
+ export declare function repoFileFindings(doc: FlowDocument, root?: string): Finding[];
@@ -0,0 +1,42 @@
1
+ // src/node/repoFiles.ts
2
+ import { existsSync } from "node:fs";
3
+ import { join, normalize, relative } from "node:path";
4
+ import { LOCAL_FILES, localFileOf, REPO_ROOT_VAR } from "@ansible-flow/core/flow/localFiles";
5
+ import { modelOf } from "@ansible-flow/core/flow/model";
6
+ import { shortName } from "@ansible-flow/core/import/builtins";
7
+ var ROOTED = new RegExp(`^\\{\\{\\s*${REPO_ROOT_VAR}\\s*\\}\\}/(?<path>[^{}%]+)$`);
8
+ function repoPathOf(module, params) {
9
+ const plain = localFileOf(module, params);
10
+ if (plain) return plain;
11
+ const spec = LOCAL_FILES[shortName(module).replace(/^ansible\.windows\./, "")];
12
+ if (!spec || !params || typeof params !== "object") return void 0;
13
+ let value = params[spec.param];
14
+ if (spec.param === "cmd" && typeof value === "string") value = value.trim().split(/\s+/)[0];
15
+ const path = typeof value === "string" ? ROOTED.exec(value.trim())?.groups?.path : void 0;
16
+ return path ? { param: spec.param, path } : void 0;
17
+ }
18
+ function repoFileFindings(doc, root) {
19
+ if (!root) return [];
20
+ const out = [];
21
+ for (const { node, path } of modelOf(doc).nodes.values()) {
22
+ const impl = node.impl;
23
+ if (impl?.kind !== "module" || !impl.ref) continue;
24
+ const file = repoPathOf(impl.ref, impl.params);
25
+ if (!file) continue;
26
+ const full = normalize(join(root, file.path));
27
+ if (relative(root, full).startsWith("..")) continue;
28
+ if (existsSync(full)) continue;
29
+ out.push({
30
+ code: "binding-file-missing",
31
+ severity: "warning",
32
+ message: `"${node.title}" reads ${file.path}, which is not in the repository: a relative path is from the repository root when the flow runs`,
33
+ path: [...path, "impl", "params", file.param],
34
+ node: node.id
35
+ });
36
+ }
37
+ return out;
38
+ }
39
+ export {
40
+ repoFileFindings
41
+ };
42
+ //# sourceMappingURL=repoFiles.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/node/repoFiles.ts"],
4
+ "sourcesContent": ["// AFD-291: a step that reads a repository file (a template's or copy's source, a script, a vars\n// file) whose file is not there. Generated plays read such a path from the repository root\n// (core/flow/localFiles.ts), so it is looked for there, written as the step has it or already\n// from `{{ afd_repo_root }}` (as import writes it).\nimport { existsSync } from 'node:fs';\nimport { join, normalize, relative } from 'node:path';\nimport { LOCAL_FILES, localFileOf, REPO_ROOT_VAR } from '../core/flow/localFiles';\nimport { modelOf } from '../core/flow/model';\nimport type { FlowDocument } from '../core/flow/types';\nimport type { Finding } from '../core/flow/validate';\nimport { shortName } from '../core/import/builtins';\n\nconst ROOTED = new RegExp(`^\\\\{\\\\{\\\\s*${REPO_ROOT_VAR}\\\\s*\\\\}\\\\}/(?<path>[^{}%]+)$`);\n\n/** The repository path a step's file param names, relative or from afd_repo_root; else none. */\nfunction repoPathOf(module: string, params: unknown): { param: string; path: string } | undefined {\n const plain = localFileOf(module, params);\n if (plain) return plain;\n const spec = LOCAL_FILES[shortName(module).replace(/^ansible\\.windows\\./, '')];\n if (!spec || !params || typeof params !== 'object') return undefined;\n let value = (params as Record<string, unknown>)[spec.param];\n if (spec.param === 'cmd' && typeof value === 'string') value = value.trim().split(/\\s+/)[0];\n const path = typeof value === 'string' ? ROOTED.exec(value.trim())?.groups?.path : undefined;\n return path ? { param: spec.param, path } : undefined;\n}\n\n/** Warnings for steps whose repository file is missing; none without a repository root. */\nexport function repoFileFindings(doc: FlowDocument, root?: string): Finding[] {\n if (!root) return [];\n const out: Finding[] = [];\n for (const { node, path } of modelOf(doc).nodes.values()) {\n const impl = node.impl;\n if (impl?.kind !== 'module' || !impl.ref) continue;\n const file = repoPathOf(impl.ref, impl.params);\n if (!file) continue;\n const full = normalize(join(root, file.path));\n // A path that climbs out of the repository is not checked here.\n if (relative(root, full).startsWith('..')) continue;\n if (existsSync(full)) continue;\n out.push({\n code: 'binding-file-missing',\n severity: 'warning',\n message: `\"${node.title}\" reads ${file.path}, which is not in the repository: a relative path is from the repository root when the flow runs`,\n path: [...path, 'impl', 'params', file.param],\n node: node.id,\n });\n }\n return out;\n}\n"],
5
+ "mappings": ";AAIA,SAAS,kBAAkB;AAC3B,SAAS,MAAM,WAAW,gBAAgB;AAC1C,SAAS,aAAa,aAAa,qBAAqB;AACxD,SAAS,eAAe;AAGxB,SAAS,iBAAiB;AAE1B,IAAM,SAAS,IAAI,OAAO,cAAc,aAAa,8BAA8B;AAGnF,SAAS,WAAW,QAAgB,QAA8D;AAChG,QAAM,QAAQ,YAAY,QAAQ,MAAM;AACxC,MAAI,MAAO,QAAO;AAClB,QAAM,OAAO,YAAY,UAAU,MAAM,EAAE,QAAQ,uBAAuB,EAAE,CAAC;AAC7E,MAAI,CAAC,QAAQ,CAAC,UAAU,OAAO,WAAW,SAAU,QAAO;AAC3D,MAAI,QAAS,OAAmC,KAAK,KAAK;AAC1D,MAAI,KAAK,UAAU,SAAS,OAAO,UAAU,SAAU,SAAQ,MAAM,KAAK,EAAE,MAAM,KAAK,EAAE,CAAC;AAC1F,QAAM,OAAO,OAAO,UAAU,WAAW,OAAO,KAAK,MAAM,KAAK,CAAC,GAAG,QAAQ,OAAO;AACnF,SAAO,OAAO,EAAE,OAAO,KAAK,OAAO,KAAK,IAAI;AAC9C;AAGO,SAAS,iBAAiB,KAAmB,MAA0B;AAC5E,MAAI,CAAC,KAAM,QAAO,CAAC;AACnB,QAAM,MAAiB,CAAC;AACxB,aAAW,EAAE,MAAM,KAAK,KAAK,QAAQ,GAAG,EAAE,MAAM,OAAO,GAAG;AACxD,UAAM,OAAO,KAAK;AAClB,QAAI,MAAM,SAAS,YAAY,CAAC,KAAK,IAAK;AAC1C,UAAM,OAAO,WAAW,KAAK,KAAK,KAAK,MAAM;AAC7C,QAAI,CAAC,KAAM;AACX,UAAM,OAAO,UAAU,KAAK,MAAM,KAAK,IAAI,CAAC;AAE5C,QAAI,SAAS,MAAM,IAAI,EAAE,WAAW,IAAI,EAAG;AAC3C,QAAI,WAAW,IAAI,EAAG;AACtB,QAAI,KAAK;AAAA,MACP,MAAM;AAAA,MACN,UAAU;AAAA,MACV,SAAS,IAAI,KAAK,KAAK,WAAW,KAAK,IAAI;AAAA,MAC3C,MAAM,CAAC,GAAG,MAAM,QAAQ,UAAU,KAAK,KAAK;AAAA,MAC5C,MAAM,KAAK;AAAA,IACb,CAAC;AAAA,EACH;AACA,SAAO;AACT;",
6
+ "names": []
7
+ }
@@ -1,4 +1,5 @@
1
1
  import type { FlowDocument } from '@ansible-flow/core/flow/types';
2
+ import type { RepositoryConfig } from '@ansible-flow/core/run/command';
2
3
  import { type LaunchFacts } from '@ansible-flow/core/run/launch';
3
4
  /** The probe of the Ansible environment a run uses, and the name people know it by. */
4
5
  export interface RunProbe {
@@ -33,7 +34,4 @@ export declare function inventoryGroups(probe: RunProbe, inventory: string | und
33
34
  * AFD-141: the callbacks the repository's ansible.cfg enables, so they keep running next to the
34
35
  * run's own; undefined when the environment does not answer (the run's callback alone, then).
35
36
  */
36
- export declare function repositoryCallbacks(probe: RunProbe): Promise<{
37
- enabled: string[];
38
- paths: string[];
39
- } | undefined>;
37
+ export declare function repositoryCallbacks(probe: RunProbe): Promise<RepositoryConfig | undefined>;
package/dist/run/facts.js CHANGED
@@ -82,11 +82,12 @@ async function inventoryGroups(probe, inventory) {
82
82
  }
83
83
  }
84
84
  async function repositoryCallbacks(probe) {
85
- return probe.client.call(
86
- "run_config",
87
- {},
88
- TIMEOUTS.probeInventory
89
- ).then((c) => ({ enabled: c.callbacks_enabled ?? [], paths: c.callback_plugins ?? [] })).catch(() => void 0);
85
+ return probe.client.call("run_config", {}, TIMEOUTS.probeInventory).then((c) => ({
86
+ enabled: c.callbacks_enabled ?? [],
87
+ paths: c.callback_plugins ?? [],
88
+ // AFD-291: an older probe says nothing of these: the run leaves the paths alone then.
89
+ ...c.plugin_paths ? { plugins: c.plugin_paths, varsEnabled: c.vars_enabled ?? [] } : {}
90
+ })).catch(() => void 0);
90
91
  }
91
92
  export {
92
93
  inventoryGroups,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/node/run/facts.ts"],
4
- "sourcesContent": ["// What a run would touch, asked of the inventory through the probe (the real-run confirmation and\n// the launch dialog, AFD-82). AFD-271: without VS Code, for every host that runs flows.\nimport { resolvePattern, targetUses } from '../../core/engine/targets';\nimport { buildModel } from '../../core/flow/model';\nimport type { FlowDocument } from '../../core/flow/types';\nimport { type LaunchFacts, type TargetUse, targetsOf } from '../../core/run/launch';\nimport { groupHosts, type InventoryTree } from '../../core/view/inventoryTree';\nimport { TIMEOUTS } from '../timeouts';\n\n/** The probe of the Ansible environment a run uses, and the name people know it by. */\nexport interface RunProbe {\n client: {\n call<T>(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;\n };\n label: string;\n}\n\ninterface InventoryFacts {\n sources: string[];\n count: number;\n hosts: string[];\n unknown: string[];\n prod_groups: string[];\n prod_hosts: string[];\n vault: boolean;\n /** Per group, how many of the matched hosts it has. */\n groups?: Record<string, number>;\n}\n\nasync function targetFacts(\n doc: FlowDocument,\n inputs: Record<string, unknown>,\n count: (pattern: string) => Promise<InventoryFacts>,\n): Promise<TargetUse[]> {\n return Promise.all(\n targetUses(buildModel(doc)).map(async (u): Promise<TargetUse> => {\n const pattern = resolvePattern(u.target, inputs);\n const use: TargetUse = { pattern, declared: u.target, nodes: u.nodes, prod: [] };\n if (!pattern) return use;\n try {\n const f = await count(pattern);\n use.count = f.count;\n use.prod = [...f.prod_groups, ...f.prod_hosts];\n } catch {\n // Counted at run time only; the confirmation says so.\n }\n return use;\n }),\n );\n}\n\n/**\n * What a run with these inputs would touch: the hosts the targets match in the inventory, the\n * inventory_targets of the flow's steps, what looks like production.\n */\nexport async function launchFacts(\n doc: FlowDocument,\n inputs: Record<string, unknown>,\n probe: RunProbe,\n inventory: string | undefined,\n): Promise<{ ok: true; facts: LaunchFacts } | { ok: false; error: string }> {\n const targets = targetsOf(doc, inputs);\n if (targets.length === 0)\n return { ok: false, error: 'No target hosts: the targets input is empty' };\n let found: InventoryFacts;\n try {\n found = await probe.client.call<InventoryFacts>(\n 'inventory',\n { inventory, targets },\n TIMEOUTS.probeInventory,\n );\n } catch (e) {\n return {\n ok: false,\n error: `The Ansible environment (${probe.label}) did not answer: ${(e as Error).message}`,\n };\n }\n if (found.count === 0)\n return { ok: false, error: `None of the targets (${targets.join(', ')}) is in the inventory` };\n const uses = await targetFacts(doc, inputs, (pattern) =>\n probe.client.call<InventoryFacts>(\n 'inventory',\n { inventory, targets: [pattern] },\n TIMEOUTS.probeInventory,\n ),\n );\n const prod = [\n ...new Set([...found.prod_groups, ...found.prod_hosts, ...uses.flatMap((u) => u.prod)]),\n ];\n return {\n ok: true,\n facts: {\n sources: found.sources,\n count: found.count,\n hosts: found.hosts,\n unknown: found.unknown,\n uses,\n prod,\n vault: found.vault,\n environment: probe.label,\n },\n };\n}\n\n/**\n * The inventory's groups, for the dialog's host list suggestions (AFD-82), with each group's hosts\n * (AFD-95, from the probe's inventory_tree, one inventory parse as before); none when it fails.\n */\nexport async function inventoryGroups(\n probe: RunProbe,\n inventory: string | undefined,\n): Promise<{ groups: string[]; hosts: Record<string, { count: number; hosts: string[] }> }> {\n try {\n const tree = await probe.client.call<InventoryTree>(\n 'inventory_tree',\n { inventory },\n TIMEOUTS.probeInventory,\n );\n const hosts = groupHosts(tree);\n return { groups: Object.keys(hosts).sort((a, b) => a.localeCompare(b)), hosts };\n } catch {\n return { groups: [], hosts: {} };\n }\n}\n\n/**\n * AFD-141: the callbacks the repository's ansible.cfg enables, so they keep running next to the\n * run's own; undefined when the environment does not answer (the run's callback alone, then).\n */\nexport async function repositoryCallbacks(\n probe: RunProbe,\n): Promise<{ enabled: string[]; paths: string[] } | undefined> {\n return probe.client\n .call<{ callbacks_enabled: string[]; callback_plugins: string[] }>(\n 'run_config',\n {},\n TIMEOUTS.probeInventory,\n )\n .then((c) => ({ enabled: c.callbacks_enabled ?? [], paths: c.callback_plugins ?? [] }))\n .catch(() => undefined);\n}\n"],
5
- "mappings": ";;;;;AAEA,SAAS,gBAAgB,kBAAkB;AAC3C,SAAS,kBAAkB;AAE3B,SAA2C,iBAAiB;AAC5D,SAAS,kBAAsC;AAuB/C,eAAe,YACb,KACA,QACA,OACsB;AACtB,SAAO,QAAQ;AAAA,IACb,WAAW,WAAW,GAAG,CAAC,EAAE,IAAI,OAAO,MAA0B;AAC/D,YAAM,UAAU,eAAe,EAAE,QAAQ,MAAM;AAC/C,YAAM,MAAiB,EAAE,SAAS,UAAU,EAAE,QAAQ,OAAO,EAAE,OAAO,MAAM,CAAC,EAAE;AAC/E,UAAI,CAAC,QAAS,QAAO;AACrB,UAAI;AACF,cAAM,IAAI,MAAM,MAAM,OAAO;AAC7B,YAAI,QAAQ,EAAE;AACd,YAAI,OAAO,CAAC,GAAG,EAAE,aAAa,GAAG,EAAE,UAAU;AAAA,MAC/C,QAAQ;AAAA,MAER;AACA,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF;AAMA,eAAsB,YACpB,KACA,QACA,OACA,WAC0E;AAC1E,QAAM,UAAU,UAAU,KAAK,MAAM;AACrC,MAAI,QAAQ,WAAW;AACrB,WAAO,EAAE,IAAI,OAAO,OAAO,8CAA8C;AAC3E,MAAI;AACJ,MAAI;AACF,YAAQ,MAAM,MAAM,OAAO;AAAA,MACzB;AAAA,MACA,EAAE,WAAW,QAAQ;AAAA,MACrB,SAAS;AAAA,IACX;AAAA,EACF,SAAS,GAAG;AACV,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO,4BAA4B,MAAM,KAAK,qBAAsB,EAAY,OAAO;AAAA,IACzF;AAAA,EACF;AACA,MAAI,MAAM,UAAU;AAClB,WAAO,EAAE,IAAI,OAAO,OAAO,wBAAwB,QAAQ,KAAK,IAAI,CAAC,wBAAwB;AAC/F,QAAM,OAAO,MAAM;AAAA,IAAY;AAAA,IAAK;AAAA,IAAQ,CAAC,YAC3C,MAAM,OAAO;AAAA,MACX;AAAA,MACA,EAAE,WAAW,SAAS,CAAC,OAAO,EAAE;AAAA,MAChC,SAAS;AAAA,IACX;AAAA,EACF;AACA,QAAM,OAAO;AAAA,IACX,GAAG,oBAAI,IAAI,CAAC,GAAG,MAAM,aAAa,GAAG,MAAM,YAAY,GAAG,KAAK,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAAA,EACxF;AACA,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,OAAO;AAAA,MACL,SAAS,MAAM;AAAA,MACf,OAAO,MAAM;AAAA,MACb,OAAO,MAAM;AAAA,MACb,SAAS,MAAM;AAAA,MACf;AAAA,MACA;AAAA,MACA,OAAO,MAAM;AAAA,MACb,aAAa,MAAM;AAAA,IACrB;AAAA,EACF;AACF;AAMA,eAAsB,gBACpB,OACA,WAC0F;AAC1F,MAAI;AACF,UAAM,OAAO,MAAM,MAAM,OAAO;AAAA,MAC9B;AAAA,MACA,EAAE,UAAU;AAAA,MACZ,SAAS;AAAA,IACX;AACA,UAAM,QAAQ,WAAW,IAAI;AAC7B,WAAO,EAAE,QAAQ,OAAO,KAAK,KAAK,EAAE,KAAK,CAAC,GAAG,MAAM,EAAE,cAAc,CAAC,CAAC,GAAG,MAAM;AAAA,EAChF,QAAQ;AACN,WAAO,EAAE,QAAQ,CAAC,GAAG,OAAO,CAAC,EAAE;AAAA,EACjC;AACF;AAMA,eAAsB,oBACpB,OAC6D;AAC7D,SAAO,MAAM,OACV;AAAA,IACC;AAAA,IACA,CAAC;AAAA,IACD,SAAS;AAAA,EACX,EACC,KAAK,CAAC,OAAO,EAAE,SAAS,EAAE,qBAAqB,CAAC,GAAG,OAAO,EAAE,oBAAoB,CAAC,EAAE,EAAE,EACrF,MAAM,MAAM,MAAS;AAC1B;",
4
+ "sourcesContent": ["// What a run would touch, asked of the inventory through the probe (the real-run confirmation and\n// the launch dialog, AFD-82). AFD-271: without VS Code, for every host that runs flows.\nimport { resolvePattern, targetUses } from '../../core/engine/targets';\nimport { buildModel } from '../../core/flow/model';\nimport type { FlowDocument } from '../../core/flow/types';\nimport type { RepositoryConfig } from '../../core/run/command';\nimport { type LaunchFacts, type TargetUse, targetsOf } from '../../core/run/launch';\nimport { groupHosts, type InventoryTree } from '../../core/view/inventoryTree';\nimport { TIMEOUTS } from '../timeouts';\n\n/** The probe of the Ansible environment a run uses, and the name people know it by. */\nexport interface RunProbe {\n client: {\n call<T>(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;\n };\n label: string;\n}\n\ninterface InventoryFacts {\n sources: string[];\n count: number;\n hosts: string[];\n unknown: string[];\n prod_groups: string[];\n prod_hosts: string[];\n vault: boolean;\n /** Per group, how many of the matched hosts it has. */\n groups?: Record<string, number>;\n}\n\nasync function targetFacts(\n doc: FlowDocument,\n inputs: Record<string, unknown>,\n count: (pattern: string) => Promise<InventoryFacts>,\n): Promise<TargetUse[]> {\n return Promise.all(\n targetUses(buildModel(doc)).map(async (u): Promise<TargetUse> => {\n const pattern = resolvePattern(u.target, inputs);\n const use: TargetUse = { pattern, declared: u.target, nodes: u.nodes, prod: [] };\n if (!pattern) return use;\n try {\n const f = await count(pattern);\n use.count = f.count;\n use.prod = [...f.prod_groups, ...f.prod_hosts];\n } catch {\n // Counted at run time only; the confirmation says so.\n }\n return use;\n }),\n );\n}\n\n/**\n * What a run with these inputs would touch: the hosts the targets match in the inventory, the\n * inventory_targets of the flow's steps, what looks like production.\n */\nexport async function launchFacts(\n doc: FlowDocument,\n inputs: Record<string, unknown>,\n probe: RunProbe,\n inventory: string | undefined,\n): Promise<{ ok: true; facts: LaunchFacts } | { ok: false; error: string }> {\n const targets = targetsOf(doc, inputs);\n if (targets.length === 0)\n return { ok: false, error: 'No target hosts: the targets input is empty' };\n let found: InventoryFacts;\n try {\n found = await probe.client.call<InventoryFacts>(\n 'inventory',\n { inventory, targets },\n TIMEOUTS.probeInventory,\n );\n } catch (e) {\n return {\n ok: false,\n error: `The Ansible environment (${probe.label}) did not answer: ${(e as Error).message}`,\n };\n }\n if (found.count === 0)\n return { ok: false, error: `None of the targets (${targets.join(', ')}) is in the inventory` };\n const uses = await targetFacts(doc, inputs, (pattern) =>\n probe.client.call<InventoryFacts>(\n 'inventory',\n { inventory, targets: [pattern] },\n TIMEOUTS.probeInventory,\n ),\n );\n const prod = [\n ...new Set([...found.prod_groups, ...found.prod_hosts, ...uses.flatMap((u) => u.prod)]),\n ];\n return {\n ok: true,\n facts: {\n sources: found.sources,\n count: found.count,\n hosts: found.hosts,\n unknown: found.unknown,\n uses,\n prod,\n vault: found.vault,\n environment: probe.label,\n },\n };\n}\n\n/**\n * The inventory's groups, for the dialog's host list suggestions (AFD-82), with each group's hosts\n * (AFD-95, from the probe's inventory_tree, one inventory parse as before); none when it fails.\n */\nexport async function inventoryGroups(\n probe: RunProbe,\n inventory: string | undefined,\n): Promise<{ groups: string[]; hosts: Record<string, { count: number; hosts: string[] }> }> {\n try {\n const tree = await probe.client.call<InventoryTree>(\n 'inventory_tree',\n { inventory },\n TIMEOUTS.probeInventory,\n );\n const hosts = groupHosts(tree);\n return { groups: Object.keys(hosts).sort((a, b) => a.localeCompare(b)), hosts };\n } catch {\n return { groups: [], hosts: {} };\n }\n}\n\n/**\n * AFD-141: the callbacks the repository's ansible.cfg enables, so they keep running next to the\n * run's own; undefined when the environment does not answer (the run's callback alone, then).\n */\nexport async function repositoryCallbacks(probe: RunProbe): Promise<RepositoryConfig | undefined> {\n return probe.client\n .call<{\n callbacks_enabled: string[];\n callback_plugins: string[];\n plugin_paths?: Record<string, string[]>;\n vars_enabled?: string[];\n }>('run_config', {}, TIMEOUTS.probeInventory)\n .then((c) => ({\n enabled: c.callbacks_enabled ?? [],\n paths: c.callback_plugins ?? [],\n // AFD-291: an older probe says nothing of these: the run leaves the paths alone then.\n ...(c.plugin_paths ? { plugins: c.plugin_paths, varsEnabled: c.vars_enabled ?? [] } : {}),\n }))\n .catch(() => undefined);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import type { GenerateResult } from '@ansible-flow/core/engine/types';
2
2
  import type { FlowDocument } from '@ansible-flow/core/flow/types';
3
3
  import type { RunView } from '@ansible-flow/core/protocol';
4
- import { type ProcessSpec } from '@ansible-flow/core/run/command';
4
+ import { type ProcessSpec, type RepositoryConfig } from '@ansible-flow/core/run/command';
5
5
  import type { Environment } from '@ansible-flow/core/run/environment';
6
6
  import { type RunSummary } from '@ansible-flow/core/run/events';
7
7
  import { type Execution } from './exec';
@@ -35,10 +35,7 @@ export interface RunFlowRequest {
35
35
  inputs: Record<string, unknown>;
36
36
  vaultPassword?: string;
37
37
  /** The repository's own callbacks, kept running beside the run's (`repositoryCallbacks`). */
38
- callbacks?: {
39
- enabled: string[];
40
- paths: string[];
41
- };
38
+ callbacks?: RepositoryConfig;
42
39
  /** AFD-244: the run is of a part of the flow; its code is in a folder of its own. */
43
40
  partial?: RunView['partial'];
44
41
  /** AFD-247: paths the SSH copy needs that Git does not list (a partial run's folder). */
@@ -2,16 +2,16 @@ import {
2
2
  LIVE_MS,
3
3
  STALL_MS,
4
4
  runFlow
5
- } from "../chunks/chunk-LGUG2Q7Z.js";
5
+ } from "../chunks/chunk-N5T2ZJ72.js";
6
+ import "../chunks/chunk-SSZNBJZQ.js";
6
7
  import "../chunks/chunk-RETJPSKL.js";
7
8
  import "../chunks/chunk-THNSGAKN.js";
8
9
  import "../chunks/chunk-FG53MLUX.js";
10
+ import "../chunks/chunk-I6FIOP7G.js";
9
11
  import "../chunks/chunk-4FHPTGI4.js";
10
- import "../chunks/chunk-SSZNBJZQ.js";
11
12
  import "../chunks/chunk-JDPN3LTT.js";
12
13
  import "../chunks/chunk-NTZRNEZM.js";
13
14
  import "../chunks/chunk-SUV3VHKR.js";
14
- import "../chunks/chunk-I6FIOP7G.js";
15
15
  export {
16
16
  LIVE_MS,
17
17
  STALL_MS,
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  runFlow
3
- } from "../chunks/chunk-LGUG2Q7Z.js";
3
+ } from "../chunks/chunk-N5T2ZJ72.js";
4
+ import "../chunks/chunk-SSZNBJZQ.js";
4
5
  import "../chunks/chunk-RETJPSKL.js";
5
6
  import "../chunks/chunk-THNSGAKN.js";
6
7
  import "../chunks/chunk-FG53MLUX.js";
8
+ import "../chunks/chunk-I6FIOP7G.js";
7
9
  import {
8
10
  runsDir
9
11
  } from "../chunks/chunk-4FHPTGI4.js";
10
- import "../chunks/chunk-SSZNBJZQ.js";
11
12
  import "../chunks/chunk-JDPN3LTT.js";
12
13
  import "../chunks/chunk-NTZRNEZM.js";
13
14
  import "../chunks/chunk-SUV3VHKR.js";
14
- import "../chunks/chunk-I6FIOP7G.js";
15
15
  import {
16
16
  withoutBookkeeping
17
17
  } from "../chunks/chunk-A2NMQGQE.js";
@@ -98,6 +98,27 @@ export interface LaunchRequest {
98
98
  export declare const RUNNER_API = "v1";
99
99
  /** Why this client and the server do not fit for a flow in `format`, or undefined. */
100
100
  export declare function incompatible(meta: RunnerMeta, format: number): string | undefined;
101
+ /** AFD-284: what happened to which flows between two commits, and who did it. */
102
+ export interface RunnerChanges {
103
+ from: string;
104
+ to: string;
105
+ flows: {
106
+ file: string;
107
+ change: 'added' | 'changed' | 'removed';
108
+ authors: string[];
109
+ at?: string;
110
+ commit?: string;
111
+ }[];
112
+ }
113
+ /** AFD-284: where the project's branch is (`commit`), and each time it moves (`moved`). */
114
+ export type ProjectEvent = {
115
+ type: 'commit';
116
+ commit: string | null;
117
+ } | {
118
+ type: 'moved';
119
+ from?: string;
120
+ to: string;
121
+ };
101
122
  export interface RunnerOptions {
102
123
  verifyTls?: boolean;
103
124
  /** Send the token over plain http to another machine (a lab only). */
@@ -112,6 +133,14 @@ export declare class RunnerClient {
112
133
  constructor(url: string, token: string, options?: RunnerOptions);
113
134
  /** A run's page on the server. */
114
135
  runUrl(id: string): string;
136
+ /** Refuses a URL the token must not go to: another origin, or plain http without consent. */
137
+ private guard;
138
+ /**
139
+ * AFD-284: follows a project's event stream (server-sent events) until `stop` is called, going
140
+ * back after a dropped connection (5 s, then up to a minute between tries). `commit` comes at
141
+ * once and after each reconnect, so a move missed while away is noticed.
142
+ */
143
+ follow(project: string, onEvent: (e: ProjectEvent) => void): () => void;
115
144
  /** The answer's body as text, and its status. */
116
145
  private send;
117
146
  request<T>(method: string, path: string, body?: unknown, raw?: {
@@ -142,6 +171,16 @@ export declare class RunnerClient {
142
171
  commit: string;
143
172
  expiresAt: string;
144
173
  }>;
174
+ /** AFD-284: the flows changed between `from` and the project's commit (or `to`). */
175
+ changes: (project: string, from: string, to?: string) => Promise<RunnerChanges>;
176
+ /** AFD-284: one flow as the server reads it at a commit (its document in `snapshot.doc`). */
177
+ flowAt: (project: string, path: string, commit: string) => Promise<{
178
+ commit: string;
179
+ file: string;
180
+ snapshot: {
181
+ doc?: unknown;
182
+ };
183
+ }>;
145
184
  /** What one host did in one step of the run. */
146
185
  hostOutput: (id: string, node: string, host: string) => Promise<{
147
186
  entries?: unknown[];
@@ -47,17 +47,86 @@ var RunnerClient = class {
47
47
  runUrl(id) {
48
48
  return `${this.base}/runs/${enc(id)}`;
49
49
  }
50
- /** The answer's body as text, and its status. */
51
- send(method, path, body, raw) {
52
- const url = new URL(`${this.base}${path}`);
50
+ /** Refuses a URL the token must not go to: another origin, or plain http without consent. */
51
+ guard(url) {
53
52
  if (url.origin !== this.origin)
54
- return Promise.reject(new RunnerError(`Not sending the token to ${url.origin}`));
53
+ return new RunnerError(`Not sending the token to ${url.origin}`);
55
54
  if (cleartextTo(url) && !this.options.allowHttp)
56
- return Promise.reject(
57
- new RunnerError(
58
- `Not sending the token unencrypted to ${url.origin}: use an https address, or turn on afd.runner.allowHttp in your user settings (a lab server only)`
59
- )
55
+ return new RunnerError(
56
+ `Not sending the token unencrypted to ${url.origin}: use an https address, or turn on afd.runner.allowHttp in your user settings (a lab server only)`
60
57
  );
58
+ return void 0;
59
+ }
60
+ /**
61
+ * AFD-284: follows a project's event stream (server-sent events) until `stop` is called, going
62
+ * back after a dropped connection (5 s, then up to a minute between tries). `commit` comes at
63
+ * once and after each reconnect, so a move missed while away is noticed.
64
+ */
65
+ follow(project, onEvent) {
66
+ let stopped = false;
67
+ let req;
68
+ let retry;
69
+ let wait = 5e3;
70
+ const url = new URL(`${this.base}/api/v1/projects/${enc(project)}/events`);
71
+ const problem = this.guard(url);
72
+ const again = () => {
73
+ if (stopped) return;
74
+ retry = setTimeout(connect, wait);
75
+ wait = Math.min(wait * 2, 6e4);
76
+ };
77
+ const connect = () => {
78
+ if (stopped || problem) return;
79
+ const lib = url.protocol === "https:" ? https : http;
80
+ req = lib.request(
81
+ url,
82
+ {
83
+ method: "GET",
84
+ headers: { Authorization: `Bearer ${this.token}`, Accept: "text/event-stream" },
85
+ ...url.protocol === "https:" ? { rejectUnauthorized: this.options.verifyTls !== false } : {}
86
+ },
87
+ (res) => {
88
+ if (res.statusCode !== 200) {
89
+ res.resume();
90
+ if (res.statusCode === 401 || res.statusCode === 403 || res.statusCode === 404) return;
91
+ again();
92
+ return;
93
+ }
94
+ wait = 5e3;
95
+ res.setEncoding("utf8");
96
+ let buffer = "";
97
+ res.on("data", (chunk) => {
98
+ buffer += chunk;
99
+ for (let at = buffer.indexOf("\n\n"); at >= 0; at = buffer.indexOf("\n\n")) {
100
+ const block = buffer.slice(0, at);
101
+ buffer = buffer.slice(at + 2);
102
+ const type = /^event: (.*)$/m.exec(block)?.[1];
103
+ const data = /^data: (.*)$/m.exec(block)?.[1];
104
+ if (!data || type !== "commit" && type !== "moved") continue;
105
+ try {
106
+ onEvent({ type, ...JSON.parse(data) });
107
+ } catch {
108
+ }
109
+ }
110
+ });
111
+ res.on("end", again);
112
+ res.on("error", again);
113
+ }
114
+ );
115
+ req.on("error", again);
116
+ req.end();
117
+ };
118
+ connect();
119
+ return () => {
120
+ stopped = true;
121
+ clearTimeout(retry);
122
+ req?.destroy();
123
+ };
124
+ }
125
+ /** The answer's body as text, and its status. */
126
+ send(method, path, body, raw) {
127
+ const url = new URL(`${this.base}${path}`);
128
+ const refused = this.guard(url);
129
+ if (refused) return Promise.reject(refused);
61
130
  const lib = url.protocol === "https:" ? https : http;
62
131
  const data = raw?.bytes ?? (body === void 0 ? void 0 : JSON.stringify(body));
63
132
  const type = raw?.type ?? "application/json";
@@ -153,6 +222,16 @@ var RunnerClient = class {
153
222
  void 0,
154
223
  { bytes, type: "application/x-git-bundle" }
155
224
  );
225
+ /** AFD-284: the flows changed between `from` and the project's commit (or `to`). */
226
+ changes = (project, from, to) => this.request(
227
+ "GET",
228
+ `/api/v1/projects/${enc(project)}/changes?from=${enc(from)}${to ? `&to=${enc(to)}` : ""}`
229
+ );
230
+ /** AFD-284: one flow as the server reads it at a commit (its document in `snapshot.doc`). */
231
+ flowAt = (project, path, commit) => this.request(
232
+ "GET",
233
+ `/api/v1/projects/${enc(project)}/flows/view?path=${enc(path)}&commit=${enc(commit)}`
234
+ );
156
235
  /** What one host did in one step of the run. */
157
236
  hostOutput = (id, node, host) => this.request(
158
237
  "GET",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/node/runner/client.ts"],
4
- "sourcesContent": ["// A client of Ansible Flow Server (AFD-276): the routes the extension's server target uses, as\n// the designer's contract with the server names them (contract/server-api.json, AFD-280).\n// node:http(s) rather than fetch so TLS verification can be turned off for a lab server, as the\n// AWX client does; the token travels only in the Authorization header, to the configured server.\nimport * as http from 'node:http';\nimport * as https from 'node:https';\nimport { cleartextTo } from '../awx/client';\nimport { TIMEOUTS } from '../timeouts';\n\nexport class RunnerError extends Error {\n constructor(\n message: string,\n readonly status?: number,\n /** The server's error code (`not_found`, `read_only_token`, \u2026). */\n readonly code?: string,\n /** A launch's problems, by field. */\n readonly problems?: { field?: string; message: string }[],\n ) {\n super(message);\n this.name = 'RunnerError';\n }\n}\n\nexport interface RunnerMeta {\n name: string;\n version: string;\n api: string;\n designer: { version: string; format: number; formats?: number[]; bundle?: string };\n features?: string[];\n}\n\nexport interface RunnerProject {\n id: string;\n key: string;\n name: string;\n gitPath: string;\n gitRef: string;\n commit?: string;\n}\n\nexport interface RunnerConfig {\n id: string;\n name: string;\n flowFile: string;\n mode: 'check' | 'real';\n inventory?: string;\n gitRef?: string;\n commit?: string;\n}\n\n/** What a launch would run (the server's PreparedLaunch, without its inputs). */\nexport interface RunnerLaunch {\n commit: string;\n ref?: string;\n flow: { file: string; key: string; name: string; version: number };\n mode: 'check' | 'real';\n inventory?: string;\n targets: string[];\n}\n\nexport interface RunnerRun {\n id: string;\n number: number;\n project: string;\n commit: string;\n flow: { file: string; key: string; name: string };\n mode: 'check' | 'real';\n state: 'queued' | 'running' | 'waiting' | 'succeeded' | 'failed' | 'cancelled' | 'lost';\n error?: string;\n endedAt?: string;\n /** AFD-282: a check run of someone's unpushed work. */\n dev?: true;\n}\n\nexport interface RunnerGate {\n node: { id: string; key: string; title: string };\n status: string;\n mayDecide: boolean;\n why?: string;\n}\n\nexport interface LaunchRequest {\n mode?: 'check' | 'real';\n /** AFD-282: a dev bundle's id: the server runs its commit, in check mode. */\n dev?: string;\n limit?: string;\n /** The flow's inputs as typed text, by name. */\n inputs?: Record<string, string>;\n}\n\n/** The REST API this client speaks. */\nexport const RUNNER_API = 'v1';\n\n/** Why this client and the server do not fit for a flow in `format`, or undefined. */\nexport function incompatible(meta: RunnerMeta, format: number): string | undefined {\n if (meta.name !== 'Ansible Flow Server') return 'The address is not an Ansible Flow Server';\n if (meta.api !== RUNNER_API)\n return `The server speaks API ${meta.api}; this extension speaks ${RUNNER_API}: update the one that is older`;\n const formats = meta.designer.formats ?? [meta.designer.format];\n if (!formats.includes(format))\n return `The flow is in format ${format}; the server (${meta.version}) reads formats ${formats.join(', ')}: upgrade the server`;\n return undefined;\n}\n\nexport interface RunnerOptions {\n verifyTls?: boolean;\n /** Send the token over plain http to another machine (a lab only). */\n allowHttp?: boolean;\n timeoutMs?: number;\n}\n\nconst MAX_BODY = 32 * 1024 * 1024;\nconst enc = encodeURIComponent;\n\nexport class RunnerClient {\n readonly base: string;\n private readonly origin: string;\n\n constructor(\n url: string,\n private readonly token: string,\n private readonly options: RunnerOptions = {},\n ) {\n this.base = url.replace(/\\/+$/, '');\n this.origin = new URL(this.base).origin;\n }\n\n /** A run's page on the server. */\n runUrl(id: string): string {\n return `${this.base}/runs/${enc(id)}`;\n }\n\n /** The answer's body as text, and its status. */\n private send(\n method: string,\n path: string,\n body?: unknown,\n /** A body sent as it is, with its content type (a git bundle). */\n raw?: { bytes: Buffer; type: string },\n ): Promise<{ status: number; text: string }> {\n const url = new URL(`${this.base}${path}`);\n if (url.origin !== this.origin)\n return Promise.reject(new RunnerError(`Not sending the token to ${url.origin}`));\n if (cleartextTo(url) && !this.options.allowHttp)\n return Promise.reject(\n new RunnerError(\n `Not sending the token unencrypted to ${url.origin}: use an https address, or turn on afd.runner.allowHttp in your user settings (a lab server only)`,\n ),\n );\n const lib = url.protocol === 'https:' ? https : http;\n const data = raw?.bytes ?? (body === undefined ? undefined : JSON.stringify(body));\n const type = raw?.type ?? 'application/json';\n return new Promise((resolve, reject) => {\n const req = lib.request(\n url,\n {\n method,\n headers: {\n Authorization: `Bearer ${this.token}`,\n Accept: 'application/json',\n ...(data ? { 'Content-Type': type, 'Content-Length': Buffer.byteLength(data) } : {}),\n },\n timeout: this.options.timeoutMs ?? TIMEOUTS.runnerRequest,\n ...(url.protocol === 'https:'\n ? { rejectUnauthorized: this.options.verifyTls !== false }\n : {}),\n },\n (res) => {\n const chunks: Buffer[] = [];\n let size = 0;\n res.on('data', (c: Buffer) => {\n size += c.length;\n if (size > MAX_BODY) {\n req.destroy(new Error('the answer is too large'));\n return;\n }\n chunks.push(c);\n });\n res.on('end', () =>\n resolve({ status: res.statusCode ?? 0, text: Buffer.concat(chunks).toString('utf8') }),\n );\n res.on('error', reject);\n },\n );\n req.on('timeout', () => req.destroy(new Error('no answer in time')));\n req.on('error', (e) =>\n reject(new RunnerError(`The server at ${this.base} does not answer: ${e.message}`)),\n );\n req.end(data);\n });\n }\n\n async request<T>(\n method: string,\n path: string,\n body?: unknown,\n raw?: { bytes: Buffer; type: string },\n ): Promise<T> {\n const { status, text } = await this.send(method, path, body, raw);\n let json: unknown;\n try {\n json = text ? JSON.parse(text) : undefined;\n } catch {\n json = undefined;\n }\n if (status >= 200 && status < 300) return json as T;\n const e = (json ?? {}) as {\n error?: string;\n message?: string;\n problems?: { field?: string; message: string }[];\n };\n const why =\n status === 401\n ? 'The server does not take the token: set it again (it may have expired or been revoked)'\n : status === 413\n ? 'The work is larger than the server takes (AFS_DEV_BUNDLE_MAX_MB)'\n : (e.message ?? `The server answered ${status}`);\n throw new RunnerError(why, status, e.error, e.problems);\n }\n\n /** The output of a request whose answer is plain text. */\n async text(path: string): Promise<string> {\n const { status, text } = await this.send('GET', path);\n if (status >= 200 && status < 300) return text;\n throw new RunnerError(`The server answered ${status}`, status);\n }\n\n meta = () => this.request<RunnerMeta>('GET', '/api/v1/meta');\n me = () => this.request<{ login: string; name: string }>('GET', '/api/v1/session');\n projects = (workspace: string) =>\n this.request<RunnerProject[]>('GET', `/api/v1/workspaces/${enc(workspace)}/projects`);\n project = (id: string) => this.request<RunnerProject>('GET', `/api/v1/projects/${enc(id)}`);\n configs = (project: string) =>\n this.request<RunnerConfig[]>('GET', `/api/v1/projects/${enc(project)}/launch-configs`);\n check = async (config: string, req: LaunchRequest) =>\n (\n await this.request<{ launch: RunnerLaunch }>(\n 'POST',\n `/api/v1/launch-configs/${enc(config)}/check`,\n req,\n )\n ).launch;\n launch = (config: string, req: LaunchRequest) =>\n this.request<RunnerRun>('POST', `/api/v1/launch-configs/${enc(config)}/launch`, req);\n run = (id: string) => this.request<RunnerRun>('GET', `/api/v1/runs/${enc(id)}`);\n /** How the run went, as the designer shows it (the server's RunView); 404 until it has one. */\n view = async (id: string) => {\n try {\n return await this.request<Record<string, unknown>>('GET', `/api/v1/runs/${enc(id)}/view`);\n } catch (e) {\n if (e instanceof RunnerError && e.status === 404) return undefined;\n throw e;\n }\n };\n gates = (id: string) => this.request<RunnerGate[]>('GET', `/api/v1/runs/${enc(id)}/approvals`);\n cancel = (id: string) => this.request<RunnerRun>('POST', `/api/v1/runs/${enc(id)}/cancel`);\n decide = (id: string, node: string, decision: 'approved' | 'rejected', comment?: string) =>\n this.request<unknown>('POST', `/api/v1/runs/${enc(id)}/approvals/${enc(node)}`, {\n decision,\n ...(comment?.trim() ? { comment: comment.trim() } : {}),\n });\n /** AFD-282: unpushed work for a dev run; resolves with the bundle's id and commit. */\n devBundle = (project: string, bytes: Buffer) =>\n this.request<{ id: string; commit: string; expiresAt: string }>(\n 'POST',\n `/api/v1/projects/${enc(project)}/dev-bundles`,\n undefined,\n { bytes, type: 'application/x-git-bundle' },\n );\n /** What one host did in one step of the run. */\n hostOutput = (id: string, node: string, host: string) =>\n this.request<{ entries?: unknown[] }>(\n 'GET',\n `/api/v1/runs/${enc(id)}/hosts/${enc(node)}/${enc(host)}`,\n );\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// A client of Ansible Flow Server (AFD-276): the routes the extension's server target uses, as\n// the designer's contract with the server names them (contract/server-api.json, AFD-280).\n// node:http(s) rather than fetch so TLS verification can be turned off for a lab server, as the\n// AWX client does; the token travels only in the Authorization header, to the configured server.\nimport * as http from 'node:http';\nimport * as https from 'node:https';\nimport { cleartextTo } from '../awx/client';\nimport { TIMEOUTS } from '../timeouts';\n\nexport class RunnerError extends Error {\n constructor(\n message: string,\n readonly status?: number,\n /** The server's error code (`not_found`, `read_only_token`, \u2026). */\n readonly code?: string,\n /** A launch's problems, by field. */\n readonly problems?: { field?: string; message: string }[],\n ) {\n super(message);\n this.name = 'RunnerError';\n }\n}\n\nexport interface RunnerMeta {\n name: string;\n version: string;\n api: string;\n designer: { version: string; format: number; formats?: number[]; bundle?: string };\n features?: string[];\n}\n\nexport interface RunnerProject {\n id: string;\n key: string;\n name: string;\n gitPath: string;\n gitRef: string;\n commit?: string;\n}\n\nexport interface RunnerConfig {\n id: string;\n name: string;\n flowFile: string;\n mode: 'check' | 'real';\n inventory?: string;\n gitRef?: string;\n commit?: string;\n}\n\n/** What a launch would run (the server's PreparedLaunch, without its inputs). */\nexport interface RunnerLaunch {\n commit: string;\n ref?: string;\n flow: { file: string; key: string; name: string; version: number };\n mode: 'check' | 'real';\n inventory?: string;\n targets: string[];\n}\n\nexport interface RunnerRun {\n id: string;\n number: number;\n project: string;\n commit: string;\n flow: { file: string; key: string; name: string };\n mode: 'check' | 'real';\n state: 'queued' | 'running' | 'waiting' | 'succeeded' | 'failed' | 'cancelled' | 'lost';\n error?: string;\n endedAt?: string;\n /** AFD-282: a check run of someone's unpushed work. */\n dev?: true;\n}\n\nexport interface RunnerGate {\n node: { id: string; key: string; title: string };\n status: string;\n mayDecide: boolean;\n why?: string;\n}\n\nexport interface LaunchRequest {\n mode?: 'check' | 'real';\n /** AFD-282: a dev bundle's id: the server runs its commit, in check mode. */\n dev?: string;\n limit?: string;\n /** The flow's inputs as typed text, by name. */\n inputs?: Record<string, string>;\n}\n\n/** The REST API this client speaks. */\nexport const RUNNER_API = 'v1';\n\n/** Why this client and the server do not fit for a flow in `format`, or undefined. */\nexport function incompatible(meta: RunnerMeta, format: number): string | undefined {\n if (meta.name !== 'Ansible Flow Server') return 'The address is not an Ansible Flow Server';\n if (meta.api !== RUNNER_API)\n return `The server speaks API ${meta.api}; this extension speaks ${RUNNER_API}: update the one that is older`;\n const formats = meta.designer.formats ?? [meta.designer.format];\n if (!formats.includes(format))\n return `The flow is in format ${format}; the server (${meta.version}) reads formats ${formats.join(', ')}: upgrade the server`;\n return undefined;\n}\n\n/** AFD-284: what happened to which flows between two commits, and who did it. */\nexport interface RunnerChanges {\n from: string;\n to: string;\n flows: {\n file: string;\n change: 'added' | 'changed' | 'removed';\n authors: string[];\n at?: string;\n commit?: string;\n }[];\n}\n\n/** AFD-284: where the project's branch is (`commit`), and each time it moves (`moved`). */\nexport type ProjectEvent =\n | { type: 'commit'; commit: string | null }\n | { type: 'moved'; from?: string; to: string };\n\nexport interface RunnerOptions {\n verifyTls?: boolean;\n /** Send the token over plain http to another machine (a lab only). */\n allowHttp?: boolean;\n timeoutMs?: number;\n}\n\nconst MAX_BODY = 32 * 1024 * 1024;\nconst enc = encodeURIComponent;\n\nexport class RunnerClient {\n readonly base: string;\n private readonly origin: string;\n\n constructor(\n url: string,\n private readonly token: string,\n private readonly options: RunnerOptions = {},\n ) {\n this.base = url.replace(/\\/+$/, '');\n this.origin = new URL(this.base).origin;\n }\n\n /** A run's page on the server. */\n runUrl(id: string): string {\n return `${this.base}/runs/${enc(id)}`;\n }\n\n /** Refuses a URL the token must not go to: another origin, or plain http without consent. */\n private guard(url: URL): RunnerError | undefined {\n if (url.origin !== this.origin)\n return new RunnerError(`Not sending the token to ${url.origin}`);\n if (cleartextTo(url) && !this.options.allowHttp)\n return new RunnerError(\n `Not sending the token unencrypted to ${url.origin}: use an https address, or turn on afd.runner.allowHttp in your user settings (a lab server only)`,\n );\n return undefined;\n }\n\n /**\n * AFD-284: follows a project's event stream (server-sent events) until `stop` is called, going\n * back after a dropped connection (5 s, then up to a minute between tries). `commit` comes at\n * once and after each reconnect, so a move missed while away is noticed.\n */\n follow(project: string, onEvent: (e: ProjectEvent) => void): () => void {\n let stopped = false;\n let req: http.ClientRequest | undefined;\n let retry: NodeJS.Timeout | undefined;\n let wait = 5_000;\n const url = new URL(`${this.base}/api/v1/projects/${enc(project)}/events`);\n const problem = this.guard(url);\n const again = () => {\n if (stopped) return;\n retry = setTimeout(connect, wait);\n wait = Math.min(wait * 2, 60_000);\n };\n const connect = () => {\n if (stopped || problem) return;\n const lib = url.protocol === 'https:' ? https : http;\n req = lib.request(\n url,\n {\n method: 'GET',\n headers: { Authorization: `Bearer ${this.token}`, Accept: 'text/event-stream' },\n ...(url.protocol === 'https:'\n ? { rejectUnauthorized: this.options.verifyTls !== false }\n : {}),\n },\n (res) => {\n if (res.statusCode !== 200) {\n res.resume();\n // A token the server refuses, or a project gone: trying again changes nothing.\n if (res.statusCode === 401 || res.statusCode === 403 || res.statusCode === 404) return;\n again();\n return;\n }\n wait = 5_000;\n res.setEncoding('utf8');\n let buffer = '';\n res.on('data', (chunk: string) => {\n buffer += chunk;\n for (let at = buffer.indexOf('\\n\\n'); at >= 0; at = buffer.indexOf('\\n\\n')) {\n const block = buffer.slice(0, at);\n buffer = buffer.slice(at + 2);\n const type = /^event: (.*)$/m.exec(block)?.[1];\n const data = /^data: (.*)$/m.exec(block)?.[1];\n if (!data || (type !== 'commit' && type !== 'moved')) continue;\n try {\n onEvent({ type, ...JSON.parse(data) } as ProjectEvent);\n } catch {\n // A line it cannot read: the next event says where the branch is.\n }\n }\n });\n res.on('end', again);\n res.on('error', again);\n },\n );\n req.on('error', again);\n req.end();\n };\n connect();\n return () => {\n stopped = true;\n clearTimeout(retry);\n req?.destroy();\n };\n }\n\n /** The answer's body as text, and its status. */\n private send(\n method: string,\n path: string,\n body?: unknown,\n /** A body sent as it is, with its content type (a git bundle). */\n raw?: { bytes: Buffer; type: string },\n ): Promise<{ status: number; text: string }> {\n const url = new URL(`${this.base}${path}`);\n const refused = this.guard(url);\n if (refused) return Promise.reject(refused);\n const lib = url.protocol === 'https:' ? https : http;\n const data = raw?.bytes ?? (body === undefined ? undefined : JSON.stringify(body));\n const type = raw?.type ?? 'application/json';\n return new Promise((resolve, reject) => {\n const req = lib.request(\n url,\n {\n method,\n headers: {\n Authorization: `Bearer ${this.token}`,\n Accept: 'application/json',\n ...(data ? { 'Content-Type': type, 'Content-Length': Buffer.byteLength(data) } : {}),\n },\n timeout: this.options.timeoutMs ?? TIMEOUTS.runnerRequest,\n ...(url.protocol === 'https:'\n ? { rejectUnauthorized: this.options.verifyTls !== false }\n : {}),\n },\n (res) => {\n const chunks: Buffer[] = [];\n let size = 0;\n res.on('data', (c: Buffer) => {\n size += c.length;\n if (size > MAX_BODY) {\n req.destroy(new Error('the answer is too large'));\n return;\n }\n chunks.push(c);\n });\n res.on('end', () =>\n resolve({ status: res.statusCode ?? 0, text: Buffer.concat(chunks).toString('utf8') }),\n );\n res.on('error', reject);\n },\n );\n req.on('timeout', () => req.destroy(new Error('no answer in time')));\n req.on('error', (e) =>\n reject(new RunnerError(`The server at ${this.base} does not answer: ${e.message}`)),\n );\n req.end(data);\n });\n }\n\n async request<T>(\n method: string,\n path: string,\n body?: unknown,\n raw?: { bytes: Buffer; type: string },\n ): Promise<T> {\n const { status, text } = await this.send(method, path, body, raw);\n let json: unknown;\n try {\n json = text ? JSON.parse(text) : undefined;\n } catch {\n json = undefined;\n }\n if (status >= 200 && status < 300) return json as T;\n const e = (json ?? {}) as {\n error?: string;\n message?: string;\n problems?: { field?: string; message: string }[];\n };\n const why =\n status === 401\n ? 'The server does not take the token: set it again (it may have expired or been revoked)'\n : status === 413\n ? 'The work is larger than the server takes (AFS_DEV_BUNDLE_MAX_MB)'\n : (e.message ?? `The server answered ${status}`);\n throw new RunnerError(why, status, e.error, e.problems);\n }\n\n /** The output of a request whose answer is plain text. */\n async text(path: string): Promise<string> {\n const { status, text } = await this.send('GET', path);\n if (status >= 200 && status < 300) return text;\n throw new RunnerError(`The server answered ${status}`, status);\n }\n\n meta = () => this.request<RunnerMeta>('GET', '/api/v1/meta');\n me = () => this.request<{ login: string; name: string }>('GET', '/api/v1/session');\n projects = (workspace: string) =>\n this.request<RunnerProject[]>('GET', `/api/v1/workspaces/${enc(workspace)}/projects`);\n project = (id: string) => this.request<RunnerProject>('GET', `/api/v1/projects/${enc(id)}`);\n configs = (project: string) =>\n this.request<RunnerConfig[]>('GET', `/api/v1/projects/${enc(project)}/launch-configs`);\n check = async (config: string, req: LaunchRequest) =>\n (\n await this.request<{ launch: RunnerLaunch }>(\n 'POST',\n `/api/v1/launch-configs/${enc(config)}/check`,\n req,\n )\n ).launch;\n launch = (config: string, req: LaunchRequest) =>\n this.request<RunnerRun>('POST', `/api/v1/launch-configs/${enc(config)}/launch`, req);\n run = (id: string) => this.request<RunnerRun>('GET', `/api/v1/runs/${enc(id)}`);\n /** How the run went, as the designer shows it (the server's RunView); 404 until it has one. */\n view = async (id: string) => {\n try {\n return await this.request<Record<string, unknown>>('GET', `/api/v1/runs/${enc(id)}/view`);\n } catch (e) {\n if (e instanceof RunnerError && e.status === 404) return undefined;\n throw e;\n }\n };\n gates = (id: string) => this.request<RunnerGate[]>('GET', `/api/v1/runs/${enc(id)}/approvals`);\n cancel = (id: string) => this.request<RunnerRun>('POST', `/api/v1/runs/${enc(id)}/cancel`);\n decide = (id: string, node: string, decision: 'approved' | 'rejected', comment?: string) =>\n this.request<unknown>('POST', `/api/v1/runs/${enc(id)}/approvals/${enc(node)}`, {\n decision,\n ...(comment?.trim() ? { comment: comment.trim() } : {}),\n });\n /** AFD-282: unpushed work for a dev run; resolves with the bundle's id and commit. */\n devBundle = (project: string, bytes: Buffer) =>\n this.request<{ id: string; commit: string; expiresAt: string }>(\n 'POST',\n `/api/v1/projects/${enc(project)}/dev-bundles`,\n undefined,\n { bytes, type: 'application/x-git-bundle' },\n );\n /** AFD-284: the flows changed between `from` and the project's commit (or `to`). */\n changes = (project: string, from: string, to?: string) =>\n this.request<RunnerChanges>(\n 'GET',\n `/api/v1/projects/${enc(project)}/changes?from=${enc(from)}${to ? `&to=${enc(to)}` : ''}`,\n );\n /** AFD-284: one flow as the server reads it at a commit (its document in `snapshot.doc`). */\n flowAt = (project: string, path: string, commit: string) =>\n this.request<{ commit: string; file: string; snapshot: { doc?: unknown } }>(\n 'GET',\n `/api/v1/projects/${enc(project)}/flows/view?path=${enc(path)}&commit=${enc(commit)}`,\n );\n /** What one host did in one step of the run. */\n hostOutput = (id: string, node: string, host: string) =>\n this.request<{ entries?: unknown[] }>(\n 'GET',\n `/api/v1/runs/${enc(id)}/hosts/${enc(node)}/${enc(host)}`,\n );\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,12 +6,12 @@ import {
6
6
  } from "../chunks/chunk-RETJPSKL.js";
7
7
  import "../chunks/chunk-THNSGAKN.js";
8
8
  import "../chunks/chunk-FG53MLUX.js";
9
- import {
10
- stopStarted
11
- } from "../chunks/chunk-JDPN3LTT.js";
12
9
  import {
13
10
  startProcess
14
11
  } from "../chunks/chunk-I6FIOP7G.js";
12
+ import {
13
+ stopStarted
14
+ } from "../chunks/chunk-JDPN3LTT.js";
15
15
 
16
16
  // src/node/scenario/runner.ts
17
17
  import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ansible-flow/node",
3
- "version": "0.86.0",
3
+ "version": "0.88.0",
4
4
  "description": "Ansible Flow Designer for Node hosts: layout, the probe client, runs, the AWX client, the module catalog and SSH.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ChistokhinSV/ansible-flow-designer/blob/main/docs/packages.md",
@@ -26,7 +26,7 @@
26
26
  "node": ">=22.13"
27
27
  },
28
28
  "dependencies": {
29
- "@ansible-flow/core": "0.86.0",
29
+ "@ansible-flow/core": "0.88.0",
30
30
  "elkjs": "0.12.0",
31
31
  "jsonc-parser": "3.3.1",
32
32
  "yaml": "2.9.1"
@@ -1,7 +0,0 @@
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": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAOA,SAAS,kBAAkB;AAC3B,SAAS,OAAO,UAAU,iBAAiB;AAC3C,SAAS,YAAY;AAErB,SAAS,mBAAmB;AAG5B,SAA2B,YAAY,mBAAmB;AAE1D,SAAS,kBAAmC,oBAAoB;AAChE,SAAS,YAAY,YAAY,oBAAoB;AAU9C,IAAM,UAAU;AAEhB,IAAM,WAAW;AAuExB,eAAsB,QAAQ,SAAyB,OAAuC;AAC5F,QAAM,EAAE,IAAI,MAAM,KAAK,MAAM,WAAW,eAAe,QAAQ,IAAI;AACnE,QAAM,OAAO,CAAC,UAA2B;AACvC,UAAM,MAAe,EAAE,IAAI,MAAM,OAAO,SAAS,WAAW,MAAM;AAClE,UAAM,OAAO,GAAG;AAChB,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,KAAK,QAAQ,QAAQ,SAAS,IAAI,KAAK,EAAE,GAAG,EAAE;AAC7D,QAAM,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;AAEvC,QAAM,EAAE,OAAO,OAAO,IAAI,YAAY,QAAQ,MAAM;AACpD,QAAM,UAAU,OAAO,KAAK,MAAM,EAAE,SAAS;AAC7C,QAAM,UAAU,KAAK,QAAQ,WAAW,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,GAAG,MAAM;AACjF,QAAM,UAAU,QAAQ,SAAS,IAAI,KAAK,IAAI,QAAQ,YAAY,SAAS,EAAE;AAAA,IAC3E,MAAM;AAAA,EACR;AAGA,QAAM,SAAS,QAAQ,IAAI,SAAS,QAAQ,QAAQ,MAAM;AAC1D,MAAI,QAAQ;AACV,UAAM,SACH,MAAM;AAAA,MACL;AAAA,MACA,EAAE,MAAM,QAAQ,MAAM,OAAO,QAAQ,SAAS;AAAA,MAC9C,QAAQ,YAAY,CAAC;AAAA,IACvB,KAAO,MAAM,WAAW,QAAQ,QAAQ,aAAa,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AACrF,QAAI,OAAQ,QAAO,KAAK,MAAM;AAAA,EAChC;AACA,QAAM,OAAO;AAAA,IACX,QAAQ;AAAA,IACR,EAAE,MAAM,QAAQ,MAAM,OAAO,QAAQ,SAAS;AAAA,IAC9C;AAAA,MACE,UAAU,QAAQ;AAAA,MAClB,WAAW,QAAQ;AAAA,MACnB,OAAO,SAAS;AAAA,MAChB;AAAA,MACA,OAAO,kBAAkB;AAAA,MACzB;AAAA,MACA,WAAW,WAAW,KAAK,QAAQ,MAAM,aAAa,CAAC;AAAA,MACvD,WAAW,QAAQ;AAAA,IACrB;AAAA,IACA,WAAW,GAAG,YAAY,CAAC;AAAA,EAC7B;AAEA,MAAI,QAAQ;AACV,UAAM,YAAY,QAAQ,SAAS,EAAE,OAAO,UAAU,KAAK,IAAI,GAAG,QAAQ,MAAM,CAAC;AACnF,QAAM,UAAmB;AAAA,IACvB;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP;AAAA,IACA,aAAa,QAAQ;AAAA,IACrB,QAAQ;AAAA,IACR,aAAa,IAAI,KAAK;AAAA,IACtB,QAAQ,YAAY,GAAG;AAAA,IACvB,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/B;AACA,QAAM,OAAO,OAAO;AAEpB,QAAM,MAA8B,EAAE,GAAG,KAAK,IAAI;AAElD,MAAI,KAAK,IAAI,UAAU,QAAQ,IAAI;AACjC,QAAI,SAAS,GAAG,QAAQ,IAAI,MAAM,IAAI,KAAK,IAAI,MAAM;AACvD,MAAI,kBAAkB,OAAW,KAAI,qBAAqB;AAC1D,MAAI,QAAS,KAAI,kBAAkB,KAAK,UAAU,MAAM;AAExD,QAAM,KAAK,KAAK,IAAI;AAGpB,QAAM,SAAS,aAAa,gBAAgB,EAAE,GAAG,QAAQ,IAAI,cAAc,IAAI,MAAM;AACrF,QAAM,OAAO,iBAAiB,KAAK,MAAM;AACzC,QAAM,aAAa,KAAK,QAAQ,cAAc;AAE9C,QAAM,YAAY,SAAS,cAAc,QAAQ,QAAQ,gBAAgB,UAAU,IAAI;AACvF,MAAI,YAAY,KAAK,IAAI;AACzB,MAAI,UAAU;AACd,MAAI,QAAQ;AACZ,QAAM,OAAO,SAAS,YAAY,CAAC,UAAU;AAC3C,eAAW,QAAQ,MAAO,MAAK,KAAK,WAAW,MAAM,MAAM,CAAC;AAC5D,gBAAY,KAAK,IAAI;AACrB,cAAU;AACV,YAAQ;AAAA,EACV,CAAC;AACD,QAAM,SAAS,YAAY,MAAM;AAC/B,QAAI,MAAM,cAAc,CAAC,SAAS,KAAK,IAAI,IAAI,YAAY,UAAU;AACnE,cAAQ;AACR,gBAAU;AACV,YAAM,QAAQ;AAAA,IAChB;AACA,QAAI,CAAC,QAAS;AACd,cAAU;AACV,UAAM,OAAO;AAAA,MACX,GAAG;AAAA,MACH,SAAS,KAAK,QAAQ,EAAE,SAAS,KAAK,CAAC;AAAA,MACvC,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IAC3B,CAAC;AAAA,EACH,GAAG,OAAO;AAGV,MAAI,UAAU;AACd,QAAM,UAAU,MAAM,YAAY,CAAC,GAAG,MAAM,aAAa,GAAG,GAAG,MAAM,QAAQ,MAAM;AAAA,EAAC,EAAE;AACtF,MAAI;AACJ,MAAI;AACF,gBAAY,MAAM,QAAQ,MAAM,KAAK,MAAM,OAAO;AAAA,EACpD,QAAQ;AACN,gBAAY,EAAE,MAAM,QAAQ,QAAQ,MAAS,GAAG,WAAW,MAAM,OAAU;AAAA,EAC7E;AACA,QAAM,OAAO,YAAY;AACvB,cAAU;AAGV,UAAM,YAAY,IAAI;AACtB,cAAU,UAAU;AAAA,EACtB;AACA,QAAM,UAAU,MAAM,KAAK,KAAK;AAChC,MAAI,QAAQ,QAAQ,QAAS,SAAQ;AAAA,MAChC,SAAQ,QAAQ,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;AACtE,QAAM,UAAU,MAAM,UAAU,IAAI;AACpC,QAAM,KAAK,MAAM,UAAU;AAC3B,YAAU;AACV,UAAQ,QAAQ,oBAAoB,SAAS,OAAO;AACpD,gBAAc,MAAM;AACpB,QAAM,WAAW,KAAK;AACtB,QAAM,KAAK,KAAK;AAChB,MAAI,OAAQ,OAAM,aAAa,QAAQ,MAAM;AAE7C,QAAM,SAAS,MAAM,EAAE,MAAM,MAAM,MAAS;AAC5C,QAAM,MAAM,QAAQ;AACpB,MAAI,OAAO;AACX,MAAI;AACF,WAAO,MAAM,SAAS,YAAY,MAAM;AAExC,QAAI,OAAO,SAAS,GAAG;AACrB,aAAO,WAAW,MAAM,MAAM;AAC9B,YAAM,UAAU,YAAY,MAAM,MAAM;AAAA,IAC1C;AAAA,EACF,QAAQ;AAAA,EAAC;AACT,QAAM,UAAU,aAAa,MAAM,KAAK,MAAM;AAC9C,QAAM,WAAW,EAAE,SAAS,IAAI,QAAQ,CAAC,CAAC,KAAK,KAAK,GAAG,QAAQ,CAAC;AAChE,QAAM,OAAgB;AAAA,IACpB,GAAG;AAAA,IACH,OAAO,OAAO,UAAa,UAAU,UAAU;AAAA,IAC/C,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,IACnC;AAAA,IACA;AAAA,IACA,OAAO,UACH,wBACA,OAAO,SACL,iCACA,OACE,SACA;AAAA,IACR,GAAI,UAAU,EAAE,WAAW,YAAqB,IAAI,CAAC;AAAA,EACvD;AACA,MAAI,QAAQ;AACV,UAAM,YAAY,QAAQ,SAAS;AAAA,MACjC,OAAO;AAAA,MACP,KAAK;AAAA,MACL;AAAA,MACA,IAAI,KAAK,IAAI,IAAI;AAAA,MACjB,OAAO,QAAQ,MAAM;AAAA,MACrB,QAAQ,OAAO,KAAK,QAAQ,OAAO,EAAE,SAAS,QAAQ,SAAS;AAAA,IACjE,CAAC;AAEH,QAAM,QAAQ,QAAQ,IAAI;AAC1B,QAAM,OAAO,IAAI;AACjB,SAAO;AACT;",
6
- "names": []
7
- }