@ansible-flow/node 0.0.0-stage → 0.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -2
  3. package/dist/ai/agents.d.ts +34 -0
  4. package/dist/ai/agents.js +16 -0
  5. package/dist/ai/agents.js.map +7 -0
  6. package/dist/ai/hostConfig.d.ts +44 -0
  7. package/dist/ai/hostConfig.js +120 -0
  8. package/dist/ai/hostConfig.js.map +7 -0
  9. package/dist/ai/hosts.d.ts +28 -0
  10. package/dist/ai/hosts.js +9 -0
  11. package/dist/ai/hosts.js.map +7 -0
  12. package/dist/ai/writers.d.ts +124 -0
  13. package/dist/ai/writers.js +75 -0
  14. package/dist/ai/writers.js.map +7 -0
  15. package/dist/autoLayout.d.ts +38 -0
  16. package/dist/autoLayout.js +145 -0
  17. package/dist/autoLayout.js.map +7 -0
  18. package/dist/awx/awxWorkflow.d.ts +47 -0
  19. package/dist/awx/awxWorkflow.js +17 -0
  20. package/dist/awx/awxWorkflow.js.map +7 -0
  21. package/dist/awx/client.d.ts +77 -0
  22. package/dist/awx/client.js +22 -0
  23. package/dist/awx/client.js.map +7 -0
  24. package/dist/catalog/bindingCheck.d.ts +4 -0
  25. package/dist/catalog/bindingCheck.js +69 -0
  26. package/dist/catalog/bindingCheck.js.map +7 -0
  27. package/dist/catalog/catalogIndex.d.ts +52 -0
  28. package/dist/catalog/catalogIndex.js +13 -0
  29. package/dist/catalog/catalogIndex.js.map +7 -0
  30. package/dist/catalog/catalogWorker.d.ts +17 -0
  31. package/dist/catalog/catalogWorker.js +38 -0
  32. package/dist/catalog/catalogWorker.js.map +7 -0
  33. package/dist/catalog/dumpFiles.d.ts +3 -0
  34. package/dist/catalog/dumpFiles.js +19 -0
  35. package/dist/catalog/dumpFiles.js.map +7 -0
  36. package/dist/chunks/chunk-2MZQHIXP.js +614 -0
  37. package/dist/chunks/chunk-2MZQHIXP.js.map +7 -0
  38. package/dist/chunks/chunk-4FHPTGI4.js +52 -0
  39. package/dist/chunks/chunk-4FHPTGI4.js.map +7 -0
  40. package/dist/chunks/chunk-A2NMQGQE.js +189 -0
  41. package/dist/chunks/chunk-A2NMQGQE.js.map +7 -0
  42. package/dist/chunks/chunk-EPJKJBNW.js +181 -0
  43. package/dist/chunks/chunk-EPJKJBNW.js.map +7 -0
  44. package/dist/chunks/chunk-FG53MLUX.js +136 -0
  45. package/dist/chunks/chunk-FG53MLUX.js.map +7 -0
  46. package/dist/chunks/chunk-GAL5LW6Y.js +269 -0
  47. package/dist/chunks/chunk-GAL5LW6Y.js.map +7 -0
  48. package/dist/chunks/chunk-HPVT6I36.js +371 -0
  49. package/dist/chunks/chunk-HPVT6I36.js.map +7 -0
  50. package/dist/chunks/chunk-I6FIOP7G.js +48 -0
  51. package/dist/chunks/chunk-I6FIOP7G.js.map +7 -0
  52. package/dist/chunks/chunk-IIFP6UQR.js +44 -0
  53. package/dist/chunks/chunk-IIFP6UQR.js.map +7 -0
  54. package/dist/chunks/chunk-JDPN3LTT.js +18 -0
  55. package/dist/chunks/chunk-JDPN3LTT.js.map +7 -0
  56. package/dist/chunks/chunk-LGUG2Q7Z.js +191 -0
  57. package/dist/chunks/chunk-LGUG2Q7Z.js.map +7 -0
  58. package/dist/chunks/chunk-NTZRNEZM.js +25 -0
  59. package/dist/chunks/chunk-NTZRNEZM.js.map +7 -0
  60. package/dist/chunks/chunk-RETJPSKL.js +123 -0
  61. package/dist/chunks/chunk-RETJPSKL.js.map +7 -0
  62. package/dist/chunks/chunk-SSZNBJZQ.js +52 -0
  63. package/dist/chunks/chunk-SSZNBJZQ.js.map +7 -0
  64. package/dist/chunks/chunk-SUV3VHKR.js +52 -0
  65. package/dist/chunks/chunk-SUV3VHKR.js.map +7 -0
  66. package/dist/chunks/chunk-THNSGAKN.js +133 -0
  67. package/dist/chunks/chunk-THNSGAKN.js.map +7 -0
  68. package/dist/chunks/chunk-UNCR5JOZ.js +85 -0
  69. package/dist/chunks/chunk-UNCR5JOZ.js.map +7 -0
  70. package/dist/env/probeClient.d.ts +77 -0
  71. package/dist/env/probeClient.js +244 -0
  72. package/dist/env/probeClient.js.map +7 -0
  73. package/dist/env/userSettings.d.ts +33 -0
  74. package/dist/env/userSettings.js +101 -0
  75. package/dist/env/userSettings.js.map +7 -0
  76. package/dist/run/audit.d.ts +2 -0
  77. package/dist/run/audit.js +10 -0
  78. package/dist/run/audit.js.map +7 -0
  79. package/dist/run/exec.d.ts +14 -0
  80. package/dist/run/exec.js +7 -0
  81. package/dist/run/exec.js.map +7 -0
  82. package/dist/run/facts.d.ts +39 -0
  83. package/dist/run/facts.js +96 -0
  84. package/dist/run/facts.js.map +7 -0
  85. package/dist/run/ids.d.ts +2 -0
  86. package/dist/run/ids.js +8 -0
  87. package/dist/run/ids.js.map +7 -0
  88. package/dist/run/pastRuns.d.ts +9 -0
  89. package/dist/run/pastRuns.js +13 -0
  90. package/dist/run/pastRuns.js.map +7 -0
  91. package/dist/run/runFlow.d.ts +78 -0
  92. package/dist/run/runFlow.js +20 -0
  93. package/dist/run/runFlow.js.map +7 -0
  94. package/dist/run/segments.d.ts +43 -0
  95. package/dist/run/segments.js +157 -0
  96. package/dist/run/segments.js.map +7 -0
  97. package/dist/run/stop.d.ts +3 -0
  98. package/dist/run/stop.js +7 -0
  99. package/dist/run/stop.js.map +7 -0
  100. package/dist/run/storage.d.ts +16 -0
  101. package/dist/run/storage.js +15 -0
  102. package/dist/run/storage.js.map +7 -0
  103. package/dist/run/tail.d.ts +5 -0
  104. package/dist/run/tail.js +7 -0
  105. package/dist/run/tail.js.map +7 -0
  106. package/dist/runner/client.d.ts +149 -0
  107. package/dist/runner/client.js +168 -0
  108. package/dist/runner/client.js.map +7 -0
  109. package/dist/runner/devBundle.d.ts +14 -0
  110. package/dist/runner/devBundle.js +78 -0
  111. package/dist/runner/devBundle.js.map +7 -0
  112. package/dist/scenario/cli.d.ts +39 -0
  113. package/dist/scenario/cli.js +125 -0
  114. package/dist/scenario/cli.js.map +7 -0
  115. package/dist/scenario/runner.d.ts +50 -0
  116. package/dist/scenario/runner.js +185 -0
  117. package/dist/scenario/runner.js.map +7 -0
  118. package/dist/sharedRefs.d.ts +22 -0
  119. package/dist/sharedRefs.js +114 -0
  120. package/dist/sharedRefs.js.map +7 -0
  121. package/dist/ssh/exec.d.ts +39 -0
  122. package/dist/ssh/exec.js +13 -0
  123. package/dist/ssh/exec.js.map +7 -0
  124. package/dist/ssh/remote.d.ts +40 -0
  125. package/dist/ssh/remote.js +25 -0
  126. package/dist/ssh/remote.js.map +7 -0
  127. package/dist/ssh/sshRun.d.ts +1 -0
  128. package/dist/ssh/sshRun.js +24 -0
  129. package/dist/ssh/sshRun.js.map +7 -0
  130. package/dist/ssh/sync.d.ts +22 -0
  131. package/dist/ssh/sync.js +12 -0
  132. package/dist/ssh/sync.js.map +7 -0
  133. package/dist/timeouts.d.ts +38 -0
  134. package/dist/timeouts.js +7 -0
  135. package/dist/timeouts.js.map +7 -0
  136. package/package.json +35 -4
