@getmonoceros/workbench 1.21.2 → 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.
- package/bundled-components/features/atlassian/component.yml +52 -0
- package/bundled-components/features/atlassian/install.sh +176 -0
- package/bundled-components/features/claude-code/component.yml +32 -0
- package/bundled-components/features/claude-code/install.sh +56 -0
- package/bundled-components/features/github-cli/component.yml +18 -0
- package/bundled-components/features/github-cli/install.sh +53 -0
- package/bundled-components/languages/dotnet/component.yml +8 -0
- package/bundled-components/languages/go/component.yml +8 -0
- package/bundled-components/languages/java/component.yml +17 -0
- package/bundled-components/languages/node/component.yml +9 -0
- package/bundled-components/languages/python/component.yml +8 -0
- package/bundled-components/languages/rust/component.yml +8 -0
- package/bundled-components/services/mysql/component.yml +40 -0
- package/bundled-components/services/postgres/component.yml +34 -0
- package/bundled-components/services/redis/component.yml +16 -0
- package/dist/bin.js +791 -369
- package/dist/bin.js.map +1 -1
- package/package.json +5 -4
- package/features/atlassian/devcontainer-feature.json +0 -67
- package/features/claude-code/devcontainer-feature.json +0 -49
- package/features/github-cli/devcontainer-feature.json +0 -32
- package/templates/components/README.md +0 -95
- package/templates/components/atlassian/rovodev.yml +0 -14
- package/templates/components/atlassian/twg.yml +0 -14
- package/templates/components/atlassian.yml +0 -15
- package/templates/components/claude.yml +0 -16
- package/templates/components/dotnet.yml +0 -8
- package/templates/components/github.yml +0 -10
- package/templates/components/go.yml +0 -8
- package/templates/components/java.yml +0 -9
- package/templates/components/mysql.yml +0 -8
- package/templates/components/node.yml +0 -9
- package/templates/components/postgres.yml +0 -10
- package/templates/components/python.yml +0 -9
- package/templates/components/redis.yml +0 -7
- 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(
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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", "
|
|
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
|
-
|
|
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
|
-
|
|
689
|
-
if (!manifestPath) return void 0;
|
|
981
|
+
let descriptor;
|
|
690
982
|
try {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
const
|
|
748
|
-
for (const
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
if (
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
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
|
|
813
|
-
import
|
|
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 =
|
|
963
|
-
const credentialsPath =
|
|
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
|
|
1028
|
-
await
|
|
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
|
|
1184
|
-
import { z as
|
|
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
|
|
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
|
|
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
|
|
1245
|
-
await
|
|
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
|
|
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 =
|
|
1379
|
-
schemaVersion:
|
|
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:
|
|
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:
|
|
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:
|
|
1404
|
-
|
|
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
|
-
|
|
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:
|
|
1415
|
-
hostPort:
|
|
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:
|
|
1421
|
-
staleDays:
|
|
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
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
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
|
-
|
|
1444
|
-
const
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
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
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2186
|
+
const def = SERVICE_CATALOG[svc.name];
|
|
2187
|
+
if (!def?.connectionEnv) continue;
|
|
1916
2188
|
const host = svc.name;
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
env
|
|
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
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
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
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
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,
|
|
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, "
|
|
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
|
-
|
|
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
|
-
|
|
2358
|
-
|
|
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
|
|
2382
|
-
const manifestPath = path9.join(localSourceDir, "devcontainer-feature.json");
|
|
2664
|
+
function featureDescriptor(name) {
|
|
2383
2665
|
try {
|
|
2384
|
-
const
|
|
2385
|
-
|
|
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
|
|
2669
|
+
return void 0;
|
|
2392
2670
|
}
|
|
2393
2671
|
}
|
|
2394
|
-
function
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
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.
|
|
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
|
|
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) =>
|
|
2972
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|
|
@@ -4967,6 +5316,13 @@ function generateAgentsMd(input) {
|
|
|
4967
5316
|
}
|
|
4968
5317
|
}
|
|
4969
5318
|
lines.push("");
|
|
5319
|
+
lines.push(
|
|
5320
|
+
"To show the user a running app, open it in their host browser with",
|
|
5321
|
+
`\`xdg-open http://${input.containerName}.localhost\` \u2014 Monoceros relays`,
|
|
5322
|
+
"browser-opens from the container to the host machine. Also tell the user",
|
|
5323
|
+
"the URL, so they can open it themselves if no bridge is active."
|
|
5324
|
+
);
|
|
5325
|
+
lines.push("");
|
|
4970
5326
|
}
|
|
4971
5327
|
lines.push("## How to extend this container");
|
|
4972
5328
|
lines.push("");
|
|
@@ -5548,13 +5904,13 @@ var init_runtime_pull_hint = __esm({
|
|
|
5548
5904
|
|
|
5549
5905
|
// src/devcontainer/cli.ts
|
|
5550
5906
|
import { spawn as spawn4 } from "child_process";
|
|
5551
|
-
import { readFileSync as
|
|
5907
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
5552
5908
|
import { createRequire } from "module";
|
|
5553
5909
|
import path14 from "path";
|
|
5554
5910
|
function devcontainerCliPath() {
|
|
5555
5911
|
if (cachedBinaryPath) return cachedBinaryPath;
|
|
5556
5912
|
const pkgJsonPath = require_.resolve("@devcontainers/cli/package.json");
|
|
5557
|
-
const pkg = JSON.parse(
|
|
5913
|
+
const pkg = JSON.parse(readFileSync4(pkgJsonPath, "utf8"));
|
|
5558
5914
|
const binEntry = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.devcontainer ?? "";
|
|
5559
5915
|
if (!binEntry) {
|
|
5560
5916
|
throw new Error("Could not resolve @devcontainers/cli bin entry.");
|
|
@@ -6699,7 +7055,7 @@ var CLI_VERSION;
|
|
|
6699
7055
|
var init_version = __esm({
|
|
6700
7056
|
"src/version.ts"() {
|
|
6701
7057
|
"use strict";
|
|
6702
|
-
CLI_VERSION = true ? "1.
|
|
7058
|
+
CLI_VERSION = true ? "1.22.0" : "dev";
|
|
6703
7059
|
}
|
|
6704
7060
|
});
|
|
6705
7061
|
|
|
@@ -7346,7 +7702,21 @@ function generateComposedYml(name, composed, lookupManifest, repoUrls = [], port
|
|
|
7346
7702
|
false
|
|
7347
7703
|
);
|
|
7348
7704
|
lines.push("languages:");
|
|
7349
|
-
for (const lang of composed.languages)
|
|
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
|
+
}
|
|
7350
7720
|
lines.push("");
|
|
7351
7721
|
}
|
|
7352
7722
|
if (composed.aptPackages.length > 0) {
|
|
@@ -7454,7 +7824,8 @@ function generateDocumentedYml(name, catalog, lookupManifest, repoUrls = [], por
|
|
|
7454
7824
|
lines.push("# languages:");
|
|
7455
7825
|
for (const c of byCategory.language) {
|
|
7456
7826
|
for (const lang of c.file.contributes.languages ?? []) {
|
|
7457
|
-
|
|
7827
|
+
const ver = LANGUAGE_CATALOG[lang]?.defaultVersion;
|
|
7828
|
+
lines.push(`# - ${ver ? `${lang}:${ver}` : lang}`);
|
|
7458
7829
|
}
|
|
7459
7830
|
}
|
|
7460
7831
|
lines.push("");
|
|
@@ -7697,7 +8068,6 @@ import { existsSync as existsSync10, promises as fs14 } from "fs";
|
|
|
7697
8068
|
import path19 from "path";
|
|
7698
8069
|
import { consola as consola13 } from "consola";
|
|
7699
8070
|
async function runInit(opts) {
|
|
7700
|
-
const workbench = opts.workbenchRoot ?? workbenchRoot();
|
|
7701
8071
|
const home = opts.monocerosHome ?? monocerosHome();
|
|
7702
8072
|
const logger = opts.logger ?? {
|
|
7703
8073
|
success: (msg) => consola13.success(msg),
|
|
@@ -7714,14 +8084,14 @@ async function runInit(opts) {
|
|
|
7714
8084
|
`Config already exists: ${dest}. Delete it manually before re-running \`monoceros init\` \u2014 this protects any hand-edits.`
|
|
7715
8085
|
);
|
|
7716
8086
|
}
|
|
7717
|
-
const
|
|
8087
|
+
const componentsRoot = opts.workbenchRoot ? path19.join(opts.workbenchRoot, "components") : componentsRootDir();
|
|
8088
|
+
const catalog = await loadComponentCatalog(componentsRoot);
|
|
7718
8089
|
if (catalog.size === 0) {
|
|
7719
8090
|
throw new Error(
|
|
7720
|
-
`No components found under ${
|
|
8091
|
+
`No components found under ${componentsRoot}. The workbench checkout is incomplete.`
|
|
7721
8092
|
);
|
|
7722
8093
|
}
|
|
7723
|
-
const
|
|
7724
|
-
const lookup = (ref) => loadFeatureManifestSummary(ref, checkoutRoot);
|
|
8094
|
+
const lookup = (ref) => loadFeatureManifestSummary(ref, componentsRoot);
|
|
7725
8095
|
const reposRaw = (opts.withRepo ?? []).map((u) => u.trim()).filter((u) => u.length > 0);
|
|
7726
8096
|
const repos = [];
|
|
7727
8097
|
const seenRepoUrls = /* @__PURE__ */ new Set();
|
|
@@ -7842,7 +8212,14 @@ function resolveInitLanguages(entries) {
|
|
|
7842
8212
|
continue;
|
|
7843
8213
|
}
|
|
7844
8214
|
seen.add(e);
|
|
7845
|
-
|
|
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
|
+
});
|
|
7846
8223
|
}
|
|
7847
8224
|
if (unknown.length > 0) {
|
|
7848
8225
|
throw new Error(
|
|
@@ -8077,6 +8454,45 @@ var init_init2 = __esm({
|
|
|
8077
8454
|
}
|
|
8078
8455
|
});
|
|
8079
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
|
+
|
|
8080
8496
|
// src/commands/list-components.ts
|
|
8081
8497
|
import { defineCommand as defineCommand12 } from "citty";
|
|
8082
8498
|
import { consola as consola15 } from "consola";
|
|
@@ -8084,7 +8500,8 @@ var CATEGORY_LABELS, CATEGORY_ORDER, listComponentsCommand;
|
|
|
8084
8500
|
var init_list_components = __esm({
|
|
8085
8501
|
"src/commands/list-components.ts"() {
|
|
8086
8502
|
"use strict";
|
|
8087
|
-
|
|
8503
|
+
init_load();
|
|
8504
|
+
init_expand();
|
|
8088
8505
|
init_format();
|
|
8089
8506
|
CATEGORY_LABELS = {
|
|
8090
8507
|
language: "Languages",
|
|
@@ -8105,7 +8522,7 @@ var init_list_components = __esm({
|
|
|
8105
8522
|
args: {},
|
|
8106
8523
|
async run() {
|
|
8107
8524
|
try {
|
|
8108
|
-
const catalog = await
|
|
8525
|
+
const catalog = expandSelectable(await loadDescriptorCatalog());
|
|
8109
8526
|
if (catalog.size === 0) {
|
|
8110
8527
|
consola15.warn(
|
|
8111
8528
|
"No components found. The workbench checkout looks incomplete."
|
|
@@ -8116,9 +8533,9 @@ var init_list_components = __esm({
|
|
|
8116
8533
|
const isTty2 = process.stdout.isTTY ?? false;
|
|
8117
8534
|
const byCategory = /* @__PURE__ */ new Map();
|
|
8118
8535
|
for (const c of catalog.values()) {
|
|
8119
|
-
const list = byCategory.get(c.
|
|
8120
|
-
list.push({ name: c.name, desc: c.
|
|
8121
|
-
byCategory.set(c.
|
|
8536
|
+
const list = byCategory.get(c.category) ?? [];
|
|
8537
|
+
list.push({ name: c.name, desc: c.displayName });
|
|
8538
|
+
byCategory.set(c.category, list);
|
|
8122
8539
|
}
|
|
8123
8540
|
for (const list of byCategory.values()) {
|
|
8124
8541
|
list.sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -8994,7 +9411,7 @@ var init_remove_service = __esm({
|
|
|
8994
9411
|
|
|
8995
9412
|
// src/devcontainer/browser-bridge.ts
|
|
8996
9413
|
import { spawn as spawn9 } from "child_process";
|
|
8997
|
-
import { existsSync as existsSync14, promises as fsp4, readFileSync as
|
|
9414
|
+
import { existsSync as existsSync14, promises as fsp4, readFileSync as readFileSync5 } from "fs";
|
|
8998
9415
|
import http from "http";
|
|
8999
9416
|
import path23 from "path";
|
|
9000
9417
|
function parseCallbackTarget(authUrl) {
|
|
@@ -9011,6 +9428,10 @@ function parseCallbackTarget(authUrl) {
|
|
|
9011
9428
|
return null;
|
|
9012
9429
|
}
|
|
9013
9430
|
}
|
|
9431
|
+
function nextRelayUrl(content, lastOpened) {
|
|
9432
|
+
const url = content.trim();
|
|
9433
|
+
return url && url !== lastOpened ? url : null;
|
|
9434
|
+
}
|
|
9014
9435
|
function openInBrowser(url) {
|
|
9015
9436
|
const platform = process.platform;
|
|
9016
9437
|
const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
@@ -9041,7 +9462,7 @@ exit 0
|
|
|
9041
9462
|
);
|
|
9042
9463
|
await fsp4.chmod(relayScript, 493);
|
|
9043
9464
|
const servers = [];
|
|
9044
|
-
let
|
|
9465
|
+
let lastOpened = null;
|
|
9045
9466
|
const onUrl = (url) => {
|
|
9046
9467
|
openInBrowser(url);
|
|
9047
9468
|
const target = parseCallbackTarget(url);
|
|
@@ -9072,16 +9493,17 @@ exit 0
|
|
|
9072
9493
|
servers.push(server);
|
|
9073
9494
|
};
|
|
9074
9495
|
const poll = setInterval(() => {
|
|
9075
|
-
if (
|
|
9496
|
+
if (!existsSync14(urlFile)) return;
|
|
9076
9497
|
let content = "";
|
|
9077
9498
|
try {
|
|
9078
|
-
content =
|
|
9499
|
+
content = readFileSync5(urlFile, "utf8");
|
|
9079
9500
|
} catch {
|
|
9080
9501
|
return;
|
|
9081
9502
|
}
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9503
|
+
const url = nextRelayUrl(content, lastOpened);
|
|
9504
|
+
if (!url) return;
|
|
9505
|
+
lastOpened = url;
|
|
9506
|
+
onUrl(url);
|
|
9085
9507
|
}, 250);
|
|
9086
9508
|
return {
|
|
9087
9509
|
relayDirInContainer: `/workspaces/${opts.name}/${RELAY_DIRNAME}`,
|