@dev-anywhere/proxy 0.1.0 → 0.1.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.
package/dist/index.js CHANGED
@@ -1,22 +1,24 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- daemonEnvArgs,
4
- setDesiredDaemonEnv
5
- } from "./chunk-7PXDRNLY.js";
3
+ daemonRelayArgs,
4
+ setDesiredDaemonRelay
5
+ } from "./chunk-CDKXSDAV.js";
6
6
  import {
7
7
  spawnScript
8
8
  } from "./chunk-ZUWAB67J.js";
9
9
  import {
10
10
  CONFIG_PATH,
11
11
  PID_PATH,
12
+ PROFILE_NAME,
12
13
  SERVICE_LOG_PATH,
13
14
  SOCK_PATH,
14
15
  STOPPED_PATH,
15
16
  createIpcReader,
17
+ ensureProfileWorkspace,
16
18
  initWorkspace,
17
19
  isInitialized,
18
20
  serializeIpc
19
- } from "./chunk-QWPI6YON.js";
21
+ } from "./chunk-DFLQ3TFT.js";
20
22
 
21
23
  // src/index.ts
22
24
  import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
@@ -34,6 +36,25 @@ function normalizeCliArgs(args) {
34
36
  }
35
37
  return normalized;
36
38
  }
39
+ function stripProxyProfileArgs(args) {
40
+ const result = [];
41
+ for (let i = 0; i < args.length; i++) {
42
+ const arg = args[i];
43
+ if (arg === "claude" || arg === "codex") {
44
+ result.push(...args.slice(i));
45
+ break;
46
+ }
47
+ if (arg === "--profile") {
48
+ i++;
49
+ continue;
50
+ }
51
+ if (arg.startsWith("--profile=")) {
52
+ continue;
53
+ }
54
+ result.push(arg);
55
+ }
56
+ return result;
57
+ }
37
58
  function extractAgentInvocation(args) {
38
59
  const [agent, ...providerArgs] = args;
39
60
  if (agent !== "claude" && agent !== "codex") {
@@ -72,6 +93,7 @@ function showStatus() {
72
93
  lines++;
73
94
  };
74
95
  if (!existsSync(PID_PATH)) {
96
+ log(`Profile: ${PROFILE_NAME}`);
75
97
  log("Service: not running");
76
98
  resolve(lines);
77
99
  return;
@@ -88,19 +110,22 @@ function showStatus() {
88
110
  resolve(lines);
89
111
  return;
90
112
  }
113
+ log(`Profile: ${PROFILE_NAME}`);
91
114
  log(`Service: running (PID ${pid})`);
92
115
  log(`Socket: ${SOCK_PATH}`);
93
116
  log(`Log: ${SERVICE_LOG_PATH}`);
94
117
  const sock = connect(SOCK_PATH);
95
118
  sock.on("error", () => {
96
119
  log("Sessions: unable to connect");
120
+ sock.destroy();
97
121
  resolve(lines);
98
122
  });
99
123
  sock.on("connect", () => {
100
124
  createIpcReader(sock, (msg) => {
101
125
  if (msg.type === "service_status_response") {
102
126
  const config = msg.config;
103
- log(`Env: ${config.envName ?? "single"} (${config.envNameSource})`);
127
+ log(`Daemon: profile ${config.profile ?? PROFILE_NAME}`);
128
+ log(`Relay: ${config.relayName} (${config.relayNameSource})`);
104
129
  log(`Config: relay ${config.relayUrl ?? "(unset)"} (${config.relayUrlSource})`);
105
130
  const relay = msg.relay;
106
131
  if (!relay) {
@@ -152,6 +177,7 @@ async function waitForServeReady(timeoutMs) {
152
177
  return false;
153
178
  }
154
179
  async function startDaemon(options) {
180
+ ensureProfileWorkspace();
155
181
  if (existsSync(PID_PATH)) {
156
182
  const pid = parseInt(readFileSync(PID_PATH, "utf-8").trim(), 10);
157
183
  try {
@@ -162,8 +188,9 @@ async function startDaemon(options) {
162
188
  }
163
189
  }
164
190
  if (existsSync(STOPPED_PATH)) unlinkSync(STOPPED_PATH);
165
- const serveArgs = daemonEnvArgs(options?.envName);
191
+ const serveArgs = ["--profile", PROFILE_NAME, ...daemonRelayArgs(options?.relayName)];
166
192
  const child = spawnScript(new URL("./serve", import.meta.url), serveArgs, {
193
+ env: { ...process.env },
167
194
  stdio: ["ignore", "ignore", "pipe"],
168
195
  unref: false
169
196
  });
@@ -205,15 +232,15 @@ async function startDaemon(options) {
205
232
  }
206
233
  process.exit(1);
207
234
  }
208
- var program = new Command("dev-anywhere").description("Dev Anywhere - transparent local AI CLI proxy with remote control").version(pkg.version).allowUnknownOption().allowExcessArguments().action(async () => {
235
+ var program = new Command("dev-anywhere").description("Dev Anywhere - transparent local AI CLI proxy with remote control").version(pkg.version).option("--profile <name>", "Use an isolated local proxy profile").allowUnknownOption().allowExcessArguments().action(async () => {
209
236
  if (!isInitialized()) {
210
237
  console.error(`Dev Anywhere is not initialized. Run "dev-anywhere init" first.`);
211
238
  process.exit(1);
212
239
  }
213
- const { startTerminal } = await import("./terminal-ZPPKNAL4.js");
240
+ const { startTerminal } = await import("./terminal-GIU6MXOR.js");
214
241
  let invocation;
215
242
  try {
216
- invocation = extractAgentInvocation(cliArgs);
243
+ invocation = extractAgentInvocation(cliArgsWithoutProfile);
217
244
  } catch (err) {
218
245
  console.error(err instanceof Error ? err.message : String(err));
219
246
  process.exit(1);
@@ -221,26 +248,26 @@ var program = new Command("dev-anywhere").description("Dev Anywhere - transparen
221
248
  const { provider, args } = invocation;
222
249
  await startTerminal(args, provider);
223
250
  });
224
- var serve = new Command("serve").description("Manage the dev-anywhere background service").option("-d, --daemon", "Run in background").action(async (opts) => {
251
+ var serve = new Command("serve").description("Manage the dev-anywhere background service").option("--profile <name>", "Use an isolated local proxy profile").option("-d, --daemon", "Run in background").action(async (opts) => {
225
252
  if (!isInitialized()) {
226
253
  console.error(`Dev Anywhere is not initialized. Run "dev-anywhere init" first.`);
227
254
  process.exit(1);
228
255
  }
229
256
  if (opts.daemon) {
230
- setDesiredDaemonEnv(void 0);
257
+ setDesiredDaemonRelay(void 0);
231
258
  await startDaemon();
232
259
  } else {
233
260
  const { startService } = await import("./serve.js");
234
261
  await startService();
235
262
  }
236
263
  });
237
- serve.command("start").description("Start the background service").option("--env <name>", "Use a named config environment").action(async (opts) => {
264
+ serve.command("start").description("Start the background service").option("--relay <name>", "Use a named relay from config").action(async (opts) => {
238
265
  if (!isInitialized()) {
239
266
  console.error(`Dev Anywhere is not initialized. Run "dev-anywhere init" first.`);
240
267
  process.exit(1);
241
268
  }
242
- setDesiredDaemonEnv(opts.env);
243
- await startDaemon({ envName: opts.env });
269
+ setDesiredDaemonRelay(opts.relay);
270
+ await startDaemon({ relayName: opts.relay });
244
271
  });
245
272
  serve.command("status").description("Show service status and active sessions").option("-w, --watch", "Continuous monitoring mode").option("-n, --interval <seconds>", "Refresh interval in seconds", "2").action(async (opts) => {
246
273
  if (opts.watch) {
@@ -259,10 +286,10 @@ serve.command("status").description("Show service status and active sessions").o
259
286
  serve.command("stop").description("Stop the background service").action(() => {
260
287
  stopService();
261
288
  });
262
- serve.command("restart").description("Restart the background service").option("--env <name>", "Use a named config environment").action(async (opts) => {
263
- setDesiredDaemonEnv(opts.env);
289
+ serve.command("restart").description("Restart the background service").option("--relay <name>", "Use a named relay from config").action(async (opts) => {
290
+ setDesiredDaemonRelay(opts.relay);
264
291
  stopService();
265
- await startDaemon({ envName: opts.env });
292
+ await startDaemon({ relayName: opts.relay });
266
293
  });
267
294
  program.addCommand(serve);
268
295
  program.command("init").description("Initialize dev-anywhere workspace (~/.dev-anywhere)").action(() => {
@@ -275,5 +302,6 @@ program.command("init").description("Initialize dev-anywhere workspace (~/.dev-a
275
302
  console.log(`Edit ${CONFIG_PATH} to configure relay server URL.`);
276
303
  });
277
304
  var cliArgs = normalizeCliArgs(process.argv.slice(2));
278
- program.parse(cliArgs, { from: "user" });
305
+ var cliArgsWithoutProfile = stripProxyProfileArgs(cliArgs);
306
+ program.parse(cliArgsWithoutProfile, { from: "user" });
279
307
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/cli-args.ts"],"sourcesContent":["import { existsSync, readFileSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { connect } from \"node:net\";\nimport { setTimeout as sleep } from \"node:timers/promises\";\nimport { fileURLToPath } from \"node:url\";\nimport { dirname, join } from \"node:path\";\nimport { Command } from \"commander\";\nimport {\n PID_PATH,\n SOCK_PATH,\n STOPPED_PATH,\n SERVICE_LOG_PATH,\n CONFIG_PATH,\n isInitialized,\n initWorkspace,\n} from \"./common/paths.js\";\nimport { spawnScript } from \"./common/env.js\";\nimport { daemonEnvArgs, setDesiredDaemonEnv } from \"./common/daemon-env.js\";\nimport { createIpcReader, serializeIpc } from \"./ipc/ipc-protocol.js\";\nimport { extractAgentInvocation, normalizeCliArgs } from \"./cli-args.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkg = JSON.parse(readFileSync(join(__dirname, \"..\", \"package.json\"), \"utf-8\")) as {\n version: string;\n};\n\nfunction stopService(): boolean {\n if (!existsSync(PID_PATH)) {\n console.error(\"Service is not running (no PID file)\");\n return false;\n }\n const pid = parseInt(readFileSync(PID_PATH, \"utf-8\").trim(), 10);\n try {\n process.kill(pid, \"SIGTERM\");\n console.log(`Service stopped (PID ${pid})`);\n } catch {\n console.error(`Process ${pid} not found, cleaning up stale files`);\n }\n if (existsSync(PID_PATH)) unlinkSync(PID_PATH);\n if (existsSync(SOCK_PATH)) unlinkSync(SOCK_PATH);\n writeFileSync(STOPPED_PATH, String(Date.now()));\n return true;\n}\n\nfunction showStatus(): Promise<number> {\n return new Promise((resolve) => {\n let lines = 0;\n const log = (s: string) => {\n console.log(s);\n lines++;\n };\n\n if (!existsSync(PID_PATH)) {\n log(\"Service: not running\");\n resolve(lines);\n return;\n }\n const pid = parseInt(readFileSync(PID_PATH, \"utf-8\").trim(), 10);\n let alive = false;\n try {\n process.kill(pid, 0);\n alive = true;\n } catch {\n // process.kill(pid, 0) 抛错表示进程不存在\n }\n\n if (!alive) {\n log(\"Service: dead (stale PID file)\");\n resolve(lines);\n return;\n }\n\n log(`Service: running (PID ${pid})`);\n log(`Socket: ${SOCK_PATH}`);\n log(`Log: ${SERVICE_LOG_PATH}`);\n\n const sock = connect(SOCK_PATH);\n sock.on(\"error\", () => {\n log(\"Sessions: unable to connect\");\n resolve(lines);\n });\n sock.on(\"connect\", () => {\n createIpcReader(sock, (msg) => {\n if (msg.type === \"service_status_response\") {\n const config = msg.config;\n log(`Env: ${config.envName ?? \"single\"} (${config.envNameSource})`);\n log(`Config: relay ${config.relayUrl ?? \"(unset)\"} (${config.relayUrlSource})`);\n const relay = msg.relay;\n if (!relay) {\n log(\"Relay: not configured\");\n } else if (relay.connected) {\n log(`Relay: connected (proxy: ${relay.proxyId})`);\n log(\n ` queue depth: ${relay.queueDepth}, reconnect attempts: ${relay.reconnectAttempt}`,\n );\n } else {\n log(\n `Relay: disconnected (proxy: ${relay.proxyId}, reconnecting: attempt ${relay.reconnectAttempt}, queued: ${relay.queueDepth})`,\n );\n }\n log(\"\");\n\n // 显示会话列表\n const sessions = msg.sessions;\n if (sessions.length === 0) {\n log(\"Sessions: none\");\n } else {\n log(`Sessions: ${sessions.length}`);\n for (const s of sessions) {\n log(` ${s.id} ${s.mode} ${s.state} worker: ${s.hasWorker ? \"yes\" : \"no\"}`);\n }\n }\n sock.destroy();\n resolve(lines);\n }\n });\n sock.write(serializeIpc({ type: \"service_status_request\" }));\n });\n });\n}\n\nconst DAEMON_STARTUP_TIMEOUT_MS = 30_000;\nconst DAEMON_STARTUP_POLL_MS = 200;\n\n// 轮询 SOCK_PATH 直到可连接,作为 serve 的 readiness 信号。\n// serve.ts 里 server.listen(SOCK_PATH) 是启动序列的最后一步,连上即代表 ready。\nasync function waitForServeReady(timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const connected = await new Promise<boolean>((resolve) => {\n const sock = connect(SOCK_PATH);\n sock.once(\"connect\", () => {\n sock.destroy();\n resolve(true);\n });\n sock.once(\"error\", () => resolve(false));\n });\n if (connected) return true;\n await sleep(DAEMON_STARTUP_POLL_MS);\n }\n return false;\n}\n\nasync function startDaemon(options?: { envName?: string }): Promise<void> {\n if (existsSync(PID_PATH)) {\n const pid = parseInt(readFileSync(PID_PATH, \"utf-8\").trim(), 10);\n try {\n process.kill(pid, 0);\n console.error(`Service is already running (PID ${pid})`);\n return;\n } catch {\n // process.kill(pid, 0) 抛错表示进程不存在,继续启动\n }\n }\n if (existsSync(STOPPED_PATH)) unlinkSync(STOPPED_PATH);\n\n // stderr 走 pipe 由父 CLI 订阅:子进程 ready 前(pino logger 未接管)的启动错误\n // 会被捕获;ready 后父 detach,pino 接管所有输出到 service.log。\n // start 命令必须等 daemon socket 可连接后再退出;否则用户会看到“启动成功”,实际服务还没就绪。\n const serveArgs = daemonEnvArgs(options?.envName);\n const child = spawnScript(new URL(\"./serve\", import.meta.url), serveArgs, {\n stdio: [\"ignore\", \"ignore\", \"pipe\"],\n unref: false,\n });\n\n const stderrChunks: Buffer[] = [];\n child.stderr!.on(\"data\", (chunk: Buffer) => {\n stderrChunks.push(chunk);\n });\n\n // race: readiness handshake vs. 子进程先挂。子进程 ready 前就 exit 说明启动硬失败,\n // 不必再等到 30s 超时才报错。\n type Outcome =\n | { kind: \"ready\" }\n | { kind: \"timeout\" }\n | { kind: \"exited\"; code: number | null; signal: NodeJS.Signals | null };\n\n const readyOutcome: Promise<Outcome> = waitForServeReady(DAEMON_STARTUP_TIMEOUT_MS).then((ok) =>\n ok ? { kind: \"ready\" as const } : { kind: \"timeout\" as const },\n );\n const exitOutcome: Promise<Outcome> = new Promise((resolve) => {\n // 设 listener 前已经 exit 的边界:Node 记在 exitCode 上\n if (child.exitCode !== null) {\n resolve({ kind: \"exited\", code: child.exitCode, signal: child.signalCode });\n return;\n }\n child.once(\"exit\", (code, signal) => resolve({ kind: \"exited\", code, signal }));\n });\n\n const result = await Promise.race([readyOutcome, exitOutcome]);\n\n if (result.kind === \"ready\") {\n console.log(`Service started in background (PID ${child.pid})`);\n // ready 后 detach:摘 stderr 订阅 + destroy pipe + unref 子进程。\n // 单独 child.unref() 不够,父侧的 stderr pipe fd 还在事件循环里会让父 CLI 永不退出;\n // 必须 destroy 掉 pipe 才能真正释放 refcount。pino 已接管子进程的输出到 service.log。\n child.stderr!.removeAllListeners(\"data\");\n child.stderr!.destroy();\n child.unref();\n return;\n }\n\n // 失败路径:timeout 或 exited\n const stderrOutput = Buffer.concat(stderrChunks).toString(\"utf-8\").trim();\n if (result.kind === \"exited\") {\n console.error(`Service exited during startup (code=${result.code}, signal=${result.signal}).`);\n } else {\n console.error(`Service failed to become ready within ${DAEMON_STARTUP_TIMEOUT_MS / 1000}s.`);\n try {\n process.kill(child.pid!, \"SIGTERM\");\n } catch {\n // 子进程可能已自己退出,kill 失败不影响后续退出码\n }\n }\n if (stderrOutput) {\n console.error(\"--- child stderr ---\");\n console.error(stderrOutput);\n }\n process.exit(1);\n}\n\nconst program = new Command(\"dev-anywhere\")\n .description(\"Dev Anywhere - transparent local AI CLI proxy with remote control\")\n .version(pkg.version)\n .allowUnknownOption()\n .allowExcessArguments()\n .action(async () => {\n if (!isInitialized()) {\n console.error(`Dev Anywhere is not initialized. Run \"dev-anywhere init\" first.`);\n process.exit(1);\n }\n // 延迟导入 terminal: CLI 的其他子命令(init/stop/status)不需要 PTY + xterm 相关依赖,\n // tsup 基于 dynamic import 自动代码分裂,避免所有命令都为 terminal 付出 14KB 额外启动成本。\n const { startTerminal } = await import(\"./terminal.js\");\n let invocation: ReturnType<typeof extractAgentInvocation>;\n try {\n invocation = extractAgentInvocation(cliArgs);\n } catch (err) {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n const { provider, args } = invocation;\n await startTerminal(args, provider);\n });\n\n// serve 子命令组\nconst serve = new Command(\"serve\")\n .description(\"Manage the dev-anywhere background service\")\n .option(\"-d, --daemon\", \"Run in background\")\n .action(async (opts) => {\n if (!isInitialized()) {\n console.error(`Dev Anywhere is not initialized. Run \"dev-anywhere init\" first.`);\n process.exit(1);\n }\n if (opts.daemon) {\n setDesiredDaemonEnv(undefined);\n await startDaemon();\n } else {\n // 延迟导入 serve: daemon 模式只需要 startDaemon(纯 spawn),不需要加载 70KB 的 serve bundle\n const { startService } = await import(\"./serve.js\");\n await startService();\n }\n });\n\nserve\n .command(\"start\")\n .description(\"Start the background service\")\n .option(\"--env <name>\", \"Use a named config environment\")\n .action(async (opts) => {\n if (!isInitialized()) {\n console.error(`Dev Anywhere is not initialized. Run \"dev-anywhere init\" first.`);\n process.exit(1);\n }\n setDesiredDaemonEnv(opts.env);\n await startDaemon({ envName: opts.env });\n });\n\nserve\n .command(\"status\")\n .description(\"Show service status and active sessions\")\n .option(\"-w, --watch\", \"Continuous monitoring mode\")\n .option(\"-n, --interval <seconds>\", \"Refresh interval in seconds\", \"2\")\n .action(async (opts) => {\n if (opts.watch) {\n const intervalMs = Number(opts.interval) * 1000;\n let lastLines = await showStatus();\n setInterval(async () => {\n if (lastLines > 0) {\n process.stdout.write(`\\x1B[${lastLines}A\\x1B[J`);\n }\n lastLines = await showStatus();\n }, intervalMs);\n } else {\n await showStatus();\n }\n });\n\nserve\n .command(\"stop\")\n .description(\"Stop the background service\")\n .action(() => {\n stopService();\n });\n\nserve\n .command(\"restart\")\n .description(\"Restart the background service\")\n .option(\"--env <name>\", \"Use a named config environment\")\n .action(async (opts) => {\n setDesiredDaemonEnv(opts.env);\n stopService();\n await startDaemon({ envName: opts.env });\n });\n\nprogram.addCommand(serve);\n\nprogram\n .command(\"init\")\n .description(\"Initialize dev-anywhere workspace (~/.dev-anywhere)\")\n .action(() => {\n if (isInitialized()) {\n console.log(`Already initialized. Config at ${CONFIG_PATH}`);\n return;\n }\n initWorkspace();\n console.log(\"Initialized ~/.dev-anywhere/\");\n console.log(`Edit ${CONFIG_PATH} to configure relay server URL.`);\n });\n\n// pnpm run dev -- args 会在参数前插入 \"--\"。根脚本和用户命令都可能再加一层\n// 分隔符,所以这里过滤所有前导分隔符,再交给 Commander 和 provider 参数解析。\nconst cliArgs = normalizeCliArgs(process.argv.slice(2));\n\nprogram.parse(cliArgs, { from: \"user\" });\n","import type { ProviderId } from \"./providers/index.js\";\n\nexport function normalizeCliArgs(args: string[]): string[] {\n const normalized = [...args];\n while (normalized[0] === \"--\") {\n normalized.shift();\n }\n return normalized;\n}\n\nexport function extractAgentInvocation(args: string[]): { provider: ProviderId; args: string[] } {\n const [agent, ...providerArgs] = args;\n if (agent !== \"claude\" && agent !== \"codex\") {\n throw new Error(\n 'Missing Agent CLI. Use \"dev-anywhere claude ...\" or \"dev-anywhere codex ...\".',\n );\n }\n return { provider: agent, args: providerArgs };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/cli-args.ts"],"sourcesContent":["import { existsSync, readFileSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { connect } from \"node:net\";\nimport { setTimeout as sleep } from \"node:timers/promises\";\nimport { fileURLToPath } from \"node:url\";\nimport { dirname, join } from \"node:path\";\nimport { Command } from \"commander\";\nimport {\n PID_PATH,\n SOCK_PATH,\n STOPPED_PATH,\n SERVICE_LOG_PATH,\n CONFIG_PATH,\n PROFILE_NAME,\n ensureProfileWorkspace,\n isInitialized,\n initWorkspace,\n} from \"./common/paths.js\";\nimport { spawnScript } from \"./common/env.js\";\nimport { daemonRelayArgs, setDesiredDaemonRelay } from \"./common/daemon-env.js\";\nimport { createIpcReader, serializeIpc } from \"./ipc/ipc-protocol.js\";\nimport { extractAgentInvocation, normalizeCliArgs, stripProxyProfileArgs } from \"./cli-args.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\nconst pkg = JSON.parse(readFileSync(join(__dirname, \"..\", \"package.json\"), \"utf-8\")) as {\n version: string;\n};\n\nfunction stopService(): boolean {\n if (!existsSync(PID_PATH)) {\n console.error(\"Service is not running (no PID file)\");\n return false;\n }\n const pid = parseInt(readFileSync(PID_PATH, \"utf-8\").trim(), 10);\n try {\n process.kill(pid, \"SIGTERM\");\n console.log(`Service stopped (PID ${pid})`);\n } catch {\n console.error(`Process ${pid} not found, cleaning up stale files`);\n }\n if (existsSync(PID_PATH)) unlinkSync(PID_PATH);\n if (existsSync(SOCK_PATH)) unlinkSync(SOCK_PATH);\n writeFileSync(STOPPED_PATH, String(Date.now()));\n return true;\n}\n\nfunction showStatus(): Promise<number> {\n return new Promise((resolve) => {\n let lines = 0;\n const log = (s: string) => {\n console.log(s);\n lines++;\n };\n\n if (!existsSync(PID_PATH)) {\n log(`Profile: ${PROFILE_NAME}`);\n log(\"Service: not running\");\n resolve(lines);\n return;\n }\n const pid = parseInt(readFileSync(PID_PATH, \"utf-8\").trim(), 10);\n let alive = false;\n try {\n process.kill(pid, 0);\n alive = true;\n } catch {\n // process.kill(pid, 0) 抛错表示进程不存在\n }\n\n if (!alive) {\n log(\"Service: dead (stale PID file)\");\n resolve(lines);\n return;\n }\n\n log(`Profile: ${PROFILE_NAME}`);\n log(`Service: running (PID ${pid})`);\n log(`Socket: ${SOCK_PATH}`);\n log(`Log: ${SERVICE_LOG_PATH}`);\n\n const sock = connect(SOCK_PATH);\n sock.on(\"error\", () => {\n log(\"Sessions: unable to connect\");\n sock.destroy();\n resolve(lines);\n });\n sock.on(\"connect\", () => {\n createIpcReader(sock, (msg) => {\n if (msg.type === \"service_status_response\") {\n const config = msg.config;\n log(`Daemon: profile ${config.profile ?? PROFILE_NAME}`);\n log(`Relay: ${config.relayName} (${config.relayNameSource})`);\n log(`Config: relay ${config.relayUrl ?? \"(unset)\"} (${config.relayUrlSource})`);\n const relay = msg.relay;\n if (!relay) {\n log(\"Relay: not configured\");\n } else if (relay.connected) {\n log(`Relay: connected (proxy: ${relay.proxyId})`);\n log(\n ` queue depth: ${relay.queueDepth}, reconnect attempts: ${relay.reconnectAttempt}`,\n );\n } else {\n log(\n `Relay: disconnected (proxy: ${relay.proxyId}, reconnecting: attempt ${relay.reconnectAttempt}, queued: ${relay.queueDepth})`,\n );\n }\n log(\"\");\n\n // 显示会话列表\n const sessions = msg.sessions;\n if (sessions.length === 0) {\n log(\"Sessions: none\");\n } else {\n log(`Sessions: ${sessions.length}`);\n for (const s of sessions) {\n log(` ${s.id} ${s.mode} ${s.state} worker: ${s.hasWorker ? \"yes\" : \"no\"}`);\n }\n }\n sock.destroy();\n resolve(lines);\n }\n });\n sock.write(serializeIpc({ type: \"service_status_request\" }));\n });\n });\n}\n\nconst DAEMON_STARTUP_TIMEOUT_MS = 30_000;\nconst DAEMON_STARTUP_POLL_MS = 200;\n\n// 轮询 SOCK_PATH 直到可连接,作为 serve 的 readiness 信号。\n// serve.ts 里 server.listen(SOCK_PATH) 是启动序列的最后一步,连上即代表 ready。\nasync function waitForServeReady(timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n const connected = await new Promise<boolean>((resolve) => {\n const sock = connect(SOCK_PATH);\n sock.once(\"connect\", () => {\n sock.destroy();\n resolve(true);\n });\n sock.once(\"error\", () => resolve(false));\n });\n if (connected) return true;\n await sleep(DAEMON_STARTUP_POLL_MS);\n }\n return false;\n}\n\nasync function startDaemon(options?: { relayName?: string }): Promise<void> {\n ensureProfileWorkspace();\n if (existsSync(PID_PATH)) {\n const pid = parseInt(readFileSync(PID_PATH, \"utf-8\").trim(), 10);\n try {\n process.kill(pid, 0);\n console.error(`Service is already running (PID ${pid})`);\n return;\n } catch {\n // process.kill(pid, 0) 抛错表示进程不存在,继续启动\n }\n }\n if (existsSync(STOPPED_PATH)) unlinkSync(STOPPED_PATH);\n\n // stderr 走 pipe 由父 CLI 订阅:子进程 ready 前(pino logger 未接管)的启动错误\n // 会被捕获;ready 后父 detach,pino 接管所有输出到 service.log。\n // start 命令必须等 daemon socket 可连接后再退出;否则用户会看到“启动成功”,实际服务还没就绪。\n const serveArgs = [\"--profile\", PROFILE_NAME, ...daemonRelayArgs(options?.relayName)];\n const child = spawnScript(new URL(\"./serve\", import.meta.url), serveArgs, {\n env: { ...process.env },\n stdio: [\"ignore\", \"ignore\", \"pipe\"],\n unref: false,\n });\n\n const stderrChunks: Buffer[] = [];\n child.stderr!.on(\"data\", (chunk: Buffer) => {\n stderrChunks.push(chunk);\n });\n\n // race: readiness handshake vs. 子进程先挂。子进程 ready 前就 exit 说明启动硬失败,\n // 不必再等到 30s 超时才报错。\n type Outcome =\n | { kind: \"ready\" }\n | { kind: \"timeout\" }\n | { kind: \"exited\"; code: number | null; signal: NodeJS.Signals | null };\n\n const readyOutcome: Promise<Outcome> = waitForServeReady(DAEMON_STARTUP_TIMEOUT_MS).then((ok) =>\n ok ? { kind: \"ready\" as const } : { kind: \"timeout\" as const },\n );\n const exitOutcome: Promise<Outcome> = new Promise((resolve) => {\n // 设 listener 前已经 exit 的边界:Node 记在 exitCode 上\n if (child.exitCode !== null) {\n resolve({ kind: \"exited\", code: child.exitCode, signal: child.signalCode });\n return;\n }\n child.once(\"exit\", (code, signal) => resolve({ kind: \"exited\", code, signal }));\n });\n\n const result = await Promise.race([readyOutcome, exitOutcome]);\n\n if (result.kind === \"ready\") {\n console.log(`Service started in background (PID ${child.pid})`);\n // ready 后 detach:摘 stderr 订阅 + destroy pipe + unref 子进程。\n // 单独 child.unref() 不够,父侧的 stderr pipe fd 还在事件循环里会让父 CLI 永不退出;\n // 必须 destroy 掉 pipe 才能真正释放 refcount。pino 已接管子进程的输出到 service.log。\n child.stderr!.removeAllListeners(\"data\");\n child.stderr!.destroy();\n child.unref();\n return;\n }\n\n // 失败路径:timeout 或 exited\n const stderrOutput = Buffer.concat(stderrChunks).toString(\"utf-8\").trim();\n if (result.kind === \"exited\") {\n console.error(`Service exited during startup (code=${result.code}, signal=${result.signal}).`);\n } else {\n console.error(`Service failed to become ready within ${DAEMON_STARTUP_TIMEOUT_MS / 1000}s.`);\n try {\n process.kill(child.pid!, \"SIGTERM\");\n } catch {\n // 子进程可能已自己退出,kill 失败不影响后续退出码\n }\n }\n if (stderrOutput) {\n console.error(\"--- child stderr ---\");\n console.error(stderrOutput);\n }\n process.exit(1);\n}\n\nconst program = new Command(\"dev-anywhere\")\n .description(\"Dev Anywhere - transparent local AI CLI proxy with remote control\")\n .version(pkg.version)\n .option(\"--profile <name>\", \"Use an isolated local proxy profile\")\n .allowUnknownOption()\n .allowExcessArguments()\n .action(async () => {\n if (!isInitialized()) {\n console.error(`Dev Anywhere is not initialized. Run \"dev-anywhere init\" first.`);\n process.exit(1);\n }\n // 延迟导入 terminal: CLI 的其他子命令(init/stop/status)不需要 PTY + xterm 相关依赖,\n // tsup 基于 dynamic import 自动代码分裂,避免所有命令都为 terminal 付出 14KB 额外启动成本。\n const { startTerminal } = await import(\"./terminal.js\");\n let invocation: ReturnType<typeof extractAgentInvocation>;\n try {\n invocation = extractAgentInvocation(cliArgsWithoutProfile);\n } catch (err) {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n }\n const { provider, args } = invocation;\n await startTerminal(args, provider);\n });\n\n// serve 子命令组\nconst serve = new Command(\"serve\")\n .description(\"Manage the dev-anywhere background service\")\n .option(\"--profile <name>\", \"Use an isolated local proxy profile\")\n .option(\"-d, --daemon\", \"Run in background\")\n .action(async (opts) => {\n if (!isInitialized()) {\n console.error(`Dev Anywhere is not initialized. Run \"dev-anywhere init\" first.`);\n process.exit(1);\n }\n if (opts.daemon) {\n setDesiredDaemonRelay(undefined);\n await startDaemon();\n } else {\n // 延迟导入 serve: daemon 模式只需要 startDaemon(纯 spawn),不需要加载 70KB 的 serve bundle\n const { startService } = await import(\"./serve.js\");\n await startService();\n }\n });\n\nserve\n .command(\"start\")\n .description(\"Start the background service\")\n .option(\"--relay <name>\", \"Use a named relay from config\")\n .action(async (opts) => {\n if (!isInitialized()) {\n console.error(`Dev Anywhere is not initialized. Run \"dev-anywhere init\" first.`);\n process.exit(1);\n }\n setDesiredDaemonRelay(opts.relay);\n await startDaemon({ relayName: opts.relay });\n });\n\nserve\n .command(\"status\")\n .description(\"Show service status and active sessions\")\n .option(\"-w, --watch\", \"Continuous monitoring mode\")\n .option(\"-n, --interval <seconds>\", \"Refresh interval in seconds\", \"2\")\n .action(async (opts) => {\n if (opts.watch) {\n const intervalMs = Number(opts.interval) * 1000;\n let lastLines = await showStatus();\n setInterval(async () => {\n if (lastLines > 0) {\n process.stdout.write(`\\x1B[${lastLines}A\\x1B[J`);\n }\n lastLines = await showStatus();\n }, intervalMs);\n } else {\n await showStatus();\n }\n });\n\nserve\n .command(\"stop\")\n .description(\"Stop the background service\")\n .action(() => {\n stopService();\n });\n\nserve\n .command(\"restart\")\n .description(\"Restart the background service\")\n .option(\"--relay <name>\", \"Use a named relay from config\")\n .action(async (opts) => {\n setDesiredDaemonRelay(opts.relay);\n stopService();\n await startDaemon({ relayName: opts.relay });\n });\n\nprogram.addCommand(serve);\n\nprogram\n .command(\"init\")\n .description(\"Initialize dev-anywhere workspace (~/.dev-anywhere)\")\n .action(() => {\n if (isInitialized()) {\n console.log(`Already initialized. Config at ${CONFIG_PATH}`);\n return;\n }\n initWorkspace();\n console.log(\"Initialized ~/.dev-anywhere/\");\n console.log(`Edit ${CONFIG_PATH} to configure relay server URL.`);\n });\n\n// pnpm run dev -- args 会在参数前插入 \"--\"。根脚本和用户命令都可能再加一层\n// 分隔符,所以这里过滤所有前导分隔符,再交给 Commander 和 provider 参数解析。\nconst cliArgs = normalizeCliArgs(process.argv.slice(2));\nconst cliArgsWithoutProfile = stripProxyProfileArgs(cliArgs);\n\nprogram.parse(cliArgsWithoutProfile, { from: \"user\" });\n","import type { ProviderId } from \"./providers/index.js\";\n\nexport function normalizeCliArgs(args: string[]): string[] {\n const normalized = [...args];\n while (normalized[0] === \"--\") {\n normalized.shift();\n }\n return normalized;\n}\n\nexport function stripProxyProfileArgs(args: string[]): string[] {\n const result: string[] = [];\n for (let i = 0; i < args.length; i++) {\n const arg = args[i];\n if (arg === \"claude\" || arg === \"codex\") {\n result.push(...args.slice(i));\n break;\n }\n if (arg === \"--profile\") {\n i++;\n continue;\n }\n if (arg.startsWith(\"--profile=\")) {\n continue;\n }\n result.push(arg);\n }\n return result;\n}\n\nexport function extractAgentInvocation(args: string[]): { provider: ProviderId; args: string[] } {\n const [agent, ...providerArgs] = args;\n if (agent !== \"claude\" && agent !== \"codex\") {\n throw new Error(\n 'Missing Agent CLI. Use \"dev-anywhere claude ...\" or \"dev-anywhere codex ...\".',\n );\n }\n return { provider: agent, args: providerArgs };\n}\n"],"mappings":"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","names":[]}
package/dist/serve.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  KnownContentBlockSchema,
6
6
  SeqCounter,
7
7
  StreamJsonEventSchema
8
- } from "./chunk-JPJMOVQ5.js";
8
+ } from "./chunk-QXOARRC2.js";
9
9
  import {
10
10
  createFSM,
11
11
  defineFSM,
@@ -14,7 +14,7 @@ import {
14
14
  serviceLogger,
15
15
  shouldReleaseApprovalWait,
16
16
  stateAfterApprovalRelease
17
- } from "./chunk-WXWH6L7J.js";
17
+ } from "./chunk-TG7JPHE5.js";
18
18
  import {
19
19
  spawnScript
20
20
  } from "./chunk-ZUWAB67J.js";
@@ -27,9 +27,12 @@ import {
27
27
  CONFIG_PATH,
28
28
  ControlErrorCode,
29
29
  DATA_DIR,
30
+ DEFAULT_PROXY_PROFILE,
30
31
  HOOK_REGISTRY_PATH,
31
32
  MessageEnvelopeSchema,
32
33
  PID_PATH,
34
+ PROFILE_NAME,
35
+ PROXY_ID_PATH,
33
36
  SESSIONS_PATH,
34
37
  SOCK_PATH,
35
38
  STOPPED_PATH,
@@ -37,11 +40,13 @@ import {
37
40
  buildMessage,
38
41
  createIpcReader,
39
42
  createWorkerReader,
43
+ defaultHookPortForProfile,
44
+ ensureProfileWorkspace,
40
45
  serializeIpc,
41
46
  serializeWorkerMsg,
42
47
  sessionPaths,
43
48
  tildify
44
- } from "./chunk-QWPI6YON.js";
49
+ } from "./chunk-DFLQ3TFT.js";
45
50
 
46
51
  // src/serve.ts
47
52
  import { createServer as createServer2 } from "net";
@@ -620,31 +625,20 @@ function parsePort(value, source) {
620
625
  }
621
626
  return port;
622
627
  }
623
- function resolveFileConfig(fromFile, requestedEnv) {
624
- if (!fromFile.envs) {
625
- return {
626
- envName: void 0,
627
- envNameSource: fromFile.relayUrl || fromFile.relayToken || fromFile.hookPort ? "single" : "none",
628
- config: fromFile
629
- };
630
- }
631
- const envName = requestedEnv ?? fromFile.defaultEnv ?? "local";
632
- const config = fromFile.envs[envName];
633
- if (!config) {
634
- const available = Object.keys(fromFile.envs).sort();
635
- throw new Error(
636
- `Unknown config env "${envName}". Available envs: ${available.length > 0 ? available.join(", ") : "(none)"}`
637
- );
628
+ function isRecord(value) {
629
+ return typeof value === "object" && value !== null && !Array.isArray(value);
630
+ }
631
+ function validateConfigShape(value) {
632
+ if (!isRecord(value) || !isRecord(value.profiles) || !isRecord(value.relays)) {
633
+ throw new Error(`Invalid config shape in ${CONFIG_PATH}: expected "profiles" and "relays".`);
638
634
  }
639
- return {
640
- envName,
641
- envNameSource: requestedEnv ? "cli" : fromFile.defaultEnv ? "file" : "default",
642
- config
643
- };
635
+ return value;
644
636
  }
645
637
  function readConfigFile() {
646
- if (!existsSync3(CONFIG_PATH)) return {};
647
- return JSON.parse(readFileSync3(CONFIG_PATH, "utf-8"));
638
+ if (!existsSync3(CONFIG_PATH)) {
639
+ throw new Error(`Dev Anywhere config not found at ${CONFIG_PATH}. Run "dev-anywhere init".`);
640
+ }
641
+ return validateConfigShape(JSON.parse(readFileSync3(CONFIG_PATH, "utf-8")));
648
642
  }
649
643
  function agentCliField(provider) {
650
644
  return provider === "claude" ? "claudeBin" : "codexBin";
@@ -669,68 +663,71 @@ function uniqueAbsolutePaths(paths) {
669
663
  }
670
664
  return result;
671
665
  }
672
- function updateAgentCliPathInEnvConfig(config, provider, path) {
673
- const field = agentCliField(provider);
674
- const historyField = agentCliHistoryField(provider);
675
- const history = uniqueAbsolutePaths([path, ...config[historyField] ?? []]).slice(0, 8);
666
+ function resolveRelayConfig(fromFile, requestedRelayName) {
667
+ const profile = fromFile.profiles[PROFILE_NAME];
668
+ if (!profile) {
669
+ const available = Object.keys(fromFile.profiles).sort();
670
+ throw new Error(
671
+ `Unknown profile "${PROFILE_NAME}". Available profiles: ${available.length > 0 ? available.join(", ") : "(none)"}`
672
+ );
673
+ }
674
+ const relayName = requestedRelayName?.trim() || profile.relay?.trim();
675
+ if (!relayName) {
676
+ throw new Error(`Profile "${PROFILE_NAME}" must specify a relay.`);
677
+ }
678
+ const relay = fromFile.relays[relayName];
679
+ if (!relay) {
680
+ const available = Object.keys(fromFile.relays).sort();
681
+ throw new Error(
682
+ `Unknown relay "${relayName}". Available relays: ${available.length > 0 ? available.join(", ") : "(none)"}`
683
+ );
684
+ }
676
685
  return {
677
- ...config,
678
- [field]: path,
679
- [historyField]: history
686
+ relayName,
687
+ relayNameSource: requestedRelayName?.trim() ? "cli" : "profile",
688
+ relay
680
689
  };
681
690
  }
682
691
  function loadConfig(options) {
683
- let fromFile = {};
684
- if (existsSync3(CONFIG_PATH)) {
685
- try {
686
- fromFile = readConfigFile();
687
- } catch (err) {
688
- serviceLogger.warn(
689
- { path: CONFIG_PATH, err: err instanceof Error ? err.message : String(err) },
690
- "Failed to parse config file, falling back to env-only"
691
- );
692
- }
693
- } else {
694
- serviceLogger.debug({ path: CONFIG_PATH }, "Config file not found, using env-only");
695
- }
696
- const resolved = resolveFileConfig(fromFile, options?.envName);
697
- const hookPortFromFile = resolved.config.hookPort ?? fromFile.hookPort;
698
- const claudeBinFromFile = resolved.config.claudeBin ?? fromFile.claudeBin;
699
- const codexBinFromFile = resolved.config.codexBin ?? fromFile.codexBin;
700
- const claudeBinHistory = [
701
- ...resolved.config.claudeBinHistory ?? [],
702
- ...fromFile.claudeBinHistory ?? []
703
- ];
704
- const codexBinHistory = [
705
- ...resolved.config.codexBinHistory ?? [],
706
- ...fromFile.codexBinHistory ?? []
707
- ];
708
- const claudeBin = process.env.CLAUDE_BIN ?? claudeBinFromFile;
709
- const codexBin = process.env.CODEX_BIN ?? codexBinFromFile;
692
+ const fromFile = readConfigFile();
693
+ const agentCli = fromFile.agentCli ?? {};
694
+ const resolved = resolveRelayConfig(fromFile, options?.relayName);
695
+ const claudeBin = process.env.CLAUDE_BIN ?? agentCli.claudeBin;
696
+ const codexBin = process.env.CODEX_BIN ?? agentCli.codexBin;
710
697
  const config = {
711
- envName: resolved.envName,
712
- relayUrl: process.env.RELAY_URL ?? resolved.config.relayUrl,
713
- relayToken: process.env.RELAY_PROXY_TOKEN ?? resolved.config.relayToken,
714
- hookPort: parsePort(process.env.DEV_ANYWHERE_HOOK_PORT, "DEV_ANYWHERE_HOOK_PORT") ?? hookPortFromFile ?? 17654,
698
+ profileName: PROFILE_NAME,
699
+ relayName: resolved.relayName,
700
+ relayUrl: process.env.RELAY_URL ?? resolved.relay.url,
701
+ relayToken: process.env.RELAY_PROXY_TOKEN ?? resolved.relay.proxyToken,
702
+ hookPort: parsePort(process.env.DEV_ANYWHERE_HOOK_PORT, "DEV_ANYWHERE_HOOK_PORT") ?? defaultHookPortForProfile(PROFILE_NAME),
715
703
  claudeBin,
716
704
  codexBin,
717
705
  agentCliSuggestions: {
718
- claude: uniqueAbsolutePaths([process.env.CLAUDE_BIN, claudeBinFromFile, ...claudeBinHistory]),
719
- codex: uniqueAbsolutePaths([process.env.CODEX_BIN, codexBinFromFile, ...codexBinHistory])
706
+ claude: uniqueAbsolutePaths([
707
+ process.env.CLAUDE_BIN,
708
+ agentCli.claudeBin,
709
+ ...agentCli.claudeBinHistory ?? []
710
+ ]),
711
+ codex: uniqueAbsolutePaths([
712
+ process.env.CODEX_BIN,
713
+ agentCli.codexBin,
714
+ ...agentCli.codexBinHistory ?? []
715
+ ])
720
716
  },
721
717
  sources: {
722
- envName: resolved.envNameSource,
723
- relayUrl: process.env.RELAY_URL ? "env" : resolved.config.relayUrl ? "file" : "none",
724
- relayToken: process.env.RELAY_PROXY_TOKEN ? "env" : resolved.config.relayToken ? "file" : "none",
725
- hookPort: process.env.DEV_ANYWHERE_HOOK_PORT ? "env" : hookPortFromFile ? "file" : "default",
726
- claudeBin: process.env.CLAUDE_BIN ? "env" : claudeBinFromFile ? "file" : "none",
727
- codexBin: process.env.CODEX_BIN ? "env" : codexBinFromFile ? "file" : "none"
718
+ relayName: resolved.relayNameSource,
719
+ relayUrl: process.env.RELAY_URL ? "env" : resolved.relay.url ? "file" : "none",
720
+ relayToken: process.env.RELAY_PROXY_TOKEN ? "env" : resolved.relay.proxyToken ? "file" : "none",
721
+ hookPort: process.env.DEV_ANYWHERE_HOOK_PORT ? "env" : "default",
722
+ claudeBin: process.env.CLAUDE_BIN ? "env" : agentCli.claudeBin ? "file" : "none",
723
+ codexBin: process.env.CODEX_BIN ? "env" : agentCli.codexBin ? "file" : "none"
728
724
  }
729
725
  };
730
726
  serviceLogger.info(
731
727
  {
732
- envName: config.envName ?? "(single)",
733
- envNameSource: config.sources.envName,
728
+ profile: config.profileName,
729
+ relayName: config.relayName,
730
+ relayNameSource: config.sources.relayName,
734
731
  relayUrl: config.relayUrl ?? "(unset)",
735
732
  relayUrlSource: config.sources.relayUrl,
736
733
  relayTokenSource: config.sources.relayToken,
@@ -750,20 +747,20 @@ function buildProviderEnv(config, baseEnv = process.env) {
750
747
  ...config.codexBin ? { CODEX_BIN: config.codexBin } : {}
751
748
  };
752
749
  }
753
- function saveAgentCliPath(provider, path, options) {
750
+ function updateAgentCliConfig(config, provider, path) {
751
+ const field = agentCliField(provider);
752
+ const historyField = agentCliHistoryField(provider);
753
+ const history = uniqueAbsolutePaths([path, ...config[historyField] ?? []]).slice(0, 8);
754
+ return {
755
+ ...config,
756
+ [field]: path,
757
+ [historyField]: history
758
+ };
759
+ }
760
+ function saveAgentCliPath(provider, path) {
754
761
  const normalized = validateAgentCliPath(path);
755
762
  const fromFile = readConfigFile();
756
- const resolved = resolveFileConfig(fromFile, options?.envName);
757
- if (fromFile.envs) {
758
- const envName = resolved.envName ?? options?.envName ?? fromFile.defaultEnv ?? "local";
759
- fromFile.envs[envName] = updateAgentCliPathInEnvConfig(
760
- fromFile.envs[envName] ?? {},
761
- provider,
762
- normalized
763
- );
764
- } else {
765
- Object.assign(fromFile, updateAgentCliPathInEnvConfig(fromFile, provider, normalized));
766
- }
763
+ fromFile.agentCli = updateAgentCliConfig(fromFile.agentCli ?? {}, provider, normalized);
767
764
  mkdirSync3(dirname3(CONFIG_PATH), { recursive: true });
768
765
  writeFileSync3(CONFIG_PATH, `${JSON.stringify(fromFile, null, 2)}
769
766
  `, "utf-8");
@@ -2491,7 +2488,7 @@ var RelayResourceHandlers = class {
2491
2488
  }
2492
2489
  try {
2493
2490
  const path = validateExecutablePath(rawPath ?? "");
2494
- saveAgentCliPath(provider, path, { envName: this.deps.envName });
2491
+ saveAgentCliPath(provider, path);
2495
2492
  this.deps.setAgentCliPath(provider, path);
2496
2493
  const agentCli = detectAgentCliStatus(this.deps.getProviderEnv(), {
2497
2494
  suggestions: this.deps.getAgentCliSuggestions()
@@ -3110,7 +3107,6 @@ var RelayRouter = class {
3110
3107
  relaySend: deps.relaySend,
3111
3108
  controlHandlers: deps.controlHandlers,
3112
3109
  sessionManager: deps.sessionManager,
3113
- envName: deps.envName,
3114
3110
  getProviderEnv: deps.getProviderEnv,
3115
3111
  getAgentCliSuggestions: deps.getAgentCliSuggestions,
3116
3112
  setAgentCliPath: deps.setAgentCliPath
@@ -3696,8 +3692,13 @@ async function cleanupStaleResources() {
3696
3692
  serviceLogger.info("Removed stale PID file");
3697
3693
  }
3698
3694
  }
3695
+ function formatProxyNameForProfile(baseName, profileName = PROFILE_NAME) {
3696
+ return profileName === DEFAULT_PROXY_PROFILE ? baseName : `${baseName} (${profileName})`;
3697
+ }
3699
3698
  function getProxyName() {
3700
- return process.env.DEV_ANYWHERE_PROXY_NAME || getComputerName() || hostname();
3699
+ const explicitName = process.env.DEV_ANYWHERE_PROXY_NAME?.trim();
3700
+ if (explicitName) return explicitName;
3701
+ return formatProxyNameForProfile(getComputerName() || hostname());
3701
3702
  }
3702
3703
  function getComputerName() {
3703
3704
  try {
@@ -4434,24 +4435,25 @@ function parseServiceOptions(argv) {
4434
4435
  const options = {};
4435
4436
  for (let i = 0; i < argv.length; i++) {
4436
4437
  const arg = argv[i];
4437
- if (arg === "--env") {
4438
- const envName = argv[i + 1];
4439
- if (!envName || envName.startsWith("-")) {
4440
- throw new Error("Missing value for --env");
4438
+ if (arg === "--relay") {
4439
+ const relayName = argv[i + 1];
4440
+ if (!relayName || relayName.startsWith("-")) {
4441
+ throw new Error("Missing value for --relay");
4441
4442
  }
4442
- options.envName = envName;
4443
+ options.relayName = relayName;
4443
4444
  i++;
4444
4445
  continue;
4445
4446
  }
4446
- if (arg.startsWith("--env=")) {
4447
- const envName = arg.slice("--env=".length);
4448
- if (!envName) throw new Error("Missing value for --env");
4449
- options.envName = envName;
4447
+ if (arg.startsWith("--relay=")) {
4448
+ const relayName = arg.slice("--relay=".length);
4449
+ if (!relayName) throw new Error("Missing value for --relay");
4450
+ options.relayName = relayName;
4450
4451
  }
4451
4452
  }
4452
4453
  return options;
4453
4454
  }
4454
4455
  async function startService(options) {
4456
+ ensureProfileWorkspace();
4455
4457
  await cleanupStaleResources();
4456
4458
  try {
4457
4459
  unlinkSync3(STOPPED_PATH);
@@ -4479,7 +4481,7 @@ async function startService(options) {
4479
4481
  sessionManager.startReaper();
4480
4482
  const terminalSockets = /* @__PURE__ */ new Map();
4481
4483
  const proxyName = getProxyName();
4482
- let proxyConfig = loadConfig({ envName: options?.envName });
4484
+ let proxyConfig = loadConfig({ relayName: options?.relayName });
4483
4485
  const getProviderEnv = () => buildProviderEnv(proxyConfig, process.env);
4484
4486
  const getAgentCliSuggestions = () => proxyConfig.agentCliSuggestions;
4485
4487
  const setAgentCliPath = (provider, path) => {
@@ -4501,8 +4503,9 @@ async function startService(options) {
4501
4503
  const relayUrl = options?.relayUrl ?? proxyConfig.relayUrl;
4502
4504
  const relayToken = proxyConfig.relayToken;
4503
4505
  const statusConfig = {
4504
- envName: proxyConfig.envName,
4505
- envNameSource: proxyConfig.sources.envName,
4506
+ profile: PROFILE_NAME,
4507
+ relayName: proxyConfig.relayName,
4508
+ relayNameSource: proxyConfig.sources.relayName,
4506
4509
  relayUrl,
4507
4510
  relayUrlSource: proxyConfig.sources.relayUrl,
4508
4511
  relayTokenSource: proxyConfig.sources.relayToken,
@@ -4510,12 +4513,16 @@ async function startService(options) {
4510
4513
  hookPortSource: proxyConfig.sources.hookPort
4511
4514
  };
4512
4515
  if (!relayUrl) {
4513
- const msg = 'Relay URL is required. Set it via RELAY_URL or ~/.dev-anywhere/config.json {"defaultEnv":"local","envs":{"local":{"relayUrl":"ws://..."}}}.';
4516
+ const msg = `Relay URL is required. Set relays.${proxyConfig.relayName}.url in ~/.dev-anywhere/config.json or pass --relay <name>.`;
4514
4517
  serviceLogger.error(msg);
4515
4518
  console.error(msg);
4516
4519
  process.exit(1);
4517
4520
  }
4518
- const relayConnection = new RelayConnection(relayUrl, { name: proxyName, token: relayToken });
4521
+ const relayConnection = new RelayConnection(relayUrl, {
4522
+ name: proxyName,
4523
+ token: relayToken,
4524
+ proxyIdPath: PROXY_ID_PATH
4525
+ });
4519
4526
  const relaySend = (data) => relayConnection.sendRaw(data);
4520
4527
  const controlHandlers = createControlMessageHandlers(relaySend, sessionManager);
4521
4528
  const observerChangeState = (sessionId, next) => changeSessionState(sessionManager, relayConnection, sessionId, next);
@@ -4577,7 +4584,8 @@ async function startService(options) {
4577
4584
  relayConnection.connect();
4578
4585
  serviceLogger.info(
4579
4586
  {
4580
- envName: proxyConfig.envName ?? "(single)",
4587
+ relayName: proxyConfig.relayName,
4588
+ profile: PROFILE_NAME,
4581
4589
  relayUrl,
4582
4590
  proxyName,
4583
4591
  tokenSet: !!relayToken,
@@ -4601,7 +4609,6 @@ async function startService(options) {
4601
4609
  permissionBroker,
4602
4610
  hookEventRouter: hookRuntime.hookEventRouter,
4603
4611
  agentStatusRegistry,
4604
- envName: proxyConfig.envName,
4605
4612
  getProviderEnv,
4606
4613
  getAgentCliSuggestions,
4607
4614
  setAgentCliPath