container-superposition 0.1.1 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +569 -8
- package/dist/scripts/init.js +436 -254
- package/dist/scripts/init.js.map +1 -1
- package/dist/tool/commands/doctor.d.ts +15 -0
- package/dist/tool/commands/doctor.d.ts.map +1 -0
- package/dist/tool/commands/doctor.js +862 -0
- package/dist/tool/commands/doctor.js.map +1 -0
- package/dist/tool/commands/explain.d.ts +13 -0
- package/dist/tool/commands/explain.d.ts.map +1 -0
- package/dist/tool/commands/explain.js +299 -0
- package/dist/tool/commands/explain.js.map +1 -0
- package/dist/tool/commands/list.d.ts +16 -0
- package/dist/tool/commands/list.d.ts.map +1 -0
- package/dist/tool/commands/list.js +121 -0
- package/dist/tool/commands/list.js.map +1 -0
- package/dist/tool/commands/plan.d.ts +67 -0
- package/dist/tool/commands/plan.d.ts.map +1 -0
- package/dist/tool/commands/plan.js +851 -0
- package/dist/tool/commands/plan.js.map +1 -0
- package/dist/tool/questionnaire/composer.d.ts +16 -2
- package/dist/tool/questionnaire/composer.d.ts.map +1 -1
- package/dist/tool/questionnaire/composer.js +411 -200
- package/dist/tool/questionnaire/composer.js.map +1 -1
- package/dist/tool/readme/markdown-parser.d.ts.map +1 -1
- package/dist/tool/readme/markdown-parser.js.map +1 -1
- package/dist/tool/readme/readme-generator.d.ts.map +1 -1
- package/dist/tool/readme/readme-generator.js +11 -6
- package/dist/tool/readme/readme-generator.js.map +1 -1
- package/dist/tool/schema/deployment-targets.d.ts +77 -0
- package/dist/tool/schema/deployment-targets.d.ts.map +1 -0
- package/dist/tool/schema/deployment-targets.js +91 -0
- package/dist/tool/schema/deployment-targets.js.map +1 -0
- package/dist/tool/schema/manifest-migrations.d.ts +51 -0
- package/dist/tool/schema/manifest-migrations.d.ts.map +1 -0
- package/dist/tool/schema/manifest-migrations.js +159 -0
- package/dist/tool/schema/manifest-migrations.js.map +1 -0
- package/dist/tool/schema/overlay-loader.d.ts +1 -1
- package/dist/tool/schema/overlay-loader.d.ts.map +1 -1
- package/dist/tool/schema/overlay-loader.js +42 -14
- package/dist/tool/schema/overlay-loader.js.map +1 -1
- package/dist/tool/schema/types.d.ts +62 -2
- package/dist/tool/schema/types.d.ts.map +1 -1
- package/dist/tool/utils/gitignore.d.ts +15 -0
- package/dist/tool/utils/gitignore.d.ts.map +1 -0
- package/dist/tool/utils/gitignore.js +41 -0
- package/dist/tool/utils/gitignore.js.map +1 -0
- package/dist/tool/utils/merge.d.ts +134 -0
- package/dist/tool/utils/merge.d.ts.map +1 -0
- package/dist/tool/utils/merge.js +277 -0
- package/dist/tool/utils/merge.js.map +1 -0
- package/dist/tool/utils/port-utils.d.ts +29 -0
- package/dist/tool/utils/port-utils.d.ts.map +1 -0
- package/dist/tool/utils/port-utils.js +128 -0
- package/dist/tool/utils/port-utils.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/dist/tool/utils/version.d.ts +9 -0
- package/dist/tool/utils/version.d.ts.map +1 -0
- package/dist/tool/utils/version.js +32 -0
- package/dist/tool/utils/version.js.map +1 -0
- package/docs/architecture.md +25 -21
- package/docs/deployment-targets.md +150 -0
- package/docs/discovery-commands.md +442 -0
- package/docs/merge-strategy.md +700 -0
- package/docs/minimal-and-editor.md +265 -0
- package/docs/overlay-imports.md +209 -0
- package/docs/overlay-manifest-refactoring.md +2 -2
- package/docs/overlay-metadata-archive.md +1 -1
- package/docs/overlays.md +139 -28
- package/docs/presets-architecture.md +3 -3
- package/docs/presets.md +1 -1
- package/docs/publishing.md +36 -35
- package/docs/team-workflow.md +540 -0
- package/overlays/.presets/data-engineering.yml +392 -0
- package/overlays/.presets/event-sourced-service.yml +262 -0
- package/overlays/.presets/frontend.yml +287 -0
- package/overlays/.presets/k8s-operator-dev.yml +462 -0
- package/overlays/{presets → .presets}/microservice.yml +32 -6
- package/overlays/.presets/web-api.yml +129 -0
- package/overlays/.registry/README.md +1 -1
- package/overlays/.registry/deployment-targets.yml +54 -0
- package/overlays/.shared/README.md +43 -0
- package/overlays/.shared/compose/common-healthchecks.yml +38 -0
- package/overlays/.shared/otel/instrumentation.env +20 -0
- package/overlays/.shared/otel/otel-base-config.yaml +30 -0
- package/overlays/.shared/vscode/recommended-extensions.json +14 -0
- package/overlays/README.md +1 -1
- 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/codex/overlay.yml +1 -0
- package/overlays/direnv/README.md +6 -4
- package/overlays/direnv/setup.sh +0 -12
- package/overlays/duckdb/README.md +274 -0
- package/overlays/duckdb/devcontainer.patch.json +10 -0
- package/overlays/duckdb/overlay.yml +17 -0
- package/overlays/duckdb/setup.sh +45 -0
- package/overlays/duckdb/verify.sh +32 -0
- package/overlays/git-helpers/overlay.yml +1 -0
- package/overlays/grafana/README.md +5 -5
- package/overlays/grafana/dashboard-provider.yml +1 -1
- package/overlays/grafana/docker-compose.yml +2 -2
- package/overlays/grafana/overlay.yml +6 -1
- 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/jaeger/overlay.yml +16 -3
- package/overlays/jupyter/.env.example +6 -0
- package/overlays/jupyter/README.md +210 -0
- package/overlays/jupyter/devcontainer.patch.json +14 -0
- package/overlays/jupyter/docker-compose.yml +23 -0
- package/overlays/jupyter/overlay.yml +18 -0
- package/overlays/jupyter/verify.sh +35 -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/kind/README.md +221 -0
- package/overlays/kind/devcontainer.patch.json +10 -0
- package/overlays/kind/overlay.yml +18 -0
- package/overlays/kind/setup.sh +43 -0
- package/overlays/kind/verify.sh +40 -0
- package/overlays/localstack/.env.example +6 -0
- package/overlays/localstack/README.md +188 -0
- package/overlays/localstack/devcontainer.patch.json +21 -0
- package/overlays/localstack/docker-compose.yml +25 -0
- package/overlays/localstack/overlay.yml +18 -0
- package/overlays/localstack/verify.sh +47 -0
- package/overlays/loki/overlay.yml +6 -1
- 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/modern-cli-tools/overlay.yml +1 -0
- package/overlays/mongodb/overlay.yml +12 -2
- package/overlays/mysql/overlay.yml +12 -2
- package/overlays/nats/overlay.yml +12 -2
- package/overlays/ngrok/overlay.yml +2 -1
- package/overlays/openapi-tools/README.md +243 -0
- package/overlays/openapi-tools/devcontainer.patch.json +10 -0
- package/overlays/openapi-tools/overlay.yml +16 -0
- package/overlays/openapi-tools/setup.sh +45 -0
- package/overlays/openapi-tools/verify.sh +51 -0
- package/overlays/otel-collector/overlay.yml.example +26 -0
- package/overlays/postgres/overlay.yml +6 -1
- package/overlays/prometheus/overlay.yml +6 -1
- package/overlays/python/README.md +51 -35
- package/overlays/python/devcontainer.patch.json +7 -4
- package/overlays/python/setup.sh +50 -23
- package/overlays/python/verify.sh +29 -1
- package/overlays/rabbitmq/overlay.yml +12 -2
- package/overlays/redis/overlay.yml +6 -1
- package/overlays/tilt/README.md +259 -0
- package/overlays/tilt/devcontainer.patch.json +17 -0
- package/overlays/tilt/overlay.yml +19 -0
- package/overlays/tilt/setup.sh +25 -0
- package/overlays/tilt/verify.sh +24 -0
- package/package.json +8 -6
- package/tool/README.md +12 -16
- package/tool/schema/overlay-manifest.schema.json +64 -4
- package/tool/schema/superposition-manifest.schema.json +104 -0
- package/overlays/presets/web-api.yml +0 -109
- /package/overlays/{presets → .presets}/docs-site.yml +0 -0
- /package/overlays/{presets → .presets}/fullstack.yml +0 -0
package/dist/scripts/init.js
CHANGED
|
@@ -2,14 +2,24 @@
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
5
6
|
import { Command } from 'commander';
|
|
6
7
|
import chalk from 'chalk';
|
|
7
8
|
import boxen from 'boxen';
|
|
8
9
|
import ora from 'ora';
|
|
9
10
|
import { select, checkbox, input } from '@inquirer/prompts';
|
|
10
11
|
import yaml from 'js-yaml';
|
|
11
|
-
import { composeDevContainer } from '../tool/questionnaire/composer.js';
|
|
12
|
+
import { composeDevContainer, generateManifestOnly } from '../tool/questionnaire/composer.js';
|
|
12
13
|
import { loadOverlaysConfig } from '../tool/schema/overlay-loader.js';
|
|
14
|
+
import { listCommand } from '../tool/commands/list.js';
|
|
15
|
+
import { explainCommand } from '../tool/commands/explain.js';
|
|
16
|
+
import { planCommand } from '../tool/commands/plan.js';
|
|
17
|
+
import { doctorCommand } from '../tool/commands/doctor.js';
|
|
18
|
+
import { getIncompatibleOverlays, DEPLOYMENT_TARGETS } from '../tool/schema/deployment-targets.js';
|
|
19
|
+
import { migrateManifest, needsMigration, detectManifestVersion, isVersionSupported, CURRENT_MANIFEST_VERSION, SUPPORTED_MANIFEST_VERSIONS, } from '../tool/schema/manifest-migrations.js';
|
|
20
|
+
import { getToolVersion } from '../tool/utils/version.js';
|
|
21
|
+
import { printSummary } from '../tool/utils/summary.js';
|
|
22
|
+
import { appendGitignoreSection } from '../tool/utils/gitignore.js';
|
|
13
23
|
// Get __dirname equivalent in ESM
|
|
14
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
25
|
const __dirname = path.dirname(__filename);
|
|
@@ -32,8 +42,8 @@ const OVERLAYS_CONFIG_CANDIDATES = [
|
|
|
32
42
|
const OVERLAYS_CONFIG_PATH = OVERLAYS_CONFIG_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
|
|
33
43
|
OVERLAYS_CONFIG_CANDIDATES[0];
|
|
34
44
|
const PRESETS_DIR_CANDIDATES = [
|
|
35
|
-
path.join(__dirname, '..', 'overlays', 'presets'),
|
|
36
|
-
path.join(__dirname, '..', '..', 'overlays', 'presets'),
|
|
45
|
+
path.join(__dirname, '..', 'overlays', '.presets'),
|
|
46
|
+
path.join(__dirname, '..', '..', 'overlays', '.presets'),
|
|
37
47
|
];
|
|
38
48
|
const PRESETS_DIR = PRESETS_DIR_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
|
|
39
49
|
PRESETS_DIR_CANDIDATES[0];
|
|
@@ -58,7 +68,7 @@ function loadPresetDefinition(presetId) {
|
|
|
58
68
|
/**
|
|
59
69
|
* Expand a preset into a list of overlay IDs with user choices resolved
|
|
60
70
|
*/
|
|
61
|
-
async function expandPreset(presetId, stack) {
|
|
71
|
+
async function expandPreset(presetId, stack, preProvidedChoices = {}) {
|
|
62
72
|
const preset = loadPresetDefinition(presetId);
|
|
63
73
|
if (!preset) {
|
|
64
74
|
return { overlays: [], choices: {} };
|
|
@@ -66,23 +76,72 @@ async function expandPreset(presetId, stack) {
|
|
|
66
76
|
console.log(chalk.cyan(`\n📦 Expanding preset: ${preset.name}\n`));
|
|
67
77
|
const overlays = [...preset.selects.required];
|
|
68
78
|
const choices = {};
|
|
69
|
-
// Handle user choices
|
|
79
|
+
// Handle user choices (single overlay per option)
|
|
70
80
|
if (preset.selects.userChoice) {
|
|
71
81
|
for (const [key, choice] of Object.entries(preset.selects.userChoice)) {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
const preProvidedValue = preProvidedChoices[key];
|
|
83
|
+
if (preProvidedValue !== undefined) {
|
|
84
|
+
// Validate the pre-provided value
|
|
85
|
+
if (!choice.options.includes(preProvidedValue)) {
|
|
86
|
+
const valid = choice.options.join(', ');
|
|
87
|
+
throw new Error(`Invalid value '${preProvidedValue}' for preset choice '${key}'. Valid options: ${valid}`);
|
|
88
|
+
}
|
|
89
|
+
console.log(chalk.dim(`✓ ${key}: ${preProvidedValue} (from CLI)`));
|
|
90
|
+
overlays.push(preProvidedValue);
|
|
91
|
+
choices[key] = preProvidedValue;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const selectedOption = (await select({
|
|
95
|
+
message: choice.prompt,
|
|
96
|
+
choices: choice.options.map((opt) => ({
|
|
97
|
+
name: opt,
|
|
98
|
+
value: opt,
|
|
99
|
+
})),
|
|
100
|
+
default: choice.defaultOption,
|
|
101
|
+
}));
|
|
102
|
+
overlays.push(selectedOption);
|
|
103
|
+
choices[key] = selectedOption;
|
|
104
|
+
}
|
|
82
105
|
}
|
|
83
106
|
}
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
// Handle parameterized slots (multiple overlays per option)
|
|
108
|
+
if (preset.parameters) {
|
|
109
|
+
for (const [key, param] of Object.entries(preset.parameters)) {
|
|
110
|
+
const preProvidedValue = preProvidedChoices[key];
|
|
111
|
+
let selectedId;
|
|
112
|
+
if (preProvidedValue !== undefined) {
|
|
113
|
+
// Validate the pre-provided value
|
|
114
|
+
const validOption = param.options.find((o) => o.id === preProvidedValue);
|
|
115
|
+
if (!validOption) {
|
|
116
|
+
const valid = param.options.map((o) => o.id).join(', ');
|
|
117
|
+
throw new Error(`Invalid value '${preProvidedValue}' for preset parameter '${key}'. Valid options: ${valid}`);
|
|
118
|
+
}
|
|
119
|
+
console.log(chalk.dim(`✓ ${key}: ${preProvidedValue} (from CLI)`));
|
|
120
|
+
selectedId = preProvidedValue;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
const description = param.description || `Select ${key}`;
|
|
124
|
+
selectedId = (await select({
|
|
125
|
+
message: description,
|
|
126
|
+
choices: param.options.map((opt) => ({
|
|
127
|
+
name: opt.description ? `${opt.id} - ${opt.description}` : opt.id,
|
|
128
|
+
value: opt.id,
|
|
129
|
+
})),
|
|
130
|
+
default: param.default,
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
// Add overlays for the selected option
|
|
134
|
+
const selectedOption = param.options.find((o) => o.id === selectedId);
|
|
135
|
+
if (selectedOption) {
|
|
136
|
+
overlays.push(...selectedOption.overlays);
|
|
137
|
+
}
|
|
138
|
+
choices[key] = selectedId;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Deduplicate overlays
|
|
142
|
+
const uniqueOverlays = [...new Set(overlays)];
|
|
143
|
+
console.log(chalk.dim(`✓ Preset will include: ${uniqueOverlays.join(', ')}\n`));
|
|
144
|
+
return { overlays: uniqueOverlays, choices, glueConfig: preset.glueConfig };
|
|
86
145
|
}
|
|
87
146
|
/**
|
|
88
147
|
* Search for manifest file in multiple locations
|
|
@@ -111,10 +170,29 @@ function findManifestFile(manifestPath) {
|
|
|
111
170
|
function loadManifest(manifestPath) {
|
|
112
171
|
try {
|
|
113
172
|
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
114
|
-
const
|
|
173
|
+
const rawManifest = JSON.parse(content);
|
|
174
|
+
// Detect manifest version
|
|
175
|
+
const detectedVersion = detectManifestVersion(rawManifest);
|
|
176
|
+
// Check if version is supported
|
|
177
|
+
if (!isVersionSupported(detectedVersion)) {
|
|
178
|
+
console.error(chalk.red(`✗ Manifest version ${detectedVersion} is not supported.\n` +
|
|
179
|
+
` This tool supports versions: ${SUPPORTED_MANIFEST_VERSIONS.join(', ')}\n` +
|
|
180
|
+
` Please upgrade your tool or regenerate the manifest.`));
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
// Migrate if needed
|
|
184
|
+
let manifest;
|
|
185
|
+
if (needsMigration(rawManifest)) {
|
|
186
|
+
const oldVersion = rawManifest.manifestVersion || rawManifest.version || 'legacy';
|
|
187
|
+
console.log(chalk.cyan(`ℹ️ Migrating manifest from version ${oldVersion} to ${CURRENT_MANIFEST_VERSION}...`));
|
|
188
|
+
manifest = migrateManifest(rawManifest);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
manifest = rawManifest;
|
|
192
|
+
}
|
|
115
193
|
// Basic validation
|
|
116
|
-
if (!manifest.
|
|
117
|
-
console.error(chalk.red('✗ Invalid manifest format: missing required
|
|
194
|
+
if (!manifest.baseTemplate) {
|
|
195
|
+
console.error(chalk.red('✗ Invalid manifest format: missing required field "baseTemplate"'));
|
|
118
196
|
return null;
|
|
119
197
|
}
|
|
120
198
|
if (!Array.isArray(manifest.overlays)) {
|
|
@@ -125,10 +203,6 @@ function loadManifest(manifestPath) {
|
|
|
125
203
|
console.error(chalk.red('✗ Invalid manifest format: all "overlays" entries must be strings'));
|
|
126
204
|
return null;
|
|
127
205
|
}
|
|
128
|
-
// Version check (warn if different, but continue)
|
|
129
|
-
if (manifest.version !== '0.1.0') {
|
|
130
|
-
console.warn(chalk.yellow(`⚠️ Manifest version ${manifest.version} may not be fully compatible with this tool`));
|
|
131
|
-
}
|
|
132
206
|
return manifest;
|
|
133
207
|
}
|
|
134
208
|
catch (error) {
|
|
@@ -136,6 +210,32 @@ function loadManifest(manifestPath) {
|
|
|
136
210
|
return null;
|
|
137
211
|
}
|
|
138
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Detect whether a directory (or any of its parents) is inside a git repository.
|
|
215
|
+
* First tries `git rev-parse --git-dir`; if git is unavailable, falls back to
|
|
216
|
+
* walking up the directory tree looking for a `.git` entry.
|
|
217
|
+
*/
|
|
218
|
+
function isInsideGitRepo(dirPath) {
|
|
219
|
+
try {
|
|
220
|
+
execSync('git rev-parse --git-dir', { cwd: dirPath, stdio: 'ignore' });
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// git command failed (not a repo) or git is not installed — walk up looking for .git
|
|
225
|
+
let current = path.resolve(dirPath);
|
|
226
|
+
while (true) {
|
|
227
|
+
if (fs.existsSync(path.join(current, '.git'))) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
const parent = path.dirname(current);
|
|
231
|
+
if (parent === current) {
|
|
232
|
+
break; // reached filesystem root
|
|
233
|
+
}
|
|
234
|
+
current = parent;
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
139
239
|
/**
|
|
140
240
|
* Create timestamped backup of existing devcontainer and manifest
|
|
141
241
|
*/
|
|
@@ -219,31 +319,16 @@ async function copyDirectory(src, dest) {
|
|
|
219
319
|
/**
|
|
220
320
|
* Ensure backup patterns are in .gitignore
|
|
221
321
|
*/
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const resolvedOutputPath = path.resolve(outputPath);
|
|
225
|
-
const projectRoot = path.dirname(resolvedOutputPath);
|
|
322
|
+
function ensureBackupPatternsInGitignore(outputPath) {
|
|
323
|
+
const projectRoot = path.dirname(path.resolve(outputPath));
|
|
226
324
|
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
227
|
-
const
|
|
228
|
-
'',
|
|
229
|
-
'# Container Superposition backups',
|
|
325
|
+
const written = appendGitignoreSection(gitignorePath, 'container-superposition backups', [
|
|
230
326
|
'.devcontainer.backup-*/',
|
|
231
327
|
'*.backup-*',
|
|
232
328
|
'superposition.json.backup-*',
|
|
233
|
-
]
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
await fs.promises.writeFile(gitignorePath, backupPatterns + '\n');
|
|
237
|
-
console.log(chalk.dim(' 📝 Created .gitignore with backup patterns'));
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
// Check if patterns already exist
|
|
241
|
-
const content = await fs.promises.readFile(gitignorePath, 'utf-8');
|
|
242
|
-
if (!content.includes('Container Superposition backups')) {
|
|
243
|
-
// Append patterns
|
|
244
|
-
await fs.promises.appendFile(gitignorePath, '\n' + backupPatterns + '\n');
|
|
245
|
-
console.log(chalk.dim(' 📝 Updated .gitignore with backup patterns'));
|
|
246
|
-
}
|
|
329
|
+
]);
|
|
330
|
+
if (written) {
|
|
331
|
+
console.log(chalk.dim(' 📝 Updated .gitignore with backup patterns'));
|
|
247
332
|
}
|
|
248
333
|
}
|
|
249
334
|
/**
|
|
@@ -275,7 +360,7 @@ function buildOverlayChoices(config, stack, categoryList, preselected) {
|
|
|
275
360
|
/**
|
|
276
361
|
* Interactive questionnaire with modern checkbox selections
|
|
277
362
|
*/
|
|
278
|
-
async function runQuestionnaire(manifest, manifestDir) {
|
|
363
|
+
async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetChoices) {
|
|
279
364
|
const config = loadOverlaysConfigWrapper();
|
|
280
365
|
// Pretty banner
|
|
281
366
|
console.log('\n' +
|
|
@@ -309,32 +394,50 @@ async function runQuestionnaire(manifest, manifestDir) {
|
|
|
309
394
|
try {
|
|
310
395
|
// Question 0: Optional preset selection
|
|
311
396
|
let usePreset = false;
|
|
312
|
-
|
|
313
|
-
let
|
|
397
|
+
// CLI preset takes precedence over manifest preset
|
|
398
|
+
let selectedPresetId = cliPresetId || manifest?.preset;
|
|
399
|
+
// CLI preset choices merged with manifest choices (CLI takes precedence)
|
|
400
|
+
let presetChoices = {
|
|
401
|
+
...(manifest?.presetChoices || {}),
|
|
402
|
+
...(cliPresetChoices || {}),
|
|
403
|
+
};
|
|
314
404
|
let presetGlueConfig;
|
|
315
405
|
const presetOverlaysFiltered = config.overlays.filter((o) => o.category === 'preset');
|
|
316
406
|
let presetOverlays = [];
|
|
317
407
|
if (presetOverlaysFiltered.length > 0) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
message: 'Start from a preset or build custom?',
|
|
321
|
-
choices: [
|
|
322
|
-
{
|
|
323
|
-
name: 'Custom (select overlays manually)',
|
|
324
|
-
value: 'custom',
|
|
325
|
-
description: 'Choose individual overlays yourself',
|
|
326
|
-
},
|
|
327
|
-
...presetOverlaysFiltered.map((p) => ({
|
|
328
|
-
name: p.name,
|
|
329
|
-
value: p.id,
|
|
330
|
-
description: p.description,
|
|
331
|
-
})),
|
|
332
|
-
],
|
|
333
|
-
default: defaultPreset,
|
|
334
|
-
}));
|
|
335
|
-
if (presetChoice !== 'custom') {
|
|
408
|
+
// If a preset was pre-selected via CLI or manifest, skip the prompt
|
|
409
|
+
if (selectedPresetId) {
|
|
336
410
|
usePreset = true;
|
|
337
|
-
|
|
411
|
+
console.log(chalk.cyan(`\n📦 Using preset: ${selectedPresetId}\n`));
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
const defaultPreset = 'custom';
|
|
415
|
+
const presetChoice = (await select({
|
|
416
|
+
message: 'Start from a preset or build custom?',
|
|
417
|
+
choices: [
|
|
418
|
+
{
|
|
419
|
+
name: 'Custom (select overlays manually)',
|
|
420
|
+
value: 'custom',
|
|
421
|
+
description: 'Choose individual overlays yourself',
|
|
422
|
+
},
|
|
423
|
+
...presetOverlaysFiltered.map((p) => ({
|
|
424
|
+
name: p.name,
|
|
425
|
+
value: p.id,
|
|
426
|
+
description: p.description,
|
|
427
|
+
})),
|
|
428
|
+
],
|
|
429
|
+
default: defaultPreset,
|
|
430
|
+
}));
|
|
431
|
+
if (presetChoice !== 'custom') {
|
|
432
|
+
usePreset = true;
|
|
433
|
+
selectedPresetId = presetChoice;
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
// User chose custom - discard any pre-provided preset choices so the
|
|
437
|
+
// manifest cannot end up with presetChoices but no preset.
|
|
438
|
+
presetChoices = {};
|
|
439
|
+
selectedPresetId = undefined;
|
|
440
|
+
}
|
|
338
441
|
}
|
|
339
442
|
}
|
|
340
443
|
// Question 1: Base template
|
|
@@ -347,9 +450,9 @@ async function runQuestionnaire(manifest, manifestDir) {
|
|
|
347
450
|
})),
|
|
348
451
|
default: manifest?.baseTemplate,
|
|
349
452
|
}));
|
|
350
|
-
// If using preset, expand it now
|
|
453
|
+
// If using preset, expand it now (pass pre-provided choices to skip those prompts)
|
|
351
454
|
if (usePreset && selectedPresetId) {
|
|
352
|
-
const expansion = await expandPreset(selectedPresetId, stack);
|
|
455
|
+
const expansion = await expandPreset(selectedPresetId, stack, presetChoices);
|
|
353
456
|
if (!expansion.overlays || expansion.overlays.length === 0) {
|
|
354
457
|
// Preset failed to expand (e.g., missing or invalid preset definition).
|
|
355
458
|
// Treat this as "no preset" so the manifest does not incorrectly record one.
|
|
@@ -603,6 +706,73 @@ async function runQuestionnaire(manifest, manifestDir) {
|
|
|
603
706
|
const devTools = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'dev');
|
|
604
707
|
const database = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'database');
|
|
605
708
|
const playwright = selectedOverlays.includes('playwright');
|
|
709
|
+
// Check for deployment target compatibility
|
|
710
|
+
let target;
|
|
711
|
+
// Check if any incompatible overlays selected
|
|
712
|
+
const incompatibleOverlays = getIncompatibleOverlays(selectedOverlays, undefined);
|
|
713
|
+
if (incompatibleOverlays.length > 0) {
|
|
714
|
+
console.log(chalk.yellow('\n⚠️ Deployment Target Compatibility Check:\n'));
|
|
715
|
+
console.log(chalk.gray('Some selected overlays may not work in all environments.'));
|
|
716
|
+
console.log();
|
|
717
|
+
// Show incompatibilities
|
|
718
|
+
for (const { overlay, alternatives } of incompatibleOverlays) {
|
|
719
|
+
const overlayMeta = allOverlaysMap.get(overlay);
|
|
720
|
+
console.log(chalk.yellow(` • ${overlayMeta?.name || overlay}`));
|
|
721
|
+
console.log(chalk.gray(` Not compatible with: ${DEPLOYMENT_TARGETS.codespaces.name}, ${DEPLOYMENT_TARGETS.gitpod.name}`));
|
|
722
|
+
if (alternatives.length > 0) {
|
|
723
|
+
const altNames = alternatives
|
|
724
|
+
.map((id) => allOverlaysMap.get(id)?.name || id)
|
|
725
|
+
.join(', ');
|
|
726
|
+
console.log(chalk.cyan(` Alternatives: ${altNames}`));
|
|
727
|
+
}
|
|
728
|
+
console.log();
|
|
729
|
+
}
|
|
730
|
+
const targetChoice = (await select({
|
|
731
|
+
message: 'Which environment are you targeting?',
|
|
732
|
+
choices: [
|
|
733
|
+
{
|
|
734
|
+
name: '🖥️ Local Development (Docker Desktop)',
|
|
735
|
+
value: 'local',
|
|
736
|
+
description: 'Running on your local machine - supports all overlays',
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
name: '☁️ GitHub Codespaces',
|
|
740
|
+
value: 'codespaces',
|
|
741
|
+
description: 'Cloud development - may require docker-in-docker',
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
name: '🌐 Gitpod',
|
|
745
|
+
value: 'gitpod',
|
|
746
|
+
description: 'Cloud development - may require docker-in-docker',
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
name: '📦 DevPod',
|
|
750
|
+
value: 'devpod',
|
|
751
|
+
description: 'Client-only dev environments',
|
|
752
|
+
},
|
|
753
|
+
],
|
|
754
|
+
default: 'local',
|
|
755
|
+
}));
|
|
756
|
+
target = targetChoice;
|
|
757
|
+
// Show specific incompatibilities for selected target
|
|
758
|
+
if (target !== 'local') {
|
|
759
|
+
const targetIncompatible = getIncompatibleOverlays(selectedOverlays, target);
|
|
760
|
+
if (targetIncompatible.length > 0) {
|
|
761
|
+
console.log(chalk.yellow(`\n⚠️ Warning: Some overlays won't work in ${DEPLOYMENT_TARGETS[target].name}:\n`));
|
|
762
|
+
for (const { overlay, alternatives } of targetIncompatible) {
|
|
763
|
+
const overlayMeta = allOverlaysMap.get(overlay);
|
|
764
|
+
console.log(chalk.red(` ✗ ${overlayMeta?.name || overlay}`));
|
|
765
|
+
if (alternatives.length > 0) {
|
|
766
|
+
const altNames = alternatives
|
|
767
|
+
.map((id) => allOverlaysMap.get(id)?.name || id)
|
|
768
|
+
.join(', ');
|
|
769
|
+
console.log(chalk.cyan(` → Recommended: ${altNames}`));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
console.log();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
606
776
|
return {
|
|
607
777
|
stack,
|
|
608
778
|
baseImage,
|
|
@@ -620,6 +790,7 @@ async function runQuestionnaire(manifest, manifestDir) {
|
|
|
620
790
|
observability,
|
|
621
791
|
outputPath,
|
|
622
792
|
portOffset,
|
|
793
|
+
target,
|
|
623
794
|
};
|
|
624
795
|
}
|
|
625
796
|
catch (error) {
|
|
@@ -724,6 +895,12 @@ function buildAnswersFromCliArgs(config) {
|
|
|
724
895
|
answers.preset = config.preset;
|
|
725
896
|
if (config.presetChoices)
|
|
726
897
|
answers.presetChoices = config.presetChoices;
|
|
898
|
+
if (config.target)
|
|
899
|
+
answers.target = config.target;
|
|
900
|
+
if (config.minimal !== undefined)
|
|
901
|
+
answers.minimal = config.minimal;
|
|
902
|
+
if (config.editor)
|
|
903
|
+
answers.editor = config.editor;
|
|
727
904
|
return answers;
|
|
728
905
|
}
|
|
729
906
|
/**
|
|
@@ -768,157 +945,6 @@ function mergeAnswers(...partials) {
|
|
|
768
945
|
}
|
|
769
946
|
return merged;
|
|
770
947
|
}
|
|
771
|
-
/**
|
|
772
|
-
* List available overlays command
|
|
773
|
-
*/
|
|
774
|
-
async function listOverlays(options) {
|
|
775
|
-
try {
|
|
776
|
-
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
777
|
-
const category = options.category?.toLowerCase();
|
|
778
|
-
console.log('\n' +
|
|
779
|
-
boxen(chalk.bold('Available Overlays'), {
|
|
780
|
-
padding: 0.5,
|
|
781
|
-
borderColor: 'cyan',
|
|
782
|
-
borderStyle: 'round',
|
|
783
|
-
}));
|
|
784
|
-
const categories = [
|
|
785
|
-
{ name: 'language', title: '📚 Language & Framework' },
|
|
786
|
-
{ name: 'database', title: '🗄️ Database & Messaging' },
|
|
787
|
-
{ name: 'observability', title: '📊 Observability' },
|
|
788
|
-
{ name: 'cloud', title: '☁️ Cloud Tools' },
|
|
789
|
-
{ name: 'dev', title: '🔧 Dev Tools' },
|
|
790
|
-
{ name: 'preset', title: '🎯 Presets' },
|
|
791
|
-
];
|
|
792
|
-
for (const cat of categories) {
|
|
793
|
-
if (category && cat.name !== category)
|
|
794
|
-
continue;
|
|
795
|
-
const overlays = overlaysConfig.overlays.filter((o) => o.category === cat.name);
|
|
796
|
-
if (overlays.length === 0)
|
|
797
|
-
continue;
|
|
798
|
-
console.log(`\n${chalk.bold(cat.title)}`);
|
|
799
|
-
for (const overlay of overlays) {
|
|
800
|
-
console.log(` ${chalk.cyan(overlay.id.padEnd(20))} ${chalk.gray(overlay.description)}`);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
console.log(chalk.dim(`\n💡 Use "container-superposition init --language nodejs,python --database postgres" to compose overlays\n`));
|
|
804
|
-
process.exit(0);
|
|
805
|
-
}
|
|
806
|
-
catch (error) {
|
|
807
|
-
console.error(chalk.red('✗ Error listing overlays:'), error);
|
|
808
|
-
process.exit(1);
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
/**
|
|
812
|
-
* Doctor command - check environment and validate configuration
|
|
813
|
-
*/
|
|
814
|
-
async function runDoctor(options) {
|
|
815
|
-
try {
|
|
816
|
-
const outputPath = options.output || './.devcontainer';
|
|
817
|
-
console.log('\n' +
|
|
818
|
-
boxen(chalk.bold('Environment Check'), {
|
|
819
|
-
padding: 0.5,
|
|
820
|
-
borderColor: 'cyan',
|
|
821
|
-
borderStyle: 'round',
|
|
822
|
-
}));
|
|
823
|
-
const checks = [];
|
|
824
|
-
// Helper function for semantic version comparison
|
|
825
|
-
const isVersionAtLeast = (current, required) => {
|
|
826
|
-
const parse = (v) => {
|
|
827
|
-
const parts = v.split('.');
|
|
828
|
-
const major = parseInt(parts[0] ?? '0', 10) || 0;
|
|
829
|
-
const minor = parseInt(parts[1] ?? '0', 10) || 0;
|
|
830
|
-
const patch = parseInt(parts[2] ?? '0', 10) || 0;
|
|
831
|
-
return [major, minor, patch];
|
|
832
|
-
};
|
|
833
|
-
const [cMajor, cMinor, cPatch] = parse(current);
|
|
834
|
-
const [rMajor, rMinor, rPatch] = parse(required);
|
|
835
|
-
if (cMajor !== rMajor) {
|
|
836
|
-
return cMajor > rMajor;
|
|
837
|
-
}
|
|
838
|
-
if (cMinor !== rMinor) {
|
|
839
|
-
return cMinor > rMinor;
|
|
840
|
-
}
|
|
841
|
-
return cPatch >= rPatch;
|
|
842
|
-
};
|
|
843
|
-
// Check Node.js version
|
|
844
|
-
const nodeVersion = process.version;
|
|
845
|
-
const requiredVersion = '20.0.0';
|
|
846
|
-
const versionMatch = nodeVersion.match(/^v(\d+\.\d+\.\d+)/);
|
|
847
|
-
const currentVersion = versionMatch ? versionMatch[1] : '0.0.0';
|
|
848
|
-
const nodeOk = isVersionAtLeast(currentVersion, requiredVersion);
|
|
849
|
-
checks.push({
|
|
850
|
-
name: 'Node.js version',
|
|
851
|
-
status: nodeOk,
|
|
852
|
-
message: nodeOk
|
|
853
|
-
? `${nodeVersion} ✓`
|
|
854
|
-
: `${nodeVersion} (requires >= ${requiredVersion})`,
|
|
855
|
-
});
|
|
856
|
-
// Check if Docker is available
|
|
857
|
-
let dockerOk = false;
|
|
858
|
-
try {
|
|
859
|
-
const { execSync } = await import('child_process');
|
|
860
|
-
execSync('docker --version', { stdio: 'ignore' });
|
|
861
|
-
dockerOk = true;
|
|
862
|
-
}
|
|
863
|
-
catch {
|
|
864
|
-
dockerOk = false;
|
|
865
|
-
}
|
|
866
|
-
checks.push({
|
|
867
|
-
name: 'Docker',
|
|
868
|
-
status: dockerOk,
|
|
869
|
-
message: dockerOk ? 'Available ✓' : 'Not found (required for devcontainers)',
|
|
870
|
-
});
|
|
871
|
-
// Check if devcontainer exists
|
|
872
|
-
const devcontainerExists = fs.existsSync(outputPath);
|
|
873
|
-
checks.push({
|
|
874
|
-
name: 'Devcontainer path',
|
|
875
|
-
status: devcontainerExists,
|
|
876
|
-
message: devcontainerExists
|
|
877
|
-
? `${outputPath} exists ✓`
|
|
878
|
-
: `${outputPath} not found (run init first)`,
|
|
879
|
-
});
|
|
880
|
-
// Check if manifest exists
|
|
881
|
-
if (devcontainerExists) {
|
|
882
|
-
const manifestPath = path.join(outputPath, 'superposition.json');
|
|
883
|
-
const manifestExists = fs.existsSync(manifestPath);
|
|
884
|
-
checks.push({
|
|
885
|
-
name: 'Manifest',
|
|
886
|
-
status: manifestExists,
|
|
887
|
-
message: manifestExists
|
|
888
|
-
? 'superposition.json found ✓'
|
|
889
|
-
: 'superposition.json missing (manual edit or old version)',
|
|
890
|
-
});
|
|
891
|
-
// Check devcontainer.json
|
|
892
|
-
const devcontainerJsonPath = path.join(outputPath, 'devcontainer.json');
|
|
893
|
-
const devcontainerJsonExists = fs.existsSync(devcontainerJsonPath);
|
|
894
|
-
checks.push({
|
|
895
|
-
name: 'DevContainer config',
|
|
896
|
-
status: devcontainerJsonExists,
|
|
897
|
-
message: devcontainerJsonExists
|
|
898
|
-
? 'devcontainer.json found ✓'
|
|
899
|
-
: 'devcontainer.json missing',
|
|
900
|
-
});
|
|
901
|
-
}
|
|
902
|
-
console.log('');
|
|
903
|
-
for (const check of checks) {
|
|
904
|
-
const icon = check.status ? chalk.green('✓') : chalk.red('✗');
|
|
905
|
-
console.log(` ${icon} ${chalk.white(check.name)}: ${chalk.gray(check.message)}`);
|
|
906
|
-
}
|
|
907
|
-
const allPassed = checks.every((c) => c.status);
|
|
908
|
-
if (allPassed) {
|
|
909
|
-
console.log(chalk.green('\n✓ All checks passed!\n'));
|
|
910
|
-
process.exit(0);
|
|
911
|
-
}
|
|
912
|
-
else {
|
|
913
|
-
console.log(chalk.yellow('\n⚠ Some checks failed. See above for details.\n'));
|
|
914
|
-
process.exit(1);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
catch (error) {
|
|
918
|
-
console.error(chalk.red('✗ Error running doctor:'), error);
|
|
919
|
-
process.exit(1);
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
948
|
/**
|
|
923
949
|
* Parse CLI arguments
|
|
924
950
|
*/
|
|
@@ -929,14 +955,14 @@ async function parseCliArgs() {
|
|
|
929
955
|
program
|
|
930
956
|
.name('container-superposition')
|
|
931
957
|
.description('Composable devcontainer scaffolds')
|
|
932
|
-
.version(
|
|
958
|
+
.version(getToolVersion());
|
|
933
959
|
// Init command (default)
|
|
934
960
|
program
|
|
935
961
|
.command('init', { isDefault: true })
|
|
936
962
|
.description('Initialize a new devcontainer configuration')
|
|
937
963
|
.option('--from-manifest <path>', 'Load configuration from existing superposition.json manifest')
|
|
938
964
|
.option('--no-interactive', 'Use manifest values directly without questionnaire (requires --from-manifest)')
|
|
939
|
-
.option('--
|
|
965
|
+
.option('--backup', 'Force or suppress backup; default is --no-backup inside a git repo, --backup outside')
|
|
940
966
|
.option('--backup-dir <path>', 'Custom backup directory location')
|
|
941
967
|
.option('--stack <type>', 'Base template: plain, compose')
|
|
942
968
|
.option('--language <list>', 'Comma-separated language overlays: dotnet, nodejs, python, mkdocs, java, go, rust, bun, powershell')
|
|
@@ -946,7 +972,13 @@ async function parseCliArgs() {
|
|
|
946
972
|
.option('--cloud-tools <list>', 'Comma-separated: aws-cli, azure-cli, gcloud, kubectl-helm, terraform, pulumi')
|
|
947
973
|
.option('--dev-tools <list>', 'Comma-separated: docker-in-docker, docker-sock, playwright, codex, git-helpers, pre-commit, commitlint, just, direnv, modern-cli-tools, ngrok')
|
|
948
974
|
.option('--port-offset <number>', 'Add offset to all exposed ports (e.g., 100 makes Grafana 3100 instead of 3000)')
|
|
975
|
+
.option('--target <environment>', 'Deployment target: local, codespaces, gitpod, devpod (optimizes for environment)', 'local')
|
|
976
|
+
.option('--minimal', 'Minimal mode - exclude optional/nice-to-have features and extensions')
|
|
977
|
+
.option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
|
|
949
978
|
.option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
|
|
979
|
+
.option('--write-manifest-only', 'Generate only superposition.json manifest without creating .devcontainer/ files')
|
|
980
|
+
.option('--preset <id>', 'Start from a preset (e.g., web-api, microservice)')
|
|
981
|
+
.option('--preset-param <value>', 'Set a preset parameter value (format: key=value, can be repeated)', (value, previous) => previous.concat([value]), [])
|
|
950
982
|
.action((options) => {
|
|
951
983
|
// Store options for main() to process
|
|
952
984
|
initOptions = options;
|
|
@@ -956,14 +988,31 @@ async function parseCliArgs() {
|
|
|
956
988
|
.command('regen')
|
|
957
989
|
.description('Regenerate devcontainer from existing superposition.json manifest')
|
|
958
990
|
.option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
|
|
959
|
-
.option('--
|
|
991
|
+
.option('--backup', 'Force or suppress backup; default is --no-backup inside a git repo, --backup outside')
|
|
960
992
|
.option('--backup-dir <path>', 'Custom backup directory location')
|
|
993
|
+
.option('--minimal', 'Minimal mode - exclude optional/nice-to-have features and extensions')
|
|
994
|
+
.option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
|
|
961
995
|
.action((options) => {
|
|
962
996
|
const outputPath = options.output || './.devcontainer';
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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'));
|
|
967
1016
|
process.exit(1);
|
|
968
1017
|
}
|
|
969
1018
|
// Store options for main() to process
|
|
@@ -978,16 +1027,51 @@ async function parseCliArgs() {
|
|
|
978
1027
|
.command('list')
|
|
979
1028
|
.description('List available overlays and presets')
|
|
980
1029
|
.option('--category <type>', 'Filter by category: language, database, observability, cloud, dev, preset')
|
|
1030
|
+
.option('--tags <list>', 'Filter by tags (comma-separated)')
|
|
1031
|
+
.option('--supports <stack>', 'Filter by stack support: plain, compose')
|
|
1032
|
+
.option('--json', 'Output as JSON for scripting')
|
|
1033
|
+
.action(async (options) => {
|
|
1034
|
+
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
1035
|
+
await listCommand(overlaysConfig, options);
|
|
1036
|
+
process.exit(0);
|
|
1037
|
+
});
|
|
1038
|
+
// Explain command
|
|
1039
|
+
program
|
|
1040
|
+
.command('explain <overlay>')
|
|
1041
|
+
.description('Show detailed information about an overlay')
|
|
1042
|
+
.option('--json', 'Output as JSON for scripting')
|
|
1043
|
+
.action(async (overlayId, options) => {
|
|
1044
|
+
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
1045
|
+
await explainCommand(overlaysConfig, OVERLAYS_DIR, overlayId, options);
|
|
1046
|
+
process.exit(0);
|
|
1047
|
+
});
|
|
1048
|
+
// Plan command
|
|
1049
|
+
program
|
|
1050
|
+
.command('plan')
|
|
1051
|
+
.description('Preview what will be generated before creating devcontainer')
|
|
1052
|
+
.option('--stack <type>', 'Base template: plain, compose', 'compose')
|
|
1053
|
+
.option('--overlays <list>', 'Comma-separated list of overlay IDs')
|
|
1054
|
+
.option('--port-offset <number>', 'Add offset to all exposed ports', (val) => parseInt(val, 10), 0)
|
|
1055
|
+
.option('-o, --output <path>', 'Compare against existing config at this path (default: ./.devcontainer)')
|
|
1056
|
+
.option('--diff', 'Compare planned output vs existing configuration')
|
|
1057
|
+
.option('--diff-format <format>', 'Diff output format: color (default), json', 'color')
|
|
1058
|
+
.option('--diff-context <lines>', 'Context lines in diff output', (val) => parseInt(val, 10), 3)
|
|
1059
|
+
.option('--json', 'Output as JSON for scripting')
|
|
981
1060
|
.action(async (options) => {
|
|
982
|
-
|
|
1061
|
+
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
1062
|
+
await planCommand(overlaysConfig, OVERLAYS_DIR, options);
|
|
1063
|
+
process.exit(0);
|
|
983
1064
|
});
|
|
984
1065
|
// Doctor command
|
|
985
1066
|
program
|
|
986
1067
|
.command('doctor')
|
|
987
1068
|
.description('Check environment and validate configuration')
|
|
988
1069
|
.option('-o, --output <path>', 'Devcontainer path to validate (default: ./.devcontainer)')
|
|
1070
|
+
.option('--fix', 'Apply automatic fixes where possible')
|
|
1071
|
+
.option('--json', 'Output as JSON for scripting')
|
|
989
1072
|
.action(async (options) => {
|
|
990
|
-
|
|
1073
|
+
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
1074
|
+
await doctorCommand(overlaysConfig, OVERLAYS_DIR, options);
|
|
991
1075
|
});
|
|
992
1076
|
await program.parseAsync(process.argv);
|
|
993
1077
|
// If init or regen command was run, return the options
|
|
@@ -1030,14 +1114,61 @@ async function parseCliArgs() {
|
|
|
1030
1114
|
if (initOptions.portOffset) {
|
|
1031
1115
|
config.portOffset = parseInt(initOptions.portOffset, 10);
|
|
1032
1116
|
}
|
|
1117
|
+
if (initOptions.target) {
|
|
1118
|
+
config.target = initOptions.target;
|
|
1119
|
+
}
|
|
1120
|
+
if (initOptions.minimal) {
|
|
1121
|
+
config.minimal = true;
|
|
1122
|
+
}
|
|
1123
|
+
if (initOptions.editor) {
|
|
1124
|
+
const editorLower = initOptions.editor.toLowerCase();
|
|
1125
|
+
if (['vscode', 'jetbrains', 'none'].includes(editorLower)) {
|
|
1126
|
+
config.editor = editorLower;
|
|
1127
|
+
}
|
|
1128
|
+
else {
|
|
1129
|
+
console.warn(chalk.yellow(`⚠️ Invalid editor profile: ${initOptions.editor}, using default (vscode)`));
|
|
1130
|
+
config.editor = 'vscode';
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1033
1133
|
if (initOptions.output)
|
|
1034
1134
|
config.outputPath = initOptions.output;
|
|
1135
|
+
// Handle --preset flag
|
|
1136
|
+
if (initOptions.preset) {
|
|
1137
|
+
config.preset = initOptions.preset;
|
|
1138
|
+
}
|
|
1139
|
+
// Handle --preset-param flags (can be repeated)
|
|
1140
|
+
if (initOptions.presetParam && initOptions.presetParam.length > 0) {
|
|
1141
|
+
if (!initOptions.preset) {
|
|
1142
|
+
console.warn(chalk.yellow('⚠️ Ignoring --preset-param because no --preset was provided. ' +
|
|
1143
|
+
'Preset parameters only apply when a preset is selected (e.g., --preset web-api --preset-param broker=nats).'));
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
const presetChoices = {};
|
|
1147
|
+
for (const param of initOptions.presetParam) {
|
|
1148
|
+
const eqIdx = param.indexOf('=');
|
|
1149
|
+
if (eqIdx > 0) {
|
|
1150
|
+
const key = param.slice(0, eqIdx).trim();
|
|
1151
|
+
const value = param.slice(eqIdx + 1).trim();
|
|
1152
|
+
if (key) {
|
|
1153
|
+
presetChoices[key] = value;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
else {
|
|
1157
|
+
console.warn(chalk.yellow(`⚠️ Invalid --preset-param format: "${param}". Expected "key=value" (e.g., --preset-param broker=nats).`));
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (Object.keys(presetChoices).length > 0) {
|
|
1161
|
+
config.presetChoices = presetChoices;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1035
1165
|
return {
|
|
1036
1166
|
config,
|
|
1037
1167
|
manifestPath: initOptions.fromManifest,
|
|
1038
|
-
|
|
1168
|
+
backupOverride: initOptions.backup, // undefined = auto-detect; true = --backup; false = --no-backup
|
|
1039
1169
|
backupDir: initOptions.backupDir,
|
|
1040
1170
|
noInteractive: initOptions.interactive === false, // Commander creates options.interactive = false for --no-interactive
|
|
1171
|
+
writeManifestOnly: initOptions.writeManifestOnly === true,
|
|
1041
1172
|
};
|
|
1042
1173
|
}
|
|
1043
1174
|
async function main() {
|
|
@@ -1051,7 +1182,6 @@ async function main() {
|
|
|
1051
1182
|
}
|
|
1052
1183
|
let manifest;
|
|
1053
1184
|
let manifestDir;
|
|
1054
|
-
let shouldBackup = true;
|
|
1055
1185
|
let backupDir;
|
|
1056
1186
|
let useManifestOnly = false;
|
|
1057
1187
|
// Handle manifest loading
|
|
@@ -1068,10 +1198,7 @@ async function main() {
|
|
|
1068
1198
|
process.exit(1);
|
|
1069
1199
|
}
|
|
1070
1200
|
manifest = loadedManifest;
|
|
1071
|
-
// Check for
|
|
1072
|
-
if (cliArgs.noBackup) {
|
|
1073
|
-
shouldBackup = false;
|
|
1074
|
-
}
|
|
1201
|
+
// Check for interaction options
|
|
1075
1202
|
if (cliArgs.backupDir) {
|
|
1076
1203
|
backupDir = cliArgs.backupDir;
|
|
1077
1204
|
}
|
|
@@ -1079,20 +1206,51 @@ async function main() {
|
|
|
1079
1206
|
useManifestOnly = true;
|
|
1080
1207
|
}
|
|
1081
1208
|
}
|
|
1209
|
+
// Determine whether to create a backup:
|
|
1210
|
+
// --backup → always backup
|
|
1211
|
+
// --no-backup → never backup
|
|
1212
|
+
// (neither) → backup only when NOT inside a git repository
|
|
1213
|
+
// (git already tracks history, so backups are redundant)
|
|
1214
|
+
const backupCheckPath = path.resolve(cliArgs?.config?.outputPath || manifestDir || './.devcontainer');
|
|
1215
|
+
const inGitRepo = isInsideGitRepo(backupCheckPath);
|
|
1216
|
+
let shouldBackup;
|
|
1217
|
+
if (cliArgs?.backupOverride === true) {
|
|
1218
|
+
shouldBackup = true;
|
|
1219
|
+
}
|
|
1220
|
+
else if (cliArgs?.backupOverride === false) {
|
|
1221
|
+
shouldBackup = false;
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
// Auto-detect based on git presence
|
|
1225
|
+
shouldBackup = !inGitRepo;
|
|
1226
|
+
if (!shouldBackup) {
|
|
1227
|
+
console.log(chalk.dim('ℹ Skipping backup — git repo detected (use --backup to force one)\n'));
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1082
1230
|
// Create backup if needed
|
|
1231
|
+
let actualBackupPath;
|
|
1083
1232
|
if (shouldBackup && manifest) {
|
|
1084
1233
|
// Output path is the directory containing the manifest
|
|
1085
1234
|
const outputPath = manifestDir || './.devcontainer';
|
|
1086
1235
|
const backupPath = await createBackup(outputPath, backupDir);
|
|
1087
1236
|
if (backupPath) {
|
|
1237
|
+
actualBackupPath = backupPath;
|
|
1088
1238
|
console.log(chalk.green(`✓ Backup created: ${backupPath}\n`));
|
|
1089
|
-
|
|
1239
|
+
ensureBackupPatternsInGitignore(outputPath);
|
|
1090
1240
|
}
|
|
1091
1241
|
}
|
|
1092
1242
|
// Build answers based on mode
|
|
1093
1243
|
let answers;
|
|
1094
|
-
|
|
1095
|
-
|
|
1244
|
+
const isRegen = !!manifest;
|
|
1245
|
+
// Check if there are CLI overrides beyond just output path and preset flags
|
|
1246
|
+
// Preset/presetChoices alone don't constitute "CLI overrides" that bypass interactive mode
|
|
1247
|
+
const hasCliOverrides = cliArgs &&
|
|
1248
|
+
Object.keys(cliArgs.config).some((key) => key !== 'outputPath' &&
|
|
1249
|
+
key !== 'preset' &&
|
|
1250
|
+
key !== 'presetChoices' &&
|
|
1251
|
+
cliArgs.config[key] !== undefined);
|
|
1252
|
+
if (useManifestOnly && manifest && !hasCliOverrides) {
|
|
1253
|
+
// Mode 1: Manifest-only (--from-manifest --no-interactive, no CLI overrides)
|
|
1096
1254
|
const manifestAnswers = buildAnswersFromManifest(manifest, manifestDir);
|
|
1097
1255
|
answers = mergeAnswers(manifestAnswers);
|
|
1098
1256
|
console.log('\n' +
|
|
@@ -1110,8 +1268,9 @@ async function main() {
|
|
|
1110
1268
|
: '') +
|
|
1111
1269
|
chalk.gray(` Output: ${answers.outputPath}`), { padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: 1 }));
|
|
1112
1270
|
}
|
|
1113
|
-
else if (cliArgs && cliArgs.config.stack) {
|
|
1271
|
+
else if (cliArgs && (cliArgs.config.stack || hasCliOverrides)) {
|
|
1114
1272
|
// Mode 2: CLI-based (with optional manifest defaults)
|
|
1273
|
+
// This includes regen with --minimal or --editor flags
|
|
1115
1274
|
const cliAnswers = buildAnswersFromCliArgs(cliArgs.config);
|
|
1116
1275
|
const manifestAnswers = manifest
|
|
1117
1276
|
? buildAnswersFromManifest(manifest, manifestDir)
|
|
@@ -1119,16 +1278,30 @@ async function main() {
|
|
|
1119
1278
|
answers = mergeAnswers(manifestAnswers, cliAnswers, {
|
|
1120
1279
|
outputPath: cliAnswers.outputPath || './.devcontainer',
|
|
1121
1280
|
});
|
|
1281
|
+
const modeLabel = useManifestOnly && hasCliOverrides
|
|
1282
|
+
? 'Regenerating from Manifest with Overrides'
|
|
1283
|
+
: 'Running in CLI mode';
|
|
1122
1284
|
console.log('\n' +
|
|
1123
|
-
boxen(chalk.bold(
|
|
1285
|
+
boxen(chalk.bold(modeLabel), {
|
|
1124
1286
|
padding: 0.5,
|
|
1125
1287
|
borderColor: 'blue',
|
|
1126
1288
|
borderStyle: 'round',
|
|
1127
1289
|
}));
|
|
1290
|
+
// Show what's being overridden
|
|
1291
|
+
if (useManifestOnly && hasCliOverrides) {
|
|
1292
|
+
const overrides = [];
|
|
1293
|
+
if (cliAnswers.minimal)
|
|
1294
|
+
overrides.push('minimal mode');
|
|
1295
|
+
if (cliAnswers.editor)
|
|
1296
|
+
overrides.push(`editor: ${cliAnswers.editor}`);
|
|
1297
|
+
if (overrides.length > 0) {
|
|
1298
|
+
console.log(chalk.dim(` Overrides: ${overrides.join(', ')}`));
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1128
1301
|
}
|
|
1129
1302
|
else {
|
|
1130
|
-
// Mode 3: Interactive (with optional manifest pre-population)
|
|
1131
|
-
const interactiveAnswers = await runQuestionnaire(manifest, manifestDir);
|
|
1303
|
+
// Mode 3: Interactive (with optional manifest pre-population and CLI preset pre-selection)
|
|
1304
|
+
const interactiveAnswers = await runQuestionnaire(manifest, manifestDir, cliArgs?.config.preset, cliArgs?.config.presetChoices);
|
|
1132
1305
|
answers = mergeAnswers(interactiveAnswers);
|
|
1133
1306
|
}
|
|
1134
1307
|
// Show configuration summary
|
|
@@ -1157,27 +1330,36 @@ async function main() {
|
|
|
1157
1330
|
borderStyle: 'round',
|
|
1158
1331
|
margin: { top: 0, bottom: 1 },
|
|
1159
1332
|
}));
|
|
1333
|
+
// Check if we're in manifest-only mode
|
|
1334
|
+
const isManifestOnly = cliArgs?.writeManifestOnly === true;
|
|
1160
1335
|
// Generate with spinner
|
|
1161
1336
|
const spinner = ora({
|
|
1162
|
-
text:
|
|
1337
|
+
text: isManifestOnly
|
|
1338
|
+
? chalk.cyan('Generating manifest file...')
|
|
1339
|
+
: chalk.cyan('Generating devcontainer configuration...'),
|
|
1163
1340
|
color: 'cyan',
|
|
1164
1341
|
}).start();
|
|
1165
1342
|
try {
|
|
1166
|
-
|
|
1167
|
-
|
|
1343
|
+
let summary;
|
|
1344
|
+
if (isManifestOnly) {
|
|
1345
|
+
summary = await generateManifestOnly(answers, undefined, { isRegen });
|
|
1346
|
+
spinner.succeed(chalk.green('Manifest created successfully!'));
|
|
1347
|
+
}
|
|
1348
|
+
else {
|
|
1349
|
+
summary = await composeDevContainer(answers, undefined, { isRegen });
|
|
1350
|
+
spinner.succeed(chalk.green('DevContainer created successfully!'));
|
|
1351
|
+
}
|
|
1352
|
+
// Update summary with backup path and regen status
|
|
1353
|
+
if (actualBackupPath) {
|
|
1354
|
+
summary.backupPath = actualBackupPath;
|
|
1355
|
+
}
|
|
1356
|
+
// Print comprehensive summary
|
|
1357
|
+
printSummary(summary);
|
|
1168
1358
|
}
|
|
1169
1359
|
catch (error) {
|
|
1170
|
-
spinner.fail(chalk.red('Failed to create devcontainer'));
|
|
1360
|
+
spinner.fail(chalk.red(isManifestOnly ? 'Failed to create manifest' : 'Failed to create devcontainer'));
|
|
1171
1361
|
throw error;
|
|
1172
1362
|
}
|
|
1173
|
-
// Success message
|
|
1174
|
-
console.log('\n' +
|
|
1175
|
-
boxen(chalk.bold.green('✓ Setup Complete!\n\n') +
|
|
1176
|
-
chalk.white('Next steps:\n') +
|
|
1177
|
-
chalk.gray(' 1. Review the generated .devcontainer/ folder\n') +
|
|
1178
|
-
chalk.gray(" 2. Customize as needed (it's just normal JSON!)\n") +
|
|
1179
|
-
chalk.gray(' 3. Open in VS Code and rebuild container\n\n') +
|
|
1180
|
-
chalk.dim('The generated configuration is fully editable and independent of this tool.'), { padding: 1, borderColor: 'green', borderStyle: 'double', margin: 1 }));
|
|
1181
1363
|
}
|
|
1182
1364
|
catch (error) {
|
|
1183
1365
|
console.error('\n' +
|