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
|
@@ -23,6 +23,8 @@ import {
|
|
|
23
23
|
readModelsStatus,
|
|
24
24
|
resolveFullModelId,
|
|
25
25
|
} from "../services/models-manager.js";
|
|
26
|
+
import { activateProfile } from "../services/profile-activate.js";
|
|
27
|
+
import { liveHash, readActiveMarker } from "../services/profile-live.js";
|
|
26
28
|
import type { ProfileManifest } from "../types/index.js";
|
|
27
29
|
|
|
28
30
|
let project: string;
|
|
@@ -55,22 +57,16 @@ function manifestWith(profile: string, extra = {}): ProfileManifest {
|
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
59
|
* Stage a project that already has a profile ACTIVE, the way `magus install`
|
|
58
|
-
* leaves one:
|
|
59
|
-
* Built
|
|
60
|
+
* leaves one: the live files rendered from the manifest and recorded in the
|
|
61
|
+
* marker. Built without running install, which needs the claude CLI.
|
|
60
62
|
*/
|
|
61
63
|
async function stageActiveProfile(
|
|
62
64
|
name: string,
|
|
63
65
|
entry: Record<string, unknown> = {},
|
|
64
|
-
settings: Record<string, unknown> = {},
|
|
65
66
|
): Promise<void> {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
await
|
|
69
|
-
await fs.ensureDir(join(project, ".claude"));
|
|
70
|
-
await fs.symlink(
|
|
71
|
-
join("_profiles", name, "settings.json"),
|
|
72
|
-
join(project, ".claude", "settings.json"),
|
|
73
|
-
);
|
|
67
|
+
const manifest = manifestWith(name, entry);
|
|
68
|
+
await writeManifest(manifest, project);
|
|
69
|
+
await activateProfile(manifest, name, project, { onDisplace: () => {} });
|
|
74
70
|
}
|
|
75
71
|
|
|
76
72
|
async function readSettings(): Promise<Record<string, unknown>> {
|
|
@@ -131,34 +127,55 @@ describe("applyModelPreset", () => {
|
|
|
131
127
|
expect(again.hook).toBe("already");
|
|
132
128
|
});
|
|
133
129
|
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
test("keeps the profile symlink and updates profiles.json under it", async () => {
|
|
130
|
+
// settings.json and models.json are real files rendered from the manifest
|
|
131
|
+
// (Claude Code refuses to write through a symlink), recorded in the marker.
|
|
132
|
+
test("renders settings.json and models.json from the profile as real files", async () => {
|
|
138
133
|
await stageActiveProfile("team", { plugins: { "dev@magus": "latest" } });
|
|
139
134
|
|
|
140
135
|
await applyModelPreset({ projectPath: project, preset: PRESET });
|
|
141
136
|
|
|
142
|
-
const
|
|
143
|
-
expect((await fs.lstat(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
137
|
+
const live = join(project, ".claude", "settings.json");
|
|
138
|
+
expect((await fs.lstat(live)).isFile()).toBe(true);
|
|
139
|
+
const settings = await readSettings();
|
|
140
|
+
expect(settings.model).toBe("opus");
|
|
141
|
+
// Rendering from the manifest kept the plugins the entry declares.
|
|
142
|
+
expect(
|
|
143
|
+
(settings.enabledPlugins as Record<string, boolean>)["dev@magus"],
|
|
144
|
+
).toBe(true);
|
|
145
|
+
const marker = await readActiveMarker(project);
|
|
146
|
+
expect(marker?.files.settings).toBe(
|
|
147
|
+
(await liveHash("settings", project)) ?? "",
|
|
152
148
|
);
|
|
153
|
-
expect(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
// And .claude/models.json is a link into the same profile dir, not a copy.
|
|
157
|
-
const modelsLink = join(project, ".claude", "models.json");
|
|
158
|
-
expect((await fs.lstat(modelsLink)).isSymbolicLink()).toBe(true);
|
|
159
|
-
expect(await fs.readlink(modelsLink)).toBe(
|
|
160
|
-
join("_profiles", "team", "models.json"),
|
|
149
|
+
expect(marker?.files.models).toBe(
|
|
150
|
+
(await liveHash("models", project)) ?? "",
|
|
161
151
|
);
|
|
152
|
+
|
|
153
|
+
const modelsFile = join(project, ".claude", "models.json");
|
|
154
|
+
expect((await fs.lstat(modelsFile)).isSymbolicLink()).toBe(false);
|
|
155
|
+
expect((await fs.readJson(modelsFile)).preset).toBe(PRESET);
|
|
156
|
+
expect(
|
|
157
|
+
await fs.pathExists(join(project, ".claude", "profiles", "team")),
|
|
158
|
+
).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("a Claude Code edit to settings.json is absorbed into the profile, then the keys are added", async () => {
|
|
162
|
+
await stageActiveProfile("team");
|
|
163
|
+
// Claude Code wrote a permission; that is a change to the active profile.
|
|
164
|
+
await fs.writeJson(join(project, ".claude", "settings.json"), {
|
|
165
|
+
enabledPlugins: {},
|
|
166
|
+
permissions: { allow: ["Bash(ls)"] },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await applyModelPreset({ projectPath: project, preset: PRESET });
|
|
170
|
+
|
|
171
|
+
const settings = await readSettings();
|
|
172
|
+
expect(settings.model).toBe("opus");
|
|
173
|
+
expect(settings.permissions).toEqual({ allow: ["Bash(ls)"] });
|
|
174
|
+
const manifest = await fs.readJson(getManifestPath(project));
|
|
175
|
+
expect(manifest.profiles.team.settings.permissions).toEqual({
|
|
176
|
+
allow: ["Bash(ls)"],
|
|
177
|
+
});
|
|
178
|
+
expect(manifest.profiles.team.settings.model).toBe("opus");
|
|
162
179
|
});
|
|
163
180
|
|
|
164
181
|
test("without a profile it writes a plain models.json and records nothing", async () => {
|
|
@@ -212,20 +229,15 @@ describe("applyModelPreset", () => {
|
|
|
212
229
|
});
|
|
213
230
|
|
|
214
231
|
describe("clearModels", () => {
|
|
215
|
-
test("removes the config
|
|
232
|
+
test("removes the config file and the manifest entry", async () => {
|
|
216
233
|
await stageActiveProfile("team");
|
|
217
234
|
await applyModelPreset({ projectPath: project, preset: PRESET });
|
|
218
235
|
|
|
219
236
|
await clearModels(project);
|
|
220
237
|
|
|
221
238
|
expect(await fs.pathExists(configPath(project))).toBe(false);
|
|
222
|
-
// lstat, not pathExists: a DANGLING symlink is the failure this guards.
|
|
223
239
|
await expect(fs.lstat(configPath(project))).rejects.toThrow();
|
|
224
|
-
expect(
|
|
225
|
-
await fs.pathExists(
|
|
226
|
-
join(project, ".claude", "_profiles", "team", "models.json"),
|
|
227
|
-
),
|
|
228
|
-
).toBe(false);
|
|
240
|
+
expect((await readActiveMarker(project))?.files.models).toBeUndefined();
|
|
229
241
|
|
|
230
242
|
const manifest = await fs.readJson(getManifestPath(project));
|
|
231
243
|
expect(manifest.profiles.team.models).toBeUndefined();
|