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
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import React, { useEffect, useCallback } from "react";
|
|
2
|
+
import {
|
|
3
|
+
activationFailureReport,
|
|
4
|
+
profileProblemsReport,
|
|
5
|
+
stepFailureReport,
|
|
6
|
+
} from "../adapters/startupReports.js";
|
|
2
7
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
3
8
|
import { useScreenKeys } from "../input/KeyRouter.js";
|
|
4
9
|
import { moveDirection } from "../keymap.js";
|
|
5
|
-
import { useApp, useModal } from "../state/AppContext.js";
|
|
10
|
+
import { useApp, useFailurePopup, useModal } from "../state/AppContext.js";
|
|
6
11
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
7
12
|
|
|
8
13
|
import {
|
|
@@ -15,7 +20,6 @@ import {
|
|
|
15
20
|
writeSettings,
|
|
16
21
|
} from "../../services/claude-settings.js";
|
|
17
22
|
import { reconcileActiveProfile } from "../../services/profile-absorb.js";
|
|
18
|
-
import { activationNotes } from "../../services/profile-activate.js";
|
|
19
23
|
import { switchToProfile } from "../../services/profile-state.js";
|
|
20
24
|
import {
|
|
21
25
|
applyUserProfile,
|
|
@@ -32,7 +36,6 @@ import {
|
|
|
32
36
|
writeClipboard,
|
|
33
37
|
} from "../../utils/clipboard.js";
|
|
34
38
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
35
|
-
import { describeOutcome } from "../hooks/useProfileStartupModal.js";
|
|
36
39
|
import {
|
|
37
40
|
type ProfileListItem,
|
|
38
41
|
buildProfileListItems,
|
|
@@ -45,6 +48,7 @@ export function ProfilesScreen() {
|
|
|
45
48
|
const { state, dispatch } = useApp();
|
|
46
49
|
const { profiles: profilesState } = state;
|
|
47
50
|
const modal = useModal();
|
|
51
|
+
const showFailures = useFailurePopup();
|
|
48
52
|
const dimensions = useDimensions();
|
|
49
53
|
|
|
50
54
|
const fetchData = useCallback(async () => {
|
|
@@ -165,20 +169,23 @@ export function ProfilesScreen() {
|
|
|
165
169
|
const recorded = await reconcileActiveProfile(state.projectPath);
|
|
166
170
|
modal.hideModal();
|
|
167
171
|
dispatch({ type: "DATA_REFRESH_COMPLETE" });
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
// What was recorded is routine and the list shows it; a refusal to
|
|
173
|
+
// record it is a failure the user has to fix before the merge reaches
|
|
174
|
+
// the profile.
|
|
175
|
+
await showFailures(
|
|
176
|
+
profileProblemsReport(
|
|
177
|
+
recorded.problems,
|
|
178
|
+
`"${profile.name}" was merged in, but the active profile does not record it`,
|
|
179
|
+
),
|
|
175
180
|
);
|
|
176
181
|
} catch (error) {
|
|
177
182
|
modal.hideModal();
|
|
178
|
-
await
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
await showFailures(
|
|
184
|
+
stepFailureReport(
|
|
185
|
+
`"${profile.name}" was not applied`,
|
|
186
|
+
profile.name,
|
|
187
|
+
error,
|
|
188
|
+
),
|
|
182
189
|
);
|
|
183
190
|
}
|
|
184
191
|
};
|
|
@@ -215,35 +222,37 @@ export function ProfilesScreen() {
|
|
|
215
222
|
{ onDisplace: () => {} },
|
|
216
223
|
);
|
|
217
224
|
modal.hideModal();
|
|
218
|
-
dispatch({ type: "DATA_REFRESH_COMPLETE" });
|
|
219
225
|
if (result.problems.length > 0 || !result.activation) {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
// Nothing activated, but what was recorded on the way may have
|
|
227
|
+
// changed the list. Each refusal goes into the failure popup, the
|
|
228
|
+
// file it is about named with what to do.
|
|
229
|
+
dispatch({ type: "DATA_REFRESH_COMPLETE" });
|
|
230
|
+
const refused = profileProblemsReport(
|
|
231
|
+
result.problems.length > 0
|
|
232
|
+
? result.problems
|
|
233
|
+
: ["the profile was not activated"],
|
|
234
|
+
`Did not switch to "${selectedProfile.name}"`,
|
|
224
235
|
);
|
|
236
|
+
if (refused) await modal.failures(refused);
|
|
225
237
|
return;
|
|
226
238
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
result.lines.length > 0
|
|
237
|
-
? `${result.lines.join("\n")}\n\n${outcome}`
|
|
238
|
-
: outcome,
|
|
239
|
-
"success",
|
|
240
|
-
);
|
|
239
|
+
// The same signal as the startup question: every screen reloads,
|
|
240
|
+
// and the app re-runs its gitignore check — the profile switched
|
|
241
|
+
// to may generate files git still tracks, and that question is how
|
|
242
|
+
// the user hears of them.
|
|
243
|
+
dispatch({ type: "PROFILE_ACTIVATED" });
|
|
244
|
+
// Switched; what the activation could not do — a plugin that will
|
|
245
|
+
// not load, a skill that did not install — once it has settled.
|
|
246
|
+
const report = activationFailureReport([result.activation]);
|
|
247
|
+
if (report) await modal.failures(report);
|
|
241
248
|
} catch (error) {
|
|
242
249
|
modal.hideModal();
|
|
243
|
-
await
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
250
|
+
await showFailures(
|
|
251
|
+
stepFailureReport(
|
|
252
|
+
`Did not switch to "${selectedProfile.name}"`,
|
|
253
|
+
selectedProfile.name,
|
|
254
|
+
error,
|
|
255
|
+
),
|
|
247
256
|
);
|
|
248
257
|
}
|
|
249
258
|
return;
|
|
@@ -266,17 +275,14 @@ export function ProfilesScreen() {
|
|
|
266
275
|
await applyUserProfile(selectedProfile);
|
|
267
276
|
modal.hideModal();
|
|
268
277
|
dispatch({ type: "DATA_REFRESH_COMPLETE" });
|
|
269
|
-
await modal.message(
|
|
270
|
-
"Applied",
|
|
271
|
-
`Profile "${selectedProfile.name}" applied to ~/.claude/settings.json.`,
|
|
272
|
-
"success",
|
|
273
|
-
);
|
|
274
278
|
} catch (error) {
|
|
275
279
|
modal.hideModal();
|
|
276
|
-
await
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
+
await showFailures(
|
|
281
|
+
stepFailureReport(
|
|
282
|
+
`"${selectedProfile.name}" was not applied`,
|
|
283
|
+
"USER",
|
|
284
|
+
error,
|
|
285
|
+
),
|
|
280
286
|
);
|
|
281
287
|
}
|
|
282
288
|
};
|
|
@@ -301,15 +307,17 @@ export function ProfilesScreen() {
|
|
|
301
307
|
state.projectPath,
|
|
302
308
|
);
|
|
303
309
|
modal.hideModal();
|
|
310
|
+
// The row shows the new name; nothing else to say.
|
|
304
311
|
await fetchData();
|
|
305
|
-
await modal.message(
|
|
306
|
-
"Renamed",
|
|
307
|
-
`Profile renamed to "${newName.trim()}".`,
|
|
308
|
-
"success",
|
|
309
|
-
);
|
|
310
312
|
} catch (error) {
|
|
311
313
|
modal.hideModal();
|
|
312
|
-
await
|
|
314
|
+
await showFailures(
|
|
315
|
+
stepFailureReport(
|
|
316
|
+
`"${selectedProfile.name}" was not renamed`,
|
|
317
|
+
selectedProfile.name,
|
|
318
|
+
error,
|
|
319
|
+
),
|
|
320
|
+
);
|
|
313
321
|
}
|
|
314
322
|
};
|
|
315
323
|
|
|
@@ -336,11 +344,17 @@ export function ProfilesScreen() {
|
|
|
336
344
|
Math.min(profilesState.selectedIndex, allItems.length - 2),
|
|
337
345
|
);
|
|
338
346
|
dispatch({ type: "PROFILES_SELECT", index: newIndex });
|
|
347
|
+
// The row is gone from the list; nothing else to say.
|
|
339
348
|
await fetchData();
|
|
340
|
-
await modal.message("Deleted", "Profile deleted.", "success");
|
|
341
349
|
} catch (error) {
|
|
342
350
|
modal.hideModal();
|
|
343
|
-
await
|
|
351
|
+
await showFailures(
|
|
352
|
+
stepFailureReport(
|
|
353
|
+
`"${selectedProfile.name}" was not deleted`,
|
|
354
|
+
selectedProfile.name,
|
|
355
|
+
error,
|
|
356
|
+
),
|
|
357
|
+
);
|
|
344
358
|
}
|
|
345
359
|
};
|
|
346
360
|
|
|
@@ -358,21 +372,22 @@ export function ProfilesScreen() {
|
|
|
358
372
|
modal.hideModal();
|
|
359
373
|
try {
|
|
360
374
|
await writeClipboard(json);
|
|
361
|
-
await modal.message(
|
|
362
|
-
"Copied",
|
|
363
|
-
"Profile JSON copied to clipboard.\nShare it with teammates to import.",
|
|
364
|
-
"success",
|
|
365
|
-
);
|
|
366
375
|
} catch (err) {
|
|
367
376
|
if (err instanceof ClipboardUnavailableError) {
|
|
368
|
-
await modal.message("Profile JSON (copy manually)", json
|
|
377
|
+
await modal.message("Profile JSON (copy manually)", json);
|
|
369
378
|
} else {
|
|
370
379
|
throw err;
|
|
371
380
|
}
|
|
372
381
|
}
|
|
373
382
|
} catch (error) {
|
|
374
383
|
modal.hideModal();
|
|
375
|
-
await
|
|
384
|
+
await showFailures(
|
|
385
|
+
stepFailureReport(
|
|
386
|
+
`"${selectedProfile.name}" was not copied`,
|
|
387
|
+
selectedProfile.name,
|
|
388
|
+
error,
|
|
389
|
+
),
|
|
390
|
+
);
|
|
376
391
|
}
|
|
377
392
|
};
|
|
378
393
|
|
|
@@ -385,7 +400,9 @@ export function ProfilesScreen() {
|
|
|
385
400
|
if (err instanceof ClipboardUnavailableError) {
|
|
386
401
|
json = await modal.input("Import Profile", "Paste profile JSON:");
|
|
387
402
|
} else {
|
|
388
|
-
await
|
|
403
|
+
await showFailures(
|
|
404
|
+
stepFailureReport("No profile was imported", "clipboard", err),
|
|
405
|
+
);
|
|
389
406
|
return;
|
|
390
407
|
}
|
|
391
408
|
}
|
|
@@ -409,21 +426,19 @@ export function ProfilesScreen() {
|
|
|
409
426
|
|
|
410
427
|
modal.loading("Importing...");
|
|
411
428
|
try {
|
|
412
|
-
|
|
413
|
-
json,
|
|
414
|
-
targetScope,
|
|
415
|
-
state.projectPath,
|
|
416
|
-
);
|
|
429
|
+
await importProfileFromJson(json, targetScope, state.projectPath);
|
|
417
430
|
modal.hideModal();
|
|
431
|
+
// The imported profile appears in the list; nothing else to say.
|
|
418
432
|
await fetchData();
|
|
419
|
-
await modal.message(
|
|
420
|
-
"Imported",
|
|
421
|
-
`Profile imported successfully (id: ${id}).`,
|
|
422
|
-
"success",
|
|
423
|
-
);
|
|
424
433
|
} catch (error) {
|
|
425
434
|
modal.hideModal();
|
|
426
|
-
await
|
|
435
|
+
await showFailures(
|
|
436
|
+
stepFailureReport(
|
|
437
|
+
"No profile was imported",
|
|
438
|
+
targetScope.toUpperCase(),
|
|
439
|
+
error,
|
|
440
|
+
),
|
|
441
|
+
);
|
|
427
442
|
}
|
|
428
443
|
};
|
|
429
444
|
|
|
@@ -8,11 +8,15 @@ import React, {
|
|
|
8
8
|
useState,
|
|
9
9
|
useRef,
|
|
10
10
|
} from "react";
|
|
11
|
+
import { counted, failureReport } from "../adapters/failureReport.js";
|
|
12
|
+
import { exceptionReason } from "../adapters/pluginsAdapter.js";
|
|
13
|
+
import { stepFailureReport } from "../adapters/startupReports.js";
|
|
11
14
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
12
15
|
import { useScreenKeys, useSearchInput } from "../input/KeyRouter.js";
|
|
13
16
|
import { moveDirection } from "../keymap.js";
|
|
14
|
-
import { useApp,
|
|
17
|
+
import { useApp, useFailurePopup } from "../state/AppContext.js";
|
|
15
18
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
19
|
+
import type { FailureRow } from "../state/types.js";
|
|
16
20
|
|
|
17
21
|
import {
|
|
18
22
|
DEFAULT_SKILL_REPOS,
|
|
@@ -53,7 +57,6 @@ import { theme } from "../theme.js";
|
|
|
53
57
|
export function SkillsScreen() {
|
|
54
58
|
const { state, dispatch } = useApp();
|
|
55
59
|
const { skills: skillsState } = state;
|
|
56
|
-
const modal = useModal();
|
|
57
60
|
const dimensions = useDimensions();
|
|
58
61
|
const [curatedSkills, setCuratedSkills] = useState<RecommendedSkill[]>([]);
|
|
59
62
|
const [skillSets, setSkillSets] = useState<SkillSetInfo[]>([]);
|
|
@@ -181,20 +184,9 @@ export function SkillsScreen() {
|
|
|
181
184
|
};
|
|
182
185
|
}, [skillsState.searchQuery]);
|
|
183
186
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
tone: "success" | "error";
|
|
188
|
-
} | null>(null);
|
|
189
|
-
const statusTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
190
|
-
const showStatus = useCallback(
|
|
191
|
-
(text: string, tone: "success" | "error" = "success") => {
|
|
192
|
-
setStatusMsg({ text, tone });
|
|
193
|
-
if (statusTimerRef.current) clearTimeout(statusTimerRef.current);
|
|
194
|
-
statusTimerRef.current = setTimeout(() => setStatusMsg(null), 3000);
|
|
195
|
-
},
|
|
196
|
-
[],
|
|
197
|
-
);
|
|
187
|
+
// No status messages: a success shows as the rows' new state, and what
|
|
188
|
+
// failed goes into the one failure popup.
|
|
189
|
+
const showFailures = useFailurePopup();
|
|
198
190
|
|
|
199
191
|
// ── Disk scan for installed skills ────────────────────────────────────────
|
|
200
192
|
|
|
@@ -214,13 +206,13 @@ export function SkillsScreen() {
|
|
|
214
206
|
* Which skills are on disk, per scope. Never rejects: it runs fire-and-forget
|
|
215
207
|
* from effects and after every install, and a rejection there is an
|
|
216
208
|
* unhandled one, which ends the app. A directory that exists but cannot be
|
|
217
|
-
* read
|
|
209
|
+
* read goes into the failure popup — silently treating it as "no skills
|
|
218
210
|
* installed" would misreport the installed rows.
|
|
219
211
|
*/
|
|
220
212
|
const scanDisk = useCallback(async () => {
|
|
221
213
|
const user = new Map<string, SkillFrontmatter | null>();
|
|
222
214
|
const project = new Map<string, SkillFrontmatter | null>();
|
|
223
|
-
const unreadable:
|
|
215
|
+
const unreadable: FailureRow[] = [];
|
|
224
216
|
const userDir = path.join(os.homedir(), ".claude", "skills");
|
|
225
217
|
const projDir = path.join(
|
|
226
218
|
state.projectPath || process.cwd(),
|
|
@@ -242,16 +234,22 @@ export function SkillsScreen() {
|
|
|
242
234
|
}
|
|
243
235
|
}
|
|
244
236
|
} catch (error) {
|
|
245
|
-
unreadable.push(
|
|
246
|
-
|
|
247
|
-
|
|
237
|
+
unreadable.push({
|
|
238
|
+
subject: dir,
|
|
239
|
+
reason: exceptionReason(
|
|
240
|
+
error instanceof Error ? error.message : String(error),
|
|
241
|
+
).reason,
|
|
242
|
+
});
|
|
248
243
|
}
|
|
249
244
|
}
|
|
250
245
|
setInstalledFromDisk({ user, project, scanned: true });
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
246
|
+
void showFailures(
|
|
247
|
+
failureReport(
|
|
248
|
+
`${counted(unreadable.length, "skills folder")} could not be read`,
|
|
249
|
+
unreadable,
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
}, [state.projectPath, showFailures]);
|
|
255
253
|
|
|
256
254
|
// biome-ignore lint/correctness/useExhaustiveDependencies: dataRefreshVersion is the external refresh trigger; bumping it must re-scan the disk even though the body never reads it
|
|
257
255
|
useEffect(() => {
|
|
@@ -347,45 +345,50 @@ export function SkillsScreen() {
|
|
|
347
345
|
const set = skillSets.find((s) => s.id === setId);
|
|
348
346
|
if (!set || !set.loaded) return;
|
|
349
347
|
|
|
348
|
+
// Every skill already installed: the rows say so.
|
|
350
349
|
const toInstall = set.skills.filter((s) => !s.installed);
|
|
351
|
-
if (toInstall.length === 0)
|
|
352
|
-
showStatus(`All ${set.name} skills already installed`);
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
350
|
+
if (toInstall.length === 0) return;
|
|
355
351
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
352
|
+
dispatch({
|
|
353
|
+
type: "SHOW_MODAL",
|
|
354
|
+
modal: {
|
|
355
|
+
type: "loading",
|
|
356
|
+
message: `Installing ${toInstall.length} skills from ${set.name}…`,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
const problems: FailureRow[] = [];
|
|
360
360
|
for (const skill of toInstall) {
|
|
361
361
|
try {
|
|
362
362
|
const result = await installSkill(skill, scope, state.projectPath);
|
|
363
|
-
installed
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
363
|
+
// Incomplete is not the same as installed. Say so rather than let
|
|
364
|
+
// a half-written skill pass as a clean success.
|
|
365
|
+
if (result.degraded) {
|
|
366
|
+
problems.push({
|
|
367
|
+
subject: skill.name,
|
|
368
|
+
reason: `installed incomplete: ${result.degraded}`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
} catch (error) {
|
|
372
|
+
problems.push({
|
|
373
|
+
subject: skill.name,
|
|
374
|
+
reason: exceptionReason(
|
|
375
|
+
error instanceof Error ? error.message : String(error),
|
|
376
|
+
).reason,
|
|
377
|
+
});
|
|
367
378
|
}
|
|
368
379
|
}
|
|
369
380
|
await scanDisk();
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
"
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
`Installed ${installed} skills from ${set.name} to ${scope} — ${degraded} incomplete (missing supporting files)`,
|
|
380
|
-
"error",
|
|
381
|
-
);
|
|
382
|
-
} else {
|
|
383
|
-
showStatus(
|
|
384
|
-
`Installed ${installed} skills from ${set.name} to ${scope}`,
|
|
385
|
-
);
|
|
386
|
-
}
|
|
381
|
+
dispatch({ type: "HIDE_MODAL" });
|
|
382
|
+
await showFailures(
|
|
383
|
+
failureReport(
|
|
384
|
+
`${counted(problems.length, "skill")} from ${set.name} ${
|
|
385
|
+
problems.length === 1 ? "was" : "were"
|
|
386
|
+
} not fully installed`,
|
|
387
|
+
problems,
|
|
388
|
+
),
|
|
389
|
+
);
|
|
387
390
|
},
|
|
388
|
-
[skillSets, state.projectPath, scanDisk,
|
|
391
|
+
[skillSets, state.projectPath, scanDisk, showFailures, dispatch],
|
|
389
392
|
);
|
|
390
393
|
|
|
391
394
|
// ── Derived data ──────────────────────────────────────────────────────────
|
|
@@ -584,23 +587,29 @@ export function SkillsScreen() {
|
|
|
584
587
|
state.projectPath,
|
|
585
588
|
);
|
|
586
589
|
await scanDisk(); // Re-scan disk only — no network re-fetch needed
|
|
587
|
-
const files =
|
|
588
|
-
result.fileCount === 1 ? "1 file" : `${result.fileCount} files`;
|
|
589
590
|
// An install that silently dropped supporting files used to report plain
|
|
590
591
|
// success, so a half-installed skill looked identical to a good one.
|
|
591
592
|
if (result.degraded) {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
593
|
+
await showFailures(
|
|
594
|
+
failureReport(`${selectedSkill.name} was not fully installed`, [
|
|
595
|
+
{
|
|
596
|
+
subject: selectedSkill.name,
|
|
597
|
+
reason: `installed incomplete: ${result.degraded}`,
|
|
598
|
+
},
|
|
599
|
+
]),
|
|
595
600
|
);
|
|
596
|
-
} else {
|
|
597
|
-
showStatus(`Installed ${selectedSkill.name} to ${scope} (${files})`);
|
|
598
601
|
}
|
|
599
602
|
} catch (error) {
|
|
600
|
-
|
|
603
|
+
await showFailures(
|
|
604
|
+
stepFailureReport(
|
|
605
|
+
`${selectedSkill.name} was not installed`,
|
|
606
|
+
selectedSkill.name,
|
|
607
|
+
error,
|
|
608
|
+
),
|
|
609
|
+
);
|
|
601
610
|
}
|
|
602
611
|
},
|
|
603
|
-
[selectedSkill, state.projectPath, scanDisk,
|
|
612
|
+
[selectedSkill, state.projectPath, scanDisk, showFailures],
|
|
604
613
|
);
|
|
605
614
|
|
|
606
615
|
const handleUninstall = useCallback(async () => {
|
|
@@ -611,11 +620,16 @@ export function SkillsScreen() {
|
|
|
611
620
|
try {
|
|
612
621
|
await uninstallSkill(selectedSkill.name, scope, state.projectPath);
|
|
613
622
|
await scanDisk(); // Re-scan disk only — no network re-fetch needed
|
|
614
|
-
showStatus(`Removed ${selectedSkill.name} from ${scope}`);
|
|
615
623
|
} catch (error) {
|
|
616
|
-
|
|
624
|
+
await showFailures(
|
|
625
|
+
stepFailureReport(
|
|
626
|
+
`${selectedSkill.name} was not removed`,
|
|
627
|
+
selectedSkill.name,
|
|
628
|
+
error,
|
|
629
|
+
),
|
|
630
|
+
);
|
|
617
631
|
}
|
|
618
|
-
}, [selectedSkill, state.projectPath, scanDisk,
|
|
632
|
+
}, [selectedSkill, state.projectPath, scanDisk, showFailures]);
|
|
619
633
|
|
|
620
634
|
// ── Keys ──────────────────────────────────────────────────────────────────
|
|
621
635
|
|
|
@@ -709,19 +723,8 @@ export function SkillsScreen() {
|
|
|
709
723
|
).length;
|
|
710
724
|
const query = skillsState.searchQuery.trim();
|
|
711
725
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
<span
|
|
715
|
-
fg={
|
|
716
|
-
statusMsg.tone === "success"
|
|
717
|
-
? theme.colors.success
|
|
718
|
-
: theme.colors.danger
|
|
719
|
-
}
|
|
720
|
-
>
|
|
721
|
-
{statusMsg.text}
|
|
722
|
-
</span>
|
|
723
|
-
</text>
|
|
724
|
-
) : (
|
|
726
|
+
// The header's state line only — counts and search progress, never a message.
|
|
727
|
+
const statusContent = (
|
|
725
728
|
<text fg={theme.colors.text}>
|
|
726
729
|
<span fg={theme.colors.muted}>Skills: </span>
|
|
727
730
|
{installedFromDisk.scanned ? (
|