sessix-server 0.3.9 → 0.4.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 +551 -46
- package/dist/server.js +545 -40
- 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
|
|
311
|
-
var
|
|
312
|
-
var
|
|
313
|
-
var
|
|
314
|
-
var
|
|
315
|
-
var
|
|
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");
|
|
@@ -1655,7 +1655,14 @@ var SessionManager = class {
|
|
|
1655
1655
|
runningStartedAt = /* @__PURE__ */ new Map();
|
|
1656
1656
|
/** assistant 事件合并缓冲区(30ms 窗口内的 assistant 事件合并为一次发送) */
|
|
1657
1657
|
pendingAssistantEvents = /* @__PURE__ */ new Map();
|
|
1658
|
-
/**
|
|
1658
|
+
/**
|
|
1659
|
+
* 标记哪些会话的缓冲区不能代表完整历史,需要从 JSONL 补全。
|
|
1660
|
+
* 两种情况会被标记:
|
|
1661
|
+
* 1. 缓冲区溢出过 BUFFER_MAX(旧事件被丢弃)
|
|
1662
|
+
* 2. 会话是通过 --resume 启动的(缓冲区只有恢复后的新事件,完整历史在 JSONL 中)
|
|
1663
|
+
* 例如:服务器重启后用户继续聊天,sendMessage 走 resume 路径再次创建会话,
|
|
1664
|
+
* 此时 buffer 只有 system init + 少量新事件,不能用它替换手机端已加载的完整 turns。
|
|
1665
|
+
*/
|
|
1659
1666
|
bufferTruncated = /* @__PURE__ */ new Set();
|
|
1660
1667
|
/** sessionId → projectPath 映射,用于截断时从 JSONL 补全历史 */
|
|
1661
1668
|
sessionProjectPaths = /* @__PURE__ */ new Map();
|
|
@@ -1713,6 +1720,9 @@ var SessionManager = class {
|
|
|
1713
1720
|
this.sessionAgentType.set(session.id, resolvedAgentType);
|
|
1714
1721
|
this.lastBroadcastStatus.set(session.id, session.status);
|
|
1715
1722
|
this.sessionProjectPaths.set(session.id, projectPath);
|
|
1723
|
+
if (resumeSessionId) {
|
|
1724
|
+
this.bufferTruncated.add(session.id);
|
|
1725
|
+
}
|
|
1716
1726
|
this.unsubscribeSession(session.id);
|
|
1717
1727
|
this.subscribeToSession(session.id);
|
|
1718
1728
|
console.log(`[SessionManager] Session created: ${session.id} (project: ${projectPath})`);
|
|
@@ -3131,9 +3141,26 @@ process.stdin.on('end', async () => {
|
|
|
3131
3141
|
signal: AbortSignal.timeout(320000),
|
|
3132
3142
|
})
|
|
3133
3143
|
const data = await res.json()
|
|
3134
|
-
|
|
3144
|
+
const decision = data.decision === 'deny' ? 'deny' : 'allow'
|
|
3145
|
+
const output = {
|
|
3146
|
+
hookSpecificOutput: {
|
|
3147
|
+
hookEventName: 'PreToolUse',
|
|
3148
|
+
permissionDecision: decision,
|
|
3149
|
+
},
|
|
3150
|
+
}
|
|
3151
|
+
if (decision === 'deny' && data.reason) {
|
|
3152
|
+
output.hookSpecificOutput.permissionDecisionReason = String(data.reason)
|
|
3153
|
+
}
|
|
3154
|
+
process.stdout.write(JSON.stringify(output))
|
|
3155
|
+
process.exit(0)
|
|
3135
3156
|
} catch {
|
|
3136
|
-
// Sessix \u670D\u52A1\u5668\u4E0D\u53EF\u7528\uFF0C\u9ED8\u8BA4\u653E\u884C
|
|
3157
|
+
// 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
|
|
3158
|
+
process.stdout.write(JSON.stringify({
|
|
3159
|
+
hookSpecificOutput: {
|
|
3160
|
+
hookEventName: 'PreToolUse',
|
|
3161
|
+
permissionDecision: 'allow',
|
|
3162
|
+
},
|
|
3163
|
+
}))
|
|
3137
3164
|
process.exit(0)
|
|
3138
3165
|
}
|
|
3139
3166
|
})
|
|
@@ -3176,15 +3203,19 @@ var HookInstaller = class {
|
|
|
3176
3203
|
console.log("[HookInstaller] Hook uninstalled");
|
|
3177
3204
|
}
|
|
3178
3205
|
/**
|
|
3179
|
-
* 检查 hook
|
|
3180
|
-
*
|
|
3206
|
+
* 检查 hook 是否已安装且为最新版本
|
|
3207
|
+
*
|
|
3208
|
+
* 必须同时满足:
|
|
3209
|
+
* 1. 两个脚本文件都存在
|
|
3210
|
+
* 2. settings.json 中有 Sessix hook 配置
|
|
3211
|
+
* 3. approval-hook.js 包含最新的 permissionDecision 输出协议
|
|
3212
|
+
* (旧版仅用 exit code,会导致 ExitPlanMode 等工具卡住)
|
|
3181
3213
|
*/
|
|
3182
3214
|
async isInstalled() {
|
|
3183
|
-
let
|
|
3215
|
+
let approvalScriptContent = "";
|
|
3184
3216
|
let permissionScriptExists = false;
|
|
3185
3217
|
try {
|
|
3186
|
-
await (0, import_promises2.
|
|
3187
|
-
approvalScriptExists = true;
|
|
3218
|
+
approvalScriptContent = await (0, import_promises2.readFile)(HOOK_SCRIPT_PATH, "utf-8");
|
|
3188
3219
|
} catch {
|
|
3189
3220
|
}
|
|
3190
3221
|
try {
|
|
@@ -3192,9 +3223,10 @@ var HookInstaller = class {
|
|
|
3192
3223
|
permissionScriptExists = true;
|
|
3193
3224
|
} catch {
|
|
3194
3225
|
}
|
|
3226
|
+
const isLatestVersion = approvalScriptContent.includes("permissionDecision");
|
|
3195
3227
|
const settings = await this.readClaudeSettings();
|
|
3196
3228
|
const configExists = this.hasHookConfig(settings);
|
|
3197
|
-
return
|
|
3229
|
+
return isLatestVersion && permissionScriptExists && configExists;
|
|
3198
3230
|
}
|
|
3199
3231
|
// ============================================
|
|
3200
3232
|
// 内部方法
|
|
@@ -4368,7 +4400,7 @@ var AuthManager = class extends import_events3.EventEmitter {
|
|
|
4368
4400
|
};
|
|
4369
4401
|
|
|
4370
4402
|
// src/server.ts
|
|
4371
|
-
var
|
|
4403
|
+
var import_promises6 = require("fs/promises");
|
|
4372
4404
|
|
|
4373
4405
|
// src/terminal/TerminalExecutor.ts
|
|
4374
4406
|
var import_node_child_process7 = require("child_process");
|
|
@@ -4457,8 +4489,422 @@ var TerminalExecutor = class {
|
|
|
4457
4489
|
}
|
|
4458
4490
|
};
|
|
4459
4491
|
|
|
4460
|
-
// src/
|
|
4492
|
+
// src/xcode/XcodeBuildExecutor.ts
|
|
4461
4493
|
var import_node_child_process8 = require("child_process");
|
|
4494
|
+
var import_node_util = require("util");
|
|
4495
|
+
var import_promises4 = require("fs/promises");
|
|
4496
|
+
var import_node_path6 = require("path");
|
|
4497
|
+
var import_node_os7 = require("os");
|
|
4498
|
+
var import_uuid6 = require("uuid");
|
|
4499
|
+
var execAsync = (0, import_node_util.promisify)(import_node_child_process8.exec);
|
|
4500
|
+
var BUILD_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
4501
|
+
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
4502
|
+
var CONFIG_FILE = (0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix", "xcode-config.json");
|
|
4503
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
4504
|
+
"node_modules",
|
|
4505
|
+
".git",
|
|
4506
|
+
"DerivedData",
|
|
4507
|
+
"Pods",
|
|
4508
|
+
".build",
|
|
4509
|
+
"build",
|
|
4510
|
+
"dist",
|
|
4511
|
+
"__pycache__",
|
|
4512
|
+
".next",
|
|
4513
|
+
"vendor",
|
|
4514
|
+
".expo",
|
|
4515
|
+
"android",
|
|
4516
|
+
".gradle",
|
|
4517
|
+
"Carthage",
|
|
4518
|
+
"xcarchive"
|
|
4519
|
+
]);
|
|
4520
|
+
var MAX_SCAN_DEPTH = 4;
|
|
4521
|
+
var XcodeBuildExecutor = class {
|
|
4522
|
+
builds = /* @__PURE__ */ new Map();
|
|
4523
|
+
installs = /* @__PURE__ */ new Map();
|
|
4524
|
+
eventCallbacks = [];
|
|
4525
|
+
configCache = null;
|
|
4526
|
+
onEvent(callback) {
|
|
4527
|
+
this.eventCallbacks.push(callback);
|
|
4528
|
+
return () => {
|
|
4529
|
+
const idx = this.eventCallbacks.indexOf(callback);
|
|
4530
|
+
if (idx !== -1) this.eventCallbacks.splice(idx, 1);
|
|
4531
|
+
};
|
|
4532
|
+
}
|
|
4533
|
+
emit(event) {
|
|
4534
|
+
for (const cb of this.eventCallbacks) {
|
|
4535
|
+
try {
|
|
4536
|
+
cb(event);
|
|
4537
|
+
} catch (err) {
|
|
4538
|
+
console.error("[XcodeBuildExecutor] Event callback error:", err);
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
}
|
|
4542
|
+
// ============================================
|
|
4543
|
+
// 配置持久化
|
|
4544
|
+
// ============================================
|
|
4545
|
+
async loadConfigs() {
|
|
4546
|
+
if (this.configCache) return this.configCache;
|
|
4547
|
+
try {
|
|
4548
|
+
const raw = await (0, import_promises4.readFile)(CONFIG_FILE, "utf8");
|
|
4549
|
+
this.configCache = JSON.parse(raw);
|
|
4550
|
+
} catch {
|
|
4551
|
+
this.configCache = {};
|
|
4552
|
+
}
|
|
4553
|
+
return this.configCache;
|
|
4554
|
+
}
|
|
4555
|
+
async writeConfigs(store) {
|
|
4556
|
+
await (0, import_promises4.mkdir)((0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix"), { recursive: true });
|
|
4557
|
+
await (0, import_promises4.writeFile)(CONFIG_FILE, JSON.stringify(store, null, 2), "utf8");
|
|
4558
|
+
this.configCache = store;
|
|
4559
|
+
}
|
|
4560
|
+
async getSavedConfig(projectPath) {
|
|
4561
|
+
const store = await this.loadConfigs();
|
|
4562
|
+
return store[projectPath];
|
|
4563
|
+
}
|
|
4564
|
+
async saveConfig(projectPath, config) {
|
|
4565
|
+
const store = await this.loadConfigs();
|
|
4566
|
+
store[projectPath] = config;
|
|
4567
|
+
await this.writeConfigs(store);
|
|
4568
|
+
}
|
|
4569
|
+
// ============================================
|
|
4570
|
+
// 递归扫描 Xcode 工程
|
|
4571
|
+
// ============================================
|
|
4572
|
+
async findAllContainers(projectPath) {
|
|
4573
|
+
const results = [];
|
|
4574
|
+
await this.scanDir(projectPath, projectPath, 0, results);
|
|
4575
|
+
results.sort((a, b) => a.path.split("/").length - b.path.split("/").length);
|
|
4576
|
+
return results;
|
|
4577
|
+
}
|
|
4578
|
+
async scanDir(rootPath, currentPath, depth, results) {
|
|
4579
|
+
if (depth > MAX_SCAN_DEPTH) return;
|
|
4580
|
+
let entries;
|
|
4581
|
+
try {
|
|
4582
|
+
entries = await (0, import_promises4.readdir)(currentPath);
|
|
4583
|
+
} catch {
|
|
4584
|
+
return;
|
|
4585
|
+
}
|
|
4586
|
+
let foundWorkspace;
|
|
4587
|
+
let foundProject;
|
|
4588
|
+
for (const name of entries) {
|
|
4589
|
+
if (name.endsWith(".xcworkspace") && !name.endsWith("project.xcworkspace")) {
|
|
4590
|
+
foundWorkspace = foundWorkspace ?? name;
|
|
4591
|
+
} else if (name.endsWith(".xcodeproj")) {
|
|
4592
|
+
foundProject = foundProject ?? name;
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
if (foundWorkspace || foundProject) {
|
|
4596
|
+
const relDir = currentPath === rootPath ? "" : currentPath.slice(rootPath.length + 1);
|
|
4597
|
+
const containerName = foundWorkspace ? foundWorkspace.replace(/\.xcworkspace$/, "") : foundProject.replace(/\.xcodeproj$/, "");
|
|
4598
|
+
results.push({
|
|
4599
|
+
path: relDir,
|
|
4600
|
+
name: containerName,
|
|
4601
|
+
workspace: foundWorkspace ? relDir ? `${relDir}/${foundWorkspace}` : foundWorkspace : void 0,
|
|
4602
|
+
project: foundProject ? relDir ? `${relDir}/${foundProject}` : foundProject : void 0
|
|
4603
|
+
});
|
|
4604
|
+
}
|
|
4605
|
+
for (const name of entries) {
|
|
4606
|
+
if (SKIP_DIRS.has(name)) continue;
|
|
4607
|
+
if (name.startsWith(".")) continue;
|
|
4608
|
+
if (name.endsWith(".xcodeproj") || name.endsWith(".xcworkspace")) continue;
|
|
4609
|
+
const childPath = (0, import_node_path6.join)(currentPath, name);
|
|
4610
|
+
await this.scanDir(rootPath, childPath, depth + 1, results);
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
// ============================================
|
|
4614
|
+
// 探测 schemes(针对指定 container)
|
|
4615
|
+
// ============================================
|
|
4616
|
+
async detect(projectPath) {
|
|
4617
|
+
if (process.platform !== "darwin") {
|
|
4618
|
+
return { available: false, error: "Xcode \u6784\u5EFA\u53EA\u652F\u6301 macOS", containers: [], schemes: [] };
|
|
4619
|
+
}
|
|
4620
|
+
const containers = await this.findAllContainers(projectPath);
|
|
4621
|
+
if (containers.length === 0) {
|
|
4622
|
+
return { available: false, error: "\u672A\u627E\u5230 .xcworkspace \u6216 .xcodeproj", containers: [], schemes: [] };
|
|
4623
|
+
}
|
|
4624
|
+
const firstContainer = containers[0];
|
|
4625
|
+
const schemes = await this.getSchemesForContainer(projectPath, firstContainer);
|
|
4626
|
+
const saved = await this.getSavedConfig(projectPath);
|
|
4627
|
+
return {
|
|
4628
|
+
available: true,
|
|
4629
|
+
containers,
|
|
4630
|
+
schemes,
|
|
4631
|
+
saved
|
|
4632
|
+
};
|
|
4633
|
+
}
|
|
4634
|
+
async getSchemesForContainer(projectPath, container) {
|
|
4635
|
+
const args = container.workspace ? ["-workspace", container.workspace, "-list", "-json"] : ["-project", container.project, "-list", "-json"];
|
|
4636
|
+
try {
|
|
4637
|
+
const { stdout } = await execAsync(
|
|
4638
|
+
`xcodebuild ${args.map(shellQuote).join(" ")}`,
|
|
4639
|
+
{ cwd: projectPath, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
|
|
4640
|
+
);
|
|
4641
|
+
const parsed = JSON.parse(stdout);
|
|
4642
|
+
return parsed.workspace?.schemes ?? parsed.project?.schemes ?? [];
|
|
4643
|
+
} catch {
|
|
4644
|
+
return [];
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
// ============================================
|
|
4648
|
+
// 列举 destinations
|
|
4649
|
+
// ============================================
|
|
4650
|
+
async listDestinations(projectPath, scheme, container) {
|
|
4651
|
+
if (process.platform !== "darwin") return [];
|
|
4652
|
+
const args = [
|
|
4653
|
+
...container.workspace ? ["-workspace", container.workspace] : ["-project", container.project],
|
|
4654
|
+
"-scheme",
|
|
4655
|
+
scheme,
|
|
4656
|
+
"-showdestinations"
|
|
4657
|
+
];
|
|
4658
|
+
try {
|
|
4659
|
+
const { stdout, stderr } = await execAsync(
|
|
4660
|
+
`xcodebuild ${args.map(shellQuote).join(" ")}`,
|
|
4661
|
+
{ cwd: projectPath, timeout: 6e4, maxBuffer: 4 * 1024 * 1024 }
|
|
4662
|
+
);
|
|
4663
|
+
return parseDestinations(stdout + "\n" + stderr);
|
|
4664
|
+
} catch (err) {
|
|
4665
|
+
const e = err;
|
|
4666
|
+
const parsed = parseDestinations(`${e.stdout ?? ""}
|
|
4667
|
+
${e.stderr ?? ""}`);
|
|
4668
|
+
if (parsed.length > 0) return parsed;
|
|
4669
|
+
throw err;
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
// ============================================
|
|
4673
|
+
// 构建
|
|
4674
|
+
// ============================================
|
|
4675
|
+
async build(sessionId, projectPath, override) {
|
|
4676
|
+
if (process.platform !== "darwin") {
|
|
4677
|
+
this.emitBuildError(sessionId, "", "Xcode \u6784\u5EFA\u4EC5\u652F\u6301 macOS\n");
|
|
4678
|
+
return null;
|
|
4679
|
+
}
|
|
4680
|
+
const config = override ?? await this.getSavedConfig(projectPath);
|
|
4681
|
+
if (!config) {
|
|
4682
|
+
this.emitBuildError(sessionId, "", "\u672A\u914D\u7F6E Xcode \u6784\u5EFA\u53C2\u6570\uFF0C\u8BF7\u5148\u9009\u62E9 scheme \u4E0E\u76EE\u6807\u8BBE\u5907\n");
|
|
4683
|
+
return null;
|
|
4684
|
+
}
|
|
4685
|
+
if (override) await this.saveConfig(projectPath, override);
|
|
4686
|
+
const buildId = (0, import_uuid6.v4)();
|
|
4687
|
+
const args = buildArgs(config);
|
|
4688
|
+
const proc = (0, import_node_child_process8.spawn)("xcodebuild", args, {
|
|
4689
|
+
cwd: projectPath,
|
|
4690
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4691
|
+
env: { ...process.env, NSUnbufferedIO: "YES" }
|
|
4692
|
+
});
|
|
4693
|
+
this.builds.set(buildId, proc);
|
|
4694
|
+
this.emit({
|
|
4695
|
+
type: "xcode_build_output",
|
|
4696
|
+
sessionId,
|
|
4697
|
+
buildId,
|
|
4698
|
+
stream: "stdout",
|
|
4699
|
+
data: `$ xcodebuild ${args.map((a) => a.includes(" ") ? `"${a}"` : a).join(" ")}
|
|
4700
|
+
cwd: ${projectPath}
|
|
4701
|
+
destination: ${config.destinationName}
|
|
4702
|
+
|
|
4703
|
+
`
|
|
4704
|
+
});
|
|
4705
|
+
proc.stdout?.on("data", (chunk) => {
|
|
4706
|
+
this.emit({ type: "xcode_build_output", sessionId, buildId, stream: "stdout", data: chunk.toString() });
|
|
4707
|
+
});
|
|
4708
|
+
proc.stderr?.on("data", (chunk) => {
|
|
4709
|
+
this.emit({ type: "xcode_build_output", sessionId, buildId, stream: "stderr", data: chunk.toString() });
|
|
4710
|
+
});
|
|
4711
|
+
proc.on("error", (err) => {
|
|
4712
|
+
this.emit({ type: "xcode_build_output", sessionId, buildId, stream: "stderr", data: `[spawn error] ${err.message}
|
|
4713
|
+
` });
|
|
4714
|
+
});
|
|
4715
|
+
const timer = setTimeout(() => {
|
|
4716
|
+
if (this.builds.has(buildId)) killProcessCrossPlatform(proc);
|
|
4717
|
+
}, BUILD_TIMEOUT_MS);
|
|
4718
|
+
proc.on("exit", (code, signal) => {
|
|
4719
|
+
clearTimeout(timer);
|
|
4720
|
+
this.builds.delete(buildId);
|
|
4721
|
+
this.emit({ type: "xcode_build_exit", sessionId, buildId, code, signal });
|
|
4722
|
+
});
|
|
4723
|
+
console.log(`[XcodeBuildExecutor] build ${buildId} scheme=${config.scheme} dest=${config.destinationName}`);
|
|
4724
|
+
return buildId;
|
|
4725
|
+
}
|
|
4726
|
+
killBuild(buildId) {
|
|
4727
|
+
const proc = this.builds.get(buildId);
|
|
4728
|
+
if (proc) {
|
|
4729
|
+
killProcessCrossPlatform(proc);
|
|
4730
|
+
console.log(`[XcodeBuildExecutor] kill build ${buildId}`);
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
emitBuildError(sessionId, buildId, msg) {
|
|
4734
|
+
this.emit({ type: "xcode_build_output", sessionId, buildId, stream: "stderr", data: msg });
|
|
4735
|
+
this.emit({ type: "xcode_build_exit", sessionId, buildId, code: 1, signal: null });
|
|
4736
|
+
}
|
|
4737
|
+
// ============================================
|
|
4738
|
+
// 安装
|
|
4739
|
+
// ============================================
|
|
4740
|
+
async install(sessionId, projectPath) {
|
|
4741
|
+
if (process.platform !== "darwin") {
|
|
4742
|
+
this.emitInstallError(sessionId, "", "Xcode \u5B89\u88C5\u4EC5\u652F\u6301 macOS\n");
|
|
4743
|
+
return null;
|
|
4744
|
+
}
|
|
4745
|
+
const config = await this.getSavedConfig(projectPath);
|
|
4746
|
+
if (!config) {
|
|
4747
|
+
this.emitInstallError(sessionId, "", "\u672A\u627E\u5230\u6784\u5EFA\u914D\u7F6E\uFF0C\u8BF7\u5148\u6784\u5EFA\u4E00\u6B21\n");
|
|
4748
|
+
return null;
|
|
4749
|
+
}
|
|
4750
|
+
const installId = (0, import_uuid6.v4)();
|
|
4751
|
+
let appPath;
|
|
4752
|
+
try {
|
|
4753
|
+
appPath = await this.getAppPath(projectPath, config);
|
|
4754
|
+
} catch (err) {
|
|
4755
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4756
|
+
this.emitInstallError(sessionId, installId, `\u65E0\u6CD5\u5B9A\u4F4D\u6784\u5EFA\u4EA7\u7269: ${msg}
|
|
4757
|
+
`);
|
|
4758
|
+
return null;
|
|
4759
|
+
}
|
|
4760
|
+
const { destinationKind, destinationId, destinationName } = config;
|
|
4761
|
+
let installCmd;
|
|
4762
|
+
if (destinationKind === "simulator") {
|
|
4763
|
+
installCmd = ["xcrun", "simctl", "install", destinationId, appPath];
|
|
4764
|
+
} else if (destinationKind === "device") {
|
|
4765
|
+
installCmd = ["xcrun", "devicectl", "device", "install", "app", "--device", destinationId, appPath];
|
|
4766
|
+
} else if (destinationKind === "mac") {
|
|
4767
|
+
installCmd = ["open", appPath];
|
|
4768
|
+
} else {
|
|
4769
|
+
this.emitInstallError(sessionId, installId, `\u672A\u77E5\u76EE\u6807\u7C7B\u578B\uFF0C\u65E0\u6CD5\u81EA\u52A8\u5B89\u88C5
|
|
4770
|
+
`);
|
|
4771
|
+
return null;
|
|
4772
|
+
}
|
|
4773
|
+
this.emit({
|
|
4774
|
+
type: "xcode_install_output",
|
|
4775
|
+
sessionId,
|
|
4776
|
+
installId,
|
|
4777
|
+
stream: "stdout",
|
|
4778
|
+
data: `$ ${installCmd.join(" ")}
|
|
4779
|
+
destination: ${destinationName}
|
|
4780
|
+
app: ${appPath}
|
|
4781
|
+
|
|
4782
|
+
`
|
|
4783
|
+
});
|
|
4784
|
+
const proc = (0, import_node_child_process8.spawn)(installCmd[0], installCmd.slice(1), {
|
|
4785
|
+
cwd: projectPath,
|
|
4786
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4787
|
+
});
|
|
4788
|
+
this.installs.set(installId, proc);
|
|
4789
|
+
proc.stdout?.on("data", (chunk) => {
|
|
4790
|
+
this.emit({ type: "xcode_install_output", sessionId, installId, stream: "stdout", data: chunk.toString() });
|
|
4791
|
+
});
|
|
4792
|
+
proc.stderr?.on("data", (chunk) => {
|
|
4793
|
+
this.emit({ type: "xcode_install_output", sessionId, installId, stream: "stderr", data: chunk.toString() });
|
|
4794
|
+
});
|
|
4795
|
+
proc.on("error", (err) => {
|
|
4796
|
+
this.emit({ type: "xcode_install_output", sessionId, installId, stream: "stderr", data: `[spawn error] ${err.message}
|
|
4797
|
+
` });
|
|
4798
|
+
});
|
|
4799
|
+
const timer = setTimeout(() => {
|
|
4800
|
+
if (this.installs.has(installId)) killProcessCrossPlatform(proc);
|
|
4801
|
+
}, INSTALL_TIMEOUT_MS);
|
|
4802
|
+
proc.on("exit", (code, signal) => {
|
|
4803
|
+
clearTimeout(timer);
|
|
4804
|
+
this.installs.delete(installId);
|
|
4805
|
+
this.emit({ type: "xcode_install_exit", sessionId, installId, code, signal });
|
|
4806
|
+
});
|
|
4807
|
+
console.log(`[XcodeBuildExecutor] install ${installId} dest=${destinationName} kind=${destinationKind}`);
|
|
4808
|
+
return installId;
|
|
4809
|
+
}
|
|
4810
|
+
killInstall(installId) {
|
|
4811
|
+
const proc = this.installs.get(installId);
|
|
4812
|
+
if (proc) {
|
|
4813
|
+
killProcessCrossPlatform(proc);
|
|
4814
|
+
console.log(`[XcodeBuildExecutor] kill install ${installId}`);
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
emitInstallError(sessionId, installId, msg) {
|
|
4818
|
+
this.emit({ type: "xcode_install_output", sessionId, installId, stream: "stderr", data: msg });
|
|
4819
|
+
this.emit({ type: "xcode_install_exit", sessionId, installId, code: 1, signal: null });
|
|
4820
|
+
}
|
|
4821
|
+
/** 通过 xcodebuild -showBuildSettings 定位 .app 路径 */
|
|
4822
|
+
async getAppPath(projectPath, config) {
|
|
4823
|
+
const args = [
|
|
4824
|
+
...buildArgs(config).filter((a) => a !== "build"),
|
|
4825
|
+
"-showBuildSettings"
|
|
4826
|
+
];
|
|
4827
|
+
const { stdout } = await execAsync(
|
|
4828
|
+
`xcodebuild ${args.map(shellQuote).join(" ")}`,
|
|
4829
|
+
{ cwd: projectPath, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
|
|
4830
|
+
);
|
|
4831
|
+
const builtDir = extractBuildSetting(stdout, "BUILT_PRODUCTS_DIR");
|
|
4832
|
+
const productName = extractBuildSetting(stdout, "FULL_PRODUCT_NAME");
|
|
4833
|
+
if (!builtDir || !productName) {
|
|
4834
|
+
throw new Error("\u65E0\u6CD5\u4ECE -showBuildSettings \u4E2D\u8BFB\u53D6 BUILT_PRODUCTS_DIR / FULL_PRODUCT_NAME");
|
|
4835
|
+
}
|
|
4836
|
+
return (0, import_node_path6.join)(builtDir, productName);
|
|
4837
|
+
}
|
|
4838
|
+
// ============================================
|
|
4839
|
+
// 清理
|
|
4840
|
+
// ============================================
|
|
4841
|
+
destroy() {
|
|
4842
|
+
for (const [id, proc] of this.builds) {
|
|
4843
|
+
killProcessCrossPlatform(proc);
|
|
4844
|
+
console.log(`[XcodeBuildExecutor] cleanup build ${id}`);
|
|
4845
|
+
}
|
|
4846
|
+
for (const [id, proc] of this.installs) {
|
|
4847
|
+
killProcessCrossPlatform(proc);
|
|
4848
|
+
console.log(`[XcodeBuildExecutor] cleanup install ${id}`);
|
|
4849
|
+
}
|
|
4850
|
+
this.builds.clear();
|
|
4851
|
+
this.installs.clear();
|
|
4852
|
+
this.eventCallbacks.length = 0;
|
|
4853
|
+
}
|
|
4854
|
+
};
|
|
4855
|
+
function shellQuote(s) {
|
|
4856
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
4857
|
+
}
|
|
4858
|
+
function buildArgs(config) {
|
|
4859
|
+
const container = config.workspace ? ["-workspace", config.workspace] : config.project ? ["-project", config.project] : [];
|
|
4860
|
+
return [
|
|
4861
|
+
...container,
|
|
4862
|
+
"-scheme",
|
|
4863
|
+
config.scheme,
|
|
4864
|
+
"-destination",
|
|
4865
|
+
`id=${config.destinationId}`,
|
|
4866
|
+
"-configuration",
|
|
4867
|
+
config.configuration ?? "Debug",
|
|
4868
|
+
"build"
|
|
4869
|
+
];
|
|
4870
|
+
}
|
|
4871
|
+
function extractBuildSetting(output, key) {
|
|
4872
|
+
const match = new RegExp(`^\\s*${key}\\s*=\\s*(.+)$`, "m").exec(output);
|
|
4873
|
+
return match?.[1]?.trim();
|
|
4874
|
+
}
|
|
4875
|
+
function parseDestinations(text) {
|
|
4876
|
+
const results = [];
|
|
4877
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4878
|
+
const lineRegex = /\{\s*([^{}]+)\s*\}/g;
|
|
4879
|
+
let match;
|
|
4880
|
+
while ((match = lineRegex.exec(text)) !== null) {
|
|
4881
|
+
const fields = {};
|
|
4882
|
+
for (const part of match[1].split(",")) {
|
|
4883
|
+
const colon = part.indexOf(":");
|
|
4884
|
+
if (colon === -1) continue;
|
|
4885
|
+
const key = part.slice(0, colon).trim();
|
|
4886
|
+
const value = part.slice(colon + 1).trim();
|
|
4887
|
+
if (key) fields[key] = value;
|
|
4888
|
+
}
|
|
4889
|
+
const { id, name, platform: platform2 } = fields;
|
|
4890
|
+
if (!id || !name || !platform2) continue;
|
|
4891
|
+
if (seen.has(id)) continue;
|
|
4892
|
+
seen.add(id);
|
|
4893
|
+
let kind = "unknown";
|
|
4894
|
+
if (platform2.includes("Simulator")) kind = "simulator";
|
|
4895
|
+
else if (platform2 === "iOS" || platform2 === "watchOS" || platform2 === "tvOS" || platform2 === "visionOS") kind = "device";
|
|
4896
|
+
else if (platform2 === "macOS") kind = "mac";
|
|
4897
|
+
results.push({ id, name, platform: platform2, os: fields.OS, kind });
|
|
4898
|
+
}
|
|
4899
|
+
results.sort((a, b) => kindOrder(a.kind) - kindOrder(b.kind));
|
|
4900
|
+
return results;
|
|
4901
|
+
}
|
|
4902
|
+
function kindOrder(k) {
|
|
4903
|
+
return k === "device" ? 0 : k === "simulator" ? 1 : k === "mac" ? 2 : 3;
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
// src/utils/cliCapabilities.ts
|
|
4907
|
+
var import_node_child_process9 = require("child_process");
|
|
4462
4908
|
var DEFAULT_CAPABILITIES = {
|
|
4463
4909
|
effortLevels: ["low", "medium", "high", "xhigh", "max"]
|
|
4464
4910
|
};
|
|
@@ -4486,7 +4932,7 @@ async function parseCliCapabilities() {
|
|
|
4486
4932
|
}
|
|
4487
4933
|
function runCli(path2, args) {
|
|
4488
4934
|
return new Promise((resolve) => {
|
|
4489
|
-
(0,
|
|
4935
|
+
(0, import_node_child_process9.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
|
|
4490
4936
|
if (err) {
|
|
4491
4937
|
console.warn(`[CliCapabilities] Failed to run ${path2} ${args.join(" ")}:`, err.message);
|
|
4492
4938
|
resolve(null);
|
|
@@ -4500,11 +4946,11 @@ function runCli(path2, args) {
|
|
|
4500
4946
|
// src/server.ts
|
|
4501
4947
|
var WS_PORT = 3745;
|
|
4502
4948
|
var HTTP_PORT = 3746;
|
|
4503
|
-
var
|
|
4949
|
+
var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process10.exec);
|
|
4504
4950
|
async function killPortProcess(port) {
|
|
4505
4951
|
try {
|
|
4506
4952
|
if (isWindows) {
|
|
4507
|
-
const { stdout } = await
|
|
4953
|
+
const { stdout } = await execAsync2(
|
|
4508
4954
|
`netstat -ano | findstr :${port} | findstr LISTENING`
|
|
4509
4955
|
);
|
|
4510
4956
|
const pids = /* @__PURE__ */ new Set();
|
|
@@ -4514,14 +4960,14 @@ async function killPortProcess(port) {
|
|
|
4514
4960
|
if (pid && /^\d+$/.test(pid) && pid !== "0") pids.add(pid);
|
|
4515
4961
|
}
|
|
4516
4962
|
for (const pid of pids) {
|
|
4517
|
-
await
|
|
4963
|
+
await execAsync2(`taskkill /PID ${pid} /F`).catch(() => {
|
|
4518
4964
|
});
|
|
4519
4965
|
}
|
|
4520
4966
|
} else {
|
|
4521
|
-
const { stdout } = await
|
|
4967
|
+
const { stdout } = await execAsync2(`lsof -ti :${port}`);
|
|
4522
4968
|
const pids = stdout.trim().split("\n").filter((p) => p && /^\d+$/.test(p));
|
|
4523
4969
|
if (pids.length > 0) {
|
|
4524
|
-
await
|
|
4970
|
+
await execAsync2(`kill -9 ${pids.join(" ")}`);
|
|
4525
4971
|
}
|
|
4526
4972
|
}
|
|
4527
4973
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
@@ -4542,8 +4988,8 @@ async function createWithRetry(label, port, factory) {
|
|
|
4542
4988
|
}
|
|
4543
4989
|
}
|
|
4544
4990
|
async function start(opts = {}) {
|
|
4545
|
-
const configDir = (0,
|
|
4546
|
-
const tokenFile = (0,
|
|
4991
|
+
const configDir = (0, import_node_path7.join)((0, import_node_os8.homedir)(), ".sessix");
|
|
4992
|
+
const tokenFile = (0, import_node_path7.join)(configDir, "token");
|
|
4547
4993
|
let token;
|
|
4548
4994
|
if (opts.token !== void 0) {
|
|
4549
4995
|
token = opts.token;
|
|
@@ -4553,17 +4999,23 @@ async function start(opts = {}) {
|
|
|
4553
4999
|
token = envToken;
|
|
4554
5000
|
} else {
|
|
4555
5001
|
try {
|
|
4556
|
-
token = (await (0,
|
|
5002
|
+
token = (await (0, import_promises5.readFile)(tokenFile, "utf8")).trim();
|
|
4557
5003
|
} catch {
|
|
4558
|
-
token = (0,
|
|
4559
|
-
await (0,
|
|
4560
|
-
await (0,
|
|
5004
|
+
token = (0, import_uuid7.v4)();
|
|
5005
|
+
await (0, import_promises5.mkdir)(configDir, { recursive: true });
|
|
5006
|
+
await (0, import_promises5.writeFile)(tokenFile, token, "utf8");
|
|
4561
5007
|
}
|
|
4562
5008
|
}
|
|
4563
5009
|
}
|
|
4564
5010
|
const providerFactory = new ProviderFactory();
|
|
4565
5011
|
const sessionManager = new SessionManager(providerFactory);
|
|
4566
5012
|
const terminalExecutor = new TerminalExecutor();
|
|
5013
|
+
const xcodeBuildExecutor = new XcodeBuildExecutor();
|
|
5014
|
+
const approvalProxy = await createWithRetry(
|
|
5015
|
+
"ApprovalProxy",
|
|
5016
|
+
HTTP_PORT,
|
|
5017
|
+
() => ApprovalProxy.create({ port: HTTP_PORT, token })
|
|
5018
|
+
);
|
|
4567
5019
|
const wsBridge = await createWithRetry(
|
|
4568
5020
|
"WsBridge",
|
|
4569
5021
|
WS_PORT,
|
|
@@ -4594,15 +5046,10 @@ async function start(opts = {}) {
|
|
|
4594
5046
|
const sessionFileWatcher = new SessionFileWatcher((event) => {
|
|
4595
5047
|
wsBridge.broadcast(event);
|
|
4596
5048
|
});
|
|
4597
|
-
const approvalProxy = await createWithRetry(
|
|
4598
|
-
"ApprovalProxy",
|
|
4599
|
-
HTTP_PORT,
|
|
4600
|
-
() => ApprovalProxy.create({ port: HTTP_PORT, token })
|
|
4601
|
-
);
|
|
4602
5049
|
let mdnsService = null;
|
|
4603
5050
|
const pairingManager = new PairingManager({
|
|
4604
5051
|
token,
|
|
4605
|
-
serverName: (0,
|
|
5052
|
+
serverName: (0, import_node_os8.hostname)(),
|
|
4606
5053
|
version: "0.2.0",
|
|
4607
5054
|
onStateChange: (state) => mdnsService?.updatePairingState(state)
|
|
4608
5055
|
});
|
|
@@ -4656,7 +5103,7 @@ async function start(opts = {}) {
|
|
|
4656
5103
|
try {
|
|
4657
5104
|
switch (event.type) {
|
|
4658
5105
|
case "create_session": {
|
|
4659
|
-
await (0,
|
|
5106
|
+
await (0, import_promises5.mkdir)(event.projectPath, { recursive: true });
|
|
4660
5107
|
const resumeId = event.resumeSessionId ?? event.newSessionId;
|
|
4661
5108
|
if (resumeId) sessionFileWatcher.unwatch(resumeId);
|
|
4662
5109
|
await sessionManager.createSession(
|
|
@@ -4844,7 +5291,7 @@ async function start(opts = {}) {
|
|
|
4844
5291
|
if (!isStreaming) {
|
|
4845
5292
|
const filePath = getSessionFilePath(event.projectPath, event.sessionId);
|
|
4846
5293
|
try {
|
|
4847
|
-
const fileStat = await (0,
|
|
5294
|
+
const fileStat = await (0, import_promises6.stat)(filePath);
|
|
4848
5295
|
sessionFileWatcher.watch(event.sessionId, filePath, fileStat.size);
|
|
4849
5296
|
} catch {
|
|
4850
5297
|
}
|
|
@@ -4953,6 +5400,60 @@ async function start(opts = {}) {
|
|
|
4953
5400
|
wsBridge.send(ws, { type: "agent_list", agents });
|
|
4954
5401
|
break;
|
|
4955
5402
|
}
|
|
5403
|
+
case "xcode_detect": {
|
|
5404
|
+
const info = await xcodeBuildExecutor.detect(event.projectPath);
|
|
5405
|
+
wsBridge.send(ws, { type: "xcode_info", sessionId: event.sessionId, info });
|
|
5406
|
+
break;
|
|
5407
|
+
}
|
|
5408
|
+
case "xcode_list_schemes": {
|
|
5409
|
+
const schemes = await xcodeBuildExecutor.getSchemesForContainer(event.projectPath, event.container);
|
|
5410
|
+
wsBridge.send(ws, {
|
|
5411
|
+
type: "xcode_info",
|
|
5412
|
+
sessionId: event.sessionId,
|
|
5413
|
+
info: {
|
|
5414
|
+
available: schemes.length > 0,
|
|
5415
|
+
containers: [event.container],
|
|
5416
|
+
schemes
|
|
5417
|
+
}
|
|
5418
|
+
});
|
|
5419
|
+
break;
|
|
5420
|
+
}
|
|
5421
|
+
case "xcode_list_destinations": {
|
|
5422
|
+
try {
|
|
5423
|
+
const destinations = await xcodeBuildExecutor.listDestinations(event.projectPath, event.scheme, event.container);
|
|
5424
|
+
wsBridge.send(ws, { type: "xcode_destinations", sessionId: event.sessionId, scheme: event.scheme, destinations });
|
|
5425
|
+
} catch (err) {
|
|
5426
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5427
|
+
wsBridge.send(ws, {
|
|
5428
|
+
type: "xcode_destinations",
|
|
5429
|
+
sessionId: event.sessionId,
|
|
5430
|
+
scheme: event.scheme,
|
|
5431
|
+
destinations: [],
|
|
5432
|
+
error: message.split("\n")[0]
|
|
5433
|
+
});
|
|
5434
|
+
}
|
|
5435
|
+
break;
|
|
5436
|
+
}
|
|
5437
|
+
case "xcode_save_config": {
|
|
5438
|
+
await xcodeBuildExecutor.saveConfig(event.projectPath, event.config);
|
|
5439
|
+
break;
|
|
5440
|
+
}
|
|
5441
|
+
case "xcode_build": {
|
|
5442
|
+
await xcodeBuildExecutor.build(event.sessionId, event.projectPath, event.config);
|
|
5443
|
+
break;
|
|
5444
|
+
}
|
|
5445
|
+
case "xcode_build_kill": {
|
|
5446
|
+
xcodeBuildExecutor.killBuild(event.buildId);
|
|
5447
|
+
break;
|
|
5448
|
+
}
|
|
5449
|
+
case "xcode_install": {
|
|
5450
|
+
await xcodeBuildExecutor.install(event.sessionId, event.projectPath);
|
|
5451
|
+
break;
|
|
5452
|
+
}
|
|
5453
|
+
case "xcode_install_kill": {
|
|
5454
|
+
xcodeBuildExecutor.killInstall(event.installId);
|
|
5455
|
+
break;
|
|
5456
|
+
}
|
|
4956
5457
|
default: {
|
|
4957
5458
|
wsBridge.send(ws, {
|
|
4958
5459
|
type: "error",
|
|
@@ -4988,6 +5489,9 @@ async function start(opts = {}) {
|
|
|
4988
5489
|
terminalExecutor.onEvent((event) => {
|
|
4989
5490
|
wsBridge.broadcast(event);
|
|
4990
5491
|
});
|
|
5492
|
+
xcodeBuildExecutor.onEvent((event) => {
|
|
5493
|
+
wsBridge.broadcast(event);
|
|
5494
|
+
});
|
|
4991
5495
|
wsBridge.onDisconnect(() => {
|
|
4992
5496
|
if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
|
|
4993
5497
|
approvalProxy.approveAll(t("server.phoneDisconnected"));
|
|
@@ -5095,6 +5599,7 @@ async function start(opts = {}) {
|
|
|
5095
5599
|
await attempt(() => approvalProxy.close(), "ApprovalProxy");
|
|
5096
5600
|
await attempt(() => sessionManager.destroy(), "SessionManager");
|
|
5097
5601
|
await attempt(() => terminalExecutor.destroy(), "TerminalExecutor");
|
|
5602
|
+
await attempt(() => xcodeBuildExecutor.destroy(), "XcodeBuildExecutor");
|
|
5098
5603
|
await attempt(() => notificationService.destroy(), "NotificationService");
|
|
5099
5604
|
await attempt(() => sessionFileWatcher.destroy(), "SessionFileWatcher");
|
|
5100
5605
|
if (errors.length > 0) {
|
|
@@ -5123,9 +5628,9 @@ async function start(opts = {}) {
|
|
|
5123
5628
|
openPairing: (duration) => pairingManager.open(duration),
|
|
5124
5629
|
closePairing: () => pairingManager.close(),
|
|
5125
5630
|
regenerateToken: async () => {
|
|
5126
|
-
const newToken = (0,
|
|
5127
|
-
await (0,
|
|
5128
|
-
await (0,
|
|
5631
|
+
const newToken = (0, import_uuid7.v4)();
|
|
5632
|
+
await (0, import_promises5.mkdir)(configDir, { recursive: true });
|
|
5633
|
+
await (0, import_promises5.writeFile)(tokenFile, newToken, "utf8");
|
|
5129
5634
|
instance.token = newToken;
|
|
5130
5635
|
wsBridge.updateToken(newToken);
|
|
5131
5636
|
approvalProxy.updateToken(newToken);
|