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
|
@@ -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
|
|
|
@@ -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;
|
|
@@ -75,10 +75,10 @@ import {
|
|
|
75
75
|
updateSkillIgnoreLines,
|
|
76
76
|
} from "./profile-skills.js";
|
|
77
77
|
import {
|
|
78
|
-
|
|
79
|
-
TrackedLiveFileError,
|
|
78
|
+
GitCheckError,
|
|
80
79
|
trackedPaths,
|
|
81
|
-
|
|
80
|
+
trackedSkillFolders,
|
|
81
|
+
trackedWarning,
|
|
82
82
|
} from "./tracked-live-files.js";
|
|
83
83
|
import type { ApplyReporter } from "./update-engine.js";
|
|
84
84
|
|
|
@@ -91,8 +91,12 @@ export interface ActivationResult {
|
|
|
91
91
|
skills: SkillSyncResult;
|
|
92
92
|
/** What converting an older layout did, if there was one. */
|
|
93
93
|
conversion: LegacySkillConversion;
|
|
94
|
-
/**
|
|
95
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Generated paths git still tracks (see {@link trackedGeneratedFiles}) —
|
|
96
|
+
* written all the same, and each warned about. A string instead when git
|
|
97
|
+
* could not say (`GitCheckError`).
|
|
98
|
+
*/
|
|
99
|
+
tracked: string[] | string;
|
|
96
100
|
/** Where `.claude/_profiles/` was kept, project-relative, or null. */
|
|
97
101
|
keptAt: string | null;
|
|
98
102
|
/** Unsaved changes added to a profile before activating, one line each. */
|
|
@@ -101,6 +105,11 @@ export interface ActivationResult {
|
|
|
101
105
|
dependencies?: DependencyReport;
|
|
102
106
|
/** Listed skills whose folder the team committed: that copy is used. */
|
|
103
107
|
committedSkills?: string[];
|
|
108
|
+
/**
|
|
109
|
+
* Skill folders the marker recorded as magus-installed that git tracks —
|
|
110
|
+
* handed back to the team (see `releasedCommittedSkillNote`).
|
|
111
|
+
*/
|
|
112
|
+
releasedCommitted?: string[];
|
|
104
113
|
}
|
|
105
114
|
|
|
106
115
|
export interface ActivateOptions {
|
|
@@ -123,20 +132,19 @@ export function reportDisplaced(
|
|
|
123
132
|
}
|
|
124
133
|
|
|
125
134
|
/**
|
|
126
|
-
* Generated
|
|
127
|
-
* `.claude/skills` SYMLINK (an older layout committed by mistake)
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
135
|
+
* Generated files that git tracks: the three live files, and a committed
|
|
136
|
+
* `.claude/skills` SYMLINK (an older layout committed by mistake). A skill
|
|
137
|
+
* FOLDER git tracks is never one of them — it is the team's copy (see
|
|
138
|
+
* `trackedSkillFolders`). Empty outside a git repository; throws
|
|
139
|
+
* `GitCheckError` when git cannot say. Reported only — nothing refuses to
|
|
140
|
+
* write because of it (see `tracked-live-files.ts`).
|
|
131
141
|
*/
|
|
132
|
-
export async function
|
|
142
|
+
export async function trackedGeneratedFiles(
|
|
133
143
|
projectPath: string,
|
|
134
|
-
skillFolders: readonly string[] = [],
|
|
135
144
|
): Promise<string[]> {
|
|
136
145
|
const rel = (abs: string) => path.relative(projectPath, abs);
|
|
137
146
|
const live = LIVE_FILE_KEYS.map((key) => rel(livePath(key, projectPath)));
|
|
138
147
|
const skills = rel(skillsRoot(projectPath));
|
|
139
|
-
const folders = skillFolders.map((folder) => path.join(skills, folder));
|
|
140
148
|
let skillsIsLink = false;
|
|
141
149
|
try {
|
|
142
150
|
skillsIsLink = (await lstat(skillsRoot(projectPath))).isSymbolicLink();
|
|
@@ -144,112 +152,44 @@ export async function trackedWriteTargets(
|
|
|
144
152
|
// No .claude/skills at all.
|
|
145
153
|
}
|
|
146
154
|
const files = [...live, ...(skillsIsLink ? [skills] : [])];
|
|
147
|
-
const tracked = await trackedPaths(projectPath, files
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* The folders of `skills` the team committed under `.claude/skills/`. Such a
|
|
153
|
-
* folder is the team's copy of the skill: magus neither installs over it nor
|
|
154
|
-
* manages it, so it never has cause to suggest untracking it.
|
|
155
|
-
*/
|
|
156
|
-
export async function committedSkillFolders(
|
|
157
|
-
projectPath: string,
|
|
158
|
-
skills: readonly ProfileSkillRef[],
|
|
159
|
-
): Promise<Set<string>> {
|
|
160
|
-
const root = path.relative(projectPath, skillsRoot(projectPath));
|
|
161
|
-
const folders = skills.map((ref) => skillFolder(ref));
|
|
162
|
-
const tracked = await trackedPaths(
|
|
163
|
-
projectPath,
|
|
164
|
-
[],
|
|
165
|
-
folders.map((folder) => path.join(root, folder)),
|
|
166
|
-
);
|
|
167
|
-
return new Set(folders.filter((f) => tracked.has(path.join(root, f))));
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** {@link trackedWriteTargets} for the live files alone — what doctor reports. */
|
|
171
|
-
export async function trackedGeneratedFiles(
|
|
172
|
-
projectPath: string,
|
|
173
|
-
): Promise<string[]> {
|
|
174
|
-
return trackedWriteTargets(projectPath);
|
|
155
|
+
const tracked = await trackedPaths(projectPath, files);
|
|
156
|
+
return files.filter((c) => tracked.has(c));
|
|
175
157
|
}
|
|
176
158
|
|
|
177
159
|
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* yet — unless the team committed them (see {@link committedSkillFolders}).
|
|
181
|
-
* An existing folder magus does not manage is a custom skill, which
|
|
182
|
-
* activation refuses as a clash rather than writing.
|
|
160
|
+
* {@link trackedGeneratedFiles} as the warning lines the CLI prints: one per
|
|
161
|
+
* tracked path, or one saying git could not be asked.
|
|
183
162
|
*/
|
|
184
|
-
export async function
|
|
163
|
+
export async function trackedGeneratedWarnings(
|
|
185
164
|
projectPath: string,
|
|
186
|
-
skills: readonly ProfileSkillRef[],
|
|
187
|
-
managed: Readonly<Record<string, string>> = {},
|
|
188
165
|
): Promise<string[]> {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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));
|
|
166
|
+
try {
|
|
167
|
+
return (await trackedGeneratedFiles(projectPath)).map(trackedWarning);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (error instanceof GitCheckError) return [error.message];
|
|
170
|
+
throw error;
|
|
203
171
|
}
|
|
204
|
-
return [...out];
|
|
205
172
|
}
|
|
206
173
|
|
|
207
174
|
/**
|
|
208
|
-
* The
|
|
209
|
-
*
|
|
210
|
-
*
|
|
175
|
+
* The folders of `skills` the team committed under `.claude/skills/`. Such a
|
|
176
|
+
* folder is the team's copy of the skill: magus neither installs over it nor
|
|
177
|
+
* manages it, so it never has cause to suggest untracking it. None when git
|
|
178
|
+
* cannot say — each existing folder is then judged as a custom skill.
|
|
211
179
|
*/
|
|
212
|
-
export async function
|
|
180
|
+
export async function committedSkillFolders(
|
|
213
181
|
projectPath: string,
|
|
214
|
-
|
|
215
|
-
): Promise<string
|
|
216
|
-
|
|
217
|
-
if (!marker) return [];
|
|
218
|
-
return writableSkillFolders(
|
|
182
|
+
skills: readonly ProfileSkillRef[],
|
|
183
|
+
): Promise<Set<string>> {
|
|
184
|
+
return trackedSkillFolders(
|
|
219
185
|
projectPath,
|
|
220
|
-
|
|
221
|
-
marker.skills,
|
|
186
|
+
skills.map((ref) => skillFolder(ref)),
|
|
222
187
|
);
|
|
223
188
|
}
|
|
224
189
|
|
|
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);
|
|
190
|
+
/** The line for a recorded skill folder handed back to the team. */
|
|
191
|
+
export function releasedCommittedSkillNote(folder: string): string {
|
|
192
|
+
return `Skill ${folder} is committed by the team — magus no longer manages it`;
|
|
253
193
|
}
|
|
254
194
|
|
|
255
195
|
/**
|
|
@@ -274,16 +214,16 @@ export function activationNotes(result: ActivationResult): string[] {
|
|
|
274
214
|
for (const f of result.skills.failed) {
|
|
275
215
|
lines.push(`Skill "${f.skill}" could not be installed: ${f.reason}`);
|
|
276
216
|
}
|
|
217
|
+
for (const folder of result.releasedCommitted ?? []) {
|
|
218
|
+
lines.push(releasedCommittedSkillNote(folder));
|
|
219
|
+
}
|
|
277
220
|
for (const folder of result.committedSkills ?? []) {
|
|
278
221
|
lines.push(
|
|
279
222
|
`Skill .claude/skills/${folder} is committed to git — the team's copy is used; magus does not install or manage it`,
|
|
280
223
|
);
|
|
281
224
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
`${rel} is generated but tracked by git — run \`${untrackCommand(rel)}\`, then commit`,
|
|
285
|
-
);
|
|
286
|
-
}
|
|
225
|
+
if (typeof result.tracked === "string") lines.push(result.tracked);
|
|
226
|
+
else lines.push(...result.tracked.map(trackedWarning));
|
|
287
227
|
if (result.keptAt) {
|
|
288
228
|
lines.push(
|
|
289
229
|
`The older layout's .claude/_profiles/ was kept at ${result.keptAt}/`,
|
|
@@ -295,9 +235,10 @@ export function activationNotes(result: ActivationResult): string[] {
|
|
|
295
235
|
/**
|
|
296
236
|
* Render `profileId` from `manifest` and make it the active profile.
|
|
297
237
|
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
* nothing, when a live file of a managed project does not
|
|
238
|
+
* A generated file git tracks is written like any other, and named in
|
|
239
|
+
* `tracked` so the caller can warn about it. Throws `SettingsParseError`,
|
|
240
|
+
* having changed nothing, when a live file of a managed project does not
|
|
241
|
+
* parse. Throws
|
|
301
242
|
* {@link SkillClashError} — having
|
|
302
243
|
* changed nothing but moving an older layout's skills into `.claude/skills/` —
|
|
303
244
|
* when a listed skill's folder is already a custom skill.
|
|
@@ -315,21 +256,27 @@ export async function activateProfile(
|
|
|
315
256
|
// Read before converting: the older marker lives in the directory the
|
|
316
257
|
// conversion moves away, and an older layout is recognised by its links.
|
|
317
258
|
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
259
|
const wasOldLayout = (await oldLayoutProfile(projectPath)) !== null;
|
|
324
260
|
// A managed project's live files are the profile's; one that does not
|
|
325
261
|
// parse is somebody mid-edit, and replacing it would lose their work. (At
|
|
326
262
|
// first adoption a hand-written file is moved to a backup, not replaced.)
|
|
327
263
|
if (previous !== null) await assertLiveFilesParse(projectPath);
|
|
328
264
|
const conversion = await convertLegacySkills(manifest, projectPath);
|
|
329
|
-
const
|
|
265
|
+
const recorded: Record<string, string> = {
|
|
330
266
|
...(previous?.skills ?? {}),
|
|
331
267
|
...conversion.managed,
|
|
332
268
|
};
|
|
269
|
+
// A recorded folder git tracks is the team's copy — 7.5.0's conversion
|
|
270
|
+
// recorded committed skills as magus-installed. Dropped from the record
|
|
271
|
+
// here, before a sync could remove it, and said once.
|
|
272
|
+
const committedRecorded = await trackedSkillFolders(
|
|
273
|
+
projectPath,
|
|
274
|
+
Object.keys(recorded),
|
|
275
|
+
);
|
|
276
|
+
const managedBefore: Record<string, string> = {};
|
|
277
|
+
for (const [folder, hash] of Object.entries(recorded)) {
|
|
278
|
+
if (!committedRecorded.has(folder)) managedBefore[folder] = hash;
|
|
279
|
+
}
|
|
333
280
|
|
|
334
281
|
// Listed skills the team committed and magus does not manage: that copy
|
|
335
282
|
// stands, and magus neither installs over it nor reports it as a clash.
|
|
@@ -397,7 +344,7 @@ export async function activateProfile(
|
|
|
397
344
|
);
|
|
398
345
|
await updateSkillIgnoreLines(
|
|
399
346
|
projectPath,
|
|
400
|
-
Object.keys(
|
|
347
|
+
Object.keys(recorded),
|
|
401
348
|
Object.keys(skills.skills),
|
|
402
349
|
);
|
|
403
350
|
await writeActiveMarker(
|
|
@@ -412,7 +359,12 @@ export async function activateProfile(
|
|
|
412
359
|
displaced,
|
|
413
360
|
skills,
|
|
414
361
|
conversion,
|
|
415
|
-
tracked: await trackedGeneratedFiles(projectPath)
|
|
362
|
+
tracked: await trackedGeneratedFiles(projectPath).catch(
|
|
363
|
+
(error: unknown) => {
|
|
364
|
+
if (error instanceof GitCheckError) return error.message;
|
|
365
|
+
throw error;
|
|
366
|
+
},
|
|
367
|
+
),
|
|
416
368
|
keptAt: conversion.keptAt
|
|
417
369
|
? path.relative(projectPath, conversion.keptAt)
|
|
418
370
|
: null,
|
|
@@ -420,6 +372,9 @@ export async function activateProfile(
|
|
|
420
372
|
...(committedSkills.size > 0
|
|
421
373
|
? { committedSkills: [...committedSkills].sort() }
|
|
422
374
|
: {}),
|
|
375
|
+
...(committedRecorded.size > 0
|
|
376
|
+
? { releasedCommitted: [...committedRecorded].sort() }
|
|
377
|
+
: {}),
|
|
423
378
|
};
|
|
424
379
|
}
|
|
425
380
|
|
|
@@ -445,7 +400,6 @@ export async function regenerateLiveFiles(
|
|
|
445
400
|
const marker = await readActiveMarker(projectPath);
|
|
446
401
|
const manifest = await readManifest(projectPath);
|
|
447
402
|
if (!marker || !manifest.profiles[marker.profile]) return false;
|
|
448
|
-
await assertNoTrackedWriteTargets(projectPath);
|
|
449
403
|
await assertLiveFilesParse(projectPath);
|
|
450
404
|
const rendered = await renderProfile(manifest, marker.profile, projectPath);
|
|
451
405
|
const files = await writeRenderedFiles(rendered, projectPath);
|
|
@@ -41,10 +41,6 @@ import {
|
|
|
41
41
|
type ProfileChoice,
|
|
42
42
|
activateProfile,
|
|
43
43
|
activationNotes,
|
|
44
|
-
activeWritableSkillFolders,
|
|
45
|
-
assertNoTrackedWriteTargets,
|
|
46
|
-
writableSkillFolders,
|
|
47
|
-
writeRefusal,
|
|
48
44
|
} from "./profile-activate.js";
|
|
49
45
|
import { adoptCurrentProject } from "./profile-adopt.js";
|
|
50
46
|
import {
|
|
@@ -73,7 +69,6 @@ import {
|
|
|
73
69
|
skillFolder,
|
|
74
70
|
} from "./profile-skills.js";
|
|
75
71
|
import type { SettingsParseError } from "./settings-file.js";
|
|
76
|
-
import type { LiveFileRefusal } from "./tracked-live-files.js";
|
|
77
72
|
|
|
78
73
|
export type ActiveProfileCheck =
|
|
79
74
|
| { state: "no-profiles" }
|
|
@@ -304,11 +299,6 @@ export interface ProfileStatePrep {
|
|
|
304
299
|
problems: string[];
|
|
305
300
|
/** The files behind `problems` that could not be parsed. */
|
|
306
301
|
unreadable: SettingsParseError[];
|
|
307
|
-
/**
|
|
308
|
-
* Set when a file magus would write is tracked by git, or git could not
|
|
309
|
-
* say: nothing ran, and `problems` holds its message.
|
|
310
|
-
*/
|
|
311
|
-
refusal?: LiveFileRefusal;
|
|
312
302
|
}
|
|
313
303
|
|
|
314
304
|
/**
|
|
@@ -322,27 +312,6 @@ export async function prepareProfileState(
|
|
|
322
312
|
projectPath: string,
|
|
323
313
|
opts: Pick<ActivateOptions, "installSkill"> = {},
|
|
324
314
|
): Promise<ProfileStatePrep> {
|
|
325
|
-
// A project with profiles whose live files — or the active profile's
|
|
326
|
-
// magus-installed skill folders — git tracks: every path below would write
|
|
327
|
-
// one of them (or the marker, or the manifest on the way), so nothing runs,
|
|
328
|
-
// not even the marker a unique match would restore.
|
|
329
|
-
const manifest = await readManifest(projectPath);
|
|
330
|
-
if (Object.keys(manifest.profiles).length > 0) {
|
|
331
|
-
const refusal = await writeRefusal(
|
|
332
|
-
projectPath,
|
|
333
|
-
await activeWritableSkillFolders(projectPath, manifest),
|
|
334
|
-
);
|
|
335
|
-
if (refusal) {
|
|
336
|
-
return {
|
|
337
|
-
check: await checkActiveProfile(projectPath, { restore: false }),
|
|
338
|
-
reconcile: null,
|
|
339
|
-
lines: [],
|
|
340
|
-
problems: [refusal.message],
|
|
341
|
-
unreadable: [],
|
|
342
|
-
refusal,
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
315
|
const check = await checkActiveProfile(projectPath);
|
|
347
316
|
const out: ProfileStatePrep = {
|
|
348
317
|
check,
|
|
@@ -524,22 +493,6 @@ export async function switchToProfile(
|
|
|
524
493
|
);
|
|
525
494
|
return result;
|
|
526
495
|
}
|
|
527
|
-
// Before the absorbs below write profiles.json: nothing at all is written
|
|
528
|
-
// when a live file this switch would write is tracked by git.
|
|
529
|
-
const marker = await readActiveMarker(projectPath);
|
|
530
|
-
const refusal = await writeRefusal(projectPath, [
|
|
531
|
-
...(await writableSkillFolders(
|
|
532
|
-
projectPath,
|
|
533
|
-
target.skills ?? [],
|
|
534
|
-
marker?.skills,
|
|
535
|
-
)),
|
|
536
|
-
...(await activeWritableSkillFolders(projectPath, before)),
|
|
537
|
-
]);
|
|
538
|
-
if (refusal) {
|
|
539
|
-
result.problems.push(refusal.message);
|
|
540
|
-
return result;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
496
|
if (!take(await absorbBeforeConversion(projectPath, name))) return result;
|
|
544
497
|
const installSkill = { installSkill: opts.installSkill };
|
|
545
498
|
if (!take(await reconcileActiveProfile(projectPath, installSkill))) {
|
|
@@ -580,20 +533,6 @@ export async function applyProfileChoice(
|
|
|
580
533
|
projectPath: string,
|
|
581
534
|
opts: ActivateOptions = {},
|
|
582
535
|
): Promise<ActivationResult> {
|
|
583
|
-
// Nothing is written — not the adopted manifest, not an absorbed edit —
|
|
584
|
-
// when a live file this choice would write is tracked by git.
|
|
585
|
-
const skills =
|
|
586
|
-
choice.kind === "activate"
|
|
587
|
-
? (manifest.profiles[choice.id]?.skills ?? [])
|
|
588
|
-
: [];
|
|
589
|
-
await assertNoTrackedWriteTargets(
|
|
590
|
-
projectPath,
|
|
591
|
-
await writableSkillFolders(
|
|
592
|
-
projectPath,
|
|
593
|
-
skills,
|
|
594
|
-
(await readActiveMarker(projectPath))?.skills,
|
|
595
|
-
),
|
|
596
|
-
);
|
|
597
536
|
if (choice.kind === "activate") {
|
|
598
537
|
const absorbed: string[] = [];
|
|
599
538
|
let changed = false;
|
|
@@ -635,7 +574,6 @@ export async function prepareProfileWrite(
|
|
|
635
574
|
projectPath: string,
|
|
636
575
|
): Promise<ProfileStatePrep> {
|
|
637
576
|
const prep = await prepareProfileState(projectPath);
|
|
638
|
-
if (prep.refusal) throw prep.refusal;
|
|
639
577
|
refuseOnProblems(prep);
|
|
640
578
|
return prep;
|
|
641
579
|
}
|