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
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { FailureReport, FailureRow } from "../../state/types.js";
|
|
2
|
+
import { theme } from "../../theme.js";
|
|
3
|
+
|
|
4
|
+
/** Border (2) and horizontal padding (3 + 3). */
|
|
5
|
+
const CHROME = 8;
|
|
6
|
+
const MAX_WIDTH = 88;
|
|
7
|
+
const MIN_WIDTH = 40;
|
|
8
|
+
/** "✗ " before each row's subject. */
|
|
9
|
+
const MARK = "✗ ";
|
|
10
|
+
/**
|
|
11
|
+
* Rows of the popup that are not failure rows: border (2), padding (2), the
|
|
12
|
+
* headline and its gap (2), the footer and its gap (2), and a row of air
|
|
13
|
+
* between the popup and the terminal's edge (2).
|
|
14
|
+
*/
|
|
15
|
+
const FIXED_ROWS = 10;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The popup's width for a terminal `terminalWidth` columns wide: as wide as the
|
|
19
|
+
* rows need, never wider than the terminal leaves room for.
|
|
20
|
+
*/
|
|
21
|
+
export function failureListWidth(terminalWidth: number): number {
|
|
22
|
+
return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, terminalWidth - 4));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether a row's reason goes on its own line under the subject. The subject
|
|
27
|
+
* never wraps — it is what the user scans for — so a long one (several plugins
|
|
28
|
+
* sharing a reason) takes the line to itself and the reason goes beneath it,
|
|
29
|
+
* rather than being squeezed into a sliver beside it.
|
|
30
|
+
*/
|
|
31
|
+
export function reasonBelow(subject: string, innerWidth: number): boolean {
|
|
32
|
+
return MARK.length + subject.length + 2 > innerWidth / 2;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Lines `text` takes when word-wrapped at `width` columns. */
|
|
36
|
+
function wrappedLines(text: string, width: number): number {
|
|
37
|
+
if (width <= 0) return text.length;
|
|
38
|
+
let lines = 1;
|
|
39
|
+
let used = 0;
|
|
40
|
+
for (const word of text.split(/\s+/).filter(Boolean)) {
|
|
41
|
+
const len = word.length;
|
|
42
|
+
if (used === 0) {
|
|
43
|
+
used = len;
|
|
44
|
+
} else if (used + 1 + len <= width) {
|
|
45
|
+
used += 1 + len;
|
|
46
|
+
} else {
|
|
47
|
+
lines++;
|
|
48
|
+
used = len;
|
|
49
|
+
}
|
|
50
|
+
while (used > width) {
|
|
51
|
+
lines++;
|
|
52
|
+
used -= width;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return lines;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Terminal rows one failure row takes at an inner width of `inner`. */
|
|
59
|
+
function rowHeight(row: FailureRow, inner: number): number {
|
|
60
|
+
if (reasonBelow(row.subject, inner)) {
|
|
61
|
+
const column = inner - MARK.length;
|
|
62
|
+
return (
|
|
63
|
+
1 +
|
|
64
|
+
wrappedLines(row.reason, column) +
|
|
65
|
+
(row.fix ? wrappedLines(row.fix, column) : 0)
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const column = inner - (MARK.length + row.subject.length + 2);
|
|
69
|
+
return (
|
|
70
|
+
wrappedLines(row.reason, column) +
|
|
71
|
+
(row.fix ? wrappedLines(row.fix, column) : 0)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The rows that fit a terminal `terminalHeight` rows tall, and how many did
|
|
77
|
+
* not. Without a height every row is drawn. A popup taller than the terminal
|
|
78
|
+
* would push its own headline and footer off screen, so it stops early and
|
|
79
|
+
* says `+N more` instead — and always shows at least one row.
|
|
80
|
+
*/
|
|
81
|
+
export function visibleFailureRows(
|
|
82
|
+
report: FailureReport,
|
|
83
|
+
terminalWidth: number,
|
|
84
|
+
terminalHeight?: number,
|
|
85
|
+
): { rows: FailureRow[]; hidden: number } {
|
|
86
|
+
if (terminalHeight === undefined) return { rows: report.rows, hidden: 0 };
|
|
87
|
+
const inner = failureListWidth(terminalWidth) - CHROME;
|
|
88
|
+
const fixRows = report.fix ? 1 + wrappedLines(report.fix, inner) : 0;
|
|
89
|
+
const budget = terminalHeight - FIXED_ROWS - fixRows;
|
|
90
|
+
const total = report.rows.reduce(
|
|
91
|
+
(sum, row) => sum + rowHeight(row, inner),
|
|
92
|
+
0,
|
|
93
|
+
);
|
|
94
|
+
if (total <= budget) return { rows: report.rows, hidden: 0 };
|
|
95
|
+
|
|
96
|
+
// One row is kept back for the "+N more" line.
|
|
97
|
+
const rows: FailureRow[] = [];
|
|
98
|
+
let used = 0;
|
|
99
|
+
for (const row of report.rows) {
|
|
100
|
+
const height = rowHeight(row, inner);
|
|
101
|
+
if (rows.length > 0 && used + height > budget - 1) break;
|
|
102
|
+
rows.push(row);
|
|
103
|
+
used += height;
|
|
104
|
+
}
|
|
105
|
+
return { rows, hidden: report.rows.length - rows.length };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A row's reason and, when it has its own, its fix beneath it. A plain call,
|
|
110
|
+
* not a component, so the whole popup is one element tree.
|
|
111
|
+
*/
|
|
112
|
+
function reasonColumn(row: FailureRow) {
|
|
113
|
+
return (
|
|
114
|
+
<box flexDirection="column" flexGrow={1} flexShrink={1}>
|
|
115
|
+
<text fg={theme.colors.muted} wrapMode="word">
|
|
116
|
+
{row.reason}
|
|
117
|
+
</text>
|
|
118
|
+
{row.fix ? (
|
|
119
|
+
<text fg={theme.colors.text} wrapMode="word">
|
|
120
|
+
{row.fix}
|
|
121
|
+
</text>
|
|
122
|
+
) : null}
|
|
123
|
+
</box>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* THE failure popup: what did not happen, counted; each thing that failed with
|
|
129
|
+
* why; and the one fix, when there is one. The shape is {@link FailureReport},
|
|
130
|
+
* built by a pure function per screen, so every failure reads the same way.
|
|
131
|
+
*
|
|
132
|
+
* Theme tokens only: the ✗ in danger, the subject in the text colour, the
|
|
133
|
+
* reason muted — the reason is the detail, the subject is what the user scans.
|
|
134
|
+
* A fix is in the text colour, under its row when only that row has it, or
|
|
135
|
+
* once under all of them. Reasons wrap; subjects do not. Given the terminal's
|
|
136
|
+
* height, it draws only the rows that fit and ends with `+N more`. Any key
|
|
137
|
+
* dismisses it (ModalContainer).
|
|
138
|
+
*/
|
|
139
|
+
export function FailureListModal({
|
|
140
|
+
report,
|
|
141
|
+
terminalWidth,
|
|
142
|
+
terminalHeight,
|
|
143
|
+
}: {
|
|
144
|
+
report: FailureReport;
|
|
145
|
+
terminalWidth: number;
|
|
146
|
+
/** Cap the rows to this many terminal rows; every row is drawn without it. */
|
|
147
|
+
terminalHeight?: number;
|
|
148
|
+
}) {
|
|
149
|
+
const width = failureListWidth(terminalWidth);
|
|
150
|
+
const inner = width - CHROME;
|
|
151
|
+
const { rows, hidden } = visibleFailureRows(
|
|
152
|
+
report,
|
|
153
|
+
terminalWidth,
|
|
154
|
+
terminalHeight,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<box
|
|
159
|
+
flexDirection="column"
|
|
160
|
+
border
|
|
161
|
+
borderStyle="rounded"
|
|
162
|
+
borderColor={theme.colors.dim}
|
|
163
|
+
backgroundColor={theme.surface.bg}
|
|
164
|
+
paddingLeft={3}
|
|
165
|
+
paddingRight={3}
|
|
166
|
+
paddingTop={1}
|
|
167
|
+
paddingBottom={1}
|
|
168
|
+
width={width}
|
|
169
|
+
>
|
|
170
|
+
<box marginBottom={1}>
|
|
171
|
+
<text fg={theme.colors.text}>
|
|
172
|
+
<span fg={theme.colors.danger}>{MARK}</span>
|
|
173
|
+
<strong>{report.headline}</strong>
|
|
174
|
+
</text>
|
|
175
|
+
</box>
|
|
176
|
+
|
|
177
|
+
{/*
|
|
178
|
+
* The index is part of the key: a report built with `group: false`
|
|
179
|
+
* keeps rows apart even when subject and reason match, and a repeated
|
|
180
|
+
* key makes React drop or reuse one of them.
|
|
181
|
+
*/}
|
|
182
|
+
{rows.map((row, index) =>
|
|
183
|
+
reasonBelow(row.subject, inner) ? (
|
|
184
|
+
<box
|
|
185
|
+
key={`${row.subject}:${row.reason}:${index}`}
|
|
186
|
+
flexDirection="column"
|
|
187
|
+
>
|
|
188
|
+
<text fg={theme.colors.text}>
|
|
189
|
+
<span fg={theme.colors.danger}>{MARK}</span>
|
|
190
|
+
<span fg={theme.colors.text}>{row.subject}</span>
|
|
191
|
+
</text>
|
|
192
|
+
<box paddingLeft={MARK.length}>{reasonColumn(row)}</box>
|
|
193
|
+
</box>
|
|
194
|
+
) : (
|
|
195
|
+
<box
|
|
196
|
+
key={`${row.subject}:${row.reason}:${index}`}
|
|
197
|
+
flexDirection="row"
|
|
198
|
+
>
|
|
199
|
+
<box flexShrink={0}>
|
|
200
|
+
<text fg={theme.colors.text}>
|
|
201
|
+
<span fg={theme.colors.danger}>{MARK}</span>
|
|
202
|
+
<span fg={theme.colors.text}>{`${row.subject} `}</span>
|
|
203
|
+
</text>
|
|
204
|
+
</box>
|
|
205
|
+
{reasonColumn(row)}
|
|
206
|
+
</box>
|
|
207
|
+
),
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{hidden > 0 ? (
|
|
211
|
+
<box paddingLeft={MARK.length}>
|
|
212
|
+
<text fg={theme.colors.muted}>{`+${hidden} more`}</text>
|
|
213
|
+
</box>
|
|
214
|
+
) : null}
|
|
215
|
+
|
|
216
|
+
{report.fix ? (
|
|
217
|
+
<box marginTop={1}>
|
|
218
|
+
<text fg={theme.colors.text} wrapMode="word">
|
|
219
|
+
{report.fix}
|
|
220
|
+
</text>
|
|
221
|
+
</box>
|
|
222
|
+
) : null}
|
|
223
|
+
|
|
224
|
+
<box marginTop={1}>
|
|
225
|
+
<text fg={theme.colors.muted}>Press any key to continue</text>
|
|
226
|
+
</box>
|
|
227
|
+
</box>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export default FailureListModal;
|
|
@@ -6,19 +6,14 @@ interface MessageModalProps {
|
|
|
6
6
|
title: string;
|
|
7
7
|
/** Modal message */
|
|
8
8
|
message: string;
|
|
9
|
-
/** Message variant */
|
|
10
|
-
variant: "info" | "error";
|
|
11
9
|
}
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export function MessageModal({ title, message, variant }: MessageModalProps) {
|
|
20
|
-
const config = variantConfig[variant];
|
|
21
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Draws information to read: help text, a command to run. Never a failure —
|
|
13
|
+
* those go in `FailureListModal`. Any key dismisses it — ModalContainer
|
|
14
|
+
* handles that.
|
|
15
|
+
*/
|
|
16
|
+
export function MessageModal({ title, message }: MessageModalProps) {
|
|
22
17
|
return (
|
|
23
18
|
<box
|
|
24
19
|
flexDirection="column"
|
|
@@ -33,7 +28,7 @@ export function MessageModal({ title, message, variant }: MessageModalProps) {
|
|
|
33
28
|
width={60}
|
|
34
29
|
>
|
|
35
30
|
<box marginBottom={1}>
|
|
36
|
-
<text fg={
|
|
31
|
+
<text fg={theme.colors.info}>ℹ </text>
|
|
37
32
|
<text fg={theme.colors.text}>
|
|
38
33
|
<strong>{title}</strong>
|
|
39
34
|
</text>
|
|
@@ -5,6 +5,7 @@ import { type KeyPress, isScrollKey } from "../../input/key-dispatch.js";
|
|
|
5
5
|
import { useApp, useModal } from "../../state/AppContext.js";
|
|
6
6
|
import { useDimensions } from "../../state/DimensionsContext.js";
|
|
7
7
|
import { ConfirmModal } from "./ConfirmModal.js";
|
|
8
|
+
import { FailureListModal } from "./FailureListModal.js";
|
|
8
9
|
import { HelpOverlay, helpOverlaySize } from "./HelpOverlay.js";
|
|
9
10
|
import { InputModal } from "./InputModal.js";
|
|
10
11
|
import { LoadingModal } from "./LoadingModal.js";
|
|
@@ -37,7 +38,7 @@ const isPlain = (key: KeyPress) => !key.ctrl && !key.meta;
|
|
|
37
38
|
* Only an <input> that was ALREADY focused when the key arrived:
|
|
38
39
|
* the Enter that opens an input modal from a select never reaches
|
|
39
40
|
* it (KeyRouter's `dispatchThenGuardFocus`).
|
|
40
|
-
* - message: any key dismisses.
|
|
41
|
+
* - message, failures: any key dismisses.
|
|
41
42
|
* - loading: keys are swallowed until the work finishes.
|
|
42
43
|
* - help: ↑↓ / PgUp / PgDn / Ctrl+U / Ctrl+D scroll; Esc or `?` close.
|
|
43
44
|
*/
|
|
@@ -102,6 +103,7 @@ export function ModalContainer() {
|
|
|
102
103
|
break;
|
|
103
104
|
|
|
104
105
|
case "message":
|
|
106
|
+
case "failures":
|
|
105
107
|
modal.onDismiss();
|
|
106
108
|
break;
|
|
107
109
|
|
|
@@ -174,11 +176,14 @@ export function ModalContainer() {
|
|
|
174
176
|
);
|
|
175
177
|
|
|
176
178
|
case "message":
|
|
179
|
+
return <MessageModal title={modal.title} message={modal.message} />;
|
|
180
|
+
|
|
181
|
+
case "failures":
|
|
177
182
|
return (
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
<FailureListModal
|
|
184
|
+
report={modal.report}
|
|
185
|
+
terminalWidth={dimensions.terminalWidth}
|
|
186
|
+
terminalHeight={dimensions.terminalHeight}
|
|
182
187
|
/>
|
|
183
188
|
);
|
|
184
189
|
|
|
@@ -2,5 +2,6 @@ export { ConfirmModal } from "./ConfirmModal.js";
|
|
|
2
2
|
export { InputModal } from "./InputModal.js";
|
|
3
3
|
export { SelectModal } from "./SelectModal.js";
|
|
4
4
|
export { MessageModal } from "./MessageModal.js";
|
|
5
|
+
export { FailureListModal } from "./FailureListModal.js";
|
|
5
6
|
export { LoadingModal } from "./LoadingModal.js";
|
|
6
7
|
export { ModalContainer } from "./ModalContainer.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the app shows next at startup, when the modal slot is free — the ONE
|
|
3
|
+
* order every startup popup and question is asked in. Pure, so the order is
|
|
4
|
+
* tested without mounting the app; `App.tsx`'s startup effect acts on it.
|
|
5
|
+
*
|
|
6
|
+
* The order:
|
|
7
|
+
*
|
|
8
|
+
* 1. Failure reports, oldest first. A failure is something that already
|
|
9
|
+
* happened, and the question after it may be answered differently once
|
|
10
|
+
* it is known — so the profile gate's refusals, queued in the same tick as
|
|
11
|
+
* the profile question, are read BEFORE it and never replace it.
|
|
12
|
+
* 2. The gitignore question, once no newer check is pending.
|
|
13
|
+
* 3. The broken-plugins question.
|
|
14
|
+
* 4. The profile question — last, and asked again whenever the slot is free
|
|
15
|
+
* and it has not been answered (it cannot be dismissed).
|
|
16
|
+
*
|
|
17
|
+
* Nothing is shown while any modal is open: two needs arriving in one commit
|
|
18
|
+
* would otherwise both see an empty slot, and the later would replace the
|
|
19
|
+
* earlier after its need was cleared.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { FailureReport } from "../state/types.js";
|
|
23
|
+
|
|
24
|
+
export interface StartupQueueState<ProfileNeed> {
|
|
25
|
+
/** A modal (any modal) is on screen. */
|
|
26
|
+
modalOpen: boolean;
|
|
27
|
+
/** Failure reports not yet shown, oldest first. */
|
|
28
|
+
reports: readonly FailureReport[];
|
|
29
|
+
/** A gitignore answer is waiting and no newer check is running. */
|
|
30
|
+
gitignoreReady: boolean;
|
|
31
|
+
/** Plugins found loading an older version than installed, or null. */
|
|
32
|
+
brokenCount: number | null;
|
|
33
|
+
/** The unanswered profile question, or null. */
|
|
34
|
+
profileNeed: ProfileNeed | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type StartupStep<ProfileNeed> =
|
|
38
|
+
| { kind: "report"; report: FailureReport; rest: FailureReport[] }
|
|
39
|
+
| { kind: "gitignore" }
|
|
40
|
+
| { kind: "stale-load"; brokenCount: number }
|
|
41
|
+
| { kind: "profile"; need: ProfileNeed }
|
|
42
|
+
| null;
|
|
43
|
+
|
|
44
|
+
export function nextStartupStep<ProfileNeed>(
|
|
45
|
+
s: StartupQueueState<ProfileNeed>,
|
|
46
|
+
): StartupStep<ProfileNeed> {
|
|
47
|
+
if (s.modalOpen) return null;
|
|
48
|
+
const [report, ...rest] = s.reports;
|
|
49
|
+
if (report) return { kind: "report", report, rest };
|
|
50
|
+
if (s.gitignoreReady) return { kind: "gitignore" };
|
|
51
|
+
if (s.brokenCount !== null) {
|
|
52
|
+
return { kind: "stale-load", brokenCount: s.brokenCount };
|
|
53
|
+
}
|
|
54
|
+
if (s.profileNeed !== null) return { kind: "profile", need: s.profileNeed };
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
|
-
import
|
|
3
|
-
import { useApp, useNavigation
|
|
4
|
-
import type {
|
|
2
|
+
import { stepFailureReport } from "../adapters/startupReports.js";
|
|
3
|
+
import { useApp, useNavigation } from "../state/AppContext.js";
|
|
4
|
+
import type { FailureReport, ModalState } from "../state/types.js";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Startup gitignore-violation modal: magus launches, detects a problem, and
|
|
@@ -80,49 +80,25 @@ export function buildGitignoreModal(args: BuildModalArgs): ModalState {
|
|
|
80
80
|
};
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
/**
|
|
84
|
-
* What the startup fix did, as notices. Untracking is a warning because it is
|
|
85
|
-
* not finished until the user commits the staged removal — the one thing here
|
|
86
|
-
* they must still do. The files are named only there, and in a failure, which
|
|
87
|
-
* the user has to deal with by hand.
|
|
88
|
-
*/
|
|
89
|
-
export function startupFixNotices(result: StartupFixResult): NoticeLine[] {
|
|
90
|
-
const lines: NoticeLine[] = [];
|
|
91
|
-
if (result.untracked.length > 0) {
|
|
92
|
-
lines.push({
|
|
93
|
-
text: `Stopped tracking ${result.untracked.join(", ")} — commit the staged removal`,
|
|
94
|
-
tone: "warning",
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
if (result.appended.length > 0) {
|
|
98
|
-
lines.push({
|
|
99
|
-
text: `Added ${entries(result.appended.length)}`,
|
|
100
|
-
tone: "success",
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
for (const f of result.failed) {
|
|
104
|
-
lines.push({
|
|
105
|
-
text: `Could not stop tracking ${f.path}: ${f.reason}`,
|
|
106
|
-
tone: "warning",
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
return lines.length > 0
|
|
110
|
-
? lines
|
|
111
|
-
: [{ text: "Gitignore already up to date", tone: "success" }];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
83
|
export function useGitignoreModal() {
|
|
115
84
|
const { dispatch } = useApp();
|
|
116
85
|
const { navigateToScreen } = useNavigation();
|
|
117
|
-
const notify = useNotify();
|
|
118
86
|
|
|
119
87
|
const show = useCallback(
|
|
120
88
|
(args: {
|
|
121
89
|
violationCount: number;
|
|
122
90
|
safeCount: number;
|
|
123
91
|
generated?: readonly string[];
|
|
124
|
-
/**
|
|
125
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Runs the fix and answers what it could not do — null when all of it
|
|
94
|
+
* applied. The Git State tab shows what changed; failures go into the
|
|
95
|
+
* one failure popup.
|
|
96
|
+
*/
|
|
97
|
+
onFixSafe: () =>
|
|
98
|
+
| Promise<FailureReport | null | undefined>
|
|
99
|
+
| FailureReport
|
|
100
|
+
| null
|
|
101
|
+
| undefined;
|
|
126
102
|
}) => {
|
|
127
103
|
if (args.violationCount === 0) return;
|
|
128
104
|
const dismiss = () => dispatch({ type: "HIDE_MODAL" });
|
|
@@ -144,26 +120,28 @@ export function useGitignoreModal() {
|
|
|
144
120
|
// Called inside `.then`, not passed to `Promise.resolve`: a fixer
|
|
145
121
|
// that throws synchronously must land in `.catch` below too, or
|
|
146
122
|
// the loading modal above stays up and swallows every key.
|
|
123
|
+
const fail = (report: FailureReport) =>
|
|
124
|
+
dispatch({
|
|
125
|
+
type: "SHOW_MODAL",
|
|
126
|
+
modal: { type: "failures", report, onDismiss: dismiss },
|
|
127
|
+
});
|
|
147
128
|
void Promise.resolve()
|
|
148
129
|
.then(() => args.onFixSafe())
|
|
149
130
|
.then((report) => {
|
|
150
131
|
// The loading modal must go — it swallows every key — and the
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
132
|
+
// empty slot lets the next startup question in. What the fix
|
|
133
|
+
// could not do takes the slot first, in the failure popup.
|
|
134
|
+
if (report) fail(report);
|
|
135
|
+
else dispatch({ type: "HIDE_MODAL" });
|
|
155
136
|
})
|
|
156
137
|
.catch((err) => {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
onDismiss: dismiss,
|
|
165
|
-
},
|
|
166
|
-
});
|
|
138
|
+
fail(
|
|
139
|
+
stepFailureReport(
|
|
140
|
+
"The gitignore fixes were not applied",
|
|
141
|
+
".gitignore",
|
|
142
|
+
err,
|
|
143
|
+
),
|
|
144
|
+
);
|
|
167
145
|
});
|
|
168
146
|
},
|
|
169
147
|
onOpenTab: () => {
|
|
@@ -174,7 +152,7 @@ export function useGitignoreModal() {
|
|
|
174
152
|
});
|
|
175
153
|
dispatch({ type: "SHOW_MODAL", modal });
|
|
176
154
|
},
|
|
177
|
-
[dispatch, navigateToScreen
|
|
155
|
+
[dispatch, navigateToScreen],
|
|
178
156
|
);
|
|
179
157
|
|
|
180
158
|
return { show };
|