@lovelybunch/api 1.0.57 → 1.0.59
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/lib/git.d.ts +13 -0
- package/dist/lib/git.js +137 -5
- package/dist/lib/jobs/global-job-scheduler.d.ts +2 -0
- package/dist/lib/jobs/global-job-scheduler.js +11 -0
- package/dist/lib/jobs/job-runner.d.ts +17 -0
- package/dist/lib/jobs/job-runner.js +167 -0
- package/dist/lib/jobs/job-scheduler.d.ts +39 -0
- package/dist/lib/jobs/job-scheduler.js +309 -0
- package/dist/lib/jobs/job-store.d.ts +16 -0
- package/dist/lib/jobs/job-store.js +211 -0
- package/dist/lib/storage/file-storage.js +7 -5
- package/dist/lib/terminal/terminal-manager.d.ts +2 -0
- package/dist/lib/terminal/terminal-manager.js +65 -0
- package/dist/routes/api/v1/ai/route.d.ts +1 -7
- package/dist/routes/api/v1/ai/route.js +25 -12
- package/dist/routes/api/v1/git/index.js +63 -1
- package/dist/routes/api/v1/jobs/[id]/route.d.ts +133 -0
- package/dist/routes/api/v1/jobs/[id]/route.js +135 -0
- package/dist/routes/api/v1/jobs/[id]/run/route.d.ts +31 -0
- package/dist/routes/api/v1/jobs/[id]/run/route.js +37 -0
- package/dist/routes/api/v1/jobs/index.d.ts +3 -0
- package/dist/routes/api/v1/jobs/index.js +14 -0
- package/dist/routes/api/v1/jobs/route.d.ts +108 -0
- package/dist/routes/api/v1/jobs/route.js +144 -0
- package/dist/routes/api/v1/jobs/status/route.d.ts +23 -0
- package/dist/routes/api/v1/jobs/status/route.js +21 -0
- package/dist/routes/api/v1/resources/[id]/route.d.ts +2 -44
- package/dist/server-with-static.js +5 -0
- package/dist/server.js +5 -0
- package/package.json +4 -4
- package/static/assets/index-CHq6mL1J.css +33 -0
- package/static/assets/index-QHnHUcsV.js +820 -0
- package/static/index.html +2 -2
- package/static/assets/index-CRg4lVi6.js +0 -779
- package/static/assets/index-VqhUTak4.css +0 -33
package/dist/lib/git.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare function sanitizeBranchName(name: string): string;
|
|
|
4
4
|
export declare function resolveSafeWorktreePath(name: string): Promise<string>;
|
|
5
5
|
export declare function runGit(args: string[], opts?: {
|
|
6
6
|
cwd?: string;
|
|
7
|
+
timeout?: number;
|
|
7
8
|
}): Promise<{
|
|
8
9
|
stdout: string;
|
|
9
10
|
stderr: string;
|
|
@@ -25,9 +26,21 @@ export declare function listBranches(): Promise<{
|
|
|
25
26
|
}[]>;
|
|
26
27
|
export declare function createBranch(name: string, from?: string): Promise<void>;
|
|
27
28
|
export declare function deleteBranch(name: string): Promise<void>;
|
|
29
|
+
export declare function switchBranch(name: string): Promise<void>;
|
|
30
|
+
export declare function mergeBranch(branchName: string, strategy?: 'merge' | 'squash' | 'rebase'): Promise<string>;
|
|
28
31
|
export declare function pushCurrent(): Promise<string>;
|
|
29
32
|
export type PullStrategy = 'merge' | 'rebase' | 'ff-only';
|
|
30
33
|
export declare function pullCurrent(strategy?: PullStrategy): Promise<string>;
|
|
34
|
+
export declare function checkRemoteAuth(): Promise<{
|
|
35
|
+
authenticated: boolean;
|
|
36
|
+
remote?: string;
|
|
37
|
+
error?: string;
|
|
38
|
+
}>;
|
|
39
|
+
export declare function getCredentialConfig(): Promise<{
|
|
40
|
+
helper?: string;
|
|
41
|
+
origin?: string;
|
|
42
|
+
}>;
|
|
43
|
+
export declare function storeCredentials(username: string, password: string): Promise<void>;
|
|
31
44
|
export interface WorktreeInfo {
|
|
32
45
|
name: string;
|
|
33
46
|
path: string;
|
package/dist/lib/git.js
CHANGED
|
@@ -43,7 +43,13 @@ export async function resolveSafeWorktreePath(name) {
|
|
|
43
43
|
export async function runGit(args, opts) {
|
|
44
44
|
const repoRoot = await getRepoRoot();
|
|
45
45
|
const cwd = opts?.cwd || repoRoot;
|
|
46
|
-
|
|
46
|
+
const timeout = opts?.timeout || 120000; // 2 minutes default
|
|
47
|
+
// Prevent git from prompting for credentials in non-interactive environment
|
|
48
|
+
const env = {
|
|
49
|
+
...process.env,
|
|
50
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
51
|
+
};
|
|
52
|
+
return execFile('git', args, { cwd, maxBuffer: 10 * 1024 * 1024, timeout, env });
|
|
47
53
|
}
|
|
48
54
|
// --- Status ---
|
|
49
55
|
export async function getRepoStatus() {
|
|
@@ -106,16 +112,142 @@ export async function deleteBranch(name) {
|
|
|
106
112
|
const safe = sanitizeBranchName(name);
|
|
107
113
|
await runGit(['branch', '-D', safe]);
|
|
108
114
|
}
|
|
115
|
+
export async function switchBranch(name) {
|
|
116
|
+
const safe = sanitizeBranchName(name);
|
|
117
|
+
await runGit(['switch', safe]);
|
|
118
|
+
}
|
|
119
|
+
export async function mergeBranch(branchName, strategy = 'merge') {
|
|
120
|
+
const safe = sanitizeBranchName(branchName);
|
|
121
|
+
if (strategy === 'rebase') {
|
|
122
|
+
const { stdout } = await runGit(['rebase', safe]);
|
|
123
|
+
return stdout;
|
|
124
|
+
}
|
|
125
|
+
else if (strategy === 'squash') {
|
|
126
|
+
const { stdout } = await runGit(['merge', '--squash', safe]);
|
|
127
|
+
return stdout;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const { stdout } = await runGit(['merge', safe]);
|
|
131
|
+
return stdout;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
109
134
|
// --- Push / Pull ---
|
|
110
135
|
export async function pushCurrent() {
|
|
111
|
-
const { stdout } = await runGit(['push']);
|
|
136
|
+
const { stdout } = await runGit(['push'], { timeout: 30000 }); // 30 second timeout for push
|
|
112
137
|
return stdout;
|
|
113
138
|
}
|
|
114
139
|
export async function pullCurrent(strategy = 'rebase') {
|
|
115
140
|
const args = strategy === 'rebase' ? ['pull', '--rebase'] : strategy === 'ff-only' ? ['pull', '--ff-only'] : ['pull', '--no-rebase'];
|
|
116
|
-
const { stdout } = await runGit(args);
|
|
141
|
+
const { stdout } = await runGit(args, { timeout: 30000 }); // 30 second timeout for pull
|
|
117
142
|
return stdout;
|
|
118
143
|
}
|
|
144
|
+
// --- Auth Status ---
|
|
145
|
+
export async function checkRemoteAuth() {
|
|
146
|
+
try {
|
|
147
|
+
// Get remote URL
|
|
148
|
+
const { stdout: remoteUrl } = await runGit(['config', '--get', 'remote.origin.url']);
|
|
149
|
+
const remote = remoteUrl.trim();
|
|
150
|
+
// Try to ls-remote with minimal timeout
|
|
151
|
+
await runGit(['ls-remote', '--exit-code', 'origin', 'HEAD'], { timeout: 10000 });
|
|
152
|
+
return { authenticated: true, remote };
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
// Check if it's an auth error vs other errors
|
|
156
|
+
const message = error.message || error.stderr || '';
|
|
157
|
+
const isAuthError = message.includes('Authentication failed') ||
|
|
158
|
+
message.includes('could not read Username') ||
|
|
159
|
+
message.includes('could not read Password') ||
|
|
160
|
+
message.includes('terminal prompts disabled') ||
|
|
161
|
+
error.code === 'ETIMEDOUT';
|
|
162
|
+
return {
|
|
163
|
+
authenticated: false,
|
|
164
|
+
error: isAuthError ? 'Authentication required' : message
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export async function getCredentialConfig() {
|
|
169
|
+
try {
|
|
170
|
+
const { stdout } = await runGit(['config', '--show-origin', '--get', 'credential.helper']);
|
|
171
|
+
const lines = stdout.trim().split('\n');
|
|
172
|
+
const lastLine = lines[lines.length - 1]; // Get the last configured helper (takes precedence)
|
|
173
|
+
if (lastLine) {
|
|
174
|
+
// Format: "file:/path/to/config\thelper-value"
|
|
175
|
+
const [origin, helper] = lastLine.split('\t');
|
|
176
|
+
return { helper: helper?.trim(), origin: origin?.replace('file:', '').trim() };
|
|
177
|
+
}
|
|
178
|
+
return {};
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
// No credential helper configured
|
|
182
|
+
return {};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export async function storeCredentials(username, password) {
|
|
186
|
+
// Get remote URL to determine protocol and host
|
|
187
|
+
const { stdout: remoteUrl } = await runGit(['config', '--get', 'remote.origin.url']);
|
|
188
|
+
const remote = remoteUrl.trim();
|
|
189
|
+
// Parse the remote URL to extract protocol and host
|
|
190
|
+
let protocol = 'https';
|
|
191
|
+
let host = '';
|
|
192
|
+
if (remote.startsWith('https://')) {
|
|
193
|
+
protocol = 'https';
|
|
194
|
+
host = remote.replace('https://', '').split('/')[0];
|
|
195
|
+
}
|
|
196
|
+
else if (remote.startsWith('http://')) {
|
|
197
|
+
protocol = 'http';
|
|
198
|
+
host = remote.replace('http://', '').split('/')[0];
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
throw new Error('Remote URL is not HTTPS/HTTP. Please use SSH keys for SSH remotes.');
|
|
202
|
+
}
|
|
203
|
+
// Ensure a credential helper is configured
|
|
204
|
+
const config = await getCredentialConfig();
|
|
205
|
+
if (!config.helper) {
|
|
206
|
+
// Auto-configure based on platform
|
|
207
|
+
const platform = process.platform;
|
|
208
|
+
if (platform === 'darwin') {
|
|
209
|
+
await runGit(['config', '--global', 'credential.helper', 'osxkeychain']);
|
|
210
|
+
}
|
|
211
|
+
else if (platform === 'win32') {
|
|
212
|
+
await runGit(['config', '--global', 'credential.helper', 'manager']);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
// Linux - use store as fallback
|
|
216
|
+
await runGit(['config', '--global', 'credential.helper', 'store']);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// Use git credential approve to store credentials via stdin
|
|
220
|
+
const credentialInput = `protocol=${protocol}\nhost=${host}\nusername=${username}\npassword=${password}\n\n`;
|
|
221
|
+
const repoRoot = await getRepoRoot();
|
|
222
|
+
const { spawn } = await import('child_process');
|
|
223
|
+
return new Promise((resolve, reject) => {
|
|
224
|
+
const child = spawn('git', ['credential', 'approve'], {
|
|
225
|
+
cwd: repoRoot,
|
|
226
|
+
env: {
|
|
227
|
+
...process.env,
|
|
228
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
let stderr = '';
|
|
232
|
+
child.stderr?.on('data', (data) => {
|
|
233
|
+
stderr += data.toString();
|
|
234
|
+
});
|
|
235
|
+
child.on('error', (err) => {
|
|
236
|
+
reject(err);
|
|
237
|
+
});
|
|
238
|
+
child.on('close', (code) => {
|
|
239
|
+
if (code === 0) {
|
|
240
|
+
resolve();
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
reject(new Error(`git credential approve failed with code ${code}: ${stderr}`));
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
// Write credentials to stdin
|
|
247
|
+
child.stdin?.write(credentialInput);
|
|
248
|
+
child.stdin?.end();
|
|
249
|
+
});
|
|
250
|
+
}
|
|
119
251
|
export async function listWorktrees() {
|
|
120
252
|
const { stdout } = await runGit(['worktree', 'list', '--porcelain']);
|
|
121
253
|
// Parse porcelain groups separated by blank lines with fields: worktree <path>, HEAD <sha>, branch <refs/heads/x>, locked
|
|
@@ -195,13 +327,13 @@ export async function commitInWorktree(name, message, files) {
|
|
|
195
327
|
export async function pushWorktree(name) {
|
|
196
328
|
const found = await getWorktreeByName(name);
|
|
197
329
|
const wtPath = found ? found.path : await resolveSafeWorktreePath(name);
|
|
198
|
-
const { stdout } = await runGit(['-C', wtPath, 'push']);
|
|
330
|
+
const { stdout } = await runGit(['-C', wtPath, 'push'], { timeout: 30000 }); // 30 second timeout
|
|
199
331
|
return stdout;
|
|
200
332
|
}
|
|
201
333
|
export async function pullWorktree(name, strategy = 'rebase') {
|
|
202
334
|
const found = await getWorktreeByName(name);
|
|
203
335
|
const wtPath = found ? found.path : await resolveSafeWorktreePath(name);
|
|
204
336
|
const args = strategy === 'rebase' ? ['-C', wtPath, 'pull', '--rebase'] : strategy === 'ff-only' ? ['-C', wtPath, 'pull', '--ff-only'] : ['-C', wtPath, 'pull', '--no-rebase'];
|
|
205
|
-
const { stdout } = await runGit(args);
|
|
337
|
+
const { stdout } = await runGit(args, { timeout: 30000 }); // 30 second timeout
|
|
206
338
|
return stdout;
|
|
207
339
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { JobScheduler } from './job-scheduler.js';
|
|
2
|
+
let scheduler = null;
|
|
3
|
+
export function getGlobalJobScheduler() {
|
|
4
|
+
if (!scheduler) {
|
|
5
|
+
scheduler = new JobScheduler();
|
|
6
|
+
scheduler.initialize().catch((error) => {
|
|
7
|
+
console.error('Failed to initialize job scheduler:', error);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
return scheduler;
|
|
11
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ScheduledJob } from '@lovelybunch/types';
|
|
2
|
+
interface JobRunResult {
|
|
3
|
+
status: 'succeeded' | 'failed';
|
|
4
|
+
summary?: string;
|
|
5
|
+
outputPath?: string;
|
|
6
|
+
error?: string;
|
|
7
|
+
cliCommand: string;
|
|
8
|
+
}
|
|
9
|
+
export declare class JobRunner {
|
|
10
|
+
private projectRootPromise;
|
|
11
|
+
constructor();
|
|
12
|
+
private ensureCliAvailable;
|
|
13
|
+
private ensureLogPath;
|
|
14
|
+
private buildInstruction;
|
|
15
|
+
run(job: ScheduledJob, runId: string): Promise<JobRunResult>;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { spawn, spawnSync } from 'child_process';
|
|
2
|
+
import { createWriteStream } from 'fs';
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { getProjectRoot } from '../project-paths.js';
|
|
6
|
+
function shellQuote(value) {
|
|
7
|
+
if (value === '')
|
|
8
|
+
return "''";
|
|
9
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
10
|
+
}
|
|
11
|
+
function resolveAgent(model) {
|
|
12
|
+
if (!model)
|
|
13
|
+
return 'claude';
|
|
14
|
+
const lower = model.toLowerCase();
|
|
15
|
+
if (lower.includes('gemini'))
|
|
16
|
+
return 'gemini';
|
|
17
|
+
if (lower.includes('codex') || lower.includes('gpt') || lower.includes('openai'))
|
|
18
|
+
return 'codex';
|
|
19
|
+
return 'claude';
|
|
20
|
+
}
|
|
21
|
+
function buildCommand(agent, instruction) {
|
|
22
|
+
const quotedInstruction = shellQuote(instruction);
|
|
23
|
+
switch (agent) {
|
|
24
|
+
case 'gemini': {
|
|
25
|
+
const cmd = `gemini --yolo -i ${quotedInstruction}`;
|
|
26
|
+
return { command: 'gemini', shellCommand: cmd };
|
|
27
|
+
}
|
|
28
|
+
case 'codex': {
|
|
29
|
+
const cmd = `codex ${quotedInstruction} --dangerously-bypass-approvals-and-sandbox`;
|
|
30
|
+
return { command: 'codex', shellCommand: cmd };
|
|
31
|
+
}
|
|
32
|
+
case 'claude':
|
|
33
|
+
default: {
|
|
34
|
+
const cmd = `claude ${quotedInstruction} --dangerously-skip-permissions`;
|
|
35
|
+
return { command: 'claude', shellCommand: cmd };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const CLI_AGENT_LABEL = {
|
|
40
|
+
claude: 'Claude',
|
|
41
|
+
gemini: 'Gemini',
|
|
42
|
+
codex: 'Code'
|
|
43
|
+
};
|
|
44
|
+
const CLI_AGENT_BINARY = {
|
|
45
|
+
claude: 'claude',
|
|
46
|
+
gemini: 'gemini',
|
|
47
|
+
codex: 'codex'
|
|
48
|
+
};
|
|
49
|
+
const DEFAULT_MAX_RUNTIME_MS = 30 * 60 * 1000; // 30 minutes
|
|
50
|
+
function getMaxRuntime() {
|
|
51
|
+
const raw = process.env.COCONUT_JOB_MAX_RUNTIME_MS;
|
|
52
|
+
if (!raw)
|
|
53
|
+
return DEFAULT_MAX_RUNTIME_MS;
|
|
54
|
+
const parsed = Number(raw);
|
|
55
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
56
|
+
return DEFAULT_MAX_RUNTIME_MS;
|
|
57
|
+
}
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
export class JobRunner {
|
|
61
|
+
projectRootPromise;
|
|
62
|
+
constructor() {
|
|
63
|
+
this.projectRootPromise = getProjectRoot();
|
|
64
|
+
}
|
|
65
|
+
ensureCliAvailable(agent) {
|
|
66
|
+
const binary = CLI_AGENT_BINARY[agent];
|
|
67
|
+
const result = spawnSync('bash', ['-lc', `command -v ${binary}`], { stdio: 'ignore' });
|
|
68
|
+
if (result.status !== 0) {
|
|
69
|
+
throw new Error(`${CLI_AGENT_LABEL[agent]} CLI (“${binary}”) is not installed or not on PATH.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async ensureLogPath(jobId, runId) {
|
|
73
|
+
const projectRoot = await this.projectRootPromise;
|
|
74
|
+
const logsDir = path.join(projectRoot, '.nut', 'jobs', 'logs', jobId);
|
|
75
|
+
await fs.mkdir(logsDir, { recursive: true });
|
|
76
|
+
return path.join(logsDir, `${runId}.log`);
|
|
77
|
+
}
|
|
78
|
+
buildInstruction(job, agentLabel) {
|
|
79
|
+
const prompt = job.prompt.trim();
|
|
80
|
+
const scheduleDescription = job.schedule.type === 'cron'
|
|
81
|
+
? `Cron: ${job.schedule.expression}`
|
|
82
|
+
: `Interval: every ${job.schedule.hours}h on ${job.schedule.daysOfWeek.join(', ')}`;
|
|
83
|
+
return `Run this scheduled Coconut job (${job.id}).\nSchedule: ${scheduleDescription}.\nPreferred CLI agent: ${agentLabel}.\nInstruction:\n${prompt}`;
|
|
84
|
+
}
|
|
85
|
+
async run(job, runId) {
|
|
86
|
+
const agent = resolveAgent(job.model);
|
|
87
|
+
const instruction = this.buildInstruction(job, CLI_AGENT_LABEL[agent] || agent);
|
|
88
|
+
const { shellCommand } = buildCommand(agent, instruction);
|
|
89
|
+
const projectRoot = await this.projectRootPromise;
|
|
90
|
+
const logPath = await this.ensureLogPath(job.id, runId);
|
|
91
|
+
const logStream = createWriteStream(logPath, { flags: 'a' });
|
|
92
|
+
const summaryChunks = [];
|
|
93
|
+
logStream.write(`[${new Date().toISOString()}] Starting job ${job.id} using ${agent} CLI\n`);
|
|
94
|
+
logStream.write(`Instruction: ${instruction}\n`);
|
|
95
|
+
logStream.write(`Command: ${shellCommand}\n`);
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
let cliMissingError = null;
|
|
98
|
+
try {
|
|
99
|
+
this.ensureCliAvailable(agent);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
cliMissingError = error instanceof Error ? error : new Error(String(error));
|
|
103
|
+
}
|
|
104
|
+
if (cliMissingError) {
|
|
105
|
+
const message = cliMissingError.message;
|
|
106
|
+
logStream.write(`${message}\n`);
|
|
107
|
+
logStream.end();
|
|
108
|
+
resolve({
|
|
109
|
+
status: 'failed',
|
|
110
|
+
error: message,
|
|
111
|
+
summary: message,
|
|
112
|
+
outputPath: path.relative(projectRoot, logPath),
|
|
113
|
+
cliCommand: shellCommand,
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const child = spawn('bash', ['-lc', shellCommand], {
|
|
118
|
+
cwd: projectRoot,
|
|
119
|
+
env: process.env,
|
|
120
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
121
|
+
});
|
|
122
|
+
const maxRuntime = getMaxRuntime();
|
|
123
|
+
const abortTimeout = setTimeout(() => {
|
|
124
|
+
logStream.write(`\n[${new Date().toISOString()}] Max runtime ${maxRuntime}ms exceeded. Sending SIGTERM...\n`);
|
|
125
|
+
child.kill('SIGTERM');
|
|
126
|
+
setTimeout(() => child.kill('SIGKILL'), 10_000);
|
|
127
|
+
}, maxRuntime);
|
|
128
|
+
child.stdout?.on('data', (chunk) => {
|
|
129
|
+
const text = chunk.toString();
|
|
130
|
+
logStream.write(text);
|
|
131
|
+
summaryChunks.push(text);
|
|
132
|
+
});
|
|
133
|
+
child.stderr?.on('data', (chunk) => {
|
|
134
|
+
const text = chunk.toString();
|
|
135
|
+
logStream.write(text);
|
|
136
|
+
summaryChunks.push(text);
|
|
137
|
+
});
|
|
138
|
+
child.on('error', (error) => {
|
|
139
|
+
const message = `Failed to start CLI command: ${error.message}`;
|
|
140
|
+
logStream.write(`${message}\n`);
|
|
141
|
+
logStream.end();
|
|
142
|
+
clearTimeout(abortTimeout);
|
|
143
|
+
resolve({
|
|
144
|
+
status: 'failed',
|
|
145
|
+
error: message,
|
|
146
|
+
summary: summaryChunks.join('').slice(-600),
|
|
147
|
+
outputPath: path.relative(projectRoot, logPath),
|
|
148
|
+
cliCommand: shellCommand,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
child.on('close', (code) => {
|
|
152
|
+
const status = code === 0 ? 'succeeded' : 'failed';
|
|
153
|
+
logStream.write(`\n[${new Date().toISOString()}] Job ${job.id} completed with exit code ${code}\n`);
|
|
154
|
+
logStream.end();
|
|
155
|
+
clearTimeout(abortTimeout);
|
|
156
|
+
const summary = summaryChunks.join('');
|
|
157
|
+
resolve({
|
|
158
|
+
status,
|
|
159
|
+
summary: summary.slice(Math.max(0, summary.length - 2000)),
|
|
160
|
+
outputPath: path.relative(projectRoot, logPath),
|
|
161
|
+
error: code === 0 ? undefined : `CLI exited with code ${code}`,
|
|
162
|
+
cliCommand: shellCommand,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ScheduledJob, ScheduledJobRun, ScheduledJobStatus, ScheduledJobTrigger } from '@lovelybunch/types';
|
|
2
|
+
import { JobStore } from './job-store.js';
|
|
3
|
+
import { JobRunner } from './job-runner.js';
|
|
4
|
+
export declare class JobScheduler {
|
|
5
|
+
private store;
|
|
6
|
+
private runner;
|
|
7
|
+
private jobs;
|
|
8
|
+
private runningJobs;
|
|
9
|
+
private initialized;
|
|
10
|
+
constructor(store?: JobStore, runner?: JobRunner);
|
|
11
|
+
initialize(): Promise<void>;
|
|
12
|
+
register(job: ScheduledJob): Promise<void>;
|
|
13
|
+
unregister(jobId: string): Promise<void>;
|
|
14
|
+
refresh(jobId: string): Promise<void>;
|
|
15
|
+
runNow(jobId: string, trigger?: ScheduledJobTrigger): Promise<ScheduledJobRun | null>;
|
|
16
|
+
private clearTimer;
|
|
17
|
+
private execute;
|
|
18
|
+
private updateJobMetadata;
|
|
19
|
+
private calculateNextRun;
|
|
20
|
+
private calculateNextInterval;
|
|
21
|
+
private calculateNextCron;
|
|
22
|
+
private resolveDayMatch;
|
|
23
|
+
private parseCronField;
|
|
24
|
+
private expandCronSegment;
|
|
25
|
+
private storeCronValue;
|
|
26
|
+
getStatus(): {
|
|
27
|
+
initialized: boolean;
|
|
28
|
+
jobCount: number;
|
|
29
|
+
runningCount: number;
|
|
30
|
+
jobs: {
|
|
31
|
+
id: string;
|
|
32
|
+
status: ScheduledJobStatus;
|
|
33
|
+
nextRunAt?: Date;
|
|
34
|
+
lastRunAt?: Date;
|
|
35
|
+
timerActive: boolean;
|
|
36
|
+
running: boolean;
|
|
37
|
+
}[];
|
|
38
|
+
};
|
|
39
|
+
}
|