magus-cli 7.7.0 → 7.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +4 -4
- package/src/__tests__/blackbox-parity.test.ts +45 -26
- package/src/__tests__/blackbox-plugin-state.test.ts +25 -6
- package/src/__tests__/blind/plugin-deps/doctor.test.ts +622 -0
- package/src/__tests__/blind/plugin-deps/helpers.ts +340 -0
- package/src/__tests__/blind/plugin-deps/hook.test.ts +328 -0
- package/src/__tests__/blind/plugin-deps/probe.test.ts +694 -0
- package/src/__tests__/blind/plugin-deps/read-only.test.ts +96 -0
- package/src/__tests__/blind/plugin-deps/schema.test.ts +390 -0
- package/src/__tests__/blind-780-cli-tools.test.ts +796 -0
- package/src/__tests__/blind-780-deprecated.test.ts +631 -0
- package/src/__tests__/blind-780-failure-reasons.test.ts +1004 -0
- package/src/__tests__/blind-780-plugin-resolution.test.ts +431 -0
- package/src/__tests__/blind-780-review2.test.ts +1178 -0
- package/src/__tests__/cli-apply-seams.test.ts +2 -67
- package/src/__tests__/cli-tool-commands.test.ts +131 -6
- package/src/__tests__/cli-tools-adapter.test.ts +52 -2
- package/src/__tests__/cli-tools-plugin-managed.test.tsx +83 -0
- package/src/__tests__/cli-update-view.test.ts +3 -3
- package/src/__tests__/deprecated-remove.test.ts +827 -0
- package/src/__tests__/deps-adapter.test.ts +191 -0
- package/src/__tests__/deps-check-hook.test.ts +220 -0
- package/src/__tests__/deps-plan.test.ts +896 -0
- package/src/__tests__/deps-report.test.ts +197 -0
- package/src/__tests__/doctor-bins.test.ts +134 -36
- package/src/__tests__/doctor-json.test.ts +261 -0
- package/src/__tests__/doctor.test.ts +159 -1
- package/src/__tests__/failure-popup-review2.test.tsx +896 -0
- package/src/__tests__/failure-popup.test.tsx +419 -0
- package/src/__tests__/final-fixes-780.test.tsx +740 -0
- package/src/__tests__/fixtures/bins.ts +55 -0
- package/src/__tests__/format-update-outcome.test.ts +6 -32
- package/src/__tests__/gitignore-profile.test.ts +1 -9
- package/src/__tests__/install-plan.test.ts +15 -1
- package/src/__tests__/marketplace-refresh.test.ts +9 -3
- package/src/__tests__/models-screen-state.test.ts +18 -40
- package/src/__tests__/no-shell-spawn.test.ts +77 -0
- package/src/__tests__/npm-package-name.test.ts +6 -9
- package/src/__tests__/plugin-requires.test.ts +138 -153
- package/src/__tests__/plugin-resolution.test.ts +52 -13
- package/src/__tests__/plugin-row-text.test.ts +30 -37
- package/src/__tests__/profile-dependencies.test.ts +7 -0
- package/src/__tests__/profile-startup-old-format.test.ts +0 -3
- package/src/__tests__/profile-startup.test.ts +40 -12
- package/src/__tests__/requires-probe.test.ts +858 -0
- package/src/__tests__/resolver.test.ts +129 -52
- package/src/__tests__/run.test.ts +108 -0
- package/src/__tests__/stale-load-repair.test.ts +31 -23
- package/src/__tests__/tool-probe.test.ts +1 -1
- package/src/__tests__/toolchain.test.ts +123 -52
- package/src/__tests__/update-apply.test.ts +13 -29
- package/src/__tests__/update-plan.test.ts +24 -18
- package/src/__tests__/useStaleLoadModal.test.ts +1 -57
- package/src/__tests__/version-unknown-state.test.ts +15 -17
- package/src/cli/ansi.ts +2 -7
- package/src/cli/deps-view.ts +213 -0
- package/src/cli/doctor.ts +320 -54
- package/src/cli/install.ts +55 -71
- package/src/cli/profile.ts +20 -2
- package/src/cli/router.ts +6 -2
- package/src/cli/update-view.ts +58 -15
- package/src/cli/update.ts +55 -12
- package/src/data/cli-tools.ts +24 -9
- package/src/services/active-closure.ts +3 -3
- package/src/services/catalog-cache-store.ts +8 -6
- package/src/services/claude-cli.ts +106 -19
- package/src/services/claude-settings.ts +88 -384
- package/src/services/cli-tool-commands.ts +104 -3
- package/src/services/deps-plan.ts +962 -0
- package/src/services/deps-report.ts +265 -0
- package/src/services/doctor-bins.ts +115 -43
- package/src/services/install-plan.ts +11 -1
- package/src/services/local-marketplace.ts +12 -206
- package/src/services/marketplace-refresh.ts +4 -4
- package/src/services/plugin-env-needs.ts +69 -0
- package/src/services/plugin-manager.ts +35 -37
- package/src/services/plugin-requires.ts +93 -162
- package/src/services/plugin-resolution.ts +34 -42
- package/src/services/profile-activate.ts +1 -2
- package/src/services/profile-startup.ts +18 -43
- package/src/services/profile-state.ts +72 -2
- package/src/services/resolver.ts +263 -59
- package/src/services/tool-probe.ts +18 -25
- package/src/services/toolchain.ts +117 -111
- package/src/services/update-engine.ts +345 -45
- package/src/services/update-plan.ts +296 -36
- package/src/shared/plugin/hooks/bun-missing.sh +39 -0
- package/src/shared/plugin/hooks/deps-check.ts +224 -0
- package/src/shared/plugin/hooks/lib/requires-probe.ts +1183 -0
- package/src/types/index.ts +60 -16
- package/src/ui/App.tsx +81 -171
- package/src/ui/adapters/cliToolsAdapter.ts +252 -12
- package/src/ui/adapters/depsAdapter.ts +129 -0
- package/src/ui/adapters/failureReport.ts +137 -0
- package/src/ui/adapters/pluginsAdapter.ts +297 -45
- package/src/ui/adapters/startupReports.ts +260 -0
- package/src/ui/components/modals/FailureListModal.tsx +231 -0
- package/src/ui/components/modals/MessageModal.tsx +7 -12
- package/src/ui/components/modals/ModalContainer.tsx +10 -5
- package/src/ui/components/modals/index.ts +1 -0
- package/src/ui/hooks/startupQueue.ts +56 -0
- package/src/ui/hooks/useGitignoreModal.ts +30 -52
- package/src/ui/hooks/useProfileStartupModal.ts +119 -142
- package/src/ui/hooks/useStaleLoadModal.ts +35 -65
- package/src/ui/keymap.ts +14 -1
- package/src/ui/renderers/cliToolRenderers.tsx +135 -2
- package/src/ui/renderers/mcpRenderers.tsx +110 -3
- package/src/ui/renderers/pluginRenderers.tsx +77 -6
- package/src/ui/renderers/pluginRowText.ts +25 -25
- package/src/ui/renderers/pluginStatusText.ts +18 -1
- package/src/ui/screens/AliasScreen.tsx +38 -45
- package/src/ui/screens/CliToolsScreen.tsx +199 -98
- package/src/ui/screens/EnvVarsScreen.tsx +17 -5
- package/src/ui/screens/GitignoreScreen.tsx +48 -67
- package/src/ui/screens/McpRegistryScreen.tsx +67 -38
- package/src/ui/screens/McpScreen.tsx +46 -19
- package/src/ui/screens/ModelsScreen.tsx +46 -86
- package/src/ui/screens/PluginsScreen.tsx +315 -259
- package/src/ui/screens/ProfilesScreen.tsx +79 -40
- package/src/ui/screens/SkillsScreen.tsx +83 -80
- package/src/ui/screens/StylesScreen.tsx +211 -275
- package/src/ui/state/AppContext.tsx +66 -44
- package/src/ui/state/DimensionsContext.tsx +3 -22
- package/src/ui/state/reducer.ts +12 -100
- package/src/ui/state/types.ts +48 -69
- package/src/utils/command-utils.ts +4 -5
- package/src/utils/run.ts +123 -52
- package/src/utils/string-utils.ts +9 -0
- package/src/utils/user-path.ts +60 -0
- package/src/__tests__/go-module.test.ts +0 -43
- package/src/__tests__/notices-rules.test.ts +0 -476
- package/src/__tests__/notices.test.tsx +0 -604
- package/src/__tests__/plugin-setup.test.ts +0 -1016
- package/src/__tests__/shell-script-callers.test.ts +0 -92
- package/src/__tests__/styles-status-line.test.ts +0 -72
- package/src/__tests__/version-snapshot.test.ts +0 -212
- package/src/services/plugin-setup.ts +0 -965
- package/src/services/version-snapshot.ts +0 -189
- package/src/utils/go-module.ts +0 -36
package/src/utils/run.ts
CHANGED
|
@@ -1,67 +1,138 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The one way magus runs an installer or any other child process it waits on.
|
|
3
3
|
*
|
|
4
|
-
* Lives in `utils/` rather than `cli
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* one of them is how the two front ends ended up with different update logic in
|
|
8
|
-
* the first place.
|
|
4
|
+
* Lives in `utils/` rather than `cli/` because services need it and a service
|
|
5
|
+
* must not import from a front end — `cli/` and `ui/` are sibling front ends
|
|
6
|
+
* over `services/`.
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* Contract:
|
|
9
|
+
*
|
|
10
|
+
* - **argv only.** No `shell` option: `cmd` is resolved on PATH as an
|
|
11
|
+
* executable, and every argument stays exactly one argument. Package names
|
|
12
|
+
* reach here from `.claude/profiles.json` and plugin `requires` — committed
|
|
13
|
+
* by teammates, fetched from marketplaces — so under a shell a space or a
|
|
14
|
+
* `$(…)` in one of them would execute.
|
|
15
|
+
* - **Never writes to the terminal.** stdin is ignored and stdout/stderr are
|
|
16
|
+
* piped and captured; what (if anything) is shown, and where, is the
|
|
17
|
+
* caller's decision through `onOutput`. The guarantee is exactly this: a
|
|
18
|
+
* child cannot read the terminal through its stdin. A child that opens
|
|
19
|
+
* `/dev/tty` itself still can; it keeps magus's controlling terminal and
|
|
20
|
+
* session on purpose, so a `sudo -v` ticket stays usable by `sudo -n`.
|
|
21
|
+
* - **Bounded.** At `timeoutMs` the child gets SIGTERM, then SIGKILL after
|
|
22
|
+
* `killGraceMs` (5 s by default; a `sudo` step passes more, because SIGKILL
|
|
23
|
+
* to `sudo` cannot be relayed and would orphan its root child).
|
|
13
24
|
*/
|
|
14
25
|
|
|
15
26
|
import { spawn } from "node:child_process";
|
|
16
27
|
import type { Command } from "./command-utils.js";
|
|
28
|
+
import { stripAnsi } from "./string-utils.js";
|
|
17
29
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
/** How much combined output a {@link RunResult} keeps: the last 16 KiB. */
|
|
31
|
+
export const OUTPUT_TAIL_BYTES = 16 * 1024;
|
|
32
|
+
|
|
33
|
+
const DEFAULT_KILL_GRACE_MS = 5_000;
|
|
34
|
+
|
|
35
|
+
export interface RunResult {
|
|
36
|
+
/** null when killed by a signal or never spawned. */
|
|
37
|
+
exitCode: number | null;
|
|
38
|
+
signal: string | null;
|
|
39
|
+
timedOut: boolean;
|
|
40
|
+
/** Set when the process could not be started (ENOENT, EACCES, …). */
|
|
41
|
+
spawnError?: string;
|
|
42
|
+
/** Combined stdout + stderr, last {@link OUTPUT_TAIL_BYTES}, ANSI stripped. */
|
|
43
|
+
outputTail: string;
|
|
44
|
+
durationMs: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RunOptions {
|
|
48
|
+
timeoutMs: number;
|
|
49
|
+
/** Replaces the child's environment when given. */
|
|
50
|
+
env?: Record<string, string | undefined>;
|
|
51
|
+
cwd?: string;
|
|
52
|
+
/** Between SIGTERM and SIGKILL once `timeoutMs` passes. */
|
|
53
|
+
killGraceMs?: number;
|
|
54
|
+
/** Each output chunk as it arrives, ANSI intact. Never called after return. */
|
|
55
|
+
onOutput?: (chunk: string) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Keep the last {@link OUTPUT_TAIL_BYTES} of a growing buffer. */
|
|
59
|
+
function tail(text: string): string {
|
|
60
|
+
const bytes = Buffer.from(text, "utf8");
|
|
61
|
+
if (bytes.length <= OUTPUT_TAIL_BYTES) return text;
|
|
62
|
+
return bytes.subarray(bytes.length - OUTPUT_TAIL_BYTES).toString("utf8");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Run `c` as argv and capture it; resolves when it exits or is killed. */
|
|
66
|
+
export function runArgv(c: Command, opts: RunOptions): Promise<RunResult> {
|
|
67
|
+
const started = Date.now();
|
|
38
68
|
return new Promise((resolve) => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
69
|
+
let output = "";
|
|
70
|
+
let timedOut = false;
|
|
71
|
+
let settled = false;
|
|
72
|
+
let killTimer: ReturnType<typeof setTimeout> | undefined;
|
|
73
|
+
const finish = (result: Omit<RunResult, "outputTail" | "durationMs">) => {
|
|
74
|
+
if (settled) return;
|
|
75
|
+
settled = true;
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
if (killTimer) clearTimeout(killTimer);
|
|
78
|
+
resolve({
|
|
79
|
+
...result,
|
|
80
|
+
outputTail: stripAnsi(tail(output)),
|
|
81
|
+
durationMs: Date.now() - started,
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
const child = spawn(c.cmd, [...c.args], {
|
|
85
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
86
|
+
...(opts.env ? { env: opts.env } : {}),
|
|
87
|
+
...(opts.cwd ? { cwd: opts.cwd } : {}),
|
|
88
|
+
});
|
|
89
|
+
const onData = (data: Buffer) => {
|
|
90
|
+
const chunk = data.toString("utf8");
|
|
91
|
+
output = tail(output + chunk);
|
|
92
|
+
if (!settled) opts.onOutput?.(chunk);
|
|
93
|
+
};
|
|
94
|
+
child.stdout?.on("data", onData);
|
|
95
|
+
child.stderr?.on("data", onData);
|
|
96
|
+
const timer = setTimeout(() => {
|
|
97
|
+
timedOut = true;
|
|
98
|
+
child.kill("SIGTERM");
|
|
99
|
+
killTimer = setTimeout(
|
|
100
|
+
() => child.kill("SIGKILL"),
|
|
101
|
+
opts.killGraceMs ?? DEFAULT_KILL_GRACE_MS,
|
|
102
|
+
);
|
|
103
|
+
}, opts.timeoutMs);
|
|
104
|
+
child.on("error", (error) =>
|
|
105
|
+
finish({
|
|
106
|
+
exitCode: null,
|
|
107
|
+
signal: null,
|
|
108
|
+
timedOut,
|
|
109
|
+
spawnError: error.message,
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
child.on("close", (code, signal) =>
|
|
113
|
+
finish({ exitCode: code, signal, timedOut }),
|
|
114
|
+
);
|
|
42
115
|
});
|
|
43
116
|
}
|
|
44
117
|
|
|
118
|
+
/** True when the run exited 0 (and was neither killed nor unstartable). */
|
|
119
|
+
export function succeeded(r: RunResult): boolean {
|
|
120
|
+
return r.exitCode === 0 && !r.timedOut && r.spawnError === undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
45
123
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* **Not for installer commands — use {@link runCommand}.** This exists for
|
|
49
|
-
* exactly one input: the values of `TOOLCHAIN_BOOTSTRAP`, which are the
|
|
50
|
-
* upstream-published one-liners for bun and Homebrew. Those genuinely are shell
|
|
51
|
-
* text — one contains a pipe, the other a command substitution — so they cannot
|
|
52
|
-
* be expressed as argv without wrapping them in a fake `/bin/sh -c` at the one
|
|
53
|
-
* place where a shell is actually correct.
|
|
54
|
-
*
|
|
55
|
-
* The distinction is who wrote the string. A bootstrap line is a constant in
|
|
56
|
-
* this repo, copied from bun.sh and brew.sh. An installer command is assembled
|
|
57
|
-
* from names that arrive with a profile. Passing the second kind here is the
|
|
58
|
-
* defect that motivated splitting the two, so
|
|
59
|
-
* `src/__tests__/shell-script-callers.test.ts` pins the caller set to one file.
|
|
124
|
+
* One line saying why a run did not succeed, for a failure report. The output
|
|
125
|
+
* tail's last non-empty line is the installer's own reason more often than not.
|
|
60
126
|
*/
|
|
61
|
-
export
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
127
|
+
export function runFailureReason(r: RunResult): string {
|
|
128
|
+
if (r.spawnError) return r.spawnError;
|
|
129
|
+
if (r.timedOut) return `timed out after ${Math.round(r.durationMs / 1000)}s`;
|
|
130
|
+
const last = r.outputTail
|
|
131
|
+
.split("\n")
|
|
132
|
+
.map((l) => l.trim())
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.pop();
|
|
135
|
+
const code =
|
|
136
|
+
r.exitCode !== null ? `exit ${r.exitCode}` : `killed by ${r.signal}`;
|
|
137
|
+
return last ? `${code}: ${last}` : code;
|
|
67
138
|
}
|
|
@@ -86,3 +86,12 @@ export function parsePluginId(pluginId: string): ParsedPluginId | null {
|
|
|
86
86
|
raw: trimmed,
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
/** Every CSI sequence, not only SGR — the live region emits cursor moves too. */
|
|
91
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ESC is the thing being matched.
|
|
92
|
+
export const CSI = /\x1b\[[0-9;?]*[\x20-\x2f]*[\x40-\x7e]/g;
|
|
93
|
+
|
|
94
|
+
/** `text` without its ANSI CSI sequences. */
|
|
95
|
+
export function stripAnsi(text: string): string {
|
|
96
|
+
return text.replace(CSI, "");
|
|
97
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A file as the user names it, never as an absolute path.
|
|
3
|
+
*
|
|
4
|
+
* magus names a file only when the user must act on it (fix it by hand,
|
|
5
|
+
* commit it), and then in the words they would type: `~/.claude/settings.json`
|
|
6
|
+
* for their own settings, `.claude/settings.json` for this project's. The
|
|
7
|
+
* absolute path is where this machine keeps it — the mechanism, and noise
|
|
8
|
+
* that pushes the actual problem off the end of the line.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
/** `true` when `file` is `dir` itself or inside it. */
|
|
15
|
+
function isInside(file: string, dir: string): boolean {
|
|
16
|
+
const rel = path.relative(dir, file);
|
|
17
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `file` in the user's terms:
|
|
22
|
+
*
|
|
23
|
+
* 1. Under the user's own `~/.claude/` → `~/.claude/…` (their USER settings),
|
|
24
|
+
* checked first so a project opened at the home folder still names it so.
|
|
25
|
+
* 2. Under the project folder → relative to it: `.claude/settings.json`.
|
|
26
|
+
* 3. Anywhere else with a `.claude/` folder in its path → from that folder
|
|
27
|
+
* on, which is how the user knows a project's settings file.
|
|
28
|
+
* 4. Under the home folder → `~/…`.
|
|
29
|
+
* 5. Otherwise its name alone.
|
|
30
|
+
*/
|
|
31
|
+
export function userFacingPath(
|
|
32
|
+
file: string,
|
|
33
|
+
projectDir: string = process.cwd(),
|
|
34
|
+
home: string = os.homedir(),
|
|
35
|
+
): string {
|
|
36
|
+
const abs = path.resolve(file);
|
|
37
|
+
const userClaude = home ? path.join(home, ".claude") : "";
|
|
38
|
+
if (userClaude && isInside(abs, userClaude)) {
|
|
39
|
+
return `~/${path.relative(home, abs)}`;
|
|
40
|
+
}
|
|
41
|
+
if (projectDir && isInside(abs, path.resolve(projectDir))) {
|
|
42
|
+
const rel = path.relative(path.resolve(projectDir), abs);
|
|
43
|
+
if (rel) return rel;
|
|
44
|
+
}
|
|
45
|
+
const marker = `${path.sep}.claude${path.sep}`;
|
|
46
|
+
const at = abs.lastIndexOf(marker);
|
|
47
|
+
if (at >= 0) return abs.slice(at + 1);
|
|
48
|
+
if (home && isInside(abs, home)) return `~/${path.relative(home, abs)}`;
|
|
49
|
+
return path.basename(abs);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Every occurrence of `file`'s absolute path in `text`, in the user's terms. */
|
|
53
|
+
export function withUserFacingPath(
|
|
54
|
+
text: string,
|
|
55
|
+
file: string,
|
|
56
|
+
projectDir?: string,
|
|
57
|
+
home?: string,
|
|
58
|
+
): string {
|
|
59
|
+
return text.split(file).join(userFacingPath(file, projectDir, home));
|
|
60
|
+
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* goBinaryName: the one rule for turning a Go module path into the binary
|
|
3
|
-
* `go install` produces. Both consumers (plugin-setup's Go path and
|
|
4
|
-
* plugin-requires' `setup.go` branch) are tested through their own surfaces;
|
|
5
|
-
* this file pins the rule itself.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, expect, test } from "bun:test";
|
|
9
|
-
import { goBinaryName } from "../utils/go-module.js";
|
|
10
|
-
|
|
11
|
-
describe("goBinaryName", () => {
|
|
12
|
-
test("takes the last segment of a v0/v1 module path", () => {
|
|
13
|
-
expect(goBinaryName("github.com/MadAppGang/tmux-mcp")).toBe("tmux-mcp");
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test("strips a trailing /v2 major-version suffix before taking the name", () => {
|
|
17
|
-
expect(goBinaryName("github.com/MadAppGang/tmux-mcp/v2")).toBe("tmux-mcp");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test("strips multi-digit suffixes (/v10, /v42)", () => {
|
|
21
|
-
expect(goBinaryName("github.com/x/tool/v10")).toBe("tool");
|
|
22
|
-
expect(goBinaryName("github.com/x/tool/v42")).toBe("tool");
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("a segment that merely starts with v2 is a name, not a suffix", () => {
|
|
26
|
-
expect(goBinaryName("github.com/x/v2tool")).toBe("v2tool");
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("v0 and v1 are never path suffixes, so they stay as names", () => {
|
|
30
|
-
// Nobody names a binary this, but the rule must not invent an exception.
|
|
31
|
-
expect(goBinaryName("github.com/x/v1")).toBe("v1");
|
|
32
|
-
expect(goBinaryName("github.com/x/v0")).toBe("v0");
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("a suffix in the middle of the path is not trailing and is kept", () => {
|
|
36
|
-
expect(goBinaryName("github.com/x/repo/v2/cmd/cli")).toBe("cli");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
test("single-segment and empty inputs pass through", () => {
|
|
40
|
-
expect(goBinaryName("mytool")).toBe("mytool");
|
|
41
|
-
expect(goBinaryName("")).toBe("");
|
|
42
|
-
});
|
|
43
|
-
});
|