docks-kit 0.17.1 → 0.17.3

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.
Files changed (87) hide show
  1. package/AGENTS.md +41 -33
  2. package/cli/docs/omp-context.md +104 -0
  3. package/cli/docs/omp-models.md +106 -56
  4. package/cli/src/argv.ts +38 -469
  5. package/cli/src/argvSurface.ts +371 -0
  6. package/cli/src/argvValidate.ts +122 -0
  7. package/cli/src/commands/docs.ts +62 -42
  8. package/cli/src/commands/harnesses.ts +38 -45
  9. package/cli/src/commands/model.ts +48 -53
  10. package/cli/src/commands/models.ts +24 -26
  11. package/cli/src/commands/omp.ts +132 -112
  12. package/cli/src/commands/plugins.ts +20 -20
  13. package/cli/src/commands/skills.ts +22 -20
  14. package/cli/src/commands/status.ts +87 -80
  15. package/cli/src/commands/sync.ts +113 -87
  16. package/cli/src/commands/toolchain.ts +21 -21
  17. package/cli/src/commands/update.ts +139 -117
  18. package/cli/src/efforts.ts +35 -34
  19. package/cli/src/engine-native/DESIGN.md +30 -30
  20. package/cli/src/engine-native/bun.ts +77 -55
  21. package/cli/src/engine-native/claudeLsp.ts +154 -0
  22. package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
  23. package/cli/src/engine-native/claudePluginPasses.ts +348 -0
  24. package/cli/src/engine-native/claudeRemovals.ts +195 -0
  25. package/cli/src/engine-native/claudeRetired.ts +4 -4
  26. package/cli/src/engine-native/claudeRuntime.ts +94 -68
  27. package/cli/src/engine-native/claudeSettings.ts +174 -0
  28. package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
  29. package/cli/src/engine-native/claudeSync.ts +176 -456
  30. package/cli/src/engine-native/codexConfig.ts +158 -0
  31. package/cli/src/engine-native/codexHooks.ts +91 -0
  32. package/cli/src/engine-native/codexPlugins.ts +290 -0
  33. package/cli/src/engine-native/codexStatus.ts +24 -0
  34. package/cli/src/engine-native/codexSync.ts +110 -568
  35. package/cli/src/engine-native/codexToml.ts +140 -138
  36. package/cli/src/engine-native/deps.ts +144 -107
  37. package/cli/src/engine-native/engineCtx.ts +126 -0
  38. package/cli/src/engine-native/exec.ts +98 -87
  39. package/cli/src/engine-native/failures.ts +3 -3
  40. package/cli/src/engine-native/harnesses.ts +62 -72
  41. package/cli/src/engine-native/index.ts +34 -315
  42. package/cli/src/engine-native/jq.ts +25 -24
  43. package/cli/src/engine-native/logger.ts +98 -100
  44. package/cli/src/engine-native/models.ts +50 -45
  45. package/cli/src/engine-native/modes.ts +83 -80
  46. package/cli/src/engine-native/ompFileDeploy.ts +111 -0
  47. package/cli/src/engine-native/ompMarketplace.ts +136 -0
  48. package/cli/src/engine-native/ompOverlay.ts +67 -64
  49. package/cli/src/engine-native/ompPaths.ts +49 -38
  50. package/cli/src/engine-native/ompPlugins.ts +170 -0
  51. package/cli/src/engine-native/ompRemovals.ts +159 -0
  52. package/cli/src/engine-native/ompSync.ts +69 -409
  53. package/cli/src/engine-native/ompYaml.ts +43 -41
  54. package/cli/src/engine-native/os/darwin.ts +5 -5
  55. package/cli/src/engine-native/os/index.ts +15 -15
  56. package/cli/src/engine-native/os/linux.ts +5 -5
  57. package/cli/src/engine-native/os/posix.ts +28 -24
  58. package/cli/src/engine-native/os/targets.ts +17 -17
  59. package/cli/src/engine-native/os/types.ts +31 -31
  60. package/cli/src/engine-native/os/windows.ts +61 -61
  61. package/cli/src/engine-native/parseArgs.ts +122 -372
  62. package/cli/src/engine-native/parseHelp.ts +66 -0
  63. package/cli/src/engine-native/parseModifiers.ts +222 -0
  64. package/cli/src/engine-native/services.ts +34 -34
  65. package/cli/src/engine-native/settings.ts +15 -15
  66. package/cli/src/engine-native/sharedTypes.d.ts +46 -0
  67. package/cli/src/engine-native/skillsInstall.ts +105 -0
  68. package/cli/src/engine-native/skillsLinks.ts +203 -0
  69. package/cli/src/engine-native/skillsManifest.ts +16 -0
  70. package/cli/src/engine-native/skillsPrune.ts +108 -0
  71. package/cli/src/engine-native/skillsSync.ts +28 -362
  72. package/cli/src/engine-native/syncConcurrency.ts +55 -0
  73. package/cli/src/engine-native/syncDispatch.ts +141 -0
  74. package/cli/src/engine-native/toolchain.ts +65 -62
  75. package/cli/src/engine.ts +65 -57
  76. package/cli/src/generated/sotPayload.ts +7 -7
  77. package/cli/src/kitHome.ts +44 -40
  78. package/cli/src/main.ts +63 -56
  79. package/cli/src/manifests.ts +53 -54
  80. package/cli/src/md.d.ts +2 -2
  81. package/cli/src/payload.ts +9 -9
  82. package/cli/src/services.ts +21 -13
  83. package/cli/tsconfig.json +4 -1
  84. package/docks-kit +13 -2
  85. package/docks-kit.ps1 +21 -6
  86. package/package.json +10 -2
  87. package/cli/src/engine-native/claudePlugins.ts +0 -510
