@ansible-flow/node 0.0.0-stage → 0.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +9 -2
- package/dist/ai/agents.d.ts +34 -0
- package/dist/ai/agents.js +16 -0
- package/dist/ai/agents.js.map +7 -0
- package/dist/ai/hostConfig.d.ts +44 -0
- package/dist/ai/hostConfig.js +120 -0
- package/dist/ai/hostConfig.js.map +7 -0
- package/dist/ai/hosts.d.ts +28 -0
- package/dist/ai/hosts.js +9 -0
- package/dist/ai/hosts.js.map +7 -0
- package/dist/ai/writers.d.ts +124 -0
- package/dist/ai/writers.js +75 -0
- package/dist/ai/writers.js.map +7 -0
- package/dist/autoLayout.d.ts +38 -0
- package/dist/autoLayout.js +145 -0
- package/dist/autoLayout.js.map +7 -0
- package/dist/awx/awxWorkflow.d.ts +47 -0
- package/dist/awx/awxWorkflow.js +17 -0
- package/dist/awx/awxWorkflow.js.map +7 -0
- package/dist/awx/client.d.ts +77 -0
- package/dist/awx/client.js +22 -0
- package/dist/awx/client.js.map +7 -0
- package/dist/catalog/bindingCheck.d.ts +4 -0
- package/dist/catalog/bindingCheck.js +69 -0
- package/dist/catalog/bindingCheck.js.map +7 -0
- package/dist/catalog/catalogIndex.d.ts +52 -0
- package/dist/catalog/catalogIndex.js +13 -0
- package/dist/catalog/catalogIndex.js.map +7 -0
- package/dist/catalog/catalogWorker.d.ts +17 -0
- package/dist/catalog/catalogWorker.js +38 -0
- package/dist/catalog/catalogWorker.js.map +7 -0
- package/dist/catalog/dumpFiles.d.ts +3 -0
- package/dist/catalog/dumpFiles.js +19 -0
- package/dist/catalog/dumpFiles.js.map +7 -0
- package/dist/chunks/chunk-2MZQHIXP.js +614 -0
- package/dist/chunks/chunk-2MZQHIXP.js.map +7 -0
- package/dist/chunks/chunk-4FHPTGI4.js +52 -0
- package/dist/chunks/chunk-4FHPTGI4.js.map +7 -0
- package/dist/chunks/chunk-A2NMQGQE.js +189 -0
- package/dist/chunks/chunk-A2NMQGQE.js.map +7 -0
- package/dist/chunks/chunk-EPJKJBNW.js +181 -0
- package/dist/chunks/chunk-EPJKJBNW.js.map +7 -0
- package/dist/chunks/chunk-FG53MLUX.js +136 -0
- package/dist/chunks/chunk-FG53MLUX.js.map +7 -0
- package/dist/chunks/chunk-GAL5LW6Y.js +269 -0
- package/dist/chunks/chunk-GAL5LW6Y.js.map +7 -0
- package/dist/chunks/chunk-HPVT6I36.js +371 -0
- package/dist/chunks/chunk-HPVT6I36.js.map +7 -0
- package/dist/chunks/chunk-I6FIOP7G.js +48 -0
- package/dist/chunks/chunk-I6FIOP7G.js.map +7 -0
- package/dist/chunks/chunk-IIFP6UQR.js +44 -0
- package/dist/chunks/chunk-IIFP6UQR.js.map +7 -0
- package/dist/chunks/chunk-JDPN3LTT.js +18 -0
- package/dist/chunks/chunk-JDPN3LTT.js.map +7 -0
- package/dist/chunks/chunk-LGUG2Q7Z.js +191 -0
- package/dist/chunks/chunk-LGUG2Q7Z.js.map +7 -0
- package/dist/chunks/chunk-NTZRNEZM.js +25 -0
- package/dist/chunks/chunk-NTZRNEZM.js.map +7 -0
- package/dist/chunks/chunk-RETJPSKL.js +123 -0
- package/dist/chunks/chunk-RETJPSKL.js.map +7 -0
- package/dist/chunks/chunk-SSZNBJZQ.js +52 -0
- package/dist/chunks/chunk-SSZNBJZQ.js.map +7 -0
- package/dist/chunks/chunk-SUV3VHKR.js +52 -0
- package/dist/chunks/chunk-SUV3VHKR.js.map +7 -0
- package/dist/chunks/chunk-THNSGAKN.js +133 -0
- package/dist/chunks/chunk-THNSGAKN.js.map +7 -0
- package/dist/chunks/chunk-UNCR5JOZ.js +85 -0
- package/dist/chunks/chunk-UNCR5JOZ.js.map +7 -0
- package/dist/env/probeClient.d.ts +77 -0
- package/dist/env/probeClient.js +244 -0
- package/dist/env/probeClient.js.map +7 -0
- package/dist/env/userSettings.d.ts +33 -0
- package/dist/env/userSettings.js +101 -0
- package/dist/env/userSettings.js.map +7 -0
- package/dist/flowMerge.d.ts +35 -0
- package/dist/flowMerge.js +74 -0
- package/dist/flowMerge.js.map +7 -0
- package/dist/run/audit.d.ts +2 -0
- package/dist/run/audit.js +10 -0
- package/dist/run/audit.js.map +7 -0
- package/dist/run/exec.d.ts +14 -0
- package/dist/run/exec.js +7 -0
- package/dist/run/exec.js.map +7 -0
- package/dist/run/facts.d.ts +39 -0
- package/dist/run/facts.js +96 -0
- package/dist/run/facts.js.map +7 -0
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +8 -0
- package/dist/run/ids.js.map +7 -0
- package/dist/run/pastRuns.d.ts +9 -0
- package/dist/run/pastRuns.js +13 -0
- package/dist/run/pastRuns.js.map +7 -0
- package/dist/run/runFlow.d.ts +78 -0
- package/dist/run/runFlow.js +20 -0
- package/dist/run/runFlow.js.map +7 -0
- package/dist/run/segments.d.ts +43 -0
- package/dist/run/segments.js +157 -0
- package/dist/run/segments.js.map +7 -0
- package/dist/run/stop.d.ts +3 -0
- package/dist/run/stop.js +7 -0
- package/dist/run/stop.js.map +7 -0
- package/dist/run/storage.d.ts +16 -0
- package/dist/run/storage.js +15 -0
- package/dist/run/storage.js.map +7 -0
- package/dist/run/tail.d.ts +5 -0
- package/dist/run/tail.js +7 -0
- package/dist/run/tail.js.map +7 -0
- package/dist/runner/client.d.ts +149 -0
- package/dist/runner/client.js +168 -0
- package/dist/runner/client.js.map +7 -0
- package/dist/runner/devBundle.d.ts +14 -0
- package/dist/runner/devBundle.js +78 -0
- package/dist/runner/devBundle.js.map +7 -0
- package/dist/scenario/cli.d.ts +39 -0
- package/dist/scenario/cli.js +125 -0
- package/dist/scenario/cli.js.map +7 -0
- package/dist/scenario/runner.d.ts +50 -0
- package/dist/scenario/runner.js +185 -0
- package/dist/scenario/runner.js.map +7 -0
- package/dist/sharedRefs.d.ts +22 -0
- package/dist/sharedRefs.js +114 -0
- package/dist/sharedRefs.js.map +7 -0
- package/dist/ssh/exec.d.ts +39 -0
- package/dist/ssh/exec.js +13 -0
- package/dist/ssh/exec.js.map +7 -0
- package/dist/ssh/remote.d.ts +40 -0
- package/dist/ssh/remote.js +25 -0
- package/dist/ssh/remote.js.map +7 -0
- package/dist/ssh/sshRun.d.ts +1 -0
- package/dist/ssh/sshRun.js +24 -0
- package/dist/ssh/sshRun.js.map +7 -0
- package/dist/ssh/sync.d.ts +22 -0
- package/dist/ssh/sync.js +12 -0
- package/dist/ssh/sync.js.map +7 -0
- package/dist/timeouts.d.ts +38 -0
- package/dist/timeouts.js +7 -0
- package/dist/timeouts.js.map +7 -0
- package/package.json +35 -4
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** The settings keys that choose the environment (package.json's restrictedConfigurations). */
|
|
2
|
+
export declare const ENVIRONMENT_KEYS: readonly ['afd.environment.kind', 'afd.environment.image', 'afd.environment.containerEngine', 'afd.environment.containerOptions', 'afd.environment.python', 'afd.environment.wslDistro', 'afd.environment.sshHost', 'afd.environment.sshDir', 'afd.environment.sshImage', 'ansible.executionEnvironment.enabled', 'ansible.executionEnvironment.image', 'ansible.executionEnvironment.containerEngine', 'ansible.executionEnvironment.containerOptions', 'ansible.executionEnvironment.volumeMounts', 'ansible.python.interpreterPath'];
|
|
3
|
+
export interface SettingsHost {
|
|
4
|
+
platform: NodeJS.Platform;
|
|
5
|
+
env: NodeJS.ProcessEnv;
|
|
6
|
+
home: string;
|
|
7
|
+
}
|
|
8
|
+
/** User settings.json files to try; AFD_USER_SETTINGS names one explicitly. */
|
|
9
|
+
export declare function userSettingsCandidates(host: SettingsHost): string[];
|
|
10
|
+
export declare const workspaceEnvironmentAllowed: (env: NodeJS.ProcessEnv) => boolean;
|
|
11
|
+
export interface EnvironmentSettings {
|
|
12
|
+
/** The environment keys, the workspace's over the user's when the workspace is allowed. */
|
|
13
|
+
json: Record<string, unknown>;
|
|
14
|
+
/** The settings files the keys came from. */
|
|
15
|
+
sources: string[];
|
|
16
|
+
/** The workspace sets environment keys that were not used (no opt-in). */
|
|
17
|
+
ignoredWorkspace: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** The environment settings for a root: the first user settings.json found, then the workspace's. */
|
|
20
|
+
export declare function environmentSettings(root: string, host?: SettingsHost): Promise<EnvironmentSettings>;
|
|
21
|
+
/**
|
|
22
|
+
* `afd.canvas.direction` for a root (AFD-192), so the MCP tools lay out unplaced nodes as the
|
|
23
|
+
* canvas does: the workspace's .vscode/settings.json, else the first user settings.json found
|
|
24
|
+
* (VS Code's order). Not a restricted key: it only places boxes. TB when neither sets it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function canvasDirection(root: string, host?: SettingsHost): Promise<'TB' | 'LR'>;
|
|
27
|
+
/**
|
|
28
|
+
* `afd.run.inventory` for a root (AFD-261), so the agents' inventory tool works on the inventory
|
|
29
|
+
* the runs use: the workspace's .vscode/settings.json, else the first user settings.json found.
|
|
30
|
+
* Not a restricted key: it names a file of the repository, which the tool reads inside the root
|
|
31
|
+
* only. Empty when neither sets it (the default of ansible.cfg).
|
|
32
|
+
*/
|
|
33
|
+
export declare function runInventory(root: string, host?: SettingsHost): Promise<string>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// src/node/env/userSettings.ts
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { getNodeValue, parseTree } from "jsonc-parser";
|
|
6
|
+
var PRODUCTS = ["Code", "Code - Insiders", "VSCodium", "Cursor", "Windsurf"];
|
|
7
|
+
var ENVIRONMENT_KEYS = [
|
|
8
|
+
"afd.environment.kind",
|
|
9
|
+
"afd.environment.image",
|
|
10
|
+
"afd.environment.containerEngine",
|
|
11
|
+
"afd.environment.containerOptions",
|
|
12
|
+
"afd.environment.python",
|
|
13
|
+
"afd.environment.wslDistro",
|
|
14
|
+
// AFD-252: without these an SSH environment had a kind and no host.
|
|
15
|
+
"afd.environment.sshHost",
|
|
16
|
+
"afd.environment.sshDir",
|
|
17
|
+
"afd.environment.sshImage",
|
|
18
|
+
"ansible.executionEnvironment.enabled",
|
|
19
|
+
"ansible.executionEnvironment.image",
|
|
20
|
+
"ansible.executionEnvironment.containerEngine",
|
|
21
|
+
"ansible.executionEnvironment.containerOptions",
|
|
22
|
+
"ansible.executionEnvironment.volumeMounts",
|
|
23
|
+
"ansible.python.interpreterPath"
|
|
24
|
+
];
|
|
25
|
+
var thisHost = () => ({
|
|
26
|
+
platform: process.platform,
|
|
27
|
+
env: process.env,
|
|
28
|
+
home: homedir()
|
|
29
|
+
});
|
|
30
|
+
function userSettingsCandidates(host) {
|
|
31
|
+
if (host.env.AFD_USER_SETTINGS) return [host.env.AFD_USER_SETTINGS];
|
|
32
|
+
const base = host.platform === "win32" ? host.env.APPDATA ?? join(host.home, "AppData", "Roaming") : host.platform === "darwin" ? join(host.home, "Library", "Application Support") : host.env.XDG_CONFIG_HOME ?? join(host.home, ".config");
|
|
33
|
+
return PRODUCTS.map((p) => join(base, p, "User", "settings.json"));
|
|
34
|
+
}
|
|
35
|
+
var workspaceEnvironmentAllowed = (env) => /^(1|true|yes)$/i.test(env.AFD_MCP_WORKSPACE_ENVIRONMENT ?? "");
|
|
36
|
+
async function readSettings(path) {
|
|
37
|
+
try {
|
|
38
|
+
const tree = parseTree(await fs.readFile(path, "utf8"));
|
|
39
|
+
const value = tree ? getNodeValue(tree) : void 0;
|
|
40
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
41
|
+
} catch {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
var environmentOnly = (json) => Object.fromEntries(ENVIRONMENT_KEYS.filter((k) => k in json).map((k) => [k, json[k]]));
|
|
46
|
+
async function environmentSettings(root, host = thisHost()) {
|
|
47
|
+
const sources = [];
|
|
48
|
+
let json = {};
|
|
49
|
+
for (const path of userSettingsCandidates(host)) {
|
|
50
|
+
const user = await readSettings(path);
|
|
51
|
+
if (!user) continue;
|
|
52
|
+
json = environmentOnly(user);
|
|
53
|
+
sources.push(path);
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
const workspacePath = join(root, ".vscode", "settings.json");
|
|
57
|
+
const workspace = environmentOnly(await readSettings(workspacePath) ?? {});
|
|
58
|
+
const used = Object.keys(workspace).length > 0 && workspaceEnvironmentAllowed(host.env);
|
|
59
|
+
if (used) {
|
|
60
|
+
json = { ...json, ...workspace };
|
|
61
|
+
sources.push(workspacePath);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
json,
|
|
65
|
+
sources,
|
|
66
|
+
ignoredWorkspace: Object.keys(workspace).length > 0 && !used
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
async function canvasDirection(root, host = thisHost()) {
|
|
70
|
+
const workspace = await readSettings(join(root, ".vscode", "settings.json"));
|
|
71
|
+
let value = workspace?.["afd.canvas.direction"];
|
|
72
|
+
if (value === void 0)
|
|
73
|
+
for (const path of userSettingsCandidates(host)) {
|
|
74
|
+
const user = await readSettings(path);
|
|
75
|
+
if (!user) continue;
|
|
76
|
+
value = user["afd.canvas.direction"];
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
return value === "LR" ? "LR" : "TB";
|
|
80
|
+
}
|
|
81
|
+
async function runInventory(root, host = thisHost()) {
|
|
82
|
+
const workspace = await readSettings(join(root, ".vscode", "settings.json"));
|
|
83
|
+
let value = workspace?.["afd.run.inventory"];
|
|
84
|
+
if (value === void 0)
|
|
85
|
+
for (const path of userSettingsCandidates(host)) {
|
|
86
|
+
const user = await readSettings(path);
|
|
87
|
+
if (!user) continue;
|
|
88
|
+
value = user["afd.run.inventory"];
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
return typeof value === "string" ? value.trim() : "";
|
|
92
|
+
}
|
|
93
|
+
export {
|
|
94
|
+
ENVIRONMENT_KEYS,
|
|
95
|
+
canvasDirection,
|
|
96
|
+
environmentSettings,
|
|
97
|
+
runInventory,
|
|
98
|
+
userSettingsCandidates,
|
|
99
|
+
workspaceEnvironmentAllowed
|
|
100
|
+
};
|
|
101
|
+
//# sourceMappingURL=userSettings.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/node/env/userSettings.ts"],
|
|
4
|
+
"sourcesContent": ["// Where the MCP server takes its Ansible environment from (AFD-126). The image, container options,\n// mounts and interpreter decide what runs on this machine, so they come from the user's own VS Code\n// settings. A workspace's .vscode/settings.json is repository content: VS Code ignores these keys\n// there until the workspace is trusted, and the server has no trust prompt, so it reads them only\n// when the user starts it with AFD_MCP_WORKSPACE_ENVIRONMENT=1.\n\nimport { promises as fs } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { getNodeValue, parseTree } from 'jsonc-parser';\n\n/** VS Code and the forks the setup registers the server for, in the order they are tried. */\nconst PRODUCTS = ['Code', 'Code - Insiders', 'VSCodium', 'Cursor', 'Windsurf'];\n\n/** The settings keys that choose the environment (package.json's restrictedConfigurations). */\nexport const ENVIRONMENT_KEYS = [\n 'afd.environment.kind',\n 'afd.environment.image',\n 'afd.environment.containerEngine',\n 'afd.environment.containerOptions',\n 'afd.environment.python',\n 'afd.environment.wslDistro',\n // AFD-252: without these an SSH environment had a kind and no host.\n 'afd.environment.sshHost',\n 'afd.environment.sshDir',\n 'afd.environment.sshImage',\n 'ansible.executionEnvironment.enabled',\n 'ansible.executionEnvironment.image',\n 'ansible.executionEnvironment.containerEngine',\n 'ansible.executionEnvironment.containerOptions',\n 'ansible.executionEnvironment.volumeMounts',\n 'ansible.python.interpreterPath',\n] as const;\n\nexport interface SettingsHost {\n platform: NodeJS.Platform;\n env: NodeJS.ProcessEnv;\n home: string;\n}\n\nconst thisHost = (): SettingsHost => ({\n platform: process.platform,\n env: process.env,\n home: homedir(),\n});\n\n/** User settings.json files to try; AFD_USER_SETTINGS names one explicitly. */\nexport function userSettingsCandidates(host: SettingsHost): string[] {\n if (host.env.AFD_USER_SETTINGS) return [host.env.AFD_USER_SETTINGS];\n const base =\n host.platform === 'win32'\n ? (host.env.APPDATA ?? join(host.home, 'AppData', 'Roaming'))\n : host.platform === 'darwin'\n ? join(host.home, 'Library', 'Application Support')\n : (host.env.XDG_CONFIG_HOME ?? join(host.home, '.config'));\n return PRODUCTS.map((p) => join(base, p, 'User', 'settings.json'));\n}\n\nexport const workspaceEnvironmentAllowed = (env: NodeJS.ProcessEnv): boolean =>\n /^(1|true|yes)$/i.test(env.AFD_MCP_WORKSPACE_ENVIRONMENT ?? '');\n\nasync function readSettings(path: string): Promise<Record<string, unknown> | undefined> {\n try {\n const tree = parseTree(await fs.readFile(path, 'utf8'));\n const value = tree ? getNodeValue(tree) : undefined;\n return value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined;\n } catch {\n return undefined;\n }\n}\n\nconst environmentOnly = (json: Record<string, unknown>) =>\n Object.fromEntries(ENVIRONMENT_KEYS.filter((k) => k in json).map((k) => [k, json[k]]));\n\nexport interface EnvironmentSettings {\n /** The environment keys, the workspace's over the user's when the workspace is allowed. */\n json: Record<string, unknown>;\n /** The settings files the keys came from. */\n sources: string[];\n /** The workspace sets environment keys that were not used (no opt-in). */\n ignoredWorkspace: boolean;\n}\n\n/** The environment settings for a root: the first user settings.json found, then the workspace's. */\nexport async function environmentSettings(\n root: string,\n host: SettingsHost = thisHost(),\n): Promise<EnvironmentSettings> {\n const sources: string[] = [];\n let json: Record<string, unknown> = {};\n for (const path of userSettingsCandidates(host)) {\n const user = await readSettings(path);\n if (!user) continue;\n json = environmentOnly(user);\n sources.push(path);\n break;\n }\n const workspacePath = join(root, '.vscode', 'settings.json');\n const workspace = environmentOnly((await readSettings(workspacePath)) ?? {});\n const used = Object.keys(workspace).length > 0 && workspaceEnvironmentAllowed(host.env);\n if (used) {\n json = { ...json, ...workspace };\n sources.push(workspacePath);\n }\n return {\n json,\n sources,\n ignoredWorkspace: Object.keys(workspace).length > 0 && !used,\n };\n}\n\n/**\n * `afd.canvas.direction` for a root (AFD-192), so the MCP tools lay out unplaced nodes as the\n * canvas does: the workspace's .vscode/settings.json, else the first user settings.json found\n * (VS Code's order). Not a restricted key: it only places boxes. TB when neither sets it.\n */\nexport async function canvasDirection(\n root: string,\n host: SettingsHost = thisHost(),\n): Promise<'TB' | 'LR'> {\n const workspace = await readSettings(join(root, '.vscode', 'settings.json'));\n let value = workspace?.['afd.canvas.direction'];\n if (value === undefined)\n for (const path of userSettingsCandidates(host)) {\n const user = await readSettings(path);\n if (!user) continue;\n value = user['afd.canvas.direction'];\n break;\n }\n return value === 'LR' ? 'LR' : 'TB';\n}\n\n/**\n * `afd.run.inventory` for a root (AFD-261), so the agents' inventory tool works on the inventory\n * the runs use: the workspace's .vscode/settings.json, else the first user settings.json found.\n * Not a restricted key: it names a file of the repository, which the tool reads inside the root\n * only. Empty when neither sets it (the default of ansible.cfg).\n */\nexport async function runInventory(root: string, host: SettingsHost = thisHost()): Promise<string> {\n const workspace = await readSettings(join(root, '.vscode', 'settings.json'));\n let value = workspace?.['afd.run.inventory'];\n if (value === undefined)\n for (const path of userSettingsCandidates(host)) {\n const user = await readSettings(path);\n if (!user) continue;\n value = user['afd.run.inventory'];\n break;\n }\n return typeof value === 'string' ? value.trim() : '';\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type FlowMerge, type MergeSide } from '@ansible-flow/core/flow/merge';
|
|
2
|
+
/** A merge waiting for choices: the three versions, the file, and what was chosen so far. */
|
|
3
|
+
export interface MergeState {
|
|
4
|
+
/** The file's path in the repository, as git names it. */
|
|
5
|
+
file: string;
|
|
6
|
+
base: string;
|
|
7
|
+
ours: string;
|
|
8
|
+
theirs: string;
|
|
9
|
+
choices: Record<string, MergeSide>;
|
|
10
|
+
at: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const MERGE_FOLDER = "afd-merge";
|
|
13
|
+
/** Where the merge of `file` waits, in the repository's git folder. */
|
|
14
|
+
export declare function mergeStatePath(gitDir: string, file: string): string;
|
|
15
|
+
export declare function readMergeState(gitDir: string, file: string): MergeState | undefined;
|
|
16
|
+
export declare function writeMergeState(gitDir: string, state: MergeState): void;
|
|
17
|
+
export declare function removeMergeState(gitDir: string, file: string): void;
|
|
18
|
+
export type Merged =
|
|
19
|
+
/** Not three flows this can read: the caller merges the text instead. */
|
|
20
|
+
{
|
|
21
|
+
kind: 'not-flows';
|
|
22
|
+
why: string;
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'merged';
|
|
25
|
+
text: string;
|
|
26
|
+
merge: FlowMerge;
|
|
27
|
+
/** Errors in the merged flow that neither side had: a person looks before it is taken. */
|
|
28
|
+
broken: string[];
|
|
29
|
+
};
|
|
30
|
+
/** Merges three versions of a flow file's text, with `choices` made so far. */
|
|
31
|
+
export declare function mergeFlowTexts(base: string, ours: string, theirs: string, choices?: Record<string, MergeSide>): Merged;
|
|
32
|
+
export declare const MERGE_ATTRIBUTES: string[];
|
|
33
|
+
export declare const MERGE_DRIVER = "node .afd/afd-merge.js %O %A %B %P";
|
|
34
|
+
/** `.gitattributes` with the flow lines it lacks, or undefined when it has them all. */
|
|
35
|
+
export declare function withMergeAttributes(text: string): string | undefined;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/node/flowMerge.ts
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { parseFlow } from "@ansible-flow/core/flow/document";
|
|
5
|
+
import { mergeFlows } from "@ansible-flow/core/flow/merge";
|
|
6
|
+
import { writeMergedFlow } from "@ansible-flow/core/flow/mergeText";
|
|
7
|
+
import { validateFlow } from "@ansible-flow/core/flow/validate";
|
|
8
|
+
var MERGE_FOLDER = "afd-merge";
|
|
9
|
+
function mergeStatePath(gitDir, file) {
|
|
10
|
+
return join(gitDir, MERGE_FOLDER, `${encodeURIComponent(file.replaceAll("\\", "/"))}.json`);
|
|
11
|
+
}
|
|
12
|
+
function readMergeState(gitDir, file) {
|
|
13
|
+
const path = mergeStatePath(gitDir, file);
|
|
14
|
+
if (!existsSync(path)) return void 0;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
17
|
+
} catch {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function writeMergeState(gitDir, state) {
|
|
22
|
+
const path = mergeStatePath(gitDir, state.file);
|
|
23
|
+
mkdirSync(join(gitDir, MERGE_FOLDER), { recursive: true });
|
|
24
|
+
writeFileSync(path, `${JSON.stringify(state, null, 1)}
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
function removeMergeState(gitDir, file) {
|
|
28
|
+
rmSync(mergeStatePath(gitDir, file), { force: true });
|
|
29
|
+
}
|
|
30
|
+
var errorsOf = (doc) => new Set(
|
|
31
|
+
validateFlow(doc).findings.filter((f) => f.severity === "error").map((f) => f.message)
|
|
32
|
+
);
|
|
33
|
+
function mergeFlowTexts(base, ours, theirs, choices = {}) {
|
|
34
|
+
const docs = [base, ours, theirs].map((t2) => parseFlow(t2));
|
|
35
|
+
const names = ["the common version", "ours", "theirs"];
|
|
36
|
+
for (const [i, d] of docs.entries())
|
|
37
|
+
if (!d.doc) return { kind: "not-flows", why: `${names[i]} is not a flow this can read` };
|
|
38
|
+
const [b, o, t] = docs.map((d) => d.doc);
|
|
39
|
+
const merge = mergeFlows(b, o, t, choices);
|
|
40
|
+
let text;
|
|
41
|
+
try {
|
|
42
|
+
text = writeMergedFlow(ours, merge.doc);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return {
|
|
45
|
+
kind: "not-flows",
|
|
46
|
+
why: `the merged flow could not be written: ${e.message}`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const had = /* @__PURE__ */ new Set([...errorsOf(o), ...errorsOf(t)]);
|
|
50
|
+
const broken = [...errorsOf(merge.doc)].filter((m) => !had.has(m));
|
|
51
|
+
return { kind: "merged", text, merge, broken };
|
|
52
|
+
}
|
|
53
|
+
var MERGE_ATTRIBUTES = ["*.flow.yaml merge=ansible-flow", "*.flow.yml merge=ansible-flow"];
|
|
54
|
+
var MERGE_DRIVER = "node .afd/afd-merge.js %O %A %B %P";
|
|
55
|
+
function withMergeAttributes(text) {
|
|
56
|
+
const have = new Set(text.split(/\r?\n/).map((l) => l.trim().replace(/\s+/g, " ")));
|
|
57
|
+
const missing = MERGE_ATTRIBUTES.filter((l) => !have.has(l));
|
|
58
|
+
if (!missing.length) return void 0;
|
|
59
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
60
|
+
const head = text === "" || text.endsWith("\n") ? text : `${text}${eol}`;
|
|
61
|
+
return `${head}# Flows merge by their steps, not their lines (Ansible Flow Designer)${eol}${missing.join(eol)}${eol}`;
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
MERGE_ATTRIBUTES,
|
|
65
|
+
MERGE_DRIVER,
|
|
66
|
+
MERGE_FOLDER,
|
|
67
|
+
mergeFlowTexts,
|
|
68
|
+
mergeStatePath,
|
|
69
|
+
readMergeState,
|
|
70
|
+
removeMergeState,
|
|
71
|
+
withMergeAttributes,
|
|
72
|
+
writeMergeState
|
|
73
|
+
};
|
|
74
|
+
//# sourceMappingURL=flowMerge.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/node/flowMerge.ts"],
|
|
4
|
+
"sourcesContent": ["// AFD-283: merging a flow file the way git asks a merge driver to, and the merge left for a person\n// to finish. The driver writes the merged flow over \"ours\"; when the two sides changed the same\n// place, it writes the flow with \"ours\" for each such place, keeps the three versions beside the\n// repository's git folder (never in the work tree), and tells git the file is not merged. The\n// designer reads that state, lets the person choose, and writes the file again.\nimport { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { parseFlow } from '../core/flow/document';\nimport { type FlowMerge, type MergeSide, mergeFlows } from '../core/flow/merge';\nimport { writeMergedFlow } from '../core/flow/mergeText';\nimport type { FlowDocument } from '../core/flow/types';\nimport { validateFlow } from '../core/flow/validate';\n\n/** A merge waiting for choices: the three versions, the file, and what was chosen so far. */\nexport interface MergeState {\n /** The file's path in the repository, as git names it. */\n file: string;\n base: string;\n ours: string;\n theirs: string;\n choices: Record<string, MergeSide>;\n at: string;\n}\n\nexport const MERGE_FOLDER = 'afd-merge';\n\n/** Where the merge of `file` waits, in the repository's git folder. */\nexport function mergeStatePath(gitDir: string, file: string): string {\n return join(gitDir, MERGE_FOLDER, `${encodeURIComponent(file.replaceAll('\\\\', '/'))}.json`);\n}\n\nexport function readMergeState(gitDir: string, file: string): MergeState | undefined {\n const path = mergeStatePath(gitDir, file);\n if (!existsSync(path)) return undefined;\n try {\n return JSON.parse(readFileSync(path, 'utf8')) as MergeState;\n } catch {\n return undefined;\n }\n}\n\nexport function writeMergeState(gitDir: string, state: MergeState): void {\n const path = mergeStatePath(gitDir, state.file);\n mkdirSync(join(gitDir, MERGE_FOLDER), { recursive: true });\n writeFileSync(path, `${JSON.stringify(state, null, 1)}\\n`);\n}\n\nexport function removeMergeState(gitDir: string, file: string): void {\n rmSync(mergeStatePath(gitDir, file), { force: true });\n}\n\nexport type Merged =\n /** Not three flows this can read: the caller merges the text instead. */\n | { kind: 'not-flows'; why: string }\n | {\n kind: 'merged';\n text: string;\n merge: FlowMerge;\n /** Errors in the merged flow that neither side had: a person looks before it is taken. */\n broken: string[];\n };\n\nconst errorsOf = (doc: FlowDocument) =>\n new Set(\n validateFlow(doc)\n .findings.filter((f) => f.severity === 'error')\n .map((f) => f.message),\n );\n\n/** Merges three versions of a flow file's text, with `choices` made so far. */\nexport function mergeFlowTexts(\n base: string,\n ours: string,\n theirs: string,\n choices: Record<string, MergeSide> = {},\n): Merged {\n const docs = [base, ours, theirs].map((t) => parseFlow(t));\n const names = ['the common version', 'ours', 'theirs'];\n for (const [i, d] of docs.entries())\n if (!d.doc) return { kind: 'not-flows', why: `${names[i]} is not a flow this can read` };\n const [b, o, t] = docs.map((d) => d.doc as FlowDocument) as [\n FlowDocument,\n FlowDocument,\n FlowDocument,\n ];\n const merge = mergeFlows(b, o, t, choices);\n let text: string;\n try {\n text = writeMergedFlow(ours, merge.doc);\n } catch (e) {\n return {\n kind: 'not-flows',\n why: `the merged flow could not be written: ${(e as Error).message}`,\n };\n }\n const had = new Set([...errorsOf(o), ...errorsOf(t)]);\n const broken = [...errorsOf(merge.doc)].filter((m) => !had.has(m));\n return { kind: 'merged', text, merge, broken };\n}\n\n// --- setting a repository up (Set Up Flow Merging in Repository)\n\nexport const MERGE_ATTRIBUTES = ['*.flow.yaml merge=ansible-flow', '*.flow.yml merge=ansible-flow'];\nexport const MERGE_DRIVER = 'node .afd/afd-merge.js %O %A %B %P';\n\n/** `.gitattributes` with the flow lines it lacks, or undefined when it has them all. */\nexport function withMergeAttributes(text: string): string | undefined {\n const have = new Set(text.split(/\\r?\\n/).map((l) => l.trim().replace(/\\s+/g, ' ')));\n const missing = MERGE_ATTRIBUTES.filter((l) => !have.has(l));\n if (!missing.length) return undefined;\n const eol = text.includes('\\r\\n') ? '\\r\\n' : '\\n';\n const head = text === '' || text.endsWith('\\n') ? text : `${text}${eol}`;\n return `${head}# Flows merge by their steps, not their lines (Ansible Flow Designer)${eol}${missing.join(eol)}${eol}`;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["t"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ProcessSpec } from '@ansible-flow/core/run/command';
|
|
2
|
+
/** A process that was started: how it ended, and how to end it. */
|
|
3
|
+
export interface Execution {
|
|
4
|
+
/** The exit code; undefined when the process could not start or ended without one. */
|
|
5
|
+
exit: Promise<number | undefined>;
|
|
6
|
+
/** Ends the process. What it started beyond itself (a container) is ended with `stopStarted`. */
|
|
7
|
+
terminate(): void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Runs `spec` with `env` on top of this process's environment, feeding its output to `log` line
|
|
11
|
+
* by line. AFD-247: a spec with `input` names the variables whose values go to the process's
|
|
12
|
+
* input, one base64 line each, instead of its arguments (what afd-ssh-run does for a terminal).
|
|
13
|
+
*/
|
|
14
|
+
export declare function startProcess(spec: ProcessSpec, env: Record<string, string>, log: (line: string) => void): Execution;
|
package/dist/run/exec.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { FlowDocument } from '@ansible-flow/core/flow/types';
|
|
2
|
+
import { type LaunchFacts } from '@ansible-flow/core/run/launch';
|
|
3
|
+
/** The probe of the Ansible environment a run uses, and the name people know it by. */
|
|
4
|
+
export interface RunProbe {
|
|
5
|
+
client: {
|
|
6
|
+
call<T>(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
|
|
7
|
+
};
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* What a run with these inputs would touch: the hosts the targets match in the inventory, the
|
|
12
|
+
* inventory_targets of the flow's steps, what looks like production.
|
|
13
|
+
*/
|
|
14
|
+
export declare function launchFacts(doc: FlowDocument, inputs: Record<string, unknown>, probe: RunProbe, inventory: string | undefined): Promise<{
|
|
15
|
+
ok: true;
|
|
16
|
+
facts: LaunchFacts;
|
|
17
|
+
} | {
|
|
18
|
+
ok: false;
|
|
19
|
+
error: string;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* The inventory's groups, for the dialog's host list suggestions (AFD-82), with each group's hosts
|
|
23
|
+
* (AFD-95, from the probe's inventory_tree, one inventory parse as before); none when it fails.
|
|
24
|
+
*/
|
|
25
|
+
export declare function inventoryGroups(probe: RunProbe, inventory: string | undefined): Promise<{
|
|
26
|
+
groups: string[];
|
|
27
|
+
hosts: Record<string, {
|
|
28
|
+
count: number;
|
|
29
|
+
hosts: string[];
|
|
30
|
+
}>;
|
|
31
|
+
}>;
|
|
32
|
+
/**
|
|
33
|
+
* AFD-141: the callbacks the repository's ansible.cfg enables, so they keep running next to the
|
|
34
|
+
* run's own; undefined when the environment does not answer (the run's callback alone, then).
|
|
35
|
+
*/
|
|
36
|
+
export declare function repositoryCallbacks(probe: RunProbe): Promise<{
|
|
37
|
+
enabled: string[];
|
|
38
|
+
paths: string[];
|
|
39
|
+
} | undefined>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TIMEOUTS
|
|
3
|
+
} from "../chunks/chunk-IIFP6UQR.js";
|
|
4
|
+
|
|
5
|
+
// src/node/run/facts.ts
|
|
6
|
+
import { resolvePattern, targetUses } from "@ansible-flow/core/engine/targets";
|
|
7
|
+
import { buildModel } from "@ansible-flow/core/flow/model";
|
|
8
|
+
import { targetsOf } from "@ansible-flow/core/run/launch";
|
|
9
|
+
import { groupHosts } from "@ansible-flow/core/view/inventoryTree";
|
|
10
|
+
async function targetFacts(doc, inputs, count) {
|
|
11
|
+
return Promise.all(
|
|
12
|
+
targetUses(buildModel(doc)).map(async (u) => {
|
|
13
|
+
const pattern = resolvePattern(u.target, inputs);
|
|
14
|
+
const use = { pattern, declared: u.target, nodes: u.nodes, prod: [] };
|
|
15
|
+
if (!pattern) return use;
|
|
16
|
+
try {
|
|
17
|
+
const f = await count(pattern);
|
|
18
|
+
use.count = f.count;
|
|
19
|
+
use.prod = [...f.prod_groups, ...f.prod_hosts];
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
return use;
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
async function launchFacts(doc, inputs, probe, inventory) {
|
|
27
|
+
const targets = targetsOf(doc, inputs);
|
|
28
|
+
if (targets.length === 0)
|
|
29
|
+
return { ok: false, error: "No target hosts: the targets input is empty" };
|
|
30
|
+
let found;
|
|
31
|
+
try {
|
|
32
|
+
found = await probe.client.call(
|
|
33
|
+
"inventory",
|
|
34
|
+
{ inventory, targets },
|
|
35
|
+
TIMEOUTS.probeInventory
|
|
36
|
+
);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
error: `The Ansible environment (${probe.label}) did not answer: ${e.message}`
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (found.count === 0)
|
|
44
|
+
return { ok: false, error: `None of the targets (${targets.join(", ")}) is in the inventory` };
|
|
45
|
+
const uses = await targetFacts(
|
|
46
|
+
doc,
|
|
47
|
+
inputs,
|
|
48
|
+
(pattern) => probe.client.call(
|
|
49
|
+
"inventory",
|
|
50
|
+
{ inventory, targets: [pattern] },
|
|
51
|
+
TIMEOUTS.probeInventory
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
const prod = [
|
|
55
|
+
.../* @__PURE__ */ new Set([...found.prod_groups, ...found.prod_hosts, ...uses.flatMap((u) => u.prod)])
|
|
56
|
+
];
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
facts: {
|
|
60
|
+
sources: found.sources,
|
|
61
|
+
count: found.count,
|
|
62
|
+
hosts: found.hosts,
|
|
63
|
+
unknown: found.unknown,
|
|
64
|
+
uses,
|
|
65
|
+
prod,
|
|
66
|
+
vault: found.vault,
|
|
67
|
+
environment: probe.label
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async function inventoryGroups(probe, inventory) {
|
|
72
|
+
try {
|
|
73
|
+
const tree = await probe.client.call(
|
|
74
|
+
"inventory_tree",
|
|
75
|
+
{ inventory },
|
|
76
|
+
TIMEOUTS.probeInventory
|
|
77
|
+
);
|
|
78
|
+
const hosts = groupHosts(tree);
|
|
79
|
+
return { groups: Object.keys(hosts).sort((a, b) => a.localeCompare(b)), hosts };
|
|
80
|
+
} catch {
|
|
81
|
+
return { groups: [], hosts: {} };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
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);
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
inventoryGroups,
|
|
93
|
+
launchFacts,
|
|
94
|
+
repositoryCallbacks
|
|
95
|
+
};
|
|
96
|
+
//# sourceMappingURL=facts.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 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;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/run/ids.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/node/run/ids.ts"],
|
|
4
|
+
"sourcesContent": ["/** A run id: the start time, sortable (run folders are pruned by it, AFD-29). */\nexport function stamp(d = new Date()): string {\n return d\n .toISOString()\n .replaceAll(/[-:]/g, '')\n .replace(/\\.\\d+Z$/, 'Z');\n}\n"],
|
|
5
|
+
"mappings": ";AACO,SAAS,MAAM,IAAI,oBAAI,KAAK,GAAW;AAC5C,SAAO,EACJ,YAAY,EACZ,WAAW,SAAS,EAAE,EACtB,QAAQ,WAAW,GAAG;AAC3B;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RunView } from '@ansible-flow/core/protocol';
|
|
2
|
+
import { type PastRun } from '@ansible-flow/core/run/pastRuns';
|
|
3
|
+
/** Where the runs of a flow are kept. */
|
|
4
|
+
export declare function runsDir(storage: string, flowId: string): string;
|
|
5
|
+
/** Keeps a finished run with its events. The view holds host names and results, no input values. */
|
|
6
|
+
export declare function keepRun(runDir: string, run: RunView): Promise<void>;
|
|
7
|
+
/** The kept runs of a flow, the newest first. */
|
|
8
|
+
export declare function listRuns(storage: string, flowId: string): Promise<PastRun[]>;
|
|
9
|
+
export declare function loadRun(storage: string, flowId: string, id: string): Promise<RunView | undefined>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { GenerateResult } from '@ansible-flow/core/engine/types';
|
|
2
|
+
import type { FlowDocument } from '@ansible-flow/core/flow/types';
|
|
3
|
+
import type { RunView } from '@ansible-flow/core/protocol';
|
|
4
|
+
import { type ProcessSpec } from '@ansible-flow/core/run/command';
|
|
5
|
+
import type { Environment } from '@ansible-flow/core/run/environment';
|
|
6
|
+
import { type RunSummary } from '@ansible-flow/core/run/events';
|
|
7
|
+
import { type Execution } from './exec';
|
|
8
|
+
/** How often a live run updates its view, at most (AFD-81). */
|
|
9
|
+
export declare const LIVE_MS = 500;
|
|
10
|
+
/** No new events for this long: the run may wait for input. */
|
|
11
|
+
export declare const STALL_MS = 20000;
|
|
12
|
+
export interface RunFlowRequest {
|
|
13
|
+
/** The run's id (`stamp()`), which names its folder. */
|
|
14
|
+
id: string;
|
|
15
|
+
startedAt: string;
|
|
16
|
+
mode: 'check' | 'real';
|
|
17
|
+
doc: FlowDocument;
|
|
18
|
+
/** The code that runs: its routes map the events back to nodes. */
|
|
19
|
+
code: GenerateResult;
|
|
20
|
+
env: Environment;
|
|
21
|
+
/** The environment's name for people ("afd-ee:2.19 (docker)"). */
|
|
22
|
+
environment: string;
|
|
23
|
+
/** Repository root on this machine. */
|
|
24
|
+
root: string;
|
|
25
|
+
/** Folder with afd_probe/ and callback_plugins/afd_events.py. */
|
|
26
|
+
probeDir: string;
|
|
27
|
+
/** The playbook to run, relative to `root`, with forward slashes. */
|
|
28
|
+
playbook: string;
|
|
29
|
+
inventory?: string;
|
|
30
|
+
/** Where runs are kept: `<storage>/runs/<flow id>/<run id>/`, and the audit log. */
|
|
31
|
+
storage: string;
|
|
32
|
+
/** How many runs of the flow to keep (default `KEEP_RUNS`). */
|
|
33
|
+
keepRuns?: number;
|
|
34
|
+
/** Flow input values. Secret ones never reach a file. */
|
|
35
|
+
inputs: Record<string, unknown>;
|
|
36
|
+
vaultPassword?: string;
|
|
37
|
+
/** The repository's own callbacks, kept running beside the run's (`repositoryCallbacks`). */
|
|
38
|
+
callbacks?: {
|
|
39
|
+
enabled: string[];
|
|
40
|
+
paths: string[];
|
|
41
|
+
};
|
|
42
|
+
/** AFD-244: the run is of a part of the flow; its code is in a folder of its own. */
|
|
43
|
+
partial?: RunView['partial'];
|
|
44
|
+
/** AFD-247: paths the SSH copy needs that Git does not list (a partial run's folder). */
|
|
45
|
+
syncAlso?: string[];
|
|
46
|
+
/** What the audit log says about the launch, besides the run's id. Never input values. */
|
|
47
|
+
audit?: Record<string, unknown>;
|
|
48
|
+
/** Stops the run when it aborts. */
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
}
|
|
51
|
+
export interface RunFlowHooks {
|
|
52
|
+
/** The run's view: when it starts, as it goes (at most every `LIVE_MS`), and when it ends. */
|
|
53
|
+
report(run: RunView): void;
|
|
54
|
+
/**
|
|
55
|
+
* Starts the process. Default: a child process whose output goes to `log`. `stopping` says the
|
|
56
|
+
* run is being stopped, for a host whose process may end without an exit code then.
|
|
57
|
+
*/
|
|
58
|
+
execute?(spec: ProcessSpec, env: Record<string, string>, stopping: () => boolean): Execution | Promise<Execution>;
|
|
59
|
+
/** The default process's output, line by line. */
|
|
60
|
+
log?(line: string): void;
|
|
61
|
+
/** The run started; `stop` ends it. Returns what to call when the run has ended. */
|
|
62
|
+
started?(stop: () => Promise<void>): (() => void) | undefined;
|
|
63
|
+
/** Whether to watch for a run gone quiet (no event for `STALL_MS`). */
|
|
64
|
+
watchQuiet?: boolean;
|
|
65
|
+
/** The run went quiet. */
|
|
66
|
+
quiet?(): void;
|
|
67
|
+
/** The process ended; before its events are read. */
|
|
68
|
+
ended?(): Promise<void>;
|
|
69
|
+
/** How the run ended, before it is kept and reported. */
|
|
70
|
+
finished?(end: {
|
|
71
|
+
stopped: boolean;
|
|
72
|
+
rc: number | undefined;
|
|
73
|
+
events: boolean;
|
|
74
|
+
summary: RunSummary;
|
|
75
|
+
}): void;
|
|
76
|
+
}
|
|
77
|
+
/** Runs the flow's code and resolves with the finished run (also reported). Never rejects. */
|
|
78
|
+
export declare function runFlow(request: RunFlowRequest, hooks: RunFlowHooks): Promise<RunView>;
|