docks-kit 0.14.4 → 0.15.0

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 CHANGED
@@ -21,7 +21,7 @@ Bun source.
21
21
  |------|---------|
22
22
  | `docks-kit` | CLI launcher: on supported hosts, runs the platform binary in `cli/dist/` only when its `--version` matches `package.json`, otherwise Bun-from-source (auto-installs Bun + `node_modules`). Unsupported hosts fail before source fallback. No-Bun recovery is the standalone platform release binary |
23
23
  | `cli/src/engine-native/` | EngineNative implementation for `sync`, `model`, and `toolchain`; idempotent, flag-gated for destructive reconciliation |
24
- | `cli/` | Effect-TS CLI + bundled docs topics |
24
+ | `cli/` | Effect 4 beta CLI + bundled docs topics |
25
25
  | `SoT/models.json` | Kit-verified Claude and Codex model catalog |
26
26
  | `SoT/toolchain.json` | Toolchain floors manifest (verified pins consumed by EngineNative) |
27
27
  | `SoT/.claude/bin/` | Dependency-free Bun runtime programs for Claude's statusline, SessionStart, and Notification |
@@ -51,6 +51,8 @@ For per-tool SoT layouts (`SoT/.claude/`, `SoT/.codex/`), see the matching SoT d
51
51
 
52
52
  - **Idempotent operations.** Every EngineNative sync step must be safe to re-run. Settings merges, plugin installs, and marketplace adds are all idempotent — re-running with no SoT changes is a no-op.
53
53
  - **Removed bash engine.** The bash engine was removed after the `bash-engine-final` tag. `DOCKS_KIT_ENGINE=bash` must fail with the removed-engine message; engine bugs are fixed forward in EngineNative.
54
+ - **Effect 4 CLI stack.** The CLI pins `effect@4.0.0-beta.107` (including `effect/unstable/cli`), `@effect/platform-bun@4.0.0-beta.107` (`BunServices.layer`, `BunRuntime.runMain`), `@effect/vitest@4.0.0-beta.107`, and `vitest@4.1.10` (required by the `@effect/vitest` peer range). `@effect/cli` and `@effect/platform` are removed and must not be reintroduced.
55
+ - **Effect skill routing.** Effect work in this checkout must use the `effect-v4` skill from the already-enabled `effect-kit@docks` plugin; `effect-ts-setup`, `effect-ts-port`, and `effect-ts-specialist` target Effect 3.x and do not apply. Verify migration and API call shapes against the installed declarations under `node_modules/effect/dist/unstable/cli/`, never from memory or a mutable dist-tag.
54
56
  - **Targeted syncs.** `./docks-kit sync` accepts positional targets: `claude`, `codex`, and `agents`. Use the narrowest target that matches the SoT change (for example, `./docks-kit sync codex` for Codex-only config edits); targets can be combined with `--dry-run`, `--skip-bubblewrap` (skip optional bubblewrap bootstrap for the Codex Linux sandbox), `--skip-plugin-refresh` (install missing plugins without refreshing existing caches; used by `docks-kit update`), `--reconcile`, `--prune`, `--yes` (auto-accept toolchain prompts), and the deploy-time modifiers `--claude-compact-window=<tokens>` / `--claude-permissive` / `--claude-model=<m>` / `--claude-effort=<level>` / `--claude-advisor=<on|off|default>` / `--codex-model=<m>` / `--codex-effort=<level>` (see `CLAUDE.md` § Deploy-time modifiers).
55
57
  - **Additive by default.** Keys present in deployed config but absent from SoT are preserved on default sync. This protects user-only additions, but means drift accumulates — neither flag-less reset can clean it up. The one exception is the Claude `removed` manifest (`claude::_removed_manifest`), a curated list of unambiguous kit-owned artifacts that `claude::sync_removals` force-prunes on every sync, including the home-relative `~/.local/bin/session-relay` artifact installed outside `~/.claude`; see `CLAUDE.md` § Pruning stale artifacts.
56
58
  - **`--reconcile` / `--prune` are the kit-owned reconcile flags.** Orthogonal — `--reconcile` reconciles the settings layer (SoT-declared keys/tables/arrays win; user-only keys and nested objects are preserved; permissions arrays are replaced wholesale by SoT). `--prune` uninstalls kit-managed installations not in the SoT (plugins, marketplaces, and `~/.agents/skills/*` entries tracked in `~/.agents/.kit-managed-skills`). Combine for a full reset to SoT's kit-managed scope. User-only additions outside the kit's scope (custom env vars, mcpServers, manually-installed skills, third-party plugins not declared in SoT) are always preserved. Each tool's per-tool file documents the specific paths and diff recipes.
package/README.md CHANGED
@@ -52,9 +52,15 @@ docks-kit docs [topic] self-documentation (9 topics)
52
52
  --help --version --wizard --completions built-in
