magus-cli 7.5.2 → 7.5.4
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__/blackbox-convergence.test.ts +1 -2
- package/src/__tests__/blackbox-parity.test.ts +16 -14
- package/src/__tests__/blackbox-plugin-state.test.ts +13 -15
- package/src/__tests__/cli-apply-seams.test.ts +3 -3
- package/src/__tests__/cli-profile-prompts.test.ts +1165 -0
- package/src/__tests__/content-drift.test.ts +0 -1
- package/src/__tests__/files-missing-scope.test.ts +235 -0
- package/src/__tests__/{enabled-not-installed.test.ts → files-missing.test.ts} +32 -26
- package/src/__tests__/format-update-outcome.test.ts +367 -0
- package/src/__tests__/gitignore-check.test.ts +11 -29
- package/src/__tests__/gitignore-profile.test.ts +248 -0
- package/src/__tests__/installed-version-scope.test.tsx +425 -0
- package/src/__tests__/mate-availability.test.ts +11 -13
- package/src/__tests__/models-cli.test.ts +11 -0
- package/src/__tests__/plugin-contents.test.ts +0 -1
- package/src/__tests__/plugin-detail-scopes.test.tsx +270 -0
- package/src/__tests__/plugin-row-text.test.ts +18 -21
- package/src/__tests__/profile-command.test.ts +2 -2
- package/src/__tests__/profile-migration-offer.test.ts +5 -4
- package/src/__tests__/profile-skills.test.ts +17 -22
- package/src/__tests__/profile-startup-nested.test.ts +329 -0
- package/src/__tests__/profile-startup-old-format.test.ts +390 -0
- package/src/__tests__/profile-state.test.ts +152 -164
- package/src/__tests__/scope-action.test.ts +35 -38
- package/src/__tests__/scope-row-lookup.test.ts +126 -0
- package/src/__tests__/select-restore.test.ts +209 -0
- package/src/__tests__/stale-load-repair.test.ts +1 -2
- package/src/__tests__/update-apply.test.ts +18 -2
- package/src/__tests__/update-null-readback.test.ts +300 -0
- package/src/__tests__/update-plan-parity.test.ts +249 -0
- package/src/__tests__/update-plan.test.ts +66 -67
- package/src/__tests__/version-unknown-state.test.ts +38 -39
- package/src/cli/bootstrap.ts +239 -70
- package/src/cli/doctor.ts +7 -20
- package/src/cli/install.ts +83 -27
- package/src/cli/prompt.ts +7 -3
- package/src/cli/router.ts +6 -4
- package/src/cli/select.ts +168 -0
- package/src/cli/update-view.ts +118 -6
- package/src/cli/update.ts +10 -1
- package/src/services/claude-cli.ts +0 -24
- package/src/services/claude-settings.ts +28 -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/manifest.ts +31 -0
- package/src/services/mate-availability.ts +23 -29
- package/src/services/plugin-manager.ts +247 -105
- package/src/services/profile-absorb.ts +41 -24
- package/src/services/profile-activate.ts +102 -127
- package/src/services/profile-startup.ts +160 -4
- package/src/services/profile-state.ts +7 -68
- package/src/services/tracked-live-files.ts +109 -74
- package/src/services/update-engine.ts +28 -10
- package/src/services/update-plan.ts +78 -94
- package/src/ui/App.tsx +41 -35
- package/src/ui/adapters/pluginsAdapter.ts +1 -1
- package/src/ui/components/primitives/ScopeSquares.tsx +10 -5
- package/src/ui/hooks/useGitignoreModal.ts +63 -17
- package/src/ui/hooks/useProfileStartupModal.ts +94 -9
- package/src/ui/keymap.ts +1 -1
- package/src/ui/renderers/pluginRenderers.tsx +39 -45
- package/src/ui/renderers/pluginRowText.ts +6 -2
- package/src/ui/renderers/pluginStatusText.ts +4 -3
- package/src/ui/screens/PluginsScreen.tsx +30 -52
- package/src/ui/screens/ProfilesScreen.tsx +3 -10
- package/src/cli/tui-prompt.tsx +0 -143
|
@@ -61,8 +61,7 @@ import {
|
|
|
61
61
|
type ActivateOptions,
|
|
62
62
|
activateProfile,
|
|
63
63
|
activationNotes,
|
|
64
|
-
|
|
65
|
-
writeRefusal,
|
|
64
|
+
releasedCommittedSkillNote,
|
|
66
65
|
} from "./profile-activate.js";
|
|
67
66
|
import {
|
|
68
67
|
type ActiveMarker,
|
|
@@ -94,6 +93,7 @@ import {
|
|
|
94
93
|
} from "./profile-skills.js";
|
|
95
94
|
import { resolveExtends, resolveProfile } from "./resolver.js";
|
|
96
95
|
import { SettingsParseError } from "./settings-file.js";
|
|
96
|
+
import { trackedSkillFolders } from "./tracked-live-files.js";
|
|
97
97
|
|
|
98
98
|
// ─── Changes ─────────────────────────────────────────────────────────────────
|
|
99
99
|
|
|
@@ -947,7 +947,7 @@ export async function absorbOrphanedProfile(
|
|
|
947
947
|
);
|
|
948
948
|
if (!base && changes.length > 0) {
|
|
949
949
|
result.lines.push(
|
|
950
|
-
`There is no record of what "${orphan}" contained, so
|
|
950
|
+
`There is no record of what "${orphan}" contained, so every plugin and setting here that "${target}" lacked was kept`,
|
|
951
951
|
);
|
|
952
952
|
}
|
|
953
953
|
result.lines.push(...result.changes.map((c) => describeChange(c, target)));
|
|
@@ -1022,7 +1022,7 @@ export async function absorbOlderLayout(
|
|
|
1022
1022
|
: new SettingsParseError(livePath(key, projectPath), error);
|
|
1023
1023
|
result.unreadable.push(parseError);
|
|
1024
1024
|
result.problems.push(
|
|
1025
|
-
`${rel(projectPath, key)} cannot be read (${parseError.detail}) — fix it by hand; magus
|
|
1025
|
+
`${rel(projectPath, key)} cannot be read (${parseError.detail}) — fix it by hand; magus did not update the old profile format`,
|
|
1026
1026
|
);
|
|
1027
1027
|
continue;
|
|
1028
1028
|
}
|
|
@@ -1056,8 +1056,8 @@ export async function absorbOlderLayout(
|
|
|
1056
1056
|
result.changes.push(...changes);
|
|
1057
1057
|
result.lines.push(
|
|
1058
1058
|
exact
|
|
1059
|
-
? `Changes Claude Code made
|
|
1060
|
-
: `Profile "${named ?? "?"}" no longer exists in profiles.json — its unsaved changes
|
|
1059
|
+
? `Changes Claude Code made were added to profile "${into}"`
|
|
1060
|
+
: `Profile "${named ?? "?"}" no longer exists in profiles.json — its unsaved changes were added to profile "${into}"`,
|
|
1061
1061
|
);
|
|
1062
1062
|
if (!exact) {
|
|
1063
1063
|
result.lines.push(
|
|
@@ -1072,7 +1072,7 @@ export async function absorbOlderLayout(
|
|
|
1072
1072
|
);
|
|
1073
1073
|
if (absorbed.keptMore) {
|
|
1074
1074
|
result.lines.push(
|
|
1075
|
-
|
|
1075
|
+
"profiles.json has entries the old profile format lacked; they were kept — a removal made in the old format is not applied (the old format is backed up)",
|
|
1076
1076
|
);
|
|
1077
1077
|
}
|
|
1078
1078
|
return result;
|
|
@@ -1114,7 +1114,7 @@ async function olderRenderIds(
|
|
|
1114
1114
|
|
|
1115
1115
|
/** One line for a value the older layout's file and profiles.json disagree on. */
|
|
1116
1116
|
export function describeLayoutConflict(at: string, profile: string): string {
|
|
1117
|
-
return `Profile "${profile}": ${at} differs between the
|
|
1117
|
+
return `Profile "${profile}": ${at} differs between the old profile format and profiles.json — kept profiles.json's value (the old one is backed up)`;
|
|
1118
1118
|
}
|
|
1119
1119
|
|
|
1120
1120
|
/**
|
|
@@ -1302,13 +1302,39 @@ export async function reconcileActiveProfile(
|
|
|
1302
1302
|
opts: ReconcileOptions = {},
|
|
1303
1303
|
): Promise<ReconcileResult> {
|
|
1304
1304
|
if (!(await hasCurrentMarker(projectPath))) return emptyResult(null);
|
|
1305
|
-
|
|
1305
|
+
let marker = await readActiveMarker(projectPath);
|
|
1306
1306
|
if (!marker) return emptyResult(null);
|
|
1307
1307
|
const manifest = await readManifest(projectPath);
|
|
1308
1308
|
const current = manifest.profiles[marker.profile];
|
|
1309
1309
|
if (!current) return emptyResult(null);
|
|
1310
1310
|
const name = marker.profile;
|
|
1311
1311
|
|
|
1312
|
+
// A recorded skill folder git tracks is the team's copy: 7.5.0's conversion
|
|
1313
|
+
// recorded committed skills as magus-installed. Handed back — dropped from
|
|
1314
|
+
// the marker, and from magus's ignore lines — before anything below could
|
|
1315
|
+
// treat it as magus's to sync or remove, and said once.
|
|
1316
|
+
const handedBackLines: string[] = [];
|
|
1317
|
+
const handedBack = await trackedSkillFolders(
|
|
1318
|
+
projectPath,
|
|
1319
|
+
Object.keys(marker.skills),
|
|
1320
|
+
);
|
|
1321
|
+
if (handedBack.size > 0) {
|
|
1322
|
+
const skills: Record<string, string> = {};
|
|
1323
|
+
for (const [folder, hash] of Object.entries(marker.skills)) {
|
|
1324
|
+
if (!handedBack.has(folder)) skills[folder] = hash;
|
|
1325
|
+
}
|
|
1326
|
+
await updateSkillIgnoreLines(
|
|
1327
|
+
projectPath,
|
|
1328
|
+
[...handedBack],
|
|
1329
|
+
Object.keys(skills),
|
|
1330
|
+
);
|
|
1331
|
+
marker = { ...marker, skills };
|
|
1332
|
+
await writeActiveMarker(marker, projectPath);
|
|
1333
|
+
handedBackLines.push(
|
|
1334
|
+
...[...handedBack].sort().map(releasedCommittedSkillNote),
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1312
1338
|
const edited: LiveFileKey[] = [];
|
|
1313
1339
|
const missing: LiveFileKey[] = [];
|
|
1314
1340
|
for (const key of LIVE_FILE_KEYS) {
|
|
@@ -1343,23 +1369,11 @@ export async function reconcileActiveProfile(
|
|
|
1343
1369
|
!definitionChanged &&
|
|
1344
1370
|
!marker.cliBookkeeping
|
|
1345
1371
|
) {
|
|
1346
|
-
return emptyResult(name);
|
|
1372
|
+
return { ...emptyResult(name), lines: handedBackLines };
|
|
1347
1373
|
}
|
|
1348
1374
|
|
|
1349
1375
|
const result = emptyResult(name);
|
|
1350
|
-
|
|
1351
|
-
// Every write below lands in a live file, a skill folder the profile
|
|
1352
|
-
// installs (or the manifest, on the way to one). None happens when git
|
|
1353
|
-
// tracks one of them, or cannot say: see `tracked-live-files.ts`.
|
|
1354
|
-
const refusal = await writeRefusal(
|
|
1355
|
-
projectPath,
|
|
1356
|
-
await activeWritableSkillFolders(projectPath, manifest),
|
|
1357
|
-
);
|
|
1358
|
-
if (refusal) {
|
|
1359
|
-
result.problems.push(refusal.message);
|
|
1360
|
-
result.lines.push(refusal.message);
|
|
1361
|
-
return result;
|
|
1362
|
-
}
|
|
1376
|
+
result.lines.push(...handedBackLines);
|
|
1363
1377
|
|
|
1364
1378
|
// ── a. absorb ────────────────────────────────────────────────────────────
|
|
1365
1379
|
const base = marker.base ?? current;
|
|
@@ -1511,7 +1525,10 @@ export async function reconcileActiveProfile(
|
|
|
1511
1525
|
installSkill: opts.installSkill,
|
|
1512
1526
|
},
|
|
1513
1527
|
);
|
|
1514
|
-
|
|
1528
|
+
// A re-render in the background never reports a tracked file: the
|
|
1529
|
+
// command that ran it reports that once, itself, and repeating it here
|
|
1530
|
+
// printed the same warning three times in one `magus update`.
|
|
1531
|
+
notes.push(...activationNotes(activation, { tracked: false }));
|
|
1515
1532
|
} catch (error) {
|
|
1516
1533
|
result.problems.push(
|
|
1517
1534
|
error instanceof Error ? error.message : String(error),
|
|
@@ -75,10 +75,11 @@ import {
|
|
|
75
75
|
updateSkillIgnoreLines,
|
|
76
76
|
} from "./profile-skills.js";
|
|
77
77
|
import {
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
GitCheckError,
|
|
79
|
+
type TrackedNotice,
|
|
80
|
+
trackedNotice,
|
|
80
81
|
trackedPaths,
|
|
81
|
-
|
|
82
|
+
trackedSkillFolders,
|
|
82
83
|
} from "./tracked-live-files.js";
|
|
83
84
|
import type { ApplyReporter } from "./update-engine.js";
|
|
84
85
|
|
|
@@ -91,8 +92,12 @@ export interface ActivationResult {
|
|
|
91
92
|
skills: SkillSyncResult;
|
|
92
93
|
/** What converting an older layout did, if there was one. */
|
|
93
94
|
conversion: LegacySkillConversion;
|
|
94
|
-
/**
|
|
95
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Generated paths git still tracks (see {@link trackedGeneratedFiles}) —
|
|
97
|
+
* written all the same, and each warned about. A string instead when git
|
|
98
|
+
* could not say (`GitCheckError`).
|
|
99
|
+
*/
|
|
100
|
+
tracked: string[] | string;
|
|
96
101
|
/** Where `.claude/_profiles/` was kept, project-relative, or null. */
|
|
97
102
|
keptAt: string | null;
|
|
98
103
|
/** Unsaved changes added to a profile before activating, one line each. */
|
|
@@ -101,6 +106,11 @@ export interface ActivationResult {
|
|
|
101
106
|
dependencies?: DependencyReport;
|
|
102
107
|
/** Listed skills whose folder the team committed: that copy is used. */
|
|
103
108
|
committedSkills?: string[];
|
|
109
|
+
/**
|
|
110
|
+
* Skill folders the marker recorded as magus-installed that git tracks —
|
|
111
|
+
* handed back to the team (see `releasedCommittedSkillNote`).
|
|
112
|
+
*/
|
|
113
|
+
releasedCommitted?: string[];
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
export interface ActivateOptions {
|
|
@@ -118,25 +128,27 @@ export function reportDisplaced(
|
|
|
118
128
|
projectPath: string,
|
|
119
129
|
): (artifact: DisplacedArtifact) => void {
|
|
120
130
|
const rel = (p: string) => path.relative(projectPath, p);
|
|
121
|
-
return ({ link
|
|
122
|
-
|
|
131
|
+
return ({ link }) =>
|
|
132
|
+
// The user's file, never where the backup went: that is magus's own
|
|
133
|
+
// storage, and the owner wants only what the user uses on screen.
|
|
134
|
+
// `savedTo` stays in the result for anything that needs to find it.
|
|
135
|
+
console.log(` Backed up your previous ${rel(link)}`);
|
|
123
136
|
}
|
|
124
137
|
|
|
125
138
|
/**
|
|
126
|
-
* Generated
|
|
127
|
-
* `.claude/skills` SYMLINK (an older layout committed by mistake)
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
139
|
+
* Generated files that git tracks: the three live files, and a committed
|
|
140
|
+
* `.claude/skills` SYMLINK (an older layout committed by mistake). A skill
|
|
141
|
+
* FOLDER git tracks is never one of them — it is the team's copy (see
|
|
142
|
+
* `trackedSkillFolders`). Empty outside a git repository; throws
|
|
143
|
+
* `GitCheckError` when git cannot say. Reported only — nothing refuses to
|
|
144
|
+
* write because of it (see `tracked-live-files.ts`).
|
|
131
145
|
*/
|
|
132
|
-
export async function
|
|
146
|
+
export async function trackedGeneratedFiles(
|
|
133
147
|
projectPath: string,
|
|
134
|
-
skillFolders: readonly string[] = [],
|
|
135
148
|
): Promise<string[]> {
|
|
136
149
|
const rel = (abs: string) => path.relative(projectPath, abs);
|
|
137
150
|
const live = LIVE_FILE_KEYS.map((key) => rel(livePath(key, projectPath)));
|
|
138
151
|
const skills = rel(skillsRoot(projectPath));
|
|
139
|
-
const folders = skillFolders.map((folder) => path.join(skills, folder));
|
|
140
152
|
let skillsIsLink = false;
|
|
141
153
|
try {
|
|
142
154
|
skillsIsLink = (await lstat(skillsRoot(projectPath))).isSymbolicLink();
|
|
@@ -144,127 +156,75 @@ export async function trackedWriteTargets(
|
|
|
144
156
|
// No .claude/skills at all.
|
|
145
157
|
}
|
|
146
158
|
const files = [...live, ...(skillsIsLink ? [skills] : [])];
|
|
147
|
-
const tracked = await trackedPaths(projectPath, files
|
|
148
|
-
return
|
|
159
|
+
const tracked = await trackedPaths(projectPath, files);
|
|
160
|
+
return files.filter((c) => tracked.has(c));
|
|
149
161
|
}
|
|
150
162
|
|
|
151
163
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* manages it, so it never has cause to suggest untracking it.
|
|
164
|
+
* {@link trackedGeneratedFiles} as the notices the CLI prints: one per tracked
|
|
165
|
+
* path, or one saying git could not be asked.
|
|
155
166
|
*/
|
|
156
|
-
export async function
|
|
167
|
+
export async function trackedGeneratedNotices(
|
|
157
168
|
projectPath: string,
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return new Set(folders.filter((f) => tracked.has(path.join(root, f))));
|
|
169
|
+
): Promise<TrackedNotice[]> {
|
|
170
|
+
try {
|
|
171
|
+
return (await trackedGeneratedFiles(projectPath)).map(trackedNotice);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (error instanceof GitCheckError) {
|
|
174
|
+
return [{ problem: error.message, fix: [] }];
|
|
175
|
+
}
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
168
178
|
}
|
|
169
179
|
|
|
170
|
-
/**
|
|
171
|
-
export
|
|
172
|
-
|
|
173
|
-
):
|
|
174
|
-
return
|
|
180
|
+
/** The tracked-path notices an activation result carries. */
|
|
181
|
+
export function activationTrackedNotices(
|
|
182
|
+
result: ActivationResult,
|
|
183
|
+
): TrackedNotice[] {
|
|
184
|
+
return typeof result.tracked === "string"
|
|
185
|
+
? [{ problem: result.tracked, fix: [] }]
|
|
186
|
+
: result.tracked.map(trackedNotice);
|
|
175
187
|
}
|
|
176
188
|
|
|
177
189
|
/**
|
|
178
|
-
* The
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* activation refuses as a clash rather than writing.
|
|
190
|
+
* The folders of `skills` the team committed under `.claude/skills/`. Such a
|
|
191
|
+
* folder is the team's copy of the skill: magus neither installs over it nor
|
|
192
|
+
* manages it, so it never has cause to suggest untracking it. None when git
|
|
193
|
+
* cannot say — each existing folder is then judged as a custom skill.
|
|
183
194
|
*/
|
|
184
|
-
export async function
|
|
195
|
+
export async function committedSkillFolders(
|
|
185
196
|
projectPath: string,
|
|
186
197
|
skills: readonly ProfileSkillRef[],
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const out = new Set(Object.keys(managed));
|
|
190
|
-
const missing: ProfileSkillRef[] = [];
|
|
191
|
-
for (const ref of skills) {
|
|
192
|
-
const folder = skillFolder(ref);
|
|
193
|
-
if (out.has(folder)) continue;
|
|
194
|
-
try {
|
|
195
|
-
await lstat(path.join(skillsRoot(projectPath), folder));
|
|
196
|
-
} catch {
|
|
197
|
-
missing.push(ref);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
const committed = await committedSkillFolders(projectPath, missing);
|
|
201
|
-
for (const ref of missing) {
|
|
202
|
-
if (!committed.has(skillFolder(ref))) out.add(skillFolder(ref));
|
|
203
|
-
}
|
|
204
|
-
return [...out];
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* The skill folders the ACTIVE profile's next render could write — see
|
|
209
|
-
* {@link writableSkillFolders}. For the checks made before a reconcile, which
|
|
210
|
-
* may re-render and so install or remove them.
|
|
211
|
-
*/
|
|
212
|
-
export async function activeWritableSkillFolders(
|
|
213
|
-
projectPath: string,
|
|
214
|
-
manifest: ProfileManifest,
|
|
215
|
-
): Promise<string[]> {
|
|
216
|
-
const marker = await readActiveMarker(projectPath);
|
|
217
|
-
if (!marker) return [];
|
|
218
|
-
return writableSkillFolders(
|
|
198
|
+
): Promise<Set<string>> {
|
|
199
|
+
return trackedSkillFolders(
|
|
219
200
|
projectPath,
|
|
220
|
-
|
|
221
|
-
marker.skills,
|
|
201
|
+
skills.map((ref) => skillFolder(ref)),
|
|
222
202
|
);
|
|
223
203
|
}
|
|
224
204
|
|
|
225
|
-
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
*/
|
|
229
|
-
export async function writeRefusal(
|
|
230
|
-
projectPath: string,
|
|
231
|
-
skillFolders: readonly string[] = [],
|
|
232
|
-
): Promise<LiveFileRefusal | null> {
|
|
233
|
-
try {
|
|
234
|
-
await assertNoTrackedWriteTargets(projectPath, skillFolders);
|
|
235
|
-
return null;
|
|
236
|
-
} catch (error) {
|
|
237
|
-
if (error instanceof LiveFileRefusal) return error;
|
|
238
|
-
throw error;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Throw {@link TrackedLiveFileError} — before anything is written — when any
|
|
244
|
-
* of {@link trackedWriteTargets} is tracked, or `GitCheckError` when git
|
|
245
|
-
* cannot say.
|
|
246
|
-
*/
|
|
247
|
-
export async function assertNoTrackedWriteTargets(
|
|
248
|
-
projectPath: string,
|
|
249
|
-
skillFolders: readonly string[] = [],
|
|
250
|
-
): Promise<void> {
|
|
251
|
-
const tracked = await trackedWriteTargets(projectPath, skillFolders);
|
|
252
|
-
if (tracked.length > 0) throw new TrackedLiveFileError(tracked);
|
|
205
|
+
/** The line for a recorded skill folder handed back to the team. */
|
|
206
|
+
export function releasedCommittedSkillNote(folder: string): string {
|
|
207
|
+
return `Skill ${folder} is committed by the team — magus no longer manages it`;
|
|
253
208
|
}
|
|
254
209
|
|
|
255
210
|
/**
|
|
256
211
|
* Lines describing what activation did beyond the switch itself, phrased for
|
|
257
212
|
* both the terminal and the TUI's outcome modal.
|
|
213
|
+
*
|
|
214
|
+
* `tracked: false` leaves the tracked-path notices out, for the CLI, which
|
|
215
|
+
* prints them itself as a warning with the fix beneath it
|
|
216
|
+
* ({@link activationTrackedNotices}).
|
|
258
217
|
*/
|
|
259
|
-
export function activationNotes(
|
|
218
|
+
export function activationNotes(
|
|
219
|
+
result: ActivationResult,
|
|
220
|
+
opts: { tracked?: boolean } = {},
|
|
221
|
+
): string[] {
|
|
260
222
|
const lines: string[] = [
|
|
261
223
|
...(result.absorbed ?? []),
|
|
262
224
|
...describeDependencies(result.dependencies, result.profileId),
|
|
263
225
|
];
|
|
264
226
|
for (const folder of result.conversion.restored) {
|
|
265
|
-
lines.push(
|
|
266
|
-
`Restored custom skill .claude/skills/${folder} (the older profile layout had moved it)`,
|
|
267
|
-
);
|
|
227
|
+
lines.push(`Restored your custom skill "${folder}"`);
|
|
268
228
|
}
|
|
269
229
|
for (const c of result.conversion.conflicts) {
|
|
270
230
|
lines.push(
|
|
@@ -274,20 +234,21 @@ export function activationNotes(result: ActivationResult): string[] {
|
|
|
274
234
|
for (const f of result.skills.failed) {
|
|
275
235
|
lines.push(`Skill "${f.skill}" could not be installed: ${f.reason}`);
|
|
276
236
|
}
|
|
237
|
+
for (const folder of result.releasedCommitted ?? []) {
|
|
238
|
+
lines.push(releasedCommittedSkillNote(folder));
|
|
239
|
+
}
|
|
277
240
|
for (const folder of result.committedSkills ?? []) {
|
|
278
241
|
lines.push(
|
|
279
242
|
`Skill .claude/skills/${folder} is committed to git — the team's copy is used; magus does not install or manage it`,
|
|
280
243
|
);
|
|
281
244
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
245
|
+
if (opts.tracked !== false) {
|
|
246
|
+
for (const n of activationTrackedNotices(result)) {
|
|
247
|
+
lines.push(n.problem, ...n.fix);
|
|
248
|
+
}
|
|
286
249
|
}
|
|
287
250
|
if (result.keptAt) {
|
|
288
|
-
lines.push(
|
|
289
|
-
`The older layout's .claude/_profiles/ was kept at ${result.keptAt}/`,
|
|
290
|
-
);
|
|
251
|
+
lines.push("A backup of the old profile format was kept");
|
|
291
252
|
}
|
|
292
253
|
return lines;
|
|
293
254
|
}
|
|
@@ -295,9 +256,10 @@ export function activationNotes(result: ActivationResult): string[] {
|
|
|
295
256
|
/**
|
|
296
257
|
* Render `profileId` from `manifest` and make it the active profile.
|
|
297
258
|
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
* nothing, when a live file of a managed project does not
|
|
259
|
+
* A generated file git tracks is written like any other, and named in
|
|
260
|
+
* `tracked` so the caller can warn about it. Throws `SettingsParseError`,
|
|
261
|
+
* having changed nothing, when a live file of a managed project does not
|
|
262
|
+
* parse. Throws
|
|
301
263
|
* {@link SkillClashError} — having
|
|
302
264
|
* changed nothing but moving an older layout's skills into `.claude/skills/` —
|
|
303
265
|
* when a listed skill's folder is already a custom skill.
|
|
@@ -315,21 +277,27 @@ export async function activateProfile(
|
|
|
315
277
|
// Read before converting: the older marker lives in the directory the
|
|
316
278
|
// conversion moves away, and an older layout is recognised by its links.
|
|
317
279
|
const previous = await readActiveMarker(projectPath);
|
|
318
|
-
// Before anything else is touched — the conversion below writes too.
|
|
319
|
-
await assertNoTrackedWriteTargets(
|
|
320
|
-
projectPath,
|
|
321
|
-
await writableSkillFolders(projectPath, skillRefs, previous?.skills),
|
|
322
|
-
);
|
|
323
280
|
const wasOldLayout = (await oldLayoutProfile(projectPath)) !== null;
|
|
324
281
|
// A managed project's live files are the profile's; one that does not
|
|
325
282
|
// parse is somebody mid-edit, and replacing it would lose their work. (At
|
|
326
283
|
// first adoption a hand-written file is moved to a backup, not replaced.)
|
|
327
284
|
if (previous !== null) await assertLiveFilesParse(projectPath);
|
|
328
285
|
const conversion = await convertLegacySkills(manifest, projectPath);
|
|
329
|
-
const
|
|
286
|
+
const recorded: Record<string, string> = {
|
|
330
287
|
...(previous?.skills ?? {}),
|
|
331
288
|
...conversion.managed,
|
|
332
289
|
};
|
|
290
|
+
// A recorded folder git tracks is the team's copy — 7.5.0's conversion
|
|
291
|
+
// recorded committed skills as magus-installed. Dropped from the record
|
|
292
|
+
// here, before a sync could remove it, and said once.
|
|
293
|
+
const committedRecorded = await trackedSkillFolders(
|
|
294
|
+
projectPath,
|
|
295
|
+
Object.keys(recorded),
|
|
296
|
+
);
|
|
297
|
+
const managedBefore: Record<string, string> = {};
|
|
298
|
+
for (const [folder, hash] of Object.entries(recorded)) {
|
|
299
|
+
if (!committedRecorded.has(folder)) managedBefore[folder] = hash;
|
|
300
|
+
}
|
|
333
301
|
|
|
334
302
|
// Listed skills the team committed and magus does not manage: that copy
|
|
335
303
|
// stands, and magus neither installs over it nor reports it as a clash.
|
|
@@ -397,7 +365,7 @@ export async function activateProfile(
|
|
|
397
365
|
);
|
|
398
366
|
await updateSkillIgnoreLines(
|
|
399
367
|
projectPath,
|
|
400
|
-
Object.keys(
|
|
368
|
+
Object.keys(recorded),
|
|
401
369
|
Object.keys(skills.skills),
|
|
402
370
|
);
|
|
403
371
|
await writeActiveMarker(
|
|
@@ -412,7 +380,12 @@ export async function activateProfile(
|
|
|
412
380
|
displaced,
|
|
413
381
|
skills,
|
|
414
382
|
conversion,
|
|
415
|
-
tracked: await trackedGeneratedFiles(projectPath)
|
|
383
|
+
tracked: await trackedGeneratedFiles(projectPath).catch(
|
|
384
|
+
(error: unknown) => {
|
|
385
|
+
if (error instanceof GitCheckError) return error.message;
|
|
386
|
+
throw error;
|
|
387
|
+
},
|
|
388
|
+
),
|
|
416
389
|
keptAt: conversion.keptAt
|
|
417
390
|
? path.relative(projectPath, conversion.keptAt)
|
|
418
391
|
: null,
|
|
@@ -420,6 +393,9 @@ export async function activateProfile(
|
|
|
420
393
|
...(committedSkills.size > 0
|
|
421
394
|
? { committedSkills: [...committedSkills].sort() }
|
|
422
395
|
: {}),
|
|
396
|
+
...(committedRecorded.size > 0
|
|
397
|
+
? { releasedCommitted: [...committedRecorded].sort() }
|
|
398
|
+
: {}),
|
|
423
399
|
};
|
|
424
400
|
}
|
|
425
401
|
|
|
@@ -445,7 +421,6 @@ export async function regenerateLiveFiles(
|
|
|
445
421
|
const marker = await readActiveMarker(projectPath);
|
|
446
422
|
const manifest = await readManifest(projectPath);
|
|
447
423
|
if (!marker || !manifest.profiles[marker.profile]) return false;
|
|
448
|
-
await assertNoTrackedWriteTargets(projectPath);
|
|
449
424
|
await assertLiveFilesParse(projectPath);
|
|
450
425
|
const rendered = await renderProfile(manifest, marker.profile, projectPath);
|
|
451
426
|
const files = await writeRenderedFiles(rendered, projectPath);
|