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.
- package/README.md +365 -9
- package/dist/scripts/init.js +220 -94
- package/dist/scripts/init.js.map +1 -1
- package/dist/tool/commands/doctor.js +2 -2
- package/dist/tool/commands/explain.d.ts.map +1 -1
- package/dist/tool/commands/explain.js +88 -0
- package/dist/tool/commands/explain.js.map +1 -1
- package/dist/tool/commands/plan.d.ts +51 -0
- package/dist/tool/commands/plan.d.ts.map +1 -1
- package/dist/tool/commands/plan.js +523 -1
- package/dist/tool/commands/plan.js.map +1 -1
- package/dist/tool/questionnaire/composer.d.ts +12 -3
- package/dist/tool/questionnaire/composer.d.ts.map +1 -1
- package/dist/tool/questionnaire/composer.js +133 -20
- package/dist/tool/questionnaire/composer.js.map +1 -1
- package/dist/tool/schema/types.d.ts +18 -0
- package/dist/tool/schema/types.d.ts.map +1 -1
- package/dist/tool/utils/gitignore.d.ts +15 -0
- package/dist/tool/utils/gitignore.d.ts.map +1 -0
- package/dist/tool/utils/gitignore.js +41 -0
- package/dist/tool/utils/gitignore.js.map +1 -0
- package/dist/tool/utils/services-export.d.ts +14 -0
- package/dist/tool/utils/services-export.d.ts.map +1 -0
- package/dist/tool/utils/services-export.js +478 -0
- package/dist/tool/utils/services-export.js.map +1 -0
- package/dist/tool/utils/summary.d.ts +69 -0
- package/dist/tool/utils/summary.d.ts.map +1 -0
- package/dist/tool/utils/summary.js +260 -0
- package/dist/tool/utils/summary.js.map +1 -0
- package/docs/overlays.md +48 -5
- package/overlays/.presets/microservice.yml +32 -6
- package/overlays/.presets/web-api.yml +76 -56
- package/overlays/cloudflared/README.md +190 -0
- package/overlays/cloudflared/devcontainer.patch.json +3 -0
- package/overlays/cloudflared/overlay.yml +15 -0
- package/overlays/cloudflared/setup.sh +49 -0
- package/overlays/cloudflared/verify.sh +21 -0
- package/overlays/direnv/README.md +6 -4
- package/overlays/direnv/setup.sh +0 -12
- package/overlays/grpc-tools/README.md +242 -0
- package/overlays/grpc-tools/devcontainer.patch.json +14 -0
- package/overlays/grpc-tools/overlay.yml +14 -0
- package/overlays/grpc-tools/setup.sh +57 -0
- package/overlays/grpc-tools/verify.sh +47 -0
- package/overlays/keycloak/.env.example +5 -0
- package/overlays/keycloak/README.md +238 -0
- package/overlays/keycloak/devcontainer.patch.json +17 -0
- package/overlays/keycloak/docker-compose.yml +32 -0
- package/overlays/keycloak/overlay.yml +23 -0
- package/overlays/keycloak/verify.sh +54 -0
- package/overlays/mailpit/.env.example +4 -0
- package/overlays/mailpit/README.md +191 -0
- package/overlays/mailpit/devcontainer.patch.json +20 -0
- package/overlays/mailpit/docker-compose.yml +17 -0
- package/overlays/mailpit/overlay.yml +26 -0
- package/overlays/mailpit/verify.sh +52 -0
- package/overlays/ngrok/overlay.yml +2 -1
- package/overlays/python/README.md +51 -35
- package/overlays/python/devcontainer.patch.json +7 -4
- package/overlays/python/setup.sh +50 -23
- package/overlays/python/verify.sh +29 -1
- package/package.json +1 -1
package/dist/scripts/init.js
CHANGED
|
@@ -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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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
|
|
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
|
-
]
|
|
256
|
-
if (
|
|
257
|
-
|
|
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
|
-
|
|
335
|
-
let
|
|
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
|
-
|
|
341
|
-
|
|
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
|
-
|
|
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('--
|
|
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('--
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1239
|
+
ensureBackupPatternsInGitignore(outputPath);
|
|
1104
1240
|
}
|
|
1105
1241
|
}
|
|
1106
1242
|
// Build answers based on mode
|
|
1107
1243
|
let answers;
|
|
1108
|
-
|
|
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' +
|