agent-yes 1.72.4 → 1.73.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.
@@ -1,5 +1,6 @@
1
1
  import { execFileSync } from "child_process";
2
2
  import { execaCommand } from "execa";
3
+ import { existsSync, lstatSync, readlinkSync } from "fs";
3
4
  import { mkdir, readFile, writeFile } from "fs/promises";
4
5
  import { homedir } from "os";
5
6
  import path from "path";
@@ -50,8 +51,15 @@ function detectPackageManager(): string {
50
51
  export async function checkAndAutoUpdate(): Promise<void> {
51
52
  if (process.env.AGENT_YES_NO_UPDATE) return;
52
53
 
53
- // Prevent infinite re-exec: if we just updated to this version, skip
54
- if (process.env.AGENT_YES_UPDATED === pkg.version) return;
54
+ // Prevent infinite re-exec: if we just updated, skip
55
+ if (process.env.AGENT_YES_UPDATED) return;
56
+
57
+ // Skip auto-update when running from a linked local dev checkout (git repo)
58
+ if (import.meta.url.startsWith("file://") && !import.meta.url.includes("node_modules")) {
59
+ const scriptDir = path.dirname(new URL(import.meta.url).pathname);
60
+ const repoRoot = path.resolve(scriptDir, "..");
61
+ if (existsSync(path.join(repoRoot, ".git"))) return;
62
+ }
55
63
 
56
64
  try {
57
65
  let latestVersion: string | undefined;
@@ -155,31 +163,80 @@ export function compareVersions(v1: string, v2: string): number {
155
163
  return 0;
156
164
  }
157
165
 
166
+ /**
167
+ * Detect how agent-yes was installed.
168
+ * Returns a short label: "git", "bun link", "bun", "npm", "npx", or "unknown"
169
+ */
170
+ export function detectInstallMethod(): string {
171
+ try {
172
+ // Check if running from a file path outside node_modules (git clone / bun link dev)
173
+ const scriptDir = path.dirname(new URL(import.meta.url).pathname);
174
+
175
+ if (!scriptDir.includes("node_modules")) {
176
+ // Running directly from source — is this a git repo?
177
+ const repoRoot = path.resolve(scriptDir, "..");
178
+ if (existsSync(path.join(repoRoot, ".git"))) {
179
+ return "git";
180
+ }
181
+ return "source";
182
+ }
183
+
184
+ // Check if the node_modules entry is a symlink (bun link)
185
+ const nodeModulesEntry = scriptDir.replace(/\/dist$/, "");
186
+ try {
187
+ const stat = lstatSync(nodeModulesEntry);
188
+ if (stat.isSymbolicLink()) {
189
+ const target = readlinkSync(nodeModulesEntry);
190
+ // bun link creates a symlink to the local repo
191
+ const resolvedTarget = path.resolve(path.dirname(nodeModulesEntry), target);
192
+ if (existsSync(path.join(resolvedTarget, ".git"))) {
193
+ return "bun link (git)";
194
+ }
195
+ return "bun link";
196
+ }
197
+ } catch {
198
+ // not a symlink, continue
199
+ }
200
+
201
+ // Detect package manager from path or env
202
+ if (scriptDir.includes(".bun/")) return "bun";
203
+ if (scriptDir.includes(".npm/")) return "npx";
204
+ if (process.env.npm_execpath?.includes("bun")) return "bun";
205
+ if (process.env.npm_config_user_agent?.startsWith("bun")) return "bun";
206
+ if (process.env.npm_config_user_agent?.startsWith("npm")) return "npm";
207
+
208
+ return "npm";
209
+ } catch {
210
+ return "unknown";
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Format version string with install method
216
+ */
217
+ export function versionString(): string {
218
+ return `agent-yes v${pkg.version} (${detectInstallMethod()})`;
219
+ }
220
+
158
221
  /**
159
222
  * Display version information with async latest version check
160
223
  */
161
224
  export async function displayVersion(): Promise<void> {
162
- // Display current version immediately
163
- console.log(pkg.version);
225
+ console.log(versionString());
164
226
 
165
- // Check latest version asynchronously
166
227
  const latestVersion = await fetchLatestVersion();
167
228
 
168
229
  if (latestVersion) {
169
230
  const comparison = compareVersions(pkg.version, latestVersion);
170
231
 
171
232
  if (comparison < 0) {
172
- // Current version is older
173
233
  console.log(`\x1b[33m${latestVersion} (update available)\x1b[0m`);
174
234
  } else if (comparison > 0) {
175
- // Current version is newer (pre-release or local dev)
176
235
  console.log(`${latestVersion} (latest published)`);
177
236
  } else {
178
- // Versions are equal
179
237
  console.log(`${latestVersion} (latest)`);
180
238
  }
181
239
  } else {
182
- // Failed to fetch latest version
183
240
  console.log("(unable to check for updates)");
184
241
  }
185
242
  }
@@ -0,0 +1,130 @@
1
+ import xterm from "@xterm/headless";
2
+ const { Terminal } = xterm;
3
+ import { logger } from "./logger.ts";
4
+
5
+ /**
6
+ * XtermProxy wraps @xterm/headless to act as a full xterm terminal emulator
7
+ * between a PTY process and downstream consumers.
8
+ *
9
+ * It automatically responds to ALL terminal queries (DSR, DA, OSC, etc.)
10
+ * by piping xterm's onData responses back to the PTY — so the spawned
11
+ * process never blocks waiting for a terminal reply, even in non-TTY
12
+ * environments or when the real terminal is backgrounded.
13
+ */
14
+ export class XtermProxy {
15
+ private term: Terminal;
16
+ private writeToPty: (data: string) => void;
17
+ private readableController: ReadableStreamDefaultController<string> | null = null;
18
+
19
+ /** Downstream readable — passthrough of PTY output for sflow pipeline */
20
+ readonly readable: ReadableStream<string>;
21
+
22
+ constructor(opts: { cols?: number; rows?: number; writeToPty: (data: string) => void }) {
23
+ const cols = opts.cols ?? 80;
24
+ const rows = opts.rows ?? 24;
25
+ this.writeToPty = opts.writeToPty;
26
+
27
+ this.term = new Terminal({
28
+ cols,
29
+ rows,
30
+ allowProposedApi: true,
31
+ scrollback: 10000,
32
+ });
33
+
34
+ // xterm internally generates responses to terminal queries (DSR, DA, etc.)
35
+ // and fires them via onData. Pipe those back to the PTY stdin.
36
+ this.term.onData((data) => {
37
+ logger.debug("xterm-proxy|onData response", data);
38
+ this.writeToPty(data);
39
+ });
40
+
41
+ // Create a ReadableStream for downstream consumption (sflow pipeline)
42
+ this.readable = new ReadableStream<string>({
43
+ start: (controller) => {
44
+ this.readableController = controller;
45
+ },
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Feed PTY output into the xterm emulator.
51
+ * - xterm processes escape sequences and updates internal state
52
+ * - Terminal queries (ESC[6n, ESC[c, etc.) trigger onData → writeToPty
53
+ * - Raw data is pushed to readable for downstream consumption
54
+ */
55
+ write(data: string): void {
56
+ // Feed to xterm for state tracking and query auto-response first.
57
+ // xterm.write() is buffered/async, so only emit to downstream once the
58
+ // terminal state has been updated for this chunk.
59
+ this.term.write(data, () => {
60
+ try {
61
+ this.readableController?.enqueue(data);
62
+ } catch {
63
+ // Stream already closed/canceled — ignore
64
+ }
65
+ });
66
+ }
67
+
68
+ /** Get cursor position from xterm's buffer state */
69
+ getCursorPosition(): { row: number; col: number } {
70
+ const buf = this.term.buffer.active;
71
+ // xterm uses 0-based; terminal-render used 0-based too
72
+ return { row: buf.cursorY, col: buf.cursorX };
73
+ }
74
+
75
+ /**
76
+ * Get the last N lines of rendered terminal content (plain text, no ANSI).
77
+ * Equivalent to terminal-render's tail(n).
78
+ */
79
+ tail(n: number): string {
80
+ const buf = this.term.buffer.active;
81
+ const totalLines = buf.length;
82
+ const startLine = Math.max(0, totalLines - n);
83
+ const lines: string[] = [];
84
+ for (let i = startLine; i < totalLines; i++) {
85
+ const line = buf.getLine(i);
86
+ lines.push(line ? line.translateToString(true) : "");
87
+ }
88
+ // Trim trailing empty lines
89
+ while (lines.length > 1 && lines[lines.length - 1] === "") {
90
+ lines.pop();
91
+ }
92
+ return lines.join("\n");
93
+ }
94
+
95
+ /**
96
+ * Render the full terminal buffer as plain text.
97
+ * Equivalent to terminal-render's render().
98
+ */
99
+ render(): string {
100
+ const buf = this.term.buffer.active;
101
+ const lines: string[] = [];
102
+ for (let i = 0; i < buf.length; i++) {
103
+ const line = buf.getLine(i);
104
+ lines.push(line ? line.translateToString(true) : "");
105
+ }
106
+ // Trim trailing empty lines
107
+ while (lines.length > 1 && lines[lines.length - 1] === "") {
108
+ lines.pop();
109
+ }
110
+ return lines.join("\n");
111
+ }
112
+
113
+ /** Resize the virtual terminal */
114
+ resize(cols: number, rows: number): void {
115
+ this.term.resize(cols, rows);
116
+ }
117
+
118
+ /** Clean up resources */
119
+ dispose(): void {
120
+ if (this.readableController) {
121
+ try {
122
+ this.readableController.close();
123
+ } catch {
124
+ // Already closed
125
+ }
126
+ this.readableController = null;
127
+ }
128
+ this.term.dispose();
129
+ }
130
+ }