claude-yes 1.17.1 → 1.19.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/idleWaiter.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * A utility class to wait for idle periods based on activity pings.
3
+ *
4
+ * @example
5
+ * const idleWaiter = new IdleWaiter();
6
+ *
7
+ * // Somewhere in your code, when activity occurs:
8
+ * idleWaiter.ping();
9
+ *
10
+ * // To wait for an idle period of 5 seconds:
11
+ * await idleWaiter.wait(5000);
12
+ * console.log('System has been idle for 5 seconds');
13
+ */
14
+ export class IdleWaiter {
15
+ lastActivityTime = Date.now();
16
+ checkInterval = 100; // Default check interval in milliseconds
17
+
18
+ constructor() {
19
+ this.ping();
20
+ }
21
+
22
+ ping() {
23
+ this.lastActivityTime = Date.now();
24
+ return this;
25
+ }
26
+
27
+ async wait(ms: number) {
28
+ while (this.lastActivityTime >= Date.now() - ms)
29
+ await new Promise((resolve) => setTimeout(resolve, this.checkInterval));
30
+ }
31
+ }
package/index.ts CHANGED
@@ -1,37 +1,93 @@
1
1
  import { fromReadable, fromWritable } from 'from-node-stream';
2
+ import { mkdir, writeFile } from 'fs/promises';
3
+ import path from 'path';
4
+ import DIE from 'phpdie';
2
5
  import sflow from 'sflow';
3
- import { createIdleWatcher } from './createIdleWatcher';
4
- import { removeControlCharacters } from './removeControlCharacters';
5
- import { sleepms } from './utils';
6
6
  import { TerminalTextRender } from 'terminal-render';
7
- import { writeFile } from 'fs/promises';
8
- import path from 'path';
9
- import { mkdir } from 'fs/promises';
7
+ import { IdleWaiter } from './idleWaiter';
10
8
  import { ReadyManager } from './ReadyManager';
9
+ import { removeControlCharacters } from './removeControlCharacters';
11
10
 
11
+ export const CLI_CONFIGURES = {
12
+ claude: {
13
+ ready: /^> /, // regex matcher for stdin ready,
14
+ enter: [/❯ 1. Yes/, /❯ 1. Dark mode✔/, /Press Enter to continue…/],
15
+ fatal: [
16
+ /No conversation found to continue/,
17
+ /⎿ {2}Claude usage limit reached\./,
18
+ ],
19
+ },
20
+ gemini: {
21
+ // match the agent prompt after initial lines; handled by index logic using line index
22
+ ready: /Type your message/, // used with line index check
23
+ enter: [/│ ● 1. Yes, allow once/],
24
+ fatal: [],
25
+ },
26
+ codex: {
27
+ ready: /⏎ send/,
28
+ enter: [/ > 1. Approve/, /> 1. Yes, allow Codex to work in this folder/],
29
+ fatal: [/Error: The cursor position could not be read within/],
30
+ // add to codex --search by default when not provided by the user
31
+ ensureArgs: (args: string[]) => {
32
+ if (!args.includes('--search')) return ['--search', ...args];
33
+ return args;
34
+ },
35
+ },
36
+ copilot: {
37
+ ready: /^ > /,
38
+ enter: [/ │ ❯ 1. Yes, proceed/, /❯ 1. Yes/],
39
+ fatal: [],
40
+ },
41
+ cursor: {
42
+ // map logical "cursor" cli name to actual binary name
43
+ binary: 'cursor-agent',
44
+ ready: /\/ commands/,
45
+ enter: [/→ Run \(once\) \(y\) \(enter\)/, /▶ \[a\] Trust this workspace/],
46
+ fatal: [],
47
+ },
48
+ };
12
49
  /**
13
- * Main function to run Claude with automatic yes/no respojnses
50
+ * Main function to run Claude with automatic yes/no responses
14
51
  * @param options Configuration options
15
52
  * @param options.continueOnCrash - If true, automatically restart Claude when it crashes:
16
53
  * 1. Shows message 'Claude crashed, restarting..'
17
54
  * 2. Spawns a new 'claude --continue' process
18
55
  * 3. Re-attaches the new process to the shell stdio (pipes new process stdin/stdout)
19
56
  * 4. If it crashes with "No conversation found to continue", exits the process
20
- * @param options.exitOnIdle - Exit when Claude is idle. Boolean or timeout in milliseconds
57
+ * @param options.exitOnIdle - Exit when Claude is idle. Boolean or timeout in milliseconds, recommended 5000 - 60000, default is false
21
58
  * @param options.claudeArgs - Additional arguments to pass to the Claude CLI
22
59
  * @param options.removeControlCharactersFromStdout - Remove ANSI control characters from stdout. Defaults to !process.stdout.isTTY
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * import claudeYes from 'claude-yes';
64
+ * await claudeYes({
65
+ * prompt: 'help me solve all todos in my codebase',
66
+ *
67
+ * // optional
68
+ * cli: 'claude',
69
+ * cliArgs: ['--verbose'], // additional args to pass to claude
70
+ * exitOnIdle: 30000, // exit after 30 seconds of idle
71
+ * continueOnCrash: true, // restart if claude crashes, default is true
72
+ * logFile: 'claude.log', // save logs to file
73
+ * });
74
+ * ```
23
75
  */
