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