53
53
  ```
54
54
 
55
- The CLI is a typed front-end ([Effect-TS](https://effect.website) on Bun);
56
- all mutation runs through EngineNative in `cli/src/engine-native/`, with
57
- golden-regression coverage for dry-run output, mutation snapshots, and argv logs.
55
+ The CLI is a typed Effect 4 beta front-end on Bun; all mutation runs through
56
+ EngineNative in `cli/src/engine-native/`, with golden-regression coverage for
57
+ dry-run output, mutation snapshots, and argv logs. Its dependency graph is
58
+ `effect@4.0.0-beta.107` (including `effect/unstable/cli`),
59
+ `@effect/platform-bun@4.0.0-beta.107` (`BunServices.layer` and
60
+ `BunRuntime.runMain`), `@effect/vitest@4.0.0-beta.107`, and `vitest@4.1.10`
61
+ (required by the `@effect/vitest` peer range). `@effect/cli` and
62
+ `@effect/platform` are removed: neither has a 4.x release, the CLI API moved
63
+ into core, and the standalone platform package was unused here.
58
64
 
59
65
  ### Key flags (`docks-kit sync`)
60
66
 
@@ -106,7 +112,7 @@ and a later flag-less sync reverts them. Full reference: `docks-kit docs flags`
106
112
  | `SoT/toolchain.json` | Verified-version floors |
107
113
  | `cli/src/engine-native/` | EngineNative sync/model/toolchain implementation |
108
114
  | `cli/src/generated/sotPayload.ts` | Generated in-memory payload used by standalone and npm installs |
109
- | `cli/` | docks-kit CLI (Effect-TS on Bun) + bundled docs topics |
115
+ | `cli/` | docks-kit CLI (Effect 4 beta on Bun) + bundled docs topics |
110
116
  | `docks-kit` | Launcher (binary → bun-from-source) |
111
117
  | `install.sh` | Global installer (Bun bootstrap + `bun add -g`) |
112
118
  | `docs/plans/` | Multi-commit work-item plans |
@@ -128,7 +134,7 @@ Details: `docks-kit docs platforms`.
128
134
  Tagging `cli-v*` builds four standalone binaries (Linux x64/arm64 and macOS
129
135
  x64/arm64) plus `SHA256SUMS` and attaches them to the GitHub release; npm
130
136
  publishes the exact package tarball through trusted publishing with OIDC provenance.
131
- Package `docks-kit` 0.14.4 bundles the CLI + generated payload, so npm releases
137
+ Package `docks-kit` 0.15.0 bundles the CLI + generated payload, so npm releases
132
138
  are versioned config snapshots without shipping the authoring `SoT/` tree.
133
139
 
134
140
  ## Deeper docs
@@ -16,7 +16,7 @@ AI-assisted dev environment on every machine.
16
16
  | `SoT/toolchain.json` | Verified-version floors for external tools (see `docks-kit docs toolchain`) |
17
17
  | `cli/src/generated/sotPayload.ts` | Deterministic generated payload embedded in standalone/npm execution |
18
18
  | `cli/src/engine-native/` | EngineNative mutation logic for sync/model/toolchain |
19
- | `cli/` | This CLI (Effect-TS on Bun) plus bundled docs |
19
+ | `cli/` | This CLI (Effect 4 beta on Bun) plus bundled docs |
20
20
  | `docks-kit` | Launcher: version-matching compiled binary → bun-from-source, with Bun auto-install |
21
21
 
22
22
  ## Design rules
@@ -0,0 +1,433 @@
1
+ import { GlobalFlag } from "effect/unstable/cli"
2
+ import { docsCommand } from "./commands/docs"
3
+ import { modelCommand } from "./commands/model"
4
+ import { modelsCommand } from "./commands/models"
5
+ import { pluginsCommand } from "./commands/plugins"
6
+ import { skillsCommand } from "./commands/skills"
7
+ import { statusCommand } from "./commands/status"
8
+ import { syncCommand } from "./commands/sync"
9
+ import { toolchainCommand } from "./commands/toolchain"
10
+ import { updateCommand } from "./commands/update"
11
+ import {
12
+ advisorCatalog,
13
+ advisorFlagGrammar,
14
+ effortCatalog,
15
+ effortFlagGrammar
16
+ } from "./efforts"
17
+ import { KNOWN_CLAUDE_OPTIN_PLUGINS } from "./engine-native/parseArgs"
18
+ import { modelCatalog, type Tool } from "./manifests"
19
+
20
+ export type ArgvOutcome =
21
+ | { readonly kind: "reject"; readonly message: string; readonly exitCode: number }
22
+ | { readonly kind: "accept"; readonly args: ReadonlyArray<string> }
23
+
24
+ interface FlagSurface {
25
+ readonly longFlags: ReadonlyArray<string>
26
+ readonly aliases: ReadonlyMap<string, string>
27
+ readonly valueFlags: ReadonlyArray<string>
28
+ readonly repeatableFlags: ReadonlyArray<string>
29
+ }
30
+ interface GlobalFlagSurface extends FlagSurface {
31
+ readonly actionFlags: ReadonlySet<string>
32
+ }
33
+
34
+ interface CommandValue {
35
+ readonly name: string
36
+ }
37
+
38
+
39
+ const flagMetadata = (
40
+ param: unknown
41
+ ): {
42
+ readonly name: string
43
+ readonly aliases: ReadonlyArray<string>
44
+ readonly takesValue: boolean
45
+ readonly repeatable: boolean
46
+ } => {
47
+ let current = param
48
+ let repeatable = false
49
+
50
+ while (typeof current === "object" && current !== null) {
51
+ if (!("_tag" in current)) break
52
+ if (current._tag === "Variadic") repeatable = true
53
+ if (current._tag !== "Single") {
54
+ if (!("param" in current)) break
55
+ current = current.param
56
+ continue
57
+ }
58
+
59
+ if (!("name" in current) || typeof current.name !== "string") {
60
+ throw new Error("Effect CLI exposed a flag without a string name")
61
+ }
62
+ if (
63
+ !("aliases" in current) ||
64
+ !Array.isArray(current.aliases) ||
65
+ !current.aliases.every((alias) => typeof alias === "string")
66
+ ) {
67
+ throw new Error(`Effect CLI exposed invalid aliases for --${current.name}`)
68
+ }
69
+ if (
70
+ !("primitiveType" in current) ||
71
+ typeof current.primitiveType !== "object" ||
72
+ current.primitiveType === null ||
73
+ !("_tag" in current.primitiveType) ||
74
+ typeof current.primitiveType._tag !== "string"
75
+ ) {
76
+ throw new Error(`Effect CLI exposed no primitive type for --${current.name}`)
77
+ }
78
+ const aliases: ReadonlyArray<string> = current.aliases
79
+ return {
80
+ name: current.name,
81
+ aliases,
82
+ takesValue: current.primitiveType._tag !== "Boolean",
83
+ repeatable
84
+ }
85
+ }
86
+
87
+ throw new Error("Effect CLI exposed an unsupported flag parameter")
88
+ }
89
+
90
+ const flagSurface = (params: ReadonlyArray<unknown>): FlagSurface => {
91
+ const longFlags: Array<string> = []
92
+ const aliases = new Map<string, string>()
93
+ const valueFlags: Array<string> = []
94
+ const repeatableFlags: Array<string> = []
95
+
96
+ for (const param of params) {
97
+ const metadata = flagMetadata(param)
98
+ const longName = `--${metadata.name}`
99
+ longFlags.push(longName)
100
+ if (metadata.takesValue) valueFlags.push(longName)
101
+ if (metadata.repeatable) repeatableFlags.push(longName)
102
+ for (const alias of metadata.aliases) aliases.set(`-${alias}`, longName)
103
+ }
104
+
105
+ return { longFlags, aliases, valueFlags, repeatableFlags }
106
+ }
107
+
108
+ const commandSurface = (command: CommandValue): FlagSurface => {
109
+ if (!("config" in command)) {
110
+ throw new Error(`Effect CLI did not expose flags for '${command.name}'`)
111
+ }
112
+ const config = command.config
113
+ if (
114
+ typeof config !== "object" ||
115
+ config === null ||
116
+ !("flags" in config) ||
117
+ !Array.isArray(config.flags)
118
+ ) {
119
+ throw new Error(`Effect CLI did not expose flags for '${command.name}'`)
120
+ }
121
+
122
+ return flagSurface(config.flags)
123
+ }
124
+
125
+ const globalSurface = (): GlobalFlagSurface => {
126
+ const builtIns: unknown = GlobalFlag.BuiltIns
127
+ if (!Array.isArray(builtIns)) {
128
+ throw new Error("Effect CLI did not expose its built-in global flags")
129
+ }
130
+
131
+ const actionFlags = new Set<string>()
132
+ const flags = builtIns.map((builtIn) => {
133
+ if (
134
+ (typeof builtIn !== "object" && typeof builtIn !== "function") ||
135
+ builtIn === null ||
136
+ !("flag" in builtIn) ||
137
+ !("_tag" in builtIn) ||
138
+ typeof builtIn._tag !== "string"
139
+ ) {
140
+ throw new Error("Effect CLI exposed an unsupported built-in global flag")
141
+ }
142
+ if (builtIn._tag === "Action") {
143
+ // Only a presence-based action is idempotent. `--completions` is an action
144
+ // that takes a value, and Effect 4 would silently keep the first one, which
145
+ // is exactly what the duplicate rule exists to refuse.
146
+ const metadata = flagMetadata(builtIn.flag)
147
+ if (!metadata.takesValue) actionFlags.add(`--${metadata.name}`)
148
+ }
149
+ return builtIn.flag
150
+ })
151
+ return { ...flagSurface(flags), actionFlags }
152
+ }
153
+
154
+ const COMMANDS: ReadonlyArray<CommandValue> = [
155
+ syncCommand,
156
+ updateCommand,
157
+ modelCommand,
158
+ toolchainCommand,
159
+ modelsCommand,
160
+ statusCommand,
161
+ pluginsCommand,
162
+ skillsCommand,
163
+ docsCommand
164
+ ]
165
+
166
+ // A Map, not a plain object: an object literal answers `toString` and friends from
167
+ // `Object.prototype`, so an unknown subcommand with such a name would slip past the
168
+ // unknown-command guard and dereference a surface that was never built.
169
+ const COMMAND_SURFACES: ReadonlyMap<string, FlagSurface> = new Map(
170
+ COMMANDS.map((command) => [command.name, commandSurface(command)])
171
+ )
172
+
173
+ const GLOBAL_SURFACE: GlobalFlagSurface = globalSurface()
174
+
175
+ const LEGACY_HINTS: Readonly<Record<string, string>> = {
176
+ "--force": "--force was renamed to --reconcile",
177
+ "--remove-plugins":
178
+ "--remove-plugins was renamed to --prune (it also removes marketplaces + kit-managed skills)",
179
+ "--680k": "--680k was renamed to --claude-compact-window=680k",
180
+ "--permissive": "--permissive was renamed to --claude-permissive",
181
+ "--supabase": "--supabase was renamed to --claude-plugin=supabase",
182
+ "--n8n": "--n8n was renamed to --claude-plugin=n8n",
183
+ "--skip-rtk": "--skip-rtk was renamed to --skip-bubblewrap",
184
+ "--claude": "--claude was renamed: pass the target as a word, e.g. 'sync claude'",
185
+ "--codex": "--codex was renamed: pass the target as a word, e.g. 'sync codex'",
186
+ "--agents": "--agents was renamed: pass the target as a word, e.g. 'sync agents'"
187
+ }
188
+
189
+ const flagNameOf = (token: string): string => {
190
+ const equals = token.indexOf("=")
191
+ return equals === -1 ? token : token.slice(0, equals)
192
+ }
193
+
194
+ const canonicalFlagName = (
195
+ name: string,
196
+ surface: FlagSurface | undefined
197
+ ): string | undefined => {
198
+ if (surface === undefined) return undefined
199
+ const aliased = surface.aliases.get(name)
200
+ if (aliased !== undefined) return aliased
201
+ return surface.longFlags.includes(name) ? name : undefined
202
+ }
203
+
204
+ const declaredFlagName = (
205
+ name: string,
206
+ commandSurface: FlagSurface | undefined
207
+ ): string | undefined =>
208
+ canonicalFlagName(name, commandSurface) ?? canonicalFlagName(name, GLOBAL_SURFACE)
209
+
210
+ const declaredInAnySurface = (name: string): boolean => {
211
+ if (canonicalFlagName(name, GLOBAL_SURFACE) !== undefined) return true
212
+ return COMMANDS.some(
213
+ (command) => canonicalFlagName(name, COMMAND_SURFACES.get(command.name)) !== undefined
214
+ )
215
+ }
216
+
217
+ const takesValueInAnySurface = (name: string): boolean => {
218
+ const globalName = canonicalFlagName(name, GLOBAL_SURFACE)
219
+ if (globalName !== undefined && GLOBAL_SURFACE.valueFlags.includes(globalName)) return true
220
+ return COMMANDS.some((command) => {
221
+ const surface = COMMAND_SURFACES.get(command.name)
222
+ if (surface === undefined) return false
223
+ const canonicalName = canonicalFlagName(name, surface)
224
+ return canonicalName !== undefined && surface.valueFlags.includes(canonicalName)
225
+ })
226
+ }
227
+
228
+ /** The resolved subcommand word, or undefined at the root. */
229
+ export const subcommandName = (args: ReadonlyArray<string>): string | undefined => {
230
+ for (let index = 0; index < args.length; index++) {
231
+ const token = args[index]
232
+ if (token === "--") return undefined
233
+ const name = flagNameOf(token)
234
+ if (token === name && takesValueInAnySurface(name)) {
235
+ const next = args[index + 1]
236
+ if (next === "--") return undefined
237
+ if (
238
+ next !== undefined &&
239
+ !(next.startsWith("-") && declaredInAnySurface(flagNameOf(next)))
240
+ ) {
241
+ index++
242
+ continue
243
+ }
244
+ }
245
+ if (!token.startsWith("-")) return token
246
+ }
247
+ return undefined
248
+ }
249
+
250
+ interface ScannedFlag {
251
+ readonly token: string
252
+ readonly name: string
253
+ readonly canonicalName: string | undefined
254
+ readonly hasValue: boolean
255
+ readonly takesValue: boolean
256
+ }
257
+
258
+ interface ArgvNormalization {
259
+ readonly flagIndex: number
260
+ readonly valueIndex: number
261
+ readonly token: string
262
+ }
263
+
264
+ interface ScannedArgv {
265
+ readonly flags: ReadonlyArray<ScannedFlag>
266
+ readonly normalizations: ReadonlyArray<ArgvNormalization>
267
+ }
268
+
269
+ const scanArgv = (
270
+ args: ReadonlyArray<string>,
271
+ commandSurface: FlagSurface | undefined
272
+ ): ScannedArgv => {
273
+ const flags: Array<ScannedFlag> = []
274
+ const normalizations: Array<ArgvNormalization> = []
275
+
276
+ for (let index = 0; index < args.length; index++) {
277
+ const token = args[index]
278
+ if (token === "--") break
279
+ if (!token.startsWith("-")) continue
280
+
281
+ const name = flagNameOf(token)
282
+ const canonicalName = declaredFlagName(name, commandSurface)
283
+ let hasValue = token.includes("=")
284
+ const takesValue =
285
+ canonicalName !== undefined &&
286
+ (commandSurface?.valueFlags.includes(canonicalName) === true ||
287
+ GLOBAL_SURFACE.valueFlags.includes(canonicalName))
288
+
289
+ if (!hasValue && takesValue) {
290
+ const next = args[index + 1]
291
+ const nextIsRecognizedFlag =
292
+ next !== undefined &&
293
+ next.startsWith("-") &&
294
+ declaredFlagName(flagNameOf(next), commandSurface) !== undefined
295
+ // A legitimate value may begin with `-`; only a recognized flag proves it is missing.
296
+ if (next !== undefined && next !== "--" && !nextIsRecognizedFlag) {
297
+ hasValue = true
298
+ if (next.startsWith("-")) {
299
+ // Effect 4's lexer treats any `-`-leading token as an option, so a legitimate
300
+ // dash-leading value only survives in the inline form.
301
+ normalizations.push({
302
+ flagIndex: index,
303
+ valueIndex: index + 1,
304
+ token: `${token}=${next}`
305
+ })
306
+ }
307
+ index++
308
+ }
309
+ }
310
+
311
+ flags.push({ token, name, canonicalName, hasValue, takesValue })
312
+ }
313
+
314
+ return { flags, normalizations }
315
+ }
316
+
317
+ const normalizeArgv = (
318
+ args: ReadonlyArray<string>,
319
+ normalizations: ReadonlyArray<ArgvNormalization>
320
+ ): ReadonlyArray<string> => {
321
+ if (normalizations.length === 0) return args
322
+
323
+ const normalized: Array<string> = []
324
+ let normalizationIndex = 0
325
+ for (let index = 0; index < args.length; index++) {
326
+ const normalization = normalizations[normalizationIndex]
327
+ if (normalization !== undefined && normalization.flagIndex === index) {
328
+ normalized.push(normalization.token)
329
+ index = normalization.valueIndex
330
+ normalizationIndex++
331
+ continue
332
+ }
333
+ normalized.push(args[index])
334
+ }
335
+ return normalized
336
+ }
337
+
338
+ const reject = (message: string): ArgvOutcome => ({
339
+ kind: "reject",
340
+ message,
341
+ exitCode: 2
342
+ })
343
+
344
+ const modelCatalogHint = (tool: Tool): string => {
345
+ const catalog = modelCatalog(tool)
346
+ const list = catalog.models
347
+ .map((model) => ` ${model.id}${model.note !== undefined ? ` — ${model.note}` : ""}`)
348
+ .join("\n")
349
+ return `Available ${tool} models (kit-verified ${catalog.verified} — SoT/models.json):\n${list}`
350
+ }
351
+
352
+ const missingModifierValue = (flag: string): string | undefined => {
353
+ switch (flag) {
354
+ case "--claude-model":
355
+ case "--codex-model": {
356
+ const tool: Tool = flag === "--claude-model" ? "claude" : "codex"
357
+ return `${modelCatalogHint(tool)}\n${flag} requires a value: ${flag}=<model>`
358
+ }
359
+ case "--claude-effort":
360
+ case "--codex-effort": {
361
+ const tool: Tool = flag === "--claude-effort" ? "claude" : "codex"
362
+ return `${effortCatalog(tool)}\n${flag} requires a value: ${effortFlagGrammar(tool)}`
363
+ }
364
+ case "--claude-advisor":
365
+ return `${advisorCatalog()}\n${flag} requires a value: ${advisorFlagGrammar()}`
366
+ case "--claude-compact-window":
367
+ return `${flag} requires a value: ${flag}=<tokens> (e.g. 680k)`
368
+ case "--claude-plugin":
369
+ return `${flag} requires a value: ${flag}=<${KNOWN_CLAUDE_OPTIN_PLUGINS.join("|")}>`
370
+ default:
371
+ return undefined
372
+ }
373
+ }
374
+
375
+ /** Validate the argument list, then hand back the arguments Effect 4 should parse. */
376
+ export const prepareArgv = (args: ReadonlyArray<string>): ArgvOutcome => {
377
+ const subcommand = subcommandName(args)
378
+ const commandSurface = subcommand === undefined ? undefined : COMMAND_SURFACES.get(subcommand)
379
+ const { flags, normalizations } = scanArgv(args, commandSurface)
380
+ const unknownCommandWouldMisdiagnoseFlag =
381
+ subcommand !== undefined &&
382
+ commandSurface === undefined &&
383
+ flags.some((flag) => flag.canonicalName === undefined)
384
+ if (unknownCommandWouldMisdiagnoseFlag) {
385
+ return reject(`unknown command '${subcommand}'`)
386
+ }
387
+
388
+ if (subcommand === "sync") {
389
+ for (const flag of flags) {
390
+ const hint = LEGACY_HINTS[flag.name]
391
+ if (hint !== undefined) return reject(hint)
392
+ }
393
+ }
394
+
395
+ // Effect 4 expands clustered shorts, but the kit declares no clusterable alias
396
+ // pair, so refusing a cluster as one unknown flag is the conservative direction.
397
+ for (const flag of flags) {
398
+ if (flag.canonicalName !== undefined) continue
399
+ const scope = subcommand === undefined ? "" : ` for '${subcommand}'`
400
+ return reject(`unknown flag ${flag.token}${scope}`)
401
+ }
402
+
403
+ for (const flag of flags) {
404
+ if (!flag.token.includes("=") || flag.takesValue) continue
405
+ // These booleans are presence-based; Effect 4 would otherwise let
406
+ // `--dry-run=false` read as a dry run while performing a real mutating sync.
407
+ return reject(`flag ${flag.name} does not take a value`)
408
+ }
409
+
410
+ const seen = new Set<string>()
411
+ for (const flag of flags) {
412
+ const name = flag.canonicalName
413
+ if (name === undefined) continue
414
+ const duplicateAllowed =
415
+ commandSurface?.repeatableFlags.includes(name) === true ||
416
+ GLOBAL_SURFACE.repeatableFlags.includes(name) ||
417
+ GLOBAL_SURFACE.actionFlags.has(name)
418
+ if (!duplicateAllowed && seen.has(name)) {
419
+ return reject(`flag ${name} was given more than once`)
420
+ }
421
+ seen.add(name)
422
+ }
423
+
424
+ if (subcommand === "sync") {
425
+ for (const flag of flags) {
426
+ if (flag.hasValue) continue
427
+ const message = missingModifierValue(flag.name)
428
+ if (message !== undefined) return reject(message)
429
+ }
430
+ }
431
+
432
+ return { kind: "accept", args: normalizeArgv(args, normalizations) }
433
+ }
@@ -1,4 +1,4 @@
1
- import { Args, Command, Options } from "@effect/cli"
1
+ import { Argument, Command, Flag } from "effect/unstable/cli"
2
2
  import { Console, Effect, Option } from "effect"
3
3
  import { bail } from "../engine"
4
4
  import overview from "../../docs/overview.md" with { type: "text" }
@@ -23,12 +23,12 @@ const TOPICS: Record<string, { summary: string; body: string }> = {
23
23
  "platforms": { summary: "Platform support: Linux/macOS x64 and arm64", body: platforms }
24
24
  }
25
25
 
26
- const topic = Args.text({ name: "topic" }).pipe(
27
- Args.withDescription(`One of: ${Object.keys(TOPICS).join(", ")}`),
28
- Args.optional
26
+ const topic = Argument.string("topic").pipe(
27
+ Argument.withDescription(`One of: ${Object.keys(TOPICS).join(", ")}`),
28
+ Argument.optional
29
29
  )
30
- const json = Options.boolean("json").pipe(
31
- Options.withDescription("List topics as JSON")
30
+ const json = Flag.boolean("json").pipe(
31
+ Flag.withDescription("List topics as JSON")
32
32
  )
33
33
 
34
34
  export const docsCommand = Command.make("docs", { topic, json }, (config) =>
@@ -1,21 +1,21 @@
1
- import { Args, Command, Options, Prompt } from "@effect/cli"
1
+ import { Argument, Command, Flag, Prompt } from "effect/unstable/cli"
2
2
  import { Effect, Option } from "effect"
3
3
  import { bail, engine } from "../engine"
4
4
  import { modelCatalog, type Tool } from "../manifests"
5
5
 
6
- const tool = Args.text({ name: "tool" }).pipe(
7
- Args.withDescription("Which tool: claude | codex")
6
+ const tool = Argument.string("tool").pipe(
7
+ Argument.withDescription("Which tool: claude | codex")
8
8
  )
9
- const value = Args.text({ name: "value" }).pipe(
10
- Args.withDescription("Model to set (omit to view current + pick interactively on a TTY)"),
11
- Args.optional
9
+ const value = Argument.string("value").pipe(
10
+ Argument.withDescription("Model to set (omit to view current + pick interactively on a TTY)"),
11
+ Argument.optional
12
12
  )
13
- const dryRun = Options.boolean("dry-run").pipe(
14
- Options.withDescription("Preview without applying")
13
+ const dryRun = Flag.boolean("dry-run").pipe(
14
+ Flag.withDescription("Preview without applying")
15
15
  )
16
- const verbose = Options.boolean("verbose").pipe(
17
- Options.withAlias("v"),
18
- Options.withDescription("Also print no-op confirmations (already in sync, up to date)")
16
+ const verbose = Flag.boolean("verbose").pipe(
17
+ Flag.withAlias("v"),
18
+ Flag.withDescription("Also print no-op confirmations (already in sync, up to date)")
19
19
  )
20
20
 
21
21
  const KEEP = "__keep__"
@@ -1,14 +1,14 @@
1
- import { Args, Command, Options } from "@effect/cli"
1
+ import { Argument, Command, Flag } from "effect/unstable/cli"
2
2
  import { Console, Effect, Option } from "effect"
3
3
  import { bail } from "../engine"
4
4
  import { modelCatalog, type Tool } from "../manifests"
5
5
 
6
- const tool = Args.text({ name: "tool" }).pipe(
7
- Args.withDescription("claude | codex (omit for both tool catalogs)"),
8
- Args.optional
6
+ const tool = Argument.string("tool").pipe(
7
+ Argument.withDescription("claude | codex (omit for both tool catalogs)"),
8
+ Argument.optional
9
9
  )
10
- const json = Options.boolean("json").pipe(
11
- Options.withDescription("Machine-readable output")
10
+ const json = Flag.boolean("json").pipe(
11
+ Flag.withDescription("Machine-readable output")
12
12
  )
13
13
 
14
14
  const renderTool = (t: Tool) =>
@@ -1,14 +1,14 @@
1
- import { Args, Command, Options } from "@effect/cli"
1
+ import { Argument, Command, Flag } from "effect/unstable/cli"
2
2
  import { Console, Effect, Option } from "effect"
3
3
  import { bail } from "../engine"
4
4
  import { pluginsView } from "../manifests"
5
5
 
6
- const action = Args.text({ name: "action" }).pipe(
7
- Args.withDescription("list (default)"),
8
- Args.optional
6
+ const action = Argument.string("action").pipe(
7
+ Argument.withDescription("list (default)"),
8
+ Argument.optional
9
9
  )
10
- const json = Options.boolean("json").pipe(
11
- Options.withDescription("Machine-readable output")
10
+ const json = Flag.boolean("json").pipe(
11
+ Flag.withDescription("Machine-readable output")
12
12
  )
13
13
 
14
14
  export const pluginsCommand = Command.make("plugins", { action, json }, (config) =>
@@ -1,14 +1,14 @@
1
- import { Args, Command, Options } from "@effect/cli"
1
+ import { Argument, Command, Flag } from "effect/unstable/cli"
2
2
  import { Console, Effect, Option } from "effect"
3
3
  import { bail } from "../engine"
4
4
  import { skillsView } from "../manifests"
5
5
 
6
- const action = Args.text({ name: "action" }).pipe(
7
- Args.withDescription("list (default)"),
8
- Args.optional
6
+ const action = Argument.string("action").pipe(
7
+ Argument.withDescription("list (default)"),
8
+ Argument.optional
9
9
  )
10
- const json = Options.boolean("json").pipe(
11
- Options.withDescription("Machine-readable output")
10
+ const json = Flag.boolean("json").pipe(
11
+ Flag.withDescription("Machine-readable output")
12
12
  )
13
13
 
14
14
  export const skillsCommand = Command.make("skills", { action, json }, (config) =>
@@ -1,4 +1,4 @@
1
- import { Command, Options } from "@effect/cli"
1
+ import { Command, Flag } from "effect/unstable/cli"
2
2
  import { Console, Effect } from "effect"
3
3
  import { engineCapture } from "../engine"
4
4
  import {
@@ -11,8 +11,8 @@ import {
11
11
  } from "../manifests"
12
12
  import { kitHome } from "../kitHome"
13
13
 
14
- const json = Options.boolean("json").pipe(
15
- Options.withDescription("Machine-readable output")
14
+ const json = Flag.boolean("json").pipe(
15
+ Flag.withDescription("Machine-readable output")
16
16
  )
17
17
 
18
18
  interface Drift {
@@ -1,18 +1,11 @@
1
- import { Args, Command, Options } from "@effect/cli"
1
+ import { Argument, Command, Flag } from "effect/unstable/cli"
2
2
  import { Effect, Option } from "effect"
3
3
  import { spawnSync } from "node:child_process"
4
4
  import { existsSync } from "node:fs"
5
5
  import { join } from "node:path"
6
6
  import { bail, engine } from "../engine"
7
7
  import type { Logger } from "../engine-native/logger"
8
- import {
9
- advisorCatalog,
10
- advisorFlagGrammar,
11
- effortCatalog,
12
- effortFlagGrammar
13
- } from "../efforts"
14
8
  import { kitHome } from "../kitHome"
15
- import { modelCatalog, type Tool } from "../manifests"
16
9
  import { LoggerService } from "../services"
17
10
 
18
11
  /** Best-effort update autodetection: nudge (never block, never fail) when
@@ -38,90 +31,66 @@ const updateNudge = (logger: Logger): void => {
38
31
 
39
32
  const VALID_TARGETS = ["claude", "codex", "agents"]
40
33
 
41
- // Renamed pre-CLI flags: @effect/cli routes unknown flags into the excess
42
- // positional args, so the rename hints are mirrored here before EngineNative
43
- // sees the normalized argv.
44
- const LEGACY_HINTS: Record<string, string> = {
45
- "--force": "--force was renamed to --reconcile",
46
- "--remove-plugins":
47
- "--remove-plugins was renamed to --prune (it also removes marketplaces + kit-managed skills)",
48
- "--680k": "--680k was renamed to --claude-compact-window=680k",
49
- "--permissive": "--permissive was renamed to --claude-permissive",
50
- "--supabase": "--supabase was renamed to --claude-plugin=supabase",
51
- "--n8n": "--n8n was renamed to --claude-plugin=n8n",
52
- "--skip-rtk": "--skip-rtk was renamed to --skip-bubblewrap",
53
- "--claude": "--claude was renamed: pass the target as a word, e.g. 'sync claude'",
54
- "--codex": "--codex was renamed: pass the target as a word, e.g. 'sync codex'",
55
- "--agents": "--agents was renamed: pass the target as a word, e.g. 'sync agents'"
56
- }
57
-
58
- const modelCatalogHint = (t: Tool): string => {
59
- const c = modelCatalog(t)
60
- const list = c.models
61
- .map((m) => ` ${m.id}${m.note !== undefined ? ` — ${m.note}` : ""}`)
62
- .join("\n")
63
- return `Available ${t} models (kit-verified ${c.verified} — SoT/models.json):\n${list}`
64
- }
65
-
66
- const targets = Args.text({ name: "target" }).pipe(
67
- Args.withDescription("Sync targets: claude, codex, agents (default: all three)"),
68
- Args.repeated
34
+ const targets = Argument.variadic(
35
+ Argument.string("target").pipe(
36
+ Argument.withDescription("Sync targets: claude, codex, agents (default: all three)")
37
+ )
69
38
  )
70
39
 
71
- const dryRun = Options.boolean("dry-run").pipe(
72
- Options.withDescription("Preview without applying")
40
+ const dryRun = Flag.boolean("dry-run").pipe(
41
+ Flag.withDescription("Preview without applying")
73
42
  )
74
- const reconcile = Options.boolean("reconcile").pipe(
75
- Options.withDescription("Reconcile kit-owned settings with SoT (SoT keys win; user-only keys preserved; permissions arrays replaced)")
43
+ const reconcile = Flag.boolean("reconcile").pipe(
44
+ Flag.withDescription("Reconcile kit-owned settings with SoT (SoT keys win; user-only keys preserved; permissions arrays replaced)")
76
45
  )
77
- const prune = Options.boolean("prune").pipe(
78
- Options.withDescription("Uninstall kit-managed installs not in SoT (plugins, marketplaces, universal skills)")
46
+ const prune = Flag.boolean("prune").pipe(
47
+ Flag.withDescription("Uninstall kit-managed installs not in SoT (plugins, marketplaces, universal skills)")
79
48
  )
80
- const skipBubblewrap = Options.boolean("skip-bubblewrap").pipe(
81
- Options.withDescription("Skip optional bubblewrap bootstrap (Codex Linux sandbox)")
49
+ const skipBubblewrap = Flag.boolean("skip-bubblewrap").pipe(
50
+ Flag.withDescription("Skip optional bubblewrap bootstrap (Codex Linux sandbox)")
82
51
  )
83
- const skipPluginRefresh = Options.boolean("skip-plugin-refresh").pipe(
84
- Options.withDescription("Install missing plugins but skip refresh-only updates for existing plugins")
52
+ const skipPluginRefresh = Flag.boolean("skip-plugin-refresh").pipe(
53
+ Flag.withDescription("Install missing plugins but skip refresh-only updates for existing plugins")
85
54
  )
86
- const yes = Options.boolean("yes").pipe(
87
- Options.withDescription("Auto-accept toolchain prompts (containers/CI)")
55
+ const yes = Flag.boolean("yes").pipe(
56
+ Flag.withDescription("Auto-accept toolchain prompts (containers/CI)")
88
57
  )
89
- const verbose = Options.boolean("verbose").pipe(
90
- Options.withAlias("v"),
91
- Options.withDescription("Also print no-op confirmations (already in sync, up to date, left as-is)")
58
+ const verbose = Flag.boolean("verbose").pipe(
59
+ Flag.withAlias("v"),
60
+ Flag.withDescription("Also print no-op confirmations (already in sync, up to date, left as-is)")
92
61
  )
93
- const claudeModel = Options.text("claude-model").pipe(
94
- Options.withDescription("Deploy-time modifier: set deployed Claude model (see `docks-kit models claude`)"),
95
- Options.optional
62
+ const claudeModel = Flag.string("claude-model").pipe(
63
+ Flag.withDescription("Deploy-time modifier: set deployed Claude model (see `docks-kit models claude`)"),
64
+ Flag.optional
96
65
  )
97
- const claudeEffort = Options.text("claude-effort").pipe(
98
- Options.withDescription("Deploy-time modifier: set Claude effortLevel (bare flag shows valid levels)"),
99
- Options.optional
66
+ const claudeEffort = Flag.string("claude-effort").pipe(
67
+ Flag.withDescription("Deploy-time modifier: set Claude effortLevel (bare flag shows valid levels)"),
68
+ Flag.optional
100
69
  )
101
- const claudeAdvisor = Options.text("claude-advisor").pipe(
102
- Options.withDescription("Deploy-time modifier: set Claude advisor on/off/default"),
103
- Options.optional
70
+ const claudeAdvisor = Flag.string("claude-advisor").pipe(
71
+ Flag.withDescription("Deploy-time modifier: set Claude advisor on/off/default"),
72
+ Flag.optional
104
73
  )
105
- const claudeCompactWindow = Options.text("claude-compact-window").pipe(
106
- Options.withDescription("Deploy-time modifier: set deployed autocompact window in tokens (e.g. 680000 or 680k)"),
107
- Options.optional
74
+ const claudeCompactWindow = Flag.string("claude-compact-window").pipe(
75
+ Flag.withDescription("Deploy-time modifier: set deployed autocompact window in tokens (e.g. 680000 or 680k)"),
76
+ Flag.optional
108
77
  )
109
- const claudePermissive = Options.boolean("claude-permissive").pipe(
110
- Options.withDescription("Deploy-time modifier: empty permissions.ask/deny in deployed settings (sandboxes)")
78
+ const claudePermissive = Flag.boolean("claude-permissive").pipe(
79
+ Flag.withDescription("Deploy-time modifier: empty permissions.ask/deny in deployed settings (sandboxes)")
111
80
  )
112
- const claudePlugin = Options.text("claude-plugin").pipe(
113
- Options.withDescription(
81
+ const claudePlugin = Flag.string("claude-plugin").pipe(
82
+ Flag.withDescription(
114
83
  "Sticky opt-in plugin(s); repeatable and/or comma-separated (known: supabase, n8n)"
115
84
  ),
116
- Options.repeated
85
+ Flag.atLeast(0)
117
86
  )
118
- const codexModel = Options.text("codex-model").pipe(
119
- Options.withDescription("Deploy-time modifier: set deployed Codex model (see `docks-kit models codex`)"),
120
- Options.optional
87
+ const codexModel = Flag.string("codex-model").pipe(
88
+ Flag.withDescription("Deploy-time modifier: set deployed Codex model (see `docks-kit models codex`)"),
89
+ Flag.optional
121
90
  )
122
- const codexEffort = Options.text("codex-effort").pipe(
123
- Options.withDescription("Deploy-time modifier: set Codex model_reasoning_effort (bare flag shows valid levels)"),
124
- Options.optional
91
+ const codexEffort = Flag.string("codex-effort").pipe(
92
+ Flag.withDescription("Deploy-time modifier: set Codex model_reasoning_effort (bare flag shows valid levels)"),
93
+ Flag.optional
125
94
  )
126
95
 
127
96
  export const syncCommand = Command.make(
@@ -146,24 +115,6 @@ export const syncCommand = Command.make(
146
115
  },
147
116
  (config) =>
148
117
  Effect.gen(function* () {
149
- for (const t of config.targets) {
150
- if (VALID_TARGETS.includes(t)) continue
151
- if (t === "--claude-model" || t === "--codex-model") {
152
- const tool: Tool = t === "--claude-model" ? "claude" : "codex"
153
- return yield* bail(`${modelCatalogHint(tool)}\n${t} requires a value: ${t}=<model>`)
154
- }
155
- if (t === "--claude-effort" || t === "--codex-effort") {
156
- const tool: Tool = t === "--claude-effort" ? "claude" : "codex"
157
- return yield* bail(`${effortCatalog(tool)}\n${t} requires a value: ${effortFlagGrammar(tool)}`)
158
- }
159
- if (t === "--claude-advisor") {
160
- return yield* bail(`${advisorCatalog()}\n${t} requires a value: ${advisorFlagGrammar()}`)
161
- }
162
- const hint = LEGACY_HINTS[t]
163
- if (hint !== undefined) {
164
- return yield* bail(hint)
165
- }
166
- }
167
118
  const bad = config.targets.filter((t) => !VALID_TARGETS.includes(t))
168
119
  if (bad.length > 0) {
169
120
  return yield* bail(
@@ -1,23 +1,23 @@
1
- import { Args, Command, Options } from "@effect/cli"
1
+ 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
5
  const MANAGED = ["bun", "effect-solutions"]
6
6
 
7
- const op = Args.text({ name: "op" }).pipe(
8
- Args.withDescription("check (default) | ensure <tool>"),
9
- Args.optional
7
+ const op = Argument.string("op").pipe(
8
+ Argument.withDescription("check (default) | ensure <tool>"),
9
+ Argument.optional
10
10
  )
11
- const tool = Args.text({ name: "tool" }).pipe(
12
- Args.withDescription(`Managed tool for ensure: ${MANAGED.join(", ")}`),
13
- Args.optional
11
+ const tool = Argument.string("tool").pipe(
12
+ Argument.withDescription(`Managed tool for ensure: ${MANAGED.join(", ")}`),
13
+ Argument.optional
14
14
  )
15
- const yes = Options.boolean("yes").pipe(
16
- Options.withDescription("Auto-accept above-verified installs")
15
+ const yes = Flag.boolean("yes").pipe(
16
+ Flag.withDescription("Auto-accept above-verified installs")
17
17
  )
18
- const verbose = Options.boolean("verbose").pipe(
19
- Options.withAlias("v"),
20
- Options.withDescription("Also print no-op confirmations (present, up to date)")
18
+ const verbose = Flag.boolean("verbose").pipe(
19
+ Flag.withAlias("v"),
20
+ Flag.withDescription("Also print no-op confirmations (present, up to date)")
21
21
  )
22
22
 
23
23
  export const toolchainCommand = Command.make("toolchain", { op, tool, yes, verbose }, (config) =>
@@ -1,4 +1,4 @@
1
- import { Command, Options } from "@effect/cli"
1
+ import { Command, Flag } from "effect/unstable/cli"
2
2
  import { Console, Effect } from "effect"
3
3
  import { spawnSync } from "node:child_process"
4
4
  import { existsSync, readFileSync } from "node:fs"
@@ -6,8 +6,8 @@ import { join } from "node:path"
6
6
  import { bail, compiled } from "../engine"
7
7
  import { kitHome } from "../kitHome"
8
8
 
9
- const noSync = Options.boolean("no-sync").pipe(
10
- Options.withDescription("Update the kit only; skip the chained flag-less sync")
9
+ const noSync = Flag.boolean("no-sync").pipe(
10
+ Flag.withDescription("Update the kit only; skip the chained flag-less sync")
11
11
  )
12
12
 
13
13
  const git = (home: string, args: Array<string>): { ok: boolean; out: string } => {
@@ -23,7 +23,7 @@ export class ExitError extends Error {
23
23
  }
24
24
  }
25
25
 
26
- const KNOWN_CLAUDE_OPTIN_PLUGINS = ["supabase", "n8n"]
26
+ export const KNOWN_CLAUDE_OPTIN_PLUGINS = ["supabase", "n8n"]
27
27
  const MODIFIER_FLAGS = new Set<ModifierFlag>([
28
28
  "--claude-model",
29
29
  "--claude-effort",
package/cli/src/engine.ts CHANGED
@@ -7,7 +7,7 @@ import { DependencyManagerService, LoggerService, PlatformService } from "./serv
7
7
 
8
8
  /**
9
9
  * The single seam between the typed CLI and EngineNative. Engine execution
10
- * stays in-process after @effect/cli has parsed pickers and flag spellings.
10
+ * stays in-process after effect/unstable/cli has parsed pickers and flag spellings.
11
11
  */
12
12
  const bashRemovedMessage = "bash engine removed — recover at tag bash-engine-final"
13
13
  const bashEngineRequested = (): boolean => process.env["DOCKS_KIT_ENGINE"] === "bash"
@@ -1,7 +1,7 @@
1
1
  // Generated by cli/scripts/generate-sot-payload.ts. DO NOT EDIT.
2
2
  // Edit SoT/, notification.mp3, or package.json, then run: bun cli/scripts/generate-sot-payload.ts
3
3
 
4
- export const GENERATED_PACKAGE_VERSION = "0.14.4"
4
+ export const GENERATED_PACKAGE_VERSION = "0.15.0"
5
5
 
6
6
  export const GENERATED_PAYLOAD_TEXT = {
7
7
  "SoT/.agents/skills.txt": "# Universal AI-agent skill manifest intentionally empty.\n# Global skill discovery is opt-in: add one <owner>/<repo> slug per line.\n# EngineNative ignores comments and blank lines.\n",
package/cli/src/main.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env bun
2
- import { Command } from "@effect/cli"
3
- import { BunContext, BunRuntime } from "@effect/platform-bun"
2
+ import { Command, CliOutput } from "effect/unstable/cli"
3
+ import { BunRuntime, BunServices } from "@effect/platform-bun"
4
4
  import { Console, Effect, Layer } from "effect"
5
- import { engine } from "./engine"
6
5
  import { EngineServicesLive } from "./services"
7
6
  import { docsCommand } from "./commands/docs"
8
7
  import { modelCommand } from "./commands/model"
@@ -14,6 +13,7 @@ import { syncCommand } from "./commands/sync"
14
13
  import { toolchainCommand } from "./commands/toolchain"
15
14
  import { updateCommand } from "./commands/update"
16
15
  import { GENERATED_PACKAGE_VERSION } from "./generated/sotPayload"
16
+ import { prepareArgv } from "./argv"
17
17
 
18
18
 
19
19
  const root = Command.make("docks-kit", {}, () =>
@@ -50,8 +50,16 @@ const root = Command.make("docks-kit", {}, () =>
50
50
  ])
51
51
  )
52
52
 
53
+ // v4's default formatter renders `name vversion`, but the `docks-kit` launcher
54
+ // compares `--version` against the bare `package.json` version. The trailing
55
+ // newline reproduces the characterized byte-for-byte output of the v3 CLI.
56
+ const bareVersionFormatter: CliOutput.Formatter = {
57
+ ...CliOutput.defaultFormatter(),
58
+ formatVersion: (_name, version) => `${version}\n`
59
+ }
60
+
53
61
  // Harness-private raw channel:
54
- // `DOCKS_KIT_ENGINE=native-raw` bypasses @effect/cli and hands the raw engine
62
+ // `DOCKS_KIT_ENGINE=native-raw` bypasses effect/unstable/cli and hands the raw engine
55
63
  // argv to EngineNative so golden tests drive the internal vocabulary directly.
56
64
  // PUBLIC engine execution lives at the engine.ts seam after the CLI has
57
65
  // parsed/normalized pickers, --flag value forms, and non-engine commands.
@@ -60,27 +68,18 @@ if (process.env["DOCKS_KIT_ENGINE"] === "native-raw") {
60
68
  process.exit(await runEngineNative(process.argv.slice(2)))
61
69
  }
62
70
 
63
- const cli = Command.run(root, {
64
- name: "docks-kit",
65
- version: GENERATED_PACKAGE_VERSION
66
- })
67
-
68
- // Normalize the repeatable plugin's documented equals form and exact empty
69
- // text-option assignments that @effect/cli otherwise routes into positional
70
- // targets. EngineNative owns the resulting shared empty-value validation.
71
- const emptyTextOptions = new Set([
72
- "--claude-model=",
73
- "--claude-effort=",
74
- "--claude-advisor=",
75
- "--codex-model=",
76
- "--codex-effort=",
77
- ])
78
- const argv = process.argv.flatMap((a, index, all) => {
79
- if (a.startsWith("--claude-plugin=")) {
80
- return ["--claude-plugin", a.slice("--claude-plugin=".length)]
81
- }
82
- if (emptyTextOptions.has(a)) return [a.slice(0, -1), ""]
83
- return [a]
84
- })
71
+ // Validate and normalize before parsing because the kit refuses to guess at unrecognized
72
+ // or duplicated flags, and Effect 4 would otherwise negate `--no-<flag>` into a real
73
+ // mutating run. This seam owns argument normalization.
74
+ const prepared = prepareArgv(process.argv.slice(2))
75
+ if (prepared.kind === "reject") {
76
+ process.stderr.write(`${prepared.message}\n`)
77
+ process.exit(prepared.exitCode)
78
+ }
85
79
 
86
- cli(argv).pipe(Effect.provide(Layer.mergeAll(BunContext.layer, EngineServicesLive)), BunRuntime.runMain)
80
+ Command.runWith(root, { version: GENERATED_PACKAGE_VERSION })(prepared.args).pipe(
81
+ Effect.provide(
82
+ Layer.mergeAll(BunServices.layer, EngineServicesLive, CliOutput.layer(bareVersionFormatter))
83
+ ),
84
+ BunRuntime.runMain
85
+ )
@@ -8,14 +8,14 @@ import { Context, Layer } from "effect"
8
8
  import { makeLogger, type Logger, type LoggerSinks } from "./engine-native/logger"
9
9
  import { makeEngineServices, makePlatform, type DependencyManager, type Platform } from "./engine-native/services"
10
10
 
11
- export class LoggerService extends Context.Tag("docks-kit/Logger")<LoggerService, Logger>() {}
11
+ export class LoggerService extends Context.Service<LoggerService, Logger>()("docks-kit/Logger") {}
12
12
 
13
- export class DependencyManagerService extends Context.Tag("docks-kit/DependencyManager")<
13
+ export class DependencyManagerService extends Context.Service<
14
14
  DependencyManagerService,
15
15
  DependencyManager
16
- >() {}
16
+ >()("docks-kit/DependencyManager") {}
17
17
 
18
- export class PlatformService extends Context.Tag("docks-kit/Platform")<PlatformService, Platform>() {}
18
+ export class PlatformService extends Context.Service<PlatformService, Platform>()("docks-kit/Platform") {}
19
19
 
20
20
  const live = makeEngineServices()
21
21
 
package/docks-kit CHANGED
@@ -69,7 +69,7 @@ fi
69
69
 
70
70
  # Sentinel is a real dependency dir, not bare node_modules/ — a failed or
71
71
  # partial install leaves node_modules/ present and would suppress the repair.
72
- if [[ ! -d "$REPO_DIR/node_modules/@effect/cli" ]]; then
72
+ if [[ ! -d "$REPO_DIR/node_modules/effect" ]]; then
73
73
  echo "[docks-kit] Installing CLI dependencies (bun install --frozen-lockfile)..." >&2
74
74
  (cd "$REPO_DIR" && "$BUN" install --frozen-lockfile >/dev/null)
75
75
  fi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docks-kit",
3
- "version": "0.14.4",
3
+ "version": "0.15.0",
4
4
  "description": "Portable AI coding agent config kit — SoT sync engine + typed CLI for Claude Code, Codex, and universal agent skills",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,15 +35,13 @@
35
35
  "test:ci": "bun run check:generated && bun run typecheck && bun run test:unit && bun run test:runtime:posix && bun run golden:dryrun && bun run golden:mutation"
36
36
  },
37
37
  "dependencies": {
38
- "@effect/cli": "0.75.2",
39
- "@effect/platform": "0.96.1",
40
- "@effect/platform-bun": "0.90.0",
41
- "effect": "3.21.4"
38
+ "@effect/platform-bun": "4.0.0-beta.107",
39
+ "effect": "4.0.0-beta.107"
42
40
  },
43
41
  "devDependencies": {
44
- "@effect/vitest": "0.29.0",
42
+ "@effect/vitest": "4.0.0-beta.107",
45
43
  "@types/bun": "^1.3.0",
46
44
  "typescript": "7.0.2",
47
- "vitest": "3.2.7"
45
+ "vitest": "4.1.10"
48
46
  }
49
47
  }