@evo-dev/evodev 0.0.1-alpha.17 → 0.0.1-alpha.19

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "evodev",
3
3
  "description": "EvoDev hook, skill, and Teams MCP integration for disciplined AI coding workflows.",
4
- "version": "0.0.1-alpha.17",
4
+ "version": "0.0.1-alpha.19",
5
5
  "author": {
6
6
  "name": "EvoDev"
7
7
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evodev",
3
- "version": "0.0.1-alpha.17",
3
+ "version": "0.0.1-alpha.19",
4
4
  "description": "EvoDev hook, skill, and Teams MCP integration for disciplined AI coding workflows.",
5
5
  "author": {
6
6
  "name": "EvoDev"
@@ -54,6 +54,7 @@ export interface CodexPluginOptions {
54
54
  pluginSelector?: string;
55
55
  pluginMarketplaceSource?: string;
56
56
  pluginMarketplaceFallbackSource?: string;
57
+ pluginVersion?: string;
57
58
  paths?: CodexPaths;
58
59
  syncFileSystem?: CodexSyncFileSystem;
59
60
  }
@@ -195,6 +196,7 @@ async function installCodexPlugin(
195
196
  | "pluginSelector"
196
197
  | "pluginMarketplaceSource"
197
198
  | "pluginMarketplaceFallbackSource"
199
+ | "pluginVersion"
198
200
  | "paths"
199
201
  >,
200
202
  ): Promise<PluginInstallResult> {
@@ -202,8 +204,46 @@ async function installCodexPlugin(
202
204
  const selector = options.pluginSelector ?? "evodev@evodev";
203
205
  const args = ["plugin", "add", selector];
204
206
  const runner = options.commandRunner ?? new NodeCodexCommandRunner();
207
+ const installedPlugin =
208
+ input.force === true || options.pluginVersion === undefined
209
+ ? null
210
+ : await readInstalledCodexPlugin(runner, selector);
211
+ let needsRepair = false;
212
+
213
+ if (
214
+ installedPlugin !== null &&
215
+ installedPlugin.version === options.pluginVersion &&
216
+ installedPlugin.enabled
217
+ ) {
218
+ try {
219
+ const hydration = await hydrateCodexWorkspaceCoreDependency({
220
+ paths: options.paths,
221
+ selector,
222
+ });
223
+ if (hydration.warnings.length === 0 && hydration.errors.length === 0) {
224
+ return createPluginInstallResult(
225
+ input.targetPlugin,
226
+ "already-installed",
227
+ command,
228
+ args,
229
+ `Codex plugin ${selector} already matches EvoDev ${options.pluginVersion}; skipped marketplace refresh.`,
230
+ );
231
+ }
232
+ } catch {
233
+ // Fall through to the normal repair path, which returns a structured install failure.
234
+ }
235
+ needsRepair = true;
236
+ }
237
+
238
+ const effectiveInput =
239
+ input.force === true ||
240
+ needsRepair ||
241
+ (installedPlugin !== null &&
242
+ (installedPlugin.version !== options.pluginVersion || !installedPlugin.enabled))
243
+ ? { ...input, force: true }
244
+ : input;
205
245
  const primaryResult = await installCodexPluginOnce({
206
- input,
246
+ input: effectiveInput,
207
247
  command,
208
248
  selector,
209
249
  args,
@@ -224,7 +264,7 @@ async function installCodexPlugin(
224
264
  }
225
265
 
226
266
  const fallbackResult = await installCodexPluginOnce({
227
- input,
267
+ input: effectiveInput,
228
268
  command,
229
269
  selector,
230
270
  args,
@@ -243,6 +283,33 @@ async function installCodexPlugin(
243
283
  };
244
284
  }
245
285
 
286
+ interface InstalledCodexPlugin {
287
+ version: string | null;
288
+ enabled: boolean;
289
+ }
290
+
291
+ async function readInstalledCodexPlugin(
292
+ runner: CodexCommandRunner,
293
+ selector: string,
294
+ ): Promise<InstalledCodexPlugin | null> {
295
+ try {
296
+ const result = await runner.run("codex", ["plugin", "list", "--json"]);
297
+ if (result.exitCode !== 0 || result.stdout === undefined) return null;
298
+ const parsed = JSON.parse(result.stdout) as unknown;
299
+ const plugins = isRecord(parsed) && Array.isArray(parsed.installed) ? parsed.installed : [];
300
+ const plugin = plugins.find(
301
+ (candidate) => isRecord(candidate) && candidate.pluginId === selector,
302
+ );
303
+ if (!isRecord(plugin)) return null;
304
+ return {
305
+ version: typeof plugin.version === "string" ? plugin.version : null,
306
+ enabled: plugin.enabled !== false,
307
+ };
308
+ } catch {
309
+ return null;
310
+ }
311
+ }
312
+
246
313
  async function installCodexPluginOnce(input: {
247
314
  input: PluginInstallInput;
248
315
  command: string;
@@ -56,6 +56,7 @@ export interface ClaudePluginOptions {
56
56
  pluginSelector?: string;
57
57
  pluginMarketplaceSource?: string;
58
58
  pluginScope?: "user";
59
+ pluginVersion?: string;
59
60
  }
60
61
 
61
62
  export function createClaudePlugin(options: ClaudePluginOptions = {}): CodeAgentPlugin {
@@ -186,7 +187,12 @@ async function installClaudeCodePlugin(
186
187
  input: PluginInstallInput,
187
188
  options: Pick<
188
189
  ClaudePluginOptions,
189
- "commandRunner" | "paths" | "pluginSelector" | "pluginMarketplaceSource" | "pluginScope"
190
+ | "commandRunner"
191
+ | "paths"
192
+ | "pluginSelector"
193
+ | "pluginMarketplaceSource"
194
+ | "pluginScope"
195
+ | "pluginVersion"
190
196
  >,
191
197
  ): Promise<PluginInstallResult> {
192
198
  const command = "claude";
@@ -195,6 +201,42 @@ async function installClaudeCodePlugin(
195
201
  const args = ["plugin", "install", selector, "--scope", scope];
196
202
  const runner = options.commandRunner ?? new NodeClaudeCommandRunner();
197
203
  const warnings: string[] = [];
204
+ const installedPlugin =
205
+ input.force === true || options.pluginVersion === undefined
206
+ ? null
207
+ : await readInstalledClaudePlugin(runner, selector);
208
+ let needsRepair = false;
209
+
210
+ if (
211
+ installedPlugin !== null &&
212
+ installedPlugin.version === options.pluginVersion &&
213
+ installedPlugin.enabled
214
+ ) {
215
+ try {
216
+ const hydration = await hydrateClaudeWorkspaceCoreDependency({
217
+ paths: options.paths,
218
+ selector,
219
+ });
220
+ if (hydration.warnings.length === 0 && hydration.errors.length === 0) {
221
+ return createPluginInstallResult(
222
+ input.targetPlugin,
223
+ "already-installed",
224
+ command,
225
+ args,
226
+ `Claude Code plugin ${selector} already matches EvoDev ${options.pluginVersion}; skipped marketplace refresh.`,
227
+ );
228
+ }
229
+ } catch {
230
+ // Fall through to the normal repair path, which returns a structured install failure.
231
+ }
232
+ needsRepair = true;
233
+ }
234
+
235
+ const forceRefresh =
236
+ input.force === true ||
237
+ needsRepair ||
238
+ (installedPlugin !== null &&
239
+ (installedPlugin.version !== options.pluginVersion || !installedPlugin.enabled));
198
240
 
199
241
  try {
200
242
  if (options.pluginMarketplaceSource !== undefined) {
@@ -244,7 +286,7 @@ async function installClaudeCodePlugin(
244
286
  }
245
287
  }
246
288
 
247
- if (input.force === true) {
289
+ if (forceRefresh) {
248
290
  const uninstallArgs = [
249
291
  "plugin",
250
292
  "uninstall",
@@ -293,7 +335,7 @@ async function installClaudeCodePlugin(
293
335
  result.stdout,
294
336
  [
295
337
  ...warnings,
296
- input.force === true
338
+ forceRefresh
297
339
  ? "Claude Code reported the plugin is already installed after forced refresh; check the local plugin cache if changes are not visible."
298
340
  : "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.",
299
341
  ],
@@ -330,6 +372,35 @@ async function installClaudeCodePlugin(
330
372
  }
331
373
  }
332
374
 
375
+ interface InstalledClaudePlugin {
376
+ version: string | null;
377
+ enabled: boolean;
378
+ }
379
+
380
+ async function readInstalledClaudePlugin(
381
+ runner: ClaudeCommandRunner,
382
+ selector: string,
383
+ ): Promise<InstalledClaudePlugin | null> {
384
+ try {
385
+ const result = await runner.run("claude", ["plugin", "list", "--json"]);
386
+ if (result.exitCode !== 0 || result.stdout === undefined) return null;
387
+ const parsed = JSON.parse(result.stdout) as unknown;
388
+ const plugins = Array.isArray(parsed)
389
+ ? parsed
390
+ : isRecord(parsed) && Array.isArray(parsed.plugins)
391
+ ? parsed.plugins
392
+ : [];
393
+ const plugin = plugins.find((candidate) => isRecord(candidate) && candidate.id === selector);
394
+ if (!isRecord(plugin)) return null;
395
+ return {
396
+ version: typeof plugin.version === "string" ? plugin.version : null,
397
+ enabled: plugin.enabled !== false,
398
+ };
399
+ } catch {
400
+ return null;
401
+ }
402
+ }
403
+
333
404
  function isAlreadyInstalledMessage(message: string | undefined): boolean {
334
405
  return message !== undefined && /already\s+installed/i.test(message);
335
406
  }
@@ -655,6 +726,10 @@ function isNotDirectoryError(error: unknown): boolean {
655
726
  );
656
727
  }
657
728
 
729
+ function isRecord(value: unknown): value is Record<string, unknown> {
730
+ return typeof value === "object" && value !== null && !Array.isArray(value);
731
+ }
732
+
658
733
  function describeError(error: unknown): string {
659
734
  if (error instanceof Error) {
660
735
  return error.message;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evo-dev/plugin",
3
- "version": "0.0.1-alpha.17",
3
+ "version": "0.0.1-alpha.19",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./hooks/index.ts"