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
|
@@ -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,
|
|
10
|
+
import { useApp, useFailurePopup, useModal } from "../state/AppContext.js";
|
|
6
11
|
import { useDimensions } from "../state/DimensionsContext.js";
|
|
7
12
|
|
|
8
13
|
import {
|
|
@@ -31,7 +36,6 @@ import {
|
|
|
31
36
|
writeClipboard,
|
|
32
37
|
} from "../../utils/clipboard.js";
|
|
33
38
|
import { ScrollableList } from "../components/ScrollableList.js";
|
|
34
|
-
import { activationNotices } from "../hooks/useProfileStartupModal.js";
|
|
35
39
|
import {
|
|
36
40
|
type ProfileListItem,
|
|
37
41
|
buildProfileListItems,
|
|
@@ -44,7 +48,7 @@ export function ProfilesScreen() {
|
|
|
44
48
|
const { state, dispatch } = useApp();
|
|
45
49
|
const { profiles: profilesState } = state;
|
|
46
50
|
const modal = useModal();
|
|
47
|
-
const
|
|
51
|
+
const showFailures = useFailurePopup();
|
|
48
52
|
const dimensions = useDimensions();
|
|
49
53
|
|
|
50
54
|
const fetchData = useCallback(async () => {
|
|
@@ -165,16 +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
|
-
// the
|
|
171
|
-
|
|
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
|
+
),
|
|
180
|
+
);
|
|
172
181
|
} catch (error) {
|
|
173
182
|
modal.hideModal();
|
|
174
|
-
await
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
183
|
+
await showFailures(
|
|
184
|
+
stepFailureReport(
|
|
185
|
+
`"${profile.name}" was not applied`,
|
|
186
|
+
profile.name,
|
|
187
|
+
error,
|
|
188
|
+
),
|
|
178
189
|
);
|
|
179
190
|
}
|
|
180
191
|
};
|
|
@@ -213,31 +224,35 @@ export function ProfilesScreen() {
|
|
|
213
224
|
modal.hideModal();
|
|
214
225
|
if (result.problems.length > 0 || !result.activation) {
|
|
215
226
|
// Nothing activated, but what was recorded on the way may have
|
|
216
|
-
// changed the list.
|
|
227
|
+
// changed the list. Each refusal goes into the failure popup, the
|
|
228
|
+
// file it is about named with what to do.
|
|
217
229
|
dispatch({ type: "DATA_REFRESH_COMPLETE" });
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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}"`,
|
|
222
235
|
);
|
|
236
|
+
if (refused) await modal.failures(refused);
|
|
223
237
|
return;
|
|
224
238
|
}
|
|
225
239
|
// The same signal as the startup question: every screen reloads,
|
|
226
240
|
// and the app re-runs its gitignore check — the profile switched
|
|
227
241
|
// to may generate files git still tracks, and that question is how
|
|
228
|
-
// the user hears of them
|
|
242
|
+
// the user hears of them.
|
|
229
243
|
dispatch({ type: "PROFILE_ACTIVATED" });
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
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);
|
|
235
248
|
} catch (error) {
|
|
236
249
|
modal.hideModal();
|
|
237
|
-
await
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
250
|
+
await showFailures(
|
|
251
|
+
stepFailureReport(
|
|
252
|
+
`Did not switch to "${selectedProfile.name}"`,
|
|
253
|
+
selectedProfile.name,
|
|
254
|
+
error,
|
|
255
|
+
),
|
|
241
256
|
);
|
|
242
257
|
}
|
|
243
258
|
return;
|
|
@@ -260,13 +275,14 @@ export function ProfilesScreen() {
|
|
|
260
275
|
await applyUserProfile(selectedProfile);
|
|
261
276
|
modal.hideModal();
|
|
262
277
|
dispatch({ type: "DATA_REFRESH_COMPLETE" });
|
|
263
|
-
notify(`Applied "${selectedProfile.name}" at USER scope`);
|
|
264
278
|
} catch (error) {
|
|
265
279
|
modal.hideModal();
|
|
266
|
-
await
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
280
|
+
await showFailures(
|
|
281
|
+
stepFailureReport(
|
|
282
|
+
`"${selectedProfile.name}" was not applied`,
|
|
283
|
+
"USER",
|
|
284
|
+
error,
|
|
285
|
+
),
|
|
270
286
|
);
|
|
271
287
|
}
|
|
272
288
|
};
|
|
@@ -295,7 +311,13 @@ export function ProfilesScreen() {
|
|
|
295
311
|
await fetchData();
|
|
296
312
|
} catch (error) {
|
|
297
313
|
modal.hideModal();
|
|
298
|
-
await
|
|
314
|
+
await showFailures(
|
|
315
|
+
stepFailureReport(
|
|
316
|
+
`"${selectedProfile.name}" was not renamed`,
|
|
317
|
+
selectedProfile.name,
|
|
318
|
+
error,
|
|
319
|
+
),
|
|
320
|
+
);
|
|
299
321
|
}
|
|
300
322
|
};
|
|
301
323
|
|
|
@@ -326,7 +348,13 @@ export function ProfilesScreen() {
|
|
|
326
348
|
await fetchData();
|
|
327
349
|
} catch (error) {
|
|
328
350
|
modal.hideModal();
|
|
329
|
-
await
|
|
351
|
+
await showFailures(
|
|
352
|
+
stepFailureReport(
|
|
353
|
+
`"${selectedProfile.name}" was not deleted`,
|
|
354
|
+
selectedProfile.name,
|
|
355
|
+
error,
|
|
356
|
+
),
|
|
357
|
+
);
|
|
330
358
|
}
|
|
331
359
|
};
|
|
332
360
|
|
|
@@ -344,19 +372,22 @@ export function ProfilesScreen() {
|
|
|
344
372
|
modal.hideModal();
|
|
345
373
|
try {
|
|
346
374
|
await writeClipboard(json);
|
|
347
|
-
notify(
|
|
348
|
-
`Copied "${selectedProfile.name}" — share it for teammates to import`,
|
|
349
|
-
);
|
|
350
375
|
} catch (err) {
|
|
351
376
|
if (err instanceof ClipboardUnavailableError) {
|
|
352
|
-
await modal.message("Profile JSON (copy manually)", json
|
|
377
|
+
await modal.message("Profile JSON (copy manually)", json);
|
|
353
378
|
} else {
|
|
354
379
|
throw err;
|
|
355
380
|
}
|
|
356
381
|
}
|
|
357
382
|
} catch (error) {
|
|
358
383
|
modal.hideModal();
|
|
359
|
-
await
|
|
384
|
+
await showFailures(
|
|
385
|
+
stepFailureReport(
|
|
386
|
+
`"${selectedProfile.name}" was not copied`,
|
|
387
|
+
selectedProfile.name,
|
|
388
|
+
error,
|
|
389
|
+
),
|
|
390
|
+
);
|
|
360
391
|
}
|
|
361
392
|
};
|
|
362
393
|
|
|
@@ -369,7 +400,9 @@ export function ProfilesScreen() {
|
|
|
369
400
|
if (err instanceof ClipboardUnavailableError) {
|
|
370
401
|
json = await modal.input("Import Profile", "Paste profile JSON:");
|
|
371
402
|
} else {
|
|
372
|
-
await
|
|
403
|
+
await showFailures(
|
|
404
|
+
stepFailureReport("No profile was imported", "clipboard", err),
|
|
405
|
+
);
|
|
373
406
|
return;
|
|
374
407
|
}
|
|
375
408
|
}
|
|
@@ -399,7 +432,13 @@ export function ProfilesScreen() {
|
|
|
399
432
|
await fetchData();
|
|
400
433
|
} catch (error) {
|
|
401
434
|
modal.hideModal();
|
|
402
|
-
await
|
|
435
|
+
await showFailures(
|
|
436
|
+
stepFailureReport(
|
|
437
|
+
"No profile was imported",
|
|
438
|
+
targetScope.toUpperCase(),
|
|
439
|
+
error,
|
|
440
|
+
),
|
|
441
|
+
);
|
|
403
442
|
}
|
|
404
443
|
};
|
|
405
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 ? (
|