palmier 0.9.6 → 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.
- package/README.md +28 -13
- package/dist/agents/agent.d.ts +0 -1
- package/dist/agents/agent.js +0 -1
- package/dist/agents/aider.d.ts +0 -1
- package/dist/agents/aider.js +0 -1
- package/dist/agents/claude.d.ts +0 -1
- package/dist/agents/claude.js +0 -1
- package/dist/agents/cline.d.ts +0 -1
- package/dist/agents/cline.js +0 -1
- package/dist/agents/codex.d.ts +0 -1
- package/dist/agents/codex.js +0 -1
- package/dist/agents/copilot.d.ts +0 -1
- package/dist/agents/copilot.js +0 -1
- package/dist/agents/cursor.d.ts +0 -1
- package/dist/agents/cursor.js +0 -1
- package/dist/agents/deepagents.d.ts +0 -1
- package/dist/agents/deepagents.js +0 -1
- package/dist/agents/droid.d.ts +0 -1
- package/dist/agents/droid.js +0 -1
- package/dist/agents/gemini.d.ts +0 -1
- package/dist/agents/gemini.js +0 -1
- package/dist/agents/goose.d.ts +0 -1
- package/dist/agents/goose.js +0 -1
- package/dist/agents/hermes.d.ts +0 -1
- package/dist/agents/hermes.js +0 -1
- package/dist/agents/kimi.d.ts +0 -1
- package/dist/agents/kimi.js +0 -1
- package/dist/agents/kiro.d.ts +0 -1
- package/dist/agents/kiro.js +0 -1
- package/dist/agents/openclaw.d.ts +0 -1
- package/dist/agents/openclaw.js +0 -1
- package/dist/agents/opencode.d.ts +0 -1
- package/dist/agents/opencode.js +0 -1
- package/dist/agents/qoder.d.ts +0 -1
- package/dist/agents/qoder.js +0 -1
- package/dist/agents/qwen.d.ts +0 -1
- package/dist/agents/qwen.js +0 -1
- package/dist/agents/shared-prompt.d.ts +0 -1
- package/dist/agents/shared-prompt.js +0 -1
- package/dist/client-store.d.ts +0 -1
- package/dist/client-store.js +0 -1
- package/dist/commands/clients.d.ts +0 -1
- package/dist/commands/clients.js +0 -1
- package/dist/commands/info.d.ts +0 -1
- package/dist/commands/info.js +0 -1
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +1 -2
- package/dist/commands/pair.d.ts +0 -1
- package/dist/commands/pair.js +0 -1
- package/dist/commands/restart.d.ts +0 -1
- package/dist/commands/restart.js +0 -1
- package/dist/commands/run.d.ts +0 -1
- package/dist/commands/run.js +0 -1
- package/dist/commands/serve.d.ts +0 -1
- package/dist/commands/serve.js +0 -1
- package/dist/commands/uninstall.d.ts +0 -1
- package/dist/commands/uninstall.js +0 -1
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/event-queues.d.ts +0 -1
- package/dist/event-queues.js +0 -1
- package/dist/events.d.ts +0 -1
- package/dist/events.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/linked-device.d.ts +0 -1
- package/dist/linked-device.js +0 -1
- package/dist/mcp-handler.d.ts +0 -1
- package/dist/mcp-handler.js +0 -1
- package/dist/mcp-tools.d.ts +0 -1
- package/dist/mcp-tools.js +0 -1
- package/dist/nats-client.d.ts +0 -1
- package/dist/nats-client.js +0 -1
- package/dist/network.d.ts +0 -1
- package/dist/network.js +0 -1
- package/dist/notification-store.d.ts +0 -1
- package/dist/notification-store.js +0 -1
- package/dist/pending-requests.d.ts +0 -1
- package/dist/pending-requests.js +0 -1
- package/dist/platform/index.d.ts +0 -1
- package/dist/platform/index.js +0 -1
- package/dist/platform/linux.d.ts +0 -1
- package/dist/platform/linux.js +0 -1
- package/dist/platform/macos.d.ts +0 -1
- package/dist/platform/macos.js +0 -1
- package/dist/platform/platform.d.ts +0 -1
- package/dist/platform/platform.js +0 -1
- package/dist/platform/windows.d.ts +0 -1
- package/dist/platform/windows.js +0 -1
- package/dist/pwa/assets/{index-MLEFUP3r.js → index-DWvRAUiy.js} +31 -31
- package/dist/pwa/assets/{web-B1sKCc7e.js → web-C4iZbqTC.js} +1 -1
- package/dist/pwa/assets/{web-ETD-8ZHd.js → web-CBFqJGX6.js} +1 -1
- package/dist/pwa/assets/{web-B4xEa6WO.js → web-DL4uXOpS.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.d.ts +0 -1
- package/dist/rpc-handler.js +0 -1
- package/dist/sms-store.d.ts +0 -1
- package/dist/sms-store.js +0 -1
- package/dist/spawn-command.d.ts +0 -1
- package/dist/spawn-command.js +0 -1
- package/dist/task.d.ts +0 -1
- package/dist/task.js +0 -1
- package/dist/transports/http-transport.d.ts +0 -1
- package/dist/transports/http-transport.js +0 -1
- package/dist/transports/nats-transport.d.ts +0 -1
- package/dist/transports/nats-transport.js +0 -1
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -1
- package/dist/update-checker.d.ts +0 -1
- package/dist/update-checker.js +0 -1
- package/package.json +5 -1
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -37
- package/CLAUDE.md +0 -22
- package/palmier-server/.github/workflows/ci.yml +0 -21
- package/palmier-server/.github/workflows/deploy.yml +0 -38
- package/palmier-server/CLAUDE.md +0 -17
- package/palmier-server/PRODUCTION.md +0 -358
- package/palmier-server/README.md +0 -231
- package/palmier-server/nats.conf +0 -19
- package/palmier-server/package.json +0 -15
- package/palmier-server/pnpm-lock.yaml +0 -7639
- package/palmier-server/pnpm-workspace.yaml +0 -3
- package/palmier-server/pwa/index.html +0 -16
- package/palmier-server/pwa/logo/logo_20260421.png +0 -0
- package/palmier-server/pwa/package.json +0 -34
- package/palmier-server/pwa/public/apple-touch-icon.png +0 -0
- package/palmier-server/pwa/public/favicon.ico +0 -0
- package/palmier-server/pwa/public/pwa-192x192.png +0 -0
- package/palmier-server/pwa/public/pwa-512x512.png +0 -0
- package/palmier-server/pwa/src/App.css +0 -3012
- package/palmier-server/pwa/src/App.tsx +0 -59
- package/palmier-server/pwa/src/agentLabels.ts +0 -11
- package/palmier-server/pwa/src/api.ts +0 -67
- package/palmier-server/pwa/src/components/CapabilityToggles.tsx +0 -170
- package/palmier-server/pwa/src/components/ConnectionStatusIcon.tsx +0 -113
- package/palmier-server/pwa/src/components/HostMenu.tsx +0 -429
- package/palmier-server/pwa/src/components/PermissionsDialog.tsx +0 -34
- package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +0 -46
- package/palmier-server/pwa/src/components/RunDetailView.tsx +0 -343
- package/palmier-server/pwa/src/components/SessionComposer.tsx +0 -157
- package/palmier-server/pwa/src/components/SessionsView.tsx +0 -326
- package/palmier-server/pwa/src/components/SwipeToDeleteRow.tsx +0 -170
- package/palmier-server/pwa/src/components/TabBar.tsx +0 -40
- package/palmier-server/pwa/src/components/TaskCard.tsx +0 -255
- package/palmier-server/pwa/src/components/TaskForm.tsx +0 -766
- package/palmier-server/pwa/src/components/TasksView.tsx +0 -179
- package/palmier-server/pwa/src/constants.ts +0 -2
- package/palmier-server/pwa/src/contexts/HostConnectionContext.tsx +0 -432
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +0 -124
- package/palmier-server/pwa/src/draftGuard.ts +0 -24
- package/palmier-server/pwa/src/formatTime.ts +0 -44
- package/palmier-server/pwa/src/hooks/useBackClose.ts +0 -75
- package/palmier-server/pwa/src/hooks/useMediaQuery.ts +0 -17
- package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +0 -102
- package/palmier-server/pwa/src/hooks/usePushSubscription.ts +0 -77
- package/palmier-server/pwa/src/main.tsx +0 -14
- package/palmier-server/pwa/src/native/Device.ts +0 -49
- package/palmier-server/pwa/src/pages/Dashboard.tsx +0 -542
- package/palmier-server/pwa/src/pages/PairHost.tsx +0 -232
- package/palmier-server/pwa/src/pages/PairSetup.tsx +0 -134
- package/palmier-server/pwa/src/service-worker.ts +0 -142
- package/palmier-server/pwa/src/types.ts +0 -75
- package/palmier-server/pwa/src/vite-env.d.ts +0 -11
- package/palmier-server/pwa/tsconfig.json +0 -21
- package/palmier-server/pwa/tsconfig.node.json +0 -19
- package/palmier-server/pwa/vite.config.ts +0 -47
- package/palmier-server/server/.env.example +0 -20
- package/palmier-server/server/package.json +0 -36
- package/palmier-server/server/src/db.ts +0 -44
- package/palmier-server/server/src/fcm.ts +0 -74
- package/palmier-server/server/src/index.ts +0 -688
- package/palmier-server/server/src/nats-jwt.ts +0 -299
- package/palmier-server/server/src/nats-setup.ts +0 -48
- package/palmier-server/server/src/nats.ts +0 -33
- package/palmier-server/server/src/notify.ts +0 -34
- package/palmier-server/server/src/push.ts +0 -68
- package/palmier-server/server/src/routes/device.ts +0 -224
- package/palmier-server/server/src/routes/fcm.ts +0 -64
- package/palmier-server/server/src/routes/hosts.ts +0 -56
- package/palmier-server/server/src/routes/push.ts +0 -101
- package/palmier-server/server/tsconfig.json +0 -20
- package/palmier-server/spec.md +0 -533
- package/src/agents/agent-instructions.md +0 -28
- package/src/agents/agent.ts +0 -114
- package/src/agents/aider.ts +0 -35
- package/src/agents/claude.ts +0 -39
- package/src/agents/cline.ts +0 -35
- package/src/agents/codex.ts +0 -40
- package/src/agents/copilot.ts +0 -37
- package/src/agents/cursor.ts +0 -36
- package/src/agents/deepagents.ts +0 -36
- package/src/agents/droid.ts +0 -35
- package/src/agents/gemini.ts +0 -43
- package/src/agents/goose.ts +0 -33
- package/src/agents/hermes.ts +0 -36
- package/src/agents/kimi.ts +0 -35
- package/src/agents/kiro.ts +0 -36
- package/src/agents/openclaw.ts +0 -29
- package/src/agents/opencode.ts +0 -36
- package/src/agents/qoder.ts +0 -36
- package/src/agents/qwen.ts +0 -32
- package/src/agents/shared-prompt.ts +0 -30
- package/src/client-store.ts +0 -68
- package/src/commands/clients.ts +0 -29
- package/src/commands/info.ts +0 -29
- package/src/commands/init.ts +0 -165
- package/src/commands/pair.ts +0 -137
- package/src/commands/restart.ts +0 -6
- package/src/commands/run.ts +0 -608
- package/src/commands/serve.ts +0 -211
- package/src/commands/uninstall.ts +0 -9
- package/src/config.ts +0 -36
- package/src/cross-spawn.d.ts +0 -5
- package/src/event-queues.ts +0 -41
- package/src/events.ts +0 -29
- package/src/index.ts +0 -111
- package/src/linked-device.ts +0 -52
- package/src/mcp-handler.ts +0 -200
- package/src/mcp-tools.ts +0 -839
- package/src/nats-client.ts +0 -19
- package/src/network.ts +0 -96
- package/src/notification-store.ts +0 -30
- package/src/pending-requests.ts +0 -73
- package/src/platform/index.ts +0 -20
- package/src/platform/linux.ts +0 -296
- package/src/platform/macos.ts +0 -329
- package/src/platform/platform.ts +0 -31
- package/src/platform/windows.ts +0 -299
- package/src/rpc-handler.ts +0 -691
- package/src/sms-store.ts +0 -28
- package/src/spawn-command.ts +0 -123
- package/src/task.ts +0 -343
- package/src/transports/http-transport.ts +0 -478
- package/src/transports/nats-transport.ts +0 -76
- package/src/types.ts +0 -89
- package/src/update-checker.ts +0 -40
- package/test/agent-instructions.test.ts +0 -209
- package/test/agent-output-parsing.test.ts +0 -74
- package/test/linux-cron.test.ts +0 -41
- package/test/macos-plist.test.ts +0 -112
- package/test/notification-store.test.ts +0 -57
- package/test/pairing.test.ts +0 -35
- package/test/result-state.test.ts +0 -110
- package/test/task-parsing.test.ts +0 -82
- package/test/taskrun-messages.test.ts +0 -224
- package/test/tsconfig.json +0 -9
- package/test/windows-xml.test.ts +0 -89
- package/tsconfig.json +0 -19
package/src/nats-client.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { connect, jwtAuthenticator, type NatsConnection } from "nats";
|
|
2
|
-
import type { HostConfig } from "./types.js";
|
|
3
|
-
|
|
4
|
-
export async function connectNats(config: HostConfig): Promise<NatsConnection> {
|
|
5
|
-
if (!config.natsJwt || !config.natsNkeySeed) {
|
|
6
|
-
throw new Error("NATS JWT credentials not configured. Re-run palmier init.");
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const nc = await connect({
|
|
10
|
-
servers: config.natsUrl,
|
|
11
|
-
authenticator: jwtAuthenticator(
|
|
12
|
-
config.natsJwt,
|
|
13
|
-
new TextEncoder().encode(config.natsNkeySeed),
|
|
14
|
-
),
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
// Do not log — it would pollute stdout for the MCP server.
|
|
18
|
-
return nc;
|
|
19
|
-
}
|
package/src/network.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import * as os from "node:os";
|
|
2
|
-
import * as dgram from "node:dgram";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Resolve the name of the network interface used for the IPv4 default route.
|
|
6
|
-
* Falls back to the first non-internal IPv4 interface when the gateway lookup
|
|
7
|
-
* fails — `default-gateway` shells out to `wmic` on Windows, which was removed
|
|
8
|
-
* in Windows 11 24H2.
|
|
9
|
-
*/
|
|
10
|
-
function findInterfaceByIp(ip: string): string | null {
|
|
11
|
-
for (const [name, addrs] of Object.entries(os.networkInterfaces())) {
|
|
12
|
-
for (const addr of addrs ?? []) {
|
|
13
|
-
if (addr.family === "IPv4" && addr.address === ip) return name;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
return null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Ask the kernel which local IPv4 would route to an external address. No packet is sent. */
|
|
20
|
-
function probeOutboundIp(): Promise<string | null> {
|
|
21
|
-
return new Promise((resolve) => {
|
|
22
|
-
const sock = dgram.createSocket("udp4");
|
|
23
|
-
const cleanup = (ip: string | null) => { try { sock.close(); } catch { /* ignore */ } resolve(ip); };
|
|
24
|
-
sock.on("error", () => cleanup(null));
|
|
25
|
-
try {
|
|
26
|
-
sock.connect(80, "8.8.8.8", () => {
|
|
27
|
-
const addr = sock.address();
|
|
28
|
-
cleanup(addr.address && addr.address !== "0.0.0.0" ? addr.address : null);
|
|
29
|
-
});
|
|
30
|
-
} catch {
|
|
31
|
-
cleanup(null);
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
type IpClass = 0 | 1 | 2 | 3;
|
|
37
|
-
|
|
38
|
-
/** Lower score = more preferred. 0=192.168, 1=10.x, 2=172.16-31, 3=everything else. */
|
|
39
|
-
function ipClass(ip: string): IpClass {
|
|
40
|
-
const parts = ip.split(".").map(Number);
|
|
41
|
-
if (parts.length !== 4 || parts.some((p) => Number.isNaN(p))) return 3;
|
|
42
|
-
const [a, b] = parts;
|
|
43
|
-
if (a === 192 && b === 168) return 0;
|
|
44
|
-
if (a === 10) return 1;
|
|
45
|
-
if (a === 172 && b >= 16 && b <= 31) return 2;
|
|
46
|
-
return 3;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Names that commonly belong to virtual/VPN adapters we'd rather skip. */
|
|
50
|
-
const VIRTUAL_NAME_PATTERNS = [
|
|
51
|
-
"vethernet", "virtualbox", "vmware", "hyper-v", "docker", "bridge",
|
|
52
|
-
"tailscale", "wireguard", "meta", "vpn", "tun", "tap", "loopback",
|
|
53
|
-
"wsl", "utun",
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
function isVirtualName(name: string): boolean {
|
|
57
|
-
const lower = name.toLowerCase();
|
|
58
|
-
return VIRTUAL_NAME_PATTERNS.some((p) => lower.includes(p));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function detectDefaultInterface(): Promise<string | null> {
|
|
62
|
-
const probedIp = await probeOutboundIp();
|
|
63
|
-
if (probedIp) {
|
|
64
|
-
const name = findInterfaceByIp(probedIp);
|
|
65
|
-
if (name && !isVirtualName(name)) return name;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
type Candidate = { name: string; klass: IpClass; virtual: boolean };
|
|
69
|
-
const candidates: Candidate[] = [];
|
|
70
|
-
for (const [name, addrs] of Object.entries(os.networkInterfaces())) {
|
|
71
|
-
for (const addr of addrs ?? []) {
|
|
72
|
-
if (addr.family !== "IPv4" || addr.internal) continue;
|
|
73
|
-
candidates.push({ name, klass: ipClass(addr.address), virtual: isVirtualName(name) });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
candidates.sort((a, b) => {
|
|
77
|
-
if (a.virtual !== b.virtual) return a.virtual ? 1 : -1;
|
|
78
|
-
return a.klass - b.klass;
|
|
79
|
-
});
|
|
80
|
-
return candidates[0]?.name ?? null;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function getInterfaceIpv4(interfaceName: string): string | null {
|
|
84
|
-
const addrs = os.networkInterfaces()[interfaceName];
|
|
85
|
-
if (!addrs) return null;
|
|
86
|
-
for (const addr of addrs) {
|
|
87
|
-
if (addr.family === "IPv4" && !addr.internal) return addr.address;
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function buildLanUrl(port: number, interfaceName: string | undefined): string | null {
|
|
93
|
-
if (!interfaceName) return null;
|
|
94
|
-
const ip = getInterfaceIpv4(interfaceName);
|
|
95
|
-
return ip ? `http://${ip}:${port}` : null;
|
|
96
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
export interface DeviceNotification {
|
|
2
|
-
id: string;
|
|
3
|
-
packageName: string;
|
|
4
|
-
appName: string;
|
|
5
|
-
title: string;
|
|
6
|
-
text: string;
|
|
7
|
-
timestamp: number;
|
|
8
|
-
receivedAt: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const MAX_NOTIFICATIONS = 50;
|
|
12
|
-
const notifications: DeviceNotification[] = [];
|
|
13
|
-
const listeners = new Set<() => void>();
|
|
14
|
-
|
|
15
|
-
export function addNotification(n: DeviceNotification): void {
|
|
16
|
-
notifications.push(n);
|
|
17
|
-
if (notifications.length > MAX_NOTIFICATIONS) {
|
|
18
|
-
notifications.shift();
|
|
19
|
-
}
|
|
20
|
-
for (const cb of listeners) cb();
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function getNotifications(): DeviceNotification[] {
|
|
24
|
-
return [...notifications];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function onNotificationsChanged(cb: () => void): () => void {
|
|
28
|
-
listeners.add(cb);
|
|
29
|
-
return () => { listeners.delete(cb); };
|
|
30
|
-
}
|
package/src/pending-requests.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type { RequiredPermission } from "./types.js";
|
|
2
|
-
|
|
3
|
-
export interface PendingRequestMeta {
|
|
4
|
-
/** Doubles as task_id for permission-type entries (the key the task uses). */
|
|
5
|
-
session_id?: string;
|
|
6
|
-
/** Human-readable label for whoever opened the prompt — agent name for
|
|
7
|
-
* confirm/input, task name for permission. */
|
|
8
|
-
session_name?: string;
|
|
9
|
-
description?: string;
|
|
10
|
-
input_questions?: string[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface PendingRequest {
|
|
14
|
-
type: "confirmation" | "permission" | "input";
|
|
15
|
-
resolve: (value: string[]) => void;
|
|
16
|
-
/** Permission list (for 'permission') or input descriptions (for 'input'). */
|
|
17
|
-
params?: RequiredPermission[] | string[];
|
|
18
|
-
/** Display context for PWAs that connect while this request is already open. */
|
|
19
|
-
meta?: PendingRequestMeta;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const pending = new Map<string, PendingRequest>();
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Key is sessionId for confirmation/input, taskId for permission. Only one
|
|
26
|
-
* pending request per key at a time. `meta` is surfaced via host.info so a
|
|
27
|
-
* freshly-connected PWA can render the modal without replaying events.
|
|
28
|
-
*/
|
|
29
|
-
export function registerPending(
|
|
30
|
-
key: string,
|
|
31
|
-
type: PendingRequest["type"],
|
|
32
|
-
params?: PendingRequest["params"],
|
|
33
|
-
meta?: PendingRequestMeta,
|
|
34
|
-
): Promise<string[]> {
|
|
35
|
-
if (pending.has(key)) {
|
|
36
|
-
return Promise.reject(new Error(`Key ${key} already has a pending request`));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return new Promise<string[]>((resolve) => {
|
|
40
|
-
pending.set(key, { type, resolve, params, meta });
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function resolvePending(key: string, value: string[]): boolean {
|
|
45
|
-
const entry = pending.get(key);
|
|
46
|
-
if (!entry) return false;
|
|
47
|
-
pending.delete(key);
|
|
48
|
-
entry.resolve(value);
|
|
49
|
-
return true;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function getPending(key: string): PendingRequest | undefined {
|
|
53
|
-
return pending.get(key);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function removePending(key: string): void {
|
|
57
|
-
pending.delete(key);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Pending requests stripped of the unserializable `resolve` callback. */
|
|
61
|
-
export function listPending(): Array<{
|
|
62
|
-
key: string;
|
|
63
|
-
type: PendingRequest["type"];
|
|
64
|
-
params?: PendingRequest["params"];
|
|
65
|
-
meta?: PendingRequestMeta;
|
|
66
|
-
}> {
|
|
67
|
-
return [...pending.entries()].map(([key, entry]) => ({
|
|
68
|
-
key,
|
|
69
|
-
type: entry.type,
|
|
70
|
-
params: entry.params,
|
|
71
|
-
meta: entry.meta,
|
|
72
|
-
}));
|
|
73
|
-
}
|
package/src/platform/index.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { PlatformService } from "./platform.js";
|
|
2
|
-
import { LinuxPlatform } from "./linux.js";
|
|
3
|
-
import { WindowsPlatform } from "./windows.js";
|
|
4
|
-
import { MacOsPlatform } from "./macos.js";
|
|
5
|
-
|
|
6
|
-
/** Windows needs an explicit shell for execSync to resolve .cmd shims. */
|
|
7
|
-
export const SHELL: string | undefined = process.platform === "win32" ? "cmd.exe" : undefined;
|
|
8
|
-
|
|
9
|
-
let _instance: PlatformService | undefined;
|
|
10
|
-
|
|
11
|
-
export function getPlatform(): PlatformService {
|
|
12
|
-
if (!_instance) {
|
|
13
|
-
if (process.platform === "win32") _instance = new WindowsPlatform();
|
|
14
|
-
else if (process.platform === "darwin") _instance = new MacOsPlatform();
|
|
15
|
-
else _instance = new LinuxPlatform();
|
|
16
|
-
}
|
|
17
|
-
return _instance;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type { PlatformService } from "./platform.js";
|
package/src/platform/linux.ts
DELETED
|
@@ -1,296 +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 { loadConfig } from "../config.js";
|
|
9
|
-
import { getTaskDir, readTaskStatus } from "../task.js";
|
|
10
|
-
|
|
11
|
-
const execAsync = promisify(exec);
|
|
12
|
-
|
|
13
|
-
const UNIT_DIR = path.join(homedir(), ".config", "systemd", "user");
|
|
14
|
-
const PATH_FILE = path.join(homedir(), ".config", "palmier", "user-path");
|
|
15
|
-
|
|
16
|
-
function getTimerName(taskId: string): string {
|
|
17
|
-
return `palmier-task-${taskId}.timer`;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function getServiceName(taskId: string): string {
|
|
21
|
-
return `palmier-task-${taskId}.service`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Only the 4 cron patterns the PWA UI produces are supported:
|
|
26
|
-
* hourly "0 * * * *", daily "MM HH * * *", weekly "MM HH * * D", monthly "MM HH D * *".
|
|
27
|
-
* Arbitrary expressions (ranges, lists, sub-hour steps) are not handled.
|
|
28
|
-
*/
|
|
29
|
-
export function cronToOnCalendar(cron: string): string {
|
|
30
|
-
const parts = cron.trim().split(/\s+/);
|
|
31
|
-
if (parts.length !== 5) {
|
|
32
|
-
throw new Error(`Invalid cron expression (expected 5 fields): ${cron}`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const [minute, hour, dayOfMonth, , dayOfWeek] = parts;
|
|
36
|
-
|
|
37
|
-
const dowMap: Record<string, string> = {
|
|
38
|
-
"0": "Sun", "1": "Mon", "2": "Tue", "3": "Wed",
|
|
39
|
-
"4": "Thu", "5": "Fri", "6": "Sat", "7": "Sun",
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const monthPart = "*";
|
|
43
|
-
const dayPart = dayOfMonth === "*" ? "*" : dayOfMonth.padStart(2, "0");
|
|
44
|
-
const hourPart = hour === "*" ? "*" : hour.padStart(2, "0");
|
|
45
|
-
const minutePart = minute === "*" ? "*" : minute.padStart(2, "0");
|
|
46
|
-
|
|
47
|
-
if (dayOfWeek !== "*") {
|
|
48
|
-
const dow = dowMap[dayOfWeek] ?? dayOfWeek;
|
|
49
|
-
return `${dow} *-${monthPart}-${dayPart} ${hourPart}:${minutePart}:00`;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return `*-${monthPart}-${dayPart} ${hourPart}:${minutePart}:00`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function daemonReload(): void {
|
|
56
|
-
try {
|
|
57
|
-
execSync("systemctl --user daemon-reload", { encoding: "utf-8" });
|
|
58
|
-
} catch (err: unknown) {
|
|
59
|
-
const e = err as { stderr?: string };
|
|
60
|
-
console.error(`daemon-reload failed: ${e.stderr || err}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export class LinuxPlatform implements PlatformService {
|
|
65
|
-
installDaemon(config: HostConfig): void {
|
|
66
|
-
fs.mkdirSync(UNIT_DIR, { recursive: true });
|
|
67
|
-
|
|
68
|
-
const palmierBin = process.argv[1] || "palmier";
|
|
69
|
-
// Save the user's shell PATH so restartDaemon can reuse it later — under
|
|
70
|
-
// systemd the daemon itself runs with a limited PATH.
|
|
71
|
-
const userPath = process.env.PATH || "/usr/local/bin:/usr/bin:/bin";
|
|
72
|
-
fs.mkdirSync(path.dirname(PATH_FILE), { recursive: true });
|
|
73
|
-
fs.writeFileSync(PATH_FILE, userPath, "utf-8");
|
|
74
|
-
|
|
75
|
-
const serviceContent = `[Unit]
|
|
76
|
-
Description=Palmier Host
|
|
77
|
-
After=network-online.target
|
|
78
|
-
Wants=network-online.target
|
|
79
|
-
|
|
80
|
-
[Service]
|
|
81
|
-
Type=simple
|
|
82
|
-
ExecStart=${palmierBin} serve
|
|
83
|
-
WorkingDirectory=${config.projectRoot}
|
|
84
|
-
Restart=on-failure
|
|
85
|
-
RestartSec=5
|
|
86
|
-
Environment=PATH=${userPath}
|
|
87
|
-
|
|
88
|
-
[Install]
|
|
89
|
-
WantedBy=default.target
|
|
90
|
-
`;
|
|
91
|
-
|
|
92
|
-
const servicePath = path.join(UNIT_DIR, "palmier.service");
|
|
93
|
-
fs.writeFileSync(servicePath, serviceContent, "utf-8");
|
|
94
|
-
console.log("Systemd service installed at:", servicePath);
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
execSync("systemctl --user daemon-reload", { stdio: "inherit" });
|
|
98
|
-
execSync("systemctl --user enable palmier.service", { stdio: "inherit" });
|
|
99
|
-
execSync("systemctl --user restart palmier.service", { stdio: "inherit" });
|
|
100
|
-
console.log("Palmier host service enabled and started.");
|
|
101
|
-
} catch (err) {
|
|
102
|
-
console.error(`Warning: failed to enable systemd service: ${err}`);
|
|
103
|
-
console.error("You may need to start it manually: systemctl --user enable --now palmier.service");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Lingering lets the service run without an active login session.
|
|
107
|
-
try {
|
|
108
|
-
execSync(`loginctl enable-linger ${process.env.USER || ""}`, { stdio: "inherit" });
|
|
109
|
-
console.log("Login lingering enabled.");
|
|
110
|
-
} catch (err) {
|
|
111
|
-
console.error(`Warning: failed to enable linger: ${err}`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
console.log("\nHost initialization complete!");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
uninstallDaemon(): void {
|
|
118
|
-
try {
|
|
119
|
-
execSync("systemctl --user stop palmier.service 2>/dev/null", { stdio: "pipe" });
|
|
120
|
-
execSync("systemctl --user disable palmier.service 2>/dev/null", { stdio: "pipe" });
|
|
121
|
-
} catch { /* service may not exist */ }
|
|
122
|
-
|
|
123
|
-
const servicePath = path.join(UNIT_DIR, "palmier.service");
|
|
124
|
-
try { fs.unlinkSync(servicePath); } catch { /* ignore */ }
|
|
125
|
-
|
|
126
|
-
try {
|
|
127
|
-
const files = fs.readdirSync(UNIT_DIR).filter((f) => f.startsWith("palmier-task-"));
|
|
128
|
-
for (const f of files) {
|
|
129
|
-
const unit = f.replace(/\.(timer|service)$/, "");
|
|
130
|
-
try { execSync(`systemctl --user stop ${f} 2>/dev/null`, { stdio: "pipe" }); } catch { /* ignore */ }
|
|
131
|
-
try { execSync(`systemctl --user disable ${f} 2>/dev/null`, { stdio: "pipe" }); } catch { /* ignore */ }
|
|
132
|
-
try { fs.unlinkSync(path.join(UNIT_DIR, f)); } catch { /* ignore */ }
|
|
133
|
-
}
|
|
134
|
-
} catch { /* ignore */ }
|
|
135
|
-
|
|
136
|
-
try { execSync("systemctl --user daemon-reload", { stdio: "pipe" }); } catch { /* ignore */ }
|
|
137
|
-
|
|
138
|
-
console.log("Palmier daemon and tasks uninstalled.");
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async restartDaemon(): Promise<void> {
|
|
142
|
-
// From a TTY, snapshot the current PATH; from the daemon (auto-update),
|
|
143
|
-
// reuse whatever was last saved.
|
|
144
|
-
if (process.stdin.isTTY) {
|
|
145
|
-
fs.mkdirSync(path.dirname(PATH_FILE), { recursive: true });
|
|
146
|
-
fs.writeFileSync(PATH_FILE, process.env.PATH || "", "utf-8");
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const servicePath = path.join(UNIT_DIR, "palmier.service");
|
|
150
|
-
if (fs.existsSync(servicePath) && fs.existsSync(PATH_FILE)) {
|
|
151
|
-
const userPath = fs.readFileSync(PATH_FILE, "utf-8").trim();
|
|
152
|
-
if (userPath) {
|
|
153
|
-
const content = fs.readFileSync(servicePath, "utf-8");
|
|
154
|
-
const updated = content.replace(
|
|
155
|
-
/^Environment=PATH=.*/m,
|
|
156
|
-
`Environment=PATH=${userPath}`,
|
|
157
|
-
);
|
|
158
|
-
if (updated !== content) {
|
|
159
|
-
fs.writeFileSync(servicePath, updated, "utf-8");
|
|
160
|
-
execSync("systemctl --user daemon-reload", { encoding: "utf-8" });
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
execSync("systemctl --user restart palmier.service", { stdio: "inherit" });
|
|
165
|
-
console.log("Palmier daemon restarted.");
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
installTaskTimer(config: HostConfig, task: ParsedTask): void {
|
|
169
|
-
fs.mkdirSync(UNIT_DIR, { recursive: true });
|
|
170
|
-
|
|
171
|
-
const taskId = task.frontmatter.id;
|
|
172
|
-
const serviceName = getServiceName(taskId);
|
|
173
|
-
const timerName = getTimerName(taskId);
|
|
174
|
-
const palmierBin = process.argv[1] || "palmier";
|
|
175
|
-
|
|
176
|
-
const serviceContent = `[Unit]
|
|
177
|
-
Description=Palmier Task: ${taskId}
|
|
178
|
-
|
|
179
|
-
[Service]
|
|
180
|
-
Type=oneshot
|
|
181
|
-
TimeoutStartSec=infinity
|
|
182
|
-
ExecStart=${palmierBin} run ${taskId}
|
|
183
|
-
WorkingDirectory=${config.projectRoot}
|
|
184
|
-
Environment=PATH=${process.env.PATH || "/usr/local/bin:/usr/bin:/bin"}
|
|
185
|
-
`;
|
|
186
|
-
|
|
187
|
-
fs.writeFileSync(path.join(UNIT_DIR, serviceName), serviceContent, "utf-8");
|
|
188
|
-
|
|
189
|
-
// Tear down any previously installed timer unit so on-demand tasks don't
|
|
190
|
-
// keep firing on the old schedule. Service unit stays so startTask works.
|
|
191
|
-
const timerPath = path.join(UNIT_DIR, timerName);
|
|
192
|
-
if (fs.existsSync(timerPath)) {
|
|
193
|
-
try { execSync(`systemctl --user stop ${timerName}`, { encoding: "utf-8" }); } catch { /* not running */ }
|
|
194
|
-
try { execSync(`systemctl --user disable ${timerName}`, { encoding: "utf-8" }); } catch { /* not enabled */ }
|
|
195
|
-
fs.unlinkSync(timerPath);
|
|
196
|
-
}
|
|
197
|
-
daemonReload();
|
|
198
|
-
|
|
199
|
-
if (!task.frontmatter.schedule_enabled) return;
|
|
200
|
-
const scheduleType = task.frontmatter.schedule_type;
|
|
201
|
-
const scheduleValues = task.frontmatter.schedule_values;
|
|
202
|
-
if (!scheduleType || !scheduleValues?.length) return;
|
|
203
|
-
const onCalendarLines: string[] = [];
|
|
204
|
-
for (const value of scheduleValues) {
|
|
205
|
-
if (scheduleType === "crons") {
|
|
206
|
-
onCalendarLines.push(`OnCalendar=${cronToOnCalendar(value)}`);
|
|
207
|
-
} else if (scheduleType === "specific_times") {
|
|
208
|
-
onCalendarLines.push(`OnActiveSec=${value}`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (onCalendarLines.length > 0) {
|
|
213
|
-
const timerContent = `[Unit]
|
|
214
|
-
Description=Timer for Palmier Task: ${taskId}
|
|
215
|
-
|
|
216
|
-
[Timer]
|
|
217
|
-
${onCalendarLines.join("\n")}
|
|
218
|
-
Persistent=true
|
|
219
|
-
|
|
220
|
-
[Install]
|
|
221
|
-
WantedBy=timers.target
|
|
222
|
-
`;
|
|
223
|
-
fs.writeFileSync(path.join(UNIT_DIR, timerName), timerContent, "utf-8");
|
|
224
|
-
daemonReload();
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
execSync(`systemctl --user enable --now ${timerName}`, { encoding: "utf-8" });
|
|
228
|
-
} catch (err: unknown) {
|
|
229
|
-
const e = err as { stderr?: string };
|
|
230
|
-
console.error(`Failed to enable timer ${timerName}: ${e.stderr || err}`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
removeTaskTimer(taskId: string): void {
|
|
236
|
-
const timerName = getTimerName(taskId);
|
|
237
|
-
const serviceName = getServiceName(taskId);
|
|
238
|
-
const timerPath = path.join(UNIT_DIR, timerName);
|
|
239
|
-
const servicePath = path.join(UNIT_DIR, serviceName);
|
|
240
|
-
|
|
241
|
-
if (fs.existsSync(timerPath)) {
|
|
242
|
-
try { execSync(`systemctl --user stop ${timerName}`, { encoding: "utf-8" }); } catch { /* timer might not be running */ }
|
|
243
|
-
try { execSync(`systemctl --user disable ${timerName}`, { encoding: "utf-8" }); } catch { /* timer might not be enabled */ }
|
|
244
|
-
fs.unlinkSync(timerPath);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (fs.existsSync(servicePath)) fs.unlinkSync(servicePath);
|
|
248
|
-
daemonReload();
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async startTask(taskId: string): Promise<void> {
|
|
252
|
-
const serviceName = getServiceName(taskId);
|
|
253
|
-
await execAsync(`systemctl --user start --no-block ${serviceName}`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async stopTask(taskId: string): Promise<void> {
|
|
257
|
-
const serviceName = getServiceName(taskId);
|
|
258
|
-
await execAsync(`systemctl --user stop ${serviceName}`);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
isTaskRunning(taskId: string): boolean {
|
|
262
|
-
const serviceName = getServiceName(taskId);
|
|
263
|
-
try {
|
|
264
|
-
const out = execSync(
|
|
265
|
-
`systemctl --user show -p ActiveState --value ${serviceName}`,
|
|
266
|
-
{ encoding: "utf-8" },
|
|
267
|
-
);
|
|
268
|
-
const state = out.trim();
|
|
269
|
-
if (state === "active" || state === "activating") return true;
|
|
270
|
-
} catch { /* service may not exist */ }
|
|
271
|
-
|
|
272
|
-
// Follow-up runs are spawned directly, so check PID too.
|
|
273
|
-
try {
|
|
274
|
-
const taskDir = getTaskDir(loadConfig().projectRoot, taskId);
|
|
275
|
-
const status = readTaskStatus(taskDir);
|
|
276
|
-
if (status?.pid) {
|
|
277
|
-
process.kill(status.pid, 0); // signal 0 = check if process exists
|
|
278
|
-
return true;
|
|
279
|
-
}
|
|
280
|
-
} catch { /* process not running or config unavailable */ }
|
|
281
|
-
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
getGuiEnv(): Record<string, string> {
|
|
286
|
-
const uid = process.getuid?.();
|
|
287
|
-
const runtimeDir =
|
|
288
|
-
process.env.XDG_RUNTIME_DIR ||
|
|
289
|
-
(uid !== undefined ? `/run/user/${uid}` : "");
|
|
290
|
-
|
|
291
|
-
return {
|
|
292
|
-
DISPLAY: ":0",
|
|
293
|
-
...(runtimeDir ? { XDG_RUNTIME_DIR: runtimeDir } : {}),
|
|
294
|
-
};
|
|
295
|
-
}
|
|
296
|
-
}
|