claudeup 3.16.0 → 3.17.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "3.16.0",
3
+ "version": "3.17.0",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -0,0 +1,191 @@
1
+ export const PREDEFINED_PROFILES = [
2
+ {
3
+ id: "frontend-pro",
4
+ name: "Frontend Pro",
5
+ description: "UI implementation, design fidelity, browser-driven workflows",
6
+ icon: "🎨",
7
+ magusPlugins: [
8
+ "dev",
9
+ "code-analysis",
10
+ "terminal",
11
+ "statusline",
12
+ "designer",
13
+ "browser-use",
14
+ "multimodel",
15
+ ],
16
+ anthropicPlugins: [
17
+ "feature-dev",
18
+ "frontend-design",
19
+ "code-simplifier",
20
+ "explanatory-output-style",
21
+ "typescript-lsp",
22
+ ],
23
+ skills: [
24
+ "React Best Practices",
25
+ "Web Design Guidelines",
26
+ "shadcn/ui",
27
+ "UI/UX Pro Max",
28
+ "Find Skills",
29
+ ],
30
+ settings: {
31
+ effortLevel: "high",
32
+ alwaysThinkingEnabled: true,
33
+ model: "claude-sonnet-4-6",
34
+ outputStyle: "explanatory",
35
+ env: {
36
+ CLAUDE_CODE_ENABLE_TASKS: "true",
37
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "true",
38
+ },
39
+ includeGitInstructions: true,
40
+ respectGitignore: true,
41
+ enableAllProjectMcpServers: true,
42
+ },
43
+ },
44
+ {
45
+ id: "backend-forge",
46
+ name: "Backend Forge",
47
+ description: "API development, debugging, code quality, data workflows",
48
+ icon: "⚙️",
49
+ magusPlugins: [
50
+ "dev",
51
+ "code-analysis",
52
+ "terminal",
53
+ "statusline",
54
+ "conductor",
55
+ "multimodel",
56
+ "gtd",
57
+ ],
58
+ anthropicPlugins: [
59
+ "feature-dev",
60
+ "code-review",
61
+ "code-simplifier",
62
+ "commit-commands",
63
+ "security-guidance",
64
+ "agent-sdk-dev",
65
+ ],
66
+ skills: ["Systematic Debugging", "Neon Postgres", "Find Skills"],
67
+ settings: {
68
+ effortLevel: "high",
69
+ alwaysThinkingEnabled: true,
70
+ model: "claude-sonnet-4-6",
71
+ outputStyle: "concise",
72
+ env: {
73
+ CLAUDE_CODE_ENABLE_TASKS: "true",
74
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "true",
75
+ },
76
+ includeGitInstructions: true,
77
+ respectGitignore: true,
78
+ enableAllProjectMcpServers: true,
79
+ },
80
+ },
81
+ {
82
+ id: "infra-ops",
83
+ name: "Infra Ops",
84
+ description: "Infrastructure, operational debugging, automation, terminal-first",
85
+ icon: "🔧",
86
+ magusPlugins: [
87
+ "dev",
88
+ "code-analysis",
89
+ "terminal",
90
+ "statusline",
91
+ "multimodel",
92
+ "gtd",
93
+ "browser-use",
94
+ ],
95
+ anthropicPlugins: [
96
+ "claude-code-setup",
97
+ "hookify",
98
+ "code-review",
99
+ "commit-commands",
100
+ "security-guidance",
101
+ "plugin-dev",
102
+ ],
103
+ skills: ["Systematic Debugging", "Find Skills"],
104
+ settings: {
105
+ effortLevel: "high",
106
+ alwaysThinkingEnabled: true,
107
+ model: "claude-opus-4-6",
108
+ outputStyle: "concise",
109
+ env: {
110
+ CLAUDE_CODE_ENABLE_TASKS: "true",
111
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "true",
112
+ },
113
+ includeGitInstructions: true,
114
+ respectGitignore: true,
115
+ enableAllProjectMcpServers: true,
116
+ },
117
+ },
118
+ {
119
+ id: "growth-marketer",
120
+ name: "Growth Marketer",
121
+ description: "SEO, website audits, content production, marketing automation",
122
+ icon: "📈",
123
+ magusPlugins: [
124
+ "dev",
125
+ "code-analysis",
126
+ "terminal",
127
+ "statusline",
128
+ "seo",
129
+ "browser-use",
130
+ "nanobanana",
131
+ "video-editing",
132
+ ],
133
+ anthropicPlugins: [
134
+ "explanatory-output-style",
135
+ "frontend-design",
136
+ "playground",
137
+ ],
138
+ skills: [
139
+ "Audit Website",
140
+ "Web Design Guidelines",
141
+ "ElevenLabs TTS",
142
+ "Find Skills",
143
+ ],
144
+ settings: {
145
+ effortLevel: "medium",
146
+ alwaysThinkingEnabled: false,
147
+ model: "claude-sonnet-4-6",
148
+ outputStyle: "explanatory",
149
+ env: { CLAUDE_CODE_ENABLE_TASKS: "true" },
150
+ respectGitignore: true,
151
+ enableAllProjectMcpServers: true,
152
+ },
153
+ },
154
+ {
155
+ id: "team-lead",
156
+ name: "Team Lead",
157
+ description: "Planning, code review, coordination, and broad repo visibility",
158
+ icon: "👔",
159
+ magusPlugins: [
160
+ "dev",
161
+ "code-analysis",
162
+ "terminal",
163
+ "statusline",
164
+ "multimodel",
165
+ "gtd",
166
+ "agentdev",
167
+ ],
168
+ anthropicPlugins: [
169
+ "code-review",
170
+ "pr-review-toolkit",
171
+ "claude-md-management",
172
+ "commit-commands",
173
+ "explanatory-output-style",
174
+ "skill-creator",
175
+ ],
176
+ skills: ["Systematic Debugging", "Find Skills", "Audit Website"],
177
+ settings: {
178
+ effortLevel: "medium",
179
+ alwaysThinkingEnabled: true,
180
+ model: "claude-sonnet-4-6",
181
+ outputStyle: "explanatory",
182
+ env: {
183
+ CLAUDE_CODE_ENABLE_TASKS: "true",
184
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "true",
185
+ },
186
+ includeGitInstructions: true,
187
+ respectGitignore: true,
188
+ enableAllProjectMcpServers: true,
189
+ },
190
+ },
191
+ ];
@@ -0,0 +1,205 @@
1
+ export interface PredefinedProfile {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ icon: string; // emoji or symbol
6
+ magusPlugins: string[];
7
+ anthropicPlugins: string[];
8
+ skills: string[];
9
+ settings: Record<string, unknown>;
10
+ }
11
+
12
+ export const PREDEFINED_PROFILES: PredefinedProfile[] = [
13
+ {
14
+ id: "frontend-pro",
15
+ name: "Frontend Pro",
16
+ description: "UI implementation, design fidelity, browser-driven workflows",
17
+ icon: "🎨",
18
+ magusPlugins: [
19
+ "dev",
20
+ "code-analysis",
21
+ "terminal",
22
+ "statusline",
23
+ "designer",
24
+ "browser-use",
25
+ "multimodel",
26
+ ],
27
+ anthropicPlugins: [
28
+ "feature-dev",
29
+ "frontend-design",
30
+ "code-simplifier",
31
+ "explanatory-output-style",
32
+ "typescript-lsp",
33
+ ],
34
+ skills: [
35
+ "React Best Practices",
36
+ "Web Design Guidelines",
37
+ "shadcn/ui",
38
+ "UI/UX Pro Max",
39
+ "Find Skills",
40
+ ],
41
+ settings: {
42
+ effortLevel: "high",
43
+ alwaysThinkingEnabled: true,
44
+ model: "claude-sonnet-4-6",
45
+ outputStyle: "explanatory",
46
+ env: {
47
+ CLAUDE_CODE_ENABLE_TASKS: "true",
48
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "true",
49
+ },
50
+ includeGitInstructions: true,
51
+ respectGitignore: true,
52
+ enableAllProjectMcpServers: true,
53
+ },
54
+ },
55
+ {
56
+ id: "backend-forge",
57
+ name: "Backend Forge",
58
+ description: "API development, debugging, code quality, data workflows",
59
+ icon: "⚙️",
60
+ magusPlugins: [
61
+ "dev",
62
+ "code-analysis",
63
+ "terminal",
64
+ "statusline",
65
+ "conductor",
66
+ "multimodel",
67
+ "gtd",
68
+ ],
69
+ anthropicPlugins: [
70
+ "feature-dev",
71
+ "code-review",
72
+ "code-simplifier",
73
+ "commit-commands",
74
+ "security-guidance",
75
+ "agent-sdk-dev",
76
+ ],
77
+ skills: ["Systematic Debugging", "Neon Postgres", "Find Skills"],
78
+ settings: {
79
+ effortLevel: "high",
80
+ alwaysThinkingEnabled: true,
81
+ model: "claude-sonnet-4-6",
82
+ outputStyle: "concise",
83
+ env: {
84
+ CLAUDE_CODE_ENABLE_TASKS: "true",
85
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "true",
86
+ },
87
+ includeGitInstructions: true,
88
+ respectGitignore: true,
89
+ enableAllProjectMcpServers: true,
90
+ },
91
+ },
92
+ {
93
+ id: "infra-ops",
94
+ name: "Infra Ops",
95
+ description:
96
+ "Infrastructure, operational debugging, automation, terminal-first",
97
+ icon: "🔧",
98
+ magusPlugins: [
99
+ "dev",
100
+ "code-analysis",
101
+ "terminal",
102
+ "statusline",
103
+ "multimodel",
104
+ "gtd",
105
+ "browser-use",
106
+ ],
107
+ anthropicPlugins: [
108
+ "claude-code-setup",
109
+ "hookify",
110
+ "code-review",
111
+ "commit-commands",
112
+ "security-guidance",
113
+ "plugin-dev",
114
+ ],
115
+ skills: ["Systematic Debugging", "Find Skills"],
116
+ settings: {
117
+ effortLevel: "high",
118
+ alwaysThinkingEnabled: true,
119
+ model: "claude-opus-4-6",
120
+ outputStyle: "concise",
121
+ env: {
122
+ CLAUDE_CODE_ENABLE_TASKS: "true",
123
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "true",
124
+ },
125
+ includeGitInstructions: true,
126
+ respectGitignore: true,
127
+ enableAllProjectMcpServers: true,
128
+ },
129
+ },
130
+ {
131
+ id: "growth-marketer",
132
+ name: "Growth Marketer",
133
+ description:
134
+ "SEO, website audits, content production, marketing automation",
135
+ icon: "📈",
136
+ magusPlugins: [
137
+ "dev",
138
+ "code-analysis",
139
+ "terminal",
140
+ "statusline",
141
+ "seo",
142
+ "browser-use",
143
+ "nanobanana",
144
+ "video-editing",
145
+ ],
146
+ anthropicPlugins: [
147
+ "explanatory-output-style",
148
+ "frontend-design",
149
+ "playground",
150
+ ],
151
+ skills: [
152
+ "Audit Website",
153
+ "Web Design Guidelines",
154
+ "ElevenLabs TTS",
155
+ "Find Skills",
156
+ ],
157
+ settings: {
158
+ effortLevel: "medium",
159
+ alwaysThinkingEnabled: false,
160
+ model: "claude-sonnet-4-6",
161
+ outputStyle: "explanatory",
162
+ env: { CLAUDE_CODE_ENABLE_TASKS: "true" },
163
+ respectGitignore: true,
164
+ enableAllProjectMcpServers: true,
165
+ },
166
+ },
167
+ {
168
+ id: "team-lead",
169
+ name: "Team Lead",
170
+ description:
171
+ "Planning, code review, coordination, and broad repo visibility",
172
+ icon: "👔",
173
+ magusPlugins: [
174
+ "dev",
175
+ "code-analysis",
176
+ "terminal",
177
+ "statusline",
178
+ "multimodel",
179
+ "gtd",
180
+ "agentdev",
181
+ ],
182
+ anthropicPlugins: [
183
+ "code-review",
184
+ "pr-review-toolkit",
185
+ "claude-md-management",
186
+ "commit-commands",
187
+ "explanatory-output-style",
188
+ "skill-creator",
189
+ ],
190
+ skills: ["Systematic Debugging", "Find Skills", "Audit Website"],
191
+ settings: {
192
+ effortLevel: "medium",
193
+ alwaysThinkingEnabled: true,
194
+ model: "claude-sonnet-4-6",
195
+ outputStyle: "explanatory",
196
+ env: {
197
+ CLAUDE_CODE_ENABLE_TASKS: "true",
198
+ CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: "true",
199
+ },
200
+ includeGitInstructions: true,
201
+ respectGitignore: true,
202
+ enableAllProjectMcpServers: true,
203
+ },
204
+ },
205
+ ];
@@ -6,7 +6,21 @@ import { useKeyboard } from "../hooks/useKeyboard.js";
6
6
  import { ScreenLayout } from "../components/layout/index.js";
