container-superposition 0.1.4 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/README.md +74 -1370
  2. package/dist/scripts/init.js +350 -185
  3. package/dist/scripts/init.js.map +1 -1
  4. package/dist/tool/commands/adopt.d.ts +63 -0
  5. package/dist/tool/commands/adopt.d.ts.map +1 -0
  6. package/dist/tool/commands/adopt.js +1104 -0
  7. package/dist/tool/commands/adopt.js.map +1 -0
  8. package/dist/tool/commands/hash.d.ts +36 -0
  9. package/dist/tool/commands/hash.d.ts.map +1 -0
  10. package/dist/tool/commands/hash.js +242 -0
  11. package/dist/tool/commands/hash.js.map +1 -0
  12. package/dist/tool/commands/plan.d.ts +2 -0
  13. package/dist/tool/commands/plan.d.ts.map +1 -1
  14. package/dist/tool/commands/plan.js +262 -42
  15. package/dist/tool/commands/plan.js.map +1 -1
  16. package/dist/tool/schema/project-config.d.ts +17 -0
  17. package/dist/tool/schema/project-config.d.ts.map +1 -0
  18. package/dist/tool/schema/project-config.js +441 -0
  19. package/dist/tool/schema/project-config.js.map +1 -0
  20. package/dist/tool/schema/types.d.ts +39 -1
  21. package/dist/tool/schema/types.d.ts.map +1 -1
  22. package/dist/tool/utils/backup.d.ts +23 -0
  23. package/dist/tool/utils/backup.d.ts.map +1 -0
  24. package/dist/tool/utils/backup.js +123 -0
  25. package/dist/tool/utils/backup.js.map +1 -0
  26. package/docs/README.md +12 -2
  27. package/docs/adopt.md +202 -0
  28. package/docs/custom-patches.md +1 -1
  29. package/docs/discovery-commands.md +55 -3
  30. package/docs/examples.md +40 -6
  31. package/docs/filesystem-contract.md +58 -0
  32. package/docs/hash.md +183 -0
  33. package/docs/minimal-and-editor.md +1 -1
  34. package/docs/overlays.md +70 -0
  35. package/docs/presets-architecture.md +1 -1
  36. package/docs/presets.md +1 -1
  37. package/docs/publishing.md +36 -23
  38. package/docs/security.md +43 -0
  39. package/docs/specs/001-verbose-plan-graph/checklists/requirements.md +36 -0
  40. package/docs/specs/001-verbose-plan-graph/contracts/plan-verbose-output.md +96 -0
  41. package/docs/specs/001-verbose-plan-graph/data-model.md +111 -0
  42. package/docs/specs/001-verbose-plan-graph/plan.md +127 -0
  43. package/docs/specs/001-verbose-plan-graph/quickstart.md +106 -0
  44. package/docs/specs/001-verbose-plan-graph/research.md +100 -0
  45. package/docs/specs/001-verbose-plan-graph/spec.md +128 -0
  46. package/docs/specs/001-verbose-plan-graph/tasks.md +223 -0
  47. package/docs/specs/002-superposition-config-file/checklists/requirements.md +36 -0
  48. package/docs/specs/002-superposition-config-file/contracts/init-project-config.md +98 -0
  49. package/docs/specs/002-superposition-config-file/data-model.md +126 -0
  50. package/docs/specs/002-superposition-config-file/plan.md +213 -0
  51. package/docs/specs/002-superposition-config-file/quickstart.md +140 -0
  52. package/docs/specs/002-superposition-config-file/research.md +144 -0
  53. package/docs/specs/002-superposition-config-file/spec.md +136 -0
  54. package/docs/specs/002-superposition-config-file/tasks.md +215 -0
  55. package/docs/team-workflow.md +33 -1
  56. package/docs/workflows.md +139 -0
  57. package/features/cross-distro-packages/README.md +18 -0
  58. package/features/cross-distro-packages/devcontainer-feature.json +3 -3
  59. package/features/cross-distro-packages/install.sh +49 -7
  60. package/overlays/.presets/sdd.yml +84 -0
  61. package/overlays/README.md +7 -1
  62. package/overlays/amp/README.md +70 -0
  63. package/overlays/amp/devcontainer.patch.json +3 -0
  64. package/overlays/amp/overlay.yml +15 -0
  65. package/overlays/amp/setup.sh +21 -0
  66. package/overlays/amp/verify.sh +21 -0
  67. package/overlays/claude-code/README.md +83 -0
  68. package/overlays/claude-code/devcontainer.patch.json +3 -0
  69. package/overlays/claude-code/overlay.yml +15 -0
  70. package/overlays/claude-code/setup.sh +21 -0
  71. package/overlays/claude-code/verify.sh +21 -0
  72. package/overlays/gemini-cli/README.md +77 -0
  73. package/overlays/gemini-cli/devcontainer.patch.json +3 -0
  74. package/overlays/gemini-cli/overlay.yml +15 -0
  75. package/overlays/gemini-cli/setup.sh +21 -0
  76. package/overlays/gemini-cli/verify.sh +21 -0
  77. package/overlays/opencode/README.md +76 -0
  78. package/overlays/opencode/devcontainer.patch.json +3 -0
  79. package/overlays/opencode/overlay.yml +14 -0
  80. package/overlays/opencode/setup.sh +21 -0
  81. package/overlays/opencode/verify.sh +21 -0
  82. package/overlays/pandoc/README.md +279 -0
  83. package/overlays/pandoc/devcontainer.patch.json +14 -0
  84. package/overlays/pandoc/overlay.yml +19 -0
  85. package/overlays/pandoc/setup.sh +94 -0
  86. package/overlays/pandoc/verify.sh +13 -0
  87. package/overlays/spec-kit/README.md +181 -0
  88. package/overlays/spec-kit/devcontainer.patch.json +6 -0
  89. package/overlays/spec-kit/overlay.yml +19 -0
  90. package/overlays/spec-kit/setup.sh +45 -0
  91. package/overlays/spec-kit/verify.sh +33 -0
  92. package/overlays/windsurf-cli/README.md +69 -0
  93. package/overlays/windsurf-cli/devcontainer.patch.json +3 -0
  94. package/overlays/windsurf-cli/overlay.yml +15 -0
  95. package/overlays/windsurf-cli/setup.sh +21 -0
  96. package/overlays/windsurf-cli/verify.sh +21 -0
  97. package/package.json +1 -1
  98. package/tool/schema/config.schema.json +138 -9
