claude-yes 1.24.1 → 1.25.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/README.md +136 -38
- package/dist/claude-yes.js +11286 -10933
- package/dist/cli.js +11286 -10933
- package/dist/cli.js.map +130 -5
- package/dist/codex-yes.js +11286 -10933
- package/dist/copilot-yes.js +11286 -10933
- package/dist/cursor-yes.js +11286 -10933
- package/dist/gemini-yes.js +11286 -10933
- package/dist/grok-yes.js +11286 -10933
- package/dist/index.js +263 -116
- package/dist/index.js.map +10 -5
- package/dist/qwen-yes.js +12093 -0
- package/package.json +45 -11
- package/ts/cli-idle.spec.ts +20 -0
- package/ts/cli.spec.ts +18 -0
- package/ts/cli.ts +124 -0
- package/ts/defineConfig.ts +12 -0
- package/{index.ts → ts/index.ts} +152 -146
- package/ts/postbuild.ts +18 -0
- package/{runningLock.spec.ts → ts/runningLock.spec.ts} +45 -31
- package/{runningLock.ts → ts/runningLock.ts} +49 -0
- package/ts/tryCatch.ts +25 -0
- package/ts/utils.ts +23 -0
- package/ts/yesLog.ts +27 -0
- package/cli-idle.spec.ts +0 -17
- package/cli.test.ts +0 -63
- package/cli.ts +0 -97
- package/postbuild.ts +0 -15
- package/utils.ts +0 -3
- /package/{ReadyManager.ts → ts/ReadyManager.ts} +0 -0
- /package/{idleWaiter.ts → ts/idleWaiter.ts} +0 -0
- /package/{removeControlCharacters.ts → ts/removeControlCharacters.ts} +0 -0
- /package/{sleep.ts → ts/sleep.ts} +0 -0
package/{index.ts → ts/index.ts}
RENAMED
|
@@ -5,6 +5,8 @@ import DIE from 'phpdie';
|
|
|
5
5
|
import sflow from 'sflow';
|
|
6
6
|
import { TerminalTextRender } from 'terminal-render';
|
|
7
7
|
import tsaComposer from 'tsa-composer';
|
|
8
|
+
import rawConfig from '../cli-yes.config.js';
|
|
9
|
+
import { defineCliYesConfig } from './defineConfig.js';
|
|
8
10
|
import { IdleWaiter } from './idleWaiter';
|
|
9
11
|
import { ReadyManager } from './ReadyManager';
|
|
10
12
|
import { removeControlCharacters } from './removeControlCharacters';
|
|
@@ -14,74 +16,36 @@ import {
|
|
|
14
16
|
shouldUseLock,
|
|
15
17
|
updateCurrentTaskStatus,
|
|
16
18
|
} from './runningLock';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
string,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
> = {
|
|
34
|
-
grok: {
|
|
35
|
-
install: 'npm install -g @vibe-kit/grok-cli',
|
|
36
|
-
ready: [/^ │ ❯ /],
|
|
37
|
-
enter: [/^ 1. Yes/],
|
|
38
|
-
},
|
|
39
|
-
claude: {
|
|
40
|
-
install: 'npm install -g @anthropic-ai/claude-code',
|
|
41
|
-
// ready: [/^> /], // regex matcher for stdin ready
|
|
42
|
-
ready: [/\? for shortcuts/], // regex matcher for stdin ready
|
|
43
|
-
enter: [/❯ 1. Yes/, /❯ 1. Dark mode✔/, /Press Enter to continue…/],
|
|
44
|
-
fatal: [
|
|
45
|
-
/No conversation found to continue/,
|
|
46
|
-
/⎿ Claude usage limit reached\./,
|
|
47
|
-
],
|
|
48
|
-
},
|
|
49
|
-
gemini: {
|
|
50
|
-
install: 'npm install -g @google/gemini-cli',
|
|
51
|
-
// match the agent prompt after initial lines; handled by index logic using line index
|
|
52
|
-
ready: [/Type your message/], // used with line index check
|
|
53
|
-
enter: [/│ ● 1. Yes, allow once/],
|
|
54
|
-
fatal: [],
|
|
55
|
-
},
|
|
56
|
-
codex: {
|
|
57
|
-
install: 'npm install -g @openai/codex-cli',
|
|
58
|
-
ready: [/⏎ send/],
|
|
59
|
-
enter: [
|
|
60
|
-
/> 1. Yes, allow Codex to work in this folder/,
|
|
61
|
-
/> 1. Approve and run now/,
|
|
62
|
-
],
|
|
63
|
-
fatal: [/Error: The cursor position could not be read within/],
|
|
64
|
-
// add to codex --search by default when not provided by the user
|
|
65
|
-
ensureArgs: (args: string[]) => {
|
|
66
|
-
if (!args.includes('--search')) return ['--search', ...args];
|
|
67
|
-
return args;
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
copilot: {
|
|
71
|
-
install: 'npm install -g @github/copilot',
|
|
72
|
-
ready: [/^ +> /, /Ctrl\+c Exit/],
|
|
73
|
-
enter: [/ │ ❯ 1. Yes, proceed/, /❯ 1. Yes/],
|
|
74
|
-
fatal: [],
|
|
75
|
-
},
|
|
76
|
-
cursor: {
|
|
77
|
-
install: 'open https://cursor.com/ja/docs/cli/installation',
|
|
78
|
-
// map logical "cursor" cli name to actual binary name
|
|
79
|
-
binary: 'cursor-agent',
|
|
80
|
-
ready: [/\/ commands/],
|
|
81
|
-
enter: [/→ Run \(once\) \(y\) \(enter\)/, /▶ \[a\] Trust this workspace/],
|
|
82
|
-
fatal: [/^ Error: You've hit your usage limit/],
|
|
83
|
-
},
|
|
19
|
+
import { catcher } from './tryCatch';
|
|
20
|
+
import { deepMixin } from './utils';
|
|
21
|
+
import { yesLog } from './yesLog';
|
|
22
|
+
|
|
23
|
+
export type AgentCliConfig = {
|
|
24
|
+
install?: string; // hint user for install command if not installed
|
|
25
|
+
version?: string; // hint user for version command to check if installed
|
|
26
|
+
binary?: string; // actual binary name if different from cli, e.g. cursor -> cursor-agent
|
|
27
|
+
ready?: RegExp[]; // regex matcher for stdin ready, or line index for gemini
|
|
28
|
+
enter?: RegExp[]; // array of regex to match for sending Enter
|
|
29
|
+
fatal?: RegExp[]; // array of regex to match for fatal errors
|
|
30
|
+
restoreArgs?: string[]; // arguments to continue the session when crashed
|
|
31
|
+
defaultArgs?: string[]; // function to ensure certain args are present
|
|
32
|
+
noEOL?: boolean; // if true, do not split lines by \n, used for codex, which uses cursor-move csi code instead of \n to move lines
|
|
33
|
+
promptArg?: (string & {}) | 'first-arg' | 'last-arg'; // argument name to pass the prompt, e.g. --prompt, or first-arg for positional arg
|
|
84
34
|
};
|
|
35
|
+
export type CliYesConfig = {
|
|
36
|
+
clis: { [key: string]: AgentCliConfig };
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// load user config from cli-yes.config.ts if exists
|
|
40
|
+
export const config = await rawConfig;
|
|
41
|
+
|
|
42
|
+
export const CLIS_CONFIG = config.clis as Record<
|
|
43
|
+
keyof Awaited<typeof config>['clis'],
|
|
44
|
+
AgentCliConfig
|
|
45
|
+
>;
|
|
46
|
+
export type SUPPORTED_CLIS = keyof typeof CLIS_CONFIG;
|
|
47
|
+
export const SUPPORTED_CLIS = Object.keys(CLIS_CONFIG) as SUPPORTED_CLIS[];
|
|
48
|
+
|
|
85
49
|
/**
|
|
86
50
|
* Main function to run agent-cli with automatic yes/no responses
|
|
87
51
|
* @param options Configuration options
|
|
@@ -97,77 +61,86 @@ export const CLI_CONFIGURES: Record<
|
|
|
97
61
|
*
|
|
98
62
|
* @example
|
|
99
63
|
* ```typescript
|
|
100
|
-
* import
|
|
101
|
-
* await
|
|
64
|
+
* import cliYes from 'cli-yes';
|
|
65
|
+
* await cliYes({
|
|
102
66
|
* prompt: 'help me solve all todos in my codebase',
|
|
103
67
|
*
|
|
104
68
|
* // optional
|
|
105
|
-
*
|
|
106
|
-
* cliArgs: ['--verbose'], // additional args to pass to claude
|
|
69
|
+
* cliArgs: ['--verbose'], // additional args to pass to agent-cli
|
|
107
70
|
* exitOnIdle: 30000, // exit after 30 seconds of idle
|
|
108
|
-
*
|
|
71
|
+
* robust: true, // auto restart with --continue if claude crashes, default is true
|
|
109
72
|
* logFile: 'claude.log', // save logs to file
|
|
110
73
|
* disableLock: false, // disable running lock (default is false)
|
|
111
74
|
* });
|
|
112
75
|
* ```
|
|
113
76
|
*/
|
|
114
|
-
export default async function
|
|
115
|
-
cli
|
|
77
|
+
export default async function cliYes({
|
|
78
|
+
cli,
|
|
116
79
|
cliArgs = [],
|
|
117
80
|
prompt,
|
|
118
|
-
|
|
81
|
+
robust = true,
|
|
119
82
|
cwd,
|
|
120
83
|
env,
|
|
121
84
|
exitOnIdle,
|
|
122
85
|
logFile,
|
|
123
86
|
removeControlCharactersFromStdout = false, // = !process.stdout.isTTY,
|
|
124
87
|
verbose = false,
|
|
125
|
-
|
|
88
|
+
queue = true,
|
|
126
89
|
}: {
|
|
127
|
-
cli
|
|
90
|
+
cli: SUPPORTED_CLIS;
|
|
128
91
|
cliArgs?: string[];
|
|
129
92
|
prompt?: string;
|
|
130
|
-
|
|
93
|
+
robust?: boolean;
|
|
131
94
|
cwd?: string;
|
|
132
95
|
env?: Record<string, string>;
|
|
133
96
|
exitOnIdle?: number;
|
|
134
97
|
logFile?: string;
|
|
135
98
|
removeControlCharactersFromStdout?: boolean;
|
|
136
99
|
verbose?: boolean;
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
100
|
+
queue?: boolean;
|
|
101
|
+
}) {
|
|
102
|
+
// those overrides seems only works in bun
|
|
103
|
+
// await Promise.allSettled([
|
|
104
|
+
// import(path.join(process.cwd(), "cli-yes.config")),
|
|
105
|
+
// ])
|
|
106
|
+
// .then((e) => e.flatMap((e) => (e.status === "fulfilled" ? [e.value] : [])))
|
|
107
|
+
// .then(e=>e.at(0))
|
|
108
|
+
// .then((e) => e.default as ReturnType<typeof defineCliYesConfig>)
|
|
109
|
+
// .then(async (override) => deepMixin(config, override || {}))
|
|
110
|
+
// .catch((error) => {
|
|
111
|
+
// if (process.env.VERBOSE)
|
|
112
|
+
// console.warn("Fail to load cli-yes.config.ts", error);
|
|
113
|
+
// });
|
|
114
|
+
|
|
115
|
+
if (!cli) throw new Error(`cli is required`);
|
|
116
|
+
const conf = CLIS_CONFIG[cli] || DIE(`Unsupported cli tool: ${cli}`);
|
|
144
117
|
|
|
145
118
|
// Acquire lock before starting agent (if in git repo or same cwd and lock is not disabled)
|
|
146
119
|
const workingDir = cwd ?? process.cwd();
|
|
147
|
-
if (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// Register cleanup handlers for lock release
|
|
152
|
-
const cleanupLock = async () => {
|
|
153
|
-
if (!disableLock && shouldUseLock(workingDir)) {
|
|
154
|
-
await releaseLock().catch(() => null); // Ignore errors during cleanup
|
|
120
|
+
if (queue) {
|
|
121
|
+
if (queue && shouldUseLock(workingDir)) {
|
|
122
|
+
await acquireLock(workingDir, prompt ?? 'Interactive session');
|
|
155
123
|
}
|
|
156
|
-
};
|
|
157
124
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
process.exit(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
125
|
+
// Register cleanup handlers for lock release
|
|
126
|
+
const cleanupLock = async () => {
|
|
127
|
+
if (queue && shouldUseLock(workingDir)) {
|
|
128
|
+
await releaseLock().catch(() => null); // Ignore errors during cleanup
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
process.on('exit', () => {
|
|
133
|
+
if (queue) releaseLock().catch(() => null);
|
|
134
|
+
});
|
|
135
|
+
process.on('SIGINT', async (code) => {
|
|
136
|
+
await cleanupLock();
|
|
137
|
+
process.exit(code);
|
|
138
|
+
});
|
|
139
|
+
process.on('SIGTERM', async (code) => {
|
|
140
|
+
await cleanupLock();
|
|
141
|
+
process.exit(code);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
171
144
|
|
|
172
145
|
process.stdin.setRawMode?.(true); // must be called any stdout/stdin usage
|
|
173
146
|
let isFatal = false; // when true, do not restart on crash, and exit agent
|
|
@@ -193,21 +166,49 @@ export default async function claudeYes({
|
|
|
193
166
|
});
|
|
194
167
|
|
|
195
168
|
// Apply CLI specific configurations (moved to CLI_CONFIGURES)
|
|
196
|
-
const cliConf = (
|
|
197
|
-
cliArgs = cliConf.
|
|
169
|
+
const cliConf = (CLIS_CONFIG as Record<string, AgentCliConfig>)[cli] || {};
|
|
170
|
+
cliArgs = cliConf.defaultArgs
|
|
171
|
+
? [...cliConf.defaultArgs, ...cliArgs]
|
|
172
|
+
: cliArgs;
|
|
173
|
+
|
|
174
|
+
if (prompt && cliConf.promptArg) {
|
|
175
|
+
if (cliConf.promptArg === 'first-arg') {
|
|
176
|
+
cliArgs = [prompt, ...cliArgs];
|
|
177
|
+
prompt = undefined; // clear prompt to avoid sending later
|
|
178
|
+
} else if (cliConf.promptArg === 'last-arg') {
|
|
179
|
+
cliArgs = [...cliArgs, prompt];
|
|
180
|
+
prompt = undefined; // clear prompt to avoid sending later
|
|
181
|
+
} else if (cliConf.promptArg.startsWith('--')) {
|
|
182
|
+
cliArgs = [cliConf.promptArg, prompt, ...cliArgs];
|
|
183
|
+
prompt = undefined; // clear prompt to avoid sending later
|
|
184
|
+
} else {
|
|
185
|
+
console.warn(`Unknown promptArg format: ${cliConf.promptArg}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
198
188
|
const cliCommand = cliConf?.binary || cli;
|
|
199
189
|
|
|
200
|
-
let shell =
|
|
201
|
-
() => pty.spawn(cliCommand, cliArgs, getPtyOptions()),
|
|
190
|
+
let shell = catcher(
|
|
202
191
|
(error: unknown) => {
|
|
203
192
|
console.error(`Fatal: Failed to start ${cliCommand}.`);
|
|
204
|
-
if (cliConf?.install)
|
|
193
|
+
if (cliConf?.install && isCommandNotFoundError(error))
|
|
205
194
|
console.error(
|
|
206
195
|
`If you did not installed it yet, Please install it first: ${cliConf.install}`,
|
|
207
196
|
);
|
|
208
197
|
throw error;
|
|
198
|
+
|
|
199
|
+
function isCommandNotFoundError(e: unknown) {
|
|
200
|
+
if (e instanceof Error) {
|
|
201
|
+
return (
|
|
202
|
+
e.message.includes('command not found') ||
|
|
203
|
+
e.message.includes('ENOENT') ||
|
|
204
|
+
e.message.includes('spawn') // windows
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
209
|
},
|
|
210
|
-
|
|
210
|
+
() => pty.spawn(cliCommand, cliArgs, getPtyOptions()),
|
|
211
|
+
)();
|
|
211
212
|
const pendingExitCode = Promise.withResolvers<number | null>();
|
|
212
213
|
let pendingExitCodeValue = null;
|
|
213
214
|
|
|
@@ -215,22 +216,22 @@ export default async function claudeYes({
|
|
|
215
216
|
// npm install -g @anthropic-ai/claude-code
|
|
216
217
|
|
|
217
218
|
async function onData(data: string) {
|
|
218
|
-
nextStdout.ready();
|
|
219
219
|
// append data to the buffer, so we can process it later
|
|
220
220
|
await outputWriter.write(data);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
shell.onData(onData);
|
|
224
224
|
shell.onExit(function onExit({ exitCode }) {
|
|
225
|
-
nextStdout.ready();
|
|
226
225
|
stdinReady.unready(); // start buffer stdin
|
|
227
226
|
const agentCrashed = exitCode !== 0;
|
|
228
|
-
const continueArg = (continueArgs as Record<string, string[]>)[cli];
|
|
229
227
|
|
|
230
|
-
if (agentCrashed &&
|
|
231
|
-
if (!
|
|
228
|
+
if (agentCrashed && robust && conf?.restoreArgs) {
|
|
229
|
+
if (!conf.restoreArgs) {
|
|
232
230
|
return console.warn(
|
|
233
|
-
`
|
|
231
|
+
`robust is only supported for ${Object.entries(CLIS_CONFIG)
|
|
232
|
+
.filter(([_, v]) => v.restoreArgs)
|
|
233
|
+
.map(([k]) => k)
|
|
234
|
+
.join(', ')} currently, not ${cli}`,
|
|
234
235
|
);
|
|
235
236
|
}
|
|
236
237
|
if (isFatal) {
|
|
@@ -239,7 +240,7 @@ export default async function claudeYes({
|
|
|
239
240
|
|
|
240
241
|
console.log(`${cli} crashed, restarting...`);
|
|
241
242
|
|
|
242
|
-
shell = pty.spawn(cli,
|
|
243
|
+
shell = pty.spawn(cli, conf.restoreArgs, getPtyOptions());
|
|
243
244
|
shell.onData(onData);
|
|
244
245
|
shell.onExit(onExit);
|
|
245
246
|
return;
|
|
@@ -295,7 +296,8 @@ export default async function claudeYes({
|
|
|
295
296
|
readable: shellOutputStream.readable,
|
|
296
297
|
})
|
|
297
298
|
.forEach(() => idleWaiter.ping())
|
|
298
|
-
.forEach((
|
|
299
|
+
.forEach(() => nextStdout.ready())
|
|
300
|
+
.forEach(async (text) => {
|
|
299
301
|
terminalRender.write(text);
|
|
300
302
|
// todo: .onStatus((msg)=> shell.write(msg))
|
|
301
303
|
if (process.stdin.isTTY) return; // only handle it when stdin is not tty
|
|
@@ -307,7 +309,7 @@ export default async function claudeYes({
|
|
|
307
309
|
// const rendered = terminalRender.render();
|
|
308
310
|
const { col, row } = terminalRender.getCursorPosition();
|
|
309
311
|
shell.write(`\u001b[${row};${col}R`); // reply cli when getting cursor position
|
|
310
|
-
|
|
312
|
+
await yesLog`cursor|respond position: row=${String(row)}, col=${String(col)}`;
|
|
311
313
|
// const row = rendered.split('\n').length + 1;
|
|
312
314
|
// const col = (rendered.split('\n').slice(-1)[0]?.length || 0) + 1;
|
|
313
315
|
})
|
|
@@ -318,33 +320,29 @@ export default async function claudeYes({
|
|
|
318
320
|
.map((e) => removeControlCharacters(e))
|
|
319
321
|
.map((e) => e.replaceAll('\r', '')) // remove carriage return
|
|
320
322
|
.by((s) => {
|
|
321
|
-
if (
|
|
323
|
+
if (conf.noEOL) return s; // codex use cursor-move csi code insteadof \n to move lines, so the output have no \n at all, this hack prevents stuck on unended line
|
|
322
324
|
return s.lines({ EOL: 'NONE' }); // other clis use ink, which is rerendering the block based on \n lines
|
|
323
325
|
})
|
|
324
|
-
|
|
326
|
+
.forEach((e) => yesLog`output|${e}`) // for debugging
|
|
325
327
|
// Generic auto-response handler driven by CLI_CONFIGURES
|
|
326
328
|
.forEach(async (e, i) => {
|
|
327
|
-
const conf =
|
|
328
|
-
CLI_CONFIGURES[cli as keyof typeof CLI_CONFIGURES] || null;
|
|
329
|
-
if (!conf) return;
|
|
330
|
-
|
|
331
329
|
// ready matcher: if matched, mark stdin ready
|
|
332
330
|
if (conf.ready?.some((rx: RegExp) => e.match(rx))) {
|
|
333
|
-
|
|
331
|
+
await yesLog`ready |${e}`;
|
|
334
332
|
if (cli === 'gemini' && i <= 80) return; // gemini initial noise, only after many lines
|
|
335
333
|
stdinReady.ready();
|
|
336
334
|
}
|
|
337
335
|
|
|
338
336
|
// enter matchers: send Enter when any enter regex matches
|
|
339
337
|
if (conf.enter?.some((rx: RegExp) => e.match(rx))) {
|
|
340
|
-
|
|
338
|
+
await yesLog`enter |${e}`;
|
|
341
339
|
await sendEnter(300); // send Enter after 300ms idle wait
|
|
342
340
|
return;
|
|
343
341
|
}
|
|
344
342
|
|
|
345
343
|
// fatal matchers: set isFatal flag when matched
|
|
346
344
|
if (conf.fatal?.some((rx: RegExp) => e.match(rx))) {
|
|
347
|
-
|
|
345
|
+
await yesLog`fatal |${e}`;
|
|
348
346
|
isFatal = true;
|
|
349
347
|
await exitAgent();
|
|
350
348
|
}
|
|
@@ -365,7 +363,7 @@ export default async function claudeYes({
|
|
|
365
363
|
console.log(`[${cli}-yes] ${cli} exited with code ${exitCode}`);
|
|
366
364
|
|
|
367
365
|
// Update task status and release lock
|
|
368
|
-
if (
|
|
366
|
+
if (queue && shouldUseLock(workingDir)) {
|
|
369
367
|
await updateCurrentTaskStatus(
|
|
370
368
|
exitCode === 0 ? 'completed' : 'failed',
|
|
371
369
|
).catch(() => null);
|
|
@@ -389,24 +387,40 @@ export default async function claudeYes({
|
|
|
389
387
|
await idleWaiter.wait(waitms);
|
|
390
388
|
const et = Date.now();
|
|
391
389
|
// process.stdout.write(`\ridleWaiter.wait(${waitms}) took ${et - st}ms\r`);
|
|
392
|
-
|
|
393
|
-
|
|
390
|
+
await yesLog`sendEn| idleWaiter.wait(${String(waitms)}) took ${String(et - st)}ms`;
|
|
391
|
+
nextStdout.unready();
|
|
394
392
|
shell.write('\r');
|
|
393
|
+
// retry once if not received any output in 1 second after sending Enter
|
|
394
|
+
await Promise.race([
|
|
395
|
+
nextStdout.wait(),
|
|
396
|
+
new Promise<void>((resolve) =>
|
|
397
|
+
setTimeout(() => {
|
|
398
|
+
if (!nextStdout.ready) {
|
|
399
|
+
shell.write('\r');
|
|
400
|
+
}
|
|
401
|
+
resolve();
|
|
402
|
+
}, 1000),
|
|
403
|
+
),
|
|
404
|
+
]);
|
|
395
405
|
}
|
|
396
406
|
|
|
397
407
|
async function sendMessage(message: string) {
|
|
398
408
|
await stdinReady.wait();
|
|
399
409
|
// show in-place message: write msg and move cursor back start
|
|
400
|
-
|
|
401
|
-
shell.write(message);
|
|
410
|
+
yesLog`send |${message}`;
|
|
402
411
|
nextStdout.unready();
|
|
412
|
+
shell.write(message);
|
|
403
413
|
idleWaiter.ping(); // just sent a message, wait for echo
|
|
414
|
+
yesLog`waiting next stdout|${message}`;
|
|
404
415
|
await nextStdout.wait();
|
|
405
|
-
|
|
416
|
+
yesLog`sending enter`;
|
|
417
|
+
await sendEnter(1000);
|
|
418
|
+
yesLog`sent enter`;
|
|
406
419
|
}
|
|
407
420
|
|
|
408
421
|
async function exitAgent() {
|
|
409
|
-
|
|
422
|
+
robust = false; // disable robust to avoid auto restart
|
|
423
|
+
|
|
410
424
|
// send exit command to the shell, must sleep a bit to avoid claude treat it as pasted input
|
|
411
425
|
await sendMessage('/exit');
|
|
412
426
|
|
|
@@ -438,11 +452,3 @@ export default async function claudeYes({
|
|
|
438
452
|
}
|
|
439
453
|
|
|
440
454
|
export { removeControlCharacters };
|
|
441
|
-
|
|
442
|
-
function tryCatch<T, R>(fn: () => T, catchFn: (error: unknown) => R): T | R {
|
|
443
|
-
try {
|
|
444
|
-
return fn();
|
|
445
|
-
} catch (error) {
|
|
446
|
-
return catchFn(error);
|
|
447
|
-
}
|
|
448
|
-
}
|
package/ts/postbuild.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#! /usr/bin/env bun
|
|
2
|
+
import { execaCommand } from 'execa';
|
|
3
|
+
import { copyFile } from 'fs/promises';
|
|
4
|
+
import * as pkg from '../package.json';
|
|
5
|
+
import { CLIS_CONFIG } from '.';
|
|
6
|
+
|
|
7
|
+
const src = 'dist/cli.js';
|
|
8
|
+
await Promise.all(
|
|
9
|
+
Object.keys(CLIS_CONFIG).map(async (cli) => {
|
|
10
|
+
const dst = `dist/${cli}-yes.js`;
|
|
11
|
+
if (!(pkg.bin as Record<string, string>)?.[`${cli}-yes`]) {
|
|
12
|
+
console.log(`package.json Updated bin.${cli}-yes = ${dst}`);
|
|
13
|
+
await execaCommand(`npm pkg set bin.${cli}-yes=${dst}`);
|
|
14
|
+
}
|
|
15
|
+
await copyFile(src, dst);
|
|
16
|
+
console.log(`${dst} Updated`);
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
@@ -133,14 +133,21 @@ describe('runningLock', () => {
|
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
it('should not have gitRoot for non-git directory', async () => {
|
|
136
|
-
//
|
|
137
|
-
|
|
136
|
+
// Create a temporary directory outside of any git repo
|
|
137
|
+
const tempDir = path.join('/tmp', 'test-non-git-' + Date.now());
|
|
138
|
+
await mkdir(tempDir, { recursive: true });
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
expect(lockData.tasks[0].cwd).toBe('/tmp');
|
|
140
|
+
try {
|
|
141
|
+
await acquireLock(tempDir, 'Non-git task');
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
const lockData = await readLockFile();
|
|
144
|
+
expect(lockData.tasks[0].gitRoot).toBeUndefined();
|
|
145
|
+
expect(lockData.tasks[0].cwd).toBe(path.resolve(tempDir));
|
|
146
|
+
|
|
147
|
+
await releaseLock();
|
|
148
|
+
} finally {
|
|
149
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
150
|
+
}
|
|
144
151
|
});
|
|
145
152
|
});
|
|
146
153
|
|
|
@@ -247,31 +254,27 @@ describe('runningLock', () => {
|
|
|
247
254
|
|
|
248
255
|
describe('concurrent access', () => {
|
|
249
256
|
it('should handle multiple tasks from different processes', async () => {
|
|
250
|
-
//
|
|
257
|
+
// Acquire first task
|
|
251
258
|
await acquireLock(TEST_DIR, 'Task 1');
|
|
252
259
|
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
lockData.tasks.
|
|
256
|
-
|
|
257
|
-
task: 'Task 2',
|
|
258
|
-
pid: process.pid + 1,
|
|
259
|
-
status: 'running',
|
|
260
|
-
startedAt: Date.now(),
|
|
261
|
-
lockedAt: Date.now(),
|
|
262
|
-
});
|
|
263
|
-
await writeFile(LOCK_FILE, JSON.stringify(lockData, null, 2));
|
|
260
|
+
// Verify the task exists
|
|
261
|
+
let lockData = await readLockFile();
|
|
262
|
+
expect(lockData.tasks).toHaveLength(1);
|
|
263
|
+
expect(lockData.tasks[0].task).toBe('Task 1');
|
|
264
264
|
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
265
|
+
// Acquire a second task with the same PID (should replace the first)
|
|
266
|
+
await acquireLock('/tmp', 'Task 2');
|
|
267
|
+
|
|
268
|
+
// Should have only one task (the latest one)
|
|
269
|
+
lockData = await readLockFile();
|
|
270
|
+
expect(lockData.tasks).toHaveLength(1);
|
|
271
|
+
expect(lockData.tasks[0].task).toBe('Task 2');
|
|
268
272
|
|
|
269
273
|
await releaseLock();
|
|
270
274
|
|
|
271
|
-
// After release,
|
|
275
|
+
// After release, no tasks should remain
|
|
272
276
|
const finalLockData = await readLockFile();
|
|
273
|
-
expect(finalLockData.tasks).toHaveLength(
|
|
274
|
-
expect(finalLockData.tasks[0].pid).toBe(process.pid + 1);
|
|
277
|
+
expect(finalLockData.tasks).toHaveLength(0);
|
|
275
278
|
});
|
|
276
279
|
|
|
277
280
|
it('should not duplicate tasks with same PID', async () => {
|
|
@@ -411,13 +414,16 @@ describe('runningLock', () => {
|
|
|
411
414
|
});
|
|
412
415
|
|
|
413
416
|
it('should allow different directories without git repos', async () => {
|
|
414
|
-
//
|
|
417
|
+
// Test that when we already have a task, acquiring a new one replaces it
|
|
418
|
+
// (since both use the same PID)
|
|
419
|
+
|
|
420
|
+
// Create lock for /tmp manually
|
|
415
421
|
const lock = {
|
|
416
422
|
tasks: [
|
|
417
423
|
{
|
|
418
424
|
cwd: '/tmp',
|
|
419
425
|
task: 'Tmp task',
|
|
420
|
-
pid: process.pid
|
|
426
|
+
pid: process.pid,
|
|
421
427
|
status: 'running' as const,
|
|
422
428
|
startedAt: Date.now(),
|
|
423
429
|
lockedAt: Date.now(),
|
|
@@ -426,12 +432,18 @@ describe('runningLock', () => {
|
|
|
426
432
|
};
|
|
427
433
|
await writeFile(LOCK_FILE, JSON.stringify(lock, null, 2));
|
|
428
434
|
|
|
429
|
-
//
|
|
435
|
+
// Verify initial state
|
|
436
|
+
let lockData = await readLockFile();
|
|
437
|
+
expect(lockData.tasks).toHaveLength(1);
|
|
438
|
+
expect(lockData.tasks[0].task).toBe('Tmp task');
|
|
439
|
+
|
|
440
|
+
// Acquire lock for different directory (should replace the existing task)
|
|
430
441
|
await acquireLock(TEST_DIR, 'Test task');
|
|
431
442
|
|
|
432
|
-
//
|
|
433
|
-
|
|
434
|
-
expect(lockData.tasks).toHaveLength(
|
|
443
|
+
// Should only have the new task
|
|
444
|
+
lockData = await readLockFile();
|
|
445
|
+
expect(lockData.tasks).toHaveLength(1);
|
|
446
|
+
expect(lockData.tasks[0].task).toBe('Test task');
|
|
435
447
|
|
|
436
448
|
await releaseLock();
|
|
437
449
|
});
|
|
@@ -470,7 +482,9 @@ async function cleanupLockFile() {
|
|
|
470
482
|
async function readLockFile(): Promise<{ tasks: Task[] }> {
|
|
471
483
|
try {
|
|
472
484
|
const content = await readFile(LOCK_FILE, 'utf8');
|
|
473
|
-
|
|
485
|
+
const lockFile = JSON.parse(content);
|
|
486
|
+
// Don't clean stale locks in tests - we want to see the raw data
|
|
487
|
+
return lockFile;
|
|
474
488
|
} catch {
|
|
475
489
|
return { tasks: [] };
|
|
476
490
|
}
|
|
@@ -221,13 +221,57 @@ async function waitForUnlock(
|
|
|
221
221
|
currentTask: Task,
|
|
222
222
|
): Promise<void> {
|
|
223
223
|
const blockingTask = blockingTasks[0];
|
|
224
|
+
if (!blockingTask) return;
|
|
224
225
|
console.log(`⏳ Queueing for unlock of: ${blockingTask.task}`);
|
|
226
|
+
console.log(` Press 'b' to bypass queue, 'k' to kill previous instance`);
|
|
225
227
|
|
|
226
228
|
// Add current task as 'queued'
|
|
227
229
|
await addTask({ ...currentTask, status: 'queued' });
|
|
228
230
|
|
|
231
|
+
// Set up keyboard input handling
|
|
232
|
+
const stdin = process.stdin;
|
|
233
|
+
const wasRaw = stdin.isRaw;
|
|
234
|
+
stdin.setRawMode?.(true);
|
|
235
|
+
stdin.resume();
|
|
236
|
+
|
|
237
|
+
let bypassed = false;
|
|
238
|
+
let killed = false;
|
|
239
|
+
|
|
240
|
+
const keyHandler = (key: Buffer) => {
|
|
241
|
+
const char = key.toString();
|
|
242
|
+
if (char === 'b' || char === 'B') {
|
|
243
|
+
console.log('\n⚡ Bypassing queue...');
|
|
244
|
+
bypassed = true;
|
|
245
|
+
} else if (char === 'k' || char === 'K') {
|
|
246
|
+
console.log('\n🔪 Killing previous instance...');
|
|
247
|
+
killed = true;
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
stdin.on('data', keyHandler);
|
|
252
|
+
|
|
229
253
|
let dots = 0;
|
|
230
254
|
while (true) {
|
|
255
|
+
if (bypassed) {
|
|
256
|
+
// Force bypass - update status to running immediately
|
|
257
|
+
await updateTaskStatus(currentTask.pid, 'running');
|
|
258
|
+
console.log('✓ Queue bypassed, starting task...');
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (killed && blockingTask) {
|
|
263
|
+
// Kill the blocking task's process
|
|
264
|
+
try {
|
|
265
|
+
process.kill(blockingTask.pid, 'SIGTERM');
|
|
266
|
+
console.log(`✓ Killed process ${blockingTask.pid}`);
|
|
267
|
+
// Wait a bit for the process to be killed
|
|
268
|
+
await sleep(1000);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
console.log(`⚠️ Could not kill process ${blockingTask.pid}: ${err}`);
|
|
271
|
+
}
|
|
272
|
+
killed = false; // Reset flag after attempting kill
|
|
273
|
+
}
|
|
274
|
+
|
|
231
275
|
await sleep(POLL_INTERVAL);
|
|
232
276
|
|
|
233
277
|
const lockCheck = await checkLock(currentTask.cwd, currentTask.task);
|
|
@@ -245,6 +289,11 @@ async function waitForUnlock(
|
|
|
245
289
|
`\r⏳ Queueing${'.'.repeat(dots)}${' '.repeat(3 - dots)}`,
|
|
246
290
|
);
|
|
247
291
|
}
|
|
292
|
+
|
|
293
|
+
// Clean up keyboard handler
|
|
294
|
+
stdin.off('data', keyHandler);
|
|
295
|
+
stdin.setRawMode?.(wasRaw);
|
|
296
|
+
if (!wasRaw) stdin.pause();
|
|
248
297
|
}
|
|
249
298
|
|
|
250
299
|
/**
|