Skip to content

Commit 246ac59

Browse files
author
Marvin Zhang
committed
feat(init): add re-initialization options and upgrade strategy for existing LeanSpec configurations
1 parent c5f4f8e commit 246ac59

File tree

1 file changed

+239
-8
lines changed

1 file changed

+239
-8
lines changed

packages/cli/src/commands/init.ts

Lines changed: 239 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -95,19 +95,227 @@ async function attemptAutoMerge(cwd: string, promptPath: string, autoExecute: bo
9595
}
9696
}
9797

98+
/**
99+
* Re-initialization strategy options
100+
*/
101+
type ReinitStrategy = 'upgrade' | 'reset-config' | 'full-reset' | 'cancel';
102+
103+
/**
104+
* Handle re-initialization when LeanSpec is already initialized
105+
*/
106+
async function handleReinitialize(cwd: string, skipPrompts: boolean, forceReinit: boolean): Promise<ReinitStrategy> {
107+
const specsDir = path.join(cwd, 'specs');
108+
let specCount = 0;
109+
110+
try {
111+
const entries = await fs.readdir(specsDir, { withFileTypes: true });
112+
specCount = entries.filter(e => e.isDirectory()).length;
113+
} catch {
114+
// specs/ doesn't exist
115+
}
116+
117+
console.log('');
118+
console.log(chalk.yellow('⚠ LeanSpec is already initialized in this directory.'));
119+
120+
if (specCount > 0) {
121+
console.log(chalk.cyan(` Found ${specCount} spec${specCount > 1 ? 's' : ''} in specs/`));
122+
}
123+
console.log('');
124+
125+
// Force flag: reset config but preserve specs (safe default)
126+
if (forceReinit) {
127+
console.log(chalk.gray('Force flag detected. Resetting configuration...'));
128+
return 'reset-config';
129+
}
130+
131+
// With -y flag, default to upgrade (safest)
132+
if (skipPrompts) {
133+
console.log(chalk.gray('Using safe upgrade (preserving all existing files)'));
134+
return 'upgrade';
135+
}
136+
137+
// Interactive mode: let user choose
138+
const strategy = await select<ReinitStrategy>({
139+
message: 'What would you like to do?',
140+
choices: [
141+
{
142+
name: 'Upgrade configuration (recommended)',
143+
value: 'upgrade',
144+
description: 'Update config to latest version. Keeps specs and AGENTS.md untouched.',
145+
},
146+
{
147+
name: 'Reset configuration',
148+
value: 'reset-config',
149+
description: 'Fresh config from template. Keeps specs/ directory.',
150+
},
151+
{
152+
name: 'Full reset',
153+
value: 'full-reset',
154+
description: 'Remove .lean-spec/, specs/, and AGENTS.md. Start completely fresh.',
155+
},
156+
{
157+
name: 'Cancel',
158+
value: 'cancel',
159+
description: 'Exit without changes.',
160+
},
161+
],
162+
});
163+
164+
// Confirm destructive actions
165+
if (strategy === 'full-reset') {
166+
const warnings: string[] = [];
167+
168+
if (specCount > 0) {
169+
warnings.push(`${specCount} spec${specCount > 1 ? 's' : ''} in specs/`);
170+
}
171+
172+
try {
173+
await fs.access(path.join(cwd, 'AGENTS.md'));
174+
warnings.push('AGENTS.md');
175+
} catch {}
176+
177+
if (warnings.length > 0) {
178+
console.log('');
179+
console.log(chalk.red('⚠ This will permanently delete:'));
180+
for (const warning of warnings) {
181+
console.log(chalk.red(` - ${warning}`));
182+
}
183+
console.log('');
184+
185+
const confirmed = await confirm({
186+
message: 'Are you sure you want to continue?',
187+
default: false,
188+
});
189+
190+
if (!confirmed) {
191+
console.log(chalk.gray('Cancelled.'));
192+
return 'cancel';
193+
}
194+
}
195+
196+
// Perform full reset
197+
console.log(chalk.gray('Performing full reset...'));
198+
199+
// Remove .lean-spec/
200+
await fs.rm(path.join(cwd, '.lean-spec'), { recursive: true, force: true });
201+
console.log(chalk.gray(' Removed .lean-spec/'));
202+
203+
// Remove specs/
204+
try {
205+
await fs.rm(specsDir, { recursive: true, force: true });
206+
console.log(chalk.gray(' Removed specs/'));
207+
} catch {}
208+
209+
// Remove AGENTS.md and symlinks
210+
for (const file of ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']) {
211+
try {
212+
await fs.rm(path.join(cwd, file), { force: true });
213+
console.log(chalk.gray(` Removed ${file}`));
214+
} catch {}
215+
}
216+
}
217+
218+
return strategy;
219+
}
220+
221+
/**
222+
* Upgrade existing LeanSpec configuration
223+
* This preserves all user content (specs, AGENTS.md) while updating config
224+
*/
225+
async function upgradeConfig(cwd: string): Promise<void> {
226+
const configPath = path.join(cwd, '.lean-spec', 'config.json');
227+
228+
// Read existing config
229+
let existingConfig: LeanSpecConfig;
230+
try {
231+
const content = await fs.readFile(configPath, 'utf-8');
232+
existingConfig = JSON.parse(content);
233+
} catch {
234+
console.error(chalk.red('Error reading existing config'));
235+
process.exit(1);
236+
}
237+
238+
// Load standard template config as reference for defaults
239+
const templateConfigPath = path.join(TEMPLATES_DIR, 'standard', 'config.json');
240+
let templateConfig: LeanSpecConfig;
241+
try {
242+
const content = await fs.readFile(templateConfigPath, 'utf-8');
243+
templateConfig = JSON.parse(content).config;
244+
} catch {
245+
console.error(chalk.red('Error reading template config'));
246+
process.exit(1);
247+
}
248+
249+
// Merge configs - preserve user settings, add new defaults
250+
const upgradedConfig: LeanSpecConfig = {
251+
...templateConfig,
252+
...existingConfig,
253+
// Deep merge structure
254+
structure: {
255+
...templateConfig.structure,
256+
...existingConfig.structure,
257+
},
258+
};
259+
260+
// Ensure templates directory exists
261+
const templatesDir = path.join(cwd, '.lean-spec', 'templates');
262+
try {
263+
await fs.mkdir(templatesDir, { recursive: true });
264+
} catch {}
265+
266+
// Check if templates need updating
267+
const templateFiles = ['spec-template.md'];
268+
let templatesUpdated = false;
269+
270+
for (const file of templateFiles) {
271+
const destPath = path.join(templatesDir, file);
272+
try {
273+
await fs.access(destPath);
274+
// File exists, don't overwrite
275+
} catch {
276+
// File doesn't exist, copy from template
277+
const srcPath = path.join(TEMPLATES_DIR, 'standard', 'files', 'README.md');
278+
try {
279+
await fs.copyFile(srcPath, destPath);
280+
templatesUpdated = true;
281+
console.log(chalk.green(`✓ Added missing template: ${file}`));
282+
} catch {}
283+
}
284+
}
285+
286+
// Save upgraded config
287+
await saveConfig(upgradedConfig, cwd);
288+
289+
console.log('');
290+
console.log(chalk.green('✓ Configuration upgraded!'));
291+
console.log('');
292+
console.log(chalk.gray('What was updated:'));
293+
console.log(chalk.gray(' - Config merged with latest defaults'));
294+
if (templatesUpdated) {
295+
console.log(chalk.gray(' - Missing templates added'));
296+
}
297+
console.log('');
298+
console.log(chalk.gray('What was preserved:'));
299+
console.log(chalk.gray(' - Your specs/ directory'));
300+
console.log(chalk.gray(' - Your AGENTS.md'));
301+
console.log(chalk.gray(' - Your custom settings'));
302+
console.log('');
303+
}
304+
98305
/**
99306
* Init command - initialize LeanSpec in current directory
100307
*/
101308
export function initCommand(): Command {
102309
return new Command('init')
103310
.description('Initialize LeanSpec in current directory')
104311
.option('-y, --yes', 'Skip prompts and use defaults (quick start with standard template)')
312+
.option('-f, --force', 'Force re-initialization (resets config, keeps specs)')
105313
.option('--template <name>', 'Use specific template (standard or detailed)')
106314
.option('--example [name]', 'Scaffold an example project for tutorials (interactive if no name provided)')
107315
.option('--name <dirname>', 'Custom directory name for example project')
108316
.option('--list', 'List available example projects')
109317
.option('--agent-tools <tools>', 'AI tools to create symlinks for (comma-separated: claude,gemini,copilot or "all" or "none")')
110-
.action(async (options: { yes?: boolean; template?: string; example?: string; name?: string; list?: boolean; agentTools?: string }) => {
318+
.action(async (options: { yes?: boolean; force?: boolean; template?: string; example?: string; name?: string; list?: boolean; agentTools?: string }) => {
111319
if (options.list) {
112320
await listExamples();
113321
return;
@@ -118,21 +326,44 @@ export function initCommand(): Command {
118326
return;
119327
}
120328

121-
await initProject(options.yes, options.template, options.agentTools);
329+
await initProject(options.yes, options.template, options.agentTools, options.force);
122330
});
123331
}
124332

125-
export async function initProject(skipPrompts = false, templateOption?: string, agentToolsOption?: string): Promise<void> {
333+
export async function initProject(skipPrompts = false, templateOption?: string, agentToolsOption?: string, forceReinit = false): Promise<void> {
126334
const cwd = process.cwd();
127335

128336
// Check if already initialized
337+
const configPath = path.join(cwd, '.lean-spec', 'config.json');
338+
let isAlreadyInitialized = false;
339+
129340
try {
130-
await fs.access(path.join(cwd, '.lean-spec', 'config.json'));
131-
console.log(chalk.yellow('⚠ LeanSpec already initialized in this directory.'));
132-
console.log(chalk.gray('To reinitialize, delete .lean-spec/ directory first.'));
133-
return;
341+
await fs.access(configPath);
342+
isAlreadyInitialized = true;
134343
} catch {
135-
// Not initialized, continue
344+
// Not initialized, continue with fresh init
345+
}
346+
347+
// Handle re-initialization
348+
if (isAlreadyInitialized) {
349+
const strategy = await handleReinitialize(cwd, skipPrompts, forceReinit);
350+
351+
if (strategy === 'cancel') {
352+
return;
353+
}
354+
355+
if (strategy === 'upgrade') {
356+
await upgradeConfig(cwd);
357+
return;
358+
}
359+
360+
// For 'reset-config' and 'full-reset', we continue with normal init flow
361+
// but 'full-reset' will have already cleaned up the directory
362+
if (strategy === 'reset-config') {
363+
// Just remove config, keep specs
364+
await fs.rm(path.join(cwd, '.lean-spec'), { recursive: true, force: true });
365+
console.log(chalk.gray('Removed .lean-spec/ configuration'));
366+
}
136367
}
137368

138369
console.log('');

0 commit comments

Comments
 (0)