@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
package/src/index.ts ADDED
@@ -0,0 +1,594 @@
1
+ import path from "node:path";
2
+ import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
3
+ import { internalSecretsRef } from "@checkstack/secrets-backend";
4
+ import {
5
+ createRegistryTokenStore,
6
+ migrateRegistryTokenToPlatform,
7
+ type RegistryTokenStore,
8
+ } from "./registry-token";
9
+ import {
10
+ pluginMetadata,
11
+ scriptPackagesAccessRules,
12
+ scriptPackagesContract,
13
+ type BlobGcSummary,
14
+ } from "@checkstack/script-packages-common";
15
+ import type { PluginMetadata } from "@checkstack/common";
16
+ import { extractErrorMessage } from "@checkstack/common";
17
+ import { blobStoreExtensionPoint, type BlobStore } from "./blob-store";
18
+ import {
19
+ createBlobStoreRegistry,
20
+ type BlobStoreRegistry,
21
+ } from "./blob-store-registry";
22
+ import { resolveScriptPackagesDir, storePaths } from "./data-dir";
23
+ import {
24
+ createPackageStore,
25
+ createRegistryConfigStore,
26
+ createSizeCapStore,
27
+ createStorageConfigStore,
28
+ createBlobIndexStore,
29
+ createBlobGcStateStore,
30
+ createLockfileHistoryStore,
31
+ } from "./stores";
32
+ import { createBlobGcTrigger } from "./blob-gc-runner";
33
+ import {
34
+ createInstallStateStore,
35
+ createInstallerLock,
36
+ } from "./install-state-store";
37
+ import { runInstallNow } from "./install-controller";
38
+ import {
39
+ runStorageMigration,
40
+ resumeCrashedMigration,
41
+ } from "./storage-migration";
42
+ import { createCentralResolver } from "./resolver";
43
+ import { resolveRegistryRequestConfig } from "./registry-request-config";
44
+ import { createReconcileFsDeps } from "./reconcile-fs";
45
+ import { reconcileToHash } from "./reconciler";
46
+ import { scriptPackagesChangedHook } from "./hooks";
47
+ import { createScriptPackagesRouter } from "./router";
48
+ import { createTypeClosureHttpHandler } from "./type-acquisition-route";
49
+ import { TYPE_ACQUISITION_PATH_PREFIX } from "@checkstack/script-packages-common";
50
+ import * as schema from "./schema";
51
+
52
+ interface EnvStash {
53
+ blobStores: BlobStoreRegistry;
54
+ /**
55
+ * Set in `afterPluginsReady` (the only phase where `emitHook` exists) and
56
+ * called by the installer (wired in `init`) after a successful install.
57
+ * Undefined until `afterPluginsReady` runs.
58
+ */
59
+ emitChanged?: (lockfileHash: string) => Promise<void>;
60
+ /** Registry token store (internal secrets), set in `init`. */
61
+ registryToken?: RegistryTokenStore;
62
+ /**
63
+ * Blob-GC trigger built in `init` (wires stores + the installer lock).
64
+ * Reused by the scheduled recurring job registered in `afterPluginsReady`.
65
+ */
66
+ triggerBlobGc?: () => Promise<BlobGcSummary>;
67
+ }
68
+
69
+ export default createBackendPlugin({
70
+ metadata: pluginMetadata,
71
+
72
+ register(env) {
73
+ const blobStores = createBlobStoreRegistry();
74
+ (env as unknown as EnvStash).blobStores = blobStores;
75
+
76
+ env.registerAccessRules(scriptPackagesAccessRules);
77
+
78
+ env.registerExtensionPoint(blobStoreExtensionPoint, {
79
+ registerBlobStore: (store: BlobStore, _metadata: PluginMetadata) => {
80
+ blobStores.register(store);
81
+ },
82
+ });
83
+
84
+ env.registerInit({
85
+ schema,
86
+ deps: {
87
+ logger: coreServices.logger,
88
+ rpc: coreServices.rpc,
89
+ auth: coreServices.auth,
90
+ advisoryLock: coreServices.advisoryLock,
91
+ queueManager: coreServices.queueManager,
92
+ internalSecrets: internalSecretsRef,
93
+ },
94
+ init: async ({
95
+ logger,
96
+ database,
97
+ rpc,
98
+ auth,
99
+ advisoryLock,
100
+ internalSecrets,
101
+ }) => {
102
+ logger.debug("๐Ÿ“ฆ Initializing Script Packages Backend...");
103
+
104
+ const storeRoot = resolveScriptPackagesDir();
105
+ const packages = createPackageStore(database);
106
+ const registry = createRegistryConfigStore(database);
107
+ const storage = createStorageConfigStore(database);
108
+ const registryToken = createRegistryTokenStore({ internalSecrets });
109
+ (env as unknown as EnvStash).registryToken = registryToken;
110
+ const sizeCap = createSizeCapStore(database);
111
+ const blobIndex = createBlobIndexStore(database);
112
+ const lockfileHistory = createLockfileHistoryStore(database);
113
+ const blobGcState = createBlobGcStateStore(database);
114
+ const installState = createInstallStateStore(database);
115
+ const installerLock = createInstallerLock(advisoryLock);
116
+
117
+ // Whether an install OR storage migration is in flight โ€” the guard
118
+ // both `triggerInstall` (migration check) and the blob GC use.
119
+ const isBusy = async () => {
120
+ const [cfg, state] = await Promise.all([
121
+ storage.get(),
122
+ installState.load(),
123
+ ]);
124
+ return (
125
+ cfg.migrationStatus === "migrating" || state.status === "installing"
126
+ );
127
+ };
128
+
129
+ // Blob GC trigger: shared by the admin `gcBlobs` RPC and the
130
+ // scheduled recurring job. Holds the installer lock for the pass.
131
+ const triggerBlobGc = createBlobGcTrigger({
132
+ installerLock,
133
+ blobStores,
134
+ loadCurrent: async () => {
135
+ const state = await installState.load();
136
+ return {
137
+ lockfileHash: state.lockfileHash,
138
+ manifest: state.manifest,
139
+ };
140
+ },
141
+ recentHistory: (limit) => lockfileHistory.recent(limit),
142
+ pruneHistory: (keep) => lockfileHistory.pruneOlderThan(keep),
143
+ listBlobs: () => blobIndex.listWithMeta(),
144
+ removeBlobRow: (integrity) => blobIndex.remove(integrity),
145
+ isBusy,
146
+ recordRun: (r) => blobGcState.recordRun(r),
147
+ logger,
148
+ });
149
+ (env as unknown as EnvStash).triggerBlobGc = triggerBlobGc;
150
+
151
+ // Build the install orchestration. The resolver + active blob store
152
+ // are resolved lazily at install time so config/registry changes
153
+ // and store-plugin registration order don't matter.
154
+ const triggerInstall = async () => {
155
+ // Same registry + token resolution the live registry-client RPCs
156
+ // use (shared helper) so the install and autocomplete paths can
157
+ // never drift on how they talk to the registry.
158
+ const reqConfig = await resolveRegistryRequestConfig({
159
+ registry,
160
+ registryToken,
161
+ logger,
162
+ });
163
+ const reg = await registry.get();
164
+ const storageConfig = await storage.get();
165
+ const activeBackend = storageConfig.activeBackend;
166
+ if (!blobStores.has(activeBackend)) {
167
+ return {
168
+ started: false,
169
+ reason: `Active blob store "${activeBackend}" is not registered.`,
170
+ };
171
+ }
172
+ const store = blobStores.get(activeBackend);
173
+ const paths = storePaths(storeRoot);
174
+ const resolver = createCentralResolver({
175
+ scratchDir: path.join(paths.root, ".install-scratch"),
176
+ cacheDir: paths.cache,
177
+ registry: {
178
+ registryUrl: reqConfig.registryUrl,
179
+ scopedRegistries: reqConfig.scopedRegistries,
180
+ authToken: reqConfig.authToken,
181
+ },
182
+ });
183
+ return runInstallNow({
184
+ installState,
185
+ installerLock,
186
+ resolver,
187
+ blobStore: {
188
+ id: store.id,
189
+ has: (i) => store.has(i),
190
+ put: (i) => store.put(i),
191
+ },
192
+ blobIndex,
193
+ loadInstallInputs: async () => ({
194
+ packages: await packages.list(),
195
+ ignoreScripts: reg.ignoreScripts,
196
+ }),
197
+ sizeCap: () => sizeCap.get(),
198
+ isMigrationInFlight: async () => {
199
+ const cfg = await storage.get();
200
+ return cfg.migrationStatus === "migrating";
201
+ },
202
+ recordHistory: ({ lockfileHash, manifest }) =>
203
+ lockfileHistory.record({ lockfileHash, manifest }),
204
+ emitChanged: async ({ lockfileHash }) => {
205
+ await (env as unknown as EnvStash).emitChanged?.(lockfileHash);
206
+ },
207
+ logger,
208
+ });
209
+ };
210
+
211
+ // Kick a storage migration in the background. Mutually exclusive
212
+ // with installs via the installer advisory lock (an install refuses
213
+ // while a migration is in flight via `isMigrationInFlight`; this
214
+ // refuses while an install holds the lock). Returns immediately;
215
+ // progress is polled via `getStorageMigrationState`.
216
+ const triggerMigration = async ({ target }: { target: string }) => {
217
+ const current = await storage.get();
218
+ if (current.migrationStatus === "migrating") {
219
+ return {
220
+ started: false,
221
+ reason: "A storage migration is already in progress.",
222
+ };
223
+ }
224
+ if (current.activeBackend === target) {
225
+ return {
226
+ started: false,
227
+ reason: `"${target}" is already the active backend.`,
228
+ };
229
+ }
230
+ if (!blobStores.has(target)) {
231
+ return {
232
+ started: false,
233
+ reason: `Target blob store "${target}" is not registered.`,
234
+ };
235
+ }
236
+ const lock = await installerLock.tryInstallerLock();
237
+ if (!lock) {
238
+ return {
239
+ started: false,
240
+ reason: "An install is in progress; try again once it completes.",
241
+ };
242
+ }
243
+ // Run in the background; release the lock when done.
244
+ void runStorageMigration({
245
+ blobIndex,
246
+ storage,
247
+ getStore: (id) => blobStores.get(id),
248
+ activeBackend: current.activeBackend,
249
+ target,
250
+ logger,
251
+ }).finally(() => {
252
+ void lock.release();
253
+ });
254
+ return { started: true };
255
+ };
256
+
257
+ const router = createScriptPackagesRouter({
258
+ db: database,
259
+ blobStores,
260
+ logger,
261
+ triggerInstall,
262
+ triggerMigration,
263
+ triggerBlobGc,
264
+ registryToken,
265
+ });
266
+ rpc.registerRouter(router, scriptPackagesContract);
267
+
268
+ // Raw, HTTP-cacheable route for editor lazy ATA (package `.d.ts`
269
+ // closures). Served outside oRPC so the response can carry
270
+ // `Cache-Control` (oRPC procedures here can't set response headers).
271
+ // Mounted at `/api/script-packages/types/:hash/:specifier`.
272
+ rpc.registerHttpHandler(
273
+ createTypeClosureHttpHandler({
274
+ auth,
275
+ getLockfileHash: async () => {
276
+ const state = await installState.load();
277
+ return state.lockfileHash;
278
+ },
279
+ storeRoot,
280
+ logger,
281
+ }),
282
+ TYPE_ACQUISITION_PATH_PREFIX,
283
+ );
284
+
285
+ logger.debug("โœ… Script Packages Backend initialized.");
286
+ },
287
+
288
+ afterPluginsReady: async ({
289
+ logger,
290
+ database,
291
+ onHook,
292
+ emitHook,
293
+ advisoryLock,
294
+ queueManager,
295
+ }) => {
296
+ const stash = env as unknown as EnvStash;
297
+ const blobStores = stash.blobStores;
298
+ const storeRoot = resolveScriptPackagesDir();
299
+ const installState = createInstallStateStore(database);
300
+ const installerLock = createInstallerLock(advisoryLock);
301
+ const storage = createStorageConfigStore(database);
302
+ const blobIndex = createBlobIndexStore(database);
303
+
304
+ // One-time, idempotent, parity-verified migration of a legacy
305
+ // inline-ciphertext registry token onto the secrets platform's
306
+ // internal secrets. No-op once migrated (column holds the marker)
307
+ // or when no token is set. Never drops the legacy value until the
308
+ // platform copy reads back identically.
309
+ const registryToken = stash.registryToken;
310
+ if (registryToken) {
311
+ try {
312
+ const registry = createRegistryConfigStore(database);
313
+ const currentRef = await registry.authSecretRef();
314
+ const reg = await registry.get();
315
+ const outcome = await migrateRegistryTokenToPlatform({
316
+ currentRef,
317
+ tokenStore: registryToken,
318
+ rewrite: async (marker) => {
319
+ await registry.set({
320
+ registryUrl: reg.registryUrl,
321
+ scopedRegistries: reg.scopedRegistries,
322
+ ignoreScripts: reg.ignoreScripts,
323
+ authSecretRef: marker,
324
+ });
325
+ },
326
+ });
327
+ if (outcome === "migrated") {
328
+ logger.debug(
329
+ "๐Ÿ” Migrated script-package registry token onto the secrets platform.",
330
+ );
331
+ }
332
+ } catch (error) {
333
+ logger.error(
334
+ `Registry-token migration failed: ${extractErrorMessage(error)}`,
335
+ );
336
+ }
337
+ }
338
+
339
+ // Startup backstop: resume a storage migration that crashed
340
+ // mid-flight. A migration that died leaves `migrationStatus` stuck
341
+ // at "migrating" โ€” which blocks `installNow` AND makes
342
+ // `triggerMigration` refuse to restart it โ€” so nothing would ever
343
+ // unwedge it without operator intervention. `runStorageMigration`
344
+ // is idempotent + resumable (it re-derives its work set from the
345
+ // index, skipping blobs already on the target), so we relaunch it
346
+ // toward the recorded target under the installer-election lock so
347
+ // exactly one pod resumes (and an in-flight install on another pod
348
+ // is mutually excluded). Fire-and-forget; progress is polled.
349
+ try {
350
+ await resumeCrashedMigration({
351
+ loadState: async () => {
352
+ const cfg = await storage.get();
353
+ return {
354
+ migrationStatus: cfg.migrationStatus,
355
+ migrationTarget: cfg.migrationTarget,
356
+ activeBackend: cfg.activeBackend,
357
+ };
358
+ },
359
+ tryLock: () => installerLock.tryInstallerLock(),
360
+ runMigration: ({ target, activeBackend }) =>
361
+ runStorageMigration({
362
+ blobIndex,
363
+ storage,
364
+ getStore: (id) => blobStores.get(id),
365
+ activeBackend,
366
+ target,
367
+ logger,
368
+ }),
369
+ logger,
370
+ });
371
+ } catch (error) {
372
+ logger.error(
373
+ `Storage-migration resume check failed: ${extractErrorMessage(error)}`,
374
+ );
375
+ }
376
+
377
+ // Let the installer (in init's triggerInstall) emit the hook.
378
+ stash.emitChanged = async (lockfileHash: string) => {
379
+ await emitHook(scriptPackagesChangedHook, { lockfileHash });
380
+ };
381
+
382
+ // Reconcile this instance to a desired hash using the shared blob
383
+ // store (delta pull from whichever backend holds each blob).
384
+ const reconcileLocal = async (input: {
385
+ lockfileHash: string;
386
+ manifest: Awaited<ReturnType<typeof installState.load>>["manifest"];
387
+ }) => {
388
+ const deps = createReconcileFsDeps({
389
+ storeRoot,
390
+ logger,
391
+ fetchBlob: async ({ integrity }) => {
392
+ const cfg = await storage.get();
393
+ const active = cfg.activeBackend;
394
+ const res = await blobStores.readWithFallback({
395
+ integrity,
396
+ activeBackendId: active,
397
+ });
398
+ if (!res) {
399
+ throw new Error(
400
+ `Blob ${integrity} not available in any backend.`,
401
+ );
402
+ }
403
+ return res.bytes;
404
+ },
405
+ });
406
+ await reconcileToHash({
407
+ lockfileHash: input.lockfileHash,
408
+ manifest: input.manifest,
409
+ deps,
410
+ });
411
+ };
412
+
413
+ // Broadcast subscription: EVERY core instance reconciles on change
414
+ // (the deliberate inverse of installer-election). Best-effort
415
+ // liveness; the startup backstop below guarantees convergence.
416
+ onHook(
417
+ scriptPackagesChangedHook,
418
+ async ({ lockfileHash }) => {
419
+ const state = await installState.load();
420
+ if (state.lockfileHash === lockfileHash) {
421
+ await reconcileLocal({ lockfileHash, manifest: state.manifest });
422
+ }
423
+ },
424
+ { mode: "broadcast" },
425
+ );
426
+
427
+ // Startup backstop: converge to the durable desired hash regardless
428
+ // of whether this pod ever saw the broadcast. Idempotent (no-op when
429
+ // already at the hash).
430
+ try {
431
+ const state = await installState.load();
432
+ if (state.lockfileHash && state.status === "ready") {
433
+ await reconcileLocal({
434
+ lockfileHash: state.lockfileHash,
435
+ manifest: state.manifest,
436
+ });
437
+ }
438
+ } catch (error) {
439
+ logger.error(
440
+ `Startup script-package reconcile failed: ${extractErrorMessage(error)}`,
441
+ );
442
+ }
443
+
444
+ // Scheduled recurring blob GC: prune unreferenced, past-grace blobs
445
+ // from the active/recorded backends so Postgres/S3 storage is
446
+ // reclaimed. The trigger (built in `init`) holds the installer
447
+ // advisory lock for the pass, so exactly one pod GCs at a time and it
448
+ // is mutually exclusive with installs / migrations. Runs daily; the
449
+ // grace window (default 24h) makes a once-a-day cadence safe.
450
+ const triggerBlobGc = stash.triggerBlobGc;
451
+ if (triggerBlobGc) {
452
+ try {
453
+ const gcQueue = queueManager.getQueue<Record<string, never>>(
454
+ "script-packages-blob-gc",
455
+ );
456
+ await gcQueue.consume(
457
+ async () => {
458
+ const summary = await triggerBlobGc();
459
+ if (summary.ran) {
460
+ logger.debug(
461
+ `Scheduled blob GC: ${summary.deleted} deleted (${summary.bytesReclaimed} bytes), ${summary.keptWithinGrace} kept within grace.`,
462
+ );
463
+ } else {
464
+ logger.debug(
465
+ `Scheduled blob GC skipped: ${summary.reason ?? "unknown"}.`,
466
+ );
467
+ }
468
+ },
469
+ { consumerGroup: "script-packages-blob-gc-worker", maxRetries: 0 },
470
+ );
471
+ await gcQueue.scheduleRecurring(
472
+ {},
473
+ {
474
+ jobId: "script-packages-blob-gc-daily",
475
+ intervalSeconds: 24 * 60 * 60,
476
+ },
477
+ );
478
+ logger.debug("๐Ÿงน Script-packages blob GC scheduled (daily).");
479
+ } catch (error) {
480
+ logger.error(
481
+ `Failed to schedule blob GC: ${extractErrorMessage(error)}`,
482
+ );
483
+ }
484
+ }
485
+
486
+ logger.debug("โœ… Script Packages Backend afterPluginsReady complete.");
487
+ },
488
+ });
489
+ },
490
+ });
491
+
492
+ // โ”€โ”€โ”€ Public surface โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
493
+
494
+ export {
495
+ blobStoreExtensionPoint,
496
+ type BlobStore,
497
+ type BlobStoreExtensionPoint,
498
+ } from "./blob-store";
499
+ export {
500
+ createBlobStoreRegistry,
501
+ type BlobStoreRegistry,
502
+ } from "./blob-store-registry";
503
+ export {
504
+ resolveDataDir,
505
+ resolveScriptPackagesDir,
506
+ storePaths,
507
+ } from "./data-dir";
508
+ export {
509
+ buildDependencies,
510
+ buildStorePackageJson,
511
+ computeLockfileHash,
512
+ sortManifest,
513
+ } from "./lockfile";
514
+ export { renderNpmrc, type NpmrcInput } from "./npmrc";
515
+ export {
516
+ resolveRegistryRequestConfig,
517
+ type RegistryRequestConfig,
518
+ } from "./registry-request-config";
519
+ export {
520
+ searchPackages,
521
+ getPackageVersions,
522
+ registryUrlForName,
523
+ RegistryClientError,
524
+ type PackageSearchResult,
525
+ } from "./registry-client";
526
+ export { parseBunLock, splitSpec } from "./parse-bun-lock";
527
+ export {
528
+ createInstallStateStore,
529
+ createInstallerLock,
530
+ type InstallStateStore,
531
+ type InstallerLock,
532
+ } from "./install-state-store";
533
+ export {
534
+ performInstall,
535
+ type BlobIndex,
536
+ type BlobPublisher,
537
+ type InstallResult,
538
+ type Resolver,
539
+ type ResolvedPackage,
540
+ } from "./install-service";
541
+ export { evaluateSizeCap, type SizeCapVerdict } from "./size-cap";
542
+ export { packDir, unpackInto } from "./cache-archive";
543
+ export { blobSha256, verifyBlobSha256 } from "./blob-hash";
544
+ export { computeMissingBlobs } from "./reconcile-diff";
545
+ export { atomicSymlinkSwap, readCurrentTarget } from "./atomic-symlink";
546
+ export {
547
+ reconcileToHash,
548
+ type ReconcileDeps,
549
+ type ReconcileResult,
550
+ } from "./reconciler";
551
+ export { scriptPackagesChangedHook } from "./hooks";
552
+ export {
553
+ createCentralResolver,
554
+ type CentralResolverOptions,
555
+ } from "./resolver";
556
+ export { createReconcileFsDeps } from "./reconcile-fs";
557
+ export { findCacheEntry, type CacheEntryLocation } from "./cache-layout";
558
+ export {
559
+ resolvePackageTypeClosure,
560
+ typesPackageDirName,
561
+ extractReferences,
562
+ } from "./package-types";
563
+ export { createTypeClosureHttpHandler } from "./type-acquisition-route";
564
+ export {
565
+ resolveResolutionRoot,
566
+ resolveResolutionRootForHost,
567
+ resolveResolutionRootFromStore,
568
+ type ResolutionRootStatus,
569
+ } from "./resolution-root";
570
+ export {
571
+ runInstallNow,
572
+ type InstallControllerDeps,
573
+ type InstallOutcome,
574
+ } from "./install-controller";
575
+ export {
576
+ runStorageMigration,
577
+ resumeCrashedMigration,
578
+ type StorageMigrationDeps,
579
+ type StorageMigrationResult,
580
+ type ResumeCrashedMigrationDeps,
581
+ type ResumeCrashedMigrationResult,
582
+ type MigrationStateSnapshot,
583
+ } from "./storage-migration";
584
+ export { runBlobGc, type BlobGcDeps, type GcBlob } from "./blob-gc";
585
+ export {
586
+ createBlobGcTrigger,
587
+ type BlobGcRunnerDeps,
588
+ } from "./blob-gc-runner";
589
+ export { sweepTreeGc, type TreeGcResult } from "./tree-gc";
590
+ export {
591
+ createLockfileHistoryStore,
592
+ createBlobGcStateStore,
593
+ } from "./stores";
594
+ export * as schema from "./schema";