docks-kit 0.16.4 → 0.16.6
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 +2 -1
- package/cli/docs/omp-models.md +34 -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 +3 -3
- package/cli/src/manifests.ts +1 -18
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -66,6 +66,7 @@ omp SoT notes:
|
|
|
66
66
|
- `ompSync.ts syncConfig` deep-merges `config.yml` through `mergeOmpConfig`.
|
|
67
67
|
- `cycleOrder` ends with the `fable` role (`modelRoles.fable` = `anthropic/claude-fable-5-1:medium`, `modelTags.fable` visible, `retry.fallbackChains.fable` empty), so the model switcher reaches Fable 5.1 as its fourth stop and never falls back off it. The hidden `switch_fable` role points at the same model and level.
|
|
68
68
|
- `modelRoles.task` is `openai-codex/gpt-6-astra:low`, chosen for 2.60 s TTFT and 4k output tokens per task at cost parity with the previous `gpt-5.6-sol:high`. The four reviewer agents in `task.agentModelOverrides` inherit `@task`, and Artificial Analysis publishes no per-level Astra Coding Agent Index score, so reviewer output is the signal to watch.
|
|
69
|
+
- `task.maxEffort` is `low`. The bundled `task` agent runs `thinking-level: auto`, which rated many prompts `high`, and a caller's `effort: hi` also reached Astra `high`; the ceiling clamps both. Verified for the five bundled agents: `task`, `reviewer`, and `security-reviewer` resolve Astra `low`; `scout` and `sonic` (bundled `@smol`, Luna) drop to Luna `low`, the accepted cost. `cli/docs/omp-models.md` records the verification.
|
|
69
70
|
- `SoT/.omp/AGENTS.md` carries the rule `Please remove all mannered prose.` Anthropic's Fable 5.1 prompting guide documents mannered prose as a Fable 5.1 behavior and gives that sentence as its short-version fix: https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-fable-5-1
|
|
70
71
|
- `cli/docs/omp-models.md` (topic `omp-models`) records the role map rationale and the Artificial Analysis snapshot behind it. Model choices change with published benchmarks, so update that topic in the same commit as a role change.
|
|
71
72
|
- Sync registers the `docks` marketplace. It installs or upgrades `docks@docks` and `plan-lifecycle@docks` at user scope.
|
|
@@ -128,7 +129,7 @@ The Output Standard wins.
|
|
|
128
129
|
Use `unslop` only for its pattern-detection lists.
|
|
129
130
|
|
|
130
131
|
<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. `
|
|
132
|
+
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
133
|
</constraint>
|
|
133
134
|
|
|
134
135
|
**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/docs/omp-models.md
CHANGED
|
@@ -135,7 +135,7 @@ published numeric cache price. Context 1M.
|
|
|
135
135
|
Price: $0.20 in, $1.20 out, $0.02 cache read per 1M; no published cache-write
|
|
136
136
|
price. Context 1M. AA lists cost per task only for max and xhigh.
|
|
137
137
|
|
|
138
|
-
## Why `task`
|
|
138
|
+
## Why `task` points at Astra low
|
|
139
139
|
|
|
140
140
|
| Metric | Astra low | Sol high | Sol max | Opus 5 high |
|
|
141
141
|
|---|---:|---:|---:|---:|
|
|
@@ -166,6 +166,39 @@ level except max. Watch reviewer output, because the four reviewer agents
|
|
|
166
166
|
inherit `@task`. If review quality drops, pin those four agents to
|
|
167
167
|
`anthropic/claude-opus-5:high` rather than reverting the whole role.
|
|
168
168
|
|
|
169
|
+
## What resolves to Astra low in practice
|
|
170
|
+
|
|
171
|
+
`modelRoles.task` sets the model. The `:low` suffix is the role's own level;
|
|
172
|
+
it does not by itself decide the level of every spawn, and the role does not
|
|
173
|
+
cover every agent.
|
|
174
|
+
|
|
175
|
+
- The bundled `scout` and `sonic` agents carry `model: "@smol"` and
|
|
176
|
+
`thinking-level: medium` in their embedded frontmatter, so they run Luna,
|
|
177
|
+
not Astra. To move them, change `modelRoles.smol` or add a
|
|
178
|
+
`task.agentModelOverrides` entry for the agent name.
|
|
179
|
+
- The bundled `task` agent carries `model: "@task"` and
|
|
180
|
+
`thinking-level: auto`. `auto` classifies each prompt and picks a level for
|
|
181
|
+
the resolved model. With no ceiling, `task` spawns without an `effort` hint
|
|
182
|
+
were observed at both `low` and `high`.
|
|
183
|
+
- `task.enableEffort` is `true`, so a caller can pass `effort: lo`, `med`, or
|
|
184
|
+
`hi`, which overrides `auto`.
|
|
185
|
+
- `task.maxEffort` is `low` in the SoT. This ceiling clamps both the `auto`
|
|
186
|
+
classifier and the `effort` hint. Verified on 2026-09-08 for the five
|
|
187
|
+
bundled agents: `task`, `reviewer`, and `security-reviewer` resolved
|
|
188
|
+
`gpt-6-astra:low` with no hint and with `effort: hi`; `scout` and `sonic`
|
|
189
|
+
with `effort: hi` resolved `gpt-5.6-luna:low`. The `plan-lifecycle` plugin
|
|
190
|
+
agents `code-reviewer` and `plan-reviewer` carry no `model` or
|
|
191
|
+
`thinking-level` frontmatter and inherit `@task`; the cap applies to their
|
|
192
|
+
spawns by the same rule, but they were not probed.
|
|
193
|
+
|
|
194
|
+
So the bundled subagents run their models at `low`. The Astra low figures
|
|
195
|
+
above are the level `task`, `reviewer`, and `security-reviewer` get. The cost
|
|
196
|
+
of the cap is that `scout` and `sonic` also drop from Luna `medium` to Luna
|
|
197
|
+
`low` (index 22 against 26). Raising `task.maxEffort` lets `auto` and
|
|
198
|
+
`effort: hi` reach Astra `high` again ($1.72 per index task, 45.63 s TTFT).
|
|
199
|
+
|
|
200
|
+
omp's model catalog lists Astra with a 272k context window, while AA lists 1M.
|
|
201
|
+
|
|
169
202
|
## Maintenance
|
|
170
203
|
|
|
171
204
|
- Refresh the snapshot from the AA release page of each family
|
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
|
+
}
|