container-superposition 0.1.1
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 +843 -0
- package/dist/scripts/init.d.ts +3 -0
- package/dist/scripts/init.d.ts.map +1 -0
- package/dist/scripts/init.js +1190 -0
- package/dist/scripts/init.js.map +1 -0
- package/dist/scripts/migrate-to-manifests.d.ts +12 -0
- package/dist/scripts/migrate-to-manifests.d.ts.map +1 -0
- package/dist/scripts/migrate-to-manifests.js +230 -0
- package/dist/scripts/migrate-to-manifests.js.map +1 -0
- package/dist/tool/questionnaire/composer.d.ts +6 -0
- package/dist/tool/questionnaire/composer.d.ts.map +1 -0
- package/dist/tool/questionnaire/composer.js +1232 -0
- package/dist/tool/questionnaire/composer.js.map +1 -0
- package/dist/tool/readme/markdown-parser.d.ts +30 -0
- package/dist/tool/readme/markdown-parser.d.ts.map +1 -0
- package/dist/tool/readme/markdown-parser.js +139 -0
- package/dist/tool/readme/markdown-parser.js.map +1 -0
- package/dist/tool/readme/readme-generator.d.ts +9 -0
- package/dist/tool/readme/readme-generator.d.ts.map +1 -0
- package/dist/tool/readme/readme-generator.js +422 -0
- package/dist/tool/readme/readme-generator.js.map +1 -0
- package/dist/tool/schema/custom-loader.d.ts +17 -0
- package/dist/tool/schema/custom-loader.d.ts.map +1 -0
- package/dist/tool/schema/custom-loader.js +149 -0
- package/dist/tool/schema/custom-loader.js.map +1 -0
- package/dist/tool/schema/overlay-loader.d.ts +47 -0
- package/dist/tool/schema/overlay-loader.d.ts.map +1 -0
- package/dist/tool/schema/overlay-loader.js +252 -0
- package/dist/tool/schema/overlay-loader.js.map +1 -0
- package/dist/tool/schema/types.d.ts +212 -0
- package/dist/tool/schema/types.d.ts.map +1 -0
- package/dist/tool/schema/types.js +5 -0
- package/dist/tool/schema/types.js.map +1 -0
- package/docs/README.md +308 -0
- package/docs/architecture.md +233 -0
- package/docs/creating-overlays.md +549 -0
- package/docs/custom-patches.md +540 -0
- package/docs/dependencies.md +279 -0
- package/docs/examples/custom-patches-example.md +85 -0
- package/docs/examples.md +576 -0
- package/docs/messaging-comparison.md +265 -0
- package/docs/messaging-quick-start.md +385 -0
- package/docs/observability-workflow.md +537 -0
- package/docs/overlay-manifest-refactoring.md +214 -0
- package/docs/overlay-metadata-archive.md +54 -0
- package/docs/overlays.md +523 -0
- package/docs/presets-architecture.md +498 -0
- package/docs/presets.md +366 -0
- package/docs/publishing.md +476 -0
- package/docs/quick-reference.md +326 -0
- package/docs/ux.md +170 -0
- package/features/README.md +85 -0
- package/features/cross-distro-packages/README.md +146 -0
- package/features/cross-distro-packages/devcontainer-feature.json +20 -0
- package/features/cross-distro-packages/install.sh +58 -0
- package/features/local-secrets-manager/devcontainer-feature.json +18 -0
- package/features/local-secrets-manager/install.sh +127 -0
- package/features/project-scaffolder/devcontainer-feature.json +24 -0
- package/features/project-scaffolder/install.sh +100 -0
- package/features/team-conventions/devcontainer-feature.json +24 -0
- package/features/team-conventions/install.sh +93 -0
- package/overlays/.registry/README.md +14 -0
- package/overlays/.registry/base-images.yml +26 -0
- package/overlays/.registry/base-templates.yml +7 -0
- package/overlays/README.md +155 -0
- package/overlays/alertmanager/.env.example +5 -0
- package/overlays/alertmanager/README.md +465 -0
- package/overlays/alertmanager/alert-rules.yml +56 -0
- package/overlays/alertmanager/alertmanager.yml +42 -0
- package/overlays/alertmanager/devcontainer.patch.json +12 -0
- package/overlays/alertmanager/docker-compose.yml +20 -0
- package/overlays/alertmanager/overlay.yml +17 -0
- package/overlays/alertmanager/setup.sh +53 -0
- package/overlays/alertmanager/verify.sh +31 -0
- package/overlays/aws-cli/README.md +473 -0
- package/overlays/aws-cli/devcontainer.patch.json +13 -0
- package/overlays/aws-cli/overlay.yml +13 -0
- package/overlays/azure-cli/README.md +551 -0
- package/overlays/azure-cli/devcontainer.patch.json +8 -0
- package/overlays/azure-cli/overlay.yml +13 -0
- package/overlays/bun/README.md +312 -0
- package/overlays/bun/devcontainer.patch.json +41 -0
- package/overlays/bun/overlay.yml +16 -0
- package/overlays/bun/setup.sh +79 -0
- package/overlays/bun/verify.sh +30 -0
- package/overlays/codex/README.md +128 -0
- package/overlays/codex/devcontainer.patch.json +3 -0
- package/overlays/codex/overlay.yml +14 -0
- package/overlays/codex/setup.sh +24 -0
- package/overlays/codex/verify.sh +30 -0
- package/overlays/commitlint/README.md +333 -0
- package/overlays/commitlint/devcontainer.patch.json +8 -0
- package/overlays/commitlint/overlay.yml +16 -0
- package/overlays/commitlint/setup.sh +234 -0
- package/overlays/direnv/README.md +504 -0
- package/overlays/direnv/devcontainer.patch.json +6 -0
- package/overlays/direnv/overlay.yml +13 -0
- package/overlays/direnv/setup.sh +139 -0
- package/overlays/docker-in-docker/README.md +534 -0
- package/overlays/docker-in-docker/devcontainer.patch.json +10 -0
- package/overlays/docker-in-docker/overlay.yml +13 -0
- package/overlays/docker-sock/README.md +256 -0
- package/overlays/docker-sock/devcontainer.patch.json +9 -0
- package/overlays/docker-sock/docker-compose.yml +8 -0
- package/overlays/docker-sock/overlay.yml +13 -0
- package/overlays/dotnet/README.md +147 -0
- package/overlays/dotnet/devcontainer.patch.json +51 -0
- package/overlays/dotnet/global-tools.txt +24 -0
- package/overlays/dotnet/overlay.yml +13 -0
- package/overlays/dotnet/setup.sh +51 -0
- package/overlays/dotnet/verify.sh +26 -0
- package/overlays/gcloud/README.md +269 -0
- package/overlays/gcloud/devcontainer.patch.json +14 -0
- package/overlays/gcloud/overlay.yml +14 -0
- package/overlays/gcloud/verify.sh +52 -0
- package/overlays/git-helpers/README.md +168 -0
- package/overlays/git-helpers/devcontainer.patch.json +33 -0
- package/overlays/git-helpers/overlay.yml +15 -0
- package/overlays/git-helpers/setup.sh +91 -0
- package/overlays/go/README.md +293 -0
- package/overlays/go/devcontainer.patch.json +43 -0
- package/overlays/go/overlay.yml +15 -0
- package/overlays/go/setup.sh +33 -0
- package/overlays/go/verify.sh +40 -0
- package/overlays/grafana/.env.example +9 -0
- package/overlays/grafana/README.md +462 -0
- package/overlays/grafana/dashboard-provider.yml +11 -0
- package/overlays/grafana/dashboards/observability-overview.json +263 -0
- package/overlays/grafana/devcontainer.patch.json +12 -0
- package/overlays/grafana/docker-compose.yml +27 -0
- package/overlays/grafana/grafana-datasources.yml +57 -0
- package/overlays/grafana/overlay.yml +21 -0
- package/overlays/grafana/verify.sh +34 -0
- package/overlays/jaeger/.env.example +7 -0
- package/overlays/jaeger/README.md +867 -0
- package/overlays/jaeger/devcontainer.patch.json +12 -0
- package/overlays/jaeger/docker-compose.yml +17 -0
- package/overlays/jaeger/overlay.yml +19 -0
- package/overlays/java/README.md +267 -0
- package/overlays/java/devcontainer.patch.json +44 -0
- package/overlays/java/overlay.yml +16 -0
- package/overlays/java/setup.sh +41 -0
- package/overlays/java/verify.sh +42 -0
- package/overlays/just/README.md +443 -0
- package/overlays/just/devcontainer.patch.json +3 -0
- package/overlays/just/overlay.yml +13 -0
- package/overlays/just/setup.sh +182 -0
- package/overlays/kubectl-helm/README.md +660 -0
- package/overlays/kubectl-helm/devcontainer.patch.json +10 -0
- package/overlays/kubectl-helm/overlay.yml +13 -0
- package/overlays/loki/.env.example +5 -0
- package/overlays/loki/README.md +1156 -0
- package/overlays/loki/devcontainer.patch.json +12 -0
- package/overlays/loki/docker-compose.yml +18 -0
- package/overlays/loki/loki-config.yaml +45 -0
- package/overlays/loki/overlay.yml +17 -0
- package/overlays/minio/.env.example +9 -0
- package/overlays/minio/README.md +639 -0
- package/overlays/minio/devcontainer.patch.json +30 -0
- package/overlays/minio/docker-compose.yml +28 -0
- package/overlays/minio/overlay.yml +18 -0
- package/overlays/minio/setup.sh +61 -0
- package/overlays/minio/verify.sh +64 -0
- package/overlays/mkdocs/README.md +309 -0
- package/overlays/mkdocs/devcontainer.patch.json +24 -0
- package/overlays/mkdocs/overlay.yml +15 -0
- package/overlays/modern-cli-tools/README.md +556 -0
- package/overlays/modern-cli-tools/devcontainer.patch.json +3 -0
- package/overlays/modern-cli-tools/overlay.yml +13 -0
- package/overlays/modern-cli-tools/setup.sh +153 -0
- package/overlays/mongodb/.env.example +9 -0
- package/overlays/mongodb/README.md +481 -0
- package/overlays/mongodb/devcontainer.patch.json +32 -0
- package/overlays/mongodb/docker-compose.yml +44 -0
- package/overlays/mongodb/overlay.yml +17 -0
- package/overlays/mongodb/verify.sh +48 -0
- package/overlays/mysql/.env.example +11 -0
- package/overlays/mysql/README.md +542 -0
- package/overlays/mysql/devcontainer.patch.json +34 -0
- package/overlays/mysql/docker-compose.yml +55 -0
- package/overlays/mysql/overlay.yml +16 -0
- package/overlays/mysql/verify.sh +48 -0
- package/overlays/nats/.env.example +5 -0
- package/overlays/nats/README.md +762 -0
- package/overlays/nats/devcontainer.patch.json +24 -0
- package/overlays/nats/docker-compose.yml +31 -0
- package/overlays/nats/overlay.yml +18 -0
- package/overlays/nats/verify.sh +50 -0
- package/overlays/ngrok/README.md +503 -0
- package/overlays/ngrok/devcontainer.patch.json +3 -0
- package/overlays/ngrok/overlay.yml +14 -0
- package/overlays/ngrok/setup.sh +125 -0
- package/overlays/nodejs/README.md +192 -0
- package/overlays/nodejs/devcontainer.patch.json +49 -0
- package/overlays/nodejs/global-packages.txt +16 -0
- package/overlays/nodejs/overlay.yml +14 -0
- package/overlays/nodejs/setup.sh +46 -0
- package/overlays/nodejs/verify.sh +32 -0
- package/overlays/otel-collector/.env.example +9 -0
- package/overlays/otel-collector/README.md +1257 -0
- package/overlays/otel-collector/devcontainer.patch.json +28 -0
- package/overlays/otel-collector/docker-compose.yml +22 -0
- package/overlays/otel-collector/otel-collector-config.yaml +68 -0
- package/overlays/otel-collector/overlay.yml +21 -0
- package/overlays/otel-collector/setup.sh +49 -0
- package/overlays/otel-demo-nodejs/.env.example +2 -0
- package/overlays/otel-demo-nodejs/Dockerfile-otel-demo-nodejs +17 -0
- package/overlays/otel-demo-nodejs/README.md +409 -0
- package/overlays/otel-demo-nodejs/devcontainer.patch.json +12 -0
- package/overlays/otel-demo-nodejs/docker-compose.yml +19 -0
- package/overlays/otel-demo-nodejs/overlay.yml +23 -0
- package/overlays/otel-demo-nodejs/package-otel-demo-nodejs.json +20 -0
- package/overlays/otel-demo-nodejs/server-otel-demo-nodejs.js +259 -0
- package/overlays/otel-demo-nodejs/tracing-otel-demo-nodejs.js +57 -0
- package/overlays/otel-demo-nodejs/verify.sh +31 -0
- package/overlays/otel-demo-python/.env.example +2 -0
- package/overlays/otel-demo-python/Dockerfile-otel-demo-python +16 -0
- package/overlays/otel-demo-python/README.md +82 -0
- package/overlays/otel-demo-python/app-otel-demo-python.py +208 -0
- package/overlays/otel-demo-python/devcontainer.patch.json +12 -0
- package/overlays/otel-demo-python/docker-compose.yml +19 -0
- package/overlays/otel-demo-python/overlay.yml +23 -0
- package/overlays/otel-demo-python/requirements-otel-demo-python.txt +4 -0
- package/overlays/otel-demo-python/verify.sh +31 -0
- package/overlays/playwright/README.md +629 -0
- package/overlays/playwright/devcontainer.patch.json +9 -0
- package/overlays/playwright/overlay.yml +13 -0
- package/overlays/postgres/.env.example +6 -0
- package/overlays/postgres/README.md +602 -0
- package/overlays/postgres/devcontainer.patch.json +21 -0
- package/overlays/postgres/docker-compose.yml +22 -0
- package/overlays/postgres/overlay.yml +15 -0
- package/overlays/postgres/verify.sh +45 -0
- package/overlays/powershell/README.md +314 -0
- package/overlays/powershell/devcontainer.patch.json +22 -0
- package/overlays/powershell/overlay.yml +13 -0
- package/overlays/powershell/setup.sh +29 -0
- package/overlays/powershell/verify.sh +38 -0
- package/overlays/pre-commit/README.md +263 -0
- package/overlays/pre-commit/devcontainer.patch.json +9 -0
- package/overlays/pre-commit/overlay.yml +16 -0
- package/overlays/pre-commit/setup.sh +129 -0
- package/overlays/presets/docs-site.yml +118 -0
- package/overlays/presets/fullstack.yml +181 -0
- package/overlays/presets/microservice.yml +118 -0
- package/overlays/presets/web-api.yml +109 -0
- package/overlays/prometheus/.env.example +5 -0
- package/overlays/prometheus/README.md +1246 -0
- package/overlays/prometheus/devcontainer.patch.json +12 -0
- package/overlays/prometheus/docker-compose.yml +22 -0
- package/overlays/prometheus/overlay.yml +17 -0
- package/overlays/prometheus/prometheus.yml +12 -0
- package/overlays/prometheus/verify.sh +34 -0
- package/overlays/promtail/.env.example +2 -0
- package/overlays/promtail/README.md +357 -0
- package/overlays/promtail/devcontainer.patch.json +5 -0
- package/overlays/promtail/docker-compose.yml +16 -0
- package/overlays/promtail/overlay.yml +17 -0
- package/overlays/promtail/promtail-config.yaml +60 -0
- package/overlays/promtail/verify.sh +31 -0
- package/overlays/pulumi/README.md +472 -0
- package/overlays/pulumi/devcontainer.patch.json +13 -0
- package/overlays/pulumi/overlay.yml +14 -0
- package/overlays/pulumi/verify.sh +31 -0
- package/overlays/python/README.md +919 -0
- package/overlays/python/devcontainer.patch.json +41 -0
- package/overlays/python/overlay.yml +12 -0
- package/overlays/python/requirements-overlay.txt +13 -0
- package/overlays/python/setup.sh +47 -0
- package/overlays/python/verify.sh +32 -0
- package/overlays/rabbitmq/.env.example +7 -0
- package/overlays/rabbitmq/README.md +680 -0
- package/overlays/rabbitmq/devcontainer.patch.json +28 -0
- package/overlays/rabbitmq/docker-compose.yml +30 -0
- package/overlays/rabbitmq/overlay.yml +18 -0
- package/overlays/rabbitmq/verify.sh +41 -0
- package/overlays/redis/.env.example +4 -0
- package/overlays/redis/README.md +776 -0
- package/overlays/redis/devcontainer.patch.json +21 -0
- package/overlays/redis/docker-compose.yml +21 -0
- package/overlays/redis/overlay.yml +15 -0
- package/overlays/redis/verify.sh +41 -0
- package/overlays/redpanda/.env.example +10 -0
- package/overlays/redpanda/README.md +703 -0
- package/overlays/redpanda/devcontainer.patch.json +37 -0
- package/overlays/redpanda/docker-compose.yml +67 -0
- package/overlays/redpanda/overlay.yml +21 -0
- package/overlays/redpanda/verify.sh +48 -0
- package/overlays/rust/README.md +299 -0
- package/overlays/rust/devcontainer.patch.json +39 -0
- package/overlays/rust/overlay.yml +15 -0
- package/overlays/rust/setup.sh +36 -0
- package/overlays/rust/verify.sh +51 -0
- package/overlays/sqlite/README.md +584 -0
- package/overlays/sqlite/devcontainer.patch.json +14 -0
- package/overlays/sqlite/overlay.yml +15 -0
- package/overlays/sqlite/setup.sh +27 -0
- package/overlays/sqlite/verify.sh +43 -0
- package/overlays/sqlserver/.env.example +6 -0
- package/overlays/sqlserver/README.md +592 -0
- package/overlays/sqlserver/devcontainer.patch.json +22 -0
- package/overlays/sqlserver/docker-compose.yml +32 -0
- package/overlays/sqlserver/overlay.yml +17 -0
- package/overlays/sqlserver/verify.sh +30 -0
- package/overlays/tempo/.env.example +5 -0
- package/overlays/tempo/README.md +273 -0
- package/overlays/tempo/devcontainer.patch.json +12 -0
- package/overlays/tempo/docker-compose.yml +20 -0
- package/overlays/tempo/overlay.yml +20 -0
- package/overlays/tempo/tempo-config.yaml +32 -0
- package/overlays/tempo/verify.sh +31 -0
- package/overlays/terraform/README.md +389 -0
- package/overlays/terraform/devcontainer.patch.json +15 -0
- package/overlays/terraform/overlay.yml +14 -0
- package/overlays/terraform/verify.sh +63 -0
- package/package.json +74 -0
- package/templates/README.md +285 -0
- package/templates/compose/.devcontainer/devcontainer.json +46 -0
- package/templates/compose/.devcontainer/docker-compose.yml +12 -0
- package/templates/compose/README.md +20 -0
- package/templates/plain/.devcontainer/devcontainer.json +35 -0
- package/templates/plain/README.md +21 -0
- package/tool/README.md +281 -0
- package/tool/schema/base-images.schema.json +43 -0
- package/tool/schema/base-templates.schema.json +34 -0
- package/tool/schema/config.schema.json +71 -0
- package/tool/schema/overlay-manifest.schema.json +86 -0
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from 'fs';
|
|
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 } from '../tool/questionnaire/composer.js';
|
|
12
|
+
import { loadOverlaysConfig } from '../tool/schema/overlay-loader.js';
|
|
13
|
+
// Get __dirname equivalent in ESM
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
const OVERLAYS_DIR_CANDIDATES = [
|
|
17
|
+
// When running from TypeScript sources (e.g. ts-node), __dirname is "<root>/scripts"
|
|
18
|
+
path.join(__dirname, '..', 'overlays'),
|
|
19
|
+
// When running from compiled JS in "dist/scripts", __dirname is "<root>/dist/scripts"
|
|
20
|
+
path.join(__dirname, '..', '..', 'overlays'),
|
|
21
|
+
];
|
|
22
|
+
const OVERLAYS_DIR = OVERLAYS_DIR_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
|
|
23
|
+
OVERLAYS_DIR_CANDIDATES[0];
|
|
24
|
+
const OVERLAYS_CONFIG_CANDIDATES = [
|
|
25
|
+
// When running from TypeScript sources (e.g. ts-node), __dirname is "<root>/scripts"
|
|
26
|
+
// and "../overlays/index.yml" resolves to "<root>/overlays/index.yml".
|
|
27
|
+
path.join(__dirname, '..', 'overlays', 'index.yml'),
|
|
28
|
+
// When running from compiled JS in "dist/scripts", __dirname is "<root>/dist/scripts"
|
|
29
|
+
// and "../../overlays/index.yml" resolves to "<root>/overlays/index.yml".
|
|
30
|
+
path.join(__dirname, '..', '..', 'overlays', 'index.yml'),
|
|
31
|
+
];
|
|
32
|
+
const OVERLAYS_CONFIG_PATH = OVERLAYS_CONFIG_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
|
|
33
|
+
OVERLAYS_CONFIG_CANDIDATES[0];
|
|
34
|
+
const PRESETS_DIR_CANDIDATES = [
|
|
35
|
+
path.join(__dirname, '..', 'overlays', 'presets'),
|
|
36
|
+
path.join(__dirname, '..', '..', 'overlays', 'presets'),
|
|
37
|
+
];
|
|
38
|
+
const PRESETS_DIR = PRESETS_DIR_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
|
|
39
|
+
PRESETS_DIR_CANDIDATES[0];
|
|
40
|
+
/**
|
|
41
|
+
* Load overlay metadata from individual manifests or fallback to YAML file
|
|
42
|
+
*/
|
|
43
|
+
function loadOverlaysConfigWrapper() {
|
|
44
|
+
return loadOverlaysConfig(OVERLAYS_DIR, OVERLAYS_CONFIG_PATH);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Load preset definition from YAML file
|
|
48
|
+
*/
|
|
49
|
+
function loadPresetDefinition(presetId) {
|
|
50
|
+
const presetPath = path.join(PRESETS_DIR, `${presetId}.yml`);
|
|
51
|
+
if (!fs.existsSync(presetPath)) {
|
|
52
|
+
console.warn(chalk.yellow(`⚠️ Preset definition not found: ${presetPath}`));
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const content = fs.readFileSync(presetPath, 'utf8');
|
|
56
|
+
return yaml.load(content);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Expand a preset into a list of overlay IDs with user choices resolved
|
|
60
|
+
*/
|
|
61
|
+
async function expandPreset(presetId, stack) {
|
|
62
|
+
const preset = loadPresetDefinition(presetId);
|
|
63
|
+
if (!preset) {
|
|
64
|
+
return { overlays: [], choices: {} };
|
|
65
|
+
}
|
|
66
|
+
console.log(chalk.cyan(`\n📦 Expanding preset: ${preset.name}\n`));
|
|
67
|
+
const overlays = [...preset.selects.required];
|
|
68
|
+
const choices = {};
|
|
69
|
+
// Handle user choices
|
|
70
|
+
if (preset.selects.userChoice) {
|
|
71
|
+
for (const [key, choice] of Object.entries(preset.selects.userChoice)) {
|
|
72
|
+
const selectedOption = (await select({
|
|
73
|
+
message: choice.prompt,
|
|
74
|
+
choices: choice.options.map((opt) => ({
|
|
75
|
+
name: opt,
|
|
76
|
+
value: opt,
|
|
77
|
+
})),
|
|
78
|
+
default: choice.defaultOption,
|
|
79
|
+
}));
|
|
80
|
+
overlays.push(selectedOption);
|
|
81
|
+
choices[key] = selectedOption;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
console.log(chalk.dim(`✓ Preset will include: ${overlays.join(', ')}\n`));
|
|
85
|
+
return { overlays, choices, glueConfig: preset.glueConfig };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Search for manifest file in multiple locations
|
|
89
|
+
*/
|
|
90
|
+
function findManifestFile(manifestPath) {
|
|
91
|
+
const searchPaths = [];
|
|
92
|
+
if (manifestPath) {
|
|
93
|
+
// If path specified, use it directly
|
|
94
|
+
searchPaths.push(manifestPath);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Search in common locations
|
|
98
|
+
searchPaths.push('superposition.json', '.devcontainer/superposition.json', '../superposition.json', path.join(process.cwd(), 'superposition.json'), path.join(process.cwd(), '.devcontainer', 'superposition.json'));
|
|
99
|
+
}
|
|
100
|
+
for (const searchPath of searchPaths) {
|
|
101
|
+
const resolvedPath = path.resolve(searchPath);
|
|
102
|
+
if (fs.existsSync(resolvedPath)) {
|
|
103
|
+
return resolvedPath;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Load and validate manifest file
|
|
110
|
+
*/
|
|
111
|
+
function loadManifest(manifestPath) {
|
|
112
|
+
try {
|
|
113
|
+
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
114
|
+
const manifest = JSON.parse(content);
|
|
115
|
+
// Basic validation
|
|
116
|
+
if (!manifest.version || !manifest.baseTemplate) {
|
|
117
|
+
console.error(chalk.red('✗ Invalid manifest format: missing required fields (version, baseTemplate)'));
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (!Array.isArray(manifest.overlays)) {
|
|
121
|
+
console.error(chalk.red('✗ Invalid manifest format: "overlays" must be an array'));
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
if (!manifest.overlays.every((overlay) => typeof overlay === 'string')) {
|
|
125
|
+
console.error(chalk.red('✗ Invalid manifest format: all "overlays" entries must be strings'));
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
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
|
+
return manifest;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
console.error(chalk.red(`✗ Failed to load manifest: ${error instanceof Error ? error.message : String(error)}`));
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Create timestamped backup of existing devcontainer and manifest
|
|
141
|
+
*/
|
|
142
|
+
async function createBackup(outputPath, backupDir) {
|
|
143
|
+
// Check for devcontainer files to backup
|
|
144
|
+
const devcontainerJsonPath = path.join(outputPath, 'devcontainer.json');
|
|
145
|
+
const dockerComposePath = path.join(outputPath, 'docker-compose.yml');
|
|
146
|
+
const devcontainerSubdir = path.join(outputPath, '.devcontainer');
|
|
147
|
+
const manifestPath = path.join(outputPath, 'superposition.json');
|
|
148
|
+
// Determine what exists
|
|
149
|
+
const hasDevcontainerJson = fs.existsSync(devcontainerJsonPath);
|
|
150
|
+
const hasDockerCompose = fs.existsSync(dockerComposePath);
|
|
151
|
+
const hasDevcontainerSubdir = fs.existsSync(devcontainerSubdir) && fs.statSync(devcontainerSubdir).isDirectory();
|
|
152
|
+
const hasManifest = fs.existsSync(manifestPath);
|
|
153
|
+
if (!hasDevcontainerJson && !hasDockerCompose && !hasDevcontainerSubdir && !hasManifest) {
|
|
154
|
+
return null; // Nothing to backup
|
|
155
|
+
}
|
|
156
|
+
// Create timestamp
|
|
157
|
+
const timestamp = new Date()
|
|
158
|
+
.toISOString()
|
|
159
|
+
.replace(/:/g, '-')
|
|
160
|
+
.replace(/\..+/, '')
|
|
161
|
+
.replace('T', '-');
|
|
162
|
+
// Determine backup location - create next to outputPath, not inside it
|
|
163
|
+
const resolvedOutputPath = path.resolve(outputPath);
|
|
164
|
+
const outputParentDir = path.dirname(resolvedOutputPath);
|
|
165
|
+
const outputBaseName = path.basename(resolvedOutputPath);
|
|
166
|
+
const backupBaseName = outputBaseName === '.devcontainer' ? '.devcontainer' : outputBaseName;
|
|
167
|
+
const backupPath = backupDir
|
|
168
|
+
? path.resolve(backupDir)
|
|
169
|
+
: path.join(outputParentDir, `${backupBaseName}.backup-${timestamp}`);
|
|
170
|
+
// Create backup directory
|
|
171
|
+
fs.mkdirSync(backupPath, { recursive: true });
|
|
172
|
+
// Backup files and directories
|
|
173
|
+
if (hasDevcontainerJson) {
|
|
174
|
+
fs.copyFileSync(devcontainerJsonPath, path.join(backupPath, 'devcontainer.json'));
|
|
175
|
+
}
|
|
176
|
+
if (hasDockerCompose) {
|
|
177
|
+
fs.copyFileSync(dockerComposePath, path.join(backupPath, 'docker-compose.yml'));
|
|
178
|
+
}
|
|
179
|
+
if (hasDevcontainerSubdir) {
|
|
180
|
+
const destDir = path.join(backupPath, '.devcontainer');
|
|
181
|
+
await copyDirectory(devcontainerSubdir, destDir);
|
|
182
|
+
}
|
|
183
|
+
if (hasManifest) {
|
|
184
|
+
fs.copyFileSync(manifestPath, path.join(backupPath, 'superposition.json'));
|
|
185
|
+
}
|
|
186
|
+
// Also backup other common devcontainer files
|
|
187
|
+
const otherFiles = ['.env', '.env.example', '.gitignore', 'features', 'scripts'];
|
|
188
|
+
for (const file of otherFiles) {
|
|
189
|
+
const srcPath = path.join(outputPath, file);
|
|
190
|
+
if (fs.existsSync(srcPath)) {
|
|
191
|
+
const destPath = path.join(backupPath, file);
|
|
192
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
193
|
+
await copyDirectory(srcPath, destPath);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
fs.copyFileSync(srcPath, destPath);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return backupPath;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Recursively copy directory
|
|
204
|
+
*/
|
|
205
|
+
async function copyDirectory(src, dest) {
|
|
206
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
207
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
const srcPath = path.join(src, entry.name);
|
|
210
|
+
const destPath = path.join(dest, entry.name);
|
|
211
|
+
if (entry.isDirectory()) {
|
|
212
|
+
await copyDirectory(srcPath, destPath);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
fs.copyFileSync(srcPath, destPath);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Ensure backup patterns are in .gitignore
|
|
221
|
+
*/
|
|
222
|
+
async function ensureBackupPatternsInGitignore(outputPath) {
|
|
223
|
+
// Write to the parent directory's .gitignore (project root), not inside outputPath
|
|
224
|
+
const resolvedOutputPath = path.resolve(outputPath);
|
|
225
|
+
const projectRoot = path.dirname(resolvedOutputPath);
|
|
226
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
227
|
+
const backupPatterns = [
|
|
228
|
+
'',
|
|
229
|
+
'# Container Superposition backups',
|
|
230
|
+
'.devcontainer.backup-*/',
|
|
231
|
+
'*.backup-*',
|
|
232
|
+
'superposition.json.backup-*',
|
|
233
|
+
].join('\n');
|
|
234
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
235
|
+
// Create new .gitignore with backup patterns
|
|
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
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Build checkbox choices for overlay selection with optional pre-selection
|
|
251
|
+
*/
|
|
252
|
+
function buildOverlayChoices(config, stack, categoryList, preselected) {
|
|
253
|
+
const choices = [];
|
|
254
|
+
categoryList.forEach((category) => {
|
|
255
|
+
const filtered = category.overlays.filter((o) => !o.supports || o.supports.length === 0 || o.supports.includes(stack));
|
|
256
|
+
if (filtered.length > 0) {
|
|
257
|
+
// Add category separator
|
|
258
|
+
choices.push({
|
|
259
|
+
type: 'separator',
|
|
260
|
+
separator: chalk.cyan(`──── ${category.name} ────`),
|
|
261
|
+
});
|
|
262
|
+
// Add overlays in this category
|
|
263
|
+
filtered.forEach((overlay) => {
|
|
264
|
+
choices.push({
|
|
265
|
+
name: overlay.name,
|
|
266
|
+
value: overlay.id,
|
|
267
|
+
description: overlay.description,
|
|
268
|
+
checked: preselected.includes(overlay.id),
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return choices;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Interactive questionnaire with modern checkbox selections
|
|
277
|
+
*/
|
|
278
|
+
async function runQuestionnaire(manifest, manifestDir) {
|
|
279
|
+
const config = loadOverlaysConfigWrapper();
|
|
280
|
+
// Pretty banner
|
|
281
|
+
console.log('\n' +
|
|
282
|
+
boxen(chalk.bold.cyan('Container Superposition') +
|
|
283
|
+
'\n' +
|
|
284
|
+
chalk.gray(manifest ? 'DevContainer Regenerator' : 'DevContainer Initializer'), {
|
|
285
|
+
padding: 1,
|
|
286
|
+
margin: 1,
|
|
287
|
+
borderStyle: 'round',
|
|
288
|
+
borderColor: 'cyan',
|
|
289
|
+
textAlignment: 'center',
|
|
290
|
+
}));
|
|
291
|
+
if (manifest) {
|
|
292
|
+
console.log(chalk.cyan('📋 Loaded from manifest:'));
|
|
293
|
+
console.log(chalk.dim(` Template: ${manifest.baseTemplate}`));
|
|
294
|
+
console.log(chalk.dim(` Overlays: ${manifest.overlays.join(', ')}`));
|
|
295
|
+
if (manifest.preset) {
|
|
296
|
+
console.log(chalk.dim(` Preset: ${manifest.preset}`));
|
|
297
|
+
}
|
|
298
|
+
if (manifest.portOffset) {
|
|
299
|
+
console.log(chalk.dim(` Port offset: ${manifest.portOffset}`));
|
|
300
|
+
}
|
|
301
|
+
console.log();
|
|
302
|
+
}
|
|
303
|
+
console.log(chalk.dim('Compose your ideal devcontainer from modular overlays.'));
|
|
304
|
+
console.log(chalk.dim('Use ') +
|
|
305
|
+
chalk.cyan('space') +
|
|
306
|
+
chalk.dim(' to select, ') +
|
|
307
|
+
chalk.cyan('enter') +
|
|
308
|
+
chalk.dim(' to confirm.\n'));
|
|
309
|
+
try {
|
|
310
|
+
// Question 0: Optional preset selection
|
|
311
|
+
let usePreset = false;
|
|
312
|
+
let selectedPresetId = manifest?.preset;
|
|
313
|
+
let presetChoices = manifest?.presetChoices || {};
|
|
314
|
+
let presetGlueConfig;
|
|
315
|
+
const presetOverlaysFiltered = config.overlays.filter((o) => o.category === 'preset');
|
|
316
|
+
let presetOverlays = [];
|
|
317
|
+
if (presetOverlaysFiltered.length > 0) {
|
|
318
|
+
const defaultPreset = manifest?.preset || 'custom';
|
|
319
|
+
const presetChoice = (await select({
|
|
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') {
|
|
336
|
+
usePreset = true;
|
|
337
|
+
selectedPresetId = presetChoice;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Question 1: Base template
|
|
341
|
+
const stack = (await select({
|
|
342
|
+
message: 'Select base template:',
|
|
343
|
+
choices: config.base_templates.map((t) => ({
|
|
344
|
+
name: t.name,
|
|
345
|
+
value: t.id,
|
|
346
|
+
description: t.description,
|
|
347
|
+
})),
|
|
348
|
+
default: manifest?.baseTemplate,
|
|
349
|
+
}));
|
|
350
|
+
// If using preset, expand it now
|
|
351
|
+
if (usePreset && selectedPresetId) {
|
|
352
|
+
const expansion = await expandPreset(selectedPresetId, stack);
|
|
353
|
+
if (!expansion.overlays || expansion.overlays.length === 0) {
|
|
354
|
+
// Preset failed to expand (e.g., missing or invalid preset definition).
|
|
355
|
+
// Treat this as "no preset" so the manifest does not incorrectly record one.
|
|
356
|
+
console.log(chalk.yellow(`\n⚠️ Preset "${selectedPresetId}" could not be applied. Falling back to custom overlay selection.\n`));
|
|
357
|
+
usePreset = false;
|
|
358
|
+
selectedPresetId = undefined;
|
|
359
|
+
presetOverlays = [];
|
|
360
|
+
presetChoices = {};
|
|
361
|
+
presetGlueConfig = undefined;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
presetOverlays = expansion.overlays;
|
|
365
|
+
presetChoices = expansion.choices;
|
|
366
|
+
presetGlueConfig = expansion.glueConfig;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Question 2: Base image selection
|
|
370
|
+
// Check if manifest has a custom image or a known base image
|
|
371
|
+
const knownBaseImageIds = config.base_images.map((img) => img.id);
|
|
372
|
+
const manifestBaseImageIsKnown = manifest?.baseImage && knownBaseImageIds.includes(manifest.baseImage);
|
|
373
|
+
const manifestDefaultBaseImage = manifestBaseImageIsKnown ? manifest.baseImage : 'custom';
|
|
374
|
+
const baseImage = (await select({
|
|
375
|
+
message: 'Select base image:',
|
|
376
|
+
choices: config.base_images.map((img) => ({
|
|
377
|
+
name: img.name,
|
|
378
|
+
value: img.id,
|
|
379
|
+
description: img.description,
|
|
380
|
+
})),
|
|
381
|
+
default: manifestDefaultBaseImage,
|
|
382
|
+
}));
|
|
383
|
+
// Question 2a: If custom, ask for image name
|
|
384
|
+
let customImage;
|
|
385
|
+
if (baseImage === 'custom') {
|
|
386
|
+
// If manifest has a custom image, use it as default
|
|
387
|
+
const manifestCustomImage = !manifestBaseImageIsKnown && manifest?.baseImage ? manifest.baseImage : undefined;
|
|
388
|
+
customImage = await input({
|
|
389
|
+
message: 'Enter custom Docker image (e.g., ubuntu:22.04):',
|
|
390
|
+
default: manifestCustomImage,
|
|
391
|
+
validate: (value) => {
|
|
392
|
+
if (!value || value.trim() === '') {
|
|
393
|
+
return 'Image name is required';
|
|
394
|
+
}
|
|
395
|
+
return true;
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
console.log(chalk.yellow('\n⚠️ Warning: Custom images may conflict with overlays.'));
|
|
399
|
+
console.log(chalk.dim(' Test thoroughly and adjust configurations as needed.\n'));
|
|
400
|
+
}
|
|
401
|
+
// Build categorized overlays with separators
|
|
402
|
+
const categoryList = [
|
|
403
|
+
{
|
|
404
|
+
name: 'Language',
|
|
405
|
+
overlays: config.overlays.filter((o) => o.category === 'language'),
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
name: 'Database',
|
|
409
|
+
overlays: config.overlays.filter((o) => o.category === 'database'),
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: 'Observability',
|
|
413
|
+
overlays: config.overlays.filter((o) => o.category === 'observability'),
|
|
414
|
+
},
|
|
415
|
+
{ name: 'Cloud', overlays: config.overlays.filter((o) => o.category === 'cloud') },
|
|
416
|
+
{ name: 'DevTool', overlays: config.overlays.filter((o) => o.category === 'dev') },
|
|
417
|
+
];
|
|
418
|
+
// Create a map of all overlays for dependency lookup
|
|
419
|
+
const allOverlaysMap = new Map(config.overlays.map((o) => [o.id, o]));
|
|
420
|
+
// Question 3: Categorized multi-select overlays with dependency tracking
|
|
421
|
+
let userSelection;
|
|
422
|
+
if (usePreset && presetOverlays.length > 0) {
|
|
423
|
+
// Preset mode: Ask if user wants to customize
|
|
424
|
+
console.log(chalk.cyan(`\n✓ Preset includes these overlays: ${presetOverlays.join(', ')}\n`));
|
|
425
|
+
const customizePreset = (await select({
|
|
426
|
+
message: 'Do you want to customize the overlay selection?',
|
|
427
|
+
choices: [
|
|
428
|
+
{
|
|
429
|
+
name: 'Use preset as-is',
|
|
430
|
+
value: 'no',
|
|
431
|
+
description: 'Keep the preset overlay selection',
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
name: 'Customize selection',
|
|
435
|
+
value: 'yes',
|
|
436
|
+
description: 'Add or remove overlays from the preset',
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
}));
|
|
440
|
+
if (customizePreset === 'yes') {
|
|
441
|
+
// Show overlay selection with preset overlays pre-selected
|
|
442
|
+
console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm'));
|
|
443
|
+
console.log(chalk.dim(' Preset overlays are pre-selected\n'));
|
|
444
|
+
const choices = buildOverlayChoices(config, stack, categoryList, presetOverlays);
|
|
445
|
+
userSelection = await checkbox({
|
|
446
|
+
message: 'Select overlays to include:',
|
|
447
|
+
choices,
|
|
448
|
+
pageSize: 15,
|
|
449
|
+
loop: false,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
// Use preset selection as-is
|
|
454
|
+
userSelection = presetOverlays;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
else if (manifest) {
|
|
458
|
+
// Manifest mode: Pre-select overlays from manifest
|
|
459
|
+
console.log(chalk.cyan(`\n✓ Manifest includes these overlays: ${manifest.overlays.join(', ')}\n`));
|
|
460
|
+
const customizeManifest = (await select({
|
|
461
|
+
message: 'Do you want to customize the overlay selection?',
|
|
462
|
+
choices: [
|
|
463
|
+
{
|
|
464
|
+
name: 'Use manifest as-is',
|
|
465
|
+
value: 'no',
|
|
466
|
+
description: 'Keep the manifest overlay selection',
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
name: 'Customize selection',
|
|
470
|
+
value: 'yes',
|
|
471
|
+
description: 'Add or remove overlays from the manifest',
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
}));
|
|
475
|
+
if (customizeManifest === 'yes') {
|
|
476
|
+
// Show overlay selection with manifest overlays pre-selected
|
|
477
|
+
console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm'));
|
|
478
|
+
console.log(chalk.dim(' Manifest overlays are pre-selected\n'));
|
|
479
|
+
// Filter out overlays that don't exist anymore
|
|
480
|
+
const existingOverlays = manifest.overlays.filter((id) => allOverlaysMap.has(id));
|
|
481
|
+
const missingOverlays = manifest.overlays.filter((id) => !allOverlaysMap.has(id));
|
|
482
|
+
if (missingOverlays.length > 0) {
|
|
483
|
+
console.log(chalk.yellow(`⚠️ Warning: Some overlays from manifest no longer exist: ${missingOverlays.join(', ')}\n`));
|
|
484
|
+
}
|
|
485
|
+
const choices = buildOverlayChoices(config, stack, categoryList, existingOverlays);
|
|
486
|
+
userSelection = await checkbox({
|
|
487
|
+
message: 'Select overlays to include:',
|
|
488
|
+
choices,
|
|
489
|
+
pageSize: 15,
|
|
490
|
+
loop: false,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
// Use manifest selection as-is (filtering out missing overlays)
|
|
495
|
+
const existingOverlays = manifest.overlays.filter((id) => allOverlaysMap.has(id));
|
|
496
|
+
const missingOverlays = manifest.overlays.filter((id) => !allOverlaysMap.has(id));
|
|
497
|
+
if (missingOverlays.length > 0) {
|
|
498
|
+
console.log(chalk.yellow(`⚠️ Warning: Some overlays from manifest no longer exist and will be skipped: ${missingOverlays.join(', ')}\n`));
|
|
499
|
+
}
|
|
500
|
+
userSelection = existingOverlays;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
// Custom mode: Normal overlay selection
|
|
505
|
+
console.log(chalk.dim('\n💡 Select overlays: Space to toggle, ↑/↓ to navigate, Enter to confirm\n'));
|
|
506
|
+
const choices = buildOverlayChoices(config, stack, categoryList, []);
|
|
507
|
+
userSelection = await checkbox({
|
|
508
|
+
message: 'Select overlays to include:',
|
|
509
|
+
choices,
|
|
510
|
+
pageSize: 15,
|
|
511
|
+
loop: false,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
// Add all required dependencies
|
|
515
|
+
const withDependencies = new Set(userSelection);
|
|
516
|
+
const toProcess = [...userSelection];
|
|
517
|
+
while (toProcess.length > 0) {
|
|
518
|
+
const current = toProcess.pop();
|
|
519
|
+
const overlay = allOverlaysMap.get(current);
|
|
520
|
+
if (overlay?.requires) {
|
|
521
|
+
overlay.requires.forEach((req) => {
|
|
522
|
+
if (!withDependencies.has(req)) {
|
|
523
|
+
withDependencies.add(req);
|
|
524
|
+
toProcess.push(req);
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
let selectedOverlays = Array.from(withDependencies);
|
|
530
|
+
// Check for conflicts and resolve
|
|
531
|
+
let hasConflicts = true;
|
|
532
|
+
while (hasConflicts) {
|
|
533
|
+
const conflicts = new Map();
|
|
534
|
+
// Find all conflicts
|
|
535
|
+
selectedOverlays.forEach((selectedId) => {
|
|
536
|
+
const overlay = allOverlaysMap.get(selectedId);
|
|
537
|
+
if (overlay?.conflicts) {
|
|
538
|
+
overlay.conflicts.forEach((conflictId) => {
|
|
539
|
+
if (selectedOverlays.includes(conflictId)) {
|
|
540
|
+
if (!conflicts.has(selectedId)) {
|
|
541
|
+
conflicts.set(selectedId, []);
|
|
542
|
+
}
|
|
543
|
+
conflicts.get(selectedId).push(conflictId);
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
if (conflicts.size === 0) {
|
|
549
|
+
hasConflicts = false;
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
// Show conflict resolution UI
|
|
553
|
+
console.log(chalk.yellow('\n⚠️ Conflicts detected in selection:\n'));
|
|
554
|
+
const conflictChoices = [];
|
|
555
|
+
conflicts.forEach((conflictingWith, overlayId) => {
|
|
556
|
+
const overlay = allOverlaysMap.get(overlayId);
|
|
557
|
+
const conflictNames = conflictingWith
|
|
558
|
+
.map((id) => allOverlaysMap.get(id)?.name)
|
|
559
|
+
.join(', ');
|
|
560
|
+
conflictChoices.push({
|
|
561
|
+
name: `Remove ${overlay.name}`,
|
|
562
|
+
value: overlayId,
|
|
563
|
+
description: `Conflicts with: ${conflictNames}`,
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
const toRemove = await checkbox({
|
|
567
|
+
message: 'Select overlays to remove to resolve conflicts:',
|
|
568
|
+
choices: conflictChoices,
|
|
569
|
+
pageSize: 15,
|
|
570
|
+
loop: false,
|
|
571
|
+
});
|
|
572
|
+
if (toRemove.length === 0) {
|
|
573
|
+
console.log(chalk.red('\n❌ You must remove at least one conflicting overlay'));
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
// Remove selected overlays
|
|
577
|
+
selectedOverlays = selectedOverlays.filter((id) => !toRemove.includes(id));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// Question 4: Container name
|
|
581
|
+
const containerName = await input({
|
|
582
|
+
message: 'Container/project name (optional):',
|
|
583
|
+
default: manifest?.containerName || '',
|
|
584
|
+
});
|
|
585
|
+
// Question 5: Output path
|
|
586
|
+
// If manifest provided, default to its location; otherwise use ./.devcontainer
|
|
587
|
+
const defaultOutput = manifestDir || './.devcontainer';
|
|
588
|
+
const outputPath = await input({
|
|
589
|
+
message: 'Output path:',
|
|
590
|
+
default: defaultOutput,
|
|
591
|
+
});
|
|
592
|
+
// Question 6: Port offset (optional, for running multiple instances)
|
|
593
|
+
const portOffsetInput = await input({
|
|
594
|
+
message: 'Port offset (leave empty for default ports, e.g., 100 to avoid conflicts):',
|
|
595
|
+
default: manifest?.portOffset ? String(manifest.portOffset) : '',
|
|
596
|
+
});
|
|
597
|
+
const portOffset = portOffsetInput ? parseInt(portOffsetInput, 10) : undefined;
|
|
598
|
+
// Parse selected overlays into categories
|
|
599
|
+
const overlayMap = new Map(config.overlays.map((o) => [o.id, o]));
|
|
600
|
+
const language = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'language');
|
|
601
|
+
const observability = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'observability');
|
|
602
|
+
const cloudTools = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'cloud');
|
|
603
|
+
const devTools = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'dev');
|
|
604
|
+
const database = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'database');
|
|
605
|
+
const playwright = selectedOverlays.includes('playwright');
|
|
606
|
+
return {
|
|
607
|
+
stack,
|
|
608
|
+
baseImage,
|
|
609
|
+
customImage,
|
|
610
|
+
containerName: containerName || undefined,
|
|
611
|
+
preset: selectedPresetId,
|
|
612
|
+
presetChoices: Object.keys(presetChoices).length > 0 ? presetChoices : undefined,
|
|
613
|
+
presetGlueConfig,
|
|
614
|
+
language,
|
|
615
|
+
needsDocker: stack === 'compose', // Compose template includes docker-outside-of-docker
|
|
616
|
+
database,
|
|
617
|
+
playwright,
|
|
618
|
+
cloudTools,
|
|
619
|
+
devTools,
|
|
620
|
+
observability,
|
|
621
|
+
outputPath,
|
|
622
|
+
portOffset,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
626
|
+
if (error.name === 'ExitPromptError') {
|
|
627
|
+
console.log('\n' + chalk.yellow('Cancelled by user'));
|
|
628
|
+
process.exit(0);
|
|
629
|
+
}
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Build partial answers from manifest
|
|
635
|
+
* Note: Categories are only used for UI/questionnaire grouping.
|
|
636
|
+
* The composer works with overlay IDs regardless of category.
|
|
637
|
+
*/
|
|
638
|
+
function buildAnswersFromManifest(manifest, manifestDir) {
|
|
639
|
+
const config = loadOverlaysConfigWrapper();
|
|
640
|
+
// Helper to categorize overlays by type (for QuestionnaireAnswers structure)
|
|
641
|
+
const categorizeOverlays = (overlayIds) => {
|
|
642
|
+
const language = [];
|
|
643
|
+
const database = [];
|
|
644
|
+
const observability = [];
|
|
645
|
+
const cloudTools = [];
|
|
646
|
+
const devTools = [];
|
|
647
|
+
// Build lookup map from unified overlays array
|
|
648
|
+
const overlayMap = new Map(config.overlays.map((o) => [o.id, o]));
|
|
649
|
+
// Categorize based on overlay metadata
|
|
650
|
+
for (const id of overlayIds) {
|
|
651
|
+
const overlay = overlayMap.get(id);
|
|
652
|
+
if (!overlay)
|
|
653
|
+
continue;
|
|
654
|
+
switch (overlay.category) {
|
|
655
|
+
case 'language':
|
|
656
|
+
language.push(id);
|
|
657
|
+
break;
|
|
658
|
+
case 'database':
|
|
659
|
+
database.push(id);
|
|
660
|
+
break;
|
|
661
|
+
case 'observability':
|
|
662
|
+
observability.push(id);
|
|
663
|
+
break;
|
|
664
|
+
case 'cloud':
|
|
665
|
+
cloudTools.push(id);
|
|
666
|
+
break;
|
|
667
|
+
case 'dev':
|
|
668
|
+
devTools.push(id);
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return { language, database, observability, cloudTools, devTools };
|
|
673
|
+
};
|
|
674
|
+
const categories = categorizeOverlays(manifest.overlays);
|
|
675
|
+
// Output path is always the directory containing the manifest
|
|
676
|
+
const outputPath = manifestDir || './.devcontainer';
|
|
677
|
+
// Handle baseImage - check if it's a known ID or a custom image string
|
|
678
|
+
const knownBaseImageIds = ['bookworm', 'trixie', 'alpine', 'ubuntu', 'custom'];
|
|
679
|
+
const isKnownBaseImage = knownBaseImageIds.includes(manifest.baseImage);
|
|
680
|
+
return {
|
|
681
|
+
stack: manifest.baseTemplate,
|
|
682
|
+
baseImage: isKnownBaseImage ? manifest.baseImage : 'custom',
|
|
683
|
+
customImage: isKnownBaseImage ? undefined : manifest.baseImage,
|
|
684
|
+
containerName: manifest.containerName,
|
|
685
|
+
preset: manifest.preset,
|
|
686
|
+
presetChoices: manifest.presetChoices,
|
|
687
|
+
...categories,
|
|
688
|
+
needsDocker: manifest.baseTemplate === 'compose',
|
|
689
|
+
playwright: categories.devTools.includes('playwright'),
|
|
690
|
+
outputPath,
|
|
691
|
+
portOffset: manifest.portOffset,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* Build partial answers from CLI arguments
|
|
696
|
+
*/
|
|
697
|
+
function buildAnswersFromCliArgs(config) {
|
|
698
|
+
const answers = {};
|
|
699
|
+
if (config.stack) {
|
|
700
|
+
answers.stack = config.stack;
|
|
701
|
+
answers.needsDocker = config.stack === 'compose';
|
|
702
|
+
}
|
|
703
|
+
if (config.baseImage)
|
|
704
|
+
answers.baseImage = config.baseImage;
|
|
705
|
+
if (config.containerName)
|
|
706
|
+
answers.containerName = config.containerName;
|
|
707
|
+
if (config.language)
|
|
708
|
+
answers.language = config.language;
|
|
709
|
+
if (config.database)
|
|
710
|
+
answers.database = config.database;
|
|
711
|
+
if (config.playwright !== undefined)
|
|
712
|
+
answers.playwright = config.playwright;
|
|
713
|
+
if (config.observability)
|
|
714
|
+
answers.observability = config.observability;
|
|
715
|
+
if (config.cloudTools)
|
|
716
|
+
answers.cloudTools = config.cloudTools;
|
|
717
|
+
if (config.devTools)
|
|
718
|
+
answers.devTools = config.devTools;
|
|
719
|
+
if (config.portOffset !== undefined)
|
|
720
|
+
answers.portOffset = config.portOffset;
|
|
721
|
+
if (config.outputPath)
|
|
722
|
+
answers.outputPath = config.outputPath;
|
|
723
|
+
if (config.preset)
|
|
724
|
+
answers.preset = config.preset;
|
|
725
|
+
if (config.presetChoices)
|
|
726
|
+
answers.presetChoices = config.presetChoices;
|
|
727
|
+
return answers;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Merge multiple partial answers with precedence: cli > interactive > manifest > defaults
|
|
731
|
+
*/
|
|
732
|
+
function mergeAnswers(...partials) {
|
|
733
|
+
const merged = {
|
|
734
|
+
language: [],
|
|
735
|
+
database: [],
|
|
736
|
+
cloudTools: [],
|
|
737
|
+
devTools: [],
|
|
738
|
+
observability: [],
|
|
739
|
+
playwright: false,
|
|
740
|
+
outputPath: './.devcontainer',
|
|
741
|
+
};
|
|
742
|
+
// Merge in order (later overrides earlier)
|
|
743
|
+
for (const partial of partials) {
|
|
744
|
+
if (!partial)
|
|
745
|
+
continue;
|
|
746
|
+
Object.keys(partial).forEach((key) => {
|
|
747
|
+
const value = partial[key];
|
|
748
|
+
if (value !== undefined && value !== null) {
|
|
749
|
+
// For arrays, prefer non-empty values
|
|
750
|
+
if (Array.isArray(value)) {
|
|
751
|
+
if (value.length > 0) {
|
|
752
|
+
merged[key] = value;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
merged[key] = value;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
// Ensure required fields have defaults
|
|
762
|
+
if (!merged.stack)
|
|
763
|
+
merged.stack = 'plain';
|
|
764
|
+
if (!merged.baseImage)
|
|
765
|
+
merged.baseImage = 'bookworm';
|
|
766
|
+
if (!merged.needsDocker && merged.stack) {
|
|
767
|
+
merged.needsDocker = merged.stack === 'compose';
|
|
768
|
+
}
|
|
769
|
+
return merged;
|
|
770
|
+
}
|
|
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
|
+
/**
|
|
923
|
+
* Parse CLI arguments
|
|
924
|
+
*/
|
|
925
|
+
async function parseCliArgs() {
|
|
926
|
+
const program = new Command();
|
|
927
|
+
// Store init options for access after parsing
|
|
928
|
+
let initOptions = null;
|
|
929
|
+
program
|
|
930
|
+
.name('container-superposition')
|
|
931
|
+
.description('Composable devcontainer scaffolds')
|
|
932
|
+
.version('0.1.0');
|
|
933
|
+
// Init command (default)
|
|
934
|
+
program
|
|
935
|
+
.command('init', { isDefault: true })
|
|
936
|
+
.description('Initialize a new devcontainer configuration')
|
|
937
|
+
.option('--from-manifest <path>', 'Load configuration from existing superposition.json manifest')
|
|
938
|
+
.option('--no-interactive', 'Use manifest values directly without questionnaire (requires --from-manifest)')
|
|
939
|
+
.option('--no-backup', 'Skip creating backup before regeneration')
|
|
940
|
+
.option('--backup-dir <path>', 'Custom backup directory location')
|
|
941
|
+
.option('--stack <type>', 'Base template: plain, compose')
|
|
942
|
+
.option('--language <list>', 'Comma-separated language overlays: dotnet, nodejs, python, mkdocs, java, go, rust, bun, powershell')
|
|
943
|
+
.option('--database <list>', 'Comma-separated database overlays: postgres, redis, mongodb, mysql, sqlserver, sqlite, minio, rabbitmq, redpanda, nats')
|
|
944
|
+
.option('--observability <list>', 'Comma-separated: otel-collector, jaeger, prometheus, grafana, loki')
|
|
945
|
+
.option('--playwright', 'Include Playwright browser automation')
|
|
946
|
+
.option('--cloud-tools <list>', 'Comma-separated: aws-cli, azure-cli, gcloud, kubectl-helm, terraform, pulumi')
|
|
947
|
+
.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
|
+
.option('--port-offset <number>', 'Add offset to all exposed ports (e.g., 100 makes Grafana 3100 instead of 3000)')
|
|
949
|
+
.option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
|
|
950
|
+
.action((options) => {
|
|
951
|
+
// Store options for main() to process
|
|
952
|
+
initOptions = options;
|
|
953
|
+
});
|
|
954
|
+
// Regen command
|
|
955
|
+
program
|
|
956
|
+
.command('regen')
|
|
957
|
+
.description('Regenerate devcontainer from existing superposition.json manifest')
|
|
958
|
+
.option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
|
|
959
|
+
.option('--no-backup', 'Skip creating backup before regeneration')
|
|
960
|
+
.option('--backup-dir <path>', 'Custom backup directory location')
|
|
961
|
+
.action((options) => {
|
|
962
|
+
const outputPath = options.output || './.devcontainer';
|
|
963
|
+
const manifestPath = path.join(outputPath, 'superposition.json');
|
|
964
|
+
if (!fs.existsSync(manifestPath)) {
|
|
965
|
+
console.error(chalk.red(`✗ Error: No manifest found at ${manifestPath}`));
|
|
966
|
+
console.error(chalk.gray(' Run "container-superposition init" first to create a configuration'));
|
|
967
|
+
process.exit(1);
|
|
968
|
+
}
|
|
969
|
+
// Store options for main() to process
|
|
970
|
+
initOptions = {
|
|
971
|
+
...options,
|
|
972
|
+
fromManifest: manifestPath,
|
|
973
|
+
interactive: false,
|
|
974
|
+
};
|
|
975
|
+
});
|
|
976
|
+
// List command
|
|
977
|
+
program
|
|
978
|
+
.command('list')
|
|
979
|
+
.description('List available overlays and presets')
|
|
980
|
+
.option('--category <type>', 'Filter by category: language, database, observability, cloud, dev, preset')
|
|
981
|
+
.action(async (options) => {
|
|
982
|
+
await listOverlays(options);
|
|
983
|
+
});
|
|
984
|
+
// Doctor command
|
|
985
|
+
program
|
|
986
|
+
.command('doctor')
|
|
987
|
+
.description('Check environment and validate configuration')
|
|
988
|
+
.option('-o, --output <path>', 'Devcontainer path to validate (default: ./.devcontainer)')
|
|
989
|
+
.action(async (options) => {
|
|
990
|
+
await runDoctor(options);
|
|
991
|
+
});
|
|
992
|
+
await program.parseAsync(process.argv);
|
|
993
|
+
// If init or regen command was run, return the options
|
|
994
|
+
if (!initOptions) {
|
|
995
|
+
// No init/regen command run (list or doctor ran instead)
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
// If no options provided to init, return null to trigger interactive mode
|
|
999
|
+
if (Object.keys(initOptions).length === 0) {
|
|
1000
|
+
return null;
|
|
1001
|
+
}
|
|
1002
|
+
const config = {};
|
|
1003
|
+
if (initOptions.stack)
|
|
1004
|
+
config.stack = initOptions.stack;
|
|
1005
|
+
if (initOptions.language) {
|
|
1006
|
+
config.language = initOptions.language
|
|
1007
|
+
.split(',')
|
|
1008
|
+
.map((l) => l.trim());
|
|
1009
|
+
}
|
|
1010
|
+
if (initOptions.database) {
|
|
1011
|
+
config.database = initOptions.database
|
|
1012
|
+
.split(',')
|
|
1013
|
+
.map((d) => d.trim());
|
|
1014
|
+
}
|
|
1015
|
+
if (initOptions.observability) {
|
|
1016
|
+
config.observability = initOptions.observability
|
|
1017
|
+
.split(',')
|
|
1018
|
+
.map((t) => t.trim());
|
|
1019
|
+
}
|
|
1020
|
+
if (initOptions.playwright)
|
|
1021
|
+
config.playwright = true;
|
|
1022
|
+
if (initOptions.cloudTools) {
|
|
1023
|
+
config.cloudTools = initOptions.cloudTools
|
|
1024
|
+
.split(',')
|
|
1025
|
+
.map((t) => t.trim());
|
|
1026
|
+
}
|
|
1027
|
+
if (initOptions.devTools) {
|
|
1028
|
+
config.devTools = initOptions.devTools.split(',').map((t) => t.trim());
|
|
1029
|
+
}
|
|
1030
|
+
if (initOptions.portOffset) {
|
|
1031
|
+
config.portOffset = parseInt(initOptions.portOffset, 10);
|
|
1032
|
+
}
|
|
1033
|
+
if (initOptions.output)
|
|
1034
|
+
config.outputPath = initOptions.output;
|
|
1035
|
+
return {
|
|
1036
|
+
config,
|
|
1037
|
+
manifestPath: initOptions.fromManifest,
|
|
1038
|
+
noBackup: initOptions.backup === false, // Commander creates options.backup = false for --no-backup
|
|
1039
|
+
backupDir: initOptions.backupDir,
|
|
1040
|
+
noInteractive: initOptions.interactive === false, // Commander creates options.interactive = false for --no-interactive
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
async function main() {
|
|
1044
|
+
try {
|
|
1045
|
+
const cliArgs = await parseCliArgs();
|
|
1046
|
+
// Validate --no-interactive requires --from-manifest
|
|
1047
|
+
if (cliArgs?.noInteractive && !cliArgs?.manifestPath) {
|
|
1048
|
+
console.error(chalk.red('✗ Error: --no-interactive requires --from-manifest'));
|
|
1049
|
+
console.error(chalk.dim(' Use both flags together: --from-manifest <path> --no-interactive'));
|
|
1050
|
+
process.exit(1);
|
|
1051
|
+
}
|
|
1052
|
+
let manifest;
|
|
1053
|
+
let manifestDir;
|
|
1054
|
+
let shouldBackup = true;
|
|
1055
|
+
let backupDir;
|
|
1056
|
+
let useManifestOnly = false;
|
|
1057
|
+
// Handle manifest loading
|
|
1058
|
+
if (cliArgs?.manifestPath) {
|
|
1059
|
+
const manifestPath = findManifestFile(cliArgs.manifestPath);
|
|
1060
|
+
if (!manifestPath) {
|
|
1061
|
+
console.error(chalk.red('✗ Could not find manifest file'));
|
|
1062
|
+
console.error(chalk.red(` Searched for: ${cliArgs.manifestPath}`));
|
|
1063
|
+
process.exit(1);
|
|
1064
|
+
}
|
|
1065
|
+
manifestDir = path.dirname(manifestPath);
|
|
1066
|
+
const loadedManifest = loadManifest(manifestPath);
|
|
1067
|
+
if (!loadedManifest) {
|
|
1068
|
+
process.exit(1);
|
|
1069
|
+
}
|
|
1070
|
+
manifest = loadedManifest;
|
|
1071
|
+
// Check for backup and interaction options
|
|
1072
|
+
if (cliArgs.noBackup) {
|
|
1073
|
+
shouldBackup = false;
|
|
1074
|
+
}
|
|
1075
|
+
if (cliArgs.backupDir) {
|
|
1076
|
+
backupDir = cliArgs.backupDir;
|
|
1077
|
+
}
|
|
1078
|
+
if (cliArgs.noInteractive) {
|
|
1079
|
+
useManifestOnly = true;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
// Create backup if needed
|
|
1083
|
+
if (shouldBackup && manifest) {
|
|
1084
|
+
// Output path is the directory containing the manifest
|
|
1085
|
+
const outputPath = manifestDir || './.devcontainer';
|
|
1086
|
+
const backupPath = await createBackup(outputPath, backupDir);
|
|
1087
|
+
if (backupPath) {
|
|
1088
|
+
console.log(chalk.green(`✓ Backup created: ${backupPath}\n`));
|
|
1089
|
+
await ensureBackupPatternsInGitignore(outputPath);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
// Build answers based on mode
|
|
1093
|
+
let answers;
|
|
1094
|
+
if (useManifestOnly && manifest) {
|
|
1095
|
+
// Mode 1: Manifest-only (--from-manifest --no-interactive)
|
|
1096
|
+
const manifestAnswers = buildAnswersFromManifest(manifest, manifestDir);
|
|
1097
|
+
answers = mergeAnswers(manifestAnswers);
|
|
1098
|
+
console.log('\n' +
|
|
1099
|
+
boxen(chalk.bold.cyan('Regenerating from Manifest (No Interactive)\n\n') +
|
|
1100
|
+
chalk.white('Configuration:\n') +
|
|
1101
|
+
chalk.gray(` Template: ${manifest.baseTemplate}\n`) +
|
|
1102
|
+
chalk.gray(` Base Image: ${manifest.baseImage}\n`) +
|
|
1103
|
+
(manifest.containerName
|
|
1104
|
+
? chalk.gray(` Container: ${manifest.containerName}\n`)
|
|
1105
|
+
: '') +
|
|
1106
|
+
chalk.gray(` Overlays: ${manifest.overlays.join(', ')}\n`) +
|
|
1107
|
+
(manifest.preset ? chalk.gray(` Preset: ${manifest.preset}\n`) : '') +
|
|
1108
|
+
(manifest.portOffset
|
|
1109
|
+
? chalk.gray(` Port offset: ${manifest.portOffset}\n`)
|
|
1110
|
+
: '') +
|
|
1111
|
+
chalk.gray(` Output: ${answers.outputPath}`), { padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: 1 }));
|
|
1112
|
+
}
|
|
1113
|
+
else if (cliArgs && cliArgs.config.stack) {
|
|
1114
|
+
// Mode 2: CLI-based (with optional manifest defaults)
|
|
1115
|
+
const cliAnswers = buildAnswersFromCliArgs(cliArgs.config);
|
|
1116
|
+
const manifestAnswers = manifest
|
|
1117
|
+
? buildAnswersFromManifest(manifest, manifestDir)
|
|
1118
|
+
: undefined;
|
|
1119
|
+
answers = mergeAnswers(manifestAnswers, cliAnswers, {
|
|
1120
|
+
outputPath: cliAnswers.outputPath || './.devcontainer',
|
|
1121
|
+
});
|
|
1122
|
+
console.log('\n' +
|
|
1123
|
+
boxen(chalk.bold('Running in CLI mode'), {
|
|
1124
|
+
padding: 0.5,
|
|
1125
|
+
borderColor: 'blue',
|
|
1126
|
+
borderStyle: 'round',
|
|
1127
|
+
}));
|
|
1128
|
+
}
|
|
1129
|
+
else {
|
|
1130
|
+
// Mode 3: Interactive (with optional manifest pre-population)
|
|
1131
|
+
const interactiveAnswers = await runQuestionnaire(manifest, manifestDir);
|
|
1132
|
+
answers = mergeAnswers(interactiveAnswers);
|
|
1133
|
+
}
|
|
1134
|
+
// Show configuration summary
|
|
1135
|
+
const summaryLines = [
|
|
1136
|
+
chalk.bold.white('Configuration Summary\n'),
|
|
1137
|
+
chalk.cyan('Base: ') + chalk.white(answers.stack),
|
|
1138
|
+
];
|
|
1139
|
+
if (answers.language && answers.language.length > 0) {
|
|
1140
|
+
summaryLines.push(chalk.cyan('Languages: ') + chalk.white(answers.language.join(', ')));
|
|
1141
|
+
}
|
|
1142
|
+
if (answers.database && answers.database.length > 0) {
|
|
1143
|
+
summaryLines.push(chalk.cyan('Database: ') + chalk.white(answers.database.join(', ')));
|
|
1144
|
+
}
|
|
1145
|
+
summaryLines.push(chalk.cyan('Playwright: ') + chalk.white(answers.playwright ? 'Yes' : 'No'));
|
|
1146
|
+
if (answers.observability && answers.observability.length > 0) {
|
|
1147
|
+
summaryLines.push(chalk.cyan('Observability: ') + chalk.white(answers.observability.join(', ')));
|
|
1148
|
+
}
|
|
1149
|
+
if (answers.cloudTools && answers.cloudTools.length > 0) {
|
|
1150
|
+
summaryLines.push(chalk.cyan('Cloud tools: ') + chalk.white(answers.cloudTools.join(', ')));
|
|
1151
|
+
}
|
|
1152
|
+
summaryLines.push(chalk.cyan('Output: ') + chalk.white(answers.outputPath));
|
|
1153
|
+
console.log('\n' +
|
|
1154
|
+
boxen(summaryLines.join('\n'), {
|
|
1155
|
+
padding: 1,
|
|
1156
|
+
borderColor: 'green',
|
|
1157
|
+
borderStyle: 'round',
|
|
1158
|
+
margin: { top: 0, bottom: 1 },
|
|
1159
|
+
}));
|
|
1160
|
+
// Generate with spinner
|
|
1161
|
+
const spinner = ora({
|
|
1162
|
+
text: chalk.cyan('Generating devcontainer configuration...'),
|
|
1163
|
+
color: 'cyan',
|
|
1164
|
+
}).start();
|
|
1165
|
+
try {
|
|
1166
|
+
await composeDevContainer(answers);
|
|
1167
|
+
spinner.succeed(chalk.green('DevContainer created successfully!'));
|
|
1168
|
+
}
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
spinner.fail(chalk.red('Failed to create devcontainer'));
|
|
1171
|
+
throw error;
|
|
1172
|
+
}
|
|
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
|
+
}
|
|
1182
|
+
catch (error) {
|
|
1183
|
+
console.error('\n' +
|
|
1184
|
+
boxen(chalk.bold.red('Error\n\n') +
|
|
1185
|
+
chalk.white(error instanceof Error ? error.message : String(error)), { padding: 1, borderColor: 'red', borderStyle: 'round' }));
|
|
1186
|
+
process.exit(1);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
main();
|
|
1190
|
+
//# sourceMappingURL=init.js.map
|