Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions create-bestax/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ CLI that scaffolds a Vite app wired for `@allxsmith/bestax-bulma`. Agents and CI
first-class consumers: **every prompt must have a flag equivalent**, and the non-interactive
path (`-y` + flags, no TTY) must never hang or regress (#192).

The one question `-y` does not answer is whether to delete a non-empty target directory: only
`--overwrite` or a yes at the prompt does, and without either the run stops and changes nothing.
Every flag is validated before anything on disk changes, and the directory is emptied only after
the last prompt, so a typo or a Ctrl+C never costs the user their files (#945).

## Architecture

- `src/index.ts` — bin entry (Node version check); `src/cli.ts` — the commander program
Expand Down
13 changes: 11 additions & 2 deletions create-bestax/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ npm create bestax@latest

You'll be asked to:

1. Enter a project name
1. Enter a project name (and, if that directory already holds files, confirm removing them;
nothing is removed until you have answered the last question)
2. Select a framework (Vite or Vite + TypeScript)
3. Choose a Bulma CSS flavor (Complete or Minimal)
4. Select an icon library (Font Awesome, Material Icons, etc.)
Expand All @@ -60,7 +61,8 @@ npm create bestax@latest [project-directory] [options]

**Arguments:**

- `[project-directory]` - Optional project directory name
- `[project-directory]` - Optional project directory name. It also becomes the `package.json`
name, lower-cased the way `npm init` does and without a leading `_`, since npm rejects both.

**Options:**

Expand Down Expand Up @@ -91,6 +93,13 @@ npm create bestax@latest [project-directory] [options]
- Icon library: `none`
- AI skills: installed (pass `--no-skills` to opt out)

`-y` never deletes files. If the target directory is not empty, the run stops, lists what is
there, and changes nothing; add `--overwrite` to empty it first. Earlier versions emptied the
directory under `-y` alone, so a script that relied on that now needs `--overwrite`.
Comment thread
claude[bot] marked this conversation as resolved.

- `--overwrite` - Remove existing files if the target directory is not empty, without asking.
Every other flag is checked first, so a typo stops the run before anything is deleted.

**Example:**

```bash
Expand Down
11 changes: 11 additions & 0 deletions create-bestax/src/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,17 @@ describe('cli', () => {
);
});

it('should offer --overwrite and say -y never removes files (#945)', () => {
const program = createCLI();
const overwrite = program.options.find(opt => opt.long === '--overwrite');
const yes = program.options.find(opt => opt.long === '--yes');
expect(overwrite?.description).toContain('remove existing files');
expect(yes?.description).toContain('--overwrite');

program.parse(['my-app', '-y', '--overwrite'], { from: 'user' });
expect(program.opts().overwrite).toBe(true);
});

it('should leave telemetry undefined when neither flag is given', () => {
const program = createCLI();
program.parse(['my-app', '-y'], { from: 'user' });
Expand Down
11 changes: 8 additions & 3 deletions create-bestax/src/__tests__/constants.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,9 +399,14 @@ describe('constants', () => {
});

it('should have functions that return strings', () => {
expect(typeof MESSAGES.DIRECTORY_NOT_EMPTY('test')).toBe('string');
expect(MESSAGES.DIRECTORY_NOT_EMPTY('mydir')).toContain('mydir');
expect(MESSAGES.DIRECTORY_NOT_EMPTY('mydir')).toContain('not empty');
expect(typeof MESSAGES.DIRECTORY_NOT_EMPTY('test', ['a'])).toBe('string');
expect(MESSAGES.DIRECTORY_NOT_EMPTY('mydir', ['a'])).toContain('mydir');
expect(MESSAGES.DIRECTORY_NOT_EMPTY('mydir', ['a'])).toContain(
'not empty (a)'
);
expect(MESSAGES.NOT_A_DIRECTORY('notes.txt')).toContain(
'notes.txt already exists and is not a directory'
);

expect(typeof MESSAGES.EMPTYING_DIRECTORY('test')).toBe('string');
expect(MESSAGES.EMPTYING_DIRECTORY('mydir')).toContain('mydir');
Expand Down
58 changes: 58 additions & 0 deletions create-bestax/src/__tests__/file-system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ jest.unstable_mockModule('fs-extra', () => ({
default: {
existsSync: jest.fn(),
readdirSync: jest.fn(),
statSync: jest.fn(),
emptyDir: jest.fn(),
ensureDir: jest.fn(),
copy: jest.fn(),
Expand All @@ -25,6 +26,7 @@ jest.unstable_mockModule('fs-extra', () => ({
},
existsSync: jest.fn(),
readdirSync: jest.fn(),
statSync: jest.fn(),
emptyDir: jest.fn(),
ensureDir: jest.fn(),
copy: jest.fn(),
Expand All @@ -39,6 +41,8 @@ const fs = await import('fs-extra');
const {
checkDirectoryExists,
isDirectoryEmpty,
listDirectoryEntries,
existsAsNonDirectory,
emptyDirectory,
ensureDirectory,
copyDirectory,
Expand Down Expand Up @@ -113,6 +117,60 @@ describe('file-system', () => {
});
});

describe('listDirectoryEntries', () => {
it('returns no entries for a directory that does not exist', async () => {
(
fs.default.existsSync as jest.MockedFunction<typeof fs.existsSync>
).mockReturnValue(false);
expect(await listDirectoryEntries('/test/path')).toEqual([]);
expect(fs.default.readdirSync).not.toHaveBeenCalled();
});

it('returns the top-level entries in a stable, sorted order', async () => {
(
fs.default.existsSync as jest.MockedFunction<typeof fs.existsSync>
).mockReturnValue(true);
(
fs.default.readdirSync as jest.MockedFunction<typeof fs.readdirSync>
).mockReturnValue(['src', 'notes.txt', '.git'] as unknown);
expect(await listDirectoryEntries('/test/path')).toEqual([
'.git',
'notes.txt',
'src',
]);
});
});

describe('existsAsNonDirectory', () => {
const statSync = fs.default.statSync as jest.MockedFunction<
typeof fs.statSync
>;

it('is false when nothing is there', async () => {
statSync.mockReturnValue(
undefined as unknown as ReturnType<typeof fs.statSync>
);
expect(await existsAsNonDirectory('/test/path')).toBe(false);
expect(statSync).toHaveBeenCalledWith('/test/path', {
throwIfNoEntry: false,
});
});

it('is false for a directory', async () => {
statSync.mockReturnValue({
isDirectory: () => true,
} as unknown as ReturnType<typeof fs.statSync>);
expect(await existsAsNonDirectory('/test/path')).toBe(false);
});

it('is true for a file', async () => {
statSync.mockReturnValue({
isDirectory: () => false,
} as unknown as ReturnType<typeof fs.statSync>);
expect(await existsAsNonDirectory('/test/notes.txt')).toBe(true);
});
});

describe('emptyDirectory', () => {
it('should empty a directory', async () => {
await emptyDirectory('/test/path');
Expand Down
Loading
Loading