@evo-dev/evodev 0.0.1-alpha → 0.0.1-alpha.10
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/dist/.claude-plugin/marketplace.json +2 -2
- package/dist/assets/agents/review/code-reviewer/examples.md +1 -1
- package/dist/assets/agents/review/code-reviewer/prompt.md +1 -1
- package/dist/assets/agents/review/code-reviewer/verification.md +1 -1
- package/dist/assets/skills/coding/knowledge-distillation/SKILL.md +251 -0
- package/dist/assets/skills/coding/knowledge-distillation/manifest.json +10 -0
- package/dist/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +126 -0
- package/dist/assets/team/agents/code-reviewer.md +48 -0
- package/dist/assets/team/agents/docs-maintainer.md +51 -0
- package/dist/assets/team/agents/implementation-engineer.md +51 -0
- package/dist/assets/team/agents/product-scope-analyst.md +58 -0
- package/dist/assets/team/agents/release-engineer.md +55 -0
- package/dist/assets/team/agents/security-boundary-reviewer.md +50 -0
- package/dist/assets/team/agents/solution-architect.md +51 -0
- package/dist/assets/team/agents/verification-engineer.md +51 -0
- package/dist/assets/team/team.md +102 -0
- package/dist/assets/workflows/rd-bug-fix/WORKFLOW.json +1 -1
- package/dist/assets/workflows/rd-code-review/WORKFLOW.json +1 -1
- package/dist/assets/workflows/rd-docs-update/WORKFLOW.json +1 -1
- package/dist/assets/workflows/rd-feature-implementation/WORKFLOW.json +1 -1
- package/dist/assets/workflows/rd-refactor/WORKFLOW.json +1 -1
- package/dist/assets/workflows/rd-release-readiness/WORKFLOW.json +1 -1
- package/dist/assets/workflows/rd-security-boundary-review/WORKFLOW.json +2 -2
- package/dist/assets/workflows/rd-test-generation/WORKFLOW.json +1 -1
- package/dist/index.js +29774 -9310
- package/dist/plugins/evodev/.claude-plugin/plugin.json +2 -2
- package/dist/plugins/evodev/.codex-plugin/plugin.json +8 -6
- package/dist/plugins/evodev/.mcp.json +6 -0
- package/dist/plugins/evodev/hooks/codex-hooks.json +10 -10
- package/dist/plugins/evodev/hooks/codex.ts +596 -34
- package/dist/plugins/evodev/hooks/hooks.json +18 -18
- package/dist/plugins/evodev/hooks/hooks.ts +470 -25
- package/dist/plugins/evodev/hooks/index.ts +15 -0
- package/dist/plugins/evodev/hooks/paths.ts +44 -1
- package/dist/plugins/evodev/hooks/plugin.ts +160 -9
- package/dist/plugins/evodev/hooks/runtime.ts +234 -43
- package/dist/plugins/evodev/hooks/transform-agent.ts +30 -0
- package/dist/plugins/evodev/hooks/workspace-core.ts +154 -0
- package/dist/plugins/evodev/package.json +3 -3
- package/dist/plugins/evodev/skills/engineering-discipline/SKILL.md +63 -0
- package/dist/plugins/evodev/skills/engineering-discipline/anti-patterns.md +21 -0
- package/dist/plugins/evodev/skills/engineering-discipline/examples.md +19 -0
- package/dist/plugins/evodev/skills/engineering-discipline/verification.md +11 -0
- package/dist/plugins/evodev/skills/knowledge-distillation/SKILL.md +251 -0
- package/dist/plugins/evodev/skills/knowledge-distillation/references/knowledge-distillation-methods.md +126 -0
- package/dist/ui/app.js +9 -0
- package/dist/ui/styles.css +2 -0
- package/package.json +9 -7
- package/dist/evodev +0 -11
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
1
4
|
import type {
|
|
2
5
|
CodeAgentCapabilities,
|
|
3
6
|
CodeAgentPlugin,
|
|
4
7
|
DoctorCheck,
|
|
5
8
|
DoctorContext,
|
|
9
|
+
HookEventType,
|
|
6
10
|
PluginDetectionResult,
|
|
7
11
|
PluginInstallInput,
|
|
8
12
|
PluginInstallResult,
|
|
@@ -11,9 +15,29 @@ import type {
|
|
|
11
15
|
SyncSkillsInput,
|
|
12
16
|
} from "@evo-dev/core/plugins";
|
|
13
17
|
import { runNodeCommand } from "./command-runner.ts";
|
|
18
|
+
import { CODEX_HOOK_RUNTIME_COMMAND } from "./hooks.ts";
|
|
19
|
+
import { type CodexPaths, resolveCodexAgentTargetPath, resolveCodexPaths } from "./paths.ts";
|
|
20
|
+
import { transformCodexAgent } from "./transform-agent.ts";
|
|
21
|
+
import {
|
|
22
|
+
type WorkspaceCoreHydrationResult,
|
|
23
|
+
hydrateWorkspaceCoreDependency,
|
|
24
|
+
isSafePluginCacheSegment,
|
|
25
|
+
} from "./workspace-core.ts";
|
|
14
26
|
|
|
15
|
-
const
|
|
16
|
-
"Codex
|
|
27
|
+
const CODEX_SKILL_BUNDLE_MESSAGE =
|
|
28
|
+
"Codex skills are bundled in the EvoDev Codex plugin; direct skill sync is skipped.";
|
|
29
|
+
const CODEX_CAPABILITY_HOOK_EVENTS: HookEventType[] = [
|
|
30
|
+
"SessionStart",
|
|
31
|
+
"UserPromptSubmit",
|
|
32
|
+
"PreToolUse",
|
|
33
|
+
"PermissionRequest",
|
|
34
|
+
"PostToolUse",
|
|
35
|
+
"PreCompact",
|
|
36
|
+
"PostCompact",
|
|
37
|
+
"SubagentStart",
|
|
38
|
+
"SubagentStop",
|
|
39
|
+
"Stop",
|
|
40
|
+
];
|
|
17
41
|
|
|
18
42
|
export interface CodexCommandResult {
|
|
19
43
|
exitCode: number;
|
|
@@ -29,9 +53,55 @@ export interface CodexPluginOptions {
|
|
|
29
53
|
commandRunner?: CodexCommandRunner;
|
|
30
54
|
pluginSelector?: string;
|
|
31
55
|
pluginMarketplaceSource?: string;
|
|
56
|
+
pluginMarketplaceFallbackSource?: string;
|
|
57
|
+
paths?: CodexPaths;
|
|
58
|
+
syncFileSystem?: CodexSyncFileSystem;
|
|
32
59
|
}
|
|
33
60
|
|
|
61
|
+
export interface CodexSyncFileSystem {
|
|
62
|
+
readFile(path: string): Promise<string>;
|
|
63
|
+
writeFile(path: string, content: string, options?: { overwrite?: boolean }): Promise<void>;
|
|
64
|
+
fileExists(path: string): Promise<boolean>;
|
|
65
|
+
ensureDir(path: string): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const nodeSyncFileSystem: CodexSyncFileSystem = {
|
|
69
|
+
async readFile(path: string): Promise<string> {
|
|
70
|
+
return readFile(path, "utf8");
|
|
71
|
+
},
|
|
72
|
+
async writeFile(
|
|
73
|
+
path: string,
|
|
74
|
+
content: string,
|
|
75
|
+
options: { overwrite?: boolean } = {},
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
await writeFile(path, content, {
|
|
78
|
+
encoding: "utf8",
|
|
79
|
+
flag: options.overwrite === true ? "w" : "wx",
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
async fileExists(path: string): Promise<boolean> {
|
|
83
|
+
try {
|
|
84
|
+
const fileStat = await stat(path);
|
|
85
|
+
return fileStat.isFile();
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (isNotFoundError(error)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
async ensureDir(path: string): Promise<void> {
|
|
95
|
+
await mkdir(path, { recursive: true });
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
34
99
|
export function createCodexPlugin(options: CodexPluginOptions = {}): CodeAgentPlugin {
|
|
100
|
+
const syncFileSystem = options.syncFileSystem ?? nodeSyncFileSystem;
|
|
101
|
+
const getSyncPaths = (homeDir?: string) =>
|
|
102
|
+
options.paths ??
|
|
103
|
+
(homeDir === undefined ? resolveCodexPathsFromHome() : resolveCodexPaths({ homeDir }));
|
|
104
|
+
|
|
35
105
|
return {
|
|
36
106
|
id: "codex",
|
|
37
107
|
name: "Codex",
|
|
@@ -40,9 +110,9 @@ export function createCodexPlugin(options: CodexPluginOptions = {}): CodeAgentPl
|
|
|
40
110
|
},
|
|
41
111
|
async getCapabilities(): Promise<CodeAgentCapabilities> {
|
|
42
112
|
return {
|
|
43
|
-
skills: { supported:
|
|
44
|
-
agents: { supported:
|
|
45
|
-
hooks: { supported:
|
|
113
|
+
skills: { supported: true, format: "codex-skill" },
|
|
114
|
+
agents: { supported: true, format: "codex-agent" },
|
|
115
|
+
hooks: { supported: true, events: [...CODEX_CAPABILITY_HOOK_EVENTS] },
|
|
46
116
|
};
|
|
47
117
|
},
|
|
48
118
|
async doctor(context: DoctorContext): Promise<DoctorCheck[]> {
|
|
@@ -52,29 +122,30 @@ export function createCodexPlugin(options: CodexPluginOptions = {}): CodeAgentPl
|
|
|
52
122
|
return [
|
|
53
123
|
detectionToDoctorCheck(detection),
|
|
54
124
|
{
|
|
55
|
-
id: "codex.
|
|
56
|
-
status: enabled ? "
|
|
125
|
+
id: "codex.skills.plugin-bundled",
|
|
126
|
+
status: enabled ? "pass" : "skip",
|
|
57
127
|
message: enabled
|
|
58
|
-
?
|
|
59
|
-
: `${
|
|
128
|
+
? "Codex skills are packaged in the EvoDev plugin and loaded through Codex plugin installation."
|
|
129
|
+
: `${CODEX_SKILL_BUNDLE_MESSAGE} Codex is disabled in settings.`,
|
|
60
130
|
},
|
|
131
|
+
codexAgentsDoctorCheck(enabled, getSyncPaths(context.homeDir)),
|
|
132
|
+
await codexStopHooksDoctorCheck(getSyncPaths(context.homeDir)),
|
|
61
133
|
];
|
|
62
134
|
},
|
|
63
135
|
async syncSkills(input: SyncSkillsInput): Promise<SyncResult> {
|
|
64
|
-
const result =
|
|
136
|
+
const result = createEmptySyncResult(input.targetPlugin);
|
|
65
137
|
for (const skill of input.skills) {
|
|
66
138
|
result.skipped.push(skill.registryKey);
|
|
139
|
+
if (skill.manifest.targets.includes("codex")) {
|
|
140
|
+
result.warnings.push(
|
|
141
|
+
`${CODEX_SKILL_BUNDLE_MESSAGE} Refresh or reinstall the Codex plugin to update ${skill.registryKey}.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
67
144
|
}
|
|
68
|
-
addCodexSyncDiagnostic(result, input.dryRun, "skill");
|
|
69
145
|
return result;
|
|
70
146
|
},
|
|
71
147
|
async syncAgents(input: SyncAgentsInput): Promise<SyncResult> {
|
|
72
|
-
|
|
73
|
-
for (const agent of input.agents) {
|
|
74
|
-
result.skipped.push(agent.registryKey);
|
|
75
|
-
}
|
|
76
|
-
addCodexSyncDiagnostic(result, input.dryRun, "agent");
|
|
77
|
-
return result;
|
|
148
|
+
return syncCodexAgents(input, getSyncPaths(), syncFileSystem);
|
|
78
149
|
},
|
|
79
150
|
async installPlugin(input: PluginInstallInput): Promise<PluginInstallResult> {
|
|
80
151
|
return installCodexPlugin(input, options);
|
|
@@ -118,24 +189,116 @@ async function detectCodex(
|
|
|
118
189
|
|
|
119
190
|
async function installCodexPlugin(
|
|
120
191
|
input: PluginInstallInput,
|
|
121
|
-
options: Pick<
|
|
192
|
+
options: Pick<
|
|
193
|
+
CodexPluginOptions,
|
|
194
|
+
| "commandRunner"
|
|
195
|
+
| "pluginSelector"
|
|
196
|
+
| "pluginMarketplaceSource"
|
|
197
|
+
| "pluginMarketplaceFallbackSource"
|
|
198
|
+
| "paths"
|
|
199
|
+
>,
|
|
122
200
|
): Promise<PluginInstallResult> {
|
|
123
201
|
const command = "codex";
|
|
124
202
|
const selector = options.pluginSelector ?? "evodev@evodev";
|
|
125
203
|
const args = ["plugin", "add", selector];
|
|
126
204
|
const runner = options.commandRunner ?? new NodeCodexCommandRunner();
|
|
205
|
+
const primaryResult = await installCodexPluginOnce({
|
|
206
|
+
input,
|
|
207
|
+
command,
|
|
208
|
+
selector,
|
|
209
|
+
args,
|
|
210
|
+
runner,
|
|
211
|
+
marketplaceSource: options.pluginMarketplaceSource,
|
|
212
|
+
paths: options.paths,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const fallbackSource = options.pluginMarketplaceFallbackSource;
|
|
216
|
+
if (
|
|
217
|
+
primaryResult.status !== "failed" ||
|
|
218
|
+
options.pluginMarketplaceSource === undefined ||
|
|
219
|
+
fallbackSource === undefined ||
|
|
220
|
+
fallbackSource === options.pluginMarketplaceSource ||
|
|
221
|
+
!isSshGitMarketplaceSource(options.pluginMarketplaceSource)
|
|
222
|
+
) {
|
|
223
|
+
return primaryResult;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const fallbackResult = await installCodexPluginOnce({
|
|
227
|
+
input,
|
|
228
|
+
command,
|
|
229
|
+
selector,
|
|
230
|
+
args,
|
|
231
|
+
runner,
|
|
232
|
+
marketplaceSource: fallbackSource,
|
|
233
|
+
removeMarketplaceBeforeAdd: true,
|
|
234
|
+
paths: options.paths,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
...fallbackResult,
|
|
239
|
+
warnings: [
|
|
240
|
+
`Codex SSH marketplace source failed (${primaryResult.message}); retried with fallback source: ${fallbackSource}.`,
|
|
241
|
+
...fallbackResult.warnings,
|
|
242
|
+
],
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function installCodexPluginOnce(input: {
|
|
247
|
+
input: PluginInstallInput;
|
|
248
|
+
command: string;
|
|
249
|
+
selector: string;
|
|
250
|
+
args: string[];
|
|
251
|
+
runner: CodexCommandRunner;
|
|
252
|
+
marketplaceSource?: string;
|
|
253
|
+
removeMarketplaceBeforeAdd?: boolean;
|
|
254
|
+
paths?: CodexPaths;
|
|
255
|
+
}): Promise<PluginInstallResult> {
|
|
256
|
+
const { command, selector, args, runner } = input;
|
|
257
|
+
const warnings: string[] = [];
|
|
127
258
|
|
|
128
259
|
try {
|
|
129
|
-
if (
|
|
260
|
+
if (input.marketplaceSource !== undefined) {
|
|
261
|
+
if (
|
|
262
|
+
input.removeMarketplaceBeforeAdd === true ||
|
|
263
|
+
(input.input.force === true && isGitMarketplaceSource(input.marketplaceSource))
|
|
264
|
+
) {
|
|
265
|
+
const marketplaceName = extractMarketplaceNameFromSelector(selector);
|
|
266
|
+
if (marketplaceName === undefined) {
|
|
267
|
+
warnings.push(
|
|
268
|
+
`Skipped Codex marketplace remove before refresh because selector does not include a marketplace name: ${selector}`,
|
|
269
|
+
);
|
|
270
|
+
} else {
|
|
271
|
+
const removeMarketplaceResult = await runner.run(command, [
|
|
272
|
+
"plugin",
|
|
273
|
+
"marketplace",
|
|
274
|
+
"remove",
|
|
275
|
+
marketplaceName,
|
|
276
|
+
]);
|
|
277
|
+
const removeMarketplaceMessage =
|
|
278
|
+
removeMarketplaceResult.stderr ?? removeMarketplaceResult.stdout;
|
|
279
|
+
if (
|
|
280
|
+
removeMarketplaceResult.exitCode !== 0 &&
|
|
281
|
+
!isNotInstalledMessage(removeMarketplaceMessage)
|
|
282
|
+
) {
|
|
283
|
+
warnings.push(
|
|
284
|
+
`Codex marketplace refresh remove did not complete: ${
|
|
285
|
+
removeMarketplaceMessage ??
|
|
286
|
+
`Codex marketplace remove exited with code ${removeMarketplaceResult.exitCode}`
|
|
287
|
+
}`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
130
293
|
const marketplaceResult = await runner.run(command, [
|
|
131
294
|
"plugin",
|
|
132
295
|
"marketplace",
|
|
133
296
|
"add",
|
|
134
|
-
|
|
297
|
+
input.marketplaceSource,
|
|
135
298
|
]);
|
|
136
299
|
if (marketplaceResult.exitCode !== 0) {
|
|
137
300
|
return createPluginInstallResult(
|
|
138
|
-
input.targetPlugin,
|
|
301
|
+
input.input.targetPlugin,
|
|
139
302
|
"failed",
|
|
140
303
|
command,
|
|
141
304
|
args,
|
|
@@ -144,21 +307,78 @@ async function installCodexPlugin(
|
|
|
144
307
|
`Codex marketplace add exited with code ${marketplaceResult.exitCode}`,
|
|
145
308
|
);
|
|
146
309
|
}
|
|
310
|
+
|
|
311
|
+
if (isGitMarketplaceSource(input.marketplaceSource)) {
|
|
312
|
+
const marketplaceName = extractMarketplaceNameFromSelector(selector);
|
|
313
|
+
if (marketplaceName === undefined) {
|
|
314
|
+
warnings.push(
|
|
315
|
+
`Skipped Codex marketplace upgrade because selector does not include a marketplace name: ${selector}`,
|
|
316
|
+
);
|
|
317
|
+
} else {
|
|
318
|
+
const upgradeResult = await runner.run(command, [
|
|
319
|
+
"plugin",
|
|
320
|
+
"marketplace",
|
|
321
|
+
"upgrade",
|
|
322
|
+
marketplaceName,
|
|
323
|
+
]);
|
|
324
|
+
if (upgradeResult.exitCode !== 0) {
|
|
325
|
+
return createPluginInstallResult(
|
|
326
|
+
input.input.targetPlugin,
|
|
327
|
+
"failed",
|
|
328
|
+
command,
|
|
329
|
+
args,
|
|
330
|
+
upgradeResult.stderr ??
|
|
331
|
+
upgradeResult.stdout ??
|
|
332
|
+
`Codex marketplace upgrade exited with code ${upgradeResult.exitCode}`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (input.input.force === true) {
|
|
340
|
+
const removeArgs = ["plugin", "remove", selector];
|
|
341
|
+
const removeResult = await runner.run(command, removeArgs);
|
|
342
|
+
const removeMessage = removeResult.stderr ?? removeResult.stdout;
|
|
343
|
+
if (removeResult.exitCode !== 0 && !isNotInstalledMessage(removeMessage)) {
|
|
344
|
+
warnings.push(
|
|
345
|
+
`Codex plugin refresh remove did not complete: ${
|
|
346
|
+
removeMessage ?? `Codex plugin remove exited with code ${removeResult.exitCode}`
|
|
347
|
+
}`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
147
350
|
}
|
|
148
351
|
|
|
149
352
|
const result = await runner.run(command, args);
|
|
150
353
|
if (result.exitCode === 0) {
|
|
354
|
+
const hydration = await hydrateCodexWorkspaceCoreDependency({
|
|
355
|
+
paths: input.paths,
|
|
356
|
+
selector,
|
|
357
|
+
});
|
|
358
|
+
warnings.push(...hydration.warnings);
|
|
359
|
+
if (hydration.errors.length > 0) {
|
|
360
|
+
return createPluginInstallResult(
|
|
361
|
+
input.input.targetPlugin,
|
|
362
|
+
"failed",
|
|
363
|
+
command,
|
|
364
|
+
args,
|
|
365
|
+
hydration.errors.join("\n"),
|
|
366
|
+
warnings,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
151
370
|
return createPluginInstallResult(
|
|
152
|
-
input.targetPlugin,
|
|
371
|
+
input.input.targetPlugin,
|
|
153
372
|
"installed",
|
|
154
373
|
command,
|
|
155
374
|
args,
|
|
156
375
|
result.stdout,
|
|
376
|
+
warnings,
|
|
157
377
|
);
|
|
158
378
|
}
|
|
159
379
|
|
|
160
380
|
return createPluginInstallResult(
|
|
161
|
-
input.targetPlugin,
|
|
381
|
+
input.input.targetPlugin,
|
|
162
382
|
"failed",
|
|
163
383
|
command,
|
|
164
384
|
args,
|
|
@@ -166,7 +386,7 @@ async function installCodexPlugin(
|
|
|
166
386
|
);
|
|
167
387
|
} catch (error) {
|
|
168
388
|
return createPluginInstallResult(
|
|
169
|
-
input.targetPlugin,
|
|
389
|
+
input.input.targetPlugin,
|
|
170
390
|
"failed",
|
|
171
391
|
command,
|
|
172
392
|
args,
|
|
@@ -175,6 +395,149 @@ async function installCodexPlugin(
|
|
|
175
395
|
}
|
|
176
396
|
}
|
|
177
397
|
|
|
398
|
+
async function hydrateCodexWorkspaceCoreDependency(input: {
|
|
399
|
+
paths?: CodexPaths;
|
|
400
|
+
selector: string;
|
|
401
|
+
}): Promise<WorkspaceCoreHydrationResult> {
|
|
402
|
+
const warnings: string[] = [];
|
|
403
|
+
const errors: string[] = [];
|
|
404
|
+
|
|
405
|
+
if (input.paths === undefined) {
|
|
406
|
+
return { warnings, errors };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const marketplaceName = extractMarketplaceNameFromSelector(input.selector);
|
|
410
|
+
const pluginName = extractPluginNameFromSelector(input.selector);
|
|
411
|
+
if (marketplaceName === undefined || pluginName === undefined) {
|
|
412
|
+
warnings.push(
|
|
413
|
+
`Skipped @evo-dev/core hydration because Codex plugin selector is not marketplace-qualified: ${input.selector}`,
|
|
414
|
+
);
|
|
415
|
+
return { warnings, errors };
|
|
416
|
+
}
|
|
417
|
+
if (!isSafePluginCacheSegment(marketplaceName) || !isSafePluginCacheSegment(pluginName)) {
|
|
418
|
+
warnings.push(
|
|
419
|
+
`Skipped @evo-dev/core hydration because Codex plugin selector contains an unsafe cache segment: ${input.selector}`,
|
|
420
|
+
);
|
|
421
|
+
return { warnings, errors };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const cachePluginRoot = `${input.paths.codexDir}/plugins/cache/${marketplaceName}/${pluginName}`;
|
|
425
|
+
const marketplaceCoreRoot = `${input.paths.codexDir}/.tmp/marketplaces/${marketplaceName}/packages/core`;
|
|
426
|
+
const hydration = await hydrateWorkspaceCoreDependency({
|
|
427
|
+
sourceCoreRoot: marketplaceCoreRoot,
|
|
428
|
+
cachePluginRoot,
|
|
429
|
+
runtimeLabel: "Codex",
|
|
430
|
+
});
|
|
431
|
+
const hookPatch = await patchCodexInstalledHookRuntimeCommands({ cachePluginRoot });
|
|
432
|
+
return {
|
|
433
|
+
warnings: [...warnings, ...hydration.warnings, ...hookPatch.warnings],
|
|
434
|
+
errors: [...errors, ...hydration.errors, ...hookPatch.errors],
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function patchCodexInstalledHookRuntimeCommands(input: {
|
|
439
|
+
cachePluginRoot: string;
|
|
440
|
+
}): Promise<WorkspaceCoreHydrationResult> {
|
|
441
|
+
const warnings: string[] = [];
|
|
442
|
+
const errors: string[] = [];
|
|
443
|
+
const installedPluginRoot = await findLatestInstalledCodexPluginRoot(input.cachePluginRoot);
|
|
444
|
+
if (installedPluginRoot === null) {
|
|
445
|
+
warnings.push(
|
|
446
|
+
`Skipped Codex hook runtime command patch because the plugin cache was not found: ${input.cachePluginRoot}`,
|
|
447
|
+
);
|
|
448
|
+
return { warnings, errors };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const hooksPath = `${installedPluginRoot}/hooks/codex-hooks.json`;
|
|
452
|
+
try {
|
|
453
|
+
const hooksText = await readTextFileIfExists(hooksPath);
|
|
454
|
+
if (hooksText === null || hooksText.trim() === "") {
|
|
455
|
+
warnings.push(
|
|
456
|
+
`Skipped Codex hook runtime command patch because hooks file is missing: ${hooksPath}`,
|
|
457
|
+
);
|
|
458
|
+
} else {
|
|
459
|
+
const hooksConfig = JSON.parse(hooksText) as unknown;
|
|
460
|
+
if (rewriteCodexHookRuntimeCommands(hooksConfig)) {
|
|
461
|
+
await writeFile(hooksPath, `${JSON.stringify(hooksConfig, null, 2)}\n`, "utf8");
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
} catch (error) {
|
|
465
|
+
errors.push(
|
|
466
|
+
`Cannot patch Codex hook runtime command in installed plugin cache: ${describeError(error)}`,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const hooksSourcePath = `${installedPluginRoot}/hooks/hooks.ts`;
|
|
471
|
+
try {
|
|
472
|
+
const hooksSource = await readTextFileIfExists(hooksSourcePath);
|
|
473
|
+
if (hooksSource !== null) {
|
|
474
|
+
const next = hooksSource.replaceAll(
|
|
475
|
+
'bun run "${PLUGIN_ROOT}/hooks/runtime.ts" hook runtime --target codex',
|
|
476
|
+
CODEX_HOOK_RUNTIME_COMMAND,
|
|
477
|
+
);
|
|
478
|
+
if (next !== hooksSource) {
|
|
479
|
+
await writeFile(hooksSourcePath, next, "utf8");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} catch (error) {
|
|
483
|
+
errors.push(
|
|
484
|
+
`Cannot patch Codex hook runtime source in installed plugin cache: ${describeError(error)}`,
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return { warnings, errors };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function rewriteCodexHookRuntimeCommands(value: unknown): boolean {
|
|
492
|
+
if (!isRecord(value)) return false;
|
|
493
|
+
const root = isRecord(value.hooks) ? value.hooks : value;
|
|
494
|
+
let changed = false;
|
|
495
|
+
for (const groups of Object.values(root)) {
|
|
496
|
+
if (!Array.isArray(groups)) continue;
|
|
497
|
+
for (const group of groups) {
|
|
498
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) continue;
|
|
499
|
+
for (const hook of group.hooks) {
|
|
500
|
+
if (!isRecord(hook) || hook.type !== "command" || typeof hook.command !== "string") {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (
|
|
504
|
+
hook.command.includes("hook runtime --target codex") &&
|
|
505
|
+
hook.command !== CODEX_HOOK_RUNTIME_COMMAND
|
|
506
|
+
) {
|
|
507
|
+
hook.command = CODEX_HOOK_RUNTIME_COMMAND;
|
|
508
|
+
changed = true;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return changed;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function findLatestInstalledCodexPluginRoot(cachePluginRoot: string): Promise<string | null> {
|
|
517
|
+
let entries: Dirent[];
|
|
518
|
+
try {
|
|
519
|
+
entries = await readdir(cachePluginRoot, { withFileTypes: true });
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if (isNotFoundError(error)) return null;
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const versionDirs = await Promise.all(
|
|
526
|
+
entries
|
|
527
|
+
.filter((entry) => entry.isDirectory())
|
|
528
|
+
.map(async (entry) => {
|
|
529
|
+
const path = `${cachePluginRoot}/${entry.name}`;
|
|
530
|
+
const pathStat = await stat(path);
|
|
531
|
+
return { path, mtimeMs: pathStat.mtimeMs };
|
|
532
|
+
}),
|
|
533
|
+
);
|
|
534
|
+
if (versionDirs.length === 0) return null;
|
|
535
|
+
versionDirs.sort(
|
|
536
|
+
(left, right) => right.mtimeMs - left.mtimeMs || right.path.localeCompare(left.path),
|
|
537
|
+
);
|
|
538
|
+
return versionDirs[0].path;
|
|
539
|
+
}
|
|
540
|
+
|
|
178
541
|
class NodeCodexCommandRunner implements CodexCommandRunner {
|
|
179
542
|
async run(command: string, args: string[]): Promise<CodexCommandResult> {
|
|
180
543
|
return runNodeCommand(command, args);
|
|
@@ -193,7 +556,151 @@ function detectionToDoctorCheck(detection: PluginDetectionResult): DoctorCheck {
|
|
|
193
556
|
return { id: "codex.available", status: "warn", message: detection.message };
|
|
194
557
|
}
|
|
195
558
|
|
|
196
|
-
function
|
|
559
|
+
async function codexStopHooksDoctorCheck(paths: CodexPaths): Promise<DoctorCheck> {
|
|
560
|
+
const hooksText = await readTextFileIfExists(paths.hooksPath);
|
|
561
|
+
if (hooksText === null || hooksText.trim() === "") {
|
|
562
|
+
return {
|
|
563
|
+
id: "codex.hooks.stop",
|
|
564
|
+
status: "skip",
|
|
565
|
+
message: `No user-level Codex hooks file found at: ${paths.hooksPath}`,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
let hooksConfig: unknown;
|
|
570
|
+
try {
|
|
571
|
+
hooksConfig = JSON.parse(hooksText) as unknown;
|
|
572
|
+
} catch {
|
|
573
|
+
return {
|
|
574
|
+
id: "codex.hooks.stop",
|
|
575
|
+
status: "warn",
|
|
576
|
+
message: `Codex hooks file is not valid JSON; existing Stop hook failures may come from: ${paths.hooksPath}`,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const stopHooks = collectCodexCommandHooks(hooksConfig, "Stop");
|
|
581
|
+
if (stopHooks.length === 0) {
|
|
582
|
+
return {
|
|
583
|
+
id: "codex.hooks.stop",
|
|
584
|
+
status: "skip",
|
|
585
|
+
message: `No user-level Codex Stop hooks configured in: ${paths.hooksPath}`,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const hooksMissingTimeout = stopHooks.filter((hook) => hook.timeout === null);
|
|
590
|
+
if (hooksMissingTimeout.length > 0) {
|
|
591
|
+
return {
|
|
592
|
+
id: "codex.hooks.stop",
|
|
593
|
+
status: "warn",
|
|
594
|
+
message: `Codex Stop hook command(s) without timeout may be killed by the host and show "hook exited without a status code": ${hooksMissingTimeout
|
|
595
|
+
.map((hook) => hook.label)
|
|
596
|
+
.join(", ")}`,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
id: "codex.hooks.stop",
|
|
602
|
+
status: "pass",
|
|
603
|
+
message: `Codex Stop hooks have explicit timeouts in: ${paths.hooksPath}`,
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function collectCodexCommandHooks(
|
|
608
|
+
hooksConfig: unknown,
|
|
609
|
+
eventName: string,
|
|
610
|
+
): Array<{ label: string; timeout: number | null }> {
|
|
611
|
+
if (!isRecord(hooksConfig)) return [];
|
|
612
|
+
const root = isRecord(hooksConfig.hooks) ? hooksConfig.hooks : hooksConfig;
|
|
613
|
+
const groups = isRecord(root) && Array.isArray(root[eventName]) ? root[eventName] : [];
|
|
614
|
+
const hooks: Array<{ label: string; timeout: number | null }> = [];
|
|
615
|
+
|
|
616
|
+
groups.forEach((group, groupIndex) => {
|
|
617
|
+
if (!isRecord(group) || !Array.isArray(group.hooks)) return;
|
|
618
|
+
group.hooks.forEach((hook, hookIndex) => {
|
|
619
|
+
if (!isRecord(hook) || typeof hook.command !== "string" || hook.type !== "command") return;
|
|
620
|
+
hooks.push({
|
|
621
|
+
label: `${eventName}[${groupIndex}].hooks[${hookIndex}] ${summarizeHookCommand(
|
|
622
|
+
hook.command,
|
|
623
|
+
)}`,
|
|
624
|
+
timeout:
|
|
625
|
+
typeof hook.timeout === "number" && Number.isFinite(hook.timeout) ? hook.timeout : null,
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
return hooks;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function summarizeHookCommand(command: string): string {
|
|
634
|
+
const compact = command.replace(/\s+/g, " ").trim();
|
|
635
|
+
if (compact.includes("@dp/ab-agent-collect-event")) return "@dp/ab-agent-collect-event";
|
|
636
|
+
if (compact.includes("flux-hooks")) return "flux-hooks";
|
|
637
|
+
if (compact.includes("flux-bits-report")) return "flux-bits-report";
|
|
638
|
+
if (compact.includes("hook runtime --target codex")) return "evodev codex hook runtime";
|
|
639
|
+
return compact.length <= 80 ? compact : `${compact.slice(0, 77)}...`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async function readTextFileIfExists(path: string): Promise<string | null> {
|
|
643
|
+
try {
|
|
644
|
+
return await readFile(path, "utf8");
|
|
645
|
+
} catch (error) {
|
|
646
|
+
if (isNotFoundError(error)) return null;
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
652
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
async function syncCodexAgents(
|
|
656
|
+
input: SyncAgentsInput,
|
|
657
|
+
paths: CodexPaths,
|
|
658
|
+
fileSystem: CodexSyncFileSystem,
|
|
659
|
+
): Promise<SyncResult> {
|
|
660
|
+
const result = createEmptySyncResult(input.targetPlugin);
|
|
661
|
+
|
|
662
|
+
for (const asset of input.agents) {
|
|
663
|
+
if (!asset.manifest.targets.includes("codex")) {
|
|
664
|
+
result.skipped.push(asset.registryKey);
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
try {
|
|
669
|
+
const source = await fileSystem.readFile(asset.entryPath);
|
|
670
|
+
const transformed = transformCodexAgent({ asset, source });
|
|
671
|
+
const targetPath = resolveCodexAgentTargetPath(transformed.name, paths);
|
|
672
|
+
const targetExists = await fileSystem.fileExists(targetPath);
|
|
673
|
+
|
|
674
|
+
if (targetExists && input.force !== true) {
|
|
675
|
+
result.skipped.push(asset.registryKey);
|
|
676
|
+
result.warnings.push(`Skipped existing Codex agent target: ${targetPath}`);
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (!input.dryRun) {
|
|
681
|
+
await fileSystem.ensureDir(dirname(targetPath));
|
|
682
|
+
await fileSystem.writeFile(targetPath, ensureTrailingNewline(transformed.content), {
|
|
683
|
+
overwrite: input.force === true,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (targetExists) {
|
|
688
|
+
result.warnings.push(
|
|
689
|
+
`${input.dryRun ? "Would update" : "Updated"} existing Codex agent target: ${targetPath}`,
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
result.syncedAgents.push(asset.registryKey);
|
|
693
|
+
} catch (error) {
|
|
694
|
+
result.errors.push(
|
|
695
|
+
`Failed to sync Codex agent ${asset.registryKey}: ${describeError(error)}`,
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function createEmptySyncResult(targetPlugin: string): SyncResult {
|
|
197
704
|
return {
|
|
198
705
|
targetPlugin,
|
|
199
706
|
syncedSkills: [],
|
|
@@ -204,14 +711,20 @@ function createSkippedSyncResult(targetPlugin: string): SyncResult {
|
|
|
204
711
|
};
|
|
205
712
|
}
|
|
206
713
|
|
|
207
|
-
function
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
714
|
+
function codexAgentsDoctorCheck(enabled: boolean, paths: CodexPaths): DoctorCheck {
|
|
715
|
+
if (!enabled) {
|
|
716
|
+
return {
|
|
717
|
+
id: "codex.paths.agents",
|
|
718
|
+
status: "skip",
|
|
719
|
+
message: `Codex agent sync is disabled in settings. Planned user-level path: ${paths.agentsDir}`,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
id: "codex.paths.agents",
|
|
725
|
+
status: "pass",
|
|
726
|
+
message: `Codex agents will sync to user-level TOML files under: ${paths.agentsDir}`,
|
|
727
|
+
};
|
|
215
728
|
}
|
|
216
729
|
|
|
217
730
|
function createPluginInstallResult(
|
|
@@ -220,6 +733,7 @@ function createPluginInstallResult(
|
|
|
220
733
|
command: string,
|
|
221
734
|
args: string[],
|
|
222
735
|
message?: string,
|
|
736
|
+
warnings: string[] = [],
|
|
223
737
|
): PluginInstallResult {
|
|
224
738
|
const normalizedMessage = message?.trim() || `${command} ${args.join(" ")}`;
|
|
225
739
|
return {
|
|
@@ -228,11 +742,39 @@ function createPluginInstallResult(
|
|
|
228
742
|
command,
|
|
229
743
|
args,
|
|
230
744
|
message: normalizedMessage,
|
|
231
|
-
warnings
|
|
745
|
+
warnings,
|
|
232
746
|
errors: status === "failed" ? [normalizedMessage] : [],
|
|
233
747
|
};
|
|
234
748
|
}
|
|
235
749
|
|
|
750
|
+
function isNotInstalledMessage(message: string | undefined): boolean {
|
|
751
|
+
return message !== undefined && /(not\s+installed|not\s+found|no\s+installed)/i.test(message);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function extractMarketplaceNameFromSelector(selector: string): string | undefined {
|
|
755
|
+
const separator = selector.lastIndexOf("@");
|
|
756
|
+
if (separator <= 0 || separator === selector.length - 1) return undefined;
|
|
757
|
+
return selector.slice(separator + 1);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function extractPluginNameFromSelector(selector: string): string | undefined {
|
|
761
|
+
const separator = selector.lastIndexOf("@");
|
|
762
|
+
const pluginName = separator <= 0 ? selector : selector.slice(0, separator);
|
|
763
|
+
return pluginName.trim() === "" ? undefined : pluginName;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function isGitMarketplaceSource(source: string): boolean {
|
|
767
|
+
const trimmed = source.trim();
|
|
768
|
+
if (/^(https?|ssh|git):\/\//i.test(trimmed)) return true;
|
|
769
|
+
if (/^git@[^:]+:.+/i.test(trimmed)) return true;
|
|
770
|
+
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.test(trimmed);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function isSshGitMarketplaceSource(source: string): boolean {
|
|
774
|
+
const trimmed = source.trim();
|
|
775
|
+
return /^ssh:\/\//i.test(trimmed) || /^git@[^:]+:.+/i.test(trimmed);
|
|
776
|
+
}
|
|
777
|
+
|
|
236
778
|
function isMissingExit(exitCode: number, stderr?: string): boolean {
|
|
237
779
|
if (exitCode === 127) return true;
|
|
238
780
|
const normalized = stderr?.toLowerCase() ?? "";
|
|
@@ -248,6 +790,12 @@ function isCommandMissingError(error: unknown): boolean {
|
|
|
248
790
|
);
|
|
249
791
|
}
|
|
250
792
|
|
|
793
|
+
function isNotFoundError(error: unknown): boolean {
|
|
794
|
+
return (
|
|
795
|
+
error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT"
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
251
799
|
function describeError(error: unknown): string {
|
|
252
800
|
if (error instanceof Error) {
|
|
253
801
|
return error.message;
|
|
@@ -255,3 +803,17 @@ function describeError(error: unknown): string {
|
|
|
255
803
|
|
|
256
804
|
return String(error);
|
|
257
805
|
}
|
|
806
|
+
|
|
807
|
+
function resolveCodexPathsFromHome(): CodexPaths {
|
|
808
|
+
const home = process.env.HOME;
|
|
809
|
+
|
|
810
|
+
if (home === undefined || home.trim() === "") {
|
|
811
|
+
throw new Error("Cannot resolve Codex paths: HOME is not set");
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return resolveCodexPaths({ homeDir: home });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function ensureTrailingNewline(content: string): string {
|
|
818
|
+
return content.endsWith("\n") ? content : `${content}\n`;
|
|
819
|
+
}
|