@@ -2,7 +2,6 @@
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
- import { execSync } from 'child_process';
6
5
  import { Command } from 'commander';
7
6
  import chalk from 'chalk';
8
7
  import boxen from 'boxen';
@@ -15,11 +14,14 @@ import { listCommand } from '../tool/commands/list.js';
15
14
  import { explainCommand } from '../tool/commands/explain.js';
16
15
  import { planCommand } from '../tool/commands/plan.js';
17
16
  import { doctorCommand } from '../tool/commands/doctor.js';
17
+ import { adoptCommand } from '../tool/commands/adopt.js';
18
+ import { hashCommand } from '../tool/commands/hash.js';
18
19
  import { getIncompatibleOverlays, DEPLOYMENT_TARGETS } from '../tool/schema/deployment-targets.js';
19
20
  import { migrateManifest, needsMigration, detectManifestVersion, isVersionSupported, CURRENT_MANIFEST_VERSION, SUPPORTED_MANIFEST_VERSIONS, } from '../tool/schema/manifest-migrations.js';
20
21
  import { getToolVersion } from '../tool/utils/version.js';
21
22
  import { printSummary } from '../tool/utils/summary.js';
22
- import { appendGitignoreSection } from '../tool/utils/gitignore.js';
23
+ import { buildAnswersFromProjectConfig, loadProjectConfig, writeProjectConfigCustomizations, } from '../tool/schema/project-config.js';
24
+ import { isInsideGitRepo, createBackup, ensureBackupPatternsInGitignore, } from '../tool/utils/backup.js';
23
25
  // Get __dirname equivalent in ESM
24
26
  const __filename = fileURLToPath(import.meta.url);
25
27
  const __dirname = path.dirname(__filename);
@@ -65,6 +67,100 @@ function loadPresetDefinition(presetId) {
65
67
  const content = fs.readFileSync(presetPath, 'utf8');
66
68
  return yaml.load(content);
67
69
  }
70
+ function categorizeOverlayIds(overlayIds, config) {
71
+ const language = [];
72
+ const database = [];
73
+ const observability = [];
74
+ const cloudTools = [];
75
+ const devTools = [];
76
+ const overlayMap = new Map(config.overlays.map((o) => [o.id, o]));
77
+ for (const id of overlayIds) {
78
+ const overlay = overlayMap.get(id);
79
+ if (!overlay)
80
+ continue;
81
+ switch (overlay.category) {
82
+ case 'language':
83
+ language.push(id);
84
+ break;
85
+ case 'database':
86
+ database.push(id);
87
+ break;
88
+ case 'observability':
89
+ observability.push(id);
90
+ break;
91
+ case 'cloud':
92
+ cloudTools.push(id);
93
+ break;
94
+ case 'dev':
95
+ devTools.push(id);
96
+ break;
97
+ }
98
+ }
99
+ return { language, database, observability, cloudTools, devTools };
100
+ }
101
+ function mergeUnique(left, right) {
102
+ const merged = [...(left ?? []), ...(right ?? [])];
103
+ return merged.length > 0 ? [...new Set(merged)] : undefined;
104
+ }
105
+ function expandPresetWithDefaults(presetId, stack, providedChoices = {}) {
106
+ const preset = loadPresetDefinition(presetId);
107
+ if (!preset) {
108
+ throw new Error(`Preset definition not found for ${presetId}`);
109
+ }
110
+ const overlays = [...preset.selects.required];
111
+ const choices = {};
112
+ if (preset.selects.userChoice) {
113
+ for (const [key, choice] of Object.entries(preset.selects.userChoice)) {
114
+ const selectedOption = providedChoices[key] ?? choice.defaultOption;
115
+ if (!selectedOption || !choice.options.includes(selectedOption)) {
116
+ const valid = choice.options.join(', ');
117
+ throw new Error(`Preset choice '${key}' must be one of: ${valid}`);
118
+ }
119
+ overlays.push(selectedOption);
120
+ choices[key] = selectedOption;
121
+ }
122
+ }
123
+ if (preset.parameters) {
124
+ for (const [key, param] of Object.entries(preset.parameters)) {
125
+ const selectedId = providedChoices[key] ?? param.default;
126
+ const selectedOption = param.options.find((option) => option.id === selectedId);
127
+ if (!selectedOption) {
128
+ const valid = param.options.map((option) => option.id).join(', ');
129
+ throw new Error(`Preset parameter '${key}' must be one of: ${valid}`);
130
+ }
131
+ overlays.push(...selectedOption.overlays);
132
+ choices[key] = selectedId;
133
+ }
134
+ }
135
+ const uniqueOverlays = [...new Set(overlays)];
136
+ let resolvedGlueConfig = preset.glueConfig;
137
+ if (resolvedGlueConfig?.environment) {
138
+ const resolvedEnv = {};
139
+ for (const [envKey, envValue] of Object.entries(resolvedGlueConfig.environment)) {
140
+ resolvedEnv[envKey] = envValue.replace(/\{\{parameters\.(\w+)\.id\}\}/g, (_match, paramKey) => choices[paramKey] ?? _match);
141
+ }
142
+ resolvedGlueConfig = { ...resolvedGlueConfig, environment: resolvedEnv };
143
+ }
144
+ return { overlays: uniqueOverlays, choices, glueConfig: resolvedGlueConfig };
145
+ }
146
+ function applyPresetSelections(answers) {
147
+ if (!answers.preset) {
148
+ return answers;
149
+ }
150
+ const expansion = expandPresetWithDefaults(answers.preset, answers.stack ?? 'plain', answers.presetChoices ?? {});
151
+ const categories = categorizeOverlayIds(expansion.overlays, loadOverlaysConfigWrapper());
152
+ return {
153
+ ...answers,
154
+ language: mergeUnique(categories.language, answers.language),
155
+ database: mergeUnique(categories.database, answers.database),
156
+ observability: mergeUnique(categories.observability, answers.observability),
157
+ cloudTools: mergeUnique(categories.cloudTools, answers.cloudTools),
158
+ devTools: mergeUnique(categories.devTools, answers.devTools),
159
+ playwright: answers.playwright ?? expansion.overlays.includes('playwright'),
160
+ presetChoices: Object.keys(expansion.choices).length > 0 ? expansion.choices : undefined,
161
+ presetGlueConfig: expansion.glueConfig,
162
+ };
163
+ }
68
164
  /**
69
165
  * Expand a preset into a list of overlay IDs with user choices resolved
70
166
  */
