@getmonoceros/workbench 1.24.2 → 1.26.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.
@@ -0,0 +1,17 @@
1
+ id: mailpit
2
+ category: service
3
+ displayName: Mailpit
4
+ description: 'SMTP catcher for local dev — apps send mail to it and nothing leaves the machine. SMTP on `mailpit:1025`; web UI + API on 8025 (reach via `monoceros tunnel <name> mailpit:8025`).'
5
+ service:
6
+ image: axllent/mailpit:latest
7
+ defaultPort: 1025
8
+ healthcheck:
9
+ # Mailpit ships its own readiness probe in the image — no curl/wget needed.
10
+ test: [CMD, /mailpit, readyz]
11
+ interval: 15s
12
+ timeout: 5s
13
+ retries: 5
14
+ connectionEnv:
15
+ URL: smtp://${host}:${port}
16
+ HOST: ${host}
17
+ PORT: ${port}
@@ -0,0 +1,36 @@
1
+ id: mongodb
2
+ category: service
3
+ displayName: MongoDB
4
+ description: 'MongoDB with seeded dev credentials and a readiness healthcheck. Reachable in-container as host `mongodb`.'
5
+ service:
6
+ image: mongo:8
7
+ defaultPort: 27017
8
+ dataMount: /data/db
9
+ healthcheck:
10
+ test: [CMD, mongosh, --quiet, --eval, 'db.adminCommand({ ping: 1 }).ok']
11
+ interval: 10s
12
+ timeout: 5s
13
+ retries: 5
14
+ vscodeExtensions: [mongodb.mongodb-vscode]
15
+ client:
16
+ npm: [mongosh]
17
+ connectionEnv:
18
+ URL: mongodb://${MONGO_INITDB_ROOT_USERNAME}:${MONGO_INITDB_ROOT_PASSWORD}@${host}:${port}/${MONGO_INITDB_DATABASE}?authSource=admin
19
+ HOST: ${host}
20
+ PORT: ${port}
21
+ USER: ${MONGO_INITDB_ROOT_USERNAME}
22
+ PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD}
23
+ DB: ${MONGO_INITDB_DATABASE}
24
+ options:
25
+ MONGO_INITDB_ROOT_USERNAME:
26
+ type: string
27
+ default: monoceros
28
+ surface: env
29
+ MONGO_INITDB_ROOT_PASSWORD:
30
+ type: string
31
+ default: monoceros
32
+ surface: env
33
+ MONGO_INITDB_DATABASE:
34
+ type: string
35
+ default: monoceros
36
+ surface: env
@@ -22,13 +22,15 @@ service:
22
22
  timeout: 5s
23
23
  retries: 5
24
24
  vscodeExtensions: [cweijan.vscode-database-client2]
25
+ client:
26
+ apt: [default-mysql-client]
25
27
  connectionEnv:
26
- MYSQL_HOST: ${host}
27
- MYSQL_PORT: ${port}
28
- MYSQL_USER: root
29
- MYSQL_PASSWORD: ${MYSQL_ROOT_PASSWORD}
30
- MYSQL_DATABASE: ${MYSQL_DATABASE}
31
- DATABASE_URL: mysql://root:${MYSQL_ROOT_PASSWORD}@${host}:${port}/${MYSQL_DATABASE}
28
+ URL: mysql://root:${MYSQL_ROOT_PASSWORD}@${host}:${port}/${MYSQL_DATABASE}
29
+ HOST: ${host}
30
+ PORT: ${port}
31
+ USER: root
32
+ PASSWORD: ${MYSQL_ROOT_PASSWORD}
33
+ DB: ${MYSQL_DATABASE}
32
34
  options:
33
35
  MYSQL_ROOT_PASSWORD:
34
36
  type: string
