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
|
@@ -12,19 +12,38 @@ import type { ModalState } from "../state/types.js";
|
|
|
12
12
|
export interface BuildModalArgs {
|
|
13
13
|
violationCount: number;
|
|
14
14
|
safeCount: number;
|
|
15
|
+
/**
|
|
16
|
+
* Files magus generates from `.claude/profiles.json` that git tracks. The
|
|
17
|
+
* fix untracks these in place; every other destructive fix stays in the tab.
|
|
18
|
+
*/
|
|
19
|
+
generated?: readonly string[];
|
|
15
20
|
onFixSafe: () => void;
|
|
16
21
|
onOpenTab: () => void;
|
|
17
22
|
onDismiss: () => void;
|
|
18
23
|
}
|
|
19
24
|
|
|
25
|
+
function entries(n: number): string {
|
|
26
|
+
return `${n} missing .gitignore entr${n === 1 ? "y" : "ies"}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function fixLabel(safeCount: number, generated: number): string {
|
|
30
|
+
if (generated > 0) {
|
|
31
|
+
const files = `${generated} generated file${generated === 1 ? "" : "s"}`;
|
|
32
|
+
return safeCount > 0
|
|
33
|
+
? `Stop tracking ${files} and add ${entries(safeCount)}`
|
|
34
|
+
: `Stop tracking ${files}`;
|
|
35
|
+
}
|
|
36
|
+
return safeCount > 0
|
|
37
|
+
? `Add ${entries(safeCount)}`
|
|
38
|
+
: "(no missing .gitignore entries)";
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
export function buildGitignoreModal(args: BuildModalArgs): ModalState {
|
|
21
42
|
const { violationCount, safeCount, onFixSafe, onOpenTab, onDismiss } = args;
|
|
43
|
+
const generated = args.generated ?? [];
|
|
22
44
|
const options = [
|
|
23
45
|
{
|
|
24
|
-
label:
|
|
25
|
-
safeCount > 0
|
|
26
|
-
? `Add ${safeCount} missing .gitignore entr${safeCount === 1 ? "y" : "ies"}`
|
|
27
|
-
: "(no missing .gitignore entries)",
|
|
46
|
+
label: fixLabel(safeCount, generated.length),
|
|
28
47
|
value: "fix-safe",
|
|
29
48
|
},
|
|
30
49
|
{
|
|
@@ -36,10 +55,20 @@ export function buildGitignoreModal(args: BuildModalArgs): ModalState {
|
|
|
36
55
|
value: "dismiss",
|
|
37
56
|
},
|
|
38
57
|
];
|
|
58
|
+
const generatedNote =
|
|
59
|
+
generated.length > 0
|
|
60
|
+
? `magus generates ${generated.length === 1 ? "this file" : "these files"} from .claude/profiles.json for each developer, so ${
|
|
61
|
+
generated.length === 1 ? "it is" : "they are"
|
|
62
|
+
} expected to be gitignored — but git tracks ${generated.length === 1 ? "it" : "them"}:\n${generated
|
|
63
|
+
.map((p) => ` ${p}`)
|
|
64
|
+
.join(
|
|
65
|
+
"\n",
|
|
66
|
+
)}\n\nThe first choice stops tracking ${generated.length === 1 ? "it" : "them"} (git rm --cached, staged for you to commit) and adds the .gitignore entries.\n\n`
|
|
67
|
+
: "";
|
|
39
68
|
return {
|
|
40
69
|
type: "select",
|
|
41
70
|
title: "Gitignore violations detected",
|
|
42
|
-
message:
|
|
71
|
+
message: `${generatedNote}Found ${violationCount} managed gitignore item(s) that do not match the current repo state.\n\nMissing .gitignore entries can be added now. Other items that change git tracking are reviewed and fixed from the Gitignore tab.`,
|
|
43
72
|
options,
|
|
44
73
|
onSelect: (value: string) => {
|
|
45
74
|
if (value === "fix-safe") onFixSafe();
|
|
@@ -58,27 +87,44 @@ export function useGitignoreModal() {
|
|
|
58
87
|
(args: {
|
|
59
88
|
violationCount: number;
|
|
60
89
|
safeCount: number;
|
|
61
|
-
|
|
90
|
+
generated?: readonly string[];
|
|
91
|
+
/** Returns what it did, shown before returning to the app. */
|
|
92
|
+
onFixSafe: () => Promise<string | undefined> | string | undefined;
|
|
62
93
|
}) => {
|
|
63
94
|
if (args.violationCount === 0) return;
|
|
64
95
|
const dismiss = () => dispatch({ type: "HIDE_MODAL" });
|
|
65
96
|
const modal = buildGitignoreModal({
|
|
66
97
|
violationCount: args.violationCount,
|
|
67
98
|
safeCount: args.safeCount,
|
|
99
|
+
generated: args.generated,
|
|
68
100
|
onFixSafe: () => {
|
|
69
101
|
dispatch({ type: "HIDE_MODAL" });
|
|
70
|
-
void Promise.resolve(args.onFixSafe())
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
type: "
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
102
|
+
void Promise.resolve(args.onFixSafe())
|
|
103
|
+
.then((report) => {
|
|
104
|
+
if (!report) return;
|
|
105
|
+
dispatch({
|
|
106
|
+
type: "SHOW_MODAL",
|
|
107
|
+
modal: {
|
|
108
|
+
type: "message",
|
|
109
|
+
title: "Gitignore fixed",
|
|
110
|
+
message: report,
|
|
111
|
+
variant: "success",
|
|
112
|
+
onDismiss: dismiss,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
})
|
|
116
|
+
.catch((err) => {
|
|
117
|
+
dispatch({
|
|
118
|
+
type: "SHOW_MODAL",
|
|
119
|
+
modal: {
|
|
120
|
+
type: "message",
|
|
121
|
+
title: "Safe fixes failed",
|
|
122
|
+
message: err instanceof Error ? err.message : String(err),
|
|
123
|
+
variant: "error",
|
|
124
|
+
onDismiss: dismiss,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
80
127
|
});
|
|
81
|
-
});
|
|
82
128
|
},
|
|
83
129
|
onOpenTab: () => {
|
|
84
130
|
dispatch({ type: "HIDE_MODAL" });
|
|
@@ -157,7 +157,11 @@ export function describeFailure(
|
|
|
157
157
|
].join("\n");
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
export function useProfileStartupModal(
|
|
160
|
+
export function useProfileStartupModal(
|
|
161
|
+
onQuit: () => void,
|
|
162
|
+
/** Called once a choice has activated a profile — see App's gitignore re-check. */
|
|
163
|
+
onActivated?: () => void,
|
|
164
|
+
) {
|
|
161
165
|
const { state, dispatch } = useApp();
|
|
162
166
|
const { navigateToScreen } = useNavigation();
|
|
163
167
|
const projectPath = state.projectPath;
|
|
@@ -191,6 +195,7 @@ export function useProfileStartupModal(onQuit: () => void) {
|
|
|
191
195
|
onDisplace: (artifact) => moved.push(artifact),
|
|
192
196
|
})
|
|
193
197
|
.then((result: ActivationResult) => {
|
|
198
|
+
onActivated?.();
|
|
194
199
|
const { profileId, displaced } = result;
|
|
195
200
|
dispatch({
|
|
196
201
|
type: "SHOW_MODAL",
|
|
@@ -270,7 +275,7 @@ export function useProfileStartupModal(onQuit: () => void) {
|
|
|
270
275
|
}
|
|
271
276
|
ask();
|
|
272
277
|
},
|
|
273
|
-
[dispatch, navigateToScreen, projectPath, onQuit],
|
|
278
|
+
[dispatch, navigateToScreen, projectPath, onQuit, onActivated],
|
|
274
279
|
);
|
|
275
280
|
|
|
276
281
|
return { show };
|
|
@@ -44,7 +44,6 @@ import {
|
|
|
44
44
|
fetchPluginContents,
|
|
45
45
|
fetchPluginSkillDetail,
|
|
46
46
|
} from "../../services/skills-manager.js";
|
|
47
|
-
import { LiveFileRefusal } from "../../services/tracked-live-files.js";
|
|
48
47
|
// The one update implementation, shared with `magus update`. See the header
|
|
49
48
|
// of update-engine.ts for what drifted while the TUI had its own loop.
|
|
50
49
|
import {
|
|
@@ -983,11 +982,7 @@ export function PluginsScreen() {
|
|
|
983
982
|
fetchData();
|
|
984
983
|
} catch (error) {
|
|
985
984
|
modal.hideModal();
|
|
986
|
-
await modal.message(
|
|
987
|
-
"Error",
|
|
988
|
-
error instanceof LiveFileRefusal ? error.message : `Failed: ${error}`,
|
|
989
|
-
"error",
|
|
990
|
-
);
|
|
985
|
+
await modal.message("Error", `Failed: ${error}`, "error");
|
|
991
986
|
}
|
|
992
987
|
}
|
|
993
988
|
};
|
|
@@ -1207,11 +1202,7 @@ export function PluginsScreen() {
|
|
|
1207
1202
|
fetchData();
|
|
1208
1203
|
} catch (error) {
|
|
1209
1204
|
modal.hideModal();
|
|
1210
|
-
await modal.message(
|
|
1211
|
-
"Error",
|
|
1212
|
-
error instanceof LiveFileRefusal ? error.message : `Failed: ${error}`,
|
|
1213
|
-
"error",
|
|
1214
|
-
);
|
|
1205
|
+
await modal.message("Error", `Failed: ${error}`, "error");
|
|
1215
1206
|
}
|
|
1216
1207
|
};
|
|
1217
1208
|
|
|
@@ -26,7 +26,6 @@ import {
|
|
|
26
26
|
renameProfile,
|
|
27
27
|
} from "../../services/profiles.js";
|
|
28
28
|
import { writableMap } from "../../services/settings-file.js";
|
|
29
|
-
import { LiveFileRefusal } from "../../services/tracked-live-files.js";
|
|
30
29
|
import {
|
|
31
30
|
ClipboardUnavailableError,
|
|
32
31
|
readClipboard,
|
|
@@ -178,9 +177,7 @@ export function ProfilesScreen() {
|
|
|
178
177
|
modal.hideModal();
|
|
179
178
|
await modal.message(
|
|
180
179
|
"Error",
|
|
181
|
-
|
|
182
|
-
? error.message
|
|
183
|
-
: `Failed to apply profile: ${error}`,
|
|
180
|
+
`Failed to apply profile: ${error}`,
|
|
184
181
|
"error",
|
|
185
182
|
);
|
|
186
183
|
}
|
|
@@ -245,9 +242,7 @@ export function ProfilesScreen() {
|
|
|
245
242
|
modal.hideModal();
|
|
246
243
|
await modal.message(
|
|
247
244
|
"Error",
|
|
248
|
-
|
|
249
|
-
? error.message
|
|
250
|
-
: `Failed to switch profile: ${error}`,
|
|
245
|
+
`Failed to switch profile: ${error}`,
|
|
251
246
|
"error",
|
|
252
247
|
);
|
|
253
248
|
}
|
|
@@ -280,9 +275,7 @@ export function ProfilesScreen() {
|
|
|
280
275
|
modal.hideModal();
|
|
281
276
|
await modal.message(
|
|
282
277
|
"Error",
|
|
283
|
-
|
|
284
|
-
? error.message
|
|
285
|
-
: `Failed to apply profile: ${error}`,
|
|
278
|
+
`Failed to apply profile: ${error}`,
|
|
286
279
|
"error",
|
|
287
280
|
);
|
|
288
281
|
}
|