24
76
  export default async function claudeYes({
25
- claudeArgs = [],
77
+ cli = 'claude',
78
+ cliArgs = [],
79
+ prompt,
26
80
  continueOnCrash,
27
81
  cwd,
28
82
  env,
29
- exitOnIdle = 60e3,
83
+ exitOnIdle,
30
84
  logFile,
31
85
  removeControlCharactersFromStdout = false, // = !process.stdout.isTTY,
32
86
  verbose = false,
33
87
  }: {
34
- claudeArgs?: string[];
88
+ cli?: (string & {}) | keyof typeof CLI_CONFIGURES;
89
+ cliArgs?: string[];
90
+ prompt?: string;
35
91
  continueOnCrash?: boolean;
36
92
  cwd?: string;
37
93
  env?: Record<string, string>;
@@ -40,52 +96,60 @@ export default async function claudeYes({
40
96
  removeControlCharactersFromStdout?: boolean;
41
97
  verbose?: boolean;
42
98
  } = {}) {
43
- if (verbose) {
44
- console.log('calling claudeYes: ', {
45
- continueOnCrash,
46
- exitOnIdle,
47
- claudeArgs,
48
- cwd,
49
- removeControlCharactersFromStdout,
50
- logFile,
51
- verbose,
52
- });
53
- }
54
- console.log(
55
- '⭐ Starting claude, automatically responding to yes/no prompts...'
56
- );
57
- console.log(
58
- '⚠️ Important Security Warning: Only run this on trusted repositories. This tool automatically responds to prompts and can execute commands without user confirmation. Be aware of potential prompt injection attacks where malicious code or instructions could be embedded in files or user inputs to manipulate the automated responses.'
59
- );
60
-
61
- process.stdin.setRawMode?.(true); //must be called any stdout/stdin usage
62
- const prefix = ''; // "YESC|"
63
- const PREFIXLENGTH = prefix.length;
64
- let errorNoConversation = false; // match 'No conversation found to continue'
65
- const shellReady = new ReadyManager();
99
+ const continueArgs = {
100
+ codex: 'resume --last'.split(' '),
101
+ claude: '--continue'.split(' '),
102
+ gemini: [], // not possible yet
103
+ };
104
+
105
+ // if (verbose) {
106
+ // console.log('calling claudeYes: ', {
107
+ // cli,
108
+ // continueOnCrash,
109
+ // exitOnIdle,
110
+ // cliArgs,
111
+ // cwd,
112
+ // removeControlCharactersFromStdout,
113
+ // logFile,
114
+ // verbose,
115
+ // });
116
+ // }
117
+ // console.log(
118
+ // `⭐ Starting ${cli}, automatically responding to yes/no prompts...`
119
+ // );
120
+ // console.log(
121
+ // '⚠️ Important Security Warning: Only run this on trusted repositories. This tool automatically responds to prompts and can execute commands without user confirmation. Be aware of potential prompt injection attacks where malicious code or instructions could be embedded in files or user inputs to manipulate the automated responses.'
122
+ // );
123
+
124
+ process.stdin.setRawMode?.(true); // must be called any stdout/stdin usage
125
+ let isFatal = false; // match 'No conversation found to continue'
126
+ const stdinReady = new ReadyManager();
66
127
 
67
128
  const shellOutputStream = new TransformStream<string, string>();
68
129
  const outputWriter = shellOutputStream.writable.getWriter();
69
130
  // const pty = await import('node-pty');
70
131
 
71
- // recommened to use bun pty in windows
72
- const pty = process.versions.bun
73
- ? await import('bun-pty').catch(() => {
74
- throw new Error('Please install bun-pty');
75
- })
76
- : await import('node-pty').catch(() => {
77
- throw new Error('Please install node-pty');
78
- });
132
+ // its recommened to use bun-pty in windows
133
+ const pty = await import('node-pty')
134
+ .catch(async () => await import('bun-pty'))
135
+ .catch(async () =>
136
+ DIE('Please install node-pty or bun-pty, run this: bun install bun-pty'),
137
+ );
79
138
 
80
139
  const getPtyOptions = () => ({
81
140
  name: 'xterm-color',
82
- cols: process.stdout.columns - PREFIXLENGTH,
83
- rows: process.stdout.rows,
141
+ ...getTerminalDimensions(),
84
142
  cwd: cwd ?? process.cwd(),
85
143
  env: env ?? (process.env as Record<string, string>),
86
144
  });
87
- let shell = pty.spawn('claude', claudeArgs, getPtyOptions());
88
- let pendingExitCode = Promise.withResolvers<number | null>();
145
+
146
+ // Apply CLI specific configurations (moved to CLI_CONFIGURES)
147
+ const cliConf = (CLI_CONFIGURES as Record<string, any>)[cli] || {};
148
+ cliArgs = cliConf.ensureArgs?.(cliArgs) ?? cliArgs;
149
+ const cliCommand = cliConf?.binary || cli;
150
+
151
+ let shell = pty.spawn(cliCommand, cliArgs, getPtyOptions());
152
+ const pendingExitCode = Promise.withResolvers<number | null>();
89
153
  let pendingExitCodeValue = null;
90
154
 
91
155
  // TODO handle error if claude is not installed, show msg:
@@ -94,23 +158,29 @@ export default async function claudeYes({
94
158
  async function onData(data: string) {
95
159
  // append data to the buffer, so we can process it later
96
160
  await outputWriter.write(data);
97
- shellReady.ready(); // shell has output, also means ready for stdin
98
161
  }
99
162
 
100
163
  shell.onData(onData);
101
164
  shell.onExit(function onExit({ exitCode }) {
102
- shellReady.unready(); // start buffer stdin
103
- const claudeCrashed = exitCode !== 0;
104
- if (claudeCrashed && continueOnCrash) {
105
- if (errorNoConversation) {
165
+ stdinReady.unready(); // start buffer stdin
166
+ const agentCrashed = exitCode !== 0;
167
+ const continueArg = (continueArgs as Record<string, string[]>)[cli];
168
+
169
+ if (agentCrashed && continueOnCrash && continueArg) {
170
+ if (!continueArg) {
171
+ return console.warn(
172
+ `continueOnCrash is only supported for ${Object.keys(continueArgs).join(', ')} currently, not ${cli}`,
173
+ );
174
+ }
175
+ if (isFatal) {
106
176
  console.log(
107
- 'Claude crashed with "No conversation found to continue", exiting...'
177
+ `${cli} crashed with "No conversation found to continue", exiting...`,
108
178
  );
109
179
  return pendingExitCode.resolve((pendingExitCodeValue = exitCode));
110
180
  }
111
- console.log('Claude crashed, restarting...');
181
+ console.log(`${cli} crashed, restarting...`);
112
182
 
113
- shell = pty.spawn('claude', ['--continue', 'continue'], getPtyOptions());
183
+ shell = pty.spawn(cli, continueArg, getPtyOptions());
114
184
  shell.onData(onData);
115
185
  shell.onExit(onExit);
116
186
  return;
@@ -118,112 +188,194 @@ export default async function claudeYes({
118
188
  return pendingExitCode.resolve((pendingExitCodeValue = exitCode));
119
189
  });
120
190
 
121
- const exitClaudeCode = async () => {
122
- continueOnCrash = false;
123
- // send exit command to the shell, must sleep a bit to avoid claude treat it as pasted input
124
- await sflow(['\r', '/exit', '\r'])
125
- .forEach(async () => await sleepms(200))
126
- .forEach(async (e) => shell.write(e))
127
- .run();
128
-
129
- // wait for shell to exit or kill it with a timeout
130
- let exited = false;
131
- await Promise.race([
132
- pendingExitCode.promise.then(() => (exited = true)), // resolve when shell exits
133
- // if shell doesn't exit in 5 seconds, kill it
134
- new Promise<void>((resolve) =>
135
- setTimeout(() => {
136
- if (exited) return; // if shell already exited, do nothing
137
- shell.kill(); // kill the shell process if it doesn't exit in time
138
- resolve();
139
- }, 5000)
140
- ), // 5 seconds timeout
141
- ]);
142
- };
143
-
144
191
  // when current tty resized, resize the pty
145
192
  process.stdout.on('resize', () => {
146
- const { columns, rows } = process.stdout;
147
- shell.resize(columns - PREFIXLENGTH, rows);
193
+ const { cols, rows } = getTerminalDimensions(); // minimum 80 columns to avoid layout issues
194
+ shell.resize(cols, rows); // minimum 80 columns to avoid layout issues
148
195
  });
149
196
 
150
- const render = new TerminalTextRender();
151
- const idleWatcher = !exitOnIdle
152
- ? null
153
- : createIdleWatcher(async () => {
154
- if (
155
- render
156
- .render()
157
- .replace(/\s+/g, ' ')
158
- .match(/esc to interrupt|to run in background/)
159
- ) {
160
- console.log(
161
- '[claude-yes] Claude is idle, but seems still working, not exiting yet'
162
- );
163
- } else {
164
- console.log('[claude-yes] Claude is idle, exiting...');
165
- await exitClaudeCode();
166
- }
167
- }, exitOnIdle);
168
- const confirm = async () => {
169
- await sleepms(200);
170
- shell.write('\r');
171
- };
197
+ const terminalRender = new TerminalTextRender();
198
+ const isStillWorkingQ = () =>
199
+ terminalRender
200
+ .render()
201
+ .replace(/\s+/g, ' ')
202
+ .match(/esc to interrupt|to run in background/);
172
203
 
204
+ const idleWaiter = new IdleWaiter();
205
+ if (exitOnIdle)
206
+ idleWaiter.wait(exitOnIdle).then(async () => {
207
+ if (isStillWorkingQ()) {
208
+ console.log(
209
+ '[${cli}-yes] ${cli} is idle, but seems still working, not exiting yet',
210
+ );
211
+ return;
212
+ }
213
+
214
+ console.log('[${cli}-yes] ${cli} is idle, exiting...');
215
+ await exitAgent();
216
+ });
217
+
218
+ // Message streaming
173
219
  sflow(fromReadable<Buffer>(process.stdin))
174
220
  .map((buffer) => buffer.toString())
221
+ // .map((e) => e.replaceAll('\x1a', '')) // remove ctrl+z from user's input (seems bug)
175
222
  // .forEach(e => appendFile('.cache/io.log', "input |" + JSON.stringify(e) + '\n')) // for debugging
176
223
  // pipe
177
224
  .by({
178
225
  writable: new WritableStream<string>({
179
226
  write: async (data) => {
180
- await shellReady.wait();
227
+ await stdinReady.wait();
228
+ // await idleWaiter.wait(20); // wait for idle for 200ms to avoid messing up claude's input
181
229
  shell.write(data);
182
230
  },
183
231
  }),
184
232
  readable: shellOutputStream.readable,
185
233
  })
186
- // handle terminal render
187
- .forEach((text) => render.write(text))
234
+ .forEach(() => idleWaiter.ping())
235
+ .forEach((text) => {
236
+ terminalRender.write(text);
237
+ // todo: .onStatus((msg)=> shell.write(msg))
238
+ if (process.stdin.isTTY) return; // only handle it when stdin is not tty
239
+ if (text.includes('\u001b[6n')) return; // only asked
240
+
241
+ // todo: use terminalRender API to get cursor position when new version is available
242
+ // xterm replies CSI row; column R if asked cursor position
243
+ // https://en.wikipedia.org/wiki/ANSI_escape_code#:~:text=citation%20needed%5D-,xterm%20replies,-CSI%20row%C2%A0%3B
244
+ // when agent asking position, respond with row; col
245
+ const rendered = terminalRender.render();
246
+ const row = rendered.split('\n').length + 1;
247
+ const col = (rendered.split('\n').slice(-1)[0]?.length || 0) + 1;
248
+ shell.write(`\u001b[${row};${col}R`);
249
+ })
188
250
 
189
- // handle idle
190
- .forEach(() => idleWatcher?.ping()) // ping the idle watcher on output for last active time to keep track of claude status
191
251
  // auto-response
192
252
  .forkTo((e) =>
193
253
  e
194
- .map((e) => removeControlCharacters(e as string))
254
+ .map((e) => removeControlCharacters(e))
195
255
  .map((e) => e.replaceAll('\r', '')) // remove carriage return
196
- .forEach(async (e) => {
197
- if (e.match(/❯ 1. Yes/)) return await confirm();
198
- if (e.match(/❯ 1. Dark mode✔|Press Enter to continue…/))
199
- return await confirm();
200
- if (e.match(/No conversation found to continue/)) {
201
- errorNoConversation = true; // set flag to true if error message is found
256
+ .lines({ EOL: 'NONE' })
257
+ // Generic auto-response handler driven by CLI_CONFIGURES
258
+ .forEach(async (e, i) => {
259
+ const conf =
260
+ CLI_CONFIGURES[cli as keyof typeof CLI_CONFIGURES] || null;
261
+ if (!conf) return;
262
+
263
+ try {
264
+ // ready matcher: if matched, mark stdin ready
265
+ if (conf.ready) {
266
+ // special-case gemini to avoid initial prompt noise: only after many lines
267
+ if (cli === 'gemini' && conf.ready instanceof RegExp) {
268
+ if (e.match(conf.ready) && i > 80) return stdinReady.ready();
269
+ } else if (e.match(conf.ready)) {
270
+ return stdinReady.ready();
271
+ }
272
+ }
273
+
274
+ // enter matchers: send Enter when any enter regex matches
275
+ if (conf.enter && Array.isArray(conf.enter)) {
276
+ for (const rx of conf.enter) {
277
+ if (e.match(rx)) return await sendEnter();
278
+ }
279
+ }
280
+
281
+ // fatal matchers: set isFatal flag when matched
282
+ if (conf.fatal && Array.isArray(conf.fatal)) {
283
+ for (const rx of conf.fatal) {
284
+ if (e.match(rx)) return (isFatal = true);
285
+ }
286
+ }
287
+ } catch (err) {
288
+ // defensive: if e.match throws (e.g., not a string), ignore
202
289
  return;
203
290
  }
204
291
  })
205
292
  // .forEach(e => appendFile('.cache/io.log', "output|" + JSON.stringify(e) + '\n')) // for debugging
206
- .run()
293
+ .run(),
207
294
  )
208
- .replaceAll(/.*(?:\r\n?|\r?\n)/g, (line) => prefix + line) // add prefix
209
295
  .map((e) =>
210
- removeControlCharactersFromStdout ? removeControlCharacters(e) : e
296
+ removeControlCharactersFromStdout ? removeControlCharacters(e) : e,
211
297
  )
212
- .to(fromWritable(process.stdout));
298
+ .to(fromWritable(process.stdout))
299
+ .then(() => null); // run it immediately without await
300
+
301
+ // wait for cli ready and send prompt if provided
302
+ if (prompt)
303
+ (async () => {
304
+ // console.log(`[${cli}-yes] Ready to send prompt to ${cli}: ${prompt}`);
305
+ // idleWaiter.ping();
306
+ // console.log(
307
+ // 'await idleWaiter.wait(1000); // wait a bit for claude to start'
308
+ // );
309
+ // await idleWaiter.wait(1000); // wait a bit for claude to start
310
+ // console.log('await stdinReady.wait();');
311
+ // await stdinReady.wait();
312
+ // console.log(`[${cli}-yes] Waiting for ${cli} to be ready...`);
313
+ // console.log('await idleWaiter.wait(200);');
314
+ // await idleWaiter.wait(200);
315
+ // console.log(`[${cli}-yes] Sending prompt to ${cli}: ${prompt}`);
316
+ await sendMessage(prompt);
317
+ })();
213
318
 
214
319
  const exitCode = await pendingExitCode.promise; // wait for the shell to exit
215
- console.log(`[claude-yes] claude exited with code ${exitCode}`);
320
+ console.log(`[${cli}-yes] ${cli} exited with code ${exitCode}`);
216
321
 
217
322
  if (logFile) {
218
- verbose && console.log(`[claude-yes] Writing rendered logs to ${logFile}`);
323
+ verbose && console.log(`[${cli}-yes] Writing rendered logs to ${logFile}`);
219
324
  const logFilePath = path.resolve(logFile);
220
325
  await mkdir(path.dirname(logFilePath), { recursive: true }).catch(
221
- () => null
326
+ () => null,
222
327
  );
223
- await writeFile(logFilePath, render.render());
328
+ await writeFile(logFilePath, terminalRender.render());
329
+ }
330
+
331
+ return { exitCode, logs: terminalRender.render() };
332
+
333
+ async function sendEnter(waitms = 1000) {
334
+ // wait for idle for a bit to let agent cli finish rendering
335
+ const st = Date.now();
336
+
337
+ await idleWaiter.wait(waitms);
338
+ const et = Date.now();
339
+ process.stdout.write(`\ridleWaiter.wait(${waitms}) took ${et - st}ms\r`);
340
+
341
+ shell.write('\r');
342
+ }
343
+
344
+ async function sendMessage(message: string) {
345
+ await stdinReady.wait();
346
+ // show in-place message: write msg and move cursor back start
347
+ shell.write(message);
348
+ idleWaiter.ping(); // just sent a message, wait for echo
349
+ await sendEnter();
350
+ }
351
+
352
+ async function exitAgent() {
353
+ continueOnCrash = false;
354
+ // send exit command to the shell, must sleep a bit to avoid claude treat it as pasted input
355
+ await sendMessage('/exit');
356
+
357
+ // wait for shell to exit or kill it with a timeout
358
+ let exited = false;
359
+ await Promise.race([
360
+ pendingExitCode.promise.then(() => (exited = true)), // resolve when shell exits
361
+
362
+ // if shell doesn't exit in 5 seconds, kill it
363
+ new Promise<void>((resolve) =>
364
+ setTimeout(() => {
365
+ if (exited) return; // if shell already exited, do nothing
366
+ shell.kill(); // kill the shell process if it doesn't exit in time
367
+ resolve();
368
+ }, 5000),
369
+ ), // 5 seconds timeout
370
+ ]);
224
371
  }
225
372
 
226
- return { exitCode, logs: render.render() };
373
+ function getTerminalDimensions() {
374
+ return {
375
+ cols: Math.max(process.stdout.columns, 80),
376
+ rows: process.stdout.rows,
377
+ };
378
+ }
227
379
  }
228
380
 
229
381
  export { removeControlCharacters };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-yes",
3
- "version": "1.17.1",
3
+ "version": "1.19.0",
4
4
  "description": "A wrapper tool that automates interactions with the Claude CLI by automatically handling common prompts and responses.",
5
5
  "keywords": [
6
6
  "claude",
@@ -31,7 +31,11 @@
31
31
  "module": "index.ts",
32
32
  "types": "./index.ts",
33
33
  "bin": {
34
- "claude-yes": "dist/cli.js"
34
+ "claude-yes": "dist/claude-yes.js",
35
+ "codex-yes": "dist/codex-yes.js",
36
+ "gemini-yes": "dist/gemini-yes.js",
37
+ "cursor-yes": "dist/cursor-yes.js",
38
+ "copilot-yes": "dist/copilot-yes.js"
35
39
  },
36
40
  "directories": {
37
41
  "doc": "docs"
@@ -41,18 +45,21 @@
41
45
  "dist"
42
46
  ],
43
47
  "scripts": {
44
- "build": "bun build index.ts cli.ts --outdir=dist --external=node-pty --external=bun-pty --target=node --sourcemap",
48
+ "build": "bun build index.ts cli.ts --packages=external --outdir=dist --target=node --sourcemap",
49
+ "postbuild": "bun ./postbuild.ts",
45
50
  "dev": "tsx index.ts",
46
- "fmt": "prettier -w .",
47
- "prepare": "husky",
51
+ "fmt": "bunx @biomejs/biome check --fix",
52
+ "prepack": "bun run build",
53
+ "prepare": "bunx husky",
48
54
  "test": "vitest"
49
55
  },
50
56
  "lint-staged": {
51
57
  "*.{ts,js,json,md}": [
52
- "prettier --write"
58
+ "bunx @biomejs/biome check --fix"
53
59
  ]
54
60
  },
55
61
  "devDependencies": {
62
+ "@biomejs/biome": "^2.2.5",
56
63
  "@types/bun": "^1.2.18",
57
64
  "@types/jest": "^30.0.0",
58
65
  "@types/node": "^24.0.10",
@@ -62,7 +69,6 @@
62
69
  "from-node-stream": "^0.0.11",
63
70
  "husky": "^9.1.7",
64
71
  "lint-staged": "^16.1.4",
65
- "prettier": "^3.6.2",
66
72
  "rambda": "^10.3.2",
67
73
  "semantic-release": "^24.2.6",
68
74
  "sflow": "^1.20.2",
@@ -77,6 +83,8 @@
77
83
  "node-pty": "^1.0.0"
78
84
  },
79
85
  "dependencies": {
80
- "bun-pty": "^0.3.2"
86
+ "bun-pty": "^0.3.2",
87
+ "p-map": "^7.0.3",
88
+ "phpdie": "^1.7.0"
81
89
  }
82
90
  }
package/postbuild.ts ADDED
@@ -0,0 +1,6 @@
1
+ #! /usr/bin/env bun
2
+ import { copyFile } from 'fs/promises';
3
+ import * as pkg from './package.json';
4
+
5
+ const src = 'dist/cli.js';
6
+ await Promise.all(Object.values(pkg.bin).map((dst) => copyFile(src, dst)));
@@ -1,4 +1,7 @@
1
1
  export function removeControlCharacters(str: string): string {
2
- // Matches control characters in the C0 and C1 ranges, including Delete (U+007F)
3
- return str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
2
+ // Matches control characters in the C0 and C1 ranges, including Delete (U+007F)
3
+ return str.replace(
4
+ /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
5
+ '',
6
+ );
4
7
  }
package/sleep.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export function sleep(ms: number) {
2
- return new Promise(resolve => setTimeout(resolve, ms));
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
3
  }
package/utils.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export function sleepms(ms: number) {
2
- return new Promise(resolve => setTimeout(resolve, ms));
2
+ return new Promise((resolve) => setTimeout(resolve, ms));
3
3
  }
@@ -1,41 +0,0 @@
1
- import { expect, it } from 'vitest';
2
- import { createIdleWatcher } from "./createIdleWatcher";
3
- import { sleepms } from "./utils";
4
-
5
- it('createIdleWatcher should trigger onIdle after timeout', async () => {
6
- let idleTriggered = false;
7
- const watcher = createIdleWatcher(() => {
8
- idleTriggered = true;
9
- }, 100);
10
-
11
- watcher.ping();
12
- await sleepms(150);
13
- expect(idleTriggered).toBe(true);
14
- }, 1000);
15
-
16
- it.concurrent('createIdleWatcher should reset timeout on ping', async () => {
17
- let idleTriggered = false;
18
- const watcher = createIdleWatcher(() => {
19
- idleTriggered = true;
20
- }, 100);
21
-
22
- watcher.ping();
23
- await sleepms(50);
24
- watcher.ping();
25
- await sleepms(50);
26
- expect(idleTriggered).toBe(false);
27
- await sleepms(100);
28
- expect(idleTriggered).toBe(true);
29
- }, 1000);
30
-
31
- it.concurrent('createIdleWatcher should update lastActiveTime on ping', async () => {
32
- const watcher = createIdleWatcher(() => { }, 1000);
33
-
34
- const initialTime = watcher.getLastActiveTime();
35
- await sleepms(50);
36
- watcher.ping();
37
- const updatedTime = watcher.getLastActiveTime();
38
-
39
- expect(updatedTime.getTime()).toBeGreaterThan(initialTime.getTime());
40
- }, 1000);
41
-