@@ -0,0 +1,36 @@
1
+ id: pgvector
2
+ category: service
3
+ displayName: pgvector (PostgreSQL + vectors)
4
+ description: 'PostgreSQL with the pgvector extension for embeddings / vector search — a drop-in Postgres. Reachable in-container as host `pgvector`. Enable the extension once with `CREATE EXTENSION vector;` (or in your migration).'
5
+ service:
6
+ image: pgvector/pgvector:pg18
7
+ defaultPort: 5432
8
+ dataMount: /var/lib/postgresql
9
+ healthcheck:
10
+ test: [CMD, pg_isready, -U, '${POSTGRES_USER}', -d, '${POSTGRES_DB}']
11
+ interval: 10s
12
+ timeout: 5s
13
+ retries: 5
14
+ vscodeExtensions: [cweijan.vscode-database-client2]
15
+ client:
16
+ apt: [postgresql-client]
17
+ connectionEnv:
18
+ URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${host}:${port}/${POSTGRES_DB}
19
+ HOST: ${host}
20
+ PORT: ${port}
21
+ USER: ${POSTGRES_USER}
22
+ PASSWORD: ${POSTGRES_PASSWORD}
23
+ DB: ${POSTGRES_DB}
24
+ options:
25
+ POSTGRES_USER:
26
+ type: string
27
+ default: monoceros
28
+ surface: env
29
+ POSTGRES_PASSWORD:
30
+ type: string
31
+ default: monoceros
32
+ surface: env
33
+ POSTGRES_DB:
34
+ type: string
35
+ default: monoceros
36
+ surface: env
@@ -12,13 +12,15 @@ service:
12
12
  timeout: 5s
13
13
  retries: 5
14
14
  vscodeExtensions: [cweijan.vscode-database-client2]
15
+ client:
16
+ apt: [postgresql-client]
15
17
  connectionEnv:
16
- PGHOST: ${host}
17
- PGPORT: ${port}
18
- PGUSER: ${POSTGRES_USER}
19
- PGPASSWORD: ${POSTGRES_PASSWORD}
20
- PGDATABASE: ${POSTGRES_DB}
21
- DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${host}:${port}/${POSTGRES_DB}
18
+ URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${host}:${port}/${POSTGRES_DB}
19
+ HOST: ${host}
20
+ PORT: ${port}
21
+ USER: ${POSTGRES_USER}
22
+ PASSWORD: ${POSTGRES_PASSWORD}
23
+ DB: ${POSTGRES_DB}
22
24
  options:
23
25
  POSTGRES_USER:
24
26
  type: string
@@ -12,5 +12,9 @@ service:
12
12
  timeout: 5s
13
13
  retries: 5
14
14
  vscodeExtensions: [cweijan.vscode-database-client2]
15
+ client:
16
+ apt: [redis-tools]
15
17
  connectionEnv:
16
- REDIS_URL: redis://${host}:${port}
18
+ URL: redis://${host}:${port}
19
+ HOST: ${host}
20
+ PORT: ${port}
@@ -0,0 +1,28 @@
1
+ id: rustfs
2
+ category: service
3
+ displayName: RustFS
4
+ description: 'S3-compatible object storage (MinIO-compatible API). S3 API on `rustfs:9000`; web console on 9001 (enable with `RUSTFS_CONSOLE_ENABLE=true`, reach via `monoceros tunnel <name> rustfs:9001`).'
5
+ service:
6
+ image: rustfs/rustfs:latest
7
+ defaultPort: 9000
8
+ dataMount: /data
9
+ healthcheck:
10
+ test: [CMD, curl, -f, 'http://localhost:9000/minio/health/live']
11
+ interval: 10s
12
+ timeout: 5s
13
+ retries: 5
14
+ connectionEnv:
15
+ URL: http://${host}:${port}
16
+ HOST: ${host}
17
+ PORT: ${port}
18
+ ACCESS_KEY: ${RUSTFS_ACCESS_KEY}
19
+ SECRET_KEY: ${RUSTFS_SECRET_KEY}
20
+ options:
21
+ RUSTFS_ACCESS_KEY:
22
+ type: string
23
+ default: rustfsadmin
24
+ surface: env
25
+ RUSTFS_SECRET_KEY:
26
+ type: string
27
+ default: rustfsadmin
28
+ surface: env
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,15 +729,30 @@ var init_descriptor = __esm({
711
729
  dataMount: z2.string().optional(),
712
730
  healthcheck: HealthcheckSchema.optional(),
713
731
  /**
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}
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(),
743
+ /**
744
+ * CLI client tool(s) for this service, installed into the WORKSPACE
745
+ * container at apply so the dev/agent can use them (the service runs in its
746
+ * own container; the workspace has no client otherwise). `apt` packages are
747
+ * merged into the workspace's apt-packages feature (build-time, cached);
748
+ * `npm` packages are installed globally in post-create (guarded, so it's a
749
+ * no-op once present). E.g. postgres → apt `postgresql-client` (`psql`),
750
+ * mongodb → npm `mongosh`. See ADR 0020.
751
+ */
752
+ client: z2.object({
753
+ apt: z2.array(z2.string()).optional(),
754
+ npm: z2.array(z2.string()).optional()
755
+ }).optional(),
723
756
  vscodeExtensions: z2.array(z2.string()).optional()
724
757
  });
