claude-dev-env 8.48.2 → 8.48.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/install-plan.mjs
CHANGED
|
@@ -214,7 +214,6 @@ export function preflightSettingsIfNeeded(settingsPath, shouldInstallHooks, io =
|
|
|
214
214
|
*/
|
|
215
215
|
export function buildInstallPlan(input) {
|
|
216
216
|
const io = input.io || {};
|
|
217
|
-
const exists = io.existsSync || existsSync;
|
|
218
217
|
const packageName = input.packageName || 'claude-dev-env';
|
|
219
218
|
|
|
220
219
|
preflightManagedRoot(input.managedRoot, io);
|
|
@@ -250,9 +249,6 @@ export function buildInstallPlan(input) {
|
|
|
250
249
|
preflightSettingsIfNeeded(settingsPath, shouldInstallHooks, io);
|
|
251
250
|
|
|
252
251
|
const isUpdateRefresh = Boolean(input.isUpdateRefresh);
|
|
253
|
-
const shouldPurgeBeforeReinstall = isUpdateRefresh
|
|
254
|
-
&& !input.selectedGroups
|
|
255
|
-
&& exists(input.manifestFilePath);
|
|
256
252
|
|
|
257
253
|
return Object.freeze({
|
|
258
254
|
packageRoot: input.packageRoot,
|
|
@@ -261,7 +257,6 @@ export function buildInstallPlan(input) {
|
|
|
261
257
|
targetIdentity: input.targetIdentity,
|
|
262
258
|
selectedGroups: input.selectedGroups,
|
|
263
259
|
isUpdateRefresh,
|
|
264
|
-
shouldPurgeBeforeReinstall,
|
|
265
260
|
pythonCommand,
|
|
266
261
|
shouldInstallHooks,
|
|
267
262
|
priorManifest: Object.freeze({
|
|
@@ -277,7 +272,6 @@ export function buildInstallPlan(input) {
|
|
|
277
272
|
* High-level mutation kinds the executor applies for this plan (E2 surface).
|
|
278
273
|
*
|
|
279
274
|
* @param {{
|
|
280
|
-
* shouldPurgeBeforeReinstall: boolean,
|
|
281
275
|
* shouldInstallHooks: boolean,
|
|
282
276
|
* selectedGroups: string[]|null,
|
|
283
277
|
* }} plan
|
|
@@ -286,9 +280,6 @@ export function buildInstallPlan(input) {
|
|
|
286
280
|
export function describeInstallMutations(plan) {
|
|
287
281
|
/** @type {string[]} */
|
|
288
282
|
const allMutations = [];
|
|
289
|
-
if (plan.shouldPurgeBeforeReinstall) {
|
|
290
|
-
allMutations.push('purge_managed_installation');
|
|
291
|
-
}
|
|
292
283
|
allMutations.push(
|
|
293
284
|
'ensure_managed_root',
|
|
294
285
|
'publish_directory_pointers',
|
package/bin/install.mjs
CHANGED
|
@@ -2597,7 +2597,7 @@ function runFullInstallPrunes(
|
|
|
2597
2597
|
* available to a later prune and to uninstall.
|
|
2598
2598
|
*
|
|
2599
2599
|
* @param {string[]|null} selectedGroups The `--only` group names, or null for a full install.
|
|
2600
|
-
* @param {{isUpdateRefresh?: boolean}} [options] Run options; `isUpdateRefresh`
|
|
2600
|
+
* @param {{isUpdateRefresh?: boolean}} [options] Run options; `isUpdateRefresh` labels the run as an update in the log.
|
|
2601
2601
|
* @returns {void}
|
|
2602
2602
|
*/
|
|
2603
2603
|
function install(selectedGroups, options = {}) {
|
|
@@ -2674,15 +2674,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2674
2674
|
const isUpdateRefresh = plan.isUpdateRefresh;
|
|
2675
2675
|
const pythonCommand = plan.pythonCommand;
|
|
2676
2676
|
|
|
2677
|
-
if (
|
|
2678
|
-
console.log(
|
|
2679
|
-
`${PACKAGE_NAME}: --update — removing prior managed files under ${CLAUDE_HOME}, then reinstalling from the package.\n`,
|
|
2680
|
-
);
|
|
2681
|
-
purgeManagedInstallation({
|
|
2682
|
-
isManifestRequired: false,
|
|
2683
|
-
throwIfFault,
|
|
2684
|
-
});
|
|
2685
|
-
} else if (isUpdateRefresh) {
|
|
2677
|
+
if (isUpdateRefresh) {
|
|
2686
2678
|
const installScope = selectedGroups ? `groups: ${selectedGroups.join(', ')}` : 'full';
|
|
2687
2679
|
console.log(`${PACKAGE_NAME}: --update — re-running ${installScope} install into ${CLAUDE_HOME}\n`);
|
|
2688
2680
|
}
|
|
@@ -3335,28 +3327,6 @@ function executeUninstallPlan(plan, helpers = {}) {
|
|
|
3335
3327
|
return removed;
|
|
3336
3328
|
}
|
|
3337
3329
|
|
|
3338
|
-
/**
|
|
3339
|
-
* Remove every file the ownership record lists for this managed root.
|
|
3340
|
-
*
|
|
3341
|
-
* Preflights the uninstall plan (settings JSON must be an object when present)
|
|
3342
|
-
* before any removal. When called from `--update` inside an install transaction,
|
|
3343
|
-
* pass that transaction's `throwIfFault` so a fault restores the outer snapshot
|
|
3344
|
-
* without nesting a second journal.
|
|
3345
|
-
*
|
|
3346
|
-
* @param {{
|
|
3347
|
-
* isManifestRequired: boolean,
|
|
3348
|
-
* throwIfFault?: (phase: string) => void,
|
|
3349
|
-
* }} options
|
|
3350
|
-
* @returns {number|void} 0 when no manifest exists and none is required.
|
|
3351
|
-
*/
|
|
3352
|
-
function purgeManagedInstallation({ isManifestRequired, throwIfFault }) {
|
|
3353
|
-
const plan = resolveUninstallPlan(isManifestRequired);
|
|
3354
|
-
if (plan.isNoOp) {
|
|
3355
|
-
return 0;
|
|
3356
|
-
}
|
|
3357
|
-
return executeUninstallPlan(plan, { throwIfFault });
|
|
3358
|
-
}
|
|
3359
|
-
|
|
3360
3330
|
/**
|
|
3361
3331
|
* Uninstall the selected managed root inside a snapshot/restore transaction.
|
|
3362
3332
|
*
|
|
@@ -3435,7 +3405,7 @@ ${PACKAGE_NAME} - Claude Code development standards installer
|
|
|
3435
3405
|
|
|
3436
3406
|
Usage:
|
|
3437
3407
|
npx ${PACKAGE_NAME} Install everything into the main default root
|
|
3438
|
-
npx ${PACKAGE_NAME} --update Full install:
|
|
3408
|
+
npx ${PACKAGE_NAME} --update Full install: copy the package over the prior install, then prune files it no longer ships
|
|
3439
3409
|
npx ${PACKAGE_NAME} --only X Install specific groups
|
|
3440
3410
|
npx ${PACKAGE_NAME} --target DIR Install into DIR instead of ~/.claude (overrides CLAUDE_CONFIG_DIR)
|
|
3441
3411
|
npx ${PACKAGE_NAME} --profile ID Install into one named profile root (under the profiles root)
|
|
@@ -91,7 +91,6 @@ test('buildInstallPlan is zero-write and freezes a success plan', () => {
|
|
|
91
91
|
const plan = buildInstallPlan(planInput(packageRoot, managedRoot));
|
|
92
92
|
assert.equal(plan.pythonCommand, 'python3');
|
|
93
93
|
assert.equal(plan.shouldInstallHooks, true);
|
|
94
|
-
assert.equal(plan.shouldPurgeBeforeReinstall, false);
|
|
95
94
|
assert.deepEqual(plan.priorManifest.skills, ['a']);
|
|
96
95
|
assert.ok(Object.isFrozen(plan));
|
|
97
96
|
assert.equal(treeStamp(managedRoot) + '//' + treeStamp(packageRoot), before);
|
|
@@ -166,7 +165,7 @@ test('settings preflight is hooks-gated; broken manifest stays tolerant', () =>
|
|
|
166
165
|
}
|
|
167
166
|
});
|
|
168
167
|
|
|
169
|
-
test('update plan
|
|
168
|
+
test('update plan copies before it prunes and never purges; journal omits hooks', () => {
|
|
170
169
|
const { root, managedRoot, packageRoot } = sandbox();
|
|
171
170
|
try {
|
|
172
171
|
const manifestFilePath = join(managedRoot, '.claude-dev-env-manifest.json');
|
|
@@ -177,9 +176,11 @@ test('update plan marks purge and lists E2 mutation kinds; journal omits hooks',
|
|
|
177
176
|
targetIdentity: 'profile-a',
|
|
178
177
|
isUpdateRefresh: true,
|
|
179
178
|
}));
|
|
180
|
-
assert.equal(plan.shouldPurgeBeforeReinstall, true);
|
|
181
179
|
const allMutations = describeInstallMutations(plan);
|
|
182
|
-
assert.ok(allMutations.includes('purge_managed_installation'));
|
|
180
|
+
assert.ok(!allMutations.includes('purge_managed_installation'));
|
|
181
|
+
assert.ok(
|
|
182
|
+
allMutations.indexOf('copy_hook_files') < allMutations.indexOf('prune_retired_and_stale'),
|
|
183
|
+
);
|
|
183
184
|
assert.ok(allMutations.includes('publish_directory_pointers'));
|
|
184
185
|
assert.ok(allMutations.includes('merge_hooks_settings'));
|
|
185
186
|
assert.ok(allMutations.includes('write_manifest'));
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An `--update` reinstall keeps every registered hook script on disk for the
|
|
3
|
+
* whole run, so a live session's hook call never finds its script missing.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { test } from 'node:test';
|
|
7
|
+
import { strict as assert } from 'node:assert';
|
|
8
|
+
import { existsSync, mkdtempSync, rmSync } from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
13
|
+
import { INSTALL_FAULT_ENV } from './install-transaction.mjs';
|
|
14
|
+
|
|
15
|
+
const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
|
|
17
|
+
const REGISTERED_HOOK_RELATIVE_PATHS = [
|
|
18
|
+
join('blocking', 'bash_post_call_dispatcher.py'),
|
|
19
|
+
join('blocking', 'verify_before_acting.py'),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function installerEnvironment(homeDirectory) {
|
|
23
|
+
const childEnvironment = {
|
|
24
|
+
...process.env,
|
|
25
|
+
CDE_INSTALL_PSTACK: '0',
|
|
26
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
27
|
+
HOME: homeDirectory,
|
|
28
|
+
USERPROFILE: homeDirectory,
|
|
29
|
+
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
30
|
+
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
31
|
+
};
|
|
32
|
+
delete childEnvironment[INSTALL_FAULT_ENV];
|
|
33
|
+
return childEnvironment;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function runUpdateWhileWatching(homeDirectory, watchedPaths) {
|
|
37
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
38
|
+
const missingCountByPath = new Map();
|
|
39
|
+
let outputText = '';
|
|
40
|
+
let hasExited = false;
|
|
41
|
+
const child = spawn(process.execPath, [INSTALLER_PATH, '--update'], {
|
|
42
|
+
cwd: THIS_DIRECTORY,
|
|
43
|
+
env: installerEnvironment(homeDirectory),
|
|
44
|
+
});
|
|
45
|
+
child.stdout.on('data', (chunk) => { outputText += chunk; });
|
|
46
|
+
child.stderr.on('data', (chunk) => { outputText += chunk; });
|
|
47
|
+
child.on('error', rejectPromise);
|
|
48
|
+
child.on('exit', (exitCode) => {
|
|
49
|
+
hasExited = true;
|
|
50
|
+
resolvePromise({ exitCode, outputText, missingCountByPath });
|
|
51
|
+
});
|
|
52
|
+
const pollOnce = () => {
|
|
53
|
+
if (hasExited) return;
|
|
54
|
+
for (const watchedPath of watchedPaths) {
|
|
55
|
+
if (!existsSync(watchedPath)) {
|
|
56
|
+
missingCountByPath.set(watchedPath, (missingCountByPath.get(watchedPath) ?? 0) + 1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
setImmediate(pollOnce);
|
|
60
|
+
};
|
|
61
|
+
setImmediate(pollOnce);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
test('--update never leaves a registered hook script missing mid-install', async () => {
|
|
66
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-update-hooks-'));
|
|
67
|
+
try {
|
|
68
|
+
const firstInstall = spawnSync(process.execPath, [INSTALLER_PATH], {
|
|
69
|
+
cwd: THIS_DIRECTORY,
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
env: installerEnvironment(homeDirectory),
|
|
72
|
+
});
|
|
73
|
+
assert.equal(firstInstall.status, 0, firstInstall.stdout + firstInstall.stderr);
|
|
74
|
+
const hooksDirectory = join(homeDirectory, '.agents', 'hooks');
|
|
75
|
+
const watchedPaths = REGISTERED_HOOK_RELATIVE_PATHS.map(
|
|
76
|
+
(relativePath) => join(hooksDirectory, relativePath),
|
|
77
|
+
);
|
|
78
|
+
for (const watchedPath of watchedPaths) {
|
|
79
|
+
assert.equal(existsSync(watchedPath), true, `first install wrote ${watchedPath}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const updateRun = await runUpdateWhileWatching(homeDirectory, watchedPaths);
|
|
83
|
+
|
|
84
|
+
assert.equal(updateRun.exitCode, 0, updateRun.outputText);
|
|
85
|
+
assert.deepEqual(
|
|
86
|
+
Object.fromEntries(updateRun.missingCountByPath),
|
|
87
|
+
{},
|
|
88
|
+
'a registered hook script was absent while --update ran',
|
|
89
|
+
);
|
|
90
|
+
for (const watchedPath of watchedPaths) {
|
|
91
|
+
assert.equal(existsSync(watchedPath), true, `--update left ${watchedPath} in place`);
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
rmSync(homeDirectory, { recursive: true, force: true });
|
|
95
|
+
}
|
|
96
|
+
});
|