palmier 0.2.5 → 0.2.7
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/.github/workflows/ci.yml +16 -0
- package/LICENSE +190 -0
- package/README.md +286 -219
- package/dist/agents/agent.d.ts +6 -3
- package/dist/agents/agent.js +2 -0
- package/dist/agents/claude.d.ts +1 -1
- package/dist/agents/claude.js +12 -9
- package/dist/agents/codex.d.ts +1 -1
- package/dist/agents/codex.js +12 -10
- package/dist/agents/gemini.d.ts +1 -1
- package/dist/agents/gemini.js +13 -9
- package/dist/agents/openclaw.d.ts +2 -2
- package/dist/agents/openclaw.js +8 -7
- package/dist/agents/shared-prompt.d.ts +5 -4
- package/dist/agents/shared-prompt.js +10 -8
- package/dist/commands/agents.js +11 -0
- package/dist/commands/info.js +0 -22
- package/dist/commands/init.js +59 -95
- package/dist/commands/lan.d.ts +8 -0
- package/dist/commands/lan.js +51 -0
- package/dist/commands/mcpserver.js +12 -27
- package/dist/commands/pair.d.ts +1 -1
- package/dist/commands/pair.js +52 -56
- package/dist/commands/plan-generation.md +24 -32
- package/dist/commands/restart.d.ts +5 -0
- package/dist/commands/restart.js +9 -0
- package/dist/commands/run.js +311 -124
- package/dist/commands/serve.d.ts +1 -1
- package/dist/commands/serve.js +77 -17
- package/dist/commands/task-cleanup.d.ts +14 -0
- package/dist/commands/task-cleanup.js +84 -0
- package/dist/config.js +3 -17
- package/dist/events.d.ts +9 -0
- package/dist/events.js +46 -0
- package/dist/index.js +15 -0
- package/dist/platform/linux.d.ts +2 -0
- package/dist/platform/linux.js +22 -1
- package/dist/platform/platform.d.ts +4 -0
- package/dist/platform/windows.d.ts +3 -0
- package/dist/platform/windows.js +99 -82
- package/dist/rpc-handler.d.ts +2 -1
- package/dist/rpc-handler.js +43 -52
- package/dist/spawn-command.d.ts +29 -6
- package/dist/spawn-command.js +38 -15
- package/dist/transports/http-transport.d.ts +1 -1
- package/dist/transports/http-transport.js +103 -18
- package/dist/transports/nats-transport.d.ts +4 -2
- package/dist/transports/nats-transport.js +3 -4
- package/dist/types.d.ts +5 -5
- package/package.json +5 -3
- package/src/agents/agent.ts +8 -3
- package/src/agents/claude.ts +44 -43
- package/src/agents/codex.ts +11 -12
- package/src/agents/gemini.ts +12 -10
- package/src/agents/openclaw.ts +8 -7
- package/src/agents/shared-prompt.ts +10 -8
- package/src/commands/agents.ts +11 -0
- package/src/commands/info.ts +0 -24
- package/src/commands/init.ts +62 -119
- package/src/commands/lan.ts +58 -0
- package/src/commands/mcpserver.ts +12 -31
- package/src/commands/pair.ts +50 -63
- package/src/commands/plan-generation.md +24 -32
- package/src/commands/restart.ts +9 -0
- package/src/commands/run.ts +375 -143
- package/src/commands/serve.ts +96 -17
- package/src/config.ts +3 -18
- package/src/cross-spawn.d.ts +5 -0
- package/src/events.ts +51 -0
- package/src/index.ts +17 -0
- package/src/platform/linux.ts +25 -1
- package/src/platform/platform.ts +6 -0
- package/src/platform/windows.ts +100 -89
- package/src/rpc-handler.ts +46 -55
- package/src/spawn-command.ts +120 -83
- package/src/transports/http-transport.ts +123 -19
- package/src/transports/nats-transport.ts +4 -4
- package/src/types.ts +6 -8
package/src/commands/serve.ts
CHANGED
|
@@ -1,26 +1,105 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
1
3
|
import { loadConfig } from "../config.js";
|
|
4
|
+
import { connectNats } from "../nats-client.js";
|
|
2
5
|
import { createRpcHandler } from "../rpc-handler.js";
|
|
3
6
|
import { startNatsTransport } from "../transports/nats-transport.js";
|
|
4
|
-
import {
|
|
7
|
+
import { getTaskDir, readTaskStatus, writeTaskStatus, appendHistory, parseTaskFile } from "../task.js";
|
|
8
|
+
import { publishHostEvent } from "../events.js";
|
|
9
|
+
import { getPlatform } from "../platform/index.js";
|
|
10
|
+
import type { HostConfig } from "../types.js";
|
|
11
|
+
import { CONFIG_DIR } from "../config.js";
|
|
12
|
+
import type { NatsConnection } from "nats";
|
|
13
|
+
|
|
14
|
+
const POLL_INTERVAL_MS = 30_000;
|
|
15
|
+
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
5
16
|
|
|
6
17
|
/**
|
|
7
|
-
*
|
|
18
|
+
* Mark a stuck task as failed: update status.json, write RESULT, append history,
|
|
19
|
+
* and broadcast the failure event.
|
|
20
|
+
*/
|
|
21
|
+
async function markTaskFailed(
|
|
22
|
+
config: HostConfig,
|
|
23
|
+
nc: NatsConnection | undefined,
|
|
24
|
+
taskId: string,
|
|
25
|
+
reason: string,
|
|
26
|
+
): Promise<void> {
|
|
27
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
28
|
+
const status = readTaskStatus(taskDir);
|
|
29
|
+
if (!status || status.running_state !== "started") return;
|
|
30
|
+
|
|
31
|
+
console.log(`[monitor] Task ${taskId} ${reason}, marking as failed.`);
|
|
32
|
+
const endTime = Date.now();
|
|
33
|
+
writeTaskStatus(taskDir, { running_state: "failed", time_stamp: endTime });
|
|
34
|
+
|
|
35
|
+
let taskName = taskId;
|
|
36
|
+
try {
|
|
37
|
+
const task = parseTaskFile(taskDir);
|
|
38
|
+
taskName = task.frontmatter.name || taskId;
|
|
39
|
+
} catch { /* use taskId as fallback */ }
|
|
40
|
+
|
|
41
|
+
const resultFileName = `RESULT-${endTime}.md`;
|
|
42
|
+
const content = `---\ntask_name: ${taskName}\nrunning_state: failed\nstart_time: ${status.time_stamp}\nend_time: ${endTime}\ntask_file: \n---\n${reason}`;
|
|
43
|
+
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
44
|
+
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
45
|
+
|
|
46
|
+
const payload: Record<string, unknown> = { event_type: "running-state", running_state: "failed", name: taskName };
|
|
47
|
+
await publishHostEvent(nc, config.hostId, taskId, payload);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Scan all tasks for any stuck in "start" state whose process is no longer alive.
|
|
52
|
+
* Uses the system scheduler (Task Scheduler / systemd) as the authoritative source.
|
|
53
|
+
*/
|
|
54
|
+
async function checkStaleTasks(
|
|
55
|
+
config: HostConfig,
|
|
56
|
+
nc: NatsConnection | undefined,
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
const tasksJsonl = path.join(config.projectRoot, "tasks.jsonl");
|
|
59
|
+
if (!fs.existsSync(tasksJsonl)) return;
|
|
60
|
+
|
|
61
|
+
const platform = getPlatform();
|
|
62
|
+
const lines = fs.readFileSync(tasksJsonl, "utf-8").split("\n").filter(Boolean);
|
|
63
|
+
for (const line of lines) {
|
|
64
|
+
let taskId: string;
|
|
65
|
+
try {
|
|
66
|
+
taskId = (JSON.parse(line) as { task_id: string }).task_id;
|
|
67
|
+
} catch { continue; }
|
|
68
|
+
|
|
69
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
70
|
+
const status = readTaskStatus(taskDir);
|
|
71
|
+
if (!status || status.running_state !== "started") continue;
|
|
72
|
+
|
|
73
|
+
// Ask the system scheduler if the task is still running
|
|
74
|
+
if (platform.isTaskRunning(taskId)) continue;
|
|
75
|
+
|
|
76
|
+
await markTaskFailed(config, nc, taskId, "Task process exited unexpectedly");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Start the persistent RPC handler (NATS only).
|
|
8
82
|
*/
|
|
9
83
|
export async function serveCommand(): Promise<void> {
|
|
10
84
|
const config = loadConfig();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
85
|
+
|
|
86
|
+
// Write PID so `palmier restart` can find us regardless of how we were started
|
|
87
|
+
fs.writeFileSync(DAEMON_PID_FILE, String(process.pid), "utf-8");
|
|
88
|
+
|
|
89
|
+
console.log("Starting...");
|
|
90
|
+
|
|
91
|
+
const nc = await connectNats(config);
|
|
92
|
+
|
|
93
|
+
// Reconcile any tasks stuck from before daemon started
|
|
94
|
+
await checkStaleTasks(config, nc);
|
|
95
|
+
|
|
96
|
+
// Poll for crashed tasks every 30 seconds
|
|
97
|
+
setInterval(() => {
|
|
98
|
+
checkStaleTasks(config, nc).catch((err) => {
|
|
99
|
+
console.error("[monitor] Error checking stale tasks:", err);
|
|
100
|
+
});
|
|
101
|
+
}, POLL_INTERVAL_MS);
|
|
102
|
+
|
|
103
|
+
const handleRpc = createRpcHandler(config, nc);
|
|
104
|
+
await startNatsTransport(config, handleRpc, nc);
|
|
26
105
|
}
|
package/src/config.ts
CHANGED
|
@@ -21,30 +21,15 @@ export function loadConfig(): HostConfig {
|
|
|
21
21
|
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
22
22
|
const config = JSON.parse(raw) as HostConfig;
|
|
23
23
|
|
|
24
|
-
// Default mode to "nats" for backward compatibility
|
|
25
|
-
if (!config.mode) {
|
|
26
|
-
config.mode = "nats";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
24
|
if (!config.hostId) {
|
|
30
25
|
throw new Error("Invalid host config: missing hostId");
|
|
31
26
|
}
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (!config.natsUrl || !config.natsToken) {
|
|
36
|
-
throw new Error(`Invalid host config: mode "${mode}" requires natsUrl and natsToken`);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
if (mode === "lan" || mode === "auto") {
|
|
40
|
-
if (!config.directToken) {
|
|
41
|
-
throw new Error(`Invalid host config: mode "${mode}" requires directToken`);
|
|
42
|
-
}
|
|
43
|
-
if (!config.directPort) {
|
|
44
|
-
config.directPort = 7400;
|
|
45
|
-
}
|
|
28
|
+
if (!config.natsUrl || !config.natsToken) {
|
|
29
|
+
throw new Error("Invalid host config: missing natsUrl or natsToken");
|
|
46
30
|
}
|
|
47
31
|
|
|
32
|
+
config.nats = true;
|
|
48
33
|
return config;
|
|
49
34
|
}
|
|
50
35
|
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { StringCodec, type NatsConnection } from "nats";
|
|
4
|
+
import { CONFIG_DIR } from "./config.js";
|
|
5
|
+
|
|
6
|
+
const sc = StringCodec();
|
|
7
|
+
const LAN_LOCKFILE = path.join(CONFIG_DIR, "lan.json");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read the LAN lockfile to determine if `palmier lan` is running.
|
|
11
|
+
*/
|
|
12
|
+
function getLanPort(): number | null {
|
|
13
|
+
try {
|
|
14
|
+
const raw = fs.readFileSync(LAN_LOCKFILE, "utf-8");
|
|
15
|
+
return (JSON.parse(raw) as { port: number }).port;
|
|
16
|
+
} catch { return null; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Broadcast an event to connected clients via NATS and HTTP SSE (if LAN server is running).
|
|
21
|
+
*
|
|
22
|
+
* - NATS: publishes to `host-event.{hostId}.{taskId}`
|
|
23
|
+
* - HTTP: POSTs to the LAN server's `/internal/event` endpoint (auto-detected via lockfile)
|
|
24
|
+
*/
|
|
25
|
+
export async function publishHostEvent(
|
|
26
|
+
nc: NatsConnection | undefined,
|
|
27
|
+
hostId: string,
|
|
28
|
+
taskId: string,
|
|
29
|
+
payload: Record<string, unknown>,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
const subject = `host-event.${hostId}.${taskId}`;
|
|
32
|
+
|
|
33
|
+
if (nc) {
|
|
34
|
+
nc.publish(subject, sc.encode(JSON.stringify(payload)));
|
|
35
|
+
console.log(`[nats] ${subject} →`, payload);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const lanPort = getLanPort();
|
|
39
|
+
if (lanPort) {
|
|
40
|
+
try {
|
|
41
|
+
await fetch(`http://localhost:${lanPort}/internal/event`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: { "Content-Type": "application/json" },
|
|
44
|
+
body: JSON.stringify({ task_id: taskId, ...payload }),
|
|
45
|
+
});
|
|
46
|
+
console.log(`[http] host-event: ${taskId} →`, payload);
|
|
47
|
+
} catch {
|
|
48
|
+
// LAN server may have shut down — ignore
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { serveCommand } from "./commands/serve.js";
|
|
|
12
12
|
import { mcpserverCommand } from "./commands/mcpserver.js";
|
|
13
13
|
import { agentsCommand } from "./commands/agents.js";
|
|
14
14
|
import { pairCommand } from "./commands/pair.js";
|
|
15
|
+
import { lanCommand } from "./commands/lan.js";
|
|
16
|
+
import { restartCommand } from "./commands/restart.js";
|
|
15
17
|
import { sessionsListCommand, sessionsRevokeCommand, sessionsRevokeAllCommand } from "./commands/sessions.js";
|
|
16
18
|
|
|
17
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -52,6 +54,13 @@ program
|
|
|
52
54
|
await serveCommand();
|
|
53
55
|
});
|
|
54
56
|
|
|
57
|
+
program
|
|
58
|
+
.command("restart")
|
|
59
|
+
.description("Restart the palmier serve daemon")
|
|
60
|
+
.action(async () => {
|
|
61
|
+
await restartCommand();
|
|
62
|
+
});
|
|
63
|
+
|
|
55
64
|
program
|
|
56
65
|
.command("mcpserver")
|
|
57
66
|
.description("Start an MCP server exposing Palmier tools (stdio transport)")
|
|
@@ -73,6 +82,14 @@ program
|
|
|
73
82
|
await pairCommand();
|
|
74
83
|
});
|
|
75
84
|
|
|
85
|
+
program
|
|
86
|
+
.command("lan")
|
|
87
|
+
.description("Start an on-demand LAN server for direct HTTP connections")
|
|
88
|
+
.option("-p, --port <port>", "Port to listen on", "7400")
|
|
89
|
+
.action(async (opts: { port: string }) => {
|
|
90
|
+
await lanCommand({ port: parseInt(opts.port, 10) });
|
|
91
|
+
});
|
|
92
|
+
|
|
76
93
|
const sessionsCmd = program
|
|
77
94
|
.command("sessions")
|
|
78
95
|
.description("Manage paired client sessions");
|
package/src/platform/linux.ts
CHANGED
|
@@ -112,6 +112,11 @@ WantedBy=default.target
|
|
|
112
112
|
console.log("\nHost initialization complete!");
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
async restartDaemon(): Promise<void> {
|
|
116
|
+
execSync("systemctl --user restart palmier.service", { stdio: "inherit" });
|
|
117
|
+
console.log("Palmier daemon restarted.");
|
|
118
|
+
}
|
|
119
|
+
|
|
115
120
|
installTaskTimer(config: HostConfig, task: ParsedTask): void {
|
|
116
121
|
fs.mkdirSync(UNIT_DIR, { recursive: true });
|
|
117
122
|
|
|
@@ -125,6 +130,7 @@ Description=Palmier Task: ${taskId}
|
|
|
125
130
|
|
|
126
131
|
[Service]
|
|
127
132
|
Type=oneshot
|
|
133
|
+
TimeoutStartSec=infinity
|
|
128
134
|
ExecStart=${palmierBin} run ${taskId}
|
|
129
135
|
WorkingDirectory=${config.projectRoot}
|
|
130
136
|
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
@@ -133,7 +139,8 @@ Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
|
133
139
|
fs.writeFileSync(path.join(UNIT_DIR, serviceName), serviceContent, "utf-8");
|
|
134
140
|
daemonReload();
|
|
135
141
|
|
|
136
|
-
// Only create and enable a timer if
|
|
142
|
+
// Only create and enable a timer if triggers exist and are enabled
|
|
143
|
+
if (!task.frontmatter.triggers_enabled) return;
|
|
137
144
|
const triggers = task.frontmatter.triggers || [];
|
|
138
145
|
const onCalendarLines: string[] = [];
|
|
139
146
|
for (const trigger of triggers) {
|
|
@@ -193,6 +200,23 @@ WantedBy=timers.target
|
|
|
193
200
|
await execAsync(`systemctl --user stop ${serviceName}`);
|
|
194
201
|
}
|
|
195
202
|
|
|
203
|
+
isTaskRunning(taskId: string): boolean {
|
|
204
|
+
const serviceName = getServiceName(taskId);
|
|
205
|
+
try {
|
|
206
|
+
// is-active exits 0 only for "active". For oneshot services (Type=oneshot),
|
|
207
|
+
// the state is "activating" while running, which exits non-zero.
|
|
208
|
+
// Use show -p ActiveState to reliably get the state without exit code issues.
|
|
209
|
+
const out = execSync(
|
|
210
|
+
`systemctl --user show -p ActiveState --value ${serviceName}`,
|
|
211
|
+
{ encoding: "utf-8" },
|
|
212
|
+
);
|
|
213
|
+
const state = out.trim();
|
|
214
|
+
return state === "active" || state === "activating";
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
196
220
|
getGuiEnv(): Record<string, string> {
|
|
197
221
|
const uid = process.getuid?.();
|
|
198
222
|
const runtimeDir =
|
package/src/platform/platform.ts
CHANGED
|
@@ -8,6 +8,9 @@ export interface PlatformService {
|
|
|
8
8
|
/** Install the main `palmier serve` daemon to start at boot. */
|
|
9
9
|
installDaemon(config: HostConfig): void;
|
|
10
10
|
|
|
11
|
+
/** Restart the `palmier serve` daemon. */
|
|
12
|
+
restartDaemon(): Promise<void>;
|
|
13
|
+
|
|
11
14
|
/** Install a scheduled trigger (timer) for a task. */
|
|
12
15
|
installTaskTimer(config: HostConfig, task: ParsedTask): void;
|
|
13
16
|
|
|
@@ -20,6 +23,9 @@ export interface PlatformService {
|
|
|
20
23
|
/** Abort/stop a running task. */
|
|
21
24
|
stopTask(taskId: string): Promise<void>;
|
|
22
25
|
|
|
26
|
+
/** Check if a task is currently running via the system scheduler. */
|
|
27
|
+
isTaskRunning(taskId: string): boolean;
|
|
28
|
+
|
|
23
29
|
/** Return env vars needed for GUI access (Linux: DISPLAY, etc.). */
|
|
24
30
|
getGuiEnv(): Record<string, string>;
|
|
25
31
|
}
|
package/src/platform/windows.ts
CHANGED
|
@@ -1,27 +1,23 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
|
+
import { spawn as nodeSpawn } from "child_process";
|
|
5
5
|
import type { PlatformService } from "./platform.js";
|
|
6
6
|
import type { HostConfig, ParsedTask } from "../types.js";
|
|
7
|
-
import {
|
|
8
|
-
import { loadConfig } from "../config.js";
|
|
7
|
+
import { CONFIG_DIR } from "../config.js";
|
|
9
8
|
|
|
10
|
-
const execAsync = promisify(exec);
|
|
11
9
|
|
|
12
|
-
const TASK_PREFIX = "PalmierTask-";
|
|
10
|
+
const TASK_PREFIX = "\\Palmier\\PalmierTask-";
|
|
13
11
|
const DAEMON_TASK_NAME = "PalmierDaemon";
|
|
14
|
-
const
|
|
12
|
+
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
15
13
|
|
|
16
14
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* Build the /tr value for schtasks: a single string with quoted paths
|
|
16
|
+
* so Task Scheduler can invoke node with the palmier script + subcommand.
|
|
19
17
|
*/
|
|
20
|
-
function
|
|
21
|
-
// process.argv[1] is the script path; wrap with node so it works as
|
|
22
|
-
// a Task Scheduler command without relying on file associations.
|
|
18
|
+
function schtasksTr(...subcommand: string[]): string {
|
|
23
19
|
const script = process.argv[1] || "palmier";
|
|
24
|
-
return `"${process.execPath}" "${script}"`;
|
|
20
|
+
return `"${process.execPath}" "${script}" ${subcommand.join(" ")}`;
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
/**
|
|
@@ -79,65 +75,86 @@ function schtasksTaskName(taskId: string): string {
|
|
|
79
75
|
|
|
80
76
|
export class WindowsPlatform implements PlatformService {
|
|
81
77
|
installDaemon(config: HostConfig): void {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
// Try ONSTART first (requires elevation), fall back to ONLOGON
|
|
85
|
-
const baseArgs = [
|
|
86
|
-
"/create", "/tn", DAEMON_TASK_NAME,
|
|
87
|
-
"/tr", `${cmd} serve`,
|
|
88
|
-
"/rl", "HIGHEST",
|
|
89
|
-
"/f", // force overwrite if exists
|
|
90
|
-
];
|
|
78
|
+
const script = process.argv[1] || "palmier";
|
|
79
|
+
const regValue = `"${process.execPath}" "${script}" serve`;
|
|
91
80
|
|
|
92
81
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
);
|
|
97
|
-
console.log(`
|
|
98
|
-
} catch {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
execSync(
|
|
102
|
-
`schtasks ${[...baseArgs, "/sc", "ONLOGON"].join(" ")}`,
|
|
103
|
-
{ encoding: "utf-8", shell: SHELL },
|
|
104
|
-
);
|
|
105
|
-
console.log(`Task Scheduler: "${DAEMON_TASK_NAME}" installed (runs at logon).`);
|
|
106
|
-
console.log(" Tip: run as Administrator to use ONSTART instead.");
|
|
107
|
-
} catch (err) {
|
|
108
|
-
console.error(`Warning: failed to create scheduled task: ${err}`);
|
|
109
|
-
console.error("You may need to start palmier serve manually.");
|
|
110
|
-
}
|
|
82
|
+
execFileSync("reg", [
|
|
83
|
+
"add", "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run",
|
|
84
|
+
"/v", DAEMON_TASK_NAME, "/t", "REG_SZ", "/d", regValue, "/f",
|
|
85
|
+
], { encoding: "utf-8", stdio: "pipe" });
|
|
86
|
+
console.log(`Registry Run key "${DAEMON_TASK_NAME}" installed (runs at logon).`);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.error(`Warning: failed to install registry run entry: ${err}`);
|
|
89
|
+
console.error("You may need to start palmier serve manually.");
|
|
111
90
|
}
|
|
112
91
|
|
|
113
92
|
// Start the daemon now
|
|
114
|
-
|
|
115
|
-
execSync(`schtasks /run /tn ${DAEMON_TASK_NAME}`, { encoding: "utf-8", shell: SHELL });
|
|
116
|
-
console.log("Palmier daemon started.");
|
|
117
|
-
} catch {
|
|
118
|
-
console.log("Note: could not start daemon immediately. It will start at next login/boot.");
|
|
119
|
-
}
|
|
93
|
+
this.spawnDaemon(script);
|
|
120
94
|
|
|
121
95
|
console.log("\nHost initialization complete!");
|
|
122
96
|
}
|
|
123
97
|
|
|
98
|
+
async restartDaemon(): Promise<void> {
|
|
99
|
+
// Kill the old daemon if we have its PID
|
|
100
|
+
if (fs.existsSync(DAEMON_PID_FILE)) {
|
|
101
|
+
const oldPid = fs.readFileSync(DAEMON_PID_FILE, "utf-8").trim();
|
|
102
|
+
try {
|
|
103
|
+
execFileSync("taskkill", ["/pid", oldPid, "/t", "/f"], { windowsHide: true });
|
|
104
|
+
} catch {
|
|
105
|
+
// Process may have already exited
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const script = process.argv[1] || "palmier";
|
|
110
|
+
this.spawnDaemon(script);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private spawnDaemon(script: string): void {
|
|
114
|
+
const child = nodeSpawn(process.execPath, [script, "serve"], {
|
|
115
|
+
detached: true,
|
|
116
|
+
stdio: "ignore",
|
|
117
|
+
windowsHide: true,
|
|
118
|
+
});
|
|
119
|
+
if (child.pid) {
|
|
120
|
+
fs.writeFileSync(DAEMON_PID_FILE, String(child.pid), "utf-8");
|
|
121
|
+
}
|
|
122
|
+
child.unref();
|
|
123
|
+
console.log("Palmier daemon started.");
|
|
124
|
+
}
|
|
125
|
+
|
|
124
126
|
installTaskTimer(config: HostConfig, task: ParsedTask): void {
|
|
125
127
|
const taskId = task.frontmatter.id;
|
|
126
128
|
const tn = schtasksTaskName(taskId);
|
|
127
|
-
const
|
|
129
|
+
const tr = schtasksTr("run", taskId);
|
|
130
|
+
|
|
131
|
+
// Always create the scheduled task with a dummy trigger first.
|
|
132
|
+
// This ensures startTask (/run) works even when no triggers are configured.
|
|
133
|
+
try {
|
|
134
|
+
execFileSync("schtasks", [
|
|
135
|
+
"/create", "/tn", tn,
|
|
136
|
+
"/tr", tr,
|
|
137
|
+
"/sc", "ONCE", "/sd", "01/01/2000", "/st", "00:00",
|
|
138
|
+
"/f",
|
|
139
|
+
], { encoding: "utf-8", windowsHide: true });
|
|
140
|
+
} catch (err: unknown) {
|
|
141
|
+
const e = err as { stderr?: string };
|
|
142
|
+
console.error(`Failed to create scheduled task ${tn}: ${e.stderr || err}`);
|
|
143
|
+
}
|
|
128
144
|
|
|
145
|
+
// Overlay with real schedule triggers if enabled
|
|
146
|
+
if (!task.frontmatter.triggers_enabled) return;
|
|
129
147
|
const triggers = task.frontmatter.triggers || [];
|
|
130
148
|
for (const trigger of triggers) {
|
|
131
149
|
if (trigger.type === "cron") {
|
|
132
150
|
const schedArgs = cronToSchtasksArgs(trigger.value);
|
|
133
|
-
const args = [
|
|
134
|
-
"/create", "/tn", tn,
|
|
135
|
-
"/tr", `${cmd} run ${taskId}`,
|
|
136
|
-
...schedArgs,
|
|
137
|
-
"/f",
|
|
138
|
-
];
|
|
139
151
|
try {
|
|
140
|
-
|
|
152
|
+
execFileSync("schtasks", [
|
|
153
|
+
"/create", "/tn", tn,
|
|
154
|
+
"/tr", tr,
|
|
155
|
+
...schedArgs,
|
|
156
|
+
"/f",
|
|
157
|
+
], { encoding: "utf-8", windowsHide: true });
|
|
141
158
|
} catch (err: unknown) {
|
|
142
159
|
const e = err as { stderr?: string };
|
|
143
160
|
console.error(`Failed to create scheduled task ${tn}: ${e.stderr || err}`);
|
|
@@ -153,14 +170,13 @@ export class WindowsPlatform implements PlatformService {
|
|
|
153
170
|
const [year, month, day] = datePart.split("-");
|
|
154
171
|
const sd = `${month}/${day}/${year}`;
|
|
155
172
|
const st = timePart.slice(0, 5);
|
|
156
|
-
const args = [
|
|
157
|
-
"/create", "/tn", tn,
|
|
158
|
-
"/tr", `${cmd} run ${taskId}`,
|
|
159
|
-
"/sc", "ONCE", "/sd", sd, "/st", st,
|
|
160
|
-
"/f",
|
|
161
|
-
];
|
|
162
173
|
try {
|
|
163
|
-
|
|
174
|
+
execFileSync("schtasks", [
|
|
175
|
+
"/create", "/tn", tn,
|
|
176
|
+
"/tr", tr,
|
|
177
|
+
"/sc", "ONCE", "/sd", sd, "/st", st,
|
|
178
|
+
"/f",
|
|
179
|
+
], { encoding: "utf-8", windowsHide: true });
|
|
164
180
|
} catch (err: unknown) {
|
|
165
181
|
const e = err as { stderr?: string };
|
|
166
182
|
console.error(`Failed to create once task ${tn}: ${e.stderr || err}`);
|
|
@@ -172,47 +188,42 @@ export class WindowsPlatform implements PlatformService {
|
|
|
172
188
|
removeTaskTimer(taskId: string): void {
|
|
173
189
|
const tn = schtasksTaskName(taskId);
|
|
174
190
|
try {
|
|
175
|
-
|
|
191
|
+
execFileSync("schtasks", ["/delete", "/tn", tn, "/f"], { encoding: "utf-8", windowsHide: true });
|
|
176
192
|
} catch {
|
|
177
193
|
// Task might not exist — that's fine
|
|
178
194
|
}
|
|
179
195
|
}
|
|
180
196
|
|
|
181
197
|
async startTask(taskId: string): Promise<void> {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
detached: true,
|
|
189
|
-
stdio: "ignore",
|
|
190
|
-
cwd: config.projectRoot,
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
if (child.pid) {
|
|
194
|
-
fs.mkdirSync(taskDir, { recursive: true });
|
|
195
|
-
fs.writeFileSync(path.join(taskDir, "pid"), String(child.pid), "utf-8");
|
|
198
|
+
const tn = schtasksTaskName(taskId);
|
|
199
|
+
try {
|
|
200
|
+
execFileSync("schtasks", ["/run", "/tn", tn], { encoding: "utf-8", windowsHide: true });
|
|
201
|
+
} catch (err: unknown) {
|
|
202
|
+
const e = err as { stderr?: string; message?: string };
|
|
203
|
+
throw new Error(`Failed to start task via schtasks: ${e.stderr || e.message}`);
|
|
196
204
|
}
|
|
197
|
-
child.unref();
|
|
198
205
|
}
|
|
199
206
|
|
|
200
207
|
async stopTask(taskId: string): Promise<void> {
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
throw new Error(`
|
|
208
|
+
const tn = schtasksTaskName(taskId);
|
|
209
|
+
try {
|
|
210
|
+
execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true });
|
|
211
|
+
} catch (err: unknown) {
|
|
212
|
+
const e = err as { stderr?: string; message?: string };
|
|
213
|
+
throw new Error(`Failed to stop task via schtasks: ${e.stderr || e.message}`);
|
|
207
214
|
}
|
|
215
|
+
}
|
|
208
216
|
|
|
209
|
-
|
|
217
|
+
isTaskRunning(taskId: string): boolean {
|
|
218
|
+
const tn = schtasksTaskName(taskId);
|
|
210
219
|
try {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
220
|
+
const out = execFileSync("schtasks", ["/query", "/tn", tn, "/fo", "CSV", "/nh"], {
|
|
221
|
+
encoding: "utf-8",
|
|
222
|
+
windowsHide: true,
|
|
223
|
+
});
|
|
224
|
+
return out.includes('"Running"');
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
216
227
|
}
|
|
217
228
|
}
|
|
218
229
|
|