725
758
  PersistentHomeFileSchema = z2.object({
@@ -2149,7 +2182,8 @@ function resolveService(entry2) {
2149
2182
  volumes: entry2.volumes ? [...entry2.volumes] : [],
2150
2183
  ...entry2.healthcheck ? { healthcheck: entry2.healthcheck } : {},
2151
2184
  ...entry2.restart ? { restart: entry2.restart } : {},
2152
- ...entry2.command ? { command: entry2.command } : {}
2185
+ ...entry2.command ? { command: entry2.command } : {},
2186
+ ...entry2.connectionEnv ? { connectionEnv: { ...entry2.connectionEnv } } : {}
2153
2187
  };
2154
2188
  }
2155
2189
  function isCuratedService(name) {
@@ -2173,6 +2207,10 @@ function expandCuratedService(name) {
2173
2207
  } : {},
2174
2208
  ...def.dataMount ? { volumes: [`data:${def.dataMount}`] } : {},
2175
2209
  ...def.healthcheck ? { healthcheck: def.healthcheck } : {},
2210
+ // Bake the connection-env templates into the yml (suffix → template) so
2211
+ // they travel with the service: a renamed/duplicated instance keeps them,
2212
+ // and `serviceConnectionEnv` prefixes by the instance's current name.
2213
+ ...def.connectionEnv ? { connectionEnv: { ...def.connectionEnv } } : {},
2176
2214
  restart: "unless-stopped"
2177
2215
  };
2178
2216
  }
@@ -2183,21 +2221,40 @@ function curatedServiceEnvDefaults(name) {
2183
2221
  function serviceConnectionEnv(services) {
2184
2222
  const env = {};
2185
2223
  for (const svc of services) {
2186
- const def = SERVICE_CATALOG[svc.name];
2187
- if (!def?.connectionEnv) continue;
2224
+ const conn = svc.connectionEnv ?? SERVICE_CATALOG[svc.name]?.connectionEnv;
2225
+ if (!conn || Object.keys(conn).length === 0) continue;
2226
+ const prefix = svc.name.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase();
2188
2227
  const host = svc.name;
2189
- const port = String(svc.port ?? def.defaultPort);
2228
+ const port = svc.port !== void 0 ? String(svc.port) : "";
2190
2229
  const fill = (template) => template.replace(/\$\{([A-Za-z0-9_]+)\}/g, (_, token) => {
2191
2230
  if (token === "host") return host;
2192
2231
  if (token === "port") return port;
2193
2232
  return svc.env[token] ?? "";
2194
2233
  });
2195
- for (const [key, template] of Object.entries(def.connectionEnv)) {
2196
- env[key] = fill(template);
2234
+ for (const [suffix, template] of Object.entries(conn)) {
2235
+ env[`${prefix}_${suffix}`] = fill(template);
2197
2236
  }
2198
2237
  }
2199
2238
  return env;
2200
2239
  }
2240
+ function serviceClientAptPackages(services) {
2241
+ const pkgs = /* @__PURE__ */ new Set();
2242
+ for (const svc of services) {
2243
+ for (const pkg of SERVICE_CATALOG[svc.name]?.client?.apt ?? []) {
2244
+ pkgs.add(pkg);
2245
+ }
2246
+ }
2247
+ return [...pkgs].sort();
2248
+ }
2249
+ function serviceClientNpmPackages(services) {
2250
+ const pkgs = /* @__PURE__ */ new Set();
2251
+ for (const svc of services) {
2252
+ for (const pkg of SERVICE_CATALOG[svc.name]?.client?.npm ?? []) {
2253
+ pkgs.add(pkg);
2254
+ }
2255
+ }
2256
+ return [...pkgs].sort();
2257
+ }
2201
2258
  function deriveServiceName(image) {
2202
2259
  const lastSegment = image.split("/").pop() ?? image;
2203
2260
  const noTag = lastSegment.split("@")[0].split(":")[0];
@@ -2257,7 +2314,8 @@ var init_catalog = __esm({
2257
2314
  ...svc.dataMount ? { dataMount: svc.dataMount } : {},
2258
2315
  defaultPort: svc.defaultPort,
2259
2316
  ...svc.vscodeExtensions ? { vscodeExtensions: svc.vscodeExtensions } : {},
2260
- ...svc.connectionEnv ? { connectionEnv: svc.connectionEnv } : {}
2317
+ ...svc.connectionEnv ? { connectionEnv: svc.connectionEnv } : {},
2318
+ ...svc.client ? { client: svc.client } : {}
2261
2319
  };
2262
2320
  return [key, entry2];
2263
2321
  })
@@ -2774,10 +2832,16 @@ function resolveFeatures(opts) {
2774
2832
  persistentHomeFiles: []
2775
2833
  });
2776
2834
  }
