container-superposition 0.1.1 → 0.1.3
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 +206 -1
- package/dist/scripts/init.js +235 -179
- package/dist/scripts/init.js.map +1 -1
- package/dist/tool/commands/doctor.d.ts +15 -0
- package/dist/tool/commands/doctor.d.ts.map +1 -0
- package/dist/tool/commands/doctor.js +862 -0
- package/dist/tool/commands/doctor.js.map +1 -0
- package/dist/tool/commands/explain.d.ts +13 -0
- package/dist/tool/commands/explain.d.ts.map +1 -0
- package/dist/tool/commands/explain.js +211 -0
- package/dist/tool/commands/explain.js.map +1 -0
- package/dist/tool/commands/list.d.ts +16 -0
- package/dist/tool/commands/list.d.ts.map +1 -0
- package/dist/tool/commands/list.js +121 -0
- package/dist/tool/commands/list.js.map +1 -0
- package/dist/tool/commands/plan.d.ts +16 -0
- package/dist/tool/commands/plan.d.ts.map +1 -0
- package/dist/tool/commands/plan.js +329 -0
- package/dist/tool/commands/plan.js.map +1 -0
- package/dist/tool/questionnaire/composer.d.ts +6 -1
- package/dist/tool/questionnaire/composer.d.ts.map +1 -1
- package/dist/tool/questionnaire/composer.js +300 -202
- package/dist/tool/questionnaire/composer.js.map +1 -1
- package/dist/tool/readme/markdown-parser.d.ts.map +1 -1
- package/dist/tool/readme/markdown-parser.js.map +1 -1
- package/dist/tool/readme/readme-generator.d.ts.map +1 -1
- package/dist/tool/readme/readme-generator.js +11 -6
- package/dist/tool/readme/readme-generator.js.map +1 -1
- package/dist/tool/schema/deployment-targets.d.ts +77 -0
- package/dist/tool/schema/deployment-targets.d.ts.map +1 -0
- package/dist/tool/schema/deployment-targets.js +91 -0
- package/dist/tool/schema/deployment-targets.js.map +1 -0
- package/dist/tool/schema/manifest-migrations.d.ts +51 -0
- package/dist/tool/schema/manifest-migrations.d.ts.map +1 -0
- package/dist/tool/schema/manifest-migrations.js +159 -0
- package/dist/tool/schema/manifest-migrations.js.map +1 -0
- package/dist/tool/schema/overlay-loader.d.ts +1 -1
- package/dist/tool/schema/overlay-loader.d.ts.map +1 -1
- package/dist/tool/schema/overlay-loader.js +42 -14
- package/dist/tool/schema/overlay-loader.js.map +1 -1
- package/dist/tool/schema/types.d.ts +44 -2
- package/dist/tool/schema/types.d.ts.map +1 -1
- package/dist/tool/utils/merge.d.ts +134 -0
- package/dist/tool/utils/merge.d.ts.map +1 -0
- package/dist/tool/utils/merge.js +277 -0
- package/dist/tool/utils/merge.js.map +1 -0
- package/dist/tool/utils/port-utils.d.ts +29 -0
- package/dist/tool/utils/port-utils.d.ts.map +1 -0
- package/dist/tool/utils/port-utils.js +128 -0
- package/dist/tool/utils/port-utils.js.map +1 -0
- package/dist/tool/utils/version.d.ts +9 -0
- package/dist/tool/utils/version.d.ts.map +1 -0
- package/dist/tool/utils/version.js +32 -0
- package/dist/tool/utils/version.js.map +1 -0
- package/docs/architecture.md +25 -21
- package/docs/deployment-targets.md +150 -0
- package/docs/discovery-commands.md +442 -0
- package/docs/merge-strategy.md +700 -0
- package/docs/minimal-and-editor.md +265 -0
- package/docs/overlay-imports.md +209 -0
- package/docs/overlay-manifest-refactoring.md +2 -2
- package/docs/overlay-metadata-archive.md +1 -1
- package/docs/overlays.md +91 -23
- package/docs/presets-architecture.md +3 -3
- package/docs/presets.md +1 -1
- package/docs/publishing.md +36 -35
- package/docs/team-workflow.md +540 -0
- package/overlays/.presets/data-engineering.yml +392 -0
- package/overlays/.presets/event-sourced-service.yml +262 -0
- package/overlays/.presets/frontend.yml +287 -0
- package/overlays/.presets/k8s-operator-dev.yml +462 -0
- package/overlays/.registry/README.md +1 -1
- package/overlays/.registry/deployment-targets.yml +54 -0
- package/overlays/.shared/README.md +43 -0
- package/overlays/.shared/compose/common-healthchecks.yml +38 -0
- package/overlays/.shared/otel/instrumentation.env +20 -0
- package/overlays/.shared/otel/otel-base-config.yaml +30 -0
- package/overlays/.shared/vscode/recommended-extensions.json +14 -0
- package/overlays/README.md +1 -1
- package/overlays/codex/overlay.yml +1 -0
- package/overlays/duckdb/README.md +274 -0
- package/overlays/duckdb/devcontainer.patch.json +10 -0
- package/overlays/duckdb/overlay.yml +17 -0
- package/overlays/duckdb/setup.sh +45 -0
- package/overlays/duckdb/verify.sh +32 -0
- package/overlays/git-helpers/overlay.yml +1 -0
- package/overlays/grafana/README.md +5 -5
- package/overlays/grafana/dashboard-provider.yml +1 -1
- package/overlays/grafana/docker-compose.yml +2 -2
- package/overlays/grafana/overlay.yml +6 -1
- package/overlays/jaeger/overlay.yml +16 -3
- package/overlays/jupyter/.env.example +6 -0
- package/overlays/jupyter/README.md +210 -0
- package/overlays/jupyter/devcontainer.patch.json +14 -0
- package/overlays/jupyter/docker-compose.yml +23 -0
- package/overlays/jupyter/overlay.yml +18 -0
- package/overlays/jupyter/verify.sh +35 -0
- package/overlays/kind/README.md +221 -0
- package/overlays/kind/devcontainer.patch.json +10 -0
- package/overlays/kind/overlay.yml +18 -0
- package/overlays/kind/setup.sh +43 -0
- package/overlays/kind/verify.sh +40 -0
- package/overlays/localstack/.env.example +6 -0
- package/overlays/localstack/README.md +188 -0
- package/overlays/localstack/devcontainer.patch.json +21 -0
- package/overlays/localstack/docker-compose.yml +25 -0
- package/overlays/localstack/overlay.yml +18 -0
- package/overlays/localstack/verify.sh +47 -0
- package/overlays/loki/overlay.yml +6 -1
- package/overlays/modern-cli-tools/overlay.yml +1 -0
- package/overlays/mongodb/overlay.yml +12 -2
- package/overlays/mysql/overlay.yml +12 -2
- package/overlays/nats/overlay.yml +12 -2
- package/overlays/openapi-tools/README.md +243 -0
- package/overlays/openapi-tools/devcontainer.patch.json +10 -0
- package/overlays/openapi-tools/overlay.yml +16 -0
- package/overlays/openapi-tools/setup.sh +45 -0
- package/overlays/openapi-tools/verify.sh +51 -0
- package/overlays/otel-collector/overlay.yml.example +26 -0
- package/overlays/postgres/overlay.yml +6 -1
- package/overlays/prometheus/overlay.yml +6 -1
- package/overlays/rabbitmq/overlay.yml +12 -2
- package/overlays/redis/overlay.yml +6 -1
- package/overlays/tilt/README.md +259 -0
- package/overlays/tilt/devcontainer.patch.json +17 -0
- package/overlays/tilt/overlay.yml +19 -0
- package/overlays/tilt/setup.sh +25 -0
- package/overlays/tilt/verify.sh +24 -0
- package/package.json +8 -6
- package/tool/README.md +12 -16
- package/tool/schema/overlay-manifest.schema.json +64 -4
- package/tool/schema/superposition-manifest.schema.json +104 -0
- /package/overlays/{presets → .presets}/docs-site.yml +0 -0
- /package/overlays/{presets → .presets}/fullstack.yml +0 -0
- /package/overlays/{presets → .presets}/microservice.yml +0 -0
- /package/overlays/{presets → .presets}/web-api.yml +0 -0
package/dist/scripts/init.js
CHANGED
|
@@ -8,8 +8,15 @@ import boxen from 'boxen';
|
|
|
8
8
|
import ora from 'ora';
|
|
9
9
|
import { select, checkbox, input } from '@inquirer/prompts';
|
|
10
10
|
import yaml from 'js-yaml';
|
|
11
|
-
import { composeDevContainer } from '../tool/questionnaire/composer.js';
|
|
11
|
+
import { composeDevContainer, generateManifestOnly } from '../tool/questionnaire/composer.js';
|
|
12
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 { getIncompatibleOverlays, DEPLOYMENT_TARGETS } from '../tool/schema/deployment-targets.js';
|
|
18
|
+
import { migrateManifest, needsMigration, detectManifestVersion, isVersionSupported, CURRENT_MANIFEST_VERSION, SUPPORTED_MANIFEST_VERSIONS, } from '../tool/schema/manifest-migrations.js';
|
|
19
|
+
import { getToolVersion } from '../tool/utils/version.js';
|
|
13
20
|
// Get __dirname equivalent in ESM
|
|
14
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
22
|
const __dirname = path.dirname(__filename);
|
|
@@ -32,8 +39,8 @@ const OVERLAYS_CONFIG_CANDIDATES = [
|
|
|
32
39
|
const OVERLAYS_CONFIG_PATH = OVERLAYS_CONFIG_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
|
|
33
40
|
OVERLAYS_CONFIG_CANDIDATES[0];
|
|
34
41
|
const PRESETS_DIR_CANDIDATES = [
|
|
35
|
-
path.join(__dirname, '..', 'overlays', 'presets'),
|
|
36
|
-
path.join(__dirname, '..', '..', 'overlays', 'presets'),
|
|
42
|
+
path.join(__dirname, '..', 'overlays', '.presets'),
|
|
43
|
+
path.join(__dirname, '..', '..', 'overlays', '.presets'),
|
|
37
44
|
];
|
|
38
45
|
const PRESETS_DIR = PRESETS_DIR_CANDIDATES.find((candidate) => fs.existsSync(candidate)) ??
|
|
39
46
|
PRESETS_DIR_CANDIDATES[0];
|
|
@@ -111,10 +118,29 @@ function findManifestFile(manifestPath) {
|
|
|
111
118
|
function loadManifest(manifestPath) {
|
|
112
119
|
try {
|
|
113
120
|
const content = fs.readFileSync(manifestPath, 'utf-8');
|
|
114
|
-
const
|
|
121
|
+
const rawManifest = JSON.parse(content);
|
|
122
|
+
// Detect manifest version
|
|
123
|
+
const detectedVersion = detectManifestVersion(rawManifest);
|
|
124
|
+
// Check if version is supported
|
|
125
|
+
if (!isVersionSupported(detectedVersion)) {
|
|
126
|
+
console.error(chalk.red(`✗ Manifest version ${detectedVersion} is not supported.\n` +
|
|
127
|
+
` This tool supports versions: ${SUPPORTED_MANIFEST_VERSIONS.join(', ')}\n` +
|
|
128
|
+
` Please upgrade your tool or regenerate the manifest.`));
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
// Migrate if needed
|
|
132
|
+
let manifest;
|
|
133
|
+
if (needsMigration(rawManifest)) {
|
|
134
|
+
const oldVersion = rawManifest.manifestVersion || rawManifest.version || 'legacy';
|
|
135
|
+
console.log(chalk.cyan(`ℹ️ Migrating manifest from version ${oldVersion} to ${CURRENT_MANIFEST_VERSION}...`));
|
|
136
|
+
manifest = migrateManifest(rawManifest);
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
manifest = rawManifest;
|
|
140
|
+
}
|
|
115
141
|
// Basic validation
|
|
116
|
-
if (!manifest.
|
|
117
|
-
console.error(chalk.red('✗ Invalid manifest format: missing required
|
|
142
|
+
if (!manifest.baseTemplate) {
|
|
143
|
+
console.error(chalk.red('✗ Invalid manifest format: missing required field "baseTemplate"'));
|
|
118
144
|
return null;
|
|
119
145
|
}
|
|
120
146
|
if (!Array.isArray(manifest.overlays)) {
|
|
@@ -125,10 +151,6 @@ function loadManifest(manifestPath) {
|
|
|
125
151
|
console.error(chalk.red('✗ Invalid manifest format: all "overlays" entries must be strings'));
|
|
126
152
|
return null;
|
|
127
153
|
}
|
|
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
154
|
return manifest;
|
|
133
155
|
}
|
|
134
156
|
catch (error) {
|
|
@@ -603,6 +625,73 @@ async function runQuestionnaire(manifest, manifestDir) {
|
|
|
603
625
|
const devTools = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'dev');
|
|
604
626
|
const database = selectedOverlays.filter((o) => overlayMap.get(o)?.category === 'database');
|
|
605
627
|
const playwright = selectedOverlays.includes('playwright');
|
|
628
|
+
// Check for deployment target compatibility
|
|
629
|
+
let target;
|
|
630
|
+
// Check if any incompatible overlays selected
|
|
631
|
+
const incompatibleOverlays = getIncompatibleOverlays(selectedOverlays, undefined);
|
|
632
|
+
if (incompatibleOverlays.length > 0) {
|
|
633
|
+
console.log(chalk.yellow('\n⚠️ Deployment Target Compatibility Check:\n'));
|
|
634
|
+
console.log(chalk.gray('Some selected overlays may not work in all environments.'));
|
|
635
|
+
console.log();
|
|
636
|
+
// Show incompatibilities
|
|
637
|
+
for (const { overlay, alternatives } of incompatibleOverlays) {
|
|
638
|
+
const overlayMeta = allOverlaysMap.get(overlay);
|
|
639
|
+
console.log(chalk.yellow(` • ${overlayMeta?.name || overlay}`));
|
|
640
|
+
console.log(chalk.gray(` Not compatible with: ${DEPLOYMENT_TARGETS.codespaces.name}, ${DEPLOYMENT_TARGETS.gitpod.name}`));
|
|
641
|
+
if (alternatives.length > 0) {
|
|
642
|
+
const altNames = alternatives
|
|
643
|
+
.map((id) => allOverlaysMap.get(id)?.name || id)
|
|
644
|
+
.join(', ');
|
|
645
|
+
console.log(chalk.cyan(` Alternatives: ${altNames}`));
|
|
646
|
+
}
|
|
647
|
+
console.log();
|
|
648
|
+
}
|
|
649
|
+
const targetChoice = (await select({
|
|
650
|
+
message: 'Which environment are you targeting?',
|
|
651
|
+
choices: [
|
|
652
|
+
{
|
|
653
|
+
name: '🖥️ Local Development (Docker Desktop)',
|
|
654
|
+
value: 'local',
|
|
655
|
+
description: 'Running on your local machine - supports all overlays',
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
name: '☁️ GitHub Codespaces',
|
|
659
|
+
value: 'codespaces',
|
|
660
|
+
description: 'Cloud development - may require docker-in-docker',
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
name: '🌐 Gitpod',
|
|
664
|
+
value: 'gitpod',
|
|
665
|
+
description: 'Cloud development - may require docker-in-docker',
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
name: '📦 DevPod',
|
|
669
|
+
value: 'devpod',
|
|
670
|
+
description: 'Client-only dev environments',
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
default: 'local',
|
|
674
|
+
}));
|
|
675
|
+
target = targetChoice;
|
|
676
|
+
// Show specific incompatibilities for selected target
|
|
677
|
+
if (target !== 'local') {
|
|
678
|
+
const targetIncompatible = getIncompatibleOverlays(selectedOverlays, target);
|
|
679
|
+
if (targetIncompatible.length > 0) {
|
|
680
|
+
console.log(chalk.yellow(`\n⚠️ Warning: Some overlays won't work in ${DEPLOYMENT_TARGETS[target].name}:\n`));
|
|
681
|
+
for (const { overlay, alternatives } of targetIncompatible) {
|
|
682
|
+
const overlayMeta = allOverlaysMap.get(overlay);
|
|
683
|
+
console.log(chalk.red(` ✗ ${overlayMeta?.name || overlay}`));
|
|
684
|
+
if (alternatives.length > 0) {
|
|
685
|
+
const altNames = alternatives
|
|
686
|
+
.map((id) => allOverlaysMap.get(id)?.name || id)
|
|
687
|
+
.join(', ');
|
|
688
|
+
console.log(chalk.cyan(` → Recommended: ${altNames}`));
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
console.log();
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
606
695
|
return {
|
|
607
696
|
stack,
|
|
608
697
|
baseImage,
|
|
@@ -620,6 +709,7 @@ async function runQuestionnaire(manifest, manifestDir) {
|
|
|
620
709
|
observability,
|
|
621
710
|
outputPath,
|
|
622
711
|
portOffset,
|
|
712
|
+
target,
|
|
623
713
|
};
|
|
624
714
|
}
|
|
625
715
|
catch (error) {
|
|
@@ -724,6 +814,12 @@ function buildAnswersFromCliArgs(config) {
|
|
|
724
814
|
answers.preset = config.preset;
|
|
725
815
|
if (config.presetChoices)
|
|
726
816
|
answers.presetChoices = config.presetChoices;
|
|
817
|
+
if (config.target)
|
|
818
|
+
answers.target = config.target;
|
|
819
|
+
if (config.minimal !== undefined)
|
|
820
|
+
answers.minimal = config.minimal;
|
|
821
|
+
if (config.editor)
|
|
822
|
+
answers.editor = config.editor;
|
|
727
823
|
return answers;
|
|
728
824
|
}
|
|
729
825
|
/**
|
|
@@ -768,157 +864,6 @@ function mergeAnswers(...partials) {
|
|
|
768
864
|
}
|
|
769
865
|
return merged;
|
|
770
866
|
}
|
|
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
867
|
/**
|
|
923
868
|
* Parse CLI arguments
|
|
924
869
|
*/
|
|
@@ -929,7 +874,7 @@ async function parseCliArgs() {
|
|
|
929
874
|
program
|
|
930
875
|
.name('container-superposition')
|
|
931
876
|
.description('Composable devcontainer scaffolds')
|
|
932
|
-
.version(
|
|
877
|
+
.version(getToolVersion());
|
|
933
878
|
// Init command (default)
|
|
934
879
|
program
|
|
935
880
|
.command('init', { isDefault: true })
|
|
@@ -946,7 +891,11 @@ async function parseCliArgs() {
|
|
|
946
891
|
.option('--cloud-tools <list>', 'Comma-separated: aws-cli, azure-cli, gcloud, kubectl-helm, terraform, pulumi')
|
|
947
892
|
.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
893
|
.option('--port-offset <number>', 'Add offset to all exposed ports (e.g., 100 makes Grafana 3100 instead of 3000)')
|
|
894
|
+
.option('--target <environment>', 'Deployment target: local, codespaces, gitpod, devpod (optimizes for environment)', 'local')
|
|
895
|
+
.option('--minimal', 'Minimal mode - exclude optional/nice-to-have features and extensions')
|
|
896
|
+
.option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
|
|
949
897
|
.option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
|
|
898
|
+
.option('--write-manifest-only', 'Generate only superposition.json manifest without creating .devcontainer/ files')
|
|
950
899
|
.action((options) => {
|
|
951
900
|
// Store options for main() to process
|
|
952
901
|
initOptions = options;
|
|
@@ -958,12 +907,29 @@ async function parseCliArgs() {
|
|
|
958
907
|
.option('-o, --output <path>', 'Output path (default: ./.devcontainer)')
|
|
959
908
|
.option('--no-backup', 'Skip creating backup before regeneration')
|
|
960
909
|
.option('--backup-dir <path>', 'Custom backup directory location')
|
|
910
|
+
.option('--minimal', 'Minimal mode - exclude optional/nice-to-have features and extensions')
|
|
911
|
+
.option('--editor <profile>', 'Editor profile: vscode (default), jetbrains, none', 'vscode')
|
|
961
912
|
.action((options) => {
|
|
962
913
|
const outputPath = options.output || './.devcontainer';
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
914
|
+
// Look for manifest in multiple locations for team workflow:
|
|
915
|
+
// 1. Current directory (./superposition.json) - team workflow
|
|
916
|
+
// 2. Output directory (e.g., ./.devcontainer/superposition.json) - legacy
|
|
917
|
+
const manifestSearchPaths = [
|
|
918
|
+
'superposition.json',
|
|
919
|
+
path.join(outputPath, 'superposition.json'),
|
|
920
|
+
];
|
|
921
|
+
let manifestPath = null;
|
|
922
|
+
for (const searchPath of manifestSearchPaths) {
|
|
923
|
+
if (fs.existsSync(searchPath)) {
|
|
924
|
+
manifestPath = searchPath;
|
|
925
|
+
break;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (!manifestPath) {
|
|
929
|
+
console.error(chalk.red(`✗ Error: No manifest found`));
|
|
930
|
+
console.error(chalk.gray(' Searched for: ./superposition.json, ' +
|
|
931
|
+
path.join(outputPath, 'superposition.json')));
|
|
932
|
+
console.error(chalk.gray(' Run "container-superposition init --write-manifest-only" to create a manifest'));
|
|
967
933
|
process.exit(1);
|
|
968
934
|
}
|
|
969
935
|
// Store options for main() to process
|
|
@@ -978,16 +944,47 @@ async function parseCliArgs() {
|
|
|
978
944
|
.command('list')
|
|
979
945
|
.description('List available overlays and presets')
|
|
980
946
|
.option('--category <type>', 'Filter by category: language, database, observability, cloud, dev, preset')
|
|
947
|
+
.option('--tags <list>', 'Filter by tags (comma-separated)')
|
|
948
|
+
.option('--supports <stack>', 'Filter by stack support: plain, compose')
|
|
949
|
+
.option('--json', 'Output as JSON for scripting')
|
|
981
950
|
.action(async (options) => {
|
|
982
|
-
|
|
951
|
+
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
952
|
+
await listCommand(overlaysConfig, options);
|
|
953
|
+
process.exit(0);
|
|
954
|
+
});
|
|
955
|
+
// Explain command
|
|
956
|
+
program
|
|
957
|
+
.command('explain <overlay>')
|
|
958
|
+
.description('Show detailed information about an overlay')
|
|
959
|
+
.option('--json', 'Output as JSON for scripting')
|
|
960
|
+
.action(async (overlayId, options) => {
|
|
961
|
+
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
962
|
+
await explainCommand(overlaysConfig, OVERLAYS_DIR, overlayId, options);
|
|
963
|
+
process.exit(0);
|
|
964
|
+
});
|
|
965
|
+
// Plan command
|
|
966
|
+
program
|
|
967
|
+
.command('plan')
|
|
968
|
+
.description('Preview what will be generated before creating devcontainer')
|
|
969
|
+
.option('--stack <type>', 'Base template: plain, compose', 'compose')
|
|
970
|
+
.option('--overlays <list>', 'Comma-separated list of overlay IDs')
|
|
971
|
+
.option('--port-offset <number>', 'Add offset to all exposed ports', (val) => parseInt(val, 10), 0)
|
|
972
|
+
.option('--json', 'Output as JSON for scripting')
|
|
973
|
+
.action(async (options) => {
|
|
974
|
+
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
975
|
+
await planCommand(overlaysConfig, OVERLAYS_DIR, options);
|
|
976
|
+
process.exit(0);
|
|
983
977
|
});
|
|
984
978
|
// Doctor command
|
|
985
979
|
program
|
|
986
980
|
.command('doctor')
|
|
987
981
|
.description('Check environment and validate configuration')
|
|
988
982
|
.option('-o, --output <path>', 'Devcontainer path to validate (default: ./.devcontainer)')
|
|
983
|
+
.option('--fix', 'Apply automatic fixes where possible')
|
|
984
|
+
.option('--json', 'Output as JSON for scripting')
|
|
989
985
|
.action(async (options) => {
|
|
990
|
-
|
|
986
|
+
const overlaysConfig = loadOverlaysConfigWrapper();
|
|
987
|
+
await doctorCommand(overlaysConfig, OVERLAYS_DIR, options);
|
|
991
988
|
});
|
|
992
989
|
await program.parseAsync(process.argv);
|
|
993
990
|
// If init or regen command was run, return the options
|
|
@@ -1030,6 +1027,22 @@ async function parseCliArgs() {
|
|
|
1030
1027
|
if (initOptions.portOffset) {
|
|
1031
1028
|
config.portOffset = parseInt(initOptions.portOffset, 10);
|
|
1032
1029
|
}
|
|
1030
|
+
if (initOptions.target) {
|
|
1031
|
+
config.target = initOptions.target;
|
|
1032
|
+
}
|
|
1033
|
+
if (initOptions.minimal) {
|
|
1034
|
+
config.minimal = true;
|
|
1035
|
+
}
|
|
1036
|
+
if (initOptions.editor) {
|
|
1037
|
+
const editorLower = initOptions.editor.toLowerCase();
|
|
1038
|
+
if (['vscode', 'jetbrains', 'none'].includes(editorLower)) {
|
|
1039
|
+
config.editor = editorLower;
|
|
1040
|
+
}
|
|
1041
|
+
else {
|
|
1042
|
+
console.warn(chalk.yellow(`⚠️ Invalid editor profile: ${initOptions.editor}, using default (vscode)`));
|
|
1043
|
+
config.editor = 'vscode';
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1033
1046
|
if (initOptions.output)
|
|
1034
1047
|
config.outputPath = initOptions.output;
|
|
1035
1048
|
return {
|
|
@@ -1038,6 +1051,7 @@ async function parseCliArgs() {
|
|
|
1038
1051
|
noBackup: initOptions.backup === false, // Commander creates options.backup = false for --no-backup
|
|
1039
1052
|
backupDir: initOptions.backupDir,
|
|
1040
1053
|
noInteractive: initOptions.interactive === false, // Commander creates options.interactive = false for --no-interactive
|
|
1054
|
+
writeManifestOnly: initOptions.writeManifestOnly === true,
|
|
1041
1055
|
};
|
|
1042
1056
|
}
|
|
1043
1057
|
async function main() {
|
|
@@ -1091,8 +1105,12 @@ async function main() {
|
|
|
1091
1105
|
}
|
|
1092
1106
|
// Build answers based on mode
|
|
1093
1107
|
let answers;
|
|
1094
|
-
if
|
|
1095
|
-
|
|
1108
|
+
// Check if there are CLI overrides beyond just output path
|
|
1109
|
+
const hasCliOverrides = cliArgs &&
|
|
1110
|
+
Object.keys(cliArgs.config).some((key) => key !== 'outputPath' &&
|
|
1111
|
+
cliArgs.config[key] !== undefined);
|
|
1112
|
+
if (useManifestOnly && manifest && !hasCliOverrides) {
|
|
1113
|
+
// Mode 1: Manifest-only (--from-manifest --no-interactive, no CLI overrides)
|
|
1096
1114
|
const manifestAnswers = buildAnswersFromManifest(manifest, manifestDir);
|
|
1097
1115
|
answers = mergeAnswers(manifestAnswers);
|
|
1098
1116
|
console.log('\n' +
|
|
@@ -1110,8 +1128,9 @@ async function main() {
|
|
|
1110
1128
|
: '') +
|
|
1111
1129
|
chalk.gray(` Output: ${answers.outputPath}`), { padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: 1 }));
|
|
1112
1130
|
}
|
|
1113
|
-
else if (cliArgs && cliArgs.config.stack) {
|
|
1131
|
+
else if (cliArgs && (cliArgs.config.stack || hasCliOverrides)) {
|
|
1114
1132
|
// Mode 2: CLI-based (with optional manifest defaults)
|
|
1133
|
+
// This includes regen with --minimal or --editor flags
|
|
1115
1134
|
const cliAnswers = buildAnswersFromCliArgs(cliArgs.config);
|
|
1116
1135
|
const manifestAnswers = manifest
|
|
1117
1136
|
? buildAnswersFromManifest(manifest, manifestDir)
|
|
@@ -1119,12 +1138,26 @@ async function main() {
|
|
|
1119
1138
|
answers = mergeAnswers(manifestAnswers, cliAnswers, {
|
|
1120
1139
|
outputPath: cliAnswers.outputPath || './.devcontainer',
|
|
1121
1140
|
});
|
|
1141
|
+
const modeLabel = useManifestOnly && hasCliOverrides
|
|
1142
|
+
? 'Regenerating from Manifest with Overrides'
|
|
1143
|
+
: 'Running in CLI mode';
|
|
1122
1144
|
console.log('\n' +
|
|
1123
|
-
boxen(chalk.bold(
|
|
1145
|
+
boxen(chalk.bold(modeLabel), {
|
|
1124
1146
|
padding: 0.5,
|
|
1125
1147
|
borderColor: 'blue',
|
|
1126
1148
|
borderStyle: 'round',
|
|
1127
1149
|
}));
|
|
1150
|
+
// Show what's being overridden
|
|
1151
|
+
if (useManifestOnly && hasCliOverrides) {
|
|
1152
|
+
const overrides = [];
|
|
1153
|
+
if (cliAnswers.minimal)
|
|
1154
|
+
overrides.push('minimal mode');
|
|
1155
|
+
if (cliAnswers.editor)
|
|
1156
|
+
overrides.push(`editor: ${cliAnswers.editor}`);
|
|
1157
|
+
if (overrides.length > 0) {
|
|
1158
|
+
console.log(chalk.dim(` Overrides: ${overrides.join(', ')}`));
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1128
1161
|
}
|
|
1129
1162
|
else {
|
|
1130
1163
|
// Mode 3: Interactive (with optional manifest pre-population)
|
|
@@ -1157,27 +1190,50 @@ async function main() {
|
|
|
1157
1190
|
borderStyle: 'round',
|
|
1158
1191
|
margin: { top: 0, bottom: 1 },
|
|
1159
1192
|
}));
|
|
1193
|
+
// Check if we're in manifest-only mode
|
|
1194
|
+
const isManifestOnly = cliArgs?.writeManifestOnly === true;
|
|
1160
1195
|
// Generate with spinner
|
|
1161
1196
|
const spinner = ora({
|
|
1162
|
-
text:
|
|
1197
|
+
text: isManifestOnly
|
|
1198
|
+
? chalk.cyan('Generating manifest file...')
|
|
1199
|
+
: chalk.cyan('Generating devcontainer configuration...'),
|
|
1163
1200
|
color: 'cyan',
|
|
1164
1201
|
}).start();
|
|
1165
1202
|
try {
|
|
1166
|
-
|
|
1167
|
-
|
|
1203
|
+
if (isManifestOnly) {
|
|
1204
|
+
await generateManifestOnly(answers);
|
|
1205
|
+
spinner.succeed(chalk.green('Manifest created successfully!'));
|
|
1206
|
+
}
|
|
1207
|
+
else {
|
|
1208
|
+
await composeDevContainer(answers);
|
|
1209
|
+
spinner.succeed(chalk.green('DevContainer created successfully!'));
|
|
1210
|
+
}
|
|
1168
1211
|
}
|
|
1169
1212
|
catch (error) {
|
|
1170
|
-
spinner.fail(chalk.red('Failed to create devcontainer'));
|
|
1213
|
+
spinner.fail(chalk.red(isManifestOnly ? 'Failed to create manifest' : 'Failed to create devcontainer'));
|
|
1171
1214
|
throw error;
|
|
1172
1215
|
}
|
|
1173
1216
|
// Success message
|
|
1174
|
-
|
|
1175
|
-
|
|
1217
|
+
const successMessage = isManifestOnly
|
|
1218
|
+
? chalk.bold.green('✓ Manifest Created!\n\n') +
|
|
1219
|
+
chalk.white('Next steps:\n') +
|
|
1220
|
+
chalk.gray(' 1. Review the generated superposition.json file\n') +
|
|
1221
|
+
chalk.gray(' 2. Commit it to your repository\n') +
|
|
1222
|
+
chalk.gray(' 3. Team members can run "npx container-superposition regen"\n\n') +
|
|
1223
|
+
chalk.dim('Team workflow: commit manifest, .gitignore .devcontainer/, customize with .devcontainer/custom/')
|
|
1224
|
+
: chalk.bold.green('✓ Setup Complete!\n\n') +
|
|
1176
1225
|
chalk.white('Next steps:\n') +
|
|
1177
1226
|
chalk.gray(' 1. Review the generated .devcontainer/ folder\n') +
|
|
1178
1227
|
chalk.gray(" 2. Customize as needed (it's just normal JSON!)\n") +
|
|
1179
1228
|
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.')
|
|
1229
|
+
chalk.dim('The generated configuration is fully editable and independent of this tool.');
|
|
1230
|
+
console.log('\n' +
|
|
1231
|
+
boxen(successMessage, {
|
|
1232
|
+
padding: 1,
|
|
1233
|
+
borderColor: 'green',
|
|
1234
|
+
borderStyle: 'double',
|
|
1235
|
+
margin: 1,
|
|
1236
|
+
}));
|
|
1181
1237
|
}
|
|
1182
1238
|
catch (error) {
|
|
1183
1239
|
console.error('\n' +
|