docks-kit 0.17.0 → 0.17.2

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 (84) hide show
  1. package/AGENTS.md +37 -33
  2. package/README.md +1 -0
  3. package/cli/docs/omp-models.md +114 -58
  4. package/cli/src/argv.ts +38 -469
  5. package/cli/src/argvSurface.ts +371 -0
  6. package/cli/src/argvValidate.ts +122 -0
  7. package/cli/src/commands/docs.ts +57 -42
  8. package/cli/src/commands/harnesses.ts +38 -45
  9. package/cli/src/commands/model.ts +48 -53
  10. package/cli/src/commands/models.ts +24 -26
  11. package/cli/src/commands/omp.ts +178 -88
  12. package/cli/src/commands/plugins.ts +20 -20
  13. package/cli/src/commands/skills.ts +22 -20
  14. package/cli/src/commands/status.ts +87 -80
  15. package/cli/src/commands/sync.ts +113 -87
  16. package/cli/src/commands/toolchain.ts +21 -21
  17. package/cli/src/commands/update.ts +139 -117
  18. package/cli/src/efforts.ts +35 -34
  19. package/cli/src/engine-native/DESIGN.md +30 -30
  20. package/cli/src/engine-native/bun.ts +77 -55
  21. package/cli/src/engine-native/claudeLsp.ts +154 -0
  22. package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
  23. package/cli/src/engine-native/claudePluginPasses.ts +348 -0
  24. package/cli/src/engine-native/claudeRemovals.ts +195 -0
  25. package/cli/src/engine-native/claudeRetired.ts +4 -4
  26. package/cli/src/engine-native/claudeRuntime.ts +94 -68
  27. package/cli/src/engine-native/claudeSettings.ts +174 -0
  28. package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
  29. package/cli/src/engine-native/claudeSync.ts +176 -456
  30. package/cli/src/engine-native/codexConfig.ts +158 -0
  31. package/cli/src/engine-native/codexHooks.ts +91 -0
  32. package/cli/src/engine-native/codexPlugins.ts +290 -0
  33. package/cli/src/engine-native/codexStatus.ts +24 -0
  34. package/cli/src/engine-native/codexSync.ts +110 -568
  35. package/cli/src/engine-native/codexToml.ts +140 -138
  36. package/cli/src/engine-native/deps.ts +144 -107
  37. package/cli/src/engine-native/engineCtx.ts +126 -0
  38. package/cli/src/engine-native/exec.ts +98 -87
  39. package/cli/src/engine-native/failures.ts +3 -3
  40. package/cli/src/engine-native/harnesses.ts +62 -72
  41. package/cli/src/engine-native/index.ts +34 -315
  42. package/cli/src/engine-native/jq.ts +25 -24
  43. package/cli/src/engine-native/logger.ts +98 -100
  44. package/cli/src/engine-native/models.ts +50 -45
  45. package/cli/src/engine-native/modes.ts +83 -80
  46. package/cli/src/engine-native/ompFileDeploy.ts +111 -0
  47. package/cli/src/engine-native/ompMarketplace.ts +136 -0
  48. package/cli/src/engine-native/ompOverlay.ts +93 -53
  49. package/cli/src/engine-native/ompPaths.ts +49 -38
  50. package/cli/src/engine-native/ompPlugins.ts +170 -0
  51. package/cli/src/engine-native/ompSync.ts +64 -408
  52. package/cli/src/engine-native/ompYaml.ts +43 -41
  53. package/cli/src/engine-native/os/darwin.ts +5 -5
  54. package/cli/src/engine-native/os/index.ts +15 -15
  55. package/cli/src/engine-native/os/linux.ts +5 -5
  56. package/cli/src/engine-native/os/posix.ts +28 -24
  57. package/cli/src/engine-native/os/targets.ts +17 -17
  58. package/cli/src/engine-native/os/types.ts +31 -31
  59. package/cli/src/engine-native/os/windows.ts +61 -61
  60. package/cli/src/engine-native/parseArgs.ts +122 -372
  61. package/cli/src/engine-native/parseHelp.ts +66 -0
  62. package/cli/src/engine-native/parseModifiers.ts +222 -0
  63. package/cli/src/engine-native/services.ts +34 -34
  64. package/cli/src/engine-native/settings.ts +15 -15
  65. package/cli/src/engine-native/sharedTypes.d.ts +46 -0
  66. package/cli/src/engine-native/skillsInstall.ts +105 -0
  67. package/cli/src/engine-native/skillsLinks.ts +203 -0
  68. package/cli/src/engine-native/skillsManifest.ts +16 -0
  69. package/cli/src/engine-native/skillsPrune.ts +108 -0
  70. package/cli/src/engine-native/skillsSync.ts +28 -362
  71. package/cli/src/engine-native/syncConcurrency.ts +55 -0
  72. package/cli/src/engine-native/syncDispatch.ts +141 -0
  73. package/cli/src/engine-native/toolchain.ts +65 -62
  74. package/cli/src/engine.ts +65 -57
  75. package/cli/src/generated/sotPayload.ts +7 -7
  76. package/cli/src/kitHome.ts +44 -40
  77. package/cli/src/main.ts +63 -56
  78. package/cli/src/manifests.ts +53 -54
  79. package/cli/src/md.d.ts +2 -2
  80. package/cli/src/payload.ts +9 -9
  81. package/cli/src/services.ts +21 -13
  82. package/cli/tsconfig.json +4 -1
  83. package/package.json +9 -2
  84. package/cli/src/engine-native/claudePlugins.ts +0 -510
