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,5 +1,5 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
|
-
import type { McpServer } from "../../types/index.js";
|
|
2
|
+
import type { McpRegistryServer, McpServer } from "../../types/index.js";
|
|
3
3
|
import { rowLead } from "../components/primitives/index.js";
|
|
4
4
|
import { theme } from "../theme.js";
|
|
5
5
|
|
|
@@ -10,11 +10,27 @@ import { theme } from "../theme.js";
|
|
|
10
10
|
*/
|
|
11
11
|
const SERVER_ICON = "⬢";
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* A server added or removed this magus session, on its row: a running Claude
|
|
15
|
+
* Code picks the change up only after it restarts. One string for the curated
|
|
16
|
+
* and the registry rows alike.
|
|
17
|
+
*/
|
|
18
|
+
const RESTART_MARK = " ↻ restart Claude Code";
|
|
19
|
+
|
|
13
20
|
// ─── List item types ───────────────────────────────────────────────────────────
|
|
14
21
|
|
|
15
22
|
export type McpListItem =
|
|
16
23
|
| { kind: "category"; label: string }
|
|
17
|
-
| {
|
|
24
|
+
| {
|
|
25
|
+
kind: "server";
|
|
26
|
+
server: McpServer;
|
|
27
|
+
isInstalled: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Added or removed in this magus session: a running Claude Code picks
|
|
30
|
+
* it up only after it restarts. Absent means false.
|
|
31
|
+
*/
|
|
32
|
+
needsRestart?: boolean;
|
|
33
|
+
};
|
|
18
34
|
|
|
19
35
|
// ─── Row renderers ─────────────────────────────────────────────────────────────
|
|
20
36
|
|
|
@@ -39,13 +55,15 @@ export function renderMcpRow(
|
|
|
39
55
|
const { server, isInstalled } = item;
|
|
40
56
|
const icon = isInstalled ? "●" : "○";
|
|
41
57
|
const iconColor = isInstalled ? theme.colors.success : theme.colors.muted;
|
|
58
|
+
const restart = item.needsRestart ? RESTART_MARK : "";
|
|
42
59
|
|
|
43
60
|
if (isSelected) {
|
|
44
61
|
return (
|
|
45
62
|
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
46
63
|
{rowLead(true)}
|
|
47
64
|
{icon} {server.name}
|
|
48
|
-
{server.requiresConfig ? " ⚙" : ""}
|
|
65
|
+
{server.requiresConfig ? " ⚙" : ""}
|
|
66
|
+
{restart}{" "}
|
|
49
67
|
</text>
|
|
50
68
|
);
|
|
51
69
|
}
|
|
@@ -58,10 +76,91 @@ export function renderMcpRow(
|
|
|
58
76
|
</span>
|
|
59
77
|
<span fg={theme.colors.text}> {server.name}</span>
|
|
60
78
|
{server.requiresConfig ? <span fg={theme.colors.warning}> ⚙</span> : null}
|
|
79
|
+
{restart ? <span fg={theme.colors.warning}>{restart}</span> : null}
|
|
61
80
|
</text>
|
|
62
81
|
);
|
|
63
82
|
}
|
|
64
83
|
|
|
84
|
+
// ─── Registry rows ─────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* One server on the MCP Registry screen, with the same two states a curated
|
|
88
|
+
* row on the MCP screen carries: installed in this project, and — when it was
|
|
89
|
+
* added this magus session — that a running Claude Code picks it up only after
|
|
90
|
+
* it restarts.
|
|
91
|
+
*
|
|
92
|
+
* Adding from the registry used to show nothing at all: the restart state was
|
|
93
|
+
* drawn only on curated rows, and a registry server is not one, so the promise
|
|
94
|
+
* "shown on the MCP screen" had no row to be shown on.
|
|
95
|
+
*/
|
|
96
|
+
export interface McpRegistryListItem {
|
|
97
|
+
server: McpRegistryServer;
|
|
98
|
+
isInstalled: boolean;
|
|
99
|
+
needsRestart: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function renderMcpRegistryRow(
|
|
103
|
+
item: McpRegistryListItem,
|
|
104
|
+
_index: number,
|
|
105
|
+
isSelected: boolean,
|
|
106
|
+
): React.ReactNode {
|
|
107
|
+
const { server, isInstalled, needsRestart } = item;
|
|
108
|
+
const version = server.version ? ` v${server.version}` : "";
|
|
109
|
+
const icon = isInstalled ? "● " : "";
|
|
110
|
+
const restart = needsRestart ? RESTART_MARK : "";
|
|
111
|
+
if (isSelected) {
|
|
112
|
+
return (
|
|
113
|
+
<text bg={theme.selection.bg} fg={theme.selection.fg}>
|
|
114
|
+
{rowLead(true)}
|
|
115
|
+
{icon}
|
|
116
|
+
{server.name}
|
|
117
|
+
{version}
|
|
118
|
+
{restart}{" "}
|
|
119
|
+
</text>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
return (
|
|
123
|
+
<text fg={theme.colors.text}>
|
|
124
|
+
<span>{rowLead(false)}</span>
|
|
125
|
+
{icon ? <span fg={theme.colors.success}>{icon}</span> : null}
|
|
126
|
+
<span>
|
|
127
|
+
<strong>{server.name}</strong>
|
|
128
|
+
</span>
|
|
129
|
+
<span fg={theme.colors.info}>{version}</span>
|
|
130
|
+
{restart ? <span fg={theme.colors.warning}>{restart}</span> : null}
|
|
131
|
+
</text>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The registry detail panel's state lines — installed or not, and the
|
|
137
|
+
* restart — in the same words and colours as a curated server's detail.
|
|
138
|
+
*/
|
|
139
|
+
export function renderMcpRegistryStatus(
|
|
140
|
+
item: McpRegistryListItem,
|
|
141
|
+
): React.ReactNode {
|
|
142
|
+
return (
|
|
143
|
+
<box flexDirection="column">
|
|
144
|
+
<box flexDirection="row">
|
|
145
|
+
<text fg={theme.colors.muted}>{"Status "}</text>
|
|
146
|
+
{item.isInstalled ? (
|
|
147
|
+
<text fg={theme.colors.success}>{"● Installed"}</text>
|
|
148
|
+
) : (
|
|
149
|
+
<text fg={theme.colors.muted}>{"○ Not installed"}</text>
|
|
150
|
+
)}
|
|
151
|
+
</box>
|
|
152
|
+
{item.needsRestart ? (
|
|
153
|
+
<box flexDirection="row">
|
|
154
|
+
<text fg={theme.colors.muted}>{"Takes effect "}</text>
|
|
155
|
+
<text fg={theme.colors.warning} wrapMode="word">
|
|
156
|
+
{"↻ after restarting Claude Code"}
|
|
157
|
+
</text>
|
|
158
|
+
</box>
|
|
159
|
+
) : null}
|
|
160
|
+
</box>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
65
164
|
// ─── Detail renderer ───────────────────────────────────────────────────────────
|
|
66
165
|
|
|
67
166
|
export function renderMcpDetail(
|
|
@@ -112,6 +211,14 @@ export function renderMcpDetail(
|
|
|
112
211
|
<text fg={theme.colors.muted}>{"○ Not installed"}</text>
|
|
113
212
|
)}
|
|
114
213
|
</box>
|
|
214
|
+
{item.needsRestart ? (
|
|
215
|
+
<box flexDirection="row">
|
|
216
|
+
<text fg={theme.colors.muted}>{"Takes effect "}</text>
|
|
217
|
+
<text fg={theme.colors.warning} wrapMode="word">
|
|
218
|
+
{"↻ after restarting Claude Code"}
|
|
219
|
+
</text>
|
|
220
|
+
</box>
|
|
221
|
+
) : null}
|
|
115
222
|
<box flexDirection="row">
|
|
116
223
|
<text fg={theme.colors.muted}>{"Type "}</text>
|
|
117
224
|
<text fg={theme.colors.text}>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
2
|
import { getMarketplaceVersion } from "../../services/marketplace-fetcher.js";
|
|
3
|
+
import { mcpSettingsNeededText } from "../../services/plugin-env-needs.js";
|
|
3
4
|
import {
|
|
4
5
|
compareVersions,
|
|
5
6
|
hasMissingFiles,
|
|
@@ -10,11 +11,11 @@ import {
|
|
|
10
11
|
} from "../../services/plugin-manager.js";
|
|
11
12
|
import type { ScopeStatus } from "../../services/plugin-manager.js";
|
|
12
13
|
import type { PluginComponentKind } from "../../services/skills-manager.js";
|
|
13
|
-
import
|
|
14
|
-
PluginUpdateAction,
|
|
15
|
-
PluginUpdateItem,
|
|
14
|
+
import {
|
|
15
|
+
type PluginUpdateAction,
|
|
16
|
+
type PluginUpdateItem,
|
|
17
|
+
isKeptDeprecated,
|
|
16
18
|
} from "../../services/update-plan.js";
|
|
17
|
-
import { isKnownVersion } from "../../services/version-snapshot.js";
|
|
18
19
|
import type { PluginRelease } from "../../types/index.js";
|
|
19
20
|
import { highlightMatches } from "../../utils/fuzzy-search.js";
|
|
20
21
|
import type {
|
|
@@ -43,7 +44,11 @@ import type {
|
|
|
43
44
|
import { componentChip } from "../theme-mode.js";
|
|
44
45
|
import { type UiColor, theme } from "../theme.js";
|
|
45
46
|
import { renderMarkdown } from "./markdownRenderer.js";
|
|
46
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
type PluginRowSegment,
|
|
49
|
+
isKnownVersion,
|
|
50
|
+
pluginRowText,
|
|
51
|
+
} from "./pluginRowText.js";
|
|
47
52
|
import { type StatusTone, pluginStatusText } from "./pluginStatusText.js";
|
|
48
53
|
|
|
49
54
|
// ─── Category renderers ───────────────────────────────────────────────────────
|
|
@@ -285,6 +290,7 @@ const ACTION_LABEL: Record<PluginUpdateAction, string> = {
|
|
|
285
290
|
// The DETAIL panel's label, not the row's — this is where the mechanism is
|
|
286
291
|
// allowed to be named, and the note beside it spells out both versions.
|
|
287
292
|
"stale-load": "BROKEN — OLDER VERSION LOADS",
|
|
293
|
+
remove: "DEPRECATED",
|
|
288
294
|
unknown: "UNKNOWN",
|
|
289
295
|
};
|
|
290
296
|
|
|
@@ -321,12 +327,14 @@ function StatusLine({
|
|
|
321
327
|
: status.action === "update" && status.target
|
|
322
328
|
? `→ v${status.target}`
|
|
323
329
|
: "";
|
|
330
|
+
// Moved or still required: kept on purpose, which is known — never UNKNOWN.
|
|
331
|
+
const label = isKeptDeprecated(status) ? "KEPT" : ACTION_LABEL[status.action];
|
|
324
332
|
return (
|
|
325
333
|
<KeyValueLine
|
|
326
334
|
label="Status"
|
|
327
335
|
value={
|
|
328
336
|
<span>
|
|
329
|
-
<span fg={toneColor(tone)}>{
|
|
337
|
+
<span fg={toneColor(tone)}>{label}</span>
|
|
330
338
|
{detail ? <span fg={theme.colors.text}> {detail}</span> : null}
|
|
331
339
|
</span>
|
|
332
340
|
}
|
|
@@ -349,6 +357,57 @@ function pluginDetail(
|
|
|
349
357
|
|
|
350
358
|
// Published by no marketplace we read. The status line says which of the
|
|
351
359
|
// three things that means — moved, withdrawn, or its marketplace unread.
|
|
360
|
+
//
|
|
361
|
+
// Deprecated — a marketplace we read no longer lists it — is the one of the
|
|
362
|
+
// three that is a fact, and gets one plain line in the user's terms. The row
|
|
363
|
+
// already says `a` removes it; `d` removes just this one.
|
|
364
|
+
if (plugin.isOrphaned && status?.action === "remove") {
|
|
365
|
+
return (
|
|
366
|
+
<box flexDirection="column">
|
|
367
|
+
<box justifyContent="center">
|
|
368
|
+
<text bg={theme.colors.danger} fg={theme.hints.fg}>
|
|
369
|
+
<strong> {plugin.name} — DEPRECATED </strong>
|
|
370
|
+
</text>
|
|
371
|
+
</box>
|
|
372
|
+
{/* No Status line: the heading says DEPRECATED and the line below
|
|
373
|
+
says why, so the note would be the same fact a third time. */}
|
|
374
|
+
<box marginTop={1}>
|
|
375
|
+
<text fg={theme.colors.text}>
|
|
376
|
+
No longer published by{" "}
|
|
377
|
+
{plugin.marketplaceDisplay || plugin.marketplace}. Claude Code does
|
|
378
|
+
not load it.
|
|
379
|
+
</text>
|
|
380
|
+
</box>
|
|
381
|
+
<ActionHints hints={[{ key: "d", label: "Remove", tone: "danger" }]} />
|
|
382
|
+
</box>
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
// Kept on purpose: it moved to another marketplace, or a plugin that stays
|
|
386
|
+
// still requires it. The status line's note says which; nothing is wrong
|
|
387
|
+
// enough for amber, and `d` would only undo a deliberate keep.
|
|
388
|
+
if (plugin.isOrphaned && status && isKeptDeprecated(status)) {
|
|
389
|
+
return (
|
|
390
|
+
<box flexDirection="column">
|
|
391
|
+
<box justifyContent="center">
|
|
392
|
+
<text bg={theme.colors.muted} fg={theme.hints.fg}>
|
|
393
|
+
<strong> {plugin.name} — KEPT </strong>
|
|
394
|
+
</text>
|
|
395
|
+
</box>
|
|
396
|
+
<box marginTop={1}>
|
|
397
|
+
<StatusLine status={status} />
|
|
398
|
+
</box>
|
|
399
|
+
<box marginTop={1}>
|
|
400
|
+
<text fg={theme.colors.muted}>
|
|
401
|
+
No longer published by{" "}
|
|
402
|
+
{plugin.marketplaceDisplay || plugin.marketplace}.{" "}
|
|
403
|
+
{status.reason === "moved"
|
|
404
|
+
? "magus leaves this copy installed; the plugin is still published elsewhere."
|
|
405
|
+
: "magus keeps it installed while a plugin that needs it stays."}
|
|
406
|
+
</text>
|
|
407
|
+
</box>
|
|
408
|
+
</box>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
352
411
|
if (plugin.isOrphaned) {
|
|
353
412
|
return (
|
|
354
413
|
<box flexDirection="column">
|
|
@@ -466,6 +525,18 @@ function pluginDetail(
|
|
|
466
525
|
</text>
|
|
467
526
|
</box>
|
|
468
527
|
) : null}
|
|
528
|
+
{/*
|
|
529
|
+
* A lasting state, not a message: derived from the current config each
|
|
530
|
+
* load, so it is here for as long as the variables are unset — however
|
|
531
|
+
* the install question was answered — and gone once they are set.
|
|
532
|
+
*/}
|
|
533
|
+
{item.mcpSettingsNeeded ? (
|
|
534
|
+
<box>
|
|
535
|
+
<text fg={theme.colors.warning}>
|
|
536
|
+
⚙ {mcpSettingsNeededText(item.mcpSettingsNeeded)}
|
|
537
|
+
</text>
|
|
538
|
+
</box>
|
|
539
|
+
) : null}
|
|
469
540
|
{/*
|
|
470
541
|
* THE panel where the mechanism is spelled out in full. The row above
|
|
471
542
|
* says only "BROKEN — press a to fix", on purpose; a reader who wants to
|
|
@@ -12,27 +12,43 @@
|
|
|
12
12
|
* - an orphan's text depended on the cursor — the selected row said
|
|
13
13
|
* "moved → X", the unselected one always said "deprecated".
|
|
14
14
|
*
|
|
15
|
+
* A row shows the current version and the state, nothing about history. It
|
|
16
|
+
* used to badge what changed since magus last looked ("v1.8.0 → v1.7.4
|
|
17
|
+
* updated"), from a snapshot of versions seen, and rendered a downgrade as
|
|
18
|
+
* "updated" beside an offer to update again.
|
|
19
|
+
*
|
|
15
20
|
* Pure and `.ts`, so a test can assert on exactly what the row prints.
|
|
16
21
|
*/
|
|
17
22
|
|
|
18
|
-
import { isKnownVersion } from "../../services/version-snapshot.js";
|
|
19
23
|
import type { PluginPluginItem } from "../adapters/pluginsAdapter.js";
|
|
20
24
|
import { type StatusTone, pluginStatusText } from "./pluginStatusText.js";
|
|
21
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Sentinel recorded by Claude Code for a plugin that is installed but whose
|
|
28
|
+
* version is unknown — every plugin in the official Anthropic marketplace uses
|
|
29
|
+
* it. It is not a real version and must never be rendered as one.
|
|
30
|
+
*/
|
|
31
|
+
export const UNKNOWN_VERSION = "0.0.0";
|
|
32
|
+
|
|
33
|
+
/** True when a version string carries information worth printing. */
|
|
34
|
+
export function isKnownVersion(v: string | undefined): v is string {
|
|
35
|
+
return !!v && v !== UNKNOWN_VERSION;
|
|
36
|
+
}
|
|
37
|
+
|
|
22
38
|
export interface PluginRowSegment {
|
|
23
39
|
text: string;
|
|
24
40
|
tone: StatusTone | "text";
|
|
25
41
|
}
|
|
26
42
|
|
|
27
43
|
export interface PluginRowText {
|
|
28
|
-
/** After the name: the version,
|
|
44
|
+
/** After the name: the version, then the status suffix. */
|
|
29
45
|
segments: PluginRowSegment[];
|
|
30
46
|
/** The segments joined — exactly what the row renders after the name. */
|
|
31
47
|
text: string;
|
|
32
48
|
}
|
|
33
49
|
|
|
34
50
|
export function pluginRowText(item: PluginPluginItem): PluginRowText {
|
|
35
|
-
const {
|
|
51
|
+
const { status } = item;
|
|
36
52
|
const segments: PluginRowSegment[] = [];
|
|
37
53
|
|
|
38
54
|
// A version is shown only when it carries information. "0.0.0" means
|
|
@@ -43,32 +59,16 @@ export function pluginRowText(item: PluginPluginItem): PluginRowText {
|
|
|
43
59
|
// `plugin.installedVersion` printed a version where the classifier had none —
|
|
44
60
|
// "v0.8.1 files missing", with 0.8.1 read off a scope not installed here.
|
|
45
61
|
const installed = status?.installed ?? undefined;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// "What just changed" and "what state it is in now" are independent facts
|
|
49
|
-
// and both can be true at once: an external update can move a plugin
|
|
50
|
-
// 2.9.0 → 3.0.1 while the marketplace already offers 3.3.1. So the badge
|
|
51
|
-
// replaces only the version, and the status suffix is always appended.
|
|
52
|
-
if (plugin.recentlyUpdatedFrom && shown) {
|
|
53
|
-
// Show what actually happened, not just where it landed — the update may
|
|
54
|
-
// have come from Claude Code or a direct CLI call without telling us.
|
|
55
|
-
segments.push({
|
|
56
|
-
text: ` v${plugin.recentlyUpdatedFrom} → v${shown} updated`,
|
|
57
|
-
tone: "success",
|
|
58
|
-
});
|
|
59
|
-
} else if (plugin.recentlyInstalled) {
|
|
60
|
-
// Newly seen. A plugin can be genuinely installed with no version we can
|
|
61
|
-
// name, so the version is optional here.
|
|
62
|
-
segments.push({
|
|
63
|
-
text: shown ? ` v${shown} new` : " new",
|
|
64
|
-
tone: "success",
|
|
65
|
-
});
|
|
66
|
-
} else if (shown) {
|
|
67
|
-
segments.push({ text: ` v${shown}`, tone: "muted" });
|
|
62
|
+
if (isKnownVersion(installed)) {
|
|
63
|
+
segments.push({ text: ` v${installed}`, tone: "muted" });
|
|
68
64
|
}
|
|
69
65
|
|
|
70
66
|
const suffix = pluginStatusText(status);
|
|
71
67
|
if (suffix.suffix) segments.push({ text: suffix.suffix, tone: suffix.tone });
|
|
72
68
|
|
|
69
|
+
// Its MCP server lacks variables. The same ⚙ the MCP screen puts on a
|
|
70
|
+
// server that needs configuring; the detail panel says how many, and the key.
|
|
71
|
+
if (item.mcpSettingsNeeded) segments.push({ text: " ⚙", tone: "warning" });
|
|
72
|
+
|
|
73
73
|
return { segments, text: segments.map((s) => s.text).join("") };
|
|
74
74
|
}
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
* which is what makes the two front ends say the same thing.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import
|
|
14
|
+
import {
|
|
15
|
+
type PluginUpdateItem,
|
|
16
|
+
isKeptDeprecated,
|
|
17
|
+
} from "../../services/update-plan.js";
|
|
15
18
|
|
|
16
19
|
export type StatusTone = "muted" | "warning" | "danger" | "success" | "info";
|
|
17
20
|
|
|
@@ -27,6 +30,16 @@ export function pluginStatusText(
|
|
|
27
30
|
if (!status) return { suffix: "", tone: "muted" };
|
|
28
31
|
const withNote = status.note ? ` (${status.note})` : "";
|
|
29
32
|
|
|
33
|
+
// No longer published here, and kept on purpose — it moved, or a plugin
|
|
34
|
+
// that stays still requires it. A fact, not "cannot tell": muted, with the
|
|
35
|
+
// note ("moved to Magus Marketing"), never the amber "?".
|
|
36
|
+
if (isKeptDeprecated(status)) {
|
|
37
|
+
return {
|
|
38
|
+
suffix: ` ${status.note ?? "no longer published"}`,
|
|
39
|
+
tone: "muted",
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
30
43
|
switch (status.action) {
|
|
31
44
|
case "unknown":
|
|
32
45
|
// Amber, not red: "cannot tell" is not a failure.
|
|
@@ -55,6 +68,10 @@ export function pluginStatusText(
|
|
|
55
68
|
// whose session runs a version nobody installed is broken, and there is
|
|
56
69
|
// now a key that fixes it.
|
|
57
70
|
return { suffix: " BROKEN — press a to fix", tone: "danger" };
|
|
71
|
+
case "remove":
|
|
72
|
+
// No longer published. Red, like BROKEN: Claude Code refuses to load it,
|
|
73
|
+
// and `a` removes it. Which marketplace dropped it is the detail panel's.
|
|
74
|
+
return { suffix: " deprecated — press a to remove", tone: "danger" };
|
|
58
75
|
case "install":
|
|
59
76
|
// `absent` only ever comes from a profile pin (see `pinForRow`), so it
|
|
60
77
|
// may say so. An undeclared row whose files are missing must not.
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
validateAliasName,
|
|
33
33
|
withoutDerivedChannel,
|
|
34
34
|
} from "../../services/alias-store.js";
|
|
35
|
+
import { failureReport } from "../adapters/failureReport.js";
|
|
36
|
+
import { stepFailureReport } from "../adapters/startupReports.js";
|
|
35
37
|
import {
|
|
36
38
|
FlagDetailEditor,
|
|
37
39
|
type FlagDetailEditorHandle,
|
|
@@ -39,7 +41,7 @@ import {
|
|
|
39
41
|
import { ScreenLayout } from "../components/layout/index.js";
|
|
40
42
|
import { useScreenKeys } from "../input/KeyRouter.js";
|
|
41
43
|
import { moveDirection } from "../keymap.js";
|
|
42
|
-
import { useApp, useModal } from "../state/AppContext.js";
|
|
44
|
+
import { useApp, useFailurePopup, useModal } from "../state/AppContext.js";
|
|
43
45
|
import { displayWidth, truncate } from "../text-width.js";
|
|
44
46
|
import { theme } from "../theme.js";
|
|
45
47
|
|
|
@@ -114,6 +116,7 @@ type ViewMode = { kind: "main" } | { kind: "flag-detail"; flagId: string };
|
|
|
114
116
|
export function AliasScreen() {
|
|
115
117
|
const { state } = useApp();
|
|
116
118
|
const modal = useModal();
|
|
119
|
+
const showFailures = useFailurePopup();
|
|
117
120
|
|
|
118
121
|
const [config, setConfig] = useState<AliasConfig | null>(null);
|
|
119
122
|
const [shells, setShells] = useState<ShellTarget[]>([]);
|
|
@@ -126,11 +129,6 @@ export function AliasScreen() {
|
|
|
126
129
|
string,
|
|
127
130
|
FlagValue
|
|
128
131
|
> | null>(null);
|
|
129
|
-
// Transient write feedback shown on the header status line (not a blocking
|
|
130
|
-
// modal). Auto-clears after a few seconds. Errors still use a modal since
|
|
131
|
-
// they need to be read and acknowledged.
|
|
132
|
-
const [writeStatus, setWriteStatus] = useState<string | null>(null);
|
|
133
|
-
|
|
134
132
|
// A hand-written `claude` alias found OUTSIDE any managed block on mount.
|
|
135
133
|
// Set when the rc file has no managed block but a stray alias we could
|
|
136
134
|
// adopt — drives the one-time "import it?" offer below. Cleared once the
|
|
@@ -143,13 +141,6 @@ export function AliasScreen() {
|
|
|
143
141
|
[number, number] | null
|
|
144
142
|
>(null);
|
|
145
143
|
|
|
146
|
-
// Clear the write status line a few seconds after it appears.
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
if (!writeStatus) return;
|
|
149
|
-
const t = setTimeout(() => setWriteStatus(null), 4000);
|
|
150
|
-
return () => clearTimeout(t);
|
|
151
|
-
}, [writeStatus]);
|
|
152
|
-
|
|
153
144
|
// One-time "adopt a hand-written alias?" offer. Fires when the mount load
|
|
154
145
|
// found a stray `claude` alias outside any managed block. We import only the
|
|
155
146
|
// flags we recognized; whether we also TAKE OWNERSHIP (remove the original
|
|
@@ -420,14 +411,12 @@ export function AliasScreen() {
|
|
|
420
411
|
try {
|
|
421
412
|
await saveAliasName(name);
|
|
422
413
|
} catch (err) {
|
|
423
|
-
await
|
|
424
|
-
"
|
|
425
|
-
err instanceof Error ? err.message : String(err),
|
|
426
|
-
"error",
|
|
414
|
+
await showFailures(
|
|
415
|
+
stepFailureReport("The alias name was not saved", name, err),
|
|
427
416
|
);
|
|
428
417
|
}
|
|
429
418
|
},
|
|
430
|
-
[
|
|
419
|
+
[showFailures],
|
|
431
420
|
);
|
|
432
421
|
|
|
433
422
|
// Cascade dependencies on every mutation:
|
|
@@ -499,12 +488,16 @@ export function AliasScreen() {
|
|
|
499
488
|
if (v === null) return;
|
|
500
489
|
const reason = validateAliasName(v);
|
|
501
490
|
if (reason) {
|
|
502
|
-
await
|
|
491
|
+
await showFailures(
|
|
492
|
+
failureReport("The alias name was not changed", [
|
|
493
|
+
{ subject: v === "" ? "(empty)" : v, reason },
|
|
494
|
+
]),
|
|
495
|
+
);
|
|
503
496
|
return;
|
|
504
497
|
}
|
|
505
498
|
setConfig((prev) => (prev ? { ...prev, aliasName: v } : prev));
|
|
506
499
|
void persistAliasName(v);
|
|
507
|
-
}, [config, modal, persistAliasName]);
|
|
500
|
+
}, [config, modal, persistAliasName, showFailures]);
|
|
508
501
|
|
|
509
502
|
const editSelected = useCallback(async () => {
|
|
510
503
|
if (!selectedRow) return;
|
|
@@ -565,10 +558,16 @@ export function AliasScreen() {
|
|
|
565
558
|
shells.find((s) => s.exists);
|
|
566
559
|
|
|
567
560
|
if (!target) {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
"
|
|
561
|
+
// Nothing was written — a failure, like the write failing below. Named
|
|
562
|
+
// files, because creating one is the fix.
|
|
563
|
+
await showFailures(
|
|
564
|
+
failureReport("The alias was not written", [
|
|
565
|
+
{
|
|
566
|
+
subject: "shell config",
|
|
567
|
+
reason: "there is no shell config file to write it to",
|
|
568
|
+
fix: "Create ~/.zshrc, ~/.bashrc or ~/.config/fish/config.fish, then press w again",
|
|
569
|
+
},
|
|
570
|
+
]),
|
|
572
571
|
);
|
|
573
572
|
return;
|
|
574
573
|
}
|
|
@@ -578,7 +577,7 @@ export function AliasScreen() {
|
|
|
578
577
|
// the FIRST write replaces that line in place (absorbing it into the
|
|
579
578
|
// managed block) rather than appending a second alias. Once the block
|
|
580
579
|
// exists, the writer ignores the range and splices by markers.
|
|
581
|
-
|
|
580
|
+
await writeAliasToShell(
|
|
582
581
|
config,
|
|
583
582
|
target,
|
|
584
583
|
pendingAdoptRange ? { adoptLineRange: pendingAdoptRange } : {},
|
|
@@ -586,23 +585,20 @@ export function AliasScreen() {
|
|
|
586
585
|
// The adopted line is now gone (replaced by the block); drop the range
|
|
587
586
|
// so subsequent writes don't try to re-target a line that moved.
|
|
588
587
|
if (pendingAdoptRange) setPendingAdoptRange(null);
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
setWriteStatus(
|
|
592
|
-
`✓ ${r.action === "created" ? "Created" : "Updated"} ${name} — source it or open a new shell`,
|
|
593
|
-
);
|
|
588
|
+
// Written: no message. The header shows it — the snapshot now matches
|
|
589
|
+
// the in-memory flags, so "unsaved, w writes …" disappears.
|
|
594
590
|
setShells(await detectShells());
|
|
595
|
-
// We just wrote the rc file with current in-memory flag values, so
|
|
596
|
-
// the snapshot now matches in-memory state. Dirty indicator clears.
|
|
597
591
|
setRcSnapshot(config.flags);
|
|
598
592
|
} catch (err) {
|
|
599
|
-
await
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
593
|
+
await showFailures(
|
|
594
|
+
stepFailureReport(
|
|
595
|
+
"The alias was not written",
|
|
596
|
+
`${target.kind} config`,
|
|
597
|
+
err,
|
|
598
|
+
),
|
|
603
599
|
);
|
|
604
600
|
}
|
|
605
|
-
}, [config, shells,
|
|
601
|
+
}, [config, shells, pendingAdoptRange, showFailures]);
|
|
606
602
|
|
|
607
603
|
// ── Keyboard ──────────────────────────────────────────────────────────
|
|
608
604
|
// j/k navigate the selectable subset, jumping over headers/legend lines.
|
|
@@ -829,13 +825,11 @@ export function AliasScreen() {
|
|
|
829
825
|
: 0;
|
|
830
826
|
const subtitle = inDetail
|
|
831
827
|
? `editing ${detailFlag.flag}`
|
|
832
|
-
:
|
|
833
|
-
?
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
}`
|
|
838
|
-
: "";
|
|
828
|
+
: config
|
|
829
|
+
? `${enabledCount} flag${enabledCount === 1 ? "" : "s"} enabled${
|
|
830
|
+
dirty ? ` │ * unsaved, w writes ${writeLabel}` : ""
|
|
831
|
+
}`
|
|
832
|
+
: "";
|
|
839
833
|
|
|
840
834
|
return (
|
|
841
835
|
<ScreenLayout
|