7
7
  import { ScrollableList } from "../components/ScrollableList.js";
8
8
  import { listProfiles, applyProfile, renameProfile, deleteProfile, exportProfileToJson, importProfileFromJson, } from "../../services/profiles.js";
9
+ import { readSettings, writeSettings, } from "../../services/claude-settings.js";
9
10
  import { writeClipboard, readClipboard, ClipboardUnavailableError, } from "../../utils/clipboard.js";
11
+ import { PREDEFINED_PROFILES, } from "../../data/predefined-profiles.js";
12
+ function buildListItems(profileList) {
13
+ const predefined = PREDEFINED_PROFILES.map((p) => ({
14
+ kind: "predefined",
15
+ profile: p,
16
+ }));
17
+ const saved = profileList.map((e) => ({
18
+ kind: "saved",
19
+ entry: e,
20
+ }));
21
+ return [...predefined, ...saved];
22
+ }
23
+ // ─── Component ────────────────────────────────────────────────────────────────
10
24
  export function ProfilesScreen() {
11
25
  const { state, dispatch } = useApp();
12
26
  const { profiles: profilesState } = state;
@@ -32,7 +46,8 @@ export function ProfilesScreen() {
32
46
  const profileList = profilesState.profiles.status === "success"
33
47
  ? profilesState.profiles.data
34
48
  : [];
35
- const selectedProfile = profileList[profilesState.selectedIndex];
49
+ const allItems = buildListItems(profileList);
50
+ const selectedItem = allItems[profilesState.selectedIndex];
36
51
  // Keyboard handling
37
52
  useKeyboard((event) => {
38
53
  if (state.isSearching || state.modal)
@@ -42,28 +57,88 @@ export function ProfilesScreen() {
42
57
  dispatch({ type: "PROFILES_SELECT", index: newIndex });
43
58
  }
44
59
  else if (event.name === "down" || event.name === "j") {
45
- const newIndex = Math.min(Math.max(0, profileList.length - 1), profilesState.selectedIndex + 1);
60
+ const newIndex = Math.min(Math.max(0, allItems.length - 1), profilesState.selectedIndex + 1);
46
61
  dispatch({ type: "PROFILES_SELECT", index: newIndex });
47
62
  }
48
63
  else if (event.name === "enter" || event.name === "a") {
49
- handleApply();
64
+ if (selectedItem?.kind === "predefined") {
65
+ void handleApplyPredefined(selectedItem.profile);
66
+ }
67
+ else {
68
+ void handleApply();
69
+ }
50
70
  }
51
71
  else if (event.name === "r") {
52
- handleRename();
72
+ if (selectedItem?.kind === "saved")
73
+ void handleRename();
53
74
  }
54
75
  else if (event.name === "d") {
55
- handleDelete();
76
+ if (selectedItem?.kind === "saved")
77
+ void handleDelete();
56
78
  }
57
79
  else if (event.name === "c") {
58
- handleCopy();
80
+ if (selectedItem?.kind === "saved")
81
+ void handleCopy();
59
82
  }
60
83
  else if (event.name === "i") {
61
- handleImport();
84
+ void handleImport();
62
85
  }
63
86
  });
87
+ // ─── Predefined profile apply ─────────────────────────────────────────────
88
+ const handleApplyPredefined = async (profile) => {
89
+ const allPlugins = [
90
+ ...profile.magusPlugins.map((p) => `${p}@magus`),
91
+ ...profile.anthropicPlugins.map((p) => `${p}@claude-plugins-official`),
92
+ ];
93
+ const settingsCount = Object.keys(profile.settings).length;
94
+ const confirmed = await modal.confirm(`Apply ${profile.name}?`, `This will add ${allPlugins.length} plugins, ${profile.skills.length} skills, and update ${settingsCount} settings.\n\nSettings are merged additively — existing values are kept.`);
95
+ if (!confirmed)
96
+ return;
97
+ modal.loading(`Applying "${profile.name}"...`);
98
+ try {
99
+ const settings = await readSettings(state.projectPath);
100
+ // Merge plugins (additive only)
101
+ settings.enabledPlugins = settings.enabledPlugins ?? {};
102
+ for (const plugin of allPlugins) {
103
+ if (!settings.enabledPlugins[plugin]) {
104
+ settings.enabledPlugins[plugin] = true;
105
+ }
106
+ }
107
+ // Merge top-level settings (additive — only set if not already set)
108
+ for (const [key, value] of Object.entries(profile.settings)) {
109
+ if (key === "env") {
110
+ const envMap = value;
111
+ const existing = settings;
112
+ const existingEnv = existing["env"] ?? {};
113
+ for (const [envKey, envVal] of Object.entries(envMap)) {
114
+ if (!existingEnv[envKey]) {
115
+ existingEnv[envKey] = envVal;
116
+ }
117
+ }
118
+ settings["env"] = existingEnv;
119
+ }
120
+ else {
121
+ const settingsMap = settings;
122
+ if (settingsMap[key] === undefined) {
123
+ settingsMap[key] = value;
124
+ }
125
+ }
126
+ }
127
+ await writeSettings(settings, state.projectPath);
128
+ modal.hideModal();
129
+ dispatch({ type: "DATA_REFRESH_COMPLETE" });
130
+ await modal.message("Applied", `Profile "${profile.name}" merged into project settings.`, "success");
131
+ }
132
+ catch (error) {
133
+ modal.hideModal();
134
+ await modal.message("Error", `Failed to apply profile: ${error}`, "error");
135
+ }
136
+ };
137
+ // ─── Saved profile actions ────────────────────────────────────────────────
64
138
  const handleApply = async () => {
65
- if (!selectedProfile)
139
+ if (selectedItem?.kind !== "saved")
66
140
  return;
141
+ const selectedProfile = selectedItem.entry;
67
142
  const scopeChoice = await modal.select("Apply Profile", `Apply "${selectedProfile.name}" to which scope?`, [
68
143
  { label: "User — ~/.claude/settings.json (global)", value: "user" },
69
144
  {
@@ -98,8 +173,9 @@ export function ProfilesScreen() {
98
173
  }
99
174
  };
100
175
  const handleRename = async () => {
101
- if (!selectedProfile)
176
+ if (selectedItem?.kind !== "saved")
102
177
  return;
178
+ const selectedProfile = selectedItem.entry;
103
179
  const newName = await modal.input("Rename Profile", "New name:", selectedProfile.name);
104
180
  if (newName === null || !newName.trim())
105
181
  return;
@@ -116,8 +192,9 @@ export function ProfilesScreen() {
116
192
  }
117
193
  };
118
194
  const handleDelete = async () => {
119
- if (!selectedProfile)
195
+ if (selectedItem?.kind !== "saved")
120
196
  return;
197
+ const selectedProfile = selectedItem.entry;
121
198
  const confirmed = await modal.confirm(`Delete "${selectedProfile.name}"?`, "This will permanently remove the profile.");
122
199
  if (!confirmed)
123
200
  return;
@@ -126,7 +203,7 @@ export function ProfilesScreen() {
126
203
  await deleteProfile(selectedProfile.id, selectedProfile.scope, state.projectPath);
127
204
  modal.hideModal();
128
205
  // Adjust selection if we deleted the last item
129
- const newIndex = Math.max(0, Math.min(profilesState.selectedIndex, profileList.length - 2));
206
+ const newIndex = Math.max(0, Math.min(profilesState.selectedIndex, allItems.length - 2));
130
207
  dispatch({ type: "PROFILES_SELECT", index: newIndex });
131
208
  await fetchData();
132
209
  await modal.message("Deleted", "Profile deleted.", "success");
@@ -137,8 +214,9 @@ export function ProfilesScreen() {
137
214
  }
138
215
  };
139
216
  const handleCopy = async () => {
140
- if (!selectedProfile)
217
+ if (selectedItem?.kind !== "saved")
141
218
  return;
219
+ const selectedProfile = selectedItem.entry;
142
220
  modal.loading("Exporting...");
143
221
  try {
144
222
  const json = await exportProfileToJson(selectedProfile.id, selectedProfile.scope, state.projectPath);
@@ -202,6 +280,7 @@ export function ProfilesScreen() {
202
280
  await modal.message("Error", `Failed to import: ${error}`, "error");
203
281
  }
204
282
  };
283
+ // ─── Rendering helpers ────────────────────────────────────────────────────
205
284
  const formatDate = (iso) => {
206
285
  try {
207
286
  const d = new Date(iso);
@@ -215,7 +294,18 @@ export function ProfilesScreen() {
215
294
  return iso;
216
295
  }
217
296
  };
218
- const renderListItem = (entry, _idx, isSelected) => {
297
+ const renderListItem = (item, _idx, isSelected) => {
298
+ if (item.kind === "predefined") {
299
+ const { profile } = item;
300
+ const pluginCount = profile.magusPlugins.length + profile.anthropicPlugins.length;
301
+ const skillCount = profile.skills.length;
302
+ if (isSelected) {
303
+ return (_jsxs("text", { bg: "blue", fg: "white", children: [" ", profile.icon, " ", profile.name, " \u2014 ", pluginCount, " plugins \u00B7 ", skillCount, " ", "skill", skillCount !== 1 ? "s" : "", " "] }));
304
+ }
305
+ return (_jsxs("text", { children: [_jsx("span", { fg: "blue", children: "[preset]" }), _jsx("span", { children: " " }), _jsxs("span", { fg: "white", children: [profile.icon, " ", profile.name] }), _jsxs("span", { fg: "gray", children: [" ", "\u2014 ", pluginCount, " plugins \u00B7 ", skillCount, " skill", skillCount !== 1 ? "s" : ""] })] }));
306
+ }
307
+ // Saved profile
308
+ const { entry } = item;
219
309
  const pluginCount = Object.keys(entry.plugins).length;
220
310
  const dateStr = formatDate(entry.updatedAt);
221
311
  const scopeColor = entry.scope === "user" ? "cyan" : "green";
@@ -232,12 +322,24 @@ export function ProfilesScreen() {
232
322
  if (profilesState.profiles.status === "error") {
233
323
  return (_jsxs("text", { fg: "red", children: ["Error: ", profilesState.profiles.error.message] }));
234
324
  }
235
- if (profileList.length === 0) {
236
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "gray", children: "No profiles yet." }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "green", children: "Press 's' in the Plugins screen to save the current selection as a profile." }) })] }));
237
- }
238
- if (!selectedProfile) {
325
+ if (!selectedItem) {
239
326
  return _jsx("text", { fg: "gray", children: "Select a profile to see details" });
240
327
  }
328
+ if (selectedItem.kind === "predefined") {
329
+ return renderPredefinedDetail(selectedItem.profile);
330
+ }
331
+ return renderSavedDetail(selectedItem.entry);
332
+ };
333
+ const renderPredefinedDetail = (profile) => {
334
+ const allPlugins = [
335
+ ...profile.magusPlugins.map((p) => `${p}@magus`),
336
+ ...profile.anthropicPlugins.map((p) => `${p}@claude-plugins-official`),
337
+ ];
338
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "blue", children: _jsxs("strong", { children: [profile.icon, " ", profile.name] }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: profile.description }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: "gray", children: ["Magus plugins (", profile.magusPlugins.length, "):"] }), profile.magusPlugins.map((p) => (_jsx("box", { children: _jsxs("text", { fg: "cyan", children: [" ", p, "@magus"] }) }, p)))] }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: "gray", children: ["Anthropic plugins (", profile.anthropicPlugins.length, "):"] }), profile.anthropicPlugins.map((p) => (_jsx("box", { children: _jsxs("text", { fg: "yellow", children: [" ", p, "@claude-plugins-official"] }) }, p)))] }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: "gray", children: ["Skills (", profile.skills.length, "):"] }), profile.skills.map((s) => (_jsx("box", { children: _jsxs("text", { fg: "white", children: [" ", s] }) }, s)))] }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { fg: "gray", children: ["Settings (", Object.keys(profile.settings).length, "):"] }), Object.entries(profile.settings)
339
+ .filter(([k]) => k !== "env")
340
+ .map(([k, v]) => (_jsx("box", { children: _jsxs("text", { fg: "white", children: [" ", k, ": ", String(v)] }) }, k)))] }), _jsx("box", { marginTop: 2, flexDirection: "column", children: _jsxs("box", { children: [_jsxs("text", { bg: "blue", fg: "white", children: [" ", "Enter/a", " "] }), _jsxs("text", { fg: "gray", children: [" ", "Apply (merges ", allPlugins.length, " plugins into project settings)"] })] }) })] }));
341
+ };
342
+ const renderSavedDetail = (selectedProfile) => {
241
343
  const plugins = Object.keys(selectedProfile.plugins);
242
344
  const scopeColor = selectedProfile.scope === "user" ? "cyan" : "green";
243
345
  const scopeLabel = selectedProfile.scope === "user"
@@ -248,8 +350,7 @@ export function ProfilesScreen() {
248
350
  const profileCount = profileList.length;
249
351
  const userCount = profileList.filter((p) => p.scope === "user").length;
250
352
  const projCount = profileList.filter((p) => p.scope === "project").length;
251
- const statusContent = (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Profiles: " }), _jsxs("span", { fg: "cyan", children: [userCount, " user"] }), _jsx("span", { fg: "gray", children: " + " }), _jsxs("span", { fg: "green", children: [projCount, " project"] }), _jsx("span", { fg: "gray", children: " = " }), _jsxs("span", { fg: "white", children: [profileCount, " total"] })] }));
252
- return (_jsx(ScreenLayout, { title: "claudeup Plugin Profiles", currentScreen: "profiles", statusLine: statusContent, footerHints: "\u2191\u2193:nav \u2502 Enter/a:apply \u2502 r:rename \u2502 d:delete \u2502 c:copy \u2502 i:import", listPanel: profileList.length === 0 &&
253
- profilesState.profiles.status !== "loading" ? (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "gray", children: "No profiles yet." }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "green", children: "Go to Plugins (1) and press 's' to save a profile." }) })] })) : (_jsx(ScrollableList, { items: profileList, selectedIndex: profilesState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight })), detailPanel: renderDetail() }));
353
+ const statusContent = (_jsxs("text", { children: [_jsxs("span", { fg: "blue", children: [PREDEFINED_PROFILES.length, " presets"] }), _jsx("span", { fg: "gray", children: " + " }), _jsxs("span", { fg: "cyan", children: [userCount, " user"] }), _jsx("span", { fg: "gray", children: " + " }), _jsxs("span", { fg: "green", children: [projCount, " project"] }), _jsx("span", { fg: "gray", children: " = " }), _jsxs("span", { fg: "white", children: [PREDEFINED_PROFILES.length + profileCount, " total"] })] }));
354
+ return (_jsx(ScreenLayout, { title: "claudeup Plugin Profiles", currentScreen: "profiles", statusLine: statusContent, footerHints: "\u2191\u2193:nav \u2502 Enter/a:apply \u2502 r:rename \u2502 d:delete \u2502 c:copy \u2502 i:import", listPanel: _jsx(ScrollableList, { items: allItems, selectedIndex: profilesState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), detailPanel: renderDetail() }));
254
355
  }
