container-superposition 0.1.6 → 0.1.8
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 +24 -15
- package/dist/scripts/init.js +1 -1534
- package/dist/scripts/init.js.map +1 -1
- package/dist/tool/cli/args.d.ts +20 -0
- package/dist/tool/cli/args.d.ts.map +1 -0
- package/dist/tool/cli/args.js +325 -0
- package/dist/tool/cli/args.js.map +1 -0
- package/dist/tool/cli/run.d.ts +2 -0
- package/dist/tool/cli/run.d.ts.map +1 -0
- package/dist/tool/cli/run.js +318 -0
- package/dist/tool/cli/run.js.map +1 -0
- package/dist/tool/commands/adopt.d.ts.map +1 -1
- package/dist/tool/commands/adopt.js +1 -27
- package/dist/tool/commands/adopt.js.map +1 -1
- package/dist/tool/commands/doctor.d.ts +3 -0
- package/dist/tool/commands/doctor.d.ts.map +1 -1
- package/dist/tool/commands/doctor.js +1068 -70
- package/dist/tool/commands/doctor.js.map +1 -1
- package/dist/tool/commands/explain.d.ts.map +1 -1
- package/dist/tool/commands/explain.js +18 -0
- package/dist/tool/commands/explain.js.map +1 -1
- package/dist/tool/commands/migrate.d.ts +7 -0
- package/dist/tool/commands/migrate.d.ts.map +1 -0
- package/dist/tool/commands/migrate.js +52 -0
- package/dist/tool/commands/migrate.js.map +1 -0
- package/dist/tool/questionnaire/answers.d.ts +16 -0
- package/dist/tool/questionnaire/answers.d.ts.map +1 -0
- package/dist/tool/questionnaire/answers.js +102 -0
- package/dist/tool/questionnaire/answers.js.map +1 -0
- package/dist/tool/questionnaire/composer.d.ts +3 -3
- package/dist/tool/questionnaire/composer.d.ts.map +1 -1
- package/dist/tool/questionnaire/composer.js +902 -37
- package/dist/tool/questionnaire/composer.js.map +1 -1
- package/dist/tool/questionnaire/presets.d.ts +60 -0
- package/dist/tool/questionnaire/presets.d.ts.map +1 -0
- package/dist/tool/questionnaire/presets.js +164 -0
- package/dist/tool/questionnaire/presets.js.map +1 -0
- package/dist/tool/questionnaire/questionnaire.d.ts +10 -0
- package/dist/tool/questionnaire/questionnaire.d.ts.map +1 -0
- package/dist/tool/questionnaire/questionnaire.js +580 -0
- package/dist/tool/questionnaire/questionnaire.js.map +1 -0
- package/dist/tool/schema/manifest-migrations.d.ts +5 -0
- package/dist/tool/schema/manifest-migrations.d.ts.map +1 -1
- package/dist/tool/schema/manifest-migrations.js +45 -0
- package/dist/tool/schema/manifest-migrations.js.map +1 -1
- package/dist/tool/schema/overlay-loader.d.ts.map +1 -1
- package/dist/tool/schema/overlay-loader.js +25 -0
- package/dist/tool/schema/overlay-loader.js.map +1 -1
- package/dist/tool/schema/project-config.d.ts +14 -2
- package/dist/tool/schema/project-config.d.ts.map +1 -1
- package/dist/tool/schema/project-config.js +277 -34
- package/dist/tool/schema/project-config.js.map +1 -1
- package/dist/tool/schema/target-rules.d.ts +78 -0
- package/dist/tool/schema/target-rules.d.ts.map +1 -0
- package/dist/tool/schema/target-rules.js +367 -0
- package/dist/tool/schema/target-rules.js.map +1 -0
- package/dist/tool/schema/types.d.ts +123 -12
- package/dist/tool/schema/types.d.ts.map +1 -1
- package/dist/tool/utils/merge.d.ts.map +1 -1
- package/dist/tool/utils/merge.js +9 -0
- package/dist/tool/utils/merge.js.map +1 -1
- package/dist/tool/utils/parameters.d.ts +76 -0
- package/dist/tool/utils/parameters.d.ts.map +1 -0
- package/dist/tool/utils/parameters.js +125 -0
- package/dist/tool/utils/parameters.js.map +1 -0
- package/dist/tool/utils/paths.d.ts +2 -0
- package/dist/tool/utils/paths.d.ts.map +1 -0
- package/dist/tool/utils/paths.js +31 -0
- package/dist/tool/utils/paths.js.map +1 -0
- package/docs/creating-overlays.md +151 -2
- package/docs/deployment-targets.md +88 -56
- package/docs/examples.md +20 -17
- package/docs/filesystem-contract.md +5 -0
- package/docs/minimal-and-editor.md +65 -5
- package/docs/overlay-imports.md +202 -101
- package/docs/overlays.md +162 -34
- package/docs/quick-reference.md +99 -0
- package/docs/specs/003-mkdocs2-overlay/spec.md +114 -0
- package/docs/specs/004-doctor-fix/spec.md +70 -0
- package/docs/specs/005-cuda-overlay/spec.md +101 -0
- package/docs/specs/006-rocm-overlay/spec.md +109 -0
- package/docs/specs/007-init-project-file/spec.md +66 -0
- package/docs/specs/007-target-aware-generation/spec.md +126 -0
- package/docs/specs/008-project-file-canonical/spec.md +83 -0
- package/docs/specs/009-project-env/spec.md +147 -0
- package/docs/specs/010-compose-env-materialization/spec.md +130 -0
- package/docs/specs/011-overlay-parameters/spec.md +235 -0
- package/overlays/.shared/README.md +105 -21
- package/overlays/.shared/compose/common-healthchecks.md +60 -0
- package/overlays/.shared/compose/nvidia-gpu-devcontainer.yml +22 -0
- package/overlays/.shared/vscode/recommended-extensions.json +15 -11
- package/overlays/alertmanager/setup.sh +4 -19
- package/overlays/alertmanager/verify.sh +8 -9
- package/overlays/all/README.md +43 -0
- package/overlays/all/devcontainer.patch.json +6 -0
- package/overlays/all/overlay.yml +14 -0
- package/overlays/amp/setup.sh +5 -0
- package/overlays/bun/setup.sh +10 -1
- package/overlays/bun/verify.sh +6 -1
- package/overlays/claude-code/setup.sh +5 -0
- package/overlays/cloudflared/setup.sh +9 -12
- package/overlays/codex/README.md +9 -6
- package/overlays/codex/devcontainer.patch.json +7 -1
- package/overlays/codex/setup.sh +5 -0
- package/overlays/codex/verify.sh +8 -0
- package/overlays/comfyui/.env.example +34 -0
- package/overlays/comfyui/README.md +342 -0
- package/overlays/comfyui/devcontainer.patch.json +15 -0
- package/overlays/comfyui/docker-compose.yml +39 -0
- package/overlays/comfyui/overlay.yml +20 -0
- package/overlays/comfyui/setup.sh +36 -0
- package/overlays/comfyui/verify.sh +103 -0
- package/overlays/commitlint/setup.sh +5 -0
- package/overlays/cuda/README.md +179 -0
- package/overlays/cuda/devcontainer.patch.json +7 -0
- package/overlays/cuda/overlay.yml +17 -0
- package/overlays/cuda/setup.sh +32 -0
- package/overlays/cuda/verify.sh +38 -0
- package/overlays/devcontainer-cli/README.md +50 -0
- package/overlays/devcontainer-cli/devcontainer.patch.json +13 -0
- package/overlays/devcontainer-cli/overlay.yml +16 -0
- package/overlays/devcontainer-cli/setup.sh +14 -0
- package/overlays/direnv/devcontainer.patch.json +6 -0
- package/overlays/direnv/setup.sh +7 -6
- package/overlays/dotnet/setup.sh +14 -7
- package/overlays/duckdb/devcontainer.patch.json +1 -2
- package/overlays/gcloud/devcontainer.patch.json +0 -6
- package/overlays/gcloud/setup.sh +51 -0
- package/overlays/gemini-cli/setup.sh +5 -0
- package/overlays/git-helpers/devcontainer.patch.json +2 -1
- package/overlays/go/setup.sh +15 -14
- package/overlays/jaeger/overlay.yml +2 -0
- package/overlays/just/setup.sh +5 -17
- package/overlays/k3d/README.md +201 -0
- package/overlays/k3d/devcontainer.patch.json +9 -0
- package/overlays/k3d/overlay.yml +19 -0
- package/overlays/k3d/setup.sh +34 -0
- package/overlays/k3d/verify.sh +38 -0
- package/overlays/keycloak/docker-compose.yml +6 -4
- package/overlays/keycloak/verify.sh +4 -3
- package/overlays/kind/devcontainer.patch.json +1 -2
- package/overlays/kind/setup.sh +8 -17
- package/overlays/minio/setup.sh +10 -18
- package/overlays/mkdocs/overlay.yml +2 -1
- package/overlays/mkdocs2/README.md +135 -0
- package/overlays/mkdocs2/devcontainer.patch.json +19 -0
- package/overlays/mkdocs2/overlay.yml +17 -0
- package/overlays/mkdocs2/setup.sh +67 -0
- package/overlays/mkdocs2/verify.sh +35 -0
- package/overlays/modern-cli-tools/devcontainer.patch.json +7 -1
- package/overlays/modern-cli-tools/setup.sh +21 -71
- package/overlays/mongodb/devcontainer.patch.json +0 -6
- package/overlays/mongodb/setup.sh +59 -0
- package/overlays/mysql/verify.sh +4 -3
- package/overlays/nats/.env.example +1 -1
- package/overlays/nats/README.md +1 -1
- package/overlays/nats/docker-compose.yml +1 -1
- package/overlays/ngrok/setup.sh +9 -6
- package/overlays/nodejs/setup.sh +5 -0
- package/overlays/ollama/.env.example +14 -0
- package/overlays/ollama/README.md +325 -0
- package/overlays/ollama/devcontainer.patch.json +14 -0
- package/overlays/ollama/docker-compose.yml +24 -0
- package/overlays/ollama/overlay.yml +22 -0
- package/overlays/ollama/setup.sh +106 -0
- package/overlays/ollama/verify.sh +99 -0
- package/overlays/open-webui/.env.example +5 -0
- package/overlays/open-webui/README.md +162 -0
- package/overlays/open-webui/devcontainer.patch.json +14 -0
- package/overlays/open-webui/docker-compose.yml +23 -0
- package/overlays/open-webui/overlay.yml +38 -0
- package/overlays/openapi-tools/devcontainer.patch.json +1 -2
- package/overlays/openapi-tools/setup.sh +9 -8
- package/overlays/opencode/setup.sh +5 -0
- package/overlays/otel-collector/overlay.yml +2 -0
- package/overlays/otel-collector/setup.sh +3 -16
- package/overlays/otel-demo-nodejs/verify.sh +8 -9
- package/overlays/otel-demo-python/verify.sh +16 -10
- package/overlays/pandoc/README.md +22 -15
- package/overlays/pandoc/devcontainer.patch.json +6 -2
- package/overlays/pandoc/setup.sh +217 -18
- package/overlays/pandoc/verify.sh +16 -4
- package/overlays/pgvector/.env.example +6 -0
- package/overlays/pgvector/README.md +215 -0
- package/overlays/pgvector/devcontainer.patch.json +23 -0
- package/overlays/pgvector/docker-compose.yml +32 -0
- package/overlays/pgvector/overlay.yml +44 -0
- package/overlays/playwright/devcontainer.patch.json +3 -1
- package/overlays/playwright/setup.sh +37 -0
- package/overlays/postgres/.env.example +5 -5
- package/overlays/postgres/devcontainer.patch.json +4 -4
- package/overlays/postgres/docker-compose.yml +15 -5
- package/overlays/postgres/overlay.yml +19 -1
- package/overlays/powershell/setup.sh +49 -13
- package/overlays/pre-commit/setup.sh +12 -3
- package/overlays/prometheus/overlay.yml +2 -0
- package/overlays/promtail/verify.sh +16 -10
- package/overlays/pulumi/devcontainer.patch.json +1 -1
- package/overlays/python/setup.sh +28 -9
- package/overlays/python/verify.sh +4 -2
- package/overlays/qdrant/.env.example +4 -0
- package/overlays/qdrant/README.md +216 -0
- package/overlays/qdrant/devcontainer.patch.json +20 -0
- package/overlays/qdrant/docker-compose.yml +25 -0
- package/overlays/qdrant/overlay.yml +40 -0
- package/overlays/redpanda/docker-compose.yml +3 -5
- package/overlays/rocm/README.md +227 -0
- package/overlays/rocm/devcontainer.patch.json +4 -0
- package/overlays/rocm/overlay.yml +17 -0
- package/overlays/rocm/setup.sh +45 -0
- package/overlays/rocm/verify.sh +47 -0
- package/overlays/rust/setup.sh +11 -18
- package/overlays/skaffold/README.md +256 -0
- package/overlays/skaffold/devcontainer.patch.json +9 -0
- package/overlays/skaffold/overlay.yml +20 -0
- package/overlays/skaffold/setup.sh +33 -0
- package/overlays/skaffold/verify.sh +24 -0
- package/overlays/spec-kit/setup.sh +7 -3
- package/overlays/sqlite/setup.sh +14 -14
- package/overlays/sqlserver/docker-compose.yml +3 -3
- package/overlays/sqlserver/verify.sh +22 -5
- package/overlays/tempo/verify.sh +16 -10
- package/overlays/tilt/devcontainer.patch.json +1 -2
- package/overlays/tilt/setup.sh +14 -4
- package/overlays/windsurf-cli/setup.sh +27 -4
- package/overlays/windsurf-cli/verify.sh +13 -3
- package/package.json +4 -2
- package/templates/scripts/setup-utils.sh +228 -0
- package/tool/schema/config.schema.json +141 -9
- package/tool/schema/overlay-manifest.schema.json +38 -0
- package/overlays/.shared/compose/common-healthchecks.yml +0 -38
- /package/overlays/otel-demo-nodejs/{Dockerfile-otel-demo-nodejs → Dockerfile} +0 -0
- /package/overlays/otel-demo-nodejs/{package-otel-demo-nodejs.json → package.json} +0 -0
- /package/overlays/otel-demo-nodejs/{server-otel-demo-nodejs.js → server.js} +0 -0
- /package/overlays/otel-demo-nodejs/{tracing-otel-demo-nodejs.js → tracing.js} +0 -0
- /package/overlays/otel-demo-python/{Dockerfile-otel-demo-python → Dockerfile} +0 -0
- /package/overlays/otel-demo-python/{app-otel-demo-python.py → app.py} +0 -0
- /package/overlays/otel-demo-python/{requirements-otel-demo-python.txt → requirements.txt} +0 -0
package/dist/scripts/init.js
CHANGED
|
@@ -1,1537 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { Command } from 'commander';
|
|
6
|
-
import chalk from 'chalk';
|
|
7
|
-
import boxen from 'boxen';
|
|
8
|
-
import ora from 'ora';
|
|
9
|
-
import { select, checkbox, input } from '@inquirer/prompts';
|
|
10
|
-
import yaml from 'js-yaml';
|
|
11
|
-
import { composeDevContainer, generateManifestOnly } from '../tool/questionnaire/composer.js';
|
|
12
|
-
import { loadOverlaysConfig } from '../tool/schema/overlay-loader.js';
|
|
13
|
-
import { listCommand } from '../tool/commands/list.js';
|
|
14
|
-
import { explainCommand } from '../tool/commands/explain.js';
|
|
15
|
-
import { planCommand } from '../tool/commands/plan.js';
|
|
16
|
-
import { doctorCommand } from '../tool/commands/doctor.js';
|
|
17
|
-
import { adoptCommand } from '../tool/commands/adopt.js';
|
|
18
|
-
import { hashCommand } from '../tool/commands/hash.js';
|
|
19
|
-
import { getIncompatibleOverlays, DEPLOYMENT_TARGETS } from '../tool/schema/deployment-targets.js';
|
|
20
|
-
import { migrateManifest, needsMigration, detectManifestVersion, isVersionSupported, CURRENT_MANIFEST_VERSION, SUPPORTED_MANIFEST_VERSIONS, } from '../tool/schema/manifest-migrations.js';
|
|
21
|
-
import { getToolVersion } from '../tool/utils/version.js';
|
|
22
|
-
import { printSummary } from '../tool/utils/summary.js';
|
|
23
|
-
import { buildAnswersFromProjectConfig, loadProjectConfig, writeProjectConfigCustomizations, } from '../tool/schema/project-config.js';
|
|
24
|
-
import { isInsideGitRepo, createBackup, ensureBackupPatternsInGitignore, } from '../tool/utils/backup.js';
|
|
25
|
-
// Get __dirname equivalent in ESM
|
|
26
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
-
const __dirname = path.dirname(__filename);
|
|
28
|
-
const OVERLAYS_DIR_CANDIDATES = [
|
|
29
|
-
// When running from TypeScript sources (e.g. ts-node), __dirname is "<root>/scripts"
|
|
30
|
-
path.join(__dirname, '..', 'overlays'),
|
|
31
|
-
// When running from compiled JS in "dist/scripts", __dirname is "<root>/dist/scripts"
|
|
32
|
-
path.join(__dirname, '..', '..', 'overlays'),
|
|
33
|
-
];
|
|
34
|
-
const OVERLAYS_DIR = OVERLAYS_DIR_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
|
|
35
|
-
OVERLAYS_DIR_CANDIDATES[0];
|
|
36
|
-
const OVERLAYS_CONFIG_CANDIDATES = [
|
|
37
|
-
// When running from TypeScript sources (e.g. ts-node), __dirname is "<root>/scripts"
|
|
38
|
-
// and "../overlays/index.yml" resolves to "<root>/overlays/index.yml".
|
|
39
|
-
path.join(__dirname, '..', 'overlays', 'index.yml'),
|
|
40
|
-
// When running from compiled JS in "dist/scripts", __dirname is "<root>/dist/scripts"
|
|
41
|
-
// and "../../overlays/index.yml" resolves to "<root>/overlays/index.yml".
|
|
42
|
-
path.join(__dirname, '..', '..', 'overlays', 'index.yml'),
|
|
43
|
-
];
|
|
44
|
-
const OVERLAYS_CONFIG_PATH = OVERLAYS_CONFIG_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
|
|
45
|
-
OVERLAYS_CONFIG_CANDIDATES[0];
|
|
46
|
-
const PRESETS_DIR_CANDIDATES = [
|
|
47
|
-
path.join(__dirname, '..', 'overlays', '.presets'),
|
|
48
|
-
path.join(__dirname, '..', '..', 'overlays', '.presets'),
|
|
49
|
-
];
|
|
50
|
-
const PRESETS_DIR = PRESETS_DIR_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
|
|
51
|
-
PRESETS_DIR_CANDIDATES[0];
|
|
52
|
-
/**
|
|
53
|
-
* Load overlay metadata from individual manifests or fallback to YAML file
|
|
54
|
-
*/
|
|
55
|
-
function loadOverlaysConfigWrapper() {
|
|
56
|
-
return loadOverlaysConfig(OVERLAYS_DIR, OVERLAYS_CONFIG_PATH);
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Load preset definition from YAML file
|
|
60
|
-
*/
|
|
61
|
-
function loadPresetDefinition(presetId) {
|
|
62
|
-
const presetPath = path.join(PRESETS_DIR, `${presetId}.yml`);
|
|
63
|
-
if (!fs.existsSync(presetPath)) {
|
|
64
|
-
console.warn(chalk.yellow(`⚠️ Preset definition not found: ${presetPath}`));
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
const content = fs.readFileSync(presetPath, 'utf8');
|
|
68
|
-
return yaml.load(content);
|
|
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
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Expand a preset into a list of overlay IDs with user choices resolved
|
|
166
|
-
*/
|
|
167
|
-
async function expandPreset(presetId, stack, preProvidedChoices = {}) {
|
|
168
|
-
const preset = loadPresetDefinition(presetId);
|
|
169
|
-
if (!preset) {
|
|
170
|
-
return { overlays: [], choices: {} };
|
|
171
|
-
}
|
|
172
|
-
console.log(chalk.cyan(`\n📦 Expanding preset: ${preset.name}\n`));
|
|
173
|
-
const overlays = [...preset.selects.required];
|
|
174
|
-
const choices = {};
|
|
175
|
-
// Handle user choices (single overlay per option)
|
|
176
|
-
if (preset.selects.userChoice) {
|
|
177
|
-
for (const [key, choice] of Object.entries(preset.selects.userChoice)) {
|
|
178
|
-
const preProvidedValue = preProvidedChoices[key];
|
|
179
|
-
if (preProvidedValue !== undefined) {
|
|
180
|
-
// Validate the pre-provided value
|
|
181
|
-
if (!choice.options.includes(preProvidedValue)) {
|
|
182
|
-
const valid = choice.options.join(', ');
|
|
183
|
-
throw new Error(`Invalid value '${preProvidedValue}' for preset choice '${key}'. Valid options: ${valid}`);
|
|
184
|
-
}
|
|
185
|
-
console.log(chalk.dim(`✓ ${key}: ${preProvidedValue} (from CLI)`));
|
|
186
|
-
overlays.push(preProvidedValue);
|
|
187
|
-
choices[key] = preProvidedValue;
|
|
188
|
-
}
|
|
189
|
-
else {
|
|
190
|
-
const selectedOption = (await select({
|
|
191
|
-
message: choice.prompt,
|
|
192
|
-
choices: choice.options.map((opt) => ({
|
|
193
|
-
name: opt,
|
|
194
|
-
value: opt,
|
|
195
|
-
})),
|
|
196
|
-
default: choice.defaultOption,
|
|
197
|
-
}));
|
|
198
|
-
overlays.push(selectedOption);
|
|
199
|
-
choices[key] = selectedOption;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
// Handle parameterized slots (multiple overlays per option)
|
|
204
|
-
if (preset.parameters) {
|
|
205
|
-
for (const [key, param] of Object.entries(preset.parameters)) {
|
|
206
|
-
const preProvidedValue = preProvidedChoices[key];
|
|
207
|
-
let selectedId;
|
|
208
|
-
if (preProvidedValue !== undefined) {
|
|
209
|
-
// Validate the pre-provided value
|
|
210
|
-
const validOption = param.options.find((o) => o.id === preProvidedValue);
|
|
211
|
-
if (!validOption) {
|
|
212
|
-
const valid = param.options.map((o) => o.id).join(', ');
|
|
213
|
-
throw new Error(`Invalid value '${preProvidedValue}' for preset parameter '${key}'. Valid options: ${valid}`);
|
|
214
|
-
}
|
|
215
|
-
console.log(chalk.dim(`✓ ${key}: ${preProvidedValue} (from CLI)`));
|
|
216
|
-
selectedId = preProvidedValue;
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
const description = param.description || `Select ${key}`;
|
|
220
|
-
selectedId = (await select({
|
|
221
|
-
message: description,
|
|
222
|
-
choices: param.options.map((opt) => ({
|
|
223
|
-
name: opt.description ? `${opt.id} - ${opt.description}` : opt.id,
|
|
224
|
-
value: opt.id,
|
|
225
|
-
})),
|
|
226
|
-
default: param.default,
|
|
227
|
-
}));
|
|
228
|
-
}
|
|
229
|
-
// Add overlays for the selected option
|
|
230
|
-
const selectedOption = param.options.find((o) => o.id === selectedId);
|
|
231
|
-
if (selectedOption) {
|
|
232
|
-
overlays.push(...selectedOption.overlays);
|
|
233
|
-
}
|
|
234
|
-
choices[key] = selectedId;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
// Deduplicate overlays
|
|
238
|
-
const uniqueOverlays = [...new Set(overlays)];
|
|
239
|
-
console.log(chalk.dim(`✓ Preset will include: ${uniqueOverlays.join(', ')}\n`));
|
|
240
|
-
// Apply template substitution to glueConfig.environment values.
|
|
241
|
-
// Replaces {{parameters.<key>.id}} with the selected choice for <key>.
|
|
242
|
-
let resolvedGlueConfig = preset.glueConfig;
|
|
243
|
-
if (resolvedGlueConfig?.environment) {
|
|
244
|
-
const resolvedEnv = {};
|
|
245
|
-
for (const [envKey, envValue] of Object.entries(resolvedGlueConfig.environment)) {
|
|
246
|
-
resolvedEnv[envKey] = envValue.replace(/\{\{parameters\.(\w+)\.id\}\}/g, (_match, paramKey) => choices[paramKey] ?? _match);
|
|
247
|
-
}
|
|
248
|
-
resolvedGlueConfig = { ...resolvedGlueConfig, environment: resolvedEnv };
|
|
249
|
-
}
|
|
250
|
-
return { overlays: uniqueOverlays, choices, glueConfig: resolvedGlueConfig };
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Search for manifest file in multiple locations
|
|
254
|
-
*/
|
|
255
|
-
function findManifestFile(manifestPath) {
|
|
256
|
-
const searchPaths = [];
|
|
257
|
-
if (manifestPath) {
|
|
258
|
-
// If path specified, use it directly
|
|
259
|
-
searchPaths.push(manifestPath);
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
// Search in common locations
|
|
263
|
-
searchPaths.push('superposition.json', '.devcontainer/superposition.json', '../superposition.json', path.join(process.cwd(), 'superposition.json'), path.join(process.cwd(), '.devcontainer', 'superposition.json'));
|
|
264
|
-
}
|
|
265
|
-
for (const searchPath of searchPaths) {
|
|
266
|
-
const resolvedPath = path.resolve(searchPath);
|
|
267
|
-
if (fs.existsSync(resolvedPath)) {
|
|
268
|
-
return resolvedPath;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
return null;
|
|
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
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Load and validate manifest file
|
|
285
|
-
*/
|
|
286
|
-
function loadManifest(manifestPath) {
|
|
287
|
-
try {
|
|
288
|
-
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
289
|
-
const rawManifest = JSON.parse(content);
|
|
290
|
-
// Detect manifest version
|
|
291
|
-
const detectedVersion = detectManifestVersion(rawManifest);
|
|
292
|
-
// Check if version is supported
|
|
293
|
-
if (!isVersionSupported(detectedVersion)) {
|
|
294
|
-
console.error(chalk.red(`✗ Manifest version ${detectedVersion} is not supported.\n` +
|
|
295
|
-
` This tool supports versions: ${SUPPORTED_MANIFEST_VERSIONS.join(', ')}\n` +
|
|
296
|
-
` Please upgrade your tool or regenerate the manifest.`));
|
|
297
|
-
return null;
|
|
298
|
-
}
|
|
299
|
-
// Migrate if needed
|
|
300
|
-
let manifest;
|
|
301
|
-
if (needsMigration(rawManifest)) {
|
|
302
|
-
const oldVersion = rawManifest.manifestVersion || rawManifest.version || 'legacy';
|
|
303
|
-
console.log(chalk.cyan(`ℹ️ Migrating manifest from version ${oldVersion} to ${CURRENT_MANIFEST_VERSION}...`));
|
|
304
|
-
manifest = migrateManifest(rawManifest);
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
manifest = rawManifest;
|
|
308
|
-
}
|
|
309
|
-
// Basic validation
|
|
310
|
-
if (!manifest.baseTemplate) {
|
|
311
|
-
console.error(chalk.red('✗ Invalid manifest format: missing required field "baseTemplate"'));
|
|
312
|
-
return null;
|
|
313
|
-
}
|
|
314
|
-
if (!Array.isArray(manifest.overlays)) {
|
|
315
|
-
console.error(chalk.red('✗ Invalid manifest format: "overlays" must be an array'));
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
if (!manifest.overlays.every((overlay) => typeof overlay === 'string')) {
|
|
319
|
-
console.error(chalk.red('✗ Invalid manifest format: all "overlays" entries must be strings'));
|
|
320
|
-
return null;
|
|
321
|
-
}
|
|
322
|
-
return manifest;
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
console.error(chalk.red(`✗ Failed to load manifest: ${error instanceof Error ? error.message : String(error)}`));
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
/**
|
|
330
|
-
* Build checkbox choices for overlay selection with optional pre-selection
|
|
331
|
-
*/
|
|
332
|
-
function buildOverlayChoices(config, stack, categoryList, preselected) {
|
|
333
|
-
const choices = [];
|
|
334
|
-
categoryList.forEach((category) => {
|
|
335
|
-
const filtered = category.overlays.filter((o) => !o.supports || o.supports.length === 0 || o.supports.includes(stack));
|
|
336
|
-
if (filtered.length > 0) {
|
|
337
|
-
// Add category separator
|
|
338
|
-
choices.push({
|
|
339
|
-
type: 'separator',
|
|
340
|
-
separator: chalk.cyan(`──── ${category.name} ────`),
|
|
341
|
-
});
|
|
342
|
-
// Add overlays in this category
|
|
343
|
-
filtered.forEach((overlay) => {
|
|
344
|
-
choices.push({
|
|
345
|
-
name: overlay.name,
|
|
346
|
-
value: overlay.id,
|
|
347
|
-
description: overlay.description,
|
|
348
|
-
checked: preselected.includes(overlay.id),
|
|
349
|
-
});
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
return choices;
|
|
354
|
-
}
|
|
355
|
-
/**
|
|
356
|
-
* Interactive questionnaire with modern checkbox selections
|
|
357
|
-
*/
|
|
358
|
-
async function runQuestionnaire(manifest, manifestDir, cliPresetId, cliPresetChoices, defaultAnswers) {
|
|
359
|
-
const config = loadOverlaysConfigWrapper();
|
|
360
|
-
// Pretty banner
|
|
361
|
-
console.log('\n' +
|
|
362
|
-
boxen(chalk.bold.cyan('Container Superposition') +
|
|
363
|
-
'\n' +
|
|
364
|
-
chalk.gray(manifest ? 'DevContainer Regenerator' : 'DevContainer Initializer'), {
|
|
365
|
-
padding: 1,
|
|
366
|
-
margin: 1,
|
|
367
|
-
borderStyle: 'round',
|
|
368
|
-
borderColor: 'cyan',
|
|
369
|
-
textAlignment: 'center',
|
|
370
|
-
}));
|
|
371
|
-
if (manifest) {
|
|
372
|
-
console.log(chalk.cyan('📋 Loaded from manifest:'));
|
|
373
|
-
console.log(chalk.dim(` Template: ${manifest.baseTemplate}`));
|
|
374
|
-
console.log(chalk.dim(` Overlays: ${manifest.overlays.join(', ')}`));
|
|
375
|
-
if (manifest.preset) {
|
|
376
|
-
console.log(chalk.dim(` Preset: ${manifest.preset}`));
|
|
377
|
-
}
|
|
378
|
-
if (manifest.portOffset) {
|
|
379
|
-
console.log(chalk.dim(` Port offset: ${manifest.portOffset}`));
|
|
380
|
-
}
|
|
381
|
-
console.log();
|
|
382
|
-
}
|
|
383
|
-
console.log(chalk.dim('Compose your ideal devcontainer from modular overlays.'));
|
|
384
|
-
console.log(chalk.dim('Use ') +
|
|
385
|
-
chalk.cyan('space') +
|
|
386
|
-
chalk.dim(' to select, ') +
|
|
387
|
-
chalk.cyan('enter') +
|
|
388
|
-
chalk.dim(' to confirm.\n'));
|
|
389
|
-
try {
|
|
390
|
-
// Question 0: Optional preset selection
|
|
391
|
-
let usePreset = false;
|
|
392
|
-
// CLI preset takes precedence over manifest preset
|
|
393
|
-
let selectedPresetId = cliPresetId || manifest?.preset;
|
|
394
|
-
// CLI preset choices merged with manifest choices (CLI takes precedence)
|
|
395
|
-
let presetChoices = {
|
|
396
|
-
...(manifest?.presetChoices || {}),
|
|
397
|
-
...(cliPresetChoices || {}),
|
|
398
|
-
};
|
|
399
|
-
let presetGlueConfig;
|
|
400
|
-
const presetOverlaysFiltered = config.overlays.filter((o) => o.category === 'preset');
|
|
401
|
-
let presetOverlays = [];
|
|
402
|
-
if (presetOverlaysFiltered.length > 0) {
|
|
403
|
-
// If a preset was pre-selected via CLI or manifest, skip the prompt
|
|
404
|
-
if (selectedPresetId) {
|
|
405
|
-
usePreset = true;
|
|
406
|
-
console.log(chalk.cyan(`\n📦 Using preset: ${selectedPresetId}\n`));
|
|
407
|
-
}
|
|
408
|
-
else {
|
|
409
|
-
const defaultPreset = 'custom';
|
|
410
|
-
const presetChoice = (await select({
|
|
411
|
-
message: 'Start from a preset or build custom?',
|
|
412
|
-
choices: [
|
|
413
|
-
{
|
|
414
|
-
name: 'Custom (select overlays manually)',
|
|
415
|
-
value: 'custom',
|
|
416
|
-
description: 'Choose individual overlays yourself',
|
|
417
|
-
},
|
|
418
|
-
...presetOverlaysFiltered.map((p) => ({
|
|
419
|
-
name: p.name,
|
|
420
|
-
value: p.id,
|
|
421
|
-
description: p.description,
|
|
422
|
-
})),
|
|
423
|
-
],
|
|
424
|
-
default: defaultPreset,
|
|
425
|
-
}));
|
|
426
|
-
if (presetChoice !== 'custom') {
|
|
427
|
-
usePreset = true;
|
|
428
|
-
selectedPresetId = presetChoice;
|
|
429
|
-
}
|
|
430
|
-
else {
|
|
431
|
-
// User chose custom - discard any pre-provided preset choices so the
|
|
432
|
-
// manifest cannot end up with presetChoices but no preset.
|
|
433
|
-
presetChoices = {};
|
|
434
|
-
selectedPresetId = undefined;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
// Question 1: Base template
|
|
439
|
-
const stack = (await select({
|
|
440
|
-
message: 'Select base template:',
|
|
441
|
-
choices: config.base_templates.map((t) => ({
|
|
442
|
-
name: t.name,
|
|
443
|
-
value: t.id,
|
|
444
|
-
description: t.description,
|
|
445
|
-
})),
|
|
446
|
-
default: manifest?.baseTemplate || defaultAnswers?.stack,
|
|
447
|
-
}));
|
|
448
|
-
// If using preset, expand it now (pass pre-provided choices to skip those prompts)
|
|
449
|
-
if (usePreset && selectedPresetId) {
|
|
450
|
-
const expansion = await expandPreset(selectedPresetId, stack, presetChoices);
|
|
451
|
-
if (!expansion.overlays || expansion.overlays.length === 0) {
|
|
452
|
-
// Preset failed to expand (e.g., missing or invalid preset definition).
|
|
453
|
-
// Treat this as "no preset" so the manifest does not incorrectly record one.
|
|
454
|
-
console.log(chalk.yellow(`\n⚠️ Preset "${selectedPresetId}" could not be applied. Falling back to custom overlay selection.\n`));
|
|
455
|
-
usePreset = false;
|
|
456
|
-
selectedPresetId = undefined;
|
|
457
|
-
presetOverlays = [];
|
|
458
|
-
presetChoices = {};
|
|
459
|
-
presetGlueConfig = undefined;
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
presetOverlays = expansion.overlays;
|
|
463
|
-
presetChoices = expansion.choices;
|
|
464
|
-
presetGlueConfig = expansion.glueConfig;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
// Question 2: Base image selection
|
|
468
|
-
// Check if manifest has a custom image or a known base image
|
|
469
|
-
const knownBaseImageIds = config.base_images.map((img) => img.id);
|
|
470
|
-
const manifestBaseImageIsKnown = manifest?.baseImage && knownBaseImageIds.includes(manifest.baseImage);
|
|
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);
|
|
480
|
-
const baseImage = (await select({
|
|
481
|
-
message: 'Select base image:',
|
|
482
|
-
choices: config.base_images.map((img) => ({
|
|
483
|
-
name: img.name,
|
|
484
|
-
value: img.id,
|
|
485
|
-
description: img.description,
|
|
486
|
-
})),
|
|
487
|
-
default: defaultBaseImage,
|
|
488
|
-
}));
|
|
489
|
-
// Question 2a: If custom, ask for image name
|
|
490
|
-
let customImage;
|
|
491
|
-
if (baseImage === 'custom') {
|
|
492
|
-
// If manifest has a custom image, use it as default
|
|
493
|
-
const manifestCustomImage = !manifestBaseImageIsKnown && manifest?.baseImage ? manifest.baseImage : undefined;
|
|
494
|
-
const defaultCustomImage = manifestCustomImage || defaultAnswers?.customImage;
|
|
495
|
-
customImage = await input({
|
|
496
|
-
message: 'Enter custom Docker image (e.g., ubuntu:22.04):',
|
|
497
|
-
default: defaultCustomImage,
|
|
498
|
-
validate: (value) => {
|
|
499
|
-
if (!value || value.trim() === '') {
|
|
500
|
-
return 'Image name is required';
|
|
501
|
-
}
|
|
502
|
-
return true;
|
|
503
|
-
},
|
|
504
|
-
});
|
|
505
|
-
console.log(chalk.yellow('\n⚠️ Warning: Custom images may conflict with overlays.'));
|
|
506
|
-
console.log(chalk.dim(' Test thoroughly and adjust configurations as needed.\n'));
|
|
507
|
-
}
|
|
508
|
-
// Build categorized overlays with separators
|
|
509
|
-
const categoryList = [
|
|
510
|
-
{
|
|
511
|
-
name: 'Language',
|
|
512
|
-
overlays: config.overlays.filter((o) => o.category === 'language'),
|
|
513
|
-
},
|
|
514
|
-
{
|
|
515
|
-
name: 'Database',
|
|
516
|
-
overlays: config.overlays.filter((o) => o.category === 'database'),
|
|
517
|
-
},
|
|
518
|
-
{
|
|
519
|
-
name: 'Observability',
|
|
520
|
-
overlays: config.overlays.filter((o) => o.category === 'observability'),
|
|
521
|
-
},
|
|
522
|
-
{ name: 'Cloud', overlays: config.overlays.filter((o) => o.category === 'cloud') },
|
|
523
|
-
{ name: 'DevTool', overlays: config.overlays.filter((o) => o.category === 'dev') },
|
|
524
|
-
];
|
|
525
|
-
// Create a map of all overlays for dependency lookup
|
|
526
|
-
const allOverlaysMap = new Map(config.overlays.map((o) => [o.id, o]));
|
|
527
|
-
// Question 3: Categorized multi-select overlays with dependency tracking
|
|
528
|
-
let userSelection;
|
|
529
|
-
if (usePreset && presetOverlays.length > 0) {
|
|
530
|
-
// Preset mode: Ask if user wants to customize
|
|
531
|
-
console.log(chalk.cyan(`\n✓ Preset includes these overlays: ${presetOverlays.join(', ')}\n`));
|
|
532
|
-
const customizePreset = (await select({
|
|
533
|
-
message: 'Do you want to customize the overlay selection?',
|
|
534
|
-
choices: [
|
|
535
|
-
{
|
|
536
|
-
name: 'Use preset as-is',
|
|
537
|
-
value: 'no',
|
|
538
|
-
description: 'Keep the preset overlay selection',
|
|
539
|
-
},
|
|
540
|
-
{
|
|
541
|
-
name: 'Customize selection',
|
|
542
|
-
value: 'yes',
|
|
543
|
-
description: 'Add or remove overlays from the preset',
|
|
544
|
-
},
|
|
545
|
-
],
|
|
546
|
-
}));
|
|
547
|
-
if (customizePreset === 'yes') {
|
|
548
|
-
// Show overlay selection with preset overlays pre-selected
|
|
549
|
-
console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm'));
|
|
550
|
-
console.log(chalk.dim(' Preset overlays are pre-selected\n'));
|
|
551
|
-
const choices = buildOverlayChoices(config, stack, categoryList, presetOverlays);
|
|
552
|
-
userSelection = await checkbox({
|
|
553
|
-
message: 'Select overlays to include:',
|
|
554
|
-
choices,
|
|
555
|
-
pageSize: 15,
|
|
556
|
-
loop: false,
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
else {
|
|
560
|
-
// Use preset selection as-is
|
|
561
|
-
userSelection = presetOverlays;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
else if (manifest) {
|
|
565
|
-
// Manifest mode: Pre-select overlays from manifest
|
|
566
|
-
console.log(chalk.cyan(`\n✓ Manifest includes these overlays: ${manifest.overlays.join(', ')}\n`));
|
|
567
|
-
const customizeManifest = (await select({
|
|
568
|
-
message: 'Do you want to customize the overlay selection?',
|
|
569
|
-
choices: [
|
|
570
|
-
{
|
|
571
|
-
name: 'Use manifest as-is',
|
|
572
|
-
value: 'no',
|
|
573
|
-
description: 'Keep the manifest overlay selection',
|
|
574
|
-
},
|
|
575
|
-
{
|
|
576
|
-
name: 'Customize selection',
|
|
577
|
-
value: 'yes',
|
|
578
|
-
description: 'Add or remove overlays from the manifest',
|
|
579
|
-
},
|
|
580
|
-
],
|
|
581
|
-
}));
|
|
582
|
-
if (customizeManifest === 'yes') {
|
|
583
|
-
// Show overlay selection with manifest overlays pre-selected
|
|
584
|
-
console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm'));
|
|
585
|
-
console.log(chalk.dim(' Manifest overlays are pre-selected\n'));
|
|
586
|
-
// Filter out overlays that don't exist anymore
|
|
587
|
-
const existingOverlays = manifest.overlays.filter((id) => allOverlaysMap.has(id));
|
|
588
|
-
const missingOverlays = manifest.overlays.filter((id) => !allOverlaysMap.has(id));
|
|
589
|
-
if (missingOverlays.length > 0) {
|
|
590
|
-
console.log(chalk.yellow(`⚠️ Warning: Some overlays from manifest no longer exist: ${missingOverlays.join(', ')}\n`));
|
|
591
|
-
}
|
|
592
|
-
const choices = buildOverlayChoices(config, stack, categoryList, existingOverlays);
|
|
593
|
-
userSelection = await checkbox({
|
|
594
|
-
message: 'Select overlays to include:',
|
|
595
|
-
choices,
|
|
596
|
-
pageSize: 15,
|
|
597
|
-
loop: false,
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
else {
|
|
601
|
-
// Use manifest selection as-is (filtering out missing overlays)
|
|
602
|
-
const existingOverlays = manifest.overlays.filter((id) => allOverlaysMap.has(id));
|
|
603
|
-
const missingOverlays = manifest.overlays.filter((id) => !allOverlaysMap.has(id));
|
|
604
|
-
if (missingOverlays.length > 0) {
|
|
605
|
-
console.log(chalk.yellow(`⚠️ Warning: Some overlays from manifest no longer exist and will be skipped: ${missingOverlays.join(', ')}\n`));
|
|
606
|
-
}
|
|
607
|
-
userSelection = existingOverlays;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
else {
|
|
611
|
-
// Custom mode: Normal overlay selection
|
|
612
|
-
console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm\n'));
|
|
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);
|
|
622
|
-
userSelection = await checkbox({
|
|
623
|
-
message: 'Select overlays to include:',
|
|
624
|
-
choices,
|
|
625
|
-
pageSize: 15,
|
|
626
|
-
loop: false,
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
// Add all required dependencies
|
|
630
|
-
const withDependencies = new Set(userSelection);
|
|
631
|
-
const toProcess = [...userSelection];
|
|
632
|
-
while (toProcess.length > 0) {
|
|
633
|
-
const current = toProcess.pop();
|
|
634
|
-
const overlay = allOverlaysMap.get(current);
|
|
635
|
-
if (overlay?.requires) {
|
|
636
|
-
overlay.requires.forEach((req) => {
|
|
637
|
-
if (!withDependencies.has(req)) {
|
|
638
|
-
withDependencies.add(req);
|
|
639
|
-
toProcess.push(req);
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
let selectedOverlays = Array.from(withDependencies);
|
|
645
|
-
// Check for conflicts and resolve
|
|
646
|
-
let hasConflicts = true;
|
|
647
|
-
while (hasConflicts) {
|
|
648
|
-
const conflicts = new Map();
|
|
649
|
-
// Find all conflicts
|
|
650
|
-
selectedOverlays.forEach((selectedId) => {
|
|
651
|
-
const overlay = allOverlaysMap.get(selectedId);
|
|
652
|
-
if (overlay?.conflicts) {
|
|
653
|
-
overlay.conflicts.forEach((conflictId) => {
|
|
654
|
-
if (selectedOverlays.includes(conflictId)) {
|
|
655
|
-
if (!conflicts.has(selectedId)) {
|
|
656
|
-
conflicts.set(selectedId, []);
|
|
657
|
-
}
|
|
658
|
-
conflicts.get(selectedId).push(conflictId);
|
|
659
|
-
}
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
});
|
|
663
|
-
if (conflicts.size === 0) {
|
|
664
|
-
hasConflicts = false;
|
|
665
|
-
}
|
|
666
|
-
else {
|
|
667
|
-
// Show conflict resolution UI
|
|
668
|
-
console.log(chalk.yellow('\n⚠️ Conflicts detected in selection:\n'));
|
|
669
|
-
const conflictChoices = [];
|
|
670
|
-
conflicts.forEach((conflictingWith, overlayId) => {
|
|
671
|
-
const overlay = allOverlaysMap.get(overlayId);
|
|
672
|
-
const conflictNames = conflictingWith
|
|
673
|
-
.map((id) => allOverlaysMap.get(id)?.name)
|
|
674
|
-
.join(', ');
|
|
675
|
-
conflictChoices.push({
|
|
676
|
-
name: `Remove ${overlay.name}`,
|
|
677
|
-
value: overlayId,
|
|
678
|
-
description: `Conflicts with: ${conflictNames}`,
|
|
679
|
-
});
|
|
680
|
-
});
|
|
681
|
-
const toRemove = await checkbox({
|
|
682
|
-
message: 'Select overlays to remove to resolve conflicts:',
|
|
683
|
-
choices: conflictChoices,
|
|
684
|
-
pageSize: 15,
|
|
685
|
-
loop: false,
|
|
686
|
-
});
|
|
687
|
-
if (toRemove.length === 0) {
|
|
688
|
-
console.log(chalk.red('\n❌ You must remove at least one conflicting overlay'));
|
|
689
|
-
continue;
|
|
690
|
-
}
|
|
691
|
-
// Remove selected overlays
|
|
692
|
-
selectedOverlays = selectedOverlays.filter((id) => !toRemove.includes(id));
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
// Question 4: Container name
|
|
696
|
-
const containerName = await input({
|
|
697
|
-
message: 'Container/project name (optional):',
|
|
698
|
-
default: manifest?.containerName || defaultAnswers?.containerName || '',
|
|
699
|
-
});
|
|
700
|
-
// Question 5: Output path
|
|
701
|
-
// If manifest provided, default to its location; otherwise use ./.devcontainer
|
|
702
|
-
const defaultOutput = manifestDir || defaultAnswers?.outputPath || './.devcontainer';
|
|
703
|
-
const outputPath = await input({
|
|
704
|
-
message: 'Output path:',
|
|
705
|
-
default: defaultOutput,
|
|
706
|
-
});
|
|
707
|
-
// Question 6: Port offset (optional, for running multiple instances)
|
|
708
|
-
const portOffsetInput = await input({
|
|
709
|
-
message: 'Port offset (leave empty for default ports, e.g., 100 to avoid conflicts):',
|
|
710
|
-
default: manifest?.portOffset !== undefined
|
|
711
|
-
? String(manifest.portOffset)
|
|
712
|
-
: defaultAnswers?.portOffset !== undefined
|
|
713
|
-
? String(defaultAnswers.portOffset)
|
|
714
|
-
: '',
|
|
715
|
-
});
|
|
716
|
-
const portOffset = portOffsetInput ? parseInt(portOffsetInput, 10) : undefined;
|
|
717
|
-
// Parse selected overlays into categories
|
|
718
|
-
const overlayMap = new Map(config.overlays.map((o) => [o.id, o]));
|
|
719
|
-
const language = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'language');
|
|
720
|
-
const observability = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'observability');
|
|
721
|
-
const cloudTools = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'cloud');
|
|
722
|
-
const devTools = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'dev');
|
|
723
|
-
const database = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'database');
|
|
724
|
-
const playwright = selectedOverlays.includes('playwright');
|
|
725
|
-
// Check for deployment target compatibility
|
|
726
|
-
let target;
|
|
727
|
-
// Check if any incompatible overlays selected
|
|
728
|
-
const incompatibleOverlays = getIncompatibleOverlays(selectedOverlays, undefined);
|
|
729
|
-
if (incompatibleOverlays.length > 0) {
|
|
730
|
-
console.log(chalk.yellow('\n⚠️ Deployment Target Compatibility Check:\n'));
|
|
731
|
-
console.log(chalk.gray('Some selected overlays may not work in all environments.'));
|
|
732
|
-
console.log();
|
|
733
|
-
// Show incompatibilities
|
|
734
|
-
for (const { overlay, alternatives } of incompatibleOverlays) {
|
|
735
|
-
const overlayMeta = allOverlaysMap.get(overlay);
|
|
736
|
-
console.log(chalk.yellow(` • ${overlayMeta?.name || overlay}`));
|
|
737
|
-
console.log(chalk.gray(` Not compatible with: ${DEPLOYMENT_TARGETS.codespaces.name}, ${DEPLOYMENT_TARGETS.gitpod.name}`));
|
|
738
|
-
if (alternatives.length > 0) {
|
|
739
|
-
const altNames = alternatives
|
|
740
|
-
.map((id) => allOverlaysMap.get(id)?.name || id)
|
|
741
|
-
.join(', ');
|
|
742
|
-
console.log(chalk.cyan(` Alternatives: ${altNames}`));
|
|
743
|
-
}
|
|
744
|
-
console.log();
|
|
745
|
-
}
|
|
746
|
-
const targetChoice = (await select({
|
|
747
|
-
message: 'Which environment are you targeting?',
|
|
748
|
-
choices: [
|
|
749
|
-
{
|
|
750
|
-
name: '🖥️ Local Development (Docker Desktop)',
|
|
751
|
-
value: 'local',
|
|
752
|
-
description: 'Running on your local machine - supports all overlays',
|
|
753
|
-
},
|
|
754
|
-
{
|
|
755
|
-
name: '☁️ GitHub Codespaces',
|
|
756
|
-
value: 'codespaces',
|
|
757
|
-
description: 'Cloud development - may require docker-in-docker',
|
|
758
|
-
},
|
|
759
|
-
{
|
|
760
|
-
name: '🌐 Gitpod',
|
|
761
|
-
value: 'gitpod',
|
|
762
|
-
description: 'Cloud development - may require docker-in-docker',
|
|
763
|
-
},
|
|
764
|
-
{
|
|
765
|
-
name: '📦 DevPod',
|
|
766
|
-
value: 'devpod',
|
|
767
|
-
description: 'Client-only dev environments',
|
|
768
|
-
},
|
|
769
|
-
],
|
|
770
|
-
default: 'local',
|
|
771
|
-
}));
|
|
772
|
-
target = targetChoice;
|
|
773
|
-
// Show specific incompatibilities for selected target
|
|
774
|
-
if (target !== 'local') {
|
|
775
|
-
const targetIncompatible = getIncompatibleOverlays(selectedOverlays, target);
|
|
776
|
-
if (targetIncompatible.length > 0) {
|
|
777
|
-
console.log(chalk.yellow(`\n⚠️ Warning: Some overlays won't work in ${DEPLOYMENT_TARGETS[target].name}:\n`));
|
|
778
|
-
for (const { overlay, alternatives } of targetIncompatible) {
|
|
779
|
-
const overlayMeta = allOverlaysMap.get(overlay);
|
|
780
|
-
console.log(chalk.red(` ✗ ${overlayMeta?.name || overlay}`));
|
|
781
|
-
if (alternatives.length > 0) {
|
|
782
|
-
const altNames = alternatives
|
|
783
|
-
.map((id) => allOverlaysMap.get(id)?.name || id)
|
|
784
|
-
.join(', ');
|
|
785
|
-
console.log(chalk.cyan(` → Recommended: ${altNames}`));
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
console.log();
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
return {
|
|
793
|
-
stack,
|
|
794
|
-
baseImage,
|
|
795
|
-
customImage,
|
|
796
|
-
containerName: containerName || undefined,
|
|
797
|
-
preset: selectedPresetId,
|
|
798
|
-
presetChoices: Object.keys(presetChoices).length > 0 ? presetChoices : undefined,
|
|
799
|
-
presetGlueConfig,
|
|
800
|
-
language,
|
|
801
|
-
needsDocker: stack === 'compose', // Compose template includes docker-outside-of-docker
|
|
802
|
-
database,
|
|
803
|
-
playwright,
|
|
804
|
-
cloudTools,
|
|
805
|
-
devTools,
|
|
806
|
-
observability,
|
|
807
|
-
outputPath,
|
|
808
|
-
portOffset,
|
|
809
|
-
target: target ?? defaultAnswers?.target,
|
|
810
|
-
minimal: defaultAnswers?.minimal,
|
|
811
|
-
editor: defaultAnswers?.editor,
|
|
812
|
-
customizations: defaultAnswers?.customizations,
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
catch (error) {
|
|
816
|
-
if (error.name === 'ExitPromptError') {
|
|
817
|
-
console.log('\n' + chalk.yellow('Cancelled by user'));
|
|
818
|
-
process.exit(0);
|
|
819
|
-
}
|
|
820
|
-
throw error;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
/**
|
|
824
|
-
* Build partial answers from manifest
|
|
825
|
-
* Note: Categories are only used for UI/questionnaire grouping.
|
|
826
|
-
* The composer works with overlay IDs regardless of category.
|
|
827
|
-
*/
|
|
828
|
-
function buildAnswersFromManifest(manifest, manifestDir) {
|
|
829
|
-
const config = loadOverlaysConfigWrapper();
|
|
830
|
-
// Helper to categorize overlays by type (for QuestionnaireAnswers structure)
|
|
831
|
-
const categorizeOverlays = (overlayIds) => {
|
|
832
|
-
const language = [];
|
|
833
|
-
const database = [];
|
|
834
|
-
const observability = [];
|
|
835
|
-
const cloudTools = [];
|
|
836
|
-
const devTools = [];
|
|
837
|
-
// Build lookup map from unified overlays array
|
|
838
|
-
const overlayMap = new Map(config.overlays.map((o) => [o.id, o]));
|
|
839
|
-
// Categorize based on overlay metadata
|
|
840
|
-
for (const id of overlayIds) {
|
|
841
|
-
const overlay = overlayMap.get(id);
|
|
842
|
-
if (!overlay)
|
|
843
|
-
continue;
|
|
844
|
-
switch (overlay.category) {
|
|
845
|
-
case 'language':
|
|
846
|
-
language.push(id);
|
|
847
|
-
break;
|
|
848
|
-
case 'database':
|
|
849
|
-
database.push(id);
|
|
850
|
-
break;
|
|
851
|
-
case 'observability':
|
|
852
|
-
observability.push(id);
|
|
853
|
-
break;
|
|
854
|
-
case 'cloud':
|
|
855
|
-
cloudTools.push(id);
|
|
856
|
-
break;
|
|
857
|
-
case 'dev':
|
|
858
|
-
devTools.push(id);
|
|
859
|
-
break;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
return { language, database, observability, cloudTools, devTools };
|
|
863
|
-
};
|
|
864
|
-
const categories = categorizeOverlays(manifest.overlays);
|
|
865
|
-
// Output path is always the directory containing the manifest
|
|
866
|
-
const outputPath = manifestDir || './.devcontainer';
|
|
867
|
-
// Handle baseImage - check if it's a known ID or a custom image string
|
|
868
|
-
const knownBaseImageIds = ['bookworm', 'trixie', 'alpine', 'ubuntu', 'custom'];
|
|
869
|
-
const isKnownBaseImage = knownBaseImageIds.includes(manifest.baseImage);
|
|
870
|
-
return {
|
|
871
|
-
stack: manifest.baseTemplate,
|
|
872
|
-
baseImage: isKnownBaseImage ? manifest.baseImage : 'custom',
|
|
873
|
-
customImage: isKnownBaseImage ? undefined : manifest.baseImage,
|
|
874
|
-
containerName: manifest.containerName,
|
|
875
|
-
preset: manifest.preset,
|
|
876
|
-
presetChoices: manifest.presetChoices,
|
|
877
|
-
...categories,
|
|
878
|
-
needsDocker: manifest.baseTemplate === 'compose',
|
|
879
|
-
playwright: categories.devTools.includes('playwright'),
|
|
880
|
-
outputPath,
|
|
881
|
-
portOffset: manifest.portOffset,
|
|
882
|
-
};
|
|
883
|
-
}
|
|
884
|
-
/**
|
|
885
|
-
* Build partial answers from CLI arguments
|
|
886
|
-
*/
|
|
887
|
-
function buildAnswersFromCliArgs(config) {
|
|
888
|
-
const answers = {};
|
|
889
|
-
if (config.stack) {
|
|
890
|
-
answers.stack = config.stack;
|
|
891
|
-
answers.needsDocker = config.stack === 'compose';
|
|
892
|
-
}
|
|
893
|
-
if (config.baseImage)
|
|
894
|
-
answers.baseImage = config.baseImage;
|
|
895
|
-
if (config.containerName)
|
|
896
|
-
answers.containerName = config.containerName;
|
|
897
|
-
if (config.language)
|
|
898
|
-
answers.language = config.language;
|
|
899
|
-
if (config.database)
|
|
900
|
-
answers.database = config.database;
|
|
901
|
-
if (config.playwright !== undefined)
|
|
902
|
-
answers.playwright = config.playwright;
|
|
903
|
-
if (config.observability)
|
|
904
|
-
answers.observability = config.observability;
|
|
905
|
-
if (config.cloudTools)
|
|
906
|
-
answers.cloudTools = config.cloudTools;
|
|
907
|
-
if (config.devTools)
|
|
908
|
-
answers.devTools = config.devTools;
|
|
909
|
-
if (config.portOffset !== undefined)
|
|
910
|
-
answers.portOffset = config.portOffset;
|
|
911
|
-
if (config.outputPath)
|
|
912
|
-
answers.outputPath = config.outputPath;
|
|
913
|
-
if (config.preset)
|
|
914
|
-
answers.preset = config.preset;
|
|
915
|
-
if (config.presetChoices)
|
|
916
|
-
answers.presetChoices = config.presetChoices;
|
|
917
|
-
if (config.target)
|
|
918
|
-
answers.target = config.target;
|
|
919
|
-
if (config.minimal !== undefined)
|
|
920
|
-
answers.minimal = config.minimal;
|
|
921
|
-
if (config.editor)
|
|
922
|
-
answers.editor = config.editor;
|
|
923
|
-
return answers;
|
|
924
|
-
}
|
|
925
|
-
/**
|
|
926
|
-
* Merge multiple partial answers with precedence: cli > interactive > manifest > defaults
|
|
927
|
-
*/
|
|
928
|
-
function mergeAnswers(...partials) {
|
|
929
|
-
const merged = {
|
|
930
|
-
language: [],
|
|
931
|
-
database: [],
|
|
932
|
-
cloudTools: [],
|
|
933
|
-
devTools: [],
|
|
934
|
-
observability: [],
|
|
935
|
-
playwright: false,
|
|
936
|
-
outputPath: './.devcontainer',
|
|
937
|
-
};
|
|
938
|
-
// Merge in order (later overrides earlier)
|
|
939
|
-
for (const partial of partials) {
|
|
940
|
-
if (!partial)
|
|
941
|
-
continue;
|
|
942
|
-
Object.keys(partial).forEach((key) => {
|
|
943
|
-
const value = partial[key];
|
|
944
|
-
if (value !== undefined && value !== null) {
|
|
945
|
-
// For arrays, prefer non-empty values
|
|
946
|
-
if (Array.isArray(value)) {
|
|
947
|
-
if (value.length > 0) {
|
|
948
|
-
merged[key] = value;
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
else {
|
|
952
|
-
merged[key] = value;
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
// Ensure required fields have defaults
|
|
958
|
-
if (!merged.stack)
|
|
959
|
-
merged.stack = 'plain';
|
|
960
|
-
if (!merged.baseImage)
|
|
961
|
-
merged.baseImage = 'bookworm';
|
|
962
|
-
if (!merged.needsDocker && merged.stack) {
|
|
963
|
-
merged.needsDocker = merged.stack === 'compose';
|
|
964
|
-
}
|
|
965
|
-
return merged;
|
|
966
|
-
}
|
|
967
|
-
/**
|
|
968
|
-
* Parse CLI arguments
|
|
969
|
-
*/
|
|
970
|
-
async function parseCliArgs() {
|
|
971
|
-
const program = new Command();
|
|
972
|
-
// Store init options for access after parsing
|
|
973
|
-
let initOptions = null;
|
|
974
|
-
program
|
|
975
|
-
.name('container-superposition')
|
|
976
|
-
.description('Composable devcontainer scaffolds')
|
|
977
|
-
.version(getToolVersion());
|
|
978
|
-
// Init command (default)
|
|
979
|
-
program
|
|
980
|
-
.command('init', { isDefault: true })
|
|
981
|
-
.description('Initialize a new devcontainer configuration')
|
|
982
|
-
.option('--from-project', 'Load configuration from the repository project file')
|
|
983
|
-
.option('--project-root <path>', 'Run project-file and manifest discovery relative to a different repository root')
|
|
984
|
-
.option('--from-manifest <path>', 'Load configuration from existing superposition.json manifest')
|
|
985
|
-
.option('--no-interactive', 'Use persisted input values directly without questionnaire')
|
|
986
|
-
.option('--backup', 'Force or suppress backup; default is --no-backup inside a git repo, --backup outside')
|
|
987
|
-
.option('--backup-dir <path>', 'Custom backup directory location')
|
|
988
|
-
.option('--stack <type>', 'Base template: plain, compose')
|
|
989
|
-
.option('--language <list>', 'Comma-separated language overlays: dotnet, nodejs, python, mkdocs, java, go, rust, bun, powershell')
|
|
990
|
-
.option('--database <list>', 'Comma-separated database overlays: postgres, redis, mongodb, mysql, sqlserver, sqlite, minio, rabbitmq, redpanda, nats')
|
|
991
|
-
.option('--observability <list>', 'Comma-separated: otel-collector, jaeger, prometheus, grafana, loki')
|
|
992
|
-
.option('--playwright', 'Include Playwright browser automation')
|
|
993
|
-
.option('--cloud-tools <list>', 'Comma-separated: aws-cli, azure-cli, gcloud, kubectl-helm, terraform, pulumi')
|
|
994
|
-
.option('--dev-tools <list>', 'Comma-separated: docker-in-docker, docker-sock, playwright, codex, git-helpers, pre-commit, commitlint, just, direnv, modern-cli-tools, ngrok')
|
|
995
|
-
.option('--port-offset <number>', 'Add offset to all exposed ports (e.g., 100 makes Grafana 3100 instead of 3000)')
|
|
996
|
-
.option('--target <environment>', 'Deployment target: local, codespaces, gitpod, devpod (optimizes for environment)', 'local')
|
|
997
|
-
.option('--minimal', 'Minimal mode - exclude optional/nice-to-have features and extensions')
|
|
998
|
-
.option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
|
|
999
|
-
.option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
|
|
1000
|
-
.option('--write-manifest-only', 'Generate only superposition.json manifest without creating .devcontainer/ files')
|
|
1001
|
-
.option('--preset <id>', 'Start from a preset (e.g., web-api, microservice)')
|
|
1002
|
-
.option('--preset-param <value>', 'Set a preset parameter value (format: key=value, can be repeated)', (value, previous) => previous.concat([value]), [])
|
|
1003
|
-
.action((options, command) => {
|
|
1004
|
-
// Store options for main() to process
|
|
1005
|
-
initOptions = {
|
|
1006
|
-
...options,
|
|
1007
|
-
commandName: 'init',
|
|
1008
|
-
_targetSource: command.getOptionValueSource('target'),
|
|
1009
|
-
_editorSource: command.getOptionValueSource('editor'),
|
|
1010
|
-
};
|
|
1011
|
-
});
|
|
1012
|
-
// Regen command
|
|
1013
|
-
program
|
|
1014
|
-
.command('regen')
|
|
1015
|
-
.description('Regenerate devcontainer from a project file or existing superposition.json manifest')
|
|
1016
|
-
.option('--from-project', 'Load configuration from the repository project file')
|
|
1017
|
-
.option('--project-root <path>', 'Run project-file and manifest discovery relative to a different repository root')
|
|
1018
|
-
.option('--from-manifest <path>', 'Load configuration from existing superposition.json manifest')
|
|
1019
|
-
.option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
|
|
1020
|
-
.option('--backup', 'Force or suppress backup; default is --no-backup inside a git repo, --backup outside')
|
|
1021
|
-
.option('--backup-dir <path>', 'Custom backup directory location')
|
|
1022
|
-
.option('--minimal', 'Minimal mode - exclude optional/nice-to-have features and extensions')
|
|
1023
|
-
.option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
|
|
1024
|
-
.action((options, command) => {
|
|
1025
|
-
initOptions = {
|
|
1026
|
-
...options,
|
|
1027
|
-
commandName: 'regen',
|
|
1028
|
-
interactive: false,
|
|
1029
|
-
_editorSource: command.getOptionValueSource('editor'),
|
|
1030
|
-
};
|
|
1031
|
-
});
|
|
1032
|
-
// List command
|
|
1033
|
-
program
|
|
1034
|
-
.command('list')
|
|
1035
|
-
.description('List available overlays and presets')
|
|
1036
|
-
.option('--category <type>', 'Filter by category: language, database, observability, cloud, dev, preset')
|
|
1037
|
-
.option('--tags <list>', 'Filter by tags (comma-separated)')
|
|
1038
|
-
.option('--supports <stack>', 'Filter by stack support: plain, compose')
|
|
1039
|
-
.option('--json', 'Output as JSON for scripting')
|
|
1040
|
-
.action(async (options) => {
|
|
1041
|
-
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
1042
|
-
await listCommand(overlaysConfig, options);
|
|
1043
|
-
process.exit(0);
|
|
1044
|
-
});
|
|
1045
|
-
// Explain command
|
|
1046
|
-
program
|
|
1047
|
-
.command('explain <overlay>')
|
|
1048
|
-
.description('Show detailed information about an overlay')
|
|
1049
|
-
.option('--json', 'Output as JSON for scripting')
|
|
1050
|
-
.action(async (overlayId, options) => {
|
|
1051
|
-
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
1052
|
-
await explainCommand(overlaysConfig, OVERLAYS_DIR, overlayId, options);
|
|
1053
|
-
process.exit(0);
|
|
1054
|
-
});
|
|
1055
|
-
// Plan command
|
|
1056
|
-
program
|
|
1057
|
-
.command('plan')
|
|
1058
|
-
.description('Preview what will be generated before creating devcontainer')
|
|
1059
|
-
.option('--stack <type>', 'Base template: plain, compose')
|
|
1060
|
-
.option('--overlays <list>', 'Comma-separated list of overlay IDs')
|
|
1061
|
-
.option('--from-manifest <path>', 'Load stack and overlays from an existing superposition.json manifest')
|
|
1062
|
-
.option('--port-offset <number>', 'Add offset to all exposed ports', (val) => parseInt(val, 10), 0)
|
|
1063
|
-
.option('-o, --output <path>', 'Compare against existing config at this path (default: ./.devcontainer)')
|
|
1064
|
-
.option('--diff', 'Compare planned output vs existing configuration')
|
|
1065
|
-
.option('--diff-format <format>', 'Diff output format: color (default), json', 'color')
|
|
1066
|
-
.option('--diff-context <lines>', 'Context lines in diff output', (val) => parseInt(val, 10), 3)
|
|
1067
|
-
.option('--verbose', 'Explain why each overlay was included in the resolved plan')
|
|
1068
|
-
.option('--json', 'Output as JSON for scripting')
|
|
1069
|
-
.action(async (options) => {
|
|
1070
|
-
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
1071
|
-
await planCommand(overlaysConfig, OVERLAYS_DIR, options);
|
|
1072
|
-
process.exit(0);
|
|
1073
|
-
});
|
|
1074
|
-
// Doctor command
|
|
1075
|
-
program
|
|
1076
|
-
.command('doctor')
|
|
1077
|
-
.description('Check environment and validate configuration')
|
|
1078
|
-
.option('-o, --output <path>', 'Devcontainer path to validate (default: ./.devcontainer)')
|
|
1079
|
-
.option('--fix', 'Apply automatic fixes where possible')
|
|
1080
|
-
.option('--json', 'Output as JSON for scripting')
|
|
1081
|
-
.action(async (options) => {
|
|
1082
|
-
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
1083
|
-
await doctorCommand(overlaysConfig, OVERLAYS_DIR, options);
|
|
1084
|
-
});
|
|
1085
|
-
// Adopt command
|
|
1086
|
-
program
|
|
1087
|
-
.command('adopt')
|
|
1088
|
-
.description('Analyse an existing .devcontainer/ and suggest an equivalent overlay-based configuration')
|
|
1089
|
-
.option('-d, --dir <path>', 'Path to the existing .devcontainer directory (default: ./.devcontainer)')
|
|
1090
|
-
.option('--dry-run', 'Print analysis and suggested command only; no files written')
|
|
1091
|
-
.option('--force', 'Overwrite existing superposition.json if present')
|
|
1092
|
-
.option('--backup', 'Force backup creation even when inside a git repo (default: backup only outside git repos)')
|
|
1093
|
-
.option('--no-backup', 'Disable backup creation even when it would normally be performed')
|
|
1094
|
-
.option('--backup-dir <path>', 'Custom backup directory location')
|
|
1095
|
-
.option('--project-file', 'Also write a repository-root project config (.superposition.yml or existing project file)')
|
|
1096
|
-
.option('--json', 'Output as JSON for scripting')
|
|
1097
|
-
.action(async (options) => {
|
|
1098
|
-
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
1099
|
-
await adoptCommand(overlaysConfig, OVERLAYS_DIR, options);
|
|
1100
|
-
process.exit(0);
|
|
1101
|
-
});
|
|
1102
|
-
// Hash command
|
|
1103
|
-
program
|
|
1104
|
-
.command('hash')
|
|
1105
|
-
.description('Compute a deterministic fingerprint for a given configuration')
|
|
1106
|
-
.option('--stack <type>', 'Base template: plain, compose')
|
|
1107
|
-
.option('--overlays <list>', 'Comma-separated list of overlay IDs')
|
|
1108
|
-
.option('--preset <id>', 'Preset ID (optional)')
|
|
1109
|
-
.option('--base <image>', 'Base image / distro variant (e.g. bookworm, alpine)')
|
|
1110
|
-
.option('--manifest <path>', 'Path to superposition.json manifest')
|
|
1111
|
-
.option('-o, --output <path>', 'Directory to write hash file (used with --write)')
|
|
1112
|
-
.option('--write', 'Write hash to .devcontainer/superposition.hash')
|
|
1113
|
-
.option('--json', 'Output as JSON for scripting')
|
|
1114
|
-
.action(async (options) => {
|
|
1115
|
-
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
1116
|
-
await hashCommand(overlaysConfig, OVERLAYS_DIR, options);
|
|
1117
|
-
process.exit(0);
|
|
1118
|
-
});
|
|
1119
|
-
await program.parseAsync(process.argv);
|
|
1120
|
-
// If init or regen command was run, return the options
|
|
1121
|
-
if (!initOptions) {
|
|
1122
|
-
// No init/regen command run (list or doctor ran instead)
|
|
1123
|
-
return null;
|
|
1124
|
-
}
|
|
1125
|
-
// If no options provided to init, return null to trigger interactive mode
|
|
1126
|
-
if (Object.keys(initOptions).length === 0) {
|
|
1127
|
-
return null;
|
|
1128
|
-
}
|
|
1129
|
-
const hasSourceFlags = Number(Boolean(initOptions.fromProject)) + Number(Boolean(initOptions.fromManifest));
|
|
1130
|
-
if (hasSourceFlags > 1) {
|
|
1131
|
-
console.error(chalk.red('✗ Error: --from-project and --from-manifest cannot be used together'));
|
|
1132
|
-
process.exit(1);
|
|
1133
|
-
}
|
|
1134
|
-
const sourceSelectionConflicts = [
|
|
1135
|
-
'stack',
|
|
1136
|
-
'language',
|
|
1137
|
-
'database',
|
|
1138
|
-
'observability',
|
|
1139
|
-
'playwright',
|
|
1140
|
-
'cloudTools',
|
|
1141
|
-
'devTools',
|
|
1142
|
-
'portOffset',
|
|
1143
|
-
'preset',
|
|
1144
|
-
];
|
|
1145
|
-
const hasPresetParams = Array.isArray(initOptions.presetParam) && initOptions.presetParam.length > 0;
|
|
1146
|
-
const conflictingSelectionFlags = sourceSelectionConflicts.filter((key) => initOptions[key] !== undefined && initOptions[key] !== false);
|
|
1147
|
-
if ((initOptions.fromProject || initOptions.fromManifest) &&
|
|
1148
|
-
(conflictingSelectionFlags.length > 0 || hasPresetParams)) {
|
|
1149
|
-
const conflicts = [...conflictingSelectionFlags.map((key) => `--${key}`)];
|
|
1150
|
-
if (hasPresetParams) {
|
|
1151
|
-
conflicts.push('--preset-param');
|
|
1152
|
-
}
|
|
1153
|
-
console.error(chalk.red(`✗ Error: Persisted input sources cannot be combined with clean-generation selection flags: ${conflicts.join(', ')}`));
|
|
1154
|
-
console.error(chalk.dim(' Choose either a persisted input source (--from-project or --from-manifest) or direct selection flags for that run.'));
|
|
1155
|
-
process.exit(1);
|
|
1156
|
-
}
|
|
1157
|
-
const config = {};
|
|
1158
|
-
if (initOptions.stack)
|
|
1159
|
-
config.stack = initOptions.stack;
|
|
1160
|
-
if (initOptions.language) {
|
|
1161
|
-
config.language = initOptions.language
|
|
1162
|
-
.split(',')
|
|
1163
|
-
.map((l) => l.trim());
|
|
1164
|
-
}
|
|
1165
|
-
if (initOptions.database) {
|
|
1166
|
-
config.database = initOptions.database
|
|
1167
|
-
.split(',')
|
|
1168
|
-
.map((d) => d.trim());
|
|
1169
|
-
}
|
|
1170
|
-
if (initOptions.observability) {
|
|
1171
|
-
config.observability = initOptions.observability
|
|
1172
|
-
.split(',')
|
|
1173
|
-
.map((t) => t.trim());
|
|
1174
|
-
}
|
|
1175
|
-
if (initOptions.playwright)
|
|
1176
|
-
config.playwright = true;
|
|
1177
|
-
if (initOptions.cloudTools) {
|
|
1178
|
-
config.cloudTools = initOptions.cloudTools
|
|
1179
|
-
.split(',')
|
|
1180
|
-
.map((t) => t.trim());
|
|
1181
|
-
}
|
|
1182
|
-
if (initOptions.devTools) {
|
|
1183
|
-
config.devTools = initOptions.devTools.split(',').map((t) => t.trim());
|
|
1184
|
-
}
|
|
1185
|
-
if (initOptions.portOffset) {
|
|
1186
|
-
config.portOffset = parseInt(initOptions.portOffset, 10);
|
|
1187
|
-
}
|
|
1188
|
-
if (initOptions.target && initOptions._targetSource !== 'default') {
|
|
1189
|
-
config.target = initOptions.target;
|
|
1190
|
-
}
|
|
1191
|
-
if (initOptions.minimal) {
|
|
1192
|
-
config.minimal = true;
|
|
1193
|
-
}
|
|
1194
|
-
if (initOptions.editor && initOptions._editorSource !== 'default') {
|
|
1195
|
-
const editorLower = initOptions.editor.toLowerCase();
|
|
1196
|
-
if (['vscode', 'jetbrains', 'none'].includes(editorLower)) {
|
|
1197
|
-
config.editor = editorLower;
|
|
1198
|
-
}
|
|
1199
|
-
else {
|
|
1200
|
-
console.warn(chalk.yellow(`⚠️ Invalid editor profile: ${initOptions.editor}, using default (vscode)`));
|
|
1201
|
-
config.editor = 'vscode';
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
if (initOptions.output)
|
|
1205
|
-
config.outputPath = initOptions.output;
|
|
1206
|
-
// Handle --preset flag
|
|
1207
|
-
if (initOptions.preset) {
|
|
1208
|
-
config.preset = initOptions.preset;
|
|
1209
|
-
}
|
|
1210
|
-
// Handle --preset-param flags (can be repeated)
|
|
1211
|
-
if (initOptions.presetParam && initOptions.presetParam.length > 0) {
|
|
1212
|
-
if (!initOptions.preset) {
|
|
1213
|
-
console.warn(chalk.yellow('⚠️ Ignoring --preset-param because no --preset was provided. ' +
|
|
1214
|
-
'Preset parameters only apply when a preset is selected (e.g., --preset web-api --preset-param broker=nats).'));
|
|
1215
|
-
}
|
|
1216
|
-
else {
|
|
1217
|
-
const presetChoices = {};
|
|
1218
|
-
for (const param of initOptions.presetParam) {
|
|
1219
|
-
const eqIdx = param.indexOf('=');
|
|
1220
|
-
if (eqIdx > 0) {
|
|
1221
|
-
const key = param.slice(0, eqIdx).trim();
|
|
1222
|
-
const value = param.slice(eqIdx + 1).trim();
|
|
1223
|
-
if (key) {
|
|
1224
|
-
presetChoices[key] = value;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
else {
|
|
1228
|
-
console.warn(chalk.yellow(`⚠️ Invalid --preset-param format: "${param}". Expected "key=value" (e.g., --preset-param broker=nats).`));
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
if (Object.keys(presetChoices).length > 0) {
|
|
1232
|
-
config.presetChoices = presetChoices;
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
return {
|
|
1237
|
-
commandName: initOptions.commandName,
|
|
1238
|
-
config,
|
|
1239
|
-
manifestPath: initOptions.fromManifest,
|
|
1240
|
-
fromProject: initOptions.fromProject === true,
|
|
1241
|
-
projectRoot: initOptions.projectRoot,
|
|
1242
|
-
backupOverride: initOptions.backup, // undefined = auto-detect; true = --backup; false = --no-backup
|
|
1243
|
-
backupDir: initOptions.backupDir,
|
|
1244
|
-
noInteractive: initOptions.interactive === false, // Commander creates options.interactive = false for --no-interactive
|
|
1245
|
-
writeManifestOnly: initOptions.writeManifestOnly === true,
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
async function main() {
|
|
1249
|
-
try {
|
|
1250
|
-
const cliArgs = await parseCliArgs();
|
|
1251
|
-
const initialCwd = process.cwd();
|
|
1252
|
-
if (cliArgs?.projectRoot) {
|
|
1253
|
-
const resolvedProjectRoot = path.resolve(initialCwd, cliArgs.projectRoot);
|
|
1254
|
-
if (!fs.existsSync(resolvedProjectRoot)) {
|
|
1255
|
-
console.error(chalk.red(`✗ Project root not found: ${resolvedProjectRoot}`));
|
|
1256
|
-
process.exit(1);
|
|
1257
|
-
}
|
|
1258
|
-
if (!fs.statSync(resolvedProjectRoot).isDirectory()) {
|
|
1259
|
-
console.error(chalk.red(`✗ Project root is not a directory: ${resolvedProjectRoot}`));
|
|
1260
|
-
process.exit(1);
|
|
1261
|
-
}
|
|
1262
|
-
process.chdir(resolvedProjectRoot);
|
|
1263
|
-
}
|
|
1264
|
-
let projectConfig = undefined;
|
|
1265
|
-
let projectConfigAnswers;
|
|
1266
|
-
if (!cliArgs?.manifestPath) {
|
|
1267
|
-
projectConfig =
|
|
1268
|
-
loadProjectConfig(loadOverlaysConfigWrapper(), process.cwd()) ?? undefined;
|
|
1269
|
-
if (projectConfig) {
|
|
1270
|
-
projectConfigAnswers = applyPresetSelections(buildAnswersFromProjectConfig(projectConfig.selection));
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
let manifest;
|
|
1274
|
-
let manifestDir;
|
|
1275
|
-
let backupDir;
|
|
1276
|
-
let useManifestOnly = false;
|
|
1277
|
-
let useProjectOnly = false;
|
|
1278
|
-
if (cliArgs?.commandName === 'regen' && !cliArgs.manifestPath && !cliArgs.fromProject) {
|
|
1279
|
-
if (projectConfigAnswers) {
|
|
1280
|
-
useProjectOnly = true;
|
|
1281
|
-
}
|
|
1282
|
-
else {
|
|
1283
|
-
const discoveredManifestPath = findDefaultRegenManifest(cliArgs?.config?.outputPath || './.devcontainer');
|
|
1284
|
-
if (!discoveredManifestPath) {
|
|
1285
|
-
console.error(chalk.red('✗ Error: No project file or manifest found'));
|
|
1286
|
-
console.error(chalk.gray(' Looked for .superposition.yml or superposition.yml in the repository root, and superposition.json in common manifest locations.'));
|
|
1287
|
-
process.exit(1);
|
|
1288
|
-
}
|
|
1289
|
-
cliArgs.manifestPath = discoveredManifestPath;
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
// Handle manifest loading
|
|
1293
|
-
if (cliArgs?.manifestPath) {
|
|
1294
|
-
const manifestPath = findManifestFile(cliArgs.manifestPath);
|
|
1295
|
-
if (!manifestPath) {
|
|
1296
|
-
console.error(chalk.red('✗ Could not find manifest file'));
|
|
1297
|
-
console.error(chalk.red(` Searched for: ${cliArgs.manifestPath}`));
|
|
1298
|
-
process.exit(1);
|
|
1299
|
-
}
|
|
1300
|
-
manifestDir = path.dirname(manifestPath);
|
|
1301
|
-
const loadedManifest = loadManifest(manifestPath);
|
|
1302
|
-
if (!loadedManifest) {
|
|
1303
|
-
process.exit(1);
|
|
1304
|
-
}
|
|
1305
|
-
manifest = loadedManifest;
|
|
1306
|
-
// Check for interaction options
|
|
1307
|
-
if (cliArgs.backupDir) {
|
|
1308
|
-
backupDir = cliArgs.backupDir;
|
|
1309
|
-
}
|
|
1310
|
-
if (cliArgs.noInteractive) {
|
|
1311
|
-
useManifestOnly = true;
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
if (cliArgs?.fromProject) {
|
|
1315
|
-
if (!projectConfigAnswers || !projectConfig) {
|
|
1316
|
-
console.error(chalk.red('✗ Could not find project file'));
|
|
1317
|
-
console.error(chalk.red(' Searched for: .superposition.yml, superposition.yml'));
|
|
1318
|
-
process.exit(1);
|
|
1319
|
-
}
|
|
1320
|
-
useProjectOnly = cliArgs.noInteractive || cliArgs.commandName === 'regen';
|
|
1321
|
-
}
|
|
1322
|
-
// Validate --no-interactive requires a persisted input source
|
|
1323
|
-
if (cliArgs?.noInteractive && !cliArgs?.manifestPath && !projectConfigAnswers) {
|
|
1324
|
-
console.error(chalk.red('✗ Error: --no-interactive requires persisted input'));
|
|
1325
|
-
console.error(chalk.dim(' Use --from-project, --from-manifest <path>, or run from a repository with .superposition.yml or superposition.yml'));
|
|
1326
|
-
process.exit(1);
|
|
1327
|
-
}
|
|
1328
|
-
// Determine whether to create a backup:
|
|
1329
|
-
// --backup → always backup
|
|
1330
|
-
// --no-backup → never backup
|
|
1331
|
-
// (neither) → backup only when NOT inside a git repository
|
|
1332
|
-
// (git already tracks history, so backups are redundant)
|
|
1333
|
-
const resolvedOutputPath = cliArgs?.config?.outputPath ||
|
|
1334
|
-
projectConfigAnswers?.outputPath ||
|
|
1335
|
-
manifestDir ||
|
|
1336
|
-
'./.devcontainer';
|
|
1337
|
-
const backupCheckPath = path.resolve(resolvedOutputPath);
|
|
1338
|
-
const inGitRepo = isInsideGitRepo(backupCheckPath);
|
|
1339
|
-
let shouldBackup;
|
|
1340
|
-
if (cliArgs?.backupOverride === true) {
|
|
1341
|
-
shouldBackup = true;
|
|
1342
|
-
}
|
|
1343
|
-
else if (cliArgs?.backupOverride === false) {
|
|
1344
|
-
shouldBackup = false;
|
|
1345
|
-
}
|
|
1346
|
-
else {
|
|
1347
|
-
// Auto-detect based on git presence
|
|
1348
|
-
shouldBackup = !inGitRepo;
|
|
1349
|
-
if (!shouldBackup) {
|
|
1350
|
-
console.log(chalk.dim('ℹ Skipping backup — git repo detected (use --backup to force one)\n'));
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
const isReplayMode = cliArgs?.commandName === 'regen' || useManifestOnly || useProjectOnly;
|
|
1354
|
-
// Create backup if needed
|
|
1355
|
-
let actualBackupPath;
|
|
1356
|
-
if (shouldBackup && isReplayMode) {
|
|
1357
|
-
const outputPath = resolvedOutputPath;
|
|
1358
|
-
const backupPath = await createBackup(outputPath, backupDir);
|
|
1359
|
-
if (backupPath) {
|
|
1360
|
-
actualBackupPath = backupPath;
|
|
1361
|
-
console.log(chalk.green(`✓ Backup created: ${backupPath}\n`));
|
|
1362
|
-
ensureBackupPatternsInGitignore(outputPath);
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
// Build answers based on mode
|
|
1366
|
-
let answers;
|
|
1367
|
-
// Check if there are CLI overrides beyond just output path and preset flags
|
|
1368
|
-
// Preset/presetChoices alone don't constitute "CLI overrides" that bypass interactive mode
|
|
1369
|
-
const hasCliOverrides = cliArgs &&
|
|
1370
|
-
Object.keys(cliArgs.config).some((key) => key !== 'outputPath' &&
|
|
1371
|
-
key !== 'preset' &&
|
|
1372
|
-
key !== 'presetChoices' &&
|
|
1373
|
-
!(key === 'target' && cliArgs.config.target === 'local') &&
|
|
1374
|
-
!(key === 'editor' && cliArgs.config.editor === 'vscode') &&
|
|
1375
|
-
cliArgs.config[key] !== undefined);
|
|
1376
|
-
const hasAnyCliConfig = cliArgs &&
|
|
1377
|
-
Object.entries(cliArgs.config).some(([key, value]) => value !== undefined &&
|
|
1378
|
-
!(key === 'target' && value === 'local') &&
|
|
1379
|
-
!(key === 'editor' && value === 'vscode'));
|
|
1380
|
-
if (useManifestOnly && manifest && !hasCliOverrides) {
|
|
1381
|
-
// Mode 1: Manifest-only (--from-manifest --no-interactive, no CLI overrides)
|
|
1382
|
-
const manifestAnswers = buildAnswersFromManifest(manifest, manifestDir);
|
|
1383
|
-
answers = mergeAnswers(manifestAnswers);
|
|
1384
|
-
console.log('\n' +
|
|
1385
|
-
boxen(chalk.bold.cyan('Regenerating from Manifest (No Interactive)\n\n') +
|
|
1386
|
-
chalk.white('Configuration:\n') +
|
|
1387
|
-
chalk.gray(` Template: ${manifest.baseTemplate}\n`) +
|
|
1388
|
-
chalk.gray(` Base Image: ${manifest.baseImage}\n`) +
|
|
1389
|
-
(manifest.containerName
|
|
1390
|
-
? chalk.gray(` Container: ${manifest.containerName}\n`)
|
|
1391
|
-
: '') +
|
|
1392
|
-
chalk.gray(` Overlays: ${manifest.overlays.join(', ')}\n`) +
|
|
1393
|
-
(manifest.preset ? chalk.gray(` Preset: ${manifest.preset}\n`) : '') +
|
|
1394
|
-
(manifest.portOffset
|
|
1395
|
-
? chalk.gray(` Port offset: ${manifest.portOffset}\n`)
|
|
1396
|
-
: '') +
|
|
1397
|
-
chalk.gray(` Output: ${answers.outputPath}`), { padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: 1 }));
|
|
1398
|
-
}
|
|
1399
|
-
else if (useProjectOnly && projectConfigAnswers && !hasCliOverrides) {
|
|
1400
|
-
const projectFileName = projectConfig?.file.fileName ?? '.superposition.yml';
|
|
1401
|
-
answers = mergeAnswers(projectConfigAnswers, {
|
|
1402
|
-
outputPath: cliArgs?.config?.outputPath ||
|
|
1403
|
-
projectConfigAnswers.outputPath ||
|
|
1404
|
-
'./.devcontainer',
|
|
1405
|
-
minimal: cliArgs?.config?.minimal,
|
|
1406
|
-
editor: cliArgs?.config?.editor,
|
|
1407
|
-
});
|
|
1408
|
-
console.log('\n' +
|
|
1409
|
-
boxen(chalk.bold.cyan('Regenerating from Project File (No Interactive)\n\n') +
|
|
1410
|
-
chalk.white('Configuration:\n') +
|
|
1411
|
-
chalk.gray(` Project file: ${projectFileName}\n`) +
|
|
1412
|
-
chalk.gray(` Output: ${answers.outputPath}`), {
|
|
1413
|
-
padding: 1,
|
|
1414
|
-
borderColor: 'cyan',
|
|
1415
|
-
borderStyle: 'round',
|
|
1416
|
-
margin: 1,
|
|
1417
|
-
}));
|
|
1418
|
-
}
|
|
1419
|
-
else if ((cliArgs && (cliArgs.config.stack || hasCliOverrides)) ||
|
|
1420
|
-
(projectConfigAnswers && (cliArgs?.noInteractive || hasAnyCliConfig))) {
|
|
1421
|
-
// Mode 2: CLI-based (with optional manifest defaults)
|
|
1422
|
-
// This includes regen with --minimal or --editor flags
|
|
1423
|
-
const cliAnswers = buildAnswersFromCliArgs(cliArgs.config);
|
|
1424
|
-
const manifestAnswers = manifest
|
|
1425
|
-
? buildAnswersFromManifest(manifest, manifestDir)
|
|
1426
|
-
: undefined;
|
|
1427
|
-
answers = mergeAnswers(projectConfigAnswers, manifestAnswers, cliAnswers, {
|
|
1428
|
-
outputPath: cliAnswers.outputPath || projectConfigAnswers?.outputPath || './.devcontainer',
|
|
1429
|
-
});
|
|
1430
|
-
const modeLabel = useManifestOnly && hasCliOverrides
|
|
1431
|
-
? 'Regenerating from Manifest with Overrides'
|
|
1432
|
-
: useProjectOnly && projectConfigAnswers
|
|
1433
|
-
? 'Regenerating from Project File with Overrides'
|
|
1434
|
-
: projectConfigAnswers && !manifest
|
|
1435
|
-
? 'Running from Project Config'
|
|
1436
|
-
: 'Running in CLI mode';
|
|
1437
|
-
console.log('\n' +
|
|
1438
|
-
boxen(chalk.bold(modeLabel), {
|
|
1439
|
-
padding: 0.5,
|
|
1440
|
-
borderColor: 'blue',
|
|
1441
|
-
borderStyle: 'round',
|
|
1442
|
-
}));
|
|
1443
|
-
// Show what's being overridden
|
|
1444
|
-
if ((useManifestOnly || useProjectOnly) && hasCliOverrides) {
|
|
1445
|
-
const overrides = [];
|
|
1446
|
-
if (cliAnswers.minimal)
|
|
1447
|
-
overrides.push('minimal mode');
|
|
1448
|
-
if (cliAnswers.editor)
|
|
1449
|
-
overrides.push(`editor: ${cliAnswers.editor}`);
|
|
1450
|
-
if (overrides.length > 0) {
|
|
1451
|
-
console.log(chalk.dim(` Overrides: ${overrides.join(', ')}`));
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
else {
|
|
1456
|
-
// Mode 3: Interactive (with optional manifest pre-population and CLI preset pre-selection)
|
|
1457
|
-
const interactiveAnswers = await runQuestionnaire(manifest, manifestDir, cliArgs?.config.preset || projectConfigAnswers?.preset, cliArgs?.config.presetChoices || projectConfigAnswers?.presetChoices, projectConfigAnswers);
|
|
1458
|
-
answers = mergeAnswers(projectConfigAnswers, interactiveAnswers);
|
|
1459
|
-
}
|
|
1460
|
-
if (!manifest && projectConfig?.selection.customizations) {
|
|
1461
|
-
const materializedOutputPath = path.resolve(answers.outputPath);
|
|
1462
|
-
if (!fs.existsSync(materializedOutputPath)) {
|
|
1463
|
-
fs.mkdirSync(materializedOutputPath, { recursive: true });
|
|
1464
|
-
}
|
|
1465
|
-
writeProjectConfigCustomizations(materializedOutputPath, projectConfig.selection.customizations);
|
|
1466
|
-
}
|
|
1467
|
-
// Show configuration summary
|
|
1468
|
-
const summaryLines = [
|
|
1469
|
-
chalk.bold.white('Configuration Summary\n'),
|
|
1470
|
-
chalk.cyan('Base: ') + chalk.white(answers.stack),
|
|
1471
|
-
];
|
|
1472
|
-
if (answers.language && answers.language.length > 0) {
|
|
1473
|
-
summaryLines.push(chalk.cyan('Languages: ') + chalk.white(answers.language.join(', ')));
|
|
1474
|
-
}
|
|
1475
|
-
if (answers.database && answers.database.length > 0) {
|
|
1476
|
-
summaryLines.push(chalk.cyan('Database: ') + chalk.white(answers.database.join(', ')));
|
|
1477
|
-
}
|
|
1478
|
-
summaryLines.push(chalk.cyan('Playwright: ') + chalk.white(answers.playwright ? 'Yes' : 'No'));
|
|
1479
|
-
if (answers.observability && answers.observability.length > 0) {
|
|
1480
|
-
summaryLines.push(chalk.cyan('Observability: ') + chalk.white(answers.observability.join(', ')));
|
|
1481
|
-
}
|
|
1482
|
-
if (answers.cloudTools && answers.cloudTools.length > 0) {
|
|
1483
|
-
summaryLines.push(chalk.cyan('Cloud tools: ') + chalk.white(answers.cloudTools.join(', ')));
|
|
1484
|
-
}
|
|
1485
|
-
if (projectConfig?.file && !manifest) {
|
|
1486
|
-
summaryLines.push(chalk.cyan('Project config: ') + chalk.white(projectConfig.file.fileName));
|
|
1487
|
-
}
|
|
1488
|
-
summaryLines.push(chalk.cyan('Output: ') + chalk.white(answers.outputPath));
|
|
1489
|
-
console.log('\n' +
|
|
1490
|
-
boxen(summaryLines.join('\n'), {
|
|
1491
|
-
padding: 1,
|
|
1492
|
-
borderColor: 'green',
|
|
1493
|
-
borderStyle: 'round',
|
|
1494
|
-
margin: { top: 0, bottom: 1 },
|
|
1495
|
-
}));
|
|
1496
|
-
// Check if we're in manifest-only mode
|
|
1497
|
-
const isManifestOnly = cliArgs?.writeManifestOnly === true;
|
|
1498
|
-
// Generate with spinner
|
|
1499
|
-
const spinner = ora({
|
|
1500
|
-
text: isManifestOnly
|
|
1501
|
-
? chalk.cyan('Generating manifest file...')
|
|
1502
|
-
: chalk.cyan('Generating devcontainer configuration...'),
|
|
1503
|
-
color: 'cyan',
|
|
1504
|
-
}).start();
|
|
1505
|
-
try {
|
|
1506
|
-
let summary;
|
|
1507
|
-
if (isManifestOnly) {
|
|
1508
|
-
summary = await generateManifestOnly(answers, undefined, {
|
|
1509
|
-
isRegen: isReplayMode,
|
|
1510
|
-
});
|
|
1511
|
-
spinner.succeed(chalk.green('Manifest created successfully!'));
|
|
1512
|
-
}
|
|
1513
|
-
else {
|
|
1514
|
-
summary = await composeDevContainer(answers, undefined, { isRegen: isReplayMode });
|
|
1515
|
-
spinner.succeed(chalk.green('DevContainer created successfully!'));
|
|
1516
|
-
}
|
|
1517
|
-
// Update summary with backup path and regen status
|
|
1518
|
-
if (actualBackupPath) {
|
|
1519
|
-
summary.backupPath = actualBackupPath;
|
|
1520
|
-
}
|
|
1521
|
-
// Print comprehensive summary
|
|
1522
|
-
printSummary(summary);
|
|
1523
|
-
}
|
|
1524
|
-
catch (error) {
|
|
1525
|
-
spinner.fail(chalk.red(isManifestOnly ? 'Failed to create manifest' : 'Failed to create devcontainer'));
|
|
1526
|
-
throw error;
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
catch (error) {
|
|
1530
|
-
console.error('\n' +
|
|
1531
|
-
boxen(chalk.bold.red('Error\n\n') +
|
|
1532
|
-
chalk.white(error instanceof Error ? error.message : String(error)), { padding: 1, borderColor: 'red', borderStyle: 'round' }));
|
|
1533
|
-
process.exit(1);
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
2
|
+
import { main } from '../tool/cli/run.js';
|
|
1536
3
|
main();
|
|
1537
4
|
//# sourceMappingURL=init.js.map
|