@@ -3,633 +3,175 @@
3
3
  * avoid a TOML library because reformatting user configs would be a behavior
4
4
  * change. Guard order, message strings, and backup behavior are golden-tested.
5
5
  */
6
- import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs"
7
-
8
- import { mergeTableSettings, mergeTopLevelSettings, syncCodexEffort, syncCodexModel } from "./codexToml"
9
- import { p, spawnProcess } from "./exec"
10
- import { recordFailure } from "./failures"
11
- import type { Ctx } from "./index"
12
- import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq"
13
- import { hostOs } from "./os"
14
- import { payloadBytes, payloadDisplayPath, payloadPaths, payloadText, type PayloadPath } from "../payload"
6
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+
8
+ import {
9
+ payloadBytes,
10
+ payloadDisplayPath,
11
+ payloadPaths,
12
+ payloadText,
13
+ type PayloadPath,
14
+ } from "../payload";
15
+ import { syncConfig } from "./codexConfig";
16
+ import { removeRetiredImportedHooks } from "./codexHooks";
17
+ import { removeLegacyDocksMarketplace, syncMarketplace, syncPlugins } from "./codexPlugins";
18
+ import { syncCodexEffort, syncCodexModel } from "./codexToml";
19
+ import { p, spawnProcess } from "./exec";
20
+ import type { Ctx } from "./index";
21
+ import { compareCodepoints } from "./jq";
22
+ import { hostOs } from "./os";
23
+
24
+ export { removeRetiredPluginTablesText } from "./codexConfig";
25
+ export { codexNextSteps, codexSummary } from "./codexStatus";
15
26
 
