@walldock/agent 0.1.0
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/dist/api.d.ts +54 -0
- package/dist/api.js +110 -0
- package/dist/auth.d.ts +12 -0
- package/dist/auth.js +144 -0
- package/dist/cache.d.ts +3 -0
- package/dist/cache.js +119 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +186 -0
- package/dist/pid.d.ts +6 -0
- package/dist/pid.js +77 -0
- package/dist/screens/index.d.ts +12 -0
- package/dist/screens/index.js +61 -0
- package/dist/screens/linux.d.ts +2 -0
- package/dist/screens/linux.js +118 -0
- package/dist/screens/macos.d.ts +2 -0
- package/dist/screens/macos.js +53 -0
- package/dist/screens/stable-id.d.ts +10 -0
- package/dist/screens/stable-id.js +70 -0
- package/dist/screens/windows.d.ts +2 -0
- package/dist/screens/windows.js +74 -0
- package/dist/sse.d.ts +8 -0
- package/dist/sse.js +66 -0
- package/dist/startup.d.ts +5 -0
- package/dist/startup.js +208 -0
- package/dist/sync.d.ts +32 -0
- package/dist/sync.js +144 -0
- package/dist/token-storage.d.ts +10 -0
- package/dist/token-storage.js +142 -0
- package/dist/wallpaper/index.d.ts +2 -0
- package/dist/wallpaper/index.js +51 -0
- package/dist/wallpaper/linux.d.ts +1 -0
- package/dist/wallpaper/linux.js +152 -0
- package/dist/wallpaper/macos.d.ts +6 -0
- package/dist/wallpaper/macos.js +19 -0
- package/dist/wallpaper/windows.d.ts +5 -0
- package/dist/wallpaper/windows.js +58 -0
- package/package.json +28 -0
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface DeviceScreen {
|
|
2
|
+
stableId: string;
|
|
3
|
+
label: string;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
scaleFactor: number;
|
|
7
|
+
positionX: number;
|
|
8
|
+
positionY: number;
|
|
9
|
+
isPrimary: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface StartDevicePairingPayload {
|
|
12
|
+
pairingCode: string;
|
|
13
|
+
pairingSessionId: string;
|
|
14
|
+
expiresAt: string;
|
|
15
|
+
}
|
|
16
|
+
export interface DevicePairingStatusPayload {
|
|
17
|
+
status: 'pending' | 'approved' | 'rejected' | 'expired';
|
|
18
|
+
deviceToken: string | null;
|
|
19
|
+
deviceId: string | null;
|
|
20
|
+
deviceName: string | null;
|
|
21
|
+
}
|
|
22
|
+
export interface ValidateDeviceTokenPayload {
|
|
23
|
+
valid: boolean;
|
|
24
|
+
deviceId: string | null;
|
|
25
|
+
deviceName: string | null;
|
|
26
|
+
}
|
|
27
|
+
export interface DeviceScreenAssignmentPayload {
|
|
28
|
+
stableId: string;
|
|
29
|
+
wallpaperId: string | null;
|
|
30
|
+
imageUrl: string | null;
|
|
31
|
+
mimeType: string | null;
|
|
32
|
+
assignedAt: string | null;
|
|
33
|
+
appliedAt: string | null;
|
|
34
|
+
}
|
|
35
|
+
export interface DeviceCommandPayload {
|
|
36
|
+
id: string;
|
|
37
|
+
type: string;
|
|
38
|
+
payload: unknown;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
}
|
|
41
|
+
export declare class AgentApi {
|
|
42
|
+
readonly endpoint: string;
|
|
43
|
+
constructor(endpoint?: string);
|
|
44
|
+
startPairing(): Promise<StartDevicePairingPayload>;
|
|
45
|
+
getPairingStatus(pairingSessionId: string): Promise<DevicePairingStatusPayload>;
|
|
46
|
+
validateToken(token: string): Promise<ValidateDeviceTokenPayload>;
|
|
47
|
+
unlinkDevice(token: string): Promise<void>;
|
|
48
|
+
getDeviceCommands(token: string, deviceId: string, signal?: AbortSignal): Promise<DeviceCommandPayload[]>;
|
|
49
|
+
reportDeviceScreens(token: string, deviceId: string, screens: DeviceScreen[], signal?: AbortSignal): Promise<void>;
|
|
50
|
+
getDeviceScreenAssignments(token: string, deviceId: string, signal?: AbortSignal): Promise<DeviceScreenAssignmentPayload[]>;
|
|
51
|
+
acknowledgeApplied(token: string, deviceId: string, stableId: string, wallpaperId: string): Promise<void>;
|
|
52
|
+
absoluteImageUrl(relativeOrAbsolute: string): string;
|
|
53
|
+
private request;
|
|
54
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AgentApi = void 0;
|
|
4
|
+
const PROD_GRAPHQL_ENDPOINT = 'https://walldock.com/api/graphql';
|
|
5
|
+
const START_DEVICE_PAIRING = `
|
|
6
|
+
mutation StartDevicePairing {
|
|
7
|
+
startDevicePairing { pairingCode pairingSessionId expiresAt }
|
|
8
|
+
}`;
|
|
9
|
+
const DEVICE_PAIRING_STATUS = `
|
|
10
|
+
query DevicePairingStatus($pairingSessionId: String!) {
|
|
11
|
+
devicePairingStatus(pairingSessionId: $pairingSessionId) {
|
|
12
|
+
status deviceToken deviceId deviceName
|
|
13
|
+
}
|
|
14
|
+
}`;
|
|
15
|
+
const VALIDATE_DEVICE_TOKEN = `
|
|
16
|
+
query ValidateDeviceToken {
|
|
17
|
+
validateDeviceToken { valid deviceId deviceName }
|
|
18
|
+
}`;
|
|
19
|
+
const UNLINK_DEVICE = `
|
|
20
|
+
mutation UnlinkDevice {
|
|
21
|
+
unlinkDevice { success }
|
|
22
|
+
}`;
|
|
23
|
+
const DEVICE_COMMANDS = `
|
|
24
|
+
query DeviceCommands($deviceId: String!) {
|
|
25
|
+
deviceCommands(deviceId: $deviceId) { id type payload createdAt }
|
|
26
|
+
}`;
|
|
27
|
+
const REPORT_DEVICE_SCREENS = `
|
|
28
|
+
mutation ReportDeviceScreens($deviceId: String!, $screens: [DeviceScreenInput!]!) {
|
|
29
|
+
reportDeviceScreens(deviceId: $deviceId, screens: $screens) { success screenCount }
|
|
30
|
+
}`;
|
|
31
|
+
const DEVICE_SCREEN_ASSIGNMENTS = `
|
|
32
|
+
query DeviceScreenAssignments($deviceId: String!) {
|
|
33
|
+
deviceScreenAssignments(deviceId: $deviceId) {
|
|
34
|
+
stableId wallpaperId imageUrl mimeType assignedAt appliedAt
|
|
35
|
+
}
|
|
36
|
+
}`;
|
|
37
|
+
const ACKNOWLEDGE_APPLIED = `
|
|
38
|
+
mutation AcknowledgeScreenWallpaperApplied($deviceId: String!, $stableId: String!, $wallpaperId: String!) {
|
|
39
|
+
acknowledgeScreenWallpaperApplied(deviceId: $deviceId, stableId: $stableId, wallpaperId: $wallpaperId)
|
|
40
|
+
}`;
|
|
41
|
+
class AgentApi {
|
|
42
|
+
endpoint;
|
|
43
|
+
constructor(endpoint = PROD_GRAPHQL_ENDPOINT) {
|
|
44
|
+
this.endpoint = endpoint;
|
|
45
|
+
}
|
|
46
|
+
async startPairing() {
|
|
47
|
+
const r = await this.request(START_DEVICE_PAIRING);
|
|
48
|
+
return r.startDevicePairing;
|
|
49
|
+
}
|
|
50
|
+
async getPairingStatus(pairingSessionId) {
|
|
51
|
+
const r = await this.request(DEVICE_PAIRING_STATUS, { pairingSessionId });
|
|
52
|
+
return r.devicePairingStatus;
|
|
53
|
+
}
|
|
54
|
+
async validateToken(token) {
|
|
55
|
+
const r = await this.request(VALIDATE_DEVICE_TOKEN, undefined, token);
|
|
56
|
+
return r.validateDeviceToken;
|
|
57
|
+
}
|
|
58
|
+
async unlinkDevice(token) {
|
|
59
|
+
await this.request(UNLINK_DEVICE, undefined, token);
|
|
60
|
+
}
|
|
61
|
+
async getDeviceCommands(token, deviceId, signal) {
|
|
62
|
+
const r = await this.request(DEVICE_COMMANDS, { deviceId }, token, signal);
|
|
63
|
+
return r.deviceCommands;
|
|
64
|
+
}
|
|
65
|
+
async reportDeviceScreens(token, deviceId, screens, signal) {
|
|
66
|
+
await this.request(REPORT_DEVICE_SCREENS, { deviceId, screens }, token, signal);
|
|
67
|
+
}
|
|
68
|
+
async getDeviceScreenAssignments(token, deviceId, signal) {
|
|
69
|
+
const r = await this.request(DEVICE_SCREEN_ASSIGNMENTS, { deviceId }, token, signal);
|
|
70
|
+
return r.deviceScreenAssignments;
|
|
71
|
+
}
|
|
72
|
+
async acknowledgeApplied(token, deviceId, stableId, wallpaperId) {
|
|
73
|
+
await this.request(ACKNOWLEDGE_APPLIED, { deviceId, stableId, wallpaperId }, token);
|
|
74
|
+
}
|
|
75
|
+
absoluteImageUrl(relativeOrAbsolute) {
|
|
76
|
+
if (/^https?:\/\//i.test(relativeOrAbsolute))
|
|
77
|
+
return relativeOrAbsolute;
|
|
78
|
+
const origin = new URL(this.endpoint).origin;
|
|
79
|
+
return `${origin}${relativeOrAbsolute}`;
|
|
80
|
+
}
|
|
81
|
+
async request(query, variables, token, signal) {
|
|
82
|
+
const headers = { 'content-type': 'application/json' };
|
|
83
|
+
if (token)
|
|
84
|
+
headers['authorization'] = `Bearer ${token}`;
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const timeout = setTimeout(() => controller.abort(), 10_000);
|
|
87
|
+
if (signal)
|
|
88
|
+
signal.addEventListener('abort', () => controller.abort(), { once: true });
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch(this.endpoint, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers,
|
|
93
|
+
body: JSON.stringify({ query, variables }),
|
|
94
|
+
signal: controller.signal,
|
|
95
|
+
});
|
|
96
|
+
if (!res.ok)
|
|
97
|
+
throw new Error(`GraphQL request failed: HTTP ${res.status}`);
|
|
98
|
+
const payload = (await res.json());
|
|
99
|
+
if (payload.errors?.length)
|
|
100
|
+
throw new Error(payload.errors.map(e => e.message).join('; '));
|
|
101
|
+
if (!payload.data)
|
|
102
|
+
throw new Error('GraphQL response missing data');
|
|
103
|
+
return payload.data;
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
clearTimeout(timeout);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.AgentApi = AgentApi;
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AgentApi } from './api';
|
|
2
|
+
export interface PairingResult {
|
|
3
|
+
token: string;
|
|
4
|
+
deviceId: string;
|
|
5
|
+
deviceName: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function runPairingFlow(api: AgentApi): Promise<PairingResult>;
|
|
8
|
+
export declare function validateStoredToken(api: AgentApi): Promise<{
|
|
9
|
+
token: string;
|
|
10
|
+
deviceId: string;
|
|
11
|
+
deviceName: string;
|
|
12
|
+
} | null>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.runPairingFlow = runPairingFlow;
|
|
37
|
+
exports.validateStoredToken = validateStoredToken;
|
|
38
|
+
const promises_1 = require("node:readline/promises");
|
|
39
|
+
const node_child_process_1 = require("node:child_process");
|
|
40
|
+
const storage = __importStar(require("./token-storage"));
|
|
41
|
+
function openBrowser(url) {
|
|
42
|
+
const cmd = process.platform === 'darwin' ? 'open'
|
|
43
|
+
: process.platform === 'win32' ? 'start'
|
|
44
|
+
: 'xdg-open';
|
|
45
|
+
(0, node_child_process_1.execFile)(cmd, [url], () => undefined);
|
|
46
|
+
}
|
|
47
|
+
async function ask(question) {
|
|
48
|
+
const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
49
|
+
try {
|
|
50
|
+
return (await rl.question(question)).trim();
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
rl.close();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function spinner(text) {
|
|
57
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
58
|
+
let i = 0;
|
|
59
|
+
const timer = setInterval(() => {
|
|
60
|
+
process.stdout.write(`\r${frames[i++ % frames.length]} ${text} `);
|
|
61
|
+
}, 80);
|
|
62
|
+
return () => { clearInterval(timer); process.stdout.write('\r\x1b[K'); };
|
|
63
|
+
}
|
|
64
|
+
async function runPairingFlow(api) {
|
|
65
|
+
process.stdout.write('Requesting pairing session…\n');
|
|
66
|
+
let session;
|
|
67
|
+
try {
|
|
68
|
+
session = await api.startPairing();
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
throw new Error(`Could not reach Walldock backend at ${api.endpoint}: ${err}`);
|
|
72
|
+
}
|
|
73
|
+
const pairingUrl = `https://walldock.com/app/pair?pairingId=${encodeURIComponent(session.pairingSessionId)}`;
|
|
74
|
+
const expiresAt = new Date(session.expiresAt);
|
|
75
|
+
console.log('');
|
|
76
|
+
console.log('╔════════════════════════════════════════════════════════╗');
|
|
77
|
+
console.log(`║ Pairing code: ${session.pairingCode.padEnd(40)} ║`);
|
|
78
|
+
console.log('╠════════════════════════════════════════════════════════╣');
|
|
79
|
+
console.log(`║ ${pairingUrl.padEnd(54)} ║`);
|
|
80
|
+
console.log(`║ Expires: ${expiresAt.toLocaleTimeString().padEnd(45)} ║`);
|
|
81
|
+
console.log('╚════════════════════════════════════════════════════════╝');
|
|
82
|
+
console.log('');
|
|
83
|
+
const openAnswer = await ask('Open pairing page in browser? [y/N] ');
|
|
84
|
+
if (openAnswer.toLowerCase() === 'y') {
|
|
85
|
+
openBrowser(pairingUrl);
|
|
86
|
+
console.log('Browser opened. Approve the device in the Walldock web app.');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.log('Visit the URL above in your browser and approve this device.');
|
|
90
|
+
}
|
|
91
|
+
console.log('');
|
|
92
|
+
const stopSpinner = spinner('Waiting for approval…');
|
|
93
|
+
try {
|
|
94
|
+
return await pollForApproval(api, session.pairingSessionId, session.expiresAt);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
stopSpinner();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function pollForApproval(api, sessionId, expiresAt) {
|
|
101
|
+
const expiry = new Date(expiresAt).getTime();
|
|
102
|
+
for (;;) {
|
|
103
|
+
if (Date.now() > expiry)
|
|
104
|
+
throw new Error('Pairing session expired. Run the agent again to start a new session.');
|
|
105
|
+
await new Promise(r => setTimeout(r, 2_000));
|
|
106
|
+
let status;
|
|
107
|
+
try {
|
|
108
|
+
status = await api.getPairingStatus(sessionId);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
continue; // transient network error — keep polling
|
|
112
|
+
}
|
|
113
|
+
if (status.status === 'pending')
|
|
114
|
+
continue;
|
|
115
|
+
if (status.status === 'approved') {
|
|
116
|
+
if (!status.deviceToken || !status.deviceId || !status.deviceName) {
|
|
117
|
+
throw new Error('Approved pairing missing credentials.');
|
|
118
|
+
}
|
|
119
|
+
return { token: status.deviceToken, deviceId: status.deviceId, deviceName: status.deviceName };
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`Pairing ${status.status}. Run the agent again to try again.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function validateStoredToken(api) {
|
|
125
|
+
const token = await storage.getDeviceToken();
|
|
126
|
+
if (!token)
|
|
127
|
+
return null;
|
|
128
|
+
try {
|
|
129
|
+
const v = await api.validateToken(token);
|
|
130
|
+
if (!v.valid || !v.deviceId || !v.deviceName) {
|
|
131
|
+
await storage.deleteDeviceToken();
|
|
132
|
+
await storage.clearDeviceIdentity();
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return { token, deviceId: v.deviceId, deviceName: v.deviceName };
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// Network error — try cached identity for offline start
|
|
139
|
+
const cached = await storage.getDeviceIdentity();
|
|
140
|
+
if (cached)
|
|
141
|
+
return { token, ...cached };
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function cachedPath(wallpaperId: string, mimeType: string | null, imageUrl: string | null): string;
|
|
2
|
+
export declare function ensureWallpaper(wallpaperId: string, imageUrl: string, mimeType: string | null): Promise<string>;
|
|
3
|
+
export declare function pruneCache(keepWallpaperIds: string[]): Promise<void>;
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.cachedPath = cachedPath;
|
|
37
|
+
exports.ensureWallpaper = ensureWallpaper;
|
|
38
|
+
exports.pruneCache = pruneCache;
|
|
39
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const os = __importStar(require("node:os"));
|
|
42
|
+
function cacheDir() {
|
|
43
|
+
if (process.platform === 'win32') {
|
|
44
|
+
return path.join(process.env['LOCALAPPDATA'] ?? os.homedir(), 'walldock', 'wallpapers');
|
|
45
|
+
}
|
|
46
|
+
if (process.platform === 'darwin') {
|
|
47
|
+
return path.join(os.homedir(), 'Library', 'Caches', 'walldock', 'wallpapers');
|
|
48
|
+
}
|
|
49
|
+
return path.join(process.env['XDG_CACHE_HOME'] ?? path.join(os.homedir(), '.cache'), 'walldock', 'wallpapers');
|
|
50
|
+
}
|
|
51
|
+
const CACHE_DIR = cacheDir();
|
|
52
|
+
const EXTENSION_BY_MIME = {
|
|
53
|
+
'image/jpeg': 'jpg',
|
|
54
|
+
'image/jpg': 'jpg',
|
|
55
|
+
'image/png': 'png',
|
|
56
|
+
'image/webp': 'webp',
|
|
57
|
+
'image/gif': 'gif',
|
|
58
|
+
'image/heic': 'heic',
|
|
59
|
+
'image/heif': 'heif',
|
|
60
|
+
};
|
|
61
|
+
function guessExtension(mimeType, imageUrl) {
|
|
62
|
+
if (mimeType) {
|
|
63
|
+
const ext = EXTENSION_BY_MIME[mimeType.toLowerCase()];
|
|
64
|
+
if (ext)
|
|
65
|
+
return ext;
|
|
66
|
+
}
|
|
67
|
+
if (imageUrl) {
|
|
68
|
+
const m = /\.([a-zA-Z0-9]{2,5})(?:\?|$)/.exec(imageUrl);
|
|
69
|
+
if (m?.[1])
|
|
70
|
+
return m[1].toLowerCase();
|
|
71
|
+
}
|
|
72
|
+
return 'jpg';
|
|
73
|
+
}
|
|
74
|
+
function safeId(wallpaperId) {
|
|
75
|
+
return wallpaperId.replace(/[^a-zA-Z0-9\-_]/g, '_');
|
|
76
|
+
}
|
|
77
|
+
function cachedPath(wallpaperId, mimeType, imageUrl) {
|
|
78
|
+
const ext = guessExtension(mimeType, imageUrl);
|
|
79
|
+
return path.join(CACHE_DIR, `${safeId(wallpaperId)}.${ext}`);
|
|
80
|
+
}
|
|
81
|
+
async function download(url, destPath) {
|
|
82
|
+
const res = await fetch(url, { redirect: 'follow' });
|
|
83
|
+
if (!res.ok)
|
|
84
|
+
throw new Error(`HTTP ${res.status} downloading ${url}`);
|
|
85
|
+
const partPath = `${destPath}.part`;
|
|
86
|
+
await fs.writeFile(partPath, Buffer.from(await res.arrayBuffer()));
|
|
87
|
+
await fs.rename(partPath, destPath);
|
|
88
|
+
}
|
|
89
|
+
async function ensureWallpaper(wallpaperId, imageUrl, mimeType) {
|
|
90
|
+
await fs.mkdir(CACHE_DIR, { recursive: true });
|
|
91
|
+
const dest = cachedPath(wallpaperId, mimeType, imageUrl);
|
|
92
|
+
try {
|
|
93
|
+
await fs.access(dest);
|
|
94
|
+
return dest; // already cached
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// not cached yet — download
|
|
98
|
+
}
|
|
99
|
+
await download(imageUrl, dest);
|
|
100
|
+
return dest;
|
|
101
|
+
}
|
|
102
|
+
async function pruneCache(keepWallpaperIds) {
|
|
103
|
+
let entries;
|
|
104
|
+
try {
|
|
105
|
+
entries = await fs.readdir(CACHE_DIR);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const keepSet = new Set(keepWallpaperIds.map(safeId));
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
if (entry.endsWith('.part'))
|
|
113
|
+
continue;
|
|
114
|
+
const base = path.basename(entry, path.extname(entry));
|
|
115
|
+
if (!keepSet.has(base)) {
|
|
116
|
+
await fs.unlink(path.join(CACHE_DIR, entry)).catch(() => undefined);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
package/dist/main.d.ts
ADDED
package/dist/main.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const promises_1 = require("node:readline/promises");
|
|
38
|
+
const fs = __importStar(require("node:fs/promises"));
|
|
39
|
+
const path = __importStar(require("node:path"));
|
|
40
|
+
const os = __importStar(require("node:os"));
|
|
41
|
+
const api_1 = require("./api");
|
|
42
|
+
const auth_1 = require("./auth");
|
|
43
|
+
const sync_1 = require("./sync");
|
|
44
|
+
const storage = __importStar(require("./token-storage"));
|
|
45
|
+
const startup_1 = require("./startup");
|
|
46
|
+
const pid_1 = require("./pid");
|
|
47
|
+
// ─── Logging ─────────────────────────────────────────────────────────────────
|
|
48
|
+
const isDaemon = process.argv.includes('--daemon');
|
|
49
|
+
function logDir() {
|
|
50
|
+
if (process.platform === 'darwin')
|
|
51
|
+
return path.join(os.homedir(), 'Library', 'Logs');
|
|
52
|
+
if (process.platform === 'win32')
|
|
53
|
+
return path.join(process.env['APPDATA'] ?? os.homedir(), 'walldock');
|
|
54
|
+
return path.join(process.env['XDG_STATE_HOME'] ?? path.join(os.homedir(), '.local', 'state'), 'walldock');
|
|
55
|
+
}
|
|
56
|
+
let logStream = null;
|
|
57
|
+
async function openLog() {
|
|
58
|
+
const dir = logDir();
|
|
59
|
+
await fs.mkdir(dir, { recursive: true });
|
|
60
|
+
logStream = await fs.open(path.join(dir, 'walldock-agent.log'), 'a');
|
|
61
|
+
}
|
|
62
|
+
function log(msg) {
|
|
63
|
+
const line = `[${new Date().toISOString()}] ${msg}\n`;
|
|
64
|
+
if (isDaemon && logStream) {
|
|
65
|
+
logStream.write(line).catch(() => undefined);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
process.stdout.write(line);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
72
|
+
async function main() {
|
|
73
|
+
const args = process.argv.slice(2);
|
|
74
|
+
// --unlink: remove stored credentials and startup entry
|
|
75
|
+
if (args.includes('--unlink')) {
|
|
76
|
+
const token = await storage.getDeviceToken();
|
|
77
|
+
if (token) {
|
|
78
|
+
const api = new api_1.AgentApi();
|
|
79
|
+
await api.unlinkDevice(token).catch(() => undefined);
|
|
80
|
+
}
|
|
81
|
+
await storage.deleteDeviceToken();
|
|
82
|
+
await storage.clearDeviceIdentity();
|
|
83
|
+
await (0, startup_1.unregisterStartup)().catch(() => undefined);
|
|
84
|
+
console.log('Device unlinked and startup entry removed.');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// --status: print current link status
|
|
88
|
+
if (args.includes('--status')) {
|
|
89
|
+
const identity = await storage.getDeviceIdentity();
|
|
90
|
+
if (!identity) {
|
|
91
|
+
console.log('Status: not linked');
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
console.log(`Status: linked as "${identity.deviceName}" (${identity.deviceId})`);
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (isDaemon)
|
|
99
|
+
await openLog();
|
|
100
|
+
// Single-instance guard
|
|
101
|
+
try {
|
|
102
|
+
await (0, pid_1.acquireLock)();
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
console.error(`\n${err}`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
const api = new api_1.AgentApi(process.env['WALLDOCK_API_URL'] ?? undefined);
|
|
109
|
+
const sync = new sync_1.SyncService(api, {
|
|
110
|
+
onTick(status, lastSyncAt) {
|
|
111
|
+
if (status === 'error')
|
|
112
|
+
log('[sync] connection error');
|
|
113
|
+
else if (lastSyncAt)
|
|
114
|
+
log(`[sync] ok at ${lastSyncAt}`);
|
|
115
|
+
},
|
|
116
|
+
onLog: log,
|
|
117
|
+
});
|
|
118
|
+
// Graceful shutdown
|
|
119
|
+
let stopping = false;
|
|
120
|
+
async function shutdown() {
|
|
121
|
+
if (stopping)
|
|
122
|
+
return;
|
|
123
|
+
stopping = true;
|
|
124
|
+
sync.stop();
|
|
125
|
+
await (0, pid_1.releaseLock)();
|
|
126
|
+
log('Walldock Agent stopped.');
|
|
127
|
+
await logStream?.close().catch(() => undefined);
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
process.on('SIGINT', () => { void shutdown(); });
|
|
131
|
+
process.on('SIGTERM', () => { void shutdown(); });
|
|
132
|
+
// Try to resume from stored token
|
|
133
|
+
log('Walldock Agent starting…');
|
|
134
|
+
const existing = await (0, auth_1.validateStoredToken)(api);
|
|
135
|
+
if (existing) {
|
|
136
|
+
await storage.setDeviceIdentity({ deviceId: existing.deviceId, deviceName: existing.deviceName });
|
|
137
|
+
log(`Linked as "${existing.deviceName}". Starting sync…`);
|
|
138
|
+
sync.start({ token: existing.token, deviceId: existing.deviceId });
|
|
139
|
+
return; // sync loop keeps process alive
|
|
140
|
+
}
|
|
141
|
+
// No valid token — need to pair
|
|
142
|
+
if (isDaemon) {
|
|
143
|
+
log('No valid device token found. Run without --daemon to pair this device.');
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
// Interactive pairing
|
|
147
|
+
let result;
|
|
148
|
+
try {
|
|
149
|
+
result = await (0, auth_1.runPairingFlow)(api);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
console.error(`\nPairing failed: ${err}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
await storage.setDeviceToken(result.token);
|
|
156
|
+
await storage.setDeviceIdentity({ deviceId: result.deviceId, deviceName: result.deviceName });
|
|
157
|
+
console.log(`\n✓ Linked as "${result.deviceName}"`);
|
|
158
|
+
console.log('');
|
|
159
|
+
// Offer startup registration (only if globally installed)
|
|
160
|
+
const globalBin = await (0, startup_1.getGlobalBin)();
|
|
161
|
+
if (!globalBin) {
|
|
162
|
+
console.warn(' Note: walldock-agent is not globally installed — skipping startup registration.');
|
|
163
|
+
console.warn(' Run "npm install -g @walldock/agent" to enable automatic startup.');
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
|
|
167
|
+
const answer = (await rl.question('Register Walldock Agent to start automatically at login? [Y/n] ')).trim();
|
|
168
|
+
rl.close();
|
|
169
|
+
if (answer.toLowerCase() !== 'n') {
|
|
170
|
+
try {
|
|
171
|
+
await (0, startup_1.registerStartup)();
|
|
172
|
+
console.log(`✓ Registered for startup (${(0, startup_1.startupDescription)()})`);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
console.warn(` Could not register for startup: ${err}`);
|
|
176
|
+
console.warn(' You can run "walldock-agent --daemon" manually to start syncing.');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
console.log('\nStarting sync. Press Ctrl+C to stop.\n');
|
|
181
|
+
sync.start({ token: result.token, deviceId: result.deviceId });
|
|
182
|
+
}
|
|
183
|
+
main().catch(err => {
|
|
184
|
+
console.error('Fatal:', err);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
});
|
package/dist/pid.d.ts
ADDED