@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.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 {
@@ -1376,41 +1530,74 @@ var executors = {
1376
1530
  github_copilot_cli: new GithubCopilotCliExecutor()
1377
1531
  };
1378
1532
  function getExecutor(taskType) {
1379
- return executors[taskType];
1533
+ const executor = executors[taskType];
1534
+ if (!executor) {
1535
+ throw new Error(`Unsupported bridge task type: ${taskType}`);
1536
+ }
1537
+ return executor;
1380
1538
  }
1381
1539
 
1382
1540
  // src/scheduled-tasks/TaskConfigManager.ts
1383
1541
  import { randomUUID } from "crypto";
1384
- import * as fs5 from "fs";
1385
- 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";
1386
1545
  var CONFIG_DIR3 = ".serviceme";
1387
1546
  var CONFIG_FILE = "scheduled-tasks.json";
1388
1547
  function emptyConfig() {
1389
1548
  return { version: 1, tasks: [] };
1390
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
+ }
1391
1578
  var TaskConfigManager = class {
1392
1579
  constructor(workspacePath) {
1393
- this.configPath = path5.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1580
+ this.configPath = path6.join(workspacePath, CONFIG_DIR3, CONFIG_FILE);
1394
1581
  }
1395
1582
  getConfigPath() {
1396
1583
  return this.configPath;
1397
1584
  }
1398
1585
  readConfig() {
1399
- if (!fs5.existsSync(this.configPath)) {
1586
+ if (!fs7.existsSync(this.configPath)) {
1400
1587
  return emptyConfig();
1401
1588
  }
1402
- const raw = fs5.readFileSync(this.configPath, "utf-8");
1589
+ const raw = fs7.readFileSync(this.configPath, "utf-8");
1403
1590
  const parsed = JSON.parse(raw);
1404
1591
  return parsed;
1405
1592
  }
1406
1593
  writeConfig(config) {
1407
- const dir = path5.dirname(this.configPath);
1408
- if (!fs5.existsSync(dir)) {
1409
- fs5.mkdirSync(dir, { recursive: true });
1594
+ const dir = path6.dirname(this.configPath);
1595
+ if (!fs7.existsSync(dir)) {
1596
+ fs7.mkdirSync(dir, { recursive: true });
1410
1597
  }
1411
1598
  const tmp = `${this.configPath}.tmp`;
1412
- fs5.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1413
- fs5.renameSync(tmp, this.configPath);
1599
+ fs7.writeFileSync(tmp, JSON.stringify(config, null, " "), "utf-8");
1600
+ fs7.renameSync(tmp, this.configPath);
1414
1601
  }
1415
1602
  listTasks() {
1416
1603
  return this.readConfig().tasks;
@@ -1422,6 +1609,7 @@ var TaskConfigManager = class {
1422
1609
  return this.readConfig().tasks.find((t) => t.name === name);
1423
1610
  }
1424
1611
  createTask(input) {
1612
+ validateTaskPayload(input.taskType, input.payload);
1425
1613
  const config = this.readConfig();
1426
1614
  const now = (/* @__PURE__ */ new Date()).toISOString();
1427
1615
  const task = {
@@ -1450,6 +1638,9 @@ var TaskConfigManager = class {
1450
1638
  if (!existing) {
1451
1639
  throw new Error(`Task '${id}' not found`);
1452
1640
  }
1641
+ const nextTaskType = input.taskType ?? existing.taskType;
1642
+ const nextPayload = input.payload ?? existing.payload;
1643
+ validateTaskPayload(nextTaskType, nextPayload);
1453
1644
  const updated = {
1454
1645
  ...existing,
1455
1646
  ...input.name !== void 0 && { name: input.name },
@@ -1488,10 +1679,197 @@ var TaskConfigManager = class {
1488
1679
  }
1489
1680
  };
1490
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
+
1491
1869
  // src/scheduled-tasks/TaskLogManager.ts
1492
1870
  import { randomUUID as randomUUID2 } from "crypto";
1493
- import * as fs6 from "fs";
1494
- import * as path6 from "path";
1871
+ import * as fs8 from "fs";
1872
+ import * as path7 from "path";
1495
1873
  var CONFIG_DIR4 = ".serviceme";
1496
1874
  var LOG_FILE2 = "scheduled-tasks-log.json";
1497
1875
  var MAX_LOGS = 200;
@@ -1516,17 +1894,17 @@ function validateAndRepairLogFile(raw) {
1516
1894
  }
1517
1895
  var TaskLogManager = class {
1518
1896
  constructor(workspacePath) {
1519
- this.logPath = path6.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
1897
+ this.logPath = path7.join(workspacePath, CONFIG_DIR4, LOG_FILE2);
1520
1898
  }
1521
1899
  getLogPath() {
1522
1900
  return this.logPath;
1523
1901
  }
1524
1902
  readLogFile() {
1525
- if (!fs6.existsSync(this.logPath)) {
1903
+ if (!fs8.existsSync(this.logPath)) {
1526
1904
  return emptyLogFile();
1527
1905
  }
1528
1906
  try {
1529
- const raw = fs6.readFileSync(this.logPath, "utf-8");
1907
+ const raw = fs8.readFileSync(this.logPath, "utf-8");
1530
1908
  const parsed = JSON.parse(raw);
1531
1909
  const file = validateAndRepairLogFile(parsed);
1532
1910
  if (!parsed || typeof parsed !== "object" || !Array.isArray(parsed.logs) || parsed.logs.length !== file.logs.length) {
@@ -1542,21 +1920,21 @@ var TaskLogManager = class {
1542
1920
  }
1543
1921
  backupCorruptedFile() {
1544
1922
  try {
1545
- if (fs6.existsSync(this.logPath)) {
1923
+ if (fs8.existsSync(this.logPath)) {
1546
1924
  const backupPath = `${this.logPath}.corrupted.${Date.now()}`;
1547
- fs6.copyFileSync(this.logPath, backupPath);
1925
+ fs8.copyFileSync(this.logPath, backupPath);
1548
1926
  }
1549
1927
  } catch {
1550
1928
  }
1551
1929
  }
1552
1930
  writeLogFile(file) {
1553
- const dir = path6.dirname(this.logPath);
1554
- if (!fs6.existsSync(dir)) {
1555
- fs6.mkdirSync(dir, { recursive: true });
1931
+ const dir = path7.dirname(this.logPath);
1932
+ if (!fs8.existsSync(dir)) {
1933
+ fs8.mkdirSync(dir, { recursive: true });
1556
1934
  }
1557
1935
  const tmp = `${this.logPath}.tmp`;
1558
- fs6.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
1559
- fs6.renameSync(tmp, this.logPath);
1936
+ fs8.writeFileSync(tmp, JSON.stringify(file, null, " "), "utf-8");
1937
+ fs8.renameSync(tmp, this.logPath);
1560
1938
  }
1561
1939
  appendLog(input) {
1562
1940
  const file = this.readLogFile();
@@ -1626,7 +2004,23 @@ var SchedulerDaemon = class {
1626
2004
  this.running = true;
1627
2005
  this.startTime = Date.now();
1628
2006
  this.pidManager.writePid(process.pid);
1629
- 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
+ );
1630
2024
  this.tickTimer = setInterval(() => this.tick(), TICK_INTERVAL);
1631
2025
  this.setupConfigWatch();
1632
2026
  process.on("SIGTERM", () => this.stop());
@@ -1651,8 +2045,8 @@ var SchedulerDaemon = class {
1651
2045
  const configPath = this.configManager.getConfigPath();
1652
2046
  const dir = configPath.substring(0, configPath.lastIndexOf("/"));
1653
2047
  try {
1654
- if (fs7.existsSync(dir)) {
1655
- this.watcher = fs7.watch(dir, (_eventType, filename) => {
2048
+ if (fs9.existsSync(dir)) {
2049
+ this.watcher = fs9.watch(dir, (_eventType, filename) => {
1656
2050
  if (filename === "scheduled-tasks.json") {
1657
2051
  this.logger.log("info", "Config file changed, reconciling...");
1658
2052
  }
@@ -1668,7 +2062,11 @@ var SchedulerDaemon = class {
1668
2062
  for (const task of config.tasks) {
1669
2063
  if (!task.enabled) continue;
1670
2064
  if (this.taskRunning.has(task.id)) continue;
1671
- 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
+ }
1672
2070
  if (this.shouldRun(task, lastExec, now)) {
1673
2071
  this.executeTask(task, now);
1674
2072
  }
@@ -1690,11 +2088,25 @@ var SchedulerDaemon = class {
1690
2088
  this.taskRunning.add(task.id);
1691
2089
  this.lastRun.set(task.id, now);
1692
2090
  const startedAt = new Date(now).toISOString();
1693
- 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
+ );
1694
2099
  try {
2100
+ const executionPayload = resolveTaskExecutionPayload(
2101
+ task.taskType,
2102
+ task.payload,
2103
+ this.workspacePath
2104
+ );
2105
+ validateTaskPayload(task.taskType, executionPayload);
1695
2106
  const executor = getExecutor(task.taskType);
1696
- const result = await executor.execute(task.payload);
2107
+ const result = await executor.execute(executionPayload);
1697
2108
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2109
+ const durationMs = Date.now() - startMs;
1698
2110
  this.logManager.appendLog({
1699
2111
  taskId: task.id,
1700
2112
  taskName: task.name,
@@ -1704,9 +2116,17 @@ var SchedulerDaemon = class {
1704
2116
  output: result.output,
1705
2117
  error: result.error
1706
2118
  });
1707
- 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
+ );
1708
2127
  } catch (err) {
1709
2128
  const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
2129
+ const durationMs = Date.now() - startMs;
1710
2130
  const message = err instanceof Error ? err.message : String(err);
1711
2131
  this.logManager.appendLog({
1712
2132
  taskId: task.id,
@@ -1716,7 +2136,12 @@ var SchedulerDaemon = class {
1716
2136
  status: "failure",
1717
2137
  error: message
1718
2138
  });
1719
- 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
+ );
1720
2145
  } finally {
1721
2146
  this.taskRunning.delete(task.id);
1722
2147
  }
@@ -1773,164 +2198,6 @@ function matchCronField(field, value) {
1773
2198
  const values = field.split(",");
1774
2199
  return values.some((v) => Number.parseInt(v, 10) === value);
1775
2200
  }
1776
-
1777
- // src/scheduled-tasks/TaskExecutionEngine.ts
1778
- var TaskExecutionEngine = class {
1779
- constructor(getExecutor2) {
1780
- this.getExecutor = getExecutor2;
1781
- this.running = /* @__PURE__ */ new Map();
1782
- this.taskExecutions = /* @__PURE__ */ new Map();
1783
- this.listener = null;
1784
- }
1785
- setListener(listener) {
1786
- this.listener = listener;
1787
- }
1788
- async execute(snapshot) {
1789
- const policy = snapshot.concurrencyPolicy ?? "reject";
1790
- if (policy === "reject") {
1791
- const existingIds = this.taskExecutions.get(snapshot.taskId);
1792
- if (existingIds && existingIds.size > 0) {
1793
- throw new Error(
1794
- `TASK_ALREADY_RUNNING: Task ${snapshot.taskId} is already running`
1795
- );
1796
- }
1797
- }
1798
- const executor = this.getExecutor(snapshot.type);
1799
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1800
- if (!this.taskExecutions.has(snapshot.taskId)) {
1801
- this.taskExecutions.set(snapshot.taskId, /* @__PURE__ */ new Set());
1802
- }
1803
- this.taskExecutions.get(snapshot.taskId)?.add(snapshot.executionId);
1804
- const ac = new AbortController();
1805
- const runningExec = {
1806
- executionId: snapshot.executionId,
1807
- taskId: snapshot.taskId,
1808
- startedAt,
1809
- cancel: () => ac.abort()
1810
- };
1811
- this.running.set(snapshot.executionId, runningExec);
1812
- this.listener?.onStarted({
1813
- executionId: snapshot.executionId,
1814
- taskId: snapshot.taskId,
1815
- startedAt
1816
- });
1817
- try {
1818
- let result;
1819
- if (isStreamingTaskExecutor(executor)) {
1820
- const handle = executor.executeStreaming(
1821
- snapshot.payload,
1822
- (stream, data) => {
1823
- this.listener?.onOutput({
1824
- executionId: snapshot.executionId,
1825
- stream,
1826
- data
1827
- });
1828
- },
1829
- ac.signal
1830
- );
1831
- runningExec.cancel = () => {
1832
- handle.cancel();
1833
- ac.abort();
1834
- };
1835
- result = await handle.result;
1836
- } else {
1837
- result = await executor.execute(snapshot.payload, ac.signal);
1838
- }
1839
- const log = {
1840
- id: snapshot.executionId,
1841
- taskId: snapshot.taskId,
1842
- taskName: snapshot.name,
1843
- startedAt,
1844
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1845
- status: result.status === "running" ? "failure" : result.status,
1846
- output: result.output,
1847
- error: result.error
1848
- };
1849
- switch (log.status) {
1850
- case "success":
1851
- this.listener?.onCompleted({
1852
- executionId: snapshot.executionId,
1853
- log
1854
- });
1855
- break;
1856
- case "cancelled":
1857
- this.listener?.onCancelled({
1858
- executionId: snapshot.executionId,
1859
- log
1860
- });
1861
- break;
1862
- case "timeout":
1863
- this.listener?.onFailed({
1864
- executionId: snapshot.executionId,
1865
- status: "timeout",
1866
- reason: "timeout",
1867
- log
1868
- });
1869
- break;
1870
- default:
1871
- this.listener?.onFailed({
1872
- executionId: snapshot.executionId,
1873
- status: "failure",
1874
- reason: "error",
1875
- log
1876
- });
1877
- break;
1878
- }
1879
- } catch (err) {
1880
- const log = {
1881
- id: snapshot.executionId,
1882
- taskId: snapshot.taskId,
1883
- taskName: snapshot.name,
1884
- startedAt,
1885
- finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1886
- status: ac.signal.aborted ? "cancelled" : "failure",
1887
- error: err instanceof Error ? err.message : String(err)
1888
- };
1889
- if (ac.signal.aborted) {
1890
- this.listener?.onCancelled({
1891
- executionId: snapshot.executionId,
1892
- log
1893
- });
1894
- } else {
1895
- this.listener?.onFailed({
1896
- executionId: snapshot.executionId,
1897
- status: "failure",
1898
- reason: "error",
1899
- log
1900
- });
1901
- }
1902
- } finally {
1903
- this.running.delete(snapshot.executionId);
1904
- const taskExecIds = this.taskExecutions.get(snapshot.taskId);
1905
- if (taskExecIds) {
1906
- taskExecIds.delete(snapshot.executionId);
1907
- if (taskExecIds.size === 0) {
1908
- this.taskExecutions.delete(snapshot.taskId);
1909
- }
1910
- }
1911
- }
1912
- }
1913
- cancel(executionId) {
1914
- const exec = this.running.get(executionId);
1915
- if (!exec) return false;
1916
- exec.cancel();
1917
- return true;
1918
- }
1919
- listRunning() {
1920
- return Array.from(this.running.values()).map((e) => ({
1921
- executionId: e.executionId,
1922
- taskId: e.taskId,
1923
- startedAt: e.startedAt
1924
- }));
1925
- }
1926
- dispose() {
1927
- for (const exec of this.running.values()) {
1928
- exec.cancel();
1929
- }
1930
- this.running.clear();
1931
- this.taskExecutions.clear();
1932
- }
1933
- };
1934
2201
  export {
1935
2202
  DaemonLogger,
1936
2203
  EnvironmentInspector,
@@ -1959,5 +2226,7 @@ export {
1959
2226
  moveFiles,
1960
2227
  noopLogger,
1961
2228
  parseIntervalMs,
1962
- unzipFile
2229
+ resolveTaskExecutionPayload,
2230
+ unzipFile,
2231
+ validateTaskPayload
1963
2232
  };