16
27
  export async function codexSync(ctx: Ctx): Promise<void> {
17
- const codexDir = p(ctx.home, ".codex")
18
- const sotConfig = payloadText("SoT/.codex/config.toml")
19
- const userConfig = p(codexDir, "config.toml")
20
-
21
- await ensureBubblewrap(ctx)
22
- if (!ctx.dryRun) mkdirSync(codexDir, { recursive: true })
23
- syncConfig(ctx, sotConfig, userConfig)
24
- removeRetiredImportedHooks(ctx, codexDir)
25
- syncCodexModel(ctx, ctx.codexModel)
26
- syncCodexEffort(ctx, ctx.codexEffort)
27
- syncRules(ctx, payloadPaths("SoT/.codex/rules/"), p(codexDir, "rules"))
28
- syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"))
29
- syncMarketplace(ctx, payloadText("SoT/.codex/plugins/marketplace.json"), p(ctx.agentsDir, "plugins", "marketplace.json"))
30
- await removeLegacyDocksMarketplace(ctx, userConfig)
31
- await syncPlugins(ctx, sotConfig)
32
- }
33
-
34
- const RETIRED_CONTEXT_HOOK = `echo "[CONTEXT] Current date: $(date '+%A, %Y-%m-%d %H:%M:%S %Z')"`
35
- const RETIRED_SKILLS_HOOK = `SKILL_COUNT=$(find .claude/skills -name 'SKILL.md' -mindepth 2 -maxdepth 2 2>/dev/null | wc -l); [ "$SKILL_COUNT" -gt 0 ] && echo "[SKILLS] $SKILL_COUNT project skills available in .claude/skills/. Claude Code loads them on demand via Skill tool. After code changes affecting documented patterns, update the relevant skill and its metadata.updated field." || true`
36
- const RETIRED_CONFIG_HOOKS = new Set([
37
- `echo "[CONFIG] Context: $([ \\\"\${CLAUDE_CODE_DISABLE_1M_CONTEXT:-0}\\\" = \\\"1\\\" ] && echo '200K' || echo '1M') | Compact-window: \${CLAUDE_CODE_AUTO_COMPACT_WINDOW:-full} | Effort: \${CLAUDE_CODE_EFFORT_LEVEL:-high} | Thinking: adaptive | Subagent: \${CLAUDE_CODE_SUBAGENT_MODEL:-default}"`,
38
- `echo "[CONFIG] Context: $([ \\\"\${CLAUDE_CODE_DISABLE_1M_CONTEXT:-0}\\\" = \\\"1\\\" ] && echo '200K' || echo '1M') | Compact-window: \${CLAUDE_CODE_AUTO_COMPACT_WINDOW:-full} | Effort: \${CLAUDE_CODE_EFFORT_LEVEL:-high} | Thinking: adaptive | Model: \${ANTHROPIC_DEFAULT_OPUS_MODEL:-default} | Subagent: \${CLAUDE_CODE_SUBAGENT_MODEL:-default}"`,
39
- `echo "[CONFIG] Context: $([ \\\"\${CLAUDE_CODE_DISABLE_1M_CONTEXT:-0}\\\" = \\\"1\\\" ] && echo '200K' || echo '1M') | Compact-window: \${CLAUDE_CODE_AUTO_COMPACT_WINDOW:-full} | Effort: \${CLAUDE_CODE_EFFORT_LEVEL:-$(jq -r .effortLevel $HOME/.claude/settings.json 2>/dev/null || echo default)} | Thinking: adaptive | Subagent: \${CLAUDE_CODE_SUBAGENT_MODEL:-default}"`
40
- ])
41
-
42
- function isRetiredImportedSessionStartHook(handler: Json, codexDir: string): boolean {
43
- if (!isObject(handler) || handler["type"] !== "command" || typeof handler["command"] !== "string") return false
44
- const command = handler["command"]
45
- const connectorScript = p(codexDir, "hooks", "disable-claudeai-connectors.sh")
46
- const retiredConnectorHook = `[ -x '${connectorScript}' ] && '${connectorScript}' || true`
47
- return command === RETIRED_CONTEXT_HOOK || command === RETIRED_SKILLS_HOOK || RETIRED_CONFIG_HOOKS.has(command) || command === retiredConnectorHook
48
- }
49
-
50
- function withoutRetiredImportedHooks(doc: { [key: string]: Json }, codexDir: string): { readonly value: Json; readonly removed: number } {
51
- const hooks = doc["hooks"]
52
- if (!isObject(hooks) || !Array.isArray(hooks["SessionStart"])) return { value: doc, removed: 0 }
53
-
54
- let removed = 0
55
- const groups: Array<Json> = []
56
- for (const group of hooks["SessionStart"]) {
57
- if (!isObject(group) || !Array.isArray(group["hooks"])) {
58
- groups.push(group)
59
- continue
60
- }
61
- const kept = group["hooks"].filter((handler) => {
62
- if (!isRetiredImportedSessionStartHook(handler, codexDir)) return true
63
- removed++
64
- return false
65
- })
66
- if (kept.length > 0) groups.push({ ...group, hooks: kept })
67
- }
68
- if (removed === 0) return { value: doc, removed: 0 }
69
-
70
- const nextHooks = { ...hooks }
71
- if (groups.length > 0) nextHooks["SessionStart"] = groups
72
- else delete nextHooks["SessionStart"]
73
- return { value: { ...doc, hooks: nextHooks }, removed }
74
- }
75
-
76
- /** Remove only Claude hooks that docks-kit retired before Codex could import them safely. */
77
- function removeRetiredImportedHooks(ctx: Ctx, codexDir: string): void {
78
- const { change, echo, warn } = ctx.services.logger
79
- const hooksFile = p(codexDir, "hooks.json")
80
- if (!existsSync(hooksFile)) return
81
- const before = readFileSync(hooksFile, "utf8")
82
- const doc = parseJson(before)
83
- if (doc === undefined || !isObject(doc)) {
84
- warn(`Codex hooks file is not a valid JSON object; retired hook cleanup skipped: ${hooksFile}`)
85
- return
86
- }
87
- const result = withoutRetiredImportedHooks(doc, codexDir)
88
- if (result.removed === 0) return
89
-
90
- if (ctx.dryRun) {
91
- echo(`[dry-run] remove ${result.removed} retired imported docks-kit SessionStart hook(s) from ${hooksFile}`)
92
- return
93
- }
94
-
95
- copyFileSync(hooksFile, `${hooksFile}.bak`)
96
- writeFileSync(`${hooksFile}.tmp`, jqStringify(result.value))
97
- renameSync(`${hooksFile}.tmp`, hooksFile)
98
- change(`Codex: removed ${result.removed} retired imported docks-kit SessionStart hook(s) (backup at hooks.json.bak)`)
99
- ctx.nextStepTriggers.codexRestart = true
28
+ const codexDir = p(ctx.home, ".codex");
29
+ const sotConfig = payloadText("SoT/.codex/config.toml");
30
+ const userConfig = p(codexDir, "config.toml");
31
+
32
+ await ensureBubblewrap(ctx);
33
+ if (!ctx.dryRun) mkdirSync(codexDir, { recursive: true });
34
+ syncConfig(ctx, sotConfig, userConfig);
35
+ removeRetiredImportedHooks(ctx, codexDir);
36
+ syncCodexModel(ctx, ctx.codexModel);
37
+ syncCodexEffort(ctx, ctx.codexEffort);
38
+ syncRules(ctx, payloadPaths("SoT/.codex/rules/"), p(codexDir, "rules"));
39
+ syncAgentsMd(ctx, payloadText("SoT/.codex/AGENTS.md"), p(codexDir, "AGENTS.md"));
40
+ syncMarketplace(
41
+ ctx,
42
+ payloadText("SoT/.codex/plugins/marketplace.json"),
43
+ p(ctx.agentsDir, "plugins", "marketplace.json"),
44
+ );
45
+ await removeLegacyDocksMarketplace(ctx, userConfig);
46
+ await syncPlugins(ctx, sotConfig);
100
47
  }
101
48
 
102
49
  // ---------------------------------------------------------- bubblewrap ----
103
50
 
104
51
  async function ensureBubblewrap(ctx: Ctx): Promise<void> {
105
- const { change, echo, warn } = ctx.services.logger
106
- if (!bwrapSupportedOs(ctx)) return
52
+ const { change, echo, warn } = ctx.services.logger;
53
+ if (!bwrapSupportedOs(ctx)) return;
107
54
 
108
55
  if (ctx.dryRun) {
109
- echo("[dry-run] verify bubblewrap installed (recommended Codex Linux sandbox runtime)")
110
- return
56
+ echo("[dry-run] verify bubblewrap installed (recommended Codex Linux sandbox runtime)");
57
+ return;
111
58
  }
112
59
 
113
- if (ctx.services.deps.probe("bwrap").state === "present") return
60
+ if (ctx.services.deps.probe("bwrap").state === "present") return;
114
61
 
115
62
  if (ctx.skipBubblewrap) {
116
63
  warn(
117
- "bubblewrap not installed (--skip-bubblewrap skips auto-install). Codex may use its bundled helper if user namespaces work; recommended install: sudo apt install -y bubblewrap"
118
- )
119
- return
64
+ "bubblewrap not installed (--skip-bubblewrap skips auto-install). Codex may use its bundled helper if user namespaces work; recommended install: sudo apt install -y bubblewrap",
65
+ );
66
+ return;
120
67
  }
