Skip to content
This repository was archived by the owner on Feb 5, 2026. It is now read-only.

Commit 5591b95

Browse files
committed
Gracefully handle a user not adding any repository URLs
1 parent 910257d commit 5591b95

File tree

6 files changed

+136
-6
lines changed

6 files changed

+136
-6
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@multiverse-io/cari": patch
3+
---
4+
5+
Gracefully handle user not entering any repo URLs during init

src/commands/init.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ describe("init command", () => {
8383
},
8484
]);
8585
await init();
86-
expect(pathExists(`${homeDir}/.cari`)).resolves.toBe(true);
86+
await expect(pathExists(`${homeDir}/.cari`)).resolves.toBe(true);
8787
expect(gitMock.clone).toHaveBeenCalledWith(
8888
repoUrl,
8989
`${homeDir}/.cari/my-org/ai-rules`
@@ -183,4 +183,27 @@ describe("init command", () => {
183183
"No rules were selected to include. Please select at least one rule to include with <space> or press Ctrl-c to exit."
184184
);
185185
});
186+
187+
it("should exit with an error when no repositories are provided", async () => {
188+
mockDirs(populatedAriHomeDir, emptyProjectDir);
189+
190+
// Mock empty input to simulate no repos being added
191+
inputMock.mockResolvedValueOnce("");
192+
193+
// Suppress console.error messages because this test will throw an (expected) error
194+
const consoleErrorSpy = vi
195+
.spyOn(console, "error")
196+
.mockImplementation(() => {});
197+
198+
const mockExit = vi.spyOn(process, "exit").mockImplementation((code) => {
199+
throw new Error(`Process exited with code ${code}`);
200+
});
201+
202+
// Expect the init function to throw the error from our mocked process.exit
203+
await expect(init()).rejects.toThrow("Process exited with code 1");
204+
205+
expect(errorMessageMock).toHaveBeenCalledWith(
206+
"No repositories were added. Please try again and enter at least one repository URL."
207+
);
208+
});
186209
});

src/commands/init.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ import { RepoRules, SelectedRules } from "../rules/types.js";
2323
export const init = async (): Promise<void> => {
2424
try {
2525
const repoUrls = await askUserToSelectRepos();
26+
if (!repoUrls.ok) {
27+
errorMessage(repoUrls.error.message);
28+
process.exit(1);
29+
}
2630
await createAriHomeDirIfNotExists();
27-
const allRepoDetails = repoUrls.map((repoUrl) =>
31+
const allRepoDetails = repoUrls.value.map((repoUrl) =>
2832
extractRepoDetails(repoUrl)
2933
);
3034
for (const repoDetails of allRepoDetails) {

src/prompting/init-prompts.test.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
import { describe, expect, it } from "vitest";
1+
import { describe, expect, it, vi, beforeEach } from "vitest";
22
import { directoryChoice, fileChoice, repoChoice } from "./common.js";
3-
import { getSelectRuleChoices } from "./init-prompts.js";
3+
import { askUserToSelectRepos, getSelectRuleChoices } from "./init-prompts.js";
4+
import { input } from "@inquirer/prompts";
5+
import { ok, error, userInputError } from "../utils/result.js";
6+
7+
// Mock the input prompt
8+
vi.mock("@inquirer/prompts", () => ({
9+
input: vi.fn(),
10+
checkbox: vi.fn(),
11+
}));
412

513
describe("getSelectRuleChoices", () => {
614
it("should return a list of choices", () => {
@@ -95,3 +103,40 @@ describe("getSelectRuleChoices", () => {
95103
]);
96104
});
97105
});
106+
107+
describe("askUserToSelectRepos", () => {
108+
beforeEach(() => {
109+
vi.clearAllMocks();
110+
});
111+
112+
it("should return a success result with repository URLs when valid repositories are provided", async () => {
113+
const repoUrl1 = "git@github.com:my-org/my-repo.git";
114+
const repoUrl2 = "git@github.com:my-other-org/my-other-repo.git";
115+
116+
// Mock input to first return two repo URLs and then an empty string to finish
117+
vi.mocked(input).mockResolvedValueOnce(repoUrl1);
118+
vi.mocked(input).mockResolvedValueOnce(repoUrl2);
119+
vi.mocked(input).mockResolvedValueOnce("");
120+
121+
const result = await askUserToSelectRepos();
122+
123+
expect(result).toEqual(ok([repoUrl1, repoUrl2]));
124+
expect(input).toHaveBeenCalledTimes(3);
125+
});
126+
127+
it("should return an error result when no repositories are provided", async () => {
128+
// Mock input to immediately return an empty string (user doesn't add any repos)
129+
vi.mocked(input).mockResolvedValueOnce("");
130+
131+
const result = await askUserToSelectRepos();
132+
133+
expect(result).toEqual(
134+
error(
135+
userInputError(
136+
"No repositories were added. Please try again and enter at least one repository URL."
137+
)
138+
)
139+
);
140+
expect(input).toHaveBeenCalledTimes(1);
141+
});
142+
});

src/prompting/init-prompts.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,17 @@ import {
1212
normaliseSelectedRules,
1313
} from "../rules/rule-flattening.js";
1414
import { RepoRules, RuleFilePath, SelectedRules } from "../rules/types.js";
15+
import {
16+
error,
17+
ok,
18+
Result,
19+
userInputError,
20+
UserInputError,
21+
} from "../utils/result.js";
1522

16-
export const askUserToSelectRepos = async (): Promise<string[]> => {
23+
export const askUserToSelectRepos = async (): Promise<
24+
Result<string[], UserInputError>
25+
> => {
1726
const repos: string[] = [];
1827
while (true) {
1928
const repoToAdd = await input({
@@ -24,7 +33,14 @@ export const askUserToSelectRepos = async (): Promise<string[]> => {
2433
}
2534
repos.push(repoToAdd);
2635
}
27-
return repos;
36+
if (repos.length === 0) {
37+
return error(
38+
userInputError(
39+
"No repositories were added. Please try again and enter at least one repository URL."
40+
)
41+
);
42+
}
43+
return ok(repos);
2844
};
2945

3046
export const askUserToSelectRules = async (

src/utils/result.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export type Ok<T> = {
2+
ok: true;
3+
value: T;
4+
};
5+
6+
export function ok<T>(value: T): Ok<T> {
7+
return {
8+
ok: true,
9+
value,
10+
};
11+
}
12+
13+
export type Error<E> = {
14+
ok: false;
15+
error: E;
16+
};
17+
18+
export function error<E>(error: E): Error<E> {
19+
return {
20+
ok: false,
21+
error,
22+
};
23+
}
24+
25+
export type Result<T, E> = Ok<T> | Error<E>;
26+
27+
export type UserInputError = {
28+
type: "user-input";
29+
message: string;
30+
};
31+
32+
export function userInputError(message: string): UserInputError {
33+
return {
34+
type: "user-input",
35+
message,
36+
};
37+
}

0 commit comments

Comments
 (0)