magus-cli 7.7.0 → 7.9.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/README.md +1 -1
- package/package.json +4 -4
- package/src/__tests__/blackbox-parity.test.ts +45 -26
- package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
- package/src/__tests__/blind/plugin-deps/doctor.test.ts +622 -0
- package/src/__tests__/blind/plugin-deps/helpers.ts +340 -0
- package/src/__tests__/blind/plugin-deps/hook.test.ts +328 -0
- package/src/__tests__/blind/plugin-deps/probe.test.ts +694 -0
- package/src/__tests__/blind/plugin-deps/read-only.test.ts +96 -0
- package/src/__tests__/blind/plugin-deps/schema.test.ts +390 -0
- package/src/__tests__/blind-780-cli-tools.test.ts +796 -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 +1178 -0
- package/src/__tests__/cli-apply-seams.test.ts +2 -67
- package/src/__tests__/cli-tool-commands.test.ts +131 -6
- package/src/__tests__/cli-tools-adapter.test.ts +52 -2
- package/src/__tests__/cli-tools-plugin-managed.test.tsx +83 -0
- package/src/__tests__/cli-update-view.test.ts +3 -3
- package/src/__tests__/deprecated-remove.test.ts +827 -0
- package/src/__tests__/deps-adapter.test.ts +191 -0
- package/src/__tests__/deps-check-hook.test.ts +220 -0
- package/src/__tests__/deps-plan.test.ts +896 -0
- package/src/__tests__/deps-report.test.ts +197 -0
- package/src/__tests__/doctor-bins.test.ts +134 -36
- package/src/__tests__/doctor-json.test.ts +261 -0
- package/src/__tests__/doctor.test.ts +159 -1
- package/src/__tests__/failure-popup-review2.test.tsx +896 -0
- package/src/__tests__/failure-popup.test.tsx +419 -0
- package/src/__tests__/final-fixes-780.test.tsx +740 -0
- package/src/__tests__/fixtures/bins.ts +55 -0
- package/src/__tests__/format-update-outcome.test.ts +6 -32
- package/src/__tests__/gitignore-profile.test.ts +1 -9
- package/src/__tests__/install-plan.test.ts +15 -1
- package/src/__tests__/marketplace-refresh.test.ts +9 -3
- package/src/__tests__/models-screen-state.test.ts +18 -40
- package/src/__tests__/no-shell-spawn.test.ts +77 -0
- package/src/__tests__/npm-package-name.test.ts +6 -9
- package/src/__tests__/plugin-requires.test.ts +138 -153
- 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__/requires-probe.test.ts +858 -0
- package/src/__tests__/resolver.test.ts +129 -52
- package/src/__tests__/run.test.ts +108 -0
- package/src/__tests__/stale-load-repair.test.ts +31 -23
- package/src/__tests__/tool-probe.test.ts +1 -1
- package/src/__tests__/toolchain.test.ts +123 -52
- package/src/__tests__/update-apply.test.ts +13 -29
- package/src/__tests__/update-plan.test.ts +24 -18
- package/src/__tests__/useStaleLoadModal.test.ts +1 -57
- package/src/__tests__/version-unknown-state.test.ts +15 -17
- package/src/cli/ansi.ts +2 -7
- package/src/cli/deps-view.ts +213 -0
- package/src/cli/doctor.ts +320 -54
- package/src/cli/install.ts +55 -71
- package/src/cli/profile.ts +20 -2
- package/src/cli/router.ts +6 -2
- package/src/cli/update-view.ts +58 -15
- package/src/cli/update.ts +55 -12
- package/src/data/cli-tools.ts +24 -9
- package/src/services/active-closure.ts +3 -3
- package/src/services/catalog-cache-store.ts +8 -6
- package/src/services/claude-cli.ts +106 -19
- package/src/services/claude-settings.ts +88 -384
- package/src/services/cli-tool-commands.ts +104 -3
- package/src/services/deps-plan.ts +962 -0
- package/src/services/deps-report.ts +265 -0
- package/src/services/doctor-bins.ts +115 -43
- package/src/services/install-plan.ts +11 -1
- package/src/services/local-marketplace.ts +12 -206
- package/src/services/marketplace-refresh.ts +4 -4
- package/src/services/plugin-env-needs.ts +69 -0
- package/src/services/plugin-manager.ts +35 -37
- package/src/services/plugin-requires.ts +93 -162
- package/src/services/plugin-resolution.ts +34 -42
- package/src/services/profile-activate.ts +1 -2
- package/src/services/profile-startup.ts +18 -43
- package/src/services/profile-state.ts +72 -2
- package/src/services/resolver.ts +263 -59
- package/src/services/tool-probe.ts +18 -25
- package/src/services/toolchain.ts +117 -111
- package/src/services/update-engine.ts +345 -45
- package/src/services/update-plan.ts +296 -36
- package/src/shared/plugin/hooks/bun-missing.sh +39 -0
- package/src/shared/plugin/hooks/deps-check.ts +224 -0
- package/src/shared/plugin/hooks/lib/requires-probe.ts +1183 -0
- package/src/types/index.ts +60 -16
- package/src/ui/App.tsx +81 -171
- package/src/ui/adapters/cliToolsAdapter.ts +252 -12
- package/src/ui/adapters/depsAdapter.ts +129 -0
- package/src/ui/adapters/failureReport.ts +137 -0
- package/src/ui/adapters/pluginsAdapter.ts +297 -45
- package/src/ui/adapters/startupReports.ts +260 -0
- package/src/ui/components/modals/FailureListModal.tsx +231 -0
- package/src/ui/components/modals/MessageModal.tsx +7 -12
- 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 +30 -52
- package/src/ui/hooks/useProfileStartupModal.ts +119 -142
- package/src/ui/hooks/useStaleLoadModal.ts +35 -65
- package/src/ui/keymap.ts +14 -1
- package/src/ui/renderers/cliToolRenderers.tsx +135 -2
- 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 -45
- package/src/ui/screens/CliToolsScreen.tsx +199 -98
- package/src/ui/screens/EnvVarsScreen.tsx +17 -5
- package/src/ui/screens/GitignoreScreen.tsx +48 -67
- package/src/ui/screens/McpRegistryScreen.tsx +67 -38
- package/src/ui/screens/McpScreen.tsx +46 -19
- package/src/ui/screens/ModelsScreen.tsx +46 -86
- package/src/ui/screens/PluginsScreen.tsx +315 -259
- package/src/ui/screens/ProfilesScreen.tsx +79 -40
- package/src/ui/screens/SkillsScreen.tsx +83 -80
- package/src/ui/screens/StylesScreen.tsx +211 -275
- package/src/ui/state/AppContext.tsx +66 -44
- package/src/ui/state/DimensionsContext.tsx +3 -22
- package/src/ui/state/reducer.ts +12 -100
- package/src/ui/state/types.ts +48 -69
- package/src/utils/command-utils.ts +4 -5
- package/src/utils/run.ts +123 -52
- package/src/utils/string-utils.ts +9 -0
- package/src/utils/user-path.ts +60 -0
- package/src/__tests__/go-module.test.ts +0 -43
- package/src/__tests__/notices-rules.test.ts +0 -476
- package/src/__tests__/notices.test.tsx +0 -604
- package/src/__tests__/plugin-setup.test.ts +0 -1016
- package/src/__tests__/shell-script-callers.test.ts +0 -92
- package/src/__tests__/styles-status-line.test.ts +0 -72
- package/src/__tests__/version-snapshot.test.ts +0 -212
- package/src/services/plugin-setup.ts +0 -965
- package/src/services/version-snapshot.ts +0 -189
- package/src/utils/go-module.ts +0 -36
|
@@ -17,10 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { useCallback, useRef } from "react";
|
|
19
19
|
import { getManifestPath } from "../../services/manifest.js";
|
|
20
|
-
import type {
|
|
21
|
-
ActivationResult,
|
|
22
|
-
ProfileChoice,
|
|
23
|
-
} from "../../services/profile-activate.js";
|
|
20
|
+
import type { ProfileChoice } from "../../services/profile-activate.js";
|
|
24
21
|
import type { DisplacedArtifact } from "../../services/profile-live.js";
|
|
25
22
|
import {
|
|
26
23
|
type ProfileStartupNeed,
|
|
@@ -36,8 +33,17 @@ import {
|
|
|
36
33
|
} from "../../services/profile-startup.js";
|
|
37
34
|
import { applyProfileChoice } from "../../services/profile-state.js";
|
|
38
35
|
import { SettingsParseError } from "../../services/settings-file.js";
|
|
39
|
-
import {
|
|
40
|
-
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";
|
|
41
47
|
|
|
42
48
|
export interface BuildProfileModalArgs {
|
|
43
49
|
need: ProfileStartupNeed;
|
|
@@ -84,81 +90,6 @@ export function buildProfileStartupModal(
|
|
|
84
90
|
};
|
|
85
91
|
}
|
|
86
92
|
|
|
87
|
-
/** `typescript-lsp@claude-plugins-official` → `typescript-lsp`. */
|
|
88
|
-
function pluginName(pluginId: string): string {
|
|
89
|
-
const at = pluginId.lastIndexOf("@");
|
|
90
|
-
return at > 0 ? pluginId.slice(0, at) : pluginId;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** "a", "a and b", "a, b and c". */
|
|
94
|
-
function listed(names: readonly string[]): string {
|
|
95
|
-
if (names.length <= 1) return names.join("");
|
|
96
|
-
return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* What an activation did, as notices — for the startup question and the
|
|
101
|
-
* Profiles tab alike: one line saying which profile is in use, then
|
|
102
|
-
* {@link activationWarnings}.
|
|
103
|
-
*/
|
|
104
|
-
export function activationNotices(result: ActivationResult): NoticeLine[] {
|
|
105
|
-
return [
|
|
106
|
-
{ text: `Using profile "${result.profileId}"`, tone: "success" },
|
|
107
|
-
...activationWarnings(result),
|
|
108
|
-
];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* What an activation left for the user to act on, one warning per thing, in
|
|
113
|
-
* user terms. Alone, without {@link activationNotices}' "Using profile" line,
|
|
114
|
-
* for an activation the user did not ask for — a re-render at startup, a
|
|
115
|
-
* profile format update — where which profile is in use is not news.
|
|
116
|
-
*
|
|
117
|
-
* Deliberately NOT {@link activationNotes}, which the CLI prints. Several of
|
|
118
|
-
* its lines are for a terminal log and would only be noise on this banner:
|
|
119
|
-
* where displaced files were backed up (the backup still happens; nobody needs
|
|
120
|
-
* to be told where), edits recorded into the profile being left (the switch
|
|
121
|
-
* confirmation already said so), skills handed back to the team. Tracked
|
|
122
|
-
* generated files are left out too: every activation signals
|
|
123
|
-
* PROFILE_ACTIVATED, and the app re-runs its gitignore check on it — the
|
|
124
|
-
* startup question and a Profiles-tab switch alike — which asks about exactly
|
|
125
|
-
* those files, with a way to fix them.
|
|
126
|
-
*/
|
|
127
|
-
export function activationWarnings(result: ActivationResult): NoticeLine[] {
|
|
128
|
-
const lines: NoticeLine[] = [];
|
|
129
|
-
const report = result.dependencies;
|
|
130
|
-
for (const { plugin, dependency } of report?.blocked ?? []) {
|
|
131
|
-
lines.push({
|
|
132
|
-
text: `${pluginName(plugin)} won't load: profile "${result.profileId}" turns off ${pluginName(dependency)}, which it needs`,
|
|
133
|
-
tone: "warning",
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
// The plugin's details are not on this machine yet, so what it depends on
|
|
137
|
-
// could not be enabled. One line for all of them: the fix is the same.
|
|
138
|
-
const missing = (report?.unreadable ?? []).map(pluginName);
|
|
139
|
-
if (missing.length > 0) {
|
|
140
|
-
lines.push({
|
|
141
|
-
text: `${listed(missing)} ${missing.length === 1 ? "isn't" : "aren't"} downloaded yet — run magus install`,
|
|
142
|
-
tone: "warning",
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
for (const f of result.skills.failed) {
|
|
146
|
-
lines.push({
|
|
147
|
-
text: `Skill "${f.skill}" could not be installed: ${f.reason}`,
|
|
148
|
-
tone: "warning",
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
// The one case that names a path: two copies of a skill, and which to keep
|
|
152
|
-
// is the user's call — they need to know where the other one is.
|
|
153
|
-
for (const c of result.conversion.conflicts) {
|
|
154
|
-
lines.push({
|
|
155
|
-
text: `Skill "${c.folder}" exists in two versions; kept ${c.kept}, the other is at ${c.other}`,
|
|
156
|
-
tone: "warning",
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
return lines;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
93
|
/** Project-relative, so a path does not wrap over two lines. */
|
|
163
94
|
function rel(p: string, projectPath: string): string {
|
|
164
95
|
return p.startsWith(projectPath) ? p.slice(projectPath.length + 1) : p;
|
|
@@ -184,27 +115,50 @@ export function describeFailure(
|
|
|
184
115
|
moved: DisplacedArtifact[],
|
|
185
116
|
projectPath: string,
|
|
186
117
|
declaredDefault: string | null,
|
|
187
|
-
|
|
188
|
-
|
|
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
|
+
);
|
|
189
129
|
if (moved.length === 0) {
|
|
190
|
-
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
|
+
};
|
|
191
140
|
}
|
|
192
|
-
return
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
],
|
|
200
153
|
// The trap the reviewer found: re-choosing "save what you have" after a
|
|
201
154
|
// partial failure runs adoption again, and adoption reads settings.json —
|
|
202
155
|
// which is now in the backup. It would capture an empty plugin set into a
|
|
203
156
|
// second profile and call that the user's setup.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
+
};
|
|
208
162
|
}
|
|
209
163
|
|
|
210
164
|
export interface ProfileStartupModalCallbacks {
|
|
@@ -228,8 +182,8 @@ export interface ProfileStartupModalCallbacks {
|
|
|
228
182
|
* question on screen NOW, which is not always the one magus opened with: "Update
|
|
229
183
|
* it now" asks a new one, and a manifest that stopped parsing turns the question
|
|
230
184
|
* into the unparseable one. `working` is true while an activation or a format
|
|
231
|
-
* update is running — when it settles it empties the slot (success
|
|
232
|
-
*
|
|
185
|
+
* update is running — when it settles it empties the slot (success) or puts its
|
|
186
|
+
* error modal there.
|
|
233
187
|
*/
|
|
234
188
|
interface ProfileFlow {
|
|
235
189
|
need: ProfileStartupNeed;
|
|
@@ -242,7 +196,6 @@ export function useProfileStartupModal({
|
|
|
242
196
|
onAnswered,
|
|
243
197
|
}: ProfileStartupModalCallbacks) {
|
|
244
198
|
const { state, dispatch } = useApp();
|
|
245
|
-
const notify = useNotify();
|
|
246
199
|
const projectPath = state.projectPath;
|
|
247
200
|
// A ref, not state: `show` is recreated on every render, and the flow must
|
|
248
201
|
// survive that — a re-show has to know what was last asked, and that work is
|
|
@@ -271,6 +224,26 @@ export function useProfileStartupModal({
|
|
|
271
224
|
answered();
|
|
272
225
|
onQuit();
|
|
273
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
|
+
};
|
|
274
247
|
|
|
275
248
|
/**
|
|
276
249
|
* Apply a choice, reporting the outcome either way. `current` is the
|
|
@@ -306,20 +279,17 @@ export function useProfileStartupModal({
|
|
|
306
279
|
void applyProfileChoice(choice, current.manifest, projectPath, {
|
|
307
280
|
onDisplace: (artifact) => moved.push(artifact),
|
|
308
281
|
})
|
|
309
|
-
.then((result
|
|
282
|
+
.then((result) => {
|
|
310
283
|
// Answered here, on success — NOT when the question was shown.
|
|
311
284
|
// Clearing it on show let a screen's modal replace it and the
|
|
312
285
|
// question never came back.
|
|
313
286
|
answered();
|
|
314
287
|
onActivated?.();
|
|
315
288
|
// No result modal and no jump to the Profiles tab: the user
|
|
316
|
-
// stays where they are,
|
|
317
|
-
// activation
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
for (const line of activationNotices(result)) {
|
|
321
|
-
notify(line.text, line.tone);
|
|
322
|
-
}
|
|
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]));
|
|
323
293
|
})
|
|
324
294
|
.catch((error: unknown) => {
|
|
325
295
|
// The manifest stopped parsing between launch and this answer —
|
|
@@ -337,18 +307,14 @@ export function useProfileStartupModal({
|
|
|
337
307
|
dispatch({
|
|
338
308
|
type: "SHOW_MODAL",
|
|
339
309
|
modal: {
|
|
340
|
-
type: "
|
|
341
|
-
|
|
342
|
-
choice.kind === "adopt"
|
|
343
|
-
? "Could not create the profile"
|
|
344
|
-
: "Could not activate the profile",
|
|
345
|
-
message: describeFailure(
|
|
310
|
+
type: "failures",
|
|
311
|
+
report: describeFailure(
|
|
346
312
|
error,
|
|
347
313
|
moved,
|
|
348
314
|
projectPath,
|
|
349
315
|
current.declaredDefault,
|
|
316
|
+
choice,
|
|
350
317
|
),
|
|
351
|
-
variant: "error",
|
|
352
318
|
// A failure leaves the project without an active profile, so
|
|
353
319
|
// dismissing returns to the question — including when the
|
|
354
320
|
// attempt was automatic, which is the one case where there
|
|
@@ -373,37 +339,47 @@ export function useProfileStartupModal({
|
|
|
373
339
|
void prepareProfileStartup(projectPath, { convert: true })
|
|
374
340
|
.then((result) => {
|
|
375
341
|
const outcome = formatUpdateOutcome(result);
|
|
376
|
-
|
|
377
|
-
// will not load, one not downloaded yet, a skill that failed —
|
|
378
|
-
// in user terms. Posted whichever way it went, except a refusal,
|
|
379
|
-
// which converted nothing. Posting under the next question is
|
|
380
|
-
// safe: notices do not run down while a modal is open.
|
|
381
|
-
const warnings = (): void => {
|
|
382
|
-
for (const activation of result.activations) {
|
|
383
|
-
for (const line of activationWarnings(activation)) {
|
|
384
|
-
notify(line.text, line.tone);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
};
|
|
342
|
+
const report = activationFailureReport(result.activations ?? []);
|
|
388
343
|
if (outcome.kind === "ask") {
|
|
389
344
|
// Converted, but no profile is active yet: a new question,
|
|
390
|
-
// still unanswered
|
|
345
|
+
// still unanswered — asked once what the conversion could not
|
|
346
|
+
// do has been read.
|
|
391
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
|
+
}
|
|
392
360
|
ask(outcome.need);
|
|
393
|
-
warnings();
|
|
394
361
|
return;
|
|
395
362
|
}
|
|
396
363
|
if (outcome.kind === "failed") {
|
|
397
364
|
// Refused, so still the old format: back to the question,
|
|
398
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.
|
|
399
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
|
+
);
|
|
400
378
|
dispatch({
|
|
401
379
|
type: "SHOW_MODAL",
|
|
402
380
|
modal: {
|
|
403
|
-
type: "
|
|
404
|
-
|
|
405
|
-
message: outcome.message,
|
|
406
|
-
variant: "error",
|
|
381
|
+
type: "failures",
|
|
382
|
+
report: refused,
|
|
407
383
|
onDismiss: () => ask(need),
|
|
408
384
|
},
|
|
409
385
|
});
|
|
@@ -412,19 +388,20 @@ export function useProfileStartupModal({
|
|
|
412
388
|
// Nothing left to ask: the converted profile is active.
|
|
413
389
|
answered();
|
|
414
390
|
onActivated?.();
|
|
415
|
-
|
|
416
|
-
notify(outcome.notice);
|
|
417
|
-
warnings();
|
|
391
|
+
settle(report);
|
|
418
392
|
})
|
|
419
393
|
.catch((error: unknown) => {
|
|
420
394
|
flowRef.current = { need, working: false };
|
|
421
395
|
dispatch({
|
|
422
396
|
type: "SHOW_MODAL",
|
|
423
397
|
modal: {
|
|
424
|
-
type: "
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
+
),
|
|
428
405
|
onDismiss: () => ask(need),
|
|
429
406
|
},
|
|
430
407
|
});
|
|
@@ -494,15 +471,15 @@ export function useProfileStartupModal({
|
|
|
494
471
|
|
|
495
472
|
// The repo already answered and there is nothing of the user's to
|
|
496
473
|
// replace: asking here would ask someone to re-decide what their team
|
|
497
|
-
// decided, with nothing at stake.
|
|
498
|
-
//
|
|
474
|
+
// decided, with nothing at stake. The Profiles tab names the profile
|
|
475
|
+
// in use.
|
|
499
476
|
if (need.autoActivate && need.declaredDefault !== null) {
|
|
500
477
|
apply({ kind: "activate", id: need.declaredDefault });
|
|
501
478
|
return;
|
|
502
479
|
}
|
|
503
480
|
ask();
|
|
504
481
|
},
|
|
505
|
-
[dispatch,
|
|
482
|
+
[dispatch, projectPath, onQuit, onActivated, onAnswered],
|
|
506
483
|
);
|
|
507
484
|
|
|
508
485
|
return { show };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { stepFailureReport } from "../adapters/startupReports.js";
|
|
3
|
+
import { useApp, useNavigation } from "../state/AppContext.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
|
|
@@ -71,13 +72,20 @@ export function buildStaleLoadModal(args: BuildStaleLoadModalArgs): ModalState {
|
|
|
71
72
|
export function useStaleLoadModal() {
|
|
72
73
|
const { dispatch } = useApp();
|
|
73
74
|
const { navigateToScreen } = useNavigation();
|
|
74
|
-
const notify = useNotify();
|
|
75
75
|
|
|
76
76
|
const show = useCallback(
|
|
77
77
|
(args: {
|
|
78
78
|
brokenCount: number;
|
|
79
|
-
/**
|
|
80
|
-
|
|
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;
|
|
81
89
|
}) => {
|
|
82
90
|
if (args.brokenCount === 0) return;
|
|
83
91
|
const dismiss = () => dispatch({ type: "HIDE_MODAL" });
|
|
@@ -88,33 +96,34 @@ export function useStaleLoadModal() {
|
|
|
88
96
|
type: "SHOW_MODAL",
|
|
89
97
|
modal: { type: "loading", message: "Repairing plugins…" },
|
|
90
98
|
});
|
|
91
|
-
// The outcome is
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// 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.
|
|
95
102
|
//
|
|
96
103
|
// Called inside `.then` so a synchronous throw reaches `.catch`
|
|
97
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
|
+
});
|
|
98
110
|
void Promise.resolve()
|
|
99
111
|
.then(() => args.onFix())
|
|
100
|
-
.then((
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
|
|
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" });
|
|
106
118
|
})
|
|
107
119
|
.catch((err) => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
onDismiss: dismiss,
|
|
116
|
-
},
|
|
117
|
-
});
|
|
120
|
+
fail(
|
|
121
|
+
stepFailureReport(
|
|
122
|
+
"The plugins were not repaired",
|
|
123
|
+
"repair",
|
|
124
|
+
err,
|
|
125
|
+
),
|
|
126
|
+
);
|
|
118
127
|
});
|
|
119
128
|
},
|
|
120
129
|
onOpenTab: () => {
|
|
@@ -125,47 +134,8 @@ export function useStaleLoadModal() {
|
|
|
125
134
|
});
|
|
126
135
|
dispatch({ type: "SHOW_MODAL", modal });
|
|
127
136
|
},
|
|
128
|
-
[dispatch, navigateToScreen
|
|
137
|
+
[dispatch, navigateToScreen],
|
|
129
138
|
);
|
|
130
139
|
|
|
131
140
|
return { show };
|
|
132
141
|
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* What the repair did, as notices: one line for every plugin that now loads
|
|
136
|
-
* the version installed, and a warning for each that still does not.
|
|
137
|
-
*
|
|
138
|
-
* Exported and pure so the wording is tested rather than inspected by eye. The
|
|
139
|
-
* lines say what loads NOW, because that — not the row count — is the thing
|
|
140
|
-
* the user came here to change. A failure is never folded into the success
|
|
141
|
-
* line: `ok` is the loader re-asked after the write, and a repair that did not
|
|
142
|
-
* take must read as one.
|
|
143
|
-
*/
|
|
144
|
-
export function staleLoadRepairNotices(
|
|
145
|
-
results: ReadonlyArray<{
|
|
146
|
-
pluginId: string;
|
|
147
|
-
target: string;
|
|
148
|
-
ok: boolean;
|
|
149
|
-
error?: string;
|
|
150
|
-
}>,
|
|
151
|
-
): NoticeLine[] {
|
|
152
|
-
if (results.length === 0) {
|
|
153
|
-
return [{ text: "Nothing needed repairing", tone: "success" }];
|
|
154
|
-
}
|
|
155
|
-
const lines: NoticeLine[] = [];
|
|
156
|
-
const fixed = results.filter((r) => r.ok);
|
|
157
|
-
if (fixed.length > 0) {
|
|
158
|
-
lines.push({
|
|
159
|
-
text: `Now loading ${fixed.map((r) => `${r.pluginId} v${r.target}`).join(", ")}`,
|
|
160
|
-
tone: "success",
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
for (const r of results) {
|
|
164
|
-
if (r.ok) continue;
|
|
165
|
-
lines.push({
|
|
166
|
-
text: `${r.pluginId} not repaired: ${r.error ?? "repair did not take effect"}`,
|
|
167
|
-
tone: "warning",
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
return lines;
|
|
171
|
-
}
|
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),
|