255
356
  export default ProfilesScreen;
@@ -12,12 +12,40 @@ import {
12
12
  exportProfileToJson,
13
13
  importProfileFromJson,
14
14
  } from "../../services/profiles.js";
15
+ import {
16
+ readSettings,
17
+ writeSettings,
18
+ } from "../../services/claude-settings.js";
15
19
  import {
16
20
  writeClipboard,
17
21
  readClipboard,
18
22
  ClipboardUnavailableError,
19
23
  } from "../../utils/clipboard.js";
20
24
  import type { ProfileEntry } from "../../types/index.js";
25
+ import {
26
+ PREDEFINED_PROFILES,
27
+ type PredefinedProfile,
28
+ } from "../../data/predefined-profiles.js";
29
+
30
+ // ─── List item discriminated union ───────────────────────────────────────────
31
+
32
+ type ListItem =
33
+ | { kind: "predefined"; profile: PredefinedProfile }
34
+ | { kind: "saved"; entry: ProfileEntry };
35
+
36
+ function buildListItems(profileList: ProfileEntry[]): ListItem[] {
37
+ const predefined: ListItem[] = PREDEFINED_PROFILES.map((p) => ({
38
+ kind: "predefined" as const,
39
+ profile: p,
40
+ }));
41
+ const saved: ListItem[] = profileList.map((e) => ({
42
+ kind: "saved" as const,
43
+ entry: e,
44
+ }));
45
+ return [...predefined, ...saved];
46
+ }
47
+
48
+ // ─── Component ────────────────────────────────────────────────────────────────
21
49
 
