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,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test builders for requirements, from the version-managed shape tests read
|
|
3
|
+
* best (`{ name, via, package, version }`), through the one bridge production
|
|
4
|
+
* uses (`managedOption`). Shared so no test hand-builds the `SelectedBin`
|
|
5
|
+
* shape its own way.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { type CliTool, cliTools, isOwnedEntry } from "../../data/cli-tools.js";
|
|
9
|
+
import { ownedCliTool } from "../../services/cli-tool-commands.js";
|
|
10
|
+
import { managedOption } from "../../services/toolchain.js";
|
|
11
|
+
import type {
|
|
12
|
+
ManagedTool,
|
|
13
|
+
ResolvedBin,
|
|
14
|
+
SelectedBin,
|
|
15
|
+
} from "../../types/index.js";
|
|
16
|
+
|
|
17
|
+
/** A selected requirement whose one option (and choice) is `t`'s installer. */
|
|
18
|
+
export function selected(t: ManagedTool, root?: string): SelectedBin {
|
|
19
|
+
const option = managedOption(t);
|
|
20
|
+
return {
|
|
21
|
+
name: t.name,
|
|
22
|
+
check: (t.check ?? `${t.name} --version`).trim().split(/\s+/),
|
|
23
|
+
install: [option],
|
|
24
|
+
chosen: option,
|
|
25
|
+
...(root ? { root } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** {@link selected}, with provenance. */
|
|
30
|
+
export function resolved(t: ManagedTool, sources: string[]): ResolvedBin {
|
|
31
|
+
return { ...selected(t), sources };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A plugin-owned catalogue tool's requirement as its plugin declares it today:
|
|
36
|
+
* a bun install of the package named after the tool.
|
|
37
|
+
*/
|
|
38
|
+
export async function ownedAsDeclared(
|
|
39
|
+
_pluginId: string,
|
|
40
|
+
name: string,
|
|
41
|
+
): Promise<SelectedBin> {
|
|
42
|
+
return selected({ name, via: "bun", package: name });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The catalogue as CLI tools, owned entries resolved as {@link ownedAsDeclared}. */
|
|
46
|
+
export function testCatalogue(): CliTool[] {
|
|
47
|
+
return cliTools.map((entry) =>
|
|
48
|
+
isOwnedEntry(entry)
|
|
49
|
+
? (ownedCliTool(
|
|
50
|
+
entry,
|
|
51
|
+
selected({ name: entry.name, via: "bun", package: entry.name }),
|
|
52
|
+
) as CliTool)
|
|
53
|
+
: entry,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -64,7 +64,6 @@ describe("REQ-12 formatUpdateOutcome: what the TUI shows after Update it now", (
|
|
|
64
64
|
test('REQ-12a a problem makes the outcome "failed" with the fixed title and the problem in the message, even when the state reads ok', () => {
|
|
65
65
|
const result: ProfileStartup = {
|
|
66
66
|
need: needIn("ok"),
|
|
67
|
-
notices: [],
|
|
68
67
|
problems: ["boom"],
|
|
69
68
|
};
|
|
70
69
|
|
|
@@ -79,7 +78,7 @@ describe("REQ-12 formatUpdateOutcome: what the TUI shows after Update it now", (
|
|
|
79
78
|
|
|
80
79
|
test('REQ-12b no problems and a need still to answer gives "ask" carrying the same need object', () => {
|
|
81
80
|
const need = needIn("inactive");
|
|
82
|
-
const result: ProfileStartup = { need,
|
|
81
|
+
const result: ProfileStartup = { need, problems: [] };
|
|
83
82
|
|
|
84
83
|
const outcome = formatUpdateOutcome(result);
|
|
85
84
|
|
|
@@ -87,34 +86,12 @@ describe("REQ-12 formatUpdateOutcome: what the TUI shows after Update it now", (
|
|
|
87
86
|
expect((outcome as { need?: unknown }).need).toBe(need);
|
|
88
87
|
});
|
|
89
88
|
|
|
90
|
-
test('REQ-12c no problems
|
|
91
|
-
const result: ProfileStartup = {
|
|
92
|
-
need: needIn("ok"),
|
|
93
|
-
notices: ["absorbed 1 edit"],
|
|
94
|
-
problems: [],
|
|
95
|
-
};
|
|
89
|
+
test('REQ-12c no problems and state ok gives "updated", and nothing to say about it', () => {
|
|
90
|
+
const result: ProfileStartup = { need: needIn("ok"), problems: [] };
|
|
96
91
|
|
|
97
92
|
const outcome = formatUpdateOutcome(result);
|
|
98
93
|
|
|
99
|
-
expect(outcome).toEqual({
|
|
100
|
-
kind: "updated",
|
|
101
|
-
notice: "Profile format updated",
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
test('REQ-12d no problems, state ok and no notices gives the same "updated" notice', () => {
|
|
106
|
-
const result: ProfileStartup = {
|
|
107
|
-
need: needIn("ok"),
|
|
108
|
-
notices: [],
|
|
109
|
-
problems: [],
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const outcome = formatUpdateOutcome(result);
|
|
113
|
-
|
|
114
|
-
expect(outcome).toEqual({
|
|
115
|
-
kind: "updated",
|
|
116
|
-
notice: "Profile format updated",
|
|
117
|
-
});
|
|
94
|
+
expect(outcome).toEqual({ kind: "updated" });
|
|
118
95
|
});
|
|
119
96
|
});
|
|
120
97
|
|
|
@@ -137,7 +114,6 @@ for (const stream of [process.stdin, process.stdout, process.stderr]) {
|
|
|
137
114
|
|
|
138
115
|
const out = {
|
|
139
116
|
state: null,
|
|
140
|
-
notices: null,
|
|
141
117
|
problems: null,
|
|
142
118
|
outcome: null,
|
|
143
119
|
error: null,
|
|
@@ -148,14 +124,13 @@ try {
|
|
|
148
124
|
? await startup.prepareProfileStartup(project, { convert: true })
|
|
149
125
|
: await startup.prepareProfileStartup(project);
|
|
150
126
|
out.state = prepared.need.state;
|
|
151
|
-
out.notices = prepared.notices;
|
|
152
127
|
out.problems = Array.isArray(prepared.problems) ? prepared.problems : null;
|
|
153
128
|
const outcome = startup.formatUpdateOutcome(prepared);
|
|
154
129
|
out.outcome =
|
|
155
130
|
outcome.kind === "ask"
|
|
156
131
|
? { kind: "ask", needState: outcome.need.state }
|
|
157
132
|
: outcome.kind === "updated"
|
|
158
|
-
? { kind: "updated"
|
|
133
|
+
? { kind: "updated" }
|
|
159
134
|
: { kind: outcome.kind, title: outcome.title, message: outcome.message };
|
|
160
135
|
} catch (e) {
|
|
161
136
|
out.error = e instanceof Error ? (e.stack ?? e.message) : String(e);
|
|
@@ -166,12 +141,11 @@ process.exit(0);
|
|
|
166
141
|
|
|
167
142
|
interface StartupRun {
|
|
168
143
|
state: ProfileStartupState | null;
|
|
169
|
-
notices: string[] | null;
|
|
170
144
|
/** Null when the result carried no `problems` array at all. */
|
|
171
145
|
problems: string[] | null;
|
|
172
146
|
outcome:
|
|
173
147
|
| { kind: "ask"; needState: ProfileStartupState }
|
|
174
|
-
| { kind: "updated"
|
|
148
|
+
| { kind: "updated" }
|
|
175
149
|
| { kind: "failed"; title: string; message: string }
|
|
176
150
|
| null;
|
|
177
151
|
error: string | null;
|
|
@@ -31,10 +31,7 @@ import {
|
|
|
31
31
|
prepareProfileState,
|
|
32
32
|
} from "../services/profile-state.js";
|
|
33
33
|
import type { ProfileManifest } from "../types/index.js";
|
|
34
|
-
import {
|
|
35
|
-
buildGitignoreModal,
|
|
36
|
-
startupFixNotices,
|
|
37
|
-
} from "../ui/hooks/useGitignoreModal.js";
|
|
34
|
+
import { buildGitignoreModal } from "../ui/hooks/useGitignoreModal.js";
|
|
38
35
|
|
|
39
36
|
const MANIFEST: ProfileManifest = {
|
|
40
37
|
version: 2,
|
|
@@ -155,11 +152,6 @@ describe("the startup fix untracks generated files in place", () => {
|
|
|
155
152
|
expect(await fs.readFile(join(project, ".gitignore"), "utf8")).toContain(
|
|
156
153
|
".claude/settings.json",
|
|
157
154
|
);
|
|
158
|
-
// Untracking is unfinished until the user commits, so it is a warning.
|
|
159
|
-
expect(startupFixNotices(result)).toContainEqual({
|
|
160
|
-
text: "Stopped tracking .claude/settings.json — commit the staged removal",
|
|
161
|
-
tone: "warning",
|
|
162
|
-
});
|
|
163
155
|
// Asked again on the next open: nothing generated is tracked any more.
|
|
164
156
|
expect((await startupGitignoreNeed(project))?.generated ?? []).toEqual([]);
|
|
165
157
|
});
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
InstalledPluginsRegistry,
|
|
9
9
|
ResolvedClosure,
|
|
10
10
|
} from "../types/index.js";
|
|
11
|
+
import { resolved } from "./fixtures/bins.js";
|
|
11
12
|
|
|
12
13
|
function closure(overrides: Partial<ResolvedClosure> = {}): ResolvedClosure {
|
|
13
14
|
return {
|
|
@@ -91,7 +92,7 @@ describe("summarizeClosure", () => {
|
|
|
91
92
|
marketplaces: { magus: { source: "github", repo: "MadAppGang/magus" } },
|
|
92
93
|
plugins: { "dev@magus": "latest", "terminal@magus": "4.1.0" },
|
|
93
94
|
bins: [
|
|
94
|
-
{ name: "claudish", via: "bun", version: "1.2.0",
|
|
95
|
+
resolved({ name: "claudish", via: "bun", version: "1.2.0" }, ["x"]),
|
|
95
96
|
],
|
|
96
97
|
env: { required: ["FIGMA_TOKEN"], optional: [] },
|
|
97
98
|
});
|
|
@@ -101,4 +102,17 @@ describe("summarizeClosure", () => {
|
|
|
101
102
|
expect(lines.some((l) => l.includes("claudish@1.2.0"))).toBe(true);
|
|
102
103
|
expect(lines.some((l) => l.startsWith("env"))).toBe(true);
|
|
103
104
|
});
|
|
105
|
+
|
|
106
|
+
test("a refused pin is shown with the tool, marked as not applied", () => {
|
|
107
|
+
const bin = {
|
|
108
|
+
...resolved({ name: "claudish", via: "brew", formula: "claudish" }, [
|
|
109
|
+
"profile:p.cliTools",
|
|
110
|
+
]),
|
|
111
|
+
refusedPins: ["6.1.0"],
|
|
112
|
+
};
|
|
113
|
+
const lines = summarizeClosure(closure({ bins: [bin] }));
|
|
114
|
+
expect(
|
|
115
|
+
lines.some((l) => l.includes("claudish@6.1.0 (pin not applied)")),
|
|
116
|
+
).toBe(true);
|
|
117
|
+
});
|
|
104
118
|
});
|
|
@@ -57,7 +57,13 @@ function writePluginJson(dir: string, version: string, pin: string) {
|
|
|
57
57
|
fs.mkdirSync(p, { recursive: true });
|
|
58
58
|
fs.writeFileSync(
|
|
59
59
|
path.join(p, "plugin.json"),
|
|
60
|
-
JSON.stringify({
|
|
60
|
+
JSON.stringify({
|
|
61
|
+
version,
|
|
62
|
+
requires: {
|
|
63
|
+
schema: 1,
|
|
64
|
+
bin: [{ name: "x", install: [{ via: "release", tag: pin }] }],
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
61
67
|
);
|
|
62
68
|
}
|
|
63
69
|
function readPin(dir: string): { version: string; pin: string } {
|
|
@@ -67,7 +73,7 @@ function readPin(dir: string): { version: string; pin: string } {
|
|
|
67
73
|
"utf8",
|
|
68
74
|
),
|
|
69
75
|
);
|
|
70
|
-
return { version: j.version, pin: j.
|
|
76
|
+
return { version: j.version, pin: j.requires.bin[0].install[0].tag };
|
|
71
77
|
}
|
|
72
78
|
/** Clone origin into the marketplace dir for `name`, at origin's current HEAD. */
|
|
73
79
|
function installClone(name: string): string {
|
|
@@ -111,7 +117,7 @@ afterEach(() => {
|
|
|
111
117
|
});
|
|
112
118
|
|
|
113
119
|
describe("refreshRegisteredMarketplaces — the fix (CLAIM #1)", () => {
|
|
114
|
-
it("fast-forwards a stale clone so a newly pinned
|
|
120
|
+
it("fast-forwards a stale clone so a newly pinned release tag becomes visible", async () => {
|
|
115
121
|
const dir = installClone("magus");
|
|
116
122
|
expect(readPin(dir).pin).toBe("x@latest"); // stale on-disk clone
|
|
117
123
|
publishV2(); // a release lands upstream
|
|
@@ -51,10 +51,9 @@ const roundTrip: AppAction[] = [
|
|
|
51
51
|
];
|
|
52
52
|
|
|
53
53
|
describe("models screen initial state", () => {
|
|
54
|
-
test("starts idle, unfiltered, and
|
|
54
|
+
test("starts idle, unfiltered, and not writing", () => {
|
|
55
55
|
expect(initialState.models.data.status).toBe("idle");
|
|
56
56
|
expect(initialState.models.searchQuery).toBe("");
|
|
57
|
-
expect(initialState.models.status).toBeNull();
|
|
58
57
|
expect(initialState.models.isApplying).toBe(false);
|
|
59
58
|
// The mate slots are not drawn until something says they can be served. Starting
|
|
60
59
|
// true would put three rows on screen before anything had been asked.
|
|
@@ -166,39 +165,6 @@ describe("models data state", () => {
|
|
|
166
165
|
});
|
|
167
166
|
});
|
|
168
167
|
|
|
169
|
-
describe("models status line", () => {
|
|
170
|
-
test("survives the screen unmounting, because it lives in app state", () => {
|
|
171
|
-
// `Router` swaps the component type on a tab change, so a message held in
|
|
172
|
-
// the screen's own useState is destroyed by 0 → 1 → 0 — and the message is
|
|
173
|
-
// the only record of what the last apply did.
|
|
174
|
-
const messaged = run([
|
|
175
|
-
{
|
|
176
|
-
type: "MODELS_STATUS_SET",
|
|
177
|
-
status: { text: "Routed opus-lead", tone: "success" },
|
|
178
|
-
},
|
|
179
|
-
]);
|
|
180
|
-
expect(run(roundTrip, messaged).models.status?.text).toBe(
|
|
181
|
-
"Routed opus-lead",
|
|
182
|
-
);
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("is replaced by the next message, not appended to", () => {
|
|
186
|
-
const state = run([
|
|
187
|
-
{ type: "MODELS_STATUS_SET", status: { text: "one", tone: "success" } },
|
|
188
|
-
{ type: "MODELS_STATUS_SET", status: { text: "two", tone: "error" } },
|
|
189
|
-
]);
|
|
190
|
-
expect(state.models.status).toEqual({ text: "two", tone: "error" });
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
test("clearing removes it", () => {
|
|
194
|
-
const state = run([
|
|
195
|
-
{ type: "MODELS_STATUS_SET", status: { text: "one", tone: "success" } },
|
|
196
|
-
{ type: "MODELS_STATUS_CLEAR" },
|
|
197
|
-
]);
|
|
198
|
-
expect(state.models.status).toBeNull();
|
|
199
|
-
});
|
|
200
|
-
});
|
|
201
|
-
|
|
202
168
|
describe("apply-in-flight state", () => {
|
|
203
169
|
test("survives the screen unmounting", () => {
|
|
204
170
|
// An apply rewrites the profile, re-materializes it and registers a hook.
|
|
@@ -297,11 +263,10 @@ describe("ModelsScreen keybindings", () => {
|
|
|
297
263
|
expect(src.slice(apply, apply + 120)).toContain("handleApply()");
|
|
298
264
|
});
|
|
299
265
|
|
|
300
|
-
test("digits and Tab never reach the screen
|
|
301
|
-
//
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
// on the way OUT of the tab — the loss the state was moved to prevent.
|
|
266
|
+
test("digits and Tab never reach the screen", () => {
|
|
267
|
+
// Every digit (0 included) and Tab navigate, and they belong to the global
|
|
268
|
+
// key layer: a Models binding claiming one would make the tab unleavable
|
|
269
|
+
// by that key.
|
|
305
270
|
for (const digit of "1234567890") {
|
|
306
271
|
expect(
|
|
307
272
|
matchBinding(SCREEN_BINDINGS.models, press(digit)),
|
|
@@ -319,6 +284,19 @@ describe("ModelsScreen keybindings", () => {
|
|
|
319
284
|
expect(styles).not.toContain("/^[1-9]$/");
|
|
320
285
|
});
|
|
321
286
|
|
|
287
|
+
test("no status line: a failure goes to the failure popup, a success says nothing", async () => {
|
|
288
|
+
// The TUI shows no status lines, notices or success messages; the screen
|
|
289
|
+
// reloads and shows state. The header row carries state only.
|
|
290
|
+
const src = stripComments(await fs.readFile(SCREEN, "utf8"));
|
|
291
|
+
expect(src).not.toContain("notice=");
|
|
292
|
+
expect(src).not.toContain("MODELS_STATUS");
|
|
293
|
+
expect(src).not.toMatch(/\bApplied\b/);
|
|
294
|
+
// Negative control: the failure path is really wired, so the guard is
|
|
295
|
+
// not passing over a screen that reports nothing at all.
|
|
296
|
+
expect(src).toContain("useFailurePopup()");
|
|
297
|
+
expect(src).toContain("stepFailureReport(");
|
|
298
|
+
});
|
|
299
|
+
|
|
322
300
|
test("screen state is held in the reducer, never in useState", async () => {
|
|
323
301
|
// The rule the two "survives unmounting" tests above enforce at runtime,
|
|
324
302
|
// pinned at the source so a new piece of state cannot quietly opt out.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin-dependency code paths never hand anything to a shell.
|
|
3
|
+
*
|
|
4
|
+
* Scoped, deliberately, to the files on the installer path: the one reader of
|
|
5
|
+
* `requires` (and the hook that runs it at session start), the executor, the
|
|
6
|
+
* runner, and the modules that feed them. Package names, check argv and script
|
|
7
|
+
* paths reach these from plugin manifests and profiles — data a marketplace or
|
|
8
|
+
* a teammate wrote — so here a shell is never acceptable. The test makes no
|
|
9
|
+
* claim about the rest of `src/`. A new file on the installer path joins
|
|
10
|
+
* {@link INSTALLER_PATH}.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, test } from "bun:test";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import fs from "fs-extra";
|
|
16
|
+
|
|
17
|
+
const SRC_DIR = path.join(import.meta.dir, "..");
|
|
18
|
+
|
|
19
|
+
/** Files on the installer path, relative to `src/`. */
|
|
20
|
+
const INSTALLER_PATH = [
|
|
21
|
+
"shared/plugin/hooks/lib/requires-probe.ts",
|
|
22
|
+
"shared/plugin/hooks/deps-check.ts",
|
|
23
|
+
"services/deps-plan.ts",
|
|
24
|
+
"services/deps-report.ts",
|
|
25
|
+
"services/doctor-bins.ts",
|
|
26
|
+
"utils/run.ts",
|
|
27
|
+
"cli/deps-view.ts",
|
|
28
|
+
"ui/adapters/depsAdapter.ts",
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/** Each way a string can reach a shell, as the line that would do it. */
|
|
32
|
+
const SHELL_PATTERNS: Array<[string, RegExp]> = [
|
|
33
|
+
["a shell spawn option", /\bshell:\s*(true|["'`])/],
|
|
34
|
+
["exec / execSync (string commands)", /\bexec(Sync)?\s*\(/],
|
|
35
|
+
[
|
|
36
|
+
"a string handed to spawn",
|
|
37
|
+
/(?<!Bun\.)\bspawn(Sync)?\s*\(\s*["'`][^"'`]*\s/,
|
|
38
|
+
],
|
|
39
|
+
["a string handed to Bun.spawn", /\bBun\.spawn(Sync)?\s*\(\s*["'`]/],
|
|
40
|
+
["Bun's shell", /\$`|from\s+["']bun["'].*\$/],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const isComment = (line: string) => {
|
|
44
|
+
const t = line.trim();
|
|
45
|
+
return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function shellUses(text: string): string[] {
|
|
49
|
+
const found: string[] = [];
|
|
50
|
+
text.split("\n").forEach((line, i) => {
|
|
51
|
+
if (isComment(line)) return;
|
|
52
|
+
for (const [what, re] of SHELL_PATTERNS) {
|
|
53
|
+
if (re.test(line)) found.push(`line ${i + 1}: ${what}`);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return found;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe("the installer path never spawns a shell", () => {
|
|
60
|
+
for (const rel of INSTALLER_PATH) {
|
|
61
|
+
test(rel, async () => {
|
|
62
|
+
const text = await fs.readFile(path.join(SRC_DIR, rel), "utf8");
|
|
63
|
+
expect(shellUses(text)).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
test("the scan can fail", () => {
|
|
68
|
+
// Negative controls: each pattern fires on the line it exists for.
|
|
69
|
+
expect(shellUses("spawn(cmd, { shell: true });")).toHaveLength(1);
|
|
70
|
+
expect(shellUses(`spawn(cmd, { shell: "/bin/sh" });`)).toHaveLength(1);
|
|
71
|
+
expect(shellUses("execSync(`brew install ${f}`);")).toHaveLength(1);
|
|
72
|
+
expect(shellUses(`spawn("brew install tmux");`)).toHaveLength(1);
|
|
73
|
+
expect(shellUses(`Bun.spawn("brew install tmux");`)).toHaveLength(1);
|
|
74
|
+
// …and not on argv.
|
|
75
|
+
expect(shellUses(`spawn(exe, args, { stdio: "ignore" });`)).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -199,7 +199,7 @@ describe("the CLI Tools entry for magus", () => {
|
|
|
199
199
|
test("install targets the package", () => {
|
|
200
200
|
expect(cliToolInstall(magusEntry())).toEqual({
|
|
201
201
|
cmd: "bun",
|
|
202
|
-
args: ["
|
|
202
|
+
args: ["add", "-g", PACKAGE],
|
|
203
203
|
});
|
|
204
204
|
});
|
|
205
205
|
|
|
@@ -251,10 +251,8 @@ describe("pins and probes resolve to the package", () => {
|
|
|
251
251
|
test("a pinned version installs `<package>@<version>` and checks the command", () => {
|
|
252
252
|
expect(cliToolToBin(COMMAND, PKG.version)).toMatchObject({
|
|
253
253
|
name: COMMAND,
|
|
254
|
-
|
|
255
|
-
package: PACKAGE,
|
|
256
|
-
check: "magus --version",
|
|
257
|
-
version: PKG.version,
|
|
254
|
+
check: ["magus", "--version"],
|
|
255
|
+
chosen: { via: "bun", package: PACKAGE, version: PKG.version },
|
|
258
256
|
});
|
|
259
257
|
});
|
|
260
258
|
|
|
@@ -262,11 +260,10 @@ describe("pins and probes resolve to the package", () => {
|
|
|
262
260
|
const bin = cliToolToBin(COMMAND, "latest");
|
|
263
261
|
expect(bin).toMatchObject({
|
|
264
262
|
name: COMMAND,
|
|
265
|
-
|
|
266
|
-
package: PACKAGE,
|
|
267
|
-
check: "magus --version",
|
|
263
|
+
check: ["magus", "--version"],
|
|
264
|
+
chosen: { via: "bun", package: PACKAGE },
|
|
268
265
|
});
|
|
269
|
-
expect(bin.version).
|
|
266
|
+
expect(bin.chosen && "version" in bin.chosen).toBe(false);
|
|
270
267
|
});
|
|
271
268
|
|
|
272
269
|
test("the catalogue spec (the dist-tags probe input) names the package", () => {
|