@supatype/cli 0.1.5 → 0.1.7

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 (247) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +143 -93
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/deno.d.ts +52 -0
  5. package/dist/augmentation-generator.d.ts +22 -0
  6. package/dist/augmentation-generator.d.ts.map +1 -1
  7. package/dist/augmentation-generator.js +43 -7
  8. package/dist/augmentation-generator.js.map +1 -1
  9. package/dist/binary-cache.d.ts +14 -0
  10. package/dist/binary-cache.d.ts.map +1 -1
  11. package/dist/binary-cache.js +33 -13
  12. package/dist/binary-cache.js.map +1 -1
  13. package/dist/cache-clean.d.ts +15 -0
  14. package/dist/cache-clean.d.ts.map +1 -0
  15. package/dist/cache-clean.js +76 -0
  16. package/dist/cache-clean.js.map +1 -0
  17. package/dist/cache-pins.d.ts +51 -0
  18. package/dist/cache-pins.d.ts.map +1 -0
  19. package/dist/cache-pins.js +184 -0
  20. package/dist/cache-pins.js.map +1 -0
  21. package/dist/cli-install-method.d.ts +19 -0
  22. package/dist/cli-install-method.d.ts.map +1 -0
  23. package/dist/cli-install-method.js +60 -0
  24. package/dist/cli-install-method.js.map +1 -0
  25. package/dist/cli.d.ts.map +1 -1
  26. package/dist/cli.js +6 -0
  27. package/dist/cli.js.map +1 -1
  28. package/dist/cloud-credentials.d.ts +34 -0
  29. package/dist/cloud-credentials.d.ts.map +1 -0
  30. package/dist/cloud-credentials.js +139 -0
  31. package/dist/cloud-credentials.js.map +1 -0
  32. package/dist/commands/admin.d.ts +11 -0
  33. package/dist/commands/admin.d.ts.map +1 -1
  34. package/dist/commands/admin.js +131 -22
  35. package/dist/commands/admin.js.map +1 -1
  36. package/dist/commands/cache.d.ts.map +1 -1
  37. package/dist/commands/cache.js +67 -71
  38. package/dist/commands/cache.js.map +1 -1
  39. package/dist/commands/cloud.d.ts.map +1 -1
  40. package/dist/commands/cloud.js +32 -8
  41. package/dist/commands/cloud.js.map +1 -1
  42. package/dist/commands/db.d.ts +1 -0
  43. package/dist/commands/db.d.ts.map +1 -1
  44. package/dist/commands/db.js +147 -1
  45. package/dist/commands/db.js.map +1 -1
  46. package/dist/commands/dev.d.ts.map +1 -1
  47. package/dist/commands/dev.js +50 -10
  48. package/dist/commands/dev.js.map +1 -1
  49. package/dist/commands/doctor.d.ts +28 -0
  50. package/dist/commands/doctor.d.ts.map +1 -1
  51. package/dist/commands/doctor.js +60 -3
  52. package/dist/commands/doctor.js.map +1 -1
  53. package/dist/commands/functions.d.ts +7 -0
  54. package/dist/commands/functions.d.ts.map +1 -1
  55. package/dist/commands/functions.js +40 -6
  56. package/dist/commands/functions.js.map +1 -1
  57. package/dist/commands/generate.d.ts.map +1 -1
  58. package/dist/commands/generate.js +5 -1
  59. package/dist/commands/generate.js.map +1 -1
  60. package/dist/commands/hooks.d.ts +12 -0
  61. package/dist/commands/hooks.d.ts.map +1 -0
  62. package/dist/commands/hooks.js +118 -0
  63. package/dist/commands/hooks.js.map +1 -0
  64. package/dist/commands/init.d.ts.map +1 -1
  65. package/dist/commands/init.js +41 -7
  66. package/dist/commands/init.js.map +1 -1
  67. package/dist/commands/internal.d.ts +6 -0
  68. package/dist/commands/internal.d.ts.map +1 -0
  69. package/dist/commands/internal.js +22 -0
  70. package/dist/commands/internal.js.map +1 -0
  71. package/dist/commands/keys.d.ts.map +1 -1
  72. package/dist/commands/keys.js +2 -0
  73. package/dist/commands/keys.js.map +1 -1
  74. package/dist/commands/link-helpers.d.ts.map +1 -1
  75. package/dist/commands/link-helpers.js +8 -2
  76. package/dist/commands/link-helpers.js.map +1 -1
  77. package/dist/commands/login.d.ts +7 -0
  78. package/dist/commands/login.d.ts.map +1 -0
  79. package/dist/commands/login.js +110 -0
  80. package/dist/commands/login.js.map +1 -0
  81. package/dist/commands/push.d.ts.map +1 -1
  82. package/dist/commands/push.js +110 -7
  83. package/dist/commands/push.js.map +1 -1
  84. package/dist/commands/self-update.d.ts +7 -0
  85. package/dist/commands/self-update.d.ts.map +1 -1
  86. package/dist/commands/self-update.js +18 -11
  87. package/dist/commands/self-update.js.map +1 -1
  88. package/dist/config.d.ts.map +1 -1
  89. package/dist/config.js +18 -3
  90. package/dist/config.js.map +1 -1
  91. package/dist/db-preflight.d.ts +78 -0
  92. package/dist/db-preflight.d.ts.map +1 -0
  93. package/dist/db-preflight.js +452 -0
  94. package/dist/db-preflight.js.map +1 -0
  95. package/dist/dev-compose.d.ts +1 -1
  96. package/dist/dev-compose.d.ts.map +1 -1
  97. package/dist/dev-compose.js +129 -72
  98. package/dist/dev-compose.js.map +1 -1
  99. package/dist/env-file.d.ts +40 -2
  100. package/dist/env-file.d.ts.map +1 -1
  101. package/dist/env-file.js +132 -11
  102. package/dist/env-file.js.map +1 -1
  103. package/dist/field-masking-tier.d.ts +50 -0
  104. package/dist/field-masking-tier.d.ts.map +1 -0
  105. package/dist/field-masking-tier.js +91 -0
  106. package/dist/field-masking-tier.js.map +1 -0
  107. package/dist/functions-deno-types.d.ts +24 -0
  108. package/dist/functions-deno-types.d.ts.map +1 -0
  109. package/dist/functions-deno-types.js +155 -0
  110. package/dist/functions-deno-types.js.map +1 -0
  111. package/dist/hook-upload.d.ts +30 -0
  112. package/dist/hook-upload.d.ts.map +1 -0
  113. package/dist/hook-upload.js +86 -0
  114. package/dist/hook-upload.js.map +1 -0
  115. package/dist/hooks-generator.d.ts +8 -0
  116. package/dist/hooks-generator.d.ts.map +1 -0
  117. package/dist/hooks-generator.js +332 -0
  118. package/dist/hooks-generator.js.map +1 -0
  119. package/dist/link.d.ts +4 -0
  120. package/dist/link.d.ts.map +1 -1
  121. package/dist/link.js +2 -0
  122. package/dist/link.js.map +1 -1
  123. package/dist/local-secrets.d.ts +71 -0
  124. package/dist/local-secrets.d.ts.map +1 -0
  125. package/dist/local-secrets.js +100 -0
  126. package/dist/local-secrets.js.map +1 -0
  127. package/dist/model-hooks.d.ts +73 -0
  128. package/dist/model-hooks.d.ts.map +1 -0
  129. package/dist/model-hooks.js +211 -0
  130. package/dist/model-hooks.js.map +1 -0
  131. package/dist/postgres-ctl.d.ts +11 -0
  132. package/dist/postgres-ctl.d.ts.map +1 -1
  133. package/dist/postgres-ctl.js +25 -2
  134. package/dist/postgres-ctl.js.map +1 -1
  135. package/dist/project-config.d.ts +121 -1
  136. package/dist/project-config.d.ts.map +1 -1
  137. package/dist/project-config.js +150 -1
  138. package/dist/project-config.js.map +1 -1
  139. package/dist/resolve-target.d.ts +4 -0
  140. package/dist/resolve-target.d.ts.map +1 -1
  141. package/dist/resolve-target.js +69 -73
  142. package/dist/resolve-target.js.map +1 -1
  143. package/dist/schema-ast-v2.d.ts +2 -1
  144. package/dist/schema-ast-v2.d.ts.map +1 -1
  145. package/dist/schema-ast-v2.js +4 -2
  146. package/dist/schema-ast-v2.js.map +1 -1
  147. package/dist/schema-push-lock.d.ts.map +1 -1
  148. package/dist/schema-push-lock.js +43 -16
  149. package/dist/schema-push-lock.js.map +1 -1
  150. package/dist/self-host-compose.d.ts +24 -0
  151. package/dist/self-host-compose.d.ts.map +1 -1
  152. package/dist/self-host-compose.js +301 -69
  153. package/dist/self-host-compose.js.map +1 -1
  154. package/dist/service-role-check.d.ts +21 -0
  155. package/dist/service-role-check.d.ts.map +1 -0
  156. package/dist/service-role-check.js +85 -0
  157. package/dist/service-role-check.js.map +1 -0
  158. package/dist/target-client.d.ts +17 -0
  159. package/dist/target-client.d.ts.map +1 -1
  160. package/dist/target-client.js +69 -17
  161. package/dist/target-client.js.map +1 -1
  162. package/dist/type-extractor.d.ts.map +1 -1
  163. package/dist/type-extractor.js +736 -32
  164. package/dist/type-extractor.js.map +1 -1
  165. package/dist/ui/runtime/command-chrome.js +1 -1
  166. package/dist/ui/runtime/command-chrome.js.map +1 -1
  167. package/dist/ui/runtime/stdin-after-ink.d.ts.map +1 -1
  168. package/dist/ui/runtime/stdin-after-ink.js +3 -1
  169. package/dist/ui/runtime/stdin-after-ink.js.map +1 -1
  170. package/package.json +4 -1
  171. package/scripts/generate-homebrew-formula.mjs +121 -0
  172. package/src/augmentation-generator.ts +46 -9
  173. package/src/binary-cache.ts +47 -15
  174. package/src/cache-clean.ts +81 -0
  175. package/src/cache-pins.ts +243 -0
  176. package/src/cli-install-method.ts +67 -0
  177. package/src/cli.ts +6 -0
  178. package/src/cloud-credentials.ts +183 -0
  179. package/src/commands/admin.ts +153 -28
  180. package/src/commands/cache.ts +90 -75
  181. package/src/commands/cloud.ts +32 -8
  182. package/src/commands/db.ts +162 -1
  183. package/src/commands/dev.ts +57 -8
  184. package/src/commands/doctor.ts +69 -3
  185. package/src/commands/functions.ts +42 -5
  186. package/src/commands/generate.ts +5 -1
  187. package/src/commands/hooks.ts +138 -0
  188. package/src/commands/init.ts +42 -8
  189. package/src/commands/internal.ts +23 -0
  190. package/src/commands/keys.ts +2 -0
  191. package/src/commands/link-helpers.ts +9 -2
  192. package/src/commands/login.ts +127 -0
  193. package/src/commands/push.ts +124 -7
  194. package/src/commands/self-update.ts +85 -13
  195. package/src/config.ts +22 -3
  196. package/src/db-preflight.ts +575 -0
  197. package/src/dev-compose.ts +144 -73
  198. package/src/env-file.ts +157 -11
  199. package/src/field-masking-tier.ts +107 -0
  200. package/src/functions-deno-types.ts +188 -0
  201. package/src/hook-upload.ts +107 -0
  202. package/src/hooks-generator.ts +352 -0
  203. package/src/link.ts +7 -0
  204. package/src/local-secrets.ts +104 -0
  205. package/src/model-hooks.ts +260 -0
  206. package/src/postgres-ctl.ts +29 -2
  207. package/src/project-config.ts +236 -2
  208. package/src/resolve-target.ts +113 -68
  209. package/src/schema-ast-v2.ts +5 -2
  210. package/src/schema-push-lock.ts +56 -17
  211. package/src/self-host-compose.ts +344 -68
  212. package/src/service-role-check.ts +111 -0
  213. package/src/target-client.ts +105 -18
  214. package/src/type-extractor.ts +883 -28
  215. package/src/ui/runtime/command-chrome.ts +1 -1
  216. package/src/ui/runtime/stdin-after-ink.ts +3 -1
  217. package/tests/access-composition.test.ts +667 -0
  218. package/tests/admin-studio-membership.test.ts +121 -0
  219. package/tests/api-schemas.test.ts +83 -0
  220. package/tests/archive-name-alias.test.ts +42 -0
  221. package/tests/augmentation-generator.test.ts +72 -4
  222. package/tests/cache-clean.test.ts +206 -0
  223. package/tests/cli-help.test.ts +3 -0
  224. package/tests/cli-install-method.test.ts +66 -0
  225. package/tests/command-chrome-coverage.test.ts +31 -26
  226. package/tests/command-chrome-wrap.test.ts +1 -0
  227. package/tests/db-preflight.test.ts +72 -0
  228. package/tests/doctor-render.test.ts +62 -0
  229. package/tests/doctor-service-role.test.ts +55 -0
  230. package/tests/env-file.test.ts +121 -0
  231. package/tests/external-database-compose.test.ts +184 -0
  232. package/tests/external-database-config.test.ts +154 -0
  233. package/tests/field-masking-tier.test.ts +184 -0
  234. package/tests/functions-deno-types.test.ts +197 -0
  235. package/tests/homebrew-formula.test.ts +58 -0
  236. package/tests/hook-upload.test.ts +125 -0
  237. package/tests/hooks-generator.test.ts +212 -0
  238. package/tests/init.test.ts +3 -0
  239. package/tests/local-secrets.test.ts +92 -0
  240. package/tests/model-hooks.test.ts +322 -0
  241. package/tests/runtime-contract.test.ts +113 -1
  242. package/tests/service-role-check.test.ts +109 -0
  243. package/tests/service-role-routes.test.ts +51 -0
  244. package/tests/target-client-refresh.test.ts +94 -0
  245. package/tests/type-extractor.test.ts +187 -0
  246. package/tsconfig.tsbuildinfo +1 -1
  247. package/tests/engine-distribution.test.ts +0 -418
