docks-kit 0.15.1 → 0.15.2
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 +36 -2
- package/README.md +1 -1
- 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 -0
- package/cli/src/commands/update.ts +114 -20
- package/cli/src/efforts.ts +5 -5
- package/cli/src/engine-native/DESIGN.md +1 -1
- package/cli/src/engine-native/bun.ts +37 -11
- package/cli/src/engine-native/claudeSettingsModifiers.ts +29 -11
- package/cli/src/engine-native/claudeSync.ts +39 -31
- package/cli/src/engine-native/codexSync.ts +163 -44
- package/cli/src/engine-native/codexToml.ts +12 -7
- package/cli/src/engine-native/deps.ts +17 -15
- package/cli/src/engine-native/exec.ts +1 -14
- package/cli/src/engine-native/index.ts +17 -4
- package/cli/src/engine-native/models.ts +2 -9
- package/cli/src/engine-native/modes.ts +31 -21
- package/cli/src/engine-native/os.ts +0 -8
- package/cli/src/engine-native/parseArgs.ts +147 -44
- package/cli/src/engine-native/services.ts +1 -5
- package/cli/src/engine-native/settings.ts +3 -2
- package/cli/src/engine-native/skillsSync.ts +30 -26
- package/cli/src/engine-native/toolchain.ts +17 -13
- package/cli/src/engine.ts +38 -9
- package/cli/src/generated/sotPayload.ts +3 -3
- package/cli/src/kitHome.ts +42 -5
- package/cli/src/main.ts +12 -2
- package/cli/src/manifests.ts +17 -10
- package/cli/src/payload.ts +2 -5
- package/docks-kit +3 -3
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -91,9 +91,43 @@ When a kit-mechanic skill, its `references/`, or a wrapper agent (`.claude/agent
|
|
|
91
91
|
|
|
92
92
|
## Plans
|
|
93
93
|
|
|
94
|
-
Use direct implementation for one clear reversible low-risk local diff with one
|
|
94
|
+
Use direct implementation for one clear, reversible, low-risk local diff with one
|
|
95
|
+
bounded acceptance path; it creates no tracked plan, reviewer, or automatic
|
|
96
|
+
commit. Use a canonical plan for explicit planning, multi-commit or
|
|
97
|
+
cross-repository work, cold handoff, an unresolved decision, a cross-subsystem or
|
|
98
|
+
public-contract change, security-sensitive or destructive work, or any
|
|
99
|
+
non-`local` effect.
|
|
95
100
|
|
|
96
|
-
|
|
101
|
+
<constraint>
|
|
102
|
+
Canonical plans live in `docs/plans/active/`; status is frontmatter and
|
|
103
|
+
`docs/plans/finished/` is terminal. Exactly three skills own the workflow:
|
|
104
|
+
`plan-workspace` maintains the workspace; main-context `plan-manager` runs six
|
|
105
|
+
phases — decide, draft, research, one plan review, implement, code review — and
|
|
106
|
+
archives; internal `plan-reviewer` returns a readable pre-implementation verdict.
|
|
107
|
+
Two read-only reviewer wrappers ship, `plan-reviewer` and `code-reviewer`, and
|
|
108
|
+
nothing else in the lifecycle has a wrapper.
|
|
109
|
+
</constraint>
|
|
110
|
+
|
|
111
|
+
The record is markdown only: `plan_contract: v2` frontmatter plus eight `##`
|
|
112
|
+
sections — `## Goal`, `## Research`, `## Steps`, `## Acceptance`,
|
|
113
|
+
`## Do not touch`, `## Open questions`, `## Review`, `## Verification Results`.
|
|
114
|
+
There are no hashes, permits, run identities, locks, bundles, or `v2`/`vN` plan
|
|
115
|
+
files, and the `plan.mjs` shipped inside the installed `plan-lifecycle` plugin
|
|
116
|
+
is the only lifecycle tool. This lifecycle creates zero commits and never
|
|
117
|
+
pushes; commit when the user asks, under `docks:commit-discipline`.
|
|
118
|
+
|
|
119
|
+
Every Steps row carries an `Effect` of exactly
|
|
120
|
+
`local|probe|production_access|publish|push|release|deploy`. A step whose
|
|
121
|
+
`Effect` is not `local` requires an in-session `ask` confirmation immediately
|
|
122
|
+
before it runs; when `ask` is unavailable the step is set `blocked` with
|
|
123
|
+
`blocked_reason` naming the unconfirmed effect. Persisted effects record intent
|
|
124
|
+
only.
|
|
125
|
+
|
|
126
|
+
A plan carrying a `Plan-run:` line is a v1 plan: render it, never parse or
|
|
127
|
+
migrate it, and finish it by hand by moving the file byte-unchanged to
|
|
128
|
+
`docs/plans/finished/<YYYY-MM-DD>-<slug>.md` with a `## Retirement` section
|
|
129
|
+
appended. The complete contract lives in `docs/plans/AGENTS.md`;
|
|
130
|
+
`docs/plans/CLAUDE.md` contains only `@AGENTS.md`.
|
|
97
131
|
|
|
98
132
|
Distinct from per-tool **Open Concerns** sections (wait-on-upstream
|
|
99
133
|
blockers tied to a vendor shipping a fix — these live inside the per-tool
|
package/README.md
CHANGED
|
@@ -134,7 +134,7 @@ Details: `docks-kit docs platforms`.
|
|
|
134
134
|
Tagging `cli-v*` builds four standalone binaries (Linux x64/arm64 and macOS
|
|
135
135
|
x64/arm64) plus `SHA256SUMS` and attaches them to the GitHub release; npm
|
|
136
136
|
publishes the exact package tarball through trusted publishing with OIDC provenance.
|
|
137
|
-
Package `docks-kit` 0.15.
|
|
137
|
+
Package `docks-kit` 0.15.2 bundles the CLI + generated payload, so npm releases
|
|
138
138
|
are versioned config snapshots without shipping the authoring `SoT/` tree.
|
|
139
139
|
|
|
140
140
|
## Deeper docs
|
|
@@ -32,6 +32,9 @@ export const modelCommand = Command.make(
|
|
|
32
32
|
const dry = [...(config.dryRun ? ["--dry-run"] : []), ...(config.verbose ? ["--verbose"] : [])]
|
|
33
33
|
|
|
34
34
|
if (Option.isSome(config.value)) {
|
|
35
|
+
if (config.value.value.trim() === "") {
|
|
36
|
+
return yield* bail("Model value must not be empty or blank")
|
|
37
|
+
}
|
|
35
38
|
return yield* engine(["model", t, config.value.value, ...dry])
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -18,6 +18,11 @@ const renderTool = (t: Tool) =>
|
|
|
18
18
|
for (const m of catalog.models) {
|
|
19
19
|
yield* Console.log(` ${m.id.padEnd(28)} ${m.kind.padEnd(6)} ${m.note ?? ""}`)
|
|
20
20
|
}
|
|
21
|
+
yield* Console.log(
|
|
22
|
+
t === "claude"
|
|
23
|
+
? " (full claude-* model IDs outside the catalog are accepted with a warning)"
|
|
24
|
+
: " (well-formed IDs outside the catalog are accepted with a warning)"
|
|
25
|
+
)
|
|
21
26
|
yield* Console.log("")
|
|
22
27
|
})
|
|
23
28
|
|
|
@@ -37,9 +42,6 @@ export const modelsCommand = Command.make("models", { tool, json }, (config) =>
|
|
|
37
42
|
for (const t of tools) {
|
|
38
43
|
yield* renderTool(t)
|
|
39
44
|
}
|
|
40
|
-
yield* Console.log(
|
|
41
|
-
"Catalog: SoT/models.json (research-verified). Well-formed IDs outside it apply with a warning."
|
|
42
|
-
)
|
|
43
45
|
})
|
|
44
46
|
).pipe(
|
|
45
47
|
Command.withDescription("List kit-verified Claude and Codex models (SoT/models.json).")
|
|
@@ -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
|
@@ -138,6 +138,10 @@ export const syncCommand = Command.make(
|
|
|
138
138
|
Option.map(config.codexModel, (m) => args.push(`--codex-model=${m}`))
|
|
139
139
|
Option.map(config.codexEffort, (level) => args.push(`--codex-effort=${level}`))
|
|
140
140
|
for (const occurrence of config.claudePlugin) {
|
|
141
|
+
if (occurrence.trim() === "") {
|
|
142
|
+
args.push("--claude-plugin=")
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
141
145
|
occurrence
|
|
142
146
|
.split(",")
|
|
143
147
|
.map((p) => p.trim())
|
|
@@ -42,11 +42,99 @@ const readPackageVersion = (home: string): string => {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export type PackageManager = "bun" | "npm"
|
|
46
|
+
|
|
47
|
+
interface PackageRootCapture {
|
|
48
|
+
readonly status: number | null
|
|
49
|
+
readonly stdout: string
|
|
50
|
+
readonly error?: Error
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type CapturePackageRoot = (
|
|
54
|
+
command: string,
|
|
55
|
+
args: ReadonlyArray<string>
|
|
56
|
+
) => PackageRootCapture
|
|
57
|
+
|
|
58
|
+
const capturePackageRoot: CapturePackageRoot = (command, args) => {
|
|
59
|
+
const res = spawnSync(command, [...args], {
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
62
|
+
})
|
|
63
|
+
return {
|
|
64
|
+
status: res.status,
|
|
65
|
+
stdout: res.stdout ?? "",
|
|
66
|
+
...(res.error === undefined ? {} : { error: res.error })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const packageManagerForHome = (
|
|
71
|
+
home: string,
|
|
72
|
+
environment: NodeJS.ProcessEnv = process.env
|
|
73
|
+
): PackageManager => {
|
|
74
|
+
const underEnvironmentRoot = (name: "BUN_INSTALL_GLOBAL_DIR" | "BUN_INSTALL"): boolean => {
|
|
75
|
+
const root = environment[name]?.trim()
|
|
76
|
+
return root !== undefined && root !== "" && (home === root || home.startsWith(`${root}/`))
|
|
77
|
+
}
|
|
78
|
+
return home.includes("/.bun/") ||
|
|
79
|
+
underEnvironmentRoot("BUN_INSTALL_GLOBAL_DIR") ||
|
|
80
|
+
underEnvironmentRoot("BUN_INSTALL")
|
|
81
|
+
? "bun"
|
|
82
|
+
: "npm"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type GlobalPackageHome =
|
|
86
|
+
| { readonly ok: true; readonly home: string }
|
|
87
|
+
| { readonly ok: false; readonly diagnostic: string }
|
|
88
|
+
|
|
89
|
+
export const resolveGlobalPackageHome = (
|
|
90
|
+
manager: PackageManager,
|
|
91
|
+
capture: CapturePackageRoot = capturePackageRoot
|
|
92
|
+
): GlobalPackageHome => {
|
|
93
|
+
const commandArgs = manager === "bun" ? ["pm", "-g", "ls"] : ["root", "-g"]
|
|
94
|
+
const result = capture(manager, commandArgs)
|
|
95
|
+
if (result.error !== undefined || result.status !== 0) {
|
|
96
|
+
const detail =
|
|
97
|
+
result.error !== undefined
|
|
98
|
+
? result.error.message
|
|
99
|
+
: `exit ${result.status ?? "without status"}`
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
diagnostic: `${manager} ${commandArgs.join(" ")} failed: ${detail}`
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (manager === "npm") {
|
|
107
|
+
const root = result.stdout.trim()
|
|
108
|
+
return root === ""
|
|
109
|
+
? { ok: false, diagnostic: "npm root -g failed: empty output" }
|
|
110
|
+
: { ok: true, home: join(root, "docks-kit") }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const globalHeader = result.stdout
|
|
114
|
+
.split(/\r?\n/)
|
|
115
|
+
.map((line) => line.trim())
|
|
116
|
+
.find((line) => / node_modules(?: \(\d+\))?$/.test(line))
|
|
117
|
+
const globalDir =
|
|
118
|
+
globalHeader === undefined
|
|
119
|
+
? undefined
|
|
120
|
+
: /^(.*) node_modules(?: \(\d+\))?$/.exec(globalHeader)?.[1]
|
|
121
|
+
return globalDir === undefined || globalDir === ""
|
|
122
|
+
? { ok: false, diagnostic: "bun pm -g ls did not report its global package root" }
|
|
123
|
+
: { ok: true, home: join(globalDir, "node_modules", "docks-kit") }
|
|
124
|
+
}
|
|
125
|
+
|
|
45
126
|
export const packageUpdateResult = (
|
|
46
127
|
before: string,
|
|
47
|
-
after: string
|
|
128
|
+
after: string,
|
|
129
|
+
samePackageRoot = true
|
|
48
130
|
): { alreadyCurrent: boolean; message: string } => {
|
|
49
131
|
if (before === "" || after === "") return { alreadyCurrent: false, message: "" }
|
|
132
|
+
if (!samePackageRoot) {
|
|
133
|
+
return {
|
|
134
|
+
alreadyCurrent: false,
|
|
135
|
+
message: `Installed ${after} in the selected global package root.`
|
|
136
|
+
}
|
|
137
|
+
}
|
|
50
138
|
if (before === after) {
|
|
51
139
|
return { alreadyCurrent: true, message: `Already at the latest version (${after}).` }
|
|
52
140
|
}
|
|
@@ -99,33 +187,39 @@ const updateCheckout = (home: string, skipSync: boolean) =>
|
|
|
99
187
|
|
|
100
188
|
const updatePackage = (home: string, skipSync: boolean) =>
|
|
101
189
|
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")
|
|
190
|
+
const manager = packageManagerForHome(home)
|
|
113
191
|
const beforeVersion = readPackageVersion(home)
|
|
114
|
-
const res =
|
|
115
|
-
|
|
116
|
-
|
|
192
|
+
const res =
|
|
193
|
+
manager === "bun"
|
|
194
|
+
? spawnSync("bun", ["add", "-g", "docks-kit@latest"], { stdio: "inherit" })
|
|
195
|
+
: spawnSync("npm", ["install", "-g", "docks-kit@latest"], { stdio: "inherit" })
|
|
117
196
|
if (res.error !== undefined || res.status !== 0) {
|
|
118
|
-
return yield* bail(
|
|
197
|
+
return yield* bail(
|
|
198
|
+
`global package update failed (${manager === "bun" ? "bun add -g" : "npm install -g"} docks-kit@latest)`,
|
|
199
|
+
1
|
|
200
|
+
)
|
|
119
201
|
}
|
|
120
202
|
|
|
121
|
-
const
|
|
203
|
+
const updated = resolveGlobalPackageHome(manager)
|
|
204
|
+
if (!updated.ok) {
|
|
205
|
+
return yield* bail(
|
|
206
|
+
`global package update completed, but the updated package root could not be resolved: ${updated.diagnostic}`,
|
|
207
|
+
1
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
const afterVersion = readPackageVersion(updated.home)
|
|
211
|
+
if (afterVersion === "") {
|
|
212
|
+
return yield* bail(
|
|
213
|
+
`global package update completed, but ${join(updated.home, "package.json")} has no readable version`,
|
|
214
|
+
1
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
const result = packageUpdateResult(beforeVersion, afterVersion, home === updated.home)
|
|
122
218
|
if (result.message !== "") yield* Console.log(result.message)
|
|
123
219
|
if (result.alreadyCurrent) return
|
|
124
220
|
if (skipSync) return yield* Console.log("Kit updated. Run: docks-kit sync")
|
|
125
221
|
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))
|
|
222
|
+
return yield* chainSync(process.execPath, updateSyncArgs(updated.home))
|
|
129
223
|
})
|
|
130
224
|
|
|
131
225
|
export const updateCommand = Command.make("update", { noSync }, (config) =>
|
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
|
|
|
@@ -153,7 +153,7 @@ active logger binding.
|
|
|
153
153
|
| `exec.ts` | slash-stable path helpers, POSIX command probes, capture/spawn wrappers, and change-detecting write/copy helpers |
|
|
154
154
|
| `logger.ts` | Logger shape + stable raw stdout/stderr sink factory; the run-scoped verbosity gate lives in `index.ts` |
|
|
155
155
|
| `deps.ts` | external-tool registry: identity, requirement class, presence probe, supported-host install hints, per-manager missing-tool dedup; callers supply the current run Logger to `warnMissing` |
|
|
156
|
-
| `os.ts` | platform capability seam —
|
|
156
|
+
| `os.ts` | platform capability seam — host reader and injected platform normalization (`rawPlatform`, `platformName`) |
|
|
157
157
|
| `services.ts` | shared raw-Logger + DependencyManager + Platform factory; wrapped in Effect Layers at `cli/src/services.ts`, with the run-scoped Logger gate applied only by `runEngineNative` |
|
|
158
158
|
|
|
159
159
|
## Platform Support
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { rmSync } from "node:fs"
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs"
|
|
2
2
|
import { tmpdir } from "node:os"
|
|
3
3
|
|
|
4
|
-
import { p, spawnProcess } from "./exec"
|
|
4
|
+
import { p, spawnProcess, type AsyncProcessOptions, type AsyncProcessResult } from "./exec"
|
|
5
5
|
import type { Ctx } from "./index"
|
|
6
6
|
import type { EngineServices } from "./services"
|
|
7
7
|
import { field } from "./toolchain"
|
|
8
8
|
|
|
9
9
|
export type BunRuntimeState =
|
|
10
10
|
| { readonly kind: "ready"; readonly executable: string }
|
|
11
|
-
| {
|
|
12
|
-
|
|
11
|
+
| {
|
|
12
|
+
readonly kind: "deferred"
|
|
13
|
+
readonly reason: "missing-curl" | "download-failed" | "installer-failed" | "install-failed"
|
|
14
|
+
}
|
|
13
15
|
|
|
14
16
|
function predictedExecutable(ctx: Ctx): string {
|
|
15
17
|
const root = process.env["BUN_INSTALL"] !== undefined && process.env["BUN_INSTALL"] !== ""
|
|
@@ -18,11 +20,26 @@ function predictedExecutable(ctx: Ctx): string {
|
|
|
18
20
|
return p(root, "bin", "bun")
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
type BunInstallResult =
|
|
24
|
+
| { readonly ok: true }
|
|
25
|
+
| { readonly ok: false; readonly reason: "download-failed" | "installer-failed"; readonly detail: string }
|
|
26
|
+
|
|
27
|
+
function processFailure(result: AsyncProcessResult): string {
|
|
28
|
+
const details = [result.error?.message, result.stderr.trim()].filter((value): value is string => value !== undefined && value !== "")
|
|
29
|
+
return details.join(": ") || (result.exitCode === null ? "the process ended without an exit code" : `exit code ${result.exitCode}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function installBun(pin: string, installer: string): Promise<BunInstallResult> {
|
|
33
|
+
const options: AsyncProcessOptions = { stdio: ["ignore", "ignore", "pipe"] }
|
|
34
|
+
const download = await spawnProcess("curl", ["-fsSL", "https://bun.sh/install", "-o", installer], options)
|
|
35
|
+
if (download.error !== undefined || download.exitCode !== 0) {
|
|
36
|
+
return { ok: false, reason: "download-failed", detail: processFailure(download) }
|
|
37
|
+
}
|
|
38
|
+
const install = await spawnProcess("bash", [installer, `bun-v${pin}`], options)
|
|
39
|
+
if (install.error !== undefined || install.exitCode !== 0) {
|
|
40
|
+
return { ok: false, reason: "installer-failed", detail: processFailure(install) }
|
|
25
41
|
}
|
|
42
|
+
return { ok: true }
|
|
26
43
|
}
|
|
27
44
|
|
|
28
45
|
export function bunBootstrap(ctx: Ctx, services: EngineServices): Promise<BunRuntimeState> {
|
|
@@ -52,11 +69,20 @@ async function runBunBootstrap(ctx: Ctx, services: EngineServices): Promise<BunR
|
|
|
52
69
|
return { kind: "ready", executable }
|
|
53
70
|
}
|
|
54
71
|
services.logger.warn(`Bun not found — installing Bun ${pin} (kit-verified)...`)
|
|
55
|
-
const
|
|
72
|
+
const temporaryDir = mkdtempSync(p(tmpdir(), "docks-kit-bun-"))
|
|
73
|
+
const installer = p(temporaryDir, "install.sh")
|
|
74
|
+
let result: BunInstallResult
|
|
56
75
|
try {
|
|
57
|
-
await installBun(pin, installer)
|
|
76
|
+
result = await installBun(pin, installer)
|
|
58
77
|
} finally {
|
|
59
|
-
rmSync(
|
|
78
|
+
rmSync(temporaryDir, { recursive: true, force: true })
|
|
79
|
+
}
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
const stage = result.reason === "download-failed" ? "installer download" : "installer"
|
|
82
|
+
services.logger.warn(
|
|
83
|
+
`Bun ${stage} failed (${result.detail}). Install Bun manually from https://bun.sh/docs/installation, then re-run sync.`
|
|
84
|
+
)
|
|
85
|
+
return { kind: "deferred", reason: result.reason }
|
|
60
86
|
}
|
|
61
87
|
|
|
62
88
|
const installed = await services.deps.path("bun")
|
|
@@ -4,6 +4,7 @@ import { readFileSync, renameSync, writeFileSync } from "node:fs"
|
|
|
4
4
|
|
|
5
5
|
import { resolveEffort } from "../efforts"
|
|
6
6
|
import type { Ctx } from "./index"
|
|
7
|
+
import { ExitError } from "./parseArgs"
|
|
7
8
|
import { isObject, jqStringify, parseJson } from "./jq"
|
|
8
9
|
|
|
9
10
|
interface ClaudeSettingEdit {
|
|
@@ -19,27 +20,44 @@ function syncClaudeSetting(ctx: Ctx, edit: ClaudeSettingEdit): void {
|
|
|
19
20
|
const { change, echo, err, verbose, warn } = ctx.services.logger
|
|
20
21
|
const userSettings = p(ctx.home, ".claude", "settings.json")
|
|
21
22
|
|
|
22
|
-
if (ctx.dryRun) {
|
|
23
|
-
echo(`[dry-run] (${edit.tag}) ${edit.dryRun} in ${userSettings}`)
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
|
|
27
23
|
let text: string
|
|
28
24
|
try {
|
|
29
25
|
text = readFileSync(userSettings, "utf8")
|
|
30
|
-
} catch {
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error !== null && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
28
|
+
// A dry-run sync writes nothing, so an absent file here does not mean the
|
|
29
|
+
// edit is skipped: the same run already previewed installing the file,
|
|
30
|
+
// and the real run applies the edit to it. Outside a sync, such as
|
|
31
|
+
// `docks-kit model claude <m>`, nothing creates the file and the skip
|
|
32
|
+
// stands.
|
|
33
|
+
if (ctx.dryRun && ctx.syncClaude) {
|
|
34
|
+
echo(`[dry-run] (${edit.tag}) ${edit.dryRun} in ${userSettings}`)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
warn(`(${edit.tag}) ${userSettings} missing — skipped`)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
const cause = error instanceof Error ? error.message : String(error)
|
|
41
|
+
err(`(${edit.tag}) could not read ${userSettings}: ${cause}`)
|
|
42
|
+
throw new ExitError(1)
|
|
33
43
|
}
|
|
34
44
|
const doc = parseJson(text)
|
|
35
45
|
if (doc === undefined) {
|
|
36
46
|
err(`(${edit.tag}) ${userSettings} is not valid JSON — skipped`)
|
|
37
47
|
return
|
|
38
48
|
}
|
|
39
|
-
if (isObject(doc)) {
|
|
40
|
-
|
|
41
|
-
|
|
49
|
+
if (!isObject(doc)) {
|
|
50
|
+
err(`(${edit.tag}) ${userSettings} must contain a JSON object — aborting`)
|
|
51
|
+
throw new ExitError(1)
|
|
42
52
|
}
|
|
53
|
+
|
|
54
|
+
if (ctx.dryRun) {
|
|
55
|
+
echo(`[dry-run] (${edit.tag}) ${edit.dryRun} in ${userSettings}`)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (edit.value === undefined) delete doc[edit.key]
|
|
60
|
+
else doc[edit.key] = edit.value
|
|
43
61
|
const out = jqStringify(doc)
|
|
44
62
|
if (out === text) {
|
|
45
63
|
verbose(edit.unchanged)
|