docks-kit 0.17.1 → 0.17.3
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/AGENTS.md +41 -33
- package/cli/docs/omp-context.md +104 -0
- package/cli/docs/omp-models.md +106 -56
- package/cli/src/argv.ts +38 -469
- package/cli/src/argvSurface.ts +371 -0
- package/cli/src/argvValidate.ts +122 -0
- package/cli/src/commands/docs.ts +62 -42
- package/cli/src/commands/harnesses.ts +38 -45
- package/cli/src/commands/model.ts +48 -53
- package/cli/src/commands/models.ts +24 -26
- package/cli/src/commands/omp.ts +132 -112
- package/cli/src/commands/plugins.ts +20 -20
- package/cli/src/commands/skills.ts +22 -20
- package/cli/src/commands/status.ts +87 -80
- package/cli/src/commands/sync.ts +113 -87
- package/cli/src/commands/toolchain.ts +21 -21
- package/cli/src/commands/update.ts +139 -117
- package/cli/src/efforts.ts +35 -34
- package/cli/src/engine-native/DESIGN.md +30 -30
- package/cli/src/engine-native/bun.ts +77 -55
- package/cli/src/engine-native/claudeLsp.ts +154 -0
- package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
- package/cli/src/engine-native/claudePluginPasses.ts +348 -0
- package/cli/src/engine-native/claudeRemovals.ts +195 -0
- package/cli/src/engine-native/claudeRetired.ts +4 -4
- package/cli/src/engine-native/claudeRuntime.ts +94 -68
- package/cli/src/engine-native/claudeSettings.ts +174 -0
- package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
- package/cli/src/engine-native/claudeSync.ts +176 -456
- package/cli/src/engine-native/codexConfig.ts +158 -0
- package/cli/src/engine-native/codexHooks.ts +91 -0
- package/cli/src/engine-native/codexPlugins.ts +290 -0
- package/cli/src/engine-native/codexStatus.ts +24 -0
- package/cli/src/engine-native/codexSync.ts +110 -568
- package/cli/src/engine-native/codexToml.ts +140 -138
- package/cli/src/engine-native/deps.ts +144 -107
- package/cli/src/engine-native/engineCtx.ts +126 -0
- package/cli/src/engine-native/exec.ts +98 -87
- package/cli/src/engine-native/failures.ts +3 -3
- package/cli/src/engine-native/harnesses.ts +62 -72
- package/cli/src/engine-native/index.ts +34 -315
- package/cli/src/engine-native/jq.ts +25 -24
- package/cli/src/engine-native/logger.ts +98 -100
- package/cli/src/engine-native/models.ts +50 -45
- package/cli/src/engine-native/modes.ts +83 -80
- package/cli/src/engine-native/ompFileDeploy.ts +111 -0
- package/cli/src/engine-native/ompMarketplace.ts +136 -0
- package/cli/src/engine-native/ompOverlay.ts +67 -64
- package/cli/src/engine-native/ompPaths.ts +49 -38
- package/cli/src/engine-native/ompPlugins.ts +170 -0
- package/cli/src/engine-native/ompRemovals.ts +159 -0
- package/cli/src/engine-native/ompSync.ts +69 -409
- package/cli/src/engine-native/ompYaml.ts +43 -41
- package/cli/src/engine-native/os/darwin.ts +5 -5
- package/cli/src/engine-native/os/index.ts +15 -15
- package/cli/src/engine-native/os/linux.ts +5 -5
- package/cli/src/engine-native/os/posix.ts +28 -24
- package/cli/src/engine-native/os/targets.ts +17 -17
- package/cli/src/engine-native/os/types.ts +31 -31
- package/cli/src/engine-native/os/windows.ts +61 -61
- package/cli/src/engine-native/parseArgs.ts +122 -372
- package/cli/src/engine-native/parseHelp.ts +66 -0
- package/cli/src/engine-native/parseModifiers.ts +222 -0
- package/cli/src/engine-native/services.ts +34 -34
- package/cli/src/engine-native/settings.ts +15 -15
- package/cli/src/engine-native/sharedTypes.d.ts +46 -0
- package/cli/src/engine-native/skillsInstall.ts +105 -0
- package/cli/src/engine-native/skillsLinks.ts +203 -0
- package/cli/src/engine-native/skillsManifest.ts +16 -0
- package/cli/src/engine-native/skillsPrune.ts +108 -0
- package/cli/src/engine-native/skillsSync.ts +28 -362
- package/cli/src/engine-native/syncConcurrency.ts +55 -0
- package/cli/src/engine-native/syncDispatch.ts +141 -0
- package/cli/src/engine-native/toolchain.ts +65 -62
- package/cli/src/engine.ts +65 -57
- package/cli/src/generated/sotPayload.ts +7 -7
- package/cli/src/kitHome.ts +44 -40
- package/cli/src/main.ts +63 -56
- package/cli/src/manifests.ts +53 -54
- package/cli/src/md.d.ts +2 -2
- package/cli/src/payload.ts +9 -9
- package/cli/src/services.ts +21 -13
- package/cli/tsconfig.json +4 -1
- package/docks-kit +13 -2
- package/docks-kit.ps1 +21 -6
- package/package.json +10 -2
- package/cli/src/engine-native/claudePlugins.ts +0 -510
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "docks-kit",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.3",
|
|
4
4
|
"description": "Portable AI coding agent config kit — SoT sync engine + typed CLI for Claude Code, Codex, and universal agent skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,7 +37,13 @@
|
|
|
37
37
|
"test:unit": "vitest run",
|
|
38
38
|
"test:runtime:posix": "bun cli/test/statusline-runtime-smoke.mjs posix",
|
|
39
39
|
"test:runtime:windows": "bun cli/test/statusline-runtime-smoke.mjs windows",
|
|
40
|
-
"test:ci": "bun run check:generated && bun run --parallel typecheck test:unit golden:dryrun golden:mutation && bun run test:runtime:posix"
|
|
40
|
+
"test:ci": "bun run check:generated && bun run --parallel typecheck test:unit golden:dryrun golden:mutation format:check && bun run test:runtime:posix",
|
|
41
|
+
"test:integration": "bun run --parallel golden:dryrun golden:mutation",
|
|
42
|
+
"test": "bun run --parallel test:unit test:integration",
|
|
43
|
+
"lint": "oxlint",
|
|
44
|
+
"format": "oxfmt --write cli/src cli/test cli/scripts vitest.config.ts",
|
|
45
|
+
"format:check": "oxfmt --check cli/src cli/test cli/scripts vitest.config.ts",
|
|
46
|
+
"check": "bun run --parallel lint format:check typecheck test"
|
|
41
47
|
},
|
|
42
48
|
"dependencies": {
|
|
43
49
|
"@effect/platform-bun": "4.0.0-rc.115",
|
|
@@ -48,6 +54,8 @@
|
|
|
48
54
|
"devDependencies": {
|
|
49
55
|
"@effect/vitest": "4.0.0-rc.115",
|
|
50
56
|
"@types/bun": "^1.4.0",
|
|
57
|
+
"oxfmt": "0.67.0",
|
|
58
|
+
"oxlint": "1.82.0",
|
|
51
59
|
"typescript": "7.0.2",
|
|
52
60
|
"vitest": "5.0.0"
|
|
53
61
|
}
|
|
@@ -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
|
-
}
|