22
50
  export function ProfilesScreen() {
23
51
  const { state, dispatch } = useApp();
@@ -48,8 +76,8 @@ export function ProfilesScreen() {
48
76
  ? profilesState.profiles.data
49
77
  : [];
50
78
 
51
- const selectedProfile: ProfileEntry | undefined =
52
- profileList[profilesState.selectedIndex];
79
+ const allItems = buildListItems(profileList);
80
+ const selectedItem: ListItem | undefined = allItems[profilesState.selectedIndex];
53
81
 
54
82
  // Keyboard handling
55
83
  useKeyboard((event) => {
@@ -60,25 +88,100 @@ export function ProfilesScreen() {
60
88
  dispatch({ type: "PROFILES_SELECT", index: newIndex });
61
89
  } else if (event.name === "down" || event.name === "j") {
62
90
  const newIndex = Math.min(
63
- Math.max(0, profileList.length - 1),
91
+ Math.max(0, allItems.length - 1),
64
92
  profilesState.selectedIndex + 1,
65
93
  );
66
94
  dispatch({ type: "PROFILES_SELECT", index: newIndex });
67
95
  } else if (event.name === "enter" || event.name === "a") {
68
- handleApply();
96
+ if (selectedItem?.kind === "predefined") {
97
+ void handleApplyPredefined(selectedItem.profile);
98
+ } else {
99
+ void handleApply();
100
+ }
69
101
  } else if (event.name === "r") {
70
- handleRename();
102
+ if (selectedItem?.kind === "saved") void handleRename();
71
103
  } else if (event.name === "d") {
72
- handleDelete();
104
+ if (selectedItem?.kind === "saved") void handleDelete();
73
105
  } else if (event.name === "c") {
74
- handleCopy();
106
+ if (selectedItem?.kind === "saved") void handleCopy();
75
107
  } else if (event.name === "i") {
76
- handleImport();
108
+ void handleImport();
77
109
  }
78
110
  });
79
111
 
112
+ // ─── Predefined profile apply ─────────────────────────────────────────────
113
+
114
+ const handleApplyPredefined = async (profile: PredefinedProfile) => {
115
+ const allPlugins = [
116
+ ...profile.magusPlugins.map((p) => `${p}@magus`),
117
+ ...profile.anthropicPlugins.map(
118
+ (p) => `${p}@claude-plugins-official`,
119
+ ),
120
+ ];
121
+ const settingsCount = Object.keys(profile.settings).length;
122
+
123
+ const confirmed = await modal.confirm(
124
+ `Apply ${profile.name}?`,
125
+ `This will add ${allPlugins.length} plugins, ${profile.skills.length} skills, and update ${settingsCount} settings.\n\nSettings are merged additively — existing values are kept.`,
126
+ );
127
+ if (!confirmed) return;
128
+
129
+ modal.loading(`Applying "${profile.name}"...`);
130
+ try {
131
+ const settings = await readSettings(state.projectPath);
132
+
133
+ // Merge plugins (additive only)
134
+ settings.enabledPlugins = settings.enabledPlugins ?? {};
135
+ for (const plugin of allPlugins) {
136
+ if (!settings.enabledPlugins[plugin]) {
137
+ settings.enabledPlugins[plugin] = true;
138
+ }
139
+ }
140
+
141
+ // Merge top-level settings (additive — only set if not already set)
142
+ for (const [key, value] of Object.entries(profile.settings)) {
143
+ if (key === "env") {
144
+ const envMap = value as Record<string, string>;
145
+ const existing = settings as Record<string, unknown>;
146
+ const existingEnv =
147
+ (existing["env"] as Record<string, string> | undefined) ?? {};
148
+ for (const [envKey, envVal] of Object.entries(envMap)) {
149
+ if (!existingEnv[envKey]) {
150
+ existingEnv[envKey] = envVal;
151
+ }
152
+ }
153
+ (settings as Record<string, unknown>)["env"] = existingEnv;
154
+ } else {
155
+ const settingsMap = settings as Record<string, unknown>;
156
+ if (settingsMap[key] === undefined) {
157
+ settingsMap[key] = value;
158
+ }
159
+ }
160
+ }
161
+
162
+ await writeSettings(settings, state.projectPath);
163
+ modal.hideModal();
164
+ dispatch({ type: "DATA_REFRESH_COMPLETE" });
165
+ await modal.message(
166
+ "Applied",
167
+ `Profile "${profile.name}" merged into project settings.`,
168
+ "success",
169
+ );
170
+ } catch (error) {
171
+ modal.hideModal();
172
+ await modal.message(
173
+ "Error",
174
+ `Failed to apply profile: ${error}`,
175
+ "error",
176
+ );
177
+ }
178
+ };
179
+
180
+ // ─── Saved profile actions ────────────────────────────────────────────────
181
+
80
182
  const handleApply = async () => {
81
- if (!selectedProfile) return;
183
+ if (selectedItem?.kind !== "saved") return;
184
+ const selectedProfile = selectedItem.entry;
82
185
 
83
186
  const scopeChoice = await modal.select(
84
187
  "Apply Profile",
@@ -138,7 +241,8 @@ export function ProfilesScreen() {
138
241
  };
139
242
 
140
243
  const handleRename = async () => {
141
- if (!selectedProfile) return;
244
+ if (selectedItem?.kind !== "saved") return;
245
+ const selectedProfile = selectedItem.entry;
142
246
 
143
247
  const newName = await modal.input(
144
248
  "Rename Profile",
@@ -169,7 +273,8 @@ export function ProfilesScreen() {
169
273
  };
170
274
 
171
275
  const handleDelete = async () => {
172
- if (!selectedProfile) return;
276
+ if (selectedItem?.kind !== "saved") return;
277
+ const selectedProfile = selectedItem.entry;
173
278
 
174
279
  const confirmed = await modal.confirm(
175
280
  `Delete "${selectedProfile.name}"?`,
@@ -188,7 +293,7 @@ export function ProfilesScreen() {
188
293
  // Adjust selection if we deleted the last item
189
294
  const newIndex = Math.max(
190
295
  0,
191
- Math.min(profilesState.selectedIndex, profileList.length - 2),
296
+ Math.min(profilesState.selectedIndex, allItems.length - 2),
192
297
  );
193
298
  dispatch({ type: "PROFILES_SELECT", index: newIndex });
194
299
  await fetchData();
@@ -200,7 +305,8 @@ export function ProfilesScreen() {
200
305
  };
201
306
 
202
307
  const handleCopy = async () => {
203
- if (!selectedProfile) return;
308
+ if (selectedItem?.kind !== "saved") return;
309
+ const selectedProfile = selectedItem.entry;
204
310
 
205
311
  modal.loading("Exporting...");
206
312
  try {
@@ -284,6 +390,8 @@ export function ProfilesScreen() {
284
390
  }
285
391
  };
286
392
 
393
+ // ─── Rendering helpers ────────────────────────────────────────────────────
394
+
287
395
  const formatDate = (iso: string): string => {
288
396
  try {
289
397
  const d = new Date(iso);
@@ -297,11 +405,41 @@ export function ProfilesScreen() {
297
405
  }
298
406
  };
299
407
 
300
- const renderListItem = (
301
- entry: ProfileEntry,
302
- _idx: number,
303
- isSelected: boolean,
304
- ) => {
408
+ const renderListItem = (item: ListItem, _idx: number, isSelected: boolean) => {
409
+ if (item.kind === "predefined") {
410
+ const { profile } = item;
411
+ const pluginCount =
412
+ profile.magusPlugins.length + profile.anthropicPlugins.length;
413
+ const skillCount = profile.skills.length;
414
+
415
+ if (isSelected) {
416
+ return (
417
+ <text bg="blue" fg="white">
418
+ {" "}
419
+ {profile.icon} {profile.name} — {pluginCount} plugins · {skillCount}{" "}
420
+ skill{skillCount !== 1 ? "s" : ""}{" "}
421
+ </text>
422
+ );
423
+ }
424
+
425
+ return (
426
+ <text>
427
+ <span fg="blue">[preset]</span>
428
+ <span> </span>
429
+ <span fg="white">
430
+ {profile.icon} {profile.name}
431
+ </span>
432
+ <span fg="gray">
433
+ {" "}
434
+ — {pluginCount} plugins · {skillCount} skill
435
+ {skillCount !== 1 ? "s" : ""}
436
+ </span>
437
+ </text>
438
+ );
439
+ }
440
+
441
+ // Saved profile
442
+ const { entry } = item;
305
443
  const pluginCount = Object.keys(entry.plugins).length;
306
444
  const dateStr = formatDate(entry.updatedAt);
307
445
  const scopeColor = entry.scope === "user" ? "cyan" : "green";
@@ -341,24 +479,93 @@ export function ProfilesScreen() {
341
479
  );
342
480
  }
343
481
 
344
- if (profileList.length === 0) {
345
- return (
346
- <box flexDirection="column">
347
- <text fg="gray">No profiles yet.</text>
348
- <box marginTop={1}>
349
- <text fg="green">
350
- Press 's' in the Plugins screen to save the current selection as a
351
- profile.
352
- </text>
353
- </box>
354
- </box>
355
- );
482
+ if (!selectedItem) {
483
+ return <text fg="gray">Select a profile to see details</text>;
356
484
  }
357
485
 
358
- if (!selectedProfile) {
359
- return <text fg="gray">Select a profile to see details</text>;
486
+ if (selectedItem.kind === "predefined") {
487
+ return renderPredefinedDetail(selectedItem.profile);
360
488
  }
361
489
 
490
+ return renderSavedDetail(selectedItem.entry);
491
+ };
492
+
493
+ const renderPredefinedDetail = (profile: PredefinedProfile) => {
494
+ const allPlugins = [
495
+ ...profile.magusPlugins.map((p) => `${p}@magus`),
496
+ ...profile.anthropicPlugins.map((p) => `${p}@claude-plugins-official`),
497
+ ];
498
+
499
+ return (
500
+ <box flexDirection="column">
501
+ <text fg="blue">
502
+ <strong>
503
+ {profile.icon} {profile.name}
504
+ </strong>
505
+ </text>
506
+ <box marginTop={1}>
507
+ <text fg="gray">{profile.description}</text>
508
+ </box>
509
+ <box marginTop={1} flexDirection="column">
510
+ <text fg="gray">
511
+ Magus plugins ({profile.magusPlugins.length}):
512
+ </text>
513
+ {profile.magusPlugins.map((p) => (
514
+ <box key={p}>
515
+ <text fg="cyan"> {p}@magus</text>
516
+ </box>
517
+ ))}
518
+ </box>
519
+ <box marginTop={1} flexDirection="column">
520
+ <text fg="gray">
521
+ Anthropic plugins ({profile.anthropicPlugins.length}):
522
+ </text>
523
+ {profile.anthropicPlugins.map((p) => (
524
+ <box key={p}>
525
+ <text fg="yellow"> {p}@claude-plugins-official</text>
526
+ </box>
527
+ ))}
528
+ </box>
529
+ <box marginTop={1} flexDirection="column">
530
+ <text fg="gray">Skills ({profile.skills.length}):</text>
531
+ {profile.skills.map((s) => (
532
+ <box key={s}>
533
+ <text fg="white"> {s}</text>
534
+ </box>
535
+ ))}
536
+ </box>
537
+ <box marginTop={1} flexDirection="column">
538
+ <text fg="gray">
539
+ Settings ({Object.keys(profile.settings).length}):
540
+ </text>
541
+ {Object.entries(profile.settings)
542
+ .filter(([k]) => k !== "env")
543
+ .map(([k, v]) => (
544
+ <box key={k}>
545
+ <text fg="white">
546
+ {" "}
547
+ {k}: {String(v)}
548
+ </text>
549
+ </box>
550
+ ))}
551
+ </box>
552
+ <box marginTop={2} flexDirection="column">
553
+ <box>
554
+ <text bg="blue" fg="white">
555
+ {" "}
556
+ Enter/a{" "}
557
+ </text>
558
+ <text fg="gray">
559
+ {" "}
560
+ Apply (merges {allPlugins.length} plugins into project settings)
561
+ </text>
562
+ </box>
563
+ </box>
564
+ </box>
565
+ );
566
+ };
567
+
568
+ const renderSavedDetail = (selectedProfile: ProfileEntry) => {
362
569
  const plugins = Object.keys(selectedProfile.plugins);
363
570
  const scopeColor = selectedProfile.scope === "user" ? "cyan" : "green";
364
571
  const scopeLabel =
@@ -444,12 +651,13 @@ export function ProfilesScreen() {
444
651
 
445
652
  const statusContent = (
446
653
  <text>
447
- <span fg="gray">Profiles: </span>
654
+ <span fg="blue">{PREDEFINED_PROFILES.length} presets</span>
655
+ <span fg="gray"> + </span>
448
656
  <span fg="cyan">{userCount} user</span>
449
657
  <span fg="gray"> + </span>
450
658
  <span fg="green">{projCount} project</span>
451
659
  <span fg="gray"> = </span>
452
- <span fg="white">{profileCount} total</span>
660
+ <span fg="white">{PREDEFINED_PROFILES.length + profileCount} total</span>
453
661
  </text>
454
662
  );
455
663
 
@@ -460,24 +668,12 @@ export function ProfilesScreen() {
460
668
  statusLine={statusContent}
461
669
  footerHints="↑↓:nav │ Enter/a:apply │ r:rename │ d:delete │ c:copy │ i:import"
462
670
  listPanel={
463
- profileList.length === 0 &&
464
- profilesState.profiles.status !== "loading" ? (
465
- <box flexDirection="column">
466
- <text fg="gray">No profiles yet.</text>
467
- <box marginTop={1}>
468
- <text fg="green">
469
- Go to Plugins (1) and press 's' to save a profile.
470
- </text>
471
- </box>
472
- </box>
473
- ) : (
474
- <ScrollableList
475
- items={profileList}
476
- selectedIndex={profilesState.selectedIndex}
477
- renderItem={renderListItem}
478
- maxHeight={dimensions.listPanelHeight}
479
- />
480
- )
671
+ <ScrollableList
672
+ items={allItems}
673
+ selectedIndex={profilesState.selectedIndex}
674
+ renderItem={renderListItem}
675
+ maxHeight={dimensions.listPanelHeight}
676
+ />
481
677
  }
482
678
  detailPanel={renderDetail()}
483
679
  />