diff --git a/src/core/init.ts b/src/core/init.ts index 95728dc7e..b968e4211 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -453,40 +453,47 @@ export class InitCommand { // ═══════════════════════════════════════════════════════════ private async createDirectoryStructure(openspecPath: string, extendMode: boolean): Promise { + 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 { + 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 // ═══════════════════════════════════════════════════════════ diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 6af92aed2..a6e37c0d8 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -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); + }); + it('should create config.yaml with default schema', async () => { const initCommand = new InitCommand({ tools: 'claude', force: true });