fix(cli): keep classifyKtxRepoOwnership total for non-directory paths

The setup ownership guard runs before the existing not-a-directory check, so
pointing a custom/--project-dir path at a file made classifyKtxRepoOwnership
lstat `<file>/.git`, hit ENOTDIR, and throw — crashing the setup step instead
of returning the friendly "path exists and is not a directory" result.

A path that is a file (or missing) holds no git repo for ktx to avoid, so treat
ENOTDIR like ENOENT and return 'unowned'. The downstream existingFolderState
check still rejects a non-directory with its friendly message, and the
classifier no longer throws raw errno for any caller.
This commit is contained in:
Andrey Avtomonov 2026-06-09 23:49:15 +02:00
parent 4578b2d3a9
commit 2553b05554
3 changed files with 32 additions and 3 deletions

View file

@ -151,4 +151,10 @@ describe('classifyKtxRepoOwnership', () => {
await writeFile(join(dir, '.git'), 'gitdir: ../actual.git\n', 'utf-8');
expect(await classifyKtxRepoOwnership(dir)).toBe('foreign');
});
it('reports unowned when the path is itself a file', async () => {
const filePath = join(tempDir, 'notes.txt');
await writeFile(filePath, 'a file, not a folder\n', 'utf-8');
expect(await classifyKtxRepoOwnership(filePath)).toBe('unowned');
});
});

View file

@ -344,6 +344,24 @@ describe('setup project step', () => {
await expect(stat(join(projectDir, 'ktx.yaml'))).rejects.toThrow();
});
it('rejects a custom path that points at an existing file without crashing', async () => {
const startDir = join(tempDir, 'start');
await mkdir(startDir, { recursive: true });
await writeFile(join(startDir, 'notes.txt'), 'a file, not a folder\n', 'utf-8');
const prompts = makePromptAdapter({ choices: ['new-custom'], textValue: 'notes.txt' });
const testIo = makeIo({ stdoutIsTty: true });
await expect(
runKtxSetupProjectStep(
{ projectDir: startDir, mode: 'auto', inputMode: 'auto', yes: false },
testIo.io,
{ prompts },
),
).resolves.toMatchObject({ status: 'missing-input', projectDir: startDir });
expect(testIo.stderr()).toContain('exists and is not a directory');
});
it('prompts to exit and returns cancelled in interactive auto mode', async () => {
const projectDir = join(tempDir, 'warehouse');
const prompts = makePromptAdapter({ choice: 'exit' });