@percher/core 0.3.0 → 0.4.1

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 (270) hide show
  1. package/dist/commands/account.d.ts +24 -14
  2. package/dist/commands/account.d.ts.map +1 -1
  3. package/dist/commands/account.js +17 -4
  4. package/dist/commands/account.js.map +1 -1
  5. package/dist/commands/admin-reconcile-routes.d.ts +18 -0
  6. package/dist/commands/admin-reconcile-routes.d.ts.map +1 -0
  7. package/dist/commands/admin-reconcile-routes.js +22 -0
  8. package/dist/commands/admin-reconcile-routes.js.map +1 -0
  9. package/dist/commands/ai-files.d.ts +5 -17
  10. package/dist/commands/ai-files.d.ts.map +1 -1
  11. package/dist/commands/ai-files.js +3 -4
  12. package/dist/commands/ai-files.js.map +1 -1
  13. package/dist/commands/alerts.d.ts +69 -0
  14. package/dist/commands/alerts.d.ts.map +1 -0
  15. package/dist/commands/alerts.js +80 -0
  16. package/dist/commands/alerts.js.map +1 -0
  17. package/dist/commands/app-resources.d.ts +30 -0
  18. package/dist/commands/app-resources.d.ts.map +1 -0
  19. package/dist/commands/app-resources.js +34 -0
  20. package/dist/commands/app-resources.js.map +1 -0
  21. package/dist/commands/app-topology.d.ts +18 -0
  22. package/dist/commands/app-topology.d.ts.map +1 -0
  23. package/dist/commands/app-topology.js +25 -0
  24. package/dist/commands/app-topology.js.map +1 -0
  25. package/dist/commands/billing.d.ts +8 -8
  26. package/dist/commands/billing.d.ts.map +1 -1
  27. package/dist/commands/billing.js +1 -1
  28. package/dist/commands/billing.js.map +1 -1
  29. package/dist/commands/continue.d.ts +1 -1
  30. package/dist/commands/create.d.ts +2 -12
  31. package/dist/commands/create.d.ts.map +1 -1
  32. package/dist/commands/create.js +1 -1
  33. package/dist/commands/create.js.map +1 -1
  34. package/dist/commands/dashboard.d.ts +2 -8
  35. package/dist/commands/dashboard.d.ts.map +1 -1
  36. package/dist/commands/dashboard.js +1 -1
  37. package/dist/commands/dashboard.js.map +1 -1
  38. package/dist/commands/data-export.d.ts +2 -8
  39. package/dist/commands/data-export.d.ts.map +1 -1
  40. package/dist/commands/data-export.js +1 -1
  41. package/dist/commands/data-export.js.map +1 -1
  42. package/dist/commands/data.d.ts +2 -8
  43. package/dist/commands/data.d.ts.map +1 -1
  44. package/dist/commands/data.js +1 -1
  45. package/dist/commands/data.js.map +1 -1
  46. package/dist/commands/delete.d.ts +2 -8
  47. package/dist/commands/delete.d.ts.map +1 -1
  48. package/dist/commands/delete.js +1 -1
  49. package/dist/commands/delete.js.map +1 -1
  50. package/dist/commands/deploys.d.ts +4 -28
  51. package/dist/commands/deploys.d.ts.map +1 -1
  52. package/dist/commands/deploys.js +1 -1
  53. package/dist/commands/deploys.js.map +1 -1
  54. package/dist/commands/dev.d.ts +2 -6
  55. package/dist/commands/dev.d.ts.map +1 -1
  56. package/dist/commands/dev.js +3 -2
  57. package/dist/commands/dev.js.map +1 -1
  58. package/dist/commands/diagnose.d.ts +2 -22
  59. package/dist/commands/diagnose.d.ts.map +1 -1
  60. package/dist/commands/diagnose.js +1 -1
  61. package/dist/commands/diagnose.js.map +1 -1
  62. package/dist/commands/doctor.d.ts +20 -35
  63. package/dist/commands/doctor.d.ts.map +1 -1
  64. package/dist/commands/doctor.js +42 -4
  65. package/dist/commands/doctor.js.map +1 -1
  66. package/dist/commands/domains.d.ts +5 -27
  67. package/dist/commands/domains.d.ts.map +1 -1
  68. package/dist/commands/domains.js +1 -1
  69. package/dist/commands/domains.js.map +1 -1
  70. package/dist/commands/env-scan.js +1 -1
  71. package/dist/commands/env-scan.js.map +1 -1
  72. package/dist/commands/env.d.ts +4 -20
  73. package/dist/commands/env.d.ts.map +1 -1
  74. package/dist/commands/env.js +1 -1
  75. package/dist/commands/env.js.map +1 -1
  76. package/dist/commands/export.d.ts +1 -1
  77. package/dist/commands/forgejo.d.ts +45 -0
  78. package/dist/commands/forgejo.d.ts.map +1 -0
  79. package/dist/commands/forgejo.js +125 -0
  80. package/dist/commands/forgejo.js.map +1 -0
  81. package/dist/commands/generate.d.ts +2 -6
  82. package/dist/commands/generate.d.ts.map +1 -1
  83. package/dist/commands/generate.js +1 -1
  84. package/dist/commands/generate.js.map +1 -1
  85. package/dist/commands/github.d.ts +4 -15
  86. package/dist/commands/github.d.ts.map +1 -1
  87. package/dist/commands/github.js +17 -1
  88. package/dist/commands/github.js.map +1 -1
  89. package/dist/commands/import-project.d.ts +13 -9
  90. package/dist/commands/import-project.d.ts.map +1 -1
  91. package/dist/commands/import-project.js +73 -22
  92. package/dist/commands/import-project.js.map +1 -1
  93. package/dist/commands/init.d.ts +26 -11
  94. package/dist/commands/init.d.ts.map +1 -1
  95. package/dist/commands/init.js +103 -2
  96. package/dist/commands/init.js.map +1 -1
  97. package/dist/commands/insights.d.ts +2 -6
  98. package/dist/commands/insights.d.ts.map +1 -1
  99. package/dist/commands/insights.js +1 -1
  100. package/dist/commands/insights.js.map +1 -1
  101. package/dist/commands/login.d.ts +2 -8
  102. package/dist/commands/login.d.ts.map +1 -1
  103. package/dist/commands/login.js +22 -1
  104. package/dist/commands/login.js.map +1 -1
  105. package/dist/commands/logs.d.ts +25 -10
  106. package/dist/commands/logs.d.ts.map +1 -1
  107. package/dist/commands/logs.js +65 -5
  108. package/dist/commands/logs.js.map +1 -1
  109. package/dist/commands/mcp.d.ts +2 -2
  110. package/dist/commands/mcp.d.ts.map +1 -1
  111. package/dist/commands/mcp.js +1 -1
  112. package/dist/commands/mcp.js.map +1 -1
  113. package/dist/commands/migrate-supabase-map.d.ts +171 -0
  114. package/dist/commands/migrate-supabase-map.d.ts.map +1 -0
  115. package/dist/commands/migrate-supabase-map.js +452 -0
  116. package/dist/commands/migrate-supabase-map.js.map +1 -0
  117. package/dist/commands/migrate-supabase-schema.d.ts +67 -0
  118. package/dist/commands/migrate-supabase-schema.d.ts.map +1 -0
  119. package/dist/commands/migrate-supabase-schema.js +321 -0
  120. package/dist/commands/migrate-supabase-schema.js.map +1 -0
  121. package/dist/commands/migrate-supabase-scripts.d.ts +64 -0
  122. package/dist/commands/migrate-supabase-scripts.d.ts.map +1 -0
  123. package/dist/commands/migrate-supabase-scripts.js +564 -0
  124. package/dist/commands/migrate-supabase-scripts.js.map +1 -0
  125. package/dist/commands/migrate-supabase-sdk.d.ts +133 -0
  126. package/dist/commands/migrate-supabase-sdk.d.ts.map +1 -0
  127. package/dist/commands/migrate-supabase-sdk.js +1119 -0
  128. package/dist/commands/migrate-supabase-sdk.js.map +1 -0
  129. package/dist/commands/migrate-supabase-walker.d.ts +93 -0
  130. package/dist/commands/migrate-supabase-walker.d.ts.map +1 -0
  131. package/dist/commands/migrate-supabase-walker.js +413 -0
  132. package/dist/commands/migrate-supabase-walker.js.map +1 -0
  133. package/dist/commands/migrate-supabase.d.ts +81 -0
  134. package/dist/commands/migrate-supabase.d.ts.map +1 -0
  135. package/dist/commands/migrate-supabase.js +579 -0
  136. package/dist/commands/migrate-supabase.js.map +1 -0
  137. package/dist/commands/open.d.ts +2 -6
  138. package/dist/commands/open.d.ts.map +1 -1
  139. package/dist/commands/open.js +1 -1
  140. package/dist/commands/open.js.map +1 -1
  141. package/dist/commands/publish-api-error.d.ts +46 -0
  142. package/dist/commands/publish-api-error.d.ts.map +1 -0
  143. package/dist/commands/publish-api-error.js +307 -0
  144. package/dist/commands/publish-api-error.js.map +1 -0
  145. package/dist/commands/publish-failure.d.ts.map +1 -1
  146. package/dist/commands/publish-failure.js +11 -3
  147. package/dist/commands/publish-failure.js.map +1 -1
  148. package/dist/commands/publish-node.d.ts +5 -2
  149. package/dist/commands/publish-node.d.ts.map +1 -1
  150. package/dist/commands/publish-node.js +7 -3
  151. package/dist/commands/publish-node.js.map +1 -1
  152. package/dist/commands/publish.d.ts +58 -17
  153. package/dist/commands/publish.d.ts.map +1 -1
  154. package/dist/commands/publish.js +407 -145
  155. package/dist/commands/publish.js.map +1 -1
  156. package/dist/commands/push.d.ts +2 -12
  157. package/dist/commands/push.d.ts.map +1 -1
  158. package/dist/commands/push.js +25 -7
  159. package/dist/commands/push.js.map +1 -1
  160. package/dist/commands/redeploy.d.ts +2 -8
  161. package/dist/commands/redeploy.d.ts.map +1 -1
  162. package/dist/commands/redeploy.js +20 -16
  163. package/dist/commands/redeploy.js.map +1 -1
  164. package/dist/commands/rename.d.ts +2 -8
  165. package/dist/commands/rename.d.ts.map +1 -1
  166. package/dist/commands/rename.js +1 -1
  167. package/dist/commands/rename.js.map +1 -1
  168. package/dist/commands/reproduce.d.ts +2 -8
  169. package/dist/commands/reproduce.d.ts.map +1 -1
  170. package/dist/commands/reproduce.js +1 -1
  171. package/dist/commands/reproduce.js.map +1 -1
  172. package/dist/commands/reset-superuser.d.ts +2 -16
  173. package/dist/commands/reset-superuser.d.ts.map +1 -1
  174. package/dist/commands/reset-superuser.js +1 -1
  175. package/dist/commands/reset-superuser.js.map +1 -1
  176. package/dist/commands/restore.d.ts +7 -22
  177. package/dist/commands/restore.d.ts.map +1 -1
  178. package/dist/commands/restore.js +1 -1
  179. package/dist/commands/restore.js.map +1 -1
  180. package/dist/commands/resume.d.ts +2 -6
  181. package/dist/commands/resume.d.ts.map +1 -1
  182. package/dist/commands/resume.js +1 -1
  183. package/dist/commands/resume.js.map +1 -1
  184. package/dist/commands/rollback.d.ts +4 -9
  185. package/dist/commands/rollback.d.ts.map +1 -1
  186. package/dist/commands/rollback.js +3 -2
  187. package/dist/commands/rollback.js.map +1 -1
  188. package/dist/commands/sharing.d.ts +48 -0
  189. package/dist/commands/sharing.d.ts.map +1 -0
  190. package/dist/commands/sharing.js +85 -0
  191. package/dist/commands/sharing.js.map +1 -0
  192. package/dist/commands/status.d.ts +29 -0
  193. package/dist/commands/status.d.ts.map +1 -0
  194. package/dist/commands/status.js +48 -0
  195. package/dist/commands/status.js.map +1 -0
  196. package/dist/commands/transfers.d.ts +34 -0
  197. package/dist/commands/transfers.d.ts.map +1 -0
  198. package/dist/commands/transfers.js +62 -0
  199. package/dist/commands/transfers.js.map +1 -0
  200. package/dist/commands/unsuspend.d.ts +2 -6
  201. package/dist/commands/unsuspend.d.ts.map +1 -1
  202. package/dist/commands/unsuspend.js +1 -1
  203. package/dist/commands/unsuspend.js.map +1 -1
  204. package/dist/commands/versions.d.ts +2 -6
  205. package/dist/commands/versions.d.ts.map +1 -1
  206. package/dist/commands/versions.js +1 -1
  207. package/dist/commands/versions.js.map +1 -1
  208. package/dist/commands/wait-deploy.d.ts +2 -12
  209. package/dist/commands/wait-deploy.d.ts.map +1 -1
  210. package/dist/commands/wait-deploy.js +5 -4
  211. package/dist/commands/wait-deploy.js.map +1 -1
  212. package/dist/context.d.ts +15 -0
  213. package/dist/context.d.ts.map +1 -1
  214. package/dist/detect.d.ts +11 -0
  215. package/dist/detect.d.ts.map +1 -1
  216. package/dist/detect.js +31 -8
  217. package/dist/detect.js.map +1 -1
  218. package/dist/env-scan-source.js +1 -1
  219. package/dist/env-scan-source.js.map +1 -1
  220. package/dist/error-classifier.d.ts +17 -0
  221. package/dist/error-classifier.d.ts.map +1 -1
  222. package/dist/error-classifier.js +95 -9
  223. package/dist/error-classifier.js.map +1 -1
  224. package/dist/errors.d.ts +1 -1
  225. package/dist/errors.d.ts.map +1 -1
  226. package/dist/errors.js.map +1 -1
  227. package/dist/event-renderer.d.ts +17 -0
  228. package/dist/event-renderer.d.ts.map +1 -0
  229. package/dist/event-renderer.js +130 -0
  230. package/dist/event-renderer.js.map +1 -0
  231. package/dist/index.d.ts +63 -47
  232. package/dist/index.d.ts.map +1 -1
  233. package/dist/index.js +56 -40
  234. package/dist/index.js.map +1 -1
  235. package/dist/plans.d.ts +70 -5
  236. package/dist/plans.d.ts.map +1 -1
  237. package/dist/plans.js +83 -18
  238. package/dist/plans.js.map +1 -1
  239. package/dist/poll-deployment.d.ts +13 -1
  240. package/dist/poll-deployment.d.ts.map +1 -1
  241. package/dist/poll-deployment.js +37 -1
  242. package/dist/poll-deployment.js.map +1 -1
  243. package/dist/publish-retry.d.ts +29 -0
  244. package/dist/publish-retry.d.ts.map +1 -0
  245. package/dist/publish-retry.js +224 -0
  246. package/dist/publish-retry.js.map +1 -0
  247. package/dist/recovery.d.ts +60 -3
  248. package/dist/recovery.d.ts.map +1 -1
  249. package/dist/recovery.js +24 -1
  250. package/dist/recovery.js.map +1 -1
  251. package/dist/static-docker.d.ts +77 -0
  252. package/dist/static-docker.d.ts.map +1 -0
  253. package/dist/static-docker.js +105 -0
  254. package/dist/static-docker.js.map +1 -0
  255. package/dist/structured-error-codes.d.ts +30 -0
  256. package/dist/structured-error-codes.d.ts.map +1 -0
  257. package/dist/structured-error-codes.js +86 -0
  258. package/dist/structured-error-codes.js.map +1 -0
  259. package/dist/tarball.d.ts +11 -0
  260. package/dist/tarball.d.ts.map +1 -1
  261. package/dist/tarball.js +31 -10
  262. package/dist/tarball.js.map +1 -1
  263. package/dist/templates/ai-files/cursor-percher-mdc.d.ts.map +1 -1
  264. package/dist/templates/ai-files/cursor-percher-mdc.js +12 -9
  265. package/dist/templates/ai-files/cursor-percher-mdc.js.map +1 -1
  266. package/dist/templates.js +11 -11
  267. package/dist/templates.js.map +1 -1
  268. package/dist/watcher.js +1 -1
  269. package/dist/watcher.js.map +1 -1
  270. package/package.json +7 -2
