@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.js CHANGED
@@ -57,7 +57,9 @@ __export(index_exports, {
57
57
  moveFiles: () => moveFiles,
58
58
  noopLogger: () => noopLogger,
59
59
  parseIntervalMs: () => parseIntervalMs,
60
- unzipFile: () => unzipFile
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 = "";
@@ -1113,12 +1116,51 @@ var PidManager = class {
1113
1116
  };
1114
1117
 
1115
1118
  // src/scheduled-tasks/daemon/SchedulerDaemon.ts
1116
- var fs7 = __toESM(require("fs"));
1119
+ var fs9 = __toESM(require("fs"));
1120
+ var os = __toESM(require("os"));
1117
1121
 
1118
1122
  // src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
1119
1123
  var import_node_child_process2 = require("child_process");
1120
- var DEFAULT_TIMEOUT = 3e5;
1124
+ var fs5 = __toESM(require("fs"));
1125
+
1126
+ // src/scheduled-tasks/executors/timeout.ts
1127
+ function resolveConfiguredTimeoutMs(timeoutSeconds, defaultTimeoutMs) {
1128
+ if (timeoutSeconds === 0) {
1129
+ return void 0;
1130
+ }
1131
+ if (typeof timeoutSeconds !== "number" || !Number.isFinite(timeoutSeconds) || timeoutSeconds < 0) {
1132
+ return defaultTimeoutMs;
1133
+ }
1134
+ return timeoutSeconds * 1e3;
1135
+ }
1136
+
1137
+ // src/scheduled-tasks/executors/GithubCopilotCliExecutor.ts
1121
1138
  var MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
1139
+ var DEFAULT_TIMEOUT_MS2 = 3e5;
1140
+ function redactArgs(args) {
1141
+ const redacted = [];
1142
+ let redactNext = false;
1143
+ for (const arg of args) {
1144
+ if (redactNext) {
1145
+ redacted.push("<redacted>");
1146
+ redactNext = false;
1147
+ continue;
1148
+ }
1149
+ redacted.push(arg);
1150
+ if (arg === "--prompt") {
1151
+ redactNext = true;
1152
+ }
1153
+ }
1154
+ return redacted;
1155
+ }
1156
+ function writeDiagnostic(message) {
1157
+ const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
1158
+ if (logPath) {
1159
+ fs5.appendFileSync(logPath, message);
1160
+ return;
1161
+ }
1162
+ process.stderr.write(message);
1163
+ }
1122
1164
  var GithubCopilotCliExecutor = class {
1123
1165
  async execute(payload, abortSignal) {
1124
1166
  const authenticated = await isCopilotAuthenticated();
@@ -1141,7 +1183,7 @@ var GithubCopilotCliExecutor = class {
1141
1183
  }
1142
1184
  executeStreaming(payload, onOutput, abortSignal) {
1143
1185
  const p = payload;
1144
- const timeout = (p.timeout != null ? p.timeout * 1e3 : null) ?? DEFAULT_TIMEOUT;
1186
+ const timeoutMs = resolveConfiguredTimeoutMs(p.timeout, DEFAULT_TIMEOUT_MS2);
1145
1187
  let resolve;
1146
1188
  const resultPromise = new Promise((r) => {
1147
1189
  resolve = r;
@@ -1150,7 +1192,7 @@ var GithubCopilotCliExecutor = class {
1150
1192
  const settle = (result) => {
1151
1193
  if (settled) return;
1152
1194
  settled = true;
1153
- clearTimeout(timer);
1195
+ if (timer) clearTimeout(timer);
1154
1196
  resolve?.(result);
1155
1197
  };
1156
1198
  if (abortSignal?.aborted) {
@@ -1176,23 +1218,32 @@ var GithubCopilotCliExecutor = class {
1176
1218
  if (p.agent) {
1177
1219
  args.push("--agent", p.agent);
1178
1220
  }
1179
- if (p.timeout != null) {
1180
- args.push("--timeout", String(p.timeout));
1181
- }
1221
+ args.push("--timeout", String(p.timeout ?? 0));
1222
+ writeDiagnostic(
1223
+ `[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}
1224
+ `
1225
+ );
1182
1226
  const child = (0, import_node_child_process2.spawn)("serviceme", args, {
1183
1227
  cwd: p.workspace,
1184
- stdio: ["ignore", "pipe", "pipe"]
1228
+ stdio: ["ignore", "pipe", "pipe"],
1229
+ windowsHide: true
1185
1230
  });
1186
- const timer = setTimeout(() => {
1231
+ if (child.pid) {
1232
+ writeDiagnostic(
1233
+ `[GithubCopilotCliExecutor] spawned child PID=${child.pid}
1234
+ `
1235
+ );
1236
+ }
1237
+ const timer = timeoutMs != null ? setTimeout(() => {
1187
1238
  child.kill("SIGTERM");
1188
1239
  setTimeout(() => {
1189
1240
  if (!child.killed) child.kill("SIGKILL");
1190
1241
  }, 5e3);
1191
1242
  settle({
1192
1243
  status: "timeout",
1193
- error: `Copilot CLI execution timed out after ${timeout / 1e3}s`
1244
+ error: `Copilot CLI execution timed out after ${timeoutMs / 1e3}s`
1194
1245
  });
1195
- }, timeout);
1246
+ }, timeoutMs) : void 0;
1196
1247
  let stdoutBuf = "";
1197
1248
  let stderrBuf = "";
1198
1249
  child.stdout.on("data", (chunk) => {
@@ -1236,19 +1287,19 @@ var GithubCopilotCliExecutor = class {
1236
1287
  };
1237
1288
 
1238
1289
  // src/scheduled-tasks/executors/HttpRequestExecutor.ts
1239
- var DEFAULT_TIMEOUT2 = 3e4;
1290
+ var DEFAULT_TIMEOUT_MS3 = 3e4;
1240
1291
  var HttpRequestExecutor = class {
1241
1292
  async execute(payload, abortSignal) {
1242
1293
  const p = payload;
1243
- const timeout = (p.timeout != null ? p.timeout * 1e3 : null) ?? DEFAULT_TIMEOUT2;
1294
+ const timeoutMs = resolveConfiguredTimeoutMs(p.timeout, DEFAULT_TIMEOUT_MS3);
1244
1295
  const ac = new AbortController();
1245
- let _timedOut = false;
1246
- const timer = setTimeout(() => {
1247
- _timedOut = true;
1296
+ let timedOut = false;
1297
+ const timer = timeoutMs != null ? setTimeout(() => {
1298
+ timedOut = true;
1248
1299
  ac.abort();
1249
- }, timeout);
1300
+ }, timeoutMs) : void 0;
1250
1301
  if (abortSignal?.aborted) {
1251
- clearTimeout(timer);
1302
+ if (timer) clearTimeout(timer);
1252
1303
  return { status: "cancelled", error: "Execution aborted" };
1253
1304
  }
1254
1305
  let externalAbort = false;
@@ -1256,7 +1307,7 @@ var HttpRequestExecutor = class {
1256
1307
  "abort",
1257
1308
  () => {
1258
1309
  externalAbort = true;
1259
- clearTimeout(timer);
1310
+ if (timer) clearTimeout(timer);
1260
1311
  ac.abort();
1261
1312
  },
1262
1313
  { once: true }
@@ -1268,7 +1319,7 @@ var HttpRequestExecutor = class {
1268
1319
  body: p.body,
1269
1320
  signal: ac.signal
1270
1321
  });
1271
- clearTimeout(timer);
1322
+ if (timer) clearTimeout(timer);
1272
1323
  const body = await response.text();
1273
1324
  if (response.ok) {
1274
1325
  return {
@@ -1283,14 +1334,17 @@ ${body}`.trim()
1283
1334
  ${body}`.trim()
1284
1335
  };
1285
1336
  } catch (err) {
1286
- clearTimeout(timer);
1337
+ if (timer) clearTimeout(timer);
1287
1338
  if (err instanceof Error && err.name === "AbortError") {
1288
1339
  if (externalAbort) {
1289
1340
  return { status: "cancelled", error: "Execution cancelled" };
1290
1341
  }
1342
+ if (!timedOut || timeoutMs == null) {
1343
+ return { status: "failure", error: err.message };
1344
+ }
1291
1345
  return {
1292
1346
  status: "timeout",
1293
- error: `HTTP request timed out after ${timeout / 1e3}s`
1347
+ error: `HTTP request timed out after ${timeoutMs / 1e3}s`
1294
1348
  };
1295
1349
  }
1296
1350
  const message = err instanceof Error ? err.message : String(err);
@@ -1301,8 +1355,82 @@ ${body}`.trim()
1301
1355
 
1302
1356
  // src/scheduled-tasks/executors/ShellExecutor.ts
1303
1357
  var import_node_child_process3 = require("child_process");
1304
- var DEFAULT_TIMEOUT3 = 6e4;
1358
+ var fs6 = __toESM(require("fs"));
1359
+ var path5 = __toESM(require("path"));
1305
1360
  var MAX_OUTPUT_BYTES2 = 1024 * 1024;
1361
+ var DEFAULT_TIMEOUT_MS4 = 6e4;
1362
+ var POSIX_SHELL_CANDIDATES = ["bash.exe", "sh.exe"];
1363
+ function resolveShellExecution(script, options = {}) {
1364
+ const platform = options.platform ?? process.platform;
1365
+ const env = options.env ?? process.env;
1366
+ const fileExists = options.fileExists ?? fs6.existsSync;
1367
+ if (platform === "win32") {
1368
+ const posixShell = usesPosixShellSyntax(script) ? findWindowsPosixShell(env, fileExists) : null;
1369
+ if (posixShell) {
1370
+ return {
1371
+ command: posixShell,
1372
+ args: ["-s"],
1373
+ stdinScript: script,
1374
+ outputEncoding: "utf8",
1375
+ shellKind: "posix"
1376
+ };
1377
+ }
1378
+ return {
1379
+ command: env.ComSpec ?? env.COMSPEC ?? "cmd.exe",
1380
+ args: ["/d", "/s", "/c", script],
1381
+ outputEncoding: "utf8",
1382
+ shellKind: "cmd"
1383
+ };
1384
+ }
1385
+ return {
1386
+ command: "sh",
1387
+ args: ["-s"],
1388
+ stdinScript: script,
1389
+ outputEncoding: "utf8",
1390
+ shellKind: "posix"
1391
+ };
1392
+ }
1393
+ function usesPosixShellSyntax(script) {
1394
+ return /\$\([^)]*\)|`[^`]*`/.test(script);
1395
+ }
1396
+ function findWindowsPosixShell(env, fileExists) {
1397
+ const explicitShell = env.SERVICEME_POSIX_SHELL;
1398
+ if (explicitShell && fileExists(explicitShell)) {
1399
+ return explicitShell;
1400
+ }
1401
+ const knownGitBashPaths = [
1402
+ "C:\\Program Files\\Git\\bin\\bash.exe",
1403
+ "C:\\Program Files\\Git\\usr\\bin\\bash.exe",
1404
+ "C:\\Program Files (x86)\\Git\\bin\\bash.exe",
1405
+ "C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe"
1406
+ ];
1407
+ for (const candidate of knownGitBashPaths) {
1408
+ if (fileExists(candidate)) return candidate;
1409
+ }
1410
+ const pathValue = env.Path ?? env.PATH ?? "";
1411
+ for (const dir of pathValue.split(path5.win32.delimiter)) {
1412
+ if (!dir) continue;
1413
+ for (const executable of POSIX_SHELL_CANDIDATES) {
1414
+ const candidate = path5.win32.join(dir, executable);
1415
+ if (fileExists(candidate) && !isWindowsWslLauncher(candidate)) {
1416
+ return candidate;
1417
+ }
1418
+ }
1419
+ }
1420
+ return null;
1421
+ }
1422
+ function isWindowsWslLauncher(candidate) {
1423
+ const normalized = path5.win32.normalize(candidate).toLowerCase();
1424
+ return normalized.endsWith("\\windows\\system32\\bash.exe") || normalized.endsWith("\\windows\\syswow64\\bash.exe");
1425
+ }
1426
+ function writeDiagnostic2(message) {
1427
+ const logPath = process.env.SERVICEME_SCHEDULER_LOG_PATH;
1428
+ if (logPath) {
1429
+ fs6.appendFileSync(logPath, message);
1430
+ return;
1431
+ }
1432
+ process.stderr.write(message);
1433
+ }
1306
1434
  var ShellExecutor = class {
1307
1435
  async execute(payload, abortSignal) {
1308
1436
  let output = "";
@@ -1318,7 +1446,7 @@ var ShellExecutor = class {
1318
1446
  }
1319
1447
  executeStreaming(payload, onOutput, abortSignal) {
1320
1448
  const p = payload;
1321
- const timeout = (p.timeout != null ? p.timeout * 1e3 : null) ?? DEFAULT_TIMEOUT3;
1449
+ const timeoutMs = resolveConfiguredTimeoutMs(p.timeout, DEFAULT_TIMEOUT_MS4);
1322
1450
  let resolve;
1323
1451
  const resultPromise = new Promise((r) => {
1324
1452
  resolve = r;
@@ -1327,7 +1455,7 @@ var ShellExecutor = class {
1327
1455
  const settle = (result) => {
1328
1456
  if (settled) return;
1329
1457
  settled = true;
1330
- clearTimeout(timer);
1458
+ if (timer) clearTimeout(timer);
1331
1459
  resolve?.(result);
1332
1460
  };
1333
1461
  if (abortSignal?.aborted) {
@@ -1340,37 +1468,65 @@ var ShellExecutor = class {
1340
1468
  }
1341
1469
  };
1342
1470
  }
1343
- const child = (0, import_node_child_process3.spawn)("sh", ["-c", p.script], {
1471
+ const shellExecution = resolveShellExecution(p.script);
1472
+ const spawnOpts = {
1344
1473
  cwd: p.cwd,
1345
- stdio: ["ignore", "pipe", "pipe"]
1346
- });
1347
- const timer = setTimeout(() => {
1474
+ stdio: [shellExecution.stdinScript ? "pipe" : "ignore", "pipe", "pipe"],
1475
+ windowsHide: true
1476
+ };
1477
+ writeDiagnostic2(
1478
+ `[ShellExecutor] spawn:
1479
+ platform=${process.platform}
1480
+ shell=${shellExecution.shellKind}
1481
+ command=${shellExecution.command}
1482
+ args=${JSON.stringify(shellExecution.args.filter((a) => a !== p.script))}
1483
+ stdin=${shellExecution.stdinScript ? "true" : "false"}
1484
+ cwd=${p.cwd ?? "(default)"}
1485
+ timeout=${timeoutMs != null ? `${timeoutMs}ms` : "unlimited"}
1486
+ scriptLen=${p.script.length}
1487
+ windowsHide=true
1488
+ daemonPid=${process.pid}
1489
+ `
1490
+ );
1491
+ const child = (0, import_node_child_process3.spawn)(shellExecution.command, shellExecution.args, spawnOpts);
1492
+ if (shellExecution.stdinScript && child.stdin) {
1493
+ child.stdin.end(shellExecution.stdinScript);
1494
+ }
1495
+ if (child.pid) {
1496
+ writeDiagnostic2(`[ShellExecutor] spawned child PID=${child.pid}
1497
+ `);
1498
+ }
1499
+ const timer = timeoutMs != null ? setTimeout(() => {
1348
1500
  child.kill("SIGTERM");
1349
1501
  setTimeout(() => {
1350
1502
  if (!child.killed) child.kill("SIGKILL");
1351
1503
  }, 5e3);
1352
1504
  settle({
1353
1505
  status: "timeout",
1354
- error: `Shell execution timed out after ${timeout / 1e3}s`
1506
+ error: `Shell execution timed out after ${timeoutMs / 1e3}s`
1355
1507
  });
1356
- }, timeout);
1508
+ }, timeoutMs) : void 0;
1357
1509
  let stdoutBuf = "";
1358
1510
  let stderrBuf = "";
1359
- child.stdout.on("data", (chunk) => {
1360
- const data = chunk.toString();
1511
+ child.stdout?.on("data", (chunk) => {
1512
+ const data = chunk.toString(shellExecution.outputEncoding);
1361
1513
  stdoutBuf += data;
1362
1514
  if (stdoutBuf.length > MAX_OUTPUT_BYTES2)
1363
1515
  stdoutBuf = stdoutBuf.slice(-MAX_OUTPUT_BYTES2);
1364
1516
  onOutput("stdout", data);
1365
1517
  });
1366
- child.stderr.on("data", (chunk) => {
1367
- const data = chunk.toString();
1518
+ child.stderr?.on("data", (chunk) => {
1519
+ const data = chunk.toString(shellExecution.outputEncoding);
1368
1520
  stderrBuf += data;
1369
1521
  if (stderrBuf.length > MAX_OUTPUT_BYTES2)
1370
1522
  stderrBuf = stderrBuf.slice(-MAX_OUTPUT_BYTES2);
1371
1523
  onOutput("stderr", data);
1372
1524
  });
1373
1525
  child.on("close", (code) => {
1526
+ writeDiagnostic2(
1527
+ `[ShellExecutor] child PID=${child.pid} exited: code=${code}
1528
+ `
1529
+ );
1374
1530
  if (code === 0) {
1375
1531
  settle({ status: "success", output: stdoutBuf || void 0 });
1376
1532
  } else {
@@ -1417,36 +1573,65 @@ function getExecutor(taskType) {
1417
1573
 
1418
1574
  // src/scheduled-tasks/TaskConfigManager.ts
1419
1575
  var import_node_crypto = require("crypto");
1420
- var fs5 = __toESM(require("fs"));
1421
- var path5 = __toESM(require("path"));
1576
+ var fs7 = __toESM(require("fs"));
1577
+ var path6 = __toESM(require("path"));
1578
+ var import_devtools_protocol7 = require("@serviceme/devtools-protocol");
1422
1579
  var CONFIG_DIR3 = ".serviceme";
1423
1580
  var CONFIG_FILE = "scheduled-tasks.json";
1424
1581
  function emptyConfig() {
1425
1582
  return { version: 1, tasks: [] };
1426
1583
  }
1584
+ function isRecord(value) {
1585
+ return value !== null && typeof value === "object" && !Array.isArray(value);
1586
+ }
1587
+ function requireNonEmptyString(payload, field, taskType) {
1588
+ if (!isRecord(payload) || typeof payload[field] !== "string" || !payload[field].trim()) {
1589
+ throw (0, import_devtools_protocol7.createServicemeError)(
1590
+ "invalid_payload",
1591
+ `${taskType} payload ${field} is required`
1592
+ );
1593
+ }
1594
+ }
1595
+ function validateTaskPayload(taskType, payload) {
1596
+ switch (taskType) {
1597
+ case "command":
1598
+ requireNonEmptyString(payload, "command", taskType);
1599
+ return;
1600
+ case "shell":
1601
+ requireNonEmptyString(payload, "script", taskType);
1602
+ return;
1603
+ case "http_request":
1604
+ requireNonEmptyString(payload, "url", taskType);
1605
+ requireNonEmptyString(payload, "method", taskType);
1606
+ return;
1607
+ case "github_copilot_cli":
1608
+ requireNonEmptyString(payload, "prompt", taskType);
1609
+ return;
1610
+ }
1611
+ }
1427
1612
  var TaskConfigManager = class {
1428
1613
  constructor(workspacePath) {
1429
- this.configPath = path5.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1614
+ this.configPath = path6.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1430
1615
  }
1431
1616
  getConfigPath() {
1432
1617
  return this.configPath;
1433
1618
  }
1434
1619
  readConfig() {
1435
- if (!fs5.existsSync(this.configPath)) {
1620
+ if (!fs7.existsSync(this.configPath)) {
1436
1621
  return emptyConfig();
1437
1622
  }
1438
- const raw = fs5.readFileSync(this.configPath, "utf-8");
1623
+ const raw = fs7.readFileSync(this.configPath, "utf-8");
1439
1624
  const parsed = JSON.parse(raw);
1440
1625
  return parsed;
1441
1626
  }
1442
1627
  writeConfig(config) {
1443
- const dir = path5.dirname(this.configPath);
1444
- if (!fs5.existsSync(dir)) {
1445
- fs5.mkdirSync(dir, { recursive: true });
1628
+ const dir = path6.dirname(this.configPath);
1629
+ if (!fs7.existsSync(dir)) {
1630
+ fs7.mkdirSync(dir, { recursive: true });
1446
1631
  }
1447
1632
  const tmp = `${this.configPath}.tmp`;
1448
- fs5.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1449
- fs5.renameSync(tmp, this.configPath);
1633
+ fs7.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1634
+ fs7.renameSync(tmp, this.configPath);
1450
1635
  }
1451
1636
  listTasks() {
1452
1637
  return this.readConfig().tasks;
@@ -1458,6 +1643,7 @@ var TaskConfigManager = class {
1458
1643
  return this.readConfig().tasks.find((t) => t.name === name);
1459
1644
  }
1460
1645
  createTask(input) {
1646
+ validateTaskPayload(input.taskType, input.payload);
1461
1647
  const config = this.readConfig();
1462
1648
  const now = (/* @__PURE__ */ new Date()).toISOString();
1463
1649
  const task = {
@@ -1486,6 +1672,9 @@ var TaskConfigManager = class {
1486
1672
  if (!existing) {
1487
1673
  throw new Error(`Task '${id}' not found`);
1488
1674
  }
1675
+ const nextTaskType = input.taskType ?? existing.taskType;
1676
+ const nextPayload = input.payload ?? existing.payload;
1677
+ validateTaskPayload(nextTaskType, nextPayload);
1489
1678
  const updated = {
1490
1679
  ...existing,
1491
1680
  ...input.name !== void 0 && { name: input.name },
@@ -1524,10 +1713,197 @@ var TaskConfigManager = class {
1524
1713
  }
1525
1714
  };
1526
1715
 
1716
+ // src/scheduled-tasks/TaskExecutionEngine.ts
1717
+ function hasNonEmptyString(value) {
1718
+ return typeof value === "string" && value.trim().length > 0;
1719
+ }
1720
+ function resolveTaskExecutionPayload(taskType, payload, workspacePath) {
1721
+ if (!hasNonEmptyString(workspacePath)) {
1722
+ return payload;
1723
+ }
1724
+ if (taskType === "shell") {
1725
+ const shellPayload = payload;
1726
+ if (hasNonEmptyString(shellPayload.cwd)) {
1727
+ return shellPayload;
1728
+ }
1729
+ return { ...shellPayload, cwd: workspacePath };
1730
+ }
1731
+ if (taskType === "github_copilot_cli") {
1732
+ const copilotPayload = payload;
1733
+ if (hasNonEmptyString(copilotPayload.workspace)) {
1734
+ return copilotPayload;
1735
+ }
1736
+ return { ...copilotPayload, workspace: workspacePath };
1737
+ }
1738
+ return payload;
1739
+ }
1740
+ var TaskExecutionEngine = class {
1741
+ constructor(getExecutor2) {
1742
+ this.getExecutor = getExecutor2;
1743
+ this.running = /* @__PURE__ */ new Map();
1744
+ this.taskExecutions = /* @__PURE__ */ new Map();
1745
+ this.listener = null;
1746
+ }
1747
+ setListener(listener) {
1748
+ this.listener = listener;
1749
+ }
1750
+ async execute(snapshot) {
1751
+ const policy = snapshot.concurrencyPolicy ?? "reject";
1752
+ if (policy === "reject") {
1753
+ const existingIds = this.taskExecutions.get(snapshot.taskId);
1754
+ if (existingIds && existingIds.size > 0) {
1755
+ throw new Error(
1756
+ `TASK_ALREADY_RUNNING: Task ${snapshot.taskId} is already running`
1757
+ );
1758
+ }
1759
+ }
1760
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1761
+ if (!this.taskExecutions.has(snapshot.taskId)) {
1762
+ this.taskExecutions.set(snapshot.taskId, /* @__PURE__ */ new Set());
1763
+ }
1764
+ this.taskExecutions.get(snapshot.taskId)?.add(snapshot.executionId);
1765
+ const ac = new AbortController();
1766
+ const runningExec = {
1767
+ executionId: snapshot.executionId,
1768
+ taskId: snapshot.taskId,
1769
+ startedAt,
1770
+ cancel: () => ac.abort()
1771
+ };
1772
+ this.running.set(snapshot.executionId, runningExec);
1773
+ this.listener?.onStarted({
1774
+ executionId: snapshot.executionId,
1775
+ taskId: snapshot.taskId,
1776
+ startedAt
1777
+ });
1778
+ try {
1779
+ const executionPayload = resolveTaskExecutionPayload(
1780
+ snapshot.type,
1781
+ snapshot.payload,
1782
+ snapshot.workspacePath
1783
+ );
1784
+ validateTaskPayload(snapshot.type, executionPayload);
1785
+ const executor = this.getExecutor(snapshot.type);
1786
+ let result;
1787
+ if (isStreamingTaskExecutor(executor)) {
1788
+ const handle = executor.executeStreaming(
1789
+ executionPayload,
1790
+ (stream, data) => {
1791
+ this.listener?.onOutput({
1792
+ executionId: snapshot.executionId,
1793
+ stream,
1794
+ data
1795
+ });
1796
+ },
1797
+ ac.signal
1798
+ );
1799
+ runningExec.cancel = () => {
1800
+ handle.cancel();
1801
+ ac.abort();
1802
+ };
1803
+ result = await handle.result;
1804
+ } else {
1805
+ result = await executor.execute(executionPayload, ac.signal);
1806
+ }
1807
+ const log = {
1808
+ id: snapshot.executionId,
1809
+ taskId: snapshot.taskId,
1810
+ taskName: snapshot.name,
1811
+ startedAt,
1812
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1813
+ status: result.status === "running" ? "failure" : result.status,
1814
+ output: result.output,
1815
+ error: result.error
1816
+ };
1817
+ switch (log.status) {
1818
+ case "success":
1819
+ this.listener?.onCompleted({
1820
+ executionId: snapshot.executionId,
1821
+ log
1822
+ });
1823
+ break;
1824
+ case "cancelled":
1825
+ this.listener?.onCancelled({
1826
+ executionId: snapshot.executionId,
1827
+ log
1828
+ });
1829
+ break;
1830
+ case "timeout":
1831
+ this.listener?.onFailed({
1832
+ executionId: snapshot.executionId,
1833
+ status: "timeout",
1834
+ reason: "timeout",
1835
+ log
1836
+ });
1837
+ break;
1838
+ default:
1839
+ this.listener?.onFailed({
1840
+ executionId: snapshot.executionId,
1841
+ status: "failure",
1842
+ reason: "error",
1843
+ log
1844
+ });
1845
+ break;
1846
+ }
1847
+ } catch (err) {
1848
+ const log = {
1849
+ id: snapshot.executionId,
1850
+ taskId: snapshot.taskId,
1851
+ taskName: snapshot.name,
1852
+ startedAt,
1853
+ finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1854
+ status: ac.signal.aborted ? "cancelled" : "failure",
1855
+ error: err instanceof Error ? err.message : String(err)
1856
+ };
1857
+ if (ac.signal.aborted) {
1858
+ this.listener?.onCancelled({
1859
+ executionId: snapshot.executionId,
1860
+ log
1861
+ });
1862
+ } else {
1863
+ this.listener?.onFailed({
1864
+ executionId: snapshot.executionId,
1865
+ status: "failure",
1866
+ reason: "error",
1867
+ log
1868
+ });
1869
+ }
1870
+ } finally {
1871
+ this.running.delete(snapshot.executionId);
1872
+ const taskExecIds = this.taskExecutions.get(snapshot.taskId);
1873
+ if (taskExecIds) {
1874
+ taskExecIds.delete(snapshot.executionId);
1875
+ if (taskExecIds.size === 0) {
1876
+ this.taskExecutions.delete(snapshot.taskId);
1877
+ }
1878
+ }
1879
+ }
1880
+ }
1881
+ cancel(executionId) {
1882
+ const exec = this.running.get(executionId);
1883
+ if (!exec) return false;
1884
+ exec.cancel();
1885
+ return true;
1886
+ }
1887
+ listRunning() {
1888
+ return Array.from(this.running.values()).map((e) => ({
1889
+ executionId: e.executionId,
1890
+ taskId: e.taskId,
1891
+ startedAt: e.startedAt
1892
+ }));
1893
+ }
1894
+ dispose() {
1895
+ for (const exec of this.running.values()) {
1896
+ exec.cancel();
1897
+ }
1898
+ this.running.clear();
1899
+ this.taskExecutions.clear();
1900
+ }
1901
+ };
1902
+
1527
1903
  // src/scheduled-tasks/TaskLogManager.ts
1528
1904
  var import_node_crypto2 = require("crypto");
1529
- var fs6 = __toESM(require("fs"));
1530
- var path6 = __toESM(require("path"));
1905
+ var fs8 = __toESM(require("fs"));
1906
+ var path7 = __toESM(require("path"));
1531
1907
  var CONFIG_DIR4 = ".serviceme";
1532
1908
  var LOG_FILE2 = "scheduled-tasks-log.json";
1533
1909
  var MAX_LOGS = 200;
@@ -1552,17 +1928,17 @@ function validateAndRepairLogFile(raw) {
1552
1928
  }
1553
1929
  var TaskLogManager = class {
1554
1930
  constructor(workspacePath) {
1555
- this.logPath = path6.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
1931
+ this.logPath = path7.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
1556
1932
  }
1557
1933
  getLogPath() {
1558
1934
  return this.logPath;
1559
1935
  }
1560
1936
  readLogFile() {
1561
- if (!fs6.existsSync(this.logPath)) {
1937
+ if (!fs8.existsSync(this.logPath)) {
1562
1938
  return emptyLogFile();
1563
1939
  }
1564
1940
  try {
1565
- const raw = fs6.readFileSync(this.logPath, "utf-8");
1941
+ const raw = fs8.readFileSync(this.logPath, "utf-8");
1566
1942
  const parsed = JSON.parse(raw);
1567
1943
  const file = validateAndRepairLogFile(parsed);
1568
1944
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.logs) || parsed.logs.length !== file.logs.length) {
@@ -1578,21 +1954,21 @@ var TaskLogManager = class {
1578
1954
  }
1579
1955
  backupCorruptedFile() {
1580
1956
  try {
1581
- if (fs6.existsSync(this.logPath)) {
1957
+ if (fs8.existsSync(this.logPath)) {
1582
1958
  const backupPath = `${this.logPath}.corrupted.${Date.now()}`;
1583
- fs6.copyFileSync(this.logPath, backupPath);
1959
+ fs8.copyFileSync(this.logPath, backupPath);
1584
1960
  }
1585
1961
  } catch {
1586
1962
  }
1587
1963
  }
1588
1964
  writeLogFile(file) {
1589
- const dir = path6.dirname(this.logPath);
1590
- if (!fs6.existsSync(dir)) {
1591
- fs6.mkdirSync(dir, { recursive: true });
1965
+ const dir = path7.dirname(this.logPath);
1966
+ if (!fs8.existsSync(dir)) {
1967
+ fs8.mkdirSync(dir, { recursive: true });
1592
1968
  }
1593
1969
  const tmp = `${this.logPath}.tmp`;
1594
- fs6.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
1595
- fs6.renameSync(tmp, this.logPath);
1970
+ fs8.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
1971
+ fs8.renameSync(tmp, this.logPath);
1596
1972
  }
1597
1973
  appendLog(input) {
1598
1974
  const file = this.readLogFile();
@@ -1662,7 +2038,23 @@ var SchedulerDaemon = class {
1662
2038
  this.running = true;
1663
2039
  this.startTime = Date.now();
1664
2040
  this.pidManager.writePid(process.pid);
1665
- this.logger.log("info", `Scheduler daemon started (PID: ${process.pid})`);
2041
+ const configPath = this.configManager.getConfigPath();
2042
+ const logPath = this.logger.getLogPath();
2043
+ this.logger.log("info", "=".repeat(60));
2044
+ this.logger.log(
2045
+ "info",
2046
+ `Scheduler daemon started
2047
+ PID: ${process.pid}
2048
+ Node: ${process.version}
2049
+ OS: ${os.type()} ${os.release()} (${process.arch})
2050
+ Platform: ${process.platform}
2051
+ Hostname: ${os.hostname()}
2052
+ User: ${os.userInfo().username}
2053
+ Workspace: ${this.workspacePath}
2054
+ Config: ${configPath}
2055
+ LogPath: ${logPath}
2056
+ ExecPath: ${process.execPath}`
2057
+ );
1666
2058
  this.tickTimer = setInterval(() => this.tick(), TICK_INTERVAL);
1667
2059
  this.setupConfigWatch();
1668
2060
  process.on("SIGTERM", () => this.stop());
@@ -1687,8 +2079,8 @@ var SchedulerDaemon = class {
1687
2079
  const configPath = this.configManager.getConfigPath();
1688
2080
  const dir = configPath.substring(0, configPath.lastIndexOf("/"));
1689
2081
  try {
1690
- if (fs7.existsSync(dir)) {
1691
- this.watcher = fs7.watch(dir, (_eventType, filename) => {
2082
+ if (fs9.existsSync(dir)) {
2083
+ this.watcher = fs9.watch(dir, (_eventType, filename) => {
1692
2084
  if (filename === "scheduled-tasks.json") {
1693
2085
  this.logger.log("info", "Config file changed, reconciling...");
1694
2086
  }
@@ -1704,7 +2096,11 @@ var SchedulerDaemon = class {
1704
2096
  for (const task of config.tasks) {
1705
2097
  if (!task.enabled) continue;
1706
2098
  if (this.taskRunning.has(task.id)) continue;
1707
- const lastExec = this.lastRun.get(task.id) ?? 0;
2099
+ let lastExec = this.lastRun.get(task.id);
2100
+ if (lastExec === void 0) {
2101
+ this.lastRun.set(task.id, now);
2102
+ lastExec = now;
2103
+ }
1708
2104
  if (this.shouldRun(task, lastExec, now)) {
1709
2105
  this.executeTask(task, now);
1710
2106
  }
@@ -1726,11 +2122,25 @@ var SchedulerDaemon = class {
1726
2122
  this.taskRunning.add(task.id);
1727
2123
  this.lastRun.set(task.id, now);
1728
2124
  const startedAt = new Date(now).toISOString();
1729
- this.logger.log("info", `Executing task: ${task.name} (${task.id})`);
2125
+ const startMs = Date.now();
2126
+ this.logger.log(
2127
+ "info",
2128
+ `Executing task: ${task.name} (${task.id})
2129
+ type: ${task.taskType}
2130
+ schedule: ${task.schedule} (${task.scheduleType})
2131
+ enabled: ${task.enabled}`
2132
+ );
1730
2133
  try {
2134
+ const executionPayload = resolveTaskExecutionPayload(
2135
+ task.taskType,
2136
+ task.payload,
2137
+ this.workspacePath
2138
+ );
2139
+ validateTaskPayload(task.taskType, executionPayload);
1731
2140
  const executor = getExecutor(task.taskType);
1732
- const result = await executor.execute(task.payload);
2141
+ const result = await executor.execute(executionPayload);
1733
2142
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2143
+ const durationMs = Date.now() - startMs;
1734
2144
  this.logManager.appendLog({
1735
2145
  taskId: task.id,
1736
2146
  taskName: task.name,
@@ -1740,9 +2150,17 @@ var SchedulerDaemon = class {
1740
2150
  output: result.output,
1741
2151
  error: result.error
1742
2152
  });
1743
- this.logger.log("info", `Task ${task.name} completed: ${result.status}`);
2153
+ const outputBytes = result.output ? Buffer.byteLength(result.output) : 0;
2154
+ this.logger.log(
2155
+ "info",
2156
+ `Task completed: ${task.name} (${task.id})
2157
+ status: ${result.status}
2158
+ duration: ${durationMs}ms
2159
+ outputBytes: ${outputBytes}`
2160
+ );
1744
2161
  } catch (err) {
1745
2162
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2163
+ const durationMs = Date.now() - startMs;
1746
2164
  const message = err instanceof Error ? err.message : String(err);
1747
2165
  this.logManager.appendLog({
1748
2166
  taskId: task.id,
@@ -1752,7 +2170,12 @@ var SchedulerDaemon = class {
1752
2170
  status: "failure",
1753
2171
  error: message
1754
2172
  });
1755
- this.logger.log("error", `Task ${task.name} failed: ${message}`);
2173
+ this.logger.log(
2174
+ "error",
2175
+ `Task failed: ${task.name} (${task.id})
2176
+ duration: ${durationMs}ms
2177
+ error: ${message}`
2178
+ );
1756
2179
  } finally {
1757
2180
  this.taskRunning.delete(task.id);
1758
2181
  }
@@ -1809,164 +2232,6 @@ function matchCronField(field, value) {
1809
2232
  const values = field.split(",");
1810
2233
  return values.some((v) => Number.parseInt(v, 10) === value);
1811
2234
  }
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
2235
  // Annotate the CommonJS export names for ESM import in node:
1971
2236
  0 && (module.exports = {
1972
2237
  DaemonLogger,
@@ -1996,5 +2261,7 @@ var TaskExecutionEngine = class {
1996
2261
  moveFiles,
1997
2262
  noopLogger,
1998
2263
  parseIntervalMs,
1999
- unzipFile
2264
+ resolveTaskExecutionPayload,
2265
+ unzipFile,
2266
+ validateTaskPayload
2000
2267
  });