claude-dev-env 8.51.4 → 8.52.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.
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +12 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +93 -6
- package/_shared/process-tree/scripts/test_process_tree_kill.py +149 -0
- package/bin/install-pstack-plugin.mjs +68 -33
- package/bin/install-pstack-plugin.test.mjs +77 -32
- package/bin/install.agents-home.test.mjs +1 -0
- package/bin/install.codex-rules.test.mjs +2 -0
- package/bin/install.cursor-rules.test.mjs +1 -0
- package/bin/install.mjs +18 -12
- package/bin/install.prune.test.mjs +1 -0
- package/bin/install.question-presentation.test.mjs +4 -0
- package/bin/install.settings-defaults.test.mjs +1 -0
- package/bin/install.shared-settings.test.mjs +1 -0
- package/bin/install.test.mjs +56 -0
- package/bin/install.transaction.test.mjs +8 -0
- package/bin/install.uninstall-transaction.test.mjs +2 -0
- package/bin/install.update-keeps-hooks.test.mjs +1 -0
- package/package.json +1 -1
- package/scripts/account_broker_support.py +22 -6
- package/scripts/test_account_broker_support.py +83 -24
|
@@ -8,10 +8,11 @@ import {
|
|
|
8
8
|
PSTACK_PLUGIN_HOSTS,
|
|
9
9
|
PSTACK_PLUGIN_IDENTIFIER,
|
|
10
10
|
PSTACK_PLUGIN_SPEC,
|
|
11
|
+
SUBAGENT_MODELS_PLUGIN_SPEC,
|
|
11
12
|
USAGE_WRAPUP_PLUGIN_SPEC,
|
|
12
13
|
hostCommandInvocation,
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
installMarketplacePlugins,
|
|
15
|
+
marketplacePluginsPlan,
|
|
15
16
|
removeOrphanedPluginVersions,
|
|
16
17
|
shouldInstallMarketplacePlugin,
|
|
17
18
|
} from './install-pstack-plugin.mjs';
|
|
@@ -36,7 +37,7 @@ const ROOTS = Object.freeze({
|
|
|
36
37
|
const WINDOWS_ENVIRONMENT = Object.freeze({ ComSpec: 'C:\\Windows\\system32\\cmd.exe' });
|
|
37
38
|
|
|
38
39
|
test('the Claude plan carries the two documented plugin commands', () => {
|
|
39
|
-
const plan =
|
|
40
|
+
const plan = marketplacePluginsPlan([PSTACK_PLUGIN_SPEC], 'claude');
|
|
40
41
|
assert.equal(plan.executable, 'claude');
|
|
41
42
|
assert.deepEqual(plan.commands, [
|
|
42
43
|
['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
|
|
@@ -45,7 +46,7 @@ test('the Claude plan carries the two documented plugin commands', () => {
|
|
|
45
46
|
});
|
|
46
47
|
|
|
47
48
|
test('the Codex plan adds the plugin with the Codex verb', () => {
|
|
48
|
-
const plan =
|
|
49
|
+
const plan = marketplacePluginsPlan([PSTACK_PLUGIN_SPEC], 'codex');
|
|
49
50
|
assert.equal(plan.executable, 'codex');
|
|
50
51
|
assert.deepEqual(plan.commands, [
|
|
51
52
|
['plugin', 'marketplace', 'add', PSTACK_MARKETPLACE_REPOSITORY],
|
|
@@ -54,13 +55,13 @@ test('the Codex plan adds the plugin with the Codex verb', () => {
|
|
|
54
55
|
});
|
|
55
56
|
|
|
56
57
|
test('an unsupported host names the hosts this installer knows', () => {
|
|
57
|
-
assert.throws(() =>
|
|
58
|
+
assert.throws(() => marketplacePluginsPlan([PSTACK_PLUGIN_SPEC], 'cursor'), /cursor/);
|
|
58
59
|
assert.deepEqual(PSTACK_PLUGIN_HOSTS, ['claude', 'codex']);
|
|
59
60
|
});
|
|
60
61
|
|
|
61
62
|
test('each host installs into the managed root this run wrote', () => {
|
|
62
63
|
const runner = recordingRunner();
|
|
63
|
-
const outcome =
|
|
64
|
+
const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, runner);
|
|
64
65
|
assert.equal(outcome.status, 'installed');
|
|
65
66
|
assert.deepEqual(runner.calls.map(call => [call.executable, ...call.commandArguments]), [
|
|
66
67
|
['claude', 'plugin', 'uninstall', PSTACK_PLUGIN_IDENTIFIER],
|
|
@@ -77,11 +78,11 @@ test('each host installs into the managed root this run wrote', () => {
|
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
test('each host removes the installed plugin and marketplace before it installs', () => {
|
|
80
|
-
assert.deepEqual(
|
|
81
|
+
assert.deepEqual(marketplacePluginsPlan([PSTACK_PLUGIN_SPEC], 'claude').removalCommands, [
|
|
81
82
|
['plugin', 'uninstall', PSTACK_PLUGIN_IDENTIFIER],
|
|
82
83
|
['plugin', 'marketplace', 'remove', 'pstack-claude'],
|
|
83
84
|
]);
|
|
84
|
-
assert.deepEqual(
|
|
85
|
+
assert.deepEqual(marketplacePluginsPlan([PSTACK_PLUGIN_SPEC], 'codex').removalCommands, [
|
|
85
86
|
['plugin', 'remove', PSTACK_PLUGIN_IDENTIFIER],
|
|
86
87
|
['plugin', 'marketplace', 'remove', 'pstack-claude'],
|
|
87
88
|
]);
|
|
@@ -92,7 +93,7 @@ test('a removal that finds nothing installed still lets the install run', () =>
|
|
|
92
93
|
commandArguments.includes('uninstall') || commandArguments.includes('remove')
|
|
93
94
|
? { status: 1, stderr: 'not installed' }
|
|
94
95
|
: { status: 0, stderr: '' }));
|
|
95
|
-
const outcome =
|
|
96
|
+
const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], { ...ROOTS, hosts: ['claude'] }, runner);
|
|
96
97
|
assert.equal(outcome.status, 'installed');
|
|
97
98
|
assert.deepEqual(runner.calls.at(-1).commandArguments, ['plugin', 'install', PSTACK_PLUGIN_IDENTIFIER]);
|
|
98
99
|
});
|
|
@@ -101,7 +102,7 @@ test('an absent host command skips that host and leaves the other installed', ()
|
|
|
101
102
|
const runner = recordingRunner((executable) => (executable === 'codex'
|
|
102
103
|
? { status: null, error: Object.assign(new Error('spawn codex ENOENT'), { code: 'ENOENT' }) }
|
|
103
104
|
: { status: 0, stderr: '' }));
|
|
104
|
-
const outcome =
|
|
105
|
+
const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, runner);
|
|
105
106
|
assert.equal(outcome.hosts.find(host => host.host === 'claude').status, 'installed');
|
|
106
107
|
const codex = outcome.hosts.find(host => host.host === 'codex');
|
|
107
108
|
assert.equal(codex.status, 'skipped');
|
|
@@ -114,7 +115,7 @@ test('a failing command reports that host and still installs the next one', () =
|
|
|
114
115
|
executable === 'claude' && commandArguments[2] === 'add'
|
|
115
116
|
? { status: 1, stderr: 'marketplace unreachable' }
|
|
116
117
|
: { status: 0, stderr: '' }));
|
|
117
|
-
const outcome =
|
|
118
|
+
const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, runner);
|
|
118
119
|
assert.equal(outcome.status, 'failed');
|
|
119
120
|
const claude = outcome.hosts.find(host => host.host === 'claude');
|
|
120
121
|
assert.equal(claude.status, 'failed');
|
|
@@ -126,8 +127,8 @@ test('a failing command reports that host and still installs the next one', () =
|
|
|
126
127
|
|
|
127
128
|
test('an executable override replaces the host command for that host only', () => {
|
|
128
129
|
const runner = recordingRunner();
|
|
129
|
-
|
|
130
|
-
PSTACK_PLUGIN_SPEC,
|
|
130
|
+
installMarketplacePlugins(
|
|
131
|
+
[PSTACK_PLUGIN_SPEC],
|
|
131
132
|
{ ...ROOTS, environment: { CDE_CODEX_EXECUTABLE: '/opt/codex/bin/codex' } },
|
|
132
133
|
runner,
|
|
133
134
|
);
|
|
@@ -146,7 +147,7 @@ test('the opt-out flag and variable both turn the step off', () => {
|
|
|
146
147
|
|
|
147
148
|
test('a selected host list installs only that host', () => {
|
|
148
149
|
const runner = recordingRunner();
|
|
149
|
-
const outcome =
|
|
150
|
+
const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], { ...ROOTS, hosts: ['codex'] }, runner);
|
|
150
151
|
assert.deepEqual(outcome.hosts.map(host => host.host), ['codex']);
|
|
151
152
|
assert.deepEqual([...new Set(runner.calls.map(call => call.executable))], ['codex']);
|
|
152
153
|
});
|
|
@@ -199,7 +200,7 @@ test("cmd.exe's command-not-found exit code reads as an absent host, not a failu
|
|
|
199
200
|
stderr: "'codex' is not recognized as an internal or external command,\noperable program or batch file.",
|
|
200
201
|
}
|
|
201
202
|
: { status: 0, stderr: '' }));
|
|
202
|
-
const outcome =
|
|
203
|
+
const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, runner);
|
|
203
204
|
const codex = outcome.hosts.find(host => host.host === 'codex');
|
|
204
205
|
assert.equal(codex.status, 'skipped');
|
|
205
206
|
assert.equal(outcome.status, 'installed');
|
|
@@ -208,13 +209,13 @@ test("cmd.exe's command-not-found exit code reads as an absent host, not a failu
|
|
|
208
209
|
|
|
209
210
|
test('a non-zero exit that is not command-not-found still reads as a failure', () => {
|
|
210
211
|
const runner = recordingRunner(() => ({ status: 9009, stderr: 'the marketplace rejected the catalog' }));
|
|
211
|
-
const outcome =
|
|
212
|
+
const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], { ...ROOTS, hosts: ['claude'] }, runner);
|
|
212
213
|
assert.equal(outcome.hosts[0].status, 'failed');
|
|
213
214
|
assert.match(outcome.hosts[0].warning, /rejected the catalog/);
|
|
214
215
|
});
|
|
215
216
|
|
|
216
217
|
test('the usage-wrapup plan adds this repository marketplace and installs the plugin on Claude', () => {
|
|
217
|
-
const plan =
|
|
218
|
+
const plan = marketplacePluginsPlan([USAGE_WRAPUP_PLUGIN_SPEC], 'claude');
|
|
218
219
|
assert.equal(plan.executable, 'claude');
|
|
219
220
|
assert.equal(plan.homeVariable, 'CLAUDE_CONFIG_DIR');
|
|
220
221
|
assert.deepEqual(plan.commands, [
|
|
@@ -225,12 +226,12 @@ test('the usage-wrapup plan adds this repository marketplace and installs the pl
|
|
|
225
226
|
|
|
226
227
|
test('usage-wrapup is a Claude-only plugin and refuses a Codex plan', () => {
|
|
227
228
|
assert.deepEqual(USAGE_WRAPUP_PLUGIN_SPEC.hosts, ['claude']);
|
|
228
|
-
assert.throws(() =>
|
|
229
|
+
assert.throws(() => marketplacePluginsPlan([USAGE_WRAPUP_PLUGIN_SPEC], 'codex'), /usage-wrapup plugin host codex/);
|
|
229
230
|
});
|
|
230
231
|
|
|
231
232
|
test('installing usage-wrapup runs only the Claude commands, inside the managed Claude root', () => {
|
|
232
233
|
const runner = recordingRunner();
|
|
233
|
-
const outcome =
|
|
234
|
+
const outcome = installMarketplacePlugins([USAGE_WRAPUP_PLUGIN_SPEC], ROOTS, runner);
|
|
234
235
|
assert.equal(outcome.status, 'installed');
|
|
235
236
|
assert.deepEqual(outcome.hosts.map(host => host.host), ['claude']);
|
|
236
237
|
assert.deepEqual(runner.calls.map(call => [call.executable, ...call.commandArguments]), [
|
|
@@ -249,7 +250,7 @@ test('an absent claude command skips usage-wrapup and names the plugin in the wa
|
|
|
249
250
|
status: null,
|
|
250
251
|
error: Object.assign(new Error('spawn claude ENOENT'), { code: 'ENOENT' }),
|
|
251
252
|
}));
|
|
252
|
-
const outcome =
|
|
253
|
+
const outcome = installMarketplacePlugins([USAGE_WRAPUP_PLUGIN_SPEC], ROOTS, runner);
|
|
253
254
|
assert.equal(outcome.status, 'skipped');
|
|
254
255
|
assert.match(outcome.warning, /usage-wrapup was not installed for claude/);
|
|
255
256
|
});
|
|
@@ -268,16 +269,60 @@ test('the usage-wrapup opt-out flag and variable turn off only usage-wrapup', ()
|
|
|
268
269
|
);
|
|
269
270
|
});
|
|
270
271
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
272
|
+
for (const [spec, packageFolder] of [
|
|
273
|
+
[USAGE_WRAPUP_PLUGIN_SPEC, './packages/usage-wrapup'],
|
|
274
|
+
[SUBAGENT_MODELS_PLUGIN_SPEC, './packages/subagent-models'],
|
|
275
|
+
]) {
|
|
276
|
+
test(`the repository marketplace lists ${spec.name} from its package folder in this repository`, () => {
|
|
277
|
+
const marketplace = JSON.parse(readFileSync(
|
|
278
|
+
new URL('../../../.claude-plugin/marketplace.json', import.meta.url),
|
|
279
|
+
'utf8',
|
|
280
|
+
));
|
|
281
|
+
const [pluginName, marketplaceName] = spec.pluginIdentifier.split('@');
|
|
282
|
+
assert.equal(marketplace.name, marketplaceName);
|
|
283
|
+
const entry = marketplace.plugins.find(plugin => plugin.name === pluginName);
|
|
284
|
+
assert.ok(entry, `the marketplace lists ${pluginName}`);
|
|
285
|
+
assert.equal(entry.source, packageFolder);
|
|
286
|
+
assert.ok(spec.sparsePaths.includes(packageFolder.slice(2)), `the sparse checkout holds ${packageFolder}`);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
test('should remove and add the shared marketplace once when installing usage-wrapup and subagent-models together', () => {
|
|
291
|
+
const runner = recordingRunner();
|
|
292
|
+
const outcome = installMarketplacePlugins([USAGE_WRAPUP_PLUGIN_SPEC, SUBAGENT_MODELS_PLUGIN_SPEC], ROOTS, runner);
|
|
293
|
+
assert.equal(outcome.status, 'installed');
|
|
294
|
+
assert.deepEqual(outcome.hosts.map(host => host.host), ['claude']);
|
|
295
|
+
assert.deepEqual(runner.calls.map(call => [call.executable, ...call.commandArguments]), [
|
|
296
|
+
['claude', 'plugin', 'uninstall', 'usage-wrapup@claude-dev-env'],
|
|
297
|
+
['claude', 'plugin', 'uninstall', 'subagent-models@claude-dev-env'],
|
|
298
|
+
['claude', 'plugin', 'marketplace', 'remove', 'claude-dev-env'],
|
|
299
|
+
[
|
|
300
|
+
'claude', 'plugin', 'marketplace', 'add', 'jl-cmd/claude-dev-env',
|
|
301
|
+
'--sparse', '.claude-plugin', 'packages/usage-wrapup', 'packages/subagent-models',
|
|
302
|
+
],
|
|
303
|
+
['claude', 'plugin', 'install', 'usage-wrapup@claude-dev-env'],
|
|
304
|
+
['claude', 'plugin', 'install', 'subagent-models@claude-dev-env'],
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('should refuse one plan for plugins from two marketplaces', () => {
|
|
309
|
+
assert.throws(
|
|
310
|
+
() => marketplacePluginsPlan([PSTACK_PLUGIN_SPEC, SUBAGENT_MODELS_PLUGIN_SPEC], 'claude'),
|
|
311
|
+
/subagent-models comes from jl-cmd\/claude-dev-env/,
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test('should turn off only subagent-models with its opt-out flag or variable', () => {
|
|
316
|
+
assert.equal(shouldInstallMarketplacePlugin(SUBAGENT_MODELS_PLUGIN_SPEC, [], {}), true);
|
|
317
|
+
assert.equal(shouldInstallMarketplacePlugin(SUBAGENT_MODELS_PLUGIN_SPEC, ['--no-subagent-models'], {}), false);
|
|
318
|
+
assert.equal(
|
|
319
|
+
shouldInstallMarketplacePlugin(SUBAGENT_MODELS_PLUGIN_SPEC, [], { CDE_INSTALL_SUBAGENT_MODELS: '0' }),
|
|
320
|
+
false,
|
|
321
|
+
);
|
|
322
|
+
assert.equal(
|
|
323
|
+
shouldInstallMarketplacePlugin(USAGE_WRAPUP_PLUGIN_SPEC, ['--no-subagent-models'], { CDE_INSTALL_SUBAGENT_MODELS: '0' }),
|
|
324
|
+
true,
|
|
325
|
+
);
|
|
281
326
|
});
|
|
282
327
|
|
|
283
328
|
test('a clean install deletes the orphaned pstack versions and keeps the installed one', () => {
|
|
@@ -286,8 +331,8 @@ test('a clean install deletes the orphaned pstack versions and keeps the install
|
|
|
286
331
|
mkdirSync(join(versionsDirectory, '0.9.55'), { recursive: true });
|
|
287
332
|
writeFileSync(join(versionsDirectory, '0.9.55', '.orphaned_at'), '1');
|
|
288
333
|
mkdirSync(join(versionsDirectory, '0.9.64'), { recursive: true });
|
|
289
|
-
const outcome =
|
|
290
|
-
PSTACK_PLUGIN_SPEC,
|
|
334
|
+
const outcome = installMarketplacePlugins(
|
|
335
|
+
[PSTACK_PLUGIN_SPEC],
|
|
291
336
|
{ claudeRoot, hosts: ['claude'], environment: {} },
|
|
292
337
|
recordingRunner(),
|
|
293
338
|
);
|
|
@@ -52,6 +52,7 @@ function runInstaller(homeDirectory, extraArguments, environmentOverrides = {})
|
|
|
52
52
|
...process.env,
|
|
53
53
|
CDE_INSTALL_PSTACK: '0',
|
|
54
54
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
55
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
55
56
|
HOME: homeDirectory,
|
|
56
57
|
USERPROFILE: homeDirectory,
|
|
57
58
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
@@ -58,6 +58,7 @@ function runInstaller(homeDirectory, extraArguments) {
|
|
|
58
58
|
...process.env,
|
|
59
59
|
CDE_INSTALL_PSTACK: '0',
|
|
60
60
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
61
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
61
62
|
HOME: homeDirectory,
|
|
62
63
|
USERPROFILE: homeDirectory,
|
|
63
64
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
@@ -87,6 +88,7 @@ function scanTextWithProductionPiiScanner(scannedText) {
|
|
|
87
88
|
...process.env,
|
|
88
89
|
CDE_INSTALL_PSTACK: '0',
|
|
89
90
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
91
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
90
92
|
PYTHONPATH: BLOCKING_HOOKS_DIRECTORY,
|
|
91
93
|
CLAUDE_LOCAL_IDENTITY_PATH: join(
|
|
92
94
|
temporaryDirectory,
|
|
@@ -33,6 +33,7 @@ function runInstaller(homeDirectory, extraArguments, environmentOverrides = {})
|
|
|
33
33
|
...process.env,
|
|
34
34
|
CDE_INSTALL_PSTACK: '0',
|
|
35
35
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
36
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
36
37
|
HOME: homeDirectory,
|
|
37
38
|
USERPROFILE: homeDirectory,
|
|
38
39
|
CODEX_HOME: join(homeDirectory, '.codex'),
|
package/bin/install.mjs
CHANGED
|
@@ -49,8 +49,9 @@ import {
|
|
|
49
49
|
} from './install-constants.mjs';
|
|
50
50
|
import {
|
|
51
51
|
PSTACK_PLUGIN_SPEC,
|
|
52
|
+
SUBAGENT_MODELS_PLUGIN_SPEC,
|
|
52
53
|
USAGE_WRAPUP_PLUGIN_SPEC,
|
|
53
|
-
|
|
54
|
+
installMarketplacePlugins,
|
|
54
55
|
shouldInstallMarketplacePlugin,
|
|
55
56
|
} from './install-pstack-plugin.mjs';
|
|
56
57
|
import { seedCodexPstackModels } from './seed-codex-pstack-models.mjs';
|
|
@@ -407,17 +408,19 @@ export function pythonFileAndPrefixArguments(pythonCommand) {
|
|
|
407
408
|
* @returns {{status: string, hosts: object[], warning: string|null}} The outcome.
|
|
408
409
|
*/
|
|
409
410
|
export function installPstackPluginForHosts() {
|
|
410
|
-
return
|
|
411
|
+
return installMarketplacePlugins([PSTACK_PLUGIN_SPEC], {
|
|
411
412
|
claudeRoot: CLAUDE_HOME,
|
|
412
413
|
codexHome: INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
413
414
|
});
|
|
414
415
|
}
|
|
415
416
|
|
|
416
|
-
function printPluginHostOutcomes(
|
|
417
|
-
for (const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
417
|
+
function printPluginHostOutcomes(allSpecs, pluginOutcome) {
|
|
418
|
+
for (const eachSpec of allSpecs) {
|
|
419
|
+
for (const eachHost of pluginOutcome.hosts) {
|
|
420
|
+
console.log(eachHost.warning
|
|
421
|
+
? ` ${eachSpec.label} (${eachHost.host}): ${eachHost.status} — ${eachHost.warning}`
|
|
422
|
+
: ` ${eachSpec.label} (${eachHost.host}): ${eachHost.status}`);
|
|
423
|
+
}
|
|
421
424
|
}
|
|
422
425
|
}
|
|
423
426
|
|
|
@@ -2890,14 +2893,16 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2890
2893
|
console.log(` \u2713 ${skillLoadGuidancePath} (Codex skill-load line)`);
|
|
2891
2894
|
}
|
|
2892
2895
|
}
|
|
2893
|
-
printPluginHostOutcomes(PSTACK_PLUGIN_SPEC, pstackPlugin);
|
|
2896
|
+
printPluginHostOutcomes([PSTACK_PLUGIN_SPEC], pstackPlugin);
|
|
2894
2897
|
}
|
|
2895
|
-
|
|
2896
|
-
|
|
2898
|
+
const allRepositoryPluginSpecs = [USAGE_WRAPUP_PLUGIN_SPEC, SUBAGENT_MODELS_PLUGIN_SPEC]
|
|
2899
|
+
.filter(eachSpec => shouldInstallMarketplacePlugin(eachSpec));
|
|
2900
|
+
if (!selectedGroups && allRepositoryPluginSpecs.length > 0) {
|
|
2901
|
+
const repositoryPlugins = installMarketplacePlugins(allRepositoryPluginSpecs, {
|
|
2897
2902
|
claudeRoot: CLAUDE_HOME,
|
|
2898
2903
|
});
|
|
2899
|
-
summary.
|
|
2900
|
-
printPluginHostOutcomes(
|
|
2904
|
+
summary.repositoryPlugins = repositoryPlugins;
|
|
2905
|
+
printPluginHostOutcomes(allRepositoryPluginSpecs, repositoryPlugins);
|
|
2901
2906
|
}
|
|
2902
2907
|
if (!selectedGroups) {
|
|
2903
2908
|
const packageGuidancePath = writeCodexPackageGuidance(
|
|
@@ -3412,6 +3417,7 @@ Usage:
|
|
|
3412
3417
|
npx ${PACKAGE_NAME} --profiles A,B Install into each selected profile (one ownership manifest per target)
|
|
3413
3418
|
npx ${PACKAGE_NAME} --no-pstack Full install without the pstack plugin (also CDE_INSTALL_PSTACK=0)
|
|
3414
3419
|
npx ${PACKAGE_NAME} --no-usage-wrapup Full install without the usage-wrapup plugin (also CDE_INSTALL_USAGE_WRAPUP=0)
|
|
3420
|
+
npx ${PACKAGE_NAME} --no-subagent-models Full install without the subagent-models plugin (also CDE_INSTALL_SUBAGENT_MODELS=0)
|
|
3415
3421
|
npx ${PACKAGE_NAME} --uninstall Remove installed files from the selected root
|
|
3416
3422
|
npx ${PACKAGE_NAME} --help Show this help
|
|
3417
3423
|
|
|
@@ -261,6 +261,7 @@ function resolveInstallerInvocation(homeDirectory) {
|
|
|
261
261
|
...process.env,
|
|
262
262
|
CDE_INSTALL_PSTACK: '0',
|
|
263
263
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
264
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
264
265
|
HOME: homeDirectory,
|
|
265
266
|
USERPROFILE: homeDirectory,
|
|
266
267
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
@@ -24,6 +24,7 @@ function installInScratchHome(homeDirectory, argumentsList = [], extraEnvironmen
|
|
|
24
24
|
...process.env,
|
|
25
25
|
CDE_INSTALL_PSTACK: '0',
|
|
26
26
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
27
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
27
28
|
HOME: homeDirectory,
|
|
28
29
|
USERPROFILE: homeDirectory,
|
|
29
30
|
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
@@ -179,6 +180,7 @@ test('a successful pstack install retains its skill block and gains the question
|
|
|
179
180
|
const installation = installInScratchHome(homeDirectory, [], {
|
|
180
181
|
CDE_INSTALL_PSTACK: '1',
|
|
181
182
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
183
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
182
184
|
CDE_CODEX_EXECUTABLE: commandPath,
|
|
183
185
|
CDE_CLAUDE_EXECUTABLE: commandPath,
|
|
184
186
|
});
|
|
@@ -206,6 +208,7 @@ test('enabling pstack after a pstack-off install preserves question guidance', (
|
|
|
206
208
|
const secondInstallation = installInScratchHome(homeDirectory, [], {
|
|
207
209
|
CDE_INSTALL_PSTACK: '1',
|
|
208
210
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
211
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
209
212
|
CDE_CODEX_EXECUTABLE: commandPath,
|
|
210
213
|
CDE_CLAUDE_EXECUTABLE: commandPath,
|
|
211
214
|
});
|
|
@@ -234,6 +237,7 @@ test('a failed pstack transition restores question-only guidance and removes its
|
|
|
234
237
|
const failedInstallation = installInScratchHome(homeDirectory, [], {
|
|
235
238
|
CDE_INSTALL_PSTACK: '1',
|
|
236
239
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
240
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
237
241
|
CDE_CODEX_EXECUTABLE: commandPath,
|
|
238
242
|
CDE_CLAUDE_EXECUTABLE: commandPath,
|
|
239
243
|
CLAUDE_DEV_ENV_INSTALL_FAULT: 'after_file_staging',
|
|
@@ -58,6 +58,7 @@ function runInstallerInSandbox(sandboxHome, installerArguments = []) {
|
|
|
58
58
|
...process.env,
|
|
59
59
|
CDE_INSTALL_PSTACK: '0',
|
|
60
60
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
61
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
61
62
|
HOME: sandboxHome,
|
|
62
63
|
USERPROFILE: sandboxHome,
|
|
63
64
|
GIT_CONFIG_GLOBAL: gitConfigGlobal,
|
package/bin/install.test.mjs
CHANGED
|
@@ -3026,6 +3026,7 @@ test('a failed update preserves an existing skills lookup pointer', () => {
|
|
|
3026
3026
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
3027
3027
|
CDE_INSTALL_PSTACK: '0',
|
|
3028
3028
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
3029
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
3029
3030
|
};
|
|
3030
3031
|
const argumentsForCoreInstall = [PSTACK_TEST_INSTALLER_PATH, '--only', 'core'];
|
|
3031
3032
|
const firstRun = spawnSync(process.execPath, argumentsForCoreInstall, {
|
|
@@ -3077,6 +3078,7 @@ function runPstackInstaller(homeDirectory, extraArguments, environmentOverrides
|
|
|
3077
3078
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
3078
3079
|
CDE_INSTALL_PSTACK: '1',
|
|
3079
3080
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
3081
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
3080
3082
|
...environmentOverrides,
|
|
3081
3083
|
},
|
|
3082
3084
|
});
|
|
@@ -3391,6 +3393,60 @@ test('the help output names the usage-wrapup opt-out beside the pstack one', ()
|
|
|
3391
3393
|
assert.match(helpRun.stdout, /--no-pstack .*\n.*--no-usage-wrapup .*CDE_INSTALL_USAGE_WRAPUP=0/);
|
|
3392
3394
|
});
|
|
3393
3395
|
|
|
3396
|
+
const REPOSITORY_PLUGIN_CLAUDE_COMMANDS = Object.freeze([
|
|
3397
|
+
'claude plugin uninstall usage-wrapup@claude-dev-env',
|
|
3398
|
+
'claude plugin uninstall subagent-models@claude-dev-env',
|
|
3399
|
+
'claude plugin marketplace remove claude-dev-env',
|
|
3400
|
+
'claude plugin marketplace add jl-cmd/claude-dev-env --sparse .claude-plugin packages/usage-wrapup packages/subagent-models',
|
|
3401
|
+
'claude plugin install usage-wrapup@claude-dev-env',
|
|
3402
|
+
'claude plugin install subagent-models@claude-dev-env',
|
|
3403
|
+
]);
|
|
3404
|
+
|
|
3405
|
+
test('should install subagent-models beside usage-wrapup with one marketplace add on a full install', t => {
|
|
3406
|
+
const sandbox = pstackPluginSandbox(t);
|
|
3407
|
+
|
|
3408
|
+
const installerOutput = runPstackInstaller(sandbox.homeDirectory, ['--no-pstack'], {
|
|
3409
|
+
...sandbox.environment,
|
|
3410
|
+
CDE_INSTALL_USAGE_WRAPUP: '1',
|
|
3411
|
+
CDE_INSTALL_SUBAGENT_MODELS: '1',
|
|
3412
|
+
});
|
|
3413
|
+
|
|
3414
|
+
assert.deepEqual(usageWrapupCommands(sandbox.recordedCommands()), REPOSITORY_PLUGIN_CLAUDE_COMMANDS);
|
|
3415
|
+
assert.match(installerOutput, /Usage-wrapup \(claude\): installed/);
|
|
3416
|
+
assert.match(installerOutput, /Subagent-models \(claude\): installed/);
|
|
3417
|
+
});
|
|
3418
|
+
|
|
3419
|
+
for (const [caseName, extraArguments, environmentOverrides] of [
|
|
3420
|
+
['--no-subagent-models', ['--no-subagent-models'], { CDE_INSTALL_SUBAGENT_MODELS: '1' }],
|
|
3421
|
+
['CDE_INSTALL_SUBAGENT_MODELS=0', [], { CDE_INSTALL_SUBAGENT_MODELS: '0' }],
|
|
3422
|
+
['an --only run', ['--only', 'core'], { CDE_INSTALL_SUBAGENT_MODELS: '1' }],
|
|
3423
|
+
]) {
|
|
3424
|
+
test(`should skip subagent-models on ${caseName}`, t => {
|
|
3425
|
+
const sandbox = pstackPluginSandbox(t);
|
|
3426
|
+
|
|
3427
|
+
const installerOutput = runPstackInstaller(sandbox.homeDirectory, extraArguments, {
|
|
3428
|
+
...sandbox.environment,
|
|
3429
|
+
...environmentOverrides,
|
|
3430
|
+
});
|
|
3431
|
+
|
|
3432
|
+
assert.equal(
|
|
3433
|
+
sandbox.recordedCommands().some(eachCommand => eachCommand.includes('subagent-models')),
|
|
3434
|
+
false,
|
|
3435
|
+
);
|
|
3436
|
+
assert.doesNotMatch(installerOutput, /Subagent-models \(/);
|
|
3437
|
+
});
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
test('should name the subagent-models opt-out in the help output', () => {
|
|
3441
|
+
const helpRun = spawnSync(process.execPath, [PSTACK_TEST_INSTALLER_PATH, '--help'], {
|
|
3442
|
+
encoding: 'utf8',
|
|
3443
|
+
env: process.env,
|
|
3444
|
+
});
|
|
3445
|
+
|
|
3446
|
+
assert.equal(helpRun.status, 0, helpRun.stderr);
|
|
3447
|
+
assert.match(helpRun.stdout, /--no-subagent-models .*CDE_INSTALL_SUBAGENT_MODELS=0/);
|
|
3448
|
+
});
|
|
3449
|
+
|
|
3394
3450
|
function continuityCommandCount(configurationPath) {
|
|
3395
3451
|
if (!existsSync(configurationPath)) return 0;
|
|
3396
3452
|
const configuration = JSON.parse(readFileSync(configurationPath, 'utf8'));
|
|
@@ -87,6 +87,7 @@ test('capture and restore recover settings, manifest, files, and hooksPath', ()
|
|
|
87
87
|
...process.env,
|
|
88
88
|
CDE_INSTALL_PSTACK: '0',
|
|
89
89
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
90
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
90
91
|
HOME: box.root,
|
|
91
92
|
USERPROFILE: box.root,
|
|
92
93
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -149,6 +150,7 @@ test('capture and restore recover an additional host settings file', () => {
|
|
|
149
150
|
...process.env,
|
|
150
151
|
CDE_INSTALL_PSTACK: '0',
|
|
151
152
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
153
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
152
154
|
HOME: box.root,
|
|
153
155
|
USERPROFILE: box.root,
|
|
154
156
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -184,6 +186,7 @@ test('runWithInstallTransaction restores prior state on injected fault', () => {
|
|
|
184
186
|
...process.env,
|
|
185
187
|
CDE_INSTALL_PSTACK: '0',
|
|
186
188
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
189
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
187
190
|
HOME: box.root,
|
|
188
191
|
USERPROFILE: box.root,
|
|
189
192
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -238,6 +241,7 @@ test('runWithInstallTransaction commits and discards journal on success', () =>
|
|
|
238
241
|
...process.env,
|
|
239
242
|
CDE_INSTALL_PSTACK: '0',
|
|
240
243
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
244
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
241
245
|
HOME: box.root,
|
|
242
246
|
USERPROFILE: box.root,
|
|
243
247
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -281,6 +285,7 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
|
|
|
281
285
|
...process.env,
|
|
282
286
|
CDE_INSTALL_PSTACK: '0',
|
|
283
287
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
288
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
284
289
|
HOME: homeDirectory,
|
|
285
290
|
USERPROFILE: homeDirectory,
|
|
286
291
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
@@ -316,6 +321,7 @@ test('a later install fault removes newly seeded Codex pstack files', () => {
|
|
|
316
321
|
environment: {
|
|
317
322
|
CDE_INSTALL_PSTACK: '1',
|
|
318
323
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
324
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
319
325
|
CDE_CODEX_EXECUTABLE: codexCommandPath,
|
|
320
326
|
},
|
|
321
327
|
});
|
|
@@ -459,6 +465,7 @@ test('installer fault after_git_config restores prior core.hooksPath', () => {
|
|
|
459
465
|
...process.env,
|
|
460
466
|
CDE_INSTALL_PSTACK: '0',
|
|
461
467
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
468
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
462
469
|
HOME: homeDirectory,
|
|
463
470
|
USERPROFILE: homeDirectory,
|
|
464
471
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -476,6 +483,7 @@ test('installer fault after_git_config restores prior core.hooksPath', () => {
|
|
|
476
483
|
...process.env,
|
|
477
484
|
CDE_INSTALL_PSTACK: '0',
|
|
478
485
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
486
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
479
487
|
HOME: homeDirectory,
|
|
480
488
|
USERPROFILE: homeDirectory,
|
|
481
489
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -73,6 +73,7 @@ function runInstaller(homeDirectory, extraArguments, options = {}) {
|
|
|
73
73
|
...process.env,
|
|
74
74
|
CDE_INSTALL_PSTACK: '0',
|
|
75
75
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
76
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
76
77
|
HOME: homeDirectory,
|
|
77
78
|
USERPROFILE: homeDirectory,
|
|
78
79
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
@@ -307,6 +308,7 @@ test('uninstall fault after_git_config restores prior core.hooksPath', () => {
|
|
|
307
308
|
...process.env,
|
|
308
309
|
CDE_INSTALL_PSTACK: '0',
|
|
309
310
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
311
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
310
312
|
HOME: homeDirectory,
|
|
311
313
|
USERPROFILE: homeDirectory,
|
|
312
314
|
GIT_CONFIG_GLOBAL: gitConfigPath,
|
|
@@ -24,6 +24,7 @@ function installerEnvironment(homeDirectory) {
|
|
|
24
24
|
...process.env,
|
|
25
25
|
CDE_INSTALL_PSTACK: '0',
|
|
26
26
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
27
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
27
28
|
HOME: homeDirectory,
|
|
28
29
|
USERPROFILE: homeDirectory,
|
|
29
30
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
package/package.json
CHANGED
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import dataclasses
|
|
6
6
|
import errno
|
|
7
|
+
import importlib
|
|
7
8
|
import json
|
|
8
9
|
import os
|
|
9
10
|
import shutil
|
|
@@ -63,6 +64,16 @@ from dev_env_scripts_constants.codex_account_constants import (
|
|
|
63
64
|
WEEKLY_WINDOW_MINUTES,
|
|
64
65
|
)
|
|
65
66
|
from dev_env_scripts_constants.shared_tree_constants import CLAUDE_CONFIG_DIR_ENV_VAR
|
|
67
|
+
from shared_tree_paths import resolve_shared_process_tree_scripts_directory
|
|
68
|
+
|
|
69
|
+
_shared_process_tree_scripts_directory = resolve_shared_process_tree_scripts_directory(
|
|
70
|
+
__file__,
|
|
71
|
+
all_environment=os.environ,
|
|
72
|
+
)
|
|
73
|
+
if str(_shared_process_tree_scripts_directory) not in sys.path:
|
|
74
|
+
sys.path.insert(0, str(_shared_process_tree_scripts_directory))
|
|
75
|
+
|
|
76
|
+
_process_tree_kill = importlib.import_module("process_tree_kill")
|
|
66
77
|
|
|
67
78
|
|
|
68
79
|
def _read_list(path: Path) -> object:
|
|
@@ -492,21 +503,26 @@ def _run_captured_subprocess(all_argv: Sequence[str], **options: object) -> subp
|
|
|
492
503
|
errors = str(options.get("errors") or "replace")
|
|
493
504
|
stdin_bytes = options.get("input")
|
|
494
505
|
with tempfile.TemporaryFile() as stdout_file, tempfile.TemporaryFile() as stderr_file:
|
|
495
|
-
|
|
506
|
+
with subprocess.Popen(
|
|
496
507
|
_resolve_command(all_argv),
|
|
497
|
-
|
|
508
|
+
stdin=subprocess.PIPE if stdin_bytes is not None else None,
|
|
498
509
|
stdout=stdout_file,
|
|
499
510
|
stderr=stderr_file,
|
|
500
511
|
env=options.get("env"),
|
|
501
512
|
cwd=options.get("cwd"),
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
513
|
+
start_new_session=_process_tree_kill.should_start_new_session(),
|
|
514
|
+
) as process:
|
|
515
|
+
try:
|
|
516
|
+
process.communicate(input=stdin_bytes, timeout=options.get("timeout"))
|
|
517
|
+
except (subprocess.TimeoutExpired, KeyboardInterrupt):
|
|
518
|
+
_process_tree_kill.terminate_process_tree(process)
|
|
519
|
+
process.wait()
|
|
520
|
+
raise
|
|
505
521
|
stdout_file.seek(0)
|
|
506
522
|
stderr_file.seek(0)
|
|
507
523
|
stdout = stdout_file.read().decode(encoding, errors).replace("\r\n", "\n").replace("\r", "\n")
|
|
508
524
|
stderr = stderr_file.read().decode(encoding, errors).replace("\r\n", "\n").replace("\r", "\n")
|
|
509
|
-
return subprocess.CompletedProcess(list(all_argv),
|
|
525
|
+
return subprocess.CompletedProcess(list(all_argv), process.returncode, stdout, stderr)
|
|
510
526
|
|
|
511
527
|
|
|
512
528
|
subprocess_runner: SubprocessRunner = _run_captured_subprocess
|