@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.
Files changed (152) hide show
  1. package/README.md +94 -20
  2. package/dist/index.d.ts +3 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +2 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/install.d.ts +89 -0
  7. package/dist/install.d.ts.map +1 -1
  8. package/dist/install.js +219 -18
  9. package/dist/install.js.map +1 -1
  10. package/dist/validate/index.d.ts +21 -0
  11. package/dist/validate/index.d.ts.map +1 -1
  12. package/dist/validate/index.js +77 -0
  13. package/dist/validate/index.js.map +1 -1
  14. package/extensions/cmux/.agents/plugin.json +37 -0
  15. package/extensions/cmux/skills/cmux/SKILL.md +82 -0
  16. package/extensions/cmux/skills/cmux/agents/openai.yaml +4 -0
  17. package/extensions/cmux/skills/cmux/references/handles-and-identify.md +35 -0
  18. package/extensions/cmux/skills/cmux/references/panes-surfaces.md +37 -0
  19. package/extensions/cmux/skills/cmux/references/trigger-flash-and-health.md +23 -0
  20. package/extensions/cmux/skills/cmux/references/windows-workspaces.md +31 -0
  21. package/extensions/cmux/skills/cmux-vm-monitor/SKILL.md +122 -0
  22. package/extensions/cmux/skills/cmux-vm-monitor/agents/openai.yaml +4 -0
  23. package/extensions/cmux/skills/cmux-vm-monitor/references/cmux-commands.md +66 -0
  24. package/extensions/cmux/skills/cmux-vm-monitor/scripts/codex_vm_monitor.sh +45 -0
  25. package/extensions/cmux/skills/cmux-workspace/SKILL.md +93 -0
  26. package/extensions/dev-state/.agents/plugin.json +35 -0
  27. package/extensions/dev-state/skills/dev-state-plan-graduation/SKILL.md +194 -0
  28. package/extensions/dev-state/skills/dev-state-plan-graduation/agents/openai.yaml +4 -0
  29. package/extensions/dev-state/skills/dev-state-plan-graduation/references/reference.md +130 -0
  30. package/extensions/devops/.agents/plugin.json +36 -0
  31. package/extensions/devops/skills/azure-devops-cli/SKILL.md +431 -0
  32. package/extensions/devops/skills/azure-devops-cli/agents/openai.yaml +4 -0
  33. package/extensions/devops/skills/ci-pipeline-strategy/SKILL.md +217 -0
  34. package/extensions/devops/skills/ci-pipeline-strategy/agents/openai.yaml +4 -0
  35. package/{plugins/net-revenue-management/.codex-plugin → extensions/net-revenue-management/.agents}/plugin.json +10 -6
  36. package/extensions/plugin-dev/.agents/plugin.json +42 -0
  37. package/extensions/plugin-dev/.mcp.json +3 -0
  38. package/extensions/plugin-dev/agents/agent-creator.md +199 -0
  39. package/extensions/plugin-dev/agents/plugin-validator.md +91 -0
  40. package/extensions/plugin-dev/agents/skill-reviewer.md +212 -0
  41. package/extensions/plugin-dev/commands/_archive/create-marketplace.md +427 -0
  42. package/extensions/plugin-dev/commands/_archive/plugin-dev-guide.md +12 -0
  43. package/extensions/plugin-dev/commands/create-plugin.md +498 -0
  44. package/extensions/plugin-dev/commands/start.md +81 -0
  45. package/extensions/plugin-dev/hooks/hooks.json +3 -0
  46. package/extensions/plugin-dev/skills/agent-development/SKILL.md +641 -0
  47. package/extensions/plugin-dev/skills/agent-development/examples/agent-creation-prompt.md +250 -0
  48. package/extensions/plugin-dev/skills/agent-development/examples/complete-agent-examples.md +461 -0
  49. package/extensions/plugin-dev/skills/agent-development/references/advanced-agent-fields.md +246 -0
  50. package/extensions/plugin-dev/skills/agent-development/references/agent-creation-system-prompt.md +216 -0
  51. package/extensions/plugin-dev/skills/agent-development/references/permission-modes-rules.md +226 -0
  52. package/extensions/plugin-dev/skills/agent-development/references/system-prompt-design.md +464 -0
  53. package/extensions/plugin-dev/skills/agent-development/references/triggering-examples.md +474 -0
  54. package/extensions/plugin-dev/skills/agent-development/scripts/create-agent-skeleton.sh +176 -0
  55. package/extensions/plugin-dev/skills/agent-development/scripts/test-agent-trigger.sh +227 -0
  56. package/extensions/plugin-dev/skills/agent-development/scripts/validate-agent.sh +227 -0
  57. package/extensions/plugin-dev/skills/command-development/SKILL.md +763 -0
  58. package/extensions/plugin-dev/skills/command-development/examples/plugin-commands.md +612 -0
  59. package/extensions/plugin-dev/skills/command-development/examples/simple-commands.md +527 -0
  60. package/extensions/plugin-dev/skills/command-development/references/advanced-workflows.md +762 -0
  61. package/extensions/plugin-dev/skills/command-development/references/documentation-patterns.md +769 -0
  62. package/extensions/plugin-dev/skills/command-development/references/frontmatter-reference.md +508 -0
  63. package/extensions/plugin-dev/skills/command-development/references/interactive-commands.md +966 -0
  64. package/extensions/plugin-dev/skills/command-development/references/marketplace-considerations.md +943 -0
  65. package/extensions/plugin-dev/skills/command-development/references/plugin-features-reference.md +637 -0
  66. package/extensions/plugin-dev/skills/command-development/references/plugin-integration.md +191 -0
  67. package/extensions/plugin-dev/skills/command-development/references/skill-tool.md +447 -0
  68. package/extensions/plugin-dev/skills/command-development/references/testing-strategies.md +723 -0
  69. package/extensions/plugin-dev/skills/command-development/scripts/check-frontmatter.sh +234 -0
  70. package/extensions/plugin-dev/skills/command-development/scripts/validate-command.sh +160 -0
  71. package/extensions/plugin-dev/skills/hook-development/SKILL.md +861 -0
  72. package/extensions/plugin-dev/skills/hook-development/examples/load-context.sh +55 -0
  73. package/extensions/plugin-dev/skills/hook-development/examples/validate-bash.sh +57 -0
  74. package/extensions/plugin-dev/skills/hook-development/examples/validate-write.sh +48 -0
  75. package/extensions/plugin-dev/skills/hook-development/references/advanced.md +871 -0
  76. package/extensions/plugin-dev/skills/hook-development/references/hook-input-schemas.md +145 -0
  77. package/extensions/plugin-dev/skills/hook-development/references/migration.md +392 -0
  78. package/extensions/plugin-dev/skills/hook-development/references/patterns.md +430 -0
  79. package/extensions/plugin-dev/skills/hook-development/scripts/README.md +181 -0
  80. package/extensions/plugin-dev/skills/hook-development/scripts/hook-linter.sh +153 -0
  81. package/extensions/plugin-dev/skills/hook-development/scripts/test-hook.sh +276 -0
  82. package/extensions/plugin-dev/skills/hook-development/scripts/validate-hook-schema.sh +159 -0
  83. package/extensions/plugin-dev/skills/mcp-integration/SKILL.md +775 -0
  84. package/extensions/plugin-dev/skills/mcp-integration/examples/http-server.json +20 -0
  85. package/extensions/plugin-dev/skills/mcp-integration/examples/sse-server.json +19 -0
  86. package/extensions/plugin-dev/skills/mcp-integration/examples/stdio-server.json +38 -0
  87. package/extensions/plugin-dev/skills/mcp-integration/examples/ws-server.json +26 -0
  88. package/extensions/plugin-dev/skills/mcp-integration/references/authentication.md +601 -0
  89. package/extensions/plugin-dev/skills/mcp-integration/references/server-discovery.md +190 -0
  90. package/extensions/plugin-dev/skills/mcp-integration/references/server-types.md +572 -0
  91. package/extensions/plugin-dev/skills/mcp-integration/references/tool-usage.md +623 -0
  92. package/extensions/plugin-dev/skills/plugin-dev-guide/SKILL.md +222 -0
  93. package/extensions/plugin-dev/skills/plugin-structure/SKILL.md +705 -0
  94. package/extensions/plugin-dev/skills/plugin-structure/examples/advanced-plugin.md +774 -0
  95. package/extensions/plugin-dev/skills/plugin-structure/examples/minimal-plugin.md +83 -0
  96. package/extensions/plugin-dev/skills/plugin-structure/examples/standard-plugin.md +611 -0
  97. package/extensions/plugin-dev/skills/plugin-structure/references/advanced-topics.md +289 -0
  98. package/extensions/plugin-dev/skills/plugin-structure/references/component-patterns.md +592 -0
  99. package/extensions/plugin-dev/skills/plugin-structure/references/github-actions.md +233 -0
  100. package/extensions/plugin-dev/skills/plugin-structure/references/headless-ci-mode.md +193 -0
  101. package/extensions/plugin-dev/skills/plugin-structure/references/manifest-reference.md +625 -0
  102. package/extensions/plugin-dev/skills/plugin-structure/references/output-styles.md +116 -0
  103. package/extensions/plugin-dev/skills/skill-development/SKILL.md +564 -0
  104. package/extensions/plugin-dev/skills/skill-development/examples/complete-skill.md +465 -0
  105. package/extensions/plugin-dev/skills/skill-development/examples/frontmatter-templates.md +167 -0
  106. package/extensions/plugin-dev/skills/skill-development/examples/minimal-skill.md +111 -0
  107. package/extensions/plugin-dev/skills/skill-development/references/advanced-frontmatter.md +225 -0
  108. package/extensions/plugin-dev/skills/skill-development/references/commands-vs-skills.md +39 -0
  109. package/extensions/plugin-dev/skills/skill-development/references/skill-creation-workflow.md +379 -0
  110. package/extensions/plugin-dev/skills/skill-development/references/skill-creator-original.md +210 -0
  111. package/package.json +8 -11
  112. package/scripts/enact-extensions.mjs +751 -16
  113. package/scripts/hooks/session-start-drift-check.mjs +58 -0
  114. package/scripts/lib/build-index.mjs +50 -0
  115. package/scripts/lib/bundle-hash.mjs +137 -0
  116. package/scripts/lib/hooks.mjs +389 -0
  117. package/scripts/lib/ledger.mjs +162 -0
  118. package/scripts/lib/list-bundles.mjs +70 -0
  119. package/scripts/lib/outdated.mjs +144 -0
  120. package/scripts/lib/provision-mcp.mjs +369 -0
  121. package/scripts/lib/resolve-bundle.mjs +121 -0
  122. package/scripts/lib/run-install.mjs +321 -39
  123. package/scripts/lib/run-uninstall.mjs +220 -0
  124. package/scripts/lib/run-update.mjs +152 -0
  125. package/scripts/lib/run-validate.mjs +12 -18
  126. package/scripts/lib/serve.mjs +454 -0
  127. package/scripts/postinstall.mjs +63 -0
  128. package/scripts/setup-enact-context.sh +2 -2
  129. package/spec/index.json +59 -0
  130. package/web/assets/README.md +111 -0
  131. package/web/assets/logo-full.png +0 -0
  132. package/web/assets/logo-slim.png +0 -0
  133. package/web/assets/tokens/base.css +45 -0
  134. package/web/assets/tokens/colors.css +248 -0
  135. package/web/assets/tokens/effects.css +24 -0
  136. package/web/assets/tokens/fonts.css +8 -0
  137. package/web/assets/tokens/index.css +18 -0
  138. package/web/assets/tokens/spacing.css +50 -0
  139. package/web/index.html +1188 -0
  140. package/.agents/plugins/marketplace.json +0 -20
  141. package/catalog/enact-context.json +0 -9
  142. package/catalog/enact-factory.json +0 -7
  143. package/catalog/enact-operator.json +0 -7
  144. package/catalog/enact-wiki.json +0 -7
  145. package/catalog/net-revenue-management.json +0 -8
  146. package/scripts/rename-supervisor-to-operator.pl +0 -66
  147. package/scripts/sync-manifests.mjs +0 -23
  148. package/scripts/validate-catalog.mjs +0 -37
  149. package/scripts/validate-plugin.mjs +0 -10
  150. /package/{plugins → extensions}/net-revenue-management/.mcp.json +0 -0
  151. /package/{plugins → extensions}/net-revenue-management/skills/net-revenue-risks/SKILL.md +0 -0
  152. /package/{plugins → extensions}/net-revenue-management/skills/net-revenue-scenario/SKILL.md +0 -0
