@ganglion/xacpx 0.17.0-beta.6 → 0.17.0-beta.7

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/README.md CHANGED
@@ -29,7 +29,7 @@ If you need to code or work remotely on a temporary basis, `xacpx` gives you a f
29
29
 
30
30
  ### Prerequisites
31
31
 
32
- - Node.js 22+ or Bun
32
+ - Node.js 22.12+ or Bun
33
33
  - A working agent CLI you intend to use, such as Codex / Claude Code / Gemini / OpenCode
34
34
  - A phone with WeChat, Feishu, or Yuanbao installed
35
35
 
package/dist/cli.js CHANGED
@@ -5310,6 +5310,9 @@ var init_agent_templates = __esm(() => {
5310
5310
  factorydroid: {
5311
5311
  driver: "factorydroid"
5312
5312
  },
5313
+ "grok-build": {
5314
+ driver: "grok-build"
5315
+ },
5313
5316
  iflow: {
5314
5317
  driver: "iflow"
5315
5318
  },
@@ -5322,6 +5325,9 @@ var init_agent_templates = __esm(() => {
5322
5325
  kiro: {
5323
5326
  driver: "kiro"
5324
5327
  },
5328
+ mux: {
5329
+ driver: "mux"
5330
+ },
5325
5331
  opencode: {
5326
5332
  driver: "opencode"
5327
5333
  },
@@ -35123,11 +35129,385 @@ var init_workspace_fs = __esm(() => {
35123
35129
  };
35124
35130
  });
35125
35131
 
35132
+ // src/control/workspace-git.ts
35133
+ import { execFile as execFile2 } from "node:child_process";
35134
+ import { createHash as createHash4 } from "node:crypto";
35135
+ import { lstat, mkdir as mkdir10, realpath as realpath2 } from "node:fs/promises";
35136
+ import { homedir as homedir13 } from "node:os";
35137
+ import { basename as basename4, isAbsolute as isAbsolute4, join as join22, resolve as resolve4, sep as sep2 } from "node:path";
35138
+ import { promisify as promisify2 } from "node:util";
35139
+ function expandHome3(path14) {
35140
+ if (path14 === "~")
35141
+ return homedir13();
35142
+ if (path14.startsWith(`~${sep2}`) || path14.startsWith("~/"))
35143
+ return resolve4(homedir13(), path14.slice(2));
35144
+ return path14;
35145
+ }
35146
+
35147
+ class WorkspaceGit {
35148
+ listWorkspaces;
35149
+ managedWorktreesRoot;
35150
+ writeTails = new Map;
35151
+ constructor(listWorkspaces, options = {}) {
35152
+ this.listWorkspaces = listWorkspaces;
35153
+ this.managedWorktreesRoot = options.managedWorktreesRoot ?? join22(homedir13(), ".xacpx", "worktrees");
35154
+ }
35155
+ async rootFor(workspace3) {
35156
+ const ref = this.listWorkspaces().find((item) => item.name === workspace3);
35157
+ if (!ref)
35158
+ throw new Error("unknown-workspace");
35159
+ try {
35160
+ return await realpath2(expandHome3(ref.cwd));
35161
+ } catch {
35162
+ throw new Error("workspace-root-missing");
35163
+ }
35164
+ }
35165
+ async run(root, args) {
35166
+ return (await this.runRaw(root, args)).trim();
35167
+ }
35168
+ async runRaw(root, args) {
35169
+ const result = await execFileAsync2("git", ["-C", root, ...args], {
35170
+ timeout: GIT_TIMEOUT_MS2,
35171
+ killSignal: "SIGKILL",
35172
+ maxBuffer: GIT_MAX_BUFFER2
35173
+ });
35174
+ return result.stdout;
35175
+ }
35176
+ validatePaths(paths) {
35177
+ if (!paths.length)
35178
+ throw new Error("git-paths-required");
35179
+ for (const path14 of paths) {
35180
+ if (!path14 || isAbsolute4(path14) || path14.split(/[\\/]/).includes("..")) {
35181
+ throw new Error("invalid-git-path");
35182
+ }
35183
+ }
35184
+ return paths;
35185
+ }
35186
+ async validateBranch(root, raw) {
35187
+ const branch = raw.trim();
35188
+ if (!branch)
35189
+ throw new Error("branch-required");
35190
+ try {
35191
+ await this.run(root, ["check-ref-format", "--branch", branch]);
35192
+ } catch {
35193
+ throw new Error("invalid-branch-name");
35194
+ }
35195
+ return branch;
35196
+ }
35197
+ async validateStartPoint(root, raw) {
35198
+ const startPoint = raw?.trim();
35199
+ if (!startPoint)
35200
+ return;
35201
+ try {
35202
+ await this.run(root, ["rev-parse", "--verify", "--end-of-options", `${startPoint}^{commit}`]);
35203
+ } catch {
35204
+ throw new Error("invalid-start-point");
35205
+ }
35206
+ return startPoint;
35207
+ }
35208
+ async includeRenameSources(root, paths) {
35209
+ const selected = new Set(paths);
35210
+ const expanded = new Set(paths);
35211
+ const fields = (await this.runRaw(root, ["-c", "core.quotePath=false", "status", "--porcelain", "-z"])).split("\x00");
35212
+ for (let index = 0;index < fields.length; index += 1) {
35213
+ const field = fields[index];
35214
+ if (!field)
35215
+ continue;
35216
+ const status = field.slice(0, 2);
35217
+ const path14 = field.slice(3);
35218
+ if (status[0] !== "R" && status[0] !== "C")
35219
+ continue;
35220
+ const originalPath = fields[++index];
35221
+ if (originalPath && selected.has(path14))
35222
+ expanded.add(originalPath);
35223
+ }
35224
+ return [...expanded];
35225
+ }
35226
+ async withWrite(workspace3, action) {
35227
+ const previous = this.writeTails.get(workspace3) ?? Promise.resolve();
35228
+ let release;
35229
+ const current = new Promise((resolve5) => {
35230
+ release = resolve5;
35231
+ });
35232
+ const tail2 = previous.catch(() => {}).then(() => current);
35233
+ this.writeTails.set(workspace3, tail2);
35234
+ await previous.catch(() => {});
35235
+ try {
35236
+ return await action();
35237
+ } finally {
35238
+ release();
35239
+ if (this.writeTails.get(workspace3) === tail2)
35240
+ this.writeTails.delete(workspace3);
35241
+ }
35242
+ }
35243
+ async stage(workspace3, paths) {
35244
+ await this.withWrite(workspace3, async () => {
35245
+ const root = await this.rootFor(workspace3);
35246
+ await this.run(root, ["--literal-pathspecs", "add", "--", ...this.validatePaths(paths)]);
35247
+ });
35248
+ }
35249
+ async unstage(workspace3, paths) {
35250
+ await this.withWrite(workspace3, async () => {
35251
+ const root = await this.rootFor(workspace3);
35252
+ const valid = await this.includeRenameSources(root, this.validatePaths(paths));
35253
+ try {
35254
+ await this.run(root, ["--literal-pathspecs", "restore", "--staged", "--", ...valid]);
35255
+ } catch {
35256
+ await this.run(root, ["--literal-pathspecs", "reset", "--", ...valid]);
35257
+ }
35258
+ });
35259
+ }
35260
+ async commit(workspace3, message) {
35261
+ return this.withWrite(workspace3, async () => {
35262
+ const root = await this.rootFor(workspace3);
35263
+ const normalized = message.trim();
35264
+ if (!normalized)
35265
+ throw new Error("commit-message-required");
35266
+ await this.run(root, ["commit", "-m", normalized]);
35267
+ const hash = await this.run(root, ["rev-parse", "HEAD"]);
35268
+ const shortHash2 = await this.run(root, ["rev-parse", "--short", "HEAD"]);
35269
+ const summary = await this.run(root, ["show", "-s", "--format=%s", "HEAD"]);
35270
+ return { hash, shortHash: shortHash2, summary };
35271
+ });
35272
+ }
35273
+ async checkout(workspace3, options) {
35274
+ await this.withWrite(workspace3, async () => {
35275
+ const root = await this.rootFor(workspace3);
35276
+ const branch = await this.validateBranch(root, options.branch);
35277
+ if (await this.runRaw(root, ["status", "--porcelain", "-z"]) !== "") {
35278
+ throw new Error("dirty-worktree");
35279
+ }
35280
+ if (options.create) {
35281
+ const startPoint = await this.validateStartPoint(root, options.startPoint);
35282
+ await this.run(root, ["switch", "-c", branch, ...startPoint ? [startPoint] : []]);
35283
+ return;
35284
+ }
35285
+ await this.run(root, ["switch", branch]);
35286
+ });
35287
+ }
35288
+ async fetch(workspace3, remote = "origin") {
35289
+ await this.withWrite(workspace3, async () => {
35290
+ const root = await this.rootFor(workspace3);
35291
+ await this.assertRemote(root, remote);
35292
+ await this.run(root, ["fetch", "--prune", "--", remote]);
35293
+ });
35294
+ }
35295
+ async pull(workspace3) {
35296
+ await this.withWrite(workspace3, async () => {
35297
+ const root = await this.rootFor(workspace3);
35298
+ if (await this.runRaw(root, ["status", "--porcelain", "-z"]) !== "") {
35299
+ throw new Error("dirty-worktree");
35300
+ }
35301
+ try {
35302
+ await this.run(root, ["rev-parse", "--verify", "@{upstream}"]);
35303
+ } catch {
35304
+ throw new Error("no-upstream");
35305
+ }
35306
+ await this.run(root, ["pull", "--ff-only"]);
35307
+ });
35308
+ }
35309
+ async push(workspace3, options = {}) {
35310
+ await this.withWrite(workspace3, async () => {
35311
+ const root = await this.rootFor(workspace3);
35312
+ if (options.setUpstream) {
35313
+ const remote = options.remote?.trim() || "origin";
35314
+ await this.assertRemote(root, remote);
35315
+ const branch = await this.run(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
35316
+ if (branch === "HEAD")
35317
+ throw new Error("detached-head");
35318
+ await this.run(root, ["push", "--set-upstream", "--", remote, branch]);
35319
+ return;
35320
+ }
35321
+ try {
35322
+ await this.run(root, ["rev-parse", "--verify", "@{upstream}"]);
35323
+ } catch {
35324
+ throw new Error("no-upstream");
35325
+ }
35326
+ await this.run(root, ["push"]);
35327
+ });
35328
+ }
35329
+ async createWorktree(workspace3, options) {
35330
+ return this.withWrite(workspace3, async () => {
35331
+ const root = await this.rootFor(workspace3);
35332
+ const branch = await this.validateBranch(root, options.branch);
35333
+ const startPoint = await this.validateStartPoint(root, options.startPoint);
35334
+ await mkdir10(this.managedWorktreesRoot, { recursive: true });
35335
+ const managedRoot = await realpath2(this.managedWorktreesRoot);
35336
+ const commonDir = await this.run(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
35337
+ const top = await this.run(root, ["rev-parse", "--show-toplevel"]);
35338
+ const repoKey = `${basename4(top)}-${createHash4("sha256").update(commonDir).digest("hex").slice(0, 8)}`;
35339
+ const branchSlug = branch.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "branch";
35340
+ const branchKey = `${branchSlug}-${createHash4("sha256").update(branch).digest("hex").slice(0, 6)}`;
35341
+ const repoDir = join22(managedRoot, repoKey);
35342
+ try {
35343
+ const existing = await lstat(repoDir);
35344
+ if (!existing.isDirectory() || existing.isSymbolicLink())
35345
+ throw new Error("worktree-path-unsafe");
35346
+ } catch (error2) {
35347
+ if (error2.code !== "ENOENT")
35348
+ throw error2;
35349
+ await mkdir10(repoDir);
35350
+ }
35351
+ const realRepoDir = await realpath2(repoDir);
35352
+ if (!realRepoDir.startsWith(managedRoot + sep2))
35353
+ throw new Error("worktree-path-unsafe");
35354
+ const target = join22(realRepoDir, branchKey);
35355
+ try {
35356
+ await lstat(target);
35357
+ throw new Error("worktree-path-exists");
35358
+ } catch (error2) {
35359
+ if (error2.code !== "ENOENT")
35360
+ throw error2;
35361
+ }
35362
+ if (options.createBranch) {
35363
+ await this.run(root, ["worktree", "add", "-b", branch, target, ...startPoint ? [startPoint] : []]);
35364
+ } else {
35365
+ await this.run(root, ["worktree", "add", target, branch]);
35366
+ }
35367
+ const createdPath = await realpath2(target);
35368
+ if (!createdPath.startsWith(managedRoot + sep2))
35369
+ throw new Error("worktree-path-unsafe");
35370
+ return { path: createdPath, branch, linked: true };
35371
+ });
35372
+ }
35373
+ async removeManagedWorktree(workspace3, path14) {
35374
+ await this.withWrite(workspace3, async () => {
35375
+ const root = await this.rootFor(workspace3);
35376
+ const managedRoot = await realpath2(this.managedWorktreesRoot);
35377
+ const target = await realpath2(path14);
35378
+ if (!target.startsWith(managedRoot + sep2))
35379
+ throw new Error("worktree-outside-managed-root");
35380
+ const worktrees = await this.readWorktrees(root);
35381
+ const worktree = worktrees.find((item) => item.path === target);
35382
+ if (!worktree)
35383
+ throw new Error("worktree-not-found");
35384
+ if (worktree.current)
35385
+ throw new Error("cannot-remove-current-worktree");
35386
+ await this.run(root, ["worktree", "remove", target]);
35387
+ });
35388
+ }
35389
+ async assertRemote(root, remote) {
35390
+ const normalized = remote.trim();
35391
+ if (normalized.startsWith("-"))
35392
+ throw new Error("invalid-remote");
35393
+ const remotes = (await this.run(root, ["remote"])).split(`
35394
+ `).filter(Boolean);
35395
+ if (!normalized || !remotes.includes(normalized))
35396
+ throw new Error("unknown-remote");
35397
+ }
35398
+ async status(workspace3) {
35399
+ const root = await this.rootFor(workspace3);
35400
+ try {
35401
+ await this.run(root, ["rev-parse", "--is-inside-work-tree"]);
35402
+ } catch {
35403
+ throw new Error("not-a-git-repo");
35404
+ }
35405
+ let head;
35406
+ try {
35407
+ head = await this.run(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
35408
+ } catch {
35409
+ try {
35410
+ head = await this.run(root, ["symbolic-ref", "--short", "HEAD"]);
35411
+ } catch {
35412
+ head = "HEAD";
35413
+ }
35414
+ }
35415
+ const branch = head === "HEAD" ? undefined : head;
35416
+ let upstream;
35417
+ let ahead = 0;
35418
+ let behind = 0;
35419
+ try {
35420
+ upstream = await this.run(root, ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]);
35421
+ const [aheadRaw = "0", behindRaw = "0"] = (await this.run(root, ["rev-list", "--left-right", "--count", "HEAD...@{upstream}"])).split(/\s+/);
35422
+ ahead = Number(aheadRaw) || 0;
35423
+ behind = Number(behindRaw) || 0;
35424
+ } catch {
35425
+ upstream = undefined;
35426
+ }
35427
+ const worktrees = await this.readWorktrees(root);
35428
+ const currentWorktree = worktrees.find((item) => item.current);
35429
+ if (!currentWorktree)
35430
+ throw new Error("git-worktree-context-missing");
35431
+ const branchesRaw = await this.run(root, [
35432
+ "for-each-ref",
35433
+ "--format=%(refname:short)%00%(worktreepath)",
35434
+ "refs/heads"
35435
+ ]);
35436
+ const branches = branchesRaw === "" ? [] : branchesRaw.split(`
35437
+ `).map((line) => {
35438
+ const [name = "", worktreePath = ""] = line.split("\x00");
35439
+ return {
35440
+ name,
35441
+ current: name === branch,
35442
+ ...worktreePath ? { worktreePath } : {}
35443
+ };
35444
+ });
35445
+ const statusRaw = await this.runRaw(root, ["-c", "core.quotePath=false", "status", "--porcelain", "-z"]);
35446
+ const files = [];
35447
+ const fields = statusRaw.split("\x00");
35448
+ for (let index = 0;index < fields.length; index += 1) {
35449
+ const field = fields[index];
35450
+ if (!field)
35451
+ continue;
35452
+ const status = field.slice(0, 2);
35453
+ const path14 = field.slice(3);
35454
+ if (status[0] === "R" || status[0] === "C")
35455
+ index += 1;
35456
+ files.push({ path: path14, status });
35457
+ }
35458
+ files.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
35459
+ return {
35460
+ workspace: workspace3,
35461
+ ...branch ? { branch } : {},
35462
+ detached: !branch,
35463
+ ...upstream ? { upstream } : {},
35464
+ ahead,
35465
+ behind,
35466
+ worktree: { root: currentWorktree.path, linked: currentWorktree.linked },
35467
+ files,
35468
+ branches,
35469
+ worktrees
35470
+ };
35471
+ }
35472
+ async readWorktrees(root) {
35473
+ const output = await this.run(root, ["worktree", "list", "--porcelain"]);
35474
+ const blocks = output.split(/\n\n+/).filter(Boolean);
35475
+ const parsed = blocks.map((block) => {
35476
+ let path14 = "";
35477
+ let branch;
35478
+ let detached = false;
35479
+ for (const line of block.split(`
35480
+ `)) {
35481
+ if (line.startsWith("worktree "))
35482
+ path14 = line.slice("worktree ".length);
35483
+ else if (line.startsWith("branch refs/heads/"))
35484
+ branch = line.slice("branch refs/heads/".length);
35485
+ else if (line === "detached")
35486
+ detached = true;
35487
+ }
35488
+ return { path: path14, branch, detached };
35489
+ });
35490
+ const currentRoot = await realpath2(root);
35491
+ return parsed.map((item, index) => ({
35492
+ path: item.path,
35493
+ ...item.branch ? { branch: item.branch } : {},
35494
+ ...item.detached ? { detached: true } : {},
35495
+ current: item.path === currentRoot,
35496
+ linked: index !== 0
35497
+ }));
35498
+ }
35499
+ }
35500
+ var execFileAsync2, GIT_TIMEOUT_MS2 = 30000, GIT_MAX_BUFFER2;
35501
+ var init_workspace_git = __esm(() => {
35502
+ execFileAsync2 = promisify2(execFile2);
35503
+ GIT_MAX_BUFFER2 = 8 * 1024 * 1024;
35504
+ });
35505
+
35126
35506
  // src/control/turn-support.ts
35127
35507
  async function raceWithTimeout(promise2, ms) {
35128
35508
  let timer;
35129
- const timeout = new Promise((resolve4) => {
35130
- timer = setTimeout(resolve4, ms);
35509
+ const timeout = new Promise((resolve5) => {
35510
+ timer = setTimeout(resolve5, ms);
35131
35511
  });
35132
35512
  try {
35133
35513
  await Promise.race([promise2, timeout]);
@@ -35408,8 +35788,8 @@ class TurnQueue {
35408
35788
  params.abortSignal.addEventListener("abort", () => controller.abort(), { once: true });
35409
35789
  }
35410
35790
  let resolveSettled;
35411
- const settled = new Promise((resolve4) => {
35412
- resolveSettled = resolve4;
35791
+ const settled = new Promise((resolve5) => {
35792
+ resolveSettled = resolve5;
35413
35793
  });
35414
35794
  this.inFlight.set(key, { controller, settled });
35415
35795
  if (params.drained) {
@@ -35530,9 +35910,12 @@ class ControlService {
35530
35910
  deps;
35531
35911
  runner;
35532
35912
  turnQueue;
35913
+ workspaceGit;
35533
35914
  modelSetTails = new Map;
35915
+ worktreeRegistrationTails = new Map;
35534
35916
  constructor(deps) {
35535
35917
  this.deps = deps;
35918
+ this.workspaceGit = new WorkspaceGit(() => this.deps.workspaces.list().map((w) => ({ name: w.name, cwd: w.cwd })), { ...deps.gitWorktreesRoot ? { managedWorktreesRoot: deps.gitWorktreesRoot } : {} });
35536
35919
  this.runner = new SessionTurnRunner(deps);
35537
35920
  this.turnQueue = new TurnQueue({
35538
35921
  runTurn: (req, signal, onActivity) => this.runner.run(req, signal, onActivity),
@@ -35559,6 +35942,75 @@ class ControlService {
35559
35942
  workspaceGitDiff(workspace3, path15) {
35560
35943
  return this.workspaceFs.gitDiff(workspace3, path15);
35561
35944
  }
35945
+ workspaceGitStatus(workspace3) {
35946
+ return this.workspaceGit.status(workspace3);
35947
+ }
35948
+ async mutateWorkspaceGit(operation) {
35949
+ if (!this.deps.filesWriteEnabled())
35950
+ throw new Error("files-write-disabled");
35951
+ return operation();
35952
+ }
35953
+ async withWorktreeRegistration(workspaceName, operation) {
35954
+ const previous = this.worktreeRegistrationTails.get(workspaceName) ?? Promise.resolve();
35955
+ let release;
35956
+ const current = new Promise((resolve5) => {
35957
+ release = resolve5;
35958
+ });
35959
+ const tail2 = previous.catch(() => {}).then(() => current);
35960
+ this.worktreeRegistrationTails.set(workspaceName, tail2);
35961
+ await previous.catch(() => {});
35962
+ try {
35963
+ return await operation();
35964
+ } finally {
35965
+ release();
35966
+ if (this.worktreeRegistrationTails.get(workspaceName) === tail2) {
35967
+ this.worktreeRegistrationTails.delete(workspaceName);
35968
+ }
35969
+ }
35970
+ }
35971
+ gitStage(workspace3, paths) {
35972
+ return this.mutateWorkspaceGit(() => this.workspaceGit.stage(workspace3, paths));
35973
+ }
35974
+ gitUnstage(workspace3, paths) {
35975
+ return this.mutateWorkspaceGit(() => this.workspaceGit.unstage(workspace3, paths));
35976
+ }
35977
+ gitCommit(workspace3, message) {
35978
+ return this.mutateWorkspaceGit(() => this.workspaceGit.commit(workspace3, message));
35979
+ }
35980
+ gitFetch(workspace3, remote) {
35981
+ return this.mutateWorkspaceGit(() => this.workspaceGit.fetch(workspace3, remote));
35982
+ }
35983
+ gitPull(workspace3) {
35984
+ return this.mutateWorkspaceGit(() => this.workspaceGit.pull(workspace3));
35985
+ }
35986
+ gitPush(workspace3, options) {
35987
+ return this.mutateWorkspaceGit(() => this.workspaceGit.push(workspace3, options));
35988
+ }
35989
+ gitCheckout(workspace3, options) {
35990
+ return this.mutateWorkspaceGit(() => this.workspaceGit.checkout(workspace3, options));
35991
+ }
35992
+ async gitCreateWorktree(workspace3, input) {
35993
+ const workspaceName = input.workspaceName.trim();
35994
+ if (!workspaceName)
35995
+ throw new Error("workspace-name-required");
35996
+ return this.mutateWorkspaceGit(() => this.withWorktreeRegistration(workspaceName, async () => {
35997
+ if (this.deps.workspaces.list().some((item) => item.name === workspaceName)) {
35998
+ throw new Error("workspace-name-exists");
35999
+ }
36000
+ const worktree = await this.workspaceGit.createWorktree(workspace3, input);
36001
+ try {
36002
+ const registered = await this.deps.workspaces.create(workspaceName, worktree.path, `Git worktree for ${worktree.branch}`);
36003
+ return { worktree, workspace: registered };
36004
+ } catch (registrationError) {
36005
+ try {
36006
+ await this.workspaceGit.removeManagedWorktree(workspace3, worktree.path);
36007
+ } catch (rollbackError) {
36008
+ throw new AggregateError([registrationError, rollbackError], "workspace-registration-rollback-failed");
36009
+ }
36010
+ throw registrationError;
36011
+ }
36012
+ }));
36013
+ }
35562
36014
  searchWorkspace(workspace3, opts) {
35563
36015
  return this.workspaceFs.search(workspace3, opts);
35564
36016
  }
@@ -35641,8 +36093,8 @@ class ControlService {
35641
36093
  async runModelSetExclusive(sessionAlias, operation) {
35642
36094
  const previous = this.modelSetTails.get(sessionAlias) ?? Promise.resolve();
35643
36095
  let release;
35644
- const gate = new Promise((resolve4) => {
35645
- release = resolve4;
36096
+ const gate = new Promise((resolve5) => {
36097
+ release = resolve5;
35646
36098
  });
35647
36099
  const tail2 = previous.catch(() => {}).then(() => gate);
35648
36100
  this.modelSetTails.set(sessionAlias, tail2);
@@ -35858,6 +36310,7 @@ var init_control_service = __esm(() => {
35858
36310
  init_channel_scope();
35859
36311
  init_native_session_history();
35860
36312
  init_workspace_fs();
36313
+ init_workspace_git();
35861
36314
  init_session_turn_runner();
35862
36315
  init_turn_queue();
35863
36316
  init_turn_support();
@@ -36078,10 +36531,10 @@ var init_terminal_service = __esm(() => {
36078
36531
 
36079
36532
  // src/control/upload-store.ts
36080
36533
  import { mkdtemp as mkdtemp2, readdir as readdir5, rm as rm11, stat as stat4, writeFile as writeFile9 } from "node:fs/promises";
36081
- import { homedir as homedir13 } from "node:os";
36534
+ import { homedir as homedir14 } from "node:os";
36082
36535
  import path15 from "node:path";
36083
36536
  function defaultRootDir() {
36084
- const home = process.env.HOME ?? homedir13();
36537
+ const home = process.env.HOME ?? homedir14();
36085
36538
  return path15.join(coreHomeDir(home), "runtime", "uploads");
36086
36539
  }
36087
36540
  function sanitizeUploadFilename(raw) {
@@ -36113,8 +36566,8 @@ class UploadStore {
36113
36566
  if (bytes.byteLength > this.maxBytes)
36114
36567
  throw new Error("file-too-large");
36115
36568
  const safeName = sanitizeUploadFilename(filename);
36116
- const { mkdir: mkdir10 } = await import("node:fs/promises");
36117
- await mkdir10(this.rootDir, { recursive: true });
36569
+ const { mkdir: mkdir11 } = await import("node:fs/promises");
36570
+ await mkdir11(this.rootDir, { recursive: true });
36118
36571
  const dir = await mkdtemp2(path15.join(this.rootDir, "u-"));
36119
36572
  const filePath = path15.join(dir, safeName);
36120
36573
  await writeFile9(filePath, bytes);
@@ -36156,54 +36609,52 @@ var init_upload_store = __esm(() => {
36156
36609
  });
36157
36610
 
36158
36611
  // src/config/agent-catalog.ts
36159
- import { existsSync as existsSync3 } from "node:fs";
36160
- import { delimiter as delimiter2, join as join22 } from "node:path";
36161
- function isBinaryOnPath(binary) {
36162
- const path16 = process.env.PATH ?? "";
36163
- const exts = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
36164
- for (const dir of path16.split(delimiter2)) {
36165
- if (!dir)
36166
- continue;
36167
- for (const ext of exts) {
36168
- try {
36169
- if (existsSync3(join22(dir, binary + ext)))
36170
- return true;
36171
- } catch {}
36172
- }
36173
- }
36174
- return false;
36612
+ function probeTarget(command) {
36613
+ return command.trim().split(/\s+/)[0] ?? "";
36175
36614
  }
36176
- function listAgentCatalog(config4, probe = isBinaryOnPath) {
36615
+ function listAgentCatalog(config4, { registry: registry2, probe = (binary) => isExecutableOnPath(binary) }) {
36177
36616
  const agents = config4.agents ?? {};
36178
36617
  const driverConfigured = (driver) => Object.entries(agents).some(([name, a]) => name === driver || a.driver === driver);
36179
36618
  return listAgentTemplates().map((driver) => {
36180
- let installed;
36181
- if (BUILTIN_DRIVERS.has(driver)) {
36182
- installed = "builtin";
36183
- } else {
36184
- const binary = DRIVER_BINARIES[driver] ?? driver;
36185
- installed = probe(binary) ? "yes" : "unknown";
36186
- }
36619
+ const binary = probeTarget(registry2?.resolve(driver) ?? driver);
36620
+ const installed = binary === "npx" || binary === "uvx" ? "builtin" : probe(binary) ? "yes" : "unknown";
36187
36621
  return { driver, configured: driverConfigured(driver), installed };
36188
36622
  });
36189
36623
  }
36190
- var BUILTIN_DRIVERS, DRIVER_BINARIES;
36191
36624
  var init_agent_catalog = __esm(() => {
36192
36625
  init_agent_templates();
36193
- BUILTIN_DRIVERS = new Set(["codex", "claude"]);
36194
- DRIVER_BINARIES = {
36195
- cursor: "cursor-agent"
36626
+ init_local_agent_bin();
36627
+ });
36628
+
36629
+ // src/transport/agent-registry.ts
36630
+ import { createRequire as createRequire7 } from "node:module";
36631
+ function createAcpxAgentRegistryLoader(deps = {}) {
36632
+ let cached2;
36633
+ return () => {
36634
+ if (cached2 !== undefined)
36635
+ return cached2;
36636
+ try {
36637
+ cached2 = (deps.loadRuntime ?? loadRuntime)().createAgentRegistry();
36638
+ } catch (error2) {
36639
+ cached2 = null;
36640
+ deps.logger?.error("transport.agent_registry.unavailable", "could not read acpx's agent registry; install hints fall back to probing the driver name, so an installed CLI may show as not detected", { error: error2 instanceof Error ? error2.message : String(error2) });
36641
+ }
36642
+ return cached2;
36196
36643
  };
36644
+ }
36645
+ var require6, loadRuntime = () => require6("acpx/runtime");
36646
+ var init_agent_registry = __esm(() => {
36647
+ require6 = createRequire7(import.meta.url);
36197
36648
  });
36198
36649
 
36199
36650
  // src/config/config-watcher.ts
36200
36651
  import { watch } from "node:fs";
36201
- import { basename as basename4, dirname as dirname13 } from "node:path";
36652
+ import { basename as basename5, dirname as dirname13 } from "node:path";
36202
36653
  function startConfigWatcher(options) {
36203
36654
  const { configPath, onChange, debounceMs = 100, logger } = options;
36204
36655
  const watchFactory = options.watchFactory ?? watch;
36205
36656
  const dir = dirname13(configPath);
36206
- const target = basename4(configPath);
36657
+ const target = basename5(configPath);
36207
36658
  let timer;
36208
36659
  let watcher;
36209
36660
  const fire = () => {
@@ -36218,7 +36669,7 @@ function startConfigWatcher(options) {
36218
36669
  };
36219
36670
  try {
36220
36671
  watcher = watchFactory(dir, { persistent: false }, (_event, filename) => {
36221
- if (filename !== null && basename4(filename.toString()) !== target)
36672
+ if (filename !== null && basename5(filename.toString()) !== target)
36222
36673
  return;
36223
36674
  if (timer)
36224
36675
  clearTimeout(timer);
@@ -36257,7 +36708,7 @@ __export(exports_main, {
36257
36708
  buildApp: () => buildApp
36258
36709
  });
36259
36710
  import { randomUUID as randomUUID5 } from "node:crypto";
36260
- import { homedir as homedir14 } from "node:os";
36711
+ import { homedir as homedir15 } from "node:os";
36261
36712
  import { dirname as dirname14, join as join23 } from "node:path";
36262
36713
  import { fileURLToPath as fileURLToPath5 } from "node:url";
36263
36714
  function startProgressHeartbeat(orchestration3, config4, logger, channel) {
@@ -36761,6 +37212,7 @@ async function buildApp(paths, deps = {}) {
36761
37212
  shell: () => terminalShell(config4)
36762
37213
  });
36763
37214
  const uploadStore = new UploadStore;
37215
+ const loadAgentRegistry = createAcpxAgentRegistryLoader({ logger });
36764
37216
  uploadStore.cleanup();
36765
37217
  const uploadCleanupInterval = setInterval(() => void uploadStore.cleanup().catch(() => {}), 60 * 60 * 1000);
36766
37218
  const control = new ControlService({
@@ -36780,7 +37232,7 @@ async function buildApp(paths, deps = {}) {
36780
37232
  events: controlEvents,
36781
37233
  agents: {
36782
37234
  list: () => Object.entries(config4.agents).map(([name, agentConfig]) => ({ name, driver: agentConfig.driver })),
36783
- catalog: () => listAgentCatalog(config4),
37235
+ catalog: () => listAgentCatalog(config4, { registry: loadAgentRegistry() }),
36784
37236
  create: async (name, driver) => {
36785
37237
  const updated = await configStore.upsertAgent(name, { driver });
36786
37238
  replaceRuntimeConfig(config4, updated);
@@ -36983,7 +37435,7 @@ async function prepareChannelMedia(configPath, config4) {
36983
37435
  return { mediaStore, channelDeps: { mediaStore, allowedMediaRoots } };
36984
37436
  }
36985
37437
  function resolveRuntimePaths() {
36986
- const home = process.env.HOME ?? homedir14();
37438
+ const home = process.env.HOME ?? homedir15();
36987
37439
  if (!home) {
36988
37440
  throw new Error("Unable to resolve the current user home directory");
36989
37441
  }
@@ -37059,6 +37511,7 @@ var init_main = __esm(async () => {
37059
37511
  init_terminal_service();
37060
37512
  init_upload_store();
37061
37513
  init_agent_catalog();
37514
+ init_agent_registry();
37062
37515
  init_config_watcher();
37063
37516
  init_perf_tracer();
37064
37517
  init_bootstrap();
@@ -37113,7 +37566,7 @@ function buildDetails(metadata, version2, verbose) {
37113
37566
  }
37114
37567
  async function defaultRunVersion(command) {
37115
37568
  const spawnSpec = resolveSpawnCommand(command, ["--version"]);
37116
- return await new Promise((resolve4, reject) => {
37569
+ return await new Promise((resolve5, reject) => {
37117
37570
  const child = spawn12(spawnSpec.command, spawnSpec.args, {
37118
37571
  stdio: ["ignore", "pipe", "pipe"]
37119
37572
  });
@@ -37130,7 +37583,7 @@ async function defaultRunVersion(command) {
37130
37583
  if (code === 0) {
37131
37584
  const version2 = stdout2.trim() || stderr.trim();
37132
37585
  if (version2.length > 0) {
37133
- resolve4(version2);
37586
+ resolve5(version2);
37134
37587
  return;
37135
37588
  }
37136
37589
  }
@@ -37254,10 +37707,10 @@ var init_config_check = __esm(async () => {
37254
37707
  // src/doctor/checks/daemon-check.ts
37255
37708
  import { readdir as readdir6, readFile as readFile16, rm as rm12 } from "node:fs/promises";
37256
37709
  import { fileURLToPath as fileURLToPath6 } from "node:url";
37257
- import { homedir as homedir15 } from "node:os";
37710
+ import { homedir as homedir16 } from "node:os";
37258
37711
  import { join as join24 } from "node:path";
37259
37712
  async function checkDaemon(options = {}) {
37260
- const home = options.home ?? process.env.HOME ?? homedir15();
37713
+ const home = options.home ?? process.env.HOME ?? homedir16();
37261
37714
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
37262
37715
  const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
37263
37716
  home,
@@ -37422,10 +37875,10 @@ var init_daemon_check = __esm(() => {
37422
37875
 
37423
37876
  // src/doctor/checks/logs-check.ts
37424
37877
  import { stat as stat5, readdir as readdir7 } from "node:fs/promises";
37425
- import { basename as basename5, join as join25 } from "node:path";
37426
- import { homedir as homedir16 } from "node:os";
37878
+ import { basename as basename6, join as join25 } from "node:path";
37879
+ import { homedir as homedir17 } from "node:os";
37427
37880
  async function checkLogs(options = {}) {
37428
- const home = options.home ?? process.env.HOME ?? homedir16();
37881
+ const home = options.home ?? process.env.HOME ?? homedir17();
37429
37882
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
37430
37883
  const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
37431
37884
  home,
@@ -37452,7 +37905,7 @@ async function checkLogs(options = {}) {
37452
37905
  `error: ${formatError6(error2)}`
37453
37906
  ]);
37454
37907
  }
37455
- const baseNames = [basename5(paths.appLog), basename5(paths.stdoutLog), basename5(paths.stderrLog)];
37908
+ const baseNames = [basename6(paths.appLog), basename6(paths.stdoutLog), basename6(paths.stderrLog)];
37456
37909
  const tracked = new Set(baseNames);
37457
37910
  const matched = entries.filter((entry) => isTrackedLogName(entry, tracked));
37458
37911
  const files = [];
@@ -37607,9 +38060,9 @@ var init_orchestration_health = __esm(() => {
37607
38060
  });
37608
38061
 
37609
38062
  // src/doctor/checks/orchestration-socket-check.ts
37610
- import { homedir as homedir17 } from "node:os";
38063
+ import { homedir as homedir18 } from "node:os";
37611
38064
  async function checkOrchestrationSocket(options = {}) {
37612
- const home = options.home ?? process.env.HOME ?? homedir17();
38065
+ const home = options.home ?? process.env.HOME ?? homedir18();
37613
38066
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
37614
38067
  const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
37615
38068
  home,
@@ -37784,9 +38237,9 @@ var init_plugin_check = __esm(async () => {
37784
38237
  import { constants } from "node:fs";
37785
38238
  import { access as access4, stat as stat6 } from "node:fs/promises";
37786
38239
  import { dirname as dirname15 } from "node:path";
37787
- import { homedir as homedir18 } from "node:os";
38240
+ import { homedir as homedir19 } from "node:os";
37788
38241
  async function checkRuntime(options = {}) {
37789
- const home = options.home ?? process.env.HOME ?? homedir18();
38242
+ const home = options.home ?? process.env.HOME ?? homedir19();
37790
38243
  const runtimeDir = options.configPath ? resolveRuntimeDirFromConfigPath(options.configPath) : undefined;
37791
38244
  const paths = (options.resolveDaemonPaths ?? resolveDaemonPaths)({
37792
38245
  home,
@@ -38419,10 +38872,10 @@ var init_render_doctor = __esm(() => {
38419
38872
  });
38420
38873
 
38421
38874
  // src/doctor/doctor.ts
38422
- import { homedir as homedir19 } from "node:os";
38875
+ import { homedir as homedir20 } from "node:os";
38423
38876
  import { join as join26 } from "node:path";
38424
38877
  async function runDoctor(options = {}, deps = {}) {
38425
- const home = deps.home ?? process.env.HOME ?? homedir19();
38878
+ const home = deps.home ?? process.env.HOME ?? homedir20();
38426
38879
  const runtimePaths = resolveDoctorRuntimePaths(home, deps.resolveRuntimePaths);
38427
38880
  const sharedLoadConfig = createSharedLoadConfig(runtimePaths, deps.loadConfig ?? loadConfig);
38428
38881
  const runners = [
@@ -38759,8 +39212,8 @@ var init_doctor2 = __esm(async () => {
38759
39212
  // src/cli.ts
38760
39213
  init_core_home();
38761
39214
  import { randomUUID as randomUUID6 } from "node:crypto";
38762
- import { homedir as homedir20 } from "node:os";
38763
- import { dirname as dirname16, join as join27, sep as sep2 } from "node:path";
39215
+ import { homedir as homedir21 } from "node:os";
39216
+ import { dirname as dirname16, join as join27, sep as sep3 } from "node:path";
38764
39217
  import { fileURLToPath as fileURLToPath7 } from "node:url";
38765
39218
 
38766
39219
  // src/runtime/migrate-core-home.ts
@@ -55073,7 +55526,7 @@ async function defaultRun(options = {}) {
55073
55526
  const firstRunOnboarding = options.firstRunOnboarding ?? decodeFirstRunOnboarding(coreEnv("FIRST_RUN_ONBOARDING"));
55074
55527
  await runConsole2(runtimePaths, {
55075
55528
  buildApp: (paths) => buildApp2(paths, {
55076
- defaultLoggingLevel: resolveCliEntryPath2().includes(`${sep2}src${sep2}`) ? "debug" : "info",
55529
+ defaultLoggingLevel: resolveCliEntryPath2().includes(`${sep3}src${sep3}`) ? "debug" : "info",
55077
55530
  channel: channelRegistry
55078
55531
  }),
55079
55532
  beforeReady: firstRunOnboarding ? async (runtime) => {
@@ -55084,7 +55537,7 @@ async function defaultRun(options = {}) {
55084
55537
  daemonRuntime,
55085
55538
  ...firstLockCreator ? {
55086
55539
  consumerLockFactory: (runtime) => firstLockCreator.create({
55087
- lockFilePath: `${daemonPaths.runtimeDir}${sep2}${firstLockCreator.channel.id}-consumer.lock.json`,
55540
+ lockFilePath: `${daemonPaths.runtimeDir}${sep3}${firstLockCreator.channel.id}-consumer.lock.json`,
55088
55541
  onDiagnostic: async (event, context) => {
55089
55542
  await runtime.logger.info(`${firstLockCreator.channel.id}.consumer_lock.${event}`, `${firstLockCreator.channel.id} consumer lock diagnostic`, context);
55090
55543
  }
@@ -55279,7 +55732,7 @@ async function defaultPromptSecret(message) {
55279
55732
  process.stdout.write(message);
55280
55733
  process.stdin.setRawMode(true);
55281
55734
  process.stdin.resume();
55282
- return await new Promise((resolve4, reject) => {
55735
+ return await new Promise((resolve5, reject) => {
55283
55736
  const chunks = [];
55284
55737
  let inEscape = false;
55285
55738
  const cleanup = () => {
@@ -55310,7 +55763,7 @@ async function defaultPromptSecret(message) {
55310
55763
  if (char === "\r" || char === `
55311
55764
  `) {
55312
55765
  cleanup();
55313
- resolve4(chunks.join(""));
55766
+ resolve5(chunks.join(""));
55314
55767
  return;
55315
55768
  }
55316
55769
  if (char === "" || char === "\b") {
@@ -55365,7 +55818,7 @@ function decodeFirstRunOnboarding(raw) {
55365
55818
  return null;
55366
55819
  }
55367
55820
  function requireHome2() {
55368
- const home = process.env.HOME ?? homedir20();
55821
+ const home = process.env.HOME ?? homedir21();
55369
55822
  if (!home) {
55370
55823
  throw new Error("Unable to resolve the current user home directory");
55371
55824
  }
@@ -4,11 +4,29 @@ export interface AgentCatalogEntry {
4
4
  configured: boolean;
5
5
  installed: "builtin" | "yes" | "unknown";
6
6
  }
7
- /** True if `binary` is found in any PATH directory (no extension assumptions on POSIX). */
8
- export declare function isBinaryOnPath(binary: string): boolean;
7
+ /** Narrow view of acpx's registry supplied by the transport boundary. */
8
+ export interface AgentCommandRegistry {
9
+ resolve(driver: string): string;
10
+ }
11
+ export interface ListAgentCatalogOptions {
12
+ registry: AgentCommandRegistry | null;
13
+ probe?: (binary: string) => boolean;
14
+ }
9
15
  /**
10
- * Catalog of every acpx driver xacpx knows (from `listAgentTemplates()`), each
11
- * tagged with whether it's already configured and a best-effort install hint.
12
- * `probe` is injectable for tests; defaults to a real PATH lookup.
16
+ * Catalog of every acpx driver xacpx knows (from `listAgentTemplates()`), each tagged with
17
+ * whether it's already configured and a best-effort install hint.
18
+ *
19
+ * The probe target is derived from the acpx registry supplied by the transport boundary,
20
+ * so it can't drift from acpx the way a hand-copied table did. It resolves aliases too
21
+ * (`factory-droid` and `factorydroid` both reach `droid exec --output-format acp`). What we
22
+ * derive is acpx's DEFAULT command for a
23
+ * driver: its registry accepts an `overrides` map (which acpx populates from ~/.acpx/config.json)
24
+ * that we do not reconstruct, and xacpx itself may substitute a local CLI at spawn time
25
+ * (resolveLocalAgentCommand). Neither changes the answer for any driver today.
26
+ *
27
+ * `resolve()` does not throw on an unknown name — it echoes the name back — so a driver acpx
28
+ * has dropped degrades to probing a binary of the same name.
29
+ *
30
+ * The transport layer supplies `registry`; `probe` remains injectable for tests.
13
31
  */
14
- export declare function listAgentCatalog(config: AppConfig, probe?: (binary: string) => boolean): AgentCatalogEntry[];
32
+ export declare function listAgentCatalog(config: AppConfig, { registry, probe }: ListAgentCatalogOptions): AgentCatalogEntry[];
@@ -9,6 +9,7 @@ import type { OrchestrationTaskRecord } from "../orchestration/orchestration-typ
9
9
  import type { ControlEventBus } from "./control-event-bus";
10
10
  import type { AgentCatalogEntry } from "../config/agent-catalog";
11
11
  import { type DirListing, type FileContent, type SearchOptions, type SearchResult, type WorkspaceDiff } from "./workspace-fs";
12
+ import { type GitCheckoutOptions, type GitCommitResult, type GitPushOptions, type GitStatus, type GitWorktreeCreateOptions, type GitWorktreeCreateResult } from "./workspace-git";
12
13
  import type { PromptAttachmentRef } from "@ganglion/xacpx-relay-protocol";
13
14
  import type { UploadStore } from "./upload-store.js";
14
15
  import { type TurnIdleTimeoutDetail } from "./turn-support";
@@ -89,6 +90,8 @@ export interface ControlServiceDeps {
89
90
  terminal: import("./terminal-service").TerminalService;
90
91
  terminalEnabled: () => boolean;
91
92
  filesWriteEnabled: () => boolean;
93
+ /** Test/embedding override; production defaults to ~/.xacpx/worktrees. */
94
+ gitWorktreesRoot?: string;
92
95
  turnIdleTimeoutMs?: () => number;
93
96
  onTurnIdleTimeout?: (detail: TurnIdleTimeoutDetail) => void;
94
97
  }
@@ -136,12 +139,30 @@ export declare class ControlService {
136
139
  private readonly deps;
137
140
  private readonly runner;
138
141
  private readonly turnQueue;
142
+ private readonly workspaceGit;
139
143
  private readonly modelSetTails;
144
+ private readonly worktreeRegistrationTails;
140
145
  constructor(deps: ControlServiceDeps);
141
146
  private readonly workspaceFs;
142
147
  listDirectory(workspace: string, path?: string): Promise<DirListing>;
143
148
  readWorkspaceFile(workspace: string, path: string): Promise<FileContent>;
144
149
  workspaceGitDiff(workspace: string, path?: string): Promise<WorkspaceDiff>;
150
+ workspaceGitStatus(workspace: string): Promise<GitStatus>;
151
+ private mutateWorkspaceGit;
152
+ private withWorktreeRegistration;
153
+ gitStage(workspace: string, paths: string[]): Promise<void>;
154
+ gitUnstage(workspace: string, paths: string[]): Promise<void>;
155
+ gitCommit(workspace: string, message: string): Promise<GitCommitResult>;
156
+ gitFetch(workspace: string, remote?: string): Promise<void>;
157
+ gitPull(workspace: string): Promise<void>;
158
+ gitPush(workspace: string, options?: GitPushOptions): Promise<void>;
159
+ gitCheckout(workspace: string, options: GitCheckoutOptions): Promise<void>;
160
+ gitCreateWorktree(workspace: string, input: GitWorktreeCreateOptions & {
161
+ workspaceName: string;
162
+ }): Promise<{
163
+ worktree: GitWorktreeCreateResult;
164
+ workspace: ControlWorkspaceInfo;
165
+ }>;
145
166
  searchWorkspace(workspace: string, opts: SearchOptions): Promise<SearchResult>;
146
167
  fsCreate(workspace: string, path: string, kind: "file" | "dir"): Promise<{
147
168
  path: string;
@@ -0,0 +1,93 @@
1
+ export interface GitWorkspaceRef {
2
+ name: string;
3
+ cwd: string;
4
+ }
5
+ export interface GitBranchInfo {
6
+ name: string;
7
+ current: boolean;
8
+ /** Present when Git reports this branch checked out in a worktree. */
9
+ worktreePath?: string;
10
+ }
11
+ export interface GitWorktreeInfo {
12
+ path: string;
13
+ branch?: string;
14
+ detached?: boolean;
15
+ current: boolean;
16
+ linked: boolean;
17
+ }
18
+ export interface GitStatus {
19
+ workspace: string;
20
+ branch?: string;
21
+ detached: boolean;
22
+ upstream?: string;
23
+ ahead: number;
24
+ behind: number;
25
+ worktree: {
26
+ root: string;
27
+ linked: boolean;
28
+ };
29
+ files: Array<{
30
+ path: string;
31
+ status: string;
32
+ }>;
33
+ branches: GitBranchInfo[];
34
+ worktrees: GitWorktreeInfo[];
35
+ }
36
+ export interface GitCommitResult {
37
+ hash: string;
38
+ shortHash: string;
39
+ summary: string;
40
+ }
41
+ export interface GitCheckoutOptions {
42
+ branch: string;
43
+ create?: boolean;
44
+ startPoint?: string;
45
+ }
46
+ export interface GitPushOptions {
47
+ setUpstream?: boolean;
48
+ remote?: string;
49
+ }
50
+ export interface GitWorktreeCreateOptions {
51
+ branch: string;
52
+ createBranch?: boolean;
53
+ startPoint?: string;
54
+ }
55
+ export interface GitWorktreeCreateResult {
56
+ path: string;
57
+ branch: string;
58
+ linked: true;
59
+ }
60
+ export declare class WorkspaceGit {
61
+ private readonly listWorkspaces;
62
+ private readonly managedWorktreesRoot;
63
+ private readonly writeTails;
64
+ constructor(listWorkspaces: () => GitWorkspaceRef[], options?: {
65
+ managedWorktreesRoot?: string;
66
+ });
67
+ private rootFor;
68
+ private run;
69
+ private runRaw;
70
+ private validatePaths;
71
+ private validateBranch;
72
+ private validateStartPoint;
73
+ private includeRenameSources;
74
+ /** Git write commands for one configured workspace share a FIFO. Reads remain
75
+ * concurrent, and a failed write releases the next command instead of poisoning
76
+ * the queue. */
77
+ private withWrite;
78
+ stage(workspace: string, paths: string[]): Promise<void>;
79
+ unstage(workspace: string, paths: string[]): Promise<void>;
80
+ commit(workspace: string, message: string): Promise<GitCommitResult>;
81
+ checkout(workspace: string, options: GitCheckoutOptions): Promise<void>;
82
+ fetch(workspace: string, remote?: string): Promise<void>;
83
+ pull(workspace: string): Promise<void>;
84
+ push(workspace: string, options?: GitPushOptions): Promise<void>;
85
+ createWorktree(workspace: string, options: GitWorktreeCreateOptions): Promise<GitWorktreeCreateResult>;
86
+ /** Internal compensation for a failed workspace-registration step. Not exposed as a
87
+ * user-facing delete operation: it only accepts a non-current worktree beneath the
88
+ * server-managed root. */
89
+ removeManagedWorktree(workspace: string, path: string): Promise<void>;
90
+ private assertRemote;
91
+ status(workspace: string): Promise<GitStatus>;
92
+ private readWorktrees;
93
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.17.0-beta.6",
3
+ "version": "0.17.0-beta.7",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",
@@ -99,7 +99,7 @@
99
99
  "registry": "https://registry.npmjs.org/"
100
100
  },
101
101
  "engines": {
102
- "node": ">=22"
102
+ "node": ">=22.12"
103
103
  },
104
104
  "devDependencies": {
105
105
  "@types/proper-lockfile": "^4.1.4",