@getmonoceros/workbench 1.17.1 → 1.19.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/dist/bin.js CHANGED
@@ -1903,6 +1903,41 @@ function curatedServiceEnvDefaults(name) {
1903
1903
  const def = SERVICE_CATALOG[name];
1904
1904
  return def?.env ? { ...def.env } : {};
1905
1905
  }
1906
+ function serviceConnectionEnv(services) {
1907
+ const env = {};
1908
+ for (const svc of services) {
1909
+ if (!isCuratedService(svc.name)) continue;
1910
+ const host = svc.name;
1911
+ if (svc.name === "postgres") {
1912
+ const user = svc.env.POSTGRES_USER ?? "postgres";
1913
+ const pass = svc.env.POSTGRES_PASSWORD ?? "";
1914
+ const db = svc.env.POSTGRES_DB ?? user;
1915
+ const port = svc.port ?? 5432;
1916
+ env.PGHOST = host;
1917
+ env.PGPORT = String(port);
1918
+ env.PGUSER = user;
1919
+ env.PGPASSWORD = pass;
1920
+ env.PGDATABASE = db;
1921
+ env.DATABASE_URL = `postgresql://${user}:${pass}@${host}:${port}/${db}`;
1922
+ } else if (svc.name === "mysql") {
1923
+ const pass = svc.env.MYSQL_ROOT_PASSWORD ?? "";
1924
+ const db = svc.env.MYSQL_DATABASE ?? "";
1925
+ const port = svc.port ?? 3306;
1926
+ env.MYSQL_HOST = host;
1927
+ env.MYSQL_PORT = String(port);
1928
+ env.MYSQL_USER = "root";
1929
+ env.MYSQL_PASSWORD = pass;
1930
+ env.MYSQL_DATABASE = db;
1931
+ if (env.DATABASE_URL === void 0) {
1932
+ env.DATABASE_URL = `mysql://root:${pass}@${host}:${port}/${db}`;
1933
+ }
1934
+ } else if (svc.name === "redis") {
1935
+ const port = svc.port ?? 6379;
1936
+ env.REDIS_URL = `redis://${host}:${port}`;
1937
+ }
1938
+ }
1939
+ return env;
1940
+ }
1906
1941
  function deriveServiceName(image) {
1907
1942
  const lastSegment = image.split("/").pop() ?? image;
1908
1943
  const noTag = lastSegment.split("@")[0].split(":")[0];
@@ -2438,6 +2473,14 @@ function buildComposeYaml(opts, dockerMode = "rootful") {
2438
2473
  for (const v of ideVolumes) {
2439
2474
  lines.push(` - ${v.volume}:${v.target}`);
2440
2475
  }
2476
+ const wsEnv = serviceConnectionEnv(opts.services);
2477
+ const wsEnvKeys = Object.keys(wsEnv);
2478
+ if (wsEnvKeys.length > 0) {
2479
+ lines.push(" environment:");
2480
+ for (const k of wsEnvKeys) {
2481
+ lines.push(` ${k}: ${composeScalar(wsEnv[k])}`);
2482
+ }
2483
+ }
2441
2484
  for (const svc of opts.services) {
2442
2485
  lines.push(` ${svc.name}:`);
2443
2486
  lines.push(` image: ${svc.image}`);
@@ -3216,8 +3259,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
3216
3259
  if (!isMap2(item)) return false;
3217
3260
  const url = item.get("url");
3218
3261
  if (url === urlOrPath) return true;
3219
- const path22 = item.get("path");
3220
- const effectivePath = typeof path22 === "string" ? path22 : typeof url === "string" ? deriveRepoName(url) : void 0;
3262
+ const path24 = item.get("path");
3263
+ const effectivePath = typeof path24 === "string" ? path24 : typeof url === "string" ? deriveRepoName(url) : void 0;
3221
3264
  return effectivePath === urlOrPath;
3222
3265
  });
3223
3266
  if (idx < 0) return false;
@@ -3329,7 +3372,7 @@ async function runAddRepo(input) {
3329
3372
  "Missing repo URL. Usage: monoceros add-repo <containername> <url>."
3330
3373
  );
3331
3374
  }
3332
- const path22 = (input.path ?? deriveRepoName(url)).trim();
3375
+ const path24 = (input.path ?? deriveRepoName(url)).trim();
3333
3376
  const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
3334
3377
  const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
