container-superposition 0.1.3 → 0.1.4

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 (62) hide show
  1. package/README.md +365 -9
  2. package/dist/scripts/init.js +220 -94
  3. package/dist/scripts/init.js.map +1 -1
  4. package/dist/tool/commands/doctor.js +2 -2
  5. package/dist/tool/commands/explain.d.ts.map +1 -1
  6. package/dist/tool/commands/explain.js +88 -0
  7. package/dist/tool/commands/explain.js.map +1 -1
  8. package/dist/tool/commands/plan.d.ts +51 -0
  9. package/dist/tool/commands/plan.d.ts.map +1 -1
  10. package/dist/tool/commands/plan.js +523 -1
  11. package/dist/tool/commands/plan.js.map +1 -1
  12. package/dist/tool/questionnaire/composer.d.ts +12 -3
  13. package/dist/tool/questionnaire/composer.d.ts.map +1 -1
  14. package/dist/tool/questionnaire/composer.js +133 -20
  15. package/dist/tool/questionnaire/composer.js.map +1 -1
  16. package/dist/tool/schema/types.d.ts +18 -0
  17. package/dist/tool/schema/types.d.ts.map +1 -1
  18. package/dist/tool/utils/gitignore.d.ts +15 -0
  19. package/dist/tool/utils/gitignore.d.ts.map +1 -0
  20. package/dist/tool/utils/gitignore.js +41 -0
  21. package/dist/tool/utils/gitignore.js.map +1 -0
  22. package/dist/tool/utils/services-export.d.ts +14 -0
  23. package/dist/tool/utils/services-export.d.ts.map +1 -0
  24. package/dist/tool/utils/services-export.js +478 -0
  25. package/dist/tool/utils/services-export.js.map +1 -0
  26. package/dist/tool/utils/summary.d.ts +69 -0
  27. package/dist/tool/utils/summary.d.ts.map +1 -0
  28. package/dist/tool/utils/summary.js +260 -0
  29. package/dist/tool/utils/summary.js.map +1 -0
  30. package/docs/overlays.md +48 -5
  31. package/overlays/.presets/microservice.yml +32 -6
  32. package/overlays/.presets/web-api.yml +76 -56
  33. package/overlays/cloudflared/README.md +190 -0
  34. package/overlays/cloudflared/devcontainer.patch.json +3 -0
  35. package/overlays/cloudflared/overlay.yml +15 -0
  36. package/overlays/cloudflared/setup.sh +49 -0
  37. package/overlays/cloudflared/verify.sh +21 -0
  38. package/overlays/direnv/README.md +6 -4
  39. package/overlays/direnv/setup.sh +0 -12
  40. package/overlays/grpc-tools/README.md +242 -0
  41. package/overlays/grpc-tools/devcontainer.patch.json +14 -0
  42. package/overlays/grpc-tools/overlay.yml +14 -0
  43. package/overlays/grpc-tools/setup.sh +57 -0
  44. package/overlays/grpc-tools/verify.sh +47 -0
  45. package/overlays/keycloak/.env.example +5 -0
  46. package/overlays/keycloak/README.md +238 -0
  47. package/overlays/keycloak/devcontainer.patch.json +17 -0
  48. package/overlays/keycloak/docker-compose.yml +32 -0
  49. package/overlays/keycloak/overlay.yml +23 -0
  50. package/overlays/keycloak/verify.sh +54 -0
  51. package/overlays/mailpit/.env.example +4 -0
  52. package/overlays/mailpit/README.md +191 -0
  53. package/overlays/mailpit/devcontainer.patch.json +20 -0
  54. package/overlays/mailpit/docker-compose.yml +17 -0
  55. package/overlays/mailpit/overlay.yml +26 -0
  56. package/overlays/mailpit/verify.sh +52 -0
  57. package/overlays/ngrok/overlay.yml +2 -1
  58. package/overlays/python/README.md +51 -35
  59. package/overlays/python/devcontainer.patch.json +7 -4
  60. package/overlays/python/setup.sh +50 -23
  61. package/overlays/python/verify.sh +29 -1
  62. package/package.json +1 -1
