magus-cli 7.4.0 → 7.5.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 +9 -8
- package/package.json +4 -4
- package/src/__tests__/blind/c19-paste-routing.test.ts +252 -0
- package/src/__tests__/blind/c20-registry-and-file-safety.test.ts +560 -0
- 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 +1138 -0
- package/src/__tests__/profile-command.test.ts +90 -32
- 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 +425 -0
- package/src/__tests__/profile-startup.test.ts +44 -14
- package/src/__tests__/profile-state.test.ts +795 -0
- package/src/__tests__/settings-file.test.ts +5 -7
- package/src/__tests__/stale-load-repair.test.ts +5 -4
- package/src/__tests__/styles-manager.test.ts +47 -74
- package/src/__tests__/styles-screen-state.test.ts +11 -10
- package/src/cli/bootstrap.ts +87 -27
- package/src/cli/doctor.ts +152 -44
- package/src/cli/install.ts +92 -127
- package/src/cli/models.ts +21 -1
- package/src/cli/profile.ts +48 -77
- package/src/cli/router.ts +32 -6
- package/src/cli/update.ts +43 -6
- package/src/data/gitignore-defaults.ts +3 -3
- package/src/services/active-closure.ts +3 -3
- package/src/services/claude-settings.ts +283 -125
- package/src/services/cli-tool-commands.ts +30 -0
- package/src/services/conventions-manager.ts +88 -0
- package/src/services/git-worktree.ts +21 -0
- package/src/services/gitignore-service.ts +39 -22
- package/src/services/manifest.ts +4 -5
- package/src/services/models-manager.ts +36 -73
- package/src/services/profile-absorb.ts +1532 -0
- package/src/services/profile-activate.ts +348 -96
- package/src/services/profile-live.ts +825 -0
- package/src/services/profile-render.ts +115 -0
- package/src/services/profile-skills.ts +462 -0
- package/src/services/profile-startup.ts +88 -16
- package/src/services/profile-state.ts +622 -0
- package/src/services/profiles.ts +20 -32
- package/src/services/resolver.ts +6 -1
- package/src/services/settings-file.ts +14 -8
- package/src/services/skills-manager.ts +14 -8
- package/src/services/styles-manager.ts +45 -28
- package/src/services/tool-probe.ts +48 -3
- package/src/types/index.ts +15 -9
- package/src/ui/App.tsx +25 -2
- package/src/ui/components/modals/ModalContainer.tsx +9 -1
- package/src/ui/hooks/useProfileStartupModal.ts +20 -15
- package/src/ui/input/KeyRouter.tsx +147 -23
- package/src/ui/input/key-dispatch.ts +48 -0
- package/src/ui/keymap.ts +8 -1
- package/src/ui/renderers/cliToolRenderers.tsx +65 -27
- package/src/ui/screens/CliToolsScreen.tsx +74 -13
- package/src/ui/screens/PluginsScreen.tsx +6 -2
- package/src/ui/screens/ProfilesScreen.tsx +75 -27
- 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
|
@@ -5,11 +5,9 @@
|
|
|
5
5
|
* their configuration:
|
|
6
6
|
*
|
|
7
7
|
* - an unparseable file must be REFUSED, and left byte-for-byte alone;
|
|
8
|
-
* - a symlinked settings.json must be written THROUGH the link
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* replace the link with a plain file and detach the project from its
|
|
12
|
-
* profile, which nothing downstream would notice.
|
|
8
|
+
* - a symlinked settings.json must be written THROUGH the link: a symlink a
|
|
9
|
+
* person set up deliberately is theirs. Remove-then-create would replace
|
|
10
|
+
* the link with a plain file, which nothing downstream would notice.
|
|
13
11
|
*/
|
|
14
12
|
|
|
15
13
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
@@ -154,7 +152,7 @@ describe("updateSettingsFile", () => {
|
|
|
154
152
|
expect(readFileSync(file, "utf8")).toBe(original);
|
|
155
153
|
});
|
|
156
154
|
|
|
157
|
-
test("S-6: a symlinked settings.json is written THROUGH the link
|
|
155
|
+
test("S-6: a symlinked settings.json is written THROUGH the link", async () => {
|
|
158
156
|
const dir = tempDir();
|
|
159
157
|
const target = path.join(dir, "profile-settings.json");
|
|
160
158
|
const link = path.join(dir, "settings.json");
|
|
@@ -168,7 +166,7 @@ describe("updateSettingsFile", () => {
|
|
|
168
166
|
settings.outputStyle = "composed";
|
|
169
167
|
});
|
|
170
168
|
|
|
171
|
-
// The
|
|
169
|
+
// The link target received the write …
|
|
172
170
|
expect(JSON.parse(readFileSync(target, "utf8"))).toEqual({
|
|
173
171
|
enabledPlugins: { "dev@magus": true },
|
|
174
172
|
outputStyle: "composed",
|
|
@@ -344,10 +344,11 @@ describe("repairStaleLoad — refusals", () => {
|
|
|
344
344
|
});
|
|
345
345
|
|
|
346
346
|
test("an unreadable registry is refused rather than replaced", async () => {
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
347
|
+
// Writing over a corrupt registry would erase every project's installs on
|
|
348
|
+
// the machine. The repair reads and parses the same bytes it will check
|
|
349
|
+
// before writing, so a document it cannot parse stops it here — and it
|
|
350
|
+
// reports that as a result rather than throwing, because one item in a
|
|
351
|
+
// plan of repairs must not end the plan.
|
|
351
352
|
await fs.outputFile(registryFile, "{ not json");
|
|
352
353
|
|
|
353
354
|
const result = await repairStaleLoad(PLUGIN, "0.5.0", here, root);
|
|
@@ -4,6 +4,13 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import fs from "fs-extra";
|
|
6
6
|
import { EMBEDDED_PRESETS } from "../data/styles/index.js";
|
|
7
|
+
import { writeManifest } from "../services/manifest.js";
|
|
8
|
+
import { activateProfile } from "../services/profile-activate.js";
|
|
9
|
+
import {
|
|
10
|
+
liveHash,
|
|
11
|
+
readActiveMarker,
|
|
12
|
+
writeActiveMarker,
|
|
13
|
+
} from "../services/profile-live.js";
|
|
7
14
|
import {
|
|
8
15
|
INTEGRITY_BLOCK,
|
|
9
16
|
type ImportedStyle,
|
|
@@ -19,6 +26,7 @@ import {
|
|
|
19
26
|
splitFrontmatter,
|
|
20
27
|
validateSelection,
|
|
21
28
|
} from "../services/styles-manager.js";
|
|
29
|
+
import type { ProfileManifest } from "../types/index.js";
|
|
22
30
|
import {
|
|
23
31
|
buildStyleBrowserItems,
|
|
24
32
|
firstSelectableIndex,
|
|
@@ -405,20 +413,26 @@ describe("applyStyles", () => {
|
|
|
405
413
|
).rejects.toThrow(/Nothing selected/);
|
|
406
414
|
});
|
|
407
415
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
416
|
+
const DEV: ProfileManifest = {
|
|
417
|
+
version: 2,
|
|
418
|
+
profiles: { dev: { name: "dev", plugins: {} } },
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
test("an older settings symlink is converted to a real file, not written through", async () => {
|
|
422
|
+
// Claude Code refuses to write through a symlink, so the style must not
|
|
423
|
+
// be applied by writing through one either: the file is rendered from
|
|
424
|
+
// the manifest in place of the link.
|
|
425
|
+
const oldSettings = join(
|
|
413
426
|
dir,
|
|
414
427
|
".claude",
|
|
415
428
|
"_profiles",
|
|
416
429
|
"dev",
|
|
417
430
|
"settings.json",
|
|
418
431
|
);
|
|
419
|
-
await fs.outputJson(
|
|
432
|
+
await fs.outputJson(oldSettings, { enabledPlugins: {} });
|
|
420
433
|
const link = join(dir, ".claude", "settings.json");
|
|
421
434
|
await symlink("_profiles/dev/settings.json", link);
|
|
435
|
+
await writeManifest(DEV, dir);
|
|
422
436
|
|
|
423
437
|
const result = await applyStyles({
|
|
424
438
|
projectPath: dir,
|
|
@@ -428,33 +442,13 @@ describe("applyStyles", () => {
|
|
|
428
442
|
|
|
429
443
|
expect(result.profile).toBe("dev");
|
|
430
444
|
expect(result.styleName).toBe("composed-dev");
|
|
431
|
-
expect((await fs.lstat(link)).isSymbolicLink()).toBe(
|
|
432
|
-
|
|
433
|
-
// profile" means.
|
|
434
|
-
expect((await fs.readJson(profileSettings)).outputStyle).toBe(
|
|
435
|
-
"composed-dev",
|
|
436
|
-
);
|
|
445
|
+
expect((await fs.lstat(link)).isSymbolicLink()).toBe(false);
|
|
446
|
+
expect((await fs.readJson(link)).outputStyle).toBe("composed-dev");
|
|
437
447
|
});
|
|
438
448
|
|
|
439
|
-
test("
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const profileSettings = join(
|
|
443
|
-
dir,
|
|
444
|
-
".claude",
|
|
445
|
-
"_profiles",
|
|
446
|
-
"dev",
|
|
447
|
-
"settings.json",
|
|
448
|
-
);
|
|
449
|
-
await fs.outputJson(profileSettings, { enabledPlugins: {} });
|
|
450
|
-
await symlink(
|
|
451
|
-
"_profiles/dev/settings.json",
|
|
452
|
-
join(dir, ".claude", "settings.json"),
|
|
453
|
-
);
|
|
454
|
-
await fs.outputJson(join(dir, ".claude", "profiles.json"), {
|
|
455
|
-
version: 2,
|
|
456
|
-
profiles: { dev: { name: "dev", plugins: {} } },
|
|
457
|
-
});
|
|
449
|
+
test("with an activated profile, the live file stays real and is recorded", async () => {
|
|
450
|
+
await writeManifest(DEV, dir);
|
|
451
|
+
await activateProfile(DEV, "dev", dir, { onDisplace: () => {} });
|
|
458
452
|
|
|
459
453
|
const result = await applyStyles({
|
|
460
454
|
projectPath: dir,
|
|
@@ -462,67 +456,46 @@ describe("applyStyles", () => {
|
|
|
462
456
|
imports: [],
|
|
463
457
|
});
|
|
464
458
|
|
|
459
|
+
const live = join(dir, ".claude", "settings.json");
|
|
465
460
|
expect(result.recordedInManifest).toBe(true);
|
|
466
|
-
|
|
467
|
-
expect(
|
|
461
|
+
expect((await fs.lstat(live)).isFile()).toBe(true);
|
|
462
|
+
expect((await fs.readJson(live)).outputStyle).toBe("composed-dev");
|
|
463
|
+
// Rendered from the manifest and recorded, so it reads as unchanged.
|
|
464
|
+
expect((await readActiveMarker(dir))?.files.settings).toBe(
|
|
465
|
+
(await liveHash("settings", dir)) ?? "",
|
|
466
|
+
);
|
|
468
467
|
});
|
|
469
468
|
|
|
470
|
-
test("
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
".claude",
|
|
474
|
-
"_profiles",
|
|
475
|
-
"dev",
|
|
476
|
-
"settings.json",
|
|
477
|
-
);
|
|
478
|
-
await fs.outputJson(profileSettings, {});
|
|
479
|
-
await symlink(
|
|
480
|
-
"_profiles/dev/settings.json",
|
|
481
|
-
join(dir, ".claude", "settings.json"),
|
|
482
|
-
);
|
|
469
|
+
test("records the style in profiles.json, where rendering reads it", async () => {
|
|
470
|
+
await writeManifest(DEV, dir);
|
|
471
|
+
await activateProfile(DEV, "dev", dir, { onDisplace: () => {} });
|
|
483
472
|
|
|
484
473
|
const result = await applyStyles({
|
|
485
474
|
projectPath: dir,
|
|
486
475
|
presets: [preset({ name: "direct" })],
|
|
487
476
|
imports: [],
|
|
488
477
|
});
|
|
489
|
-
expect(result.profile).toBe("dev");
|
|
490
|
-
expect(result.recordedInManifest).toBe(false);
|
|
491
|
-
});
|
|
492
|
-
});
|
|
493
478
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
presets: [preset({ name: "direct" })],
|
|
499
|
-
imports: [],
|
|
500
|
-
});
|
|
501
|
-
await clearStyle(dir);
|
|
479
|
+
expect(result.recordedInManifest).toBe(true);
|
|
480
|
+
const manifest = await fs.readJson(join(dir, ".claude", "profiles.json"));
|
|
481
|
+
expect(manifest.profiles.dev.settings.outputStyle).toBe("composed-dev");
|
|
482
|
+
});
|
|
502
483
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
484
|
+
test("a marker naming a profile profiles.json lacks is no active profile", async () => {
|
|
485
|
+
await writeManifest(DEV, dir);
|
|
486
|
+
await writeActiveMarker(
|
|
487
|
+
{ version: 1, profile: "gone", files: {}, skills: {} },
|
|
488
|
+
dir,
|
|
506
489
|
);
|
|
507
|
-
|
|
508
|
-
expect(JSON.parse(raw)).toEqual({});
|
|
509
|
-
});
|
|
490
|
+
await fs.outputJson(join(dir, ".claude", "settings.json"), {});
|
|
510
491
|
|
|
511
|
-
test("leaves the generated file on disk so a re-apply is one keypress away", async () => {
|
|
512
492
|
const result = await applyStyles({
|
|
513
493
|
projectPath: dir,
|
|
514
494
|
presets: [preset({ name: "direct" })],
|
|
515
495
|
imports: [],
|
|
516
496
|
});
|
|
517
|
-
|
|
518
|
-
expect(
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
test("is a no-op on a project that never had a style", async () => {
|
|
522
|
-
await clearStyle(dir);
|
|
523
|
-
expect(await fs.pathExists(join(dir, ".claude", "settings.json"))).toBe(
|
|
524
|
-
false,
|
|
525
|
-
);
|
|
497
|
+
expect(result.profile).toBeNull();
|
|
498
|
+
expect(result.recordedInManifest).toBe(false);
|
|
526
499
|
});
|
|
527
500
|
});
|
|
528
501
|
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdtemp, rm
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { parseKeypress } from "@opentui/core";
|
|
6
6
|
import fs from "fs-extra";
|
|
7
|
+
import { writeManifest } from "../services/manifest.js";
|
|
8
|
+
import { activateProfile } from "../services/profile-activate.js";
|
|
7
9
|
import {
|
|
8
10
|
applyStyles,
|
|
9
11
|
clearStyle,
|
|
10
12
|
communityCacheDirOrNull,
|
|
11
13
|
loadStyles,
|
|
12
14
|
} from "../services/styles-manager.js";
|
|
15
|
+
import type { ProfileManifest } from "../types/index.js";
|
|
13
16
|
import type { KeyPress } from "../ui/input/key-dispatch.js";
|
|
14
17
|
import { SCREEN_BINDINGS, matchBinding } from "../ui/keymap.js";
|
|
15
18
|
import { appReducer, initialState } from "../ui/state/reducer.js";
|
|
@@ -291,16 +294,14 @@ describe("loadStyles reports what is actually in force", () => {
|
|
|
291
294
|
});
|
|
292
295
|
|
|
293
296
|
test("looks for the per-profile style when a profile is active", async () => {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
{
|
|
297
|
-
outputStyle: "composed-dev",
|
|
297
|
+
const manifest: ProfileManifest = {
|
|
298
|
+
version: 2,
|
|
299
|
+
profiles: {
|
|
300
|
+
dev: { name: "dev", settings: { outputStyle: "composed-dev" } },
|
|
298
301
|
},
|
|
299
|
-
|
|
300
|
-
await
|
|
301
|
-
|
|
302
|
-
join(dir, ".claude", "settings.json"),
|
|
303
|
-
);
|
|
302
|
+
};
|
|
303
|
+
await writeManifest(manifest, dir);
|
|
304
|
+
await activateProfile(manifest, "dev", dir, { onDisplace: () => {} });
|
|
304
305
|
await fs.outputFile(
|
|
305
306
|
join(dir, ".claude", "output-styles", "composed-dev.md"),
|
|
306
307
|
"---\nname: composed-dev\nstyle-presets: terse\nstyle-imports: none\n---\n\nbody",
|
package/src/cli/bootstrap.ts
CHANGED
|
@@ -15,8 +15,9 @@ import {
|
|
|
15
15
|
writeManifest,
|
|
16
16
|
} from "../services/manifest.js";
|
|
17
17
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
type ActivationResult,
|
|
19
|
+
activateProfile,
|
|
20
|
+
activationNotes,
|
|
20
21
|
reportDisplaced,
|
|
21
22
|
} from "../services/profile-activate.js";
|
|
22
23
|
import {
|
|
@@ -31,6 +32,13 @@ import {
|
|
|
31
32
|
needsNothing,
|
|
32
33
|
parseProfileChoice,
|
|
33
34
|
} from "../services/profile-startup.js";
|
|
35
|
+
import {
|
|
36
|
+
applyProfileChoice,
|
|
37
|
+
describeUnknownProfile,
|
|
38
|
+
prepareProfileState,
|
|
39
|
+
suggestedProfile,
|
|
40
|
+
switchHint,
|
|
41
|
+
} from "../services/profile-state.js";
|
|
34
42
|
import { SettingsParseError } from "../services/settings-file.js";
|
|
35
43
|
import type { ProfileManifest } from "../types/index.js";
|
|
36
44
|
import { dim } from "./ansi.js";
|
|
@@ -57,8 +65,8 @@ export function reportUnparseable(error: SettingsParseError): void {
|
|
|
57
65
|
* Write the adopted manifest, then put the project ON the profile it describes.
|
|
58
66
|
*
|
|
59
67
|
* Both halves or neither. Writing alone is what made migrating feel like a
|
|
60
|
-
* no-op: `activeProfile()`
|
|
61
|
-
*
|
|
68
|
+
* no-op: `activeProfile()` reads the marker activation writes, so a manifest
|
|
69
|
+
* nothing activated changes no behaviour, and the run that just migrated still has to
|
|
62
70
|
* ask which profile to use.
|
|
63
71
|
*
|
|
64
72
|
* Activation is reported in full, including anything it moved aside, because it
|
|
@@ -79,20 +87,73 @@ async function writeAndActivate(
|
|
|
79
87
|
console.log(`✓ Wrote ${rel(getManifestPath(projectPath))}`);
|
|
80
88
|
|
|
81
89
|
// Each move is printed as it happens, not from the returned list afterwards.
|
|
82
|
-
// Activation
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
await materializeAndActivate(
|
|
90
|
+
// Activation writes three files in sequence, so a throw on the third would
|
|
91
|
+
// discard the record of the first two — and the first of those is the
|
|
92
|
+
// user's `.claude/settings.json`, now absent from where they left it.
|
|
93
|
+
const activation = await activateProfile(
|
|
87
94
|
result.manifest,
|
|
88
95
|
result.profileId,
|
|
89
96
|
projectPath,
|
|
90
|
-
reportDisplaced(projectPath),
|
|
97
|
+
{ onDisplace: reportDisplaced(projectPath) },
|
|
91
98
|
);
|
|
92
99
|
console.log(`✓ Active profile: ${result.profileId}`);
|
|
100
|
+
printActivationNotes(activation);
|
|
93
101
|
console.log();
|
|
94
102
|
}
|
|
95
103
|
|
|
104
|
+
/** Print what activation did beyond the switch itself, one line each. */
|
|
105
|
+
export function printActivationNotes(result: ActivationResult): void {
|
|
106
|
+
for (const line of activationNotes(result)) console.log(noteLine(line));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface ProfileGateOptions {
|
|
110
|
+
/**
|
|
111
|
+
* Carry on when the marker names a profile profiles.json does not define.
|
|
112
|
+
* Only for commands that fix exactly that by activating a profile that
|
|
113
|
+
* exists — `profile switch`, `install`.
|
|
114
|
+
*/
|
|
115
|
+
allowUnknown?: boolean;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The gate every CLI command that reads or writes profile state passes first.
|
|
120
|
+
*
|
|
121
|
+
* Restores a lost marker when the live config matches exactly one profile,
|
|
122
|
+
* converts an older layout, absorbs what Claude Code wrote to the live config
|
|
123
|
+
* into the active profile, and regenerates the live config when profiles.json
|
|
124
|
+
* changed — printing one line for each. Returns false, having said why, when
|
|
125
|
+
* the command must not act: a marker naming a profile that does not exist, or
|
|
126
|
+
* a live file that cannot be read (it would be overwritten).
|
|
127
|
+
*/
|
|
128
|
+
export async function profileGate(
|
|
129
|
+
projectPath: string,
|
|
130
|
+
opts: ProfileGateOptions = {},
|
|
131
|
+
): Promise<boolean> {
|
|
132
|
+
let prep: Awaited<ReturnType<typeof prepareProfileState>>;
|
|
133
|
+
try {
|
|
134
|
+
prep = await prepareProfileState(projectPath);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error instanceof SettingsParseError) {
|
|
137
|
+
reportUnparseable(error);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
console.error(
|
|
141
|
+
failLine(error instanceof Error ? error.message : String(error)),
|
|
142
|
+
);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
for (const line of prep.lines) console.log(noteLine(line));
|
|
146
|
+
if (prep.problems.length > 0) {
|
|
147
|
+
for (const problem of prep.problems) console.error(failLine(problem));
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
if (prep.check.state === "unknown" && !opts.allowUnknown) {
|
|
151
|
+
console.error(failLine(describeUnknownProfile(prep.check)));
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
96
157
|
/** Print what adoption would capture, so the user can judge it before writing. */
|
|
97
158
|
function printAdoptionPlan(result: AdoptResult): void {
|
|
98
159
|
console.log(
|
|
@@ -141,12 +202,11 @@ function printAdoptionPlan(result: AdoptResult): void {
|
|
|
141
202
|
// onto the profile, which replaces two files that are in the working tree
|
|
142
203
|
// right now. The prompt below is the only place to decline.
|
|
143
204
|
console.log(
|
|
144
|
-
"The project then runs this profile: .claude/settings.json
|
|
205
|
+
"The project then runs this profile: .claude/settings.json and .mcp.json are",
|
|
145
206
|
);
|
|
146
207
|
console.log(
|
|
147
|
-
"
|
|
208
|
+
"generated from it, and your current copies are kept where the run reports.",
|
|
148
209
|
);
|
|
149
|
-
console.log("the run reports.");
|
|
150
210
|
}
|
|
151
211
|
|
|
152
212
|
export interface EnsureManifestOptions {
|
|
@@ -231,7 +291,7 @@ export async function ensureManifest(
|
|
|
231
291
|
* state is read from and where updates are written. With none there is nothing
|
|
232
292
|
* to read and nowhere to write, so there is no useful work to do — and the
|
|
233
293
|
* previous version of this offered exactly that anyway. It asked only when the
|
|
234
|
-
* manifest
|
|
294
|
+
* manifest disagreed with `enabledPlugins`, let the user decline, and `update` then carried on with
|
|
235
295
|
* a profile "for this run only", resolving against a manifest the project was
|
|
236
296
|
* not running. Two worlds live at once, which is the confusion being removed.
|
|
237
297
|
*
|
|
@@ -262,15 +322,16 @@ export async function requireActiveProfile(
|
|
|
262
322
|
// `--check` and with no TTY: this is the committed answer being applied, not
|
|
263
323
|
// a decision being made, and it is exactly the case CI hits.
|
|
264
324
|
if (need.autoActivate && need.declaredDefault !== null && !opts.check) {
|
|
265
|
-
const
|
|
325
|
+
const result = await applyProfileChoice(
|
|
266
326
|
{ kind: "activate", id: need.declaredDefault },
|
|
267
327
|
need.manifest,
|
|
268
328
|
projectPath,
|
|
269
|
-
reportDisplaced(projectPath),
|
|
329
|
+
{ onDisplace: reportDisplaced(projectPath) },
|
|
270
330
|
);
|
|
271
331
|
console.log(
|
|
272
|
-
`✓ Active profile: ${profileId} ${dim("(declared by this project)")}`,
|
|
332
|
+
`✓ Active profile: ${result.profileId} ${dim("(declared by this project)")}`,
|
|
273
333
|
);
|
|
334
|
+
printActivationNotes(result);
|
|
274
335
|
console.log("");
|
|
275
336
|
return readManifest(projectPath);
|
|
276
337
|
}
|
|
@@ -292,9 +353,10 @@ export async function requireActiveProfile(
|
|
|
292
353
|
noteLine(
|
|
293
354
|
need.state === "unmigrated"
|
|
294
355
|
? "Run `magus profile init` to create one deliberately."
|
|
295
|
-
: `Run
|
|
296
|
-
.map((p) => p.id)
|
|
297
|
-
.
|
|
356
|
+
: `Run ${switchHint(
|
|
357
|
+
need.activate.map((p) => p.id),
|
|
358
|
+
need.declaredDefault ?? suggestedProfile(need.manifest),
|
|
359
|
+
)} first.`,
|
|
298
360
|
),
|
|
299
361
|
);
|
|
300
362
|
return null;
|
|
@@ -316,13 +378,11 @@ export async function requireActiveProfile(
|
|
|
316
378
|
return null;
|
|
317
379
|
}
|
|
318
380
|
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
);
|
|
325
|
-
console.log(`✓ Active profile: ${profileId}`);
|
|
381
|
+
const result = await applyProfileChoice(choice, need.manifest, projectPath, {
|
|
382
|
+
onDisplace: reportDisplaced(projectPath),
|
|
383
|
+
});
|
|
384
|
+
console.log(`✓ Active profile: ${result.profileId}`);
|
|
385
|
+
printActivationNotes(result);
|
|
326
386
|
console.log("");
|
|
327
387
|
|
|
328
388
|
// Re-read rather than return `need.manifest`: adopting wrote a new profile
|