magus-cli 7.3.1 → 7.4.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__/blind/c1-npm-package.test.ts +206 -0
- package/src/__tests__/blind/c10-colour-roles.test.ts +265 -0
- package/src/__tests__/blind/c11-cli-parse-refusal.test.ts +454 -0
- package/src/__tests__/blind/c12-skill-frontmatter.test.ts +323 -0
- package/src/__tests__/blind/c13-wrong-shape.test.ts +603 -0
- package/src/__tests__/blind/c14-cli-row-label.test.ts +259 -0
- package/src/__tests__/blind/c15-probe-ownership.test.ts +533 -0
- package/src/__tests__/blind/c16-nested-wrong-shape.test.ts +297 -0
- package/src/__tests__/blind/c17-enter-label.test.ts +625 -0
- package/src/__tests__/blind/c18-hook-shape.test.ts +354 -0
- package/src/__tests__/blind/c2-settings-parse.test.ts +350 -0
- package/src/__tests__/blind/c4-key-dispatch.test.ts +397 -0
- package/src/__tests__/blind/c5-keymap.test.ts +461 -0
- package/src/__tests__/blind/c6-request-gate.test.ts +133 -0
- package/src/__tests__/blind/c7-tab-tiers.test.ts +320 -0
- package/src/__tests__/blind/c8-text-width.test.ts +137 -0
- package/src/__tests__/blind/c9-cli-tool-version.test.ts +239 -0
- package/src/__tests__/blind/child-run.ts +114 -0
- package/src/__tests__/cli-tools-adapter.test.ts +5 -2
- package/src/__tests__/component-badge.test.ts +20 -36
- package/src/__tests__/footer-hints.test.ts +1 -1
- package/src/__tests__/manifest.test.ts +25 -17
- package/src/__tests__/marketplace-badge.test.ts +4 -4
- package/src/__tests__/models-screen-state.test.ts +36 -13
- package/src/__tests__/scope-squares.test.tsx +28 -114
- package/src/__tests__/shutdown.test.ts +578 -0
- package/src/__tests__/styles-manager.test.ts +4 -4
- package/src/__tests__/styles-screen-state.test.ts +26 -12
- package/src/__tests__/tabbar-layout.test.ts +44 -32
- package/src/__tests__/theme-adaptive-colors.test.ts +6 -59
- package/src/__tests__/tool-probe.test.ts +47 -22
- package/src/__tests__/uppercase-keybindings.test.ts +25 -12
- package/src/cli/ansi.ts +2 -2
- package/src/cli/bootstrap.ts +33 -2
- package/src/cli/doctor.ts +105 -14
- package/src/cli/install.ts +8 -2
- package/src/cli/profile.ts +18 -1
- package/src/cli/router.ts +31 -18
- package/src/cli/update-view.ts +1 -1
- package/src/data/cli-tools.ts +9 -1
- package/src/services/claude-settings.ts +151 -73
- package/src/services/cli-tool-commands.ts +15 -5
- package/src/services/cli-tool-version.ts +101 -0
- package/src/services/hook-registration.ts +23 -6
- package/src/services/manifest.ts +117 -52
- package/src/services/plugin-manager.ts +19 -5
- package/src/services/profile-adopt.ts +7 -2
- package/src/services/profile-startup.ts +87 -7
- package/src/services/profiles.ts +36 -23
- package/src/services/settings-file.ts +158 -4
- package/src/services/settings-manager.ts +19 -5
- package/src/services/skills-manager.ts +46 -0
- package/src/services/tool-probe.ts +272 -45
- package/src/services/toolchain.ts +39 -0
- package/src/services/update-engine.ts +5 -0
- package/src/services/update-plan.ts +9 -0
- package/src/tui.tsx +48 -21
- package/src/ui/App.tsx +58 -197
- package/src/ui/adapters/cliToolsAdapter.ts +141 -12
- package/src/ui/adapters/pluginsAdapter.ts +10 -7
- package/src/ui/adapters/settingsAdapter.ts +0 -21
- package/src/ui/adapters/skillsAdapter.ts +27 -8
- package/src/ui/adapters/stylesAdapter.ts +15 -11
- package/src/ui/components/CategoryHeader.tsx +7 -1
- package/src/ui/components/EmptyFilterState.tsx +2 -2
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScreenErrorBoundary.tsx +105 -0
- package/src/ui/components/TabBar.tsx +93 -21
- package/src/ui/components/layout/FooterHints.tsx +6 -27
- package/src/ui/components/layout/ScreenLayout.tsx +68 -42
- package/src/ui/components/layout/index.ts +1 -5
- package/src/ui/components/modals/ConfirmModal.tsx +2 -19
- package/src/ui/components/modals/HelpOverlay.tsx +186 -0
- package/src/ui/components/modals/MessageModal.tsx +3 -15
- package/src/ui/components/modals/ModalContainer.tsx +99 -60
- package/src/ui/components/primitives/ErrorNotice.tsx +38 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +18 -7
- package/src/ui/components/primitives/MetaText.tsx +1 -1
- package/src/ui/components/primitives/ScopeDetail.tsx +31 -26
- package/src/ui/components/primitives/ScopeSquares.tsx +23 -28
- package/src/ui/components/primitives/SelectableRow.tsx +24 -1
- package/src/ui/components/primitives/index.ts +4 -3
- package/src/ui/hooks/useProfileStartupModal.ts +15 -3
- package/src/ui/input/KeyRouter.tsx +180 -0
- package/src/ui/input/key-dispatch.ts +149 -0
- package/src/ui/keymap.ts +898 -0
- package/src/ui/renderers/cliToolRenderers.tsx +128 -66
- package/src/ui/renderers/gitignoreRenderers.tsx +41 -43
- package/src/ui/renderers/markdownRenderer.tsx +2 -2
- package/src/ui/renderers/mcpRenderers.tsx +28 -14
- package/src/ui/renderers/modelRenderers.tsx +12 -13
- package/src/ui/renderers/modelVisuals.tsx +0 -2
- package/src/ui/renderers/pluginRenderers.tsx +47 -54
- package/src/ui/renderers/profileRenderers.tsx +32 -37
- package/src/ui/renderers/settingsRenderers.tsx +84 -55
- package/src/ui/renderers/skillRenderers.tsx +237 -190
- package/src/ui/renderers/styleRenderers.tsx +13 -2
- package/src/ui/runtime/shutdown.ts +268 -0
- package/src/ui/screens/AliasScreen.tsx +118 -100
- package/src/ui/screens/CliToolsScreen.tsx +98 -87
- package/src/ui/screens/EnvVarsScreen.tsx +44 -42
- package/src/ui/screens/GitignoreScreen.tsx +50 -55
- package/src/ui/screens/McpRegistryScreen.tsx +52 -86
- package/src/ui/screens/McpScreen.tsx +47 -33
- package/src/ui/screens/ModelsScreen.tsx +59 -104
- package/src/ui/screens/PluginsScreen.tsx +123 -127
- package/src/ui/screens/ProfilesScreen.tsx +49 -46
- package/src/ui/screens/SkillsScreen.tsx +184 -204
- package/src/ui/screens/StylesScreen.tsx +104 -157
- package/src/ui/state/AppContext.tsx +6 -0
- package/src/ui/state/DimensionsContext.tsx +1 -10
- package/src/ui/state/reducer.ts +5 -8
- package/src/ui/state/types.ts +11 -9
- package/src/ui/text-width.ts +341 -0
- package/src/ui/theme-mode.ts +34 -71
- package/src/ui/theme.ts +50 -28
- package/src/utils/request-gate.ts +37 -0
- package/src/ui/components/ScopeIndicator.tsx +0 -63
- package/src/ui/components/SearchInput.tsx +0 -57
- package/src/ui/hooks/index.ts +0 -2
- package/src/ui/hooks/useKeyboard.ts +0 -24
- package/src/ui/hooks/useKeyboardHandler.ts +0 -63
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Blind tests — contract C14 (requirement R9.6).
|
|
3
|
+
*
|
|
4
|
+
* Written from requirements.md and architecture.md only.
|
|
5
|
+
*
|
|
6
|
+
* A CLI Tools row in state `unknown` shows a short label chosen by its
|
|
7
|
+
* reason: at most 24 terminal cells, never a filesystem path. The long note
|
|
8
|
+
* (today "on PATH at /Users/…", always cut off) belongs to the detail pane's
|
|
9
|
+
* "Why" line, not to the row.
|
|
10
|
+
*
|
|
11
|
+
* Pure: rows are built by hand to the CliToolRow contract. No IO.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, test } from "bun:test";
|
|
14
|
+
import { type CliTool, cliTools } from "../../data/cli-tools.js";
|
|
15
|
+
import type { ToolStatus } from "../../services/tool-probe.js";
|
|
16
|
+
import type { BinRequirement } from "../../types/index.js";
|
|
17
|
+
import {
|
|
18
|
+
type CliToolRow,
|
|
19
|
+
cliToolStatusText,
|
|
20
|
+
} from "../../ui/adapters/cliToolsAdapter.js";
|
|
21
|
+
|
|
22
|
+
const MAX_CELLS = 24;
|
|
23
|
+
|
|
24
|
+
const LONG_PATH_NOTE =
|
|
25
|
+
"on PATH at /Users/someone/.nvm/versions/node/v24/lib/node_modules/x/run.js, which is not the copy npm manages (npm root -g is /Users/someone/.local/lib/node_modules)";
|
|
26
|
+
const OTHER_NOTE =
|
|
27
|
+
"a second copy was found at /opt/homebrew/lib/node_modules/x/bin/x.js and another under ~/.bun/install/global/node_modules/x";
|
|
28
|
+
|
|
29
|
+
// ── terminal cell width, independent of the code under test ────────────────
|
|
30
|
+
|
|
31
|
+
/** BMP code points that terminals draw two cells wide as emoji. */
|
|
32
|
+
const WIDE_BMP_EMOJI = new Set([
|
|
33
|
+
0x231a, 0x231b, 0x23e9, 0x23ea, 0x23eb, 0x23ec, 0x23f0, 0x23f3, 0x25fd,
|
|
34
|
+
0x25fe, 0x2614, 0x2615, 0x267f, 0x2693, 0x26a1, 0x26aa, 0x26ab, 0x26bd,
|
|
35
|
+
0x26be, 0x26c4, 0x26c5, 0x26ce, 0x26d4, 0x26ea, 0x26f2, 0x26f3, 0x26f5,
|
|
36
|
+
0x26fa, 0x26fd, 0x2705, 0x270a, 0x270b, 0x2728, 0x274c, 0x274e, 0x2753,
|
|
37
|
+
0x2754, 0x2755, 0x2757, 0x2795, 0x2796, 0x2797, 0x27b0, 0x27bf, 0x2b1b,
|
|
38
|
+
0x2b1c, 0x2b50, 0x2b55,
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
function isWide(cp: number): boolean {
|
|
42
|
+
return (
|
|
43
|
+
(cp >= 0x1100 && cp <= 0x115f) ||
|
|
44
|
+
(cp >= 0x2648 && cp <= 0x2653) ||
|
|
45
|
+
(cp >= 0x2e80 && cp <= 0x303e) ||
|
|
46
|
+
(cp >= 0x3041 && cp <= 0x33ff) ||
|
|
47
|
+
(cp >= 0x3400 && cp <= 0x4dbf) ||
|
|
48
|
+
(cp >= 0x4e00 && cp <= 0x9fff) ||
|
|
49
|
+
(cp >= 0xa000 && cp <= 0xa4cf) ||
|
|
50
|
+
(cp >= 0xac00 && cp <= 0xd7a3) ||
|
|
51
|
+
(cp >= 0xf900 && cp <= 0xfaff) ||
|
|
52
|
+
(cp >= 0xfe30 && cp <= 0xfe4f) ||
|
|
53
|
+
(cp >= 0xff00 && cp <= 0xff60) ||
|
|
54
|
+
(cp >= 0xffe0 && cp <= 0xffe6) ||
|
|
55
|
+
(cp >= 0x1f300 && cp <= 0x1f64f) ||
|
|
56
|
+
(cp >= 0x1f900 && cp <= 0x1f9ff) ||
|
|
57
|
+
(cp >= 0x1f680 && cp <= 0x1f6ff) ||
|
|
58
|
+
(cp >= 0x1fa70 && cp <= 0x1faff) ||
|
|
59
|
+
(cp >= 0x20000 && cp <= 0x3fffd) ||
|
|
60
|
+
WIDE_BMP_EMOJI.has(cp)
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isZeroWidth(cp: number): boolean {
|
|
65
|
+
return (
|
|
66
|
+
(cp >= 0x0300 && cp <= 0x036f) ||
|
|
67
|
+
(cp >= 0x200b && cp <= 0x200f) ||
|
|
68
|
+
(cp >= 0xfe00 && cp <= 0xfe0e) ||
|
|
69
|
+
cp === 0x2060
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** An ANSI CSI sequence (ESC [ … letter); built so no control byte sits in source. */
|
|
74
|
+
const ANSI_CSI = new RegExp(
|
|
75
|
+
`${String.fromCharCode(27)}\\[[0-9;?]*[A-Za-z]`,
|
|
76
|
+
"g",
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
/** Terminal cells: wide characters count 2, combining marks 0, ANSI 0. */
|
|
80
|
+
function cells(text: string): number {
|
|
81
|
+
const plain = text.replace(ANSI_CSI, "");
|
|
82
|
+
const cps = Array.from(plain, (ch) => ch.codePointAt(0) ?? 0);
|
|
83
|
+
let width = 0;
|
|
84
|
+
for (let i = 0; i < cps.length; i++) {
|
|
85
|
+
const cp = cps[i] as number;
|
|
86
|
+
if (cp === 0xfe0f) continue; // counted with its base, below
|
|
87
|
+
if (isZeroWidth(cp)) continue;
|
|
88
|
+
// A narrow base with emoji presentation (U+FE0F) is drawn two wide.
|
|
89
|
+
width += isWide(cp) || cps[i + 1] === 0xfe0f ? 2 : 1;
|
|
90
|
+
}
|
|
91
|
+
return width;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
describe("C14 — the test's own cell counter", () => {
|
|
95
|
+
test("R9.6 control: ASCII counts 1, CJK and emoji count 2", () => {
|
|
96
|
+
expect(cells("abc")).toBe(3);
|
|
97
|
+
expect(cells("路径")).toBe(4);
|
|
98
|
+
expect(cells("⚠️")).toBe(2);
|
|
99
|
+
expect(cells("✅ ok")).toBe(5);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// ── rows ────────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
const TOOL: CliTool = (cliTools.find((t) => t.name === "crush") ??
|
|
106
|
+
cliTools[0]) as CliTool;
|
|
107
|
+
|
|
108
|
+
const SPEC: BinRequirement = {
|
|
109
|
+
name: TOOL.name,
|
|
110
|
+
via: "npm",
|
|
111
|
+
package: "@example/x",
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
type UnknownReason = Exclude<
|
|
115
|
+
ToolStatus["reason"],
|
|
116
|
+
"absent" | "up-to-date" | "behind" | "at-pin" | "behind-pin"
|
|
117
|
+
>;
|
|
118
|
+
|
|
119
|
+
/** Every reason that can put a row in `unknown`, with realistic versions. */
|
|
120
|
+
const UNKNOWN: Record<
|
|
121
|
+
UnknownReason,
|
|
122
|
+
Pick<ToolStatus, "installed" | "latest" | "target"> & { pin?: string }
|
|
123
|
+
> = {
|
|
124
|
+
"version-unreadable": { installed: null, latest: "2.0.0", target: null },
|
|
125
|
+
unorderable: {
|
|
126
|
+
installed: "nightly-2026-09-01",
|
|
127
|
+
latest: "2.0.0",
|
|
128
|
+
target: null,
|
|
129
|
+
},
|
|
130
|
+
"ahead-of-pin": {
|
|
131
|
+
installed: "2.1.0",
|
|
132
|
+
latest: "2.1.0",
|
|
133
|
+
target: null,
|
|
134
|
+
pin: "2.0.0",
|
|
135
|
+
},
|
|
136
|
+
"not-managed": { installed: "1.2.3", latest: "2.0.0", target: null },
|
|
137
|
+
"held-back": {
|
|
138
|
+
installed: "1.2.3",
|
|
139
|
+
latest: "2.0.0",
|
|
140
|
+
target: null,
|
|
141
|
+
pin: "1.2.3",
|
|
142
|
+
},
|
|
143
|
+
"pin-not-installable": {
|
|
144
|
+
installed: "1.2.3",
|
|
145
|
+
latest: "2.0.0",
|
|
146
|
+
target: null,
|
|
147
|
+
pin: "9.9.9",
|
|
148
|
+
},
|
|
149
|
+
"latest-unavailable": { installed: "1.2.3", latest: null, target: null },
|
|
150
|
+
"not-probed": { installed: null, latest: null, target: null },
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
function row(status: ToolStatus): CliToolRow {
|
|
154
|
+
return {
|
|
155
|
+
tool: TOOL,
|
|
156
|
+
spec: SPEC,
|
|
157
|
+
checking: false,
|
|
158
|
+
status,
|
|
159
|
+
installCommand: { cmd: "npm", args: ["install", "-g", "@example/x"] },
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function unknownRow(reason: UnknownReason, note: string | undefined) {
|
|
164
|
+
const fields = UNKNOWN[reason];
|
|
165
|
+
const status: ToolStatus = {
|
|
166
|
+
name: TOOL.name,
|
|
167
|
+
state: "unknown",
|
|
168
|
+
reason,
|
|
169
|
+
installed: fields.installed,
|
|
170
|
+
latest: fields.latest,
|
|
171
|
+
target: fields.target,
|
|
172
|
+
...(fields.pin ? { pin: fields.pin } : {}),
|
|
173
|
+
...(note !== undefined ? { note } : {}),
|
|
174
|
+
};
|
|
175
|
+
return row(status);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const REASONS = Object.keys(UNKNOWN) as UnknownReason[];
|
|
179
|
+
|
|
180
|
+
describe("C14 — cliToolStatusText on an unknown row", () => {
|
|
181
|
+
test("R9.6 control: the test covers all eight unknown-capable reasons", () => {
|
|
182
|
+
expect([...REASONS].sort()).toEqual(
|
|
183
|
+
[
|
|
184
|
+
"ahead-of-pin",
|
|
185
|
+
"held-back",
|
|
186
|
+
"latest-unavailable",
|
|
187
|
+
"not-managed",
|
|
188
|
+
"not-probed",
|
|
189
|
+
"pin-not-installable",
|
|
190
|
+
"unorderable",
|
|
191
|
+
"version-unreadable",
|
|
192
|
+
].sort(),
|
|
193
|
+
);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
for (const reason of REASONS) {
|
|
197
|
+
test(`R9.6 unknown / ${reason} with a long path note: the suffix is at most ${MAX_CELLS} cells`, () => {
|
|
198
|
+
const { suffix } = cliToolStatusText(unknownRow(reason, LONG_PATH_NOTE));
|
|
199
|
+
// The suffix rides along so a failure shows what the row printed.
|
|
200
|
+
expect({ suffix, fits: cells(suffix) <= MAX_CELLS }).toEqual({
|
|
201
|
+
suffix,
|
|
202
|
+
fits: true,
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test(`R9.6 unknown / ${reason} with a long path note: the suffix holds no filesystem path`, () => {
|
|
207
|
+
const { suffix } = cliToolStatusText(unknownRow(reason, LONG_PATH_NOTE));
|
|
208
|
+
expect(suffix).not.toContain("/");
|
|
209
|
+
expect(suffix).not.toContain("node_modules");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test(`R9.6 unknown / ${reason}: the row shows a label, not nothing`, () => {
|
|
213
|
+
const { suffix } = cliToolStatusText(unknownRow(reason, LONG_PATH_NOTE));
|
|
214
|
+
expect(suffix.trim().length).toBeGreaterThan(0);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test(`R9.6 unknown / ${reason}: the label is chosen by the reason, not by the note`, () => {
|
|
218
|
+
const a = cliToolStatusText(unknownRow(reason, LONG_PATH_NOTE)).suffix;
|
|
219
|
+
const b = cliToolStatusText(unknownRow(reason, OTHER_NOTE)).suffix;
|
|
220
|
+
expect(b).toBe(a);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
test("R9.6 a note full of wide characters still yields a suffix within the cell budget", () => {
|
|
225
|
+
const note = "工具在路径 /opt/工具/bin/工具 找到,但不是 npm 管理的副本 ⚠️";
|
|
226
|
+
for (const reason of REASONS) {
|
|
227
|
+
const { suffix } = cliToolStatusText(unknownRow(reason, note));
|
|
228
|
+
expect({ reason, cells: cells(suffix) <= MAX_CELLS }).toEqual({
|
|
229
|
+
reason,
|
|
230
|
+
cells: true,
|
|
231
|
+
});
|
|
232
|
+
expect({ reason, slash: suffix.includes("/") }).toEqual({
|
|
233
|
+
reason,
|
|
234
|
+
slash: false,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("C14 — control: rows that are not unknown", () => {
|
|
241
|
+
test("R9.6 control: an upgrade row with a target still shows the target version", () => {
|
|
242
|
+
const status: ToolStatus = {
|
|
243
|
+
name: TOOL.name,
|
|
244
|
+
state: "upgrade",
|
|
245
|
+
reason: "behind",
|
|
246
|
+
installed: "1.0.0",
|
|
247
|
+
latest: "9.8.7",
|
|
248
|
+
target: "9.8.7",
|
|
249
|
+
};
|
|
250
|
+
const { suffix } = cliToolStatusText({
|
|
251
|
+
...row(status),
|
|
252
|
+
updateCommand: {
|
|
253
|
+
cmd: "npm",
|
|
254
|
+
args: ["install", "-g", "@example/x@latest"],
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
expect(suffix).toContain("9.8.7");
|
|
258
|
+
});
|
|
259
|
+
});
|