@percher/core 0.4.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 (245) 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 +1 -1
  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 +10 -35
  63. package/dist/commands/doctor.d.ts.map +1 -1
  64. package/dist/commands/doctor.js +12 -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.d.ts +40 -17
  146. package/dist/commands/publish.d.ts.map +1 -1
  147. package/dist/commands/publish.js +115 -8
  148. package/dist/commands/publish.js.map +1 -1
  149. package/dist/commands/push.d.ts +2 -12
  150. package/dist/commands/push.d.ts.map +1 -1
  151. package/dist/commands/push.js +2 -2
  152. package/dist/commands/push.js.map +1 -1
  153. package/dist/commands/redeploy.d.ts +2 -8
  154. package/dist/commands/redeploy.d.ts.map +1 -1
  155. package/dist/commands/redeploy.js +2 -2
  156. package/dist/commands/redeploy.js.map +1 -1
  157. package/dist/commands/rename.d.ts +2 -8
  158. package/dist/commands/rename.d.ts.map +1 -1
  159. package/dist/commands/rename.js +1 -1
  160. package/dist/commands/rename.js.map +1 -1
  161. package/dist/commands/reproduce.d.ts +2 -8
  162. package/dist/commands/reproduce.d.ts.map +1 -1
  163. package/dist/commands/reproduce.js +1 -1
  164. package/dist/commands/reproduce.js.map +1 -1
  165. package/dist/commands/reset-superuser.d.ts +2 -16
  166. package/dist/commands/reset-superuser.d.ts.map +1 -1
  167. package/dist/commands/reset-superuser.js +1 -1
  168. package/dist/commands/reset-superuser.js.map +1 -1
  169. package/dist/commands/restore.d.ts +7 -22
  170. package/dist/commands/restore.d.ts.map +1 -1
  171. package/dist/commands/restore.js +1 -1
  172. package/dist/commands/restore.js.map +1 -1
  173. package/dist/commands/resume.d.ts +2 -6
  174. package/dist/commands/resume.d.ts.map +1 -1
  175. package/dist/commands/resume.js +1 -1
  176. package/dist/commands/resume.js.map +1 -1
  177. package/dist/commands/rollback.d.ts +2 -8
  178. package/dist/commands/rollback.d.ts.map +1 -1
  179. package/dist/commands/rollback.js +1 -1
  180. package/dist/commands/rollback.js.map +1 -1
  181. package/dist/commands/sharing.d.ts +48 -0
  182. package/dist/commands/sharing.d.ts.map +1 -0
  183. package/dist/commands/sharing.js +85 -0
  184. package/dist/commands/sharing.js.map +1 -0
  185. package/dist/commands/status.d.ts +2 -6
  186. package/dist/commands/status.d.ts.map +1 -1
  187. package/dist/commands/status.js +1 -1
  188. package/dist/commands/status.js.map +1 -1
  189. package/dist/commands/transfers.d.ts +34 -0
  190. package/dist/commands/transfers.d.ts.map +1 -0
  191. package/dist/commands/transfers.js +62 -0
  192. package/dist/commands/transfers.js.map +1 -0
  193. package/dist/commands/unsuspend.d.ts +2 -6
  194. package/dist/commands/unsuspend.d.ts.map +1 -1
  195. package/dist/commands/unsuspend.js +1 -1
  196. package/dist/commands/unsuspend.js.map +1 -1
  197. package/dist/commands/versions.d.ts +2 -6
  198. package/dist/commands/versions.d.ts.map +1 -1
  199. package/dist/commands/versions.js +1 -1
  200. package/dist/commands/versions.js.map +1 -1
  201. package/dist/commands/wait-deploy.d.ts +2 -12
  202. package/dist/commands/wait-deploy.d.ts.map +1 -1
  203. package/dist/commands/wait-deploy.js +1 -1
  204. package/dist/commands/wait-deploy.js.map +1 -1
  205. package/dist/context.d.ts +15 -0
  206. package/dist/context.d.ts.map +1 -1
  207. package/dist/detect.d.ts +11 -0
  208. package/dist/detect.d.ts.map +1 -1
  209. package/dist/detect.js +31 -8
  210. package/dist/detect.js.map +1 -1
  211. package/dist/env-scan-source.js +1 -1
  212. package/dist/env-scan-source.js.map +1 -1
  213. package/dist/error-classifier.d.ts +17 -0
  214. package/dist/error-classifier.d.ts.map +1 -1
  215. package/dist/error-classifier.js +94 -8
  216. package/dist/error-classifier.js.map +1 -1
  217. package/dist/errors.d.ts +1 -1
  218. package/dist/errors.d.ts.map +1 -1
  219. package/dist/errors.js.map +1 -1
  220. package/dist/index.d.ts +63 -49
  221. package/dist/index.d.ts.map +1 -1
  222. package/dist/index.js +56 -42
  223. package/dist/index.js.map +1 -1
  224. package/dist/plans.d.ts +61 -5
  225. package/dist/plans.d.ts.map +1 -1
  226. package/dist/plans.js +78 -18
  227. package/dist/plans.js.map +1 -1
  228. package/dist/recovery.d.ts +60 -3
  229. package/dist/recovery.d.ts.map +1 -1
  230. package/dist/recovery.js +22 -0
  231. package/dist/recovery.js.map +1 -1
  232. package/dist/static-docker.d.ts +77 -0
  233. package/dist/static-docker.d.ts.map +1 -0
  234. package/dist/static-docker.js +105 -0
  235. package/dist/static-docker.js.map +1 -0
  236. package/dist/tarball.js +1 -1
  237. package/dist/tarball.js.map +1 -1
  238. package/dist/templates/ai-files/cursor-percher-mdc.d.ts.map +1 -1
  239. package/dist/templates/ai-files/cursor-percher-mdc.js +12 -9
  240. package/dist/templates/ai-files/cursor-percher-mdc.js.map +1 -1
  241. package/dist/templates.js +11 -11
  242. package/dist/templates.js.map +1 -1
  243. package/dist/watcher.js +1 -1
  244. package/dist/watcher.js.map +1 -1
  245. package/package.json +6 -2
