@serviceme/devtools-core 0.1.2 → 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 {
@@ -1408,41 +1564,74 @@ var executors = {
1408
1564
  github_copilot_cli: new GithubCopilotCliExecutor()
1409
1565
  };
1410
1566
  function getExecutor(taskType) {
1411
- return executors[taskType];
1567
+ const executor = executors[taskType];
1568
+ if (!executor) {
1569
+ throw new Error(`Unsupported bridge task type: ${taskType}`);
1570
+ }
1571
+ return executor;
1412
1572
  }
1413
1573
 
1414
1574
  // src/scheduled-tasks/TaskConfigManager.ts
1415
1575
  var import_node_crypto = require("crypto");
1416
- var fs5 = __toESM(require("fs"));
1417
- 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");
1418
1579
  var CONFIG_DIR3 = ".serviceme";
1419
1580
  var CONFIG_FILE = "scheduled-tasks.json";
1420
1581
  function emptyConfig() {
1421
1582
  return { version: 1, tasks: [] };
1422
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
+ }
1423
1612
  var TaskConfigManager = class {
1424
1613
  constructor(workspacePath) {
1425
- this.configPath = path5.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1614
+ this.configPath = path6.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1426
1615
  }
1427
1616
  getConfigPath() {
1428
1617
  return this.configPath;
1429
1618
  }
1430
1619
  readConfig() {
1431
- if (!fs5.existsSync(this.configPath)) {
1620
+ if (!fs7.existsSync(this.configPath)) {
1432
1621
  return emptyConfig();
1433
1622
  }
1434
- const raw = fs5.readFileSync(this.configPath, "utf-8");
1623
+ const raw = fs7.readFileSync(this.configPath, "utf-8");
1435
1624
  const parsed = JSON.parse(raw);
1436
1625
  return parsed;
1437
1626
  }
1438
1627
  writeConfig(config) {
1439
- const dir = path5.dirname(this.configPath);
1440
- if (!fs5.existsSync(dir)) {
1441
- fs5.mkdirSync(dir, { recursive: true });
1628
+ const dir = path6.dirname(this.configPath);
1629
+ if (!fs7.existsSync(dir)) {
1630
+ fs7.mkdirSync(dir, { recursive: true });
1442
1631
  }
1443
1632
  const tmp = `${this.configPath}.tmp`;
1444
- fs5.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1445
- fs5.renameSync(tmp, this.configPath);
1633
+ fs7.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1634
+ fs7.renameSync(tmp, this.configPath);
1446
1635
  }
1447
1636
  listTasks() {
1448
1637
  return this.readConfig().tasks;
@@ -1454,6 +1643,7 @@ var TaskConfigManager = class {
1454
1643
  return this.readConfig().tasks.find((t) => t.name === name);
1455
1644
  }
1456
1645
  createTask(input) {
1646
+ validateTaskPayload(input.taskType, input.payload);
1457
1647
  const config = this.readConfig();
1458
1648
  const now = (/* @__PURE__ */ new Date()).toISOString();
1459
1649
  const task = {
@@ -1482,6 +1672,9 @@ var TaskConfigManager = class {
1482
1672
  if (!existing) {
1483
1673
  throw new Error(`Task '${id}' not found`);
1484
1674
  }
1675
+ const nextTaskType = input.taskType ?? existing.taskType;
1676
+ const nextPayload = input.payload ?? existing.payload;
1677
+ validateTaskPayload(nextTaskType, nextPayload);
1485
1678
  const updated = {
1486
1679
  ...existing,
1487
1680
  ...input.name !== void 0 && { name: input.name },
@@ -1520,10 +1713,197 @@ var TaskConfigManager = class {
1520
1713
  }
1521
1714
  };
1522
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
+
1523
1903
  // src/scheduled-tasks/TaskLogManager.ts
1524
1904
  var import_node_crypto2 = require("crypto");
1525
- var fs6 = __toESM(require("fs"));
1526
- var path6 = __toESM(require("path"));
1905
+ var fs8 = __toESM(require("fs"));
1906
+ var path7 = __toESM(require("path"));
1527
1907
  var CONFIG_DIR4 = ".serviceme";
1528
1908
  var LOG_FILE2 = "scheduled-tasks-log.json";
1529
1909
  var MAX_LOGS = 200;
@@ -1548,17 +1928,17 @@ function validateAndRepairLogFile(raw) {
1548
1928
  }
1549
1929
  var TaskLogManager = class {
1550
1930
  constructor(workspacePath) {
1551
- this.logPath = path6.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
1931
+ this.logPath = path7.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
1552
1932
  }
1553
1933
  getLogPath() {
1554
1934
  return this.logPath;
1555
1935
  }
1556
1936
  readLogFile() {
1557
- if (!fs6.existsSync(this.logPath)) {
1937
+ if (!fs8.existsSync(this.logPath)) {
1558
1938
  return emptyLogFile();
1559
1939
  }
1560
1940
  try {
1561
- const raw = fs6.readFileSync(this.logPath, "utf-8");
1941
+ const raw = fs8.readFileSync(this.logPath, "utf-8");
1562
1942
  const parsed = JSON.parse(raw);
1563
1943
  const file = validateAndRepairLogFile(parsed);
1564
1944
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.logs) || parsed.logs.length !== file.logs.length) {
@@ -1574,21 +1954,21 @@ var TaskLogManager = class {
1574
1954
  }
1575
1955
  backupCorruptedFile() {
1576
1956
  try {
1577
- if (fs6.existsSync(this.logPath)) {
1957
+ if (fs8.existsSync(this.logPath)) {
1578
1958
  const backupPath = `${this.logPath}.corrupted.${Date.now()}`;
1579
- fs6.copyFileSync(this.logPath, backupPath);
1959
+ fs8.copyFileSync(this.logPath, backupPath);
1580
1960
  }
1581
1961
  } catch {
1582
1962
  }
1583
1963
  }
1584
1964
  writeLogFile(file) {
1585
- const dir = path6.dirname(this.logPath);
1586
- if (!fs6.existsSync(dir)) {
1587
- fs6.mkdirSync(dir, { recursive: true });
1965
+ const dir = path7.dirname(this.logPath);
1966
+ if (!fs8.existsSync(dir)) {
1967
+ fs8.mkdirSync(dir, { recursive: true });
1588
1968
  }
1589
1969
  const tmp = `${this.logPath}.tmp`;
1590
- fs6.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
1591
- fs6.renameSync(tmp, this.logPath);
1970
+ fs8.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
1971
+ fs8.renameSync(tmp, this.logPath);
1592
1972
  }
1593
1973
  appendLog(input) {
1594
1974
  const file = this.readLogFile();
@@ -1658,7 +2038,23 @@ var SchedulerDaemon = class {
1658
2038
  this.running = true;
1659
2039
  this.startTime = Date.now();
1660
2040
  this.pidManager.writePid(process.pid);
1661
- 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
+ );
1662
2058
  this.tickTimer = setInterval(() => this.tick(), TICK_INTERVAL);
1663
2059
  this.setupConfigWatch();
1664
2060
  process.on("SIGTERM", () => this.stop());
@@ -1683,8 +2079,8 @@ var SchedulerDaemon = class {
1683
2079
  const configPath = this.configManager.getConfigPath();
1684
2080
  const dir = configPath.substring(0, configPath.lastIndexOf("/"));
1685
2081
  try {
1686
- if (fs7.existsSync(dir)) {
1687
- this.watcher = fs7.watch(dir, (_eventType, filename) => {
2082
+ if (fs9.existsSync(dir)) {
2083
+ this.watcher = fs9.watch(dir, (_eventType, filename) => {
1688
2084
  if (filename === "scheduled-tasks.json") {
1689
2085
  this.logger.log("info", "Config file changed, reconciling...");
1690
2086
  }
@@ -1700,7 +2096,11 @@ var SchedulerDaemon = class {
1700
2096
  for (const task of config.tasks) {
1701
2097
  if (!task.enabled) continue;
1702
2098
  if (this.taskRunning.has(task.id)) continue;
1703
- 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
+ }
1704
2104
  if (this.shouldRun(task, lastExec, now)) {
1705
2105
  this.executeTask(task, now);
1706
2106
  }
@@ -1722,11 +2122,25 @@ var SchedulerDaemon = class {
1722
2122
  this.taskRunning.add(task.id);
1723
2123
  this.lastRun.set(task.id, now);
1724
2124
  const startedAt = new Date(now).toISOString();
1725
- 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
+ );
1726
2133
  try {
2134
+ const executionPayload = resolveTaskExecutionPayload(
2135
+ task.taskType,
2136
+ task.payload,
2137
+ this.workspacePath
2138
+ );
2139
+ validateTaskPayload(task.taskType, executionPayload);
1727
2140
  const executor = getExecutor(task.taskType);
1728
- const result = await executor.execute(task.payload);
2141
+ const result = await executor.execute(executionPayload);
1729
2142
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2143
+ const durationMs = Date.now() - startMs;
1730
2144
  this.logManager.appendLog({
1731
2145
  taskId: task.id,
1732
2146
  taskName: task.name,
@@ -1736,9 +2150,17 @@ var SchedulerDaemon = class {
1736
2150
  output: result.output,
1737
2151
  error: result.error
1738
2152
  });
1739
- 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
+ );
1740
2161
  } catch (err) {
1741
2162
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2163
+ const durationMs = Date.now() - startMs;
1742
2164
  const message = err instanceof Error ? err.message : String(err);
1743
2165
  this.logManager.appendLog({
1744
2166
  taskId: task.id,
@@ -1748,7 +2170,12 @@ var SchedulerDaemon = class {
1748
2170
  status: "failure",
1749
2171
  error: message
1750
2172
  });
1751
- 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
+ );
1752
2179
  } finally {
1753
2180
  this.taskRunning.delete(task.id);
1754
2181
  }
@@ -1805,164 +2232,6 @@ function matchCronField(field, value) {
1805
2232
  const values = field.split(",");
1806
2233
  return values.some((v) => Number.parseInt(v, 10) === value);
1807
2234
  }
1808
-
1809
- // src/scheduled-tasks/TaskExecutionEngine.ts
1810
- var TaskExecutionEngine = class {
1811
- constructor(getExecutor2) {
1812
- this.getExecutor = getExecutor2;
1813
- this.running = /* @__PURE__ */ new Map();
1814
- this.taskExecutions = /* @__PURE__ */ new Map();
1815
- this.listener = null;
1816
- }
1817
- setListener(listener) {
1818
- this.listener = listener;
1819
- }
1820
- async execute(snapshot) {
1821
- const policy = snapshot.concurrencyPolicy ?? "reject";
1822
- if (policy === "reject") {
1823
- const existingIds = this.taskExecutions.get(snapshot.taskId);
1824
- if (existingIds && existingIds.size > 0) {
1825
- throw new Error(
1826
- `TASK_ALREADY_RUNNING: Task ${snapshot.taskId} is already running`
1827
- );
1828
- }
1829
- }
1830
- const executor = this.getExecutor(snapshot.type);
1831
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1832
- if (!this.taskExecutions.has(snapshot.taskId)) {
1833
- this.taskExecutions.set(snapshot.taskId, /* @__PURE__ */ new Set());
1834
- }
1835
- this.taskExecutions.get(snapshot.taskId)?.add(snapshot.executionId);
1836
- const ac = new AbortController();
1837
- const runningExec = {
1838
- executionId: snapshot.executionId,
1839
- taskId: snapshot.taskId,
1840
- startedAt,
1841
- cancel: () => ac.abort()
1842
- };
1843
- this.running.set(snapshot.executionId, runningExec);
1844
- this.listener?.onStarted({
1845
- executionId: snapshot.executionId,
1846
- taskId: snapshot.taskId,
1847
- startedAt
1848
- });
1849
- try {
1850
- let result;
1851
- if (isStreamingTaskExecutor(executor)) {
1852
- const handle = executor.executeStreaming(
1853
- snapshot.payload,
1854
- (stream, data) => {
1855
- this.listener?.onOutput({
1856
- executionId: snapshot.executionId,
1857
- stream,
1858
- data
1859
- });
1860
- },
1861
- ac.signal
1862
- );
1863
- runningExec.cancel = () => {
1864
- handle.cancel();
1865
- ac.abort();
1866
- };
1867
- result = await handle.result;
1868
- } else {
1869
- result = await executor.execute(snapshot.payload, ac.signal);
1870
- }
1871
- const log = {
1872
- id: snapshot.executionId,
1873
- taskId: snapshot.taskId,
1874
- taskName: snapshot.name,
1875
- startedAt,
1876
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1877
- status: result.status === "running" ? "failure" : result.status,
1878
- output: result.output,
1879
- error: result.error
1880
- };
1881
- switch (log.status) {
1882
- case "success":
1883
- this.listener?.onCompleted({
1884
- executionId: snapshot.executionId,
1885
- log
1886
- });
1887
- break;
1888
- case "cancelled":
1889
- this.listener?.onCancelled({
1890
- executionId: snapshot.executionId,
1891
- log
1892
- });
1893
- break;
1894
- case "timeout":
1895
- this.listener?.onFailed({
1896
- executionId: snapshot.executionId,
1897
- status: "timeout",
1898
- reason: "timeout",
1899
- log
1900
- });
1901
- break;
1902
- default:
1903
- this.listener?.onFailed({
1904
- executionId: snapshot.executionId,
1905
- status: "failure",
1906
- reason: "error",
1907
- log
1908
- });
1909
- break;
1910
- }
1911
- } catch (err) {
1912
- const log = {
1913
- id: snapshot.executionId,
1914
- taskId: snapshot.taskId,
1915
- taskName: snapshot.name,
1916
- startedAt,
1917
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1918
- status: ac.signal.aborted ? "cancelled" : "failure",
1919
- error: err instanceof Error ? err.message : String(err)
1920
- };
1921
- if (ac.signal.aborted) {
1922
- this.listener?.onCancelled({
1923
- executionId: snapshot.executionId,
1924
- log
1925
- });
1926
- } else {
1927
- this.listener?.onFailed({
1928
- executionId: snapshot.executionId,
1929
- status: "failure",
1930
- reason: "error",
1931
- log
1932
- });
1933
- }
1934
- } finally {
1935
- this.running.delete(snapshot.executionId);
1936
- const taskExecIds = this.taskExecutions.get(snapshot.taskId);
1937
- if (taskExecIds) {
1938
- taskExecIds.delete(snapshot.executionId);
1939
- if (taskExecIds.size === 0) {
1940
- this.taskExecutions.delete(snapshot.taskId);
1941
- }
1942
- }
1943
- }
1944
- }
1945
- cancel(executionId) {
1946
- const exec = this.running.get(executionId);
1947
- if (!exec) return false;
1948
- exec.cancel();
1949
- return true;
1950
- }
1951
- listRunning() {
1952
- return Array.from(this.running.values()).map((e) => ({
1953
- executionId: e.executionId,
1954
- taskId: e.taskId,
1955
- startedAt: e.startedAt
1956
- }));
1957
- }
1958
- dispose() {
1959
- for (const exec of this.running.values()) {
1960
- exec.cancel();
1961
- }
1962
- this.running.clear();
1963
- this.taskExecutions.clear();
1964
- }
1965
- };
1966
2235
  // Annotate the CommonJS export names for ESM import in node:
1967
2236
  0 && (module.exports = {
1968
2237
  DaemonLogger,
@@ -1992,5 +2261,7 @@ var TaskExecutionEngine = class {
1992
2261
  moveFiles,
1993
2262
  noopLogger,
1994
2263
  parseIntervalMs,
1995
- unzipFile
2264
+ resolveTaskExecutionPayload,
2265
+ unzipFile,
2266
+ validateTaskPayload
1996
2267
  });