docks-kit 0.14.3 → 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/README.md +1 -1
- package/cli/docs/flags.md +6 -0
- package/cli/src/engine-native/DESIGN.md +15 -4
- package/cli/src/engine-native/bun.ts +22 -21
- package/cli/src/engine-native/claudeSync.ts +30 -31
- package/cli/src/engine-native/codexSync.ts +24 -24
- package/cli/src/engine-native/deps.ts +43 -35
- package/cli/src/engine-native/exec.ts +61 -5
- package/cli/src/engine-native/index.ts +141 -33
- package/cli/src/engine-native/logger.ts +114 -14
- package/cli/src/engine-native/modes.ts +4 -4
- package/cli/src/engine-native/services.ts +5 -5
- package/cli/src/engine-native/skillsSync.ts +19 -20
- package/cli/src/engine-native/toolchain.ts +40 -27
- package/cli/src/engine.ts +1 -1
- package/cli/src/generated/sotPayload.ts +1 -1
- package/cli/src/main.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,7 +128,7 @@ Details: `docks-kit docs platforms`.
|
|
|
128
128
|
Tagging `cli-v*` builds four standalone binaries (Linux x64/arm64 and macOS
|
|
129
129
|
x64/arm64) plus `SHA256SUMS` and attaches them to the GitHub release; npm
|
|
130
130
|
publishes the exact package tarball through trusted publishing with OIDC provenance.
|
|
131
|
-
Package `docks-kit` 0.14.
|
|
131
|
+
Package `docks-kit` 0.14.4 bundles the CLI + generated payload, so npm releases
|
|
132
132
|
are versioned config snapshots without shipping the authoring `SoT/` tree.
|
|
133
133
|
|
|
134
134
|
## Deeper docs
|
package/cli/docs/flags.md
CHANGED
|
@@ -23,6 +23,12 @@ docks-kit sync claude agents # two
|
|
|
23
23
|
| `--yes` | Auto-accept toolchain above-verified prompts (containers/CI) |
|
|
24
24
|
| `--verbose` / `-v` | Also print no-op confirmations (already in sync, up to date, left as-is); accepted on `sync`, `model`, and `toolchain` |
|
|
25
25
|
|
|
26
|
+
## Environment overrides
|
|
27
|
+
|
|
28
|
+
| Variable | Effect |
|
|
29
|
+
|----------|--------|
|
|
30
|
+
| `DOCKS_KIT_SYNC_CONCURRENCY=1\|2\|3` | Maximum selected sync pipelines running at once. Default `3`; use `1` for serial golden/debug execution. Invalid values fail `sync` with exit 2 but do not affect `model` or `toolchain`. |
|
|
31
|
+
|
|
26
32
|
## Per-tool flags
|
|
27
33
|
|
|
28
34
|
| Flag | Effect |
|
|
@@ -31,6 +31,11 @@ explicit removed-engine diagnostic and exits 2 with the recovery tag message.
|
|
|
31
31
|
materialized settings, writes runtime assets, commits settings, then performs
|
|
32
32
|
readiness-gated legacy cleanup. Modifiers run after the base commit, removals
|
|
33
33
|
before plugins, and LSP checks after plugin state.
|
|
34
|
+
- **Pipeline concurrency is bounded.** Selected Claude, Codex, and skills
|
|
35
|
+
pipelines overlap through one input-ordered pool. The production default is
|
|
36
|
+
3; `DOCKS_KIT_SYNC_CONCURRENCY=1` restores serial execution for golden tests
|
|
37
|
+
and debugging (`2` allows two-way overlap). Each individual pipeline remains
|
|
38
|
+
serial, and summaries retain canonical Claude, Codex, skills order.
|
|
34
39
|
- **External CLIs stay external.** `claude`, `codex`, `npx`, `npm`, `bun`,
|
|
35
40
|
`curl`, and platform package managers are spawned with argv arrays,
|
|
36
41
|
not shell command strings except where the external installer contract is a
|
|
@@ -69,10 +74,16 @@ warnings, and the summary. Status-quo confirmations exist but are opt-in.
|
|
|
69
74
|
stderr sink exists and `process.stderr.isTTY` is `true`. It disables progress
|
|
70
75
|
in every other case. Injected golden sinks therefore keep their existing
|
|
71
76
|
bytes unless the harness supplies a progress sink.
|
|
72
|
-
- Each progress write replaces one terminal line.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
- Each progress write replaces one terminal line. Every durable `change`,
|
|
78
|
+
`verbose`, `warn`, `err`, or `echo` write erases a visible transient before
|
|
79
|
+
its own write.
|
|
80
|
+
- During sync, the coordinator holds one run-scoped terminal lease and owns
|
|
81
|
+
the transient line. Terminal-exclusive sections serialize input ownership
|
|
82
|
+
by acquisition order and suspend progress redraw while an inherited-stdio
|
|
83
|
+
installer or blocking prompt holds the lease.
|
|
84
|
+
- Automated tests cannot exercise real input contention: golden children use
|
|
85
|
+
ignored stdin, so `process.stdin.isTTY` is false. The simultaneous-prompt
|
|
86
|
+
path requires hand verification in a real TTY.
|
|
76
87
|
|
|
77
88
|
### Change detection
|
|
78
89
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process"
|
|
2
1
|
import { rmSync } from "node:fs"
|
|
3
2
|
import { tmpdir } from "node:os"
|
|
4
3
|
|
|
5
|
-
import { p } from "./exec"
|
|
4
|
+
import { p, spawnProcess } from "./exec"
|
|
6
5
|
import type { Ctx } from "./index"
|
|
7
6
|
import type { EngineServices } from "./services"
|
|
8
7
|
import { field } from "./toolchain"
|
|
@@ -11,10 +10,6 @@ export type BunRuntimeState =
|
|
|
11
10
|
| { readonly kind: "ready"; readonly executable: string }
|
|
12
11
|
| { readonly kind: "deferred"; readonly reason: "missing-curl" | "install-failed" }
|
|
13
12
|
|
|
14
|
-
function remember(ctx: Ctx, state: BunRuntimeState): BunRuntimeState {
|
|
15
|
-
ctx.bunRuntime = state
|
|
16
|
-
return state
|
|
17
|
-
}
|
|
18
13
|
|
|
19
14
|
function predictedExecutable(ctx: Ctx): string {
|
|
20
15
|
const root = process.env["BUN_INSTALL"] !== undefined && process.env["BUN_INSTALL"] !== ""
|
|
@@ -23,47 +18,53 @@ function predictedExecutable(ctx: Ctx): string {
|
|
|
23
18
|
return p(root, "bin", "bun")
|
|
24
19
|
}
|
|
25
20
|
|
|
26
|
-
function installBun(pin: string, installer: string): void {
|
|
27
|
-
const download =
|
|
28
|
-
if (download.error === undefined && download.
|
|
29
|
-
|
|
21
|
+
async function installBun(pin: string, installer: string): Promise<void> {
|
|
22
|
+
const download = await spawnProcess("curl", ["-fsSL", "https://bun.sh/install", "-o", installer], { stdio: "ignore" })
|
|
23
|
+
if (download.error === undefined && download.exitCode === 0) {
|
|
24
|
+
await spawnProcess("bash", [installer, `bun-v${pin}`], { stdio: "ignore" })
|
|
30
25
|
}
|
|
31
26
|
}
|
|
32
27
|
|
|
33
|
-
export function bunBootstrap(ctx: Ctx, services: EngineServices): BunRuntimeState {
|
|
28
|
+
export function bunBootstrap(ctx: Ctx, services: EngineServices): Promise<BunRuntimeState> {
|
|
34
29
|
if (ctx.bunRuntime !== undefined) return ctx.bunRuntime
|
|
30
|
+
const pending = runBunBootstrap(ctx, services)
|
|
31
|
+
ctx.bunRuntime = pending
|
|
32
|
+
return pending
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function runBunBootstrap(ctx: Ctx, services: EngineServices): Promise<BunRuntimeState> {
|
|
35
36
|
|
|
36
|
-
const existing = services.deps.path("bun")
|
|
37
|
-
if (existing !== "") return
|
|
37
|
+
const existing = await services.deps.path("bun")
|
|
38
|
+
if (existing !== "") return { kind: "ready", executable: existing }
|
|
38
39
|
|
|
39
40
|
const pin = field(ctx, "bun", "verified")
|
|
40
41
|
if (pin === "") {
|
|
41
42
|
services.logger.warn("Bun bootstrap aborted — SoT/toolchain.json has no verified Bun pin")
|
|
42
|
-
return
|
|
43
|
+
return { kind: "deferred", reason: "install-failed" }
|
|
43
44
|
}
|
|
44
45
|
if (services.deps.probe("curl").state === "missing") {
|
|
45
46
|
services.deps.warnMissing("curl", services.logger, "cannot bootstrap Bun; install Bun manually, then re-run sync")
|
|
46
|
-
return
|
|
47
|
+
return { kind: "deferred", reason: "missing-curl" }
|
|
47
48
|
}
|
|
48
49
|
if (ctx.dryRun) {
|
|
49
50
|
const executable = predictedExecutable(ctx)
|
|
50
51
|
services.logger.echo(`[dry-run] install Bun ${pin} (kit-verified) -> ${executable}`)
|
|
51
|
-
return
|
|
52
|
+
return { kind: "ready", executable }
|
|
52
53
|
}
|
|
53
54
|
services.logger.warn(`Bun not found — installing Bun ${pin} (kit-verified)...`)
|
|
54
55
|
const installer = p(tmpdir(), `bun-install-${process.pid}.sh`)
|
|
55
56
|
try {
|
|
56
|
-
installBun(pin, installer)
|
|
57
|
+
await installBun(pin, installer)
|
|
57
58
|
} finally {
|
|
58
59
|
rmSync(installer, { force: true })
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
const installed = services.deps.path("bun")
|
|
62
|
+
const installed = await services.deps.path("bun")
|
|
62
63
|
if (installed === "") {
|
|
63
64
|
services.logger.warn("Bun install failed. Install manually from https://bun.sh/docs/installation, then re-run sync.")
|
|
64
|
-
return
|
|
65
|
+
return { kind: "deferred", reason: "install-failed" }
|
|
65
66
|
}
|
|
66
|
-
const version = services.deps.version("bun")
|
|
67
|
+
const version = await services.deps.version("bun")
|
|
67
68
|
services.logger.change(`Bun installed (${version !== "" ? version : "version unknown"})`)
|
|
68
|
-
return
|
|
69
|
+
return { kind: "ready", executable: installed }
|
|
69
70
|
}
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* plugins. Message strings, guard order, JSON semantics, and spawned argv are
|
|
5
5
|
* golden-tested.
|
|
6
6
|
*/
|
|
7
|
-
import { spawnSync } from "node:child_process"
|
|
8
7
|
import {
|
|
9
8
|
appendFileSync,
|
|
10
9
|
copyFileSync,
|
|
@@ -23,7 +22,7 @@ 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
28
|
import { ExitError } from "./parseArgs"
|
|
@@ -35,7 +34,7 @@ export type ClaudeRuntimeState =
|
|
|
35
34
|
| { readonly kind: "ready"; readonly paths: ClaudeRuntimePaths }
|
|
36
35
|
| { readonly kind: "deferred"; readonly reason: "bun-unavailable" }
|
|
37
36
|
|
|
38
|
-
export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
|
|
37
|
+
export async function claudeSync(ctx: Ctx): Promise<ClaudeRuntimeState> {
|
|
39
38
|
const { err, warn } = ctx.services.logger
|
|
40
39
|
const claudeDir = p(ctx.home, ".claude")
|
|
41
40
|
|
|
@@ -47,7 +46,7 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
|
|
|
47
46
|
)
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
const bun = bunBootstrap(ctx, ctx.services)
|
|
49
|
+
const bun = await bunBootstrap(ctx, ctx.services)
|
|
51
50
|
const runtime: ClaudeRuntimeState = bun.kind === "ready"
|
|
52
51
|
? { kind: "ready", paths: claudeRuntimePaths(claudeDir, bun.executable) }
|
|
53
52
|
: { kind: "deferred", reason: "bun-unavailable" }
|
|
@@ -78,9 +77,9 @@ export function claudeSync(ctx: Ctx): ClaudeRuntimeState {
|
|
|
78
77
|
syncClaudeAdvisor(ctx, ctx.claudeAdvisor)
|
|
79
78
|
syncClaudeJson(ctx)
|
|
80
79
|
syncConnectorEnv(ctx)
|
|
81
|
-
syncPlugins(ctx, claudeDir)
|
|
82
|
-
syncOptionalPlugins(ctx, claudeDir)
|
|
83
|
-
syncLspServers(ctx)
|
|
80
|
+
await syncPlugins(ctx, claudeDir)
|
|
81
|
+
await syncOptionalPlugins(ctx, claudeDir)
|
|
82
|
+
await syncLspServers(ctx)
|
|
84
83
|
return runtime
|
|
85
84
|
}
|
|
86
85
|
|
|
@@ -523,9 +522,9 @@ function syncRemovals(ctx: Ctx, claudeDir: string, runtime: ClaudeRuntimeState):
|
|
|
523
522
|
|
|
524
523
|
// -------------------------------------------------------------- plugins ----
|
|
525
524
|
|
|
526
|
-
function cli(args: Array<string>): { ok: boolean; out: string } {
|
|
527
|
-
const res =
|
|
528
|
-
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}` }
|
|
529
528
|
}
|
|
530
529
|
|
|
531
530
|
function readJsonFile(file: string): Json | undefined {
|
|
@@ -564,7 +563,7 @@ function nonUserScopeMarketplaces(installedDoc: Json | undefined): Set<string> {
|
|
|
564
563
|
return marketplaces
|
|
565
564
|
}
|
|
566
565
|
|
|
567
|
-
function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
566
|
+
async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
568
567
|
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
569
568
|
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
570
569
|
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
@@ -607,7 +606,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
607
606
|
if (known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false) continue
|
|
608
607
|
const repo = isObject(mpValue) && isObject(mpValue["source"]) ? String((mpValue["source"] as { [k: string]: Json })["repo"] ?? "") : ""
|
|
609
608
|
progress(`Adding marketplace ${mpName}...`)
|
|
610
|
-
const marketplaceResult = cli(["plugin", "marketplace", "add", repo])
|
|
609
|
+
const marketplaceResult = await cli(["plugin", "marketplace", "add", repo])
|
|
611
610
|
clearProgress()
|
|
612
611
|
if (marketplaceResult.ok) {
|
|
613
612
|
addedMp++
|
|
@@ -628,12 +627,12 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
628
627
|
const mpName = separator > 0 ? pluginId.slice(separator + 1) : ""
|
|
629
628
|
if (mpName !== "" && !refreshedMarketplaces.has(mpName)) {
|
|
630
629
|
progress(`Refreshing marketplace ${mpName}...`)
|
|
631
|
-
cli(["plugin", "marketplace", "update", mpName])
|
|
630
|
+
await cli(["plugin", "marketplace", "update", mpName])
|
|
632
631
|
clearProgress()
|
|
633
632
|
refreshedMarketplaces.add(mpName)
|
|
634
633
|
}
|
|
635
634
|
progress(`Installing plugin ${pluginId}...`)
|
|
636
|
-
const installResult = cli(["plugin", "install", pluginId])
|
|
635
|
+
const installResult = await cli(["plugin", "install", pluginId])
|
|
637
636
|
clearProgress()
|
|
638
637
|
if (installResult.ok) {
|
|
639
638
|
addedPl++
|
|
@@ -664,13 +663,13 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
664
663
|
if (!ctx.skipPluginRefresh) {
|
|
665
664
|
for (const mpName of [...kitMarketplaces].sort(compareCodepoints)) {
|
|
666
665
|
progress(`Refreshing marketplace ${mpName}...`)
|
|
667
|
-
cli(["plugin", "marketplace", "update", mpName])
|
|
666
|
+
await cli(["plugin", "marketplace", "update", mpName])
|
|
668
667
|
clearProgress()
|
|
669
668
|
}
|
|
670
669
|
for (const pluginId of [...kitPluginIds].sort(compareCodepoints)) {
|
|
671
670
|
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
672
671
|
progress(`Updating plugin ${pluginId}...`)
|
|
673
|
-
const updateResult = cli(["plugin", "update", pluginId, "--scope", "user"])
|
|
672
|
+
const updateResult = await cli(["plugin", "update", pluginId, "--scope", "user"])
|
|
674
673
|
clearProgress()
|
|
675
674
|
if (updateResult.out.includes("Successfully updated")) updatedPl++
|
|
676
675
|
}
|
|
@@ -686,7 +685,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
686
685
|
if (isObject(sotPlugins) && Object.prototype.hasOwnProperty.call(sotPlugins, pluginId)) continue
|
|
687
686
|
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
688
687
|
progress(`Uninstalling plugin ${pluginId}...`)
|
|
689
|
-
const uninstallResult = cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId])
|
|
688
|
+
const uninstallResult = await cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId])
|
|
690
689
|
clearProgress()
|
|
691
690
|
if (uninstallResult.ok) {
|
|
692
691
|
removedPl++
|
|
@@ -702,7 +701,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
702
701
|
const declared = isObject(sotMarketplaces) ? sotMarketplaces[mpName] : undefined
|
|
703
702
|
if (declared !== undefined && declared !== null && declared !== false) continue
|
|
704
703
|
progress(`Removing marketplace ${mpName}...`)
|
|
705
|
-
const removeResult = cli(["plugin", "marketplace", "remove", mpName])
|
|
704
|
+
const removeResult = await cli(["plugin", "marketplace", "remove", mpName])
|
|
706
705
|
clearProgress()
|
|
707
706
|
if (removeResult.ok) {
|
|
708
707
|
removedMp++
|
|
@@ -714,7 +713,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
714
713
|
}
|
|
715
714
|
|
|
716
715
|
// Pass 6 — re-assert SoT enabled-state in the user settings.
|
|
717
|
-
if (reassertEnabledState(ctx, repoObj, p(claudeDir, "settings.json"))) {
|
|
716
|
+
if (await reassertEnabledState(ctx, repoObj, p(claudeDir, "settings.json"))) {
|
|
718
717
|
change("Plugin enable-state re-asserted from SoT in settings.json")
|
|
719
718
|
ctx.nextStepTriggers.claudePlugins = true
|
|
720
719
|
}
|
|
@@ -731,7 +730,7 @@ function syncPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
731
730
|
}
|
|
732
731
|
}
|
|
733
732
|
|
|
734
|
-
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> {
|
|
735
734
|
const { warn } = ctx.services.logger
|
|
736
735
|
if (!existsSync(userSettingsFile)) return false
|
|
737
736
|
const sotPlugins = isObject(repoObj["enabledPlugins"]) ? repoObj["enabledPlugins"] : {}
|
|
@@ -742,7 +741,7 @@ function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSett
|
|
|
742
741
|
const user = readJsonFile(userSettingsFile)
|
|
743
742
|
const enabled = user !== undefined && isObject(user) && isObject(user["enabledPlugins"]) ? (user["enabledPlugins"] as { [k: string]: Json })[pluginId] : undefined
|
|
744
743
|
if (enabled !== true) continue
|
|
745
|
-
if (cli(["plugin", "disable", pluginId]).ok) {
|
|
744
|
+
if ((await cli(["plugin", "disable", pluginId])).ok) {
|
|
746
745
|
cliDisabled = true
|
|
747
746
|
} else {
|
|
748
747
|
warn(`Failed to disable SoT-false plugin: ${pluginId} (will retry next sync)`)
|
|
@@ -765,7 +764,7 @@ function reassertEnabledState(ctx: Ctx, repoObj: { [k: string]: Json }, userSett
|
|
|
765
764
|
|
|
766
765
|
// ------------------------------------------------------ optional plugins ----
|
|
767
766
|
|
|
768
|
-
function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): boolean {
|
|
767
|
+
async function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, marketplaceRepo: string): Promise<boolean> {
|
|
769
768
|
const { change, clearProgress, progress, verbose, warn } = ctx.services.logger
|
|
770
769
|
const installedPlugins = p(claudeDir, "plugins", "installed_plugins.json")
|
|
771
770
|
const knownMarketplaces = p(claudeDir, "plugins", "known_marketplaces.json")
|
|
@@ -776,7 +775,7 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
|
|
|
776
775
|
const known = readJsonFile(knownMarketplaces)
|
|
777
776
|
const has = known !== undefined && isObject(known) && known[mpName] !== undefined && known[mpName] !== null && known[mpName] !== false
|
|
778
777
|
if (!has) {
|
|
779
|
-
if (!cli(["plugin", "marketplace", "add", marketplaceRepo]).ok) {
|
|
778
|
+
if (!(await cli(["plugin", "marketplace", "add", marketplaceRepo])).ok) {
|
|
780
779
|
warn(`Failed to add marketplace ${marketplaceRepo} for ${pluginId}`)
|
|
781
780
|
return false
|
|
782
781
|
}
|
|
@@ -787,7 +786,7 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
|
|
|
787
786
|
const wasInstalled = pluginUserScopeInstalled(installedPlugins, pluginId)
|
|
788
787
|
if (!wasInstalled) {
|
|
789
788
|
progress(`Installing plugin ${pluginId}...`)
|
|
790
|
-
const installResult = cli(["plugin", "install", pluginId])
|
|
789
|
+
const installResult = await cli(["plugin", "install", pluginId])
|
|
791
790
|
clearProgress()
|
|
792
791
|
if (!installResult.ok) {
|
|
793
792
|
if (marketplaceAdded) change(`Optional plugin ${pluginId}: marketplace added (install failed — will retry next sync)`)
|
|
@@ -802,7 +801,7 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
|
|
|
802
801
|
? (settingsDoc["enabledPlugins"] as { [k: string]: Json })[pluginId] === true
|
|
803
802
|
: false
|
|
804
803
|
|
|
805
|
-
if (!cli(["plugin", "enable", pluginId]).ok) {
|
|
804
|
+
if (!(await cli(["plugin", "enable", pluginId])).ok) {
|
|
806
805
|
if (marketplaceAdded || !wasInstalled) change(`Optional plugin ${pluginId}: installed (enable failed — will retry next sync)`)
|
|
807
806
|
warn(`Failed to enable optional plugin ${pluginId}`)
|
|
808
807
|
return marketplaceAdded || !wasInstalled
|
|
@@ -813,7 +812,7 @@ function enableOptionalPlugin(ctx: Ctx, claudeDir: string, pluginId: string, mar
|
|
|
813
812
|
return changed
|
|
814
813
|
}
|
|
815
814
|
|
|
816
|
-
function syncOptionalPlugins(ctx: Ctx, claudeDir: string): void {
|
|
815
|
+
async function syncOptionalPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
817
816
|
const { echo, warn } = ctx.services.logger
|
|
818
817
|
if (ctx.claudePlugins.length === 0) return
|
|
819
818
|
|
|
@@ -833,10 +832,10 @@ function syncOptionalPlugins(ctx: Ctx, claudeDir: string): void {
|
|
|
833
832
|
}
|
|
834
833
|
|
|
835
834
|
if (ctx.claudePlugins.includes("supabase")) {
|
|
836
|
-
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
|
|
837
836
|
}
|
|
838
837
|
if (ctx.claudePlugins.includes("n8n")) {
|
|
839
|
-
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
|
|
840
839
|
}
|
|
841
840
|
}
|
|
842
841
|
|
|
@@ -847,7 +846,7 @@ function lspPkg(ctx: Ctx, tool: string, pkg: string): string {
|
|
|
847
846
|
return v !== "" ? `${pkg}@${v}` : pkg
|
|
848
847
|
}
|
|
849
848
|
|
|
850
|
-
function syncLspServers(ctx: Ctx): void {
|
|
849
|
+
async function syncLspServers(ctx: Ctx): Promise<void> {
|
|
851
850
|
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
852
851
|
const sot = parseJson(payloadText("SoT/.claude/settings.json"))
|
|
853
852
|
const enabled = sot !== undefined && isObject(sot) && isObject(sot["enabledPlugins"]) ? sot["enabledPlugins"] : undefined
|
|
@@ -889,9 +888,9 @@ function syncLspServers(ctx: Ctx): void {
|
|
|
889
888
|
|
|
890
889
|
verbose(`Installing LSP servers via npm: ${specs}...`)
|
|
891
890
|
progress(`Installing LSP servers via npm: ${specs}...`)
|
|
892
|
-
const installResult =
|
|
891
|
+
const installResult = await spawnProcess("npm", ["install", "-g", ...missing], { stdio: "ignore" })
|
|
893
892
|
clearProgress()
|
|
894
|
-
if (installResult.
|
|
893
|
+
if (installResult.exitCode === 0) {
|
|
895
894
|
change(`LSP servers installed (${specs})`)
|
|
896
895
|
ctx.nextStepTriggers.claudeRestart = true
|
|
897
896
|
} else {
|
|
@@ -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
|
|
|
@@ -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,7 +490,7 @@ function installedPluginIdsFromCli(): Set<string> | undefined {
|
|
|
490
490
|
return ids
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
-
function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
493
|
+
async function syncPlugins(ctx: Ctx, sotConfigText: string): Promise<void> {
|
|
494
494
|
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
|
|
495
495
|
if (ctx.dryRun) {
|
|
496
496
|
echo(
|
|
@@ -520,7 +520,7 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
|
520
520
|
let pluginIds = desiredPluginIds
|
|
521
521
|
if (ctx.skipPluginRefresh) {
|
|
522
522
|
progress("Checking installed Codex plugins...")
|
|
523
|
-
const installedPluginIds = installedPluginIdsFromCli()
|
|
523
|
+
const installedPluginIds = await installedPluginIdsFromCli()
|
|
524
524
|
clearProgress()
|
|
525
525
|
if (installedPluginIds === undefined) {
|
|
526
526
|
warn("Codex plugin inventory unavailable — falling back to the full refresh path")
|
|
@@ -533,10 +533,10 @@ function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
|
533
533
|
let failed = 0
|
|
534
534
|
for (const pluginId of pluginIds) {
|
|
535
535
|
progress(`Updating Codex plugin ${pluginId}...`)
|
|
536
|
-
const res =
|
|
536
|
+
const res = await spawnProcess("codex", ["plugin", "add", pluginId], { stdio: ["ignore", "pipe", "pipe"] })
|
|
537
537
|
clearProgress()
|
|
538
|
-
const addOut = `${res.stdout
|
|
539
|
-
if (res.error === undefined && res.
|
|
538
|
+
const addOut = `${res.stdout}${res.stderr}`
|
|
539
|
+
if (res.error === undefined && res.exitCode === 0) {
|
|
540
540
|
refreshed++
|
|
541
541
|
} else if (addOut.includes("could not find a Codex CLI binary")) {
|
|
542
542
|
warn(
|