121
68
 
122
- const pmInstall = bwrapDetectPmInstallCmd(ctx)
69
+ const pmInstall = bwrapDetectPmInstallCmd(ctx);
123
70
  if (pmInstall === "") {
124
71
  warn(
125
- "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."
126
- )
127
- return
72
+ "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.",
73
+ );
74
+ return;
128
75
  }
129
76
 
130
- warn(`bubblewrap not installed - recommended for Codex Linux sandbox. Running: ${pmInstall} (sudo prompt may appear)`)
77
+ warn(
78
+ `bubblewrap not installed - recommended for Codex Linux sandbox. Running: ${pmInstall} (sudo prompt may appear)`,
79
+ );
131
80
  const runInstaller = () =>
132
- spawnProcess("bash", ["-c", pmInstall], { stdio: ["inherit", "inherit", "inherit"] })
133
- const res = await (ctx.terminalLease?.withExclusive(runInstaller) ?? runInstaller())
81
+ spawnProcess("bash", ["-c", pmInstall], { stdio: ["inherit", "inherit", "inherit"] });
82
+ const res = await (ctx.terminalLease?.withExclusive(runInstaller) ?? runInstaller());
134
83
  if (res.exitCode !== 0) {
135
- warn(`Failed to auto-install bubblewrap. Install manually: ${pmInstall}`)
136
- return
84
+ warn(`Failed to auto-install bubblewrap. Install manually: ${pmInstall}`);
85
+ return;
137
86
  }
138
87
 
139
88
  if (ctx.services.deps.probe("bwrap").state === "missing") {
140
- warn("Package install reported success but bwrap not on PATH — check installation manually")
141
- return
89
+ warn("Package install reported success but bwrap not on PATH — check installation manually");
90
+ return;
142
91
  }
143
92
 
144
- const namespaceProbe = await spawnProcess("unshare", ["-Ur", "true"], { stdio: "ignore" })
93
+ const namespaceProbe = await spawnProcess("unshare", ["-Ur", "true"], { stdio: "ignore" });
145
94
  if (namespaceProbe.error !== undefined) {
146
- warn(`Could not run unshare to check user namespaces: ${namespaceProbe.error.message}`)
95
+ warn(`Could not run unshare to check user namespaces: ${namespaceProbe.error.message}`);
147
96
  } else if (namespaceProbe.exitCode === 0) {
148
- change(`bubblewrap installed and functional (${await ctx.services.deps.version("bwrap")})`)
97
+ change(`bubblewrap installed and functional (${await ctx.services.deps.version("bwrap")})`);
149
98
  } else {
150
99
  warn(
151
- "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"
152
- )
100
+ "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",
101
+ );
153
102
  }
154
103
  }
155
104
 
156
105
  function bwrapSupportedOs(ctx: Ctx): boolean {
157
- const { warn } = ctx.services.logger
158
- const os = hostOs(ctx.services.platform.name())
159
- if (os.supportsBubblewrap) return true
106
+ const { warn } = ctx.services.logger;
107
+ const os = hostOs(ctx.services.platform.name());
108
+ if (os.supportsBubblewrap) return true;
160
109
  if (os.id === "unknown") {
161
- warn("Unknown OS — skipping bubblewrap check; Codex sandbox may not work")
110
+ warn("Unknown OS — skipping bubblewrap check; Codex sandbox may not work");
162
111
  }
163
- return false
112
+ return false;
164
113
  }
165
114
 
