docks-kit 0.14.2 → 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 +3 -4
- package/README.md +5 -5
- package/cli/docs/flags.md +1 -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 +24 -15
- package/cli/src/engine-native/claudeSync.ts +70 -114
- package/cli/src/engine-native/codexSync.ts +7 -3
- package/cli/src/engine-native/deps.ts +21 -45
- 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 -7
- 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 -3
- package/cli/src/generated/sotPayload.ts +4 -4
- package/package.json +1 -1
|
@@ -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,10 +26,9 @@ 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
33
|
|
|
35
34
|
export type ClaudeRuntimeState =
|
|
@@ -48,7 +47,6 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
|
|
|
48
47
|
)
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
syncRtk(ctx, claudeDir)
|
|
52
50
|
const bun = bunBootstrap(ctx, ctx.services)
|
|
53
51
|
const runtime: ClaudeRuntimeState = bun.kind === "ready"
|
|
54
52
|
? { kind: "ready", paths: claudeRuntimePaths(claudeDir, bun.executable) }
|
|
@@ -86,86 +84,6 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
|
|
|
86
84
|
return runtime
|
|
87
85
|
}
|
|
88
86
|
|
|
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
87
|
// ----------------------------------------------------------- runtime ----
|
|
170
88
|
|
|
171
89
|
function syncClaudeRuntime(ctx: Ctx, runtime: ClaudeRuntimeState): void {
|
|
@@ -448,10 +366,11 @@ const REMOVED_MANIFEST = {
|
|
|
448
366
|
"env.CLAUDE_CODE_DISABLE_1M_CONTEXT",
|
|
449
367
|
"env.CLAUDE_CODE_FORK_SUBAGENT",
|
|
450
368
|
"env.CLAUDE_CODE_EFFORT_LEVEL",
|
|
451
|
-
"enabledPlugins.session-relay@docks"
|
|
369
|
+
"enabledPlugins.session-relay@docks",
|
|
370
|
+
"hooks.PreToolUse"
|
|
452
371
|
],
|
|
453
372
|
permissionRules: {
|
|
454
|
-
allow: ["Write(./)"],
|
|
373
|
+
allow: ["Write(./)", "Bash(rtk *)"],
|
|
455
374
|
deny: ["Write(**/.env)", "Write(**/.env.local)", "Write(**/secrets/**)"]
|
|
456
375
|
},
|
|
457
376
|
claudeJsonKeys: [] as Array<string>,
|
|
@@ -646,7 +565,7 @@ function nonUserScopeMarketplaces(installedDoc: Json | undefined): Set<string> {
|
|
|
646
565
|
}
|
|
647
566
|
|
|
648
567
|
function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
649
|
-
const { change, echo, verbose, warn } = ctx.services.logger
|
|
568
|
+
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
650
569
|
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
651
570
|
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
652
571
|
|
|
@@ -687,7 +606,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
687
606
|
const known = readJsonFile(knownMarketplaces)
|
|
688
607
|
if (known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false) continue
|
|
689
608
|
const repo = isObject(mpValue) && isObject(mpValue["source"]) ? String((mpValue["source"] as { [k: string]: Json })["repo"] ?? "") : ""
|
|
690
|
-
|
|
609
|
+
progress(`Adding marketplace ${mpName}...`)
|
|
610
|
+
const marketplaceResult = cli(["plugin", "marketplace", "add", repo])
|
|
611
|
+
clearProgress()
|
|
612
|
+
if (marketplaceResult.ok) {
|
|
691
613
|
addedMp++
|
|
692
614
|
} else {
|
|
693
615
|
warn(`Failed to add marketplace: ${mpName} (${repo})`)
|
|
@@ -695,17 +617,25 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
695
617
|
}
|
|
696
618
|
}
|
|
697
619
|
|
|
698
|
-
// 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.
|
|
699
622
|
let addedPl = 0
|
|
700
623
|
let f2 = 0
|
|
701
|
-
|
|
624
|
+
const refreshedMarketplaces = new Set<string>()
|
|
702
625
|
for (const pluginId of sortedKeys(sotPlugins)) {
|
|
703
626
|
if (pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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)
|
|
707
634
|
}
|
|
708
|
-
|
|
635
|
+
progress(`Installing plugin ${pluginId}...`)
|
|
636
|
+
const installResult = cli(["plugin", "install", pluginId])
|
|
637
|
+
clearProgress()
|
|
638
|
+
if (installResult.ok) {
|
|
709
639
|
addedPl++
|
|
710
640
|
} else {
|
|
711
641
|
warn(`Failed to install plugin: ${pluginId}`)
|
|
@@ -717,12 +647,32 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
717
647
|
const installedDoc = readJsonFile(installedPlugins)
|
|
718
648
|
const installedKeys = installedDoc !== undefined && isObject(installedDoc) ? sortedKeys(installedDoc["plugins"]) : []
|
|
719
649
|
const nonUserMarketplaces = nonUserScopeMarketplaces(installedDoc)
|
|
720
|
-
//
|
|
721
|
-
|
|
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.
|
|
722
664
|
if (!ctx.skipPluginRefresh) {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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++
|
|
726
676
|
}
|
|
727
677
|
}
|
|
728
678
|
|
|
@@ -735,7 +685,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
735
685
|
for (const pluginId of installedKeys) {
|
|
736
686
|
if (isObject(sotPlugins) && Object.prototype.hasOwnProperty.call(sotPlugins, pluginId)) continue
|
|
737
687
|
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
738
|
-
|
|
688
|
+
progress(`Uninstalling plugin ${pluginId}...`)
|
|
689
|
+
const uninstallResult = cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId])
|
|
690
|
+
clearProgress()
|
|
691
|
+
if (uninstallResult.ok) {
|
|
739
692
|
removedPl++
|
|
740
693
|
} else {
|
|
741
694
|
warn(`Failed to uninstall plugin: ${pluginId}`)
|
|
@@ -748,7 +701,10 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
748
701
|
if (nonUserMarketplaces.has(mpName)) continue
|
|
749
702
|
const declared = isObject(sotMarketplaces) ? sotMarketplaces[mpName] : undefined
|
|
750
703
|
if (declared !== undefined && declared !== null && declared !== false) continue
|
|
751
|
-
|
|
704
|
+
progress(`Removing marketplace ${mpName}...`)
|
|
705
|
+
const removeResult = cli(["plugin", "marketplace", "remove", mpName])
|
|
706
|
+
clearProgress()
|
|
707
|
+
if (removeResult.ok) {
|
|
752
708
|
removedMp++
|
|
753
709
|
} else {
|
|
754
710
|
warn(`Failed to remove marketplace: ${mpName}`)
|
|
@@ -810,7 +766,7 @@ function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSett
|
|
|
810
766
|
// ------------------------------------------------------ optional plugins ----
|
|
811
767
|
|
|
812
768
|
function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): boolean {
|
|
813
|
-
const { change, verbose, warn } = ctx.services.logger
|
|
769
|
+
const { change, clearProgress, progress, verbose, warn } = ctx.services.logger
|
|
814
770
|
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
815
771
|
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
816
772
|
const mpName = pluginId.slice(pluginId.lastIndexOf("@") + 1)
|
|
@@ -830,7 +786,10 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
|
|
|
830
786
|
|
|
831
787
|
const wasInstalled = pluginUserScopeInstalled(installedPlugins, pluginId)
|
|
832
788
|
if (!wasInstalled) {
|
|
833
|
-
|
|
789
|
+
progress(`Installing plugin ${pluginId}...`)
|
|
790
|
+
const installResult = cli(["plugin", "install", pluginId])
|
|
791
|
+
clearProgress()
|
|
792
|
+
if (!installResult.ok) {
|
|
834
793
|
if (marketplaceAdded) change(`Optional plugin ${pluginId}: marketplace added (install failed — will retry next sync)`)
|
|
835
794
|
warn(`Failed to install optional plugin ${pluginId}`)
|
|
836
795
|
return marketplaceAdded
|
|
@@ -889,7 +848,7 @@ function lspPkg(ctx: Ctx, tool: string, pkg: string): string {
|
|
|
889
848
|
}
|
|
890
849
|
|
|
891
850
|
function syncLspServers(ctx: Ctx): void {
|
|
892
|
-
const { change, echo, verbose, warn } = ctx.services.logger
|
|
851
|
+
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
893
852
|
const sot = parseJson(payloadText("SoT/.claude/settings.json"))
|
|
894
853
|
const enabled = sot !== undefined && isObject(sot) && isObject(sot["enabledPlugins"]) ? sot["enabledPlugins"] : undefined
|
|
895
854
|
if (enabled === undefined) return
|
|
@@ -929,7 +888,10 @@ function syncLspServers(ctx: Ctx): void {
|
|
|
929
888
|
}
|
|
930
889
|
|
|
931
890
|
verbose(`Installing LSP servers via npm: ${specs}...`)
|
|
932
|
-
|
|
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) {
|
|
933
895
|
change(`LSP servers installed (${specs})`)
|
|
934
896
|
ctx.nextStepTriggers.claudeRestart = true
|
|
935
897
|
} else {
|
|
@@ -949,12 +911,6 @@ export function claudeSummary(ctx: Ctx, runtime: ClaudeRuntimeState): void {
|
|
|
949
911
|
} else {
|
|
950
912
|
echo("Hooks: migration deferred (Bun unavailable; existing hook/statusline settings preserved)")
|
|
951
913
|
}
|
|
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
914
|
if (ctx.services.deps.probe("claude").state === "present") {
|
|
959
915
|
const installed = readJsonFile(p(claudeDir, "plugins", "installed_plugins.json"))
|
|
960
916
|
const count = installed !== undefined && isObject(installed) && isObject(installed["plugins"]) ? Object.keys(installed["plugins"]).length : 0
|
|
@@ -42,9 +42,9 @@ function ensureBubblewrap(ctx: Ctx): void {
|
|
|
42
42
|
|
|
43
43
|
if (ctx.services.deps.probe("bwrap").state === "present") return
|
|
44
44
|
|
|
45
|
-
if (ctx.
|
|
45
|
+
if (ctx.skipBubblewrap) {
|
|
46
46
|
warn(
|
|
47
|
-
"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"
|
|
48
48
|
)
|
|
49
49
|
return
|
|
50
50
|
}
|
|
@@ -491,7 +491,7 @@ function installedPluginIdsFromCli(): Set<string> | undefined {
|
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
494
|
-
const { change, echo, verbose, warn } = ctx.services.logger
|
|
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
|
+
progress("Checking installed Codex plugins...")
|
|
522
523
|
const installedPluginIds = 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,7 +532,9 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
|
530
532
|
let refreshed = 0
|
|
531
533
|
let failed = 0
|
|
532
534
|
for (const pluginId of pluginIds) {
|
|
535
|
+
progress(`Updating Codex plugin ${pluginId}...`)
|
|
533
536
|
const res = spawnSync("codex", ["plugin", "add", pluginId], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
|
|
537
|
+
clearProgress()
|
|
534
538
|
const addOut = `${res.stdout ?? ""}${res.stderr ?? ""}`
|
|
535
539
|
if (res.error === undefined && res.status === 0) {
|
|
536
540
|
refreshed++
|
|
@@ -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,12 +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
|
-
| "chrome-for-testing"
|
|
33
29
|
| "ffplay"
|
|
34
30
|
| "intelephense"
|
|
35
31
|
| "typescript-language-server"
|
|
@@ -166,35 +162,26 @@ const locateEffectSolutions = (exec: ProbeExecutor): DependencyLocation => {
|
|
|
166
162
|
return { path: resolved, binDir: globalBin }
|
|
167
163
|
}
|
|
168
164
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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"]
|
|
179
176
|
}
|
|
180
177
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (path !== "") return { state: "present", path }
|
|
184
|
-
}
|
|
185
|
-
return { state: "missing" }
|
|
178
|
+
npmGlobalCache.set(exec, out)
|
|
179
|
+
return out
|
|
186
180
|
}
|
|
187
181
|
|
|
188
|
-
const
|
|
189
|
-
if (!exec.commandExists("curl")) return ""
|
|
190
|
-
const doc = parseJson(
|
|
191
|
-
exec.capture("curl", ["-fsSL", "--max-time", "5", "https://api.github.com/repos/rtk-ai/rtk/releases/latest"])
|
|
192
|
-
)
|
|
193
|
-
const tag = doc !== undefined && isObject(doc) && typeof doc["tag_name"] === "string" ? doc["tag_name"] : ""
|
|
194
|
-
return tag.replace(/^v/, "")
|
|
195
|
-
}
|
|
182
|
+
const versionNpmGlobal = (pkg: string) => (exec: ProbeExecutor): string => npmGlobalVersions(exec)[pkg] ?? ""
|
|
196
183
|
|
|
197
|
-
const latestNpm = (id: "
|
|
184
|
+
const latestNpm = (id: "effect-solutions") => (exec: ProbeExecutor): string =>
|
|
198
185
|
exec.commandExists("npm") ? exec.capture("npm", ["view", id, "version"]) : ""
|
|
199
186
|
|
|
200
187
|
export const defaultProbeExecutor: ProbeExecutor = { commandExists, capture, which }
|
|
@@ -236,10 +223,6 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
236
223
|
() => 'tmp=$(mktemp) && curl -fsSL https://chatgpt.com/codex/install.sh -o "$tmp" && CODEX_NON_INTERACTIVE=1 sh "$tmp"',
|
|
237
224
|
{ version: versionProbe("codex") }
|
|
238
225
|
),
|
|
239
|
-
rtk: spec("rtk", "optional", () => "see https://github.com/rtk-ai/rtk (kit auto-install is Linux/macOS-only)", {
|
|
240
|
-
version: versionProbe("rtk"),
|
|
241
|
-
latest: latestRtk
|
|
242
|
-
}),
|
|
243
226
|
bun: spec(
|
|
244
227
|
"bun",
|
|
245
228
|
"optional",
|
|
@@ -250,10 +233,8 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
250
233
|
locate: (exec) => ({ path: findBun(exec)?.path ?? "", binDir: "" })
|
|
251
234
|
}
|
|
252
235
|
),
|
|
253
|
-
bwrap: spec("bwrap", "optional", () => "sudo apt install -y bubblewrap (or dnf/pacman/zypper equivalent)"
|
|
254
|
-
|
|
255
|
-
version: versionProbe("agent-browser"),
|
|
256
|
-
latest: latestNpm("agent-browser")
|
|
236
|
+
bwrap: spec("bwrap", "optional", () => "sudo apt install -y bubblewrap (or dnf/pacman/zypper equivalent)", {
|
|
237
|
+
version: versionProbe("bwrap")
|
|
257
238
|
}),
|
|
258
239
|
"effect-solutions": spec("effect-solutions", "optional", () => "bun add -g effect-solutions", {
|
|
259
240
|
resolve: resolveEffectSolutions,
|
|
@@ -261,27 +242,22 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
261
242
|
locate: locateEffectSolutions,
|
|
262
243
|
latest: latestNpm("effect-solutions")
|
|
263
244
|
}),
|
|
264
|
-
"chrome-for-testing": spec(
|
|
265
|
-
"chrome-for-testing",
|
|
266
|
-
"optional",
|
|
267
|
-
(pf = rawPlatform()) => (pf === "linux" ? "agent-browser install --with-deps" : "agent-browser install"),
|
|
268
|
-
{ resolve: resolveChrome }
|
|
269
|
-
),
|
|
270
245
|
ffplay: spec(
|
|
271
246
|
"ffplay",
|
|
272
247
|
"optional",
|
|
273
248
|
(pf = rawPlatform()) =>
|
|
274
249
|
pf === "darwin" ? "brew install ffmpeg" : "sudo apt install -y ffmpeg",
|
|
275
|
-
{ versionArgs: ["-version"], resolve: pathProbe("ffplay") }
|
|
250
|
+
{ versionArgs: ["-version"], version: versionProbe("ffplay", ["-version"]), resolve: pathProbe("ffplay") }
|
|
276
251
|
),
|
|
277
252
|
intelephense: spec("intelephense", "optional", () => "npm install -g intelephense", {
|
|
278
|
-
resolve: pathProbe("intelephense")
|
|
253
|
+
resolve: pathProbe("intelephense"),
|
|
254
|
+
version: versionNpmGlobal("intelephense")
|
|
279
255
|
}),
|
|
280
256
|
"typescript-language-server": spec(
|
|
281
257
|
"typescript-language-server",
|
|
282
258
|
"optional",
|
|
283
259
|
() => "npm install -g typescript-language-server typescript",
|
|
284
|
-
{ resolve: pathProbe("typescript-language-server") }
|
|
260
|
+
{ resolve: pathProbe("typescript-language-server"), version: versionProbe("typescript-language-server") }
|
|
285
261
|
),
|
|
286
262
|
tsc: spec("tsc", "optional", () => "npm install -g typescript", {
|
|
287
263
|
resolve: pathProbe("tsc"),
|
|
@@ -11,9 +11,9 @@ import { homedir } from "node:os"
|
|
|
11
11
|
import { kitHome } from "../kitHome"
|
|
12
12
|
import { makeEngineServices, type EngineServices, type Logger } from "./services"
|
|
13
13
|
import type { BunRuntimeState } from "./bun"
|
|
14
|
-
import { claudeNextSteps, claudeSummary, claudeSync } from "./claudeSync"
|
|
14
|
+
import { claudeNextSteps, claudeSummary, claudeSync, type ClaudeRuntimeState } from "./claudeSync"
|
|
15
15
|
import { codexNextSteps, codexSummary, codexSync } from "./codexSync"
|
|
16
|
-
import { skillsNextSteps, skillsSummary, skillsSync } from "./skillsSync"
|
|
16
|
+
import { skillsNextSteps, skillsSummary, skillsSync, type SkillsState } from "./skillsSync"
|
|
17
17
|
import { modeModel, modeToolchain } from "./modes"
|
|
18
18
|
import { ExitError, parseArgs, validateModifierFlags } from "./parseArgs"
|
|
19
19
|
|
|
@@ -30,7 +30,7 @@ export interface Ctx {
|
|
|
30
30
|
readonly agentsDir: string
|
|
31
31
|
dryRun: boolean
|
|
32
32
|
verbose: boolean
|
|
33
|
-
|
|
33
|
+
skipBubblewrap: boolean
|
|
34
34
|
skipPluginRefresh?: boolean
|
|
35
35
|
reconcile: boolean
|
|
36
36
|
prune: boolean
|
|
@@ -71,7 +71,7 @@ function makeCtx(services: EngineServices): Ctx {
|
|
|
71
71
|
agentsDir: env["AGENTS_DIR"] !== undefined && env["AGENTS_DIR"] !== "" ? env["AGENTS_DIR"] : p(home, ".agents"),
|
|
72
72
|
dryRun: env["DRY_RUN"] === "1",
|
|
73
73
|
verbose: env["DOCKS_KIT_VERBOSE"] === "1",
|
|
74
|
-
|
|
74
|
+
skipBubblewrap: env["SKIP_BUBBLEWRAP"] === "1",
|
|
75
75
|
skipPluginRefresh: false,
|
|
76
76
|
reconcile: env["RECONCILE"] === "1",
|
|
77
77
|
prune: env["PRUNE"] === "1",
|
|
@@ -95,17 +95,40 @@ function makeCtx(services: EngineServices): Ctx {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
function engineSync(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
98
|
-
const { echo } = ctx.services.logger
|
|
98
|
+
const { clearProgress, echo, progress } = ctx.services.logger
|
|
99
99
|
parseArgs(ctx, args)
|
|
100
100
|
validateModifierFlags(ctx)
|
|
101
101
|
|
|
102
102
|
const claudeRan = ctx.syncClaude
|
|
103
|
-
|
|
103
|
+
let claudeRuntime: ClaudeRuntimeState | undefined
|
|
104
|
+
if (claudeRan) {
|
|
105
|
+
progress("Syncing Claude...")
|
|
106
|
+
try {
|
|
107
|
+
claudeRuntime = claudeSync(ctx)
|
|
108
|
+
} finally {
|
|
109
|
+
clearProgress()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
104
112
|
|
|
105
113
|
const codexRan = ctx.syncCodex
|
|
106
|
-
if (codexRan)
|
|
114
|
+
if (codexRan) {
|
|
115
|
+
progress("Syncing Codex...")
|
|
116
|
+
try {
|
|
117
|
+
codexSync(ctx)
|
|
118
|
+
} finally {
|
|
119
|
+
clearProgress()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
107
122
|
|
|
108
|
-
|
|
123
|
+
let skillsState: SkillsState | undefined
|
|
124
|
+
if (ctx.syncAgents) {
|
|
125
|
+
progress("Syncing skills...")
|
|
126
|
+
try {
|
|
127
|
+
skillsState = skillsSync(ctx)
|
|
128
|
+
} finally {
|
|
129
|
+
clearProgress()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
109
132
|
|
|
110
133
|
echo("")
|
|
111
134
|
echo("--- Sync complete ---")
|
|
@@ -133,6 +156,8 @@ export function runEngineNative(argv: ReadonlyArray<string>, services?: EngineSe
|
|
|
133
156
|
const baseLogger = baseServices.logger
|
|
134
157
|
const logger: Logger = {
|
|
135
158
|
change: (msg) => baseLogger.change(msg),
|
|
159
|
+
progress: (msg) => baseLogger.progress(msg),
|
|
160
|
+
clearProgress: () => baseLogger.clearProgress(),
|
|
136
161
|
verbose: (msg) => {
|
|
137
162
|
if (ctx.verbose) baseLogger.verbose(msg)
|
|
138
163
|
},
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Leveled stderr logger
|
|
3
|
-
* DESIGN.md. Filtering is explicit and
|
|
4
|
-
* so fiber-scoped Effect log levels
|
|
5
|
-
*
|
|
2
|
+
* Leveled stderr logger, transient progress writer, and stdout data writer —
|
|
3
|
+
* the Output Policy contract in DESIGN.md. Filtering is explicit and
|
|
4
|
+
* synchronous: engine code is imperative, so fiber-scoped Effect log levels
|
|
5
|
+
* cannot see these writes. Transient progress is active only through an
|
|
6
|
+
* injected progress sink or an interactive stderr. The prefixes and ANSI codes
|
|
7
|
+
* are stable golden surface; the level controls visibility only.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
export interface Logger {
|
|
9
11
|
/** `[ok]` green — an operation actually mutated something. Always visible. */
|
|
10
12
|
readonly change: (msg: string) => void
|
|
13
|
+
/** Dim, transient single-line status for blocking work. */
|
|
14
|
+
readonly progress: (msg: string) => void
|
|
15
|
+
/** Erase a pending transient status line. */
|
|
16
|
+
readonly clearProgress: () => void
|
|
11
17
|
/** `[ok]` green — status-quo confirmation; visible only with verbosity on. */
|
|
12
18
|
readonly verbose: (msg: string) => void
|
|
13
19
|
readonly warn: (msg: string) => void
|
|
@@ -18,18 +24,75 @@ export interface Logger {
|
|
|
18
24
|
|
|
19
25
|
export interface LoggerSinks {
|
|
20
26
|
readonly stderr?: (chunk: string) => void
|
|
27
|
+
readonly progress?: (chunk: string) => void
|
|
21
28
|
readonly stdout?: (chunk: string) => void
|
|
22
29
|
}
|
|
23
30
|
|
|
31
|
+
interface WritableStreamLike {
|
|
32
|
+
write: (chunk: string) => unknown
|
|
33
|
+
on?: (event: "error", listener: (error: unknown) => void) => unknown
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const epipeGuarded = new WeakSet<WritableStreamLike>()
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A downstream reader may close the pipe early (`docks-kit toolchain check |
|
|
40
|
+
* head`). That is a normal end of consumption, not a CLI failure, so both the
|
|
41
|
+
* synchronous throw and the asynchronous error event are ignored for EPIPE.
|
|
42
|
+
*/
|
|
43
|
+
export function writeIgnoringEpipe(stream: WritableStreamLike, chunk: string): void {
|
|
44
|
+
if (!epipeGuarded.has(stream)) {
|
|
45
|
+
epipeGuarded.add(stream)
|
|
46
|
+
stream.on?.("error", (error) => {
|
|
47
|
+
if ((error as NodeJS.ErrnoException | null)?.code !== "EPIPE") throw error
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
stream.write(chunk)
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if ((error as NodeJS.ErrnoException | null)?.code !== "EPIPE") throw error
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
24
57
|
export function makeLogger(sinks: LoggerSinks): Logger {
|
|
25
|
-
const errWrite = sinks.stderr ?? ((chunk: string) =>
|
|
26
|
-
const outWrite = sinks.stdout ?? ((chunk: string) =>
|
|
27
|
-
const
|
|
58
|
+
const errWrite = sinks.stderr ?? ((chunk: string) => writeIgnoringEpipe(process.stderr, chunk))
|
|
59
|
+
const outWrite = sinks.stdout ?? ((chunk: string) => writeIgnoringEpipe(process.stdout, chunk))
|
|
60
|
+
const progressWrite =
|
|
61
|
+
sinks.progress ??
|
|
62
|
+
(sinks.stderr === undefined && process.stderr.isTTY === true
|
|
63
|
+
? (chunk: string) => writeIgnoringEpipe(process.stderr, chunk)
|
|
64
|
+
: undefined)
|
|
65
|
+
let progressPending = false
|
|
66
|
+
|
|
67
|
+
const clearProgress = (): void => {
|
|
68
|
+
if (!progressPending || progressWrite === undefined) return
|
|
69
|
+
progressWrite("\r\x1b[2K")
|
|
70
|
+
progressPending = false
|
|
71
|
+
}
|
|
72
|
+
const ok = (msg: string): void => {
|
|
73
|
+
clearProgress()
|
|
74
|
+
errWrite(`\x1b[1;32m[ok]\x1b[0m ${msg}\n`)
|
|
75
|
+
}
|
|
28
76
|
return {
|
|
29
77
|
change: ok,
|
|
30
78
|
verbose: ok,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
79
|
+
progress: (msg) => {
|
|
80
|
+
if (progressWrite === undefined) return
|
|
81
|
+
progressWrite(`\r\x1b[2K\x1b[2m${msg}\x1b[0m`)
|
|
82
|
+
progressPending = true
|
|
83
|
+
},
|
|
84
|
+
clearProgress,
|
|
85
|
+
warn: (msg) => {
|
|
86
|
+
clearProgress()
|
|
87
|
+
errWrite(`\x1b[1;33m[warn]\x1b[0m ${msg}\n`)
|
|
88
|
+
},
|
|
89
|
+
err: (msg) => {
|
|
90
|
+
clearProgress()
|
|
91
|
+
errWrite(`\x1b[1;31m[err]\x1b[0m ${msg}\n`)
|
|
92
|
+
},
|
|
93
|
+
echo: (line) => {
|
|
94
|
+
clearProgress()
|
|
95
|
+
outWrite(`${line}\n`)
|
|
96
|
+
}
|
|
34
97
|
}
|
|
35
98
|
}
|