sessix-server 0.3.8 → 0.4.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.
Files changed (3) hide show
  1. package/dist/index.js +579 -48
  2. package/dist/server.js +573 -42
  3. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -307,12 +307,12 @@ function t(key, params) {
307
307
  }
308
308
 
309
309
  // src/server.ts
310
- var import_uuid6 = require("uuid");
311
- var import_promises4 = require("fs/promises");
312
- var import_node_os7 = require("os");
313
- var import_node_path6 = require("path");
314
- var import_node_child_process9 = require("child_process");
315
- var import_node_util = require("util");
310
+ var import_uuid7 = require("uuid");
311
+ var import_promises5 = require("fs/promises");
312
+ var import_node_os8 = require("os");
313
+ var import_node_path7 = require("path");
314
+ var import_node_child_process10 = require("child_process");
315
+ var import_node_util2 = require("util");
316
316
 
317
317
  // src/providers/ProcessProvider.ts
318
318
  var import_child_process = require("child_process");
@@ -2528,6 +2528,8 @@ var ApprovalProxy = class _ApprovalProxy {
2528
2528
  pendingApprovals = /* @__PURE__ */ new Map();
2529
2529
  /** 审批请求回调(通知外部推送到手机) */
2530
2530
  approvalRequestCallbacks = [];
2531
+ /** 审批 resolve 回调(任何来源的 resolve 都会触发,用于 WS 广播清理 */
2532
+ approvalResolvedCallbacks = [];
2531
2533
  /** YOLO 模式状态:sessionId -> enabled */
2532
2534
  yoloSessions = /* @__PURE__ */ new Map();
2533
2535
  /** 内存缓存:已被"始终允许"的工具名(避免每次读 settings.json) */
@@ -2565,6 +2567,30 @@ var ApprovalProxy = class _ApprovalProxy {
2565
2567
  onApprovalRequest(callback) {
2566
2568
  this.approvalRequestCallbacks.push(callback);
2567
2569
  }
2570
+ /**
2571
+ * 注册审批 resolve 回调
2572
+ *
2573
+ * 任何来源的 resolve 都会触发:
2574
+ * - resolveApproval(手机端 approve/reject)
2575
+ * - 5 分钟超时自动 allow
2576
+ * - clearPendingForSession(会话被 kill)
2577
+ * - approveAll(手机全断时自动 allow)
2578
+ *
2579
+ * 用于向所有客户端广播 approval_resolved,清理可能残留的审批 UI。
2580
+ */
2581
+ onApprovalResolved(callback) {
2582
+ this.approvalResolvedCallbacks.push(callback);
2583
+ }
2584
+ /** 通知所有 resolve 回调(内部调用) */
2585
+ notifyApprovalResolved(requestId, decision) {
2586
+ for (const callback of this.approvalResolvedCallbacks) {
2587
+ try {
2588
+ callback(requestId, decision);
2589
+ } catch (err) {
2590
+ console.error("[ApprovalProxy] Approval resolved callback error:", err);
2591
+ }
2592
+ }
2593
+ }
2568
2594
  /** 设置状态信息提供者(用于 /health 端点) */
2569
2595
  setStatusInfoProvider(provider) {
2570
2596
  this.statusInfoProvider = provider;
@@ -2598,6 +2624,7 @@ var ApprovalProxy = class _ApprovalProxy {
2598
2624
  pending.resolve(decision);
2599
2625
  this.pendingApprovals.delete(requestId);
2600
2626
  console.log(`[ApprovalProxy] ${t("approval.requestProcessed", { id: requestId })}: ${decision.decision}`);
2627
+ this.notifyApprovalResolved(requestId, decision);
2601
2628
  return true;
2602
2629
  }
2603
2630
  /** 获取当前待处理的审批数量 */
@@ -2613,9 +2640,11 @@ var ApprovalProxy = class _ApprovalProxy {
2613
2640
  for (const [requestId, pending] of this.pendingApprovals) {
2614
2641
  if (pending.request.sessionId === sessionId) {
2615
2642
  clearTimeout(pending.timer);
2616
- pending.resolve({ decision: "allow" });
2643
+ const decision = { decision: "allow" };
2644
+ pending.resolve(decision);
2617
2645
  this.pendingApprovals.delete(requestId);
2618
2646
  console.log(`[ApprovalProxy] Session ${sessionId} killed, auto-allowed pending approval ${requestId}`);
2647
+ this.notifyApprovalResolved(requestId, decision);
2619
2648
  }
2620
2649
  }
2621
2650
  }
@@ -2703,9 +2732,11 @@ var ApprovalProxy = class _ApprovalProxy {
2703
2732
  const entries = Array.from(this.pendingApprovals.entries());
2704
2733
  for (const [requestId, pending] of entries) {
2705
2734
  clearTimeout(pending.timer);
2706
- pending.resolve({ decision: "allow" });
2735
+ const decision = { decision: "allow" };
2736
+ pending.resolve(decision);
2707
2737
  this.pendingApprovals.delete(requestId);
2708
2738
  console.log(`[ApprovalProxy] ${t("approval.autoAllowed", { id: requestId, reason: reason ? `\uFF08${reason}\uFF09` : "" })}`);
2739
+ this.notifyApprovalResolved(requestId, decision);
2709
2740
  }
2710
2741
  }
2711
2742
  /** 优雅关闭 HTTP 服务 */
@@ -2797,7 +2828,9 @@ var ApprovalProxy = class _ApprovalProxy {
2797
2828
  const timer = setTimeout(() => {
2798
2829
  console.log(`[ApprovalProxy] ${t("approval.timeout", { id: requestId })}`);
2799
2830
  this.pendingApprovals.delete(requestId);
2800
- resolve({ decision: "allow" });
2831
+ const autoDecision = { decision: "allow" };
2832
+ resolve(autoDecision);
2833
+ this.notifyApprovalResolved(requestId, autoDecision);
2801
2834
  }, 325e3);
2802
2835
  this.pendingApprovals.set(requestId, { resolve, timer, request: approvalRequest });
2803
2836
  });
@@ -3098,9 +3131,26 @@ process.stdin.on('end', async () => {
3098
3131
  signal: AbortSignal.timeout(320000),
3099
3132
  })
3100
3133
  const data = await res.json()
3101
- process.exit(data.decision === 'deny' ? 1 : 0)
3134
+ const decision = data.decision === 'deny' ? 'deny' : 'allow'
3135
+ const output = {
3136
+ hookSpecificOutput: {
3137
+ hookEventName: 'PreToolUse',
3138
+ permissionDecision: decision,
3139
+ },
3140
+ }
3141
+ if (decision === 'deny' && data.reason) {
3142
+ output.hookSpecificOutput.permissionDecisionReason = String(data.reason)
3143
+ }
3144
+ process.stdout.write(JSON.stringify(output))
3145
+ process.exit(0)
3102
3146
  } catch {
3103
- // Sessix \u670D\u52A1\u5668\u4E0D\u53EF\u7528\uFF0C\u9ED8\u8BA4\u653E\u884C
3147
+ // Sessix \u670D\u52A1\u5668\u4E0D\u53EF\u7528\uFF0C\u9ED8\u8BA4\u653E\u884C\uFF08\u8F93\u51FA\u663E\u5F0F allow\uFF0C\u907F\u514D\u843D\u5230\u7EC8\u7AEF\u63D0\u793A\uFF09
3148
+ process.stdout.write(JSON.stringify({
3149
+ hookSpecificOutput: {
3150
+ hookEventName: 'PreToolUse',
3151
+ permissionDecision: 'allow',
3152
+ },
3153
+ }))
3104
3154
  process.exit(0)
3105
3155
  }
3106
3156
  })
