@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,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/node/ai/writers.ts"],
|
|
4
|
+
"sourcesContent": ["// What Set Up AI Assistants writes for agents outside Copilot (AFD-19, AFD-20), without VS Code so\n// it can be tested against a temporary home folder. Skill copies carry a manifest so updates, hand\n// edits and removal are told apart; MCP registrations go where each agent reads them: Claude Code\n// through its CLI, Codex as a marked block in ~/.codex/config.toml, the others as one key in their\n// JSON(C) config. Every config file gets a one-time *.afd-backup before its first change.\nimport { execFile } from 'node:child_process';\nimport { createHash } from 'node:crypto';\nimport { promises as fs } from 'node:fs';\nimport { dirname, isAbsolute, join, relative } from 'node:path';\nimport {\n applyEdits,\n type FormattingOptions,\n findNodeAtLocation,\n modify,\n type ParseError,\n parse as parseJsonc,\n parseTree,\n} from 'jsonc-parser';\nimport { TOOL_SPECS } from '../../core/ai/specs';\n\nexport const AGENTS = [\n 'claude',\n 'codex',\n 'gemini',\n 'opencode',\n 'zcode',\n 'copilot',\n 'cursor',\n // AFD-173: the agents built into VS Code forks, by the files they read (AFD-172 probes).\n 'kiro',\n 'antigravity',\n 'devin',\n] as const;\nexport type Agent = (typeof AGENTS)[number];\nexport type Scope = 'repo' | 'user';\n\nexport const SERVER_NAME = 'ansible-flow';\nexport const SKILL_NAME = 'ansible-flow';\nconst MANIFEST = '.afd-skill.json';\nconst BEGIN = `# >>> ${SERVER_NAME} (managed by Ansible Flow Designer; Remove AI Assistant Setup deletes it)`;\nconst END = `# <<< ${SERVER_NAME}`;\n\n/** How agents start the MCP server. */\nexport interface Launch {\n command: string;\n args: string[];\n env?: Record<string, string>;\n}\n\nexport interface Item {\n agent: Agent;\n what: 'skill' | 'mcp';\n /** File or folder, or the command for Claude's registration. */\n target: string;\n action: 'create' | 'update' | 'unchanged' | 'skip' | 'remove';\n /** Why an item is skipped, or what the user must do. */\n note?: string;\n}\n\nexport interface Env {\n home: string;\n /** Where Devin keeps its MCP file depends on it (default: this process's platform). */\n platform?: NodeJS.Platform;\n /** XDG_CONFIG_HOME, which Devin honours outside Windows. */\n xdgConfig?: string;\n /** The repository (workspace folder) for repo-scope skill copies. */\n repo?: string;\n /** Runs the claude CLI; undefined when it is not installed. */\n claude?: (args: string[]) => Promise<{ code: number; out: string }>;\n}\n\nexport interface SkillFiles {\n version: string;\n /** Relative path (posix) -> text. */\n files: Record<string, string>;\n}\n\ninterface Manifest {\n version: string;\n files: Record<string, string>;\n}\n\nconst hash = (text: string) => createHash('sha256').update(text).digest('hex').slice(0, 16);\n\nasync function readText(path: string): Promise<string | undefined> {\n try {\n return await fs.readFile(path, 'utf8');\n } catch {\n return undefined;\n }\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await fs.stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function backup(path: string): Promise<void> {\n const copy = `${path}.afd-backup`;\n if ((await exists(path)) && !(await exists(copy))) await fs.copyFile(path, copy);\n}\n\n// ---- skills ---------------------------------------------------------------------------------\n\n/**\n * The folder (under the repository or home) each agent reads skills from. Agents that also read\n * another agent's folder share its copy: OpenCode and ZCode read .agents/skills, Cursor reads\n * Claude's, Gemini reads .agents/skills in a repository.\n */\nconst SKILL_HOME: Record<Agent, Record<Scope, string>> = {\n claude: { repo: '.claude', user: '.claude' },\n codex: { repo: '.agents', user: '.agents' },\n gemini: { repo: '.agents', user: '.gemini' },\n opencode: { repo: '.agents', user: '.agents' },\n zcode: { repo: '.agents', user: '.agents' },\n copilot: { repo: '.github', user: '.copilot' },\n cursor: { repo: '.claude', user: '.claude' },\n kiro: { repo: '.kiro', user: '.kiro' },\n // Antigravity's customization roots: .agents in a repository, ~/.gemini/config for the user.\n antigravity: { repo: '.agents', user: '.gemini/config' },\n // Devin reads ~/.codeium/windsurf/skills and ~/.agents/skills, not ~/.devin/skills.\n devin: { repo: '.agents', user: '.codeium/windsurf' },\n};\n\n/** Folders an agent reads the skill from, per scope. */\nexport function skillDirs(agent: Agent, scope: Scope, env: Env): string[] {\n const base = scope === 'repo' ? env.repo : env.home;\n if (!base) return [];\n return [join(base, SKILL_HOME[agent][scope], 'skills', SKILL_NAME)];\n}\n\nexport interface SkillState {\n dir: string;\n /** Not ours: the folder exists without our manifest. */\n foreign: boolean;\n version?: string;\n /** Files of ours the user changed since they were written. */\n edited: string[];\n}\n\n/**\n * The manifest of a skill folder we wrote, or undefined when it is not ours. AFD-127: a repository\n * can plant one (repo-scope copies live in the repository), so only file names that stay inside\n * the folder are kept; a manifest that does not read makes the folder foreign, never deleted.\n */\nasync function readManifest(dir: string): Promise<Manifest | undefined> {\n const raw = await readText(join(dir, MANIFEST));\n if (raw === undefined) return undefined;\n let data: unknown;\n try {\n data = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (!data || typeof data !== 'object') return undefined;\n const { version, files } = data as { version?: unknown; files?: unknown };\n if (!files || typeof files !== 'object' || Array.isArray(files)) return undefined;\n const kept: Record<string, string> = {};\n for (const [name, h] of Object.entries(files))\n if (typeof h === 'string' && SKILL_FILE.test(name) && name !== MANIFEST) kept[name] = h;\n return { version: typeof version === 'string' ? version : '', files: kept };\n}\n\n/** A relative posix name of plain segments: no `.`/`..`, no drive, no backslash (AFD-127). */\nconst SKILL_FILE = /^(?!.*(?:^|\\/)\\.\\.?(?:\\/|$))[\\w.-]+(?:\\/[\\w.-]+)*$/;\n\nconst insideDir = (root: string, path: string) => {\n const rel = relative(root, path);\n return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));\n};\n\n/** The real path of `path`, or of its nearest ancestor that exists. */\nasync function nearestReal(path: string): Promise<string> {\n for (let p = path; ; p = dirname(p)) {\n try {\n return await fs.realpath(p);\n } catch {\n if (dirname(p) === p) return p;\n }\n }\n}\n\n/**\n * Where a skill file goes, or undefined when it would not land inside the folder: its name is not\n * a plain relative name, or a link on the way leads out (AFD-127).\n */\nasync function skillPath(dir: string, name: string): Promise<string | undefined> {\n if (!SKILL_FILE.test(name)) return undefined;\n const path = join(dir, ...name.split('/'));\n const [root, parent] = await Promise.all([nearestReal(dir), nearestReal(dirname(path))]);\n return insideDir(root, parent) ? path : undefined;\n}\n\nexport async function skillState(dir: string): Promise<SkillState | undefined> {\n if (!(await exists(dir))) return undefined;\n // A link in place of the folder is not ours: removal would act on what it points to.\n const link = await fs.lstat(dir).then(\n (s) => s.isSymbolicLink(),\n () => false,\n );\n const manifest = link ? undefined : await readManifest(dir);\n if (!manifest) return { dir, foreign: true, edited: [] };\n const edited: string[] = [];\n for (const [name, h] of Object.entries(manifest.files)) {\n const path = await skillPath(dir, name);\n const text = path && (await readText(path));\n if (text !== undefined && hash(text) !== h) edited.push(name);\n }\n return { dir, foreign: false, version: manifest.version, edited };\n}\n\n/** a > b for versions like 0.75.0 (non-numeric parts count as 0). */\nexport function newerVersion(a: string, b: string): boolean {\n const pa = a.split('.').map((n) => Number.parseInt(n, 10) || 0);\n const pb = b.split('.').map((n) => Number.parseInt(n, 10) || 0);\n for (let i = 0; i < Math.max(pa.length, pb.length); i++) {\n const d = (pa[i] ?? 0) - (pb[i] ?? 0);\n if (d !== 0) return d > 0;\n }\n return false;\n}\n\nexport async function planSkill(dir: string, skill: SkillFiles): Promise<Item['action']> {\n const state = await skillState(dir);\n if (!state) return 'create';\n if (state.foreign) return 'skip';\n // AFD-173: an editor with a newer Ansible Flow Designer wrote it (VS Code and a fork on one\n // machine): the older one leaves it, so the two do not overwrite each other at every start.\n if (state.version && newerVersion(state.version, skill.version)) return 'unchanged';\n for (const [name, text] of Object.entries(skill.files)) {\n const path = await skillPath(dir, name);\n if (!path || (await readText(path)) !== text) return 'update';\n }\n return state.version === skill.version ? 'unchanged' : 'update';\n}\n\nexport async function writeSkill(dir: string, skill: SkillFiles): Promise<void> {\n const before = await skillState(dir);\n if (before?.foreign) throw new Error(`${dir} exists and is not ours`);\n // Files of an older version that the new one no longer has.\n const old = before ? await readManifest(dir) : undefined;\n for (const name of Object.keys(old?.files ?? {})) {\n if (name in skill.files) continue;\n const path = await skillPath(dir, name);\n if (path) await fs.rm(path, { force: true });\n }\n for (const [name, text] of Object.entries(skill.files)) {\n const path = await skillPath(dir, name);\n if (!path) throw new Error(`${name} would be written outside ${dir}`);\n await fs.mkdir(dirname(path), { recursive: true });\n await fs.writeFile(path, text, 'utf8');\n }\n const manifest: Manifest = {\n version: skill.version,\n files: Object.fromEntries(Object.entries(skill.files).map(([n, t]) => [n, hash(t)])),\n };\n await fs.writeFile(join(dir, MANIFEST), `${JSON.stringify(manifest, null, 2)}\\n`, 'utf8');\n}\n\n/** Removes our files (and the folder when nothing else is left in it); foreign folders stay. */\nexport async function removeSkill(dir: string): Promise<boolean> {\n const state = await skillState(dir);\n if (!state || state.foreign) return false;\n const manifest = await readManifest(dir);\n const names = Object.keys(manifest?.files ?? {});\n for (const name of names) {\n const path = await skillPath(dir, name);\n if (path) await fs.rm(path, { force: true });\n }\n await fs.rm(join(dir, MANIFEST), { force: true });\n const prune = async (d: string): Promise<void> => {\n if (!insideDir(dir, d)) return;\n try {\n if ((await fs.readdir(d)).length === 0) {\n await fs.rmdir(d);\n if (d !== dir) await prune(dirname(d));\n }\n } catch {}\n };\n for (const name of names) await prune(dirname(join(dir, ...name.split('/'))));\n await prune(dir);\n return true;\n}\n\n// ---- MCP: Codex (~/.codex/config.toml) --------------------------------------------------------\n\nexport const codexConfig = (env: Env) => join(env.home, '.codex', 'config.toml');\n\n/** TOML basic strings take JSON's escapes for the characters paths and names use. */\nconst tomlString = (s: string) => JSON.stringify(s);\n\nexport function codexBlock(launch: Launch): string {\n const lines = [\n BEGIN,\n `[mcp_servers.${SERVER_NAME}]`,\n `command = ${tomlString(launch.command)}`,\n `args = [${launch.args.map(tomlString).join(', ')}]`,\n ];\n if (launch.env && Object.keys(launch.env).length)\n lines.push(\n `env = { ${Object.entries(launch.env)\n .map(([k, v]) => `${k} = ${tomlString(v)}`)\n .join(', ')} }`,\n );\n lines.push(END);\n return lines.join('\\n');\n}\n\nfunction splitBlock(text: string): { before: string; block?: string; after: string } {\n const start = text.indexOf(BEGIN);\n const end = start < 0 ? -1 : text.indexOf(END, start);\n if (start < 0 || end < 0) return { before: text, after: '' };\n return {\n before: text.slice(0, start),\n block: text.slice(start, end + END.length),\n after: text.slice(end + END.length),\n };\n}\n\n/** A table of the same name written by the user (outside our block). */\nconst handWritten = (text: string) =>\n new RegExp(`^\\\\s*\\\\[mcp_servers\\\\.(${SERVER_NAME}|\"${SERVER_NAME}\")\\\\]`, 'm').test(\n splitBlock(text).before + splitBlock(text).after,\n );\n\nexport async function planCodex(env: Env, launch: Launch): Promise<Item> {\n const target = codexConfig(env);\n const text = (await readText(target)) ?? '';\n const item = { agent: 'codex' as const, what: 'mcp' as const, target };\n if (handWritten(text))\n return {\n ...item,\n action: 'skip',\n note: `[mcp_servers.${SERVER_NAME}] is written by hand there; left as it is`,\n };\n const { block } = splitBlock(text);\n if (block === undefined) return { ...item, action: 'create' };\n return { ...item, action: block === codexBlock(launch) ? 'unchanged' : 'update' };\n}\n\nexport async function writeCodex(env: Env, launch: Launch): Promise<void> {\n const target = codexConfig(env);\n const text = (await readText(target)) ?? '';\n if (handWritten(text)) return;\n const eol = text.includes('\\r\\n') ? '\\r\\n' : '\\n';\n const block = codexBlock(launch).replaceAll('\\n', eol);\n const parts = splitBlock(text);\n const next =\n parts.block === undefined\n ? `${text}${text && !text.endsWith('\\n') ? eol : ''}${text ? eol : ''}${block}${eol}`\n : `${parts.before}${block}${parts.after}`;\n if (next === text) return;\n await fs.mkdir(dirname(target), { recursive: true });\n await backup(target);\n await fs.writeFile(target, next, 'utf8');\n}\n\nexport async function removeCodex(env: Env): Promise<boolean> {\n const target = codexConfig(env);\n const text = await readText(target);\n if (text === undefined) return false;\n const parts = splitBlock(text);\n if (parts.block === undefined) return false;\n const eol = text.includes('\\r\\n') ? '\\r\\n' : '\\n';\n const after = parts.after.startsWith(eol) ? parts.after.slice(eol.length) : parts.after;\n await fs.writeFile(\n target,\n `${parts.before.replace(/(\\r?\\n)+$/, eol)}${after}`.replace(/^\\r?\\n$/, ''),\n 'utf8',\n );\n return true;\n}\n\n// ---- MCP: one key in a JSON(C) config (Gemini, OpenCode, ZCode, Copilot CLI, Cursor) ------------\n\ntype JsonAgent = Exclude<Agent, 'claude' | 'codex'>;\n\nconst stdio = (l: Launch) => ({\n command: l.command,\n args: l.args,\n ...(l.env ? { env: l.env } : {}),\n});\n\n/** OpenCode reads opencode.jsonc or opencode.json in ~/.config/opencode; ours goes in the one there. */\nasync function opencodeConfig(env: Env): Promise<string> {\n const dir = join(env.home, '.config', 'opencode');\n const jsonc = join(dir, 'opencode.jsonc');\n return (await exists(jsonc)) ? jsonc : join(dir, 'opencode.json');\n}\n\ninterface JsonSpec {\n file: (env: Env) => Promise<string>;\n path: string[];\n entry: (l: Launch) => Record<string, unknown>;\n /**\n * AFD-173: the keys of an existing entry that are ours. Others stay as the user or the agent set\n * them: Kiro's disabled, autoApprove and disabledTools, Cursor's and Devin's tool toggles.\n */\n managed: string[];\n}\n\nconst STDIO_KEYS = ['command', 'args', 'env'];\n\n/** The flow tools that only read (flow_list, flow_describe, \u2026). */\nconst READ_ONLY_TOOLS = TOOL_SPECS.filter((s) => s.readOnly).map((s) => s.name);\n\n/** Cursor's variable for the open folder, resolved by Cursor when it starts the server. */\n// biome-ignore lint/suspicious/noTemplateCurlyInString: a Cursor config variable, not a template\nexport const CURSOR_WORKSPACE = '${workspaceFolder}';\n\nconst JSON_MCP: Record<JsonAgent, JsonSpec> = {\n gemini: {\n file: async (env) => join(env.home, '.gemini', 'settings.json'),\n path: ['mcpServers', SERVER_NAME],\n entry: (l) => ({\n ...stdio(l),\n description: 'Ansible Flow Designer: read and edit *.flow.yaml flows by key',\n }),\n managed: [...STDIO_KEYS, 'description'],\n },\n opencode: {\n file: opencodeConfig,\n path: ['mcp', SERVER_NAME],\n entry: (l) => ({\n type: 'local',\n command: [l.command, ...l.args],\n ...(l.env ? { environment: l.env } : {}),\n enabled: true,\n }),\n managed: ['type', 'command', 'environment'],\n },\n // ZCode reads ~/.agents/mcp.json (besides ~/.zcode/config.json): a file other agents share.\n zcode: {\n file: async (env) => join(env.home, '.agents', 'mcp.json'),\n path: ['mcpServers', SERVER_NAME],\n entry: stdio,\n managed: STDIO_KEYS,\n },\n copilot: {\n file: async (env) => join(env.home, '.copilot', 'mcp-config.json'),\n path: ['mcpServers', SERVER_NAME],\n entry: (l) => ({ type: 'local', ...stdio(l), tools: ['*'] }),\n managed: ['type', ...STDIO_KEYS],\n },\n cursor: {\n file: async (env) => join(env.home, '.cursor', 'mcp.json'),\n path: ['mcpServers', SERVER_NAME],\n // AFD-173: Cursor starts global servers in the home folder and sends no roots; the workspace\n // comes as a variable (the server leaves it out when Cursor does not resolve it).\n entry: (l) => ({\n ...stdio(l),\n env: { ...(l.env ?? {}), AFD_MCP_ROOTS: CURSOR_WORKSPACE },\n }),\n managed: STDIO_KEYS,\n },\n kiro: {\n file: async (env) => join(env.home, '.kiro', 'settings', 'mcp.json'),\n path: ['mcpServers', SERVER_NAME],\n // AFD-174: Kiro asks before every call; the tools that only read are approved when the entry\n // is first written. Not managed: what the user approves or takes back later stays.\n entry: (l) => ({ ...stdio(l), autoApprove: READ_ONLY_TOOLS }),\n managed: STDIO_KEYS,\n },\n // Antigravity IDE's global customization root; its own plugin folders do not load MCP servers.\n antigravity: {\n file: async (env) => join(env.home, '.gemini', 'config', 'mcp_config.json'),\n path: ['mcpServers', SERVER_NAME],\n entry: stdio,\n managed: STDIO_KEYS,\n },\n devin: {\n file: async (env) => devinMcpFile(env),\n path: ['mcpServers', SERVER_NAME],\n entry: stdio,\n managed: STDIO_KEYS,\n },\n};\n\n/**\n * Devin Desktop's MCP file, as its own code builds it: %USERPROFILE%\\AppData\\Roaming\\devin on\n * Windows, else $XDG_CONFIG_HOME/devin or ~/.config/devin. (It reads Windsurf's\n * ~/.codeium/windsurf/mcp_config.json too, and Claude Code's and Cursor's entries.)\n */\nexport function devinMcpFile(env: Env): string {\n const platform = env.platform ?? process.platform;\n return platform === 'win32'\n ? join(env.home, 'AppData', 'Roaming', 'devin', 'mcp_config.json')\n : join(env.xdgConfig || join(env.home, '.config'), 'devin', 'mcp_config.json');\n}\n\n/** The config file an agent's registration goes in (Claude's is managed by its CLI). */\nexport async function mcpTarget(agent: Agent, env: Env, launch?: Launch): Promise<string> {\n if (agent === 'claude') return launch ? claudeCommandLine(launch) : 'claude mcp';\n if (agent === 'codex') return codexConfig(env);\n return JSON_MCP[agent].file(env);\n}\n\nfunction formatting(text: string): FormattingOptions {\n const indent = /\\n([ \\t]+)\"/.exec(text)?.[1] ?? ' ';\n return {\n insertSpaces: !indent.includes('\\t'),\n tabSize: indent.includes('\\t') ? 1 : indent.length,\n eol: text.includes('\\r\\n') ? '\\r\\n' : '\\n',\n };\n}\n\nfunction current(text: string, path: string[]): unknown {\n const tree = parseTree(text);\n const node = tree && findNodeAtLocation(tree, path);\n return node ? parseJsonc(text.slice(node.offset, node.offset + node.length)) : undefined;\n}\n\n/** A config that does not parse is left alone: an edit could make it worse. */\nfunction broken(text: string): boolean {\n const errors: ParseError[] = [];\n parseJsonc(text, errors, { allowTrailingComma: true });\n return errors.length > 0;\n}\n\nexport async function planJson(agent: JsonAgent, env: Env, launch: Launch): Promise<Item> {\n const spec = JSON_MCP[agent];\n const target = await spec.file(env);\n const text = (await readText(target)) ?? '';\n const item = { agent, what: 'mcp' as const, target };\n if (text.trim() && broken(text))\n return { ...item, action: 'skip', note: 'it does not parse as JSON; left as it is' };\n const now = text.trim() ? current(text, spec.path) : undefined;\n if (now === undefined) return { ...item, action: 'create' };\n const want = spec.entry(launch);\n const have = (now && typeof now === 'object' ? now : {}) as Record<string, unknown>;\n const same = spec.managed.every((k) => JSON.stringify(have[k]) === JSON.stringify(want[k]));\n return { ...item, action: same ? 'unchanged' : 'update' };\n}\n\nexport async function writeJson(agent: JsonAgent, env: Env, launch: Launch): Promise<void> {\n const spec = JSON_MCP[agent];\n const target = await spec.file(env);\n const text = (await readText(target)) ?? '';\n if (text.trim() && broken(text)) return;\n const base = text.trim() ? text : '{}\\n';\n const want = spec.entry(launch);\n const now = current(base, spec.path);\n let next = base;\n if (now === undefined || !now || typeof now !== 'object' || Array.isArray(now))\n next = applyEdits(next, modify(next, spec.path, want, { formattingOptions: formatting(next) }));\n else\n for (const key of spec.managed)\n next = applyEdits(\n next,\n modify(next, [...spec.path, key], want[key], { formattingOptions: formatting(next) }),\n );\n if (next === text) return;\n await fs.mkdir(dirname(target), { recursive: true });\n await backup(target);\n await fs.writeFile(target, next, 'utf8');\n}\n\nexport async function removeJson(agent: JsonAgent, env: Env): Promise<boolean> {\n const spec = JSON_MCP[agent];\n const target = await spec.file(env);\n const text = await readText(target);\n if (!text || broken(text) || current(text, spec.path) === undefined) return false;\n await fs.writeFile(\n target,\n applyEdits(text, modify(text, spec.path, undefined, { formattingOptions: formatting(text) })),\n 'utf8',\n );\n return true;\n}\n\n// ---- MCP: Claude Code (its CLI) ---------------------------------------------------------------\n\nexport function claudeAddArgs(launch: Launch): string[] {\n const env = Object.entries(launch.env ?? {}).flatMap(([k, v]) => ['-e', `${k}=${v}`]);\n return ['mcp', 'add', '-s', 'user', SERVER_NAME, ...env, '--', launch.command, ...launch.args];\n}\n\n/** The command a user runs when the claude CLI cannot be run from here. */\nexport function claudeCommandLine(launch: Launch): string {\n const q = (a: string) => (/[\\s\"]/.test(a) ? `\"${a.replaceAll('\"', '\\\\\"')}\"` : a);\n return ['claude', ...claudeAddArgs(launch)].map(q).join(' ');\n}\n\nexport async function planClaude(env: Env, launch: Launch): Promise<Item> {\n const item = {\n agent: 'claude' as const,\n what: 'mcp' as const,\n target: claudeCommandLine(launch),\n };\n if (!env.claude)\n return {\n ...item,\n action: 'skip',\n note: 'claude is not on PATH: run this command once it is installed',\n };\n const got = await env.claude(['mcp', 'get', SERVER_NAME]);\n if (got.code !== 0) return { ...item, action: 'create' };\n const same = got.out.includes(launch.command) && launch.args.every((a) => got.out.includes(a));\n return { ...item, action: same ? 'unchanged' : 'update' };\n}\n\nexport async function writeClaude(env: Env, launch: Launch): Promise<void> {\n if (!env.claude) return;\n if ((await env.claude(['mcp', 'get', SERVER_NAME])).code === 0)\n await env.claude(['mcp', 'remove', SERVER_NAME, '-s', 'user']);\n const r = await env.claude(claudeAddArgs(launch));\n if (r.code !== 0) throw new Error(`claude mcp add failed: ${r.out.trim()}`);\n}\n\nexport async function removeClaude(env: Env): Promise<boolean> {\n if (!env.claude || (await env.claude(['mcp', 'get', SERVER_NAME])).code !== 0) return false;\n return (await env.claude(['mcp', 'remove', SERVER_NAME, '-s', 'user'])).code === 0;\n}\n\n/** Runs the claude CLI found at this path (a .cmd shim needs the shell, with quoted arguments). */\nexport function claudeRunner(path: string): Env['claude'] {\n const shim = /\\.(cmd|bat)$/i.test(path);\n return (args) =>\n new Promise((done) => {\n const quoted = shim ? args.map((a) => `\"${a.replaceAll('\"', '\"\"')}\"`) : args;\n execFile(\n shim ? `\"${path}\"` : path,\n quoted,\n { timeout: 30_000, windowsHide: true, shell: shim },\n (err, stdout, stderr) =>\n done({\n code: err ? ((err as { code?: number }).code ?? 1) : 0,\n out: `${stdout}${stderr}`,\n }),\n );\n });\n}\n\n// ---- plan, apply, remove ----------------------------------------------------------------------\n\n/** The registration of the flow tools for one agent (no skill). */\nexport function planMcp(agent: Agent, env: Env, launch: Launch): Promise<Item> {\n return agent === 'claude'\n ? planClaude(env, launch)\n : agent === 'codex'\n ? planCodex(env, launch)\n : planJson(agent, env, launch);\n}\n\nexport async function planSetup(\n agents: Agent[],\n scope: Scope,\n env: Env,\n launch: Launch,\n skill: SkillFiles,\n): Promise<Item[]> {\n const items: Item[] = [];\n const seen = new Set<string>();\n for (const agent of agents) {\n for (const dir of skillDirs(agent, scope, env)) {\n if (seen.has(dir)) continue;\n seen.add(dir);\n const action = await planSkill(dir, skill);\n const state = action === 'update' ? await skillState(dir) : undefined;\n items.push({\n agent,\n what: 'skill',\n target: dir,\n action,\n ...(action === 'skip'\n ? { note: 'a folder of that name exists and is not ours; left as it is' }\n : {}),\n ...(state?.edited.length\n ? { note: `you changed ${state.edited.join(', ')}; it will be replaced` }\n : {}),\n });\n }\n items.push(await planMcp(agent, env, launch));\n }\n return items;\n}\n\nexport async function applySetup(\n items: Item[],\n env: Env,\n launch: Launch,\n skill: SkillFiles,\n): Promise<void> {\n for (const item of items) {\n if (item.action !== 'create' && item.action !== 'update') continue;\n if (item.what === 'skill') await writeSkill(item.target, skill);\n else if (item.agent === 'claude') await writeClaude(env, launch);\n else if (item.agent === 'codex') await writeCodex(env, launch);\n else await writeJson(item.agent, env, launch);\n }\n}\n\n/** Removes skill copies (both scopes) and registrations of these agents; returns what went. */\nexport async function removeSetup(agents: Agent[], env: Env): Promise<Item[]> {\n const removed: Item[] = [];\n const seen = new Set<string>();\n for (const agent of agents) {\n for (const scope of ['repo', 'user'] as const)\n for (const dir of skillDirs(agent, scope, env)) {\n if (seen.has(dir)) continue;\n seen.add(dir);\n if (await removeSkill(dir))\n removed.push({ agent, what: 'skill', target: dir, action: 'remove' });\n }\n const gone =\n agent === 'claude'\n ? await removeClaude(env)\n : agent === 'codex'\n ? await removeCodex(env)\n : await removeJson(agent, env);\n if (gone)\n removed.push({ agent, what: 'mcp', target: await mcpTarget(agent, env), action: 'remove' });\n }\n return removed;\n}\n\n// ---- AFD-195: registrations that no longer start this server ----------------------------------\n\n/** Claude Code's user-scope servers, read from the file: `claude mcp get` starts the server. */\nexport const claudeConfig = (env: Env) => join(env.home, '.claude.json');\n\n/** What an agent's ansible-flow entry starts, read from its config file (never by running it). */\nexport interface Registration {\n agent: Agent;\n /** The config file the entry is in. */\n file: string;\n command?: string;\n /** The mcp.js it starts; none when it starts something else (npx, a script of the user's). */\n server?: string;\n /** Codex: a table the user wrote outside our block, which Set Up never changes. */\n hand?: boolean;\n}\n\nconst isServer = (a: unknown): a is string => typeof a === 'string' && /(^|[\\\\/])mcp\\.js$/i.test(a);\n\n/** The command and the mcp.js of an MCP entry: stdio (command, args) or OpenCode's command list. */\nexport function entryServer(entry: unknown): { command?: string; server?: string } {\n if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return {};\n const e = entry as Record<string, unknown>;\n const words = Array.isArray(e.command)\n ? (e.command as unknown[])\n : [e.command, ...(Array.isArray(e.args) ? (e.args as unknown[]) : [])];\n const [command] = words;\n const server = words.slice(1).find(isServer);\n return {\n ...(typeof command === 'string' ? { command } : {}),\n ...(server ? { server } : {}),\n };\n}\n\nconst parseOr = (text: string | undefined): unknown => {\n try {\n return text === undefined ? undefined : JSON.parse(text);\n } catch {\n return undefined;\n }\n};\n\nexport async function registration(agent: Agent, env: Env): Promise<Registration | undefined> {\n const file = agent === 'claude' ? claudeConfig(env) : await mcpTarget(agent, env);\n const text = await readText(file);\n // ~/.claude.json holds Claude Code's history too (megabytes): parsed only when it names us.\n if (!text?.includes(SERVER_NAME)) return undefined;\n if (agent === 'codex') {\n if (handWritten(text)) return { agent, file, hand: true };\n const { block } = splitBlock(text);\n if (block === undefined) return undefined;\n // Our block, as codexBlock writes it: TOML basic strings in JSON's escapes.\n const command = parseOr(/^command = (\".*\")\\s*$/m.exec(block)?.[1]);\n const args = parseOr(/^args = (\\[.*\\])\\s*$/m.exec(block)?.[1]);\n return { agent, file, ...entryServer({ command, args }) };\n }\n let entry: unknown;\n if (agent === 'claude') {\n const data = parseOr(text) as { mcpServers?: Record<string, unknown> } | undefined;\n entry = data?.mcpServers?.[SERVER_NAME];\n } else {\n if (broken(text)) return undefined;\n entry = current(text, JSON_MCP[agent].path);\n }\n return entry === undefined ? undefined : { agent, file, ...entryServer(entry) };\n}\n\n/** The extension ID of a server copied into an editor's storage: \u2026/globalStorage/<id>/mcp/mcp.js. */\nexport function storageId(server: string): string | undefined {\n return /[\\\\/]globalStorage[\\\\/]([^\\\\/]+)[\\\\/]mcp[\\\\/]mcp\\.js$/i.exec(server)?.[1]?.toLowerCase();\n}\n\nexport interface StaleRegistration extends Registration {\n server: string;\n /** missing: the mcp.js is gone; elsewhere: another extension ID's copy (an older publisher). */\n why: 'missing' | 'elsewhere';\n}\n\n/**\n * Entries of ours that no longer start this server (`server`, this extension's installed copy):\n * the mcp.js they name is gone, as when the publisher ID changed and the old ID's storage went\n * with it, or it is another extension ID's copy. Only an entry that starts an mcp.js in an\n * editor's global storage is ours: one the user wrote (a Codex table outside our block, a path of\n * their own) is never reported. Another editor's copy under the same ID is not stale.\n */\nexport async function staleRegistrations(\n agents: readonly Agent[],\n env: Env,\n server: string,\n): Promise<StaleRegistration[]> {\n const ours = storageId(server);\n const found: StaleRegistration[] = [];\n for (const agent of agents) {\n const r = await registration(agent, env);\n if (!r?.server || r.hand) continue;\n const id = storageId(r.server);\n if (!id) continue;\n if (!(await exists(r.server))) found.push({ ...r, server: r.server, why: 'missing' });\n else if (ours && id !== ours) found.push({ ...r, server: r.server, why: 'elsewhere' });\n }\n return found;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/node/run/pastRuns.ts
|
|
2
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { flowFileId, isFileId } from "@ansible-flow/core/flow/ids";
|
|
5
|
+
import { pastRunOf } from "@ansible-flow/core/run/pastRuns";
|
|
6
|
+
var FILE = "run.json";
|
|
7
|
+
function runsDir(storage, flowId) {
|
|
8
|
+
return join(storage, "runs", flowFileId(flowId));
|
|
9
|
+
}
|
|
10
|
+
async function keepRun(runDir, run) {
|
|
11
|
+
try {
|
|
12
|
+
await writeFile(join(runDir, FILE), JSON.stringify(run), "utf8");
|
|
13
|
+
} catch {
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function readRun(dir, id) {
|
|
17
|
+
try {
|
|
18
|
+
const run = JSON.parse(await readFile(join(dir, id, FILE), "utf8"));
|
|
19
|
+
return run && typeof run === "object" && run.id === id ? run : void 0;
|
|
20
|
+
} catch {
|
|
21
|
+
return void 0;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
async function listRuns(storage, flowId) {
|
|
25
|
+
if (!isFileId(flowId)) return [];
|
|
26
|
+
const dir = runsDir(storage, flowId);
|
|
27
|
+
let ids = [];
|
|
28
|
+
try {
|
|
29
|
+
ids = (await readdir(dir, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
30
|
+
} catch {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const id of ids.sort().reverse()) {
|
|
35
|
+
const run = await readRun(dir, id);
|
|
36
|
+
const past = run ? pastRunOf(run) : void 0;
|
|
37
|
+
if (past) out.push(past);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
async function loadRun(storage, flowId, id) {
|
|
42
|
+
if (!isFileId(flowId) || !/^[0-9A-Za-z]+$/.test(id)) return void 0;
|
|
43
|
+
return readRun(runsDir(storage, flowId), id);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
runsDir,
|
|
48
|
+
keepRun,
|
|
49
|
+
listRuns,
|
|
50
|
+
loadRun
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=chunk-4FHPTGI4.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/node/run/pastRuns.ts"],
|
|
4
|
+
"sourcesContent": ["// AFD-245: reads and writes the runs kept of a flow (a storage folder, runs/<flow id>/<run id>):\n// run.json beside the run's events. Retention is the run folders' own (afd.runs.keep).\nimport { readdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { flowFileId, isFileId } from '../../core/flow/ids';\nimport type { RunView } from '../../core/protocol';\nimport { type PastRun, pastRunOf } from '../../core/run/pastRuns';\n\nconst FILE = 'run.json';\n\n/** Where the runs of a flow are kept. */\nexport function runsDir(storage: string, flowId: string): string {\n return join(storage, 'runs', flowFileId(flowId));\n}\n\n/** Keeps a finished run with its events. The view holds host names and results, no input values. */\nexport async function keepRun(runDir: string, run: RunView): Promise<void> {\n try {\n await writeFile(join(runDir, FILE), JSON.stringify(run), 'utf8');\n } catch {\n // The run is shown now all the same; it is only missing from the list later.\n }\n}\n\nasync function readRun(dir: string, id: string): Promise<RunView | undefined> {\n try {\n const run = JSON.parse(await readFile(join(dir, id, FILE), 'utf8')) as RunView;\n return run && typeof run === 'object' && run.id === id ? run : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** The kept runs of a flow, the newest first. */\nexport async function listRuns(storage: string, flowId: string): Promise<PastRun[]> {\n if (!isFileId(flowId)) return [];\n const dir = runsDir(storage, flowId);\n let ids: string[] = [];\n try {\n ids = (await readdir(dir, { withFileTypes: true }))\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name);\n } catch {\n return [];\n }\n const out: PastRun[] = [];\n for (const id of ids.sort().reverse()) {\n const run = await readRun(dir, id);\n const past = run ? pastRunOf(run) : undefined;\n if (past) out.push(past);\n }\n return out;\n}\n\nexport async function loadRun(\n storage: string,\n flowId: string,\n id: string,\n): Promise<RunView | undefined> {\n // The id names a folder: only the ids the list gave are read.\n if (!isFileId(flowId) || !/^[0-9A-Za-z]+$/.test(id)) return undefined;\n return readRun(runsDir(storage, flowId), id);\n}\n"],
|
|
5
|
+
"mappings": ";AAEA,SAAS,SAAS,UAAU,iBAAiB;AAC7C,SAAS,YAAY;AACrB,SAAS,YAAY,gBAAgB;AAErC,SAAuB,iBAAiB;AAExC,IAAM,OAAO;AAGN,SAAS,QAAQ,SAAiB,QAAwB;AAC/D,SAAO,KAAK,SAAS,QAAQ,WAAW,MAAM,CAAC;AACjD;AAGA,eAAsB,QAAQ,QAAgB,KAA6B;AACzE,MAAI;AACF,UAAM,UAAU,KAAK,QAAQ,IAAI,GAAG,KAAK,UAAU,GAAG,GAAG,MAAM;AAAA,EACjE,QAAQ;AAAA,EAER;AACF;AAEA,eAAe,QAAQ,KAAa,IAA0C;AAC5E,MAAI;AACF,UAAM,MAAM,KAAK,MAAM,MAAM,SAAS,KAAK,KAAK,IAAI,IAAI,GAAG,MAAM,CAAC;AAClE,WAAO,OAAO,OAAO,QAAQ,YAAY,IAAI,OAAO,KAAK,MAAM;AAAA,EACjE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,SAAS,SAAiB,QAAoC;AAClF,MAAI,CAAC,SAAS,MAAM,EAAG,QAAO,CAAC;AAC/B,QAAM,MAAM,QAAQ,SAAS,MAAM;AACnC,MAAI,MAAgB,CAAC;AACrB,MAAI;AACF,WAAO,MAAM,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC,GAC9C,OAAO,CAAC,UAAU,MAAM,YAAY,CAAC,EACrC,IAAI,CAAC,UAAU,MAAM,IAAI;AAAA,EAC9B,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACA,QAAM,MAAiB,CAAC;AACxB,aAAW,MAAM,IAAI,KAAK,EAAE,QAAQ,GAAG;AACrC,UAAM,MAAM,MAAM,QAAQ,KAAK,EAAE;AACjC,UAAM,OAAO,MAAM,UAAU,GAAG,IAAI;AACpC,QAAI,KAAM,KAAI,KAAK,IAAI;AAAA,EACzB;AACA,SAAO;AACT;AAEA,eAAsB,QACpB,SACA,QACA,IAC8B;AAE9B,MAAI,CAAC,SAAS,MAAM,KAAK,CAAC,iBAAiB,KAAK,EAAE,EAAG,QAAO;AAC5D,SAAO,QAAQ,QAAQ,SAAS,MAAM,GAAG,EAAE;AAC7C;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
StoppedFollowing
|
|
3
|
+
} from "./chunk-GAL5LW6Y.js";
|
|
4
|
+
import {
|
|
5
|
+
TIMEOUTS
|
|
6
|
+
} from "./chunk-IIFP6UQR.js";
|
|
7
|
+
|
|
8
|
+
// src/node/awx/awxWorkflow.ts
|
|
9
|
+
import { awxEventsToLines, playNodes } from "@ansible-flow/core/awx/events";
|
|
10
|
+
import {
|
|
11
|
+
FINISHED
|
|
12
|
+
} from "@ansible-flow/core/awx/types";
|
|
13
|
+
import { durationSeconds } from "@ansible-flow/core/engine/util";
|
|
14
|
+
import { buildModel } from "@ansible-flow/core/flow/model";
|
|
15
|
+
import { summarizeRun } from "@ansible-flow/core/run/events";
|
|
16
|
+
var workflowName = (doc) => `AFD \xB7 ${doc.flow.key}`;
|
|
17
|
+
var segmentTemplateName = (doc, segment) => `AFD \xB7 ${doc.flow.key} \xB7 ${segment}`;
|
|
18
|
+
var gateId = (segment) => `${segment}.gate`;
|
|
19
|
+
var finishId = (segment) => `${segment}.finish`;
|
|
20
|
+
async function setupWorkflow(awx, doc, segments, picks) {
|
|
21
|
+
const templates = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const [i, s] of segments.entries()) {
|
|
23
|
+
const jt = await awx.upsertTemplate({
|
|
24
|
+
name: segmentTemplateName(doc, s.name),
|
|
25
|
+
description: `Ansible Flow Designer: ${doc.flow.name} (${doc.flow.key}), workflow segment ${s.name}`,
|
|
26
|
+
project: picks.project.id,
|
|
27
|
+
playbook: picks.playbooks[i],
|
|
28
|
+
inventory: picks.inventory.id,
|
|
29
|
+
execution_environment: picks.environment?.id,
|
|
30
|
+
credentials: picks.credentials.map((c) => c.id)
|
|
31
|
+
});
|
|
32
|
+
templates.set(s.name, jt.id);
|
|
33
|
+
}
|
|
34
|
+
const project = await awx.get(
|
|
35
|
+
`/api/v2/projects/${picks.project.id}/`
|
|
36
|
+
);
|
|
37
|
+
const name = workflowName(doc);
|
|
38
|
+
const body = {
|
|
39
|
+
name,
|
|
40
|
+
description: `Ansible Flow Designer: ${doc.flow.name} (${doc.flow.key}), split at its approvals`,
|
|
41
|
+
organization: project.organization,
|
|
42
|
+
ask_variables_on_launch: true
|
|
43
|
+
};
|
|
44
|
+
const found = await awx.named("workflow_job_templates", name);
|
|
45
|
+
const wf = found ? await awx.patch(`/api/v2/workflow_job_templates/${found.id}/`, body) : await awx.post("/api/v2/workflow_job_templates/", body);
|
|
46
|
+
for (const n of await awx.all(
|
|
47
|
+
`/api/v2/workflow_job_templates/${wf.id}/workflow_nodes/`
|
|
48
|
+
))
|
|
49
|
+
await awx.request("DELETE", `/api/v2/workflow_job_template_nodes/${n.id}/`);
|
|
50
|
+
const add = (node) => awx.post(`/api/v2/workflow_job_templates/${wf.id}/workflow_nodes/`, node);
|
|
51
|
+
const link = (from, kind, to) => awx.post(`/api/v2/workflow_job_template_nodes/${from}/${kind}_nodes/`, { id: to });
|
|
52
|
+
const ids = /* @__PURE__ */ new Map();
|
|
53
|
+
for (const s of segments)
|
|
54
|
+
if (!s.finish)
|
|
55
|
+
ids.set(
|
|
56
|
+
s.name,
|
|
57
|
+
(await add({ identifier: s.name, unified_job_template: templates.get(s.name) })).id
|
|
58
|
+
);
|
|
59
|
+
const model = buildModel(doc);
|
|
60
|
+
for (const s of segments) {
|
|
61
|
+
const gate = s.gate ? model.nodes.get(s.gate)?.node : void 0;
|
|
62
|
+
const from = ids.get(s.name);
|
|
63
|
+
if (!gate || from === void 0) continue;
|
|
64
|
+
const approval = await add({ identifier: gateId(s.name) });
|
|
65
|
+
await awx.post(`/api/v2/workflow_job_template_nodes/${approval.id}/create_approval_template/`, {
|
|
66
|
+
name: `[${gate.key}] ${gate.title}`,
|
|
67
|
+
// AFD-273: what the flow tells its approvers; AWX's own approvers are set in AWX.
|
|
68
|
+
description: gate.approval?.message ?? `${doc.flow.name}: approve to continue with the hosts waiting here`,
|
|
69
|
+
timeout: durationSeconds(gate.settings?.timeout) ?? 0
|
|
70
|
+
});
|
|
71
|
+
const finish = await add({
|
|
72
|
+
identifier: finishId(s.name),
|
|
73
|
+
unified_job_template: templates.get("finish")
|
|
74
|
+
});
|
|
75
|
+
await link(from, "success", approval.id);
|
|
76
|
+
await link(from, "failure", finish.id);
|
|
77
|
+
const child = (outcome) => segments.find((c) => c.parent === s.name && c.entry?.outcome === outcome)?.name;
|
|
78
|
+
const approved = ids.get(child("approved") ?? "");
|
|
79
|
+
const rejected = ids.get(child("rejected") ?? "");
|
|
80
|
+
if (approved !== void 0) await link(approval.id, "success", approved);
|
|
81
|
+
if (rejected !== void 0) await link(approval.id, "failure", rejected);
|
|
82
|
+
}
|
|
83
|
+
return name;
|
|
84
|
+
}
|
|
85
|
+
function withoutBookkeeping(summary) {
|
|
86
|
+
const routed = Object.values(summary.nodes).some(
|
|
87
|
+
(ports) => Object.values(ports).some((hosts) => hosts.includes("localhost"))
|
|
88
|
+
);
|
|
89
|
+
return {
|
|
90
|
+
...summary,
|
|
91
|
+
hosts: routed ? summary.hosts : summary.hosts.filter((h) => h !== "localhost"),
|
|
92
|
+
failures: summary.failures.filter((f) => f.node || f.host !== "localhost")
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async function runWorkflow(watch, templateId, inputs) {
|
|
96
|
+
const { awx, doc } = watch;
|
|
97
|
+
const launched = await awx.post(
|
|
98
|
+
`/api/v2/workflow_job_templates/${templateId}/launch/`,
|
|
99
|
+
{ extra_vars: inputs }
|
|
100
|
+
);
|
|
101
|
+
watch.launched?.(launched.workflow_job);
|
|
102
|
+
const plays = playNodes(doc);
|
|
103
|
+
const gates = new Map(
|
|
104
|
+
watch.segments.filter((s) => s.gate).map((s) => [gateId(s.name), s.gate])
|
|
105
|
+
);
|
|
106
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
107
|
+
const settledJob = (j) => !!j.complete || j.ended !== void 0 && Date.now() - j.ended > TIMEOUTS.awxEventsGrace;
|
|
108
|
+
const approvals = {};
|
|
109
|
+
let workflow = await awx.get(`/api/v2/workflow_jobs/${launched.workflow_job}/`);
|
|
110
|
+
let ended;
|
|
111
|
+
const state = () => {
|
|
112
|
+
const ordered = [...jobs.entries()].sort(([a], [b]) => a - b).map(([, j]) => j);
|
|
113
|
+
const lines = ordered.map((j) => awxEventsToLines(j.events, plays)).filter(Boolean);
|
|
114
|
+
const revision = ordered[0]?.job.scm_revision || void 0;
|
|
115
|
+
const info = {
|
|
116
|
+
jobId: workflow.id,
|
|
117
|
+
url: `${awx.base}/#/jobs/workflow/${workflow.id}/output`,
|
|
118
|
+
template: workflowName(doc),
|
|
119
|
+
status: workflow.status,
|
|
120
|
+
revision,
|
|
121
|
+
revisionMismatch: !!(revision && watch.sha && revision !== watch.sha),
|
|
122
|
+
workflow: true,
|
|
123
|
+
approvals: { ...approvals },
|
|
124
|
+
segments: ordered.map((j) => ({ name: j.segment, job: j.job.id, status: j.job.status }))
|
|
125
|
+
};
|
|
126
|
+
const summary = withoutBookkeeping(summarizeRun(lines.join("\n"), watch.routes));
|
|
127
|
+
return { info, summary, workflow };
|
|
128
|
+
};
|
|
129
|
+
for (; ; ) {
|
|
130
|
+
await new Promise((r) => setTimeout(r, TIMEOUTS.awxPoll));
|
|
131
|
+
const why = watch.stop?.();
|
|
132
|
+
if (why)
|
|
133
|
+
throw new StoppedFollowing("workflow", workflow.id, state().info.url, why === "timeout");
|
|
134
|
+
workflow = await awx.get(`/api/v2/workflow_jobs/${workflow.id}/`);
|
|
135
|
+
const nodes = await awx.all(
|
|
136
|
+
`/api/v2/workflow_jobs/${workflow.id}/workflow_nodes/`
|
|
137
|
+
);
|
|
138
|
+
let changed = false;
|
|
139
|
+
for (const n of nodes) {
|
|
140
|
+
const j = n.summary_fields.job;
|
|
141
|
+
if (!j) continue;
|
|
142
|
+
if (j.type === "workflow_approval") {
|
|
143
|
+
const node = gates.get(n.identifier);
|
|
144
|
+
if (!node) continue;
|
|
145
|
+
let status = "waiting";
|
|
146
|
+
if (j.status === "successful") status = "approved";
|
|
147
|
+
else if (FINISHED.has(j.status)) {
|
|
148
|
+
const a = await awx.get(`/api/v2/workflow_approvals/${j.id}/`);
|
|
149
|
+
status = a.timed_out ? "timed out" : "denied";
|
|
150
|
+
}
|
|
151
|
+
if (approvals[node]?.status !== status) changed = true;
|
|
152
|
+
approvals[node] = { status, url: `${awx.base}/#/workflow_approvals/${j.id}/details` };
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (j.type !== "job") continue;
|
|
156
|
+
const segment = n.identifier.endsWith(".finish") ? "finish" : n.identifier;
|
|
157
|
+
let tracked = jobs.get(j.id);
|
|
158
|
+
if (!tracked) {
|
|
159
|
+
tracked = { segment, job: await awx.job(j.id), events: [] };
|
|
160
|
+
jobs.set(j.id, tracked);
|
|
161
|
+
changed = true;
|
|
162
|
+
}
|
|
163
|
+
if (settledJob(tracked)) continue;
|
|
164
|
+
tracked.job = await awx.job(j.id);
|
|
165
|
+
const fresh = await awx.events(j.id, tracked.events.at(-1)?.counter ?? 0);
|
|
166
|
+
tracked.events.push(...fresh);
|
|
167
|
+
if (fresh.length > 0) changed = true;
|
|
168
|
+
if (FINISHED.has(tracked.job.status)) {
|
|
169
|
+
tracked.ended ??= Date.now();
|
|
170
|
+
tracked.complete = !!tracked.job.event_processing_finished;
|
|
171
|
+
if (tracked.complete) tracked.events = await awx.events(j.id, 0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (FINISHED.has(workflow.status)) ended ??= Date.now();
|
|
175
|
+
const settled = [...jobs.values()].every(settledJob);
|
|
176
|
+
if (ended !== void 0 && (settled || Date.now() - ended > TIMEOUTS.awxEventsGrace))
|
|
177
|
+
return state();
|
|
178
|
+
if (changed) watch.update(state());
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export {
|
|
183
|
+
workflowName,
|
|
184
|
+
segmentTemplateName,
|
|
185
|
+
setupWorkflow,
|
|
186
|
+
withoutBookkeeping,
|
|
187
|
+
runWorkflow
|
|
188
|
+
};
|
|
189
|
+
//# sourceMappingURL=chunk-A2NMQGQE.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../../src/node/awx/awxWorkflow.ts"],
|
|
4
|
+
"sourcesContent": ["// AWX workflows for flows with top-level approvals (AFD-11, ADR-0016). Set up: one job template per\n// segment playbook, then the workflow `AFD \u00B7 <key>`. Each gated segment leads to an AWX approval\n// node (approve: `<gate>-approved`, deny or time out: `<gate>-rejected`) and, when nobody reached\n// the gate, to `finish`. Launch follows the workflow job: its segment jobs' events are mapped back\n// to nodes like any AWX job, and each approval's state is shown on its node.\nimport { awxEventsToLines, playNodes } from '../../core/awx/events';\nimport {\n type AwxJob,\n type AwxJobEvent,\n type AwxNamed,\n type AwxWorkflowJob,\n type AwxWorkflowJobNode,\n FINISHED,\n} from '../../core/awx/types';\nimport type { Route, SegmentFile } from '../../core/engine/types';\nimport { durationSeconds } from '../../core/engine/util';\nimport { buildModel } from '../../core/flow/model';\nimport type { FlowDocument, Ulid } from '../../core/flow/types';\nimport type { AwxApproval, AwxRunInfo } from '../../core/protocol';\nimport { type RunSummary, summarizeRun } from '../../core/run/events';\nimport { TIMEOUTS } from '../timeouts';\nimport { type AwxClient, StoppedFollowing } from './client';\n\nexport const workflowName = (doc: FlowDocument) => `AFD \u00B7 ${doc.flow.key}`;\nexport const segmentTemplateName = (doc: FlowDocument, segment: string) =>\n `AFD \u00B7 ${doc.flow.key} \u00B7 ${segment}`;\nconst gateId = (segment: string) => `${segment}.gate`;\nconst finishId = (segment: string) => `${segment}.finish`;\n\nexport interface WorkflowPicks {\n project: AwxNamed;\n inventory: AwxNamed;\n environment?: AwxNamed;\n credentials: AwxNamed[];\n /** Repository paths of the segment playbooks, in segment order. */\n playbooks: string[];\n}\n\n/** Creates or updates the segment job templates and the workflow; returns the workflow name. */\nexport async function setupWorkflow(\n awx: AwxClient,\n doc: FlowDocument,\n segments: SegmentFile[],\n picks: WorkflowPicks,\n): Promise<string> {\n const templates = new Map<string, number>();\n for (const [i, s] of segments.entries()) {\n const jt = await awx.upsertTemplate({\n name: segmentTemplateName(doc, s.name),\n description: `Ansible Flow Designer: ${doc.flow.name} (${doc.flow.key}), workflow segment ${s.name}`,\n project: picks.project.id,\n playbook: picks.playbooks[i] as string,\n inventory: picks.inventory.id,\n execution_environment: picks.environment?.id,\n credentials: picks.credentials.map((c) => c.id),\n });\n templates.set(s.name, jt.id);\n }\n const project = await awx.get<{ organization: number | null }>(\n `/api/v2/projects/${picks.project.id}/`,\n );\n const name = workflowName(doc);\n const body = {\n name,\n description: `Ansible Flow Designer: ${doc.flow.name} (${doc.flow.key}), split at its approvals`,\n organization: project.organization,\n ask_variables_on_launch: true,\n };\n const found = await awx.named<AwxNamed>('workflow_job_templates', name);\n const wf = found\n ? await awx.patch<AwxNamed>(`/api/v2/workflow_job_templates/${found.id}/`, body)\n : await awx.post<AwxNamed>('/api/v2/workflow_job_templates/', body);\n // Rebuilt every time, so the workflow always matches the generated segments.\n for (const n of await awx.all<AwxNamed>(\n `/api/v2/workflow_job_templates/${wf.id}/workflow_nodes/`,\n ))\n await awx.request('DELETE', `/api/v2/workflow_job_template_nodes/${n.id}/`);\n const add = (node: Record<string, unknown>) =>\n awx.post<{ id: number }>(`/api/v2/workflow_job_templates/${wf.id}/workflow_nodes/`, node);\n const link = (from: number, kind: 'success' | 'failure', to: number) =>\n awx.post(`/api/v2/workflow_job_template_nodes/${from}/${kind}_nodes/`, { id: to });\n const ids = new Map<string, number>();\n for (const s of segments)\n if (!s.finish)\n ids.set(\n s.name,\n (await add({ identifier: s.name, unified_job_template: templates.get(s.name) })).id,\n );\n const model = buildModel(doc);\n for (const s of segments) {\n const gate = s.gate ? model.nodes.get(s.gate)?.node : undefined;\n const from = ids.get(s.name);\n if (!gate || from === undefined) continue;\n const approval = await add({ identifier: gateId(s.name) });\n await awx.post(`/api/v2/workflow_job_template_nodes/${approval.id}/create_approval_template/`, {\n name: `[${gate.key}] ${gate.title}`,\n // AFD-273: what the flow tells its approvers; AWX's own approvers are set in AWX.\n description:\n gate.approval?.message ??\n `${doc.flow.name}: approve to continue with the hosts waiting here`,\n timeout: durationSeconds(gate.settings?.timeout) ?? 0,\n });\n const finish = await add({\n identifier: finishId(s.name),\n unified_job_template: templates.get('finish'),\n });\n await link(from, 'success', approval.id);\n await link(from, 'failure', finish.id);\n const child = (outcome: 'approved' | 'rejected') =>\n segments.find((c) => c.parent === s.name && c.entry?.outcome === outcome)?.name;\n const approved = ids.get(child('approved') ?? '');\n const rejected = ids.get(child('rejected') ?? '');\n if (approved !== undefined) await link(approval.id, 'success', approved);\n if (rejected !== undefined) await link(approval.id, 'failure', rejected);\n }\n return name;\n}\n\n/**\n * The segment plays that restore and carry host sets run on localhost outside any node; the empty-\n * gate stop is one of them. Neither is a host of the flow or a failure of one.\n */\nexport function withoutBookkeeping(summary: RunSummary): RunSummary {\n const routed = Object.values(summary.nodes).some((ports) =>\n Object.values(ports).some((hosts) => hosts.includes('localhost')),\n );\n return {\n ...summary,\n hosts: routed ? summary.hosts : summary.hosts.filter((h) => h !== 'localhost'),\n failures: summary.failures.filter((f) => f.node || f.host !== 'localhost'),\n };\n}\n\nexport interface WorkflowWatch {\n awx: AwxClient;\n doc: FlowDocument;\n segments: SegmentFile[];\n routes: Record<string, Route>;\n /** Called with every change while the workflow runs. */\n update(state: { info: AwxRunInfo; summary: RunSummary; workflow: AwxWorkflowJob }): void;\n /** Local HEAD, for the revision check. */\n sha?: string;\n /** Asked before each poll: a reason to stop following (the workflow keeps running in AWX). */\n stop?: () => 'cancelled' | 'timeout' | undefined;\n /** The workflow job started (AFD-39: Stop Run cancels it). */\n launched?: (workflowJobId: number) => void;\n}\n\n/** Launches the workflow with the inputs and follows it until it and its jobs' events are done. */\nexport async function runWorkflow(\n watch: WorkflowWatch,\n templateId: number,\n inputs: Record<string, unknown>,\n): Promise<{ info: AwxRunInfo; summary: RunSummary; workflow: AwxWorkflowJob }> {\n const { awx, doc } = watch;\n const launched = await awx.post<{ workflow_job: number }>(\n `/api/v2/workflow_job_templates/${templateId}/launch/`,\n { extra_vars: inputs },\n );\n watch.launched?.(launched.workflow_job);\n const plays = playNodes(doc);\n const gates = new Map(\n watch.segments.filter((s) => s.gate).map((s) => [gateId(s.name), s.gate as Ulid]),\n );\n interface Tracked {\n segment: string;\n job: AwxJob;\n events: AwxJobEvent[];\n /** When the job finished; `complete` once AWX stored all its events. */\n ended?: number;\n complete?: boolean;\n }\n const jobs = new Map<number, Tracked>();\n const settledJob = (j: Tracked) =>\n !!j.complete || (j.ended !== undefined && Date.now() - j.ended > TIMEOUTS.awxEventsGrace);\n const approvals: Record<Ulid, AwxApproval> = {};\n let workflow = await awx.get<AwxWorkflowJob>(`/api/v2/workflow_jobs/${launched.workflow_job}/`);\n let ended: number | undefined;\n\n const state = () => {\n const ordered = [...jobs.entries()].sort(([a], [b]) => a - b).map(([, j]) => j);\n const lines = ordered.map((j) => awxEventsToLines(j.events, plays)).filter(Boolean);\n const revision = ordered[0]?.job.scm_revision || undefined;\n const info: AwxRunInfo = {\n jobId: workflow.id,\n url: `${awx.base}/#/jobs/workflow/${workflow.id}/output`,\n template: workflowName(doc),\n status: workflow.status,\n revision,\n revisionMismatch: !!(revision && watch.sha && revision !== watch.sha),\n workflow: true,\n approvals: { ...approvals },\n segments: ordered.map((j) => ({ name: j.segment, job: j.job.id, status: j.job.status })),\n };\n const summary = withoutBookkeeping(summarizeRun(lines.join('\\n'), watch.routes));\n return { info, summary, workflow };\n };\n\n for (;;) {\n await new Promise((r) => setTimeout(r, TIMEOUTS.awxPoll));\n const why = watch.stop?.();\n if (why)\n throw new StoppedFollowing('workflow', workflow.id, state().info.url, why === 'timeout');\n workflow = await awx.get<AwxWorkflowJob>(`/api/v2/workflow_jobs/${workflow.id}/`);\n const nodes = await awx.all<AwxWorkflowJobNode>(\n `/api/v2/workflow_jobs/${workflow.id}/workflow_nodes/`,\n );\n let changed = false;\n for (const n of nodes) {\n const j = n.summary_fields.job;\n if (!j) continue;\n if (j.type === 'workflow_approval') {\n const node = gates.get(n.identifier);\n if (!node) continue;\n let status: AwxApproval['status'] = 'waiting';\n if (j.status === 'successful') status = 'approved';\n else if (FINISHED.has(j.status)) {\n const a = await awx.get<{ timed_out?: boolean }>(`/api/v2/workflow_approvals/${j.id}/`);\n status = a.timed_out ? 'timed out' : 'denied';\n }\n if (approvals[node]?.status !== status) changed = true;\n approvals[node] = { status, url: `${awx.base}/#/workflow_approvals/${j.id}/details` };\n continue;\n }\n if (j.type !== 'job') continue;\n // Segment nodes are named after their segment; every finish node runs `finish`.\n const segment = n.identifier.endsWith('.finish') ? 'finish' : n.identifier;\n let tracked = jobs.get(j.id);\n if (!tracked) {\n tracked = { segment, job: await awx.job(j.id), events: [] };\n jobs.set(j.id, tracked);\n changed = true;\n }\n if (settledJob(tracked)) continue;\n tracked.job = await awx.job(j.id);\n const fresh = await awx.events(j.id, tracked.events.at(-1)?.counter ?? 0);\n tracked.events.push(...fresh);\n if (fresh.length > 0) changed = true;\n if (FINISHED.has(tracked.job.status)) {\n tracked.ended ??= Date.now();\n tracked.complete = !!(tracked.job as AwxJob & { event_processing_finished?: boolean })\n .event_processing_finished;\n // AWX stores a running job's events out of counter order, so polling `counter__gt` can\n // skip one that lands late: once they are all stored, read them all again.\n if (tracked.complete) tracked.events = await awx.events(j.id, 0);\n }\n }\n if (FINISHED.has(workflow.status)) ended ??= Date.now();\n const settled = [...jobs.values()].every(settledJob);\n if (ended !== undefined && (settled || Date.now() - ended > TIMEOUTS.awxEventsGrace))\n return state();\n if (changed) watch.update(state());\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SERVER_NAME,
|
|
3
|
+
SKILL_NAME,
|
|
4
|
+
claudeCommandLine
|
|
5
|
+
} from "./chunk-2MZQHIXP.js";
|
|
6
|
+
|
|
7
|
+
// src/node/ai/agents.ts
|
|
8
|
+
import { promises as fs } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
var AGENT_INFO = [
|
|
11
|
+
{
|
|
12
|
+
id: "claude",
|
|
13
|
+
label: "Claude Code",
|
|
14
|
+
detail: "CLI and VS Code extension: skill in .claude/skills, tools via claude mcp",
|
|
15
|
+
bins: ["claude"],
|
|
16
|
+
markers: [".claude.json"],
|
|
17
|
+
apps: {},
|
|
18
|
+
extensions: ["anthropic.claude-code"]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "codex",
|
|
22
|
+
label: "Codex",
|
|
23
|
+
detail: "CLI, app and VS Code extension: skill in .agents/skills, tools in ~/.codex/config.toml",
|
|
24
|
+
bins: ["codex"],
|
|
25
|
+
markers: [".codex"],
|
|
26
|
+
apps: {},
|
|
27
|
+
extensions: ["openai.chatgpt"]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "gemini",
|
|
31
|
+
label: "Gemini CLI",
|
|
32
|
+
detail: "skill in ~/.gemini/skills (.agents/skills in a repository), tools in ~/.gemini/settings.json",
|
|
33
|
+
bins: ["gemini"],
|
|
34
|
+
markers: [".gemini"],
|
|
35
|
+
apps: {},
|
|
36
|
+
extensions: ["google.gemini-cli-vscode-ide-companion"]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "opencode",
|
|
40
|
+
label: "OpenCode",
|
|
41
|
+
detail: "CLI and app: skill in .agents/skills, tools in ~/.config/opencode/opencode.json(c)",
|
|
42
|
+
bins: ["opencode"],
|
|
43
|
+
markers: [".config/opencode", ".local/share/opencode"],
|
|
44
|
+
apps: {},
|
|
45
|
+
extensions: ["sst-dev.opencode"]
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "zcode",
|
|
49
|
+
label: "ZCode",
|
|
50
|
+
detail: "app: skill in .agents/skills, tools in ~/.agents/mcp.json",
|
|
51
|
+
bins: ["zcode"],
|
|
52
|
+
markers: [".zcode"],
|
|
53
|
+
apps: { win: "Programs/ZCode/ZCode.exe", mac: "/Applications/ZCode.app" },
|
|
54
|
+
extensions: []
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "copilot",
|
|
58
|
+
label: "GitHub Copilot CLI",
|
|
59
|
+
detail: "skill in ~/.copilot/skills (.github/skills in a repository), tools in ~/.copilot/mcp-config.json",
|
|
60
|
+
bins: ["copilot"],
|
|
61
|
+
markers: [".copilot/config.json", ".copilot/mcp-config.json"],
|
|
62
|
+
apps: {},
|
|
63
|
+
extensions: []
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "cursor",
|
|
67
|
+
label: "Cursor",
|
|
68
|
+
detail: "skill in .claude/skills (Cursor reads it), tools in ~/.cursor/mcp.json",
|
|
69
|
+
bins: ["cursor"],
|
|
70
|
+
markers: [".cursor"],
|
|
71
|
+
apps: { win: "Programs/cursor/Cursor.exe", mac: "/Applications/Cursor.app" },
|
|
72
|
+
extensions: []
|
|
73
|
+
},
|
|
74
|
+
// AFD-173: agents built into VS Code forks; paths from what each agent was seen to read (AFD-172).
|
|
75
|
+
{
|
|
76
|
+
id: "kiro",
|
|
77
|
+
label: "Kiro",
|
|
78
|
+
detail: "skill in ~/.kiro/skills, tools in ~/.kiro/settings/mcp.json",
|
|
79
|
+
bins: ["kiro"],
|
|
80
|
+
markers: [".kiro"],
|
|
81
|
+
apps: { win: "Programs/Kiro/Kiro.exe", mac: "/Applications/Kiro.app" },
|
|
82
|
+
extensions: []
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "antigravity",
|
|
86
|
+
label: "Antigravity IDE",
|
|
87
|
+
detail: "skill in ~/.gemini/config/skills, tools in ~/.gemini/config/mcp_config.json",
|
|
88
|
+
bins: ["antigravity-ide"],
|
|
89
|
+
markers: [".antigravity-ide", ".gemini/antigravity-ide"],
|
|
90
|
+
apps: {
|
|
91
|
+
win: "Programs/Antigravity IDE/Antigravity IDE.exe",
|
|
92
|
+
mac: "/Applications/Antigravity IDE.app"
|
|
93
|
+
},
|
|
94
|
+
extensions: []
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "devin",
|
|
98
|
+
label: "Devin Desktop (Windsurf)",
|
|
99
|
+
detail: "skill in ~/.codeium/windsurf/skills, tools in Devin\u2019s mcp_config.json (local agent mode)",
|
|
100
|
+
bins: ["devin-desktop"],
|
|
101
|
+
markers: [".devin", ".codeium/windsurf"],
|
|
102
|
+
apps: { win: "Programs/Devin/Devin.exe", mac: "/Applications/Devin.app" },
|
|
103
|
+
extensions: []
|
|
104
|
+
}
|
|
105
|
+
];
|
|
106
|
+
var agentLabel = (id) => AGENT_INFO.find((a) => a.id === id)?.label ?? id;
|
|
107
|
+
var exists = (path) => fs.stat(path).then(
|
|
108
|
+
() => true,
|
|
109
|
+
() => false
|
|
110
|
+
);
|
|
111
|
+
async function evidence(info, probe) {
|
|
112
|
+
for (const bin of info.bins) if (await probe.onPath(bin)) return `${bin} on PATH`;
|
|
113
|
+
const ext = info.extensions.find((e) => probe.extensions.includes(e));
|
|
114
|
+
if (ext) return `VS Code extension ${ext}`;
|
|
115
|
+
for (const m of info.markers) if (await exists(join(probe.home, m))) return `~/${m}`;
|
|
116
|
+
const app = probe.platform === "win32" && info.apps.win && probe.localAppData ? join(probe.localAppData, info.apps.win) : probe.platform === "darwin" ? info.apps.mac : void 0;
|
|
117
|
+
if (app && await exists(app)) return `${info.label} app`;
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
async function findAgents(probe) {
|
|
121
|
+
const found = /* @__PURE__ */ new Map();
|
|
122
|
+
const why = await Promise.all(AGENT_INFO.map((info) => evidence(info, probe)));
|
|
123
|
+
AGENT_INFO.forEach((info, i) => {
|
|
124
|
+
const w = why[i];
|
|
125
|
+
if (w) found.set(info.id, w);
|
|
126
|
+
});
|
|
127
|
+
return found;
|
|
128
|
+
}
|
|
129
|
+
function mcpJson(launch) {
|
|
130
|
+
const entry = {
|
|
131
|
+
command: launch.command,
|
|
132
|
+
args: launch.args,
|
|
133
|
+
...launch.env ? { env: launch.env } : {}
|
|
134
|
+
};
|
|
135
|
+
return JSON.stringify({ mcpServers: { [SERVER_NAME]: entry } }, null, 2);
|
|
136
|
+
}
|
|
137
|
+
function otherAgentGuide(launch) {
|
|
138
|
+
const fence = "```";
|
|
139
|
+
return [
|
|
140
|
+
"# Ansible flows in another AI agent",
|
|
141
|
+
"",
|
|
142
|
+
"Ansible Flow Designer gives AI agents two things. Set Up AI Assistants writes both for Claude Code, Codex, Gemini CLI, OpenCode, ZCode, GitHub Copilot CLI, Cursor, Kiro, Antigravity IDE and Devin Desktop (Windsurf); for any other agent, do it once by hand:",
|
|
143
|
+
"",
|
|
144
|
+
`1. **The skill** \`${SKILL_NAME}\`: the rules, the workflow and the format reference. Use _Copy Skill To Folder\u2026_ and pick the folder your agent reads skills from: many read \`.agents/skills\` or \`.claude/skills\` in the repository or in your home folder.`,
|
|
145
|
+
`2. **The flow tools**: an MCP server (stdio) named \`${SERVER_NAME}\`. Most agents take this entry in their MCP config (_Copy MCP Config_):`,
|
|
146
|
+
"",
|
|
147
|
+
`${fence}json`,
|
|
148
|
+
mcpJson(launch),
|
|
149
|
+
fence,
|
|
150
|
+
"",
|
|
151
|
+
" As a command line:",
|
|
152
|
+
"",
|
|
153
|
+
`${fence}text`,
|
|
154
|
+
`${launch.env ? `${Object.entries(launch.env).map(([k, v]) => `${k}=${v}`).join(" ")} ` : ""}"${launch.command}" ${launch.args.map((a) => `"${a}"`).join(" ")}`,
|
|
155
|
+
fence,
|
|
156
|
+
"",
|
|
157
|
+
'3. Restart the agent and ask it, for example, to "create an Ansible flow that upgrades the firewalls, with checks before and after and a rollback".',
|
|
158
|
+
"",
|
|
159
|
+
"Where some agents keep their MCP config (check the agent\u2019s documentation, these move):",
|
|
160
|
+
"",
|
|
161
|
+
"| Agent | MCP config |",
|
|
162
|
+
"| --- | --- |",
|
|
163
|
+
"| Qwen Code | `~/.qwen/settings.json` (`mcpServers`) |",
|
|
164
|
+
"| Cline, Roo Code | MCP Servers panel, Configure (`mcpServers`) |",
|
|
165
|
+
"| Claude Desktop | `claude_desktop_config.json` (`mcpServers`) |",
|
|
166
|
+
"",
|
|
167
|
+
"The server file lives in VS Code\u2019s storage for this extension and is refreshed with every update; the command stays the same unless VS Code itself moves. Without Node 20+ on PATH the command uses VS Code\u2019s own runtime (`ELECTRON_RUN_AS_NODE=1`).",
|
|
168
|
+
"",
|
|
169
|
+
`Claude Code, for reference: \`${claudeCommandLine(launch)}\``,
|
|
170
|
+
""
|
|
171
|
+
].join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export {
|
|
175
|
+
AGENT_INFO,
|
|
176
|
+
agentLabel,
|
|
177
|
+
findAgents,
|
|
178
|
+
mcpJson,
|
|
179
|
+
otherAgentGuide
|
|
180
|
+
};
|
|
181
|
+
//# sourceMappingURL=chunk-EPJKJBNW.js.map
|