@@ -1,98 +1,120 @@
1
- import { mkdtempSync, rmSync } from "node:fs"
2
- import { tmpdir } from "node:os"
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
3
 
4
- import { p, spawnProcess, type AsyncProcessOptions, type AsyncProcessResult } from "./exec"
5
- import type { Ctx } from "./index"
6
- import type { EngineServices } from "./services"
7
- import { hostOs, type HostOs } from "./os"
8
- import { field } from "./toolchain"
4
+ import { p, spawnProcess, type AsyncProcessOptions, type AsyncProcessResult } from "./exec";
5
+ import type { Ctx } from "./index";
6
+ import type { EngineServices } from "./services";
7
+ import { hostOs, type HostOs } from "./os";
8
+ import { field } from "./toolchain";
9
9
 
10
10
  export type BunRuntimeState =
11
11
  | { readonly kind: "ready"; readonly executable: string }
12
12
  | {
13
- readonly kind: "deferred"
14
- readonly reason: "missing-curl" | "download-failed" | "installer-failed" | "install-failed"
15
- }
13
+ readonly kind: "deferred";
14
+ readonly reason: "missing-curl" | "download-failed" | "installer-failed" | "install-failed";
15
+ };
16
16
 
17
17
  function predictedExecutable(ctx: Ctx, host: HostOs = hostOs()): string {
18
- const root = process.env["BUN_INSTALL"] !== undefined && process.env["BUN_INSTALL"] !== ""
19
- ? process.env["BUN_INSTALL"]!
20
- : p(ctx.home, ".bun")
21
- return p(root, "bin", host.bunExecutableName)
18
+ const root =
19
+ process.env["BUN_INSTALL"] !== undefined && process.env["BUN_INSTALL"] !== ""
20
+ ? process.env["BUN_INSTALL"]!
21
+ : p(ctx.home, ".bun");
22
+ return p(root, "bin", host.bunExecutableName);
22
23
  }
23
24
 
24
25
  type BunInstallResult =
25
26
  | { readonly ok: true }
26
- | { readonly ok: false; readonly reason: "download-failed" | "installer-failed"; readonly detail: string }
27
+ | {
28
+ readonly ok: false;
29
+ readonly reason: "download-failed" | "installer-failed";
30
+ readonly detail: string;
31
+ };
27
32
 
