mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
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:
parent
4578b2d3a9
commit
2553b05554
3 changed files with 32 additions and 3 deletions
|
|
@ -48,8 +48,11 @@ function isNodeErrnoException(error: unknown): error is NodeJS.ErrnoException {
|
|||
/**
|
||||
* Classify whether ktx may own a git repository rooted exactly at `dir`.
|
||||
*
|
||||
* - `unowned`: no `<dir>/.git` exists → ktx can `git init` here. Covers a fresh
|
||||
* standalone directory and a fresh directory nested inside a parent repo.
|
||||
* - `unowned`: there is no git repository for ktx to avoid here → ktx may
|
||||
* `git init`. Covers a fresh standalone directory, a fresh directory nested
|
||||
* inside a parent repo, a path that does not exist yet, and a path that is not
|
||||
* a directory at all (whether the path is a usable project directory is a
|
||||
* separate concern for the caller to validate).
|
||||
* - `ktx-managed`: `<dir>/.git` is a directory carrying ktx's ownership marker.
|
||||
* - `foreign`: a repo ktx did not create — a `.git` directory without the marker,
|
||||
* or a `.git` *file* (a linked worktree). ktx must never adopt or mutate it.
|
||||
|
|
@ -64,7 +67,9 @@ export async function classifyKtxRepoOwnership(dir: string): Promise<KtxRepoOwne
|
|||
try {
|
||||
dotGitIsDirectory = (await fs.lstat(join(dir, '.git'))).isDirectory();
|
||||
} catch (error) {
|
||||
if (isNodeErrnoException(error) && error.code === 'ENOENT') {
|
||||
// ENOENT: `<dir>/.git` is absent. ENOTDIR: `<dir>` itself is a file, so it
|
||||
// can hold no repo. Either way there is nothing for ktx to avoid here.
|
||||
if (isNodeErrnoException(error) && (error.code === 'ENOENT' || error.code === 'ENOTDIR')) {
|
||||
return 'unowned';
|
||||
}
|
||||
throw error;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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' });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue