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
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EngineNative Codex config merge and validation. Line-based TOML passes
|
|
3
|
+
* intentionally avoid a TOML library because reformatting user configs would
|
|
4
|
+
* be a behavior change. Guard order, message strings, and backup behavior
|
|
5
|
+
* are golden-tested.
|
|
6
|
+
*/
|
|
7
|
+
import { copyFileSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
|
|
9
|
+
import { payloadDisplayPath } from "../payload";
|
|
10
|
+
import { mergeTableSettings, mergeTopLevelSettings } from "./codexToml";
|
|
11
|
+
import type { Ctx } from "./index";
|
|
12
|
+
|
|
13
|
+
// --------------------------------------------------------------- config ----
|
|
14
|
+
|
|
15
|
+
export function syncConfig(ctx: Ctx, sotConfigText: string, userConfig: string): void {
|
|
16
|
+
const { change, echo, verbose } = ctx.services.logger;
|
|
17
|
+
const sotConfig = payloadDisplayPath("SoT/.codex/config.toml");
|
|
18
|
+
|
|
19
|
+
if (ctx.dryRun) {
|
|
20
|
+
if (existsSync(userConfig)) {
|
|
21
|
+
echo(`[dry-run] merge ${sotConfig} -> ${userConfig}`);
|
|
22
|
+
} else {
|
|
23
|
+
echo(`[dry-run] install ${sotConfig} -> ${userConfig}`);
|
|
24
|
+
}
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!existsSync(userConfig)) {
|
|
29
|
+
writeFileSync(userConfig, sotConfigText);
|
|
30
|
+
change("Codex config installed");
|
|
31
|
+
ctx.nextStepTriggers.codexRestart = true;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Merge into a staging copy so `.bak` is written only when the config
|
|
36
|
+
// actually changes — an unconditional early backup lets a later no-op run
|
|
37
|
+
// overwrite the recovery copy with already-merged content.
|
|
38
|
+
const before = readFileSync(userConfig, "utf8");
|
|
39
|
+
const staging = `${userConfig}.merge.tmp`;
|
|
40
|
+
// Normalize once before record transforms because CR bytes change table-header identity.
|
|
41
|
+
writeFileSync(staging, before.replace(/\r\n/g, "\n"));
|
|
42
|
+
|
|
43
|
+
scrubDeprecatedFeatures(ctx, staging);
|
|
44
|
+
removeRetiredPluginTables(ctx, staging);
|
|
45
|
+
mergeTopLevelSettings(sotConfigText, staging);
|
|
46
|
+
mergeTableSettings(sotConfigText, staging);
|
|
47
|
+
|
|
48
|
+
if (readFileSync(staging, "utf8") === before) {
|
|
49
|
+
rmSync(staging, { force: true });
|
|
50
|
+
verbose("Codex config already in sync");
|
|
51
|
+
} else {
|
|
52
|
+
copyFileSync(userConfig, `${userConfig}.bak`);
|
|
53
|
+
renameSync(staging, userConfig);
|
|
54
|
+
change("Codex config merged (backup at config.toml.bak; user-only keys/tables preserved)");
|
|
55
|
+
ctx.nextStepTriggers.codexRestart = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** codex::scrub_deprecated_features — the [features].use_legacy_landlock awk pass. */
|
|
60
|
+
function scrubDeprecatedFeaturesText(content: string): string {
|
|
61
|
+
const lines = content.split("\n");
|
|
62
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
63
|
+
let out = "";
|
|
64
|
+
let inFeatures = false;
|
|
65
|
+
let header = "";
|
|
66
|
+
let body = "";
|
|
67
|
+
let keep = false;
|
|
68
|
+
let changed = false;
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
if (inFeatures) {
|
|
71
|
+
if (line.startsWith("[")) {
|
|
72
|
+
inFeatures = false;
|
|
73
|
+
if (keep) out += `${header}\n${body}`;
|
|
74
|
+
else changed = true;
|
|
75
|
+
out += `${line}\n`;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (/^use_legacy_landlock[ \t]*=/.test(line)) {
|
|
79
|
+
changed = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
body += `${line}\n`;
|
|
83
|
+
if (/[^ \t\f\v\r]/.test(line)) keep = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (/^\[features\][ \t]*$/.test(line)) {
|
|
87
|
+
inFeatures = true;
|
|
88
|
+
header = line;
|
|
89
|
+
body = "";
|
|
90
|
+
keep = false;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
out += `${line}\n`;
|
|
94
|
+
}
|
|
95
|
+
if (inFeatures) {
|
|
96
|
+
if (keep) out += `${header}\n${body}`;
|
|
97
|
+
else changed = true;
|
|
98
|
+
}
|
|
99
|
+
return changed ? out : content;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function scrubDeprecatedFeatures(ctx: Ctx, userConfig: string): void {
|
|
103
|
+
const { change } = ctx.services.logger;
|
|
104
|
+
if (!existsSync(userConfig)) return;
|
|
105
|
+
const content = readFileSync(userConfig, "utf8");
|
|
106
|
+
const next = scrubDeprecatedFeaturesText(content);
|
|
107
|
+
if (next === content) return;
|
|
108
|
+
|
|
109
|
+
writeFileSync(`${userConfig}.tmp`, next);
|
|
110
|
+
renameSync(`${userConfig}.tmp`, userConfig);
|
|
111
|
+
change("Codex: scrubbed deprecated [features].use_legacy_landlock");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const PLUGIN_TABLE_HEADER = /^\[plugins\."([^"]+)"\][ \t]*$/;
|
|
115
|
+
/** Plugin ids the kit retired; their deployed tables are stripped on every sync. */
|
|
116
|
+
const RETIRED_PLUGIN_IDS: Readonly<Record<string, true>> = {
|
|
117
|
+
"effect-kit@docks": true,
|
|
118
|
+
"session-relay@docks": true,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** codex::remove_retired_plugin_tables — drop [plugins."<id>"] blocks for retired ids. */
|
|
122
|
+
export function removeRetiredPluginTablesText(content: string): string {
|
|
123
|
+
const lines = content.split("\n");
|
|
124
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
125
|
+
let out = "";
|
|
126
|
+
let skipping = false;
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
const header = PLUGIN_TABLE_HEADER.exec(line);
|
|
129
|
+
if (header !== null) {
|
|
130
|
+
skipping = RETIRED_PLUGIN_IDS[header[1]!] === true;
|
|
131
|
+
if (skipping) continue;
|
|
132
|
+
} else if (skipping) {
|
|
133
|
+
if (!line.startsWith("[")) continue;
|
|
134
|
+
skipping = false;
|
|
135
|
+
}
|
|
136
|
+
out += `${line}\n`;
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function retiredPluginTables(content: string): Array<string> {
|
|
142
|
+
return content
|
|
143
|
+
.split("\n")
|
|
144
|
+
.map((line) => PLUGIN_TABLE_HEADER.exec(line)?.[1])
|
|
145
|
+
.filter((id): id is string => id !== undefined && RETIRED_PLUGIN_IDS[id] === true);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function removeRetiredPluginTables(ctx: Ctx, userConfig: string): void {
|
|
149
|
+
const { change } = ctx.services.logger;
|
|
150
|
+
if (!existsSync(userConfig)) return;
|
|
151
|
+
const content = readFileSync(userConfig, "utf8");
|
|
152
|
+
const present = retiredPluginTables(content);
|
|
153
|
+
if (present.length === 0) return;
|
|
154
|
+
|
|
155
|
+
writeFileSync(`${userConfig}.tmp`, removeRetiredPluginTablesText(content));
|
|
156
|
+
renameSync(`${userConfig}.tmp`, userConfig);
|
|
157
|
+
for (const id of present) change(`Codex: removed retired plugin table [plugins."${id}"]`);
|
|
158
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EngineNative Codex retired-hook pruning. Removes only the Claude hooks
|
|
3
|
+
* docks-kit retired before Codex could import them safely. Guard order,
|
|
4
|
+
* message strings, and backup behavior are golden-tested.
|
|
5
|
+
*/
|
|
6
|
+
import { copyFileSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
import { p } from "./exec";
|
|
9
|
+
import type { Ctx } from "./index";
|
|
10
|
+
import { isObject, jqStringify, parseJson, type Json } from "./jq";
|
|
11
|
+
|
|
12
|
+
const RETIRED_CONTEXT_HOOK = `echo "[CONTEXT] Current date: $(date '+%A, %Y-%m-%d %H:%M:%S %Z')"`;
|
|
13
|
+
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`;
|
|
14
|
+
const RETIRED_CONFIG_HOOKS = new Set([
|
|
15
|
+
`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}"`,
|
|
16
|
+
`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}"`,
|
|
17
|
+
`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}"`,
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function isRetiredImportedSessionStartHook(handler: Json, codexDir: string): boolean {
|
|
21
|
+
if (!isObject(handler) || handler["type"] !== "command" || typeof handler["command"] !== "string")
|
|
22
|
+
return false;
|
|
23
|
+
const command = handler["command"];
|
|
24
|
+
const connectorScript = p(codexDir, "hooks", "disable-claudeai-connectors.sh");
|
|
25
|
+
const retiredConnectorHook = `[ -x '${connectorScript}' ] && '${connectorScript}' || true`;
|
|
26
|
+
return (
|
|
27
|
+
command === RETIRED_CONTEXT_HOOK ||
|
|
28
|
+
command === RETIRED_SKILLS_HOOK ||
|
|
29
|
+
RETIRED_CONFIG_HOOKS.has(command) ||
|
|
30
|
+
command === retiredConnectorHook
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function withoutRetiredImportedHooks(
|
|
35
|
+
doc: { [key: string]: Json },
|
|
36
|
+
codexDir: string,
|
|
37
|
+
): { readonly value: Json; readonly removed: number } {
|
|
38
|
+
const hooks = doc["hooks"];
|
|
39
|
+
if (!isObject(hooks) || !Array.isArray(hooks["SessionStart"])) return { value: doc, removed: 0 };
|
|
40
|
+
|
|
41
|
+
let removed = 0;
|
|
42
|
+
const groups: Array<Json> = [];
|
|
43
|
+
for (const group of hooks["SessionStart"]) {
|
|
44
|
+
if (!isObject(group) || !Array.isArray(group["hooks"])) {
|
|
45
|
+
groups.push(group);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const kept = group["hooks"].filter((handler) => {
|
|
49
|
+
if (!isRetiredImportedSessionStartHook(handler, codexDir)) return true;
|
|
50
|
+
removed++;
|
|
51
|
+
return false;
|
|
52
|
+
});
|
|
53
|
+
if (kept.length > 0) groups.push({ ...group, hooks: kept });
|
|
54
|
+
}
|
|
55
|
+
if (removed === 0) return { value: doc, removed: 0 };
|
|
56
|
+
|
|
57
|
+
const nextHooks = { ...hooks };
|
|
58
|
+
if (groups.length > 0) nextHooks["SessionStart"] = groups;
|
|
59
|
+
else delete nextHooks["SessionStart"];
|
|
60
|
+
return { value: { ...doc, hooks: nextHooks }, removed };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Remove only Claude hooks that docks-kit retired before Codex could import them safely. */
|
|
64
|
+
export function removeRetiredImportedHooks(ctx: Ctx, codexDir: string): void {
|
|
65
|
+
const { change, echo, warn } = ctx.services.logger;
|
|
66
|
+
const hooksFile = p(codexDir, "hooks.json");
|
|
67
|
+
if (!existsSync(hooksFile)) return;
|
|
68
|
+
const before = readFileSync(hooksFile, "utf8");
|
|
69
|
+
const doc = parseJson(before);
|
|
70
|
+
if (doc === undefined || !isObject(doc)) {
|
|
71
|
+
warn(`Codex hooks file is not a valid JSON object; retired hook cleanup skipped: ${hooksFile}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const result = withoutRetiredImportedHooks(doc, codexDir);
|
|
75
|
+
if (result.removed === 0) return;
|
|
76
|
+
|
|
77
|
+
if (ctx.dryRun) {
|
|
78
|
+
echo(
|
|
79
|
+
`[dry-run] remove ${result.removed} retired imported docks-kit SessionStart hook(s) from ${hooksFile}`,
|
|
80
|
+
);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
copyFileSync(hooksFile, `${hooksFile}.bak`);
|
|
85
|
+
writeFileSync(`${hooksFile}.tmp`, jqStringify(result.value));
|
|
86
|
+
renameSync(`${hooksFile}.tmp`, hooksFile);
|
|
87
|
+
change(
|
|
88
|
+
`Codex: removed ${result.removed} retired imported docks-kit SessionStart hook(s) (backup at hooks.json.bak)`,
|
|
89
|
+
);
|
|
90
|
+
ctx.nextStepTriggers.codexRestart = true;
|
|
91
|
+
}
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EngineNative Codex marketplace and plugin passes: personal marketplace
|
|
3
|
+
* merge, legacy marketplace removal, and enabled-plugin refresh. Message
|
|
4
|
+
* strings and backup behavior are golden-tested.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
copyFileSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
renameSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
|
|
15
|
+
import { payloadDisplayPath } from "../payload";
|
|
16
|
+
import { PLUGIN_TABLE_HEADER } from "./codexConfig";
|
|
17
|
+
import { p, spawnProcess } from "./exec";
|
|
18
|
+
import { recordFailure } from "./failures";
|
|
19
|
+
import type { Ctx } from "./index";
|
|
20
|
+
import { compareCodepoints, isObject, jqStringify, parseJson, type Json } from "./jq";
|
|
21
|
+
import type { JsonObject } from "./sharedTypes";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------- marketplace ----
|
|
24
|
+
|
|
25
|
+
export function syncMarketplace(
|
|
26
|
+
ctx: Ctx,
|
|
27
|
+
sotMarketplaceText: string,
|
|
28
|
+
userMarketplace: string,
|
|
29
|
+
): void {
|
|
30
|
+
const { change, echo, verbose } = ctx.services.logger;
|
|
31
|
+
const sotMarketplace = payloadDisplayPath("SoT/.codex/plugins/marketplace.json");
|
|
32
|
+
const repo = parseJson(sotMarketplaceText);
|
|
33
|
+
if (repo === undefined) throw new Error(`invalid SoT marketplace JSON: ${sotMarketplace}`);
|
|
34
|
+
|
|
35
|
+
const userText = existsSync(userMarketplace) ? readFileSync(userMarketplace, "utf8") : undefined;
|
|
36
|
+
const user = userText === undefined ? undefined : parseJson(userText);
|
|
37
|
+
if (userText !== undefined && user === undefined) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`invalid deployed Codex marketplace JSON: ${userMarketplace}. Fix or delete it.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const out = user === undefined ? undefined : jqStringify(mergeMarketplace(repo, user));
|
|
43
|
+
|
|
44
|
+
if (ctx.dryRun) {
|
|
45
|
+
if (userText === undefined) {
|
|
46
|
+
echo(`[dry-run] cp ${sotMarketplace} -> ${userMarketplace}`);
|
|
47
|
+
} else if (out === userText) {
|
|
48
|
+
verbose("Codex marketplace already in sync");
|
|
49
|
+
} else {
|
|
50
|
+
echo(
|
|
51
|
+
`[dry-run] merge ${sotMarketplace} -> ${userMarketplace} (backup at ${userMarketplace}.bak)`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
mkdirSync(p(ctx.agentsDir, "plugins"), { recursive: true });
|
|
58
|
+
if (userText !== undefined && out !== undefined) {
|
|
59
|
+
if (out === userText) {
|
|
60
|
+
verbose("Codex marketplace already in sync");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
copyFileSync(userMarketplace, `${userMarketplace}.bak`);
|
|
64
|
+
writeFileSync(`${userMarketplace}.tmp`, out);
|
|
65
|
+
renameSync(`${userMarketplace}.tmp`, userMarketplace);
|
|
66
|
+
change("Codex marketplace merged (backup at marketplace.json.bak)");
|
|
67
|
+
ctx.nextStepTriggers.codexRestart = true;
|
|
68
|
+
} else {
|
|
69
|
+
writeFileSync(userMarketplace, sotMarketplaceText);
|
|
70
|
+
change("Codex marketplace installed");
|
|
71
|
+
ctx.nextStepTriggers.codexRestart = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The jq -s marketplace merge: `$user *` a {name, interface} coalesce, then
|
|
77
|
+
* plugins = user+repo | reverse | unique_by(.name) | reverse — SoT (repo)
|
|
78
|
+
* wins per plugin name; distinct names end up descending by name, exactly
|
|
79
|
+
* like jq's unique_by (ascending) followed by reverse.
|
|
80
|
+
*/
|
|
81
|
+
function mergeMarketplace(repo: Json, user: Json): Json {
|
|
82
|
+
const u = isObject(user) ? user : {};
|
|
83
|
+
const r = isObject(repo) ? repo : {};
|
|
84
|
+
const coalesce = (a: Json | undefined, b: Json | undefined): Json =>
|
|
85
|
+
a !== undefined && a !== null && a !== false ? a : (b ?? null);
|
|
86
|
+
const merged: JsonObject = {
|
|
87
|
+
...u,
|
|
88
|
+
name: coalesce(u["name"], r["name"]),
|
|
89
|
+
interface: coalesce(u["interface"], r["interface"]),
|
|
90
|
+
};
|
|
91
|
+
const plugins = [
|
|
92
|
+
...(Array.isArray(u["plugins"]) ? u["plugins"] : []),
|
|
93
|
+
...(Array.isArray(r["plugins"]) ? r["plugins"] : []),
|
|
94
|
+
];
|
|
95
|
+
const firstByName = new Map<string, Json>();
|
|
96
|
+
for (const entry of [...plugins].reverse()) {
|
|
97
|
+
const name = isObject(entry) && typeof entry["name"] === "string" ? entry["name"] : "";
|
|
98
|
+
if (!firstByName.has(name)) firstByName.set(name, entry);
|
|
99
|
+
}
|
|
100
|
+
merged["plugins"] = [...firstByName.keys()]
|
|
101
|
+
.sort(compareCodepoints)
|
|
102
|
+
.map((n) => firstByName.get(n)!)
|
|
103
|
+
.reverse();
|
|
104
|
+
return merged;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// -------------------------------------------------------------- plugins ----
|
|
108
|
+
|
|
109
|
+
/** codex::_marketplace_source — first `source =` inside [marketplaces.<name>]. */
|
|
110
|
+
function marketplaceSource(marketplace: string, configFile: string): string {
|
|
111
|
+
if (!existsSync(configFile)) return "";
|
|
112
|
+
let inMarketplace = false;
|
|
113
|
+
for (const line of readFileSync(configFile, "utf8").split("\n")) {
|
|
114
|
+
if (line === `[marketplaces.${marketplace}]`) {
|
|
115
|
+
inMarketplace = true;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (line.startsWith("[")) inMarketplace = false;
|
|
119
|
+
if (inMarketplace && /^[ \t]*source[ \t]*=/.test(line)) {
|
|
120
|
+
return line
|
|
121
|
+
.replace(/^[^=]+=[ \t]*/, "")
|
|
122
|
+
.replace(/[ \t]*#.*/, "")
|
|
123
|
+
.replace(/^"|"$/g, "");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return "";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function removeLegacyDocksMarketplace(ctx: Ctx, userConfig: string): Promise<void> {
|
|
130
|
+
const { change, echo } = ctx.services.logger;
|
|
131
|
+
if (ctx.dryRun) {
|
|
132
|
+
echo(
|
|
133
|
+
"[dry-run] remove legacy configured Codex Docks marketplace when personal marketplace is deployed",
|
|
134
|
+
);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (ctx.services.deps.probe("codex").state === "missing") return;
|
|
139
|
+
|
|
140
|
+
const source = marketplaceSource("docks", userConfig);
|
|
141
|
+
if (source !== "https://github.com/DocksDocks/docks.git" && source !== "DocksDocks/docks") return;
|
|
142
|
+
const res = await spawnProcess("codex", ["plugin", "marketplace", "remove", "docks"], {
|
|
143
|
+
stdio: "ignore",
|
|
144
|
+
});
|
|
145
|
+
if (res.error === undefined && res.exitCode === 0) {
|
|
146
|
+
change("Removed legacy configured Codex Docks marketplace; using personal marketplace file");
|
|
147
|
+
ctx.nextStepTriggers.codexRestart = true;
|
|
148
|
+
} else {
|
|
149
|
+
recordFailure(ctx, "Failed to remove legacy configured Codex Docks marketplace");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** codex::_standalone_install_command — per-OS official standalone installer. */
|
|
154
|
+
const standaloneInstallCommand = (ctx: Ctx): string =>
|
|
155
|
+
ctx.services.deps.spec("codex").installHint();
|
|
156
|
+
|
|
157
|
+
/** codex::_enabled_plugin_ids — [plugins."<id>"] tables with enabled = true. */
|
|
158
|
+
export function enabledPluginIds(configFile: string): Array<string> {
|
|
159
|
+
if (!existsSync(configFile)) return [];
|
|
160
|
+
return enabledPluginIdsFromText(readFileSync(configFile, "utf8"));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function enabledPluginIdsFromText(configText: string): Array<string> {
|
|
164
|
+
const ids: Array<string> = [];
|
|
165
|
+
let plugin = "";
|
|
166
|
+
let enabled = false;
|
|
167
|
+
const flush = (): void => {
|
|
168
|
+
if (plugin !== "" && enabled) ids.push(plugin);
|
|
169
|
+
};
|
|
170
|
+
for (const line of configText.split("\n")) {
|
|
171
|
+
const m = PLUGIN_TABLE_HEADER.exec(line);
|
|
172
|
+
if (m !== null) {
|
|
173
|
+
flush();
|
|
174
|
+
plugin = m[1]!;
|
|
175
|
+
enabled = false;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (line.startsWith("[")) {
|
|
179
|
+
flush();
|
|
180
|
+
plugin = "";
|
|
181
|
+
enabled = false;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (plugin !== "" && /^[ \t]*enabled[ \t]*=[ \t]*true([ \t]*(#.*)?)?$/.test(line)) {
|
|
185
|
+
enabled = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
flush();
|
|
189
|
+
return ids;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function manualPluginRefreshCommand(sotConfigText: string): string {
|
|
193
|
+
const first = enabledPluginIdsFromText(sotConfigText)[0];
|
|
194
|
+
return first !== undefined
|
|
195
|
+
? `codex plugin add ${first}`
|
|
196
|
+
: "codex plugin add <plugin@marketplace>";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function installedPluginIdsFromCli(): Promise<Set<string> | undefined> {
|
|
200
|
+
const result = await spawnProcess("codex", ["plugin", "list", "--json"], {
|
|
201
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
202
|
+
});
|
|
203
|
+
if (result.error !== undefined || result.exitCode !== 0) return undefined;
|
|
204
|
+
const value = parseJson(result.stdout);
|
|
205
|
+
if (value === undefined || !isObject(value) || !Array.isArray(value["installed"]))
|
|
206
|
+
return undefined;
|
|
207
|
+
const ids = new Set<string>();
|
|
208
|
+
for (const row of value["installed"]) {
|
|
209
|
+
if (!isObject(row) || row["installed"] !== true || typeof row["pluginId"] !== "string")
|
|
210
|
+
continue;
|
|
211
|
+
ids.add(row["pluginId"]);
|
|
212
|
+
}
|
|
213
|
+
return ids;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function syncPlugins(ctx: Ctx, sotConfigText: string): Promise<void> {
|
|
217
|
+
const { change, clearProgress, echo, progress, verbose, warn } = ctx.services.logger;
|
|
218
|
+
if (ctx.dryRun) {
|
|
219
|
+
echo(
|
|
220
|
+
ctx.skipPluginRefresh
|
|
221
|
+
? "[dry-run] add missing enabled Codex plugins from SoT; skip refresh-only plugin updates"
|
|
222
|
+
: "[dry-run] add enabled Codex plugins from SoT",
|
|
223
|
+
);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (ctx.services.deps.probe("codex").state === "missing") {
|
|
228
|
+
warn(
|
|
229
|
+
`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)}`,
|
|
230
|
+
);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (ctx.services.deps.probe("git").state === "missing") {
|
|
234
|
+
ctx.services.deps.warnMissing(
|
|
235
|
+
"git",
|
|
236
|
+
ctx.services.logger,
|
|
237
|
+
"plugin marketplaces are git repos — Codex plugin refresh skipped; re-run sync after installing",
|
|
238
|
+
);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const desiredPluginIds = enabledPluginIdsFromText(sotConfigText);
|
|
243
|
+
let pluginIds = desiredPluginIds;
|
|
244
|
+
if (ctx.skipPluginRefresh) {
|
|
245
|
+
progress("Checking installed Codex plugins...");
|
|
246
|
+
const installedPluginIds = await installedPluginIdsFromCli();
|
|
247
|
+
clearProgress();
|
|
248
|
+
if (installedPluginIds === undefined) {
|
|
249
|
+
warn("Codex plugin inventory unavailable — falling back to the full refresh path");
|
|
250
|
+
} else {
|
|
251
|
+
pluginIds = desiredPluginIds.filter((pluginId) => !installedPluginIds.has(pluginId));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let refreshed = 0;
|
|
256
|
+
let failed = 0;
|
|
257
|
+
for (const pluginId of pluginIds) {
|
|
258
|
+
progress(`Updating Codex plugin ${pluginId}...`);
|
|
259
|
+
const res = await spawnProcess("codex", ["plugin", "add", pluginId], {
|
|
260
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
261
|
+
});
|
|
262
|
+
clearProgress();
|
|
263
|
+
const addOut = `${res.stdout}${res.stderr}`;
|
|
264
|
+
if (res.error === undefined && res.exitCode === 0) {
|
|
265
|
+
refreshed++;
|
|
266
|
+
} else if (addOut.includes("could not find a Codex CLI binary")) {
|
|
267
|
+
recordFailure(
|
|
268
|
+
ctx,
|
|
269
|
+
`Codex plugin refresh hit a stale launcher/wrapper on PATH - install current standalone Codex with: ${standaloneInstallCommand(ctx)}`,
|
|
270
|
+
);
|
|
271
|
+
failed++;
|
|
272
|
+
} else {
|
|
273
|
+
const failureLine = addOut.split("\n")[0] ?? "";
|
|
274
|
+
recordFailure(
|
|
275
|
+
ctx,
|
|
276
|
+
`Codex plugin refresh failed for ${pluginId}: ${failureLine !== "" ? failureLine : "unknown error"}; run manually: codex plugin add ${pluginId}`,
|
|
277
|
+
);
|
|
278
|
+
failed++;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (refreshed > 0) {
|
|
283
|
+
change(`Codex plugins synced (plugins: ~${refreshed})`);
|
|
284
|
+
ctx.nextStepTriggers.codexRestart = true;
|
|
285
|
+
}
|
|
286
|
+
if (ctx.skipPluginRefresh && pluginIds.length === 0)
|
|
287
|
+
verbose("Codex plugins already installed; refresh-only updates skipped");
|
|
288
|
+
if (failed > 0)
|
|
289
|
+
warn(`${failed} Codex plugin operation(s) failed — re-run sync or install manually`);
|
|
290
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EngineNative Codex status reporting: summary lines and next-step hints.
|
|
3
|
+
*/
|
|
4
|
+
import { p } from "./exec";
|
|
5
|
+
import { enabledPluginIds } from "./codexPlugins";
|
|
6
|
+
import type { Ctx } from "./index";
|
|
7
|
+
|
|
8
|
+
// -------------------------------------------------------------- summary ----
|
|
9
|
+
|
|
10
|
+
export function codexSummary(ctx: Ctx): void {
|
|
11
|
+
const { echo } = ctx.services.logger;
|
|
12
|
+
const codexDir = p(ctx.home, ".codex");
|
|
13
|
+
echo(`Codex: ${codexDir}`);
|
|
14
|
+
if (!ctx.dryRun) {
|
|
15
|
+
const count = enabledPluginIds(p(codexDir, "config.toml")).length;
|
|
16
|
+
echo(`Codex plugins: ${count} enabled in config.toml`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function codexNextSteps(ctx: Ctx): Array<string> {
|
|
21
|
+
return ctx.verbose || ctx.nextStepTriggers.codexRestart
|
|
22
|
+
? ["Restart Codex to load any refreshed plugins, skills, or tools."]
|
|
23
|
+
: [];
|
|
24
|
+
}
|