@@ -2,6 +2,7 @@
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';
5
6
  import { Command } from 'commander';
6
7
  import chalk from 'chalk';
7
8
  import boxen from 'boxen';
@@ -17,6 +18,8 @@ import { doctorCommand } from '../tool/commands/doctor.js';
17
18
  import { getIncompatibleOverlays, DEPLOYMENT_TARGETS } from '../tool/schema/deployment-targets.js';
18
19
  import { migrateManifest, needsMigration, detectManifestVersion, isVersionSupported, CURRENT_MANIFEST_VERSION, SUPPORTED_MANIFEST_VERSIONS, } from '../tool/schema/manifest-migrations.js';
19
20
  import { getToolVersion } from '../tool/utils/version.js';
21
+ import { printSummary } from '../tool/utils/summary.js';
22
+ import { appendGitignoreSection } from '../tool/utils/gitignore.js';
20
23
  // Get __dirname equivalent in ESM
21
24
  const __filename = fileURLToPath(import.meta.url);
22
25
  const __dirname = path.dirname(__filename);
@@ -65,7 +68,7 @@ function loadPresetDefinition(presetId) {
65
68
  /**
66
69
  * Expand a preset into a list of overlay IDs with user choices resolved
67
70
  */
68
- async function expandPreset(presetId, stack) {
71
+ async function expandPreset(presetId, stack, preProvidedChoices = {}) {
69
72
  const preset = loadPresetDefinition(presetId);
70
73
  if (!preset) {
71
74
  return { overlays: [], choices: {} };
@@ -73,23 +76,72 @@ async function expandPreset(presetId, stack) {
73
76
  console.log(chalk.cyan(`\nšŸ“¦ Expanding preset: ${preset.name}\n`));
74
77
  const overlays = [...preset.selects.required];
75
78
  const choices = {};
76
- // Handle user choices
79
+ // Handle user choices (single overlay per option)
77
80
  if (preset.selects.userChoice) {
78
81
  for (const [key, choice] of Object.entries(preset.selects.userChoice)) {
79
- const selectedOption = (await select({
80
- message: choice.prompt,
81
- choices: choice.options.map((opt) => ({
82
- name: opt,
83
- value: opt,
84
- })),
85
- default: choice.defaultOption,
86
- }));
87
- overlays.push(selectedOption);
88
- choices[key] = selectedOption;
82
+ const preProvidedValue = preProvidedChoices[key];
83
+ if (preProvidedValue !== undefined) {
84
+ // Validate the pre-provided value
85
+ if (!choice.options.includes(preProvidedValue)) {
86
+ const valid = choice.options.join(', ');
87
+ throw new Error(`Invalid value '${preProvidedValue}' for preset choice '${key}'. Valid options: ${valid}`);
88
+ }
89
+ console.log(chalk.dim(`āœ“ ${key}: ${preProvidedValue} (from CLI)`));
90
+ overlays.push(preProvidedValue);
91
+ choices[key] = preProvidedValue;
92
+ }
93
+ else {
94
+ const selectedOption = (await select({
95
+ message: choice.prompt,
96
+ choices: choice.options.map((opt) => ({
97
+ name: opt,
98
+ value: opt,
99
+ })),
100
+ default: choice.defaultOption,
101
+ }));
102
+ overlays.push(selectedOption);
103
+ choices[key] = selectedOption;
104
+ }
105
+ }
106
+ }
107
+ // Handle parameterized slots (multiple overlays per option)
108
+ if (preset.parameters) {
109
+ for (const [key, param] of Object.entries(preset.parameters)) {
110
+ const preProvidedValue = preProvidedChoices[key];
111
+ let selectedId;
112
+ if (preProvidedValue !== undefined) {
113
+ // Validate the pre-provided value
114
+ const validOption = param.options.find((o) => o.id === preProvidedValue);
115
+ if (!validOption) {
116
+ const valid = param.options.map((o) => o.id).join(', ');
117
+ throw new Error(`Invalid value '${preProvidedValue}' for preset parameter '${key}'. Valid options: ${valid}`);
118
+ }
119
+ console.log(chalk.dim(`āœ“ ${key}: ${preProvidedValue} (from CLI)`));
120
+ selectedId = preProvidedValue;
121
+ }
122
+ else {
123
+ const description = param.description || `Select ${key}`;
124
+ selectedId = (await select({
125
+ message: description,
126
+ choices: param.options.map((opt) => ({
127
+ name: opt.description ? `${opt.id} - ${opt.description}` : opt.id,
128
+ value: opt.id,
129
+ })),
130
+ default: param.default,
131
+ }));
132
+ }
133
+ // Add overlays for the selected option
134
+ const selectedOption = param.options.find((o) => o.id === selectedId);
135
+ if (selectedOption) {
136
+ overlays.push(...selectedOption.overlays);
137
+ }
138
+ choices[key] = selectedId;
89
139
  }
90
140
  }
91
- console.log(chalk.dim(`āœ“ Preset will include: ${overlays.join(', ')}\n`));
92
- return { overlays, choices, glueConfig: preset.glueConfig };
141
+ // Deduplicate overlays
142
+ const uniqueOverlays = [...new Set(overlays)];
143
+ console.log(chalk.dim(`āœ“ Preset will include: ${uniqueOverlays.join(', ')}\n`));
144
+ return { overlays: uniqueOverlays, choices, glueConfig: preset.glueConfig };
93
145
  }
94
146
  /**
95
147
  * Search for manifest file in multiple locations
@@ -158,6 +210,32 @@ function loadManifest(manifestPath) {
158
210
  return null;
159
211
  }
160
212
  }
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
+ }
161
239
  /**
162
240
  * Create timestamped backup of existing devcontainer and manifest
163
241
  */
@@ -241,31 +319,16 @@ async function copyDirectory(src, dest) {
241
319
  /**
242
320
  * Ensure backup patterns are in .gitignore
243
321
  */
244
- async function ensureBackupPatternsInGitignore(outputPath) {
245
- // Write to the parent directory's .gitignore (project root), not inside outputPath
246
- const resolvedOutputPath = path.resolve(outputPath);
247
- const projectRoot = path.dirname(resolvedOutputPath);
322
+ function ensureBackupPatternsInGitignore(outputPath) {
323
+ const projectRoot = path.dirname(path.resolve(outputPath));
248
324
  const gitignorePath = path.join(projectRoot, '.gitignore');
249
- const backupPatterns = [
250
- '',
251
- '# Container Superposition backups',
325
+ const written = appendGitignoreSection(gitignorePath, 'container-superposition backups', [
252
326
  '.devcontainer.backup-*/',
253
327
  '*.backup-*',
254
328
  'superposition.json.backup-*',
255
- ].join('\n');
256
- if (!fs.existsSync(gitignorePath)) {
257
- // Create new .gitignore with backup patterns
258
- await fs.promises.writeFile(gitignorePath, backupPatterns + '\n');
259
- console.log(chalk.dim(' šŸ“ Created .gitignore with backup patterns'));
260
- }
261
- else {
262
- // Check if patterns already exist
263
- const content = await fs.promises.readFile(gitignorePath, 'utf-8');
264
- if (!content.includes('Container Superposition backups')) {
265
- // Append patterns
266
- await fs.promises.appendFile(gitignorePath, '\n' + backupPatterns + '\n');
267
- console.log(chalk.dim(' šŸ“ Updated .gitignore with backup patterns'));
268
- }
329
+ ]);
330
+ if (written) {
331
+ console.log(chalk.dim(' šŸ“ Updated .gitignore with backup patterns'));
269
332
  }
270
333
  }
271
334
  /**
@@ -297,7 +360,7 @@ function buildOverlayChoices(config, stack, categoryList, preselected) {
297
360
  /**
298
361
  * Interactive questionnaire with modern checkbox selections
299
362
  */
300
- async function runQuestionnaire(manifest, manifestDir) {
363
+ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetChoices) {
301
364
  const config = loadOverlaysConfigWrapper();
302
365
  // Pretty banner
303
366
  console.log('\n' +
@@ -331,32 +394,50 @@ async function runQuestionnaire(manifest, manifestDir) {
331
394
  try {
332
395
  // Question 0: Optional preset selection
333
396
  let usePreset = false;
334
- let selectedPresetId = manifest?.preset;
335
- let presetChoices = manifest?.presetChoices || {};
397
+ // CLI preset takes precedence over manifest preset
398
+ let selectedPresetId = cliPresetId || manifest?.preset;
399
+ // CLI preset choices merged with manifest choices (CLI takes precedence)
400
+ let presetChoices = {
401
+ ...(manifest?.presetChoices || {}),
402
+ ...(cliPresetChoices || {}),
403
+ };
336
404
  let presetGlueConfig;
337
405
  const presetOverlaysFiltered = config.overlays.filter((o) => o.category === 'preset');
338
406
  let presetOverlays = [];
339
407
  if (presetOverlaysFiltered.length > 0) {
340
- const defaultPreset = manifest?.preset || 'custom';
341
- const presetChoice = (await select({
342
- message: 'Start from a preset or build custom?',
343
- choices: [
344
- {
345
- name: 'Custom (select overlays manually)',
346
- value: 'custom',
347
- description: 'Choose individual overlays yourself',
348
- },
349
- ...presetOverlaysFiltered.map((p) => ({
350
- name: p.name,
351
- value: p.id,
352
- description: p.description,
353
- })),
354
- ],
355
- default: defaultPreset,
356
- }));
357
- if (presetChoice !== 'custom') {
408
+ // If a preset was pre-selected via CLI or manifest, skip the prompt
409
+ if (selectedPresetId) {
358
410
  usePreset = true;
359
- selectedPresetId = presetChoice;
411
+ console.log(chalk.cyan(`\nšŸ“¦ Using preset: ${selectedPresetId}\n`));
412
+ }
413
+ else {
414
+ const defaultPreset = 'custom';
415
+ const presetChoice = (await select({
416
+ message: 'Start from a preset or build custom?',
417
+ choices: [
418
+ {
419
+ name: 'Custom (select overlays manually)',
420
+ value: 'custom',
421
+ description: 'Choose individual overlays yourself',
422
+ },
423
+ ...presetOverlaysFiltered.map((p) => ({
424
+ name: p.name,
425
+ value: p.id,
426
+ description: p.description,
427
+ })),
428
+ ],
429
+ default: defaultPreset,
430
+ }));
431
+ if (presetChoice !== 'custom') {
432
+ usePreset = true;
433
+ selectedPresetId = presetChoice;
434
+ }
435
+ else {
436
+ // User chose custom - discard any pre-provided preset choices so the
437
+ // manifest cannot end up with presetChoices but no preset.
438
+ presetChoices = {};
439
+ selectedPresetId = undefined;
440
+ }
360
441
  }
361
442
  }
362
443
  // Question 1: Base template
@@ -369,9 +450,9 @@ async function runQuestionnaire(manifest, manifestDir) {
369
450
  })),
370
451
  default: manifest?.baseTemplate,
371
452
  }));
372
- // If using preset, expand it now
453
+ // If using preset, expand it now (pass pre-provided choices to skip those prompts)
373
454
  if (usePreset && selectedPresetId) {
374
- const expansion = await expandPreset(selectedPresetId, stack);
455
+ const expansion = await expandPreset(selectedPresetId, stack, presetChoices);
375
456
  if (!expansion.overlays || expansion.overlays.length === 0) {
376
457
  // Preset failed to expand (e.g., missing or invalid preset definition).
377
458
  // Treat this as "no preset" so the manifest does not incorrectly record one.
@@ -881,7 +962,7 @@ async function parseCliArgs() {
881
962
  .description('Initialize a new devcontainer configuration')
882
963
  .option('--from-manifest <path>', 'Load configuration from existing superposition.json manifest')
883
964
  .option('--no-interactive', 'Use manifest values directly without questionnaire (requires --from-manifest)')
884
- .option('--no-backup', 'Skip creating backup before regeneration')
965
+ .option('--backup', 'Force or suppress backup; default is --no-backup inside a git repo, --backup outside')
885
966
  .option('--backup-dir <path>', 'Custom backup directory location')
886
967
  .option('--stack <type>', 'Base template: plain, compose')
887
968
  .option('--language <list>', 'Comma-separated language overlays: dotnet, nodejs, python, mkdocs, java, go, rust, bun, powershell')
@@ -896,6 +977,8 @@ async function parseCliArgs() {
896
977
  .option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
897
978
  .option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
898
979
  .option('--write-manifest-only', 'Generate only superposition.json manifest without creating .devcontainer/ files')
980
+ .option('--preset <id>', 'Start from a preset (e.g., web-api, microservice)')
981
+ .option('--preset-param <value>', 'Set a preset parameter value (format: key=value, can be repeated)', (value, previous) => previous.concat([value]), [])
899
982
  .action((options) => {
900
983
  // Store options for main() to process
901
984
  initOptions = options;
@@ -905,7 +988,7 @@ async function parseCliArgs() {
905
988
  .command('regen')
906
989
  .description('Regenerate devcontainer from existing superposition.json manifest')
907
990
  .option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
908
- .option('--no-backup', 'Skip creating backup before regeneration')
991
+ .option('--backup', 'Force or suppress backup; default is --no-backup inside a git repo, --backup outside')
909
992
  .option('--backup-dir <path>', 'Custom backup directory location')
910
993
  .option('--minimal', 'Minimal mode - exclude optional/nice-to-have features and extensions')
911
994
  .option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
@@ -969,6 +1052,10 @@ async function parseCliArgs() {
969
1052
  .option('--stack <type>', 'Base template: plain, compose', 'compose')
970
1053
  .option('--overlays <list>', 'Comma-separated list of overlay IDs')
971
1054
  .option('--port-offset <number>', 'Add offset to all exposed ports', (val) => parseInt(val, 10), 0)
1055
+ .option('-o, --output <path>', 'Compare against existing config at this path (default: ./.devcontainer)')
1056
+ .option('--diff', 'Compare planned output vs existing configuration')
1057
+ .option('--diff-format <format>', 'Diff output format: color (default), json', 'color')
1058
+ .option('--diff-context <lines>', 'Context lines in diff output', (val) => parseInt(val, 10), 3)
972
1059
  .option('--json', 'Output as JSON for scripting')
973
1060
  .action(async (options) => {
974
1061
  const overlaysConfig = loadOverlaysConfigWrapper();
@@ -1045,10 +1132,40 @@ async function parseCliArgs() {
1045
1132
  }
1046
1133
  if (initOptions.output)
1047
1134
  config.outputPath = initOptions.output;
1135
+ // Handle --preset flag
1136
+ if (initOptions.preset) {
1137
+ config.preset = initOptions.preset;
1138
+ }
1139
+ // Handle --preset-param flags (can be repeated)
1140
+ if (initOptions.presetParam && initOptions.presetParam.length > 0) {
1141
+ if (!initOptions.preset) {
1142
+ console.warn(chalk.yellow('āš ļø Ignoring --preset-param because no --preset was provided. ' +
1143
+ 'Preset parameters only apply when a preset is selected (e.g., --preset web-api --preset-param broker=nats).'));
1144
+ }
1145
+ else {
1146
+ const presetChoices = {};
1147
+ for (const param of initOptions.presetParam) {
1148
+ const eqIdx = param.indexOf('=');
1149
+ if (eqIdx > 0) {
1150
+ const key = param.slice(0, eqIdx).trim();
1151
+ const value = param.slice(eqIdx + 1).trim();
1152
+ if (key) {
1153
+ presetChoices[key] = value;
1154
+ }
1155
+ }
1156
+ else {
1157
+ console.warn(chalk.yellow(`āš ļø Invalid --preset-param format: "${param}". Expected "key=value" (e.g., --preset-param broker=nats).`));
1158
+ }
1159
+ }
1160
+ if (Object.keys(presetChoices).length > 0) {
1161
+ config.presetChoices = presetChoices;
1162
+ }
1163
+ }
1164
+ }
1048
1165
  return {
1049
1166
  config,
1050
1167
  manifestPath: initOptions.fromManifest,
1051
- noBackup: initOptions.backup === false, // Commander creates options.backup = false for --no-backup
1168
+ backupOverride: initOptions.backup, // undefined = auto-detect; true = --backup; false = --no-backup
1052
1169
  backupDir: initOptions.backupDir,
1053
1170
  noInteractive: initOptions.interactive === false, // Commander creates options.interactive = false for --no-interactive
1054
1171
  writeManifestOnly: initOptions.writeManifestOnly === true,
@@ -1065,7 +1182,6 @@ async function main() {
1065
1182
  }
1066
1183
  let manifest;
1067
1184
  let manifestDir;
1068
- let shouldBackup = true;
1069
1185
  let backupDir;
1070
1186
  let useManifestOnly = false;
1071
1187
  // Handle manifest loading
@@ -1082,10 +1198,7 @@ async function main() {
1082
1198
  process.exit(1);
1083
1199
  }
1084
1200
  manifest = loadedManifest;
1085
- // Check for backup and interaction options
1086
- if (cliArgs.noBackup) {
1087
- shouldBackup = false;
1088
- }
1201
+ // Check for interaction options
1089
1202
  if (cliArgs.backupDir) {
1090
1203
  backupDir = cliArgs.backupDir;
1091
1204
  }
@@ -1093,21 +1206,48 @@ async function main() {
1093
1206
  useManifestOnly = true;
1094
1207
  }
1095
1208
  }
1209
+ // Determine whether to create a backup:
1210
+ // --backup → always backup
1211
+ // --no-backup → never backup
1212
+ // (neither) → backup only when NOT inside a git repository
1213
+ // (git already tracks history, so backups are redundant)
1214
+ const backupCheckPath = path.resolve(cliArgs?.config?.outputPath || manifestDir || './.devcontainer');
1215
+ const inGitRepo = isInsideGitRepo(backupCheckPath);
1216
+ let shouldBackup;
1217
+ if (cliArgs?.backupOverride === true) {
1218
+ shouldBackup = true;
1219
+ }
1220
+ else if (cliArgs?.backupOverride === false) {
1221
+ shouldBackup = false;
1222
+ }
1223
+ else {
1224
+ // Auto-detect based on git presence
1225
+ shouldBackup = !inGitRepo;
1226
+ if (!shouldBackup) {
1227
+ console.log(chalk.dim('ℹ Skipping backup — git repo detected (use --backup to force one)\n'));
1228
+ }
1229
+ }
1096
1230
  // Create backup if needed
1231
+ let actualBackupPath;
1097
1232
  if (shouldBackup && manifest) {
1098
1233
  // Output path is the directory containing the manifest
1099
1234
  const outputPath = manifestDir || './.devcontainer';
1100
1235
  const backupPath = await createBackup(outputPath, backupDir);
1101
1236
  if (backupPath) {
1237
+ actualBackupPath = backupPath;
1102
1238
  console.log(chalk.green(`āœ“ Backup created: ${backupPath}\n`));
1103
- await ensureBackupPatternsInGitignore(outputPath);
1239
+ ensureBackupPatternsInGitignore(outputPath);
1104
1240
  }
1105
1241
  }
1106
1242
  // Build answers based on mode
1107
1243
  let answers;
1108
- // Check if there are CLI overrides beyond just output path
1244
+ const isRegen = !!manifest;
1245
+ // Check if there are CLI overrides beyond just output path and preset flags
1246
+ // Preset/presetChoices alone don't constitute "CLI overrides" that bypass interactive mode
1109
1247
  const hasCliOverrides = cliArgs &&
1110
1248
  Object.keys(cliArgs.config).some((key) => key !== 'outputPath' &&
1249
+ key !== 'preset' &&
1250
+ key !== 'presetChoices' &&
1111
1251
  cliArgs.config[key] !== undefined);
1112
1252
  if (useManifestOnly && manifest && !hasCliOverrides) {
1113
1253
  // Mode 1: Manifest-only (--from-manifest --no-interactive, no CLI overrides)
@@ -1160,8 +1300,8 @@ async function main() {
1160
1300
  }
1161
1301
  }
1162
1302
  else {
1163
- // Mode 3: Interactive (with optional manifest pre-population)
1164
- const interactiveAnswers = await runQuestionnaire(manifest, manifestDir);
1303
+ // 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);
1165
1305
  answers = mergeAnswers(interactiveAnswers);
1166
1306
  }
1167
1307
  // Show configuration summary
@@ -1200,40 +1340,26 @@ async function main() {
1200
1340
  color: 'cyan',
1201
1341
  }).start();
1202
1342
  try {
1343
+ let summary;
1203
1344
  if (isManifestOnly) {
1204
- await generateManifestOnly(answers);
1345
+ summary = await generateManifestOnly(answers, undefined, { isRegen });
1205
1346
  spinner.succeed(chalk.green('Manifest created successfully!'));
1206
1347
  }
1207
1348
  else {
1208
- await composeDevContainer(answers);
1349
+ summary = await composeDevContainer(answers, undefined, { isRegen });
1209
1350
  spinner.succeed(chalk.green('DevContainer created successfully!'));
1210
1351
  }
1352
+ // Update summary with backup path and regen status
1353
+ if (actualBackupPath) {
1354
+ summary.backupPath = actualBackupPath;
1355
+ }
1356
+ // Print comprehensive summary
1357
+ printSummary(summary);
1211
1358
  }
1212
1359
  catch (error) {
1213
1360
  spinner.fail(chalk.red(isManifestOnly ? 'Failed to create manifest' : 'Failed to create devcontainer'));
1214
1361
  throw error;
1215
1362
  }
1216
- // Success message
1217
- const successMessage = isManifestOnly
1218
- ? chalk.bold.green('āœ“ Manifest Created!\n\n') +
1219
- chalk.white('Next steps:\n') +
1220
- chalk.gray(' 1. Review the generated superposition.json file\n') +
1221
- chalk.gray(' 2. Commit it to your repository\n') +
1222
- chalk.gray(' 3. Team members can run "npx container-superposition regen"\n\n') +
1223
- chalk.dim('Team workflow: commit manifest, .gitignore .devcontainer/, customize with .devcontainer/custom/')
1224
- : chalk.bold.green('āœ“ Setup Complete!\n\n') +
1225
- chalk.white('Next steps:\n') +
1226
- chalk.gray(' 1. Review the generated .devcontainer/ folder\n') +
1227
- chalk.gray(" 2. Customize as needed (it's just normal JSON!)\n") +
1228
- chalk.gray(' 3. Open in VS Code and rebuild container\n\n') +
1229
- chalk.dim('The generated configuration is fully editable and independent of this tool.');
1230
- console.log('\n' +
1231
- boxen(successMessage, {
1232
- padding: 1,
1233
- borderColor: 'green',
1234
- borderStyle: 'double',
1235
- margin: 1,
1236
- }));
1237
1363
  }
1238
1364
  catch (error) {
1239
1365
  console.error('\n' +