palmier 0.2.4 → 0.2.6
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 +288 -223
- 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 +15 -9
- package/dist/agents/codex.d.ts +1 -1
- package/dist/agents/codex.js +14 -10
- package/dist/agents/gemini.d.ts +1 -1
- package/dist/agents/gemini.js +15 -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 +17 -10
- package/dist/commands/init.d.ts +2 -10
- package/dist/commands/init.js +171 -163
- package/dist/commands/mcpserver.js +11 -21
- 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 +295 -114
- package/dist/commands/serve.js +83 -4
- package/dist/commands/sessions.js +1 -4
- package/dist/commands/task-cleanup.d.ts +14 -0
- package/dist/commands/task-cleanup.js +84 -0
- package/dist/config.js +1 -1
- package/dist/events.d.ts +10 -0
- package/dist/events.js +29 -0
- package/dist/index.js +14 -7
- package/dist/platform/index.d.ts +4 -0
- package/dist/platform/index.js +12 -0
- package/dist/platform/linux.d.ts +13 -0
- package/dist/platform/linux.js +207 -0
- package/dist/platform/platform.d.ts +24 -0
- package/dist/platform/platform.js +2 -0
- package/dist/platform/windows.d.ts +14 -0
- package/dist/platform/windows.js +218 -0
- package/dist/rpc-handler.d.ts +2 -1
- package/dist/rpc-handler.js +53 -55
- package/dist/spawn-command.d.ts +32 -6
- package/dist/spawn-command.js +38 -13
- package/dist/transports/http-transport.js +2 -7
- package/dist/transports/nats-transport.d.ts +4 -2
- package/dist/transports/nats-transport.js +3 -4
- package/dist/types.d.ts +4 -2
- package/package.json +5 -3
- package/src/agents/agent.ts +8 -3
- package/src/agents/claude.ts +44 -39
- package/src/agents/codex.ts +14 -12
- package/src/agents/gemini.ts +15 -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 +18 -10
- package/src/commands/init.ts +201 -186
- package/src/commands/mcpserver.ts +11 -22
- package/src/commands/plan-generation.md +24 -32
- package/src/commands/restart.ts +9 -0
- package/src/commands/run.ts +367 -133
- package/src/commands/serve.ts +101 -5
- package/src/commands/sessions.ts +1 -4
- package/src/config.ts +1 -1
- package/src/cross-spawn.d.ts +5 -0
- package/src/events.ts +38 -0
- package/src/index.ts +16 -7
- package/src/platform/index.ts +16 -0
- package/src/platform/linux.ts +231 -0
- package/src/platform/platform.ts +31 -0
- package/src/platform/windows.ts +234 -0
- package/src/rpc-handler.ts +56 -63
- package/src/spawn-command.ts +120 -78
- package/src/transports/http-transport.ts +2 -5
- package/src/transports/nats-transport.ts +4 -4
- package/src/types.ts +4 -2
- package/src/systemd.ts +0 -164
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { loadConfig } from "../config.js";
|
|
4
|
+
import { connectNats } from "../nats-client.js";
|
|
5
|
+
import { getTaskDir, readTaskStatus, writeTaskStatus, appendHistory, parseTaskFile } from "../task.js";
|
|
6
|
+
import { publishHostEvent } from "../events.js";
|
|
7
|
+
/**
|
|
8
|
+
* Write a minimal RESULT file for a task that exited without writing one itself.
|
|
9
|
+
* Uses the status.json time_stamp as the start time.
|
|
10
|
+
*/
|
|
11
|
+
function writeCleanupResult(taskDir, taskName, runningState, startTime) {
|
|
12
|
+
const endTime = Date.now();
|
|
13
|
+
const resultFileName = `RESULT-${endTime}.md`;
|
|
14
|
+
// Find the task snapshot file that matches the start time
|
|
15
|
+
const taskSnapshotName = `TASK-${startTime}.md`;
|
|
16
|
+
const taskFile = fs.existsSync(path.join(taskDir, taskSnapshotName)) ? taskSnapshotName : "";
|
|
17
|
+
const content = `---\ntask_name: ${taskName}\nrunning_state: ${runningState}\nstart_time: ${startTime}\nend_time: ${endTime}\ntask_file: ${taskFile}\n---\nTask process exited unexpectedly.`;
|
|
18
|
+
fs.writeFileSync(path.join(taskDir, resultFileName), content, "utf-8");
|
|
19
|
+
return resultFileName;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Post-exit cleanup for a task process.
|
|
23
|
+
*
|
|
24
|
+
* Called by the platform hook (ExecStopPost on Linux, wrapper script on Windows)
|
|
25
|
+
* after the main `palmier run <taskId>` process exits.
|
|
26
|
+
*
|
|
27
|
+
* - If status.json shows "finish" or "fail", the process handled its own cleanup — no-op.
|
|
28
|
+
* - If status.json shows "abort", the RPC handler already wrote status and broadcast —
|
|
29
|
+
* just write the RESULT file and append history.
|
|
30
|
+
* - If status.json shows "start", the process died unexpectedly — write "fail" status,
|
|
31
|
+
* RESULT file, append history, and broadcast event.
|
|
32
|
+
*/
|
|
33
|
+
export async function taskCleanupCommand(taskId) {
|
|
34
|
+
const config = loadConfig();
|
|
35
|
+
const taskDir = getTaskDir(config.projectRoot, taskId);
|
|
36
|
+
const status = readTaskStatus(taskDir);
|
|
37
|
+
if (!status) {
|
|
38
|
+
console.log(`[task-cleanup] No status.json for task ${taskId}, nothing to do.`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Process already handled its own cleanup
|
|
42
|
+
if (status.running_state === "finish" || status.running_state === "fail") {
|
|
43
|
+
console.log(`[task-cleanup] Task ${taskId} already in terminal state: ${status.running_state}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Read task name for RESULT file
|
|
47
|
+
let taskName = taskId;
|
|
48
|
+
try {
|
|
49
|
+
const task = parseTaskFile(taskDir);
|
|
50
|
+
taskName = task.frontmatter.name || taskId;
|
|
51
|
+
}
|
|
52
|
+
catch { /* use taskId as fallback name */ }
|
|
53
|
+
const startTime = status.time_stamp;
|
|
54
|
+
if (status.running_state === "abort") {
|
|
55
|
+
// RPC handler already wrote status and broadcast — just write RESULT + history
|
|
56
|
+
console.log(`[task-cleanup] Task ${taskId} was aborted via RPC, writing RESULT.`);
|
|
57
|
+
const resultFileName = writeCleanupResult(taskDir, taskName, "abort", startTime);
|
|
58
|
+
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// status.running_state === "start" — unexpected death
|
|
62
|
+
console.log(`[task-cleanup] Task ${taskId} died unexpectedly, marking as failed.`);
|
|
63
|
+
writeTaskStatus(taskDir, { running_state: "fail", time_stamp: Date.now() });
|
|
64
|
+
const resultFileName = writeCleanupResult(taskDir, taskName, "fail", startTime);
|
|
65
|
+
appendHistory(config.projectRoot, { task_id: taskId, result_file: resultFileName });
|
|
66
|
+
// Broadcast failure event via NATS and/or HTTP, consistent with other status pushes
|
|
67
|
+
const mode = config.mode ?? "nats";
|
|
68
|
+
const useNats = mode === "nats" || mode === "auto";
|
|
69
|
+
const useHttp = mode === "lan" || mode === "auto";
|
|
70
|
+
let nc;
|
|
71
|
+
try {
|
|
72
|
+
if (useNats) {
|
|
73
|
+
nc = await connectNats(config);
|
|
74
|
+
}
|
|
75
|
+
const payload = { event_type: "running-state", running_state: "fail", name: taskName };
|
|
76
|
+
await publishHostEvent(nc, config, taskId, payload, useHttp);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
if (nc && !nc.isClosed()) {
|
|
80
|
+
await nc.drain();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=task-cleanup.js.map
|
package/dist/config.js
CHANGED
|
@@ -9,7 +9,7 @@ const CONFIG_FILE = path.join(CONFIG_DIR, "host.json");
|
|
|
9
9
|
*/
|
|
10
10
|
export function loadConfig() {
|
|
11
11
|
if (!fs.existsSync(CONFIG_FILE)) {
|
|
12
|
-
throw new Error("Host not provisioned. Run `palmier init
|
|
12
|
+
throw new Error("Host not provisioned. Run `palmier init` first.\n" +
|
|
13
13
|
`Expected config at: ${CONFIG_FILE}`);
|
|
14
14
|
}
|
|
15
15
|
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type NatsConnection } from "nats";
|
|
2
|
+
import type { HostConfig } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Broadcast an event to connected clients via NATS and/or HTTP SSE.
|
|
5
|
+
*
|
|
6
|
+
* - NATS: publishes to `host-event.{hostId}.{taskId}`
|
|
7
|
+
* - HTTP: POSTs to the daemon's `/internal/event` endpoint which fans out via SSE
|
|
8
|
+
*/
|
|
9
|
+
export declare function publishHostEvent(nc: NatsConnection | undefined, config: HostConfig, taskId: string, payload: Record<string, unknown>, useHttp: boolean): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=events.d.ts.map
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { StringCodec } from "nats";
|
|
2
|
+
const sc = StringCodec();
|
|
3
|
+
/**
|
|
4
|
+
* Broadcast an event to connected clients via NATS and/or HTTP SSE.
|
|
5
|
+
*
|
|
6
|
+
* - NATS: publishes to `host-event.{hostId}.{taskId}`
|
|
7
|
+
* - HTTP: POSTs to the daemon's `/internal/event` endpoint which fans out via SSE
|
|
8
|
+
*/
|
|
9
|
+
export async function publishHostEvent(nc, config, taskId, payload, useHttp) {
|
|
10
|
+
const subject = `host-event.${config.hostId}.${taskId}`;
|
|
11
|
+
if (nc) {
|
|
12
|
+
nc.publish(subject, sc.encode(JSON.stringify(payload)));
|
|
13
|
+
console.log(`[nats] ${subject} →`, payload);
|
|
14
|
+
}
|
|
15
|
+
if (useHttp && config.directPort) {
|
|
16
|
+
try {
|
|
17
|
+
await fetch(`http://localhost:${config.directPort}/internal/event`, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: { "Content-Type": "application/json" },
|
|
20
|
+
body: JSON.stringify({ task_id: taskId, ...payload }),
|
|
21
|
+
});
|
|
22
|
+
console.log(`[http] host-event: ${taskId} →`, payload);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
console.error(`[http] Failed to push event:`, err);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=events.js.map
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { serveCommand } from "./commands/serve.js";
|
|
|
11
11
|
import { mcpserverCommand } from "./commands/mcpserver.js";
|
|
12
12
|
import { agentsCommand } from "./commands/agents.js";
|
|
13
13
|
import { pairCommand } from "./commands/pair.js";
|
|
14
|
+
import { restartCommand } from "./commands/restart.js";
|
|
14
15
|
import { sessionsListCommand, sessionsRevokeCommand, sessionsRevokeAllCommand } from "./commands/sessions.js";
|
|
15
16
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
@@ -22,12 +23,8 @@ program
|
|
|
22
23
|
program
|
|
23
24
|
.command("init")
|
|
24
25
|
.description("Provision this host")
|
|
25
|
-
.
|
|
26
|
-
|
|
27
|
-
.option("--host <ip>", "Override detected LAN IP address")
|
|
28
|
-
.option("--port <port>", "Direct HTTP port (default: 7400)", parseInt)
|
|
29
|
-
.action(async (options) => {
|
|
30
|
-
await initCommand(options);
|
|
26
|
+
.action(async () => {
|
|
27
|
+
await initCommand();
|
|
31
28
|
});
|
|
32
29
|
program
|
|
33
30
|
.command("info")
|
|
@@ -42,11 +39,17 @@ program
|
|
|
42
39
|
await runCommand(taskId);
|
|
43
40
|
});
|
|
44
41
|
program
|
|
45
|
-
.command("serve"
|
|
42
|
+
.command("serve")
|
|
46
43
|
.description("Start the persistent RPC handler")
|
|
47
44
|
.action(async () => {
|
|
48
45
|
await serveCommand();
|
|
49
46
|
});
|
|
47
|
+
program
|
|
48
|
+
.command("restart")
|
|
49
|
+
.description("Restart the palmier serve daemon")
|
|
50
|
+
.action(async () => {
|
|
51
|
+
await restartCommand();
|
|
52
|
+
});
|
|
50
53
|
program
|
|
51
54
|
.command("mcpserver")
|
|
52
55
|
.description("Start an MCP server exposing Palmier tools (stdio transport)")
|
|
@@ -86,6 +89,10 @@ sessionsCmd
|
|
|
86
89
|
.action(async () => {
|
|
87
90
|
await sessionsRevokeAllCommand();
|
|
88
91
|
});
|
|
92
|
+
// No subcommand → default to serve
|
|
93
|
+
if (process.argv.length <= 2) {
|
|
94
|
+
process.argv.push("serve");
|
|
95
|
+
}
|
|
89
96
|
program.parseAsync(process.argv).catch((err) => {
|
|
90
97
|
console.error(err);
|
|
91
98
|
process.exit(1);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LinuxPlatform } from "./linux.js";
|
|
2
|
+
import { WindowsPlatform } from "./windows.js";
|
|
3
|
+
let _instance;
|
|
4
|
+
export function getPlatform() {
|
|
5
|
+
if (!_instance) {
|
|
6
|
+
_instance = process.platform === "win32"
|
|
7
|
+
? new WindowsPlatform()
|
|
8
|
+
: new LinuxPlatform();
|
|
9
|
+
}
|
|
10
|
+
return _instance;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PlatformService } from "./platform.js";
|
|
2
|
+
import type { HostConfig, ParsedTask } from "../types.js";
|
|
3
|
+
export declare class LinuxPlatform implements PlatformService {
|
|
4
|
+
installDaemon(config: HostConfig): void;
|
|
5
|
+
restartDaemon(): Promise<void>;
|
|
6
|
+
installTaskTimer(config: HostConfig, task: ParsedTask): void;
|
|
7
|
+
removeTaskTimer(taskId: string): void;
|
|
8
|
+
startTask(taskId: string): Promise<void>;
|
|
9
|
+
stopTask(taskId: string): Promise<void>;
|
|
10
|
+
isTaskRunning(taskId: string): boolean;
|
|
11
|
+
getGuiEnv(): Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=linux.d.ts.map
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { execSync, exec } from "child_process";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
const UNIT_DIR = path.join(homedir(), ".config", "systemd", "user");
|
|
8
|
+
function getTimerName(taskId) {
|
|
9
|
+
return `palmier-task-${taskId}.timer`;
|
|
10
|
+
}
|
|
11
|
+
function getServiceName(taskId) {
|
|
12
|
+
return `palmier-task-${taskId}.service`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Convert a cron expression to a systemd OnCalendar string.
|
|
16
|
+
*
|
|
17
|
+
* Only the 4 cron patterns the PWA UI can produce are supported:
|
|
18
|
+
* hourly: "0 * * * *"
|
|
19
|
+
* daily: "MM HH * * *"
|
|
20
|
+
* weekly: "MM HH * * D"
|
|
21
|
+
* monthly: "MM HH D * *"
|
|
22
|
+
* Arbitrary cron expressions (ranges, lists, steps beyond hourly) are NOT
|
|
23
|
+
* handled because the UI never generates them.
|
|
24
|
+
*/
|
|
25
|
+
function cronToOnCalendar(cron) {
|
|
26
|
+
const parts = cron.trim().split(/\s+/);
|
|
27
|
+
if (parts.length !== 5) {
|
|
28
|
+
throw new Error(`Invalid cron expression (expected 5 fields): ${cron}`);
|
|
29
|
+
}
|
|
30
|
+
const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
|
|
31
|
+
// Map cron day-of-week numbers to systemd abbreviated names
|
|
32
|
+
const dowMap = {
|
|
33
|
+
"0": "Sun", "1": "Mon", "2": "Tue", "3": "Wed",
|
|
34
|
+
"4": "Thu", "5": "Fri", "6": "Sat", "7": "Sun",
|
|
35
|
+
};
|
|
36
|
+
const monthPart = "*";
|
|
37
|
+
const dayPart = dayOfMonth === "*" ? "*" : dayOfMonth.padStart(2, "0");
|
|
38
|
+
const hourPart = hour === "*" ? "*" : hour.padStart(2, "0");
|
|
39
|
+
const minutePart = minute === "*" ? "*" : minute.padStart(2, "0");
|
|
40
|
+
if (dayOfWeek !== "*") {
|
|
41
|
+
const dow = dowMap[dayOfWeek] ?? dayOfWeek;
|
|
42
|
+
return `${dow} *-${monthPart}-${dayPart} ${hourPart}:${minutePart}:00`;
|
|
43
|
+
}
|
|
44
|
+
return `*-${monthPart}-${dayPart} ${hourPart}:${minutePart}:00`;
|
|
45
|
+
}
|
|
46
|
+
function daemonReload() {
|
|
47
|
+
try {
|
|
48
|
+
execSync("systemctl --user daemon-reload", { encoding: "utf-8" });
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const e = err;
|
|
52
|
+
console.error(`daemon-reload failed: ${e.stderr || err}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export class LinuxPlatform {
|
|
56
|
+
installDaemon(config) {
|
|
57
|
+
fs.mkdirSync(UNIT_DIR, { recursive: true });
|
|
58
|
+
const palmierBin = process.argv[1] || "palmier";
|
|
59
|
+
const serviceContent = `[Unit]
|
|
60
|
+
Description=Palmier Host
|
|
61
|
+
After=network-online.target
|
|
62
|
+
Wants=network-online.target
|
|
63
|
+
|
|
64
|
+
[Service]
|
|
65
|
+
Type=simple
|
|
66
|
+
ExecStart=${palmierBin} serve
|
|
67
|
+
WorkingDirectory=${config.projectRoot}
|
|
68
|
+
Restart=on-failure
|
|
69
|
+
RestartSec=5
|
|
70
|
+
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
71
|
+
|
|
72
|
+
[Install]
|
|
73
|
+
WantedBy=default.target
|
|
74
|
+
`;
|
|
75
|
+
const servicePath = path.join(UNIT_DIR, "palmier.service");
|
|
76
|
+
fs.writeFileSync(servicePath, serviceContent, "utf-8");
|
|
77
|
+
console.log("Systemd service installed at:", servicePath);
|
|
78
|
+
try {
|
|
79
|
+
execSync("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
80
|
+
execSync("systemctl --user enable palmier.service", { stdio: "inherit" });
|
|
81
|
+
execSync("systemctl --user restart palmier.service", { stdio: "inherit" });
|
|
82
|
+
console.log("Palmier host service enabled and started.");
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
console.error(`Warning: failed to enable systemd service: ${err}`);
|
|
86
|
+
console.error("You may need to start it manually: systemctl --user enable --now palmier.service");
|
|
87
|
+
}
|
|
88
|
+
// Enable lingering so service runs without active login session
|
|
89
|
+
try {
|
|
90
|
+
execSync(`loginctl enable-linger ${process.env.USER || ""}`, { stdio: "inherit" });
|
|
91
|
+
console.log("Login lingering enabled.");
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
console.error(`Warning: failed to enable linger: ${err}`);
|
|
95
|
+
}
|
|
96
|
+
console.log("\nHost initialization complete!");
|
|
97
|
+
}
|
|
98
|
+
async restartDaemon() {
|
|
99
|
+
execSync("systemctl --user restart palmier.service", { stdio: "inherit" });
|
|
100
|
+
console.log("Palmier daemon restarted.");
|
|
101
|
+
}
|
|
102
|
+
installTaskTimer(config, task) {
|
|
103
|
+
fs.mkdirSync(UNIT_DIR, { recursive: true });
|
|
104
|
+
const taskId = task.frontmatter.id;
|
|
105
|
+
const serviceName = getServiceName(taskId);
|
|
106
|
+
const timerName = getTimerName(taskId);
|
|
107
|
+
const palmierBin = process.argv[1] || "palmier";
|
|
108
|
+
const serviceContent = `[Unit]
|
|
109
|
+
Description=Palmier Task: ${taskId}
|
|
110
|
+
|
|
111
|
+
[Service]
|
|
112
|
+
Type=oneshot
|
|
113
|
+
TimeoutStartSec=infinity
|
|
114
|
+
ExecStart=${palmierBin} run ${taskId}
|
|
115
|
+
WorkingDirectory=${config.projectRoot}
|
|
116
|
+
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
117
|
+
`;
|
|
118
|
+
fs.writeFileSync(path.join(UNIT_DIR, serviceName), serviceContent, "utf-8");
|
|
119
|
+
daemonReload();
|
|
120
|
+
// Only create and enable a timer if triggers exist and are enabled
|
|
121
|
+
if (!task.frontmatter.triggers_enabled)
|
|
122
|
+
return;
|
|
123
|
+
const triggers = task.frontmatter.triggers || [];
|
|
124
|
+
const onCalendarLines = [];
|
|
125
|
+
for (const trigger of triggers) {
|
|
126
|
+
if (trigger.type === "cron") {
|
|
127
|
+
onCalendarLines.push(`OnCalendar=${cronToOnCalendar(trigger.value)}`);
|
|
128
|
+
}
|
|
129
|
+
else if (trigger.type === "once") {
|
|
130
|
+
onCalendarLines.push(`OnActiveSec=${trigger.value}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (onCalendarLines.length > 0) {
|
|
134
|
+
const timerContent = `[Unit]
|
|
135
|
+
Description=Timer for Palmier Task: ${taskId}
|
|
136
|
+
|
|
137
|
+
[Timer]
|
|
138
|
+
${onCalendarLines.join("\n")}
|
|
139
|
+
Persistent=true
|
|
140
|
+
|
|
141
|
+
[Install]
|
|
142
|
+
WantedBy=timers.target
|
|
143
|
+
`;
|
|
144
|
+
fs.writeFileSync(path.join(UNIT_DIR, timerName), timerContent, "utf-8");
|
|
145
|
+
daemonReload();
|
|
146
|
+
try {
|
|
147
|
+
execSync(`systemctl --user enable --now ${timerName}`, { encoding: "utf-8" });
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
const e = err;
|
|
151
|
+
console.error(`Failed to enable timer ${timerName}: ${e.stderr || err}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
removeTaskTimer(taskId) {
|
|
156
|
+
const timerName = getTimerName(taskId);
|
|
157
|
+
const serviceName = getServiceName(taskId);
|
|
158
|
+
const timerPath = path.join(UNIT_DIR, timerName);
|
|
159
|
+
const servicePath = path.join(UNIT_DIR, serviceName);
|
|
160
|
+
if (fs.existsSync(timerPath)) {
|
|
161
|
+
try {
|
|
162
|
+
execSync(`systemctl --user stop ${timerName}`, { encoding: "utf-8" });
|
|
163
|
+
}
|
|
164
|
+
catch { /* timer might not be running */ }
|
|
165
|
+
try {
|
|
166
|
+
execSync(`systemctl --user disable ${timerName}`, { encoding: "utf-8" });
|
|
167
|
+
}
|
|
168
|
+
catch { /* timer might not be enabled */ }
|
|
169
|
+
fs.unlinkSync(timerPath);
|
|
170
|
+
}
|
|
171
|
+
if (fs.existsSync(servicePath))
|
|
172
|
+
fs.unlinkSync(servicePath);
|
|
173
|
+
daemonReload();
|
|
174
|
+
}
|
|
175
|
+
async startTask(taskId) {
|
|
176
|
+
const serviceName = getServiceName(taskId);
|
|
177
|
+
await execAsync(`systemctl --user start --no-block ${serviceName}`);
|
|
178
|
+
}
|
|
179
|
+
async stopTask(taskId) {
|
|
180
|
+
const serviceName = getServiceName(taskId);
|
|
181
|
+
await execAsync(`systemctl --user stop ${serviceName}`);
|
|
182
|
+
}
|
|
183
|
+
isTaskRunning(taskId) {
|
|
184
|
+
const serviceName = getServiceName(taskId);
|
|
185
|
+
try {
|
|
186
|
+
// is-active exits 0 only for "active". For oneshot services (Type=oneshot),
|
|
187
|
+
// the state is "activating" while running, which exits non-zero.
|
|
188
|
+
// Use show -p ActiveState to reliably get the state without exit code issues.
|
|
189
|
+
const out = execSync(`systemctl --user show -p ActiveState --value ${serviceName}`, { encoding: "utf-8" });
|
|
190
|
+
const state = out.trim();
|
|
191
|
+
return state === "active" || state === "activating";
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
getGuiEnv() {
|
|
198
|
+
const uid = process.getuid?.();
|
|
199
|
+
const runtimeDir = process.env.XDG_RUNTIME_DIR ||
|
|
200
|
+
(uid !== undefined ? `/run/user/${uid}` : "");
|
|
201
|
+
return {
|
|
202
|
+
DISPLAY: ":0",
|
|
203
|
+
...(runtimeDir ? { XDG_RUNTIME_DIR: runtimeDir } : {}),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
//# sourceMappingURL=linux.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { HostConfig, ParsedTask } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Abstracts OS-specific daemon, scheduling, and process management.
|
|
4
|
+
* Linux uses systemd; Windows uses Task Scheduler; macOS will use launchd.
|
|
5
|
+
*/
|
|
6
|
+
export interface PlatformService {
|
|
7
|
+
/** Install the main `palmier serve` daemon to start at boot. */
|
|
8
|
+
installDaemon(config: HostConfig): void;
|
|
9
|
+
/** Restart the `palmier serve` daemon. */
|
|
10
|
+
restartDaemon(): Promise<void>;
|
|
11
|
+
/** Install a scheduled trigger (timer) for a task. */
|
|
12
|
+
installTaskTimer(config: HostConfig, task: ParsedTask): void;
|
|
13
|
+
/** Remove a task's scheduled trigger and service files. */
|
|
14
|
+
removeTaskTimer(taskId: string): void;
|
|
15
|
+
/** Start a task execution (non-blocking). */
|
|
16
|
+
startTask(taskId: string): Promise<void>;
|
|
17
|
+
/** Abort/stop a running task. */
|
|
18
|
+
stopTask(taskId: string): Promise<void>;
|
|
19
|
+
/** Check if a task is currently running via the system scheduler. */
|
|
20
|
+
isTaskRunning(taskId: string): boolean;
|
|
21
|
+
/** Return env vars needed for GUI access (Linux: DISPLAY, etc.). */
|
|
22
|
+
getGuiEnv(): Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=platform.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PlatformService } from "./platform.js";
|
|
2
|
+
import type { HostConfig, ParsedTask } from "../types.js";
|
|
3
|
+
export declare class WindowsPlatform implements PlatformService {
|
|
4
|
+
installDaemon(config: HostConfig): void;
|
|
5
|
+
restartDaemon(): Promise<void>;
|
|
6
|
+
private spawnDaemon;
|
|
7
|
+
installTaskTimer(config: HostConfig, task: ParsedTask): void;
|
|
8
|
+
removeTaskTimer(taskId: string): void;
|
|
9
|
+
startTask(taskId: string): Promise<void>;
|
|
10
|
+
stopTask(taskId: string): Promise<void>;
|
|
11
|
+
isTaskRunning(taskId: string): boolean;
|
|
12
|
+
getGuiEnv(): Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=windows.d.ts.map
|