@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
@@ -23,6 +23,11 @@ import {
23
23
  } from "./paths.ts";
24
24
  import { transformClaudeAgent } from "./transform-agent.ts";
25
25
  import { transformClaudeSkill } from "./transform-skill.ts";
26
+ import {
27
+ type WorkspaceCoreHydrationResult,
28
+ hydrateWorkspaceCoreDependency,
29
+ isSafePluginCacheSegment,
30
+ } from "./workspace-core.ts";
26
31
 
27
32
  export interface ClaudePathAccessResult {
28
33
  exists: boolean;
@@ -37,7 +42,7 @@ export interface ClaudePathAccess {
37
42
 
38
43
  export interface ClaudeSyncFileSystem {
39
44
  readFile(path: string): Promise<string>;
40
- writeFile(path: string, content: string): Promise<void>;
45
+ writeFile(path: string, content: string, options?: { overwrite?: boolean }): Promise<void>;
41
46
  fileExists(path: string): Promise<boolean>;
42
47
  ensureDir(path: string): Promise<void>;
43
48
  }
@@ -142,8 +147,15 @@ const nodeSyncFileSystem: ClaudeSyncFileSystem = {
142
147
  async readFile(path: string): Promise<string> {
143
148
  return readFile(path, "utf8");
144
149
  },
145
- async writeFile(path: string, content: string): Promise<void> {
146
- await writeFile(path, content, { encoding: "utf8", flag: "wx" });
150
+ async writeFile(
151
+ path: string,
152
+ content: string,
153
+ options: { overwrite?: boolean } = {},
154
+ ): Promise<void> {
155
+ await writeFile(path, content, {
156
+ encoding: "utf8",
157
+ flag: options.overwrite === true ? "w" : "wx",
158
+ });
147
159
  },
148
160
  async fileExists(path: string): Promise<boolean> {
149
161
  try {
@@ -174,7 +186,7 @@ async function installClaudeCodePlugin(
174
186
  input: PluginInstallInput,
175
187
  options: Pick<
176
188
  ClaudePluginOptions,
177
- "commandRunner" | "pluginSelector" | "pluginMarketplaceSource" | "pluginScope"
189
+ "commandRunner" | "paths" | "pluginSelector" | "pluginMarketplaceSource" | "pluginScope"
178
190
  >,
179
191
  ): Promise<PluginInstallResult> {
180
192
  const command = "claude";
@@ -182,6 +194,7 @@ async function installClaudeCodePlugin(
182
194
  const scope = options.pluginScope ?? "user";
183
195
  const args = ["plugin", "install", selector, "--scope", scope];
184
196
  const runner = options.commandRunner ?? new NodeClaudeCommandRunner();
197
+ const warnings: string[] = [];
185
198
 
186
199
  try {
187
200
  if (options.pluginMarketplaceSource !== undefined) {
@@ -204,10 +217,73 @@ async function installClaudeCodePlugin(
204
217
  `Claude Code marketplace add exited with code ${marketplaceResult.exitCode}`,
205
218
  );
206
219
  }
220
+
221
+ const marketplaceName = extractMarketplaceNameFromSelector(selector);
222
+ if (marketplaceName === undefined) {
223
+ warnings.push(
224
+ `Skipped Claude Code marketplace update because selector does not include a marketplace name: ${selector}`,
225
+ );
226
+ } else {
227
+ const updateResult = await runner.run(command, [
228
+ "plugin",
229
+ "marketplace",
230
+ "update",
231
+ marketplaceName,
232
+ ]);
233
+ if (updateResult.exitCode !== 0) {
234
+ return createPluginInstallResult(
235
+ input.targetPlugin,
236
+ "failed",
237
+ command,
238
+ args,
239
+ updateResult.stderr ??
240
+ updateResult.stdout ??
241
+ `Claude Code marketplace update exited with code ${updateResult.exitCode}`,
242
+ );
243
+ }
244
+ }
245
+ }
246
+
247
+ if (input.force === true) {
248
+ const uninstallArgs = [
249
+ "plugin",
250
+ "uninstall",
251
+ selector,
252
+ "--scope",
253
+ scope,
254
+ "--keep-data",
255
+ "-y",
256
+ ];
257
+ const uninstallResult = await runner.run(command, uninstallArgs);
258
+ const uninstallMessage = uninstallResult.stderr ?? uninstallResult.stdout;
259
+ if (uninstallResult.exitCode !== 0 && !isNotInstalledMessage(uninstallMessage)) {
260
+ warnings.push(
261
+ `Claude Code plugin refresh uninstall did not complete: ${
262
+ uninstallMessage ??
263
+ `Claude Code plugin uninstall exited with code ${uninstallResult.exitCode}`
264
+ }`,
265
+ );
266
+ }
207
267
  }
208
268
 
209
269
  const result = await runner.run(command, args);
210
270
  if (result.exitCode === 0) {
271
+ const hydration = await hydrateClaudeWorkspaceCoreDependency({
272
+ paths: options.paths,
273
+ selector,
274
+ });
275
+ warnings.push(...hydration.warnings);
276
+ if (hydration.errors.length > 0) {
277
+ return createPluginInstallResult(
278
+ input.targetPlugin,
279
+ "failed",
280
+ command,
281
+ args,
282
+ hydration.errors.join("\n"),
283
+ warnings,
284
+ );
285
+ }
286
+
211
287
  if (isAlreadyInstalledMessage(result.stdout ?? result.stderr)) {
212
288
  return createPluginInstallResult(
213
289
  input.targetPlugin,
@@ -216,7 +292,10 @@ async function installClaudeCodePlugin(
216
292
  args,
217
293
  result.stdout,
218
294
  [
219
- "Claude Code reported the plugin is already installed; if local plugin files changed without a version bump, Claude may keep the existing cached copy. Bump the plugin version or uninstall and reinstall to refresh the cache.",
295
+ ...warnings,
296
+ input.force === true
297
+ ? "Claude Code reported the plugin is already installed after forced refresh; check the local plugin cache if changes are not visible."
298
+ : "Claude Code reported the plugin is already installed; if local plugin files changed without a version bump, Claude may keep the existing cached copy. Bump the plugin version or uninstall and reinstall to refresh the cache.",
220
299
  ],
221
300
  );
222
301
  }
@@ -227,6 +306,7 @@ async function installClaudeCodePlugin(
227
306
  command,
228
307
  args,
229
308
  result.stdout,
309
+ warnings,
230
310
  );
231
311
  }
232
312
 
@@ -254,6 +334,61 @@ function isAlreadyInstalledMessage(message: string | undefined): boolean {
254
334
  return message !== undefined && /already\s+installed/i.test(message);
255
335
  }
256
336
 
337
+ function isNotInstalledMessage(message: string | undefined): boolean {
338
+ return message !== undefined && /(not\s+installed|not\s+found|no\s+installed)/i.test(message);
339
+ }
340
+
341
+ function extractMarketplaceNameFromSelector(selector: string): string | undefined {
342
+ const separator = selector.lastIndexOf("@");
343
+ if (separator <= 0 || separator === selector.length - 1) return undefined;
344
+ return selector.slice(separator + 1);
345
+ }
346
+
347
+ function extractPluginNameFromSelector(selector: string): string | undefined {
348
+ const separator = selector.lastIndexOf("@");
349
+ const pluginName = separator <= 0 ? selector : selector.slice(0, separator);
350
+ return pluginName.trim() === "" ? undefined : pluginName;
351
+ }
352
+
353
+ async function hydrateClaudeWorkspaceCoreDependency(input: {
354
+ paths?: ClaudePaths;
355
+ selector: string;
356
+ }): Promise<WorkspaceCoreHydrationResult> {
357
+ const warnings: string[] = [];
358
+ const errors: string[] = [];
359
+
360
+ if (input.paths === undefined) {
361
+ return { warnings, errors };
362
+ }
363
+
364
+ const marketplaceName = extractMarketplaceNameFromSelector(input.selector);
365
+ const pluginName = extractPluginNameFromSelector(input.selector);
366
+ if (marketplaceName === undefined || pluginName === undefined) {
367
+ warnings.push(
368
+ `Skipped @evo-dev/core hydration because Claude plugin selector is not marketplace-qualified: ${input.selector}`,
369
+ );
370
+ return { warnings, errors };
371
+ }
372
+ if (!isSafePluginCacheSegment(marketplaceName) || !isSafePluginCacheSegment(pluginName)) {
373
+ warnings.push(
374
+ `Skipped @evo-dev/core hydration because Claude plugin selector contains an unsafe cache segment: ${input.selector}`,
375
+ );
376
+ return { warnings, errors };
377
+ }
378
+
379
+ const cachePluginRoot = `${input.paths.claudeDir}/plugins/cache/${marketplaceName}/${pluginName}`;
380
+ const marketplaceCoreRoot = `${input.paths.claudeDir}/plugins/marketplaces/${marketplaceName}/packages/core`;
381
+ const hydration = await hydrateWorkspaceCoreDependency({
382
+ sourceCoreRoot: marketplaceCoreRoot,
383
+ cachePluginRoot,
384
+ runtimeLabel: "Claude",
385
+ });
386
+ return {
387
+ warnings: [...warnings, ...hydration.warnings],
388
+ errors: [...errors, ...hydration.errors],
389
+ };
390
+ }
391
+
257
392
  class NodeClaudeCommandRunner implements ClaudeCommandRunner {
258
393
  async run(
259
394
  command: string,
@@ -384,8 +519,9 @@ async function syncClaudeSkills(
384
519
  const source = await fileSystem.readFile(asset.entryPath);
385
520
  const transformed = transformClaudeSkill({ asset, source });
386
521
  const targetPath = resolveClaudeSkillTargetPath(transformed.name, paths);
522
+ const targetExists = await fileSystem.fileExists(targetPath);
387
523
 
388
- if (await fileSystem.fileExists(targetPath)) {
524
+ if (targetExists && input.force !== true) {
389
525
  result.skipped.push(asset.registryKey);
390
526
  result.warnings.push(`Skipped existing Claude skill target: ${targetPath}`);
391
527
  continue;
@@ -393,9 +529,16 @@ async function syncClaudeSkills(
393
529
 
394
530
  if (!input.dryRun) {
395
531
  await fileSystem.ensureDir(dirname(targetPath));
396
- await fileSystem.writeFile(targetPath, ensureTrailingNewline(transformed.content));
532
+ await fileSystem.writeFile(targetPath, ensureTrailingNewline(transformed.content), {
533
+ overwrite: input.force === true,
534
+ });
397
535
  }
398
536
 
537
+ if (targetExists) {
538
+ result.warnings.push(
539
+ `${input.dryRun ? "Would update" : "Updated"} existing Claude skill target: ${targetPath}`,
540
+ );
541
+ }
399
542
  result.syncedSkills.push(asset.registryKey);
400
543
  } catch (error) {
401
544
  result.errors.push(
@@ -424,8 +567,9 @@ async function syncClaudeAgents(
424
567
  const source = await fileSystem.readFile(asset.entryPath);
425
568
  const transformed = transformClaudeAgent({ asset, source });
426
569
  const targetPath = resolveClaudeAgentTargetPath(transformed.name, paths);
570
+ const targetExists = await fileSystem.fileExists(targetPath);
427
571
 
428
- if (await fileSystem.fileExists(targetPath)) {
572
+ if (targetExists && input.force !== true) {
429
573
  result.skipped.push(asset.registryKey);
430
574
  result.warnings.push(`Skipped existing Claude agent target: ${targetPath}`);
431
575
  continue;
@@ -433,9 +577,16 @@ async function syncClaudeAgents(
433
577
 
434
578
  if (!input.dryRun) {
435
579
  await fileSystem.ensureDir(dirname(targetPath));
436
- await fileSystem.writeFile(targetPath, ensureTrailingNewline(transformed.content));
580
+ await fileSystem.writeFile(targetPath, ensureTrailingNewline(transformed.content), {
581
+ overwrite: input.force === true,
582
+ });
437
583
  }
438
584
 
585
+ if (targetExists) {
586
+ result.warnings.push(
587
+ `${input.dryRun ? "Would update" : "Updated"} existing Claude agent target: ${targetPath}`,
588
+ );
589
+ }
439
590
  result.syncedAgents.push(asset.registryKey);
440
591
  } catch (error) {
441
592
  result.errors.push(
@@ -1,15 +1,21 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import {
4
+ type HookEventV1,
5
+ type HookRuntimeResult,
6
+ appendTraceLogEntry,
4
7
  createCoreConfigStore,
5
8
  createDefaultSettings,
9
+ createTraceLogEntry,
6
10
  formatHookRuntimeOutput,
7
11
  handleHookRuntime,
12
+ resolveTraceTeamContext,
8
13
  } from "@evo-dev/core";
9
14
  import { normalizeClaudeRuntimeHookPayload, normalizeCodexRuntimeHookPayload } from "./hooks.ts";
10
15
 
11
16
  export interface HookRuntimeCliOptions {
12
17
  argv?: string[];
18
+ environment?: Record<string, string | undefined>;
13
19
  homeDir?: string;
14
20
  stdin?: string | (() => Promise<string>);
15
21
  write?: (message: string) => void;
@@ -17,41 +23,89 @@ export interface HookRuntimeCliOptions {
17
23
 
18
24
  export async function runHookRuntimeCli(options: HookRuntimeCliOptions = {}): Promise<number> {
19
25
  const argv = options.argv ?? process.argv.slice(2);
26
+ const environment = options.environment ?? process.env;
20
27
  const write = options.write ?? process.stdout.write.bind(process.stdout);
28
+ const startedAt = new Date();
29
+ let homeDir = options.homeDir ?? process.env.HOME ?? "~";
30
+ let target = "unknown";
31
+ let payload: Record<string, unknown> | null = null;
32
+ let stdinBytes: number | null = null;
21
33
 
22
- if (argv[0] !== "hook" || argv[1] !== "runtime") {
23
- throw new Error(`Unknown plugin command: ${argv.join(" ")}`.trim());
24
- }
34
+ try {
35
+ if (argv[0] !== "hook" || argv[1] !== "runtime") {
36
+ throw new Error(`Unknown plugin command: ${argv.join(" ")}`.trim());
37
+ }
25
38
 
26
- const flags = parseHookRuntimeFlags(argv.slice(2));
27
- if (flags.target !== "claude" && flags.target !== "codex") {
28
- throw new Error(`Unsupported hook target: ${flags.target}`);
29
- }
39
+ const flags = parseHookRuntimeFlags(argv.slice(2));
40
+ target = flags.target;
41
+ if (flags.target !== "claude" && flags.target !== "codex") {
42
+ throw new Error(`Unsupported hook target: ${flags.target}`);
43
+ }
44
+
45
+ homeDir = options.homeDir ?? process.env.HOME ?? "~";
46
+ const stdin = await readHookRuntimeStdin(options.stdin);
47
+ stdinBytes = Buffer.byteLength(stdin, "utf8");
48
+ payload = JSON.parse(stdin) as Record<string, unknown>;
49
+ await appendTraceLogSafely({
50
+ homeDir,
51
+ phase: "started",
52
+ target,
53
+ argv,
54
+ payload,
55
+ stdinBytes,
56
+ environment,
57
+ });
30
58
 
31
- const homeDir = options.homeDir ?? process.env.HOME ?? "~";
32
- const payload = JSON.parse(await readHookRuntimeStdin(options.stdin)) as Record<string, unknown>;
33
- const settings = await readHookSettings(homeDir);
34
- const event =
35
- flags.target === "codex"
36
- ? normalizeCodexRuntimeHookPayload({
37
- payload,
38
- receivedAt: new Date().toISOString(),
39
- })
40
- : normalizeClaudeRuntimeHookPayload({
41
- payload,
42
- receivedAt: new Date().toISOString(),
43
- });
44
- const result = await handleHookRuntime({
45
- target: flags.target,
46
- homeDir,
47
- settings: settings.hooks,
48
- event,
49
- rawPayload: payload,
50
- receivedAt: event.time.receivedAt,
51
- });
52
- const formatted = formatHookRuntimeOutput(result);
53
- if (formatted.length > 0) write(formatted);
54
- return 0;
59
+ const settings = await readHookSettings(homeDir);
60
+ const event =
61
+ flags.target === "codex"
62
+ ? normalizeCodexRuntimeHookPayload({
63
+ payload,
64
+ receivedAt: new Date().toISOString(),
65
+ })
66
+ : normalizeClaudeRuntimeHookPayload({
67
+ payload,
68
+ receivedAt: new Date().toISOString(),
69
+ });
70
+ const result = await handleHookRuntime({
71
+ target: flags.target,
72
+ homeDir,
73
+ settings: settings.hooks,
74
+ event,
75
+ rawPayload: payload,
76
+ receivedAt: event.time.receivedAt,
77
+ environment,
78
+ });
79
+ const formatted = formatHookRuntimeOutput(result);
80
+ await appendTraceLogSafely({
81
+ homeDir,
82
+ phase: "completed",
83
+ target,
84
+ argv,
85
+ payload,
86
+ stdinBytes,
87
+ event,
88
+ result,
89
+ formatted,
90
+ environment,
91
+ durationMs: Date.now() - startedAt.getTime(),
92
+ });
93
+ if (formatted.length > 0) write(formatted);
94
+ return 0;
95
+ } catch (error) {
96
+ await appendTraceLogSafely({
97
+ homeDir,
98
+ phase: "failed",
99
+ target,
100
+ argv,
101
+ payload,
102
+ stdinBytes,
103
+ error,
104
+ environment,
105
+ durationMs: Date.now() - startedAt.getTime(),
106
+ });
107
+ throw error;
108
+ }
55
109
  }
56
110
 
57
111
  function parseHookRuntimeFlags(argv: string[]): { target: string } {
@@ -95,6 +149,67 @@ async function readHookSettings(homeDir: string) {
95
149
  }
96
150
  }
97
151
 
152
+ async function appendTraceLogSafely(input: {
153
+ homeDir: string;
154
+ phase: "started" | "completed" | "failed";
155
+ target: string;
156
+ argv: string[];
157
+ payload: Record<string, unknown> | null;
158
+ stdinBytes: number | null;
159
+ event?: HookEventV1;
160
+ result?: HookRuntimeResult;
161
+ formatted?: string;
162
+ durationMs?: number | null;
163
+ error?: unknown;
164
+ environment?: Record<string, string | undefined>;
165
+ }): Promise<void> {
166
+ try {
167
+ await appendTraceLogEntry(
168
+ input.homeDir,
169
+ createTraceLogEntry({
170
+ phase: input.phase,
171
+ target: input.target,
172
+ runtime: {
173
+ surface: "plugin",
174
+ argv: input.argv,
175
+ runtimeFile: import.meta.url,
176
+ },
177
+ payload: input.payload,
178
+ stdinBytes: input.stdinBytes,
179
+ team: resolveTraceTeamContext({
180
+ homeDir: input.homeDir,
181
+ environment: input.environment,
182
+ payload: input.payload,
183
+ }),
184
+ event:
185
+ input.event === undefined
186
+ ? null
187
+ : {
188
+ eventId: input.event.eventId,
189
+ type: input.event.type,
190
+ summary: input.event.payload.summary,
191
+ metadata: input.event.payload.metadata,
192
+ decision: input.event.decision,
193
+ },
194
+ result:
195
+ input.result === undefined
196
+ ? null
197
+ : {
198
+ enabled: input.result.enabled,
199
+ summary: input.result.summary,
200
+ stateWrites: input.result.stateWrites,
201
+ warnings: input.result.warnings,
202
+ formattedResponse: input.formatted?.trim() || null,
203
+ durationMs: input.durationMs ?? null,
204
+ },
205
+ error: input.error,
206
+ }),
207
+ );
208
+ } catch {
209
+ // Trace logging must never change hook runtime behavior.
210
+ }
211
+ }
212
+
98
213
  function isNotFoundError(error: unknown): boolean {
99
214
  return (
100
215
  error instanceof Error &&
@@ -10,6 +10,16 @@ export interface ClaudeAgentTransformResult {
10
10
  content: string;
11
11
  }
12
12
 
13
+ export interface CodexAgentTransformInput {
14
+ asset: ScannedAsset<AgentManifest>;
15
+ source: string;
16
+ }
17
+
18
+ export interface CodexAgentTransformResult {
19
+ name: string;
20
+ content: string;
21
+ }
22
+
13
23
  export function transformClaudeAgent(input: ClaudeAgentTransformInput): ClaudeAgentTransformResult {
14
24
  const { manifest } = input.asset;
15
25
  const frontmatter = [
@@ -25,6 +35,26 @@ export function transformClaudeAgent(input: ClaudeAgentTransformInput): ClaudeAg
25
35
  };
26
36
  }
27
37
 
38
+ export function transformCodexAgent(input: CodexAgentTransformInput): CodexAgentTransformResult {
39
+ const { manifest } = input.asset;
40
+ const lines = [
41
+ `# Generated by EvoDev from ${input.asset.registryKey}`,
42
+ `name = ${tomlString(manifest.id)}`,
43
+ `description = ${tomlString(manifest.description)}`,
44
+ `developer_instructions = ${tomlString(input.source)}`,
45
+ "",
46
+ ];
47
+
48
+ return {
49
+ name: manifest.id,
50
+ content: lines.join("\n"),
51
+ };
52
+ }
53
+
28
54
  function escapeFrontmatterValue(value: string): string {
29
55
  return value.replaceAll("\n", " ");
30
56
  }
57
+
58
+ function tomlString(value: string): string {
59
+ return JSON.stringify(value);
60
+ }
@@ -0,0 +1,181 @@
1
+ import type { Dirent } from "node:fs";
2
+ import { cp, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+
5
+ export interface WorkspaceCoreHydrationResult {
6
+ warnings: string[];
7
+ errors: string[];
8
+ }
9
+
10
+ export async function hydrateWorkspaceCoreDependency(input: {
11
+ sourceCoreRoot: string;
12
+ cachePluginRoot: string;
13
+ runtimeLabel: string;
14
+ }): Promise<WorkspaceCoreHydrationResult> {
15
+ const warnings: string[] = [];
16
+ const errors: string[] = [];
17
+
18
+ const installedPluginRoot = await findInstalledPluginRoot(input.cachePluginRoot);
19
+ if (installedPluginRoot === null) {
20
+ warnings.push(
21
+ `Skipped @evo-dev/core hydration because the ${input.runtimeLabel} plugin cache was not found: ${input.cachePluginRoot}`,
22
+ );
23
+ return { warnings, errors };
24
+ }
25
+
26
+ if (!(await isDirectory(input.sourceCoreRoot))) {
27
+ errors.push(
28
+ `Cannot hydrate @evo-dev/core for ${input.runtimeLabel} plugin runtime: marketplace core package not found at ${input.sourceCoreRoot}`,
29
+ );
30
+ return { warnings, errors };
31
+ }
32
+
33
+ try {
34
+ await writeRuntimeCorePackage({
35
+ sourceCoreRoot: input.sourceCoreRoot,
36
+ targetCoreRoot: `${installedPluginRoot}/node_modules/@evo-dev/core`,
37
+ });
38
+ } catch (error) {
39
+ errors.push(
40
+ `Cannot hydrate @evo-dev/core for ${input.runtimeLabel} plugin runtime: ${describeError(error)}`,
41
+ );
42
+ }
43
+
44
+ return { warnings, errors };
45
+ }
46
+
47
+ export function isSafePluginCacheSegment(segment: string): boolean {
48
+ return /^[A-Za-z0-9._-]+$/.test(segment) && segment !== "." && segment !== "..";
49
+ }
50
+
51
+ async function findInstalledPluginRoot(cachePluginRoot: string): Promise<string | null> {
52
+ let entries: Dirent[];
53
+ try {
54
+ entries = await readdir(cachePluginRoot, { withFileTypes: true });
55
+ } catch (error) {
56
+ if (isNotFoundError(error)) {
57
+ return null;
58
+ }
59
+ throw error;
60
+ }
61
+
62
+ const versionDirs = await Promise.all(
63
+ entries
64
+ .filter((entry) => entry.isDirectory())
65
+ .map(async (entry) => {
66
+ const path = `${cachePluginRoot}/${entry.name}`;
67
+ const pathStat = await stat(path);
68
+ return { path, mtimeMs: pathStat.mtimeMs };
69
+ }),
70
+ );
71
+ if (versionDirs.length === 0) {
72
+ return null;
73
+ }
74
+
75
+ versionDirs.sort(
76
+ (left, right) => right.mtimeMs - left.mtimeMs || right.path.localeCompare(left.path),
77
+ );
78
+ return versionDirs[0].path;
79
+ }
80
+
81
+ async function writeRuntimeCorePackage(input: {
82
+ sourceCoreRoot: string;
83
+ targetCoreRoot: string;
84
+ }): Promise<void> {
85
+ const sourcePackagePath = `${input.sourceCoreRoot}/package.json`;
86
+ if (!(await isFile(sourcePackagePath))) {
87
+ throw new Error(`source package.json not found at ${sourcePackagePath}`);
88
+ }
89
+ if (!(await isDirectory(`${input.sourceCoreRoot}/src`))) {
90
+ throw new Error(`source src directory not found at ${input.sourceCoreRoot}/src`);
91
+ }
92
+
93
+ await rm(input.targetCoreRoot, { recursive: true, force: true });
94
+ await mkdir(dirname(input.targetCoreRoot), { recursive: true });
95
+ await mkdir(input.targetCoreRoot, { recursive: true });
96
+ await cp(`${input.sourceCoreRoot}/src`, `${input.targetCoreRoot}/src`, {
97
+ recursive: true,
98
+ });
99
+ if (await isDirectory(`${input.sourceCoreRoot}/assets`)) {
100
+ await cp(`${input.sourceCoreRoot}/assets`, `${input.targetCoreRoot}/assets`, {
101
+ recursive: true,
102
+ });
103
+ }
104
+
105
+ const sourcePackageJson = JSON.parse(await readFile(sourcePackagePath, "utf8")) as Record<
106
+ string,
107
+ unknown
108
+ >;
109
+ const runtimePackageJson = {
110
+ ...sourcePackageJson,
111
+ exports: rewritePackageExportsToSource(sourcePackageJson.exports),
112
+ files: ["src", "assets", "package.json"],
113
+ };
114
+ await writeFile(
115
+ `${input.targetCoreRoot}/package.json`,
116
+ `${JSON.stringify(runtimePackageJson, null, 2)}\n`,
117
+ "utf8",
118
+ );
119
+ }
120
+
121
+ function rewritePackageExportsToSource(exportsValue: unknown): unknown {
122
+ if (typeof exportsValue === "string") {
123
+ return rewriteDistPathToSource(exportsValue);
124
+ }
125
+ if (Array.isArray(exportsValue)) {
126
+ return exportsValue.map((value) => rewritePackageExportsToSource(value));
127
+ }
128
+ if (exportsValue !== null && typeof exportsValue === "object") {
129
+ return Object.fromEntries(
130
+ Object.entries(exportsValue).map(([key, value]) => [
131
+ key,
132
+ rewritePackageExportsToSource(value),
133
+ ]),
134
+ );
135
+ }
136
+ return exportsValue;
137
+ }
138
+
139
+ function rewriteDistPathToSource(path: string): string {
140
+ if (!path.startsWith("./dist/") || !path.endsWith(".js")) {
141
+ return path;
142
+ }
143
+
144
+ return `./src/${path.slice("./dist/".length, -".js".length)}.ts`;
145
+ }
146
+
147
+ async function isDirectory(path: string): Promise<boolean> {
148
+ try {
149
+ return (await stat(path)).isDirectory();
150
+ } catch (error) {
151
+ if (isNotFoundError(error)) {
152
+ return false;
153
+ }
154
+ throw error;
155
+ }
156
+ }
157
+
158
+ async function isFile(path: string): Promise<boolean> {
159
+ try {
160
+ return (await stat(path)).isFile();
161
+ } catch (error) {
162
+ if (isNotFoundError(error)) {
163
+ return false;
164
+ }
165
+ throw error;
166
+ }
167
+ }
168
+
169
+ function isNotFoundError(error: unknown): boolean {
170
+ return (
171
+ error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT"
172
+ );
173
+ }
174
+
175
+ function describeError(error: unknown): string {
176
+ if (error instanceof Error) {
177
+ return error.message;
178
+ }
179
+
180
+ return String(error);
181
+ }