2777
- if (opts.aptPackages && opts.aptPackages.length > 0) {
2835
+ const aptPackages = [
2836
+ .../* @__PURE__ */ new Set([
2837
+ ...opts.aptPackages ?? [],
2838
+ ...serviceClientAptPackages(opts.services)
2839
+ ])
2840
+ ].sort();
2841
+ if (aptPackages.length > 0) {
2778
2842
  resolved.push({
2779
2843
  devcontainerKey: "ghcr.io/devcontainers-contrib/features/apt-packages:1",
2780
- options: { packages: opts.aptPackages.join(",") },
2844
+ options: { packages: aptPackages.join(",") },
2781
2845
  persistentHomePaths: [],
2782
2846
  persistentHomeFiles: []
2783
2847
  });
@@ -3171,6 +3235,18 @@ function buildPostCreateScript(opts) {
3171
3235
  " pnpm install",
3172
3236
  "fi"
3173
3237
  ];
3238
+ const clientNpm = serviceClientNpmPackages(opts.services);
3239
+ if (clientNpm.length > 0) {
3240
+ lines.push(
3241
+ "",
3242
+ "# CLI clients for the curated services (global npm installs)."
3243
+ );
3244
+ for (const pkg of clientNpm) {
3245
+ lines.push(
3246
+ `command -v ${pkg} >/dev/null 2>&1 || npm install -g --no-audit --no-fund ${pkg}`
3247
+ );
3248
+ }
3249
+ }
3174
3250
  if (opts.installUrls && opts.installUrls.length > 0) {
3175
3251
  lines.push(
3176
3252
  "",
@@ -5405,29 +5481,30 @@ function generateAgentsMd(input) {
5405
5481
  }
5406
5482
  lines.push("");
5407
5483
  const connEnv = serviceConnectionEnv(input.services);
5408
- if (Object.keys(connEnv).length > 0) {
5484
+ const connUrlKeys = Object.keys(connEnv).filter((k) => k.endsWith("_URL"));
5485
+ if (connUrlKeys.length > 0) {
5409
5486
  lines.push(
5410
- "Connection details for the curated services above are already set as",
5411
- "environment variables in this container. Read them from the",
5412
- "environment \u2014 do not ask the user for credentials, and do not",
5413
- "hardcode them:"
5487
+ "Connection details for the curated services above are set as",
5488
+ "environment variables in this container \u2014 one set per service, prefixed",
5489
+ "with the (uppercased) service name. Read them from the environment; do",
5490
+ "not ask the user for credentials and do not hardcode them. The URLs:"
5414
5491
  );
5415
5492
  lines.push("");
5416
- if (connEnv.DATABASE_URL !== void 0) {
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.");
5493
+ for (const k of connUrlKeys) {
5494
+ lines.push(`- \`${k}\``);
5425
5495
  }
5426
5496
  lines.push("");
5427
5497
  lines.push(
5428
- "These are dev-only defaults for the local container, fine to use",
5429
- "directly. Prefer reading the variable (e.g. `process.env.DATABASE_URL`)",
5430
- "over copying its value into code."
5498
+ "Each service also exposes `<SERVICE>_HOST`, `<SERVICE>_PORT` and, for SQL",
5499
+ "databases, `<SERVICE>_USER` / `<SERVICE>_PASSWORD` / `<SERVICE>_DB`. These",
5500
+ "are dev-only defaults for the local container, fine to read directly."
5501
+ );
5502
+ lines.push("");
5503
+ lines.push(
5504
+ "There is deliberately **no** bare `DATABASE_URL` (multiple databases",
5505
+ "would collide on it). If a framework or tool defaults to `DATABASE_URL`,",
5506
+ "set it in the project's `.env` to the right service URL, e.g.",
5507
+ "`DATABASE_URL=$POSTGRES_URL`."
5431
5508
  );
5432
5509
  }
5433
5510
  const hasCustom = input.services.some((s) => !isCuratedService(s.name));
@@ -7233,7 +7310,7 @@ var CLI_VERSION;
7233
7310
  var init_version = __esm({
7234
7311
  "src/version.ts"() {
7235
7312
  "use strict";
7236
- CLI_VERSION = true ? "1.24.2" : "dev";
7313
+ CLI_VERSION = true ? "1.26.0" : "dev";
7237
7314
  }
7238
7315
  });
7239
7316