@serviceme/devtools-core 0.1.3 → 0.1.4

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.mjs CHANGED
@@ -39,7 +39,8 @@ async function runCommand(command, options = {}) {
39
39
  cwd: options.cwd,
40
40
  env: options.env,
41
41
  shell: options.shell,
42
- stdio: "pipe"
42
+ stdio: "pipe",
43
+ windowsHide: true
43
44
  });
44
45
  let stdout = "";
45
46
  let stderr = "";
@@ -1081,12 +1082,51 @@ var PidManager = class {
1081
1082
  };
1082
1083
 
1083
1084
  // src/scheduled-tasks/daemon/SchedulerDaemon.ts
1084
- import * as fs7 from "fs";
1085
+ import * as fs9 from "fs";
1086
+ import * as os from "os";
1085
1087
 
1086
1088
  // src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
1087
1089
  import { spawn as spawn2 } from "child_process";
1088
- var DEFAULT_TIMEOUT = 3e5;
1090
+ import * as fs5 from "fs";
1091
+
1092
+ // src/scheduled-tasks/executors/timeout.ts
1093
+ function resolveConfiguredTimeoutMs(timeoutSeconds, defaultTimeoutMs) {
1094
+ if (timeoutSeconds === 0) {
1095
+ return void 0;
1096
+ }
1097
+ if (typeof timeoutSeconds !== "number" || !Number.isFinite(timeoutSeconds) || timeoutSeconds < 0) {
1098
+ return defaultTimeoutMs;
1099
+ }
1100
+ return timeoutSeconds * 1e3;
1101
+ }
1102
+
1103
+ // src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
1089
1104
  var MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
