palmier 0.4.3 → 0.4.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/README.md +5 -3
- package/dist/commands/request-input.d.ts +1 -2
- package/dist/commands/request-input.js +7 -21
- package/dist/commands/run.d.ts +4 -0
- package/dist/commands/run.js +43 -53
- package/dist/commands/serve.js +31 -33
- package/dist/platform/linux.js +16 -6
- package/dist/platform/windows.js +37 -12
- package/dist/rpc-handler.js +177 -30
- package/dist/task.d.ts +13 -13
- package/dist/task.js +59 -51
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/commands/request-input.ts +7 -21
- package/src/commands/run.ts +43 -56
- package/src/commands/serve.ts +34 -41
- package/src/platform/linux.ts +17 -7
- package/src/platform/windows.ts +36 -13
- package/src/rpc-handler.ts +195 -34
- package/src/task.ts +60 -55
- package/src/types.ts +2 -2
package/src/commands/run.ts
CHANGED
|
@@ -4,7 +4,7 @@ import * as readline from "readline";
|
|
|
4
4
|
import { spawnCommand, spawnStreamingCommand } from "../spawn-command.js";
|
|
5
5
|
import { loadConfig } from "../config.js";
|
|
6
6
|
import { connectNats } from "../nats-client.js";
|
|
7
|
-
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory,
|
|
7
|
+
import { parseTaskFile, getTaskDir, writeTaskFile, writeTaskStatus, readTaskStatus, appendHistory, createRunDir, appendRunMessage, readRunMessages, getRunDir } from "../task.js";
|
|
8
8
|
import { getAgent } from "../agents/agent.js";
|
|
9
9
|
import { getPlatform } from "../platform/index.js";
|
|
10
10
|
import { TASK_SUCCESS_MARKER, TASK_FAILURE_MARKER, TASK_REPORT_PREFIX, TASK_PERMISSION_PREFIX } from "../agents/shared-prompt.js";
|
|
@@ -22,7 +22,7 @@ interface InvocationContext {
|
|
|
22
22
|
agent: AgentTool;
|
|
23
23
|
task: ParsedTask;
|
|
24
24
|
taskDir: string;
|
|
25
|
-
|
|
25
|
+
runId: string;
|
|
26
26
|
guiEnv: Record<string, string>;
|
|
27
27
|
nc: NatsConnection | undefined;
|
|
28
28
|
config: HostConfig;
|
|
@@ -51,8 +51,8 @@ async function invokeAgentWithRetry(
|
|
|
51
51
|
while (true) {
|
|
52
52
|
const { command, args, stdin } = ctx.agent.getTaskRunCommandLine(invokeTask, retryPrompt, ctx.transientPermissions);
|
|
53
53
|
const result = await spawnCommand(command, args, {
|
|
54
|
-
cwd: ctx.taskDir,
|
|
55
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id,
|
|
54
|
+
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
55
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
|
|
56
56
|
echoStdout: true,
|
|
57
57
|
resolveOnFailure: true,
|
|
58
58
|
stdin,
|
|
@@ -118,7 +118,7 @@ async function invokeAgentWithRetry(
|
|
|
118
118
|
/**
|
|
119
119
|
* Strip [PALMIER_*] marker lines from agent output.
|
|
120
120
|
*/
|
|
121
|
-
function stripPalmierMarkers(output: string): string {
|
|
121
|
+
export function stripPalmierMarkers(output: string): string {
|
|
122
122
|
return output.split("\n").filter((l) => !l.startsWith("[PALMIER")).join("\n").trim();
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -127,22 +127,24 @@ function stripPalmierMarkers(output: string): string {
|
|
|
127
127
|
*/
|
|
128
128
|
async function appendAndNotify(
|
|
129
129
|
ctx: InvocationContext,
|
|
130
|
-
msg: Parameters<typeof
|
|
130
|
+
msg: Parameters<typeof appendRunMessage>[2],
|
|
131
131
|
): Promise<void> {
|
|
132
|
-
|
|
133
|
-
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated" });
|
|
132
|
+
appendRunMessage(ctx.taskDir, ctx.runId, msg);
|
|
133
|
+
await publishHostEvent(ctx.nc, ctx.config.hostId, ctx.taskId, { event_type: "result-updated", run_id: ctx.runId });
|
|
134
134
|
}
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
|
-
* Find
|
|
137
|
+
* Find the latest run dir that has no status messages yet (just created by the RPC handler).
|
|
138
138
|
*/
|
|
139
|
-
function
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
function findLatestPendingRunId(taskDir: string): string | null {
|
|
140
|
+
const dirs = fs.readdirSync(taskDir)
|
|
141
|
+
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
|
|
142
|
+
.sort();
|
|
143
|
+
if (dirs.length === 0) return null;
|
|
144
|
+
const latest = dirs[dirs.length - 1];
|
|
145
|
+
const messages = readRunMessages(taskDir, latest);
|
|
146
|
+
const hasStatus = messages.some((m) => m.role === "status");
|
|
147
|
+
return hasStatus ? null : latest;
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
/**
|
|
@@ -167,15 +169,11 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
167
169
|
let nc: NatsConnection | undefined;
|
|
168
170
|
const taskName = task.frontmatter.name;
|
|
169
171
|
|
|
170
|
-
//
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// Snapshot the task file at run time
|
|
176
|
-
const taskSnapshotName = `TASK-${startTime}.md`;
|
|
177
|
-
if (!fs.existsSync(path.join(taskDir, taskSnapshotName))) {
|
|
178
|
-
fs.copyFileSync(path.join(taskDir, "TASK.md"), path.join(taskDir, taskSnapshotName));
|
|
172
|
+
// Use existing run dir if just created by RPC, otherwise create a new one
|
|
173
|
+
const existingRunId = findLatestPendingRunId(taskDir);
|
|
174
|
+
const runId = existingRunId ?? createRunDir(taskDir, taskName, Date.now());
|
|
175
|
+
if (!existingRunId) {
|
|
176
|
+
appendHistory(config.projectRoot, { task_id: taskId, run_id: runId });
|
|
179
177
|
}
|
|
180
178
|
|
|
181
179
|
const cleanup = async () => {
|
|
@@ -184,19 +182,12 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
184
182
|
}
|
|
185
183
|
};
|
|
186
184
|
|
|
187
|
-
if (!existingResult) {
|
|
188
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
189
|
-
}
|
|
190
|
-
|
|
191
185
|
try {
|
|
192
186
|
nc = await connectNats(config);
|
|
193
187
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// Status: started
|
|
198
|
-
appendResultMessage(taskDir, resultFileName, { role: "status", time: Date.now(), content: "", type: "started" });
|
|
199
|
-
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated" });
|
|
188
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "started", taskName, runId);
|
|
189
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "started" });
|
|
190
|
+
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
200
191
|
|
|
201
192
|
// If requires_confirmation, notify clients and wait
|
|
202
193
|
if (task.frontmatter.requires_confirmation) {
|
|
@@ -205,22 +196,21 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
205
196
|
await publishConfirmResolved(nc, config, taskId, resolvedStatus);
|
|
206
197
|
if (!confirmed) {
|
|
207
198
|
console.log("Task aborted by user.");
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, resultFileName);
|
|
199
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "aborted" });
|
|
200
|
+
await publishTaskEvent(nc, config, taskDir, taskId, "aborted", taskName, runId);
|
|
211
201
|
await cleanup();
|
|
212
202
|
return;
|
|
213
203
|
}
|
|
214
204
|
console.log("Task confirmed by user.");
|
|
215
|
-
|
|
216
|
-
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated" });
|
|
205
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: "confirmation" });
|
|
206
|
+
await publishHostEvent(nc, config.hostId, taskId, { event_type: "result-updated", run_id: runId });
|
|
217
207
|
}
|
|
218
208
|
|
|
219
209
|
// Shared invocation context
|
|
220
210
|
const guiEnv = getPlatform().getGuiEnv();
|
|
221
211
|
const agent = getAgent(task.frontmatter.agent);
|
|
222
212
|
const ctx: InvocationContext = {
|
|
223
|
-
agent, task, taskDir,
|
|
213
|
+
agent, task, taskDir, runId, guiEnv, nc, config, taskId,
|
|
224
214
|
transientPermissions: [],
|
|
225
215
|
};
|
|
226
216
|
|
|
@@ -228,9 +218,8 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
228
218
|
// Command-triggered mode
|
|
229
219
|
const result = await runCommandTriggeredMode(ctx);
|
|
230
220
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
221
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
222
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
234
223
|
console.log(`Task ${taskId} completed (command-triggered).`);
|
|
235
224
|
} else {
|
|
236
225
|
// Standard execution — add user prompt as first message
|
|
@@ -242,23 +231,21 @@ export async function runCommand(taskId: string): Promise<void> {
|
|
|
242
231
|
|
|
243
232
|
const result = await invokeAgentWithRetry(ctx, task);
|
|
244
233
|
const outcome = resolveOutcome(taskDir, result.outcome);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
234
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
235
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
248
236
|
console.log(`Task ${taskId} completed.`);
|
|
249
237
|
}
|
|
250
238
|
} catch (err) {
|
|
251
239
|
console.error(`Task ${taskId} failed:`, err);
|
|
252
240
|
const outcome = resolveOutcome(taskDir, "failed");
|
|
253
241
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
254
|
-
|
|
242
|
+
appendRunMessage(taskDir, runId, {
|
|
255
243
|
role: "assistant",
|
|
256
244
|
time: Date.now(),
|
|
257
245
|
content: errorMsg,
|
|
258
246
|
});
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, resultFileName);
|
|
247
|
+
appendRunMessage(taskDir, runId, { role: "status", time: Date.now(), content: "", type: outcome });
|
|
248
|
+
await publishTaskEvent(nc, config, taskDir, taskId, outcome, taskName, runId);
|
|
262
249
|
process.exitCode = 1;
|
|
263
250
|
} finally {
|
|
264
251
|
await cleanup();
|
|
@@ -284,8 +271,8 @@ async function runCommandTriggeredMode(
|
|
|
284
271
|
console.log(`[command-triggered] Spawning: ${commandStr}`);
|
|
285
272
|
|
|
286
273
|
const child = spawnStreamingCommand(commandStr, {
|
|
287
|
-
cwd: ctx.taskDir,
|
|
288
|
-
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id },
|
|
274
|
+
cwd: getRunDir(ctx.taskDir, ctx.runId),
|
|
275
|
+
env: { ...ctx.guiEnv, PALMIER_TASK_ID: ctx.task.frontmatter.id, PALMIER_RUN_DIR: getRunDir(ctx.taskDir, ctx.runId) },
|
|
289
276
|
});
|
|
290
277
|
|
|
291
278
|
let linesProcessed = 0;
|
|
@@ -297,7 +284,7 @@ async function runCommandTriggeredMode(
|
|
|
297
284
|
let commandExited = false;
|
|
298
285
|
let resolveWhenDone: (() => void) | undefined;
|
|
299
286
|
|
|
300
|
-
const logPath = path.join(ctx.taskDir, "command-output.log");
|
|
287
|
+
const logPath = path.join(getRunDir(ctx.taskDir, ctx.runId), "command-output.log");
|
|
301
288
|
function appendLog(line: string, agentOutput: string, outcome: string) {
|
|
302
289
|
const entry = `[${new Date().toISOString()}] (${outcome}) input: ${line}\n${agentOutput}\n---\n`;
|
|
303
290
|
fs.appendFileSync(logPath, entry, "utf-8");
|
|
@@ -404,7 +391,7 @@ async function publishTaskEvent(
|
|
|
404
391
|
taskId: string,
|
|
405
392
|
eventType: TaskRunningState,
|
|
406
393
|
taskName?: string,
|
|
407
|
-
|
|
394
|
+
runId?: string,
|
|
408
395
|
): Promise<void> {
|
|
409
396
|
writeTaskStatus(taskDir, {
|
|
410
397
|
running_state: eventType,
|
|
@@ -414,7 +401,7 @@ async function publishTaskEvent(
|
|
|
414
401
|
|
|
415
402
|
const payload: Record<string, unknown> = { event_type: "running-state", running_state: eventType };
|
|
416
403
|
if (taskName) payload.name = taskName;
|
|
417
|
-
if (
|
|
404
|
+
if (runId) payload.run_id = runId;
|
|
418
405
|
await publishHostEvent(nc, config.hostId, taskId, payload);
|
|
419
406
|
}
|
|
420
407
|
|
package/src/commands/serve.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { loadConfig } from "../config.js";
|
|
|
4
4
|
import { connectNats } from "../nats-client.js";
|
|
5
5
|
import { createRpcHandler } from "../rpc-handler.js";
|
|
6
6
|
import { startNatsTransport } from "../transports/nats-transport.js";
|
|
7
|
-
import { getTaskDir, readTaskStatus, writeTaskStatus,
|
|
7
|
+
import { getTaskDir, readTaskStatus, writeTaskStatus, parseTaskFile, appendRunMessage } from "../task.js";
|
|
8
8
|
import { publishHostEvent } from "../events.js";
|
|
9
9
|
import { getPlatform } from "../platform/index.js";
|
|
10
10
|
import { detectAgents } from "../agents/agent.js";
|
|
@@ -17,46 +17,11 @@ const POLL_INTERVAL_MS = 30_000;
|
|
|
17
17
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
* and broadcast the failure event.
|
|
22
|
-
*/
|
|
23
|
-
async function markTaskFailed(
|
|
24
|
-
config: HostConfig,
|
|
25
|
-
nc: NatsConnection | undefined,
|
|
26
|
-
taskId: string,
|
|
27
|
-
reason: string,
|
|
28
|
-
): Promise<void> {
|
|
29
|
-
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
30
|
-
const status = readTaskStatus(taskDir);
|
|
31
|
-
if (!status || status.running_state !== "started") return;
|
|
32
|
-
|
|
33
|
-
console.log(`[monitor] Task ${taskId} ${reason}, marking as failed.`);
|
|
34
|
-
const endTime = Date.now();
|
|
35
|
-
writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
|
|
36
|
-
|
|
37
|
-
let taskName = taskId;
|
|
38
|
-
try {
|
|
39
|
-
const task = parseTaskFile(taskDir);
|
|
40
|
-
taskName = task.frontmatter.name || taskId;
|
|
41
|
-
} catch { /* use taskId as fallback */ }
|
|
42
|
-
|
|
43
|
-
const resultFileName = `RESULT-${endTime}.md`;
|
|
44
|
-
const content = `---\ntask_name: ${taskName}\nrunning_state: failed\nstart_time: ${status.time_stamp}\nend_time: ${endTime}\ntask_file: \n---\n\n`;
|
|
45
|
-
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
46
|
-
appendResultMessage(taskDir, resultFileName, {
|
|
47
|
-
role: "assistant",
|
|
48
|
-
time: endTime,
|
|
49
|
-
content: reason,
|
|
50
|
-
});
|
|
51
|
-
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
52
|
-
|
|
53
|
-
const payload: Record<string, unknown> = { event_type: "running-state", running_state: "failed", name: taskName };
|
|
54
|
-
await publishHostEvent(nc, config.hostId, taskId, payload);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Scan all tasks for any stuck in "start" state whose process is no longer alive.
|
|
20
|
+
* Scan all tasks for any stuck in "started" state whose process is no longer alive.
|
|
59
21
|
* Uses the system scheduler (Task Scheduler / systemd) as the authoritative source.
|
|
22
|
+
*
|
|
23
|
+
* Since run.ts creates the RESULT file and history entry at start, we just need to
|
|
24
|
+
* finalize the existing RESULT file, append a failed status entry, and broadcast.
|
|
60
25
|
*/
|
|
61
26
|
async function checkStaleTasks(
|
|
62
27
|
config: HostConfig,
|
|
@@ -80,7 +45,35 @@ async function checkStaleTasks(
|
|
|
80
45
|
// Ask the system scheduler if the task is still running
|
|
81
46
|
if (platform.isTaskRunning(taskId)) continue;
|
|
82
47
|
|
|
83
|
-
|
|
48
|
+
console.log(`[monitor] Task ${taskId} process exited unexpectedly, marking as failed.`);
|
|
49
|
+
const endTime = Date.now();
|
|
50
|
+
writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
|
|
51
|
+
|
|
52
|
+
// Find the latest run directory (created by run.ts at start)
|
|
53
|
+
const runId = fs.readdirSync(taskDir)
|
|
54
|
+
.filter((f) => /^\d+$/.test(f) && fs.existsSync(path.join(taskDir, f, "TASKRUN.md")))
|
|
55
|
+
.sort()
|
|
56
|
+
.pop();
|
|
57
|
+
|
|
58
|
+
if (runId) {
|
|
59
|
+
appendRunMessage(taskDir, runId, {
|
|
60
|
+
role: "status",
|
|
61
|
+
time: endTime,
|
|
62
|
+
content: "",
|
|
63
|
+
type: "failed",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let taskName = taskId;
|
|
68
|
+
try {
|
|
69
|
+
taskName = parseTaskFile(taskDir).frontmatter.name || taskId;
|
|
70
|
+
} catch { /* use taskId as fallback */ }
|
|
71
|
+
|
|
72
|
+
await publishHostEvent(nc, config.hostId, taskId, {
|
|
73
|
+
event_type: "running-state",
|
|
74
|
+
running_state: "failed",
|
|
75
|
+
name: taskName,
|
|
76
|
+
});
|
|
84
77
|
}
|
|
85
78
|
}
|
|
86
79
|
|
package/src/platform/linux.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { execSync, exec } from "child_process";
|
|
|
5
5
|
import { promisify } from "util";
|
|
6
6
|
import type { PlatformService } from "./platform.js";
|
|
7
7
|
import type { HostConfig, ParsedTask } from "../types.js";
|
|
8
|
+
import { loadConfig } from "../config.js";
|
|
9
|
+
import { getTaskDir, readTaskStatus } from "../task.js";
|
|
8
10
|
|
|
9
11
|
const execAsync = promisify(exec);
|
|
10
12
|
|
|
@@ -230,20 +232,28 @@ WantedBy=timers.target
|
|
|
230
232
|
}
|
|
231
233
|
|
|
232
234
|
isTaskRunning(taskId: string): boolean {
|
|
235
|
+
// Check systemd first (for scheduled/on-demand runs)
|
|
233
236
|
const serviceName = getServiceName(taskId);
|
|
234
237
|
try {
|
|
235
|
-
// is-active exits 0 only for "active". For oneshot services (Type=oneshot),
|
|
236
|
-
// the state is "activating" while running, which exits non-zero.
|
|
237
|
-
// Use show -p ActiveState to reliably get the state without exit code issues.
|
|
238
238
|
const out = execSync(
|
|
239
239
|
`systemctl --user show -p ActiveState --value ${serviceName}`,
|
|
240
240
|
{ encoding: "utf-8" },
|
|
241
241
|
);
|
|
242
242
|
const state = out.trim();
|
|
243
|
-
|
|
244
|
-
} catch {
|
|
245
|
-
|
|
246
|
-
|
|
243
|
+
if (state === "active" || state === "activating") return true;
|
|
244
|
+
} catch { /* service may not exist */ }
|
|
245
|
+
|
|
246
|
+
// Fall back to PID check (for follow-up runs spawned directly)
|
|
247
|
+
try {
|
|
248
|
+
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
249
|
+
const status = readTaskStatus(taskDir);
|
|
250
|
+
if (status?.pid) {
|
|
251
|
+
process.kill(status.pid, 0); // signal 0 = check if process exists
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
} catch { /* process not running or config unavailable */ }
|
|
255
|
+
|
|
256
|
+
return false;
|
|
247
257
|
}
|
|
248
258
|
|
|
249
259
|
getGuiEnv(): Record<string, string> {
|
package/src/platform/windows.ts
CHANGED
|
@@ -13,14 +13,6 @@ const DAEMON_TASK_NAME = "PalmierDaemon";
|
|
|
13
13
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
14
14
|
const DAEMON_VBS_FILE = path.join(CONFIG_DIR, "daemon.vbs");
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
* Build the /tr value for schtasks: a single string with quoted paths
|
|
18
|
-
* so Task Scheduler can invoke node with the palmier script + subcommand.
|
|
19
|
-
*/
|
|
20
|
-
function schtasksTr(...subcommand: string[]): string {
|
|
21
|
-
const script = process.argv[1] || "palmier";
|
|
22
|
-
return `"${process.execPath}" "${script}" ${subcommand.join(" ")}`;
|
|
23
|
-
}
|
|
24
16
|
|
|
25
17
|
const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
|
26
18
|
|
|
@@ -169,7 +161,15 @@ export class WindowsPlatform implements PlatformService {
|
|
|
169
161
|
installTaskTimer(config: HostConfig, task: ParsedTask): void {
|
|
170
162
|
const taskId = task.frontmatter.id;
|
|
171
163
|
const tn = schtasksTaskName(taskId);
|
|
172
|
-
const
|
|
164
|
+
const script = process.argv[1] || "palmier";
|
|
165
|
+
|
|
166
|
+
// Write a VBS launcher so the task runs without a visible console window
|
|
167
|
+
const vbsPath = path.join(CONFIG_DIR, `task-${taskId}.vbs`);
|
|
168
|
+
const vbs = `CreateObject("WScript.Shell").Run """${process.execPath}"" ""${script}"" run ${taskId}", 0, True`;
|
|
169
|
+
fs.writeFileSync(vbsPath, vbs, "utf-8");
|
|
170
|
+
|
|
171
|
+
const wscript = `${process.env.SYSTEMROOT || "C:\\Windows"}\\System32\\wscript.exe`;
|
|
172
|
+
const tr = `"${wscript}" "${vbsPath}"`;
|
|
173
173
|
|
|
174
174
|
// Build trigger XML elements
|
|
175
175
|
const triggerElements: string[] = [];
|
|
@@ -213,6 +213,7 @@ export class WindowsPlatform implements PlatformService {
|
|
|
213
213
|
} catch {
|
|
214
214
|
// Task might not exist — that's fine
|
|
215
215
|
}
|
|
216
|
+
try { fs.unlinkSync(path.join(CONFIG_DIR, `task-${taskId}.vbs`)); } catch { /* ignore */ }
|
|
216
217
|
}
|
|
217
218
|
|
|
218
219
|
async startTask(taskId: string): Promise<void> {
|
|
@@ -250,16 +251,38 @@ export class WindowsPlatform implements PlatformService {
|
|
|
250
251
|
}
|
|
251
252
|
|
|
252
253
|
isTaskRunning(taskId: string): boolean {
|
|
254
|
+
// Check Task Scheduler first (for scheduled/on-demand runs)
|
|
253
255
|
const tn = schtasksTaskName(taskId);
|
|
254
256
|
try {
|
|
255
257
|
const out = execFileSync("schtasks", ["/query", "/tn", tn, "/fo", "CSV", "/nh"], {
|
|
256
258
|
encoding: "utf-8",
|
|
257
259
|
windowsHide: true,
|
|
258
260
|
});
|
|
259
|
-
|
|
260
|
-
} catch {
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
if (out.includes('"Running"')) return true;
|
|
262
|
+
} catch { /* task may not exist in scheduler */ }
|
|
263
|
+
|
|
264
|
+
// Fall back to PID check (for follow-up runs spawned directly, not via schtasks)
|
|
265
|
+
try {
|
|
266
|
+
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
267
|
+
const status = readTaskStatus(taskDir);
|
|
268
|
+
if (status?.pid) {
|
|
269
|
+
// tasklist exits 0 if the PID is found
|
|
270
|
+
execFileSync("tasklist", ["/fi", `PID eq ${status.pid}`, "/nh"], {
|
|
271
|
+
encoding: "utf-8",
|
|
272
|
+
windowsHide: true,
|
|
273
|
+
stdio: "pipe",
|
|
274
|
+
});
|
|
275
|
+
// tasklist always exits 0; check if output contains the PID
|
|
276
|
+
const out = execFileSync("tasklist", ["/fi", `PID eq ${status.pid}`, "/fo", "CSV", "/nh"], {
|
|
277
|
+
encoding: "utf-8",
|
|
278
|
+
windowsHide: true,
|
|
279
|
+
stdio: "pipe",
|
|
280
|
+
});
|
|
281
|
+
if (out.includes(`"${status.pid}"`)) return true;
|
|
282
|
+
}
|
|
283
|
+
} catch { /* ignore */ }
|
|
284
|
+
|
|
285
|
+
return false;
|
|
263
286
|
}
|
|
264
287
|
|
|
265
288
|
getGuiEnv(): Record<string, string> {
|