@getmonoceros/workbench 1.24.1 → 1.25.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.
|
@@ -23,12 +23,12 @@ service:
|
|
|
23
23
|
retries: 5
|
|
24
24
|
vscodeExtensions: [cweijan.vscode-database-client2]
|
|
25
25
|
connectionEnv:
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
URL: mysql://root:${MYSQL_ROOT_PASSWORD}@${host}:${port}/${MYSQL_DATABASE}
|
|
27
|
+
HOST: ${host}
|
|
28
|
+
PORT: ${port}
|
|
29
|
+
USER: root
|
|
30
|
+
PASSWORD: ${MYSQL_ROOT_PASSWORD}
|
|
31
|
+
DB: ${MYSQL_DATABASE}
|
|
32
32
|
options:
|
|
33
33
|
MYSQL_ROOT_PASSWORD:
|
|
34
34
|
type: string
|
|
@@ -13,12 +13,12 @@ service:
|
|
|
13
13
|
retries: 5
|
|
14
14
|
vscodeExtensions: [cweijan.vscode-database-client2]
|
|
15
15
|
connectionEnv:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${host}:${port}/${POSTGRES_DB}
|
|
17
|
+
HOST: ${host}
|
|
18
|
+
PORT: ${port}
|
|
19
|
+
USER: ${POSTGRES_USER}
|
|
20
|
+
PASSWORD: ${POSTGRES_PASSWORD}
|
|
21
|
+
DB: ${POSTGRES_DB}
|
|
22
22
|
options:
|
|
23
23
|
POSTGRES_USER:
|
|
24
24
|
type: string
|
package/dist/bin.js
CHANGED
|
@@ -203,7 +203,13 @@ var init_schema = __esm({
|
|
|
203
203
|
).optional(),
|
|
204
204
|
healthcheck: ServiceHealthcheckSchema.optional(),
|
|
205
205
|
restart: z.enum(SERVICE_RESTART_VALUES).optional(),
|
|
206
|
-
command: z.string().optional()
|
|
206
|
+
command: z.string().optional(),
|
|
207
|
+
// Connection-env templates keyed by logical SUFFIX (URL/HOST/PORT/USER/
|
|
208
|
+
// PASSWORD/DB). Baked in by `expandCuratedService`; emitted into the
|
|
209
|
+
// workspace as `<UPPER(name)>_<SUFFIX>` at apply (ADR 0021). `${host}`/
|
|
210
|
+
// `${port}`/`${<OPTION>}` are render tokens, NOT `.env` placeholders, and
|
|
211
|
+
// are left untouched by `interpolateServices`.
|
|
212
|
+
connectionEnv: z.record(z.string(), z.string()).optional()
|
|
207
213
|
});
|
|
208
214
|
ExternalServicesSchema = z.object({
|
|
209
215
|
postgres: z.string().regex(
|
|
@@ -261,6 +267,18 @@ var init_schema = __esm({
|
|
|
261
267
|
git: z.object({
|
|
262
268
|
user: GitUserSchema.optional()
|
|
263
269
|
}).optional()
|
|
270
|
+
}).superRefine((cfg, ctx) => {
|
|
271
|
+
const seen = /* @__PURE__ */ new Set();
|
|
272
|
+
cfg.services.forEach((svc, i) => {
|
|
273
|
+
if (seen.has(svc.name)) {
|
|
274
|
+
ctx.addIssue({
|
|
275
|
+
code: z.ZodIssueCode.custom,
|
|
276
|
+
path: ["services", i, "name"],
|
|
277
|
+
message: `Duplicate service name '${svc.name}'. Service names must be unique (use \`add-service \u2026 --as <name>\` to add a second instance of the same image).`
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
seen.add(svc.name);
|
|
281
|
+
});
|
|
264
282
|
});
|
|
265
283
|
}
|
|
266
284
|
});
|
|
@@ -711,13 +729,15 @@ var init_descriptor = __esm({
|
|
|
711
729
|
dataMount: z2.string().optional(),
|
|
712
730
|
healthcheck: HealthcheckSchema.optional(),
|
|
713
731
|
/**
|
|
714
|
-
* Connection env
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
718
|
-
* `${
|
|
719
|
-
* `${
|
|
720
|
-
*
|
|
732
|
+
* Connection env injected into the WORKSPACE container so the app / agent
|
|
733
|
+
* can reach this service without hardcoding anything. Keyed by logical
|
|
734
|
+
* SUFFIX → template; emitted at apply as `<UPPER(name)>_<SUFFIX>` per
|
|
735
|
+
* service instance (ADR 0021), e.g. suffix `URL` on a service named
|
|
736
|
+
* `postgres` → `POSTGRES_URL`. Tokens: `${host}` (the service's instance
|
|
737
|
+
* name), `${port}` (its port, falling back to `defaultPort`), and
|
|
738
|
+
* `${<OPTION>}` (its own option values, e.g. `${POSTGRES_USER}`). Example:
|
|
739
|
+
* URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${host}:${port}/${POSTGRES_DB}
|
|
740
|
+
* HOST: ${host}
|
|
721
741
|
*/
|
|
722
742
|
connectionEnv: z2.record(z2.string(), z2.string()).optional(),
|
|
723
743
|
vscodeExtensions: z2.array(z2.string()).optional()
|
|
@@ -2149,7 +2169,8 @@ function resolveService(entry2) {
|
|
|
2149
2169
|
volumes: entry2.volumes ? [...entry2.volumes] : [],
|
|
2150
2170
|
...entry2.healthcheck ? { healthcheck: entry2.healthcheck } : {},
|
|
2151
2171
|
...entry2.restart ? { restart: entry2.restart } : {},
|
|
2152
|
-
...entry2.command ? { command: entry2.command } : {}
|
|
2172
|
+
...entry2.command ? { command: entry2.command } : {},
|
|
2173
|
+
...entry2.connectionEnv ? { connectionEnv: { ...entry2.connectionEnv } } : {}
|
|
2153
2174
|
};
|
|
2154
2175
|
}
|
|
2155
2176
|
function isCuratedService(name) {
|
|
@@ -2173,6 +2194,10 @@ function expandCuratedService(name) {
|
|
|
2173
2194
|
} : {},
|
|
2174
2195
|
...def.dataMount ? { volumes: [`data:${def.dataMount}`] } : {},
|
|
2175
2196
|
...def.healthcheck ? { healthcheck: def.healthcheck } : {},
|
|
2197
|
+
// Bake the connection-env templates into the yml (suffix → template) so
|
|
2198
|
+
// they travel with the service: a renamed/duplicated instance keeps them,
|
|
2199
|
+
// and `serviceConnectionEnv` prefixes by the instance's current name.
|
|
2200
|
+
...def.connectionEnv ? { connectionEnv: { ...def.connectionEnv } } : {},
|
|
2176
2201
|
restart: "unless-stopped"
|
|
2177
2202
|
};
|
|
2178
2203
|
}
|
|
@@ -2183,17 +2208,18 @@ function curatedServiceEnvDefaults(name) {
|
|
|
2183
2208
|
function serviceConnectionEnv(services) {
|
|
2184
2209
|
const env = {};
|
|
2185
2210
|
for (const svc of services) {
|
|
2186
|
-
const
|
|
2187
|
-
if (!
|
|
2211
|
+
const conn = svc.connectionEnv;
|
|
2212
|
+
if (!conn || Object.keys(conn).length === 0) continue;
|
|
2213
|
+
const prefix = svc.name.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
|
|
2188
2214
|
const host = svc.name;
|
|
2189
|
-
const port = String(svc.port
|
|
2215
|
+
const port = svc.port !== void 0 ? String(svc.port) : "";
|
|
2190
2216
|
const fill = (template) => template.replace(/\$\{([A-Za-z0-9_]+)\}/g, (_, token) => {
|
|
2191
2217
|
if (token === "host") return host;
|
|
2192
2218
|
if (token === "port") return port;
|
|
2193
2219
|
return svc.env[token] ?? "";
|
|
2194
2220
|
});
|
|
2195
|
-
for (const [
|
|
2196
|
-
env[
|
|
2221
|
+
for (const [suffix, template] of Object.entries(conn)) {
|
|
2222
|
+
env[`${prefix}_${suffix}`] = fill(template);
|
|
2197
2223
|
}
|
|
2198
2224
|
}
|
|
2199
2225
|
return env;
|
|
@@ -5405,29 +5431,30 @@ function generateAgentsMd(input) {
|
|
|
5405
5431
|
}
|
|
5406
5432
|
lines.push("");
|
|
5407
5433
|
const connEnv = serviceConnectionEnv(input.services);
|
|
5408
|
-
|
|
5434
|
+
const connUrlKeys = Object.keys(connEnv).filter((k) => k.endsWith("_URL"));
|
|
5435
|
+
if (connUrlKeys.length > 0) {
|
|
5409
5436
|
lines.push(
|
|
5410
|
-
"Connection details for the curated services above are
|
|
5411
|
-
"environment variables in this container
|
|
5412
|
-
"
|
|
5413
|
-
"hardcode them:"
|
|
5437
|
+
"Connection details for the curated services above are set as",
|
|
5438
|
+
"environment variables in this container \u2014 one set per service, prefixed",
|
|
5439
|
+
"with the (uppercased) service name. Read them from the environment; do",
|
|
5440
|
+
"not ask the user for credentials and do not hardcode them. The URLs:"
|
|
5414
5441
|
);
|
|
5415
5442
|
lines.push("");
|
|
5416
|
-
|
|
5417
|
-
lines.push(
|
|
5418
|
-
"- `DATABASE_URL` \u2014 the SQL database. Engine-specific variables are",
|
|
5419
|
-
" set too (`PGHOST`/`PGPORT`/`PGUSER`/`PGPASSWORD`/`PGDATABASE` for",
|
|
5420
|
-
" Postgres, `MYSQL_*` for MySQL)."
|
|
5421
|
-
);
|
|
5422
|
-
}
|
|
5423
|
-
if (connEnv.REDIS_URL !== void 0) {
|
|
5424
|
-
lines.push("- `REDIS_URL` \u2014 Redis.");
|
|
5443
|
+
for (const k of connUrlKeys) {
|
|
5444
|
+
lines.push(`- \`${k}\``);
|
|
5425
5445
|
}
|
|
5426
5446
|
lines.push("");
|
|
5427
5447
|
lines.push(
|
|
5428
|
-
"
|
|
5429
|
-
"
|
|
5430
|
-
"
|
|
5448
|
+
"Each service also exposes `<SERVICE>_HOST`, `<SERVICE>_PORT` and, for SQL",
|
|
5449
|
+
"databases, `<SERVICE>_USER` / `<SERVICE>_PASSWORD` / `<SERVICE>_DB`. These",
|
|
5450
|
+
"are dev-only defaults for the local container, fine to read directly."
|
|
5451
|
+
);
|
|
5452
|
+
lines.push("");
|
|
5453
|
+
lines.push(
|
|
5454
|
+
"There is deliberately **no** bare `DATABASE_URL` (multiple databases",
|
|
5455
|
+
"would collide on it). If a framework or tool defaults to `DATABASE_URL`,",
|
|
5456
|
+
"set it in the project's `.env` to the right service URL, e.g.",
|
|
5457
|
+
"`DATABASE_URL=$POSTGRES_URL`."
|
|
5431
5458
|
);
|
|
5432
5459
|
}
|
|
5433
5460
|
const hasCustom = input.services.some((s) => !isCuratedService(s.name));
|
|
@@ -7233,7 +7260,7 @@ var CLI_VERSION;
|
|
|
7233
7260
|
var init_version = __esm({
|
|
7234
7261
|
"src/version.ts"() {
|
|
7235
7262
|
"use strict";
|
|
7236
|
-
CLI_VERSION = true ? "1.
|
|
7263
|
+
CLI_VERSION = true ? "1.25.0" : "dev";
|
|
7237
7264
|
}
|
|
7238
7265
|
});
|
|
7239
7266
|
|
|
@@ -9610,6 +9637,24 @@ function nextRelayUrl(content, lastOpened) {
|
|
|
9610
9637
|
const url = content.trim();
|
|
9611
9638
|
return url && url !== lastOpened ? url : null;
|
|
9612
9639
|
}
|
|
9640
|
+
function wrapExec(command, opts) {
|
|
9641
|
+
const leading = [];
|
|
9642
|
+
const stmts = [];
|
|
9643
|
+
if (opts.pathPrepend) {
|
|
9644
|
+
leading.push(opts.pathPrepend);
|
|
9645
|
+
const i = leading.length;
|
|
9646
|
+
stmts.push(`export PATH="$${i}:$PATH"`);
|
|
9647
|
+
stmts.push(`export BROWSER="$${i}/xdg-open"`);
|
|
9648
|
+
}
|
|
9649
|
+
if (opts.cwd) {
|
|
9650
|
+
leading.push(opts.cwd);
|
|
9651
|
+
stmts.push(`cd -- "$${leading.length}"`);
|
|
9652
|
+
}
|
|
9653
|
+
if (leading.length === 0) return command;
|
|
9654
|
+
const shift = leading.length === 1 ? "shift" : `shift ${leading.length}`;
|
|
9655
|
+
const script = `${stmts.join(" && ")} && ${shift} && exec "$@"`;
|
|
9656
|
+
return ["bash", "-lc", script, "bash", ...leading, ...command];
|
|
9657
|
+
}
|
|
9613
9658
|
function openInBrowser(url) {
|
|
9614
9659
|
const platform = process.platform;
|
|
9615
9660
|
const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
@@ -9755,17 +9800,28 @@ async function runShell(opts) {
|
|
|
9755
9800
|
{ quiet: true }
|
|
9756
9801
|
);
|
|
9757
9802
|
if (upCode !== 0) return upCode;
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9803
|
+
const name = opts.name ?? path26.basename(opts.root);
|
|
9804
|
+
const isTty2 = opts.isTty ?? process.stdout.isTTY;
|
|
9805
|
+
const bridge = isTty2 ? await startBrowserBridge({ name, root: opts.root, spawn: spawnFn }) : null;
|
|
9806
|
+
try {
|
|
9807
|
+
const innerExec = wrapExec(
|
|
9808
|
+
["bash"],
|
|
9809
|
+
bridge ? { pathPrepend: bridge.relayDirInContainer } : {}
|
|
9810
|
+
);
|
|
9811
|
+
return await spawnFn(
|
|
9812
|
+
[
|
|
9813
|
+
"exec",
|
|
9814
|
+
"--workspace-folder",
|
|
9815
|
+
opts.root,
|
|
9816
|
+
"--mount-workspace-git-root=false",
|
|
9817
|
+
...innerExec
|
|
9818
|
+
],
|
|
9762
9819
|
opts.root,
|
|
9763
|
-
|
|
9764
|
-
|
|
9765
|
-
|
|
9766
|
-
|
|
9767
|
-
|
|
9768
|
-
);
|
|
9820
|
+
{ interactive: true }
|
|
9821
|
+
);
|
|
9822
|
+
} finally {
|
|
9823
|
+
if (bridge) await bridge.dispose();
|
|
9824
|
+
}
|
|
9769
9825
|
}
|
|
9770
9826
|
function assertContainerExists(root) {
|
|
9771
9827
|
if (!existsSync17(path26.join(root, ".devcontainer"))) {
|
|
@@ -9777,29 +9833,12 @@ function assertContainerExists(root) {
|
|
|
9777
9833
|
var init_shell = __esm({
|
|
9778
9834
|
"src/devcontainer/shell.ts"() {
|
|
9779
9835
|
"use strict";
|
|
9836
|
+
init_browser_bridge();
|
|
9780
9837
|
init_cli();
|
|
9781
9838
|
}
|
|
9782
9839
|
});
|
|
9783
9840
|
|
|
9784
9841
|
// src/devcontainer/run.ts
|
|
9785
|
-
function wrapExec(command, opts) {
|
|
9786
|
-
const leading = [];
|
|
9787
|
-
const stmts = [];
|
|
9788
|
-
if (opts.pathPrepend) {
|
|
9789
|
-
leading.push(opts.pathPrepend);
|
|
9790
|
-
const i = leading.length;
|
|
9791
|
-
stmts.push(`export PATH="$${i}:$PATH"`);
|
|
9792
|
-
stmts.push(`export BROWSER="$${i}/xdg-open"`);
|
|
9793
|
-
}
|
|
9794
|
-
if (opts.cwd) {
|
|
9795
|
-
leading.push(opts.cwd);
|
|
9796
|
-
stmts.push(`cd -- "$${leading.length}"`);
|
|
9797
|
-
}
|
|
9798
|
-
if (leading.length === 0) return command;
|
|
9799
|
-
const shift = leading.length === 1 ? "shift" : `shift ${leading.length}`;
|
|
9800
|
-
const script = `${stmts.join(" && ")} && ${shift} && exec "$@"`;
|
|
9801
|
-
return ["bash", "-lc", script, "bash", ...leading, ...command];
|
|
9802
|
-
}
|
|
9803
9842
|
async function runInContainer(opts) {
|
|
9804
9843
|
if (opts.command.length === 0) {
|
|
9805
9844
|
throw new Error(
|
|
@@ -9932,7 +9971,10 @@ var init_shell2 = __esm({
|
|
|
9932
9971
|
},
|
|
9933
9972
|
async run({ args }) {
|
|
9934
9973
|
try {
|
|
9935
|
-
const exitCode = await runShell({
|
|
9974
|
+
const exitCode = await runShell({
|
|
9975
|
+
root: containerDir(args.name),
|
|
9976
|
+
name: args.name
|
|
9977
|
+
});
|
|
9936
9978
|
process.exit(exitCode);
|
|
9937
9979
|
} catch (err) {
|
|
9938
9980
|
consola30.error(err instanceof Error ? err.message : String(err));
|