bet-cli 0.1.2 → 0.1.3
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 +21 -1
- package/dist/commands/completion.js +2 -0
- package/dist/commands/ignore.js +78 -0
- package/dist/commands/update.js +139 -80
- package/dist/index.js +3 -1
- package/dist/lib/config.js +27 -0
- package/dist/lib/cron.js +115 -6
- package/dist/lib/ignore.js +9 -0
- package/dist/lib/log-dir.js +22 -0
- package/dist/lib/logger.js +80 -0
- package/dist/lib/metadata.js +2 -3
- package/dist/lib/scan.js +5 -10
- package/package.json +1 -1
- package/src/commands/completion.ts +2 -0
- package/src/commands/ignore.ts +93 -0
- package/src/commands/update.ts +80 -17
- package/src/index.ts +3 -1
- package/src/lib/config.ts +28 -1
- package/src/lib/cron.ts +152 -9
- package/src/lib/ignore.ts +13 -0
- package/src/lib/log-dir.ts +26 -0
- package/src/lib/logger.ts +92 -0
- package/src/lib/metadata.ts +2 -3
- package/src/lib/scan.ts +5 -10
- package/src/lib/types.ts +6 -0
- package/tests/config.test.ts +175 -1
- package/tests/cron.test.ts +243 -0
- package/tests/ignore.test.ts +179 -0
- package/tests/metadata.test.ts +3 -2
- package/tests/scan.test.ts +5 -4
- package/tests/update.test.ts +20 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import {
|
|
6
|
+
parseCronSchedule,
|
|
7
|
+
scheduleToCronExpression,
|
|
8
|
+
formatScheduleLabel,
|
|
9
|
+
installUpdateCron,
|
|
10
|
+
uninstallUpdateCron,
|
|
11
|
+
} from "../src/lib/cron.js";
|
|
12
|
+
import { getConfigPath } from "../src/lib/config.js";
|
|
13
|
+
|
|
14
|
+
vi.mock("../src/lib/config.js", () => ({
|
|
15
|
+
getConfigPath: vi.fn(() => path.join("/tmp", "bet-cron-test", "config.json")),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("node:fs/promises", () => ({
|
|
19
|
+
default: {
|
|
20
|
+
readFile: vi.fn(),
|
|
21
|
+
writeFile: vi.fn(),
|
|
22
|
+
mkdir: vi.fn(),
|
|
23
|
+
chmod: vi.fn(),
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("node:child_process", () => ({
|
|
28
|
+
spawnSync: vi.fn(),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
describe("cron", () => {
|
|
32
|
+
describe("parseCronSchedule", () => {
|
|
33
|
+
it("parses valid minute schedules (1-59)", () => {
|
|
34
|
+
expect(parseCronSchedule("1m")).toEqual({ value: 1, unit: "m" });
|
|
35
|
+
expect(parseCronSchedule("5m")).toEqual({ value: 5, unit: "m" });
|
|
36
|
+
expect(parseCronSchedule("30m")).toEqual({ value: 30, unit: "m" });
|
|
37
|
+
expect(parseCronSchedule("59m")).toEqual({ value: 59, unit: "m" });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("parses valid hour schedules (1-24)", () => {
|
|
41
|
+
expect(parseCronSchedule("1h")).toEqual({ value: 1, unit: "h" });
|
|
42
|
+
expect(parseCronSchedule("12h")).toEqual({ value: 12, unit: "h" });
|
|
43
|
+
expect(parseCronSchedule("24h")).toEqual({ value: 24, unit: "h" });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("parses valid day schedules (1-31)", () => {
|
|
47
|
+
expect(parseCronSchedule("1d")).toEqual({ value: 1, unit: "d" });
|
|
48
|
+
expect(parseCronSchedule("7d")).toEqual({ value: 7, unit: "d" });
|
|
49
|
+
expect(parseCronSchedule("31d")).toEqual({ value: 31, unit: "d" });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("accepts case-insensitive unit", () => {
|
|
53
|
+
expect(parseCronSchedule("5M")).toEqual({ value: 5, unit: "m" });
|
|
54
|
+
expect(parseCronSchedule("2H")).toEqual({ value: 2, unit: "h" });
|
|
55
|
+
expect(parseCronSchedule("1D")).toEqual({ value: 1, unit: "d" });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("trims and lowercases input", () => {
|
|
59
|
+
expect(parseCronSchedule(" 5m ")).toEqual({ value: 5, unit: "m" });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("throws on invalid format", () => {
|
|
63
|
+
expect(() => parseCronSchedule("")).toThrow(/Invalid cron schedule/);
|
|
64
|
+
expect(() => parseCronSchedule("5")).toThrow(/Invalid cron schedule/);
|
|
65
|
+
expect(() => parseCronSchedule("m")).toThrow(/Invalid cron schedule/);
|
|
66
|
+
expect(() => parseCronSchedule("1x")).toThrow(/Invalid cron schedule/);
|
|
67
|
+
expect(() => parseCronSchedule("m5")).toThrow(/Invalid cron schedule/);
|
|
68
|
+
expect(() => parseCronSchedule("5min")).toThrow(/Invalid cron schedule/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("throws on invalid minute range (0, 60, 61)", () => {
|
|
72
|
+
expect(() => parseCronSchedule("0m")).toThrow(/Invalid minutes: 0/);
|
|
73
|
+
expect(() => parseCronSchedule("60m")).toThrow(/Invalid minutes: 60/);
|
|
74
|
+
expect(() => parseCronSchedule("61m")).toThrow(/Invalid minutes: 61/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("throws on invalid hour range (0, 25)", () => {
|
|
78
|
+
expect(() => parseCronSchedule("0h")).toThrow(/Invalid hours: 0/);
|
|
79
|
+
expect(() => parseCronSchedule("25h")).toThrow(/Invalid hours: 25/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("throws on invalid day range (0, 32)", () => {
|
|
83
|
+
expect(() => parseCronSchedule("0d")).toThrow(/Invalid days: 0/);
|
|
84
|
+
expect(() => parseCronSchedule("32d")).toThrow(/Invalid days: 32/);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("scheduleToCronExpression", () => {
|
|
89
|
+
it("maps minutes to */N * * * *", () => {
|
|
90
|
+
expect(scheduleToCronExpression({ value: 1, unit: "m" })).toBe("*/1 * * * *");
|
|
91
|
+
expect(scheduleToCronExpression({ value: 5, unit: "m" })).toBe("*/5 * * * *");
|
|
92
|
+
expect(scheduleToCronExpression({ value: 30, unit: "m" })).toBe("*/30 * * * *");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("maps hours to 0 */N * * *", () => {
|
|
96
|
+
expect(scheduleToCronExpression({ value: 1, unit: "h" })).toBe("0 */1 * * *");
|
|
97
|
+
expect(scheduleToCronExpression({ value: 2, unit: "h" })).toBe("0 */2 * * *");
|
|
98
|
+
expect(scheduleToCronExpression({ value: 12, unit: "h" })).toBe("0 */12 * * *");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("maps 24h to midnight daily", () => {
|
|
102
|
+
expect(scheduleToCronExpression({ value: 24, unit: "h" })).toBe("0 0 * * *");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("maps days to 0 0 */N * *", () => {
|
|
106
|
+
expect(scheduleToCronExpression({ value: 1, unit: "d" })).toBe("0 0 */1 * *");
|
|
107
|
+
expect(scheduleToCronExpression({ value: 7, unit: "d" })).toBe("0 0 */7 * *");
|
|
108
|
+
expect(scheduleToCronExpression({ value: 31, unit: "d" })).toBe("0 0 */31 * *");
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("formatScheduleLabel", () => {
|
|
113
|
+
it("formats minutes (singular and plural)", () => {
|
|
114
|
+
expect(formatScheduleLabel({ value: 1, unit: "m" })).toBe("every minute");
|
|
115
|
+
expect(formatScheduleLabel({ value: 5, unit: "m" })).toBe("every 5 minutes");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("formats hours (singular and plural)", () => {
|
|
119
|
+
expect(formatScheduleLabel({ value: 1, unit: "h" })).toBe("every hour");
|
|
120
|
+
expect(formatScheduleLabel({ value: 2, unit: "h" })).toBe("every 2 hours");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("formats days (singular and plural)", () => {
|
|
124
|
+
expect(formatScheduleLabel({ value: 1, unit: "d" })).toBe("every day");
|
|
125
|
+
expect(formatScheduleLabel({ value: 7, unit: "d" })).toBe("every 7 days");
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("installUpdateCron (mocked)", () => {
|
|
130
|
+
const configDir = path.join("/tmp", "bet-cron-test");
|
|
131
|
+
const wrapperPath = path.join(configDir, "bet-update-cron.sh");
|
|
132
|
+
|
|
133
|
+
beforeEach(() => {
|
|
134
|
+
vi.mocked(getConfigPath).mockReturnValue(path.join(configDir, "config.json"));
|
|
135
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
136
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
137
|
+
vi.mocked(fs.chmod).mockResolvedValue(undefined);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("throws on invalid schedule", async () => {
|
|
141
|
+
await expect(
|
|
142
|
+
installUpdateCron({
|
|
143
|
+
nodePath: "/usr/bin/node",
|
|
144
|
+
entryScriptPath: "/usr/bin/bet",
|
|
145
|
+
schedule: "61m",
|
|
146
|
+
}),
|
|
147
|
+
).rejects.toThrow(/Invalid minutes: 61/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("replaces existing bet cron block (single block)", async () => {
|
|
151
|
+
const existingCrontab = [
|
|
152
|
+
"# other job",
|
|
153
|
+
"0 9 * * * /run/backup",
|
|
154
|
+
"# bet:update",
|
|
155
|
+
"0 * * * * /old/wrapper.sh",
|
|
156
|
+
"",
|
|
157
|
+
].join("\n");
|
|
158
|
+
|
|
159
|
+
let writtenCrontab = "";
|
|
160
|
+
vi.mocked(spawnSync).mockImplementation((cmd, args, opts) => {
|
|
161
|
+
if (cmd === "crontab" && args?.[0] === "-l") {
|
|
162
|
+
return { status: 0, stdout: existingCrontab, stderr: "", output: [] } as ReturnType<typeof spawnSync>;
|
|
163
|
+
}
|
|
164
|
+
if (cmd === "crontab" && args?.[0] === "-" && opts?.input) {
|
|
165
|
+
writtenCrontab = opts.input as string;
|
|
166
|
+
return { status: 0, stdout: "", stderr: "", output: [] } as ReturnType<typeof spawnSync>;
|
|
167
|
+
}
|
|
168
|
+
return { status: 1, stdout: "", stderr: "unexpected", output: [] } as ReturnType<typeof spawnSync>;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await installUpdateCron({
|
|
172
|
+
nodePath: "/usr/bin/node",
|
|
173
|
+
entryScriptPath: "/usr/bin/bet",
|
|
174
|
+
schedule: "5m",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(writtenCrontab).toContain("# other job");
|
|
178
|
+
expect(writtenCrontab).toContain("0 9 * * * /run/backup");
|
|
179
|
+
expect(writtenCrontab).toContain("# bet:update");
|
|
180
|
+
expect(writtenCrontab).toContain("*/5 * * * *");
|
|
181
|
+
expect(writtenCrontab).toContain(wrapperPath);
|
|
182
|
+
expect(writtenCrontab).not.toContain("/old/wrapper.sh");
|
|
183
|
+
const betBlocks = (writtenCrontab.match(/# bet:update/g) ?? []).length;
|
|
184
|
+
expect(betBlocks).toBe(1);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe("uninstallUpdateCron (mocked)", () => {
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
vi.mocked(spawnSync).mockReset();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("removes bet block and keeps rest of crontab", async () => {
|
|
194
|
+
const existingCrontab = [
|
|
195
|
+
"0 9 * * * /run/backup",
|
|
196
|
+
"# bet:update",
|
|
197
|
+
"0 * * * * /path/to/wrapper.sh",
|
|
198
|
+
"",
|
|
199
|
+
].join("\n");
|
|
200
|
+
let writtenCrontab = "";
|
|
201
|
+
|
|
202
|
+
vi.mocked(spawnSync).mockImplementation((cmd, args, opts) => {
|
|
203
|
+
if (cmd === "crontab" && args?.[0] === "-l") {
|
|
204
|
+
return { status: 0, stdout: existingCrontab, stderr: "", output: [] } as ReturnType<typeof spawnSync>;
|
|
205
|
+
}
|
|
206
|
+
if (cmd === "crontab" && args?.[0] === "-" && opts?.input) {
|
|
207
|
+
writtenCrontab = opts.input as string;
|
|
208
|
+
return { status: 0, stdout: "", stderr: "", output: [] } as ReturnType<typeof spawnSync>;
|
|
209
|
+
}
|
|
210
|
+
return { status: 1, stdout: "", stderr: "unexpected", output: [] } as ReturnType<typeof spawnSync>;
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await uninstallUpdateCron();
|
|
214
|
+
|
|
215
|
+
expect(writtenCrontab).toContain("0 9 * * * /run/backup");
|
|
216
|
+
expect(writtenCrontab).not.toContain("# bet:update");
|
|
217
|
+
expect(writtenCrontab).not.toContain("/path/to/wrapper.sh");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("removes crontab when bet was the only entry", async () => {
|
|
221
|
+
const existingCrontab = ["# bet:update", "0 * * * * /path/to/wrapper.sh"].join("\n");
|
|
222
|
+
const calls: { cmd: string; args?: string[] }[] = [];
|
|
223
|
+
|
|
224
|
+
vi.mocked(spawnSync).mockImplementation((cmd, args) => {
|
|
225
|
+
calls.push({ cmd, args: args as string[] });
|
|
226
|
+
if (cmd === "crontab" && args?.[0] === "-l") {
|
|
227
|
+
return { status: 0, stdout: existingCrontab, stderr: "", output: [] } as ReturnType<typeof spawnSync>;
|
|
228
|
+
}
|
|
229
|
+
if (cmd === "crontab" && args?.[0] === "-r") {
|
|
230
|
+
return { status: 0, stdout: "", stderr: "", output: [] } as ReturnType<typeof spawnSync>;
|
|
231
|
+
}
|
|
232
|
+
if (cmd === "crontab" && args?.[0] === "-") {
|
|
233
|
+
return { status: 0, stdout: "", stderr: "", output: [] } as ReturnType<typeof spawnSync>;
|
|
234
|
+
}
|
|
235
|
+
return { status: 1, stdout: "", stderr: "unexpected", output: [] } as ReturnType<typeof spawnSync>;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await uninstallUpdateCron();
|
|
239
|
+
|
|
240
|
+
expect(calls.some((c) => c.cmd === "crontab" && c.args?.[0] === "-r")).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isPathIgnored } from "../src/lib/ignore.js";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import * as config from "../src/lib/config.js";
|
|
6
|
+
|
|
7
|
+
vi.mock("../src/lib/config.js", () => ({
|
|
8
|
+
readConfig: vi.fn(),
|
|
9
|
+
writeConfig: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { registerIgnore } from "../src/commands/ignore.js";
|
|
13
|
+
|
|
14
|
+
describe("ignore lib", () => {
|
|
15
|
+
describe("isPathIgnored", () => {
|
|
16
|
+
it("returns true when project path equals an ignored path", () => {
|
|
17
|
+
const ignoredPaths = [path.resolve("/code/foo"), path.resolve("/code/bar")];
|
|
18
|
+
expect(isPathIgnored(path.resolve("/code/foo"), ignoredPaths)).toBe(true);
|
|
19
|
+
expect(isPathIgnored(path.resolve("/code/bar"), ignoredPaths)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns true when project path is under an ignored path", () => {
|
|
23
|
+
const ignoredPaths = [path.resolve("/code/foo")];
|
|
24
|
+
expect(isPathIgnored(path.resolve("/code/foo/nested"), ignoredPaths)).toBe(true);
|
|
25
|
+
expect(isPathIgnored(path.resolve("/code/foo/nested/deep"), ignoredPaths)).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns false when project path is not in list and not under any entry", () => {
|
|
29
|
+
const ignoredPaths = [path.resolve("/code/foo")];
|
|
30
|
+
expect(isPathIgnored(path.resolve("/code/bar"), ignoredPaths)).toBe(false);
|
|
31
|
+
expect(isPathIgnored(path.resolve("/code"), ignoredPaths)).toBe(false);
|
|
32
|
+
expect(isPathIgnored(path.resolve("/other/foo"), ignoredPaths)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns false when ignoredPaths is empty", () => {
|
|
36
|
+
expect(isPathIgnored(path.resolve("/code/foo"), [])).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("ignore command", () => {
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.mocked(config.readConfig).mockReset();
|
|
44
|
+
vi.mocked(config.writeConfig).mockReset();
|
|
45
|
+
process.exitCode = undefined;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
async function runIgnore(args: string[]): Promise<string> {
|
|
49
|
+
const chunks: string[] = [];
|
|
50
|
+
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
|
51
|
+
const stderrWrite = process.stderr.write.bind(process.stderr);
|
|
52
|
+
process.stdout.write = (chunk: string | Uint8Array) => {
|
|
53
|
+
chunks.push(typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk));
|
|
54
|
+
return true;
|
|
55
|
+
};
|
|
56
|
+
process.stderr.write = (chunk: string | Uint8Array) => {
|
|
57
|
+
chunks.push(typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk));
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
60
|
+
const program = new Command();
|
|
61
|
+
program.name("bet").version("0.1.0");
|
|
62
|
+
registerIgnore(program);
|
|
63
|
+
await program.parseAsync(["node", "bet", "ignore", ...args]);
|
|
64
|
+
process.stdout.write = stdoutWrite;
|
|
65
|
+
process.stderr.write = stderrWrite;
|
|
66
|
+
return chunks.join("");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
it("ignore add fails when no roots configured", async () => {
|
|
70
|
+
vi.mocked(config.readConfig).mockResolvedValue({
|
|
71
|
+
version: 1,
|
|
72
|
+
roots: [],
|
|
73
|
+
projects: {},
|
|
74
|
+
} as Awaited<ReturnType<typeof config.readConfig>>);
|
|
75
|
+
|
|
76
|
+
await runIgnore(["add", "/code/foo"]);
|
|
77
|
+
|
|
78
|
+
expect(config.writeConfig).not.toHaveBeenCalled();
|
|
79
|
+
expect(process.exitCode).toBe(1);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("ignore add fails when path is not under any root", async () => {
|
|
83
|
+
const codeRoot = path.resolve("/code");
|
|
84
|
+
vi.mocked(config.readConfig).mockResolvedValue({
|
|
85
|
+
version: 1,
|
|
86
|
+
roots: [{ path: codeRoot, name: "code" }],
|
|
87
|
+
projects: {},
|
|
88
|
+
} as Awaited<ReturnType<typeof config.readConfig>>);
|
|
89
|
+
|
|
90
|
+
await runIgnore(["add", "/other/foo"]);
|
|
91
|
+
|
|
92
|
+
expect(config.writeConfig).not.toHaveBeenCalled();
|
|
93
|
+
expect(process.exitCode).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("ignore add persists path when under a root", async () => {
|
|
97
|
+
const codeRoot = path.resolve("/code");
|
|
98
|
+
vi.mocked(config.readConfig).mockResolvedValue({
|
|
99
|
+
version: 1,
|
|
100
|
+
roots: [{ path: codeRoot, name: "code" }],
|
|
101
|
+
projects: {},
|
|
102
|
+
} as Awaited<ReturnType<typeof config.readConfig>>);
|
|
103
|
+
|
|
104
|
+
await runIgnore(["add", path.join(codeRoot, "my-project")]);
|
|
105
|
+
|
|
106
|
+
expect(config.writeConfig).toHaveBeenCalledTimes(1);
|
|
107
|
+
const written = vi.mocked(config.writeConfig).mock.calls[0][0];
|
|
108
|
+
expect(written.ignoredPaths).toContain(path.resolve(codeRoot, "my-project"));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("ignore add --this uses current folder", async () => {
|
|
112
|
+
const codeRoot = path.resolve("/code");
|
|
113
|
+
const cwd = path.join(codeRoot, "current-project");
|
|
114
|
+
const cwdSpy = vi.spyOn(process, "cwd").mockReturnValue(cwd);
|
|
115
|
+
vi.mocked(config.readConfig).mockResolvedValue({
|
|
116
|
+
version: 1,
|
|
117
|
+
roots: [{ path: codeRoot, name: "code" }],
|
|
118
|
+
projects: {},
|
|
119
|
+
} as Awaited<ReturnType<typeof config.readConfig>>);
|
|
120
|
+
|
|
121
|
+
await runIgnore(["add", "--this"]);
|
|
122
|
+
|
|
123
|
+
cwdSpy.mockRestore();
|
|
124
|
+
expect(config.writeConfig).toHaveBeenCalledTimes(1);
|
|
125
|
+
const written = vi.mocked(config.writeConfig).mock.calls[0][0];
|
|
126
|
+
expect(written.ignoredPaths).toContain(cwd);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("ignore add errors when no path and no --this", async () => {
|
|
130
|
+
await runIgnore(["add"]);
|
|
131
|
+
|
|
132
|
+
expect(config.writeConfig).not.toHaveBeenCalled();
|
|
133
|
+
expect(process.exitCode).toBe(1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("ignore rm removes path from list", async () => {
|
|
137
|
+
const codeRoot = path.resolve("/code");
|
|
138
|
+
const ignoredPath = path.join(codeRoot, "foo");
|
|
139
|
+
vi.mocked(config.readConfig).mockResolvedValue({
|
|
140
|
+
version: 1,
|
|
141
|
+
roots: [{ path: codeRoot, name: "code" }],
|
|
142
|
+
projects: {},
|
|
143
|
+
ignoredPaths: [ignoredPath],
|
|
144
|
+
} as Awaited<ReturnType<typeof config.readConfig>>);
|
|
145
|
+
|
|
146
|
+
await runIgnore(["rm", ignoredPath]);
|
|
147
|
+
|
|
148
|
+
expect(config.writeConfig).toHaveBeenCalledTimes(1);
|
|
149
|
+
const written = vi.mocked(config.writeConfig).mock.calls[0][0];
|
|
150
|
+
expect(written.ignoredPaths).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("ignore list prints each ignored path", async () => {
|
|
154
|
+
const codeRoot = path.resolve("/code");
|
|
155
|
+
vi.mocked(config.readConfig).mockResolvedValue({
|
|
156
|
+
version: 1,
|
|
157
|
+
roots: [],
|
|
158
|
+
projects: {},
|
|
159
|
+
ignoredPaths: [path.join(codeRoot, "a"), path.join(codeRoot, "b")],
|
|
160
|
+
} as Awaited<ReturnType<typeof config.readConfig>>);
|
|
161
|
+
|
|
162
|
+
const out = await runIgnore(["list"]);
|
|
163
|
+
|
|
164
|
+
expect(out).toContain(path.join(codeRoot, "a"));
|
|
165
|
+
expect(out).toContain(path.join(codeRoot, "b"));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("ignore list prints nothing when no ignores", async () => {
|
|
169
|
+
vi.mocked(config.readConfig).mockResolvedValue({
|
|
170
|
+
version: 1,
|
|
171
|
+
roots: [],
|
|
172
|
+
projects: {},
|
|
173
|
+
} as Awaited<ReturnType<typeof config.readConfig>>);
|
|
174
|
+
|
|
175
|
+
const out = await runIgnore(["list"]);
|
|
176
|
+
|
|
177
|
+
expect(out).toBe("");
|
|
178
|
+
});
|
|
179
|
+
});
|
package/tests/metadata.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import { computeMetadata } from "../src/lib/metadata.js";
|
|
3
|
+
import { DEFAULT_IGNORES } from "../src/lib/ignore.js";
|
|
3
4
|
|
|
4
5
|
const mockFg = vi.fn();
|
|
5
6
|
vi.mock("fast-glob", () => ({
|
|
@@ -31,7 +32,7 @@ describe("metadata", () => {
|
|
|
31
32
|
{ stats: { mtimeMs: now.getTime() - 10000 } },
|
|
32
33
|
{ stats: { mtimeMs: now.getTime() - 5000 } },
|
|
33
34
|
]);
|
|
34
|
-
const result = await computeMetadata("/some/project", false);
|
|
35
|
+
const result = await computeMetadata("/some/project", false, DEFAULT_IGNORES);
|
|
35
36
|
expect(result.lastIndexedAt).toBeDefined();
|
|
36
37
|
expect(result.startedAt).toBeDefined();
|
|
37
38
|
expect(result.lastModifiedAt).toBeDefined();
|
|
@@ -47,7 +48,7 @@ describe("metadata", () => {
|
|
|
47
48
|
const readme = await import("../src/lib/readme.js");
|
|
48
49
|
vi.mocked(readme.readReadmeDescription).mockResolvedValue("A cool project");
|
|
49
50
|
|
|
50
|
-
const result = await computeMetadata("/repo", true);
|
|
51
|
+
const result = await computeMetadata("/repo", true, DEFAULT_IGNORES);
|
|
51
52
|
expect(result.startedAt).toBe("2021-06-01T00:00:00Z");
|
|
52
53
|
expect(result.dirty).toBe(true);
|
|
53
54
|
expect(result.description).toBe("A cool project");
|
package/tests/scan.test.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { scanRoots } from "../src/lib/scan.js";
|
|
4
|
+
import { DEFAULT_IGNORES } from "../src/lib/ignore.js";
|
|
4
5
|
|
|
5
6
|
const mockFg = vi.fn();
|
|
6
7
|
vi.mock("fast-glob", () => ({
|
|
@@ -24,7 +25,7 @@ describe("scan", () => {
|
|
|
24
25
|
mockFg
|
|
25
26
|
.mockResolvedValueOnce(["proj/.git"])
|
|
26
27
|
.mockResolvedValueOnce([]);
|
|
27
|
-
const result = await scanRoots([root]);
|
|
28
|
+
const result = await scanRoots([root], DEFAULT_IGNORES);
|
|
28
29
|
expect(result.length).toBe(1);
|
|
29
30
|
expect(result[0].path).toBe(path.join(root, "proj"));
|
|
30
31
|
expect(result[0].hasGit).toBe(true);
|
|
@@ -35,7 +36,7 @@ describe("scan", () => {
|
|
|
35
36
|
mockFg
|
|
36
37
|
.mockResolvedValueOnce([])
|
|
37
38
|
.mockResolvedValueOnce(["other/README.md"]);
|
|
38
|
-
const result = await scanRoots([root]);
|
|
39
|
+
const result = await scanRoots([root], DEFAULT_IGNORES);
|
|
39
40
|
expect(result.length).toBe(1);
|
|
40
41
|
expect(result[0].path).toBe(path.join(root, "other"));
|
|
41
42
|
expect(result[0].hasReadme).toBe(true);
|
|
@@ -46,7 +47,7 @@ describe("scan", () => {
|
|
|
46
47
|
mockFg
|
|
47
48
|
.mockResolvedValueOnce(["parent/.git", "parent/child/.git"])
|
|
48
49
|
.mockResolvedValueOnce([]);
|
|
49
|
-
const result = await scanRoots([root]);
|
|
50
|
+
const result = await scanRoots([root], DEFAULT_IGNORES);
|
|
50
51
|
expect(result.length).toBe(1);
|
|
51
52
|
expect(result[0].path).toBe(path.join(root, "parent"));
|
|
52
53
|
});
|
|
@@ -60,7 +61,7 @@ describe("scan", () => {
|
|
|
60
61
|
mockIsInsideGitRepo.mockImplementation((cwd: string) =>
|
|
61
62
|
Promise.resolve(cwd === projectPath)
|
|
62
63
|
);
|
|
63
|
-
const result = await scanRoots([root]);
|
|
64
|
+
const result = await scanRoots([root], DEFAULT_IGNORES);
|
|
64
65
|
expect(result.length).toBe(1);
|
|
65
66
|
expect(result[0].hasGit).toBe(true);
|
|
66
67
|
});
|
package/tests/update.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { willOverrideRoots } from "../src/commands/update.js";
|
|
2
|
+
import { willOverrideRoots, projectSlug, DEFAULT_SLUG_PARENT_FOLDERS } from "../src/commands/update.js";
|
|
3
3
|
import type { RootConfig } from "../src/lib/types.js";
|
|
4
4
|
|
|
5
5
|
const root = (path: string, name: string): RootConfig => ({ path, name });
|
|
@@ -27,4 +27,23 @@ describe("update", () => {
|
|
|
27
27
|
expect(willOverrideRoots(undefined, [])).toBe(false);
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
|
+
|
|
31
|
+
describe("projectSlug", () => {
|
|
32
|
+
it("uses parent name when path ends with default slugParentFolders (src, app)", () => {
|
|
33
|
+
expect(projectSlug("/code/my-api/src", DEFAULT_SLUG_PARENT_FOLDERS)).toBe("my-api");
|
|
34
|
+
expect(projectSlug("/code/my-api/app", DEFAULT_SLUG_PARENT_FOLDERS)).toBe("my-api");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("uses basename when path does not end with default slugParentFolders", () => {
|
|
38
|
+
expect(projectSlug("/code/my-api/other", DEFAULT_SLUG_PARENT_FOLDERS)).toBe("other");
|
|
39
|
+
expect(projectSlug("/code/my-api", DEFAULT_SLUG_PARENT_FOLDERS)).toBe("my-api");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("uses custom slugParentFolders when provided", () => {
|
|
43
|
+
const custom = ["lib"];
|
|
44
|
+
expect(projectSlug("/code/my-api/lib", custom)).toBe("my-api");
|
|
45
|
+
expect(projectSlug("/code/my-api/src", custom)).toBe("src");
|
|
46
|
+
expect(projectSlug("/code/my-api/app", custom)).toBe("app");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
30
49
|
});
|