@dev-anywhere/proxy 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ CONFIG_PATH,
4
+ LOG_DIR,
5
+ createLogger
6
+ } from "./chunk-QFYI6AMN.js";
7
+
8
+ // src/common/logger.ts
9
+ import { existsSync, readFileSync } from "fs";
10
+
11
+ // src/common/runtime-env.ts
12
+ var VALID_LOG_LEVELS = [
13
+ "trace",
14
+ "debug",
15
+ "info",
16
+ "warn",
17
+ "error",
18
+ "fatal",
19
+ "silent"
20
+ ];
21
+ function parseLogLevel(value) {
22
+ if (!value) return void 0;
23
+ if (VALID_LOG_LEVELS.includes(value)) return value;
24
+ throw new Error(
25
+ `Invalid LOG_LEVEL=${JSON.stringify(value)}; expected one of ${VALID_LOG_LEVELS.join(", ")}`
26
+ );
27
+ }
28
+ function parsePort(value, source) {
29
+ if (!value) return void 0;
30
+ const port = Number(value);
31
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
32
+ throw new Error(`Invalid ${source}=${JSON.stringify(value)}: expected TCP port 1-65535`);
33
+ }
34
+ return port;
35
+ }
36
+ function nonEmpty(value) {
37
+ return value && value.length > 0 ? value : void 0;
38
+ }
39
+ function loadProxyRuntimeEnv(env2 = process.env) {
40
+ return {
41
+ relayUrl: nonEmpty(env2.RELAY_URL),
42
+ relayProxyToken: nonEmpty(env2.RELAY_PROXY_TOKEN),
43
+ hookPort: parsePort(env2.DEV_ANYWHERE_HOOK_PORT, "DEV_ANYWHERE_HOOK_PORT"),
44
+ claudeBin: nonEmpty(env2.CLAUDE_BIN),
45
+ codexBin: nonEmpty(env2.CODEX_BIN),
46
+ logLevel: parseLogLevel(env2.LOG_LEVEL),
47
+ isVitest: !!env2.VITEST
48
+ };
49
+ }
50
+
51
+ // src/common/logger.ts
52
+ var env = loadProxyRuntimeEnv();
53
+ function readConfigLogLevel() {
54
+ if (env.logLevel) return void 0;
55
+ if (!existsSync(CONFIG_PATH)) return void 0;
56
+ try {
57
+ const raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
58
+ if (typeof raw.logLevel !== "string") return void 0;
59
+ return VALID_LOG_LEVELS.includes(raw.logLevel) ? raw.logLevel : void 0;
60
+ } catch {
61
+ return void 0;
62
+ }
63
+ }
64
+ var overrideLevel = env.logLevel ?? readConfigLogLevel();
65
+ var serviceLogger = createLogger({
66
+ name: "service",
67
+ level: overrideLevel ?? "info",
68
+ logDir: LOG_DIR,
69
+ silent: env.isVitest
70
+ });
71
+ var terminalLogger = createLogger({
72
+ name: "terminal",
73
+ level: overrideLevel ?? "debug",
74
+ logDir: LOG_DIR,
75
+ silent: env.isVitest
76
+ });
77
+
78
+ export {
79
+ VALID_LOG_LEVELS,
80
+ loadProxyRuntimeEnv,
81
+ serviceLogger,
82
+ terminalLogger
83
+ };
84
+ //# sourceMappingURL=chunk-2JUB4LDU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/common/logger.ts","../src/common/runtime-env.ts"],"sourcesContent":["import { existsSync, readFileSync } from \"node:fs\";\nimport { createLogger } from \"@dev-anywhere/shared\";\nimport { CONFIG_PATH, LOG_DIR } from \"./paths.js\";\nimport { loadProxyRuntimeEnv, VALID_LOG_LEVELS } from \"./runtime-env.js\";\n\nconst env = loadProxyRuntimeEnv();\n\n// 直接读 config.json 而不走 config.ts loadConfig:loadConfig 依赖 logger.ts,\n// 反向引用会形成循环。这里只取 logLevel 字段做 best-effort 降级,全量 schema 校验仍由\n// config.ts 在第一次 loadConfig 时执行。\nfunction readConfigLogLevel(): string | undefined {\n if (env.logLevel) return undefined;\n if (!existsSync(CONFIG_PATH)) return undefined;\n try {\n const raw = JSON.parse(readFileSync(CONFIG_PATH, \"utf-8\")) as { logLevel?: unknown };\n if (typeof raw.logLevel !== \"string\") return undefined;\n return (VALID_LOG_LEVELS as readonly string[]).includes(raw.logLevel)\n ? raw.logLevel\n : undefined;\n } catch {\n return undefined;\n }\n}\n\n// 三级 precedence:LOG_LEVEL env > config.logLevel > 各 logger 自己的默认。\nconst overrideLevel = env.logLevel ?? readConfigLogLevel();\n\nexport const serviceLogger = createLogger({\n name: \"service\",\n level: overrideLevel ?? \"info\",\n logDir: LOG_DIR,\n silent: env.isVitest,\n});\n\nexport const terminalLogger = createLogger({\n name: \"terminal\",\n level: overrideLevel ?? \"debug\",\n logDir: LOG_DIR,\n silent: env.isVitest,\n});\n","// 单一入口集中读取 proxy 运行时关心的环境变量,类型化输出,避免 process.env.X\n// 散落在多个文件里各自做 parseInt / 空串判断 / undefined 兜底。\n//\n// 范围:用户面对的运行时旋钮 + 构建/测试模式标志。\n// 不包含:proxy → provider/hook 子进程之间的 plumbing(DEV_ANYWHERE_HOOK_TOKEN /\n// HOOK_URL / HOOK_MARKER / HOOK_EVENT / SESSION_ID 等),这些是内部传参,不是用户旋钮,\n// 留在各自消费点。\n\nexport const VALID_LOG_LEVELS = [\n \"trace\",\n \"debug\",\n \"info\",\n \"warn\",\n \"error\",\n \"fatal\",\n \"silent\",\n] as const;\nexport type LogLevel = (typeof VALID_LOG_LEVELS)[number];\n\nexport interface ProxyRuntimeEnv {\n // RELAY_URL —— 覆盖 config.relays[name].url;用于一次性指向另一个 relay。\n relayUrl: string | undefined;\n // RELAY_PROXY_TOKEN —— 覆盖 config.relays[name].proxyToken。\n relayProxyToken: string | undefined;\n // DEV_ANYWHERE_HOOK_PORT —— 覆盖按 profile 推导的 hook server 端口。\n hookPort: number | undefined;\n // CLAUDE_BIN / CODEX_BIN —— 覆盖 config.agentCli 里的 CLI 可执行文件路径。\n claudeBin: string | undefined;\n codexBin: string | undefined;\n // LOG_LEVEL —— 用户最高优先级;config.logLevel 是次优先;都缺则各 logger 自己 default。\n logLevel: LogLevel | undefined;\n // VITEST —— 测试运行器存在则把 logger 静默,避免污染 vitest 输出。\n isVitest: boolean;\n // NODE_ENV 故意不在这里:env.ts 把它读成 top-level const 让 tsup 静态折叠 + dead-code\n // elimination dev 分支。走函数调用会破坏这个 build-time 优化。\n}\n\nfunction parseLogLevel(value: string | undefined): LogLevel | undefined {\n if (!value) return undefined;\n if ((VALID_LOG_LEVELS as readonly string[]).includes(value)) return value as LogLevel;\n throw new Error(\n `Invalid LOG_LEVEL=${JSON.stringify(value)}; expected one of ${VALID_LOG_LEVELS.join(\", \")}`,\n );\n}\n\nfunction parsePort(value: string | undefined, source: string): number | undefined {\n if (!value) return undefined;\n const port = Number(value);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(`Invalid ${source}=${JSON.stringify(value)}: expected TCP port 1-65535`);\n }\n return port;\n}\n\nfunction nonEmpty(value: string | undefined): string | undefined {\n return value && value.length > 0 ? value : undefined;\n}\n\nexport function loadProxyRuntimeEnv(env: NodeJS.ProcessEnv = process.env): ProxyRuntimeEnv {\n return {\n relayUrl: nonEmpty(env.RELAY_URL),\n relayProxyToken: nonEmpty(env.RELAY_PROXY_TOKEN),\n hookPort: parsePort(env.DEV_ANYWHERE_HOOK_PORT, \"DEV_ANYWHERE_HOOK_PORT\"),\n claudeBin: nonEmpty(env.CLAUDE_BIN),\n codexBin: nonEmpty(env.CODEX_BIN),\n logLevel: parseLogLevel(env.LOG_LEVEL),\n isVitest: !!env.VITEST,\n };\n}\n"],"mappings":";;;;;;;;AAAA,SAAS,YAAY,oBAAoB;;;ACQlC,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAqBA,SAAS,cAAc,OAAiD;AACtE,MAAI,CAAC,MAAO,QAAO;AACnB,MAAK,iBAAuC,SAAS,KAAK,EAAG,QAAO;AACpE,QAAM,IAAI;AAAA,IACR,qBAAqB,KAAK,UAAU,KAAK,CAAC,qBAAqB,iBAAiB,KAAK,IAAI,CAAC;AAAA,EAC5F;AACF;AAEA,SAAS,UAAU,OAA2B,QAAoC;AAChF,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,OAAO,OAAO,KAAK;AACzB,MAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,OAAO;AACvD,UAAM,IAAI,MAAM,WAAW,MAAM,IAAI,KAAK,UAAU,KAAK,CAAC,6BAA6B;AAAA,EACzF;AACA,SAAO;AACT;AAEA,SAAS,SAAS,OAA+C;AAC/D,SAAO,SAAS,MAAM,SAAS,IAAI,QAAQ;AAC7C;AAEO,SAAS,oBAAoBA,OAAyB,QAAQ,KAAsB;AACzF,SAAO;AAAA,IACL,UAAU,SAASA,KAAI,SAAS;AAAA,IAChC,iBAAiB,SAASA,KAAI,iBAAiB;AAAA,IAC/C,UAAU,UAAUA,KAAI,wBAAwB,wBAAwB;AAAA,IACxE,WAAW,SAASA,KAAI,UAAU;AAAA,IAClC,UAAU,SAASA,KAAI,SAAS;AAAA,IAChC,UAAU,cAAcA,KAAI,SAAS;AAAA,IACrC,UAAU,CAAC,CAACA,KAAI;AAAA,EAClB;AACF;;;AD/DA,IAAM,MAAM,oBAAoB;AAKhC,SAAS,qBAAyC;AAChD,MAAI,IAAI,SAAU,QAAO;AACzB,MAAI,CAAC,WAAW,WAAW,EAAG,QAAO;AACrC,MAAI;AACF,UAAM,MAAM,KAAK,MAAM,aAAa,aAAa,OAAO,CAAC;AACzD,QAAI,OAAO,IAAI,aAAa,SAAU,QAAO;AAC7C,WAAQ,iBAAuC,SAAS,IAAI,QAAQ,IAChE,IAAI,WACJ;AAAA,EACN,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,IAAM,gBAAgB,IAAI,YAAY,mBAAmB;AAElD,IAAM,gBAAgB,aAAa;AAAA,EACxC,MAAM;AAAA,EACN,OAAO,iBAAiB;AAAA,EACxB,QAAQ;AAAA,EACR,QAAQ,IAAI;AACd,CAAC;AAEM,IAAM,iBAAiB,aAAa;AAAA,EACzC,MAAM;AAAA,EACN,OAAO,iBAAiB;AAAA,EACxB,QAAQ;AAAA,EACR,QAAQ,IAAI;AACd,CAAC;","names":["env"]}
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  sessionPaths
4
- } from "./chunk-RFBTVZ2X.js";
4
+ } from "./chunk-QFYI6AMN.js";
5
5
 
