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
25 changes: 24 additions & 1 deletion src/core/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
64 changes: 64 additions & 0 deletions test/core/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down
Loading