claudeup 4.30.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.
- package/README.md +98 -92
- package/package.json +4 -4
- package/src/__tests__/profile-materializer.test.ts +87 -0
- package/src/cli/install.ts +36 -5
- package/src/cli/router.ts +17 -9
- package/src/services/profile-materializer.ts +55 -4
package/README.md
CHANGED
|
@@ -1,128 +1,134 @@
|
|
|
1
1
|
# claudeup
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Manage a Claude Code setup — plugins, MCP servers, skills, CLI tools and the
|
|
4
|
+
binaries plugins depend on — from a TUI, a CLI, or a committed team manifest.
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## Install
|
|
6
7
|
|
|
7
8
|
```bash
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# Using pnpx (no install)
|
|
12
|
-
pnpx claudeup
|
|
13
|
-
|
|
14
|
-
# Using npm
|
|
15
|
-
npm install -g claudeup
|
|
16
|
-
|
|
17
|
-
# Using Bun
|
|
18
|
-
bun add -g claudeup
|
|
9
|
+
bun add -g claudeup # recommended
|
|
10
|
+
npm install -g claudeup # also works
|
|
19
11
|
```
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
### 1. MCP Server Setup
|
|
24
|
-
Configure Model Context Protocol servers with a curated list:
|
|
25
|
-
- **File System** - Read/write files, search directories
|
|
26
|
-
- **Database** - SQLite, PostgreSQL connections
|
|
27
|
-
- **Developer Tools** - GitHub, GitLab, Chrome DevTools, Puppeteer
|
|
28
|
-
- **API & Web** - HTTP requests, Brave Search
|
|
29
|
-
- **Productivity** - Google Drive, Slack, Memory
|
|
30
|
-
- **AI & Intelligence** - Claude Context (semantic search), Sequential Thinking
|
|
31
|
-
|
|
32
|
-
### 2. Plugin Marketplaces
|
|
33
|
-
Add official and community plugin marketplaces:
|
|
34
|
-
- **Anthropic Official** - Official Claude Code plugins
|
|
35
|
-
- **MadAppGang** - Frontend, Backend, Code Analysis plugins
|
|
36
|
-
|
|
37
|
-
### 3. Manage Plugins
|
|
38
|
-
Install and configure plugins from added marketplaces:
|
|
39
|
-
- Frontend Development (React/TypeScript)
|
|
40
|
-
- Code Analysis (Deep investigation)
|
|
41
|
-
- Bun Backend (TypeScript backend)
|
|
42
|
-
- Orchestration (Multi-agent patterns)
|
|
43
|
-
- Agent Development (Create agents)
|
|
44
|
-
|
|
45
|
-
### 4. Status Line Configuration
|
|
46
|
-
Configure the Claude Code status line with presets:
|
|
47
|
-
- Minimal, Standard, Detailed
|
|
48
|
-
- Git-aware, Token-focused
|
|
49
|
-
- Time-tracking, Developer
|
|
50
|
-
|
|
51
|
-
## Usage
|
|
13
|
+
Updates go through claudeup itself, not the package manager:
|
|
52
14
|
|
|
53
15
|
```bash
|
|
54
|
-
|
|
55
|
-
claudeup
|
|
56
|
-
|
|
57
|
-
# Or with npx
|
|
58
|
-
npx claudeup
|
|
16
|
+
claudeup update
|
|
59
17
|
```
|
|
60
18
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
- `Escape/q` - Go back / Exit
|
|
65
|
-
- `?` - Show help
|
|
66
|
-
- `1-4` - Quick jump to screens
|
|
19
|
+
You get a self-contained binary for your platform (an `optionalDependencies`
|
|
20
|
+
package gated on `os`/`cpu`). On a platform without a prebuilt binary the
|
|
21
|
+
launcher falls back to running from source under Bun, so the install still works.
|
|
67
22
|
|
|
68
|
-
##
|
|
23
|
+
## Two ways to use it
|
|
69
24
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
pnpm install
|
|
25
|
+
**Interactive** — run `claudeup` with no arguments for the TUI. Good for
|
|
26
|
+
exploring what is available and toggling things on one machine.
|
|
73
27
|
|
|
74
|
-
|
|
75
|
-
|
|
28
|
+
**Declarative** — commit a `.claude/profiles.json` and run `claudeup install`.
|
|
29
|
+
Good for making a teammate's machine match yours. This is the
|
|
30
|
+
[team configuration guide](docs/team-configuration.md).
|
|
76
31
|
|
|
77
|
-
|
|
78
|
-
pnpm start:bun # With Bun
|
|
79
|
-
pnpm start # With Node
|
|
80
|
-
```
|
|
32
|
+
## Commands
|
|
81
33
|
|
|
82
|
-
|
|
34
|
+
```
|
|
35
|
+
claudeup Open the interactive TUI
|
|
36
|
+
|
|
37
|
+
install [profile] Install a profile's plugins, binaries, skills and
|
|
38
|
+
env, then activate it. No argument = every profile.
|
|
39
|
+
--check Report drift only, write nothing (CI gate)
|
|
40
|
+
--yes, -y Skip the confirmation prompt
|
|
41
|
+
--force Discard unsynced local edits instead of refusing
|
|
42
|
+
profile list Show every profile; ● marks the active one
|
|
43
|
+
profile show <name> Print a profile's fully resolved closure
|
|
44
|
+
profile switch <name> Repoint the active profile — offline, no reinstall
|
|
45
|
+
profile sync Promote local edits back into .claude/profiles.json
|
|
46
|
+
doctor [--fix] Check binary deps, profile symlinks, conventions
|
|
47
|
+
|
|
48
|
+
claude [args...] Check for plugin updates (1h cache), then run claude
|
|
49
|
+
--force, -f Force the update check
|
|
50
|
+
update Update claudeup itself
|
|
51
|
+
--version, -v Version + update check
|
|
52
|
+
--help, -h This list
|
|
53
|
+
```
|
|
83
54
|
|
|
84
|
-
claudeup
|
|
55
|
+
`claudeup doctor` exits non-zero when it finds a problem, so it works as a CI
|
|
56
|
+
check. So does `claudeup install --check`.
|
|
85
57
|
|
|
86
|
-
|
|
87
|
-
- `.claude/settings.local.json` - Local settings (MCP servers, allowMcp)
|
|
58
|
+
## What the TUI covers
|
|
88
59
|
|
|
89
|
-
|
|
60
|
+
| Screen | What it manages |
|
|
61
|
+
|---|---|
|
|
62
|
+
| Plugins | Install, enable, disable; shows version changes and newly installed plugins |
|
|
63
|
+
| Skills | Browse and install skills from configured skill repos |
|
|
64
|
+
| MCP | Add MCP servers from a curated catalog |
|
|
65
|
+
| Settings | Claude Code settings, from a catalog of known keys |
|
|
66
|
+
| Profiles | Saved plugin sets (the TUI's own, older profile concept) |
|
|
67
|
+
| CLI Tools | Install claudish, mnemex and friends via the right package manager |
|
|
68
|
+
| Git State | Gitignore conventions and repo hygiene |
|
|
69
|
+
| Alias | Shell alias + flag management for launching `claude` |
|
|
90
70
|
|
|
91
|
-
|
|
71
|
+
Navigate with `↑/↓` or `j/k`, `Enter` to select, `r` to refresh, `?` for help,
|
|
72
|
+
`q`/`Escape` to go back. Number keys `1`–`8` jump straight to a screen.
|
|
92
73
|
|
|
93
|
-
|
|
74
|
+
## Files claudeup touches
|
|
94
75
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
76
|
+
| Path | Owner | What claudeup does |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `.claude/profiles.json` | you, committed | reads it; `profile sync` writes to it |
|
|
79
|
+
| `.claude/_profiles/<name>/` | claudeup, gitignored | generated build output |
|
|
80
|
+
| `.claude/settings.json` | Claude Code | in profile mode, a symlink into `_profiles/` |
|
|
81
|
+
| `.claude/settings.local.json` | you, gitignored | env values collected during install |
|
|
82
|
+
| `.mcp.json` | Claude Code | in profile mode, a symlink into `_profiles/` |
|
|
83
|
+
| `~/.claude/plugins/*` | Claude Code | never written directly — always via the `claude` CLI |
|
|
100
84
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
85
|
+
claudeup never hand-edits Claude Code's own registries
|
|
86
|
+
(`installed_plugins.json`, `known_marketplaces.json`, the plugin cache). Those go
|
|
87
|
+
through `claude plugin ...` so Claude Code stays the single writer.
|
|
104
88
|
|
|
105
|
-
|
|
89
|
+
## Development
|
|
106
90
|
|
|
107
91
|
```bash
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
92
|
+
bun install
|
|
93
|
+
bun test # full suite
|
|
94
|
+
bun run typecheck
|
|
95
|
+
bun run lint
|
|
96
|
+
bun run src/main.tsx # run from source
|
|
97
|
+
bun run build:binaries # cross-compile all platform binaries + their packages
|
|
113
98
|
```
|
|
114
99
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
100
|
+
Tests run per-file isolated in CI (`bun run test:ci`) because `mock.module`
|
|
101
|
+
leaks across files in a shared process.
|
|
102
|
+
|
|
103
|
+
**Do not bump `@opentui` past 0.1.x.** 0.4.x breaks `bun build --compile`: it
|
|
104
|
+
resolves tree-sitter workers eagerly, bypassing the `OTUI_ASSET_ROOT` override,
|
|
105
|
+
so a bare `import "@opentui/core"` crashes at import time in the compiled
|
|
106
|
+
binary. Re-run the compile spike before changing that pin.
|
|
118
107
|
|
|
119
|
-
|
|
108
|
+
## Releasing
|
|
109
|
+
|
|
110
|
+
Tag-driven. `.github/workflows/claudeup-release.yml` fires on `tools/claudeup/v*`:
|
|
111
|
+
|
|
112
|
+
1. **verify** — install, typecheck, `test:ci`, version and optional-deps gates
|
|
113
|
+
2. **binaries** — a matrix building each target on its **native** runner
|
|
114
|
+
(`bun --compile` cannot cross-compile in CI because opentui ships a
|
|
115
|
+
per-platform native library)
|
|
116
|
+
3. **publish** — platform packages first, main package last, so its
|
|
117
|
+
`optionalDependencies` already resolve
|
|
120
118
|
|
|
121
119
|
```bash
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
# bump "version" AND the three optionalDependencies to match
|
|
121
|
+
bun run check:optional-deps
|
|
122
|
+
git commit -am "feat(claudeup): vX.Y.Z - description"
|
|
123
|
+
git tag -a tools/claudeup/vX.Y.Z -m "Release message"
|
|
124
|
+
git push origin main --tags
|
|
124
125
|
```
|
|
125
126
|
|
|
127
|
+
Auth is npm OIDC trusted publishing — the workflow references no secrets. Adding
|
|
128
|
+
a **new** platform target needs a one-time manual publish to create the package
|
|
129
|
+
name (OIDC can only authenticate against a package that already exists); see
|
|
130
|
+
[`docs/npm-trusted-publishing-setup.md`](../../docs/npm-trusted-publishing-setup.md).
|
|
131
|
+
|
|
126
132
|
## License
|
|
127
133
|
|
|
128
134
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claudeup",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.31.0",
|
|
4
4
|
"description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.tsx",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"typescript": "^5.6.3"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
|
-
"claudeup-darwin-arm64": "4.
|
|
68
|
-
"claudeup-darwin-x64": "4.
|
|
69
|
-
"claudeup-linux-x64": "4.
|
|
67
|
+
"claudeup-darwin-arm64": "4.31.0",
|
|
68
|
+
"claudeup-darwin-x64": "4.31.0",
|
|
69
|
+
"claudeup-linux-x64": "4.31.0"
|
|
70
70
|
}
|
|
71
71
|
}
|
|
@@ -38,6 +38,36 @@ describe("buildProfileSettings", () => {
|
|
|
38
38
|
});
|
|
39
39
|
expect(s.enableAllProjectMcpServers).toBe(true);
|
|
40
40
|
});
|
|
41
|
+
|
|
42
|
+
// Regression: `install` enables the manifest-wide union at USER scope, and
|
|
43
|
+
// Claude Code resolves enabledPlugins per plugin id — an id the project
|
|
44
|
+
// scope omits falls through to the user scope's `true`. A profile that
|
|
45
|
+
// merely omits another profile's plugins therefore never disables them, so
|
|
46
|
+
// `profile switch` silently becomes additive instead of exclusive.
|
|
47
|
+
test("explicitly disables union plugins the profile excludes", () => {
|
|
48
|
+
const s = buildProfileSettings(
|
|
49
|
+
closure({ plugins: { "designer@magus": "latest" } }),
|
|
50
|
+
["designer@magus", "terminal@magus", "gtd@magus"],
|
|
51
|
+
);
|
|
52
|
+
expect(s.enabledPlugins).toEqual({
|
|
53
|
+
"designer@magus": true,
|
|
54
|
+
"terminal@magus": false,
|
|
55
|
+
"gtd@magus": false,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("a profile's own plugin wins over its union entry", () => {
|
|
60
|
+
const s = buildProfileSettings(
|
|
61
|
+
closure({ plugins: { "terminal@magus": "4.1.4" } }),
|
|
62
|
+
["terminal@magus"],
|
|
63
|
+
);
|
|
64
|
+
expect(s.enabledPlugins).toEqual({ "terminal@magus": true });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("no union given -> no false entries (unchanged legacy shape)", () => {
|
|
68
|
+
const s = buildProfileSettings(closure({ plugins: { "dev@magus": "latest" } }));
|
|
69
|
+
expect(s.enabledPlugins).toEqual({ "dev@magus": true });
|
|
70
|
+
});
|
|
41
71
|
});
|
|
42
72
|
|
|
43
73
|
describe("buildProfileMcp", () => {
|
|
@@ -79,4 +109,61 @@ describe("materializeProfile", () => {
|
|
|
79
109
|
const settings = await fs.readJson(join(dir, "settings.json"));
|
|
80
110
|
expect(settings.enabledPlugins).toEqual({ "b@m": true });
|
|
81
111
|
});
|
|
112
|
+
|
|
113
|
+
// Regression: activating a profile replaces .claude/skills with a symlink,
|
|
114
|
+
// and replacing means fs.remove first. A repo that committed its own project
|
|
115
|
+
// skills before adopting profiles would lose them at that moment.
|
|
116
|
+
test("seeds skills/ from the project's pre-existing .claude/skills", async () => {
|
|
117
|
+
const src = join(project, ".claude", "skills", "systematic-debugging");
|
|
118
|
+
await fs.outputFile(join(src, "SKILL.md"), "# committed project skill\n");
|
|
119
|
+
|
|
120
|
+
const dir = await materializeProfile("p", closure(), project);
|
|
121
|
+
|
|
122
|
+
expect(
|
|
123
|
+
await fs.readFile(join(dir, "skills", "systematic-debugging", "SKILL.md"), "utf8"),
|
|
124
|
+
).toContain("committed project skill");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("does not re-seed skills/ once the profile dir exists", async () => {
|
|
128
|
+
await fs.outputFile(
|
|
129
|
+
join(project, ".claude", "skills", "gone", "SKILL.md"),
|
|
130
|
+
"# removed later\n",
|
|
131
|
+
);
|
|
132
|
+
const dir = await materializeProfile("p", closure(), project);
|
|
133
|
+
await fs.remove(join(dir, "skills", "gone"));
|
|
134
|
+
|
|
135
|
+
await materializeProfile("p", closure(), project);
|
|
136
|
+
|
|
137
|
+
expect(await fs.pathExists(join(dir, "skills", "gone"))).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("still creates an empty skills/ when the project has none", async () => {
|
|
141
|
+
const dir = await materializeProfile("p", closure(), project);
|
|
142
|
+
expect(await fs.pathExists(join(dir, "skills"))).toBe(true);
|
|
143
|
+
expect(await fs.readdir(join(dir, "skills"))).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("two profiles from one union each disable the other's plugins", async () => {
|
|
147
|
+
const union = ["designer@magus", "terminal@magus"];
|
|
148
|
+
const fe = await materializeProfile(
|
|
149
|
+
"frontend",
|
|
150
|
+
closure({ plugins: { "designer@magus": "latest" } }),
|
|
151
|
+
project,
|
|
152
|
+
union,
|
|
153
|
+
);
|
|
154
|
+
const be = await materializeProfile(
|
|
155
|
+
"backend",
|
|
156
|
+
closure({ plugins: { "terminal@magus": "latest" } }),
|
|
157
|
+
project,
|
|
158
|
+
union,
|
|
159
|
+
);
|
|
160
|
+
expect((await fs.readJson(join(fe, "settings.json"))).enabledPlugins).toEqual({
|
|
161
|
+
"designer@magus": true,
|
|
162
|
+
"terminal@magus": false,
|
|
163
|
+
});
|
|
164
|
+
expect((await fs.readJson(join(be, "settings.json"))).enabledPlugins).toEqual({
|
|
165
|
+
"designer@magus": false,
|
|
166
|
+
"terminal@magus": true,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
82
169
|
});
|
package/src/cli/install.ts
CHANGED
|
@@ -126,7 +126,16 @@ async function runCheck(
|
|
|
126
126
|
// ── install steps ───────────────────────────────────────────────────────────
|
|
127
127
|
|
|
128
128
|
async function ensureToolchains(closure: ResolvedClosure, yes: boolean): Promise<void> {
|
|
129
|
-
|
|
129
|
+
// Only the binaries actually missing can need a toolchain. Checking all of
|
|
130
|
+
// them warns "pip isn't installed" on a machine whose pip-provided binaries
|
|
131
|
+
// are already on PATH — an alarming message about work that will not happen.
|
|
132
|
+
const missing: ResolvedBin[] = [];
|
|
133
|
+
for (const bin of closure.bins) {
|
|
134
|
+
if (!(await resolveExecutable(bin.name))) missing.push(bin);
|
|
135
|
+
}
|
|
136
|
+
if (missing.length === 0) return;
|
|
137
|
+
|
|
138
|
+
const toolchains = await detectToolchains(missing);
|
|
130
139
|
for (const tc of toolchains) {
|
|
131
140
|
if (tc.present) continue;
|
|
132
141
|
const bootstrap = TOOLCHAIN_BOOTSTRAP[tc.name];
|
|
@@ -160,11 +169,25 @@ async function registerMarketplaces(closure: ResolvedClosure): Promise<void> {
|
|
|
160
169
|
}
|
|
161
170
|
}
|
|
162
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Install the closure's plugins at PROJECT scope.
|
|
174
|
+
*
|
|
175
|
+
* A profile is a property of one repo, so its plugins must be too. At user
|
|
176
|
+
* scope, adopting profiles in a single repo would enable that repo's plugins in
|
|
177
|
+
* every other project on the machine — and since Claude Code resolves
|
|
178
|
+
* enabledPlugins per id with fall-through, those would stay on everywhere.
|
|
179
|
+
* Project scope keeps the blast radius to this repo. The plugin *cache* is
|
|
180
|
+
* global either way, so `profile switch` stays offline.
|
|
181
|
+
*
|
|
182
|
+
* Whatever the CLI writes into .claude/settings.json here is transient:
|
|
183
|
+
* materialization rewrites that file from the manifest, and activation replaces
|
|
184
|
+
* it with a symlink. The manifest stays authoritative.
|
|
185
|
+
*/
|
|
163
186
|
async function installPlugins(closure: ResolvedClosure): Promise<void> {
|
|
164
187
|
for (const pluginId of Object.keys(closure.plugins)) {
|
|
165
188
|
try {
|
|
166
189
|
console.log(`+ plugin ${pluginId}`);
|
|
167
|
-
await installPlugin(pluginId, "
|
|
190
|
+
await installPlugin(pluginId, "project");
|
|
168
191
|
} catch (e) {
|
|
169
192
|
console.warn(`⚠ plugin ${pluginId}: ${(e as Error).message}`);
|
|
170
193
|
}
|
|
@@ -317,10 +340,18 @@ export async function runInstallCommand(
|
|
|
317
340
|
|
|
318
341
|
// Materialize every profile (so `profile switch` needs no reinstall), then
|
|
319
342
|
// activate the target (or the sole/first profile).
|
|
320
|
-
|
|
321
|
-
|
|
343
|
+
//
|
|
344
|
+
// Every profile is materialized even when one was named: a profile arg scopes
|
|
345
|
+
// what gets *installed* and which becomes active, not which settings files
|
|
346
|
+
// exist. It also keeps the union below consistent across all of them — a
|
|
347
|
+
// profile left holding an older union would stop disabling plugins added
|
|
348
|
+
// since, silently making `switch` additive again.
|
|
349
|
+
const unionPluginIds = Object.keys(
|
|
350
|
+
(flags.profile ? await resolveAllProfiles(manifest) : closure).plugins,
|
|
351
|
+
);
|
|
352
|
+
for (const id of profileIds) {
|
|
322
353
|
const profileClosure = await resolveProfile(manifest, id);
|
|
323
|
-
await materializeProfile(id, profileClosure, projectPath);
|
|
354
|
+
await materializeProfile(id, profileClosure, projectPath, unionPluginIds);
|
|
324
355
|
}
|
|
325
356
|
|
|
326
357
|
// Keep the generated dir + active-profile symlinks out of git before
|
package/src/cli/router.ts
CHANGED
|
@@ -73,25 +73,33 @@ function printHelp(version: string): void {
|
|
|
73
73
|
|
|
74
74
|
TUI tool for managing Claude Code plugins, MCPs, and configuration.
|
|
75
75
|
|
|
76
|
-
Usage: claudeup
|
|
77
|
-
claudeup
|
|
78
|
-
claudeup update Update claudeup to latest version
|
|
76
|
+
Usage: claudeup Open the interactive TUI
|
|
77
|
+
claudeup <command> [args]
|
|
79
78
|
|
|
80
79
|
Options:
|
|
81
80
|
-v, --version Show version and check for updates
|
|
82
81
|
-h, --help Show this help message
|
|
83
82
|
--no-refresh Skip auto-refresh of marketplaces on startup
|
|
84
83
|
|
|
85
|
-
|
|
84
|
+
Team profiles — reproduce this repo's Claude Code setup on any machine.
|
|
85
|
+
The manifest is .claude/profiles.json, committed. See docs/team-configuration.md.
|
|
86
|
+
install [profile] Install the profile's plugins, binaries, skills and env,
|
|
87
|
+
then activate it. No argument installs every profile.
|
|
88
|
+
--check Report drift only, write nothing (exits 1 in strict mode)
|
|
89
|
+
--yes, -y Skip the confirmation prompt
|
|
90
|
+
--force Discard unsynced local edits instead of refusing
|
|
91
|
+
profile list Show every profile; ● marks the active one
|
|
92
|
+
profile show <n> Print a profile's fully resolved closure
|
|
93
|
+
profile switch <n> Repoint the active profile — offline, no reinstall
|
|
94
|
+
profile sync Promote local edits back into .claude/profiles.json
|
|
95
|
+
doctor Check binary deps, profile symlinks, and conventions
|
|
96
|
+
--fix Apply the repairs it can make itself
|
|
97
|
+
|
|
98
|
+
Other commands:
|
|
86
99
|
claude [args...] Check for plugin updates (1h cache), then run claude
|
|
87
100
|
-f, --force Force update check (bypass 1h cache)
|
|
88
101
|
update Update claudeup itself to latest version
|
|
89
102
|
|
|
90
|
-
Experimental (in development):
|
|
91
|
-
install [profile] Install a team profile from .claude/profiles.json
|
|
92
|
-
profile <cmd> list | show | switch | sync team profiles
|
|
93
|
-
doctor Diagnose & repair plugin/marketplace/profile state
|
|
94
|
-
|
|
95
103
|
Navigation (TUI):
|
|
96
104
|
[1] Plugins [4] Settings [7] Git State
|
|
97
105
|
[2] Skills [5] Profiles [8] Alias
|
|
@@ -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(
|
|
54
|
-
|
|
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
|
-
|
|
108
|
+
const skillsDir = path.join(dir, "skills");
|
|
109
|
+
await seedSkillsFromProject(skillsDir, projectPath);
|
|
110
|
+
await fs.ensureDir(skillsDir);
|
|
60
111
|
return dir;
|
|
61
112
|
}
|