@@ -3143,15 +3193,19 @@ var HookInstaller = class {
3143
3193
  console.log("[HookInstaller] Hook uninstalled");
3144
3194
  }
3145
3195
  /**
3146
- * 检查 hook 是否已安装
3147
- * 脚本文件和 settings.json 配置必须同时存在才算已安装
3196
+ * 检查 hook 是否已安装且为最新版本
3197
+ *
3198
+ * 必须同时满足:
3199
+ * 1. 两个脚本文件都存在
3200
+ * 2. settings.json 中有 Sessix hook 配置
3201
+ * 3. approval-hook.js 包含最新的 permissionDecision 输出协议
3202
+ * (旧版仅用 exit code,会导致 ExitPlanMode 等工具卡住)
3148
3203
  */
3149
3204
  async isInstalled() {
3150
- let approvalScriptExists = false;
3205
+ let approvalScriptContent = "";
3151
3206
  let permissionScriptExists = false;
3152
3207
  try {
3153
- await (0, import_promises2.access)(HOOK_SCRIPT_PATH);
3154
- approvalScriptExists = true;
3208
+ approvalScriptContent = await (0, import_promises2.readFile)(HOOK_SCRIPT_PATH, "utf-8");
3155
3209
  } catch {
3156
3210
  }
3157
3211
  try {
@@ -3159,9 +3213,10 @@ var HookInstaller = class {
3159
3213
  permissionScriptExists = true;
3160
3214
  } catch {
3161
3215
  }
3216
+ const isLatestVersion = approvalScriptContent.includes("permissionDecision");
3162
3217
  const settings = await this.readClaudeSettings();
3163
3218
  const configExists = this.hasHookConfig(settings);
3164
- return approvalScriptExists && permissionScriptExists && configExists;
3219
+ return isLatestVersion && permissionScriptExists && configExists;
3165
3220
  }
3166
3221
  // ============================================
3167
3222
  // 内部方法
@@ -4335,7 +4390,7 @@ var AuthManager = class extends import_events3.EventEmitter {
4335
4390
  };
4336
4391
 
4337
4392
  // src/server.ts
4338
- var import_promises5 = require("fs/promises");
4393
+ var import_promises6 = require("fs/promises");
4339
4394
 
4340
4395
  // src/terminal/TerminalExecutor.ts
4341
4396
  var import_node_child_process7 = require("child_process");
@@ -4424,8 +4479,422 @@ var TerminalExecutor = class {
4424
4479
  }
4425
4480
  };
4426
4481
 
4427
- // src/utils/cliCapabilities.ts
4482
+ // src/xcode/XcodeBuildExecutor.ts
4428
4483
  var import_node_child_process8 = require("child_process");
