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.
Files changed (49) hide show
  1. package/AGENTS.md +57 -17
  2. package/README.md +33 -31
  3. package/cli/docs/flags.md +0 -1
  4. package/cli/docs/install.md +28 -13
  5. package/cli/docs/overview.md +2 -2
  6. package/cli/docs/platforms.md +5 -2
  7. package/cli/docs/sync-layers.md +3 -4
  8. package/cli/docs/toolchain.md +26 -34
  9. package/cli/src/commands/docs.ts +3 -3
  10. package/cli/src/commands/model.ts +3 -0
  11. package/cli/src/commands/models.ts +5 -3
  12. package/cli/src/commands/status.ts +145 -32
  13. package/cli/src/commands/sync.ts +4 -5
  14. package/cli/src/commands/toolchain.ts +4 -7
  15. package/cli/src/commands/update.ts +177 -32
  16. package/cli/src/efforts.ts +5 -5
  17. package/cli/src/engine-native/DESIGN.md +31 -22
  18. package/cli/src/engine-native/bun.ts +42 -14
  19. package/cli/src/engine-native/claudeRuntime.ts +17 -9
  20. package/cli/src/engine-native/claudeSettingsModifiers.ts +29 -11
  21. package/cli/src/engine-native/claudeSync.ts +91 -55
  22. package/cli/src/engine-native/codexSync.ts +173 -49
  23. package/cli/src/engine-native/codexToml.ts +12 -7
  24. package/cli/src/engine-native/deps.ts +36 -95
  25. package/cli/src/engine-native/exec.ts +42 -24
  26. package/cli/src/engine-native/index.ts +17 -6
  27. package/cli/src/engine-native/models.ts +2 -9
  28. package/cli/src/engine-native/modes.ts +54 -35
  29. package/cli/src/engine-native/os/darwin.ts +62 -0
  30. package/cli/src/engine-native/os/index.ts +42 -0
  31. package/cli/src/engine-native/os/linux.ts +62 -0
  32. package/cli/src/engine-native/os/targets.ts +73 -0
  33. package/cli/src/engine-native/os/types.ts +75 -0
  34. package/cli/src/engine-native/os/windows.ts +176 -0
  35. package/cli/src/engine-native/parseArgs.ts +147 -48
  36. package/cli/src/engine-native/services.ts +1 -11
  37. package/cli/src/engine-native/settings.ts +3 -2
  38. package/cli/src/engine-native/skillsSync.ts +141 -89
  39. package/cli/src/engine-native/toolchain.ts +5 -147
  40. package/cli/src/engine.ts +41 -11
  41. package/cli/src/generated/sotPayload.ts +7 -7
  42. package/cli/src/kitHome.ts +42 -5
  43. package/cli/src/main.ts +12 -2
  44. package/cli/src/manifests.ts +28 -11
  45. package/cli/src/payload.ts +2 -5
  46. package/docks-kit +4 -4
  47. package/docks-kit.ps1 +123 -0
  48. package/package.json +9 -5
  49. 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
- const gatherDrift = (): Array<Drift> => {
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 dep = deployedClaudeSettings() ?? {}
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
- return [
34
- row("claude.model", dep.model, sot.model),
35
- row("claude.effortLevel", dep.effortLevel, sot.effortLevel),
36
- row(
37
- "claude.compactWindow",
38
- dep.env?.CLAUDE_CODE_AUTO_COMPACT_WINDOW,
39
- sot.env?.CLAUDE_CODE_AUTO_COMPACT_WINDOW
40
- ),
41
- row("codex.model", deployedCodexModel(), sotCodexModel())
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 toolchainTable = yield* engineCapture(["toolchain", "check"])
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
- return yield* Console.log(
54
- JSON.stringify({ kitHome: kitHome(), drift, plugins, skills, toolchainTable }, null, 2)
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
- yield* Console.log(`Kit home: ${kitHome()}\n`)
59
- yield* Console.log("Deployed vs SoT (drift is expected for deploy-time modifiers):")
60
- for (const d of drift) {
61
- const mark = d.drifted ? "≠" : "="
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(
@@ -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", "effect-solutions"]
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, yes, verbose }, (config) =>
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 = [...(config.yes ? ["--yes"] : []), ...(config.verbose ? ["--verbose"] : [])]
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/upgrades one managed tool per policy (above-verified versions prompt; --yes accepts)."
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
- const git = (home: string, args: Array<string>): { ok: boolean; out: string } => {
14
- const res = spawnSync("git", ["-C", home, ...args], {
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
- stdio: ["ignore", "pipe", "pipe"]
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 = spawnSync(argv0, args, { stdio: "inherit" })
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
- join(home, "cli/src/main.ts"),
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(join(home, "package.json"), "utf8"))
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 (spawnSync("git", ["--version"], { stdio: "ignore" }).status !== 0) {
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 = spawnSync("bun", ["install", "--frozen-lockfile"], { cwd: home, stdio: "inherit" })
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
- // Bun's global dir is configurable (BUN_INSTALL_GLOBAL_DIR / BUN_INSTALL),
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 res = viaBun
115
- ? spawnSync("bun", ["add", "-g", "docks-kit@latest"], { stdio: "inherit" })
116
- : spawnSync("npm", ["install", "-g", "docks-kit@latest"], { stdio: "inherit" })
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(`global package update failed (${viaBun ? "bun add -g" : "npm install -g"} docks-kit@latest)`, 1)
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 result = packageUpdateResult(beforeVersion, readPackageVersion(home))
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
- // Chain through the package dir just updated (global installs update in
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(join(home, ".git"))) {
279
+ if (existsSync(p(home, ".git"))) {
135
280
  return yield* updateCheckout(home, config.noSync)
136
281
  }
137
282
  if (home.includes("node_modules")) {
@@ -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 = tool === "claude" ? sotClaudeSettings().effortLevel : codexSotEffort()
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