Skip to content
Open
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
35 changes: 21 additions & 14 deletions src/core/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,40 +453,47 @@ export class InitCommand {
// ═══════════════════════════════════════════════════════════

private async createDirectoryStructure(openspecPath: string, extendMode: boolean): Promise<void> {
const directories = [
openspecPath,
path.join(openspecPath, 'specs'),
path.join(openspecPath, 'changes'),
path.join(openspecPath, 'changes', 'archive'),
];

if (extendMode) {
// In extend mode, just ensure directories exist without spinner
const directories = [
openspecPath,
path.join(openspecPath, 'specs'),
path.join(openspecPath, 'changes'),
path.join(openspecPath, 'changes', 'archive'),
];

for (const dir of directories) {
await FileSystemUtils.createDirectory(dir);
}
await this.writeGitkeepFiles(openspecPath);
return;
}

const spinner = this.startSpinner('Creating OpenSpec structure...');

const directories = [
openspecPath,
path.join(openspecPath, 'specs'),
path.join(openspecPath, 'changes'),
path.join(openspecPath, 'changes', 'archive'),
];

for (const dir of directories) {
await FileSystemUtils.createDirectory(dir);
}

await this.writeGitkeepFiles(openspecPath);

spinner.stopAndPersist({
symbol: PALETTE.white('▌'),
text: PALETTE.white('OpenSpec structure created'),
});
}

private async writeGitkeepFiles(openspecPath: string): Promise<void> {
const emptyDirs = [
path.join(openspecPath, 'specs'),
path.join(openspecPath, 'changes'),
path.join(openspecPath, 'changes', 'archive'),
];
for (const dir of emptyDirs) {
await FileSystemUtils.writeFile(path.join(dir, '.gitkeep'), '');
}
}

// ═══════════════════════════════════════════════════════════
// SKILL & COMMAND GENERATION
// ═══════════════════════════════════════════════════════════
Expand Down
31 changes: 31 additions & 0 deletions test/core/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,37 @@ describe('InitCommand', () => {
expect(await directoryExists(path.join(openspecPath, 'changes', 'archive'))).toBe(true);
});

it('should create .gitkeep files in empty directories', async () => {
const initCommand = new InitCommand({ tools: 'claude', force: true });

await initCommand.execute(testDir);

const openspecPath = path.join(testDir, 'openspec');
expect(await fileExists(path.join(openspecPath, 'specs', '.gitkeep'))).toBe(true);
expect(await fileExists(path.join(openspecPath, 'changes', '.gitkeep'))).toBe(true);
expect(await fileExists(path.join(openspecPath, 'changes', 'archive', '.gitkeep'))).toBe(true);
});

it('should create .gitkeep files in extend mode', async () => {
const initCommand1 = new InitCommand({ tools: 'claude', force: true });
await initCommand1.execute(testDir);

const openspecPath = path.join(testDir, 'openspec');

// Remove .gitkeep files to simulate a cloned repo without them
await fs.unlink(path.join(openspecPath, 'specs', '.gitkeep'));
await fs.unlink(path.join(openspecPath, 'changes', '.gitkeep'));
await fs.unlink(path.join(openspecPath, 'changes', 'archive', '.gitkeep'));

// Re-run init (triggers extend mode since openspec dir already exists)
const initCommand2 = new InitCommand({ tools: 'claude', force: true });
await initCommand2.execute(testDir);

expect(await fileExists(path.join(openspecPath, 'specs', '.gitkeep'))).toBe(true);
expect(await fileExists(path.join(openspecPath, 'changes', '.gitkeep'))).toBe(true);
expect(await fileExists(path.join(openspecPath, 'changes', 'archive', '.gitkeep'))).toBe(true);
});
Comment on lines +68 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extend mode .gitkeep creation is not tested

The PR description states that .gitkeep files are written in both the normal init path and the extend mode path. However, this test only exercises the normal (first-run) code path — a fresh testDir means extendMode is false in createDirectoryStructure.

The extend mode branch (lines 469–478 of init.ts) has no test coverage. If the extend mode logic were broken or accidentally removed, no test would catch it.

A minimal extend-mode test would look like:

it('should create .gitkeep files in extend mode', async () => {
  const initCommand1 = new InitCommand({ tools: 'claude', force: true });
  await initCommand1.execute(testDir);

  // Simulate re-running init (extend mode: openspec dir already exists)
  const initCommand2 = new InitCommand({ tools: 'claude', force: true });
  await initCommand2.execute(testDir);

  const openspecPath = path.join(testDir, 'openspec');
  expect(await fileExists(path.join(openspecPath, 'specs', '.gitkeep'))).toBe(true);
  expect(await fileExists(path.join(openspecPath, 'changes', '.gitkeep'))).toBe(true);
  expect(await fileExists(path.join(openspecPath, 'changes', 'archive', '.gitkeep'))).toBe(true);
});


it('should create config.yaml with default schema', async () => {
const initCommand = new InitCommand({ tools: 'claude', force: true });

Expand Down