claude-dev-env 8.42.6 → 8.43.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.
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, readdirSync, rmSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
|
|
3
|
-
export const PSTACK_MARKETPLACE_REPOSITORY = '
|
|
5
|
+
export const PSTACK_MARKETPLACE_REPOSITORY = 'jl-cmd/pstack-claude';
|
|
4
6
|
export const PSTACK_MARKETPLACE_NAME = 'pstack-claude';
|
|
5
7
|
export const PSTACK_PLUGIN_IDENTIFIER = `pstack@${PSTACK_MARKETPLACE_NAME}`;
|
|
6
8
|
export const PSTACK_PLUGIN_HOSTS = Object.freeze(['claude', 'codex']);
|
|
@@ -12,6 +14,7 @@ export const PSTACK_PLUGIN_SPEC = Object.freeze({
|
|
|
12
14
|
name: 'pstack',
|
|
13
15
|
label: 'Pstack',
|
|
14
16
|
marketplaceRepository: PSTACK_MARKETPLACE_REPOSITORY,
|
|
17
|
+
marketplaceName: PSTACK_MARKETPLACE_NAME,
|
|
15
18
|
marketplaceAddArguments: Object.freeze([]),
|
|
16
19
|
pluginIdentifier: PSTACK_PLUGIN_IDENTIFIER,
|
|
17
20
|
hosts: PSTACK_PLUGIN_HOSTS,
|
|
@@ -23,6 +26,7 @@ export const USAGE_WRAPUP_PLUGIN_SPEC = Object.freeze({
|
|
|
23
26
|
name: 'usage-wrapup',
|
|
24
27
|
label: 'Usage-wrapup',
|
|
25
28
|
marketplaceRepository: 'jl-cmd/claude-dev-env',
|
|
29
|
+
marketplaceName: 'claude-dev-env',
|
|
26
30
|
marketplaceAddArguments: Object.freeze(['--sparse', '.claude-plugin']),
|
|
27
31
|
pluginIdentifier: 'usage-wrapup@claude-dev-env',
|
|
28
32
|
hosts: Object.freeze(['claude']),
|
|
@@ -35,19 +39,28 @@ const HOST_DEFINITIONS = Object.freeze({
|
|
|
35
39
|
executable: 'claude',
|
|
36
40
|
executableVariable: 'CDE_CLAUDE_EXECUTABLE',
|
|
37
41
|
homeVariable: 'CLAUDE_CONFIG_DIR',
|
|
42
|
+
orphanedVersionsDirectory: (homeDirectory, spec) => join(
|
|
43
|
+
homeDirectory, 'plugins', 'cache', spec.marketplaceName, spec.name,
|
|
44
|
+
),
|
|
38
45
|
installVerb: 'install',
|
|
46
|
+
uninstallVerb: 'uninstall',
|
|
39
47
|
}),
|
|
40
48
|
codex: Object.freeze({
|
|
41
49
|
executable: 'codex',
|
|
42
50
|
executableVariable: 'CDE_CODEX_EXECUTABLE',
|
|
43
51
|
homeVariable: 'CODEX_HOME',
|
|
44
52
|
installVerb: 'add',
|
|
53
|
+
uninstallVerb: 'remove',
|
|
45
54
|
}),
|
|
46
55
|
});
|
|
47
56
|
|
|
48
57
|
/**
|
|
49
58
|
* Read the marketplace and plugin commands one host installs a plugin with.
|
|
50
59
|
*
|
|
60
|
+
* The removal commands run first and take out the installed plugin, its
|
|
61
|
+
* cached versions, and the marketplace clone, so the install commands that
|
|
62
|
+
* follow fetch the newest published version into an empty slot.
|
|
63
|
+
*
|
|
51
64
|
* Claude Code and Codex publish a marketplace under different plugin verbs, so
|
|
52
65
|
* the install verb belongs to the host and the marketplace and identifier
|
|
53
66
|
* belong to the plugin spec. A spec whose marketplace is a large repository
|
|
@@ -57,6 +70,7 @@ const HOST_DEFINITIONS = Object.freeze({
|
|
|
57
70
|
* @param {typeof PSTACK_PLUGIN_SPEC} spec The plugin to install.
|
|
58
71
|
* @param {string} host A host the spec names, `claude` or `codex`.
|
|
59
72
|
* @returns {{executable: string, executableVariable: string, homeVariable: string,
|
|
73
|
+
* removalCommands: ReadonlyArray<ReadonlyArray<string>>,
|
|
60
74
|
* commands: ReadonlyArray<ReadonlyArray<string>>}} The host's install plan.
|
|
61
75
|
*/
|
|
62
76
|
export function marketplacePluginPlan(spec, host) {
|
|
@@ -70,6 +84,10 @@ export function marketplacePluginPlan(spec, host) {
|
|
|
70
84
|
executable: hostDefinition.executable,
|
|
71
85
|
executableVariable: hostDefinition.executableVariable,
|
|
72
86
|
homeVariable: hostDefinition.homeVariable,
|
|
87
|
+
removalCommands: [
|
|
88
|
+
['plugin', hostDefinition.uninstallVerb, spec.pluginIdentifier],
|
|
89
|
+
['plugin', 'marketplace', 'remove', spec.marketplaceName],
|
|
90
|
+
],
|
|
73
91
|
commands: [
|
|
74
92
|
['plugin', 'marketplace', 'add', spec.marketplaceRepository, ...spec.marketplaceAddArguments],
|
|
75
93
|
['plugin', hostDefinition.installVerb, spec.pluginIdentifier],
|
|
@@ -156,22 +174,54 @@ function firstLine(text) {
|
|
|
156
174
|
return trimmed.split(/\r?\n/).filter(Boolean).at(-1);
|
|
157
175
|
}
|
|
158
176
|
|
|
177
|
+
const ORPHANED_VERSION_MARKER = '.orphaned_at';
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Delete each cached plugin version the host marked as orphaned.
|
|
181
|
+
*
|
|
182
|
+
* Claude Code keeps an uninstalled version in its plugin cache with an
|
|
183
|
+
* `.orphaned_at` marker, so the clean install deletes those versions and
|
|
184
|
+
* leaves the version it just installed.
|
|
185
|
+
*
|
|
186
|
+
* @param {string|undefined} versionsDirectory The plugin's cache directory.
|
|
187
|
+
* @returns {string[]} The version directories removed.
|
|
188
|
+
*/
|
|
189
|
+
export function removeOrphanedPluginVersions(versionsDirectory) {
|
|
190
|
+
if (!versionsDirectory || !existsSync(versionsDirectory)) return [];
|
|
191
|
+
const removedPaths = [];
|
|
192
|
+
for (const versionName of readdirSync(versionsDirectory)) {
|
|
193
|
+
const versionPath = join(versionsDirectory, versionName);
|
|
194
|
+
if (!existsSync(join(versionPath, ORPHANED_VERSION_MARKER))) continue;
|
|
195
|
+
rmSync(versionPath, { recursive: true, force: true });
|
|
196
|
+
removedPaths.push(versionPath);
|
|
197
|
+
}
|
|
198
|
+
return removedPaths;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function absentHostOutcome(spec, host, executable) {
|
|
202
|
+
return {
|
|
203
|
+
host,
|
|
204
|
+
executable,
|
|
205
|
+
status: 'skipped',
|
|
206
|
+
warning: `${executable} is not on PATH, so ${spec.name} was not installed for ${host}.`,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
159
210
|
function installForHost(spec, host, homeDirectory, environment, runCommand) {
|
|
160
211
|
const plan = marketplacePluginPlan(spec, host);
|
|
161
212
|
const executable = environment[plan.executableVariable] || plan.executable;
|
|
162
213
|
const commandEnvironment = { [plan.homeVariable]: homeDirectory };
|
|
214
|
+
for (const commandArguments of plan.removalCommands) {
|
|
215
|
+
const outcome = runCommand(executable, [...commandArguments], {
|
|
216
|
+
environment: commandEnvironment,
|
|
217
|
+
});
|
|
218
|
+
if (namesAnAbsentCommand(outcome)) return absentHostOutcome(spec, host, executable);
|
|
219
|
+
}
|
|
163
220
|
for (const commandArguments of plan.commands) {
|
|
164
221
|
const outcome = runCommand(executable, [...commandArguments], {
|
|
165
222
|
environment: commandEnvironment,
|
|
166
223
|
});
|
|
167
|
-
if (namesAnAbsentCommand(outcome))
|
|
168
|
-
return {
|
|
169
|
-
host,
|
|
170
|
-
executable,
|
|
171
|
-
status: 'skipped',
|
|
172
|
-
warning: `${executable} is not on PATH, so ${spec.name} was not installed for ${host}.`,
|
|
173
|
-
};
|
|
174
|
-
}
|
|
224
|
+
if (namesAnAbsentCommand(outcome)) return absentHostOutcome(spec, host, executable);
|
|
175
225
|
if (outcome.status !== 0 || outcome.error) {
|
|
176
226
|
const detail = firstLine(outcome.stderr) || outcome.error?.message || `exit ${outcome.status}`;
|
|
177
227
|
return {
|
|
@@ -182,12 +232,20 @@ function installForHost(spec, host, homeDirectory, environment, runCommand) {
|
|
|
182
232
|
};
|
|
183
233
|
}
|
|
184
234
|
}
|
|
235
|
+
const hostDefinition = HOST_DEFINITIONS[host];
|
|
236
|
+
if (hostDefinition.orphanedVersionsDirectory && homeDirectory) {
|
|
237
|
+
removeOrphanedPluginVersions(hostDefinition.orphanedVersionsDirectory(homeDirectory, spec));
|
|
238
|
+
}
|
|
185
239
|
return { host, executable, status: 'installed', warning: null };
|
|
186
240
|
}
|
|
187
241
|
|
|
188
242
|
/**
|
|
189
243
|
* Install one plugin from its marketplace into each host's own home.
|
|
190
244
|
*
|
|
245
|
+
* Each host first removes the plugin and its marketplace, so an older
|
|
246
|
+
* version never survives beside the new one. A removal that finds nothing
|
|
247
|
+
* to remove exits non-zero on a fresh home, and the install goes on.
|
|
248
|
+
*
|
|
191
249
|
* Each host is one member of the batch. A host without its command-line tool
|
|
192
250
|
* is skipped and a host whose command fails is reported, so the rules, hooks,
|
|
193
251
|
* and skills this run already wrote still reach their durable places. An
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import { strict as assert } from 'node:assert';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
4
6
|
import {
|
|
5
7
|
PSTACK_MARKETPLACE_REPOSITORY,
|
|
6
8
|
PSTACK_PLUGIN_HOSTS,
|
|
@@ -10,6 +12,7 @@ import {
|
|
|
10
12
|
hostCommandInvocation,
|
|
11
13
|
installMarketplacePlugin,
|
|
12
14
|
marketplacePluginPlan,
|
|
15
|
+
removeOrphanedPluginVersions,
|
|
13
16
|
shouldInstallMarketplacePlugin,
|
|
14
17
|
} from './install-pstack-plugin.mjs';
|
|
15
18
|
|
|
@@ -60,13 +63,38 @@ test('each host installs into the managed root this run wrote', () => {
|
|
|
60
63
|
const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, ROOTS, runner);
|
|
61
64
|
assert.equal(outcome.status, 'installed');
|
|
62
65
|
assert.deepEqual(runner.calls.map(call => [call.executable, ...call.commandArguments]), [
|
|
66
|
+
['claude', 'plugin', 'uninstall', PSTACK_PLUGIN_IDENTIFIER],
|
|
67
|
+
['claude', 'plugin', 'marketplace', 'remove', 'pstack-claude'],
|
|
63
68
|
['claude', 'plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
|
|
64
69
|
['claude', 'plugin', 'install', PSTACK_PLUGIN_IDENTIFIER],
|
|
70
|
+
['codex', 'plugin', 'remove', PSTACK_PLUGIN_IDENTIFIER],
|
|
71
|
+
['codex', 'plugin', 'marketplace', 'remove', 'pstack-claude'],
|
|
65
72
|
['codex', 'plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
|
|
66
73
|
['codex', 'plugin', 'add', PSTACK_PLUGIN_IDENTIFIER],
|
|
67
74
|
]);
|
|
68
75
|
assert.equal(runner.calls[0].environment.CLAUDE_CONFIG_DIR, ROOTS.claudeRoot);
|
|
69
|
-
assert.equal(runner.calls[
|
|
76
|
+
assert.equal(runner.calls[4].environment.CODEX_HOME, ROOTS.codexHome);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('each host removes the installed plugin and marketplace before it installs', () => {
|
|
80
|
+
assert.deepEqual(marketplacePluginPlan(PSTACK_PLUGIN_SPEC, 'claude').removalCommands, [
|
|
81
|
+
['plugin', 'uninstall', PSTACK_PLUGIN_IDENTIFIER],
|
|
82
|
+
['plugin', 'marketplace', 'remove', 'pstack-claude'],
|
|
83
|
+
]);
|
|
84
|
+
assert.deepEqual(marketplacePluginPlan(PSTACK_PLUGIN_SPEC, 'codex').removalCommands, [
|
|
85
|
+
['plugin', 'remove', PSTACK_PLUGIN_IDENTIFIER],
|
|
86
|
+
['plugin', 'marketplace', 'remove', 'pstack-claude'],
|
|
87
|
+
]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('a removal that finds nothing installed still lets the install run', () => {
|
|
91
|
+
const runner = recordingRunner((executable, commandArguments) => (
|
|
92
|
+
commandArguments.includes('uninstall') || commandArguments.includes('remove')
|
|
93
|
+
? { status: 1, stderr: 'not installed' }
|
|
94
|
+
: { status: 0, stderr: '' }));
|
|
95
|
+
const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, { ...ROOTS, hosts: ['claude'] }, runner);
|
|
96
|
+
assert.equal(outcome.status, 'installed');
|
|
97
|
+
assert.deepEqual(runner.calls.at(-1).commandArguments, ['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER]);
|
|
70
98
|
});
|
|
71
99
|
|
|
72
100
|
test('an absent host command skips that host and leaves the other installed', () => {
|
|
@@ -83,7 +111,7 @@ test('an absent host command skips that host and leaves the other installed', ()
|
|
|
83
111
|
|
|
84
112
|
test('a failing command reports that host and still installs the next one', () => {
|
|
85
113
|
const runner = recordingRunner((executable, commandArguments) => (
|
|
86
|
-
executable === 'claude' && commandArguments[
|
|
114
|
+
executable === 'claude' && commandArguments[2] === 'add'
|
|
87
115
|
? { status: 1, stderr: 'marketplace unreachable' }
|
|
88
116
|
: { status: 0, stderr: '' }));
|
|
89
117
|
const outcome = installMarketplacePlugin(PSTACK_PLUGIN_SPEC, ROOTS, runner);
|
|
@@ -92,7 +120,8 @@ test('a failing command reports that host and still installs the next one', () =
|
|
|
92
120
|
assert.equal(claude.status, 'failed');
|
|
93
121
|
assert.match(claude.warning, /marketplace unreachable/);
|
|
94
122
|
assert.equal(outcome.hosts.find(host => host.host === 'codex').status, 'installed');
|
|
95
|
-
assert.
|
|
123
|
+
assert.deepEqual(runner.calls.filter(call => call.executable === 'claude').at(-1).commandArguments,
|
|
124
|
+
['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY]);
|
|
96
125
|
});
|
|
97
126
|
|
|
98
127
|
test('an executable override replaces the host command for that host only', () => {
|
|
@@ -207,6 +236,8 @@ test('installing usage-wrapup runs only the Claude commands, inside the managed
|
|
|
207
236
|
assert.equal(outcome.status, 'installed');
|
|
208
237
|
assert.deepEqual(outcome.hosts.map(host => host.host), ['claude']);
|
|
209
238
|
assert.deepEqual(runner.calls.map(call => [call.executable, ...call.commandArguments]), [
|
|
239
|
+
['claude', 'plugin', 'uninstall', 'usage-wrapup@claude-dev-env'],
|
|
240
|
+
['claude', 'plugin', 'marketplace', 'remove', 'claude-dev-env'],
|
|
210
241
|
['claude', 'plugin', 'marketplace', 'add', 'jl-cmd/claude-dev-env', '--sparse', '.claude-plugin'],
|
|
211
242
|
['claude', 'plugin', 'install', 'usage-wrapup@claude-dev-env'],
|
|
212
243
|
]);
|
|
@@ -255,3 +286,20 @@ test('the repository marketplace lists usage-wrapup from the fork, pinned to the
|
|
|
255
286
|
sha: USAGE_WRAPUP_PINNED_COMMIT,
|
|
256
287
|
});
|
|
257
288
|
});
|
|
289
|
+
|
|
290
|
+
test('a clean install deletes the orphaned pstack versions and keeps the installed one', () => {
|
|
291
|
+
const claudeRoot = mkdtempSync(join(tmpdir(), 'pstack-orphans-'));
|
|
292
|
+
const versionsDirectory = join(claudeRoot, 'plugins', 'cache', 'pstack-claude', 'pstack');
|
|
293
|
+
mkdirSync(join(versionsDirectory, '0.9.55'), { recursive: true });
|
|
294
|
+
writeFileSync(join(versionsDirectory, '0.9.55', '.orphaned_at'), '1');
|
|
295
|
+
mkdirSync(join(versionsDirectory, '0.9.64'), { recursive: true });
|
|
296
|
+
const outcome = installMarketplacePlugin(
|
|
297
|
+
PSTACK_PLUGIN_SPEC,
|
|
298
|
+
{ claudeRoot, hosts: ['claude'], environment: {} },
|
|
299
|
+
recordingRunner(),
|
|
300
|
+
);
|
|
301
|
+
assert.equal(outcome.status, 'installed');
|
|
302
|
+
assert.equal(existsSync(join(versionsDirectory, '0.9.55')), false);
|
|
303
|
+
assert.equal(existsSync(join(versionsDirectory, '0.9.64')), true);
|
|
304
|
+
assert.deepEqual(removeOrphanedPluginVersions(versionsDirectory), []);
|
|
305
|
+
});
|
package/bin/install.test.mjs
CHANGED
|
@@ -3110,9 +3110,13 @@ test('a full install adds the pstack marketplace and plugin on both hosts', t =>
|
|
|
3110
3110
|
const installerOutput = runPstackInstaller(sandbox.homeDirectory, [], sandbox.environment);
|
|
3111
3111
|
|
|
3112
3112
|
assert.deepEqual(sandbox.recordedCommands(), [
|
|
3113
|
-
'claude plugin
|
|
3113
|
+
'claude plugin uninstall pstack@pstack-claude',
|
|
3114
|
+
'claude plugin marketplace remove pstack-claude',
|
|
3115
|
+
'claude plugin marketplace add jl-cmd/pstack-claude',
|
|
3114
3116
|
'claude plugin install pstack@pstack-claude',
|
|
3115
|
-
'codex plugin
|
|
3117
|
+
'codex plugin remove pstack@pstack-claude',
|
|
3118
|
+
'codex plugin marketplace remove pstack-claude',
|
|
3119
|
+
'codex plugin marketplace add jl-cmd/pstack-claude',
|
|
3116
3120
|
'codex plugin add pstack@pstack-claude',
|
|
3117
3121
|
]);
|
|
3118
3122
|
assert.match(installerOutput, /Pstack \(claude\): installed/);
|
|
@@ -3311,6 +3315,8 @@ test('a pstack install after a --no-pstack install seeds the Codex model sheet',
|
|
|
3311
3315
|
});
|
|
3312
3316
|
|
|
3313
3317
|
const USAGE_WRAPUP_CLAUDE_COMMANDS = Object.freeze([
|
|
3318
|
+
'claude plugin uninstall usage-wrapup@claude-dev-env',
|
|
3319
|
+
'claude plugin marketplace remove claude-dev-env',
|
|
3314
3320
|
'claude plugin marketplace add jl-cmd/claude-dev-env --sparse .claude-plugin',
|
|
3315
3321
|
'claude plugin install usage-wrapup@claude-dev-env',
|
|
3316
3322
|
]);
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ def _comment_policy_phrases() -> tuple[str, ...]:
|
|
|
13
13
|
"leave comments tied to untouched code unchanged",
|
|
14
14
|
"keep comment cleanup inside the requested task",
|
|
15
15
|
"production and tests follow one rule",
|
|
16
|
-
"changed directive, todo, fixme, hack, xxx, and type-ignore comments are removed
|
|
16
|
+
"changed directive, todo, fixme, hack, xxx, and type-ignore comments are removed",
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
|