docks-kit 0.15.1 → 0.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +57 -17
- package/README.md +33 -31
- package/cli/docs/flags.md +0 -1
- package/cli/docs/install.md +28 -13
- 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 +26 -34
- package/cli/src/commands/docs.ts +3 -3
- package/cli/src/commands/model.ts +3 -0
- package/cli/src/commands/models.ts +5 -3
- package/cli/src/commands/status.ts +145 -32
- package/cli/src/commands/sync.ts +4 -5
- package/cli/src/commands/toolchain.ts +4 -7
- package/cli/src/commands/update.ts +177 -32
- package/cli/src/efforts.ts +5 -5
- package/cli/src/engine-native/DESIGN.md +31 -22
- package/cli/src/engine-native/bun.ts +42 -14
- package/cli/src/engine-native/claudeRuntime.ts +17 -9
- package/cli/src/engine-native/claudeSettingsModifiers.ts +29 -11
- package/cli/src/engine-native/claudeSync.ts +91 -55
- package/cli/src/engine-native/codexSync.ts +173 -49
- package/cli/src/engine-native/codexToml.ts +12 -7
- package/cli/src/engine-native/deps.ts +36 -95
- package/cli/src/engine-native/exec.ts +42 -24
- package/cli/src/engine-native/index.ts +17 -6
- package/cli/src/engine-native/models.ts +2 -9
- package/cli/src/engine-native/modes.ts +54 -35
- 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 +147 -48
- package/cli/src/engine-native/services.ts +1 -11
- package/cli/src/engine-native/settings.ts +3 -2
- package/cli/src/engine-native/skillsSync.ts +141 -89
- package/cli/src/engine-native/toolchain.ts +5 -147
- package/cli/src/engine.ts +41 -11
- package/cli/src/generated/sotPayload.ts +7 -7
- package/cli/src/kitHome.ts +42 -5
- package/cli/src/main.ts +12 -2
- package/cli/src/manifests.ts +28 -11
- package/cli/src/payload.ts +2 -5
- package/docks-kit +4 -4
- package/docks-kit.ps1 +123 -0
- package/package.json +9 -5
- package/cli/src/engine-native/os.ts +0 -24
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* substitution: stdout with trailing newlines stripped, empty on failure.
|
|
5
5
|
*/
|
|
6
6
|
import { spawn, type ChildProcess, type SpawnOptions } from "node:child_process"
|
|
7
|
-
import { accessSync,
|
|
8
|
-
import { delimiter, isAbsolute, join } from "node:path"
|
|
7
|
+
import { accessSync, constants, existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
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,33 +116,26 @@ 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
|
|
101
132
|
}
|
|
102
133
|
}
|
|
103
134
|
|
|
104
|
-
export function fileExists(p: string): boolean {
|
|
105
|
-
return existsSync(p)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
135
|
// Change-detection primitives (Output Policy in DESIGN.md): operations report
|
|
109
136
|
// changed:boolean so unchanged repeat runs log at verbose instead of [ok].
|
|
110
137
|
|
|
111
138
|
/** Write only when the content differs; returns whether a write happened. */
|
|
112
|
-
/** Add missing +x bits; returns whether a repair actually happened. */
|
|
113
|
-
export function ensureExecutable(path: string): boolean {
|
|
114
|
-
const mode = statSync(path).mode
|
|
115
|
-
const want = mode | 0o111
|
|
116
|
-
if (mode === want) return false
|
|
117
|
-
chmodSync(path, want)
|
|
118
|
-
return true
|
|
119
|
-
}
|
|
120
|
-
|
|
121
139
|
export function writeTextIfChanged(path: string, content: string): boolean {
|
|
122
140
|
if (existsSync(path) && readFileSync(path, "utf8") === content) return false
|
|
123
141
|
writeFileSync(path, content)
|
|
@@ -17,12 +17,15 @@ import { claudeNextSteps, claudeSummary, claudeSync, type ClaudeRuntimeState } f
|
|
|
17
17
|
import { codexNextSteps, codexSummary, codexSync } from "./codexSync"
|
|
18
18
|
import { normalizeManifest, skillsNextSteps, skillsSummary, skillsSync, type SkillsState } from "./skillsSync"
|
|
19
19
|
import { modeModel, modeToolchain } from "./modes"
|
|
20
|
-
import { ExitError, parseArgs, validateModifierFlags } from "./parseArgs"
|
|
20
|
+
import { ExitError, parseArgs, parseClaudePlugin, parseCompactWindow, validateModifierFlags } from "./parseArgs"
|
|
21
21
|
|
|
22
22
|
export type ModifierFlag =
|
|
23
23
|
| "--claude-model"
|
|
24
24
|
| "--claude-effort"
|
|
25
25
|
| "--claude-advisor"
|
|
26
|
+
| "--claude-compact-window"
|
|
27
|
+
| "--claude-permissive"
|
|
28
|
+
| "--claude-plugin"
|
|
26
29
|
| "--codex-model"
|
|
27
30
|
| "--codex-effort"
|
|
28
31
|
|
|
@@ -84,7 +87,6 @@ export interface Ctx {
|
|
|
84
87
|
skipPluginRefresh?: boolean
|
|
85
88
|
reconcile: boolean
|
|
86
89
|
prune: boolean
|
|
87
|
-
assumeYes: boolean
|
|
88
90
|
claudeCompactWindow: string
|
|
89
91
|
claudePermissive: boolean
|
|
90
92
|
claudePlugins: Array<string>
|
|
@@ -117,6 +119,16 @@ export interface Ctx {
|
|
|
117
119
|
function makeCtx(services: EngineServices): Ctx {
|
|
118
120
|
const env = process.env
|
|
119
121
|
const home = env["HOME"] !== undefined && env["HOME"] !== "" ? env["HOME"] : homedir()
|
|
122
|
+
const compactWindowSource = env["CLAUDE_COMPACT_WINDOW"] ?? ""
|
|
123
|
+
const claudeCompactWindow = compactWindowSource === "" ? "" : parseCompactWindow(compactWindowSource)
|
|
124
|
+
if (claudeCompactWindow === undefined) {
|
|
125
|
+
services.logger.err("CLAUDE_COMPACT_WINDOW expects a token count (e.g. 680000 or 680k)")
|
|
126
|
+
throw new ExitError(2)
|
|
127
|
+
}
|
|
128
|
+
const claudePlugins = (env["CLAUDE_PLUGINS"] ?? "")
|
|
129
|
+
.split(" ")
|
|
130
|
+
.filter((plugin) => plugin !== "")
|
|
131
|
+
.map((plugin) => parseClaudePlugin(plugin, services.logger.err))
|
|
120
132
|
return {
|
|
121
133
|
repoDir: kitHome(),
|
|
122
134
|
home,
|
|
@@ -127,10 +139,9 @@ function makeCtx(services: EngineServices): Ctx {
|
|
|
127
139
|
skipPluginRefresh: false,
|
|
128
140
|
reconcile: env["RECONCILE"] === "1",
|
|
129
141
|
prune: env["PRUNE"] === "1",
|
|
130
|
-
|
|
131
|
-
claudeCompactWindow: env["CLAUDE_COMPACT_WINDOW"] ?? "",
|
|
142
|
+
claudeCompactWindow,
|
|
132
143
|
claudePermissive: env["CLAUDE_PERMISSIVE"] === "1",
|
|
133
|
-
claudePlugins
|
|
144
|
+
claudePlugins,
|
|
134
145
|
claudeModel: env["CLAUDE_MODEL"] ?? "",
|
|
135
146
|
claudeEffort: "",
|
|
136
147
|
claudeAdvisor: "",
|
|
@@ -278,8 +289,8 @@ export async function runEngineNative(argv: ReadonlyArray<string>, services?: En
|
|
|
278
289
|
deps: baseServices.deps,
|
|
279
290
|
platform: baseServices.platform
|
|
280
291
|
}
|
|
281
|
-
ctx = makeCtx(runServices)
|
|
282
292
|
try {
|
|
293
|
+
ctx = makeCtx(runServices)
|
|
283
294
|
switch (argv[0]) {
|
|
284
295
|
case "model":
|
|
285
296
|
return modeModel(ctx, argv.slice(1))
|
|
@@ -6,16 +6,9 @@ import type { Ctx } from "./index"
|
|
|
6
6
|
import { isObject, parseJson, type Json } from "./jq"
|
|
7
7
|
import { payloadDisplayPath, payloadText } from "../payload"
|
|
8
8
|
|
|
9
|
-
function catalog(): Json | undefined {
|
|
10
|
-
try {
|
|
11
|
-
return parseJson(payloadText("SoT/models.json"))
|
|
12
|
-
} catch {
|
|
13
|
-
return undefined
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
9
|
|
|
17
10
|
function toolEntry(tool: string): { [k: string]: Json } | undefined {
|
|
18
|
-
const doc =
|
|
11
|
+
const doc = parseJson(payloadText("SoT/models.json"))
|
|
19
12
|
if (doc === undefined || !isObject(doc)) return undefined
|
|
20
13
|
const entry = doc[tool]
|
|
21
14
|
return entry !== undefined && isObject(entry) ? entry : undefined
|
|
@@ -37,7 +30,7 @@ export function printModels(ctx: Ctx, tool: string): void {
|
|
|
37
30
|
const { echo, warn } = ctx.services.logger
|
|
38
31
|
const entry = toolEntry(tool)
|
|
39
32
|
if (entry === undefined) {
|
|
40
|
-
warn(`Model catalog unavailable (${payloadDisplayPath("SoT/models.json"
|
|
33
|
+
warn(`Model catalog unavailable (${payloadDisplayPath("SoT/models.json")})`)
|
|
41
34
|
return
|
|
42
35
|
}
|
|
43
36
|
const verified = typeof entry["verified"] === "string" ? entry["verified"] : "?"
|
|
@@ -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
|
|
@@ -37,19 +36,29 @@ export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
37
36
|
if (value === "") {
|
|
38
37
|
if (tool === "claude") {
|
|
39
38
|
const deployed = p(ctx.home, ".claude", "settings.json")
|
|
40
|
-
|
|
39
|
+
const result = readConfig(deployed)
|
|
40
|
+
if (result.kind === "missing") {
|
|
41
41
|
warn("~/.claude/settings.json missing")
|
|
42
42
|
return 0
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
if (result.kind === "read-error") {
|
|
45
|
+
err(`Failed to read ~/.claude/settings.json: ${String(result.error)}`)
|
|
46
|
+
return 1
|
|
47
|
+
}
|
|
48
|
+
echo(`deployed: ${jsonModelText(result.data)}`)
|
|
45
49
|
echo(`SoT: ${jsonModelText(payloadText("SoT/.claude/settings.json"))}`)
|
|
46
50
|
} else {
|
|
47
51
|
const deployed = p(ctx.home, ".codex", "config.toml")
|
|
48
|
-
|
|
52
|
+
const result = readConfig(deployed)
|
|
53
|
+
if (result.kind === "missing") {
|
|
49
54
|
warn("~/.codex/config.toml missing")
|
|
50
55
|
return 0
|
|
51
56
|
}
|
|
52
|
-
|
|
57
|
+
if (result.kind === "read-error") {
|
|
58
|
+
err(`Failed to read ~/.codex/config.toml: ${String(result.error)}`)
|
|
59
|
+
return 1
|
|
60
|
+
}
|
|
61
|
+
echo(`deployed: ${tomlModelText(result.data)}`)
|
|
53
62
|
echo(`SoT: ${tomlModelText(payloadText("SoT/.codex/config.toml"))}`)
|
|
54
63
|
}
|
|
55
64
|
printModels(ctx, tool)
|
|
@@ -74,20 +83,23 @@ export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
74
83
|
return 0
|
|
75
84
|
}
|
|
76
85
|
|
|
77
|
-
|
|
86
|
+
type ConfigReadResult =
|
|
87
|
+
| { readonly kind: "missing" }
|
|
88
|
+
| { readonly kind: "read-error"; readonly error: unknown }
|
|
89
|
+
| { readonly kind: "data"; readonly data: string }
|
|
90
|
+
|
|
91
|
+
function readConfig(file: string): ConfigReadResult {
|
|
78
92
|
try {
|
|
79
|
-
readFileSync(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
return { kind: "data", data: readFileSync(file, "utf8") }
|
|
94
|
+
} catch (error) {
|
|
95
|
+
const code =
|
|
96
|
+
typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
|
|
97
|
+
? error.code
|
|
98
|
+
: undefined
|
|
99
|
+
return code === "ENOENT" ? { kind: "missing" } : { kind: "read-error", error }
|
|
83
100
|
}
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
/** `jq -r '.model // "default (unset)"'` — empty on unparseable input. */
|
|
87
|
-
function jsonModelField(file: string): string {
|
|
88
|
-
return jsonModelText(readFileSync(file, "utf8"))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
103
|
function jsonModelText(text: string): string {
|
|
92
104
|
const doc = parseJson(text)
|
|
93
105
|
if (doc === undefined) return ""
|
|
@@ -97,9 +109,6 @@ function jsonModelText(text: string): string {
|
|
|
97
109
|
}
|
|
98
110
|
|
|
99
111
|
/** `awk -F'"' '/^model[[:space:]]*=/{print $2; exit}'`. */
|
|
100
|
-
function tomlModelField(file: string): string {
|
|
101
|
-
return tomlModelText(readFileSync(file, "utf8"))
|
|
102
|
-
}
|
|
103
112
|
|
|
104
113
|
function tomlModelText(text: string): string {
|
|
105
114
|
for (const line of text.split("\n")) {
|
|
@@ -109,15 +118,12 @@ function tomlModelText(text: string): string {
|
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
|
|
112
|
-
const { err } = ctx.services.logger
|
|
113
|
-
const words = args.filter((
|
|
114
|
-
const op = words[0] ??
|
|
115
|
-
const tool = words[1] ??
|
|
121
|
+
const { echo, err, verbose } = ctx.services.logger
|
|
122
|
+
const words = args.filter((arg) => !arg.startsWith("--"))
|
|
123
|
+
const op = words[0] ?? "check"
|
|
124
|
+
const tool = words[1] ?? ""
|
|
116
125
|
for (const arg of args) {
|
|
117
|
-
if (arg === "--
|
|
118
|
-
else if (arg === "--verbose") {
|
|
119
|
-
ctx.verbose = true
|
|
120
|
-
}
|
|
126
|
+
if (arg === "--verbose") ctx.verbose = true
|
|
121
127
|
}
|
|
122
128
|
|
|
123
129
|
if (op === "check") {
|
|
@@ -125,20 +131,33 @@ export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Prom
|
|
|
125
131
|
return 0
|
|
126
132
|
}
|
|
127
133
|
if (op !== "ensure") {
|
|
128
|
-
err("Usage: toolchain [check|ensure <tool>]
|
|
134
|
+
err("Usage: toolchain [check|ensure <tool>]")
|
|
129
135
|
return 2
|
|
130
136
|
}
|
|
131
|
-
if (tool === ""
|
|
132
|
-
err("Usage: toolchain ensure <tool>
|
|
137
|
+
if (tool === "") {
|
|
138
|
+
err("Usage: toolchain ensure <tool>")
|
|
133
139
|
return 2
|
|
134
140
|
}
|
|
135
141
|
switch (tool) {
|
|
136
|
-
case "bun":
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
}
|
|
140
159
|
default:
|
|
141
|
-
err("toolchain ensure supports managed tools only (bun
|
|
160
|
+
err("toolchain ensure supports managed tools only (bun)")
|
|
142
161
|
return 2
|
|
143
162
|
}
|
|
144
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
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host modules own only facts that cannot be probed. Anything that can fail at
|
|
3
|
+
* runtime stays try-then-fallback at the call site; a host module contributes
|
|
4
|
+
* only the order of that fallback chain.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type PlatformName = "linux" | "darwin" | "windows" | "unknown"
|
|
8
|
+
/** Directory-link mechanisms, tried in order before the copy fallback. */
|
|
9
|
+
export type DirectoryLinkKind = "symlink" | "junction"
|
|
10
|
+
/** Tools whose install command differs per host OS. */
|
|
11
|
+
export type HintedTool = "git" | "jq" | "curl" | "ffplay" | "claude" | "codex"
|
|
12
|
+
|
|
13
|
+
/** A command plus argv, already shaped for how this host must invoke it. */
|
|
14
|
+
export interface Invocation {
|
|
15
|
+
readonly command: string
|
|
16
|
+
readonly args: ReadonlyArray<string>
|
|
17
|
+
/** Preserve a fully quoted Windows command line instead of applying libuv quoting. */
|
|
18
|
+
readonly windowsVerbatimArguments?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** How this host persists a user-level environment variable. */
|
|
22
|
+
export type EnvironmentSetting =
|
|
23
|
+
| {
|
|
24
|
+
readonly kind: "profile"
|
|
25
|
+
/** Home-relative files scanned for an existing setting. */
|
|
26
|
+
readonly candidates: ReadonlyArray<string>
|
|
27
|
+
/** Home-relative file that receives the write for this shell. */
|
|
28
|
+
readonly target: (shell: string | undefined) => string
|
|
29
|
+
/** Line appended to the target file. */
|
|
30
|
+
readonly line: string
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
readonly kind: "command"
|
|
34
|
+
/** Exits zero when the variable is already persisted. */
|
|
35
|
+
readonly probe: Invocation
|
|
36
|
+
/** Persists the variable. */
|
|
37
|
+
readonly apply: Invocation
|
|
38
|
+
/** Where the value lands, named for the log line. */
|
|
39
|
+
readonly location: string
|
|
40
|
+
/** Manual recovery when `apply` fails. */
|
|
41
|
+
readonly manualHint: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The two steps that fetch and run a pinned Bun installer. */
|
|
45
|
+
export interface BunInstaller {
|
|
46
|
+
readonly scriptPath: string
|
|
47
|
+
readonly download: Invocation
|
|
48
|
+
readonly run: Invocation
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface HostOs {
|
|
52
|
+
readonly id: PlatformName
|
|
53
|
+
/** `SoT/toolchain.json` `os` field this host matches; "" matches every row. */
|
|
54
|
+
readonly toolchainOs: string
|
|
55
|
+
/** Bubblewrap is the Linux-only Codex sandbox runtime. */
|
|
56
|
+
readonly supportsBubblewrap: boolean
|
|
57
|
+
/** Directory-link mechanisms tried in order before `skillsSync.ts linkOrCopy` copies. */
|
|
58
|
+
readonly directoryLinkKinds: ReadonlyArray<DirectoryLinkKind>
|
|
59
|
+
/** One-line install command for a tool whose package differs per OS. */
|
|
60
|
+
readonly installHint: (tool: HintedTool) => string
|
|
61
|
+
/** Filename suffixes tried in order when resolving a bare tool name on PATH. */
|
|
62
|
+
readonly executableSuffixes: ReadonlyArray<string>
|
|
63
|
+
/** Shape the argv that actually runs a resolved executable on this host. */
|
|
64
|
+
readonly invoke: (executablePath: string, args: ReadonlyArray<string>) => Invocation
|
|
65
|
+
/** Bun executable filename inside `<bunRoot>/bin`. */
|
|
66
|
+
readonly bunExecutableName: string
|
|
67
|
+
/** Download-then-run installer for a pinned Bun version placed in `directory`. */
|
|
68
|
+
readonly bunInstaller: (pin: string, directory: string) => BunInstaller
|
|
69
|
+
/** How this host persists a user-level environment variable. */
|
|
70
|
+
readonly environmentSetting: (name: string, value: string) => EnvironmentSetting
|
|
71
|
+
/** Claude `statusLine.command` for a resolved bun executable and script. */
|
|
72
|
+
readonly statusLineCommand: (bun: string, script: string) => string
|
|
73
|
+
/** Reshape the SoT `PostToolUseFailure` command for this host's shell. */
|
|
74
|
+
readonly failureHookCommand: (command: string) => string
|
|
75
|
+
}
|