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
package/src/cli/bootstrap.ts
CHANGED
|
@@ -15,8 +15,10 @@ import {
|
|
|
15
15
|
writeManifest,
|
|
16
16
|
} from "../services/manifest.js";
|
|
17
17
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
type ActivationResult,
|
|
19
|
+
activateProfile,
|
|
20
|
+
activationNotes,
|
|
21
|
+
assertNoTrackedWriteTargets,
|
|
20
22
|
reportDisplaced,
|
|
21
23
|
} from "../services/profile-activate.js";
|
|
22
24
|
import {
|
|
@@ -31,6 +33,13 @@ import {
|
|
|
31
33
|
needsNothing,
|
|
32
34
|
parseProfileChoice,
|
|
33
35
|
} from "../services/profile-startup.js";
|
|
36
|
+
import {
|
|
37
|
+
applyProfileChoice,
|
|
38
|
+
describeUnknownProfile,
|
|
39
|
+
prepareProfileState,
|
|
40
|
+
suggestedProfile,
|
|
41
|
+
switchHint,
|
|
42
|
+
} from "../services/profile-state.js";
|
|
34
43
|
import { SettingsParseError } from "../services/settings-file.js";
|
|
35
44
|
import type { ProfileManifest } from "../types/index.js";
|
|
36
45
|
import { dim } from "./ansi.js";
|
|
@@ -57,8 +66,8 @@ export function reportUnparseable(error: SettingsParseError): void {
|
|
|
57
66
|
* Write the adopted manifest, then put the project ON the profile it describes.
|
|
58
67
|
*
|
|
59
68
|
* Both halves or neither. Writing alone is what made migrating feel like a
|
|
60
|
-
* no-op: `activeProfile()`
|
|
61
|
-
*
|
|
69
|
+
* no-op: `activeProfile()` reads the marker activation writes, so a manifest
|
|
70
|
+
* nothing activated changes no behaviour, and the run that just migrated still has to
|
|
62
71
|
* ask which profile to use.
|
|
63
72
|
*
|
|
64
73
|
* Activation is reported in full, including anything it moved aside, because it
|
|
@@ -75,24 +84,80 @@ async function writeAndActivate(
|
|
|
75
84
|
// their hand-written files went.
|
|
76
85
|
const rel = (p: string) => path.relative(projectPath, p);
|
|
77
86
|
|
|
87
|
+
// Before the manifest is written: adopting activates, and activating a
|
|
88
|
+
// project whose settings.json git tracks would take that file over.
|
|
89
|
+
await assertNoTrackedWriteTargets(projectPath);
|
|
78
90
|
await writeManifest(result.manifest, projectPath);
|
|
79
91
|
console.log(`✓ Wrote ${rel(getManifestPath(projectPath))}`);
|
|
80
92
|
|
|
81
93
|
// Each move is printed as it happens, not from the returned list afterwards.
|
|
82
|
-
// Activation
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
await materializeAndActivate(
|
|
94
|
+
// Activation writes three files in sequence, so a throw on the third would
|
|
95
|
+
// discard the record of the first two — and the first of those is the
|
|
96
|
+
// user's `.claude/settings.json`, now absent from where they left it.
|
|
97
|
+
const activation = await activateProfile(
|
|
87
98
|
result.manifest,
|
|
88
99
|
result.profileId,
|
|
89
100
|
projectPath,
|
|
90
|
-
reportDisplaced(projectPath),
|
|
101
|
+
{ onDisplace: reportDisplaced(projectPath) },
|
|
91
102
|
);
|
|
92
103
|
console.log(`✓ Active profile: ${result.profileId}`);
|
|
104
|
+
printActivationNotes(activation);
|
|
93
105
|
console.log();
|
|
94
106
|
}
|
|
95
107
|
|
|
108
|
+
/** Print what activation did beyond the switch itself, one line each. */
|
|
109
|
+
export function printActivationNotes(result: ActivationResult): void {
|
|
110
|
+
for (const line of activationNotes(result)) console.log(noteLine(line));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface ProfileGateOptions {
|
|
114
|
+
/**
|
|
115
|
+
* Carry on when the marker names a profile profiles.json does not define.
|
|
116
|
+
* Only for commands that fix exactly that by activating a profile that
|
|
117
|
+
* exists — `profile switch`, `install`.
|
|
118
|
+
*/
|
|
119
|
+
allowUnknown?: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The gate every CLI command that reads or writes profile state passes first.
|
|
124
|
+
*
|
|
125
|
+
* Restores a lost marker when the live config matches exactly one profile,
|
|
126
|
+
* converts an older layout, absorbs what Claude Code wrote to the live config
|
|
127
|
+
* into the active profile, and regenerates the live config when profiles.json
|
|
128
|
+
* changed — printing one line for each. Returns false, having said why, when
|
|
129
|
+
* the command must not act: a marker naming a profile that does not exist, or
|
|
130
|
+
* a live file that cannot be read (it would be overwritten).
|
|
131
|
+
*/
|
|
132
|
+
export async function profileGate(
|
|
133
|
+
projectPath: string,
|
|
134
|
+
opts: ProfileGateOptions = {},
|
|
135
|
+
): Promise<boolean> {
|
|
136
|
+
let prep: Awaited<ReturnType<typeof prepareProfileState>>;
|
|
137
|
+
try {
|
|
138
|
+
prep = await prepareProfileState(projectPath);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (error instanceof SettingsParseError) {
|
|
141
|
+
reportUnparseable(error);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
console.error(
|
|
145
|
+
failLine(error instanceof Error ? error.message : String(error)),
|
|
146
|
+
);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
for (const line of prep.lines) console.log(noteLine(line));
|
|
150
|
+
if (prep.problems.length > 0) {
|
|
151
|
+
for (const problem of prep.problems) console.error(failLine(problem));
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if (prep.check.state === "unknown" && !opts.allowUnknown) {
|
|
155
|
+
console.error(failLine(describeUnknownProfile(prep.check)));
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
|
|
96
161
|
/** Print what adoption would capture, so the user can judge it before writing. */
|
|
97
162
|
function printAdoptionPlan(result: AdoptResult): void {
|
|
98
163
|
console.log(
|
|
@@ -141,12 +206,11 @@ function printAdoptionPlan(result: AdoptResult): void {
|
|
|
141
206
|
// onto the profile, which replaces two files that are in the working tree
|
|
142
207
|
// right now. The prompt below is the only place to decline.
|
|
143
208
|
console.log(
|
|
144
|
-
"The project then runs this profile: .claude/settings.json
|
|
209
|
+
"The project then runs this profile: .claude/settings.json and .mcp.json are",
|
|
145
210
|
);
|
|
146
211
|
console.log(
|
|
147
|
-
"
|
|
212
|
+
"generated from it, and your current copies are kept where the run reports.",
|
|
148
213
|
);
|
|
149
|
-
console.log("the run reports.");
|
|
150
214
|
}
|
|
151
215
|
|
|
152
216
|
export interface EnsureManifestOptions {
|
|
@@ -231,7 +295,7 @@ export async function ensureManifest(
|
|
|
231
295
|
* state is read from and where updates are written. With none there is nothing
|
|
232
296
|
* to read and nowhere to write, so there is no useful work to do — and the
|
|
233
297
|
* previous version of this offered exactly that anyway. It asked only when the
|
|
234
|
-
* manifest
|
|
298
|
+
* manifest disagreed with `enabledPlugins`, let the user decline, and `update` then carried on with
|
|
235
299
|
* a profile "for this run only", resolving against a manifest the project was
|
|
236
300
|
* not running. Two worlds live at once, which is the confusion being removed.
|
|
237
301
|
*
|
|
@@ -262,15 +326,16 @@ export async function requireActiveProfile(
|
|
|
262
326
|
// `--check` and with no TTY: this is the committed answer being applied, not
|
|
263
327
|
// a decision being made, and it is exactly the case CI hits.
|
|
264
328
|
if (need.autoActivate && need.declaredDefault !== null && !opts.check) {
|
|
265
|
-
const
|
|
329
|
+
const result = await applyProfileChoice(
|
|
266
330
|
{ kind: "activate", id: need.declaredDefault },
|
|
267
331
|
need.manifest,
|
|
268
332
|
projectPath,
|
|
269
|
-
reportDisplaced(projectPath),
|
|
333
|
+
{ onDisplace: reportDisplaced(projectPath) },
|
|
270
334
|
);
|
|
271
335
|
console.log(
|
|
272
|
-
`✓ Active profile: ${profileId} ${dim("(declared by this project)")}`,
|
|
336
|
+
`✓ Active profile: ${result.profileId} ${dim("(declared by this project)")}`,
|
|
273
337
|
);
|
|
338
|
+
printActivationNotes(result);
|
|
274
339
|
console.log("");
|
|
275
340
|
return readManifest(projectPath);
|
|
276
341
|
}
|
|
@@ -292,9 +357,10 @@ export async function requireActiveProfile(
|
|
|
292
357
|
noteLine(
|
|
293
358
|
need.state === "unmigrated"
|
|
294
359
|
? "Run `magus profile init` to create one deliberately."
|
|
295
|
-
: `Run
|
|
296
|
-
.map((p) => p.id)
|
|
297
|
-
.
|
|
360
|
+
: `Run ${switchHint(
|
|
361
|
+
need.activate.map((p) => p.id),
|
|
362
|
+
need.declaredDefault ?? suggestedProfile(need.manifest),
|
|
363
|
+
)} first.`,
|
|
298
364
|
),
|
|
299
365
|
);
|
|
300
366
|
return null;
|
|
@@ -316,13 +382,11 @@ export async function requireActiveProfile(
|
|
|
316
382
|
return null;
|
|
317
383
|
}
|
|
318
384
|
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
);
|
|
325
|
-
console.log(`✓ Active profile: ${profileId}`);
|
|
385
|
+
const result = await applyProfileChoice(choice, need.manifest, projectPath, {
|
|
386
|
+
onDisplace: reportDisplaced(projectPath),
|
|
387
|
+
});
|
|
388
|
+
console.log(`✓ Active profile: ${result.profileId}`);
|
|
389
|
+
printActivationNotes(result);
|
|
326
390
|
console.log("");
|
|
327
391
|
|
|
328
392
|
// Re-read rather than return `need.manifest`: adopting wrote a new profile
|
package/src/cli/doctor.ts
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
* 0. Global settings — `~/.claude/settings.json` parses.
|
|
6
6
|
* 1. Binary dependencies — every requires.bin / cliTools binary on PATH.
|
|
7
7
|
* This is the check that would have caught a dead `tmux-mcp`.
|
|
8
|
-
* 2. Profile
|
|
8
|
+
* 2. Profile state — which profile is active (strictly), what the next magus
|
|
9
|
+
* command would record or regenerate (with `--fix`, it does it now and
|
|
10
|
+
* says what it did), generated files git still tracks, and
|
|
11
|
+
* magus-installed skills that are missing. Without `--fix` this section
|
|
12
|
+
* writes nothing: no absorbing, no converting an older layout.
|
|
9
13
|
* 3. Convention compliance — .gitignore + CLAUDE.md blocks (ported engine).
|
|
10
14
|
* 4. Model routing — config validity, hook registration, and drift.
|
|
11
15
|
* 5. Plugin loads — what Claude Code will load here vs what is installed.
|
|
@@ -42,8 +46,31 @@ import {
|
|
|
42
46
|
type RepoRootCache,
|
|
43
47
|
pickLoadedEntry,
|
|
44
48
|
} from "../services/plugin-resolution.js";
|
|
49
|
+
import { trackedGeneratedFiles } from "../services/profile-activate.js";
|
|
50
|
+
import {
|
|
51
|
+
LIVE_FILE_KEYS,
|
|
52
|
+
liveKind,
|
|
53
|
+
livePath,
|
|
54
|
+
readActiveMarker,
|
|
55
|
+
} from "../services/profile-live.js";
|
|
56
|
+
import {
|
|
57
|
+
describeDependencies,
|
|
58
|
+
renderProfile,
|
|
59
|
+
} from "../services/profile-render.js";
|
|
60
|
+
import { inspectManagedSkills } from "../services/profile-skills.js";
|
|
61
|
+
import {
|
|
62
|
+
type ActiveProfileCheck,
|
|
63
|
+
describeUnknownProfile,
|
|
64
|
+
describeUnmarked,
|
|
65
|
+
inspectProfileState,
|
|
66
|
+
prepareProfileState,
|
|
67
|
+
switchHint,
|
|
68
|
+
} from "../services/profile-state.js";
|
|
45
69
|
import { resolveAllProfiles } from "../services/resolver.js";
|
|
46
|
-
import {
|
|
70
|
+
import {
|
|
71
|
+
GitCheckError,
|
|
72
|
+
untrackCommand,
|
|
73
|
+
} from "../services/tracked-live-files.js";
|
|
47
74
|
import type {
|
|
48
75
|
ClaudeSettings,
|
|
49
76
|
InstalledPluginsRegistry,
|
|
@@ -98,18 +125,11 @@ export async function runDoctorCommand(args: string[]): Promise<number> {
|
|
|
98
125
|
}
|
|
99
126
|
}
|
|
100
127
|
|
|
101
|
-
// 2. Profile
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
console.log(`Profile "${active}" has broken symlinks:`);
|
|
107
|
-
for (const l of dangling) console.log(` ✗ ${l}`);
|
|
108
|
-
console.log(` Fix: magus install ${active}`);
|
|
109
|
-
problems += dangling.length;
|
|
110
|
-
} else {
|
|
111
|
-
console.log(`✓ Active profile "${active}" symlinks resolve.`);
|
|
112
|
-
}
|
|
128
|
+
// 2. Profile state.
|
|
129
|
+
if (manifest) {
|
|
130
|
+
const profile = await reportProfileState(projectPath, fix);
|
|
131
|
+
problems += profile.fixable;
|
|
132
|
+
unfixable += profile.unfixable;
|
|
113
133
|
}
|
|
114
134
|
|
|
115
135
|
// 3. Convention compliance — scoped to THIS project only (the ported engine
|
|
@@ -418,32 +438,139 @@ async function checkModels(
|
|
|
418
438
|
return { fixable: problems, unfixable };
|
|
419
439
|
}
|
|
420
440
|
|
|
421
|
-
/**
|
|
422
|
-
|
|
423
|
-
|
|
441
|
+
/**
|
|
442
|
+
* Section 2: which profile is active, and whether its live config is whole.
|
|
443
|
+
*
|
|
444
|
+
* Runs the same gate as every other command first — restoring a lost marker,
|
|
445
|
+
* converting an older layout, absorbing what Claude Code wrote into the live
|
|
446
|
+
* config and regenerating from a changed profiles.json — and reports what it
|
|
447
|
+
* did. What is left is reported plainly, each with its fix.
|
|
448
|
+
*/
|
|
449
|
+
export async function reportProfileState(
|
|
424
450
|
projectPath: string,
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
{
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (stat.isSymbolicLink() && !(await fs.pathExists(target))) {
|
|
442
|
-
broken.push(path.relative(projectPath, link));
|
|
451
|
+
fix: boolean,
|
|
452
|
+
): Promise<{ fixable: number; unfixable: number }> {
|
|
453
|
+
const rel = (p: string) => path.relative(projectPath, p);
|
|
454
|
+
let fixable = 0;
|
|
455
|
+
let unfixable = 0;
|
|
456
|
+
let check: ActiveProfileCheck;
|
|
457
|
+
try {
|
|
458
|
+
if (fix) {
|
|
459
|
+
// `--fix`: the same gate every other command runs, reported.
|
|
460
|
+
const prep = await prepareProfileState(projectPath);
|
|
461
|
+
check = prep.check;
|
|
462
|
+
if (check.state === "no-profiles") return { fixable, unfixable };
|
|
463
|
+
for (const line of prep.lines) console.log(`• ${line}`);
|
|
464
|
+
for (const problem of prep.problems) {
|
|
465
|
+
console.log(`✗ ${problem}`);
|
|
466
|
+
unfixable++;
|
|
443
467
|
}
|
|
444
|
-
}
|
|
445
|
-
//
|
|
468
|
+
} else {
|
|
469
|
+
// Report only. Nothing here writes — not an absorb, not a marker, not
|
|
470
|
+
// a conversion: a diagnostic that changes the project is not one.
|
|
471
|
+
const report = await inspectProfileState(projectPath);
|
|
472
|
+
check = report.check;
|
|
473
|
+
if (check.state === "no-profiles") return { fixable, unfixable };
|
|
474
|
+
for (const error of report.unreadable) {
|
|
475
|
+
console.log(`✗ ${error.message}`);
|
|
476
|
+
console.log(
|
|
477
|
+
" Fix the JSON by hand; magus leaves the file as it is.",
|
|
478
|
+
);
|
|
479
|
+
unfixable++;
|
|
480
|
+
}
|
|
481
|
+
// What the next magus command will do on its own — record an edit,
|
|
482
|
+
// regenerate, convert, restore a lost marker — is information, not
|
|
483
|
+
// a fault: none of it makes doctor exit non-zero.
|
|
484
|
+
for (const line of report.pending) console.log(`• ${line}`);
|
|
485
|
+
}
|
|
486
|
+
} catch (error) {
|
|
487
|
+
console.log("\nProfile:");
|
|
488
|
+
console.log(
|
|
489
|
+
` ✗ ${error instanceof Error ? error.message : String(error)}`,
|
|
490
|
+
);
|
|
491
|
+
return { fixable: 0, unfixable: 1 };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (check.state === "unknown") {
|
|
495
|
+
console.log(`✗ ${describeUnknownProfile(check)}`);
|
|
496
|
+
unfixable++;
|
|
497
|
+
} else if (check.state === "unmarked" && check.match !== null) {
|
|
498
|
+
// The live config matches exactly one profile: the pending line above
|
|
499
|
+
// already says the next command restores the marker. Not a fault.
|
|
500
|
+
} else if (check.state === "unmarked") {
|
|
501
|
+
console.log(
|
|
502
|
+
check.liveFiles
|
|
503
|
+
? `✗ ${describeUnmarked(check)}`
|
|
504
|
+
: `✗ No profile is active yet. Run \`magus install\` or ${switchHint(check.available, check.suggested)}.`,
|
|
505
|
+
);
|
|
506
|
+
unfixable++;
|
|
507
|
+
} else if (check.state === "active" || check.state === "old-layout") {
|
|
508
|
+
const broken: string[] = [];
|
|
509
|
+
for (const key of LIVE_FILE_KEYS) {
|
|
510
|
+
const kind = await liveKind(key, projectPath);
|
|
511
|
+
// An older layout's links are reported as pending conversion above.
|
|
512
|
+
if (
|
|
513
|
+
kind === "other" ||
|
|
514
|
+
(kind === "symlink" && check.state === "active")
|
|
515
|
+
) {
|
|
516
|
+
broken.push(`${rel(livePath(key, projectPath))} is not a regular file`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const marker = await readActiveMarker(projectPath);
|
|
520
|
+
if (marker) {
|
|
521
|
+
const { missing } = await inspectManagedSkills(
|
|
522
|
+
marker.skills,
|
|
523
|
+
projectPath,
|
|
524
|
+
);
|
|
525
|
+
for (const folder of missing) {
|
|
526
|
+
broken.push(`skill .claude/skills/${folder} is missing`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (broken.length > 0) {
|
|
530
|
+
console.log(`Profile "${check.profile}" is not fully in place:`);
|
|
531
|
+
for (const line of broken) console.log(` ✗ ${line}`);
|
|
532
|
+
console.log(` Fix: magus profile switch ${check.profile}`);
|
|
533
|
+
fixable += broken.length;
|
|
534
|
+
} else if (check.state === "active") {
|
|
535
|
+
console.log(`✓ Active profile "${check.profile}" is in place.`);
|
|
446
536
|
}
|
|
537
|
+
// Plugin dependencies: a dependency the profile turns off stops a plugin
|
|
538
|
+
// loading; a plugin.json that cannot be read leaves its dependencies
|
|
539
|
+
// unknown, and they were not enabled.
|
|
540
|
+
const manifest = await readManifest(projectPath);
|
|
541
|
+
const { dependencies } = manifest.profiles[check.profile]
|
|
542
|
+
? await renderProfile(manifest, check.profile, projectPath)
|
|
543
|
+
: { dependencies: undefined };
|
|
544
|
+
for (const line of describeDependencies(
|
|
545
|
+
dependencies && { ...dependencies, unreadable: [] },
|
|
546
|
+
check.profile,
|
|
547
|
+
)) {
|
|
548
|
+
console.log(`✗ ${line}`);
|
|
549
|
+
unfixable++;
|
|
550
|
+
}
|
|
551
|
+
for (const line of describeDependencies(
|
|
552
|
+
dependencies && { ...dependencies, blocked: [] },
|
|
553
|
+
check.profile,
|
|
554
|
+
)) {
|
|
555
|
+
console.log(`⚠ ${line}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// A .gitignore entry does not untrack a file committed before it. Report
|
|
560
|
+
// only: magus never runs git rm.
|
|
561
|
+
let tracked: string[] = [];
|
|
562
|
+
try {
|
|
563
|
+
tracked = await trackedGeneratedFiles(projectPath);
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (!(error instanceof GitCheckError)) throw error;
|
|
566
|
+
console.log(`✗ ${error.message}`);
|
|
567
|
+
unfixable++;
|
|
568
|
+
}
|
|
569
|
+
for (const rel of tracked) {
|
|
570
|
+
console.log(
|
|
571
|
+
`✗ ${rel} is generated from profiles.json but tracked by git. Fix: \`${untrackCommand(rel)}\`, then commit.`,
|
|
572
|
+
);
|
|
573
|
+
unfixable++;
|
|
447
574
|
}
|
|
448
|
-
return
|
|
575
|
+
return { fixable, unfixable };
|
|
449
576
|
}
|