@checkstack/script-packages-backend 0.2.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +273 -0
  2. package/drizzle/0000_flashy_squadron_supreme.sql +63 -0
  3. package/drizzle/0001_flawless_drax.sql +15 -0
  4. package/drizzle/meta/0000_snapshot.json +395 -0
  5. package/drizzle/meta/0001_snapshot.json +491 -0
  6. package/drizzle/meta/_journal.json +20 -0
  7. package/drizzle.config.ts +7 -0
  8. package/package.json +32 -0
  9. package/src/atomic-symlink.test.ts +47 -0
  10. package/src/atomic-symlink.ts +66 -0
  11. package/src/blob-gc-runner.test.ts +120 -0
  12. package/src/blob-gc-runner.ts +139 -0
  13. package/src/blob-gc.test.ts +182 -0
  14. package/src/blob-gc.ts +161 -0
  15. package/src/blob-hash.test.ts +70 -0
  16. package/src/blob-hash.ts +56 -0
  17. package/src/blob-store-registry.test.ts +78 -0
  18. package/src/blob-store-registry.ts +75 -0
  19. package/src/blob-store.ts +51 -0
  20. package/src/cache-archive.test.ts +164 -0
  21. package/src/cache-archive.ts +192 -0
  22. package/src/cache-layout.ts +64 -0
  23. package/src/data-dir.test.ts +41 -0
  24. package/src/data-dir.ts +42 -0
  25. package/src/e2e-install-reconcile.test.ts +121 -0
  26. package/src/hooks.ts +20 -0
  27. package/src/index.ts +594 -0
  28. package/src/install-controller.test.ts +257 -0
  29. package/src/install-controller.ts +144 -0
  30. package/src/install-service.test.ts +104 -0
  31. package/src/install-service.ts +116 -0
  32. package/src/install-state-store.ts +131 -0
  33. package/src/lockfile.test.ts +60 -0
  34. package/src/lockfile.ts +0 -0
  35. package/src/npmrc.test.ts +48 -0
  36. package/src/npmrc.ts +42 -0
  37. package/src/package-types.test.ts +293 -0
  38. package/src/package-types.ts +408 -0
  39. package/src/parse-bun-lock.test.ts +62 -0
  40. package/src/parse-bun-lock.ts +59 -0
  41. package/src/reconcile-diff.test.ts +41 -0
  42. package/src/reconcile-diff.ts +26 -0
  43. package/src/reconcile-fs.ts +199 -0
  44. package/src/reconciler.test.ts +289 -0
  45. package/src/reconciler.ts +81 -0
  46. package/src/registry-client.test.ts +314 -0
  47. package/src/registry-client.ts +0 -0
  48. package/src/registry-request-config.ts +63 -0
  49. package/src/registry-token.test.ts +124 -0
  50. package/src/registry-token.ts +104 -0
  51. package/src/resolution-root.test.ts +82 -0
  52. package/src/resolution-root.ts +127 -0
  53. package/src/resolver.test.ts +133 -0
  54. package/src/resolver.ts +132 -0
  55. package/src/router.ts +273 -0
  56. package/src/schema.ts +166 -0
  57. package/src/size-cap.test.ts +32 -0
  58. package/src/size-cap.ts +40 -0
  59. package/src/storage-migration.test.ts +318 -0
  60. package/src/storage-migration.ts +213 -0
  61. package/src/stores.ts +533 -0
  62. package/src/tree-gc.test.ts +184 -0
  63. package/src/tree-gc.ts +160 -0
  64. package/src/tree-retirement.ts +81 -0
  65. package/src/type-acquisition-route.ts +178 -0
  66. package/tsconfig.json +23 -0