4484
+ var import_node_util = require("util");
4485
+ var import_promises4 = require("fs/promises");
4486
+ var import_node_path6 = require("path");
4487
+ var import_node_os7 = require("os");
4488
+ var import_uuid6 = require("uuid");
4489
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process8.exec);
4490
+ var BUILD_TIMEOUT_MS = 30 * 60 * 1e3;
4491
+ var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
4492
+ var CONFIG_FILE = (0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix", "xcode-config.json");
4493
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
4494
+ "node_modules",
4495
+ ".git",
4496
+ "DerivedData",
4497
+ "Pods",
4498
+ ".build",
4499
+ "build",
4500
+ "dist",
4501
+ "__pycache__",
4502
+ ".next",
4503
+ "vendor",
4504
+ ".expo",
4505
+ "android",
4506
+ ".gradle",
4507
+ "Carthage",
4508
+ "xcarchive"
4509
+ ]);
4510
+ var MAX_SCAN_DEPTH = 4;
4511
+ var XcodeBuildExecutor = class {
4512
+ builds = /* @__PURE__ */ new Map();
4513
+ installs = /* @__PURE__ */ new Map();
4514
+ eventCallbacks = [];
4515
+ configCache = null;
4516
+ onEvent(callback) {
4517
+ this.eventCallbacks.push(callback);
4518
+ return () => {
4519
+ const idx = this.eventCallbacks.indexOf(callback);
4520
+ if (idx !== -1) this.eventCallbacks.splice(idx, 1);
4521
+ };
4522
+ }
4523
+ emit(event) {
4524
+ for (const cb of this.eventCallbacks) {
4525
+ try {
4526
+ cb(event);
4527
+ } catch (err) {
4528
+ console.error("[XcodeBuildExecutor] Event callback error:", err);
4529
+ }
4530
+ }
4531
+ }
4532
+ // ============================================
4533
+ // 配置持久化
4534
+ // ============================================
4535
+ async loadConfigs() {
4536
+ if (this.configCache) return this.configCache;
4537
+ try {
4538
+ const raw = await (0, import_promises4.readFile)(CONFIG_FILE, "utf8");
4539
+ this.configCache = JSON.parse(raw);
4540
+ } catch {
4541
+ this.configCache = {};
4542
+ }
4543
+ return this.configCache;
4544
+ }
4545
+ async writeConfigs(store) {
4546
+ await (0, import_promises4.mkdir)((0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix"), { recursive: true });
4547
+ await (0, import_promises4.writeFile)(CONFIG_FILE, JSON.stringify(store, null, 2), "utf8");
4548
+ this.configCache = store;
4549
+ }
4550
+ async getSavedConfig(projectPath) {
4551
+ const store = await this.loadConfigs();
4552
+ return store[projectPath];
4553
+ }
4554
+ async saveConfig(projectPath, config) {
4555
+ const store = await this.loadConfigs();
4556
+ store[projectPath] = config;
4557
+ await this.writeConfigs(store);
4558
+ }
4559
+ // ============================================
4560
+ // 递归扫描 Xcode 工程
4561
+ // ============================================
4562
+ async findAllContainers(projectPath) {
4563
+ const results = [];
4564
+ await this.scanDir(projectPath, projectPath, 0, results);
4565
+ results.sort((a, b) => a.path.split("/").length - b.path.split("/").length);
4566
+ return results;
4567
+ }
4568
+ async scanDir(rootPath, currentPath, depth, results) {
4569
+ if (depth > MAX_SCAN_DEPTH) return;
4570
+ let entries;
4571
+ try {
4572
+ entries = await (0, import_promises4.readdir)(currentPath);
4573
+ } catch {
4574
+ return;
4575
+ }
4576
+ let foundWorkspace;
4577
+ let foundProject;
4578
+ for (const name of entries) {
4579
+ if (name.endsWith(".xcworkspace") && !name.endsWith("project.xcworkspace")) {
4580
+ foundWorkspace = foundWorkspace ?? name;
4581
+ } else if (name.endsWith(".xcodeproj")) {
4582
+ foundProject = foundProject ?? name;
4583
+ }
4584
+ }
4585
+ if (foundWorkspace || foundProject) {
4586
+ const relDir = currentPath === rootPath ? "" : currentPath.slice(rootPath.length + 1);
4587
+ const containerName = foundWorkspace ? foundWorkspace.replace(/\.xcworkspace$/, "") : foundProject.replace(/\.xcodeproj$/, "");
4588
+ results.push({
4589
+ path: relDir,
4590
+ name: containerName,
4591
+ workspace: foundWorkspace ? relDir ? `${relDir}/${foundWorkspace}` : foundWorkspace : void 0,
4592
+ project: foundProject ? relDir ? `${relDir}/${foundProject}` : foundProject : void 0
4593
+ });
4594
+ }
4595
+ for (const name of entries) {
4596
+ if (SKIP_DIRS.has(name)) continue;
4597
+ if (name.startsWith(".")) continue;
4598
+ if (name.endsWith(".xcodeproj") || name.endsWith(".xcworkspace")) continue;
4599
+ const childPath = (0, import_node_path6.join)(currentPath, name);
4600
+ await this.scanDir(rootPath, childPath, depth + 1, results);
4601
+ }
4602
+ }
4603
+ // ============================================
4604
+ // 探测 schemes(针对指定 container)
4605
+ // ============================================
4606
+ async detect(projectPath) {
4607
+ if (process.platform !== "darwin") {
4608
+ return { available: false, error: "Xcode \u6784\u5EFA\u53EA\u652F\u6301 macOS", containers: [], schemes: [] };
4609
+ }
4610
+ const containers = await this.findAllContainers(projectPath);
4611
+ if (containers.length === 0) {
4612
+ return { available: false, error: "\u672A\u627E\u5230 .xcworkspace \u6216 .xcodeproj", containers: [], schemes: [] };
4613
+ }
4614
+ const firstContainer = containers[0];
4615
+ const schemes = await this.getSchemesForContainer(projectPath, firstContainer);
4616
+ const saved = await this.getSavedConfig(projectPath);
4617
+ return {
4618
+ available: true,
4619
+ containers,
4620
+ schemes,
4621
+ saved
4622
+ };
4623
+ }
4624
+ async getSchemesForContainer(projectPath, container) {
4625
+ const args = container.workspace ? ["-workspace", container.workspace, "-list", "-json"] : ["-project", container.project, "-list", "-json"];
4626
+ try {
4627
+ const { stdout } = await execAsync(
4628
+ `xcodebuild ${args.map(shellQuote).join(" ")}`,
4629
+ { cwd: projectPath, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
4630
+ );
4631
+ const parsed = JSON.parse(stdout);
4632
+ return parsed.workspace?.schemes ?? parsed.project?.schemes ?? [];
4633
+ } catch {
4634
+ return [];
4635
+ }
4636
+ }
4637
+ // ============================================
4638
+ // 列举 destinations
4639
+ // ============================================
4640
+ async listDestinations(projectPath, scheme, container) {
4641
+ if (process.platform !== "darwin") return [];
4642
+ const args = [
4643
+ ...container.workspace ? ["-workspace", container.workspace] : ["-project", container.project],
4644
+ "-scheme",
4645
+ scheme,
4646
+ "-showdestinations"
4647
+ ];
4648
+ try {
4649
+ const { stdout, stderr } = await execAsync(
4650
+ `xcodebuild ${args.map(shellQuote).join(" ")}`,
4651
+ { cwd: projectPath, timeout: 6e4, maxBuffer: 4 * 1024 * 1024 }
4652
+ );
4653
+ return parseDestinations(stdout + "\n" + stderr);
4654
+ } catch (err) {
4655
+ const e = err;
4656
+ const parsed = parseDestinations(`${e.stdout ?? ""}
4657
+ ${e.stderr ?? ""}`);
4658
+ if (parsed.length > 0) return parsed;
4659
+ throw err;
4660
+ }
4661
+ }
4662
+ // ============================================
4663
+ // 构建
4664
+ // ============================================
4665
+ async build(sessionId, projectPath, override) {
4666
+ if (process.platform !== "darwin") {
4667
+ this.emitBuildError(sessionId, "", "Xcode \u6784\u5EFA\u4EC5\u652F\u6301 macOS\n");
4668
+ return null;
4669
+ }
4670
+ const config = override ?? await this.getSavedConfig(projectPath);
4671
+ if (!config) {
4672
+ this.emitBuildError(sessionId, "", "\u672A\u914D\u7F6E Xcode \u6784\u5EFA\u53C2\u6570\uFF0C\u8BF7\u5148\u9009\u62E9 scheme \u4E0E\u76EE\u6807\u8BBE\u5907\n");
4673
+ return null;
4674
+ }
4675
+ if (override) await this.saveConfig(projectPath, override);
4676
+ const buildId = (0, import_uuid6.v4)();
4677
+ const args = buildArgs(config);
4678
+ const proc = (0, import_node_child_process8.spawn)("xcodebuild", args, {
4679
+ cwd: projectPath,
4680
+ stdio: ["ignore", "pipe", "pipe"],
4681
+ env: { ...process.env, NSUnbufferedIO: "YES" }
4682
+ });
4683
+ this.builds.set(buildId, proc);
4684
+ this.emit({
4685
+ type: "xcode_build_output",
4686
+ sessionId,
4687
+ buildId,
4688
+ stream: "stdout",
4689
+ data: `$ xcodebuild ${args.map((a) => a.includes(" ") ? `"${a}"` : a).join(" ")}
4690
+ cwd: ${projectPath}
4691
+ destination: ${config.destinationName}
4692
+
4693
+ `
4694
+ });
4695
+ proc.stdout?.on("data", (chunk) => {
4696
+ this.emit({ type: "xcode_build_output", sessionId, buildId, stream: "stdout", data: chunk.toString() });
4697
+ });
4698
+ proc.stderr?.on("data", (chunk) => {
4699
+ this.emit({ type: "xcode_build_output", sessionId, buildId, stream: "stderr", data: chunk.toString() });
4700
+ });
4701
+ proc.on("error", (err) => {
4702
+ this.emit({ type: "xcode_build_output", sessionId, buildId, stream: "stderr", data: `[spawn error] ${err.message}
4703
+ ` });
4704
+ });
4705
+ const timer = setTimeout(() => {
4706
+ if (this.builds.has(buildId)) killProcessCrossPlatform(proc);
4707
+ }, BUILD_TIMEOUT_MS);
4708
+ proc.on("exit", (code, signal) => {
4709
+ clearTimeout(timer);
4710
+ this.builds.delete(buildId);
4711
+ this.emit({ type: "xcode_build_exit", sessionId, buildId, code, signal });
4712
+ });
4713
+ console.log(`[XcodeBuildExecutor] build ${buildId} scheme=${config.scheme} dest=${config.destinationName}`);
4714
+ return buildId;
4715
+ }
4716
+ killBuild(buildId) {
4717
+ const proc = this.builds.get(buildId);
4718
+ if (proc) {
4719
+ killProcessCrossPlatform(proc);
4720
+ console.log(`[XcodeBuildExecutor] kill build ${buildId}`);
4721
+ }
4722
+ }
4723
+ emitBuildError(sessionId, buildId, msg) {
4724
+ this.emit({ type: "xcode_build_output", sessionId, buildId, stream: "stderr", data: msg });
4725
+ this.emit({ type: "xcode_build_exit", sessionId, buildId, code: 1, signal: null });
4726
+ }
4727
+ // ============================================
4728
+ // 安装
4729
+ // ============================================
4730
+ async install(sessionId, projectPath) {
4731
+ if (process.platform !== "darwin") {
4732
+ this.emitInstallError(sessionId, "", "Xcode \u5B89\u88C5\u4EC5\u652F\u6301 macOS\n");
4733
+ return null;
4734
+ }
4735
+ const config = await this.getSavedConfig(projectPath);
4736
+ if (!config) {
4737
+ this.emitInstallError(sessionId, "", "\u672A\u627E\u5230\u6784\u5EFA\u914D\u7F6E\uFF0C\u8BF7\u5148\u6784\u5EFA\u4E00\u6B21\n");
4738
+ return null;
4739
+ }
4740
+ const installId = (0, import_uuid6.v4)();
4741
+ let appPath;
4742
+ try {
4743
+ appPath = await this.getAppPath(projectPath, config);
4744
+ } catch (err) {
4745
+ const msg = err instanceof Error ? err.message : String(err);
4746
+ this.emitInstallError(sessionId, installId, `\u65E0\u6CD5\u5B9A\u4F4D\u6784\u5EFA\u4EA7\u7269: ${msg}
4747
+ `);
4748
+ return null;
4749
+ }
4750
+ const { destinationKind, destinationId, destinationName } = config;
4751
+ let installCmd;
4752
+ if (destinationKind === "simulator") {
4753
+ installCmd = ["xcrun", "simctl", "install", destinationId, appPath];
4754
+ } else if (destinationKind === "device") {
4755
+ installCmd = ["xcrun", "devicectl", "device", "install", "app", "--device-id", destinationId, appPath];
4756
+ } else if (destinationKind === "mac") {
4757
+ installCmd = ["open", appPath];
4758
+ } else {
4759
+ this.emitInstallError(sessionId, installId, `\u672A\u77E5\u76EE\u6807\u7C7B\u578B\uFF0C\u65E0\u6CD5\u81EA\u52A8\u5B89\u88C5
4760
+ `);
4761
+ return null;
4762
+ }
4763
+ this.emit({
4764
+ type: "xcode_install_output",
4765
+ sessionId,
4766
+ installId,
4767
+ stream: "stdout",
4768
+ data: `$ ${installCmd.join(" ")}
4769
+ destination: ${destinationName}
4770
+ app: ${appPath}
4771
+
4772
+ `
4773
+ });
4774
+ const proc = (0, import_node_child_process8.spawn)(installCmd[0], installCmd.slice(1), {
4775
+ cwd: projectPath,
4776
+ stdio: ["ignore", "pipe", "pipe"]
4777
+ });
4778
+ this.installs.set(installId, proc);
4779
+ proc.stdout?.on("data", (chunk) => {
4780
+ this.emit({ type: "xcode_install_output", sessionId, installId, stream: "stdout", data: chunk.toString() });
4781
+ });
4782
+ proc.stderr?.on("data", (chunk) => {
4783
+ this.emit({ type: "xcode_install_output", sessionId, installId, stream: "stderr", data: chunk.toString() });
4784
+ });
4785
+ proc.on("error", (err) => {
4786
+ this.emit({ type: "xcode_install_output", sessionId, installId, stream: "stderr", data: `[spawn error] ${err.message}
4787
+ ` });
4788
+ });
4789
+ const timer = setTimeout(() => {
4790
+ if (this.installs.has(installId)) killProcessCrossPlatform(proc);
4791
+ }, INSTALL_TIMEOUT_MS);
4792
+ proc.on("exit", (code, signal) => {
4793
+ clearTimeout(timer);
4794
+ this.installs.delete(installId);
4795
+ this.emit({ type: "xcode_install_exit", sessionId, installId, code, signal });
4796
+ });
4797
+ console.log(`[XcodeBuildExecutor] install ${installId} dest=${destinationName} kind=${destinationKind}`);
4798
+ return installId;
4799
+ }
4800
+ killInstall(installId) {
4801
+ const proc = this.installs.get(installId);
4802
+ if (proc) {
4803
+ killProcessCrossPlatform(proc);
4804
+ console.log(`[XcodeBuildExecutor] kill install ${installId}`);
4805
+ }
4806
+ }
4807
+ emitInstallError(sessionId, installId, msg) {
4808
+ this.emit({ type: "xcode_install_output", sessionId, installId, stream: "stderr", data: msg });
4809
+ this.emit({ type: "xcode_install_exit", sessionId, installId, code: 1, signal: null });
4810
+ }
4811
+ /** 通过 xcodebuild -showBuildSettings 定位 .app 路径 */
4812
+ async getAppPath(projectPath, config) {
4813
+ const args = [
4814
+ ...buildArgs(config).filter((a) => a !== "build"),
4815
+ "-showBuildSettings"
4816
+ ];
4817
+ const { stdout } = await execAsync(
4818
+ `xcodebuild ${args.map(shellQuote).join(" ")}`,
4819
+ { cwd: projectPath, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
4820
+ );
4821
+ const builtDir = extractBuildSetting(stdout, "BUILT_PRODUCTS_DIR");
4822
+ const productName = extractBuildSetting(stdout, "FULL_PRODUCT_NAME");
4823
+ if (!builtDir || !productName) {
4824
+ throw new Error("\u65E0\u6CD5\u4ECE -showBuildSettings \u4E2D\u8BFB\u53D6 BUILT_PRODUCTS_DIR / FULL_PRODUCT_NAME");
4825
+ }
4826
+ return (0, import_node_path6.join)(builtDir, productName);
4827
+ }
4828
+ // ============================================
4829
+ // 清理
4830
+ // ============================================
4831
+ destroy() {
4832
+ for (const [id, proc] of this.builds) {
4833
+ killProcessCrossPlatform(proc);
4834
+ console.log(`[XcodeBuildExecutor] cleanup build ${id}`);
4835
+ }
4836
+ for (const [id, proc] of this.installs) {
4837
+ killProcessCrossPlatform(proc);
4838
+ console.log(`[XcodeBuildExecutor] cleanup install ${id}`);
4839
+ }
4840
+ this.builds.clear();
4841
+ this.installs.clear();
4842
+ this.eventCallbacks.length = 0;
4843
+ }
4844
+ };
4845
+ function shellQuote(s) {
4846
+ return `'${s.replace(/'/g, "'\\''")}'`;
4847
+ }
4848
+ function buildArgs(config) {
4849
+ const container = config.workspace ? ["-workspace", config.workspace] : config.project ? ["-project", config.project] : [];
4850
+ return [
4851
+ ...container,
4852
+ "-scheme",
4853
+ config.scheme,
4854
+ "-destination",
4855
+ `id=${config.destinationId}`,
4856
+ "-configuration",
4857
+ config.configuration ?? "Debug",
4858
+ "build"
4859
+ ];
4860
+ }
4861
+ function extractBuildSetting(output, key) {
4862
+ const match = new RegExp(`^\\s*${key}\\s*=\\s*(.+)$`, "m").exec(output);
4863
+ return match?.[1]?.trim();
4864
+ }
4865
+ function parseDestinations(text) {
4866
+ const results = [];
4867
+ const seen = /* @__PURE__ */ new Set();
4868
+ const lineRegex = /\{\s*([^{}]+)\s*\}/g;
4869
+ let match;
4870
+ while ((match = lineRegex.exec(text)) !== null) {
4871
+ const fields = {};
4872
+ for (const part of match[1].split(",")) {
4873
+ const colon = part.indexOf(":");
4874
+ if (colon === -1) continue;
4875
+ const key = part.slice(0, colon).trim();
4876
+ const value = part.slice(colon + 1).trim();
4877
+ if (key) fields[key] = value;
4878
+ }
4879
+ const { id, name, platform: platform2 } = fields;
4880
+ if (!id || !name || !platform2) continue;
4881
+ if (seen.has(id)) continue;
4882
+ seen.add(id);
4883
+ let kind = "unknown";
4884
+ if (platform2.includes("Simulator")) kind = "simulator";
4885
+ else if (platform2 === "iOS" || platform2 === "watchOS" || platform2 === "tvOS" || platform2 === "visionOS") kind = "device";
4886
+ else if (platform2 === "macOS") kind = "mac";
4887
+ results.push({ id, name, platform: platform2, os: fields.OS, kind });
4888
+ }
4889
+ results.sort((a, b) => kindOrder(a.kind) - kindOrder(b.kind));
4890
+ return results;
4891
+ }
4892
+ function kindOrder(k) {
4893
+ return k === "device" ? 0 : k === "simulator" ? 1 : k === "mac" ? 2 : 3;
4894
+ }
4895
+
4896
+ // src/utils/cliCapabilities.ts
4897
+ var import_node_child_process9 = require("child_process");
4429
4898
  var DEFAULT_CAPABILITIES = {
4430
4899
  effortLevels: ["low", "medium", "high", "xhigh", "max"]
4431
4900
  };
@@ -4453,7 +4922,7 @@ async function parseCliCapabilities() {
4453
4922
  }
4454
4923
  function runCli(path2, args) {
4455
4924
  return new Promise((resolve) => {
4456
- (0, import_node_child_process8.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
4925
+ (0, import_node_child_process9.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
4457
4926
  if (err) {
4458
4927
  console.warn(`[CliCapabilities] Failed to run ${path2} ${args.join(" ")}:`, err.message);
4459
4928
  resolve(null);
@@ -4467,11 +4936,11 @@ function runCli(path2, args) {
4467
4936
  // src/server.ts
4468
4937
  var WS_PORT = 3745;
4469
4938
  var HTTP_PORT = 3746;
4470
- var execAsync = (0, import_node_util.promisify)(import_node_child_process9.exec);
4939
+ var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process10.exec);
4471
4940
  async function killPortProcess(port) {
4472
4941
  try {
4473
4942
  if (isWindows) {
4474
- const { stdout } = await execAsync(
4943
+ const { stdout } = await execAsync2(
4475
4944
  `netstat -ano | findstr :${port} | findstr LISTENING`
4476
4945
  );
4477
4946
  const pids = /* @__PURE__ */ new Set();
@@ -4481,14 +4950,14 @@ async function killPortProcess(port) {
4481
4950
  if (pid && /^\d+$/.test(pid) && pid !== "0") pids.add(pid);
4482
4951
  }
4483
4952
  for (const pid of pids) {
4484
- await execAsync(`taskkill /PID ${pid} /F`).catch(() => {
4953
+ await execAsync2(`taskkill /PID ${pid} /F`).catch(() => {
4485
4954
  });
4486
4955
  }
4487
4956
  } else {
4488
- const { stdout } = await execAsync(`lsof -ti :${port}`);
4957
+ const { stdout } = await execAsync2(`lsof -ti :${port}`);
4489
4958
  const pids = stdout.trim().split("\n").filter((p) => p && /^\d+$/.test(p));
4490
4959
  if (pids.length > 0) {
4491
- await execAsync(`kill -9 ${pids.join(" ")}`);
4960
+ await execAsync2(`kill -9 ${pids.join(" ")}`);
4492
4961
  }
4493
4962
  }
4494
4963
  await new Promise((resolve) => setTimeout(resolve, 600));
@@ -4509,8 +4978,8 @@ async function createWithRetry(label, port, factory) {
4509
4978
  }
4510
4979
  }
4511
4980
  async function start(opts = {}) {
4512
- const configDir = (0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix");
4513
- const tokenFile = (0, import_node_path6.join)(configDir, "token");
4981
+ const configDir = (0, import_node_path7.join)((0, import_node_os8.homedir)(), ".sessix");
4982
+ const tokenFile = (0, import_node_path7.join)(configDir, "token");
4514
4983
  let token;
4515
4984
  if (opts.token !== void 0) {
4516
4985
  token = opts.token;
@@ -4520,17 +4989,23 @@ async function start(opts = {}) {
4520
4989
  token = envToken;
4521
4990
  } else {
4522
4991
  try {
4523
- token = (await (0, import_promises4.readFile)(tokenFile, "utf8")).trim();
4992
+ token = (await (0, import_promises5.readFile)(tokenFile, "utf8")).trim();
4524
4993
  } catch {
4525
- token = (0, import_uuid6.v4)();
4526
- await (0, import_promises4.mkdir)(configDir, { recursive: true });
4527
- await (0, import_promises4.writeFile)(tokenFile, token, "utf8");
4994
+ token = (0, import_uuid7.v4)();
4995
+ await (0, import_promises5.mkdir)(configDir, { recursive: true });
4996
+ await (0, import_promises5.writeFile)(tokenFile, token, "utf8");
4528
4997
  }
4529
4998
  }
4530
4999
  }
4531
5000
  const providerFactory = new ProviderFactory();
4532
5001
  const sessionManager = new SessionManager(providerFactory);
4533
5002
  const terminalExecutor = new TerminalExecutor();
5003
+ const xcodeBuildExecutor = new XcodeBuildExecutor();
5004
+ const approvalProxy = await createWithRetry(
5005
+ "ApprovalProxy",
5006
+ HTTP_PORT,
5007
+ () => ApprovalProxy.create({ port: HTTP_PORT, token })
5008
+ );
4534
5009
  const wsBridge = await createWithRetry(
4535
5010
  "WsBridge",
4536
5011
  WS_PORT,
@@ -4561,15 +5036,10 @@ async function start(opts = {}) {
4561
5036
  const sessionFileWatcher = new SessionFileWatcher((event) => {
4562
5037
  wsBridge.broadcast(event);
4563
5038
  });
4564
- const approvalProxy = await createWithRetry(
4565
- "ApprovalProxy",
4566
- HTTP_PORT,
4567
- () => ApprovalProxy.create({ port: HTTP_PORT, token })
4568
- );
4569
5039
  let mdnsService = null;
4570
5040
  const pairingManager = new PairingManager({
4571
5041
  token,
4572
- serverName: (0, import_node_os7.hostname)(),
5042
+ serverName: (0, import_node_os8.hostname)(),
4573
5043
  version: "0.2.0",
4574
5044
  onStateChange: (state) => mdnsService?.updatePairingState(state)
4575
5045
  });
@@ -4623,7 +5093,7 @@ async function start(opts = {}) {
4623
5093
  try {
4624
5094
  switch (event.type) {
4625
5095
  case "create_session": {
4626
- await (0, import_promises4.mkdir)(event.projectPath, { recursive: true });
5096
+ await (0, import_promises5.mkdir)(event.projectPath, { recursive: true });
4627
5097
  const resumeId = event.resumeSessionId ?? event.newSessionId;
4628
5098
  if (resumeId) sessionFileWatcher.unwatch(resumeId);
4629
5099
  await sessionManager.createSession(
@@ -4811,7 +5281,7 @@ async function start(opts = {}) {
4811
5281
  if (!isStreaming) {
4812
5282
  const filePath = getSessionFilePath(event.projectPath, event.sessionId);
4813
5283
  try {
4814
- const fileStat = await (0, import_promises5.stat)(filePath);
5284
+ const fileStat = await (0, import_promises6.stat)(filePath);
4815
5285
  sessionFileWatcher.watch(event.sessionId, filePath, fileStat.size);
4816
5286
  } catch {
4817
5287
  }
@@ -4920,6 +5390,56 @@ async function start(opts = {}) {
4920
5390
  wsBridge.send(ws, { type: "agent_list", agents });
4921
5391
  break;
4922
5392
  }
5393
+ case "xcode_detect": {
5394
+ const info = await xcodeBuildExecutor.detect(event.projectPath);
5395
+ wsBridge.send(ws, { type: "xcode_info", sessionId: event.sessionId, info });
5396
+ break;
5397
+ }
5398
+ case "xcode_list_schemes": {
5399
+ const schemes = await xcodeBuildExecutor.getSchemesForContainer(event.projectPath, event.container);
5400
+ wsBridge.send(ws, {
5401
+ type: "xcode_info",
5402
+ sessionId: event.sessionId,
5403
+ info: {
5404
+ available: schemes.length > 0,
5405
+ containers: [event.container],
5406
+ schemes
5407
+ }
5408
+ });
5409
+ break;
5410
+ }
5411
+ case "xcode_list_destinations": {
5412
+ try {
5413
+ const destinations = await xcodeBuildExecutor.listDestinations(event.projectPath, event.scheme, event.container);
5414
+ wsBridge.send(ws, { type: "xcode_destinations", sessionId: event.sessionId, scheme: event.scheme, destinations });
5415
+ } catch (err) {
5416
+ const message = err instanceof Error ? err.message : String(err);
5417
+ wsBridge.send(ws, {
5418
+ type: "xcode_destinations",
5419
+ sessionId: event.sessionId,
5420
+ scheme: event.scheme,
5421
+ destinations: [],
5422
+ error: message.split("\n")[0]
5423
+ });
5424
+ }
5425
+ break;
5426
+ }
5427
+ case "xcode_save_config": {
5428
+ await xcodeBuildExecutor.saveConfig(event.projectPath, event.config);
5429
+ break;
5430
+ }
5431
+ case "xcode_build": {
5432
+ await xcodeBuildExecutor.build(event.sessionId, event.projectPath, event.config);
5433
+ break;
5434
+ }
5435
+ case "xcode_build_kill": {
5436
+ xcodeBuildExecutor.killBuild(event.buildId);
5437
+ break;
5438
+ }
5439
+ case "xcode_install": {
5440
+ await xcodeBuildExecutor.install(event.sessionId, event.projectPath);
5441
+ break;
5442
+ }
4923
5443
  default: {
4924
5444
  wsBridge.send(ws, {
4925
5445
  type: "error",
@@ -4955,11 +5475,21 @@ async function start(opts = {}) {
4955
5475
  terminalExecutor.onEvent((event) => {
4956
5476
  wsBridge.broadcast(event);
4957
5477
  });
5478
+ xcodeBuildExecutor.onEvent((event) => {
5479
+ wsBridge.broadcast(event);
5480
+ });
4958
5481
  wsBridge.onDisconnect(() => {
4959
5482
  if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
4960
5483
  approvalProxy.approveAll(t("server.phoneDisconnected"));
4961
5484
  }
4962
5485
  });
5486
+ approvalProxy.onApprovalResolved((requestId, decision) => {
5487
+ wsBridge.broadcast({
5488
+ type: "approval_resolved",
5489
+ requestId,
5490
+ decision: decision.decision
5491
+ });
5492
+ });
4963
5493
  approvalProxy.onApprovalRequest((request) => {
4964
5494
  wsBridge.broadcast({ type: "approval_request", request });
4965
5495
  setTimeout(() => {
@@ -5055,6 +5585,7 @@ async function start(opts = {}) {
5055
5585
  await attempt(() => approvalProxy.close(), "ApprovalProxy");
5056
5586
  await attempt(() => sessionManager.destroy(), "SessionManager");
5057
5587
  await attempt(() => terminalExecutor.destroy(), "TerminalExecutor");
5588
+ await attempt(() => xcodeBuildExecutor.destroy(), "XcodeBuildExecutor");
5058
5589
  await attempt(() => notificationService.destroy(), "NotificationService");
5059
5590
  await attempt(() => sessionFileWatcher.destroy(), "SessionFileWatcher");
5060
5591
  if (errors.length > 0) {
@@ -5083,9 +5614,9 @@ async function start(opts = {}) {
5083
5614
  openPairing: (duration) => pairingManager.open(duration),
5084
5615
  closePairing: () => pairingManager.close(),
5085
5616
  regenerateToken: async () => {
5086
- const newToken = (0, import_uuid6.v4)();
5087
- await (0, import_promises4.mkdir)(configDir, { recursive: true });
5088
- await (0, import_promises4.writeFile)(tokenFile, newToken, "utf8");
5617
+ const newToken = (0, import_uuid7.v4)();
5618
+ await (0, import_promises5.mkdir)(configDir, { recursive: true });
5619
+ await (0, import_promises5.writeFile)(tokenFile, newToken, "utf8");
5089
5620
  instance.token = newToken;
5090
5621
  wsBridge.updateToken(newToken);
5091
5622
  approvalProxy.updateToken(newToken);