6
6
  // src/common/seq-counter.ts
7
7
  import { mkdirSync, readFileSync, writeFileSync, existsSync } from "fs";
@@ -152,4 +152,4 @@ export {
152
152
  IGNORED_EVENT_TYPES,
153
153
  SeqCounter
154
154
  };
155
- //# sourceMappingURL=chunk-ODK6N2NP.js.map
155
+ //# sourceMappingURL=chunk-7XMJMVIL.js.map
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  DESIRED_RELAY_PATH
4
- } from "./chunk-RFBTVZ2X.js";
4
+ } from "./chunk-QFYI6AMN.js";
5
5
 
6
6
  // src/common/daemon-env.ts
7
7
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
@@ -33,4 +33,4 @@ export {
33
33
  setDesiredDaemonRelay,
34
34
  daemonRelayArgs
35
35
  };
36
- //# sourceMappingURL=chunk-TX6HNHDB.js.map
36
+ //# sourceMappingURL=chunk-BMVYMCKF.js.map
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ VALID_LOG_LEVELS,
4
+ loadProxyRuntimeEnv,
5
+ serviceLogger
6
+ } from "./chunk-2JUB4LDU.js";
7
+ import {
8
+ CONFIG_PATH,
9
+ PROFILE_NAME,
10
+ defaultHookPortForProfile
11
+ } from "./chunk-QFYI6AMN.js";
12
+
13
+ // src/common/config.ts
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
15
+ import { dirname, isAbsolute } from "path";
16
+ import { z } from "zod";
17
+ var LogLevelSchema = z.enum(VALID_LOG_LEVELS);
18
+ var RelayTargetSchema = z.object({
19
+ url: z.string().optional(),
20
+ proxyToken: z.string().optional()
21
+ }).strict();
22
+ var ProxyProfileSchema = z.object({
23
+ relay: z.string().optional()
24
+ }).strict();
25
+ var AgentCliSchema = z.object({
26
+ claudeBin: z.string().optional(),
27
+ codexBin: z.string().optional(),
28
+ claudeBinHistory: z.array(z.string()).optional(),
29
+ codexBinHistory: z.array(z.string()).optional()
30
+ }).strict();
31
+ var ProxyConfigFileSchema = z.object({
32
+ defaultProfile: z.string().optional(),
33
+ profiles: z.record(z.string(), ProxyProfileSchema),
34
+ relays: z.record(z.string(), RelayTargetSchema),
35
+ agentCli: AgentCliSchema.optional(),
36
+ previewRoots: z.array(z.string()).optional(),
37
+ logLevel: LogLevelSchema.optional()
38
+ }).strict();
39
+ function readConfigFile() {
40
+ if (!existsSync(CONFIG_PATH)) {
41
+ throw new Error(`Dev Anywhere config not found at ${CONFIG_PATH}. Run "dev-anywhere init".`);
42
+ }
43
+ let raw;
44
+ try {
45
+ raw = JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
46
+ } catch (err) {
47
+ throw new Error(
48
+ `${CONFIG_PATH} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
49
+ { cause: err }
50
+ );
51
+ }
52
+ const parsed = ProxyConfigFileSchema.safeParse(raw);
53
+ if (!parsed.success) {
54
+ const issues = parsed.error.issues.map(
55
+ (issue) => ` ${issue.path.length > 0 ? issue.path.join(".") : "(root)"}: ${issue.message}`
56
+ ).join("\n");
57
+ throw new Error(`Invalid config at ${CONFIG_PATH}:
58
+ ${issues}`);
59
+ }
60
+ return parsed.data;
61
+ }
62
+ function agentCliField(provider) {
63
+ return provider === "claude" ? "claudeBin" : "codexBin";
64
+ }
65
+ function agentCliHistoryField(provider) {
66
+ return provider === "claude" ? "claudeBinHistory" : "codexBinHistory";
67
+ }
68
+ function validateAgentCliPath(path) {
69
+ const normalized = path.trim();
70
+ if (!normalized) throw new Error("\u8BF7\u8F93\u5165 CLI \u8DEF\u5F84");
71
+ if (!isAbsolute(normalized)) throw new Error("CLI \u8DEF\u5F84\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
72
+ return normalized;
73
+ }
74
+ function uniqueAbsolutePaths(paths) {
75
+ const seen = /* @__PURE__ */ new Set();
76
+ const result = [];
77
+ for (const path of paths) {
78
+ const normalized = path?.trim();
79
+ if (!normalized || !isAbsolute(normalized) || seen.has(normalized)) continue;
80
+ seen.add(normalized);
81
+ result.push(normalized);
82
+ }
83
+ return result;
84
+ }
85
+ function resolveRelayConfig(fromFile, requestedRelayName) {
86
+ const profile = fromFile.profiles[PROFILE_NAME];
87
+ if (!profile) {
88
+ const available = Object.keys(fromFile.profiles).sort();
89
+ throw new Error(
90
+ `Unknown profile "${PROFILE_NAME}". Available profiles: ${available.length > 0 ? available.join(", ") : "(none)"}`
91
+ );
92
+ }
93
+ const relayName = requestedRelayName?.trim() || profile.relay?.trim();
94
+ if (!relayName) {
95
+ throw new Error(`Profile "${PROFILE_NAME}" must specify a relay.`);
96
+ }
97
+ const relay = fromFile.relays[relayName];
98
+ if (!relay) {
99
+ const available = Object.keys(fromFile.relays).sort();
100
+ throw new Error(
101
+ `Unknown relay "${relayName}". Available relays: ${available.length > 0 ? available.join(", ") : "(none)"}`
102
+ );
103
+ }
104
+ return {
105
+ relayName,
106
+ relayNameSource: requestedRelayName?.trim() ? "cli" : "profile",
107
+ relay
108
+ };
109
+ }
110
+ function loadConfig(options) {
111
+ const env = loadProxyRuntimeEnv();
112
+ const fromFile = readConfigFile();
113
+ const agentCli = fromFile.agentCli ?? {};
114
+ const resolved = resolveRelayConfig(fromFile, options?.relayName);
115
+ const claudeBin = env.claudeBin ?? agentCli.claudeBin;
116
+ const codexBin = env.codexBin ?? agentCli.codexBin;
117
+ const config = {
118
+ profileName: PROFILE_NAME,
119
+ relayName: resolved.relayName,
120
+ relayUrl: env.relayUrl ?? resolved.relay.url,
121
+ relayToken: env.relayProxyToken ?? resolved.relay.proxyToken,
122
+ hookPort: env.hookPort ?? defaultHookPortForProfile(PROFILE_NAME),
123
+ claudeBin,
124
+ codexBin,
125
+ previewRoots: uniqueAbsolutePaths(fromFile.previewRoots ?? []),
126
+ agentCliSuggestions: {
127
+ claude: uniqueAbsolutePaths([
128
+ env.claudeBin,
129
+ agentCli.claudeBin,
130
+ ...agentCli.claudeBinHistory ?? []
131
+ ]),
132
+ codex: uniqueAbsolutePaths([
133
+ env.codexBin,
134
+ agentCli.codexBin,
135
+ ...agentCli.codexBinHistory ?? []
136
+ ])
137
+ },
138
+ sources: {
139
+ relayName: resolved.relayNameSource,
140
+ relayUrl: env.relayUrl ? "env" : resolved.relay.url ? "file" : "none",
141
+ relayToken: env.relayProxyToken ? "env" : resolved.relay.proxyToken ? "file" : "none",
142
+ hookPort: env.hookPort !== void 0 ? "env" : "default",
143
+ claudeBin: env.claudeBin ? "env" : agentCli.claudeBin ? "file" : "none",
144
+ codexBin: env.codexBin ? "env" : agentCli.codexBin ? "file" : "none"
145
+ }
146
+ };
147
+ serviceLogger.info(
148
+ {
149
+ profile: config.profileName,
150
+ relayName: config.relayName,
151
+ relayNameSource: config.sources.relayName,
152
+ relayUrl: config.relayUrl ?? "(unset)",
153
+ relayUrlSource: config.sources.relayUrl,
154
+ relayTokenSource: config.sources.relayToken,
155
+ hookPort: config.hookPort,
156
+ hookPortSource: config.sources.hookPort,
157
+ claudeBinSource: config.sources.claudeBin,
158
+ codexBinSource: config.sources.codexBin
159
+ },
160
+ "Config loaded"
161
+ );
162
+ return config;
163
+ }
164
+ function buildProviderEnv(config, baseEnv = process.env) {
165
+ return {
166
+ ...baseEnv,
167
+ ...config.claudeBin ? { CLAUDE_BIN: config.claudeBin } : {},
168
+ ...config.codexBin ? { CODEX_BIN: config.codexBin } : {}
169
+ };
170
+ }
171
+ function updateAgentCliConfig(config, provider, path) {
172
+ const field = agentCliField(provider);
173
+ const historyField = agentCliHistoryField(provider);
174
+ const history = uniqueAbsolutePaths([path, ...config[historyField] ?? []]).slice(0, 8);
175
+ return {
176
+ ...config,
177
+ [field]: path,
178
+ [historyField]: history
179
+ };
180
+ }
181
+ function saveAgentCliPath(provider, path) {
182
+ const normalized = validateAgentCliPath(path);
183
+ const fromFile = readConfigFile();
184
+ fromFile.agentCli = updateAgentCliConfig(fromFile.agentCli ?? {}, provider, normalized);
185
+ mkdirSync(dirname(CONFIG_PATH), { recursive: true });
186
+ writeFileSync(CONFIG_PATH, `${JSON.stringify(fromFile, null, 2)}
187
+ `, "utf-8");
188
+ }
189
+
190
+ export {
191
+ loadConfig,
192
+ buildProviderEnv,
193
+ saveAgentCliPath
194
+ };
195
+ //# sourceMappingURL=chunk-DCDXAM76.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/common/config.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname, isAbsolute } from \"node:path\";\nimport { z } from \"zod\";\nimport { CONFIG_PATH, PROFILE_NAME, defaultHookPortForProfile } from \"./paths.js\";\nimport { serviceLogger } from \"./logger.js\";\nimport { VALID_LOG_LEVELS, loadProxyRuntimeEnv } from \"./runtime-env.js\";\nimport type { ProviderId } from \"../providers/types.js\";\n\nexport type { LogLevel } from \"./runtime-env.js\";\n\nexport interface ProxyConfig {\n profileName: string;\n relayName: string;\n relayUrl?: string;\n // /proxy 端点的预共享 token, 和 relay 侧 RELAY_PROXY_TOKEN 对应. 公网 relay 必须设置\n relayToken?: string;\n hookPort?: number;\n claudeBin?: string;\n codexBin?: string;\n previewRoots: string[];\n agentCliSuggestions: Record<ProviderId, string[]>;\n sources: {\n relayName: \"cli\" | \"profile\";\n relayUrl: \"env\" | \"file\" | \"none\";\n relayToken: \"env\" | \"file\" | \"none\";\n hookPort: \"env\" | \"default\";\n claudeBin: \"env\" | \"file\" | \"none\";\n codexBin: \"env\" | \"file\" | \"none\";\n };\n}\n\nconst LogLevelSchema = z.enum(VALID_LOG_LEVELS);\n// LogLevel 由 runtime-env.ts 定义并 re-export,schema 用同一组字面量保证两边对齐。\n\nconst RelayTargetSchema = z\n .object({\n url: z.string().optional(),\n proxyToken: z.string().optional(),\n })\n .strict();\n\nconst ProxyProfileSchema = z\n .object({\n relay: z.string().optional(),\n })\n .strict();\n\nconst AgentCliSchema = z\n .object({\n claudeBin: z.string().optional(),\n codexBin: z.string().optional(),\n claudeBinHistory: z.array(z.string()).optional(),\n codexBinHistory: z.array(z.string()).optional(),\n })\n .strict();\n\n// .strict() 在顶层捕获拼错的字段(\"relayss\" / \"profile\"),但 profiles/relays 内部\n// 是 record(用户定义键),不限制键名。\nconst ProxyConfigFileSchema = z\n .object({\n defaultProfile: z.string().optional(),\n profiles: z.record(z.string(), ProxyProfileSchema),\n relays: z.record(z.string(), RelayTargetSchema),\n agentCli: AgentCliSchema.optional(),\n previewRoots: z.array(z.string()).optional(),\n logLevel: LogLevelSchema.optional(),\n })\n .strict();\n\ntype ProxyConfigFile = z.infer<typeof ProxyConfigFileSchema>;\ntype RelayTargetConfig = z.infer<typeof RelayTargetSchema>;\ntype AgentCliConfig = z.infer<typeof AgentCliSchema>;\n\nfunction readConfigFile(): ProxyConfigFile {\n if (!existsSync(CONFIG_PATH)) {\n throw new Error(`Dev Anywhere config not found at ${CONFIG_PATH}. Run \"dev-anywhere init\".`);\n }\n let raw: unknown;\n try {\n raw = JSON.parse(readFileSync(CONFIG_PATH, \"utf-8\"));\n } catch (err) {\n throw new Error(\n `${CONFIG_PATH} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,\n { cause: err },\n );\n }\n const parsed = ProxyConfigFileSchema.safeParse(raw);\n if (!parsed.success) {\n const issues = parsed.error.issues\n .map(\n (issue) => ` ${issue.path.length > 0 ? issue.path.join(\".\") : \"(root)\"}: ${issue.message}`,\n )\n .join(\"\\n\");\n throw new Error(`Invalid config at ${CONFIG_PATH}:\\n${issues}`);\n }\n return parsed.data;\n}\n\nfunction agentCliField(provider: ProviderId): \"claudeBin\" | \"codexBin\" {\n return provider === \"claude\" ? \"claudeBin\" : \"codexBin\";\n}\n\nfunction agentCliHistoryField(provider: ProviderId): \"claudeBinHistory\" | \"codexBinHistory\" {\n return provider === \"claude\" ? \"claudeBinHistory\" : \"codexBinHistory\";\n}\n\nfunction validateAgentCliPath(path: string): string {\n const normalized = path.trim();\n if (!normalized) throw new Error(\"请输入 CLI 路径\");\n if (!isAbsolute(normalized)) throw new Error(\"CLI 路径必须是绝对路径\");\n return normalized;\n}\n\nfunction uniqueAbsolutePaths(paths: Array<string | undefined>): string[] {\n const seen = new Set<string>();\n const result: string[] = [];\n for (const path of paths) {\n const normalized = path?.trim();\n if (!normalized || !isAbsolute(normalized) || seen.has(normalized)) continue;\n seen.add(normalized);\n result.push(normalized);\n }\n return result;\n}\n\nfunction resolveRelayConfig(\n fromFile: ProxyConfigFile,\n requestedRelayName?: string,\n): {\n relayName: string;\n relayNameSource: ProxyConfig[\"sources\"][\"relayName\"];\n relay: RelayTargetConfig;\n} {\n const profile = fromFile.profiles[PROFILE_NAME];\n if (!profile) {\n const available = Object.keys(fromFile.profiles).sort();\n throw new Error(\n `Unknown profile \"${PROFILE_NAME}\". Available profiles: ${available.length > 0 ? available.join(\", \") : \"(none)\"}`,\n );\n }\n\n const relayName = requestedRelayName?.trim() || profile.relay?.trim();\n if (!relayName) {\n throw new Error(`Profile \"${PROFILE_NAME}\" must specify a relay.`);\n }\n\n const relay = fromFile.relays[relayName];\n if (!relay) {\n const available = Object.keys(fromFile.relays).sort();\n throw new Error(\n `Unknown relay \"${relayName}\". Available relays: ${available.length > 0 ? available.join(\", \") : \"(none)\"}`,\n );\n }\n\n return {\n relayName,\n relayNameSource: requestedRelayName?.trim() ? \"cli\" : \"profile\",\n relay,\n };\n}\n\nexport function loadConfig(options?: { relayName?: string }): ProxyConfig {\n const env = loadProxyRuntimeEnv();\n const fromFile = readConfigFile();\n const agentCli = fromFile.agentCli ?? {};\n const resolved = resolveRelayConfig(fromFile, options?.relayName);\n const claudeBin = env.claudeBin ?? agentCli.claudeBin;\n const codexBin = env.codexBin ?? agentCli.codexBin;\n const config: ProxyConfig = {\n profileName: PROFILE_NAME,\n relayName: resolved.relayName,\n relayUrl: env.relayUrl ?? resolved.relay.url,\n relayToken: env.relayProxyToken ?? resolved.relay.proxyToken,\n hookPort: env.hookPort ?? defaultHookPortForProfile(PROFILE_NAME),\n claudeBin,\n codexBin,\n previewRoots: uniqueAbsolutePaths(fromFile.previewRoots ?? []),\n agentCliSuggestions: {\n claude: uniqueAbsolutePaths([\n env.claudeBin,\n agentCli.claudeBin,\n ...(agentCli.claudeBinHistory ?? []),\n ]),\n codex: uniqueAbsolutePaths([\n env.codexBin,\n agentCli.codexBin,\n ...(agentCli.codexBinHistory ?? []),\n ]),\n },\n sources: {\n relayName: resolved.relayNameSource,\n relayUrl: env.relayUrl ? \"env\" : resolved.relay.url ? \"file\" : \"none\",\n relayToken: env.relayProxyToken ? \"env\" : resolved.relay.proxyToken ? \"file\" : \"none\",\n hookPort: env.hookPort !== undefined ? \"env\" : \"default\",\n claudeBin: env.claudeBin ? \"env\" : agentCli.claudeBin ? \"file\" : \"none\",\n codexBin: env.codexBin ? \"env\" : agentCli.codexBin ? \"file\" : \"none\",\n },\n };\n\n serviceLogger.info(\n {\n profile: config.profileName,\n relayName: config.relayName,\n relayNameSource: config.sources.relayName,\n relayUrl: config.relayUrl ?? \"(unset)\",\n relayUrlSource: config.sources.relayUrl,\n relayTokenSource: config.sources.relayToken,\n hookPort: config.hookPort,\n hookPortSource: config.sources.hookPort,\n claudeBinSource: config.sources.claudeBin,\n codexBinSource: config.sources.codexBin,\n },\n \"Config loaded\",\n );\n\n return config;\n}\n\nexport function buildProviderEnv(\n config: ProxyConfig,\n baseEnv: NodeJS.ProcessEnv = process.env,\n): NodeJS.ProcessEnv {\n return {\n ...baseEnv,\n ...(config.claudeBin ? { CLAUDE_BIN: config.claudeBin } : {}),\n ...(config.codexBin ? { CODEX_BIN: config.codexBin } : {}),\n };\n}\n\nfunction updateAgentCliConfig(\n config: AgentCliConfig,\n provider: ProviderId,\n path: string,\n): AgentCliConfig {\n const field = agentCliField(provider);\n const historyField = agentCliHistoryField(provider);\n const history = uniqueAbsolutePaths([path, ...(config[historyField] ?? [])]).slice(0, 8);\n return {\n ...config,\n [field]: path,\n [historyField]: history,\n };\n}\n\nexport function saveAgentCliPath(provider: ProviderId, path: string): void {\n const normalized = validateAgentCliPath(path);\n const fromFile = readConfigFile();\n fromFile.agentCli = updateAgentCliConfig(fromFile.agentCli ?? {}, provider, normalized);\n mkdirSync(dirname(CONFIG_PATH), { recursive: true });\n writeFileSync(CONFIG_PATH, `${JSON.stringify(fromFile, null, 2)}\\n`, \"utf-8\");\n}\n"],"mappings":"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","names":[]}
@@ -1,8 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- LOG_DIR,
4
- createLogger
5
- } from "./chunk-RFBTVZ2X.js";
6
2
 
