@ganglion/xacpx 0.17.0-beta.2 → 0.17.0-beta.4

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/cli.js CHANGED
@@ -25585,6 +25585,10 @@ function terminalIdleTimeoutSeconds(config4) {
25585
25585
  const v = config4.terminal?.idleTimeoutSeconds;
25586
25586
  return typeof v === "number" && v > 0 ? v : 900;
25587
25587
  }
25588
+ function terminalShell(config4) {
25589
+ const v = config4.terminal?.shell;
25590
+ return typeof v === "string" && v.trim() ? v : undefined;
25591
+ }
25588
25592
  function filesWriteEnabled(config4) {
25589
25593
  return config4.files?.writeEnabled === true;
25590
25594
  }
@@ -33889,6 +33893,7 @@ class WorkspaceFs {
33889
33893
  path: rel,
33890
33894
  content: binary ? "" : slice.toString("utf8"),
33891
33895
  size: info.size,
33896
+ mtimeMs: info.mtimeMs,
33892
33897
  truncated: info.size > FILE_READ_CAP,
33893
33898
  binary
33894
33899
  };
@@ -33896,6 +33901,23 @@ class WorkspaceFs {
33896
33901
  await fh.close();
33897
33902
  }
33898
33903
  }
33904
+ async writeFile(workspace3, relPath, content, expected) {
33905
+ const { abs, rel } = await this.resolve(workspace3, relPath);
33906
+ const info = await stat3(abs);
33907
+ if (!info.isFile())
33908
+ throw new Error("not-a-file");
33909
+ if (info.size > FILE_READ_CAP)
33910
+ throw new Error("file-too-large");
33911
+ if (content.includes("\x00"))
33912
+ throw new Error("is-binary");
33913
+ if (Buffer.byteLength(content, "utf8") > FILE_READ_CAP)
33914
+ throw new Error("file-too-large");
33915
+ if (info.mtimeMs !== expected.mtimeMs || info.size !== expected.size)
33916
+ throw new Error("stale-write");
33917
+ await import_write_file_atomic3.default(abs, content);
33918
+ const after = await stat3(abs);
33919
+ return { path: rel, mtimeMs: after.mtimeMs, size: after.size };
33920
+ }
33899
33921
  async search(workspace3, opts) {
33900
33922
  const { root, abs: base, rel: scopeRel } = await this.resolve(workspace3, opts.path);
33901
33923
  const query = opts.query.trim();
@@ -34276,8 +34298,9 @@ function globToRegExp(glob) {
34276
34298
  return /$^/;
34277
34299
  }
34278
34300
  }