28
33
  function processFailure(result: AsyncProcessResult): string {
29
- const details = [result.error?.message, result.stderr.trim()].filter((value): value is string => value !== undefined && value !== "")
30
- return details.join(": ") || (result.exitCode === null ? "the process ended without an exit code" : `exit code ${result.exitCode}`)
34
+ const details = [result.error?.message, result.stderr.trim()].filter(
35
+ (value): value is string => value !== undefined && value !== "",
36
+ );
37
+ return (
38
+ details.join(": ") ||
39
+ (result.exitCode === null
40
+ ? "the process ended without an exit code"
41
+ : `exit code ${result.exitCode}`)
42
+ );
31
43
  }
32
44
 
33
- async function installBun(pin: string, directory: string, host: HostOs = hostOs()): Promise<BunInstallResult> {
34
- const options: AsyncProcessOptions = { stdio: ["ignore", "ignore", "pipe"] }
35
- const installer = host.bunInstaller(pin, directory)
36
- const download = await spawnProcess(installer.download.command, installer.download.args, options)
45
+ async function installBun(
46
+ pin: string,
47
+ directory: string,
48
+ host: HostOs = hostOs(),
49
+ ): Promise<BunInstallResult> {
50
+ const options: AsyncProcessOptions = { stdio: ["ignore", "ignore", "pipe"] };
51
+ const installer = host.bunInstaller(pin, directory);
52
+ const download = await spawnProcess(installer.download.command, installer.download.args, options);
37
53
  if (download.error !== undefined || download.exitCode !== 0) {
38
- return { ok: false, reason: "download-failed", detail: processFailure(download) }
54
+ return { ok: false, reason: "download-failed", detail: processFailure(download) };
39
55
  }
40
- const install = await spawnProcess(installer.run.command, installer.run.args, options)
56
+ const install = await spawnProcess(installer.run.command, installer.run.args, options);
41
57
  if (install.error !== undefined || install.exitCode !== 0) {
42
- return { ok: false, reason: "installer-failed", detail: processFailure(install) }
58
+ return { ok: false, reason: "installer-failed", detail: processFailure(install) };
43
59
  }
44
- return { ok: true }
60
+ return { ok: true };
45
61
  }
46
62
 
47
63
  export function bunBootstrap(ctx: Ctx, services: EngineServices): Promise<BunRuntimeState> {
48
- if (ctx.bunRuntime !== undefined) return ctx.bunRuntime
49
- const pending = runBunBootstrap(ctx, services)
50
- ctx.bunRuntime = pending
51
- return pending
64
+ if (ctx.bunRuntime !== undefined) return ctx.bunRuntime;
65
+ const pending = runBunBootstrap(ctx, services);
66
+ ctx.bunRuntime = pending;
67
+ return pending;
52
68
  }
53
69
 
54
70
  async function runBunBootstrap(ctx: Ctx, services: EngineServices): Promise<BunRuntimeState> {
55
- const host = hostOs(services.platform.name())
71
+ const host = hostOs(services.platform.name());
56
72
 
57
- const existing = await services.deps.path("bun")
58
- if (existing !== "") return { kind: "ready", executable: existing }
73
+ const existing = await services.deps.path("bun");
74
+ if (existing !== "") return { kind: "ready", executable: existing };
59
75
 
60
- const pin = field("bun", "verified")
76
+ const pin = field("bun", "verified");
61
77
  if (pin === "") {
62
- services.logger.warn("Bun bootstrap aborted — SoT/toolchain.json has no verified Bun pin")
63
- return { kind: "deferred", reason: "install-failed" }
78
+ services.logger.warn("Bun bootstrap aborted — SoT/toolchain.json has no verified Bun pin");
79
+ return { kind: "deferred", reason: "install-failed" };
64
80
  }
65
81
  if (services.deps.probe("curl").state === "missing") {
66
- services.deps.warnMissing("curl", services.logger, "cannot bootstrap Bun; install Bun manually, then re-run sync")
67
- return { kind: "deferred", reason: "missing-curl" }
82
+ services.deps.warnMissing(
83
+ "curl",
84
+ services.logger,
85
+ "cannot bootstrap Bun; install Bun manually, then re-run sync",
86
+ );
87
+ return { kind: "deferred", reason: "missing-curl" };
68
88
  }
69
89
  if (ctx.dryRun) {
70
- const executable = predictedExecutable(ctx, host)
71
- services.logger.echo(`[dry-run] install Bun ${pin} (kit-verified) -> ${executable}`)
72
- return { kind: "ready", executable }
90
+ const executable = predictedExecutable(ctx, host);
91
+ services.logger.echo(`[dry-run] install Bun ${pin} (kit-verified) -> ${executable}`);
92
+ return { kind: "ready", executable };
73
93
  }
74
- services.logger.warn(`Bun not found — installing Bun ${pin} (kit-verified)...`)
75
- const temporaryDir = mkdtempSync(p(tmpdir(), "docks-kit-bun-"))
76
- let result: BunInstallResult
94
+ services.logger.warn(`Bun not found — installing Bun ${pin} (kit-verified)...`);
95
+ const temporaryDir = mkdtempSync(p(tmpdir(), "docks-kit-bun-"));
96
+ let result: BunInstallResult;
77
97
  try {
78
- result = await installBun(pin, temporaryDir, host)
98
+ result = await installBun(pin, temporaryDir, host);
79
99
  } finally {
80
- rmSync(temporaryDir, { recursive: true, force: true })
100
+ rmSync(temporaryDir, { recursive: true, force: true });
81
101
  }
82
102
  if (!result.ok) {
83
- const stage = result.reason === "download-failed" ? "installer download" : "installer"
103
+ const stage = result.reason === "download-failed" ? "installer download" : "installer";
84
104
  services.logger.warn(
85
- `Bun ${stage} failed (${result.detail}). Install Bun manually from https://bun.sh/docs/installation, then re-run sync.`
86
- )
87
- return { kind: "deferred", reason: result.reason }
105
+ `Bun ${stage} failed (${result.detail}). Install Bun manually from https://bun.sh/docs/installation, then re-run sync.`,
106
+ );
107
+ return { kind: "deferred", reason: result.reason };
88
108
  }
89
109
 
90
- const installed = await services.deps.path("bun")
110
+ const installed = await services.deps.path("bun");
91
111
  if (installed === "") {
92
- services.logger.warn("Bun install failed. Install manually from https://bun.sh/docs/installation, then re-run sync.")
93
- return { kind: "deferred", reason: "install-failed" }
112
+ services.logger.warn(
113
+ "Bun install failed. Install manually from https://bun.sh/docs/installation, then re-run sync.",
114
+ );
115
+ return { kind: "deferred", reason: "install-failed" };
94
116
  }
95
- const version = await services.deps.version("bun")
96
- services.logger.change(`Bun installed (${version !== "" ? version : "version unknown"})`)
97
- return { kind: "ready", executable: installed }
117
+ const version = await services.deps.version("bun");
118
+ services.logger.change(`Bun installed (${version !== "" ? version : "version unknown"})`);
119
+ return { kind: "ready", executable: installed };
98
120
  }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * EngineNative `sync claude` LSP server probes and installs. The php-lsp,
