moondesk 0.1.1 → 0.1.4
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 +2 -0
- package/npm/install-binary.js +349 -0
- package/npm/moondesk.js +31 -18
- package/package.json +3 -6
- package/npm/postinstall.js +0 -96
package/README.md
CHANGED
|
@@ -88,6 +88,8 @@ I tried this with GPT-5.2 before, and the results were poor. However, **GPT-5.4
|
|
|
88
88
|
npm install -g moondesk
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
MoonDesk does not rely on npm lifecycle/install scripts. On the first `moondesk` run, the small npm wrapper downloads the matching native binary from the GitHub Release, verifies it against that release's `SHA256SUMS`, and stores it in a versioned user cache under `~/.moondesk/npm-bin/`. This works with npm 12's default-deny install-script policy and also keeps the running native executable outside npm's `node_modules`, which makes Windows package upgrades less likely to hit locked-file errors.
|
|
92
|
+
|
|
91
93
|
2. Run MoonDesk from any terminal directory.
|
|
92
94
|
|
|
93
95
|
```bash
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
|
|
8
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
9
|
+
const packageJson = require(path.join(packageRoot, "package.json"));
|
|
10
|
+
const version = packageJson.version;
|
|
11
|
+
const releaseTag = `v${version}`;
|
|
12
|
+
const defaultReleaseBaseUrl = `https://github.com/Shattermoon/moondesk/releases/download/${releaseTag}`;
|
|
13
|
+
|
|
14
|
+
const MAX_BINARY_BYTES = 128 * 1024 * 1024;
|
|
15
|
+
const MAX_CHECKSUM_BYTES = 1024 * 1024;
|
|
16
|
+
const METADATA_TIMEOUT_MS = 60_000;
|
|
17
|
+
const BINARY_TIMEOUT_MS = 10 * 60_000;
|
|
18
|
+
const LOCK_STALE_MS = 15 * 60_000;
|
|
19
|
+
const LOCK_WAIT_MS = 15 * 60_000;
|
|
20
|
+
const LOCK_POLL_MS = 100;
|
|
21
|
+
|
|
22
|
+
const supportedTargets = new Set([
|
|
23
|
+
"linux-x64",
|
|
24
|
+
"linux-arm64",
|
|
25
|
+
"darwin-x64",
|
|
26
|
+
"darwin-arm64",
|
|
27
|
+
"win32-x64",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function resolveTarget(platform = process.platform, arch = process.arch) {
|
|
31
|
+
const target = `${platform}-${arch}`;
|
|
32
|
+
if (!supportedTargets.has(target)) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`MoonDesk does not provide a prebuilt binary for ${target}. Supported targets: ${Array.from(supportedTargets).join(", ")}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
platform,
|
|
40
|
+
arch,
|
|
41
|
+
target,
|
|
42
|
+
assetName: platform === "win32" ? `moondesk-${target}.exe` : `moondesk-${target}`,
|
|
43
|
+
executableName: platform === "win32" ? "moondesk.exe" : "moondesk",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function defaultInstallDir(target) {
|
|
48
|
+
if (process.env.MOONDESK_BINARY_CACHE_DIR) {
|
|
49
|
+
return path.resolve(process.env.MOONDESK_BINARY_CACHE_DIR);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return path.join(os.homedir(), ".moondesk", "npm-bin", releaseTag, target);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function fetchRequired(fetchImpl, url, maxBytes, timeoutMs = METADATA_TIMEOUT_MS) {
|
|
56
|
+
const response = await fetchImpl(url, {
|
|
57
|
+
headers: {
|
|
58
|
+
"User-Agent": `moondesk-npm/${version}`,
|
|
59
|
+
},
|
|
60
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
throw new Error(`${url} returned HTTP ${response.status}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
68
|
+
if (Number.isFinite(contentLength) && contentLength > maxBytes) {
|
|
69
|
+
throw new Error(`${url} is unexpectedly large (${contentLength} bytes)`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
73
|
+
if (buffer.length > maxBytes) {
|
|
74
|
+
throw new Error(`${url} exceeded the ${maxBytes}-byte download limit`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return buffer;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function expectedSha256(checksums, name) {
|
|
81
|
+
for (const line of checksums.split(/\r?\n/)) {
|
|
82
|
+
const match = line.trim().match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
|
|
83
|
+
if (match && path.basename(match[2]) === name) {
|
|
84
|
+
return match[1].toLowerCase();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw new Error(`SHA256SUMS does not contain ${name}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function sha256Buffer(buffer) {
|
|
92
|
+
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sha256File(filePath) {
|
|
96
|
+
return sha256Buffer(fs.readFileSync(filePath));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cacheMetadata(stat, expected) {
|
|
100
|
+
return {
|
|
101
|
+
version: 1,
|
|
102
|
+
sha256: expected,
|
|
103
|
+
size: stat.size,
|
|
104
|
+
mtimeMs: stat.mtimeMs,
|
|
105
|
+
ctimeMs: stat.ctimeMs,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function writeCacheMetadata(metadataPath, binaryPath, expected) {
|
|
110
|
+
const stat = fs.statSync(binaryPath);
|
|
111
|
+
fs.writeFileSync(metadataPath, `${JSON.stringify(cacheMetadata(stat, expected))}\n`, {
|
|
112
|
+
mode: 0o600,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function validCachedBinary(binaryPath, checksumPath, metadataPath, platform) {
|
|
117
|
+
if (!fs.existsSync(binaryPath) || !fs.existsSync(checksumPath)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let stat = fs.statSync(binaryPath);
|
|
122
|
+
if (!stat.isFile() || stat.size === 0 || stat.size > MAX_BINARY_BYTES) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const expected = fs.readFileSync(checksumPath, "utf8").trim().toLowerCase();
|
|
127
|
+
if (!/^[a-f0-9]{64}$/.test(expected)) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let metadata;
|
|
132
|
+
try {
|
|
133
|
+
metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
|
134
|
+
} catch {
|
|
135
|
+
metadata = null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const fastPathMatches =
|
|
139
|
+
metadata?.version === 1 &&
|
|
140
|
+
metadata.sha256 === expected &&
|
|
141
|
+
metadata.size === stat.size &&
|
|
142
|
+
metadata.mtimeMs === stat.mtimeMs &&
|
|
143
|
+
metadata.ctimeMs === stat.ctimeMs;
|
|
144
|
+
|
|
145
|
+
if (fastPathMatches) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (sha256File(binaryPath) !== expected) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (platform !== "win32" && (stat.mode & 0o111) === 0) {
|
|
154
|
+
fs.chmodSync(binaryPath, 0o755);
|
|
155
|
+
stat = fs.statSync(binaryPath);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fs.writeFileSync(metadataPath, `${JSON.stringify(cacheMetadata(stat, expected))}\n`, {
|
|
159
|
+
mode: 0o600,
|
|
160
|
+
});
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sleep(ms) {
|
|
165
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function lockOwnerIsAlive(lockPath) {
|
|
169
|
+
try {
|
|
170
|
+
const [pidText] = fs.readFileSync(lockPath, "utf8").split(/\r?\n/);
|
|
171
|
+
const pid = Number(pidText);
|
|
172
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
process.kill(pid, 0);
|
|
178
|
+
return true;
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error.code === "ESRCH") {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
if (error.code === "EPERM") {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (error.code === "ENOENT") {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function acquireInstallLock(
|
|
197
|
+
lockPath,
|
|
198
|
+
binaryPath,
|
|
199
|
+
checksumPath,
|
|
200
|
+
metadataPath,
|
|
201
|
+
platform,
|
|
202
|
+
) {
|
|
203
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
204
|
+
|
|
205
|
+
while (Date.now() < deadline) {
|
|
206
|
+
if (validCachedBinary(binaryPath, checksumPath, metadataPath, platform)) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const fd = fs.openSync(lockPath, "wx", 0o600);
|
|
212
|
+
fs.writeFileSync(fd, `${process.pid}\n${Date.now()}\n`);
|
|
213
|
+
return fd;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error.code !== "EEXIST") {
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const age = Date.now() - fs.statSync(lockPath).mtimeMs;
|
|
221
|
+
if (!lockOwnerIsAlive(lockPath) || age > LOCK_STALE_MS) {
|
|
222
|
+
fs.rmSync(lockPath, { force: true });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
} catch (statError) {
|
|
226
|
+
if (statError.code !== "ENOENT") {
|
|
227
|
+
throw statError;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await sleep(LOCK_POLL_MS);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (validCachedBinary(binaryPath, checksumPath, metadataPath, platform)) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
throw new Error("Timed out waiting for another MoonDesk process to finish installing the native binary");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function ensureBinary(options = {}) {
|
|
243
|
+
const targetInfo = resolveTarget(options.platform, options.arch);
|
|
244
|
+
const installDir = options.installDir ?? defaultInstallDir(targetInfo.target);
|
|
245
|
+
const releaseBaseUrl = options.releaseBaseUrl ?? defaultReleaseBaseUrl;
|
|
246
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
247
|
+
|
|
248
|
+
if (typeof fetchImpl !== "function") {
|
|
249
|
+
throw new Error("MoonDesk requires Node.js 18 or newer so the native binary can be downloaded securely");
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const binaryPath = path.join(installDir, targetInfo.executableName);
|
|
253
|
+
const checksumPath = `${binaryPath}.sha256`;
|
|
254
|
+
const metadataPath = `${binaryPath}.metadata.json`;
|
|
255
|
+
const lockPath = path.join(installDir, ".install.lock");
|
|
256
|
+
|
|
257
|
+
fs.mkdirSync(installDir, { recursive: true, mode: 0o700 });
|
|
258
|
+
|
|
259
|
+
if (validCachedBinary(binaryPath, checksumPath, metadataPath, targetInfo.platform)) {
|
|
260
|
+
return binaryPath;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const lockFd = await acquireInstallLock(
|
|
264
|
+
lockPath,
|
|
265
|
+
binaryPath,
|
|
266
|
+
checksumPath,
|
|
267
|
+
metadataPath,
|
|
268
|
+
targetInfo.platform,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (lockFd === null) {
|
|
272
|
+
return binaryPath;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const nonce = `${process.pid}-${crypto.randomBytes(8).toString("hex")}`;
|
|
276
|
+
const tempBinary = `${binaryPath}.tmp-${nonce}`;
|
|
277
|
+
const tempChecksum = `${checksumPath}.tmp-${nonce}`;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
if (validCachedBinary(binaryPath, checksumPath, metadataPath, targetInfo.platform)) {
|
|
281
|
+
return binaryPath;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const checksumsBuffer = await fetchRequired(
|
|
285
|
+
fetchImpl,
|
|
286
|
+
`${releaseBaseUrl}/SHA256SUMS`,
|
|
287
|
+
MAX_CHECKSUM_BYTES,
|
|
288
|
+
);
|
|
289
|
+
const expected = expectedSha256(checksumsBuffer.toString("utf8"), targetInfo.assetName);
|
|
290
|
+
const binary = await fetchRequired(
|
|
291
|
+
fetchImpl,
|
|
292
|
+
`${releaseBaseUrl}/${targetInfo.assetName}`,
|
|
293
|
+
MAX_BINARY_BYTES,
|
|
294
|
+
BINARY_TIMEOUT_MS,
|
|
295
|
+
);
|
|
296
|
+
const actual = sha256Buffer(binary);
|
|
297
|
+
|
|
298
|
+
if (actual !== expected) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Checksum mismatch for ${targetInfo.assetName}: expected ${expected}, got ${actual}`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
fs.writeFileSync(tempBinary, binary, { mode: 0o755 });
|
|
305
|
+
if (targetInfo.platform !== "win32") {
|
|
306
|
+
fs.chmodSync(tempBinary, 0o755);
|
|
307
|
+
}
|
|
308
|
+
fs.writeFileSync(tempChecksum, `${expected}\n`, { mode: 0o600 });
|
|
309
|
+
|
|
310
|
+
fs.rmSync(binaryPath, { force: true });
|
|
311
|
+
fs.rmSync(checksumPath, { force: true });
|
|
312
|
+
fs.rmSync(metadataPath, { force: true });
|
|
313
|
+
fs.renameSync(tempBinary, binaryPath);
|
|
314
|
+
fs.renameSync(tempChecksum, checksumPath);
|
|
315
|
+
writeCacheMetadata(metadataPath, binaryPath, expected);
|
|
316
|
+
|
|
317
|
+
if (!validCachedBinary(binaryPath, checksumPath, metadataPath, targetInfo.platform)) {
|
|
318
|
+
throw new Error(`Installed ${targetInfo.assetName} failed its local checksum verification`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return binaryPath;
|
|
322
|
+
} finally {
|
|
323
|
+
fs.rmSync(tempBinary, { force: true });
|
|
324
|
+
fs.rmSync(tempChecksum, { force: true });
|
|
325
|
+
try {
|
|
326
|
+
fs.closeSync(lockFd);
|
|
327
|
+
} finally {
|
|
328
|
+
fs.rmSync(lockPath, { force: true });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
module.exports = {
|
|
334
|
+
ensureBinary,
|
|
335
|
+
expectedSha256,
|
|
336
|
+
resolveTarget,
|
|
337
|
+
sha256Buffer,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
if (require.main === module) {
|
|
341
|
+
ensureBinary()
|
|
342
|
+
.then((binaryPath) => {
|
|
343
|
+
console.log(`MoonDesk native binary ready at ${binaryPath}`);
|
|
344
|
+
})
|
|
345
|
+
.catch((error) => {
|
|
346
|
+
console.error(`MoonDesk binary install failed: ${error.message}`);
|
|
347
|
+
process.exit(1);
|
|
348
|
+
});
|
|
349
|
+
}
|
package/npm/moondesk.js
CHANGED
|
@@ -1,27 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { spawn } = require("node:child_process");
|
|
4
|
-
const
|
|
4
|
+
const { ensureBinary } = require("./install-binary");
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
async function main() {
|
|
7
|
+
let binaryPath;
|
|
8
|
+
try {
|
|
9
|
+
binaryPath = await ensureBinary();
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error(`MoonDesk could not prepare its native binary: ${error.message}`);
|
|
12
|
+
console.error("Check your network connection and the matching GitHub Release, then run MoonDesk again.");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
8
16
|
|
|
9
|
-
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
});
|
|
17
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
18
|
+
cwd: process.cwd(),
|
|
19
|
+
env: process.env,
|
|
20
|
+
stdio: "inherit",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
child.on("error", (error) => {
|
|
24
|
+
console.error(`MoonDesk failed to start: ${error.message}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
child.on("exit", (code, signal) => {
|
|
29
|
+
if (signal) {
|
|
30
|
+
process.kill(process.pid, signal);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
process.exit(code ?? 1);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
14
36
|
|
|
15
|
-
|
|
37
|
+
main().catch((error) => {
|
|
16
38
|
console.error(`MoonDesk failed to start: ${error.message}`);
|
|
17
|
-
console.error("Reinstall MoonDesk after the matching GitHub Release binary is available.");
|
|
18
39
|
process.exit(1);
|
|
19
40
|
});
|
|
20
|
-
|
|
21
|
-
child.on("exit", (code, signal) => {
|
|
22
|
-
if (signal) {
|
|
23
|
-
process.kill(process.pid, signal);
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
process.exit(code ?? 1);
|
|
27
|
-
});
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moondesk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "MoonDesk — use ChatGPT Chat as a local coding agent, by Shattermoon.",
|
|
5
5
|
"author": "Shattermoon",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/Shattermoon/moondesk"
|
|
9
|
+
"url": "git+https://github.com/Shattermoon/moondesk.git"
|
|
10
10
|
},
|
|
11
11
|
"homepage": "https://github.com/Shattermoon/moondesk#readme",
|
|
12
12
|
"bugs": {
|
|
@@ -15,12 +15,9 @@
|
|
|
15
15
|
"bin": {
|
|
16
16
|
"moondesk": "npm/moondesk.js"
|
|
17
17
|
},
|
|
18
|
-
"scripts": {
|
|
19
|
-
"postinstall": "node npm/postinstall.js"
|
|
20
|
-
},
|
|
21
18
|
"files": [
|
|
22
19
|
"npm/moondesk.js",
|
|
23
|
-
"npm/
|
|
20
|
+
"npm/install-binary.js",
|
|
24
21
|
"LICENSE",
|
|
25
22
|
"README.md"
|
|
26
23
|
],
|
package/npm/postinstall.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
const crypto = require("node:crypto");
|
|
4
|
-
const fs = require("node:fs");
|
|
5
|
-
const path = require("node:path");
|
|
6
|
-
|
|
7
|
-
const packageRoot = path.resolve(__dirname, "..");
|
|
8
|
-
const packageJson = require(path.join(packageRoot, "package.json"));
|
|
9
|
-
const version = packageJson.version;
|
|
10
|
-
const releaseTag = `v${version}`;
|
|
11
|
-
const releaseBaseUrl = `https://github.com/Shattermoon/moondesk/releases/download/${releaseTag}`;
|
|
12
|
-
|
|
13
|
-
const supportedTargets = new Set([
|
|
14
|
-
"linux-x64",
|
|
15
|
-
"linux-arm64",
|
|
16
|
-
"darwin-x64",
|
|
17
|
-
"darwin-arm64",
|
|
18
|
-
"win32-x64",
|
|
19
|
-
]);
|
|
20
|
-
|
|
21
|
-
const platform = process.platform;
|
|
22
|
-
const arch = process.arch;
|
|
23
|
-
const target = `${platform}-${arch}`;
|
|
24
|
-
|
|
25
|
-
if (!supportedTargets.has(target)) {
|
|
26
|
-
console.error(`MoonDesk does not provide a prebuilt binary for ${target}.`);
|
|
27
|
-
console.error(`Supported targets: ${Array.from(supportedTargets).join(", ")}`);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const assetName = platform === "win32" ? `moondesk-${target}.exe` : `moondesk-${target}`;
|
|
32
|
-
const executableName = platform === "win32" ? "moondesk.exe" : "moondesk";
|
|
33
|
-
const binDir = path.join(__dirname, "bin");
|
|
34
|
-
const installedBinary = path.join(binDir, executableName);
|
|
35
|
-
|
|
36
|
-
async function fetchRequired(url) {
|
|
37
|
-
const response = await fetch(url, {
|
|
38
|
-
headers: {
|
|
39
|
-
"User-Agent": `moondesk-npm-install/${version}`,
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
if (!response.ok) {
|
|
44
|
-
throw new Error(`${url} returned HTTP ${response.status}`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return response;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function downloadBuffer(url) {
|
|
51
|
-
const response = await fetchRequired(url);
|
|
52
|
-
return Buffer.from(await response.arrayBuffer());
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function downloadText(url) {
|
|
56
|
-
const response = await fetchRequired(url);
|
|
57
|
-
return response.text();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function expectedSha256(checksums, name) {
|
|
61
|
-
for (const line of checksums.split(/\r?\n/)) {
|
|
62
|
-
const match = line.trim().match(/^([a-fA-F0-9]{64})\s+\*?(.+)$/);
|
|
63
|
-
if (match && path.basename(match[2]) === name) {
|
|
64
|
-
return match[1].toLowerCase();
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
throw new Error(`SHA256SUMS does not contain ${name}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function sha256(buffer) {
|
|
72
|
-
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function main() {
|
|
76
|
-
const checksums = await downloadText(`${releaseBaseUrl}/SHA256SUMS`);
|
|
77
|
-
const expected = expectedSha256(checksums, assetName);
|
|
78
|
-
const binary = await downloadBuffer(`${releaseBaseUrl}/${assetName}`);
|
|
79
|
-
const actual = sha256(binary);
|
|
80
|
-
|
|
81
|
-
if (actual !== expected) {
|
|
82
|
-
throw new Error(`Checksum mismatch for ${assetName}: expected ${expected}, got ${actual}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
86
|
-
fs.writeFileSync(installedBinary, binary);
|
|
87
|
-
|
|
88
|
-
if (platform !== "win32") {
|
|
89
|
-
fs.chmodSync(installedBinary, 0o755);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
main().catch((error) => {
|
|
94
|
-
console.error(`MoonDesk install failed: ${error.message}`);
|
|
95
|
-
process.exit(1);
|
|
96
|
-
});
|