@@ -196,6 +196,26 @@ function binaryName(component: Component, version: string, platform: PlatformId)
196
196
  }
197
197
  }
198
198
 
199
+ /**
200
+ * Names a published archive may carry, most-canonical first.
201
+ *
202
+ * `supatype-postgres` published the Intel macOS archive as `darwin-x86_64` while every other platform
203
+ * used `amd64` — the spelling the CLI derives from `process.arch === "x64"`. Releases already on the CDN
204
+ * carry the old name, so both are accepted and the *signed* checksums manifest decides which exists.
205
+ * Nothing is guessed: the URL and the hash always come from the same manifest entry.
206
+ */
207
+ export function archiveNameCandidates(
208
+ component: Component,
209
+ version: string,
210
+ platform: PlatformId,
211
+ ): string[] {
212
+ const canonical = binaryName(component, version, platform)
213
+ if (component === "postgres" && platform.os === "darwin" && platform.arch === "amd64") {
214
+ return [canonical, canonical.replace("darwin-amd64", "darwin-x86_64")]
215
+ }
216
+ return [canonical]
217
+ }
218
+
199
219
  // ---------------------------------------------------------------------------
200
220
  // Platform detection
201
221
  // ---------------------------------------------------------------------------
@@ -390,7 +410,6 @@ export async function download(
390
410
  return destPath
391
411
  }
392
412
 
393
- const binaryUrl = `${CDN_BASE}${CDN_PATHS[component](version, platform)}`
394
413
  const checksumsUrl = `${CDN_BASE}${checksumsDirPath(component, version)}`
395
414
  const minisigUrl = `${checksumsUrl}.minisig`
396
415
 
@@ -399,15 +418,20 @@ export async function download(
399
418
  const tmpPath = destPath + ".tmp"
400
419
  try {
401
420
  // ── Fetch checksums + optional minisig (retried on transient failures) ───
402
- const expectedChecksum = await withRetry(() =>
403
- fetchChecksums(checksumsUrl, minisigUrl, name),
421
+ const match = await withRetry(() =>
422
+ fetchChecksums(checksumsUrl, minisigUrl, archiveNameCandidates(component, version, platform)),
404
423
  )
405
424
 
425
+ // The directory from the path template, the filename from the manifest: a release published under
426
+ // a legacy name is fetched under that name rather than one the CLI assumed.
427
+ const cdnDir = CDN_PATHS[component](version, platform).replace(/\/[^/]+$/, "")
428
+ const binaryUrl = `${CDN_BASE}${cdnDir}/${match.filename}`
429
+
406
430
  // ── Stream-download binary with progress (retried on transient failures) ─
407
431
  await withRetry(() => streamToFileWithProgress(binaryUrl, tmpPath))
408
432
 
409
433
  // ── Verify SHA256 ────────────────────────────────────────────────────────
410
- await verifyChecksum(tmpPath, expectedChecksum, component)
434
+ await verifyChecksum(tmpPath, match.checksum, component)
411
435
 
412
436
  writeFileSync(destPath, readFileSync(tmpPath))
413
437
 
@@ -440,8 +464,8 @@ export async function download(
440
464
  async function fetchChecksums(
441
465
  checksumsUrl: string,
442
466
  minisigUrl: string,
443
- binaryFilename: string,
444
- ): Promise<string> {
467
+ binaryFilenames: string | string[],
468
+ ): Promise<ChecksumMatch> {
445
469
  const csResp = await fetch(checksumsUrl)
446
470
  if (!csResp.ok) {
447
471
  throw new Error(`Failed to fetch checksums from ${checksumsUrl}: HTTP ${csResp.status}`)
@@ -458,7 +482,7 @@ async function fetchChecksums(
458
482
  "[supatype] \u26a0 SUPATYPE_ALLOW_UNVERIFIED_DOWNLOADS=1 — no minisign public " +
459
483
  "key configured; verifying SHA256 only (authenticity NOT checked).",
460
484
  )
461
- return extractChecksum(checksumsText, binaryFilename)
485
+ return extractChecksum(checksumsText, binaryFilenames)
462
486
  }
463
487
  throw new Error(
464
488
  "No minisign public key configured — cannot verify release authenticity.\n" +
@@ -479,7 +503,7 @@ async function fetchChecksums(
479
503
  const sigText = await sigResp.text()
480
504
  verifyMinisign(Buffer.from(checksumsText, "utf8"), sigText, pubKey)
481
505
 
482
- return extractChecksum(checksumsText, binaryFilename)
506
+ return extractChecksum(checksumsText, binaryFilenames)
483
507
  }
484
508
 
485
509
  // ---------------------------------------------------------------------------
@@ -568,20 +592,28 @@ export function verifyMinisign(fileBytes: Buffer, sigFileContent: string, pubKey
568
592
  * Extract the SHA256 hash for `filename` from a checksums.sha256 file.
569
593
  * Format: `<hash> <filename>` (sha256sum output, two spaces).
570
594
  */
571
- function extractChecksum(checksumsText: string, filename: string): string {
572
- const target = basename(filename)
573
- for (const line of checksumsText.split("\n")) {
574
- const parts = line.trim().split(/\s+/)
575
- if (parts.length >= 2 && parts[1] === target) {
576
- return parts[0]!
595
+ function extractChecksum(checksumsText: string, filenames: string | string[]): ChecksumMatch {
596
+ const targets = (Array.isArray(filenames) ? filenames : [filenames]).map((f) => basename(f))
597
+ for (const target of targets) {
598
+ for (const line of checksumsText.split("\n")) {
599
+ const parts = line.trim().split(/\s+/)
600
+ if (parts.length >= 2 && parts[1] === target) {
601
+ return { filename: target, checksum: parts[0]! }
602
+ }
577
603
  }
578
604
  }
579
605
  throw new Error(
580
- `Checksum not found for "${target}" in checksums.sha256.\n` +
606
+ `Checksum not found for ${targets.map((t) => `"${t}"`).join(" or ")} in checksums.sha256.\n` +
581
607
  "The checksums file may be from a different release.",
582
608
  )
583
609
  }
584
610
 
611
+ /** Which published name matched, and its hash — both from the same signed manifest entry. */
612
+ export interface ChecksumMatch {
613
+ filename: string
614
+ checksum: string
615
+ }
616
+
585
617
  // ---------------------------------------------------------------------------
586
618
  // Streaming download with progress bar
587
619
  // ---------------------------------------------------------------------------
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Helpers for `supatype cache list` / `cache clean` (shared sizing + formatting).
3
+ */
4
+
5
+ import { readdirSync, statSync } from "node:fs"
6
+ import { join } from "node:path"
7
+ import { BINARY_COMPONENTS, type Component } from "./components.js"
8
+ import { cacheRoot } from "./binary-cache.js"
9
+ import { isVersionPinned, loadPinnedVersionsForProjects, resolveCacheProjectDirs } from "./cache-pins.js"
10
+
11
+ export function dirSize(dir: string): number {
12
+ let total = 0
13
+ try {
14
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
15
+ const full = join(dir, entry.name)
16
+ if (entry.isDirectory()) {
17
+ total += dirSize(full)
18
+ } else {
19
+ total += statSync(full).size
20
+ }
21
+ }
22
+ } catch {
23
+ // skip unreadable
24
+ }
25
+ return total
26
+ }
27
+
28
+ export function formatBytes(bytes: number): string {
29
+ if (bytes < 1024) return `${bytes} B`
30
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
31
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
32
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`
33
+ }
34
+
35
+ export function listCacheEntries(projectDirs: string[]): Array<{
36
+ component: Component
37
+ version: string
38
+ bytes: number
39
+ pinned: boolean
40
+ }> {
41
+ const root = cacheRoot()
42
+ const { protectedVersions } = loadPinnedVersionsForProjects(projectDirs)
43
+ const entries: Array<{ component: Component; version: string; bytes: number; pinned: boolean }> = []
44
+
45
+ for (const component of BINARY_COMPONENTS) {
46
+ const compDir = join(root, component)
47
+ if (!statExists(compDir)) continue
48
+
49
+ for (const version of readdirSync(compDir)) {
50
+ const vDir = join(compDir, version)
51
+ if (!statIsDir(vDir)) continue
52
+ entries.push({
53
+ component,
54
+ version,
55
+ bytes: dirSize(vDir),
56
+ pinned: protectedVersions.get(component)?.has(version) ?? false,
57
+ })
58
+ }
59
+ }
60
+
61
+ return entries
62
+ }
63
+
64
+ function statExists(path: string): boolean {
65
+ try {
66
+ statSync(path)
67
+ return true
68
+ } catch {
69
+ return false
70
+ }
71
+ }
72
+
73
+ function statIsDir(path: string): boolean {
74
+ try {
75
+ return statSync(path).isDirectory()
76
+ } catch {
77
+ return false
78
+ }
79
+ }
80
+
81
+ export { BINARY_COMPONENTS, resolveCacheProjectDirs }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Pin-aware cache eviction — collect `versions.*` pins across local projects (C13).
3
+ */
4
+
5
+ import { existsSync, readdirSync, rmSync, statSync } from "node:fs"
6
+ import { join, resolve } from "node:path"
7
+ import { BINARY_COMPONENTS, type Component } from "./components.js"
8
+ import { cachePath, cacheRoot, pinnedVersion, VERSION_PIN_LOCAL } from "./binary-cache.js"
9
+ import { loadConfig } from "./config.js"
10
+ import type { SupatypeProjectConfig } from "./project-config.js"
11
+
12
+ const CONFIG_FILENAMES = ["supatype.config.ts", "supatype.config.js", "supatype.config.mjs"] as const
13
+
14
+ export interface PinSource {
15
+ component: Component
16
+ version: string
17
+ projectDir: string
18
+ projectName: string
19
+ }
20
+
21
+ export interface CleanCacheEntry {
22
+ component: Component
23
+ version: string
24
+ bytes: number
25
+ }
26
+
27
+ export interface CleanCacheResult {
28
+ removed: CleanCacheEntry[]
29
+ skipped: Array<CleanCacheEntry & { reason: string }>
30
+ warned: string[]
31
+ }
32
+
33
+ /** Union pinned semver versions from one or more loaded configs. */
34
+ export function pinnedVersionsFromConfigs(
35
+ entries: Array<{ projectDir: string; config: SupatypeProjectConfig }>,
36
+ ): { protectedVersions: Map<Component, Set<string>>; sources: PinSource[] } {
37
+ const protectedVersions = new Map<Component, Set<string>>()
38
+ const sources: PinSource[] = []
39
+
40
+ for (const { projectDir, config } of entries) {
41
+ const projectName = config.project?.name?.trim() || projectDir
42
+ for (const component of BINARY_COMPONENTS) {
43
+ const version = pinnedVersion(component, config)
44
+ if (!version || version === VERSION_PIN_LOCAL) continue
45
+ let set = protectedVersions.get(component)
46
+ if (!set) {
47
+ set = new Set<string>()
48
+ protectedVersions.set(component, set)
49
+ }
50
+ set.add(version)
51
+ sources.push({ component, version, projectDir, projectName })
52
+ }
53
+ }
54
+
55
+ return { protectedVersions, sources }
56
+ }
57
+
58
+ function hasSupatypeConfig(dir: string): boolean {
59
+ return CONFIG_FILENAMES.some((name) => existsSync(join(dir, name)))
60
+ }
61
+
62
+ /** Walk `root` up to `maxDepth` looking for directories that contain a Supatype config file. */
63
+ export function findSupatypeProjectDirs(root: string, maxDepth = 4): string[] {
64
+ const absRoot = resolve(root)
65
+ if (!existsSync(absRoot)) return []
66
+
67
+ const found: string[] = []
68
+ const queue: Array<{ dir: string; depth: number }> = [{ dir: absRoot, depth: 0 }]
69
+
70
+ while (queue.length > 0) {
71
+ const item = queue.shift()
72
+ if (!item) break
73
+ const { dir, depth } = item
74
+
75
+ if (hasSupatypeConfig(dir)) {
76
+ found.push(dir)
77
+ continue
78
+ }
79
+
80
+ if (depth >= maxDepth) continue
81
+
82
+ let entries
83
+ try {
84
+ entries = readdirSync(dir, { withFileTypes: true })
85
+ } catch {
86
+ continue
87
+ }
88
+
89
+ for (const entry of entries) {
90
+ if (!entry.isDirectory()) continue
91
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === ".supatype") continue
92
+ queue.push({ dir: join(dir, entry.name), depth: depth + 1 })
93
+ }
94
+ }
95
+
96
+ return found
97
+ }
98
+
99
+ /** Resolve project directories whose `versions` pins should protect cache entries. */
100
+ export function resolveCacheProjectDirs(cwd: string, extraProjects: string[]): string[] {
101
+ const dirs = new Set<string>()
102
+
103
+ for (const raw of extraProjects) {
104
+ const abs = resolve(raw)
105
+ if (hasSupatypeConfig(abs)) dirs.add(abs)
106
+ }
107
+
108
+ if (hasSupatypeConfig(cwd)) dirs.add(resolve(cwd))
109
+
110
+ const envRoots = process.env.SUPATYPE_CACHE_PROJECT_ROOTS?.trim()
111
+ if (envRoots) {
112
+ for (const segment of envRoots.split(",")) {
113
+ const root = segment.trim()
114
+ if (!root) continue
115
+ for (const dir of findSupatypeProjectDirs(root)) {
116
+ dirs.add(dir)
117
+ }
118
+ }
119
+ }
120
+
121
+ return [...dirs]
122
+ }
123
+
124
+ export function loadPinnedVersionsForProjects(
125
+ projectDirs: string[],
126
+ ): { protectedVersions: Map<Component, Set<string>>; sources: PinSource[]; loadWarnings: string[] } {
127
+ const configs: Array<{ projectDir: string; config: SupatypeProjectConfig }> = []
128
+ const loadWarnings: string[] = []
129
+
130
+ for (const projectDir of projectDirs) {
131
+ try {
132
+ configs.push({ projectDir, config: loadConfig(projectDir) })
133
+ } catch (err) {
134
+ const message = err instanceof Error ? err.message : String(err)
135
+ loadWarnings.push(`${projectDir}: ${message}`)
136
+ }
137
+ }
138
+
139
+ const { protectedVersions, sources } = pinnedVersionsFromConfigs(configs)
140
+ return { protectedVersions, sources, loadWarnings }
141
+ }
142
+
143
+ export function listCachedVersions(component: Component): string[] {
144
+ const compDir = join(cacheRoot(), component)
145
+ if (!existsSync(compDir)) return []
146
+ return readdirSync(compDir).filter((name) => {
147
+ try {
148
+ return statSync(join(compDir, name)).isDirectory()
149
+ } catch {
150
+ return false
151
+ }
152
+ })
153
+ }
154
+
155
+ function pinReason(
156
+ component: Component,
157
+ version: string,
158
+ sources: PinSource[],
159
+ ): string | undefined {
160
+ const matches = sources.filter((s) => s.component === component && s.version === version)
161
+ if (matches.length === 0) return undefined
162
+ const names = [...new Set(matches.map((m) => m.projectName))]
163
+ return `pinned by ${names.join(", ")}`
164
+ }
165
+
166
+ function dirSize(dir: string): number {
167
+ let total = 0
168
+ try {
169
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
170
+ const full = join(dir, entry.name)
171
+ if (entry.isDirectory()) {
172
+ total += dirSize(full)
173
+ } else {
174
+ total += statSync(full).size
175
+ }
176
+ }
177
+ } catch {
178
+ // skip unreadable
179
+ }
180
+ return total
181
+ }
182
+
183
+ export interface CleanCachedBinariesOptions {
184
+ components: Component[]
185
+ version?: string
186
+ force?: boolean
187
+ dryRun?: boolean
188
+ projectDirs: string[]
189
+ }
190
+
191
+ export function cleanCachedBinaries(options: CleanCachedBinariesOptions): CleanCacheResult {
192
+ const { protectedVersions, sources, loadWarnings } = loadPinnedVersionsForProjects(options.projectDirs)
193
+ const result: CleanCacheResult = { removed: [], skipped: [], warned: [...loadWarnings] }
194
+
195
+ const force = options.force === true
196
+ const dryRun = options.dryRun === true
197
+ const explicitVersion = options.version?.trim()
198
+
199
+ for (const component of options.components) {
200
+ const versions = explicitVersion ? [explicitVersion] : listCachedVersions(component)
201
+
202
+ for (const version of versions) {
203
+ const vDir = cachePath(component, version)
204
+ if (!existsSync(vDir)) {
205
+ if (explicitVersion) continue
206
+ continue
207
+ }
208
+
209
+ const bytes = dirSize(vDir)
210
+ const entry: CleanCacheEntry = { component, version, bytes }
211
+ const pinned = protectedVersions.get(component)?.has(version) ?? false
212
+
213
+ if (!force && !explicitVersion && pinned) {
214
+ const reason = pinReason(component, version, sources) ?? "pinned"
215
+ result.skipped.push({ ...entry, reason })
216
+ continue
217
+ }
218
+
219
+ if (!force && explicitVersion && pinned) {
220
+ const reason = pinReason(component, version, sources) ?? "pinned"
221
+ result.warned.push(
222
+ `${component}@${version} is ${reason} — removing anyway (explicit version)`,
223
+ )
224
+ }
225
+
226
+ if (!dryRun) {
227
+ rmSync(vDir, { recursive: true, force: true })
228
+ }
229
+ result.removed.push(entry)
230
+ }
231
+ }
232
+
233
+ return result
234
+ }
235
+
236
+ export function isVersionPinned(
237
+ component: Component,
238
+ version: string,
239
+ projectDirs: string[],
240
+ ): boolean {
241
+ const { protectedVersions } = loadPinnedVersionsForProjects(projectDirs)
242
+ return protectedVersions.get(component)?.has(version) ?? false
243
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Detect how the supatype CLI was installed (npm, Homebrew, standalone CDN).
3
+ */
4
+
5
+ export type CliInstallMethod = "npm" | "homebrew" | "standalone"
6
+
7
+ function normalizePath(path: string): string {
8
+ return path.replace(/\\/g, "/")
9
+ }
10
+
11
+ /** The running binary sits in an npm or workspace layout. */
12
+ function hasNpmInstallPath(): boolean {
13
+ const main = normalizePath(process.argv[1] ?? "")
14
+ return (
15
+ main.includes("node_modules") ||
16
+ main.includes("/dist/cli") ||
17
+ main.includes("/bin/supatype.js")
18
+ )
19
+ }
20
+
21
+ /** Launched through an npm/pnpm/yarn script, whatever the binary's own layout. */
22
+ function ranUnderPackageManager(): boolean {
23
+ return Boolean(process.env["npm_execpath"]) || Boolean(process.env["npm_lifecycle_event"])
24
+ }
25
+
26
+ /** npm global, project dependency, or monorepo workspace build. */
27
+ export function looksLikeNpmOrWorkspaceCLI(): boolean {
28
+ return hasNpmInstallPath() || ranUnderPackageManager()
29
+ }
30
+
31
+ /** Homebrew / Linuxbrew Cellar layout. */
32
+ export function looksLikeHomebrewCLI(): boolean {
33
+ const main = normalizePath(process.argv[1] ?? "")
34
+ if (!main) return false
35
+
36
+ if (/(?:^|\/)Cellar\/supatype\//.test(main)) return true
37
+
38
+ const cellar = process.env["HOMEBREW_CELLAR"]
39
+ if (cellar && normalizePath(main).startsWith(normalizePath(cellar) + "/") && main.includes("/supatype/")) {
40
+ return true
41
+ }
42
+
43
+ return false
44
+ }
45
+
46
+ /**
47
+ * Which installer owns the running binary — this decides what `self-update`
48
+ * tells the user to run, so it has to describe the binary on disk.
49
+ *
50
+ * Path shape wins over `npm_*` in the environment: those are set whenever the
51
+ * CLI is invoked from a package script, which says nothing about how the binary
52
+ * got there. Trusting them meant a Homebrew or curl install called from an npm
53
+ * script was told to update itself with npm.
54
+ */
55
+ export function detectCliInstallMethod(): CliInstallMethod {
56
+ if (hasNpmInstallPath()) return "npm"
57
+ if (looksLikeHomebrewCLI()) return "homebrew"
58
+ if (isStandalonePath()) return "standalone"
59
+ if (ranUnderPackageManager()) return "npm"
60
+ return "standalone"
61
+ }
62
+
63
+ /** The `curl | sh` installer's default target. */
64
+ function isStandalonePath(): boolean {
65
+ const main = normalizePath(process.argv[1] ?? "")
66
+ return main.includes("/.supatype/bin/")
67
+ }
package/src/cli.ts CHANGED
@@ -18,6 +18,7 @@ import { registerApp } from "./commands/app.js"
18
18
  import { registerAdd } from "./commands/add.js"
19
19
  import { registerSelfHost } from "./commands/self-host.js"
20
20
  import { registerCloud } from "./commands/cloud.js"
21
+ import { registerLogin } from "./commands/login.js"
21
22
  import { registerEngine } from "./commands/engine.js"
22
23
  import { registerDb } from "./commands/db.js"
23
24
  import { registerDeploy } from "./commands/deploy.js"
@@ -25,10 +26,12 @@ import { registerStatus } from "./commands/status.js"
25
26
  import { registerLogs } from "./commands/logs.js"
26
27
  import { registerAdmin } from "./commands/admin.js"
27
28
  import { registerFunctions } from "./commands/functions.js"
29
+ import { registerHooks } from "./commands/hooks.js"
28
30
  import { registerPlugins } from "./commands/plugins.js"
29
31
  import { registerTypes } from "./commands/types.js"
30
32
  import { registerMigrateFromV1 } from "./commands/migrate-from-v1.js"
31
33
  import { registerSelfUpdate } from "./commands/self-update.js"
34
+ import { registerInternalCommands } from "./commands/internal.js"
32
35
  import { cliPackageVersion } from "./cli-package-version.js"
33
36
  import { reportCliFatal } from "./ui/fatal.js"
34
37
  import { wrapProgramActionsWithChrome } from "./ui/runtime/command-chrome.js"
@@ -43,6 +46,7 @@ export async function run(): Promise<void> {
43
46
  registerDev(program)
44
47
  registerCache(program)
45
48
  registerSelfUpdate(program)
49
+ registerInternalCommands(program)
46
50
  registerUpdate(program)
47
51
  registerPg(program)
48
52
  registerPush(program)
@@ -58,6 +62,7 @@ export async function run(): Promise<void> {
58
62
  registerApp(program)
59
63
  registerAdd(program)
60
64
  registerSelfHost(program)
65
+ registerLogin(program)
61
66
  registerCloud(program)
62
67
  registerEngine(program)
63
68
  registerDb(program)
@@ -66,6 +71,7 @@ export async function run(): Promise<void> {
66
71
  registerLogs(program)
67
72
  registerAdmin(program)
68
73
  registerFunctions(program)
74
+ registerHooks(program)
69
75
  registerPlugins(program)
70
76
  registerTypes(program)
71
77
  registerMigrateFromV1(program)