166
115
  function bwrapDetectPmInstallCmd(ctx: Ctx): string {
167
- if (ctx.services.deps.probe("apt-get").state === "present") return "sudo apt-get install -y bubblewrap"
168
- if (ctx.services.deps.probe("dnf").state === "present") return "sudo dnf install -y bubblewrap"
169
- if (ctx.services.deps.probe("pacman").state === "present") return "sudo pacman -S --noconfirm bubblewrap"
170
- if (ctx.services.deps.probe("zypper").state === "present") return "sudo zypper install -y bubblewrap"
171
- return ""
172
- }
173
-
174
- // --------------------------------------------------------------- config ----
175
-
176
- function syncConfig(ctx: Ctx, sotConfigText: string, userConfig: string): void {
177
- const { change, echo, verbose } = ctx.services.logger
178
- const sotConfig = payloadDisplayPath("SoT/.codex/config.toml")
179
-
180
- if (ctx.dryRun) {
181
- if (existsSync(userConfig)) {
182
- echo(`[dry-run] merge ${sotConfig} -> ${userConfig}`)
183
- } else {
184
- echo(`[dry-run] install ${sotConfig} -> ${userConfig}`)
185
- }
186
- return
187
- }
188
-
189
- if (!existsSync(userConfig)) {
190
- writeFileSync(userConfig, sotConfigText)
191
- change("Codex config installed")
192
- ctx.nextStepTriggers.codexRestart = true
193
- return
194
- }
195
-
196
- // Merge into a staging copy so `.bak` is written only when the config
197
- // actually changes — an unconditional early backup lets a later no-op run
198
- // overwrite the recovery copy with already-merged content.
199
- const before = readFileSync(userConfig, "utf8")
200
- const staging = `${userConfig}.merge.tmp`
201
- // Normalize once before record transforms because CR bytes change table-header identity.
202
- writeFileSync(staging, before.replace(/\r\n/g, "\n"))
203
-
204
- scrubDeprecatedFeatures(ctx, staging)
205
- removeRetiredPluginTables(ctx, staging)
206
- mergeTopLevelSettings(sotConfigText, staging)
207
- mergeTableSettings(sotConfigText, staging)
208
-
209
- if (readFileSync(staging, "utf8") === before) {
210
- rmSync(staging, { force: true })
211
- verbose("Codex config already in sync")
212
- } else {
213
- copyFileSync(userConfig, `${userConfig}.bak`)
214
- renameSync(staging, userConfig)
215
- change("Codex config merged (backup at config.toml.bak; user-only keys/tables preserved)")
216
- ctx.nextStepTriggers.codexRestart = true
217
- }
218
- }
219
-
220
- /** codex::scrub_deprecated_features — the [features].use_legacy_landlock awk pass. */
221
- function scrubDeprecatedFeaturesText(content: string): string {
222
- const lines = content.split("\n")
223
- if (lines[lines.length - 1] === "") lines.pop()
224
- let out = ""
225
- let inFeatures = false
226
- let header = ""
227
- let body = ""
228
- let keep = false
229
- let changed = false
230
- for (const line of lines) {
231
- if (inFeatures) {
232
- if (line.startsWith("[")) {
233
- inFeatures = false
234
- if (keep) out += `${header}\n${body}`
235
- else changed = true
236
- out += `${line}\n`
237
- continue
238
- }
239
- if (/^use_legacy_landlock[ \t]*=/.test(line)) {
240
- changed = true
241
- continue
242
- }
243
- body += `${line}\n`
244
- if (/[^ \t\f\v\r]/.test(line)) keep = true
245
- continue
246
- }
247
- if (/^\[features\][ \t]*$/.test(line)) {
248
- inFeatures = true
249
- header = line
250
- body = ""
251
- keep = false
252
- continue
253
- }
254
- out += `${line}\n`
255
- }
256
- if (inFeatures) {
257
- if (keep) out += `${header}\n${body}`
258
- else changed = true
259
- }
260
- return changed ? out : content
261
- }
262
-
263
- function scrubDeprecatedFeatures(ctx: Ctx, userConfig: string): void {
264
- const { change } = ctx.services.logger
265
- if (!existsSync(userConfig)) return
266
- const content = readFileSync(userConfig, "utf8")
267
- const next = scrubDeprecatedFeaturesText(content)
268
- if (next === content) return
269
-
270
- writeFileSync(`${userConfig}.tmp`, next)
271
- renameSync(`${userConfig}.tmp`, userConfig)
272
- change("Codex: scrubbed deprecated [features].use_legacy_landlock")
273
- }
274
-
275
- const PLUGIN_TABLE_HEADER = /^\[plugins\."([^"]+)"\][ \t]*$/
276
- /** Plugin ids the kit retired; their deployed tables are stripped on every sync. */
277
- const RETIRED_PLUGIN_IDS: Readonly<Record<string, true>> = {
278
- "effect-kit@docks": true,
279
- "session-relay@docks": true
280
- }
281
-
282
- /** codex::remove_retired_plugin_tables — drop [plugins."<id>"] blocks for retired ids. */
283
- export function removeRetiredPluginTablesText(content: string): string {
284
- const lines = content.split("\n")
285
- if (lines[lines.length - 1] === "") lines.pop()
286
- let out = ""
287
- let skipping = false
288
- for (const line of lines) {
289
- const header = PLUGIN_TABLE_HEADER.exec(line)
290
- if (header !== null) {
291
- skipping = RETIRED_PLUGIN_IDS[header[1]!] === true
292
- if (skipping) continue
293
- } else if (skipping) {
294
- if (!line.startsWith("[")) continue
295
- skipping = false
296
- }
297
- out += `${line}\n`
298
- }
299
- return out
300
- }
301
-
302
- function retiredPluginTables(content: string): Array<string> {
303
- return content
304
- .split("\n")
305
- .map((line) => PLUGIN_TABLE_HEADER.exec(line)?.[1])
306
- .filter((id): id is string => id !== undefined && RETIRED_PLUGIN_IDS[id] === true)
307
- }
308
-
309
- function removeRetiredPluginTables(ctx: Ctx, userConfig: string): void {
310
- const { change } = ctx.services.logger
311
- if (!existsSync(userConfig)) return
312
- const content = readFileSync(userConfig, "utf8")
313
- const present = retiredPluginTables(content)
314
- if (present.length === 0) return
315
-
316
- writeFileSync(`${userConfig}.tmp`, removeRetiredPluginTablesText(content))
317
- renameSync(`${userConfig}.tmp`, userConfig)
318
- for (const id of present) change(`Codex: removed retired plugin table [plugins."${id}"]`)
116
+ if (ctx.services.deps.probe("apt-get").state === "present")
117
+ return "sudo apt-get install -y bubblewrap";
118
+ if (ctx.services.deps.probe("dnf").state === "present") return "sudo dnf install -y bubblewrap";
119
+ if (ctx.services.deps.probe("pacman").state === "present")
120
+ return "sudo pacman -S --noconfirm bubblewrap";
121
+ if (ctx.services.deps.probe("zypper").state === "present")
122
+ return "sudo zypper install -y bubblewrap";
123
+ return "";
319
124
  }
320
125
 
321
126
  // ------------------------------------------------------- rules + agents ----
322
127
 