@@ -0,0 +1,81 @@
1
+ import { z } from "zod";
2
+ import type { Context } from "../context";
3
+ import { type AskUserRecovery, type NoneRecovery, type ToolRecovery } from "../recovery";
4
+ /**
5
+ * FUTURE9 Phase A — Supabase schema introspection.
6
+ *
7
+ * Client-side only: the Supabase personal access token is a
8
+ * high-privilege credential (full project access). We never route
9
+ * it through Percher's servers — the call goes from the user's
10
+ * process directly to api.supabase.com. See decision 1 in
11
+ * docs/plans/FUTURE9_fas-8-lovable-migration-plan.md.
12
+ *
13
+ * This command performs no file writes; it returns a structured
14
+ * schema description for later phases (B–D) to build on, and so
15
+ * the user can sanity-check what would migrate before committing.
16
+ */
17
+ export declare const inspectSupabaseInputSchema: z.ZodObject<{
18
+ projectRef: z.ZodString;
19
+ token: z.ZodString;
20
+ }, z.core.$strip>;
21
+ export type InspectSupabaseInput = z.infer<typeof inspectSupabaseInputSchema>;
22
+ export interface SupabaseForeignKey {
23
+ table: string;
24
+ column: string;
25
+ }
26
+ export interface SupabaseColumn {
27
+ name: string;
28
+ postgresType: string;
29
+ isNullable: boolean;
30
+ hasDefault: boolean;
31
+ defaultExpr: string | null;
32
+ isPrimaryKey: boolean;
33
+ foreignKey: SupabaseForeignKey | null;
34
+ }
35
+ export interface SupabaseRlsPolicy {
36
+ name: string;
37
+ command: string;
38
+ usingExpr: string | null;
39
+ withCheckExpr: string | null;
40
+ }
41
+ export interface SupabaseTable {
42
+ schema: string;
43
+ name: string;
44
+ columns: SupabaseColumn[];
45
+ rlsPolicies: SupabaseRlsPolicy[];
46
+ estimatedRowCount: number;
47
+ }
48
+ export interface SupabaseStorageBucket {
49
+ id: string;
50
+ name: string;
51
+ public: boolean;
52
+ }
53
+ export type InspectSupabaseStatus = "ok" | "failed";
54
+ export interface InspectSupabaseError {
55
+ code: "auth_failed" | "project_not_found" | "project_paused" | "query_timeout" | "network_error" | "unexpected_response";
56
+ title: string;
57
+ explanation: string;
58
+ suggestion: string;
59
+ }
60
+ export interface InspectSupabaseResult {
61
+ status: InspectSupabaseStatus;
62
+ projectRef: string;
63
+ tables: SupabaseTable[];
64
+ authUsersCount: number | null;
65
+ storageBuckets: SupabaseStorageBucket[];
66
+ warnings: string[];
67
+ error?: InspectSupabaseError;
68
+ recovery: ToolRecovery | NoneRecovery | AskUserRecovery;
69
+ summary: string;
70
+ }
71
+ export declare function inspectSupabase(ctx: Context, input: InspectSupabaseInput): Promise<InspectSupabaseResult>;
72
+ /**
73
+ * Helper for downstream code (phases B–D) to filter tables to just the
74
+ * ones we'd migrate — i.e. nothing in a Supabase-managed system schema.
75
+ * Phase A returns these already filtered, but the predicate is exported
76
+ * so a caller can re-filter a result they've stored.
77
+ */
78
+ export declare function isMigratableTable(table: {
79
+ schema: string;
80
+ }): boolean;
81
+ //# sourceMappingURL=migrate-supabase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate-supabase.d.ts","sourceRoot":"","sources":["../../src/commands/migrate-supabase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,YAAY,EAGjB,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;GAYG;AAEH,eAAO,MAAM,0BAA0B;;;iBAarC,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,0BAA0B,CAAC,CAAC;AAY9E,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACvC;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,qBAAqB,GAAG,IAAI,GAAG,QAAQ,CAAC;AAEpD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EACA,aAAa,GACb,mBAAmB,GACnB,gBAAgB,GAChB,eAAe,GACf,eAAe,GACf,qBAAqB,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,qBAAqB,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,qBAAqB,EAAE,CAAC;IACxC,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,QAAQ,EAAE,YAAY,GAAG,YAAY,GAAG,eAAe,CAAC;IACxD,OAAO,EAAE,MAAM,CAAC;CACjB;AA4JD,wBAAsB,eAAe,CACnC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,oBAAoB,GAC1B,OAAO,CAAC,qBAAqB,CAAC,CAsLhC;AAyQD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAEpE"}
@@ -0,0 +1,579 @@
1
+ import { z } from "zod";
2
+ import { recoveryAsk, recoveryNone, } from "../recovery";
3
+ /**
4
+ * FUTURE9 Phase A — Supabase schema introspection.
5
+ *
6
+ * Client-side only: the Supabase personal access token is a
7
+ * high-privilege credential (full project access). We never route
8
+ * it through Percher's servers — the call goes from the user's
9
+ * process directly to api.supabase.com. See decision 1 in
10
+ * docs/plans/FUTURE9_fas-8-lovable-migration-plan.md.
11
+ *
12
+ * This command performs no file writes; it returns a structured
13
+ * schema description for later phases (B–D) to build on, and so
14
+ * the user can sanity-check what would migrate before committing.
15
+ */
16
+ export const inspectSupabaseInputSchema = z.object({
17
+ projectRef: z
18
+ .string()
19
+ .min(1, "projectRef is required")
20
+ .describe("Supabase project reference (the 20-char id from the project URL — e.g. `abcdefghijklmnopqrst`)."),
21
+ token: z
22
+ .string()
23
+ .min(1, "token is required")
24
+ .describe("Supabase personal access token (sbp_...). Never leaves the user's machine — sent directly to api.supabase.com, not via Percher."),
25
+ });
26
+ /**
27
+ * Production Supabase Management API host. Hardcoded — not overridable
28
+ * via the public input schema or CLI flags, because the input carries
29
+ * a high-privilege personal access token and the tool's contract
30
+ * promises the call goes only to api.supabase.com. Tests intercept
31
+ * fetch at the global level instead (they don't need to redirect to
32
+ * a different host to assert behavior — the mock catches every call).
33
+ */
34
+ const SUPABASE_API_BASE_URL = "https://api.supabase.com";
35
+ // User-schema reserved set — these are Supabase platform-managed
36
+ // or Postgres internals we don't migrate. Mirror this in the SQL
37
+ // WHERE clause; keep the TS list for downstream phases that need
38
+ // to filter post-hoc.
39
+ const SYSTEM_SCHEMAS = new Set([
40
+ "pg_catalog",
41
+ "information_schema",
42
+ "pgsodium",
43
+ "pgsodium_masks",
44
+ "vault",
45
+ "extensions",
46
+ "pgbouncer",
47
+ "realtime",
48
+ "_realtime",
49
+ "graphql",
50
+ "graphql_public",
51
+ "supabase_functions",
52
+ "supabase_migrations",
53
+ "auth",
54
+ "storage",
55
+ "net",
56
+ ]);
57
+ const INTROSPECTION_SQL = `
58
+ WITH
59
+ pks AS (
60
+ SELECT tc.table_schema, tc.table_name, kcu.column_name
61
+ FROM information_schema.table_constraints tc
62
+ JOIN information_schema.key_column_usage kcu
63
+ ON tc.constraint_name = kcu.constraint_name
64
+ AND tc.table_schema = kcu.table_schema
65
+ WHERE tc.constraint_type = 'PRIMARY KEY'
66
+ ),
67
+ fks AS (
68
+ SELECT tc.table_schema, tc.table_name, kcu.column_name,
69
+ ccu.table_name AS foreign_table,
70
+ ccu.column_name AS foreign_column
71
+ FROM information_schema.table_constraints tc
72
+ JOIN information_schema.key_column_usage kcu
73
+ ON tc.constraint_name = kcu.constraint_name
74
+ AND tc.table_schema = kcu.table_schema
75
+ JOIN information_schema.constraint_column_usage ccu
76
+ ON tc.constraint_name = ccu.constraint_name
77
+ AND tc.table_schema = ccu.table_schema
78
+ WHERE tc.constraint_type = 'FOREIGN KEY'
79
+ ),
80
+ cols AS (
81
+ SELECT
82
+ c.table_schema, c.table_name, c.column_name,
83
+ -- information_schema.columns reports array columns as data_type='ARRAY'
84
+ -- with the element type hidden in udt_name (prefixed with an
85
+ -- underscore — e.g. text[] → udt_name='_text'). Format here so the
86
+ -- result carries the user-friendly Postgres syntax (text[], int4[])
87
+ -- and so downstream type-mapping (Phase B) can pattern-match on the
88
+ -- ANSI shape rather than the catalog quirk.
89
+ CASE
90
+ WHEN c.data_type = 'ARRAY' THEN substr(c.udt_name, 2) || '[]'
91
+ ELSE c.data_type
92
+ END AS data_type,
93
+ c.is_nullable, c.column_default, c.ordinal_position,
94
+ (pk.column_name IS NOT NULL) AS is_primary_key,
95
+ CASE WHEN fk.foreign_table IS NOT NULL
96
+ THEN json_build_object('table', fk.foreign_table, 'column', fk.foreign_column)
97
+ END AS foreign_key
98
+ FROM information_schema.columns c
99
+ LEFT JOIN pks pk
100
+ ON c.table_schema = pk.table_schema
101
+ AND c.table_name = pk.table_name
102
+ AND c.column_name = pk.column_name
103
+ LEFT JOIN fks fk
104
+ ON c.table_schema = fk.table_schema
105
+ AND c.table_name = fk.table_name
106
+ AND c.column_name = fk.column_name
107
+ WHERE c.table_schema NOT IN (
108
+ 'pg_catalog','information_schema','pgsodium','pgsodium_masks','vault',
109
+ 'extensions','pgbouncer','realtime','_realtime','graphql','graphql_public',
110
+ 'supabase_functions','supabase_migrations','auth','storage','net'
111
+ )
112
+ ),
113
+ tables_json AS (
114
+ SELECT
115
+ table_schema,
116
+ table_name,
117
+ json_agg(
118
+ json_build_object(
119
+ 'name', column_name,
120
+ 'postgresType', data_type,
121
+ 'isNullable', is_nullable = 'YES',
122
+ 'hasDefault', column_default IS NOT NULL,
123
+ 'defaultExpr', column_default,
124
+ 'isPrimaryKey', is_primary_key,
125
+ 'foreignKey', foreign_key
126
+ ) ORDER BY ordinal_position
127
+ ) AS columns
128
+ FROM cols
129
+ GROUP BY table_schema, table_name
130
+ ),
131
+ policies AS (
132
+ SELECT schemaname AS table_schema, tablename AS table_name,
133
+ json_agg(json_build_object(
134
+ 'name', policyname,
135
+ 'command', cmd,
136
+ 'usingExpr', qual,
137
+ 'withCheckExpr', with_check
138
+ )) AS policies
139
+ FROM pg_policies
140
+ GROUP BY schemaname, tablename
141
+ ),
142
+ row_counts AS (
143
+ SELECT n.nspname AS table_schema, c.relname AS table_name,
144
+ GREATEST(c.reltuples, 0)::bigint AS estimated_row_count
145
+ FROM pg_class c
146
+ JOIN pg_namespace n ON n.oid = c.relnamespace
147
+ WHERE c.relkind = 'r'
148
+ ),
149
+ tables_full AS (
150
+ SELECT
151
+ t.table_schema,
152
+ t.table_name,
153
+ t.columns,
154
+ COALESCE(p.policies, '[]'::json) AS rls_policies,
155
+ COALESCE(r.estimated_row_count, 0) AS estimated_row_count
156
+ FROM tables_json t
157
+ LEFT JOIN policies p
158
+ ON t.table_schema = p.table_schema AND t.table_name = p.table_name
159
+ LEFT JOIN row_counts r
160
+ ON t.table_schema = r.table_schema AND t.table_name = r.table_name
161
+ )
162
+ SELECT json_build_object(
163
+ 'tables', COALESCE((
164
+ SELECT json_agg(json_build_object(
165
+ 'schema', table_schema,
166
+ 'name', table_name,
167
+ 'columns', columns,
168
+ 'rlsPolicies', rls_policies,
169
+ 'estimatedRowCount', estimated_row_count
170
+ ) ORDER BY table_schema, table_name) FROM tables_full
171
+ ), '[]'::json),
172
+ 'storageBuckets', COALESCE((
173
+ SELECT json_agg(json_build_object('id', id, 'name', name, 'public', public))
174
+ FROM storage.buckets
175
+ ), '[]'::json),
176
+ 'authUsersCount', (SELECT count(*)::bigint FROM auth.users)
177
+ ) AS schema;
178
+ `;
179
+ export async function inspectSupabase(ctx, input) {
180
+ const url = `${SUPABASE_API_BASE_URL}/v1/projects/${encodeURIComponent(input.projectRef)}/database/query`;
181
+ ctx.status(`Inspecting Supabase project ${input.projectRef}...`);
182
+ let response;
183
+ try {
184
+ response = await fetch(url, {
185
+ method: "POST",
186
+ headers: {
187
+ "content-type": "application/json",
188
+ authorization: `Bearer ${input.token}`,
189
+ accept: "application/json",
190
+ },
191
+ body: JSON.stringify({ query: INTROSPECTION_SQL }),
192
+ });
193
+ }
194
+ catch (err) {
195
+ return failure({
196
+ projectRef: input.projectRef,
197
+ code: "network_error",
198
+ title: "Couldn't reach Supabase",
199
+ explanation: `Network error contacting ${SUPABASE_API_BASE_URL}: ${err instanceof Error ? err.message : String(err)}.`,
200
+ suggestion: "Check your internet connection and try again.",
201
+ // No `retry` recovery: the SuggestedTool union doesn't yet include
202
+ // `percher_inspect_supabase`, and we don't want to round-trip the
203
+ // Supabase PAT through `recovery.args` (where it can land in logs
204
+ // and telemetry). Ask the user to rerun the command — they hold
205
+ // the token locally and can re-issue with no recovery-channel
206
+ // leak.
207
+ recovery: recoveryAsk({
208
+ prompt: `Couldn't reach Supabase. Check your connection and re-run \`bunx percher migrate-from-supabase --project ${input.projectRef} --token <token>\`.`,
209
+ reasonCode: "infra_transient",
210
+ retryable: true,
211
+ }),
212
+ });
213
+ }
214
+ if (response.status === 401 || response.status === 403) {
215
+ return failure({
216
+ projectRef: input.projectRef,
217
+ code: "auth_failed",
218
+ title: "Supabase token rejected",
219
+ explanation: "The Supabase personal access token was rejected (401/403). It may be expired, revoked, or lack the required project scope.",
220
+ suggestion: "Generate a fresh token at https://supabase.com/dashboard/account/tokens and pass it via --token.",
221
+ recovery: recoveryAsk({
222
+ prompt: "Your Supabase access token was rejected. Generate a new one at https://supabase.com/dashboard/account/tokens and re-run with --token <new-token>.",
223
+ reasonCode: "auth_required",
224
+ }),
225
+ });
226
+ }
227
+ if (response.status === 404) {
228
+ return failure({
229
+ projectRef: input.projectRef,
230
+ code: "project_not_found",
231
+ title: "Supabase project not found",
232
+ explanation: `Project ${input.projectRef} doesn't exist or the token doesn't have access to it.`,
233
+ suggestion: "Check the project ref on https://supabase.com/dashboard and confirm the token belongs to the same account.",
234
+ recovery: recoveryAsk({
235
+ prompt: `Supabase project "${input.projectRef}" was not found. Verify the project ref at https://supabase.com/dashboard and re-run with the correct --project.`,
236
+ reasonCode: "app_not_found",
237
+ }),
238
+ });
239
+ }
240
+ const rawBody = await safeReadText(response);
241
+ if (response.status === 408 || response.status === 504) {
242
+ return failure({
243
+ projectRef: input.projectRef,
244
+ code: "query_timeout",
245
+ title: "Schema query timed out",
246
+ explanation: "Supabase took too long to run the introspection query. This usually happens on very large databases.",
247
+ suggestion: "Re-run later, or contact Percher if it keeps timing out — we may need to chunk the introspection for huge schemas.",
248
+ // See network_error above for why this is ask_user not retry.
249
+ recovery: recoveryAsk({
250
+ prompt: `Supabase took too long to run the introspection query. Re-run \`bunx percher migrate-from-supabase --project ${input.projectRef} --token <token>\` in a moment, or open an issue if it keeps timing out.`,
251
+ reasonCode: "infra_transient",
252
+ retryable: true,
253
+ }),
254
+ });
255
+ }
256
+ if (response.status === 503) {
257
+ const body = parseJsonSafe(rawBody);
258
+ const message = extractErrorMessage(body) ?? "Supabase project is unavailable.";
259
+ if (looksLikePaused(message)) {
260
+ return failure({
261
+ projectRef: input.projectRef,
262
+ code: "project_paused",
263
+ title: "Supabase project is paused",
264
+ explanation: "Supabase auto-pauses inactive free-tier projects. Schema introspection can't run while the project is paused.",
265
+ suggestion: "Unpause the project from https://supabase.com/dashboard and retry once it's running.",
266
+ recovery: recoveryAsk({
267
+ prompt: `Supabase project "${input.projectRef}" is paused. Unpause it at https://supabase.com/dashboard, wait until it's running, and re-run the same command.`,
268
+ reasonCode: "infra_unavailable",
269
+ }),
270
+ });
271
+ }
272
+ }
273
+ if (!response.ok) {
274
+ return failure({
275
+ projectRef: input.projectRef,
276
+ code: "unexpected_response",
277
+ title: `Supabase returned ${response.status}`,
278
+ explanation: `Supabase Management API responded with HTTP ${response.status}: ${truncate(rawBody, 400)}`,
279
+ suggestion: "Inspect the response body above. If the issue is transient, retry; otherwise verify the token's scopes.",
280
+ recovery: recoveryAsk({
281
+ prompt: `Supabase Management API responded with HTTP ${response.status}. Inspect the message and decide whether to retry, switch tokens, or skip migration.`,
282
+ reasonCode: "unknown",
283
+ }),
284
+ });
285
+ }
286
+ const parsed = parseJsonSafe(rawBody);
287
+ const rows = extractRows(parsed);
288
+ if (!rows || rows.length === 0) {
289
+ return failure({
290
+ projectRef: input.projectRef,
291
+ code: "unexpected_response",
292
+ title: "Supabase returned an empty result",
293
+ explanation: "The Management API accepted the query but returned no rows. This is unexpected — the introspection query always returns at least one aggregate row.",
294
+ suggestion: "Re-run the command; if it persists, file an issue with the project ref.",
295
+ // See network_error above for why this is ask_user not retry.
296
+ recovery: recoveryAsk({
297
+ prompt: `Supabase returned an empty result for the schema query — unexpected. Re-run \`bunx percher migrate-from-supabase --project ${input.projectRef} --token <token>\`; if it persists, file an issue.`,
298
+ reasonCode: "infra_transient",
299
+ retryable: true,
300
+ }),
301
+ });
302
+ }
303
+ const schemaBlob = rows[0]?.schema;
304
+ if (!schemaBlob || typeof schemaBlob !== "object") {
305
+ return failure({
306
+ projectRef: input.projectRef,
307
+ code: "unexpected_response",
308
+ title: "Couldn't parse Supabase response",
309
+ explanation: `Expected a JSON object under \`schema\`; got: ${truncate(rawBody, 400)}.`,
310
+ suggestion: "Re-run; if it persists, file an issue with the response excerpt.",
311
+ recovery: recoveryAsk({
312
+ prompt: "Supabase returned a response the inspector couldn't parse. Try again, or open an issue with the response excerpt.",
313
+ reasonCode: "unknown",
314
+ }),
315
+ });
316
+ }
317
+ const tables = normalizeTables(schemaBlob.tables);
318
+ const storageBuckets = normalizeBuckets(schemaBlob.storageBuckets);
319
+ const authUsersCount = toFiniteNumber(schemaBlob.authUsersCount);
320
+ const warnings = collectWarnings(tables, storageBuckets, authUsersCount);
321
+ const summary = renderSummary({
322
+ projectRef: input.projectRef,
323
+ tables,
324
+ authUsersCount,
325
+ storageBuckets,
326
+ warnings,
327
+ });
328
+ return {
329
+ status: "ok",
330
+ projectRef: input.projectRef,
331
+ tables,
332
+ authUsersCount,
333
+ storageBuckets,
334
+ warnings,
335
+ recovery: recoveryNone(),
336
+ summary,
337
+ };
338
+ }
339
+ function failure(args) {
340
+ return {
341
+ status: "failed",
342
+ projectRef: args.projectRef,
343
+ tables: [],
344
+ authUsersCount: null,
345
+ storageBuckets: [],
346
+ warnings: [],
347
+ error: {
348
+ code: args.code,
349
+ title: args.title,
350
+ explanation: args.explanation,
351
+ suggestion: args.suggestion,
352
+ },
353
+ recovery: args.recovery,
354
+ summary: `${args.title}. ${args.suggestion}`,
355
+ };
356
+ }
357
+ async function safeReadText(response) {
358
+ try {
359
+ return await response.text();
360
+ }
361
+ catch {
362
+ return "";
363
+ }
364
+ }
365
+ function parseJsonSafe(text) {
366
+ if (!text)
367
+ return null;
368
+ try {
369
+ return JSON.parse(text);
370
+ }
371
+ catch {
372
+ return null;
373
+ }
374
+ }
375
+ function extractRows(body) {
376
+ if (Array.isArray(body))
377
+ return body;
378
+ if (body && typeof body === "object") {
379
+ const maybeRows = body.result ?? body.rows;
380
+ if (Array.isArray(maybeRows))
381
+ return maybeRows;
382
+ }
383
+ return null;
384
+ }
385
+ function extractErrorMessage(body) {
386
+ if (!body || typeof body !== "object")
387
+ return null;
388
+ const candidate = body.message ??
389
+ body.error?.message ??
390
+ body.msg;
391
+ return typeof candidate === "string" ? candidate : null;
392
+ }
393
+ function looksLikePaused(message) {
394
+ const lower = message.toLowerCase();
395
+ return lower.includes("paused") || lower.includes("inactive");
396
+ }
397
+ function normalizeTables(input) {
398
+ if (!Array.isArray(input))
399
+ return [];
400
+ const tables = [];
401
+ for (const raw of input) {
402
+ if (!raw || typeof raw !== "object")
403
+ continue;
404
+ const r = raw;
405
+ const schema = typeof r.schema === "string" ? r.schema : "";
406
+ const name = typeof r.name === "string" ? r.name : "";
407
+ if (!schema || !name)
408
+ continue;
409
+ if (SYSTEM_SCHEMAS.has(schema))
410
+ continue;
411
+ tables.push({
412
+ schema,
413
+ name,
414
+ columns: normalizeColumns(r.columns),
415
+ rlsPolicies: normalizeRlsPolicies(r.rlsPolicies),
416
+ estimatedRowCount: toFiniteNumber(r.estimatedRowCount) ?? 0,
417
+ });
418
+ }
419
+ return tables;
420
+ }
421
+ function normalizeColumns(input) {
422
+ if (!Array.isArray(input))
423
+ return [];
424
+ const cols = [];
425
+ for (const raw of input) {
426
+ if (!raw || typeof raw !== "object")
427
+ continue;
428
+ const r = raw;
429
+ const name = typeof r.name === "string" ? r.name : "";
430
+ const postgresType = typeof r.postgresType === "string" ? r.postgresType : "";
431
+ if (!name || !postgresType)
432
+ continue;
433
+ cols.push({
434
+ name,
435
+ postgresType,
436
+ isNullable: r.isNullable === true,
437
+ hasDefault: r.hasDefault === true,
438
+ defaultExpr: typeof r.defaultExpr === "string" ? r.defaultExpr : null,
439
+ isPrimaryKey: r.isPrimaryKey === true,
440
+ foreignKey: normalizeForeignKey(r.foreignKey),
441
+ });
442
+ }
443
+ return cols;
444
+ }
445
+ function normalizeForeignKey(input) {
446
+ if (!input || typeof input !== "object")
447
+ return null;
448
+ const r = input;
449
+ const table = typeof r.table === "string" ? r.table : "";
450
+ const column = typeof r.column === "string" ? r.column : "";
451
+ if (!table || !column)
452
+ return null;
453
+ return { table, column };
454
+ }
455
+ function normalizeRlsPolicies(input) {
456
+ if (!Array.isArray(input))
457
+ return [];
458
+ const policies = [];
459
+ for (const raw of input) {
460
+ if (!raw || typeof raw !== "object")
461
+ continue;
462
+ const r = raw;
463
+ const name = typeof r.name === "string" ? r.name : "";
464
+ const command = typeof r.command === "string" ? r.command : "";
465
+ if (!name)
466
+ continue;
467
+ policies.push({
468
+ name,
469
+ command,
470
+ usingExpr: typeof r.usingExpr === "string" ? r.usingExpr : null,
471
+ withCheckExpr: typeof r.withCheckExpr === "string" ? r.withCheckExpr : null,
472
+ });
473
+ }
474
+ return policies;
475
+ }
476
+ function normalizeBuckets(input) {
477
+ if (!Array.isArray(input))
478
+ return [];
479
+ const buckets = [];
480
+ for (const raw of input) {
481
+ if (!raw || typeof raw !== "object")
482
+ continue;
483
+ const r = raw;
484
+ const id = typeof r.id === "string" ? r.id : "";
485
+ const name = typeof r.name === "string" ? r.name : "";
486
+ if (!id || !name)
487
+ continue;
488
+ buckets.push({ id, name, public: r.public === true });
489
+ }
490
+ return buckets;
491
+ }
492
+ function toFiniteNumber(input) {
493
+ if (typeof input === "number" && Number.isFinite(input))
494
+ return input;
495
+ if (typeof input === "string") {
496
+ const n = Number(input);
497
+ return Number.isFinite(n) ? n : null;
498
+ }
499
+ return null;
500
+ }
501
+ function truncate(s, max) {
502
+ if (s.length <= max)
503
+ return s;
504
+ return `${s.slice(0, max)}…`;
505
+ }
506
+ /**
507
+ * True for any Postgres array column. Accepts both the formatted
508
+ * `text[]` shape that our SQL emits AND the raw `ARRAY` string that
509
+ * information_schema reports without formatting. The second branch is
510
+ * the defensive one — if Supabase ever changes their query envelope or
511
+ * a regression strips the SQL's array-formatting CASE, the warning
512
+ * still fires instead of going silent.
513
+ */
514
+ function isArrayType(postgresType) {
515
+ return postgresType === "ARRAY" || postgresType.endsWith("[]");
516
+ }
517
+ function collectWarnings(tables, buckets, authUsersCount) {
518
+ const warnings = [];
519
+ for (const table of tables) {
520
+ const pkCount = table.columns.filter((c) => c.isPrimaryKey).length;
521
+ if (pkCount === 0) {
522
+ warnings.push(`Table "${table.schema}.${table.name}" has no primary key — PocketBase requires one. A synthetic id will be generated.`);
523
+ }
524
+ else if (pkCount > 1) {
525
+ warnings.push(`Table "${table.schema}.${table.name}" has a composite primary key (${pkCount} columns) — PocketBase only supports a single id; needs manual review.`);
526
+ }
527
+ for (const col of table.columns) {
528
+ // The SQL collapses information_schema's `ARRAY` data_type +
529
+ // underscore-prefixed udt_name into the ANSI `text[]` syntax.
530
+ // Defensive: also treat the raw `ARRAY` literal as an array — if a
531
+ // SQL regression ever leaks the unformatted form through, the
532
+ // warning still fires instead of going silent. Codex P2 finding
533
+ // pinned this with a test below.
534
+ if (isArrayType(col.postgresType)) {
535
+ warnings.push(`Column "${table.schema}.${table.name}.${col.name}" is an array (${col.postgresType}) — PocketBase has no array UI; will map to json.`);
536
+ }
537
+ if (col.postgresType === "USER-DEFINED") {
538
+ warnings.push(`Column "${table.schema}.${table.name}.${col.name}" uses a user-defined type (likely enum or custom) — verify the PocketBase mapping manually.`);
539
+ }
540
+ }
541
+ if (table.rlsPolicies.length > 0) {
542
+ warnings.push(`Table "${table.schema}.${table.name}" has ${table.rlsPolicies.length} RLS polic${table.rlsPolicies.length === 1 ? "y" : "ies"} — PocketBase API rules use different syntax; phase D will report each policy for manual rewrite.`);
543
+ }
544
+ }
545
+ if (buckets.length > 0) {
546
+ warnings.push(`${buckets.length} storage bucket${buckets.length === 1 ? "" : "s"} found — bucket files need a separate storage migration script (phase C).`);
547
+ }
548
+ if (authUsersCount !== null && authUsersCount > 0) {
549
+ warnings.push(`${authUsersCount} auth user${authUsersCount === 1 ? "" : "s"} found — Supabase bcrypt hashes are NOT directly compatible with PocketBase; users will need to reset their password.`);
550
+ }
551
+ return warnings;
552
+ }
553
+ function renderSummary(args) {
554
+ const columnCount = args.tables.reduce((acc, t) => acc + t.columns.length, 0);
555
+ const policyCount = args.tables.reduce((acc, t) => acc + t.rlsPolicies.length, 0);
556
+ const parts = [
557
+ `Inspected Supabase project ${args.projectRef}:`,
558
+ `${args.tables.length} table${args.tables.length === 1 ? "" : "s"} (${columnCount} columns)`,
559
+ `${policyCount} RLS polic${policyCount === 1 ? "y" : "ies"}`,
560
+ `${args.storageBuckets.length} storage bucket${args.storageBuckets.length === 1 ? "" : "s"}`,
561
+ args.authUsersCount === null
562
+ ? "auth.users unavailable"
563
+ : `${args.authUsersCount} auth user${args.authUsersCount === 1 ? "" : "s"}`,
564
+ ];
565
+ const head = `${parts[0]} ${parts.slice(1).join(", ")}.`;
566
+ if (args.warnings.length === 0)
567
+ return head;
568
+ return `${head} ${args.warnings.length} warning${args.warnings.length === 1 ? "" : "s"} — see warnings[] for details.`;
569
+ }
570
+ /**
571
+ * Helper for downstream code (phases B–D) to filter tables to just the
572
+ * ones we'd migrate — i.e. nothing in a Supabase-managed system schema.
573
+ * Phase A returns these already filtered, but the predicate is exported
574
+ * so a caller can re-filter a result they've stored.
575
+ */
576
+ export function isMigratableTable(table) {
577
+ return !SYSTEM_SCHEMAS.has(table.schema);
578
+ }
579
+ //# sourceMappingURL=migrate-supabase.js.map