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
|
@@ -17,12 +17,15 @@ import { claudeNextSteps, claudeSummary, claudeSync, type ClaudeRuntimeState } f
|
|
|
17
17
|
import { codexNextSteps, codexSummary, codexSync } from "./codexSync"
|
|
18
18
|
import { normalizeManifest, skillsNextSteps, skillsSummary, skillsSync, type SkillsState } from "./skillsSync"
|
|
19
19
|
import { modeModel, modeToolchain } from "./modes"
|
|
20
|
-
import { ExitError, parseArgs, validateModifierFlags } from "./parseArgs"
|
|
20
|
+
import { ExitError, parseArgs, parseClaudePlugin, parseCompactWindow, validateModifierFlags } from "./parseArgs"
|
|
21
21
|
|
|
22
22
|
export type ModifierFlag =
|
|
23
23
|
| "--claude-model"
|
|
24
24
|
| "--claude-effort"
|
|
25
25
|
| "--claude-advisor"
|
|
26
|
+
| "--claude-compact-window"
|
|
27
|
+
| "--claude-permissive"
|
|
28
|
+
| "--claude-plugin"
|
|
26
29
|
| "--codex-model"
|
|
27
30
|
| "--codex-effort"
|
|
28
31
|
|
|
@@ -117,6 +120,16 @@ export interface Ctx {
|
|
|
117
120
|
function makeCtx(services: EngineServices): Ctx {
|
|
118
121
|
const env = process.env
|
|
119
122
|
const home = env["HOME"] !== undefined && env["HOME"] !== "" ? env["HOME"] : homedir()
|
|
123
|
+
const compactWindowSource = env["CLAUDE_COMPACT_WINDOW"] ?? ""
|
|
124
|
+
const claudeCompactWindow = compactWindowSource === "" ? "" : parseCompactWindow(compactWindowSource)
|
|
125
|
+
if (claudeCompactWindow === undefined) {
|
|
126
|
+
services.logger.err("CLAUDE_COMPACT_WINDOW expects a token count (e.g. 680000 or 680k)")
|
|
127
|
+
throw new ExitError(2)
|
|
128
|
+
}
|
|
129
|
+
const claudePlugins = (env["CLAUDE_PLUGINS"] ?? "")
|
|
130
|
+
.split(" ")
|
|
131
|
+
.filter((plugin) => plugin !== "")
|
|
132
|
+
.map((plugin) => parseClaudePlugin(plugin, services.logger.err))
|
|
120
133
|
return {
|
|
121
134
|
repoDir: kitHome(),
|
|
122
135
|
home,
|
|
@@ -128,9 +141,9 @@ function makeCtx(services: EngineServices): Ctx {
|
|
|
128
141
|
reconcile: env["RECONCILE"] === "1",
|
|
129
142
|
prune: env["PRUNE"] === "1",
|
|
130
143
|
assumeYes: env["ASSUME_YES"] === "1",
|
|
131
|
-
claudeCompactWindow
|
|
144
|
+
claudeCompactWindow,
|
|
132
145
|
claudePermissive: env["CLAUDE_PERMISSIVE"] === "1",
|
|
133
|
-
claudePlugins
|
|
146
|
+
claudePlugins,
|
|
134
147
|
claudeModel: env["CLAUDE_MODEL"] ?? "",
|
|
135
148
|
claudeEffort: "",
|
|
136
149
|
claudeAdvisor: "",
|
|
@@ -278,8 +291,8 @@ export async function runEngineNative(argv: ReadonlyArray<string>, services?: En
|
|
|
278
291
|
deps: baseServices.deps,
|
|
279
292
|
platform: baseServices.platform
|
|
280
293
|
}
|
|
281
|
-
ctx = makeCtx(runServices)
|
|
282
294
|
try {
|
|
295
|
+
ctx = makeCtx(runServices)
|
|
283
296
|
switch (argv[0]) {
|
|
284
297
|
case "model":
|
|
285
298
|
return modeModel(ctx, argv.slice(1))
|
|
@@ -6,16 +6,9 @@ import type { Ctx } from "./index"
|
|
|
6
6
|
import { isObject, parseJson, type Json } from "./jq"
|
|
7
7
|
import { payloadDisplayPath, payloadText } from "../payload"
|
|
8
8
|
|
|
9
|
-
function catalog(): Json | undefined {
|
|
10
|
-
try {
|
|
11
|
-
return parseJson(payloadText("SoT/models.json"))
|
|
12
|
-
} catch {
|
|
13
|
-
return undefined
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
9
|
|
|
17
10
|
function toolEntry(tool: string): { [k: string]: Json } | undefined {
|
|
18
|
-
const doc =
|
|
11
|
+
const doc = parseJson(payloadText("SoT/models.json"))
|
|
19
12
|
if (doc === undefined || !isObject(doc)) return undefined
|
|
20
13
|
const entry = doc[tool]
|
|
21
14
|
return entry !== undefined && isObject(entry) ? entry : undefined
|
|
@@ -37,7 +30,7 @@ export function printModels(ctx: Ctx, tool: string): void {
|
|
|
37
30
|
const { echo, warn } = ctx.services.logger
|
|
38
31
|
const entry = toolEntry(tool)
|
|
39
32
|
if (entry === undefined) {
|
|
40
|
-
warn(`Model catalog unavailable (${payloadDisplayPath("SoT/models.json"
|
|
33
|
+
warn(`Model catalog unavailable (${payloadDisplayPath("SoT/models.json")})`)
|
|
41
34
|
return
|
|
42
35
|
}
|
|
43
36
|
const verified = typeof entry["verified"] === "string" ? entry["verified"] : "?"
|
|
@@ -37,19 +37,29 @@ export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
37
37
|
if (value === "") {
|
|
38
38
|
if (tool === "claude") {
|
|
39
39
|
const deployed = p(ctx.home, ".claude", "settings.json")
|
|
40
|
-
|
|
40
|
+
const result = readConfig(deployed)
|
|
41
|
+
if (result.kind === "missing") {
|
|
41
42
|
warn("~/.claude/settings.json missing")
|
|
42
43
|
return 0
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
if (result.kind === "read-error") {
|
|
46
|
+
err(`Failed to read ~/.claude/settings.json: ${String(result.error)}`)
|
|
47
|
+
return 1
|
|
48
|
+
}
|
|
49
|
+
echo(`deployed: ${jsonModelText(result.data)}`)
|
|
45
50
|
echo(`SoT: ${jsonModelText(payloadText("SoT/.claude/settings.json"))}`)
|
|
46
51
|
} else {
|
|
47
52
|
const deployed = p(ctx.home, ".codex", "config.toml")
|
|
48
|
-
|
|
53
|
+
const result = readConfig(deployed)
|
|
54
|
+
if (result.kind === "missing") {
|
|
49
55
|
warn("~/.codex/config.toml missing")
|
|
50
56
|
return 0
|
|
51
57
|
}
|
|
52
|
-
|
|
58
|
+
if (result.kind === "read-error") {
|
|
59
|
+
err(`Failed to read ~/.codex/config.toml: ${String(result.error)}`)
|
|
60
|
+
return 1
|
|
61
|
+
}
|
|
62
|
+
echo(`deployed: ${tomlModelText(result.data)}`)
|
|
53
63
|
echo(`SoT: ${tomlModelText(payloadText("SoT/.codex/config.toml"))}`)
|
|
54
64
|
}
|
|
55
65
|
printModels(ctx, tool)
|
|
@@ -74,20 +84,23 @@ export function modeModel(ctx: Ctx, args: ReadonlyArray<string>): number {
|
|
|
74
84
|
return 0
|
|
75
85
|
}
|
|
76
86
|
|
|
77
|
-
|
|
87
|
+
type ConfigReadResult =
|
|
88
|
+
| { readonly kind: "missing" }
|
|
89
|
+
| { readonly kind: "read-error"; readonly error: unknown }
|
|
90
|
+
| { readonly kind: "data"; readonly data: string }
|
|
91
|
+
|
|
92
|
+
function readConfig(file: string): ConfigReadResult {
|
|
78
93
|
try {
|
|
79
|
-
readFileSync(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
94
|
+
return { kind: "data", data: readFileSync(file, "utf8") }
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const code =
|
|
97
|
+
typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
|
|
98
|
+
? error.code
|
|
99
|
+
: undefined
|
|
100
|
+
return code === "ENOENT" ? { kind: "missing" } : { kind: "read-error", error }
|
|
83
101
|
}
|
|
84
102
|
}
|
|
85
103
|
|
|
86
|
-
/** `jq -r '.model // "default (unset)"'` — empty on unparseable input. */
|
|
87
|
-
function jsonModelField(file: string): string {
|
|
88
|
-
return jsonModelText(readFileSync(file, "utf8"))
|
|
89
|
-
}
|
|
90
|
-
|
|
91
104
|
function jsonModelText(text: string): string {
|
|
92
105
|
const doc = parseJson(text)
|
|
93
106
|
if (doc === undefined) return ""
|
|
@@ -97,9 +110,6 @@ function jsonModelText(text: string): string {
|
|
|
97
110
|
}
|
|
98
111
|
|
|
99
112
|
/** `awk -F'"' '/^model[[:space:]]*=/{print $2; exit}'`. */
|
|
100
|
-
function tomlModelField(file: string): string {
|
|
101
|
-
return tomlModelText(readFileSync(file, "utf8"))
|
|
102
|
-
}
|
|
103
113
|
|
|
104
114
|
function tomlModelText(text: string): string {
|
|
105
115
|
for (const line of text.split("\n")) {
|
|
@@ -110,9 +120,9 @@ function tomlModelText(text: string): string {
|
|
|
110
120
|
|
|
111
121
|
export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
|
|
112
122
|
const { err } = ctx.services.logger
|
|
113
|
-
const words = args.filter((
|
|
114
|
-
const op = words[0] ??
|
|
115
|
-
const tool = words[1] ??
|
|
123
|
+
const words = args.filter((arg) => !arg.startsWith("--"))
|
|
124
|
+
const op = words[0] ?? "check"
|
|
125
|
+
const tool = words[1] ?? ""
|
|
116
126
|
for (const arg of args) {
|
|
117
127
|
if (arg === "--yes") ctx.assumeYes = true
|
|
118
128
|
else if (arg === "--verbose") {
|
|
@@ -128,7 +138,7 @@ export async function modeToolchain(ctx: Ctx, args: ReadonlyArray<string>): Prom
|
|
|
128
138
|
err("Usage: toolchain [check|ensure <tool>] [--yes]")
|
|
129
139
|
return 2
|
|
130
140
|
}
|
|
131
|
-
if (tool === ""
|
|
141
|
+
if (tool === "") {
|
|
132
142
|
err("Usage: toolchain ensure <tool> [--yes]")
|
|
133
143
|
return 2
|
|
134
144
|
}
|
|
@@ -14,11 +14,3 @@ export function platformName(pf: NodeJS.Platform = rawPlatform()): PlatformName
|
|
|
14
14
|
return pf === "linux" ? "linux" : pf === "darwin" ? "darwin" : "unknown"
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function isLinux(): boolean {
|
|
18
|
-
return rawPlatform() === "linux"
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Shell-rc exports apply only on supported hosts. */
|
|
22
|
-
export function shellRcApplicable(): boolean {
|
|
23
|
-
return platformName() !== "unknown"
|
|
24
|
-
}
|
|
@@ -24,13 +24,96 @@ export class ExitError extends Error {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export const KNOWN_CLAUDE_OPTIN_PLUGINS = ["supabase", "n8n"]
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
|
|
28
|
+
interface ModifierMetadata {
|
|
29
|
+
readonly target: "claude" | "codex"
|
|
30
|
+
readonly ignoredWarning: string
|
|
31
|
+
readonly hasValue: (ctx: Ctx) => boolean
|
|
32
|
+
readonly clear: (ctx: Ctx) => void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const MODIFIER_METADATA = {
|
|
36
|
+
"--claude-model": {
|
|
37
|
+
target: "claude",
|
|
38
|
+
ignoredWarning: "--claude-model ignored: claude target not selected",
|
|
39
|
+
hasValue: (ctx) => ctx.claudeModel !== "",
|
|
40
|
+
clear: (ctx) => {
|
|
41
|
+
ctx.claudeModel = ""
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"--claude-effort": {
|
|
45
|
+
target: "claude",
|
|
46
|
+
ignoredWarning: "--claude-effort ignored: claude target not selected",
|
|
47
|
+
hasValue: (ctx) => ctx.claudeEffort !== "",
|
|
48
|
+
clear: (ctx) => {
|
|
49
|
+
ctx.claudeEffort = ""
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"--claude-advisor": {
|
|
53
|
+
target: "claude",
|
|
54
|
+
ignoredWarning: "--claude-advisor ignored: claude target not selected",
|
|
55
|
+
hasValue: (ctx) => ctx.claudeAdvisor !== "",
|
|
56
|
+
clear: (ctx) => {
|
|
57
|
+
ctx.claudeAdvisor = ""
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"--claude-compact-window": {
|
|
61
|
+
target: "claude",
|
|
62
|
+
ignoredWarning: "--claude-compact-window ignored: claude target not selected",
|
|
63
|
+
hasValue: (ctx) => ctx.claudeCompactWindow !== "",
|
|
64
|
+
clear: (ctx) => {
|
|
65
|
+
ctx.claudeCompactWindow = ""
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"--claude-permissive": {
|
|
69
|
+
target: "claude",
|
|
70
|
+
ignoredWarning: "--claude-permissive ignored: claude target not selected",
|
|
71
|
+
hasValue: (ctx) => ctx.claudePermissive,
|
|
72
|
+
clear: (ctx) => {
|
|
73
|
+
ctx.claudePermissive = false
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"--claude-plugin": {
|
|
77
|
+
target: "claude",
|
|
78
|
+
ignoredWarning: "--claude-plugin ignored: claude target not selected",
|
|
79
|
+
hasValue: (ctx) => ctx.claudePlugins.length > 0,
|
|
80
|
+
clear: (ctx) => {
|
|
81
|
+
ctx.claudePlugins = []
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"--codex-model": {
|
|
85
|
+
target: "codex",
|
|
86
|
+
ignoredWarning: "--codex-model ignored: codex target not selected",
|
|
87
|
+
hasValue: (ctx) => ctx.codexModel !== "",
|
|
88
|
+
clear: (ctx) => {
|
|
89
|
+
ctx.codexModel = ""
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"--codex-effort": {
|
|
93
|
+
target: "codex",
|
|
94
|
+
ignoredWarning: "--codex-effort ignored: codex target not selected",
|
|
95
|
+
hasValue: (ctx) => ctx.codexEffort !== "",
|
|
96
|
+
clear: (ctx) => {
|
|
97
|
+
ctx.codexEffort = ""
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} satisfies Record<ModifierFlag, ModifierMetadata>
|
|
101
|
+
|
|
102
|
+
type ScalarModifierFlag =
|
|
103
|
+
| "--claude-model"
|
|
104
|
+
| "--claude-effort"
|
|
105
|
+
| "--claude-advisor"
|
|
106
|
+
| "--codex-model"
|
|
107
|
+
| "--codex-effort"
|
|
108
|
+
|
|
109
|
+
const SCALAR_MODIFIER_FLAGS: Record<ScalarModifierFlag, true> = {
|
|
110
|
+
"--claude-model": true,
|
|
111
|
+
"--claude-effort": true,
|
|
112
|
+
"--claude-advisor": true,
|
|
113
|
+
"--codex-model": true,
|
|
114
|
+
"--codex-effort": true
|
|
115
|
+
}
|
|
116
|
+
|
|
34
117
|
|
|
35
118
|
function usage(ctx: Ctx): void {
|
|
36
119
|
const { echo } = ctx.services.logger
|
|
@@ -86,13 +169,33 @@ export function parseCompactWindow(v: string): string | undefined {
|
|
|
86
169
|
return /^[0-9]+$/.test(v) ? v : undefined
|
|
87
170
|
}
|
|
88
171
|
|
|
89
|
-
function
|
|
90
|
-
const { err } = ctx.services.logger
|
|
172
|
+
export function parseClaudePlugin(name: string, err: (message: string) => void): string {
|
|
91
173
|
if (!KNOWN_CLAUDE_OPTIN_PLUGINS.includes(name)) {
|
|
92
174
|
err(`Unknown opt-in plugin '${name}'. Known: ${KNOWN_CLAUDE_OPTIN_PLUGINS.join(", ")}`)
|
|
93
175
|
throw new ExitError(2)
|
|
94
176
|
}
|
|
95
|
-
|
|
177
|
+
return name
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function markModifier(ctx: Ctx, flag: ModifierFlag): void {
|
|
181
|
+
const flags = ctx.modifierFlags ?? new Set<ModifierFlag>()
|
|
182
|
+
flags.add(flag)
|
|
183
|
+
ctx.modifierFlags = flags
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function addClaudePlugin(ctx: Ctx, name: string): void {
|
|
187
|
+
if (name === "") {
|
|
188
|
+
printCatalog(
|
|
189
|
+
ctx,
|
|
190
|
+
`Available Claude optional plugins:\n${KNOWN_CLAUDE_OPTIN_PLUGINS.map((plugin) => ` ${plugin}`).join("\n")}`
|
|
191
|
+
)
|
|
192
|
+
ctx.services.logger.err(
|
|
193
|
+
`Invalid Claude plugin '' — valid: ${KNOWN_CLAUDE_OPTIN_PLUGINS.join("|")}`
|
|
194
|
+
)
|
|
195
|
+
throw new ExitError(2)
|
|
196
|
+
}
|
|
197
|
+
ctx.claudePlugins.push(parseClaudePlugin(name, ctx.services.logger.err))
|
|
198
|
+
markModifier(ctx, "--claude-plugin")
|
|
96
199
|
}
|
|
97
200
|
|
|
98
201
|
function selectTarget(ctx: Ctx, target: string): void {
|
|
@@ -102,7 +205,7 @@ function selectTarget(ctx: Ctx, target: string): void {
|
|
|
102
205
|
ctx.targetFilterSet = true
|
|
103
206
|
}
|
|
104
207
|
|
|
105
|
-
function setModifier(ctx: Ctx, flag:
|
|
208
|
+
function setModifier(ctx: Ctx, flag: ScalarModifierFlag, value: string): void {
|
|
106
209
|
switch (flag) {
|
|
107
210
|
case "--claude-model":
|
|
108
211
|
ctx.claudeModel = value
|
|
@@ -120,20 +223,19 @@ function setModifier(ctx: Ctx, flag: ModifierFlag, value: string): void {
|
|
|
120
223
|
ctx.codexEffort = value
|
|
121
224
|
break
|
|
122
225
|
}
|
|
123
|
-
|
|
124
|
-
flags.add(flag)
|
|
125
|
-
ctx.modifierFlags = flags
|
|
226
|
+
markModifier(ctx, flag)
|
|
126
227
|
}
|
|
127
228
|
|
|
128
|
-
function
|
|
129
|
-
return
|
|
229
|
+
function isScalarModifierFlag(value: string): value is ScalarModifierFlag {
|
|
230
|
+
return SCALAR_MODIFIER_FLAGS[value as ScalarModifierFlag] === true
|
|
130
231
|
}
|
|
131
232
|
|
|
233
|
+
|
|
132
234
|
export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
133
235
|
const { err } = ctx.services.logger
|
|
134
236
|
for (let index = 0; index < args.length; index += 1) {
|
|
135
237
|
const arg = args[index] ?? ""
|
|
136
|
-
if (
|
|
238
|
+
if (isScalarModifierFlag(arg) && args[index + 1] === "") {
|
|
137
239
|
setModifier(ctx, arg, "")
|
|
138
240
|
index += 1
|
|
139
241
|
continue
|
|
@@ -190,6 +292,7 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
|
190
292
|
throw new ExitError(2)
|
|
191
293
|
case "--claude-permissive":
|
|
192
294
|
ctx.claudePermissive = true
|
|
295
|
+
markModifier(ctx, "--claude-permissive")
|
|
193
296
|
continue
|
|
194
297
|
case "--claude-plugin":
|
|
195
298
|
err(`--claude-plugin requires a value: --claude-plugin=<${KNOWN_CLAUDE_OPTIN_PLUGINS.join("|")}>`)
|
|
@@ -244,8 +347,12 @@ export function parseArgs(ctx: Ctx, args: ReadonlyArray<string>): void {
|
|
|
244
347
|
throw new ExitError(2)
|
|
245
348
|
}
|
|
246
349
|
ctx.claudeCompactWindow = parsed
|
|
350
|
+
markModifier(ctx, "--claude-compact-window")
|
|
247
351
|
} else if (arg.startsWith("--claude-plugin=")) {
|
|
248
352
|
addClaudePlugin(ctx, arg.slice("--claude-plugin=".length))
|
|
353
|
+
} else if (arg.startsWith("--claude-permissive=")) {
|
|
354
|
+
err("--claude-permissive does not take a value")
|
|
355
|
+
throw new ExitError(2)
|
|
249
356
|
} else {
|
|
250
357
|
err(`Unknown arg: ${arg}`)
|
|
251
358
|
throw new ExitError(2)
|
|
@@ -266,53 +373,49 @@ function printCatalog(ctx: Ctx, catalog: string): void {
|
|
|
266
373
|
|
|
267
374
|
export function validateModifierFlags(ctx: Ctx): void {
|
|
268
375
|
const { err, warn } = ctx.services.logger
|
|
269
|
-
const supplied = (flag: ModifierFlag
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
376
|
+
const supplied = (flag: ModifierFlag): boolean =>
|
|
377
|
+
MODIFIER_METADATA[flag].hasValue(ctx) || ctx.modifierFlags?.has(flag) === true
|
|
378
|
+
|
|
379
|
+
for (const flag of Object.keys(MODIFIER_METADATA) as Array<ModifierFlag>) {
|
|
380
|
+
const metadata = MODIFIER_METADATA[flag]
|
|
381
|
+
const targetSelected = metadata.target === "claude" ? ctx.syncClaude : ctx.syncCodex
|
|
382
|
+
if (!targetSelected && supplied(flag)) {
|
|
383
|
+
warn(metadata.ignoredWarning)
|
|
384
|
+
metadata.clear(ctx)
|
|
385
|
+
ctx.modifierFlags?.delete(flag)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (supplied("--claude-model")) {
|
|
390
|
+
if (!validateClaudeModel(ctx, ctx.claudeModel)) {
|
|
276
391
|
printModels(ctx, "claude")
|
|
277
392
|
err(`Invalid Claude model '${ctx.claudeModel}' — use an alias above or a full claude-* ID`)
|
|
278
393
|
throw new ExitError(2)
|
|
279
394
|
}
|
|
280
395
|
}
|
|
281
|
-
if (supplied("--claude-effort"
|
|
282
|
-
if (!ctx.
|
|
283
|
-
warn("--claude-effort ignored: claude target not selected")
|
|
284
|
-
ctx.claudeEffort = ""
|
|
285
|
-
} else if (!isEffortModifierValue("claude", ctx.claudeEffort)) {
|
|
396
|
+
if (supplied("--claude-effort")) {
|
|
397
|
+
if (!isEffortModifierValue("claude", ctx.claudeEffort)) {
|
|
286
398
|
printCatalog(ctx, effortCatalog("claude"))
|
|
287
399
|
err(`Invalid Claude effort '${ctx.claudeEffort}' — valid: ${effortValueGrammar("claude")}`)
|
|
288
400
|
throw new ExitError(2)
|
|
289
401
|
}
|
|
290
402
|
}
|
|
291
|
-
if (supplied("--claude-advisor"
|
|
292
|
-
if (!ctx.
|
|
293
|
-
warn("--claude-advisor ignored: claude target not selected")
|
|
294
|
-
ctx.claudeAdvisor = ""
|
|
295
|
-
} else if (!CLAUDE_ADVISOR_STATES.some((state) => state === ctx.claudeAdvisor)) {
|
|
403
|
+
if (supplied("--claude-advisor")) {
|
|
404
|
+
if (!CLAUDE_ADVISOR_STATES.some((state) => state === ctx.claudeAdvisor)) {
|
|
296
405
|
printCatalog(ctx, advisorCatalog())
|
|
297
406
|
err(`Invalid Claude advisor state '${ctx.claudeAdvisor}' — valid: ${advisorValueGrammar()}`)
|
|
298
407
|
throw new ExitError(2)
|
|
299
408
|
}
|
|
300
409
|
}
|
|
301
|
-
if (supplied("--codex-model"
|
|
302
|
-
if (!ctx.
|
|
303
|
-
warn("--codex-model ignored: codex target not selected")
|
|
304
|
-
ctx.codexModel = ""
|
|
305
|
-
} else if (!validateCodexModel(ctx, ctx.codexModel)) {
|
|
410
|
+
if (supplied("--codex-model")) {
|
|
411
|
+
if (!validateCodexModel(ctx, ctx.codexModel)) {
|
|
306
412
|
printModels(ctx, "codex")
|
|
307
413
|
err(`Invalid Codex model '${ctx.codexModel}' — must match ^[A-Za-z0-9._-]+$`)
|
|
308
414
|
throw new ExitError(2)
|
|
309
415
|
}
|
|
310
416
|
}
|
|
311
|
-
if (supplied("--codex-effort"
|
|
312
|
-
if (!ctx.
|
|
313
|
-
warn("--codex-effort ignored: codex target not selected")
|
|
314
|
-
ctx.codexEffort = ""
|
|
315
|
-
} else if (!isEffortModifierValue("codex", ctx.codexEffort)) {
|
|
417
|
+
if (supplied("--codex-effort")) {
|
|
418
|
+
if (!isEffortModifierValue("codex", ctx.codexEffort)) {
|
|
316
419
|
printCatalog(ctx, effortCatalog("codex"))
|
|
317
420
|
err(`Invalid Codex effort '${ctx.codexEffort}' — valid: ${effortValueGrammar("codex")}`)
|
|
318
421
|
throw new ExitError(2)
|
|
@@ -36,8 +36,6 @@ export interface DependencyManager {
|
|
|
36
36
|
export interface Platform {
|
|
37
37
|
readonly raw: () => NodeJS.Platform
|
|
38
38
|
readonly name: () => PlatformName
|
|
39
|
-
readonly isLinux: () => boolean
|
|
40
|
-
readonly shellRcApplicable: () => boolean
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
export interface EngineServices {
|
|
@@ -53,9 +51,7 @@ export interface EngineServiceOptions {
|
|
|
53
51
|
/** Platform view over an injectable platform id. */
|
|
54
52
|
export const makePlatform = (pf: NodeJS.Platform = rawPlatform()): Platform => ({
|
|
55
53
|
raw: () => pf,
|
|
56
|
-
name: () => platformName(pf)
|
|
57
|
-
isLinux: () => pf === "linux",
|
|
58
|
-
shellRcApplicable: () => pf === "linux" || pf === "darwin"
|
|
54
|
+
name: () => platformName(pf)
|
|
59
55
|
})
|
|
60
56
|
|
|
61
57
|
/** DependencyManager whose hints default to the INJECTED platform, not the host. */
|
|
@@ -16,8 +16,9 @@ export function reconcileSettings(repo: Json, user: Json): Json {
|
|
|
16
16
|
* `unique` — i.e. codepoint-sorted and deduplicated, matching jq).
|
|
17
17
|
*/
|
|
18
18
|
export function mergeSettings(repo: Json, user: Json): Json {
|
|
19
|
-
const
|
|
20
|
-
if (!isObject(
|
|
19
|
+
const candidate = deepMerge(user, repo)
|
|
20
|
+
if (!isObject(candidate)) return candidate
|
|
21
|
+
const merged = { ...candidate }
|
|
21
22
|
const permissions = isObject(merged["permissions"]) ? merged["permissions"] : {}
|
|
22
23
|
merged["permissions"] = {
|
|
23
24
|
...permissions,
|
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
* the kit-managed snapshot, the effect-solutions toolchain callback, and the
|
|
5
5
|
* snapshot write.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, statSync, symlinkSync } from "node:fs"
|
|
8
|
+
import { dirname, relative, resolve } from "node:path"
|
|
8
9
|
import { p, spawnProcess, writeFileIfChanged } from "./exec"
|
|
9
10
|
import { bunBootstrap } from "./bun"
|
|
10
11
|
import type { Ctx } from "./index"
|
|
11
|
-
import { compareCodepoints } from "./jq"
|
|
12
|
+
import { compareCodepoints, isObject, parseJson } from "./jq"
|
|
12
13
|
import type { EngineServices } from "./services"
|
|
13
14
|
import { ensure, field } from "./toolchain"
|
|
14
15
|
import { payloadText } from "../payload"
|
|
16
|
+
import { ExitError } from "./parseArgs"
|
|
15
17
|
|
|
16
18
|
export interface SkillsState {
|
|
17
19
|
present: number
|
|
@@ -26,16 +28,18 @@ export async function skillsSync(ctx: Ctx): Promise<SkillsState> {
|
|
|
26
28
|
if (!ctx.dryRun) mkdirSync(skillsDir, { recursive: true })
|
|
27
29
|
|
|
28
30
|
await syncUniversal(ctx, state, skillsDir, manifest)
|
|
29
|
-
|
|
31
|
+
const failedRemovals = ctx.prune ? await reconcileRemovals(ctx, manifest, snapshot) : []
|
|
30
32
|
await syncEffectSolutionsCli(ctx)
|
|
31
|
-
updateSnapshot(ctx, manifest, snapshot)
|
|
33
|
+
updateSnapshot(ctx, manifest, snapshot, failedRemovals)
|
|
32
34
|
return state
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/** skills::_skills_cli — the pinned npx package spec. */
|
|
36
38
|
function skillsCli(ctx: Ctx): string {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
+
const version = field(ctx, "skills-cli", "verified")
|
|
40
|
+
if (version !== "") return `skills@${version}`
|
|
41
|
+
ctx.services.logger.err("Universal skills sync aborted because SoT/toolchain.json has no verified skills-cli pin")
|
|
42
|
+
throw new ExitError(1)
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
/** skills::_normalize_manifest — cleaned slugs, one per line. */
|
|
@@ -131,7 +135,7 @@ function healClaudeSymlink(ctx: Ctx, skillsDir: string, base: string): boolean {
|
|
|
131
135
|
const canonical = p(skillsDir, base)
|
|
132
136
|
const claudeSkillsDir = p(ctx.home, ".claude", "skills")
|
|
133
137
|
const claudeLink = p(claudeSkillsDir, base)
|
|
134
|
-
const relTarget =
|
|
138
|
+
const relTarget = relative(dirname(claudeLink), canonical)
|
|
135
139
|
|
|
136
140
|
if (!isDir(canonical)) return false
|
|
137
141
|
|
|
@@ -186,31 +190,23 @@ function removeLink(path: string): boolean {
|
|
|
186
190
|
}
|
|
187
191
|
}
|
|
188
192
|
|
|
189
|
-
/** skills::_link_or_copy —
|
|
193
|
+
/** skills::_link_or_copy — create a symlink without replacing its source. */
|
|
190
194
|
export function linkOrCopy(target: string, link: string): boolean {
|
|
195
|
+
const resolvedLink = resolve(link)
|
|
196
|
+
if (resolve(dirname(resolvedLink), target) === resolvedLink) return true
|
|
191
197
|
removeLink(link)
|
|
192
198
|
try {
|
|
193
199
|
symlinkSync(target, link)
|
|
194
200
|
} catch {
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
if (lstat(link)?.isSymbolicLink() === true) return true
|
|
198
|
-
try {
|
|
199
|
-
// Resolve a relative target against the link's parent, like ln does.
|
|
200
|
-
const resolved = target.startsWith("/") ? target : p(link.slice(0, link.lastIndexOf("/")), target)
|
|
201
|
-
cpSync(resolved, link, { recursive: true })
|
|
202
|
-
} catch {
|
|
203
|
-
// fall through to the existence check below
|
|
201
|
+
return false
|
|
204
202
|
}
|
|
205
|
-
return
|
|
203
|
+
return lstat(link)?.isSymbolicLink() === true
|
|
206
204
|
}
|
|
207
205
|
|
|
208
206
|
function linkOrCopyWithWarnings(target: string, link: string, services: EngineServices): boolean {
|
|
209
207
|
const linked = linkOrCopy(target, link)
|
|
210
208
|
if (!linked) {
|
|
211
|
-
services.logger.warn(`could not create ${link}
|
|
212
|
-
} else if (lstat(link)?.isSymbolicLink() !== true) {
|
|
213
|
-
services.logger.warn(`symlinks unsupported here — ${link} is a copy refreshed on sync`)
|
|
209
|
+
services.logger.warn(`could not create symlink ${link}`)
|
|
214
210
|
}
|
|
215
211
|
return linked
|
|
216
212
|
}
|
|
@@ -255,7 +251,10 @@ export function effectSolutionsInstall(
|
|
|
255
251
|
|
|
256
252
|
async function syncEffectSolutionsCli(ctx: Ctx): Promise<void> {
|
|
257
253
|
const { clearProgress, progress, warn } = ctx.services.logger
|
|
258
|
-
|
|
254
|
+
const settings = parseJson(payloadText("SoT/.claude/settings.json"))
|
|
255
|
+
if (settings === undefined || !isObject(settings)) return
|
|
256
|
+
const enabledPlugins = settings["enabledPlugins"]
|
|
257
|
+
if (enabledPlugins === undefined || !isObject(enabledPlugins) || enabledPlugins["effect-kit@docks"] !== true) return
|
|
259
258
|
|
|
260
259
|
progress("Checking effect-solutions CLI...")
|
|
261
260
|
const result = await ensure(ctx, "effect-solutions", effectSolutionsInstall(ctx))
|
|
@@ -267,7 +266,7 @@ async function syncEffectSolutionsCli(ctx: Ctx): Promise<void> {
|
|
|
267
266
|
|
|
268
267
|
// ----------------------------------------------------- prune + snapshot ----
|
|
269
268
|
|
|
270
|
-
async function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): Promise<
|
|
269
|
+
async function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string): Promise<Array<string>> {
|
|
271
270
|
const { change, clearProgress, echo, progress, warn } = ctx.services.logger
|
|
272
271
|
if (!existsSync(snapshot)) {
|
|
273
272
|
if (ctx.dryRun) {
|
|
@@ -275,15 +274,18 @@ async function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string):
|
|
|
275
274
|
`[dry-run] (--prune) no kit-managed-skills snapshot yet; first real sync writes ${snapshot}, then future --prune runs reconcile against it`
|
|
276
275
|
)
|
|
277
276
|
}
|
|
278
|
-
return
|
|
277
|
+
return []
|
|
279
278
|
}
|
|
280
279
|
|
|
281
280
|
const current = normalizeManifest(manifest)
|
|
281
|
+
const currentBases = new Set(current.map((slug) => slug.slice(slug.lastIndexOf("/") + 1)))
|
|
282
282
|
let removed = 0
|
|
283
283
|
let failed = 0
|
|
284
|
+
const failedSlugs: Array<string> = []
|
|
284
285
|
for (const slug of readSlugs(snapshot)) {
|
|
285
286
|
if (current.includes(slug)) continue
|
|
286
287
|
const base = slug.slice(slug.lastIndexOf("/") + 1)
|
|
288
|
+
if (currentBases.has(base)) continue
|
|
287
289
|
if (ctx.dryRun) {
|
|
288
290
|
echo(`[dry-run] kit-managed skill no longer in SoT — would remove: ${base}`)
|
|
289
291
|
continue
|
|
@@ -298,6 +300,7 @@ async function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string):
|
|
|
298
300
|
} else {
|
|
299
301
|
warn(`Failed to remove kit-managed skill: ${base}`)
|
|
300
302
|
failed++
|
|
303
|
+
failedSlugs.push(slug)
|
|
301
304
|
}
|
|
302
305
|
}
|
|
303
306
|
|
|
@@ -306,13 +309,14 @@ async function reconcileRemovals(ctx: Ctx, manifest: string, snapshot: string):
|
|
|
306
309
|
ctx.nextStepTriggers.skillsRestart = true
|
|
307
310
|
}
|
|
308
311
|
if (failed > 0) warn(`${failed} skill remove(s) failed — re-run with --prune or run: npx skills remove --global <name> -y`)
|
|
312
|
+
return failedSlugs
|
|
309
313
|
}
|
|
310
314
|
|
|
311
|
-
function updateSnapshot(ctx: Ctx, manifest: string, snapshot: string): void {
|
|
315
|
+
function updateSnapshot(ctx: Ctx, manifest: string, snapshot: string, failedRemovals: ReadonlyArray<string>): void {
|
|
312
316
|
if (ctx.dryRun) return
|
|
313
317
|
|
|
314
318
|
mkdirSync(ctx.agentsDir, { recursive: true })
|
|
315
|
-
const sorted = [...new Set(normalizeManifest(manifest))].sort(compareCodepoints)
|
|
319
|
+
const sorted = [...new Set([...normalizeManifest(manifest), ...failedRemovals])].sort(compareCodepoints)
|
|
316
320
|
writeFileIfChanged(snapshot, sorted.length > 0 ? `${sorted.join("\n")}\n` : "")
|
|
317
321
|
}
|
|
318
322
|
|