34279
- var execFileAsync, MAX_ENTRIES = 2000, FILE_READ_CAP, DIFF_CAP, GIT_MAX_BUFFER, SEARCH_MAX_RESULTS = 200, SEARCH_MAX_SCAN = 20000, SEARCH_SKIP_DIRS, SEARCH_CONTENT_MAX_HITS = 500, SEARCH_CONTENT_MAX_FILE, LINE_MATCH_CAP = 2000, DOWNLOAD_MAX, GIT_TIMEOUT_MS = 1e4, SEARCH_DEADLINE_MS = 4000, MIME_BY_EXT;
34301
+ var import_write_file_atomic3, execFileAsync, MAX_ENTRIES = 2000, FILE_READ_CAP, DIFF_CAP, GIT_MAX_BUFFER, SEARCH_MAX_RESULTS = 200, SEARCH_MAX_SCAN = 20000, SEARCH_SKIP_DIRS, SEARCH_CONTENT_MAX_HITS = 500, SEARCH_CONTENT_MAX_FILE, LINE_MATCH_CAP = 2000, DOWNLOAD_MAX, GIT_TIMEOUT_MS = 1e4, SEARCH_DEADLINE_MS = 4000, MIME_BY_EXT;
34280
34302
  var init_workspace_fs = __esm(() => {
34303
+ import_write_file_atomic3 = __toESM(require_lib(), 1);
34281
34304
  execFileAsync = promisify(execFile);
34282
34305
  FILE_READ_CAP = 256 * 1024;
34283
34306
  DIFF_CAP = 512 * 1024;
@@ -34348,6 +34371,11 @@ class ControlService {
34348
34371
  throw new Error("files-write-disabled");
34349
34372
  return this.workspaceFs.duplicate(workspace3, path16);
34350
34373
  }
34374
+ async fsWrite(workspace3, path16, content, expected) {
34375
+ if (!this.deps.filesWriteEnabled())
34376
+ throw new Error("files-write-disabled");
34377
+ return this.workspaceFs.writeFile(workspace3, path16, content, expected);
34378
+ }
34351
34379
  async fsDownload(workspace3, path16) {
34352
34380
  return this.workspaceFs.readFileBytes(workspace3, path16);
34353
34381
  }
@@ -34816,6 +34844,11 @@ ${chunk}` : chunk
34816
34844
  throw new Error("session-not-found");
34817
34845
  return this.deps.terminal.create({ cwd: session3.cwd, cols, rows });
34818
34846
  }
34847
+ attachTerminal(terminalId) {
34848
+ if (!this.deps.terminalEnabled())
34849
+ throw new Error("terminal-disabled");
34850
+ return this.deps.terminal.attach(terminalId);
34851
+ }
34819
34852
  writeTerminal(terminalId, data) {
34820
34853
  this.deps.terminal.write(terminalId, data);
34821
34854
  }
@@ -34862,6 +34895,7 @@ var init_control_service = __esm(() => {
34862
34895
  // src/control/terminal-service.ts
34863
34896
  import { randomUUID as randomUUID4 } from "node:crypto";
34864
34897
  import { createRequire as createRequire6 } from "node:module";
34898
+ import { statSync as statSync3 } from "node:fs";
34865
34899
  import { spawn as spawnPty2 } from "node-pty";
34866
34900
  function scrubEnv() {
34867
34901
  const out = {};
@@ -34876,9 +34910,33 @@ function scrubEnv() {
34876
34910
  out.LANG = out.LANG ?? "en_US.UTF-8";
34877
34911
  return out;
34878
34912
  }
34879
- function defaultShell(platform) {
34880
- if (process.env.SHELL)
34881
- return process.env.SHELL;
34913
+ function defaultExists(p) {
34914
+ try {
34915
+ return statSync3(p).isFile();
34916
+ } catch {
34917
+ return false;
34918
+ }
34919
+ }
34920
+ function resolveShell(args) {
34921
+ const { platform, env, shellOverride } = args;
34922
+ const exists = args.exists ?? defaultExists;
34923
+ if (shellOverride && shellOverride.trim())
34924
+ return shellOverride;
34925
+ if (platform === "win32") {
34926
+ const pathValue = env.PATH ?? env.Path ?? "";
34927
+ const dirs = pathValue.split(";").filter(Boolean);
34928
+ for (const name of ["pwsh.exe", "powershell.exe"]) {
34929
+ for (const dir of dirs) {
34930
+ const clean = dir.replace(/^"|"$/g, "");
34931
+ const full = `${clean}\\${name}`;
34932
+ if (exists(full))
34933
+ return full;
34934
+ }
34935
+ }
34936
+ return env.ComSpec ?? "cmd.exe";
34937
+ }
34938
+ if (env.SHELL)
34939
+ return env.SHELL;
34882
34940
  return platform === "darwin" ? "/bin/zsh" : "/bin/bash";
34883
34941
  }
34884
34942
  function realPtySpawn(file, args, opts) {
@@ -34886,6 +34944,26 @@ function realPtySpawn(file, args, opts) {
34886
34944
  ensureNodePtyHelperExecutable(helperPath);
34887
34945
  return spawnPty2(file, args, opts);
34888
34946
  }
34947
+ function appendToBuffer(session3, data) {
34948
+ session3.buffer += data;
34949
+ session3.bufBytes += Buffer.byteLength(data, "utf8");
34950
+ if (session3.bufBytes <= MAX_BUFFER_BYTES)
34951
+ return;
34952
+ while (session3.bufBytes > MAX_BUFFER_BYTES) {
34953
+ const nl = session3.buffer.indexOf(`
34954
+ `);
34955
+ if (nl === -1)
34956
+ break;
34957
+ const removed = session3.buffer.slice(0, nl + 1);
34958
+ session3.buffer = session3.buffer.slice(nl + 1);
34959
+ session3.bufBytes -= Buffer.byteLength(removed, "utf8");
34960
+ }
34961
+ while (session3.bufBytes > MAX_BUFFER_BYTES && session3.buffer.length > 0) {
34962
+ const ch = session3.buffer[0];
34963
+ session3.buffer = session3.buffer.slice(1);
34964
+ session3.bufBytes -= Buffer.byteLength(ch, "utf8");
34965
+ }
34966
+ }
34889
34967
  function createTerminalService(deps) {
34890
34968
  const spawn12 = deps.spawn ?? realPtySpawn;
34891
34969
  const platform = deps.platform ?? process.platform;
@@ -34910,13 +34988,13 @@ function createTerminalService(deps) {
34910
34988
  };
34911
34989
  return {
34912
34990
  create({ cwd, cols, rows }) {
34913
- if (platform === "win32")
34914
- throw new Error("terminal-unsupported-platform");
34991
+ const shell = resolveShell({ platform, env: process.env, shellOverride: deps.shell?.() });
34915
34992
  const terminalId = randomUUID4();
34916
- const handle = spawn12(defaultShell(platform), [], { name: "xterm-256color", cols, rows, cwd, env: scrubEnv() });
34917
- const session3 = { handle, seq: 0, idleTimer: null };
34993
+ const handle = spawn12(shell, [], { name: "xterm-256color", cols, rows, cwd, env: scrubEnv() });
34994
+ const session3 = { handle, seq: 0, idleTimer: null, buffer: "", bufBytes: 0 };
34918
34995
  sessions.set(terminalId, session3);
34919
34996
  handle.onData((data) => {
34997
+ appendToBuffer(session3, data);
34920
34998
  deps.events.emit({ type: "terminal-output", terminalId, seq: session3.seq++, data });
34921
34999
  });
34922
35000
  handle.onExit(({ exitCode }) => {
@@ -34928,6 +35006,13 @@ function createTerminalService(deps) {
34928
35006
  resetIdle(terminalId);
34929
35007
  return { terminalId };
34930
35008
  },
35009
+ attach(terminalId) {
35010
+ const s = sessions.get(terminalId);
35011
+ if (!s)
35012
+ return { ok: false };
35013
+ resetIdle(terminalId);
35014
+ return { ok: true, buffer: s.buffer, lastSeq: s.seq - 1 };
35015
+ },
34931
35016
  write(terminalId, data) {
34932
35017
  const s = sessions.get(terminalId);
34933
35018
  if (!s)
@@ -34966,7 +35051,7 @@ function createTerminalService(deps) {
34966
35051
  }
34967
35052
  };
34968
35053
  }
34969
- var require5, SENSITIVE_ENV_KEYS;
35054
+ var require5, SENSITIVE_ENV_KEYS, MAX_BUFFER_BYTES;
34970
35055
  var init_terminal_service = __esm(() => {
34971
35056
  init_node_pty_helper();
34972
35057
  require5 = createRequire6(import.meta.url);
@@ -34984,6 +35069,7 @@ var init_terminal_service = __esm(() => {
34984
35069
  "GITHUB_TOKEN",
34985
35070
  "NPM_TOKEN"
34986
35071
  ];
35072
+ MAX_BUFFER_BYTES = 256 * 1024;
34987
35073
  });
34988
35074
 
34989
35075
  // src/control/upload-store.ts
@@ -35661,7 +35747,8 @@ async function buildApp(paths, deps = {}) {
35661
35747
  const controlEvents = createControlEventBus(logger2);
35662
35748
  const terminalService = createTerminalService({
35663
35749
  events: controlEvents,
35664
- idleTimeoutSeconds: () => terminalIdleTimeoutSeconds(config4)
35750
+ idleTimeoutSeconds: () => terminalIdleTimeoutSeconds(config4),
35751
+ shell: () => terminalShell(config4)
35665
35752
  });
35666
35753
  const uploadStore = new UploadStore;
35667
35754
  uploadStore.cleanup();
@@ -47,6 +47,8 @@ export interface TerminalConfig {
47
47
  enabled: boolean;
48
48
  /** Idle seconds before a terminal PTY is auto-killed. Defaults to 900 (15 min). */
49
49
  idleTimeoutSeconds?: number;
50
+ /** Explicit shell override (absolute path or bare name). Cross-platform: wins over SHELL / platform default. */
51
+ shell?: string;
50
52
  }
51
53
  export interface FilesConfig {
52
54
  /** Default false. When false, all fs write ops (new/rename/delete/copy) are rejected
@@ -119,4 +121,5 @@ export interface AppConfig {
119
121
  }
120
122
  export declare function terminalEnabled(config: AppConfig): boolean;
121
123
  export declare function terminalIdleTimeoutSeconds(config: AppConfig): number;
124
+ export declare function terminalShell(config: AppConfig): string | undefined;
122
125
  export declare function filesWriteEnabled(config: AppConfig): boolean;
@@ -152,6 +152,14 @@ export declare class ControlService {
152
152
  fsCopy(workspace: string, path: string): Promise<{
153
153
  path: string;
154
154
  }>;
155
+ fsWrite(workspace: string, path: string, content: string, expected: {
156
+ mtimeMs: number;
157
+ size: number;
158
+ }): Promise<{
159
+ path: string;
160
+ mtimeMs: number;
161
+ size: number;
162
+ }>;
155
163
  fsDownload(workspace: string, path: string): Promise<{
156
164
  path: string;
157
165
  base64: string;
@@ -233,6 +241,7 @@ export declare class ControlService {
233
241
  createTerminal(chatKey: string, sessionAlias: string, cols: number, rows: number): Promise<{
234
242
  terminalId: string;
235
243
  }>;
244
+ attachTerminal(terminalId: string): import("./terminal-service").TerminalAttachResult;
236
245
  writeTerminal(terminalId: string, data: string): void;
237
246
  resizeTerminal(terminalId: string, cols: number, rows: number): void;
238
247
  closeTerminal(terminalId: string): void;
@@ -27,10 +27,18 @@ export interface TerminalCreateInput {
27
27
  cols: number;
28
28
  rows: number;
29
29
  }
30
+ export type TerminalAttachResult = {
31
+ ok: false;
32
+ } | {
33
+ ok: true;
34
+ buffer: string;
35
+ lastSeq: number;
36
+ };
30
37
  export interface TerminalService {
31
38
  create(input: TerminalCreateInput): {
32
39
  terminalId: string;
33
40
  };
41
+ attach(terminalId: string): TerminalAttachResult;
34
42
  write(terminalId: string, data: string): void;
35
43
  resize(terminalId: string, cols: number, rows: number): void;
36
44
  close(terminalId: string): void;
@@ -39,10 +47,25 @@ export interface TerminalService {
39
47
  export interface TerminalServiceDeps {
40
48
  events: ControlEventBus;
41
49
  idleTimeoutSeconds: () => number;
50
+ /** Optional explicit shell override from config (terminal.shell). */
51
+ shell?: () => string | undefined;
42
52
  spawn?: PtySpawn;
43
53
  platform?: NodeJS.Platform;
44
54
  /** Injectable timer primitives; defaults to global setTimeout/clearTimeout. */
45
55
  setTimer?: (fn: () => void, ms: number) => unknown;
46
56
  clearTimer?: (id: unknown) => void;
47
57
  }
58
+ export interface ResolveShellArgs {
59
+ platform: NodeJS.Platform;
60
+ env: NodeJS.ProcessEnv;
61
+ /** Explicit override from config terminal.shell — wins on every platform. */
62
+ shellOverride?: string;
63
+ /** Injectable executable-existence predicate (defaults to fs statSync). */
64
+ exists?: (p: string) => boolean;
65
+ }
66
+ /** Pick the shell to spawn. Priority: explicit override -> platform default.
67
+ * win32 deliberately ignores SHELL (git-bash sets it to an MSYS path that
68
+ * node-pty can't spawn on Windows) and scans PATH for pwsh -> powershell,
69
+ * falling back to %ComSpec% (cmd.exe). Never throws — always returns a value. */
70
+ export declare function resolveShell(args: ResolveShellArgs): string;
48
71
  export declare function createTerminalService(deps: TerminalServiceDeps): TerminalService;
@@ -19,6 +19,7 @@ export interface FileContent {
19
19
  path: string;
20
20
  content: string;
21
21
  size: number;
22
+ mtimeMs: number;
22
23
  truncated: boolean;
23
24
  binary: boolean;
24
25
  }
@@ -109,6 +110,18 @@ export declare class WorkspaceFs {
109
110
  * "nothing ignored". Never throws. */
110
111
  private gitCheckIgnore;
111
112
  readFile(workspace: string, relPath: string): Promise<FileContent>;
113
+ /** Overwrite an EXISTING file's content, guarded by a stale-write token. The target must
114
+ * resolve inside the workspace (realpath containment), be a regular file, and match the
115
+ * caller's `{mtimeMs,size}` token — otherwise `stale-write`. Content must be UTF-8 text
116
+ * (no NUL) and within the read cap. Writes atomically. Returns a fresh token. */
117
+ writeFile(workspace: string, relPath: string, content: string, expected: {
118
+ mtimeMs: number;
119
+ size: number;
120
+ }): Promise<{
121
+ path: string;
122
+ mtimeMs: number;
123
+ size: number;
124
+ }>;
112
125
  /** Find files by name (default) or content. Name mode walks relative paths
113
126
  * breadth-first, skipping `.git`/`node_modules` and never following symlinks (so it
114
127
  * stays contained), applying the query plus any include/exclude globs. Content mode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.17.0-beta.2",
3
+ "version": "0.17.0-beta.4",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",