@serviceme/devtools-core 0.0.5 → 0.1.2
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/index.d.mts +179 -57
- package/dist/index.d.ts +179 -57
- package/dist/index.js +1172 -442
- package/dist/index.mjs +1164 -445
- package/package.json +4 -2
package/dist/index.mjs
CHANGED
|
@@ -5,11 +5,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
5
5
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
6
|
});
|
|
7
7
|
|
|
8
|
-
// src/
|
|
8
|
+
// src/copilot/doctor.ts
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
COPILOT_ERROR_CODES,
|
|
11
|
+
createServicemeError
|
|
11
12
|
} from "@serviceme/devtools-protocol";
|
|
12
|
-
import { createServicemeError } from "@serviceme/devtools-protocol";
|
|
13
13
|
|
|
14
14
|
// src/process/runCommand.ts
|
|
15
15
|
import { spawn } from "child_process";
|
|
@@ -119,19 +119,144 @@ async function commandExists(command) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// src/copilot/doctor.ts
|
|
123
|
+
var COPILOT_COMMAND = "copilot";
|
|
124
|
+
var GH_COMMAND = "gh";
|
|
125
|
+
async function isCopilotAuthenticated() {
|
|
126
|
+
try {
|
|
127
|
+
await runCommand(GH_COMMAND, {
|
|
128
|
+
args: ["auth", "status"],
|
|
129
|
+
timeoutMs: 1e4
|
|
130
|
+
});
|
|
131
|
+
return true;
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function copilotDoctor() {
|
|
137
|
+
const exists = await commandExists(COPILOT_COMMAND);
|
|
138
|
+
if (!exists) {
|
|
139
|
+
return { installed: false, version: null, authenticated: false };
|
|
140
|
+
}
|
|
141
|
+
let version = null;
|
|
142
|
+
try {
|
|
143
|
+
const versionResult = await runCommand(COPILOT_COMMAND, {
|
|
144
|
+
args: ["--version"],
|
|
145
|
+
timeoutMs: 1e4
|
|
146
|
+
});
|
|
147
|
+
version = versionResult.stdout.trim();
|
|
148
|
+
} catch {
|
|
149
|
+
return { installed: true, version: null, authenticated: false };
|
|
150
|
+
}
|
|
151
|
+
const authenticated = await isCopilotAuthenticated();
|
|
152
|
+
return { installed: true, version, authenticated };
|
|
153
|
+
}
|
|
154
|
+
function createCopilotNotInstalledError() {
|
|
155
|
+
return createServicemeError(
|
|
156
|
+
COPILOT_ERROR_CODES.NOT_INSTALLED,
|
|
157
|
+
"GitHub Copilot CLI is not installed. Visit https://docs.github.com/en/copilot/using-github-copilot/using-github-copilot-in-the-command-line to install."
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
function createCopilotAuthRequiredError() {
|
|
161
|
+
return createServicemeError(
|
|
162
|
+
COPILOT_ERROR_CODES.AUTH_REQUIRED,
|
|
163
|
+
"GitHub Copilot CLI requires authentication. Run `copilot auth login` to sign in."
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/copilot/prompt.ts
|
|
168
|
+
import {
|
|
169
|
+
COPILOT_ERROR_CODES as COPILOT_ERROR_CODES2,
|
|
170
|
+
createServicemeError as createServicemeError2
|
|
171
|
+
} from "@serviceme/devtools-protocol";
|
|
172
|
+
var COPILOT_COMMAND2 = "copilot";
|
|
173
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
174
|
+
function stripAnsi(text) {
|
|
175
|
+
return text.replace(/\x1B\[[0-9;]*[A-Za-z]/g, "");
|
|
176
|
+
}
|
|
177
|
+
async function copilotPrompt(options) {
|
|
178
|
+
const args = ["-p", options.prompt];
|
|
179
|
+
if (options.workspace) {
|
|
180
|
+
args.push("--add-dir", options.workspace);
|
|
181
|
+
}
|
|
182
|
+
if (options.autopilot) {
|
|
183
|
+
args.push("--allow-all-tools");
|
|
184
|
+
}
|
|
185
|
+
if (options.allowTools?.length) {
|
|
186
|
+
for (const tool of options.allowTools) {
|
|
187
|
+
args.push("--allow-tool", tool);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (options.model) {
|
|
191
|
+
args.push("--model", options.model);
|
|
192
|
+
}
|
|
193
|
+
if (options.agent) {
|
|
194
|
+
args.push("--agent", options.agent);
|
|
195
|
+
}
|
|
196
|
+
const timeout = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
197
|
+
try {
|
|
198
|
+
const result = await runCommand(COPILOT_COMMAND2, {
|
|
199
|
+
args,
|
|
200
|
+
cwd: options.workspace,
|
|
201
|
+
timeoutMs: timeout
|
|
202
|
+
});
|
|
203
|
+
const output = stripAnsi(result.stdout);
|
|
204
|
+
if (output.includes("I'm sorry, but I cannot assist") && result.stderr?.includes("Stream completed without a response")) {
|
|
205
|
+
throw createServicemeError2(
|
|
206
|
+
COPILOT_ERROR_CODES2.AUTH_REQUIRED,
|
|
207
|
+
"Copilot CLI returned a refusal response. This typically indicates an expired authentication token. Run `gh auth login` to re-authenticate.",
|
|
208
|
+
{ exitCode: 0, output, stderr: result.stderr }
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return { output, exitCode: 0 };
|
|
212
|
+
} catch (error) {
|
|
213
|
+
const err = error;
|
|
214
|
+
if (err.code === "ETIMEDOUT") {
|
|
215
|
+
throw createServicemeError2(
|
|
216
|
+
COPILOT_ERROR_CODES2.TIMEOUT,
|
|
217
|
+
`Copilot prompt timed out after ${String(timeout)}ms.`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const exitCode = typeof err.code === "number" ? err.code : 1;
|
|
221
|
+
const output = stripAnsi(err.stdout ?? "");
|
|
222
|
+
const stderr = err.stderr ?? err.message ?? "";
|
|
223
|
+
if (output.includes("I'm sorry, but I cannot assist") || stderr.includes("Stream completed without a response")) {
|
|
224
|
+
throw createServicemeError2(
|
|
225
|
+
COPILOT_ERROR_CODES2.AUTH_REQUIRED,
|
|
226
|
+
"Copilot CLI returned a refusal response. This typically indicates an expired authentication token. Run `gh auth login` to re-authenticate.",
|
|
227
|
+
{ exitCode, output, stderr }
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (exitCode !== 0 && output) {
|
|
231
|
+
return { output, exitCode };
|
|
232
|
+
}
|
|
233
|
+
throw createServicemeError2(
|
|
234
|
+
COPILOT_ERROR_CODES2.EXECUTION_FAILED,
|
|
235
|
+
stderr || `Copilot exited with code ${String(exitCode)}.`,
|
|
236
|
+
{ exitCode, stderr }
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
122
241
|
// src/env/environmentInspector.ts
|
|
242
|
+
import {
|
|
243
|
+
createServicemeError as createServicemeError3,
|
|
244
|
+
KNOWN_ENVIRONMENT_TOOLS
|
|
245
|
+
} from "@serviceme/devtools-protocol";
|
|
123
246
|
var TOOL_CHECK_TIMEOUT_MS = 5e3;
|
|
124
247
|
var ERROR_CODE_NOT_FOUND = 127;
|
|
125
248
|
var EnvironmentInspector = class {
|
|
126
249
|
async checkEnvironment() {
|
|
127
250
|
const results = await Promise.all(
|
|
128
|
-
KNOWN_ENVIRONMENT_TOOLS.map(
|
|
251
|
+
KNOWN_ENVIRONMENT_TOOLS.map(
|
|
252
|
+
async (tool) => [tool, await this.checkTool(tool)]
|
|
253
|
+
)
|
|
129
254
|
);
|
|
130
255
|
return Object.fromEntries(results);
|
|
131
256
|
}
|
|
132
257
|
async checkTool(toolName) {
|
|
133
258
|
if (!/^[a-zA-Z0-9-]+$/.test(toolName)) {
|
|
134
|
-
throw
|
|
259
|
+
throw createServicemeError3("invalid_params", "Invalid tool name.");
|
|
135
260
|
}
|
|
136
261
|
try {
|
|
137
262
|
if (toolName === "nvm") {
|
|
@@ -289,9 +414,17 @@ var EnvironmentInspector = class {
|
|
|
289
414
|
import * as fs from "fs/promises";
|
|
290
415
|
import * as path from "path";
|
|
291
416
|
import {
|
|
292
|
-
createServicemeError as
|
|
417
|
+
createServicemeError as createServicemeError4
|
|
293
418
|
} from "@serviceme/devtools-protocol";
|
|
294
|
-
var SUPPORTED_FORMATS = [
|
|
419
|
+
var SUPPORTED_FORMATS = [
|
|
420
|
+
".jpg",
|
|
421
|
+
".jpeg",
|
|
422
|
+
".png",
|
|
423
|
+
".webp",
|
|
424
|
+
".gif",
|
|
425
|
+
".bmp",
|
|
426
|
+
".tiff"
|
|
427
|
+
];
|
|
295
428
|
var DEFAULT_MINIMUM_COMPRESSION_RATIO = 5;
|
|
296
429
|
var ImageTools = class {
|
|
297
430
|
getSupportedFormats() {
|
|
@@ -326,7 +459,10 @@ var ImageTools = class {
|
|
|
326
459
|
async compress(imagePath, options) {
|
|
327
460
|
const validation = await this.validate(imagePath, options.sharpModulePath);
|
|
328
461
|
if (!validation.valid) {
|
|
329
|
-
throw
|
|
462
|
+
throw createServicemeError4(
|
|
463
|
+
"invalid_params",
|
|
464
|
+
`Invalid image file: ${imagePath}`
|
|
465
|
+
);
|
|
330
466
|
}
|
|
331
467
|
const originalStats = await fs.stat(imagePath);
|
|
332
468
|
const originalSize = originalStats.size;
|
|
@@ -397,7 +533,7 @@ var ImageTools = class {
|
|
|
397
533
|
try {
|
|
398
534
|
return sharpModulePath ? __require(sharpModulePath) : __require("sharp");
|
|
399
535
|
} catch (error) {
|
|
400
|
-
throw
|
|
536
|
+
throw createServicemeError4(
|
|
401
537
|
"internal_error",
|
|
402
538
|
`sharp is required for image operations: ${error instanceof Error ? error.message : String(error)}`
|
|
403
539
|
);
|
|
@@ -409,12 +545,12 @@ function createImageTools() {
|
|
|
409
545
|
}
|
|
410
546
|
|
|
411
547
|
// src/json/jsonTools.ts
|
|
412
|
-
import { parse as parseCommentJson } from "comment-json";
|
|
413
|
-
import JSON5 from "json5";
|
|
414
548
|
import {
|
|
549
|
+
createServicemeError as createServicemeError5,
|
|
415
550
|
DEFAULT_JSON_SORT_OPTIONS
|
|
416
551
|
} from "@serviceme/devtools-protocol";
|
|
417
|
-
import {
|
|
552
|
+
import { parse as parseCommentJson } from "comment-json";
|
|
553
|
+
import JSON5 from "json5";
|
|
418
554
|
function createJsonTools() {
|
|
419
555
|
return {
|
|
420
556
|
sort(text, options) {
|
|
@@ -454,7 +590,7 @@ function parseJson(text) {
|
|
|
454
590
|
} catch {
|
|
455
591
|
}
|
|
456
592
|
}
|
|
457
|
-
throw
|
|
593
|
+
throw createServicemeError5("json_invalid_input", "Invalid JSON format.");
|
|
458
594
|
}
|
|
459
595
|
function sortObject(obj, options) {
|
|
460
596
|
if (Array.isArray(obj)) {
|
|
@@ -480,9 +616,6 @@ function sortKeys(keys, options) {
|
|
|
480
616
|
case "alphaNum":
|
|
481
617
|
compareFn = (a, b) => a.localeCompare(b, void 0, { numeric: true });
|
|
482
618
|
break;
|
|
483
|
-
case "values":
|
|
484
|
-
case "type":
|
|
485
|
-
case "default":
|
|
486
619
|
default:
|
|
487
620
|
compareFn = (a, b) => a.localeCompare(b);
|
|
488
621
|
break;
|
|
@@ -526,8 +659,10 @@ function formatArgs(args) {
|
|
|
526
659
|
function createConsoleLogger(prefix = "serviceme") {
|
|
527
660
|
return {
|
|
528
661
|
debug(message, ...args) {
|
|
529
|
-
process.stderr.write(
|
|
530
|
-
`)
|
|
662
|
+
process.stderr.write(
|
|
663
|
+
`[${prefix}] DEBUG ${message} ${formatArgs(args)}
|
|
664
|
+
`
|
|
665
|
+
);
|
|
531
666
|
},
|
|
532
667
|
info(message, ...args) {
|
|
533
668
|
process.stderr.write(`[${prefix}] INFO ${message} ${formatArgs(args)}
|
|
@@ -538,404 +673,19 @@ function createConsoleLogger(prefix = "serviceme") {
|
|
|
538
673
|
`);
|
|
539
674
|
},
|
|
540
675
|
error(message, ...args) {
|
|
541
|
-
process.stderr.write(
|
|
542
|
-
`)
|
|
676
|
+
process.stderr.write(
|
|
677
|
+
`[${prefix}] ERROR ${message} ${formatArgs(args)}
|
|
678
|
+
`
|
|
679
|
+
);
|
|
543
680
|
}
|
|
544
681
|
};
|
|
545
682
|
}
|
|
546
683
|
|
|
547
|
-
// src/opencode/OpenCodeManager.ts
|
|
548
|
-
import { spawn as spawn2 } from "child_process";
|
|
549
|
-
import { randomInt } from "crypto";
|
|
550
|
-
import {
|
|
551
|
-
createServicemeError as createServicemeError4
|
|
552
|
-
} from "@serviceme/devtools-protocol";
|
|
553
|
-
var DEFAULT_OPTIONS = {
|
|
554
|
-
command: "opencode",
|
|
555
|
-
installUrl: "https://opencode.ai",
|
|
556
|
-
host: "127.0.0.1",
|
|
557
|
-
portMin: 16384,
|
|
558
|
-
portMax: 65535,
|
|
559
|
-
healthPath: "/app",
|
|
560
|
-
sessionPath: "/session",
|
|
561
|
-
sessionStatusPath: "/session/status",
|
|
562
|
-
startupPollIntervalMs: 200,
|
|
563
|
-
idleTimeoutMs: 10 * 60 * 1e3,
|
|
564
|
-
stdoutLogLimitBytes: 64 * 1024
|
|
565
|
-
};
|
|
566
|
-
var OpenCodeManager = class {
|
|
567
|
-
constructor(options = {}) {
|
|
568
|
-
this.stdoutBytes = 0;
|
|
569
|
-
this.options = {
|
|
570
|
-
...DEFAULT_OPTIONS,
|
|
571
|
-
...options
|
|
572
|
-
};
|
|
573
|
-
this.logger = options.logger ?? noopLogger;
|
|
574
|
-
}
|
|
575
|
-
async isAvailable() {
|
|
576
|
-
return commandExists(this.options.command);
|
|
577
|
-
}
|
|
578
|
-
isRunning() {
|
|
579
|
-
return Boolean(this.childProcess && !this.childProcess.killed && this.port);
|
|
580
|
-
}
|
|
581
|
-
async ensureServer(runtime = {}) {
|
|
582
|
-
await this.ensureServerStarted(runtime);
|
|
583
|
-
return this.getServerState();
|
|
584
|
-
}
|
|
585
|
-
async getServerState() {
|
|
586
|
-
const available = await this.isAvailable();
|
|
587
|
-
const running = this.isRunning();
|
|
588
|
-
const healthy = this.port ? await this.isServerHealthy(this.port) : false;
|
|
589
|
-
return {
|
|
590
|
-
available,
|
|
591
|
-
running,
|
|
592
|
-
healthy,
|
|
593
|
-
installUrl: this.options.installUrl,
|
|
594
|
-
pid: this.childProcess?.pid,
|
|
595
|
-
port: this.port
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
async createSession(runtime = {}) {
|
|
599
|
-
await this.ensureServerStarted(runtime);
|
|
600
|
-
const session = await this.request(
|
|
601
|
-
this.options.sessionPath,
|
|
602
|
-
{
|
|
603
|
-
method: "POST",
|
|
604
|
-
query: this.createQuery(runtime.workspaceRoot),
|
|
605
|
-
body: {}
|
|
606
|
-
}
|
|
607
|
-
);
|
|
608
|
-
this.resetIdleTimer();
|
|
609
|
-
return {
|
|
610
|
-
sessionId: session.id,
|
|
611
|
-
title: session.title,
|
|
612
|
-
status: "running"
|
|
613
|
-
};
|
|
614
|
-
}
|
|
615
|
-
async prompt(input) {
|
|
616
|
-
await this.ensureServerStarted({ workspaceRoot: input.workspaceRoot });
|
|
617
|
-
await this.request(
|
|
618
|
-
`${this.options.sessionPath}/${input.sessionId}/message`,
|
|
619
|
-
{
|
|
620
|
-
method: "POST",
|
|
621
|
-
query: this.createQuery(input.workspaceRoot),
|
|
622
|
-
body: {
|
|
623
|
-
parts: [
|
|
624
|
-
{
|
|
625
|
-
type: "text",
|
|
626
|
-
text: input.prompt
|
|
627
|
-
}
|
|
628
|
-
]
|
|
629
|
-
},
|
|
630
|
-
signal: input.signal
|
|
631
|
-
}
|
|
632
|
-
);
|
|
633
|
-
const latestMessage = await this.getLatestAssistantMessage(
|
|
634
|
-
input.sessionId,
|
|
635
|
-
input.workspaceRoot,
|
|
636
|
-
input.signal
|
|
637
|
-
);
|
|
638
|
-
input.onEvent?.({
|
|
639
|
-
type: "status",
|
|
640
|
-
status: "running"
|
|
641
|
-
});
|
|
642
|
-
if (latestMessage) {
|
|
643
|
-
input.onEvent?.({
|
|
644
|
-
type: "message",
|
|
645
|
-
text: latestMessage
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
input.onEvent?.({
|
|
649
|
-
type: "completed"
|
|
650
|
-
});
|
|
651
|
-
this.resetIdleTimer();
|
|
652
|
-
}
|
|
653
|
-
async getSessionStatus(sessionId, workspaceRoot) {
|
|
654
|
-
await this.ensureServerStarted({ workspaceRoot });
|
|
655
|
-
const response = await this.request(this.options.sessionStatusPath, {
|
|
656
|
-
method: "GET",
|
|
657
|
-
query: this.createQuery(workspaceRoot)
|
|
658
|
-
});
|
|
659
|
-
const session = response.find((item) => item.id === sessionId);
|
|
660
|
-
this.resetIdleTimer();
|
|
661
|
-
return this.mapStatus(session?.status);
|
|
662
|
-
}
|
|
663
|
-
async abortSession(sessionId, workspaceRoot) {
|
|
664
|
-
await this.ensureServerStarted({ workspaceRoot });
|
|
665
|
-
await this.request(`${this.options.sessionPath}/${sessionId}/abort`, {
|
|
666
|
-
method: "POST",
|
|
667
|
-
query: this.createQuery(workspaceRoot)
|
|
668
|
-
});
|
|
669
|
-
this.resetIdleTimer();
|
|
670
|
-
}
|
|
671
|
-
async dispose() {
|
|
672
|
-
if (this.idleTimer) {
|
|
673
|
-
clearTimeout(this.idleTimer);
|
|
674
|
-
this.idleTimer = void 0;
|
|
675
|
-
}
|
|
676
|
-
await this.terminateTrackedProcess("dispose");
|
|
677
|
-
this.startupPromise = void 0;
|
|
678
|
-
}
|
|
679
|
-
async ensureServerStarted(runtime) {
|
|
680
|
-
if (this.port) {
|
|
681
|
-
if (await this.isServerHealthy(this.port)) {
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
await this.terminateTrackedProcess("restart-unhealthy");
|
|
685
|
-
}
|
|
686
|
-
if (this.startupPromise) {
|
|
687
|
-
await this.startupPromise;
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
if (!await this.isAvailable()) {
|
|
691
|
-
throw createServicemeError4(
|
|
692
|
-
"opencode_not_installed",
|
|
693
|
-
"OpenCode CLI is not installed."
|
|
694
|
-
);
|
|
695
|
-
}
|
|
696
|
-
const MAX_PORT_RETRIES = 3;
|
|
697
|
-
let lastError;
|
|
698
|
-
for (let attempt = 0; attempt < MAX_PORT_RETRIES; attempt++) {
|
|
699
|
-
this.startupPromise = this.startServerProcess(runtime);
|
|
700
|
-
try {
|
|
701
|
-
await this.startupPromise;
|
|
702
|
-
return;
|
|
703
|
-
} catch (error) {
|
|
704
|
-
lastError = error;
|
|
705
|
-
this.logger.warn(
|
|
706
|
-
`OpenCode server start attempt ${String(attempt + 1)} failed, retrying...`
|
|
707
|
-
);
|
|
708
|
-
} finally {
|
|
709
|
-
this.startupPromise = void 0;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
throw lastError;
|
|
713
|
-
}
|
|
714
|
-
async startServerProcess(runtime) {
|
|
715
|
-
const port = randomInt(this.options.portMin, this.options.portMax + 1);
|
|
716
|
-
const child = spawn2(this.options.command, ["--port", String(port)], {
|
|
717
|
-
cwd: runtime.workspaceRoot,
|
|
718
|
-
detached: process.platform !== "win32",
|
|
719
|
-
env: {
|
|
720
|
-
...process.env,
|
|
721
|
-
OPENCODE_CALLER: "serviceme-cli"
|
|
722
|
-
},
|
|
723
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
724
|
-
});
|
|
725
|
-
this.stdoutBytes = 0;
|
|
726
|
-
child.stdout?.on("data", (chunk) => {
|
|
727
|
-
if (this.stdoutBytes < this.options.stdoutLogLimitBytes) {
|
|
728
|
-
this.logger.debug("OpenCode stdout", chunk.toString());
|
|
729
|
-
this.stdoutBytes += chunk.length;
|
|
730
|
-
}
|
|
731
|
-
});
|
|
732
|
-
child.stderr?.on("data", (chunk) => {
|
|
733
|
-
this.logger.warn("OpenCode stderr", chunk.toString());
|
|
734
|
-
});
|
|
735
|
-
child.on("error", (error) => {
|
|
736
|
-
this.logger.error("OpenCode process error", error);
|
|
737
|
-
if (this.childProcess === child) {
|
|
738
|
-
this.childProcess = void 0;
|
|
739
|
-
this.port = void 0;
|
|
740
|
-
}
|
|
741
|
-
});
|
|
742
|
-
child.on("exit", () => {
|
|
743
|
-
if (this.childProcess === child) {
|
|
744
|
-
this.childProcess = void 0;
|
|
745
|
-
this.port = void 0;
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
if (process.platform !== "win32" && child.pid) {
|
|
749
|
-
spawn2("renice", ["+10", String(child.pid)], { stdio: "ignore" }).unref();
|
|
750
|
-
}
|
|
751
|
-
this.childProcess = child;
|
|
752
|
-
this.port = port;
|
|
753
|
-
const ready = await this.waitForHealth(
|
|
754
|
-
port,
|
|
755
|
-
runtime.startupTimeoutMs ?? 5e3
|
|
756
|
-
);
|
|
757
|
-
if (!ready) {
|
|
758
|
-
await this.terminateTrackedProcess("startup-timeout");
|
|
759
|
-
throw createServicemeError4(
|
|
760
|
-
"opencode_startup_timeout",
|
|
761
|
-
"OpenCode local server did not start in time."
|
|
762
|
-
);
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
async waitForHealth(port, timeoutMs) {
|
|
766
|
-
const startedAt = Date.now();
|
|
767
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
768
|
-
if (await this.isServerHealthy(port)) {
|
|
769
|
-
return true;
|
|
770
|
-
}
|
|
771
|
-
await new Promise((resolve) => {
|
|
772
|
-
setTimeout(resolve, this.options.startupPollIntervalMs);
|
|
773
|
-
});
|
|
774
|
-
}
|
|
775
|
-
return false;
|
|
776
|
-
}
|
|
777
|
-
async isServerHealthy(port) {
|
|
778
|
-
try {
|
|
779
|
-
const response = await fetch(
|
|
780
|
-
`http://${this.options.host}:${port}${this.options.healthPath}`
|
|
781
|
-
);
|
|
782
|
-
return response.ok;
|
|
783
|
-
} catch {
|
|
784
|
-
return false;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
resetIdleTimer() {
|
|
788
|
-
if (this.idleTimer) {
|
|
789
|
-
clearTimeout(this.idleTimer);
|
|
790
|
-
}
|
|
791
|
-
this.idleTimer = setTimeout(() => {
|
|
792
|
-
void this.dispose();
|
|
793
|
-
}, this.options.idleTimeoutMs);
|
|
794
|
-
this.idleTimer.unref?.();
|
|
795
|
-
}
|
|
796
|
-
async terminateTrackedProcess(reason) {
|
|
797
|
-
const child = this.childProcess;
|
|
798
|
-
this.childProcess = void 0;
|
|
799
|
-
this.port = void 0;
|
|
800
|
-
if (!child || child.killed) {
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
this.logger.info("Stopping tracked OpenCode process", {
|
|
804
|
-
reason,
|
|
805
|
-
pid: child.pid
|
|
806
|
-
});
|
|
807
|
-
if (process.platform === "win32") {
|
|
808
|
-
if (child.pid) {
|
|
809
|
-
await new Promise((resolve) => {
|
|
810
|
-
const killProcess = spawn2(
|
|
811
|
-
"taskkill",
|
|
812
|
-
["/pid", String(child.pid), "/T", "/F"],
|
|
813
|
-
{
|
|
814
|
-
stdio: "ignore",
|
|
815
|
-
windowsHide: true
|
|
816
|
-
}
|
|
817
|
-
);
|
|
818
|
-
killProcess.once("exit", () => resolve());
|
|
819
|
-
killProcess.once("error", () => {
|
|
820
|
-
child.kill();
|
|
821
|
-
resolve();
|
|
822
|
-
});
|
|
823
|
-
});
|
|
824
|
-
return;
|
|
825
|
-
}
|
|
826
|
-
child.kill();
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
if (!child.pid) {
|
|
830
|
-
child.kill("SIGTERM");
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
try {
|
|
834
|
-
process.kill(-child.pid, "SIGTERM");
|
|
835
|
-
} catch {
|
|
836
|
-
child.kill("SIGTERM");
|
|
837
|
-
return;
|
|
838
|
-
}
|
|
839
|
-
const childPid = child.pid;
|
|
840
|
-
await new Promise((resolve) => {
|
|
841
|
-
const escalationTimer = setTimeout(() => {
|
|
842
|
-
try {
|
|
843
|
-
process.kill(-childPid, "SIGKILL");
|
|
844
|
-
} catch {
|
|
845
|
-
}
|
|
846
|
-
resolve();
|
|
847
|
-
}, 2e3);
|
|
848
|
-
child.once("exit", () => {
|
|
849
|
-
clearTimeout(escalationTimer);
|
|
850
|
-
try {
|
|
851
|
-
process.kill(-childPid, "SIGKILL");
|
|
852
|
-
} catch {
|
|
853
|
-
}
|
|
854
|
-
resolve();
|
|
855
|
-
});
|
|
856
|
-
});
|
|
857
|
-
}
|
|
858
|
-
async request(path3, options) {
|
|
859
|
-
if (!this.port) {
|
|
860
|
-
throw createServicemeError4(
|
|
861
|
-
"opencode_backend_not_running",
|
|
862
|
-
"OpenCode local server is not running."
|
|
863
|
-
);
|
|
864
|
-
}
|
|
865
|
-
const url = new URL(`http://${this.options.host}:${this.port}${path3}`);
|
|
866
|
-
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
867
|
-
if (value) {
|
|
868
|
-
url.searchParams.set(key, value);
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
const response = await fetch(url, {
|
|
872
|
-
method: options.method,
|
|
873
|
-
headers: {
|
|
874
|
-
"Content-Type": "application/json"
|
|
875
|
-
},
|
|
876
|
-
body: options.body ? JSON.stringify(options.body) : void 0,
|
|
877
|
-
signal: options.signal
|
|
878
|
-
});
|
|
879
|
-
if (!response.ok) {
|
|
880
|
-
throw createServicemeError4(
|
|
881
|
-
"opencode_request_failed",
|
|
882
|
-
`OpenCode request failed: ${response.status} ${response.statusText}`
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
if (response.status === 204) {
|
|
886
|
-
return void 0;
|
|
887
|
-
}
|
|
888
|
-
return await response.json();
|
|
889
|
-
}
|
|
890
|
-
createQuery(workspaceRoot) {
|
|
891
|
-
return {
|
|
892
|
-
directory: workspaceRoot
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
async getLatestAssistantMessage(sessionId, workspaceRoot, signal) {
|
|
896
|
-
const messages = await this.request(
|
|
897
|
-
`${this.options.sessionPath}/${sessionId}/message`,
|
|
898
|
-
{
|
|
899
|
-
method: "GET",
|
|
900
|
-
query: {
|
|
901
|
-
...this.createQuery(workspaceRoot),
|
|
902
|
-
limit: "20"
|
|
903
|
-
},
|
|
904
|
-
signal
|
|
905
|
-
}
|
|
906
|
-
);
|
|
907
|
-
const latestAssistant = [...messages].reverse().find((message) => message.info?.role === "assistant");
|
|
908
|
-
if (!latestAssistant?.parts) {
|
|
909
|
-
return void 0;
|
|
910
|
-
}
|
|
911
|
-
return latestAssistant.parts.filter(
|
|
912
|
-
(part) => part.type === "text" && typeof part.text === "string"
|
|
913
|
-
).map((part) => part.text).join("\n\n");
|
|
914
|
-
}
|
|
915
|
-
mapStatus(status) {
|
|
916
|
-
switch (status) {
|
|
917
|
-
case "running":
|
|
918
|
-
case "active":
|
|
919
|
-
return "running";
|
|
920
|
-
case "needs_input":
|
|
921
|
-
return "needs-input";
|
|
922
|
-
case "completed":
|
|
923
|
-
return "completed";
|
|
924
|
-
case "failed":
|
|
925
|
-
return "failed";
|
|
926
|
-
case "aborted":
|
|
927
|
-
return "aborted";
|
|
928
|
-
default:
|
|
929
|
-
return "idle";
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
};
|
|
933
|
-
|
|
934
684
|
// src/project/projectTools.ts
|
|
935
685
|
import * as fs2 from "fs/promises";
|
|
936
686
|
import * as path2 from "path";
|
|
937
687
|
import {
|
|
938
|
-
createServicemeError as
|
|
688
|
+
createServicemeError as createServicemeError6
|
|
939
689
|
} from "@serviceme/devtools-protocol";
|
|
940
690
|
|
|
941
691
|
// src/utils/fileUtils.ts
|
|
@@ -953,39 +703,49 @@ import { dirname as dirname2, join as join2 } from "path";
|
|
|
953
703
|
import { open } from "yauzl";
|
|
954
704
|
var unzipFile = (zipPath, dest) => {
|
|
955
705
|
return new Promise((resolve, reject) => {
|
|
956
|
-
open(
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
zipfile
|
|
960
|
-
|
|
961
|
-
if (
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
706
|
+
open(
|
|
707
|
+
zipPath,
|
|
708
|
+
{ lazyEntries: true },
|
|
709
|
+
(err, zipfile) => {
|
|
710
|
+
if (err) return reject(err);
|
|
711
|
+
if (!zipfile) return reject(new Error("Failed to open zip file."));
|
|
712
|
+
zipfile.readEntry();
|
|
713
|
+
zipfile.on("entry", (entry) => {
|
|
714
|
+
if (/\/$/.test(entry.fileName)) {
|
|
715
|
+
void mkdir(join2(dest, entry.fileName), { recursive: true }).then(() => {
|
|
716
|
+
zipfile.readEntry();
|
|
717
|
+
}).catch(reject);
|
|
718
|
+
} else {
|
|
719
|
+
const outputPath = join2(dest, entry.fileName);
|
|
720
|
+
void mkdir(dirname2(outputPath), { recursive: true }).then(() => {
|
|
721
|
+
zipfile.openReadStream(
|
|
722
|
+
entry,
|
|
723
|
+
(streamError, readStream) => {
|
|
724
|
+
if (streamError) return reject(streamError);
|
|
725
|
+
if (!readStream)
|
|
726
|
+
return reject(
|
|
727
|
+
new Error("Failed to open zip entry stream.")
|
|
728
|
+
);
|
|
729
|
+
const writeStream = createWriteStream(outputPath);
|
|
730
|
+
readStream.on("error", reject);
|
|
731
|
+
writeStream.on("error", reject);
|
|
732
|
+
writeStream.on("close", () => {
|
|
733
|
+
zipfile.readEntry();
|
|
734
|
+
});
|
|
735
|
+
readStream.pipe(writeStream);
|
|
736
|
+
}
|
|
737
|
+
);
|
|
738
|
+
}).catch(reject);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
zipfile.on("end", () => {
|
|
742
|
+
resolve();
|
|
743
|
+
});
|
|
744
|
+
zipfile.on("error", (zipError) => {
|
|
745
|
+
reject(zipError);
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
);
|
|
989
749
|
});
|
|
990
750
|
};
|
|
991
751
|
var tryLstat = async (targetPath) => {
|
|
@@ -1009,7 +769,11 @@ var mergeEntry = async (sourcePath, destPath, overwrite) => {
|
|
|
1009
769
|
await mkdir(destPath, { recursive: true });
|
|
1010
770
|
const children = await readdir(sourcePath);
|
|
1011
771
|
for (const child of children) {
|
|
1012
|
-
await mergeEntry(
|
|
772
|
+
await mergeEntry(
|
|
773
|
+
join2(sourcePath, child),
|
|
774
|
+
join2(destPath, child),
|
|
775
|
+
overwrite
|
|
776
|
+
);
|
|
1013
777
|
}
|
|
1014
778
|
await rm(sourcePath, { recursive: true, force: true });
|
|
1015
779
|
return;
|
|
@@ -1086,7 +850,7 @@ var ProjectTools = class {
|
|
|
1086
850
|
}
|
|
1087
851
|
async installDependencies(workspacePath, command) {
|
|
1088
852
|
if (!command) {
|
|
1089
|
-
throw
|
|
853
|
+
throw createServicemeError6("invalid_params", "Expected install command.");
|
|
1090
854
|
}
|
|
1091
855
|
await runCommand(command, {
|
|
1092
856
|
cwd: workspacePath,
|
|
@@ -1149,7 +913,7 @@ var ProjectTools = class {
|
|
|
1149
913
|
}
|
|
1150
914
|
async runScaffoldPrune(workspacePath, preset) {
|
|
1151
915
|
if (!preset) {
|
|
1152
|
-
throw
|
|
916
|
+
throw createServicemeError6("invalid_params", "Expected scaffold preset.");
|
|
1153
917
|
}
|
|
1154
918
|
const commands = [
|
|
1155
919
|
`pnpm run scaffold:prune -- --preset ${preset} --project-root .`,
|
|
@@ -1229,16 +993,971 @@ var ProjectTools = class {
|
|
|
1229
993
|
function createProjectTools() {
|
|
1230
994
|
return new ProjectTools();
|
|
1231
995
|
}
|
|
996
|
+
|
|
997
|
+
// src/scheduled-tasks/daemon/DaemonLogger.ts
|
|
998
|
+
import * as fs3 from "fs";
|
|
999
|
+
import * as path3 from "path";
|
|
1000
|
+
var CONFIG_DIR = ".serviceme";
|
|
1001
|
+
var LOG_FILE = "scheduler.log";
|
|
1002
|
+
var MAX_LOG_SIZE = 1024 * 1024;
|
|
1003
|
+
var DaemonLogger = class {
|
|
1004
|
+
constructor(workspacePath) {
|
|
1005
|
+
this.logPath = path3.join(workspacePath, CONFIG_DIR, LOG_FILE);
|
|
1006
|
+
const dir = path3.dirname(this.logPath);
|
|
1007
|
+
if (!fs3.existsSync(dir)) {
|
|
1008
|
+
fs3.mkdirSync(dir, { recursive: true });
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
getLogPath() {
|
|
1012
|
+
return this.logPath;
|
|
1013
|
+
}
|
|
1014
|
+
log(level, message) {
|
|
1015
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
1016
|
+
const line = `[${ts}] [${level.toUpperCase()}] ${message}
|
|
1017
|
+
`;
|
|
1018
|
+
this.rotateIfNeeded();
|
|
1019
|
+
fs3.appendFileSync(this.logPath, line, "utf-8");
|
|
1020
|
+
}
|
|
1021
|
+
rotateIfNeeded() {
|
|
1022
|
+
try {
|
|
1023
|
+
const stats = fs3.statSync(this.logPath);
|
|
1024
|
+
if (stats.size > MAX_LOG_SIZE) {
|
|
1025
|
+
const content = fs3.readFileSync(this.logPath, "utf-8");
|
|
1026
|
+
const halfIdx = content.indexOf("\n", Math.floor(content.length / 2));
|
|
1027
|
+
if (halfIdx > 0) {
|
|
1028
|
+
fs3.writeFileSync(this.logPath, content.slice(halfIdx + 1), "utf-8");
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
} catch {
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
// src/scheduled-tasks/daemon/PidManager.ts
|
|
1037
|
+
import * as fs4 from "fs";
|
|
1038
|
+
import * as path4 from "path";
|
|
1039
|
+
var CONFIG_DIR2 = ".serviceme";
|
|
1040
|
+
var PID_FILE = "scheduler.pid";
|
|
1041
|
+
var PidManager = class {
|
|
1042
|
+
constructor(workspacePath) {
|
|
1043
|
+
this.pidPath = path4.join(workspacePath, CONFIG_DIR2, PID_FILE);
|
|
1044
|
+
}
|
|
1045
|
+
getPidPath() {
|
|
1046
|
+
return this.pidPath;
|
|
1047
|
+
}
|
|
1048
|
+
writePid(pid) {
|
|
1049
|
+
const dir = path4.dirname(this.pidPath);
|
|
1050
|
+
if (!fs4.existsSync(dir)) {
|
|
1051
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
1052
|
+
}
|
|
1053
|
+
fs4.writeFileSync(this.pidPath, String(pid), "utf-8");
|
|
1054
|
+
}
|
|
1055
|
+
readPid() {
|
|
1056
|
+
if (!fs4.existsSync(this.pidPath)) return null;
|
|
1057
|
+
const raw = fs4.readFileSync(this.pidPath, "utf-8").trim();
|
|
1058
|
+
const pid = Number.parseInt(raw, 10);
|
|
1059
|
+
return Number.isNaN(pid) ? null : pid;
|
|
1060
|
+
}
|
|
1061
|
+
removePid() {
|
|
1062
|
+
if (fs4.existsSync(this.pidPath)) {
|
|
1063
|
+
fs4.unlinkSync(this.pidPath);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
isProcessRunning(pid) {
|
|
1067
|
+
try {
|
|
1068
|
+
process.kill(pid, 0);
|
|
1069
|
+
return true;
|
|
1070
|
+
} catch {
|
|
1071
|
+
return false;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
getRunningPid() {
|
|
1075
|
+
const pid = this.readPid();
|
|
1076
|
+
if (pid === null) return null;
|
|
1077
|
+
if (this.isProcessRunning(pid)) return pid;
|
|
1078
|
+
this.removePid();
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
};
|
|
1082
|
+
|
|
1083
|
+
// src/scheduled-tasks/daemon/SchedulerDaemon.ts
|
|
1084
|
+
import * as fs7 from "fs";
|
|
1085
|
+
|
|
1086
|
+
// src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
|
|
1087
|
+
import { spawn as spawn2 } from "child_process";
|
|
1088
|
+
var DEFAULT_TIMEOUT = 3e5;
|
|
1089
|
+
var MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
1090
|
+
var GithubCopilotCliExecutor = class {
|
|
1091
|
+
async execute(payload, abortSignal) {
|
|
1092
|
+
const authenticated = await isCopilotAuthenticated();
|
|
1093
|
+
if (!authenticated) {
|
|
1094
|
+
return {
|
|
1095
|
+
status: "failure",
|
|
1096
|
+
error: "GitHub Copilot CLI authentication failed. Please run `gh auth login` to re-authenticate."
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1099
|
+
let output = "";
|
|
1100
|
+
const handle = this.executeStreaming(
|
|
1101
|
+
payload,
|
|
1102
|
+
(_stream, data) => {
|
|
1103
|
+
output += data;
|
|
1104
|
+
},
|
|
1105
|
+
abortSignal
|
|
1106
|
+
);
|
|
1107
|
+
const result = await handle.result;
|
|
1108
|
+
return { ...result, output: output || result.output };
|
|
1109
|
+
}
|
|
1110
|
+
executeStreaming(payload, onOutput, abortSignal) {
|
|
1111
|
+
const p = payload;
|
|
1112
|
+
const timeout = (p.timeout != null ? p.timeout * 1e3 : null) ?? DEFAULT_TIMEOUT;
|
|
1113
|
+
let resolve;
|
|
1114
|
+
const resultPromise = new Promise((r) => {
|
|
1115
|
+
resolve = r;
|
|
1116
|
+
});
|
|
1117
|
+
let settled = false;
|
|
1118
|
+
const settle = (result) => {
|
|
1119
|
+
if (settled) return;
|
|
1120
|
+
settled = true;
|
|
1121
|
+
clearTimeout(timer);
|
|
1122
|
+
resolve?.(result);
|
|
1123
|
+
};
|
|
1124
|
+
if (abortSignal?.aborted) {
|
|
1125
|
+
return {
|
|
1126
|
+
result: Promise.resolve({
|
|
1127
|
+
status: "cancelled",
|
|
1128
|
+
error: "Execution aborted"
|
|
1129
|
+
}),
|
|
1130
|
+
cancel: () => {
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
const args = ["copilot", "prompt", "--prompt", p.prompt];
|
|
1135
|
+
if (p.autopilot) {
|
|
1136
|
+
args.push("--autopilot");
|
|
1137
|
+
}
|
|
1138
|
+
if (p.allowTools && p.allowTools.length > 0) {
|
|
1139
|
+
args.push("--allow-tools", p.allowTools.join(","));
|
|
1140
|
+
}
|
|
1141
|
+
if (p.model) {
|
|
1142
|
+
args.push("--model", p.model);
|
|
1143
|
+
}
|
|
1144
|
+
if (p.agent) {
|
|
1145
|
+
args.push("--agent", p.agent);
|
|
1146
|
+
}
|
|
1147
|
+
if (p.timeout != null) {
|
|
1148
|
+
args.push("--timeout", String(p.timeout));
|
|
1149
|
+
}
|
|
1150
|
+
const child = spawn2("serviceme", args, {
|
|
1151
|
+
cwd: p.workspace,
|
|
1152
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1153
|
+
});
|
|
1154
|
+
const timer = setTimeout(() => {
|
|
1155
|
+
child.kill("SIGTERM");
|
|
1156
|
+
setTimeout(() => {
|
|
1157
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
1158
|
+
}, 5e3);
|
|
1159
|
+
settle({
|
|
1160
|
+
status: "timeout",
|
|
1161
|
+
error: `Copilot CLI execution timed out after ${timeout / 1e3}s`
|
|
1162
|
+
});
|
|
1163
|
+
}, timeout);
|
|
1164
|
+
let stdoutBuf = "";
|
|
1165
|
+
let stderrBuf = "";
|
|
1166
|
+
child.stdout.on("data", (chunk) => {
|
|
1167
|
+
const data = chunk.toString();
|
|
1168
|
+
stdoutBuf += data;
|
|
1169
|
+
if (stdoutBuf.length > MAX_OUTPUT_BYTES)
|
|
1170
|
+
stdoutBuf = stdoutBuf.slice(-MAX_OUTPUT_BYTES);
|
|
1171
|
+
onOutput("stdout", data);
|
|
1172
|
+
});
|
|
1173
|
+
child.stderr.on("data", (chunk) => {
|
|
1174
|
+
const data = chunk.toString();
|
|
1175
|
+
stderrBuf += data;
|
|
1176
|
+
if (stderrBuf.length > MAX_OUTPUT_BYTES)
|
|
1177
|
+
stderrBuf = stderrBuf.slice(-MAX_OUTPUT_BYTES);
|
|
1178
|
+
onOutput("stderr", data);
|
|
1179
|
+
});
|
|
1180
|
+
child.on("close", (code) => {
|
|
1181
|
+
if (code === 0) {
|
|
1182
|
+
settle({ status: "success", output: stdoutBuf || void 0 });
|
|
1183
|
+
} else {
|
|
1184
|
+
settle({
|
|
1185
|
+
status: "failure",
|
|
1186
|
+
output: stdoutBuf || void 0,
|
|
1187
|
+
error: stderrBuf || `Process exited with code ${code}`
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
child.on("error", (err) => {
|
|
1192
|
+
settle({ status: "failure", error: err.message });
|
|
1193
|
+
});
|
|
1194
|
+
const cancelFn = () => {
|
|
1195
|
+
child.kill("SIGTERM");
|
|
1196
|
+
setTimeout(() => {
|
|
1197
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
1198
|
+
}, 5e3);
|
|
1199
|
+
settle({ status: "cancelled", error: "Execution cancelled" });
|
|
1200
|
+
};
|
|
1201
|
+
abortSignal?.addEventListener("abort", () => cancelFn(), { once: true });
|
|
1202
|
+
return { result: resultPromise, cancel: cancelFn };
|
|
1203
|
+
}
|
|
1204
|
+
};
|
|
1205
|
+
|
|
1206
|
+
// src/scheduled-tasks/executors/HttpRequestExecutor.ts
|
|
1207
|
+
var DEFAULT_TIMEOUT2 = 3e4;
|
|
1208
|
+
var HttpRequestExecutor = class {
|
|
1209
|
+
async execute(payload, abortSignal) {
|
|
1210
|
+
const p = payload;
|
|
1211
|
+
const timeout = (p.timeout != null ? p.timeout * 1e3 : null) ?? DEFAULT_TIMEOUT2;
|
|
1212
|
+
const ac = new AbortController();
|
|
1213
|
+
let _timedOut = false;
|
|
1214
|
+
const timer = setTimeout(() => {
|
|
1215
|
+
_timedOut = true;
|
|
1216
|
+
ac.abort();
|
|
1217
|
+
}, timeout);
|
|
1218
|
+
if (abortSignal?.aborted) {
|
|
1219
|
+
clearTimeout(timer);
|
|
1220
|
+
return { status: "cancelled", error: "Execution aborted" };
|
|
1221
|
+
}
|
|
1222
|
+
let externalAbort = false;
|
|
1223
|
+
abortSignal?.addEventListener(
|
|
1224
|
+
"abort",
|
|
1225
|
+
() => {
|
|
1226
|
+
externalAbort = true;
|
|
1227
|
+
clearTimeout(timer);
|
|
1228
|
+
ac.abort();
|
|
1229
|
+
},
|
|
1230
|
+
{ once: true }
|
|
1231
|
+
);
|
|
1232
|
+
try {
|
|
1233
|
+
const response = await fetch(p.url, {
|
|
1234
|
+
method: p.method,
|
|
1235
|
+
headers: p.headers,
|
|
1236
|
+
body: p.body,
|
|
1237
|
+
signal: ac.signal
|
|
1238
|
+
});
|
|
1239
|
+
clearTimeout(timer);
|
|
1240
|
+
const body = await response.text();
|
|
1241
|
+
if (response.ok) {
|
|
1242
|
+
return {
|
|
1243
|
+
status: "success",
|
|
1244
|
+
output: `${response.status} ${response.statusText}
|
|
1245
|
+
${body}`.trim()
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
return {
|
|
1249
|
+
status: "failure",
|
|
1250
|
+
error: `HTTP ${response.status} ${response.statusText}
|
|
1251
|
+
${body}`.trim()
|
|
1252
|
+
};
|
|
1253
|
+
} catch (err) {
|
|
1254
|
+
clearTimeout(timer);
|
|
1255
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1256
|
+
if (externalAbort) {
|
|
1257
|
+
return { status: "cancelled", error: "Execution cancelled" };
|
|
1258
|
+
}
|
|
1259
|
+
return {
|
|
1260
|
+
status: "timeout",
|
|
1261
|
+
error: `HTTP request timed out after ${timeout / 1e3}s`
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1265
|
+
return { status: "failure", error: message };
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
// src/scheduled-tasks/executors/ShellExecutor.ts
|
|
1271
|
+
import { spawn as spawn3 } from "child_process";
|
|
1272
|
+
var DEFAULT_TIMEOUT3 = 6e4;
|
|
1273
|
+
var MAX_OUTPUT_BYTES2 = 1024 * 1024;
|
|
1274
|
+
var ShellExecutor = class {
|
|
1275
|
+
async execute(payload, abortSignal) {
|
|
1276
|
+
let output = "";
|
|
1277
|
+
const handle = this.executeStreaming(
|
|
1278
|
+
payload,
|
|
1279
|
+
(_stream, data) => {
|
|
1280
|
+
output += data;
|
|
1281
|
+
},
|
|
1282
|
+
abortSignal
|
|
1283
|
+
);
|
|
1284
|
+
const result = await handle.result;
|
|
1285
|
+
return { ...result, output: output || result.output };
|
|
1286
|
+
}
|
|
1287
|
+
executeStreaming(payload, onOutput, abortSignal) {
|
|
1288
|
+
const p = payload;
|
|
1289
|
+
const timeout = (p.timeout != null ? p.timeout * 1e3 : null) ?? DEFAULT_TIMEOUT3;
|
|
1290
|
+
let resolve;
|
|
1291
|
+
const resultPromise = new Promise((r) => {
|
|
1292
|
+
resolve = r;
|
|
1293
|
+
});
|
|
1294
|
+
let settled = false;
|
|
1295
|
+
const settle = (result) => {
|
|
1296
|
+
if (settled) return;
|
|
1297
|
+
settled = true;
|
|
1298
|
+
clearTimeout(timer);
|
|
1299
|
+
resolve?.(result);
|
|
1300
|
+
};
|
|
1301
|
+
if (abortSignal?.aborted) {
|
|
1302
|
+
return {
|
|
1303
|
+
result: Promise.resolve({
|
|
1304
|
+
status: "cancelled",
|
|
1305
|
+
error: "Execution aborted"
|
|
1306
|
+
}),
|
|
1307
|
+
cancel: () => {
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
const child = spawn3("sh", ["-c", p.script], {
|
|
1312
|
+
cwd: p.cwd,
|
|
1313
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1314
|
+
});
|
|
1315
|
+
const timer = setTimeout(() => {
|
|
1316
|
+
child.kill("SIGTERM");
|
|
1317
|
+
setTimeout(() => {
|
|
1318
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
1319
|
+
}, 5e3);
|
|
1320
|
+
settle({
|
|
1321
|
+
status: "timeout",
|
|
1322
|
+
error: `Shell execution timed out after ${timeout / 1e3}s`
|
|
1323
|
+
});
|
|
1324
|
+
}, timeout);
|
|
1325
|
+
let stdoutBuf = "";
|
|
1326
|
+
let stderrBuf = "";
|
|
1327
|
+
child.stdout.on("data", (chunk) => {
|
|
1328
|
+
const data = chunk.toString();
|
|
1329
|
+
stdoutBuf += data;
|
|
1330
|
+
if (stdoutBuf.length > MAX_OUTPUT_BYTES2)
|
|
1331
|
+
stdoutBuf = stdoutBuf.slice(-MAX_OUTPUT_BYTES2);
|
|
1332
|
+
onOutput("stdout", data);
|
|
1333
|
+
});
|
|
1334
|
+
child.stderr.on("data", (chunk) => {
|
|
1335
|
+
const data = chunk.toString();
|
|
1336
|
+
stderrBuf += data;
|
|
1337
|
+
if (stderrBuf.length > MAX_OUTPUT_BYTES2)
|
|
1338
|
+
stderrBuf = stderrBuf.slice(-MAX_OUTPUT_BYTES2);
|
|
1339
|
+
onOutput("stderr", data);
|
|
1340
|
+
});
|
|
1341
|
+
child.on("close", (code) => {
|
|
1342
|
+
if (code === 0) {
|
|
1343
|
+
settle({ status: "success", output: stdoutBuf || void 0 });
|
|
1344
|
+
} else {
|
|
1345
|
+
settle({
|
|
1346
|
+
status: "failure",
|
|
1347
|
+
output: stdoutBuf || void 0,
|
|
1348
|
+
error: stderrBuf || `Process exited with code ${code}`
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
});
|
|
1352
|
+
child.on("error", (err) => {
|
|
1353
|
+
settle({ status: "failure", error: err.message });
|
|
1354
|
+
});
|
|
1355
|
+
const cancelFn = () => {
|
|
1356
|
+
child.kill("SIGTERM");
|
|
1357
|
+
setTimeout(() => {
|
|
1358
|
+
if (!child.killed) child.kill("SIGKILL");
|
|
1359
|
+
}, 5e3);
|
|
1360
|
+
settle({ status: "cancelled", error: "Execution cancelled" });
|
|
1361
|
+
};
|
|
1362
|
+
abortSignal?.addEventListener("abort", () => cancelFn(), { once: true });
|
|
1363
|
+
return { result: resultPromise, cancel: cancelFn };
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
// src/scheduled-tasks/executors/types.ts
|
|
1368
|
+
function isStreamingTaskExecutor(executor) {
|
|
1369
|
+
return "executeStreaming" in executor && typeof executor.executeStreaming === "function";
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// src/scheduled-tasks/executors/index.ts
|
|
1373
|
+
var executors = {
|
|
1374
|
+
shell: new ShellExecutor(),
|
|
1375
|
+
http_request: new HttpRequestExecutor(),
|
|
1376
|
+
github_copilot_cli: new GithubCopilotCliExecutor()
|
|
1377
|
+
};
|
|
1378
|
+
function getExecutor(taskType) {
|
|
1379
|
+
return executors[taskType];
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// src/scheduled-tasks/TaskConfigManager.ts
|
|
1383
|
+
import { randomUUID } from "crypto";
|
|
1384
|
+
import * as fs5 from "fs";
|
|
1385
|
+
import * as path5 from "path";
|
|
1386
|
+
var CONFIG_DIR3 = ".serviceme";
|
|
1387
|
+
var CONFIG_FILE = "scheduled-tasks.json";
|
|
1388
|
+
function emptyConfig() {
|
|
1389
|
+
return { version: 1, tasks: [] };
|
|
1390
|
+
}
|
|
1391
|
+
var TaskConfigManager = class {
|
|
1392
|
+
constructor(workspacePath) {
|
|
1393
|
+
this.configPath = path5.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
|
|
1394
|
+
}
|
|
1395
|
+
getConfigPath() {
|
|
1396
|
+
return this.configPath;
|
|
1397
|
+
}
|
|
1398
|
+
readConfig() {
|
|
1399
|
+
if (!fs5.existsSync(this.configPath)) {
|
|
1400
|
+
return emptyConfig();
|
|
1401
|
+
}
|
|
1402
|
+
const raw = fs5.readFileSync(this.configPath, "utf-8");
|
|
1403
|
+
const parsed = JSON.parse(raw);
|
|
1404
|
+
return parsed;
|
|
1405
|
+
}
|
|
1406
|
+
writeConfig(config) {
|
|
1407
|
+
const dir = path5.dirname(this.configPath);
|
|
1408
|
+
if (!fs5.existsSync(dir)) {
|
|
1409
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1410
|
+
}
|
|
1411
|
+
const tmp = `${this.configPath}.tmp`;
|
|
1412
|
+
fs5.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
|
|
1413
|
+
fs5.renameSync(tmp, this.configPath);
|
|
1414
|
+
}
|
|
1415
|
+
listTasks() {
|
|
1416
|
+
return this.readConfig().tasks;
|
|
1417
|
+
}
|
|
1418
|
+
getTask(id) {
|
|
1419
|
+
return this.readConfig().tasks.find((t) => t.id === id);
|
|
1420
|
+
}
|
|
1421
|
+
getTaskByName(name) {
|
|
1422
|
+
return this.readConfig().tasks.find((t) => t.name === name);
|
|
1423
|
+
}
|
|
1424
|
+
createTask(input) {
|
|
1425
|
+
const config = this.readConfig();
|
|
1426
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1427
|
+
const task = {
|
|
1428
|
+
id: randomUUID(),
|
|
1429
|
+
name: input.name,
|
|
1430
|
+
description: input.description,
|
|
1431
|
+
enabled: input.enabled ?? true,
|
|
1432
|
+
scheduleType: input.scheduleType,
|
|
1433
|
+
schedule: input.schedule,
|
|
1434
|
+
taskType: input.taskType,
|
|
1435
|
+
payload: input.payload,
|
|
1436
|
+
createdAt: now,
|
|
1437
|
+
updatedAt: now
|
|
1438
|
+
};
|
|
1439
|
+
config.tasks.push(task);
|
|
1440
|
+
this.writeConfig(config);
|
|
1441
|
+
return task;
|
|
1442
|
+
}
|
|
1443
|
+
editTask(id, input) {
|
|
1444
|
+
const config = this.readConfig();
|
|
1445
|
+
const idx = config.tasks.findIndex((t) => t.id === id);
|
|
1446
|
+
if (idx === -1) {
|
|
1447
|
+
throw new Error(`Task '${id}' not found`);
|
|
1448
|
+
}
|
|
1449
|
+
const existing = config.tasks[idx];
|
|
1450
|
+
if (!existing) {
|
|
1451
|
+
throw new Error(`Task '${id}' not found`);
|
|
1452
|
+
}
|
|
1453
|
+
const updated = {
|
|
1454
|
+
...existing,
|
|
1455
|
+
...input.name !== void 0 && { name: input.name },
|
|
1456
|
+
...input.description !== void 0 && {
|
|
1457
|
+
description: input.description
|
|
1458
|
+
},
|
|
1459
|
+
...input.scheduleType !== void 0 && {
|
|
1460
|
+
scheduleType: input.scheduleType
|
|
1461
|
+
},
|
|
1462
|
+
...input.schedule !== void 0 && { schedule: input.schedule },
|
|
1463
|
+
...input.taskType !== void 0 && { taskType: input.taskType },
|
|
1464
|
+
...input.payload !== void 0 && { payload: input.payload },
|
|
1465
|
+
...input.enabled !== void 0 && { enabled: input.enabled },
|
|
1466
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1467
|
+
};
|
|
1468
|
+
config.tasks[idx] = updated;
|
|
1469
|
+
this.writeConfig(config);
|
|
1470
|
+
return updated;
|
|
1471
|
+
}
|
|
1472
|
+
deleteTask(id) {
|
|
1473
|
+
const config = this.readConfig();
|
|
1474
|
+
const idx = config.tasks.findIndex((t) => t.id === id);
|
|
1475
|
+
if (idx === -1) {
|
|
1476
|
+
throw new Error(`Task '${id}' not found`);
|
|
1477
|
+
}
|
|
1478
|
+
const removed = config.tasks[idx];
|
|
1479
|
+
if (!removed) {
|
|
1480
|
+
throw new Error(`Task '${id}' not found`);
|
|
1481
|
+
}
|
|
1482
|
+
config.tasks.splice(idx, 1);
|
|
1483
|
+
this.writeConfig(config);
|
|
1484
|
+
return removed;
|
|
1485
|
+
}
|
|
1486
|
+
toggleTask(id, enabled) {
|
|
1487
|
+
return this.editTask(id, { enabled });
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
|
|
1491
|
+
// src/scheduled-tasks/TaskLogManager.ts
|
|
1492
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
1493
|
+
import * as fs6 from "fs";
|
|
1494
|
+
import * as path6 from "path";
|
|
1495
|
+
var CONFIG_DIR4 = ".serviceme";
|
|
1496
|
+
var LOG_FILE2 = "scheduled-tasks-log.json";
|
|
1497
|
+
var MAX_LOGS = 200;
|
|
1498
|
+
function emptyLogFile() {
|
|
1499
|
+
return { logs: [] };
|
|
1500
|
+
}
|
|
1501
|
+
function isValidLogEntry(entry) {
|
|
1502
|
+
if (!entry || typeof entry !== "object") return false;
|
|
1503
|
+
const e = entry;
|
|
1504
|
+
return typeof e.id === "string" && typeof e.taskId === "string" && typeof e.taskName === "string" && typeof e.startedAt === "string" && typeof e.finishedAt === "string" && typeof e.status === "string";
|
|
1505
|
+
}
|
|
1506
|
+
function validateAndRepairLogFile(raw) {
|
|
1507
|
+
if (!raw || typeof raw !== "object") {
|
|
1508
|
+
return emptyLogFile();
|
|
1509
|
+
}
|
|
1510
|
+
const file = raw;
|
|
1511
|
+
if (!Array.isArray(file.logs)) {
|
|
1512
|
+
return emptyLogFile();
|
|
1513
|
+
}
|
|
1514
|
+
const validLogs = file.logs.filter(isValidLogEntry);
|
|
1515
|
+
return { logs: validLogs };
|
|
1516
|
+
}
|
|
1517
|
+
var TaskLogManager = class {
|
|
1518
|
+
constructor(workspacePath) {
|
|
1519
|
+
this.logPath = path6.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
|
|
1520
|
+
}
|
|
1521
|
+
getLogPath() {
|
|
1522
|
+
return this.logPath;
|
|
1523
|
+
}
|
|
1524
|
+
readLogFile() {
|
|
1525
|
+
if (!fs6.existsSync(this.logPath)) {
|
|
1526
|
+
return emptyLogFile();
|
|
1527
|
+
}
|
|
1528
|
+
try {
|
|
1529
|
+
const raw = fs6.readFileSync(this.logPath, "utf-8");
|
|
1530
|
+
const parsed = JSON.parse(raw);
|
|
1531
|
+
const file = validateAndRepairLogFile(parsed);
|
|
1532
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.logs) || parsed.logs.length !== file.logs.length) {
|
|
1533
|
+
this.writeLogFile(file);
|
|
1534
|
+
}
|
|
1535
|
+
return file;
|
|
1536
|
+
} catch {
|
|
1537
|
+
this.backupCorruptedFile();
|
|
1538
|
+
const fresh = emptyLogFile();
|
|
1539
|
+
this.writeLogFile(fresh);
|
|
1540
|
+
return fresh;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
backupCorruptedFile() {
|
|
1544
|
+
try {
|
|
1545
|
+
if (fs6.existsSync(this.logPath)) {
|
|
1546
|
+
const backupPath = `${this.logPath}.corrupted.${Date.now()}`;
|
|
1547
|
+
fs6.copyFileSync(this.logPath, backupPath);
|
|
1548
|
+
}
|
|
1549
|
+
} catch {
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
writeLogFile(file) {
|
|
1553
|
+
const dir = path6.dirname(this.logPath);
|
|
1554
|
+
if (!fs6.existsSync(dir)) {
|
|
1555
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
1556
|
+
}
|
|
1557
|
+
const tmp = `${this.logPath}.tmp`;
|
|
1558
|
+
fs6.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
|
|
1559
|
+
fs6.renameSync(tmp, this.logPath);
|
|
1560
|
+
}
|
|
1561
|
+
appendLog(input) {
|
|
1562
|
+
const file = this.readLogFile();
|
|
1563
|
+
const log = {
|
|
1564
|
+
id: randomUUID2(),
|
|
1565
|
+
taskId: input.taskId,
|
|
1566
|
+
taskName: input.taskName,
|
|
1567
|
+
startedAt: input.startedAt,
|
|
1568
|
+
finishedAt: input.finishedAt,
|
|
1569
|
+
status: input.status,
|
|
1570
|
+
output: input.output,
|
|
1571
|
+
error: input.error
|
|
1572
|
+
};
|
|
1573
|
+
file.logs.push(log);
|
|
1574
|
+
if (file.logs.length > MAX_LOGS) {
|
|
1575
|
+
file.logs = file.logs.slice(file.logs.length - MAX_LOGS);
|
|
1576
|
+
}
|
|
1577
|
+
this.writeLogFile(file);
|
|
1578
|
+
return log;
|
|
1579
|
+
}
|
|
1580
|
+
getLogs(options) {
|
|
1581
|
+
const file = this.readLogFile();
|
|
1582
|
+
let logs = file.logs;
|
|
1583
|
+
if (options?.taskId) {
|
|
1584
|
+
logs = logs.filter((l) => l.taskId === options.taskId);
|
|
1585
|
+
}
|
|
1586
|
+
const total = logs.length;
|
|
1587
|
+
logs = logs.slice().reverse();
|
|
1588
|
+
if (options?.limit && options.limit > 0) {
|
|
1589
|
+
logs = logs.slice(0, options.limit);
|
|
1590
|
+
}
|
|
1591
|
+
return { logs, total };
|
|
1592
|
+
}
|
|
1593
|
+
clearLogs(taskId) {
|
|
1594
|
+
const file = this.readLogFile();
|
|
1595
|
+
const before = file.logs.length;
|
|
1596
|
+
if (taskId) {
|
|
1597
|
+
file.logs = file.logs.filter((l) => l.taskId !== taskId);
|
|
1598
|
+
} else {
|
|
1599
|
+
file.logs = [];
|
|
1600
|
+
}
|
|
1601
|
+
this.writeLogFile(file);
|
|
1602
|
+
return before - file.logs.length;
|
|
1603
|
+
}
|
|
1604
|
+
};
|
|
1605
|
+
|
|
1606
|
+
// src/scheduled-tasks/daemon/SchedulerDaemon.ts
|
|
1607
|
+
var TICK_INTERVAL = 1e3;
|
|
1608
|
+
var MIN_SCHEDULE_INTERVAL = 1e3;
|
|
1609
|
+
var SchedulerDaemon = class {
|
|
1610
|
+
constructor(workspacePath) {
|
|
1611
|
+
this.tickTimer = null;
|
|
1612
|
+
this.watcher = null;
|
|
1613
|
+
this.running = false;
|
|
1614
|
+
this.startTime = 0;
|
|
1615
|
+
// Track last execution time and running state per task
|
|
1616
|
+
this.lastRun = /* @__PURE__ */ new Map();
|
|
1617
|
+
this.taskRunning = /* @__PURE__ */ new Set();
|
|
1618
|
+
this.workspacePath = workspacePath;
|
|
1619
|
+
this.configManager = new TaskConfigManager(workspacePath);
|
|
1620
|
+
this.logManager = new TaskLogManager(workspacePath);
|
|
1621
|
+
this.pidManager = new PidManager(workspacePath);
|
|
1622
|
+
this.logger = new DaemonLogger(workspacePath);
|
|
1623
|
+
}
|
|
1624
|
+
start() {
|
|
1625
|
+
if (this.running) return;
|
|
1626
|
+
this.running = true;
|
|
1627
|
+
this.startTime = Date.now();
|
|
1628
|
+
this.pidManager.writePid(process.pid);
|
|
1629
|
+
this.logger.log("info", `Scheduler daemon started (PID: ${process.pid})`);
|
|
1630
|
+
this.tickTimer = setInterval(() => this.tick(), TICK_INTERVAL);
|
|
1631
|
+
this.setupConfigWatch();
|
|
1632
|
+
process.on("SIGTERM", () => this.stop());
|
|
1633
|
+
process.on("SIGINT", () => this.stop());
|
|
1634
|
+
}
|
|
1635
|
+
stop() {
|
|
1636
|
+
if (!this.running) return;
|
|
1637
|
+
this.running = false;
|
|
1638
|
+
if (this.tickTimer) {
|
|
1639
|
+
clearInterval(this.tickTimer);
|
|
1640
|
+
this.tickTimer = null;
|
|
1641
|
+
}
|
|
1642
|
+
if (this.watcher) {
|
|
1643
|
+
this.watcher.close();
|
|
1644
|
+
this.watcher = null;
|
|
1645
|
+
}
|
|
1646
|
+
this.pidManager.removePid();
|
|
1647
|
+
this.logger.log("info", "Scheduler daemon stopped");
|
|
1648
|
+
process.exit(0);
|
|
1649
|
+
}
|
|
1650
|
+
setupConfigWatch() {
|
|
1651
|
+
const configPath = this.configManager.getConfigPath();
|
|
1652
|
+
const dir = configPath.substring(0, configPath.lastIndexOf("/"));
|
|
1653
|
+
try {
|
|
1654
|
+
if (fs7.existsSync(dir)) {
|
|
1655
|
+
this.watcher = fs7.watch(dir, (_eventType, filename) => {
|
|
1656
|
+
if (filename === "scheduled-tasks.json") {
|
|
1657
|
+
this.logger.log("info", "Config file changed, reconciling...");
|
|
1658
|
+
}
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
} catch {
|
|
1662
|
+
this.logger.log("warn", "Could not watch config directory");
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
tick() {
|
|
1666
|
+
const config = this.configManager.readConfig();
|
|
1667
|
+
const now = Date.now();
|
|
1668
|
+
for (const task of config.tasks) {
|
|
1669
|
+
if (!task.enabled) continue;
|
|
1670
|
+
if (this.taskRunning.has(task.id)) continue;
|
|
1671
|
+
const lastExec = this.lastRun.get(task.id) ?? 0;
|
|
1672
|
+
if (this.shouldRun(task, lastExec, now)) {
|
|
1673
|
+
this.executeTask(task, now);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
shouldRun(task, lastExec, now) {
|
|
1678
|
+
if (task.scheduleType === "interval") {
|
|
1679
|
+
const intervalMs = parseIntervalMs(task.schedule);
|
|
1680
|
+
if (intervalMs < MIN_SCHEDULE_INTERVAL) return false;
|
|
1681
|
+
return now - lastExec >= intervalMs;
|
|
1682
|
+
}
|
|
1683
|
+
if (task.scheduleType === "cron") {
|
|
1684
|
+
if (now - lastExec < 6e4) return false;
|
|
1685
|
+
return matchesCron(task.schedule, new Date(now));
|
|
1686
|
+
}
|
|
1687
|
+
return false;
|
|
1688
|
+
}
|
|
1689
|
+
async executeTask(task, now) {
|
|
1690
|
+
this.taskRunning.add(task.id);
|
|
1691
|
+
this.lastRun.set(task.id, now);
|
|
1692
|
+
const startedAt = new Date(now).toISOString();
|
|
1693
|
+
this.logger.log("info", `Executing task: ${task.name} (${task.id})`);
|
|
1694
|
+
try {
|
|
1695
|
+
const executor = getExecutor(task.taskType);
|
|
1696
|
+
const result = await executor.execute(task.payload);
|
|
1697
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1698
|
+
this.logManager.appendLog({
|
|
1699
|
+
taskId: task.id,
|
|
1700
|
+
taskName: task.name,
|
|
1701
|
+
startedAt,
|
|
1702
|
+
finishedAt,
|
|
1703
|
+
status: result.status === "running" ? "failure" : result.status,
|
|
1704
|
+
output: result.output,
|
|
1705
|
+
error: result.error
|
|
1706
|
+
});
|
|
1707
|
+
this.logger.log("info", `Task ${task.name} completed: ${result.status}`);
|
|
1708
|
+
} catch (err) {
|
|
1709
|
+
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1710
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1711
|
+
this.logManager.appendLog({
|
|
1712
|
+
taskId: task.id,
|
|
1713
|
+
taskName: task.name,
|
|
1714
|
+
startedAt,
|
|
1715
|
+
finishedAt,
|
|
1716
|
+
status: "failure",
|
|
1717
|
+
error: message
|
|
1718
|
+
});
|
|
1719
|
+
this.logger.log("error", `Task ${task.name} failed: ${message}`);
|
|
1720
|
+
} finally {
|
|
1721
|
+
this.taskRunning.delete(task.id);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
getStatus() {
|
|
1725
|
+
const config = this.configManager.readConfig();
|
|
1726
|
+
return {
|
|
1727
|
+
running: this.running,
|
|
1728
|
+
pid: process.pid,
|
|
1729
|
+
uptimeSeconds: this.running ? Math.floor((Date.now() - this.startTime) / 1e3) : null,
|
|
1730
|
+
tasksRegistered: config.tasks.length,
|
|
1731
|
+
tasksEnabled: config.tasks.filter((t) => t.enabled).length,
|
|
1732
|
+
workspacePath: this.workspacePath,
|
|
1733
|
+
pidFile: this.pidManager.getPidPath()
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
function parseIntervalMs(schedule) {
|
|
1738
|
+
const match = /^every\s+(\d+)\s*(s|sec|m|min|h|hr|d|day)s?$/i.exec(schedule);
|
|
1739
|
+
if (!match) return 0;
|
|
1740
|
+
const [, numStr, unit] = match;
|
|
1741
|
+
const num = Number.parseInt(numStr ?? "0", 10);
|
|
1742
|
+
switch (unit?.toLowerCase()) {
|
|
1743
|
+
case "s":
|
|
1744
|
+
case "sec":
|
|
1745
|
+
return num * 1e3;
|
|
1746
|
+
case "m":
|
|
1747
|
+
case "min":
|
|
1748
|
+
return num * 60 * 1e3;
|
|
1749
|
+
case "h":
|
|
1750
|
+
case "hr":
|
|
1751
|
+
return num * 60 * 60 * 1e3;
|
|
1752
|
+
case "d":
|
|
1753
|
+
case "day":
|
|
1754
|
+
return num * 24 * 60 * 60 * 1e3;
|
|
1755
|
+
default:
|
|
1756
|
+
return 0;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
function matchesCron(expression, date) {
|
|
1760
|
+
const parts = expression.trim().split(/\s+/);
|
|
1761
|
+
if (parts.length < 5) return false;
|
|
1762
|
+
const [minPart, hourPart, dayPart, monthPart, weekdayPart] = parts;
|
|
1763
|
+
if (!minPart || !hourPart || !dayPart || !monthPart || !weekdayPart)
|
|
1764
|
+
return false;
|
|
1765
|
+
return matchCronField(minPart, date.getMinutes()) && matchCronField(hourPart, date.getHours()) && matchCronField(dayPart, date.getDate()) && matchCronField(monthPart, date.getMonth() + 1) && matchCronField(weekdayPart, date.getDay());
|
|
1766
|
+
}
|
|
1767
|
+
function matchCronField(field, value) {
|
|
1768
|
+
if (field === "*") return true;
|
|
1769
|
+
if (field.startsWith("*/")) {
|
|
1770
|
+
const step = Number.parseInt(field.slice(2), 10);
|
|
1771
|
+
return step > 0 && value % step === 0;
|
|
1772
|
+
}
|
|
1773
|
+
const values = field.split(",");
|
|
1774
|
+
return values.some((v) => Number.parseInt(v, 10) === value);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/scheduled-tasks/TaskExecutionEngine.ts
|
|
1778
|
+
var TaskExecutionEngine = class {
|
|
1779
|
+
constructor(getExecutor2) {
|
|
1780
|
+
this.getExecutor = getExecutor2;
|
|
1781
|
+
this.running = /* @__PURE__ */ new Map();
|
|
1782
|
+
this.taskExecutions = /* @__PURE__ */ new Map();
|
|
1783
|
+
this.listener = null;
|
|
1784
|
+
}
|
|
1785
|
+
setListener(listener) {
|
|
1786
|
+
this.listener = listener;
|
|
1787
|
+
}
|
|
1788
|
+
async execute(snapshot) {
|
|
1789
|
+
const policy = snapshot.concurrencyPolicy ?? "reject";
|
|
1790
|
+
if (policy === "reject") {
|
|
1791
|
+
const existingIds = this.taskExecutions.get(snapshot.taskId);
|
|
1792
|
+
if (existingIds && existingIds.size > 0) {
|
|
1793
|
+
throw new Error(
|
|
1794
|
+
`TASK_ALREADY_RUNNING: Task ${snapshot.taskId} is already running`
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
const executor = this.getExecutor(snapshot.type);
|
|
1799
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1800
|
+
if (!this.taskExecutions.has(snapshot.taskId)) {
|
|
1801
|
+
this.taskExecutions.set(snapshot.taskId, /* @__PURE__ */ new Set());
|
|
1802
|
+
}
|
|
1803
|
+
this.taskExecutions.get(snapshot.taskId)?.add(snapshot.executionId);
|
|
1804
|
+
const ac = new AbortController();
|
|
1805
|
+
const runningExec = {
|
|
1806
|
+
executionId: snapshot.executionId,
|
|
1807
|
+
taskId: snapshot.taskId,
|
|
1808
|
+
startedAt,
|
|
1809
|
+
cancel: () => ac.abort()
|
|
1810
|
+
};
|
|
1811
|
+
this.running.set(snapshot.executionId, runningExec);
|
|
1812
|
+
this.listener?.onStarted({
|
|
1813
|
+
executionId: snapshot.executionId,
|
|
1814
|
+
taskId: snapshot.taskId,
|
|
1815
|
+
startedAt
|
|
1816
|
+
});
|
|
1817
|
+
try {
|
|
1818
|
+
let result;
|
|
1819
|
+
if (isStreamingTaskExecutor(executor)) {
|
|
1820
|
+
const handle = executor.executeStreaming(
|
|
1821
|
+
snapshot.payload,
|
|
1822
|
+
(stream, data) => {
|
|
1823
|
+
this.listener?.onOutput({
|
|
1824
|
+
executionId: snapshot.executionId,
|
|
1825
|
+
stream,
|
|
1826
|
+
data
|
|
1827
|
+
});
|
|
1828
|
+
},
|
|
1829
|
+
ac.signal
|
|
1830
|
+
);
|
|
1831
|
+
runningExec.cancel = () => {
|
|
1832
|
+
handle.cancel();
|
|
1833
|
+
ac.abort();
|
|
1834
|
+
};
|
|
1835
|
+
result = await handle.result;
|
|
1836
|
+
} else {
|
|
1837
|
+
result = await executor.execute(snapshot.payload, ac.signal);
|
|
1838
|
+
}
|
|
1839
|
+
const log = {
|
|
1840
|
+
id: snapshot.executionId,
|
|
1841
|
+
taskId: snapshot.taskId,
|
|
1842
|
+
taskName: snapshot.name,
|
|
1843
|
+
startedAt,
|
|
1844
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1845
|
+
status: result.status === "running" ? "failure" : result.status,
|
|
1846
|
+
output: result.output,
|
|
1847
|
+
error: result.error
|
|
1848
|
+
};
|
|
1849
|
+
switch (log.status) {
|
|
1850
|
+
case "success":
|
|
1851
|
+
this.listener?.onCompleted({
|
|
1852
|
+
executionId: snapshot.executionId,
|
|
1853
|
+
log
|
|
1854
|
+
});
|
|
1855
|
+
break;
|
|
1856
|
+
case "cancelled":
|
|
1857
|
+
this.listener?.onCancelled({
|
|
1858
|
+
executionId: snapshot.executionId,
|
|
1859
|
+
log
|
|
1860
|
+
});
|
|
1861
|
+
break;
|
|
1862
|
+
case "timeout":
|
|
1863
|
+
this.listener?.onFailed({
|
|
1864
|
+
executionId: snapshot.executionId,
|
|
1865
|
+
status: "timeout",
|
|
1866
|
+
reason: "timeout",
|
|
1867
|
+
log
|
|
1868
|
+
});
|
|
1869
|
+
break;
|
|
1870
|
+
default:
|
|
1871
|
+
this.listener?.onFailed({
|
|
1872
|
+
executionId: snapshot.executionId,
|
|
1873
|
+
status: "failure",
|
|
1874
|
+
reason: "error",
|
|
1875
|
+
log
|
|
1876
|
+
});
|
|
1877
|
+
break;
|
|
1878
|
+
}
|
|
1879
|
+
} catch (err) {
|
|
1880
|
+
const log = {
|
|
1881
|
+
id: snapshot.executionId,
|
|
1882
|
+
taskId: snapshot.taskId,
|
|
1883
|
+
taskName: snapshot.name,
|
|
1884
|
+
startedAt,
|
|
1885
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1886
|
+
status: ac.signal.aborted ? "cancelled" : "failure",
|
|
1887
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1888
|
+
};
|
|
1889
|
+
if (ac.signal.aborted) {
|
|
1890
|
+
this.listener?.onCancelled({
|
|
1891
|
+
executionId: snapshot.executionId,
|
|
1892
|
+
log
|
|
1893
|
+
});
|
|
1894
|
+
} else {
|
|
1895
|
+
this.listener?.onFailed({
|
|
1896
|
+
executionId: snapshot.executionId,
|
|
1897
|
+
status: "failure",
|
|
1898
|
+
reason: "error",
|
|
1899
|
+
log
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
} finally {
|
|
1903
|
+
this.running.delete(snapshot.executionId);
|
|
1904
|
+
const taskExecIds = this.taskExecutions.get(snapshot.taskId);
|
|
1905
|
+
if (taskExecIds) {
|
|
1906
|
+
taskExecIds.delete(snapshot.executionId);
|
|
1907
|
+
if (taskExecIds.size === 0) {
|
|
1908
|
+
this.taskExecutions.delete(snapshot.taskId);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
cancel(executionId) {
|
|
1914
|
+
const exec = this.running.get(executionId);
|
|
1915
|
+
if (!exec) return false;
|
|
1916
|
+
exec.cancel();
|
|
1917
|
+
return true;
|
|
1918
|
+
}
|
|
1919
|
+
listRunning() {
|
|
1920
|
+
return Array.from(this.running.values()).map((e) => ({
|
|
1921
|
+
executionId: e.executionId,
|
|
1922
|
+
taskId: e.taskId,
|
|
1923
|
+
startedAt: e.startedAt
|
|
1924
|
+
}));
|
|
1925
|
+
}
|
|
1926
|
+
dispose() {
|
|
1927
|
+
for (const exec of this.running.values()) {
|
|
1928
|
+
exec.cancel();
|
|
1929
|
+
}
|
|
1930
|
+
this.running.clear();
|
|
1931
|
+
this.taskExecutions.clear();
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1232
1934
|
export {
|
|
1935
|
+
DaemonLogger,
|
|
1233
1936
|
EnvironmentInspector,
|
|
1937
|
+
GithubCopilotCliExecutor,
|
|
1938
|
+
HttpRequestExecutor,
|
|
1234
1939
|
ImageTools,
|
|
1235
|
-
|
|
1940
|
+
PidManager,
|
|
1236
1941
|
ProjectTools,
|
|
1942
|
+
SchedulerDaemon,
|
|
1943
|
+
ShellExecutor,
|
|
1944
|
+
TaskConfigManager,
|
|
1945
|
+
TaskExecutionEngine,
|
|
1946
|
+
TaskLogManager,
|
|
1947
|
+
copilotDoctor,
|
|
1948
|
+
copilotPrompt,
|
|
1237
1949
|
createConsoleLogger,
|
|
1950
|
+
createCopilotAuthRequiredError,
|
|
1951
|
+
createCopilotNotInstalledError,
|
|
1238
1952
|
createImageTools,
|
|
1239
1953
|
createJsonTools,
|
|
1240
1954
|
createProjectTools,
|
|
1955
|
+
getExecutor,
|
|
1956
|
+
isCopilotAuthenticated,
|
|
1957
|
+
isStreamingTaskExecutor,
|
|
1958
|
+
matchesCron,
|
|
1241
1959
|
moveFiles,
|
|
1242
1960
|
noopLogger,
|
|
1961
|
+
parseIntervalMs,
|
|
1243
1962
|
unzipFile
|
|
1244
1963
|
};
|