7
3
  // src/common/osc-extractor.ts
8
4
  var OSC_PATTERN = /\x1b\](\d+);([^\x07\x1b]*?)(?:\x07|\x1b\\)/g;
@@ -116,27 +112,12 @@ function defineFSM(transitions) {
116
112
  };
117
113
  }
118
114
 
119
- // src/common/logger.ts
120
- var serviceLogger = createLogger({
121
- name: "service",
122
- logDir: LOG_DIR,
123
- silent: !!process.env.VITEST
124
- });
125
- var terminalLogger = createLogger({
126
- name: "terminal",
127
- level: "debug",
128
- logDir: LOG_DIR,
129
- silent: !!process.env.VITEST
130
- });
131
-
132
115
  export {
133
116
  extractOscSequences,
134
117
  extractOscSignals,
135
118
  shouldReleaseApprovalWait,
136
119
  stateAfterApprovalRelease,
137
120
  createFSM,
138
- defineFSM,
139
- serviceLogger,
140
- terminalLogger
121
+ defineFSM
141
122
  };
142
- //# sourceMappingURL=chunk-JGGDVMY5.js.map
123
+ //# sourceMappingURL=chunk-ORZTFYXR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/common/osc-extractor.ts","../src/common/pty-approval-state.ts","../src/common/state-machine.ts"],"sourcesContent":["// OSC 0: 窗口标题 -- ESC ] 0 ; <title> BEL/ST\n// OSC 9: 通知 -- ESC ] 9 ; <text> BEL/ST\n// 每次调用创建新的 regex 实例避免 g flag 导致的 lastIndex 状态泄漏\n// eslint-disable-next-line no-control-regex\nconst OSC_PATTERN = /\\x1b\\](\\d+);([^\\x07\\x1b]*?)(?:\\x07|\\x1b\\\\)/g;\n\nexport type PtySemanticState = \"working\" | \"turn_complete\" | \"approval_wait\" | \"mid_pause\";\ntype PtySignalProvider = \"claude\" | \"codex\";\n\ninterface PtyStateEvent {\n state: PtySemanticState;\n title?: string;\n tool?: string;\n}\n\ninterface OscSequence {\n code: number;\n text: string;\n}\n\nexport function extractOscSequences(rawData: string): OscSequence[] {\n const regex = new RegExp(OSC_PATTERN.source, OSC_PATTERN.flags);\n const matches: OscSequence[] = [];\n\n let match: RegExpExecArray | null;\n while ((match = regex.exec(rawData)) !== null) {\n matches.push({ code: parseInt(match[1], 10), text: match[2] });\n }\n\n return matches;\n}\n\nfunction lastSequence(matches: OscSequence[], code: number): OscSequence | undefined {\n for (let i = matches.length - 1; i >= 0; i -= 1) {\n if (matches[i].code === code) return matches[i];\n }\n return undefined;\n}\n\nfunction isCodexActionRequiredTitle(title: string): boolean {\n return /\\bAction Required\\b/i.test(title);\n}\n\n// 从 PTY 原始数据中提取 OSC 语义信号。\n// OSC 9 优先级高于 OSC 0,无匹配时返回 null。\nexport function extractOscSignals(\n rawData: string,\n provider?: PtySignalProvider,\n): PtyStateEvent | null {\n const matches = extractOscSequences(rawData);\n\n if (matches.length === 0) return null;\n\n const osc0 = lastSequence(matches, 0);\n\n // OSC 9 优先级更高,包含具体的语义信号;同帧 OSC 0 仍保留 title 给 UI。\n const osc9 = lastSequence(matches, 9);\n if (osc9) {\n if (osc9.text.includes(\"waiting for your input\") || osc9.text.trim() === \"4;0;\") {\n return { state: \"turn_complete\", ...(osc0 ? { title: osc0.text } : {}) };\n }\n if (osc9.text.includes(\"needs your permission\")) {\n const toolMatch = osc9.text.match(/permission.*?:\\s*(\\S+)/);\n return {\n state: \"approval_wait\",\n ...(osc0 ? { title: osc0.text } : {}),\n ...(toolMatch?.[1] ? { tool: toolMatch[1] } : {}),\n };\n }\n }\n\n if (provider === \"codex\" && osc0 && isCodexActionRequiredTitle(osc0.text)) {\n return { state: \"approval_wait\", title: osc0.text };\n }\n\n // 仅有 OSC 0(标题/spinner 变化)时视为 MID_PAUSE\n if (osc0 && !osc9) {\n return { state: \"mid_pause\", title: osc0.text };\n }\n\n return null;\n}\n","import type { PtySemanticState } from \"./osc-extractor.js\";\n\nexport function shouldReleaseApprovalWait(options: {\n currentState: PtySemanticState;\n signalState?: PtySemanticState;\n}): boolean {\n if (options.currentState !== \"approval_wait\") return false;\n return options.signalState !== undefined && options.signalState !== \"approval_wait\";\n}\n\nexport function stateAfterApprovalRelease(signalState?: PtySemanticState): PtySemanticState {\n return signalState && signalState !== \"approval_wait\" ? signalState : \"working\";\n}\n","// 有限状态机 helper\n//\n// 提供显式转换表的小型 FSM,区分两类调用模式:\n// - transitionTo(throw):同步确定性流程里调用,非法转移代表 bug,立即暴露\n// - tryTransitionTo(bool):异步事件回调里调用,非法转移可能是吸收态残余事件,调用方按\n// isInAbsorbingState() 分级日志\n//\n// 吸收态采用传递闭包定义:终态(transitions=[])或所有出边都指向吸收态的状态都算吸收。\n// 这样 SessionState.ERROR (→[TERMINATED]) 和 RelayConnectionState.CLOSED ([]) 都被\n// 自动识别为吸收态,不用在 caller 里硬编码状态名。\n//\n// onTransition/onRejected 仅做日志/观测,不应 throw。\n\ninterface FSMDef<S extends string> {\n initial: S;\n // from-state → 允许转入的 to-state 列表;终态对应空数组\n transitions: Record<S, readonly S[]>;\n // 合法转换发生后触发,典型用法是结构化日志\n onTransition?: (from: S, to: S) => void;\n // tryTransitionTo 非法转移时触发;isAbsorbing 指示 from 是否吸收态(晚到残余 vs. 真非法)\n onRejected?: (from: S, to: S, isAbsorbing: boolean) => void;\n}\n\ninterface FSM<S extends string> {\n current(): S;\n is(state: S): boolean;\n isIn(states: readonly S[]): boolean;\n canTransitionTo(to: S): boolean;\n // 非法转换抛 Error;同步流程里当 assert 用\n transitionTo(to: S): void;\n // 非法转换返回 false,不抛;异步回调里用,配合 isInAbsorbingState 分级日志\n tryTransitionTo(to: S): boolean;\n // 当前是否在吸收态(传递闭包)\n isInAbsorbingState(): boolean;\n}\n\n// 计算吸收态集合:终态 + 所有出边都指向吸收态的状态,迭代至不动点\nfunction computeAbsorbingSet<S extends string>(transitions: Record<S, readonly S[]>): Set<S> {\n const absorbing = new Set<S>();\n const entries = Object.entries(transitions) as Array<[S, readonly S[]]>;\n for (const [s, outs] of entries) {\n if (outs.length === 0) absorbing.add(s);\n }\n let changed = true;\n while (changed) {\n changed = false;\n for (const [s, outs] of entries) {\n if (absorbing.has(s)) continue;\n if (outs.length > 0 && outs.every((t) => absorbing.has(t))) {\n absorbing.add(s);\n changed = true;\n }\n }\n }\n return absorbing;\n}\n\nexport function createFSM<S extends string>(def: FSMDef<S>): FSM<S> {\n let state = def.initial;\n const absorbing = computeAbsorbingSet(def.transitions);\n const tryTransitionTo = (to: S): boolean => {\n const allowed = def.transitions[state];\n if (!allowed?.includes(to)) {\n def.onRejected?.(state, to, absorbing.has(state));\n return false;\n }\n const from = state;\n state = to;\n def.onTransition?.(from, to);\n return true;\n };\n return {\n current: () => state,\n is: (s) => state === s,\n isIn: (ss) => ss.includes(state),\n canTransitionTo: (to) => def.transitions[state]?.includes(to) ?? false,\n transitionTo: (to) => {\n if (!tryTransitionTo(to)) {\n throw new Error(`Invalid FSM transition: ${state} -> ${to}`);\n }\n },\n tryTransitionTo,\n isInAbsorbingState: () => absorbing.has(state),\n };\n}\n\n// 无内部 state 的 FSM 视图,供 state 存在外部(如 SessionInfo、DB 行)的 per-instance 场景使用。\n// 调用方自行传入 from,canTransition 校验;吸收态判定通过 isAbsorbing(state) 提供。\ninterface StatelessFSM<S extends string> {\n canTransition(from: S, to: S): boolean;\n isAbsorbing(state: S): boolean;\n}\n\nexport function defineFSM<S extends string>(transitions: Record<S, readonly S[]>): StatelessFSM<S> {\n const absorbing = computeAbsorbingSet(transitions);\n return {\n canTransition: (from, to) => transitions[from]?.includes(to) ?? false,\n isAbsorbing: (state) => absorbing.has(state),\n };\n}\n"],"mappings":"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","names":[]}