claudeup 4.29.0 → 4.31.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.
@@ -21,11 +21,26 @@ import { profileDir } from "./symlink-manager.js";
21
21
  * enabled, merged with the profile's declared settings. The profile's settings
22
22
  * win over the derived enabledPlugins only if it explicitly sets that key
23
23
  * (it normally won't).
24
+ *
25
+ * `allPluginIds` is the manifest-wide union of plugins across every profile.
26
+ * Plugins in the union that this profile does NOT want are written as an
27
+ * explicit `false`, which is what makes `profile switch` exclusive rather than
28
+ * additive. Claude Code resolves enabledPlugins **per plugin id**, walking from
29
+ * the highest-precedence scope down and taking the first scope that mentions
30
+ * the id — an id a scope omits falls through to the next one. Since
31
+ * `claudeup install` installs the union at user scope, every non-member would
32
+ * otherwise inherit that user-scope `true` and stay enabled while a profile
33
+ * that excludes it is active. Omitting the union (the default) preserves the
34
+ * old additive behavior for callers that have no cross-profile view.
24
35
  */
25
36
  export function buildProfileSettings(
26
37
  closure: ResolvedClosure,
38
+ allPluginIds: readonly string[] = [],
27
39
  ): Record<string, unknown> {
28
40
  const enabledPlugins: Record<string, boolean> = {};
41
+ for (const pluginId of allPluginIds) {
42
+ enabledPlugins[pluginId] = false;
43
+ }
29
44
  for (const pluginId of Object.keys(closure.plugins)) {
30
45
  enabledPlugins[pluginId] = true;
31
46
  }
@@ -39,6 +54,37 @@ export function buildProfileMcp(
39
54
  return { mcpServers: { ...closure.mcpServers } };
40
55
  }
41
56
 
57
+ /**
58
+ * Seed a profile's skills/ dir from the project's pre-existing `.claude/skills/`.
59
+ *
60
+ * Activating a profile replaces `.claude/skills` with a symlink into the profile
61
+ * dir, and replacing means `fs.remove` first. A project that committed its own
62
+ * skills before adopting profiles would lose them at that moment. So the first
63
+ * time a profile's skills/ dir is created, any real (non-symlink) project skills
64
+ * are copied in — every profile inherits what the project already had, and the
65
+ * link swap destroys nothing.
66
+ *
67
+ * Only runs when the profile's skills/ dir does not exist yet, so it never
68
+ * fights a later `skills-manager` install or re-adds a skill the user removed.
69
+ */
70
+ async function seedSkillsFromProject(
71
+ skillsDir: string,
72
+ projectPath?: string,
73
+ ): Promise<void> {
74
+ if (await fs.pathExists(skillsDir)) return;
75
+
76
+ const projectSkills = path.join(projectPath ?? process.cwd(), ".claude", "skills");
77
+ try {
78
+ // lstat, not pathExists: an existing profile symlink is not a source.
79
+ const stat = await fs.lstat(projectSkills);
80
+ if (!stat.isDirectory() || stat.isSymbolicLink()) return;
81
+ if ((await fs.readdir(projectSkills)).length === 0) return;
82
+ await fs.copy(projectSkills, skillsDir, { dereference: true });
83
+ } catch {
84
+ // No project skills dir (or unreadable) — nothing to carry over.
85
+ }
86
+ }
87
+
42
88
  /**
43
89
  * Write `_profiles/<name>/` from a resolved closure. Idempotent: overwrites
44
90
  * settings.json / mcp.json and ensures skills/ exists. Returns the dir path.
@@ -47,15 +93,20 @@ export async function materializeProfile(
47
93
  name: string,
48
94
  closure: ResolvedClosure,
49
95
  projectPath?: string,
96
+ allPluginIds: readonly string[] = [],
50
97
  ): Promise<string> {
51
98
  const dir = profileDir(name, projectPath);
52
99
  await fs.ensureDir(dir);
53
- await fs.writeJson(path.join(dir, "settings.json"), buildProfileSettings(closure), {
54
- spaces: 2,
55
- });
100
+ await fs.writeJson(
101
+ path.join(dir, "settings.json"),
102
+ buildProfileSettings(closure, allPluginIds),
103
+ { spaces: 2 },
104
+ );
56
105
  await fs.writeJson(path.join(dir, "mcp.json"), buildProfileMcp(closure), {
57
106
  spaces: 2,
58
107
  });
59
- await fs.ensureDir(path.join(dir, "skills"));
108
+ const skillsDir = path.join(dir, "skills");
109
+ await seedSkillsFromProject(skillsDir, projectPath);
110
+ await fs.ensureDir(skillsDir);
60
111
  return dir;
61
112
  }