@evo-dev/evodev 0.0.1-alpha → 0.0.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/.claude-plugin/marketplace.json +2 -2
  2. package/dist/assets/agents/review/code-reviewer/examples.md +1 -1
  3. package/dist/assets/agents/review/code-reviewer/prompt.md +1 -1
  4. package/dist/assets/agents/review/code-reviewer/verification.md +1 -1
  5. package/dist/assets/skills/coding/knowledge-distillation/SKILL.md +248 -0
  6. package/dist/assets/skills/coding/knowledge-distillation/manifest.json +10 -0
  7. package/dist/assets/skills/coding/knowledge-distillation/references/knowledge-distillation-methods.md +122 -0
  8. package/dist/assets/workflows/rd-bug-fix/WORKFLOW.json +1 -1
  9. package/dist/assets/workflows/rd-code-review/WORKFLOW.json +1 -1
  10. package/dist/assets/workflows/rd-docs-update/WORKFLOW.json +1 -1
  11. package/dist/assets/workflows/rd-feature-implementation/WORKFLOW.json +1 -1
  12. package/dist/assets/workflows/rd-refactor/WORKFLOW.json +1 -1
  13. package/dist/assets/workflows/rd-release-readiness/WORKFLOW.json +1 -1
  14. package/dist/assets/workflows/rd-security-boundary-review/WORKFLOW.json +2 -2
  15. package/dist/assets/workflows/rd-test-generation/WORKFLOW.json +1 -1
  16. package/dist/index.js +13114 -6432
  17. package/dist/plugins/evodev/.claude-plugin/plugin.json +2 -2
  18. package/dist/plugins/evodev/.codex-plugin/plugin.json +8 -6
  19. package/dist/plugins/evodev/.mcp.json +6 -0
  20. package/dist/plugins/evodev/hooks/codex-hooks.json +10 -10
  21. package/dist/plugins/evodev/hooks/codex.ts +393 -34
  22. package/dist/plugins/evodev/hooks/hooks.json +18 -18
  23. package/dist/plugins/evodev/hooks/hooks.ts +268 -10
  24. package/dist/plugins/evodev/hooks/index.ts +15 -0
  25. package/dist/plugins/evodev/hooks/paths.ts +44 -1
  26. package/dist/plugins/evodev/hooks/plugin.ts +160 -9
  27. package/dist/plugins/evodev/hooks/runtime.ts +146 -31
  28. package/dist/plugins/evodev/hooks/transform-agent.ts +30 -0
  29. package/dist/plugins/evodev/hooks/workspace-core.ts +181 -0
  30. package/dist/plugins/evodev/package.json +3 -3
  31. package/dist/plugins/evodev/skills/engineering-discipline/SKILL.md +63 -0
  32. package/dist/plugins/evodev/skills/engineering-discipline/anti-patterns.md +21 -0
  33. package/dist/plugins/evodev/skills/engineering-discipline/examples.md +19 -0
  34. package/dist/plugins/evodev/skills/engineering-discipline/verification.md +11 -0
  35. package/dist/plugins/evodev/skills/knowledge-distillation/SKILL.md +248 -0
  36. package/dist/plugins/evodev/skills/knowledge-distillation/references/knowledge-distillation-methods.md +122 -0
  37. package/package.json +3 -6
