docks-kit 0.14.2 → 0.14.4
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 +3 -4
- package/README.md +5 -5
- package/cli/docs/flags.md +7 -2
- package/cli/docs/install.md +2 -2
- package/cli/docs/sync-layers.md +9 -12
- package/cli/docs/toolchain.md +10 -13
- package/cli/src/commands/sync.ts +5 -5
- package/cli/src/commands/toolchain.ts +1 -1
- package/cli/src/commands/update.ts +28 -2
- package/cli/src/engine-native/DESIGN.md +35 -15
- package/cli/src/engine-native/bun.ts +22 -21
- package/cli/src/engine-native/claudeSync.ts +90 -135
- package/cli/src/engine-native/codexSync.ts +31 -27
- package/cli/src/engine-native/deps.ts +52 -68
- package/cli/src/engine-native/exec.ts +61 -5
- package/cli/src/engine-native/index.ts +150 -17
- package/cli/src/engine-native/logger.ts +173 -10
- package/cli/src/engine-native/modes.ts +6 -11
- package/cli/src/engine-native/parseArgs.ts +5 -5
- package/cli/src/engine-native/services.ts +5 -5
- package/cli/src/engine-native/skillsSync.ts +34 -69
- package/cli/src/engine-native/toolchain.ts +44 -27
- package/cli/src/engine.ts +1 -1
- package/cli/src/generated/sotPayload.ts +4 -4
- package/cli/src/main.ts +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* EngineNative `sync claude` pipeline. Step order is load-bearing:
|
|
3
|
-
* the settings merge, modifiers after it, removals before
|
|
4
|
-
* strings, guard order, JSON semantics, and spawned argv are
|
|
2
|
+
* EngineNative `sync claude` pipeline. Step order is load-bearing: the Bun
|
|
3
|
+
* bootstrap BEFORE the settings merge, modifiers after it, removals before
|
|
4
|
+
* plugins. Message strings, guard order, JSON semantics, and spawned argv are
|
|
5
|
+
* golden-tested.
|
|
5
6
|
*/
|
|
6
|
-
import { spawnSync } from "node:child_process"
|
|
7
7
|
import {
|
|
8
8
|
appendFileSync,
|
|
9
9
|
copyFileSync,
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
rmSync,
|
|
16
16
|
writeFileSync
|
|
17
17
|
} from "node:fs"
|
|
18
|
-
import { tmpdir } from "node:os"
|
|
19
18
|
import { bunBootstrap } from "./bun"
|
|
20
19
|
import {
|
|
21
20
|
syncClaudeAdvisor,
|
|
@@ -23,20 +22,19 @@ import {
|
|
|
23
22
|
syncClaudeModel
|
|
24
23
|
} from "./claudeSettingsModifiers"
|
|
25
24
|
import { claudeRuntimePaths, materializeClaudeSettings, type ClaudeRuntimePaths } from "./claudeRuntime"
|
|
26
|
-
import { p, writeBytesIfChanged, writeTextIfChanged } from "./exec"
|
|
25
|
+
import { p, spawnProcess, writeBytesIfChanged, writeTextIfChanged } from "./exec"
|
|
27
26
|
import type { Ctx } from "./index"
|
|
28
27
|
import { compareCodepoints, deepMerge, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
29
|
-
import type { EngineServices } from "./services"
|
|
30
28
|
import { ExitError } from "./parseArgs"
|
|
31
29
|
import { mergeSettings, reconcileSettings } from "./settings"
|
|
32
|
-
import {
|
|
30
|
+
import { field } from "./toolchain"
|
|
33
31
|
import { payloadBytes, payloadDisplayPath, payloadText } from "../payload"
|
|
34
32
|
|
|
35
33
|
export type ClaudeRuntimeState =
|
|
36
34
|
| { readonly kind: "ready"; readonly paths: ClaudeRuntimePaths }
|
|
37
35
|
| { readonly kind: "deferred"; readonly reason: "bun-unavailable" }
|
|
38
36
|
|
|
39
|
-
export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
|
|
37
|
+
export async function claudeSync(ctx: Ctx): Promise<ClaudeRuntimeState> {
|
|
40
38
|
const { err, warn } = ctx.services.logger
|
|
41
39
|
const claudeDir = p(ctx.home, ".claude")
|
|
42
40
|
|
|
@@ -48,8 +46,7 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
|
|
|
48
46
|
)
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
const bun = bunBootstrap(ctx, ctx.services)
|
|
49
|
+
const bun = await bunBootstrap(ctx, ctx.services)
|
|
53
50
|
const runtime: ClaudeRuntimeState = bun.kind === "ready"
|
|
54
51
|
? { kind: "ready", paths: claudeRuntimePaths(claudeDir, bun.executable) }
|
|
55
52
|
: { kind: "deferred", reason: "bun-unavailable" }
|
|
@@ -80,92 +77,12 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
|
|
|
80
77
|
syncClaudeAdvisor(ctx, ctx.claudeAdvisor)
|
|
81
78
|
syncClaudeJson(ctx)
|
|
82
79
|
syncConnectorEnv(ctx)
|
|
83
|
-
syncPlugins(ctx, claudeDir)
|
|
84
|
-
syncOptionalPlugins(ctx, claudeDir)
|
|
85
|
-
syncLspServers(ctx)
|
|
80
|
+
await syncPlugins(ctx, claudeDir)
|
|
81
|
+
await syncOptionalPlugins(ctx, claudeDir)
|
|
82
|
+
await syncLspServers(ctx)
|
|
86
83
|
return runtime
|
|
87
84
|
}
|
|
88
85
|
|
|
89
|
-
// ------------------------------------------------------------------ rtk ----
|
|
90
|
-
|
|
91
|
-
type RtkInstaller = ((mode: "install" | "upgrade", version: string, services: EngineServices) => number) & {
|
|
92
|
-
readonly prerequisite: (services: EngineServices) => number | undefined
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** RTK toolchain install callback with the shared contextual curl boundary. */
|
|
96
|
-
export function rtkInstall(ctx: Ctx, missingCurlContext: string, missingCurlExit: number): RtkInstaller {
|
|
97
|
-
const prerequisite = (services: EngineServices): number | undefined => {
|
|
98
|
-
if (services.deps.probe("curl").state === "present") return undefined
|
|
99
|
-
services.deps.warnMissing("curl", services.logger, missingCurlContext)
|
|
100
|
-
return missingCurlExit
|
|
101
|
-
}
|
|
102
|
-
const install = (mode: "install" | "upgrade", version: string, services: EngineServices): number => {
|
|
103
|
-
const blocked = prerequisite(services)
|
|
104
|
-
if (blocked !== undefined) return blocked
|
|
105
|
-
const { change, err, verbose, warn } = services.logger
|
|
106
|
-
const installerRef = version !== "" ? `refs/tags/v${version}` : "refs/heads/master"
|
|
107
|
-
|
|
108
|
-
if (mode === "upgrade") verbose(`Upgrading RTK${version !== "" ? ` to ${version}` : ""}...`)
|
|
109
|
-
else warn(`RTK not found. Installing${version !== "" ? ` ${version}` : ""}...`)
|
|
110
|
-
const installer = p(tmpdir(), `rtk-install-${process.pid}.sh`)
|
|
111
|
-
const dl = spawnSync("curl", ["-fsSL", `https://raw.githubusercontent.com/rtk-ai/rtk/${installerRef}/install.sh`, "-o", installer], {
|
|
112
|
-
stdio: "inherit"
|
|
113
|
-
})
|
|
114
|
-
if (dl.error === undefined && dl.status === 0) {
|
|
115
|
-
spawnSync("bash", [installer], {
|
|
116
|
-
stdio: "inherit",
|
|
117
|
-
env: { ...process.env, RTK_VERSION: version !== "" ? `v${version}` : "" }
|
|
118
|
-
})
|
|
119
|
-
}
|
|
120
|
-
rmSync(installer, { force: true })
|
|
121
|
-
process.env["PATH"] = `${ctx.home}/.local/bin:${ctx.home}/.cargo/bin:${process.env["PATH"] ?? ""}`
|
|
122
|
-
const installed = services.deps.version("rtk")
|
|
123
|
-
if (services.deps.probe("rtk").state === "present") {
|
|
124
|
-
change(`RTK ready (${installed !== "" ? installed : "version unknown"})`)
|
|
125
|
-
return 0
|
|
126
|
-
}
|
|
127
|
-
err("RTK install failed. Install manually: https://github.com/rtk-ai/rtk")
|
|
128
|
-
return 1
|
|
129
|
-
}
|
|
130
|
-
return Object.assign(install, { prerequisite })
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function ensureRtk(ctx: Ctx, missingCurlContext: string, missingCurlExit: number): number {
|
|
134
|
-
const installer = rtkInstall(ctx, missingCurlContext, missingCurlExit)
|
|
135
|
-
if (ctx.services.deps.probe("rtk").state === "missing") {
|
|
136
|
-
const blocked = installer.prerequisite(ctx.services)
|
|
137
|
-
if (blocked !== undefined) return blocked
|
|
138
|
-
}
|
|
139
|
-
return ensure(ctx, "rtk", installer)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function syncRtk(ctx: Ctx, claudeDir: string): void {
|
|
143
|
-
const { change, echo, verbose, warn } = ctx.services.logger
|
|
144
|
-
if (ctx.skipRtk) {
|
|
145
|
-
warn("Skipping RTK (--skip-rtk)")
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (ensureRtk(ctx, "cannot download RTK installer; continuing sync without RTK", 0) !== 0) {
|
|
150
|
-
warn("RTK bootstrap failed — continuing sync without it")
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (ctx.services.deps.probe("rtk").state === "missing") return
|
|
154
|
-
if (!existsSync(p(claudeDir, "RTK.md"))) {
|
|
155
|
-
if (ctx.dryRun) {
|
|
156
|
-
echo("[dry-run] rtk init --global (RTK.md missing; runs before the settings merge, which normalizes rtk's settings rewrite)")
|
|
157
|
-
return
|
|
158
|
-
}
|
|
159
|
-
// Plain command under bash set -e: a nonzero `rtk init` aborts the whole
|
|
160
|
-
// sync before the success log and before any settings/plugin mutation.
|
|
161
|
-
const res = spawnSync("rtk", ["init", "--global"], { stdio: "inherit" })
|
|
162
|
-
if (res.error !== undefined || res.status !== 0) throw new ExitError(res.status ?? 1)
|
|
163
|
-
change("RTK initialized (RTK.md generated; the following settings merge re-asserts the SoT hooks)")
|
|
164
|
-
} else if (!ctx.dryRun) {
|
|
165
|
-
verbose("RTK already initialized")
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
86
|
// ----------------------------------------------------------- runtime ----
|
|
170
87
|
|
|
171
88
|
function syncClaudeRuntime(ctx: Ctx, runtime: ClaudeRuntimeState): void {
|
|
@@ -448,10 +365,11 @@ const REMOVED_MANIFEST = {
|
|
|
448
365
|
"env.CLAUDE_CODE_DISABLE_1M_CONTEXT",
|
|
449
366
|
"env.CLAUDE_CODE_FORK_SUBAGENT",
|
|
450
367
|
"env.CLAUDE_CODE_EFFORT_LEVEL",
|
|
451
|
-
"enabledPlugins.session-relay@docks"
|
|
368
|
+
"enabledPlugins.session-relay@docks",
|
|
369
|
+
"hooks.PreToolUse"
|
|
452
370
|
],
|
|
453
371
|
permissionRules: {
|
|
454
|
-
allow: ["Write(./)"],
|
|
372
|
+
allow: ["Write(./)", "Bash(rtk *)"],
|
|
455
373
|
deny: ["Write(**/.env)", "Write(**/.env.local)", "Write(**/secrets/**)"]
|
|
456
374
|
},
|
|
457
375
|
claudeJsonKeys: [] as Array<string>,
|
|
@@ -604,9 +522,9 @@ function syncRemovals(ctx: Ctx, claudeDir: string, runtime: ClaudeRuntimeState):
|
|
|
604
522
|
|
|
605
523
|
// -------------------------------------------------------------- plugins ----
|
|
606
524
|
|
|
607
|
-
function cli(args: Array<string>): { ok: boolean; out: string } {
|
|
608
|
-
const res =
|
|
609
|
-
return { ok: res.error === undefined && res.
|
|
525
|
+
async function cli(args: Array<string>): Promise<{ ok: boolean; out: string }> {
|
|
526
|
+
const res = await spawnProcess("claude", args, { stdio: ["ignore", "pipe", "pipe"] })
|
|
527
|
+
return { ok: res.error === undefined && res.exitCode === 0, out: `${res.stdout}${res.stderr}` }
|
|
610
528
|
}
|
|
611
529
|
|
|
612
530
|
function readJsonFile(file: string): Json | undefined {
|
|
@@ -645,8 +563,8 @@ function nonUserScopeMarketplaces(installedDoc: Json | undefined): Set<string> {
|
|
|
645
563
|
return marketplaces
|
|
646
564
|
}
|
|
647
565
|
|
|
648
|
-
function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
649
|
-
const { change, echo, verbose, warn } = ctx.services.logger
|
|
566
|
+
async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
567
|
+
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
650
568
|
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
651
569
|
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
652
570
|
|
|
@@ -687,7 +605,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
687
605
|
const known = readJsonFile(knownMarketplaces)
|
|
688
606
|
if (known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false) continue
|
|
689
607
|
const repo = isObject(mpValue) && isObject(mpValue["source"]) ? String((mpValue["source"] as { [k: string]: Json })["repo"] ?? "") : ""
|
|
690
|
-
|
|
608
|
+
progress(`Adding marketplace ${mpName}...`)
|
|
609
|
+
const marketplaceResult = await cli(["plugin", "marketplace", "add", repo])
|
|
610
|
+
clearProgress()
|
|
611
|
+
if (marketplaceResult.ok) {
|
|
691
612
|
addedMp++
|
|
692
613
|
} else {
|
|
693
614
|
warn(`Failed to add marketplace: ${mpName} (${repo})`)
|
|
@@ -695,17 +616,25 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
695
616
|
}
|
|
696
617
|
}
|
|
697
618
|
|
|
698
|
-
// Pass 2 — install SoT-enabled plugins missing at user scope (jq keys[] sorts)
|
|
619
|
+
// Pass 2 — install SoT-enabled plugins missing at user scope (jq keys[] sorts),
|
|
620
|
+
// refreshing each source marketplace once so the install resolves a current snapshot.
|
|
699
621
|
let addedPl = 0
|
|
700
622
|
let f2 = 0
|
|
701
|
-
|
|
623
|
+
const refreshedMarketplaces = new Set<string>()
|
|
702
624
|
for (const pluginId of sortedKeys(sotPlugins)) {
|
|
703
625
|
if (pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
626
|
+
const separator = pluginId.lastIndexOf("@")
|
|
627
|
+
const mpName = separator > 0 ? pluginId.slice(separator + 1) : ""
|
|
628
|
+
if (mpName !== "" && !refreshedMarketplaces.has(mpName)) {
|
|
629
|
+
progress(`Refreshing marketplace ${mpName}...`)
|
|
630
|
+
await cli(["plugin", "marketplace", "update", mpName])
|
|
631
|
+
clearProgress()
|
|
632
|
+
refreshedMarketplaces.add(mpName)
|
|
707
633
|
}
|
|
708
|
-
|
|
634
|
+
progress(`Installing plugin ${pluginId}...`)
|
|
635
|
+
const installResult = await cli(["plugin", "install", pluginId])
|
|
636
|
+
clearProgress()
|
|
637
|
+
if (installResult.ok) {
|
|
709
638
|
addedPl++
|
|
710
639
|
} else {
|
|
711
640
|
warn(`Failed to install plugin: ${pluginId}`)
|
|
@@ -717,12 +646,32 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
717
646
|
const installedDoc = readJsonFile(installedPlugins)
|
|
718
647
|
const installedKeys = installedDoc !== undefined && isObject(installedDoc) ? sortedKeys(installedDoc["plugins"]) : []
|
|
719
648
|
const nonUserMarketplaces = nonUserScopeMarketplaces(installedDoc)
|
|
720
|
-
//
|
|
721
|
-
|
|
649
|
+
// Kit-owned plugin IDs: SoT-declared plus this run's --claude-plugin opt-ins.
|
|
650
|
+
const kitPluginIds = new Set<string>(Object.keys(sotPlugins))
|
|
651
|
+
if (ctx.claudePlugins.includes("supabase")) kitPluginIds.add("supabase@claude-plugins-official")
|
|
652
|
+
if (ctx.claudePlugins.includes("n8n")) kitPluginIds.add("n8n-mcp-skills@n8n-mcp-skills")
|
|
653
|
+
|
|
654
|
+
// Kit-owned marketplaces: SoT-declared plus every marketplace those plugins come from.
|
|
655
|
+
const kitMarketplaces = new Set<string>(Object.keys(sotMarketplaces))
|
|
656
|
+
for (const pluginId of kitPluginIds) {
|
|
657
|
+
const separator = pluginId.lastIndexOf("@")
|
|
658
|
+
if (separator > 0) kitMarketplaces.add(pluginId.slice(separator + 1))
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Pass 3 — refresh the kit-owned marketplaces and plugins unless the update
|
|
662
|
+
// command selected its install-missing-only fast path.
|
|
722
663
|
if (!ctx.skipPluginRefresh) {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
664
|
+
for (const mpName of [...kitMarketplaces].sort(compareCodepoints)) {
|
|
665
|
+
progress(`Refreshing marketplace ${mpName}...`)
|
|
666
|
+
await cli(["plugin", "marketplace", "update", mpName])
|
|
667
|
+
clearProgress()
|
|
668
|
+
}
|
|
669
|
+
for (const pluginId of [...kitPluginIds].sort(compareCodepoints)) {
|
|
670
|
+
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
671
|
+
progress(`Updating plugin ${pluginId}...`)
|
|
672
|
+
const updateResult = await cli(["plugin", "update", pluginId, "--scope", "user"])
|
|
673
|
+
clearProgress()
|
|
674
|
+
if (updateResult.out.includes("Successfully updated")) updatedPl++
|
|
726
675
|
}
|
|
727
676
|
}
|
|
728
677
|
|
|
@@ -735,7 +684,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
735
684
|
for (const pluginId of installedKeys) {
|
|
736
685
|
if (isObject(sotPlugins) && Object.prototype.hasOwnProperty.call(sotPlugins, pluginId)) continue
|
|
737
686
|
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
738
|
-
|
|
687
|
+
progress(`Uninstalling plugin ${pluginId}...`)
|
|
688
|
+
const uninstallResult = await cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId])
|
|
689
|
+
clearProgress()
|
|
690
|
+
if (uninstallResult.ok) {
|
|
739
691
|
removedPl++
|
|
740
692
|
} else {
|
|
741
693
|
warn(`Failed to uninstall plugin: ${pluginId}`)
|
|
@@ -748,7 +700,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
748
700
|
if (nonUserMarketplaces.has(mpName)) continue
|
|
749
701
|
const declared = isObject(sotMarketplaces) ? sotMarketplaces[mpName] : undefined
|
|
750
702
|
if (declared !== undefined && declared !== null && declared !== false) continue
|
|
751
|
-
|
|
703
|
+
progress(`Removing marketplace ${mpName}...`)
|
|
704
|
+
const removeResult = await cli(["plugin", "marketplace", "remove", mpName])
|
|
705
|
+
clearProgress()
|
|
706
|
+
if (removeResult.ok) {
|
|
752
707
|
removedMp++
|
|
753
708
|
} else {
|
|
754
709
|
warn(`Failed to remove marketplace: ${mpName}`)
|
|
@@ -758,7 +713,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
758
713
|
}
|
|
759
714
|
|
|
760
715
|
// Pass 6 — re-assert SoT enabled-state in the user settings.
|
|
761
|
-
if (reassertEnabledState(ctx, repoObj, p(claudeDir, "settings.json"))) {
|
|
716
|
+
if (await reassertEnabledState(ctx, repoObj, p(claudeDir, "settings.json"))) {
|
|
762
717
|
change("Plugin enable-state re-asserted from SoT in settings.json")
|
|
763
718
|
ctx.nextStepTriggers.claudePlugins = true
|
|
764
719
|
}
|
|
@@ -775,7 +730,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
775
730
|
}
|
|
776
731
|
}
|
|
777
732
|
|
|
778
|
-
function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSettingsFile: string): boolean {
|
|
733
|
+
async function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSettingsFile: string): Promise<boolean> {
|
|
779
734
|
const { warn } = ctx.services.logger
|
|
780
735
|
if (!existsSync(userSettingsFile)) return false
|
|
781
736
|
const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {}
|
|
@@ -786,7 +741,7 @@ function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSett
|
|
|
786
741
|
const user = readJsonFile(userSettingsFile)
|
|
787
742
|
const enabled = user !== undefined && isObject(user) && isObject(user["enabledPlugins"]) ? (user["enabledPlugins"] as { [k: string]: Json })[pluginId] : undefined
|
|
788
743
|
if (enabled !== true) continue
|
|
789
|
-
if (cli(["plugin", "disable", pluginId]).ok) {
|
|
744
|
+
if ((await cli(["plugin", "disable", pluginId])).ok) {
|
|
790
745
|
cliDisabled = true
|
|
791
746
|
} else {
|
|
792
747
|
warn(`Failed to disable SoT-false plugin: ${pluginId} (will retry next sync)`)
|
|
@@ -809,8 +764,8 @@ function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSett
|
|
|
809
764
|
|
|
810
765
|
// ------------------------------------------------------ optional plugins ----
|
|
811
766
|
|
|
812
|
-
function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): boolean {
|
|
813
|
-
const { change, verbose, warn } = ctx.services.logger
|
|
767
|
+
async function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): Promise<boolean> {
|
|
768
|
+
const { change, clearProgress, progress, verbose, warn } = ctx.services.logger
|
|
814
769
|
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
815
770
|
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
816
771
|
const mpName = pluginId.slice(pluginId.lastIndexOf("@") + 1)
|
|
@@ -820,7 +775,7 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
|
|
|
820
775
|
const known = readJsonFile(knownMarketplaces)
|
|
821
776
|
const has = known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false
|
|
822
777
|
if (!has) {
|
|
823
|
-
if (!cli(["plugin", "marketplace", "add", marketplaceRepo]).ok) {
|
|
778
|
+
if (!(await cli(["plugin", "marketplace", "add", marketplaceRepo])).ok) {
|
|
824
779
|
warn(`Failed to add marketplace ${marketplaceRepo} for ${pluginId}`)
|
|
825
780
|
return false
|
|
826
781
|
}
|
|
@@ -830,7 +785,10 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
|
|
|
830
785
|
|
|
831
786
|
const wasInstalled = pluginUserScopeInstalled(installedPlugins, pluginId)
|
|
832
787
|
if (!wasInstalled) {
|
|
833
|
-
|
|
788
|
+
progress(`Installing plugin ${pluginId}...`)
|
|
789
|
+
const installResult = await cli(["plugin", "install", pluginId])
|
|
790
|
+
clearProgress()
|
|
791
|
+
if (!installResult.ok) {
|
|
834
792
|
if (marketplaceAdded) change(`Optional plugin ${pluginId}: marketplace added (install failed — will retry next sync)`)
|
|
835
793
|
warn(`Failed to install optional plugin ${pluginId}`)
|
|
836
794
|
return marketplaceAdded
|
|
@@ -843,7 +801,7 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
|
|
|
843
801
|
? (settingsDoc["enabledPlugins"] as { [k: string]: Json })[pluginId] === true
|
|
844
802
|
: false
|
|
845
803
|
|
|
846
|
-
if (!cli(["plugin", "enable", pluginId]).ok) {
|
|
804
|
+
if (!(await cli(["plugin", "enable", pluginId])).ok) {
|
|
847
805
|
if (marketplaceAdded || !wasInstalled) change(`Optional plugin ${pluginId}: installed (enable failed — will retry next sync)`)
|
|
848
806
|
warn(`Failed to enable optional plugin ${pluginId}`)
|
|
849
807
|
return marketplaceAdded || !wasInstalled
|
|
@@ -854,7 +812,7 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
|
|
|
854
812
|
return changed
|
|
855
813
|
}
|
|
856
814
|
|
|
857
|
-
function syncOptionalPlugins(ctx: Ctx, claudeDir: string): void {
|
|
815
|
+
async function syncOptionalPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
858
816
|
const { echo, warn } = ctx.services.logger
|
|
859
817
|
if (ctx.claudePlugins.length === 0) return
|
|
860
818
|
|
|
@@ -874,10 +832,10 @@ function syncOptionalPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
874
832
|
}
|
|
875
833
|
|
|
876
834
|
if (ctx.claudePlugins.includes("supabase")) {
|
|
877
|
-
if (enableOptionalPlugin(ctx, claudeDir, "supabase@claude-plugins-official", "")) ctx.nextStepTriggers.claudePlugins = true
|
|
835
|
+
if (await enableOptionalPlugin(ctx, claudeDir, "supabase@claude-plugins-official", "")) ctx.nextStepTriggers.claudePlugins = true
|
|
878
836
|
}
|
|
879
837
|
if (ctx.claudePlugins.includes("n8n")) {
|
|
880
|
-
if (enableOptionalPlugin(ctx, claudeDir, "n8n-mcp-skills@n8n-mcp-skills", "czlonkowski/n8n-skills")) ctx.nextStepTriggers.claudePlugins = true
|
|
838
|
+
if (await enableOptionalPlugin(ctx, claudeDir, "n8n-mcp-skills@n8n-mcp-skills", "czlonkowski/n8n-skills")) ctx.nextStepTriggers.claudePlugins = true
|
|
881
839
|
}
|
|
882
840
|
}
|
|
883
841
|
|
|
@@ -888,8 +846,8 @@ function lspPkg(ctx: Ctx, tool: string, pkg: string): string {
|
|
|
888
846
|
return v !== "" ? `${pkg}@${v}` : pkg
|
|
889
847
|
}
|
|
890
848
|
|
|
891
|
-
function syncLspServers(ctx: Ctx): void {
|
|
892
|
-
const { change, echo, verbose, warn } = ctx.services.logger
|
|
849
|
+
async function syncLspServers(ctx: Ctx): Promise<void> {
|
|
850
|
+
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
893
851
|
const sot = parseJson(payloadText("SoT/.claude/settings.json"))
|
|
894
852
|
const enabled = sot !== undefined && isObject(sot) && isObject(sot["enabledPlugins"]) ? sot["enabledPlugins"] : undefined
|
|
895
853
|
if (enabled === undefined) return
|
|
@@ -929,7 +887,10 @@ function syncLspServers(ctx: Ctx): void {
|
|
|
929
887
|
}
|
|
930
888
|
|
|
931
889
|
verbose(`Installing LSP servers via npm: ${specs}...`)
|
|
932
|
-
|
|
890
|
+
progress(`Installing LSP servers via npm: ${specs}...`)
|
|
891
|
+
const installResult = await spawnProcess("npm", ["install", "-g", ...missing], { stdio: "ignore" })
|
|
892
|
+
clearProgress()
|
|
893
|
+
if (installResult.exitCode === 0) {
|
|
933
894
|
change(`LSP servers installed (${specs})`)
|
|
934
895
|
ctx.nextStepTriggers.claudeRestart = true
|
|
935
896
|
} else {
|
|
@@ -949,12 +910,6 @@ export function claudeSummary(ctx: Ctx, runtime: ClaudeRuntimeState): void {
|
|
|
949
910
|
} else {
|
|
950
911
|
echo("Hooks: migration deferred (Bun unavailable; existing hook/statusline settings preserved)")
|
|
951
912
|
}
|
|
952
|
-
if (ctx.services.deps.probe("rtk").state === "present") {
|
|
953
|
-
const version = ctx.services.deps.version("rtk")
|
|
954
|
-
echo(`RTK: ${version !== "" ? version : "installed"}`)
|
|
955
|
-
} else {
|
|
956
|
-
echo("RTK: not installed")
|
|
957
|
-
}
|
|
958
913
|
if (ctx.services.deps.probe("claude").state === "present") {
|
|
959
914
|
const installed = readJsonFile(p(claudeDir, "plugins", "installed_plugins.json"))
|
|
960
915
|
const count = installed !== undefined && isObject(installed) && isObject(installed["plugins"]) ? Object.keys(installed["plugins"]).length : 0
|
|
@@ -3,21 +3,20 @@
|
|
|
3
3
|
* avoid a TOML library because reformatting user configs would be a behavior
|
|
4
4
|
* change. Guard order, message strings, and backup behavior are golden-tested.
|
|
5
5
|
*/
|
|
6
|
-
import { spawnSync } from "node:child_process"
|
|
7
6
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
|
|
8
7
|
|
|
9
8
|
import { syncCodexEffort, syncCodexModel, replaceTopLevelSettingInFile } from "./codexToml"
|
|
10
|
-
import { p } from "./exec"
|
|
9
|
+
import { p, spawnProcess } from "./exec"
|
|
11
10
|
import type { Ctx } from "./index"
|
|
12
11
|
import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
13
12
|
import { payloadBytes, payloadDisplayPath, payloadPaths, payloadText, type PayloadPath } from "../payload"
|
|
14
13
|
|
|
15
|
-
export function codexSync(ctx: Ctx): void {
|
|
14
|
+
export async function codexSync(ctx: Ctx): Promise<void> {
|
|
16
15
|
const codexDir = p(ctx.home, ".codex")
|
|
17
16
|
const sotConfig = payloadText("SoT/.codex/config.toml")
|
|
18
17
|
const userConfig = p(codexDir, "config.toml")
|
|
19
18
|
|
|
20
|
-
ensureBubblewrap(ctx)
|
|
19
|
+
await ensureBubblewrap(ctx)
|
|
21
20
|
if (!ctx.dryRun) mkdirSync(codexDir, { recursive: true })
|
|
22
21
|
syncConfig(ctx, sotConfig, userConfig)
|
|
23
22
|
syncCodexModel(ctx, ctx.codexModel)
|
|
@@ -25,13 +24,13 @@ export function codexSync(ctx: Ctx): void {
|
|
|
25
24
|
syncRules(ctx, payloadPaths("SoT/.codex/rules/"), p(codexDir, "rules"))
|
|
26
25
|
syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"))
|
|
27
26
|
syncMarketplace(ctx, payloadText("SoT/.codex/plugins/marketplace.json"), p(ctx.agentsDir, "plugins", "marketplace.json"))
|
|
28
|
-
removeLegacyDocksMarketplace(ctx, userConfig)
|
|
29
|
-
syncPlugins(ctx, sotConfig)
|
|
27
|
+
await removeLegacyDocksMarketplace(ctx, userConfig)
|
|
28
|
+
await syncPlugins(ctx, sotConfig)
|
|
30
29
|
}
|
|
31
30
|
|
|
32
31
|
// ---------------------------------------------------------- bubblewrap ----
|
|
33
32
|
|
|
34
|
-
function ensureBubblewrap(ctx: Ctx): void {
|
|
33
|
+
async function ensureBubblewrap(ctx: Ctx): Promise<void> {
|
|
35
34
|
const { change, echo, warn } = ctx.services.logger
|
|
36
35
|
if (!bwrapSupportedOs(ctx)) return
|
|
37
36
|
|
|
@@ -42,9 +41,9 @@ function ensureBubblewrap(ctx: Ctx): void {
|
|
|
42
41
|
|
|
43
42
|
if (ctx.services.deps.probe("bwrap").state === "present") return
|
|
44
43
|
|
|
45
|
-
if (ctx.
|
|
44
|
+
if (ctx.skipBubblewrap) {
|
|
46
45
|
warn(
|
|
47
|
-
"bubblewrap not installed (--skip-
|
|
46
|
+
"bubblewrap not installed (--skip-bubblewrap skips auto-install). Codex may use its bundled helper if user namespaces work; recommended install: sudo apt install -y bubblewrap"
|
|
48
47
|
)
|
|
49
48
|
return
|
|
50
49
|
}
|
|
@@ -58,8 +57,10 @@ function ensureBubblewrap(ctx: Ctx): void {
|
|
|
58
57
|
}
|
|
59
58
|
|
|
60
59
|
warn(`bubblewrap not installed - recommended for Codex Linux sandbox. Running: ${pmInstall} (sudo prompt may appear)`)
|
|
61
|
-
const
|
|
62
|
-
|
|
60
|
+
const runInstaller = () =>
|
|
61
|
+
spawnProcess("bash", ["-c", pmInstall], { stdio: ["inherit", "inherit", "inherit"] })
|
|
62
|
+
const res = await (ctx.terminalLease?.withExclusive(runInstaller) ?? runInstaller())
|
|
63
|
+
if (res.exitCode !== 0) {
|
|
63
64
|
warn(`Failed to auto-install bubblewrap. Install manually: ${pmInstall}`)
|
|
64
65
|
return
|
|
65
66
|
}
|
|
@@ -69,8 +70,8 @@ function ensureBubblewrap(ctx: Ctx): void {
|
|
|
69
70
|
return
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
if (
|
|
73
|
-
change(`bubblewrap installed and functional (${ctx.services.deps.version("bwrap")})`)
|
|
73
|
+
if ((await spawnProcess("unshare", ["-Ur", "true"], { stdio: "ignore" })).exitCode === 0) {
|
|
74
|
+
change(`bubblewrap installed and functional (${await ctx.services.deps.version("bwrap")})`)
|
|
74
75
|
} else {
|
|
75
76
|
warn(
|
|
76
77
|
"bubblewrap installed but unprivileged user namespaces appear blocked. On Ubuntu 24.04+, prefer loading the AppArmor bwrap-userns-restrict profile; fallback: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0"
|
|
@@ -411,7 +412,7 @@ export function marketplaceSource(marketplace: string, configFile: string): stri
|
|
|
411
412
|
return ""
|
|
412
413
|
}
|
|
413
414
|
|
|
414
|
-
function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): void {
|
|
415
|
+
async function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): Promise<void> {
|
|
415
416
|
const { change, echo, warn } = ctx.services.logger
|
|
416
417
|
if (ctx.dryRun) {
|
|
417
418
|
echo("[dry-run] remove legacy configured Codex Docks marketplace when personal marketplace is deployed")
|
|
@@ -422,8 +423,8 @@ function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): void {
|
|
|
422
423
|
|
|
423
424
|
const source = marketplaceSource("docks", userConfig)
|
|
424
425
|
if (source !== "https://github.com/DocksDocks/docks.git" && source !== "DocksDocks/docks") return
|
|
425
|
-
const res =
|
|
426
|
-
if (res.error === undefined && res.
|
|
426
|
+
const res = await spawnProcess("codex", ["plugin", "marketplace", "remove", "docks"], { stdio: "ignore" })
|
|
427
|
+
if (res.error === undefined && res.exitCode === 0) {
|
|
427
428
|
change("Removed legacy configured Codex Docks marketplace; using personal marketplace file")
|
|
428
429
|
ctx.nextStepTriggers.codexRestart = true
|
|
429
430
|
} else {
|
|
@@ -474,13 +475,12 @@ function manualPluginRefreshCommand(sotConfigText: string): string {
|
|
|
474
475
|
return first !== undefined ? `codex plugin add ${first}` : "codex plugin add <plugin@marketplace>"
|
|
475
476
|
}
|
|
476
477
|
|
|
477
|
-
function installedPluginIdsFromCli(): Set<string> | undefined {
|
|
478
|
-
const result =
|
|
479
|
-
encoding: "utf8",
|
|
478
|
+
async function installedPluginIdsFromCli(): Promise<Set<string> | undefined> {
|
|
479
|
+
const result = await spawnProcess("codex", ["plugin", "list", "--json"], {
|
|
480
480
|
stdio: ["ignore", "pipe", "ignore"]
|
|
481
481
|
})
|
|
482
|
-
if (result.error !== undefined || result.
|
|
483
|
-
const value = parseJson(result.stdout
|
|
482
|
+
if (result.error !== undefined || result.exitCode !== 0) return undefined
|
|
483
|
+
const value = parseJson(result.stdout)
|
|
484
484
|
if (value === undefined || !isObject(value) || !Array.isArray(value["installed"])) return undefined
|
|
485
485
|
const ids = new Set<string>()
|
|
486
486
|
for (const row of value["installed"]) {
|
|
@@ -490,8 +490,8 @@ function installedPluginIdsFromCli(): Set<string> | undefined {
|
|
|
490
490
|
return ids
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
-
function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
494
|
-
const { change, echo, verbose, warn } = ctx.services.logger
|
|
493
|
+
async function syncPlugins(ctx: Ctx, sotConfigText: string): Promise<void> {
|
|
494
|
+
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
495
495
|
if (ctx.dryRun) {
|
|
496
496
|
echo(
|
|
497
497
|
ctx.skipPluginRefresh
|
|
@@ -519,7 +519,9 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
|
519
519
|
const desiredPluginIds = enabledPluginIdsFromText(sotConfigText)
|
|
520
520
|
let pluginIds = desiredPluginIds
|
|
521
521
|
if (ctx.skipPluginRefresh) {
|
|
522
|
-
|
|
522
|
+
progress("Checking installed Codex plugins...")
|
|
523
|
+
const installedPluginIds = await installedPluginIdsFromCli()
|
|
524
|
+
clearProgress()
|
|
523
525
|
if (installedPluginIds === undefined) {
|
|
524
526
|
warn("Codex plugin inventory unavailable — falling back to the full refresh path")
|
|
525
527
|
} else {
|
|
@@ -530,9 +532,11 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
|
530
532
|
let refreshed = 0
|
|
531
533
|
let failed = 0
|
|
532
534
|
for (const pluginId of pluginIds) {
|
|
533
|
-
|
|
534
|
-
const
|
|
535
|
-
|
|
535
|
+
progress(`Updating Codex plugin ${pluginId}...`)
|
|
536
|
+
const res = await spawnProcess("codex", ["plugin", "add", pluginId], { stdio: ["ignore", "pipe", "pipe"] })
|
|
537
|
+
clearProgress()
|
|
538
|
+
const addOut = `${res.stdout}${res.stderr}`
|
|
539
|
+
if (res.error === undefined && res.exitCode === 0) {
|
|
536
540
|
refreshed++
|
|
537
541
|
} else if (addOut.includes("could not find a Codex CLI binary")) {
|
|
538
542
|
warn(
|