diff --git a/src/core/init.ts b/src/core/init.ts index ab3dc55b..05e9be42 100644 --- a/src/core/init.ts +++ b/src/core/init.ts @@ -428,9 +428,11 @@ export class InitCommand { } else { ora({ stream: process.stdout }).info( PALETTE.midGray( - 'ℹ OpenSpec already initialized. Skipping base scaffolding.' + 'ℹ OpenSpec already initialized. Checking for missing files...' ) ); + await this.createDirectoryStructure(openspecPath); + await this.ensureTemplateFiles(openspecPath, config); } // Step 2: Configure AI tools @@ -719,6 +721,21 @@ export class InitCommand { private async generateFiles( openspecPath: string, config: OpenSpecConfig + ): Promise { + await this.writeTemplateFiles(openspecPath, config, false); + } + + private async ensureTemplateFiles( + openspecPath: string, + config: OpenSpecConfig + ): Promise { + await this.writeTemplateFiles(openspecPath, config, true); + } + + private async writeTemplateFiles( + openspecPath: string, + config: OpenSpecConfig, + skipExisting: boolean ): Promise { const context: ProjectContext = { // Could be enhanced with prompts for project details @@ -728,6 +745,12 @@ export class InitCommand { for (const template of templates) { const filePath = path.join(openspecPath, template.path); + + // Skip if file exists and we're in skipExisting mode + if (skipExisting && (await FileSystemUtils.fileExists(filePath))) { + continue; + } + const content = typeof template.content === 'function' ? template.content(context) diff --git a/test/core/init.test.ts b/test/core/init.test.ts index 4c7a91d4..b6d8e35e 100644 --- a/test/core/init.test.ts +++ b/test/core/init.test.ts @@ -579,6 +579,44 @@ describe('InitCommand', () => { await expect(initCommand.execute(testDir)).resolves.toBeUndefined(); }); + it('should recreate deleted openspec/AGENTS.md in extend mode', async () => { + await testFileRecreationInExtendMode( + testDir, + initCommand, + 'openspec/AGENTS.md', + 'OpenSpec Instructions' + ); + }); + + it('should recreate deleted openspec/project.md in extend mode', async () => { + await testFileRecreationInExtendMode( + testDir, + initCommand, + 'openspec/project.md', + 'Project Context' + ); + }); + + it('should preserve existing template files in extend mode', async () => { + queueSelections('claude', DONE, DONE); + + // First init + await initCommand.execute(testDir); + + const agentsPath = path.join(testDir, 'openspec', 'AGENTS.md'); + const customContent = '# My Custom AGENTS Content\nDo not overwrite this!'; + + // Modify the file with custom content + await fs.writeFile(agentsPath, customContent); + + // Run init again - should NOT overwrite + await initCommand.execute(testDir); + + const content = await fs.readFile(agentsPath, 'utf-8'); + expect(content).toBe(customContent); + expect(content).not.toContain('OpenSpec Instructions'); + }); + it('should handle non-existent target directory', async () => { queueSelections('claude', DONE); @@ -1160,6 +1198,32 @@ describe('InitCommand', () => { }); }); +async function testFileRecreationInExtendMode( + testDir: string, + initCommand: InitCommand, + relativePath: string, + expectedContent: string +): Promise { + queueSelections('claude', DONE, DONE); + + // First init + await initCommand.execute(testDir); + + const filePath = path.join(testDir, relativePath); + expect(await fileExists(filePath)).toBe(true); + + // Delete the file + await fs.unlink(filePath); + expect(await fileExists(filePath)).toBe(false); + + // Run init again - should recreate the file + await initCommand.execute(testDir); + expect(await fileExists(filePath)).toBe(true); + + const content = await fs.readFile(filePath, 'utf-8'); + expect(content).toContain(expectedContent); +} + async function fileExists(filePath: string): Promise { try { await fs.access(filePath);