@@ -0,0 +1,491 @@
1
+ {
2
+ "id": "6c4865a1-d9ca-4b09-a63f-8119ddd39481",
3
+ "prevId": "033f32d0-38fc-4775-b1e3-ef2930de7493",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.script_package_blob": {
8
+ "name": "script_package_blob",
9
+ "schema": "",
10
+ "columns": {
11
+ "integrity": {
12
+ "name": "integrity",
13
+ "type": "text",
14
+ "primaryKey": true,
15
+ "notNull": true
16
+ },
17
+ "name": {
18
+ "name": "name",
19
+ "type": "text",
20
+ "primaryKey": false,
21
+ "notNull": true
22
+ },
23
+ "version": {
24
+ "name": "version",
25
+ "type": "text",
26
+ "primaryKey": false,
27
+ "notNull": true
28
+ },
29
+ "backend": {
30
+ "name": "backend",
31
+ "type": "text",
32
+ "primaryKey": false,
33
+ "notNull": true
34
+ },
35
+ "size_bytes": {
36
+ "name": "size_bytes",
37
+ "type": "bigint",
38
+ "primaryKey": false,
39
+ "notNull": true
40
+ },
41
+ "created_at": {
42
+ "name": "created_at",
43
+ "type": "timestamp",
44
+ "primaryKey": false,
45
+ "notNull": true,
46
+ "default": "now()"
47
+ }
48
+ },
49
+ "indexes": {
50
+ "script_package_blob_backend_idx": {
51
+ "name": "script_package_blob_backend_idx",
52
+ "columns": [
53
+ {
54
+ "expression": "backend",
55
+ "isExpression": false,
56
+ "asc": true,
57
+ "nulls": "last"
58
+ }
59
+ ],
60
+ "isUnique": false,
61
+ "concurrently": false,
62
+ "method": "btree",
63
+ "with": {}
64
+ }
65
+ },
66
+ "foreignKeys": {},
67
+ "compositePrimaryKeys": {},
68
+ "uniqueConstraints": {},
69
+ "policies": {},
70
+ "checkConstraints": {},
71
+ "isRLSEnabled": false
72
+ },
73
+ "public.script_package_blob_gc_state": {
74
+ "name": "script_package_blob_gc_state",
75
+ "schema": "",
76
+ "columns": {
77
+ "id": {
78
+ "name": "id",
79
+ "type": "text",
80
+ "primaryKey": true,
81
+ "notNull": true,
82
+ "default": "'singleton'"
83
+ },
84
+ "last_run_at": {
85
+ "name": "last_run_at",
86
+ "type": "timestamp",
87
+ "primaryKey": false,
88
+ "notNull": false
89
+ },
90
+ "last_deleted": {
91
+ "name": "last_deleted",
92
+ "type": "integer",
93
+ "primaryKey": false,
94
+ "notNull": true,
95
+ "default": 0
96
+ },
97
+ "last_bytes_reclaimed": {
98
+ "name": "last_bytes_reclaimed",
99
+ "type": "bigint",
100
+ "primaryKey": false,
101
+ "notNull": true,
102
+ "default": 0
103
+ },
104
+ "total_bytes_reclaimed": {
105
+ "name": "total_bytes_reclaimed",
106
+ "type": "bigint",
107
+ "primaryKey": false,
108
+ "notNull": true,
109
+ "default": 0
110
+ }
111
+ },
112
+ "indexes": {},
113
+ "foreignKeys": {},
114
+ "compositePrimaryKeys": {},
115
+ "uniqueConstraints": {},
116
+ "policies": {},
117
+ "checkConstraints": {},
118
+ "isRLSEnabled": false
119
+ },
120
+ "public.script_package_install_state": {
121
+ "name": "script_package_install_state",
122
+ "schema": "",
123
+ "columns": {
124
+ "id": {
125
+ "name": "id",
126
+ "type": "text",
127
+ "primaryKey": true,
128
+ "notNull": true,
129
+ "default": "'singleton'"
130
+ },
131
+ "status": {
132
+ "name": "status",
133
+ "type": "text",
134
+ "primaryKey": false,
135
+ "notNull": true,
136
+ "default": "'idle'"
137
+ },
138
+ "lockfile_hash": {
139
+ "name": "lockfile_hash",
140
+ "type": "text",
141
+ "primaryKey": false,
142
+ "notNull": false
143
+ },
144
+ "manifest": {
145
+ "name": "manifest",
146
+ "type": "jsonb",
147
+ "primaryKey": false,
148
+ "notNull": true,
149
+ "default": "'[]'::jsonb"
150
+ },
151
+ "total_size_bytes": {
152
+ "name": "total_size_bytes",
153
+ "type": "bigint",
154
+ "primaryKey": false,
155
+ "notNull": true,
156
+ "default": 0
157
+ },
158
+ "last_installed_at": {
159
+ "name": "last_installed_at",
160
+ "type": "timestamp",
161
+ "primaryKey": false,
162
+ "notNull": false
163
+ },
164
+ "error_message": {
165
+ "name": "error_message",
166
+ "type": "text",
167
+ "primaryKey": false,
168
+ "notNull": false
169
+ }
170
+ },
171
+ "indexes": {},
172
+ "foreignKeys": {},
173
+ "compositePrimaryKeys": {},
174
+ "uniqueConstraints": {},
175
+ "policies": {},
176
+ "checkConstraints": {},
177
+ "isRLSEnabled": false
178
+ },
179
+ "public.script_package_lockfile_history": {
180
+ "name": "script_package_lockfile_history",
181
+ "schema": "",
182
+ "columns": {
183
+ "lockfile_hash": {
184
+ "name": "lockfile_hash",
185
+ "type": "text",
186
+ "primaryKey": true,
187
+ "notNull": true
188
+ },
189
+ "manifest": {
190
+ "name": "manifest",
191
+ "type": "jsonb",
192
+ "primaryKey": false,
193
+ "notNull": true,
194
+ "default": "'[]'::jsonb"
195
+ },
196
+ "recorded_at": {
197
+ "name": "recorded_at",
198
+ "type": "timestamp",
199
+ "primaryKey": false,
200
+ "notNull": true,
201
+ "default": "now()"
202
+ }
203
+ },
204
+ "indexes": {
205
+ "script_package_lockfile_history_recorded_idx": {
206
+ "name": "script_package_lockfile_history_recorded_idx",
207
+ "columns": [
208
+ {
209
+ "expression": "recorded_at",
210
+ "isExpression": false,
211
+ "asc": true,
212
+ "nulls": "last"
213
+ }
214
+ ],
215
+ "isUnique": false,
216
+ "concurrently": false,
217
+ "method": "btree",
218
+ "with": {}
219
+ }
220
+ },
221
+ "foreignKeys": {},
222
+ "compositePrimaryKeys": {},
223
+ "uniqueConstraints": {},
224
+ "policies": {},
225
+ "checkConstraints": {},
226
+ "isRLSEnabled": false
227
+ },
228
+ "public.script_package_registry_config": {
229
+ "name": "script_package_registry_config",
230
+ "schema": "",
231
+ "columns": {
232
+ "id": {
233
+ "name": "id",
234
+ "type": "text",
235
+ "primaryKey": true,
236
+ "notNull": true,
237
+ "default": "'singleton'"
238
+ },
239
+ "registry_url": {
240
+ "name": "registry_url",
241
+ "type": "text",
242
+ "primaryKey": false,
243
+ "notNull": true,
244
+ "default": "'https://registry.npmjs.org/'"
245
+ },
246
+ "scoped_registries": {
247
+ "name": "scoped_registries",
248
+ "type": "jsonb",
249
+ "primaryKey": false,
250
+ "notNull": true,
251
+ "default": "'[]'::jsonb"
252
+ },
253
+ "auth_secret_ref": {
254
+ "name": "auth_secret_ref",
255
+ "type": "text",
256
+ "primaryKey": false,
257
+ "notNull": false
258
+ },
259
+ "ignore_scripts": {
260
+ "name": "ignore_scripts",
261
+ "type": "boolean",
262
+ "primaryKey": false,
263
+ "notNull": true,
264
+ "default": true
265
+ },
266
+ "updated_at": {
267
+ "name": "updated_at",
268
+ "type": "timestamp",
269
+ "primaryKey": false,
270
+ "notNull": true,
271
+ "default": "now()"
272
+ }
273
+ },
274
+ "indexes": {},
275
+ "foreignKeys": {},
276
+ "compositePrimaryKeys": {},
277
+ "uniqueConstraints": {},
278
+ "policies": {},
279
+ "checkConstraints": {},
280
+ "isRLSEnabled": false
281
+ },
282
+ "public.script_package_satellite_state": {
283
+ "name": "script_package_satellite_state",
284
+ "schema": "",
285
+ "columns": {
286
+ "satellite_id": {
287
+ "name": "satellite_id",
288
+ "type": "text",
289
+ "primaryKey": true,
290
+ "notNull": true
291
+ },
292
+ "lockfile_hash": {
293
+ "name": "lockfile_hash",
294
+ "type": "text",
295
+ "primaryKey": false,
296
+ "notNull": false
297
+ },
298
+ "status": {
299
+ "name": "status",
300
+ "type": "text",
301
+ "primaryKey": false,
302
+ "notNull": true,
303
+ "default": "'pending'"
304
+ },
305
+ "error_message": {
306
+ "name": "error_message",
307
+ "type": "text",
308
+ "primaryKey": false,
309
+ "notNull": false
310
+ },
311
+ "synced_at": {
312
+ "name": "synced_at",
313
+ "type": "timestamp",
314
+ "primaryKey": false,
315
+ "notNull": false
316
+ }
317
+ },
318
+ "indexes": {},
319
+ "foreignKeys": {},
320
+ "compositePrimaryKeys": {},
321
+ "uniqueConstraints": {},
322
+ "policies": {},
323
+ "checkConstraints": {},
324
+ "isRLSEnabled": false
325
+ },
326
+ "public.script_package_size_cap": {
327
+ "name": "script_package_size_cap",
328
+ "schema": "",
329
+ "columns": {
330
+ "id": {
331
+ "name": "id",
332
+ "type": "text",
333
+ "primaryKey": true,
334
+ "notNull": true,
335
+ "default": "'singleton'"
336
+ },
337
+ "warn_bytes": {
338
+ "name": "warn_bytes",
339
+ "type": "bigint",
340
+ "primaryKey": false,
341
+ "notNull": true,
342
+ "default": 157286400
343
+ },
344
+ "block_bytes": {
345
+ "name": "block_bytes",
346
+ "type": "bigint",
347
+ "primaryKey": false,
348
+ "notNull": true,
349
+ "default": 314572800
350
+ },
351
+ "updated_at": {
352
+ "name": "updated_at",
353
+ "type": "timestamp",
354
+ "primaryKey": false,
355
+ "notNull": true,
356
+ "default": "now()"
357
+ }
358
+ },
359
+ "indexes": {},
360
+ "foreignKeys": {},
361
+ "compositePrimaryKeys": {},
362
+ "uniqueConstraints": {},
363
+ "policies": {},
364
+ "checkConstraints": {},
365
+ "isRLSEnabled": false
366
+ },
367
+ "public.script_package_storage_config": {
368
+ "name": "script_package_storage_config",
369
+ "schema": "",
370
+ "columns": {
371
+ "id": {
372
+ "name": "id",
373
+ "type": "text",
374
+ "primaryKey": true,
375
+ "notNull": true,
376
+ "default": "'singleton'"
377
+ },
378
+ "active_backend": {
379
+ "name": "active_backend",
380
+ "type": "text",
381
+ "primaryKey": false,
382
+ "notNull": true,
383
+ "default": "'postgres'"
384
+ },
385
+ "migration_status": {
386
+ "name": "migration_status",
387
+ "type": "text",
388
+ "primaryKey": false,
389
+ "notNull": true,
390
+ "default": "'idle'"
391
+ },
392
+ "migration_target": {
393
+ "name": "migration_target",
394
+ "type": "text",
395
+ "primaryKey": false,
396
+ "notNull": false
397
+ },
398
+ "migrated_count": {
399
+ "name": "migrated_count",
400
+ "type": "integer",
401
+ "primaryKey": false,
402
+ "notNull": true,
403
+ "default": 0
404
+ },
405
+ "migration_error": {
406
+ "name": "migration_error",
407
+ "type": "text",
408
+ "primaryKey": false,
409
+ "notNull": false
410
+ },
411
+ "updated_at": {
412
+ "name": "updated_at",
413
+ "type": "timestamp",
414
+ "primaryKey": false,
415
+ "notNull": true,
416
+ "default": "now()"
417
+ }
418
+ },
419
+ "indexes": {},
420
+ "foreignKeys": {},
421
+ "compositePrimaryKeys": {},
422
+ "uniqueConstraints": {},
423
+ "policies": {},
424
+ "checkConstraints": {},
425
+ "isRLSEnabled": false
426
+ },
427
+ "public.script_packages": {
428
+ "name": "script_packages",
429
+ "schema": "",
430
+ "columns": {
431
+ "name": {
432
+ "name": "name",
433
+ "type": "text",
434
+ "primaryKey": true,
435
+ "notNull": true
436
+ },
437
+ "version": {
438
+ "name": "version",
439
+ "type": "text",
440
+ "primaryKey": false,
441
+ "notNull": true
442
+ },
443
+ "enabled": {
444
+ "name": "enabled",
445
+ "type": "boolean",
446
+ "primaryKey": false,
447
+ "notNull": true,
448
+ "default": true
449
+ },
450
+ "added_by": {
451
+ "name": "added_by",
452
+ "type": "text",
453
+ "primaryKey": false,
454
+ "notNull": false
455
+ },
456
+ "added_at": {
457
+ "name": "added_at",
458
+ "type": "timestamp",
459
+ "primaryKey": false,
460
+ "notNull": true,
461
+ "default": "now()"
462
+ },
463
+ "updated_at": {
464
+ "name": "updated_at",
465
+ "type": "timestamp",
466
+ "primaryKey": false,
467
+ "notNull": true,
468
+ "default": "now()"
469
+ }
470
+ },
471
+ "indexes": {},
472
+ "foreignKeys": {},
473
+ "compositePrimaryKeys": {},
474
+ "uniqueConstraints": {},
475
+ "policies": {},
476
+ "checkConstraints": {},
477
+ "isRLSEnabled": false
478
+ }
479
+ },
480
+ "enums": {},
481
+ "schemas": {},
482
+ "sequences": {},
483
+ "roles": {},
484
+ "policies": {},
485
+ "views": {},
486
+ "_meta": {
487
+ "columns": {},
488
+ "schemas": {},
489
+ "tables": {}
490
+ }
491
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1780147687896,
9
+ "tag": "0000_flashy_squadron_supreme",
10
+ "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "7",
15
+ "when": 1780172549655,
16
+ "tag": "0001_flawless_drax",
17
+ "breakpoints": true
18
+ }
19
+ ]
20
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ schema: "./src/schema.ts",
5
+ out: "./drizzle",
6
+ dialect: "postgresql",
7
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@checkstack/script-packages-backend",
3
+ "version": "0.2.0",
4
+ "license": "Elastic-2.0",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "checkstack": {
8
+ "type": "backend"
9
+ },
10
+ "scripts": {
11
+ "typecheck": "tsgo -b",
12
+ "lint": "bun run lint:code",
13
+ "lint:code": "eslint . --max-warnings 0",
14
+ "test": "bun test"
15
+ },
16
+ "dependencies": {
17
+ "@checkstack/backend-api": "0.18.0",
18
+ "@checkstack/common": "0.12.0",
19
+ "@checkstack/script-packages-common": "0.1.0",
20
+ "@checkstack/secrets-common": "0.0.1",
21
+ "@checkstack/secrets-backend": "0.0.1",
22
+ "@orpc/server": "^1.13.2",
23
+ "drizzle-orm": "^0.45.0",
24
+ "zod": "^4.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@checkstack/scripts": "0.3.4",
28
+ "@checkstack/tsconfig": "0.0.7",
29
+ "drizzle-kit": "^0.31.10",
30
+ "typescript": "^5.7.2"
31
+ }
32
+ }
@@ -0,0 +1,47 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, mkdir, writeFile, readFile, rm } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { atomicSymlinkSwap, readCurrentTarget } from "./atomic-symlink";
6
+
7
+ describe("atomicSymlinkSwap", () => {
8
+ let work: string;
9
+ beforeEach(async () => {
10
+ work = await mkdtemp(path.join(tmpdir(), "cs-symlink-"));
11
+ });
12
+ afterEach(async () => {
13
+ await rm(work, { recursive: true, force: true });
14
+ });
15
+
16
+ test("points current at a tree and resolves files through it", async () => {
17
+ const treeA = path.join(work, "trees", "hashA");
18
+ await mkdir(treeA, { recursive: true });
19
+ await writeFile(path.join(treeA, "marker"), "A");
20
+ const current = path.join(work, "current");
21
+
22
+ await atomicSymlinkSwap({ linkPath: current, target: "trees/hashA" });
23
+ expect(await readCurrentTarget(current)).toBe("trees/hashA");
24
+ expect(await readFile(path.join(current, "marker"), "utf8")).toBe("A");
25
+ });
26
+
27
+ test("re-points an existing current to a new tree atomically", async () => {
28
+ const treeA = path.join(work, "trees", "hashA");
29
+ const treeB = path.join(work, "trees", "hashB");
30
+ await mkdir(treeA, { recursive: true });
31
+ await mkdir(treeB, { recursive: true });
32
+ await writeFile(path.join(treeA, "marker"), "A");
33
+ await writeFile(path.join(treeB, "marker"), "B");
34
+ const current = path.join(work, "current");
35
+
36
+ await atomicSymlinkSwap({ linkPath: current, target: "trees/hashA" });
37
+ await atomicSymlinkSwap({ linkPath: current, target: "trees/hashB" });
38
+ expect(await readCurrentTarget(current)).toBe("trees/hashB");
39
+ expect(await readFile(path.join(current, "marker"), "utf8")).toBe("B");
40
+ });
41
+
42
+ test("readCurrentTarget returns undefined when absent", async () => {
43
+ expect(
44
+ await readCurrentTarget(path.join(work, "nope")),
45
+ ).toBeUndefined();
46
+ });
47
+ });
@@ -0,0 +1,66 @@
1
+ import { symlink, rename, rm, readlink } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { markTreeRetired } from "./tree-retirement";
5
+
6
+ /**
7
+ * Atomically point `linkPath` at `target`.
8
+ *
9
+ * Creates a uniquely-named temp symlink in the same directory, then
10
+ * `rename()`s it over `linkPath` - `rename` is atomic on POSIX, so a
11
+ * concurrent reader sees either the old target or the new one, never a
12
+ * half-written link. New runs follow the new symlink; in-flight runs keep
13
+ * resolving against the dir they started on (the old tree is untouched).
14
+ *
15
+ * Records the **retirement time** of the tree being superseded: right at the
16
+ * flip we stamp a `.retired-at` marker into the OLD target's dir (when it
17
+ * differs from the new target). Tree GC keys its grace window on that
18
+ * timestamp rather than the tree dir's mtime, so a long-current tree (ancient
19
+ * mtime) is NOT instantly eligible for deletion the moment it is superseded -
20
+ * which would otherwise yank the rug from under an in-flight run still pinned
21
+ * to it. See {@link import("./tree-retirement")}.
22
+ */
23
+ export async function atomicSymlinkSwap({
24
+ linkPath,
25
+ target,
26
+ now = Date.now(),
27
+ }: {
28
+ linkPath: string;
29
+ target: string;
30
+ /** Injectable clock for the retirement timestamp (deterministic tests). */
31
+ now?: number;
32
+ }): Promise<void> {
33
+ const dir = path.dirname(linkPath);
34
+
35
+ // Stamp the tree that is about to stop being current with its retirement
36
+ // time, BEFORE the flip. Resolved relative to the link's dir (targets are
37
+ // stored relative, e.g. `trees/<hash>`). Best-effort: a read/write failure
38
+ // degrades to the "no marker" path in tree-GC, which RETAINS the tree.
39
+ const oldTarget = await readCurrentTarget(linkPath);
40
+ if (oldTarget && oldTarget !== target) {
41
+ const oldTreeDir = path.isAbsolute(oldTarget)
42
+ ? oldTarget
43
+ : path.join(dir, oldTarget);
44
+ await markTreeRetired({ treeDir: oldTreeDir, at: now });
45
+ }
46
+
47
+ const tmpLink = path.join(dir, `.current-${randomUUID()}`);
48
+ await symlink(target, tmpLink);
49
+ try {
50
+ await rename(tmpLink, linkPath);
51
+ } catch (error) {
52
+ await rm(tmpLink, { force: true }).catch(() => {});
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ /** Resolve the current symlink target, or undefined if it doesn't exist. */
58
+ export async function readCurrentTarget(
59
+ linkPath: string,
60
+ ): Promise<string | undefined> {
61
+ try {
62
+ return await readlink(linkPath);
63
+ } catch {
64
+ return;
65
+ }
66
+ }