323
128
  function syncRules(ctx: Ctx, sotRules: ReadonlyArray<PayloadPath>, userRulesDir: string): void {
324
- const { change, echo, verbose } = ctx.services.logger
325
- const firstRule = sotRules[0]
326
- if (firstRule === undefined) return
327
- const firstDisplay = payloadDisplayPath(firstRule)
328
- const sotRulesDir = firstDisplay.slice(0, firstDisplay.lastIndexOf("/"))
129
+ const { change, echo, verbose } = ctx.services.logger;
130
+ const firstRule = sotRules[0];
131
+ if (firstRule === undefined) return;
132
+ const firstDisplay = payloadDisplayPath(firstRule);
133
+ const sotRulesDir = firstDisplay.slice(0, firstDisplay.lastIndexOf("/"));
329
134
 
330
135
  if (ctx.dryRun) {
331
- echo(`[dry-run] cp ${sotRulesDir}/*.rules -> ${userRulesDir}/`)
332
- return
136
+ echo(`[dry-run] cp ${sotRulesDir}/*.rules -> ${userRulesDir}/`);
137
+ return;
333
138
  }
334
139
 
335
- mkdirSync(userRulesDir, { recursive: true })
336
- let sawRules = false
337
- let rulesChanged = false
338
- const ruleFiles = sotRules.filter((path) => path.endsWith(".rules")).sort(compareCodepoints)
140
+ mkdirSync(userRulesDir, { recursive: true });
141
+ let sawRules = false;
142
+ let rulesChanged = false;
143
+ const ruleFiles = sotRules.filter((path) => path.endsWith(".rules")).sort(compareCodepoints);
339
144
  for (const ruleFile of ruleFiles) {
340
- sawRules = true
341
- const userRuleFile = p(userRulesDir, ruleFile.slice(ruleFile.lastIndexOf("/") + 1))
342
- const content = payloadBytes(ruleFile)
343
- const identical = existsSync(userRuleFile) && readFileSync(userRuleFile).equals(content)
344
- if (identical) continue
345
- if (existsSync(userRuleFile)) copyFileSync(userRuleFile, `${userRuleFile}.bak`)
346
- writeFileSync(userRuleFile, content)
347
- rulesChanged = true
145
+ sawRules = true;
146
+ const userRuleFile = p(userRulesDir, ruleFile.slice(ruleFile.lastIndexOf("/") + 1));
147
+ const content = payloadBytes(ruleFile);
148
+ const identical = existsSync(userRuleFile) && readFileSync(userRuleFile).equals(content);
149
+ if (identical) continue;
150
+ if (existsSync(userRuleFile)) copyFileSync(userRuleFile, `${userRuleFile}.bak`);
151
+ writeFileSync(userRuleFile, content);
152
+ rulesChanged = true;
348
153
  }
349
154
  if (rulesChanged) {
350
- change("Codex rules synced")
351
- ctx.nextStepTriggers.codexRestart = true
352
- } else if (sawRules) verbose("Codex rules already in sync")
155
+ change("Codex rules synced");
156
+ ctx.nextStepTriggers.codexRestart = true;
157
+ } else if (sawRules) verbose("Codex rules already in sync");
353
158
  }
354
159
 
