@serviceme/devtools-core 0.1.3 → 0.1.5
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 +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +558 -263
- package/dist/index.mjs +555 -262
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -57,7 +57,9 @@ __export(index_exports, {
|
|
|
57
57
|
moveFiles: () => moveFiles,
|
|
58
58
|
noopLogger: () => noopLogger,
|
|
59
59
|
parseIntervalMs: () => parseIntervalMs,
|
|
60
|
-
|
|
60
|
+
resolveTaskExecutionPayload: () => resolveTaskExecutionPayload,
|
|
61
|
+
unzipFile: () => unzipFile,
|
|
62
|
+
validateTaskPayload: () => validateTaskPayload
|
|
61
63
|
});
|
|
62
64
|
module.exports = __toCommonJS(index_exports);
|
|
63
65
|
|
|
@@ -92,7 +94,8 @@ async function runCommand(command, options = {}) {
|
|
|
92
94
|
cwd: options.cwd,
|
|
93
95
|
env: options.env,
|
|
94
96
|
shell: options.shell,
|
|
95
|
-
stdio: "pipe"
|
|
97
|
+
stdio: "pipe",
|
|
98
|
+
windowsHide: true
|
|
96
99
|
});
|
|
97
100
|
let stdout = "";
|
|
98
101
|
let stderr = "";
|
|
@@ -290,8 +293,14 @@ async function copilotPrompt(options) {
|
|
|
290
293
|
|
|
291
294
|
// src/env/environmentInspector.ts
|
|
292
295
|
var import_devtools_protocol3 = require("@serviceme/devtools-protocol");
|
|
293
|
-
var
|
|
296
|
+
var DEFAULT_TOOL_CHECK_TIMEOUT_MS = 5e3;
|
|
297
|
+
var TOOL_CHECK_TIMEOUT_MS = {
|
|
298
|
+
nuget: 12e3,
|
|
299
|
+
nvm: 8e3,
|
|
300
|
+
dotnet: 8e3
|
|
301
|
+
};
|
|
294
302
|
var ERROR_CODE_NOT_FOUND = 127;
|
|
303
|
+
var ERROR_CODE_TIMEOUT = "ETIMEDOUT";
|
|
295
304
|
var EnvironmentInspector = class {
|
|
296
305
|
async checkEnvironment() {
|
|
297
306
|
const results = await Promise.all(
|
|
@@ -328,19 +337,18 @@ var EnvironmentInspector = class {
|
|
|
328
337
|
const isWindows = process.platform === "win32";
|
|
329
338
|
const result = await runCommand(isWindows ? "where" : "which", {
|
|
330
339
|
args: [toolName],
|
|
331
|
-
timeoutMs:
|
|
340
|
+
timeoutMs: this.getToolTimeout(toolName)
|
|
332
341
|
});
|
|
333
|
-
return result.stdout.trim().
|
|
342
|
+
return result.stdout.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
|
|
334
343
|
} catch {
|
|
335
344
|
return void 0;
|
|
336
345
|
}
|
|
337
346
|
}
|
|
338
347
|
async getToolVersion(toolName) {
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
timeoutMs: TOOL_CHECK_TIMEOUT_MS
|
|
348
|
+
const invocation = this.getVersionInvocation(toolName);
|
|
349
|
+
const result = await runCommand(invocation.command, {
|
|
350
|
+
args: invocation.args,
|
|
351
|
+
timeoutMs: this.getToolTimeout(toolName)
|
|
344
352
|
});
|
|
345
353
|
return this.parseVersion(toolName, result.stdout || result.stderr);
|
|
346
354
|
}
|
|
@@ -349,7 +357,7 @@ var EnvironmentInspector = class {
|
|
|
349
357
|
try {
|
|
350
358
|
const result = await runCommand("cmd.exe", {
|
|
351
359
|
args: ["/c", "nvm version"],
|
|
352
|
-
timeoutMs:
|
|
360
|
+
timeoutMs: this.getToolTimeout("nvm")
|
|
353
361
|
});
|
|
354
362
|
return {
|
|
355
363
|
installed: true,
|
|
@@ -369,7 +377,7 @@ var EnvironmentInspector = class {
|
|
|
369
377
|
"-lc",
|
|
370
378
|
'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; nvm --version'
|
|
371
379
|
],
|
|
372
|
-
timeoutMs:
|
|
380
|
+
timeoutMs: this.getToolTimeout("nvm")
|
|
373
381
|
});
|
|
374
382
|
return {
|
|
375
383
|
installed: true,
|
|
@@ -394,7 +402,7 @@ var EnvironmentInspector = class {
|
|
|
394
402
|
try {
|
|
395
403
|
const result = await runCommand("dotnet", {
|
|
396
404
|
args: ["nuget", "list", "source"],
|
|
397
|
-
timeoutMs:
|
|
405
|
+
timeoutMs: this.getToolTimeout("nuget")
|
|
398
406
|
});
|
|
399
407
|
const privateSourceUrl = "http://192.168.20.209:10010/nuget";
|
|
400
408
|
if (result.stdout.includes(privateSourceUrl)) {
|
|
@@ -412,17 +420,28 @@ var EnvironmentInspector = class {
|
|
|
412
420
|
return this.handleToolCheckError(error);
|
|
413
421
|
}
|
|
414
422
|
}
|
|
415
|
-
|
|
423
|
+
getVersionInvocation(toolName) {
|
|
424
|
+
const isWindows = process.platform === "win32";
|
|
425
|
+
if (isWindows && ["npm", "pnpm", "nrm"].includes(toolName)) {
|
|
426
|
+
return {
|
|
427
|
+
command: "cmd.exe",
|
|
428
|
+
args: ["/d", "/s", "/c", `${toolName} --version`]
|
|
429
|
+
};
|
|
430
|
+
}
|
|
416
431
|
const commands = {
|
|
417
|
-
git: "git --version",
|
|
418
|
-
node: "node --version",
|
|
419
|
-
npm: "npm --version",
|
|
420
|
-
pnpm: "pnpm --version",
|
|
421
|
-
nvm: "nvm --version",
|
|
422
|
-
nrm: "nrm --version",
|
|
423
|
-
|
|
432
|
+
git: { command: "git", args: ["--version"] },
|
|
433
|
+
node: { command: "node", args: ["--version"] },
|
|
434
|
+
npm: { command: "npm", args: ["--version"] },
|
|
435
|
+
pnpm: { command: "pnpm", args: ["--version"] },
|
|
436
|
+
nvm: { command: "nvm", args: ["--version"] },
|
|
437
|
+
nrm: { command: "nrm", args: ["--version"] },
|
|
438
|
+
rtk: { command: "rtk", args: ["--version"] },
|
|
439
|
+
dotnet: { command: "dotnet", args: ["--version"] }
|
|
424
440
|
};
|
|
425
|
-
return commands[toolName] ||
|
|
441
|
+
return commands[toolName] || { command: toolName, args: ["--version"] };
|
|
442
|
+
}
|
|
443
|
+
getToolTimeout(toolName) {
|
|
444
|
+
return TOOL_CHECK_TIMEOUT_MS[toolName] ?? DEFAULT_TOOL_CHECK_TIMEOUT_MS;
|
|
426
445
|
}
|
|
427
446
|
parseVersion(toolName, output) {
|
|
428
447
|
const cleaned = output.trim();
|
|
@@ -449,10 +468,11 @@ var EnvironmentInspector = class {
|
|
|
449
468
|
const execError = error;
|
|
450
469
|
const message = execError.message || String(error);
|
|
451
470
|
const code = execError.code;
|
|
452
|
-
const isNotFound = message.includes("command not found") || message.includes("not recognized") || code === ERROR_CODE_NOT_FOUND;
|
|
471
|
+
const isNotFound = message.includes("command not found") || message.includes("not recognized") || message.includes("ENOENT") || code === "ENOENT" || code === ERROR_CODE_NOT_FOUND;
|
|
472
|
+
const isTimeout = code === ERROR_CODE_TIMEOUT || message.toLowerCase().includes("timed out");
|
|
453
473
|
return {
|
|
454
474
|
installed: false,
|
|
455
|
-
error: isNotFound ? "Not installed" : message
|
|
475
|
+
error: isNotFound ? "Not installed" : isTimeout ? "Check timed out" : message
|
|
456
476
|
};
|
|
457
477
|
}
|
|
458
478
|
};
|
|
@@ -1113,12 +1133,75 @@ var PidManager = class {
|
|
|
1113
1133
|
};
|
|
1114
1134
|
|
|
1115
1135
|
// src/scheduled-tasks/daemon/SchedulerDaemon.ts
|
|
1116
|
-
var
|
|
1136
|
+
var fs9 = __toESM(require("fs"));
|
|
1137
|
+
var os = __toESM(require("os"));
|
|
1117
1138
|
|
|
1118
1139
|
// src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
|
|
1119
1140
|
var import_node_child_process2 = require("child_process");
|
|
1120
|
-
var
|
|
1141
|
+
var fs5 = __toESM(require("fs"));
|
|
1142
|
+
|
|
1143
|
+
// src/scheduled-tasks/executors/timeout.ts
|
|
1144
|
+
function resolveConfiguredTimeoutMs(timeoutSeconds, defaultTimeoutMs) {
|
|
1145
|
+
if (timeoutSeconds === 0) {
|
|
1146
|
+
return void 0;
|
|
1147
|
+
}
|
|
1148
|
+
if (typeof timeoutSeconds !== "number" || !Number.isFinite(timeoutSeconds) || timeoutSeconds < 0) {
|
|
1149
|
+
return defaultTimeoutMs;
|
|
1150
|
+
}
|
|
1151
|
+
return timeoutSeconds * 1e3;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
|
|
1121
1155
|
var MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
1156
|
+
var DEFAULT_TIMEOUT_MS2 = 3e5;
|
|
1157
|
+
function redactArgs(args) {
|
|
1158
|
+
const redacted = [];
|
|
1159
|
+
let redactNext = false;
|
|
1160
|
+
for (const arg of args) {
|
|
1161
|
+
if (redactNext) {
|
|
1162
|
+
redacted.push("<redacted>");
|
|
1163
|
+
redactNext = false;
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
redacted.push(arg);
|
|
1167
|
+
if (arg === "--prompt") {
|
|
1168
|
+
redactNext = true;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
return redacted;
|
|
1172
|
+
}
|
|
1173
|
+
function writeDiagnostic(message) {
|
|
1174
|
+
const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
|
|
1175
|
+
if (logPath) {
|
|
1176
|
+
fs5.appendFileSync(logPath, message);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
process.stderr.write(message);
|
|
1180
|
+
}
|
|
1181
|
+
function resolveGithubCopilotCliExecution(payload) {
|
|
1182
|
+
const args = ["copilot", "prompt", "--prompt", payload.prompt];
|
|
1183
|
+
if (payload.autopilot) {
|
|
1184
|
+
args.push("--autopilot");
|
|
1185
|
+
}
|
|
1186
|
+
if (payload.allowTools && payload.allowTools.length > 0) {
|
|
1187
|
+
args.push("--allow-tools", payload.allowTools.join(","));
|
|
1188
|
+
}
|
|
1189
|
+
if (payload.model) {
|
|
1190
|
+
args.push("--model", payload.model);
|
|
1191
|
+
}
|
|
1192
|
+
if (payload.agent) {
|
|
1193
|
+
args.push("--agent", payload.agent);
|
|
1194
|
+
}
|
|
1195
|
+
args.push("--timeout", String(payload.timeout ?? 0));
|
|
1196
|
+
return {
|
|
1197
|
+
command: "serviceme",
|
|
1198
|
+
args,
|
|
1199
|
+
cwd: payload.workspace,
|
|
1200
|
+
timeoutMs: resolveConfiguredTimeoutMs(payload.timeout, DEFAULT_TIMEOUT_MS2),
|
|
1201
|
+
diagnosticArgs: redactArgs(args),
|
|
1202
|
+
promptLen: payload.prompt.length
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1122
1205
|
var GithubCopilotCliExecutor = class {
|
|
1123
1206
|
async execute(payload, abortSignal) {
|
|
1124
1207
|
const authenticated = await isCopilotAuthenticated();
|
|
@@ -1141,7 +1224,7 @@ var GithubCopilotCliExecutor = class {
|
|
|
1141
1224
|
}
|
|
1142
1225
|
executeStreaming(payload, onOutput, abortSignal) {
|
|
1143
1226
|
const p = payload;
|
|
1144
|
-
const
|
|
1227
|
+
const execution = resolveGithubCopilotCliExecution(p);
|
|
1145
1228
|
let resolve;
|
|
1146
1229
|
const resultPromise = new Promise((r) => {
|
|
1147
1230
|
resolve = r;
|
|
@@ -1150,7 +1233,7 @@ var GithubCopilotCliExecutor = class {
|
|
|
1150
1233
|
const settle = (result) => {
|
|
1151
1234
|
if (settled) return;
|
|
1152
1235
|
settled = true;
|
|
1153
|
-
clearTimeout(timer);
|
|
1236
|
+
if (timer) clearTimeout(timer);
|
|
1154
1237
|
resolve?.(result);
|
|
1155
1238
|
};
|
|
1156
1239
|
if (abortSignal?.aborted) {
|
|
@@ -1163,36 +1246,32 @@ var GithubCopilotCliExecutor = class {
|
|
|
1163
1246
|
}
|
|
1164
1247
|
};
|
|
1165
1248
|
}
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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"]
|
|
1249
|
+
writeDiagnostic(
|
|
1250
|
+
`[GithubCopilotCliExecutor] spawn: command=${execution.command}, args=${JSON.stringify(execution.diagnosticArgs)}, promptLen=${execution.promptLen}, cwd=${execution.cwd ?? "(default)"}, platform=${process.platform}, windowsHide=true, pid=${process.pid}
|
|
1251
|
+
`
|
|
1252
|
+
);
|
|
1253
|
+
const child = (0, import_node_child_process2.spawn)(execution.command, execution.args, {
|
|
1254
|
+
cwd: execution.cwd,
|
|
1255
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1256
|
+
windowsHide: true
|
|
1185
1257
|
});
|
|
1186
|
-
|
|
1258
|
+
if (child.pid) {
|
|
1259
|
+
writeDiagnostic(
|
|
1260
|
+
`[GithubCopilotCliExecutor] spawned child PID=${child.pid}
|
|
1261
|
+
`
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
const timeoutMs = execution.timeoutMs;
|
|
1265
|
+
const timer = timeoutMs != null ? setTimeout(() => {
|
|
1187
1266
|
child.kill("SIGTERM");
|
|
1188
1267
|
setTimeout(() => {
|
|
1189
1268
|
if (!child.killed) child.kill("SIGKILL");
|
|
1190
1269
|
}, 5e3);
|
|
1191
1270
|
settle({
|
|
1192
1271
|
status: "timeout",
|
|
1193
|
-
error: `Copilot CLI execution timed out after ${
|
|
1272
|
+
error: `Copilot CLI execution timed out after ${timeoutMs / 1e3}s`
|
|
1194
1273
|
});
|
|
1195
|
-
},
|
|
1274
|
+
}, timeoutMs) : void 0;
|
|
1196
1275
|
let stdoutBuf = "";
|
|
1197
1276
|
let stderrBuf = "";
|
|
1198
1277
|
child.stdout.on("data", (chunk) => {
|
|
@@ -1236,19 +1315,19 @@ var GithubCopilotCliExecutor = class {
|
|
|
1236
1315
|
};
|
|
1237
1316
|
|
|
1238
1317
|
// src/scheduled-tasks/executors/HttpRequestExecutor.ts
|
|
1239
|
-
var
|
|
1318
|
+
var DEFAULT_TIMEOUT_MS3 = 3e4;
|
|
1240
1319
|
var HttpRequestExecutor = class {
|
|
1241
1320
|
async execute(payload, abortSignal) {
|
|
1242
1321
|
const p = payload;
|
|
1243
|
-
const
|
|
1322
|
+
const timeoutMs = resolveConfiguredTimeoutMs(p.timeout, DEFAULT_TIMEOUT_MS3);
|
|
1244
1323
|
const ac = new AbortController();
|
|
1245
|
-
let
|
|
1246
|
-
const timer = setTimeout(() => {
|
|
1247
|
-
|
|
1324
|
+
let timedOut = false;
|
|
1325
|
+
const timer = timeoutMs != null ? setTimeout(() => {
|
|
1326
|
+
timedOut = true;
|
|
1248
1327
|
ac.abort();
|
|
1249
|
-
},
|
|
1328
|
+
}, timeoutMs) : void 0;
|
|
1250
1329
|
if (abortSignal?.aborted) {
|
|
1251
|
-
clearTimeout(timer);
|
|
1330
|
+
if (timer) clearTimeout(timer);
|
|
1252
1331
|
return { status: "cancelled", error: "Execution aborted" };
|
|
1253
1332
|
}
|
|
1254
1333
|
let externalAbort = false;
|
|
@@ -1256,7 +1335,7 @@ var HttpRequestExecutor = class {
|
|
|
1256
1335
|
"abort",
|
|
1257
1336
|
() => {
|
|
1258
1337
|
externalAbort = true;
|
|
1259
|
-
clearTimeout(timer);
|
|
1338
|
+
if (timer) clearTimeout(timer);
|
|
1260
1339
|
ac.abort();
|
|
1261
1340
|
},
|
|
1262
1341
|
{ once: true }
|
|
@@ -1268,7 +1347,7 @@ var HttpRequestExecutor = class {
|
|
|
1268
1347
|
body: p.body,
|
|
1269
1348
|
signal: ac.signal
|
|
1270
1349
|
});
|
|
1271
|
-
clearTimeout(timer);
|
|
1350
|
+
if (timer) clearTimeout(timer);
|
|
1272
1351
|
const body = await response.text();
|
|
1273
1352
|
if (response.ok) {
|
|
1274
1353
|
return {
|
|
@@ -1283,14 +1362,17 @@ ${body}`.trim()
|
|
|
1283
1362
|
${body}`.trim()
|
|
1284
1363
|
};
|
|
1285
1364
|
} catch (err) {
|
|
1286
|
-
clearTimeout(timer);
|
|
1365
|
+
if (timer) clearTimeout(timer);
|
|
1287
1366
|
if (err instanceof Error && err.name === "AbortError") {
|
|
1288
1367
|
if (externalAbort) {
|
|
1289
1368
|
return { status: "cancelled", error: "Execution cancelled" };
|
|
1290
1369
|
}
|
|
1370
|
+
if (!timedOut || timeoutMs == null) {
|
|
1371
|
+
return { status: "failure", error: err.message };
|
|
1372
|
+
}
|
|
1291
1373
|
return {
|
|
1292
1374
|
status: "timeout",
|
|
1293
|
-
error: `HTTP request timed out after ${
|
|
1375
|
+
error: `HTTP request timed out after ${timeoutMs / 1e3}s`
|
|
1294
1376
|
};
|
|
1295
1377
|
}
|
|
1296
1378
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1301,8 +1383,82 @@ ${body}`.trim()
|
|
|
1301
1383
|
|
|
1302
1384
|
// src/scheduled-tasks/executors/ShellExecutor.ts
|
|
1303
1385
|
var import_node_child_process3 = require("child_process");
|
|
1304
|
-
var
|
|
1386
|
+
var fs6 = __toESM(require("fs"));
|
|
1387
|
+
var path5 = __toESM(require("path"));
|
|
1305
1388
|
var MAX_OUTPUT_BYTES2 = 1024 * 1024;
|
|
1389
|
+
var DEFAULT_TIMEOUT_MS4 = 6e4;
|
|
1390
|
+
var POSIX_SHELL_CANDIDATES = ["bash.exe", "sh.exe"];
|
|
1391
|
+
function resolveShellExecution(script, options = {}) {
|
|
1392
|
+
const platform = options.platform ?? process.platform;
|
|
1393
|
+
const env = options.env ?? process.env;
|
|
1394
|
+
const fileExists = options.fileExists ?? fs6.existsSync;
|
|
1395
|
+
if (platform === "win32") {
|
|
1396
|
+
const posixShell = usesPosixShellSyntax(script) ? findWindowsPosixShell(env, fileExists) : null;
|
|
1397
|
+
if (posixShell) {
|
|
1398
|
+
return {
|
|
1399
|
+
command: posixShell,
|
|
1400
|
+
args: ["-s"],
|
|
1401
|
+
stdinScript: script,
|
|
1402
|
+
outputEncoding: "utf8",
|
|
1403
|
+
shellKind: "posix"
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
return {
|
|
1407
|
+
command: env.ComSpec ?? env.COMSPEC ?? "cmd.exe",
|
|
1408
|
+
args: ["/d", "/s", "/c", script],
|
|
1409
|
+
outputEncoding: "utf8",
|
|
1410
|
+
shellKind: "cmd"
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
return {
|
|
1414
|
+
command: "sh",
|
|
1415
|
+
args: ["-s"],
|
|
1416
|
+
stdinScript: script,
|
|
1417
|
+
outputEncoding: "utf8",
|
|
1418
|
+
shellKind: "posix"
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
function usesPosixShellSyntax(script) {
|
|
1422
|
+
return /\$\([^)]*\)|`[^`]*`/.test(script);
|
|
1423
|
+
}
|
|
1424
|
+
function findWindowsPosixShell(env, fileExists) {
|
|
1425
|
+
const explicitShell = env.SERVICEME_POSIX_SHELL;
|
|
1426
|
+
if (explicitShell && fileExists(explicitShell)) {
|
|
1427
|
+
return explicitShell;
|
|
1428
|
+
}
|
|
1429
|
+
const knownGitBashPaths = [
|
|
1430
|
+
"C:\\Program Files\\Git\\bin\\bash.exe",
|
|
1431
|
+
"C:\\Program Files\\Git\\usr\\bin\\bash.exe",
|
|
1432
|
+
"C:\\Program Files (x86)\\Git\\bin\\bash.exe",
|
|
1433
|
+
"C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe"
|
|
1434
|
+
];
|
|
1435
|
+
for (const candidate of knownGitBashPaths) {
|
|
1436
|
+
if (fileExists(candidate)) return candidate;
|
|
1437
|
+
}
|
|
1438
|
+
const pathValue = env.Path ?? env.PATH ?? "";
|
|
1439
|
+
for (const dir of pathValue.split(path5.win32.delimiter)) {
|
|
1440
|
+
if (!dir) continue;
|
|
1441
|
+
for (const executable of POSIX_SHELL_CANDIDATES) {
|
|
1442
|
+
const candidate = path5.win32.join(dir, executable);
|
|
1443
|
+
if (fileExists(candidate) && !isWindowsWslLauncher(candidate)) {
|
|
1444
|
+
return candidate;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
function isWindowsWslLauncher(candidate) {
|
|
1451
|
+
const normalized = path5.win32.normalize(candidate).toLowerCase();
|
|
1452
|
+
return normalized.endsWith("\\windows\\system32\\bash.exe") || normalized.endsWith("\\windows\\syswow64\\bash.exe");
|
|
1453
|
+
}
|
|
1454
|
+
function writeDiagnostic2(message) {
|
|
1455
|
+
const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
|
|
1456
|
+
if (logPath) {
|
|
1457
|
+
fs6.appendFileSync(logPath, message);
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
process.stderr.write(message);
|
|
1461
|
+
}
|
|
1306
1462
|
var ShellExecutor = class {
|
|
1307
1463
|
async execute(payload, abortSignal) {
|
|
1308
1464
|
let output = "";
|
|
@@ -1318,7 +1474,7 @@ var ShellExecutor = class {
|
|
|
1318
1474
|
}
|
|
1319
1475
|
executeStreaming(payload, onOutput, abortSignal) {
|
|
1320
1476
|
const p = payload;
|
|
1321
|
-
const
|
|
1477
|
+
const timeoutMs = resolveConfiguredTimeoutMs(p.timeout, DEFAULT_TIMEOUT_MS4);
|
|
1322
1478
|
let resolve;
|
|
1323
1479
|
const resultPromise = new Promise((r) => {
|
|
1324
1480
|
resolve = r;
|
|
@@ -1327,7 +1483,7 @@ var ShellExecutor = class {
|
|
|
1327
1483
|
const settle = (result) => {
|
|
1328
1484
|
if (settled) return;
|
|
1329
1485
|
settled = true;
|
|
1330
|
-
clearTimeout(timer);
|
|
1486
|
+
if (timer) clearTimeout(timer);
|
|
1331
1487
|
resolve?.(result);
|
|
1332
1488
|
};
|
|
1333
1489
|
if (abortSignal?.aborted) {
|
|
@@ -1340,37 +1496,65 @@ var ShellExecutor = class {
|
|
|
1340
1496
|
}
|
|
1341
1497
|
};
|
|
1342
1498
|
}
|
|
1343
|
-
const
|
|
1499
|
+
const shellExecution = resolveShellExecution(p.script);
|
|
1500
|
+
const spawnOpts = {
|
|
1344
1501
|
cwd: p.cwd,
|
|
1345
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1346
|
-
|
|
1347
|
-
|
|
1502
|
+
stdio: [shellExecution.stdinScript ? "pipe" : "ignore", "pipe", "pipe"],
|
|
1503
|
+
windowsHide: true
|
|
1504
|
+
};
|
|
1505
|
+
writeDiagnostic2(
|
|
1506
|
+
`[ShellExecutor] spawn:
|
|
1507
|
+
platform=${process.platform}
|
|
1508
|
+
shell=${shellExecution.shellKind}
|
|
1509
|
+
command=${shellExecution.command}
|
|
1510
|
+
args=${JSON.stringify(shellExecution.args.filter((a) => a !== p.script))}
|
|
1511
|
+
stdin=${shellExecution.stdinScript ? "true" : "false"}
|
|
1512
|
+
cwd=${p.cwd ?? "(default)"}
|
|
1513
|
+
timeout=${timeoutMs != null ? `${timeoutMs}ms` : "unlimited"}
|
|
1514
|
+
scriptLen=${p.script.length}
|
|
1515
|
+
windowsHide=true
|
|
1516
|
+
daemonPid=${process.pid}
|
|
1517
|
+
`
|
|
1518
|
+
);
|
|
1519
|
+
const child = (0, import_node_child_process3.spawn)(shellExecution.command, shellExecution.args, spawnOpts);
|
|
1520
|
+
if (shellExecution.stdinScript && child.stdin) {
|
|
1521
|
+
child.stdin.end(shellExecution.stdinScript);
|
|
1522
|
+
}
|
|
1523
|
+
if (child.pid) {
|
|
1524
|
+
writeDiagnostic2(`[ShellExecutor] spawned child PID=${child.pid}
|
|
1525
|
+
`);
|
|
1526
|
+
}
|
|
1527
|
+
const timer = timeoutMs != null ? setTimeout(() => {
|
|
1348
1528
|
child.kill("SIGTERM");
|
|
1349
1529
|
setTimeout(() => {
|
|
1350
1530
|
if (!child.killed) child.kill("SIGKILL");
|
|
1351
1531
|
}, 5e3);
|
|
1352
1532
|
settle({
|
|
1353
1533
|
status: "timeout",
|
|
1354
|
-
error: `Shell execution timed out after ${
|
|
1534
|
+
error: `Shell execution timed out after ${timeoutMs / 1e3}s`
|
|
1355
1535
|
});
|
|
1356
|
-
},
|
|
1536
|
+
}, timeoutMs) : void 0;
|
|
1357
1537
|
let stdoutBuf = "";
|
|
1358
1538
|
let stderrBuf = "";
|
|
1359
|
-
child.stdout
|
|
1360
|
-
const data = chunk.toString();
|
|
1539
|
+
child.stdout?.on("data", (chunk) => {
|
|
1540
|
+
const data = chunk.toString(shellExecution.outputEncoding);
|
|
1361
1541
|
stdoutBuf += data;
|
|
1362
1542
|
if (stdoutBuf.length > MAX_OUTPUT_BYTES2)
|
|
1363
1543
|
stdoutBuf = stdoutBuf.slice(-MAX_OUTPUT_BYTES2);
|
|
1364
1544
|
onOutput("stdout", data);
|
|
1365
1545
|
});
|
|
1366
|
-
child.stderr
|
|
1367
|
-
const data = chunk.toString();
|
|
1546
|
+
child.stderr?.on("data", (chunk) => {
|
|
1547
|
+
const data = chunk.toString(shellExecution.outputEncoding);
|
|
1368
1548
|
stderrBuf += data;
|
|
1369
1549
|
if (stderrBuf.length > MAX_OUTPUT_BYTES2)
|
|
1370
1550
|
stderrBuf = stderrBuf.slice(-MAX_OUTPUT_BYTES2);
|
|
1371
1551
|
onOutput("stderr", data);
|
|
1372
1552
|
});
|
|
1373
1553
|
child.on("close", (code) => {
|
|
1554
|
+
writeDiagnostic2(
|
|
1555
|
+
`[ShellExecutor] child PID=${child.pid} exited: code=${code}
|
|
1556
|
+
`
|
|
1557
|
+
);
|
|
1374
1558
|
if (code === 0) {
|
|
1375
1559
|
settle({ status: "success", output: stdoutBuf || void 0 });
|
|
1376
1560
|
} else {
|
|
@@ -1417,36 +1601,65 @@ function getExecutor(taskType) {
|
|
|
1417
1601
|
|
|
1418
1602
|
// src/scheduled-tasks/TaskConfigManager.ts
|
|
1419
1603
|
var import_node_crypto = require("crypto");
|
|
1420
|
-
var
|
|
1421
|
-
var
|
|
1604
|
+
var fs7 = __toESM(require("fs"));
|
|
1605
|
+
var path6 = __toESM(require("path"));
|
|
1606
|
+
var import_devtools_protocol7 = require("@serviceme/devtools-protocol");
|
|
1422
1607
|
var CONFIG_DIR3 = ".serviceme";
|
|
1423
1608
|
var CONFIG_FILE = "scheduled-tasks.json";
|
|
1424
1609
|
function emptyConfig() {
|
|
1425
1610
|
return { version: 1, tasks: [] };
|
|
1426
1611
|
}
|
|
1612
|
+
function isRecord(value) {
|
|
1613
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1614
|
+
}
|
|
1615
|
+
function requireNonEmptyString(payload, field, taskType) {
|
|
1616
|
+
if (!isRecord(payload) || typeof payload[field] !== "string" || !payload[field].trim()) {
|
|
1617
|
+
throw (0, import_devtools_protocol7.createServicemeError)(
|
|
1618
|
+
"invalid_payload",
|
|
1619
|
+
`${taskType} payload ${field} is required`
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
function validateTaskPayload(taskType, payload) {
|
|
1624
|
+
switch (taskType) {
|
|
1625
|
+
case "command":
|
|
1626
|
+
requireNonEmptyString(payload, "command", taskType);
|
|
1627
|
+
return;
|
|
1628
|
+
case "shell":
|
|
1629
|
+
requireNonEmptyString(payload, "script", taskType);
|
|
1630
|
+
return;
|
|
1631
|
+
case "http_request":
|
|
1632
|
+
requireNonEmptyString(payload, "url", taskType);
|
|
1633
|
+
requireNonEmptyString(payload, "method", taskType);
|
|
1634
|
+
return;
|
|
1635
|
+
case "github_copilot_cli":
|
|
1636
|
+
requireNonEmptyString(payload, "prompt", taskType);
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1427
1640
|
var TaskConfigManager = class {
|
|
1428
1641
|
constructor(workspacePath) {
|
|
1429
|
-
this.configPath =
|
|
1642
|
+
this.configPath = path6.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
|
|
1430
1643
|
}
|
|
1431
1644
|
getConfigPath() {
|
|
1432
1645
|
return this.configPath;
|
|
1433
1646
|
}
|
|
1434
1647
|
readConfig() {
|
|
1435
|
-
if (!
|
|
1648
|
+
if (!fs7.existsSync(this.configPath)) {
|
|
1436
1649
|
return emptyConfig();
|
|
1437
1650
|
}
|
|
1438
|
-
const raw =
|
|
1651
|
+
const raw = fs7.readFileSync(this.configPath, "utf-8");
|
|
1439
1652
|
const parsed = JSON.parse(raw);
|
|
1440
1653
|
return parsed;
|
|
1441
1654
|
}
|
|
1442
1655
|
writeConfig(config) {
|
|
1443
|
-
const dir =
|
|
1444
|
-
if (!
|
|
1445
|
-
|
|
1656
|
+
const dir = path6.dirname(this.configPath);
|
|
1657
|
+
if (!fs7.existsSync(dir)) {
|
|
1658
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
1446
1659
|
}
|
|
1447
1660
|
const tmp = `${this.configPath}.tmp`;
|
|
1448
|
-
|
|
1449
|
-
|
|
1661
|
+
fs7.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
|
|
1662
|
+
fs7.renameSync(tmp, this.configPath);
|
|
1450
1663
|
}
|
|
1451
1664
|
listTasks() {
|
|
1452
1665
|
return this.readConfig().tasks;
|
|
@@ -1458,6 +1671,7 @@ var TaskConfigManager = class {
|
|
|
1458
1671
|
return this.readConfig().tasks.find((t) => t.name === name);
|
|
1459
1672
|
}
|
|
1460
1673
|
createTask(input) {
|
|
1674
|
+
validateTaskPayload(input.taskType, input.payload);
|
|
1461
1675
|
const config = this.readConfig();
|
|
1462
1676
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1463
1677
|
const task = {
|
|
@@ -1486,6 +1700,9 @@ var TaskConfigManager = class {
|
|
|
1486
1700
|
if (!existing) {
|
|
1487
1701
|
throw new Error(`Task '${id}' not found`);
|
|
1488
1702
|
}
|
|
1703
|
+
const nextTaskType = input.taskType ?? existing.taskType;
|
|
1704
|
+
const nextPayload = input.payload ?? existing.payload;
|
|
1705
|
+
validateTaskPayload(nextTaskType, nextPayload);
|
|
1489
1706
|
const updated = {
|
|
1490
1707
|
...existing,
|
|
1491
1708
|
...input.name !== void 0 && { name: input.name },
|
|
@@ -1524,10 +1741,197 @@ var TaskConfigManager = class {
|
|
|
1524
1741
|
}
|
|
1525
1742
|
};
|
|
1526
1743
|
|
|
1744
|
+
// src/scheduled-tasks/TaskExecutionEngine.ts
|
|
1745
|
+
function hasNonEmptyString(value) {
|
|
1746
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
1747
|
+
}
|
|
1748
|
+
function resolveTaskExecutionPayload(taskType, payload, workspacePath) {
|
|
1749
|
+
if (!hasNonEmptyString(workspacePath)) {
|
|
1750
|
+
return payload;
|
|
1751
|
+
}
|
|
1752
|
+
if (taskType === "shell") {
|
|
1753
|
+
const shellPayload = payload;
|
|
1754
|
+
if (hasNonEmptyString(shellPayload.cwd)) {
|
|
1755
|
+
return shellPayload;
|
|
1756
|
+
}
|
|
1757
|
+
return { ...shellPayload, cwd: workspacePath };
|
|
1758
|
+
}
|
|
1759
|
+
if (taskType === "github_copilot_cli") {
|
|
1760
|
+
const copilotPayload = payload;
|
|
1761
|
+
if (hasNonEmptyString(copilotPayload.workspace)) {
|
|
1762
|
+
return copilotPayload;
|
|
1763
|
+
}
|
|
1764
|
+
return { ...copilotPayload, workspace: workspacePath };
|
|
1765
|
+
}
|
|
1766
|
+
return payload;
|
|
1767
|
+
}
|
|
1768
|
+
var TaskExecutionEngine = class {
|
|
1769
|
+
constructor(getExecutor2) {
|
|
1770
|
+
this.getExecutor = getExecutor2;
|
|
1771
|
+
this.running = /* @__PURE__ */ new Map();
|
|
1772
|
+
this.taskExecutions = /* @__PURE__ */ new Map();
|
|
1773
|
+
this.listener = null;
|
|
1774
|
+
}
|
|
1775
|
+
setListener(listener) {
|
|
1776
|
+
this.listener = listener;
|
|
1777
|
+
}
|
|
1778
|
+
async execute(snapshot) {
|
|
1779
|
+
const policy = snapshot.concurrencyPolicy ?? "reject";
|
|
1780
|
+
if (policy === "reject") {
|
|
1781
|
+
const existingIds = this.taskExecutions.get(snapshot.taskId);
|
|
1782
|
+
if (existingIds && existingIds.size > 0) {
|
|
1783
|
+
throw new Error(
|
|
1784
|
+
`TASK_ALREADY_RUNNING: Task ${snapshot.taskId} is already running`
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1789
|
+
if (!this.taskExecutions.has(snapshot.taskId)) {
|
|
1790
|
+
this.taskExecutions.set(snapshot.taskId, /* @__PURE__ */ new Set());
|
|
1791
|
+
}
|
|
1792
|
+
this.taskExecutions.get(snapshot.taskId)?.add(snapshot.executionId);
|
|
1793
|
+
const ac = new AbortController();
|
|
1794
|
+
const runningExec = {
|
|
1795
|
+
executionId: snapshot.executionId,
|
|
1796
|
+
taskId: snapshot.taskId,
|
|
1797
|
+
startedAt,
|
|
1798
|
+
cancel: () => ac.abort()
|
|
1799
|
+
};
|
|
1800
|
+
this.running.set(snapshot.executionId, runningExec);
|
|
1801
|
+
this.listener?.onStarted({
|
|
1802
|
+
executionId: snapshot.executionId,
|
|
1803
|
+
taskId: snapshot.taskId,
|
|
1804
|
+
startedAt
|
|
1805
|
+
});
|
|
1806
|
+
try {
|
|
1807
|
+
const executionPayload = resolveTaskExecutionPayload(
|
|
1808
|
+
snapshot.type,
|
|
1809
|
+
snapshot.payload,
|
|
1810
|
+
snapshot.workspacePath
|
|
1811
|
+
);
|
|
1812
|
+
validateTaskPayload(snapshot.type, executionPayload);
|
|
1813
|
+
const executor = this.getExecutor(snapshot.type);
|
|
1814
|
+
let result;
|
|
1815
|
+
if (isStreamingTaskExecutor(executor)) {
|
|
1816
|
+
const handle = executor.executeStreaming(
|
|
1817
|
+
executionPayload,
|
|
1818
|
+
(stream, data) => {
|
|
1819
|
+
this.listener?.onOutput({
|
|
1820
|
+
executionId: snapshot.executionId,
|
|
1821
|
+
stream,
|
|
1822
|
+
data
|
|
1823
|
+
});
|
|
1824
|
+
},
|
|
1825
|
+
ac.signal
|
|
1826
|
+
);
|
|
1827
|
+
runningExec.cancel = () => {
|
|
1828
|
+
handle.cancel();
|
|
1829
|
+
ac.abort();
|
|
1830
|
+
};
|
|
1831
|
+
result = await handle.result;
|
|
1832
|
+
} else {
|
|
1833
|
+
result = await executor.execute(executionPayload, ac.signal);
|
|
1834
|
+
}
|
|
1835
|
+
const log = {
|
|
1836
|
+
id: snapshot.executionId,
|
|
1837
|
+
taskId: snapshot.taskId,
|
|
1838
|
+
taskName: snapshot.name,
|
|
1839
|
+
startedAt,
|
|
1840
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1841
|
+
status: result.status === "running" ? "failure" : result.status,
|
|
1842
|
+
output: result.output,
|
|
1843
|
+
error: result.error
|
|
1844
|
+
};
|
|
1845
|
+
switch (log.status) {
|
|
1846
|
+
case "success":
|
|
1847
|
+
this.listener?.onCompleted({
|
|
1848
|
+
executionId: snapshot.executionId,
|
|
1849
|
+
log
|
|
1850
|
+
});
|
|
1851
|
+
break;
|
|
1852
|
+
case "cancelled":
|
|
1853
|
+
this.listener?.onCancelled({
|
|
1854
|
+
executionId: snapshot.executionId,
|
|
1855
|
+
log
|
|
1856
|
+
});
|
|
1857
|
+
break;
|
|
1858
|
+
case "timeout":
|
|
1859
|
+
this.listener?.onFailed({
|
|
1860
|
+
executionId: snapshot.executionId,
|
|
1861
|
+
status: "timeout",
|
|
1862
|
+
reason: "timeout",
|
|
1863
|
+
log
|
|
1864
|
+
});
|
|
1865
|
+
break;
|
|
1866
|
+
default:
|
|
1867
|
+
this.listener?.onFailed({
|
|
1868
|
+
executionId: snapshot.executionId,
|
|
1869
|
+
status: "failure",
|
|
1870
|
+
reason: "error",
|
|
1871
|
+
log
|
|
1872
|
+
});
|
|
1873
|
+
break;
|
|
1874
|
+
}
|
|
1875
|
+
} catch (err) {
|
|
1876
|
+
const log = {
|
|
1877
|
+
id: snapshot.executionId,
|
|
1878
|
+
taskId: snapshot.taskId,
|
|
1879
|
+
taskName: snapshot.name,
|
|
1880
|
+
startedAt,
|
|
1881
|
+
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1882
|
+
status: ac.signal.aborted ? "cancelled" : "failure",
|
|
1883
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1884
|
+
};
|
|
1885
|
+
if (ac.signal.aborted) {
|
|
1886
|
+
this.listener?.onCancelled({
|
|
1887
|
+
executionId: snapshot.executionId,
|
|
1888
|
+
log
|
|
1889
|
+
});
|
|
1890
|
+
} else {
|
|
1891
|
+
this.listener?.onFailed({
|
|
1892
|
+
executionId: snapshot.executionId,
|
|
1893
|
+
status: "failure",
|
|
1894
|
+
reason: "error",
|
|
1895
|
+
log
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
} finally {
|
|
1899
|
+
this.running.delete(snapshot.executionId);
|
|
1900
|
+
const taskExecIds = this.taskExecutions.get(snapshot.taskId);
|
|
1901
|
+
if (taskExecIds) {
|
|
1902
|
+
taskExecIds.delete(snapshot.executionId);
|
|
1903
|
+
if (taskExecIds.size === 0) {
|
|
1904
|
+
this.taskExecutions.delete(snapshot.taskId);
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
cancel(executionId) {
|
|
1910
|
+
const exec = this.running.get(executionId);
|
|
1911
|
+
if (!exec) return false;
|
|
1912
|
+
exec.cancel();
|
|
1913
|
+
return true;
|
|
1914
|
+
}
|
|
1915
|
+
listRunning() {
|
|
1916
|
+
return Array.from(this.running.values()).map((e) => ({
|
|
1917
|
+
executionId: e.executionId,
|
|
1918
|
+
taskId: e.taskId,
|
|
1919
|
+
startedAt: e.startedAt
|
|
1920
|
+
}));
|
|
1921
|
+
}
|
|
1922
|
+
dispose() {
|
|
1923
|
+
for (const exec of this.running.values()) {
|
|
1924
|
+
exec.cancel();
|
|
1925
|
+
}
|
|
1926
|
+
this.running.clear();
|
|
1927
|
+
this.taskExecutions.clear();
|
|
1928
|
+
}
|
|
1929
|
+
};
|
|
1930
|
+
|
|
1527
1931
|
// src/scheduled-tasks/TaskLogManager.ts
|
|
1528
1932
|
var import_node_crypto2 = require("crypto");
|
|
1529
|
-
var
|
|
1530
|
-
var
|
|
1933
|
+
var fs8 = __toESM(require("fs"));
|
|
1934
|
+
var path7 = __toESM(require("path"));
|
|
1531
1935
|
var CONFIG_DIR4 = ".serviceme";
|
|
1532
1936
|
var LOG_FILE2 = "scheduled-tasks-log.json";
|
|
1533
1937
|
var MAX_LOGS = 200;
|
|
@@ -1552,17 +1956,17 @@ function validateAndRepairLogFile(raw) {
|
|
|
1552
1956
|
}
|
|
1553
1957
|
var TaskLogManager = class {
|
|
1554
1958
|
constructor(workspacePath) {
|
|
1555
|
-
this.logPath =
|
|
1959
|
+
this.logPath = path7.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
|
|
1556
1960
|
}
|
|
1557
1961
|
getLogPath() {
|
|
1558
1962
|
return this.logPath;
|
|
1559
1963
|
}
|
|
1560
1964
|
readLogFile() {
|
|
1561
|
-
if (!
|
|
1965
|
+
if (!fs8.existsSync(this.logPath)) {
|
|
1562
1966
|
return emptyLogFile();
|
|
1563
1967
|
}
|
|
1564
1968
|
try {
|
|
1565
|
-
const raw =
|
|
1969
|
+
const raw = fs8.readFileSync(this.logPath, "utf-8");
|
|
1566
1970
|
const parsed = JSON.parse(raw);
|
|
1567
1971
|
const file = validateAndRepairLogFile(parsed);
|
|
1568
1972
|
if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.logs) || parsed.logs.length !== file.logs.length) {
|
|
@@ -1578,21 +1982,21 @@ var TaskLogManager = class {
|
|
|
1578
1982
|
}
|
|
1579
1983
|
backupCorruptedFile() {
|
|
1580
1984
|
try {
|
|
1581
|
-
if (
|
|
1985
|
+
if (fs8.existsSync(this.logPath)) {
|
|
1582
1986
|
const backupPath = `${this.logPath}.corrupted.${Date.now()}`;
|
|
1583
|
-
|
|
1987
|
+
fs8.copyFileSync(this.logPath, backupPath);
|
|
1584
1988
|
}
|
|
1585
1989
|
} catch {
|
|
1586
1990
|
}
|
|
1587
1991
|
}
|
|
1588
1992
|
writeLogFile(file) {
|
|
1589
|
-
const dir =
|
|
1590
|
-
if (!
|
|
1591
|
-
|
|
1993
|
+
const dir = path7.dirname(this.logPath);
|
|
1994
|
+
if (!fs8.existsSync(dir)) {
|
|
1995
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
1592
1996
|
}
|
|
1593
1997
|
const tmp = `${this.logPath}.tmp`;
|
|
1594
|
-
|
|
1595
|
-
|
|
1998
|
+
fs8.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
|
|
1999
|
+
fs8.renameSync(tmp, this.logPath);
|
|
1596
2000
|
}
|
|
1597
2001
|
appendLog(input) {
|
|
1598
2002
|
const file = this.readLogFile();
|
|
@@ -1662,7 +2066,23 @@ var SchedulerDaemon = class {
|
|
|
1662
2066
|
this.running = true;
|
|
1663
2067
|
this.startTime = Date.now();
|
|
1664
2068
|
this.pidManager.writePid(process.pid);
|
|
1665
|
-
this.
|
|
2069
|
+
const configPath = this.configManager.getConfigPath();
|
|
2070
|
+
const logPath = this.logger.getLogPath();
|
|
2071
|
+
this.logger.log("info", "=".repeat(60));
|
|
2072
|
+
this.logger.log(
|
|
2073
|
+
"info",
|
|
2074
|
+
`Scheduler daemon started
|
|
2075
|
+
PID: ${process.pid}
|
|
2076
|
+
Node: ${process.version}
|
|
2077
|
+
OS: ${os.type()} ${os.release()} (${process.arch})
|
|
2078
|
+
Platform: ${process.platform}
|
|
2079
|
+
Hostname: ${os.hostname()}
|
|
2080
|
+
User: ${os.userInfo().username}
|
|
2081
|
+
Workspace: ${this.workspacePath}
|
|
2082
|
+
Config: ${configPath}
|
|
2083
|
+
LogPath: ${logPath}
|
|
2084
|
+
ExecPath: ${process.execPath}`
|
|
2085
|
+
);
|
|
1666
2086
|
this.tickTimer = setInterval(() => this.tick(), TICK_INTERVAL);
|
|
1667
2087
|
this.setupConfigWatch();
|
|
1668
2088
|
process.on("SIGTERM", () => this.stop());
|
|
@@ -1687,8 +2107,8 @@ var SchedulerDaemon = class {
|
|
|
1687
2107
|
const configPath = this.configManager.getConfigPath();
|
|
1688
2108
|
const dir = configPath.substring(0, configPath.lastIndexOf("/"));
|
|
1689
2109
|
try {
|
|
1690
|
-
if (
|
|
1691
|
-
this.watcher =
|
|
2110
|
+
if (fs9.existsSync(dir)) {
|
|
2111
|
+
this.watcher = fs9.watch(dir, (_eventType, filename) => {
|
|
1692
2112
|
if (filename === "scheduled-tasks.json") {
|
|
1693
2113
|
this.logger.log("info", "Config file changed, reconciling...");
|
|
1694
2114
|
}
|
|
@@ -1704,7 +2124,11 @@ var SchedulerDaemon = class {
|
|
|
1704
2124
|
for (const task of config.tasks) {
|
|
1705
2125
|
if (!task.enabled) continue;
|
|
1706
2126
|
if (this.taskRunning.has(task.id)) continue;
|
|
1707
|
-
|
|
2127
|
+
let lastExec = this.lastRun.get(task.id);
|
|
2128
|
+
if (lastExec === void 0) {
|
|
2129
|
+
this.lastRun.set(task.id, now);
|
|
2130
|
+
lastExec = now;
|
|
2131
|
+
}
|
|
1708
2132
|
if (this.shouldRun(task, lastExec, now)) {
|
|
1709
2133
|
this.executeTask(task, now);
|
|
1710
2134
|
}
|
|
@@ -1726,11 +2150,25 @@ var SchedulerDaemon = class {
|
|
|
1726
2150
|
this.taskRunning.add(task.id);
|
|
1727
2151
|
this.lastRun.set(task.id, now);
|
|
1728
2152
|
const startedAt = new Date(now).toISOString();
|
|
1729
|
-
|
|
2153
|
+
const startMs = Date.now();
|
|
2154
|
+
this.logger.log(
|
|
2155
|
+
"info",
|
|
2156
|
+
`Executing task: ${task.name} (${task.id})
|
|
2157
|
+
type: ${task.taskType}
|
|
2158
|
+
schedule: ${task.schedule} (${task.scheduleType})
|
|
2159
|
+
enabled: ${task.enabled}`
|
|
2160
|
+
);
|
|
1730
2161
|
try {
|
|
2162
|
+
const executionPayload = resolveTaskExecutionPayload(
|
|
2163
|
+
task.taskType,
|
|
2164
|
+
task.payload,
|
|
2165
|
+
this.workspacePath
|
|
2166
|
+
);
|
|
2167
|
+
validateTaskPayload(task.taskType, executionPayload);
|
|
1731
2168
|
const executor = getExecutor(task.taskType);
|
|
1732
|
-
const result = await executor.execute(
|
|
2169
|
+
const result = await executor.execute(executionPayload);
|
|
1733
2170
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2171
|
+
const durationMs = Date.now() - startMs;
|
|
1734
2172
|
this.logManager.appendLog({
|
|
1735
2173
|
taskId: task.id,
|
|
1736
2174
|
taskName: task.name,
|
|
@@ -1740,9 +2178,17 @@ var SchedulerDaemon = class {
|
|
|
1740
2178
|
output: result.output,
|
|
1741
2179
|
error: result.error
|
|
1742
2180
|
});
|
|
1743
|
-
|
|
2181
|
+
const outputBytes = result.output ? Buffer.byteLength(result.output) : 0;
|
|
2182
|
+
this.logger.log(
|
|
2183
|
+
"info",
|
|
2184
|
+
`Task completed: ${task.name} (${task.id})
|
|
2185
|
+
status: ${result.status}
|
|
2186
|
+
duration: ${durationMs}ms
|
|
2187
|
+
outputBytes: ${outputBytes}`
|
|
2188
|
+
);
|
|
1744
2189
|
} catch (err) {
|
|
1745
2190
|
const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2191
|
+
const durationMs = Date.now() - startMs;
|
|
1746
2192
|
const message = err instanceof Error ? err.message : String(err);
|
|
1747
2193
|
this.logManager.appendLog({
|
|
1748
2194
|
taskId: task.id,
|
|
@@ -1752,7 +2198,12 @@ var SchedulerDaemon = class {
|
|
|
1752
2198
|
status: "failure",
|
|
1753
2199
|
error: message
|
|
1754
2200
|
});
|
|
1755
|
-
this.logger.log(
|
|
2201
|
+
this.logger.log(
|
|
2202
|
+
"error",
|
|
2203
|
+
`Task failed: ${task.name} (${task.id})
|
|
2204
|
+
duration: ${durationMs}ms
|
|
2205
|
+
error: ${message}`
|
|
2206
|
+
);
|
|
1756
2207
|
} finally {
|
|
1757
2208
|
this.taskRunning.delete(task.id);
|
|
1758
2209
|
}
|
|
@@ -1809,164 +2260,6 @@ function matchCronField(field, value) {
|
|
|
1809
2260
|
const values = field.split(",");
|
|
1810
2261
|
return values.some((v) => Number.parseInt(v, 10) === value);
|
|
1811
2262
|
}
|
|
1812
|
-
|
|
1813
|
-
// src/scheduled-tasks/TaskExecutionEngine.ts
|
|
1814
|
-
var TaskExecutionEngine = class {
|
|
1815
|
-
constructor(getExecutor2) {
|
|
1816
|
-
this.getExecutor = getExecutor2;
|
|
1817
|
-
this.running = /* @__PURE__ */ new Map();
|
|
1818
|
-
this.taskExecutions = /* @__PURE__ */ new Map();
|
|
1819
|
-
this.listener = null;
|
|
1820
|
-
}
|
|
1821
|
-
setListener(listener) {
|
|
1822
|
-
this.listener = listener;
|
|
1823
|
-
}
|
|
1824
|
-
async execute(snapshot) {
|
|
1825
|
-
const policy = snapshot.concurrencyPolicy ?? "reject";
|
|
1826
|
-
if (policy === "reject") {
|
|
1827
|
-
const existingIds = this.taskExecutions.get(snapshot.taskId);
|
|
1828
|
-
if (existingIds && existingIds.size > 0) {
|
|
1829
|
-
throw new Error(
|
|
1830
|
-
`TASK_ALREADY_RUNNING: Task ${snapshot.taskId} is already running`
|
|
1831
|
-
);
|
|
1832
|
-
}
|
|
1833
|
-
}
|
|
1834
|
-
const executor = this.getExecutor(snapshot.type);
|
|
1835
|
-
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1836
|
-
if (!this.taskExecutions.has(snapshot.taskId)) {
|
|
1837
|
-
this.taskExecutions.set(snapshot.taskId, /* @__PURE__ */ new Set());
|
|
1838
|
-
}
|
|
1839
|
-
this.taskExecutions.get(snapshot.taskId)?.add(snapshot.executionId);
|
|
1840
|
-
const ac = new AbortController();
|
|
1841
|
-
const runningExec = {
|
|
1842
|
-
executionId: snapshot.executionId,
|
|
1843
|
-
taskId: snapshot.taskId,
|
|
1844
|
-
startedAt,
|
|
1845
|
-
cancel: () => ac.abort()
|
|
1846
|
-
};
|
|
1847
|
-
this.running.set(snapshot.executionId, runningExec);
|
|
1848
|
-
this.listener?.onStarted({
|
|
1849
|
-
executionId: snapshot.executionId,
|
|
1850
|
-
taskId: snapshot.taskId,
|
|
1851
|
-
startedAt
|
|
1852
|
-
});
|
|
1853
|
-
try {
|
|
1854
|
-
let result;
|
|
1855
|
-
if (isStreamingTaskExecutor(executor)) {
|
|
1856
|
-
const handle = executor.executeStreaming(
|
|
1857
|
-
snapshot.payload,
|
|
1858
|
-
(stream, data) => {
|
|
1859
|
-
this.listener?.onOutput({
|
|
1860
|
-
executionId: snapshot.executionId,
|
|
1861
|
-
stream,
|
|
1862
|
-
data
|
|
1863
|
-
});
|
|
1864
|
-
},
|
|
1865
|
-
ac.signal
|
|
1866
|
-
);
|
|
1867
|
-
runningExec.cancel = () => {
|
|
1868
|
-
handle.cancel();
|
|
1869
|
-
ac.abort();
|
|
1870
|
-
};
|
|
1871
|
-
result = await handle.result;
|
|
1872
|
-
} else {
|
|
1873
|
-
result = await executor.execute(snapshot.payload, ac.signal);
|
|
1874
|
-
}
|
|
1875
|
-
const log = {
|
|
1876
|
-
id: snapshot.executionId,
|
|
1877
|
-
taskId: snapshot.taskId,
|
|
1878
|
-
taskName: snapshot.name,
|
|
1879
|
-
startedAt,
|
|
1880
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1881
|
-
status: result.status === "running" ? "failure" : result.status,
|
|
1882
|
-
output: result.output,
|
|
1883
|
-
error: result.error
|
|
1884
|
-
};
|
|
1885
|
-
switch (log.status) {
|
|
1886
|
-
case "success":
|
|
1887
|
-
this.listener?.onCompleted({
|
|
1888
|
-
executionId: snapshot.executionId,
|
|
1889
|
-
log
|
|
1890
|
-
});
|
|
1891
|
-
break;
|
|
1892
|
-
case "cancelled":
|
|
1893
|
-
this.listener?.onCancelled({
|
|
1894
|
-
executionId: snapshot.executionId,
|
|
1895
|
-
log
|
|
1896
|
-
});
|
|
1897
|
-
break;
|
|
1898
|
-
case "timeout":
|
|
1899
|
-
this.listener?.onFailed({
|
|
1900
|
-
executionId: snapshot.executionId,
|
|
1901
|
-
status: "timeout",
|
|
1902
|
-
reason: "timeout",
|
|
1903
|
-
log
|
|
1904
|
-
});
|
|
1905
|
-
break;
|
|
1906
|
-
default:
|
|
1907
|
-
this.listener?.onFailed({
|
|
1908
|
-
executionId: snapshot.executionId,
|
|
1909
|
-
status: "failure",
|
|
1910
|
-
reason: "error",
|
|
1911
|
-
log
|
|
1912
|
-
});
|
|
1913
|
-
break;
|
|
1914
|
-
}
|
|
1915
|
-
} catch (err) {
|
|
1916
|
-
const log = {
|
|
1917
|
-
id: snapshot.executionId,
|
|
1918
|
-
taskId: snapshot.taskId,
|
|
1919
|
-
taskName: snapshot.name,
|
|
1920
|
-
startedAt,
|
|
1921
|
-
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1922
|
-
status: ac.signal.aborted ? "cancelled" : "failure",
|
|
1923
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1924
|
-
};
|
|
1925
|
-
if (ac.signal.aborted) {
|
|
1926
|
-
this.listener?.onCancelled({
|
|
1927
|
-
executionId: snapshot.executionId,
|
|
1928
|
-
log
|
|
1929
|
-
});
|
|
1930
|
-
} else {
|
|
1931
|
-
this.listener?.onFailed({
|
|
1932
|
-
executionId: snapshot.executionId,
|
|
1933
|
-
status: "failure",
|
|
1934
|
-
reason: "error",
|
|
1935
|
-
log
|
|
1936
|
-
});
|
|
1937
|
-
}
|
|
1938
|
-
} finally {
|
|
1939
|
-
this.running.delete(snapshot.executionId);
|
|
1940
|
-
const taskExecIds = this.taskExecutions.get(snapshot.taskId);
|
|
1941
|
-
if (taskExecIds) {
|
|
1942
|
-
taskExecIds.delete(snapshot.executionId);
|
|
1943
|
-
if (taskExecIds.size === 0) {
|
|
1944
|
-
this.taskExecutions.delete(snapshot.taskId);
|
|
1945
|
-
}
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
cancel(executionId) {
|
|
1950
|
-
const exec = this.running.get(executionId);
|
|
1951
|
-
if (!exec) return false;
|
|
1952
|
-
exec.cancel();
|
|
1953
|
-
return true;
|
|
1954
|
-
}
|
|
1955
|
-
listRunning() {
|
|
1956
|
-
return Array.from(this.running.values()).map((e) => ({
|
|
1957
|
-
executionId: e.executionId,
|
|
1958
|
-
taskId: e.taskId,
|
|
1959
|
-
startedAt: e.startedAt
|
|
1960
|
-
}));
|
|
1961
|
-
}
|
|
1962
|
-
dispose() {
|
|
1963
|
-
for (const exec of this.running.values()) {
|
|
1964
|
-
exec.cancel();
|
|
1965
|
-
}
|
|
1966
|
-
this.running.clear();
|
|
1967
|
-
this.taskExecutions.clear();
|
|
1968
|
-
}
|
|
1969
|
-
};
|
|
1970
2263
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1971
2264
|
0 && (module.exports = {
|
|
1972
2265
|
DaemonLogger,
|
|
@@ -1996,5 +2289,7 @@ var TaskExecutionEngine = class {
|
|
|
1996
2289
|
moveFiles,
|
|
1997
2290
|
noopLogger,
|
|
1998
2291
|
parseIntervalMs,
|
|
1999
|
-
|
|
2292
|
+
resolveTaskExecutionPayload,
|
|
2293
|
+
unzipFile,
|
|
2294
|
+
validateTaskPayload
|
|
2000
2295
|
});
|