@@ -141,7 +237,17 @@ async function expandPreset(presetId, stack, preProvidedChoices = {}) {
141
237
  // Deduplicate overlays
142
238
  const uniqueOverlays = [...new Set(overlays)];
143
239
  console.log(chalk.dim(`✓ Preset will include: ${uniqueOverlays.join(', ')}\n`));
144
- return { overlays: uniqueOverlays, choices, glueConfig: preset.glueConfig };
240
+ // Apply template substitution to glueConfig.environment values.
241
+ // Replaces {{parameters.<key>.id}} with the selected choice for <key>.
242
+ let resolvedGlueConfig = preset.glueConfig;
243
+ if (resolvedGlueConfig?.environment) {
244
+ const resolvedEnv = {};
245
+ for (const [envKey, envValue] of Object.entries(resolvedGlueConfig.environment)) {
246
+ resolvedEnv[envKey] = envValue.replace(/\{\{parameters\.(\w+)\.id\}\}/g, (_match, paramKey) => choices[paramKey] ?? _match);
247
+ }
248
+ resolvedGlueConfig = { ...resolvedGlueConfig, environment: resolvedEnv };
249
+ }
250
+ return { overlays: uniqueOverlays, choices, glueConfig: resolvedGlueConfig };
145
251
  }
146
252
  /**
147
253
  * Search for manifest file in multiple locations
@@ -164,6 +270,16 @@ function findManifestFile(manifestPath) {
164
270
  }
165
271
  return null;
166
272
  }
273
+ function findDefaultRegenManifest(outputPath = './.devcontainer') {
274
+ const manifestSearchPaths = ['superposition.json', path.join(outputPath, 'superposition.json')];
275
+ for (const searchPath of manifestSearchPaths) {
276
+ const resolvedPath = path.resolve(searchPath);
277
+ if (fs.existsSync(resolvedPath)) {
278
+ return resolvedPath;
279
+ }
280
+ }
281
+ return null;
282
+ }
167
283
  /**
168
284
  * Load and validate manifest file
169
285
  */
@@ -210,127 +326,6 @@ function loadManifest(manifestPath) {
210
326
  return null;
211
327
  }
212
328
  }
213
- /**
214
- * Detect whether a directory (or any of its parents) is inside a git repository.
215
- * First tries `git rev-parse --git-dir`; if git is unavailable, falls back to
216
- * walking up the directory tree looking for a `.git` entry.
217
- */
218
- function isInsideGitRepo(dirPath) {
219
- try {
220
- execSync('git rev-parse --git-dir', { cwd: dirPath, stdio: 'ignore' });
221
- return true;
222
- }
223
- catch {
224
- // git command failed (not a repo) or git is not installed — walk up looking for .git
225
- let current = path.resolve(dirPath);
226
- while (true) {
227
- if (fs.existsSync(path.join(current, '.git'))) {
228
- return true;
229
- }
230
- const parent = path.dirname(current);
231
- if (parent === current) {
232
- break; // reached filesystem root
233
- }
234
- current = parent;
235
- }
236
- return false;
237
- }
238
- }
239
- /**
240
- * Create timestamped backup of existing devcontainer and manifest
241
- */
242
- async function createBackup(outputPath, backupDir) {
243
- // Check for devcontainer files to backup
244
- const devcontainerJsonPath = path.join(outputPath, 'devcontainer.json');
245
- const dockerComposePath = path.join(outputPath, 'docker-compose.yml');
246
- const devcontainerSubdir = path.join(outputPath, '.devcontainer');
247
- const manifestPath = path.join(outputPath, 'superposition.json');
248
- // Determine what exists
249
- const hasDevcontainerJson = fs.existsSync(devcontainerJsonPath);
250
- const hasDockerCompose = fs.existsSync(dockerComposePath);
251
- const hasDevcontainerSubdir = fs.existsSync(devcontainerSubdir) && fs.statSync(devcontainerSubdir).isDirectory();
252
- const hasManifest = fs.existsSync(manifestPath);
253
- if (!hasDevcontainerJson && !hasDockerCompose && !hasDevcontainerSubdir && !hasManifest) {
254
- return null; // Nothing to backup
255
- }
256
- // Create timestamp
257
- const timestamp = new Date()
258
- .toISOString()
259
- .replace(/:/g, '-')
260
- .replace(/\..+/, '')
261
- .replace('T', '-');
262
- // Determine backup location - create next to outputPath, not inside it
263
- const resolvedOutputPath = path.resolve(outputPath);
264
- const outputParentDir = path.dirname(resolvedOutputPath);
265
- const outputBaseName = path.basename(resolvedOutputPath);
266
- const backupBaseName = outputBaseName === '.devcontainer' ? '.devcontainer' : outputBaseName;
267
- const backupPath = backupDir
268
- ? path.resolve(backupDir)
269
- : path.join(outputParentDir, `${backupBaseName}.backup-${timestamp}`);
270
- // Create backup directory
271
- fs.mkdirSync(backupPath, { recursive: true });
272
- // Backup files and directories
273
- if (hasDevcontainerJson) {
274
- fs.copyFileSync(devcontainerJsonPath, path.join(backupPath, 'devcontainer.json'));
275
- }
276
- if (hasDockerCompose) {
277
- fs.copyFileSync(dockerComposePath, path.join(backupPath, 'docker-compose.yml'));
278
- }
279
- if (hasDevcontainerSubdir) {
280
- const destDir = path.join(backupPath, '.devcontainer');
281
- await copyDirectory(devcontainerSubdir, destDir);
282
- }
283
- if (hasManifest) {
284
- fs.copyFileSync(manifestPath, path.join(backupPath, 'superposition.json'));
285
- }
286
- // Also backup other common devcontainer files
287
- const otherFiles = ['.env', '.env.example', '.gitignore', 'features', 'scripts'];
288
- for (const file of otherFiles) {
289
- const srcPath = path.join(outputPath, file);
290
- if (fs.existsSync(srcPath)) {
291
- const destPath = path.join(backupPath, file);
292
- if (fs.statSync(srcPath).isDirectory()) {
293
- await copyDirectory(srcPath, destPath);
294
- }
295
- else {
296
- fs.copyFileSync(srcPath, destPath);
297
- }
298
- }
299
- }
300
- return backupPath;
301
- }
302
- /**
303
- * Recursively copy directory
304
- */
305
- async function copyDirectory(src, dest) {
306
- fs.mkdirSync(dest, { recursive: true });
307
- const entries = fs.readdirSync(src, { withFileTypes: true });
308
- for (const entry of entries) {
309
- const srcPath = path.join(src, entry.name);
310
- const destPath = path.join(dest, entry.name);
311
- if (entry.isDirectory()) {
312
- await copyDirectory(srcPath, destPath);
313
- }
314
- else {
315
- fs.copyFileSync(srcPath, destPath);
316
- }
317
- }
318
- }
319
- /**
320
- * Ensure backup patterns are in .gitignore
321
- */
322
- function ensureBackupPatternsInGitignore(outputPath) {
323
- const projectRoot = path.dirname(path.resolve(outputPath));
324
- const gitignorePath = path.join(projectRoot, '.gitignore');
325
- const written = appendGitignoreSection(gitignorePath, 'container-superposition backups', [
326
- '.devcontainer.backup-*/',
327
- '*.backup-*',
328
- 'superposition.json.backup-*',
329
- ]);
330
- if (written) {
331
- console.log(chalk.dim(' 📝 Updated .gitignore with backup patterns'));
332
- }
333
- }
334
329
  /**
335
330
  * Build checkbox choices for overlay selection with optional pre-selection
336
331
  */
