docks-kit 0.1.5 → 0.3.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 +1 -1
- package/README.md +7 -5
- package/cli/docs/flags.md +1 -0
- package/cli/docs/install.md +9 -11
- package/cli/docs/overview.md +6 -0
- package/cli/docs/platforms.md +9 -10
- package/cli/src/commands/model.ts +7 -3
- package/cli/src/commands/sync.ts +14 -3
- package/cli/src/commands/toolchain.ts +7 -3
- package/cli/src/engine-native/DESIGN.md +79 -2
- package/cli/src/engine-native/claudeModel.ts +9 -3
- package/cli/src/engine-native/claudeSync.ts +220 -116
- package/cli/src/engine-native/codexSync.ts +131 -77
- package/cli/src/engine-native/codexToml.ts +12 -5
- package/cli/src/engine-native/deps.ts +325 -0
- package/cli/src/engine-native/exec.ts +35 -2
- package/cli/src/engine-native/index.ts +48 -13
- package/cli/src/engine-native/logger.ts +35 -0
- package/cli/src/engine-native/models.ts +22 -23
- package/cli/src/engine-native/modes.ts +30 -14
- package/cli/src/engine-native/os.ts +29 -0
- package/cli/src/engine-native/parseArgs.ts +19 -17
- package/cli/src/engine-native/services.ts +96 -0
- package/cli/src/engine-native/skillsSync.ts +77 -61
- package/cli/src/engine-native/toolchain.ts +50 -68
- package/cli/src/engine.ts +11 -2
- package/cli/src/generated/sotPayload.ts +41 -0
- package/cli/src/kitHome.ts +15 -11
- package/cli/src/main.ts +3 -2
- package/cli/src/manifests.ts +17 -15
- package/cli/src/payload.ts +28 -0
- package/cli/src/services.ts +34 -0
- package/docks-kit +6 -6
- package/package.json +2 -3
- package/SoT/.agents/skills.txt +0 -14
- package/SoT/.claude/CLAUDE.md +0 -146
- package/SoT/.claude/fetch-usage.sh +0 -66
- package/SoT/.claude/hooks/notify.sh +0 -14
- package/SoT/.claude/mcp-servers.json +0 -10
- package/SoT/.claude/settings.json +0 -235
- package/SoT/.claude/statusline.sh +0 -175
- package/SoT/.codex/AGENTS.md +0 -75
- package/SoT/.codex/agents/.gitkeep +0 -1
- package/SoT/.codex/config.toml +0 -45
- package/SoT/.codex/plugins/marketplace.json +0 -50
- package/SoT/.codex/rules/docks.rules +0 -116
- package/SoT/models.json +0 -28
- package/SoT/toolchain.json +0 -27
- package/cli/src/engine-native/output.ts +0 -20
- package/notification.mp3 +0 -0
|
@@ -4,26 +4,26 @@
|
|
|
4
4
|
* change. Guard order, message strings, and backup behavior are golden-tested.
|
|
5
5
|
*/
|
|
6
6
|
import { spawnSync } from "node:child_process"
|
|
7
|
-
import { copyFileSync, existsSync, mkdirSync,
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
|
|
8
8
|
|
|
9
9
|
import { syncCodexModel, replaceTopLevelSettingInFile } from "./codexToml"
|
|
10
|
-
import {
|
|
10
|
+
import { p } from "./exec"
|
|
11
11
|
import type { Ctx } from "./index"
|
|
12
12
|
import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
|
|
13
|
-
import {
|
|
13
|
+
import { payloadBytes, payloadDisplayPath, payloadPaths, payloadText, type PayloadPath } from "../payload"
|
|
14
14
|
|
|
15
15
|
export function codexSync(ctx: Ctx): void {
|
|
16
16
|
const codexDir = p(ctx.home, ".codex")
|
|
17
|
-
const sotConfig =
|
|
17
|
+
const sotConfig = payloadText("SoT/.codex/config.toml")
|
|
18
18
|
const userConfig = p(codexDir, "config.toml")
|
|
19
19
|
|
|
20
20
|
ensureBubblewrap(ctx)
|
|
21
21
|
if (!ctx.dryRun) mkdirSync(codexDir, { recursive: true })
|
|
22
22
|
syncConfig(ctx, sotConfig, userConfig)
|
|
23
23
|
syncCodexModel(ctx, ctx.codexModel)
|
|
24
|
-
syncRules(ctx,
|
|
25
|
-
syncAgentsMd(ctx,
|
|
26
|
-
syncMarketplace(ctx,
|
|
24
|
+
syncRules(ctx, payloadPaths("SoT/.codex/rules/"), p(codexDir, "rules"))
|
|
25
|
+
syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"))
|
|
26
|
+
syncMarketplace(ctx, payloadText("SoT/.codex/plugins/marketplace.json"), p(ctx.agentsDir, "plugins", "marketplace.json"))
|
|
27
27
|
removeLegacyDocksMarketplace(ctx, userConfig)
|
|
28
28
|
syncPlugins(ctx, sotConfig)
|
|
29
29
|
}
|
|
@@ -31,14 +31,15 @@ export function codexSync(ctx: Ctx): void {
|
|
|
31
31
|
// ---------------------------------------------------------- bubblewrap ----
|
|
32
32
|
|
|
33
33
|
function ensureBubblewrap(ctx: Ctx): void {
|
|
34
|
-
|
|
34
|
+
const { change, echo, warn } = ctx.services.logger
|
|
35
|
+
if (!bwrapSupportedOs(ctx)) return
|
|
35
36
|
|
|
36
37
|
if (ctx.dryRun) {
|
|
37
38
|
echo("[dry-run] verify bubblewrap installed (recommended Codex Linux sandbox runtime)")
|
|
38
39
|
return
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
if (
|
|
42
|
+
if (ctx.services.deps.probe("bwrap").state === "present") return
|
|
42
43
|
|
|
43
44
|
if (ctx.skipRtk) {
|
|
44
45
|
warn(
|
|
@@ -47,7 +48,7 @@ function ensureBubblewrap(ctx: Ctx): void {
|
|
|
47
48
|
return
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
const pmInstall = bwrapDetectPmInstallCmd()
|
|
51
|
+
const pmInstall = bwrapDetectPmInstallCmd(ctx)
|
|
51
52
|
if (pmInstall === "") {
|
|
52
53
|
warn(
|
|
53
54
|
"bubblewrap not installed and no supported package manager found (apt-get/dnf/pacman/zypper). Codex may use its bundled helper if user namespaces work; install system bubblewrap manually when possible."
|
|
@@ -62,14 +63,13 @@ function ensureBubblewrap(ctx: Ctx): void {
|
|
|
62
63
|
return
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
if (
|
|
66
|
+
if (ctx.services.deps.probe("bwrap").state === "missing") {
|
|
66
67
|
warn("Package install reported success but bwrap not on PATH — check installation manually")
|
|
67
68
|
return
|
|
68
69
|
}
|
|
69
70
|
|
|
70
71
|
if (spawnSync("unshare", ["-Ur", "true"], { stdio: "ignore" }).status === 0) {
|
|
71
|
-
|
|
72
|
-
log(`bubblewrap installed and functional (${version})`)
|
|
72
|
+
change(`bubblewrap installed and functional (${ctx.services.deps.version("bwrap")})`)
|
|
73
73
|
} else {
|
|
74
74
|
warn(
|
|
75
75
|
"bubblewrap installed but unprivileged user namespaces appear blocked. On Ubuntu 24.04+, prefer loading the AppArmor bwrap-userns-restrict profile; fallback: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0"
|
|
@@ -77,25 +77,28 @@ function ensureBubblewrap(ctx: Ctx): void {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
function bwrapSupportedOs(): boolean {
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
function bwrapSupportedOs(ctx: Ctx): boolean {
|
|
81
|
+
const { warn } = ctx.services.logger
|
|
82
|
+
const pn = ctx.services.platform.name()
|
|
83
|
+
if (pn === "linux") return true
|
|
84
|
+
if (pn === "darwin" || pn === "windows") return false
|
|
83
85
|
warn("Unknown OS — skipping bubblewrap check; Codex sandbox may not work")
|
|
84
86
|
return false
|
|
85
87
|
}
|
|
86
88
|
|
|
87
|
-
function bwrapDetectPmInstallCmd(): string {
|
|
88
|
-
if (
|
|
89
|
-
if (
|
|
90
|
-
if (
|
|
91
|
-
if (
|
|
89
|
+
function bwrapDetectPmInstallCmd(ctx: Ctx): string {
|
|
90
|
+
if (ctx.services.deps.probe("apt-get").state === "present") return "sudo apt-get install -y bubblewrap"
|
|
91
|
+
if (ctx.services.deps.probe("dnf").state === "present") return "sudo dnf install -y bubblewrap"
|
|
92
|
+
if (ctx.services.deps.probe("pacman").state === "present") return "sudo pacman -S --noconfirm bubblewrap"
|
|
93
|
+
if (ctx.services.deps.probe("zypper").state === "present") return "sudo zypper install -y bubblewrap"
|
|
92
94
|
return ""
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
// --------------------------------------------------------------- config ----
|
|
96
98
|
|
|
97
|
-
function syncConfig(ctx: Ctx,
|
|
98
|
-
|
|
99
|
+
function syncConfig(ctx: Ctx, sotConfigText: string, userConfig: string): void {
|
|
100
|
+
const { change, echo, verbose } = ctx.services.logger
|
|
101
|
+
const sotConfig = payloadDisplayPath("SoT/.codex/config.toml", ctx.repoDir)
|
|
99
102
|
|
|
100
103
|
if (ctx.dryRun) {
|
|
101
104
|
echo(`[dry-run] merge ${sotConfig} -> ${userConfig}`)
|
|
@@ -103,18 +106,32 @@ function syncConfig(ctx: Ctx, sotConfig: string, userConfig: string): void {
|
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
if (!existsSync(userConfig)) {
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
writeFileSync(userConfig, sotConfigText)
|
|
110
|
+
change("Codex config installed")
|
|
111
|
+
ctx.nextStepTriggers.codexRestart = true
|
|
108
112
|
return
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
|
|
115
|
+
// Merge into a staging copy so `.bak` is written only when the config
|
|
116
|
+
// actually changes — an unconditional early backup lets a later no-op run
|
|
117
|
+
// overwrite the recovery copy with already-merged content.
|
|
118
|
+
const before = readFileSync(userConfig, "utf8")
|
|
119
|
+
const staging = `${userConfig}.merge.tmp`
|
|
120
|
+
writeFileSync(staging, before)
|
|
112
121
|
|
|
113
|
-
scrubDeprecatedFeatures(
|
|
114
|
-
mergeTopLevelSettings(
|
|
115
|
-
mergeTableSettings(
|
|
122
|
+
scrubDeprecatedFeatures(ctx, staging)
|
|
123
|
+
mergeTopLevelSettings(sotConfigText, staging)
|
|
124
|
+
mergeTableSettings(sotConfigText, staging)
|
|
116
125
|
|
|
117
|
-
|
|
126
|
+
if (readFileSync(staging, "utf8") === before) {
|
|
127
|
+
rmSync(staging, { force: true })
|
|
128
|
+
verbose("Codex config already in sync")
|
|
129
|
+
} else {
|
|
130
|
+
copyFileSync(userConfig, `${userConfig}.bak`)
|
|
131
|
+
renameSync(staging, userConfig)
|
|
132
|
+
change("Codex config merged (backup at config.toml.bak; user-only keys/tables preserved)")
|
|
133
|
+
ctx.nextStepTriggers.codexRestart = true
|
|
134
|
+
}
|
|
118
135
|
}
|
|
119
136
|
|
|
120
137
|
/** codex::scrub_deprecated_features — the [features].use_legacy_landlock awk pass. */
|
|
@@ -152,18 +169,19 @@ export function scrubDeprecatedFeaturesText(content: string): string {
|
|
|
152
169
|
return out
|
|
153
170
|
}
|
|
154
171
|
|
|
155
|
-
function scrubDeprecatedFeatures(userConfig: string): void {
|
|
172
|
+
function scrubDeprecatedFeatures(ctx: Ctx, userConfig: string): void {
|
|
173
|
+
const { change } = ctx.services.logger
|
|
156
174
|
if (!existsSync(userConfig)) return
|
|
157
175
|
const content = readFileSync(userConfig, "utf8")
|
|
158
176
|
if (!content.split("\n").some((l) => /^use_legacy_landlock[ \t]*=/.test(l))) return
|
|
159
177
|
|
|
160
178
|
writeFileSync(`${userConfig}.tmp`, scrubDeprecatedFeaturesText(content))
|
|
161
179
|
renameSync(`${userConfig}.tmp`, userConfig)
|
|
162
|
-
|
|
180
|
+
change("Codex: scrubbed deprecated [features].use_legacy_landlock")
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
function mergeTopLevelSettings(
|
|
166
|
-
for (const line of
|
|
183
|
+
function mergeTopLevelSettings(sotConfigText: string, userConfig: string): void {
|
|
184
|
+
for (const line of sotConfigText.split("\n")) {
|
|
167
185
|
if (line.startsWith("[")) break
|
|
168
186
|
if (/^[ \t]*($|#)/.test(line)) continue
|
|
169
187
|
if (!/^[A-Za-z0-9_.-]+[ \t]*=/.test(line)) continue
|
|
@@ -172,8 +190,8 @@ function mergeTopLevelSettings(sotConfig: string, userConfig: string): void {
|
|
|
172
190
|
}
|
|
173
191
|
}
|
|
174
192
|
|
|
175
|
-
function mergeTableSettings(
|
|
176
|
-
const sotLines =
|
|
193
|
+
function mergeTableSettings(sotConfigText: string, userConfig: string): void {
|
|
194
|
+
const sotLines = sotConfigText.split("\n")
|
|
177
195
|
for (const tableHeader of sotLines.filter((l) => /^\[[^\]]+\]/.test(l))) {
|
|
178
196
|
// Extract the SoT block: from the exact header line to (excluding) the
|
|
179
197
|
// next table header; `$(...)` strips trailing newlines.
|
|
@@ -208,8 +226,12 @@ function mergeTableSettings(sotConfig: string, userConfig: string): void {
|
|
|
208
226
|
|
|
209
227
|
// ------------------------------------------------------- rules + agents ----
|
|
210
228
|
|
|
211
|
-
function syncRules(ctx: Ctx,
|
|
212
|
-
|
|
229
|
+
function syncRules(ctx: Ctx, sotRules: ReadonlyArray<PayloadPath>, userRulesDir: string): void {
|
|
230
|
+
const { change, echo, verbose } = ctx.services.logger
|
|
231
|
+
const firstRule = sotRules[0]
|
|
232
|
+
if (firstRule === undefined) return
|
|
233
|
+
const firstDisplay = payloadDisplayPath(firstRule, ctx.repoDir)
|
|
234
|
+
const sotRulesDir = firstDisplay.slice(0, firstDisplay.lastIndexOf("/"))
|
|
213
235
|
|
|
214
236
|
if (ctx.dryRun) {
|
|
215
237
|
echo(`[dry-run] cp ${sotRulesDir}/*.rules -> ${userRulesDir}/`)
|
|
@@ -217,37 +239,49 @@ function syncRules(ctx: Ctx, sotRulesDir: string, userRulesDir: string): void {
|
|
|
217
239
|
}
|
|
218
240
|
|
|
219
241
|
mkdirSync(userRulesDir, { recursive: true })
|
|
220
|
-
let
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
.map((e) => p(sotRulesDir, e.name))
|
|
224
|
-
.sort(compareCodepoints)
|
|
242
|
+
let sawRules = false
|
|
243
|
+
let rulesChanged = false
|
|
244
|
+
const ruleFiles = sotRules.filter((path) => path.endsWith(".rules")).sort(compareCodepoints)
|
|
225
245
|
for (const ruleFile of ruleFiles) {
|
|
246
|
+
sawRules = true
|
|
226
247
|
const userRuleFile = p(userRulesDir, ruleFile.slice(ruleFile.lastIndexOf("/") + 1))
|
|
248
|
+
const content = payloadBytes(ruleFile)
|
|
249
|
+
const identical = existsSync(userRuleFile) && readFileSync(userRuleFile).equals(content)
|
|
250
|
+
if (identical) continue
|
|
227
251
|
if (existsSync(userRuleFile)) copyFileSync(userRuleFile, `${userRuleFile}.bak`)
|
|
228
|
-
|
|
229
|
-
|
|
252
|
+
writeFileSync(userRuleFile, content)
|
|
253
|
+
rulesChanged = true
|
|
230
254
|
}
|
|
231
|
-
if (
|
|
255
|
+
if (rulesChanged) {
|
|
256
|
+
change("Codex rules synced")
|
|
257
|
+
ctx.nextStepTriggers.codexRestart = true
|
|
258
|
+
} else if (sawRules) verbose("Codex rules already in sync")
|
|
232
259
|
}
|
|
233
260
|
|
|
234
|
-
function syncAgentsMd(ctx: Ctx,
|
|
235
|
-
|
|
261
|
+
function syncAgentsMd(ctx: Ctx, sotAgentsMdText: string, userAgentsMd: string): void {
|
|
262
|
+
const { change, echo, verbose } = ctx.services.logger
|
|
263
|
+
const sotAgentsMd = payloadDisplayPath("SoT/.codex/AGENTS.md", ctx.repoDir)
|
|
236
264
|
|
|
237
265
|
if (ctx.dryRun) {
|
|
238
266
|
echo(`[dry-run] cp ${sotAgentsMd} -> ${userAgentsMd}`)
|
|
239
267
|
return
|
|
240
268
|
}
|
|
241
269
|
|
|
270
|
+
if (existsSync(userAgentsMd) && readFileSync(userAgentsMd, "utf8") === sotAgentsMdText) {
|
|
271
|
+
verbose("Codex AGENTS.md already in sync")
|
|
272
|
+
return
|
|
273
|
+
}
|
|
242
274
|
if (existsSync(userAgentsMd)) copyFileSync(userAgentsMd, `${userAgentsMd}.bak`)
|
|
243
|
-
|
|
244
|
-
|
|
275
|
+
writeFileSync(userAgentsMd, sotAgentsMdText)
|
|
276
|
+
change("Codex AGENTS.md synced")
|
|
277
|
+
ctx.nextStepTriggers.codexRestart = true
|
|
245
278
|
}
|
|
246
279
|
|
|
247
280
|
// ---------------------------------------------------------- marketplace ----
|
|
248
281
|
|
|
249
|
-
function syncMarketplace(ctx: Ctx,
|
|
250
|
-
|
|
282
|
+
function syncMarketplace(ctx: Ctx, sotMarketplaceText: string, userMarketplace: string): void {
|
|
283
|
+
const { change, echo, err, verbose } = ctx.services.logger
|
|
284
|
+
const sotMarketplace = payloadDisplayPath("SoT/.codex/plugins/marketplace.json", ctx.repoDir)
|
|
251
285
|
|
|
252
286
|
if (ctx.dryRun) {
|
|
253
287
|
echo(`[dry-run] cp ${sotMarketplace} -> ${userMarketplace}`)
|
|
@@ -255,7 +289,7 @@ function syncMarketplace(ctx: Ctx, sotMarketplace: string, userMarketplace: stri
|
|
|
255
289
|
}
|
|
256
290
|
|
|
257
291
|
mkdirSync(p(ctx.agentsDir, "plugins"), { recursive: true })
|
|
258
|
-
const repo = parseJson(
|
|
292
|
+
const repo = parseJson(sotMarketplaceText)
|
|
259
293
|
if (repo === undefined) throw new Error(`invalid SoT marketplace JSON: ${sotMarketplace}`)
|
|
260
294
|
|
|
261
295
|
if (existsSync(userMarketplace)) {
|
|
@@ -264,13 +298,20 @@ function syncMarketplace(ctx: Ctx, sotMarketplace: string, userMarketplace: stri
|
|
|
264
298
|
err(`Skipping marketplace sync: ${userMarketplace} is not valid JSON. Fix or delete it.`)
|
|
265
299
|
return
|
|
266
300
|
}
|
|
301
|
+
const out = jqStringify(mergeMarketplace(repo, user))
|
|
302
|
+
if (out === readFileSync(userMarketplace, "utf8")) {
|
|
303
|
+
verbose("Codex marketplace already in sync")
|
|
304
|
+
return
|
|
305
|
+
}
|
|
267
306
|
copyFileSync(userMarketplace, `${userMarketplace}.bak`)
|
|
268
|
-
writeFileSync(`${userMarketplace}.tmp`,
|
|
307
|
+
writeFileSync(`${userMarketplace}.tmp`, out)
|
|
269
308
|
renameSync(`${userMarketplace}.tmp`, userMarketplace)
|
|
270
|
-
|
|
309
|
+
change("Codex marketplace merged (backup at marketplace.json.bak)")
|
|
310
|
+
ctx.nextStepTriggers.codexRestart = true
|
|
271
311
|
} else {
|
|
272
|
-
|
|
273
|
-
|
|
312
|
+
writeFileSync(userMarketplace, sotMarketplaceText)
|
|
313
|
+
change("Codex marketplace installed")
|
|
314
|
+
ctx.nextStepTriggers.codexRestart = true
|
|
274
315
|
}
|
|
275
316
|
}
|
|
276
317
|
|
|
@@ -326,39 +367,42 @@ export function marketplaceSource(marketplace: string, configFile: string): stri
|
|
|
326
367
|
}
|
|
327
368
|
|
|
328
369
|
function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): void {
|
|
370
|
+
const { change, echo, warn } = ctx.services.logger
|
|
329
371
|
if (ctx.dryRun) {
|
|
330
372
|
echo("[dry-run] remove legacy configured Codex Docks marketplace when personal marketplace is deployed")
|
|
331
373
|
return
|
|
332
374
|
}
|
|
333
375
|
|
|
334
|
-
if (
|
|
376
|
+
if (ctx.services.deps.probe("codex").state === "missing") return
|
|
335
377
|
|
|
336
378
|
const source = marketplaceSource("docks", userConfig)
|
|
337
379
|
if (source !== "https://github.com/DocksDocks/docks.git" && source !== "DocksDocks/docks") return
|
|
338
380
|
const res = spawnSync("codex", ["plugin", "marketplace", "remove", "docks"], { stdio: "ignore" })
|
|
339
381
|
if (res.error === undefined && res.status === 0) {
|
|
340
|
-
|
|
382
|
+
change("Removed legacy configured Codex Docks marketplace; using personal marketplace file")
|
|
383
|
+
ctx.nextStepTriggers.codexRestart = true
|
|
341
384
|
} else {
|
|
342
385
|
warn("Failed to remove legacy configured Codex Docks marketplace")
|
|
343
386
|
}
|
|
344
387
|
}
|
|
345
388
|
|
|
346
389
|
/** codex::_standalone_install_command — per-OS official standalone installer. */
|
|
347
|
-
const standaloneInstallCommand = (): string =>
|
|
348
|
-
process.platform === "win32"
|
|
349
|
-
? `powershell -ExecutionPolicy ByPass -c "irm https://chatgpt.com/codex/install.ps1 | iex"`
|
|
350
|
-
: 'tmp=$(mktemp) && curl -fsSL https://chatgpt.com/codex/install.sh -o "$tmp" && CODEX_NON_INTERACTIVE=1 sh "$tmp"'
|
|
390
|
+
const standaloneInstallCommand = (ctx: Ctx): string => ctx.services.deps.spec("codex").installHint()
|
|
351
391
|
|
|
352
392
|
/** codex::_enabled_plugin_ids — [plugins."<id>"] tables with enabled = true. */
|
|
353
393
|
export function enabledPluginIds(configFile: string): Array<string> {
|
|
354
394
|
if (!existsSync(configFile)) return []
|
|
395
|
+
return enabledPluginIdsFromText(readFileSync(configFile, "utf8"))
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function enabledPluginIdsFromText(configText: string): Array<string> {
|
|
355
399
|
const ids: Array<string> = []
|
|
356
400
|
let plugin = ""
|
|
357
401
|
let enabled = false
|
|
358
402
|
const flush = (): void => {
|
|
359
403
|
if (plugin !== "" && enabled) ids.push(plugin)
|
|
360
404
|
}
|
|
361
|
-
for (const line of
|
|
405
|
+
for (const line of configText.split("\n")) {
|
|
362
406
|
const m = /^\[plugins\."([^"]+)"\][ \t]*$/.exec(line)
|
|
363
407
|
if (m !== null) {
|
|
364
408
|
flush()
|
|
@@ -380,39 +424,43 @@ export function enabledPluginIds(configFile: string): Array<string> {
|
|
|
380
424
|
return ids
|
|
381
425
|
}
|
|
382
426
|
|
|
383
|
-
function manualPluginRefreshCommand(
|
|
384
|
-
const first =
|
|
427
|
+
function manualPluginRefreshCommand(sotConfigText: string): string {
|
|
428
|
+
const first = enabledPluginIdsFromText(sotConfigText)[0]
|
|
385
429
|
return first !== undefined ? `codex plugin add ${first}` : "codex plugin add <plugin@marketplace>"
|
|
386
430
|
}
|
|
387
431
|
|
|
388
|
-
function syncPlugins(ctx: Ctx,
|
|
432
|
+
function syncPlugins(ctx: Ctx, sotConfigText: string): void {
|
|
433
|
+
const { change, echo, warn } = ctx.services.logger
|
|
389
434
|
if (ctx.dryRun) {
|
|
390
435
|
echo("[dry-run] add enabled Codex plugins from SoT")
|
|
391
436
|
return
|
|
392
437
|
}
|
|
393
438
|
|
|
394
|
-
if (
|
|
439
|
+
if (ctx.services.deps.probe("codex").state === "missing") {
|
|
395
440
|
warn(
|
|
396
|
-
`codex CLI not in PATH - deployed config/marketplace only; install Codex with: ${standaloneInstallCommand()} | docs: https://developers.openai.com/codex/cli; then run: ${manualPluginRefreshCommand(
|
|
441
|
+
`codex CLI not in PATH - deployed config/marketplace only; install Codex with: ${standaloneInstallCommand(ctx)} | docs: https://developers.openai.com/codex/cli; then run: ${manualPluginRefreshCommand(sotConfigText)}`
|
|
397
442
|
)
|
|
398
443
|
return
|
|
399
444
|
}
|
|
400
|
-
if (
|
|
401
|
-
|
|
402
|
-
|
|
445
|
+
if (ctx.services.deps.probe("git").state === "missing") {
|
|
446
|
+
ctx.services.deps.warnMissing(
|
|
447
|
+
"git",
|
|
448
|
+
ctx.services.logger,
|
|
449
|
+
"plugin marketplaces are git repos — Codex plugin refresh skipped; re-run sync after installing"
|
|
450
|
+
)
|
|
403
451
|
return
|
|
404
452
|
}
|
|
405
453
|
|
|
406
454
|
let refreshed = 0
|
|
407
455
|
let failed = 0
|
|
408
|
-
for (const pluginId of
|
|
456
|
+
for (const pluginId of enabledPluginIdsFromText(sotConfigText)) {
|
|
409
457
|
const res = spawnSync("codex", ["plugin", "add", pluginId], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })
|
|
410
458
|
const addOut = `${res.stdout ?? ""}${res.stderr ?? ""}`
|
|
411
459
|
if (res.error === undefined && res.status === 0) {
|
|
412
460
|
refreshed++
|
|
413
461
|
} else if (addOut.includes("could not find a Codex CLI binary")) {
|
|
414
462
|
warn(
|
|
415
|
-
`Codex plugin refresh hit a stale launcher/wrapper on PATH - install current standalone Codex with: ${standaloneInstallCommand()}`
|
|
463
|
+
`Codex plugin refresh hit a stale launcher/wrapper on PATH - install current standalone Codex with: ${standaloneInstallCommand(ctx)}`
|
|
416
464
|
)
|
|
417
465
|
failed++
|
|
418
466
|
} else {
|
|
@@ -424,13 +472,17 @@ function syncPlugins(ctx: Ctx, sotConfig: string): void {
|
|
|
424
472
|
}
|
|
425
473
|
}
|
|
426
474
|
|
|
427
|
-
if (refreshed > 0)
|
|
475
|
+
if (refreshed > 0) {
|
|
476
|
+
change(`Codex plugins synced (plugins: ~${refreshed})`)
|
|
477
|
+
ctx.nextStepTriggers.codexRestart = true
|
|
478
|
+
}
|
|
428
479
|
if (failed > 0) warn(`${failed} Codex plugin operation(s) failed — re-run sync or install manually`)
|
|
429
480
|
}
|
|
430
481
|
|
|
431
482
|
// -------------------------------------------------------------- summary ----
|
|
432
483
|
|
|
433
484
|
export function codexSummary(ctx: Ctx): void {
|
|
485
|
+
const { echo } = ctx.services.logger
|
|
434
486
|
const codexDir = p(ctx.home, ".codex")
|
|
435
487
|
echo(`Codex: ${codexDir}`)
|
|
436
488
|
if (!ctx.dryRun) {
|
|
@@ -439,6 +491,8 @@ export function codexSummary(ctx: Ctx): void {
|
|
|
439
491
|
}
|
|
440
492
|
}
|
|
441
493
|
|
|
442
|
-
export function codexNextSteps():
|
|
443
|
-
|
|
494
|
+
export function codexNextSteps(ctx: Ctx): Array<string> {
|
|
495
|
+
return ctx.verbose || ctx.nextStepTriggers.codexRestart
|
|
496
|
+
? ["Restart Codex to load any refreshed plugins, skills, or tools."]
|
|
497
|
+
: []
|
|
444
498
|
}
|
|
@@ -8,7 +8,6 @@ import { p } from "./exec"
|
|
|
8
8
|
import { readFileSync, renameSync, writeFileSync } from "node:fs"
|
|
9
9
|
|
|
10
10
|
import type { Ctx } from "./index"
|
|
11
|
-
import { echo, log, warn } from "./output"
|
|
12
11
|
|
|
13
12
|
export function replaceTopLevelSetting(content: string, key: string, replacement: string): string {
|
|
14
13
|
const lines = content.split("\n")
|
|
@@ -40,13 +39,17 @@ export function replaceTopLevelSetting(content: string, key: string, replacement
|
|
|
40
39
|
return `${out.join("\n")}\n`
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
export function replaceTopLevelSettingInFile(file: string, key: string, replacement: string):
|
|
44
|
-
const
|
|
42
|
+
export function replaceTopLevelSettingInFile(file: string, key: string, replacement: string): boolean {
|
|
43
|
+
const before = readFileSync(file, "utf8")
|
|
44
|
+
const next = replaceTopLevelSetting(before, key, replacement)
|
|
45
|
+
if (next === before) return false
|
|
45
46
|
writeFileSync(`${file}.tmp`, next)
|
|
46
47
|
renameSync(`${file}.tmp`, file)
|
|
48
|
+
return true
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export function syncCodexModel(ctx: Ctx, model: string): void {
|
|
52
|
+
const { change, echo, verbose, warn } = ctx.services.logger
|
|
50
53
|
const userCodexSettings = p(ctx.home, ".codex", "config.toml")
|
|
51
54
|
|
|
52
55
|
if (model === "") return
|
|
@@ -62,6 +65,10 @@ export function syncCodexModel(ctx: Ctx, model: string): void {
|
|
|
62
65
|
warn(`(--codex-model) ${userCodexSettings} missing — skipped`)
|
|
63
66
|
return
|
|
64
67
|
}
|
|
65
|
-
replaceTopLevelSettingInFile(userCodexSettings, "model", `model = "${model}"`)
|
|
66
|
-
|
|
68
|
+
if (replaceTopLevelSettingInFile(userCodexSettings, "model", `model = "${model}"`)) {
|
|
69
|
+
change(`Model: deployed Codex model set to ${model} (SoT unchanged; flag-less sync reverts)`)
|
|
70
|
+
ctx.nextStepTriggers.codexRestart = true
|
|
71
|
+
} else {
|
|
72
|
+
verbose(`Model: deployed Codex model already ${model}`)
|
|
73
|
+
}
|
|
67
74
|
}
|