palmier 0.9.5 → 0.9.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.
Files changed (258) hide show
  1. package/README.md +30 -13
  2. package/dist/agents/agent.d.ts +0 -1
  3. package/dist/agents/agent.js +0 -1
  4. package/dist/agents/aider.d.ts +0 -1
  5. package/dist/agents/aider.js +0 -1
  6. package/dist/agents/claude.d.ts +0 -1
  7. package/dist/agents/claude.js +0 -1
  8. package/dist/agents/cline.d.ts +0 -1
  9. package/dist/agents/cline.js +0 -1
  10. package/dist/agents/codex.d.ts +0 -1
  11. package/dist/agents/codex.js +0 -1
  12. package/dist/agents/copilot.d.ts +0 -1
  13. package/dist/agents/copilot.js +0 -1
  14. package/dist/agents/cursor.d.ts +0 -1
  15. package/dist/agents/cursor.js +0 -1
  16. package/dist/agents/deepagents.d.ts +0 -1
  17. package/dist/agents/deepagents.js +0 -1
  18. package/dist/agents/droid.d.ts +0 -1
  19. package/dist/agents/droid.js +0 -1
  20. package/dist/agents/gemini.d.ts +0 -1
  21. package/dist/agents/gemini.js +0 -1
  22. package/dist/agents/goose.d.ts +0 -1
  23. package/dist/agents/goose.js +0 -1
  24. package/dist/agents/hermes.d.ts +0 -1
  25. package/dist/agents/hermes.js +0 -1
  26. package/dist/agents/kimi.d.ts +0 -1
  27. package/dist/agents/kimi.js +0 -1
  28. package/dist/agents/kiro.d.ts +0 -1
  29. package/dist/agents/kiro.js +0 -1
  30. package/dist/agents/openclaw.d.ts +0 -1
  31. package/dist/agents/openclaw.js +0 -1
  32. package/dist/agents/opencode.d.ts +0 -1
  33. package/dist/agents/opencode.js +0 -1
  34. package/dist/agents/qoder.d.ts +0 -1
  35. package/dist/agents/qoder.js +0 -1
  36. package/dist/agents/qwen.d.ts +0 -1
  37. package/dist/agents/qwen.js +0 -1
  38. package/dist/agents/shared-prompt.d.ts +0 -1
  39. package/dist/agents/shared-prompt.js +0 -1
  40. package/dist/client-store.d.ts +0 -1
  41. package/dist/client-store.js +0 -1
  42. package/dist/commands/clients.d.ts +0 -1
  43. package/dist/commands/clients.js +0 -1
  44. package/dist/commands/info.d.ts +0 -1
  45. package/dist/commands/info.js +0 -1
  46. package/dist/commands/init.d.ts +0 -1
  47. package/dist/commands/init.js +3 -4
  48. package/dist/commands/pair.d.ts +0 -1
  49. package/dist/commands/pair.js +0 -1
  50. package/dist/commands/restart.d.ts +0 -1
  51. package/dist/commands/restart.js +0 -1
  52. package/dist/commands/run.d.ts +0 -1
  53. package/dist/commands/run.js +0 -1
  54. package/dist/commands/serve.d.ts +0 -1
  55. package/dist/commands/serve.js +0 -1
  56. package/dist/commands/uninstall.d.ts +0 -1
  57. package/dist/commands/uninstall.js +0 -1
  58. package/dist/config.d.ts +0 -1
  59. package/dist/config.js +0 -1
  60. package/dist/event-queues.d.ts +0 -1
  61. package/dist/event-queues.js +0 -1
  62. package/dist/events.d.ts +0 -1
  63. package/dist/events.js +0 -1
  64. package/dist/index.d.ts +0 -1
  65. package/dist/index.js +0 -1
  66. package/dist/linked-device.d.ts +0 -1
  67. package/dist/linked-device.js +0 -1
  68. package/dist/mcp-handler.d.ts +0 -1
  69. package/dist/mcp-handler.js +0 -1
  70. package/dist/mcp-tools.d.ts +0 -1
  71. package/dist/mcp-tools.js +0 -1
  72. package/dist/nats-client.d.ts +0 -1
  73. package/dist/nats-client.js +0 -1
  74. package/dist/network.d.ts +0 -1
  75. package/dist/network.js +0 -1
  76. package/dist/notification-store.d.ts +0 -1
  77. package/dist/notification-store.js +0 -1
  78. package/dist/pending-requests.d.ts +0 -1
  79. package/dist/pending-requests.js +0 -1
  80. package/dist/platform/index.d.ts +0 -1
  81. package/dist/platform/index.js +0 -1
  82. package/dist/platform/linux.d.ts +0 -1
  83. package/dist/platform/linux.js +0 -1
  84. package/dist/platform/macos.d.ts +0 -1
  85. package/dist/platform/macos.js +0 -1
  86. package/dist/platform/platform.d.ts +0 -1
  87. package/dist/platform/platform.js +0 -1
  88. package/dist/platform/windows.d.ts +0 -1
  89. package/dist/platform/windows.js +0 -1
  90. package/dist/pwa/apple-touch-icon.png +0 -0
  91. package/dist/pwa/assets/index-D1bIhEbd.css +1 -0
  92. package/dist/pwa/assets/{index-Cvffaohh.js → index-DWvRAUiy.js} +31 -31
  93. package/dist/pwa/assets/{web-qdLcAD7T.js → web-C4iZbqTC.js} +1 -1
  94. package/dist/pwa/assets/{web-ChtbM4nv.js → web-CBFqJGX6.js} +1 -1
  95. package/dist/pwa/assets/{web-hExASsqW.js → web-DL4uXOpS.js} +1 -1
  96. package/dist/pwa/favicon.ico +0 -0
  97. package/dist/pwa/index.html +3 -3
  98. package/dist/pwa/manifest.webmanifest +1 -1
  99. package/dist/pwa/pwa-192x192.png +0 -0
  100. package/dist/pwa/pwa-512x512.png +0 -0
  101. package/dist/pwa/service-worker.js +1 -1
  102. package/dist/rpc-handler.d.ts +0 -1
  103. package/dist/rpc-handler.js +3 -10
  104. package/dist/sms-store.d.ts +0 -1
  105. package/dist/sms-store.js +0 -1
  106. package/dist/spawn-command.d.ts +0 -1
  107. package/dist/spawn-command.js +0 -1
  108. package/dist/task.d.ts +1 -1
  109. package/dist/task.js +14 -1
  110. package/dist/transports/http-transport.d.ts +0 -1
  111. package/dist/transports/http-transport.js +0 -1
  112. package/dist/transports/nats-transport.d.ts +0 -1
  113. package/dist/transports/nats-transport.js +0 -1
  114. package/dist/types.d.ts +0 -1
  115. package/dist/types.js +0 -1
  116. package/dist/update-checker.d.ts +0 -1
  117. package/dist/update-checker.js +0 -1
  118. package/package.json +5 -1
  119. package/.github/workflows/ci.yml +0 -16
  120. package/.github/workflows/publish.yml +0 -37
  121. package/CLAUDE.md +0 -22
  122. package/dist/pwa/assets/index-DBgOYBrB.css +0 -1
  123. package/palmier-server/.github/workflows/ci.yml +0 -21
  124. package/palmier-server/.github/workflows/deploy.yml +0 -38
  125. package/palmier-server/CLAUDE.md +0 -17
  126. package/palmier-server/PRODUCTION.md +0 -358
  127. package/palmier-server/README.md +0 -231
  128. package/palmier-server/nats.conf +0 -19
  129. package/palmier-server/package.json +0 -15
  130. package/palmier-server/pnpm-lock.yaml +0 -7639
  131. package/palmier-server/pnpm-workspace.yaml +0 -3
  132. package/palmier-server/pwa/index.html +0 -16
  133. package/palmier-server/pwa/logo/logo-prompt.md +0 -28
  134. package/palmier-server/pwa/logo/logo_20260330.png +0 -0
  135. package/palmier-server/pwa/package.json +0 -34
  136. package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
  137. package/palmier-server/pwa/public/favicon.ico +0 -0
  138. package/palmier-server/pwa/public/pwa-192x192.png +0 -0
  139. package/palmier-server/pwa/public/pwa-512x512.png +0 -0
  140. package/palmier-server/pwa/src/App.css +0 -3004
  141. package/palmier-server/pwa/src/App.tsx +0 -59
  142. package/palmier-server/pwa/src/agentLabels.ts +0 -11
  143. package/palmier-server/pwa/src/api.ts +0 -67
  144. package/palmier-server/pwa/src/components/CapabilityToggles.tsx +0 -170
  145. package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +0 -114
  146. package/palmier-server/pwa/src/components/HostMenu.tsx +0 -429
  147. package/palmier-server/pwa/src/components/PermissionsDialog.tsx +0 -34
  148. package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +0 -46
  149. package/palmier-server/pwa/src/components/RunDetailView.tsx +0 -343
  150. package/palmier-server/pwa/src/components/SessionComposer.tsx +0 -157
  151. package/palmier-server/pwa/src/components/SessionsView.tsx +0 -326
  152. package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +0 -170
  153. package/palmier-server/pwa/src/components/TabBar.tsx +0 -40
  154. package/palmier-server/pwa/src/components/TaskCard.tsx +0 -255
  155. package/palmier-server/pwa/src/components/TaskForm.tsx +0 -764
  156. package/palmier-server/pwa/src/components/TasksView.tsx +0 -179
  157. package/palmier-server/pwa/src/constants.ts +0 -2
  158. package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +0 -432
  159. package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +0 -124
  160. package/palmier-server/pwa/src/draftGuard.ts +0 -24
  161. package/palmier-server/pwa/src/formatTime.ts +0 -44
  162. package/palmier-server/pwa/src/hooks/useBackClose.ts +0 -75
  163. package/palmier-server/pwa/src/hooks/useMediaQuery.ts +0 -17
  164. package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +0 -102
  165. package/palmier-server/pwa/src/hooks/usePushSubscription.ts +0 -77
  166. package/palmier-server/pwa/src/main.tsx +0 -14
  167. package/palmier-server/pwa/src/native/Device.ts +0 -49
  168. package/palmier-server/pwa/src/pages/Dashboard.tsx +0 -542
  169. package/palmier-server/pwa/src/pages/PairHost.tsx +0 -232
  170. package/palmier-server/pwa/src/pages/PairSetup.tsx +0 -134
  171. package/palmier-server/pwa/src/service-worker.ts +0 -142
  172. package/palmier-server/pwa/src/types.ts +0 -75
  173. package/palmier-server/pwa/src/vite-env.d.ts +0 -11
  174. package/palmier-server/pwa/tsconfig.json +0 -21
  175. package/palmier-server/pwa/tsconfig.node.json +0 -19
  176. package/palmier-server/pwa/vite.config.ts +0 -47
  177. package/palmier-server/server/.env.example +0 -20
  178. package/palmier-server/server/package.json +0 -36
  179. package/palmier-server/server/src/db.ts +0 -44
  180. package/palmier-server/server/src/fcm.ts +0 -74
  181. package/palmier-server/server/src/index.ts +0 -688
  182. package/palmier-server/server/src/nats-jwt.ts +0 -299
  183. package/palmier-server/server/src/nats-setup.ts +0 -48
  184. package/palmier-server/server/src/nats.ts +0 -33
  185. package/palmier-server/server/src/notify.ts +0 -34
  186. package/palmier-server/server/src/push.ts +0 -68
  187. package/palmier-server/server/src/routes/device.ts +0 -224
  188. package/palmier-server/server/src/routes/fcm.ts +0 -64
  189. package/palmier-server/server/src/routes/hosts.ts +0 -56
  190. package/palmier-server/server/src/routes/push.ts +0 -101
  191. package/palmier-server/server/tsconfig.json +0 -20
  192. package/palmier-server/spec.md +0 -533
  193. package/src/agents/agent-instructions.md +0 -28
  194. package/src/agents/agent.ts +0 -114
  195. package/src/agents/aider.ts +0 -35
  196. package/src/agents/claude.ts +0 -39
  197. package/src/agents/cline.ts +0 -35
  198. package/src/agents/codex.ts +0 -40
  199. package/src/agents/copilot.ts +0 -37
  200. package/src/agents/cursor.ts +0 -36
  201. package/src/agents/deepagents.ts +0 -36
  202. package/src/agents/droid.ts +0 -35
  203. package/src/agents/gemini.ts +0 -43
  204. package/src/agents/goose.ts +0 -33
  205. package/src/agents/hermes.ts +0 -36
  206. package/src/agents/kimi.ts +0 -35
  207. package/src/agents/kiro.ts +0 -36
  208. package/src/agents/openclaw.ts +0 -29
  209. package/src/agents/opencode.ts +0 -36
  210. package/src/agents/qoder.ts +0 -36
  211. package/src/agents/qwen.ts +0 -32
  212. package/src/agents/shared-prompt.ts +0 -30
  213. package/src/client-store.ts +0 -68
  214. package/src/commands/clients.ts +0 -29
  215. package/src/commands/info.ts +0 -29
  216. package/src/commands/init.ts +0 -165
  217. package/src/commands/pair.ts +0 -137
  218. package/src/commands/restart.ts +0 -6
  219. package/src/commands/run.ts +0 -608
  220. package/src/commands/serve.ts +0 -211
  221. package/src/commands/uninstall.ts +0 -9
  222. package/src/config.ts +0 -36
  223. package/src/cross-spawn.d.ts +0 -5
  224. package/src/event-queues.ts +0 -41
  225. package/src/events.ts +0 -29
  226. package/src/index.ts +0 -111
  227. package/src/linked-device.ts +0 -52
  228. package/src/mcp-handler.ts +0 -200
  229. package/src/mcp-tools.ts +0 -839
  230. package/src/nats-client.ts +0 -19
  231. package/src/network.ts +0 -96
  232. package/src/notification-store.ts +0 -30
  233. package/src/pending-requests.ts +0 -73
  234. package/src/platform/index.ts +0 -20
  235. package/src/platform/linux.ts +0 -296
  236. package/src/platform/macos.ts +0 -329
  237. package/src/platform/platform.ts +0 -31
  238. package/src/platform/windows.ts +0 -299
  239. package/src/rpc-handler.ts +0 -694
  240. package/src/sms-store.ts +0 -28
  241. package/src/spawn-command.ts +0 -123
  242. package/src/task.ts +0 -330
  243. package/src/transports/http-transport.ts +0 -478
  244. package/src/transports/nats-transport.ts +0 -76
  245. package/src/types.ts +0 -89
  246. package/src/update-checker.ts +0 -40
  247. package/test/agent-instructions.test.ts +0 -209
  248. package/test/agent-output-parsing.test.ts +0 -74
  249. package/test/linux-cron.test.ts +0 -41
  250. package/test/macos-plist.test.ts +0 -112
  251. package/test/notification-store.test.ts +0 -57
  252. package/test/pairing.test.ts +0 -35
  253. package/test/result-state.test.ts +0 -110
  254. package/test/task-parsing.test.ts +0 -82
  255. package/test/taskrun-messages.test.ts +0 -224
  256. package/test/tsconfig.json +0 -9
  257. package/test/windows-xml.test.ts +0 -89
  258. package/tsconfig.json +0 -19
