container-superposition 0.1.3 → 0.1.5
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 +72 -1014
- package/dist/scripts/init.js +512 -238
- package/dist/scripts/init.js.map +1 -1
- package/dist/tool/commands/adopt.d.ts +62 -0
- package/dist/tool/commands/adopt.d.ts.map +1 -0
- package/dist/tool/commands/adopt.js +767 -0
- package/dist/tool/commands/adopt.js.map +1 -0
- 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/hash.d.ts +36 -0
- package/dist/tool/commands/hash.d.ts.map +1 -0
- package/dist/tool/commands/hash.js +242 -0
- package/dist/tool/commands/hash.js.map +1 -0
- package/dist/tool/commands/plan.d.ts +53 -0
- package/dist/tool/commands/plan.d.ts.map +1 -1
- package/dist/tool/commands/plan.js +784 -42
- 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/project-config.d.ts +15 -0
- package/dist/tool/schema/project-config.d.ts.map +1 -0
- package/dist/tool/schema/project-config.js +359 -0
- package/dist/tool/schema/project-config.js.map +1 -0
- package/dist/tool/schema/types.d.ts +57 -1
- package/dist/tool/schema/types.d.ts.map +1 -1
- package/dist/tool/utils/backup.d.ts +23 -0
- package/dist/tool/utils/backup.d.ts.map +1 -0
- package/dist/tool/utils/backup.js +123 -0
- package/dist/tool/utils/backup.js.map +1 -0
- 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/README.md +12 -2
- package/docs/adopt.md +196 -0
- package/docs/custom-patches.md +1 -1
- package/docs/discovery-commands.md +55 -3
- package/docs/examples.md +40 -6
- package/docs/filesystem-contract.md +58 -0
- package/docs/hash.md +183 -0
- package/docs/minimal-and-editor.md +1 -1
- package/docs/overlays.md +108 -5
- package/docs/presets-architecture.md +1 -1
- package/docs/presets.md +1 -1
- package/docs/publishing.md +36 -23
- package/docs/security.md +43 -0
- package/docs/specs/001-verbose-plan-graph/checklists/requirements.md +36 -0
- package/docs/specs/001-verbose-plan-graph/contracts/plan-verbose-output.md +96 -0
- package/docs/specs/001-verbose-plan-graph/data-model.md +111 -0
- package/docs/specs/001-verbose-plan-graph/plan.md +127 -0
- package/docs/specs/001-verbose-plan-graph/quickstart.md +106 -0
- package/docs/specs/001-verbose-plan-graph/research.md +100 -0
- package/docs/specs/001-verbose-plan-graph/spec.md +128 -0
- package/docs/specs/001-verbose-plan-graph/tasks.md +223 -0
- package/docs/specs/002-superposition-config-file/checklists/requirements.md +36 -0
- package/docs/specs/002-superposition-config-file/contracts/init-project-config.md +98 -0
- package/docs/specs/002-superposition-config-file/data-model.md +126 -0
- package/docs/specs/002-superposition-config-file/plan.md +208 -0
- package/docs/specs/002-superposition-config-file/quickstart.md +140 -0
- package/docs/specs/002-superposition-config-file/research.md +144 -0
- package/docs/specs/002-superposition-config-file/spec.md +130 -0
- package/docs/specs/002-superposition-config-file/tasks.md +213 -0
- package/docs/team-workflow.md +27 -1
- package/docs/workflows.md +136 -0
- package/overlays/.presets/microservice.yml +32 -6
- package/overlays/.presets/sdd.yml +84 -0
- package/overlays/.presets/web-api.yml +76 -56
- package/overlays/README.md +7 -1
- package/overlays/amp/README.md +70 -0
- package/overlays/amp/devcontainer.patch.json +3 -0
- package/overlays/amp/overlay.yml +15 -0
- package/overlays/amp/setup.sh +21 -0
- package/overlays/amp/verify.sh +21 -0
- package/overlays/claude-code/README.md +83 -0
- package/overlays/claude-code/devcontainer.patch.json +3 -0
- package/overlays/claude-code/overlay.yml +15 -0
- package/overlays/claude-code/setup.sh +21 -0
- package/overlays/claude-code/verify.sh +21 -0
- 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/gemini-cli/README.md +77 -0
- package/overlays/gemini-cli/devcontainer.patch.json +3 -0
- package/overlays/gemini-cli/overlay.yml +15 -0
- package/overlays/gemini-cli/setup.sh +21 -0
- package/overlays/gemini-cli/verify.sh +21 -0
- 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/opencode/README.md +76 -0
- package/overlays/opencode/devcontainer.patch.json +3 -0
- package/overlays/opencode/overlay.yml +14 -0
- package/overlays/opencode/setup.sh +21 -0
- package/overlays/opencode/verify.sh +21 -0
- 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/overlays/spec-kit/README.md +181 -0
- package/overlays/spec-kit/devcontainer.patch.json +6 -0
- package/overlays/spec-kit/overlay.yml +19 -0
- package/overlays/spec-kit/setup.sh +45 -0
- package/overlays/spec-kit/verify.sh +33 -0
- package/overlays/windsurf-cli/README.md +69 -0
- package/overlays/windsurf-cli/devcontainer.patch.json +3 -0
- package/overlays/windsurf-cli/overlay.yml +15 -0
- package/overlays/windsurf-cli/setup.sh +21 -0
- package/overlays/windsurf-cli/verify.sh +21 -0
- package/package.json +1 -1
- package/tool/schema/config.schema.json +138 -9
package/dist/scripts/init.js
CHANGED
|
@@ -14,9 +14,14 @@ import { listCommand } from '../tool/commands/list.js';
|
|
|
14
14
|
import { explainCommand } from '../tool/commands/explain.js';
|
|
15
15
|
import { planCommand } from '../tool/commands/plan.js';
|
|
16
16
|
import { doctorCommand } from '../tool/commands/doctor.js';
|
|
17
|
+
import { adoptCommand } from '../tool/commands/adopt.js';
|
|
18
|
+
import { hashCommand } from '../tool/commands/hash.js';
|
|
17
19
|
import { getIncompatibleOverlays, DEPLOYMENT_TARGETS } from '../tool/schema/deployment-targets.js';
|
|
18
20
|
import { migrateManifest, needsMigration, detectManifestVersion, isVersionSupported, CURRENT_MANIFEST_VERSION, SUPPORTED_MANIFEST_VERSIONS, } from '../tool/schema/manifest-migrations.js';
|
|
19
21
|
import { getToolVersion } from '../tool/utils/version.js';
|
|
22
|
+
import { printSummary } from '../tool/utils/summary.js';
|
|
23
|
+
import { buildAnswersFromProjectConfig, loadProjectConfig, writeProjectConfigCustomizations, } from '../tool/schema/project-config.js';
|
|
24
|
+
import { isInsideGitRepo, createBackup, ensureBackupPatternsInGitignore, } from '../tool/utils/backup.js';
|
|
20
25
|
// Get __dirname equivalent in ESM
|
|
21
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
22
27
|
const __dirname = path.dirname(__filename);
|
|
@@ -62,10 +67,104 @@ function loadPresetDefinition(presetId) {
|
|
|
62
67
|
const content = fs.readFileSync(presetPath, 'utf8');
|
|
63
68
|
return yaml.load(content);
|
|
64
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
|
+
}
|
|
65
164
|
/**
|
|
66
165
|
* Expand a preset into a list of overlay IDs with user choices resolved
|
|
67
166
|
*/
|
|
68
|
-
async function expandPreset(presetId, stack) {
|
|
167
|
+
async function expandPreset(presetId, stack, preProvidedChoices = {}) {
|
|
69
168
|
const preset = loadPresetDefinition(presetId);
|
|
70
169
|
if (!preset) {
|
|
71
170
|
return { overlays: [], choices: {} };
|
|
@@ -73,23 +172,82 @@ async function expandPreset(presetId, stack) {
|
|
|
73
172
|
console.log(chalk.cyan(`\n📦 Expanding preset: ${preset.name}\n`));
|
|
74
173
|
const overlays = [...preset.selects.required];
|
|
75
174
|
const choices = {};
|
|
76
|
-
// Handle user choices
|
|
175
|
+
// Handle user choices (single overlay per option)
|
|
77
176
|
if (preset.selects.userChoice) {
|
|
78
177
|
for (const [key, choice] of Object.entries(preset.selects.userChoice)) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
178
|
+
const preProvidedValue = preProvidedChoices[key];
|
|
179
|
+
if (preProvidedValue !== undefined) {
|
|
180
|
+
// Validate the pre-provided value
|
|
181
|
+
if (!choice.options.includes(preProvidedValue)) {
|
|
182
|
+
const valid = choice.options.join(', ');
|
|
183
|
+
throw new Error(`Invalid value '${preProvidedValue}' for preset choice '${key}'. Valid options: ${valid}`);
|
|
184
|
+
}
|
|
185
|
+
console.log(chalk.dim(`✓ ${key}: ${preProvidedValue} (from CLI)`));
|
|
186
|
+
overlays.push(preProvidedValue);
|
|
187
|
+
choices[key] = preProvidedValue;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
const selectedOption = (await select({
|
|
191
|
+
message: choice.prompt,
|
|
192
|
+
choices: choice.options.map((opt) => ({
|
|
193
|
+
name: opt,
|
|
194
|
+
value: opt,
|
|
195
|
+
})),
|
|
196
|
+
default: choice.defaultOption,
|
|
197
|
+
}));
|
|
198
|
+
overlays.push(selectedOption);
|
|
199
|
+
choices[key] = selectedOption;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// Handle parameterized slots (multiple overlays per option)
|
|
204
|
+
if (preset.parameters) {
|
|
205
|
+
for (const [key, param] of Object.entries(preset.parameters)) {
|
|
206
|
+
const preProvidedValue = preProvidedChoices[key];
|
|
207
|
+
let selectedId;
|
|
208
|
+
if (preProvidedValue !== undefined) {
|
|
209
|
+
// Validate the pre-provided value
|
|
210
|
+
const validOption = param.options.find((o) => o.id === preProvidedValue);
|
|
211
|
+
if (!validOption) {
|
|
212
|
+
const valid = param.options.map((o) => o.id).join(', ');
|
|
213
|
+
throw new Error(`Invalid value '${preProvidedValue}' for preset parameter '${key}'. Valid options: ${valid}`);
|
|
214
|
+
}
|
|
215
|
+
console.log(chalk.dim(`✓ ${key}: ${preProvidedValue} (from CLI)`));
|
|
216
|
+
selectedId = preProvidedValue;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
const description = param.description || `Select ${key}`;
|
|
220
|
+
selectedId = (await select({
|
|
221
|
+
message: description,
|
|
222
|
+
choices: param.options.map((opt) => ({
|
|
223
|
+
name: opt.description ? `${opt.id} - ${opt.description}` : opt.id,
|
|
224
|
+
value: opt.id,
|
|
225
|
+
})),
|
|
226
|
+
default: param.default,
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
229
|
+
// Add overlays for the selected option
|
|
230
|
+
const selectedOption = param.options.find((o) => o.id === selectedId);
|
|
231
|
+
if (selectedOption) {
|
|
232
|
+
overlays.push(...selectedOption.overlays);
|
|
233
|
+
}
|
|
234
|
+
choices[key] = selectedId;
|
|
89
235
|
}
|
|
90
236
|
}
|
|
91
|
-
|
|
92
|
-
|
|
237
|
+
// Deduplicate overlays
|
|
238
|
+
const uniqueOverlays = [...new Set(overlays)];
|
|
239
|
+
console.log(chalk.dim(`✓ Preset will include: ${uniqueOverlays.join(', ')}\n`));
|
|
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 };
|
|
93
251
|
}
|
|
94
252
|
/**
|
|
95
253
|
* Search for manifest file in multiple locations
|
|
@@ -112,6 +270,16 @@ function findManifestFile(manifestPath) {
|
|
|
112
270
|
}
|
|
113
271
|
return null;
|
|
114
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
|
+
}
|
|
115
283
|
/**
|
|
116
284
|
* Load and validate manifest file
|
|
117
285
|
*/
|
|
@@ -158,116 +326,6 @@ function loadManifest(manifestPath) {
|
|
|
158
326
|
return null;
|
|
159
327
|
}
|
|
160
328
|
}
|
|
161
|
-
/**
|
|
162
|
-
* Create timestamped backup of existing devcontainer and manifest
|
|
163
|
-
*/
|
|
164
|
-
async function createBackup(outputPath, backupDir) {
|
|
165
|
-
// Check for devcontainer files to backup
|
|
166
|
-
const devcontainerJsonPath = path.join(outputPath, 'devcontainer.json');
|
|
167
|
-
const dockerComposePath = path.join(outputPath, 'docker-compose.yml');
|
|
168
|
-
const devcontainerSubdir = path.join(outputPath, '.devcontainer');
|
|
169
|
-
const manifestPath = path.join(outputPath, 'superposition.json');
|
|
170
|
-
// Determine what exists
|
|
171
|
-
const hasDevcontainerJson = fs.existsSync(devcontainerJsonPath);
|
|
172
|
-
const hasDockerCompose = fs.existsSync(dockerComposePath);
|
|
173
|
-
const hasDevcontainerSubdir = fs.existsSync(devcontainerSubdir) && fs.statSync(devcontainerSubdir).isDirectory();
|
|
174
|
-
const hasManifest = fs.existsSync(manifestPath);
|
|
175
|
-
if (!hasDevcontainerJson && !hasDockerCompose && !hasDevcontainerSubdir && !hasManifest) {
|
|
176
|
-
return null; // Nothing to backup
|
|
177
|
-
}
|
|
178
|
-
// Create timestamp
|
|
179
|
-
const timestamp = new Date()
|
|
180
|
-
.toISOString()
|
|
181
|
-
.replace(/:/g, '-')
|
|
182
|
-
.replace(/\..+/, '')
|
|
183
|
-
.replace('T', '-');
|
|
184
|
-
// Determine backup location - create next to outputPath, not inside it
|
|
185
|
-
const resolvedOutputPath = path.resolve(outputPath);
|
|
186
|
-
const outputParentDir = path.dirname(resolvedOutputPath);
|
|
187
|
-
const outputBaseName = path.basename(resolvedOutputPath);
|
|
188
|
-
const backupBaseName = outputBaseName === '.devcontainer' ? '.devcontainer' : outputBaseName;
|
|
189
|
-
const backupPath = backupDir
|
|
190
|
-
? path.resolve(backupDir)
|
|
191
|
-
: path.join(outputParentDir, `${backupBaseName}.backup-${timestamp}`);
|
|
192
|
-
// Create backup directory
|
|
193
|
-
fs.mkdirSync(backupPath, { recursive: true });
|
|
194
|
-
// Backup files and directories
|
|
195
|
-
if (hasDevcontainerJson) {
|
|
196
|
-
fs.copyFileSync(devcontainerJsonPath, path.join(backupPath, 'devcontainer.json'));
|
|
197
|
-
}
|
|
198
|
-
if (hasDockerCompose) {
|
|
199
|
-
fs.copyFileSync(dockerComposePath, path.join(backupPath, 'docker-compose.yml'));
|
|
200
|
-
}
|
|
201
|
-
if (hasDevcontainerSubdir) {
|
|
202
|
-
const destDir = path.join(backupPath, '.devcontainer');
|
|
203
|
-
await copyDirectory(devcontainerSubdir, destDir);
|
|
204
|
-
}
|
|
205
|
-
if (hasManifest) {
|
|
206
|
-
fs.copyFileSync(manifestPath, path.join(backupPath, 'superposition.json'));
|
|
207
|
-
}
|
|
208
|
-
// Also backup other common devcontainer files
|
|
209
|
-
const otherFiles = ['.env', '.env.example', '.gitignore', 'features', 'scripts'];
|
|
210
|
-
for (const file of otherFiles) {
|
|
211
|
-
const srcPath = path.join(outputPath, file);
|
|
212
|
-
if (fs.existsSync(srcPath)) {
|
|
213
|
-
const destPath = path.join(backupPath, file);
|
|
214
|
-
if (fs.statSync(srcPath).isDirectory()) {
|
|
215
|
-
await copyDirectory(srcPath, destPath);
|
|
216
|
-
}
|
|
217
|
-
else {
|
|
218
|
-
fs.copyFileSync(srcPath, destPath);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return backupPath;
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Recursively copy directory
|
|
226
|
-
*/
|
|
227
|
-
async function copyDirectory(src, dest) {
|
|
228
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
229
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
230
|
-
for (const entry of entries) {
|
|
231
|
-
const srcPath = path.join(src, entry.name);
|
|
232
|
-
const destPath = path.join(dest, entry.name);
|
|
233
|
-
if (entry.isDirectory()) {
|
|
234
|
-
await copyDirectory(srcPath, destPath);
|
|
235
|
-
}
|
|
236
|
-
else {
|
|
237
|
-
fs.copyFileSync(srcPath, destPath);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Ensure backup patterns are in .gitignore
|
|
243
|
-
*/
|
|
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);
|
|
248
|
-
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
249
|
-
const backupPatterns = [
|
|
250
|
-
'',
|
|
251
|
-
'# Container Superposition backups',
|
|
252
|
-
'.devcontainer.backup-*/',
|
|
253
|
-
'*.backup-*',
|
|
254
|
-
'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
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
329
|
/**
|
|
272
330
|
* Build checkbox choices for overlay selection with optional pre-selection
|
|
273
331
|
*/
|
|
@@ -297,7 +355,7 @@ function buildOverlayChoices(config, stack, categoryList, preselected) {
|
|
|
297
355
|
/**
|
|
298
356
|
* Interactive questionnaire with modern checkbox selections
|
|
299
357
|
*/
|
|
300
|
-
async function runQuestionnaire(manifest, manifestDir) {
|
|
358
|
+
async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetChoices, defaultAnswers) {
|
|
301
359
|
const config = loadOverlaysConfigWrapper();
|
|
302
360
|
// Pretty banner
|
|
303
361
|
console.log('\n' +
|
|
@@ -331,32 +389,50 @@ async function runQuestionnaire(manifest, manifestDir) {
|
|
|
331
389
|
try {
|
|
332
390
|
// Question 0: Optional preset selection
|
|
333
391
|
let usePreset = false;
|
|
334
|
-
|
|
335
|
-
let
|
|
392
|
+
// CLI preset takes precedence over manifest preset
|
|
393
|
+
let selectedPresetId = cliPresetId || manifest?.preset;
|
|
394
|
+
// CLI preset choices merged with manifest choices (CLI takes precedence)
|
|
395
|
+
let presetChoices = {
|
|
396
|
+
...(manifest?.presetChoices || {}),
|
|
397
|
+
...(cliPresetChoices || {}),
|
|
398
|
+
};
|
|
336
399
|
let presetGlueConfig;
|
|
337
400
|
const presetOverlaysFiltered = config.overlays.filter((o) => o.category === 'preset');
|
|
338
401
|
let presetOverlays = [];
|
|
339
402
|
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') {
|
|
403
|
+
// If a preset was pre-selected via CLI or manifest, skip the prompt
|
|
404
|
+
if (selectedPresetId) {
|
|
358
405
|
usePreset = true;
|
|
359
|
-
|
|
406
|
+
console.log(chalk.cyan(`\n📦 Using preset: ${selectedPresetId}\n`));
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
const defaultPreset = 'custom';
|
|
410
|
+
const presetChoice = (await select({
|
|
411
|
+
message: 'Start from a preset or build custom?',
|
|
412
|
+
choices: [
|
|
413
|
+
{
|
|
414
|
+
name: 'Custom (select overlays manually)',
|
|
415
|
+
value: 'custom',
|
|
416
|
+
description: 'Choose individual overlays yourself',
|
|
417
|
+
},
|
|
418
|
+
...presetOverlaysFiltered.map((p) => ({
|
|
419
|
+
name: p.name,
|
|
420
|
+
value: p.id,
|
|
421
|
+
description: p.description,
|
|
422
|
+
})),
|
|
423
|
+
],
|
|
424
|
+
default: defaultPreset,
|
|
425
|
+
}));
|
|
426
|
+
if (presetChoice !== 'custom') {
|
|
427
|
+
usePreset = true;
|
|
428
|
+
selectedPresetId = presetChoice;
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
// User chose custom - discard any pre-provided preset choices so the
|
|
432
|
+
// manifest cannot end up with presetChoices but no preset.
|
|
433
|
+
presetChoices = {};
|
|
434
|
+
selectedPresetId = undefined;
|
|
435
|
+
}
|
|
360
436
|
}
|
|
361
437
|
}
|
|
362
438
|
// Question 1: Base template
|
|
@@ -367,11 +443,11 @@ async function runQuestionnaire(manifest, manifestDir) {
|
|
|
367
443
|
value: t.id,
|
|
368
444
|
description: t.description,
|
|
369
445
|
})),
|
|
370
|
-
default: manifest?.baseTemplate,
|
|
446
|
+
default: manifest?.baseTemplate || defaultAnswers?.stack,
|
|
371
447
|
}));
|
|
372
|
-
// If using preset, expand it now
|
|
448
|
+
// If using preset, expand it now (pass pre-provided choices to skip those prompts)
|
|
373
449
|
if (usePreset && selectedPresetId) {
|
|
374
|
-
const expansion = await expandPreset(selectedPresetId, stack);
|
|
450
|
+
const expansion = await expandPreset(selectedPresetId, stack, presetChoices);
|
|
375
451
|
if (!expansion.overlays || expansion.overlays.length === 0) {
|
|
376
452
|
// Preset failed to expand (e.g., missing or invalid preset definition).
|
|
377
453
|
// Treat this as "no preset" so the manifest does not incorrectly record one.
|
|
@@ -392,7 +468,15 @@ async function runQuestionnaire(manifest, manifestDir) {
|
|
|
392
468
|
// Check if manifest has a custom image or a known base image
|
|
393
469
|
const knownBaseImageIds = config.base_images.map((img) => img.id);
|
|
394
470
|
const manifestBaseImageIsKnown = manifest?.baseImage && knownBaseImageIds.includes(manifest.baseImage);
|
|
395
|
-
const manifestDefaultBaseImage = manifestBaseImageIsKnown
|
|
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);
|
|
396
480
|
const baseImage = (await select({
|
|
397
481
|
message: 'Select base image:',
|
|
398
482
|
choices: config.base_images.map((img) => ({
|
|
@@ -400,16 +484,17 @@ async function runQuestionnaire(manifest, manifestDir) {
|
|
|
400
484
|
value: img.id,
|
|
401
485
|
description: img.description,
|
|
402
486
|
})),
|
|
403
|
-
default:
|
|
487
|
+
default: defaultBaseImage,
|
|
404
488
|
}));
|
|
405
489
|
// Question 2a: If custom, ask for image name
|
|
406
490
|
let customImage;
|
|
407
491
|
if (baseImage === 'custom') {
|
|
408
492
|
// If manifest has a custom image, use it as default
|
|
409
493
|
const manifestCustomImage = !manifestBaseImageIsKnown && manifest?.baseImage ? manifest.baseImage : undefined;
|
|
494
|
+
const defaultCustomImage = manifestCustomImage || defaultAnswers?.customImage;
|
|
410
495
|
customImage = await input({
|
|
411
496
|
message: 'Enter custom Docker image (e.g., ubuntu:22.04):',
|
|
412
|
-
default:
|
|
497
|
+
default: defaultCustomImage,
|
|
413
498
|
validate: (value) => {
|
|
414
499
|
if (!value || value.trim() === '') {
|
|
415
500
|
return 'Image name is required';
|
|
@@ -525,7 +610,15 @@ async function runQuestionnaire(manifest, manifestDir) {
|
|
|
525
610
|
else {
|
|
526
611
|
// Custom mode: Normal overlay selection
|
|
527
612
|
console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm\n'));
|
|
528
|
-
const
|
|
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);
|
|
529
622
|
userSelection = await checkbox({
|
|
530
623
|
message: 'Select overlays to include:',
|
|
531
624
|
choices,
|
|
@@ -602,11 +695,11 @@ async function runQuestionnaire(manifest, manifestDir) {
|
|
|
602
695
|
// Question 4: Container name
|
|
603
696
|
const containerName = await input({
|
|
604
697
|
message: 'Container/project name (optional):',
|
|
605
|
-
default: manifest?.containerName || '',
|
|
698
|
+
default: manifest?.containerName || defaultAnswers?.containerName || '',
|
|
606
699
|
});
|
|
607
700
|
// Question 5: Output path
|
|
608
701
|
// If manifest provided, default to its location; otherwise use ./.devcontainer
|
|
609
|
-
const defaultOutput = manifestDir || './.devcontainer';
|
|
702
|
+
const defaultOutput = manifestDir || defaultAnswers?.outputPath || './.devcontainer';
|
|
610
703
|
const outputPath = await input({
|
|
611
704
|
message: 'Output path:',
|
|
612
705
|
default: defaultOutput,
|
|
@@ -614,7 +707,11 @@ async function runQuestionnaire(manifest, manifestDir) {
|
|
|
614
707
|
// Question 6: Port offset (optional, for running multiple instances)
|
|
615
708
|
const portOffsetInput = await input({
|
|
616
709
|
message: 'Port offset (leave empty for default ports, e.g., 100 to avoid conflicts):',
|
|
617
|
-
default: manifest?.portOffset
|
|
710
|
+
default: manifest?.portOffset !== undefined
|
|
711
|
+
? String(manifest.portOffset)
|
|
712
|
+
: defaultAnswers?.portOffset !== undefined
|
|
713
|
+
? String(defaultAnswers.portOffset)
|
|
714
|
+
: '',
|
|
618
715
|
});
|
|
619
716
|
const portOffset = portOffsetInput ? parseInt(portOffsetInput, 10) : undefined;
|
|
620
717
|
// Parse selected overlays into categories
|
|
@@ -709,7 +806,10 @@ async function runQuestionnaire(manifest, manifestDir) {
|
|
|
709
806
|
observability,
|
|
710
807
|
outputPath,
|
|
711
808
|
portOffset,
|
|
712
|
-
target,
|
|
809
|
+
target: target ?? defaultAnswers?.target,
|
|
810
|
+
minimal: defaultAnswers?.minimal,
|
|
811
|
+
editor: defaultAnswers?.editor,
|
|
812
|
+
customizations: defaultAnswers?.customizations,
|
|
713
813
|
};
|
|
714
814
|
}
|
|
715
815
|
catch (error) {
|
|
@@ -879,9 +979,10 @@ async function parseCliArgs() {
|
|
|
879
979
|
program
|
|
880
980
|
.command('init', { isDefault: true })
|
|
881
981
|
.description('Initialize a new devcontainer configuration')
|
|
982
|
+
.option('--from-project', 'Load configuration from the repository project file')
|
|
882
983
|
.option('--from-manifest <path>', 'Load configuration from existing superposition.json manifest')
|
|
883
|
-
.option('--no-interactive', 'Use
|
|
884
|
-
.option('--
|
|
984
|
+
.option('--no-interactive', 'Use persisted input values directly without questionnaire')
|
|
985
|
+
.option('--backup', 'Force or suppress backup; default is --no-backup inside a git repo, --backup outside')
|
|
885
986
|
.option('--backup-dir <path>', 'Custom backup directory location')
|
|
886
987
|
.option('--stack <type>', 'Base template: plain, compose')
|
|
887
988
|
.option('--language <list>', 'Comma-separated language overlays: dotnet, nodejs, python, mkdocs, java, go, rust, bun, powershell')
|
|
@@ -896,47 +997,34 @@ async function parseCliArgs() {
|
|
|
896
997
|
.option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
|
|
897
998
|
.option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
|
|
898
999
|
.option('--write-manifest-only', 'Generate only superposition.json manifest without creating .devcontainer/ files')
|
|
899
|
-
.
|
|
1000
|
+
.option('--preset <id>', 'Start from a preset (e.g., web-api, microservice)')
|
|
1001
|
+
.option('--preset-param <value>', 'Set a preset parameter value (format: key=value, can be repeated)', (value, previous) => previous.concat([value]), [])
|
|
1002
|
+
.action((options, command) => {
|
|
900
1003
|
// Store options for main() to process
|
|
901
|
-
initOptions =
|
|
1004
|
+
initOptions = {
|
|
1005
|
+
...options,
|
|
1006
|
+
commandName: 'init',
|
|
1007
|
+
_targetSource: command.getOptionValueSource('target'),
|
|
1008
|
+
_editorSource: command.getOptionValueSource('editor'),
|
|
1009
|
+
};
|
|
902
1010
|
});
|
|
903
1011
|
// Regen command
|
|
904
1012
|
program
|
|
905
1013
|
.command('regen')
|
|
906
|
-
.description('Regenerate devcontainer from existing superposition.json manifest')
|
|
1014
|
+
.description('Regenerate devcontainer from a project file or existing superposition.json manifest')
|
|
1015
|
+
.option('--from-project', 'Load configuration from the repository project file')
|
|
1016
|
+
.option('--from-manifest <path>', 'Load configuration from existing superposition.json manifest')
|
|
907
1017
|
.option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
|
|
908
|
-
.option('--
|
|
1018
|
+
.option('--backup', 'Force or suppress backup; default is --no-backup inside a git repo, --backup outside')
|
|
909
1019
|
.option('--backup-dir <path>', 'Custom backup directory location')
|
|
910
1020
|
.option('--minimal', 'Minimal mode - exclude optional/nice-to-have features and extensions')
|
|
911
1021
|
.option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
|
|
912
|
-
.action((options) => {
|
|
913
|
-
const outputPath = options.output || './.devcontainer';
|
|
914
|
-
// Look for manifest in multiple locations for team workflow:
|
|
915
|
-
// 1. Current directory (./superposition.json) - team workflow
|
|
916
|
-
// 2. Output directory (e.g., ./.devcontainer/superposition.json) - legacy
|
|
917
|
-
const manifestSearchPaths = [
|
|
918
|
-
'superposition.json',
|
|
919
|
-
path.join(outputPath, 'superposition.json'),
|
|
920
|
-
];
|
|
921
|
-
let manifestPath = null;
|
|
922
|
-
for (const searchPath of manifestSearchPaths) {
|
|
923
|
-
if (fs.existsSync(searchPath)) {
|
|
924
|
-
manifestPath = searchPath;
|
|
925
|
-
break;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
if (!manifestPath) {
|
|
929
|
-
console.error(chalk.red(`✗ Error: No manifest found`));
|
|
930
|
-
console.error(chalk.gray(' Searched for: ./superposition.json, ' +
|
|
931
|
-
path.join(outputPath, 'superposition.json')));
|
|
932
|
-
console.error(chalk.gray(' Run "container-superposition init --write-manifest-only" to create a manifest'));
|
|
933
|
-
process.exit(1);
|
|
934
|
-
}
|
|
935
|
-
// Store options for main() to process
|
|
1022
|
+
.action((options, command) => {
|
|
936
1023
|
initOptions = {
|
|
937
1024
|
...options,
|
|
938
|
-
|
|
1025
|
+
commandName: 'regen',
|
|
939
1026
|
interactive: false,
|
|
1027
|
+
_editorSource: command.getOptionValueSource('editor'),
|
|
940
1028
|
};
|
|
941
1029
|
});
|
|
942
1030
|
// List command
|
|
@@ -966,9 +1054,15 @@ async function parseCliArgs() {
|
|
|
966
1054
|
program
|
|
967
1055
|
.command('plan')
|
|
968
1056
|
.description('Preview what will be generated before creating devcontainer')
|
|
969
|
-
.option('--stack <type>', 'Base template: plain, compose'
|
|
1057
|
+
.option('--stack <type>', 'Base template: plain, compose')
|
|
970
1058
|
.option('--overlays <list>', 'Comma-separated list of overlay IDs')
|
|
1059
|
+
.option('--from-manifest <path>', 'Load stack and overlays from an existing superposition.json manifest')
|
|
971
1060
|
.option('--port-offset <number>', 'Add offset to all exposed ports', (val) => parseInt(val, 10), 0)
|
|
1061
|
+
.option('-o, --output <path>', 'Compare against existing config at this path (default: ./.devcontainer)')
|
|
1062
|
+
.option('--diff', 'Compare planned output vs existing configuration')
|
|
1063
|
+
.option('--diff-format <format>', 'Diff output format: color (default), json', 'color')
|
|
1064
|
+
.option('--diff-context <lines>', 'Context lines in diff output', (val) => parseInt(val, 10), 3)
|
|
1065
|
+
.option('--verbose', 'Explain why each overlay was included in the resolved plan')
|
|
972
1066
|
.option('--json', 'Output as JSON for scripting')
|
|
973
1067
|
.action(async (options) => {
|
|
974
1068
|
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
@@ -986,6 +1080,39 @@ async function parseCliArgs() {
|
|
|
986
1080
|
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
987
1081
|
await doctorCommand(overlaysConfig, OVERLAYS_DIR, options);
|
|
988
1082
|
});
|
|
1083
|
+
// Adopt command
|
|
1084
|
+
program
|
|
1085
|
+
.command('adopt')
|
|
1086
|
+
.description('Analyse an existing .devcontainer/ and suggest an equivalent overlay-based configuration')
|
|
1087
|
+
.option('-d, --dir <path>', 'Path to the existing .devcontainer directory (default: ./.devcontainer)')
|
|
1088
|
+
.option('--dry-run', 'Print analysis and suggested command only; no files written')
|
|
1089
|
+
.option('--force', 'Overwrite existing superposition.json if present')
|
|
1090
|
+
.option('--backup', 'Force backup creation even when inside a git repo (default: backup only outside git repos)')
|
|
1091
|
+
.option('--no-backup', 'Disable backup creation even when it would normally be performed')
|
|
1092
|
+
.option('--backup-dir <path>', 'Custom backup directory location')
|
|
1093
|
+
.option('--json', 'Output as JSON for scripting')
|
|
1094
|
+
.action(async (options) => {
|
|
1095
|
+
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
1096
|
+
await adoptCommand(overlaysConfig, OVERLAYS_DIR, options);
|
|
1097
|
+
process.exit(0);
|
|
1098
|
+
});
|
|
1099
|
+
// Hash command
|
|
1100
|
+
program
|
|
1101
|
+
.command('hash')
|
|
1102
|
+
.description('Compute a deterministic fingerprint for a given configuration')
|
|
1103
|
+
.option('--stack <type>', 'Base template: plain, compose')
|
|
1104
|
+
.option('--overlays <list>', 'Comma-separated list of overlay IDs')
|
|
1105
|
+
.option('--preset <id>', 'Preset ID (optional)')
|
|
1106
|
+
.option('--base <image>', 'Base image / distro variant (e.g. bookworm, alpine)')
|
|
1107
|
+
.option('--manifest <path>', 'Path to superposition.json manifest')
|
|
1108
|
+
.option('-o, --output <path>', 'Directory to write hash file (used with --write)')
|
|
1109
|
+
.option('--write', 'Write hash to .devcontainer/superposition.hash')
|
|
1110
|
+
.option('--json', 'Output as JSON for scripting')
|
|
1111
|
+
.action(async (options) => {
|
|
1112
|
+
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
1113
|
+
await hashCommand(overlaysConfig, OVERLAYS_DIR, options);
|
|
1114
|
+
process.exit(0);
|
|
1115
|
+
});
|
|
989
1116
|
await program.parseAsync(process.argv);
|
|
990
1117
|
// If init or regen command was run, return the options
|
|
991
1118
|
if (!initOptions) {
|
|
@@ -996,6 +1123,34 @@ async function parseCliArgs() {
|
|
|
996
1123
|
if (Object.keys(initOptions).length === 0) {
|
|
997
1124
|
return null;
|
|
998
1125
|
}
|
|
1126
|
+
const hasSourceFlags = Number(Boolean(initOptions.fromProject)) + Number(Boolean(initOptions.fromManifest));
|
|
1127
|
+
if (hasSourceFlags > 1) {
|
|
1128
|
+
console.error(chalk.red('✗ Error: --from-project and --from-manifest cannot be used together'));
|
|
1129
|
+
process.exit(1);
|
|
1130
|
+
}
|
|
1131
|
+
const sourceSelectionConflicts = [
|
|
1132
|
+
'stack',
|
|
1133
|
+
'language',
|
|
1134
|
+
'database',
|
|
1135
|
+
'observability',
|
|
1136
|
+
'playwright',
|
|
1137
|
+
'cloudTools',
|
|
1138
|
+
'devTools',
|
|
1139
|
+
'portOffset',
|
|
1140
|
+
'preset',
|
|
1141
|
+
];
|
|
1142
|
+
const hasPresetParams = Array.isArray(initOptions.presetParam) && initOptions.presetParam.length > 0;
|
|
1143
|
+
const conflictingSelectionFlags = sourceSelectionConflicts.filter((key) => initOptions[key] !== undefined && initOptions[key] !== false);
|
|
1144
|
+
if ((initOptions.fromProject || initOptions.fromManifest) &&
|
|
1145
|
+
(conflictingSelectionFlags.length > 0 || hasPresetParams)) {
|
|
1146
|
+
const conflicts = [...conflictingSelectionFlags.map((key) => `--${key}`)];
|
|
1147
|
+
if (hasPresetParams) {
|
|
1148
|
+
conflicts.push('--preset-param');
|
|
1149
|
+
}
|
|
1150
|
+
console.error(chalk.red(`✗ Error: Persisted input sources cannot be combined with clean-generation selection flags: ${conflicts.join(', ')}`));
|
|
1151
|
+
console.error(chalk.dim(' Choose either a persisted input source (--from-project or --from-manifest) or direct selection flags for that run.'));
|
|
1152
|
+
process.exit(1);
|
|
1153
|
+
}
|
|
999
1154
|
const config = {};
|
|
1000
1155
|
if (initOptions.stack)
|
|
1001
1156
|
config.stack = initOptions.stack;
|
|
@@ -1027,13 +1182,13 @@ async function parseCliArgs() {
|
|
|
1027
1182
|
if (initOptions.portOffset) {
|
|
1028
1183
|
config.portOffset = parseInt(initOptions.portOffset, 10);
|
|
1029
1184
|
}
|
|
1030
|
-
if (initOptions.target) {
|
|
1185
|
+
if (initOptions.target && initOptions._targetSource !== 'default') {
|
|
1031
1186
|
config.target = initOptions.target;
|
|
1032
1187
|
}
|
|
1033
1188
|
if (initOptions.minimal) {
|
|
1034
1189
|
config.minimal = true;
|
|
1035
1190
|
}
|
|
1036
|
-
if (initOptions.editor) {
|
|
1191
|
+
if (initOptions.editor && initOptions._editorSource !== 'default') {
|
|
1037
1192
|
const editorLower = initOptions.editor.toLowerCase();
|
|
1038
1193
|
if (['vscode', 'jetbrains', 'none'].includes(editorLower)) {
|
|
1039
1194
|
config.editor = editorLower;
|
|
@@ -1045,10 +1200,42 @@ async function parseCliArgs() {
|
|
|
1045
1200
|
}
|
|
1046
1201
|
if (initOptions.output)
|
|
1047
1202
|
config.outputPath = initOptions.output;
|
|
1203
|
+
// Handle --preset flag
|
|
1204
|
+
if (initOptions.preset) {
|
|
1205
|
+
config.preset = initOptions.preset;
|
|
1206
|
+
}
|
|
1207
|
+
// Handle --preset-param flags (can be repeated)
|
|
1208
|
+
if (initOptions.presetParam && initOptions.presetParam.length > 0) {
|
|
1209
|
+
if (!initOptions.preset) {
|
|
1210
|
+
console.warn(chalk.yellow('⚠️ Ignoring --preset-param because no --preset was provided. ' +
|
|
1211
|
+
'Preset parameters only apply when a preset is selected (e.g., --preset web-api --preset-param broker=nats).'));
|
|
1212
|
+
}
|
|
1213
|
+
else {
|
|
1214
|
+
const presetChoices = {};
|
|
1215
|
+
for (const param of initOptions.presetParam) {
|
|
1216
|
+
const eqIdx = param.indexOf('=');
|
|
1217
|
+
if (eqIdx > 0) {
|
|
1218
|
+
const key = param.slice(0, eqIdx).trim();
|
|
1219
|
+
const value = param.slice(eqIdx + 1).trim();
|
|
1220
|
+
if (key) {
|
|
1221
|
+
presetChoices[key] = value;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
else {
|
|
1225
|
+
console.warn(chalk.yellow(`⚠️ Invalid --preset-param format: "${param}". Expected "key=value" (e.g., --preset-param broker=nats).`));
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
if (Object.keys(presetChoices).length > 0) {
|
|
1229
|
+
config.presetChoices = presetChoices;
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1048
1233
|
return {
|
|
1234
|
+
commandName: initOptions.commandName,
|
|
1049
1235
|
config,
|
|
1050
1236
|
manifestPath: initOptions.fromManifest,
|
|
1051
|
-
|
|
1237
|
+
fromProject: initOptions.fromProject === true,
|
|
1238
|
+
backupOverride: initOptions.backup, // undefined = auto-detect; true = --backup; false = --no-backup
|
|
1052
1239
|
backupDir: initOptions.backupDir,
|
|
1053
1240
|
noInteractive: initOptions.interactive === false, // Commander creates options.interactive = false for --no-interactive
|
|
1054
1241
|
writeManifestOnly: initOptions.writeManifestOnly === true,
|
|
@@ -1057,17 +1244,34 @@ async function parseCliArgs() {
|
|
|
1057
1244
|
async function main() {
|
|
1058
1245
|
try {
|
|
1059
1246
|
const cliArgs = await parseCliArgs();
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1247
|
+
let projectConfig = undefined;
|
|
1248
|
+
let projectConfigAnswers;
|
|
1249
|
+
if (!cliArgs?.manifestPath) {
|
|
1250
|
+
projectConfig =
|
|
1251
|
+
loadProjectConfig(loadOverlaysConfigWrapper(), process.cwd()) ?? undefined;
|
|
1252
|
+
if (projectConfig) {
|
|
1253
|
+
projectConfigAnswers = applyPresetSelections(buildAnswersFromProjectConfig(projectConfig.selection));
|
|
1254
|
+
}
|
|
1065
1255
|
}
|
|
1066
1256
|
let manifest;
|
|
1067
1257
|
let manifestDir;
|
|
1068
|
-
let shouldBackup = true;
|
|
1069
1258
|
let backupDir;
|
|
1070
1259
|
let useManifestOnly = false;
|
|
1260
|
+
let useProjectOnly = false;
|
|
1261
|
+
if (cliArgs?.commandName === 'regen' && !cliArgs.manifestPath && !cliArgs.fromProject) {
|
|
1262
|
+
if (projectConfigAnswers) {
|
|
1263
|
+
useProjectOnly = true;
|
|
1264
|
+
}
|
|
1265
|
+
else {
|
|
1266
|
+
const discoveredManifestPath = findDefaultRegenManifest(cliArgs?.config?.outputPath || './.devcontainer');
|
|
1267
|
+
if (!discoveredManifestPath) {
|
|
1268
|
+
console.error(chalk.red('✗ Error: No project file or manifest found'));
|
|
1269
|
+
console.error(chalk.gray(' Looked for .superposition.yml or superposition.yml in the repository root, and superposition.json in common manifest locations.'));
|
|
1270
|
+
process.exit(1);
|
|
1271
|
+
}
|
|
1272
|
+
cliArgs.manifestPath = discoveredManifestPath;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1071
1275
|
// Handle manifest loading
|
|
1072
1276
|
if (cliArgs?.manifestPath) {
|
|
1073
1277
|
const manifestPath = findManifestFile(cliArgs.manifestPath);
|
|
@@ -1082,10 +1286,7 @@ async function main() {
|
|
|
1082
1286
|
process.exit(1);
|
|
1083
1287
|
}
|
|
1084
1288
|
manifest = loadedManifest;
|
|
1085
|
-
// Check for
|
|
1086
|
-
if (cliArgs.noBackup) {
|
|
1087
|
-
shouldBackup = false;
|
|
1088
|
-
}
|
|
1289
|
+
// Check for interaction options
|
|
1089
1290
|
if (cliArgs.backupDir) {
|
|
1090
1291
|
backupDir = cliArgs.backupDir;
|
|
1091
1292
|
}
|
|
@@ -1093,22 +1294,72 @@ async function main() {
|
|
|
1093
1294
|
useManifestOnly = true;
|
|
1094
1295
|
}
|
|
1095
1296
|
}
|
|
1297
|
+
if (cliArgs?.fromProject) {
|
|
1298
|
+
if (!projectConfigAnswers || !projectConfig) {
|
|
1299
|
+
console.error(chalk.red('✗ Could not find project file'));
|
|
1300
|
+
console.error(chalk.red(' Searched for: .superposition.yml, superposition.yml'));
|
|
1301
|
+
process.exit(1);
|
|
1302
|
+
}
|
|
1303
|
+
useProjectOnly = cliArgs.noInteractive || cliArgs.commandName === 'regen';
|
|
1304
|
+
}
|
|
1305
|
+
// Validate --no-interactive requires a persisted input source
|
|
1306
|
+
if (cliArgs?.noInteractive && !cliArgs?.manifestPath && !projectConfigAnswers) {
|
|
1307
|
+
console.error(chalk.red('✗ Error: --no-interactive requires persisted input'));
|
|
1308
|
+
console.error(chalk.dim(' Use --from-project, --from-manifest <path>, or run from a repository with .superposition.yml or superposition.yml'));
|
|
1309
|
+
process.exit(1);
|
|
1310
|
+
}
|
|
1311
|
+
// Determine whether to create a backup:
|
|
1312
|
+
// --backup → always backup
|
|
1313
|
+
// --no-backup → never backup
|
|
1314
|
+
// (neither) → backup only when NOT inside a git repository
|
|
1315
|
+
// (git already tracks history, so backups are redundant)
|
|
1316
|
+
const resolvedOutputPath = cliArgs?.config?.outputPath ||
|
|
1317
|
+
projectConfigAnswers?.outputPath ||
|
|
1318
|
+
manifestDir ||
|
|
1319
|
+
'./.devcontainer';
|
|
1320
|
+
const backupCheckPath = path.resolve(resolvedOutputPath);
|
|
1321
|
+
const inGitRepo = isInsideGitRepo(backupCheckPath);
|
|
1322
|
+
let shouldBackup;
|
|
1323
|
+
if (cliArgs?.backupOverride === true) {
|
|
1324
|
+
shouldBackup = true;
|
|
1325
|
+
}
|
|
1326
|
+
else if (cliArgs?.backupOverride === false) {
|
|
1327
|
+
shouldBackup = false;
|
|
1328
|
+
}
|
|
1329
|
+
else {
|
|
1330
|
+
// Auto-detect based on git presence
|
|
1331
|
+
shouldBackup = !inGitRepo;
|
|
1332
|
+
if (!shouldBackup) {
|
|
1333
|
+
console.log(chalk.dim('ℹ Skipping backup — git repo detected (use --backup to force one)\n'));
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
const isReplayMode = cliArgs?.commandName === 'regen' || useManifestOnly || useProjectOnly;
|
|
1096
1337
|
// Create backup if needed
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
const outputPath =
|
|
1338
|
+
let actualBackupPath;
|
|
1339
|
+
if (shouldBackup && isReplayMode) {
|
|
1340
|
+
const outputPath = resolvedOutputPath;
|
|
1100
1341
|
const backupPath = await createBackup(outputPath, backupDir);
|
|
1101
1342
|
if (backupPath) {
|
|
1343
|
+
actualBackupPath = backupPath;
|
|
1102
1344
|
console.log(chalk.green(`✓ Backup created: ${backupPath}\n`));
|
|
1103
|
-
|
|
1345
|
+
ensureBackupPatternsInGitignore(outputPath);
|
|
1104
1346
|
}
|
|
1105
1347
|
}
|
|
1106
1348
|
// Build answers based on mode
|
|
1107
1349
|
let answers;
|
|
1108
|
-
// Check if there are CLI overrides beyond just output path
|
|
1350
|
+
// Check if there are CLI overrides beyond just output path and preset flags
|
|
1351
|
+
// Preset/presetChoices alone don't constitute "CLI overrides" that bypass interactive mode
|
|
1109
1352
|
const hasCliOverrides = cliArgs &&
|
|
1110
1353
|
Object.keys(cliArgs.config).some((key) => key !== 'outputPath' &&
|
|
1354
|
+
key !== 'preset' &&
|
|
1355
|
+
key !== 'presetChoices' &&
|
|
1356
|
+
!(key === 'target' && cliArgs.config.target === 'local') &&
|
|
1357
|
+
!(key === 'editor' && cliArgs.config.editor === 'vscode') &&
|
|
1111
1358
|
cliArgs.config[key] !== undefined);
|
|
1359
|
+
const hasAnyCliConfig = cliArgs &&
|
|
1360
|
+
Object.entries(cliArgs.config).some(([key, value]) => value !== undefined &&
|
|
1361
|
+
!(key === 'target' && value === 'local') &&
|
|
1362
|
+
!(key === 'editor' && value === 'vscode'));
|
|
1112
1363
|
if (useManifestOnly && manifest && !hasCliOverrides) {
|
|
1113
1364
|
// Mode 1: Manifest-only (--from-manifest --no-interactive, no CLI overrides)
|
|
1114
1365
|
const manifestAnswers = buildAnswersFromManifest(manifest, manifestDir);
|
|
@@ -1128,19 +1379,44 @@ async function main() {
|
|
|
1128
1379
|
: '') +
|
|
1129
1380
|
chalk.gray(` Output: ${answers.outputPath}`), { padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: 1 }));
|
|
1130
1381
|
}
|
|
1131
|
-
else if (
|
|
1382
|
+
else if (useProjectOnly && projectConfigAnswers && !hasCliOverrides) {
|
|
1383
|
+
const projectFileName = projectConfig?.file.fileName ?? '.superposition.yml';
|
|
1384
|
+
answers = mergeAnswers(projectConfigAnswers, {
|
|
1385
|
+
outputPath: cliArgs?.config?.outputPath ||
|
|
1386
|
+
projectConfigAnswers.outputPath ||
|
|
1387
|
+
'./.devcontainer',
|
|
1388
|
+
minimal: cliArgs?.config?.minimal,
|
|
1389
|
+
editor: cliArgs?.config?.editor,
|
|
1390
|
+
});
|
|
1391
|
+
console.log('\n' +
|
|
1392
|
+
boxen(chalk.bold.cyan('Regenerating from Project File (No Interactive)\n\n') +
|
|
1393
|
+
chalk.white('Configuration:\n') +
|
|
1394
|
+
chalk.gray(` Project file: ${projectFileName}\n`) +
|
|
1395
|
+
chalk.gray(` Output: ${answers.outputPath}`), {
|
|
1396
|
+
padding: 1,
|
|
1397
|
+
borderColor: 'cyan',
|
|
1398
|
+
borderStyle: 'round',
|
|
1399
|
+
margin: 1,
|
|
1400
|
+
}));
|
|
1401
|
+
}
|
|
1402
|
+
else if ((cliArgs && (cliArgs.config.stack || hasCliOverrides)) ||
|
|
1403
|
+
(projectConfigAnswers && (cliArgs?.noInteractive || hasAnyCliConfig))) {
|
|
1132
1404
|
// Mode 2: CLI-based (with optional manifest defaults)
|
|
1133
1405
|
// This includes regen with --minimal or --editor flags
|
|
1134
1406
|
const cliAnswers = buildAnswersFromCliArgs(cliArgs.config);
|
|
1135
1407
|
const manifestAnswers = manifest
|
|
1136
1408
|
? buildAnswersFromManifest(manifest, manifestDir)
|
|
1137
1409
|
: undefined;
|
|
1138
|
-
answers = mergeAnswers(manifestAnswers, cliAnswers, {
|
|
1139
|
-
outputPath: cliAnswers.outputPath || './.devcontainer',
|
|
1410
|
+
answers = mergeAnswers(projectConfigAnswers, manifestAnswers, cliAnswers, {
|
|
1411
|
+
outputPath: cliAnswers.outputPath || projectConfigAnswers?.outputPath || './.devcontainer',
|
|
1140
1412
|
});
|
|
1141
1413
|
const modeLabel = useManifestOnly && hasCliOverrides
|
|
1142
1414
|
? 'Regenerating from Manifest with Overrides'
|
|
1143
|
-
:
|
|
1415
|
+
: useProjectOnly && projectConfigAnswers
|
|
1416
|
+
? 'Regenerating from Project File with Overrides'
|
|
1417
|
+
: projectConfigAnswers && !manifest
|
|
1418
|
+
? 'Running from Project Config'
|
|
1419
|
+
: 'Running in CLI mode';
|
|
1144
1420
|
console.log('\n' +
|
|
1145
1421
|
boxen(chalk.bold(modeLabel), {
|
|
1146
1422
|
padding: 0.5,
|
|
@@ -1148,7 +1424,7 @@ async function main() {
|
|
|
1148
1424
|
borderStyle: 'round',
|
|
1149
1425
|
}));
|
|
1150
1426
|
// Show what's being overridden
|
|
1151
|
-
if (useManifestOnly && hasCliOverrides) {
|
|
1427
|
+
if ((useManifestOnly || useProjectOnly) && hasCliOverrides) {
|
|
1152
1428
|
const overrides = [];
|
|
1153
1429
|
if (cliAnswers.minimal)
|
|
1154
1430
|
overrides.push('minimal mode');
|
|
@@ -1160,9 +1436,16 @@ async function main() {
|
|
|
1160
1436
|
}
|
|
1161
1437
|
}
|
|
1162
1438
|
else {
|
|
1163
|
-
// Mode 3: Interactive (with optional manifest pre-population)
|
|
1164
|
-
const interactiveAnswers = await runQuestionnaire(manifest, manifestDir);
|
|
1165
|
-
answers = mergeAnswers(interactiveAnswers);
|
|
1439
|
+
// Mode 3: Interactive (with optional manifest pre-population and CLI preset pre-selection)
|
|
1440
|
+
const interactiveAnswers = await runQuestionnaire(manifest, manifestDir, cliArgs?.config.preset || projectConfigAnswers?.preset, cliArgs?.config.presetChoices || projectConfigAnswers?.presetChoices, projectConfigAnswers);
|
|
1441
|
+
answers = mergeAnswers(projectConfigAnswers, interactiveAnswers);
|
|
1442
|
+
}
|
|
1443
|
+
if (!manifest && projectConfig?.selection.customizations) {
|
|
1444
|
+
const materializedOutputPath = path.resolve(answers.outputPath);
|
|
1445
|
+
if (!fs.existsSync(materializedOutputPath)) {
|
|
1446
|
+
fs.mkdirSync(materializedOutputPath, { recursive: true });
|
|
1447
|
+
}
|
|
1448
|
+
writeProjectConfigCustomizations(materializedOutputPath, projectConfig.selection.customizations);
|
|
1166
1449
|
}
|
|
1167
1450
|
// Show configuration summary
|
|
1168
1451
|
const summaryLines = [
|
|
@@ -1182,6 +1465,9 @@ async function main() {
|
|
|
1182
1465
|
if (answers.cloudTools && answers.cloudTools.length > 0) {
|
|
1183
1466
|
summaryLines.push(chalk.cyan('Cloud tools: ') + chalk.white(answers.cloudTools.join(', ')));
|
|
1184
1467
|
}
|
|
1468
|
+
if (projectConfig?.file && !manifest) {
|
|
1469
|
+
summaryLines.push(chalk.cyan('Project config: ') + chalk.white(projectConfig.file.fileName));
|
|
1470
|
+
}
|
|
1185
1471
|
summaryLines.push(chalk.cyan('Output: ') + chalk.white(answers.outputPath));
|
|
1186
1472
|
console.log('\n' +
|
|
1187
1473
|
boxen(summaryLines.join('\n'), {
|
|
@@ -1200,40 +1486,28 @@ async function main() {
|
|
|
1200
1486
|
color: 'cyan',
|
|
1201
1487
|
}).start();
|
|
1202
1488
|
try {
|
|
1489
|
+
let summary;
|
|
1203
1490
|
if (isManifestOnly) {
|
|
1204
|
-
await generateManifestOnly(answers
|
|
1491
|
+
summary = await generateManifestOnly(answers, undefined, {
|
|
1492
|
+
isRegen: isReplayMode,
|
|
1493
|
+
});
|
|
1205
1494
|
spinner.succeed(chalk.green('Manifest created successfully!'));
|
|
1206
1495
|
}
|
|
1207
1496
|
else {
|
|
1208
|
-
await composeDevContainer(answers);
|
|
1497
|
+
summary = await composeDevContainer(answers, undefined, { isRegen: isReplayMode });
|
|
1209
1498
|
spinner.succeed(chalk.green('DevContainer created successfully!'));
|
|
1210
1499
|
}
|
|
1500
|
+
// Update summary with backup path and regen status
|
|
1501
|
+
if (actualBackupPath) {
|
|
1502
|
+
summary.backupPath = actualBackupPath;
|
|
1503
|
+
}
|
|
1504
|
+
// Print comprehensive summary
|
|
1505
|
+
printSummary(summary);
|
|
1211
1506
|
}
|
|
1212
1507
|
catch (error) {
|
|
1213
1508
|
spinner.fail(chalk.red(isManifestOnly ? 'Failed to create manifest' : 'Failed to create devcontainer'));
|
|
1214
1509
|
throw error;
|
|
1215
1510
|
}
|
|
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
1511
|
}
|
|
1238
1512
|
catch (error) {
|
|
1239
1513
|
console.error('\n' +
|