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 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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.106.0",
3
+ "version": "8.106.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {