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
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
* DependencyManager — one home for external-tool identity, presence probing,
|
|
3
3
|
* and platform-correct install hints (Output Policy in DESIGN.md).
|
|
4
4
|
*
|
|
5
|
-
* Ownership split: SoT/toolchain.json + toolchain.ts keep version floors,
|
|
6
|
-
*
|
|
7
|
-
* WHICH external tools exist, whether they are
|
|
8
|
-
* command that installs a missing one.
|
|
5
|
+
* Ownership split: SoT/toolchain.json + toolchain.ts keep version floors, pin
|
|
6
|
+
* policy, and the doctor report, while bun.ts bunBootstrap owns the one managed
|
|
7
|
+
* install; this registry owns WHICH external tools exist, whether they are
|
|
8
|
+
* required, and the one-line command that installs a missing one.
|
|
9
9
|
*/
|
|
10
10
|
import { homedir } from "node:os"
|
|
11
11
|
import { isAbsolute } from "node:path"
|
|
12
12
|
|
|
13
13
|
import { capture, commandExists, p, which } from "./exec"
|
|
14
14
|
import { isObject, parseJson } from "./jq"
|
|
15
|
-
import { rawPlatform } from "./os"
|
|
15
|
+
import { hostOs, platformName, rawPlatform } from "./os"
|
|
16
16
|
|
|
17
17
|
export type ToolId =
|
|
18
18
|
| "git"
|
|
@@ -25,7 +25,6 @@ export type ToolId =
|
|
|
25
25
|
| "codex"
|
|
26
26
|
| "bun"
|
|
27
27
|
| "bwrap"
|
|
28
|
-
| "effect-solutions"
|
|
29
28
|
| "ffplay"
|
|
30
29
|
| "intelephense"
|
|
31
30
|
| "typescript-language-server"
|
|
@@ -42,11 +41,6 @@ export type ProbeResult =
|
|
|
42
41
|
| { readonly state: "present"; readonly path?: string }
|
|
43
42
|
| { readonly state: "missing" }
|
|
44
43
|
|
|
45
|
-
export interface DependencyLocation {
|
|
46
|
-
readonly path: string
|
|
47
|
-
readonly binDir: string
|
|
48
|
-
}
|
|
49
|
-
|
|
50
44
|
export interface ProbeExecutor {
|
|
51
45
|
readonly commandExists: (name: string) => boolean
|
|
52
46
|
readonly capture: (cmd: string, args: ReadonlyArray<string>) => Promise<string>
|
|
@@ -61,16 +55,12 @@ export interface DependencySpec {
|
|
|
61
55
|
readonly installHint: (platform?: NodeJS.Platform) => string
|
|
62
56
|
readonly resolve?: (exec: ProbeExecutor, platform: NodeJS.Platform) => ProbeResult
|
|
63
57
|
readonly version?: (exec: ProbeExecutor) => Promise<string>
|
|
64
|
-
readonly locate?: (exec: ProbeExecutor, platform: NodeJS.Platform) => Promise<DependencyLocation>
|
|
65
|
-
readonly latest?: (exec: ProbeExecutor) => Promise<string>
|
|
66
58
|
}
|
|
67
59
|
|
|
68
60
|
interface SpecOptions {
|
|
69
61
|
readonly versionArgs?: ReadonlyArray<string>
|
|
70
62
|
readonly resolve?: (exec: ProbeExecutor, platform: NodeJS.Platform) => ProbeResult
|
|
71
63
|
readonly version?: (exec: ProbeExecutor) => Promise<string>
|
|
72
|
-
readonly locate?: (exec: ProbeExecutor, platform: NodeJS.Platform) => Promise<DependencyLocation>
|
|
73
|
-
readonly latest?: (exec: ProbeExecutor) => Promise<string>
|
|
74
64
|
}
|
|
75
65
|
|
|
76
66
|
const spec = (
|
|
@@ -84,16 +74,17 @@ const spec = (
|
|
|
84
74
|
versionArgs: options.versionArgs ?? ["--version"],
|
|
85
75
|
installHint,
|
|
86
76
|
resolve: options.resolve,
|
|
87
|
-
version: options.version
|
|
88
|
-
locate: options.locate,
|
|
89
|
-
latest: options.latest
|
|
77
|
+
version: options.version
|
|
90
78
|
})
|
|
91
79
|
|
|
80
|
+
// Presence is the injected executor's verdict; `which` only resolves WHERE the
|
|
81
|
+
// tool is, which on Windows is the `.cmd`/`.exe` candidate rather than the name.
|
|
92
82
|
const pathProbe = (id: string): ((exec: ProbeExecutor) => ProbeResult) =>
|
|
93
|
-
(exec) =>
|
|
94
|
-
exec.commandExists(id)
|
|
95
|
-
|
|
96
|
-
|
|
83
|
+
(exec) => {
|
|
84
|
+
if (!exec.commandExists(id)) return { state: "missing" }
|
|
85
|
+
const path = exec.which(id)
|
|
86
|
+
return path === "" ? { state: "present" } : { state: "present", path }
|
|
87
|
+
}
|
|
97
88
|
|
|
98
89
|
const versionProbe = (
|
|
99
90
|
id: string,
|
|
@@ -135,31 +126,6 @@ const resolveBun = (exec: ProbeExecutor): ProbeResult => {
|
|
|
135
126
|
: { state: "present", path: bun.path }
|
|
136
127
|
}
|
|
137
128
|
|
|
138
|
-
const resolveEffectSolutions = (exec: ProbeExecutor): ProbeResult => {
|
|
139
|
-
return exec.commandExists("effect-solutions")
|
|
140
|
-
? { state: "present", path: exec.which("effect-solutions") }
|
|
141
|
-
: { state: "missing" }
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const versionEffectSolutions = async (exec: ProbeExecutor): Promise<string> => {
|
|
145
|
-
const bun = findBun(exec)
|
|
146
|
-
if (bun === undefined) return ""
|
|
147
|
-
const match = /effect-solutions@([0-9][0-9.]*)/.exec(await exec.capture(bun.command, ["pm", "-g", "ls"]))
|
|
148
|
-
return match?.[1] ?? ""
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const locateEffectSolutions = async (exec: ProbeExecutor): Promise<DependencyLocation> => {
|
|
152
|
-
const strictBun = findBun(exec)
|
|
153
|
-
const pathBun = exec.which("bun")
|
|
154
|
-
// This site may accept a relative hit because it does not persist the Bun path.
|
|
155
|
-
const bunForGlobalBin = strictBun?.command ?? (pathBun !== "" ? "bun" : undefined)
|
|
156
|
-
if (bunForGlobalBin === undefined) return { path: "", binDir: "" }
|
|
157
|
-
const globalBin = await exec.capture(bunForGlobalBin, ["pm", "-g", "bin"])
|
|
158
|
-
const path = globalBin !== "" ? p(globalBin, "effect-solutions") : ""
|
|
159
|
-
const resolved = path !== "" && exec.which(path) !== "" ? path : ""
|
|
160
|
-
return { path: resolved, binDir: globalBin }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
129
|
const npmGlobalCache = new WeakMap<ProbeExecutor, Promise<{ [k: string]: string }>>()
|
|
164
130
|
|
|
165
131
|
const npmGlobalVersions = (exec: ProbeExecutor): Promise<{ [k: string]: string }> => {
|
|
@@ -183,31 +149,21 @@ const npmGlobalVersions = (exec: ProbeExecutor): Promise<{ [k: string]: string }
|
|
|
183
149
|
const versionNpmGlobal = (pkg: string) => async (exec: ProbeExecutor): Promise<string> =>
|
|
184
150
|
(await npmGlobalVersions(exec))[pkg] ?? ""
|
|
185
151
|
|
|
186
|
-
const latestNpm = (id: "effect-solutions") => async (exec: ProbeExecutor): Promise<string> =>
|
|
187
|
-
exec.commandExists("npm") ? await exec.capture("npm", ["view", id, "version"]) : ""
|
|
188
|
-
|
|
189
152
|
export const defaultProbeExecutor: ProbeExecutor = { commandExists, capture, which }
|
|
190
153
|
|
|
191
154
|
export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
192
155
|
git: spec(
|
|
193
156
|
"git",
|
|
194
157
|
"optional",
|
|
195
|
-
(pf = rawPlatform()) =>
|
|
196
|
-
pf === "darwin"
|
|
197
|
-
? "brew install git"
|
|
198
|
-
: "sudo apt install -y git (or your distro's package manager)",
|
|
158
|
+
(pf = rawPlatform()) => hostOs(platformName(pf)).installHint("git"),
|
|
199
159
|
{ version: versionProbe("git") }
|
|
200
160
|
),
|
|
201
|
-
jq: spec("jq", "optional", (pf = rawPlatform()) =>
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
),
|
|
207
|
-
curl: spec("curl", "optional", (pf = rawPlatform()) =>
|
|
208
|
-
pf === "darwin" ? "brew install curl" : "sudo apt install -y curl",
|
|
209
|
-
{ version: versionProbe("curl") }
|
|
210
|
-
),
|
|
161
|
+
jq: spec("jq", "optional", (pf = rawPlatform()) => hostOs(platformName(pf)).installHint("jq"), {
|
|
162
|
+
version: versionProbe("jq")
|
|
163
|
+
}),
|
|
164
|
+
curl: spec("curl", "optional", (pf = rawPlatform()) => hostOs(platformName(pf)).installHint("curl"), {
|
|
165
|
+
version: versionProbe("curl")
|
|
166
|
+
}),
|
|
211
167
|
node: spec("node", "optional", () => "install Node.js via https://nodejs.org (or your package manager)", {
|
|
212
168
|
version: versionProbe("node")
|
|
213
169
|
}),
|
|
@@ -216,13 +172,13 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
216
172
|
claude: spec(
|
|
217
173
|
"claude",
|
|
218
174
|
"optional",
|
|
219
|
-
() => "
|
|
175
|
+
(pf = rawPlatform()) => hostOs(platformName(pf)).installHint("claude"),
|
|
220
176
|
{ version: versionProbe("claude") }
|
|
221
177
|
),
|
|
222
178
|
codex: spec(
|
|
223
179
|
"codex",
|
|
224
180
|
"optional",
|
|
225
|
-
() =>
|
|
181
|
+
(pf = rawPlatform()) => hostOs(platformName(pf)).installHint("codex"),
|
|
226
182
|
{ version: versionProbe("codex") }
|
|
227
183
|
),
|
|
228
184
|
bun: spec(
|
|
@@ -235,24 +191,16 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
235
191
|
const bun = findBun(exec)
|
|
236
192
|
if (bun === undefined) return ""
|
|
237
193
|
return await exec.capture(bun.command, ["--version"])
|
|
238
|
-
}
|
|
239
|
-
locate: async (exec) => ({ path: findBun(exec)?.path ?? "", binDir: "" })
|
|
194
|
+
}
|
|
240
195
|
}
|
|
241
196
|
),
|
|
242
197
|
bwrap: spec("bwrap", "optional", () => "sudo apt install -y bubblewrap (or dnf/pacman/zypper equivalent)", {
|
|
243
198
|
version: versionProbe("bwrap")
|
|
244
199
|
}),
|
|
245
|
-
"effect-solutions": spec("effect-solutions", "optional", () => "bun add -g effect-solutions", {
|
|
246
|
-
resolve: resolveEffectSolutions,
|
|
247
|
-
version: versionEffectSolutions,
|
|
248
|
-
locate: locateEffectSolutions,
|
|
249
|
-
latest: latestNpm("effect-solutions")
|
|
250
|
-
}),
|
|
251
200
|
ffplay: spec(
|
|
252
201
|
"ffplay",
|
|
253
202
|
"optional",
|
|
254
|
-
(pf = rawPlatform()) =>
|
|
255
|
-
pf === "darwin" ? "brew install ffmpeg" : "sudo apt install -y ffmpeg",
|
|
203
|
+
(pf = rawPlatform()) => hostOs(platformName(pf)).installHint("ffplay"),
|
|
256
204
|
{ versionArgs: ["-version"], version: versionProbe("ffplay", ["-version"]), resolve: pathProbe("ffplay") }
|
|
257
205
|
),
|
|
258
206
|
intelephense: spec("intelephense", "optional", () => "npm install -g intelephense", {
|
|
@@ -288,20 +236,11 @@ export async function resolveVersion(specification: DependencySpec, exec: ProbeE
|
|
|
288
236
|
return await (specification.version ?? versionProbe(specification.id, specification.versionArgs))(exec)
|
|
289
237
|
}
|
|
290
238
|
|
|
291
|
-
export async function resolveLocation(
|
|
292
|
-
specification: DependencySpec,
|
|
293
|
-
exec: ProbeExecutor,
|
|
294
|
-
platform: NodeJS.Platform = rawPlatform()
|
|
295
|
-
): Promise<DependencyLocation> {
|
|
296
|
-
if (specification.locate !== undefined) return await specification.locate(exec, platform)
|
|
297
|
-
const result = resolveDependency(specification, exec, platform)
|
|
298
|
-
return { path: result.state === "present" ? (result.path ?? exec.which(specification.id)) : "", binDir: "" }
|
|
299
|
-
}
|
|
300
|
-
|
|
301
239
|
export async function resolvePath(
|
|
302
240
|
specification: DependencySpec,
|
|
303
241
|
exec: ProbeExecutor,
|
|
304
|
-
platform
|
|
242
|
+
platform: NodeJS.Platform = rawPlatform()
|
|
305
243
|
): Promise<string> {
|
|
306
|
-
|
|
244
|
+
const result = resolveDependency(specification, exec, platform)
|
|
245
|
+
return result.state === "present" ? (result.path ?? exec.which(specification.id)) : ""
|
|
307
246
|
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { spawn, type ChildProcess, type SpawnOptions } from "node:child_process"
|
|
7
7
|
import { accessSync, constants, existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
8
|
-
import { delimiter, isAbsolute, join } from "node:path"
|
|
8
|
+
import { delimiter, extname, isAbsolute, join } from "node:path"
|
|
9
|
+
import { hostOs, type HostOs } from "./os"
|
|
9
10
|
|
|
10
11
|
/** Keep engine paths slash-separated so rendered output is host-stable. */
|
|
11
12
|
export function p(...parts: Array<string>): string {
|
|
@@ -21,6 +22,8 @@ export interface AsyncProcessResult {
|
|
|
21
22
|
|
|
22
23
|
export interface AsyncProcessOptions {
|
|
23
24
|
readonly stdio?: SpawnOptions["stdio"]
|
|
25
|
+
/** Host whose executable resolution and argv shaping apply; tests inject it. */
|
|
26
|
+
readonly host?: HostOs
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export function spawnProcess(
|
|
@@ -29,9 +32,25 @@ export function spawnProcess(
|
|
|
29
32
|
options: AsyncProcessOptions = {}
|
|
30
33
|
): Promise<AsyncProcessResult> {
|
|
31
34
|
const { promise, resolve } = Promise.withResolvers<AsyncProcessResult>()
|
|
35
|
+
const host = options.host ?? hostOs()
|
|
36
|
+
const resolvesSuffixes = host.executableSuffixes.some((suffix) => suffix !== "")
|
|
37
|
+
const executablePath = resolvesSuffixes ? which(cmd, host.executableSuffixes) : cmd
|
|
38
|
+
if (executablePath === "") {
|
|
39
|
+
// Never hand a pathless name to a host that resolves suffixes: CreateProcess
|
|
40
|
+
// searches the parent's current directory before the system one, so an
|
|
41
|
+
// untrusted checkout could answer for a missing tool.
|
|
42
|
+
resolve({ exitCode: null, stdout: "", stderr: "", error: new Error(`command not found on PATH: ${cmd}`) })
|
|
43
|
+
return promise
|
|
44
|
+
}
|
|
32
45
|
let child: ChildProcess
|
|
33
46
|
try {
|
|
34
|
-
|
|
47
|
+
// Inside the try: a host whose invocation encoding rejects a value it
|
|
48
|
+
// cannot represent reports it like any other spawn failure.
|
|
49
|
+
const invocation = host.invoke(executablePath, args)
|
|
50
|
+
child = spawn(invocation.command, [...invocation.args], {
|
|
51
|
+
stdio: options.stdio ?? ["ignore", "pipe", "ignore"],
|
|
52
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
53
|
+
})
|
|
35
54
|
} catch (cause) {
|
|
36
55
|
const error = cause instanceof Error ? cause : new Error(String(cause))
|
|
37
56
|
resolve({ exitCode: null, stdout: "", stderr: "", error })
|
|
@@ -75,14 +94,20 @@ export async function capture(cmd: string, args: ReadonlyArray<string>): Promise
|
|
|
75
94
|
}
|
|
76
95
|
|
|
77
96
|
/** `command -v` — resolve an executable name on PATH. */
|
|
78
|
-
export function which(name: string): string {
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
export function which(name: string, suffixes: ReadonlyArray<string> = hostOs().executableSuffixes): string {
|
|
98
|
+
const runnableCandidate = (base: string): string => {
|
|
99
|
+
for (const suffix of suffixes) {
|
|
100
|
+
const candidate = `${base}${suffix}`
|
|
101
|
+
if (isExecutable(candidate, suffixes)) return candidate
|
|
102
|
+
}
|
|
103
|
+
return ""
|
|
81
104
|
}
|
|
105
|
+
|
|
106
|
+
if (isAbsolute(name) || name.includes("/")) return runnableCandidate(name)
|
|
82
107
|
for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
83
108
|
if (dir === "") continue
|
|
84
|
-
const candidate = join(dir, name)
|
|
85
|
-
if (
|
|
109
|
+
const candidate = runnableCandidate(join(dir, name))
|
|
110
|
+
if (candidate !== "") return candidate
|
|
86
111
|
}
|
|
87
112
|
return ""
|
|
88
113
|
}
|
|
@@ -91,10 +116,16 @@ export function commandExists(name: string): boolean {
|
|
|
91
116
|
return which(name) !== ""
|
|
92
117
|
}
|
|
93
118
|
|
|
94
|
-
export function isExecutable(
|
|
119
|
+
export function isExecutable(path: string, suffixes: ReadonlyArray<string> = hostOs().executableSuffixes): boolean {
|
|
95
120
|
try {
|
|
96
|
-
if (!statSync(
|
|
97
|
-
|
|
121
|
+
if (!statSync(path).isFile()) return false
|
|
122
|
+
if (suffixes.some((suffix) => suffix !== "")) {
|
|
123
|
+
const lowerPath = path.toLowerCase()
|
|
124
|
+
return suffixes.some((suffix) =>
|
|
125
|
+
suffix === "" ? extname(path) === "" : lowerPath.endsWith(suffix.toLowerCase())
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
accessSync(path, constants.X_OK)
|
|
98
129
|
return true
|
|
99
130
|
} catch {
|
|
100
131
|
return false
|
|
@@ -87,7 +87,6 @@ export interface Ctx {
|
|
|
87
87
|
skipPluginRefresh?: boolean
|
|
88
88
|
reconcile: boolean
|
|
89
89
|
prune: boolean
|
|
90
|
-
assumeYes: boolean
|
|
91
90
|
claudeCompactWindow: string
|
|
92
91
|
claudePermissive: boolean
|
|
93
92
|
claudePlugins: Array<string>
|
|
@@ -140,7 +139,6 @@ function makeCtx(services: EngineServices): Ctx {
|
|
|
140
139
|
skipPluginRefresh: false,
|
|
141
140
|
reconcile: env["RECONCILE"] === "1",
|
|
142
141
|
prune: env["PRUNE"] === "1",
|
|
143
|
-
assumeYes: env["ASSUME_YES"] === "1",
|
|
144
142
|
claudeCompactWindow,
|
|
145
143
|
claudePermissive: env["CLAUDE_PERMISSIVE"] === "1",
|
|
146
144
|
claudePlugins,
|
|
@@ -12,8 +12,7 @@ import type { Ctx } from "./index"
|
|
|
12
12
|
import { isObject, parseJson, type Json } from "./jq"
|
|
13
13
|
import { printModels, validateClaudeModel, validateCodexModel } from "./models"
|
|
14
14
|
import { bunBootstrap } from "./bun"
|
|
15
|
-
import {
|
|
16
|
-
import { ensure, report } from "./toolchain"
|
|
15
|
+
import { installedVersion, present, report } from "./toolchain"
|
|
17
16
|
|
|
18
17
|
export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
19
18
|
const { echo, err, warn } = ctx.services.logger
|
|
@@ -119,15 +118,12 @@ function tomlModelText(text: string): string {
|
|
|
119
118
|
}
|
|
120
119
|
|
|
121
120
|
export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
|
|
122
|
-
const { err } = ctx.services.logger
|
|
121
|
+
const { echo, err, verbose } = ctx.services.logger
|
|
123
122
|
const words = args.filter((arg) => !arg.startsWith("--"))
|
|
124
123
|
const op = words[0] ?? "check"
|
|
125
124
|
const tool = words[1] ?? ""
|
|
126
125
|
for (const arg of args) {
|
|
127
|
-
if (arg === "--
|
|
128
|
-
else if (arg === "--verbose") {
|
|
129
|
-
ctx.verbose = true
|
|
130
|
-
}
|
|
126
|
+
if (arg === "--verbose") ctx.verbose = true
|
|
131
127
|
}
|
|
132
128
|
|
|
133
129
|
if (op === "check") {
|
|
@@ -135,20 +131,33 @@ export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Prom
|
|
|
135
131
|
return 0
|
|
136
132
|
}
|
|
137
133
|
if (op !== "ensure") {
|
|
138
|
-
err("Usage: toolchain [check|ensure <tool>]
|
|
134
|
+
err("Usage: toolchain [check|ensure <tool>]")
|
|
139
135
|
return 2
|
|
140
136
|
}
|
|
141
137
|
if (tool === "") {
|
|
142
|
-
err("Usage: toolchain ensure <tool>
|
|
138
|
+
err("Usage: toolchain ensure <tool>")
|
|
143
139
|
return 2
|
|
144
140
|
}
|
|
145
141
|
switch (tool) {
|
|
146
|
-
case "bun":
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
142
|
+
case "bun": {
|
|
143
|
+
// `bun` is the one managed tool, and its bootstrap is silent when Bun is
|
|
144
|
+
// already installed. Probe first so the no-op confirmation the `--verbose`
|
|
145
|
+
// contract promises is not mistaken for a fresh install.
|
|
146
|
+
const alreadyInstalled = present(ctx, "bun")
|
|
147
|
+
if ((await bunBootstrap(ctx, ctx.services)).kind !== "ready") return 1
|
|
148
|
+
if (alreadyInstalled) {
|
|
149
|
+
// A present tool whose --version cannot be read reports `unknown` in the
|
|
150
|
+
// doctor table; keep the same vocabulary rather than an empty pair of
|
|
151
|
+
// parentheses.
|
|
152
|
+
const probed = await installedVersion(ctx, "bun")
|
|
153
|
+
const installed = probed === "" ? "version unknown" : probed
|
|
154
|
+
if (ctx.dryRun) echo(`[dry-run] bun up to date (${installed})`)
|
|
155
|
+
else verbose(`bun up to date (${installed})`)
|
|
156
|
+
}
|
|
157
|
+
return 0
|
|
158
|
+
}
|
|
150
159
|
default:
|
|
151
|
-
err("toolchain ensure supports managed tools only (bun
|
|
160
|
+
err("toolchain ensure supports managed tools only (bun)")
|
|
152
161
|
return 2
|
|
153
162
|
}
|
|
154
163
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { p } from "../exec"
|
|
2
|
+
import type { HostOs } from "./types"
|
|
3
|
+
|
|
4
|
+
function posixLiteral(value: string): string {
|
|
5
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const darwin: HostOs = {
|
|
9
|
+
id: "darwin",
|
|
10
|
+
toolchainOs: "darwin",
|
|
11
|
+
supportsBubblewrap: false,
|
|
12
|
+
directoryLinkKinds: ["symlink"],
|
|
13
|
+
executableSuffixes: [""],
|
|
14
|
+
invoke: (executablePath, args) => ({ command: executablePath, args }),
|
|
15
|
+
bunExecutableName: "bun",
|
|
16
|
+
bunInstaller: (pin, directory) => {
|
|
17
|
+
const scriptPath = p(directory, "install.sh")
|
|
18
|
+
return {
|
|
19
|
+
scriptPath,
|
|
20
|
+
download: {
|
|
21
|
+
command: "curl",
|
|
22
|
+
args: ["-fsSL", "https://bun.sh/install", "-o", scriptPath]
|
|
23
|
+
},
|
|
24
|
+
run: {
|
|
25
|
+
command: "bash",
|
|
26
|
+
args: [scriptPath, `bun-v${pin}`]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
environmentSetting: (name, value) => ({
|
|
31
|
+
kind: "profile",
|
|
32
|
+
candidates: [".zshrc", ".bashrc", ".bash_profile", ".profile", ".zshenv"],
|
|
33
|
+
target: (shell) => {
|
|
34
|
+
const shellPath = shell ?? "bash"
|
|
35
|
+
const shellName = shellPath.slice(shellPath.lastIndexOf("/") + 1)
|
|
36
|
+
return shellName === "zsh" ? ".zshrc" : shellName === "bash" ? ".bashrc" : ".profile"
|
|
37
|
+
},
|
|
38
|
+
line: `export ${name}=${value}`
|
|
39
|
+
}),
|
|
40
|
+
statusLineCommand: (bun, script) => {
|
|
41
|
+
const bunLiteral = posixLiteral(bun)
|
|
42
|
+
const scriptLiteral = posixLiteral(script)
|
|
43
|
+
return `test -x ${bunLiteral} && test -f ${scriptLiteral} && exec ${bunLiteral} ${scriptLiteral} || true`
|
|
44
|
+
},
|
|
45
|
+
failureHookCommand: (command) => command,
|
|
46
|
+
installHint: (tool) => {
|
|
47
|
+
switch (tool) {
|
|
48
|
+
case "git":
|
|
49
|
+
return "brew install git"
|
|
50
|
+
case "jq":
|
|
51
|
+
return "brew install jq"
|
|
52
|
+
case "curl":
|
|
53
|
+
return "brew install curl"
|
|
54
|
+
case "ffplay":
|
|
55
|
+
return "brew install ffmpeg"
|
|
56
|
+
case "claude":
|
|
57
|
+
return "curl -fsSL https://claude.ai/install.sh -o /tmp/claude-install.sh && bash /tmp/claude-install.sh"
|
|
58
|
+
case "codex":
|
|
59
|
+
return 'tmp=$(mktemp) && curl -fsSL https://chatgpt.com/codex/install.sh -o "$tmp" && CODEX_NON_INTERACTIVE=1 sh "$tmp"'
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { darwin } from "./darwin"
|
|
2
|
+
import { linux } from "./linux"
|
|
3
|
+
import type { HostOs, PlatformName } from "./types"
|
|
4
|
+
import { windows } from "./windows"
|
|
5
|
+
|
|
6
|
+
export * from "./types"
|
|
7
|
+
|
|
8
|
+
export function rawPlatform(): NodeJS.Platform {
|
|
9
|
+
return process.platform
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function platformName(pf: NodeJS.Platform = rawPlatform()): PlatformName {
|
|
13
|
+
switch (pf) {
|
|
14
|
+
case "linux":
|
|
15
|
+
return "linux"
|
|
16
|
+
case "darwin":
|
|
17
|
+
return "darwin"
|
|
18
|
+
case "win32":
|
|
19
|
+
return "windows"
|
|
20
|
+
default:
|
|
21
|
+
return "unknown"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// An unrecognized host keeps Linux hint text, filters no toolchain row, and never claims bubblewrap — exactly today's behavior.
|
|
26
|
+
const unknown: HostOs = {
|
|
27
|
+
...linux,
|
|
28
|
+
id: "unknown",
|
|
29
|
+
toolchainOs: "",
|
|
30
|
+
supportsBubblewrap: false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const HOSTS: Readonly<Record<PlatformName, HostOs>> = {
|
|
34
|
+
linux,
|
|
35
|
+
darwin,
|
|
36
|
+
windows,
|
|
37
|
+
unknown
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function hostOs(id: PlatformName = platformName()): HostOs {
|
|
41
|
+
return HOSTS[id]
|
|
42
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { p } from "../exec"
|
|
2
|
+
import type { HostOs } from "./types"
|
|
3
|
+
|
|
4
|
+
function posixLiteral(value: string): string {
|
|
5
|
+
return `'${value.replaceAll("'", `'"'"'`)}'`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const linux: HostOs = {
|
|
9
|
+
id: "linux",
|
|
10
|
+
toolchainOs: "linux",
|
|
11
|
+
supportsBubblewrap: true,
|
|
12
|
+
directoryLinkKinds: ["symlink"],
|
|
13
|
+
executableSuffixes: [""],
|
|
14
|
+
invoke: (executablePath, args) => ({ command: executablePath, args }),
|
|
15
|
+
bunExecutableName: "bun",
|
|
16
|
+
bunInstaller: (pin, directory) => {
|
|
17
|
+
const scriptPath = p(directory, "install.sh")
|
|
18
|
+
return {
|
|
19
|
+
scriptPath,
|
|
20
|
+
download: {
|
|
21
|
+
command: "curl",
|
|
22
|
+
args: ["-fsSL", "https://bun.sh/install", "-o", scriptPath]
|
|
23
|
+
},
|
|
24
|
+
run: {
|
|
25
|
+
command: "bash",
|
|
26
|
+
args: [scriptPath, `bun-v${pin}`]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
environmentSetting: (name, value) => ({
|
|
31
|
+
kind: "profile",
|
|
32
|
+
candidates: [".zshrc", ".bashrc", ".bash_profile", ".profile", ".zshenv"],
|
|
33
|
+
target: (shell) => {
|
|
34
|
+
const shellPath = shell ?? "bash"
|
|
35
|
+
const shellName = shellPath.slice(shellPath.lastIndexOf("/") + 1)
|
|
36
|
+
return shellName === "zsh" ? ".zshrc" : shellName === "bash" ? ".bashrc" : ".profile"
|
|
37
|
+
},
|
|
38
|
+
line: `export ${name}=${value}`
|
|
39
|
+
}),
|
|
40
|
+
statusLineCommand: (bun, script) => {
|
|
41
|
+
const bunLiteral = posixLiteral(bun)
|
|
42
|
+
const scriptLiteral = posixLiteral(script)
|
|
43
|
+
return `test -x ${bunLiteral} && test -f ${scriptLiteral} && exec ${bunLiteral} ${scriptLiteral} || true`
|
|
44
|
+
},
|
|
45
|
+
failureHookCommand: (command) => command,
|
|
46
|
+
installHint: (tool) => {
|
|
47
|
+
switch (tool) {
|
|
48
|
+
case "git":
|
|
49
|
+
return "sudo apt install -y git (or your distro's package manager)"
|
|
50
|
+
case "jq":
|
|
51
|
+
return "sudo apt install -y jq"
|
|
52
|
+
case "curl":
|
|
53
|
+
return "sudo apt install -y curl"
|
|
54
|
+
case "ffplay":
|
|
55
|
+
return "sudo apt install -y ffmpeg"
|
|
56
|
+
case "claude":
|
|
57
|
+
return "curl -fsSL https://claude.ai/install.sh -o /tmp/claude-install.sh && bash /tmp/claude-install.sh"
|
|
58
|
+
case "codex":
|
|
59
|
+
return 'tmp=$(mktemp) && curl -fsSL https://chatgpt.com/codex/install.sh -o "$tmp" && CODEX_NON_INTERACTIVE=1 sh "$tmp"'
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-to-artifact map (Platform seam in DESIGN.md). Every launcher, installer,
|
|
3
|
+
* build script, and release workflow selects a compiled binary from this one
|
|
4
|
+
* table; cli/test/unit/hostTargets.test.ts parses those scripts and fails when
|
|
5
|
+
* any of them drifts from it.
|
|
6
|
+
*
|
|
7
|
+
* Membership here is the support matrix: adding a row is how a host becomes
|
|
8
|
+
* supported, and `requireSupportedHost` in cli/src/engine.ts admits exactly the
|
|
9
|
+
* platform/arch pairs this table names.
|
|
10
|
+
*/
|
|
11
|
+
import { platformName, type PlatformName } from "./index"
|
|
12
|
+
|
|
13
|
+
export type TargetId =
|
|
14
|
+
| "linux-x64"
|
|
15
|
+
| "linux-arm64"
|
|
16
|
+
| "darwin-x64"
|
|
17
|
+
| "darwin-arm64"
|
|
18
|
+
| "windows-x64"
|
|
19
|
+
| "windows-arm64"
|
|
20
|
+
|
|
21
|
+
export type TargetArch = "x64" | "arm64"
|
|
22
|
+
|
|
23
|
+
export interface HostTarget {
|
|
24
|
+
readonly id: TargetId
|
|
25
|
+
readonly platform: PlatformName
|
|
26
|
+
readonly arch: TargetArch
|
|
27
|
+
/** `bun build --compile --target=` value. */
|
|
28
|
+
readonly bunTarget: string
|
|
29
|
+
/** Compiled binary file name, including the Windows `.exe` suffix. */
|
|
30
|
+
readonly artifact: string
|
|
31
|
+
/** `uname -s`-`uname -m` keys the Bash launcher matches; empty on Windows. */
|
|
32
|
+
readonly unameKeys: ReadonlyArray<string>
|
|
33
|
+
/**
|
|
34
|
+
* `$env:PROCESSOR_ARCHITECTURE` values the PowerShell launcher matches; empty
|
|
35
|
+
* off Windows. Windows PowerShell 5.1 runs emulated on ARM64 and reports
|
|
36
|
+
* `AMD64` there, so a launcher must read `PROCESSOR_ARCHITEW6432` first.
|
|
37
|
+
*/
|
|
38
|
+
readonly processorArchitectures: ReadonlyArray<string>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const target = (
|
|
42
|
+
id: TargetId,
|
|
43
|
+
platform: PlatformName,
|
|
44
|
+
arch: TargetArch,
|
|
45
|
+
unameKeys: ReadonlyArray<string>,
|
|
46
|
+
processorArchitectures: ReadonlyArray<string>
|
|
47
|
+
): HostTarget => ({
|
|
48
|
+
id,
|
|
49
|
+
platform,
|
|
50
|
+
arch,
|
|
51
|
+
bunTarget: `bun-${id}`,
|
|
52
|
+
artifact: platform === "windows" ? `docks-kit-${id}.exe` : `docks-kit-${id}`,
|
|
53
|
+
unameKeys,
|
|
54
|
+
processorArchitectures
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
export const HOST_TARGETS: ReadonlyArray<HostTarget> = [
|
|
58
|
+
target("linux-x64", "linux", "x64", ["Linux-x86_64"], []),
|
|
59
|
+
target("linux-arm64", "linux", "arm64", ["Linux-aarch64"], []),
|
|
60
|
+
target("darwin-x64", "darwin", "x64", ["Darwin-x86_64"], []),
|
|
61
|
+
target("darwin-arm64", "darwin", "arm64", ["Darwin-arm64"], []),
|
|
62
|
+
target("windows-x64", "windows", "x64", [], ["AMD64"]),
|
|
63
|
+
target("windows-arm64", "windows", "arm64", [], ["ARM64"])
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
export function targetFor(platform: PlatformName, arch: string): HostTarget | undefined {
|
|
67
|
+
return HOST_TARGETS.find((t) => t.platform === platform && t.arch === arch)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Resolve the artifact for a raw Node platform/arch pair. */
|
|
71
|
+
export function targetForHost(platform: NodeJS.Platform, arch: string): HostTarget | undefined {
|
|
72
|
+
return targetFor(platformName(platform), arch)
|
|
73
|
+
}
|