container-superposition 0.1.4 → 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 -1370
- package/dist/scripts/init.js +333 -185
- 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/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 +2 -0
- package/dist/tool/commands/plan.d.ts.map +1 -1
- package/dist/tool/commands/plan.js +262 -42
- package/dist/tool/commands/plan.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 +39 -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/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 +60 -0
- 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/sdd.yml +84 -0
- 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/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/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/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
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
-
import { execSync } from 'child_process';
|
|
6
5
|
import { Command } from 'commander';
|
|
7
6
|
import chalk from 'chalk';
|
|
8
7
|
import boxen from 'boxen';
|
|
@@ -15,11 +14,14 @@ import { listCommand } from '../tool/commands/list.js';
|
|
|
15
14
|
import { explainCommand } from '../tool/commands/explain.js';
|
|
16
15
|
import { planCommand } from '../tool/commands/plan.js';
|
|
17
16
|
import { doctorCommand } from '../tool/commands/doctor.js';
|
|
17
|
+
import { adoptCommand } from '../tool/commands/adopt.js';
|
|
18
|
+
import { hashCommand } from '../tool/commands/hash.js';
|
|
18
19
|
import { getIncompatibleOverlays, DEPLOYMENT_TARGETS } from '../tool/schema/deployment-targets.js';
|
|
19
20
|
import { migrateManifest, needsMigration, detectManifestVersion, isVersionSupported, CURRENT_MANIFEST_VERSION, SUPPORTED_MANIFEST_VERSIONS, } from '../tool/schema/manifest-migrations.js';
|
|
20
21
|
import { getToolVersion } from '../tool/utils/version.js';
|
|
21
22
|
import { printSummary } from '../tool/utils/summary.js';
|
|
22
|
-
import {
|
|
23
|
+
import { buildAnswersFromProjectConfig, loadProjectConfig, writeProjectConfigCustomizations, } from '../tool/schema/project-config.js';
|
|
24
|
+
import { isInsideGitRepo, createBackup, ensureBackupPatternsInGitignore, } from '../tool/utils/backup.js';
|
|
23
25
|
// Get __dirname equivalent in ESM
|
|
24
26
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
27
|
const __dirname = path.dirname(__filename);
|
|
@@ -65,6 +67,100 @@ function loadPresetDefinition(presetId) {
|
|
|
65
67
|
const content = fs.readFileSync(presetPath, 'utf8');
|
|
66
68
|
return yaml.load(content);
|
|
67
69
|
}
|
|
70
|
+
function categorizeOverlayIds(overlayIds, config) {
|
|
71
|
+
const language = [];
|
|
72
|
+
const database = [];
|
|
73
|
+
const observability = [];
|
|
74
|
+
const cloudTools = [];
|
|
75
|
+
const devTools = [];
|
|
76
|
+
const overlayMap = new Map(config.overlays.map((o) => [o.id, o]));
|
|
77
|
+
for (const id of overlayIds) {
|
|
78
|
+
const overlay = overlayMap.get(id);
|
|
79
|
+
if (!overlay)
|
|
80
|
+
continue;
|
|
81
|
+
switch (overlay.category) {
|
|
82
|
+
case 'language':
|
|
83
|
+
language.push(id);
|
|
84
|
+
break;
|
|
85
|
+
case 'database':
|
|
86
|
+
database.push(id);
|
|
87
|
+
break;
|
|
88
|
+
case 'observability':
|
|
89
|
+
observability.push(id);
|
|
90
|
+
break;
|
|
91
|
+
case 'cloud':
|
|
92
|
+
cloudTools.push(id);
|
|
93
|
+
break;
|
|
94
|
+
case 'dev':
|
|
95
|
+
devTools.push(id);
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { language, database, observability, cloudTools, devTools };
|
|
100
|
+
}
|
|
101
|
+
function mergeUnique(left, right) {
|
|
102
|
+
const merged = [...(left ?? []), ...(right ?? [])];
|
|
103
|
+
return merged.length > 0 ? [...new Set(merged)] : undefined;
|
|
104
|
+
}
|
|
105
|
+
function expandPresetWithDefaults(presetId, stack, providedChoices = {}) {
|
|
106
|
+
const preset = loadPresetDefinition(presetId);
|
|
107
|
+
if (!preset) {
|
|
108
|
+
throw new Error(`Preset definition not found for ${presetId}`);
|
|
109
|
+
}
|
|
110
|
+
const overlays = [...preset.selects.required];
|
|
111
|
+
const choices = {};
|
|
112
|
+
if (preset.selects.userChoice) {
|
|
113
|
+
for (const [key, choice] of Object.entries(preset.selects.userChoice)) {
|
|
114
|
+
const selectedOption = providedChoices[key] ?? choice.defaultOption;
|
|
115
|
+
if (!selectedOption || !choice.options.includes(selectedOption)) {
|
|
116
|
+
const valid = choice.options.join(', ');
|
|
117
|
+
throw new Error(`Preset choice '${key}' must be one of: ${valid}`);
|
|
118
|
+
}
|
|
119
|
+
overlays.push(selectedOption);
|
|
120
|
+
choices[key] = selectedOption;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (preset.parameters) {
|
|
124
|
+
for (const [key, param] of Object.entries(preset.parameters)) {
|
|
125
|
+
const selectedId = providedChoices[key] ?? param.default;
|
|
126
|
+
const selectedOption = param.options.find((option) => option.id === selectedId);
|
|
127
|
+
if (!selectedOption) {
|
|
128
|
+
const valid = param.options.map((option) => option.id).join(', ');
|
|
129
|
+
throw new Error(`Preset parameter '${key}' must be one of: ${valid}`);
|
|
130
|
+
}
|
|
131
|
+
overlays.push(...selectedOption.overlays);
|
|
132
|
+
choices[key] = selectedId;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const uniqueOverlays = [...new Set(overlays)];
|
|
136
|
+
let resolvedGlueConfig = preset.glueConfig;
|
|
137
|
+
if (resolvedGlueConfig?.environment) {
|
|
138
|
+
const resolvedEnv = {};
|
|
139
|
+
for (const [envKey, envValue] of Object.entries(resolvedGlueConfig.environment)) {
|
|
140
|
+
resolvedEnv[envKey] = envValue.replace(/\{\{parameters\.(\w+)\.id\}\}/g, (_match, paramKey) => choices[paramKey] ?? _match);
|
|
141
|
+
}
|
|
142
|
+
resolvedGlueConfig = { ...resolvedGlueConfig, environment: resolvedEnv };
|
|
143
|
+
}
|
|
144
|
+
return { overlays: uniqueOverlays, choices, glueConfig: resolvedGlueConfig };
|
|
145
|
+
}
|
|
146
|
+
function applyPresetSelections(answers) {
|
|
147
|
+
if (!answers.preset) {
|
|
148
|
+
return answers;
|
|
149
|
+
}
|
|
150
|
+
const expansion = expandPresetWithDefaults(answers.preset, answers.stack ?? 'plain', answers.presetChoices ?? {});
|
|
151
|
+
const categories = categorizeOverlayIds(expansion.overlays, loadOverlaysConfigWrapper());
|
|
152
|
+
return {
|
|
153
|
+
...answers,
|
|
154
|
+
language: mergeUnique(categories.language, answers.language),
|
|
155
|
+
database: mergeUnique(categories.database, answers.database),
|
|
156
|
+
observability: mergeUnique(categories.observability, answers.observability),
|
|
157
|
+
cloudTools: mergeUnique(categories.cloudTools, answers.cloudTools),
|
|
158
|
+
devTools: mergeUnique(categories.devTools, answers.devTools),
|
|
159
|
+
playwright: answers.playwright ?? expansion.overlays.includes('playwright'),
|
|
160
|
+
presetChoices: Object.keys(expansion.choices).length > 0 ? expansion.choices : undefined,
|
|
161
|
+
presetGlueConfig: expansion.glueConfig,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
68
164
|
/**
|
|
69
165
|
* Expand a preset into a list of overlay IDs with user choices resolved
|
|
70
166
|
*/
|
|
@@ -141,7 +237,17 @@ async function expandPreset(presetId, stack, preProvidedChoices = {}) {
|
|
|
141
237
|
// Deduplicate overlays
|
|
142
238
|
const uniqueOverlays = [...new Set(overlays)];
|
|
143
239
|
console.log(chalk.dim(`✓ Preset will include: ${uniqueOverlays.join(', ')}\n`));
|
|
144
|
-
|
|
240
|
+
// Apply template substitution to glueConfig.environment values.
|
|
241
|
+
// Replaces {{parameters.<key>.id}} with the selected choice for <key>.
|
|
242
|
+
let resolvedGlueConfig = preset.glueConfig;
|
|
243
|
+
if (resolvedGlueConfig?.environment) {
|
|
244
|
+
const resolvedEnv = {};
|
|
245
|
+
for (const [envKey, envValue] of Object.entries(resolvedGlueConfig.environment)) {
|
|
246
|
+
resolvedEnv[envKey] = envValue.replace(/\{\{parameters\.(\w+)\.id\}\}/g, (_match, paramKey) => choices[paramKey] ?? _match);
|
|
247
|
+
}
|
|
248
|
+
resolvedGlueConfig = { ...resolvedGlueConfig, environment: resolvedEnv };
|
|
249
|
+
}
|
|
250
|
+
return { overlays: uniqueOverlays, choices, glueConfig: resolvedGlueConfig };
|
|
145
251
|
}
|
|
146
252
|
/**
|
|
147
253
|
* Search for manifest file in multiple locations
|
|
@@ -164,6 +270,16 @@ function findManifestFile(manifestPath) {
|
|
|
164
270
|
}
|
|
165
271
|
return null;
|
|
166
272
|
}
|
|
273
|
+
function findDefaultRegenManifest(outputPath = './.devcontainer') {
|
|
274
|
+
const manifestSearchPaths = ['superposition.json', path.join(outputPath, 'superposition.json')];
|
|
275
|
+
for (const searchPath of manifestSearchPaths) {
|
|
276
|
+
const resolvedPath = path.resolve(searchPath);
|
|
277
|
+
if (fs.existsSync(resolvedPath)) {
|
|
278
|
+
return resolvedPath;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
167
283
|
/**
|
|
168
284
|
* Load and validate manifest file
|
|
169
285
|
*/
|
|
@@ -210,127 +326,6 @@ function loadManifest(manifestPath) {
|
|
|
210
326
|
return null;
|
|
211
327
|
}
|
|
212
328
|
}
|
|
213
|
-
/**
|
|
214
|
-
* Detect whether a directory (or any of its parents) is inside a git repository.
|
|
215
|
-
* First tries `git rev-parse --git-dir`; if git is unavailable, falls back to
|
|
216
|
-
* walking up the directory tree looking for a `.git` entry.
|
|
217
|
-
*/
|
|
218
|
-
function isInsideGitRepo(dirPath) {
|
|
219
|
-
try {
|
|
220
|
-
execSync('git rev-parse --git-dir', { cwd: dirPath, stdio: 'ignore' });
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
// git command failed (not a repo) or git is not installed — walk up looking for .git
|
|
225
|
-
let current = path.resolve(dirPath);
|
|
226
|
-
while (true) {
|
|
227
|
-
if (fs.existsSync(path.join(current, '.git'))) {
|
|
228
|
-
return true;
|
|
229
|
-
}
|
|
230
|
-
const parent = path.dirname(current);
|
|
231
|
-
if (parent === current) {
|
|
232
|
-
break; // reached filesystem root
|
|
233
|
-
}
|
|
234
|
-
current = parent;
|
|
235
|
-
}
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
/**
|
|
240
|
-
* Create timestamped backup of existing devcontainer and manifest
|
|
241
|
-
*/
|
|
242
|
-
async function createBackup(outputPath, backupDir) {
|
|
243
|
-
// Check for devcontainer files to backup
|
|
244
|
-
const devcontainerJsonPath = path.join(outputPath, 'devcontainer.json');
|
|
245
|
-
const dockerComposePath = path.join(outputPath, 'docker-compose.yml');
|
|
246
|
-
const devcontainerSubdir = path.join(outputPath, '.devcontainer');
|
|
247
|
-
const manifestPath = path.join(outputPath, 'superposition.json');
|
|
248
|
-
// Determine what exists
|
|
249
|
-
const hasDevcontainerJson = fs.existsSync(devcontainerJsonPath);
|
|
250
|
-
const hasDockerCompose = fs.existsSync(dockerComposePath);
|
|
251
|
-
const hasDevcontainerSubdir = fs.existsSync(devcontainerSubdir) && fs.statSync(devcontainerSubdir).isDirectory();
|
|
252
|
-
const hasManifest = fs.existsSync(manifestPath);
|
|
253
|
-
if (!hasDevcontainerJson && !hasDockerCompose && !hasDevcontainerSubdir && !hasManifest) {
|
|
254
|
-
return null; // Nothing to backup
|
|
255
|
-
}
|
|
256
|
-
// Create timestamp
|
|
257
|
-
const timestamp = new Date()
|
|
258
|
-
.toISOString()
|
|
259
|
-
.replace(/:/g, '-')
|
|
260
|
-
.replace(/\..+/, '')
|
|
261
|
-
.replace('T', '-');
|
|
262
|
-
// Determine backup location - create next to outputPath, not inside it
|
|
263
|
-
const resolvedOutputPath = path.resolve(outputPath);
|
|
264
|
-
const outputParentDir = path.dirname(resolvedOutputPath);
|
|
265
|
-
const outputBaseName = path.basename(resolvedOutputPath);
|
|
266
|
-
const backupBaseName = outputBaseName === '.devcontainer' ? '.devcontainer' : outputBaseName;
|
|
267
|
-
const backupPath = backupDir
|
|
268
|
-
? path.resolve(backupDir)
|
|
269
|
-
: path.join(outputParentDir, `${backupBaseName}.backup-${timestamp}`);
|
|
270
|
-
// Create backup directory
|
|
271
|
-
fs.mkdirSync(backupPath, { recursive: true });
|
|
272
|
-
// Backup files and directories
|
|
273
|
-
if (hasDevcontainerJson) {
|
|
274
|
-
fs.copyFileSync(devcontainerJsonPath, path.join(backupPath, 'devcontainer.json'));
|
|
275
|
-
}
|
|
276
|
-
if (hasDockerCompose) {
|
|
277
|
-
fs.copyFileSync(dockerComposePath, path.join(backupPath, 'docker-compose.yml'));
|
|
278
|
-
}
|
|
279
|
-
if (hasDevcontainerSubdir) {
|
|
280
|
-
const destDir = path.join(backupPath, '.devcontainer');
|
|
281
|
-
await copyDirectory(devcontainerSubdir, destDir);
|
|
282
|
-
}
|
|
283
|
-
if (hasManifest) {
|
|
284
|
-
fs.copyFileSync(manifestPath, path.join(backupPath, 'superposition.json'));
|
|
285
|
-
}
|
|
286
|
-
// Also backup other common devcontainer files
|
|
287
|
-
const otherFiles = ['.env', '.env.example', '.gitignore', 'features', 'scripts'];
|
|
288
|
-
for (const file of otherFiles) {
|
|
289
|
-
const srcPath = path.join(outputPath, file);
|
|
290
|
-
if (fs.existsSync(srcPath)) {
|
|
291
|
-
const destPath = path.join(backupPath, file);
|
|
292
|
-
if (fs.statSync(srcPath).isDirectory()) {
|
|
293
|
-
await copyDirectory(srcPath, destPath);
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
fs.copyFileSync(srcPath, destPath);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
return backupPath;
|
|
301
|
-
}
|
|
302
|
-
/**
|
|
303
|
-
* Recursively copy directory
|
|
304
|
-
*/
|
|
305
|
-
async function copyDirectory(src, dest) {
|
|
306
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
307
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
308
|
-
for (const entry of entries) {
|
|
309
|
-
const srcPath = path.join(src, entry.name);
|
|
310
|
-
const destPath = path.join(dest, entry.name);
|
|
311
|
-
if (entry.isDirectory()) {
|
|
312
|
-
await copyDirectory(srcPath, destPath);
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
fs.copyFileSync(srcPath, destPath);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Ensure backup patterns are in .gitignore
|
|
321
|
-
*/
|
|
322
|
-
function ensureBackupPatternsInGitignore(outputPath) {
|
|
323
|
-
const projectRoot = path.dirname(path.resolve(outputPath));
|
|
324
|
-
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
325
|
-
const written = appendGitignoreSection(gitignorePath, 'container-superposition backups', [
|
|
326
|
-
'.devcontainer.backup-*/',
|
|
327
|
-
'*.backup-*',
|
|
328
|
-
'superposition.json.backup-*',
|
|
329
|
-
]);
|
|
330
|
-
if (written) {
|
|
331
|
-
console.log(chalk.dim(' 📝 Updated .gitignore with backup patterns'));
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
329
|
/**
|
|
335
330
|
* Build checkbox choices for overlay selection with optional pre-selection
|
|
336
331
|
*/
|
|
@@ -360,7 +355,7 @@ function buildOverlayChoices(config, stack, categoryList, preselected) {
|
|
|
360
355
|
/**
|
|
361
356
|
* Interactive questionnaire with modern checkbox selections
|
|
362
357
|
*/
|
|
363
|
-
async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetChoices) {
|
|
358
|
+
async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetChoices, defaultAnswers) {
|
|
364
359
|
const config = loadOverlaysConfigWrapper();
|
|
365
360
|
// Pretty banner
|
|
366
361
|
console.log('\n' +
|
|
@@ -448,7 +443,7 @@ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetCho
|
|
|
448
443
|
value: t.id,
|
|
449
444
|
description: t.description,
|
|
450
445
|
})),
|
|
451
|
-
default: manifest?.baseTemplate,
|
|
446
|
+
default: manifest?.baseTemplate || defaultAnswers?.stack,
|
|
452
447
|
}));
|
|
453
448
|
// If using preset, expand it now (pass pre-provided choices to skip those prompts)
|
|
454
449
|
if (usePreset && selectedPresetId) {
|
|
@@ -473,7 +468,15 @@ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetCho
|
|
|
473
468
|
// Check if manifest has a custom image or a known base image
|
|
474
469
|
const knownBaseImageIds = config.base_images.map((img) => img.id);
|
|
475
470
|
const manifestBaseImageIsKnown = manifest?.baseImage && knownBaseImageIds.includes(manifest.baseImage);
|
|
476
|
-
const manifestDefaultBaseImage = manifestBaseImageIsKnown
|
|
471
|
+
const manifestDefaultBaseImage = manifestBaseImageIsKnown
|
|
472
|
+
? manifest.baseImage
|
|
473
|
+
: manifest?.baseImage
|
|
474
|
+
? 'custom'
|
|
475
|
+
: undefined;
|
|
476
|
+
const defaultBaseImage = manifestDefaultBaseImage ||
|
|
477
|
+
(defaultAnswers?.baseImage === 'custom' && defaultAnswers.customImage
|
|
478
|
+
? 'custom'
|
|
479
|
+
: defaultAnswers?.baseImage);
|
|
477
480
|
const baseImage = (await select({
|
|
478
481
|
message: 'Select base image:',
|
|
479
482
|
choices: config.base_images.map((img) => ({
|
|
@@ -481,16 +484,17 @@ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetCho
|
|
|
481
484
|
value: img.id,
|
|
482
485
|
description: img.description,
|
|
483
486
|
})),
|
|
484
|
-
default:
|
|
487
|
+
default: defaultBaseImage,
|
|
485
488
|
}));
|
|
486
489
|
// Question 2a: If custom, ask for image name
|
|
487
490
|
let customImage;
|
|
488
491
|
if (baseImage === 'custom') {
|
|
489
492
|
// If manifest has a custom image, use it as default
|
|
490
493
|
const manifestCustomImage = !manifestBaseImageIsKnown && manifest?.baseImage ? manifest.baseImage : undefined;
|
|
494
|
+
const defaultCustomImage = manifestCustomImage || defaultAnswers?.customImage;
|
|
491
495
|
customImage = await input({
|
|
492
496
|
message: 'Enter custom Docker image (e.g., ubuntu:22.04):',
|
|
493
|
-
default:
|
|
497
|
+
default: defaultCustomImage,
|
|
494
498
|
validate: (value) => {
|
|
495
499
|
if (!value || value.trim() === '') {
|
|
496
500
|
return 'Image name is required';
|
|
@@ -606,7 +610,15 @@ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetCho
|
|
|
606
610
|
else {
|
|
607
611
|
// Custom mode: Normal overlay selection
|
|
608
612
|
console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm\n'));
|
|
609
|
-
const
|
|
613
|
+
const preselectedDefaults = [
|
|
614
|
+
...(defaultAnswers?.language ?? []),
|
|
615
|
+
...(defaultAnswers?.database ?? []),
|
|
616
|
+
...(defaultAnswers?.observability ?? []),
|
|
617
|
+
...(defaultAnswers?.cloudTools ?? []),
|
|
618
|
+
...(defaultAnswers?.devTools ?? []),
|
|
619
|
+
...(defaultAnswers?.playwright ? ['playwright'] : []),
|
|
620
|
+
];
|
|
621
|
+
const choices = buildOverlayChoices(config, stack, categoryList, preselectedDefaults);
|
|
610
622
|
userSelection = await checkbox({
|
|
611
623
|
message: 'Select overlays to include:',
|
|
612
624
|
choices,
|
|
@@ -683,11 +695,11 @@ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetCho
|
|
|
683
695
|
// Question 4: Container name
|
|
684
696
|
const containerName = await input({
|
|
685
697
|
message: 'Container/project name (optional):',
|
|
686
|
-
default: manifest?.containerName || '',
|
|
698
|
+
default: manifest?.containerName || defaultAnswers?.containerName || '',
|
|
687
699
|
});
|
|
688
700
|
// Question 5: Output path
|
|
689
701
|
// If manifest provided, default to its location; otherwise use ./.devcontainer
|
|
690
|
-
const defaultOutput = manifestDir || './.devcontainer';
|
|
702
|
+
const defaultOutput = manifestDir || defaultAnswers?.outputPath || './.devcontainer';
|
|
691
703
|
const outputPath = await input({
|
|
692
704
|
message: 'Output path:',
|
|
693
705
|
default: defaultOutput,
|
|
@@ -695,7 +707,11 @@ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetCho
|
|
|
695
707
|
// Question 6: Port offset (optional, for running multiple instances)
|
|
696
708
|
const portOffsetInput = await input({
|
|
697
709
|
message: 'Port offset (leave empty for default ports, e.g., 100 to avoid conflicts):',
|
|
698
|
-
default: manifest?.portOffset
|
|
710
|
+
default: manifest?.portOffset !== undefined
|
|
711
|
+
? String(manifest.portOffset)
|
|
712
|
+
: defaultAnswers?.portOffset !== undefined
|
|
713
|
+
? String(defaultAnswers.portOffset)
|
|
714
|
+
: '',
|
|
699
715
|
});
|
|
700
716
|
const portOffset = portOffsetInput ? parseInt(portOffsetInput, 10) : undefined;
|
|
701
717
|
// Parse selected overlays into categories
|
|
@@ -790,7 +806,10 @@ async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetCho
|
|
|
790
806
|
observability,
|
|
791
807
|
outputPath,
|
|
792
808
|
portOffset,
|
|
793
|
-
target,
|
|
809
|
+
target: target ?? defaultAnswers?.target,
|
|
810
|
+
minimal: defaultAnswers?.minimal,
|
|
811
|
+
editor: defaultAnswers?.editor,
|
|
812
|
+
customizations: defaultAnswers?.customizations,
|
|
794
813
|
};
|
|
795
814
|
}
|
|
796
815
|
catch (error) {
|
|
@@ -960,8 +979,9 @@ async function parseCliArgs() {
|
|
|
960
979
|
program
|
|
961
980
|
.command('init', { isDefault: true })
|
|
962
981
|
.description('Initialize a new devcontainer configuration')
|
|
982
|
+
.option('--from-project', 'Load configuration from the repository project file')
|
|
963
983
|
.option('--from-manifest <path>', 'Load configuration from existing superposition.json manifest')
|
|
964
|
-
.option('--no-interactive', 'Use
|
|
984
|
+
.option('--no-interactive', 'Use persisted input values directly without questionnaire')
|
|
965
985
|
.option('--backup', 'Force or suppress backup; default is --no-backup inside a git repo, --backup outside')
|
|
966
986
|
.option('--backup-dir <path>', 'Custom backup directory location')
|
|
967
987
|
.option('--stack <type>', 'Base template: plain, compose')
|
|
@@ -979,47 +999,32 @@ async function parseCliArgs() {
|
|
|
979
999
|
.option('--write-manifest-only', 'Generate only superposition.json manifest without creating .devcontainer/ files')
|
|
980
1000
|
.option('--preset <id>', 'Start from a preset (e.g., web-api, microservice)')
|
|
981
1001
|
.option('--preset-param <value>', 'Set a preset parameter value (format: key=value, can be repeated)', (value, previous) => previous.concat([value]), [])
|
|
982
|
-
.action((options) => {
|
|
1002
|
+
.action((options, command) => {
|
|
983
1003
|
// Store options for main() to process
|
|
984
|
-
initOptions =
|
|
1004
|
+
initOptions = {
|
|
1005
|
+
...options,
|
|
1006
|
+
commandName: 'init',
|
|
1007
|
+
_targetSource: command.getOptionValueSource('target'),
|
|
1008
|
+
_editorSource: command.getOptionValueSource('editor'),
|
|
1009
|
+
};
|
|
985
1010
|
});
|
|
986
1011
|
// Regen command
|
|
987
1012
|
program
|
|
988
1013
|
.command('regen')
|
|
989
|
-
.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')
|
|
990
1017
|
.option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
|
|
991
1018
|
.option('--backup', 'Force or suppress backup; default is --no-backup inside a git repo, --backup outside')
|
|
992
1019
|
.option('--backup-dir <path>', 'Custom backup directory location')
|
|
993
1020
|
.option('--minimal', 'Minimal mode - exclude optional/nice-to-have features and extensions')
|
|
994
1021
|
.option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
|
|
995
|
-
.action((options) => {
|
|
996
|
-
const outputPath = options.output || './.devcontainer';
|
|
997
|
-
// Look for manifest in multiple locations for team workflow:
|
|
998
|
-
// 1. Current directory (./superposition.json) - team workflow
|
|
999
|
-
// 2. Output directory (e.g., ./.devcontainer/superposition.json) - legacy
|
|
1000
|
-
const manifestSearchPaths = [
|
|
1001
|
-
'superposition.json',
|
|
1002
|
-
path.join(outputPath, 'superposition.json'),
|
|
1003
|
-
];
|
|
1004
|
-
let manifestPath = null;
|
|
1005
|
-
for (const searchPath of manifestSearchPaths) {
|
|
1006
|
-
if (fs.existsSync(searchPath)) {
|
|
1007
|
-
manifestPath = searchPath;
|
|
1008
|
-
break;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
if (!manifestPath) {
|
|
1012
|
-
console.error(chalk.red(`✗ Error: No manifest found`));
|
|
1013
|
-
console.error(chalk.gray(' Searched for: ./superposition.json, ' +
|
|
1014
|
-
path.join(outputPath, 'superposition.json')));
|
|
1015
|
-
console.error(chalk.gray(' Run "container-superposition init --write-manifest-only" to create a manifest'));
|
|
1016
|
-
process.exit(1);
|
|
1017
|
-
}
|
|
1018
|
-
// Store options for main() to process
|
|
1022
|
+
.action((options, command) => {
|
|
1019
1023
|
initOptions = {
|
|
1020
1024
|
...options,
|
|
1021
|
-
|
|
1025
|
+
commandName: 'regen',
|
|
1022
1026
|
interactive: false,
|
|
1027
|
+
_editorSource: command.getOptionValueSource('editor'),
|
|
1023
1028
|
};
|
|
1024
1029
|
});
|
|
1025
1030
|
// List command
|
|
@@ -1049,13 +1054,15 @@ async function parseCliArgs() {
|
|
|
1049
1054
|
program
|
|
1050
1055
|
.command('plan')
|
|
1051
1056
|
.description('Preview what will be generated before creating devcontainer')
|
|
1052
|
-
.option('--stack <type>', 'Base template: plain, compose'
|
|
1057
|
+
.option('--stack <type>', 'Base template: plain, compose')
|
|
1053
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')
|
|
1054
1060
|
.option('--port-offset <number>', 'Add offset to all exposed ports', (val) => parseInt(val, 10), 0)
|
|
1055
1061
|
.option('-o, --output <path>', 'Compare against existing config at this path (default: ./.devcontainer)')
|
|
1056
1062
|
.option('--diff', 'Compare planned output vs existing configuration')
|
|
1057
1063
|
.option('--diff-format <format>', 'Diff output format: color (default), json', 'color')
|
|
1058
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')
|
|
1059
1066
|
.option('--json', 'Output as JSON for scripting')
|
|
1060
1067
|
.action(async (options) => {
|
|
1061
1068
|
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
@@ -1073,6 +1080,39 @@ async function parseCliArgs() {
|
|
|
1073
1080
|
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
1074
1081
|
await doctorCommand(overlaysConfig, OVERLAYS_DIR, options);
|
|
1075
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
|
+
});
|
|
1076
1116
|
await program.parseAsync(process.argv);
|
|
1077
1117
|
// If init or regen command was run, return the options
|
|
1078
1118
|
if (!initOptions) {
|
|
@@ -1083,6 +1123,34 @@ async function parseCliArgs() {
|
|
|
1083
1123
|
if (Object.keys(initOptions).length === 0) {
|
|
1084
1124
|
return null;
|
|
1085
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
|
+
}
|
|
1086
1154
|
const config = {};
|
|
1087
1155
|
if (initOptions.stack)
|
|
1088
1156
|
config.stack = initOptions.stack;
|
|
@@ -1114,13 +1182,13 @@ async function parseCliArgs() {
|
|
|
1114
1182
|
if (initOptions.portOffset) {
|
|
1115
1183
|
config.portOffset = parseInt(initOptions.portOffset, 10);
|
|
1116
1184
|
}
|
|
1117
|
-
if (initOptions.target) {
|
|
1185
|
+
if (initOptions.target && initOptions._targetSource !== 'default') {
|
|
1118
1186
|
config.target = initOptions.target;
|
|
1119
1187
|
}
|
|
1120
1188
|
if (initOptions.minimal) {
|
|
1121
1189
|
config.minimal = true;
|
|
1122
1190
|
}
|
|
1123
|
-
if (initOptions.editor) {
|
|
1191
|
+
if (initOptions.editor && initOptions._editorSource !== 'default') {
|
|
1124
1192
|
const editorLower = initOptions.editor.toLowerCase();
|
|
1125
1193
|
if (['vscode', 'jetbrains', 'none'].includes(editorLower)) {
|
|
1126
1194
|
config.editor = editorLower;
|
|
@@ -1163,8 +1231,10 @@ async function parseCliArgs() {
|
|
|
1163
1231
|
}
|
|
1164
1232
|
}
|
|
1165
1233
|
return {
|
|
1234
|
+
commandName: initOptions.commandName,
|
|
1166
1235
|
config,
|
|
1167
1236
|
manifestPath: initOptions.fromManifest,
|
|
1237
|
+
fromProject: initOptions.fromProject === true,
|
|
1168
1238
|
backupOverride: initOptions.backup, // undefined = auto-detect; true = --backup; false = --no-backup
|
|
1169
1239
|
backupDir: initOptions.backupDir,
|
|
1170
1240
|
noInteractive: initOptions.interactive === false, // Commander creates options.interactive = false for --no-interactive
|
|
@@ -1174,16 +1244,34 @@ async function parseCliArgs() {
|
|
|
1174
1244
|
async function main() {
|
|
1175
1245
|
try {
|
|
1176
1246
|
const cliArgs = await parseCliArgs();
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
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
|
+
}
|
|
1182
1255
|
}
|
|
1183
1256
|
let manifest;
|
|
1184
1257
|
let manifestDir;
|
|
1185
1258
|
let backupDir;
|
|
1186
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
|
+
}
|
|
1187
1275
|
// Handle manifest loading
|
|
1188
1276
|
if (cliArgs?.manifestPath) {
|
|
1189
1277
|
const manifestPath = findManifestFile(cliArgs.manifestPath);
|
|
@@ -1206,12 +1294,30 @@ async function main() {
|
|
|
1206
1294
|
useManifestOnly = true;
|
|
1207
1295
|
}
|
|
1208
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
|
+
}
|
|
1209
1311
|
// Determine whether to create a backup:
|
|
1210
1312
|
// --backup → always backup
|
|
1211
1313
|
// --no-backup → never backup
|
|
1212
1314
|
// (neither) → backup only when NOT inside a git repository
|
|
1213
1315
|
// (git already tracks history, so backups are redundant)
|
|
1214
|
-
const
|
|
1316
|
+
const resolvedOutputPath = cliArgs?.config?.outputPath ||
|
|
1317
|
+
projectConfigAnswers?.outputPath ||
|
|
1318
|
+
manifestDir ||
|
|
1319
|
+
'./.devcontainer';
|
|
1320
|
+
const backupCheckPath = path.resolve(resolvedOutputPath);
|
|
1215
1321
|
const inGitRepo = isInsideGitRepo(backupCheckPath);
|
|
1216
1322
|
let shouldBackup;
|
|
1217
1323
|
if (cliArgs?.backupOverride === true) {
|
|
@@ -1227,11 +1333,11 @@ async function main() {
|
|
|
1227
1333
|
console.log(chalk.dim('ℹ Skipping backup — git repo detected (use --backup to force one)\n'));
|
|
1228
1334
|
}
|
|
1229
1335
|
}
|
|
1336
|
+
const isReplayMode = cliArgs?.commandName === 'regen' || useManifestOnly || useProjectOnly;
|
|
1230
1337
|
// Create backup if needed
|
|
1231
1338
|
let actualBackupPath;
|
|
1232
|
-
if (shouldBackup &&
|
|
1233
|
-
|
|
1234
|
-
const outputPath = manifestDir || './.devcontainer';
|
|
1339
|
+
if (shouldBackup && isReplayMode) {
|
|
1340
|
+
const outputPath = resolvedOutputPath;
|
|
1235
1341
|
const backupPath = await createBackup(outputPath, backupDir);
|
|
1236
1342
|
if (backupPath) {
|
|
1237
1343
|
actualBackupPath = backupPath;
|
|
@@ -1241,14 +1347,19 @@ async function main() {
|
|
|
1241
1347
|
}
|
|
1242
1348
|
// Build answers based on mode
|
|
1243
1349
|
let answers;
|
|
1244
|
-
const isRegen = !!manifest;
|
|
1245
1350
|
// Check if there are CLI overrides beyond just output path and preset flags
|
|
1246
1351
|
// Preset/presetChoices alone don't constitute "CLI overrides" that bypass interactive mode
|
|
1247
1352
|
const hasCliOverrides = cliArgs &&
|
|
1248
1353
|
Object.keys(cliArgs.config).some((key) => key !== 'outputPath' &&
|
|
1249
1354
|
key !== 'preset' &&
|
|
1250
1355
|
key !== 'presetChoices' &&
|
|
1356
|
+
!(key === 'target' && cliArgs.config.target === 'local') &&
|
|
1357
|
+
!(key === 'editor' && cliArgs.config.editor === 'vscode') &&
|
|
1251
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'));
|
|
1252
1363
|
if (useManifestOnly && manifest && !hasCliOverrides) {
|
|
1253
1364
|
// Mode 1: Manifest-only (--from-manifest --no-interactive, no CLI overrides)
|
|
1254
1365
|
const manifestAnswers = buildAnswersFromManifest(manifest, manifestDir);
|
|
@@ -1268,19 +1379,44 @@ async function main() {
|
|
|
1268
1379
|
: '') +
|
|
1269
1380
|
chalk.gray(` Output: ${answers.outputPath}`), { padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: 1 }));
|
|
1270
1381
|
}
|
|
1271
|
-
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))) {
|
|
1272
1404
|
// Mode 2: CLI-based (with optional manifest defaults)
|
|
1273
1405
|
// This includes regen with --minimal or --editor flags
|
|
1274
1406
|
const cliAnswers = buildAnswersFromCliArgs(cliArgs.config);
|
|
1275
1407
|
const manifestAnswers = manifest
|
|
1276
1408
|
? buildAnswersFromManifest(manifest, manifestDir)
|
|
1277
1409
|
: undefined;
|
|
1278
|
-
answers = mergeAnswers(manifestAnswers, cliAnswers, {
|
|
1279
|
-
outputPath: cliAnswers.outputPath || './.devcontainer',
|
|
1410
|
+
answers = mergeAnswers(projectConfigAnswers, manifestAnswers, cliAnswers, {
|
|
1411
|
+
outputPath: cliAnswers.outputPath || projectConfigAnswers?.outputPath || './.devcontainer',
|
|
1280
1412
|
});
|
|
1281
1413
|
const modeLabel = useManifestOnly && hasCliOverrides
|
|
1282
1414
|
? 'Regenerating from Manifest with Overrides'
|
|
1283
|
-
:
|
|
1415
|
+
: useProjectOnly && projectConfigAnswers
|
|
1416
|
+
? 'Regenerating from Project File with Overrides'
|
|
1417
|
+
: projectConfigAnswers && !manifest
|
|
1418
|
+
? 'Running from Project Config'
|
|
1419
|
+
: 'Running in CLI mode';
|
|
1284
1420
|
console.log('\n' +
|
|
1285
1421
|
boxen(chalk.bold(modeLabel), {
|
|
1286
1422
|
padding: 0.5,
|
|
@@ -1288,7 +1424,7 @@ async function main() {
|
|
|
1288
1424
|
borderStyle: 'round',
|
|
1289
1425
|
}));
|
|
1290
1426
|
// Show what's being overridden
|
|
1291
|
-
if (useManifestOnly && hasCliOverrides) {
|
|
1427
|
+
if ((useManifestOnly || useProjectOnly) && hasCliOverrides) {
|
|
1292
1428
|
const overrides = [];
|
|
1293
1429
|
if (cliAnswers.minimal)
|
|
1294
1430
|
overrides.push('minimal mode');
|
|
@@ -1301,8 +1437,15 @@ async function main() {
|
|
|
1301
1437
|
}
|
|
1302
1438
|
else {
|
|
1303
1439
|
// Mode 3: Interactive (with optional manifest pre-population and CLI preset pre-selection)
|
|
1304
|
-
const interactiveAnswers = await runQuestionnaire(manifest, manifestDir, cliArgs?.config.preset, cliArgs?.config.presetChoices);
|
|
1305
|
-
answers = mergeAnswers(interactiveAnswers);
|
|
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);
|
|
1306
1449
|
}
|
|
1307
1450
|
// Show configuration summary
|
|
1308
1451
|
const summaryLines = [
|
|
@@ -1322,6 +1465,9 @@ async function main() {
|
|
|
1322
1465
|
if (answers.cloudTools && answers.cloudTools.length > 0) {
|
|
1323
1466
|
summaryLines.push(chalk.cyan('Cloud tools: ') + chalk.white(answers.cloudTools.join(', ')));
|
|
1324
1467
|
}
|
|
1468
|
+
if (projectConfig?.file && !manifest) {
|
|
1469
|
+
summaryLines.push(chalk.cyan('Project config: ') + chalk.white(projectConfig.file.fileName));
|
|
1470
|
+
}
|
|
1325
1471
|
summaryLines.push(chalk.cyan('Output: ') + chalk.white(answers.outputPath));
|
|
1326
1472
|
console.log('\n' +
|
|
1327
1473
|
boxen(summaryLines.join('\n'), {
|
|
@@ -1342,11 +1488,13 @@ async function main() {
|
|
|
1342
1488
|
try {
|
|
1343
1489
|
let summary;
|
|
1344
1490
|
if (isManifestOnly) {
|
|
1345
|
-
summary = await generateManifestOnly(answers, undefined, {
|
|
1491
|
+
summary = await generateManifestOnly(answers, undefined, {
|
|
1492
|
+
isRegen: isReplayMode,
|
|
1493
|
+
});
|
|
1346
1494
|
spinner.succeed(chalk.green('Manifest created successfully!'));
|
|
1347
1495
|
}
|
|
1348
1496
|
else {
|
|
1349
|
-
summary = await composeDevContainer(answers, undefined, { isRegen });
|
|
1497
|
+
summary = await composeDevContainer(answers, undefined, { isRegen: isReplayMode });
|
|
1350
1498
|
spinner.succeed(chalk.green('DevContainer created successfully!'));
|
|
1351
1499
|
}
|
|
1352
1500
|
// Update summary with backup path and regen status
|