docks-kit 0.14.1 → 0.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -14
- package/README.md +11 -23
- package/cli/docs/flags.md +1 -2
- package/cli/docs/install.md +2 -2
- package/cli/docs/platforms.md +7 -12
- package/cli/docs/sync-layers.md +17 -25
- package/cli/docs/toolchain.md +12 -32
- package/cli/src/commands/status.ts +2 -9
- 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 +24 -15
- package/cli/src/engine-native/claudeSync.ts +84 -116
- package/cli/src/engine-native/codexSync.ts +52 -11
- package/cli/src/engine-native/deps.ts +21 -51
- package/cli/src/engine-native/index.ts +33 -8
- package/cli/src/engine-native/logger.ts +73 -10
- package/cli/src/engine-native/modes.ts +2 -10
- package/cli/src/engine-native/parseArgs.ts +5 -5
- package/cli/src/engine-native/skillsSync.ts +18 -52
- package/cli/src/engine-native/toolchain.ts +7 -5
- package/cli/src/generated/sotPayload.ts +6 -6
- package/package.json +1 -1
- package/cli/src/engine-native/sessionRelayCli.ts +0 -262
- package/cli/src/engine-native/sessionRelayReadiness.ts +0 -94
|
@@ -1,7 +1,8 @@
|
|
|
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
7
|
import { spawnSync } from "node:child_process"
|
|
7
8
|
import {
|
|
@@ -15,7 +16,6 @@ import {
|
|
|
15
16
|
rmSync,
|
|
16
17
|
writeFileSync
|
|
17
18
|
} from "node:fs"
|
|
18
|
-
import { tmpdir } from "node:os"
|
|
19
19
|
import { bunBootstrap } from "./bun"
|
|
20
20
|
import {
|
|
21
21
|
syncClaudeAdvisor,
|
|
@@ -26,12 +26,10 @@ import { claudeRuntimePaths, materializeClaudeSettings, type ClaudeRuntimePaths
|
|
|
26
26
|
import { p, writeBytesIfChanged, writeTextIfChanged } from "./exec"
|
|
27
27
|
import type { Ctx } from "./index"
|
|
28
28
|
import { compareCodepoints, deepMerge, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
29
|
-
import type { EngineServices } from "./services"
|
|
30
29
|
import { ExitError } from "./parseArgs"
|
|
31
30
|
import { mergeSettings, reconcileSettings } from "./settings"
|
|
32
|
-
import {
|
|
31
|
+
import { field } from "./toolchain"
|
|
33
32
|
import { payloadBytes, payloadDisplayPath, payloadText } from "../payload"
|
|
34
|
-
import { ensureSessionRelayCli } from "./sessionRelayCli"
|
|
35
33
|
|
|
36
34
|
export type ClaudeRuntimeState =
|
|
37
35
|
| { readonly kind: "ready"; readonly paths: ClaudeRuntimePaths }
|
|
@@ -49,7 +47,6 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
|
|
|
49
47
|
)
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
syncRtk(ctx, claudeDir)
|
|
53
50
|
const bun = bunBootstrap(ctx, ctx.services)
|
|
54
51
|
const runtime: ClaudeRuntimeState = bun.kind === "ready"
|
|
55
52
|
? { kind: "ready", paths: claudeRuntimePaths(claudeDir, bun.executable) }
|
|
@@ -81,93 +78,12 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
|
|
|
81
78
|
syncClaudeAdvisor(ctx, ctx.claudeAdvisor)
|
|
82
79
|
syncClaudeJson(ctx)
|
|
83
80
|
syncConnectorEnv(ctx)
|
|
84
|
-
ensureSessionRelayCli(ctx)
|
|
85
81
|
syncPlugins(ctx, claudeDir)
|
|
86
82
|
syncOptionalPlugins(ctx, claudeDir)
|
|
87
83
|
syncLspServers(ctx)
|
|
88
84
|
return runtime
|
|
89
85
|
}
|
|
90
86
|
|
|
91
|
-
// ------------------------------------------------------------------ rtk ----
|
|
92
|
-
|
|
93
|
-
type RtkInstaller = ((mode: "install" | "upgrade", version: string, services: EngineServices) => number) & {
|
|
94
|
-
readonly prerequisite: (services: EngineServices) => number | undefined
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** RTK toolchain install callback with the shared contextual curl boundary. */
|
|
98
|
-
export function rtkInstall(ctx: Ctx, missingCurlContext: string, missingCurlExit: number): RtkInstaller {
|
|
99
|
-
const prerequisite = (services: EngineServices): number | undefined => {
|
|
100
|
-
if (services.deps.probe("curl").state === "present") return undefined
|
|
101
|
-
services.deps.warnMissing("curl", services.logger, missingCurlContext)
|
|
102
|
-
return missingCurlExit
|
|
103
|
-
}
|
|
104
|
-
const install = (mode: "install" | "upgrade", version: string, services: EngineServices): number => {
|
|
105
|
-
const blocked = prerequisite(services)
|
|
106
|
-
if (blocked !== undefined) return blocked
|
|
107
|
-
const { change, err, verbose, warn } = services.logger
|
|
108
|
-
const installerRef = version !== "" ? `refs/tags/v${version}` : "refs/heads/master"
|
|
109
|
-
|
|
110
|
-
if (mode === "upgrade") verbose(`Upgrading RTK${version !== "" ? ` to ${version}` : ""}...`)
|
|
111
|
-
else warn(`RTK not found. Installing${version !== "" ? ` ${version}` : ""}...`)
|
|
112
|
-
const installer = p(tmpdir(), `rtk-install-${process.pid}.sh`)
|
|
113
|
-
const dl = spawnSync("curl", ["-fsSL", `https://raw.githubusercontent.com/rtk-ai/rtk/${installerRef}/install.sh`, "-o", installer], {
|
|
114
|
-
stdio: "inherit"
|
|
115
|
-
})
|
|
116
|
-
if (dl.error === undefined && dl.status === 0) {
|
|
117
|
-
spawnSync("bash", [installer], {
|
|
118
|
-
stdio: "inherit",
|
|
119
|
-
env: { ...process.env, RTK_VERSION: version !== "" ? `v${version}` : "" }
|
|
120
|
-
})
|
|
121
|
-
}
|
|
122
|
-
rmSync(installer, { force: true })
|
|
123
|
-
process.env["PATH"] = `${ctx.home}/.local/bin:${ctx.home}/.cargo/bin:${process.env["PATH"] ?? ""}`
|
|
124
|
-
const installed = services.deps.version("rtk")
|
|
125
|
-
if (services.deps.probe("rtk").state === "present") {
|
|
126
|
-
change(`RTK ready (${installed !== "" ? installed : "version unknown"})`)
|
|
127
|
-
return 0
|
|
128
|
-
}
|
|
129
|
-
err("RTK install failed. Install manually: https://github.com/rtk-ai/rtk")
|
|
130
|
-
return 1
|
|
131
|
-
}
|
|
132
|
-
return Object.assign(install, { prerequisite })
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function ensureRtk(ctx: Ctx, missingCurlContext: string, missingCurlExit: number): number {
|
|
136
|
-
const installer = rtkInstall(ctx, missingCurlContext, missingCurlExit)
|
|
137
|
-
if (ctx.services.deps.probe("rtk").state === "missing") {
|
|
138
|
-
const blocked = installer.prerequisite(ctx.services)
|
|
139
|
-
if (blocked !== undefined) return blocked
|
|
140
|
-
}
|
|
141
|
-
return ensure(ctx, "rtk", installer)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function syncRtk(ctx: Ctx, claudeDir: string): void {
|
|
145
|
-
const { change, echo, verbose, warn } = ctx.services.logger
|
|
146
|
-
if (ctx.skipRtk) {
|
|
147
|
-
warn("Skipping RTK (--skip-rtk)")
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (ensureRtk(ctx, "cannot download RTK installer; continuing sync without RTK", 0) !== 0) {
|
|
152
|
-
warn("RTK bootstrap failed — continuing sync without it")
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (ctx.services.deps.probe("rtk").state === "missing") return
|
|
156
|
-
if (!existsSync(p(claudeDir, "RTK.md"))) {
|
|
157
|
-
if (ctx.dryRun) {
|
|
158
|
-
echo("[dry-run] rtk init --global (RTK.md missing; runs before the settings merge, which normalizes rtk's settings rewrite)")
|
|
159
|
-
return
|
|
160
|
-
}
|
|
161
|
-
// Plain command under bash set -e: a nonzero `rtk init` aborts the whole
|
|
162
|
-
// sync before the success log and before any settings/plugin mutation.
|
|
163
|
-
const res = spawnSync("rtk", ["init", "--global"], { stdio: "inherit" })
|
|
164
|
-
if (res.error !== undefined || res.status !== 0) throw new ExitError(res.status ?? 1)
|
|
165
|
-
change("RTK initialized (RTK.md generated; the following settings merge re-asserts the SoT hooks)")
|
|
166
|
-
} else if (!ctx.dryRun) {
|
|
167
|
-
verbose("RTK already initialized")
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
87
|
// ----------------------------------------------------------- runtime ----
|
|
172
88
|
|
|
173
89
|
function syncClaudeRuntime(ctx: Ctx, runtime: ClaudeRuntimeState): void {
|
|
@@ -449,13 +365,17 @@ const REMOVED_MANIFEST = {
|
|
|
449
365
|
"env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE",
|
|
450
366
|
"env.CLAUDE_CODE_DISABLE_1M_CONTEXT",
|
|
451
367
|
"env.CLAUDE_CODE_FORK_SUBAGENT",
|
|
452
|
-
"env.CLAUDE_CODE_EFFORT_LEVEL"
|
|
368
|
+
"env.CLAUDE_CODE_EFFORT_LEVEL",
|
|
369
|
+
"enabledPlugins.session-relay@docks",
|
|
370
|
+
"hooks.PreToolUse"
|
|
453
371
|
],
|
|
454
372
|
permissionRules: {
|
|
455
|
-
allow: ["Write(./)"],
|
|
373
|
+
allow: ["Write(./)", "Bash(rtk *)"],
|
|
456
374
|
deny: ["Write(**/.env)", "Write(**/.env.local)", "Write(**/secrets/**)"]
|
|
457
375
|
},
|
|
458
376
|
claudeJsonKeys: [] as Array<string>,
|
|
377
|
+
/** Home-relative artifacts the kit installed outside ~/.claude. */
|
|
378
|
+
homeFiles: [".local/bin/session-relay"],
|
|
459
379
|
runtimeReady: {
|
|
460
380
|
hooks: ["notify.sh"],
|
|
461
381
|
files: ["statusline.sh", "fetch-usage.sh"],
|
|
@@ -566,6 +486,17 @@ function syncRemovals(ctx: Ctx, claudeDir: string, runtime: ClaudeRuntimeState):
|
|
|
566
486
|
}
|
|
567
487
|
}
|
|
568
488
|
|
|
489
|
+
for (const rel of REMOVED_MANIFEST.homeFiles) {
|
|
490
|
+
const path = p(ctx.home, rel)
|
|
491
|
+
if (!existsSync(path)) continue
|
|
492
|
+
if (ctx.dryRun) {
|
|
493
|
+
echo(`[dry-run] rm ${path}`)
|
|
494
|
+
} else {
|
|
495
|
+
rmSync(path, { force: true })
|
|
496
|
+
filesRemoved++
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
569
500
|
const skeys = pruneJsonKeys(ctx, p(claudeDir, "settings.json"), settingsKeys)
|
|
570
501
|
const permissionRules = prunePermissionRules(
|
|
571
502
|
ctx,
|
|
@@ -634,7 +565,7 @@ function nonUserScopeMarketplaces(installedDoc: Json | undefined): Set<string> {
|
|
|
634
565
|
}
|
|
635
566
|
|
|
636
567
|
function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
637
|
-
const { change, echo, verbose, warn } = ctx.services.logger
|
|
568
|
+
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
638
569
|
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
639
570
|
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
640
571
|
|
|
@@ -675,7 +606,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
675
606
|
const known = readJsonFile(knownMarketplaces)
|
|
676
607
|
if (known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false) continue
|
|
677
608
|
const repo = isObject(mpValue) && isObject(mpValue["source"]) ? String((mpValue["source"] as { [k: string]: Json })["repo"] ?? "") : ""
|
|
678
|
-
|
|
609
|
+
progress(`Adding marketplace ${mpName}...`)
|
|
610
|
+
const marketplaceResult = cli(["plugin", "marketplace", "add", repo])
|
|
611
|
+
clearProgress()
|
|
612
|
+
if (marketplaceResult.ok) {
|
|
679
613
|
addedMp++
|
|
680
614
|
} else {
|
|
681
615
|
warn(`Failed to add marketplace: ${mpName} (${repo})`)
|
|
@@ -683,17 +617,25 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
683
617
|
}
|
|
684
618
|
}
|
|
685
619
|
|
|
686
|
-
// Pass 2 — install SoT-enabled plugins missing at user scope (jq keys[] sorts)
|
|
620
|
+
// Pass 2 — install SoT-enabled plugins missing at user scope (jq keys[] sorts),
|
|
621
|
+
// refreshing each source marketplace once so the install resolves a current snapshot.
|
|
687
622
|
let addedPl = 0
|
|
688
623
|
let f2 = 0
|
|
689
|
-
|
|
624
|
+
const refreshedMarketplaces = new Set<string>()
|
|
690
625
|
for (const pluginId of sortedKeys(sotPlugins)) {
|
|
691
626
|
if (pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
627
|
+
const separator = pluginId.lastIndexOf("@")
|
|
628
|
+
const mpName = separator > 0 ? pluginId.slice(separator + 1) : ""
|
|
629
|
+
if (mpName !== "" && !refreshedMarketplaces.has(mpName)) {
|
|
630
|
+
progress(`Refreshing marketplace ${mpName}...`)
|
|
631
|
+
cli(["plugin", "marketplace", "update", mpName])
|
|
632
|
+
clearProgress()
|
|
633
|
+
refreshedMarketplaces.add(mpName)
|
|
695
634
|
}
|
|
696
|
-
|
|
635
|
+
progress(`Installing plugin ${pluginId}...`)
|
|
636
|
+
const installResult = cli(["plugin", "install", pluginId])
|
|
637
|
+
clearProgress()
|
|
638
|
+
if (installResult.ok) {
|
|
697
639
|
addedPl++
|
|
698
640
|
} else {
|
|
699
641
|
warn(`Failed to install plugin: ${pluginId}`)
|
|
@@ -705,12 +647,32 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
705
647
|
const installedDoc = readJsonFile(installedPlugins)
|
|
706
648
|
const installedKeys = installedDoc !== undefined && isObject(installedDoc) ? sortedKeys(installedDoc["plugins"]) : []
|
|
707
649
|
const nonUserMarketplaces = nonUserScopeMarketplaces(installedDoc)
|
|
708
|
-
//
|
|
709
|
-
|
|
650
|
+
// Kit-owned plugin IDs: SoT-declared plus this run's --claude-plugin opt-ins.
|
|
651
|
+
const kitPluginIds = new Set<string>(Object.keys(sotPlugins))
|
|
652
|
+
if (ctx.claudePlugins.includes("supabase")) kitPluginIds.add("supabase@claude-plugins-official")
|
|
653
|
+
if (ctx.claudePlugins.includes("n8n")) kitPluginIds.add("n8n-mcp-skills@n8n-mcp-skills")
|
|
654
|
+
|
|
655
|
+
// Kit-owned marketplaces: SoT-declared plus every marketplace those plugins come from.
|
|
656
|
+
const kitMarketplaces = new Set<string>(Object.keys(sotMarketplaces))
|
|
657
|
+
for (const pluginId of kitPluginIds) {
|
|
658
|
+
const separator = pluginId.lastIndexOf("@")
|
|
659
|
+
if (separator > 0) kitMarketplaces.add(pluginId.slice(separator + 1))
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Pass 3 — refresh the kit-owned marketplaces and plugins unless the update
|
|
663
|
+
// command selected its install-missing-only fast path.
|
|
710
664
|
if (!ctx.skipPluginRefresh) {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
665
|
+
for (const mpName of [...kitMarketplaces].sort(compareCodepoints)) {
|
|
666
|
+
progress(`Refreshing marketplace ${mpName}...`)
|
|
667
|
+
cli(["plugin", "marketplace", "update", mpName])
|
|
668
|
+
clearProgress()
|
|
669
|
+
}
|
|
670
|
+
for (const pluginId of [...kitPluginIds].sort(compareCodepoints)) {
|
|
671
|
+
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
672
|
+
progress(`Updating plugin ${pluginId}...`)
|
|
673
|
+
const updateResult = cli(["plugin", "update", pluginId, "--scope", "user"])
|
|
674
|
+
clearProgress()
|
|
675
|
+
if (updateResult.out.includes("Successfully updated")) updatedPl++
|
|
714
676
|
}
|
|
715
677
|
}
|
|
716
678
|
|
|
@@ -723,7 +685,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
723
685
|
for (const pluginId of installedKeys) {
|
|
724
686
|
if (isObject(sotPlugins) && Object.prototype.hasOwnProperty.call(sotPlugins, pluginId)) continue
|
|
725
687
|
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
726
|
-
|
|
688
|
+
progress(`Uninstalling plugin ${pluginId}...`)
|
|
689
|
+
const uninstallResult = cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId])
|
|
690
|
+
clearProgress()
|
|
691
|
+
if (uninstallResult.ok) {
|
|
727
692
|
removedPl++
|
|
728
693
|
} else {
|
|
729
694
|
warn(`Failed to uninstall plugin: ${pluginId}`)
|
|
@@ -736,7 +701,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
736
701
|
if (nonUserMarketplaces.has(mpName)) continue
|
|
737
702
|
const declared = isObject(sotMarketplaces) ? sotMarketplaces[mpName] : undefined
|
|
738
703
|
if (declared !== undefined && declared !== null && declared !== false) continue
|
|
739
|
-
|
|
704
|
+
progress(`Removing marketplace ${mpName}...`)
|
|
705
|
+
const removeResult = cli(["plugin", "marketplace", "remove", mpName])
|
|
706
|
+
clearProgress()
|
|
707
|
+
if (removeResult.ok) {
|
|
740
708
|
removedMp++
|
|
741
709
|
} else {
|
|
742
710
|
warn(`Failed to remove marketplace: ${mpName}`)
|
|
@@ -798,7 +766,7 @@ function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSett
|
|
|
798
766
|
// ------------------------------------------------------ optional plugins ----
|
|
799
767
|
|
|
800
768
|
function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): boolean {
|
|
801
|
-
const { change, verbose, warn } = ctx.services.logger
|
|
769
|
+
const { change, clearProgress, progress, verbose, warn } = ctx.services.logger
|
|
802
770
|
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
803
771
|
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
804
772
|
const mpName = pluginId.slice(pluginId.lastIndexOf("@") + 1)
|
|
@@ -818,7 +786,10 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
|
|
|
818
786
|
|
|
819
787
|
const wasInstalled = pluginUserScopeInstalled(installedPlugins, pluginId)
|
|
820
788
|
if (!wasInstalled) {
|
|
821
|
-
|
|
789
|
+
progress(`Installing plugin ${pluginId}...`)
|
|
790
|
+
const installResult = cli(["plugin", "install", pluginId])
|
|
791
|
+
clearProgress()
|
|
792
|
+
if (!installResult.ok) {
|
|
822
793
|
if (marketplaceAdded) change(`Optional plugin ${pluginId}: marketplace added (install failed — will retry next sync)`)
|
|
823
794
|
warn(`Failed to install optional plugin ${pluginId}`)
|
|
824
795
|
return marketplaceAdded
|
|
@@ -877,7 +848,7 @@ function lspPkg(ctx: Ctx, tool: string, pkg: string): string {
|
|
|
877
848
|
}
|
|
878
849
|
|
|
879
850
|
function syncLspServers(ctx: Ctx): void {
|
|
880
|
-
const { change, echo, verbose, warn } = ctx.services.logger
|
|
851
|
+
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
881
852
|
const sot = parseJson(payloadText("SoT/.claude/settings.json"))
|
|
882
853
|
const enabled = sot !== undefined && isObject(sot) && isObject(sot["enabledPlugins"]) ? sot["enabledPlugins"] : undefined
|
|
883
854
|
if (enabled === undefined) return
|
|
@@ -917,7 +888,10 @@ function syncLspServers(ctx: Ctx): void {
|
|
|
917
888
|
}
|
|
918
889
|
|
|
919
890
|
verbose(`Installing LSP servers via npm: ${specs}...`)
|
|
920
|
-
|
|
891
|
+
progress(`Installing LSP servers via npm: ${specs}...`)
|
|
892
|
+
const installResult = spawnSync("npm", ["install", "-g", ...missing], { stdio: "ignore" })
|
|
893
|
+
clearProgress()
|
|
894
|
+
if (installResult.status === 0) {
|
|
921
895
|
change(`LSP servers installed (${specs})`)
|
|
922
896
|
ctx.nextStepTriggers.claudeRestart = true
|
|
923
897
|
} else {
|
|
@@ -937,12 +911,6 @@ export function claudeSummary(ctx: Ctx, runtime: ClaudeRuntimeState): void {
|
|
|
937
911
|
} else {
|
|
938
912
|
echo("Hooks: migration deferred (Bun unavailable; existing hook/statusline settings preserved)")
|
|
939
913
|
}
|
|
940
|
-
if (ctx.services.deps.probe("rtk").state === "present") {
|
|
941
|
-
const version = ctx.services.deps.version("rtk")
|
|
942
|
-
echo(`RTK: ${version !== "" ? version : "installed"}`)
|
|
943
|
-
} else {
|
|
944
|
-
echo("RTK: not installed")
|
|
945
|
-
}
|
|
946
914
|
if (ctx.services.deps.probe("claude").state === "present") {
|
|
947
915
|
const installed = readJsonFile(p(claudeDir, "plugins", "installed_plugins.json"))
|
|
948
916
|
const count = installed !== undefined && isObject(installed) && isObject(installed["plugins"]) ? Object.keys(installed["plugins"]).length : 0
|
|
@@ -10,9 +10,7 @@ import { syncCodexEffort, syncCodexModel, replaceTopLevelSettingInFile } from ".
|
|
|
10
10
|
import { p } from "./exec"
|
|
11
11
|
import type { Ctx } from "./index"
|
|
12
12
|
import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
13
|
-
import { sessionRelayReadiness } from "./sessionRelayReadiness"
|
|
14
13
|
import { payloadBytes, payloadDisplayPath, payloadPaths, payloadText, type PayloadPath } from "../payload"
|
|
15
|
-
import { ensureSessionRelayCli } from "./sessionRelayCli"
|
|
16
14
|
|
|
17
15
|
export function codexSync(ctx: Ctx): void {
|
|
18
16
|
const codexDir = p(ctx.home, ".codex")
|
|
@@ -28,7 +26,6 @@ export function codexSync(ctx: Ctx): void {
|
|
|
28
26
|
syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"))
|
|
29
27
|
syncMarketplace(ctx, payloadText("SoT/.codex/plugins/marketplace.json"), p(ctx.agentsDir, "plugins", "marketplace.json"))
|
|
30
28
|
removeLegacyDocksMarketplace(ctx, userConfig)
|
|
31
|
-
ensureSessionRelayCli(ctx)
|
|
32
29
|
syncPlugins(ctx, sotConfig)
|
|
33
30
|
}
|
|
34
31
|
|
|
@@ -45,9 +42,9 @@ function ensureBubblewrap(ctx: Ctx): void {
|
|
|
45
42
|
|
|
46
43
|
if (ctx.services.deps.probe("bwrap").state === "present") return
|
|
47
44
|
|
|
48
|
-
if (ctx.
|
|
45
|
+
if (ctx.skipBubblewrap) {
|
|
49
46
|
warn(
|
|
50
|
-
"bubblewrap not installed (--skip-
|
|
47
|
+
"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"
|
|
51
48
|
)
|
|
52
49
|
return
|
|
53
50
|
}
|
|
@@ -124,6 +121,7 @@ function syncConfig(ctx: Ctx, sotConfigText: string, userConfig: string): void {
|
|
|
124
121
|
writeFileSync(staging, before)
|
|
125
122
|
|
|
126
123
|
scrubDeprecatedFeatures(ctx, staging)
|
|
124
|
+
removeRetiredPluginTables(ctx, staging)
|
|
127
125
|
mergeTopLevelSettings(sotConfigText, staging)
|
|
128
126
|
mergeTableSettings(sotConfigText, staging)
|
|
129
127
|
|
|
@@ -184,6 +182,49 @@ function scrubDeprecatedFeatures(ctx: Ctx, userConfig: string): void {
|
|
|
184
182
|
change("Codex: scrubbed deprecated [features].use_legacy_landlock")
|
|
185
183
|
}
|
|
186
184
|
|
|
185
|
+
const PLUGIN_TABLE_HEADER = /^\[plugins\."([^"]+)"\][ \t]*$/
|
|
186
|
+
/** Plugin ids the kit retired; their deployed tables are stripped on every sync. */
|
|
187
|
+
const RETIRED_PLUGIN_IDS: Readonly<Record<string, true>> = { "session-relay@docks": true }
|
|
188
|
+
|
|
189
|
+
/** codex::remove_retired_plugin_tables — drop [plugins."<id>"] blocks for retired ids. */
|
|
190
|
+
export function removeRetiredPluginTablesText(content: string): string {
|
|
191
|
+
const lines = content.split("\n")
|
|
192
|
+
if (lines[lines.length - 1] === "") lines.pop()
|
|
193
|
+
let out = ""
|
|
194
|
+
let skipping = false
|
|
195
|
+
for (const line of lines) {
|
|
196
|
+
const header = PLUGIN_TABLE_HEADER.exec(line)
|
|
197
|
+
if (header !== null) {
|
|
198
|
+
skipping = RETIRED_PLUGIN_IDS[header[1]!] === true
|
|
199
|
+
if (skipping) continue
|
|
200
|
+
} else if (skipping) {
|
|
201
|
+
if (!line.startsWith("[")) continue
|
|
202
|
+
skipping = false
|
|
203
|
+
}
|
|
204
|
+
out += `${line}\n`
|
|
205
|
+
}
|
|
206
|
+
return out
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function retiredPluginTables(content: string): Array<string> {
|
|
210
|
+
return content
|
|
211
|
+
.split("\n")
|
|
212
|
+
.map((line) => PLUGIN_TABLE_HEADER.exec(line)?.[1])
|
|
213
|
+
.filter((id): id is string => id !== undefined && RETIRED_PLUGIN_IDS[id] === true)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function removeRetiredPluginTables(ctx: Ctx, userConfig: string): void {
|
|
217
|
+
const { change } = ctx.services.logger
|
|
218
|
+
if (!existsSync(userConfig)) return
|
|
219
|
+
const content = readFileSync(userConfig, "utf8")
|
|
220
|
+
const present = retiredPluginTables(content)
|
|
221
|
+
if (present.length === 0) return
|
|
222
|
+
|
|
223
|
+
writeFileSync(`${userConfig}.tmp`, removeRetiredPluginTablesText(content))
|
|
224
|
+
renameSync(`${userConfig}.tmp`, userConfig)
|
|
225
|
+
for (const id of present) change(`Codex: removed retired plugin table [plugins."${id}"]`)
|
|
226
|
+
}
|
|
227
|
+
|
|
187
228
|
function mergeTopLevelSettings(sotConfigText: string, userConfig: string): void {
|
|
188
229
|
for (const line of sotConfigText.split("\n")) {
|
|
189
230
|
if (line.startsWith("[")) break
|
|
@@ -407,7 +448,7 @@ export function enabledPluginIdsFromText(configText: string): Array<string> {
|
|
|
407
448
|
if (plugin !== "" && enabled) ids.push(plugin)
|
|
408
449
|
}
|
|
409
450
|
for (const line of configText.split("\n")) {
|
|
410
|
-
const m =
|
|
451
|
+
const m = PLUGIN_TABLE_HEADER.exec(line)
|
|
411
452
|
if (m !== null) {
|
|
412
453
|
flush()
|
|
413
454
|
plugin = m[1]!
|
|
@@ -450,7 +491,7 @@ function installedPluginIdsFromCli(): Set<string> | undefined {
|
|
|
450
491
|
}
|
|
451
492
|
|
|
452
493
|
function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
453
|
-
const { change, echo, verbose, warn } = ctx.services.logger
|
|
494
|
+
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
454
495
|
if (ctx.dryRun) {
|
|
455
496
|
echo(
|
|
456
497
|
ctx.skipPluginRefresh
|
|
@@ -478,7 +519,9 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
|
478
519
|
const desiredPluginIds = enabledPluginIdsFromText(sotConfigText)
|
|
479
520
|
let pluginIds = desiredPluginIds
|
|
480
521
|
if (ctx.skipPluginRefresh) {
|
|
522
|
+
progress("Checking installed Codex plugins...")
|
|
481
523
|
const installedPluginIds = installedPluginIdsFromCli()
|
|
524
|
+
clearProgress()
|
|
482
525
|
if (installedPluginIds === undefined) {
|
|
483
526
|
warn("Codex plugin inventory unavailable — falling back to the full refresh path")
|
|
484
527
|
} else {
|
|
@@ -489,7 +532,9 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
|
489
532
|
let refreshed = 0
|
|
490
533
|
let failed = 0
|
|
491
534
|
for (const pluginId of pluginIds) {
|
|
535
|
+
progress(`Updating Codex plugin ${pluginId}...`)
|
|
492
536
|
const res = spawnSync("codex", ["plugin", "add", pluginId], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
|
|
537
|
+
clearProgress()
|
|
493
538
|
const addOut = `${res.stdout ?? ""}${res.stderr ?? ""}`
|
|
494
539
|
if (res.error === undefined && res.status === 0) {
|
|
495
540
|
refreshed++
|
|
@@ -510,10 +555,6 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
|
510
555
|
if (refreshed > 0) {
|
|
511
556
|
change(`Codex plugins synced (plugins: ~${refreshed})`)
|
|
512
557
|
ctx.nextStepTriggers.codexRestart = true
|
|
513
|
-
const readiness = sessionRelayReadiness()
|
|
514
|
-
if (readiness.state !== "ready") {
|
|
515
|
-
warn(`Session Relay readiness unavailable after refresh: ${readiness.reason}`)
|
|
516
|
-
}
|
|
517
558
|
}
|
|
518
559
|
if (ctx.skipPluginRefresh && pluginIds.length === 0) verbose("Codex plugins already installed; refresh-only updates skipped")
|
|
519
560
|
if (failed > 0) warn(`${failed} Codex plugin operation(s) failed — re-run sync or install manually`)
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { homedir } from "node:os"
|
|
11
11
|
import { isAbsolute } from "node:path"
|
|
12
|
-
import { existsSync, readdirSync } from "node:fs"
|
|
13
12
|
|
|
14
13
|
import { capture, commandExists, p, which } from "./exec"
|
|
15
14
|
import { isObject, parseJson } from "./jq"
|
|
@@ -24,13 +23,9 @@ export type ToolId =
|
|
|
24
23
|
| "npx"
|
|
25
24
|
| "claude"
|
|
26
25
|
| "codex"
|
|
27
|
-
| "rtk"
|
|
28
26
|
| "bun"
|
|
29
27
|
| "bwrap"
|
|
30
|
-
| "agent-browser"
|
|
31
28
|
| "effect-solutions"
|
|
32
|
-
| "session-relay"
|
|
33
|
-
| "chrome-for-testing"
|
|
34
29
|
| "ffplay"
|
|
35
30
|
| "intelephense"
|
|
36
31
|
| "typescript-language-server"
|
|
@@ -167,35 +162,26 @@ const locateEffectSolutions = (exec: ProbeExecutor): DependencyLocation => {
|
|
|
167
162
|
return { path: resolved, binDir: globalBin }
|
|
168
163
|
}
|
|
169
164
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
165
|
+
const npmGlobalCache = new WeakMap<ProbeExecutor, { [k: string]: string }>()
|
|
166
|
+
|
|
167
|
+
const npmGlobalVersions = (exec: ProbeExecutor): { [k: string]: string } => {
|
|
168
|
+
const hit = npmGlobalCache.get(exec)
|
|
169
|
+
if (hit !== undefined) return hit
|
|
170
|
+
const out: { [k: string]: string } = {}
|
|
171
|
+
if (exec.commandExists("npm")) {
|
|
172
|
+
const doc = parseJson(exec.capture("npm", ["ls", "-g", "--depth=0", "--json"]))
|
|
173
|
+
const deps = doc !== undefined && isObject(doc) && isObject(doc["dependencies"]) ? doc["dependencies"] : {}
|
|
174
|
+
for (const [name, value] of Object.entries(deps)) {
|
|
175
|
+
if (isObject(value) && typeof value["version"] === "string") out[name] = value["version"]
|
|
180
176
|
}
|
|
181
177
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (path !== "") return { state: "present", path }
|
|
185
|
-
}
|
|
186
|
-
return { state: "missing" }
|
|
178
|
+
npmGlobalCache.set(exec, out)
|
|
179
|
+
return out
|
|
187
180
|
}
|
|
188
181
|
|
|
189
|
-
const
|
|
190
|
-
if (!exec.commandExists("curl")) return ""
|
|
191
|
-
const doc = parseJson(
|
|
192
|
-
exec.capture("curl", ["-fsSL", "--max-time", "5", "https://api.github.com/repos/rtk-ai/rtk/releases/latest"])
|
|
193
|
-
)
|
|
194
|
-
const tag = doc !== undefined && isObject(doc) && typeof doc["tag_name"] === "string" ? doc["tag_name"] : ""
|
|
195
|
-
return tag.replace(/^v/, "")
|
|
196
|
-
}
|
|
182
|
+
const versionNpmGlobal = (pkg: string) => (exec: ProbeExecutor): string => npmGlobalVersions(exec)[pkg] ?? ""
|
|
197
183
|
|
|
198
|
-
const latestNpm = (id: "
|
|
184
|
+
const latestNpm = (id: "effect-solutions") => (exec: ProbeExecutor): string =>
|
|
199
185
|
exec.commandExists("npm") ? exec.capture("npm", ["view", id, "version"]) : ""
|
|
200
186
|
|
|
201
187
|
export const defaultProbeExecutor: ProbeExecutor = { commandExists, capture, which }
|
|
@@ -237,15 +223,6 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
237
223
|
() => 'tmp=$(mktemp) && curl -fsSL https://chatgpt.com/codex/install.sh -o "$tmp" && CODEX_NON_INTERACTIVE=1 sh "$tmp"',
|
|
238
224
|
{ version: versionProbe("codex") }
|
|
239
225
|
),
|
|
240
|
-
rtk: spec("rtk", "optional", () => "see https://github.com/rtk-ai/rtk (kit auto-install is Linux/macOS-only)", {
|
|
241
|
-
version: versionProbe("rtk"),
|
|
242
|
-
latest: latestRtk
|
|
243
|
-
}),
|
|
244
|
-
"session-relay": spec("session-relay", "optional", () => "docks-kit toolchain ensure session-relay", {
|
|
245
|
-
resolve: (exec) => pathProbe(p(home(), ".local", "bin", "session-relay"))(exec),
|
|
246
|
-
version: (exec) => exec.capture(p(home(), ".local", "bin", "session-relay"), ["--version"]),
|
|
247
|
-
locate: () => ({ path: p(home(), ".local", "bin", "session-relay"), binDir: p(home(), ".local", "bin") })
|
|
248
|
-
}),
|
|
249
226
|
bun: spec(
|
|
250
227
|
"bun",
|
|
251
228
|
"optional",
|
|
@@ -256,10 +233,8 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
256
233
|
locate: (exec) => ({ path: findBun(exec)?.path ?? "", binDir: "" })
|
|
257
234
|
}
|
|
258
235
|
),
|
|
259
|
-
bwrap: spec("bwrap", "optional", () => "sudo apt install -y bubblewrap (or dnf/pacman/zypper equivalent)"
|
|
260
|
-
|
|
261
|
-
version: versionProbe("agent-browser"),
|
|
262
|
-
latest: latestNpm("agent-browser")
|
|
236
|
+
bwrap: spec("bwrap", "optional", () => "sudo apt install -y bubblewrap (or dnf/pacman/zypper equivalent)", {
|
|
237
|
+
version: versionProbe("bwrap")
|
|
263
238
|
}),
|
|
264
239
|
"effect-solutions": spec("effect-solutions", "optional", () => "bun add -g effect-solutions", {
|
|
265
240
|
resolve: resolveEffectSolutions,
|
|
@@ -267,27 +242,22 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
267
242
|
locate: locateEffectSolutions,
|
|
268
243
|
latest: latestNpm("effect-solutions")
|
|
269
244
|
}),
|
|
270
|
-
"chrome-for-testing": spec(
|
|
271
|
-
"chrome-for-testing",
|
|
272
|
-
"optional",
|
|
273
|
-
(pf = rawPlatform()) => (pf === "linux" ? "agent-browser install --with-deps" : "agent-browser install"),
|
|
274
|
-
{ resolve: resolveChrome }
|
|
275
|
-
),
|
|
276
245
|
ffplay: spec(
|
|
277
246
|
"ffplay",
|
|
278
247
|
"optional",
|
|
279
248
|
(pf = rawPlatform()) =>
|
|
280
249
|
pf === "darwin" ? "brew install ffmpeg" : "sudo apt install -y ffmpeg",
|
|
281
|
-
{ versionArgs: ["-version"], resolve: pathProbe("ffplay") }
|
|
250
|
+
{ versionArgs: ["-version"], version: versionProbe("ffplay", ["-version"]), resolve: pathProbe("ffplay") }
|
|
282
251
|
),
|
|
283
252
|
intelephense: spec("intelephense", "optional", () => "npm install -g intelephense", {
|
|
284
|
-
resolve: pathProbe("intelephense")
|
|
253
|
+
resolve: pathProbe("intelephense"),
|
|
254
|
+
version: versionNpmGlobal("intelephense")
|
|
285
255
|
}),
|
|
286
256
|
"typescript-language-server": spec(
|
|
287
257
|
"typescript-language-server",
|
|
288
258
|
"optional",
|
|
289
259
|
() => "npm install -g typescript-language-server typescript",
|
|
290
|
-
{ resolve: pathProbe("typescript-language-server") }
|
|
260
|
+
{ resolve: pathProbe("typescript-language-server"), version: versionProbe("typescript-language-server") }
|
|
291
261
|
),
|
|
292
262
|
tsc: spec("tsc", "optional", () => "npm install -g typescript", {
|
|
293
263
|
resolve: pathProbe("tsc"),
|