magus-cli 7.6.0 → 7.8.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/package.json +4 -4
- package/src/__tests__/blackbox-parity.test.ts +29 -15
- package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
- package/src/__tests__/blind-780-cli-tools.test.ts +808 -0
- package/src/__tests__/blind-780-deprecated.test.ts +631 -0
- package/src/__tests__/blind-780-failure-reasons.test.ts +1004 -0
- package/src/__tests__/blind-780-plugin-resolution.test.ts +431 -0
- package/src/__tests__/blind-780-review2.test.ts +1203 -0
- package/src/__tests__/cli-update-view.test.ts +3 -3
- package/src/__tests__/deprecated-remove.test.ts +827 -0
- package/src/__tests__/doctor.test.ts +159 -1
- package/src/__tests__/failure-popup-review2.test.tsx +920 -0
- package/src/__tests__/failure-popup.test.tsx +419 -0
- package/src/__tests__/final-fixes-780.test.tsx +724 -0
- package/src/__tests__/format-update-outcome.test.ts +9 -34
- package/src/__tests__/gitignore-profile.test.ts +0 -4
- package/src/__tests__/models-screen-state.test.ts +18 -40
- package/src/__tests__/plugin-resolution.test.ts +52 -13
- package/src/__tests__/plugin-row-text.test.ts +30 -37
- package/src/__tests__/profile-dependencies.test.ts +7 -0
- package/src/__tests__/profile-startup-old-format.test.ts +0 -3
- package/src/__tests__/profile-startup.test.ts +40 -12
- package/src/__tests__/stale-load-repair.test.ts +31 -23
- package/src/__tests__/update-apply.test.ts +13 -29
- package/src/__tests__/update-plan.test.ts +23 -17
- package/src/__tests__/useStaleLoadModal.test.ts +1 -42
- package/src/__tests__/version-unknown-state.test.ts +15 -17
- package/src/cli/doctor.ts +151 -30
- package/src/cli/install.ts +5 -6
- package/src/cli/update-view.ts +58 -15
- package/src/cli/update.ts +27 -5
- package/src/services/catalog-cache-store.ts +8 -6
- package/src/services/claude-cli.ts +106 -19
- package/src/services/claude-settings.ts +84 -36
- package/src/services/gitignore-service.ts +0 -19
- package/src/services/plugin-env-needs.ts +69 -0
- package/src/services/plugin-manager.ts +31 -19
- package/src/services/plugin-resolution.ts +34 -42
- package/src/services/profile-absorb.ts +9 -0
- package/src/services/profile-activate.ts +1 -1
- package/src/services/profile-startup.ts +30 -27
- package/src/services/profile-state.ts +81 -2
- package/src/services/resolver.ts +95 -1
- package/src/services/update-engine.ts +326 -31
- package/src/services/update-plan.ts +293 -33
- package/src/types/index.ts +7 -0
- package/src/ui/App.tsx +186 -123
- package/src/ui/adapters/cliToolsAdapter.ts +203 -1
- package/src/ui/adapters/failureReport.ts +137 -0
- package/src/ui/adapters/pluginsAdapter.ts +299 -28
- package/src/ui/adapters/startupReports.ts +282 -0
- package/src/ui/components/modals/FailureListModal.tsx +231 -0
- package/src/ui/components/modals/MessageModal.tsx +7 -13
- package/src/ui/components/modals/ModalContainer.tsx +10 -5
- package/src/ui/components/modals/index.ts +1 -0
- package/src/ui/hooks/startupQueue.ts +56 -0
- package/src/ui/hooks/useGitignoreModal.ts +31 -31
- package/src/ui/hooks/useProfileStartupModal.ts +127 -122
- package/src/ui/hooks/useStaleLoadModal.ts +33 -53
- package/src/ui/keymap.ts +14 -1
- package/src/ui/renderers/mcpRenderers.tsx +110 -3
- package/src/ui/renderers/pluginRenderers.tsx +77 -6
- package/src/ui/renderers/pluginRowText.ts +25 -25
- package/src/ui/renderers/pluginStatusText.ts +18 -1
- package/src/ui/screens/AliasScreen.tsx +38 -44
- package/src/ui/screens/CliToolsScreen.tsx +65 -51
- package/src/ui/screens/EnvVarsScreen.tsx +21 -6
- package/src/ui/screens/GitignoreScreen.tsx +49 -75
- package/src/ui/screens/McpRegistryScreen.tsx +72 -40
- package/src/ui/screens/McpScreen.tsx +55 -24
- package/src/ui/screens/ModelsScreen.tsx +46 -86
- package/src/ui/screens/PluginsScreen.tsx +316 -245
- package/src/ui/screens/ProfilesScreen.tsx +88 -73
- package/src/ui/screens/SkillsScreen.tsx +83 -80
- package/src/ui/screens/StylesScreen.tsx +211 -275
- package/src/ui/state/AppContext.tsx +72 -7
- package/src/ui/state/DimensionsContext.tsx +3 -22
- package/src/ui/state/reducer.ts +25 -14
- package/src/ui/state/types.ts +61 -31
- package/src/utils/user-path.ts +60 -0
- package/src/__tests__/styles-status-line.test.ts +0 -72
- package/src/__tests__/version-snapshot.test.ts +0 -212
- package/src/services/version-snapshot.ts +0 -189
|
@@ -17,11 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { useCallback, useRef } from "react";
|
|
19
19
|
import { getManifestPath } from "../../services/manifest.js";
|
|
20
|
-
import {
|
|
21
|
-
type ActivationResult,
|
|
22
|
-
type ProfileChoice,
|
|
23
|
-
activationNotes,
|
|
24
|
-
} from "../../services/profile-activate.js";
|
|
20
|
+
import type { ProfileChoice } from "../../services/profile-activate.js";
|
|
25
21
|
import type { DisplacedArtifact } from "../../services/profile-live.js";
|
|
26
22
|
import {
|
|
27
23
|
type ProfileStartupNeed,
|
|
@@ -37,8 +33,17 @@ import {
|
|
|
37
33
|
} from "../../services/profile-startup.js";
|
|
38
34
|
import { applyProfileChoice } from "../../services/profile-state.js";
|
|
39
35
|
import { SettingsParseError } from "../../services/settings-file.js";
|
|
40
|
-
import {
|
|
41
|
-
import
|
|
36
|
+
import { exceptionReason } from "../adapters/pluginsAdapter.js";
|
|
37
|
+
import {
|
|
38
|
+
activationFailureReport,
|
|
39
|
+
profileProblemsReport,
|
|
40
|
+
stepFailureReport,
|
|
41
|
+
} from "../adapters/startupReports.js";
|
|
42
|
+
import { useApp } from "../state/AppContext.js";
|
|
43
|
+
import type { FailureReport, ModalState } from "../state/types.js";
|
|
44
|
+
|
|
45
|
+
/** The headline for every way "Update it now" can fail. */
|
|
46
|
+
const FORMAT_NOT_UPDATED = "The profile format was not updated";
|
|
42
47
|
|
|
43
48
|
export interface BuildProfileModalArgs {
|
|
44
49
|
need: ProfileStartupNeed;
|
|
@@ -85,40 +90,6 @@ export function buildProfileStartupModal(
|
|
|
85
90
|
};
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
/** What changed, phrased for a message modal rather than a terminal. */
|
|
89
|
-
export function describeOutcome(
|
|
90
|
-
profileId: string,
|
|
91
|
-
displaced: DisplacedArtifact[],
|
|
92
|
-
projectPath: string,
|
|
93
|
-
/** True when this happened without asking, following the committed default. */
|
|
94
|
-
automatic = false,
|
|
95
|
-
/** Anything else activation reported (see `activationNotes`). */
|
|
96
|
-
notes: string[] = [],
|
|
97
|
-
): string {
|
|
98
|
-
const lines = [
|
|
99
|
-
automatic
|
|
100
|
-
? `This project declares "${profileId}" as its profile, so magus activated it.`
|
|
101
|
-
: `"${profileId}" is now the active profile.`,
|
|
102
|
-
];
|
|
103
|
-
if (displaced.length > 0) {
|
|
104
|
-
lines.push(
|
|
105
|
-
"",
|
|
106
|
-
"These were moved aside to make room for the profile's files, and are kept:",
|
|
107
|
-
...displaced.map((d) => {
|
|
108
|
-
const from = d.link.startsWith(projectPath)
|
|
109
|
-
? d.link.slice(projectPath.length + 1)
|
|
110
|
-
: d.link;
|
|
111
|
-
const to = d.savedTo.startsWith(projectPath)
|
|
112
|
-
? d.savedTo.slice(projectPath.length + 1)
|
|
113
|
-
: d.savedTo;
|
|
114
|
-
return ` ${from} → ${to}`;
|
|
115
|
-
}),
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
if (notes.length > 0) lines.push("", ...notes);
|
|
119
|
-
return lines.join("\n");
|
|
120
|
-
}
|
|
121
|
-
|
|
122
93
|
/** Project-relative, so a path does not wrap over two lines. */
|
|
123
94
|
function rel(p: string, projectPath: string): string {
|
|
124
95
|
return p.startsWith(projectPath) ? p.slice(projectPath.length + 1) : p;
|
|
@@ -144,33 +115,59 @@ export function describeFailure(
|
|
|
144
115
|
moved: DisplacedArtifact[],
|
|
145
116
|
projectPath: string,
|
|
146
117
|
declaredDefault: string | null,
|
|
147
|
-
|
|
148
|
-
|
|
118
|
+
choice: ProfileChoice,
|
|
119
|
+
): FailureReport {
|
|
120
|
+
const profile =
|
|
121
|
+
choice.kind === "adopt" ? (choice.name ?? choice.id) : choice.id;
|
|
122
|
+
const headline =
|
|
123
|
+
choice.kind === "adopt"
|
|
124
|
+
? `Profile "${profile}" was not created`
|
|
125
|
+
: `Profile "${profile}" was not activated`;
|
|
126
|
+
const e = exceptionReason(
|
|
127
|
+
error instanceof Error ? error.message : String(error),
|
|
128
|
+
);
|
|
149
129
|
if (moved.length === 0) {
|
|
150
|
-
return
|
|
130
|
+
return {
|
|
131
|
+
headline,
|
|
132
|
+
rows: [
|
|
133
|
+
{
|
|
134
|
+
subject: profile,
|
|
135
|
+
reason: `${e.reason}; nothing was moved, so this project is exactly as it was`,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
fix: e.fix ?? null,
|
|
139
|
+
};
|
|
151
140
|
}
|
|
152
|
-
return
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
141
|
+
return {
|
|
142
|
+
headline,
|
|
143
|
+
rows: [
|
|
144
|
+
{
|
|
145
|
+
subject: profile,
|
|
146
|
+
reason: `${e.reason}; this did not finish, and these files were already moved aside`,
|
|
147
|
+
},
|
|
148
|
+
...moved.map((d) => ({
|
|
149
|
+
subject: rel(d.link, projectPath),
|
|
150
|
+
reason: `is kept at ${rel(d.savedTo, projectPath)}`,
|
|
151
|
+
})),
|
|
152
|
+
],
|
|
160
153
|
// The trap the reviewer found: re-choosing "save what you have" after a
|
|
161
154
|
// partial failure runs adoption again, and adoption reads settings.json —
|
|
162
155
|
// which is now in the backup. It would capture an empty plugin set into a
|
|
163
156
|
// second profile and call that the user's setup.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
157
|
+
fix:
|
|
158
|
+
declaredDefault !== null
|
|
159
|
+
? `Activating ${declaredDefault} again is safe. Do NOT save-what-you-have — the files above have moved, so it would capture an empty set.`
|
|
160
|
+
: "Activating a profile again is safe. Do NOT save-what-you-have — the files above have moved, so it would capture an empty set.",
|
|
161
|
+
};
|
|
168
162
|
}
|
|
169
163
|
|
|
170
164
|
export interface ProfileStartupModalCallbacks {
|
|
171
165
|
/** Leave magus: the modal's only non-action. */
|
|
172
166
|
onQuit: () => void;
|
|
173
|
-
/**
|
|
167
|
+
/**
|
|
168
|
+
* Called once a choice has activated a profile, or a format update has
|
|
169
|
+
* rewritten it — the app dispatches PROFILE_ACTIVATED on it.
|
|
170
|
+
*/
|
|
174
171
|
onActivated?: () => void;
|
|
175
172
|
/**
|
|
176
173
|
* The question is ANSWERED: a profile is active, or the user chose to quit.
|
|
@@ -185,7 +182,8 @@ export interface ProfileStartupModalCallbacks {
|
|
|
185
182
|
* question on screen NOW, which is not always the one magus opened with: "Update
|
|
186
183
|
* it now" asks a new one, and a manifest that stopped parsing turns the question
|
|
187
184
|
* into the unparseable one. `working` is true while an activation or a format
|
|
188
|
-
* update is running —
|
|
185
|
+
* update is running — when it settles it empties the slot (success) or puts its
|
|
186
|
+
* error modal there.
|
|
189
187
|
*/
|
|
190
188
|
interface ProfileFlow {
|
|
191
189
|
need: ProfileStartupNeed;
|
|
@@ -198,7 +196,6 @@ export function useProfileStartupModal({
|
|
|
198
196
|
onAnswered,
|
|
199
197
|
}: ProfileStartupModalCallbacks) {
|
|
200
198
|
const { state, dispatch } = useApp();
|
|
201
|
-
const { navigateToScreen } = useNavigation();
|
|
202
199
|
const projectPath = state.projectPath;
|
|
203
200
|
// A ref, not state: `show` is recreated on every render, and the flow must
|
|
204
201
|
// survive that — a re-show has to know what was last asked, and that work is
|
|
@@ -227,6 +224,26 @@ export function useProfileStartupModal({
|
|
|
227
224
|
answered();
|
|
228
225
|
onQuit();
|
|
229
226
|
};
|
|
227
|
+
/**
|
|
228
|
+
* End a flow that activated a profile: empty the slot, or — when the
|
|
229
|
+
* activation could not do something — put the failure popup in it.
|
|
230
|
+
* Either way the question is already answered, so dismissing the popup
|
|
231
|
+
* lets the next startup question in.
|
|
232
|
+
*/
|
|
233
|
+
const settle = (report: FailureReport | null): void => {
|
|
234
|
+
if (!report) {
|
|
235
|
+
dispatch({ type: "HIDE_MODAL" });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
dispatch({
|
|
239
|
+
type: "SHOW_MODAL",
|
|
240
|
+
modal: {
|
|
241
|
+
type: "failures",
|
|
242
|
+
report,
|
|
243
|
+
onDismiss: () => dispatch({ type: "HIDE_MODAL" }),
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
};
|
|
230
247
|
|
|
231
248
|
/**
|
|
232
249
|
* Apply a choice, reporting the outcome either way. `current` is the
|
|
@@ -235,7 +252,6 @@ export function useProfileStartupModal({
|
|
|
235
252
|
*/
|
|
236
253
|
const apply = (
|
|
237
254
|
choice: ProfileChoice,
|
|
238
|
-
automatic: boolean,
|
|
239
255
|
current: ProfileStartupNeed = need,
|
|
240
256
|
): void => {
|
|
241
257
|
flowRef.current = { need: current, working: true };
|
|
@@ -263,34 +279,17 @@ export function useProfileStartupModal({
|
|
|
263
279
|
void applyProfileChoice(choice, current.manifest, projectPath, {
|
|
264
280
|
onDisplace: (artifact) => moved.push(artifact),
|
|
265
281
|
})
|
|
266
|
-
.then((result
|
|
282
|
+
.then((result) => {
|
|
267
283
|
// Answered here, on success — NOT when the question was shown.
|
|
268
284
|
// Clearing it on show let a screen's modal replace it and the
|
|
269
285
|
// question never came back.
|
|
270
286
|
answered();
|
|
271
287
|
onActivated?.();
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
title: "Profile active",
|
|
278
|
-
message: describeOutcome(
|
|
279
|
-
profileId,
|
|
280
|
-
displaced,
|
|
281
|
-
projectPath,
|
|
282
|
-
automatic,
|
|
283
|
-
activationNotes(result),
|
|
284
|
-
),
|
|
285
|
-
variant: "success",
|
|
286
|
-
onDismiss: () => {
|
|
287
|
-
dispatch({ type: "HIDE_MODAL" });
|
|
288
|
-
// The Profiles tab is where the result is visible, and
|
|
289
|
-
// where the user will want to look next.
|
|
290
|
-
navigateToScreen("profiles");
|
|
291
|
-
},
|
|
292
|
-
},
|
|
293
|
-
});
|
|
288
|
+
// No result modal and no jump to the Profiles tab: the user
|
|
289
|
+
// stays where they are, and the screen reloads under them —
|
|
290
|
+
// unless the activation could not do something, which the one
|
|
291
|
+
// failure popup says once it has settled.
|
|
292
|
+
settle(activationFailureReport([result]));
|
|
294
293
|
})
|
|
295
294
|
.catch((error: unknown) => {
|
|
296
295
|
// The manifest stopped parsing between launch and this answer —
|
|
@@ -308,18 +307,14 @@ export function useProfileStartupModal({
|
|
|
308
307
|
dispatch({
|
|
309
308
|
type: "SHOW_MODAL",
|
|
310
309
|
modal: {
|
|
311
|
-
type: "
|
|
312
|
-
|
|
313
|
-
choice.kind === "adopt"
|
|
314
|
-
? "Could not create the profile"
|
|
315
|
-
: "Could not activate the profile",
|
|
316
|
-
message: describeFailure(
|
|
310
|
+
type: "failures",
|
|
311
|
+
report: describeFailure(
|
|
317
312
|
error,
|
|
318
313
|
moved,
|
|
319
314
|
projectPath,
|
|
320
315
|
current.declaredDefault,
|
|
316
|
+
choice,
|
|
321
317
|
),
|
|
322
|
-
variant: "error",
|
|
323
318
|
// A failure leaves the project without an active profile, so
|
|
324
319
|
// dismissing returns to the question — including when the
|
|
325
320
|
// attempt was automatic, which is the one case where there
|
|
@@ -344,24 +339,47 @@ export function useProfileStartupModal({
|
|
|
344
339
|
void prepareProfileStartup(projectPath, { convert: true })
|
|
345
340
|
.then((result) => {
|
|
346
341
|
const outcome = formatUpdateOutcome(result);
|
|
342
|
+
const report = activationFailureReport(result.activations ?? []);
|
|
347
343
|
if (outcome.kind === "ask") {
|
|
348
344
|
// Converted, but no profile is active yet: a new question,
|
|
349
|
-
// still unanswered
|
|
345
|
+
// still unanswered — asked once what the conversion could not
|
|
346
|
+
// do has been read.
|
|
350
347
|
onActivated?.();
|
|
348
|
+
if (report) {
|
|
349
|
+
flowRef.current = { need: outcome.need, working: false };
|
|
350
|
+
dispatch({
|
|
351
|
+
type: "SHOW_MODAL",
|
|
352
|
+
modal: {
|
|
353
|
+
type: "failures",
|
|
354
|
+
report,
|
|
355
|
+
onDismiss: () => ask(outcome.need),
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
351
360
|
ask(outcome.need);
|
|
352
361
|
return;
|
|
353
362
|
}
|
|
354
363
|
if (outcome.kind === "failed") {
|
|
355
364
|
// Refused, so still the old format: back to the question,
|
|
356
365
|
// as the CLI stops on the same lines.
|
|
366
|
+
// Each refusal in the failure popup, the file it is about
|
|
367
|
+
// named, and what to do about it.
|
|
368
|
+
// A "failed" outcome always carries problems; the fallback
|
|
369
|
+
// only keeps the popup's type honest.
|
|
357
370
|
flowRef.current = { need, working: false };
|
|
371
|
+
const refused =
|
|
372
|
+
profileProblemsReport(result.problems, FORMAT_NOT_UPDATED) ??
|
|
373
|
+
stepFailureReport(
|
|
374
|
+
FORMAT_NOT_UPDATED,
|
|
375
|
+
"profile",
|
|
376
|
+
outcome.message,
|
|
377
|
+
);
|
|
358
378
|
dispatch({
|
|
359
379
|
type: "SHOW_MODAL",
|
|
360
380
|
modal: {
|
|
361
|
-
type: "
|
|
362
|
-
|
|
363
|
-
message: outcome.message,
|
|
364
|
-
variant: "error",
|
|
381
|
+
type: "failures",
|
|
382
|
+
report: refused,
|
|
365
383
|
onDismiss: () => ask(need),
|
|
366
384
|
},
|
|
367
385
|
});
|
|
@@ -370,29 +388,20 @@ export function useProfileStartupModal({
|
|
|
370
388
|
// Nothing left to ask: the converted profile is active.
|
|
371
389
|
answered();
|
|
372
390
|
onActivated?.();
|
|
373
|
-
|
|
374
|
-
type: "SHOW_MODAL",
|
|
375
|
-
modal: {
|
|
376
|
-
type: "message",
|
|
377
|
-
title: outcome.title,
|
|
378
|
-
message: outcome.message,
|
|
379
|
-
variant: "success",
|
|
380
|
-
onDismiss: () => {
|
|
381
|
-
dispatch({ type: "HIDE_MODAL" });
|
|
382
|
-
navigateToScreen("profiles");
|
|
383
|
-
},
|
|
384
|
-
},
|
|
385
|
-
});
|
|
391
|
+
settle(report);
|
|
386
392
|
})
|
|
387
393
|
.catch((error: unknown) => {
|
|
388
394
|
flowRef.current = { need, working: false };
|
|
389
395
|
dispatch({
|
|
390
396
|
type: "SHOW_MODAL",
|
|
391
397
|
modal: {
|
|
392
|
-
type: "
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
398
|
+
type: "failures",
|
|
399
|
+
report: stepFailureReport(
|
|
400
|
+
FORMAT_NOT_UPDATED,
|
|
401
|
+
"profile",
|
|
402
|
+
error,
|
|
403
|
+
"Your profiles are kept. Open magus again to retry",
|
|
404
|
+
),
|
|
396
405
|
onDismiss: () => ask(need),
|
|
397
406
|
},
|
|
398
407
|
});
|
|
@@ -427,11 +436,7 @@ export function useProfileStartupModal({
|
|
|
427
436
|
askName(suggestion, current, value, check.reason);
|
|
428
437
|
return;
|
|
429
438
|
}
|
|
430
|
-
apply(
|
|
431
|
-
{ kind: "adopt", id: check.id, name: check.name },
|
|
432
|
-
false,
|
|
433
|
-
current,
|
|
434
|
-
);
|
|
439
|
+
apply({ kind: "adopt", id: check.id, name: check.name }, current);
|
|
435
440
|
},
|
|
436
441
|
onCancel: () => ask(current),
|
|
437
442
|
},
|
|
@@ -447,7 +452,7 @@ export function useProfileStartupModal({
|
|
|
447
452
|
onChoose: (choice) =>
|
|
448
453
|
choice.kind === "adopt"
|
|
449
454
|
? askName(choice.id, current)
|
|
450
|
-
: apply(choice,
|
|
455
|
+
: apply(choice, current),
|
|
451
456
|
onUpdateFormat: updateFormat,
|
|
452
457
|
onQuit: quit,
|
|
453
458
|
}),
|
|
@@ -466,15 +471,15 @@ export function useProfileStartupModal({
|
|
|
466
471
|
|
|
467
472
|
// The repo already answered and there is nothing of the user's to
|
|
468
473
|
// replace: asking here would ask someone to re-decide what their team
|
|
469
|
-
// decided, with nothing at stake.
|
|
470
|
-
//
|
|
474
|
+
// decided, with nothing at stake. The Profiles tab names the profile
|
|
475
|
+
// in use.
|
|
471
476
|
if (need.autoActivate && need.declaredDefault !== null) {
|
|
472
|
-
apply({ kind: "activate", id: need.declaredDefault }
|
|
477
|
+
apply({ kind: "activate", id: need.declaredDefault });
|
|
473
478
|
return;
|
|
474
479
|
}
|
|
475
480
|
ask();
|
|
476
481
|
},
|
|
477
|
-
[dispatch,
|
|
482
|
+
[dispatch, projectPath, onQuit, onActivated, onAnswered],
|
|
478
483
|
);
|
|
479
484
|
|
|
480
485
|
return { show };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
|
+
import { stepFailureReport } from "../adapters/startupReports.js";
|
|
2
3
|
import { useApp, useNavigation } from "../state/AppContext.js";
|
|
3
|
-
import type { ModalState } from "../state/types.js";
|
|
4
|
+
import type { FailureReport, ModalState } from "../state/types.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Startup broken-plugin modal: magus launches, finds that Claude Code will load
|
|
@@ -75,8 +76,16 @@ export function useStaleLoadModal() {
|
|
|
75
76
|
const show = useCallback(
|
|
76
77
|
(args: {
|
|
77
78
|
brokenCount: number;
|
|
78
|
-
/**
|
|
79
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Runs the repair and answers what it could not fix — null when every
|
|
81
|
+
* plugin now loads its installed version. The Plugins screen shows
|
|
82
|
+
* what now loads; the failures go into the one failure popup.
|
|
83
|
+
*/
|
|
84
|
+
onFix: () =>
|
|
85
|
+
| Promise<FailureReport | null | undefined>
|
|
86
|
+
| FailureReport
|
|
87
|
+
| null
|
|
88
|
+
| undefined;
|
|
80
89
|
}) => {
|
|
81
90
|
if (args.brokenCount === 0) return;
|
|
82
91
|
const dismiss = () => dispatch({ type: "HIDE_MODAL" });
|
|
@@ -87,38 +96,34 @@ export function useStaleLoadModal() {
|
|
|
87
96
|
type: "SHOW_MODAL",
|
|
88
97
|
modal: { type: "loading", message: "Repairing plugins…" },
|
|
89
98
|
});
|
|
90
|
-
// The outcome is
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// a broken plugin was fixed.
|
|
99
|
+
// The outcome is never assumed: `repairStaleLoad` re-asks the loader
|
|
100
|
+
// after writing, and a plugin it could not fix still reads BROKEN on
|
|
101
|
+
// the Plugins screen.
|
|
94
102
|
//
|
|
95
103
|
// Called inside `.then` so a synchronous throw reaches `.catch`
|
|
96
104
|
// too; otherwise the loading modal stays up and swallows every key.
|
|
105
|
+
const fail = (report: FailureReport) =>
|
|
106
|
+
dispatch({
|
|
107
|
+
type: "SHOW_MODAL",
|
|
108
|
+
modal: { type: "failures", report, onDismiss: dismiss },
|
|
109
|
+
});
|
|
97
110
|
void Promise.resolve()
|
|
98
111
|
.then(() => args.onFix())
|
|
99
|
-
.then((
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
message: summary,
|
|
106
|
-
variant: "info",
|
|
107
|
-
onDismiss: dismiss,
|
|
108
|
-
},
|
|
109
|
-
});
|
|
112
|
+
.then((report) => {
|
|
113
|
+
// The loading modal must go — it swallows every key — and the
|
|
114
|
+
// next startup question is waiting for the slot. A repair
|
|
115
|
+
// that did not take puts the failure popup there instead.
|
|
116
|
+
if (report) fail(report);
|
|
117
|
+
else dispatch({ type: "HIDE_MODAL" });
|
|
110
118
|
})
|
|
111
119
|
.catch((err) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
onDismiss: dismiss,
|
|
120
|
-
},
|
|
121
|
-
});
|
|
120
|
+
fail(
|
|
121
|
+
stepFailureReport(
|
|
122
|
+
"The plugins were not repaired",
|
|
123
|
+
"repair",
|
|
124
|
+
err,
|
|
125
|
+
),
|
|
126
|
+
);
|
|
122
127
|
});
|
|
123
128
|
},
|
|
124
129
|
onOpenTab: () => {
|
|
@@ -134,28 +139,3 @@ export function useStaleLoadModal() {
|
|
|
134
139
|
|
|
135
140
|
return { show };
|
|
136
141
|
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* One line per repair attempt, for the modal that follows the fix.
|
|
140
|
-
*
|
|
141
|
-
* Exported and pure so the wording is tested rather than inspected by eye. Each
|
|
142
|
-
* line says what the loader resolves NOW, because that — not the row count — is
|
|
143
|
-
* the thing the user came here to change.
|
|
144
|
-
*/
|
|
145
|
-
export function staleLoadRepairSummary(
|
|
146
|
-
results: ReadonlyArray<{
|
|
147
|
-
pluginId: string;
|
|
148
|
-
target: string;
|
|
149
|
-
ok: boolean;
|
|
150
|
-
error?: string;
|
|
151
|
-
}>,
|
|
152
|
-
): string {
|
|
153
|
-
if (results.length === 0) return "Nothing to repair.";
|
|
154
|
-
return results
|
|
155
|
-
.map((r) =>
|
|
156
|
-
r.ok
|
|
157
|
-
? `✓ ${r.pluginId} now loads v${r.target}`
|
|
158
|
-
: `✗ ${r.pluginId} — ${r.error ?? "repair did not take effect"}`,
|
|
159
|
-
)
|
|
160
|
-
.join("\n");
|
|
161
|
-
}
|
package/src/ui/keymap.ts
CHANGED
|
@@ -166,6 +166,13 @@ const PLUGINS = [
|
|
|
166
166
|
"Remove a deprecated plugin from every scope",
|
|
167
167
|
30,
|
|
168
168
|
),
|
|
169
|
+
bind(
|
|
170
|
+
"set-env",
|
|
171
|
+
["e"],
|
|
172
|
+
"settings",
|
|
173
|
+
"Set the variables a plugin's MCP server needs",
|
|
174
|
+
35,
|
|
175
|
+
),
|
|
169
176
|
bind("add-marketplace", ["n"], "add marketplace", "Add a marketplace", 30),
|
|
170
177
|
bind("team-help", ["t"], "team", "How to share marketplaces with a team", 10),
|
|
171
178
|
refresh("Refresh the local marketplace caches"),
|
|
@@ -374,7 +381,13 @@ const ALIAS = [
|
|
|
374
381
|
"Turn the flag on or off; in the flag editor, the row",
|
|
375
382
|
90,
|
|
376
383
|
),
|
|
377
|
-
bind(
|
|
384
|
+
bind(
|
|
385
|
+
"write",
|
|
386
|
+
["w"],
|
|
387
|
+
"write",
|
|
388
|
+
"Write the alias to your shell's rc file; a new shell picks it up",
|
|
389
|
+
85,
|
|
390
|
+
),
|
|
378
391
|
bind("edit", ["e"], "edit", "Edit the flag's value", 80),
|
|
379
392
|
bind("reset", ["r"], "reset", "Reset the row to its default", 50),
|
|
380
393
|
bind("add", ["a"], "add", "Flag editor: add a value", 80),
|