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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command, Flag } from "effect/unstable/cli"
|
|
2
2
|
import { Console, Effect } from "effect"
|
|
3
|
-
import { engineCapture } from "../engine"
|
|
3
|
+
import { engineCapture, type EngineCaptureError } from "../engine"
|
|
4
4
|
import {
|
|
5
5
|
deployedClaudeSettings,
|
|
6
6
|
deployedCodexModel,
|
|
@@ -22,55 +22,168 @@ interface Drift {
|
|
|
22
22
|
readonly drifted: boolean
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
type ClaudeDeployment =
|
|
26
|
+
| { readonly state: "absent" }
|
|
27
|
+
| { readonly state: "valid"; readonly settings: Record<string, unknown> }
|
|
28
|
+
| { readonly state: "malformed"; readonly diagnostic: string }
|
|
29
|
+
|
|
30
|
+
const captureToolchainStatus = () =>
|
|
31
|
+
engineCapture(["toolchain", "check"]).pipe(
|
|
32
|
+
Effect.map((table) => ({ state: "valid" as const, table })),
|
|
33
|
+
Effect.catch((error: EngineCaptureError) =>
|
|
34
|
+
Effect.succeed({
|
|
35
|
+
state: "failed" as const,
|
|
36
|
+
table: "",
|
|
37
|
+
diagnostic: error.diagnostic,
|
|
38
|
+
exitCode: error.code
|
|
39
|
+
})
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
const readClaudeDeployment = (): ClaudeDeployment => {
|
|
44
|
+
try {
|
|
45
|
+
const settings: unknown = deployedClaudeSettings()
|
|
46
|
+
if (settings === undefined) return { state: "absent" }
|
|
47
|
+
if (settings === null || typeof settings !== "object" || Array.isArray(settings)) {
|
|
48
|
+
return {
|
|
49
|
+
state: "malformed",
|
|
50
|
+
diagnostic: "deployed Claude settings must contain a JSON object"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { state: "valid", settings: settings as Record<string, unknown> }
|
|
54
|
+
} catch (error) {
|
|
55
|
+
const detail = error instanceof Error ? error.message : String(error)
|
|
56
|
+
return {
|
|
57
|
+
state: "malformed",
|
|
58
|
+
diagnostic: `deployed Claude settings contain invalid JSON: ${detail}`
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const gatherDrift = (): {
|
|
64
|
+
readonly drift: Array<Drift>
|
|
65
|
+
readonly claudeDeployment: ClaudeDeployment
|
|
66
|
+
} => {
|
|
26
67
|
const sot = sotClaudeSettings()
|
|
27
|
-
const
|
|
68
|
+
const claudeDeployment = readClaudeDeployment()
|
|
28
69
|
const row = (setting: string, deployed: unknown, sotVal: unknown): Drift => {
|
|
29
70
|
const d = String(deployed ?? "(unset)")
|
|
30
71
|
const s = String(sotVal ?? "(unset)")
|
|
31
72
|
return { setting, deployed: d, sot: s, drifted: d !== s }
|
|
32
73
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
74
|
+
const codex = row("codex.model", deployedCodexModel(), sotCodexModel())
|
|
75
|
+
if (claudeDeployment.state === "absent") {
|
|
76
|
+
return {
|
|
77
|
+
claudeDeployment,
|
|
78
|
+
drift: [
|
|
79
|
+
{ setting: "claude.settings", deployed: "(absent)", sot: "present", drifted: true },
|
|
80
|
+
codex
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (claudeDeployment.state === "malformed") {
|
|
85
|
+
return {
|
|
86
|
+
claudeDeployment,
|
|
87
|
+
drift: [
|
|
88
|
+
{ setting: "claude.settings", deployed: "(malformed)", sot: "present", drifted: true },
|
|
89
|
+
codex
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const dep = claudeDeployment.settings
|
|
95
|
+
const env =
|
|
96
|
+
dep.env !== null && typeof dep.env === "object" && !Array.isArray(dep.env)
|
|
97
|
+
? (dep.env as Record<string, unknown>)
|
|
98
|
+
: {}
|
|
99
|
+
return {
|
|
100
|
+
claudeDeployment,
|
|
101
|
+
drift: [
|
|
102
|
+
row("claude.model", dep.model, sot.model),
|
|
103
|
+
row("claude.effortLevel", dep.effortLevel, sot.effortLevel),
|
|
104
|
+
row(
|
|
105
|
+
"claude.compactWindow",
|
|
106
|
+
env.CLAUDE_CODE_AUTO_COMPACT_WINDOW,
|
|
107
|
+
sot.env?.CLAUDE_CODE_AUTO_COMPACT_WINDOW
|
|
108
|
+
),
|
|
109
|
+
codex
|
|
110
|
+
]
|
|
111
|
+
}
|
|
43
112
|
}
|
|
44
113
|
|
|
45
114
|
export const statusCommand = Command.make("status", { json }, (config) =>
|
|
46
115
|
Effect.gen(function* () {
|
|
47
|
-
const drift = gatherDrift()
|
|
116
|
+
const { drift, claudeDeployment } = gatherDrift()
|
|
48
117
|
const plugins = pluginsView()
|
|
49
118
|
const skills = skillsView()
|
|
50
|
-
const
|
|
119
|
+
const toolchain = yield* captureToolchainStatus()
|
|
120
|
+
const diagnostics = new Array<{ source: string; message: string; exitCode: number }>()
|
|
121
|
+
if (claudeDeployment.state === "malformed") {
|
|
122
|
+
diagnostics.push({
|
|
123
|
+
source: "claude.settings",
|
|
124
|
+
message: claudeDeployment.diagnostic,
|
|
125
|
+
exitCode: 1
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
if (toolchain.state === "failed") {
|
|
129
|
+
diagnostics.push({
|
|
130
|
+
source: "toolchain",
|
|
131
|
+
message: toolchain.diagnostic ?? "toolchain capture failed",
|
|
132
|
+
exitCode: toolchain.exitCode ?? 1
|
|
133
|
+
})
|
|
134
|
+
}
|
|
51
135
|
|
|
52
136
|
if (config.json) {
|
|
53
|
-
|
|
54
|
-
|
|
137
|
+
const deployment =
|
|
138
|
+
claudeDeployment.state === "malformed"
|
|
139
|
+
? { claude: { state: claudeDeployment.state, diagnostic: claudeDeployment.diagnostic } }
|
|
140
|
+
: { claude: { state: claudeDeployment.state } }
|
|
141
|
+
yield* Console.log(
|
|
142
|
+
JSON.stringify(
|
|
143
|
+
{
|
|
144
|
+
kitHome: kitHome(),
|
|
145
|
+
deployment,
|
|
146
|
+
drift,
|
|
147
|
+
plugins,
|
|
148
|
+
skills,
|
|
149
|
+
toolchain,
|
|
150
|
+
diagnostics
|
|
151
|
+
},
|
|
152
|
+
null,
|
|
153
|
+
2
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
} else {
|
|
157
|
+
yield* Console.log(`Kit home: ${kitHome()}\n`)
|
|
158
|
+
yield* Console.log("Deployed vs SoT (drift is expected for deploy-time modifiers):")
|
|
159
|
+
for (const d of drift) {
|
|
160
|
+
const mark = d.drifted ? "≠" : "="
|
|
161
|
+
yield* Console.log(` ${d.setting.padEnd(22)} deployed=${d.deployed} ${mark} SoT=${d.sot}`)
|
|
162
|
+
}
|
|
163
|
+
if (claudeDeployment.state === "malformed") {
|
|
164
|
+
yield* Console.log(` ERROR claude.settings: ${claudeDeployment.diagnostic}`)
|
|
165
|
+
}
|
|
166
|
+
yield* Console.log("\nToolchain:")
|
|
167
|
+
if (toolchain.state === "failed") {
|
|
168
|
+
yield* Console.log(` ERROR: ${toolchain.diagnostic}`)
|
|
169
|
+
} else {
|
|
170
|
+
yield* Console.log(toolchain.table.trimEnd())
|
|
171
|
+
}
|
|
172
|
+
const enabled = plugins.filter((p) => p.sot === "true").length
|
|
173
|
+
yield* Console.log(
|
|
174
|
+
`\nPlugins: ${plugins.length} known (${enabled} SoT-enabled) — details: docks-kit plugins list`
|
|
175
|
+
)
|
|
176
|
+
const installed = skills.filter((s) => s.installed).length
|
|
177
|
+
yield* Console.log(
|
|
178
|
+
`Skills: ${skills.length} known (${installed} installed) — details: docks-kit skills list`
|
|
55
179
|
)
|
|
56
180
|
}
|
|
57
181
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
yield* Console.log(` ${d.setting.padEnd(22)} deployed=${d.deployed} ${mark} SoT=${d.sot}`)
|
|
182
|
+
if (diagnostics.length > 0) {
|
|
183
|
+
yield* Effect.sync(() => {
|
|
184
|
+
process.exitCode = diagnostics[0]?.exitCode ?? 1
|
|
185
|
+
})
|
|
63
186
|
}
|
|
64
|
-
yield* Console.log("\nToolchain:")
|
|
65
|
-
yield* Console.log(toolchainTable.trimEnd())
|
|
66
|
-
const enabled = plugins.filter((p) => p.sot === "true").length
|
|
67
|
-
yield* Console.log(
|
|
68
|
-
`\nPlugins: ${plugins.length} known (${enabled} SoT-enabled) — details: docks-kit plugins list`
|
|
69
|
-
)
|
|
70
|
-
const installed = skills.filter((s) => s.installed).length
|
|
71
|
-
yield* Console.log(
|
|
72
|
-
`Skills: ${skills.length} known (${installed} installed) — details: docks-kit skills list`
|
|
73
|
-
)
|
|
74
187
|
})
|
|
75
188
|
).pipe(
|
|
76
189
|
Command.withDescription(
|
package/cli/src/commands/sync.ts
CHANGED
|
@@ -52,9 +52,6 @@ const skipBubblewrap = Flag.boolean("skip-bubblewrap").pipe(
|
|
|
52
52
|
const skipPluginRefresh = Flag.boolean("skip-plugin-refresh").pipe(
|
|
53
53
|
Flag.withDescription("Install missing plugins but skip refresh-only updates for existing plugins")
|
|
54
54
|
)
|
|
55
|
-
const yes = Flag.boolean("yes").pipe(
|
|
56
|
-
Flag.withDescription("Auto-accept toolchain prompts (containers/CI)")
|
|
57
|
-
)
|
|
58
55
|
const verbose = Flag.boolean("verbose").pipe(
|
|
59
56
|
Flag.withAlias("v"),
|
|
60
57
|
Flag.withDescription("Also print no-op confirmations (already in sync, up to date, left as-is)")
|
|
@@ -102,7 +99,6 @@ export const syncCommand = Command.make(
|
|
|
102
99
|
prune,
|
|
103
100
|
skipBubblewrap,
|
|
104
101
|
skipPluginRefresh,
|
|
105
|
-
yes,
|
|
106
102
|
verbose,
|
|
107
103
|
claudeModel,
|
|
108
104
|
claudeEffort,
|
|
@@ -128,7 +124,6 @@ export const syncCommand = Command.make(
|
|
|
128
124
|
if (config.prune) args.push("--prune")
|
|
129
125
|
if (config.skipBubblewrap) args.push("--skip-bubblewrap")
|
|
130
126
|
if (config.skipPluginRefresh) args.push("--skip-plugin-refresh")
|
|
131
|
-
if (config.yes) args.push("--yes")
|
|
132
127
|
if (config.verbose) args.push("--verbose")
|
|
133
128
|
if (config.claudePermissive) args.push("--claude-permissive")
|
|
134
129
|
Option.map(config.claudeModel, (m) => args.push(`--claude-model=${m}`))
|
|
@@ -138,6 +133,10 @@ export const syncCommand = Command.make(
|
|
|
138
133
|
Option.map(config.codexModel, (m) => args.push(`--codex-model=${m}`))
|
|
139
134
|
Option.map(config.codexEffort, (level) => args.push(`--codex-effort=${level}`))
|
|
140
135
|
for (const occurrence of config.claudePlugin) {
|
|
136
|
+
if (occurrence.trim() === "") {
|
|
137
|
+
args.push("--claude-plugin=")
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
141
140
|
occurrence
|
|
142
141
|
.split(",")
|
|
143
142
|
.map((p) => p.trim())
|
|
@@ -2,7 +2,7 @@ import { Argument, Command, Flag } from "effect/unstable/cli"
|
|
|
2
2
|
import { Effect, Option } from "effect"
|
|
3
3
|
import { bail, engine } from "../engine"
|
|
4
4
|
|
|
5
|
-
const MANAGED = ["bun"
|
|
5
|
+
const MANAGED = ["bun"]
|
|
6
6
|
|
|
7
7
|
const op = Argument.string("op").pipe(
|
|
8
8
|
Argument.withDescription("check (default) | ensure <tool>"),
|
|
@@ -12,18 +12,15 @@ const tool = Argument.string("tool").pipe(
|
|
|
12
12
|
Argument.withDescription(`Managed tool for ensure: ${MANAGED.join(", ")}`),
|
|
13
13
|
Argument.optional
|
|
14
14
|
)
|
|
15
|
-
const yes = Flag.boolean("yes").pipe(
|
|
16
|
-
Flag.withDescription("Auto-accept above-verified installs")
|
|
17
|
-
)
|
|
18
15
|
const verbose = Flag.boolean("verbose").pipe(
|
|
19
16
|
Flag.withAlias("v"),
|
|
20
17
|
Flag.withDescription("Also print no-op confirmations (present, up to date)")
|
|
21
18
|
)
|
|
22
19
|
|
|
23
|
-
export const toolchainCommand = Command.make("toolchain", { op, tool,
|
|
20
|
+
export const toolchainCommand = Command.make("toolchain", { op, tool, verbose }, (config) =>
|
|
24
21
|
Effect.gen(function* () {
|
|
25
22
|
const operation = Option.getOrElse(config.op, () => "check")
|
|
26
|
-
const flags =
|
|
23
|
+
const flags = config.verbose ? ["--verbose"] : []
|
|
27
24
|
|
|
28
25
|
switch (operation) {
|
|
29
26
|
case "check":
|
|
@@ -41,6 +38,6 @@ export const toolchainCommand = Command.make("toolchain", { op, tool, yes, verbo
|
|
|
41
38
|
})
|
|
42
39
|
).pipe(
|
|
43
40
|
Command.withDescription(
|
|
44
|
-
"Verified-version floors for external tools (SoT/toolchain.json): check prints the doctor table; ensure installs
|
|
41
|
+
"Verified-version floors for external tools (SoT/toolchain.json): check prints the doctor table; ensure installs one managed tool when it is missing."
|
|
45
42
|
)
|
|
46
43
|
)
|
|
@@ -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 {
|
|
@@ -42,11 +84,108 @@ const readPackageVersion = (home: string): string => {
|
|
|
42
84
|
}
|
|
43
85
|
}
|
|
44
86
|
|
|
87
|
+
export type PackageManager = "bun" | "npm"
|
|
88
|
+
|
|
89
|
+
interface PackageRootCapture {
|
|
90
|
+
readonly status: number | null
|
|
91
|
+
readonly stdout: string
|
|
92
|
+
readonly error?: Error
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type CapturePackageRoot = (
|
|
96
|
+
command: string,
|
|
97
|
+
args: ReadonlyArray<string>
|
|
98
|
+
) => PackageRootCapture
|
|
99
|
+
|
|
100
|
+
const capturePackageRoot: CapturePackageRoot = (command, args) => {
|
|
101
|
+
const res = spawnUpdate(command, args)
|
|
102
|
+
return {
|
|
103
|
+
status: res.status,
|
|
104
|
+
stdout: res.stdout ?? "",
|
|
105
|
+
...(res.error === undefined ? {} : { error: res.error })
|
|
106
|
+
}
|
|
107
|
+
}
|
|
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
|
+
*/
|
|
115
|
+
export const packageManagerForHome = (
|
|
116
|
+
home: string,
|
|
117
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
118
|
+
host: HostOs = hostOs()
|
|
119
|
+
): PackageManager => {
|
|
120
|
+
const normalize = (value: string): string =>
|
|
121
|
+
host.id === "windows" ? value.replaceAll("\\", "/") : value
|
|
122
|
+
const normalizedHome = normalize(home)
|
|
123
|
+
const underEnvironmentRoot = (name: "BUN_INSTALL_GLOBAL_DIR" | "BUN_INSTALL"): boolean => {
|
|
124
|
+
const root = environment[name]?.trim()
|
|
125
|
+
if (root === undefined || root === "") return false
|
|
126
|
+
const normalizedRoot = normalize(root)
|
|
127
|
+
return normalizedHome === normalizedRoot || normalizedHome.startsWith(`${normalizedRoot}/`)
|
|
128
|
+
}
|
|
129
|
+
return normalizedHome.includes("/.bun/") ||
|
|
130
|
+
underEnvironmentRoot("BUN_INSTALL_GLOBAL_DIR") ||
|
|
131
|
+
underEnvironmentRoot("BUN_INSTALL")
|
|
132
|
+
? "bun"
|
|
133
|
+
: "npm"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type GlobalPackageHome =
|
|
137
|
+
| { readonly ok: true; readonly home: string }
|
|
138
|
+
| { readonly ok: false; readonly diagnostic: string }
|
|
139
|
+
|
|
140
|
+
export const resolveGlobalPackageHome = (
|
|
141
|
+
manager: PackageManager,
|
|
142
|
+
capture: CapturePackageRoot = capturePackageRoot
|
|
143
|
+
): GlobalPackageHome => {
|
|
144
|
+
const commandArgs = manager === "bun" ? ["pm", "-g", "ls"] : ["root", "-g"]
|
|
145
|
+
const result = capture(manager, commandArgs)
|
|
146
|
+
if (result.error !== undefined || result.status !== 0) {
|
|
147
|
+
const detail =
|
|
148
|
+
result.error !== undefined
|
|
149
|
+
? result.error.message
|
|
150
|
+
: `exit ${result.status ?? "without status"}`
|
|
151
|
+
return {
|
|
152
|
+
ok: false,
|
|
153
|
+
diagnostic: `${manager} ${commandArgs.join(" ")} failed: ${detail}`
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (manager === "npm") {
|
|
158
|
+
const root = result.stdout.trim()
|
|
159
|
+
return root === ""
|
|
160
|
+
? { ok: false, diagnostic: "npm root -g failed: empty output" }
|
|
161
|
+
: { ok: true, home: p(root, "docks-kit") }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const globalHeader = result.stdout
|
|
165
|
+
.split(/\r?\n/)
|
|
166
|
+
.map((line) => line.trim())
|
|
167
|
+
.find((line) => / node_modules(?: \(\d+\))?$/.test(line))
|
|
168
|
+
const globalDir =
|
|
169
|
+
globalHeader === undefined
|
|
170
|
+
? undefined
|
|
171
|
+
: /^(.*) node_modules(?: \(\d+\))?$/.exec(globalHeader)?.[1]
|
|
172
|
+
return globalDir === undefined || globalDir === ""
|
|
173
|
+
? { ok: false, diagnostic: "bun pm -g ls did not report its global package root" }
|
|
174
|
+
: { ok: true, home: p(globalDir, "node_modules", "docks-kit") }
|
|
175
|
+
}
|
|
176
|
+
|
|
45
177
|
export const packageUpdateResult = (
|
|
46
178
|
before: string,
|
|
47
|
-
after: string
|
|
179
|
+
after: string,
|
|
180
|
+
samePackageRoot = true
|
|
48
181
|
): { alreadyCurrent: boolean; message: string } => {
|
|
49
182
|
if (before === "" || after === "") return { alreadyCurrent: false, message: "" }
|
|
183
|
+
if (!samePackageRoot) {
|
|
184
|
+
return {
|
|
185
|
+
alreadyCurrent: false,
|
|
186
|
+
message: `Installed ${after} in the selected global package root.`
|
|
187
|
+
}
|
|
188
|
+
}
|
|
50
189
|
if (before === after) {
|
|
51
190
|
return { alreadyCurrent: true, message: `Already at the latest version (${after}).` }
|
|
52
191
|
}
|
|
@@ -55,7 +194,7 @@ export const packageUpdateResult = (
|
|
|
55
194
|
|
|
56
195
|
const updateCheckout = (home: string, skipSync: boolean) =>
|
|
57
196
|
Effect.gen(function* () {
|
|
58
|
-
if (
|
|
197
|
+
if (spawnUpdate("git", ["--version"], { stdio: "ignore" }).status !== 0) {
|
|
59
198
|
return yield* bail("git not found - cannot update the kit checkout")
|
|
60
199
|
}
|
|
61
200
|
const dirty = git(home, ["status", "--porcelain"])
|
|
@@ -81,7 +220,7 @@ const updateCheckout = (home: string, skipSync: boolean) =>
|
|
|
81
220
|
|
|
82
221
|
const touched = git(home, ["diff", "--name-only", before, after]).out.split("\n")
|
|
83
222
|
if (touched.includes("bun.lock") || touched.includes("package.json")) {
|
|
84
|
-
const res =
|
|
223
|
+
const res = spawnUpdate("bun", ["install", "--frozen-lockfile"], { cwd: home, stdio: "inherit" })
|
|
85
224
|
if (res.error !== undefined || res.status !== 0) {
|
|
86
225
|
return yield* bail("dependencies changed but 'bun install --frozen-lockfile' failed - fix that, then run docks-kit sync", 1)
|
|
87
226
|
}
|
|
@@ -99,39 +238,45 @@ const updateCheckout = (home: string, skipSync: boolean) =>
|
|
|
99
238
|
|
|
100
239
|
const updatePackage = (home: string, skipSync: boolean) =>
|
|
101
240
|
Effect.gen(function* () {
|
|
102
|
-
|
|
103
|
-
// so the ~/.bun path shape alone under-detects Bun installs.
|
|
104
|
-
const underEnvDir = (v: string): boolean => {
|
|
105
|
-
const dir = process.env[v]
|
|
106
|
-
return dir !== undefined && dir !== "" && home.startsWith(dir)
|
|
107
|
-
}
|
|
108
|
-
const viaBun =
|
|
109
|
-
home.includes("/.bun/") ||
|
|
110
|
-
home.includes("\\.bun\\") ||
|
|
111
|
-
underEnvDir("BUN_INSTALL_GLOBAL_DIR") ||
|
|
112
|
-
underEnvDir("BUN_INSTALL")
|
|
241
|
+
const manager = packageManagerForHome(home)
|
|
113
242
|
const beforeVersion = readPackageVersion(home)
|
|
114
|
-
const
|
|
115
|
-
?
|
|
116
|
-
:
|
|
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" })
|
|
117
247
|
if (res.error !== undefined || res.status !== 0) {
|
|
118
|
-
return yield* bail(
|
|
248
|
+
return yield* bail(
|
|
249
|
+
`global package update failed (${manager === "bun" ? "bun add -g" : "npm install -g"} docks-kit@latest)`,
|
|
250
|
+
1
|
|
251
|
+
)
|
|
119
252
|
}
|
|
120
253
|
|
|
121
|
-
const
|
|
254
|
+
const updated = resolveGlobalPackageHome(manager)
|
|
255
|
+
if (!updated.ok) {
|
|
256
|
+
return yield* bail(
|
|
257
|
+
`global package update completed, but the updated package root could not be resolved: ${updated.diagnostic}`,
|
|
258
|
+
1
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
const afterVersion = readPackageVersion(updated.home)
|
|
262
|
+
if (afterVersion === "") {
|
|
263
|
+
return yield* bail(
|
|
264
|
+
`global package update completed, but ${p(updated.home, "package.json")} has no readable version`,
|
|
265
|
+
1
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
const result = packageUpdateResult(beforeVersion, afterVersion, home === updated.home)
|
|
122
269
|
if (result.message !== "") yield* Console.log(result.message)
|
|
123
270
|
if (result.alreadyCurrent) return
|
|
124
271
|
if (skipSync) return yield* Console.log("Kit updated. Run: docks-kit sync")
|
|
125
272
|
yield* Console.log("Kit updated - running sync with the new version...")
|
|
126
|
-
|
|
127
|
-
// place) — a bare `docks-kit` PATH lookup could hit a different shim.
|
|
128
|
-
return yield* chainSync(process.execPath, updateSyncArgs(home))
|
|
273
|
+
return yield* chainSync(process.execPath, updateSyncArgs(updated.home))
|
|
129
274
|
})
|
|
130
275
|
|
|
131
276
|
export const updateCommand = Command.make("update", { noSync }, (config) =>
|
|
132
277
|
Effect.gen(function* () {
|
|
133
278
|
const home = kitHome()
|
|
134
|
-
if (existsSync(
|
|
279
|
+
if (existsSync(p(home, ".git"))) {
|
|
135
280
|
return yield* updateCheckout(home, config.noSync)
|
|
136
281
|
}
|
|
137
282
|
if (home.includes("node_modules")) {
|
package/cli/src/efforts.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sotClaudeSettings, type Tool } from "./manifests"
|
|
1
|
+
import { sotClaudeSettings, topLevelTomlString, type Tool } from "./manifests"
|
|
2
2
|
import { payloadText } from "./payload"
|
|
3
3
|
|
|
4
4
|
export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"] as const
|
|
@@ -53,12 +53,12 @@ export function validateEffortDefault(tool: Tool, value: unknown): string {
|
|
|
53
53
|
return value
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function codexSotEffort(): string | undefined {
|
|
57
|
-
return payloadText("SoT/.codex/config.toml").match(/^model_reasoning_effort\s*=\s*"([^"]+)"/m)?.[1]
|
|
58
|
-
}
|
|
59
56
|
|
|
60
57
|
export function sotEffort(tool: Tool): string {
|
|
61
|
-
const value =
|
|
58
|
+
const value =
|
|
59
|
+
tool === "claude"
|
|
60
|
+
? sotClaudeSettings().effortLevel
|
|
61
|
+
: topLevelTomlString(payloadText("SoT/.codex/config.toml"), "model_reasoning_effort")
|
|
62
62
|
return validateEffortDefault(tool, value)
|
|
63
63
|
}
|
|
64
64
|
|