@firefunc-agent/runner 0.5.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/LICENSE +65 -0
- package/README.md +88 -0
- package/dist/api.d.ts +64 -0
- package/dist/api.js +76 -0
- package/dist/brief.d.ts +2 -0
- package/dist/brief.js +138 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +230 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +23 -0
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.js +152 -0
- package/dist/engines/claude-budget.d.ts +4 -0
- package/dist/engines/claude-budget.js +122 -0
- package/dist/engines/claude.d.ts +2 -0
- package/dist/engines/claude.js +119 -0
- package/dist/engines/codex.d.ts +2 -0
- package/dist/engines/codex.js +117 -0
- package/dist/engines/cursor.d.ts +2 -0
- package/dist/engines/cursor.js +120 -0
- package/dist/engines/gemini.d.ts +2 -0
- package/dist/engines/gemini.js +94 -0
- package/dist/engines/index.d.ts +12 -0
- package/dist/engines/index.js +68 -0
- package/dist/engines/types.d.ts +47 -0
- package/dist/engines/types.js +2 -0
- package/dist/exec.d.ts +20 -0
- package/dist/exec.js +97 -0
- package/dist/job.d.ts +7 -0
- package/dist/job.js +391 -0
- package/dist/locks.d.ts +4 -0
- package/dist/locks.js +13 -0
- package/dist/pool.d.ts +12 -0
- package/dist/pool.js +47 -0
- package/dist/session-stream.d.ts +12 -0
- package/dist/session-stream.js +42 -0
- package/dist/viewer.d.ts +16 -0
- package/dist/viewer.js +172 -0
- package/package.json +51 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
function cursorSummary(stdout) {
|
|
2
|
+
const lines = stdout.trim().split('\n');
|
|
3
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
4
|
+
try {
|
|
5
|
+
const ev = JSON.parse(lines[i]);
|
|
6
|
+
if (ev.type === 'result' && typeof ev.result === 'string' && ev.result.trim()) {
|
|
7
|
+
return ev.result.trim();
|
|
8
|
+
}
|
|
9
|
+
if (typeof ev.message === 'string' && ev.message.trim())
|
|
10
|
+
return ev.message.trim();
|
|
11
|
+
if (typeof ev.message === 'object' && ev.message?.content) {
|
|
12
|
+
const t = ev.message.content
|
|
13
|
+
.filter((c) => c.type === 'text' && c.text)
|
|
14
|
+
.map((c) => c.text)
|
|
15
|
+
.join('')
|
|
16
|
+
.trim();
|
|
17
|
+
if (t)
|
|
18
|
+
return t;
|
|
19
|
+
}
|
|
20
|
+
if (typeof ev.text === 'string' && ev.text.trim())
|
|
21
|
+
return ev.text.trim();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
export const cursorAdapter = {
|
|
29
|
+
id: 'cursor_cli',
|
|
30
|
+
displayName: 'Cursor',
|
|
31
|
+
branchPrefix: 'cursor',
|
|
32
|
+
bin(cfg) {
|
|
33
|
+
return cfg.cursorAgentBin ?? 'cursor-agent';
|
|
34
|
+
},
|
|
35
|
+
buildInvocation(ctx) {
|
|
36
|
+
const { cfg } = ctx;
|
|
37
|
+
if (ctx.isWin) {
|
|
38
|
+
throw new Error('The Cursor CLI engine is not supported on native Windows runners (Cursor supports Windows via WSL only). Run the firefunc-runner inside WSL, or pick another engine for this project.');
|
|
39
|
+
}
|
|
40
|
+
const args = ['-p', ctx.prompt, '--force', '--output-format', 'stream-json', '--trust'];
|
|
41
|
+
if (ctx.model)
|
|
42
|
+
args.push('--model', ctx.model);
|
|
43
|
+
args.push(...(cfg.cursorArgs ?? []));
|
|
44
|
+
const env = cfg.cursorApiKey
|
|
45
|
+
? { ...ctx.runEnv, CURSOR_API_KEY: cfg.cursorApiKey }
|
|
46
|
+
: ctx.runEnv;
|
|
47
|
+
const authLogLine = cfg.cursorApiKey
|
|
48
|
+
? 'CURSOR_API_KEY from runner config (bills your Cursor plan)'
|
|
49
|
+
: ctx.runEnv.CURSOR_API_KEY
|
|
50
|
+
? 'CURSOR_API_KEY from the environment (bills your Cursor plan)'
|
|
51
|
+
: 'NO Cursor API key configured — the run will fail with a setup hint';
|
|
52
|
+
return {
|
|
53
|
+
bin: this.bin(cfg),
|
|
54
|
+
args,
|
|
55
|
+
env,
|
|
56
|
+
stdinInput: undefined,
|
|
57
|
+
shell: false,
|
|
58
|
+
authLogLine,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
makeStreamPrinter() {
|
|
62
|
+
const short = (s, n = 160) => {
|
|
63
|
+
const one = s.replace(/\s+/g, ' ').trim();
|
|
64
|
+
return one.length > n ? `${one.slice(0, n)}…` : one;
|
|
65
|
+
};
|
|
66
|
+
let buf = '';
|
|
67
|
+
return (chunk) => {
|
|
68
|
+
buf += chunk;
|
|
69
|
+
const lines = buf.split('\n');
|
|
70
|
+
buf = lines.pop() ?? '';
|
|
71
|
+
for (const line of lines) {
|
|
72
|
+
if (!line.trim())
|
|
73
|
+
continue;
|
|
74
|
+
try {
|
|
75
|
+
const ev = JSON.parse(line);
|
|
76
|
+
if (ev.type === 'result' && ev.result) {
|
|
77
|
+
process.stdout.write(` └ ${short(ev.result, 240)}\n`);
|
|
78
|
+
}
|
|
79
|
+
else if (ev.message?.content) {
|
|
80
|
+
const t = ev.message.content
|
|
81
|
+
.filter((c) => c.type === 'text' && c.text)
|
|
82
|
+
.map((c) => c.text)
|
|
83
|
+
.join('');
|
|
84
|
+
if (t.trim())
|
|
85
|
+
process.stdout.write(` │ ${short(t)}\n`);
|
|
86
|
+
}
|
|
87
|
+
else if (ev.text) {
|
|
88
|
+
process.stdout.write(` │ ${short(ev.text)}\n`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
summarize(stdout) {
|
|
97
|
+
return cursorSummary(stdout);
|
|
98
|
+
},
|
|
99
|
+
authFailureHint(text) {
|
|
100
|
+
const t = text.toLowerCase();
|
|
101
|
+
if (t.includes('no cursor api key') ||
|
|
102
|
+
t.includes('not authenticated') ||
|
|
103
|
+
/\bunauthorized\b/.test(t) ||
|
|
104
|
+
t.includes('invalid api key') ||
|
|
105
|
+
/\b401\b/.test(t) ||
|
|
106
|
+
t.includes('please login') ||
|
|
107
|
+
t.includes('agent login')) {
|
|
108
|
+
return 'Cursor could not authenticate. Create a User API Key in the Cursor dashboard and set it on the runner (`--cursor-api-key` / CURSOR_API_KEY), then retry. Note: Cursor headless runs bill your Cursor plan via the API key.';
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
},
|
|
112
|
+
rateLimitHint(text) {
|
|
113
|
+
const t = text.toLowerCase();
|
|
114
|
+
if (/rate limit|rate_limit|too many requests|\b429\b|usage limit/.test(t)) {
|
|
115
|
+
return 'Cursor hit a rate / usage limit on your plan. This run was throttled, not failed: retry shortly, or lower "max parallel".';
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=cursor.js.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
function geminiSummary(stdout) {
|
|
2
|
+
const trimmed = stdout.trim();
|
|
3
|
+
try {
|
|
4
|
+
const j = JSON.parse(trimmed);
|
|
5
|
+
if (typeof j.response === 'string' && j.response.trim())
|
|
6
|
+
return j.response.trim();
|
|
7
|
+
if (j.error?.message)
|
|
8
|
+
return j.error.message.trim();
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
}
|
|
12
|
+
const lines = trimmed.split('\n');
|
|
13
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
14
|
+
try {
|
|
15
|
+
const ev = JSON.parse(lines[i]);
|
|
16
|
+
if (typeof ev.response === 'string' && ev.response.trim())
|
|
17
|
+
return ev.response.trim();
|
|
18
|
+
if (typeof ev.message === 'string' && ev.message.trim())
|
|
19
|
+
return ev.message.trim();
|
|
20
|
+
if (typeof ev.message === 'object' && ev.message?.content)
|
|
21
|
+
return String(ev.message.content).trim();
|
|
22
|
+
if (ev.error?.message)
|
|
23
|
+
return ev.error.message.trim();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return trimmed.split('\n').slice(-8).join(' ').replace(/\s+/g, ' ').trim().slice(0, 800);
|
|
29
|
+
}
|
|
30
|
+
export const geminiAdapter = {
|
|
31
|
+
id: 'gemini_cli',
|
|
32
|
+
displayName: 'Gemini CLI',
|
|
33
|
+
branchPrefix: 'gemini',
|
|
34
|
+
bin(cfg) {
|
|
35
|
+
return cfg.geminiBin ?? 'gemini';
|
|
36
|
+
},
|
|
37
|
+
buildInvocation(ctx) {
|
|
38
|
+
const { cfg } = ctx;
|
|
39
|
+
const args = [
|
|
40
|
+
...(ctx.isWin ? [] : ['-p', ctx.prompt]),
|
|
41
|
+
'--approval-mode',
|
|
42
|
+
'yolo',
|
|
43
|
+
'--output-format',
|
|
44
|
+
'json',
|
|
45
|
+
'--skip-trust',
|
|
46
|
+
];
|
|
47
|
+
if (ctx.model)
|
|
48
|
+
args.push('--model', ctx.model);
|
|
49
|
+
args.push(...(cfg.geminiArgs ?? []));
|
|
50
|
+
const env = cfg.geminiApiKey
|
|
51
|
+
? { ...ctx.runEnv, GEMINI_API_KEY: cfg.geminiApiKey }
|
|
52
|
+
: ctx.runEnv;
|
|
53
|
+
const authLogLine = cfg.geminiApiKey
|
|
54
|
+
? 'GEMINI_API_KEY from runner config (free tier 250/day, or paid)'
|
|
55
|
+
: ctx.runEnv.GEMINI_API_KEY
|
|
56
|
+
? 'GEMINI_API_KEY from the environment'
|
|
57
|
+
: 'Gemini auth from the environment (GEMINI_API_KEY / Vertex / enterprise Code Assist)';
|
|
58
|
+
return {
|
|
59
|
+
bin: this.bin(cfg),
|
|
60
|
+
args,
|
|
61
|
+
env,
|
|
62
|
+
stdinInput: ctx.isWin ? ctx.prompt : undefined,
|
|
63
|
+
shell: ctx.isWin,
|
|
64
|
+
authLogLine,
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
makeStreamPrinter() {
|
|
68
|
+
return undefined;
|
|
69
|
+
},
|
|
70
|
+
summarize(stdout) {
|
|
71
|
+
return geminiSummary(stdout);
|
|
72
|
+
},
|
|
73
|
+
authFailureHint(text) {
|
|
74
|
+
const t = text.toLowerCase();
|
|
75
|
+
if (t.includes('please set an auth method') ||
|
|
76
|
+
t.includes('fatalauthenticationerror') ||
|
|
77
|
+
t.includes('not eligible') ||
|
|
78
|
+
t.includes('invalid api key') ||
|
|
79
|
+
t.includes('api key not valid') ||
|
|
80
|
+
t.includes('exit code 41') ||
|
|
81
|
+
/\b401\b/.test(t)) {
|
|
82
|
+
return 'Gemini CLI could not authenticate. Consumer "Login with Google" was retired for Gemini CLI in June 2026 — set a GEMINI_API_KEY (Google AI Studio; free 250 req/day) on the runner (`--gemini-api-key` / GEMINI_API_KEY) or use Vertex/enterprise Code Assist, then retry.';
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
},
|
|
86
|
+
rateLimitHint(text) {
|
|
87
|
+
const t = text.toLowerCase();
|
|
88
|
+
if (/rate limit|resource_exhausted|too many requests|\b429\b|quota exceeded|quota_exceeded/.test(t)) {
|
|
89
|
+
return 'Gemini CLI hit a rate / quota limit (the API-key free tier allows ~250 req/day). This run was throttled, not failed: retry shortly, lower "max parallel", or use a paid key.';
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
//# sourceMappingURL=gemini.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AgentAdapter, EngineId } from './types.js';
|
|
2
|
+
import type { RunnerConfig } from '../config.js';
|
|
3
|
+
export type { AgentAdapter, EngineId, Invocation, InvocationContext } from './types.js';
|
|
4
|
+
export { claudeAdapter } from './claude.js';
|
|
5
|
+
export { codexAdapter } from './codex.js';
|
|
6
|
+
export { geminiAdapter } from './gemini.js';
|
|
7
|
+
export { cursorAdapter } from './cursor.js';
|
|
8
|
+
export declare function getAdapter(engine: string | null | undefined): AgentAdapter;
|
|
9
|
+
export declare function supportedEngines(): EngineId[];
|
|
10
|
+
export declare function binaryOnPath(bin: string): boolean;
|
|
11
|
+
export declare function availableEngines(cfg: RunnerConfig): EngineId[];
|
|
12
|
+
export declare function stripForeignEngineCreds(env: NodeJS.ProcessEnv, engine: EngineId): NodeJS.ProcessEnv;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { delimiter, isAbsolute, join } from 'node:path';
|
|
3
|
+
import { claudeAdapter } from './claude.js';
|
|
4
|
+
import { codexAdapter } from './codex.js';
|
|
5
|
+
import { geminiAdapter } from './gemini.js';
|
|
6
|
+
import { cursorAdapter } from './cursor.js';
|
|
7
|
+
export { claudeAdapter } from './claude.js';
|
|
8
|
+
export { codexAdapter } from './codex.js';
|
|
9
|
+
export { geminiAdapter } from './gemini.js';
|
|
10
|
+
export { cursorAdapter } from './cursor.js';
|
|
11
|
+
const REGISTRY = {
|
|
12
|
+
claude_code: claudeAdapter,
|
|
13
|
+
codex_cli: codexAdapter,
|
|
14
|
+
gemini_cli: geminiAdapter,
|
|
15
|
+
cursor_cli: cursorAdapter,
|
|
16
|
+
};
|
|
17
|
+
export function getAdapter(engine) {
|
|
18
|
+
if (engine && engine in REGISTRY)
|
|
19
|
+
return REGISTRY[engine];
|
|
20
|
+
return claudeAdapter;
|
|
21
|
+
}
|
|
22
|
+
export function supportedEngines() {
|
|
23
|
+
return Object.keys(REGISTRY);
|
|
24
|
+
}
|
|
25
|
+
export function binaryOnPath(bin) {
|
|
26
|
+
if (!bin)
|
|
27
|
+
return false;
|
|
28
|
+
const isWin = process.platform === 'win32';
|
|
29
|
+
const exts = isWin ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT;.COM').split(';') : [''];
|
|
30
|
+
const bases = isAbsolute(bin) || bin.includes('/') || (isWin && bin.includes('\\'))
|
|
31
|
+
? [bin]
|
|
32
|
+
: (process.env.PATH ?? '')
|
|
33
|
+
.split(delimiter)
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.map((d) => join(d, bin));
|
|
36
|
+
for (const base of bases) {
|
|
37
|
+
for (const ext of exts) {
|
|
38
|
+
const p = base + ext;
|
|
39
|
+
try {
|
|
40
|
+
if (existsSync(p) && statSync(p).isFile())
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
export function availableEngines(cfg) {
|
|
50
|
+
return supportedEngines().filter((id) => binaryOnPath(REGISTRY[id].bin(cfg)));
|
|
51
|
+
}
|
|
52
|
+
const ENGINE_CRED_VARS = {
|
|
53
|
+
claude_code: ['ANTHROPIC_API_KEY', 'CLAUDE_CODE_OAUTH_TOKEN'],
|
|
54
|
+
codex_cli: ['OPENAI_API_KEY', 'CODEX_API_KEY'],
|
|
55
|
+
gemini_cli: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
|
|
56
|
+
cursor_cli: ['CURSOR_API_KEY'],
|
|
57
|
+
};
|
|
58
|
+
export function stripForeignEngineCreds(env, engine) {
|
|
59
|
+
const out = { ...env };
|
|
60
|
+
for (const [id, vars] of Object.entries(ENGINE_CRED_VARS)) {
|
|
61
|
+
if (id === engine)
|
|
62
|
+
continue;
|
|
63
|
+
for (const v of vars)
|
|
64
|
+
delete out[v];
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { RunnerConfig } from '../config.js';
|
|
2
|
+
export type EngineId = 'claude_code' | 'codex_cli' | 'gemini_cli' | 'cursor_cli';
|
|
3
|
+
export type InvocationContext = {
|
|
4
|
+
prompt: string;
|
|
5
|
+
model: string | null;
|
|
6
|
+
effort: string | null;
|
|
7
|
+
ultracode: boolean;
|
|
8
|
+
permissionMode: string;
|
|
9
|
+
allowedTools?: string;
|
|
10
|
+
maxBudgetUsd?: number;
|
|
11
|
+
attachDir?: string;
|
|
12
|
+
attachPaths: string[];
|
|
13
|
+
isWin: boolean;
|
|
14
|
+
runEnv: NodeJS.ProcessEnv;
|
|
15
|
+
cfg: RunnerConfig;
|
|
16
|
+
};
|
|
17
|
+
export type Invocation = {
|
|
18
|
+
bin: string;
|
|
19
|
+
args: string[];
|
|
20
|
+
env: NodeJS.ProcessEnv;
|
|
21
|
+
stdinInput?: string;
|
|
22
|
+
shell: boolean;
|
|
23
|
+
authLogLine: string;
|
|
24
|
+
};
|
|
25
|
+
export type EngineBudgetWindow = {
|
|
26
|
+
window: string;
|
|
27
|
+
label: string;
|
|
28
|
+
pctUsed: number;
|
|
29
|
+
resetsAt: string | null;
|
|
30
|
+
};
|
|
31
|
+
export type EngineBudgetReport = {
|
|
32
|
+
engine: EngineId;
|
|
33
|
+
planLabel?: string;
|
|
34
|
+
windows: EngineBudgetWindow[];
|
|
35
|
+
};
|
|
36
|
+
export interface AgentAdapter {
|
|
37
|
+
readonly id: EngineId;
|
|
38
|
+
readonly displayName: string;
|
|
39
|
+
readonly branchPrefix: string;
|
|
40
|
+
bin(cfg: RunnerConfig): string;
|
|
41
|
+
buildInvocation(ctx: InvocationContext): Invocation;
|
|
42
|
+
makeStreamPrinter(): ((chunk: string) => void) | undefined;
|
|
43
|
+
summarize(stdout: string): string;
|
|
44
|
+
authFailureHint(text: string): string | null;
|
|
45
|
+
rateLimitHint(text: string): string | null;
|
|
46
|
+
readBudget?(cfg: RunnerConfig): Promise<EngineBudgetReport | null>;
|
|
47
|
+
}
|
package/dist/exec.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ChildProcess } from 'node:child_process';
|
|
2
|
+
export type ExecResult = {
|
|
3
|
+
code: number;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function spawnBackground(cmd: string, opts?: {
|
|
8
|
+
cwd?: string;
|
|
9
|
+
env?: NodeJS.ProcessEnv;
|
|
10
|
+
}): ChildProcess;
|
|
11
|
+
export declare function killProcessTree(child: ChildProcess): Promise<void>;
|
|
12
|
+
export declare function exec(cmd: string, args: string[], opts?: {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
env?: NodeJS.ProcessEnv;
|
|
16
|
+
input?: string;
|
|
17
|
+
shell?: boolean;
|
|
18
|
+
onStdout?: (chunk: string) => void;
|
|
19
|
+
}): Promise<ExecResult>;
|
|
20
|
+
export declare function execOut(cmd: string, args: string[], cwd?: string): Promise<string>;
|
package/dist/exec.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
function mergeEnv(extra) {
|
|
3
|
+
const env = { ...process.env, ...extra };
|
|
4
|
+
for (const k of Object.keys(env))
|
|
5
|
+
if (env[k] === undefined)
|
|
6
|
+
delete env[k];
|
|
7
|
+
return env;
|
|
8
|
+
}
|
|
9
|
+
export function spawnBackground(cmd, opts = {}) {
|
|
10
|
+
const child = spawn(cmd, [], {
|
|
11
|
+
cwd: opts.cwd,
|
|
12
|
+
env: mergeEnv(opts.env),
|
|
13
|
+
stdio: 'ignore',
|
|
14
|
+
shell: true,
|
|
15
|
+
detached: process.platform !== 'win32',
|
|
16
|
+
});
|
|
17
|
+
child.on('error', () => { });
|
|
18
|
+
return child;
|
|
19
|
+
}
|
|
20
|
+
export async function killProcessTree(child) {
|
|
21
|
+
if (!child.pid)
|
|
22
|
+
return;
|
|
23
|
+
if (process.platform === 'win32') {
|
|
24
|
+
try {
|
|
25
|
+
spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore' });
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const groupKill = (sig) => {
|
|
32
|
+
try {
|
|
33
|
+
process.kill(-child.pid, sig);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
try {
|
|
37
|
+
child.kill(sig);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
groupKill('SIGTERM');
|
|
44
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
45
|
+
groupKill('SIGKILL');
|
|
46
|
+
}
|
|
47
|
+
export function exec(cmd, args, opts = {}) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const child = spawn(cmd, args, {
|
|
50
|
+
cwd: opts.cwd,
|
|
51
|
+
env: mergeEnv(opts.env),
|
|
52
|
+
stdio: [opts.input != null ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
53
|
+
shell: opts.shell,
|
|
54
|
+
});
|
|
55
|
+
if (opts.input != null) {
|
|
56
|
+
child.stdin?.on('error', () => { });
|
|
57
|
+
child.stdin?.write(opts.input);
|
|
58
|
+
child.stdin?.end();
|
|
59
|
+
}
|
|
60
|
+
let stdout = '';
|
|
61
|
+
let stderr = '';
|
|
62
|
+
let timer = null;
|
|
63
|
+
let timedOut = false;
|
|
64
|
+
if (opts.timeoutMs) {
|
|
65
|
+
timer = setTimeout(() => {
|
|
66
|
+
timedOut = true;
|
|
67
|
+
child.kill('SIGTERM');
|
|
68
|
+
setTimeout(() => child.kill('SIGKILL'), 5_000);
|
|
69
|
+
}, opts.timeoutMs);
|
|
70
|
+
}
|
|
71
|
+
child.stdout?.on('data', (d) => {
|
|
72
|
+
const s = d.toString();
|
|
73
|
+
stdout += s;
|
|
74
|
+
opts.onStdout?.(s);
|
|
75
|
+
});
|
|
76
|
+
child.stderr?.on('data', (d) => (stderr += d.toString()));
|
|
77
|
+
child.on('error', (err) => {
|
|
78
|
+
if (timer)
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
reject(err);
|
|
81
|
+
});
|
|
82
|
+
child.on('close', (code) => {
|
|
83
|
+
if (timer)
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
resolve({
|
|
86
|
+
code: timedOut ? 124 : (code ?? 1),
|
|
87
|
+
stdout,
|
|
88
|
+
stderr: timedOut ? `${stderr}\n[firefunc-runner] timed out` : stderr,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
export async function execOut(cmd, args, cwd) {
|
|
94
|
+
const r = await exec(cmd, args, { cwd });
|
|
95
|
+
return r.code === 0 ? r.stdout.trim() : '';
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=exec.js.map
|
package/dist/job.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { RunnerApi, ClaimedJob } from './api.js';
|
|
2
|
+
import { type RunnerConfig } from './config.js';
|
|
3
|
+
import type { SessionViewer } from './viewer.js';
|
|
4
|
+
export declare function resolveRepoPath(repo: string | null | undefined, cfg: RunnerConfig): string | null;
|
|
5
|
+
export declare function runJob(job: ClaimedJob, cfg: RunnerConfig, api: RunnerApi, viewer?: SessionViewer, opts?: {
|
|
6
|
+
devServerPort?: number;
|
|
7
|
+
}): Promise<void>;
|