@@ -1,8 +1,11 @@
1
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
1
3
  import type {
2
4
  CodeAgentCapabilities,
3
5
  CodeAgentPlugin,
4
6
  DoctorCheck,
5
7
  DoctorContext,
8
+ HookEventType,
6
9
  PluginDetectionResult,
7
10
  PluginInstallInput,
8
11
  PluginInstallResult,
@@ -11,9 +14,28 @@ import type {
11
14
  SyncSkillsInput,
12
15
  } from "@evo-dev/core/plugins";
13
16
  import { runNodeCommand } from "./command-runner.ts";
17
+ import { type CodexPaths, resolveCodexAgentTargetPath, resolveCodexPaths } from "./paths.ts";
18
+ import { transformCodexAgent } from "./transform-agent.ts";
19
+ import {
20
+ type WorkspaceCoreHydrationResult,
21
+ hydrateWorkspaceCoreDependency,
22
+ isSafePluginCacheSegment,
23
+ } from "./workspace-core.ts";
14
24
 
15
- const CODEX_RESERVED_MESSAGE =
16
- "Codex asset sync is reserved until Codex capabilities and formats are verified.";
25
+ const CODEX_SKILL_BUNDLE_MESSAGE =
26
+ "Codex skills are bundled in the EvoDev Codex plugin; direct skill sync is skipped.";
27
+ const CODEX_CAPABILITY_HOOK_EVENTS: HookEventType[] = [
28
+ "SessionStart",
29
+ "UserPromptSubmit",
30
+ "PreToolUse",
31
+ "PermissionRequest",
32
+ "PostToolUse",
33
+ "PreCompact",
34
+ "PostCompact",
35
+ "SubagentStart",
36
+ "SubagentStop",
37
+ "Stop",
38
+ ];
17
39
 
18
40
  export interface CodexCommandResult {
19
41
  exitCode: number;
@@ -29,9 +51,55 @@ export interface CodexPluginOptions {
29
51
  commandRunner?: CodexCommandRunner;
30
52
  pluginSelector?: string;
31
53
  pluginMarketplaceSource?: string;
54
+ pluginMarketplaceFallbackSource?: string;
55
+ paths?: CodexPaths;
56
+ syncFileSystem?: CodexSyncFileSystem;
32
57
  }
33
58
 
59
+ export interface CodexSyncFileSystem {
60
+ readFile(path: string): Promise<string>;
61
+ writeFile(path: string, content: string, options?: { overwrite?: boolean }): Promise<void>;
62
+ fileExists(path: string): Promise<boolean>;
63
+ ensureDir(path: string): Promise<void>;
64
+ }
65
+
66
+ const nodeSyncFileSystem: CodexSyncFileSystem = {
67
+ async readFile(path: string): Promise<string> {
68
+ return readFile(path, "utf8");
69
+ },
70
+ async writeFile(
71
+ path: string,
72
+ content: string,
73
+ options: { overwrite?: boolean } = {},
74
+ ): Promise<void> {
75
+ await writeFile(path, content, {
76
+ encoding: "utf8",
77
+ flag: options.overwrite === true ? "w" : "wx",
78
+ });
79
+ },
80
+ async fileExists(path: string): Promise<boolean> {
81
+ try {
82
+ const fileStat = await stat(path);
83
+ return fileStat.isFile();
84
+ } catch (error) {
85
+ if (isNotFoundError(error)) {
86
+ return false;
87
+ }
88
+
89
+ throw error;
90
+ }
91
+ },
92
+ async ensureDir(path: string): Promise<void> {
93
+ await mkdir(path, { recursive: true });
94
+ },
95
+ };
96
+
34
97
  export function createCodexPlugin(options: CodexPluginOptions = {}): CodeAgentPlugin {
98
+ const syncFileSystem = options.syncFileSystem ?? nodeSyncFileSystem;
99
+ const getSyncPaths = (homeDir?: string) =>
100
+ options.paths ??
101
+ (homeDir === undefined ? resolveCodexPathsFromHome() : resolveCodexPaths({ homeDir }));
102
+
35
103
  return {
36
104
  id: "codex",
37
105
  name: "Codex",
@@ -40,9 +108,9 @@ export function createCodexPlugin(options: CodexPluginOptions = {}): CodeAgentPl
40
108
  },
41
109
  async getCapabilities(): Promise<CodeAgentCapabilities> {
42
110
  return {
43
- skills: { supported: false, format: "codex-skill" },
44
- agents: { supported: false, format: "codex-agent" },
45
- hooks: { supported: false, events: [] },
111
+ skills: { supported: true, format: "codex-skill" },
112
+ agents: { supported: true, format: "codex-agent" },
113
+ hooks: { supported: true, events: [...CODEX_CAPABILITY_HOOK_EVENTS] },
46
114
  };
47
115
  },
48
116
  async doctor(context: DoctorContext): Promise<DoctorCheck[]> {
@@ -52,29 +120,29 @@ export function createCodexPlugin(options: CodexPluginOptions = {}): CodeAgentPl
52
120
  return [
53
121
  detectionToDoctorCheck(detection),
54
122
  {
55
- id: "codex.assets.reserved",
56
- status: enabled ? "warn" : "skip",
123
+ id: "codex.skills.plugin-bundled",
124
+ status: enabled ? "pass" : "skip",
57
125
  message: enabled
58
- ? `${CODEX_RESERVED_MESSAGE} Code Agent plugin install is supported, but Codex skill/agent asset sync remains disabled.`
59
- : `${CODEX_RESERVED_MESSAGE} Codex is disabled in settings.`,
126
+ ? "Codex skills are packaged in the EvoDev plugin and loaded through Codex plugin installation."
127
+ : `${CODEX_SKILL_BUNDLE_MESSAGE} Codex is disabled in settings.`,
60
128
  },
129
+ codexAgentsDoctorCheck(enabled, getSyncPaths(context.homeDir)),
61
130
  ];
62
131
  },
63
132
  async syncSkills(input: SyncSkillsInput): Promise<SyncResult> {
64
- const result = createSkippedSyncResult(input.targetPlugin);
133
+ const result = createEmptySyncResult(input.targetPlugin);
65
134
  for (const skill of input.skills) {
66
135
  result.skipped.push(skill.registryKey);
136
+ if (skill.manifest.targets.includes("codex")) {
137
+ result.warnings.push(
138
+ `${CODEX_SKILL_BUNDLE_MESSAGE} Refresh or reinstall the Codex plugin to update ${skill.registryKey}.`,
139
+ );
140
+ }
67
141
  }
68
- addCodexSyncDiagnostic(result, input.dryRun, "skill");
69
142
  return result;
70
143
  },
71
144
  async syncAgents(input: SyncAgentsInput): Promise<SyncResult> {
72
- const result = createSkippedSyncResult(input.targetPlugin);
73
- for (const agent of input.agents) {
74
- result.skipped.push(agent.registryKey);
75
- }
76
- addCodexSyncDiagnostic(result, input.dryRun, "agent");
77
- return result;
145
+ return syncCodexAgents(input, getSyncPaths(), syncFileSystem);
78
146
  },
79
147
  async installPlugin(input: PluginInstallInput): Promise<PluginInstallResult> {
80
148
  return installCodexPlugin(input, options);
@@ -118,24 +186,116 @@ async function detectCodex(
118
186
 
119
187
  async function installCodexPlugin(
120
188
  input: PluginInstallInput,
121
- options: Pick<CodexPluginOptions, "commandRunner" | "pluginSelector" | "pluginMarketplaceSource">,
189
+ options: Pick<
190
+ CodexPluginOptions,
191
+ | "commandRunner"
192
+ | "pluginSelector"
193
+ | "pluginMarketplaceSource"
194
+ | "pluginMarketplaceFallbackSource"
195
+ | "paths"
196
+ >,
122
197
  ): Promise<PluginInstallResult> {
123
198
  const command = "codex";
124
199
  const selector = options.pluginSelector ?? "evodev@evodev";
125
200
  const args = ["plugin", "add", selector];
126
201
  const runner = options.commandRunner ?? new NodeCodexCommandRunner();
202
+ const primaryResult = await installCodexPluginOnce({
203
+ input,
204
+ command,
205
+ selector,
206
+ args,
207
+ runner,
208
+ marketplaceSource: options.pluginMarketplaceSource,
209
+ paths: options.paths,
210
+ });
211
+
212
+ const fallbackSource = options.pluginMarketplaceFallbackSource;
213
+ if (
214
+ primaryResult.status !== "failed" ||
215
+ options.pluginMarketplaceSource === undefined ||
216
+ fallbackSource === undefined ||
217
+ fallbackSource === options.pluginMarketplaceSource ||
218
+ !isSshGitMarketplaceSource(options.pluginMarketplaceSource)
219
+ ) {
220
+ return primaryResult;
221
+ }
222
+
223
+ const fallbackResult = await installCodexPluginOnce({
224
+ input,
225
+ command,
226
+ selector,
227
+ args,
228
+ runner,
229
+ marketplaceSource: fallbackSource,
230
+ removeMarketplaceBeforeAdd: true,
231
+ paths: options.paths,
232
+ });
233
+
234
+ return {
235
+ ...fallbackResult,
236
+ warnings: [
237
+ `Codex SSH marketplace source failed (${primaryResult.message}); retried with fallback source: ${fallbackSource}.`,
238
+ ...fallbackResult.warnings,
239
+ ],
240
+ };
241
+ }
242
+
243
+ async function installCodexPluginOnce(input: {
244
+ input: PluginInstallInput;
245
+ command: string;
246
+ selector: string;
247
+ args: string[];
248
+ runner: CodexCommandRunner;
249
+ marketplaceSource?: string;
250
+ removeMarketplaceBeforeAdd?: boolean;
251
+ paths?: CodexPaths;
252
+ }): Promise<PluginInstallResult> {
253
+ const { command, selector, args, runner } = input;
254
+ const warnings: string[] = [];
127
255
 
128
256
  try {
129
- if (options.pluginMarketplaceSource !== undefined) {
257
+ if (input.marketplaceSource !== undefined) {
258
+ if (
259
+ input.removeMarketplaceBeforeAdd === true ||
260
+ (input.input.force === true && isGitMarketplaceSource(input.marketplaceSource))
261
+ ) {
262
+ const marketplaceName = extractMarketplaceNameFromSelector(selector);
263
+ if (marketplaceName === undefined) {
264
+ warnings.push(
265
+ `Skipped Codex marketplace remove before refresh because selector does not include a marketplace name: ${selector}`,
266
+ );
267
+ } else {
268
+ const removeMarketplaceResult = await runner.run(command, [
269
+ "plugin",
270
+ "marketplace",
271
+ "remove",
272
+ marketplaceName,
273
+ ]);
274
+ const removeMarketplaceMessage =
275
+ removeMarketplaceResult.stderr ?? removeMarketplaceResult.stdout;
276
+ if (
277
+ removeMarketplaceResult.exitCode !== 0 &&
278
+ !isNotInstalledMessage(removeMarketplaceMessage)
279
+ ) {
280
+ warnings.push(
281
+ `Codex marketplace refresh remove did not complete: ${
282
+ removeMarketplaceMessage ??
283
+ `Codex marketplace remove exited with code ${removeMarketplaceResult.exitCode}`
284
+ }`,
285
+ );
286
+ }
287
+ }
288
+ }
289
+
130
290
  const marketplaceResult = await runner.run(command, [
131
291
  "plugin",
132
292
  "marketplace",
133
293
  "add",
134
- options.pluginMarketplaceSource,
294
+ input.marketplaceSource,
135
295
  ]);
136
296
  if (marketplaceResult.exitCode !== 0) {
137
297
  return createPluginInstallResult(
138
- input.targetPlugin,
298
+ input.input.targetPlugin,
139
299
  "failed",
140
300
  command,
141
301
  args,
@@ -144,21 +304,78 @@ async function installCodexPlugin(
144
304
  `Codex marketplace add exited with code ${marketplaceResult.exitCode}`,
145
305
  );
146
306
  }
307
+
308
+ if (isGitMarketplaceSource(input.marketplaceSource)) {
309
+ const marketplaceName = extractMarketplaceNameFromSelector(selector);
310
+ if (marketplaceName === undefined) {
311
+ warnings.push(
312
+ `Skipped Codex marketplace upgrade because selector does not include a marketplace name: ${selector}`,
313
+ );
314
+ } else {
315
+ const upgradeResult = await runner.run(command, [
316
+ "plugin",
317
+ "marketplace",
318
+ "upgrade",
319
+ marketplaceName,
320
+ ]);
321
+ if (upgradeResult.exitCode !== 0) {
322
+ return createPluginInstallResult(
323
+ input.input.targetPlugin,
324
+ "failed",
325
+ command,
326
+ args,
327
+ upgradeResult.stderr ??
328
+ upgradeResult.stdout ??
329
+ `Codex marketplace upgrade exited with code ${upgradeResult.exitCode}`,
330
+ );
331
+ }
332
+ }
333
+ }
334
+ }
335
+
336
+ if (input.input.force === true) {
337
+ const removeArgs = ["plugin", "remove", selector];
338
+ const removeResult = await runner.run(command, removeArgs);
339
+ const removeMessage = removeResult.stderr ?? removeResult.stdout;
340
+ if (removeResult.exitCode !== 0 && !isNotInstalledMessage(removeMessage)) {
341
+ warnings.push(
342
+ `Codex plugin refresh remove did not complete: ${
343
+ removeMessage ?? `Codex plugin remove exited with code ${removeResult.exitCode}`
344
+ }`,
345
+ );
346
+ }
147
347
  }
148
348
 
149
349
  const result = await runner.run(command, args);
150
350
  if (result.exitCode === 0) {
351
+ const hydration = await hydrateCodexWorkspaceCoreDependency({
352
+ paths: input.paths,
353
+ selector,
354
+ });
355
+ warnings.push(...hydration.warnings);
356
+ if (hydration.errors.length > 0) {
357
+ return createPluginInstallResult(
358
+ input.input.targetPlugin,
359
+ "failed",
360
+ command,
361
+ args,
362
+ hydration.errors.join("\n"),
363
+ warnings,
364
+ );
365
+ }
366
+
151
367
  return createPluginInstallResult(
152
- input.targetPlugin,
368
+ input.input.targetPlugin,
153
369
  "installed",
154
370
  command,
155
371
  args,
156
372
  result.stdout,
373
+ warnings,
157
374
  );
158
375
  }
159
376
 
160
377
  return createPluginInstallResult(
161
- input.targetPlugin,
378
+ input.input.targetPlugin,
162
379
  "failed",
163
380
  command,
164
381
  args,
@@ -166,7 +383,7 @@ async function installCodexPlugin(
166
383
  );
167
384
  } catch (error) {
168
385
  return createPluginInstallResult(
169
- input.targetPlugin,
386
+ input.input.targetPlugin,
170
387
  "failed",
171
388
  command,
172
389
  args,
@@ -175,6 +392,45 @@ async function installCodexPlugin(
175
392
  }
176
393
  }
177
394
 
395
+ async function hydrateCodexWorkspaceCoreDependency(input: {
396
+ paths?: CodexPaths;
397
+ selector: string;
398
+ }): Promise<WorkspaceCoreHydrationResult> {
399
+ const warnings: string[] = [];
400
+ const errors: string[] = [];
401
+
402
+ if (input.paths === undefined) {
403
+ return { warnings, errors };
404
+ }
405
+
406
+ const marketplaceName = extractMarketplaceNameFromSelector(input.selector);
407
+ const pluginName = extractPluginNameFromSelector(input.selector);
408
+ if (marketplaceName === undefined || pluginName === undefined) {
409
+ warnings.push(
410
+ `Skipped @evo-dev/core hydration because Codex plugin selector is not marketplace-qualified: ${input.selector}`,
411
+ );
412
+ return { warnings, errors };
413
+ }
414
+ if (!isSafePluginCacheSegment(marketplaceName) || !isSafePluginCacheSegment(pluginName)) {
415
+ warnings.push(
416
+ `Skipped @evo-dev/core hydration because Codex plugin selector contains an unsafe cache segment: ${input.selector}`,
417
+ );
418
+ return { warnings, errors };
419
+ }
420
+
421
+ const cachePluginRoot = `${input.paths.codexDir}/plugins/cache/${marketplaceName}/${pluginName}`;
422
+ const marketplaceCoreRoot = `${input.paths.codexDir}/.tmp/marketplaces/${marketplaceName}/packages/core`;
423
+ const hydration = await hydrateWorkspaceCoreDependency({
424
+ sourceCoreRoot: marketplaceCoreRoot,
425
+ cachePluginRoot,
426
+ runtimeLabel: "Codex",
427
+ });
428
+ return {
429
+ warnings: [...warnings, ...hydration.warnings],
430
+ errors: [...errors, ...hydration.errors],
431
+ };
432
+ }
433
+
178
434
  class NodeCodexCommandRunner implements CodexCommandRunner {
179
435
  async run(command: string, args: string[]): Promise<CodexCommandResult> {
180
436
  return runNodeCommand(command, args);
@@ -193,7 +449,55 @@ function detectionToDoctorCheck(detection: PluginDetectionResult): DoctorCheck {
193
449
  return { id: "codex.available", status: "warn", message: detection.message };
194
450
  }
195
451
 
196
- function createSkippedSyncResult(targetPlugin: string): SyncResult {
452
+ async function syncCodexAgents(
453
+ input: SyncAgentsInput,
454
+ paths: CodexPaths,
455
+ fileSystem: CodexSyncFileSystem,
456
+ ): Promise<SyncResult> {
457
+ const result = createEmptySyncResult(input.targetPlugin);
458
+
459
+ for (const asset of input.agents) {
460
+ if (!asset.manifest.targets.includes("codex")) {
461
+ result.skipped.push(asset.registryKey);
462
+ continue;
463
+ }
464
+
465
+ try {
466
+ const source = await fileSystem.readFile(asset.entryPath);
467
+ const transformed = transformCodexAgent({ asset, source });
468
+ const targetPath = resolveCodexAgentTargetPath(transformed.name, paths);
469
+ const targetExists = await fileSystem.fileExists(targetPath);
470
+
471
+ if (targetExists && input.force !== true) {
472
+ result.skipped.push(asset.registryKey);
473
+ result.warnings.push(`Skipped existing Codex agent target: ${targetPath}`);
474
+ continue;
475
+ }
476
+
477
+ if (!input.dryRun) {
478
+ await fileSystem.ensureDir(dirname(targetPath));
479
+ await fileSystem.writeFile(targetPath, ensureTrailingNewline(transformed.content), {
480
+ overwrite: input.force === true,
481
+ });
482
+ }
483
+
484
+ if (targetExists) {
485
+ result.warnings.push(
486
+ `${input.dryRun ? "Would update" : "Updated"} existing Codex agent target: ${targetPath}`,
487
+ );
488
+ }
489
+ result.syncedAgents.push(asset.registryKey);
490
+ } catch (error) {
491
+ result.errors.push(
492
+ `Failed to sync Codex agent ${asset.registryKey}: ${describeError(error)}`,
493
+ );
494
+ }
495
+ }
496
+
497
+ return result;
498
+ }
499
+
500
+ function createEmptySyncResult(targetPlugin: string): SyncResult {
197
501
  return {
198
502
  targetPlugin,
199
503
  syncedSkills: [],
@@ -204,14 +508,20 @@ function createSkippedSyncResult(targetPlugin: string): SyncResult {
204
508
  };
205
509
  }
206
510
 
207
- function addCodexSyncDiagnostic(
208
- result: SyncResult,
209
- dryRun: boolean,
210
- assetKind: "skill" | "agent",
211
- ): void {
212
- const message = `Codex ${assetKind} sync unsupported: capabilities are unverified; no writes planned in I10.`;
213
- if (dryRun) result.warnings.push(message);
214
- else result.errors.push(message);
511
+ function codexAgentsDoctorCheck(enabled: boolean, paths: CodexPaths): DoctorCheck {
512
+ if (!enabled) {
513
+ return {
514
+ id: "codex.paths.agents",
515
+ status: "skip",
516
+ message: `Codex agent sync is disabled in settings. Planned user-level path: ${paths.agentsDir}`,
517
+ };
518
+ }
519
+
520
+ return {
521
+ id: "codex.paths.agents",
522
+ status: "pass",
523
+ message: `Codex agents will sync to user-level TOML files under: ${paths.agentsDir}`,
524
+ };
215
525
  }
216
526
 
217
527
  function createPluginInstallResult(
@@ -220,6 +530,7 @@ function createPluginInstallResult(
220
530
  command: string,
221
531
  args: string[],
222
532
  message?: string,
533
+ warnings: string[] = [],
223
534
  ): PluginInstallResult {
224
535
  const normalizedMessage = message?.trim() || `${command} ${args.join(" ")}`;
225
536
  return {
@@ -228,11 +539,39 @@ function createPluginInstallResult(
228
539
  command,
229
540
  args,
230
541
  message: normalizedMessage,
231
- warnings: [],
542
+ warnings,
232
543
  errors: status === "failed" ? [normalizedMessage] : [],
233
544
  };
234
545
  }
235
546
 
547
+ function isNotInstalledMessage(message: string | undefined): boolean {
548
+ return message !== undefined && /(not\s+installed|not\s+found|no\s+installed)/i.test(message);
549
+ }
550
+
551
+ function extractMarketplaceNameFromSelector(selector: string): string | undefined {
552
+ const separator = selector.lastIndexOf("@");
553
+ if (separator <= 0 || separator === selector.length - 1) return undefined;
554
+ return selector.slice(separator + 1);
555
+ }
556
+
557
+ function extractPluginNameFromSelector(selector: string): string | undefined {
558
+ const separator = selector.lastIndexOf("@");
559
+ const pluginName = separator <= 0 ? selector : selector.slice(0, separator);
560
+ return pluginName.trim() === "" ? undefined : pluginName;
561
+ }
562
+
563
+ function isGitMarketplaceSource(source: string): boolean {
564
+ const trimmed = source.trim();
565
+ if (/^(https?|ssh|git):\/\//i.test(trimmed)) return true;
566
+ if (/^git@[^:]+:.+/i.test(trimmed)) return true;
567
+ return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?$/.test(trimmed);
568
+ }
569
+
570
+ function isSshGitMarketplaceSource(source: string): boolean {
571
+ const trimmed = source.trim();
572
+ return /^ssh:\/\//i.test(trimmed) || /^git@[^:]+:.+/i.test(trimmed);
573
+ }
574
+
236
575
  function isMissingExit(exitCode: number, stderr?: string): boolean {
237
576
  if (exitCode === 127) return true;
238
577
  const normalized = stderr?.toLowerCase() ?? "";
@@ -248,6 +587,12 @@ function isCommandMissingError(error: unknown): boolean {
248
587
  );
249
588
  }
250
589
 
590
+ function isNotFoundError(error: unknown): boolean {
591
+ return (
592
+ error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT"
593
+ );
594
+ }
595
+
251
596
  function describeError(error: unknown): string {
252
597
  if (error instanceof Error) {
253
598
  return error.message;
@@ -255,3 +600,17 @@ function describeError(error: unknown): string {
255
600
 
256
601
  return String(error);
257
602
  }
603
+
604
+ function resolveCodexPathsFromHome(): CodexPaths {
605
+ const home = process.env.HOME;
606
+
607
+ if (home === undefined || home.trim() === "") {
608
+ throw new Error("Cannot resolve Codex paths: HOME is not set");
609
+ }
610
+
611
+ return resolveCodexPaths({ homeDir: home });
612
+ }
613
+
614
+ function ensureTrailingNewline(content: string): string {
615
+ return content.endsWith("\n") ? content : `${content}\n`;
616
+ }