claude-dev-env 8.99.0 → 8.99.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.
@@ -91,12 +91,13 @@ const HOST_DEFINITIONS = Object.freeze({
91
91
  * verb gets refresh commands: the marketplace add and plugin install leave an
92
92
  * existing entry in place, and the update commands then pull the newest
93
93
  * published version over it, so an installed plugin stays installed through
94
- * the whole run. When any refresh command fails, or the host has no update
95
- * verb, the removal commands run and take out each installed plugin, its
96
- * cached versions, and the marketplace clone, and the install commands that
97
- * follow fetch the newest published version into an empty slot. Removing a
98
- * marketplace uninstalls each plugin it supplied, so the plan removes and adds
99
- * that marketplace once for the whole set.
94
+ * the whole run. When a refresh command fails for a reason only a rebuild
95
+ * fixes, or the host has no update verb, the removal commands run and take
96
+ * out each installed plugin, its cached versions, and the marketplace clone,
97
+ * and the install commands that follow fetch the newest published version
98
+ * into an empty slot. Removing a marketplace uninstalls each plugin it
99
+ * supplied, so the plan removes and adds that marketplace once for the whole
100
+ * set.
100
101
  *
101
102
  * Claude Code and Codex publish a marketplace under different plugin verbs, so
102
103
  * the install verb belongs to the host and the marketplace and identifier
@@ -219,7 +220,12 @@ function runHostCommand(executable, commandArguments, options) {
219
220
  env: { ...process.env, ...options.environment },
220
221
  windowsVerbatimArguments: invocation.windowsVerbatimArguments,
221
222
  });
222
- return { status: spawned.status, stderr: spawned.stderr ?? '', error: spawned.error };
223
+ return {
224
+ status: spawned.status,
225
+ stdout: spawned.stdout ?? '',
226
+ stderr: spawned.stderr ?? '',
227
+ error: spawned.error,
228
+ };
223
229
  }
224
230
 
225
231
  const COMMAND_NOT_FOUND_EXIT_CODE = 9009;
@@ -297,15 +303,33 @@ function lockedFolderDetail(allSpecs, host, homeDirectory, errorText, findLocker
297
303
  }).join('');
298
304
  }
299
305
 
306
+ const REBUILD_REQUIRED_PATTERN = /doesn't match its extraKnownMarketplaces entry|Source path does not exist/;
307
+
308
+ function commandFailureDetail(outcome) {
309
+ return singleLineDetail(outcome.stderr) || singleLineDetail(outcome.stdout)
310
+ || outcome.error?.message || `exit ${outcome.status}`;
311
+ }
312
+
313
+ function failedCommandOutcome(allSpecs, host, homeDirectory, executable, commandArguments, outcome, findLockers) {
314
+ const detail = commandFailureDetail(outcome);
315
+ const lockAdvice = lockedFolderDetail(allSpecs, host, homeDirectory, detail, findLockers);
316
+ return {
317
+ host,
318
+ executable,
319
+ status: 'failed',
320
+ warning: `${executable} ${commandArguments.join(' ')} failed: ${detail}${lockAdvice}`,
321
+ };
322
+ }
323
+
300
324
  function refreshInPlace(plan, executable, commandEnvironment, runCommand) {
301
325
  for (const commandArguments of plan.refreshCommands) {
302
326
  const outcome = runCommand(executable, [...commandArguments], {
303
327
  environment: commandEnvironment,
304
328
  });
305
- if (namesAnAbsentCommand(outcome)) return 'absent';
306
- if (outcome.status !== 0 || outcome.error) return 'failed';
329
+ if (namesAnAbsentCommand(outcome)) return { state: 'absent' };
330
+ if (outcome.status !== 0 || outcome.error) return { state: 'failed', commandArguments, outcome };
307
331
  }
308
- return 'refreshed';
332
+ return { state: 'refreshed' };
309
333
  }
310
334
 
