docks-kit 0.16.4 → 0.16.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/cli/src/argv.ts +2 -1
- package/cli/src/commands/model.ts +2 -1
- package/cli/src/commands/models.ts +2 -1
- package/cli/src/commands/status.ts +2 -11
- package/cli/src/efforts.ts +0 -4
- package/cli/src/engine-native/bun.ts +1 -1
- package/cli/src/engine-native/claudePlugins.ts +390 -0
- package/cli/src/engine-native/claudeSync.ts +2 -387
- package/cli/src/engine-native/codexSync.ts +6 -146
- package/cli/src/engine-native/codexToml.ts +144 -3
- package/cli/src/engine-native/exec.ts +1 -5
- package/cli/src/engine-native/jq.ts +7 -0
- package/cli/src/engine-native/models.ts +31 -9
- package/cli/src/engine-native/ompSync.ts +3 -3
- package/cli/src/engine-native/os/darwin.ts +8 -56
- package/cli/src/engine-native/os/linux.ts +8 -56
- package/cli/src/engine-native/os/posix.ts +67 -0
- package/cli/src/engine-native/os/targets.ts +1 -1
- package/cli/src/engine-native/skillsSync.ts +3 -3
- package/cli/src/engine-native/toolchain.ts +7 -7
- package/cli/src/generated/sotPayload.ts +1 -1
- package/cli/src/manifests.ts +1 -18
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -128,7 +128,7 @@ The Output Standard wins.
|
|
|
128
128
|
Use `unslop` only for its pattern-detection lists.
|
|
129
129
|
|
|
130
130
|
<constraint>
|
|
131
|
-
When a kit-mechanic skill, its `references/`, or a wrapper agent (`.claude/agents/*.md` + its `.codex/agents/*.toml` twin) cites EngineNative internals, name the **module + exported/local function + semantic anchor** (e.g. `
|
|
131
|
+
When a kit-mechanic skill, its `references/`, or a wrapper agent (`.claude/agents/*.md` + its `.codex/agents/*.toml` twin) cites EngineNative internals, name the **module + exported/local function + semantic anchor** (e.g. `claudePlugins.ts syncPlugins, pass 5 uninstall guard`) — never a raw `file:NNN` line number, which goes stale on every refactor. Keep exactly one coarse `metadata.source_files[].lines` range per skill file as the sole intentional line-number touchpoint.
|
|
132
132
|
</constraint>
|
|
133
133
|
|
|
134
134
|
**Universal-skill bootstrap.** `SoT/.agents/skills.txt` is intentionally empty, so the default sync exposes no universal skills. The generic opt-in contract remains: each [agentskills.io](https://agentskills.io/specification) slug is installed by `skillsSync.ts` with `npx skills add <slug> -g -y -a claude-code codex`. The slug comes first because `-a/--agent` is variadic; naming both supported agents preserves the canonical `~/.agents/skills/<name>/SKILL.md` plus Claude's `~/.claude/skills/<name>` symlink. Existing canonical directories are reused idempotently, `--prune` removes only entries tracked in `~/.agents/.kit-managed-skills`, and skills requiring a separate CLI retain explicit helpers in `skillsSync.ts`.
|
package/cli/src/argv.ts
CHANGED
|
@@ -15,7 +15,8 @@ import {
|
|
|
15
15
|
effortFlagGrammar
|
|
16
16
|
} from "./efforts"
|
|
17
17
|
import { KNOWN_CLAUDE_OPTIN_PLUGINS } from "./engine-native/parseArgs"
|
|
18
|
-
import { modelCatalog
|
|
18
|
+
import { modelCatalog } from "./engine-native/models"
|
|
19
|
+
import type { Tool } from "./manifests"
|
|
19
20
|
|
|
20
21
|
export type ArgvOutcome =
|
|
21
22
|
| { readonly kind: "reject"; readonly message: string; readonly exitCode: number }
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Argument, Command, Flag, Prompt } from "effect/unstable/cli"
|
|
2
2
|
import { Effect, Option } from "effect"
|
|
3
3
|
import { bail, engine } from "../engine"
|
|
4
|
-
import { modelCatalog
|
|
4
|
+
import { modelCatalog } from "../engine-native/models"
|
|
5
|
+
import type { Tool } from "../manifests"
|
|
5
6
|
|
|
6
7
|
const tool = Argument.string("tool").pipe(
|
|
7
8
|
Argument.withDescription("Which tool: claude | codex")
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Argument, Command, Flag } from "effect/unstable/cli"
|
|
2
2
|
import { Console, Effect, Option } from "effect"
|
|
3
3
|
import { bail } from "../engine"
|
|
4
|
-
import { modelCatalog
|
|
4
|
+
import { modelCatalog } from "../engine-native/models"
|
|
5
|
+
import type { Tool } from "../manifests"
|
|
5
6
|
|
|
6
7
|
const tool = Argument.string("tool").pipe(
|
|
7
8
|
Argument.withDescription("claude | codex (omit for both tool catalogs)"),
|
|
@@ -72,20 +72,11 @@ const gatherDrift = (): {
|
|
|
72
72
|
return { setting, deployed: d, sot: s, drifted: d !== s }
|
|
73
73
|
}
|
|
74
74
|
const codex = row("codex.model", deployedCodexModel(), sotCodexModel())
|
|
75
|
-
if (claudeDeployment.state
|
|
75
|
+
if (claudeDeployment.state !== "valid") {
|
|
76
76
|
return {
|
|
77
77
|
claudeDeployment,
|
|
78
78
|
drift: [
|
|
79
|
-
{ setting: "claude.settings", deployed:
|
|
80
|
-
codex
|
|
81
|
-
]
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (claudeDeployment.state === "malformed") {
|
|
85
|
-
return {
|
|
86
|
-
claudeDeployment,
|
|
87
|
-
drift: [
|
|
88
|
-
{ setting: "claude.settings", deployed: "(malformed)", sot: "present", drifted: true },
|
|
79
|
+
{ setting: "claude.settings", deployed: `(${claudeDeployment.state})`, sot: "present", drifted: true },
|
|
89
80
|
codex
|
|
90
81
|
]
|
|
91
82
|
}
|
package/cli/src/efforts.ts
CHANGED
|
@@ -17,10 +17,6 @@ export const CLAUDE_ADVISOR_STATES = ["on", "off", "default"] as const
|
|
|
17
17
|
const VERIFIED = "2026-07-10"
|
|
18
18
|
const DEFAULT = "default"
|
|
19
19
|
|
|
20
|
-
export type ClaudeEffortLevel = typeof CLAUDE_EFFORT_LEVELS[number]
|
|
21
|
-
export type CodexReasoningEffort = typeof CODEX_REASONING_EFFORTS[number]
|
|
22
|
-
export type ClaudeAdvisorState = typeof CLAUDE_ADVISOR_STATES[number]
|
|
23
|
-
|
|
24
20
|
const upstreamEfforts = (tool: Tool): ReadonlyArray<string> =>
|
|
25
21
|
tool === "claude" ? CLAUDE_EFFORT_LEVELS : CODEX_REASONING_EFFORTS
|
|
26
22
|
|
|
@@ -57,7 +57,7 @@ async function runBunBootstrap(ctx: Ctx, services: EngineServices): Promise<BunR
|
|
|
57
57
|
const existing = await services.deps.path("bun")
|
|
58
58
|
if (existing !== "") return { kind: "ready", executable: existing }
|
|
59
59
|
|
|
60
|
-
const pin = field(
|
|
60
|
+
const pin = field("bun", "verified")
|
|
61
61
|
if (pin === "") {
|
|
62
62
|
services.logger.warn("Bun bootstrap aborted — SoT/toolchain.json has no verified Bun pin")
|
|
63
63
|
return { kind: "deferred", reason: "install-failed" }
|
|
@@ -0,0 +1,390 @@
|
|
|
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 type { Ctx } from "./index"
|
|
9
|
+
import { compareCodepoints, deepMerge, isObject, jqStringify, parseJson, readJsonFile, type Json } from "./jq"
|
|
10
|
+
import { field } from "./toolchain"
|
|
11
|
+
import { payloadText } from "../payload"
|
|
12
|
+
|
|
13
|
+
async function cli(args: Array<string>): Promise<{ ok: boolean; out: string }> {
|
|
14
|
+
const res = await spawnProcess("claude", args, { stdio: ["ignore", "pipe", "pipe"] })
|
|
15
|
+
return { ok: res.error === undefined && res.exitCode === 0, out: `${res.stdout}${res.stderr}` }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sortedKeys(obj: Json | undefined): Array<string> {
|
|
19
|
+
return obj !== undefined && isObject(obj) ? Object.keys(obj).sort(compareCodepoints) : []
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** claude::_plugin_user_scope_installed. */
|
|
23
|
+
export function pluginUserScopeInstalled(installedPlugins: string, pluginId: string): boolean {
|
|
24
|
+
const doc = readJsonFile(installedPlugins)
|
|
25
|
+
if (doc === undefined || !isObject(doc) || !isObject(doc["plugins"])) return false
|
|
26
|
+
const rec = (doc["plugins"] as { [k: string]: Json })[pluginId]
|
|
27
|
+
if (rec === undefined || rec === null) return false
|
|
28
|
+
const records = Array.isArray(rec) ? rec : [rec]
|
|
29
|
+
return records.some((r) => isObject(r) && r["scope"] === "user")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nonUserScopeMarketplaces(installedDoc: Json | undefined): Set<string> {
|
|
33
|
+
const marketplaces = new Set<string>()
|
|
34
|
+
if (installedDoc === undefined || !isObject(installedDoc) || !isObject(installedDoc["plugins"])) {
|
|
35
|
+
return marketplaces
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const [pluginId, value] of Object.entries(installedDoc["plugins"])) {
|
|
39
|
+
const records = Array.isArray(value) ? value : [value]
|
|
40
|
+
const hasNonUserInstall = records.some(
|
|
41
|
+
(record) => isObject(record) && (record["scope"] === "project" || record["scope"] === "local")
|
|
42
|
+
)
|
|
43
|
+
if (!hasNonUserInstall) continue
|
|
44
|
+
const separator = pluginId.lastIndexOf("@")
|
|
45
|
+
if (separator > 0) marketplaces.add(pluginId.slice(separator + 1))
|
|
46
|
+
}
|
|
47
|
+
return marketplaces
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
51
|
+
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
52
|
+
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
53
|
+
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
54
|
+
|
|
55
|
+
if (ctx.dryRun) {
|
|
56
|
+
echo(
|
|
57
|
+
ctx.skipPluginRefresh
|
|
58
|
+
? "[dry-run] bootstrap + install missing plugins from SoT; skip refresh-only plugin updates"
|
|
59
|
+
: "[dry-run] bootstrap + update plugin marketplaces + plugins from SoT"
|
|
60
|
+
)
|
|
61
|
+
if (ctx.prune) {
|
|
62
|
+
echo("[dry-run] (--prune) would also uninstall plugins not in SoT and remove extra marketplaces")
|
|
63
|
+
}
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (ctx.services.deps.probe("claude").state === "missing") {
|
|
68
|
+
warn("claude CLI not in PATH — skipping plugin reconcile (run /plugin marketplace add + /plugin install manually)")
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
if (ctx.services.deps.probe("git").state === "missing") {
|
|
72
|
+
ctx.services.deps.warnMissing(
|
|
73
|
+
"git",
|
|
74
|
+
ctx.services.logger,
|
|
75
|
+
"plugin marketplaces are git repos — Claude plugin passes skipped; re-run sync after installing"
|
|
76
|
+
)
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const repoSettings = parseJson(payloadText("SoT/.claude/settings.json"))
|
|
81
|
+
const repoObj = repoSettings !== undefined && isObject(repoSettings) ? repoSettings : {}
|
|
82
|
+
const sotMarketplaces = isObject(repoObj["extraKnownMarketplaces"]) ? repoObj["extraKnownMarketplaces"] : {}
|
|
83
|
+
const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {}
|
|
84
|
+
|
|
85
|
+
// Pass 1 — add missing marketplaces (SoT insertion order, like to_entries).
|
|
86
|
+
let addedMp = 0
|
|
87
|
+
let f1 = 0
|
|
88
|
+
for (const [mpName, mpValue] of Object.entries(sotMarketplaces)) {
|
|
89
|
+
const known = readJsonFile(knownMarketplaces)
|
|
90
|
+
if (known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false) continue
|
|
91
|
+
const repo = isObject(mpValue) && isObject(mpValue["source"]) ? String((mpValue["source"] as { [k: string]: Json })["repo"] ?? "") : ""
|
|
92
|
+
progress(`Adding marketplace ${mpName}...`)
|
|
93
|
+
const marketplaceResult = await cli(["plugin", "marketplace", "add", repo])
|
|
94
|
+
clearProgress()
|
|
95
|
+
if (marketplaceResult.ok) {
|
|
96
|
+
addedMp++
|
|
97
|
+
} else {
|
|
98
|
+
warn(`Failed to add marketplace: ${mpName} (${repo})`)
|
|
99
|
+
f1++
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Pass 2 — install SoT-enabled plugins missing at user scope (jq keys[] sorts),
|
|
104
|
+
// refreshing each source marketplace once so the install resolves a current snapshot.
|
|
105
|
+
let addedPl = 0
|
|
106
|
+
let f2 = 0
|
|
107
|
+
const refreshedMarketplaces = new Set<string>()
|
|
108
|
+
for (const pluginId of sortedKeys(sotPlugins)) {
|
|
109
|
+
if (pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
110
|
+
const separator = pluginId.lastIndexOf("@")
|
|
111
|
+
const mpName = separator > 0 ? pluginId.slice(separator + 1) : ""
|
|
112
|
+
if (mpName !== "" && !refreshedMarketplaces.has(mpName)) {
|
|
113
|
+
progress(`Refreshing marketplace ${mpName}...`)
|
|
114
|
+
await cli(["plugin", "marketplace", "update", mpName])
|
|
115
|
+
clearProgress()
|
|
116
|
+
refreshedMarketplaces.add(mpName)
|
|
117
|
+
}
|
|
118
|
+
progress(`Installing plugin ${pluginId}...`)
|
|
119
|
+
const installResult = await cli(["plugin", "install", pluginId])
|
|
120
|
+
clearProgress()
|
|
121
|
+
if (installResult.ok) {
|
|
122
|
+
addedPl++
|
|
123
|
+
} else {
|
|
124
|
+
warn(`Failed to install plugin: ${pluginId}`)
|
|
125
|
+
f2++
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let updatedPl = 0
|
|
130
|
+
const installedDoc = readJsonFile(installedPlugins)
|
|
131
|
+
const installedKeys = installedDoc !== undefined && isObject(installedDoc) ? sortedKeys(installedDoc["plugins"]) : []
|
|
132
|
+
const nonUserMarketplaces = nonUserScopeMarketplaces(installedDoc)
|
|
133
|
+
// Kit-owned plugin IDs: SoT-declared plus this run's --claude-plugin opt-ins.
|
|
134
|
+
const kitPluginIds = new Set<string>(Object.keys(sotPlugins))
|
|
135
|
+
if (ctx.claudePlugins.includes("supabase")) kitPluginIds.add("supabase@claude-plugins-official")
|
|
136
|
+
if (ctx.claudePlugins.includes("n8n")) kitPluginIds.add("n8n-mcp-skills@n8n-mcp-skills")
|
|
137
|
+
|
|
138
|
+
// Kit-owned marketplaces: SoT-declared plus every marketplace those plugins come from.
|
|
139
|
+
const kitMarketplaces = new Set<string>(Object.keys(sotMarketplaces))
|
|
140
|
+
for (const pluginId of kitPluginIds) {
|
|
141
|
+
const separator = pluginId.lastIndexOf("@")
|
|
142
|
+
if (separator > 0) kitMarketplaces.add(pluginId.slice(separator + 1))
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Pass 3 — refresh the kit-owned marketplaces unless the update command
|
|
146
|
+
// selected its install-missing-only fast path.
|
|
147
|
+
if (!ctx.skipPluginRefresh) {
|
|
148
|
+
for (const mpName of [...kitMarketplaces].sort(compareCodepoints)) {
|
|
149
|
+
progress(`Refreshing marketplace ${mpName}...`)
|
|
150
|
+
await cli(["plugin", "marketplace", "update", mpName])
|
|
151
|
+
clearProgress()
|
|
152
|
+
}
|
|
153
|
+
// Pass 4 — update the kit-owned installed plugins.
|
|
154
|
+
for (const pluginId of [...kitPluginIds].sort(compareCodepoints)) {
|
|
155
|
+
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
156
|
+
progress(`Updating plugin ${pluginId}...`)
|
|
157
|
+
const updateResult = await cli(["plugin", "update", pluginId, "--scope", "user"])
|
|
158
|
+
clearProgress()
|
|
159
|
+
if (updateResult.out.includes("Successfully updated")) updatedPl++
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Pass 5 — prune-gated user-scope plugin uninstall.
|
|
164
|
+
let removedPl = 0
|
|
165
|
+
let removedMp = 0
|
|
166
|
+
let f5 = 0
|
|
167
|
+
let f6 = 0
|
|
168
|
+
if (ctx.prune) {
|
|
169
|
+
for (const pluginId of installedKeys) {
|
|
170
|
+
if (kitPluginIds.has(pluginId)) continue
|
|
171
|
+
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
172
|
+
progress(`Uninstalling plugin ${pluginId}...`)
|
|
173
|
+
const uninstallResult = await cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId])
|
|
174
|
+
clearProgress()
|
|
175
|
+
if (uninstallResult.ok) {
|
|
176
|
+
removedPl++
|
|
177
|
+
} else {
|
|
178
|
+
warn(`Failed to uninstall plugin: ${pluginId}`)
|
|
179
|
+
f5++
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Pass 6 — prune-gated marketplace removal.
|
|
183
|
+
const known = readJsonFile(knownMarketplaces)
|
|
184
|
+
for (const mpName of sortedKeys(known)) {
|
|
185
|
+
if (mpName === "claude-plugins-official") continue
|
|
186
|
+
if (nonUserMarketplaces.has(mpName)) continue
|
|
187
|
+
if (kitMarketplaces.has(mpName)) continue
|
|
188
|
+
progress(`Removing marketplace ${mpName}...`)
|
|
189
|
+
const removeResult = await cli(["plugin", "marketplace", "remove", mpName])
|
|
190
|
+
clearProgress()
|
|
191
|
+
if (removeResult.ok) {
|
|
192
|
+
removedMp++
|
|
193
|
+
} else {
|
|
194
|
+
warn(`Failed to remove marketplace: ${mpName}`)
|
|
195
|
+
f6++
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Pass 7 — re-assert SoT enabled-state in the user settings.
|
|
201
|
+
if (await reassertEnabledState(ctx, repoObj, p(claudeDir, "settings.json"))) {
|
|
202
|
+
change("Plugin enable-state re-asserted from SoT in settings.json")
|
|
203
|
+
ctx.nextStepTriggers.claudePlugins = true
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const failed = f1 + f2 + f5 + f6
|
|
207
|
+
if (addedMp > 0 || addedPl > 0 || updatedPl > 0 || removedPl > 0 || removedMp > 0) {
|
|
208
|
+
change(`Plugins synced (marketplaces: +${addedMp} -${removedMp}, plugins: +${addedPl} ~${updatedPl} -${removedPl})`)
|
|
209
|
+
ctx.nextStepTriggers.claudePlugins = true
|
|
210
|
+
} else {
|
|
211
|
+
verbose("Plugins already in sync")
|
|
212
|
+
}
|
|
213
|
+
if (failed > 0) {
|
|
214
|
+
warn(`${failed} plugin operation(s) failed — re-run sync or install manually`)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSettingsFile: string): Promise<boolean> {
|
|
219
|
+
const { warn } = ctx.services.logger
|
|
220
|
+
if (!existsSync(userSettingsFile)) return false
|
|
221
|
+
const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {}
|
|
222
|
+
|
|
223
|
+
let cliDisabled = false
|
|
224
|
+
for (const [pluginId, value] of Object.entries(sotPlugins)) {
|
|
225
|
+
if (value !== false) continue
|
|
226
|
+
const user = readJsonFile(userSettingsFile)
|
|
227
|
+
const enabled = user !== undefined && isObject(user) && isObject(user["enabledPlugins"]) ? (user["enabledPlugins"] as { [k: string]: Json })[pluginId] : undefined
|
|
228
|
+
if (enabled !== true) continue
|
|
229
|
+
if ((await cli(["plugin", "disable", pluginId])).ok) {
|
|
230
|
+
cliDisabled = true
|
|
231
|
+
} else {
|
|
232
|
+
warn(`Failed to disable SoT-false plugin: ${pluginId} (will retry next sync)`)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const user = readJsonFile(userSettingsFile)
|
|
237
|
+
if (user === undefined || !isObject(user)) {
|
|
238
|
+
warn("enabledPlugins re-assert failed — false-keyed plugins may be left enabled")
|
|
239
|
+
return false
|
|
240
|
+
}
|
|
241
|
+
const beforeCanonical = jqStringify(user)
|
|
242
|
+
user["enabledPlugins"] = deepMerge(isObject(user["enabledPlugins"]) ? user["enabledPlugins"] : {}, sotPlugins)
|
|
243
|
+
const out = jqStringify(user)
|
|
244
|
+
if (out === beforeCanonical) return cliDisabled
|
|
245
|
+
writeFileSync(`${userSettingsFile}.tmp`, out)
|
|
246
|
+
renameSync(`${userSettingsFile}.tmp`, userSettingsFile)
|
|
247
|
+
return true
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ------------------------------------------------------ optional plugins ----
|
|
251
|
+
|
|
252
|
+
async function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): Promise<boolean> {
|
|
253
|
+
const { change, clearProgress, progress, verbose, warn } = ctx.services.logger
|
|
254
|
+
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
255
|
+
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
256
|
+
const mpName = pluginId.slice(pluginId.lastIndexOf("@") + 1)
|
|
257
|
+
|
|
258
|
+
let marketplaceAdded = false
|
|
259
|
+
if (marketplaceRepo !== "") {
|
|
260
|
+
const known = readJsonFile(knownMarketplaces)
|
|
261
|
+
const has = known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false
|
|
262
|
+
if (!has) {
|
|
263
|
+
if (!(await cli(["plugin", "marketplace", "add", marketplaceRepo])).ok) {
|
|
264
|
+
warn(`Failed to add marketplace ${marketplaceRepo} for ${pluginId}`)
|
|
265
|
+
return false
|
|
266
|
+
}
|
|
267
|
+
marketplaceAdded = true
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const wasInstalled = pluginUserScopeInstalled(installedPlugins, pluginId)
|
|
272
|
+
if (!wasInstalled) {
|
|
273
|
+
progress(`Installing plugin ${pluginId}...`)
|
|
274
|
+
const installResult = await cli(["plugin", "install", pluginId])
|
|
275
|
+
clearProgress()
|
|
276
|
+
if (!installResult.ok) {
|
|
277
|
+
if (marketplaceAdded) change(`Optional plugin ${pluginId}: marketplace added (install failed — will retry next sync)`)
|
|
278
|
+
warn(`Failed to install optional plugin ${pluginId}`)
|
|
279
|
+
return marketplaceAdded
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const settingsDoc = readJsonFile(p(claudeDir, "settings.json"))
|
|
284
|
+
const wasEnabled =
|
|
285
|
+
settingsDoc !== undefined && isObject(settingsDoc) && isObject(settingsDoc["enabledPlugins"])
|
|
286
|
+
? (settingsDoc["enabledPlugins"] as { [k: string]: Json })[pluginId] === true
|
|
287
|
+
: false
|
|
288
|
+
|
|
289
|
+
if (!(await cli(["plugin", "enable", pluginId])).ok) {
|
|
290
|
+
if (marketplaceAdded || !wasInstalled) change(`Optional plugin ${pluginId}: installed (enable failed — will retry next sync)`)
|
|
291
|
+
warn(`Failed to enable optional plugin ${pluginId}`)
|
|
292
|
+
return marketplaceAdded || !wasInstalled
|
|
293
|
+
}
|
|
294
|
+
const changed = marketplaceAdded || !wasInstalled || !wasEnabled
|
|
295
|
+
if (changed) change(`Optional plugin opted in: ${pluginId}`)
|
|
296
|
+
else verbose(`Optional plugin already opted in: ${pluginId} (enable re-asserted)`)
|
|
297
|
+
return changed
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export async function syncOptionalPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
301
|
+
const { echo, warn } = ctx.services.logger
|
|
302
|
+
if (ctx.claudePlugins.length === 0) return
|
|
303
|
+
|
|
304
|
+
if (ctx.dryRun) {
|
|
305
|
+
if (ctx.claudePlugins.includes("supabase")) {
|
|
306
|
+
echo("[dry-run] (--claude-plugin=supabase) install + enable supabase@claude-plugins-official in deployed settings")
|
|
307
|
+
}
|
|
308
|
+
if (ctx.claudePlugins.includes("n8n")) {
|
|
309
|
+
echo("[dry-run] (--claude-plugin=n8n) add czlonkowski/n8n-skills marketplace + install + enable n8n-mcp-skills@n8n-mcp-skills")
|
|
310
|
+
}
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (ctx.services.deps.probe("claude").state === "missing") {
|
|
315
|
+
warn("claude CLI not in PATH — cannot opt in optional plugins (--claude-plugin)")
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (ctx.claudePlugins.includes("supabase")) {
|
|
320
|
+
if (await enableOptionalPlugin(ctx, claudeDir, "supabase@claude-plugins-official", "")) ctx.nextStepTriggers.claudePlugins = true
|
|
321
|
+
}
|
|
322
|
+
if (ctx.claudePlugins.includes("n8n")) {
|
|
323
|
+
if (await enableOptionalPlugin(ctx, claudeDir, "n8n-mcp-skills@n8n-mcp-skills", "czlonkowski/n8n-skills")) ctx.nextStepTriggers.claudePlugins = true
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------- LSP servers ----
|
|
328
|
+
|
|
329
|
+
function lspPkg(ctx: Ctx, tool: string, pkg: string): string | undefined {
|
|
330
|
+
const version = field(tool, "verified")
|
|
331
|
+
if (version !== "") return `${pkg}@${version}`
|
|
332
|
+
ctx.services.logger.warn(`Skipping ${pkg} install: ${tool} has no verified version in SoT/toolchain.json`)
|
|
333
|
+
return undefined
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function syncLspServers(ctx: Ctx): Promise<void> {
|
|
337
|
+
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
338
|
+
const sot = parseJson(payloadText("SoT/.claude/settings.json"))
|
|
339
|
+
const enabled = sot !== undefined && isObject(sot) && isObject(sot["enabledPlugins"]) ? sot["enabledPlugins"] : undefined
|
|
340
|
+
if (enabled === undefined) return
|
|
341
|
+
const hasPhp = Object.prototype.hasOwnProperty.call(enabled, "php-lsp@claude-plugins-official")
|
|
342
|
+
const hasTs = Object.prototype.hasOwnProperty.call(enabled, "typescript-lsp@claude-plugins-official")
|
|
343
|
+
if (!hasPhp && !hasTs) return
|
|
344
|
+
|
|
345
|
+
const phpMissing = hasPhp && ctx.services.deps.probe("intelephense").state === "missing"
|
|
346
|
+
const tsServerMissing = hasTs && ctx.services.deps.probe("typescript-language-server").state === "missing"
|
|
347
|
+
const tscMissing = hasTs && ctx.services.deps.probe("tsc").state === "missing"
|
|
348
|
+
const missingToolCount = Number(phpMissing) + Number(tsServerMissing) + Number(tscMissing)
|
|
349
|
+
const missing = [
|
|
350
|
+
phpMissing ? lspPkg(ctx, "intelephense", "intelephense") : undefined,
|
|
351
|
+
tsServerMissing ? lspPkg(ctx, "typescript-language-server", "typescript-language-server") : undefined,
|
|
352
|
+
tscMissing ? lspPkg(ctx, "tsc", "typescript") : undefined
|
|
353
|
+
].filter((spec): spec is string => spec !== undefined)
|
|
354
|
+
|
|
355
|
+
if (missingToolCount === 0) {
|
|
356
|
+
if (ctx.dryRun) {
|
|
357
|
+
echo("[dry-run] LSP server binaries present")
|
|
358
|
+
} else {
|
|
359
|
+
verbose("LSP server binaries present")
|
|
360
|
+
}
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
if (missing.length === 0) return
|
|
364
|
+
|
|
365
|
+
const specs = missing.join(" ")
|
|
366
|
+
if (ctx.dryRun) {
|
|
367
|
+
echo(`[dry-run] would install: npm install -g ${specs}`)
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (ctx.services.deps.probe("npm").state === "missing") {
|
|
372
|
+
ctx.services.deps.warnMissing(
|
|
373
|
+
"npm",
|
|
374
|
+
ctx.services.logger,
|
|
375
|
+
`cannot install LSP servers (${specs}); the php-lsp/typescript-lsp plugins stay no-ops`
|
|
376
|
+
)
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
verbose(`Installing LSP servers via npm: ${specs}...`)
|
|
381
|
+
progress(`Installing LSP servers via npm: ${specs}...`)
|
|
382
|
+
const installResult = await spawnProcess("npm", ["install", "-g", ...missing], { stdio: "ignore" })
|
|
383
|
+
clearProgress()
|
|
384
|
+
if (installResult.exitCode === 0) {
|
|
385
|
+
change(`LSP servers installed (${specs})`)
|
|
386
|
+
ctx.nextStepTriggers.claudeRestart = true
|
|
387
|
+
} else {
|
|
388
|
+
warn(`npm install -g ${specs} failed. Try manually: npm install -g ${specs}`)
|
|
389
|
+
}
|
|
390
|
+
}
|