@@ -360,7 +355,7 @@ function buildOverlayChoices(config, stack, categoryList, preselected) {
360
355
  /**
361
356
  * Interactive questionnaire with modern checkbox selections
362
357
  */
363
- async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetChoices) {
358
+ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetChoices, defaultAnswers) {
364
359
  const config = loadOverlaysConfigWrapper();
365
360
  // Pretty banner
366
361
  console.log('\n' +
@@ -448,7 +443,7 @@ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetCho
448
443
  value: t.id,
449
444
  description: t.description,
450
445
  })),
451
- default: manifest?.baseTemplate,
446
+ default: manifest?.baseTemplate || defaultAnswers?.stack,
452
447
  }));
453
448
  // If using preset, expand it now (pass pre-provided choices to skip those prompts)
454
449
  if (usePreset && selectedPresetId) {
@@ -473,7 +468,15 @@ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetCho
473
468
  // Check if manifest has a custom image or a known base image
474
469
  const knownBaseImageIds = config.base_images.map((img) => img.id);
475
470
  const manifestBaseImageIsKnown = manifest?.baseImage && knownBaseImageIds.includes(manifest.baseImage);
476
- const manifestDefaultBaseImage = manifestBaseImageIsKnown ? manifest.baseImage : 'custom';
471
+ const manifestDefaultBaseImage = manifestBaseImageIsKnown
472
+ ? manifest.baseImage
473
+ : manifest?.baseImage
474
+ ? 'custom'
475
+ : undefined;
476
+ const defaultBaseImage = manifestDefaultBaseImage ||
477
+ (defaultAnswers?.baseImage === 'custom' && defaultAnswers.customImage
478
+ ? 'custom'
479
+ : defaultAnswers?.baseImage);
477
480
  const baseImage = (await select({
478
481
  message: 'Select base image:',
479
482
  choices: config.base_images.map((img) => ({
@@ -481,16 +484,17 @@ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetCho
481
484
  value: img.id,
482
485
  description: img.description,
483
486
  })),
484
- default: manifestDefaultBaseImage,
487
+ default: defaultBaseImage,
485
488
  }));
486
489
  // Question 2a: If custom, ask for image name
487
490
  let customImage;
488
491
  if (baseImage === 'custom') {
489
492
  // If manifest has a custom image, use it as default
490
493
  const manifestCustomImage = !manifestBaseImageIsKnown && manifest?.baseImage ? manifest.baseImage : undefined;
494
+ const defaultCustomImage = manifestCustomImage || defaultAnswers?.customImage;
491
495
  customImage = await input({
492
496
  message: 'Enter custom Docker image (e.g., ubuntu:22.04):',
493
- default: manifestCustomImage,
497
+ default: defaultCustomImage,
494
498
  validate: (value) => {
495
499
  if (!value || value.trim() === '') {
496
500
  return 'Image name is required';
@@ -606,7 +610,15 @@ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetCho
606
610
  else {
607
611
  // Custom mode: Normal overlay selection
608
612
  console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm\n'));
609
- const choices = buildOverlayChoices(config, stack, categoryList, []);
613
+ const preselectedDefaults = [
614
+ ...(defaultAnswers?.language ?? []),
615
+ ...(defaultAnswers?.database ?? []),
616
+ ...(defaultAnswers?.observability ?? []),
617
+ ...(defaultAnswers?.cloudTools ?? []),
618
+ ...(defaultAnswers?.devTools ?? []),
619
+ ...(defaultAnswers?.playwright ? ['playwright'] : []),
620
+ ];
621
+ const choices = buildOverlayChoices(config, stack, categoryList, preselectedDefaults);
610
622
  userSelection = await checkbox({
611
623
  message: 'Select overlays to include:',
612
624
  choices,
@@ -683,11 +695,11 @@ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetCho
683
695
  // Question 4: Container name
684
696
  const containerName = await input({
685
697
  message: 'Container/project name (optional):',
686
- default: manifest?.containerName || '',
698
+ default: manifest?.containerName || defaultAnswers?.containerName || '',
687
699
  });
688
700
  // Question 5: Output path
689
701
  // If manifest provided, default to its location; otherwise use ./.devcontainer
690
- const defaultOutput = manifestDir || './.devcontainer';
702
+ const defaultOutput = manifestDir || defaultAnswers?.outputPath || './.devcontainer';
691
703
  const outputPath = await input({
692
704
  message: 'Output path:',
693
705
  default: defaultOutput,
@@ -695,7 +707,11 @@ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetCho
695
707
  // Question 6: Port offset (optional, for running multiple instances)
696
708
  const portOffsetInput = await input({
697
709
  message: 'Port offset (leave empty for default ports, e.g., 100 to avoid conflicts):',
698
- default: manifest?.portOffset ? String(manifest.portOffset) : '',
710
+ default: manifest?.portOffset !== undefined
711
+ ? String(manifest.portOffset)
712
+ : defaultAnswers?.portOffset !== undefined
713
+ ? String(defaultAnswers.portOffset)
714
+ : '',
699
715
  });
700
716
  const portOffset = portOffsetInput ? parseInt(portOffsetInput, 10) : undefined;
701
717
  // Parse selected overlays into categories
@@ -790,7 +806,10 @@ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetCho
790
806
  observability,
791
807
  outputPath,
792
808
  portOffset,
793
- target,
809
+ target: target ?? defaultAnswers?.target,
810
+ minimal: defaultAnswers?.minimal,
811
+ editor: defaultAnswers?.editor,
812
+ customizations: defaultAnswers?.customizations,
794
813
  };
795
814
  }
796
815
  catch (error) {
@@ -960,8 +979,10 @@ async function parseCliArgs() {
960
979
  program
961
980
  .command('init', { isDefault: true })
962
981
  .description('Initialize a new devcontainer configuration')
982
+ .option('--from-project', 'Load configuration from the repository project file')
983
+ .option('--project-root <path>', 'Run project-file and manifest discovery relative to a different repository root')
963
984
  .option('--from-manifest <path>', 'Load configuration from existing superposition.json manifest')
964
- .option('--no-interactive', 'Use manifest values directly without questionnaire (requires --from-manifest)')
985
+ .option('--no-interactive', 'Use persisted input values directly without questionnaire')
965
986
  .option('--backup', 'Force or suppress backup; default is --no-backup inside a git repo, --backup outside')
966
987
  .option('--backup-dir <path>', 'Custom backup directory location')
967
988
  .option('--stack <type>', 'Base template: plain, compose')
@@ -979,47 +1000,33 @@ async function parseCliArgs() {
979
1000
  .option('--write-manifest-only', 'Generate only superposition.json manifest without creating .devcontainer/ files')
980
1001
  .option('--preset <id>', 'Start from a preset (e.g., web-api, microservice)')
981
1002
  .option('--preset-param <value>', 'Set a preset parameter value (format: key=value, can be repeated)', (value, previous) => previous.concat([value]), [])
982
- .action((options) => {
1003
+ .action((options, command) => {
983
1004
  // Store options for main() to process
984
- initOptions = options;
1005
+ initOptions = {
1006
+ ...options,
1007
+ commandName: 'init',
1008
+ _targetSource: command.getOptionValueSource('target'),
1009
+ _editorSource: command.getOptionValueSource('editor'),
1010
+ };
985
1011
  });
986
1012
  // Regen command
987
1013
  program
988
1014
  .command('regen')
989
- .description('Regenerate devcontainer from existing superposition.json manifest')
1015
+ .description('Regenerate devcontainer from a project file or existing superposition.json manifest')
1016
+ .option('--from-project', 'Load configuration from the repository project file')
1017
+ .option('--project-root <path>', 'Run project-file and manifest discovery relative to a different repository root')
1018
+ .option('--from-manifest <path>', 'Load configuration from existing superposition.json manifest')
990
1019
  .option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
991
1020
  .option('--backup', 'Force or suppress backup; default is --no-backup inside a git repo, --backup outside')
992
1021
  .option('--backup-dir <path>', 'Custom backup directory location')
993
1022
  .option('--minimal', 'Minimal mode - exclude optional/nice-to-have features and extensions')
994
1023
  .option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
995
- .action((options) => {
996
- const outputPath = options.output || './.devcontainer';
997
- // Look for manifest in multiple locations for team workflow:
998
- // 1. Current directory (./superposition.json) - team workflow
999
- // 2. Output directory (e.g., ./.devcontainer/superposition.json) - legacy
1000
- const manifestSearchPaths = [
1001
- 'superposition.json',
1002
- path.join(outputPath, 'superposition.json'),
1003
- ];
1004
- let manifestPath = null;
1005
- for (const searchPath of manifestSearchPaths) {
1006
- if (fs.existsSync(searchPath)) {
1007
- manifestPath = searchPath;
1008
- break;
1009
- }
1010
- }
1011
- if (!manifestPath) {
1012
- console.error(chalk.red(`✗ Error: No manifest found`));
1013
- console.error(chalk.gray(' Searched for: ./superposition.json, ' +
1014
- path.join(outputPath, 'superposition.json')));
1015
- console.error(chalk.gray(' Run "container-superposition init --write-manifest-only" to create a manifest'));
1016
- process.exit(1);
1017
- }
1018
- // Store options for main() to process
1024
+ .action((options, command) => {
1019
1025
  initOptions = {
1020
1026
  ...options,
1021
- fromManifest: manifestPath,
1027
+ commandName: 'regen',
1022
1028
  interactive: false,
1029
+ _editorSource: command.getOptionValueSource('editor'),
1023
1030
  };
1024
1031
  });
1025
1032
  // List command
@@ -1049,13 +1056,15 @@ async function parseCliArgs() {
1049
1056
  program
1050
1057
  .command('plan')
1051
1058
  .description('Preview what will be generated before creating devcontainer')
1052
- .option('--stack <type>', 'Base template: plain, compose', 'compose')
1059
+ .option('--stack <type>', 'Base template: plain, compose')
1053
1060
  .option('--overlays <list>', 'Comma-separated list of overlay IDs')
1061
+ .option('--from-manifest <path>', 'Load stack and overlays from an existing superposition.json manifest')
1054
1062
  .option('--port-offset <number>', 'Add offset to all exposed ports', (val) => parseInt(val, 10), 0)
1055
1063
  .option('-o, --output <path>', 'Compare against existing config at this path (default: ./.devcontainer)')
1056
1064
  .option('--diff', 'Compare planned output vs existing configuration')
1057
1065
  .option('--diff-format <format>', 'Diff output format: color (default), json', 'color')
1058
1066
  .option('--diff-context <lines>', 'Context lines in diff output', (val) => parseInt(val, 10), 3)
1067
+ .option('--verbose', 'Explain why each overlay was included in the resolved plan')
1059
1068
  .option('--json', 'Output as JSON for scripting')
1060
1069
  .action(async (options) => {
1061
1070
  const overlaysConfig = loadOverlaysConfigWrapper();
@@ -1073,6 +1082,40 @@ async function parseCliArgs() {
1073
1082
  const overlaysConfig = loadOverlaysConfigWrapper();
1074
1083
  await doctorCommand(overlaysConfig, OVERLAYS_DIR, options);
1075
1084
  });
1085
+ // Adopt command
1086
+ program
1087
+ .command('adopt')
1088
+ .description('Analyse an existing .devcontainer/ and suggest an equivalent overlay-based configuration')
1089
+ .option('-d, --dir <path>', 'Path to the existing .devcontainer directory (default: ./.devcontainer)')
1090
+ .option('--dry-run', 'Print analysis and suggested command only; no files written')
1091
+ .option('--force', 'Overwrite existing superposition.json if present')
1092
+ .option('--backup', 'Force backup creation even when inside a git repo (default: backup only outside git repos)')
1093
+ .option('--no-backup', 'Disable backup creation even when it would normally be performed')
1094
+ .option('--backup-dir <path>', 'Custom backup directory location')
1095
+ .option('--project-file', 'Also write a repository-root project config (.superposition.yml or existing project file)')
1096
+ .option('--json', 'Output as JSON for scripting')
1097
+ .action(async (options) => {
1098
+ const overlaysConfig = loadOverlaysConfigWrapper();
1099
+ await adoptCommand(overlaysConfig, OVERLAYS_DIR, options);
1100
+ process.exit(0);
1101
+ });
1102
+ // Hash command
1103
+ program
1104
+ .command('hash')
1105
+ .description('Compute a deterministic fingerprint for a given configuration')
1106
+ .option('--stack <type>', 'Base template: plain, compose')
1107
+ .option('--overlays <list>', 'Comma-separated list of overlay IDs')
1108
+ .option('--preset <id>', 'Preset ID (optional)')
1109
+ .option('--base <image>', 'Base image / distro variant (e.g. bookworm, alpine)')
1110
+ .option('--manifest <path>', 'Path to superposition.json manifest')
1111
+ .option('-o, --output <path>', 'Directory to write hash file (used with --write)')
1112
+ .option('--write', 'Write hash to .devcontainer/superposition.hash')
1113
+ .option('--json', 'Output as JSON for scripting')
1114
+ .action(async (options) => {
1115
+ const overlaysConfig = loadOverlaysConfigWrapper();
1116
+ await hashCommand(overlaysConfig, OVERLAYS_DIR, options);
1117
+ process.exit(0);
1118
+ });
1076
1119
  await program.parseAsync(process.argv);
1077
1120
  // If init or regen command was run, return the options
1078
1121
  if (!initOptions) {
@@ -1083,6 +1126,34 @@ async function parseCliArgs() {
1083
1126
  if (Object.keys(initOptions).length === 0) {
1084
1127
  return null;
1085
1128
  }
1129
+ const hasSourceFlags = Number(Boolean(initOptions.fromProject)) + Number(Boolean(initOptions.fromManifest));
1130
+ if (hasSourceFlags > 1) {
1131
+ console.error(chalk.red('✗ Error: --from-project and --from-manifest cannot be used together'));
1132
+ process.exit(1);
1133
+ }
1134
+ const sourceSelectionConflicts = [
1135
+ 'stack',
1136
+ 'language',
1137
+ 'database',
1138
+ 'observability',
1139
+ 'playwright',
1140
+ 'cloudTools',
1141
+ 'devTools',
1142
+ 'portOffset',
1143
+ 'preset',
1144
+ ];
1145
+ const hasPresetParams = Array.isArray(initOptions.presetParam) && initOptions.presetParam.length > 0;
1146
+ const conflictingSelectionFlags = sourceSelectionConflicts.filter((key) => initOptions[key] !== undefined && initOptions[key] !== false);
1147
+ if ((initOptions.fromProject || initOptions.fromManifest) &&
1148
+ (conflictingSelectionFlags.length > 0 || hasPresetParams)) {
1149
+ const conflicts = [...conflictingSelectionFlags.map((key) => `--${key}`)];
1150
+ if (hasPresetParams) {
1151
+ conflicts.push('--preset-param');
1152
+ }
1153
+ console.error(chalk.red(`✗ Error: Persisted input sources cannot be combined with clean-generation selection flags: ${conflicts.join(', ')}`));
1154
+ console.error(chalk.dim(' Choose either a persisted input source (--from-project or --from-manifest) or direct selection flags for that run.'));
1155
+ process.exit(1);
1156
+ }
1086
1157
  const config = {};
1087
1158
  if (initOptions.stack)
1088
1159
  config.stack = initOptions.stack;
@@ -1114,13 +1185,13 @@ async function parseCliArgs() {
1114
1185
  if (initOptions.portOffset) {
1115
1186
  config.portOffset = parseInt(initOptions.portOffset, 10);
1116
1187
  }
1117
- if (initOptions.target) {
1188
+ if (initOptions.target && initOptions._targetSource !== 'default') {
1118
1189
  config.target = initOptions.target;
1119
1190
  }
1120
1191
  if (initOptions.minimal) {
1121
1192
  config.minimal = true;
1122
1193
  }
1123
- if (initOptions.editor) {
1194
+ if (initOptions.editor && initOptions._editorSource !== 'default') {
1124
1195
  const editorLower = initOptions.editor.toLowerCase();
1125
1196
  if (['vscode', 'jetbrains', 'none'].includes(editorLower)) {
1126
1197
  config.editor = editorLower;
@@ -1163,8 +1234,11 @@ async function parseCliArgs() {
1163
1234
  }
1164
1235
  }
1165
1236
  return {
1237
+ commandName: initOptions.commandName,
1166
1238
  config,
1167
1239
  manifestPath: initOptions.fromManifest,
1240
+ fromProject: initOptions.fromProject === true,
1241
+ projectRoot: initOptions.projectRoot,
1168
1242
  backupOverride: initOptions.backup, // undefined = auto-detect; true = --backup; false = --no-backup
1169
1243
  backupDir: initOptions.backupDir,
1170
1244
  noInteractive: initOptions.interactive === false, // Commander creates options.interactive = false for --no-interactive
@@ -1174,16 +1248,47 @@ async function parseCliArgs() {
1174
1248
  async function main() {
1175
1249
  try {
1176
1250
  const cliArgs = await parseCliArgs();
1177
- // Validate --no-interactive requires --from-manifest
1178
- if (cliArgs?.noInteractive && !cliArgs?.manifestPath) {
1179
- console.error(chalk.red('✗ Error: --no-interactive requires --from-manifest'));
1180
- console.error(chalk.dim(' Use both flags together: --from-manifest <path> --no-interactive'));
1181
- process.exit(1);
1251
+ const initialCwd = process.cwd();
1252
+ if (cliArgs?.projectRoot) {
1253
+ const resolvedProjectRoot = path.resolve(initialCwd, cliArgs.projectRoot);
1254
+ if (!fs.existsSync(resolvedProjectRoot)) {
1255
+ console.error(chalk.red(`✗ Project root not found: ${resolvedProjectRoot}`));
1256
+ process.exit(1);
1257
+ }
1258
+ if (!fs.statSync(resolvedProjectRoot).isDirectory()) {
1259
+ console.error(chalk.red(`✗ Project root is not a directory: ${resolvedProjectRoot}`));
1260
+ process.exit(1);
1261
+ }
1262
+ process.chdir(resolvedProjectRoot);
1263
+ }
1264
+ let projectConfig = undefined;
1265
+ let projectConfigAnswers;
1266
+ if (!cliArgs?.manifestPath) {
1267
+ projectConfig =
1268
+ loadProjectConfig(loadOverlaysConfigWrapper(), process.cwd()) ?? undefined;
1269
+ if (projectConfig) {
1270
+ projectConfigAnswers = applyPresetSelections(buildAnswersFromProjectConfig(projectConfig.selection));
1271
+ }
1182
1272
  }
1183
1273
  let manifest;
1184
1274
  let manifestDir;
1185
1275
  let backupDir;
1186
1276
  let useManifestOnly = false;
1277
+ let useProjectOnly = false;
1278
+ if (cliArgs?.commandName === 'regen' && !cliArgs.manifestPath && !cliArgs.fromProject) {
1279
+ if (projectConfigAnswers) {
1280
+ useProjectOnly = true;
1281
+ }
1282
+ else {
1283
+ const discoveredManifestPath = findDefaultRegenManifest(cliArgs?.config?.outputPath || './.devcontainer');
1284
+ if (!discoveredManifestPath) {
1285
+ console.error(chalk.red('✗ Error: No project file or manifest found'));
1286
+ console.error(chalk.gray(' Looked for .superposition.yml or superposition.yml in the repository root, and superposition.json in common manifest locations.'));
1287
+ process.exit(1);
1288
+ }
1289
+ cliArgs.manifestPath = discoveredManifestPath;
1290
+ }
1291
+ }
1187
1292
  // Handle manifest loading
1188
1293
  if (cliArgs?.manifestPath) {
1189
1294
  const manifestPath = findManifestFile(cliArgs.manifestPath);
@@ -1206,12 +1311,30 @@ async function main() {
1206
1311
  useManifestOnly = true;
1207
1312
  }
1208
1313
  }
1314
+ if (cliArgs?.fromProject) {
1315
+ if (!projectConfigAnswers || !projectConfig) {
1316
+ console.error(chalk.red('✗ Could not find project file'));
1317
+ console.error(chalk.red(' Searched for: .superposition.yml, superposition.yml'));
1318
+ process.exit(1);
1319
+ }
1320
+ useProjectOnly = cliArgs.noInteractive || cliArgs.commandName === 'regen';
1321
+ }
1322
+ // Validate --no-interactive requires a persisted input source
1323
+ if (cliArgs?.noInteractive && !cliArgs?.manifestPath && !projectConfigAnswers) {
1324
+ console.error(chalk.red('✗ Error: --no-interactive requires persisted input'));
1325
+ console.error(chalk.dim(' Use --from-project, --from-manifest <path>, or run from a repository with .superposition.yml or superposition.yml'));
1326
+ process.exit(1);
1327
+ }
1209
1328
  // Determine whether to create a backup:
1210
1329
  // --backup → always backup
1211
1330
  // --no-backup → never backup
1212
1331
  // (neither) → backup only when NOT inside a git repository
1213
1332
  // (git already tracks history, so backups are redundant)
1214
- const backupCheckPath = path.resolve(cliArgs?.config?.outputPath || manifestDir || './.devcontainer');
1333
+ const resolvedOutputPath = cliArgs?.config?.outputPath ||
1334
+ projectConfigAnswers?.outputPath ||
1335
+ manifestDir ||
1336
+ './.devcontainer';
1337
+ const backupCheckPath = path.resolve(resolvedOutputPath);
1215
1338
  const inGitRepo = isInsideGitRepo(backupCheckPath);
1216
1339
  let shouldBackup;
1217
1340
  if (cliArgs?.backupOverride === true) {
@@ -1227,11 +1350,11 @@ async function main() {
1227
1350
  console.log(chalk.dim('ℹ Skipping backup — git repo detected (use --backup to force one)\n'));
1228
1351
  }
1229
1352
  }
1353
+ const isReplayMode = cliArgs?.commandName === 'regen' || useManifestOnly || useProjectOnly;
1230
1354
  // Create backup if needed
1231
1355
  let actualBackupPath;
1232
- if (shouldBackup && manifest) {
1233
- // Output path is the directory containing the manifest
1234
- const outputPath = manifestDir || './.devcontainer';
1356
+ if (shouldBackup && isReplayMode) {
1357
+ const outputPath = resolvedOutputPath;
1235
1358
  const backupPath = await createBackup(outputPath, backupDir);
1236
1359
  if (backupPath) {
1237
1360
  actualBackupPath = backupPath;
@@ -1241,14 +1364,19 @@ async function main() {
1241
1364
  }
1242
1365
  // Build answers based on mode
1243
1366
  let answers;
1244
- const isRegen = !!manifest;
1245
1367
  // Check if there are CLI overrides beyond just output path and preset flags
1246
1368
  // Preset/presetChoices alone don't constitute "CLI overrides" that bypass interactive mode
1247
1369
  const hasCliOverrides = cliArgs &&
1248
1370
  Object.keys(cliArgs.config).some((key) => key !== 'outputPath' &&
1249
1371
  key !== 'preset' &&
1250
1372
  key !== 'presetChoices' &&
1373
+ !(key === 'target' && cliArgs.config.target === 'local') &&
1374
+ !(key === 'editor' && cliArgs.config.editor === 'vscode') &&
1251
1375
  cliArgs.config[key] !== undefined);
1376
+ const hasAnyCliConfig = cliArgs &&
1377
+ Object.entries(cliArgs.config).some(([key, value]) => value !== undefined &&
1378
+ !(key === 'target' && value === 'local') &&
1379
+ !(key === 'editor' && value === 'vscode'));
1252
1380
  if (useManifestOnly && manifest && !hasCliOverrides) {
1253
1381
  // Mode 1: Manifest-only (--from-manifest --no-interactive, no CLI overrides)
1254
1382
  const manifestAnswers = buildAnswersFromManifest(manifest, manifestDir);
@@ -1268,19 +1396,44 @@ async function main() {
1268
1396
  : '') +
1269
1397
  chalk.gray(` Output: ${answers.outputPath}`), { padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: 1 }));
1270
1398
  }
1271
- else if (cliArgs && (cliArgs.config.stack || hasCliOverrides)) {
1399
+ else if (useProjectOnly && projectConfigAnswers && !hasCliOverrides) {
1400
+ const projectFileName = projectConfig?.file.fileName ?? '.superposition.yml';
1401
+ answers = mergeAnswers(projectConfigAnswers, {
1402
+ outputPath: cliArgs?.config?.outputPath ||
1403
+ projectConfigAnswers.outputPath ||
1404
+ './.devcontainer',
1405
+ minimal: cliArgs?.config?.minimal,
1406
+ editor: cliArgs?.config?.editor,
1407
+ });
1408
+ console.log('\n' +
1409
+ boxen(chalk.bold.cyan('Regenerating from Project File (No Interactive)\n\n') +
1410
+ chalk.white('Configuration:\n') +
1411
+ chalk.gray(` Project file: ${projectFileName}\n`) +
1412
+ chalk.gray(` Output: ${answers.outputPath}`), {
1413
+ padding: 1,
1414
+ borderColor: 'cyan',
1415
+ borderStyle: 'round',
1416
+ margin: 1,
1417
+ }));
1418
+ }
1419
+ else if ((cliArgs && (cliArgs.config.stack || hasCliOverrides)) ||
1420
+ (projectConfigAnswers && (cliArgs?.noInteractive || hasAnyCliConfig))) {
1272
1421
  // Mode 2: CLI-based (with optional manifest defaults)
1273
1422
  // This includes regen with --minimal or --editor flags
1274
1423
  const cliAnswers = buildAnswersFromCliArgs(cliArgs.config);
1275
1424
  const manifestAnswers = manifest
1276
1425
  ? buildAnswersFromManifest(manifest, manifestDir)
1277
1426
  : undefined;
1278
- answers = mergeAnswers(manifestAnswers, cliAnswers, {
1279
- outputPath: cliAnswers.outputPath || './.devcontainer',
1427
+ answers = mergeAnswers(projectConfigAnswers, manifestAnswers, cliAnswers, {
1428
+ outputPath: cliAnswers.outputPath || projectConfigAnswers?.outputPath || './.devcontainer',
1280
1429
  });
1281
1430
  const modeLabel = useManifestOnly && hasCliOverrides
1282
1431
  ? 'Regenerating from Manifest with Overrides'
1283
- : 'Running in CLI mode';
1432
+ : useProjectOnly && projectConfigAnswers
1433
+ ? 'Regenerating from Project File with Overrides'
1434
+ : projectConfigAnswers && !manifest
1435
+ ? 'Running from Project Config'
1436
+ : 'Running in CLI mode';
1284
1437
  console.log('\n' +
1285
1438
  boxen(chalk.bold(modeLabel), {
1286
1439
  padding: 0.5,
@@ -1288,7 +1441,7 @@ async function main() {
1288
1441
  borderStyle: 'round',
1289
1442
  }));
1290
1443
  // Show what's being overridden
1291
- if (useManifestOnly && hasCliOverrides) {
1444
+ if ((useManifestOnly || useProjectOnly) && hasCliOverrides) {
1292
1445
  const overrides = [];
1293
1446
  if (cliAnswers.minimal)
1294
1447
  overrides.push('minimal mode');
@@ -1301,8 +1454,15 @@ async function main() {
1301
1454
  }
1302
1455
  else {
1303
1456
  // Mode 3: Interactive (with optional manifest pre-population and CLI preset pre-selection)
1304
- const interactiveAnswers = await runQuestionnaire(manifest, manifestDir, cliArgs?.config.preset, cliArgs?.config.presetChoices);
1305
- answers = mergeAnswers(interactiveAnswers);
1457
+ const interactiveAnswers = await runQuestionnaire(manifest, manifestDir, cliArgs?.config.preset || projectConfigAnswers?.preset, cliArgs?.config.presetChoices || projectConfigAnswers?.presetChoices, projectConfigAnswers);
1458
+ answers = mergeAnswers(projectConfigAnswers, interactiveAnswers);
1459
+ }
1460
+ if (!manifest && projectConfig?.selection.customizations) {
1461
+ const materializedOutputPath = path.resolve(answers.outputPath);
1462
+ if (!fs.existsSync(materializedOutputPath)) {
1463
+ fs.mkdirSync(materializedOutputPath, { recursive: true });
1464
+ }
1465
+ writeProjectConfigCustomizations(materializedOutputPath, projectConfig.selection.customizations);
1306
1466
  }
1307
1467
  // Show configuration summary
1308
1468
  const summaryLines = [
@@ -1322,6 +1482,9 @@ async function main() {
1322
1482
  if (answers.cloudTools && answers.cloudTools.length > 0) {
1323
1483
  summaryLines.push(chalk.cyan('Cloud tools: ') + chalk.white(answers.cloudTools.join(', ')));
1324
1484
  }
1485
+ if (projectConfig?.file && !manifest) {
1486
+ summaryLines.push(chalk.cyan('Project config: ') + chalk.white(projectConfig.file.fileName));
1487
+ }
1325
1488
  summaryLines.push(chalk.cyan('Output: ') + chalk.white(answers.outputPath));
1326
1489
  console.log('\n' +
1327
1490
  boxen(summaryLines.join('\n'), {
@@ -1342,11 +1505,13 @@ async function main() {
1342
1505
  try {
1343
1506
  let summary;
1344
1507
  if (isManifestOnly) {
1345
- summary = await generateManifestOnly(answers, undefined, { isRegen });
1508
+ summary = await generateManifestOnly(answers, undefined, {
1509
+ isRegen: isReplayMode,
1510
+ });
1346
1511
  spinner.succeed(chalk.green('Manifest created successfully!'));
1347
1512
  }
1348
1513
  else {
1349
- summary = await composeDevContainer(answers, undefined, { isRegen });
1514
+ summary = await composeDevContainer(answers, undefined, { isRegen: isReplayMode });
1350
1515
  spinner.succeed(chalk.green('DevContainer created successfully!'));
1351
1516
  }
1352
1517
  // Update summary with backup path and regen status