3335
3378
  if (hasName !== hasEmail) {
@@ -3366,7 +3409,7 @@ async function runAddRepo(input) {
3366
3409
  const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
3367
3410
  const entry2 = {
3368
3411
  url,
3369
- path: path22,
3412
+ path: path24,
3370
3413
  ...hasName && hasEmail ? {
3371
3414
  gitUser: {
3372
3415
  name: input.gitName.trim(),
@@ -4767,22 +4810,42 @@ function generateAgentsMd(input) {
4767
4810
  lines.push(formatServiceLine(svc));
4768
4811
  }
4769
4812
  lines.push("");
4770
- lines.push(
4771
- "Credentials for these services are intentionally not stored in this",
4772
- "file. If you need to connect from code or from the command line, ask",
4773
- "the user in the current chat; they will paste the values directly.",
4774
- "Do not commit credentials provided this way into any file in the",
4775
- "repo \u2014 they belong in the user's `.env` on the host, not in source",
4776
- "control."
4777
- );
4813
+ const connEnv = serviceConnectionEnv(input.services);
4814
+ if (Object.keys(connEnv).length > 0) {
4815
+ lines.push(
4816
+ "Connection details for the curated services above are already set as",
4817
+ "environment variables in this container. Read them from the",
4818
+ "environment \u2014 do not ask the user for credentials, and do not",
4819
+ "hardcode them:"
4820
+ );
4821
+ lines.push("");
4822
+ if (connEnv.DATABASE_URL !== void 0) {
4823
+ lines.push(
4824
+ "- `DATABASE_URL` \u2014 the SQL database. Engine-specific variables are",
4825
+ " set too (`PGHOST`/`PGPORT`/`PGUSER`/`PGPASSWORD`/`PGDATABASE` for",
4826
+ " Postgres, `MYSQL_*` for MySQL)."
4827
+ );
4828
+ }
4829
+ if (connEnv.REDIS_URL !== void 0) {
4830
+ lines.push("- `REDIS_URL` \u2014 Redis.");
4831
+ }
4832
+ lines.push("");
4833
+ lines.push(
4834
+ "These are dev-only defaults for the local container, fine to use",
4835
+ "directly. Prefer reading the variable (e.g. `process.env.DATABASE_URL`)",
4836
+ "over copying its value into code."
4837
+ );
4838
+ }
4778
4839
  const hasCustom = input.services.some((s) => !isCuratedService(s.name));
4779
4840
  if (hasCustom) {
4780
4841
  lines.push("");
4781
4842
  lines.push(
4782
4843
  "For custom-image services, Monoceros does not know the service's",
4783
- "configuration (env vars, ports beyond the primary one, required",
4784
- "volumes). Treat such a service as a black box reachable at the",
4785
- "listed address; if you need more, ask the user."
4844
+ "configuration or credentials (env vars, ports beyond the primary one,",
4845
+ "required volumes). Treat such a service as a black box reachable at",
4846
+ "the listed address; if you need to connect, ask the user in the",
4847
+ "current chat. Do not commit credentials into the repo \u2014 they belong",
4848
+ "in the user's `.env` on the host."
4786
4849
  );
4787
4850
  }
4788
4851
  lines.push("");
@@ -4866,6 +4929,54 @@ function generateAgentsMd(input) {
4866
4929
  " DataGrip) to one of the services."
4867
4930
  );
4868
4931
  lines.push("");
4932
+ if (input.ports.length > 0) {
4933
+ lines.push("## Running a long-running server");
4934
+ lines.push("");
4935
+ lines.push(
4936
+ "When you build something that serves on a port (a web app, an API),",
4937
+ "start it as a **detached** background process so it keeps running after",
4938
+ "this session ends. A plain `npm start` (or any foreground start) dies",
4939
+ "the moment the user exits you or closes the terminal \u2014 and then",
4940
+ `\`${input.containerName}.localhost\` returns 502 Bad Gateway.`
4941
+ );
4942
+ lines.push("");
4943
+ lines.push(
4944
+ "Launch it in a new session with `setsid`, using the project's own",
4945
+ "start command, and record the process-group PID + log under the",
4946
+ "container's logs directory:"
4947
+ );
4948
+ lines.push("");
4949
+ lines.push("```");
4950
+ lines.push(
4951
+ `setsid sh -c 'echo $$ >/workspaces/${input.containerName}/logs/<app>.pid; \\`
4952
+ );
4953
+ lines.push(
4954
+ ` exec <the project's start command> >/workspaces/${input.containerName}/logs/<app>.log 2>&1' </dev/null &`
4955
+ );
4956
+ lines.push("```");
4957
+ lines.push("");
4958
+ lines.push(
4959
+ "Use whatever start command the project actually uses (`npm start`,",
4960
+ "`./mvnw spring-boot:run`, `python app.py`, `go run .`, \u2026) \u2014 do not force",
4961
+ "a language-specific one. `<app>` is a short name you choose."
4962
+ );
4963
+ lines.push("");
4964
+ lines.push("To stop it, kill the whole process group (also stops children");
4965
+ lines.push("like node under npm or java under maven):");
4966
+ lines.push("");
4967
+ lines.push("```");
4968
+ lines.push(
4969
+ `kill -TERM -$(cat /workspaces/${input.containerName}/logs/<app>.pid)`
4970
+ );
4971
+ lines.push("```");
4972
+ lines.push("");
4973
+ lines.push(
4974
+ `The user can follow the output with \`monoceros logs ${input.containerName} <app>\``,
4975
+ "on the host. The server must listen on `0.0.0.0` (not `127.0.0.1`) on",
4976
+ "the exposed port, or Traefik cannot reach it."
4977
+ );
4978
+ lines.push("");
4979
+ }
4869
4980
  lines.push("## Command reference");
4870
4981
  lines.push("");
4871
4982
  lines.push(
@@ -5373,10 +5484,12 @@ var init_cli = __esm({
5373
5484
  cachedBinaryPath = null;
5374
5485
  spawnDevcontainer = (args, cwd, options = {}) => {
5375
5486
  const binPath = devcontainerCliPath();
5487
+ const env = { ...process.env, DOCKER_CLI_HINTS: "false" };
5376
5488
  return new Promise((resolve, reject) => {
5377
5489
  if (options.interactive) {
5378
5490
  const child2 = spawn4(process.execPath, [binPath, ...args], {
5379
5491
  cwd,
5492
+ env,
5380
5493
  stdio: "inherit"
5381
5494
  });
5382
5495
  child2.on("error", reject);
@@ -5385,6 +5498,7 @@ var init_cli = __esm({
5385
5498
  }
5386
5499
  const child = spawn4(process.execPath, [binPath, ...args], {
5387
5500
  cwd,
5501
+ env,
5388
5502
  stdio: ["ignore", "pipe", "pipe"]
5389
5503
  });
5390
5504
  if (options.quiet) {
@@ -6293,7 +6407,7 @@ var CLI_VERSION;
6293
6407
  var init_version = __esm({
6294
6408
  "src/version.ts"() {
6295
6409
  "use strict";
6296
- CLI_VERSION = true ? "1.17.1" : "dev";
6410
+ CLI_VERSION = true ? "1.19.0" : "dev";
6297
6411
  }
6298
6412
  });
6299
6413
 
@@ -6482,6 +6596,43 @@ var init_completion = __esm({
6482
6596
  }
6483
6597
  });
6484
6598
 
6599
+ // src/login/services.ts
6600
+ function featureLeaf(ref) {
6601
+ const noTag = ref.replace(/:[^/:]*$/, "");
6602
+ return noTag.slice(noTag.lastIndexOf("/") + 1);
6603
+ }
6604
+ function loginCapableServices(refs) {
6605
+ const out = [];
6606
+ for (const ref of refs) {
6607
+ const service = LOGIN_SERVICE_BY_LEAF[featureLeaf(ref)];
6608
+ if (service && !out.includes(service)) out.push(service);
6609
+ }
6610
+ return out;
6611
+ }
6612
+ function parseCallbackTarget(authUrl) {
6613
+ try {
6614
+ const u = new URL(authUrl);
6615
+ const redirect = u.searchParams.get("redirect_uri");
6616
+ if (!redirect) return null;
6617
+ const r = new URL(redirect);
6618
+ if (r.hostname !== "localhost" && r.hostname !== "127.0.0.1") return null;
6619
+ const port = Number(r.port);
6620
+ if (!Number.isInteger(port) || port <= 0) return null;
6621
+ return { port, pathname: r.pathname };
6622
+ } catch {
6623
+ return null;
6624
+ }
6625
+ }
6626
+ var LOGIN_SERVICE_BY_LEAF;
6627
+ var init_services = __esm({
6628
+ "src/login/services.ts"() {
6629
+ "use strict";
6630
+ LOGIN_SERVICE_BY_LEAF = {
6631
+ "claude-code": "claude"
6632
+ };
6633
+ }
6634
+ });
6635
+
6485
6636
  // src/completion/resolve.ts
6486
6637
  import { existsSync as existsSync8, promises as fs13 } from "fs";
6487
6638
  import path16 from "path";
@@ -6638,6 +6789,20 @@ async function listContainerNames(ctx) {
6638
6789
  const entries = await fs13.readdir(dir);
6639
6790
  return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length)).sort();
6640
6791
  }
6792
+ async function listLoginServices(ctx) {
6793
+ const after = ctx.prev.slice(2);
6794
+ const name = after.find((t) => !t.startsWith("-"));
6795
+ if (!name) return [];
6796
+ const home = ctx.opts.monocerosHome ?? monocerosHome();
6797
+ const cfgPath = path16.join(home, "container-configs", `${name}.yml`);
6798
+ if (!existsSync8(cfgPath)) return [];
6799
+ try {
6800
+ const { config } = await readConfig(cfgPath);
6801
+ return loginCapableServices(config.features.map((f) => f.ref));
6802
+ } catch {
6803
+ return [];
6804
+ }
6805
+ }
6641
6806
  async function listFeatureComponents() {
6642
6807
  const catalog = await loadComponentCatalog();
6643
6808
  return [...catalog.values()].filter((c) => c.file.category === "feature").map((c) => c.name).sort();
@@ -6705,15 +6870,18 @@ var init_resolve = __esm({
6705
6870
  "src/completion/resolve.ts"() {
6706
6871
  "use strict";
6707
6872
  init_paths();
6873
+ init_io();
6708
6874
  init_components();
6709
6875
  init_manifest();
6710
6876
  init_catalog();
6877
+ init_services();
6711
6878
  init_schema();
6712
6879
  ALL_COMMANDS = [
6713
6880
  "init",
6714
6881
  "list-components",
6715
6882
  "shell",
6716
6883
  "run",
6884
+ "login",
6717
6885
  "logs",
6718
6886
  "start",
6719
6887
  "stop",
@@ -6782,6 +6950,7 @@ var init_resolve = __esm({
6782
6950
  positionals: [containerName],
6783
6951
  flags: { "--in": { type: "value" } }
6784
6952
  },
6953
+ login: { positionals: [containerName, (ctx) => listLoginServices(ctx)] },
6785
6954
  logs: { positionals: [containerName] },
6786
6955
  start: { positionals: [containerName] },
6787
6956
  stop: { positionals: [containerName] },
@@ -7754,8 +7923,277 @@ var init_list_components = __esm({
7754
7923
  }
7755
7924
  });
7756
7925
 
7757
- // src/commands/logs.ts
7926
+ // src/devcontainer/shell.ts
7927
+ import { existsSync as existsSync10 } from "fs";
7928
+ import path18 from "path";
7929
+ async function runShell(opts) {
7930
+ assertContainerExists(opts.root);
7931
+ const spawnFn = opts.spawn ?? spawnDevcontainer;
7932
+ const upCode = await spawnFn(
7933
+ ["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
7934
+ opts.root,
7935
+ { quiet: true }
7936
+ );
7937
+ if (upCode !== 0) return upCode;
7938
+ return spawnFn(
7939
+ [
7940
+ "exec",
7941
+ "--workspace-folder",
7942
+ opts.root,
7943
+ "--mount-workspace-git-root=false",
7944
+ "bash"
7945
+ ],
7946
+ opts.root,
7947
+ { interactive: true }
7948
+ );
7949
+ }
7950
+ function assertContainerExists(root) {
7951
+ if (!existsSync10(path18.join(root, ".devcontainer"))) {
7952
+ throw new Error(
7953
+ `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
7954
+ );
7955
+ }
7956
+ }
7957
+ var init_shell = __esm({
7958
+ "src/devcontainer/shell.ts"() {
7959
+ "use strict";
7960
+ init_cli();
7961
+ }
7962
+ });
7963
+
7964
+ // src/login/index.ts
7965
+ import { spawn as spawn8 } from "child_process";
7966
+ import { existsSync as existsSync11, promises as fsp2, readFileSync as readFileSync6 } from "fs";
7967
+ import http from "http";
7968
+ import path19 from "path";
7969
+ import { consola as consola16 } from "consola";
7970
+ function openInBrowser(url) {
7971
+ const platform = process.platform;
7972
+ const [cmd, args] = platform === "darwin" ? ["open", [url]] : platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
7973
+ try {
7974
+ const child = spawn8(cmd, args, {
7975
+ stdio: "ignore",
7976
+ detached: true
7977
+ });
7978
+ child.on("error", () => {
7979
+ });
7980
+ child.unref();
7981
+ } catch {
7982
+ }
7983
+ }
7984
+ async function runLogin(opts) {
7985
+ const root = containerDir(opts.name);
7986
+ assertContainerExists(root);
7987
+ const { config } = await readConfig(containerConfigPath(opts.name));
7988
+ const capable = loginCapableServices(config.features.map((f) => f.ref));
7989
+ if (capable.length === 0) {
7990
+ consola16.error(
7991
+ `Container "${opts.name}" has no tool with a Monoceros login. (Supported today: claude.)`
7992
+ );
7993
+ return 1;
7994
+ }
7995
+ if (!opts.feature) {
7996
+ consola16.info(
7997
+ `Login-capable tools in "${opts.name}": ${capable.join(", ")}.`
7998
+ );
7999
+ consola16.info(`Log one in with: monoceros login ${opts.name} <tool>`);
8000
+ return 0;
8001
+ }
8002
+ const service = opts.feature;
8003
+ if (!capable.includes(service)) {
8004
+ consola16.error(
8005
+ `"${service}" is not a login-capable tool in "${opts.name}". Available: ${capable.join(", ")}.`
8006
+ );
8007
+ return 1;
8008
+ }
8009
+ if (service !== "claude") {
8010
+ consola16.error(
8011
+ `Login for "${service}" is not implemented yet (only claude).`
8012
+ );
8013
+ return 1;
8014
+ }
8015
+ return runClaudeLogin(opts.name, root, opts.spawn ?? spawnDevcontainer);
8016
+ }
8017
+ async function runClaudeLogin(name, root, spawnFn) {
8018
+ const credFile = path19.join(root, "home", ".claude", ".credentials.json");
8019
+ if (existsSync11(credFile)) {
8020
+ consola16.success(
8021
+ `Claude is already logged in for "${name}". Re-apply or remove the credential to log in again.`
8022
+ );
8023
+ return 0;
8024
+ }
8025
+ const upCode = await spawnFn(
8026
+ ["up", "--workspace-folder", root, "--mount-workspace-git-root=false"],
8027
+ root,
8028
+ { quiet: true }
8029
+ );
8030
+ if (upCode !== 0) return upCode;
8031
+ const relayDir = path19.join(root, RELAY_DIRNAME);
8032
+ const relayScript = path19.join(relayDir, "xdg-open");
8033
+ const urlFile = path19.join(relayDir, "url");
8034
+ await fsp2.mkdir(relayDir, { recursive: true });
8035
+ await fsp2.rm(urlFile, { force: true });
8036
+ await fsp2.writeFile(
8037
+ relayScript,
8038
+ `#!/bin/sh
8039
+ printf '%s\\n' "$1" > "$(dirname "$0")/url"
8040
+ exit 0
8041
+ `,
8042
+ { mode: 493 }
8043
+ );
8044
+ await fsp2.chmod(relayScript, 493);
8045
+ const servers = [];
8046
+ let handledUrl = false;
8047
+ const onAuthUrl = (authUrl) => {
8048
+ consola16.info("Opening the Claude sign-in page in your browser\u2026");
8049
+ openInBrowser(authUrl);
8050
+ const target = parseCallbackTarget(authUrl);
8051
+ if (!target) {
8052
+ return;
8053
+ }
8054
+ const server = http.createServer((req, res) => {
8055
+ const reqUrl = req.url ?? target.pathname;
8056
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
8057
+ res.end(
8058
+ '<html><body style="font-family:sans-serif;padding:3rem">You are signed in. You can close this tab and return to the terminal.</body></html>'
8059
+ );
8060
+ void spawnFn(
8061
+ [
8062
+ "exec",
8063
+ "--workspace-folder",
8064
+ root,
8065
+ "--mount-workspace-git-root=false",
8066
+ "curl",
8067
+ "-fsS",
8068
+ `http://localhost:${target.port}${reqUrl}`
8069
+ ],
8070
+ root,
8071
+ { quiet: true }
8072
+ );
8073
+ });
8074
+ server.on("error", (err) => {
8075
+ consola16.warn(
8076
+ `Could not start the local callback helper on port ${target.port} (${err.message}). If the browser shows a code, paste it into the terminal instead.`
8077
+ );
8078
+ });
8079
+ server.listen(target.port, "127.0.0.1");
8080
+ servers.push(server);
8081
+ };
8082
+ const poll = setInterval(() => {
8083
+ if (handledUrl || !existsSync11(urlFile)) return;
8084
+ let content = "";
8085
+ try {
8086
+ content = readFileSync6(urlFile, "utf8");
8087
+ } catch {
8088
+ return;
8089
+ }
8090
+ if (!content.trim()) return;
8091
+ handledUrl = true;
8092
+ onAuthUrl(content.trim());
8093
+ }, 250);
8094
+ consola16.info(
8095
+ "Starting Claude login. Pick your account in Claude's menu \u2014 the browser opens for you, no copying."
8096
+ );
8097
+ const child = spawn8(
8098
+ process.execPath,
8099
+ [
8100
+ devcontainerCliPath(),
8101
+ "exec",
8102
+ "--workspace-folder",
8103
+ root,
8104
+ "--mount-workspace-git-root=false",
8105
+ "bash",
8106
+ "-lc",
8107
+ `export PATH="/workspaces/${name}/${RELAY_DIRNAME}:$PATH"; exec claude`
8108
+ ],
8109
+ {
8110
+ cwd: root,
8111
+ env: { ...process.env, DOCKER_CLI_HINTS: "false" },
8112
+ stdio: "inherit"
8113
+ }
8114
+ );
8115
+ const code = await new Promise((resolve) => {
8116
+ child.on("error", () => resolve(1));
8117
+ child.on("exit", (c) => resolve(c ?? 0));
8118
+ });
8119
+ clearInterval(poll);
8120
+ for (const s of servers) s.close();
8121
+ await fsp2.rm(relayDir, { recursive: true, force: true });
8122
+ if (existsSync11(credFile)) {
8123
+ consola16.success(
8124
+ `Claude is logged in for "${name}". The credential persists across rebuilds.`
8125
+ );
8126
+ }
8127
+ return code;
8128
+ }
8129
+ var RELAY_DIRNAME;
8130
+ var init_login = __esm({
8131
+ "src/login/index.ts"() {
8132
+ "use strict";
8133
+ init_io();
8134
+ init_paths();
8135
+ init_cli();
8136
+ init_shell();
8137
+ init_services();
8138
+ RELAY_DIRNAME = ".monoceros-login";
8139
+ }
8140
+ });
8141
+
8142
+ // src/commands/login.ts
7758
8143
  import { defineCommand as defineCommand13 } from "citty";
8144
+ import { consola as consola17 } from "consola";
8145
+ var loginCommand;
8146
+ var init_login2 = __esm({
8147
+ "src/commands/login.ts"() {
8148
+ "use strict";
8149
+ init_login();
8150
+ loginCommand = defineCommand13({
8151
+ meta: {
8152
+ name: "login",
8153
+ group: "lifecycle",
8154
+ description: "Log a curated tool in inside the container. Opens the sign-in page in your browser for you \u2014 no copying URLs. Today: Claude."
8155
+ },
8156
+ args: {
8157
+ name: {
8158
+ type: "positional",
8159
+ description: "Container name.",
8160
+ required: true
8161
+ },
8162
+ feature: {
8163
+ type: "positional",
8164
+ description: "Which tool to log in (e.g. `claude`). Optional when the container has only one login-capable tool.",
8165
+ required: false
8166
+ }
8167
+ },
8168
+ async run({ args }) {
8169
+ try {
8170
+ const code = await runLogin({
8171
+ name: args.name,
8172
+ ...args.feature ? { feature: args.feature } : {}
8173
+ });
8174
+ process.exit(code);
8175
+ } catch (err) {
8176
+ consola17.error(err instanceof Error ? err.message : String(err));
8177
+ process.exit(1);
8178
+ }
8179
+ }
8180
+ });
8181
+ }
8182
+ });
8183
+
8184
+ // src/commands/logs.ts
8185
+ import { spawn as spawn9 } from "child_process";
8186
+ import { existsSync as existsSync12 } from "fs";
8187
+ import path20 from "path";
8188
+ import { defineCommand as defineCommand14 } from "citty";
8189
+ function tailLogFile(file, follow) {
8190
+ const [cmd, args] = follow ? ["tail", ["-F", file]] : ["cat", [file]];
8191
+ return new Promise((resolve, reject) => {
8192
+ const child = spawn9(cmd, args, { stdio: "inherit" });
8193
+ child.on("error", reject);
8194
+ child.on("exit", (code) => resolve(code ?? 0));
8195
+ });
8196
+ }
7759
8197
  var logsCommand;
7760
8198
  var init_logs = __esm({
7761
8199
  "src/commands/logs.ts"() {
@@ -7763,11 +8201,11 @@ var init_logs = __esm({
7763
8201
  init_paths();
7764
8202
  init_compose();
7765
8203
  init_dispatch();
7766
- logsCommand = defineCommand13({
8204
+ logsCommand = defineCommand14({
7767
8205
  meta: {
7768
8206
  name: "logs",
7769
8207
  group: "run",
7770
- description: "Tail logs from the compose services of the named dev-container. Pass --no-follow for a one-shot dump."
8208
+ description: "Tail logs from a compose service of the named dev-container, or from a long-running app started inside it (logs/<app>.log). Pass --no-follow for a one-shot dump."
7771
8209
  },
7772
8210
  args: {
7773
8211
  name: {
@@ -7777,7 +8215,7 @@ var init_logs = __esm({
7777
8215
  },
7778
8216
  service: {
7779
8217
  type: "string",
7780
- description: "Restrict to a single compose service (e.g. postgres). Defaults to all."
8218
+ description: "A compose service (e.g. postgres) or an app whose log is at logs/<service>.log. Defaults to all compose services."
7781
8219
  },
7782
8220
  follow: {
7783
8221
  type: "boolean",
@@ -7787,10 +8225,17 @@ var init_logs = __esm({
7787
8225
  }
7788
8226
  },
7789
8227
  run({ args }) {
8228
+ const service = typeof args.service === "string" ? args.service : void 0;
8229
+ if (service) {
8230
+ const logFile = path20.join(containerLogsDir(args.name), `${service}.log`);
8231
+ if (existsSync12(logFile)) {
8232
+ return dispatch(() => tailLogFile(logFile, args.follow));
8233
+ }
8234
+ }
7790
8235
  return dispatch(
7791
8236
  () => runLogs({
7792
8237
  root: containerDir(args.name),
7793
- ...typeof args.service === "string" ? { service: args.service } : {},
8238
+ ...service ? { service } : {},
7794
8239
  follow: args.follow
7795
8240
  })
7796
8241
  );
@@ -7800,11 +8245,11 @@ var init_logs = __esm({
7800
8245
  });
7801
8246
 
7802
8247
  // src/commands/port.ts
7803
- import { defineCommand as defineCommand14 } from "citty";
7804
- import { consola as consola16 } from "consola";
8248
+ import { defineCommand as defineCommand15 } from "citty";
8249
+ import { consola as consola18 } from "consola";
7805
8250
  async function runPortListing(opts) {
7806
8251
  const out = opts.out ?? process.stdout;
7807
- const info = opts.info ?? ((m) => consola16.info(m));
8252
+ const info = opts.info ?? ((m) => consola18.info(m));
7808
8253
  const parsed = await readConfig(
7809
8254
  containerConfigPath(opts.name, opts.monocerosHome)
7810
8255
  );
@@ -7862,7 +8307,7 @@ var init_port = __esm({
7862
8307
  init_schema();
7863
8308
  init_dynamic();
7864
8309
  init_format();
7865
- portCommand = defineCommand14({
8310
+ portCommand = defineCommand15({
7866
8311
  meta: {
7867
8312
  name: "port",
7868
8313
  group: "discovery",
@@ -7880,7 +8325,7 @@ var init_port = __esm({
7880
8325
  const code = await runPortListing({ name: args.name });
7881
8326
  process.exit(code);
7882
8327
  } catch (err) {
7883
- consola16.error(err instanceof Error ? err.message : String(err));
8328
+ consola18.error(err instanceof Error ? err.message : String(err));
7884
8329
  process.exit(1);
7885
8330
  }
7886
8331
  }
@@ -7889,15 +8334,15 @@ var init_port = __esm({
7889
8334
  });
7890
8335
 
7891
8336
  // src/commands/remove-apt-packages.ts
7892
- import { defineCommand as defineCommand15 } from "citty";
7893
- import { consola as consola17 } from "consola";
8337
+ import { defineCommand as defineCommand16 } from "citty";
8338
+ import { consola as consola19 } from "consola";
7894
8339
  var removeAptPackagesCommand;
7895
8340
  var init_remove_apt_packages = __esm({
7896
8341
  "src/commands/remove-apt-packages.ts"() {
7897
8342
  "use strict";
7898
8343
  init_inner_args();
7899
8344
  init_modify();
7900
- removeAptPackagesCommand = defineCommand15({
8345
+ removeAptPackagesCommand = defineCommand16({
7901
8346
  meta: {
7902
8347
  name: "remove-apt-packages",
7903
8348
  group: "edit",
@@ -7919,7 +8364,7 @@ var init_remove_apt_packages = __esm({
7919
8364
  async run({ args }) {
7920
8365
  const packages = [...getInnerArgs()];
7921
8366
  if (packages.length === 0) {
7922
- consola17.error(
8367
+ consola19.error(
7923
8368
  "No package names given. Usage: `monoceros remove-apt-packages <containername> [--yes] -- <pkg> [<pkg> \u2026]`."
7924
8369
  );
7925
8370
  process.exit(1);
@@ -7932,7 +8377,7 @@ var init_remove_apt_packages = __esm({
7932
8377
  });
7933
8378
  process.exit(result.status === "aborted" ? 1 : 0);
7934
8379
  } catch (err) {
7935
- consola17.error(err instanceof Error ? err.message : String(err));
8380
+ consola19.error(err instanceof Error ? err.message : String(err));
7936
8381
  process.exit(1);
7937
8382
  }
7938
8383
  }
@@ -7941,14 +8386,14 @@ var init_remove_apt_packages = __esm({
7941
8386
  });
7942
8387
 
7943
8388
  // src/commands/remove-feature.ts
7944
- import { defineCommand as defineCommand16 } from "citty";
7945
- import { consola as consola18 } from "consola";
8389
+ import { defineCommand as defineCommand17 } from "citty";
8390
+ import { consola as consola20 } from "consola";
7946
8391
  var removeFeatureCommand;
7947
8392
  var init_remove_feature = __esm({
7948
8393
  "src/commands/remove-feature.ts"() {
7949
8394
  "use strict";
7950
8395
  init_modify();
7951
- removeFeatureCommand = defineCommand16({
8396
+ removeFeatureCommand = defineCommand17({
7952
8397
  meta: {
7953
8398
  name: "remove-feature",
7954
8399
  group: "edit",
@@ -7981,7 +8426,7 @@ var init_remove_feature = __esm({
7981
8426
  });
7982
8427
  process.exit(result.status === "aborted" ? 1 : 0);
7983
8428
  } catch (err) {
7984
- consola18.error(err instanceof Error ? err.message : String(err));
8429
+ consola20.error(err instanceof Error ? err.message : String(err));
7985
8430
  process.exit(1);
7986
8431
  }
7987
8432
  }
@@ -7990,15 +8435,15 @@ var init_remove_feature = __esm({
7990
8435
  });
7991
8436
 
7992
8437
  // src/remove/index.ts
7993
- import { existsSync as existsSync10, promises as fs15 } from "fs";
7994
- import path18 from "path";
7995
- import { consola as consola19 } from "consola";
8438
+ import { existsSync as existsSync13, promises as fs15 } from "fs";
8439
+ import path21 from "path";
8440
+ import { consola as consola21 } from "consola";
7996
8441
  async function runRemove(opts) {
7997
8442
  const home = opts.monocerosHome ?? monocerosHome();
7998
8443
  const logger = opts.logger ?? {
7999
- info: (msg) => consola19.info(msg),
8000
- success: (msg) => consola19.success(msg),
8001
- warn: (msg) => consola19.warn(msg)
8444
+ info: (msg) => consola21.info(msg),
8445
+ success: (msg) => consola21.success(msg),
8446
+ warn: (msg) => consola21.warn(msg)
8002
8447
  };
8003
8448
  if (!REGEX.solutionName.test(opts.name)) {
8004
8449
  throw new Error(
@@ -8008,9 +8453,9 @@ async function runRemove(opts) {
8008
8453
  const ymlPath = containerConfigPath(opts.name, home);
8009
8454
  const envPath = containerEnvPath(opts.name, home);
8010
8455
  const containerPath = containerDir(opts.name, home);
8011
- const hasYml = existsSync10(ymlPath);
8012
- const hasEnv = existsSync10(envPath);
8013
- const hasContainer = existsSync10(containerPath);
8456
+ const hasYml = existsSync13(ymlPath);
8457
+ const hasEnv = existsSync13(envPath);
8458
+ const hasContainer = existsSync13(containerPath);
8014
8459
  if (!hasYml && !hasContainer) {
8015
8460
  throw new Error(
8016
8461
  `Nothing to remove for '${opts.name}': neither ${ymlPath} nor ${containerPath} exists.`
@@ -8036,16 +8481,16 @@ async function runRemove(opts) {
8036
8481
  let backupPath = null;
8037
8482
  if (!opts.noBackup && (hasYml || hasContainer)) {
8038
8483
  const ts = (opts.now ?? /* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
8039
- backupPath = path18.join(home, "container-backups", `${opts.name}-${ts}`);
8484
+ backupPath = path21.join(home, "container-backups", `${opts.name}-${ts}`);
8040
8485
  await fs15.mkdir(backupPath, { recursive: true });
8041
8486
  if (hasYml) {
8042
- await fs15.copyFile(ymlPath, path18.join(backupPath, `${opts.name}.yml`));
8487
+ await fs15.copyFile(ymlPath, path21.join(backupPath, `${opts.name}.yml`));
8043
8488
  }
8044
8489
  if (hasEnv) {
8045
- await fs15.copyFile(envPath, path18.join(backupPath, `${opts.name}.env`));
8490
+ await fs15.copyFile(envPath, path21.join(backupPath, `${opts.name}.env`));
8046
8491
  }
8047
8492
  if (hasContainer) {
8048
- await fs15.cp(containerPath, path18.join(backupPath, "container"), {
8493
+ await fs15.cp(containerPath, path21.join(backupPath, "container"), {
8049
8494
  recursive: true
8050
8495
  });
8051
8496
  }
@@ -8134,15 +8579,15 @@ var init_remove = __esm({
8134
8579
  });
8135
8580
 
8136
8581
  // src/commands/remove.ts
8137
- import { defineCommand as defineCommand17 } from "citty";
8138
- import { consola as consola20 } from "consola";
8582
+ import { defineCommand as defineCommand18 } from "citty";
8583
+ import { consola as consola22 } from "consola";
8139
8584
  import { createInterface } from "readline/promises";
8140
8585
  var removeCommand;
8141
8586
  var init_remove2 = __esm({
8142
8587
  "src/commands/remove.ts"() {
8143
8588
  "use strict";
8144
8589
  init_remove();
8145
- removeCommand = defineCommand17({
8590
+ removeCommand = defineCommand18({
8146
8591
  meta: {
8147
8592
  name: "remove",
8148
8593
  group: "lifecycle",
@@ -8179,7 +8624,7 @@ var init_remove2 = __esm({
8179
8624
  const skipPrompt = args.yes === true;
8180
8625
  if (!skipPrompt) {
8181
8626
  const warning = noBackup ? `About to remove '${args.name}' WITHOUT a backup. Docker objects, container-configs entry, and container directory will all be deleted.` : `About to remove '${args.name}'. A backup will be written to container-backups/ first, then docker objects, container-configs entry, and container directory will all be deleted.`;
8182
- consola20.warn(warning);
8627
+ consola22.warn(warning);
8183
8628
  const rl = createInterface({
8184
8629
  input: process.stdin,
8185
8630
  output: process.stdout
@@ -8187,7 +8632,7 @@ var init_remove2 = __esm({
8187
8632
  const answer = await rl.question("Continue? [y/N] ");
8188
8633
  rl.close();
8189
8634
  if (!/^y(es)?$/i.test(answer.trim())) {
8190
- consola20.info("Aborted. Nothing changed.");
8635
+ consola22.info("Aborted. Nothing changed.");
8191
8636
  process.exit(0);
8192
8637
  }
8193
8638
  }
@@ -8196,7 +8641,7 @@ var init_remove2 = __esm({
8196
8641
  ...noBackup ? { noBackup: true } : {}
8197
8642
  });
8198
8643
  } catch (err) {
8199
- consola20.error(err instanceof Error ? err.message : String(err));
8644
+ consola22.error(err instanceof Error ? err.message : String(err));
8200
8645
  process.exit(1);
8201
8646
  }
8202
8647
  }
@@ -8205,17 +8650,17 @@ var init_remove2 = __esm({
8205
8650
  });
8206
8651
 
8207
8652
  // src/restore/index.ts
8208
- import { existsSync as existsSync11, promises as fs16 } from "fs";
8209
- import path19 from "path";
8210
- import { consola as consola21 } from "consola";
8653
+ import { existsSync as existsSync14, promises as fs16 } from "fs";
8654
+ import path22 from "path";
8655
+ import { consola as consola23 } from "consola";
8211
8656
  async function runRestore(opts) {
8212
8657
  const home = opts.monocerosHome ?? monocerosHome();
8213
8658
  const logger = opts.logger ?? {
8214
- info: (msg) => consola21.info(msg),
8215
- success: (msg) => consola21.success(msg)
8659
+ info: (msg) => consola23.info(msg),
8660
+ success: (msg) => consola23.success(msg)
8216
8661
  };
8217
- const backup = path19.resolve(opts.backupPath);
8218
- if (!existsSync11(backup)) {
8662
+ const backup = path22.resolve(opts.backupPath);
8663
+ if (!existsSync14(backup)) {
8219
8664
  throw new Error(`Backup not found: ${backup}.`);
8220
8665
  }
8221
8666
  const stat = await fs16.stat(backup);
@@ -8236,24 +8681,24 @@ async function runRestore(opts) {
8236
8681
  }
8237
8682
  const ymlFile = ymlFiles[0];
8238
8683
  const name = ymlFile.replace(/\.yml$/, "");
8239
- const containerInBackup = path19.join(backup, "container");
8240
- const hasContainer = existsSync11(containerInBackup);
8241
- const envInBackup = path19.join(backup, `${name}.env`);
8242
- const hasEnv = existsSync11(envInBackup);
8684
+ const containerInBackup = path22.join(backup, "container");
8685
+ const hasContainer = existsSync14(containerInBackup);
8686
+ const envInBackup = path22.join(backup, `${name}.env`);
8687
+ const hasEnv = existsSync14(envInBackup);
8243
8688
  const destYml = containerConfigPath(name, home);
8244
8689
  const destContainer = containerDir(name, home);
8245
- if (existsSync11(destYml)) {
8690
+ if (existsSync14(destYml)) {
8246
8691
  throw new Error(
8247
8692
  `Refusing to restore: ${destYml} already exists. Remove the current container first (\`monoceros remove ${name}\`) or rename the existing config.`
8248
8693
  );
8249
8694
  }
8250
- if (hasContainer && existsSync11(destContainer)) {
8695
+ if (hasContainer && existsSync14(destContainer)) {
8251
8696
  throw new Error(
8252
8697
  `Refusing to restore: ${destContainer} already exists. Remove the current container first (\`monoceros remove ${name}\`).`
8253
8698
  );
8254
8699
  }
8255
8700
  await fs16.mkdir(containerConfigsDir(home), { recursive: true });
8256
- await fs16.copyFile(path19.join(backup, ymlFile), destYml);
8701
+ await fs16.copyFile(path22.join(backup, ymlFile), destYml);
8257
8702
  if (hasEnv) {
8258
8703
  await fs16.copyFile(envInBackup, containerEnvPath(name, home));
8259
8704
  }
@@ -8278,14 +8723,14 @@ var init_restore = __esm({
8278
8723
  });
8279
8724
 
8280
8725
  // src/commands/restore.ts
8281
- import { defineCommand as defineCommand18 } from "citty";
8282
- import { consola as consola22 } from "consola";
8726
+ import { defineCommand as defineCommand19 } from "citty";
8727
+ import { consola as consola24 } from "consola";
8283
8728
  var restoreCommand;
8284
8729
  var init_restore2 = __esm({
8285
8730
  "src/commands/restore.ts"() {
8286
8731
  "use strict";
8287
8732
  init_restore();
8288
- restoreCommand = defineCommand18({
8733
+ restoreCommand = defineCommand19({
8289
8734
  meta: {
8290
8735
  name: "restore",
8291
8736
  group: "lifecycle",
@@ -8302,7 +8747,7 @@ var init_restore2 = __esm({
8302
8747
  try {
8303
8748
  await runRestore({ backupPath: args["backup-path"] });
8304
8749
  } catch (err) {
8305
- consola22.error(err instanceof Error ? err.message : String(err));
8750
+ consola24.error(err instanceof Error ? err.message : String(err));
8306
8751
  process.exit(1);
8307
8752
  }
8308
8753
  }
@@ -8311,14 +8756,14 @@ var init_restore2 = __esm({
8311
8756
  });
8312
8757
 
8313
8758
  // src/commands/remove-from-url.ts
8314
- import { defineCommand as defineCommand19 } from "citty";
8315
- import { consola as consola23 } from "consola";
8759
+ import { defineCommand as defineCommand20 } from "citty";
8760
+ import { consola as consola25 } from "consola";
8316
8761
  var removeFromUrlCommand;
8317
8762
  var init_remove_from_url = __esm({
8318
8763
  "src/commands/remove-from-url.ts"() {
8319
8764
  "use strict";
8320
8765
  init_modify();
8321
- removeFromUrlCommand = defineCommand19({
8766
+ removeFromUrlCommand = defineCommand20({
8322
8767
  meta: {
8323
8768
  name: "remove-from-url",
8324
8769
  group: "edit",
@@ -8351,7 +8796,7 @@ var init_remove_from_url = __esm({
8351
8796
  });
8352
8797
  process.exit(result.status === "aborted" ? 1 : 0);
8353
8798
  } catch (err) {
8354
- consola23.error(err instanceof Error ? err.message : String(err));
8799
+ consola25.error(err instanceof Error ? err.message : String(err));
8355
8800
  process.exit(1);
8356
8801
  }
8357
8802
  }
@@ -8360,14 +8805,14 @@ var init_remove_from_url = __esm({
8360
8805
  });
8361
8806
 
8362
8807
  // src/commands/remove-language.ts
8363
- import { defineCommand as defineCommand20 } from "citty";
8364
- import { consola as consola24 } from "consola";
8808
+ import { defineCommand as defineCommand21 } from "citty";
8809
+ import { consola as consola26 } from "consola";
8365
8810
  var removeLanguageCommand;
8366
8811
  var init_remove_language = __esm({
8367
8812
  "src/commands/remove-language.ts"() {
8368
8813
  "use strict";
8369
8814
  init_modify();
8370
- removeLanguageCommand = defineCommand20({
8815
+ removeLanguageCommand = defineCommand21({
8371
8816
  meta: {
8372
8817
  name: "remove-language",
8373
8818
  group: "edit",
@@ -8400,7 +8845,7 @@ var init_remove_language = __esm({
8400
8845
  });
8401
8846
  process.exit(result.status === "aborted" ? 1 : 0);
8402
8847
  } catch (err) {
8403
- consola24.error(err instanceof Error ? err.message : String(err));
8848
+ consola26.error(err instanceof Error ? err.message : String(err));
8404
8849
  process.exit(1);
8405
8850
  }
8406
8851
  }
@@ -8409,8 +8854,8 @@ var init_remove_language = __esm({
8409
8854
  });
8410
8855
 
8411
8856
  // src/commands/remove-port.ts
8412
- import { defineCommand as defineCommand21 } from "citty";
8413
- import { consola as consola25 } from "consola";
8857
+ import { defineCommand as defineCommand22 } from "citty";
8858
+ import { consola as consola27 } from "consola";
8414
8859
  function coerceToken2(raw) {
8415
8860
  const n = Number(raw);
8416
8861
  return Number.isFinite(n) ? n : raw;
@@ -8421,7 +8866,7 @@ var init_remove_port = __esm({
8421
8866
  "use strict";
8422
8867
  init_inner_args();
8423
8868
  init_modify();
8424
- removePortCommand = defineCommand21({
8869
+ removePortCommand = defineCommand22({
8425
8870
  meta: {
8426
8871
  name: "remove-port",
8427
8872
  group: "edit",
@@ -8443,7 +8888,7 @@ var init_remove_port = __esm({
8443
8888
  async run({ args }) {
8444
8889
  const tokens = [...getInnerArgs()];
8445
8890
  if (tokens.length === 0) {
8446
- consola25.error(
8891
+ consola27.error(
8447
8892
  "No ports given. Usage: `monoceros remove-port <containername> [--yes] -- <port> [<port> \u2026]`."
8448
8893
  );
8449
8894
  process.exit(1);
@@ -8456,7 +8901,7 @@ var init_remove_port = __esm({
8456
8901
  });
8457
8902
  process.exit(result.status === "aborted" ? 1 : 0);
8458
8903
  } catch (err) {
8459
- consola25.error(err instanceof Error ? err.message : String(err));
8904
+ consola27.error(err instanceof Error ? err.message : String(err));
8460
8905
  process.exit(1);
8461
8906
  }
8462
8907
  }
@@ -8465,14 +8910,14 @@ var init_remove_port = __esm({
8465
8910
  });
8466
8911
 
8467
8912
  // src/commands/remove-repo.ts
8468
- import { defineCommand as defineCommand22 } from "citty";
8469
- import { consola as consola26 } from "consola";
8913
+ import { defineCommand as defineCommand23 } from "citty";
8914
+ import { consola as consola28 } from "consola";
8470
8915
  var removeRepoCommand;
8471
8916
  var init_remove_repo = __esm({
8472
8917
  "src/commands/remove-repo.ts"() {
8473
8918
  "use strict";
8474
8919
  init_modify();
8475
- removeRepoCommand = defineCommand22({
8920
+ removeRepoCommand = defineCommand23({
8476
8921
  meta: {
8477
8922
  name: "remove-repo",
8478
8923
  group: "edit",
@@ -8505,7 +8950,7 @@ var init_remove_repo = __esm({
8505
8950
  });
8506
8951
  process.exit(result.status === "aborted" ? 1 : 0);
8507
8952
  } catch (err) {
8508
- consola26.error(err instanceof Error ? err.message : String(err));
8953
+ consola28.error(err instanceof Error ? err.message : String(err));
8509
8954
  process.exit(1);
8510
8955
  }
8511
8956
  }
@@ -8514,14 +8959,14 @@ var init_remove_repo = __esm({
8514
8959
  });
8515
8960
 
8516
8961
  // src/commands/remove-service.ts
8517
- import { defineCommand as defineCommand23 } from "citty";
8518
- import { consola as consola27 } from "consola";
8962
+ import { defineCommand as defineCommand24 } from "citty";
8963
+ import { consola as consola29 } from "consola";
8519
8964
  var removeServiceCommand;
8520
8965
  var init_remove_service = __esm({
8521
8966
  "src/commands/remove-service.ts"() {
8522
8967
  "use strict";
8523
8968
  init_modify();
8524
- removeServiceCommand = defineCommand23({
8969
+ removeServiceCommand = defineCommand24({
8525
8970
  meta: {
8526
8971
  name: "remove-service",
8527
8972
  group: "edit",
@@ -8554,7 +8999,7 @@ var init_remove_service = __esm({
8554
8999
  });
8555
9000
  process.exit(result.status === "aborted" ? 1 : 0);
8556
9001
  } catch (err) {
8557
- consola27.error(err instanceof Error ? err.message : String(err));
9002
+ consola29.error(err instanceof Error ? err.message : String(err));
8558
9003
  process.exit(1);
8559
9004
  }
8560
9005
  }
@@ -8562,44 +9007,6 @@ var init_remove_service = __esm({
8562
9007
  }
8563
9008
  });
8564
9009
 
8565
- // src/devcontainer/shell.ts
8566
- import { existsSync as existsSync12 } from "fs";
8567
- import path20 from "path";
8568
- async function runShell(opts) {
8569
- assertContainerExists(opts.root);
8570
- const spawnFn = opts.spawn ?? spawnDevcontainer;
8571
- const upCode = await spawnFn(
8572
- ["up", "--workspace-folder", opts.root, "--mount-workspace-git-root=false"],
8573
- opts.root,
8574
- { quiet: true }
8575
- );
8576
- if (upCode !== 0) return upCode;
8577
- return spawnFn(
8578
- [
8579
- "exec",
8580
- "--workspace-folder",
8581
- opts.root,
8582
- "--mount-workspace-git-root=false",
8583
- "bash"
8584
- ],
8585
- opts.root,
8586
- { interactive: true }
8587
- );
8588
- }
8589
- function assertContainerExists(root) {
8590
- if (!existsSync12(path20.join(root, ".devcontainer"))) {
8591
- throw new Error(
8592
- `No .devcontainer/ at ${root}. Run \`monoceros apply <name>\` first.`
8593
- );
8594
- }
8595
- }
8596
- var init_shell = __esm({
8597
- "src/devcontainer/shell.ts"() {
8598
- "use strict";
8599
- init_cli();
8600
- }
8601
- });
8602
-
8603
9010
  // src/devcontainer/run.ts
8604
9011
  async function runInContainer(opts) {
8605
9012
  if (opts.command.length === 0) {
@@ -8644,8 +9051,8 @@ var init_run = __esm({
8644
9051
  });
8645
9052
 
8646
9053
  // src/commands/run.ts
8647
- import { defineCommand as defineCommand24 } from "citty";
8648
- import { consola as consola28 } from "consola";
9054
+ import { defineCommand as defineCommand25 } from "citty";
9055
+ import { consola as consola30 } from "consola";
8649
9056
  var runCommand;
8650
9057
  var init_run2 = __esm({
8651
9058
  "src/commands/run.ts"() {
@@ -8653,7 +9060,7 @@ var init_run2 = __esm({
8653
9060
  init_paths();
8654
9061
  init_run();
8655
9062
  init_inner_args();
8656
- runCommand = defineCommand24({
9063
+ runCommand = defineCommand25({
8657
9064
  meta: {
8658
9065
  name: "run",
8659
9066
  group: "run",
@@ -8673,7 +9080,7 @@ var init_run2 = __esm({
8673
9080
  async run({ args }) {
8674
9081
  const command = [...getInnerArgs()];
8675
9082
  if (command.length === 0) {
8676
- consola28.error(
9083
+ consola30.error(
8677
9084
  "No command provided. Usage: `monoceros run <containername> -- <cmd> [args\u2026]`."
8678
9085
  );
8679
9086
  process.exit(1);
@@ -8686,7 +9093,7 @@ var init_run2 = __esm({
8686
9093
  });
8687
9094
  process.exit(exitCode);
8688
9095
  } catch (err) {
8689
- consola28.error(err instanceof Error ? err.message : String(err));
9096
+ consola30.error(err instanceof Error ? err.message : String(err));
8690
9097
  process.exit(1);
8691
9098
  }
8692
9099
  }
@@ -8695,15 +9102,15 @@ var init_run2 = __esm({
8695
9102
  });
8696
9103
 
8697
9104
  // src/commands/shell.ts
8698
- import { defineCommand as defineCommand25 } from "citty";
8699
- import { consola as consola29 } from "consola";
9105
+ import { defineCommand as defineCommand26 } from "citty";
9106
+ import { consola as consola31 } from "consola";
8700
9107
  var shellCommand;
8701
9108
  var init_shell2 = __esm({
8702
9109
  "src/commands/shell.ts"() {
8703
9110
  "use strict";
8704
9111
  init_paths();
8705
9112
  init_shell();
8706
- shellCommand = defineCommand25({
9113
+ shellCommand = defineCommand26({
8707
9114
  meta: {
8708
9115
  name: "shell",
8709
9116
  group: "run",
@@ -8721,7 +9128,7 @@ var init_shell2 = __esm({
8721
9128
  const exitCode = await runShell({ root: containerDir(args.name) });
8722
9129
  process.exit(exitCode);
8723
9130
  } catch (err) {
8724
- consola29.error(err instanceof Error ? err.message : String(err));
9131
+ consola31.error(err instanceof Error ? err.message : String(err));
8725
9132
  process.exit(1);
8726
9133
  }
8727
9134
  }
@@ -8730,8 +9137,8 @@ var init_shell2 = __esm({
8730
9137
  });
8731
9138
 
8732
9139
  // src/commands/start.ts
8733
- import { defineCommand as defineCommand26 } from "citty";
8734
- import { consola as consola30 } from "consola";
9140
+ import { defineCommand as defineCommand27 } from "citty";
9141
+ import { consola as consola32 } from "consola";
8735
9142
  var startCommand;
8736
9143
  var init_start = __esm({
8737
9144
  "src/commands/start.ts"() {
@@ -8743,7 +9150,7 @@ var init_start = __esm({
8743
9150
  init_proxy();
8744
9151
  init_port_check();
8745
9152
  init_dispatch();
8746
- startCommand = defineCommand26({
9153
+ startCommand = defineCommand27({
8747
9154
  meta: {
8748
9155
  name: "start",
8749
9156
  group: "run",
@@ -8768,7 +9175,7 @@ var init_start = __esm({
8768
9175
  hostPort = proxyHostPort(global);
8769
9176
  }
8770
9177
  } catch (err) {
8771
- consola30.warn(
9178
+ consola32.warn(
8772
9179
  `Could not read container yml ahead of start: ${err instanceof Error ? err.message : String(err)}. Skipping Traefik pre-flight.`
8773
9180
  );
8774
9181
  }
@@ -8784,7 +9191,7 @@ var init_start = __esm({
8784
9191
  });
8785
9192
 
8786
9193
  // src/commands/status.ts
8787
- import { defineCommand as defineCommand27 } from "citty";
9194
+ import { defineCommand as defineCommand28 } from "citty";
8788
9195
  var statusCommand;
8789
9196
  var init_status = __esm({
8790
9197
  "src/commands/status.ts"() {
@@ -8792,7 +9199,7 @@ var init_status = __esm({
8792
9199
  init_paths();
8793
9200
  init_compose();
8794
9201
  init_dispatch();
8795
- statusCommand = defineCommand27({
9202
+ statusCommand = defineCommand28({
8796
9203
  meta: {
8797
9204
  name: "status",
8798
9205
  group: "run",
@@ -8822,8 +9229,8 @@ var init_status = __esm({
8822
9229
  });
8823
9230
 
8824
9231
  // src/commands/stop.ts
8825
- import { defineCommand as defineCommand28 } from "citty";
8826
- import { consola as consola31 } from "consola";
9232
+ import { defineCommand as defineCommand29 } from "citty";
9233
+ import { consola as consola33 } from "consola";
8827
9234
  var stopCommand;
8828
9235
  var init_stop = __esm({
8829
9236
  "src/commands/stop.ts"() {
@@ -8832,7 +9239,7 @@ var init_stop = __esm({
8832
9239
  init_compose();
8833
9240
  init_proxy();
8834
9241
  init_dispatch();
8835
- stopCommand = defineCommand28({
9242
+ stopCommand = defineCommand29({
8836
9243
  meta: {
8837
9244
  name: "stop",
8838
9245
  group: "run",
@@ -8857,10 +9264,10 @@ var init_stop = __esm({
8857
9264
  });
8858
9265
  try {
8859
9266
  await maybeStopProxy({
8860
- logger: { info: (msg) => consola31.info(msg) }
9267
+ logger: { info: (msg) => consola33.info(msg) }
8861
9268
  });
8862
9269
  } catch (err) {
8863
- consola31.warn(
9270
+ consola33.warn(
8864
9271
  `Could not tear down the Traefik proxy: ${err instanceof Error ? err.message : String(err)}. Ignored.`
8865
9272
  );
8866
9273
  }
@@ -8872,11 +9279,11 @@ var init_stop = __esm({
8872
9279
  });
8873
9280
 
8874
9281
  // src/tunnel/resolve.ts
8875
- import { existsSync as existsSync13 } from "fs";
8876
- import path21 from "path";
9282
+ import { existsSync as existsSync15 } from "fs";
9283
+ import path23 from "path";
8877
9284
  async function resolveTunnelTarget(opts) {
8878
9285
  const ymlPath = containerConfigPath(opts.name, opts.monocerosHome);
8879
- if (!existsSync13(ymlPath)) {
9286
+ if (!existsSync15(ymlPath)) {
8880
9287
  throw new Error(
8881
9288
  `No yml profile for '${opts.name}' at ${ymlPath}. Run \`monoceros init ${opts.name}\` first.`
8882
9289
  );
@@ -8884,13 +9291,13 @@ async function resolveTunnelTarget(opts) {
8884
9291
  const parsed = await readConfig(ymlPath);
8885
9292
  const config = parsed.config;
8886
9293
  const containerRoot = containerDir(opts.name, opts.monocerosHome);
8887
- if (!existsSync13(containerRoot)) {
9294
+ if (!existsSync15(containerRoot)) {
8888
9295
  throw new Error(
8889
9296
  `Container '${opts.name}' is not materialised at ${containerRoot}. Run \`monoceros apply ${opts.name}\` first.`
8890
9297
  );
8891
9298
  }
8892
- const composePath = path21.join(containerRoot, ".devcontainer", "compose.yaml");
8893
- const isCompose = existsSync13(composePath);
9299
+ const composePath = path23.join(containerRoot, ".devcontainer", "compose.yaml");
9300
+ const isCompose = existsSync15(composePath);
8894
9301
  const parsedTarget = parseTargetArg(opts.target, config);
8895
9302
  const docker = opts.docker ?? defaultDockerExec;
8896
9303
  if (isCompose) {
@@ -9125,8 +9532,8 @@ var init_port_check2 = __esm({
9125
9532
  });
9126
9533
 
9127
9534
  // src/tunnel/run.ts
9128
- import { spawn as spawn8 } from "child_process";
9129
- import { consola as consola32 } from "consola";
9535
+ import { spawn as spawn10 } from "child_process";
9536
+ import { consola as consola34 } from "consola";
9130
9537
  function signalNumber(signal) {
9131
9538
  switch (signal) {
9132
9539
  case "SIGINT":
@@ -9139,8 +9546,8 @@ function signalNumber(signal) {
9139
9546
  }
9140
9547
  async function runTunnel(opts) {
9141
9548
  const log = opts.logger ?? {
9142
- info: (m) => consola32.info(m),
9143
- warn: (m) => consola32.warn(m)
9549
+ info: (m) => consola34.info(m),
9550
+ warn: (m) => consola34.warn(m)
9144
9551
  };
9145
9552
  const resolve = opts.resolve ?? resolveTunnelTarget;
9146
9553
  const resolveArgs3 = {
@@ -9215,7 +9622,7 @@ var init_run3 = __esm({
9215
9622
  init_port_check2();
9216
9623
  SOCAT_IMAGE = "alpine/socat:1.8.0.3";
9217
9624
  defaultDockerSpawn = (args) => {
9218
- const child = spawn8("docker", args, {
9625
+ const child = spawn10("docker", args, {
9219
9626
  stdio: "inherit"
9220
9627
  });
9221
9628
  const exited = new Promise((resolve, reject) => {
@@ -9246,8 +9653,8 @@ var init_run3 = __esm({
9246
9653
  });
9247
9654
 
9248
9655
  // src/commands/tunnel.ts
9249
- import { defineCommand as defineCommand29 } from "citty";
9250
- import { consola as consola33 } from "consola";
9656
+ import { defineCommand as defineCommand30 } from "citty";
9657
+ import { consola as consola35 } from "consola";
9251
9658
  function parseLocalPort(raw) {
9252
9659
  if (raw === void 0) return void 0;
9253
9660
  const n = Number(raw);
@@ -9263,7 +9670,7 @@ var init_tunnel = __esm({
9263
9670
  "src/commands/tunnel.ts"() {
9264
9671
  "use strict";
9265
9672
  init_run3();
9266
- tunnelCommand = defineCommand29({
9673
+ tunnelCommand = defineCommand30({
9267
9674
  meta: {
9268
9675
  name: "tunnel",
9269
9676
  group: "discovery",
@@ -9300,7 +9707,7 @@ var init_tunnel = __esm({
9300
9707
  });
9301
9708
  process.exit(exitCode);
9302
9709
  } catch (err) {
9303
- consola33.error(err instanceof Error ? err.message : String(err));
9710
+ consola35.error(err instanceof Error ? err.message : String(err));
9304
9711
  process.exit(1);
9305
9712
  }
9306
9713
  }
@@ -9309,8 +9716,8 @@ var init_tunnel = __esm({
9309
9716
  });
9310
9717
 
9311
9718
  // src/upgrade/index.ts
9312
- import { existsSync as existsSync14, promises as fs17 } from "fs";
9313
- import { consola as consola34 } from "consola";
9719
+ import { existsSync as existsSync16, promises as fs17 } from "fs";
9720
+ import { consola as consola36 } from "consola";
9314
9721
  async function fetchRuntimeVersions() {
9315
9722
  const tokenUrl = `https://ghcr.io/token?service=ghcr.io&scope=repository:${RUNTIME_REPO}:pull`;
9316
9723
  const tokenRes = await fetch(tokenUrl);
@@ -9348,7 +9755,7 @@ ${yml}`;
9348
9755
  }
9349
9756
  async function runUpgrade(opts) {
9350
9757
  const home = opts.monocerosHome ?? monocerosHome();
9351
- const logger = opts.logger ?? consola34;
9758
+ const logger = opts.logger ?? consola36;
9352
9759
  const fetchVersions = opts.fetchVersions ?? fetchRuntimeVersions;
9353
9760
  if (opts.list) {
9354
9761
  const versions = await fetchVersions();
@@ -9368,7 +9775,7 @@ async function runUpgrade(opts) {
9368
9775
  );
9369
9776
  }
9370
9777
  const ymlPath = containerConfigPath(opts.name, home);
9371
- if (!existsSync14(ymlPath)) {
9778
+ if (!existsSync16(ymlPath)) {
9372
9779
  throw new Error(
9373
9780
  `No such config: ${ymlPath}. Run \`monoceros init <template> ${opts.name}\` first.`
9374
9781
  );
@@ -9416,7 +9823,7 @@ var init_upgrade = __esm({
9416
9823
  });
9417
9824
 
9418
9825
  // src/commands/upgrade.ts
9419
- import { defineCommand as defineCommand30 } from "citty";
9826
+ import { defineCommand as defineCommand31 } from "citty";
9420
9827
  var upgradeCommand;
9421
9828
  var init_upgrade2 = __esm({
9422
9829
  "src/commands/upgrade.ts"() {
@@ -9424,7 +9831,7 @@ var init_upgrade2 = __esm({
9424
9831
  init_upgrade();
9425
9832
  init_version();
9426
9833
  init_dispatch();
9427
- upgradeCommand = defineCommand30({
9834
+ upgradeCommand = defineCommand31({
9428
9835
  meta: {
9429
9836
  name: "upgrade",
9430
9837
  group: "lifecycle",
@@ -9466,7 +9873,7 @@ var main_exports = {};
9466
9873
  __export(main_exports, {
9467
9874
  main: () => main
9468
9875
  });
9469
- import { defineCommand as defineCommand31 } from "citty";
9876
+ import { defineCommand as defineCommand32 } from "citty";
9470
9877
  var main;
9471
9878
  var init_main = __esm({
9472
9879
  "src/main.ts"() {
@@ -9483,6 +9890,7 @@ var init_main = __esm({
9483
9890
  init_complete();
9484
9891
  init_init2();
9485
9892
  init_list_components();
9893
+ init_login2();
9486
9894
  init_logs();
9487
9895
  init_port();
9488
9896
  init_remove_apt_packages();
@@ -9502,7 +9910,7 @@ var init_main = __esm({
9502
9910
  init_tunnel();
9503
9911
  init_upgrade2();
9504
9912
  init_version();
9505
- main = defineCommand31({
9913
+ main = defineCommand32({
9506
9914
  meta: {
9507
9915
  name: "monoceros",
9508
9916
  version: CLI_VERSION,
@@ -9513,6 +9921,7 @@ var init_main = __esm({
9513
9921
  "list-components": listComponentsCommand,
9514
9922
  shell: shellCommand,
9515
9923
  run: runCommand,
9924
+ login: loginCommand,
9516
9925
  logs: logsCommand,
9517
9926
  start: startCommand,
9518
9927
  stop: stopCommand,
@@ -9798,25 +10207,25 @@ function detectHelpRequest(argv, main2) {
9798
10207
  const separatorIdx = argv.indexOf("--");
9799
10208
  if (helpIdx === -1) return null;
9800
10209
  if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
9801
- const path22 = [];
10210
+ const path24 = [];
9802
10211
  const tokens = argv.slice(
9803
10212
  0,
9804
10213
  separatorIdx === -1 ? argv.length : separatorIdx
9805
10214
  );
9806
10215
  let cursor = main2;
9807
10216
  const mainName = (main2.meta ?? {}).name ?? "monoceros";
9808
- path22.push(mainName);
10217
+ path24.push(mainName);
9809
10218
  for (const tok of tokens) {
9810
10219
  if (tok.startsWith("-")) continue;
9811
10220
  const subs = cursor.subCommands ?? {};
9812
10221
  if (tok in subs) {
9813
10222
  cursor = subs[tok];
9814
- path22.push(tok);
10223
+ path24.push(tok);
9815
10224
  continue;
9816
10225
  }
9817
10226
  break;
9818
10227
  }
9819
- return { path: path22, cmd: cursor };
10228
+ return { path: path24, cmd: cursor };
9820
10229
  }
9821
10230
  async function maybeRenderHelp(argv, main2) {
9822
10231
  const hit = detectHelpRequest(argv, main2);