magus-cli 7.5.1 → 7.5.3
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/package.json +4 -4
- package/src/__tests__/gitignore-check.test.ts +11 -29
- package/src/__tests__/gitignore-profile.test.ts +248 -0
- package/src/__tests__/models-cli.test.ts +11 -0
- package/src/__tests__/profile-skills.test.ts +16 -21
- package/src/__tests__/profile-state.test.ts +132 -149
- package/src/__tests__/stale-load-repair.test.ts +160 -0
- package/src/__tests__/update-apply.test.ts +197 -1
- package/src/cli/bootstrap.ts +0 -4
- package/src/cli/doctor.ts +6 -20
- package/src/cli/router.ts +5 -4
- package/src/cli/update.ts +7 -0
- package/src/services/claude-cli.ts +0 -24
- package/src/services/claude-settings.ts +18 -7
- package/src/services/gitignore-check.ts +37 -49
- package/src/services/gitignore-resolver.ts +10 -1
- package/src/services/gitignore-service.ts +67 -0
- package/src/services/profile-absorb.ts +31 -17
- package/src/services/profile-activate.ts +74 -120
- package/src/services/profile-state.ts +0 -62
- package/src/services/tracked-live-files.ts +88 -74
- package/src/services/update-engine.ts +180 -7
- package/src/ui/App.tsx +34 -33
- package/src/ui/hooks/useGitignoreModal.ts +63 -17
- package/src/ui/hooks/useProfileStartupModal.ts +7 -2
- package/src/ui/screens/PluginsScreen.tsx +2 -11
- package/src/ui/screens/ProfilesScreen.tsx +3 -10
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magus-cli",
|
|
3
|
-
"version": "7.5.
|
|
3
|
+
"version": "7.5.3",
|
|
4
4
|
"description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.tsx",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"typescript": "^5.6.3"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"magus-cli-darwin-arm64": "7.5.
|
|
69
|
-
"magus-cli-darwin-x64": "7.5.
|
|
70
|
-
"magus-cli-linux-x64": "7.5.
|
|
68
|
+
"magus-cli-darwin-arm64": "7.5.3",
|
|
69
|
+
"magus-cli-darwin-x64": "7.5.3",
|
|
70
|
+
"magus-cli-linux-x64": "7.5.3"
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { BUILTIN_DEFAULTS } from "../data/gitignore-defaults.js";
|
|
7
|
-
import {
|
|
8
|
-
checkGitignore,
|
|
9
|
-
formatGitignoreWarning,
|
|
10
|
-
} from "../services/gitignore-check";
|
|
7
|
+
import { startupGitignoreNeed } from "../services/gitignore-check";
|
|
11
8
|
|
|
12
9
|
function git(cwd: string, ...args: string[]): void {
|
|
13
10
|
spawnSync("git", ["-c", "core.excludesFile=/dev/null", ...args], { cwd });
|
|
14
11
|
}
|
|
15
12
|
|
|
16
|
-
describe("
|
|
13
|
+
describe("startupGitignoreNeed", () => {
|
|
17
14
|
let projectDir: string;
|
|
18
15
|
|
|
19
16
|
beforeEach(async () => {
|
|
@@ -24,20 +21,21 @@ describe("checkGitignore", () => {
|
|
|
24
21
|
await rm(projectDir, { recursive: true, force: true });
|
|
25
22
|
});
|
|
26
23
|
|
|
27
|
-
it("
|
|
28
|
-
|
|
29
|
-
expect(r.violationCount).toBe(0);
|
|
24
|
+
it("asks nothing in a non-git directory", async () => {
|
|
25
|
+
expect(await startupGitignoreNeed(projectDir)).toBeNull();
|
|
30
26
|
});
|
|
31
27
|
|
|
32
28
|
it("flags missing built-in patterns inside a fresh git repo", async () => {
|
|
33
29
|
git(projectDir, "init", "-q");
|
|
34
|
-
const
|
|
30
|
+
const need = await startupGitignoreNeed(projectDir);
|
|
35
31
|
// BUILTIN_DEFAULTS includes ".mnemex/" etc. — none are tracked, none in
|
|
36
32
|
// .gitignore, so each one becomes "missing-from-gitignore".
|
|
37
|
-
expect(
|
|
33
|
+
expect(need?.violationCount).toBeGreaterThan(0);
|
|
34
|
+
expect(need?.safeCount).toBeGreaterThan(0);
|
|
35
|
+
expect(need?.generated).toEqual([]);
|
|
38
36
|
});
|
|
39
37
|
|
|
40
|
-
it("
|
|
38
|
+
it("asks nothing when .gitignore covers all built-in patterns and tracked files match track rules", async () => {
|
|
41
39
|
git(projectDir, "init", "-q");
|
|
42
40
|
git(projectDir, "config", "user.email", "t@t");
|
|
43
41
|
git(projectDir, "config", "user.name", "t");
|
|
@@ -50,22 +48,6 @@ describe("checkGitignore", () => {
|
|
|
50
48
|
`${BUILTIN_DEFAULTS.ignore.join("\n")}\n`,
|
|
51
49
|
);
|
|
52
50
|
// Tracked files don't have to exist for the absent-track-path case to be ignored
|
|
53
|
-
|
|
54
|
-
expect(r.violationCount).toBe(0);
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe("formatGitignoreWarning", () => {
|
|
59
|
-
it("returns null when no violations", () => {
|
|
60
|
-
expect(
|
|
61
|
-
formatGitignoreWarning({ violationCount: 0, warnings: [] }),
|
|
62
|
-
).toBeNull();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("returns a warning string when violations exist", () => {
|
|
66
|
-
const w = formatGitignoreWarning({ violationCount: 3, warnings: [] });
|
|
67
|
-
expect(w).not.toBeNull();
|
|
68
|
-
expect(w).toContain("3 git state");
|
|
69
|
-
expect(w).toContain("Git State tab");
|
|
51
|
+
expect(await startupGitignoreNeed(projectDir)).toBeNull();
|
|
70
52
|
});
|
|
71
53
|
});
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B7 — in a project with profiles, a generated file git tracks is a question
|
|
3
|
+
* asked on every TUI open, fixed in place when the user says so.
|
|
4
|
+
*
|
|
5
|
+
* The files magus generates from `.claude/profiles.json` are expected to be
|
|
6
|
+
* gitignored. The gitignore manager's `profile` tier says so, so a committed
|
|
7
|
+
* copy is reported as "tracked but should be ignored" — and the startup fix
|
|
8
|
+
* untracks exactly those files, leaving every other violation for the tab.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import fs from "fs-extra";
|
|
17
|
+
import { startupGitignoreNeed } from "../services/gitignore-check.js";
|
|
18
|
+
import {
|
|
19
|
+
applyStartupFixes,
|
|
20
|
+
describeStartupFix,
|
|
21
|
+
loadGitignoreState,
|
|
22
|
+
} from "../services/gitignore-service.js";
|
|
23
|
+
import { writeManifest } from "../services/manifest.js";
|
|
24
|
+
import { activateProfile } from "../services/profile-activate.js";
|
|
25
|
+
import {
|
|
26
|
+
folderHash,
|
|
27
|
+
readActiveMarker,
|
|
28
|
+
writeActiveMarker,
|
|
29
|
+
} from "../services/profile-live.js";
|
|
30
|
+
import {
|
|
31
|
+
applyProfileChoice,
|
|
32
|
+
prepareProfileState,
|
|
33
|
+
} from "../services/profile-state.js";
|
|
34
|
+
import type { ProfileManifest } from "../types/index.js";
|
|
35
|
+
import { buildGitignoreModal } from "../ui/hooks/useGitignoreModal.js";
|
|
36
|
+
|
|
37
|
+
const MANIFEST: ProfileManifest = {
|
|
38
|
+
version: 2,
|
|
39
|
+
profiles: { team: { name: "Team", plugins: { "dev@magus": "latest" } } },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let project: string;
|
|
43
|
+
let log: ReturnType<typeof spyOn>;
|
|
44
|
+
|
|
45
|
+
function git(...args: string[]) {
|
|
46
|
+
return spawnSync(
|
|
47
|
+
"git",
|
|
48
|
+
[
|
|
49
|
+
"-c",
|
|
50
|
+
"user.email=test@example.com",
|
|
51
|
+
"-c",
|
|
52
|
+
"user.name=test",
|
|
53
|
+
"-c",
|
|
54
|
+
"commit.gpgsign=false",
|
|
55
|
+
"-c",
|
|
56
|
+
"core.excludesFile=/dev/null",
|
|
57
|
+
...args,
|
|
58
|
+
],
|
|
59
|
+
{ cwd: project, encoding: "utf8" },
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A repo that commits `.claude/settings.json` (and, optionally, profiles.json). */
|
|
64
|
+
async function commitSettings(withProfiles: boolean): Promise<void> {
|
|
65
|
+
expect(git("init", "-q").status).toBe(0);
|
|
66
|
+
await fs.outputJson(join(project, ".claude", "settings.json"), {
|
|
67
|
+
enabledPlugins: { "dev@magus": true },
|
|
68
|
+
});
|
|
69
|
+
if (withProfiles) await writeManifest(MANIFEST, project);
|
|
70
|
+
expect(git("add", "-A").status).toBe(0);
|
|
71
|
+
expect(git("commit", "-q", "-m", "init").status).toBe(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
beforeEach(async () => {
|
|
75
|
+
project = await fs.realpath(await mkdtemp(join(tmpdir(), "gitig-prof-")));
|
|
76
|
+
log = spyOn(console, "log").mockImplementation(() => {});
|
|
77
|
+
});
|
|
78
|
+
afterEach(async () => {
|
|
79
|
+
log.mockRestore();
|
|
80
|
+
await rm(project, { recursive: true, force: true });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("the startup question names tracked generated files", () => {
|
|
84
|
+
test("B7: in a project with profiles, a committed settings.json is tracked-but-should-ignore", async () => {
|
|
85
|
+
await commitSettings(true);
|
|
86
|
+
|
|
87
|
+
const need = await startupGitignoreNeed(project);
|
|
88
|
+
|
|
89
|
+
expect(need?.generated).toEqual([".claude/settings.json"]);
|
|
90
|
+
const { violations } = await loadGitignoreState(project);
|
|
91
|
+
expect(violations).toContainEqual({
|
|
92
|
+
kind: "tracked-but-should-ignore",
|
|
93
|
+
path: ".claude/settings.json",
|
|
94
|
+
source: "profile",
|
|
95
|
+
severity: "destructive",
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("B7: without profiles, a committed settings.json is the team's file — nothing generated to ask about", async () => {
|
|
100
|
+
await commitSettings(false);
|
|
101
|
+
expect((await startupGitignoreNeed(project))?.generated ?? []).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("B7: nothing is remembered — dismissing leaves the question for the next open", async () => {
|
|
105
|
+
await commitSettings(true);
|
|
106
|
+
// Two opens, the first dismissed (dismiss changes nothing).
|
|
107
|
+
const first = await startupGitignoreNeed(project);
|
|
108
|
+
const second = await startupGitignoreNeed(project);
|
|
109
|
+
expect(first?.generated).toEqual([".claude/settings.json"]);
|
|
110
|
+
expect(second).toEqual(first);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("B7: after a first adoption in the same session, the re-check asks about the file adoption made generated", async () => {
|
|
114
|
+
await commitSettings(false);
|
|
115
|
+
expect((await startupGitignoreNeed(project))?.generated ?? []).toEqual([]);
|
|
116
|
+
|
|
117
|
+
// The profile modal's "Save what you have".
|
|
118
|
+
await applyProfileChoice(
|
|
119
|
+
{ kind: "adopt", id: "default" },
|
|
120
|
+
{ version: 2, profiles: {} },
|
|
121
|
+
project,
|
|
122
|
+
{ onDisplace: () => {} },
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect((await startupGitignoreNeed(project))?.generated).toEqual([
|
|
126
|
+
".claude/settings.json",
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("the startup fix untracks generated files in place", () => {
|
|
132
|
+
test("B7: only generated files are untracked; other violations stay; the removal is staged", async () => {
|
|
133
|
+
await commitSettings(true);
|
|
134
|
+
// Another violation magus does not own: a committed personal settings file.
|
|
135
|
+
await fs.outputJson(join(project, ".claude", "settings.local.json"), {});
|
|
136
|
+
expect(git("add", "-f", ".claude/settings.local.json").status).toBe(0);
|
|
137
|
+
expect(git("commit", "-q", "-m", "local").status).toBe(0);
|
|
138
|
+
|
|
139
|
+
const result = await applyStartupFixes(project);
|
|
140
|
+
|
|
141
|
+
expect(result.untracked).toEqual([".claude/settings.json"]);
|
|
142
|
+
expect(result.failed).toEqual([]);
|
|
143
|
+
const status = git("status", "--porcelain").stdout;
|
|
144
|
+
expect(status).toContain("D .claude/settings.json");
|
|
145
|
+
expect(status).not.toContain("settings.local.json");
|
|
146
|
+
expect(git("ls-files", ".claude/settings.local.json").stdout.trim()).toBe(
|
|
147
|
+
".claude/settings.local.json",
|
|
148
|
+
);
|
|
149
|
+
// The file itself is kept, and now ignored.
|
|
150
|
+
expect(await fs.pathExists(join(project, ".claude", "settings.json"))).toBe(
|
|
151
|
+
true,
|
|
152
|
+
);
|
|
153
|
+
expect(await fs.readFile(join(project, ".gitignore"), "utf8")).toContain(
|
|
154
|
+
".claude/settings.json",
|
|
155
|
+
);
|
|
156
|
+
expect(describeStartupFix(result)).toContain(
|
|
157
|
+
"Stopped tracking (the removal is staged — commit it):",
|
|
158
|
+
);
|
|
159
|
+
// Asked again on the next open: nothing generated is tracked any more.
|
|
160
|
+
expect((await startupGitignoreNeed(project))?.generated ?? []).toEqual([]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("B7: the modal offers the in-place fix and names the files", async () => {
|
|
164
|
+
const modal = buildGitignoreModal({
|
|
165
|
+
violationCount: 3,
|
|
166
|
+
safeCount: 2,
|
|
167
|
+
generated: [".claude/settings.json"],
|
|
168
|
+
onFixSafe: () => {},
|
|
169
|
+
onOpenTab: () => {},
|
|
170
|
+
onDismiss: () => {},
|
|
171
|
+
});
|
|
172
|
+
if (modal.type !== "select") throw new Error("type guard");
|
|
173
|
+
expect(modal.options.map((o) => o.value)).toEqual([
|
|
174
|
+
"fix-safe",
|
|
175
|
+
"open-tab",
|
|
176
|
+
"dismiss",
|
|
177
|
+
]);
|
|
178
|
+
expect(modal.options[0]?.label).toBe(
|
|
179
|
+
"Stop tracking 1 generated file and add 2 missing .gitignore entries",
|
|
180
|
+
);
|
|
181
|
+
expect(modal.message).toContain(" .claude/settings.json");
|
|
182
|
+
expect(modal.message).toContain("expected to be gitignored");
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe("review: what the profile tier must never claim", () => {
|
|
187
|
+
test("B7 review M: a committed skill an old marker records as magus's is never flagged, never untracked, and handed back", async () => {
|
|
188
|
+
expect(git("init", "-q").status).toBe(0);
|
|
189
|
+
await fs.outputFile(
|
|
190
|
+
join(project, ".claude", "skills", "review", "SKILL.md"),
|
|
191
|
+
"---\nname: review\n---\nteam skill\n",
|
|
192
|
+
);
|
|
193
|
+
expect(git("add", "-A").status).toBe(0);
|
|
194
|
+
expect(git("commit", "-q", "-m", "team skill").status).toBe(0);
|
|
195
|
+
await writeManifest(MANIFEST, project);
|
|
196
|
+
await activateProfile(MANIFEST, "team", project, { onDisplace: () => {} });
|
|
197
|
+
// What 7.5.0's conversion wrote: the committed folder recorded as installed.
|
|
198
|
+
const marker = await readActiveMarker(project);
|
|
199
|
+
if (!marker) throw new Error("no marker");
|
|
200
|
+
await writeActiveMarker(
|
|
201
|
+
{
|
|
202
|
+
...marker,
|
|
203
|
+
skills: {
|
|
204
|
+
review: (await folderHash(
|
|
205
|
+
join(project, ".claude", "skills", "review"),
|
|
206
|
+
)) as string,
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
project,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(
|
|
213
|
+
(await startupGitignoreNeed(project))?.generated ?? [],
|
|
214
|
+
).not.toContain(".claude/skills/review/SKILL.md");
|
|
215
|
+
const fixed = await applyStartupFixes(project);
|
|
216
|
+
expect(fixed.untracked).not.toContain(".claude/skills/review/SKILL.md");
|
|
217
|
+
expect(git("diff", "--cached", "--name-only").stdout).toBe("");
|
|
218
|
+
|
|
219
|
+
// The next marker write drops it, and says so once.
|
|
220
|
+
const prep = await prepareProfileState(project);
|
|
221
|
+
expect(prep.lines).toContain(
|
|
222
|
+
"Skill review is committed by the team — magus no longer manages it",
|
|
223
|
+
);
|
|
224
|
+
expect((await readActiveMarker(project))?.skills).toEqual({});
|
|
225
|
+
expect(
|
|
226
|
+
await fs.pathExists(
|
|
227
|
+
join(project, ".claude", "skills", "review", "SKILL.md"),
|
|
228
|
+
),
|
|
229
|
+
).toBe(true);
|
|
230
|
+
expect((await prepareProfileState(project)).lines.join("\n")).not.toContain(
|
|
231
|
+
"committed by the team",
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("B7 review L2: a left-over marker in a repo that dropped profiles.json is not profiled", async () => {
|
|
236
|
+
expect(git("init", "-q").status).toBe(0);
|
|
237
|
+
await writeManifest(MANIFEST, project);
|
|
238
|
+
await activateProfile(MANIFEST, "team", project, { onDisplace: () => {} });
|
|
239
|
+
await fs.remove(join(project, ".claude", "profiles.json"));
|
|
240
|
+
expect(git("add", "-f", ".claude/settings.json").status).toBe(0);
|
|
241
|
+
expect(
|
|
242
|
+
git("commit", "-q", "-m", "team commits settings again").status,
|
|
243
|
+
).toBe(0);
|
|
244
|
+
|
|
245
|
+
expect((await startupGitignoreNeed(project))?.generated ?? []).toEqual([]);
|
|
246
|
+
expect((await applyStartupFixes(project)).untracked).toEqual([]);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -14,6 +14,10 @@ import fs from "fs-extra";
|
|
|
14
14
|
import { runModelsCommand } from "../cli/models.js";
|
|
15
15
|
import { presetNames } from "../data/models-presets.js";
|
|
16
16
|
import { writeManifest } from "../services/manifest.js";
|
|
17
|
+
import {
|
|
18
|
+
clearMateCatalogCache,
|
|
19
|
+
loadMateCatalog,
|
|
20
|
+
} from "../services/mate-catalog.js";
|
|
17
21
|
import { activateProfile } from "../services/profile-activate.js";
|
|
18
22
|
|
|
19
23
|
let logs: string[];
|
|
@@ -31,10 +35,17 @@ beforeEach(async () => {
|
|
|
31
35
|
configDir = await mkdtemp(join(tmpdir(), "modelscli-config-"));
|
|
32
36
|
previousConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
|
33
37
|
process.env.CLAUDE_CONFIG_DIR = configDir;
|
|
38
|
+
// `models list` with a mate asks the catalogue, whose default fetcher spawns the real
|
|
39
|
+
// `claudish --models` — about 2s on a machine that has it, with an 8s budget, so the first
|
|
40
|
+
// test to reach it timed out under load and passed on CI, which has no claudish. An empty
|
|
41
|
+
// catalogue is the no-claudish case, and every assertion here holds against it.
|
|
42
|
+
clearMateCatalogCache();
|
|
43
|
+
await loadMateCatalog(async () => null);
|
|
34
44
|
});
|
|
35
45
|
|
|
36
46
|
afterEach(async () => {
|
|
37
47
|
mock.restore();
|
|
48
|
+
clearMateCatalogCache();
|
|
38
49
|
// biome-ignore lint/performance/noDelete: absence is the intent, not a shortcut
|
|
39
50
|
if (previousConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
40
51
|
else process.env.CLAUDE_CONFIG_DIR = previousConfigDir;
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
activationNotes,
|
|
18
18
|
recordRefetchedSkills,
|
|
19
19
|
trackFetchedSkills,
|
|
20
|
+
trackedGeneratedWarnings,
|
|
20
21
|
updatableSkills,
|
|
21
22
|
} from "../services/profile-activate.js";
|
|
22
23
|
import {
|
|
@@ -459,42 +460,36 @@ describe("older layout: committed custom skills the seeding displaced come back"
|
|
|
459
460
|
expect(again.problems).toEqual([]);
|
|
460
461
|
});
|
|
461
462
|
|
|
462
|
-
test("review
|
|
463
|
+
test("B7 review: a magus-installed folder someone committed is handed back to the team — kept, never warned about", async () => {
|
|
463
464
|
await start();
|
|
464
465
|
expect(git(project, "init", "-q").status).toBe(0);
|
|
465
466
|
expect(git(project, "add", "-f", ".claude/skills/alpha").status).toBe(0);
|
|
466
467
|
expect(git(project, "commit", "-q", "-m", "oops").status).toBe(0);
|
|
467
|
-
|
|
468
|
-
//
|
|
468
|
+
expect(await trackedGeneratedWarnings(project)).toEqual([]);
|
|
469
|
+
// A teammate drops alpha from the profile. Were it still magus's, the
|
|
470
|
+
// reconcile would delete the team's committed folder.
|
|
469
471
|
const pulled = manifest();
|
|
470
472
|
(pulled.profiles.team as { skills: ProfileSkillRef[] }).skills = [];
|
|
471
473
|
await writeManifest(pulled, project);
|
|
472
|
-
const marker = await fs.readFile(
|
|
473
|
-
join(project, ".claude", "profiles", "active.json"),
|
|
474
|
-
"utf8",
|
|
475
|
-
);
|
|
476
|
-
const manifestBytes = await fs.readFile(
|
|
477
|
-
join(project, ".claude", "profiles.json"),
|
|
478
|
-
"utf8",
|
|
479
|
-
);
|
|
480
474
|
|
|
481
475
|
const prep = await prepareProfileState(project, {
|
|
482
476
|
installSkill: fakeInstall,
|
|
483
477
|
});
|
|
484
478
|
|
|
485
|
-
expect(prep.problems[
|
|
486
|
-
|
|
479
|
+
expect(prep.problems).toEqual([]);
|
|
480
|
+
expect(prep.lines).toContain(
|
|
481
|
+
"Skill alpha is committed by the team — magus no longer manages it",
|
|
487
482
|
);
|
|
488
483
|
expect(await fs.pathExists(skill("alpha"))).toBe(true);
|
|
484
|
+
expect((await readActiveMarker(project))?.skills).toEqual({});
|
|
489
485
|
expect(
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
).toBe(manifestBytes);
|
|
486
|
+
git(project, "status", "--porcelain", "--", ".claude/skills").stdout,
|
|
487
|
+
).toBe("");
|
|
488
|
+
// Said once: the marker no longer records it.
|
|
489
|
+
const again = await prepareProfileState(project, {
|
|
490
|
+
installSkill: fakeInstall,
|
|
491
|
+
});
|
|
492
|
+
expect(again.lines.join("\n")).not.toContain("committed by the team");
|
|
498
493
|
});
|
|
499
494
|
|
|
500
495
|
test("an existing folder is never overwritten; a differing copy is reported with both paths", async () => {
|