@supatype/cli 0.1.6 → 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.
- package/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +144 -94
- package/.turbo/turbo-typecheck.log +1 -1
- package/deno.d.ts +52 -0
- package/dist/augmentation-generator.d.ts +22 -0
- package/dist/augmentation-generator.d.ts.map +1 -1
- package/dist/augmentation-generator.js +18 -4
- package/dist/augmentation-generator.js.map +1 -1
- package/dist/binary-cache.d.ts +14 -0
- package/dist/binary-cache.d.ts.map +1 -1
- package/dist/binary-cache.js +33 -13
- package/dist/binary-cache.js.map +1 -1
- package/dist/cache-clean.d.ts +15 -0
- package/dist/cache-clean.d.ts.map +1 -0
- package/dist/cache-clean.js +76 -0
- package/dist/cache-clean.js.map +1 -0
- package/dist/cache-pins.d.ts +51 -0
- package/dist/cache-pins.d.ts.map +1 -0
- package/dist/cache-pins.js +184 -0
- package/dist/cache-pins.js.map +1 -0
- package/dist/cli-install-method.d.ts +19 -0
- package/dist/cli-install-method.d.ts.map +1 -0
- package/dist/cli-install-method.js +60 -0
- package/dist/cli-install-method.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud-credentials.d.ts +34 -0
- package/dist/cloud-credentials.d.ts.map +1 -0
- package/dist/cloud-credentials.js +139 -0
- package/dist/cloud-credentials.js.map +1 -0
- package/dist/commands/admin.d.ts +11 -0
- package/dist/commands/admin.d.ts.map +1 -1
- package/dist/commands/admin.js +121 -19
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/cache.d.ts.map +1 -1
- package/dist/commands/cache.js +67 -71
- package/dist/commands/cache.js.map +1 -1
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +32 -8
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/db.d.ts +1 -0
- package/dist/commands/db.d.ts.map +1 -1
- package/dist/commands/db.js +147 -1
- package/dist/commands/db.js.map +1 -1
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +50 -10
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.d.ts +28 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +60 -3
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/functions.d.ts +7 -0
- package/dist/commands/functions.d.ts.map +1 -1
- package/dist/commands/functions.js +40 -6
- package/dist/commands/functions.js.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +5 -1
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/hooks.d.ts +12 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +118 -0
- package/dist/commands/hooks.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +41 -7
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/internal.d.ts +6 -0
- package/dist/commands/internal.d.ts.map +1 -0
- package/dist/commands/internal.js +22 -0
- package/dist/commands/internal.js.map +1 -0
- package/dist/commands/link-helpers.d.ts.map +1 -1
- package/dist/commands/link-helpers.js +8 -2
- package/dist/commands/link-helpers.js.map +1 -1
- package/dist/commands/login.d.ts +7 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +110 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +110 -7
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/self-update.d.ts +7 -0
- package/dist/commands/self-update.d.ts.map +1 -1
- package/dist/commands/self-update.js +18 -11
- package/dist/commands/self-update.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +18 -3
- package/dist/config.js.map +1 -1
- package/dist/db-preflight.d.ts +78 -0
- package/dist/db-preflight.d.ts.map +1 -0
- package/dist/db-preflight.js +452 -0
- package/dist/db-preflight.js.map +1 -0
- package/dist/dev-compose.d.ts +1 -1
- package/dist/dev-compose.d.ts.map +1 -1
- package/dist/dev-compose.js +127 -72
- package/dist/dev-compose.js.map +1 -1
- package/dist/env-file.d.ts +40 -2
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +132 -11
- package/dist/env-file.js.map +1 -1
- package/dist/field-masking-tier.d.ts +50 -0
- package/dist/field-masking-tier.d.ts.map +1 -0
- package/dist/field-masking-tier.js +91 -0
- package/dist/field-masking-tier.js.map +1 -0
- package/dist/functions-deno-types.d.ts +24 -0
- package/dist/functions-deno-types.d.ts.map +1 -0
- package/dist/functions-deno-types.js +155 -0
- package/dist/functions-deno-types.js.map +1 -0
- package/dist/hook-upload.d.ts +30 -0
- package/dist/hook-upload.d.ts.map +1 -0
- package/dist/hook-upload.js +86 -0
- package/dist/hook-upload.js.map +1 -0
- package/dist/hooks-generator.d.ts +8 -0
- package/dist/hooks-generator.d.ts.map +1 -0
- package/dist/hooks-generator.js +332 -0
- package/dist/hooks-generator.js.map +1 -0
- package/dist/link.d.ts +4 -0
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +2 -0
- package/dist/link.js.map +1 -1
- package/dist/local-secrets.d.ts +71 -0
- package/dist/local-secrets.d.ts.map +1 -0
- package/dist/local-secrets.js +100 -0
- package/dist/local-secrets.js.map +1 -0
- package/dist/model-hooks.d.ts +73 -0
- package/dist/model-hooks.d.ts.map +1 -0
- package/dist/model-hooks.js +211 -0
- package/dist/model-hooks.js.map +1 -0
- package/dist/postgres-ctl.d.ts +11 -0
- package/dist/postgres-ctl.d.ts.map +1 -1
- package/dist/postgres-ctl.js +25 -2
- package/dist/postgres-ctl.js.map +1 -1
- package/dist/project-config.d.ts +121 -1
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js +150 -1
- package/dist/project-config.js.map +1 -1
- package/dist/resolve-target.d.ts +4 -0
- package/dist/resolve-target.d.ts.map +1 -1
- package/dist/resolve-target.js +69 -73
- package/dist/resolve-target.js.map +1 -1
- package/dist/schema-ast-v2.d.ts +2 -1
- package/dist/schema-ast-v2.d.ts.map +1 -1
- package/dist/schema-ast-v2.js +4 -2
- package/dist/schema-ast-v2.js.map +1 -1
- package/dist/schema-push-lock.d.ts.map +1 -1
- package/dist/schema-push-lock.js +43 -16
- package/dist/schema-push-lock.js.map +1 -1
- package/dist/self-host-compose.d.ts +24 -0
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +301 -69
- package/dist/self-host-compose.js.map +1 -1
- package/dist/service-role-check.d.ts +21 -0
- package/dist/service-role-check.d.ts.map +1 -0
- package/dist/service-role-check.js +85 -0
- package/dist/service-role-check.js.map +1 -0
- package/dist/target-client.d.ts +17 -0
- package/dist/target-client.d.ts.map +1 -1
- package/dist/target-client.js +69 -17
- package/dist/target-client.js.map +1 -1
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +736 -32
- package/dist/type-extractor.js.map +1 -1
- package/dist/ui/runtime/command-chrome.js +1 -1
- package/dist/ui/runtime/command-chrome.js.map +1 -1
- package/dist/ui/runtime/stdin-after-ink.d.ts.map +1 -1
- package/dist/ui/runtime/stdin-after-ink.js +3 -1
- package/dist/ui/runtime/stdin-after-ink.js.map +1 -1
- package/package.json +4 -1
- package/scripts/generate-homebrew-formula.mjs +121 -0
- package/src/augmentation-generator.ts +18 -5
- package/src/binary-cache.ts +47 -15
- package/src/cache-clean.ts +81 -0
- package/src/cache-pins.ts +243 -0
- package/src/cli-install-method.ts +67 -0
- package/src/cli.ts +6 -0
- package/src/cloud-credentials.ts +183 -0
- package/src/commands/admin.ts +140 -25
- package/src/commands/cache.ts +90 -75
- package/src/commands/cloud.ts +32 -8
- package/src/commands/db.ts +162 -1
- package/src/commands/dev.ts +57 -8
- package/src/commands/doctor.ts +69 -3
- package/src/commands/functions.ts +42 -5
- package/src/commands/generate.ts +5 -1
- package/src/commands/hooks.ts +138 -0
- package/src/commands/init.ts +42 -8
- package/src/commands/internal.ts +23 -0
- package/src/commands/link-helpers.ts +9 -2
- package/src/commands/login.ts +127 -0
- package/src/commands/push.ts +124 -7
- package/src/commands/self-update.ts +85 -13
- package/src/config.ts +22 -3
- package/src/db-preflight.ts +575 -0
- package/src/dev-compose.ts +142 -73
- package/src/env-file.ts +157 -11
- package/src/field-masking-tier.ts +107 -0
- package/src/functions-deno-types.ts +188 -0
- package/src/hook-upload.ts +107 -0
- package/src/hooks-generator.ts +352 -0
- package/src/link.ts +7 -0
- package/src/local-secrets.ts +104 -0
- package/src/model-hooks.ts +260 -0
- package/src/postgres-ctl.ts +29 -2
- package/src/project-config.ts +236 -2
- package/src/resolve-target.ts +113 -68
- package/src/schema-ast-v2.ts +5 -2
- package/src/schema-push-lock.ts +56 -17
- package/src/self-host-compose.ts +344 -68
- package/src/service-role-check.ts +111 -0
- package/src/target-client.ts +105 -18
- package/src/type-extractor.ts +883 -28
- package/src/ui/runtime/command-chrome.ts +1 -1
- package/src/ui/runtime/stdin-after-ink.ts +3 -1
- package/tests/access-composition.test.ts +667 -0
- package/tests/admin-studio-membership.test.ts +121 -0
- package/tests/api-schemas.test.ts +83 -0
- package/tests/archive-name-alias.test.ts +42 -0
- package/tests/augmentation-generator.test.ts +28 -0
- package/tests/cache-clean.test.ts +206 -0
- package/tests/cli-help.test.ts +3 -0
- package/tests/cli-install-method.test.ts +66 -0
- package/tests/command-chrome-coverage.test.ts +31 -26
- package/tests/command-chrome-wrap.test.ts +1 -0
- package/tests/db-preflight.test.ts +72 -0
- package/tests/doctor-render.test.ts +62 -0
- package/tests/doctor-service-role.test.ts +55 -0
- package/tests/env-file.test.ts +121 -0
- package/tests/external-database-compose.test.ts +184 -0
- package/tests/external-database-config.test.ts +154 -0
- package/tests/field-masking-tier.test.ts +184 -0
- package/tests/functions-deno-types.test.ts +197 -0
- package/tests/homebrew-formula.test.ts +58 -0
- package/tests/hook-upload.test.ts +125 -0
- package/tests/hooks-generator.test.ts +212 -0
- package/tests/init.test.ts +3 -0
- package/tests/local-secrets.test.ts +92 -0
- package/tests/model-hooks.test.ts +322 -0
- package/tests/runtime-contract.test.ts +113 -1
- package/tests/service-role-check.test.ts +109 -0
- package/tests/service-role-routes.test.ts +51 -0
- package/tests/target-client-refresh.test.ts +94 -0
- package/tests/type-extractor.test.ts +187 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/tests/engine-distribution.test.ts +0 -418
package/src/binary-cache.ts
CHANGED
|
@@ -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
|
|
403
|
-
fetchChecksums(checksumsUrl, minisigUrl,
|
|
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,
|
|
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
|
-
|
|
444
|
-
): Promise<
|
|
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,
|
|
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,
|
|
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,
|
|
572
|
-
const
|
|
573
|
-
for (const
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
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 "${
|
|
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)
|