@getmonoceros/workbench 1.21.3 → 1.22.0

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.
Files changed (36) hide show
  1. package/bundled-components/features/atlassian/component.yml +52 -0
  2. package/bundled-components/features/atlassian/install.sh +176 -0
  3. package/bundled-components/features/claude-code/component.yml +32 -0
  4. package/bundled-components/features/claude-code/install.sh +56 -0
  5. package/bundled-components/features/github-cli/component.yml +18 -0
  6. package/bundled-components/features/github-cli/install.sh +53 -0
  7. package/bundled-components/languages/dotnet/component.yml +8 -0
  8. package/bundled-components/languages/go/component.yml +8 -0
  9. package/bundled-components/languages/java/component.yml +17 -0
  10. package/bundled-components/languages/node/component.yml +9 -0
  11. package/bundled-components/languages/python/component.yml +8 -0
  12. package/bundled-components/languages/rust/component.yml +8 -0
  13. package/bundled-components/services/mysql/component.yml +40 -0
  14. package/bundled-components/services/postgres/component.yml +34 -0
  15. package/bundled-components/services/redis/component.yml +16 -0
  16. package/dist/bin.js +774 -364
  17. package/dist/bin.js.map +1 -1
  18. package/package.json +5 -4
  19. package/features/atlassian/devcontainer-feature.json +0 -67
  20. package/features/claude-code/devcontainer-feature.json +0 -49
  21. package/features/github-cli/devcontainer-feature.json +0 -32
  22. package/templates/components/README.md +0 -95
  23. package/templates/components/atlassian/rovodev.yml +0 -14
  24. package/templates/components/atlassian/twg.yml +0 -14
  25. package/templates/components/atlassian.yml +0 -15
  26. package/templates/components/claude.yml +0 -16
  27. package/templates/components/dotnet.yml +0 -8
  28. package/templates/components/github.yml +0 -10
  29. package/templates/components/go.yml +0 -8
  30. package/templates/components/java.yml +0 -9
  31. package/templates/components/mysql.yml +0 -8
  32. package/templates/components/node.yml +0 -9
  33. package/templates/components/postgres.yml +0 -10
  34. package/templates/components/python.yml +0 -9
  35. package/templates/components/redis.yml +0 -7
  36. package/templates/components/rust.yml +0 -8
