bet-cli 0.1.4 → 0.3.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.
@@ -114,6 +114,54 @@ describe("config", () => {
114
114
  expect(config.ignores).toEqual(["**/foo/**", "**/bar"]);
115
115
  });
116
116
 
117
+ it("returns editor when set in config", async () => {
118
+ const configPath = getConfigPath();
119
+ const projectsPath = getProjectsPath();
120
+ vi.mocked(fs.readFile).mockImplementation((p: string) => {
121
+ if (p === configPath) {
122
+ return Promise.resolve(
123
+ JSON.stringify({
124
+ version: 1,
125
+ roots: [],
126
+ editor: "code -n",
127
+ }),
128
+ );
129
+ }
130
+ if (p === projectsPath) {
131
+ return Promise.resolve(JSON.stringify({ projects: {} }));
132
+ }
133
+ return Promise.reject(new Error("ENOENT"));
134
+ });
135
+
136
+ const config = await readConfig();
137
+
138
+ expect(config.editor).toBe("code -n");
139
+ });
140
+
141
+ it("normalizes blank editor to undefined", async () => {
142
+ const configPath = getConfigPath();
143
+ const projectsPath = getProjectsPath();
144
+ vi.mocked(fs.readFile).mockImplementation((p: string) => {
145
+ if (p === configPath) {
146
+ return Promise.resolve(
147
+ JSON.stringify({
148
+ version: 1,
149
+ roots: [],
150
+ editor: " ",
151
+ }),
152
+ );
153
+ }
154
+ if (p === projectsPath) {
155
+ return Promise.resolve(JSON.stringify({ projects: {} }));
156
+ }
157
+ return Promise.reject(new Error("ENOENT"));
158
+ });
159
+
160
+ const config = await readConfig();
161
+
162
+ expect(config.editor).toBeUndefined();
163
+ });
164
+
117
165
  it("leaves ignores undefined when not in config", async () => {
118
166
  const configPath = getConfigPath();
119
167
  const projectsPath = getProjectsPath();
@@ -276,5 +324,28 @@ describe("config", () => {
276
324
  );
277
325
  expect(written.ignoredPaths).toEqual([path.resolve("/code/foo"), path.resolve("/code/bar")]);
278
326
  });
327
+
328
+ it("writes editor to app config when present", async () => {
329
+ const configPath = getConfigPath();
330
+ const projectsPath = getProjectsPath();
331
+ vi.mocked(fs.readFile).mockImplementation((p: string) => {
332
+ if (p === configPath) {
333
+ return Promise.resolve(JSON.stringify({ version: 1, roots: [] }));
334
+ }
335
+ if (p === projectsPath) {
336
+ return Promise.resolve(JSON.stringify({ projects: {} }));
337
+ }
338
+ return Promise.reject(new Error("ENOENT"));
339
+ });
340
+ const config = await readConfig();
341
+
342
+ await writeConfig({ ...config, editor: "cursor" });
343
+
344
+ expect(fs.writeFile).toHaveBeenCalledWith(
345
+ configPath,
346
+ expect.stringContaining('"editor": "cursor"'),
347
+ "utf8",
348
+ );
349
+ });
279
350
  });
280
351
  });
