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
|
@@ -59,14 +59,13 @@ export async function claudeSync(ctx: Ctx): Promise<ClaudeRuntimeState> {
|
|
|
59
59
|
template,
|
|
60
60
|
runtime.kind === "ready" ? runtime.paths : undefined
|
|
61
61
|
)
|
|
62
|
-
const prepared =
|
|
62
|
+
const prepared = prepareClaudeSettings(ctx, claudeDir, materialized)
|
|
63
63
|
|
|
64
64
|
syncClaudeRuntime(ctx, runtime)
|
|
65
65
|
syncClaudeMd(ctx, claudeDir)
|
|
66
66
|
if (ctx.dryRun) {
|
|
67
67
|
describeSettingsSync(ctx, claudeDir)
|
|
68
68
|
} else {
|
|
69
|
-
if (prepared === undefined) throw new Error("Claude settings were not prepared")
|
|
70
69
|
commitClaudeSettings(ctx, prepared)
|
|
71
70
|
}
|
|
72
71
|
syncRemovals(ctx, claudeDir, runtime)
|
|
@@ -153,8 +152,9 @@ export function prepareClaudeSettings(ctx: Ctx, claudeDir: string, repo: Json):
|
|
|
153
152
|
|
|
154
153
|
const previousBytes = readFileSync(path, "utf8")
|
|
155
154
|
const user = parseJson(previousBytes)
|
|
156
|
-
if (user === undefined) {
|
|
157
|
-
|
|
155
|
+
if (user === undefined || !isObject(user)) {
|
|
156
|
+
const reason = user === undefined ? "is not valid JSON" : "must contain a JSON object"
|
|
157
|
+
ctx.services.logger.err(`Aborting sync: ${path} ${reason}. Fix it manually or delete it to reinstall.`)
|
|
158
158
|
throw new ExitError(1)
|
|
159
159
|
}
|
|
160
160
|
const merged = ctx.reconcile ? reconcileSettings(repo, user) : mergeSettings(repo, user)
|
|
@@ -186,7 +186,7 @@ export function commitClaudeSettings(ctx: Ctx, prepared: PreparedClaudeSettings)
|
|
|
186
186
|
|
|
187
187
|
function describeSettingsSync(ctx: Ctx, claudeDir: string): void {
|
|
188
188
|
const { echo } = ctx.services.logger
|
|
189
|
-
const repoSettings = payloadDisplayPath("SoT/.claude/settings.json"
|
|
189
|
+
const repoSettings = payloadDisplayPath("SoT/.claude/settings.json")
|
|
190
190
|
const userSettings = p(claudeDir, "settings.json")
|
|
191
191
|
|
|
192
192
|
if (!existsSync(userSettings)) {
|
|
@@ -292,11 +292,12 @@ function syncClaudeJson(ctx: Ctx): void {
|
|
|
292
292
|
if (existsSync(claudeJson)) {
|
|
293
293
|
const before = readFileSync(claudeJson, "utf8")
|
|
294
294
|
const doc = parseJson(before)
|
|
295
|
-
if (doc === undefined) {
|
|
296
|
-
|
|
295
|
+
if (doc === undefined || !isObject(doc)) {
|
|
296
|
+
const reason = doc === undefined ? "not valid JSON" : "root must be a JSON object"
|
|
297
|
+
err(`Skipping ~/.claude.json edit: ${reason}. Fix or delete it.`)
|
|
297
298
|
return
|
|
298
299
|
}
|
|
299
|
-
const obj =
|
|
300
|
+
const obj = doc
|
|
300
301
|
applyFilter(obj)
|
|
301
302
|
const out = jqStringify(obj)
|
|
302
303
|
if (out === before) {
|
|
@@ -536,7 +537,7 @@ function sortedKeys(obj: Json | undefined): Array<string> {
|
|
|
536
537
|
}
|
|
537
538
|
|
|
538
539
|
/** claude::_plugin_user_scope_installed. */
|
|
539
|
-
function pluginUserScopeInstalled(installedPlugins: string, pluginId: string): boolean {
|
|
540
|
+
export function pluginUserScopeInstalled(installedPlugins: string, pluginId: string): boolean {
|
|
540
541
|
const doc = readJsonFile(installedPlugins)
|
|
541
542
|
if (doc === undefined || !isObject(doc) || !isObject(doc["plugins"])) return false
|
|
542
543
|
const rec = (doc["plugins"] as { [k: string]: Json })[pluginId]
|
|
@@ -658,14 +659,15 @@ async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
|
658
659
|
if (separator > 0) kitMarketplaces.add(pluginId.slice(separator + 1))
|
|
659
660
|
}
|
|
660
661
|
|
|
661
|
-
// Pass 3 — refresh the kit-owned marketplaces
|
|
662
|
-
//
|
|
662
|
+
// Pass 3 — refresh the kit-owned marketplaces unless the update command
|
|
663
|
+
// selected its install-missing-only fast path.
|
|
663
664
|
if (!ctx.skipPluginRefresh) {
|
|
664
665
|
for (const mpName of [...kitMarketplaces].sort(compareCodepoints)) {
|
|
665
666
|
progress(`Refreshing marketplace ${mpName}...`)
|
|
666
667
|
await cli(["plugin", "marketplace", "update", mpName])
|
|
667
668
|
clearProgress()
|
|
668
669
|
}
|
|
670
|
+
// Pass 4 — update the kit-owned installed plugins.
|
|
669
671
|
for (const pluginId of [...kitPluginIds].sort(compareCodepoints)) {
|
|
670
672
|
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
671
673
|
progress(`Updating plugin ${pluginId}...`)
|
|
@@ -675,14 +677,14 @@ async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
|
675
677
|
}
|
|
676
678
|
}
|
|
677
679
|
|
|
678
|
-
//
|
|
680
|
+
// Pass 5 — prune-gated user-scope plugin uninstall.
|
|
679
681
|
let removedPl = 0
|
|
680
682
|
let removedMp = 0
|
|
681
|
-
let f4 = 0
|
|
682
683
|
let f5 = 0
|
|
684
|
+
let f6 = 0
|
|
683
685
|
if (ctx.prune) {
|
|
684
686
|
for (const pluginId of installedKeys) {
|
|
685
|
-
if (
|
|
687
|
+
if (kitPluginIds.has(pluginId)) continue
|
|
686
688
|
if (!pluginUserScopeInstalled(installedPlugins, pluginId)) continue
|
|
687
689
|
progress(`Uninstalling plugin ${pluginId}...`)
|
|
688
690
|
const uninstallResult = await cli(["plugin", "uninstall", "-y", "--scope", "user", pluginId])
|
|
@@ -691,15 +693,15 @@ async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
|
691
693
|
removedPl++
|
|
692
694
|
} else {
|
|
693
695
|
warn(`Failed to uninstall plugin: ${pluginId}`)
|
|
694
|
-
|
|
696
|
+
f5++
|
|
695
697
|
}
|
|
696
698
|
}
|
|
699
|
+
// Pass 6 — prune-gated marketplace removal.
|
|
697
700
|
const known = readJsonFile(knownMarketplaces)
|
|
698
701
|
for (const mpName of sortedKeys(known)) {
|
|
699
702
|
if (mpName === "claude-plugins-official") continue
|
|
700
703
|
if (nonUserMarketplaces.has(mpName)) continue
|
|
701
|
-
|
|
702
|
-
if (declared !== undefined && declared !== null && declared !== false) continue
|
|
704
|
+
if (kitMarketplaces.has(mpName)) continue
|
|
703
705
|
progress(`Removing marketplace ${mpName}...`)
|
|
704
706
|
const removeResult = await cli(["plugin", "marketplace", "remove", mpName])
|
|
705
707
|
clearProgress()
|
|
@@ -707,18 +709,18 @@ async function syncPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
|
707
709
|
removedMp++
|
|
708
710
|
} else {
|
|
709
711
|
warn(`Failed to remove marketplace: ${mpName}`)
|
|
710
|
-
|
|
712
|
+
f6++
|
|
711
713
|
}
|
|
712
714
|
}
|
|
713
715
|
}
|
|
714
716
|
|
|
715
|
-
// Pass
|
|
717
|
+
// Pass 7 — re-assert SoT enabled-state in the user settings.
|
|
716
718
|
if (await reassertEnabledState(ctx, repoObj, p(claudeDir, "settings.json"))) {
|
|
717
719
|
change("Plugin enable-state re-asserted from SoT in settings.json")
|
|
718
720
|
ctx.nextStepTriggers.claudePlugins = true
|
|
719
721
|
}
|
|
720
722
|
|
|
721
|
-
const failed = f1 + f2 +
|
|
723
|
+
const failed = f1 + f2 + f5 + f6
|
|
722
724
|
if (addedMp > 0 || addedPl > 0 || updatedPl > 0 || removedPl > 0 || removedMp > 0) {
|
|
723
725
|
change(`Plugins synced (marketplaces: +${addedMp} -${removedMp}, plugins: +${addedPl} ~${updatedPl} -${removedPl})`)
|
|
724
726
|
ctx.nextStepTriggers.claudePlugins = true
|
|
@@ -841,9 +843,11 @@ async function syncOptionalPlugins(ctx: Ctx, claudeDir: string): Promise<void> {
|
|
|
841
843
|
|
|
842
844
|
// ---------------------------------------------------------- LSP servers ----
|
|
843
845
|
|
|
844
|
-
function lspPkg(ctx: Ctx, tool: string, pkg: string): string {
|
|
845
|
-
const
|
|
846
|
-
|
|
846
|
+
function lspPkg(ctx: Ctx, tool: string, pkg: string): string | undefined {
|
|
847
|
+
const version = field(ctx, tool, "verified")
|
|
848
|
+
if (version !== "") return `${pkg}@${version}`
|
|
849
|
+
ctx.services.logger.warn(`Skipping ${pkg} install: ${tool} has no verified version in SoT/toolchain.json`)
|
|
850
|
+
return undefined
|
|
847
851
|
}
|
|
848
852
|
|
|
849
853
|
async function syncLspServers(ctx: Ctx): Promise<void> {
|
|
@@ -855,14 +859,17 @@ async function syncLspServers(ctx: Ctx): Promise<void> {
|
|
|
855
859
|
const hasTs = Object.prototype.hasOwnProperty.call(enabled, "typescript-lsp@claude-plugins-official")
|
|
856
860
|
if (!hasPhp && !hasTs) return
|
|
857
861
|
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
862
|
+
const phpMissing = hasPhp && ctx.services.deps.probe("intelephense").state === "missing"
|
|
863
|
+
const tsServerMissing = hasTs && ctx.services.deps.probe("typescript-language-server").state === "missing"
|
|
864
|
+
const tscMissing = hasTs && ctx.services.deps.probe("tsc").state === "missing"
|
|
865
|
+
const missingToolCount = Number(phpMissing) + Number(tsServerMissing) + Number(tscMissing)
|
|
866
|
+
const missing = [
|
|
867
|
+
phpMissing ? lspPkg(ctx, "intelephense", "intelephense") : undefined,
|
|
868
|
+
tsServerMissing ? lspPkg(ctx, "typescript-language-server", "typescript-language-server") : undefined,
|
|
869
|
+
tscMissing ? lspPkg(ctx, "tsc", "typescript") : undefined
|
|
870
|
+
].filter((spec): spec is string => spec !== undefined)
|
|
871
|
+
|
|
872
|
+
if (missingToolCount === 0) {
|
|
866
873
|
if (ctx.dryRun) {
|
|
867
874
|
echo("[dry-run] LSP server binaries present")
|
|
868
875
|
} else {
|
|
@@ -870,6 +877,7 @@ async function syncLspServers(ctx: Ctx): Promise<void> {
|
|
|
870
877
|
}
|
|
871
878
|
return
|
|
872
879
|
}
|
|
880
|
+
if (missing.length === 0) return
|
|
873
881
|
|
|
874
882
|
const specs = missing.join(" ")
|
|
875
883
|
if (ctx.dryRun) {
|
|
@@ -70,7 +70,10 @@ async function ensureBubblewrap(ctx: Ctx): Promise<void> {
|
|
|
70
70
|
return
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
const namespaceProbe = await spawnProcess("unshare", ["-Ur", "true"], { stdio: "ignore" })
|
|
74
|
+
if (namespaceProbe.error !== undefined) {
|
|
75
|
+
warn(`Could not run unshare to check user namespaces: ${namespaceProbe.error.message}`)
|
|
76
|
+
} else if (namespaceProbe.exitCode === 0) {
|
|
74
77
|
change(`bubblewrap installed and functional (${await ctx.services.deps.version("bwrap")})`)
|
|
75
78
|
} else {
|
|
76
79
|
warn(
|
|
@@ -100,10 +103,14 @@ function bwrapDetectPmInstallCmd(ctx: Ctx): string {
|
|
|
100
103
|
|
|
101
104
|
function syncConfig(ctx: Ctx, sotConfigText: string, userConfig: string): void {
|
|
102
105
|
const { change, echo, verbose } = ctx.services.logger
|
|
103
|
-
const sotConfig = payloadDisplayPath("SoT/.codex/config.toml"
|
|
106
|
+
const sotConfig = payloadDisplayPath("SoT/.codex/config.toml")
|
|
104
107
|
|
|
105
108
|
if (ctx.dryRun) {
|
|
106
|
-
|
|
109
|
+
if (existsSync(userConfig)) {
|
|
110
|
+
echo(`[dry-run] merge ${sotConfig} -> ${userConfig}`)
|
|
111
|
+
} else {
|
|
112
|
+
echo(`[dry-run] install ${sotConfig} -> ${userConfig}`)
|
|
113
|
+
}
|
|
107
114
|
return
|
|
108
115
|
}
|
|
109
116
|
|
|
@@ -119,7 +126,8 @@ function syncConfig(ctx: Ctx, sotConfigText: string, userConfig: string): void {
|
|
|
119
126
|
// overwrite the recovery copy with already-merged content.
|
|
120
127
|
const before = readFileSync(userConfig, "utf8")
|
|
121
128
|
const staging = `${userConfig}.merge.tmp`
|
|
122
|
-
|
|
129
|
+
// Normalize once before record transforms because CR bytes change table-header identity.
|
|
130
|
+
writeFileSync(staging, before.replace(/\r\n/g, "\n"))
|
|
123
131
|
|
|
124
132
|
scrubDeprecatedFeatures(ctx, staging)
|
|
125
133
|
removeRetiredPluginTables(ctx, staging)
|
|
@@ -146,15 +154,20 @@ export function scrubDeprecatedFeaturesText(content: string): string {
|
|
|
146
154
|
let header = ""
|
|
147
155
|
let body = ""
|
|
148
156
|
let keep = false
|
|
157
|
+
let changed = false
|
|
149
158
|
for (const line of lines) {
|
|
150
159
|
if (inFeatures) {
|
|
151
160
|
if (line.startsWith("[")) {
|
|
152
161
|
inFeatures = false
|
|
153
162
|
if (keep) out += `${header}\n${body}`
|
|
163
|
+
else changed = true
|
|
154
164
|
out += `${line}\n`
|
|
155
165
|
continue
|
|
156
166
|
}
|
|
157
|
-
if (/^use_legacy_landlock[ \t]*=/.test(line))
|
|
167
|
+
if (/^use_legacy_landlock[ \t]*=/.test(line)) {
|
|
168
|
+
changed = true
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
158
171
|
body += `${line}\n`
|
|
159
172
|
if (/[^ \t\f\v\r]/.test(line)) keep = true
|
|
160
173
|
continue
|
|
@@ -168,17 +181,21 @@ export function scrubDeprecatedFeaturesText(content: string): string {
|
|
|
168
181
|
}
|
|
169
182
|
out += `${line}\n`
|
|
170
183
|
}
|
|
171
|
-
if (inFeatures
|
|
172
|
-
|
|
184
|
+
if (inFeatures) {
|
|
185
|
+
if (keep) out += `${header}\n${body}`
|
|
186
|
+
else changed = true
|
|
187
|
+
}
|
|
188
|
+
return changed ? out : content
|
|
173
189
|
}
|
|
174
190
|
|
|
175
191
|
function scrubDeprecatedFeatures(ctx: Ctx, userConfig: string): void {
|
|
176
192
|
const { change } = ctx.services.logger
|
|
177
193
|
if (!existsSync(userConfig)) return
|
|
178
194
|
const content = readFileSync(userConfig, "utf8")
|
|
179
|
-
|
|
195
|
+
const next = scrubDeprecatedFeaturesText(content)
|
|
196
|
+
if (next === content) return
|
|
180
197
|
|
|
181
|
-
writeFileSync(`${userConfig}.tmp`,
|
|
198
|
+
writeFileSync(`${userConfig}.tmp`, next)
|
|
182
199
|
renameSync(`${userConfig}.tmp`, userConfig)
|
|
183
200
|
change("Codex: scrubbed deprecated [features].use_legacy_landlock")
|
|
184
201
|
}
|
|
@@ -236,38 +253,134 @@ function mergeTopLevelSettings(sotConfigText: string, userConfig: string): void
|
|
|
236
253
|
}
|
|
237
254
|
}
|
|
238
255
|
|
|
239
|
-
|
|
256
|
+
interface TomlTableHeader {
|
|
257
|
+
readonly path: string
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const TOML_BASIC_ESCAPES: Readonly<Record<string, string>> = {
|
|
261
|
+
b: "\b",
|
|
262
|
+
t: "\t",
|
|
263
|
+
n: "\n",
|
|
264
|
+
f: "\f",
|
|
265
|
+
r: "\r",
|
|
266
|
+
'"': '"',
|
|
267
|
+
"\\": "\\"
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function tomlBasicEscape(line: string, offset: number): { readonly next: number; readonly value: string } | undefined {
|
|
271
|
+
const escaped = line[offset]
|
|
272
|
+
const simple = escaped === undefined ? undefined : TOML_BASIC_ESCAPES[escaped]
|
|
273
|
+
if (simple !== undefined) return { next: offset + 1, value: simple }
|
|
274
|
+
const digits = escaped === "u" ? 4 : escaped === "U" ? 8 : 0
|
|
275
|
+
if (digits === 0) return undefined
|
|
276
|
+
const hex = line.slice(offset + 1, offset + 1 + digits)
|
|
277
|
+
if (hex.length !== digits || !/^[0-9A-Fa-f]+$/.test(hex)) return undefined
|
|
278
|
+
const codePoint = Number.parseInt(hex, 16)
|
|
279
|
+
if (codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff)) return undefined
|
|
280
|
+
return { next: offset + 1 + digits, value: String.fromCodePoint(codePoint) }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Decode a table header to the TOML path that determines managed ownership. */
|
|
284
|
+
function tomlTableHeader(line: string): TomlTableHeader | undefined {
|
|
285
|
+
let offset = 0
|
|
286
|
+
const skipWhitespace = (): void => {
|
|
287
|
+
while (line[offset] === " " || line[offset] === "\t") offset++
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
skipWhitespace()
|
|
291
|
+
if (line[offset] !== "[") return undefined
|
|
292
|
+
offset++
|
|
293
|
+
const array = line[offset] === "["
|
|
294
|
+
if (array) offset++
|
|
295
|
+
|
|
296
|
+
const keys: Array<string> = []
|
|
297
|
+
while (true) {
|
|
298
|
+
skipWhitespace()
|
|
299
|
+
const quote = line[offset]
|
|
300
|
+
let key = ""
|
|
301
|
+
if (quote === '"' || quote === "'") {
|
|
302
|
+
offset++
|
|
303
|
+
let closed = false
|
|
304
|
+
while (offset < line.length) {
|
|
305
|
+
const char = line[offset]!
|
|
306
|
+
if (char === quote) {
|
|
307
|
+
offset++
|
|
308
|
+
closed = true
|
|
309
|
+
break
|
|
310
|
+
}
|
|
311
|
+
if (quote === '"' && char === "\\") {
|
|
312
|
+
const escape = tomlBasicEscape(line, offset + 1)
|
|
313
|
+
if (escape === undefined) return undefined
|
|
314
|
+
key += escape.value
|
|
315
|
+
offset = escape.next
|
|
316
|
+
continue
|
|
317
|
+
}
|
|
318
|
+
if (char === "\n" || char === "\r") return undefined
|
|
319
|
+
key += char
|
|
320
|
+
offset++
|
|
321
|
+
}
|
|
322
|
+
if (!closed) return undefined
|
|
323
|
+
} else {
|
|
324
|
+
const start = offset
|
|
325
|
+
while (offset < line.length && /[A-Za-z0-9_-]/.test(line[offset]!)) offset++
|
|
326
|
+
if (offset === start) return undefined
|
|
327
|
+
key = line.slice(start, offset)
|
|
328
|
+
}
|
|
329
|
+
keys.push(key)
|
|
330
|
+
|
|
331
|
+
skipWhitespace()
|
|
332
|
+
if (line[offset] === ".") {
|
|
333
|
+
offset++
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
if (line[offset] !== "]") return undefined
|
|
337
|
+
offset++
|
|
338
|
+
if (array) {
|
|
339
|
+
if (line[offset] !== "]") return undefined
|
|
340
|
+
offset++
|
|
341
|
+
}
|
|
342
|
+
skipWhitespace()
|
|
343
|
+
if (offset < line.length && line[offset] !== "#") return undefined
|
|
344
|
+
return { path: JSON.stringify(keys) }
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function mergeTableSettingsText(sotConfigText: string, userConfigText: string): string {
|
|
240
349
|
const sotLines = sotConfigText.split("\n")
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
350
|
+
let merged = userConfigText
|
|
351
|
+
for (let tableOffset = 0; tableOffset < sotLines.length; tableOffset++) {
|
|
352
|
+
const managedHeader = tomlTableHeader(sotLines[tableOffset]!)
|
|
353
|
+
if (managedHeader === undefined) continue
|
|
354
|
+
|
|
245
355
|
const block: Array<string> = []
|
|
246
|
-
for (
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
356
|
+
for (let blockOffset = tableOffset; blockOffset < sotLines.length; blockOffset++) {
|
|
357
|
+
const line = sotLines[blockOffset]!
|
|
358
|
+
if (blockOffset !== tableOffset && tomlTableHeader(line) !== undefined) break
|
|
359
|
+
block.push(line)
|
|
250
360
|
}
|
|
251
361
|
const tableBlock = block.join("\n").replace(/\n+$/, "")
|
|
252
362
|
|
|
253
|
-
|
|
254
|
-
const userLines = readFileSync(userConfig, "utf8").split("\n")
|
|
363
|
+
const userLines = merged.split("\n")
|
|
255
364
|
if (userLines[userLines.length - 1] === "") userLines.pop()
|
|
256
365
|
let skip = false
|
|
257
366
|
const kept: Array<string> = []
|
|
258
367
|
for (const line of userLines) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
368
|
+
const header = tomlTableHeader(line)
|
|
369
|
+
if (header !== undefined) {
|
|
370
|
+
skip = header.path === managedHeader.path
|
|
371
|
+
if (skip) continue
|
|
262
372
|
}
|
|
263
|
-
if (skip && line.startsWith("[")) skip = false
|
|
264
373
|
if (!skip) kept.push(line)
|
|
265
374
|
}
|
|
266
|
-
|
|
267
|
-
const next = `${kept.join("\n")}\n\n${tableBlock}\n`
|
|
268
|
-
writeFileSync(`${userConfig}.tmp`, next)
|
|
269
|
-
renameSync(`${userConfig}.tmp`, userConfig)
|
|
375
|
+
merged = `${kept.join("\n")}\n\n${tableBlock}\n`
|
|
270
376
|
}
|
|
377
|
+
return merged
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function mergeTableSettings(sotConfigText: string, userConfig: string): void {
|
|
381
|
+
const next = mergeTableSettingsText(sotConfigText, readFileSync(userConfig, "utf8"))
|
|
382
|
+
writeFileSync(`${userConfig}.tmp`, next)
|
|
383
|
+
renameSync(`${userConfig}.tmp`, userConfig)
|
|
271
384
|
}
|
|
272
385
|
|
|
273
386
|
// ------------------------------------------------------- rules + agents ----
|
|
@@ -276,7 +389,7 @@ function syncRules(ctx: Ctx, sotRules: ReadonlyArray<PayloadPath>, userRulesDir:
|
|
|
276
389
|
const { change, echo, verbose } = ctx.services.logger
|
|
277
390
|
const firstRule = sotRules[0]
|
|
278
391
|
if (firstRule === undefined) return
|
|
279
|
-
const firstDisplay = payloadDisplayPath(firstRule
|
|
392
|
+
const firstDisplay = payloadDisplayPath(firstRule)
|
|
280
393
|
const sotRulesDir = firstDisplay.slice(0, firstDisplay.lastIndexOf("/"))
|
|
281
394
|
|
|
282
395
|
if (ctx.dryRun) {
|
|
@@ -306,7 +419,7 @@ function syncRules(ctx: Ctx, sotRules: ReadonlyArray<PayloadPath>, userRulesDir:
|
|
|
306
419
|
|
|
307
420
|
function syncAgentsMd(ctx: Ctx, sotAgentsMdText: string, userAgentsMd: string): void {
|
|
308
421
|
const { change, echo, verbose } = ctx.services.logger
|
|
309
|
-
const sotAgentsMd = payloadDisplayPath("SoT/.codex/AGENTS.md"
|
|
422
|
+
const sotAgentsMd = payloadDisplayPath("SoT/.codex/AGENTS.md")
|
|
310
423
|
|
|
311
424
|
if (ctx.dryRun) {
|
|
312
425
|
echo(`[dry-run] cp ${sotAgentsMd} -> ${userAgentsMd}`)
|
|
@@ -326,26 +439,32 @@ function syncAgentsMd(ctx: Ctx, sotAgentsMdText: string, userAgentsMd: string):
|
|
|
326
439
|
// ---------------------------------------------------------- marketplace ----
|
|
327
440
|
|
|
328
441
|
function syncMarketplace(ctx: Ctx, sotMarketplaceText: string, userMarketplace: string): void {
|
|
329
|
-
const { change, echo,
|
|
330
|
-
const sotMarketplace = payloadDisplayPath("SoT/.codex/plugins/marketplace.json"
|
|
442
|
+
const { change, echo, verbose } = ctx.services.logger
|
|
443
|
+
const sotMarketplace = payloadDisplayPath("SoT/.codex/plugins/marketplace.json")
|
|
444
|
+
const repo = parseJson(sotMarketplaceText)
|
|
445
|
+
if (repo === undefined) throw new Error(`invalid SoT marketplace JSON: ${sotMarketplace}`)
|
|
446
|
+
|
|
447
|
+
const userText = existsSync(userMarketplace) ? readFileSync(userMarketplace, "utf8") : undefined
|
|
448
|
+
const user = userText === undefined ? undefined : parseJson(userText)
|
|
449
|
+
if (userText !== undefined && user === undefined) {
|
|
450
|
+
throw new Error(`invalid deployed Codex marketplace JSON: ${userMarketplace}. Fix or delete it.`)
|
|
451
|
+
}
|
|
452
|
+
const out = user === undefined ? undefined : jqStringify(mergeMarketplace(repo, user))
|
|
331
453
|
|
|
332
454
|
if (ctx.dryRun) {
|
|
333
|
-
|
|
455
|
+
if (userText === undefined) {
|
|
456
|
+
echo(`[dry-run] cp ${sotMarketplace} -> ${userMarketplace}`)
|
|
457
|
+
} else if (out === userText) {
|
|
458
|
+
verbose("Codex marketplace already in sync")
|
|
459
|
+
} else {
|
|
460
|
+
echo(`[dry-run] merge ${sotMarketplace} -> ${userMarketplace} (backup at ${userMarketplace}.bak)`)
|
|
461
|
+
}
|
|
334
462
|
return
|
|
335
463
|
}
|
|
336
464
|
|
|
337
465
|
mkdirSync(p(ctx.agentsDir, "plugins"), { recursive: true })
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (existsSync(userMarketplace)) {
|
|
342
|
-
const user = parseJson(readFileSync(userMarketplace, "utf8"))
|
|
343
|
-
if (user === undefined) {
|
|
344
|
-
err(`Skipping marketplace sync: ${userMarketplace} is not valid JSON. Fix or delete it.`)
|
|
345
|
-
return
|
|
346
|
-
}
|
|
347
|
-
const out = jqStringify(mergeMarketplace(repo, user))
|
|
348
|
-
if (out === readFileSync(userMarketplace, "utf8")) {
|
|
466
|
+
if (userText !== undefined && out !== undefined) {
|
|
467
|
+
if (out === userText) {
|
|
349
468
|
verbose("Codex marketplace already in sync")
|
|
350
469
|
return
|
|
351
470
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* historical record-oriented behavior.
|
|
6
6
|
*/
|
|
7
7
|
import { p } from "./exec"
|
|
8
|
-
import { readFileSync, renameSync, writeFileSync } from "node:fs"
|
|
8
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs"
|
|
9
9
|
|
|
10
10
|
import { resolveEffort } from "../efforts"
|
|
11
11
|
import type { Ctx } from "./index"
|
|
@@ -61,17 +61,22 @@ function syncCodexSetting(ctx: Ctx, edit: CodexSettingEdit): void {
|
|
|
61
61
|
const { change, echo, verbose, warn } = ctx.services.logger
|
|
62
62
|
const userCodexSettings = p(ctx.home, ".codex", "config.toml")
|
|
63
63
|
|
|
64
|
-
if (
|
|
65
|
-
|
|
64
|
+
if (!existsSync(userCodexSettings)) {
|
|
65
|
+
// A dry-run sync already previewed installing the file, so the edit is not skipped.
|
|
66
|
+
// The `docks-kit model codex <m>` command creates no file, so the skip stands.
|
|
67
|
+
if (ctx.dryRun && ctx.syncCodex) {
|
|
68
|
+
echo(`[dry-run] (${edit.tag}) set ${edit.key} = "${edit.value}" in ${userCodexSettings}`)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
warn(`(${edit.tag}) ${userCodexSettings} missing — skipped`)
|
|
66
72
|
return
|
|
67
73
|
}
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
} catch {
|
|
72
|
-
warn(`(${edit.tag}) ${userCodexSettings} missing — skipped`)
|
|
75
|
+
if (ctx.dryRun) {
|
|
76
|
+
echo(`[dry-run] (${edit.tag}) set ${edit.key} = "${edit.value}" in ${userCodexSettings}`)
|
|
73
77
|
return
|
|
74
78
|
}
|
|
79
|
+
|
|
75
80
|
if (replaceTopLevelSettingInFile(userCodexSettings, edit.key, `${edit.key} = "${edit.value}"`)) {
|
|
76
81
|
change(edit.changed)
|
|
77
82
|
ctx.nextStepTriggers.codexRestart = true
|
|
@@ -107,11 +107,11 @@ const home = (): string => {
|
|
|
107
107
|
return envHome !== undefined && envHome !== "" ? envHome : homedir()
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
110
|
+
/**
|
|
111
|
+
* The resolved path gets persisted into global direct-exec hooks, so a
|
|
112
|
+
* relative `which` hit (relative PATH entry, relative BUN_INSTALL) would
|
|
113
|
+
* break outside the sync working directory.
|
|
114
|
+
*/
|
|
115
115
|
const findBun = (exec: ProbeExecutor): { command: string; path: string } | undefined => {
|
|
116
116
|
const onPath = exec.which("bun")
|
|
117
117
|
if (onPath !== "" && isAbsolute(onPath)) {
|
|
@@ -141,22 +141,20 @@ const resolveEffectSolutions = (exec: ProbeExecutor): ProbeResult => {
|
|
|
141
141
|
: { state: "missing" }
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
const versionBunCommand = (exec: ProbeExecutor): string =>
|
|
145
|
-
exec.commandExists("bun") ? "bun" : p(home(), ".bun", "bin", "bun")
|
|
146
|
-
|
|
147
144
|
const versionEffectSolutions = async (exec: ProbeExecutor): Promise<string> => {
|
|
148
|
-
const bun =
|
|
149
|
-
if (bun
|
|
150
|
-
const match = /effect-solutions@([0-9][0-9.]*)/.exec(await exec.capture(bun, ["pm", "-g", "ls"]))
|
|
145
|
+
const bun = findBun(exec)
|
|
146
|
+
if (bun === undefined) return ""
|
|
147
|
+
const match = /effect-solutions@([0-9][0-9.]*)/.exec(await exec.capture(bun.command, ["pm", "-g", "ls"]))
|
|
151
148
|
return match?.[1] ?? ""
|
|
152
149
|
}
|
|
153
150
|
|
|
154
151
|
const locateEffectSolutions = async (exec: ProbeExecutor): Promise<DependencyLocation> => {
|
|
155
152
|
const strictBun = findBun(exec)
|
|
156
153
|
const pathBun = exec.which("bun")
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
// This site may accept a relative hit because it does not persist the Bun path.
|
|
155
|
+
const bunForGlobalBin = strictBun?.command ?? (pathBun !== "" ? "bun" : undefined)
|
|
156
|
+
if (bunForGlobalBin === undefined) return { path: "", binDir: "" }
|
|
157
|
+
const globalBin = await exec.capture(bunForGlobalBin, ["pm", "-g", "bin"])
|
|
160
158
|
const path = globalBin !== "" ? p(globalBin, "effect-solutions") : ""
|
|
161
159
|
const resolved = path !== "" && exec.which(path) !== "" ? path : ""
|
|
162
160
|
return { path: resolved, binDir: globalBin }
|
|
@@ -233,7 +231,11 @@ export const DEPENDENCIES: Record<ToolId, DependencySpec> = {
|
|
|
233
231
|
() => "curl -fsSL https://bun.sh/install | bash",
|
|
234
232
|
{
|
|
235
233
|
resolve: resolveBun,
|
|
236
|
-
version: (exec) =>
|
|
234
|
+
version: async (exec) => {
|
|
235
|
+
const bun = findBun(exec)
|
|
236
|
+
if (bun === undefined) return ""
|
|
237
|
+
return await exec.capture(bun.command, ["--version"])
|
|
238
|
+
},
|
|
237
239
|
locate: async (exec) => ({ path: findBun(exec)?.path ?? "", binDir: "" })
|
|
238
240
|
}
|
|
239
241
|
),
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* substitution: stdout with trailing newlines stripped, empty on failure.
|
|
5
5
|
*/
|
|
6
6
|
import { spawn, type ChildProcess, type SpawnOptions } from "node:child_process"
|
|
7
|
-
import { accessSync,
|
|
7
|
+
import { accessSync, constants, existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
8
8
|
import { delimiter, isAbsolute, join } from "node:path"
|
|
9
9
|
|
|
10
10
|
/** Keep engine paths slash-separated so rendered output is host-stable. */
|
|
@@ -101,23 +101,10 @@ export function isExecutable(p: string): boolean {
|
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
export function fileExists(p: string): boolean {
|
|
105
|
-
return existsSync(p)
|
|
106
|
-
}
|
|
107
|
-
|
|
108
104
|
// Change-detection primitives (Output Policy in DESIGN.md): operations report
|
|
109
105
|
// changed:boolean so unchanged repeat runs log at verbose instead of [ok].
|
|
110
106
|
|
|
111
107
|
/** Write only when the content differs; returns whether a write happened. */
|
|
112
|
-
/** Add missing +x bits; returns whether a repair actually happened. */
|
|
113
|
-
export function ensureExecutable(path: string): boolean {
|
|
114
|
-
const mode = statSync(path).mode
|
|
115
|
-
const want = mode | 0o111
|
|
116
|
-
if (mode === want) return false
|
|
117
|
-
chmodSync(path, want)
|
|
118
|
-
return true
|
|
119
|
-
}
|
|
120
|
-
|
|
121
108
|
export function writeTextIfChanged(path: string, content: string): boolean {
|
|
122
109
|
if (existsSync(path) && readFileSync(path, "utf8") === content) return false
|
|
123
110
|
writeFileSync(path, content)
|