Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale 300d020529 Merge branch 'main' into TabishB/fix-init-recreate-agents 2025-10-25 20:52:46 +11:00
Tabish Bidiwale f036fbeeae test: improve extend mode test coverage and reduce duplication
- Extracted testFileRecreationInExtendMode helper to reduce code duplication
- Added test to verify existing files are preserved in extend mode
- Ensures skipExisting behavior is properly tested
2025-10-25 20:51:54 +11:00
Tabish Bidiwale 499241b525 refactor: consolidate duplicate logic in template file generation
Extracted shared logic from generateFiles() and ensureTemplateFiles()
into a new writeTemplateFiles() method with a skipExisting parameter.
This eliminates code duplication while maintaining the same behavior.
2025-10-25 19:04:56 +11:00
Tabish Bidiwale 01fded8c75 fix: recreate missing openspec template files in extend mode
The init command now checks for and recreates missing template files
(like openspec/AGENTS.md and openspec/project.md) when running in
extend mode, instead of skipping file generation entirely.

Previously, if a user deleted openspec/AGENTS.md and ran init again,
the file would not be recreated because the openspec/ directory
existed, triggering extend mode which skipped all file generation.

Changes:
- Added ensureTemplateFiles() method to check and recreate missing files
- Modified extend mode to call ensureTemplateFiles() instead of skipping
- Updated message to "Checking for missing files..." for clarity
- Added tests for recreating deleted openspec/AGENTS.md and project.md
2025-10-25 18:57:10 +11:00
2 changed files with 88 additions and 1 deletions
+24 -1
View File
@@ -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<void> {
await this.writeTemplateFiles(openspecPath, config, false);
}
private async ensureTemplateFiles(
openspecPath: string,
config: OpenSpecConfig
): Promise<void> {
await this.writeTemplateFiles(openspecPath, config, true);
}
private async writeTemplateFiles(
openspecPath: string,
config: OpenSpecConfig,
skipExisting: boolean
): Promise<void> {
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)
+64
View File
@@ -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<void> {
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<boolean> {
try {
await fs.access(filePath);