@techsologic/unolock-agent 0.1.38 → 0.1.40

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.
@@ -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.38";
11
- const FALLBACK_BINARY_VERSION = "0.1.38";
10
+ const PACKAGE_VERSION = "0.1.40";
11
+ const FALLBACK_BINARY_VERSION = "0.1.40";
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`;
@@ -215,8 +228,10 @@ function readReleaseMetadata() {
215
228
 
216
229
  function writeReleaseMetadata(releaseVersion) {
217
230
  ensureDir(path.dirname(metadataPath()));
231
+ const dest = metadataPath();
232
+ const temp = `${dest}.tmp-${process.pid}`;
218
233
  fs.writeFileSync(
219
- metadataPath(),
234
+ temp,
220
235
  JSON.stringify(
221
236
  {
222
237
  releaseVersion,
@@ -227,6 +242,7 @@ function writeReleaseMetadata(releaseVersion) {
227
242
  ),
228
243
  "utf8"
229
244
  );
245
+ fs.renameSync(temp, dest);
230
246
  }
231
247
 
232
248
  function cachedReleaseVersion() {
@@ -266,16 +282,88 @@ async function resolveReleaseVersion() {
266
282
  return FALLBACK_BINARY_VERSION;
267
283
  }
268
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
+
269
345
  async function ensureBinary() {
270
- const releaseVersion = await resolveReleaseVersion();
271
- const dest = binaryPath(releaseVersion);
272
- 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);
273
363
  return { dest, releaseVersion };
364
+ } finally {
365
+ releaseLock();
274
366
  }
275
- ensureDir(path.dirname(dest));
276
- process.stderr.write(`Downloading UnoLock agent ${releaseVersion} for ${process.platform}/${process.arch}...\n`);
277
- await fetchToFile(binaryUrl(releaseVersion), dest);
278
- return { dest, releaseVersion };
279
367
  }
280
368
 
281
369
  async function main() {
@@ -311,7 +399,19 @@ async function main() {
311
399
  });
312
400
  }
313
401
 
314
- main().catch((error) => {
315
- process.stderr.write(`${error.message}\n`);
316
- process.exit(1);
317
- });
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
+ }
@@ -69,7 +69,7 @@ Choose the object that matches the work:
69
69
  - `unolock-agent register '<agent-key-url>' '<pin>'`: first setup on this device
70
70
  - `unolock-agent set-agent-pin '<pin>'`: provide the PIN again after restart or re-authentication
71
71
  - `unolock-agent list-spaces`, `unolock-agent get-current-space`, `unolock-agent set-current-space <space_id>`: inspect and switch the current Space
72
- - `unolock-agent list-notes`, `unolock-agent create-note <title> <text>`, `unolock-agent update-note ...`, `unolock-agent append-note ...`: read and write notes
72
+ - `unolock-agent list-notes`, `unolock-agent create-note <title> <text>`, `unolock-agent update-note <record_ref> [--title <title>] [--text <text>]`, `unolock-agent append-note ...`: read and write notes
73
73
  - `unolock-agent list-checklists`, `unolock-agent create-checklist ...`, `unolock-agent set-checklist-item-done ...`, `unolock-agent add-checklist-item ...`, `unolock-agent remove-checklist-item ...`: read and write checklists
74
74
  - `unolock-agent list-files`, `unolock-agent get-file <archive_id>`, `unolock-agent download-file ...`, `unolock-agent upload-file ...`, `unolock-agent rename-file ...`, `unolock-agent replace-file ...`, `unolock-agent delete-file ...`: read and manage Cloud files
75
75
  - `unolock-agent get-record <record_ref>` and `unolock-agent rename-record ...`: inspect or rename an existing note or checklist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techsologic/unolock-agent",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "description": "npx wrapper for the official UnoLock Agent release binaries",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://unolock.ai",
@@ -70,7 +70,7 @@ Choose the object that matches the work:
70
70
  - `unolock-agent register '<agent-key-url>' '<pin>'`: first setup on this device
71
71
  - `unolock-agent set-agent-pin '<pin>'`: provide the PIN again after restart or re-authentication
72
72
  - `unolock-agent list-spaces`, `unolock-agent get-current-space`, `unolock-agent set-current-space <space_id>`: inspect and switch the current Space
73
- - `unolock-agent list-notes`, `unolock-agent create-note <title> <text>`, `unolock-agent update-note ...`, `unolock-agent append-note ...`: read and write notes
73
+ - `unolock-agent list-notes`, `unolock-agent create-note <title> <text>`, `unolock-agent update-note <record_ref> [--title <title>] [--text <text>]`, `unolock-agent append-note ...`: read and write notes
74
74
  - `unolock-agent list-checklists`, `unolock-agent create-checklist ...`, `unolock-agent set-checklist-item-done ...`, `unolock-agent add-checklist-item ...`, `unolock-agent remove-checklist-item ...`: read and write checklists
75
75
  - `unolock-agent list-files`, `unolock-agent get-file <archive_id>`, `unolock-agent download-file ...`, `unolock-agent upload-file ...`, `unolock-agent rename-file ...`, `unolock-agent replace-file ...`, `unolock-agent delete-file ...`: read and manage Cloud files
76
76
  - `unolock-agent get-record <record_ref>` and `unolock-agent rename-record ...`: inspect or rename an existing note or checklist