@@ -1,60 +1,256 @@
1
+ import { join } from "node:path";
2
+ import { homedir } from "node:os";
1
3
  import {
2
4
  installPluginBundle,
3
5
  installClaudePluginBundle,
4
6
  installCursorPluginBundle,
5
7
  installCodexPluginBundle,
8
+ installSharedPluginBundle,
6
9
  defaultEnactHome,
10
+ defaultSharedHome,
7
11
  } from "../../dist/index.js";
12
+ import { appendEntry } from "./ledger.mjs";
13
+ import { bundleHash } from "./bundle-hash.mjs";
14
+ import { provisionMcp, summarizeProvision } from "./provision-mcp.mjs";
8
15
 
9
- export function runInstall(pluginRoot, options = {}) {
10
- const platform = options.platform ?? "codex";
11
-
12
- if (platform === "claude") {
13
- const result = installClaudePluginBundle({
14
- pluginRoot,
15
- claudeHome: options.claudeHome,
16
- marketplaceName: options.marketplaceName,
17
- sync: options.sync,
18
- });
19
- console.log(`installed ${result.name} -> ${result.installedPluginPath}`);
20
- console.log(`registered marketplace ${result.marketplaceName} -> ${result.marketplaceDir}`);
21
- for (const path of result.refreshedPaths) {
22
- console.log(`refreshed ${path}`);
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, ...)
18
+ // An explicit --<platform>-home always wins over the scope default.
19
+ function resolveHomes(options) {
20
+ const scope = options.scope ?? "global";
21
+ const local = (sub) => join(process.cwd(), sub);
22
+ return {
23
+ scope,
24
+ codexHome: options.codexHome ?? (scope === "local" ? local(".codex") : undefined),
25
+ enactHome: options.enactHome ?? (scope === "local" ? local(".enact") : defaultEnactHome()),
26
+ claudeHome: options.claudeHome ?? (scope === "local" ? local(".claude") : undefined),
27
+ cursorHome: options.cursorHome ?? (scope === "local" ? local(".cursor") : undefined),
28
+ // For shared: sharedHome is the base directory; installSharedPluginBundle appends .agents/skills/<name>
29
+ // Global default: homedir() (skills land at ~/.agents/skills/<name>)
30
+ // Local: cwd (skills land at ./.agents/skills/<name>)
31
+ sharedHome: options.sharedHome ?? (scope === "local" ? process.cwd() : defaultSharedHome()),
32
+ };
33
+ }
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // ALL_PLATFORMS — deterministic order for `--platform all`
37
+ // ---------------------------------------------------------------------------
38
+ const ALL_PLATFORMS = ["codex", "claude", "cursor", "enact", "shared"];
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // PLATFORM_TARGETS — data-driven dispatch table
42
+ //
43
+ // Each entry describes how to install for a given platform:
44
+ // homeField — which field of `homes` supplies the target directory
45
+ // install — the function to call; receives (pluginRoot, home, options)
46
+ // log — how to log the result
47
+ // ---------------------------------------------------------------------------
48
+ const PLATFORM_TARGETS = {
49
+ claude: {
50
+ homeField: "claudeHome",
51
+ install(pluginRoot, home, options) {
52
+ return installClaudePluginBundle({
53
+ pluginRoot,
54
+ claudeHome: home,
55
+ marketplaceName: options.marketplaceName,
56
+ sync: options.sync,
57
+ });
58
+ },
59
+ log(result) {
60
+ console.log(`installed ${result.name} -> ${result.installedPluginPath}`);
61
+ console.log(`registered marketplace ${result.marketplaceName} -> ${result.marketplaceDir}`);
62
+ for (const path of result.refreshedPaths) {
63
+ console.log(`refreshed ${path}`);
64
+ }
65
+ },
66
+ },
67
+
68
+ cursor: {
69
+ homeField: "cursorHome",
70
+ install(pluginRoot, home, options) {
71
+ return installCursorPluginBundle({
72
+ pluginRoot,
73
+ cursorHome: home,
74
+ sync: options.sync,
75
+ });
76
+ },
77
+ log(result) {
78
+ console.log(`installed ${result.name} -> ${result.installedPluginPath}`);
79
+ for (const path of result.refreshedPaths) {
80
+ console.log(`refreshed ${path}`);
81
+ }
82
+ },
83
+ },
84
+
85
+ enact: {
86
+ homeField: "enactHome",
87
+ install(pluginRoot, home, options) {
88
+ return installCodexPluginBundle({
89
+ pluginRoot,
90
+ codexHome: home,
91
+ marketplaceName: options.marketplaceName,
92
+ enable: options.enable,
93
+ sync: options.sync,
94
+ });
95
+ },
96
+ log(result) {
97
+ console.log(`installed ${result.pluginConfigKey} -> ${result.configPath}`);
98
+ console.log(`wrote ${result.marketplacePath}`);
99
+ for (const path of result.refreshedPaths) {
100
+ console.log(`refreshed ${path}`);
101
+ }
102
+ },
103
+ },
104
+
105
+ shared: {
106
+ homeField: "sharedHome",
107
+ install(pluginRoot, home, _options) {
108
+ return installSharedPluginBundle({
109
+ pluginRoot,
110
+ sharedHome: home,
111
+ });
112
+ },
113
+ log(result) {
114
+ console.log(`installed ${result.name} skills -> ${result.installedSkillsPath}`);
115
+ },
116
+ },
117
+ };
118
+
119
+ // ---------------------------------------------------------------------------
120
+ // parsePlatforms — expand "all" and split comma-separated platform lists.
121
+ // Returns a deduped array of validated platform name strings.
122
+ // Throws on any unknown platform token (no silent codex fallthrough).
123
+ // ---------------------------------------------------------------------------
124
+ function parsePlatforms(platform) {
125
+ if (!platform || platform === "codex") return ["codex"];
126
+ if (platform === "all") return ALL_PLATFORMS;
127
+
128
+ // Comma-separated: "claude,cursor" → ["claude", "cursor"]. Dedup preserving order.
129
+ const parts = [...new Set(platform.split(",").map((p) => p.trim()).filter(Boolean))];
130
+ if (parts.length === 0) return ["codex"];
131
+
132
+ // Validate each token. "codex" is valid (table fallthrough); "all" is only valid
133
+ // as a standalone value, so it is rejected here when mixed with other names.
134
+ const valid = new Set(["codex", ...Object.keys(PLATFORM_TARGETS)]);
135
+ for (const part of parts) {
136
+ if (!valid.has(part)) {
137
+ throw new Error(
138
+ `Unknown platform: "${part}". Valid: codex, claude, cursor, enact, shared, all`,
139
+ );
23
140
  }
24
- return result;
25
141
  }
142
+ return parts;
143
+ }
26
144
 
27
- if (platform === "cursor") {
28
- const result = installCursorPluginBundle({
29
- pluginRoot,
30
- cursorHome: options.cursorHome,
31
- sync: options.sync,
145
+ // ---------------------------------------------------------------------------
146
+ // Ledger wiring — record successful installs to the install ledger.
147
+ //
148
+ // BEST-EFFORT: every ledger write is wrapped so a failure NEVER throws out of
149
+ // the install. On failure we emit a single-line warning to stderr and continue.
150
+ // ---------------------------------------------------------------------------
151
+
152
+ // The installed-path field varies by platform result shape.
153
+ function installedPathOf(result) {
154
+ return result.installedPluginPath ?? result.installedSkillsPath ?? result.configPath ?? "";
155
+ }
156
+
157
+ function recordLedgerEntry(entry, _homes, options) {
158
+ try {
159
+ // Single global ledger: every install (local or global scope) appends to
160
+ // <home>/.enact/extensions/ledger.jsonl. The scope distinction lives in the
161
+ // entry's `scope` field, not the path. Tests pass an explicit ledgerHome so
162
+ // they never write into the real ~/.enact.
163
+ appendEntry(entry, {
164
+ home: options.ledgerHome ?? homedir(),
32
165
  });
33
- console.log(`installed ${result.name} -> ${result.installedPluginPath}`);
34
- for (const path of result.refreshedPaths) {
35
- console.log(`refreshed ${path}`);
36
- }
37
- return result;
166
+ } catch (err) {
167
+ const msg = err instanceof Error ? err.message : String(err);
168
+ process.stderr.write(`[enact-extensions install] Warning: failed to write install ledger: ${msg}\n`);
38
169
  }
170
+ }
39
171
 
40
- if (platform === "enact") {
41
- const result = installCodexPluginBundle({
42
- pluginRoot,
43
- codexHome: options.enactHome ?? defaultEnactHome(),
44
- marketplaceName: options.marketplaceName,
45
- enable: options.enable,
46
- sync: options.sync,
47
- });
48
- console.log(`installed ${result.pluginConfigKey} -> ${result.configPath}`);
49
- console.log(`wrote ${result.marketplacePath}`);
50
- for (const path of result.refreshedPaths) {
51
- console.log(`refreshed ${path}`);
172
+ // ---------------------------------------------------------------------------
173
+ // computeBundleHash best-effort; never throws out of the install path.
174
+ // Returns the sha256 hex hash of the canonical bundle, or null on failure.
175
+ // The `bundleHashRoot` option (injected by tests) overrides the default
176
+ // pluginRoot so tests can simulate a hash failure without breaking the install.
177
+ // ---------------------------------------------------------------------------
178
+ function computeBundleHash(pluginRoot, options) {
179
+ const root = options.bundleHashRoot ?? pluginRoot;
180
+ try {
181
+ return bundleHash(root);
182
+ } catch (err) {
183
+ const msg = err instanceof Error ? err.message : String(err);
184
+ process.stderr.write(`[enact-extensions install] Warning: cannot compute bundle hash: ${msg}\n`);
185
+ return null;
186
+ }
187
+ }
188
+
189
+ // Append one install entry per installed target in a single-platform result.
190
+ // Tolerant of every result shape (single platform, shared, codex multi-home).
191
+ // `hash` is the pre-computed bundle hash (or null) for this install invocation.
192
+ function recordInstall(platform, result, homes, options, hash) {
193
+ const targets = [];
194
+ if (result && Array.isArray(result.results)) {
195
+ // codex (default) multi-home: one entry per codex home.
196
+ for (const install of result.results) {
197
+ targets.push({
198
+ name: install.name,
199
+ version: install.version,
200
+ path: installedPathOf(install),
201
+ home: install.configPath ? join(install.configPath, "..") : homes.codexHome,
202
+ });
52
203
  }
204
+ } else if (result) {
205
+ targets.push({
206
+ name: result.name,
207
+ version: result.version,
208
+ path: installedPathOf(result),
209
+ home: homes[PLATFORM_TARGETS[platform]?.homeField] ?? homes.codexHome,
210
+ });
211
+ }
212
+
213
+ for (const t of targets) {
214
+ if (!t.name) continue;
215
+ recordLedgerEntry(
216
+ {
217
+ action: "install",
218
+ name: t.name,
219
+ version: t.version,
220
+ platform,
221
+ scope: homes.scope,
222
+ home: t.home,
223
+ path: t.path,
224
+ hash,
225
+ },
226
+ homes,
227
+ options,
228
+ );
229
+ }
230
+ }
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // runSinglePlatform — install for one platform; returns the install result.
234
+ // Throws on error (caller catches for partial-failure tracking).
235
+ // `hash` is the pre-computed bundle hash (or null) for this install invocation.
236
+ // ---------------------------------------------------------------------------
237
+ function runSinglePlatform(pluginRoot, platform, homes, options, hash) {
238
+ const target = PLATFORM_TARGETS[platform];
239
+ if (target) {
240
+ const home = homes[target.homeField];
241
+ const result = target.install(pluginRoot, home, options);
242
+ target.log(result);
243
+ recordInstall(platform, result, homes, options, hash);
53
244
  return result;
54
245
  }
55
246
 
56
- // codex (default) or explicit --platform codex
57
- const result = installPluginBundle({ pluginRoot, cwd: options.cwd ?? pluginRoot, ...options });
247
+ // codex (default) multi-home discovery via installPluginBundle.
248
+ const result = installPluginBundle({
249
+ pluginRoot,
250
+ cwd: options.cwd ?? pluginRoot,
251
+ ...options,
252
+ codexHome: homes.codexHome,
253
+ });
58
254
  for (const install of result.results) {
59
255
  console.log(`installed ${install.pluginConfigKey} -> ${install.configPath}`);
60
256
  console.log(`wrote ${install.marketplacePath}`);
@@ -62,5 +258,91 @@ export function runInstall(pluginRoot, options = {}) {
62
258
  console.log(`refreshed ${path}`);
63
259
  }
64
260
  }
261
+ recordInstall("codex", result, homes, options, hash);
65
262
  return result;
66
263
  }
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // MCP dependency provisioning — best-effort, platform-independent.
267
+ //
268
+ // After a successful install we provision any packages the bundle's declared
269
+ // MCP servers need (uvx → `uv tool install`, npx → `npm install -g`, etc.).
270
+ // This runs ONCE per runInstall call (provisioning is platform-independent; a
271
+ // `--platform all` install still provisions a single time). Provisioning is on
272
+ // by DEFAULT; pass `--no-provision` / { noProvision: true } to skip. A failure
273
+ // here NEVER fails the install — provisionMcp is best-effort by contract.
274
+ // ---------------------------------------------------------------------------
275
+ function runProvision(pluginRoot, options) {
276
+ try {
277
+ const provisionResults = provisionMcp(pluginRoot, {
278
+ noProvision: options.noProvision,
279
+ exec: options.provisionExec, // injectable for tests; undefined → real spawnSync
280
+ });
281
+ for (const line of summarizeProvision(provisionResults)) {
282
+ console.log(`provision: ${line}`);
283
+ }
284
+ return provisionResults;
285
+ } catch (err) {
286
+ // Defence-in-depth: provisionMcp is already best-effort, but never let a
287
+ // provisioning bug fail an otherwise-successful install.
288
+ const msg = err instanceof Error ? err.message : String(err);
289
+ process.stderr.write(`[enact-extensions install] Warning: MCP provisioning errored: ${msg}\n`);
290
+ return [];
291
+ }
292
+ }
293
+
294
+ export function runInstall(pluginRoot, options = {}) {
295
+ const platforms = parsePlatforms(options.platform);
296
+ const homes = resolveHomes(options);
297
+ console.log(`install scope: ${homes.scope}${homes.scope === "local" ? ` (${process.cwd()})` : ""}`);
298
+
299
+ // Compute bundle hash ONCE per runInstall invocation (best-effort, never throws).
300
+ // Applied to every per-platform ledger entry for this bundle.
301
+ const hash = computeBundleHash(pluginRoot, options);
302
+
303
+ // Single-platform path — unchanged behaviour (no .platforms wrapper).
304
+ if (platforms.length === 1) {
305
+ const result = runSinglePlatform(pluginRoot, platforms[0], homes, options, hash);
306
+ // Provision once after a successful install.
307
+ result.provision = runProvision(pluginRoot, options);
308
+ return result;
309
+ }
310
+
311
+ // Multi-platform path — run each sequentially, collect results + errors.
312
+ const platformResults = [];
313
+ let hasError = false;
314
+
315
+ for (const platform of platforms) {
316
+ console.log(`\n[${platform}] installing...`);
317
+ try {
318
+ const result = runSinglePlatform(pluginRoot, platform, homes, options, hash);
319
+ console.log(`[${platform}] ✓ done`);
320
+ platformResults.push({ platform, result, error: null });
321
+ } catch (err) {
322
+ const msg = err instanceof Error ? err.message : String(err);
323
+ console.error(`[${platform}] ✗ failed: ${msg}`);
324
+ platformResults.push({ platform, result: null, error: err });
325
+ hasError = true;
326
+ }
327
+ }
328
+
329
+ // Provision ONCE for the whole multi-platform run (platform-independent), as
330
+ // long as at least one platform installed successfully.
331
+ let provision = [];
332
+ if (platformResults.some((r) => !r.error)) {
333
+ provision = runProvision(pluginRoot, options);
334
+ }
335
+
336
+ if (hasError) {
337
+ const failed = platformResults.filter((r) => r.error).map((r) => r.platform);
338
+ const succeeded = platformResults.filter((r) => !r.error).map((r) => r.platform);
339
+ const summary = `Multi-platform install: ${succeeded.length} succeeded (${succeeded.join(", ")}), ${failed.length} failed (${failed.join(", ")})`;
340
+ console.error(`\n${summary}`);
341
+ const err = new Error(summary);
342
+ err.platforms = platformResults;
343
+ err.provision = provision;
344
+ throw err;
345
+ }
346
+
347
+ return { platforms: platformResults, provision };
348
+ }
@@ -0,0 +1,220 @@
1
+ import { join } from "node:path";
2
+ import { homedir } from "node:os";
3
+ import {
4
+ uninstallCodexPluginBundle,
5
+ uninstallClaudePluginBundle,
6
+ uninstallCursorPluginBundle,
7
+ uninstallSharedPluginBundle,
8
+ defaultEnactHome,
9
+ defaultSharedHome,
10
+ } from "../../dist/index.js";
11
+ import { appendEntry } from "./ledger.mjs";
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // resolveHomes — mirrors run-install.mjs; same scope/per-platform-home logic.
15
+ // ---------------------------------------------------------------------------
16
+ function resolveHomes(options) {
17
+ const scope = options.scope ?? "global";
18
+ const local = (sub) => join(process.cwd(), sub);
19
+ return {
20
+ scope,
21
+ codexHome: options.codexHome ?? (scope === "local" ? local(".codex") : undefined),
22
+ enactHome: options.enactHome ?? (scope === "local" ? local(".enact") : defaultEnactHome()),
23
+ claudeHome: options.claudeHome ?? (scope === "local" ? local(".claude") : undefined),
24
+ cursorHome: options.cursorHome ?? (scope === "local" ? local(".cursor") : undefined),
25
+ sharedHome: options.sharedHome ?? (scope === "local" ? process.cwd() : defaultSharedHome()),
26
+ };
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // ALL_PLATFORMS / parsePlatforms — reuse same logic as run-install.mjs
31
+ // ---------------------------------------------------------------------------
32
+ const ALL_PLATFORMS = ["codex", "claude", "cursor", "enact", "shared"];
33
+
34
+ /**
35
+ * Expand "all" and split comma-separated platform lists.
36
+ * Returns a deduped array of validated platform name strings.
37
+ * Throws on unknown platform tokens.
38
+ */
39
+ export function parsePlatforms(platform) {
40
+ if (!platform || platform === "codex") return ["codex"];
41
+ if (platform === "all") return ALL_PLATFORMS;
42
+
43
+ const parts = [...new Set(platform.split(",").map((p) => p.trim()).filter(Boolean))];
44
+ if (parts.length === 0) return ["codex"];
45
+
46
+ const valid = new Set(["codex", "claude", "cursor", "enact", "shared"]);
47
+ for (const part of parts) {
48
+ if (!valid.has(part)) {
49
+ throw new Error(
50
+ `Unknown platform: "${part}". Valid: codex, claude, cursor, enact, shared, all`,
51
+ );
52
+ }
53
+ }
54
+ return parts;
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Ledger wiring — best-effort; a write failure NEVER aborts the uninstall.
59
+ // ---------------------------------------------------------------------------
60
+ function recordLedgerEntry(entry, options) {
61
+ try {
62
+ appendEntry(entry, {
63
+ home: options.ledgerHome ?? homedir(),
64
+ });
65
+ } catch (err) {
66
+ const msg = err instanceof Error ? err.message : String(err);
67
+ process.stderr.write(
68
+ `[enact-extensions uninstall] Warning: failed to write uninstall ledger: ${msg}\n`,
69
+ );
70
+ }
71
+ }
72
+
73
+ function recordUninstall(name, version, platform, homes, options, result) {
74
+ if (!name) return;
75
+ // Determine the path field from the uninstall result
76
+ const path = (result && result.removedPaths && result.removedPaths[0]) ?? "";
77
+ recordLedgerEntry(
78
+ {
79
+ action: "uninstall",
80
+ name,
81
+ version: version ?? "0.0.0",
82
+ platform,
83
+ scope: homes.scope,
84
+ home: homes[platformHomeField(platform)] ?? homes.enactHome,
85
+ path,
86
+ },
87
+ options,
88
+ );
89
+ }
90
+
91
+ function platformHomeField(platform) {
92
+ switch (platform) {
93
+ case "enact": return "enactHome";
94
+ case "codex": return "codexHome";
95
+ case "claude": return "claudeHome";
96
+ case "cursor": return "cursorHome";
97
+ case "shared": return "sharedHome";
98
+ default: return "enactHome";
99
+ }
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Per-platform uninstall dispatch
104
+ // ---------------------------------------------------------------------------
105
+ /**
106
+ * Uninstall `name` from a single platform. Returns the uninstall result.
107
+ * Emits a stderr warning (never throws) when the plugin is not installed.
108
+ */
109
+ function runSinglePlatform(name, platform, homes, options) {
110
+ let result;
111
+
112
+ switch (platform) {
113
+ case "enact":
114
+ result = uninstallCodexPluginBundle(name, {
115
+ codexHome: homes.enactHome,
116
+ marketplaceName: options.marketplaceName,
117
+ });
118
+ break;
119
+ case "codex":
120
+ result = uninstallCodexPluginBundle(name, {
121
+ codexHome: homes.codexHome,
122
+ marketplaceName: options.marketplaceName,
123
+ });
124
+ break;
125
+ case "claude":
126
+ result = uninstallClaudePluginBundle(name, {
127
+ claudeHome: homes.claudeHome,
128
+ marketplaceName: options.marketplaceName,
129
+ });
130
+ break;
131
+ case "cursor":
132
+ result = uninstallCursorPluginBundle(name, {
133
+ cursorHome: homes.cursorHome,
134
+ });
135
+ break;
136
+ case "shared":
137
+ result = uninstallSharedPluginBundle(name, {
138
+ sharedHome: homes.sharedHome,
139
+ });
140
+ break;
141
+ default:
142
+ throw new Error(`Unknown platform: "${platform}"`);
143
+ }
144
+
145
+ if (result.noop) {
146
+ process.stderr.write(
147
+ `[enact-extensions uninstall] Nothing to remove for ${name} on ${platform} (not installed).\n`,
148
+ );
149
+ return result;
150
+ }
151
+
152
+ for (const p of result.removedPaths) {
153
+ console.log(`removed ${p}`);
154
+ }
155
+ console.log(`uninstalled ${name} from ${platform}`);
156
+
157
+ // Record uninstall in ledger (best-effort)
158
+ recordUninstall(name, options.version, platform, homes, options, result);
159
+
160
+ return result;
161
+ }
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // runUninstall — main entry point (mirrors runInstall signature)
165
+ // ---------------------------------------------------------------------------
166
+ /**
167
+ * Uninstall `nameOrPath` from the given platform(s).
168
+ *
169
+ * `nameOrPath` may be:
170
+ * - a plugin name (string), used directly
171
+ * - a path to a bundle root (resolveBundlePath should have been called by the CLI)
172
+ *
173
+ * The CLI passes the resolved name. Library callers may pass the name directly.
174
+ *
175
+ * @param {string} name - Plugin name (not a bundle path).
176
+ * @param {object} options - Same option bag as runInstall, plus optional `version`.
177
+ */
178
+ export function runUninstall(name, options = {}) {
179
+ const platforms = parsePlatforms(options.platform);
180
+ const homes = resolveHomes(options);
181
+
182
+ console.log(
183
+ `uninstall scope: ${homes.scope}${homes.scope === "local" ? ` (${process.cwd()})` : ""}`,
184
+ );
185
+
186
+ // Single-platform path
187
+ if (platforms.length === 1) {
188
+ return runSinglePlatform(name, platforms[0], homes, options);
189
+ }
190
+
191
+ // Multi-platform path — run each sequentially, collect results + errors.
192
+ const platformResults = [];
193
+ let hasError = false;
194
+
195
+ for (const platform of platforms) {
196
+ console.log(`\n[${platform}] uninstalling...`);
197
+ try {
198
+ const result = runSinglePlatform(name, platform, homes, options);
199
+ console.log(`[${platform}] ✓ done`);
200
+ platformResults.push({ platform, result, error: null });
201
+ } catch (err) {
202
+ const msg = err instanceof Error ? err.message : String(err);
203
+ console.error(`[${platform}] ✗ failed: ${msg}`);
204
+ platformResults.push({ platform, result: null, error: err });
205
+ hasError = true;
206
+ }
207
+ }
208
+
209
+ if (hasError) {
210
+ const failed = platformResults.filter((r) => r.error).map((r) => r.platform);
211
+ const succeeded = platformResults.filter((r) => !r.error).map((r) => r.platform);
212
+ const summary = `Multi-platform uninstall: ${succeeded.length} succeeded (${succeeded.join(", ")}), ${failed.length} failed (${failed.join(", ")})`;
213
+ console.error(`\n${summary}`);
214
+ const err = new Error(summary);
215
+ err.platforms = platformResults;
216
+ throw err;
217
+ }
218
+
219
+ return { platforms: platformResults };
220
+ }