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,510 +0,0 @@
1
- /**
2
- * EngineNative `sync claude` plugin, optional-plugin, and LSP passes. Pass
3
- * order inside `syncPlugins` is load-bearing and golden-tested; message
4
- * strings and spawned argv are part of the contract.
5
- */
6
- import { existsSync, renameSync, writeFileSync } from "node:fs"
7
- import { p, spawnProcess } from "./exec"
8
- import { recordFailure } from "./failures"
9
- import type { Ctx } from "./index"
10
- import { compareCodepoints, deepMerge, isObject, jqStringify, parseJson, readJsonFile, type Json } from "./jq"
11
- import { belowFloor, field, installedVersion } from "./toolchain"
12
- import { payloadText } from "../payload"
13
-
14
- async function cli(args: Array<string>): Promise<{ ok: boolean; out: string; detail: string }> {
15
- const res = await spawnProcess("claude", args, { stdio: ["ignore", "pipe", "pipe"] })
16
- // A spawn error carries its cause in `error`, not in either stream, and that
17
- // is the case a failure message cannot afford to drop: it names an
18
- // unresolvable launcher instead of a rejected command.
19
- const out = res.error !== undefined ? res.error.message : `${res.stdout}${res.stderr}`
20
- const detail = out.split("\n").map((line) => line.trim()).find((line) => line !== "") ?? "unknown error"
21
- return { ok: res.error === undefined && res.exitCode === 0, out, detail }
22
- }
23
-
24
- /** Failure message tail: the cause, then the command to re-run by hand. */
25
- function manually(detail: string, args: Array<string>): string {
26
- return `${detail}; run manually: claude ${args.join(" ")}`
27
- }
28
-
29
- function sortedKeys(obj: Json | undefined): Array<string> {
30
- return obj !== undefined && isObject(obj) ? Object.keys(obj).sort(compareCodepoints) : []
31
- }
32
-
33
- /** claude::_plugin_user_scope_installed. */
34
- export function pluginUserScopeInstalled(installedPlugins: string, pluginId: string): boolean {
35
- const doc = readJsonFile(installedPlugins)
36
- if (doc === undefined || !isObject(doc) || !isObject(doc["plugins"])) return false
37
- const rec = (doc["plugins"] as { [k: string]: Json })[pluginId]
38
- if (rec === undefined || rec === null) return false
39
- const records = Array.isArray(rec) ? rec : [rec]
40
- return records.some((r) => isObject(r) && r["scope"] === "user")
41
- }
42
-
43
- function nonUserScopeMarketplaces(installedDoc: Json | undefined): Set<string> {
44
- const marketplaces = new Set<string>()
45
- if (installedDoc === undefined || !isObject(installedDoc) || !isObject(installedDoc["plugins"])) {
46
- return marketplaces
47
- }
48
-
49
- for (const [pluginId, value] of Object.entries(installedDoc["plugins"])) {
50
- const records = Array.isArray(value) ? value : [value]
51
- const hasNonUserInstall = records.some(
52
- (record) => isObject(record) && (record["scope"] === "project" || record["scope"] === "local")
53
- )
54
- if (!hasNonUserInstall) continue
55
- const separator = pluginId.lastIndexOf("@")
56
- if (separator > 0) marketplaces.add(pluginId.slice(separator + 1))
57
- }
58
- return marketplaces
59
- }
60
-
61
- export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
62
- const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
63
- const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
64
- const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
65
-
66
- if (ctx.dryRun) {
67
- echo(
68
- ctx.skipPluginRefresh
69
- ? "[dry-run] bootstrap + install missing plugins from SoT; skip refresh-only plugin updates"
70
- : "[dry-run] bootstrap + update plugin marketplaces + plugins from SoT"
71
- )
72
- if (ctx.prune) {
73
- echo("[dry-run] (--prune) would also uninstall plugins not in SoT and remove extra marketplaces")
74
- }
75
- return
76
- }
77
-
78
- if (ctx.services.deps.probe("claude").state === "missing") {
79
- warn("claude CLI not in PATH — skipping plugin reconcile (run /plugin marketplace add + /plugin install manually)")
80
- return
81
- }
82
- if (ctx.services.deps.probe("git").state === "missing") {
83
- ctx.services.deps.warnMissing(
84
- "git",
85
- ctx.services.logger,
86
- "plugin marketplaces are git repos — Claude plugin passes skipped; re-run sync after installing"
87
- )
88
- return
89
- }
90
-
91
- const repoSettings = parseJson(payloadText("SoT/.claude/settings.json"))
92
- const repoObj = repoSettings !== undefined && isObject(repoSettings) ? repoSettings : {}
93
- const sotMarketplaces = isObject(repoObj["extraKnownMarketplaces"]) ? repoObj["extraKnownMarketplaces"] : {}
94
- const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {}
95
-
96
- // Pass 1 — add missing marketplaces (SoT insertion order, like to_entries).
97
- let addedMp = 0
98
- let f1 = 0
99
- for (const [mpName, mpValue] of Object.entries(sotMarketplaces)) {
100
- const known = readJsonFile(knownMarketplaces)
101
- if (known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false) continue
102
- const repo = isObject(mpValue) && isObject(mpValue["source"]) ? String((mpValue["source"] as { [k: string]: Json })["repo"] ?? "") : ""
103
- progress(`Adding marketplace ${mpName}...`)
104
- const marketplaceResult = await cli(["plugin", "marketplace", "add", repo])
105
- clearProgress()
106
- if (marketplaceResult.ok) {
107
- addedMp++
108
- } else {
109
- recordFailure(
110
- ctx,
111
- `Failed to add marketplace: ${mpName} (${repo}): ${manually(marketplaceResult.detail, ["plugin", "marketplace", "add", repo])}`
112
- )
113
- f1++
114
- }
115
- }
116
-
117
- // Pass 2 — install SoT-enabled plugins missing at user scope (jq keys[] sorts),
118
- // refreshing each source marketplace once so the install resolves a current snapshot.
119
- let addedPl = 0
120
- let f2 = 0
121
- let f3 = 0
122
- let f4 = 0
123
- const refreshedMarketplaces = new Set<string>()
124
- for (const pluginId of sortedKeys(sotPlugins)) {
125
- if (pluginUserScopeInstalled(installedPlugins, pluginId)) continue
126
- const separator = pluginId.lastIndexOf("@")
127
- const mpName = separator > 0 ? pluginId.slice(separator + 1) : ""
128
- if (mpName !== "" && !refreshedMarketplaces.has(mpName)) {
129
- progress(`Refreshing marketplace ${mpName}...`)
130
- const refreshResult = await cli(["plugin", "marketplace", "update", mpName])
131
- clearProgress()
132
- if (!refreshResult.ok) {
133
- recordFailure(
134
- ctx,
135
- `Failed to refresh marketplace: ${mpName}: ${manually(refreshResult.detail, ["plugin", "marketplace", "update", mpName])}`
136
- )
137
- f3++
138
- }
139
- refreshedMarketplaces.add(mpName)
140
- }
141
- progress(`Installing plugin ${pluginId}...`)
142
- const installResult = await cli(["plugin", "install", pluginId])
143
- clearProgress()
144
- if (installResult.ok) {
145
- addedPl++
146
- } else {
147
- recordFailure(
148
- ctx,
149
- `Failed to install plugin: ${pluginId}: ${manually(installResult.detail, ["plugin", "install", pluginId])}`
150
- )
151
- f2++
152
- }
153
- }
154
-
155
- let updatedPl = 0
156
- const installedDoc = readJsonFile(installedPlugins)
157
- const installedKeys = installedDoc !== undefined && isObject(installedDoc) ? sortedKeys(installedDoc["plugins"]) : []
158
- const nonUserMarketplaces = nonUserScopeMarketplaces(installedDoc)
159
- // Kit-owned plugin IDs: SoT-declared plus this run's --claude-plugin opt-ins.
160
- const kitPluginIds = new Set<string>(Object.keys(sotPlugins))
161
- if (ctx.claudePlugins.includes("supabase")) kitPluginIds.add("supabase@claude-plugins-official")
162
- if (ctx.claudePlugins.includes("n8n")) kitPluginIds.add("n8n-mcp-skills@n8n-mcp-skills")
163
-
164
- // Kit-owned marketplaces: SoT-declared plus every marketplace those plugins come from.
165
- const kitMarketplaces = new Set<string>(Object.keys(sotMarketplaces))
166
- for (const pluginId of kitPluginIds) {
167
- const separator = pluginId.lastIndexOf("@")
168
- if (separator > 0) kitMarketplaces.add(pluginId.slice(separator + 1))
169
- }
170
-
171
- // Pass 3 — refresh the kit-owned marketplaces unless the caller passed
172
- // --skip-plugin-refresh by hand. Pass 2 already refreshed the source
173
- // marketplace of every plugin it installed, so skip those: a second
174
- // fetch seconds later cannot resolve a newer snapshot, and re-running it
175
- // would duplicate one failure in the ledger and in the failed-operation count.
176
- if (!ctx.skipPluginRefresh) {
177
- for (const mpName of [...kitMarketplaces].sort(compareCodepoints)) {
178
- if (refreshedMarketplaces.has(mpName)) continue
179
- progress(`Refreshing marketplace ${mpName}...`)
180
- const refreshResult = await cli(["plugin", "marketplace", "update", mpName])
181
- clearProgress()
182
- refreshedMarketplaces.add(mpName)
183
- if (!refreshResult.ok) {
184
- recordFailure(
185
- ctx,
186
- `Failed to refresh marketplace: ${mpName}: ${manually(refreshResult.detail, ["plugin", "marketplace", "update", mpName])}`
187
- )
188
- f3++
189
- }
190
- }
191
- // Pass 4 — update the kit-owned installed plugins.
192
- for (const pluginId of [...kitPluginIds].sort(compareCodepoints)) {
193
- if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
194
- progress(`Updating plugin ${pluginId}...`)
195
- const updateResult = await cli(["plugin", "update", pluginId, "--scope", "user"])
196
- clearProgress()
197
- if (!updateResult.ok) {
198
- recordFailure(
199
- ctx,
200
- `Failed to update plugin: ${pluginId}: ${manually(updateResult.detail, ["plugin", "update", pluginId, "--scope", "user"])}`
201
- )
202
- f4++
203
- } else if (updateResult.out.includes("Successfully updated")) {
204
- updatedPl++
205
- }
206
- }
207
- }
208
-
209
- // Pass 5 — prune-gated user-scope plugin uninstall.
210
- let removedPl = 0
211
- let removedMp = 0
212
- let f5 = 0
213
- let f6 = 0
214
- if (ctx.prune) {
215
- for (const pluginId of installedKeys) {
216
- if (kitPluginIds.has(pluginId)) continue
217
- if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
218
- progress(`Uninstalling plugin ${pluginId}...`)
219
- const uninstallResult = await cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId])
220
- clearProgress()
221
- if (uninstallResult.ok) {
222
- removedPl++
223
- } else {
224
- recordFailure(
225
- ctx,
226
- `Failed to uninstall plugin: ${pluginId}: ${manually(uninstallResult.detail, ["plugin", "uninstall", "-y", "--scope", "user", pluginId])}`
227
- )
228
- f5++
229
- }
230
- }
231
- // Pass 6 — prune-gated marketplace removal.
232
- const known = readJsonFile(knownMarketplaces)
233
- for (const mpName of sortedKeys(known)) {
234
- if (mpName === "claude-plugins-official") continue
235
- if (nonUserMarketplaces.has(mpName)) continue
236
- if (kitMarketplaces.has(mpName)) continue
237
- progress(`Removing marketplace ${mpName}...`)
238
- const removeResult = await cli(["plugin", "marketplace", "remove", mpName])
239
- clearProgress()
240
- if (removeResult.ok) {
241
- removedMp++
242
- } else {
243
- recordFailure(
244
- ctx,
245
- `Failed to remove marketplace: ${mpName}: ${manually(removeResult.detail, ["plugin", "marketplace", "remove", mpName])}`
246
- )
247
- f6++
248
- }
249
- }
250
- }
251
-
252
- // Pass 7 — re-assert SoT enabled-state in the user settings.
253
- if (await reassertEnabledState(ctx, repoObj, p(claudeDir, "settings.json"))) {
254
- change("Plugin enable-state re-asserted from SoT in settings.json")
255
- ctx.nextStepTriggers.claudePlugins = true
256
- }
257
-
258
- const failed = f1 + f2 + f3 + f4 + f5 + f6
259
- if (addedMp > 0 || addedPl > 0 || updatedPl > 0 || removedPl > 0 || removedMp > 0) {
260
- change(`Plugins synced (marketplaces: +${addedMp} -${removedMp}, plugins: +${addedPl} ~${updatedPl} -${removedPl})`)
261
- ctx.nextStepTriggers.claudePlugins = true
262
- } else {
263
- verbose("Plugins already in sync")
264
- }
265
- if (failed > 0) {
266
- warn(`${failed} plugin operation(s) failed — re-run sync or install manually`)
267
- }
268
- }
269
-
270
- async function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSettingsFile: string): Promise<boolean> {
271
- const { warn } = ctx.services.logger
272
- if (!existsSync(userSettingsFile)) return false
273
- const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {}
274
-
275
- let cliDisabled = false
276
- for (const [pluginId, value] of Object.entries(sotPlugins)) {
277
- if (value !== false) continue
278
- const user = readJsonFile(userSettingsFile)
279
- const enabled = user !== undefined && isObject(user) && isObject(user["enabledPlugins"]) ? (user["enabledPlugins"] as { [k: string]: Json })[pluginId] : undefined
280
- if (enabled !== true) continue
281
- const disableResult = await cli(["plugin", "disable", pluginId])
282
- if (disableResult.ok) {
283
- cliDisabled = true
284
- } else {
285
- recordFailure(
286
- ctx,
287
- `Failed to disable SoT-false plugin: ${pluginId} (will retry next sync): ${manually(disableResult.detail, ["plugin", "disable", pluginId])}`
288
- )
289
- }
290
- }
291
-
292
- const user = readJsonFile(userSettingsFile)
293
- if (user === undefined || !isObject(user)) {
294
- warn("enabledPlugins re-assert failed — false-keyed plugins may be left enabled")
295
- return false
296
- }
297
- const beforeCanonical = jqStringify(user)
298
- user["enabledPlugins"] = deepMerge(isObject(user["enabledPlugins"]) ? user["enabledPlugins"] : {}, sotPlugins)
299
- const out = jqStringify(user)
300
- if (out === beforeCanonical) return cliDisabled
301
- writeFileSync(`${userSettingsFile}.tmp`, out)
302
- renameSync(`${userSettingsFile}.tmp`, userSettingsFile)
303
- return true
304
- }
305
-
306
- // ------------------------------------------------------ optional plugins ----
307
-
308
- async function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): Promise<boolean> {
309
- const { change, clearProgress, progress, verbose } = ctx.services.logger
310
- const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
311
- const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
312
- const mpName = pluginId.slice(pluginId.lastIndexOf("@") + 1)
313
-
314
- let marketplaceAdded = false
315
- if (marketplaceRepo !== "") {
316
- const known = readJsonFile(knownMarketplaces)
317
- const has = known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false
318
- if (!has) {
319
- if (!(await cli(["plugin", "marketplace", "add", marketplaceRepo])).ok) {
320
- recordFailure(ctx, `Failed to add marketplace ${marketplaceRepo} for ${pluginId}`)
321
- return false
322
- }
323
- marketplaceAdded = true
324
- }
325
- }
326
-
327
- const wasInstalled = pluginUserScopeInstalled(installedPlugins, pluginId)
328
- if (!wasInstalled) {
329
- progress(`Installing plugin ${pluginId}...`)
330
- const installResult = await cli(["plugin", "install", pluginId])
331
- clearProgress()
332
- if (!installResult.ok) {
333
- if (marketplaceAdded) change(`Optional plugin ${pluginId}: marketplace added (install failed — will retry next sync)`)
334
- recordFailure(ctx, `Failed to install optional plugin ${pluginId}`)
335
- return marketplaceAdded
336
- }
337
- }
338
-
339
- const settingsDoc = readJsonFile(p(claudeDir, "settings.json"))
340
- const wasEnabled =
341
- settingsDoc !== undefined && isObject(settingsDoc) && isObject(settingsDoc["enabledPlugins"])
342
- ? (settingsDoc["enabledPlugins"] as { [k: string]: Json })[pluginId] === true
343
- : false
344
-
345
- if (!(await cli(["plugin", "enable", pluginId])).ok) {
346
- if (marketplaceAdded || !wasInstalled) change(`Optional plugin ${pluginId}: installed (enable failed — will retry next sync)`)
347
- recordFailure(ctx, `Failed to enable optional plugin ${pluginId}`)
348
- return marketplaceAdded || !wasInstalled
349
- }
350
- const changed = marketplaceAdded || !wasInstalled || !wasEnabled
351
- if (changed) change(`Optional plugin opted in: ${pluginId}`)
352
- else verbose(`Optional plugin already opted in: ${pluginId} (enable re-asserted)`)
353
- return changed
354
- }
355
-
356
- export async function syncOptionalPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
357
- const { echo, warn } = ctx.services.logger
358
- if (ctx.claudePlugins.length === 0) return
359
-
360
- if (ctx.dryRun) {
361
- if (ctx.claudePlugins.includes("supabase")) {
362
- echo("[dry-run] (--claude-plugin=supabase) install + enable supabase@claude-plugins-official in deployed settings")
363
- }
364
- if (ctx.claudePlugins.includes("n8n")) {
365
- echo("[dry-run] (--claude-plugin=n8n) add czlonkowski/n8n-skills marketplace + install + enable n8n-mcp-skills@n8n-mcp-skills")
366
- }
367
- return
368
- }
369
-
370
- if (ctx.services.deps.probe("claude").state === "missing") {
371
- warn("claude CLI not in PATH — cannot opt in optional plugins (--claude-plugin)")
372
- return
373
- }
374
-
375
- if (ctx.claudePlugins.includes("supabase")) {
376
- if (await enableOptionalPlugin(ctx, claudeDir, "supabase@claude-plugins-official", "")) ctx.nextStepTriggers.claudePlugins = true
377
- }
378
- if (ctx.claudePlugins.includes("n8n")) {
379
- if (await enableOptionalPlugin(ctx, claudeDir, "n8n-mcp-skills@n8n-mcp-skills", "czlonkowski/n8n-skills")) ctx.nextStepTriggers.claudePlugins = true
380
- }
381
- }
382
-
383
- // ---------------------------------------------------------- LSP servers ----
384
-
385
- function lspPkg(ctx: Ctx, tool: string, pkg: string): string | undefined {
386
- const version = field(tool, "verified")
387
- if (version !== "") return `${pkg}@${version}`
388
- ctx.services.logger.warn(`Skipping ${pkg} install: ${tool} has no verified version in SoT/toolchain.json`)
389
- return undefined
390
- }
391
-
392
- /**
393
- * npm treats a package's `engines` field as advisory unless the host opted
394
- * into engine-strict, so installing typescript-language-server 6 on an older
395
- * Node succeeds and then fails at every startup. Report the host Node version
396
- * when it is too old, and the empty string when the install is safe.
397
- */
398
- async function nodeBelowServerFloor(ctx: Ctx): Promise<string> {
399
- const floor = field("node", "floor")
400
- if (floor === "") return ""
401
- const installed = await installedVersion(ctx, "node")
402
- return belowFloor(installed, floor) ? installed : ""
403
- }
404
-
405
- /** npm-global channel: intelephense, typescript-language-server, typescript. */
406
- async function installNpmServers(ctx: Ctx, missing: ReadonlyArray<string>): Promise<void> {
407
- const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
408
- if (missing.length === 0) return
409
- const specs = missing.join(" ")
410
- if (ctx.dryRun) {
411
- echo(`[dry-run] would install: npm install -g ${specs}`)
412
- return
413
- }
414
-
415
- if (ctx.services.deps.probe("npm").state === "missing") {
416
- ctx.services.deps.warnMissing(
417
- "npm",
418
- ctx.services.logger,
419
- `cannot install LSP servers (${specs}); the php-lsp/typescript-lsp plugins stay no-ops`
420
- )
421
- return
422
- }
423
-
424
- verbose(`Installing LSP servers via npm: ${specs}...`)
425
- progress(`Installing LSP servers via npm: ${specs}...`)
426
- const installResult = await spawnProcess("npm", ["install", "-g", ...missing], { stdio: "ignore" })
427
- clearProgress()
428
- if (installResult.exitCode === 0) {
429
- change(`LSP servers installed (${specs})`)
430
- ctx.nextStepTriggers.claudeRestart = true
431
- } else {
432
- warn(`npm install -g ${specs} failed. Try manually: npm install -g ${specs}`)
433
- }
434
- }
435
-
436
- /**
437
- * rustup channel: rust-analyzer ships no npm package, and upstream's first
438
- * recommendation is the rustup component, which tracks the toolchain the host
439
- * already trusts. The component version therefore follows that toolchain, so
440
- * the manifest row carries no verified pin - the stance bubblewrap already
441
- * takes for a distro package.
442
- */
443
- async function installRustAnalyzer(ctx: Ctx): Promise<void> {
444
- const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
445
- if (ctx.dryRun) {
446
- echo("[dry-run] would install: rustup component add rust-analyzer")
447
- return
448
- }
449
- verbose("Installing rust-analyzer via rustup...")
450
- progress("Installing rust-analyzer via rustup...")
451
- const result = await spawnProcess("rustup", ["component", "add", "rust-analyzer"], { stdio: "ignore" })
452
- clearProgress()
453
- if (result.exitCode === 0) {
454
- change("LSP server installed (rust-analyzer via rustup)")
455
- ctx.nextStepTriggers.claudeRestart = true
456
- } else {
457
- warn("rustup component add rust-analyzer failed. Try manually: rustup component add rust-analyzer")
458
- }
459
- }
460
-
461
- export async function syncLspServers(ctx: Ctx): Promise<void> {
462
- const { echo, verbose, warn } = ctx.services.logger
463
- const sot = parseJson(payloadText("SoT/.claude/settings.json"))
464
- const enabled = sot !== undefined && isObject(sot) && isObject(sot["enabledPlugins"]) ? sot["enabledPlugins"] : undefined
465
- if (enabled === undefined) return
466
- const enables = (plugin: string): boolean => Object.prototype.hasOwnProperty.call(enabled, plugin)
467
- const hasPhp = enables("php-lsp@claude-plugins-official")
468
- const hasTs = enables("typescript-lsp@claude-plugins-official")
469
- const hasRust = enables("rust-analyzer-lsp@claude-plugins-official")
470
- if (!hasPhp && !hasTs && !hasRust) return
471
-
472
- const phpMissing = hasPhp && ctx.services.deps.probe("intelephense").state === "missing"
473
- const tsServerMissing = hasTs && ctx.services.deps.probe("typescript-language-server").state === "missing"
474
- const tscMissing = hasTs && ctx.services.deps.probe("tsc").state === "missing"
475
- const rustMissing = hasRust && ctx.services.deps.probe("rust-analyzer").state === "missing"
476
- // A host without rustup does no Rust work, so the plugin has nothing to
477
- // serve there. Installing is impossible and warning every sync would name no
478
- // action the user wants, so that host is not counted as missing a tool.
479
- const rustInstallable = rustMissing && ctx.services.deps.probe("rustup").state === "present"
480
- if (rustMissing && !rustInstallable) {
481
- verbose("Skipping rust-analyzer: rustup is not installed, so the rust-analyzer-lsp plugin stays a no-op")
482
- }
483
-
484
- const missingToolCount = Number(phpMissing) + Number(tsServerMissing) + Number(tscMissing) + Number(rustInstallable)
485
- if (missingToolCount === 0) {
486
- if (ctx.dryRun) {
487
- echo("[dry-run] LSP server binaries present")
488
- } else {
489
- verbose("LSP server binaries present")
490
- }
491
- return
492
- }
493
-
494
- const blockingNode = tsServerMissing ? await nodeBelowServerFloor(ctx) : ""
495
- if (blockingNode !== "") {
496
- warn(
497
- `Skipping typescript-language-server install: Node ${blockingNode} is older than the ${field("node", "floor")} that version requires. Upgrade Node, then re-run sync.`
498
- )
499
- }
500
- const missing = [
501
- phpMissing ? lspPkg(ctx, "intelephense", "intelephense") : undefined,
502
- tsServerMissing && blockingNode === ""
503
- ? lspPkg(ctx, "typescript-language-server", "typescript-language-server")
504
- : undefined,
505
- tscMissing ? lspPkg(ctx, "tsc", "typescript") : undefined
506
- ].filter((spec): spec is string => spec !== undefined)
507
-
508
- await installNpmServers(ctx, missing)
509
- if (rustInstallable) await installRustAnalyzer(ctx)
510
- }