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
package/src/types/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { ModelsConfig } from "../services/models-core.js";
|
|
2
|
+
import type {
|
|
3
|
+
InstallOption,
|
|
4
|
+
Requirement,
|
|
5
|
+
Via,
|
|
6
|
+
} from "../shared/plugin/hooks/lib/requires-probe.js";
|
|
2
7
|
|
|
3
8
|
export interface McpServer {
|
|
4
9
|
name: string;
|
|
@@ -409,14 +414,52 @@ export interface ProfileManifest {
|
|
|
409
414
|
profiles: Record<string, ProfileManifestEntry>;
|
|
410
415
|
}
|
|
411
416
|
|
|
412
|
-
// ───
|
|
417
|
+
// ─── Binary requirements (profile cliTools, and plugin requires) ────────────────
|
|
413
418
|
|
|
414
|
-
/**
|
|
415
|
-
|
|
419
|
+
/**
|
|
420
|
+
* What a plugin or a profile needs on the machine. One shape for both: a
|
|
421
|
+
* plugin's `requires` entry as requires-probe parses it, and a profile
|
|
422
|
+
* cliTools entry mapped onto the profile-only `npm`/`pip`/`go` kinds (or
|
|
423
|
+
* `bun`/`brew`) by `cliToolToBin`.
|
|
424
|
+
*/
|
|
425
|
+
export type BinRequirement = Requirement;
|
|
426
|
+
export type BinInstallOption = InstallOption;
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* A requirement with the install option chosen for this machine — selected
|
|
430
|
+
* once, where the requirement is resolved — and the plugin directory a
|
|
431
|
+
* `${CLAUDE_PLUGIN_ROOT}` check or a plugin-relative script resolves against
|
|
432
|
+
* (absent for a profile cliTool).
|
|
433
|
+
*/
|
|
434
|
+
export interface SelectedBin extends Requirement {
|
|
435
|
+
/** Null when no option can be used here (`whyUnselectable` says why). */
|
|
436
|
+
chosen: InstallOption | null;
|
|
437
|
+
root?: string;
|
|
438
|
+
/**
|
|
439
|
+
* Profile pins the chosen option cannot install at: a tool a plugin owns
|
|
440
|
+
* and installs from a release, Homebrew or apt, whose version is the
|
|
441
|
+
* declaration's. Never applied, never dropped: `planDeps` reports each as
|
|
442
|
+
* unfixable and `magus update` names it.
|
|
443
|
+
*/
|
|
444
|
+
refusedPins?: string[];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** A selected requirement with its provenance: `plugin:<id>` / `profile:<name>.cliTools`. */
|
|
448
|
+
export interface ResolvedBin extends SelectedBin {
|
|
449
|
+
sources: string[];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** The install kinds whose installed version magus tracks and updates. */
|
|
453
|
+
export type BinInstaller = Extract<Via, "bun" | "brew" | "npm" | "pip" | "go">;
|
|
416
454
|
|
|
417
|
-
/**
|
|
418
|
-
|
|
419
|
-
|
|
455
|
+
/**
|
|
456
|
+
* A requirement whose chosen option is one of the {@link BinInstaller} kinds,
|
|
457
|
+
* in the shape `magus update` and the CLI Tools screen track versions in.
|
|
458
|
+
* Derived from a {@link SelectedBin} by `managedTool` (services/toolchain.ts)
|
|
459
|
+
* or built from a catalogue entry; never declared by hand.
|
|
460
|
+
*/
|
|
461
|
+
export interface ManagedTool {
|
|
462
|
+
/** The executable name expected on PATH. */
|
|
420
463
|
name: string;
|
|
421
464
|
via: BinInstaller;
|
|
422
465
|
/** Package id for bun/npm/pip (defaults to `name`). */
|
|
@@ -425,25 +468,19 @@ export interface BinRequirement {
|
|
|
425
468
|
formula?: string;
|
|
426
469
|
/** Module path for `go install` (defaults to `name`). */
|
|
427
470
|
module?: string;
|
|
428
|
-
/**
|
|
471
|
+
/** The check, as one display line, e.g. "tmux -V" (defaults to `<name> --version`). */
|
|
429
472
|
check?: string;
|
|
430
473
|
/** Optional version pin (real for bun/npm/pip; advisory for brew/go). */
|
|
431
474
|
version?: string;
|
|
432
475
|
}
|
|
433
476
|
|
|
434
|
-
/**
|
|
435
|
-
export interface
|
|
436
|
-
|
|
477
|
+
/** A {@link ManagedTool} with its provenance. */
|
|
478
|
+
export interface ManagedBin extends ManagedTool {
|
|
479
|
+
sources: string[];
|
|
437
480
|
}
|
|
438
481
|
|
|
439
482
|
// ─── Resolved install closure (resolver output) ─────────────────────────────────
|
|
440
483
|
|
|
441
|
-
/** A binary requirement after dedup + pin precedence, with provenance. */
|
|
442
|
-
export interface ResolvedBin extends BinRequirement {
|
|
443
|
-
/** Where this requirement came from, for diagnostics. */
|
|
444
|
-
sources: string[];
|
|
445
|
-
}
|
|
446
|
-
|
|
447
484
|
/**
|
|
448
485
|
* The fully-resolved set of things to install for a profile (or the union of
|
|
449
486
|
* all profiles). Produced by the resolver; consumed by `magus install`.
|
|
@@ -486,6 +523,13 @@ export interface ResolvedClosure {
|
|
|
486
523
|
export interface DependencyReport {
|
|
487
524
|
/** Enabled only because an enabled plugin depends on them. Sorted. */
|
|
488
525
|
derived: string[];
|
|
526
|
+
/**
|
|
527
|
+
* For every dependency an enabled plugin declares — derived ones and ones
|
|
528
|
+
* the profile also names — the plugin whose plugin.json first asked for it,
|
|
529
|
+
* so a report can say which plugin still needs it. Optional because a
|
|
530
|
+
* hand-built report may carry no provenance.
|
|
531
|
+
*/
|
|
532
|
+
requiredBy?: Record<string, string>;
|
|
489
533
|
/** Enabled plugins whose plugin.json could not be read here. Sorted. */
|
|
490
534
|
unreadable: string[];
|
|
491
535
|
/** A dependency the profile itself turns off: `plugin` will not load. */
|
package/src/ui/App.tsx
CHANGED
|
@@ -3,8 +3,6 @@ import { activePluginPins } from "../services/active-closure.js";
|
|
|
3
3
|
import { isClaudeAvailable } from "../services/claude-cli.js";
|
|
4
4
|
import {
|
|
5
5
|
findStaleLoads,
|
|
6
|
-
migrateMarketplaceRename,
|
|
7
|
-
recoverMarketplaceSettings,
|
|
8
6
|
repairAllStaleLoads,
|
|
9
7
|
} from "../services/claude-settings.js";
|
|
10
8
|
import {
|
|
@@ -12,7 +10,6 @@ import {
|
|
|
12
10
|
startupGitignoreNeed,
|
|
13
11
|
} from "../services/gitignore-check.js";
|
|
14
12
|
import { applyStartupFixes } from "../services/gitignore-service.js";
|
|
15
|
-
import { repairAllMarketplaces } from "../services/local-marketplace.js";
|
|
16
13
|
import { autoAddMissingMarketplaces } from "../services/marketplace-sync.js";
|
|
17
14
|
import {
|
|
18
15
|
type ProfileStartupNeed,
|
|
@@ -24,22 +21,19 @@ import {
|
|
|
24
21
|
checkForUpdates,
|
|
25
22
|
getCurrentVersion,
|
|
26
23
|
} from "../services/version-check.js";
|
|
24
|
+
import {
|
|
25
|
+
gitignoreStartupReport,
|
|
26
|
+
profileStartupReport,
|
|
27
|
+
staleRepairReport,
|
|
28
|
+
} from "./adapters/startupReports.js";
|
|
27
29
|
import { ScreenErrorBoundary } from "./components/ScreenErrorBoundary.js";
|
|
28
30
|
import { TABS } from "./components/TabBar.js";
|
|
29
31
|
import { ProgressBar } from "./components/layout/ProgressBar.js";
|
|
30
32
|
import { ModalContainer } from "./components/modals/index.js";
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} from "./hooks/
|
|
35
|
-
import {
|
|
36
|
-
activationWarnings,
|
|
37
|
-
useProfileStartupModal,
|
|
38
|
-
} from "./hooks/useProfileStartupModal.js";
|
|
39
|
-
import {
|
|
40
|
-
staleLoadRepairNotices,
|
|
41
|
-
useStaleLoadModal,
|
|
42
|
-
} from "./hooks/useStaleLoadModal.js";
|
|
33
|
+
import { nextStartupStep } from "./hooks/startupQueue.js";
|
|
34
|
+
import { useGitignoreModal } from "./hooks/useGitignoreModal.js";
|
|
35
|
+
import { useProfileStartupModal } from "./hooks/useProfileStartupModal.js";
|
|
36
|
+
import { useStaleLoadModal } from "./hooks/useStaleLoadModal.js";
|
|
43
37
|
import { KeyRouter, useKeyLayer } from "./input/KeyRouter.js";
|
|
44
38
|
import { type GlobalHandlers, runGlobalBindings } from "./keymap.js";
|
|
45
39
|
import {
|
|
@@ -60,14 +54,12 @@ import {
|
|
|
60
54
|
useApp,
|
|
61
55
|
useModal,
|
|
62
56
|
useNavigation,
|
|
63
|
-
useNotify,
|
|
64
57
|
} from "./state/AppContext.js";
|
|
65
58
|
import {
|
|
66
59
|
DimensionsProvider,
|
|
67
60
|
useDimensions,
|
|
68
61
|
} from "./state/DimensionsContext.js";
|
|
69
|
-
import type {
|
|
70
|
-
import { truncate } from "./text-width.js";
|
|
62
|
+
import type { FailureReport, Screen } from "./state/types.js";
|
|
71
63
|
import { theme } from "./theme.js";
|
|
72
64
|
|
|
73
65
|
export const VERSION = getCurrentVersion();
|
|
@@ -185,72 +177,6 @@ function UpdateBanner({ result }: { result: VersionCheckResult }) {
|
|
|
185
177
|
);
|
|
186
178
|
}
|
|
187
179
|
|
|
188
|
-
/**
|
|
189
|
-
* NoticeBanner Component
|
|
190
|
-
* The app's one notice line (or a few): what an action did, from `useNotify`.
|
|
191
|
-
* It sits above the screen and outside the modal slot, so it never takes a key
|
|
192
|
-
* and never stands in the way of a startup question. Each line clears itself
|
|
193
|
-
* at its `expiresAt` — counting only time with no modal open.
|
|
194
|
-
*
|
|
195
|
-
* One notice is exactly ONE row: the layout reserves one row per notice
|
|
196
|
-
* (`transientBannerCount`), so a line that wrapped would push the screen's
|
|
197
|
-
* last rows off the terminal. Hence no wrapping, and the text clipped to the
|
|
198
|
-
* width — the start of a line says what happened, and that is what survives.
|
|
199
|
-
*/
|
|
200
|
-
function NoticeBanner({ notices }: { notices: readonly Notice[] }) {
|
|
201
|
-
const { state, dispatch } = useApp();
|
|
202
|
-
const { terminalWidth } = useDimensions();
|
|
203
|
-
const modalOpen = state.modal !== null;
|
|
204
|
-
const paused = state.noticesPausedAt !== null;
|
|
205
|
-
|
|
206
|
-
// Hold the notices while a modal is open. A warning posted just before a
|
|
207
|
-
// startup question, or under it, would otherwise run down behind it and be
|
|
208
|
-
// gone by the time the user has answered.
|
|
209
|
-
useEffect(() => {
|
|
210
|
-
dispatch({
|
|
211
|
-
type: modalOpen ? "PAUSE_NOTICES" : "RESUME_NOTICES",
|
|
212
|
-
now: Date.now(),
|
|
213
|
-
});
|
|
214
|
-
}, [modalOpen, dispatch]);
|
|
215
|
-
|
|
216
|
-
// One timer, for the line that expires first; the reducer then drops every
|
|
217
|
-
// line past its time, and the next render schedules the one after. None
|
|
218
|
-
// while paused: RESUME_NOTICES moves every `expiresAt` on first.
|
|
219
|
-
useEffect(() => {
|
|
220
|
-
if (notices.length === 0 || paused) return;
|
|
221
|
-
const next = Math.min(...notices.map((n) => n.expiresAt));
|
|
222
|
-
const timer = setTimeout(
|
|
223
|
-
() => dispatch({ type: "EXPIRE_NOTICES", now: Date.now() }),
|
|
224
|
-
Math.max(0, next - Date.now()),
|
|
225
|
-
);
|
|
226
|
-
return () => clearTimeout(timer);
|
|
227
|
-
}, [notices, paused, dispatch]);
|
|
228
|
-
|
|
229
|
-
// The box's left and right padding.
|
|
230
|
-
const width = Math.max(0, terminalWidth - 2);
|
|
231
|
-
return (
|
|
232
|
-
<>
|
|
233
|
-
{notices.map((notice) => (
|
|
234
|
-
<box key={notice.id} paddingLeft={1} paddingRight={1} height={1}>
|
|
235
|
-
<text
|
|
236
|
-
wrapMode="none"
|
|
237
|
-
fg={
|
|
238
|
-
notice.tone === "warning"
|
|
239
|
-
? theme.colors.warning
|
|
240
|
-
: theme.colors.success
|
|
241
|
-
}
|
|
242
|
-
>
|
|
243
|
-
{truncate(
|
|
244
|
-
`${notice.tone === "warning" ? "⚠" : "✓"} ${notice.text}`,
|
|
245
|
-
width,
|
|
246
|
-
)}
|
|
247
|
-
</text>
|
|
248
|
-
</box>
|
|
249
|
-
))}
|
|
250
|
-
</>
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
180
|
/**
|
|
255
181
|
* ProgressIndicator Component
|
|
256
182
|
* Shows progress bar when operations are running
|
|
@@ -282,13 +208,12 @@ interface AppContentInnerProps {
|
|
|
282
208
|
|
|
283
209
|
function AppContentInner({ updateInfo, onExit }: AppContentInnerProps) {
|
|
284
210
|
const { state } = useApp();
|
|
285
|
-
const { progress
|
|
211
|
+
const { progress } = state;
|
|
286
212
|
const dimensions = useDimensions();
|
|
287
213
|
|
|
288
214
|
return (
|
|
289
215
|
<box flexDirection="column" height={dimensions.terminalHeight}>
|
|
290
216
|
{updateInfo?.updateAvailable && <UpdateBanner result={updateInfo} />}
|
|
291
|
-
<NoticeBanner notices={notices} />
|
|
292
217
|
{progress && <ProgressIndicator {...progress} />}
|
|
293
218
|
<box
|
|
294
219
|
flexDirection="column"
|
|
@@ -336,7 +261,6 @@ interface AppContentProps {
|
|
|
336
261
|
function AppContent({ onExit }: AppContentProps) {
|
|
337
262
|
const { state, dispatch } = useApp();
|
|
338
263
|
const { progress } = state;
|
|
339
|
-
const notify = useNotify();
|
|
340
264
|
const [updateInfo, setUpdateInfo] = useState<VersionCheckResult | null>(null);
|
|
341
265
|
const [gitignoreData, setGitignoreData] = useState<QueuedGitignore | null>(
|
|
342
266
|
null,
|
|
@@ -354,6 +278,19 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
354
278
|
const [profileNeed, setProfileNeed] = useState<ProfileStartupNeed | null>(
|
|
355
279
|
null,
|
|
356
280
|
);
|
|
281
|
+
// What startup work could not do — the profile gate's refusals and its
|
|
282
|
+
// activations, a marketplace refresh — waiting for the modal slot, oldest
|
|
283
|
+
// first. Each is the one failure popup; the TUI says nothing else.
|
|
284
|
+
const [startupReports, setStartupReports] = useState<FailureReport[]>([]);
|
|
285
|
+
const queueReports = React.useCallback(
|
|
286
|
+
(...reports: Array<FailureReport | null>) => {
|
|
287
|
+
const present = reports.filter((r): r is FailureReport => r !== null);
|
|
288
|
+
if (present.length > 0) {
|
|
289
|
+
setStartupReports((queued) => [...queued, ...present]);
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
[],
|
|
293
|
+
);
|
|
357
294
|
const gitignoreModal = useGitignoreModal();
|
|
358
295
|
const [brokenPluginCount, setBrokenPluginCount] = useState<number | null>(
|
|
359
296
|
null,
|
|
@@ -409,8 +346,8 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
409
346
|
// The hooks keep the slot occupied for their whole flow (a loading modal
|
|
410
347
|
// while their fix runs, never HIDE_MODAL mid-flow), because an empty slot is
|
|
411
348
|
// what lets the next question in. A flow that succeeds ends by hiding its
|
|
412
|
-
// loading modal and
|
|
413
|
-
//
|
|
349
|
+
// loading modal and reports nothing: the screen it leaves shows the result.
|
|
350
|
+
// That does not stop a SCREEN from emptying
|
|
414
351
|
// it: `useModal().confirm` hides its modal when answered and the screen shows
|
|
415
352
|
// its loading modal a microtask later. A key pressed before the profile check
|
|
416
353
|
// resolved lands the profile question in exactly that gap, and the screen's
|
|
@@ -436,8 +373,32 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
436
373
|
// it and then the fresh one asked the same question twice. The fresh result
|
|
437
374
|
// replaces the queued one instead (see the check effect below).
|
|
438
375
|
useEffect(() => {
|
|
439
|
-
|
|
440
|
-
|
|
376
|
+
// The order lives in `nextStartupStep`, where it is tested: failure
|
|
377
|
+
// reports first — something that already happened, and the question
|
|
378
|
+
// after it may be answered differently once it is known — so the profile
|
|
379
|
+
// gate's refusals land before the profile question, which they do not
|
|
380
|
+
// replace; then gitignore, broken plugins, and the profile last.
|
|
381
|
+
const step = nextStartupStep({
|
|
382
|
+
modalOpen: !!state.modal,
|
|
383
|
+
reports: startupReports,
|
|
384
|
+
gitignoreReady: !!gitignoreData && !gitignorePending,
|
|
385
|
+
brokenCount: brokenPluginCount,
|
|
386
|
+
profileNeed,
|
|
387
|
+
});
|
|
388
|
+
if (!step) return;
|
|
389
|
+
if (step.kind === "report") {
|
|
390
|
+
setStartupReports(step.rest);
|
|
391
|
+
dispatch({
|
|
392
|
+
type: "SHOW_MODAL",
|
|
393
|
+
modal: {
|
|
394
|
+
type: "failures",
|
|
395
|
+
report: step.report,
|
|
396
|
+
onDismiss: () => dispatch({ type: "HIDE_MODAL" }),
|
|
397
|
+
},
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (step.kind === "gitignore" && gitignoreData) {
|
|
441
402
|
// Anything older than what is now on screen is dropped on arrival.
|
|
442
403
|
gitignoreCheck.current.shown = gitignoreData.request;
|
|
443
404
|
const need = gitignoreData.need;
|
|
@@ -447,15 +408,18 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
447
408
|
generated: need.generated,
|
|
448
409
|
// In place, then back to the app: tracked generated files are
|
|
449
410
|
// untracked, missing entries added; other items stay for the tab.
|
|
411
|
+
// A file git would not untrack is named with git's reason.
|
|
450
412
|
onFixSafe: async () =>
|
|
451
|
-
|
|
413
|
+
gitignoreStartupReport(
|
|
414
|
+
(await applyStartupFixes(state.projectPath)).failed,
|
|
415
|
+
),
|
|
452
416
|
});
|
|
453
417
|
setGitignoreData(null);
|
|
454
418
|
return;
|
|
455
419
|
}
|
|
456
|
-
if (
|
|
420
|
+
if (step.kind === "stale-load") {
|
|
457
421
|
staleLoadModal.show({
|
|
458
|
-
brokenCount:
|
|
422
|
+
brokenCount: step.brokenCount,
|
|
459
423
|
// Re-surveys before repairing, like the gitignore fixer: the registry
|
|
460
424
|
// may have moved between the startup scan and the user pressing Enter,
|
|
461
425
|
// and repairing from the stale count would act on rows that are no
|
|
@@ -465,8 +429,10 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
465
429
|
// The pins are re-read with it, from the same helper the survey used: a
|
|
466
430
|
// repair that capped at a different ceiling than the offer was counted
|
|
467
431
|
// against would move rows the dialog never mentioned.
|
|
432
|
+
//
|
|
433
|
+
// A repair that did not take is named, with its reason.
|
|
468
434
|
onFix: async () =>
|
|
469
|
-
|
|
435
|
+
staleRepairReport(
|
|
470
436
|
await repairAllStaleLoads(
|
|
471
437
|
state.projectPath,
|
|
472
438
|
undefined,
|
|
@@ -479,10 +445,12 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
479
445
|
}
|
|
480
446
|
// Not cleared here — see above. Re-called on every empty slot until the
|
|
481
447
|
// hook reports it answered.
|
|
482
|
-
if (
|
|
448
|
+
if (step.kind === "profile") profileModal.show(step.need);
|
|
483
449
|
}, [
|
|
484
450
|
state.modal,
|
|
485
451
|
state.projectPath,
|
|
452
|
+
dispatch,
|
|
453
|
+
startupReports,
|
|
486
454
|
gitignoreData,
|
|
487
455
|
gitignorePending,
|
|
488
456
|
gitignoreModal,
|
|
@@ -572,39 +540,32 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
572
540
|
// edits into the active profile, regenerate from a changed
|
|
573
541
|
// profiles.json, restore a lost marker — then the question, if one
|
|
574
542
|
// is left to ask.
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
notify(line.text, line.tone);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
for (const line of problems) notify(line, "warning");
|
|
543
|
+
// A refusal never blocks the TUI at startup: the user is still asked
|
|
544
|
+
// whatever is left, and the screens show the state as it is. What
|
|
545
|
+
// the gate refused, and what its re-render could not do, go into ONE
|
|
546
|
+
// failure popup first — one step, one popup — queued in the same
|
|
547
|
+
// tick as the question, so it is shown before it.
|
|
548
|
+
const { need, problems, activations } = await prepareProfileStartup(
|
|
549
|
+
process.cwd(),
|
|
550
|
+
);
|
|
551
|
+
queueReports(profileStartupReport(problems, activations ?? []));
|
|
588
552
|
if (!needsNothing(need)) setProfileNeed(need);
|
|
589
553
|
} catch {
|
|
590
554
|
// non-fatal: the Profiles tab still works by hand
|
|
591
555
|
}
|
|
592
556
|
})();
|
|
593
|
-
}, [
|
|
557
|
+
}, [queueReports]);
|
|
594
558
|
|
|
595
559
|
// Auto-refresh marketplaces on startup
|
|
596
560
|
useEffect(() => {
|
|
597
561
|
const noRefresh = process.argv.includes("--no-refresh");
|
|
598
562
|
if (noRefresh) return;
|
|
599
563
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
// Migrate old marketplace names -> magus (idempotent), then repair plugin.json files
|
|
606
|
-
migrateMarketplaceRename().catch(() => {}); // non-blocking, best-effort
|
|
607
|
-
|
|
564
|
+
// Opening magus reads Claude Code's files and never writes them: the
|
|
565
|
+
// marketplace registry, its clones, installed_plugins.json and every
|
|
566
|
+
// settings.json are Claude Code's. The one change made here goes through
|
|
567
|
+
// Claude Code's own CLI.
|
|
568
|
+
//
|
|
608
569
|
// Auto-add marketplaces referenced by enabledPlugins but missing from
|
|
609
570
|
// the local registry. Fixes the "fresh clone of a project whose
|
|
610
571
|
// .claude/settings.json enables @magus plugins but this machine has
|
|
@@ -612,68 +573,17 @@ function AppContent({ onExit }: AppContentProps) {
|
|
|
612
573
|
(async () => {
|
|
613
574
|
try {
|
|
614
575
|
if (!(await isClaudeAvailable())) return;
|
|
615
|
-
|
|
616
|
-
if (added.length > 0) {
|
|
617
|
-
notify(`Fixed: added marketplace(s): ${added.join(", ")}`);
|
|
618
|
-
}
|
|
576
|
+
await autoAddMissingMarketplaces(process.cwd());
|
|
619
577
|
} catch {
|
|
620
578
|
// non-fatal: user can still install marketplaces manually
|
|
621
579
|
}
|
|
622
580
|
})();
|
|
623
|
-
|
|
624
|
-
// Recover stale marketplace registry entries (e.g. "directory" -> "github")
|
|
625
|
-
recoverMarketplaceSettings()
|
|
626
|
-
.then(async (recovery) => {
|
|
627
|
-
const parts: string[] = [];
|
|
628
|
-
// Apart from `parts`: a success line would carry them in green,
|
|
629
|
-
// and a marketplace left stale is something the user may act on.
|
|
630
|
-
const failed: string[] = [];
|
|
631
|
-
if (recovery.reregistered.length > 0) {
|
|
632
|
-
// Update the marketplace clone now that the source is fixed
|
|
633
|
-
const { updateMarketplace } = await import(
|
|
634
|
-
"../services/claude-cli.js"
|
|
635
|
-
);
|
|
636
|
-
for (const mp of recovery.reregistered) {
|
|
637
|
-
try {
|
|
638
|
-
await updateMarketplace(mp);
|
|
639
|
-
parts.push(`${mp} refreshed`);
|
|
640
|
-
} catch {
|
|
641
|
-
failed.push(mp);
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
if (recovery.removed.length > 0) {
|
|
646
|
-
parts.push(`removed: ${recovery.removed.join(", ")}`);
|
|
647
|
-
}
|
|
648
|
-
if (parts.length > 0) {
|
|
649
|
-
notify(`Fixed: ${parts.join(" | ")}`);
|
|
650
|
-
}
|
|
651
|
-
if (failed.length > 0) {
|
|
652
|
-
notify(
|
|
653
|
-
`Could not refresh marketplace(s): ${failed.join(", ")}`,
|
|
654
|
-
"warning",
|
|
655
|
-
);
|
|
656
|
-
}
|
|
657
|
-
})
|
|
658
|
-
.catch(() => {}); // non-fatal
|
|
659
|
-
|
|
660
|
-
repairAllMarketplaces()
|
|
661
|
-
.then(async () => {
|
|
662
|
-
dispatch({ type: "HIDE_PROGRESS" });
|
|
663
|
-
dispatch({ type: "DATA_REFRESH_COMPLETE" });
|
|
664
|
-
})
|
|
665
|
-
.catch(() => {
|
|
666
|
-
dispatch({ type: "HIDE_PROGRESS" });
|
|
667
|
-
});
|
|
668
|
-
}, [dispatch, notify]);
|
|
581
|
+
}, []);
|
|
669
582
|
|
|
670
583
|
return (
|
|
671
584
|
<DimensionsProvider
|
|
672
585
|
showProgress={!!progress}
|
|
673
586
|
showUpdateBanner={!!updateInfo?.updateAvailable}
|
|
674
|
-
// One row per notice line, so the screen below shrinks rather than
|
|
675
|
-
// being pushed off the bottom of the terminal.
|
|
676
|
-
transientBannerCount={state.notices.length}
|
|
677
587
|
>
|
|
678
588
|
<AppContentInner updateInfo={updateInfo} onExit={onExit} />
|
|
679
589
|
</DimensionsProvider>
|