@@ -0,0 +1,167 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { EventEmitter } from "node:events";
3
+ import { getSystemOpenCommand, openProjectInEditor, parseEditorCommand } from "../src/lib/editor.js";
4
+
5
+ vi.mock("node:child_process", () => ({
6
+ spawn: vi.fn(),
7
+ }));
8
+
9
+ import { spawn } from "node:child_process";
10
+
11
+ type SpawnedProcess = EventEmitter & {
12
+ unref: ReturnType<typeof vi.fn>;
13
+ };
14
+
15
+ function createSpawnedProcess(): SpawnedProcess {
16
+ const child = new EventEmitter() as SpawnedProcess;
17
+ child.unref = vi.fn();
18
+ return child;
19
+ }
20
+
21
+ describe("editor", () => {
22
+ beforeEach(() => {
23
+ vi.mocked(spawn).mockReset();
24
+ });
25
+
26
+ describe("parseEditorCommand", () => {
27
+ it("parses command and args", () => {
28
+ expect(parseEditorCommand("code -n")).toEqual({
29
+ command: "code",
30
+ args: ["-n"],
31
+ });
32
+ });
33
+
34
+ it("handles quoted args", () => {
35
+ expect(parseEditorCommand('cursor --profile "Work Profile"')).toEqual({
36
+ command: "cursor",
37
+ args: ["--profile", "Work Profile"],
38
+ });
39
+ });
40
+
41
+ it("throws for malformed command", () => {
42
+ expect(() => parseEditorCommand('code "unterminated')).toThrow(
43
+ "Invalid editor command in config.",
44
+ );
45
+ });
46
+ });
47
+
48
+ describe("getSystemOpenCommand", () => {
49
+ it("returns open on macOS", () => {
50
+ expect(getSystemOpenCommand("/tmp/project", "darwin")).toEqual({
51
+ command: "open",
52
+ args: ["/tmp/project"],
53
+ });
54
+ });
55
+
56
+ it("returns cmd start on windows", () => {
57
+ expect(getSystemOpenCommand("C:\\tmp\\project", "win32")).toEqual({
58
+ command: "cmd",
59
+ args: ["/c", "start", "", "C:\\tmp\\project"],
60
+ });
61
+ });
62
+
63
+ it("returns xdg-open on linux", () => {
64
+ expect(getSystemOpenCommand("/tmp/project", "linux")).toEqual({
65
+ command: "xdg-open",
66
+ args: ["/tmp/project"],
67
+ });
68
+ });
69
+ });
70
+
71
+ describe("openProjectInEditor", () => {
72
+ it("uses configured editor command with project path", async () => {
73
+ const child = createSpawnedProcess();
74
+ vi.mocked(spawn).mockReturnValue(child as never);
75
+
76
+ const openPromise = openProjectInEditor("/tmp/project", "code -n");
77
+ child.emit("spawn");
78
+ await openPromise;
79
+
80
+ expect(spawn).toHaveBeenCalledWith("code", ["-n", "/tmp/project"], {
81
+ detached: true,
82
+ stdio: "ignore",
83
+ });
84
+ expect(child.unref).toHaveBeenCalledTimes(1);
85
+ });
86
+
87
+ it("falls back to system opener when editor is not configured", async () => {
88
+ const child = createSpawnedProcess();
89
+ vi.mocked(spawn).mockReturnValue(child as never);
90
+
91
+ const openPromise = openProjectInEditor("/tmp/project", undefined, {});
92
+ child.emit("spawn");
93
+ await openPromise;
94
+
95
+ expect(spawn).toHaveBeenCalledTimes(1);
96
+ expect(child.unref).toHaveBeenCalledTimes(1);
97
+ });
98
+
99
+ it("uses VISUAL when config editor is not set", async () => {
100
+ const child = createSpawnedProcess();
101
+ vi.mocked(spawn).mockReturnValue(child as never);
102
+
103
+ const openPromise = openProjectInEditor(
104
+ "/tmp/project",
105
+ undefined,
106
+ { VISUAL: "nvim" },
107
+ );
108
+ child.emit("spawn");
109
+ await openPromise;
110
+
111
+ expect(spawn).toHaveBeenCalledWith("nvim", ["/tmp/project"], {
112
+ detached: true,
113
+ stdio: "ignore",
114
+ });
115
+ expect(child.unref).toHaveBeenCalledTimes(1);
116
+ });
117
+
118
+ it("uses EDITOR when VISUAL is not set", async () => {
119
+ const child = createSpawnedProcess();
120
+ vi.mocked(spawn).mockReturnValue(child as never);
121
+
122
+ const openPromise = openProjectInEditor(
123
+ "/tmp/project",
124
+ undefined,
125
+ { EDITOR: "vim -p" },
126
+ );
127
+ child.emit("spawn");
128
+ await openPromise;
129
+
130
+ expect(spawn).toHaveBeenCalledWith("vim", ["-p", "/tmp/project"], {
131
+ detached: true,
132
+ stdio: "ignore",
133
+ });
134
+ expect(child.unref).toHaveBeenCalledTimes(1);
135
+ });
136
+
137
+ it("prefers VISUAL over EDITOR", async () => {
138
+ const child = createSpawnedProcess();
139
+ vi.mocked(spawn).mockReturnValue(child as never);
140
+
141
+ const openPromise = openProjectInEditor(
142
+ "/tmp/project",
143
+ undefined,
144
+ { VISUAL: "nvim", EDITOR: "vim" },
145
+ );
146
+ child.emit("spawn");
147
+ await openPromise;
148
+
149
+ expect(spawn).toHaveBeenCalledWith("nvim", ["/tmp/project"], {
150
+ detached: true,
151
+ stdio: "ignore",
152
+ });
153
+ expect(child.unref).toHaveBeenCalledTimes(1);
154
+ });
155
+
156
+ it("throws when spawn fails", async () => {
157
+ const child = createSpawnedProcess();
158
+ vi.mocked(spawn).mockReturnValue(child as never);
159
+
160
+ const openPromise = openProjectInEditor("/tmp/project", "code");
161
+ const error = new Error("missing executable");
162
+ child.emit("error", error);
163
+
164
+ await expect(openPromise).rejects.toThrow("missing executable");
165
+ });
166
+ });
167
+ });