355
160
  function syncAgentsMd(ctx: Ctx, sotAgentsMdText: string, userAgentsMd: string): void {
356
- const { change, echo, verbose } = ctx.services.logger
357
- const sotAgentsMd = payloadDisplayPath("SoT/.codex/AGENTS.md")
161
+ const { change, echo, verbose } = ctx.services.logger;
162
+ const sotAgentsMd = payloadDisplayPath("SoT/.codex/AGENTS.md");
358
163
 
359
164
  if (ctx.dryRun) {
360
- echo(`[dry-run] cp ${sotAgentsMd} -> ${userAgentsMd}`)
361
- return
165
+ echo(`[dry-run] cp ${sotAgentsMd} -> ${userAgentsMd}`);
166
+ return;
362
167
  }
363
168
 
364
169
  if (existsSync(userAgentsMd) && readFileSync(userAgentsMd, "utf8") === sotAgentsMdText) {
365
- verbose("Codex AGENTS.md already in sync")
366
- return
367
- }
368
- if (existsSync(userAgentsMd)) copyFileSync(userAgentsMd, `${userAgentsMd}.bak`)
369
- writeFileSync(userAgentsMd, sotAgentsMdText)
370
- change("Codex AGENTS.md synced")
371
- ctx.nextStepTriggers.codexRestart = true
372
- }
373
-
374
- // ---------------------------------------------------------- marketplace ----
375
-
376
- function syncMarketplace(ctx: Ctx, sotMarketplaceText: string, userMarketplace: string): void {
377
- const { change, echo, verbose } = ctx.services.logger
378
- const sotMarketplace = payloadDisplayPath("SoT/.codex/plugins/marketplace.json")
379
- const repo = parseJson(sotMarketplaceText)
380
- if (repo === undefined) throw new Error(`invalid SoT marketplace JSON: ${sotMarketplace}`)
381
-
382
- const userText = existsSync(userMarketplace) ? readFileSync(userMarketplace, "utf8") : undefined
383
- const user = userText === undefined ? undefined : parseJson(userText)
384
- if (userText !== undefined && user === undefined) {
385
- throw new Error(`invalid deployed Codex marketplace JSON: ${userMarketplace}. Fix or delete it.`)
386
- }
387
- const out = user === undefined ? undefined : jqStringify(mergeMarketplace(repo, user))
388
-
389
- if (ctx.dryRun) {
390
- if (userText === undefined) {
391
- echo(`[dry-run] cp ${sotMarketplace} -> ${userMarketplace}`)
392
- } else if (out === userText) {
393
- verbose("Codex marketplace already in sync")
394
- } else {
395
- echo(`[dry-run] merge ${sotMarketplace} -> ${userMarketplace} (backup at ${userMarketplace}.bak)`)
396
- }
397
- return
398
- }
399
-
400
- mkdirSync(p(ctx.agentsDir, "plugins"), { recursive: true })
401
- if (userText !== undefined && out !== undefined) {
402
- if (out === userText) {
403
- verbose("Codex marketplace already in sync")
404
- return
405
- }
406
- copyFileSync(userMarketplace, `${userMarketplace}.bak`)
407
- writeFileSync(`${userMarketplace}.tmp`, out)
408
- renameSync(`${userMarketplace}.tmp`, userMarketplace)
409
- change("Codex marketplace merged (backup at marketplace.json.bak)")
410
- ctx.nextStepTriggers.codexRestart = true
411
- } else {
412
- writeFileSync(userMarketplace, sotMarketplaceText)
413
- change("Codex marketplace installed")
414
- ctx.nextStepTriggers.codexRestart = true
415
- }
416
- }
417
-
418
- /**
419
- * The jq -s marketplace merge: `$user *` a {name, interface} coalesce, then
420
- * plugins = user+repo | reverse | unique_by(.name) | reverse — SoT (repo)
421
- * wins per plugin name; distinct names end up descending by name, exactly
422
- * like jq's unique_by (ascending) followed by reverse.
423
- */
424
- function mergeMarketplace(repo: Json, user: Json): Json {
425
- const u = isObject(user) ? user : {}
426
- const r = isObject(repo) ? repo : {}
427
- const coalesce = (a: Json | undefined, b: Json | undefined): Json =>
428
- a !== undefined && a !== null && a !== false ? a : (b ?? null)
429
- const merged: { [k: string]: Json } = {
430
- ...u,
431
- name: coalesce(u["name"], r["name"]),
432
- interface: coalesce(u["interface"], r["interface"])
433
- }
434
- const plugins = [
435
- ...(Array.isArray(u["plugins"]) ? u["plugins"] : []),
436
- ...(Array.isArray(r["plugins"]) ? r["plugins"] : [])
437
- ]
438
- const firstByName = new Map<string, Json>()
439
- for (const p of [...plugins].reverse()) {
440
- const name = isObject(p) && typeof p["name"] === "string" ? p["name"] : ""
441
- if (!firstByName.has(name)) firstByName.set(name, p)
442
- }
443
- merged["plugins"] = [...firstByName.keys()].sort(compareCodepoints).map((n) => firstByName.get(n)!).reverse()
444
- return merged
445
- }
446
-
447
- // -------------------------------------------------------------- plugins ----
448
-
449
- /** codex::_marketplace_source — first `source =` inside [marketplaces.<name>]. */
450
- function marketplaceSource(marketplace: string, configFile: string): string {
451
- if (!existsSync(configFile)) return ""
452
- let inMarketplace = false
453
- for (const line of readFileSync(configFile, "utf8").split("\n")) {
454
- if (line === `[marketplaces.${marketplace}]`) {
455
- inMarketplace = true
456
- continue
457
- }
458
- if (line.startsWith("[")) inMarketplace = false
459
- if (inMarketplace && /^[ \t]*source[ \t]*=/.test(line)) {
460
- return line
461
- .replace(/^[^=]+=[ \t]*/, "")
462
- .replace(/[ \t]*#.*/, "")
463
- .replace(/^"|"$/g, "")
464
- }
465
- }
466
- return ""
467
- }
468
-
469
- async function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): Promise<void> {
470
- const { change, echo } = ctx.services.logger
471
- if (ctx.dryRun) {
472
- echo("[dry-run] remove legacy configured Codex Docks marketplace when personal marketplace is deployed")
473
- return
474
- }
475
-
476
- if (ctx.services.deps.probe("codex").state === "missing") return
477
-
478
- const source = marketplaceSource("docks", userConfig)
479
- if (source !== "https://github.com/DocksDocks/docks.git" && source !== "DocksDocks/docks") return
480
- const res = await spawnProcess("codex", ["plugin", "marketplace", "remove", "docks"], { stdio: "ignore" })
481
- if (res.error === undefined && res.exitCode === 0) {
482
- change("Removed legacy configured Codex Docks marketplace; using personal marketplace file")
483
- ctx.nextStepTriggers.codexRestart = true
484
- } else {
485
- recordFailure(ctx, "Failed to remove legacy configured Codex Docks marketplace")
486
- }
487
- }
488
-
489
- /** codex::_standalone_install_command — per-OS official standalone installer. */
490
- const standaloneInstallCommand = (ctx: Ctx): string => ctx.services.deps.spec("codex").installHint()
491
-
492
- /** codex::_enabled_plugin_ids — [plugins."<id>"] tables with enabled = true. */
493
- function enabledPluginIds(configFile: string): Array<string> {
494
- if (!existsSync(configFile)) return []
495
- return enabledPluginIdsFromText(readFileSync(configFile, "utf8"))
496
- }
497
-
498
- function enabledPluginIdsFromText(configText: string): Array<string> {
499
- const ids: Array<string> = []
500
- let plugin = ""
501
- let enabled = false
502
- const flush = (): void => {
503
- if (plugin !== "" && enabled) ids.push(plugin)
504
- }
505
- for (const line of configText.split("\n")) {
506
- const m = PLUGIN_TABLE_HEADER.exec(line)
507
- if (m !== null) {
508
- flush()
509
- plugin = m[1]!
510
- enabled = false
511
- continue
512
- }
513
- if (line.startsWith("[")) {
514
- flush()
515
- plugin = ""
516
- enabled = false
517
- continue
518
- }
519
- if (plugin !== "" && /^[ \t]*enabled[ \t]*=[ \t]*true([ \t]*(#.*)?)?$/.test(line)) {
520
- enabled = true
521
- }
522
- }
523
- flush()
524
- return ids
525
- }
526
-
527
- function manualPluginRefreshCommand(sotConfigText: string): string {
528
- const first = enabledPluginIdsFromText(sotConfigText)[0]
529
- return first !== undefined ? `codex plugin add ${first}` : "codex plugin add <plugin@marketplace>"
530
- }
531
-
532
- async function installedPluginIdsFromCli(): Promise<Set<string> | undefined> {
533
- const result = await spawnProcess("codex", ["plugin", "list", "--json"], {
534
- stdio: ["ignore", "pipe", "ignore"]
535
- })
536
- if (result.error !== undefined || result.exitCode !== 0) return undefined
537
- const value = parseJson(result.stdout)
538
- if (value === undefined || !isObject(value) || !Array.isArray(value["installed"])) return undefined
539
- const ids = new Set<string>()
540
- for (const row of value["installed"]) {
541
- if (!isObject(row) || row["installed"] !== true || typeof row["pluginId"] !== "string") continue
542
- ids.add(row["pluginId"])
543
- }
544
- return ids
545
- }
546
-
547
- async function syncPlugins(ctx: Ctx, sotConfigText: string): Promise<void> {
548
- const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger
549
- if (ctx.dryRun) {
550
- echo(
551
- ctx.skipPluginRefresh
552
- ? "[dry-run] add missing enabled Codex plugins from SoT; skip refresh-only plugin updates"
553
- : "[dry-run] add enabled Codex plugins from SoT"
554
- )
555
- return
556
- }
557
-
558
- if (ctx.services.deps.probe("codex").state === "missing") {
559
- warn(
560
- `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)}`
561
- )
562
- return
170
+ verbose("Codex AGENTS.md already in sync");
171
+ return;
563
172
  }
564
- if (ctx.services.deps.probe("git").state === "missing") {
565
- ctx.services.deps.warnMissing(
566
- "git",
567
- ctx.services.logger,
568
- "plugin marketplaces are git repos — Codex plugin refresh skipped; re-run sync after installing"
569
- )
570
- return
571
- }
572
-
573
- const desiredPluginIds = enabledPluginIdsFromText(sotConfigText)
574
- let pluginIds = desiredPluginIds
575
- if (ctx.skipPluginRefresh) {
576
- progress("Checking installed Codex plugins...")
577
- const installedPluginIds = await installedPluginIdsFromCli()
578
- clearProgress()
579
- if (installedPluginIds === undefined) {
580
- warn("Codex plugin inventory unavailable — falling back to the full refresh path")
581
- } else {
582
- pluginIds = desiredPluginIds.filter((pluginId) => !installedPluginIds.has(pluginId))
583
- }
584
- }
585
-
586
- let refreshed = 0
587
- let failed = 0
588
- for (const pluginId of pluginIds) {
589
- progress(`Updating Codex plugin ${pluginId}...`)
590
- const res = await spawnProcess("codex", ["plugin", "add", pluginId], { stdio: ["ignore", "pipe", "pipe"] })
591
- clearProgress()
592
- const addOut = `${res.stdout}${res.stderr}`
593
- if (res.error === undefined && res.exitCode === 0) {
594
- refreshed++
595
- } else if (addOut.includes("could not find a Codex CLI binary")) {
596
- recordFailure(
597
- ctx,
598
- `Codex plugin refresh hit a stale launcher/wrapper on PATH - install current standalone Codex with: ${standaloneInstallCommand(ctx)}`
599
- )
600
- failed++
601
- } else {
602
- const failureLine = addOut.split("\n")[0] ?? ""
603
- recordFailure(
604
- ctx,
605
- `Codex plugin refresh failed for ${pluginId}: ${failureLine !== "" ? failureLine : "unknown error"}; run manually: codex plugin add ${pluginId}`
606
- )
607
- failed++
608
- }
609
- }
610
-
611
- if (refreshed > 0) {
612
- change(`Codex plugins synced (plugins: ~${refreshed})`)
613
- ctx.nextStepTriggers.codexRestart = true
614
- }
615
- if (ctx.skipPluginRefresh && pluginIds.length === 0) verbose("Codex plugins already installed; refresh-only updates skipped")
616
- if (failed > 0) warn(`${failed} Codex plugin operation(s) failed — re-run sync or install manually`)
617
- }
618
-
619
- // -------------------------------------------------------------- summary ----
620
-
621
- export function codexSummary(ctx: Ctx): void {
622
- const { echo } = ctx.services.logger
623
- const codexDir = p(ctx.home, ".codex")
624
- echo(`Codex: ${codexDir}`)
625
- if (!ctx.dryRun) {
626
- const count = enabledPluginIds(p(codexDir, "config.toml")).length
627
- echo(`Codex plugins: ${count} enabled in config.toml`)
628
- }
629
- }
630
-
631
- export function codexNextSteps(ctx: Ctx): Array<string> {
632
- return ctx.verbose || ctx.nextStepTriggers.codexRestart
633
- ? ["Restart Codex to load any refreshed plugins, skills, or tools."]
634
- : []
173
+ if (existsSync(userAgentsMd)) copyFileSync(userAgentsMd, `${userAgentsMd}.bak`);
174
+ writeFileSync(userAgentsMd, sotAgentsMdText);
175
+ change("Codex AGENTS.md synced");
176
+ ctx.nextStepTriggers.codexRestart = true;
635
177
  }