package/dist/bin.js CHANGED
@@ -74,7 +74,7 @@ ${issues}`);
74
74
  }
75
75
  return result.data;
76
76
  }
77
- var SOLUTION_NAME_RE, APT_PACKAGE_NAME_RE, RUNTIME_VERSION_RE, FEATURE_REF_RE, INSTALL_URL_RE, REPO_URL_RE, REPO_PATH_RE, POSTGRES_URL_RE, REGEX, PROVIDER_VALUES, KNOWN_PROVIDER_HOSTS, CONFIG_SCHEMA_VERSION, FeatureOptionValueSchema, FeatureEntrySchema, EMAIL_RE, GitUserSchema, RepoEntrySchema, PortEntrySchema, RoutingSchema, SERVICE_NAME_RE, ServiceEnvValueSchema, ServiceHealthcheckSchema, SERVICE_RESTART_VALUES, ServiceObjectSchema, ExternalServicesSchema, SolutionConfigSchema;
77
+ var SOLUTION_NAME_RE, APT_PACKAGE_NAME_RE, RUNTIME_VERSION_RE, FEATURE_REF_RE, INSTALL_URL_RE, REPO_URL_RE, REPO_PATH_RE, POSTGRES_URL_RE, REGEX, PROVIDER_VALUES, KNOWN_PROVIDER_HOSTS, CONFIG_SCHEMA_VERSION, FeatureOptionValueSchema, FeatureEntrySchema, EMAIL_RE, GitUserSchema, RepoEntrySchema, PortEntrySchema, RoutingSchema, SERVICE_NAME_RE, ServiceEnvValueSchema, ServiceHealthcheckSchema, SERVICE_RESTART_VALUES, ServiceObjectSchema, ExternalServicesSchema, LanguageOptionValueSchema, LanguageEntrySchema, SolutionConfigSchema;
78
78
  var init_schema = __esm({
79
79
  "src/config/schema.ts"() {
80
80
  "use strict";
@@ -211,6 +211,20 @@ var init_schema = __esm({
211
211
  "Postgres URL must start with 'postgres://' or 'postgresql://'"
212
212
  ).optional()
213
213
  });
214
+ LanguageOptionValueSchema = z.union([
215
+ z.string(),
216
+ z.number(),
217
+ z.boolean()
218
+ ]);
219
+ LanguageEntrySchema = z.union([
220
+ z.string().min(1),
221
+ z.record(
222
+ z.string().min(1),
223
+ z.record(z.string().min(1), LanguageOptionValueSchema)
224
+ ).refine((obj) => Object.keys(obj).length === 1, {
225
+ message: "a language entry object must have exactly one language name as its key"
226
+ })
227
+ ]);
214
228
  SolutionConfigSchema = z.object({
215
229
  schemaVersion: z.literal(CONFIG_SCHEMA_VERSION),
216
230
  name: z.string().regex(
@@ -226,7 +240,7 @@ var init_schema = __esm({
226
240
  RUNTIME_VERSION_RE,
227
241
  "Invalid runtimeVersion. Expected an exact version like '1.1.0'."
228
242
  ).optional(),
229
- languages: z.array(z.string().min(1)).default([]),
243
+ languages: z.array(LanguageEntrySchema).default([]),
230
244
  aptPackages: z.array(
231
245
  z.string().regex(
232
246
  APT_PACKAGE_NAME_RE,
@@ -336,11 +350,13 @@ function workbenchCheckoutRoot() {
336
350
  dir = parent;
337
351
  }
338
352
  }
339
- function componentsDir(root = workbenchRoot()) {
340
- return path.join(root, "templates", "components");
341
- }
342
- function bundledFeaturesDir(root = workbenchRoot()) {
343
- return path.join(root, "features");
353
+ function componentsRootDir() {
354
+ const checkout = workbenchCheckoutRoot();
355
+ if (checkout) {
356
+ const inCheckout = path.join(checkout, "components");
357
+ if (existsSync(inCheckout)) return inCheckout;
358
+ }
359
+ return path.join(workbenchRoot(), "bundled-components");
344
360
  }
345
361
  function containerConfigsDir(home = monocerosHome()) {
346
362
  return path.join(home, "container-configs");
@@ -378,7 +394,7 @@ var init_paths = __esm({
378
394
  "src/config/paths.ts"() {
379
395
  "use strict";
380
396
  MONOCEROS_HOME_MARKER = "monoceros-config.sample.yml";
381
- WORKBENCH_MARKER = path.join("templates", "components", "README.md");
397
+ WORKBENCH_MARKER = path.join("templates", "monoceros-config.sample.yml");
382
398
  CHECKOUT_MARKER = "pnpm-workspace.yaml";
383
399
  cachedWorkbenchRoot = null;
384
400
  cachedMonocerosHome = null;
@@ -632,6 +648,304 @@ var init_feature_doc = __esm({
632
648
  }
633
649
  });
634
650
 
651
+ // src/catalog/descriptor.ts
652
+ import { z as z2 } from "zod";
653
+ var DESCRIPTOR_ID_RE, CategorySchema, OptionTypeSchema, SurfaceSchema, OptionValueSchema, OptionSpecSchema, BriefingLineSchema, HealthcheckSchema, LanguageBlockSchema, ServiceBlockSchema, PersistentHomeFileSchema, FeatureBlockSchema, DescriptorSchema;
654
+ var init_descriptor = __esm({
655
+ "src/catalog/descriptor.ts"() {
656
+ "use strict";
657
+ init_schema();
658
+ DESCRIPTOR_ID_RE = /^[a-z0-9][a-z0-9-]*$/;
659
+ CategorySchema = z2.enum(["language", "service", "feature"]);
660
+ OptionTypeSchema = z2.enum(["string", "boolean", "number"]);
661
+ SurfaceSchema = z2.enum(["yml", "silent", "env"]);
662
+ OptionValueSchema = z2.union([z2.string(), z2.boolean(), z2.number()]);
663
+ OptionSpecSchema = z2.object({
664
+ type: OptionTypeSchema,
665
+ default: OptionValueSchema.optional(),
666
+ description: z2.string().optional(),
667
+ surface: SurfaceSchema.default("silent"),
668
+ /** Suggested values (rendered as devcontainer `proposals`). */
669
+ proposals: z2.array(z2.string()).optional()
670
+ });
671
+ BriefingLineSchema = z2.object({
672
+ text: z2.string().min(1),
673
+ /**
674
+ * Option name; the line is emitted only when that option resolves
675
+ * truthy (after merging defaults + user options). Must reference an
676
+ * option declared on the same descriptor.
677
+ */
678
+ whenOption: z2.string().optional()
679
+ });
680
+ HealthcheckSchema = z2.object({
681
+ test: z2.array(z2.string()).min(1),
682
+ interval: z2.string().optional(),
683
+ timeout: z2.string().optional(),
684
+ retries: z2.number().int().positive().optional(),
685
+ startPeriod: z2.string().optional()
686
+ });
687
+ LanguageBlockSchema = z2.object({
688
+ /** Upstream OCI feature ref, e.g. `ghcr.io/devcontainers/features/java:1`. */
689
+ feature: z2.string().regex(REGEX.featureRef),
690
+ /** True when the toolchain is already in the base runtime image (node). */
691
+ builtin: z2.boolean().default(false),
692
+ /**
693
+ * Version shown inline in the generated yml (`name:<defaultVersion>`), so
694
+ * the builder sees where to edit it. Should equal the upstream feature's
695
+ * real default to stay behavior-neutral. For a `builtin` language it is the
696
+ * base-image version; pinning that exact version stays builtin (no feature
697
+ * install), only a different version triggers the upstream feature.
698
+ * Coerced to string so bare YAML numbers (`defaultVersion: 22`) work.
699
+ */
700
+ defaultVersion: z2.coerce.string().optional(),
701
+ /**
702
+ * Versions the upstream feature accepts (docs/UX only, not enforced).
703
+ * Coerced to string so authors can write bare YAML numbers
704
+ * (`versions: [latest, 21, 17]`) without quoting.
705
+ */
706
+ versions: z2.array(z2.coerce.string()).optional()
707
+ });
708
+ ServiceBlockSchema = z2.object({
709
+ image: z2.string().min(1),
710
+ defaultPort: z2.number().int().positive().optional(),
711
+ dataMount: z2.string().optional(),
712
+ healthcheck: HealthcheckSchema.optional(),
713
+ /**
714
+ * Connection env vars injected into the WORKSPACE container so the app /
715
+ * agent can reach this service without hardcoding anything. Keyed by env
716
+ * var name → a template value. Tokens: `${host}` (the service hostname),
717
+ * `${port}` (the service's port, falling back to `defaultPort`), and
718
+ * `${<OPTION>}` (any of the service's own option values, e.g.
719
+ * `${POSTGRES_USER}`). Example:
720
+ * DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${host}:${port}/${POSTGRES_DB}
721
+ */
722
+ connectionEnv: z2.record(z2.string(), z2.string()).optional(),
723
+ vscodeExtensions: z2.array(z2.string()).optional()
724
+ });
725
+ PersistentHomeFileSchema = z2.object({
726
+ path: z2.string().min(1),
727
+ initialContent: z2.string().optional()
728
+ });
729
+ FeatureBlockSchema = z2.object({
730
+ /** Publishable feature version (devcontainer-feature.json `version`). */
731
+ version: z2.string().min(1),
732
+ persistentHomePaths: z2.array(z2.string().min(1)).optional(),
733
+ persistentHomeFiles: z2.array(PersistentHomeFileSchema).optional(),
734
+ vscodeExtensions: z2.array(z2.string()).optional()
735
+ });
736
+ DescriptorSchema = z2.object({
737
+ id: z2.string().regex(DESCRIPTOR_ID_RE, "id must be lowercase letters/digits/hyphens"),
738
+ /**
739
+ * CLI/yml selector name (catalog key). Defaults to `id`. Lets a feature
740
+ * keep a short selector (`claude`) while its published manifest id stays
741
+ * canonical (`claude-code`).
742
+ */
743
+ name: z2.string().regex(DESCRIPTOR_ID_RE, "name must be lowercase letters/digits/hyphens").optional(),
744
+ category: CategorySchema,
745
+ displayName: z2.string().min(1),
746
+ description: z2.string().min(1),
747
+ documentationURL: z2.string().url().optional(),
748
+ options: z2.record(z2.string(), OptionSpecSchema).default({}),
749
+ /** Free-text notes rendered above the component block at `init`. */
750
+ usageNotes: z2.array(z2.string()).default([]),
751
+ briefing: z2.array(BriefingLineSchema).default([]),
752
+ language: LanguageBlockSchema.optional(),
753
+ service: ServiceBlockSchema.optional(),
754
+ feature: FeatureBlockSchema.optional(),
755
+ /**
756
+ * Named option-override presets. Each becomes a selectable
757
+ * `<name>/<presetKey>` component (e.g. `atlassian/twg`); the bare
758
+ * component keeps the descriptor's own option defaults. Feature-only.
759
+ */
760
+ presets: z2.record(
761
+ z2.string().regex(DESCRIPTOR_ID_RE),
762
+ z2.record(z2.string(), OptionValueSchema)
763
+ ).optional()
764
+ }).superRefine((data, ctx) => {
765
+ const present = [
766
+ data.language ? "language" : null,
767
+ data.service ? "service" : null,
768
+ data.feature ? "feature" : null
769
+ ].filter(Boolean).sort();
770
+ if (present.length === 0) {
771
+ ctx.addIssue({
772
+ code: z2.ZodIssueCode.custom,
773
+ message: `missing the '${data.category}' block required by category '${data.category}'`
774
+ });
775
+ } else if (present.length > 1) {
776
+ ctx.addIssue({
777
+ code: z2.ZodIssueCode.custom,
778
+ message: `exactly one of language/service/feature is allowed, got: ${present.join(", ")}`
779
+ });
780
+ } else if (present[0] !== data.category) {
781
+ ctx.addIssue({
782
+ code: z2.ZodIssueCode.custom,
783
+ message: `category '${data.category}' requires a '${data.category}' block, found '${present[0]}'`
784
+ });
785
+ }
786
+ const optionKeys = new Set(Object.keys(data.options));
787
+ data.briefing.forEach((line, i) => {
788
+ if (line.whenOption !== void 0 && !optionKeys.has(line.whenOption)) {
789
+ ctx.addIssue({
790
+ code: z2.ZodIssueCode.custom,
791
+ path: ["briefing", i, "whenOption"],
792
+ message: `whenOption '${line.whenOption}' is not a declared option`
793
+ });
794
+ }
795
+ });
796
+ if (data.presets) {
797
+ if (data.category !== "feature") {
798
+ ctx.addIssue({
799
+ code: z2.ZodIssueCode.custom,
800
+ path: ["presets"],
801
+ message: `presets are only allowed on features, not '${data.category}'`
802
+ });
803
+ }
804
+ for (const [presetKey, overrides] of Object.entries(data.presets)) {
805
+ for (const optKey of Object.keys(overrides)) {
806
+ if (!optionKeys.has(optKey)) {
807
+ ctx.addIssue({
808
+ code: z2.ZodIssueCode.custom,
809
+ path: ["presets", presetKey, optKey],
810
+ message: `preset '${presetKey}' overrides '${optKey}', which is not a declared option`
811
+ });
812
+ }
813
+ }
814
+ }
815
+ }
816
+ });
817
+ }
818
+ });
819
+
820
+ // src/catalog/load.ts
821
+ import { existsSync as existsSync3, promises as fs2 } from "fs";
822
+ import path3 from "path";
823
+ import { parse as parseYaml } from "yaml";
824
+ async function loadDescriptorCatalog(rootDir = componentsRootDir()) {
825
+ const out = /* @__PURE__ */ new Map();
826
+ if (!existsSync3(rootDir)) {
827
+ return out;
828
+ }
829
+ for (const [dirName, category] of Object.entries(CATEGORY_DIRS)) {
830
+ const categoryDir = path3.join(rootDir, dirName);
831
+ if (!existsSync3(categoryDir)) continue;
832
+ const entries = await fs2.readdir(categoryDir, { withFileTypes: true });
833
+ for (const entry2 of entries) {
834
+ if (!entry2.isDirectory()) continue;
835
+ const id = entry2.name;
836
+ const sourcePath = path3.join(categoryDir, id, "component.yml");
837
+ if (!existsSync3(sourcePath)) continue;
838
+ const component = await loadOne(sourcePath, id, category);
839
+ if (out.has(component.id)) {
840
+ const first = out.get(component.id);
841
+ throw new Error(
842
+ `Duplicate component id '${component.id}': ${first.sourcePath} and ${sourcePath}.`
843
+ );
844
+ }
845
+ out.set(component.id, component);
846
+ }
847
+ }
848
+ return out;
849
+ }
850
+ async function loadOne(sourcePath, folderId, expectedCategory) {
851
+ const text = await fs2.readFile(sourcePath, "utf8");
852
+ return parseDescriptorFile(text, sourcePath, folderId, expectedCategory);
853
+ }
854
+ function parseDescriptorFile(text, sourcePath, folderId, expectedCategory) {
855
+ let raw;
856
+ try {
857
+ raw = parseYaml(text);
858
+ } catch (err) {
859
+ throw new Error(
860
+ `Failed to parse descriptor (${sourcePath}): ${err.message}`
861
+ );
862
+ }
863
+ const parsed = DescriptorSchema.safeParse(raw);
864
+ if (!parsed.success) {
865
+ const issues = parsed.error.issues.map((issue) => {
866
+ const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
867
+ return ` - ${where}: ${issue.message}`;
868
+ }).join("\n");
869
+ throw new Error(`Invalid descriptor (${sourcePath}):
870
+ ${issues}`);
871
+ }
872
+ const descriptor = parsed.data;
873
+ if (descriptor.id !== folderId) {
874
+ throw new Error(
875
+ `Descriptor id '${descriptor.id}' must match its folder name '${folderId}' (${sourcePath}).`
876
+ );
877
+ }
878
+ if (descriptor.category !== expectedCategory) {
879
+ throw new Error(
880
+ `Descriptor '${descriptor.id}' has category '${descriptor.category}' but sits under '${expectedCategory}s/' (${sourcePath}).`
881
+ );
882
+ }
883
+ return {
884
+ id: descriptor.id,
885
+ category: descriptor.category,
886
+ sourcePath,
887
+ descriptor
888
+ };
889
+ }
890
+ var CATEGORY_DIRS;
891
+ var init_load = __esm({
892
+ "src/catalog/load.ts"() {
893
+ "use strict";
894
+ init_paths();
895
+ init_descriptor();
896
+ CATEGORY_DIRS = {
897
+ languages: "language",
898
+ services: "service",
899
+ features: "feature"
900
+ };
901
+ }
902
+ });
903
+
904
+ // src/catalog/load-sync.ts
905
+ import { existsSync as existsSync4, readFileSync as readFileSync2, readdirSync } from "fs";
906
+ import path4 from "path";
907
+ function loadDescriptorCatalogSync(rootDir = componentsRootDir()) {
908
+ const out = /* @__PURE__ */ new Map();
909
+ if (!existsSync4(rootDir)) return out;
910
+ for (const [dirName, category] of Object.entries(CATEGORY_DIRS2)) {
911
+ const categoryDir = path4.join(rootDir, dirName);
912
+ if (!existsSync4(categoryDir)) continue;
913
+ for (const entry2 of readdirSync(categoryDir, { withFileTypes: true })) {
914
+ if (!entry2.isDirectory()) continue;
915
+ const id = entry2.name;
916
+ const sourcePath = path4.join(categoryDir, id, "component.yml");
917
+ if (!existsSync4(sourcePath)) continue;
918
+ const component = parseDescriptorFile(
919
+ readFileSync2(sourcePath, "utf8"),
920
+ sourcePath,
921
+ id,
922
+ category
923
+ );
924
+ if (out.has(component.id)) {
925
+ const first = out.get(component.id);
926
+ throw new Error(
927
+ `Duplicate component id '${component.id}': ${first.sourcePath} and ${sourcePath}.`
928
+ );
929
+ }
930
+ out.set(component.id, component);
931
+ }
932
+ }
933
+ return out;
934
+ }
935
+ var CATEGORY_DIRS2;
936
+ var init_load_sync = __esm({
937
+ "src/catalog/load-sync.ts"() {
938
+ "use strict";
939
+ init_paths();
940
+ init_load();
941
+ CATEGORY_DIRS2 = {
942
+ languages: "language",
943
+ services: "service",
944
+ features: "feature"
945
+ };
946
+ }
947
+ });
948
+
635
949
  // src/util/ref.ts
636
950
  function matchMonocerosFeature(ref) {
637
951
  const match = MONOCEROS_FEATURE_RE.exec(ref);
@@ -661,108 +975,59 @@ var init_ref = __esm({
661
975
  });
662
976
 
663
977
  // src/init/manifest.ts
664
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
665
- import path3 from "path";
666
- function resolveManifestPath(name, checkoutRoot) {
667
- if (checkoutRoot) {
668
- const checkoutPath = path3.join(
669
- checkoutRoot,
670
- "images",
671
- "features",
672
- name,
673
- "devcontainer-feature.json"
674
- );
675
- if (existsSync3(checkoutPath)) return checkoutPath;
676
- }
677
- const bundlePath = path3.join(
678
- bundledFeaturesDir(),
679
- name,
680
- "devcontainer-feature.json"
681
- );
682
- if (existsSync3(bundlePath)) return bundlePath;
683
- return null;
684
- }
685
- function loadFeatureManifestSummary(ref, checkoutRoot = workbenchCheckoutRoot()) {
978
+ function loadFeatureManifestSummary(ref, componentsRoot) {
686
979
  const match = matchMonocerosFeature(ref);
687
980
  if (!match) return void 0;
688
- const manifestPath = resolveManifestPath(match.name, checkoutRoot);
689
- if (!manifestPath) return void 0;
981
+ let descriptor;
690
982
  try {
691
- const text = readFileSync2(manifestPath, "utf8");
692
- const parsed = JSON.parse(text);
693
- const rawHints = parsed["x-monoceros"]?.optionHints;
694
- const optionHints = Array.isArray(rawHints) ? rawHints.filter(
695
- (x) => typeof x === "string" && x.length > 0
696
- ) : [];
697
- const rawNotes = parsed["x-monoceros"]?.usageNotes;
698
- const usageNotes = Array.isArray(rawNotes) ? rawNotes.filter(
699
- (x) => typeof x === "string" && x.length > 0
700
- ) : [];
701
- const optionDescriptions = {};
702
- const optionTypes = {};
703
- const optionDefaults = {};
704
- const optionNames = [];
705
- if (parsed.options) {
706
- for (const [key, opt] of Object.entries(parsed.options)) {
707
- if (!opt || typeof opt !== "object") continue;
708
- optionNames.push(key);
709
- if (typeof opt.description === "string" && opt.description.length > 0) {
710
- optionDescriptions[key] = opt.description;
711
- }
712
- if (opt.type === "boolean") {
713
- optionTypes[key] = "boolean";
714
- } else if (opt.type === "string") {
715
- optionTypes[key] = "string";
716
- }
717
- if (typeof opt.default === "string" || typeof opt.default === "boolean") {
718
- optionDefaults[key] = opt.default;
719
- }
720
- }
721
- }
722
- const briefing = parseBriefing(parsed["x-monoceros"]?.briefing);
723
- const name = typeof parsed.name === "string" ? parsed.name : "";
724
- const description = typeof parsed.description === "string" ? parsed.description : "";
725
- const rawUrl = typeof parsed.documentationURL === "string" ? parsed.documentationURL.trim() : "";
726
- const documentationURL = rawUrl.length > 0 && rawUrl.toLowerCase() !== "tbd" ? rawUrl : void 0;
727
- return {
728
- name,
729
- description,
730
- documentationURL,
731
- optionHints,
732
- optionDescriptions,
733
- optionNames,
734
- optionTypes,
735
- optionDefaults,
736
- usageNotes,
737
- ...briefing ? { briefing } : {}
738
- };
983
+ descriptor = loadDescriptorCatalogSync(componentsRoot).get(
984
+ match.name
985
+ )?.descriptor;
739
986
  } catch {
740
987
  return void 0;
741
988
  }
742
- }
743
- function parseBriefing(raw) {
744
- if (!raw || typeof raw !== "object") return void 0;
745
- const rawLines = raw.lines;
746
- if (!Array.isArray(rawLines)) return void 0;
747
- const lines = [];
748
- for (const entry2 of rawLines) {
749
- if (!entry2 || typeof entry2 !== "object") continue;
750
- const text = entry2.text;
751
- if (typeof text !== "string" || text.length === 0) continue;
752
- const whenOption = entry2.whenOption;
753
- const line = { text };
754
- if (typeof whenOption === "string" && whenOption.length > 0) {
755
- line.whenOption = whenOption;
756
- }
757
- lines.push(line);
758
- }
759
- if (lines.length === 0) return void 0;
760
- return { lines };
989
+ if (!descriptor || descriptor.category !== "feature") return void 0;
990
+ const optionHints = [];
991
+ const optionDescriptions = {};
992
+ const optionTypes = {};
993
+ const optionDefaults = {};
994
+ const optionNames = [];
995
+ for (const [key, spec] of Object.entries(descriptor.options)) {
996
+ optionNames.push(key);
997
+ if (spec.surface === "env") optionHints.push(key);
998
+ if (spec.description !== void 0 && spec.description.length > 0) {
999
+ optionDescriptions[key] = spec.description;
1000
+ }
1001
+ optionTypes[key] = spec.type === "boolean" ? "boolean" : "string";
1002
+ if (typeof spec.default === "string" || typeof spec.default === "boolean") {
1003
+ optionDefaults[key] = spec.default;
1004
+ }
1005
+ }
1006
+ const rawUrl = descriptor.documentationURL?.trim() ?? "";
1007
+ const documentationURL = rawUrl.length > 0 && rawUrl.toLowerCase() !== "tbd" ? rawUrl : void 0;
1008
+ const briefing = descriptor.briefing.length > 0 ? {
1009
+ lines: descriptor.briefing.map((l) => ({
1010
+ text: l.text,
1011
+ ...l.whenOption !== void 0 ? { whenOption: l.whenOption } : {}
1012
+ }))
1013
+ } : void 0;
1014
+ return {
1015
+ name: descriptor.displayName,
1016
+ description: descriptor.description,
1017
+ documentationURL,
1018
+ optionHints,
1019
+ optionDescriptions,
1020
+ optionNames,
1021
+ optionTypes,
1022
+ optionDefaults,
1023
+ usageNotes: descriptor.usageNotes,
1024
+ ...briefing ? { briefing } : {}
1025
+ };
761
1026
  }
762
1027
  var init_manifest = __esm({
763
1028
  "src/init/manifest.ts"() {
764
1029
  "use strict";
765
- init_paths();
1030
+ init_load_sync();
766
1031
  init_ref();
767
1032
  }
768
1033
  });
@@ -809,8 +1074,8 @@ var init_format = __esm({
809
1074
 
810
1075
  // src/devcontainer/credentials.ts
811
1076
  import { spawn } from "child_process";
812
- import { promises as fs2 } from "fs";
813
- import path4 from "path";
1077
+ import { promises as fs3 } from "fs";
1078
+ import path5 from "path";
814
1079
  function resolveProvider(host, explicit) {
815
1080
  const canonical = KNOWN_PROVIDER_HOSTS[host.toLowerCase()];
816
1081
  if (canonical) return canonical;
@@ -959,8 +1224,8 @@ function formatCredentialLine(host, username, password) {
959
1224
  return `https://${encUser}:${encPass}@${host}`;