1105
+ var DEFAULT_TIMEOUT_MS2 = 3e5;
1106
+ function redactArgs(args) {
1107
+ const redacted = [];
1108
+ let redactNext = false;
1109
+ for (const arg of args) {
1110
+ if (redactNext) {
1111
+ redacted.push("<redacted>");
1112
+ redactNext = false;
1113
+ continue;
1114
+ }
1115
+ redacted.push(arg);
1116
+ if (arg === "--prompt") {
1117
+ redactNext = true;
1118
+ }
1119
+ }
1120
+ return redacted;
1121
+ }
1122
+ function writeDiagnostic(message) {
1123
+ const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
1124
+ if (logPath) {
1125
+ fs5.appendFileSync(logPath, message);
1126
+ return;
1127
+ }
1128
+ process.stderr.write(message);
1129
+ }
1090
1130
  var GithubCopilotCliExecutor = class {
1091
1131
  async execute(payload, abortSignal) {
1092
1132
  const authenticated = await isCopilotAuthenticated();
@@ -1109,7 +1149,7 @@ var GithubCopilotCliExecutor = class {
1109
1149
  }
1110
1150
  executeStreaming(payload, onOutput, abortSignal) {
1111
1151
  const p = payload;
1112
- const timeout = (p.timeout != null ? p.timeout * 1e3 : null) ?? DEFAULT_TIMEOUT;
1152
+ const timeoutMs = resolveConfiguredTimeoutMs(p.timeout, DEFAULT_TIMEOUT_MS2);
1113
1153
  let resolve;
1114
1154
  const resultPromise = new Promise((r) => {
1115
1155
  resolve = r;
@@ -1118,7 +1158,7 @@ var GithubCopilotCliExecutor = class {
1118
1158
  const settle = (result) => {
1119
1159
  if (settled) return;
1120
1160
  settled = true;
1121
- clearTimeout(timer);
1161
+ if (timer) clearTimeout(timer);
1122
1162
  resolve?.(result);
1123
1163
  };
1124
1164
  if (abortSignal?.aborted) {
@@ -1144,23 +1184,32 @@ var GithubCopilotCliExecutor = class {
1144
1184
  if (p.agent) {
1145
1185
  args.push("--agent", p.agent);
1146
1186
  }
1147
- if (p.timeout != null) {
1148
- args.push("--timeout", String(p.timeout));
1149
- }
1187
+ args.push("--timeout", String(p.timeout ?? 0));
1188
+ writeDiagnostic(
1189
+ `[GithubCopilotCliExecutor] spawn: command=serviceme, args=${JSON.stringify(redactArgs(args))}, promptLen=${p.prompt.length}, cwd=${p.workspace ?? "(default)"}, platform=${process.platform}, windowsHide=true, pid=${process.pid}
1190
+ `
1191
+ );
1150
1192
  const child = spawn2("serviceme", args, {
1151
1193
  cwd: p.workspace,
1152
- stdio: ["ignore", "pipe", "pipe"]
1194
+ stdio: ["ignore", "pipe", "pipe"],
1195
+ windowsHide: true
1153
1196
  });
1154
- const timer = setTimeout(() => {
1197
+ if (child.pid) {
1198
+ writeDiagnostic(
1199
+ `[GithubCopilotCliExecutor] spawned child PID=${child.pid}
1200
+ `
1201
+ );
1202
+ }
1203
+ const timer = timeoutMs != null ? setTimeout(() => {
1155
1204
  child.kill("SIGTERM");
1156
1205
  setTimeout(() => {
1157
1206
  if (!child.killed) child.kill("SIGKILL");
1158
1207
  }, 5e3);
1159
1208
  settle({
1160
1209
  status: "timeout",
1161
- error: `Copilot CLI execution timed out after ${timeout / 1e3}s`
1210
+ error: `Copilot CLI execution timed out after ${timeoutMs / 1e3}s`
1162
1211
  });
1163
- }, timeout);
1212
+ }, timeoutMs) : void 0;
1164
1213
  let stdoutBuf = "";
1165
1214
  let stderrBuf = "";
1166
1215
  child.stdout.on("data", (chunk) => {
@@ -1204,19 +1253,19 @@ var GithubCopilotCliExecutor = class {
1204
1253
  };
1205
1254
 
1206
1255
  // src/scheduled-tasks/executors/HttpRequestExecutor.ts
1207
- var DEFAULT_TIMEOUT2 = 3e4;
1256
+ var DEFAULT_TIMEOUT_MS3 = 3e4;
1208
1257
  var HttpRequestExecutor = class {
1209
1258
  async execute(payload, abortSignal) {
1210
1259
  const p = payload;
1211
- const timeout = (p.timeout != null ? p.timeout * 1e3 : null) ?? DEFAULT_TIMEOUT2;
1260
+ const timeoutMs = resolveConfiguredTimeoutMs(p.timeout, DEFAULT_TIMEOUT_MS3);
1212
1261
  const ac = new AbortController();
1213
- let _timedOut = false;
1214
- const timer = setTimeout(() => {
1215
- _timedOut = true;
1262
+ let timedOut = false;
1263
+ const timer = timeoutMs != null ? setTimeout(() => {
1264
+ timedOut = true;
1216
1265
  ac.abort();
1217
- }, timeout);
1266
+ }, timeoutMs) : void 0;
1218
1267
  if (abortSignal?.aborted) {
1219
- clearTimeout(timer);
1268
+ if (timer) clearTimeout(timer);
1220
1269
  return { status: "cancelled", error: "Execution aborted" };
1221
1270
  }
1222
1271
  let externalAbort = false;
@@ -1224,7 +1273,7 @@ var HttpRequestExecutor = class {
1224
1273
  "abort",
1225
1274
  () => {
1226
1275
  externalAbort = true;
1227
- clearTimeout(timer);
1276
+ if (timer) clearTimeout(timer);
1228
1277
  ac.abort();
1229
1278
  },
1230
1279
  { once: true }
@@ -1236,7 +1285,7 @@ var HttpRequestExecutor = class {
1236
1285
  body: p.body,
1237
1286
  signal: ac.signal
1238
1287
  });
1239
- clearTimeout(timer);
1288
+ if (timer) clearTimeout(timer);
1240
1289
  const body = await response.text();
1241
1290
  if (response.ok) {
1242
1291
  return {
@@ -1251,14 +1300,17 @@ ${body}`.trim()
1251
1300
  ${body}`.trim()
1252
1301
  };
1253
1302
  } catch (err) {
1254
- clearTimeout(timer);
1303
+ if (timer) clearTimeout(timer);
1255
1304
  if (err instanceof Error && err.name === "AbortError") {
1256
1305
  if (externalAbort) {
1257
1306
  return { status: "cancelled", error: "Execution cancelled" };
1258
1307
  }
1308
+ if (!timedOut || timeoutMs == null) {
1309
+ return { status: "failure", error: err.message };
1310
+ }
1259
1311
  return {
1260
1312
  status: "timeout",
1261
- error: `HTTP request timed out after ${timeout / 1e3}s`
1313
+ error: `HTTP request timed out after ${timeoutMs / 1e3}s`
1262
1314
  };
1263
1315
  }
1264
1316
  const message = err instanceof Error ? err.message : String(err);
@@ -1269,8 +1321,82 @@ ${body}`.trim()
1269
1321
 
1270
1322
  // src/scheduled-tasks/executors/ShellExecutor.ts
1271
1323
  import { spawn as spawn3 } from "child_process";
1272
- var DEFAULT_TIMEOUT3 = 6e4;
1324
+ import * as fs6 from "fs";
1325
+ import * as path5 from "path";
1273
1326
  var MAX_OUTPUT_BYTES2 = 1024 * 1024;
1327
+ var DEFAULT_TIMEOUT_MS4 = 6e4;
1328
+ var POSIX_SHELL_CANDIDATES = ["bash.exe", "sh.exe"];
1329
+ function resolveShellExecution(script, options = {}) {
1330
+ const platform = options.platform ?? process.platform;
1331
+ const env = options.env ?? process.env;
1332
+ const fileExists = options.fileExists ?? fs6.existsSync;
1333
+ if (platform === "win32") {
1334
+ const posixShell = usesPosixShellSyntax(script) ? findWindowsPosixShell(env, fileExists) : null;
1335
+ if (posixShell) {
1336
+ return {
1337
+ command: posixShell,
1338
+ args: ["-s"],
1339
+ stdinScript: script,
1340
+ outputEncoding: "utf8",
1341
+ shellKind: "posix"
1342
+ };
1343
+ }
1344
+ return {
1345
+ command: env.ComSpec ?? env.COMSPEC ?? "cmd.exe",
1346
+ args: ["/d", "/s", "/c", script],
1347
+ outputEncoding: "utf8",
1348
+ shellKind: "cmd"
1349
+ };
1350
+ }
1351
+ return {
1352
+ command: "sh",
1353
+ args: ["-s"],
1354
+ stdinScript: script,
1355
+ outputEncoding: "utf8",
1356
+ shellKind: "posix"
1357
+ };
1358
+ }
1359
+ function usesPosixShellSyntax(script) {
1360
+ return /\$\([^)]*\)|`[^`]*`/.test(script);
1361
+ }
1362
+ function findWindowsPosixShell(env, fileExists) {
1363
+ const explicitShell = env.SERVICEME_POSIX_SHELL;
1364
+ if (explicitShell && fileExists(explicitShell)) {
1365
+ return explicitShell;
1366
+ }
1367
+ const knownGitBashPaths = [
1368
+ "C:\\Program Files\\Git\\bin\\bash.exe",
1369
+ "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
1370
+ "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
1371
+ "C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe"
1372
+ ];
1373
+ for (const candidate of knownGitBashPaths) {
1374
+ if (fileExists(candidate)) return candidate;
1375
+ }
1376
+ const pathValue = env.Path ?? env.PATH ?? "";
1377
+ for (const dir of pathValue.split(path5.win32.delimiter)) {
1378
+ if (!dir) continue;
1379
+ for (const executable of POSIX_SHELL_CANDIDATES) {
1380
+ const candidate = path5.win32.join(dir, executable);
1381
+ if (fileExists(candidate) && !isWindowsWslLauncher(candidate)) {
1382
+ return candidate;
1383
+ }
1384
+ }
1385
+ }
1386
+ return null;
1387
+ }
1388
+ function isWindowsWslLauncher(candidate) {
1389
+ const normalized = path5.win32.normalize(candidate).toLowerCase();
1390
+ return normalized.endsWith("\\windows\\system32\\bash.exe") || normalized.endsWith("\\windows\\syswow64\\bash.exe");
1391
+ }
1392
+ function writeDiagnostic2(message) {
1393
+ const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
1394
+ if (logPath) {
1395
+ fs6.appendFileSync(logPath, message);
1396
+ return;
1397
+ }
1398
+ process.stderr.write(message);
1399
+ }
1274
1400
  var ShellExecutor = class {
1275
1401
  async execute(payload, abortSignal) {
1276
1402
  let output = "";
@@ -1286,7 +1412,7 @@ var ShellExecutor = class {
1286
1412
  }
1287
1413
  executeStreaming(payload, onOutput, abortSignal) {
1288
1414
  const p = payload;
1289
- const timeout = (p.timeout != null ? p.timeout * 1e3 : null) ?? DEFAULT_TIMEOUT3;
1415
+ const timeoutMs = resolveConfiguredTimeoutMs(p.timeout, DEFAULT_TIMEOUT_MS4);
1290
1416
  let resolve;
1291
1417
  const resultPromise = new Promise((r) => {
1292
1418
  resolve = r;
@@ -1295,7 +1421,7 @@ var ShellExecutor = class {
1295
1421
  const settle = (result) => {
1296
1422
  if (settled) return;
1297
1423
  settled = true;
1298
- clearTimeout(timer);
1424
+ if (timer) clearTimeout(timer);
1299
1425
  resolve?.(result);
1300
1426
  };
1301
1427
  if (abortSignal?.aborted) {
@@ -1308,37 +1434,65 @@ var ShellExecutor = class {
1308
1434
  }
1309
1435
  };
1310
1436
  }
1311
- const child = spawn3("sh", ["-c", p.script], {
1437
+ const shellExecution = resolveShellExecution(p.script);
1438
+ const spawnOpts = {
1312
1439
  cwd: p.cwd,
1313
- stdio: ["ignore", "pipe", "pipe"]
1314
- });
1315
- const timer = setTimeout(() => {
1440
+ stdio: [shellExecution.stdinScript ? "pipe" : "ignore", "pipe", "pipe"],
1441
+ windowsHide: true
1442
+ };
1443
+ writeDiagnostic2(
1444
+ `[ShellExecutor] spawn:
1445
+ platform=${process.platform}
1446
+ shell=${shellExecution.shellKind}
1447
+ command=${shellExecution.command}
1448
+ args=${JSON.stringify(shellExecution.args.filter((a) => a !== p.script))}
1449
+ stdin=${shellExecution.stdinScript ? "true" : "false"}
1450
+ cwd=${p.cwd ?? "(default)"}
1451
+ timeout=${timeoutMs != null ? `${timeoutMs}ms` : "unlimited"}
1452
+ scriptLen=${p.script.length}
1453
+ windowsHide=true
1454
+ daemonPid=${process.pid}
1455
+ `
1456
+ );
1457
+ const child = spawn3(shellExecution.command, shellExecution.args, spawnOpts);
1458
+ if (shellExecution.stdinScript && child.stdin) {
1459
+ child.stdin.end(shellExecution.stdinScript);
1460
+ }
1461
+ if (child.pid) {
1462
+ writeDiagnostic2(`[ShellExecutor] spawned child PID=${child.pid}
1463
+ `);
1464
+ }
1465
+ const timer = timeoutMs != null ? setTimeout(() => {
1316
1466
  child.kill("SIGTERM");
1317
1467
  setTimeout(() => {
1318
1468
  if (!child.killed) child.kill("SIGKILL");
1319
1469
  }, 5e3);
1320
1470
  settle({
1321
1471
  status: "timeout",
1322
- error: `Shell execution timed out after ${timeout / 1e3}s`
1472
+ error: `Shell execution timed out after ${timeoutMs / 1e3}s`
1323
1473
  });
1324
- }, timeout);
1474
+ }, timeoutMs) : void 0;
1325
1475
  let stdoutBuf = "";
1326
1476
  let stderrBuf = "";
1327
- child.stdout.on("data", (chunk) => {
1328
- const data = chunk.toString();
1477
+ child.stdout?.on("data", (chunk) => {
1478
+ const data = chunk.toString(shellExecution.outputEncoding);
1329
1479
  stdoutBuf += data;
1330
1480
  if (stdoutBuf.length > MAX_OUTPUT_BYTES2)
1331
1481
  stdoutBuf = stdoutBuf.slice(-MAX_OUTPUT_BYTES2);
1332
1482
  onOutput("stdout", data);
1333
1483
  });
1334
- child.stderr.on("data", (chunk) => {
1335
- const data = chunk.toString();
1484
+ child.stderr?.on("data", (chunk) => {
1485
+ const data = chunk.toString(shellExecution.outputEncoding);
1336
1486
  stderrBuf += data;
1337
1487
  if (stderrBuf.length > MAX_OUTPUT_BYTES2)
1338
1488
  stderrBuf = stderrBuf.slice(-MAX_OUTPUT_BYTES2);
1339
1489
  onOutput("stderr", data);
1340
1490
  });
1341
1491
  child.on("close", (code) => {
1492
+ writeDiagnostic2(
1493
+ `[ShellExecutor] child PID=${child.pid} exited: code=${code}
1494
+ `
1495
+ );
1342
1496
  if (code === 0) {
1343
1497
  settle({ status: "success", output: stdoutBuf || void 0 });
1344
1498
  } else {
@@ -1385,36 +1539,65 @@ function getExecutor(taskType) {
1385
1539
 
1386
1540
  // src/scheduled-tasks/TaskConfigManager.ts
1387
1541
  import { randomUUID } from "crypto";
1388
- import * as fs5 from "fs";
1389
- import * as path5 from "path";
1542
+ import * as fs7 from "fs";
1543
+ import * as path6 from "path";
1544
+ import { createServicemeError as createServicemeError7 } from "@serviceme/devtools-protocol";
1390
1545
  var CONFIG_DIR3 = ".serviceme";
1391
1546
  var CONFIG_FILE = "scheduled-tasks.json";
1392
1547
  function emptyConfig() {
1393
1548
  return { version: 1, tasks: [] };
1394
1549
  }
1550
+ function isRecord(value) {
1551
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1552
+ }
1553
+ function requireNonEmptyString(payload, field, taskType) {
1554
+ if (!isRecord(payload) || typeof payload[field] !== "string" || !payload[field].trim()) {
1555
+ throw createServicemeError7(
1556
+ "invalid_payload",
1557
+ `${taskType} payload ${field} is required`
1558
+ );
1559
+ }
1560
+ }
1561
+ function validateTaskPayload(taskType, payload) {
1562
+ switch (taskType) {
1563
+ case "command":
1564
+ requireNonEmptyString(payload, "command", taskType);
1565
+ return;
1566
+ case "shell":
1567
+ requireNonEmptyString(payload, "script", taskType);
1568
+ return;
1569
+ case "http_request":
1570
+ requireNonEmptyString(payload, "url", taskType);
1571
+ requireNonEmptyString(payload, "method", taskType);
1572
+ return;
1573
+ case "github_copilot_cli":
1574
+ requireNonEmptyString(payload, "prompt", taskType);
1575
+ return;
1576
+ }
1577
+ }
1395
1578
  var TaskConfigManager = class {
1396
1579
  constructor(workspacePath) {
1397
- this.configPath = path5.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1580
+ this.configPath = path6.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1398
1581
  }
1399
1582
  getConfigPath() {
1400
1583
  return this.configPath;
1401
1584
  }
1402
1585
  readConfig() {
1403
- if (!fs5.existsSync(this.configPath)) {
1586
+ if (!fs7.existsSync(this.configPath)) {
1404
1587
  return emptyConfig();
1405
1588
  }
1406
- const raw = fs5.readFileSync(this.configPath, "utf-8");
1589
+ const raw = fs7.readFileSync(this.configPath, "utf-8");
1407
1590
  const parsed = JSON.parse(raw);
1408
1591
  return parsed;
1409
1592
  }
1410
1593
  writeConfig(config) {
1411
- const dir = path5.dirname(this.configPath);
1412
- if (!fs5.existsSync(dir)) {
1413
- fs5.mkdirSync(dir, { recursive: true });
1594
+ const dir = path6.dirname(this.configPath);
1595
+ if (!fs7.existsSync(dir)) {
1596
+ fs7.mkdirSync(dir, { recursive: true });
1414
1597
  }
1415
1598
  const tmp = `${this.configPath}.tmp`;
1416
- fs5.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1417
- fs5.renameSync(tmp, this.configPath);
1599
+ fs7.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1600
+ fs7.renameSync(tmp, this.configPath);
1418
1601
  }
1419
1602
  listTasks() {
1420
1603
  return this.readConfig().tasks;
@@ -1426,6 +1609,7 @@ var TaskConfigManager = class {
1426
1609
  return this.readConfig().tasks.find((t) => t.name === name);
1427
1610
  }
1428
1611
  createTask(input) {
1612
+ validateTaskPayload(input.taskType, input.payload);
1429
1613
  const config = this.readConfig();
1430
1614
  const now = (/* @__PURE__ */ new Date()).toISOString();
1431
1615
  const task = {
@@ -1454,6 +1638,9 @@ var TaskConfigManager = class {
1454
1638
  if (!existing) {
1455
1639
  throw new Error(`Task '${id}' not found`);
1456
1640
  }
1641
+ const nextTaskType = input.taskType ?? existing.taskType;
1642
+ const nextPayload = input.payload ?? existing.payload;
1643
+ validateTaskPayload(nextTaskType, nextPayload);
1457
1644
  const updated = {
1458
1645
  ...existing,
1459
1646
  ...input.name !== void 0 && { name: input.name },
@@ -1492,10 +1679,197 @@ var TaskConfigManager = class {
1492
1679
  }
1493
1680
  };
1494
1681
 
1682
+ // src/scheduled-tasks/TaskExecutionEngine.ts
1683
+ function hasNonEmptyString(value) {
1684
+ return typeof value === "string" && value.trim().length > 0;
1685
+ }
1686
+ function resolveTaskExecutionPayload(taskType, payload, workspacePath) {
1687
+ if (!hasNonEmptyString(workspacePath)) {
1688
+ return payload;
1689
+ }
1690
+ if (taskType === "shell") {
1691
+ const shellPayload = payload;
1692
+ if (hasNonEmptyString(shellPayload.cwd)) {
1693
+ return shellPayload;
1694
+ }
1695
+ return { ...shellPayload, cwd: workspacePath };
1696
+ }
1697
+ if (taskType === "github_copilot_cli") {
1698
+ const copilotPayload = payload;
1699
+ if (hasNonEmptyString(copilotPayload.workspace)) {
1700
+ return copilotPayload;
1701
+ }
1702
+ return { ...copilotPayload, workspace: workspacePath };
1703
+ }
1704
+ return payload;
1705
+ }
1706
+ var TaskExecutionEngine = class {
1707
+ constructor(getExecutor2) {
1708
+ this.getExecutor = getExecutor2;
1709
+ this.running = /* @__PURE__ */ new Map();
1710
+ this.taskExecutions = /* @__PURE__ */ new Map();
1711
+ this.listener = null;
1712
+ }
1713
+ setListener(listener) {
1714
+ this.listener = listener;
1715
+ }
1716
+ async execute(snapshot) {
1717
+ const policy = snapshot.concurrencyPolicy ?? "reject";
1718
+ if (policy === "reject") {
1719
+ const existingIds = this.taskExecutions.get(snapshot.taskId);
1720
+ if (existingIds && existingIds.size > 0) {
1721
+ throw new Error(
1722
+ `TASK_ALREADY_RUNNING: Task ${snapshot.taskId} is already running`
1723
+ );
1724
+ }
1725
+ }
1726
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1727
+ if (!this.taskExecutions.has(snapshot.taskId)) {
1728
+ this.taskExecutions.set(snapshot.taskId, /* @__PURE__ */ new Set());
1729
+ }
1730
+ this.taskExecutions.get(snapshot.taskId)?.add(snapshot.executionId);
1731
+ const ac = new AbortController();
1732
+ const runningExec = {
1733
+ executionId: snapshot.executionId,
1734
+ taskId: snapshot.taskId,
1735
+ startedAt,
1736
+ cancel: () => ac.abort()
1737
+ };
1738
+ this.running.set(snapshot.executionId, runningExec);
1739
+ this.listener?.onStarted({
1740
+ executionId: snapshot.executionId,
1741
+ taskId: snapshot.taskId,
1742
+ startedAt
1743
+ });
1744
+ try {
1745
+ const executionPayload = resolveTaskExecutionPayload(
1746
+ snapshot.type,
1747
+ snapshot.payload,
1748
+ snapshot.workspacePath
1749
+ );
1750
+ validateTaskPayload(snapshot.type, executionPayload);
1751
+ const executor = this.getExecutor(snapshot.type);
1752
+ let result;
1753
+ if (isStreamingTaskExecutor(executor)) {
1754
+ const handle = executor.executeStreaming(
1755
+ executionPayload,
1756
+ (stream, data) => {
1757
+ this.listener?.onOutput({
1758
+ executionId: snapshot.executionId,
1759
+ stream,
1760
+ data
1761
+ });
1762
+ },
1763
+ ac.signal
1764
+ );
1765
+ runningExec.cancel = () => {
1766
+ handle.cancel();
1767
+ ac.abort();
1768
+ };
1769
+ result = await handle.result;
1770
+ } else {
1771
+ result = await executor.execute(executionPayload, ac.signal);
1772
+ }
1773
+ const log = {
1774
+ id: snapshot.executionId,
1775
+ taskId: snapshot.taskId,
1776
+ taskName: snapshot.name,
1777
+ startedAt,
1778
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1779
+ status: result.status === "running" ? "failure" : result.status,
1780
+ output: result.output,
1781
+ error: result.error
1782
+ };
1783
+ switch (log.status) {
1784
+ case "success":
1785
+ this.listener?.onCompleted({
1786
+ executionId: snapshot.executionId,
1787
+ log
1788
+ });
1789
+ break;
1790
+ case "cancelled":
1791
+ this.listener?.onCancelled({
1792
+ executionId: snapshot.executionId,
1793
+ log
1794
+ });
1795
+ break;
1796
+ case "timeout":
1797
+ this.listener?.onFailed({
1798
+ executionId: snapshot.executionId,
1799
+ status: "timeout",
1800
+ reason: "timeout",
1801
+ log
1802
+ });
1803
+ break;
1804
+ default:
1805
+ this.listener?.onFailed({
1806
+ executionId: snapshot.executionId,
1807
+ status: "failure",
1808
+ reason: "error",
1809
+ log
1810
+ });
1811
+ break;
1812
+ }
1813
+ } catch (err) {
1814
+ const log = {
1815
+ id: snapshot.executionId,
1816
+ taskId: snapshot.taskId,
1817
+ taskName: snapshot.name,
1818
+ startedAt,
1819
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1820
+ status: ac.signal.aborted ? "cancelled" : "failure",
1821
+ error: err instanceof Error ? err.message : String(err)
1822
+ };
1823
+ if (ac.signal.aborted) {
1824
+ this.listener?.onCancelled({
1825
+ executionId: snapshot.executionId,
1826
+ log
1827
+ });
1828
+ } else {
1829
+ this.listener?.onFailed({
1830
+ executionId: snapshot.executionId,
1831
+ status: "failure",
1832
+ reason: "error",
1833
+ log
1834
+ });
1835
+ }
1836
+ } finally {
1837
+ this.running.delete(snapshot.executionId);
1838
+ const taskExecIds = this.taskExecutions.get(snapshot.taskId);
1839
+ if (taskExecIds) {
1840
+ taskExecIds.delete(snapshot.executionId);
1841
+ if (taskExecIds.size === 0) {
1842
+ this.taskExecutions.delete(snapshot.taskId);
1843
+ }
1844
+ }
1845
+ }
1846
+ }
1847
+ cancel(executionId) {
1848
+ const exec = this.running.get(executionId);
1849
+ if (!exec) return false;
1850
+ exec.cancel();
1851
+ return true;
1852
+ }
1853
+ listRunning() {
1854
+ return Array.from(this.running.values()).map((e) => ({
1855
+ executionId: e.executionId,
1856
+ taskId: e.taskId,
1857
+ startedAt: e.startedAt
1858
+ }));
1859
+ }
1860
+ dispose() {
1861
+ for (const exec of this.running.values()) {
1862
+ exec.cancel();
1863
+ }
1864
+ this.running.clear();
1865
+ this.taskExecutions.clear();
1866
+ }
1867
+ };
1868
+
1495
1869
  // src/scheduled-tasks/TaskLogManager.ts
1496
1870
  import { randomUUID as randomUUID2 } from "crypto";
1497
- import * as fs6 from "fs";
1498
- import * as path6 from "path";
1871
+ import * as fs8 from "fs";
1872
+ import * as path7 from "path";
1499
1873
  var CONFIG_DIR4 = ".serviceme";
1500
1874
  var LOG_FILE2 = "scheduled-tasks-log.json";
1501
1875
  var MAX_LOGS = 200;
@@ -1520,17 +1894,17 @@ function validateAndRepairLogFile(raw) {
1520
1894
  }
1521
1895
  var TaskLogManager = class {
1522
1896
  constructor(workspacePath) {
1523
- this.logPath = path6.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
1897
+ this.logPath = path7.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
1524
1898
  }
1525
1899
  getLogPath() {
1526
1900
  return this.logPath;
1527
1901
  }
1528
1902
  readLogFile() {
1529
- if (!fs6.existsSync(this.logPath)) {
1903
+ if (!fs8.existsSync(this.logPath)) {
1530
1904
  return emptyLogFile();
1531
1905
  }
1532
1906
  try {
1533
- const raw = fs6.readFileSync(this.logPath, "utf-8");
1907
+ const raw = fs8.readFileSync(this.logPath, "utf-8");
1534
1908
  const parsed = JSON.parse(raw);
1535
1909
  const file = validateAndRepairLogFile(parsed);
1536
1910
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.logs) || parsed.logs.length !== file.logs.length) {
@@ -1546,21 +1920,21 @@ var TaskLogManager = class {
1546
1920
  }
1547
1921
  backupCorruptedFile() {
1548
1922
  try {
1549
- if (fs6.existsSync(this.logPath)) {
1923
+ if (fs8.existsSync(this.logPath)) {
1550
1924
  const backupPath = `${this.logPath}.corrupted.${Date.now()}`;
1551
- fs6.copyFileSync(this.logPath, backupPath);
1925
+ fs8.copyFileSync(this.logPath, backupPath);
1552
1926
  }
1553
1927
  } catch {
1554
1928
  }
1555
1929
  }
1556
1930
  writeLogFile(file) {
1557
- const dir = path6.dirname(this.logPath);
1558
- if (!fs6.existsSync(dir)) {
1559
- fs6.mkdirSync(dir, { recursive: true });
1931
+ const dir = path7.dirname(this.logPath);
1932
+ if (!fs8.existsSync(dir)) {
1933
+ fs8.mkdirSync(dir, { recursive: true });
1560
1934
  }
1561
1935
  const tmp = `${this.logPath}.tmp`;
1562
- fs6.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
1563
- fs6.renameSync(tmp, this.logPath);
1936
+ fs8.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
1937
+ fs8.renameSync(tmp, this.logPath);
1564
1938
  }
1565
1939
  appendLog(input) {
1566
1940
  const file = this.readLogFile();
@@ -1630,7 +2004,23 @@ var SchedulerDaemon = class {
1630
2004
  this.running = true;
1631
2005
  this.startTime = Date.now();
1632
2006
  this.pidManager.writePid(process.pid);
1633
- this.logger.log("info", `Scheduler daemon started (PID: ${process.pid})`);
2007
+ const configPath = this.configManager.getConfigPath();
2008
+ const logPath = this.logger.getLogPath();
2009
+ this.logger.log("info", "=".repeat(60));
2010
+ this.logger.log(
2011
+ "info",
2012
+ `Scheduler daemon started
2013
+ PID: ${process.pid}
2014
+ Node: ${process.version}
2015
+ OS: ${os.type()} ${os.release()} (${process.arch})
2016
+ Platform: ${process.platform}
2017
+ Hostname: ${os.hostname()}
2018
+ User: ${os.userInfo().username}
2019
+ Workspace: ${this.workspacePath}
2020
+ Config: ${configPath}
2021
+ LogPath: ${logPath}
2022
+ ExecPath: ${process.execPath}`
2023
+ );
1634
2024
  this.tickTimer = setInterval(() => this.tick(), TICK_INTERVAL);
1635
2025
  this.setupConfigWatch();
1636
2026
  process.on("SIGTERM", () => this.stop());
@@ -1655,8 +2045,8 @@ var SchedulerDaemon = class {
1655
2045
  const configPath = this.configManager.getConfigPath();
1656
2046
  const dir = configPath.substring(0, configPath.lastIndexOf("/"));
1657
2047
  try {
1658
- if (fs7.existsSync(dir)) {
1659
- this.watcher = fs7.watch(dir, (_eventType, filename) => {
2048
+ if (fs9.existsSync(dir)) {
2049
+ this.watcher = fs9.watch(dir, (_eventType, filename) => {
1660
2050
  if (filename === "scheduled-tasks.json") {
1661
2051
  this.logger.log("info", "Config file changed, reconciling...");
1662
2052
  }
@@ -1672,7 +2062,11 @@ var SchedulerDaemon = class {
1672
2062
  for (const task of config.tasks) {
1673
2063
  if (!task.enabled) continue;
1674
2064
  if (this.taskRunning.has(task.id)) continue;
1675
- const lastExec = this.lastRun.get(task.id) ?? 0;
2065
+ let lastExec = this.lastRun.get(task.id);
2066
+ if (lastExec === void 0) {
2067
+ this.lastRun.set(task.id, now);
2068
+ lastExec = now;
2069
+ }
1676
2070
  if (this.shouldRun(task, lastExec, now)) {
1677
2071
  this.executeTask(task, now);
1678
2072
  }
@@ -1694,11 +2088,25 @@ var SchedulerDaemon = class {
1694
2088
  this.taskRunning.add(task.id);
1695
2089
  this.lastRun.set(task.id, now);
1696
2090
  const startedAt = new Date(now).toISOString();
1697
- this.logger.log("info", `Executing task: ${task.name} (${task.id})`);
2091
+ const startMs = Date.now();
2092
+ this.logger.log(
2093
+ "info",
2094
+ `Executing task: ${task.name} (${task.id})
2095
+ type: ${task.taskType}
2096
+ schedule: ${task.schedule} (${task.scheduleType})
2097
+ enabled: ${task.enabled}`
2098
+ );
1698
2099
  try {
2100
+ const executionPayload = resolveTaskExecutionPayload(
2101
+ task.taskType,
2102
+ task.payload,
2103
+ this.workspacePath
2104
+ );
2105
+ validateTaskPayload(task.taskType, executionPayload);
1699
2106
  const executor = getExecutor(task.taskType);
1700
- const result = await executor.execute(task.payload);
2107
+ const result = await executor.execute(executionPayload);
1701
2108
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2109
+ const durationMs = Date.now() - startMs;
1702
2110
  this.logManager.appendLog({
1703
2111
  taskId: task.id,
1704
2112
  taskName: task.name,
@@ -1708,9 +2116,17 @@ var SchedulerDaemon = class {
1708
2116
  output: result.output,
1709
2117
  error: result.error
1710
2118
  });
1711
- this.logger.log("info", `Task ${task.name} completed: ${result.status}`);
2119
+ const outputBytes = result.output ? Buffer.byteLength(result.output) : 0;
2120
+ this.logger.log(
2121
+ "info",
2122
+ `Task completed: ${task.name} (${task.id})
2123
+ status: ${result.status}
2124
+ duration: ${durationMs}ms
2125
+ outputBytes: ${outputBytes}`
2126
+ );
1712
2127
  } catch (err) {
1713
2128
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2129
+ const durationMs = Date.now() - startMs;
1714
2130
  const message = err instanceof Error ? err.message : String(err);
1715
2131
  this.logManager.appendLog({
1716
2132
  taskId: task.id,
@@ -1720,7 +2136,12 @@ var SchedulerDaemon = class {
1720
2136
  status: "failure",
1721
2137
  error: message
1722
2138
  });
1723
- this.logger.log("error", `Task ${task.name} failed: ${message}`);
2139
+ this.logger.log(
2140
+ "error",
2141
+ `Task failed: ${task.name} (${task.id})
2142
+ duration: ${durationMs}ms
2143
+ error: ${message}`
2144
+ );
1724
2145
  } finally {
1725
2146
  this.taskRunning.delete(task.id);
1726
2147
  }
@@ -1777,164 +2198,6 @@ function matchCronField(field, value) {
1777
2198
  const values = field.split(",");
1778
2199
  return values.some((v) => Number.parseInt(v, 10) === value);
1779
2200
  }
1780
-
1781
- // src/scheduled-tasks/TaskExecutionEngine.ts
1782
- var TaskExecutionEngine = class {
1783
- constructor(getExecutor2) {
1784
- this.getExecutor = getExecutor2;
1785
- this.running = /* @__PURE__ */ new Map();
1786
- this.taskExecutions = /* @__PURE__ */ new Map();
1787
- this.listener = null;
1788
- }
1789
- setListener(listener) {
1790
- this.listener = listener;
1791
- }
1792
- async execute(snapshot) {
1793
- const policy = snapshot.concurrencyPolicy ?? "reject";
1794
- if (policy === "reject") {
1795
- const existingIds = this.taskExecutions.get(snapshot.taskId);
1796
- if (existingIds && existingIds.size > 0) {
1797
- throw new Error(
1798
- `TASK_ALREADY_RUNNING: Task ${snapshot.taskId} is already running`
1799
- );
1800
- }
1801
- }
1802
- const executor = this.getExecutor(snapshot.type);
1803
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1804
- if (!this.taskExecutions.has(snapshot.taskId)) {
1805
- this.taskExecutions.set(snapshot.taskId, /* @__PURE__ */ new Set());
1806
- }
1807
- this.taskExecutions.get(snapshot.taskId)?.add(snapshot.executionId);
1808
- const ac = new AbortController();
1809
- const runningExec = {
1810
- executionId: snapshot.executionId,
1811
- taskId: snapshot.taskId,
1812
- startedAt,
1813
- cancel: () => ac.abort()
1814
- };
1815
- this.running.set(snapshot.executionId, runningExec);
1816
- this.listener?.onStarted({
1817
- executionId: snapshot.executionId,
1818
- taskId: snapshot.taskId,
1819
- startedAt
1820
- });
1821
- try {
1822
- let result;
1823
- if (isStreamingTaskExecutor(executor)) {
1824
- const handle = executor.executeStreaming(
1825
- snapshot.payload,
1826
- (stream, data) => {
1827
- this.listener?.onOutput({
1828
- executionId: snapshot.executionId,
1829
- stream,
1830
- data
1831
- });
1832
- },
1833
- ac.signal
1834
- );
1835
- runningExec.cancel = () => {
1836
- handle.cancel();
1837
- ac.abort();
1838
- };
1839
- result = await handle.result;
1840
- } else {
1841
- result = await executor.execute(snapshot.payload, ac.signal);
1842
- }
1843
- const log = {
1844
- id: snapshot.executionId,
1845
- taskId: snapshot.taskId,
1846
- taskName: snapshot.name,
1847
- startedAt,
1848
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1849
- status: result.status === "running" ? "failure" : result.status,
1850
- output: result.output,
1851
- error: result.error
1852
- };
1853
- switch (log.status) {
1854
- case "success":
1855
- this.listener?.onCompleted({
1856
- executionId: snapshot.executionId,
1857
- log
1858
- });
1859
- break;
1860
- case "cancelled":
1861
- this.listener?.onCancelled({
1862
- executionId: snapshot.executionId,
1863
- log
1864
- });
1865
- break;
1866
- case "timeout":
1867
- this.listener?.onFailed({
1868
- executionId: snapshot.executionId,
1869
- status: "timeout",
1870
- reason: "timeout",
1871
- log
1872
- });
1873
- break;
1874
- default:
1875
- this.listener?.onFailed({
1876
- executionId: snapshot.executionId,
1877
- status: "failure",
1878
- reason: "error",
1879
- log
1880
- });
1881
- break;
1882
- }
1883
- } catch (err) {
1884
- const log = {
1885
- id: snapshot.executionId,
1886
- taskId: snapshot.taskId,
1887
- taskName: snapshot.name,
1888
- startedAt,
1889
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1890
- status: ac.signal.aborted ? "cancelled" : "failure",
1891
- error: err instanceof Error ? err.message : String(err)
1892
- };
1893
- if (ac.signal.aborted) {
1894
- this.listener?.onCancelled({
1895
- executionId: snapshot.executionId,
1896
- log
1897
- });
1898
- } else {
1899
- this.listener?.onFailed({
1900
- executionId: snapshot.executionId,
1901
- status: "failure",
1902
- reason: "error",
1903
- log
1904
- });
1905
- }
1906
- } finally {
1907
- this.running.delete(snapshot.executionId);
1908
- const taskExecIds = this.taskExecutions.get(snapshot.taskId);
1909
- if (taskExecIds) {
1910
- taskExecIds.delete(snapshot.executionId);
1911
- if (taskExecIds.size === 0) {
1912
- this.taskExecutions.delete(snapshot.taskId);
1913
- }
1914
- }
1915
- }
1916
- }
1917
- cancel(executionId) {
1918
- const exec = this.running.get(executionId);
1919
- if (!exec) return false;
1920
- exec.cancel();
1921
- return true;
1922
- }
1923
- listRunning() {
1924
- return Array.from(this.running.values()).map((e) => ({
1925
- executionId: e.executionId,
1926
- taskId: e.taskId,
1927
- startedAt: e.startedAt
1928
- }));
1929
- }
1930
- dispose() {
1931
- for (const exec of this.running.values()) {
1932
- exec.cancel();
1933
- }
1934
- this.running.clear();
1935
- this.taskExecutions.clear();
1936
- }
1937
- };
1938
2201
  export {
1939
2202
  DaemonLogger,
1940
2203
  EnvironmentInspector,
@@ -1963,5 +2226,7 @@ export {
1963
2226
  moveFiles,
1964
2227
  noopLogger,
1965
2228
  parseIntervalMs,
1966
- unzipFile
2229
+ resolveTaskExecutionPayload,
2230
+ unzipFile,
2231
+ validateTaskPayload
1967
2232
  };