@@ -0,0 +1,157 @@
1
+ import {
2
+ runFlow
3
+ } from "../chunks/chunk-LGUG2Q7Z.js";
4
+ import "../chunks/chunk-RETJPSKL.js";
5
+ import "../chunks/chunk-THNSGAKN.js";
6
+ import "../chunks/chunk-FG53MLUX.js";
7
+ import {
8
+ runsDir
9
+ } from "../chunks/chunk-4FHPTGI4.js";
10
+ import "../chunks/chunk-SSZNBJZQ.js";
11
+ import "../chunks/chunk-JDPN3LTT.js";
12
+ import "../chunks/chunk-NTZRNEZM.js";
13
+ import "../chunks/chunk-SUV3VHKR.js";
14
+ import "../chunks/chunk-I6FIOP7G.js";
15
+ import {
16
+ withoutBookkeeping
17
+ } from "../chunks/chunk-A2NMQGQE.js";
18
+ import "../chunks/chunk-GAL5LW6Y.js";
19
+ import "../chunks/chunk-IIFP6UQR.js";
20
+
21
+ // src/node/run/segments.ts
22
+ import { readFile } from "node:fs/promises";
23
+ import { join } from "node:path";
24
+ import { summarizeRun } from "@ansible-flow/core/run/events";
25
+ function segmentOutcome(segment, events) {
26
+ let carried = {};
27
+ for (const line of events.split("\n")) {
28
+ if (!line.includes('"stats"')) continue;
29
+ try {
30
+ const e = JSON.parse(line);
31
+ if (e.event === "stats" && e.custom) carried = e.custom;
32
+ } catch {
33
+ }
34
+ }
35
+ const empty = String(carried.afd_gate_empty).toLowerCase() === "true";
36
+ return { carried, waiting: !!segment.gate && !empty && "afd_hostsets" in carried, empty };
37
+ }
38
+ function nextSegment(segments, from, decision) {
39
+ if (decision === "empty") return segments.find((s) => s.finish);
40
+ const outcome = decision === "approved" ? "approved" : "rejected";
41
+ return segments.find((s) => s.parent === from && s.entry?.outcome === outcome);
42
+ }
43
+ function segmentInputs(inputs, carried, decision) {
44
+ return { ...inputs, ...carried, ...decision ? { afd_gate_outcome: decision } : {} };
45
+ }
46
+ function segmentsSummary(events, routes) {
47
+ return withoutBookkeeping(summarizeRun(events.filter(Boolean).join("\n"), routes));
48
+ }
49
+ async function runSegments(request, segments, hooks) {
50
+ const generated = request.playbook.slice(0, request.playbook.lastIndexOf("/") + 1);
51
+ const approvals = {};
52
+ const events = [];
53
+ let segment = segments.find((s) => s.name === "start");
54
+ let inputs = request.inputs;
55
+ let last;
56
+ const view = (run) => ({
57
+ ...run,
58
+ id: request.id,
59
+ summary: segmentsSummary(events, request.code.routes),
60
+ ...Object.keys(approvals).length ? { approvals: { ...approvals } } : {}
61
+ });
62
+ for (let k = 0; segment; k++) {
63
+ const current = segment;
64
+ const id = `${request.id}-${k}`;
65
+ const run = await runFlow(
66
+ { ...request, id, playbook: `${generated}${current.file}`, inputs },
67
+ {
68
+ ...hooks,
69
+ // While a segment runs: what the ones before did, and what this one does so far.
70
+ report: (r) => hooks.report({
71
+ ...r,
72
+ id: request.id,
73
+ ...r.summary ? { summary: mergeLive(events, r.summary, request.code.routes) } : {},
74
+ ...Object.keys(approvals).length ? { approvals: { ...approvals } } : {}
75
+ })
76
+ }
77
+ );
78
+ last = run;
79
+ const text = await readFile(
80
+ join(runsDir(request.storage, request.doc.flow.id), id, "events.jsonl"),
81
+ "utf8"
82
+ ).catch(() => "");
83
+ events.push(text);
84
+ if (run.state === "error" && run.errorCode === "cancelled") break;
85
+ const outcome = segmentOutcome(current, text);
86
+ if (!current.gate) break;
87
+ if (outcome.empty) {
88
+ segment = nextSegment(segments, current.name, "empty");
89
+ inputs = segmentInputs(request.inputs, outcome.carried);
90
+ continue;
91
+ }
92
+ if (!outcome.waiting) break;
93
+ approvals[current.gate] = { status: "waiting" };
94
+ const waiting = view({ ...run, state: "running" });
95
+ hooks.report(waiting);
96
+ let decided;
97
+ try {
98
+ decided = await hooks.decide(current.gate, waiting);
99
+ } catch {
100
+ approvals[current.gate] = { status: "waiting" };
101
+ const stopped = view({
102
+ ...run,
103
+ state: "error",
104
+ error: "Stopped while waiting for approval",
105
+ errorCode: "cancelled",
106
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString()
107
+ });
108
+ hooks.report(stopped);
109
+ return stopped;
110
+ }
111
+ const { decision, ...said } = decided;
112
+ approvals[current.gate] = {
113
+ ...said,
114
+ status: decision === "approved" ? "approved" : decision === "timeout" ? "timed out" : "rejected"
115
+ };
116
+ segment = nextSegment(segments, current.name, decision);
117
+ inputs = segmentInputs(request.inputs, outcome.carried, decision);
118
+ }
119
+ const done = view(
120
+ last ?? {
121
+ id: request.id,
122
+ mode: request.mode,
123
+ state: "error",
124
+ startedAt: request.startedAt,
125
+ error: "The flow has no start segment"
126
+ }
127
+ );
128
+ hooks.report(done);
129
+ return done;
130
+ }
131
+ function mergeLive(before, live, routes) {
132
+ if (before.length === 0) return withoutBookkeeping(live);
133
+ const earlier = segmentsSummary(before, routes);
134
+ const nodes = structuredClone(earlier.nodes);
135
+ for (const [node, ports] of Object.entries(live.nodes))
136
+ for (const [port, hosts] of Object.entries(ports)) {
137
+ nodes[node] ??= {};
138
+ const into = nodes[node];
139
+ into[port] = [.../* @__PURE__ */ new Set([...into[port] ?? [], ...hosts])];
140
+ }
141
+ return withoutBookkeeping({
142
+ ...live,
143
+ nodes,
144
+ hosts: [.../* @__PURE__ */ new Set([...earlier.hosts, ...live.hosts])],
145
+ failures: [...earlier.failures, ...live.failures],
146
+ outcome: { ...earlier.outcome, ...live.outcome },
147
+ unreachable: [.../* @__PURE__ */ new Set([...earlier.unreachable, ...live.unreachable])]
148
+ });
149
+ }
150
+ export {
151
+ nextSegment,
152
+ runSegments,
153
+ segmentInputs,
154
+ segmentOutcome,
155
+ segmentsSummary
156
+ };
157
+ //# sourceMappingURL=segments.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/node/run/segments.ts"],
4
+ "sourcesContent": ["// Runs of a flow with top-level approvals outside AWX (AFD-272, ADR-0016). The flow's code comes\n// in segments (engine/optionA): `start` runs until the hosts reach an approval and carries the\n// host sets in set_stats; after a decision `<gate>-approved` or `<gate>-rejected` restores them\n// and goes on; `finish` runs the outcome when no host reached the gate. AWX chains them as a\n// workflow; here they are chained step by step:\n//\n// - `segmentOutcome` reads what a segment's run ended with (its events): the host sets it\n// carried, and whether hosts wait at its gate.\n// - `nextSegment` names the segment to run after a decision (or after an empty gate).\n// - `segmentInputs` are the next segment's inputs: the run's, the carried sets, the decision.\n// - `segmentsSummary` is the run as one: every segment's events, summarized with the flow's routes.\n//\n// A host that waits for a decision longer than a process should (a server that may restart)\n// keeps the carried sets itself and calls these between processes. `runSegments` chains them in\n// one call, with a `decide` that waits for each gate's decision.\nimport { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { SegmentFile } from '../../core/engine/types';\nimport type { Ulid } from '../../core/flow/types';\nimport type { RunApproval, RunView } from '../../core/protocol';\nimport { type RunSummary, summarizeRun } from '../../core/run/events';\nimport { withoutBookkeeping } from '../awx/awxWorkflow';\nimport { runsDir } from './pastRuns';\nimport { type RunFlowHooks, type RunFlowRequest, runFlow } from './runFlow';\n\n/** How a gate was decided. `timeout` takes the approval's timeout port (else rejected's). */\nexport type GateDecision = 'approved' | 'rejected' | 'timeout';\n\n/** What a segment's run left for the next one. */\nexport interface SegmentOutcome {\n /** The set_stats data the segment carried (`afd_hostsets`, `afd_gate_empty`); empty if none. */\n carried: Record<string, unknown>;\n /** Hosts wait at the segment's gate: a decision is needed. */\n waiting: boolean;\n /** The gate had no host: the flow goes on with `finish`. */\n empty: boolean;\n}\n\n/** The carried data of a segment's run, from its events (the last stats event's `custom`). */\nexport function segmentOutcome(segment: SegmentFile, events: string): SegmentOutcome {\n let carried: Record<string, unknown> = {};\n for (const line of events.split('\\n')) {\n if (!line.includes('\"stats\"')) continue;\n try {\n const e = JSON.parse(line) as { event?: string; custom?: Record<string, unknown> };\n if (e.event === 'stats' && e.custom) carried = e.custom;\n } catch {}\n }\n const empty = String(carried.afd_gate_empty).toLowerCase() === 'true';\n return { carried, waiting: !!segment.gate && !empty && 'afd_hostsets' in carried, empty };\n}\n\n/**\n * The segment after `from`: its approved or rejected continuation for a decision (a timeout runs\n * the rejected one, which then takes the timeout port), `finish` when nobody reached its gate.\n * Undefined when there is none: the run has ended.\n */\nexport function nextSegment(\n segments: SegmentFile[],\n from: string,\n decision: GateDecision | 'empty',\n): SegmentFile | undefined {\n if (decision === 'empty') return segments.find((s) => s.finish);\n const outcome = decision === 'approved' ? 'approved' : 'rejected';\n return segments.find((s) => s.parent === from && s.entry?.outcome === outcome);\n}\n\n/** The inputs of the segment after a decision: the run's, the carried host sets, the decision. */\nexport function segmentInputs(\n inputs: Record<string, unknown>,\n carried: Record<string, unknown>,\n decision?: GateDecision,\n): Record<string, unknown> {\n return { ...inputs, ...carried, ...(decision ? { afd_gate_outcome: decision } : {}) };\n}\n\n/** The run as one: the events of its segments in order, summarized with the flow's routes. */\nexport function segmentsSummary(\n events: string[],\n routes: Parameters<typeof summarizeRun>[1],\n): RunSummary {\n // The plays that restore and carry host sets run on localhost outside any node.\n return withoutBookkeeping(summarizeRun(events.filter(Boolean).join('\\n'), routes));\n}\n\nexport interface SegmentsHooks extends RunFlowHooks {\n /**\n * The decision for the approval `gate`, when hosts wait there. Called with the run as it is\n * then (state `running`, the gate's approval `waiting`). Rejects to stop the run.\n */\n decide(gate: Ulid, run: RunView): Promise<{ decision: GateDecision } & Partial<RunApproval>>;\n}\n\n/**\n * Runs a flow with approval gates segment by segment in one call: each segment is a `runFlow` of\n * its playbook (folder `<id>-<n>`), each gate waits for `decide`. Reports the run as one (id\n * `request.id`), with its approvals; resolves with the finished run. Never rejects.\n */\nexport async function runSegments(\n request: RunFlowRequest,\n segments: SegmentFile[],\n hooks: SegmentsHooks,\n): Promise<RunView> {\n const generated = request.playbook.slice(0, request.playbook.lastIndexOf('/') + 1);\n const approvals: Record<Ulid, RunApproval> = {};\n const events: string[] = [];\n let segment = segments.find((s) => s.name === 'start');\n let inputs = request.inputs;\n let last: RunView | undefined;\n /** The run as one, with what the segments that ended did. */\n const view = (run: RunView): RunView => ({\n ...run,\n id: request.id,\n summary: segmentsSummary(events, request.code.routes),\n ...(Object.keys(approvals).length ? { approvals: { ...approvals } } : {}),\n });\n for (let k = 0; segment; k++) {\n const current = segment;\n const id = `${request.id}-${k}`;\n const run = await runFlow(\n { ...request, id, playbook: `${generated}${current.file}`, inputs },\n {\n ...hooks,\n // While a segment runs: what the ones before did, and what this one does so far.\n report: (r) =>\n hooks.report({\n ...r,\n id: request.id,\n ...(r.summary ? { summary: mergeLive(events, r.summary, request.code.routes) } : {}),\n ...(Object.keys(approvals).length ? { approvals: { ...approvals } } : {}),\n }),\n },\n );\n last = run;\n const text = await readFile(\n join(runsDir(request.storage, request.doc.flow.id), id, 'events.jsonl'),\n 'utf8',\n ).catch(() => '');\n events.push(text);\n if (run.state === 'error' && run.errorCode === 'cancelled') break;\n const outcome = segmentOutcome(current, text);\n if (!current.gate) break;\n if (outcome.empty) {\n segment = nextSegment(segments, current.name, 'empty');\n inputs = segmentInputs(request.inputs, outcome.carried);\n continue;\n }\n if (!outcome.waiting) break;\n approvals[current.gate] = { status: 'waiting' };\n const waiting = view({ ...run, state: 'running' });\n hooks.report(waiting);\n let decided: { decision: GateDecision } & Partial<RunApproval>;\n try {\n decided = await hooks.decide(current.gate, waiting);\n } catch {\n approvals[current.gate] = { status: 'waiting' };\n const stopped: RunView = view({\n ...run,\n state: 'error',\n error: 'Stopped while waiting for approval',\n errorCode: 'cancelled',\n finishedAt: new Date().toISOString(),\n });\n hooks.report(stopped);\n return stopped;\n }\n const { decision, ...said } = decided;\n approvals[current.gate] = {\n ...said,\n status:\n decision === 'approved' ? 'approved' : decision === 'timeout' ? 'timed out' : 'rejected',\n };\n segment = nextSegment(segments, current.name, decision);\n inputs = segmentInputs(request.inputs, outcome.carried, decision);\n }\n const done = view(\n last ?? {\n id: request.id,\n mode: request.mode,\n state: 'error',\n startedAt: request.startedAt,\n error: 'The flow has no start segment',\n },\n );\n hooks.report(done);\n return done;\n}\n\n/** A live segment's summary added to what the segments before it did. */\nfunction mergeLive(\n before: string[],\n live: RunSummary,\n routes: Parameters<typeof summarizeRun>[1],\n): RunSummary {\n if (before.length === 0) return withoutBookkeeping(live);\n const earlier = segmentsSummary(before, routes);\n const nodes: RunSummary['nodes'] = structuredClone(earlier.nodes);\n for (const [node, ports] of Object.entries(live.nodes))\n for (const [port, hosts] of Object.entries(ports)) {\n nodes[node] ??= {};\n const into = nodes[node] as Record<string, string[]>;\n into[port] = [...new Set([...(into[port] ?? []), ...hosts])];\n }\n return withoutBookkeeping({\n ...live,\n nodes,\n hosts: [...new Set([...earlier.hosts, ...live.hosts])],\n failures: [...earlier.failures, ...live.failures],\n outcome: { ...earlier.outcome, ...live.outcome },\n unreachable: [...new Set([...earlier.unreachable, ...live.unreachable])],\n });\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProcessSpec } from '@ansible-flow/core/run/command';
2
+ /** Runs `spec.stop` when it has one. Resolves once it ended or after `timeoutMs`; never rejects. */
3
+ export declare function stopStarted(spec: ProcessSpec, timeoutMs?: number): Promise<void>;
@@ -0,0 +1,7 @@
1
+ import {
2
+ stopStarted
3
+ } from "../chunks/chunk-JDPN3LTT.js";
4
+ export {
5
+ stopStarted
6
+ };
7
+ //# sourceMappingURL=stop.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -0,0 +1,16 @@
1
+ /** Run folders kept per flow when `afd.runs.keep` is not set. */
2
+ export declare const KEEP_RUNS = 30;
3
+ /** Removes a run's vars.json once ansible-playbook is done with it. */
4
+ export declare function dropVars(runDir: string): Promise<void>;
5
+ /** Keeps the newest `keep` run folders of a flow (run ids are sortable time stamps). */
6
+ export declare function pruneRuns(storage: string, flowId: string, keep: number): Promise<void>;
7
+ /**
8
+ * At startup, when no run is going: removes the vars.json files earlier versions left behind (they
9
+ * held every input, secrets included) and applies the retention to every flow.
10
+ */
11
+ export declare function sweepRuns(storage: string, keep: number): Promise<void>;
12
+ /**
13
+ * Rotates `<name>.jsonl` once it reaches `limit` bytes: it becomes `<name>.1.jsonl`, the older ones
14
+ * move up, and only `old` of them are kept.
15
+ */
16
+ export declare function rotateLog(file: string, limit: number, old: number): Promise<void>;
@@ -0,0 +1,15 @@
1
+ import {
2
+ KEEP_RUNS,
3
+ dropVars,
4
+ pruneRuns,
5
+ rotateLog,
6
+ sweepRuns
7
+ } from "../chunks/chunk-SUV3VHKR.js";
8
+ export {
9
+ KEEP_RUNS,
10
+ dropVars,
11
+ pruneRuns,
12
+ rotateLog,
13
+ sweepRuns
14
+ };
15
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -0,0 +1,5 @@
1
+ export interface Tail {
2
+ /** One last read; a final line without a line break counts too. */
3
+ stop(): Promise<void>;
4
+ }
5
+ export declare function tailFile(path: string, onLines: (lines: string[]) => void, intervalMs?: number): Tail;
@@ -0,0 +1,7 @@
1
+ import {
2
+ tailFile
3
+ } from "../chunks/chunk-SSZNBJZQ.js";
4
+ export {
5
+ tailFile
6
+ };
7
+ //# sourceMappingURL=tail.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -0,0 +1,149 @@
1
+ export declare class RunnerError extends Error {
2
+ readonly status?: number | undefined;
3
+ /** The server's error code (`not_found`, `read_only_token`, …). */
4
+ readonly code?: string | undefined;
5
+ /** A launch's problems, by field. */
6
+ readonly problems?: {
7
+ field?: string;
8
+ message: string;
9
+ }[] | undefined;
10
+ constructor(message: string, status?: number | undefined,
11
+ /** The server's error code (`not_found`, `read_only_token`, …). */
12
+ code?: string | undefined,
13
+ /** A launch's problems, by field. */
14
+ problems?: {
15
+ field?: string;
16
+ message: string;
17
+ }[] | undefined);
18
+ }
19
+ export interface RunnerMeta {
20
+ name: string;
21
+ version: string;
22
+ api: string;
23
+ designer: {
24
+ version: string;
25
+ format: number;
26
+ formats?: number[];
27
+ bundle?: string;
28
+ };
29
+ features?: string[];
30
+ }
31
+ export interface RunnerProject {
32
+ id: string;
33
+ key: string;
34
+ name: string;
35
+ gitPath: string;
36
+ gitRef: string;
37
+ commit?: string;
38
+ }
39
+ export interface RunnerConfig {
40
+ id: string;
41
+ name: string;
42
+ flowFile: string;
43
+ mode: 'check' | 'real';
44
+ inventory?: string;
45
+ gitRef?: string;
46
+ commit?: string;
47
+ }
48
+ /** What a launch would run (the server's PreparedLaunch, without its inputs). */
49
+ export interface RunnerLaunch {
50
+ commit: string;
51
+ ref?: string;
52
+ flow: {
53
+ file: string;
54
+ key: string;
55
+ name: string;
56
+ version: number;
57
+ };
58
+ mode: 'check' | 'real';
59
+ inventory?: string;
60
+ targets: string[];
61
+ }
62
+ export interface RunnerRun {
63
+ id: string;
64
+ number: number;
65
+ project: string;
66
+ commit: string;
67
+ flow: {
68
+ file: string;
69
+ key: string;
70
+ name: string;
71
+ };
72
+ mode: 'check' | 'real';
73
+ state: 'queued' | 'running' | 'waiting' | 'succeeded' | 'failed' | 'cancelled' | 'lost';
74
+ error?: string;
75
+ endedAt?: string;
76
+ /** AFD-282: a check run of someone's unpushed work. */
77
+ dev?: true;
78
+ }
79
+ export interface RunnerGate {
80
+ node: {
81
+ id: string;
82
+ key: string;
83
+ title: string;
84
+ };
85
+ status: string;
86
+ mayDecide: boolean;
87
+ why?: string;
88
+ }
89
+ export interface LaunchRequest {
90
+ mode?: 'check' | 'real';
91
+ /** AFD-282: a dev bundle's id: the server runs its commit, in check mode. */
92
+ dev?: string;
93
+ limit?: string;
94
+ /** The flow's inputs as typed text, by name. */
95
+ inputs?: Record<string, string>;
96
+ }
97
+ /** The REST API this client speaks. */
98
+ export declare const RUNNER_API = "v1";
99
+ /** Why this client and the server do not fit for a flow in `format`, or undefined. */
100
+ export declare function incompatible(meta: RunnerMeta, format: number): string | undefined;
101
+ export interface RunnerOptions {
102
+ verifyTls?: boolean;
103
+ /** Send the token over plain http to another machine (a lab only). */
104
+ allowHttp?: boolean;
105
+ timeoutMs?: number;
106
+ }
107
+ export declare class RunnerClient {
108
+ private readonly token;
109
+ private readonly options;
110
+ readonly base: string;
111
+ private readonly origin;
112
+ constructor(url: string, token: string, options?: RunnerOptions);
113
+ /** A run's page on the server. */
114
+ runUrl(id: string): string;
115
+ /** The answer's body as text, and its status. */
116
+ private send;
117
+ request<T>(method: string, path: string, body?: unknown, raw?: {
118
+ bytes: Buffer;
119
+ type: string;
120
+ }): Promise<T>;
121
+ /** The output of a request whose answer is plain text. */
122
+ text(path: string): Promise<string>;
123
+ meta: () => Promise<RunnerMeta>;
124
+ me: () => Promise<{
125
+ login: string;
126
+ name: string;
127
+ }>;
128
+ projects: (workspace: string) => Promise<RunnerProject[]>;
129
+ project: (id: string) => Promise<RunnerProject>;
130
+ configs: (project: string) => Promise<RunnerConfig[]>;
131
+ check: (config: string, req: LaunchRequest) => Promise<RunnerLaunch>;
132
+ launch: (config: string, req: LaunchRequest) => Promise<RunnerRun>;
133
+ run: (id: string) => Promise<RunnerRun>;
134
+ /** How the run went, as the designer shows it (the server's RunView); 404 until it has one. */
135
+ view: (id: string) => Promise<Record<string, unknown> | undefined>;
136
+ gates: (id: string) => Promise<RunnerGate[]>;
137
+ cancel: (id: string) => Promise<RunnerRun>;
138
+ decide: (id: string, node: string, decision: 'approved' | 'rejected', comment?: string) => Promise<unknown>;
139
+ /** AFD-282: unpushed work for a dev run; resolves with the bundle's id and commit. */
140
+ devBundle: (project: string, bytes: Buffer) => Promise<{
141
+ id: string;
142
+ commit: string;
143
+ expiresAt: string;
144
+ }>;
145
+ /** What one host did in one step of the run. */
146
+ hostOutput: (id: string, node: string, host: string) => Promise<{
147
+ entries?: unknown[];
148
+ }>;
149
+ }
@@ -0,0 +1,168 @@
1
+ import {
2
+ cleartextTo
3
+ } from "../chunks/chunk-GAL5LW6Y.js";
4
+ import {
5
+ TIMEOUTS
6
+ } from "../chunks/chunk-IIFP6UQR.js";
7
+
8
+ // src/node/runner/client.ts
9
+ import * as http from "node:http";
10
+ import * as https from "node:https";
11
+ var RunnerError = class extends Error {
12
+ constructor(message, status, code, problems) {
13
+ super(message);
14
+ this.status = status;
15
+ this.code = code;
16
+ this.problems = problems;
17
+ this.name = "RunnerError";
18
+ }
19
+ status;
20
+ code;
21
+ problems;
22
+ };
23
+ var RUNNER_API = "v1";
24
+ function incompatible(meta, format) {
25
+ if (meta.name !== "Ansible Flow Server") return "The address is not an Ansible Flow Server";
26
+ if (meta.api !== RUNNER_API)
27
+ return `The server speaks API ${meta.api}; this extension speaks ${RUNNER_API}: update the one that is older`;
28
+ const formats = meta.designer.formats ?? [meta.designer.format];
29
+ if (!formats.includes(format))
30
+ return `The flow is in format ${format}; the server (${meta.version}) reads formats ${formats.join(", ")}: upgrade the server`;
31
+ return void 0;
32
+ }
33
+ var MAX_BODY = 32 * 1024 * 1024;
34
+ var enc = encodeURIComponent;
35
+ var RunnerClient = class {
36
+ constructor(url, token, options = {}) {
37
+ this.token = token;
38
+ this.options = options;
39
+ this.base = url.replace(/\/+$/, "");
40
+ this.origin = new URL(this.base).origin;
41
+ }
42
+ token;
43
+ options;
44
+ base;
45
+ origin;
46
+ /** A run's page on the server. */
47
+ runUrl(id) {
48
+ return `${this.base}/runs/${enc(id)}`;
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}`);
53
+ if (url.origin !== this.origin)
54
+ return Promise.reject(new RunnerError(`Not sending the token to ${url.origin}`));
55
+ 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
+ )
60
+ );
61
+ const lib = url.protocol === "https:" ? https : http;
62
+ const data = raw?.bytes ?? (body === void 0 ? void 0 : JSON.stringify(body));
63
+ const type = raw?.type ?? "application/json";
64
+ return new Promise((resolve, reject) => {
65
+ const req = lib.request(
66
+ url,
67
+ {
68
+ method,
69
+ headers: {
70
+ Authorization: `Bearer ${this.token}`,
71
+ Accept: "application/json",
72
+ ...data ? { "Content-Type": type, "Content-Length": Buffer.byteLength(data) } : {}
73
+ },
74
+ timeout: this.options.timeoutMs ?? TIMEOUTS.runnerRequest,
75
+ ...url.protocol === "https:" ? { rejectUnauthorized: this.options.verifyTls !== false } : {}
76
+ },
77
+ (res) => {
78
+ const chunks = [];
79
+ let size = 0;
80
+ res.on("data", (c) => {
81
+ size += c.length;
82
+ if (size > MAX_BODY) {
83
+ req.destroy(new Error("the answer is too large"));
84
+ return;
85
+ }
86
+ chunks.push(c);
87
+ });
88
+ res.on(
89
+ "end",
90
+ () => resolve({ status: res.statusCode ?? 0, text: Buffer.concat(chunks).toString("utf8") })
91
+ );
92
+ res.on("error", reject);
93
+ }
94
+ );
95
+ req.on("timeout", () => req.destroy(new Error("no answer in time")));
96
+ req.on(
97
+ "error",
98
+ (e) => reject(new RunnerError(`The server at ${this.base} does not answer: ${e.message}`))
99
+ );
100
+ req.end(data);
101
+ });
102
+ }
103
+ async request(method, path, body, raw) {
104
+ const { status, text } = await this.send(method, path, body, raw);
105
+ let json;
106
+ try {
107
+ json = text ? JSON.parse(text) : void 0;
108
+ } catch {
109
+ json = void 0;
110
+ }
111
+ if (status >= 200 && status < 300) return json;
112
+ const e = json ?? {};
113
+ const why = status === 401 ? "The server does not take the token: set it again (it may have expired or been revoked)" : status === 413 ? "The work is larger than the server takes (AFS_DEV_BUNDLE_MAX_MB)" : e.message ?? `The server answered ${status}`;
114
+ throw new RunnerError(why, status, e.error, e.problems);
115
+ }
116
+ /** The output of a request whose answer is plain text. */
117
+ async text(path) {
118
+ const { status, text } = await this.send("GET", path);
119
+ if (status >= 200 && status < 300) return text;
120
+ throw new RunnerError(`The server answered ${status}`, status);
121
+ }
122
+ meta = () => this.request("GET", "/api/v1/meta");
123
+ me = () => this.request("GET", "/api/v1/session");
124
+ projects = (workspace) => this.request("GET", `/api/v1/workspaces/${enc(workspace)}/projects`);
125
+ project = (id) => this.request("GET", `/api/v1/projects/${enc(id)}`);
126
+ configs = (project) => this.request("GET", `/api/v1/projects/${enc(project)}/launch-configs`);
127
+ check = async (config, req) => (await this.request(
128
+ "POST",
129
+ `/api/v1/launch-configs/${enc(config)}/check`,
130
+ req
131
+ )).launch;
132
+ launch = (config, req) => this.request("POST", `/api/v1/launch-configs/${enc(config)}/launch`, req);
133
+ run = (id) => this.request("GET", `/api/v1/runs/${enc(id)}`);
134
+ /** How the run went, as the designer shows it (the server's RunView); 404 until it has one. */
135
+ view = async (id) => {
136
+ try {
137
+ return await this.request("GET", `/api/v1/runs/${enc(id)}/view`);
138
+ } catch (e) {
139
+ if (e instanceof RunnerError && e.status === 404) return void 0;
140
+ throw e;
141
+ }
142
+ };
143
+ gates = (id) => this.request("GET", `/api/v1/runs/${enc(id)}/approvals`);
144
+ cancel = (id) => this.request("POST", `/api/v1/runs/${enc(id)}/cancel`);
145
+ decide = (id, node, decision, comment) => this.request("POST", `/api/v1/runs/${enc(id)}/approvals/${enc(node)}`, {
146
+ decision,
147
+ ...comment?.trim() ? { comment: comment.trim() } : {}
148
+ });
149
+ /** AFD-282: unpushed work for a dev run; resolves with the bundle's id and commit. */
150
+ devBundle = (project, bytes) => this.request(
151
+ "POST",
152
+ `/api/v1/projects/${enc(project)}/dev-bundles`,
153
+ void 0,
154
+ { bytes, type: "application/x-git-bundle" }
155
+ );
156
+ /** What one host did in one step of the run. */
157
+ hostOutput = (id, node, host) => this.request(
158
+ "GET",
159
+ `/api/v1/runs/${enc(id)}/hosts/${enc(node)}/${enc(host)}`
160
+ );
161
+ };
162
+ export {
163
+ RUNNER_API,
164
+ RunnerClient,
165
+ RunnerError,
166
+ incompatible
167
+ };
168
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 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": ";;;;;;;;AAIA,YAAY,UAAU;AACtB,YAAY,WAAW;AAIhB,IAAM,cAAN,cAA0B,MAAM;AAAA,EACrC,YACE,SACS,QAEA,MAEA,UACT;AACA,UAAM,OAAO;AANJ;AAEA;AAEA;AAGT,SAAK,OAAO;AAAA,EACd;AAAA,EARW;AAAA,EAEA;AAAA,EAEA;AAKb;AAsEO,IAAM,aAAa;AAGnB,SAAS,aAAa,MAAkB,QAAoC;AACjF,MAAI,KAAK,SAAS,sBAAuB,QAAO;AAChD,MAAI,KAAK,QAAQ;AACf,WAAO,yBAAyB,KAAK,GAAG,2BAA2B,UAAU;AAC/E,QAAM,UAAU,KAAK,SAAS,WAAW,CAAC,KAAK,SAAS,MAAM;AAC9D,MAAI,CAAC,QAAQ,SAAS,MAAM;AAC1B,WAAO,yBAAyB,MAAM,iBAAiB,KAAK,OAAO,mBAAmB,QAAQ,KAAK,IAAI,CAAC;AAC1G,SAAO;AACT;AASA,IAAM,WAAW,KAAK,OAAO;AAC7B,IAAM,MAAM;AAEL,IAAM,eAAN,MAAmB;AAAA,EAIxB,YACE,KACiB,OACA,UAAyB,CAAC,GAC3C;AAFiB;AACA;AAEjB,SAAK,OAAO,IAAI,QAAQ,QAAQ,EAAE;AAClC,SAAK,SAAS,IAAI,IAAI,KAAK,IAAI,EAAE;AAAA,EACnC;AAAA,EALmB;AAAA,EACA;AAAA,EANV;AAAA,EACQ;AAAA;AAAA,EAYjB,OAAO,IAAoB;AACzB,WAAO,GAAG,KAAK,IAAI,SAAS,IAAI,EAAE,CAAC;AAAA,EACrC;AAAA;AAAA,EAGQ,KACN,QACA,MACA,MAEA,KAC2C;AAC3C,UAAM,MAAM,IAAI,IAAI,GAAG,KAAK,IAAI,GAAG,IAAI,EAAE;AACzC,QAAI,IAAI,WAAW,KAAK;AACtB,aAAO,QAAQ,OAAO,IAAI,YAAY,4BAA4B,IAAI,MAAM,EAAE,CAAC;AACjF,QAAI,YAAY,GAAG,KAAK,CAAC,KAAK,QAAQ;AACpC,aAAO,QAAQ;AAAA,QACb,IAAI;AAAA,UACF,wCAAwC,IAAI,MAAM;AAAA,QACpD;AAAA,MACF;AACF,UAAM,MAAM,IAAI,aAAa,WAAW,QAAQ;AAChD,UAAM,OAAO,KAAK,UAAU,SAAS,SAAY,SAAY,KAAK,UAAU,IAAI;AAChF,UAAM,OAAO,KAAK,QAAQ;AAC1B,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,YAAM,MAAM,IAAI;AAAA,QACd;AAAA,QACA;AAAA,UACE;AAAA,UACA,SAAS;AAAA,YACP,eAAe,UAAU,KAAK,KAAK;AAAA,YACnC,QAAQ;AAAA,YACR,GAAI,OAAO,EAAE,gBAAgB,MAAM,kBAAkB,OAAO,WAAW,IAAI,EAAE,IAAI,CAAC;AAAA,UACpF;AAAA,UACA,SAAS,KAAK,QAAQ,aAAa,SAAS;AAAA,UAC5C,GAAI,IAAI,aAAa,WACjB,EAAE,oBAAoB,KAAK,QAAQ,cAAc,MAAM,IACvD,CAAC;AAAA,QACP;AAAA,QACA,CAAC,QAAQ;AACP,gBAAM,SAAmB,CAAC;AAC1B,cAAI,OAAO;AACX,cAAI,GAAG,QAAQ,CAAC,MAAc;AAC5B,oBAAQ,EAAE;AACV,gBAAI,OAAO,UAAU;AACnB,kBAAI,QAAQ,IAAI,MAAM,yBAAyB,CAAC;AAChD;AAAA,YACF;AACA,mBAAO,KAAK,CAAC;AAAA,UACf,CAAC;AACD,cAAI;AAAA,YAAG;AAAA,YAAO,MACZ,QAAQ,EAAE,QAAQ,IAAI,cAAc,GAAG,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;AAAA,UACvF;AACA,cAAI,GAAG,SAAS,MAAM;AAAA,QACxB;AAAA,MACF;AACA,UAAI,GAAG,WAAW,MAAM,IAAI,QAAQ,IAAI,MAAM,mBAAmB,CAAC,CAAC;AACnE,UAAI;AAAA,QAAG;AAAA,QAAS,CAAC,MACf,OAAO,IAAI,YAAY,iBAAiB,KAAK,IAAI,qBAAqB,EAAE,OAAO,EAAE,CAAC;AAAA,MACpF;AACA,UAAI,IAAI,IAAI;AAAA,IACd,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QACJ,QACA,MACA,MACA,KACY;AACZ,UAAM,EAAE,QAAQ,KAAK,IAAI,MAAM,KAAK,KAAK,QAAQ,MAAM,MAAM,GAAG;AAChE,QAAI;AACJ,QAAI;AACF,aAAO,OAAO,KAAK,MAAM,IAAI,IAAI;AAAA,IACnC,QAAQ;AACN,aAAO;AAAA,IACT;AACA,QAAI,UAAU,OAAO,SAAS,IAAK,QAAO;AAC1C,UAAM,IAAK,QAAQ,CAAC;AAKpB,UAAM,MACJ,WAAW,MACP,2FACA,WAAW,MACT,qEACC,EAAE,WAAW,uBAAuB,MAAM;AACnD,UAAM,IAAI,YAAY,KAAK,QAAQ,EAAE,OAAO,EAAE,QAAQ;AAAA,EACxD;AAAA;AAAA,EAGA,MAAM,KAAK,MAA+B;AACxC,UAAM,EAAE,QAAQ,KAAK,IAAI,MAAM,KAAK,KAAK,OAAO,IAAI;AACpD,QAAI,UAAU,OAAO,SAAS,IAAK,QAAO;AAC1C,UAAM,IAAI,YAAY,uBAAuB,MAAM,IAAI,MAAM;AAAA,EAC/D;AAAA,EAEA,OAAO,MAAM,KAAK,QAAoB,OAAO,cAAc;AAAA,EAC3D,KAAK,MAAM,KAAK,QAAyC,OAAO,iBAAiB;AAAA,EACjF,WAAW,CAAC,cACV,KAAK,QAAyB,OAAO,sBAAsB,IAAI,SAAS,CAAC,WAAW;AAAA,EACtF,UAAU,CAAC,OAAe,KAAK,QAAuB,OAAO,oBAAoB,IAAI,EAAE,CAAC,EAAE;AAAA,EAC1F,UAAU,CAAC,YACT,KAAK,QAAwB,OAAO,oBAAoB,IAAI,OAAO,CAAC,iBAAiB;AAAA,EACvF,QAAQ,OAAO,QAAgB,SAE3B,MAAM,KAAK;AAAA,IACT;AAAA,IACA,0BAA0B,IAAI,MAAM,CAAC;AAAA,IACrC;AAAA,EACF,GACA;AAAA,EACJ,SAAS,CAAC,QAAgB,QACxB,KAAK,QAAmB,QAAQ,0BAA0B,IAAI,MAAM,CAAC,WAAW,GAAG;AAAA,EACrF,MAAM,CAAC,OAAe,KAAK,QAAmB,OAAO,gBAAgB,IAAI,EAAE,CAAC,EAAE;AAAA;AAAA,EAE9E,OAAO,OAAO,OAAe;AAC3B,QAAI;AACF,aAAO,MAAM,KAAK,QAAiC,OAAO,gBAAgB,IAAI,EAAE,CAAC,OAAO;AAAA,IAC1F,SAAS,GAAG;AACV,UAAI,aAAa,eAAe,EAAE,WAAW,IAAK,QAAO;AACzD,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EACA,QAAQ,CAAC,OAAe,KAAK,QAAsB,OAAO,gBAAgB,IAAI,EAAE,CAAC,YAAY;AAAA,EAC7F,SAAS,CAAC,OAAe,KAAK,QAAmB,QAAQ,gBAAgB,IAAI,EAAE,CAAC,SAAS;AAAA,EACzF,SAAS,CAAC,IAAY,MAAc,UAAmC,YACrE,KAAK,QAAiB,QAAQ,gBAAgB,IAAI,EAAE,CAAC,cAAc,IAAI,IAAI,CAAC,IAAI;AAAA,IAC9E;AAAA,IACA,GAAI,SAAS,KAAK,IAAI,EAAE,SAAS,QAAQ,KAAK,EAAE,IAAI,CAAC;AAAA,EACvD,CAAC;AAAA;AAAA,EAEH,YAAY,CAAC,SAAiB,UAC5B,KAAK;AAAA,IACH;AAAA,IACA,oBAAoB,IAAI,OAAO,CAAC;AAAA,IAChC;AAAA,IACA,EAAE,OAAO,MAAM,2BAA2B;AAAA,EAC5C;AAAA;AAAA,EAEF,aAAa,CAAC,IAAY,MAAc,SACtC,KAAK;AAAA,IACH;AAAA,IACA,gBAAgB,IAAI,EAAE,CAAC,UAAU,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC;AAAA,EACzD;AACJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,14 @@
1
+ export interface DevWork {
2
+ /** The commit the bundle holds: HEAD, or the commit of the working tree. */
3
+ commit: string;
4
+ /** It has uncommitted changes in it. */
5
+ uncommitted: boolean;
6
+ /** Bundled from the server's commit (else the whole history). */
7
+ fromServer: boolean;
8
+ bundle: Buffer;
9
+ }
10
+ /**
11
+ * The work in the repository at `root` as a bundle, or undefined when it is the server's commit
12
+ * (nothing to send).
13
+ */
14
+ export declare function devBundle(root: string, serverCommit: string): Promise<DevWork | undefined>;