311
335
  function removeOrphanedVersionsForHost(allSpecs, host, homeDirectory) {
@@ -326,11 +350,18 @@ function installForHost(allSpecs, host, homeDirectory, environment, runCommand,
326
350
  }
327
351
  if (plan.refreshCommands.length > 0) {
328
352
  const refreshOutcome = refreshInPlace(plan, executable, commandEnvironment, runCommand);
329
- if (refreshOutcome === 'absent') return absentHostOutcome(allSpecs, host, executable);
330
- if (refreshOutcome === 'refreshed') {
353
+ if (refreshOutcome.state === 'absent') return absentHostOutcome(allSpecs, host, executable);
354
+ if (refreshOutcome.state === 'refreshed') {
331
355
  removeOrphanedVersionsForHost(allSpecs, host, homeDirectory);
332
356
  return { host, executable, status: 'installed', warning: null };
333
357
  }
358
+ const refreshOutput = `${refreshOutcome.outcome.stderr ?? ''}\n${refreshOutcome.outcome.stdout ?? ''}`;
359
+ if (!REBUILD_REQUIRED_PATTERN.test(refreshOutput)) {
360
+ return failedCommandOutcome(
361
+ allSpecs, host, homeDirectory, executable,
362
+ refreshOutcome.commandArguments, refreshOutcome.outcome, findLockers,
363
+ );
364
+ }
334
365
  }
335
366
  for (const commandArguments of plan.removalCommands) {
336
367
  const outcome = runCommand(executable, [...commandArguments], {
@@ -347,14 +378,9 @@ function installForHost(allSpecs, host, homeDirectory, environment, runCommand,
347
378
  });
348
379
  if (namesAnAbsentCommand(outcome)) return absentHostOutcome(allSpecs, host, executable);
349
380
  if (outcome.status !== 0 || outcome.error) {
350
- const detail = singleLineDetail(outcome.stderr) || outcome.error?.message || `exit ${outcome.status}`;
351
- const lockAdvice = lockedFolderDetail(allSpecs, host, homeDirectory, detail, findLockers);
352
- return {
353
- host,
354
- executable,
355
- status: 'failed',
356
- warning: `${executable} ${commandArguments.join(' ')} failed: ${detail}${lockAdvice}`,
357
- };
381
+ return failedCommandOutcome(
382
+ allSpecs, host, homeDirectory, executable, commandArguments, outcome, findLockers,
383
+ );
358
384
  }
359
385
  }
360
386
  removeOrphanedVersionsForHost(allSpecs, host, homeDirectory);
@@ -385,12 +411,14 @@ export function restartNotice(allSpecs, allInstalledHosts) {
385
411
  * plugin, both of which leave an existing entry alone, then updates the
386
412
  * marketplace and each plugin. An installed plugin stays installed and enabled
387
413
  * through the run, so a session that starts mid-run still loads it, and a
388
- * failed network step leaves the prior version in place. When a refresh
389
- * command fails, such as a marketplace add whose sparse paths changed, the
390
- * host falls back to the clean install Codex always runs: it removes every
391
- * plugin in the set and then the marketplace, so an older version never
392
- * survives beside the new one, and adds them again. A removal that finds
393
- * nothing to remove exits non-zero on a fresh home, and the install goes on.
414
+ * failed refresh command reports the host failed and leaves the prior version
415
+ * in place. Two refresh errors need a rebuild: a source that no longer
416
+ * matches the settings entry, and a source path the sparse clone lacks. The
417
+ * host reads either from the failing refresh command's output, then falls
418
+ * back to the clean install Codex always runs: it removes every plugin in the
419
+ * set and then the marketplace, so an older version never survives beside the
420
+ * new one, and adds them again. A removal that finds nothing to remove exits
421
+ * non-zero on a fresh home, and the install goes on.
394
422
  * Before each Claude marketplace add, the install deletes the half-made clone
395
423
  * folder an interrupted earlier add left, so the add starts in an empty slot.
396
424
  *
@@ -482,7 +510,7 @@ export function removeRetiredMarketplacePlugins(allRetiredSpecs, options, depend
482
510
  );
483
511
  if (namesAnAbsentCommand(outcome)) break;
484
512
  if (outcome.status !== 0 || outcome.error) {
485
- const detail = singleLineDetail(outcome.stderr) || outcome.error?.message || `exit ${outcome.status}`;
513
+ const detail = commandFailureDetail(outcome);
486
514
  failedRemovals.push({ pluginIdentifier: eachSpec.pluginIdentifier, detail });
487
515
  continue;
488
516
  }
@@ -117,6 +117,30 @@ test('a failed Claude refresh falls back to removing and adding the marketplace
117
117
  ]);
118
118
  });
119
119
 
120
+ test('a Claude refresh error printed on standard output still triggers the rebuild', () => {
121
+ const runner = recordingRunner((executable, commandArguments) => (
122
+ commandArguments[2] === 'add' && runner.calls.length === 1
123
+ ? { status: 1, stdout: "its source doesn't match its extraKnownMarketplaces entry", stderr: '' }
124
+ : { status: 0, stderr: '' }));
125
+ const outcome = installMarketplacePlugins([USAGE_WRAPUP_PLUGIN_SPEC], { ...ROOTS, hosts: ['claude'] }, runner);
126
+ assert.equal(outcome.status, 'installed');
127
+ assert.ok(runner.calls.some(call => call.commandArguments.includes('uninstall')));
128
+ });
129
+
130
+ test('a Claude refresh that fails on the network keeps the installed plugin and reports the failure', () => {
131
+ const runner = recordingRunner((executable, commandArguments) => (
132
+ commandArguments[1] === 'update'
133
+ ? { status: 1, stderr: 'Failed to update plugin: getaddrinfo ENOTFOUND github.com' }
134
+ : { status: 0, stderr: '' }));
135
+ const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], { ...ROOTS, hosts: ['claude'] }, runner);
136
+ assert.equal(outcome.status, 'failed');
137
+ assert.match(outcome.hosts[0].warning, /claude plugin update pstack@pstack-claude failed: .*ENOTFOUND/);
138
+ const allRemovalCalls = runner.calls.filter(call => (
139
+ call.commandArguments.includes('uninstall') || call.commandArguments.includes('remove')
140
+ ));
141
+ assert.deepEqual(allRemovalCalls, []);
142
+ });
143
+
120
144
  test('each host removes the installed plugin and marketplace before a clean install', () => {
121
145
  assert.deepEqual(marketplacePluginsPlan([PSTACK_PLUGIN_SPEC], 'claude').removalCommands, [
122
146
  ['plugin', 'uninstall', PSTACK_PLUGIN_IDENTIFIER],
@@ -131,9 +155,10 @@ test('each host removes the installed plugin and marketplace before a clean inst
131
155
  test('a removal that finds nothing installed still lets the install run', () => {
132
156
  const runner = recordingRunner((executable, commandArguments) => (
133
157
  commandArguments.includes('uninstall') || commandArguments.includes('remove')
134
- || commandArguments.includes('update')
135
158
  ? { status: 1, stderr: 'not installed' }
136
- : { status: 0, stderr: '' }));
159
+ : commandArguments.includes('update')
160
+ ? { status: 1, stderr: 'Source path does not exist: plugins/pstack' }
161
+ : { status: 0, stderr: '' }));
137
162
  const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], { ...ROOTS, hosts: ['claude'] }, runner);
138
163
  assert.equal(outcome.status, 'installed');
139
164
  assert.deepEqual(runner.calls.at(-1).commandArguments, ['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.99.0",
3
+ "version": "8.99.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {