docks-kit 0.17.1 → 0.17.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 -32
- package/cli/docs/omp-models.md +88 -53
- package/cli/src/argv.ts +38 -469
- package/cli/src/argvSurface.ts +371 -0
- package/cli/src/argvValidate.ts +122 -0
- package/cli/src/commands/docs.ts +57 -42
- package/cli/src/commands/harnesses.ts +38 -45
- package/cli/src/commands/model.ts +48 -53
- package/cli/src/commands/models.ts +24 -26
- package/cli/src/commands/omp.ts +132 -112
- package/cli/src/commands/plugins.ts +20 -20
- package/cli/src/commands/skills.ts +22 -20
- package/cli/src/commands/status.ts +87 -80
- package/cli/src/commands/sync.ts +113 -87
- package/cli/src/commands/toolchain.ts +21 -21
- package/cli/src/commands/update.ts +139 -117
- package/cli/src/efforts.ts +35 -34
- package/cli/src/engine-native/DESIGN.md +30 -30
- package/cli/src/engine-native/bun.ts +77 -55
- package/cli/src/engine-native/claudeLsp.ts +154 -0
- package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
- package/cli/src/engine-native/claudePluginPasses.ts +348 -0
- package/cli/src/engine-native/claudeRemovals.ts +195 -0
- package/cli/src/engine-native/claudeRetired.ts +4 -4
- package/cli/src/engine-native/claudeRuntime.ts +94 -68
- package/cli/src/engine-native/claudeSettings.ts +174 -0
- package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
- package/cli/src/engine-native/claudeSync.ts +176 -456
- package/cli/src/engine-native/codexConfig.ts +158 -0
- package/cli/src/engine-native/codexHooks.ts +91 -0
- package/cli/src/engine-native/codexPlugins.ts +290 -0
- package/cli/src/engine-native/codexStatus.ts +24 -0
- package/cli/src/engine-native/codexSync.ts +110 -568
- package/cli/src/engine-native/codexToml.ts +140 -138
- package/cli/src/engine-native/deps.ts +144 -107
- package/cli/src/engine-native/engineCtx.ts +126 -0
- package/cli/src/engine-native/exec.ts +98 -87
- package/cli/src/engine-native/failures.ts +3 -3
- package/cli/src/engine-native/harnesses.ts +62 -72
- package/cli/src/engine-native/index.ts +34 -315
- package/cli/src/engine-native/jq.ts +25 -24
- package/cli/src/engine-native/logger.ts +98 -100
- package/cli/src/engine-native/models.ts +50 -45
- package/cli/src/engine-native/modes.ts +83 -80
- package/cli/src/engine-native/ompFileDeploy.ts +111 -0
- package/cli/src/engine-native/ompMarketplace.ts +136 -0
- package/cli/src/engine-native/ompOverlay.ts +67 -64
- package/cli/src/engine-native/ompPaths.ts +49 -38
- package/cli/src/engine-native/ompPlugins.ts +170 -0
- package/cli/src/engine-native/ompSync.ts +64 -408
- package/cli/src/engine-native/ompYaml.ts +43 -41
- package/cli/src/engine-native/os/darwin.ts +5 -5
- package/cli/src/engine-native/os/index.ts +15 -15
- package/cli/src/engine-native/os/linux.ts +5 -5
- package/cli/src/engine-native/os/posix.ts +28 -24
- package/cli/src/engine-native/os/targets.ts +17 -17
- package/cli/src/engine-native/os/types.ts +31 -31
- package/cli/src/engine-native/os/windows.ts +61 -61
- package/cli/src/engine-native/parseArgs.ts +122 -372
- package/cli/src/engine-native/parseHelp.ts +66 -0
- package/cli/src/engine-native/parseModifiers.ts +222 -0
- package/cli/src/engine-native/services.ts +34 -34
- package/cli/src/engine-native/settings.ts +15 -15
- package/cli/src/engine-native/sharedTypes.d.ts +46 -0
- package/cli/src/engine-native/skillsInstall.ts +105 -0
- package/cli/src/engine-native/skillsLinks.ts +203 -0
- package/cli/src/engine-native/skillsManifest.ts +16 -0
- package/cli/src/engine-native/skillsPrune.ts +108 -0
- package/cli/src/engine-native/skillsSync.ts +28 -362
- package/cli/src/engine-native/syncConcurrency.ts +55 -0
- package/cli/src/engine-native/syncDispatch.ts +141 -0
- package/cli/src/engine-native/toolchain.ts +65 -62
- package/cli/src/engine.ts +65 -57
- package/cli/src/generated/sotPayload.ts +7 -7
- package/cli/src/kitHome.ts +44 -40
- package/cli/src/main.ts +63 -56
- package/cli/src/manifests.ts +53 -54
- package/cli/src/md.d.ts +2 -2
- package/cli/src/payload.ts +9 -9
- package/cli/src/services.ts +21 -13
- package/cli/tsconfig.json +4 -1
- package/package.json +9 -2
- 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,
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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(
|
|
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")
|
|
168
|
-
|
|
169
|
-
if (ctx.services.deps.probe("
|
|
170
|
-
if (ctx.services.deps.probe("
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
}
|