docks-kit 0.14.1 → 0.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +9 -14
- package/README.md +11 -23
- package/cli/docs/flags.md +1 -2
- package/cli/docs/install.md +2 -2
- package/cli/docs/platforms.md +7 -12
- package/cli/docs/sync-layers.md +17 -25
- package/cli/docs/toolchain.md +12 -32
- package/cli/src/commands/status.ts +2 -9
- package/cli/src/commands/sync.ts +5 -5
- package/cli/src/commands/toolchain.ts +1 -1
- package/cli/src/commands/update.ts +28 -2
- package/cli/src/engine-native/DESIGN.md +24 -15
- package/cli/src/engine-native/claudeSync.ts +84 -116
- package/cli/src/engine-native/codexSync.ts +52 -11
- package/cli/src/engine-native/deps.ts +21 -51
- package/cli/src/engine-native/index.ts +33 -8
- package/cli/src/engine-native/logger.ts +73 -10
- package/cli/src/engine-native/modes.ts +2 -10
- package/cli/src/engine-native/parseArgs.ts +5 -5
- package/cli/src/engine-native/skillsSync.ts +18 -52
- package/cli/src/engine-native/toolchain.ts +7 -5
- package/cli/src/generated/sotPayload.ts +6 -6
- package/package.json +1 -1
- package/cli/src/engine-native/sessionRelayCli.ts +0 -262
- package/cli/src/engine-native/sessionRelayReadiness.ts +0 -94
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process"
|
|
2
|
-
import { createHash, randomBytes } from "node:crypto"
|
|
3
|
-
import {
|
|
4
|
-
chmodSync,
|
|
5
|
-
existsSync,
|
|
6
|
-
mkdirSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
renameSync,
|
|
9
|
-
rmSync,
|
|
10
|
-
statSync
|
|
11
|
-
} from "node:fs"
|
|
12
|
-
import { dirname, join } from "node:path"
|
|
13
|
-
|
|
14
|
-
import { isObject, parseJson, type Json } from "./jq"
|
|
15
|
-
import type { Ctx } from "./index"
|
|
16
|
-
import { ExitError } from "./parseArgs"
|
|
17
|
-
import { payloadText } from "../payload"
|
|
18
|
-
|
|
19
|
-
const REPOSITORY = "DocksDocks/docks"
|
|
20
|
-
const PLUGIN_ID = "session-relay@docks"
|
|
21
|
-
const INSTALL_PATH = "~/.local/bin/session-relay"
|
|
22
|
-
// Intel macOS was retired as of Session Relay 0.16.0: the parent release
|
|
23
|
-
// publishes exactly three native binaries, so darwin/x64 hosts fail closed in
|
|
24
|
-
// sessionRelayTarget before any manifest parse or download.
|
|
25
|
-
const TARGETS = [
|
|
26
|
-
"x86_64-unknown-linux-musl",
|
|
27
|
-
"aarch64-unknown-linux-musl",
|
|
28
|
-
"aarch64-apple-darwin"
|
|
29
|
-
] as const
|
|
30
|
-
|
|
31
|
-
export type SessionRelayTarget = typeof TARGETS[number]
|
|
32
|
-
|
|
33
|
-
export interface SessionRelayManifest {
|
|
34
|
-
readonly kind: "managed-release"
|
|
35
|
-
readonly policy: "exact"
|
|
36
|
-
readonly verified: string
|
|
37
|
-
readonly repository: typeof REPOSITORY
|
|
38
|
-
readonly tag: string
|
|
39
|
-
readonly plugin_id: typeof PLUGIN_ID
|
|
40
|
-
readonly plugin_version: string
|
|
41
|
-
readonly install_path: typeof INSTALL_PATH
|
|
42
|
-
readonly assets: Readonly<Record<SessionRelayTarget, string>>
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface SessionRelayInstallOps {
|
|
46
|
-
readonly download: (url: string, destination: string) => boolean
|
|
47
|
-
readonly chmod: (path: string, mode: number) => void
|
|
48
|
-
readonly runVersion: (path: string) => { readonly ok: boolean; readonly stdout: string }
|
|
49
|
-
readonly rename: (from: string, to: string) => void
|
|
50
|
-
readonly uniqueSuffix: () => string
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface SessionRelayInstallInput {
|
|
54
|
-
readonly home: string
|
|
55
|
-
readonly dryRun: boolean
|
|
56
|
-
readonly platform: string
|
|
57
|
-
readonly arch: string
|
|
58
|
-
readonly manifestText: string
|
|
59
|
-
readonly log: (line: string) => void
|
|
60
|
-
readonly error?: (line: string) => void
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function fail(input: SessionRelayInstallInput, message: string): never {
|
|
64
|
-
input.error?.(message)
|
|
65
|
-
const error = new ExitError(1)
|
|
66
|
-
error.message = message
|
|
67
|
-
throw error
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function exactKeys(value: { [key: string]: Json }, expected: ReadonlyArray<string>, label: string): void {
|
|
71
|
-
const actual = Object.keys(value).sort()
|
|
72
|
-
const wanted = [...expected].sort()
|
|
73
|
-
if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
|
|
74
|
-
throw new Error(`${label} violates the closed Session Relay manifest schema`)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function parseSessionRelayManifest(text: string): SessionRelayManifest {
|
|
79
|
-
const value = parseJson(text)
|
|
80
|
-
if (value === undefined || !isObject(value)) throw new Error("Session Relay manifest is not a JSON object")
|
|
81
|
-
exactKeys(
|
|
82
|
-
value,
|
|
83
|
-
["kind", "policy", "verified", "repository", "tag", "plugin_id", "plugin_version", "install_path", "assets"],
|
|
84
|
-
"Session Relay manifest"
|
|
85
|
-
)
|
|
86
|
-
const version = value["verified"]
|
|
87
|
-
if (typeof version !== "string" || !/^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$/.test(version)) {
|
|
88
|
-
throw new Error("Session Relay manifest verified must be a canonical stable SemVer core")
|
|
89
|
-
}
|
|
90
|
-
const expected = {
|
|
91
|
-
kind: "managed-release",
|
|
92
|
-
policy: "exact",
|
|
93
|
-
verified: version,
|
|
94
|
-
repository: REPOSITORY,
|
|
95
|
-
tag: `session-relay--v${version}`,
|
|
96
|
-
plugin_id: PLUGIN_ID,
|
|
97
|
-
plugin_version: version,
|
|
98
|
-
install_path: INSTALL_PATH
|
|
99
|
-
} as const
|
|
100
|
-
for (const [key, wanted] of Object.entries(expected)) {
|
|
101
|
-
if (value[key] !== wanted) throw new Error(`Session Relay manifest ${key.replaceAll("_", " ")} must be ${wanted}`)
|
|
102
|
-
}
|
|
103
|
-
const assets = value["assets"]
|
|
104
|
-
if (!isObject(assets)) throw new Error("Session Relay manifest assets must be an object")
|
|
105
|
-
exactKeys(assets, TARGETS, "Session Relay manifest assets target set")
|
|
106
|
-
const parsedAssets = {} as Record<SessionRelayTarget, string>
|
|
107
|
-
for (const target of TARGETS) {
|
|
108
|
-
const digest = assets[target]
|
|
109
|
-
if (typeof digest !== "string" || !/^[0-9a-f]{64}$/.test(digest)) {
|
|
110
|
-
throw new Error(`Session Relay manifest digest for ${target} must be 64 lowercase hex characters`)
|
|
111
|
-
}
|
|
112
|
-
parsedAssets[target] = digest
|
|
113
|
-
}
|
|
114
|
-
return { ...expected, assets: parsedAssets }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function manifestEntry(): string {
|
|
118
|
-
const document = parseJson(payloadText("SoT/toolchain.json"))
|
|
119
|
-
const tools = document !== undefined && isObject(document) ? document["tools"] : undefined
|
|
120
|
-
const entry = tools !== undefined && isObject(tools) ? tools["session-relay"] : undefined
|
|
121
|
-
if (entry === undefined) throw new Error("Embedded toolchain manifest has no session-relay entry")
|
|
122
|
-
return JSON.stringify(entry)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function sessionRelayTarget(platform: string, arch: string): SessionRelayTarget {
|
|
126
|
-
if (platform === "linux" && arch === "x64") return "x86_64-unknown-linux-musl"
|
|
127
|
-
if (platform === "linux" && arch === "arm64") return "aarch64-unknown-linux-musl"
|
|
128
|
-
if (platform === "darwin" && arch === "arm64") return "aarch64-apple-darwin"
|
|
129
|
-
throw new Error(`Unsupported host for Session Relay CLI: ${platform}/${arch}; supported: linux x64|arm64, darwin arm64`)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function trimOneLineEnding(text: string): string {
|
|
133
|
-
if (text.endsWith("\r\n")) return text.slice(0, -2)
|
|
134
|
-
if (text.endsWith("\n")) return text.slice(0, -1)
|
|
135
|
-
return text
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function exactVersion(ops: SessionRelayInstallOps, path: string, version: string): boolean {
|
|
139
|
-
const result = ops.runVersion(path)
|
|
140
|
-
return result.ok && trimOneLineEnding(result.stdout) === `session-relay ${version}`
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function selectedChecksum(text: string, assetName: string): string {
|
|
144
|
-
const selected = text.split("\n").filter((line) => line.endsWith(` ${assetName}`))
|
|
145
|
-
if (selected.length !== 1 || !new RegExp(`^[0-9a-f]{64} ${assetName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`).test(selected[0]!)) {
|
|
146
|
-
throw new Error(`SHA256SUMS must contain exactly one canonical row for ${assetName}`)
|
|
147
|
-
}
|
|
148
|
-
return selected[0]!.slice(0, 64)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const defaultOps: SessionRelayInstallOps = {
|
|
152
|
-
download: (url, destination) => {
|
|
153
|
-
const result = spawnSync("curl", ["-fL", "--retry", "2", "--connect-timeout", "10", "--output", destination, url], {
|
|
154
|
-
stdio: "inherit"
|
|
155
|
-
})
|
|
156
|
-
return result.error === undefined && result.status === 0
|
|
157
|
-
},
|
|
158
|
-
chmod: chmodSync,
|
|
159
|
-
runVersion: (path) => {
|
|
160
|
-
const result = spawnSync(path, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
|
|
161
|
-
return { ok: result.error === undefined && result.status === 0, stdout: result.stdout ?? "" }
|
|
162
|
-
},
|
|
163
|
-
rename: renameSync,
|
|
164
|
-
uniqueSuffix: () => `${process.pid}-${randomBytes(8).toString("hex")}`
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function installSessionRelayCli(
|
|
168
|
-
input: SessionRelayInstallInput,
|
|
169
|
-
ops: SessionRelayInstallOps = defaultOps
|
|
170
|
-
): void {
|
|
171
|
-
let manifest: SessionRelayManifest
|
|
172
|
-
let target: SessionRelayTarget
|
|
173
|
-
try {
|
|
174
|
-
manifest = parseSessionRelayManifest(input.manifestText)
|
|
175
|
-
target = sessionRelayTarget(input.platform, input.arch)
|
|
176
|
-
} catch (error) {
|
|
177
|
-
fail(input, error instanceof Error ? error.message : String(error))
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (input.dryRun) {
|
|
181
|
-
input.log(
|
|
182
|
-
`[dry-run] ensure Session Relay CLI ${manifest.verified} from ${manifest.repository}@${manifest.tag} (${target}) -> ${manifest.install_path}`
|
|
183
|
-
)
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const stable = join(input.home, ".local", "bin", "session-relay")
|
|
188
|
-
if (existsSync(stable) && exactVersion(ops, stable, manifest.verified)) return
|
|
189
|
-
|
|
190
|
-
const parent = dirname(stable)
|
|
191
|
-
try {
|
|
192
|
-
mkdirSync(parent, { recursive: true, mode: 0o755 })
|
|
193
|
-
if (!statSync(parent).isDirectory()) {
|
|
194
|
-
fail(input, `Session Relay install parent is not a directory: ${parent}`)
|
|
195
|
-
}
|
|
196
|
-
} catch (error) {
|
|
197
|
-
if (error instanceof ExitError) throw error
|
|
198
|
-
fail(input, `Cannot prepare Session Relay install directory ${parent}: ${error instanceof Error ? error.message : String(error)}`)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const suffix = ops.uniqueSuffix()
|
|
202
|
-
const stage = join(parent, `.session-relay.stage-${suffix}`)
|
|
203
|
-
const checksumFile = join(parent, `.session-relay.checksums-${suffix}`)
|
|
204
|
-
const assetName = `session-relay-${target}`
|
|
205
|
-
const baseUrl = `https://github.com/${manifest.repository}/releases/download/${manifest.tag}`
|
|
206
|
-
|
|
207
|
-
try {
|
|
208
|
-
if (!ops.download(`${baseUrl}/${assetName}`, stage)) fail(input, `Failed to download pinned Session Relay asset ${assetName}`)
|
|
209
|
-
if (!ops.download(`${baseUrl}/SHA256SUMS`, checksumFile)) fail(input, "Failed to download pinned Session Relay SHA256SUMS")
|
|
210
|
-
|
|
211
|
-
let checksumDigest: string
|
|
212
|
-
try {
|
|
213
|
-
checksumDigest = selectedChecksum(readFileSync(checksumFile, "utf8"), assetName)
|
|
214
|
-
} catch (error) {
|
|
215
|
-
fail(input, error instanceof Error ? error.message : String(error))
|
|
216
|
-
}
|
|
217
|
-
const sourceDigest = manifest.assets[target]
|
|
218
|
-
if (checksumDigest !== sourceDigest) fail(input, `Session Relay source pin does not match SHA256SUMS for ${assetName}`)
|
|
219
|
-
const downloadedDigest = createHash("sha256").update(readFileSync(stage)).digest("hex")
|
|
220
|
-
if (downloadedDigest !== sourceDigest) fail(input, `Downloaded Session Relay checksum mismatch for ${assetName}`)
|
|
221
|
-
|
|
222
|
-
try {
|
|
223
|
-
ops.chmod(stage, 0o755)
|
|
224
|
-
} catch (error) {
|
|
225
|
-
fail(input, `Failed to chmod staged Session Relay CLI: ${error instanceof Error ? error.message : String(error)}`)
|
|
226
|
-
}
|
|
227
|
-
if (!exactVersion(ops, stage, manifest.verified)) {
|
|
228
|
-
fail(input, `Staged Session Relay CLI did not report exact version session-relay ${manifest.verified}`)
|
|
229
|
-
}
|
|
230
|
-
try {
|
|
231
|
-
ops.rename(stage, stable)
|
|
232
|
-
} catch (error) {
|
|
233
|
-
fail(input, `Failed to atomically replace Session Relay CLI: ${error instanceof Error ? error.message : String(error)}`)
|
|
234
|
-
}
|
|
235
|
-
input.log(`Session Relay CLI ready (${manifest.verified})`)
|
|
236
|
-
} finally {
|
|
237
|
-
try {
|
|
238
|
-
rmSync(stage, { force: true })
|
|
239
|
-
} catch {
|
|
240
|
-
// A cleanup failure must not turn a successful atomic replacement into
|
|
241
|
-
// an install failure or alter a pre-existing stable executable.
|
|
242
|
-
}
|
|
243
|
-
try {
|
|
244
|
-
rmSync(checksumFile, { force: true })
|
|
245
|
-
} catch {
|
|
246
|
-
// Same failure-preservation rule as the staged executable above.
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export function ensureSessionRelayCli(ctx: Ctx): number {
|
|
252
|
-
installSessionRelayCli({
|
|
253
|
-
home: ctx.home,
|
|
254
|
-
dryRun: ctx.dryRun,
|
|
255
|
-
platform: ctx.services.platform.raw(),
|
|
256
|
-
arch: process.arch,
|
|
257
|
-
manifestText: manifestEntry(),
|
|
258
|
-
log: ctx.dryRun ? ctx.services.logger.echo : ctx.services.logger.change,
|
|
259
|
-
error: ctx.services.logger.err
|
|
260
|
-
})
|
|
261
|
-
return 0
|
|
262
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process"
|
|
2
|
-
|
|
3
|
-
import { isObject, parseJson } from "./jq"
|
|
4
|
-
|
|
5
|
-
const SESSION_RELAY_PLUGIN_ID = "session-relay@docks"
|
|
6
|
-
|
|
7
|
-
export type SessionRelayReadinessReason =
|
|
8
|
-
| "codex_cli_unavailable"
|
|
9
|
-
| "plugin_list_failed"
|
|
10
|
-
| "invalid_plugin_list"
|
|
11
|
-
| "plugin_missing"
|
|
12
|
-
| "plugin_ambiguous"
|
|
13
|
-
| "plugin_not_installed"
|
|
14
|
-
| "plugin_disabled"
|
|
15
|
-
|
|
16
|
-
export interface SessionRelayReadiness {
|
|
17
|
-
readonly schema: 1
|
|
18
|
-
readonly state: "ready" | "unavailable"
|
|
19
|
-
readonly reason: SessionRelayReadinessReason | null
|
|
20
|
-
readonly version: string | null
|
|
21
|
-
readonly installed: boolean
|
|
22
|
-
readonly enabled: boolean
|
|
23
|
-
readonly scope: "new_sessions"
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface CodexPluginListProbe {
|
|
27
|
-
readonly status: number | null
|
|
28
|
-
readonly stdout: string
|
|
29
|
-
readonly errorCode: string | null
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function unavailable(
|
|
33
|
-
reason: SessionRelayReadinessReason,
|
|
34
|
-
fields: { version?: string | null; installed?: boolean; enabled?: boolean } = {}
|
|
35
|
-
): SessionRelayReadiness {
|
|
36
|
-
return {
|
|
37
|
-
schema: 1,
|
|
38
|
-
state: "unavailable",
|
|
39
|
-
reason,
|
|
40
|
-
version: fields.version ?? null,
|
|
41
|
-
installed: fields.installed ?? false,
|
|
42
|
-
enabled: fields.enabled ?? false,
|
|
43
|
-
scope: "new_sessions"
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function classifySessionRelayReadiness(probe: CodexPluginListProbe): SessionRelayReadiness {
|
|
48
|
-
if (probe.errorCode === "ENOENT") return unavailable("codex_cli_unavailable")
|
|
49
|
-
if (probe.errorCode !== null || probe.status !== 0) return unavailable("plugin_list_failed")
|
|
50
|
-
|
|
51
|
-
const value = parseJson(probe.stdout)
|
|
52
|
-
if (value === undefined || !isObject(value) || !Array.isArray(value["installed"])) {
|
|
53
|
-
return unavailable("invalid_plugin_list")
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const matches = value["installed"].filter(
|
|
57
|
-
(entry) => isObject(entry) && entry["pluginId"] === SESSION_RELAY_PLUGIN_ID
|
|
58
|
-
)
|
|
59
|
-
if (matches.length === 0) return unavailable("plugin_missing")
|
|
60
|
-
if (matches.length !== 1) return unavailable("plugin_ambiguous")
|
|
61
|
-
|
|
62
|
-
const row = matches[0]
|
|
63
|
-
if (row === undefined || !isObject(row)) return unavailable("invalid_plugin_list")
|
|
64
|
-
const version = row["version"]
|
|
65
|
-
const installed = row["installed"]
|
|
66
|
-
const enabled = row["enabled"]
|
|
67
|
-
if (typeof version !== "string" || version.length === 0 || typeof installed !== "boolean" || typeof enabled !== "boolean") {
|
|
68
|
-
return unavailable("invalid_plugin_list")
|
|
69
|
-
}
|
|
70
|
-
if (!installed) return unavailable("plugin_not_installed", { version, enabled })
|
|
71
|
-
if (!enabled) return unavailable("plugin_disabled", { version, installed })
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
schema: 1,
|
|
75
|
-
state: "ready",
|
|
76
|
-
reason: null,
|
|
77
|
-
version,
|
|
78
|
-
installed,
|
|
79
|
-
enabled,
|
|
80
|
-
scope: "new_sessions"
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function sessionRelayReadiness(): SessionRelayReadiness {
|
|
85
|
-
const result = spawnSync("codex", ["plugin", "list", "--json"], {
|
|
86
|
-
encoding: "utf8",
|
|
87
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
88
|
-
})
|
|
89
|
-
return classifySessionRelayReadiness({
|
|
90
|
-
status: result.status,
|
|
91
|
-
stdout: result.stdout ?? "",
|
|
92
|
-
errorCode: (result.error as NodeJS.ErrnoException | undefined)?.code ?? null
|
|
93
|
-
})
|
|
94
|
-
}
|