claude-dev-env 8.106.0 → 8.106.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/bin/install.mjs +16 -0
- package/bin/install.prune.test.mjs +45 -0
- package/package.json +1 -1
package/bin/install.mjs
CHANGED
|
@@ -1064,6 +1064,7 @@ export function pruneStaleInstalledFiles(
|
|
|
1064
1064
|
currentInstalledFiles.map(currentFile => comparisonKeyForPath(currentFile, options)),
|
|
1065
1065
|
);
|
|
1066
1066
|
const resolvedRoot = resolve(destinationRoot);
|
|
1067
|
+
const resolvedRootTarget = realPathOrNull(resolvedRoot);
|
|
1067
1068
|
let prunedCount = 0;
|
|
1068
1069
|
const failedPaths = [];
|
|
1069
1070
|
for (const priorFile of priorInstalledFiles) {
|
|
@@ -1072,6 +1073,17 @@ export function pruneStaleInstalledFiles(
|
|
|
1072
1073
|
if (currentFileKeys.has(comparisonKeyForPath(stalePath, options))) continue;
|
|
1073
1074
|
if (isRetainedUnmanagedPath(stalePath, managedHomeDirectory)) continue;
|
|
1074
1075
|
if (!isMovableStaleFile(stalePath)) continue;
|
|
1076
|
+
const staleTarget = realPathOrNull(stalePath);
|
|
1077
|
+
if (!staleTarget || !resolvedRootTarget) {
|
|
1078
|
+
failedPaths.push(stalePath);
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
if (!isInsideDirectory(staleTarget, resolvedRootTarget)) {
|
|
1082
|
+
console.warn(
|
|
1083
|
+
` Warning: leaving ${relative(managedHomeDirectory, stalePath)} in place — a link on its path leads outside ${resolvedRoot}`,
|
|
1084
|
+
);
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1075
1087
|
const backupRelativePath = relative(resolvedRoot, stalePath);
|
|
1076
1088
|
const didMove = moveIntoRunBackup(
|
|
1077
1089
|
stalePath, backupRoot, backupRelativePath, STALE_FILE_REASON_LABEL, managedHomeDirectory,
|
|
@@ -3013,6 +3025,10 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
3013
3025
|
if (allowedSkills && !allowedSkills.has(skillDir.name)) continue;
|
|
3014
3026
|
const skillSourceDirectory = join(skillsSource, skillDir.name);
|
|
3015
3027
|
const skillDestinationDirectory = join(CLAUDE_HOME, MANAGED_SKILLS_DIRECTORY_NAME, skillDir.name);
|
|
3028
|
+
if (lstatSync(skillDestinationDirectory, { throwIfNoEntry: false })?.isSymbolicLink()) {
|
|
3029
|
+
console.log(` = ${join(MANAGED_SKILLS_DIRECTORY_NAME, skillDir.name)} (linked skill left in place)`);
|
|
3030
|
+
continue;
|
|
3031
|
+
}
|
|
3016
3032
|
const stats = copyTree(skillSourceDirectory, skillDestinationDirectory);
|
|
3017
3033
|
skillsCreated += stats.created;
|
|
3018
3034
|
skillsUpdated += stats.updated;
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
readFileSync,
|
|
10
10
|
readdirSync,
|
|
11
11
|
rmSync,
|
|
12
|
+
symlinkSync,
|
|
13
|
+
lstatSync,
|
|
12
14
|
} from 'node:fs';
|
|
13
15
|
import { tmpdir } from 'node:os';
|
|
14
16
|
import { join, dirname, basename } from 'node:path';
|
|
@@ -828,6 +830,49 @@ test('a fresh scoped core install includes efficient-review', () => {
|
|
|
828
830
|
}
|
|
829
831
|
});
|
|
830
832
|
|
|
833
|
+
test('a full reinstall leaves a skill folder linked into another repository untouched', () => {
|
|
834
|
+
const sandbox = createSandbox();
|
|
835
|
+
const linkedRepositoryDirectory = mkdtempSync(join(tmpdir(), 'cdev-linked-skill-repo-'));
|
|
836
|
+
try {
|
|
837
|
+
runInstaller(sandbox.homeDirectory, []);
|
|
838
|
+
const linkedSkillName = 'efficient-review';
|
|
839
|
+
const repositorySkillDirectory = join(linkedRepositoryDirectory, 'skills', linkedSkillName);
|
|
840
|
+
const repositorySkillText = 'name: efficient-review\nthe repository copy\n';
|
|
841
|
+
writeFileWithParents(join(repositorySkillDirectory, 'SKILL.md'), repositorySkillText);
|
|
842
|
+
const skillLinkPath = join(sandbox.skillsDirectory, linkedSkillName);
|
|
843
|
+
rmSync(skillLinkPath, { recursive: true, force: true });
|
|
844
|
+
symlinkSync(repositorySkillDirectory, skillLinkPath, 'junction');
|
|
845
|
+
|
|
846
|
+
const installerOutput = runInstallerReadingBothStreams(sandbox.homeDirectory, []);
|
|
847
|
+
|
|
848
|
+
assert.equal(lstatSync(skillLinkPath).isSymbolicLink(), true, 'the skill link stays a link');
|
|
849
|
+
assert.equal(
|
|
850
|
+
readFileSync(join(repositorySkillDirectory, 'SKILL.md'), 'utf8'),
|
|
851
|
+
repositorySkillText,
|
|
852
|
+
'the repository SKILL.md keeps its own text',
|
|
853
|
+
);
|
|
854
|
+
assert.deepEqual(
|
|
855
|
+
readdirSync(repositorySkillDirectory),
|
|
856
|
+
['SKILL.md'],
|
|
857
|
+
'the install writes no package file into the linked repository',
|
|
858
|
+
);
|
|
859
|
+
assert.equal(
|
|
860
|
+
prunedSkillBackupContains(sandbox.claudeDirectory, join(linkedSkillName, 'SKILL.md')),
|
|
861
|
+
false,
|
|
862
|
+
'the stale prune moves no file out of the linked repository',
|
|
863
|
+
);
|
|
864
|
+
assert.match(installerOutput, /skills[\\/]efficient-review \(linked skill left in place\)/);
|
|
865
|
+
assert.equal(
|
|
866
|
+
readManifest(sandbox.manifestPath).files.some(recordedPath => recordedPath.includes(linkedSkillName)),
|
|
867
|
+
false,
|
|
868
|
+
'the manifest records no file under the linked skill',
|
|
869
|
+
);
|
|
870
|
+
} finally {
|
|
871
|
+
rmSync(sandbox.homeDirectory, { recursive: true, force: true });
|
|
872
|
+
rmSync(linkedRepositoryDirectory, { recursive: true, force: true });
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
|
|
831
876
|
test('a fresh scoped core install ships active skills and omits retired skills', () => {
|
|
832
877
|
const sandbox = createSandbox();
|
|
833
878
|
try {
|