magus-cli 7.4.1 → 7.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/package.json +5 -4
- package/src/__tests__/install-command.test.ts +4 -3
- package/src/__tests__/models-cli.test.ts +4 -7
- package/src/__tests__/models-manager.test.ts +52 -40
- package/src/__tests__/profile-absorb.test.ts +1210 -0
- package/src/__tests__/profile-command.test.ts +90 -32
- package/src/__tests__/profile-dependencies.test.ts +442 -0
- package/src/__tests__/profile-manifest-write.test.ts +231 -0
- package/src/__tests__/profile-migration-offer.test.ts +18 -15
- package/src/__tests__/profile-migration.test.ts +246 -117
- package/src/__tests__/profile-render.test.ts +141 -0
- package/src/__tests__/profile-skills.test.ts +524 -0
- package/src/__tests__/profile-startup.test.ts +44 -14
- package/src/__tests__/profile-state.test.ts +1110 -0
- package/src/__tests__/settings-file.test.ts +5 -7
- package/src/__tests__/stale-load-repair.test.ts +105 -0
- package/src/__tests__/styles-manager.test.ts +47 -74
- package/src/__tests__/styles-screen-state.test.ts +11 -10
- package/src/__tests__/support/preload.ts +14 -0
- package/src/__tests__/update-apply.test.ts +145 -0
- package/src/cli/bootstrap.ts +91 -27
- package/src/cli/doctor.ts +165 -38
- package/src/cli/install.ts +126 -122
- package/src/cli/models.ts +21 -1
- package/src/cli/profile.ts +48 -77
- package/src/cli/router.ts +39 -6
- package/src/cli/update.ts +46 -6
- package/src/data/gitignore-defaults.ts +3 -3
- package/src/services/active-closure.ts +3 -3
- package/src/services/claude-cli.ts +24 -0
- package/src/services/claude-settings.ts +27 -0
- package/src/services/conventions-manager.ts +88 -0
- package/src/services/git-worktree.ts +113 -9
- package/src/services/manifest.ts +224 -8
- package/src/services/models-manager.ts +36 -73
- package/src/services/plugin-dependencies.ts +48 -0
- package/src/services/plugin-requires.ts +137 -14
- package/src/services/profile-absorb.ts +1586 -0
- package/src/services/profile-activate.ts +492 -94
- package/src/services/profile-live.ts +884 -0
- package/src/services/profile-render.ts +182 -0
- package/src/services/profile-skills.ts +487 -0
- package/src/services/profile-startup.ts +88 -16
- package/src/services/profile-state.ts +706 -0
- package/src/services/profiles.ts +26 -37
- package/src/services/resolver.ts +100 -2
- package/src/services/settings-file.ts +5 -5
- package/src/services/skills-manager.ts +14 -8
- package/src/services/styles-manager.ts +45 -28
- package/src/services/tracked-live-files.ts +125 -0
- package/src/services/update-engine.ts +87 -2
- package/src/types/index.ts +34 -9
- package/src/ui/App.tsx +25 -2
- package/src/ui/hooks/useProfileStartupModal.ts +20 -15
- package/src/ui/screens/PluginsScreen.tsx +11 -2
- package/src/ui/screens/ProfilesScreen.tsx +84 -29
- package/src/ui/screens/StylesScreen.tsx +2 -1
- package/src/ui/state/types.ts +1 -1
- package/src/__tests__/profile-materializer.test.ts +0 -238
- package/src/__tests__/profile-sync.test.ts +0 -154
- package/src/__tests__/symlink-manager.test.ts +0 -180
- package/src/services/profile-materializer.ts +0 -133
- package/src/services/profile-sync.ts +0 -158
- package/src/services/symlink-manager.ts +0 -343
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* ## One source of truth, not two
|
|
5
5
|
*
|
|
6
|
-
* A profiled project's `.claude/settings.json` is
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* A profiled project's `.claude/settings.json` is rendered from the manifest,
|
|
7
|
+
* so the manifest is the truth and `enabledPlugins` is its output. (Claude
|
|
8
|
+
* Code may since have written to it; that is a change to the active profile,
|
|
9
|
+
* absorbed into the manifest before anything reads it — see
|
|
10
|
+
* `profile-absorb.ts` — not a question asked here.)
|
|
9
11
|
*
|
|
10
12
|
* An earlier version of this file compared the two — the manifest against the
|
|
11
13
|
* plugins `enabledPlugins` lists — and offered to create a new profile when
|
|
@@ -20,11 +22,13 @@
|
|
|
20
22
|
* a set of declared plugins to a set of installed ones — they are different
|
|
21
23
|
* kinds of thing and a comparison between them cannot be acted on.
|
|
22
24
|
*
|
|
23
|
-
* ## What is left is a
|
|
25
|
+
* ## What is left is a small set of states
|
|
24
26
|
*
|
|
25
27
|
* ok a profile is active; the manifest is in effect; ask nothing
|
|
26
28
|
* unmigrated no manifest — the project is not using profiles at all
|
|
27
|
-
* inactive a manifest with profiles, none of them
|
|
29
|
+
* inactive a manifest with profiles, none of them activated
|
|
30
|
+
* unknown the active-profile marker names a profile the manifest does
|
|
31
|
+
* not define; the only way on is activating one that exists
|
|
28
32
|
* unparseable a manifest that exists and is not valid JSON, or is valid
|
|
29
33
|
* JSON in the wrong shape (see `readProfilesFile`)
|
|
30
34
|
*
|
|
@@ -38,8 +42,12 @@
|
|
|
38
42
|
* manifest over whatever the file held. So it offers nothing but the way out,
|
|
39
43
|
* and names the file to fix.
|
|
40
44
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
45
|
+
* `detectProfileStartupNeed` is detection only — nothing it does writes, which
|
|
46
|
+
* is what makes it testable without a renderer. `prepareProfileStartup` is
|
|
47
|
+
* the TUI's entry point: it runs the same gate as every CLI command first
|
|
48
|
+
* (`prepareProfileState` — restore a lost marker, convert an older layout,
|
|
49
|
+
* absorb Claude Code's edits, regenerate from a changed manifest), then
|
|
50
|
+
* detects.
|
|
43
51
|
*/
|
|
44
52
|
|
|
45
53
|
import type { ProfileManifest } from "../types/index.js";
|
|
@@ -51,14 +59,20 @@ import type { SelectOption } from "../ui/state/types.js";
|
|
|
51
59
|
import { MANIFEST_FILE, emptyManifest, readManifest } from "./manifest.js";
|
|
52
60
|
import type { ProfileChoice } from "./profile-activate.js";
|
|
53
61
|
import { freeProfileId } from "./profile-adopt.js";
|
|
62
|
+
import {
|
|
63
|
+
activeProfile,
|
|
64
|
+
displaceableArtifacts,
|
|
65
|
+
namedActiveProfile,
|
|
66
|
+
} from "./profile-live.js";
|
|
67
|
+
import { prepareProfileState } from "./profile-state.js";
|
|
54
68
|
import { SettingsParseError } from "./settings-file.js";
|
|
55
|
-
import { activeProfile, displaceableArtifacts } from "./symlink-manager.js";
|
|
56
69
|
|
|
57
70
|
/** Whether the project can be worked on, and if not, why not. */
|
|
58
71
|
export type ProfileStartupState =
|
|
59
72
|
| "ok"
|
|
60
73
|
| "unmigrated"
|
|
61
74
|
| "inactive"
|
|
75
|
+
| "unknown"
|
|
62
76
|
| "unparseable";
|
|
63
77
|
|
|
64
78
|
/** A profile the user could activate. */
|
|
@@ -81,8 +95,13 @@ export interface ProfileStartupNeed {
|
|
|
81
95
|
* Null when a profile is already active. Never an id already in use.
|
|
82
96
|
*/
|
|
83
97
|
adopt: string | null;
|
|
84
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Profiles that could be activated. Empty unless the state is `inactive` or
|
|
100
|
+
* `unknown`.
|
|
101
|
+
*/
|
|
85
102
|
activate: ActivatableProfile[];
|
|
103
|
+
/** For `unknown`: the profile the marker names. */
|
|
104
|
+
unknownProfile?: string;
|
|
86
105
|
/**
|
|
87
106
|
* The profile this project declares it wants, if the manifest names one that
|
|
88
107
|
* exists. Used to preselect the row so Enter does the intended thing.
|
|
@@ -159,8 +178,8 @@ export async function detectProfileStartupNeed(
|
|
|
159
178
|
}
|
|
160
179
|
|
|
161
180
|
// The ONE question that decides whether the manifest is in effect. Not a
|
|
162
|
-
// comparison, not a heuristic: either
|
|
163
|
-
// profile or it
|
|
181
|
+
// comparison, not a heuristic: either the active-profile marker names a
|
|
182
|
+
// materialized profile or it does not (see `activeProfile`).
|
|
164
183
|
if ((await activeProfile(projectPath)) !== null) {
|
|
165
184
|
return {
|
|
166
185
|
state: "ok",
|
|
@@ -172,6 +191,28 @@ export async function detectProfileStartupNeed(
|
|
|
172
191
|
};
|
|
173
192
|
}
|
|
174
193
|
|
|
194
|
+
const activate = ids.map((id) => ({
|
|
195
|
+
id,
|
|
196
|
+
name: manifest.profiles[id]?.name ?? id,
|
|
197
|
+
pluginCount: Object.keys(manifest.profiles[id]?.plugins ?? {}).length,
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
// A marker naming a profile the manifest does not define: magus must not act
|
|
201
|
+
// on it, and saving "what you have" would capture the output of a profile
|
|
202
|
+
// that no longer exists. The only way on is activating one that does.
|
|
203
|
+
const named = await namedActiveProfile(projectPath);
|
|
204
|
+
if (named !== null) {
|
|
205
|
+
return {
|
|
206
|
+
state: "unknown",
|
|
207
|
+
adopt: null,
|
|
208
|
+
activate,
|
|
209
|
+
declaredDefault: null,
|
|
210
|
+
autoActivate: false,
|
|
211
|
+
unknownProfile: named.profile,
|
|
212
|
+
manifest,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
175
216
|
// Validated, not trusted: a `default` naming a profile that was renamed or
|
|
176
217
|
// removed must fall back to asking rather than throw somewhere downstream.
|
|
177
218
|
// `validateManifest` reports the typo; this keeps magus usable meanwhile.
|
|
@@ -196,15 +237,39 @@ export async function detectProfileStartupNeed(
|
|
|
196
237
|
// magus can no longer form an opinion on it without the comparison that
|
|
197
238
|
// used to be here.
|
|
198
239
|
adopt: freeProfileId(manifest),
|
|
199
|
-
activate
|
|
200
|
-
id,
|
|
201
|
-
name: manifest.profiles[id]?.name ?? id,
|
|
202
|
-
pluginCount: Object.keys(manifest.profiles[id]?.plugins ?? {}).length,
|
|
203
|
-
})),
|
|
240
|
+
activate,
|
|
204
241
|
manifest,
|
|
205
242
|
};
|
|
206
243
|
}
|
|
207
244
|
|
|
245
|
+
/** What the TUI shows at startup, beyond the question (if any). */
|
|
246
|
+
export interface ProfileStartup {
|
|
247
|
+
need: ProfileStartupNeed;
|
|
248
|
+
/** One line per thing the gate did: absorbed edits, regeneration, repairs. */
|
|
249
|
+
notices: string[];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The TUI's startup: the same gate every CLI command runs, then detection.
|
|
254
|
+
*
|
|
255
|
+
* A failure in the gate never blocks the TUI — its message becomes a notice
|
|
256
|
+
* and detection still runs, so the user is asked whatever is left to ask.
|
|
257
|
+
*/
|
|
258
|
+
export async function prepareProfileStartup(
|
|
259
|
+
projectPath: string,
|
|
260
|
+
): Promise<ProfileStartup> {
|
|
261
|
+
const notices: string[] = [];
|
|
262
|
+
try {
|
|
263
|
+
const prep = await prepareProfileState(projectPath);
|
|
264
|
+
notices.push(...prep.lines, ...prep.problems);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
if (!(error instanceof SettingsParseError)) {
|
|
267
|
+
notices.push(error instanceof Error ? error.message : String(error));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return { need: await detectProfileStartupNeed(projectPath), notices };
|
|
271
|
+
}
|
|
272
|
+
|
|
208
273
|
// ─── What to offer, as data both surfaces render ─────────────────────────────
|
|
209
274
|
|
|
210
275
|
/** Option values are parsed back, so they are data, not prose. */
|
|
@@ -284,6 +349,13 @@ export function describeNeed(need: ProfileStartupNeed): {
|
|
|
284
349
|
};
|
|
285
350
|
}
|
|
286
351
|
|
|
352
|
+
if (need.state === "unknown") {
|
|
353
|
+
return {
|
|
354
|
+
title: "Active profile no longer exists",
|
|
355
|
+
message: `This project was running the profile "${need.unknownProfile ?? "?"}", which the team's profiles file no longer defines.\n\nActivate one of the profiles below. It replaces the plugins, MCP servers and settings turned on here with that profile's.`,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
287
359
|
if (need.state === "unmigrated") {
|
|
288
360
|
return {
|
|
289
361
|
title: "magus uses profiles",
|