Skip to content

Commit bf21873

Browse files
committed
refactor(cli): one command-word lookup and one rewritten command line for the folder shortcut
1 parent d674947 commit bf21873

4 files changed

Lines changed: 55 additions & 24 deletions

File tree

‎packages/cli/src/cli.lifecycle.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ describe("CLI lifecycle", () => {
151151
try {
152152
for (const [name, argv] of [
153153
["open", [dir]],
154+
["open", ["--verbose", dir]],
154155
["render", ["render"]],
155156
] as const) {
156157
vi.doMock(`./commands/${name}.js`, () => ({
@@ -163,7 +164,8 @@ describe("CLI lifecycle", () => {
163164
vi.doUnmock(`./commands/${name}.js`);
164165
vi.resetModules();
165166
}
166-
expect(run.mock.calls.map(([ctx]) => ctx.rawArgs)).toEqual([[dir], []]);
167+
expect(run.mock.calls.map(([ctx]) => ctx.rawArgs)).toEqual([[dir], [dir], []]);
168+
expect(process.argv.slice(2)).toEqual(["render"]);
167169
} finally {
168170
rmSync(dir, { recursive: true, force: true });
169171
}

‎packages/cli/src/cli.ts‎

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,7 @@ if (rootVersionRequested) {
6363
// Capture treats CWD as project data too: its early loader must use the same
6464
// allowlist/opt-out as output ancestors, before browser selection can observe it.
6565
const { applyDotEnv, CAPTURE_ENV_KEYS } = await import("./utils/dotEnv.js");
66-
// The root has no value-taking options: Citty skips flags up to the first
67-
// positional command, and stops subcommand selection at the -- separator.
68-
const separator = argv.indexOf("--");
69-
const commandArgs = separator < 0 ? argv : argv.slice(0, separator);
70-
const captureEnv = commandArgs.find((arg) => !arg.startsWith("-")) === "capture";
66+
const captureEnv = argv[commandWordIndex(argv)] === "capture";
7167
if (!captureEnv || process.env.HYPERFRAMES_NO_CAPTURE_DOTENV !== "1") {
7268
try {
7369
const { readFileSync } = await import("node:fs");
@@ -104,7 +100,7 @@ import {
104100
type CommandResult,
105101
} from "./utils/commandResult.js";
106102
import { registerRunningCli } from "./utils/runningCli.js";
107-
import { openShortcutArgv } from "./utils/projectShortcut.js";
103+
import { commandWordIndex, openShortcutArgv } from "./utils/projectShortcut.js";
108104

109105
registerRunningCli();
110106

@@ -185,11 +181,11 @@ const commandLoaders = {
185181
};
186182

187183
// `hyperframes <dir>` is `hyperframes open <dir>`; every later read of the command sees `open`.
188-
const shortcutArgv = openShortcutArgv(argv, new Set(Object.keys(commandLoaders)));
189-
if (shortcutArgv !== argv) {
190-
argv = [...shortcutArgv];
191-
process.argv.splice(2, 0, "open");
192-
}
184+
process.argv = [
185+
...process.argv.slice(0, 2),
186+
...openShortcutArgv(argv, new Set(Object.keys(commandLoaders))),
187+
];
188+
argv = process.argv.slice(2);
193189

194190
const subCommands = Object.fromEntries(
195191
Object.entries(commandLoaders).map(([name, load]) => [name, trackCommandFailures(load)]),

‎packages/cli/src/utils/projectShortcut.test.ts‎

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
22
import { tmpdir } from "node:os";
33
import { join } from "node:path";
44
import { afterAll, describe, expect, it } from "vitest";
5-
import { openShortcutArgv } from "./projectShortcut.js";
5+
import { commandWordIndex, openShortcutArgv } from "./projectShortcut.js";
66

77
const commands = new Set(["open", "render", "lint"]);
88
const cwd = mkdtempSync(join(tmpdir(), "hf-shortcut-"));
@@ -24,14 +24,37 @@ describe("openShortcutArgv", () => {
2424
]);
2525
});
2626

27+
it("finds the folder after root flags, like the command word", () => {
28+
expect(openShortcutArgv(["--verbose", "my-video", "--json"], commands, cwd)).toEqual([
29+
"--verbose",
30+
"open",
31+
"my-video",
32+
"--json",
33+
]);
34+
});
35+
36+
it("ignores a folder after the -- separator", () => {
37+
expect(openShortcutArgv(["--", "my-video"], commands, cwd)).toEqual(["--", "my-video"]);
38+
});
39+
2740
it("lets a known command win over a folder with its name", () => {
2841
expect(openShortcutArgv(["render"], commands, cwd)).toEqual(["render"]);
2942
});
3043

31-
it.each([[[]], [["--help"]], [["--json", "my-video"]], [["notes.txt"]], [["nope"]]])(
32-
"leaves %j alone",
33-
(argv) => {
34-
expect(openShortcutArgv(argv, commands, cwd)).toEqual(argv);
35-
},
36-
);
44+
it.each([[[]], [["--help"]], [["notes.txt"]], [["nope"]]])("leaves %j alone", (argv) => {
45+
expect(openShortcutArgv(argv, commands, cwd)).toEqual(argv);
46+
});
47+
});
48+
49+
describe("commandWordIndex", () => {
50+
it.each([
51+
[["render", "x"], 0],
52+
[["--verbose", "render"], 1],
53+
[["--verbose"], -1],
54+
[["--", "render"], -1],
55+
[["--verbose", "--", "render"], -1],
56+
[[], -1],
57+
] as const)("%j -> %i", (argv, index) => {
58+
expect(commandWordIndex(argv)).toBe(index);
59+
});
3760
});
Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
11
import { statSync } from "node:fs";
22
import { resolve } from "node:path";
33

4+
/**
5+
* Where the root command word sits in argv, or -1. The root has no value-taking options: Citty
6+
* skips flags up to the first positional and stops subcommand selection at the -- separator.
7+
*/
8+
export function commandWordIndex(argv: readonly string[]): number {
9+
const end = argv.indexOf("--");
10+
const index = argv.findIndex((arg) => !arg.startsWith("-"));
11+
return end >= 0 && index >= end ? -1 : index;
12+
}
13+
414
/**
515
* `hyperframes <dir>` is `hyperframes open <dir>`. A known command always wins, even over a
6-
* folder with its name; any other first word must be an existing directory to count as a project.
16+
* folder with its name; any other command word must be an existing directory to count as a project.
717
*/
818
export function openShortcutArgv(
919
argv: readonly string[],
1020
commands: ReadonlySet<string>,
1121
cwd: string = process.cwd(),
1222
): readonly string[] {
13-
const first = argv[0];
14-
if (!first || first.startsWith("-") || commands.has(first)) return argv;
23+
const at = commandWordIndex(argv);
24+
if (at < 0 || commands.has(argv[at]!)) return argv;
1525
try {
16-
if (!statSync(resolve(cwd, first)).isDirectory()) return argv;
26+
if (!statSync(resolve(cwd, argv[at]!)).isDirectory()) return argv;
1727
} catch {
1828
return argv;
1929
}
20-
return ["open", ...argv];
30+
return [...argv.slice(0, at), "open", ...argv.slice(at)];
2131
}

0 commit comments

Comments
 (0)