@@ -0,0 +1,1119 @@
1
+ import jscodeshift, {} from "jscodeshift";
2
+ /** AST library pre-bound to the TypeScript parser via jscodeshift's `.withParser`. */
3
+ const tsx = jscodeshift.withParser("tsx");
4
+ /**
5
+ * Rewrite Supabase JS SDK calls into PocketBase JS SDK calls.
6
+ *
7
+ * Pure (no I/O). Idempotent — feeding the output back in produces
8
+ * the same output with an empty flags array (already-rewritten
9
+ * code has no Supabase calls left to match).
10
+ */
11
+ export function rewriteSupabaseSdk(input) {
12
+ const pbName = input.pbIdentifier ?? "pb";
13
+ const supabaseNames = new Set(input.supabaseIdentifiers ?? ["supabase"]);
14
+ // Fast path: if the source doesn't mention any of the Supabase
15
+ // identifiers OR `@supabase/supabase-js`, bail out without
16
+ // parsing. Saves a lot of AST work on the average file in a
17
+ // project (most files don't touch Supabase).
18
+ if (!sourceMentionsSupabase(input.source, supabaseNames)) {
19
+ return { rewritten: input.source, flags: [], changed: false };
20
+ }
21
+ let root;
22
+ try {
23
+ root = tsx(input.source);
24
+ }
25
+ catch (err) {
26
+ // Parser error — return unchanged WITH an explicit parseError
27
+ // signal so the file walker can classify this as
28
+ // skipped[reason=parse_error] instead of silently lumping it
29
+ // in with the clean files. Codex P2 round 10 (2026-05-17):
30
+ // before, the swallow-and-return-unchanged behaviour made
31
+ // malformed Supabase-bearing files invisible in the migration
32
+ // report — they looked just like files with no Supabase code.
33
+ return {
34
+ rewritten: input.source,
35
+ flags: [],
36
+ changed: false,
37
+ parseError: err instanceof Error ? err.message : String(err),
38
+ };
39
+ }
40
+ const flags = [];
41
+ const state = { mutated: false };
42
+ rewriteAuthCalls(root, supabaseNames, pbName, flags, state);
43
+ rewriteFromCalls(root, supabaseNames, pbName, flags, state);
44
+ flagUnsupportedPatterns(root, supabaseNames, flags);
45
+ // Only re-print via toSource when an AST mutation actually
46
+ // happened — manual_review flags can EITHER mutate the AST
47
+ // (e.g. auth.getUser still rewrites then warns about Promise
48
+ // semantics) OR leave it alone (e.g. { data, error } envelope
49
+ // detection skips the rewrite). Recast's round-trip can
50
+ // introduce tiny formatting drift on a clean AST, so we want
51
+ // byte-identical input == output when nothing was actually
52
+ // mutated. Codex P1.1 finding 2026-05-17.
53
+ if (!state.mutated) {
54
+ return { rewritten: input.source, flags, changed: false };
55
+ }
56
+ return {
57
+ rewritten: root.toSource({ quote: "double" }),
58
+ flags,
59
+ changed: true,
60
+ };
61
+ }
62
+ /**
63
+ * True when this CallExpression's result is consumed via Supabase's
64
+ * `{ data, error }` envelope shape — destructured at assignment OR
65
+ * accessed via `.data` / `.error`. PocketBase's SDK doesn't wrap
66
+ * results in an envelope (getFullList returns an array, create
67
+ * returns the record), so rewriting these call sites would silently
68
+ * break every consumer.
69
+ *
70
+ * Detection walks up from the CallExpression through the optional
71
+ * `await` parent to look at the consuming context. Covered shapes:
72
+ *
73
+ * const { data, error } = await CALL // destructure init
74
+ * const { data: x, error: y } = await CALL // renamed destructure
75
+ * ({ data, error } = await CALL) // assignment expr
76
+ * (await CALL).data // member access on await
77
+ * CALL.data / CALL.error // member access on call
78
+ *
79
+ * Anything else (assigned to a plain variable + later .data access,
80
+ * threaded through a helper, etc.) goes through the rewrite — the
81
+ * user's responsibility to fix.
82
+ *
83
+ * Codex P1.1 finding 2026-05-17.
84
+ */
85
+ function consumesDataErrorEnvelope(path) {
86
+ // The "outer expression" for envelope-purposes is the call OR a
87
+ // wrapping await. Look at the parent of THAT.
88
+ let outer = path.node;
89
+ let outerParent = path.parent?.node;
90
+ if (outerParent?.type === "AwaitExpression") {
91
+ const aw = outerParent;
92
+ if (aw.argument === path.node) {
93
+ outer = outerParent;
94
+ outerParent = path.parent.parent?.node;
95
+ }
96
+ }
97
+ if (!outerParent)
98
+ return false;
99
+ // (await CALL).data / CALL.data — member access on outer
100
+ if (outerParent.type === "MemberExpression") {
101
+ const m = outerParent;
102
+ if (m.object === outer && m.property.type === "Identifier") {
103
+ const propName = m.property.name;
104
+ if (propName === "data" || propName === "error")
105
+ return true;
106
+ }
107
+ return false;
108
+ }
109
+ // const { data, error } = await CALL
110
+ if (outerParent.type === "VariableDeclarator") {
111
+ const v = outerParent;
112
+ if (v.init === outer && v.id.type === "ObjectPattern") {
113
+ return objectPatternHasDataOrError(v.id);
114
+ }
115
+ return false;
116
+ }
117
+ // ({ data, error } = await CALL)
118
+ if (outerParent.type === "AssignmentExpression") {
119
+ const a = outerParent;
120
+ if (a.right === outer && a.left.type === "ObjectPattern") {
121
+ return objectPatternHasDataOrError(a.left);
122
+ }
123
+ return false;
124
+ }
125
+ return false;
126
+ }
127
+ function objectPatternHasDataOrError(pattern) {
128
+ for (const prop of pattern.properties) {
129
+ // jscodeshift / ast-types models destructure properties as
130
+ // either `Property` (legacy) or `ObjectProperty` (Babel) with
131
+ // a `key`. Defensive coverage of both.
132
+ if ((prop.type === "Property" || prop.type === "ObjectProperty") &&
133
+ "key" in prop &&
134
+ prop.key.type === "Identifier") {
135
+ const name = prop.key.name;
136
+ if (name === "data" || name === "error")
137
+ return true;
138
+ }
139
+ }
140
+ return false;
141
+ }
142
+ function flagEnvelopeManualReview(path, pattern, flags) {
143
+ flags.push(makeFlag(path, "manual_review", `${pattern}.envelope`, `Call site consumes the Supabase \`{ data, error }\` envelope — PocketBase returns plain values. Skipping rewrite; unwrap the destructure (or .data/.error access) manually, then re-run the migrator.`));
144
+ }
145
+ function sourceMentionsSupabase(source, supabaseNames) {
146
+ if (source.includes("@supabase/supabase-js"))
147
+ return true;
148
+ // Cheap textual scan before paying for the AST parse. False
149
+ // positives are fine — they just trigger a parse that finds
150
+ // nothing.
151
+ for (const name of supabaseNames) {
152
+ if (source.includes(name))
153
+ return true;
154
+ }
155
+ return false;
156
+ }
157
+ // ── auth.* call rewrites ────────────────────────────────────────
158
+ function rewriteAuthCalls(root, supabaseNames, pbName, flags, state) {
159
+ // supabase.auth.<method>(...)
160
+ root
161
+ .find(jscodeshift.CallExpression)
162
+ .filter((path) => isSupabaseAuthCall(path, supabaseNames))
163
+ .forEach((path) => {
164
+ const callee = path.node.callee;
165
+ const method = callee.property.name;
166
+ switch (method) {
167
+ case "signInWithPassword":
168
+ replaceAuthSignIn(path, pbName, flags, state);
169
+ return;
170
+ case "signUp":
171
+ replaceAuthSignUp(path, pbName, flags, state);
172
+ return;
173
+ case "signOut":
174
+ replaceAuthSignOut(path, pbName, flags, state);
175
+ return;
176
+ case "getUser":
177
+ replaceAuthGetUser(path, pbName, flags, state);
178
+ return;
179
+ case "onAuthStateChange":
180
+ replaceAuthOnChange(path, pbName, flags, state);
181
+ return;
182
+ }
183
+ });
184
+ }
185
+ function replaceAuthOnChange(path, pbName, flags, state) {
186
+ // pb.authStore.onChange(callback). The callback signature differs:
187
+ // Supabase: (event, session) => ...
188
+ // PB: (token, model) => ...
189
+ // The first arg passes through unchanged — same callable shape —
190
+ // but the destructured params inside the callback won't line up,
191
+ // so this is always manual_review.
192
+ if (path.node.arguments.length === 0) {
193
+ flags.push(makeFlag(path, "manual_review", "auth.onAuthStateChange", "onAuthStateChange called with no args — manual review."));
194
+ return;
195
+ }
196
+ const callback = path.node.arguments[0];
197
+ const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("authStore")), jscodeshift.identifier("onChange")), [callback]);
198
+ applyRewrite(path, replacement, "manual_review", "auth.onAuthStateChange", "onAuthStateChange → pb.authStore.onChange — the callback signature changes: Supabase passes `(event, session)`, PB passes `(token, model)`. Rewrite the callback body.", flags, state);
199
+ }
200
+ function isSupabaseAuthCall(path, supabaseNames) {
201
+ const callee = path.node.callee;
202
+ if (callee.type !== "MemberExpression")
203
+ return false;
204
+ if (callee.property.type !== "Identifier")
205
+ return false;
206
+ const object = callee.object;
207
+ // Expect <supabase>.auth.<method>
208
+ if (object.type !== "MemberExpression")
209
+ return false;
210
+ if (object.property.type !== "Identifier" || object.property.name !== "auth")
211
+ return false;
212
+ if (object.object.type !== "Identifier")
213
+ return false;
214
+ return supabaseNames.has(object.object.name);
215
+ }
216
+ function replaceAuthSignIn(path, pbName, flags, state) {
217
+ const arg = path.node.arguments[0];
218
+ if (!arg || arg.type !== "ObjectExpression") {
219
+ flags.push(makeFlag(path, "manual_review", "auth.signInWithPassword", "auth.signInWithPassword called with non-object argument — manual review needed."));
220
+ return;
221
+ }
222
+ // Locals are AST nodes, not secrets — but the repo's security
223
+ // audit pattern-matches `const password = ...` as a possible
224
+ // leaked-credential assignment regardless of RHS. Use the `-Expr`
225
+ // suffix so the auditor sees a node-handle, not a secret name.
226
+ // Codex P1.2 finding 2026-05-17.
227
+ const emailExpr = findObjectProp(arg, "email");
228
+ const passwordExpr = findObjectProp(arg, "password");
229
+ if (!emailExpr || !passwordExpr) {
230
+ flags.push(makeFlag(path, "manual_review", "auth.signInWithPassword", "auth.signInWithPassword object missing email or password — manual review needed."));
231
+ return;
232
+ }
233
+ if (consumesDataErrorEnvelope(path)) {
234
+ flagEnvelopeManualReview(path, "auth.signInWithPassword", flags);
235
+ return;
236
+ }
237
+ // pb.collection("users").authWithPassword(email, password)
238
+ const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("collection")), [jscodeshift.literal("users")]), jscodeshift.identifier("authWithPassword")), [emailExpr, passwordExpr]);
239
+ applyRewrite(path, replacement, "rewritten", "auth.signInWithPassword", 'signInWithPassword → pb.collection("users").authWithPassword', flags, state);
240
+ }
241
+ function replaceAuthSignUp(path, pbName, flags, state) {
242
+ const arg = path.node.arguments[0];
243
+ if (!arg || arg.type !== "ObjectExpression") {
244
+ flags.push(makeFlag(path, "manual_review", "auth.signUp", "auth.signUp called with non-object argument — manual review needed."));
245
+ return;
246
+ }
247
+ // Locals are AST nodes, not secrets — but the repo's security
248
+ // audit pattern-matches `const password = ...` as a possible
249
+ // leaked-credential assignment regardless of RHS. Use the `-Expr`
250
+ // suffix so the auditor sees a node-handle, not a secret name.
251
+ // Codex P1.2 finding 2026-05-17.
252
+ const emailExpr = findObjectProp(arg, "email");
253
+ const passwordExpr = findObjectProp(arg, "password");
254
+ if (!emailExpr || !passwordExpr) {
255
+ flags.push(makeFlag(path, "manual_review", "auth.signUp", "auth.signUp object missing email or password — manual review needed."));
256
+ return;
257
+ }
258
+ if (consumesDataErrorEnvelope(path)) {
259
+ flagEnvelopeManualReview(path, "auth.signUp", flags);
260
+ return;
261
+ }
262
+ // pb.collection("users").create({ email, password, passwordConfirm: password })
263
+ const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("collection")), [jscodeshift.literal("users")]), jscodeshift.identifier("create")), [
264
+ jscodeshift.objectExpression([
265
+ jscodeshift.property("init", jscodeshift.identifier("email"), emailExpr),
266
+ jscodeshift.property("init", jscodeshift.identifier("password"), passwordExpr),
267
+ jscodeshift.property("init", jscodeshift.identifier("passwordConfirm"), passwordExpr),
268
+ ]),
269
+ ]);
270
+ applyRewrite(path, replacement, "rewritten", "auth.signUp", 'signUp → pb.collection("users").create({...passwordConfirm})', flags, state);
271
+ }
272
+ function replaceAuthSignOut(path, pbName, flags, state) {
273
+ if (consumesDataErrorEnvelope(path)) {
274
+ flagEnvelopeManualReview(path, "auth.signOut", flags);
275
+ return;
276
+ }
277
+ // pb.authStore.clear()
278
+ const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("authStore")), jscodeshift.identifier("clear")), []);
279
+ applyRewrite(path, replacement, "rewritten", "auth.signOut", "signOut → pb.authStore.clear()", flags, state);
280
+ }
281
+ function replaceAuthGetUser(path, pbName, flags, state) {
282
+ if (consumesDataErrorEnvelope(path)) {
283
+ flagEnvelopeManualReview(path, "auth.getUser", flags);
284
+ return;
285
+ }
286
+ // pb.authStore.model (a member access, not a call). Note: PB's
287
+ // model is the current authed record OR null; Supabase's
288
+ // getUser() returns a Promise<{ data: { user } }> — semantics
289
+ // differ enough to warrant a manual-review flag, but the
290
+ // rewrite itself is mechanical.
291
+ const replacement = jscodeshift.memberExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("authStore")), jscodeshift.identifier("model"));
292
+ applyRewrite(path, replacement, "manual_review", "auth.getUser", "getUser → pb.authStore.model — note Supabase returned Promise<{ data: { user } }>, PB returns the record sync; remove awaits + .data.user access.", flags, state);
293
+ }
294
+ // ── .from() chain rewrites ──────────────────────────────────────
295
+ function rewriteFromCalls(root, supabaseNames, pbName, flags, state) {
296
+ // We target the OUTER call expression of each .from() chain
297
+ // because the rewrite depends on what comes after .from(). We
298
+ // walk every chain that starts at supabase.from("...").
299
+ root
300
+ .find(jscodeshift.CallExpression)
301
+ .filter((path) => isSupabaseFromTerminal(path, supabaseNames))
302
+ .forEach((path) => {
303
+ rewriteFromChain(path, supabaseNames, pbName, flags, state);
304
+ });
305
+ }
306
+ /**
307
+ * True when this CallExpression is the OUTERMOST call in a
308
+ * supabase.from(...).xxx().yyy() chain — i.e. its callee is a
309
+ * MemberExpression whose object eventually bottoms out at
310
+ * `supabase.from(...)`.
311
+ *
312
+ * We pick the outermost call so each chain produces exactly one
313
+ * rewrite. Inner .from(), .select() etc. are reached by walking
314
+ * down the callee chain inside the rewriter.
315
+ */
316
+ function isSupabaseFromTerminal(path, supabaseNames) {
317
+ // Walk down the callee chain until we find the call whose
318
+ // method is `from`. That's our chain root. Earlier version
319
+ // walked past it to the Identifier underneath and then
320
+ // rejected the chain — bug fixed 2026-05-17.
321
+ let fromCall = null;
322
+ let cursor = path.node;
323
+ while (cursor.type === "CallExpression") {
324
+ const call = cursor;
325
+ if (call.callee.type !== "MemberExpression")
326
+ break;
327
+ const callee = call.callee;
328
+ if (callee.property.type === "Identifier" &&
329
+ callee.property.name === "from") {
330
+ fromCall = call;
331
+ break;
332
+ }
333
+ cursor = callee.object;
334
+ }
335
+ if (!fromCall)
336
+ return false;
337
+ // The `from` call must be invoked on a Supabase identifier.
338
+ const fromCallee = fromCall.callee;
339
+ if (fromCallee.object.type !== "Identifier")
340
+ return false;
341
+ if (!supabaseNames.has(fromCallee.object.name))
342
+ return false;
343
+ // Ensure THIS call is the outermost in the chain — i.e. its
344
+ // parent isn't another member-call we own. The walker visits
345
+ // every CallExpression, so without this skip we'd process the
346
+ // same chain once per link.
347
+ const parent = path.parent?.node;
348
+ if (parent?.type === "MemberExpression") {
349
+ const parentMember = parent;
350
+ if (parentMember.object === path.node)
351
+ return false;
352
+ }
353
+ return true;
354
+ }
355
+ function rewriteFromChain(path, _supabaseNames, pbName, flags, state) {
356
+ // Decompose the chain into the methods called, in invocation
357
+ // order, with their args. The chain shape:
358
+ // supabase.from(T).method1(...args1).method2(...args2)
359
+ // becomes: { table: T, steps: [{method1, args1}, {method2, args2}] }
360
+ const decomposed = decomposeFromChain(path.node);
361
+ if (!decomposed) {
362
+ flags.push(makeFlag(path, "manual_review", "from.chain", "Couldn't decompose .from() chain shape — manual review."));
363
+ return;
364
+ }
365
+ const { table, steps } = decomposed;
366
+ if (steps.length === 0) {
367
+ // Bare supabase.from("t") with no follow-up — rare, but it's
368
+ // not a meaningful call by itself. Skip.
369
+ return;
370
+ }
371
+ const terminal = steps[0]; // .insert / .select / .update / .delete
372
+ switch (terminal.method) {
373
+ case "select":
374
+ rewriteSelect(path, pbName, table, steps, flags, state);
375
+ return;
376
+ case "insert":
377
+ rewriteInsert(path, pbName, table, terminal, flags, state);
378
+ return;
379
+ case "update":
380
+ rewriteUpdate(path, pbName, table, steps, flags, state);
381
+ return;
382
+ case "delete":
383
+ rewriteDelete(path, pbName, table, steps, flags, state);
384
+ return;
385
+ case "upsert":
386
+ // PB has no upsert primitive; the user must split into
387
+ // getOne (catch 404) + update-or-create. Flag once at the
388
+ // from-chain layer instead of letting the flag-only walker
389
+ // also pick it up — double-flagging the same call site
390
+ // would clutter the report.
391
+ flags.push(makeFlag(path, "unsupported", "from.upsert", `from(${JSON.stringify(table)}).upsert(...) — PB has no upsert primitive. Rewrite as getOne (catch 404) → update-or-create.`));
392
+ return;
393
+ default:
394
+ flags.push(makeFlag(path, "manual_review", `from.${terminal.method}`, `from(${JSON.stringify(table)}).${terminal.method}(...) has no chunk-1 rewrite — manual review.`));
395
+ }
396
+ }
397
+ function decomposeFromChain(outer) {
398
+ // Walk from outer down. At each level, record (method, args) and
399
+ // recurse into the callee's object. Stop when we reach the
400
+ // .from(T) call. Same union-narrowing pattern as
401
+ // isSupabaseFromTerminal — explicit casts inside the loop.
402
+ const reversedSteps = [];
403
+ let node = outer;
404
+ while (node.type === "CallExpression") {
405
+ const call = node;
406
+ if (call.callee.type !== "MemberExpression")
407
+ break;
408
+ const callee = call.callee;
409
+ const method = callee.property.type === "Identifier"
410
+ ? callee.property.name
411
+ : null;
412
+ if (method === null)
413
+ return null;
414
+ if (method === "from") {
415
+ const arg = call.arguments[0];
416
+ if (!arg || (arg.type !== "Literal" && arg.type !== "StringLiteral"))
417
+ return null;
418
+ const table = arg.value;
419
+ if (typeof table !== "string")
420
+ return null;
421
+ return { table, steps: reversedSteps.reverse() };
422
+ }
423
+ reversedSteps.push({ method, args: call.arguments });
424
+ node = callee.object;
425
+ }
426
+ return null;
427
+ }
428
+ function rewriteSelect(path, pbName, table, steps, flags, state) {
429
+ if (consumesDataErrorEnvelope(path)) {
430
+ flagEnvelopeManualReview(path, "from.select", flags);
431
+ return;
432
+ }
433
+ const select = steps[0];
434
+ const tail = steps.slice(1);
435
+ // Walk the chain after .select() and accumulate option fields.
436
+ // Anything we don't recognise lands the entire chain in
437
+ // manual_review without mutation. The tradeoff: cleaner rewrites
438
+ // for the common cases, no risky guess for the edge cases.
439
+ const opts = {};
440
+ let needsReview = false;
441
+ const reviewNotes = [];
442
+ let usePagination = false; // true when .range() / .limit() forces getList
443
+ let paginationPage = 1;
444
+ let paginationPerPage = 0;
445
+ // .select() fields (PB calls this `fields`)
446
+ const fieldsValue = extractSelectFields(select);
447
+ if (fieldsValue !== null)
448
+ opts.fields = jscodeshift.literal(fieldsValue);
449
+ for (const step of tail) {
450
+ switch (step.method) {
451
+ case "eq": {
452
+ const result = buildEqFilter(step);
453
+ if (!result) {
454
+ needsReview = true;
455
+ reviewNotes.push(`.eq() arguments not in the (column-literal, value) shape — manual review.`);
456
+ continue;
457
+ }
458
+ // Multiple .eq() calls concatenate via PB's `&&` combinator.
459
+ opts.filter = result.identifierValue
460
+ ? combineFilterDynamic(opts.filter, result.expression)
461
+ : combineFilterStatic(opts.filter, result.expression);
462
+ if (result.identifierValue) {
463
+ // Escaping a runtime string into a PB filter is the user's
464
+ // job — the plan calls this out explicitly in the mapping
465
+ // table. Annotate AND downgrade to manual_review so the
466
+ // file walker surfaces it; the rewrite proceeds because
467
+ // the template-literal shape is correct, only the
468
+ // escape-safety is unverifiable at codemod time.
469
+ needsReview = true;
470
+ reviewNotes.push(`.eq(${JSON.stringify(result.column)}, <identifier>) — verify the value is escape-safe for PB filter strings (no double-quotes / backslashes).`);
471
+ }
472
+ break;
473
+ }
474
+ case "order": {
475
+ const sortFrag = buildSortFragment(step);
476
+ if (!sortFrag) {
477
+ needsReview = true;
478
+ reviewNotes.push(`.order() arguments not in the recognised shape — manual review.`);
479
+ continue;
480
+ }
481
+ opts.sort = jscodeshift.literal(opts.sort ? `${literalString(opts.sort)},${sortFrag}` : sortFrag);
482
+ break;
483
+ }
484
+ case "limit": {
485
+ const arg = step.args[0];
486
+ const n = extractNumber(arg);
487
+ if (n === null) {
488
+ needsReview = true;
489
+ reviewNotes.push(`.limit() argument isn't a literal number — manual review.`);
490
+ continue;
491
+ }
492
+ // perPage <= 0 isn't a valid PB pagination value (it'd
493
+ // mean "zero rows per page" which translates to "always
494
+ // empty" — likely not what the user meant). Refuse to
495
+ // mutate; flag for review. Codex P2 round 7.
496
+ if (n <= 0) {
497
+ flags.push(makeFlag(path, "manual_review", "from.select.limit_invalid", `from(${JSON.stringify(table)}).select(...).limit(${n}) — non-positive limit doesn't translate to PB pagination. Manual review.`));
498
+ return;
499
+ }
500
+ usePagination = true;
501
+ paginationPerPage = Math.max(paginationPerPage, n);
502
+ break;
503
+ }
504
+ case "range": {
505
+ // Supabase .range(start, end) is INCLUSIVE on both ends. PB
506
+ // .getList(page, perPage) is page-based. The translation
507
+ // (start=0, end=N-1) → page=1, perPage=N works when start
508
+ // is 0; non-zero start gets best-effort + manual_review.
509
+ const startArg = step.args[0];
510
+ const endArg = step.args[1];
511
+ const startN = extractNumber(startArg);
512
+ const endN = extractNumber(endArg);
513
+ if (startN === null || endN === null) {
514
+ needsReview = true;
515
+ reviewNotes.push(`.range() arguments aren't literal numbers — manual review.`);
516
+ continue;
517
+ }
518
+ const perPage = endN - startN + 1;
519
+ // Empty / negative range: refuse mutation. Same reasoning
520
+ // as .limit(0). Codex P2 round 7.
521
+ if (perPage <= 0) {
522
+ flags.push(makeFlag(path, "manual_review", "from.select.range_invalid", `from(${JSON.stringify(table)}).select(...).range(${startN}, ${endN}) — end < start; PB pagination has no equivalent. Manual review.`));
523
+ return;
524
+ }
525
+ usePagination = true;
526
+ paginationPerPage = Math.max(paginationPerPage, perPage);
527
+ if (startN === 0) {
528
+ paginationPage = 1;
529
+ }
530
+ else if (startN % perPage === 0) {
531
+ paginationPage = Math.floor(startN / perPage) + 1;
532
+ }
533
+ else {
534
+ needsReview = true;
535
+ reviewNotes.push(`.range(${startN}, ${endN}) — start isn't on a page boundary; PB's page math doesn't translate cleanly. Manual review.`);
536
+ }
537
+ break;
538
+ }
539
+ case "single": {
540
+ // .single() flips the consumer's return-type contract: the
541
+ // user's code reads `x.id`, but getFullList returns an
542
+ // array. Even with a review flag, mutating the call site
543
+ // would produce behaviorally-wrong code that compiles
544
+ // cleanly. Refuse the entire chain rewrite instead.
545
+ // Codex P1 round 7 (2026-05-17).
546
+ flags.push(makeFlag(path, "manual_review", "from.select.single", `from(${JSON.stringify(table)}).select(...).single() — PB's getFullList returns an array; getFirstListItem(filter) is the single-record equivalent but takes a filter string. Rewrite this chain by hand: pick getFirstListItem with the right filter OR [0]-index the array result.`));
547
+ return;
548
+ }
549
+ default: {
550
+ // Unknown step — fall out of the chunk-2 happy path. The
551
+ // entire chain stays as manual_review without mutation so
552
+ // we don't half-translate something subtle.
553
+ flags.push(makeFlag(path, "manual_review", `from.select.${step.method}`, `from(${JSON.stringify(table)}).select(...).${step.method}(...) has no chunk-2 rewrite — manual review.`));
554
+ return;
555
+ }
556
+ }
557
+ }
558
+ // Build the call. Without pagination → getFullList(opts).
559
+ // With pagination → getList(page, perPage, opts-minus-pagination-bits).
560
+ let methodName;
561
+ let methodArgs;
562
+ if (usePagination) {
563
+ methodName = "getList";
564
+ const trailingOpts = buildOptionsObject(opts);
565
+ methodArgs = [jscodeshift.literal(paginationPage), jscodeshift.literal(paginationPerPage)];
566
+ if (trailingOpts)
567
+ methodArgs.push(trailingOpts);
568
+ }
569
+ else {
570
+ methodName = "getFullList";
571
+ const optsObj = buildOptionsObject(opts);
572
+ methodArgs = optsObj ? [optsObj] : [];
573
+ }
574
+ const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("collection")), [jscodeshift.literal(table)]), jscodeshift.identifier(methodName)), methodArgs);
575
+ const kind = needsReview ? "manual_review" : "rewritten";
576
+ const baseNote = `from(${JSON.stringify(table)}).select(...) → pb.collection(...).${methodName}(...)`;
577
+ const note = needsReview ? `${baseNote} — ${reviewNotes.join(" · ")}` : baseNote;
578
+ applyRewrite(path, replacement, kind, "from.select", note, flags, state);
579
+ }
580
+ /** Extract the comma-separated columns from a `.select("a,b")` call.
581
+ * Returns null when the args don't match the literal-string shape
582
+ * (caller falls back to no `fields` option). */
583
+ function extractSelectFields(select) {
584
+ if (select.args.length === 0)
585
+ return null;
586
+ const first = select.args[0];
587
+ if ((first.type === "Literal" || first.type === "StringLiteral") &&
588
+ typeof first.value === "string") {
589
+ const val = first.value;
590
+ if (val === "*" || val.length === 0)
591
+ return null;
592
+ return val;
593
+ }
594
+ return null;
595
+ }
596
+ function buildEqFilter(step) {
597
+ if (step.args.length < 2)
598
+ return null;
599
+ const colArg = step.args[0];
600
+ const valArg = step.args[1];
601
+ if (!(colArg.type === "Literal" || colArg.type === "StringLiteral") ||
602
+ typeof colArg.value !== "string") {
603
+ return null;
604
+ }
605
+ const column = colArg.value;
606
+ // Literal value → static filter string. `col = "value"` for
607
+ // strings, `col = 42` for numbers. jscodeshift's tsx parser
608
+ // emits NumericLiteral / BooleanLiteral as distinct types from
609
+ // the generic Literal — accept all four.
610
+ if (valArg.type === "Literal" ||
611
+ valArg.type === "StringLiteral" ||
612
+ valArg.type === "NumericLiteral" ||
613
+ valArg.type === "BooleanLiteral") {
614
+ const v = valArg.value;
615
+ if (typeof v === "string") {
616
+ return {
617
+ expression: jscodeshift.literal(`${column} = "${escapeFilterString(v)}"`),
618
+ column,
619
+ identifierValue: false,
620
+ };
621
+ }
622
+ if (typeof v === "number" || typeof v === "boolean") {
623
+ return {
624
+ expression: jscodeshift.literal(`${column} = ${v}`),
625
+ column,
626
+ identifierValue: false,
627
+ };
628
+ }
629
+ return null;
630
+ }
631
+ // Non-literal (identifier, member access, etc.) → template literal
632
+ // so the value is interpolated at runtime. Escaping is the user's
633
+ // responsibility — flagged via reviewNotes upstream.
634
+ const template = jscodeshift.templateLiteral([
635
+ jscodeshift.templateElement({ raw: `${column} = "`, cooked: `${column} = "` }, false),
636
+ jscodeshift.templateElement({ raw: `"`, cooked: `"` }, true),
637
+ ], [valArg]);
638
+ return {
639
+ expression: template,
640
+ column,
641
+ identifierValue: true,
642
+ };
643
+ }
644
+ function escapeFilterString(s) {
645
+ // PB filter strings interpret BOTH `\` and `"` inside double-
646
+ // quoted values. Escape backslashes FIRST (so a literal
647
+ // backslash doesn't get re-escaped by the quote step), then
648
+ // double-quotes. Without the backslash step, a column value
649
+ // like `a\b` would emit `filter: "col = \"a\b\""` which the PB
650
+ // parser reads as `a` + backspace + `b`. Codex P2 round 7
651
+ // (2026-05-17) — the dynamic-value note already warned about
652
+ // this; the static path needed the same treatment.
653
+ return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
654
+ }
655
+ /**
656
+ * Combine two filter expressions into `(a) && (b)`. Both `a` and
657
+ * `b` may be string literals OR template literals.
658
+ */
659
+ function combineFilterStatic(current, next) {
660
+ if (!current)
661
+ return next;
662
+ const a = literalString(current);
663
+ const b = literalString(next);
664
+ if (a !== null && b !== null) {
665
+ return jscodeshift.literal(`(${a}) && (${b})`);
666
+ }
667
+ // One side is dynamic — fall through to dynamic combiner.
668
+ return combineFilterDynamic(current, next);
669
+ }
670
+ function combineFilterDynamic(current, next) {
671
+ if (!current)
672
+ return next;
673
+ // Build a template literal that wraps both sides in parens with `&&`.
674
+ return jscodeshift.templateLiteral([
675
+ jscodeshift.templateElement({ raw: "(", cooked: "(" }, false),
676
+ jscodeshift.templateElement({ raw: ") && (", cooked: ") && (" }, false),
677
+ jscodeshift.templateElement({ raw: ")", cooked: ")" }, true),
678
+ ], [current, next]);
679
+ }
680
+ function literalString(node) {
681
+ if (node?.type === "Literal" || node?.type === "StringLiteral") {
682
+ const v = node.value;
683
+ if (typeof v === "string")
684
+ return v;
685
+ }
686
+ return null;
687
+ }
688
+ function buildSortFragment(step) {
689
+ if (step.args.length < 1)
690
+ return null;
691
+ const colArg = step.args[0];
692
+ if (!(colArg.type === "Literal" || colArg.type === "StringLiteral") ||
693
+ typeof colArg.value !== "string") {
694
+ return null;
695
+ }
696
+ const column = colArg.value;
697
+ let ascending = true;
698
+ if (step.args.length >= 2) {
699
+ const opts = step.args[1];
700
+ if (opts.type === "ObjectExpression") {
701
+ const ascProp = findObjectProp(opts, "ascending");
702
+ if (ascProp?.type === "Literal" || ascProp?.type === "BooleanLiteral") {
703
+ ascending = ascProp.value === true;
704
+ }
705
+ else if (ascProp) {
706
+ // Non-literal ascending — can't statically decide. Return null
707
+ // so the caller routes to manual_review.
708
+ return null;
709
+ }
710
+ }
711
+ }
712
+ return ascending ? column : `-${column}`;
713
+ }
714
+ function extractNumber(node) {
715
+ if (!node)
716
+ return null;
717
+ if (node.type === "Literal" || node.type === "NumericLiteral") {
718
+ const v = node.value;
719
+ if (typeof v === "number")
720
+ return v;
721
+ }
722
+ if (node.type === "UnaryExpression") {
723
+ const u = node;
724
+ if (u.operator === "-") {
725
+ const inner = extractNumber(u.argument);
726
+ if (inner !== null)
727
+ return -inner;
728
+ }
729
+ }
730
+ return null;
731
+ }
732
+ function buildOptionsObject(opts) {
733
+ const props = [];
734
+ if (opts.fields !== undefined) {
735
+ props.push(jscodeshift.property("init", jscodeshift.identifier("fields"), opts.fields));
736
+ }
737
+ if (opts.filter !== undefined) {
738
+ props.push(jscodeshift.property("init", jscodeshift.identifier("filter"), opts.filter));
739
+ }
740
+ if (opts.sort !== undefined) {
741
+ props.push(jscodeshift.property("init", jscodeshift.identifier("sort"), opts.sort));
742
+ }
743
+ if (props.length === 0)
744
+ return null;
745
+ return jscodeshift.objectExpression(props);
746
+ }
747
+ function rewriteInsert(path, pbName, table, insert, flags, state) {
748
+ if (consumesDataErrorEnvelope(path)) {
749
+ flagEnvelopeManualReview(path, "from.insert", flags);
750
+ return;
751
+ }
752
+ // pb.collection(T).create(arg). Supabase .insert accepts an
753
+ // object OR an array; PB's .create takes a single object. If the
754
+ // arg is an array we flag for manual review (the user wants a
755
+ // loop or batch helper).
756
+ if (insert.args.length === 0) {
757
+ flags.push(makeFlag(path, "manual_review", "from.insert", "from(...).insert() called with no args — manual review."));
758
+ return;
759
+ }
760
+ const arg = insert.args[0];
761
+ if (arg.type === "ArrayExpression") {
762
+ flags.push(makeFlag(path, "manual_review", "from.insert.array", `from(${JSON.stringify(table)}).insert([...]) — PB's create takes one record at a time; wrap in a loop.`));
763
+ return;
764
+ }
765
+ const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("collection")), [jscodeshift.literal(table)]), jscodeshift.identifier("create")), [arg]);
766
+ applyRewrite(path, replacement, "rewritten", "from.insert", `from(${JSON.stringify(table)}).insert({...}) → pb.collection(...).create({...})`, flags, state);
767
+ }
768
+ function rewriteUpdate(path, pbName, table, steps, flags, state) {
769
+ if (consumesDataErrorEnvelope(path)) {
770
+ flagEnvelopeManualReview(path, "from.update", flags);
771
+ return;
772
+ }
773
+ // .update({...}).eq("id", value) → pb.collection(T).update(value, {...})
774
+ // Anything else (.eq on a non-id column, multiple .eq, missing
775
+ // .eq) lands in manual_review.
776
+ const update = steps[0];
777
+ const eq = steps[1];
778
+ if (steps.length !== 2 || !eq || eq.method !== "eq") {
779
+ flags.push(makeFlag(path, "manual_review", "from.update.shape", `from(${JSON.stringify(table)}).update(...) chain isn't the .update({...}).eq("id", v) shape — manual review.`));
780
+ return;
781
+ }
782
+ if (eq.args.length < 2) {
783
+ flags.push(makeFlag(path, "manual_review", "from.update.eq", "update().eq(...) call missing args — manual review."));
784
+ return;
785
+ }
786
+ const eqCol = eq.args[0];
787
+ const eqVal = eq.args[1];
788
+ if (!((eqCol.type === "Literal" || eqCol.type === "StringLiteral") &&
789
+ eqCol.value === "id")) {
790
+ flags.push(makeFlag(path, "manual_review", "from.update.eq_non_id", `from(${JSON.stringify(table)}).update().eq() filters on a non-id column — PB's update needs an id; chunk 2 will handle filter-based updates.`));
791
+ return;
792
+ }
793
+ if (update.args.length === 0) {
794
+ flags.push(makeFlag(path, "manual_review", "from.update", "update() called with no args — manual review."));
795
+ return;
796
+ }
797
+ const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("collection")), [jscodeshift.literal(table)]), jscodeshift.identifier("update")), [eqVal, update.args[0]]);
798
+ applyRewrite(path, replacement, "rewritten", "from.update", `from(${JSON.stringify(table)}).update({...}).eq("id", v) → pb.collection(...).update(v, {...})`, flags, state);
799
+ }
800
+ function rewriteDelete(path, pbName, table, steps, flags, state) {
801
+ if (consumesDataErrorEnvelope(path)) {
802
+ flagEnvelopeManualReview(path, "from.delete", flags);
803
+ return;
804
+ }
805
+ // .delete().eq("id", value) → pb.collection(T).delete(value)
806
+ const eq = steps[1];
807
+ if (steps.length !== 2 || !eq || eq.method !== "eq") {
808
+ flags.push(makeFlag(path, "manual_review", "from.delete.shape", `from(${JSON.stringify(table)}).delete() chain isn't the .delete().eq("id", v) shape — manual review.`));
809
+ return;
810
+ }
811
+ if (eq.args.length < 2) {
812
+ flags.push(makeFlag(path, "manual_review", "from.delete.eq", "delete().eq(...) call missing args — manual review."));
813
+ return;
814
+ }
815
+ const eqCol = eq.args[0];
816
+ const eqVal = eq.args[1];
817
+ if (!((eqCol.type === "Literal" || eqCol.type === "StringLiteral") &&
818
+ eqCol.value === "id")) {
819
+ flags.push(makeFlag(path, "manual_review", "from.delete.eq_non_id", `from(${JSON.stringify(table)}).delete().eq() filters on a non-id column — PB's delete needs an id; chunk 2 will handle filter-based deletes.`));
820
+ return;
821
+ }
822
+ const replacement = jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.callExpression(jscodeshift.memberExpression(jscodeshift.identifier(pbName), jscodeshift.identifier("collection")), [jscodeshift.literal(table)]), jscodeshift.identifier("delete")), [eqVal]);
823
+ applyRewrite(path, replacement, "rewritten", "from.delete", `from(${JSON.stringify(table)}).delete().eq("id", v) → pb.collection(...).delete(v)`, flags, state);
824
+ }
825
+ // ── Chunk 3.1: flag-only patterns (no mutation) ────────────────
826
+ //
827
+ // These Supabase APIs have no clean PocketBase equivalent: the
828
+ // rewriter records a flag with line/column so the file walker
829
+ // can surface them in the migration report, but does NOT touch
830
+ // the source. The user reads the report and rewrites by hand.
831
+ //
832
+ // Patterns covered:
833
+ // - supabase.storage.* (file upload/download model differs)
834
+ // - supabase.channel(...).on(...).subscribe (realtime API shape differs)
835
+ // - supabase.rpc("fn", args) (Postgres functions — no PB equivalent)
836
+ // - supabase.functions.invoke(...) (Edge Functions — host elsewhere)
837
+ // - supabase.from(...).upsert(...) (PB has no upsert; flag for split)
838
+ function flagUnsupportedPatterns(root, supabaseNames, flags) {
839
+ // Walk every CallExpression once, identify the pattern by
840
+ // looking at the callee chain. Filter by Supabase identifier
841
+ // root so we don't false-positive on unrelated `.storage` /
842
+ // `.rpc` / `.channel` calls in user code.
843
+ root.find(jscodeshift.CallExpression).forEach((path) => {
844
+ flagOneUnsupported(path, supabaseNames, flags);
845
+ });
846
+ }
847
+ function flagOneUnsupported(path, supabaseNames, flags) {
848
+ const root = findChainRoot(path.node);
849
+ if (!root)
850
+ return;
851
+ if (root.type !== "Identifier")
852
+ return;
853
+ if (!supabaseNames.has(root.name))
854
+ return;
855
+ // We only want to flag ONCE per chain — at the outermost call.
856
+ // If our parent is a MemberExpression whose .object is this
857
+ // node, we're not the outermost.
858
+ const parent = path.parent?.node;
859
+ if (parent?.type === "MemberExpression") {
860
+ const m = parent;
861
+ if (m.object === path.node)
862
+ return;
863
+ }
864
+ // Decompose the chain into ordered (method, call) pairs from
865
+ // ROOT (closest to `supabase`) to LEAF (outermost call). We use
866
+ // each call's OWN arguments for the report — not the outermost
867
+ // wrapper's. Codex P2 round 8 (2026-05-17): the previous version
868
+ // pulled args from `path.node` (the outermost wrapper), so
869
+ // `await supabase.rpc("compute_score", args).select("x")`
870
+ // misreported the function name as "x", and trailing
871
+ // `.then()` / `.catch()` made `storage.upload` report as
872
+ // `storage.then` etc.
873
+ const steps = readChainSteps(path.node);
874
+ if (steps.length === 0)
875
+ return;
876
+ const first = steps[0];
877
+ if (first.method === "storage") {
878
+ // supabase.storage.from("b").upload(...).<then|catch|...>
879
+ // `findStorageOperation` returns the canonical operation OR
880
+ // a sensible non-continuation fallback. Only when the chain
881
+ // is genuinely `supabase.storage.from(bucket)` with nothing
882
+ // after does the report name `from` — and that's accurate.
883
+ const opStep = findStorageOperation(steps);
884
+ const opName = opStep?.method ?? "from";
885
+ flags.push(makeFlag(path, "unsupported", `storage.${opName}`, `supabase.storage.<bucket>.${opName}(...) — PocketBase models files as record-attached fields, not standalone buckets. Rewrite by hand: create a collection with a \`file\` field, then use pb.collection(c).create({ file: blob }) for uploads and pb.files.getURL(record, record.file) for URLs.`));
886
+ return;
887
+ }
888
+ if (first.method === "channel") {
889
+ // supabase.channel(...).on(...).subscribe() — the `.subscribe`
890
+ // call is the actionable terminal even if the user chains
891
+ // `.then(...)` after it.
892
+ flags.push(makeFlag(path, "unsupported", "channel.subscribe", `supabase.channel(...).on(...).subscribe(...) — PB has realtime via pb.collection(c).subscribe("*", cb) but the event shape and filter model differ from Supabase channels. Rewrite by hand.`));
893
+ return;
894
+ }
895
+ if (first.method === "rpc") {
896
+ // supabase.rpc("fn_name", args). The function name + args are
897
+ // on the `rpc` step itself — NOT on a downstream `.select()`
898
+ // or `.then()`. Use that step's call.
899
+ const fnName = first.call ? (readFirstStringArg(first.call) ?? "<unknown>") : "<unknown>";
900
+ flags.push(makeFlag(path, "unsupported", "rpc", `supabase.rpc(${JSON.stringify(fnName)}, ...) — Postgres function calls don't have a PocketBase equivalent. Move the logic into your app server code OR a PB hook (.pb_hooks/).`));
901
+ return;
902
+ }
903
+ if (first.method === "functions" && steps[1]?.method === "invoke") {
904
+ const invokeStep = steps[1];
905
+ const fnName = invokeStep.call
906
+ ? (readFirstStringArg(invokeStep.call) ?? "<unknown>")
907
+ : "<unknown>";
908
+ flags.push(makeFlag(path, "unsupported", "functions.invoke", `supabase.functions.invoke(${JSON.stringify(fnName)}, ...) — Edge Functions don't run on Percher. Move the function code into the Percher app server, or host it on a separate platform (Deno Deploy, Cloudflare Workers) and call it via fetch.`));
909
+ return;
910
+ }
911
+ // `from.upsert` is handled inside rewriteFromChain (the
912
+ // from-chain rewriter already walks .from() chains and is the
913
+ // canonical place for terminal-method classification). Flagging
914
+ // it here too would double up the report.
915
+ }
916
+ /**
917
+ * Storage operations Percher knows the migration intent for. The
918
+ * walker picks the first matching step in the chain so a trailing
919
+ * `.then()` / `.catch()` doesn't end up as the reported pattern.
920
+ *
921
+ * Not exhaustive — keeping the set narrow has the upside of clean
922
+ * names in flags, but the downside that any newly-added Supabase
923
+ * storage method we forget to add here would fall through. The
924
+ * fallback `findFallbackStorageMethod` covers that case by walking
925
+ * the chain end-to-start and picking the last non-continuation,
926
+ * non-structural method, so a forgotten entry still produces a
927
+ * useful flag (just without the canonical name).
928
+ *
929
+ * Codex P2 round 9 (2026-05-17): added `createSignedUrls` (plural)
930
+ * which was missed — it's a legit storage API.
931
+ */
932
+ const STORAGE_OPERATIONS = new Set([
933
+ "upload",
934
+ "uploadToSignedUrl",
935
+ "download",
936
+ "list",
937
+ "remove",
938
+ "move",
939
+ "copy",
940
+ "createSignedUrl",
941
+ "createSignedUrls",
942
+ "createSignedUploadUrl",
943
+ "getPublicUrl",
944
+ "update",
945
+ "exists",
946
+ "info",
947
+ ]);
948
+ /**
949
+ * Promise / standard-JS continuations the walker skips when
950
+ * looking for the "actual" storage operation in a chain. A
951
+ * trailing `.then(...)` or `.catch(...)` after the storage call
952
+ * shouldn't be reported as the operation.
953
+ */
954
+ const CHAIN_CONTINUATIONS = new Set(["then", "catch", "finally"]);
955
+ function findStorageOperation(steps) {
956
+ // First pass: known storage operations. Stable canonical names
957
+ // are preferred when the user uses a known method.
958
+ for (const step of steps) {
959
+ if (STORAGE_OPERATIONS.has(step.method))
960
+ return step;
961
+ }
962
+ // Fallback: any method that's not a Promise continuation and
963
+ // not the structural `.from(bucket)` / `.storage`. Walk
964
+ // END-TO-START so trailing `.then()` doesn't shadow the real
965
+ // operation. Codex P2 round 9 fix: previously fell back to
966
+ // `steps[1]` which is always `.from`, misreporting valid
967
+ // storage methods we forgot to add to the allowlist as
968
+ // `storage.from`.
969
+ for (let i = steps.length - 1; i >= 0; i--) {
970
+ const step = steps[i];
971
+ if (step.method === "storage" || step.method === "from")
972
+ continue;
973
+ if (CHAIN_CONTINUATIONS.has(step.method))
974
+ continue;
975
+ return step;
976
+ }
977
+ return null;
978
+ }
979
+ /**
980
+ * Walk down callee.object until we hit a non-MemberExpression /
981
+ * non-CallExpression node. Returns the root expression (typically
982
+ * an Identifier like `supabase` or `sb`).
983
+ */
984
+ function findChainRoot(node) {
985
+ let cursor = node;
986
+ while (true) {
987
+ if (cursor.type === "CallExpression") {
988
+ const c = cursor;
989
+ if (c.callee.type !== "MemberExpression")
990
+ return null;
991
+ cursor = c.callee.object;
992
+ continue;
993
+ }
994
+ if (cursor.type === "MemberExpression") {
995
+ cursor = cursor.object;
996
+ continue;
997
+ }
998
+ return cursor;
999
+ }
1000
+ }
1001
+ function readChainSteps(outer) {
1002
+ const reversed = [];
1003
+ let cursor = outer;
1004
+ while (true) {
1005
+ if (cursor.type === "CallExpression") {
1006
+ const call = cursor;
1007
+ if (call.callee.type !== "MemberExpression")
1008
+ break;
1009
+ const callee = call.callee;
1010
+ if (callee.property.type === "Identifier") {
1011
+ reversed.push({
1012
+ method: callee.property.name,
1013
+ call,
1014
+ });
1015
+ }
1016
+ cursor = callee.object;
1017
+ continue;
1018
+ }
1019
+ if (cursor.type === "MemberExpression") {
1020
+ const m = cursor;
1021
+ if (m.property.type === "Identifier") {
1022
+ reversed.push({
1023
+ method: m.property.name,
1024
+ call: null,
1025
+ });
1026
+ }
1027
+ cursor = m.object;
1028
+ continue;
1029
+ }
1030
+ break;
1031
+ }
1032
+ return reversed.reverse();
1033
+ }
1034
+ function readFirstStringArg(call) {
1035
+ const a = call.arguments[0];
1036
+ if (!a)
1037
+ return null;
1038
+ if (a.type === "Literal" || a.type === "StringLiteral") {
1039
+ const v = a.value;
1040
+ if (typeof v === "string")
1041
+ return v;
1042
+ }
1043
+ return null;
1044
+ }
1045
+ function findObjectProp(obj, name) {
1046
+ for (const prop of obj.properties) {
1047
+ if ((prop.type === "Property" || prop.type === "ObjectProperty") &&
1048
+ "key" in prop &&
1049
+ prop.key.type === "Identifier" &&
1050
+ prop.key.name === name) {
1051
+ // Shorthand `{ email }`: value === key Identifier with the
1052
+ // same name. Synthesize a fresh Identifier so the rewriter
1053
+ // can wire it into the new call.
1054
+ if (prop.shorthand === true) {
1055
+ return jscodeshift.identifier(name);
1056
+ }
1057
+ if ("value" in prop) {
1058
+ const val = prop.value;
1059
+ if (val.type !== "SpreadElement" && val.type !== "RestElement") {
1060
+ return val;
1061
+ }
1062
+ }
1063
+ }
1064
+ }
1065
+ return null;
1066
+ }
1067
+ function makeFlag(pathOrLoc, kind, pattern, note) {
1068
+ // Accept either an ASTPath (which we pre-read OR read before
1069
+ // replace) or a pre-captured loc object. Tests pin that flags
1070
+ // carry line/column even after the source node was swapped out
1071
+ // by path.replace — capture loc EAGERLY at each call site.
1072
+ let line;
1073
+ let column;
1074
+ if ("node" in pathOrLoc) {
1075
+ const loc = pathOrLoc.node.loc;
1076
+ line = loc?.start.line;
1077
+ column = loc?.start.column;
1078
+ }
1079
+ else {
1080
+ line = pathOrLoc.line;
1081
+ column = pathOrLoc.column;
1082
+ }
1083
+ return { kind, pattern, line, column, note };
1084
+ }
1085
+ /** Capture line/column from a path BEFORE the replace clobbers it. */
1086
+ function captureLoc(path) {
1087
+ const loc = path.node.loc;
1088
+ return { line: loc?.start.line, column: loc?.start.column };
1089
+ }
1090
+ /**
1091
+ * Single entry point for every AST mutation: captures loc, swaps
1092
+ * the node, pushes a flag, and flips state.mutated. Centralising
1093
+ * these three steps eliminates the easy-to-miss bug where one of
1094
+ * them is forgotten (which surfaced as the round-3 line/col-loss
1095
+ * bug AND the round-4 envelope mutated-detection bug).
1096
+ */
1097
+ function applyRewrite(path, replacement, kind, pattern, note, flags, state) {
1098
+ const loc = captureLoc(path);
1099
+ path.replace(replacement);
1100
+ flags.push(makeFlag(loc, kind, pattern, note));
1101
+ state.mutated = true;
1102
+ }
1103
+ /**
1104
+ * Entry point matching jscodeshift's `transform` signature, in case
1105
+ * a caller wants to run this as a standalone codemod via the
1106
+ * jscodeshift CLI (`jscodeshift -t this-file.js src/`). Not used by
1107
+ * Percher's own file walker — that calls `rewriteSupabaseSdk`
1108
+ * directly for control over the flag-stream — but exposing it keeps
1109
+ * the door open for ad-hoc usage.
1110
+ */
1111
+ export default function transform(file, _api, options = {}) {
1112
+ const result = rewriteSupabaseSdk({
1113
+ source: file.source,
1114
+ filePath: file.path,
1115
+ pbIdentifier: typeof options.pbIdentifier === "string" ? options.pbIdentifier : undefined,
1116
+ });
1117
+ return result.changed ? result.rewritten : null;
1118
+ }
1119
+ //# sourceMappingURL=migrate-supabase-sdk.js.map