claude-dev-env 8.89.0 → 8.90.0
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/.agents/skills/pr-visual/README.md +29 -0
- package/.agents/skills/pr-visual/SKILL.md +62 -0
- package/.agents/skills/pr-visual/assets/icons.symbols +27 -0
- package/.agents/skills/pr-visual/assets/viewer.html +147 -0
- package/.agents/skills/pr-visual/fixtures/bar.scene.json +193 -0
- package/.agents/skills/pr-visual/fixtures/flow.scene.json +201 -0
- package/.agents/skills/pr-visual/fixtures/head/metrics.json +16 -0
- package/.agents/skills/pr-visual/fixtures/integrated.scene.json +239 -0
- package/.agents/skills/pr-visual/fixtures/line.scene.json +193 -0
- package/.agents/skills/pr-visual/fixtures/scatter.scene.json +193 -0
- package/.agents/skills/pr-visual/fixtures/snapshot.json +21 -0
- package/.agents/skills/pr-visual/references/evidence.md +9 -0
- package/.agents/skills/pr-visual/references/formats.md +30 -0
- package/.agents/skills/pr-visual/references/scene-contract.md +48 -0
- package/.agents/skills/pr-visual/references/visual-contract.md +23 -0
- package/.agents/skills/pr-visual/scripts/collect.py +111 -0
- package/.agents/skills/pr-visual/scripts/metrics.py +99 -0
- package/.agents/skills/pr-visual/scripts/render.py +133 -0
- package/.agents/skills/pr-visual/scripts/test_collect.py +58 -0
- package/.agents/skills/pr-visual/scripts/test_metrics.py +47 -0
- package/.agents/skills/pr-visual/scripts/test_render.py +77 -0
- package/.agents/skills/pr-visual/scripts/verify.py +218 -0
- package/.agents/skills/pr-visual/scripts/visual_config/config/constants.py +104 -0
- package/bin/codex-skill-load-block.mjs +0 -25
- package/bin/codex-skill-load-block.test.mjs +5 -17
- package/bin/codex-trimmed-sol.test.mjs +0 -1
- package/bin/cursor-native-hooks.test.mjs +3 -3
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-deepstack-plugin.mjs +42 -25
- package/bin/install-deepstack-plugin.test.mjs +37 -11
- package/bin/install-pstack-plugin.mjs +58 -11
- package/bin/install-pstack-plugin.test.mjs +54 -21
- package/bin/install.agents-home.test.mjs +0 -1
- package/bin/install.codex-rules.test.mjs +0 -2
- package/bin/install.cursor-rules.test.mjs +90 -26
- package/bin/install.mjs +30 -36
- package/bin/install.pr-visual.test.mjs +40 -0
- package/bin/install.prompt-context.test.mjs +3 -4
- package/bin/install.prune.test.mjs +0 -1
- package/bin/install.question-presentation.test.mjs +0 -4
- package/bin/install.settings-defaults.test.mjs +0 -1
- package/bin/install.shared-settings.test.mjs +0 -1
- package/bin/install.test.mjs +14 -44
- package/bin/install.transaction.test.mjs +0 -8
- package/bin/install.uninstall-transaction.test.mjs +0 -2
- package/bin/install.update-keeps-hooks.test.mjs +0 -1
- package/docs/BDD_SCENARIO_QUALITY.md +1 -1
- package/docs/BDD_TEST_LAYOUT.md +1 -1
- package/docs/CODE_RULES.md +1 -1
- package/docs/rule-guides/deepstack-wording.md +3 -5
- package/docs/rule-guides/session-title.md +5 -4
- package/hooks/blocking/session_title_turn_gate.py +287 -0
- package/hooks/blocking/test_session_title_format_gate.py +5 -0
- package/hooks/blocking/test_session_title_turn_gate.py +264 -0
- package/hooks/features/conduct-gates.md +3 -0
- package/hooks/features/session-context.md +0 -4
- package/hooks/hooks.json +5 -5
- package/hooks/hooks_constants/mod_handoff_constants.py +0 -2
- package/hooks/hooks_constants/session_title_constants.py +41 -3
- package/hooks/lifecycle/nested_project_hooks.py +2 -2
- package/hooks/lifecycle/test_nested_project_hooks.py +2 -2
- package/hooks/session/compaction_user_messages.py +2 -2
- package/package.json +2 -2
- package/rules/deepstack-wording.md +3 -3
- package/scripts/ci/install-playtest.mjs +58 -314
- package/scripts/ci/install-playtest.test.mjs +24 -6
- package/scripts/policy_lint/adapter_configuration.py +1 -0
- package/scripts/policy_lint/config/constants.py +1 -0
- package/scripts/test_deepstack_review_criterion.py +2 -3
- package/scripts/test_deepstack_wording_rule.py +6 -4
- package/scripts/tests/test_adapter_configuration.py +8 -0
- package/.agents/skills/deepen/SKILL.md +0 -26
- package/commands/deepen.md +0 -14
- package/docs/decisions/deepstack-gate.md +0 -32
- package/hooks/hooks_constants/deepen_first_prompt_constants.py +0 -187
- package/hooks/session/deepen_default_rewrite.py +0 -128
- package/hooks/session/deepen_first_prompt.py +0 -383
- package/hooks/session/deepen_role_gate.py +0 -171
- package/hooks/session/deepen_turn_marker.py +0 -75
- package/hooks/session/fixtures/deepstack_replay.json +0 -15
- package/hooks/session/grade_deepstack_replay.py +0 -78
- package/hooks/session/test_deepen_default_rewrite.py +0 -90
- package/hooks/session/test_deepen_first_prompt.py +0 -452
- package/hooks/session/test_deepen_role_gate.py +0 -83
- package/hooks/session/test_deepen_turn_marker.py +0 -59
- package/hooks/session/test_grade_deepstack_replay.py +0 -47
|
@@ -9,14 +9,6 @@ export const QUESTION_PRESENTATION_BLOCK_START = '<!-- claude-dev-env question p
|
|
|
9
9
|
export const QUESTION_PRESENTATION_BLOCK_END = '<!-- claude-dev-env question presentation: end -->';
|
|
10
10
|
export const DEEPEN_GUIDANCE_BLOCK_START = '<!-- claude-dev-env deepen: start -->';
|
|
11
11
|
export const DEEPEN_GUIDANCE_BLOCK_END = '<!-- claude-dev-env deepen: end -->';
|
|
12
|
-
export const DEEPEN_GUIDANCE_TEXT = [
|
|
13
|
-
'Deepen the first interactive user task before any work starts.',
|
|
14
|
-
'Follow DeepStack intake. Write SCOPE (role), EXAMPLES (illustration only), and ZOOM-IN BLOCKER before any plan.',
|
|
15
|
-
'The worker prompt carries the marker CDE_DEEPEN:v1 on line 1 and the SCOPE (role) line.',
|
|
16
|
-
'On demand, follow the DeepStack skill or /deepen with a task.',
|
|
17
|
-
'Cursor Cloud registers the DeepStack SessionStart hook and the first-task gate. Use DeepStack intake on the launch prompt.',
|
|
18
|
-
'Codex cloud orchestration skips command hooks. This block is the local Codex instruction.',
|
|
19
|
-
].join('\n');
|
|
20
12
|
|
|
21
13
|
const WINDOWS_LONG_PATH_PREFIX = '\\\\?\\';
|
|
22
14
|
|
|
@@ -124,23 +116,6 @@ export function writeCodexPackageGuidance(codexHome, packageGuidanceText, allPac
|
|
|
124
116
|
);
|
|
125
117
|
}
|
|
126
118
|
|
|
127
|
-
export function writeCodexDeepenGuidance(codexHome, deepenText = DEEPEN_GUIDANCE_TEXT) {
|
|
128
|
-
const agentsPath = join(codexHome, 'AGENTS.md');
|
|
129
|
-
const agentsEntry = lstatSync(agentsPath, { throwIfNoEntry: false });
|
|
130
|
-
if (agentsEntry && !agentsEntry.isFile()) return null;
|
|
131
|
-
const currentText = agentsEntry ? readFileSync(agentsPath, 'utf8') : '';
|
|
132
|
-
const block = managedBlock(DEEPEN_GUIDANCE_BLOCK_START, deepenText, DEEPEN_GUIDANCE_BLOCK_END);
|
|
133
|
-
const replacedText = withBlockReplaced(
|
|
134
|
-
currentText, DEEPEN_GUIDANCE_BLOCK_START, DEEPEN_GUIDANCE_BLOCK_END, block,
|
|
135
|
-
);
|
|
136
|
-
const separator = currentText && !currentText.endsWith('\n') ? '\n' : '';
|
|
137
|
-
const updatedText = replacedText ?? `${currentText}${separator}${block}`;
|
|
138
|
-
if (updatedText === currentText) return null;
|
|
139
|
-
mkdirSync(codexHome, { recursive: true });
|
|
140
|
-
writeFileSync(agentsPath, updatedText, 'utf8');
|
|
141
|
-
return agentsPath;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
119
|
export function writeCodexQuestionGuidance(codexHome, policyText) {
|
|
145
120
|
const agentsPath = join(codexHome, 'AGENTS.md');
|
|
146
121
|
const agentsEntry = lstatSync(agentsPath, { throwIfNoEntry: false });
|
|
@@ -16,9 +16,7 @@ import {
|
|
|
16
16
|
withoutSkillLoadBlock,
|
|
17
17
|
DEEPEN_GUIDANCE_BLOCK_END,
|
|
18
18
|
DEEPEN_GUIDANCE_BLOCK_START,
|
|
19
|
-
DEEPEN_GUIDANCE_TEXT,
|
|
20
19
|
writeCodexAgentsGuidance,
|
|
21
|
-
writeCodexDeepenGuidance,
|
|
22
20
|
writeCodexPackageGuidance,
|
|
23
21
|
writeCodexQuestionGuidance,
|
|
24
22
|
} from './codex-skill-load-block.mjs';
|
|
@@ -28,6 +26,7 @@ const STALE_SKILL_LOAD_SENTENCE = 'Load the pstack:poteto-mode skill before your
|
|
|
28
26
|
+ 'including a helper started with spawn_agent, whatever its task message says.';
|
|
29
27
|
const STALE_SKILL_LOAD_BLOCK = `${SKILL_LOAD_BLOCK_START}\n${STALE_SKILL_LOAD_SENTENCE}\n${SKILL_LOAD_BLOCK_END}\n`;
|
|
30
28
|
const PACKAGE_GUIDANCE = 'Status\n\nChanged / proof / blocked.\n';
|
|
29
|
+
const RETIRED_DEEPEN_BLOCK = `${DEEPEN_GUIDANCE_BLOCK_START}\nDeepen the first interactive user task.\n${DEEPEN_GUIDANCE_BLOCK_END}\n`;
|
|
31
30
|
const QUESTION_POLICY = '# Present questions clearly\n\nAsk one question.\n';
|
|
32
31
|
|
|
33
32
|
function guidanceBlock(guidanceText) {
|
|
@@ -109,7 +108,7 @@ test('a stale skill-load sentence is removed and question and deepen blocks stay
|
|
|
109
108
|
const homes = makeHomes(context);
|
|
110
109
|
mkdirSync(homes.codexHome);
|
|
111
110
|
const questionBlock = `${QUESTION_PRESENTATION_BLOCK_START}\n${QUESTION_POLICY}${QUESTION_PRESENTATION_BLOCK_END}\n`;
|
|
112
|
-
const deepenBlock =
|
|
111
|
+
const deepenBlock = RETIRED_DEEPEN_BLOCK;
|
|
113
112
|
writeFileSync(
|
|
114
113
|
homes.agentsPath,
|
|
115
114
|
`${STALE_SKILL_LOAD_BLOCK}\n${guidanceBlock('Old guidance\n')}${questionBlock}${deepenBlock}`,
|
|
@@ -397,24 +396,13 @@ test('question removal leaves a missing guidance file absent', (context) => {
|
|
|
397
396
|
assert.equal(lstatSync(homes.agentsPath, { throwIfNoEntry: false }), undefined);
|
|
398
397
|
});
|
|
399
398
|
|
|
400
|
-
test('should
|
|
401
|
-
const homes = makeHomes(context);
|
|
402
|
-
|
|
403
|
-
assert.equal(writeCodexDeepenGuidance(homes.codexHome), homes.agentsPath);
|
|
404
|
-
const guidanceText = readFileSync(homes.agentsPath, 'utf8');
|
|
405
|
-
assert.ok(guidanceText.includes(DEEPEN_GUIDANCE_BLOCK_START));
|
|
406
|
-
assert.ok(guidanceText.includes(DEEPEN_GUIDANCE_BLOCK_END));
|
|
407
|
-
assert.ok(guidanceText.includes(DEEPEN_GUIDANCE_TEXT.split('\n')[0]));
|
|
408
|
-
assert.equal(writeCodexDeepenGuidance(homes.codexHome), null);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
test('should restore the custom guidance bytes when the deepen block is removed', (context) => {
|
|
399
|
+
test('should restore the custom guidance bytes when a retired deepen block is removed', (context) => {
|
|
412
400
|
const homes = makeHomes(context);
|
|
413
401
|
mkdirSync(homes.codexHome);
|
|
414
402
|
const customGuidance = '# My notes\n\nKeep this line.\n';
|
|
415
|
-
writeFileSync(homes.agentsPath, customGuidance);
|
|
403
|
+
writeFileSync(homes.agentsPath, `${customGuidance}${RETIRED_DEEPEN_BLOCK}`);
|
|
416
404
|
|
|
417
|
-
assert.equal(writeCodexDeepenGuidance(homes.codexHome), homes.agentsPath);
|
|
418
405
|
assert.equal(removeCodexDeepenGuidance(homes.codexHome), homes.agentsPath);
|
|
419
406
|
assert.equal(readFileSync(homes.agentsPath, 'utf8'), customGuidance);
|
|
407
|
+
assert.equal(removeCodexDeepenGuidance(homes.codexHome), null);
|
|
420
408
|
});
|
|
@@ -35,7 +35,6 @@ function installInScratchHome(homeDirectory, argumentsList = [], environment = {
|
|
|
35
35
|
CDE_INSTALL_DEEPSTACK: '0',
|
|
36
36
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
37
37
|
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
38
|
-
CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
|
|
39
38
|
CLAUDE_DEV_ENV_INSTALL_FAULT: undefined,
|
|
40
39
|
HOME: homeDirectory,
|
|
41
40
|
USERPROFILE: homeDirectory,
|
|
@@ -17,10 +17,10 @@ const LEGACY_SESSION_GROUP = {
|
|
|
17
17
|
};
|
|
18
18
|
const LEGACY_PROMPT_GROUP = {
|
|
19
19
|
matcher: '',
|
|
20
|
-
hooks: [{ type: 'command', command: 'python3 /home/dev/.agents/hooks/session/
|
|
20
|
+
hooks: [{ type: 'command', command: 'python3 /home/dev/.agents/hooks/session/first_prompt_gate.py', timeout: 10 }],
|
|
21
21
|
};
|
|
22
22
|
const USER_TOOL_GROUP = { matcher: 'Shell', hooks: [{ type: 'command', command: './audit.sh' }] };
|
|
23
|
-
const isOwned = command => command.includes('plugins/deepstack/hooks/session-start.') || command.includes('
|
|
23
|
+
const isOwned = command => command.includes('plugins/deepstack/hooks/session-start.') || command.includes('first_prompt_gate.py');
|
|
24
24
|
|
|
25
25
|
test('Claude-style groups become flat native entries under the Cursor event names', () => {
|
|
26
26
|
const native = nativeCursorHooksConfiguration({
|
|
@@ -54,7 +54,7 @@ test('owned entries collapse to one copy and removal drops only owned entries',
|
|
|
54
54
|
};
|
|
55
55
|
const ownedEntries = {
|
|
56
56
|
sessionStart: [{ command: "'/home/dev/.cursor/plugins/deepstack/hooks/session-start.sh' cursor", timeout: 10 }],
|
|
57
|
-
beforeSubmitPrompt: [{ command: 'python3 /home/dev/.agents/hooks/session/
|
|
57
|
+
beforeSubmitPrompt: [{ command: 'python3 /home/dev/.agents/hooks/session/first_prompt_gate.py', timeout: 10 }],
|
|
58
58
|
};
|
|
59
59
|
const merged = withOwnedCursorHooks(legacyFile, isOwned, ownedEntries);
|
|
60
60
|
assert.deepEqual(merged.hooks, { ...ownedEntries, stop: [{ command: './user-stop.sh' }] });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import {
|
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
shouldInstallMarketplacePlugin,
|
|
9
9
|
} from './install-pstack-plugin.mjs';
|
|
10
10
|
import {
|
|
11
|
-
CURSOR_BEFORE_SUBMIT_PROMPT_EVENT,
|
|
12
11
|
CURSOR_HOOK_TIMEOUT_SECONDS,
|
|
12
|
+
CURSOR_HOOKS_FILE_NAME,
|
|
13
13
|
CURSOR_SESSION_START_EVENT,
|
|
14
14
|
} from './install-constants.mjs';
|
|
15
15
|
import { updateCursorHooksFile, withOwnedCursorHooks, withoutOwnedCursorHooks } from './cursor-native-hooks.mjs';
|
|
@@ -32,8 +32,8 @@ export const DEEPSTACK_SESSION_POWERSHELL_RELATIVE_PATH = 'hooks/session-start.p
|
|
|
32
32
|
export const DEEPSTACK_CURSOR_SESSION_COMMAND_MARKER = 'plugins/deepstack/hooks/session-start.';
|
|
33
33
|
export const DEEPSTACK_SESSION_RUNTIME = 'cursor';
|
|
34
34
|
const WINDOWS_POWERSHELL_FILE_PREFIX = 'powershell -NoProfile -ExecutionPolicy Bypass -File';
|
|
35
|
-
|
|
36
|
-
export const
|
|
35
|
+
/** Script name of the retired first-task hook that earlier installs registered in Cursor. */
|
|
36
|
+
export const RETIRED_CURSOR_FIRST_TASK_HOOK_NAME = 'deepen_first_prompt.py';
|
|
37
37
|
export const DEEPSTACK_CODEX_MODEL_SHEET_NAME = 'deepstack-models.md';
|
|
38
38
|
|
|
39
39
|
export const DEEPSTACK_PLUGIN_SPEC = Object.freeze({
|
|
@@ -62,14 +62,13 @@ function quoteCommandPath(pathValue) {
|
|
|
62
62
|
* Read the Claude, Codex, and Cursor actions one DeepStack install performs.
|
|
63
63
|
*
|
|
64
64
|
* Claude and Codex use the marketplace verbs. Cursor has no plugin command, so
|
|
65
|
-
* its action is a copy into the Cursor home plus
|
|
65
|
+
* its action is a copy into the Cursor home plus one native session hook.
|
|
66
66
|
*
|
|
67
67
|
* @returns {{claude: object, codex: object, cursor: {
|
|
68
68
|
* pluginDirectoryName: string,
|
|
69
69
|
* skillRelativePath: string,
|
|
70
70
|
* sessionScriptRelativePath: string,
|
|
71
71
|
* sessionRuntimeArgument: string,
|
|
72
|
-
* firstTaskHookName: string,
|
|
73
72
|
* }}} The three host actions.
|
|
74
73
|
*/
|
|
75
74
|
export function deepStackInstallPlan() {
|
|
@@ -81,7 +80,6 @@ export function deepStackInstallPlan() {
|
|
|
81
80
|
skillRelativePath: DEEPSTACK_SKILL_RELATIVE_PATH,
|
|
82
81
|
sessionScriptRelativePath: DEEPSTACK_SESSION_SCRIPT_RELATIVE_PATH,
|
|
83
82
|
sessionRuntimeArgument: DEEPSTACK_SESSION_RUNTIME,
|
|
84
|
-
firstTaskHookName: DEEPSTACK_FIRST_TASK_HOOK_NAME,
|
|
85
83
|
},
|
|
86
84
|
};
|
|
87
85
|
}
|
|
@@ -142,33 +140,36 @@ export function resolveDeepStackPluginSource(options = {}) {
|
|
|
142
140
|
/**
|
|
143
141
|
* True for a Cursor hook command the DeepStack install owns.
|
|
144
142
|
*
|
|
143
|
+
* The retired first-task hook counts as owned, so the merge and the removal
|
|
144
|
+
* both strip the entry an earlier install registered.
|
|
145
|
+
*
|
|
145
146
|
* @param {string} forwardSlashedCommand A hook command with forward slashes.
|
|
146
|
-
* @returns {boolean} True for the DeepStack session hook or the first-task hook.
|
|
147
|
+
* @returns {boolean} True for the DeepStack session hook or the retired first-task hook.
|
|
147
148
|
*/
|
|
148
149
|
export function isDeepStackCursorCommand(forwardSlashedCommand) {
|
|
149
150
|
return forwardSlashedCommand.includes(DEEPSTACK_CURSOR_SESSION_COMMAND_MARKER)
|
|
150
|
-
|| forwardSlashedCommand.includes(
|
|
151
|
+
|| forwardSlashedCommand.includes(RETIRED_CURSOR_FIRST_TASK_HOOK_NAME);
|
|
151
152
|
}
|
|
152
153
|
|
|
153
154
|
/**
|
|
154
|
-
* Return the native Cursor configuration with one DeepStack session entry
|
|
155
|
+
* Return the native Cursor configuration with one DeepStack session entry.
|
|
155
156
|
*
|
|
156
|
-
* Earlier installs wrote
|
|
157
|
-
*
|
|
157
|
+
* Earlier installs wrote the session entry as a Claude-style group, which
|
|
158
|
+
* Cursor never ran, and also registered the retired first-task hook. Every
|
|
159
|
+
* owned entry, old or new, is replaced by one native session entry.
|
|
158
160
|
*
|
|
159
161
|
* @param {object} configuration The parsed Cursor hooks file.
|
|
160
|
-
* @param {{sessionCommand: string
|
|
161
|
-
* @returns {object} The configuration with
|
|
162
|
+
* @param {{sessionCommand: string}} commands The session command.
|
|
163
|
+
* @returns {object} The configuration with the session entry present once.
|
|
162
164
|
*/
|
|
163
165
|
export function mergeDeepStackCursorHooks(configuration, commands) {
|
|
164
166
|
return withOwnedCursorHooks(configuration, isDeepStackCursorCommand, {
|
|
165
167
|
[CURSOR_SESSION_START_EVENT]: [{ command: commands.sessionCommand, timeout: CURSOR_HOOK_TIMEOUT_SECONDS }],
|
|
166
|
-
[CURSOR_BEFORE_SUBMIT_PROMPT_EVENT]: [{ command: commands.firstTaskCommand, timeout: CURSOR_HOOK_TIMEOUT_SECONDS }],
|
|
167
168
|
});
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
/**
|
|
171
|
-
* Remove the DeepStack session and first-task
|
|
172
|
+
* Remove the DeepStack session entry and the retired first-task entry from the Cursor hooks file.
|
|
172
173
|
*
|
|
173
174
|
* @param {string} cursorDirectory The Cursor home.
|
|
174
175
|
* @returns {string|null} The hooks.json path when this call changed it.
|
|
@@ -180,6 +181,30 @@ export function removeDeepStackCursorHooks(cursorDirectory) {
|
|
|
180
181
|
);
|
|
181
182
|
}
|
|
182
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Remove the retired first-task entry from the Cursor hooks file.
|
|
186
|
+
*
|
|
187
|
+
* A full install runs this whether or not it installs DeepStack, so an install
|
|
188
|
+
* that opts out of DeepStack still drops the entry an earlier install wrote.
|
|
189
|
+
* A hooks file that never names the retired script is left byte for byte.
|
|
190
|
+
*
|
|
191
|
+
* @param {string} cursorDirectory The Cursor home.
|
|
192
|
+
* @returns {string|null} The hooks.json path when this call changed it.
|
|
193
|
+
*/
|
|
194
|
+
export function removeRetiredCursorFirstTaskHook(cursorDirectory) {
|
|
195
|
+
if (!cursorDirectory) return null;
|
|
196
|
+
const hooksPath = join(cursorDirectory, CURSOR_HOOKS_FILE_NAME);
|
|
197
|
+
if (!existsSync(hooksPath)) return null;
|
|
198
|
+
if (!readFileSync(hooksPath, 'utf8').includes(RETIRED_CURSOR_FIRST_TASK_HOOK_NAME)) return null;
|
|
199
|
+
return updateCursorHooksFile(
|
|
200
|
+
cursorDirectory,
|
|
201
|
+
configuration => withoutOwnedCursorHooks(
|
|
202
|
+
configuration,
|
|
203
|
+
forwardSlashedCommand => forwardSlashedCommand.includes(RETIRED_CURSOR_FIRST_TASK_HOOK_NAME),
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
183
208
|
/**
|
|
184
209
|
* Build the Cursor command for the DeepStack session script.
|
|
185
210
|
*
|
|
@@ -201,13 +226,11 @@ export function deepStackCursorSessionCommand(pluginDirectory, formatPathArgumen
|
|
|
201
226
|
}
|
|
202
227
|
|
|
203
228
|
/**
|
|
204
|
-
* Copy the plugin into the Cursor home and register
|
|
229
|
+
* Copy the plugin into the Cursor home and register its session hook in Cursor's native format.
|
|
205
230
|
*
|
|
206
231
|
* @param {object} options Copy inputs.
|
|
207
232
|
* @param {string} options.cursorDirectory The Cursor home, usually ~/.cursor.
|
|
208
233
|
* @param {string|null} options.pluginSourceDirectory The plugin tree to copy.
|
|
209
|
-
* @param {string} options.firstTaskHookPath Absolute path of deepen_first_prompt.py.
|
|
210
|
-
* @param {string} [options.pythonCommand] The interpreter the installer detected.
|
|
211
234
|
* @param {(pathValue: string) => string} [options.formatPathArgument] The platform path quoting.
|
|
212
235
|
* @param {string} [options.platform] The Node platform name.
|
|
213
236
|
* @returns {{status: string, pluginDirectory: string|null, hooksPath: string|null, warning: string|null}} The outcome.
|
|
@@ -229,10 +252,8 @@ export function installDeepStackCursor(options) {
|
|
|
229
252
|
mkdirSync(options.cursorDirectory, { recursive: true });
|
|
230
253
|
cpSync(options.pluginSourceDirectory, pluginDirectory, { recursive: true, force: true });
|
|
231
254
|
const formatPathArgument = options.formatPathArgument ?? quoteCommandPath;
|
|
232
|
-
const pythonCommand = options.pythonCommand ?? DEFAULT_PYTHON_COMMAND;
|
|
233
255
|
const commands = {
|
|
234
256
|
sessionCommand: deepStackCursorSessionCommand(pluginDirectory, formatPathArgument, options.platform ?? process.platform),
|
|
235
|
-
firstTaskCommand: `${pythonCommand} ${formatPathArgument(options.firstTaskHookPath)}`,
|
|
236
257
|
};
|
|
237
258
|
updateCursorHooksFile(
|
|
238
259
|
options.cursorDirectory,
|
|
@@ -263,8 +284,6 @@ export function seedCodexDeepStackModels(codexHome) {
|
|
|
263
284
|
* @param {string} [options.claudeRoot] The managed Claude root.
|
|
264
285
|
* @param {string} [options.codexHome] The Codex home.
|
|
265
286
|
* @param {string} [options.cursorDirectory] The Cursor home.
|
|
266
|
-
* @param {string} [options.firstTaskHookPath] Absolute path of the hook the first-task command runs.
|
|
267
|
-
* @param {string} [options.pythonCommand] The interpreter the installer detected.
|
|
268
287
|
* @param {(pathValue: string) => string} [options.formatPathArgument] The platform path quoting.
|
|
269
288
|
* @param {string} [options.sourceDirectory] A local plugin tree for the Cursor copy.
|
|
270
289
|
* @param {string[]} [options.argumentList] Command-line arguments, for the opt-out flag.
|
|
@@ -307,8 +326,6 @@ export function installDeepStackForHosts(options = {}, dependencies = {}) {
|
|
|
307
326
|
cursor = installDeepStackCursor({
|
|
308
327
|
cursorDirectory: options.cursorDirectory,
|
|
309
328
|
pluginSourceDirectory: sourceDirectory,
|
|
310
|
-
firstTaskHookPath: options.firstTaskHookPath,
|
|
311
|
-
pythonCommand: options.pythonCommand,
|
|
312
329
|
formatPathArgument: options.formatPathArgument,
|
|
313
330
|
});
|
|
314
331
|
} finally {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
installDeepStackForHosts,
|
|
16
16
|
mergeDeepStackCursorHooks,
|
|
17
17
|
removeDeepStackCursorHooks,
|
|
18
|
+
removeRetiredCursorFirstTaskHook,
|
|
18
19
|
} from './install-deepstack-plugin.mjs';
|
|
19
20
|
import { marketplacePluginsPlan, shouldInstallMarketplacePlugin } from './install-pstack-plugin.mjs';
|
|
20
21
|
|
|
@@ -31,23 +32,23 @@ test('Claude and Codex plans name jl-cmd-projects/DeepStack', () => {
|
|
|
31
32
|
assert.equal(DEEPSTACK_MARKETPLACE_REPOSITORY, 'jl-cmd-projects/DeepStack');
|
|
32
33
|
assert.equal(deepStackCloneUrl(), 'https://github.com/jl-cmd-projects/DeepStack.git');
|
|
33
34
|
assert.equal(plan.cursor.skillRelativePath, 'skills/deepstack-mode/SKILL.md');
|
|
34
|
-
assert.equal(plan.cursor
|
|
35
|
+
assert.equal('firstTaskHookName' in plan.cursor, false);
|
|
35
36
|
assert.throws(() => marketplacePluginsPlan([DEEPSTACK_PLUGIN_SPEC], 'cursor'), /cursor/);
|
|
36
37
|
});
|
|
37
38
|
|
|
38
|
-
test('cursor hooks are one native sessionStart entry and
|
|
39
|
+
test('cursor hooks are one native sessionStart entry and the retired first-task entry is dropped', () => {
|
|
39
40
|
const legacySessionGroup = {
|
|
40
41
|
matcher: 'startup|resume|clear|compact',
|
|
41
42
|
hooks: [{ type: 'command', command: "'C:\\Users\\dev\\.cursor\\plugins\\deepstack\\hooks\\session-start.sh' cursor" }],
|
|
42
43
|
};
|
|
43
44
|
const commands = {
|
|
44
45
|
sessionCommand: "'/cursor/plugins/deepstack/hooks/session-start.sh' cursor",
|
|
45
|
-
firstTaskCommand: "python3 '/claude/hooks/session/deepen_first_prompt.py'",
|
|
46
46
|
};
|
|
47
47
|
const merged = mergeDeepStackCursorHooks({
|
|
48
48
|
hooks: {
|
|
49
49
|
SessionStart: [legacySessionGroup, legacySessionGroup, legacySessionGroup],
|
|
50
50
|
UserPromptSubmit: [{ matcher: '', hooks: [{ type: 'command', command: 'python3 /old/deepen_first_prompt.py', timeout: 10 }] }],
|
|
51
|
+
beforeSubmitPrompt: [{ command: "python3 '/claude/hooks/session/deepen_first_prompt.py'", timeout: 10 }],
|
|
51
52
|
preToolUse: [],
|
|
52
53
|
},
|
|
53
54
|
}, commands);
|
|
@@ -55,7 +56,6 @@ test('cursor hooks are one native sessionStart entry and one native beforeSubmit
|
|
|
55
56
|
version: 1,
|
|
56
57
|
hooks: {
|
|
57
58
|
sessionStart: [{ command: commands.sessionCommand, timeout: 10 }],
|
|
58
|
-
beforeSubmitPrompt: [{ command: commands.firstTaskCommand, timeout: 10 }],
|
|
59
59
|
preToolUse: [],
|
|
60
60
|
},
|
|
61
61
|
});
|
|
@@ -75,7 +75,7 @@ test('the Windows session command runs the PowerShell script, other platforms th
|
|
|
75
75
|
}
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
test('the cursor copy writes the skill and
|
|
78
|
+
test('the cursor copy writes the skill and the native session hook, and removal takes out only that', () => {
|
|
79
79
|
const root = mkdtempSync(join(tmpdir(), 'deepstack-cursor-'));
|
|
80
80
|
try {
|
|
81
81
|
const source = join(root, 'source');
|
|
@@ -89,8 +89,6 @@ test('the cursor copy writes the skill and both native hook commands, and remova
|
|
|
89
89
|
const outcome = installDeepStackCursor({
|
|
90
90
|
cursorDirectory,
|
|
91
91
|
pluginSourceDirectory: source,
|
|
92
|
-
firstTaskHookPath: join(root, '.agents', 'hooks', 'session', 'deepen_first_prompt.py'),
|
|
93
|
-
pythonCommand: '/usr/bin/python3.12',
|
|
94
92
|
platform: 'linux',
|
|
95
93
|
});
|
|
96
94
|
assert.equal(outcome.status, 'installed');
|
|
@@ -102,7 +100,7 @@ test('the cursor copy writes the skill and both native hook commands, and remova
|
|
|
102
100
|
assert.equal(hooks.version, 1);
|
|
103
101
|
assert.equal(hooks.hooks.sessionStart.length, 1);
|
|
104
102
|
assert.match(hooks.hooks.sessionStart[0].command, /session-start\.sh' cursor$/);
|
|
105
|
-
assert.
|
|
103
|
+
assert.equal(hooks.hooks.beforeSubmitPrompt, undefined);
|
|
106
104
|
removeDeepStackCursorHooks(cursorDirectory);
|
|
107
105
|
assert.deepEqual(
|
|
108
106
|
JSON.parse(readFileSync(join(cursorDirectory, 'hooks.json'), 'utf8')),
|
|
@@ -113,11 +111,41 @@ test('the cursor copy writes the skill and both native hook commands, and remova
|
|
|
113
111
|
}
|
|
114
112
|
});
|
|
115
113
|
|
|
114
|
+
test('the retired first-task entry leaves the Cursor hooks file and every other entry stays', () => {
|
|
115
|
+
const root = mkdtempSync(join(tmpdir(), 'deepstack-cursor-'));
|
|
116
|
+
try {
|
|
117
|
+
const hooksPath = join(root, 'hooks.json');
|
|
118
|
+
writeFileSync(hooksPath, JSON.stringify({
|
|
119
|
+
version: 1,
|
|
120
|
+
hooks: {
|
|
121
|
+
beforeSubmitPrompt: [
|
|
122
|
+
{ command: "python3 '/home/dev/.claude/hooks/session/deepen_first_prompt.py'", timeout: 10 },
|
|
123
|
+
{ command: './user-prompt.sh' },
|
|
124
|
+
],
|
|
125
|
+
stop: [{ command: './user-stop.sh' }],
|
|
126
|
+
},
|
|
127
|
+
}));
|
|
128
|
+
assert.equal(removeRetiredCursorFirstTaskHook(root), hooksPath);
|
|
129
|
+
assert.deepEqual(JSON.parse(readFileSync(hooksPath, 'utf8')), {
|
|
130
|
+
version: 1,
|
|
131
|
+
hooks: {
|
|
132
|
+
beforeSubmitPrompt: [{ command: './user-prompt.sh' }],
|
|
133
|
+
stop: [{ command: './user-stop.sh' }],
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
const cleanText = readFileSync(hooksPath, 'utf8');
|
|
137
|
+
assert.equal(removeRetiredCursorFirstTaskHook(root), null);
|
|
138
|
+
assert.equal(readFileSync(hooksPath, 'utf8'), cleanText);
|
|
139
|
+
assert.equal(removeRetiredCursorFirstTaskHook(join(root, 'absent')), null);
|
|
140
|
+
} finally {
|
|
141
|
+
rmSync(root, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
116
145
|
test('a missing plugin tree fails the cursor copy', () => {
|
|
117
146
|
const outcome = installDeepStackCursor({
|
|
118
147
|
cursorDirectory: '/tmp/unused-cursor',
|
|
119
148
|
pluginSourceDirectory: null,
|
|
120
|
-
firstTaskHookPath: '/tmp/deepen_first_prompt.py',
|
|
121
149
|
});
|
|
122
150
|
assert.equal(outcome.status, 'failed');
|
|
123
151
|
assert.match(outcome.warning, /jl-cmd-projects\/DeepStack/);
|
|
@@ -162,7 +190,6 @@ test('the temporary clone is removed once the Cursor copy is written', () => {
|
|
|
162
190
|
claudeRoot: join(root, 'claude'),
|
|
163
191
|
codexHome: join(root, 'codex'),
|
|
164
192
|
cursorDirectory,
|
|
165
|
-
firstTaskHookPath: join(root, 'hooks', 'deepen_first_prompt.py'),
|
|
166
193
|
environment: {},
|
|
167
194
|
cloneParentDirectory,
|
|
168
195
|
}, {
|
|
@@ -190,7 +217,6 @@ test('a failed clone leaves no temporary folder behind', () => {
|
|
|
190
217
|
claudeRoot: join(root, 'claude'),
|
|
191
218
|
codexHome: join(root, 'codex'),
|
|
192
219
|
cursorDirectory: join(root, 'cursor'),
|
|
193
|
-
firstTaskHookPath: join(root, 'hooks', 'deepen_first_prompt.py'),
|
|
194
220
|
environment: {},
|
|
195
221
|
cloneParentDirectory,
|
|
196
222
|
}, { runCommand: absentHostRunner, cloneRepository: () => null });
|
|
@@ -35,17 +35,17 @@ export const USAGE_WRAPUP_PLUGIN_SPEC = Object.freeze({
|
|
|
35
35
|
optOutVariable: 'CDE_INSTALL_USAGE_WRAPUP',
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Plugins this installer used to install and now removes from each Claude home
|
|
40
|
+
* that still holds them, so an update leaves none behind.
|
|
41
|
+
*/
|
|
42
|
+
export const ALL_RETIRED_PLUGIN_SPECS = Object.freeze([
|
|
43
|
+
Object.freeze({
|
|
44
|
+
name: 'deepen-first-prompt',
|
|
45
|
+
marketplaceName: 'claude-dev-env',
|
|
46
|
+
pluginIdentifier: 'deepen-first-prompt@claude-dev-env',
|
|
47
|
+
}),
|
|
48
|
+
]);
|
|
49
49
|
|
|
50
50
|
export const SUBAGENT_MODELS_PLUGIN_SPEC = Object.freeze({
|
|
51
51
|
name: 'subagent-models',
|
|
@@ -341,3 +341,50 @@ export function installMarketplacePlugins(allSpecs, options = {}, dependencies =
|
|
|
341
341
|
const warnings = hostOutcomes.map(outcome => outcome.warning).filter(Boolean);
|
|
342
342
|
return { status, hosts: hostOutcomes, warning: warnings.join(' ') || null };
|
|
343
343
|
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Uninstall each retired plugin a Claude home still holds.
|
|
347
|
+
*
|
|
348
|
+
* A home holds a retired plugin when its plugin cache has a folder for it. For
|
|
349
|
+
* each such plugin the step runs `claude plugin uninstall` and then deletes
|
|
350
|
+
* the cached versions Claude Code marked as orphaned. A home with no cached
|
|
351
|
+
* folder runs no command, so a fresh install and a second update both leave
|
|
352
|
+
* the home as it stands. A missing `claude` command skips the step. A failed
|
|
353
|
+
* uninstall is recorded in `failedRemovals` with its detail, and its cached
|
|
354
|
+
* versions stay on disk.
|
|
355
|
+
*
|
|
356
|
+
* @param {ReadonlyArray<{name: string, marketplaceName: string, pluginIdentifier: string}>} allRetiredSpecs The retired plugins.
|
|
357
|
+
* @param {object} options Removal targets.
|
|
358
|
+
* @param {string|undefined} options.claudeRoot The managed Claude root.
|
|
359
|
+
* @param {Record<string, string|undefined>} [options.environment] The process environment.
|
|
360
|
+
* @param {object} [dependencies] Seams for the command runner.
|
|
361
|
+
* @returns {{removedIdentifiers: string[], removedVersionPaths: string[], failedRemovals: Array<{pluginIdentifier: string, detail: string}>}} What the step removed and what it could not.
|
|
362
|
+
*/
|
|
363
|
+
export function removeRetiredMarketplacePlugins(allRetiredSpecs, options, dependencies = {}) {
|
|
364
|
+
const environment = options.environment ?? process.env;
|
|
365
|
+
const runCommand = dependencies.runCommand ?? runHostCommand;
|
|
366
|
+
const hostDefinition = HOST_DEFINITIONS.claude;
|
|
367
|
+
const executable = environment[hostDefinition.executableVariable] || hostDefinition.executable;
|
|
368
|
+
const removedIdentifiers = [];
|
|
369
|
+
const removedVersionPaths = [];
|
|
370
|
+
const failedRemovals = [];
|
|
371
|
+
if (!options.claudeRoot) return { removedIdentifiers, removedVersionPaths, failedRemovals };
|
|
372
|
+
for (const eachSpec of allRetiredSpecs) {
|
|
373
|
+
const versionsDirectory = hostDefinition.orphanedVersionsDirectory(options.claudeRoot, eachSpec);
|
|
374
|
+
if (!existsSync(versionsDirectory)) continue;
|
|
375
|
+
const outcome = runCommand(
|
|
376
|
+
executable,
|
|
377
|
+
['plugin', hostDefinition.uninstallVerb, eachSpec.pluginIdentifier],
|
|
378
|
+
{ environment: { [hostDefinition.homeVariable]: options.claudeRoot } },
|
|
379
|
+
);
|
|
380
|
+
if (namesAnAbsentCommand(outcome)) break;
|
|
381
|
+
if (outcome.status !== 0 || outcome.error) {
|
|
382
|
+
const detail = singleLineDetail(outcome.stderr) || outcome.error?.message || `exit ${outcome.status}`;
|
|
383
|
+
failedRemovals.push({ pluginIdentifier: eachSpec.pluginIdentifier, detail });
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
removedIdentifiers.push(eachSpec.pluginIdentifier);
|
|
387
|
+
removedVersionPaths.push(...removeOrphanedPluginVersions(versionsDirectory));
|
|
388
|
+
}
|
|
389
|
+
return { removedIdentifiers, removedVersionPaths, failedRemovals };
|
|
390
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import { strict as assert } from 'node:assert';
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import {
|
|
7
7
|
PSTACK_MARKETPLACE_REPOSITORY,
|
|
8
8
|
PSTACK_PLUGIN_HOSTS,
|
|
9
9
|
PSTACK_PLUGIN_IDENTIFIER,
|
|
10
|
-
|
|
10
|
+
ALL_RETIRED_PLUGIN_SPECS,
|
|
11
11
|
PSTACK_PLUGIN_SPEC,
|
|
12
12
|
SUBAGENT_MODELS_PLUGIN_SPEC,
|
|
13
13
|
USAGE_WRAPUP_PLUGIN_SPEC,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
installMarketplacePlugins,
|
|
16
16
|
marketplacePluginsPlan,
|
|
17
17
|
removeOrphanedPluginVersions,
|
|
18
|
+
removeRetiredMarketplacePlugins,
|
|
18
19
|
shouldInstallMarketplacePlugin,
|
|
19
20
|
} from './install-pstack-plugin.mjs';
|
|
20
21
|
|
|
@@ -301,7 +302,6 @@ test('the usage-wrapup opt-out flag and variable turn off only usage-wrapup', ()
|
|
|
301
302
|
for (const [spec, packageFolder] of [
|
|
302
303
|
[USAGE_WRAPUP_PLUGIN_SPEC, './packages/usage-wrapup'],
|
|
303
304
|
[SUBAGENT_MODELS_PLUGIN_SPEC, './packages/subagent-models'],
|
|
304
|
-
[DEEPEN_FIRST_PROMPT_PLUGIN_SPEC, './packages/deepen-first-prompt'],
|
|
305
305
|
]) {
|
|
306
306
|
test(`the repository marketplace lists ${spec.name} from its package folder in this repository`, () => {
|
|
307
307
|
const marketplace = JSON.parse(readFileSync(
|
|
@@ -335,25 +335,58 @@ test('should remove and add the shared marketplace once when installing usage-wr
|
|
|
335
335
|
]);
|
|
336
336
|
});
|
|
337
337
|
|
|
338
|
-
test('
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
'
|
|
342
|
-
);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
338
|
+
test('the repository marketplace no longer lists a retired plugin', () => {
|
|
339
|
+
const marketplace = JSON.parse(readFileSync(
|
|
340
|
+
new URL('../../../.claude-plugin/marketplace.json', import.meta.url),
|
|
341
|
+
'utf8',
|
|
342
|
+
));
|
|
343
|
+
const allListedNames = marketplace.plugins.map(plugin => plugin.name);
|
|
344
|
+
for (const eachRetiredSpec of ALL_RETIRED_PLUGIN_SPECS) {
|
|
345
|
+
assert.equal(allListedNames.includes(eachRetiredSpec.name), false, `${eachRetiredSpec.name} is retired`);
|
|
346
|
+
}
|
|
347
|
+
assert.deepEqual(ALL_RETIRED_PLUGIN_SPECS.map(spec => spec.pluginIdentifier), ['deepen-first-prompt@claude-dev-env']);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('a Claude home that holds a retired plugin gets it uninstalled and its orphaned versions deleted', (context) => {
|
|
351
|
+
const claudeRoot = mkdtempSync(join(tmpdir(), 'retired-plugin-'));
|
|
352
|
+
context.after(() => rmSync(claudeRoot, { recursive: true, force: true }));
|
|
353
|
+
const versionsDirectory = join(claudeRoot, 'plugins', 'cache', 'claude-dev-env', 'deepen-first-prompt');
|
|
354
|
+
const orphanedVersion = join(versionsDirectory, '0.1.0');
|
|
355
|
+
mkdirSync(orphanedVersion, { recursive: true });
|
|
356
|
+
const runner = recordingRunner(() => {
|
|
357
|
+
writeFileSync(join(orphanedVersion, '.orphaned_at'), '1');
|
|
358
|
+
return { status: 0, stderr: '' };
|
|
359
|
+
});
|
|
360
|
+
const outcome = removeRetiredMarketplacePlugins(ALL_RETIRED_PLUGIN_SPECS, { claudeRoot, environment: {} }, runner);
|
|
361
|
+
assert.deepEqual(runner.calls.map(call => [call.executable, ...call.commandArguments]), [
|
|
362
|
+
['claude', 'plugin', 'uninstall', 'deepen-first-prompt@claude-dev-env'],
|
|
347
363
|
]);
|
|
348
|
-
assert.
|
|
349
|
-
assert.
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
);
|
|
364
|
+
assert.deepEqual(runner.calls[0].environment, { CLAUDE_CONFIG_DIR: claudeRoot });
|
|
365
|
+
assert.deepEqual(outcome.removedIdentifiers, ['deepen-first-prompt@claude-dev-env']);
|
|
366
|
+
assert.equal(existsSync(orphanedVersion), false);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('a Claude home with no retired plugin runs no command', (context) => {
|
|
370
|
+
const claudeRoot = mkdtempSync(join(tmpdir(), 'retired-plugin-'));
|
|
371
|
+
context.after(() => rmSync(claudeRoot, { recursive: true, force: true }));
|
|
372
|
+
const runner = recordingRunner();
|
|
373
|
+
const outcome = removeRetiredMarketplacePlugins(ALL_RETIRED_PLUGIN_SPECS, { claudeRoot, environment: {} }, runner);
|
|
374
|
+
assert.deepEqual(runner.calls, []);
|
|
375
|
+
assert.deepEqual(outcome, { removedIdentifiers: [], removedVersionPaths: [], failedRemovals: [] });
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test('a failed retired plugin uninstall is reported as failed and keeps its cached versions', (context) => {
|
|
379
|
+
const claudeRoot = mkdtempSync(join(tmpdir(), 'retired-plugin-'));
|
|
380
|
+
context.after(() => rmSync(claudeRoot, { recursive: true, force: true }));
|
|
381
|
+
const cachedVersion = join(claudeRoot, 'plugins', 'cache', 'claude-dev-env', 'deepen-first-prompt', '0.1.0');
|
|
382
|
+
mkdirSync(cachedVersion, { recursive: true });
|
|
383
|
+
const runner = recordingRunner(() => ({ status: 1, stderr: 'Plugin not found\n' }));
|
|
384
|
+
const outcome = removeRetiredMarketplacePlugins(ALL_RETIRED_PLUGIN_SPECS, { claudeRoot, environment: {} }, runner);
|
|
385
|
+
assert.deepEqual(outcome.removedIdentifiers, []);
|
|
386
|
+
assert.deepEqual(outcome.failedRemovals, [
|
|
387
|
+
{ pluginIdentifier: 'deepen-first-prompt@claude-dev-env', detail: 'Plugin not found' },
|
|
388
|
+
]);
|
|
389
|
+
assert.equal(existsSync(cachedVersion), true);
|
|
357
390
|
});
|
|
358
391
|
|
|
359
392
|
test('should refuse one plan for plugins from two marketplaces', () => {
|
|
@@ -56,7 +56,6 @@ function runInstaller(homeDirectory, extraArguments, environmentOverrides = {})
|
|
|
56
56
|
CDE_INSTALL_DEEPSTACK: '0',
|
|
57
57
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
58
58
|
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
59
|
-
CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
|
|
60
59
|
HOME: homeDirectory,
|
|
61
60
|
USERPROFILE: homeDirectory,
|
|
62
61
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
@@ -62,7 +62,6 @@ function runInstaller(homeDirectory, extraArguments) {
|
|
|
62
62
|
CDE_INSTALL_DEEPSTACK: '0',
|
|
63
63
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
64
64
|
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
65
|
-
CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
|
|
66
65
|
HOME: homeDirectory,
|
|
67
66
|
USERPROFILE: homeDirectory,
|
|
68
67
|
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
@@ -96,7 +95,6 @@ function scanTextWithProductionPiiScanner(scannedText) {
|
|
|
96
95
|
CDE_INSTALL_DEEPSTACK: '0',
|
|
97
96
|
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
98
97
|
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
99
|
-
CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
|
|
100
98
|
PYTHONPATH: BLOCKING_HOOKS_DIRECTORY,
|
|
101
99
|
CLAUDE_LOCAL_IDENTITY_PATH: join(
|
|
102
100
|
temporaryDirectory,
|