960
1225
  }
961
1226
  async function collectGitCredentials(devContainerRoot, hosts, options = {}) {
962
- const credsDir = path4.join(devContainerRoot, ".monoceros");
963
- const credentialsPath = path4.join(credsDir, "git-credentials");
1227
+ const credsDir = path5.join(devContainerRoot, ".monoceros");
1228
+ const credentialsPath = path5.join(credsDir, "git-credentials");
964
1229
  const spawnFn = options.spawn ?? realGitCredentialFill;
965
1230
  const approveFn = options.approve ?? realGitCredentialApprove;
966
1231
  const logger = options.logger ?? { info: () => {
@@ -1024,8 +1289,8 @@ password=${password}
1024
1289
  } catch {
1025
1290
  }
1026
1291
  }
1027
- await fs2.mkdir(credsDir, { recursive: true });
1028
- await fs2.writeFile(
1292
+ await fs3.mkdir(credsDir, { recursive: true });
1293
+ await fs3.writeFile(
1029
1294
  credentialsPath,
1030
1295
  lines.join("\n") + (lines.length > 0 ? "\n" : ""),
1031
1296
  {
@@ -1180,15 +1445,15 @@ var init_locate_running = __esm({
1180
1445
  });
1181
1446
 
1182
1447
  // src/config/global.ts
1183
- import { promises as fs3 } from "fs";
1184
- import { z as z2 } from "zod";
1448
+ import { promises as fs4 } from "fs";
1449
+ import { z as z3 } from "zod";
1185
1450
  import { isMap, Pair, parseDocument as parseDocument2, Scalar, YAMLMap } from "yaml";
1186
1451
  async function readMonocerosConfig(opts = {}) {
1187
1452
  const home = opts.monocerosHome ?? monocerosHome();
1188
1453
  const filePath = monocerosConfigPath(home);
1189
1454
  let text;
1190
1455
  try {
1191
- text = await fs3.readFile(filePath, "utf8");
1456
+ text = await fs4.readFile(filePath, "utf8");
1192
1457
  } catch {
1193
1458
  return void 0;
1194
1459
  }
@@ -1224,7 +1489,7 @@ async function writeGlobalDefaultGitUser(user, opts = {}) {
1224
1489
  const filePath = monocerosConfigPath(home);
1225
1490
  let text;
1226
1491
  try {
1227
- text = await fs3.readFile(filePath, "utf8");
1492
+ text = await fs4.readFile(filePath, "utf8");
1228
1493
  } catch {
1229
1494
  text = void 0;
1230
1495
  }
@@ -1241,8 +1506,8 @@ async function writeGlobalDefaultGitUser(user, opts = {}) {
1241
1506
  ` email: ${user.email}`,
1242
1507
  ""
1243
1508
  ].join("\n");
1244
- await fs3.mkdir(home, { recursive: true });
1245
- await fs3.writeFile(filePath, fresh, "utf8");
1509
+ await fs4.mkdir(home, { recursive: true });
1510
+ await fs4.writeFile(filePath, fresh, "utf8");
1246
1511
  return { filePath, created: true, alreadySet: false };
1247
1512
  }
1248
1513
  const doc = parseDocument2(text, { prettyErrors: true });
@@ -1263,7 +1528,7 @@ async function writeGlobalDefaultGitUser(user, opts = {}) {
1263
1528
  userMap.set("name", user.name);
1264
1529
  userMap.set("email", user.email);
1265
1530
  const newText = String(doc);
1266
- await fs3.writeFile(filePath, newText, "utf8");
1531
+ await fs4.writeFile(filePath, newText, "utf8");
1267
1532
  return { filePath, created: false, alreadySet: false };
1268
1533
  }
1269
1534
  function ensureMap(doc, key) {
@@ -1375,19 +1640,19 @@ var init_global = __esm({
1375
1640
  init_schema();
1376
1641
  init_paths();
1377
1642
  SCHEMA_VERSION = 1;
1378
- MonocerosConfigSchema = z2.object({
1379
- schemaVersion: z2.literal(SCHEMA_VERSION),
1643
+ MonocerosConfigSchema = z3.object({
1644
+ schemaVersion: z3.literal(SCHEMA_VERSION),
1380
1645
  // .nullish() (= .optional().nullable()) on defaults so the shipped
1381
1646
  // sample yml — where `defaults:` is uncommented but every sub-block
1382
1647
  // is commented out — parses cleanly. YAML produces `defaults: null`
1383
1648
  // in that case; without .nullish() the schema would reject it and
1384
1649
  // we'd be back to forcing builders to comment-juggle three lines.
1385
- defaults: z2.object({
1650
+ defaults: z3.object({
1386
1651
  // .nullish() (not just .optional()) so the sample yml can leave
1387
1652
  // `git:` uncommented as a category marker — YAML produces
1388
1653
  // `git: null` for an empty mapping, which zod's plain
1389
1654
  // `.optional()` would reject.
1390
- git: z2.object({
1655
+ git: z3.object({
1391
1656
  // Strict email here: monoceros-config defaults are not tied to
1392
1657
  // any container `<name>.env`, so `${VAR}` placeholders make no
1393
1658
  // sense and the format can (and should) be validated at load
@@ -1400,25 +1665,25 @@ var init_global = __esm({
1400
1665
  }).nullish(),
1401
1666
  // .nullish() for the same reason as `git` — the sample keeps
1402
1667
  // `features:` uncommented as a category marker.
1403
- features: z2.record(
1404
- z2.string().regex(
1668
+ features: z3.record(
1669
+ z3.string().regex(
1405
1670
  REGEX.featureRef,
1406
1671
  "Invalid feature ref. Expected an OCI-image-style ref like 'ghcr.io/getmonoceros/monoceros-features/<name>:<tag>'."
1407
1672
  ),
1408
- z2.record(z2.string(), FeatureOptionValueSchema)
1673
+ z3.record(z3.string(), FeatureOptionValueSchema)
1409
1674
  ).nullish()
1410
1675
  }).nullish(),
1411
1676
  // Machine-global routing settings — one Traefik per builder, so
1412
1677
  // host-port and similar live here rather than in any container yml.
1413
1678
  // See ADR 0007.
1414
- routing: z2.object({
1415
- hostPort: z2.number().int().min(1).max(65535).optional().describe(
1679
+ routing: z3.object({
1680
+ hostPort: z3.number().int().min(1).max(65535).optional().describe(
1416
1681
  "Host port the Traefik singleton binds. Default 80. Set this when 80 is held by another service on your machine \u2014 URLs then become http://<name>.localhost:<port>/."
1417
1682
  )
1418
1683
  }).nullish(),
1419
1684
  // Tool-freshness settings (ADR 0018). One machine-global knob.
1420
- upgrade: z2.object({
1421
- staleDays: z2.number().int().min(1).optional().describe(
1685
+ upgrade: z3.object({
1686
+ staleDays: z3.number().int().min(1).optional().describe(
1422
1687
  "Days after the last `monoceros upgrade` before `apply` nudges you to refresh tooling. Default 30."
1423
1688
  )
1424
1689
  }).nullish()
@@ -1428,49 +1693,81 @@ var init_global = __esm({
1428
1693
  });
1429
1694
 
1430
1695
  // src/init/components.ts
1431
- import { existsSync as existsSync4, promises as fs4 } from "fs";
1432
- import path5 from "path";
1433
- import { z as z3 } from "zod";
1434
- import { parse as parseYaml } from "yaml";
1435
- async function loadComponentCatalog(rootDir = componentsDir()) {
1436
- if (!existsSync4(rootDir)) {
1437
- return /* @__PURE__ */ new Map();
1696
+ function featureRef(d) {
1697
+ const major = (d.feature?.version ?? "1").split(".")[0];
1698
+ return `ghcr.io/getmonoceros/monoceros-features/${d.id}:${major}`;
1699
+ }
1700
+ function surfaceYmlDefaults(d) {
1701
+ const out = {};
1702
+ for (const [key, spec] of Object.entries(d.options)) {
1703
+ if (spec.surface === "yml" && spec.default !== void 0) {
1704
+ out[key] = spec.default;
1705
+ }
1438
1706
  }
1439
- const out = /* @__PURE__ */ new Map();
1440
- await walk(rootDir, rootDir, out);
1441
1707
  return out;
1442
1708
  }
1443
- async function walk(baseDir, currentDir, out) {
1444
- const entries = await fs4.readdir(currentDir, { withFileTypes: true });
1445
- for (const entry2 of entries) {
1446
- const full = path5.join(currentDir, entry2.name);
1447
- if (entry2.isDirectory()) {
1448
- await walk(baseDir, full, out);
1449
- continue;
1709
+ function baseComponentFile(d) {
1710
+ const selector = d.name ?? d.id;
1711
+ if (d.category === "language") {
1712
+ return {
1713
+ displayName: d.displayName,
1714
+ description: d.description,
1715
+ category: "language",
1716
+ contributes: { languages: [selector] }
1717
+ };
1718
+ }
1719
+ if (d.category === "service") {
1720
+ return {
1721
+ displayName: d.displayName,
1722
+ description: d.description,
1723
+ category: "service",
1724
+ contributes: { services: [selector] }
1725
+ };
1726
+ }
1727
+ return {
1728
+ displayName: d.displayName,
1729
+ description: d.description,
1730
+ category: "feature",
1731
+ contributes: {
1732
+ features: [{ ref: featureRef(d), options: surfaceYmlDefaults(d) }]
1450
1733
  }
1451
- if (!entry2.isFile() || !entry2.name.endsWith(".yml")) continue;
1452
- const relative = path5.relative(baseDir, full);
1453
- const name = relative.replace(/\.yml$/, "").split(path5.sep).join("/");
1454
- const text = await fs4.readFile(full, "utf8");
1455
- let raw;
1456
- try {
1457
- raw = parseYaml(text);
1458
- } catch (err) {
1459
- throw new Error(
1460
- `Failed to parse component ${name} (${full}): ${err.message}`
1461
- );
1734
+ };
1735
+ }
1736
+ async function loadComponentCatalog(rootDir) {
1737
+ return buildComponentCatalog(await loadDescriptorCatalog(rootDir));
1738
+ }
1739
+ function buildComponentCatalog(descriptors) {
1740
+ const out = /* @__PURE__ */ new Map();
1741
+ const add = (name, file, sourcePath) => {
1742
+ if (out.has(name)) {
1743
+ throw new Error(`Duplicate component name '${name}' (${sourcePath}).`);
1462
1744
  }
1463
- const parsed = ComponentFileSchema.safeParse(raw);
1464
- if (!parsed.success) {
1465
- const issues = parsed.error.issues.map((issue) => {
1466
- const where = issue.path.length > 0 ? issue.path.join(".") : "(root)";
1467
- return ` - ${where}: ${issue.message}`;
1468
- }).join("\n");
1469
- throw new Error(`Invalid component ${name} (${full}):
1470
- ${issues}`);
1745
+ out.set(name, { name, sourcePath, file });
1746
+ };
1747
+ for (const { descriptor: d, sourcePath } of descriptors.values()) {
1748
+ const selector = d.name ?? d.id;
1749
+ add(selector, baseComponentFile(d), sourcePath);
1750
+ for (const [presetKey, overrides] of Object.entries(d.presets ?? {})) {
1751
+ add(
1752
+ `${selector}/${presetKey}`,
1753
+ {
1754
+ displayName: `${d.displayName} (${presetKey})`,
1755
+ description: d.description,
1756
+ category: "feature",
1757
+ contributes: {
1758
+ features: [
1759
+ {
1760
+ ref: featureRef(d),
1761
+ options: { ...surfaceYmlDefaults(d), ...overrides }
1762
+ }
1763
+ ]
1764
+ }
1765
+ },
1766
+ sourcePath
1767
+ );
1471
1768
  }
1472
- out.set(name, { name, sourcePath: full, file: parsed.data });
1473
1769
  }
1770
+ return out;
1474
1771
  }
1475
1772
  function mergeFeatureOptions(a, b) {
1476
1773
  const result = { ...a };
@@ -1484,55 +1781,10 @@ function mergeFeatureOptions(a, b) {
1484
1781
  }
1485
1782
  return result;
1486
1783
  }
1487
- var CategorySchema, FeatureContributionSchema, ComponentFileSchema;
1488
1784
  var init_components = __esm({
1489
1785
  "src/init/components.ts"() {
1490
1786
  "use strict";
1491
- init_paths();
1492
- init_schema();
1493
- CategorySchema = z3.enum(["language", "service", "feature"]);
1494
- FeatureContributionSchema = z3.object({
1495
- ref: z3.string().regex(REGEX.featureRef),
1496
- options: z3.record(z3.string(), FeatureOptionValueSchema).optional()
1497
- });
1498
- ComponentFileSchema = z3.object({
1499
- displayName: z3.string().min(1),
1500
- description: z3.string().min(1),
1501
- category: CategorySchema,
1502
- contributes: z3.object({
1503
- languages: z3.array(z3.string().min(1)).optional(),
1504
- services: z3.array(z3.string().min(1)).optional(),
1505
- features: z3.array(FeatureContributionSchema).optional()
1506
- })
1507
- }).superRefine((data, ctx) => {
1508
- const c = data.contributes;
1509
- const filled = [
1510
- c.languages && c.languages.length > 0 ? "languages" : null,
1511
- c.services && c.services.length > 0 ? "services" : null,
1512
- c.features && c.features.length > 0 ? "features" : null
1513
- ].filter((x) => x !== null);
1514
- if (filled.length === 0) {
1515
- ctx.addIssue({
1516
- code: z3.ZodIssueCode.custom,
1517
- message: "contributes must set at least one of languages/services/features"
1518
- });
1519
- return;
1520
- }
1521
- if (filled.length > 1) {
1522
- ctx.addIssue({
1523
- code: z3.ZodIssueCode.custom,
1524
- message: `contributes must set exactly one of languages/services/features, got: ${filled.join(", ")}`
1525
- });
1526
- return;
1527
- }
1528
- const expected = data.category === "language" ? "languages" : data.category === "service" ? "services" : "features";
1529
- if (filled[0] !== expected) {
1530
- ctx.addIssue({
1531
- code: z3.ZodIssueCode.custom,
1532
- message: `category '${data.category}' requires contributes.${expected}, got contributes.${filled[0]}`
1533
- });
1534
- }
1535
- });
1787
+ init_load();
1536
1788
  }
1537
1789
  });
1538
1790
 
@@ -1858,6 +2110,25 @@ function runtimeSupportsIdeVolumes(version) {
1858
2110
  if (!version) return false;
1859
2111
  return compareRuntimeVersions(version, MIN_RUNTIME_FOR_IDE_VOLUMES) >= 0;
1860
2112
  }
2113
+ function descriptorOptionDefaults(options) {
2114
+ const out = {};
2115
+ for (const [key, spec] of Object.entries(options)) {
2116
+ if (spec.default !== void 0) out[key] = spec.default;
2117
+ }
2118
+ return out;
2119
+ }
2120
+ function descriptorYmlOptionDefaults(options) {
2121
+ const out = {};
2122
+ for (const [key, spec] of Object.entries(options)) {
2123
+ if (spec.surface === "yml" && spec.default !== void 0) {
2124
+ out[key] = spec.default;
2125
+ }
2126
+ }
2127
+ return out;
2128
+ }
2129
+ function descriptorSelector(c) {
2130
+ return c.descriptor.name ?? c.descriptor.id;
2131
+ }
1861
2132
  function parseLanguageSpec(spec) {
1862
2133
  const m = LANGUAGE_SPEC_RE.exec(spec);
1863
2134
  if (!m) return null;
@@ -1912,34 +2183,17 @@ function curatedServiceEnvDefaults(name) {
1912
2183
  function serviceConnectionEnv(services) {
1913
2184
  const env = {};
1914
2185
  for (const svc of services) {
1915
- if (!isCuratedService(svc.name)) continue;
2186
+ const def = SERVICE_CATALOG[svc.name];
2187
+ if (!def?.connectionEnv) continue;
1916
2188
  const host = svc.name;
1917
- if (svc.name === "postgres") {
1918
- const user = svc.env.POSTGRES_USER ?? "postgres";
1919
- const pass = svc.env.POSTGRES_PASSWORD ?? "";
1920
- const db = svc.env.POSTGRES_DB ?? user;
1921
- const port = svc.port ?? 5432;
1922
- env.PGHOST = host;
1923
- env.PGPORT = String(port);
1924
- env.PGUSER = user;
1925
- env.PGPASSWORD = pass;
1926
- env.PGDATABASE = db;
1927
- env.DATABASE_URL = `postgresql://${user}:${pass}@${host}:${port}/${db}`;
1928
- } else if (svc.name === "mysql") {
1929
- const pass = svc.env.MYSQL_ROOT_PASSWORD ?? "";
1930
- const db = svc.env.MYSQL_DATABASE ?? "";
1931
- const port = svc.port ?? 3306;
1932
- env.MYSQL_HOST = host;
1933
- env.MYSQL_PORT = String(port);
1934
- env.MYSQL_USER = "root";
1935
- env.MYSQL_PASSWORD = pass;
1936
- env.MYSQL_DATABASE = db;
1937
- if (env.DATABASE_URL === void 0) {
1938
- env.DATABASE_URL = `mysql://root:${pass}@${host}:${port}/${db}`;
1939
- }
1940
- } else if (svc.name === "redis") {
1941
- const port = svc.port ?? 6379;
1942
- env.REDIS_URL = `redis://${host}:${port}`;
2189
+ const port = String(svc.port ?? def.defaultPort);
2190
+ const fill = (template) => template.replace(/\$\{([A-Za-z0-9_]+)\}/g, (_, token) => {
2191
+ if (token === "host") return host;
2192
+ if (token === "port") return port;
2193
+ return svc.env[token] ?? "";
2194
+ });
2195
+ for (const [key, template] of Object.entries(def.connectionEnv)) {
2196
+ env[key] = fill(template);
1943
2197
  }
1944
2198
  }
1945
2199
  return env;
@@ -1949,10 +2203,11 @@ function deriveServiceName(image) {
1949
2203
  const noTag = lastSegment.split("@")[0].split(":")[0];
1950
2204
  return noTag.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
1951
2205
  }
1952
- var DEFAULT_BASE_IMAGE, override, BASE_IMAGE, RUNTIME_IMAGE_REPO, DEFAULT_RUNTIME_VERSION, MIN_RUNTIME_FOR_IDE_VOLUMES, BUILTIN_LANGUAGES, LANGUAGE_CATALOG, LANGUAGE_SPEC_RE, SERVICE_CATALOG;
2206
+ var DEFAULT_BASE_IMAGE, override, BASE_IMAGE, RUNTIME_IMAGE_REPO, DEFAULT_RUNTIME_VERSION, MIN_RUNTIME_FOR_IDE_VOLUMES, DESCRIPTORS, BUILTIN_LANGUAGES, LANGUAGE_CATALOG, LANGUAGE_SPEC_RE, SERVICE_CATALOG;
1953
2207
  var init_catalog = __esm({
1954
2208
  "src/create/catalog.ts"() {
1955
2209
  "use strict";
2210
+ init_load_sync();
1956
2211
  DEFAULT_BASE_IMAGE = "ghcr.io/getmonoceros/monoceros-runtime:1";
1957
2212
  override = process.env.MONOCEROS_BASE_IMAGE_OVERRIDE?.trim();
1958
2213
  BASE_IMAGE = override && override.length > 0 ? override : DEFAULT_BASE_IMAGE;
@@ -1964,86 +2219,49 @@ var init_catalog = __esm({
1964
2219
  "utf8"
1965
2220
  ).trim();
1966
2221
  MIN_RUNTIME_FOR_IDE_VOLUMES = "1.1.0";
1967
- BUILTIN_LANGUAGES = /* @__PURE__ */ new Set(["node"]);
1968
- LANGUAGE_CATALOG = {
1969
- node: { id: "node", feature: "ghcr.io/devcontainers/features/node:1" },
1970
- python: { id: "python", feature: "ghcr.io/devcontainers/features/python:1" },
1971
- java: { id: "java", feature: "ghcr.io/devcontainers/features/java:1" },
1972
- go: { id: "go", feature: "ghcr.io/devcontainers/features/go:1" },
1973
- rust: { id: "rust", feature: "ghcr.io/devcontainers/features/rust:1" },
1974
- dotnet: { id: "dotnet", feature: "ghcr.io/devcontainers/features/dotnet:2" }
1975
- };
2222
+ DESCRIPTORS = loadDescriptorCatalogSync();
2223
+ BUILTIN_LANGUAGES = new Set(
2224
+ [...DESCRIPTORS.values()].filter((c) => c.category === "language" && c.descriptor.language?.builtin).map(descriptorSelector)
2225
+ );
2226
+ LANGUAGE_CATALOG = Object.fromEntries(
2227
+ [...DESCRIPTORS.values()].filter((c) => c.category === "language").map((c) => {
2228
+ const key = descriptorSelector(c);
2229
+ const defaults = descriptorOptionDefaults(c.descriptor.options);
2230
+ const ymlOptions = descriptorYmlOptionDefaults(c.descriptor.options);
2231
+ const entry2 = {
2232
+ id: key,
2233
+ feature: c.descriptor.language.feature,
2234
+ ...Object.keys(defaults).length > 0 ? { defaultOptions: defaults } : {},
2235
+ ...Object.keys(ymlOptions).length > 0 ? { ymlOptions } : {},
2236
+ ...c.descriptor.language.defaultVersion !== void 0 ? { defaultVersion: c.descriptor.language.defaultVersion } : {}
2237
+ };
2238
+ return [key, entry2];
2239
+ })
2240
+ );
1976
2241
  LANGUAGE_SPEC_RE = /^([a-z][a-z0-9-]*)(?::([A-Za-z0-9._-]+))?$/;
1977
- SERVICE_CATALOG = {
1978
- postgres: {
1979
- id: "postgres",
1980
- image: "postgres:18",
1981
- env: {
1982
- POSTGRES_USER: "monoceros",
1983
- POSTGRES_PASSWORD: "monoceros",
1984
- POSTGRES_DB: "monoceros"
1985
- },
1986
- healthcheck: {
1987
- test: [
1988
- "CMD",
1989
- "pg_isready",
1990
- "-U",
1991
- "${POSTGRES_USER}",
1992
- "-d",
1993
- "${POSTGRES_DB}"
1994
- ],
1995
- interval: "10s",
1996
- timeout: "5s",
1997
- retries: 5
1998
- },
1999
- // Postgres 18+ stores data under /var/lib/postgresql/<major>/, so
2000
- // the recommended mount is the parent directory; pre-18 used
2001
- // /var/lib/postgresql/data directly. See
2002
- // https://github.com/docker-library/postgres/pull/1259.
2003
- dataMount: "/var/lib/postgresql",
2004
- defaultPort: 5432,
2005
- vscodeExtensions: ["cweijan.vscode-database-client2"]
2006
- },
2007
- mysql: {
2008
- id: "mysql",
2009
- image: "mysql:8",
2010
- env: {
2011
- MYSQL_ROOT_PASSWORD: "monoceros",
2012
- MYSQL_DATABASE: "monoceros"
2013
- },
2014
- healthcheck: {
2015
- test: [
2016
- "CMD",
2017
- "mysqladmin",
2018
- "ping",
2019
- "-h",
2020
- "127.0.0.1",
2021
- "-u",
2022
- "root",
2023
- "-p${MYSQL_ROOT_PASSWORD}"
2024
- ],
2025
- interval: "10s",
2026
- timeout: "5s",
2027
- retries: 5
2028
- },
2029
- dataMount: "/var/lib/mysql",
2030
- defaultPort: 3306,
2031
- vscodeExtensions: ["cweijan.vscode-database-client2"]
2032
- },
2033
- redis: {
2034
- id: "redis",
2035
- image: "redis:8",
2036
- healthcheck: {
2037
- test: ["CMD", "redis-cli", "ping"],
2038
- interval: "10s",
2039
- timeout: "5s",
2040
- retries: 5
2041
- },
2042
- dataMount: "/data",
2043
- defaultPort: 6379,
2044
- vscodeExtensions: ["cweijan.vscode-database-client2"]
2045
- }
2046
- };
2242
+ SERVICE_CATALOG = Object.fromEntries(
2243
+ [...DESCRIPTORS.values()].filter((c) => c.category === "service").map((c) => {
2244
+ const key = descriptorSelector(c);
2245
+ const svc = c.descriptor.service;
2246
+ if (svc.defaultPort === void 0) {
2247
+ throw new Error(
2248
+ `Service descriptor '${key}' is missing service.defaultPort.`
2249
+ );
2250
+ }
2251
+ const env = descriptorOptionDefaults(c.descriptor.options);
2252
+ const entry2 = {
2253
+ id: key,
2254
+ image: svc.image,
2255
+ ...Object.keys(env).length > 0 ? { env } : {},
2256
+ ...svc.healthcheck ? { healthcheck: svc.healthcheck } : {},
2257
+ ...svc.dataMount ? { dataMount: svc.dataMount } : {},
2258
+ defaultPort: svc.defaultPort,
2259
+ ...svc.vscodeExtensions ? { vscodeExtensions: svc.vscodeExtensions } : {},
2260
+ ...svc.connectionEnv ? { connectionEnv: svc.connectionEnv } : {}
2261
+ };
2262
+ return [key, entry2];
2263
+ })
2264
+ );
2047
2265
  }
2048
2266
  });
2049
2267
 
@@ -2104,6 +2322,66 @@ var init_service_doc = __esm({
2104
2322
  }
2105
2323
  });
2106
2324
 
2325
+ // src/catalog/generate-manifest.ts
2326
+ function descriptorToFeatureManifest(descriptor) {
2327
+ if (descriptor.category !== "feature" || !descriptor.feature) {
2328
+ throw new Error(
2329
+ `descriptorToFeatureManifest: '${descriptor.id}' is a ${descriptor.category}, not a feature.`
2330
+ );
2331
+ }
2332
+ const feat = descriptor.feature;
2333
+ const options = {};
2334
+ const optionHints = [];
2335
+ for (const [key, spec] of Object.entries(descriptor.options)) {
2336
+ const option = { type: spec.type };
2337
+ if (spec.proposals !== void 0) option.proposals = spec.proposals;
2338
+ if (spec.default !== void 0) option.default = spec.default;
2339
+ if (spec.description !== void 0) option.description = spec.description;
2340
+ options[key] = option;
2341
+ if (spec.surface === "env") optionHints.push(key);
2342
+ }
2343
+ const xMonoceros = {};
2344
+ if (feat.persistentHomePaths && feat.persistentHomePaths.length > 0) {
2345
+ xMonoceros.persistentHomePaths = feat.persistentHomePaths;
2346
+ }
2347
+ if (feat.persistentHomeFiles && feat.persistentHomeFiles.length > 0) {
2348
+ xMonoceros.persistentHomeFiles = feat.persistentHomeFiles;
2349
+ }
2350
+ xMonoceros.optionHints = optionHints;
2351
+ xMonoceros.usageNotes = descriptor.usageNotes;
2352
+ xMonoceros.briefing = {
2353
+ lines: descriptor.briefing.map((line) => ({
2354
+ ...line.whenOption !== void 0 ? { whenOption: line.whenOption } : {},
2355
+ text: line.text
2356
+ }))
2357
+ };
2358
+ const manifest = {
2359
+ $schema: DEVCONTAINER_FEATURE_SCHEMA,
2360
+ id: descriptor.id,
2361
+ name: descriptor.displayName,
2362
+ version: feat.version,
2363
+ description: descriptor.description
2364
+ };
2365
+ if (descriptor.documentationURL !== void 0) {
2366
+ manifest.documentationURL = descriptor.documentationURL;
2367
+ }
2368
+ if (Object.keys(options).length > 0) {
2369
+ manifest.options = options;
2370
+ }
2371
+ if (feat.vscodeExtensions && feat.vscodeExtensions.length > 0) {
2372
+ manifest.customizations = { vscode: { extensions: feat.vscodeExtensions } };
2373
+ }
2374
+ manifest["x-monoceros"] = xMonoceros;
2375
+ return manifest;
2376
+ }
2377
+ var DEVCONTAINER_FEATURE_SCHEMA;
2378
+ var init_generate_manifest = __esm({
2379
+ "src/catalog/generate-manifest.ts"() {
2380
+ "use strict";
2381
+ DEVCONTAINER_FEATURE_SCHEMA = "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainerFeature.schema.json";
2382
+ }
2383
+ });
2384
+
2107
2385
  // src/create/claude-settings.ts
2108
2386
  import { existsSync as existsSync5, promises as fsp2 } from "fs";
2109
2387
  import path8 from "path";
@@ -2180,7 +2458,7 @@ var init_claude_settings = __esm({
2180
2458
  });
2181
2459
 
2182
2460
  // src/create/scaffold.ts
2183
- import { existsSync as existsSync6, readFileSync as readFileSync4, promises as fs7 } from "fs";
2461
+ import { existsSync as existsSync6, promises as fs7 } from "fs";
2184
2462
  import path9 from "path";
2185
2463
  function deriveRepoName(url) {
2186
2464
  const lastSep = Math.max(url.lastIndexOf("/"), url.lastIndexOf(":"));
@@ -2294,6 +2572,7 @@ function normalizeOptions(opts) {
2294
2572
  name: opts.name,
2295
2573
  ...opts.runtimeVersion !== void 0 ? { runtimeVersion: opts.runtimeVersion } : {},
2296
2574
  languages,
2575
+ ...opts.languageOptions && Object.keys(opts.languageOptions).length > 0 ? { languageOptions: opts.languageOptions } : {},
2297
2576
  services,
2298
2577
  postgresUrl: opts.postgresUrl,
2299
2578
  ...aptPackages.length > 0 ? { aptPackages } : {},
@@ -2311,19 +2590,22 @@ function featuresSourceRoot() {
2311
2590
  const override2 = process.env.MONOCEROS_FEATURES_DIR_OVERRIDE?.trim();
2312
2591
  if (override2 && override2.length > 0) return override2;
2313
2592
  const checkout = workbenchCheckoutRoot();
2314
- return checkout ? path9.join(checkout, "images", "features") : null;
2593
+ return checkout ? path9.join(checkout, "components", "features") : null;
2315
2594
  }
2316
2595
  function resolveFeatures(opts) {
2317
2596
  const resolved = [];
2318
2597
  for (const langSpec of opts.languages) {
2319
2598
  const parsed = parseLanguageSpec(langSpec);
2320
2599
  if (!parsed) continue;
2321
- if (BUILTIN_LANGUAGES.has(parsed.name) && parsed.version === void 0) {
2322
- continue;
2323
- }
2324
2600
  const entry2 = LANGUAGE_CATALOG[parsed.name];
2325
2601
  if (!entry2) continue;
2326
- const options = {};
2602
+ if (BUILTIN_LANGUAGES.has(parsed.name) && (parsed.version === void 0 || parsed.version === entry2.defaultVersion)) {
2603
+ continue;
2604
+ }
2605
+ const options = {
2606
+ ...entry2.defaultOptions ?? {},
2607
+ ...opts.languageOptions?.[parsed.name] ?? {}
2608
+ };
2327
2609
  if (parsed.version !== void 0) options.version = parsed.version;
2328
2610
  resolved.push({
2329
2611
  devcontainerKey: entry2.feature,
@@ -2345,21 +2627,22 @@ function resolveFeatures(opts) {
2345
2627
  const match = matchMonocerosFeature(rawRef);
2346
2628
  if (match) {
2347
2629
  const name = match.name;
2630
+ const descriptor = featureDescriptor(name);
2631
+ const { paths, files } = descriptorPersistentHome(descriptor);
2348
2632
  const sourceRoot = featuresSourceRoot();
2349
2633
  const localSourceDir = sourceRoot ? path9.join(sourceRoot, name) : null;
2350
- if (localSourceDir && existsSync6(localSourceDir)) {
2351
- const { paths: paths2, files: files2 } = readPersistentHomeEntries(localSourceDir);
2634
+ if (descriptor && localSourceDir && existsSync6(localSourceDir)) {
2352
2635
  resolved.push({
2353
2636
  devcontainerKey: `./features/${name}`,
2354
2637
  options,
2355
2638
  localSourceDir,
2356
2639
  localName: name,
2357
- persistentHomePaths: paths2,
2358
- persistentHomeFiles: files2
2640
+ generatedManifest: descriptorToFeatureManifest(descriptor),
2641
+ persistentHomePaths: paths,
2642
+ persistentHomeFiles: files
2359
2643
  });
2360
2644
  continue;
2361
2645
  }
2362
- const { paths, files } = readBundledPersistentHomeEntries(name);
2363
2646
  resolved.push({
2364
2647
  devcontainerKey: rawRef,
2365
2648
  options,
@@ -2378,25 +2661,19 @@ function resolveFeatures(opts) {
2378
2661
  }
2379
2662
  return resolved;
2380
2663
  }
2381
- function readPersistentHomeEntries(localSourceDir) {
2382
- const manifestPath = path9.join(localSourceDir, "devcontainer-feature.json");
2664
+ function featureDescriptor(name) {
2383
2665
  try {
2384
- const text = readFileSync4(manifestPath, "utf8");
2385
- const parsed = JSON.parse(text);
2386
- return {
2387
- paths: filterSubpaths(parsed["x-monoceros"]?.persistentHomePaths),
2388
- files: filterFileEntries(parsed["x-monoceros"]?.persistentHomeFiles)
2389
- };
2666
+ const c = loadDescriptorCatalogSync().get(name);
2667
+ return c?.category === "feature" ? c.descriptor : void 0;
2390
2668
  } catch {
2391
- return { paths: [], files: [] };
2669
+ return void 0;
2392
2670
  }
2393
2671
  }
2394
- function readBundledPersistentHomeEntries(name) {
2395
- try {
2396
- return readPersistentHomeEntries(path9.join(bundledFeaturesDir(), name));
2397
- } catch {
2398
- return { paths: [], files: [] };
2399
- }
2672
+ function descriptorPersistentHome(descriptor) {
2673
+ return {
2674
+ paths: filterSubpaths(descriptor?.feature?.persistentHomePaths),
2675
+ files: filterFileEntries(descriptor?.feature?.persistentHomeFiles)
2676
+ };
2400
2677
  }
2401
2678
  function filterSubpaths(raw) {
2402
2679
  if (!Array.isArray(raw)) return [];
@@ -2858,7 +3135,23 @@ async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
2858
3135
  if (!f.localSourceDir || !f.localName) continue;
2859
3136
  const dest = path9.join(featuresDir, f.localName);
2860
3137
  await fs7.mkdir(dest, { recursive: true });
2861
- await fs7.cp(f.localSourceDir, dest, { recursive: true });
3138
+ const entries = await fs7.readdir(f.localSourceDir, { withFileTypes: true });
3139
+ for (const entry2 of entries) {
3140
+ if (!entry2.isFile()) continue;
3141
+ if (entry2.name === "component.yml" || entry2.name === "devcontainer-feature.json") {
3142
+ continue;
3143
+ }
3144
+ await fs7.cp(
3145
+ path9.join(f.localSourceDir, entry2.name),
3146
+ path9.join(dest, entry2.name)
3147
+ );
3148
+ }
3149
+ if (f.generatedManifest) {
3150
+ await fs7.writeFile(
3151
+ path9.join(dest, "devcontainer-feature.json"),
3152
+ JSON.stringify(f.generatedManifest, null, 2) + "\n"
3153
+ );
3154
+ }
2862
3155
  }
2863
3156
  for (const f of resolvedFeatures) {
2864
3157
  for (const sub of f.persistentHomePaths) {
@@ -2911,6 +3204,8 @@ var init_scaffold = __esm({
2911
3204
  "use strict";
2912
3205
  init_paths();
2913
3206
  init_ref();
3207
+ init_load_sync();
3208
+ init_generate_manifest();
2914
3209
  init_claude_settings();
2915
3210
  init_catalog();
2916
3211
  APT_PACKAGE_NAME_RE2 = /^[a-z0-9][a-z0-9.+-]*$/;
@@ -2966,10 +3261,33 @@ function pruneEmptySeq(doc, key) {
2966
3261
  function scalarValue(item) {
2967
3262
  return isScalar(item) ? item.value : item;
2968
3263
  }
2969
- function addLanguageToDoc(doc, lang) {
3264
+ function languageEntryName(item) {
3265
+ const v = scalarValue(item);
3266
+ if (typeof v === "string") {
3267
+ const colon = v.indexOf(":");
3268
+ return colon === -1 ? v : v.slice(0, colon);
3269
+ }
3270
+ if (isMap2(item)) {
3271
+ const firstKey = item.items[0]?.key;
3272
+ const k = isScalar(firstKey) ? firstKey.value : firstKey;
3273
+ return typeof k === "string" ? k : void 0;
3274
+ }
3275
+ return void 0;
3276
+ }
3277
+ function addLanguageToDoc(doc, name, opts = {}) {
2970
3278
  const seq = ensureSeq(doc, "languages");
2971
- if (seq.items.some((i) => scalarValue(i) === lang)) return false;
2972
- seq.add(lang);
3279
+ if (seq.items.some((i) => languageEntryName(i) === name)) return false;
3280
+ const { version, options } = opts;
3281
+ if (options && Object.keys(options).length > 0) {
3282
+ const inner = new YAMLMap2();
3283
+ if (version !== void 0) inner.set("version", version);
3284
+ for (const [key, value] of Object.entries(options)) inner.set(key, value);
3285
+ const outer = new YAMLMap2();
3286
+ outer.set(name, inner);
3287
+ seq.add(outer);
3288
+ } else {
3289
+ seq.add(version !== void 0 ? `${name}:${version}` : name);
3290
+ }
2973
3291
  return true;
2974
3292
  }
2975
3293
  function findServiceItem(seq, name) {
@@ -3382,12 +3700,22 @@ import { consola } from "consola";
3382
3700
  import { createPatch } from "diff";
3383
3701
  import path10 from "path";
3384
3702
  function runAddLanguage(input) {
3385
- if (!BUILTIN_LANGUAGES.has(input.language) && !LANGUAGE_CATALOG[input.language]) {
3703
+ const spec = parseLanguageSpec(input.language);
3704
+ if (!spec || !BUILTIN_LANGUAGES.has(spec.name) && !LANGUAGE_CATALOG[spec.name]) {
3386
3705
  throw new Error(
3387
3706
  `Unknown language: ${input.language}. Known: ${knownLanguages().join(", ")}.`
3388
3707
  );
3389
3708
  }
3390
- return mutate(input, (doc) => addLanguageToDoc(doc, input.language));
3709
+ const entry2 = LANGUAGE_CATALOG[spec.name];
3710
+ const version = spec.version ?? entry2?.defaultVersion;
3711
+ const options = entry2?.ymlOptions;
3712
+ return mutate(
3713
+ input,
3714
+ (doc) => addLanguageToDoc(doc, spec.name, {
3715
+ ...version !== void 0 ? { version } : {},
3716
+ ...options && Object.keys(options).length > 0 ? { options } : {}
3717
+ })
3718
+ );
3391
3719
  }
3392
3720
  async function runAddService(input) {
3393
3721
  const arg = input.service;
@@ -4443,6 +4771,23 @@ var init_state = __esm({
4443
4771
  });
4444
4772
 
4445
4773
  // src/config/transform.ts
4774
+ function normalizeLanguages(entries) {
4775
+ const languages = [];
4776
+ const languageOptions = {};
4777
+ for (const entry2 of entries) {
4778
+ if (typeof entry2 === "string") {
4779
+ languages.push(entry2);
4780
+ continue;
4781
+ }
4782
+ const name = Object.keys(entry2)[0];
4783
+ const opts = { ...entry2[name] };
4784
+ const version = opts.version;
4785
+ delete opts.version;
4786
+ languages.push(version !== void 0 ? `${name}:${String(version)}` : name);
4787
+ if (Object.keys(opts).length > 0) languageOptions[name] = opts;
4788
+ }
4789
+ return { languages, languageOptions };
4790
+ }
4446
4791
  function solutionConfigToCreateOptions(config, featureDefaults = {}) {
4447
4792
  const featureRecord = {};
4448
4793
  for (const entry2 of config.features) {
@@ -4452,16 +4797,20 @@ function solutionConfigToCreateOptions(config, featureDefaults = {}) {
4452
4797
  );
4453
4798
  featureRecord[entry2.ref] = { ...defaults, ...containerOpts };
4454
4799
  }
4800
+ const { languages, languageOptions } = normalizeLanguages(config.languages);
4455
4801
  const result = {
4456
4802
  name: config.name,
4457
4803
  ...config.runtimeVersion !== void 0 ? { runtimeVersion: config.runtimeVersion } : {},
4458
- languages: [...config.languages],
4804
+ languages,
4459
4805
  // Normalize every services[] entry (curated string or explicit
4460
4806
  // object) to the canonical ResolvedService shape. `${VAR}` values
4461
4807
  // survive untouched here — apply interpolates them against
4462
4808
  // <name>.env afterwards.
4463
4809
  services: config.services.map(resolveService)
4464
4810
  };
4811
+ if (Object.keys(languageOptions).length > 0) {
4812
+ result.languageOptions = languageOptions;
4813
+ }
4465
4814
  if (config.externalServices.postgres !== void 0) {
4466
4815
  result.postgresUrl = config.externalServices.postgres;
4467
4816
  }
@@ -5555,13 +5904,13 @@ var init_runtime_pull_hint = __esm({
5555
5904
 
5556
5905
  // src/devcontainer/cli.ts
5557
5906
  import { spawn as spawn4 } from "child_process";
5558
- import { readFileSync as readFileSync5 } from "fs";
5907
+ import { readFileSync as readFileSync4 } from "fs";
5559
5908
  import { createRequire } from "module";
5560
5909
  import path14 from "path";
5561
5910
  function devcontainerCliPath() {
5562
5911
  if (cachedBinaryPath) return cachedBinaryPath;
5563
5912
  const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
5564
- const pkg = JSON.parse(readFileSync5(pkgJsonPath, "utf8"));
5913
+ const pkg = JSON.parse(readFileSync4(pkgJsonPath, "utf8"));
5565
5914
  const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.devcontainer ?? "";
5566
5915
  if (!binEntry) {
5567
5916
  throw new Error("Could not resolve @devcontainers/cli bin entry.");
@@ -6706,7 +7055,7 @@ var CLI_VERSION;
6706
7055
  var init_version = __esm({
6707
7056
  "src/version.ts"() {
6708
7057
  "use strict";
6709
- CLI_VERSION = true ? "1.21.3" : "dev";
7058
+ CLI_VERSION = true ? "1.22.0" : "dev";
6710
7059
  }
6711
7060
  });
6712
7061
 
@@ -7353,7 +7702,21 @@ function generateComposedYml(name, composed, lookupManifest, repoUrls = [], port
7353
7702
  false
7354
7703
  );
7355
7704
  lines.push("languages:");
7356
- for (const lang of composed.languages) lines.push(` - ${lang}`);
7705
+ for (const lang of composed.languages) {
7706
+ const opts = lang.options ?? {};
7707
+ if (Object.keys(opts).length === 0) {
7708
+ lines.push(` - ${lang.spec}`);
7709
+ continue;
7710
+ }
7711
+ const colon = lang.spec.indexOf(":");
7712
+ const langName = colon === -1 ? lang.spec : lang.spec.slice(0, colon);
7713
+ const version = colon === -1 ? void 0 : lang.spec.slice(colon + 1);
7714
+ lines.push(` - ${langName}:`);
7715
+ if (version !== void 0) lines.push(` version: ${version}`);
7716
+ for (const [key, value] of Object.entries(opts)) {
7717
+ lines.push(` ${key}: ${String(value)}`);
7718
+ }
7719
+ }
7357
7720
  lines.push("");
7358
7721
  }
7359
7722
  if (composed.aptPackages.length > 0) {
@@ -7461,7 +7824,8 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
7461
7824
  lines.push("# languages:");
7462
7825
  for (const c of byCategory.language) {
7463
7826
  for (const lang of c.file.contributes.languages ?? []) {
7464
- lines.push(`# - ${lang}`);
7827
+ const ver = LANGUAGE_CATALOG[lang]?.defaultVersion;
7828
+ lines.push(`# - ${ver ? `${lang}:${ver}` : lang}`);
7465
7829
  }
7466
7830
  }
7467
7831
  lines.push("");
@@ -7704,7 +8068,6 @@ import { existsSync as existsSync10, promises as fs14 } from "fs";
7704
8068
  import path19 from "path";
7705
8069
  import { consola as consola13 } from "consola";
7706
8070
  async function runInit(opts) {
7707
- const workbench = opts.workbenchRoot ?? workbenchRoot();
7708
8071
  const home = opts.monocerosHome ?? monocerosHome();
7709
8072
  const logger = opts.logger ?? {
7710
8073
  success: (msg) => consola13.success(msg),
@@ -7721,14 +8084,14 @@ async function runInit(opts) {
7721
8084
  `Config already exists: ${dest}. Delete it manually before re-running \`monoceros init\` \u2014 this protects any hand-edits.`
7722
8085
  );
7723
8086
  }
7724
- const catalog = await loadComponentCatalog(componentsDir(workbench));
8087
+ const componentsRoot = opts.workbenchRoot ? path19.join(opts.workbenchRoot, "components") : componentsRootDir();
8088
+ const catalog = await loadComponentCatalog(componentsRoot);
7725
8089
  if (catalog.size === 0) {
7726
8090
  throw new Error(
7727
- `No components found under ${componentsDir(workbench)}. The workbench checkout is incomplete.`
8091
+ `No components found under ${componentsRoot}. The workbench checkout is incomplete.`
7728
8092
  );
7729
8093
  }
7730
- const checkoutRoot = opts.workbenchRoot ?? workbenchCheckoutRoot();
7731
- const lookup = (ref) => loadFeatureManifestSummary(ref, checkoutRoot);
8094
+ const lookup = (ref) => loadFeatureManifestSummary(ref, componentsRoot);
7732
8095
  const reposRaw = (opts.withRepo ?? []).map((u) => u.trim()).filter((u) => u.length > 0);
7733
8096
  const repos = [];
7734
8097
  const seenRepoUrls = /* @__PURE__ */ new Set();
@@ -7849,7 +8212,14 @@ function resolveInitLanguages(entries) {
7849
8212
  continue;
7850
8213
  }
7851
8214
  seen.add(e);
7852
- out.push(e);
8215
+ const entry2 = LANGUAGE_CATALOG[spec.name];
8216
+ const renderedSpec = spec.version === void 0 && entry2?.defaultVersion ? `${spec.name}:${entry2.defaultVersion}` : e;
8217
+ if (out.some((o) => o.spec === renderedSpec)) continue;
8218
+ const ymlOptions = entry2?.ymlOptions;
8219
+ out.push({
8220
+ spec: renderedSpec,
8221
+ ...ymlOptions && Object.keys(ymlOptions).length > 0 ? { options: ymlOptions } : {}
8222
+ });
7853
8223
  }
7854
8224
  if (unknown.length > 0) {
7855
8225
  throw new Error(
@@ -8084,6 +8454,45 @@ var init_init2 = __esm({
8084
8454
  }
8085
8455
  });
8086
8456
 
8457
+ // src/catalog/expand.ts
8458
+ function expandSelectable(catalog) {
8459
+ const out = /* @__PURE__ */ new Map();
8460
+ const add = (entry2) => {
8461
+ if (out.has(entry2.name)) {
8462
+ throw new Error(
8463
+ `Duplicate selectable component name '${entry2.name}' (from descriptor '${entry2.componentId}').`
8464
+ );
8465
+ }
8466
+ out.set(entry2.name, entry2);
8467
+ };
8468
+ for (const { descriptor: d } of catalog.values()) {
8469
+ const selector = d.name ?? d.id;
8470
+ add({
8471
+ name: selector,
8472
+ category: d.category,
8473
+ displayName: d.displayName,
8474
+ description: d.description,
8475
+ componentId: d.id
8476
+ });
8477
+ for (const [presetKey, overrides] of Object.entries(d.presets ?? {})) {
8478
+ add({
8479
+ name: `${selector}/${presetKey}`,
8480
+ category: d.category,
8481
+ displayName: `${d.displayName} (${presetKey})`,
8482
+ description: d.description,
8483
+ componentId: d.id,
8484
+ presetOptions: overrides
8485
+ });
8486
+ }
8487
+ }
8488
+ return out;
8489
+ }
8490
+ var init_expand = __esm({
8491
+ "src/catalog/expand.ts"() {
8492
+ "use strict";
8493
+ }
8494
+ });
8495
+
8087
8496
  // src/commands/list-components.ts
8088
8497
  import { defineCommand as defineCommand12 } from "citty";
8089
8498
  import { consola as consola15 } from "consola";
@@ -8091,7 +8500,8 @@ var CATEGORY_LABELS, CATEGORY_ORDER, listComponentsCommand;
8091
8500
  var init_list_components = __esm({
8092
8501
  "src/commands/list-components.ts"() {
8093
8502
  "use strict";
8094
- init_components();
8503
+ init_load();
8504
+ init_expand();
8095
8505
  init_format();
8096
8506
  CATEGORY_LABELS = {
8097
8507
  language: "Languages",
@@ -8112,7 +8522,7 @@ var init_list_components = __esm({
8112
8522
  args: {},
8113
8523
  async run() {
8114
8524
  try {
8115
- const catalog = await loadComponentCatalog();
8525
+ const catalog = expandSelectable(await loadDescriptorCatalog());
8116
8526
  if (catalog.size === 0) {
8117
8527
  consola15.warn(
8118
8528
  "No components found. The workbench checkout looks incomplete."
@@ -8123,9 +8533,9 @@ var init_list_components = __esm({
8123
8533
  const isTty2 = process.stdout.isTTY ?? false;
8124
8534
  const byCategory = /* @__PURE__ */ new Map();
8125
8535
  for (const c of catalog.values()) {
8126
- const list = byCategory.get(c.file.category) ?? [];
8127
- list.push({ name: c.name, desc: c.file.displayName });
8128
- byCategory.set(c.file.category, list);
8536
+ const list = byCategory.get(c.category) ?? [];
8537
+ list.push({ name: c.name, desc: c.displayName });
8538
+ byCategory.set(c.category, list);
8129
8539
  }
8130
8540
  for (const list of byCategory.values()) {
8131
8541
  list.sort((a, b) => a.name.localeCompare(b.name));
@@ -9001,7 +9411,7 @@ var init_remove_service = __esm({
9001
9411
 
9002
9412
  // src/devcontainer/browser-bridge.ts
9003
9413
  import { spawn as spawn9 } from "child_process";
9004
- import { existsSync as existsSync14, promises as fsp4, readFileSync as readFileSync6 } from "fs";
9414
+ import { existsSync as existsSync14, promises as fsp4, readFileSync as readFileSync5 } from "fs";
9005
9415
  import http from "http";
9006
9416
  import path23 from "path";
9007
9417
  function parseCallbackTarget(authUrl) {
@@ -9086,7 +9496,7 @@ exit 0
9086
9496
  if (!existsSync14(urlFile)) return;
9087
9497
  let content = "";
9088
9498
  try {
9089
- content = readFileSync6(urlFile, "utf8");
9499
+ content = readFileSync5(urlFile, "utf8");
9090
9500
  } catch {
9091
9501
  return;
9092
9502
  }