@techsologic/unolock-agent 0.1.37 → 0.1.39

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 (2) hide show
  1. package/bin/unolock-agent.js +136 -16
  2. package/package.json +1 -1
@@ -7,9 +7,12 @@ const path = require("path");
7
7
  const https = require("https");
8
8
  const { spawn } = require("child_process");
9
9
 
10
- const PACKAGE_VERSION = "0.1.37";
11
- const FALLBACK_BINARY_VERSION = "0.1.37";
10
+ const PACKAGE_VERSION = "0.1.39";
11
+ const FALLBACK_BINARY_VERSION = "0.1.39";
12
12
  const REPO = "TechSologic/unolock-agent";
13
+ const INSTALL_LOCK_TIMEOUT_MS = 120000;
14
+ const INSTALL_LOCK_STALE_MS = 300000;
15
+ const INSTALL_LOCK_POLL_MS = 100;
13
16
  const TOP_LEVEL_USAGE = `usage: unolock-agent [-h] [--version] {register,set-agent-pin,list-spaces,get-current-space,set-current-space,list-records,list-notes,list-checklists,get-record,create-note,update-note,append-note,rename-record,create-checklist,set-checklist-item-done,add-checklist-item,remove-checklist-item,list-files,get-file,download-file,upload-file,rename-file,replace-file,delete-file,tpm-diagnose,tpm-check,self-test,mcp} ...
14
17
 
15
18
  UnoLock Agent commands.
@@ -79,6 +82,10 @@ function metadataPath() {
79
82
  return path.join(cacheRoot(), "unolock-agent", "release.json");
80
83
  }
81
84
 
85
+ function installLockPath() {
86
+ return path.join(cacheRoot(), "unolock-agent", "install.lock");
87
+ }
88
+
82
89
  function binaryPath(releaseVersion) {
83
90
  const { executable } = platformAssetInfo();
84
91
  return path.join(cacheRoot(), "unolock-agent", releaseVersion, executable);
@@ -96,6 +103,12 @@ function ensureDir(dir) {
96
103
  fs.mkdirSync(dir, { recursive: true, mode: 0o755 });
97
104
  }
98
105
 
106
+ function sleep(ms) {
107
+ return new Promise((resolve) => {
108
+ setTimeout(resolve, ms);
109
+ });
110
+ }
111
+
99
112
  function fetchToFile(url, dest) {
100
113
  return new Promise((resolve, reject) => {
101
114
  const temp = `${dest}.download`;
@@ -188,6 +201,23 @@ function normalizeVersion(value) {
188
201
  return trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;
189
202
  }
190
203
 
204
+ function compareVersions(left, right) {
205
+ const leftParts = String(left || "").split(".").map((part) => Number.parseInt(part, 10) || 0);
206
+ const rightParts = String(right || "").split(".").map((part) => Number.parseInt(part, 10) || 0);
207
+ const length = Math.max(leftParts.length, rightParts.length);
208
+ for (let index = 0; index < length; index += 1) {
209
+ const leftValue = leftParts[index] || 0;
210
+ const rightValue = rightParts[index] || 0;
211
+ if (leftValue > rightValue) {
212
+ return 1;
213
+ }
214
+ if (leftValue < rightValue) {
215
+ return -1;
216
+ }
217
+ }
218
+ return 0;
219
+ }
220
+
191
221
  function readReleaseMetadata() {
192
222
  try {
193
223
  return JSON.parse(fs.readFileSync(metadataPath(), "utf8"));
@@ -198,8 +228,10 @@ function readReleaseMetadata() {
198
228
 
199
229
  function writeReleaseMetadata(releaseVersion) {
200
230
  ensureDir(path.dirname(metadataPath()));
231
+ const dest = metadataPath();
232
+ const temp = `${dest}.tmp-${process.pid}`;
201
233
  fs.writeFileSync(
202
- metadataPath(),
234
+ temp,
203
235
  JSON.stringify(
204
236
  {
205
237
  releaseVersion,
@@ -210,6 +242,7 @@ function writeReleaseMetadata(releaseVersion) {
210
242
  ),
211
243
  "utf8"
212
244
  );
245
+ fs.renameSync(temp, dest);
213
246
  }
214
247
 
215
248
  function cachedReleaseVersion() {
@@ -218,8 +251,11 @@ function cachedReleaseVersion() {
218
251
  return override;
219
252
  }
220
253
  const metadata = readReleaseMetadata();
221
- if (metadata && typeof metadata.releaseVersion === "string" && fs.existsSync(binaryPath(metadata.releaseVersion))) {
222
- return metadata.releaseVersion;
254
+ const cached = metadata && typeof metadata.releaseVersion === "string" ? normalizeVersion(metadata.releaseVersion) : null;
255
+ if (cached && fs.existsSync(binaryPath(cached))) {
256
+ if (compareVersions(cached, FALLBACK_BINARY_VERSION) >= 0) {
257
+ return cached;
258
+ }
223
259
  }
224
260
  if (fs.existsSync(binaryPath(FALLBACK_BINARY_VERSION))) {
225
261
  return FALLBACK_BINARY_VERSION;
@@ -246,16 +282,88 @@ async function resolveReleaseVersion() {
246
282
  return FALLBACK_BINARY_VERSION;
247
283
  }
248
284
 
285
+ function removeIfStale(lockPath) {
286
+ let stats;
287
+ try {
288
+ stats = fs.statSync(lockPath);
289
+ } catch (error) {
290
+ if (error && error.code === "ENOENT") {
291
+ return false;
292
+ }
293
+ throw error;
294
+ }
295
+ if (Date.now() - stats.mtimeMs < INSTALL_LOCK_STALE_MS) {
296
+ return false;
297
+ }
298
+ try {
299
+ fs.unlinkSync(lockPath);
300
+ return true;
301
+ } catch (error) {
302
+ if (error && error.code === "ENOENT") {
303
+ return true;
304
+ }
305
+ if (error && error.code === "EPERM") {
306
+ return false;
307
+ }
308
+ throw error;
309
+ }
310
+ }
311
+
312
+ async function acquireInstallLock() {
313
+ const lockPath = installLockPath();
314
+ ensureDir(path.dirname(lockPath));
315
+ const deadline = Date.now() + INSTALL_LOCK_TIMEOUT_MS;
316
+ while (true) {
317
+ try {
318
+ const fd = fs.openSync(lockPath, "wx", 0o600);
319
+ fs.writeFileSync(
320
+ fd,
321
+ JSON.stringify({ pid: process.pid, createdAt: Date.now() }),
322
+ "utf8"
323
+ );
324
+ return () => {
325
+ try {
326
+ fs.closeSync(fd);
327
+ } catch {}
328
+ try {
329
+ fs.unlinkSync(lockPath);
330
+ } catch {}
331
+ };
332
+ } catch (error) {
333
+ if (!error || error.code !== "EEXIST") {
334
+ throw error;
335
+ }
336
+ removeIfStale(lockPath);
337
+ if (Date.now() >= deadline) {
338
+ throw new Error("Timed out waiting for the UnoLock agent install lock");
339
+ }
340
+ await sleep(INSTALL_LOCK_POLL_MS);
341
+ }
342
+ }
343
+ }
344
+
249
345
  async function ensureBinary() {
250
- const releaseVersion = await resolveReleaseVersion();
251
- const dest = binaryPath(releaseVersion);
252
- if (fs.existsSync(dest)) {
346
+ const cached = cachedReleaseVersion();
347
+ if (cached) {
348
+ const dest = binaryPath(cached);
349
+ if (fs.existsSync(dest)) {
350
+ return { dest, releaseVersion: cached };
351
+ }
352
+ }
353
+ const releaseLock = await acquireInstallLock();
354
+ try {
355
+ const releaseVersion = await resolveReleaseVersion();
356
+ const dest = binaryPath(releaseVersion);
357
+ if (fs.existsSync(dest)) {
358
+ return { dest, releaseVersion };
359
+ }
360
+ ensureDir(path.dirname(dest));
361
+ process.stderr.write(`Downloading UnoLock agent ${releaseVersion} for ${process.platform}/${process.arch}...\n`);
362
+ await fetchToFile(binaryUrl(releaseVersion), dest);
253
363
  return { dest, releaseVersion };
364
+ } finally {
365
+ releaseLock();
254
366
  }
255
- ensureDir(path.dirname(dest));
256
- process.stderr.write(`Downloading UnoLock agent ${releaseVersion} for ${process.platform}/${process.arch}...\n`);
257
- await fetchToFile(binaryUrl(releaseVersion), dest);
258
- return { dest, releaseVersion };
259
367
  }
260
368
 
261
369
  async function main() {
@@ -291,7 +399,19 @@ async function main() {
291
399
  });
292
400
  }
293
401
 
294
- main().catch((error) => {
295
- process.stderr.write(`${error.message}\n`);
296
- process.exit(1);
297
- });
402
+ if (require.main === module) {
403
+ main().catch((error) => {
404
+ process.stderr.write(`${error.message}\n`);
405
+ process.exit(1);
406
+ });
407
+ } else {
408
+ module.exports = {
409
+ acquireInstallLock,
410
+ compareVersions,
411
+ ensureBinary,
412
+ installLockPath,
413
+ metadataPath,
414
+ sleep,
415
+ writeReleaseMetadata,
416
+ };
417
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techsologic/unolock-agent",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "npx wrapper for the official UnoLock Agent release binaries",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://unolock.ai",