docks-kit 0.15.2 → 0.15.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +21 -15
- package/README.md +33 -31
- package/cli/docs/flags.md +0 -1
- package/cli/docs/install.md +41 -30
- package/cli/docs/overview.md +2 -2
- package/cli/docs/platforms.md +5 -2
- package/cli/docs/sync-layers.md +3 -4
- package/cli/docs/toolchain.md +28 -34
- package/cli/src/commands/docs.ts +3 -3
- package/cli/src/commands/sync.ts +0 -5
- package/cli/src/commands/toolchain.ts +4 -7
- package/cli/src/commands/update.ts +77 -26
- package/cli/src/engine-native/DESIGN.md +31 -22
- package/cli/src/engine-native/bun.ts +10 -8
- package/cli/src/engine-native/claudeRuntime.ts +17 -9
- package/cli/src/engine-native/claudeSync.ts +52 -24
- package/cli/src/engine-native/codexSync.ts +10 -5
- package/cli/src/engine-native/deps.ts +27 -88
- package/cli/src/engine-native/exec.ts +41 -10
- package/cli/src/engine-native/index.ts +0 -2
- package/cli/src/engine-native/modes.ts +23 -14
- package/cli/src/engine-native/os/darwin.ts +62 -0
- package/cli/src/engine-native/os/index.ts +42 -0
- package/cli/src/engine-native/os/linux.ts +62 -0
- package/cli/src/engine-native/os/targets.ts +73 -0
- package/cli/src/engine-native/os/types.ts +75 -0
- package/cli/src/engine-native/os/windows.ts +176 -0
- package/cli/src/engine-native/parseArgs.ts +0 -4
- package/cli/src/engine-native/services.ts +0 -6
- package/cli/src/engine-native/skillsSync.ts +125 -77
- package/cli/src/engine-native/toolchain.ts +4 -150
- package/cli/src/engine.ts +8 -7
- package/cli/src/generated/sotPayload.ts +6 -6
- package/cli/src/manifests.ts +12 -2
- package/docks-kit +43 -2
- package/docks-kit.ps1 +153 -0
- package/package.json +11 -7
- package/cli/src/engine-native/os.ts +0 -16
|
@@ -1,20 +1,62 @@
|
|
|
1
1
|
import { Command, Flag } from "effect/unstable/cli"
|
|
2
2
|
import { Console, Effect } from "effect"
|
|
3
|
-
import { spawnSync } from "node:child_process"
|
|
3
|
+
import { spawnSync, type SpawnSyncOptions, type SpawnSyncOptionsWithStringEncoding, type SpawnSyncReturns } from "node:child_process"
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs"
|
|
5
|
-
import { join } from "node:path"
|
|
6
5
|
import { bail, compiled } from "../engine"
|
|
7
6
|
import { kitHome } from "../kitHome"
|
|
7
|
+
import { p, which } from "../engine-native/exec"
|
|
8
|
+
import { hostOs, type HostOs, type Invocation } from "../engine-native/os"
|
|
8
9
|
|
|
9
10
|
const noSync = Flag.boolean("no-sync").pipe(
|
|
10
11
|
Flag.withDescription("Update the kit only; skip the chained flag-less sync")
|
|
11
12
|
)
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
/** A tool this host cannot resolve, shaped like the failed spawn it replaces. */
|
|
15
|
+
const notFound = (command: string): SpawnSyncReturns<string> => ({
|
|
16
|
+
pid: 0,
|
|
17
|
+
output: [],
|
|
18
|
+
stdout: "",
|
|
19
|
+
stderr: "",
|
|
20
|
+
status: null,
|
|
21
|
+
signal: null,
|
|
22
|
+
error: new Error(`command not found on PATH: ${command}`)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Every child in this command starts here, because two host facts must never be
|
|
27
|
+
* separated from the argv they describe: a Windows shim invocation is only
|
|
28
|
+
* correct with the verbatim-arguments flag, and a pathless name would let
|
|
29
|
+
* CreateProcess search the parent's current directory before the system one.
|
|
30
|
+
*/
|
|
31
|
+
export const spawnUpdate = (
|
|
32
|
+
command: string,
|
|
33
|
+
args: ReadonlyArray<string>,
|
|
34
|
+
overrides: SpawnSyncOptions = {},
|
|
35
|
+
host: HostOs = hostOs()
|
|
36
|
+
): SpawnSyncReturns<string> => {
|
|
37
|
+
const resolvesSuffixes = host.executableSuffixes.some((suffix) => suffix !== "")
|
|
38
|
+
const executablePath = resolvesSuffixes ? which(command, host.executableSuffixes) : command
|
|
39
|
+
if (executablePath === "") return notFound(command)
|
|
40
|
+
let invocation: Invocation
|
|
41
|
+
try {
|
|
42
|
+
invocation = host.invoke(executablePath, args)
|
|
43
|
+
} catch (cause) {
|
|
44
|
+
// A value this host cannot put on a command line at all. Print the encoder's
|
|
45
|
+
// reason and exit, matching how this command reports a failed child.
|
|
46
|
+
process.stderr.write(`${cause instanceof Error ? cause.message : String(cause)}\n`)
|
|
47
|
+
return process.exit(2)
|
|
48
|
+
}
|
|
49
|
+
const options: SpawnSyncOptionsWithStringEncoding = {
|
|
50
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
51
|
+
...overrides,
|
|
15
52
|
encoding: "utf8",
|
|
16
|
-
|
|
17
|
-
}
|
|
53
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
54
|
+
}
|
|
55
|
+
return spawnSync(invocation.command, [...invocation.args], options)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const git = (home: string, args: Array<string>): { ok: boolean; out: string } => {
|
|
59
|
+
const res = spawnUpdate("git", ["-C", home, ...args])
|
|
18
60
|
return { ok: res.error === undefined && res.status === 0, out: `${res.stdout ?? ""}${res.stderr ?? ""}`.trim() }
|
|
19
61
|
}
|
|
20
62
|
|
|
@@ -22,19 +64,19 @@ const git = (home: string, args: Array<string>): { ok: boolean; out: string } =>
|
|
|
22
64
|
* version loaded, so the chained sync must be a new process. */
|
|
23
65
|
const chainSync = (argv0: string, args: Array<string>): Effect.Effect<void> =>
|
|
24
66
|
Effect.sync(() => {
|
|
25
|
-
const res =
|
|
67
|
+
const res = spawnUpdate(argv0, args, { stdio: "inherit" })
|
|
26
68
|
if (res.error !== undefined || res.status !== 0) process.exit(res.status ?? 1)
|
|
27
69
|
})
|
|
28
70
|
|
|
29
71
|
export const updateSyncArgs = (home: string): Array<string> => [
|
|
30
|
-
|
|
72
|
+
p(home, "cli/src/main.ts"),
|
|
31
73
|
"sync",
|
|
32
74
|
"--skip-plugin-refresh"
|
|
33
75
|
]
|
|
34
76
|
|
|
35
77
|
const readPackageVersion = (home: string): string => {
|
|
36
78
|
try {
|
|
37
|
-
const doc: unknown = JSON.parse(readFileSync(
|
|
79
|
+
const doc: unknown = JSON.parse(readFileSync(p(home, "package.json"), "utf8"))
|
|
38
80
|
if (doc === null || typeof doc !== "object" || !("version" in doc)) return ""
|
|
39
81
|
return typeof doc.version === "string" ? doc.version : ""
|
|
40
82
|
} catch {
|
|
@@ -56,10 +98,7 @@ type CapturePackageRoot = (
|
|
|
56
98
|
) => PackageRootCapture
|
|
57
99
|
|
|
58
100
|
const capturePackageRoot: CapturePackageRoot = (command, args) => {
|
|
59
|
-
const res =
|
|
60
|
-
encoding: "utf8",
|
|
61
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
62
|
-
})
|
|
101
|
+
const res = spawnUpdate(command, args)
|
|
63
102
|
return {
|
|
64
103
|
status: res.status,
|
|
65
104
|
stdout: res.stdout ?? "",
|
|
@@ -67,15 +106,27 @@ const capturePackageRoot: CapturePackageRoot = (command, args) => {
|
|
|
67
106
|
}
|
|
68
107
|
}
|
|
69
108
|
|
|
109
|
+
/**
|
|
110
|
+
* A Bun global home is `<root>/.bun/install/global/node_modules/<pkg>`. Windows
|
|
111
|
+
* reports that path with backslashes, so containment is tested on a normalized
|
|
112
|
+
* copy — but only on Windows, because a backslash is a legal POSIX filename
|
|
113
|
+
* character and must never be read as a separator there.
|
|
114
|
+
*/
|
|
70
115
|
export const packageManagerForHome = (
|
|
71
116
|
home: string,
|
|
72
|
-
environment: NodeJS.ProcessEnv = process.env
|
|
117
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
118
|
+
host: HostOs = hostOs()
|
|
73
119
|
): PackageManager => {
|
|
120
|
+
const normalize = (value: string): string =>
|
|
121
|
+
host.id === "windows" ? value.replaceAll("\\", "/") : value
|
|
122
|
+
const normalizedHome = normalize(home)
|
|
74
123
|
const underEnvironmentRoot = (name: "BUN_INSTALL_GLOBAL_DIR" | "BUN_INSTALL"): boolean => {
|
|
75
124
|
const root = environment[name]?.trim()
|
|
76
|
-
|
|
125
|
+
if (root === undefined || root === "") return false
|
|
126
|
+
const normalizedRoot = normalize(root)
|
|
127
|
+
return normalizedHome === normalizedRoot || normalizedHome.startsWith(`${normalizedRoot}/`)
|
|
77
128
|
}
|
|
78
|
-
return
|
|
129
|
+
return normalizedHome.includes("/.bun/") ||
|
|
79
130
|
underEnvironmentRoot("BUN_INSTALL_GLOBAL_DIR") ||
|
|
80
131
|
underEnvironmentRoot("BUN_INSTALL")
|
|
81
132
|
? "bun"
|
|
@@ -107,7 +158,7 @@ export const resolveGlobalPackageHome = (
|
|
|
107
158
|
const root = result.stdout.trim()
|
|
108
159
|
return root === ""
|
|
109
160
|
? { ok: false, diagnostic: "npm root -g failed: empty output" }
|
|
110
|
-
: { ok: true, home:
|
|
161
|
+
: { ok: true, home: p(root, "docks-kit") }
|
|
111
162
|
}
|
|
112
163
|
|
|
113
164
|
const globalHeader = result.stdout
|
|
@@ -120,7 +171,7 @@ export const resolveGlobalPackageHome = (
|
|
|
120
171
|
: /^(.*) node_modules(?: \(\d+\))?$/.exec(globalHeader)?.[1]
|
|
121
172
|
return globalDir === undefined || globalDir === ""
|
|
122
173
|
? { ok: false, diagnostic: "bun pm -g ls did not report its global package root" }
|
|
123
|
-
: { ok: true, home:
|
|
174
|
+
: { ok: true, home: p(globalDir, "node_modules", "docks-kit") }
|
|
124
175
|
}
|
|
125
176
|
|
|
126
177
|
export const packageUpdateResult = (
|
|
@@ -143,7 +194,7 @@ export const packageUpdateResult = (
|
|
|
143
194
|
|
|
144
195
|
const updateCheckout = (home: string, skipSync: boolean) =>
|
|
145
196
|
Effect.gen(function* () {
|
|
146
|
-
if (
|
|
197
|
+
if (spawnUpdate("git", ["--version"], { stdio: "ignore" }).status !== 0) {
|
|
147
198
|
return yield* bail("git not found - cannot update the kit checkout")
|
|
148
199
|
}
|
|
149
200
|
const dirty = git(home, ["status", "--porcelain"])
|
|
@@ -169,7 +220,7 @@ const updateCheckout = (home: string, skipSync: boolean) =>
|
|
|
169
220
|
|
|
170
221
|
const touched = git(home, ["diff", "--name-only", before, after]).out.split("\n")
|
|
171
222
|
if (touched.includes("bun.lock") || touched.includes("package.json")) {
|
|
172
|
-
const res =
|
|
223
|
+
const res = spawnUpdate("bun", ["install", "--frozen-lockfile"], { cwd: home, stdio: "inherit" })
|
|
173
224
|
if (res.error !== undefined || res.status !== 0) {
|
|
174
225
|
return yield* bail("dependencies changed but 'bun install --frozen-lockfile' failed - fix that, then run docks-kit sync", 1)
|
|
175
226
|
}
|
|
@@ -189,10 +240,10 @@ const updatePackage = (home: string, skipSync: boolean) =>
|
|
|
189
240
|
Effect.gen(function* () {
|
|
190
241
|
const manager = packageManagerForHome(home)
|
|
191
242
|
const beforeVersion = readPackageVersion(home)
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
243
|
+
const updateArgs = manager === "bun"
|
|
244
|
+
? ["add", "-g", "docks-kit@latest"]
|
|
245
|
+
: ["install", "-g", "docks-kit@latest"]
|
|
246
|
+
const res = spawnUpdate(manager, updateArgs, { stdio: "inherit" })
|
|
196
247
|
if (res.error !== undefined || res.status !== 0) {
|
|
197
248
|
return yield* bail(
|
|
198
249
|
`global package update failed (${manager === "bun" ? "bun add -g" : "npm install -g"} docks-kit@latest)`,
|
|
@@ -210,7 +261,7 @@ const updatePackage = (home: string, skipSync: boolean) =>
|
|
|
210
261
|
const afterVersion = readPackageVersion(updated.home)
|
|
211
262
|
if (afterVersion === "") {
|
|
212
263
|
return yield* bail(
|
|
213
|
-
`global package update completed, but ${
|
|
264
|
+
`global package update completed, but ${p(updated.home, "package.json")} has no readable version`,
|
|
214
265
|
1
|
|
215
266
|
)
|
|
216
267
|
}
|
|
@@ -225,7 +276,7 @@ const updatePackage = (home: string, skipSync: boolean) =>
|
|
|
225
276
|
export const updateCommand = Command.make("update", { noSync }, (config) =>
|
|
226
277
|
Effect.gen(function* () {
|
|
227
278
|
const home = kitHome()
|
|
228
|
-
if (existsSync(
|
|
279
|
+
if (existsSync(p(home, ".git"))) {
|
|
229
280
|
return yield* updateCheckout(home, config.noSync)
|
|
230
281
|
}
|
|
231
282
|
if (home.includes("node_modules")) {
|
|
@@ -38,8 +38,8 @@ explicit removed-engine diagnostic and exits 2 with the recovery tag message.
|
|
|
38
38
|
serial, and summaries retain canonical Claude, Codex, skills order.
|
|
39
39
|
- **External CLIs stay external.** `claude`, `codex`, `npx`, `npm`, `bun`,
|
|
40
40
|
`curl`, and platform package managers are spawned with argv arrays,
|
|
41
|
-
not shell command strings except where
|
|
42
|
-
|
|
41
|
+
not shell command strings except where an external installer contract
|
|
42
|
+
requires a script interpreter.
|
|
43
43
|
- **Backups precede mutation.** Deployed settings/config files write `.bak`
|
|
44
44
|
backups before replacement.
|
|
45
45
|
- **Runtime payload is in memory.** `SoT/` remains the reviewed authoring tree;
|
|
@@ -97,7 +97,7 @@ each such skip is an intentional behavior change named in its golden diff.
|
|
|
97
97
|
Exactly one deduplicated warn per requested missing tool per run, uniform shape:
|
|
98
98
|
`[warn] <tool> not installed — <platform-correct install command>`, sourced
|
|
99
99
|
from the dependency registry (`deps.ts`). jq and curl are optional report rows:
|
|
100
|
-
jq has no runtime consumer, while curl warns only when a requested
|
|
100
|
+
jq has no runtime consumer, while curl warns only when a requested Bun
|
|
101
101
|
bootstrap needs an installer download. A missing Bun defers Claude runtime
|
|
102
102
|
migration without deleting working legacy hooks or statusline files.
|
|
103
103
|
|
|
@@ -116,10 +116,11 @@ changed → restart line; skills changed → discovery line) or under `--verbose
|
|
|
116
116
|
|
|
117
117
|
### Platform seam
|
|
118
118
|
|
|
119
|
-
All host detection routes through `os.ts`, the engine
|
|
120
|
-
`process.platform
|
|
121
|
-
|
|
122
|
-
|
|
119
|
+
All host detection routes through `os/index.ts`, which holds the engine's only
|
|
120
|
+
`process.platform` read and keeps platform normalization injectable for tests.
|
|
121
|
+
Per-OS facts live in `os/linux.ts`, `os/darwin.ts`, and `os/windows.ts` behind
|
|
122
|
+
`HostOs`; consumers select those facts through the package rather than branching
|
|
123
|
+
on the host directly.
|
|
123
124
|
|
|
124
125
|
### Verbosity plumbing
|
|
125
126
|
|
|
@@ -139,43 +140,51 @@ active logger binding.
|
|
|
139
140
|
| `index.ts` | sync orchestration, target dispatch, run summary and next-step blocks |
|
|
140
141
|
| `../payload.ts` | generated text/byte payload reads and presentation-only source labels |
|
|
141
142
|
| `claudeSync.ts` | Claude pipeline: Bun bootstrap, prepared settings transaction, runtime assets, deploy-time modifiers, `~/.claude.json`, readiness-gated removed artifacts, plugins, optional plugins, LSP binaries |
|
|
142
|
-
| `bun.ts` | per-run memoized Bun resolution/bootstrap shared by Claude runtime
|
|
143
|
-
| `claudeRuntime.ts` | sentinel validation, absolute runtime paths, no-cutover settings projection, and
|
|
143
|
+
| `bun.ts` | per-run memoized Bun resolution/bootstrap shared by the Claude runtime and direct toolchain ensure |
|
|
144
|
+
| `claudeRuntime.ts` | sentinel validation, absolute runtime paths, no-cutover settings projection, and per-host statusline and failure-hook command materialization |
|
|
144
145
|
| `settings.ts` | pure Claude settings merge/reconcile semantics and permission-array union |
|
|
145
146
|
| `claudeModel.ts` | deployed Claude model modifier and direct `model claude` write path |
|
|
146
147
|
| `codexSync.ts` | Codex pipeline: bubblewrap check, config merge, rules, AGENTS.md, personal marketplace, plugin refresh |
|
|
147
148
|
| `codexToml.ts` | line-based top-level TOML replacement and deployed Codex model modifier |
|
|
148
|
-
| `skillsSync.ts` | universal skill install/prune,
|
|
149
|
-
| `toolchain.ts` | tool presence/version probes, verified-version
|
|
149
|
+
| `skillsSync.ts` | universal skill install/prune, ordered symlink/junction/copy fallback, Claude entry healing, managed-skill snapshot |
|
|
150
|
+
| `toolchain.ts` | tool presence/version probes, verified-version floor reporting, report table |
|
|
150
151
|
| `modes.ts` | direct `model` and `toolchain` modes |
|
|
151
152
|
| `models.ts` | model catalog listing and validation |
|
|
152
153
|
| `jq.ts` | JSON helpers that preserve jq-style merge/order/stringify behavior where the deployed file contract needs it |
|
|
153
|
-
| `exec.ts` | slash-stable path helpers,
|
|
154
|
+
| `exec.ts` | slash-stable path helpers, host-aware PATH probes and invocation, capture/spawn wrappers, and change-detecting write/copy helpers |
|
|
154
155
|
| `logger.ts` | Logger shape + stable raw stdout/stderr sink factory; the run-scoped verbosity gate lives in `index.ts` |
|
|
155
156
|
| `deps.ts` | external-tool registry: identity, requirement class, presence probe, supported-host install hints, per-manager missing-tool dedup; callers supply the current run Logger to `warnMissing` |
|
|
156
|
-
| `os
|
|
157
|
+
| `os/` | host reader, injected platform normalization, and per-OS `HostOs` fact modules |
|
|
157
158
|
| `services.ts` | shared raw-Logger + DependencyManager + Platform factory; wrapped in Effect Layers at `cli/src/services.ts`, with the run-scoped Logger gate applied only by `runEngineNative` |
|
|
158
159
|
|
|
159
160
|
## Platform Support
|
|
160
161
|
|
|
161
|
-
- EngineNative supports Linux and
|
|
162
|
+
- EngineNative supports Linux, macOS, and Windows on x64 and arm64.
|
|
162
163
|
- Unsupported hosts fail before launcher fallback, dependency probes, downloads,
|
|
163
164
|
settings writes, or sync work.
|
|
164
|
-
-
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
- Claude runtime settings use absolute Bun/script paths and materialize
|
|
166
|
+
statusline and failure-hook commands through the selected `HostOs`.
|
|
167
|
+
- Directory linking is capability-driven. The host module supplies only the
|
|
168
|
+
order while the runtime decides the outcome: symlink, then a Windows junction
|
|
169
|
+
with an absolute target, then a recursive copy. Copies carry a kit marker so
|
|
170
|
+
later sync can heal them back to a real link and prune can reclaim them
|
|
171
|
+
without touching user-owned directories.
|
|
167
172
|
|
|
168
173
|
## Tests
|
|
169
174
|
|
|
170
|
-
- `bun run test:unit` covers pure JSON merge semantics
|
|
175
|
+
- `bun run test:unit` covers pure JSON merge semantics, jq-oracle cases, and
|
|
176
|
+
the three `HostOs` modules independently of the current host.
|
|
171
177
|
- `bun run golden:dryrun` compares live native dry-run output to
|
|
172
178
|
`cli/test/goldens/dryrun.json`.
|
|
173
179
|
- `bun run golden:mutation` compares live native mutation snapshots, argv logs,
|
|
174
180
|
output, and TOML invariants to `cli/test/goldens/mutation.json`.
|
|
175
|
-
- `.github/workflows/parity.yml`
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
181
|
+
- `.github/workflows/parity.yml` runs its portable lane on `ubuntu-24.04`,
|
|
182
|
+
`macos-26`, and `windows-2025`: typecheck, unit tests, host-specific runtime
|
|
183
|
+
command checks, and compilation and execution of the native host artifact.
|
|
184
|
+
The Linux-canonical snapshot lane stays on Ubuntu and runs both golden suites
|
|
185
|
+
plus their prove-red checks.
|
|
186
|
+
- `.github/workflows/release-cli.yml` publishes six binaries for Linux, macOS,
|
|
187
|
+
and Windows on x64 and arm64, plus `SHA256SUMS` and the npm package.
|
|
179
188
|
|
|
180
189
|
## Non-Goals
|
|
181
190
|
|
|
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os"
|
|
|
4
4
|
import { p, spawnProcess, type AsyncProcessOptions, type AsyncProcessResult } from "./exec"
|
|
5
5
|
import type { Ctx } from "./index"
|
|
6
6
|
import type { EngineServices } from "./services"
|
|
7
|
+
import { hostOs, type HostOs } from "./os"
|
|
7
8
|
import { field } from "./toolchain"
|
|
8
9
|
|
|
9
10
|
export type BunRuntimeState =
|
|
@@ -13,11 +14,11 @@ export type BunRuntimeState =
|
|
|
13
14
|
readonly reason: "missing-curl" | "download-failed" | "installer-failed" | "install-failed"
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
function predictedExecutable(ctx: Ctx): string {
|
|
17
|
+
function predictedExecutable(ctx: Ctx, host: HostOs = hostOs()): string {
|
|
17
18
|
const root = process.env["BUN_INSTALL"] !== undefined && process.env["BUN_INSTALL"] !== ""
|
|
18
19
|
? process.env["BUN_INSTALL"]!
|
|
19
20
|
: p(ctx.home, ".bun")
|
|
20
|
-
return p(root, "bin",
|
|
21
|
+
return p(root, "bin", host.bunExecutableName)
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
type BunInstallResult =
|
|
@@ -29,13 +30,14 @@ function processFailure(result: AsyncProcessResult): string {
|
|
|
29
30
|
return details.join(": ") || (result.exitCode === null ? "the process ended without an exit code" : `exit code ${result.exitCode}`)
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
async function installBun(pin: string,
|
|
33
|
+
async function installBun(pin: string, directory: string, host: HostOs = hostOs()): Promise<BunInstallResult> {
|
|
33
34
|
const options: AsyncProcessOptions = { stdio: ["ignore", "ignore", "pipe"] }
|
|
34
|
-
const
|
|
35
|
+
const installer = host.bunInstaller(pin, directory)
|
|
36
|
+
const download = await spawnProcess(installer.download.command, installer.download.args, options)
|
|
35
37
|
if (download.error !== undefined || download.exitCode !== 0) {
|
|
36
38
|
return { ok: false, reason: "download-failed", detail: processFailure(download) }
|
|
37
39
|
}
|
|
38
|
-
const install = await spawnProcess(
|
|
40
|
+
const install = await spawnProcess(installer.run.command, installer.run.args, options)
|
|
39
41
|
if (install.error !== undefined || install.exitCode !== 0) {
|
|
40
42
|
return { ok: false, reason: "installer-failed", detail: processFailure(install) }
|
|
41
43
|
}
|
|
@@ -50,6 +52,7 @@ export function bunBootstrap(ctx: Ctx, services: EngineServices): Promise<BunRun
|
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
async function runBunBootstrap(ctx: Ctx, services: EngineServices): Promise<BunRuntimeState> {
|
|
55
|
+
const host = hostOs(services.platform.name())
|
|
53
56
|
|
|
54
57
|
const existing = await services.deps.path("bun")
|
|
55
58
|
if (existing !== "") return { kind: "ready", executable: existing }
|
|
@@ -64,16 +67,15 @@ async function runBunBootstrap(ctx: Ctx, services: EngineServices): Promise<BunR
|
|
|
64
67
|
return { kind: "deferred", reason: "missing-curl" }
|
|
65
68
|
}
|
|
66
69
|
if (ctx.dryRun) {
|
|
67
|
-
const executable = predictedExecutable(ctx)
|
|
70
|
+
const executable = predictedExecutable(ctx, host)
|
|
68
71
|
services.logger.echo(`[dry-run] install Bun ${pin} (kit-verified) -> ${executable}`)
|
|
69
72
|
return { kind: "ready", executable }
|
|
70
73
|
}
|
|
71
74
|
services.logger.warn(`Bun not found — installing Bun ${pin} (kit-verified)...`)
|
|
72
75
|
const temporaryDir = mkdtempSync(p(tmpdir(), "docks-kit-bun-"))
|
|
73
|
-
const installer = p(temporaryDir, "install.sh")
|
|
74
76
|
let result: BunInstallResult
|
|
75
77
|
try {
|
|
76
|
-
result = await installBun(pin,
|
|
78
|
+
result = await installBun(pin, temporaryDir, host)
|
|
77
79
|
} finally {
|
|
78
80
|
rmSync(temporaryDir, { recursive: true, force: true })
|
|
79
81
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { p } from "./exec"
|
|
2
2
|
import { isObject, parseJson, type Json } from "./jq"
|
|
3
|
+
import { hostOs, type HostOs } from "./os"
|
|
3
4
|
|
|
4
5
|
const BUN_SENTINEL = "__DOCKS_KIT_BUN__"
|
|
5
6
|
const SESSION_START_SENTINEL = "__DOCKS_KIT_SESSION_START__"
|
|
@@ -83,23 +84,30 @@ function validateTemplate(template: Json): void {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
function posixLiteral(value: string): string {
|
|
87
|
-
return `'${value.replaceAll("'", `'"'"'`)}'`
|
|
88
|
-
}
|
|
89
87
|
|
|
90
|
-
export function statusLineCommand(runtime: ClaudeRuntimePaths): string {
|
|
91
|
-
|
|
92
|
-
const script = posixLiteral(runtime.statusline)
|
|
93
|
-
return `test -x ${bun} && test -f ${script} && exec ${bun} ${script} || true`
|
|
88
|
+
export function statusLineCommand(runtime: ClaudeRuntimePaths, host: HostOs = hostOs()): string {
|
|
89
|
+
return host.statusLineCommand(runtime.bun, runtime.statusline)
|
|
94
90
|
}
|
|
95
91
|
|
|
96
92
|
export function materializeClaudeSettings(
|
|
97
93
|
template: Json,
|
|
98
|
-
runtime: ClaudeRuntimePaths | undefined
|
|
94
|
+
runtime: ClaudeRuntimePaths | undefined,
|
|
95
|
+
host: HostOs = hostOs()
|
|
99
96
|
): Json {
|
|
100
97
|
validateTemplate(template)
|
|
101
98
|
const result = cloneJson(template)
|
|
102
99
|
const hooks = hooksObject(result)
|
|
100
|
+
const failureGroups = hooks["PostToolUseFailure"]
|
|
101
|
+
if (Array.isArray(failureGroups)) {
|
|
102
|
+
for (const group of failureGroups) {
|
|
103
|
+
if (!isObject(group) || !Array.isArray(group["hooks"])) continue
|
|
104
|
+
for (const handler of group["hooks"]) {
|
|
105
|
+
if (isObject(handler) && handler["type"] === "command" && typeof handler["command"] === "string") {
|
|
106
|
+
handler["command"] = host.failureHookCommand(handler["command"])
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
103
111
|
if (runtime === undefined) {
|
|
104
112
|
delete hooks["SessionStart"]
|
|
105
113
|
delete hooks["Notification"]
|
|
@@ -113,7 +121,7 @@ export function materializeClaudeSettings(
|
|
|
113
121
|
notification["command"] = runtime.bun
|
|
114
122
|
notification["args"] = [runtime.notify]
|
|
115
123
|
if (!isObject(result) || !isObject(result["statusLine"])) throw new Error("Claude statusLine object is missing")
|
|
116
|
-
result["statusLine"]["command"] = statusLineCommand(runtime)
|
|
124
|
+
result["statusLine"]["command"] = statusLineCommand(runtime, host)
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
for (const sentinel of [BUN_SENTINEL, SESSION_START_SENTINEL, NOTIFY_SENTINEL, STATUSLINE_SENTINEL]) {
|
|
@@ -26,6 +26,7 @@ import { p, spawnProcess, writeBytesIfChanged, writeTextIfChanged } from "./exec
|
|
|
26
26
|
import type { Ctx } from "./index"
|
|
27
27
|
import { compareCodepoints, deepMerge, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
28
28
|
import { ExitError } from "./parseArgs"
|
|
29
|
+
import { hostOs } from "./os"
|
|
29
30
|
import { mergeSettings, reconcileSettings } from "./settings"
|
|
30
31
|
import { field } from "./toolchain"
|
|
31
32
|
import { payloadBytes, payloadDisplayPath, payloadText } from "../payload"
|
|
@@ -75,7 +76,7 @@ export async function claudeSync(ctx: Ctx): Promise<ClaudeRuntimeState> {
|
|
|
75
76
|
syncClaudeEffort(ctx, ctx.claudeEffort)
|
|
76
77
|
syncClaudeAdvisor(ctx, ctx.claudeAdvisor)
|
|
77
78
|
syncClaudeJson(ctx)
|
|
78
|
-
syncConnectorEnv(ctx)
|
|
79
|
+
await syncConnectorEnv(ctx)
|
|
79
80
|
await syncPlugins(ctx, claudeDir)
|
|
80
81
|
await syncOptionalPlugins(ctx, claudeDir)
|
|
81
82
|
await syncLspServers(ctx)
|
|
@@ -320,36 +321,62 @@ function syncClaudeJson(ctx: Ctx): void {
|
|
|
320
321
|
|
|
321
322
|
// -------------------------------------------------------- connector env ----
|
|
322
323
|
|
|
323
|
-
function syncConnectorEnv(ctx: Ctx): void {
|
|
324
|
-
const { change, echo, verbose } = ctx.services.logger
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
324
|
+
async function syncConnectorEnv(ctx: Ctx): Promise<void> {
|
|
325
|
+
const { change, echo, verbose, warn } = ctx.services.logger
|
|
326
|
+
const name = "ENABLE_CLAUDEAI_MCP_SERVERS"
|
|
327
|
+
const setting = hostOs().environmentSetting(name, "false")
|
|
328
|
+
|
|
329
|
+
switch (setting.kind) {
|
|
330
|
+
case "profile": {
|
|
331
|
+
const marker = "# docks-kit: disable claude.ai cloud MCP connectors (set =true to keep them)"
|
|
332
|
+
const candidates = setting.candidates.map((candidate) => p(ctx.home, candidate))
|
|
333
|
+
|
|
334
|
+
for (const f of candidates) {
|
|
335
|
+
if (existsSync(f) && readFileSync(f, "utf8").includes(name)) {
|
|
336
|
+
if (ctx.dryRun) {
|
|
337
|
+
echo(`[dry-run] ${name} already in ${f} — would skip`)
|
|
338
|
+
} else {
|
|
339
|
+
verbose(`claude.ai connectors: ${name} already set in ${f} (left as-is)`)
|
|
340
|
+
}
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
}
|
|
329
344
|
|
|
330
|
-
|
|
331
|
-
if (existsSync(f) && readFileSync(f, "utf8").includes("ENABLE_CLAUDEAI_MCP_SERVERS")) {
|
|
345
|
+
const target = p(ctx.home, setting.target(process.env["SHELL"]))
|
|
332
346
|
if (ctx.dryRun) {
|
|
333
|
-
echo(`[dry-run]
|
|
334
|
-
|
|
335
|
-
verbose(`claude.ai connectors: ENABLE_CLAUDEAI_MCP_SERVERS already set in ${f} (left as-is)`)
|
|
347
|
+
echo(`[dry-run] append '${setting.line}' to ${target}`)
|
|
348
|
+
return
|
|
336
349
|
}
|
|
350
|
+
|
|
351
|
+
appendFileSync(target, `\n${marker}\n${setting.line}\n`)
|
|
352
|
+
change(`claude.ai connectors disabled via ${target} (start a new shell to apply)`)
|
|
353
|
+
ctx.nextStepTriggers.claudeRestart = true
|
|
337
354
|
return
|
|
338
355
|
}
|
|
339
|
-
|
|
356
|
+
case "command": {
|
|
357
|
+
const existing = await spawnProcess(setting.probe.command, setting.probe.args, { stdio: "ignore" })
|
|
358
|
+
if (existing.error === undefined && existing.exitCode === 0) {
|
|
359
|
+
if (ctx.dryRun) echo(`[dry-run] ${name} already in ${setting.location} — would skip`)
|
|
360
|
+
else verbose(`claude.ai connectors: ${name} already set in ${setting.location} (left as-is)`)
|
|
361
|
+
return
|
|
362
|
+
}
|
|
340
363
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
364
|
+
const applyCommand = [setting.apply.command, ...setting.apply.args].join(" ")
|
|
365
|
+
if (ctx.dryRun) {
|
|
366
|
+
echo(`[dry-run] ${applyCommand} (${setting.location})`)
|
|
367
|
+
return
|
|
368
|
+
}
|
|
344
369
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
370
|
+
const applied = await spawnProcess(setting.apply.command, setting.apply.args, { stdio: "ignore" })
|
|
371
|
+
if (applied.error === undefined && applied.exitCode === 0) {
|
|
372
|
+
change(`claude.ai connectors disabled via ${setting.apply.command} (open a new terminal to apply)`)
|
|
373
|
+
ctx.nextStepTriggers.claudeRestart = true
|
|
374
|
+
} else {
|
|
375
|
+
warn(`${applyCommand} failed — ${setting.manualHint}`)
|
|
376
|
+
}
|
|
377
|
+
return
|
|
378
|
+
}
|
|
348
379
|
}
|
|
349
|
-
|
|
350
|
-
appendFileSync(target, `\n${marker}\n${line}\n`)
|
|
351
|
-
change(`claude.ai connectors disabled via ${target} (start a new shell to apply)`)
|
|
352
|
-
ctx.nextStepTriggers.claudeRestart = true
|
|
353
380
|
}
|
|
354
381
|
|
|
355
382
|
// ------------------------------------------------------------- removals ----
|
|
@@ -366,6 +393,7 @@ const REMOVED_MANIFEST = {
|
|
|
366
393
|
"env.CLAUDE_CODE_DISABLE_1M_CONTEXT",
|
|
367
394
|
"env.CLAUDE_CODE_FORK_SUBAGENT",
|
|
368
395
|
"env.CLAUDE_CODE_EFFORT_LEVEL",
|
|
396
|
+
"enabledPlugins.effect-kit@docks",
|
|
369
397
|
"enabledPlugins.session-relay@docks",
|
|
370
398
|
"hooks.PreToolUse"
|
|
371
399
|
],
|
|
@@ -375,7 +403,7 @@ const REMOVED_MANIFEST = {
|
|
|
375
403
|
},
|
|
376
404
|
claudeJsonKeys: [] as Array<string>,
|
|
377
405
|
/** Home-relative artifacts the kit installed outside ~/.claude. */
|
|
378
|
-
homeFiles: [".local/bin/session-relay"],
|
|
406
|
+
homeFiles: [".local/bin/effect-solutions", ".local/bin/session-relay"],
|
|
379
407
|
runtimeReady: {
|
|
380
408
|
hooks: ["notify.sh"],
|
|
381
409
|
files: ["statusline.sh", "fetch-usage.sh"],
|
|
@@ -9,6 +9,7 @@ import { syncCodexEffort, syncCodexModel, replaceTopLevelSettingInFile } from ".
|
|
|
9
9
|
import { p, spawnProcess } from "./exec"
|
|
10
10
|
import type { Ctx } from "./index"
|
|
11
11
|
import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
12
|
+
import { hostOs } from "./os"
|
|
12
13
|
import { payloadBytes, payloadDisplayPath, payloadPaths, payloadText, type PayloadPath } from "../payload"
|
|
13
14
|
|
|
14
15
|
export async function codexSync(ctx: Ctx): Promise<void> {
|
|
@@ -84,10 +85,11 @@ async function ensureBubblewrap(ctx: Ctx): Promise<void> {
|
|
|
84
85
|
|
|
85
86
|
function bwrapSupportedOs(ctx: Ctx): boolean {
|
|
86
87
|
const { warn } = ctx.services.logger
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
89
|
-
if (
|
|
90
|
-
|
|
88
|
+
const os = hostOs(ctx.services.platform.name())
|
|
89
|
+
if (os.supportsBubblewrap) return true
|
|
90
|
+
if (os.id === "unknown") {
|
|
91
|
+
warn("Unknown OS — skipping bubblewrap check; Codex sandbox may not work")
|
|
92
|
+
}
|
|
91
93
|
return false
|
|
92
94
|
}
|
|
93
95
|
|
|
@@ -202,7 +204,10 @@ function scrubDeprecatedFeatures(ctx: Ctx, userConfig: string): void {
|
|
|
202
204
|
|
|
203
205
|
const PLUGIN_TABLE_HEADER = /^\[plugins\."([^"]+)"\][ \t]*$/
|
|
204
206
|
/** Plugin ids the kit retired; their deployed tables are stripped on every sync. */
|
|
205
|
-
const RETIRED_PLUGIN_IDS: Readonly<Record<string, true>> = {
|
|
207
|
+
const RETIRED_PLUGIN_IDS: Readonly<Record<string, true>> = {
|
|
208
|
+
"effect-kit@docks": true,
|
|
209
|
+
"session-relay@docks": true
|
|
210
|
+
}
|
|
206
211
|
|
|
207
212
|
/** codex::remove_retired_plugin_tables — drop [plugins."<id>"] blocks for retired ids. */
|
|
208
213
|
export function removeRetiredPluginTablesText(content: string): string {
|