@@ -1,329 +0,0 @@
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 type { PlatformService } from "./platform.js";
7
- import type { HostConfig, ParsedTask } from "../types.js";
8
- import { CONFIG_DIR, loadConfig } from "../config.js";
9
- import { getTaskDir, readTaskStatus } from "../task.js";
10
-
11
- const execAsync = promisify(exec);
12
-
13
- const AGENT_DIR = path.join(homedir(), "Library", "LaunchAgents");
14
- const PATH_FILE = path.join(CONFIG_DIR, "user-path");
15
- const DAEMON_LABEL = "me.palmier.host";
16
- const TASK_LABEL_PREFIX = "me.palmier.task.";
17
-
18
- function daemonPlistPath(): string {
19
- return path.join(AGENT_DIR, `${DAEMON_LABEL}.plist`);
20
- }
21
-
22
- function taskLabel(taskId: string): string {
23
- return `${TASK_LABEL_PREFIX}${taskId}`;
24
- }
25
-
26
- function taskPlistPath(taskId: string): string {
27
- return path.join(AGENT_DIR, `${taskLabel(taskId)}.plist`);
28
- }
29
-
30
- function taskLogPath(taskId: string): string {
31
- return path.join(CONFIG_DIR, `task-${taskId}.log`);
32
- }
33
-
34
- function guiDomain(): string {
35
- const uid = process.getuid?.();
36
- if (uid === undefined) throw new Error("getuid() unavailable — macOS platform requires POSIX uid");
37
- return `gui/${uid}`;
38
- }
39
-
40
- /**
41
- * Convert one of the four PWA-produced cron patterns to a launchd
42
- * `StartCalendarInterval` dict.
43
- * hourly "0 * * * *" → { Minute: 0 }
44
- * daily "MM HH * * *" → { Minute, Hour }
45
- * weekly "MM HH * * D" → { Minute, Hour, Weekday }
46
- * monthly "MM HH D * *" → { Minute, Hour, Day }
47
- * launchd Weekday: Sunday is 0 (cron 7 → 0).
48
- */
49
- export function cronToCalendarInterval(cron: string): Record<string, number> {
50
- const parts = cron.trim().split(/\s+/);
51
- if (parts.length !== 5) {
52
- throw new Error(`Invalid cron expression (expected 5 fields): ${cron}`);
53
- }
54
- const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
55
- const result: Record<string, number> = {};
56
-
57
- if (minute !== "*") result.Minute = Number(minute);
58
- if (hour !== "*") result.Hour = Number(hour);
59
- if (dayOfMonth !== "*") result.Day = Number(dayOfMonth);
60
- if (dayOfWeek !== "*") {
61
- const dow = Number(dayOfWeek);
62
- result.Weekday = dow === 7 ? 0 : dow;
63
- }
64
-
65
- for (const [k, v] of Object.entries(result)) {
66
- if (!Number.isInteger(v) || v < 0) {
67
- throw new Error(`Invalid cron field ${k}=${v} in ${cron}`);
68
- }
69
- }
70
- return result;
71
- }
72
-
73
- /**
74
- * Convert a PWA `specific_times` value (ISO local datetime like "2026-04-20T09:00")
75
- * to a `StartCalendarInterval` dict. launchd has no "one-shot at date X" trigger,
76
- * so we omit Year — the task fires yearly on the same date and time. Sufficient
77
- * because the PWA regenerates/removes one-off tasks after they run.
78
- */
79
- export function specificTimeToCalendarInterval(iso: string): Record<string, number> {
80
- const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
81
- if (!m) throw new Error(`Invalid specific_times value: ${iso}`);
82
- return {
83
- Month: Number(m[2]),
84
- Day: Number(m[3]),
85
- Hour: Number(m[4]),
86
- Minute: Number(m[5]),
87
- };
88
- }
89
-
90
- function escapeXml(s: string): string {
91
- return s
92
- .replace(/&/g, "&amp;")
93
- .replace(/</g, "&lt;")
94
- .replace(/>/g, "&gt;");
95
- }
96
-
97
- /** Serialize a JS value to a plist XML fragment. Supports string/number/boolean/array/plain object. */
98
- function plistValue(value: unknown, indent: string): string {
99
- if (typeof value === "string") return `${indent}<string>${escapeXml(value)}</string>`;
100
- if (typeof value === "boolean") return `${indent}<${value ? "true" : "false"}/>`;
101
- if (typeof value === "number") {
102
- return Number.isInteger(value)
103
- ? `${indent}<integer>${value}</integer>`
104
- : `${indent}<real>${value}</real>`;
105
- }
106
- if (Array.isArray(value)) {
107
- if (value.length === 0) return `${indent}<array/>`;
108
- const inner = value.map((v) => plistValue(v, indent + " ")).join("\n");
109
- return `${indent}<array>\n${inner}\n${indent}</array>`;
110
- }
111
- if (value && typeof value === "object") {
112
- const entries = Object.entries(value as Record<string, unknown>);
113
- if (entries.length === 0) return `${indent}<dict/>`;
114
- const inner = entries
115
- .map(([k, v]) => `${indent} <key>${escapeXml(k)}</key>\n${plistValue(v, indent + " ")}`)
116
- .join("\n");
117
- return `${indent}<dict>\n${inner}\n${indent}</dict>`;
118
- }
119
- throw new Error(`Unsupported plist value type: ${typeof value}`);
120
- }
121
-
122
- export function buildPlist(dict: Record<string, unknown>): string {
123
- return [
124
- `<?xml version="1.0" encoding="UTF-8"?>`,
125
- `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">`,
126
- `<plist version="1.0">`,
127
- plistValue(dict, ""),
128
- `</plist>`,
129
- ``,
130
- ].join("\n");
131
- }
132
-
133
- function runLaunchctl(args: string[], opts: { ignoreFailure?: boolean } = {}): void {
134
- try {
135
- execSync(`launchctl ${args.join(" ")}`, { stdio: "pipe", encoding: "utf-8" });
136
- } catch (err: unknown) {
137
- if (opts.ignoreFailure) return;
138
- const e = err as { stderr?: string };
139
- console.error(`launchctl ${args[0]} failed: ${e.stderr || err}`);
140
- }
141
- }
142
-
143
- /**
144
- * Reload a LaunchAgent plist. The `enable` call is essential: after `bootout`
145
- * macOS can leave the service in a *disabled* state (tracked in
146
- * /var/db/com.apple.xpc.launchd/disabled.<uid>.plist). A subsequent bootstrap
147
- * then fails with "Bootstrap failed: 5: Input/output error".
148
- */
149
- function reloadAgent(domain: string, label: string, plistPath: string): void {
150
- runLaunchctl(["bootout", `${domain}/${label}`], { ignoreFailure: true });
151
- runLaunchctl(["enable", `${domain}/${label}`], { ignoreFailure: true });
152
- runLaunchctl(["bootstrap", domain, `"${plistPath}"`]);
153
- }
154
-
155
- export class MacOsPlatform implements PlatformService {
156
- installDaemon(config: HostConfig): void {
157
- fs.mkdirSync(AGENT_DIR, { recursive: true });
158
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
159
-
160
- const palmierBin = process.argv[1] || "palmier";
161
- const userPath = process.env.PATH || "/usr/local/bin:/usr/bin:/bin";
162
- fs.writeFileSync(PATH_FILE, userPath, "utf-8");
163
-
164
- const logPath = path.join(CONFIG_DIR, "daemon.log");
165
- const plist = buildPlist({
166
- Label: DAEMON_LABEL,
167
- ProgramArguments: [process.execPath, palmierBin, "serve"],
168
- WorkingDirectory: config.projectRoot,
169
- RunAtLoad: true,
170
- KeepAlive: { SuccessfulExit: false },
171
- EnvironmentVariables: { PATH: userPath },
172
- StandardOutPath: logPath,
173
- StandardErrorPath: logPath,
174
- });
175
-
176
- const plistPath = daemonPlistPath();
177
- fs.writeFileSync(plistPath, plist, "utf-8");
178
- console.log("LaunchAgent installed at:", plistPath);
179
-
180
- const domain = guiDomain();
181
- reloadAgent(domain, DAEMON_LABEL, plistPath);
182
- runLaunchctl(["kickstart", "-k", `${domain}/${DAEMON_LABEL}`]);
183
-
184
- console.log("Palmier host LaunchAgent loaded and started.");
185
- console.log(
186
- "Note: LaunchAgents only run while you are logged into the GUI session. " +
187
- "After reboot, tasks remain dormant until you log in at least once.",
188
- );
189
-
190
- console.log("\nHost initialization complete!");
191
- }
192
-
193
- uninstallDaemon(): void {
194
- const domain = guiDomain();
195
- runLaunchctl(["bootout", `${domain}/${DAEMON_LABEL}`], { ignoreFailure: true });
196
- try { fs.unlinkSync(daemonPlistPath()); } catch { /* may not exist */ }
197
-
198
- try {
199
- const entries = fs.readdirSync(AGENT_DIR).filter((f) => f.startsWith(TASK_LABEL_PREFIX) && f.endsWith(".plist"));
200
- for (const f of entries) {
201
- const label = f.slice(0, -".plist".length);
202
- runLaunchctl(["bootout", `${domain}/${label}`], { ignoreFailure: true });
203
- try { fs.unlinkSync(path.join(AGENT_DIR, f)); } catch { /* ignore */ }
204
- }
205
- } catch { /* AGENT_DIR may not exist */ }
206
-
207
- console.log("Palmier daemon and tasks uninstalled.");
208
- }
209
-
210
- async restartDaemon(): Promise<void> {
211
- const plistPath = daemonPlistPath();
212
- const domain = guiDomain();
213
-
214
- if (process.stdin.isTTY && fs.existsSync(plistPath)) {
215
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
216
- const userPath = process.env.PATH || "";
217
- fs.writeFileSync(PATH_FILE, userPath, "utf-8");
218
-
219
- const content = fs.readFileSync(plistPath, "utf-8");
220
- const updated = content.replace(
221
- /(<key>PATH<\/key>\s*\n\s*<string>)[^<]*(<\/string>)/,
222
- `$1${escapeXml(userPath)}$2`,
223
- );
224
- if (updated !== content) {
225
- fs.writeFileSync(plistPath, updated, "utf-8");
226
- reloadAgent(domain, DAEMON_LABEL, plistPath);
227
- }
228
- }
229
-
230
- runLaunchctl(["kickstart", "-k", `${domain}/${DAEMON_LABEL}`]);
231
- console.log("Palmier daemon restarted.");
232
- }
233
-
234
- installTaskTimer(config: HostConfig, task: ParsedTask): void {
235
- fs.mkdirSync(AGENT_DIR, { recursive: true });
236
-
237
- const taskId = task.frontmatter.id;
238
- const label = taskLabel(taskId);
239
- const plistPath = taskPlistPath(taskId);
240
- const palmierBin = process.argv[1] || "palmier";
241
-
242
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
243
- const logPath = taskLogPath(taskId);
244
- const dict: Record<string, unknown> = {
245
- Label: label,
246
- ProgramArguments: [process.execPath, palmierBin, "run", taskId],
247
- WorkingDirectory: config.projectRoot,
248
- RunAtLoad: false,
249
- EnvironmentVariables: { PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin" },
250
- StandardOutPath: logPath,
251
- StandardErrorPath: logPath,
252
- };
253
-
254
- const scheduleType = task.frontmatter.schedule_type;
255
- const scheduleValues = task.frontmatter.schedule_values;
256
- const isTimerSchedule = scheduleType === "crons" || scheduleType === "specific_times";
257
- if (task.frontmatter.schedule_enabled && isTimerSchedule && scheduleValues?.length) {
258
- const intervals: Record<string, number>[] = [];
259
- for (const value of scheduleValues) {
260
- try {
261
- intervals.push(
262
- scheduleType === "crons"
263
- ? cronToCalendarInterval(value)
264
- : specificTimeToCalendarInterval(value),
265
- );
266
- } catch (err) {
267
- console.error(`Invalid schedule value: ${err}`);
268
- }
269
- }
270
- if (intervals.length > 0) dict.StartCalendarInterval = intervals;
271
- }
272
-
273
- fs.writeFileSync(plistPath, buildPlist(dict), "utf-8");
274
-
275
- const domain = guiDomain();
276
- reloadAgent(domain, label, plistPath);
277
- }
278
-
279
- removeTaskTimer(taskId: string): void {
280
- const domain = guiDomain();
281
- runLaunchctl(["bootout", `${domain}/${taskLabel(taskId)}`], { ignoreFailure: true });
282
- try { fs.unlinkSync(taskPlistPath(taskId)); } catch { /* ignore */ }
283
- // Keep the log file — parity with journald retention on Linux, and
284
- // needed to debug the last fire of one-shot specific_times tasks.
285
- }
286
-
287
- async startTask(taskId: string): Promise<void> {
288
- await execAsync(`launchctl kickstart ${guiDomain()}/${taskLabel(taskId)}`);
289
- }
290
-
291
- async stopTask(taskId: string): Promise<void> {
292
- try {
293
- const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
294
- const status = readTaskStatus(taskDir);
295
- if (status?.pid) {
296
- process.kill(status.pid, "SIGTERM");
297
- return;
298
- }
299
- } catch { /* fall through */ }
300
-
301
- await execAsync(`launchctl kill SIGTERM ${guiDomain()}/${taskLabel(taskId)}`);
302
- }
303
-
304
- isTaskRunning(taskId: string): boolean {
305
- try {
306
- const out = execSync(`launchctl print ${guiDomain()}/${taskLabel(taskId)}`, {
307
- encoding: "utf-8",
308
- stdio: ["ignore", "pipe", "ignore"],
309
- });
310
- // Running services show a numeric `pid = N`; idle ones show `state = not running`.
311
- if (/^\s*pid\s*=\s*\d+/m.test(out)) return true;
312
- } catch { /* service may not be loaded */ }
313
-
314
- try {
315
- const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
316
- const status = readTaskStatus(taskDir);
317
- if (status?.pid) {
318
- process.kill(status.pid, 0);
319
- return true;
320
- }
321
- } catch { /* process not running or config unavailable */ }
322
-
323
- return false;
324
- }
325
-
326
- getGuiEnv(): Record<string, string> {
327
- return {};
328
- }
329
- }
@@ -1,31 +0,0 @@
1
- import type { HostConfig, ParsedTask } from "../types.js";
2
-
3
- /** Linux: systemd. Windows: Task Scheduler. macOS: launchd (planned). */
4
- export interface PlatformService {
5
- /** Install the main `palmier serve` daemon to start at boot. */
6
- installDaemon(config: HostConfig): void;
7
-
8
- /** Restart the `palmier serve` daemon. */
9
- restartDaemon(): Promise<void>;
10
-
11
- /** Stop the daemon and remove all scheduled tasks/timers. */
12
- uninstallDaemon(): void;
13
-
14
- /** Install a scheduled trigger (timer) for a task. */
15
- installTaskTimer(config: HostConfig, task: ParsedTask): void;
16
-
17
- /** Remove a task's scheduled trigger and service files. */
18
- removeTaskTimer(taskId: string): void;
19
-
20
- /** Start a task execution (non-blocking). */
21
- startTask(taskId: string): Promise<void>;
22
-
23
- /** Abort/stop a running task. */
24
- stopTask(taskId: string): Promise<void>;
25
-
26
- /** Check if a task is currently running via the system scheduler. */
27
- isTaskRunning(taskId: string): boolean;
28
-
29
- /** Return env vars needed for GUI access (Linux: DISPLAY, etc.). */
30
- getGuiEnv(): Record<string, string>;
31
- }
@@ -1,299 +0,0 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import { execFileSync } from "child_process";
4
- import type { PlatformService } from "./platform.js";
5
- import type { HostConfig, ParsedTask } from "../types.js";
6
- import { CONFIG_DIR, loadConfig } from "../config.js";
7
- import { getTaskDir, readTaskStatus } from "../task.js";
8
-
9
-
10
- const TASK_PREFIX = "\\Palmier\\PalmierTask-";
11
- const DAEMON_TASK_NAME = "PalmierDaemon";
12
-
13
-
14
- const DOW_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
15
-
16
- /**
17
- * Convert a single schedule value to a Task Scheduler XML trigger element.
18
- *
19
- * `specific_times` values are ISO datetime strings like "2026-03-28T09:00".
20
- *
21
- * `crons` values are cron expressions. Only these patterns (produced by the PWA UI) are handled:
22
- * hourly: "0 * * * *"
23
- * daily: "MM HH * * *"
24
- * weekly: "MM HH * * D"
25
- * monthly: "MM HH D * *"
26
- */
27
- export function scheduleValueToXml(scheduleType: "crons" | "specific_times", value: string): string {
28
- if (scheduleType === "specific_times") {
29
- return `<TimeTrigger><StartBoundary>${value}:00</StartBoundary></TimeTrigger>`;
30
- }
31
-
32
- const parts = value.trim().split(/\s+/);
33
- if (parts.length !== 5) throw new Error(`Invalid cron expression: ${value}`);
34
- const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
35
- const st = `${hour.padStart(2, "0")}:${minute.padStart(2, "0")}:00`;
36
- // StartBoundary needs a full date; anchor to a past one.
37
- const base = `2000-01-01T${st}`;
38
-
39
- if (hour === "*") {
40
- return `<TimeTrigger><StartBoundary>${base}</StartBoundary><Repetition><Interval>PT1H</Interval></Repetition></TimeTrigger>`;
41
- }
42
-
43
- if (dayOfMonth === "*" && dayOfWeek !== "*") {
44
- const day = DOW_NAMES[Number(dayOfWeek)] ?? "Monday";
45
- return `<CalendarTrigger><StartBoundary>${base}</StartBoundary><ScheduleByWeek><DaysOfWeek><${day} /></DaysOfWeek><WeeksInterval>1</WeeksInterval></ScheduleByWeek></CalendarTrigger>`;
46
- }
47
-
48
- if (dayOfMonth !== "*" && dayOfWeek === "*") {
49
- return `<CalendarTrigger><StartBoundary>${base}</StartBoundary><ScheduleByMonth><DaysOfMonth><Day>${dayOfMonth}</Day></DaysOfMonth><Months><January /><February /><March /><April /><May /><June /><July /><August /><September /><October /><November /><December /></Months></ScheduleByMonth></CalendarTrigger>`;
50
- }
51
-
52
- return `<CalendarTrigger><StartBoundary>${base}</StartBoundary><ScheduleByDay><DaysInterval>1</DaysInterval></ScheduleByDay></CalendarTrigger>`;
53
- }
54
-
55
- export function buildTaskXml(tr: string, triggers: string[], foreground?: boolean): string {
56
- const [command, ...argParts] = tr.match(/"[^"]*"|[^\s]+/g) ?? [];
57
- const commandStr = command?.replace(/"/g, "") ?? "";
58
- const argsStr = argParts.map((a) => a.replace(/"/g, "")).join(" ");
59
-
60
- return [
61
- `<?xml version="1.0" encoding="UTF-16"?>`,
62
- `<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">`,
63
- ` <Principals>`,
64
- ` <Principal>`,
65
- ` <LogonType>${foreground ? "InteractiveToken" : "S4U"}</LogonType>`,
66
- ` <RunLevel>LeastPrivilege</RunLevel>`,
67
- ` </Principal>`,
68
- ` </Principals>`,
69
- ` <Settings>`,
70
- ` <MultipleInstancesPolicy>StopExisting</MultipleInstancesPolicy>`,
71
- ` <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>`,
72
- ` <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>`,
73
- ` <UseUnifiedSchedulingEngine>true</UseUnifiedSchedulingEngine>`,
74
- ` </Settings>`,
75
- ` <Triggers>${triggers.join("")}</Triggers>`,
76
- ` <Actions>`,
77
- ` <Exec>`,
78
- ` <Command>${commandStr}</Command>`,
79
- ` <Arguments>${argsStr}</Arguments>`,
80
- ` </Exec>`,
81
- ` </Actions>`,
82
- `</Task>`,
83
- ].join("\n");
84
- }
85
-
86
- function schtasksTaskName(taskId: string): string {
87
- return `${TASK_PREFIX}${taskId}`;
88
- }
89
-
90
- export class WindowsPlatform implements PlatformService {
91
- installDaemon(config: HostConfig): void {
92
- const script = process.argv[1] || "palmier";
93
-
94
- this.ensureDaemonTask(script);
95
- this.startDaemonTask();
96
-
97
- console.log("\nHost initialization complete!");
98
- }
99
-
100
- uninstallDaemon(): void {
101
- const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
102
-
103
- try {
104
- execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
105
- } catch { /* task may not be running */ }
106
-
107
- // Deleting an S4U task requires elevation.
108
- try {
109
- execFileSync("powershell", [
110
- "-Command", `Start-Process -Verb RunAs -Wait -FilePath schtasks -ArgumentList '/delete /tn "${tn}" /f'`,
111
- ], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
112
- console.log("Daemon task removed.");
113
- } catch { /* task may not exist */ }
114
-
115
- try {
116
- const out = execFileSync("schtasks", ["/query", "/fo", "CSV", "/nh"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
117
- for (const line of out.split("\n")) {
118
- const match = line.match(/"(\\Palmier\\PalmierTask-[^"]+)"/);
119
- if (match) {
120
- try { execFileSync("schtasks", ["/end", "/tn", match[1]], { encoding: "utf-8", windowsHide: true, stdio: "pipe" }); } catch { /* ignore */ }
121
- try { execFileSync("schtasks", ["/delete", "/tn", match[1], "/f"], { encoding: "utf-8", windowsHide: true, stdio: "pipe" }); } catch { /* ignore */ }
122
- }
123
- }
124
- console.log("Task timers removed.");
125
- } catch { /* ignore */ }
126
-
127
- console.log("Palmier daemon and tasks uninstalled.");
128
- }
129
-
130
- async restartDaemon(): Promise<void> {
131
- const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
132
-
133
- try {
134
- execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
135
- } catch { /* task may not be running */ }
136
-
137
- this.startDaemonTask();
138
- }
139
-
140
- /** S4U LogonType requires elevation to create. */
141
- private ensureDaemonTask(script: string): void {
142
- const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
143
- const tr = `"${process.execPath}" "${script}" serve`;
144
- const xml = buildTaskXml(tr, [`<BootTrigger><Enabled>true</Enabled></BootTrigger>`]);
145
- const xmlPath = path.join(CONFIG_DIR, "daemon-task.xml");
146
- try {
147
- const bom = Buffer.from([0xFF, 0xFE]);
148
- fs.writeFileSync(xmlPath, Buffer.concat([bom, Buffer.from(xml, "utf16le")]));
149
- // S4U requires elevation — spawn schtasks via RunAs.
150
- const args = `/create /tn "${tn}" /xml "${xmlPath}" /f`;
151
- execFileSync("powershell", [
152
- "-Command", `Start-Process -Verb RunAs -Wait -FilePath schtasks -ArgumentList '${args}'`,
153
- ], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
154
- } catch (err: unknown) {
155
- const e = err as { stderr?: string };
156
- console.error(`Failed to create daemon task: ${e.stderr || err}`);
157
- } finally {
158
- try { fs.unlinkSync(xmlPath); } catch { /* ignore */ }
159
- }
160
-
161
- }
162
-
163
- /** Starting via Task Scheduler runs the daemon outside any session's job object. */
164
- private startDaemonTask(): void {
165
- const tn = `\\Palmier\\${DAEMON_TASK_NAME}`;
166
- try {
167
- execFileSync("schtasks", ["/run", "/tn", tn], { encoding: "utf-8", windowsHide: true, stdio: "pipe" });
168
- } catch (err: unknown) {
169
- const e = err as { stderr?: string };
170
- console.error(`Failed to start daemon via Task Scheduler: ${e.stderr || err}`);
171
- }
172
- console.log("Palmier daemon started.");
173
- }
174
-
175
- installTaskTimer(config: HostConfig, task: ParsedTask): void {
176
- const taskId = task.frontmatter.id;
177
- const tn = schtasksTaskName(taskId);
178
- const script = process.argv[1] || "palmier";
179
- const tr = `"${process.execPath}" "${script}" run ${taskId}`;
180
-
181
- // Event-based schedule types (on_new_notification/on_new_sms) are driven by
182
- // the run process, not the OS scheduler — they fall through to the dummy trigger.
183
- const triggerElements: string[] = [];
184
- const scheduleType = task.frontmatter.schedule_type;
185
- const scheduleValues = task.frontmatter.schedule_values;
186
- const isTimerSchedule = scheduleType === "crons" || scheduleType === "specific_times";
187
- if (task.frontmatter.schedule_enabled && isTimerSchedule && scheduleValues?.length) {
188
- for (const value of scheduleValues) {
189
- try {
190
- triggerElements.push(scheduleValueToXml(scheduleType, value));
191
- } catch (err) {
192
- console.error(`Invalid schedule value: ${err}`);
193
- }
194
- }
195
- }
196
- // Dummy trigger so schtasks /run still works.
197
- if (triggerElements.length === 0) {
198
- triggerElements.push(`<TimeTrigger><StartBoundary>2000-01-01T00:00:00</StartBoundary></TimeTrigger>`);
199
- }
200
-
201
- // XML registration (vs schtasks flags) gives us access to settings like
202
- // MultipleInstancesPolicy. S4U keeps the console hidden unless
203
- // foreground_mode is set. Works unelevated because the caller (daemon)
204
- // runs elevated.
205
- const xml = buildTaskXml(tr, triggerElements, task.frontmatter.foreground_mode);
206
- const xmlPath = path.join(CONFIG_DIR, `task-${taskId}.xml`);
207
- try {
208
- // schtasks /xml requires UTF-16LE with BOM.
209
- const bom = Buffer.from([0xFF, 0xFE]);
210
- fs.writeFileSync(xmlPath, Buffer.concat([bom, Buffer.from(xml, "utf16le")]));
211
- execFileSync("schtasks", [
212
- "/create", "/tn", tn, "/xml", xmlPath, "/f",
213
- ], { encoding: "utf-8", windowsHide: true });
214
- } catch (err: unknown) {
215
- const e = err as { stderr?: string };
216
- console.error(`Failed to create scheduled task ${tn}: ${e.stderr || err}`);
217
- } finally {
218
- try { fs.unlinkSync(xmlPath); } catch { /* ignore */ }
219
- }
220
-
221
- }
222
-
223
- removeTaskTimer(taskId: string): void {
224
- const tn = schtasksTaskName(taskId);
225
- try {
226
- execFileSync("schtasks", ["/delete", "/tn", tn, "/f"], { encoding: "utf-8", windowsHide: true });
227
- } catch { /* task may not exist */ }
228
- }
229
-
230
- async startTask(taskId: string): Promise<void> {
231
- const tn = schtasksTaskName(taskId);
232
- try {
233
- execFileSync("schtasks", ["/run", "/tn", tn], { encoding: "utf-8", windowsHide: true });
234
- } catch (err: unknown) {
235
- const e = err as { stderr?: string; message?: string };
236
- throw new Error(`Failed to start task via schtasks: ${e.stderr || e.message}`);
237
- }
238
- }
239
-
240
- async stopTask(taskId: string): Promise<void> {
241
- // schtasks /end leaves agent children orphaned, so kill the process tree
242
- // via the PID recorded in status.json first.
243
- try {
244
- const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
245
- const status = readTaskStatus(taskDir);
246
- if (status?.pid) {
247
- execFileSync("taskkill", ["/pid", String(status.pid), "/f", "/t"], { windowsHide: true, stdio: "pipe" });
248
- return;
249
- }
250
- } catch {
251
- // PID may be stale or config unavailable; fall through to schtasks /end.
252
- }
253
-
254
- const tn = schtasksTaskName(taskId);
255
- try {
256
- execFileSync("schtasks", ["/end", "/tn", tn], { encoding: "utf-8", windowsHide: true });
257
- } catch (err: unknown) {
258
- const e = err as { stderr?: string; message?: string };
259
- throw new Error(`Failed to stop task via schtasks: ${e.stderr || e.message}`);
260
- }
261
- }
262
-
263
- isTaskRunning(taskId: string): boolean {
264
- const tn = schtasksTaskName(taskId);
265
- try {
266
- const out = execFileSync("schtasks", ["/query", "/tn", tn, "/fo", "CSV", "/nh"], {
267
- encoding: "utf-8",
268
- windowsHide: true,
269
- });
270
- if (out.includes('"Running"')) return true;
271
- } catch { /* task may not exist in scheduler */ }
272
-
273
- // Follow-up runs are spawned directly (not via schtasks), so check PID too.
274
- try {
275
- const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
276
- const status = readTaskStatus(taskDir);
277
- if (status?.pid) {
278
- execFileSync("tasklist", ["/fi", `PID eq ${status.pid}`, "/nh"], {
279
- encoding: "utf-8",
280
- windowsHide: true,
281
- stdio: "pipe",
282
- });
283
- // tasklist always exits 0, so match the output for the PID.
284
- const out = execFileSync("tasklist", ["/fi", `PID eq ${status.pid}`, "/fo", "CSV", "/nh"], {
285
- encoding: "utf-8",
286
- windowsHide: true,
287
- stdio: "pipe",
288
- });
289
- if (out.includes(`"${status.pid}"`)) return true;
290
- }
291
- } catch { /* ignore */ }
292
-
293
- return false;
294
- }
295
-
296
- getGuiEnv(): Record<string, string> {
297
- return {};
298
- }
299
- }