palmier 0.8.3 → 0.8.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/README.md +6 -4
- package/dist/commands/pair.js +2 -0
- package/dist/commands/serve.js +0 -4
- package/dist/platform/index.js +7 -3
- package/dist/platform/macos.d.ts +32 -0
- package/dist/platform/macos.js +298 -0
- package/dist/pwa/assets/index-BiAE5qeC.js +120 -0
- package/dist/pwa/assets/{index-B0F9mtid.css → index-UaZFu6XL.css} +1 -1
- package/dist/pwa/assets/{web-C6lkQj9J.js → web-DYwZE4qa.js} +1 -1
- package/dist/pwa/assets/{web-Z1623me-.js → web-nSzKzI8x.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +0 -4
- package/dist/transports/http-transport.js +1 -0
- package/package.json +1 -1
- package/palmier-server/pwa/src/App.css +191 -33
- package/palmier-server/pwa/src/App.tsx +2 -0
- package/palmier-server/pwa/src/components/CapabilityToggles.tsx +288 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +15 -312
- package/palmier-server/pwa/src/components/SessionComposer.tsx +11 -2
- package/palmier-server/pwa/src/components/SessionsView.tsx +3 -1
- package/palmier-server/pwa/src/components/TaskCard.tsx +1 -1
- package/palmier-server/pwa/src/components/TaskForm.tsx +126 -74
- package/palmier-server/pwa/src/components/TasksView.tsx +3 -1
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/native/Device.ts +0 -2
- package/palmier-server/pwa/src/pages/Dashboard.tsx +2 -0
- package/palmier-server/pwa/src/pages/PairHost.tsx +3 -1
- package/palmier-server/pwa/src/pages/PairSetup.tsx +70 -0
- package/palmier-server/spec.md +3 -3
- package/src/commands/pair.ts +2 -0
- package/src/commands/serve.ts +0 -3
- package/src/platform/index.ts +4 -3
- package/src/platform/macos.ts +319 -0
- package/src/rpc-handler.ts +0 -5
- package/src/transports/http-transport.ts +1 -0
- package/test/macos-plist.test.ts +112 -0
- package/dist/app-registry.d.ts +0 -10
- package/dist/app-registry.js +0 -44
- package/dist/pwa/assets/index-SYs3mcdJ.js +0 -120
- package/src/app-registry.ts +0 -52
package/README.md
CHANGED
|
@@ -29,12 +29,12 @@ It runs on your machine as a background daemon and connects to a mobile-friendly
|
|
|
29
29
|
### Prerequisites
|
|
30
30
|
|
|
31
31
|
- **Node.js 24+**
|
|
32
|
-
- **Linux with systemd** or **Windows 10/11**
|
|
32
|
+
- **Linux with systemd**, **macOS 13+**, or **Windows 10/11**
|
|
33
33
|
- At least one supported agent CLI
|
|
34
34
|
|
|
35
35
|
## How It Works
|
|
36
36
|
|
|
37
|
-
Palmier runs as a background daemon (systemd on Linux, Task Scheduler on Windows). It invokes your agent CLIs directly, schedules tasks via native OS timers, and exposes an API that the PWA connects to — either directly over HTTP or remotely through a relay server. Agents can interact with the user's mobile device during execution — requesting input, sending push notifications and full-screen alarms, reading SMS/notifications, managing contacts and calendar, and more.
|
|
37
|
+
Palmier runs as a background daemon (systemd on Linux, launchd on macOS, Task Scheduler on Windows). It invokes your agent CLIs directly, schedules tasks via native OS timers, and exposes an API that the PWA connects to — either directly over HTTP or remotely through a relay server. Agents can interact with the user's mobile device during execution — requesting input, sending push notifications and full-screen alarms, reading SMS/notifications, managing contacts and calendar, and more.
|
|
38
38
|
|
|
39
39
|
### MCP Server
|
|
40
40
|
|
|
@@ -148,11 +148,13 @@ The wizard:
|
|
|
148
148
|
- Configures access modes (HTTP port, LAN access)
|
|
149
149
|
- Shows a summary (including any existing scheduled tasks to recover) and asks for confirmation
|
|
150
150
|
- Registers with the Palmier server, saves configuration to `~/.config/palmier/host.json`
|
|
151
|
-
- Installs a background daemon (systemd user service on Linux, Task Scheduler on Windows)
|
|
151
|
+
- Installs a background daemon (systemd user service on Linux, LaunchAgent on macOS, Task Scheduler on Windows)
|
|
152
152
|
- Auto-enters pair mode to connect your first device
|
|
153
153
|
|
|
154
154
|
The daemon automatically recovers existing tasks by reinstalling their system timers on startup.
|
|
155
155
|
|
|
156
|
+
> **macOS note:** Palmier installs as a user-level LaunchAgent, so it runs without `sudo`. LaunchAgents only run while the user is logged into the GUI session — after a reboot, scheduled tasks stay dormant until you log in at least once. Enable auto-login in System Settings → Users & Groups if you need unattended operation across reboots.
|
|
157
|
+
|
|
156
158
|
Agents are re-detected on every daemon start. Run `palmier restart` after installing or removing a CLI.
|
|
157
159
|
|
|
158
160
|
## CLI Reference
|
|
@@ -190,7 +192,7 @@ To fully remove Palmier from a machine:
|
|
|
190
192
|
|
|
191
193
|
4. **(Optional) Remove configuration and task data:**
|
|
192
194
|
|
|
193
|
-
**Linux:**
|
|
195
|
+
**Linux / macOS:**
|
|
194
196
|
```bash
|
|
195
197
|
rm -rf ~/.config/palmier
|
|
196
198
|
rm -rf ~/palmier # or wherever your Palmier root directory is
|
package/dist/commands/pair.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
|
+
import * as os from "node:os";
|
|
2
3
|
import { StringCodec } from "nats";
|
|
3
4
|
import { loadConfig } from "../config.js";
|
|
4
5
|
import { connectNats } from "../nats-client.js";
|
|
@@ -16,6 +17,7 @@ function buildPairResponse(config, label) {
|
|
|
16
17
|
return {
|
|
17
18
|
hostId: config.hostId,
|
|
18
19
|
clientToken: client.token,
|
|
20
|
+
hostName: os.hostname(),
|
|
19
21
|
};
|
|
20
22
|
}
|
|
21
23
|
function httpPairRegister(port, code) {
|
package/dist/commands/serve.js
CHANGED
|
@@ -15,7 +15,6 @@ import { StringCodec } from "nats";
|
|
|
15
15
|
import { addNotification } from "../notification-store.js";
|
|
16
16
|
import { addSmsMessage } from "../sms-store.js";
|
|
17
17
|
import { enqueueEvent } from "../event-queues.js";
|
|
18
|
-
import { recordApp } from "../app-registry.js";
|
|
19
18
|
const POLL_INTERVAL_MS = 30_000;
|
|
20
19
|
const DAEMON_PID_FILE = path.join(CONFIG_DIR, "daemon.pid");
|
|
21
20
|
/**
|
|
@@ -144,10 +143,7 @@ export async function serveCommand() {
|
|
|
144
143
|
let parsed;
|
|
145
144
|
try {
|
|
146
145
|
parsed = JSON.parse(raw);
|
|
147
|
-
const data = parsed;
|
|
148
146
|
addNotification({ ...parsed, receivedAt: Date.now() });
|
|
149
|
-
if (data.packageName && data.appName)
|
|
150
|
-
recordApp(data.packageName, data.appName);
|
|
151
147
|
}
|
|
152
148
|
catch (err) {
|
|
153
149
|
console.error("[nats] Failed to parse device notification:", err);
|
package/dist/platform/index.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { LinuxPlatform } from "./linux.js";
|
|
2
2
|
import { WindowsPlatform } from "./windows.js";
|
|
3
|
+
import { MacOsPlatform } from "./macos.js";
|
|
3
4
|
/** Windows needs an explicit shell for execSync to resolve .cmd shims. */
|
|
4
5
|
export const SHELL = process.platform === "win32" ? "cmd.exe" : undefined;
|
|
5
6
|
let _instance;
|
|
6
7
|
export function getPlatform() {
|
|
7
8
|
if (!_instance) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
if (process.platform === "win32")
|
|
10
|
+
_instance = new WindowsPlatform();
|
|
11
|
+
else if (process.platform === "darwin")
|
|
12
|
+
_instance = new MacOsPlatform();
|
|
13
|
+
else
|
|
14
|
+
_instance = new LinuxPlatform();
|
|
11
15
|
}
|
|
12
16
|
return _instance;
|
|
13
17
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PlatformService } from "./platform.js";
|
|
2
|
+
import type { HostConfig, ParsedTask } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Convert one of the four PWA-produced cron patterns to a launchd
|
|
5
|
+
* `StartCalendarInterval` dict.
|
|
6
|
+
* hourly "0 * * * *" → { Minute: 0 }
|
|
7
|
+
* daily "MM HH * * *" → { Minute, Hour }
|
|
8
|
+
* weekly "MM HH * * D" → { Minute, Hour, Weekday }
|
|
9
|
+
* monthly "MM HH D * *" → { Minute, Hour, Day }
|
|
10
|
+
* launchd Weekday: Sunday is 0 (cron 7 → 0).
|
|
11
|
+
*/
|
|
12
|
+
export declare function cronToCalendarInterval(cron: string): Record<string, number>;
|
|
13
|
+
/**
|
|
14
|
+
* Convert a PWA `specific_times` value (ISO local datetime like "2026-04-20T09:00")
|
|
15
|
+
* to a `StartCalendarInterval` dict. launchd has no "one-shot at date X" trigger,
|
|
16
|
+
* so we omit Year — the task fires yearly on the same date and time. Sufficient
|
|
17
|
+
* because the PWA regenerates/removes one-off tasks after they run.
|
|
18
|
+
*/
|
|
19
|
+
export declare function specificTimeToCalendarInterval(iso: string): Record<string, number>;
|
|
20
|
+
export declare function buildPlist(dict: Record<string, unknown>): string;
|
|
21
|
+
export declare class MacOsPlatform implements PlatformService {
|
|
22
|
+
installDaemon(config: HostConfig): void;
|
|
23
|
+
uninstallDaemon(): void;
|
|
24
|
+
restartDaemon(): Promise<void>;
|
|
25
|
+
installTaskTimer(config: HostConfig, task: ParsedTask): void;
|
|
26
|
+
removeTaskTimer(taskId: string): void;
|
|
27
|
+
startTask(taskId: string): Promise<void>;
|
|
28
|
+
stopTask(taskId: string): Promise<void>;
|
|
29
|
+
isTaskRunning(taskId: string): boolean;
|
|
30
|
+
getGuiEnv(): Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=macos.d.ts.map
|
|
@@ -0,0 +1,298 @@
|
|
|
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
|
+
import { CONFIG_DIR, loadConfig } from "../config.js";
|
|
7
|
+
import { getTaskDir, readTaskStatus } from "../task.js";
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const AGENT_DIR = path.join(homedir(), "Library", "LaunchAgents");
|
|
10
|
+
const PATH_FILE = path.join(CONFIG_DIR, "user-path");
|
|
11
|
+
const DAEMON_LABEL = "me.palmier.host";
|
|
12
|
+
const TASK_LABEL_PREFIX = "me.palmier.task.";
|
|
13
|
+
function daemonPlistPath() {
|
|
14
|
+
return path.join(AGENT_DIR, `${DAEMON_LABEL}.plist`);
|
|
15
|
+
}
|
|
16
|
+
function taskLabel(taskId) {
|
|
17
|
+
return `${TASK_LABEL_PREFIX}${taskId}`;
|
|
18
|
+
}
|
|
19
|
+
function taskPlistPath(taskId) {
|
|
20
|
+
return path.join(AGENT_DIR, `${taskLabel(taskId)}.plist`);
|
|
21
|
+
}
|
|
22
|
+
function taskLogPath(taskId) {
|
|
23
|
+
return path.join(CONFIG_DIR, `task-${taskId}.log`);
|
|
24
|
+
}
|
|
25
|
+
function guiDomain() {
|
|
26
|
+
const uid = process.getuid?.();
|
|
27
|
+
if (uid === undefined)
|
|
28
|
+
throw new Error("getuid() unavailable — macOS platform requires POSIX uid");
|
|
29
|
+
return `gui/${uid}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Convert one of the four PWA-produced cron patterns to a launchd
|
|
33
|
+
* `StartCalendarInterval` dict.
|
|
34
|
+
* hourly "0 * * * *" → { Minute: 0 }
|
|
35
|
+
* daily "MM HH * * *" → { Minute, Hour }
|
|
36
|
+
* weekly "MM HH * * D" → { Minute, Hour, Weekday }
|
|
37
|
+
* monthly "MM HH D * *" → { Minute, Hour, Day }
|
|
38
|
+
* launchd Weekday: Sunday is 0 (cron 7 → 0).
|
|
39
|
+
*/
|
|
40
|
+
export function cronToCalendarInterval(cron) {
|
|
41
|
+
const parts = cron.trim().split(/\s+/);
|
|
42
|
+
if (parts.length !== 5) {
|
|
43
|
+
throw new Error(`Invalid cron expression (expected 5 fields): ${cron}`);
|
|
44
|
+
}
|
|
45
|
+
const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
|
|
46
|
+
const result = {};
|
|
47
|
+
if (minute !== "*")
|
|
48
|
+
result.Minute = Number(minute);
|
|
49
|
+
if (hour !== "*")
|
|
50
|
+
result.Hour = Number(hour);
|
|
51
|
+
if (dayOfMonth !== "*")
|
|
52
|
+
result.Day = Number(dayOfMonth);
|
|
53
|
+
if (dayOfWeek !== "*") {
|
|
54
|
+
const dow = Number(dayOfWeek);
|
|
55
|
+
result.Weekday = dow === 7 ? 0 : dow;
|
|
56
|
+
}
|
|
57
|
+
for (const [k, v] of Object.entries(result)) {
|
|
58
|
+
if (!Number.isInteger(v) || v < 0) {
|
|
59
|
+
throw new Error(`Invalid cron field ${k}=${v} in ${cron}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Convert a PWA `specific_times` value (ISO local datetime like "2026-04-20T09:00")
|
|
66
|
+
* to a `StartCalendarInterval` dict. launchd has no "one-shot at date X" trigger,
|
|
67
|
+
* so we omit Year — the task fires yearly on the same date and time. Sufficient
|
|
68
|
+
* because the PWA regenerates/removes one-off tasks after they run.
|
|
69
|
+
*/
|
|
70
|
+
export function specificTimeToCalendarInterval(iso) {
|
|
71
|
+
const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
|
|
72
|
+
if (!m)
|
|
73
|
+
throw new Error(`Invalid specific_times value: ${iso}`);
|
|
74
|
+
return {
|
|
75
|
+
Month: Number(m[2]),
|
|
76
|
+
Day: Number(m[3]),
|
|
77
|
+
Hour: Number(m[4]),
|
|
78
|
+
Minute: Number(m[5]),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function escapeXml(s) {
|
|
82
|
+
return s
|
|
83
|
+
.replace(/&/g, "&")
|
|
84
|
+
.replace(/</g, "<")
|
|
85
|
+
.replace(/>/g, ">");
|
|
86
|
+
}
|
|
87
|
+
/** Serialize a JS value to a plist XML fragment. Supports string/number/boolean/array/plain object. */
|
|
88
|
+
function plistValue(value, indent) {
|
|
89
|
+
if (typeof value === "string")
|
|
90
|
+
return `${indent}<string>${escapeXml(value)}</string>`;
|
|
91
|
+
if (typeof value === "boolean")
|
|
92
|
+
return `${indent}<${value ? "true" : "false"}/>`;
|
|
93
|
+
if (typeof value === "number") {
|
|
94
|
+
return Number.isInteger(value)
|
|
95
|
+
? `${indent}<integer>${value}</integer>`
|
|
96
|
+
: `${indent}<real>${value}</real>`;
|
|
97
|
+
}
|
|
98
|
+
if (Array.isArray(value)) {
|
|
99
|
+
if (value.length === 0)
|
|
100
|
+
return `${indent}<array/>`;
|
|
101
|
+
const inner = value.map((v) => plistValue(v, indent + " ")).join("\n");
|
|
102
|
+
return `${indent}<array>\n${inner}\n${indent}</array>`;
|
|
103
|
+
}
|
|
104
|
+
if (value && typeof value === "object") {
|
|
105
|
+
const entries = Object.entries(value);
|
|
106
|
+
if (entries.length === 0)
|
|
107
|
+
return `${indent}<dict/>`;
|
|
108
|
+
const inner = entries
|
|
109
|
+
.map(([k, v]) => `${indent} <key>${escapeXml(k)}</key>\n${plistValue(v, indent + " ")}`)
|
|
110
|
+
.join("\n");
|
|
111
|
+
return `${indent}<dict>\n${inner}\n${indent}</dict>`;
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`Unsupported plist value type: ${typeof value}`);
|
|
114
|
+
}
|
|
115
|
+
export function buildPlist(dict) {
|
|
116
|
+
return [
|
|
117
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
118
|
+
`<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`,
|
|
119
|
+
`<plist version="1.0">`,
|
|
120
|
+
plistValue(dict, ""),
|
|
121
|
+
`</plist>`,
|
|
122
|
+
``,
|
|
123
|
+
].join("\n");
|
|
124
|
+
}
|
|
125
|
+
function runLaunchctl(args, opts = {}) {
|
|
126
|
+
try {
|
|
127
|
+
execSync(`launchctl ${args.join(" ")}`, { stdio: "pipe", encoding: "utf-8" });
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
if (opts.ignoreFailure)
|
|
131
|
+
return;
|
|
132
|
+
const e = err;
|
|
133
|
+
console.error(`launchctl ${args[0]} failed: ${e.stderr || err}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
export class MacOsPlatform {
|
|
137
|
+
installDaemon(config) {
|
|
138
|
+
fs.mkdirSync(AGENT_DIR, { recursive: true });
|
|
139
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
140
|
+
const palmierBin = process.argv[1] || "palmier";
|
|
141
|
+
const userPath = process.env.PATH || "/usr/local/bin:/usr/bin:/bin";
|
|
142
|
+
fs.writeFileSync(PATH_FILE, userPath, "utf-8");
|
|
143
|
+
const logPath = path.join(CONFIG_DIR, "daemon.log");
|
|
144
|
+
const plist = buildPlist({
|
|
145
|
+
Label: DAEMON_LABEL,
|
|
146
|
+
ProgramArguments: [process.execPath, palmierBin, "serve"],
|
|
147
|
+
WorkingDirectory: config.projectRoot,
|
|
148
|
+
RunAtLoad: true,
|
|
149
|
+
KeepAlive: { SuccessfulExit: false },
|
|
150
|
+
EnvironmentVariables: { PATH: userPath },
|
|
151
|
+
StandardOutPath: logPath,
|
|
152
|
+
StandardErrorPath: logPath,
|
|
153
|
+
});
|
|
154
|
+
const plistPath = daemonPlistPath();
|
|
155
|
+
fs.writeFileSync(plistPath, plist, "utf-8");
|
|
156
|
+
console.log("LaunchAgent installed at:", plistPath);
|
|
157
|
+
const domain = guiDomain();
|
|
158
|
+
runLaunchctl(["bootout", `${domain}/${DAEMON_LABEL}`], { ignoreFailure: true });
|
|
159
|
+
runLaunchctl(["bootstrap", domain, `"${plistPath}"`]);
|
|
160
|
+
runLaunchctl(["kickstart", "-k", `${domain}/${DAEMON_LABEL}`]);
|
|
161
|
+
console.log("Palmier host LaunchAgent loaded and started.");
|
|
162
|
+
console.log("Note: LaunchAgents only run while you are logged into the GUI session. " +
|
|
163
|
+
"After reboot, tasks remain dormant until you log in at least once.");
|
|
164
|
+
console.log("\nHost initialization complete!");
|
|
165
|
+
}
|
|
166
|
+
uninstallDaemon() {
|
|
167
|
+
const domain = guiDomain();
|
|
168
|
+
runLaunchctl(["bootout", `${domain}/${DAEMON_LABEL}`], { ignoreFailure: true });
|
|
169
|
+
try {
|
|
170
|
+
fs.unlinkSync(daemonPlistPath());
|
|
171
|
+
}
|
|
172
|
+
catch { /* may not exist */ }
|
|
173
|
+
try {
|
|
174
|
+
const entries = fs.readdirSync(AGENT_DIR).filter((f) => f.startsWith(TASK_LABEL_PREFIX) && f.endsWith(".plist"));
|
|
175
|
+
for (const f of entries) {
|
|
176
|
+
const label = f.slice(0, -".plist".length);
|
|
177
|
+
runLaunchctl(["bootout", `${domain}/${label}`], { ignoreFailure: true });
|
|
178
|
+
try {
|
|
179
|
+
fs.unlinkSync(path.join(AGENT_DIR, f));
|
|
180
|
+
}
|
|
181
|
+
catch { /* ignore */ }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch { /* AGENT_DIR may not exist */ }
|
|
185
|
+
console.log("Palmier daemon and tasks uninstalled.");
|
|
186
|
+
}
|
|
187
|
+
async restartDaemon() {
|
|
188
|
+
const plistPath = daemonPlistPath();
|
|
189
|
+
const domain = guiDomain();
|
|
190
|
+
if (process.stdin.isTTY && fs.existsSync(plistPath)) {
|
|
191
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
192
|
+
const userPath = process.env.PATH || "";
|
|
193
|
+
fs.writeFileSync(PATH_FILE, userPath, "utf-8");
|
|
194
|
+
const content = fs.readFileSync(plistPath, "utf-8");
|
|
195
|
+
const updated = content.replace(/(<key>PATH<\/key>\s*\n\s*<string>)[^<]*(<\/string>)/, `$1${escapeXml(userPath)}$2`);
|
|
196
|
+
if (updated !== content) {
|
|
197
|
+
fs.writeFileSync(plistPath, updated, "utf-8");
|
|
198
|
+
runLaunchctl(["bootout", `${domain}/${DAEMON_LABEL}`], { ignoreFailure: true });
|
|
199
|
+
runLaunchctl(["bootstrap", domain, `"${plistPath}"`]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
runLaunchctl(["kickstart", "-k", `${domain}/${DAEMON_LABEL}`]);
|
|
203
|
+
console.log("Palmier daemon restarted.");
|
|
204
|
+
}
|
|
205
|
+
installTaskTimer(config, task) {
|
|
206
|
+
fs.mkdirSync(AGENT_DIR, { recursive: true });
|
|
207
|
+
const taskId = task.frontmatter.id;
|
|
208
|
+
const label = taskLabel(taskId);
|
|
209
|
+
const plistPath = taskPlistPath(taskId);
|
|
210
|
+
const palmierBin = process.argv[1] || "palmier";
|
|
211
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
212
|
+
const logPath = taskLogPath(taskId);
|
|
213
|
+
const dict = {
|
|
214
|
+
Label: label,
|
|
215
|
+
ProgramArguments: [process.execPath, palmierBin, "run", taskId],
|
|
216
|
+
WorkingDirectory: config.projectRoot,
|
|
217
|
+
RunAtLoad: false,
|
|
218
|
+
EnvironmentVariables: { PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin" },
|
|
219
|
+
StandardOutPath: logPath,
|
|
220
|
+
StandardErrorPath: logPath,
|
|
221
|
+
};
|
|
222
|
+
const scheduleType = task.frontmatter.schedule_type;
|
|
223
|
+
const scheduleValues = task.frontmatter.schedule_values;
|
|
224
|
+
const isTimerSchedule = scheduleType === "crons" || scheduleType === "specific_times";
|
|
225
|
+
if (task.frontmatter.schedule_enabled && isTimerSchedule && scheduleValues?.length) {
|
|
226
|
+
const intervals = [];
|
|
227
|
+
for (const value of scheduleValues) {
|
|
228
|
+
try {
|
|
229
|
+
intervals.push(scheduleType === "crons"
|
|
230
|
+
? cronToCalendarInterval(value)
|
|
231
|
+
: specificTimeToCalendarInterval(value));
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
console.error(`Invalid schedule value: ${err}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (intervals.length > 0)
|
|
238
|
+
dict.StartCalendarInterval = intervals;
|
|
239
|
+
}
|
|
240
|
+
fs.writeFileSync(plistPath, buildPlist(dict), "utf-8");
|
|
241
|
+
const domain = guiDomain();
|
|
242
|
+
runLaunchctl(["bootout", `${domain}/${label}`], { ignoreFailure: true });
|
|
243
|
+
runLaunchctl(["bootstrap", domain, `"${plistPath}"`]);
|
|
244
|
+
}
|
|
245
|
+
removeTaskTimer(taskId) {
|
|
246
|
+
const domain = guiDomain();
|
|
247
|
+
runLaunchctl(["bootout", `${domain}/${taskLabel(taskId)}`], { ignoreFailure: true });
|
|
248
|
+
try {
|
|
249
|
+
fs.unlinkSync(taskPlistPath(taskId));
|
|
250
|
+
}
|
|
251
|
+
catch { /* ignore */ }
|
|
252
|
+
try {
|
|
253
|
+
fs.unlinkSync(taskLogPath(taskId));
|
|
254
|
+
}
|
|
255
|
+
catch { /* ignore */ }
|
|
256
|
+
}
|
|
257
|
+
async startTask(taskId) {
|
|
258
|
+
await execAsync(`launchctl kickstart ${guiDomain()}/${taskLabel(taskId)}`);
|
|
259
|
+
}
|
|
260
|
+
async stopTask(taskId) {
|
|
261
|
+
try {
|
|
262
|
+
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
263
|
+
const status = readTaskStatus(taskDir);
|
|
264
|
+
if (status?.pid) {
|
|
265
|
+
process.kill(status.pid, "SIGTERM");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch { /* fall through */ }
|
|
270
|
+
await execAsync(`launchctl kill SIGTERM ${guiDomain()}/${taskLabel(taskId)}`);
|
|
271
|
+
}
|
|
272
|
+
isTaskRunning(taskId) {
|
|
273
|
+
try {
|
|
274
|
+
const out = execSync(`launchctl print ${guiDomain()}/${taskLabel(taskId)}`, {
|
|
275
|
+
encoding: "utf-8",
|
|
276
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
277
|
+
});
|
|
278
|
+
// Running services show a numeric `pid = N`; idle ones show `state = not running`.
|
|
279
|
+
if (/^\s*pid\s*=\s*\d+/m.test(out))
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
catch { /* service may not be loaded */ }
|
|
283
|
+
try {
|
|
284
|
+
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
285
|
+
const status = readTaskStatus(taskDir);
|
|
286
|
+
if (status?.pid) {
|
|
287
|
+
process.kill(status.pid, 0);
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch { /* process not running or config unavailable */ }
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
getGuiEnv() {
|
|
295
|
+
return {};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=macos.js.map
|