sessix-server 0.3.9 → 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.
- package/dist/index.js +536 -45
- package/dist/server.js +530 -39
- 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");
|
|
@@ -3131,9 +3131,26 @@ process.stdin.on('end', async () => {
|
|
|
3131
3131
|
signal: AbortSignal.timeout(320000),
|
|
3132
3132
|
})
|
|
3133
3133
|
const data = await res.json()
|
|
3134
|
-
|
|
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)
|
|
3135
3146
|
} catch {
|
|
3136
|
-
// 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
|
+
}))
|
|
3137
3154
|
process.exit(0)
|
|
3138
3155
|
}
|
|
3139
3156
|
})
|
|
@@ -3176,15 +3193,19 @@ var HookInstaller = class {
|
|
|
3176
3193
|
console.log("[HookInstaller] Hook uninstalled");
|
|
3177
3194
|
}
|
|
3178
3195
|
/**
|
|
3179
|
-
* 检查 hook
|
|
3180
|
-
*
|
|
3196
|
+
* 检查 hook 是否已安装且为最新版本
|
|
3197
|
+
*
|
|
3198
|
+
* 必须同时满足:
|
|
3199
|
+
* 1. 两个脚本文件都存在
|
|
3200
|
+
* 2. settings.json 中有 Sessix hook 配置
|
|
3201
|
+
* 3. approval-hook.js 包含最新的 permissionDecision 输出协议
|
|
3202
|
+
* (旧版仅用 exit code,会导致 ExitPlanMode 等工具卡住)
|
|
3181
3203
|
*/
|
|
3182
3204
|
async isInstalled() {
|
|
3183
|
-
let
|
|
3205
|
+
let approvalScriptContent = "";
|
|
3184
3206
|
let permissionScriptExists = false;
|
|
3185
3207
|
try {
|
|
3186
|
-
await (0, import_promises2.
|
|
3187
|
-
approvalScriptExists = true;
|
|
3208
|
+
approvalScriptContent = await (0, import_promises2.readFile)(HOOK_SCRIPT_PATH, "utf-8");
|
|
3188
3209
|
} catch {
|
|
3189
3210
|
}
|
|
3190
3211
|
try {
|
|
@@ -3192,9 +3213,10 @@ var HookInstaller = class {
|
|
|
3192
3213
|
permissionScriptExists = true;
|
|
3193
3214
|
} catch {
|
|
3194
3215
|
}
|
|
3216
|
+
const isLatestVersion = approvalScriptContent.includes("permissionDecision");
|
|
3195
3217
|
const settings = await this.readClaudeSettings();
|
|
3196
3218
|
const configExists = this.hasHookConfig(settings);
|
|
3197
|
-
return
|
|
3219
|
+
return isLatestVersion && permissionScriptExists && configExists;
|
|
3198
3220
|
}
|
|
3199
3221
|
// ============================================
|
|
3200
3222
|
// 内部方法
|
|
@@ -4368,7 +4390,7 @@ var AuthManager = class extends import_events3.EventEmitter {
|
|
|
4368
4390
|
};
|
|
4369
4391
|
|
|
4370
4392
|
// src/server.ts
|
|
4371
|
-
var
|
|
4393
|
+
var import_promises6 = require("fs/promises");
|
|
4372
4394
|
|
|
4373
4395
|
// src/terminal/TerminalExecutor.ts
|
|
4374
4396
|
var import_node_child_process7 = require("child_process");
|
|
@@ -4457,8 +4479,422 @@ var TerminalExecutor = class {
|
|
|
4457
4479
|
}
|
|
4458
4480
|
};
|
|
4459
4481
|
|
|
4460
|
-
// src/
|
|
4482
|
+
// src/xcode/XcodeBuildExecutor.ts
|
|
4461
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");
|
|
4462
4898
|
var DEFAULT_CAPABILITIES = {
|
|
4463
4899
|
effortLevels: ["low", "medium", "high", "xhigh", "max"]
|
|
4464
4900
|
};
|
|
@@ -4486,7 +4922,7 @@ async function parseCliCapabilities() {
|
|
|
4486
4922
|
}
|
|
4487
4923
|
function runCli(path2, args) {
|
|
4488
4924
|
return new Promise((resolve) => {
|
|
4489
|
-
(0,
|
|
4925
|
+
(0, import_node_child_process9.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
|
|
4490
4926
|
if (err) {
|
|
4491
4927
|
console.warn(`[CliCapabilities] Failed to run ${path2} ${args.join(" ")}:`, err.message);
|
|
4492
4928
|
resolve(null);
|
|
@@ -4500,11 +4936,11 @@ function runCli(path2, args) {
|
|
|
4500
4936
|
// src/server.ts
|
|
4501
4937
|
var WS_PORT = 3745;
|
|
4502
4938
|
var HTTP_PORT = 3746;
|
|
4503
|
-
var
|
|
4939
|
+
var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process10.exec);
|
|
4504
4940
|
async function killPortProcess(port) {
|
|
4505
4941
|
try {
|
|
4506
4942
|
if (isWindows) {
|
|
4507
|
-
const { stdout } = await
|
|
4943
|
+
const { stdout } = await execAsync2(
|
|
4508
4944
|
`netstat -ano | findstr :${port} | findstr LISTENING`
|
|
4509
4945
|
);
|
|
4510
4946
|
const pids = /* @__PURE__ */ new Set();
|
|
@@ -4514,14 +4950,14 @@ async function killPortProcess(port) {
|
|
|
4514
4950
|
if (pid && /^\d+$/.test(pid) && pid !== "0") pids.add(pid);
|
|
4515
4951
|
}
|
|
4516
4952
|
for (const pid of pids) {
|
|
4517
|
-
await
|
|
4953
|
+
await execAsync2(`taskkill /PID ${pid} /F`).catch(() => {
|
|
4518
4954
|
});
|
|
4519
4955
|
}
|
|
4520
4956
|
} else {
|
|
4521
|
-
const { stdout } = await
|
|
4957
|
+
const { stdout } = await execAsync2(`lsof -ti :${port}`);
|
|
4522
4958
|
const pids = stdout.trim().split("\n").filter((p) => p && /^\d+$/.test(p));
|
|
4523
4959
|
if (pids.length > 0) {
|
|
4524
|
-
await
|
|
4960
|
+
await execAsync2(`kill -9 ${pids.join(" ")}`);
|
|
4525
4961
|
}
|
|
4526
4962
|
}
|
|
4527
4963
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
@@ -4542,8 +4978,8 @@ async function createWithRetry(label, port, factory) {
|
|
|
4542
4978
|
}
|
|
4543
4979
|
}
|
|
4544
4980
|
async function start(opts = {}) {
|
|
4545
|
-
const configDir = (0,
|
|
4546
|
-
const tokenFile = (0,
|
|
4981
|
+
const configDir = (0, import_node_path7.join)((0, import_node_os8.homedir)(), ".sessix");
|
|
4982
|
+
const tokenFile = (0, import_node_path7.join)(configDir, "token");
|
|
4547
4983
|
let token;
|
|
4548
4984
|
if (opts.token !== void 0) {
|
|
4549
4985
|
token = opts.token;
|
|
@@ -4553,17 +4989,23 @@ async function start(opts = {}) {
|
|
|
4553
4989
|
token = envToken;
|
|
4554
4990
|
} else {
|
|
4555
4991
|
try {
|
|
4556
|
-
token = (await (0,
|
|
4992
|
+
token = (await (0, import_promises5.readFile)(tokenFile, "utf8")).trim();
|
|
4557
4993
|
} catch {
|
|
4558
|
-
token = (0,
|
|
4559
|
-
await (0,
|
|
4560
|
-
await (0,
|
|
4994
|
+
token = (0, import_uuid7.v4)();
|
|
4995
|
+
await (0, import_promises5.mkdir)(configDir, { recursive: true });
|
|
4996
|
+
await (0, import_promises5.writeFile)(tokenFile, token, "utf8");
|
|
4561
4997
|
}
|
|
4562
4998
|
}
|
|
4563
4999
|
}
|
|
4564
5000
|
const providerFactory = new ProviderFactory();
|
|
4565
5001
|
const sessionManager = new SessionManager(providerFactory);
|
|
4566
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
|
+
);
|
|
4567
5009
|
const wsBridge = await createWithRetry(
|
|
4568
5010
|
"WsBridge",
|
|
4569
5011
|
WS_PORT,
|
|
@@ -4594,15 +5036,10 @@ async function start(opts = {}) {
|
|
|
4594
5036
|
const sessionFileWatcher = new SessionFileWatcher((event) => {
|
|
4595
5037
|
wsBridge.broadcast(event);
|
|
4596
5038
|
});
|
|
4597
|
-
const approvalProxy = await createWithRetry(
|
|
4598
|
-
"ApprovalProxy",
|
|
4599
|
-
HTTP_PORT,
|
|
4600
|
-
() => ApprovalProxy.create({ port: HTTP_PORT, token })
|
|
4601
|
-
);
|
|
4602
5039
|
let mdnsService = null;
|
|
4603
5040
|
const pairingManager = new PairingManager({
|
|
4604
5041
|
token,
|
|
4605
|
-
serverName: (0,
|
|
5042
|
+
serverName: (0, import_node_os8.hostname)(),
|
|
4606
5043
|
version: "0.2.0",
|
|
4607
5044
|
onStateChange: (state) => mdnsService?.updatePairingState(state)
|
|
4608
5045
|
});
|
|
@@ -4656,7 +5093,7 @@ async function start(opts = {}) {
|
|
|
4656
5093
|
try {
|
|
4657
5094
|
switch (event.type) {
|
|
4658
5095
|
case "create_session": {
|
|
4659
|
-
await (0,
|
|
5096
|
+
await (0, import_promises5.mkdir)(event.projectPath, { recursive: true });
|
|
4660
5097
|
const resumeId = event.resumeSessionId ?? event.newSessionId;
|
|
4661
5098
|
if (resumeId) sessionFileWatcher.unwatch(resumeId);
|
|
4662
5099
|
await sessionManager.createSession(
|
|
@@ -4844,7 +5281,7 @@ async function start(opts = {}) {
|
|
|
4844
5281
|
if (!isStreaming) {
|
|
4845
5282
|
const filePath = getSessionFilePath(event.projectPath, event.sessionId);
|
|
4846
5283
|
try {
|
|
4847
|
-
const fileStat = await (0,
|
|
5284
|
+
const fileStat = await (0, import_promises6.stat)(filePath);
|
|
4848
5285
|
sessionFileWatcher.watch(event.sessionId, filePath, fileStat.size);
|
|
4849
5286
|
} catch {
|
|
4850
5287
|
}
|
|
@@ -4953,6 +5390,56 @@ async function start(opts = {}) {
|
|
|
4953
5390
|
wsBridge.send(ws, { type: "agent_list", agents });
|
|
4954
5391
|
break;
|
|
4955
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
|
+
}
|
|
4956
5443
|
default: {
|
|
4957
5444
|
wsBridge.send(ws, {
|
|
4958
5445
|
type: "error",
|
|
@@ -4988,6 +5475,9 @@ async function start(opts = {}) {
|
|
|
4988
5475
|
terminalExecutor.onEvent((event) => {
|
|
4989
5476
|
wsBridge.broadcast(event);
|
|
4990
5477
|
});
|
|
5478
|
+
xcodeBuildExecutor.onEvent((event) => {
|
|
5479
|
+
wsBridge.broadcast(event);
|
|
5480
|
+
});
|
|
4991
5481
|
wsBridge.onDisconnect(() => {
|
|
4992
5482
|
if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
|
|
4993
5483
|
approvalProxy.approveAll(t("server.phoneDisconnected"));
|
|
@@ -5095,6 +5585,7 @@ async function start(opts = {}) {
|
|
|
5095
5585
|
await attempt(() => approvalProxy.close(), "ApprovalProxy");
|
|
5096
5586
|
await attempt(() => sessionManager.destroy(), "SessionManager");
|
|
5097
5587
|
await attempt(() => terminalExecutor.destroy(), "TerminalExecutor");
|
|
5588
|
+
await attempt(() => xcodeBuildExecutor.destroy(), "XcodeBuildExecutor");
|
|
5098
5589
|
await attempt(() => notificationService.destroy(), "NotificationService");
|
|
5099
5590
|
await attempt(() => sessionFileWatcher.destroy(), "SessionFileWatcher");
|
|
5100
5591
|
if (errors.length > 0) {
|
|
@@ -5123,9 +5614,9 @@ async function start(opts = {}) {
|
|
|
5123
5614
|
openPairing: (duration) => pairingManager.open(duration),
|
|
5124
5615
|
closePairing: () => pairingManager.close(),
|
|
5125
5616
|
regenerateToken: async () => {
|
|
5126
|
-
const newToken = (0,
|
|
5127
|
-
await (0,
|
|
5128
|
-
await (0,
|
|
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");
|
|
5129
5620
|
instance.token = newToken;
|
|
5130
5621
|
wsBridge.updateToken(newToken);
|
|
5131
5622
|
approvalProxy.updateToken(newToken);
|