3
+ * typescript-lsp, and rust-analyzer-lsp plugins are no-ops without their
4
+ * server binaries, so this pass installs the missing ones. Message strings
5
+ * and spawned argv are part of the contract.
6
+ */
7
+ import { spawnProcess } from "./exec";
8
+ import type { Ctx } from "./index";
9
+ import { isObject, parseJson } from "./jq";
10
+ import { belowFloor, field, installedVersion } from "./toolchain";
11
+ import { payloadText } from "../payload";
12
+
13
+ function lspPkg(ctx: Ctx, tool: string, pkg: string): string | undefined {
14
+ const version = field(tool, "verified");
15
+ if (version !== "") return `${pkg}@${version}`;
16
+ ctx.services.logger.warn(
17
+ `Skipping ${pkg} install: ${tool} has no verified version in SoT/toolchain.json`,
18
+ );
19
+ return undefined;
20
+ }
21
+
22
+ /**
23
+ * npm treats a package's `engines` field as advisory unless the host opted
24
+ * into engine-strict, so installing typescript-language-server 6 on an older
25
+ * Node succeeds and then fails at every startup. Report the host Node version
26
+ * when it is too old, and the empty string when the install is safe.
27
+ */
28
+ async function nodeBelowServerFloor(ctx: Ctx): Promise<string> {
29
+ const floor = field("node", "floor");
30
+ if (floor === "") return "";
31
+ const installed = await installedVersion(ctx, "node");
32
+ return belowFloor(installed, floor) ? installed : "";
33
+ }
34
+
35
+ /** npm-global channel: intelephense, typescript-language-server, typescript. */
36
+ async function installNpmServers(ctx: Ctx, missing: ReadonlyArray<string>): Promise<void> {
37
+ const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger;
38
+ if (missing.length === 0) return;
39
+ const specs = missing.join(" ");
40
+ if (ctx.dryRun) {
41
+ echo(`[dry-run] would install: npm install -g ${specs}`);
42
+ return;
43
+ }
44
+
45
+ if (ctx.services.deps.probe("npm").state === "missing") {
46
+ ctx.services.deps.warnMissing(
47
+ "npm",
48
+ ctx.services.logger,
49
+ `cannot install LSP servers (${specs}); the php-lsp/typescript-lsp plugins stay no-ops`,
50
+ );
51
+ return;
52
+ }
53
+
54
+ verbose(`Installing LSP servers via npm: ${specs}...`);
55
+ progress(`Installing LSP servers via npm: ${specs}...`);
56
+ const installResult = await spawnProcess("npm", ["install", "-g", ...missing], {
57
+ stdio: "ignore",
58
+ });
59
+ clearProgress();
60
+ if (installResult.exitCode === 0) {
61
+ change(`LSP servers installed (${specs})`);
62
+ ctx.nextStepTriggers.claudeRestart = true;
63
+ } else {
64
+ warn(`npm install -g ${specs} failed. Try manually: npm install -g ${specs}`);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * rustup channel: rust-analyzer ships no npm package, and upstream's first
70
+ * recommendation is the rustup component, which tracks the toolchain the host
71
+ * already trusts. The component version therefore follows that toolchain, so
72
+ * the manifest row carries no verified pin - the stance bubblewrap already
73
+ * takes for a distro package.
74
+ */
75
+ async function installRustAnalyzer(ctx: Ctx): Promise<void> {
76
+ const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger;
77
+ if (ctx.dryRun) {
78
+ echo("[dry-run] would install: rustup component add rust-analyzer");
79
+ return;
80
+ }
81
+ verbose("Installing rust-analyzer via rustup...");
82
+ progress("Installing rust-analyzer via rustup...");
83
+ const result = await spawnProcess("rustup", ["component", "add", "rust-analyzer"], {
84
+ stdio: "ignore",
85
+ });
86
+ clearProgress();
87
+ if (result.exitCode === 0) {
88
+ change("LSP server installed (rust-analyzer via rustup)");
89
+ ctx.nextStepTriggers.claudeRestart = true;
90
+ } else {
91
+ warn(
92
+ "rustup component add rust-analyzer failed. Try manually: rustup component add rust-analyzer",
93
+ );
94
+ }
95
+ }
96
+
97
+ export async function syncLspServers(ctx: Ctx): Promise<void> {
98
+ const { echo, verbose, warn } = ctx.services.logger;
99
+ const sot = parseJson(payloadText("SoT/.claude/settings.json"));
100
+ const enabled =
101
+ sot !== undefined && isObject(sot) && isObject(sot["enabledPlugins"])
102
+ ? sot["enabledPlugins"]
103
+ : undefined;
104
+ if (enabled === undefined) return;
105
+ const enables = (plugin: string): boolean =>
106
+ Object.prototype.hasOwnProperty.call(enabled, plugin);
107
+ const hasPhp = enables("php-lsp@claude-plugins-official");
108
+ const hasTs = enables("typescript-lsp@claude-plugins-official");
109
+ const hasRust = enables("rust-analyzer-lsp@claude-plugins-official");
110
+ if (!hasPhp && !hasTs && !hasRust) return;
111
+
112
+ const phpMissing = hasPhp && ctx.services.deps.probe("intelephense").state === "missing";
113
+ const tsServerMissing =
114
+ hasTs && ctx.services.deps.probe("typescript-language-server").state === "missing";
115
+ const tscMissing = hasTs && ctx.services.deps.probe("tsc").state === "missing";
116
+ const rustMissing = hasRust && ctx.services.deps.probe("rust-analyzer").state === "missing";
117
+ // A host without rustup does no Rust work, so the plugin has nothing to
118
+ // serve there. Installing is impossible and warning every sync would name no
119
+ // action the user wants, so that host is not counted as missing a tool.
120
+ const rustInstallable = rustMissing && ctx.services.deps.probe("rustup").state === "present";
121
+ if (rustMissing && !rustInstallable) {
122
+ verbose(
123
+ "Skipping rust-analyzer: rustup is not installed, so the rust-analyzer-lsp plugin stays a no-op",
124
+ );
125
+ }
126
+
127
+ const missingToolCount =
128
+ Number(phpMissing) + Number(tsServerMissing) + Number(tscMissing) + Number(rustInstallable);
129
+ if (missingToolCount === 0) {
130
+ if (ctx.dryRun) {
131
+ echo("[dry-run] LSP server binaries present");
132
+ } else {
133
+ verbose("LSP server binaries present");
134
+ }
135
+ return;
136
+ }
137
+
138
+ const blockingNode = tsServerMissing ? await nodeBelowServerFloor(ctx) : "";
139
+ if (blockingNode !== "") {
140
+ warn(
141
+ `Skipping typescript-language-server install: Node ${blockingNode} is older than the ${field("node", "floor")} that version requires. Upgrade Node, then re-run sync.`,
142
+ );
143
+ }
144
+ const missing = [
145
+ phpMissing ? lspPkg(ctx, "intelephense", "intelephense") : undefined,
146
+ tsServerMissing && blockingNode === ""
147
+ ? lspPkg(ctx, "typescript-language-server", "typescript-language-server")
148
+ : undefined,
149
+ tscMissing ? lspPkg(ctx, "tsc", "typescript") : undefined,
150
+ ].filter((spec): spec is string => spec !== undefined);
151
+
152
+ await installNpmServers(ctx, missing);
153
+ if (rustInstallable) await installRustAnalyzer(ctx);
154
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * EngineNative `sync claude` optional-plugin opt-in pass (--claude-plugin).
3
+ * Split from claudePluginPasses along the opt-in axis; message strings
4
+ * and spawned argv are part of the golden-tested contract.
5
+ */
6
+ import { p } from "./exec";
7
+ import { recordFailure } from "./failures";
8
+ import type { Ctx } from "./index";
9
+ import { isObject, readJsonFile } from "./jq";
10
+ import type { JsonObject } from "./sharedTypes";
11
+ import { cli, pluginUserScopeInstalled } from "./claudePluginPasses";
12
+
13
+ async function enableOptionalPlugin(
14
+ ctx: Ctx,
15
+ claudeDir: string,
16
+ pluginId: string,
17
+ marketplaceRepo: string,
18
+ ): Promise<boolean> {
19
+ const { change, clearProgress, progress, verbose } = ctx.services.logger;
20
+ const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json");
21
+ const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json");
22
+ const mpName = pluginId.slice(pluginId.lastIndexOf("@") + 1);
23
+
24
+ let marketplaceAdded = false;
25
+ if (marketplaceRepo !== "") {
26
+ const known = readJsonFile(knownMarketplaces);
27
+ const has =
28
+ known !== undefined &&
29
+ isObject(known) &&
30
+ known[mpName] !== undefined &&
31
+ known[mpName] !== null &&
32
+ known[mpName] !== false;
33
+ if (!has) {
34
+ if (!(await cli(["plugin", "marketplace", "add", marketplaceRepo])).ok) {
35
+ recordFailure(ctx, `Failed to add marketplace ${marketplaceRepo} for ${pluginId}`);
36
+ return false;
37
+ }
38
+ marketplaceAdded = true;
39
+ }
40
+ }
41
+
42
+ const wasInstalled = pluginUserScopeInstalled(installedPlugins, pluginId);
43
+ if (!wasInstalled) {
44
+ progress(`Installing plugin ${pluginId}...`);
45
+ const installResult = await cli(["plugin", "install", pluginId]);
46
+ clearProgress();
47
+ if (!installResult.ok) {
48
+ if (marketplaceAdded)
49
+ change(
50
+ `Optional plugin ${pluginId}: marketplace added (install failed — will retry next sync)`,
51
+ );
52
+ recordFailure(ctx, `Failed to install optional plugin ${pluginId}`);
53
+ return marketplaceAdded;
54
+ }
55
+ }
56
+
57
+ const settingsDoc = readJsonFile(p(claudeDir, "settings.json"));
58
+ const wasEnabled =
59
+ settingsDoc !== undefined && isObject(settingsDoc) && isObject(settingsDoc["enabledPlugins"])
60
+ ? (settingsDoc["enabledPlugins"] as JsonObject)[pluginId] === true
61
+ : false;
62
+
63
+ if (!(await cli(["plugin", "enable", pluginId])).ok) {
64
+ if (marketplaceAdded || !wasInstalled)
65
+ change(`Optional plugin ${pluginId}: installed (enable failed — will retry next sync)`);
66
+ recordFailure(ctx, `Failed to enable optional plugin ${pluginId}`);
67
+ return marketplaceAdded || !wasInstalled;
68
+ }
69
+ const changed = marketplaceAdded || !wasInstalled || !wasEnabled;
70
+ if (changed) change(`Optional plugin opted in: ${pluginId}`);
71
+ else verbose(`Optional plugin already opted in: ${pluginId} (enable re-asserted)`);
72
+ return changed;
73
+ }
74
+
75
+ export async function syncOptionalPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
76
+ const { echo, warn } = ctx.services.logger;
77
+ if (ctx.claudePlugins.length === 0) return;
78
+
79
+ if (ctx.dryRun) {
80
+ if (ctx.claudePlugins.includes("supabase")) {
81
+ echo(
82
+ "[dry-run] (--claude-plugin=supabase) install + enable supabase@claude-plugins-official in deployed settings",
83
+ );
84
+ }
85
+ if (ctx.claudePlugins.includes("n8n")) {
86
+ echo(
87
+ "[dry-run] (--claude-plugin=n8n) add czlonkowski/n8n-skills marketplace + install + enable n8n-mcp-skills@n8n-mcp-skills",
88
+ );
89
+ }
90
+ return;
91
+ }
92
+
93
+ if (ctx.services.deps.probe("claude").state === "missing") {
94
+ warn("claude CLI not in PATH — cannot opt in optional plugins (--claude-plugin)");
95
+ return;
96
+ }
97
+
98
+ if (ctx.claudePlugins.includes("supabase")) {
99
+ if (await enableOptionalPlugin(ctx, claudeDir, "supabase@claude-plugins-official", ""))
100
+ ctx.nextStepTriggers.claudePlugins = true;
101
+ }
102
+ if (ctx.claudePlugins.includes("n8n")) {
103
+ if (
104
+ await enableOptionalPlugin(
105
+ ctx,
106
+ claudeDir,
107
+ "n8n-mcp-skills@n8n-mcp-skills",
108
+ "czlonkowski/n8n-skills",
109
+ )
110
+ )
111
+ ctx.nextStepTriggers.claudePlugins = true;
112
+ }
113
+ }