claudeup 4.42.0 → 5.0.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 +73 -3
- package/bin/claudeup.js +16 -3
- package/package.json +4 -4
- package/scripts/build-binaries.ts +12 -1
- package/src/__tests__/cli-router.test.ts +46 -0
- package/src/__tests__/dotenv.test.ts +190 -0
- package/src/__tests__/profile-adopt.test.ts +177 -0
- package/src/__tests__/toolchain.test.ts +31 -0
- package/src/__tests__/update-apply.test.ts +223 -0
- package/src/__tests__/update-plan.test.ts +631 -0
- package/src/cli/bootstrap.ts +140 -0
- package/src/cli/install.ts +65 -83
- package/src/cli/profile.ts +41 -9
- package/src/cli/prompt.ts +42 -0
- package/src/cli/router.ts +23 -9
- package/src/cli/update.ts +459 -71
- package/src/cli/upgrade.ts +89 -0
- package/src/main.tsx +18 -0
- package/src/prerunner/index.ts +1 -22
- package/src/services/dotenv.ts +176 -0
- package/src/services/marketplace-sync.ts +37 -6
- package/src/services/plugin-manager.ts +50 -0
- package/src/services/profile-adopt.ts +206 -0
- package/src/services/resolver.ts +1 -1
- package/src/services/toolchain.ts +35 -0
- package/src/services/update-plan.ts +385 -0
- package/src/ui/App.tsx +1 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest bootstrap — the "this repo has no profiles yet" path.
|
|
3
|
+
*
|
|
4
|
+
* `install` and `update` both require `.claude/profiles.json`. Rather than
|
|
5
|
+
* giving each a manifest-free fallback (two behaviours to keep in step, and a
|
|
6
|
+
* repo that never gets a committed manifest), a repo without one is offered
|
|
7
|
+
* adoption: claudeup writes the manifest that describes the project as it
|
|
8
|
+
* stands, then the original command proceeds against it normally.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
getManifestPath,
|
|
13
|
+
readManifest,
|
|
14
|
+
writeManifest,
|
|
15
|
+
} from "../services/manifest.js";
|
|
16
|
+
import {
|
|
17
|
+
type AdoptResult,
|
|
18
|
+
adoptCurrentProject,
|
|
19
|
+
} from "../services/profile-adopt.js";
|
|
20
|
+
import type { ProfileManifest } from "../types/index.js";
|
|
21
|
+
import { confirm } from "./prompt.js";
|
|
22
|
+
|
|
23
|
+
/** Print what adoption would capture, so the user can judge it before writing. */
|
|
24
|
+
function printAdoptionPlan(result: AdoptResult): void {
|
|
25
|
+
console.log(
|
|
26
|
+
"\nThis project has no .claude/profiles.json — claudeup can create one from its current setup.\n",
|
|
27
|
+
);
|
|
28
|
+
console.log(` profile ${result.profileId} (${result.entry.name})`);
|
|
29
|
+
console.log(
|
|
30
|
+
` plugins ${result.counts.plugins} enabled, adopted as "latest"`,
|
|
31
|
+
);
|
|
32
|
+
if (result.source === "user") {
|
|
33
|
+
// Never let this be invisible: the generated file is meant to be committed,
|
|
34
|
+
// and these came from ~/.claude/settings.json — this machine's personal
|
|
35
|
+
// global set, not anything this repo chose.
|
|
36
|
+
console.log(
|
|
37
|
+
" ⚠ read from ~/.claude/settings.json (user scope) — this project",
|
|
38
|
+
);
|
|
39
|
+
console.log(
|
|
40
|
+
" enables none of its own. Review before committing.",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (result.counts.mcpServers > 0) {
|
|
44
|
+
console.log(` mcp servers ${result.counts.mcpServers} from .mcp.json`);
|
|
45
|
+
}
|
|
46
|
+
if (result.counts.settingsKeys > 0) {
|
|
47
|
+
console.log(
|
|
48
|
+
` settings ${result.counts.settingsKeys} keys carried over`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
for (const id of result.skipped) {
|
|
52
|
+
console.log(
|
|
53
|
+
` ⚠ skipped ${id} — no marketplace in its id, not reproducible`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
for (const hit of result.machinePaths) {
|
|
57
|
+
console.log(` ⚠ machine path ${hit}`);
|
|
58
|
+
}
|
|
59
|
+
if (result.machinePaths.length > 0) {
|
|
60
|
+
console.log(
|
|
61
|
+
" Those name a directory on THIS machine and will not resolve on another.",
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
console.log(
|
|
65
|
+
"\nCommit the file to share this setup with the team; edit it to pin versions.",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface EnsureManifestOptions {
|
|
70
|
+
/**
|
|
71
|
+
* Skip confirmation prompts. Deliberately does NOT authorize adoption —
|
|
72
|
+
* `--yes` means "I already know what this will do", and nobody passing it to
|
|
73
|
+
* an install has decided what their team's committed manifest should contain.
|
|
74
|
+
*/
|
|
75
|
+
yes?: boolean;
|
|
76
|
+
/** Read-only mode: never write, just report that adoption is needed. */
|
|
77
|
+
check?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Adopt without prompting. Set only by `claudeup profile init`, where
|
|
80
|
+
* creating the manifest IS the command the user asked for.
|
|
81
|
+
*/
|
|
82
|
+
allowAdopt?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Return the project's manifest, creating one by adoption if it has none.
|
|
87
|
+
*
|
|
88
|
+
* Resolves to null when there is no manifest and none was created — the caller
|
|
89
|
+
* should exit non-zero. Anything it prints has already explained why.
|
|
90
|
+
*/
|
|
91
|
+
export async function ensureManifest(
|
|
92
|
+
projectPath: string,
|
|
93
|
+
opts: EnsureManifestOptions = {},
|
|
94
|
+
): Promise<ProfileManifest | null> {
|
|
95
|
+
const manifest = await readManifest(projectPath);
|
|
96
|
+
if (Object.keys(manifest.profiles).length > 0) return manifest;
|
|
97
|
+
|
|
98
|
+
const result = await adoptCurrentProject(projectPath);
|
|
99
|
+
|
|
100
|
+
if (result.counts.plugins === 0) {
|
|
101
|
+
console.error(
|
|
102
|
+
`No .claude/profiles.json in ${projectPath}, and no enabled plugins to adopt one from.`,
|
|
103
|
+
);
|
|
104
|
+
console.error(
|
|
105
|
+
"Open claudeup and select the plugins this project needs, then re-run.",
|
|
106
|
+
);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
printAdoptionPlan(result);
|
|
111
|
+
|
|
112
|
+
if (opts.check) {
|
|
113
|
+
console.error(
|
|
114
|
+
"\n--check is read-only and will not create the manifest. Run `claudeup install` once to adopt.",
|
|
115
|
+
);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// `--yes` alone is NOT consent to author this file. It means "do not ask me
|
|
120
|
+
// about the thing I invoked", and someone running `claudeup update --yes` in
|
|
121
|
+
// CI invoked an update, not the creation of their team's committed contract —
|
|
122
|
+
// which this would otherwise write unreviewed, possibly from the machine's
|
|
123
|
+
// user-scope plugin set. Creating it needs either a human at the prompt or
|
|
124
|
+
// the command whose whole purpose is to create it.
|
|
125
|
+
if (opts.allowAdopt) {
|
|
126
|
+
// fall through to the write
|
|
127
|
+
} else if (!process.stdin.isTTY) {
|
|
128
|
+
console.error(
|
|
129
|
+
"\nRefusing to create .claude/profiles.json without a human: run `claudeup profile init` to create it deliberately.",
|
|
130
|
+
);
|
|
131
|
+
return null;
|
|
132
|
+
} else if (!(await confirm("\nCreate .claude/profiles.json?"))) {
|
|
133
|
+
console.log("Aborted — nothing written.");
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
await writeManifest(result.manifest, projectPath);
|
|
138
|
+
console.log(`✓ Wrote ${getManifestPath(projectPath)}\n`);
|
|
139
|
+
return result.manifest;
|
|
140
|
+
}
|
package/src/cli/install.ts
CHANGED
|
@@ -13,46 +13,43 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import path from "node:path";
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from "../
|
|
23
|
-
import { readManifest, validateManifest } from "../services/manifest.js";
|
|
24
|
-
import { resolveAllProfiles, resolveProfile } from "../services/resolver.js";
|
|
16
|
+
import { installPlugin, isClaudeAvailable } from "../services/claude-cli.js";
|
|
17
|
+
import {
|
|
18
|
+
readInstalledPluginsRegistry,
|
|
19
|
+
readLocalSettings,
|
|
20
|
+
writeLocalSettings,
|
|
21
|
+
} from "../services/claude-settings.js";
|
|
22
|
+
import { ensureGitignoreEntries } from "../services/conventions-manager.js";
|
|
25
23
|
import {
|
|
26
24
|
computeEnvStatus,
|
|
27
25
|
computeVersionDrift,
|
|
28
26
|
summarizeClosure,
|
|
29
27
|
} from "../services/install-plan.js";
|
|
30
|
-
import {
|
|
31
|
-
|
|
32
|
-
detectToolchains,
|
|
33
|
-
TOOLCHAIN_BOOTSTRAP,
|
|
34
|
-
} from "../services/toolchain.js";
|
|
28
|
+
import { validateManifest } from "../services/manifest.js";
|
|
29
|
+
import { registerClosureMarketplaces } from "../services/marketplace-sync.js";
|
|
35
30
|
import { materializeProfile } from "../services/profile-materializer.js";
|
|
31
|
+
import { detectProfileDrift } from "../services/profile-sync.js";
|
|
32
|
+
import { resolveAllProfiles, resolveProfile } from "../services/resolver.js";
|
|
33
|
+
import { installSkill } from "../services/skills-manager.js";
|
|
36
34
|
import {
|
|
35
|
+
PROFILE_GITIGNORE_ENTRIES,
|
|
37
36
|
activateProfile,
|
|
38
37
|
activeProfile,
|
|
39
|
-
PROFILE_GITIGNORE_ENTRIES,
|
|
40
38
|
} from "../services/symlink-manager.js";
|
|
41
|
-
import { ensureGitignoreEntries } from "../services/conventions-manager.js";
|
|
42
|
-
import { detectProfileDrift } from "../services/profile-sync.js";
|
|
43
|
-
import { resolveExecutable } from "../utils/command-utils.js";
|
|
44
39
|
import {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
} from "../services/
|
|
49
|
-
import {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
} from "../
|
|
55
|
-
import {
|
|
40
|
+
TOOLCHAIN_BOOTSTRAP,
|
|
41
|
+
binInstallCommand,
|
|
42
|
+
detectToolchains,
|
|
43
|
+
} from "../services/toolchain.js";
|
|
44
|
+
import type {
|
|
45
|
+
ProfileSkillRef,
|
|
46
|
+
ResolvedBin,
|
|
47
|
+
ResolvedClosure,
|
|
48
|
+
SkillInfo,
|
|
49
|
+
} from "../types/index.js";
|
|
50
|
+
import { resolveExecutable } from "../utils/command-utils.js";
|
|
51
|
+
import { ensureManifest } from "./bootstrap.js";
|
|
52
|
+
import { confirm, promptValue, runShell } from "./prompt.js";
|
|
56
53
|
|
|
57
54
|
interface InstallFlags {
|
|
58
55
|
check: boolean;
|
|
@@ -70,35 +67,6 @@ function parseArgs(args: string[]): InstallFlags {
|
|
|
70
67
|
};
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
async function confirm(question: string): Promise<boolean> {
|
|
74
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
75
|
-
try {
|
|
76
|
-
const answer = (await rl.question(`${question} [Y/n] `)).trim().toLowerCase();
|
|
77
|
-
return answer === "" || answer === "y" || answer === "yes";
|
|
78
|
-
} finally {
|
|
79
|
-
rl.close();
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function promptValue(question: string): Promise<string> {
|
|
84
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
85
|
-
try {
|
|
86
|
-
return (await rl.question(question)).trim();
|
|
87
|
-
} finally {
|
|
88
|
-
rl.close();
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Run a shell command, streaming output; resolve true on exit 0. */
|
|
93
|
-
async function runShell(command: string): Promise<boolean> {
|
|
94
|
-
const { spawn } = await import("node:child_process");
|
|
95
|
-
return new Promise((resolve) => {
|
|
96
|
-
const child = spawn(command, { stdio: "inherit", shell: true });
|
|
97
|
-
child.on("exit", (code) => resolve(code === 0));
|
|
98
|
-
child.on("error", () => resolve(false));
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
|
|
102
70
|
// ── --check: read-only drift gate ───────────────────────────────────────────
|
|
103
71
|
|
|
104
72
|
async function runCheck(
|
|
@@ -113,7 +81,9 @@ async function runCheck(
|
|
|
113
81
|
}
|
|
114
82
|
console.log("Plugin version drift:");
|
|
115
83
|
for (const d of drift) {
|
|
116
|
-
console.log(
|
|
84
|
+
console.log(
|
|
85
|
+
` ✗ ${d.pluginId} pinned ${d.pinned}, installed ${d.installed ?? "none"}`,
|
|
86
|
+
);
|
|
117
87
|
}
|
|
118
88
|
if (strict) {
|
|
119
89
|
console.error("\nstrictVersions is on — failing.");
|
|
@@ -125,7 +95,10 @@ async function runCheck(
|
|
|
125
95
|
|
|
126
96
|
// ── install steps ───────────────────────────────────────────────────────────
|
|
127
97
|
|
|
128
|
-
async function ensureToolchains(
|
|
98
|
+
async function ensureToolchains(
|
|
99
|
+
closure: ResolvedClosure,
|
|
100
|
+
yes: boolean,
|
|
101
|
+
): Promise<void> {
|
|
129
102
|
// Only the binaries actually missing can need a toolchain. Checking all of
|
|
130
103
|
// them warns "pip isn't installed" on a machine whose pip-provided binaries
|
|
131
104
|
// are already on PATH — an alarming message about work that will not happen.
|
|
@@ -145,7 +118,11 @@ async function ensureToolchains(closure: ResolvedClosure, yes: boolean): Promise
|
|
|
145
118
|
);
|
|
146
119
|
continue;
|
|
147
120
|
}
|
|
148
|
-
const ok =
|
|
121
|
+
const ok =
|
|
122
|
+
yes ||
|
|
123
|
+
(await confirm(
|
|
124
|
+
`${tc.name} is missing (needed by ${tc.requiredBy.join(", ")}). Install it?`,
|
|
125
|
+
));
|
|
149
126
|
if (ok) {
|
|
150
127
|
console.log(`Installing ${tc.name}…`);
|
|
151
128
|
if (!(await runShell(bootstrap))) {
|
|
@@ -155,20 +132,6 @@ async function ensureToolchains(closure: ResolvedClosure, yes: boolean): Promise
|
|
|
155
132
|
}
|
|
156
133
|
}
|
|
157
134
|
|
|
158
|
-
async function registerMarketplaces(closure: ResolvedClosure): Promise<void> {
|
|
159
|
-
for (const [name, ref] of Object.entries(closure.marketplaces)) {
|
|
160
|
-
try {
|
|
161
|
-
if (await isMarketplaceRegistered(name)) continue;
|
|
162
|
-
const source = ref.source === "github" ? ref.repo : ref.path;
|
|
163
|
-
if (!source) continue;
|
|
164
|
-
console.log(`+ marketplace ${name} (${source})`);
|
|
165
|
-
await addMarketplace(source);
|
|
166
|
-
} catch (e) {
|
|
167
|
-
console.warn(`⚠ marketplace ${name}: ${(e as Error).message}`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
135
|
/**
|
|
173
136
|
* Install the closure's plugins at PROJECT scope.
|
|
174
137
|
*
|
|
@@ -212,7 +175,11 @@ function toSkillInfo(ref: ProfileSkillRef): SkillInfo {
|
|
|
212
175
|
return {
|
|
213
176
|
id: `${ref.repo}/${ref.path}`,
|
|
214
177
|
name: ref.name,
|
|
215
|
-
source: {
|
|
178
|
+
source: {
|
|
179
|
+
label: ref.repo,
|
|
180
|
+
repo: ref.repo,
|
|
181
|
+
skillsPath: path.dirname(ref.path),
|
|
182
|
+
},
|
|
216
183
|
repoPath: ref.path,
|
|
217
184
|
gitBlobSha: "",
|
|
218
185
|
frontmatter: null,
|
|
@@ -246,8 +213,11 @@ async function collectEnv(
|
|
|
246
213
|
|
|
247
214
|
console.log("\nMissing required environment variables:");
|
|
248
215
|
if (yes) {
|
|
249
|
-
for (const name of status.missingRequired)
|
|
250
|
-
|
|
216
|
+
for (const name of status.missingRequired)
|
|
217
|
+
console.log(` ! ${name} (unset)`);
|
|
218
|
+
console.log(
|
|
219
|
+
"Run without --yes to enter values, or set them in .claude/settings.local.json.",
|
|
220
|
+
);
|
|
251
221
|
return;
|
|
252
222
|
}
|
|
253
223
|
|
|
@@ -269,7 +239,14 @@ export async function runInstallCommand(
|
|
|
269
239
|
): Promise<number> {
|
|
270
240
|
const flags = parseArgs(args);
|
|
271
241
|
|
|
272
|
-
|
|
242
|
+
// A repo with no manifest is offered adoption rather than an error — see
|
|
243
|
+
// cli/bootstrap.ts for why there is no manifest-free install path.
|
|
244
|
+
const manifest = await ensureManifest(projectPath, {
|
|
245
|
+
yes: flags.yes,
|
|
246
|
+
check: flags.check,
|
|
247
|
+
});
|
|
248
|
+
if (!manifest) return 1;
|
|
249
|
+
|
|
273
250
|
const errors = validateManifest(manifest);
|
|
274
251
|
if (errors.length > 0) {
|
|
275
252
|
console.error("Invalid .claude/profiles.json:");
|
|
@@ -318,7 +295,9 @@ export async function runInstallCommand(
|
|
|
318
295
|
console.log(`\nPlan for ${flags.profile ?? "all profiles"}:`);
|
|
319
296
|
for (const line of summarizeClosure(closure)) console.log(` ${line}`);
|
|
320
297
|
if (closure.conflicts?.length) {
|
|
321
|
-
console.log(
|
|
298
|
+
console.log(
|
|
299
|
+
"\n⚠ Version conflicts across profiles (resolved to the higher pin):",
|
|
300
|
+
);
|
|
322
301
|
for (const c of closure.conflicts) console.log(` ${c}`);
|
|
323
302
|
}
|
|
324
303
|
console.log();
|
|
@@ -329,12 +308,14 @@ export async function runInstallCommand(
|
|
|
329
308
|
}
|
|
330
309
|
|
|
331
310
|
if (!(await isClaudeAvailable())) {
|
|
332
|
-
console.error(
|
|
311
|
+
console.error(
|
|
312
|
+
"claude CLI not found on PATH — cannot install plugins/marketplaces.",
|
|
313
|
+
);
|
|
333
314
|
return 1;
|
|
334
315
|
}
|
|
335
316
|
|
|
336
317
|
await ensureToolchains(closure, flags.yes);
|
|
337
|
-
await
|
|
318
|
+
await registerClosureMarketplaces(closure.marketplaces);
|
|
338
319
|
await installPlugins(closure);
|
|
339
320
|
await installBins(closure.bins);
|
|
340
321
|
|
|
@@ -362,7 +343,8 @@ export async function runInstallCommand(
|
|
|
362
343
|
"# claudeup: generated profile build output + active-profile symlinks",
|
|
363
344
|
);
|
|
364
345
|
|
|
365
|
-
const activeId =
|
|
346
|
+
const activeId =
|
|
347
|
+
flags.profile ?? (await pickActiveProfile(profileIds, flags.yes));
|
|
366
348
|
if (activeId) {
|
|
367
349
|
await activateProfile(activeId, projectPath);
|
|
368
350
|
console.log(`\n✓ Active profile: ${activeId}`);
|
package/src/cli/profile.ts
CHANGED
|
@@ -8,19 +8,20 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import path from "node:path";
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
11
|
+
import fs from "fs-extra";
|
|
12
|
+
import { ensureGitignoreEntries } from "../services/conventions-manager.js";
|
|
13
|
+
import { checkBinaries, missingBinaries } from "../services/doctor-bins.js";
|
|
13
14
|
import { summarizeClosure } from "../services/install-plan.js";
|
|
15
|
+
import { getManifestPath, readManifest } from "../services/manifest.js";
|
|
16
|
+
import { syncProfile } from "../services/profile-sync.js";
|
|
17
|
+
import { resolveProfile } from "../services/resolver.js";
|
|
14
18
|
import {
|
|
19
|
+
PROFILE_GITIGNORE_ENTRIES,
|
|
15
20
|
activateProfile,
|
|
16
21
|
activeProfile,
|
|
17
22
|
profileDir,
|
|
18
|
-
PROFILE_GITIGNORE_ENTRIES,
|
|
19
23
|
} from "../services/symlink-manager.js";
|
|
20
|
-
import {
|
|
21
|
-
import { syncProfile } from "../services/profile-sync.js";
|
|
22
|
-
import { checkBinaries, missingBinaries } from "../services/doctor-bins.js";
|
|
23
|
-
import fs from "fs-extra";
|
|
24
|
+
import { ensureManifest } from "./bootstrap.js";
|
|
24
25
|
|
|
25
26
|
export async function runProfileCommand(args: string[]): Promise<number> {
|
|
26
27
|
const [sub, ...rest] = args;
|
|
@@ -34,16 +35,46 @@ export async function runProfileCommand(args: string[]): Promise<number> {
|
|
|
34
35
|
return showProfile(rest[0], projectPath);
|
|
35
36
|
case "switch":
|
|
36
37
|
return switchProfile(rest[0], projectPath);
|
|
38
|
+
case "init":
|
|
39
|
+
return initProfile(
|
|
40
|
+
args.includes("--yes") || args.includes("-y"),
|
|
41
|
+
projectPath,
|
|
42
|
+
);
|
|
37
43
|
case "sync":
|
|
38
44
|
return syncActiveProfile(rest[0], projectPath);
|
|
39
45
|
default:
|
|
40
46
|
console.error(
|
|
41
|
-
`Unknown profile command "${sub}". Use: list | show <name> | switch <name> | sync`,
|
|
47
|
+
`Unknown profile command "${sub}". Use: list | show <name> | switch <name> | init | sync`,
|
|
42
48
|
);
|
|
43
49
|
return 1;
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
52
|
|
|
53
|
+
/**
|
|
54
|
+
* `profile init` — write a manifest describing this project as it stands.
|
|
55
|
+
*
|
|
56
|
+
* The same adoption `install`/`update` offer, available on its own so a repo
|
|
57
|
+
* can be brought under profile management without installing anything.
|
|
58
|
+
*/
|
|
59
|
+
export async function initProfile(
|
|
60
|
+
yes: boolean,
|
|
61
|
+
projectPath: string,
|
|
62
|
+
): Promise<number> {
|
|
63
|
+
const existing = await readManifest(projectPath);
|
|
64
|
+
if (Object.keys(existing.profiles).length > 0) {
|
|
65
|
+
console.error(
|
|
66
|
+
`${getManifestPath(projectPath)} already defines: ${Object.keys(existing.profiles).join(", ")}`,
|
|
67
|
+
);
|
|
68
|
+
console.error(
|
|
69
|
+
`Use \`claudeup profile sync\` to promote local edits into it.`,
|
|
70
|
+
);
|
|
71
|
+
return 1;
|
|
72
|
+
}
|
|
73
|
+
// `profile init` exists to create the manifest, so it is the one caller that
|
|
74
|
+
// may do so unattended.
|
|
75
|
+
return (await ensureManifest(projectPath, { yes, allowAdopt: yes })) ? 0 : 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
47
78
|
export async function listProfiles(projectPath: string): Promise<number> {
|
|
48
79
|
const manifest = await readManifest(projectPath);
|
|
49
80
|
const ids = Object.keys(manifest.profiles);
|
|
@@ -129,7 +160,8 @@ export async function switchProfile(
|
|
|
129
160
|
console.log(
|
|
130
161
|
`\n⚠ ${missing.length} binary dependenc${missing.length === 1 ? "y is" : "ies are"} missing:`,
|
|
131
162
|
);
|
|
132
|
-
for (const m of missing)
|
|
163
|
+
for (const m of missing)
|
|
164
|
+
console.log(` ✗ ${m.name} — run: ${m.installCommand}`);
|
|
133
165
|
console.log(`Run \`claudeup install ${name}\` to install them.`);
|
|
134
166
|
}
|
|
135
167
|
} catch {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal prompting shared by the non-interactive commands.
|
|
3
|
+
*
|
|
4
|
+
* These lived inside cli/install.ts until `update` and the adopt flow needed
|
|
5
|
+
* the same three. One copy, so a fix to (say) the Y/n default reaches every
|
|
6
|
+
* command at once.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createInterface } from "node:readline/promises";
|
|
10
|
+
|
|
11
|
+
/** Y/n prompt. Empty input means yes. */
|
|
12
|
+
export async function confirm(question: string): Promise<boolean> {
|
|
13
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
14
|
+
try {
|
|
15
|
+
const answer = (await rl.question(`${question} [Y/n] `))
|
|
16
|
+
.trim()
|
|
17
|
+
.toLowerCase();
|
|
18
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
19
|
+
} finally {
|
|
20
|
+
rl.close();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Free-text prompt, trimmed. */
|
|
25
|
+
export async function promptValue(question: string): Promise<string> {
|
|
26
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
27
|
+
try {
|
|
28
|
+
return (await rl.question(question)).trim();
|
|
29
|
+
} finally {
|
|
30
|
+
rl.close();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Run a shell command, streaming output; resolve true on exit 0. */
|
|
35
|
+
export async function runShell(command: string): Promise<boolean> {
|
|
36
|
+
const { spawn } = await import("node:child_process");
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
const child = spawn(command, { stdio: "inherit", shell: true });
|
|
39
|
+
child.on("exit", (code) => resolve(code === 0));
|
|
40
|
+
child.on("error", () => resolve(false));
|
|
41
|
+
});
|
|
42
|
+
}
|
package/src/cli/router.ts
CHANGED
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
import { checkForUpdates } from "../services/version-check.js";
|
|
10
10
|
import { runClaudeCommand } from "./claude.js";
|
|
11
|
-
import {
|
|
11
|
+
import { runDoctorCommand } from "./doctor.js";
|
|
12
12
|
import { runInstallCommand } from "./install.js";
|
|
13
13
|
import { runProfileCommand } from "./profile.js";
|
|
14
|
-
import {
|
|
14
|
+
import { runUpdateCommand } from "./update.js";
|
|
15
|
+
import { runUpgradeCommand } from "./upgrade.js";
|
|
15
16
|
|
|
16
17
|
export interface RouteOutcome {
|
|
17
18
|
/** A subcommand (or top-level flag) handled the invocation. */
|
|
@@ -39,7 +40,7 @@ export async function route(
|
|
|
39
40
|
const result = await checkForUpdates();
|
|
40
41
|
if (result.updateAvailable) {
|
|
41
42
|
console.log(`\nUpdate available: v${result.latestVersion}`);
|
|
42
|
-
console.log("Run: claudeup
|
|
43
|
+
console.log("Run: claudeup upgrade");
|
|
43
44
|
}
|
|
44
45
|
return HANDLED(0);
|
|
45
46
|
}
|
|
@@ -55,7 +56,9 @@ export async function route(
|
|
|
55
56
|
case "claude":
|
|
56
57
|
return HANDLED(await runClaudeCommand(rest));
|
|
57
58
|
case "update":
|
|
58
|
-
return HANDLED(await runUpdateCommand());
|
|
59
|
+
return HANDLED(await runUpdateCommand(rest));
|
|
60
|
+
case "upgrade":
|
|
61
|
+
return HANDLED(await runUpgradeCommand());
|
|
59
62
|
case "install":
|
|
60
63
|
return HANDLED(await runInstallCommand(rest));
|
|
61
64
|
case "profile":
|
|
@@ -81,16 +84,27 @@ Options:
|
|
|
81
84
|
-h, --help Show this help message
|
|
82
85
|
--no-refresh Skip auto-refresh of marketplaces on startup
|
|
83
86
|
|
|
84
|
-
|
|
85
|
-
The manifest is .claude/profiles.json, committed
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
Dependency management — keep this project's plugins, CLI tools and skills
|
|
88
|
+
installed and current. The manifest is .claude/profiles.json, committed; a repo
|
|
89
|
+
without one is offered adoption on first run. See docs/team-configuration.md.
|
|
90
|
+
install [profile] Install the profile's plugins, binaries, skills and env at
|
|
91
|
+
the manifest's PINNED versions, then activate it.
|
|
92
|
+
No argument installs every profile.
|
|
88
93
|
--check Report drift only, write nothing (exits 1 in strict mode)
|
|
89
94
|
--yes, -y Skip the confirmation prompt
|
|
90
95
|
--force Discard unsynced local edits instead of refusing
|
|
96
|
+
update [profile] Keep the ACTIVE profile current: install whatever it
|
|
97
|
+
declares but this machine lacks, and advance anything
|
|
98
|
+
behind the version its marketplace publishes. Only the
|
|
99
|
+
newest version is installable, so a pin naming an older
|
|
100
|
+
one is reported, not chased.
|
|
101
|
+
--check Report only, write nothing (exits 1 if anything is
|
|
102
|
+
missing or known to be behind)
|
|
103
|
+
--yes, -y Skip the confirmation prompt
|
|
91
104
|
profile list Show every profile; ● marks the active one
|
|
92
105
|
profile show <n> Print a profile's fully resolved closure
|
|
93
106
|
profile switch <n> Repoint the active profile — offline, no reinstall
|
|
107
|
+
profile init Write .claude/profiles.json from this project's setup
|
|
94
108
|
profile sync Promote local edits back into .claude/profiles.json
|
|
95
109
|
doctor Check binary deps, profile symlinks, and conventions
|
|
96
110
|
--fix Apply the repairs it can make itself
|
|
@@ -98,7 +112,7 @@ The manifest is .claude/profiles.json, committed. See docs/team-configuration.md
|
|
|
98
112
|
Other commands:
|
|
99
113
|
claude [args...] Check for plugin updates (1h cache), then run claude
|
|
100
114
|
-f, --force Force update check (bypass 1h cache)
|
|
101
|
-
|
|
115
|
+
upgrade Update claudeup ITSELF to the latest version
|
|
102
116
|
|
|
103
117
|
Navigation (TUI):
|
|
104
118
|
[1] Plugins [4] Settings [7] Git State
|