@supatype/cli 0.1.0-alpha.10 → 0.1.0-alpha.12

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 (188) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +98 -65
  3. package/.turbo/turbo-typecheck.log +1 -1
  4. package/dist/app/framework.js +1 -3
  5. package/dist/app/framework.js.map +1 -1
  6. package/dist/app/proxy-dev-app.d.ts +14 -0
  7. package/dist/app/proxy-dev-app.d.ts.map +1 -1
  8. package/dist/app/proxy-dev-app.js +109 -6
  9. package/dist/app/proxy-dev-app.js.map +1 -1
  10. package/dist/binary-cache.d.ts +1 -1
  11. package/dist/binary-cache.d.ts.map +1 -1
  12. package/dist/binary-cache.js +6 -1
  13. package/dist/binary-cache.js.map +1 -1
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +6 -0
  16. package/dist/cli.js.map +1 -1
  17. package/dist/commands/adopt.d.ts +3 -0
  18. package/dist/commands/adopt.d.ts.map +1 -0
  19. package/dist/commands/adopt.js +58 -0
  20. package/dist/commands/adopt.js.map +1 -0
  21. package/dist/commands/cloud.d.ts +4 -9
  22. package/dist/commands/cloud.d.ts.map +1 -1
  23. package/dist/commands/cloud.js +49 -91
  24. package/dist/commands/cloud.js.map +1 -1
  25. package/dist/commands/db.d.ts.map +1 -1
  26. package/dist/commands/db.js +25 -47
  27. package/dist/commands/db.js.map +1 -1
  28. package/dist/commands/deploy.d.ts.map +1 -1
  29. package/dist/commands/deploy.js +117 -74
  30. package/dist/commands/deploy.js.map +1 -1
  31. package/dist/commands/dev.d.ts.map +1 -1
  32. package/dist/commands/dev.js +21 -3
  33. package/dist/commands/dev.js.map +1 -1
  34. package/dist/commands/diff.d.ts.map +1 -1
  35. package/dist/commands/diff.js +37 -37
  36. package/dist/commands/diff.js.map +1 -1
  37. package/dist/commands/doctor.d.ts +3 -0
  38. package/dist/commands/doctor.d.ts.map +1 -0
  39. package/dist/commands/doctor.js +77 -0
  40. package/dist/commands/doctor.js.map +1 -0
  41. package/dist/commands/functions.d.ts.map +1 -1
  42. package/dist/commands/functions.js +80 -33
  43. package/dist/commands/functions.js.map +1 -1
  44. package/dist/commands/init.d.ts +1 -0
  45. package/dist/commands/init.d.ts.map +1 -1
  46. package/dist/commands/init.js +26 -4
  47. package/dist/commands/init.js.map +1 -1
  48. package/dist/commands/introspect.d.ts +3 -0
  49. package/dist/commands/introspect.d.ts.map +1 -0
  50. package/dist/commands/introspect.js +34 -0
  51. package/dist/commands/introspect.js.map +1 -0
  52. package/dist/commands/link-helpers.d.ts +15 -0
  53. package/dist/commands/link-helpers.d.ts.map +1 -0
  54. package/dist/commands/link-helpers.js +187 -0
  55. package/dist/commands/link-helpers.js.map +1 -0
  56. package/dist/commands/migrate.d.ts.map +1 -1
  57. package/dist/commands/migrate.js +116 -14
  58. package/dist/commands/migrate.js.map +1 -1
  59. package/dist/commands/pull.d.ts.map +1 -1
  60. package/dist/commands/pull.js +32 -5
  61. package/dist/commands/pull.js.map +1 -1
  62. package/dist/commands/push.d.ts.map +1 -1
  63. package/dist/commands/push.js +102 -129
  64. package/dist/commands/push.js.map +1 -1
  65. package/dist/commands/status.d.ts +1 -1
  66. package/dist/commands/status.d.ts.map +1 -1
  67. package/dist/commands/status.js +93 -29
  68. package/dist/commands/status.js.map +1 -1
  69. package/dist/commands/update.d.ts.map +1 -1
  70. package/dist/commands/update.js +6 -2
  71. package/dist/commands/update.js.map +1 -1
  72. package/dist/config.d.ts +2 -1
  73. package/dist/config.d.ts.map +1 -1
  74. package/dist/config.js.map +1 -1
  75. package/dist/dev-compose.d.ts +23 -0
  76. package/dist/dev-compose.d.ts.map +1 -1
  77. package/dist/dev-compose.js +183 -6
  78. package/dist/dev-compose.js.map +1 -1
  79. package/dist/diff-output.d.ts +5 -1
  80. package/dist/diff-output.d.ts.map +1 -1
  81. package/dist/diff-output.js +69 -0
  82. package/dist/diff-output.js.map +1 -1
  83. package/dist/engine-client.d.ts +10 -1
  84. package/dist/engine-client.d.ts.map +1 -1
  85. package/dist/engine-client.js +64 -13
  86. package/dist/engine-client.js.map +1 -1
  87. package/dist/engine-push-output.d.ts +1 -0
  88. package/dist/engine-push-output.d.ts.map +1 -1
  89. package/dist/engine-push-output.js +4 -1
  90. package/dist/engine-push-output.js.map +1 -1
  91. package/dist/gitignore.d.ts +8 -0
  92. package/dist/gitignore.d.ts.map +1 -0
  93. package/dist/gitignore.js +41 -0
  94. package/dist/gitignore.js.map +1 -0
  95. package/dist/link.d.ts +66 -0
  96. package/dist/link.d.ts.map +1 -0
  97. package/dist/link.js +159 -0
  98. package/dist/link.js.map +1 -0
  99. package/dist/process-manager.d.ts +2 -0
  100. package/dist/process-manager.d.ts.map +1 -1
  101. package/dist/process-manager.js +2 -0
  102. package/dist/process-manager.js.map +1 -1
  103. package/dist/project-config.d.ts +8 -0
  104. package/dist/project-config.d.ts.map +1 -1
  105. package/dist/project-config.js.map +1 -1
  106. package/dist/pull-utils.d.ts +50 -14
  107. package/dist/pull-utils.d.ts.map +1 -1
  108. package/dist/pull-utils.js +152 -12
  109. package/dist/pull-utils.js.map +1 -1
  110. package/dist/resolve-target.d.ts +86 -0
  111. package/dist/resolve-target.d.ts.map +1 -0
  112. package/dist/resolve-target.js +291 -0
  113. package/dist/resolve-target.js.map +1 -0
  114. package/dist/runtime-routes.d.ts.map +1 -1
  115. package/dist/runtime-routes.js +7 -0
  116. package/dist/runtime-routes.js.map +1 -1
  117. package/dist/schema-ast-v2.d.ts +1 -1
  118. package/dist/schema-ast-v2.d.ts.map +1 -1
  119. package/dist/schema-ast-v2.js +2 -2
  120. package/dist/schema-ast-v2.js.map +1 -1
  121. package/dist/schema-sources.d.ts +40 -0
  122. package/dist/schema-sources.d.ts.map +1 -0
  123. package/dist/schema-sources.js +183 -0
  124. package/dist/schema-sources.js.map +1 -0
  125. package/dist/self-host-compose.d.ts +10 -0
  126. package/dist/self-host-compose.d.ts.map +1 -1
  127. package/dist/self-host-compose.js +85 -3
  128. package/dist/self-host-compose.js.map +1 -1
  129. package/dist/storage-provision.d.ts +4 -0
  130. package/dist/storage-provision.d.ts.map +1 -1
  131. package/dist/storage-provision.js +24 -2
  132. package/dist/storage-provision.js.map +1 -1
  133. package/dist/target-client.d.ts +10 -0
  134. package/dist/target-client.d.ts.map +1 -0
  135. package/dist/target-client.js +22 -0
  136. package/dist/target-client.js.map +1 -0
  137. package/dist/type-extractor.d.ts +11 -0
  138. package/dist/type-extractor.d.ts.map +1 -1
  139. package/dist/type-extractor.js +95 -8
  140. package/dist/type-extractor.js.map +1 -1
  141. package/package.json +1 -1
  142. package/src/app/framework.ts +1 -3
  143. package/src/app/proxy-dev-app.ts +113 -6
  144. package/src/binary-cache.ts +6 -1
  145. package/src/cli.ts +6 -0
  146. package/src/commands/adopt.ts +83 -0
  147. package/src/commands/cloud.ts +66 -108
  148. package/src/commands/db.ts +28 -52
  149. package/src/commands/deploy.ts +162 -104
  150. package/src/commands/dev.ts +24 -10
  151. package/src/commands/diff.ts +40 -41
  152. package/src/commands/doctor.ts +102 -0
  153. package/src/commands/functions.ts +95 -37
  154. package/src/commands/init.ts +25 -4
  155. package/src/commands/introspect.ts +47 -0
  156. package/src/commands/link-helpers.ts +228 -0
  157. package/src/commands/migrate.ts +163 -15
  158. package/src/commands/pull.ts +37 -9
  159. package/src/commands/push.ts +132 -166
  160. package/src/commands/status.ts +100 -33
  161. package/src/commands/update.ts +6 -2
  162. package/src/config.ts +2 -1
  163. package/src/dev-compose.ts +240 -6
  164. package/src/diff-output.ts +79 -1
  165. package/src/engine-client.ts +70 -13
  166. package/src/engine-push-output.ts +7 -3
  167. package/src/gitignore.ts +48 -0
  168. package/src/link.ts +242 -0
  169. package/src/process-manager.ts +4 -0
  170. package/src/project-config.ts +8 -0
  171. package/src/pull-utils.ts +217 -23
  172. package/src/resolve-target.ts +419 -0
  173. package/src/runtime-routes.ts +7 -0
  174. package/src/schema-ast-v2.ts +2 -1
  175. package/src/schema-sources.ts +248 -0
  176. package/src/self-host-compose.ts +87 -3
  177. package/src/storage-provision.ts +33 -1
  178. package/src/target-client.ts +40 -0
  179. package/src/type-extractor.ts +124 -11
  180. package/tests/cli-help.test.ts +27 -2
  181. package/tests/init.test.ts +1 -1
  182. package/tests/link.test.ts +148 -0
  183. package/tests/proxy-dev-app.test.ts +45 -1
  184. package/tests/pull-utils.test.ts +5 -4
  185. package/tests/runtime-contract.test.ts +44 -1
  186. package/tests/schema-sources.test.ts +119 -0
  187. package/tests/storage-provision.test.ts +100 -0
  188. package/tsconfig.tsbuildinfo +1 -1
