@getmonoceros/workbench 1.37.14 → 1.38.1

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.
@@ -9,7 +9,14 @@ service:
9
9
  # Dev mode + realm import. The Keycloak image has no auto-start default,
10
10
  # so a command is mandatory. `--import-realm` reads every *.json from
11
11
  # /opt/keycloak/data/import at startup (a no-op if the dir is empty).
12
- command: start-dev --import-realm
12
+ # `--proxy-headers=xforwarded` makes Keycloak derive the issuer host and
13
+ # scheme from X-Forwarded-Host / X-Forwarded-Proto, so the realm stamps a
14
+ # matching issuer whether it is reached over http via the Traefik proxy
15
+ # (`plantlove.localhost`) or over https via `monoceros share` on a phone.
16
+ # Without it Keycloak uses the raw connection scheme (always http behind a
17
+ # TLS-terminating proxy) and the OIDC token exchange fails on the https
18
+ # client. See ADR 0033.
19
+ command: start-dev --import-realm --proxy-headers=xforwarded
13
20
  # Starts in a host-side second wave AFTER the repo clone (ADR 0025), so a
14
21
  # mounted realm.json / theme from the project is present at boot. No
15
22
  # dataMount: the dev H2 database is ephemeral and re-seeds from the
package/dist/bin.js CHANGED
@@ -4107,12 +4107,12 @@ async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
4107
4107
  const devcontainerDir = path11.join(targetDir, ".devcontainer");
4108
4108
  const monocerosDir = path11.join(targetDir, ".monoceros");
4109
4109
  const projectsDir = path11.join(targetDir, "projects");
4110
- const homeDir = path11.join(targetDir, "home");
4110
+ const homeDir2 = path11.join(targetDir, "home");
4111
4111
  const dataDir = path11.join(targetDir, "data");
4112
4112
  await fs9.mkdir(devcontainerDir, { recursive: true });
4113
4113
  await fs9.mkdir(monocerosDir, { recursive: true });
4114
4114
  await fs9.mkdir(projectsDir, { recursive: true });
