claude-dev-env 8.88.1 → 8.88.3
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/bin/codex-trimmed-sol.mjs +100 -3
- package/bin/codex-trimmed-sol.test.mjs +155 -2
- package/bin/folder-lockers.mjs +142 -0
- package/bin/folder-lockers.test.mjs +55 -0
- package/bin/global-install-shadow.mjs +75 -0
- package/bin/global-install-shadow.test.mjs +56 -0
- package/bin/install-deepstack-plugin.mjs +45 -16
- package/bin/install-deepstack-plugin.test.mjs +65 -1
- package/bin/install-pstack-plugin.mjs +20 -4
- package/bin/install-pstack-plugin.test.mjs +53 -0
- package/bin/install.mjs +79 -6
- package/bin/install.test.mjs +93 -0
- package/bin/profile-stop-gate.mjs +193 -0
- package/bin/profile-stop-gate.test.mjs +124 -0
- package/docs/agents-guidance/asks.md +2 -1
- package/docs/codex-accounts.md +14 -2
- package/docs/decisions/deepstack-gate.md +6 -2
- package/hooks/features/session-context.md +1 -1
- package/hooks/hooks_constants/deepen_default_rewrite_constants.py +39 -0
- package/hooks/session/deepen_default_rewrite.py +128 -0
- package/hooks/session/deepen_first_prompt.py +3 -15
- package/hooks/session/test_deepen_default_rewrite.py +90 -0
- package/hooks/session/test_deepen_first_prompt.py +12 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import {
|
|
@@ -23,6 +23,9 @@ export const DEEPSTACK_PLUGIN_OPT_OUT_FLAG = '--no-deepstack';
|
|
|
23
23
|
export const DEEPSTACK_PLUGIN_OPT_OUT_VARIABLE = 'CDE_INSTALL_DEEPSTACK';
|
|
24
24
|
export const DEEPSTACK_SOURCE_VARIABLE = 'CDE_DEEPSTACK_SOURCE';
|
|
25
25
|
export const DEEPSTACK_CURSOR_DIRECTORY_NAME = 'plugins/deepstack';
|
|
26
|
+
/** Name prefix of the temporary DeepStack clone a full install makes when no local tree exists. */
|
|
27
|
+
export const DEEPSTACK_CLONE_PREFIX = 'deepstack-';
|
|
28
|
+
const CLONE_REMOVAL_RETRIES = 3;
|
|
26
29
|
export const DEEPSTACK_SKILL_RELATIVE_PATH = 'skills/deepstack-mode/SKILL.md';
|
|
27
30
|
export const DEEPSTACK_SESSION_SCRIPT_RELATIVE_PATH = 'hooks/session-start.sh';
|
|
28
31
|
export const DEEPSTACK_SESSION_POWERSHELL_RELATIVE_PATH = 'hooks/session-start.ps1';
|
|
@@ -266,7 +269,9 @@ export function seedCodexDeepStackModels(codexHome) {
|
|
|
266
269
|
* @param {string} [options.sourceDirectory] A local plugin tree for the Cursor copy.
|
|
267
270
|
* @param {string[]} [options.argumentList] Command-line arguments, for the opt-out flag.
|
|
268
271
|
* @param {Record<string, string|undefined>} [options.environment] The process environment.
|
|
269
|
-
* @param {
|
|
272
|
+
* @param {boolean} [options.allowClone] False stops the clone fallback.
|
|
273
|
+
* @param {string} [options.cloneParentDirectory] Where the temporary clone goes. Defaults to the system temp folder.
|
|
274
|
+
* @param {object} [dependencies] Seams for the marketplace command runner and the clone.
|
|
270
275
|
* @returns {{status: string, hosts: object[], cursor: object, warning: string|null}} The outcome.
|
|
271
276
|
*/
|
|
272
277
|
export function installDeepStackForHosts(options = {}, dependencies = {}) {
|
|
@@ -290,16 +295,25 @@ export function installDeepStackForHosts(options = {}, dependencies = {}) {
|
|
|
290
295
|
environment,
|
|
291
296
|
sourceDirectory: options.sourceDirectory,
|
|
292
297
|
});
|
|
293
|
-
|
|
294
|
-
|
|
298
|
+
const cloneDirectory = !sourceDirectory && options.allowClone !== false
|
|
299
|
+
? mkdtempSync(join(options.cloneParentDirectory ?? tmpdir(), DEEPSTACK_CLONE_PREFIX))
|
|
300
|
+
: null;
|
|
301
|
+
let cursor;
|
|
302
|
+
try {
|
|
303
|
+
if (cloneDirectory) {
|
|
304
|
+
const cloneRepository = dependencies.cloneRepository ?? cloneDeepStackRepository;
|
|
305
|
+
sourceDirectory = cloneRepository(cloneDirectory);
|
|
306
|
+
}
|
|
307
|
+
cursor = installDeepStackCursor({
|
|
308
|
+
cursorDirectory: options.cursorDirectory,
|
|
309
|
+
pluginSourceDirectory: sourceDirectory,
|
|
310
|
+
firstTaskHookPath: options.firstTaskHookPath,
|
|
311
|
+
pythonCommand: options.pythonCommand,
|
|
312
|
+
formatPathArgument: options.formatPathArgument,
|
|
313
|
+
});
|
|
314
|
+
} finally {
|
|
315
|
+
if (cloneDirectory) removeDeepStackClone(cloneDirectory);
|
|
295
316
|
}
|
|
296
|
-
const cursor = installDeepStackCursor({
|
|
297
|
-
cursorDirectory: options.cursorDirectory,
|
|
298
|
-
pluginSourceDirectory: sourceDirectory,
|
|
299
|
-
firstTaskHookPath: options.firstTaskHookPath,
|
|
300
|
-
pythonCommand: options.pythonCommand,
|
|
301
|
-
formatPathArgument: options.formatPathArgument,
|
|
302
|
-
});
|
|
303
317
|
if (marketplace.hosts.some(host => host.host === 'codex' && host.status === 'installed') && options.codexHome) {
|
|
304
318
|
seedCodexDeepStackModels(options.codexHome);
|
|
305
319
|
}
|
|
@@ -314,14 +328,20 @@ export function installDeepStackForHosts(options = {}, dependencies = {}) {
|
|
|
314
328
|
}
|
|
315
329
|
|
|
316
330
|
/**
|
|
317
|
-
*
|
|
331
|
+
* Remove the temporary DeepStack clone once the Cursor copy is written.
|
|
318
332
|
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
333
|
+
* Nothing reads the clone after the install: the Cursor hooks point at the
|
|
334
|
+
* copy under the Cursor home, and Claude and Codex read their own marketplace
|
|
335
|
+
* caches. Git writes its pack files read-only, which the forced removal
|
|
336
|
+
* handles on Windows too.
|
|
321
337
|
*
|
|
322
|
-
* @param {string}
|
|
323
|
-
* @returns {
|
|
338
|
+
* @param {string} cloneDirectory The directory the clone was made in.
|
|
339
|
+
* @returns {void}
|
|
324
340
|
*/
|
|
341
|
+
export function removeDeepStackClone(cloneDirectory) {
|
|
342
|
+
rmSync(cloneDirectory, { recursive: true, force: true, maxRetries: CLONE_REMOVAL_RETRIES });
|
|
343
|
+
}
|
|
344
|
+
|
|
325
345
|
/**
|
|
326
346
|
* Return the git clone URL for the DeepStack marketplace repository.
|
|
327
347
|
*
|
|
@@ -331,6 +351,15 @@ export function deepStackCloneUrl() {
|
|
|
331
351
|
return ['https:', '', 'github.com', `${DEEPSTACK_MARKETPLACE_REPOSITORY}.git`].join('/');
|
|
332
352
|
}
|
|
333
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Clone jl-cmd-projects/DeepStack when no local tree is available.
|
|
356
|
+
*
|
|
357
|
+
* The installer calls this only from a full install that has no source
|
|
358
|
+
* directory and no marketplace cache. Tests pass a source directory instead.
|
|
359
|
+
*
|
|
360
|
+
* @param {string} destination An empty directory for the clone.
|
|
361
|
+
* @returns {string|null} The plugin directory inside the clone.
|
|
362
|
+
*/
|
|
334
363
|
export function cloneDeepStackRepository(destination) {
|
|
335
364
|
const outcome = spawnSync('git', [
|
|
336
365
|
'clone',
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import { strict as assert } from 'node:assert';
|
|
3
|
-
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, existsSync, mkdtempSync, mkdirSync, readdirSync, 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
|
DEEPSTACK_MARKETPLACE_REPOSITORY,
|
|
8
|
+
DEEPSTACK_SKILL_RELATIVE_PATH,
|
|
8
9
|
DEEPSTACK_PLUGIN_IDENTIFIER,
|
|
9
10
|
DEEPSTACK_PLUGIN_SPEC,
|
|
10
11
|
deepStackCloneUrl,
|
|
@@ -136,3 +137,66 @@ test('--no-deepstack and CDE_INSTALL_DEEPSTACK=0 skip the install', () => {
|
|
|
136
137
|
assert.equal(skipped.status, 'skipped');
|
|
137
138
|
assert.equal(skipped.cursor.status, 'skipped');
|
|
138
139
|
});
|
|
140
|
+
|
|
141
|
+
const absentHostRunner = () => ({ status: null, stderr: '', error: { code: 'ENOENT' } });
|
|
142
|
+
|
|
143
|
+
function writeClonedPlugin(cloneDirectory) {
|
|
144
|
+
const pluginDirectory = join(cloneDirectory, 'plugins', 'deepstack');
|
|
145
|
+
mkdirSync(join(pluginDirectory, 'skills', 'deepstack-mode'), { recursive: true });
|
|
146
|
+
writeFileSync(join(pluginDirectory, DEEPSTACK_SKILL_RELATIVE_PATH), '# DeepStack\n');
|
|
147
|
+
mkdirSync(join(cloneDirectory, '.git', 'objects', 'pack'), { recursive: true });
|
|
148
|
+
const packPath = join(cloneDirectory, '.git', 'objects', 'pack', 'pack-1.pack');
|
|
149
|
+
writeFileSync(packPath, 'pack');
|
|
150
|
+
chmodSync(packPath, 0o444);
|
|
151
|
+
return pluginDirectory;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
test('the temporary clone is removed once the Cursor copy is written', () => {
|
|
155
|
+
const root = mkdtempSync(join(tmpdir(), 'deepstack-clone-'));
|
|
156
|
+
try {
|
|
157
|
+
const cloneParentDirectory = join(root, 'temp');
|
|
158
|
+
mkdirSync(cloneParentDirectory);
|
|
159
|
+
const cursorDirectory = join(root, 'cursor');
|
|
160
|
+
let clonedInto = null;
|
|
161
|
+
const outcome = installDeepStackForHosts({
|
|
162
|
+
claudeRoot: join(root, 'claude'),
|
|
163
|
+
codexHome: join(root, 'codex'),
|
|
164
|
+
cursorDirectory,
|
|
165
|
+
firstTaskHookPath: join(root, 'hooks', 'deepen_first_prompt.py'),
|
|
166
|
+
environment: {},
|
|
167
|
+
cloneParentDirectory,
|
|
168
|
+
}, {
|
|
169
|
+
runCommand: absentHostRunner,
|
|
170
|
+
cloneRepository: cloneDirectory => {
|
|
171
|
+
clonedInto = cloneDirectory;
|
|
172
|
+
return writeClonedPlugin(cloneDirectory);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
assert.equal(outcome.cursor.status, 'installed');
|
|
176
|
+
assert.ok(clonedInto.startsWith(cloneParentDirectory));
|
|
177
|
+
assert.ok(existsSync(join(cursorDirectory, 'plugins', 'deepstack', DEEPSTACK_SKILL_RELATIVE_PATH)));
|
|
178
|
+
assert.deepEqual(readdirSync(cloneParentDirectory), []);
|
|
179
|
+
} finally {
|
|
180
|
+
rmSync(root, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('a failed clone leaves no temporary folder behind', () => {
|
|
185
|
+
const root = mkdtempSync(join(tmpdir(), 'deepstack-clone-'));
|
|
186
|
+
try {
|
|
187
|
+
const cloneParentDirectory = join(root, 'temp');
|
|
188
|
+
mkdirSync(cloneParentDirectory);
|
|
189
|
+
const outcome = installDeepStackForHosts({
|
|
190
|
+
claudeRoot: join(root, 'claude'),
|
|
191
|
+
codexHome: join(root, 'codex'),
|
|
192
|
+
cursorDirectory: join(root, 'cursor'),
|
|
193
|
+
firstTaskHookPath: join(root, 'hooks', 'deepen_first_prompt.py'),
|
|
194
|
+
environment: {},
|
|
195
|
+
cloneParentDirectory,
|
|
196
|
+
}, { runCommand: absentHostRunner, cloneRepository: () => null });
|
|
197
|
+
assert.equal(outcome.cursor.status, 'failed');
|
|
198
|
+
assert.deepEqual(readdirSync(cloneParentDirectory), []);
|
|
199
|
+
} finally {
|
|
200
|
+
rmSync(root, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
2
|
import { existsSync, readdirSync, rmSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { findFolderLockers, lockedFolderAdvice, namesALockedFolder } from './folder-lockers.mjs';
|
|
4
5
|
|
|
5
6
|
export const PSTACK_MARKETPLACE_REPOSITORY = 'jl-cmd/pstack-claude';
|
|
6
7
|
export const PSTACK_MARKETPLACE_NAME = 'pstack-claude';
|
|
@@ -248,7 +249,17 @@ function absentHostOutcome(allSpecs, host, executable) {
|
|
|
248
249
|
};
|
|
249
250
|
}
|
|
250
251
|
|
|
251
|
-
function
|
|
252
|
+
function lockedFolderDetail(allSpecs, host, homeDirectory, errorText, findLockers) {
|
|
253
|
+
const hostDefinition = HOST_DEFINITIONS[host];
|
|
254
|
+
if (!namesALockedFolder(errorText) || !hostDefinition.orphanedVersionsDirectory || !homeDirectory) return '';
|
|
255
|
+
return allSpecs.map(eachSpec => {
|
|
256
|
+
const pluginCacheDirectory = hostDefinition.orphanedVersionsDirectory(homeDirectory, eachSpec);
|
|
257
|
+
const allLockers = findLockers(pluginCacheDirectory);
|
|
258
|
+
return allLockers === null ? '' : ` ${lockedFolderAdvice(pluginCacheDirectory, allLockers)}`;
|
|
259
|
+
}).join('');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function installForHost(allSpecs, host, homeDirectory, environment, runCommand, findLockers) {
|
|
252
263
|
const plan = marketplacePluginsPlan(allSpecs, host);
|
|
253
264
|
const executable = environment[plan.executableVariable] || plan.executable;
|
|
254
265
|
const commandEnvironment = { [plan.homeVariable]: homeDirectory };
|
|
@@ -265,11 +276,12 @@ function installForHost(allSpecs, host, homeDirectory, environment, runCommand)
|
|
|
265
276
|
if (namesAnAbsentCommand(outcome)) return absentHostOutcome(allSpecs, host, executable);
|
|
266
277
|
if (outcome.status !== 0 || outcome.error) {
|
|
267
278
|
const detail = singleLineDetail(outcome.stderr) || outcome.error?.message || `exit ${outcome.status}`;
|
|
279
|
+
const lockAdvice = lockedFolderDetail(allSpecs, host, homeDirectory, detail, findLockers);
|
|
268
280
|
return {
|
|
269
281
|
host,
|
|
270
282
|
executable,
|
|
271
283
|
status: 'failed',
|
|
272
|
-
warning: `${executable} ${commandArguments.join(' ')} failed: ${detail}`,
|
|
284
|
+
warning: `${executable} ${commandArguments.join(' ')} failed: ${detail}${lockAdvice}`,
|
|
273
285
|
};
|
|
274
286
|
}
|
|
275
287
|
}
|
|
@@ -294,7 +306,9 @@ function installForHost(allSpecs, host, homeDirectory, environment, runCommand)
|
|
|
294
306
|
* and skills this run already wrote still reach their durable places. An
|
|
295
307
|
* absent tool arrives as a spawn `ENOENT` on other platforms and as
|
|
296
308
|
* `cmd.exe`'s command-not-found exit code on Windows, and both read as
|
|
297
|
-
* skipped.
|
|
309
|
+
* skipped. When a host cannot replace a plugin folder because something holds
|
|
310
|
+
* it, the warning names each process that holds it, so the user knows what to
|
|
311
|
+
* close before running the install again.
|
|
298
312
|
*
|
|
299
313
|
* @param {ReadonlyArray<typeof PSTACK_PLUGIN_SPEC>} allSpecs The plugins to install, from one marketplace.
|
|
300
314
|
* @param {object} [options] Install targets.
|
|
@@ -302,12 +316,13 @@ function installForHost(allSpecs, host, homeDirectory, environment, runCommand)
|
|
|
302
316
|
* @param {string} [options.codexHome] The Codex home to install into.
|
|
303
317
|
* @param {string[]} [options.hosts] The hosts to install. Defaults to the hosts every spec names.
|
|
304
318
|
* @param {Record<string, string|undefined>} [options.environment] The process environment.
|
|
305
|
-
* @param {object} [dependencies] Seams for the command runner.
|
|
319
|
+
* @param {object} [dependencies] Seams for the command runner and the folder locker search.
|
|
306
320
|
* @returns {{status: string, hosts: object[], warning: string|null}} The outcome per host.
|
|
307
321
|
*/
|
|
308
322
|
export function installMarketplacePlugins(allSpecs, options = {}, dependencies = {}) {
|
|
309
323
|
const environment = options.environment ?? process.env;
|
|
310
324
|
const runCommand = dependencies.runCommand ?? runHostCommand;
|
|
325
|
+
const findLockers = dependencies.findLockers ?? findFolderLockers;
|
|
311
326
|
const homeDirectories = { claude: options.claudeRoot, codex: options.codexHome };
|
|
312
327
|
const hosts = options.hosts ?? allSpecs[0].hosts.filter(
|
|
313
328
|
eachHost => allSpecs.every(eachSpec => eachSpec.hosts.includes(eachHost)),
|
|
@@ -318,6 +333,7 @@ export function installMarketplacePlugins(allSpecs, options = {}, dependencies =
|
|
|
318
333
|
homeDirectories[host],
|
|
319
334
|
environment,
|
|
320
335
|
runCommand,
|
|
336
|
+
findLockers,
|
|
321
337
|
));
|
|
322
338
|
const failedCount = hostOutcomes.filter(outcome => outcome.status === 'failed').length;
|
|
323
339
|
const installedCount = hostOutcomes.filter(outcome => outcome.status === 'installed').length;
|
|
@@ -392,3 +392,56 @@ test('a clean install deletes the orphaned pstack versions and keeps the install
|
|
|
392
392
|
assert.equal(existsSync(join(versionsDirectory, '0.9.64')), true);
|
|
393
393
|
assert.deepEqual(removeOrphanedPluginVersions(versionsDirectory), []);
|
|
394
394
|
});
|
|
395
|
+
|
|
396
|
+
test('a plugin folder held by another program names the process to close', () => {
|
|
397
|
+
const lockedError = 'Failed to install plugin "pstack@pstack-claude": The installed copy of this plugin version'
|
|
398
|
+
+ ' could not be replaced: it is in use by another program (EPERM).';
|
|
399
|
+
const searchedFolders = [];
|
|
400
|
+
const runner = recordingRunner((executable, commandArguments) => (
|
|
401
|
+
executable === 'claude' && commandArguments[1] === 'install'
|
|
402
|
+
? { status: 1, stderr: lockedError }
|
|
403
|
+
: { status: 0, stderr: '' }));
|
|
404
|
+
const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, {
|
|
405
|
+
runCommand: runner.runCommand,
|
|
406
|
+
findLockers: folderPath => {
|
|
407
|
+
searchedFolders.push(folderPath);
|
|
408
|
+
return [{ pid: 19056, name: 'Cursor.exe', reason: 'command-line' }];
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
const claude = outcome.hosts.find(host => host.host === 'claude');
|
|
412
|
+
assert.equal(claude.status, 'failed');
|
|
413
|
+
assert.deepEqual(searchedFolders, [join('/managed/.claude', 'plugins', 'cache', 'pstack-claude', 'pstack')]);
|
|
414
|
+
assert.match(claude.warning, /Close Cursor\.exe \(PID 19056, was started on a path inside it\)/);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('a failure that is not a held folder runs no locker search', () => {
|
|
418
|
+
const runner = recordingRunner((executable, commandArguments) => (
|
|
419
|
+
executable === 'claude' && commandArguments[2] === 'add'
|
|
420
|
+
? { status: 1, stderr: 'owner: Invalid input: expected object, received undefined' }
|
|
421
|
+
: { status: 0, stderr: '' }));
|
|
422
|
+
let searched = false;
|
|
423
|
+
installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, {
|
|
424
|
+
runCommand: runner.runCommand,
|
|
425
|
+
findLockers: () => {
|
|
426
|
+
searched = true;
|
|
427
|
+
return [];
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
assert.equal(searched, false);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test('a held folder that was not searched adds no claim about what holds it', () => {
|
|
434
|
+
const lockedError = 'Failed to install plugin "pstack@pstack-claude": EPERM: operation not permitted';
|
|
435
|
+
const runner = recordingRunner((executable, commandArguments) => (
|
|
436
|
+
executable === 'claude' && commandArguments[1] === 'install'
|
|
437
|
+
? { status: 1, stderr: lockedError }
|
|
438
|
+
: { status: 0, stderr: '' }));
|
|
439
|
+
const outcome = installMarketplacePlugins([PSTACK_PLUGIN_SPEC], ROOTS, {
|
|
440
|
+
runCommand: runner.runCommand,
|
|
441
|
+
findLockers: () => null,
|
|
442
|
+
});
|
|
443
|
+
const claude = outcome.hosts.find(host => host.host === 'claude');
|
|
444
|
+
assert.equal(claude.status, 'failed');
|
|
445
|
+
assert.match(claude.warning, /EPERM: operation not permitted$/);
|
|
446
|
+
assert.doesNotMatch(claude.warning, /likely holder|Close /);
|
|
447
|
+
});
|
package/bin/install.mjs
CHANGED
|
@@ -72,7 +72,9 @@ import {
|
|
|
72
72
|
writeCursorAgentsGuidanceHooks,
|
|
73
73
|
} from './cursor-agents-guidance.mjs';
|
|
74
74
|
import {
|
|
75
|
+
codexSolPromptPath,
|
|
75
76
|
codexSolSettingSnapshotPaths,
|
|
77
|
+
installCodexSolPrompt,
|
|
76
78
|
removeCodexSolSetting,
|
|
77
79
|
SOL_PROMPT_DIRECTORY_NAME,
|
|
78
80
|
SOL_PROMPT_FILE_NAME,
|
|
@@ -126,6 +128,12 @@ import {
|
|
|
126
128
|
resolveFaultPhaseFromEnvironment,
|
|
127
129
|
runWithInstallTransaction,
|
|
128
130
|
} from './install-transaction.mjs';
|
|
131
|
+
import { findShadowingGlobalInstall, shadowingGlobalAdvice } from './global-install-shadow.mjs';
|
|
132
|
+
import {
|
|
133
|
+
addOpenTasksGateToProfiles,
|
|
134
|
+
otherProfileSettingsPaths,
|
|
135
|
+
removeOpenTasksGateFromProfiles,
|
|
136
|
+
} from './profile-stop-gate.mjs';
|
|
129
137
|
import { pythonExecutableProbeCommand } from './python-probe.mjs';
|
|
130
138
|
|
|
131
139
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -443,16 +451,28 @@ export function installPstackPluginForHosts() {
|
|
|
443
451
|
});
|
|
444
452
|
}
|
|
445
453
|
|
|
454
|
+
/** Side steps that need the user, repeated at the end of the run so none scrolls past unseen. */
|
|
455
|
+
const allActionNeededLines = [];
|
|
456
|
+
|
|
446
457
|
function printPluginHostOutcomes(allSpecs, pluginOutcome) {
|
|
447
458
|
for (const eachSpec of allSpecs) {
|
|
448
459
|
for (const eachHost of pluginOutcome.hosts) {
|
|
449
|
-
|
|
450
|
-
?
|
|
451
|
-
:
|
|
460
|
+
const outcomeLine = eachHost.warning
|
|
461
|
+
? `${eachSpec.label} (${eachHost.host}): ${eachHost.status} — ${eachHost.warning}`
|
|
462
|
+
: `${eachSpec.label} (${eachHost.host}): ${eachHost.status}`;
|
|
463
|
+
console.log(` ${outcomeLine}`);
|
|
464
|
+
if (eachHost.status === 'failed') allActionNeededLines.push(outcomeLine);
|
|
452
465
|
}
|
|
453
466
|
}
|
|
454
467
|
}
|
|
455
468
|
|
|
469
|
+
function printActionNeeded() {
|
|
470
|
+
if (allActionNeededLines.length === 0) return;
|
|
471
|
+
console.warn(`Action needed (${allActionNeededLines.length}):`);
|
|
472
|
+
for (const eachLine of allActionNeededLines) console.warn(` - ${eachLine}`);
|
|
473
|
+
console.warn('');
|
|
474
|
+
}
|
|
475
|
+
|
|
456
476
|
function seedSubagentModelPolicy() {
|
|
457
477
|
const policyDirectory = join(AGENTS_HOME, CURSOR_RULES_DIRECTORY_NAME);
|
|
458
478
|
const policyPath = join(policyDirectory, SUBAGENT_MODEL_POLICY_FILE_NAME);
|
|
@@ -2680,17 +2700,33 @@ function install(selectedGroups, options = {}) {
|
|
|
2680
2700
|
executeInstallPlan(plan);
|
|
2681
2701
|
}
|
|
2682
2702
|
|
|
2703
|
+
/**
|
|
2704
|
+
* List the profile settings paths that get the open-tasks Stop gate.
|
|
2705
|
+
*
|
|
2706
|
+
* Only an install into the main ~/.claude does this, because the gate entry
|
|
2707
|
+
* names the main home's hook script.
|
|
2708
|
+
*
|
|
2709
|
+
* @param {string} mainSettingsPath
|
|
2710
|
+
* @returns {string[]}
|
|
2711
|
+
*/
|
|
2712
|
+
function profileSettingsPathsForStopGate(mainSettingsPath) {
|
|
2713
|
+
if (!pathsAreEquivalent(CLAUDE_HOME, join(homedir(), '.claude'))) return [];
|
|
2714
|
+
return otherProfileSettingsPaths({ homeDirectory: homedir(), environment: process.env, mainSettingsPath });
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2683
2717
|
function executeInstallPlan(plan) {
|
|
2684
2718
|
const faultPhase = resolveFaultPhaseFromEnvironment(process.env);
|
|
2685
2719
|
const snapshot = capturePriorInstallSnapshot({
|
|
2686
2720
|
managedRoot: CLAUDE_HOME,
|
|
2687
2721
|
manifestFilePath: MANIFEST_FILE,
|
|
2688
2722
|
settingsPath: plan.settingsPath,
|
|
2689
|
-
additionalSettingsPaths: [CODEX_HOOKS_CONFIGURATION_PATH],
|
|
2723
|
+
additionalSettingsPaths: [CODEX_HOOKS_CONFIGURATION_PATH, ...profileSettingsPathsForStopGate(plan.settingsPath)],
|
|
2690
2724
|
priorManifestFiles: [
|
|
2691
2725
|
...(plan.priorManifest.files || []),
|
|
2692
2726
|
join(INSTALL_ROOT_RESOLUTION.codexHomeDirectory, 'AGENTS.md'),
|
|
2693
2727
|
...codexSolSettingSnapshotPaths(INSTALL_ROOT_RESOLUTION.codexHomeDirectory),
|
|
2728
|
+
codexSolPromptPath(INSTALL_ROOT_RESOLUTION.codexHomeDirectory),
|
|
2729
|
+
join(INSTALL_ROOT_RESOLUTION.codexHomeDirectory, SOL_PROMPT_FILE_NAME),
|
|
2694
2730
|
],
|
|
2695
2731
|
journalParentDirectory: join(CLAUDE_HOME, TRANSACTION_JOURNAL_DIRECTORY_NAME),
|
|
2696
2732
|
});
|
|
@@ -2737,6 +2773,16 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2737
2773
|
const groupLabel = selectedGroups ? `groups: ${selectedGroups.join(', ')}` : 'all';
|
|
2738
2774
|
console.log(`\nInstalling ${PACKAGE_NAME} (${groupLabel})...\n`);
|
|
2739
2775
|
console.log(` Python: ${pythonCommand}`);
|
|
2776
|
+
const shadowingGlobal = findShadowingGlobalInstall({
|
|
2777
|
+
packageName: PACKAGE_NAME,
|
|
2778
|
+
packageRoot: PACKAGE_ROOT,
|
|
2779
|
+
packageVersion: PACKAGE_VERSION,
|
|
2780
|
+
});
|
|
2781
|
+
if (shadowingGlobal) {
|
|
2782
|
+
const shadowAdvice = shadowingGlobalAdvice(PACKAGE_NAME, shadowingGlobal, PACKAGE_VERSION);
|
|
2783
|
+
console.warn(` Warning: ${shadowAdvice}`);
|
|
2784
|
+
allActionNeededLines.push(shadowAdvice);
|
|
2785
|
+
}
|
|
2740
2786
|
mkdirSync(CLAUDE_HOME, { recursive: true });
|
|
2741
2787
|
const publishedPointerPaths = publishManagedLookupPointers();
|
|
2742
2788
|
|
|
@@ -2996,6 +3042,9 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2996
3042
|
console.log(cursorOutcome.warning
|
|
2997
3043
|
? ` DeepStack (cursor): ${cursorOutcome.status} — ${cursorOutcome.warning}`
|
|
2998
3044
|
: ` DeepStack (cursor): ${cursorOutcome.status}`);
|
|
3045
|
+
if (cursorOutcome.status === 'failed') {
|
|
3046
|
+
allActionNeededLines.push(`DeepStack (cursor): failed — ${cursorOutcome.warning}`);
|
|
3047
|
+
}
|
|
2999
3048
|
}
|
|
3000
3049
|
const allRepositoryPluginSpecs = [
|
|
3001
3050
|
USAGE_WRAPUP_PLUGIN_SPEC,
|
|
@@ -3028,9 +3077,14 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
3028
3077
|
INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
3029
3078
|
);
|
|
3030
3079
|
if (deepenGuidancePath) allCodexGuidancePaths.push(deepenGuidancePath);
|
|
3080
|
+
const solPromptPath = installCodexSolPrompt(
|
|
3081
|
+
INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
3082
|
+
join(PACKAGE_ROOT, SOL_PROMPT_DIRECTORY_NAME, SOL_PROMPT_FILE_NAME),
|
|
3083
|
+
);
|
|
3084
|
+
if (solPromptPath) allCodexGuidancePaths.push(solPromptPath);
|
|
3031
3085
|
const solSettingPath = writeCodexSolSetting(
|
|
3032
3086
|
INSTALL_ROOT_RESOLUTION.codexHomeDirectory,
|
|
3033
|
-
|
|
3087
|
+
codexSolPromptPath(INSTALL_ROOT_RESOLUTION.codexHomeDirectory),
|
|
3034
3088
|
);
|
|
3035
3089
|
if (solSettingPath) {
|
|
3036
3090
|
allCodexGuidancePaths.push(solSettingPath);
|
|
@@ -3151,6 +3205,16 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
3151
3205
|
for (const eachLine of describeProfileSettingsOutcome(mergeOutcome, false)) {
|
|
3152
3206
|
console.log(` Profile settings: ${eachLine}`);
|
|
3153
3207
|
}
|
|
3208
|
+
for (const eachOutcome of addOpenTasksGateToProfiles({
|
|
3209
|
+
mainSettingsPath: join(CLAUDE_HOME, SETTINGS_FILE_NAME),
|
|
3210
|
+
claudeHome: CLAUDE_HOME,
|
|
3211
|
+
allProfileSettingsPaths: profileSettingsPathsForStopGate(join(CLAUDE_HOME, SETTINGS_FILE_NAME)),
|
|
3212
|
+
homeDirectory: homedir(),
|
|
3213
|
+
})) {
|
|
3214
|
+
if (eachOutcome.additions.length > 0) {
|
|
3215
|
+
console.log(` Profile settings: open-tasks Stop gate added to ${eachOutcome.settingsPath}`);
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3154
3218
|
|
|
3155
3219
|
const agentsHubSource = join(PACKAGE_ROOT, 'AGENTS.md');
|
|
3156
3220
|
if (existsSync(agentsHubSource)) {
|
|
@@ -3248,6 +3312,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
3248
3312
|
console.log(` hooks: ${summary.hookFiles.created + summary.hookFiles.updated} files, ${summary.hookGroups} groups in settings.json`);
|
|
3249
3313
|
}
|
|
3250
3314
|
console.log(` python: ${pythonCommand}\n`);
|
|
3315
|
+
printActionNeeded();
|
|
3251
3316
|
}
|
|
3252
3317
|
|
|
3253
3318
|
function normalizePathForComparison(rawPath) {
|
|
@@ -3431,6 +3496,12 @@ function executeUninstallPlan(plan, helpers = {}) {
|
|
|
3431
3496
|
writeFileSync(plan.settingsPath, JSON.stringify(settings, null, 4) + '\n');
|
|
3432
3497
|
}
|
|
3433
3498
|
}
|
|
3499
|
+
for (const eachChangedPath of removeOpenTasksGateFromProfiles({
|
|
3500
|
+
claudeHome: CLAUDE_HOME,
|
|
3501
|
+
allProfileSettingsPaths: profileSettingsPathsForStopGate(plan.settingsPath),
|
|
3502
|
+
})) {
|
|
3503
|
+
console.log(` Profile settings: open-tasks Stop gate removed from ${eachChangedPath}`);
|
|
3504
|
+
}
|
|
3434
3505
|
if (!codexRoutingHookExists()) {
|
|
3435
3506
|
pruneManagedHooksFromHostConfiguration(CODEX_HOOKS_CONFIGURATION_PATH, 2);
|
|
3436
3507
|
}
|
|
@@ -3489,11 +3560,13 @@ function uninstall() {
|
|
|
3489
3560
|
managedRoot: CLAUDE_HOME,
|
|
3490
3561
|
manifestFilePath: MANIFEST_FILE,
|
|
3491
3562
|
settingsPath: plan.settingsPath,
|
|
3492
|
-
additionalSettingsPaths: [CODEX_HOOKS_CONFIGURATION_PATH],
|
|
3563
|
+
additionalSettingsPaths: [CODEX_HOOKS_CONFIGURATION_PATH, ...profileSettingsPathsForStopGate(plan.settingsPath)],
|
|
3493
3564
|
priorManifestFiles: [
|
|
3494
3565
|
...plan.removableFiles,
|
|
3495
3566
|
join(INSTALL_ROOT_RESOLUTION.codexHomeDirectory, 'AGENTS.md'),
|
|
3496
3567
|
...codexSolSettingSnapshotPaths(INSTALL_ROOT_RESOLUTION.codexHomeDirectory),
|
|
3568
|
+
codexSolPromptPath(INSTALL_ROOT_RESOLUTION.codexHomeDirectory),
|
|
3569
|
+
join(INSTALL_ROOT_RESOLUTION.codexHomeDirectory, SOL_PROMPT_FILE_NAME),
|
|
3497
3570
|
],
|
|
3498
3571
|
journalParentDirectory: join(CLAUDE_HOME, TRANSACTION_JOURNAL_DIRECTORY_NAME),
|
|
3499
3572
|
});
|
package/bin/install.test.mjs
CHANGED
|
@@ -3633,3 +3633,96 @@ test('a full install clears a retired session-continuity registration from every
|
|
|
3633
3633
|
);
|
|
3634
3634
|
}
|
|
3635
3635
|
});
|
|
3636
|
+
|
|
3637
|
+
function installWithGlobalPrefix(context, globalPrefix) {
|
|
3638
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cde-action-needed-'));
|
|
3639
|
+
context.after(() => rmSync(homeDirectory, { recursive: true, force: true }));
|
|
3640
|
+
return spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), 'install.mjs')], {
|
|
3641
|
+
cwd: dirname(dirname(fileURLToPath(import.meta.url))),
|
|
3642
|
+
encoding: 'utf8',
|
|
3643
|
+
env: {
|
|
3644
|
+
...process.env,
|
|
3645
|
+
CLAUDE_CONFIG_DIR: undefined,
|
|
3646
|
+
LLM_SETTINGS_PROFILES_ROOT: undefined,
|
|
3647
|
+
CLAUDE_DEV_ENV_INSTALL_FAULT: undefined,
|
|
3648
|
+
CDE_INSTALL_PSTACK: '0',
|
|
3649
|
+
CDE_INSTALL_DEEPSTACK: '0',
|
|
3650
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
3651
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
3652
|
+
CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
|
|
3653
|
+
HOME: homeDirectory,
|
|
3654
|
+
USERPROFILE: homeDirectory,
|
|
3655
|
+
APPDATA: join(homeDirectory, 'AppData', 'Roaming'),
|
|
3656
|
+
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
3657
|
+
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
3658
|
+
TEMP: homeDirectory,
|
|
3659
|
+
TMP: homeDirectory,
|
|
3660
|
+
npm_config_prefix: undefined,
|
|
3661
|
+
npm_config_global_prefix: globalPrefix ?? join(homeDirectory, 'no-global-prefix'),
|
|
3662
|
+
},
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
test('install ends with an Action needed block naming an older global copy that shadows npx', (context) => {
|
|
3667
|
+
const globalPrefix = mkdtempSync(join(tmpdir(), 'cde-global-prefix-'));
|
|
3668
|
+
context.after(() => rmSync(globalPrefix, { recursive: true, force: true }));
|
|
3669
|
+
const globalPackage = process.platform === 'win32'
|
|
3670
|
+
? join(globalPrefix, 'node_modules', 'claude-dev-env')
|
|
3671
|
+
: join(globalPrefix, 'lib', 'node_modules', 'claude-dev-env');
|
|
3672
|
+
mkdirSync(globalPackage, { recursive: true });
|
|
3673
|
+
writeFileSync(join(globalPackage, 'package.json'), JSON.stringify({ name: 'claude-dev-env', version: '0.0.1' }));
|
|
3674
|
+
|
|
3675
|
+
const result = installWithGlobalPrefix(context, globalPrefix);
|
|
3676
|
+
|
|
3677
|
+
assert.equal(result.status, 0, result.stderr);
|
|
3678
|
+
const actionIndex = result.stderr.lastIndexOf('Action needed (1):');
|
|
3679
|
+
assert.ok(actionIndex >= 0, result.stderr);
|
|
3680
|
+
assert.match(result.stderr.slice(actionIndex), /claude-dev-env 0\.0\.1/);
|
|
3681
|
+
assert.match(result.stderr.slice(actionIndex), /npm uninstall -g claude-dev-env/);
|
|
3682
|
+
});
|
|
3683
|
+
|
|
3684
|
+
test('install prints no Action needed block when every side step finished', (context) => {
|
|
3685
|
+
const result = installWithGlobalPrefix(context, null);
|
|
3686
|
+
|
|
3687
|
+
assert.equal(result.status, 0, result.stderr);
|
|
3688
|
+
assert.equal(result.stderr.includes('Action needed'), false, result.stderr);
|
|
3689
|
+
});
|
|
3690
|
+
|
|
3691
|
+
test('install adds the open-tasks Stop gate to every profile with its own settings.json, and uninstall removes it', (context) => {
|
|
3692
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cde-profile-gate-install-'));
|
|
3693
|
+
context.after(() => rmSync(homeDirectory, { recursive: true, force: true }));
|
|
3694
|
+
const profileSettingsPath = join(homeDirectory, '.claude-profiles', 'prefs-only', 'settings.json');
|
|
3695
|
+
mkdirSync(dirname(profileSettingsPath), { recursive: true });
|
|
3696
|
+
writeFileSync(profileSettingsPath, JSON.stringify({ outputStyle: 'plain' }, null, 4));
|
|
3697
|
+
const runInstaller = (argumentsList) => spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), 'install.mjs'), ...argumentsList], {
|
|
3698
|
+
cwd: dirname(dirname(fileURLToPath(import.meta.url))),
|
|
3699
|
+
encoding: 'utf8',
|
|
3700
|
+
env: {
|
|
3701
|
+
...process.env,
|
|
3702
|
+
CLAUDE_CONFIG_DIR: undefined,
|
|
3703
|
+
LLM_SETTINGS_PROFILES_ROOT: undefined,
|
|
3704
|
+
CLAUDE_DEV_ENV_INSTALL_FAULT: undefined,
|
|
3705
|
+
CDE_INSTALL_PSTACK: '0',
|
|
3706
|
+
CDE_INSTALL_DEEPSTACK: '0',
|
|
3707
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
3708
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
3709
|
+
CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
|
|
3710
|
+
HOME: homeDirectory,
|
|
3711
|
+
USERPROFILE: homeDirectory,
|
|
3712
|
+
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
3713
|
+
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
3714
|
+
TEMP: homeDirectory,
|
|
3715
|
+
TMP: homeDirectory,
|
|
3716
|
+
},
|
|
3717
|
+
});
|
|
3718
|
+
const profileStopCommands = () => (JSON.parse(readFileSync(profileSettingsPath, 'utf8')).hooks?.Stop ?? [])
|
|
3719
|
+
.flatMap((eachGroup) => eachGroup.hooks.map((eachHook) => eachHook.command));
|
|
3720
|
+
|
|
3721
|
+
const installation = runInstaller([]);
|
|
3722
|
+
assert.equal(installation.status, 0, installation.stderr);
|
|
3723
|
+
assert.equal(profileStopCommands().filter((eachCommand) => eachCommand.includes('open_tasks_stop_gate.py')).length, 1);
|
|
3724
|
+
|
|
3725
|
+
const uninstallation = runInstaller(['--uninstall']);
|
|
3726
|
+
assert.equal(uninstallation.status, 0, uninstallation.stderr);
|
|
3727
|
+
assert.deepEqual(JSON.parse(readFileSync(profileSettingsPath, 'utf8')), { outputStyle: 'plain' });
|
|
3728
|
+
});
|