@@ -21,7 +21,9 @@ import {
21
21
  discoverTsFunctionsInDir,
22
22
  generateFunctionsRouterSource,
23
23
  } from "../functions-router-gen.js"
24
- import { selfHostComposePaths } from "../self-host-compose.js"
24
+ import { loadProjectLink } from "../link.js"
25
+ import { resolveTarget } from "../resolve-target.js"
26
+ import { targetFetch } from "../target-client.js"
25
27
 
26
28
  // ─── Constants ───────────────────────────────────────────────────────────────
27
29
 
@@ -56,8 +58,9 @@ export function registerFunctions(program: Command): void {
56
58
  .command("deploy")
57
59
  .description("Deploy all functions (or --only <name> for one) to the linked project")
58
60
  .option("--only <name>", "Deploy a single function")
61
+ .option("--env <name>", "Target environment when linked")
59
62
  .option("--dry-run", "Show what would be deployed without deploying")
60
- .action(async (opts: { only?: string; dryRun?: boolean }) => {
63
+ .action(async (opts: { only?: string; env?: string; dryRun?: boolean }) => {
61
64
  await deploy(process.cwd(), opts)
62
65
  })
63
66
 
@@ -327,7 +330,7 @@ async function serve(cwd: string, opts: { port: string; envFile: string }): Prom
327
330
 
328
331
  // ─── Deploy ──────────────────────────────────────────────────────────────────
329
332
 
330
- async function deploy(cwd: string, opts: { only?: string; dryRun?: boolean }): Promise<void> {
333
+ async function deploy(cwd: string, opts: { only?: string; env?: string; dryRun?: boolean }): Promise<void> {
331
334
  const allFns = discoverFunctions(cwd)
332
335
  const fns = opts.only
333
336
  ? allFns.filter(f => f.name === opts.only)
@@ -353,13 +356,27 @@ async function deploy(cwd: string, opts: { only?: string; dryRun?: boolean }): P
353
356
  return
354
357
  }
355
358
 
359
+ const link = loadProjectLink(cwd)
360
+ if (link) {
361
+ try {
362
+ const target = resolveTarget(cwd, { env: opts.env })
363
+ if (target.mode !== "direct" && target.token) {
364
+ await deployViaTarget(cwd, target, fns)
365
+ return
366
+ }
367
+ } catch {
368
+ /* fall through to compose/local */
369
+ }
370
+ }
371
+
372
+ const { selfHostComposePaths } = await import("../self-host-compose.js")
356
373
  const composePath = selfHostComposePaths(cwd).composePath
357
374
  if (existsSync(composePath)) {
358
375
  await deploySelfHosted(cwd, fns)
359
376
  return
360
377
  }
361
378
 
362
- await deployCloud(cwd, fns)
379
+ await deployCloud(cwd, fns, opts.env)
363
380
  }
364
381
 
365
382
  async function deploySelfHosted(cwd: string, fns: DiscoveredFunction[]): Promise<void> {
@@ -376,7 +393,49 @@ async function deploySelfHosted(cwd: string, fns: DiscoveredFunction[]): Promise
376
393
  console.log("\nKong → supatype-server → functions-worker (per-project worker).")
377
394
  }
378
395
 
379
- async function deployCloud(cwd: string, fns: DiscoveredFunction[]): Promise<void> {
396
+ async function deployViaTarget(
397
+ cwd: string,
398
+ target: ReturnType<typeof resolveTarget>,
399
+ fns: DiscoveredFunction[],
400
+ ): Promise<void> {
401
+ console.log(`Deploying to ${target.mode} project: ${target.projectRef} (${target.environment})\n`)
402
+
403
+ for (const fn of fns) {
404
+ const start = Date.now()
405
+ const source = readFunctionSource(fn)
406
+
407
+ try {
408
+ await targetFetch(
409
+ target.apiBaseUrl,
410
+ target.apiPrefix,
411
+ {
412
+ method: "POST",
413
+ path: `/projects/${target.projectRef}/functions/deploy`,
414
+ body: {
415
+ functions: [{
416
+ name: fn.name,
417
+ source,
418
+ entrypoint: `${fn.name}/index.ts`,
419
+ }],
420
+ },
421
+ token: target.token!,
422
+ orgId: target.orgId,
423
+ environment: target.mode === "cloud" ? target.environment : undefined,
424
+ },
425
+ )
426
+
427
+ const duration = Date.now() - start
428
+ console.log(` ${fn.name} ✓ deployed (${duration}ms)`)
429
+ } catch (err) {
430
+ console.error(` ${fn.name} ✗ ${err instanceof Error ? err.message : "unknown error"}`)
431
+ }
432
+ }
433
+
434
+ console.log(`\nDeployed ${fns.length} function(s)`)
435
+ void cwd
436
+ }
437
+
438
+ async function deployCloud(cwd: string, fns: DiscoveredFunction[], env?: string): Promise<void> {
380
439
  const { getLinkedProject, getCloudToken, getCloudApiUrl } = await loadCloudHelpers()
381
440
  const linked = getLinkedProject(cwd)
382
441
 
@@ -385,13 +444,13 @@ async function deployCloud(cwd: string, fns: DiscoveredFunction[]): Promise<void
385
444
  process.exit(1)
386
445
  }
387
446
 
388
- const token = getCloudToken()
447
+ const token = getCloudToken(cwd)
389
448
  if (!token) {
390
449
  console.error("Not logged in. Run: npx supatype cloud login")
391
450
  process.exit(1)
392
451
  }
393
452
 
394
- const apiUrl = getCloudApiUrl()
453
+ const apiUrl = getCloudApiUrl(cwd)
395
454
  console.log(`Deploying to project: ${linked.ref}\n`)
396
455
 
397
456
  for (const fn of fns) {
@@ -473,14 +532,14 @@ async function listFunctions(cwd: string): Promise<void> {
473
532
  return
474
533
  }
475
534
 
476
- const token = getCloudToken()
535
+ const token = getCloudToken(cwd)
477
536
  if (!token) {
478
537
  console.error("Not logged in. Run: npx supatype cloud login")
479
538
  process.exit(1)
480
539
  }
481
540
 
482
541
  try {
483
- const res = await fetch(`${getCloudApiUrl()}/api/v1/projects/${linked.ref}/functions`, {
542
+ const res = await fetch(`${getCloudApiUrl(cwd)}/api/v1/projects/${linked.ref}/functions`, {
484
543
  headers: {
485
544
  Authorization: `Bearer ${token}`,
486
545
  "X-Org-Id": linked.orgId ?? "",
@@ -527,14 +586,14 @@ async function deleteFunction(cwd: string, name: string): Promise<void> {
527
586
  process.exit(1)
528
587
  }
529
588
 
530
- const token = getCloudToken()
589
+ const token = getCloudToken(cwd)
531
590
  if (!token) {
532
591
  console.error("Not logged in. Run: npx supatype cloud login")
533
592
  process.exit(1)
534
593
  }
535
594
 
536
595
  try {
537
- const res = await fetch(`${getCloudApiUrl()}/api/v1/projects/${linked.ref}/functions/${name}`, {
596
+ const res = await fetch(`${getCloudApiUrl(cwd)}/api/v1/projects/${linked.ref}/functions/${name}`, {
538
597
  method: "DELETE",
539
598
  headers: {
540
599
  Authorization: `Bearer ${token}`,
@@ -567,7 +626,7 @@ async function functionLogs(cwd: string, name: string, opts: { since: string }):
567
626
  process.exit(1)
568
627
  }
569
628
 
570
- const token = getCloudToken()
629
+ const token = getCloudToken(cwd)
571
630
  if (!token) {
572
631
  console.error("Not logged in. Run: npx supatype cloud login")
573
632
  process.exit(1)
@@ -575,7 +634,7 @@ async function functionLogs(cwd: string, name: string, opts: { since: string }):
575
634
 
576
635
  try {
577
636
  const res = await fetch(
578
- `${getCloudApiUrl()}/api/v1/projects/${linked.ref}/functions/${name}/logs?since=${opts.since}`,
637
+ `${getCloudApiUrl(cwd)}/api/v1/projects/${linked.ref}/functions/${name}/logs?since=${opts.since}`,
579
638
  {
580
639
  headers: {
581
640
  Authorization: `Bearer ${token}`,
@@ -625,7 +684,7 @@ async function invoke(
625
684
  const linked = getLinkedProject(cwd)
626
685
  if (linked) {
627
686
  url = `https://${linked.ref}.supatype.dev/functions/v1/${name}`
628
- const token = getCloudToken()
687
+ const token = getCloudToken(cwd)
629
688
  if (token && opts.auth) {
630
689
  headers["Authorization"] = `Bearer ${token}`
631
690
  }
@@ -710,14 +769,14 @@ async function envList(cwd: string): Promise<void> {
710
769
  return
711
770
  }
712
771
 
713
- const token = getCloudToken()
772
+ const token = getCloudToken(cwd)
714
773
  if (!token) {
715
774
  console.error("Not logged in. Run: npx supatype cloud login")
716
775
  process.exit(1)
717
776
  }
718
777
 
719
778
  try {
720
- const res = await fetch(`${getCloudApiUrl()}/api/v1/projects/${linked.ref}/functions/env`, {
779
+ const res = await fetch(`${getCloudApiUrl(cwd)}/api/v1/projects/${linked.ref}/functions/env`, {
721
780
  headers: {
722
781
  Authorization: `Bearer ${token}`,
723
782
  "X-Org-Id": linked.orgId ?? "",
@@ -778,14 +837,14 @@ async function envSet(cwd: string, keyvalue: string): Promise<void> {
778
837
  return
779
838
  }
780
839
 
781
- const token = getCloudToken()
840
+ const token = getCloudToken(cwd)
782
841
  if (!token) {
783
842
  console.error("Not logged in. Run: npx supatype cloud login")
784
843
  process.exit(1)
785
844
  }
786
845
 
787
846
  try {
788
- const res = await fetch(`${getCloudApiUrl()}/api/v1/projects/${linked.ref}/functions/env`, {
847
+ const res = await fetch(`${getCloudApiUrl(cwd)}/api/v1/projects/${linked.ref}/functions/env`, {
789
848
  method: "POST",
790
849
  headers: {
791
850
  Authorization: `Bearer ${token}`,
@@ -828,14 +887,14 @@ async function envUnset(cwd: string, key: string): Promise<void> {
828
887
  return
829
888
  }
830
889
 
831
- const token = getCloudToken()
890
+ const token = getCloudToken(cwd)
832
891
  if (!token) {
833
892
  console.error("Not logged in. Run: npx supatype cloud login")
834
893
  process.exit(1)
835
894
  }
836
895
 
837
896
  try {
838
- const res = await fetch(`${getCloudApiUrl()}/api/v1/projects/${linked.ref}/functions/env/${key}`, {
897
+ const res = await fetch(`${getCloudApiUrl(cwd)}/api/v1/projects/${linked.ref}/functions/env/${key}`, {
839
898
  method: "DELETE",
840
899
  headers: {
841
900
  Authorization: `Bearer ${token}`,
@@ -860,32 +919,29 @@ async function envUnset(cwd: string, key: string): Promise<void> {
860
919
  // ─── Cloud helpers (lazy loaded) ─────────────────────────────────────────────
861
920
 
862
921
  interface CloudHelpers {
863
- getLinkedProject(cwd: string): { ref: string; orgId?: string } | null
864
- getCloudToken(): string | null
865
- getCloudApiUrl(): string
922
+ getLinkedProject(cwd: string): { ref: string; orgId?: string | undefined; kind?: string } | null
923
+ getCloudToken(cwd: string): string | null
924
+ getCloudApiUrl(cwd: string): string
866
925
  }
867
926
 
868
927
  async function loadCloudHelpers(): Promise<CloudHelpers> {
869
- // These helpers read the local .supatype/linked.json and auth token
870
928
  return {
871
- getLinkedProject(cwd: string): { ref: string; orgId?: string } | null {
872
- const linkedPath = resolve(cwd, ".supatype/linked.json")
873
- if (!existsSync(linkedPath)) return null
874
- try {
875
- const data = JSON.parse(readFileSync(linkedPath, "utf8")) as Record<string, string>
876
- const ref = data["ref"]
877
- const orgId = data["orgId"]
878
- return ref ? { ref, ...(orgId !== undefined ? { orgId } : {}) } : null
879
- } catch {
880
- return null
929
+ getLinkedProject(cwd: string): { ref: string; orgId?: string | undefined; kind?: string } | null {
930
+ const link = loadProjectLink(cwd)
931
+ if (!link?.projectRef) return null
932
+ return {
933
+ ref: link.projectRef,
934
+ kind: link.kind,
935
+ ...(link.orgId !== undefined ? { orgId: link.orgId } : {}),
881
936
  }
882
937
  },
883
938
 
884
- getCloudToken(): string | null {
885
- // Check env first, then config file
939
+ getCloudToken(cwd: string): string | null {
886
940
  if (process.env["SUPATYPE_ACCESS_TOKEN"]) {
887
941
  return process.env["SUPATYPE_ACCESS_TOKEN"]
888
942
  }
943
+ const link = loadProjectLink(cwd)
944
+ if (link?.token) return link.token
889
945
  const tokenPath = resolve(
890
946
  process.env["HOME"] ?? process.env["USERPROFILE"] ?? "~",
891
947
  ".supatype/token",
@@ -894,7 +950,9 @@ async function loadCloudHelpers(): Promise<CloudHelpers> {
894
950
  return readFileSync(tokenPath, "utf8").trim() || null
895
951
  },
896
952
 
897
- getCloudApiUrl(): string {
953
+ getCloudApiUrl(cwd: string): string {
954
+ const link = loadProjectLink(cwd)
955
+ if (link?.cloudApiUrl) return link.cloudApiUrl
898
956
  return process.env["SUPATYPE_API_URL"] ?? "https://api.supatype.com"
899
957
  },
900
958
  }
@@ -88,7 +88,18 @@ function scaffold(dir: string, projectName: string, mode: "dev" | "standalone" =
88
88
  write("seed.ts", seedTemplate(projectName))
89
89
  write("seeds/.gitkeep", "")
90
90
  write("public/.gitkeep", "")
91
- write(".gitignore", gitignoreTemplate())
91
+ const gitignorePath = join(dir, ".gitignore")
92
+ if (existsSync(gitignorePath)) {
93
+ const merged = mergeGitignoreTemplate(readFileSync(gitignorePath, "utf8"))
94
+ if (merged !== readFileSync(gitignorePath, "utf8")) {
95
+ writeFileSync(gitignorePath, merged, "utf8")
96
+ console.log(" updated .gitignore (added .supatype/)")
97
+ } else {
98
+ console.log(" skipped .gitignore (already exists)")
99
+ }
100
+ } else {
101
+ write(".gitignore", gitignoreTemplate())
102
+ }
92
103
  }
93
104
 
94
105
  // ─── Templates ───────────────────────────────────────────────────────────────
@@ -241,13 +252,23 @@ function gitignoreTemplate(): string {
241
252
  return `.env
242
253
  node_modules/
243
254
  dist/
244
- .supatype/engine/
245
- # Local overrides — never commit
255
+ .supatype/
246
256
  supatype.local.config.ts
247
257
  supatype.local.config.js
248
258
  supatype.local.config.mjs
249
- # Generated by supatype push
259
+ # Generated by supatype push (legacy paths — prefer output.types in config)
250
260
  src/types/supatype.d.ts
251
261
  src/lib/supatype.ts
252
262
  `
253
263
  }
264
+
265
+ export function mergeGitignoreTemplate(existingContent: string): string {
266
+ if (existingContent.includes(".supatype/") || existingContent.includes(".supatype\n")) {
267
+ return existingContent
268
+ }
269
+ const block = `
270
+ # Supatype — local runtime (contains secrets in link.json)
271
+ .supatype/
272
+ `
273
+ return existingContent.endsWith("\n") ? `${existingContent}${block}` : `${existingContent}\n${block}`
274
+ }
@@ -0,0 +1,47 @@
1
+ import type { Command } from "commander"
2
+ import { writeFileSync } from "node:fs"
3
+ import { loadProjectLink } from "../link.js"
4
+ import { resolveTarget, targetSchemaIntrospect, schemaPgSchema } from "../resolve-target.js"
5
+ import type { DatabaseStateJson } from "../pull-utils.js"
6
+ import { printIntrospectSummary } from "../pull-utils.js"
7
+
8
+ export function registerIntrospect(program: Command): void {
9
+ program
10
+ .command("introspect")
11
+ .description("Introspect the live Postgres database (JSON or summary)")
12
+ .option("--connection <url>", "Database connection URL (overrides config)")
13
+ .option("--env <name>", "Target environment when linked")
14
+ .option("--direct", "Use local engine subprocess")
15
+ .option("--json", "Output full DatabaseState JSON")
16
+ .option("--out <path>", "Write JSON output to a file")
17
+ .action(async (opts: {
18
+ connection?: string
19
+ env?: string
20
+ direct?: boolean
21
+ json?: boolean
22
+ out?: string
23
+ }) => {
24
+ const cwd = process.cwd()
25
+ const pgSchema = schemaPgSchema(cwd)
26
+
27
+ const linked = loadProjectLink(cwd)
28
+ const target = linked && !opts.direct && !opts.connection
29
+ ? resolveTarget(cwd, { env: opts.env })
30
+ : resolveTarget(cwd, { env: opts.env, direct: true, connection: opts.connection })
31
+
32
+ const state = (await targetSchemaIntrospect(target, { schema: pgSchema })) as DatabaseStateJson
33
+
34
+ if (opts.out) {
35
+ writeFileSync(opts.out, JSON.stringify(state, null, 2), "utf8")
36
+ console.log(`Wrote introspection to ${opts.out}`)
37
+ return
38
+ }
39
+
40
+ if (opts.json) {
41
+ console.log(JSON.stringify(state, null, 2))
42
+ return
43
+ }
44
+
45
+ printIntrospectSummary(state)
46
+ })
47
+ }
@@ -0,0 +1,228 @@
1
+ import type { Command } from "commander"
2
+ import { loadConfig } from "../config.js"
3
+ import {
4
+ createCloudLink,
5
+ createSelfHostLink,
6
+ loadProjectLink,
7
+ saveProjectLink,
8
+ type ProjectLink,
9
+ } from "../link.js"
10
+ import { ensureSupatypeGitignore, warnIfLinkNotGitignored } from "../gitignore.js"
11
+ import { targetFetch } from "../target-client.js"
12
+
13
+ function resolveLinkToken(opts: {
14
+ token?: string
15
+ serviceRoleKey?: string
16
+ }): string | undefined {
17
+ return (
18
+ opts.token ??
19
+ opts.serviceRoleKey ??
20
+ process.env["SUPATYPE_ACCESS_TOKEN"] ??
21
+ process.env["SUPATYPE_TOKEN"] ??
22
+ process.env["SERVICE_ROLE_KEY"]
23
+ )
24
+ }
25
+
26
+ async function probeSelfHostLink(apiUrl: string, projectRef: string, token: string): Promise<void> {
27
+ await targetFetch(apiUrl, "/platform/v1", {
28
+ method: "GET",
29
+ path: `/projects/${projectRef}/status`,
30
+ token,
31
+ })
32
+ }
33
+
34
+ export function registerEnvs(program: Command): void {
35
+ const envs = program.command("envs").description("Manage linked deployment environments")
36
+
37
+ envs
38
+ .command("list")
39
+ .description("List linked environments")
40
+ .action(() => {
41
+ const cwd = process.cwd()
42
+ const link = loadProjectLink(cwd)
43
+ if (!link) {
44
+ console.log("Not linked. Run: supatype link")
45
+ return
46
+ }
47
+ console.log(`\nProject: ${link.projectRef} (${link.kind})`)
48
+ console.log(`Default: ${link.defaultEnvironment}\n`)
49
+ for (const [name, env] of Object.entries(link.environments)) {
50
+ const mark = name === link.defaultEnvironment ? " *" : " "
51
+ console.log(`${mark} ${name.padEnd(14)} ${env.apiUrl}`)
52
+ }
53
+ console.log()
54
+ })
55
+
56
+ envs
57
+ .command("use <name>")
58
+ .description("Set the default environment")
59
+ .action((name: string) => {
60
+ const cwd = process.cwd()
61
+ const link = loadProjectLink(cwd)
62
+ if (!link?.environments[name]) {
63
+ console.error(`Environment "${name}" is not linked.`)
64
+ process.exit(1)
65
+ }
66
+ link.defaultEnvironment = name
67
+ saveProjectLink(cwd, link)
68
+ console.log(`Default environment set to "${name}".`)
69
+ })
70
+
71
+ envs
72
+ .command("create <name>")
73
+ .description("Create a cloud environment (staging/preview)")
74
+ .action(async (name: string) => {
75
+ const cwd = process.cwd()
76
+ const link = loadProjectLink(cwd)
77
+ if (!link || link.kind !== "cloud") {
78
+ console.error("Cloud link required. Run: supatype link --project <slug>")
79
+ process.exit(1)
80
+ }
81
+ if (!link.token || !link.cloudApiUrl) {
82
+ console.error("Missing cloud credentials in link.json")
83
+ process.exit(1)
84
+ }
85
+ const bodyName = name === "staging" || name === "preview" ? name : "staging"
86
+ await targetFetch(link.cloudApiUrl, "/api/v1", {
87
+ method: "POST",
88
+ path: `/projects/${link.projectRef}/environments`,
89
+ body: { name: bodyName },
90
+ token: link.token,
91
+ orgId: link.orgId,
92
+ })
93
+ const envsList = await targetFetch<Array<{ name: string; apiUrl: string }>>(
94
+ link.cloudApiUrl,
95
+ "/api/v1",
96
+ {
97
+ method: "GET",
98
+ path: `/projects/${link.projectRef}/environments`,
99
+ token: link.token,
100
+ orgId: link.orgId,
101
+ },
102
+ )
103
+ const updated = createCloudLink({
104
+ projectRef: link.projectRef,
105
+ cloudApiUrl: link.cloudApiUrl,
106
+ token: link.token,
107
+ environments: envsList.map((e) => ({ name: e.name, apiUrl: e.apiUrl })),
108
+ existing: link,
109
+ ...(link.orgId !== undefined ? { orgId: link.orgId } : {}),
110
+ })
111
+ saveProjectLink(cwd, updated)
112
+ console.log(`Environment "${bodyName}" created.`)
113
+ })
114
+ }
115
+
116
+ export function registerLinkOptions(linkCmd: Command): void {
117
+ linkCmd
118
+ .option("--project <slug>", "Cloud project slug")
119
+ .option("--url <url>", "Self-host or local Kong URL")
120
+ .option("--api-url <url>", "Cloud control plane API URL", "https://api.supatype.com")
121
+ .option("--token <token>", "Access token (cloud PAT or self-host SERVICE_ROLE_KEY)")
122
+ .option("--service-role-key <key>", "Deprecated alias for --token on self-host")
123
+ .option("--env <name>", "Environment name (default: production)", "production")
124
+ .option("--fix-gitignore", "Append .supatype/ to .gitignore if missing")
125
+ }
126
+
127
+ export async function runLinkAction(opts: {
128
+ project?: string
129
+ url?: string
130
+ apiUrl: string
131
+ token?: string
132
+ serviceRoleKey?: string
133
+ env?: string
134
+ fixGitignore?: boolean
135
+ }): Promise<void> {
136
+ const cwd = process.cwd()
137
+ const config = loadConfig(cwd)
138
+ const projectRef = config.project?.name ?? "project"
139
+ const envName = opts.env ?? "production"
140
+ const token = resolveLinkToken(opts)
141
+
142
+ if (opts.fixGitignore) {
143
+ ensureSupatypeGitignore(cwd)
144
+ } else {
145
+ warnIfLinkNotGitignored(cwd)
146
+ }
147
+
148
+ const existing = loadProjectLink(cwd)
149
+
150
+ if (opts.url) {
151
+ if (!token) {
152
+ console.error("Authentication required. Pass --token $SERVICE_ROLE_KEY")
153
+ process.exit(1)
154
+ }
155
+ const apiUrl = opts.url.replace(/\/$/, "")
156
+ await probeSelfHostLink(apiUrl, projectRef, token)
157
+ const link = createSelfHostLink({
158
+ projectRef,
159
+ apiUrl,
160
+ token,
161
+ envName,
162
+ existing,
163
+ })
164
+ saveProjectLink(cwd, link)
165
+ console.log(`\nLinked to self-host environment "${envName}" at ${apiUrl}`)
166
+ console.log(`Config saved to .supatype/link.json\n`)
167
+ return
168
+ }
169
+
170
+ if (!token) {
171
+ console.error("Authentication required. Set SUPATYPE_ACCESS_TOKEN or pass --token.")
172
+ process.exit(1)
173
+ }
174
+
175
+ const cloudApiUrl = opts.apiUrl.replace(/\/$/, "")
176
+
177
+ if (opts.project) {
178
+ const one = await targetFetch<{ slug: string; orgId: string }>(cloudApiUrl, "/api/v1", {
179
+ method: "GET",
180
+ path: `/projects/${opts.project}`,
181
+ token,
182
+ })
183
+ let environments: Array<{ name: string; apiUrl: string }> = [
184
+ { name: "production", apiUrl: cloudApiUrl },
185
+ ]
186
+ try {
187
+ const listed = await targetFetch<Array<{ name: string; apiUrl: string }>>(
188
+ cloudApiUrl,
189
+ "/api/v1",
190
+ {
191
+ method: "GET",
192
+ path: `/projects/${opts.project}/environments`,
193
+ token,
194
+ orgId: one.orgId,
195
+ },
196
+ )
197
+ if (listed.length > 0) {
198
+ environments = listed.map((e) => ({ name: e.name, apiUrl: e.apiUrl }))
199
+ }
200
+ } catch {
201
+ // environments optional on older control planes
202
+ }
203
+ const link = createCloudLink({
204
+ projectRef: opts.project,
205
+ cloudApiUrl,
206
+ token,
207
+ orgId: one.orgId,
208
+ environments,
209
+ existing,
210
+ })
211
+ saveProjectLink(cwd, link)
212
+ console.log(`\nLinked to cloud project: ${opts.project}`)
213
+ console.log(`Config saved to .supatype/link.json\n`)
214
+ return
215
+ }
216
+
217
+ console.error("Specify --project <slug> for cloud or --url <kong-url> for self-host.")
218
+ process.exit(1)
219
+ }
220
+
221
+ export function getLinkOrExit(cwd: string): ProjectLink {
222
+ const link = loadProjectLink(cwd)
223
+ if (!link) {
224
+ console.error("Not linked. Run: supatype link")
225
+ process.exit(1)
226
+ }
227
+ return link
228
+ }