4115
- await fs9.mkdir(homeDir, { recursive: true });
4115
+ await fs9.mkdir(homeDir2, { recursive: true });
4116
4116
  if (needsCompose(opts)) {
4117
4117
  await fs9.mkdir(dataDir, { recursive: true });
4118
4118
  for (const svc of opts.services) {
@@ -4176,10 +4176,10 @@ async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
4176
4176
  }
4177
4177
  for (const f of resolvedFeatures) {
4178
4178
  for (const sub of f.persistentHomePaths) {
4179
- await fs9.mkdir(path11.join(homeDir, sub), { recursive: true });
4179
+ await fs9.mkdir(path11.join(homeDir2, sub), { recursive: true });
4180
4180
  }
4181
4181
  for (const entry2 of f.persistentHomeFiles) {
4182
- const filePath = path11.join(homeDir, entry2.path);
4182
+ const filePath = path11.join(homeDir2, entry2.path);
4183
4183
  await fs9.mkdir(path11.dirname(filePath), { recursive: true });
4184
4184
  if (!existsSync8(filePath)) {
4185
4185
  await fs9.writeFile(filePath, entry2.initialContent);
@@ -4593,75 +4593,87 @@ function generateAgentsMd(input) {
4593
4593
  " DataGrip) to one of the services."
4594
4594
  );
4595
4595
  lines.push("");
4596
- if (input.ports.length > 0) {
4597
- lines.push("## Running a long-running server");
4598
- lines.push("");
4599
- lines.push(
4600
- "When you build something that serves on a port (a web app, an API),",
4601
- "it must keep running after this session ends. A plain `npm start` (or",
4602
- "any foreground start) dies the moment the user exits you or closes the",
4603
- `terminal, and then \`${input.containerName}.localhost${portSuffix}\` returns 502 Bad Gateway.`
4604
- );
4605
- lines.push("");
4606
- lines.push(
4607
- "Declare it in the app's own launch config at",
4608
- "`projects/<app>/.monoceros/launch.json`, then start it with",
4609
- "`monoceros-ctl`. Add or update an entry whenever you set up a",
4610
- "long-running server. The file travels with the app, so the human can",
4611
- "restart it later without knowing your start command:"
4612
- );
4613
- lines.push("");
4614
- lines.push("```json");
4615
- lines.push("{");
4616
- lines.push(' "configurations": [');
4617
- lines.push(
4618
- ` { "name": "web", "command": "<the project's start command>", "port": ${input.ports[0]}, "default": true }`
4619
- );
4620
- lines.push(" ]");
4621
- lines.push("}");
4622
- lines.push("```");
4623
- lines.push("");
4596
+ const examplePort = input.ports.length > 0 ? String(input.ports[0]) : "<port>";
4597
+ lines.push("## Running a long-running server");
4598
+ lines.push("");
4599
+ lines.push(
4600
+ "When you build something that serves on a port (a web app, an API),",
4601
+ "it must keep running after this session ends. A plain `npm start` (or",
4602
+ "any foreground start) dies the moment the user exits you or closes the",
4603
+ input.ports.length > 0 ? `terminal, and then \`${input.containerName}.localhost${portSuffix}\` returns 502 Bad Gateway.` : "terminal, and the app stops responding."
4604
+ );
4605
+ lines.push("");
4606
+ if (input.ports.length === 0) {
4624
4607
  lines.push(
4625
- "Use whatever start command the project actually uses (`npm run dev`,",
4626
- "`./mvnw spring-boot:run`, `python manage.py runserver`, `go run .`, \u2026).",
4627
- "Do not force a language-specific one. `<app>` is the path under",
4628
- "`projects/`; `port` must be a port already exposed on the container."
4608
+ "This container exposes **no ports yet**, so a server has nothing to be",
4609
+ "reached on. Before serving one, ask the user to add a port on the host",
4610
+ "and re-apply - you cannot do this from inside:"
4629
4611
  );
4630
4612
  lines.push("");
4631
- lines.push("Start it, stop it, tail its log:");
4632
- lines.push("");
4633
4613
  lines.push("```");
4634
- lines.push("monoceros-ctl start <app>");
4635
- lines.push("monoceros-ctl stop <app>");
4636
- lines.push("monoceros-ctl logs <app>");
4614
+ lines.push(`monoceros add-port ${input.containerName} <port>`);
4615
+ lines.push(`monoceros apply ${input.containerName}`);
4637
4616
  lines.push("```");
4638
4617
  lines.push("");
4639
- lines.push(
4640
- "`start` launches it detached (it survives your session) and, when a",
4641
- "`port` is set, waits until it actually listens before returning. The",
4642
- "human can do the same from the host with",
4643
- `\`monoceros start ${input.containerName} <app>\` / \`monoceros stop ${input.containerName} <app>\`,`,
4644
- `and follow output with \`monoceros logs ${input.containerName} <app>\`.`
4645
- );
4646
- lines.push("");
4647
- lines.push(
4648
- "An app can declare several servers (e.g. an API and a web frontend).",
4649
- 'Mark every server that should come up together with `"default": true`;',
4650
- "`monoceros-ctl start <app>` (no `--target`) then starts the whole default",
4651
- "set in the order the entries appear in the file, waiting for each",
4652
- "server's `port` to listen before starting the next - so order an entry",
4653
- "before anything that depends on it. If one fails to come up, the rest are",
4654
- "not started. Pass `--target <name>` to start or stop a single one."
4655
- );
4656
- lines.push("");
4657
- lines.push(
4658
- "The server must listen on `0.0.0.0` (not `127.0.0.1`) on the exposed",
4659
- "port, or Traefik cannot reach it. You only have the ports already",
4660
- "declared on the container; if you need another, ask the human to add it",
4661
- `on the host (\`monoceros add-port ${input.containerName} <port>\`) and re-apply.`
4662
- );
4663
- lines.push("");
4664
4618
  }
4619
+ lines.push(
4620
+ "Declare the server in the app's own launch config at",
4621
+ "`projects/<app>/.monoceros/launch.json`, then start it with",
4622
+ "`monoceros-ctl`. Add or update an entry whenever you set up a",
4623
+ "long-running server. The file travels with the app, so the human can",
4624
+ "restart it later without knowing your start command:"
4625
+ );
4626
+ lines.push("");
4627
+ lines.push("```json");
4628
+ lines.push("{");
4629
+ lines.push(' "configurations": [');
4630
+ lines.push(
4631
+ ` { "name": "web", "command": "<the project's start command>", "port": ${examplePort}, "default": true }`
4632
+ );
4633
+ lines.push(" ]");
4634
+ lines.push("}");
4635
+ lines.push("```");
4636
+ lines.push("");
4637
+ lines.push(
4638
+ "Use whatever start command the project actually uses (`npm run dev`,",
4639
+ "`./mvnw spring-boot:run`, `python manage.py runserver`, `go run .`, \u2026).",
4640
+ "Do not force a language-specific one. `<app>` is the path under",
4641
+ "`projects/`; `port` must be a port exposed on the container."
4642
+ );
4643
+ lines.push("");
4644
+ lines.push("Start it, stop it, tail its log:");
4645
+ lines.push("");
4646
+ lines.push("```");
4647
+ lines.push("monoceros-ctl start <app>");
4648
+ lines.push("monoceros-ctl stop <app>");
4649
+ lines.push("monoceros-ctl logs <app>");
4650
+ lines.push("```");
4651
+ lines.push("");
4652
+ lines.push(
4653
+ "`start` launches it detached (it survives your session) and, when a",
4654
+ "`port` is set, waits until it actually listens before returning. The",
4655
+ "human can do the same from the host with",
4656
+ `\`monoceros start ${input.containerName} <app>\` / \`monoceros stop ${input.containerName} <app>\`,`,
4657
+ `and follow output with \`monoceros logs ${input.containerName} <app>\`.`
4658
+ );
4659
+ lines.push("");
4660
+ lines.push(
4661
+ "An app can declare several servers (e.g. an API and a web frontend).",
4662
+ 'Mark every server that should come up together with `"default": true`;',
4663
+ "`monoceros-ctl start <app>` (no `--target`) then starts the whole default",
4664
+ "set in the order the entries appear in the file, waiting for each",
4665
+ "server's `port` to listen before starting the next - so order an entry",
4666
+ "before anything that depends on it. If one fails to come up, the rest are",
4667
+ "not started. Pass `--target <name>` to start or stop a single one."
4668
+ );
4669
+ lines.push("");
4670
+ lines.push(
4671
+ "The server must listen on `0.0.0.0` (not `127.0.0.1`) on the exposed",
4672
+ "port, or Traefik cannot reach it. You only have the ports already",
4673
+ "declared on the container; if you need another, ask the human to add it",
4674
+ `on the host (\`monoceros add-port ${input.containerName} <port>\`) and re-apply.`
4675
+ );
4676
+ lines.push("");
4665
4677
  lines.push("## Command reference");
4666
4678
  lines.push("");
4667
4679
  lines.push(
@@ -5481,8 +5493,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
5481
5493
  if (!isMap2(item)) return false;
5482
5494
  const url = item.get("url");
5483
5495
  if (url === urlOrPath) return true;
5484
- const path33 = item.get("path");
5485
- const effectivePath = typeof path33 === "string" ? path33 : typeof url === "string" ? deriveRepoName(url) : void 0;
5496
+ const path35 = item.get("path");
5497
+ const effectivePath = typeof path35 === "string" ? path35 : typeof url === "string" ? deriveRepoName(url) : void 0;
5486
5498
  return effectivePath === urlOrPath;
5487
5499
  });
5488
5500
  if (idx < 0) return false;
@@ -5625,7 +5637,7 @@ async function runAddRepo(input) {
5625
5637
  "Missing repo URL. Usage: monoceros add-repo <containername> <url>."
5626
5638
  );
5627
5639
  }
5628
- const path33 = (input.path ?? deriveRepoName(url)).trim();
5640
+ const path35 = (input.path ?? deriveRepoName(url)).trim();
5629
5641
  const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
5630
5642
  const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
5631
5643
  if (hasName !== hasEmail) {
@@ -5662,7 +5674,7 @@ async function runAddRepo(input) {
5662
5674
  const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
5663
5675
  const entry2 = {
5664
5676
  url,
5665
- path: path33,
5677
+ path: path35,
5666
5678
  ...hasName && hasEmail ? {
5667
5679
  gitUser: {
5668
5680
  name: input.gitName.trim(),
@@ -8974,7 +8986,7 @@ var CLI_VERSION;
8974
8986
  var init_version = __esm({
8975
8987
  "src/version.ts"() {
8976
8988
  "use strict";
8977
- CLI_VERSION = true ? "1.37.14" : "dev";
8989
+ CLI_VERSION = true ? "1.38.1" : "dev";
8978
8990
  }
8979
8991
  });
8980
8992
 
@@ -12370,12 +12382,220 @@ var init_run3 = __esm({
12370
12382
  }
12371
12383
  });
12372
12384
 
12373
- // src/share/run.ts
12385
+ // src/tls/ca.ts
12386
+ import { promises as fs21 } from "fs";
12374
12387
  import os3 from "os";
12388
+ import path33 from "path";
12389
+ import forge from "node-forge";
12390
+ function homeDir(monocerosHome2) {
12391
+ return monocerosHome2 ?? monocerosHome();
12392
+ }
12393
+ function randomSerial() {
12394
+ return "00" + forge.util.bytesToHex(forge.random.getBytesSync(16));
12395
+ }
12396
+ function caCommonName() {
12397
+ const host = os3.hostname().replace(/\.local$/, "").trim();
12398
+ return host ? `Monoceros Local CA (${host})` : "Monoceros Local CA";
12399
+ }
12400
+ async function fileExists(p) {
12401
+ try {
12402
+ await fs21.access(p);
12403
+ return true;
12404
+ } catch {
12405
+ return false;
12406
+ }
12407
+ }
12408
+ async function loadOrCreateCa(monocerosHome2) {
12409
+ const dir = path33.join(homeDir(monocerosHome2), CA_DIR);
12410
+ const certPath = path33.join(dir, CA_CERT_FILE);
12411
+ const keyPath = path33.join(dir, CA_KEY_FILE);
12412
+ if (await fileExists(certPath) && await fileExists(keyPath)) {
12413
+ const cert2 = forge.pki.certificateFromPem(
12414
+ await fs21.readFile(certPath, "utf8")
12415
+ );
12416
+ const key = forge.pki.privateKeyFromPem(
12417
+ await fs21.readFile(keyPath, "utf8")
12418
+ );
12419
+ return { cert: cert2, key, certPath, keyPath };
12420
+ }
12421
+ const keys = forge.pki.rsa.generateKeyPair(2048);
12422
+ const cert = forge.pki.createCertificate();
12423
+ cert.publicKey = keys.publicKey;
12424
+ cert.serialNumber = randomSerial();
12425
+ cert.validity.notBefore = /* @__PURE__ */ new Date();
12426
+ cert.validity.notAfter = new Date(
12427
+ cert.validity.notBefore.getTime() + CA_DAYS * 24 * 60 * 60 * 1e3
12428
+ );
12429
+ const attrs = [{ name: "commonName", value: caCommonName() }];
12430
+ cert.setSubject(attrs);
12431
+ cert.setIssuer(attrs);
12432
+ cert.setExtensions([
12433
+ { name: "basicConstraints", cA: true, critical: true },
12434
+ { name: "keyUsage", keyCertSign: true, cRLSign: true, critical: true },
12435
+ { name: "subjectKeyIdentifier" }
12436
+ ]);
12437
+ cert.sign(keys.privateKey, forge.md.sha256.create());
12438
+ await fs21.mkdir(dir, { recursive: true });
12439
+ await fs21.writeFile(certPath, forge.pki.certificateToPem(cert), {
12440
+ mode: 420
12441
+ });
12442
+ await fs21.writeFile(keyPath, forge.pki.privateKeyToPem(keys.privateKey), {
12443
+ mode: 384
12444
+ });
12445
+ return { cert, key: keys.privateKey, certPath, keyPath };
12446
+ }
12447
+ function normalizeSans(sans) {
12448
+ return [...new Set(sans.filter((s) => s.length > 0))].sort();
12449
+ }
12450
+ async function readLeafMeta(metaPath) {
12451
+ try {
12452
+ return JSON.parse(await fs21.readFile(metaPath, "utf8"));
12453
+ } catch {
12454
+ return null;
12455
+ }
12456
+ }
12457
+ function leafStillGood(meta, sans) {
12458
+ if (!meta) return false;
12459
+ if (meta.sans.join(",") !== sans.join(",")) return false;
12460
+ const notAfter = new Date(meta.notAfter).getTime();
12461
+ if (Number.isNaN(notAfter)) return false;
12462
+ return notAfter - Date.now() > LEAF_RENEW_BEFORE_MS;
12463
+ }
12464
+ async function ensureLeafCert(opts) {
12465
+ const sans = normalizeSans(opts.sans);
12466
+ const dir = path33.join(homeDir(opts.monocerosHome), CERTS_DIR);
12467
+ const certPath = path33.join(dir, LEAF_CERT_FILE);
12468
+ const keyPath = path33.join(dir, LEAF_KEY_FILE);
12469
+ const metaPath = path33.join(dir, LEAF_META_FILE);
12470
+ const ca = await loadOrCreateCa(opts.monocerosHome);
12471
+ const haveFiles = await fileExists(certPath) && await fileExists(keyPath);
12472
+ if (haveFiles && leafStillGood(await readLeafMeta(metaPath), sans)) {
12473
+ return {
12474
+ caCertPath: ca.certPath,
12475
+ certDir: dir,
12476
+ certFile: LEAF_CERT_FILE,
12477
+ keyFile: LEAF_KEY_FILE
12478
+ };
12479
+ }
12480
+ const keys = forge.pki.rsa.generateKeyPair(2048);
12481
+ const cert = forge.pki.createCertificate();
12482
+ cert.publicKey = keys.publicKey;
12483
+ cert.serialNumber = randomSerial();
12484
+ cert.validity.notBefore = /* @__PURE__ */ new Date();
12485
+ cert.validity.notAfter = new Date(
12486
+ cert.validity.notBefore.getTime() + LEAF_DAYS * 24 * 60 * 60 * 1e3
12487
+ );
12488
+ cert.setSubject([{ name: "commonName", value: "monoceros share" }]);
12489
+ cert.setIssuer(ca.cert.subject.attributes);
12490
+ cert.setExtensions([
12491
+ { name: "basicConstraints", cA: false },
12492
+ {
12493
+ name: "keyUsage",
12494
+ digitalSignature: true,
12495
+ keyEncipherment: true,
12496
+ critical: true
12497
+ },
12498
+ { name: "extKeyUsage", serverAuth: true },
12499
+ {
12500
+ name: "subjectAltName",
12501
+ altNames: sans.map(
12502
+ (s) => IPV4_RE2.test(s) ? { type: 7, ip: s } : { type: 2, value: s }
12503
+ )
12504
+ }
12505
+ ]);
12506
+ cert.sign(ca.key, forge.md.sha256.create());
12507
+ await fs21.mkdir(dir, { recursive: true });
12508
+ await fs21.writeFile(certPath, forge.pki.certificateToPem(cert), {
12509
+ mode: 420
12510
+ });
12511
+ await fs21.writeFile(keyPath, forge.pki.privateKeyToPem(keys.privateKey), {
12512
+ mode: 384
12513
+ });
12514
+ const meta = {
12515
+ sans,
12516
+ notAfter: cert.validity.notAfter.toISOString()
12517
+ };
12518
+ await fs21.writeFile(metaPath, JSON.stringify(meta), { mode: 420 });
12519
+ return {
12520
+ caCertPath: ca.certPath,
12521
+ certDir: dir,
12522
+ certFile: LEAF_CERT_FILE,
12523
+ keyFile: LEAF_KEY_FILE
12524
+ };
12525
+ }
12526
+ async function provisionShareTls(opts) {
12527
+ return ensureLeafCert(opts);
12528
+ }
12529
+ var CA_DIR, CA_CERT_FILE, CA_KEY_FILE, CERTS_DIR, LEAF_CERT_FILE, LEAF_KEY_FILE, LEAF_META_FILE, CA_DAYS, LEAF_DAYS, LEAF_RENEW_BEFORE_MS, IPV4_RE2;
12530
+ var init_ca = __esm({
12531
+ "src/tls/ca.ts"() {
12532
+ "use strict";
12533
+ init_paths();
12534
+ CA_DIR = "ca";
12535
+ CA_CERT_FILE = "rootCA.pem";
12536
+ CA_KEY_FILE = "rootCA-key.pem";
12537
+ CERTS_DIR = "certs";
12538
+ LEAF_CERT_FILE = "leaf.pem";
12539
+ LEAF_KEY_FILE = "leaf-key.pem";
12540
+ LEAF_META_FILE = "leaf.json";
12541
+ CA_DAYS = 3650;
12542
+ LEAF_DAYS = 397;
12543
+ LEAF_RENEW_BEFORE_MS = 7 * 24 * 60 * 60 * 1e3;
12544
+ IPV4_RE2 = /^(\d{1,3}\.){3}\d{1,3}$/;
12545
+ }
12546
+ });
12547
+
12548
+ // src/share/caddy.ts
12549
+ function renderCaddyfile(sites, certFile, keyFile) {
12550
+ const blocks = sites.map(
12551
+ (s) => `:${s.port} {
12552
+ tls /certs/${certFile} /certs/${keyFile}
12553
+ reverse_proxy http://${s.targetHost}:${s.port}
12554
+ }`
12555
+ );
12556
+ return [
12557
+ "{",
12558
+ " auto_https off",
12559
+ " admin off",
12560
+ " servers {",
12561
+ " protocols h1 h2",
12562
+ " }",
12563
+ "}",
12564
+ ...blocks,
12565
+ ""
12566
+ ].join("\n");
12567
+ }
12568
+ function buildCaddyDockerArgs(input) {
12569
+ const args = ["run", "--rm", "-i", `--network=${input.network}`];
12570
+ for (const port of input.ports) {
12571
+ args.push("-p", `${input.localAddress}:${port}:${port}`);
12572
+ }
12573
+ args.push(
12574
+ "-v",
12575
+ `${input.certDir}:/certs:ro`,
12576
+ "-v",
12577
+ `${input.caddyfilePath}:/etc/caddy/Caddyfile:ro`,
12578
+ CADDY_IMAGE
12579
+ );
12580
+ return args;
12581
+ }
12582
+ var CADDY_IMAGE;
12583
+ var init_caddy = __esm({
12584
+ "src/share/caddy.ts"() {
12585
+ "use strict";
12586
+ CADDY_IMAGE = "caddy:2.11.4";
12587
+ }
12588
+ });
12589
+
12590
+ // src/share/run.ts
12591
+ import os4 from "os";
12592
+ import { spawnSync as spawnSync2 } from "child_process";
12375
12593
  import { consola as consola35 } from "consola";
12594
+ import { promises as fs22 } from "fs";
12595
+ import path34 from "path";
12376
12596
  function realHostAddresses() {
12377
12597
  let ip;
12378
- for (const list of Object.values(os3.networkInterfaces())) {
12598
+ for (const list of Object.values(os4.networkInterfaces())) {
12379
12599
  for (const addr of list ?? []) {
12380
12600
  if (addr.family === "IPv4" && !addr.internal) {
12381
12601
  ip = addr.address;
@@ -12384,9 +12604,18 @@ function realHostAddresses() {
12384
12604
  }
12385
12605
  if (ip) break;
12386
12606
  }
12387
- const hn = os3.hostname();
12388
- const mdnsName = hn.endsWith(".local") ? hn : `${hn}.local`;
12389
- return { ...ip ? { ip } : {}, mdnsName };
12607
+ return { ...ip ? { ip } : {}, mdnsName: mdnsHostName() };
12608
+ }
12609
+ function mdnsHostName() {
12610
+ if (process.platform === "darwin") {
12611
+ const res = spawnSync2("scutil", ["--get", "LocalHostName"], {
12612
+ encoding: "utf8"
12613
+ });
12614
+ const name = res.status === 0 ? res.stdout.trim() : "";
12615
+ if (name) return `${name}.local`;
12616
+ }
12617
+ const hn = os4.hostname();
12618
+ return hn.endsWith(".local") ? hn : `${hn}.local`;
12390
12619
  }
12391
12620
  async function runShare(opts) {
12392
12621
  const log = opts.logger ?? {
@@ -12418,31 +12647,62 @@ async function runShare(opts) {
12418
12647
  for (const port of ports) {
12419
12648
  await preflight({ port, address: SHARE_ADDRESS });
12420
12649
  }
12650
+ const { ip, mdnsName } = (opts.hostAddresses ?? realHostAddresses)();
12651
+ const sans = [mdnsName, ip, "localhost", "127.0.0.1"].filter(
12652
+ (s) => typeof s === "string" && s.length > 0
12653
+ );
12654
+ const provisionTls = opts.provisionTls ?? provisionShareTls;
12655
+ const tls = await provisionTls({
12656
+ sans,
12657
+ ...opts.monocerosHome !== void 0 ? { monocerosHome: opts.monocerosHome } : {}
12658
+ });
12659
+ const sites = ports.map((port) => ({
12660
+ port,
12661
+ targetHost: base.targetHost
12662
+ }));
12663
+ const home = opts.monocerosHome ?? monocerosHome();
12664
+ const shareDir = path34.join(home, "share");
12665
+ await fs22.mkdir(shareDir, { recursive: true });
12666
+ const caddyfilePath = path34.join(
12667
+ shareDir,
12668
+ `${opts.name}__${opts.app}.Caddyfile`
12669
+ );
12670
+ await fs22.rm(caddyfilePath, { force: true });
12671
+ await fs22.writeFile(
12672
+ caddyfilePath,
12673
+ renderCaddyfile(sites, tls.certFile, tls.keyFile)
12674
+ );
12421
12675
  const dockerSpawn = opts.dockerSpawn ?? defaultDockerSpawn;
12422
- const handles = ports.map(
12423
- (port) => dockerSpawn(
12424
- buildDockerArgs({
12676
+ const handles = [
12677
+ dockerSpawn(
12678
+ buildCaddyDockerArgs({
12425
12679
  localAddress: SHARE_ADDRESS,
12426
- localPort: port,
12427
- internalPort: port,
12680
+ ports,
12428
12681
  network: base.network,
12429
- targetHost: base.targetHost
12682
+ certDir: tls.certDir,
12683
+ caddyfilePath
12430
12684
  })
12431
12685
  )
12432
- );
12433
- const { ip, mdnsName } = (opts.hostAddresses ?? realHostAddresses)();
12434
- const host = ip ?? mdnsName ?? "<host-ip>";
12686
+ ];
12687
+ const host = mdnsName ?? ip ?? "<host-ip>";
12435
12688
  log.info(
12436
12689
  `Sharing ${opts.name}/${opts.app} on the local network (Ctrl+C to stop):`
12437
12690
  );
12438
12691
  for (const t of ported) {
12439
- log.info(` ${cyan2(t.name)} http://${host}:${t.port}`);
12692
+ log.info(` ${cyan2(t.name)} https://${host}:${t.port}`);
12440
12693
  }
12441
12694
  if (mdnsName && ip) {
12442
12695
  log.info(
12443
- dim(` also reachable as http://${mdnsName}:<port> where mDNS resolves`)
12696
+ dim(
12697
+ ` also reachable as https://${ip}:<port> if .local does not resolve`
12698
+ )
12444
12699
  );
12445
12700
  }
12701
+ log.info(
12702
+ dim(
12703
+ ` First device? Trust the local CA once so HTTPS is warning-free: ${tls.caCertPath}`
12704
+ )
12705
+ );
12446
12706
  const installSignalHandler = opts.installSignalHandler ?? installSigintDefault;
12447
12707
  let stopping = false;
12448
12708
  const uninstall = installSignalHandler(() => {
@@ -12466,6 +12726,9 @@ var init_run4 = __esm({
12466
12726
  init_resolve2();
12467
12727
  init_port_check2();
12468
12728
  init_run3();
12729
+ init_ca();
12730
+ init_caddy();
12731
+ init_paths();
12469
12732
  init_format();
12470
12733
  SHARE_ADDRESS = "0.0.0.0";
12471
12734
  }
@@ -12483,7 +12746,7 @@ var init_share = __esm({
12483
12746
  meta: {
12484
12747
  name: "share",
12485
12748
  group: "discovery",
12486
- description: "Expose an app's ports to the local network (phone, tablet, other devices) so any device can open it - reached via the host's LAN IP (or `.local` name). Every target in the app's launch.json that declares a port is shared. Foreground: Ctrl+C stops sharing. See ADR 0030."
12749
+ description: "Expose an app's ports to the local network (phone, tablet, other devices) over HTTPS so any device can open it - reached via the host's LAN IP (or `.local` name). TLS is served with a machine-local CA; trust the printed rootCA.pem once per device for warning-free HTTPS (and a working PWA secure context). Every target in the app's launch.json that declares a port is shared. Foreground: Ctrl+C stops sharing. See ADR 0030 and 0033."
12487
12750
  },
12488
12751
  args: {
12489
12752
  name: {
@@ -13267,7 +13530,7 @@ var init_prune = __esm({
13267
13530
  });
13268
13531
 
13269
13532
  // src/upgrade/index.ts
13270
- import { existsSync as existsSync21, promises as fs21 } from "fs";
13533
+ import { existsSync as existsSync21, promises as fs23 } from "fs";
13271
13534
  import { consola as consola41 } from "consola";
13272
13535
  async function fetchRuntimeVersions() {
13273
13536
  const tokenUrl = `https://ghcr.io/token?service=ghcr.io&scope=repository:${RUNTIME_REPO}:pull`;
@@ -13384,10 +13647,10 @@ async function runUpgrade(opts) {
13384
13647
  for (const name of targets) {
13385
13648
  const ymlPath = containerConfigPath(name, home);
13386
13649
  if (!existsSync21(ymlPath)) continue;
13387
- const raw = await fs21.readFile(ymlPath, "utf8");
13650
+ const raw = await fs23.readFile(ymlPath, "utf8");
13388
13651
  const updated = setRuntimeVersion(raw, pinVersion);
13389
13652
  if (updated !== raw) {
13390
- await fs21.writeFile(ymlPath, updated);
13653
+ await fs23.writeFile(ymlPath, updated);
13391
13654
  bumped += 1;
13392
13655
  logger.info(`Pinned '${name}' to runtime ${pinVersion}.`);
13393
13656
  }
@@ -13429,7 +13692,7 @@ function formatPruneLine(prune) {
13429
13692
  }
13430
13693
  async function listContainerNames2(home) {
13431
13694
  try {
13432
- const entries = await fs21.readdir(containerConfigsDir(home));
13695
+ const entries = await fs23.readdir(containerConfigsDir(home));
13433
13696
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length));
13434
13697
  } catch {
13435
13698
  return [];
@@ -13844,25 +14107,25 @@ function detectHelpRequest(argv, main2) {
13844
14107
  const separatorIdx = argv.indexOf("--");
13845
14108
  if (helpIdx === -1) return null;
13846
14109
  if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
13847
- const path33 = [];
14110
+ const path35 = [];
13848
14111
  const tokens = argv.slice(
13849
14112
  0,
13850
14113
  separatorIdx === -1 ? argv.length : separatorIdx
13851
14114
  );
13852
14115
  let cursor = main2;
13853
14116
  const mainName = (main2.meta ?? {}).name ?? "monoceros";
13854
- path33.push(mainName);
14117
+ path35.push(mainName);
13855
14118
  for (const tok of tokens) {
13856
14119
  if (tok.startsWith("-")) continue;
13857
14120
  const subs = cursor.subCommands ?? {};
13858
14121
  if (tok in subs) {
13859
14122
  cursor = subs[tok];
13860
- path33.push(tok);
14123
+ path35.push(tok);
13861
14124
  continue;
13862
14125
  }
13863
14126
  break;
13864
14127
  }
13865
- return { path: path33, cmd: cursor };
14128
+ return { path: path35, cmd: cursor };
13866
14129
  }
13867
14130
  async function maybeRenderHelp(argv, main2) {
13868
14131
  const hit = detectHelpRequest(argv, main2);