@edge-base/cli 0.1.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 (234) hide show
  1. package/README.md +182 -0
  2. package/dist/commands/admin.d.ts +10 -0
  3. package/dist/commands/admin.d.ts.map +1 -0
  4. package/dist/commands/admin.js +307 -0
  5. package/dist/commands/admin.js.map +1 -0
  6. package/dist/commands/backup.d.ts +148 -0
  7. package/dist/commands/backup.d.ts.map +1 -0
  8. package/dist/commands/backup.js +1247 -0
  9. package/dist/commands/backup.js.map +1 -0
  10. package/dist/commands/completion.d.ts +3 -0
  11. package/dist/commands/completion.d.ts.map +1 -0
  12. package/dist/commands/completion.js +168 -0
  13. package/dist/commands/completion.js.map +1 -0
  14. package/dist/commands/create-plugin.d.ts +3 -0
  15. package/dist/commands/create-plugin.d.ts.map +1 -0
  16. package/dist/commands/create-plugin.js +208 -0
  17. package/dist/commands/create-plugin.js.map +1 -0
  18. package/dist/commands/deploy.d.ts +146 -0
  19. package/dist/commands/deploy.d.ts.map +1 -0
  20. package/dist/commands/deploy.js +1823 -0
  21. package/dist/commands/deploy.js.map +1 -0
  22. package/dist/commands/describe.d.ts +45 -0
  23. package/dist/commands/describe.d.ts.map +1 -0
  24. package/dist/commands/describe.js +114 -0
  25. package/dist/commands/describe.js.map +1 -0
  26. package/dist/commands/destroy.d.ts +13 -0
  27. package/dist/commands/destroy.d.ts.map +1 -0
  28. package/dist/commands/destroy.js +642 -0
  29. package/dist/commands/destroy.js.map +1 -0
  30. package/dist/commands/dev.d.ts +80 -0
  31. package/dist/commands/dev.d.ts.map +1 -0
  32. package/dist/commands/dev.js +1131 -0
  33. package/dist/commands/dev.js.map +1 -0
  34. package/dist/commands/docker.d.ts +22 -0
  35. package/dist/commands/docker.d.ts.map +1 -0
  36. package/dist/commands/docker.js +373 -0
  37. package/dist/commands/docker.js.map +1 -0
  38. package/dist/commands/export.d.ts +15 -0
  39. package/dist/commands/export.d.ts.map +1 -0
  40. package/dist/commands/export.js +142 -0
  41. package/dist/commands/export.js.map +1 -0
  42. package/dist/commands/init.d.ts +7 -0
  43. package/dist/commands/init.d.ts.map +1 -0
  44. package/dist/commands/init.js +506 -0
  45. package/dist/commands/init.js.map +1 -0
  46. package/dist/commands/keys.d.ts +23 -0
  47. package/dist/commands/keys.d.ts.map +1 -0
  48. package/dist/commands/keys.js +347 -0
  49. package/dist/commands/keys.js.map +1 -0
  50. package/dist/commands/logs.d.ts +17 -0
  51. package/dist/commands/logs.d.ts.map +1 -0
  52. package/dist/commands/logs.js +104 -0
  53. package/dist/commands/logs.js.map +1 -0
  54. package/dist/commands/migrate.d.ts +29 -0
  55. package/dist/commands/migrate.d.ts.map +1 -0
  56. package/dist/commands/migrate.js +302 -0
  57. package/dist/commands/migrate.js.map +1 -0
  58. package/dist/commands/migration.d.ts +18 -0
  59. package/dist/commands/migration.d.ts.map +1 -0
  60. package/dist/commands/migration.js +114 -0
  61. package/dist/commands/migration.js.map +1 -0
  62. package/dist/commands/neon.d.ts +66 -0
  63. package/dist/commands/neon.d.ts.map +1 -0
  64. package/dist/commands/neon.js +600 -0
  65. package/dist/commands/neon.js.map +1 -0
  66. package/dist/commands/plugins.d.ts +9 -0
  67. package/dist/commands/plugins.d.ts.map +1 -0
  68. package/dist/commands/plugins.js +295 -0
  69. package/dist/commands/plugins.js.map +1 -0
  70. package/dist/commands/realtime.d.ts +3 -0
  71. package/dist/commands/realtime.d.ts.map +1 -0
  72. package/dist/commands/realtime.js +71 -0
  73. package/dist/commands/realtime.js.map +1 -0
  74. package/dist/commands/secret.d.ts +7 -0
  75. package/dist/commands/secret.d.ts.map +1 -0
  76. package/dist/commands/secret.js +180 -0
  77. package/dist/commands/secret.js.map +1 -0
  78. package/dist/commands/seed.d.ts +21 -0
  79. package/dist/commands/seed.d.ts.map +1 -0
  80. package/dist/commands/seed.js +325 -0
  81. package/dist/commands/seed.js.map +1 -0
  82. package/dist/commands/telemetry.d.ts +12 -0
  83. package/dist/commands/telemetry.d.ts.map +1 -0
  84. package/dist/commands/telemetry.js +57 -0
  85. package/dist/commands/telemetry.js.map +1 -0
  86. package/dist/commands/typegen.d.ts +26 -0
  87. package/dist/commands/typegen.d.ts.map +1 -0
  88. package/dist/commands/typegen.js +212 -0
  89. package/dist/commands/typegen.js.map +1 -0
  90. package/dist/commands/upgrade.d.ts +29 -0
  91. package/dist/commands/upgrade.d.ts.map +1 -0
  92. package/dist/commands/upgrade.js +265 -0
  93. package/dist/commands/upgrade.js.map +1 -0
  94. package/dist/commands/webhook-test.d.ts +3 -0
  95. package/dist/commands/webhook-test.d.ts.map +1 -0
  96. package/dist/commands/webhook-test.js +133 -0
  97. package/dist/commands/webhook-test.js.map +1 -0
  98. package/dist/index.d.ts +3 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +183 -0
  101. package/dist/index.js.map +1 -0
  102. package/dist/lib/agent-contract.d.ts +36 -0
  103. package/dist/lib/agent-contract.d.ts.map +1 -0
  104. package/dist/lib/agent-contract.js +78 -0
  105. package/dist/lib/agent-contract.js.map +1 -0
  106. package/dist/lib/cf-auth.d.ts +76 -0
  107. package/dist/lib/cf-auth.d.ts.map +1 -0
  108. package/dist/lib/cf-auth.js +321 -0
  109. package/dist/lib/cf-auth.js.map +1 -0
  110. package/dist/lib/cli-context.d.ts +23 -0
  111. package/dist/lib/cli-context.d.ts.map +1 -0
  112. package/dist/lib/cli-context.js +40 -0
  113. package/dist/lib/cli-context.js.map +1 -0
  114. package/dist/lib/cloudflare-deploy-manifest.d.ts +26 -0
  115. package/dist/lib/cloudflare-deploy-manifest.d.ts.map +1 -0
  116. package/dist/lib/cloudflare-deploy-manifest.js +107 -0
  117. package/dist/lib/cloudflare-deploy-manifest.js.map +1 -0
  118. package/dist/lib/cloudflare-wrangler-resources.d.ts +32 -0
  119. package/dist/lib/cloudflare-wrangler-resources.d.ts.map +1 -0
  120. package/dist/lib/cloudflare-wrangler-resources.js +59 -0
  121. package/dist/lib/cloudflare-wrangler-resources.js.map +1 -0
  122. package/dist/lib/config-editor.d.ts +139 -0
  123. package/dist/lib/config-editor.d.ts.map +1 -0
  124. package/dist/lib/config-editor.js +1188 -0
  125. package/dist/lib/config-editor.js.map +1 -0
  126. package/dist/lib/deploy-shared.d.ts +55 -0
  127. package/dist/lib/deploy-shared.d.ts.map +1 -0
  128. package/dist/lib/deploy-shared.js +183 -0
  129. package/dist/lib/deploy-shared.js.map +1 -0
  130. package/dist/lib/dev-sidecar.d.ts +31 -0
  131. package/dist/lib/dev-sidecar.d.ts.map +1 -0
  132. package/dist/lib/dev-sidecar.js +1058 -0
  133. package/dist/lib/dev-sidecar.js.map +1 -0
  134. package/dist/lib/fetch-with-timeout.d.ts +14 -0
  135. package/dist/lib/fetch-with-timeout.d.ts.map +1 -0
  136. package/dist/lib/fetch-with-timeout.js +29 -0
  137. package/dist/lib/fetch-with-timeout.js.map +1 -0
  138. package/dist/lib/function-registry.d.ts +56 -0
  139. package/dist/lib/function-registry.d.ts.map +1 -0
  140. package/dist/lib/function-registry.js +210 -0
  141. package/dist/lib/function-registry.js.map +1 -0
  142. package/dist/lib/load-config.d.ts +24 -0
  143. package/dist/lib/load-config.d.ts.map +1 -0
  144. package/dist/lib/load-config.js +263 -0
  145. package/dist/lib/load-config.js.map +1 -0
  146. package/dist/lib/local-secrets.d.ts +2 -0
  147. package/dist/lib/local-secrets.d.ts.map +1 -0
  148. package/dist/lib/local-secrets.js +60 -0
  149. package/dist/lib/local-secrets.js.map +1 -0
  150. package/dist/lib/managed-resource-names.d.ts +4 -0
  151. package/dist/lib/managed-resource-names.d.ts.map +1 -0
  152. package/dist/lib/managed-resource-names.js +19 -0
  153. package/dist/lib/managed-resource-names.js.map +1 -0
  154. package/dist/lib/migrator.d.ts +57 -0
  155. package/dist/lib/migrator.d.ts.map +1 -0
  156. package/dist/lib/migrator.js +321 -0
  157. package/dist/lib/migrator.js.map +1 -0
  158. package/dist/lib/neon.d.ts +41 -0
  159. package/dist/lib/neon.d.ts.map +1 -0
  160. package/dist/lib/neon.js +325 -0
  161. package/dist/lib/neon.js.map +1 -0
  162. package/dist/lib/node-tools.d.ts +10 -0
  163. package/dist/lib/node-tools.d.ts.map +1 -0
  164. package/dist/lib/node-tools.js +32 -0
  165. package/dist/lib/node-tools.js.map +1 -0
  166. package/dist/lib/npm.d.ts +8 -0
  167. package/dist/lib/npm.d.ts.map +1 -0
  168. package/dist/lib/npm.js +10 -0
  169. package/dist/lib/npm.js.map +1 -0
  170. package/dist/lib/npx.d.ts +9 -0
  171. package/dist/lib/npx.d.ts.map +1 -0
  172. package/dist/lib/npx.js +11 -0
  173. package/dist/lib/npx.js.map +1 -0
  174. package/dist/lib/project-runtime.d.ts +38 -0
  175. package/dist/lib/project-runtime.d.ts.map +1 -0
  176. package/dist/lib/project-runtime.js +122 -0
  177. package/dist/lib/project-runtime.js.map +1 -0
  178. package/dist/lib/prompts.d.ts +28 -0
  179. package/dist/lib/prompts.d.ts.map +1 -0
  180. package/dist/lib/prompts.js +85 -0
  181. package/dist/lib/prompts.js.map +1 -0
  182. package/dist/lib/rate-limit-bindings.d.ts +11 -0
  183. package/dist/lib/rate-limit-bindings.d.ts.map +1 -0
  184. package/dist/lib/rate-limit-bindings.js +52 -0
  185. package/dist/lib/rate-limit-bindings.js.map +1 -0
  186. package/dist/lib/realtime-provision.d.ts +22 -0
  187. package/dist/lib/realtime-provision.d.ts.map +1 -0
  188. package/dist/lib/realtime-provision.js +246 -0
  189. package/dist/lib/realtime-provision.js.map +1 -0
  190. package/dist/lib/resolve-options.d.ts +42 -0
  191. package/dist/lib/resolve-options.d.ts.map +1 -0
  192. package/dist/lib/resolve-options.js +98 -0
  193. package/dist/lib/resolve-options.js.map +1 -0
  194. package/dist/lib/runtime-scaffold.d.ts +17 -0
  195. package/dist/lib/runtime-scaffold.d.ts.map +1 -0
  196. package/dist/lib/runtime-scaffold.js +366 -0
  197. package/dist/lib/runtime-scaffold.js.map +1 -0
  198. package/dist/lib/schema-check.d.ts +79 -0
  199. package/dist/lib/schema-check.d.ts.map +1 -0
  200. package/dist/lib/schema-check.js +347 -0
  201. package/dist/lib/schema-check.js.map +1 -0
  202. package/dist/lib/spinner.d.ts +20 -0
  203. package/dist/lib/spinner.d.ts.map +1 -0
  204. package/dist/lib/spinner.js +42 -0
  205. package/dist/lib/spinner.js.map +1 -0
  206. package/dist/lib/telemetry.d.ts +37 -0
  207. package/dist/lib/telemetry.d.ts.map +1 -0
  208. package/dist/lib/telemetry.js +98 -0
  209. package/dist/lib/telemetry.js.map +1 -0
  210. package/dist/lib/turnstile-provision.d.ts +27 -0
  211. package/dist/lib/turnstile-provision.d.ts.map +1 -0
  212. package/dist/lib/turnstile-provision.js +144 -0
  213. package/dist/lib/turnstile-provision.js.map +1 -0
  214. package/dist/lib/update-check.d.ts +13 -0
  215. package/dist/lib/update-check.d.ts.map +1 -0
  216. package/dist/lib/update-check.js +110 -0
  217. package/dist/lib/update-check.js.map +1 -0
  218. package/dist/lib/wrangler-secrets.d.ts +3 -0
  219. package/dist/lib/wrangler-secrets.d.ts.map +1 -0
  220. package/dist/lib/wrangler-secrets.js +32 -0
  221. package/dist/lib/wrangler-secrets.js.map +1 -0
  222. package/dist/lib/wrangler.d.ts +9 -0
  223. package/dist/lib/wrangler.d.ts.map +1 -0
  224. package/dist/lib/wrangler.js +84 -0
  225. package/dist/lib/wrangler.js.map +1 -0
  226. package/dist/templates/plugin/README.md.tmpl +91 -0
  227. package/dist/templates/plugin/client/js/package.json.tmpl +23 -0
  228. package/dist/templates/plugin/client/js/src/index.ts.tmpl +68 -0
  229. package/dist/templates/plugin/client/js/tsconfig.json.tmpl +14 -0
  230. package/dist/templates/plugin/server/package.json.tmpl +19 -0
  231. package/dist/templates/plugin/server/src/index.ts.tmpl +59 -0
  232. package/dist/templates/plugin/server/tsconfig.json.tmpl +14 -0
  233. package/llms.txt +94 -0
  234. package/package.json +60 -0
@@ -0,0 +1,1131 @@
1
+ import { Command, CommanderError } from 'commander';
2
+ import { spawn } from 'node:child_process';
3
+ import { createHash, randomBytes } from 'node:crypto';
4
+ import { createServer } from 'node:net';
5
+ import { existsSync, watch, readFileSync, copyFileSync, unlinkSync, readdirSync, mkdirSync, promises as fsPromises, } from 'node:fs';
6
+ import { tmpdir } from 'node:os';
7
+ import { dirname, relative, resolve, join } from 'node:path';
8
+ import chalk from 'chalk';
9
+ import { loadConfigSafe } from '../lib/load-config.js';
10
+ import { resolveRateLimitBindings } from '../lib/rate-limit-bindings.js';
11
+ import { checkWranglerAuth } from '../lib/cf-auth.js';
12
+ import { parseDevVars } from '../lib/dev-sidecar.js';
13
+ import { writeLocalSecrets } from '../lib/local-secrets.js';
14
+ import { ensureLocalWranglerToml, ensureRuntimeScaffold, getRuntimeServerSrcDir, resolveWranglerCommand, writeRuntimeConfigShim, } from '../lib/runtime-scaffold.js';
15
+ import { resolveLocalDevBindings } from '../lib/project-runtime.js';
16
+ import { generateFunctionRegistry, scanFunctions, validateRouteNames, } from '../lib/function-registry.js';
17
+ import { extractDatabases, generateTempWranglerToml, } from '../lib/deploy-shared.js';
18
+ import { buildSnapshot, loadSnapshot, detectDestructiveChanges, filterAutoPassChanges, handleDestructiveChanges, resetLocalDoState, saveSnapshot, detectProviderChanges, detectAuthProviderChange, } from '../lib/schema-check.js';
19
+ import { dumpCurrentData, restoreToNewProvider, promptMigration, } from '../lib/migrator.js';
20
+ import { isCliStructuredError, raiseCliError } from '../lib/agent-contract.js';
21
+ import { isNonInteractive } from '../lib/cli-context.js';
22
+ const FULL_CONFIG_EVAL = { allowRegexFallback: false };
23
+ const DEFAULT_DEV_PORT = 8787;
24
+ const DEFAULT_WRANGLER_INSPECTOR_PORT = 9231;
25
+ const MANAGED_AUTH_ENV_KEYS = ['EDGEBASE_AUTH_ALLOWED_OAUTH_PROVIDERS'];
26
+ const MANAGED_AUTH_ENV_PREFIXES = ['EDGEBASE_OAUTH_', 'EDGEBASE_OIDC_'];
27
+ const PORT_SEARCH_LIMIT = 50;
28
+ const PORT_RESERVATION_STALE_GRACE_MS = 10_000;
29
+ const PORT_RESERVATION_DIR = process.env.EDGEBASE_DEV_PORT_RESERVATION_DIR
30
+ ? resolve(process.env.EDGEBASE_DEV_PORT_RESERVATION_DIR)
31
+ : join(tmpdir(), 'edgebase-dev-port-reservations');
32
+ export { resolveLocalDevBindings };
33
+ const activePortReservations = new Set();
34
+ let portReservationCleanupRegistered = false;
35
+ function isManagedAuthEnvKey(key) {
36
+ return MANAGED_AUTH_ENV_KEYS.includes(key)
37
+ || MANAGED_AUTH_ENV_PREFIXES.some((prefix) => key.startsWith(prefix));
38
+ }
39
+ function parsePort(value, flagName) {
40
+ const port = Number.parseInt(value, 10);
41
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
42
+ throw new Error(`Invalid ${flagName}: '${value}'. Expected a port between 1 and 65535.`);
43
+ }
44
+ return port;
45
+ }
46
+ async function probePortHost(port, host) {
47
+ return new Promise((resolve) => {
48
+ const server = createServer();
49
+ server.once('error', (err) => {
50
+ if (err.code === 'EAFNOSUPPORT' || err.code === 'EADDRNOTAVAIL') {
51
+ resolve('unsupported');
52
+ return;
53
+ }
54
+ resolve('busy');
55
+ });
56
+ server.once('listening', () => {
57
+ server.close((err) => {
58
+ if (err) {
59
+ resolve('busy');
60
+ return;
61
+ }
62
+ resolve('available');
63
+ });
64
+ });
65
+ server.listen({ port, host, exclusive: true });
66
+ });
67
+ }
68
+ export async function isPortAvailable(port, exclude = new Set()) {
69
+ if (exclude.has(port))
70
+ return false;
71
+ if (await isPortReserved(port))
72
+ return false;
73
+ return isPortBindable(port, exclude);
74
+ }
75
+ async function isPortBindable(port, exclude = new Set()) {
76
+ if (exclude.has(port))
77
+ return false;
78
+ const results = await Promise.all([
79
+ probePortHost(port, '127.0.0.1'),
80
+ probePortHost(port, '::1'),
81
+ ]);
82
+ return results.every((result) => result !== 'busy') && results.some((result) => result === 'available');
83
+ }
84
+ export async function findAvailablePort(startPort, exclude = new Set(), maxAttempts = PORT_SEARCH_LIMIT) {
85
+ for (let offset = 0; offset < maxAttempts; offset++) {
86
+ const candidate = startPort + offset;
87
+ if (candidate > 65535)
88
+ break;
89
+ if (await isPortAvailable(candidate, exclude))
90
+ return candidate;
91
+ }
92
+ throw new Error(`Could not find an available port in the range ${startPort}-${Math.min(65535, startPort + maxAttempts - 1)}.`);
93
+ }
94
+ function getPortReservationPath(port) {
95
+ return join(PORT_RESERVATION_DIR, `${port}.lock`);
96
+ }
97
+ function ensurePortReservationCleanup() {
98
+ if (portReservationCleanupRegistered)
99
+ return;
100
+ portReservationCleanupRegistered = true;
101
+ process.on('exit', () => {
102
+ for (const reservationPath of activePortReservations) {
103
+ try {
104
+ unlinkSync(reservationPath);
105
+ }
106
+ catch {
107
+ // Best-effort cleanup only.
108
+ }
109
+ }
110
+ activePortReservations.clear();
111
+ });
112
+ }
113
+ function isProcessAlive(pid) {
114
+ if (!Number.isInteger(pid) || pid === null || pid <= 0)
115
+ return false;
116
+ try {
117
+ process.kill(pid, 0);
118
+ return true;
119
+ }
120
+ catch (error) {
121
+ return error.code === 'EPERM';
122
+ }
123
+ }
124
+ async function readReservedPid(reservationPath) {
125
+ try {
126
+ const raw = await fsPromises.readFile(reservationPath, 'utf8');
127
+ const parsed = JSON.parse(raw);
128
+ return typeof parsed.pid === 'number' ? parsed.pid : null;
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ async function clearStalePortReservation(reservationPath) {
135
+ let stats;
136
+ try {
137
+ stats = await fsPromises.stat(reservationPath);
138
+ }
139
+ catch (error) {
140
+ if (error.code === 'ENOENT')
141
+ return true;
142
+ throw error;
143
+ }
144
+ const reservedPid = await readReservedPid(reservationPath);
145
+ if (isProcessAlive(reservedPid))
146
+ return false;
147
+ if (reservedPid === null && Date.now() - stats.mtimeMs < PORT_RESERVATION_STALE_GRACE_MS) {
148
+ return false;
149
+ }
150
+ try {
151
+ await fsPromises.unlink(reservationPath);
152
+ return true;
153
+ }
154
+ catch (error) {
155
+ if (error.code === 'ENOENT')
156
+ return true;
157
+ throw error;
158
+ }
159
+ }
160
+ async function isPortReserved(port) {
161
+ const reservationPath = getPortReservationPath(port);
162
+ try {
163
+ await fsPromises.access(reservationPath);
164
+ }
165
+ catch (error) {
166
+ if (error.code === 'ENOENT')
167
+ return false;
168
+ throw error;
169
+ }
170
+ if (await clearStalePortReservation(reservationPath))
171
+ return false;
172
+ return true;
173
+ }
174
+ async function tryReservePort(port, exclude = new Set()) {
175
+ if (exclude.has(port))
176
+ return null;
177
+ await fsPromises.mkdir(PORT_RESERVATION_DIR, { recursive: true });
178
+ ensurePortReservationCleanup();
179
+ const reservationPath = getPortReservationPath(port);
180
+ while (true) {
181
+ try {
182
+ const handle = await fsPromises.open(reservationPath, 'wx');
183
+ try {
184
+ await handle.writeFile(JSON.stringify({
185
+ pid: process.pid,
186
+ port,
187
+ createdAt: new Date().toISOString(),
188
+ }));
189
+ }
190
+ finally {
191
+ await handle.close();
192
+ }
193
+ break;
194
+ }
195
+ catch (error) {
196
+ if (error.code !== 'EEXIST')
197
+ throw error;
198
+ if (await clearStalePortReservation(reservationPath))
199
+ continue;
200
+ return null;
201
+ }
202
+ }
203
+ if (!(await isPortBindable(port, exclude))) {
204
+ try {
205
+ await fsPromises.unlink(reservationPath);
206
+ }
207
+ catch (error) {
208
+ if (error.code !== 'ENOENT')
209
+ throw error;
210
+ }
211
+ return null;
212
+ }
213
+ activePortReservations.add(reservationPath);
214
+ let released = false;
215
+ return {
216
+ port,
217
+ release: async () => {
218
+ if (released)
219
+ return;
220
+ released = true;
221
+ activePortReservations.delete(reservationPath);
222
+ try {
223
+ await fsPromises.unlink(reservationPath);
224
+ }
225
+ catch (error) {
226
+ if (error.code !== 'ENOENT')
227
+ throw error;
228
+ }
229
+ },
230
+ };
231
+ }
232
+ async function reservePort(port, exclude = new Set()) {
233
+ const reservation = await tryReservePort(port, exclude);
234
+ if (reservation)
235
+ return reservation;
236
+ throw new Error(`Port ${port} is already in use or reserved by another EdgeBase dev server.`);
237
+ }
238
+ export async function findAndReservePort(startPort, exclude = new Set(), maxAttempts = PORT_SEARCH_LIMIT) {
239
+ for (let offset = 0; offset < maxAttempts; offset++) {
240
+ const candidate = startPort + offset;
241
+ if (candidate > 65535)
242
+ break;
243
+ const reservation = await tryReservePort(candidate, exclude);
244
+ if (reservation)
245
+ return reservation;
246
+ }
247
+ throw new Error(`Could not reserve an available port in the range ${startPort}-${Math.min(65535, startPort + maxAttempts - 1)}.`);
248
+ }
249
+ function sanitizeIsolationName(value) {
250
+ return value
251
+ .trim()
252
+ .toLowerCase()
253
+ .replace(/[^a-z0-9]+/g, '-')
254
+ .replace(/^-+|-+$/g, '');
255
+ }
256
+ export function resolveDevPersistence(projectDir, port, isolated) {
257
+ if (isolated) {
258
+ const rawName = typeof isolated === 'string' ? isolated : `port-${port}`;
259
+ const label = sanitizeIsolationName(rawName);
260
+ if (!label) {
261
+ throw new Error('Invalid --isolated value. Use letters, numbers, dashes, or omit the name.');
262
+ }
263
+ return {
264
+ label,
265
+ persistTo: join(projectDir, '.edgebase', 'dev', label, 'state'),
266
+ };
267
+ }
268
+ if (port === DEFAULT_DEV_PORT) {
269
+ return {};
270
+ }
271
+ return {
272
+ label: `port-${port}`,
273
+ persistTo: join(projectDir, '.edgebase', 'dev', `port-${port}`, 'state'),
274
+ };
275
+ }
276
+ export async function resolveDevPorts(preferredPort) {
277
+ const port = await findAvailablePort(preferredPort);
278
+ const sidecarPort = await findAvailablePort(port + 1, new Set([port]));
279
+ const inspectorPort = await findAvailablePort(DEFAULT_WRANGLER_INSPECTOR_PORT, new Set([port, sidecarPort]));
280
+ return {
281
+ port,
282
+ sidecarPort,
283
+ inspectorPort,
284
+ portChanged: port !== preferredPort,
285
+ sidecarChanged: sidecarPort !== port + 1,
286
+ inspectorChanged: inspectorPort !== DEFAULT_WRANGLER_INSPECTOR_PORT,
287
+ };
288
+ }
289
+ export async function reserveDevPorts(preferredPort, preferredInspectorPort) {
290
+ const reservations = [];
291
+ try {
292
+ const portReservation = await findAndReservePort(preferredPort);
293
+ reservations.push(portReservation);
294
+ const sidecarReservation = await findAndReservePort(portReservation.port + 1, new Set([portReservation.port]));
295
+ reservations.push(sidecarReservation);
296
+ const inspectorReservation = preferredInspectorPort !== undefined
297
+ ? await reservePort(preferredInspectorPort, new Set([portReservation.port, sidecarReservation.port]))
298
+ : await findAndReservePort(DEFAULT_WRANGLER_INSPECTOR_PORT, new Set([portReservation.port, sidecarReservation.port]));
299
+ reservations.push(inspectorReservation);
300
+ return {
301
+ port: portReservation.port,
302
+ sidecarPort: sidecarReservation.port,
303
+ inspectorPort: inspectorReservation.port,
304
+ portChanged: portReservation.port !== preferredPort,
305
+ sidecarChanged: sidecarReservation.port !== portReservation.port + 1,
306
+ inspectorChanged: inspectorReservation.port !== DEFAULT_WRANGLER_INSPECTOR_PORT,
307
+ release: async () => {
308
+ await Promise.allSettled([...reservations].reverse().map((reservation) => reservation.release()));
309
+ },
310
+ };
311
+ }
312
+ catch (error) {
313
+ await Promise.allSettled([...reservations].reverse().map((reservation) => reservation.release()));
314
+ throw error;
315
+ }
316
+ }
317
+ function generateDevSecret() {
318
+ return randomBytes(32).toString('hex');
319
+ }
320
+ function resolveWorkerVarBindings(sidecarPort) {
321
+ const vars = [];
322
+ if (sidecarPort) {
323
+ vars.push(`EDGEBASE_DEV_SIDECAR_PORT:${sidecarPort}`);
324
+ }
325
+ const internalWorkerUrl = process.env.EDGEBASE_INTERNAL_WORKER_URL?.trim();
326
+ if (internalWorkerUrl) {
327
+ vars.push(`EDGEBASE_INTERNAL_WORKER_URL:${internalWorkerUrl.replace(/\/+$/, '')}`);
328
+ }
329
+ return vars;
330
+ }
331
+ function syncEnvDevelopmentToDevVars(projectDir, log = false) {
332
+ const envDevPath = join(projectDir, '.env.development');
333
+ if (!existsSync(envDevPath))
334
+ return false;
335
+ copyFileSync(envDevPath, join(projectDir, '.dev.vars'));
336
+ if (log) {
337
+ console.log(chalk.green('✓'), '.env.development → .dev.vars synced');
338
+ console.log();
339
+ }
340
+ return true;
341
+ }
342
+ function syncDevEnvToProcess(projectDir) {
343
+ const envValues = parseDevVars(projectDir);
344
+ for (const key of Object.keys(process.env)) {
345
+ if (!isManagedAuthEnvKey(key))
346
+ continue;
347
+ if (!(key in envValues)) {
348
+ delete process.env[key];
349
+ }
350
+ }
351
+ for (const [key, value] of Object.entries(envValues)) {
352
+ process.env[key] = value;
353
+ }
354
+ return envValues;
355
+ }
356
+ export function ensureDevJwtSecrets(projectDir) {
357
+ const envDevPath = join(projectDir, '.env.development');
358
+ const devVarsPath = join(projectDir, '.dev.vars');
359
+ const primaryPath = (existsSync(envDevPath) || !existsSync(devVarsPath)) ? envDevPath : devVarsPath;
360
+ const current = parseDevVars(projectDir);
361
+ const updates = {};
362
+ if (!current.JWT_USER_SECRET) {
363
+ updates.JWT_USER_SECRET = generateDevSecret();
364
+ }
365
+ if (!current.JWT_ADMIN_SECRET) {
366
+ updates.JWT_ADMIN_SECRET = generateDevSecret();
367
+ }
368
+ const generatedKeys = Object.keys(updates);
369
+ if (generatedKeys.length > 0) {
370
+ writeLocalSecrets(projectDir, updates);
371
+ }
372
+ return {
373
+ generatedKeys,
374
+ primaryPath: generatedKeys.length > 0 ? primaryPath : null,
375
+ };
376
+ }
377
+ /**
378
+ * `npx edgebase dev` — Local development server using Miniflare.
379
+ *
380
+ * Wraps `wrangler dev` with:
381
+ * 1. Plugin detection
382
+ * 2. functions/ watch → _functions-registry.ts 자동 재생성
383
+ * 3. edgebase.config.ts watch → runtime config refresh + wrangler 재시작
384
+ */
385
+ export const devCommand = new Command('dev')
386
+ .alias('dv')
387
+ .description('Start local development server')
388
+ .option('-p, --port <port>', 'Preferred port number', String(DEFAULT_DEV_PORT))
389
+ .option('--host <host>', 'Bind wrangler dev to a specific host or IP address')
390
+ .option('--inspector-port <port>', 'Bind wrangler dev inspector to a specific port')
391
+ .option('--isolated [name]', 'Use an isolated local state directory (defaults to the selected port)')
392
+ .option('--no-open', 'Do not open admin dashboard in browser')
393
+ .action(async (options) => {
394
+ const projectDir = resolve('.');
395
+ const configPath = join(projectDir, 'edgebase.config.ts');
396
+ const configDir = join(projectDir, 'config');
397
+ if (!existsSync(configPath)) {
398
+ raiseCliError({
399
+ code: 'dev_config_not_found',
400
+ message: 'edgebase.config.ts not found.',
401
+ hint: 'Run `npm create edge-base@latest my-app` first.',
402
+ });
403
+ }
404
+ const preferredPort = parsePort(options.port, '--port');
405
+ const preferredInspectorPort = options.inspectorPort
406
+ ? parsePort(options.inspectorPort, '--inspector-port')
407
+ : undefined;
408
+ const resolvedPorts = await reserveDevPorts(preferredPort, preferredInspectorPort);
409
+ const persistence = resolveDevPersistence(projectDir, resolvedPorts.port, options.isolated);
410
+ const generatedDevSecrets = ensureDevJwtSecrets(projectDir);
411
+ if (persistence.persistTo) {
412
+ mkdirSync(persistence.persistTo, { recursive: true });
413
+ }
414
+ console.log(chalk.blue('⚡ Starting EdgeBase dev server...'));
415
+ if (resolvedPorts.portChanged) {
416
+ console.log(chalk.yellow('↪'), `Port ${preferredPort} is in use — using ${resolvedPorts.port} instead`);
417
+ }
418
+ else {
419
+ console.log(chalk.dim(` Port: ${resolvedPorts.port}`));
420
+ }
421
+ if (resolvedPorts.sidecarChanged) {
422
+ console.log(chalk.yellow('↪'), `Sidecar port ${resolvedPorts.port + 1} is in use — using ${resolvedPorts.sidecarPort} instead`);
423
+ }
424
+ if (!options.inspectorPort) {
425
+ if (resolvedPorts.inspectorChanged) {
426
+ console.log(chalk.yellow('↪'), `Inspector port ${DEFAULT_WRANGLER_INSPECTOR_PORT} is in use — using ${resolvedPorts.inspectorPort} instead`);
427
+ }
428
+ else {
429
+ console.log(chalk.dim(` Inspector: ${resolvedPorts.inspectorPort}`));
430
+ }
431
+ }
432
+ if (persistence.persistTo) {
433
+ console.log(chalk.dim(` State: ${persistence.persistTo}`));
434
+ }
435
+ if (generatedDevSecrets.generatedKeys.length > 0 && generatedDevSecrets.primaryPath) {
436
+ const label = generatedDevSecrets.generatedKeys.length === 1 ? 'secret' : 'secrets';
437
+ console.log(chalk.green('✓'), `Generated missing local JWT ${label} in ${generatedDevSecrets.primaryPath.replace(`${projectDir}/`, '')}`);
438
+ }
439
+ ensureRuntimeScaffold(projectDir);
440
+ ensureLocalWranglerToml(projectDir);
441
+ // Display release mode
442
+ try {
443
+ syncDevEnvToProcess(projectDir);
444
+ const config = loadConfigSafe(configPath, projectDir, FULL_CONFIG_EVAL);
445
+ if (config.release) {
446
+ console.log(chalk.yellow('🔒'), 'Release mode (release: true) — deny-by-default enforced');
447
+ }
448
+ else {
449
+ console.log(chalk.green('🔓'), 'Development mode (release: false) — all resources accessible without rules');
450
+ }
451
+ checkWranglerAuth(projectDir);
452
+ }
453
+ catch (err) {
454
+ const message = err.message ?? String(err);
455
+ console.error(chalk.red('✗'), 'Failed to parse edgebase.config.ts:');
456
+ // Show detailed error info (file location, syntax details) when available
457
+ for (const line of message.split('\n').slice(0, 8)) {
458
+ if (line.trim())
459
+ console.error(chalk.dim(` ${line.trim()}`));
460
+ }
461
+ console.error(chalk.dim(' → Fix the config file; dev server will retry on save.'));
462
+ }
463
+ console.log();
464
+ // ─── Initial Build: Functions + Config ───
465
+ // Plugin functions are registered at runtime from config.plugins[] (Explicit Import Pattern).
466
+ const functionsDir = join(projectDir, 'functions');
467
+ const serverSrcDir = getRuntimeServerSrcDir(projectDir);
468
+ const registryPath = join(serverSrcDir, '_functions-registry.ts');
469
+ let configDebounce = null;
470
+ let wranglerProcess = null;
471
+ let lastConfigSignature = getPathSignature(configPath);
472
+ const configFileSignatures = new Map();
473
+ const cleanupHandles = [];
474
+ let tempWranglerPath = null;
475
+ let sidecarServer = null;
476
+ let keepRunning = true;
477
+ let restartRequested = false;
478
+ let pendingDevRestore = null;
479
+ let interruptionSignal = null;
480
+ let sessionSettled = false;
481
+ rebuildFunctionsRegistry(functionsDir, registryPath);
482
+ // Hidden runtime imports edgebase.config.ts directly from the project root.
483
+ void rebundleConfig(projectDir, configPath);
484
+ // ─── Watch: functions/ → 자동 재생성 ───
485
+ if (existsSync(functionsDir)) {
486
+ let functionsDebounce = null;
487
+ try {
488
+ const watcher = watch(functionsDir, { recursive: true }, (_eventType, filename) => {
489
+ if (!filename || !filename.endsWith('.ts') || filename.startsWith('_'))
490
+ return;
491
+ // Debounce: 파일 저장 시 여러 이벤트 발생 → 300ms 후 1번만 실행
492
+ if (functionsDebounce)
493
+ clearTimeout(functionsDebounce);
494
+ functionsDebounce = setTimeout(() => {
495
+ console.log();
496
+ console.log(chalk.blue('🔄'), `functions/${filename} changed — rebuilding registry...`);
497
+ rebuildFunctionsRegistry(functionsDir, registryPath);
498
+ }, 300);
499
+ });
500
+ cleanupHandles.push(watcher);
501
+ console.log(chalk.dim(' 👀 Watching functions/ for changes'));
502
+ }
503
+ catch {
504
+ // watch may not support recursive on all platforms
505
+ }
506
+ }
507
+ // ─── Watch: edgebase.config.ts → runtime reload ───
508
+ function cleanupTempWrangler() {
509
+ if (!tempWranglerPath)
510
+ return;
511
+ try {
512
+ unlinkSync(tempWranglerPath);
513
+ }
514
+ catch {
515
+ /* cleanup non-fatal */
516
+ }
517
+ tempWranglerPath = null;
518
+ }
519
+ async function releaseSessionResources() {
520
+ cleanupTempWrangler();
521
+ for (const handle of cleanupHandles.splice(0)) {
522
+ try {
523
+ handle.close();
524
+ }
525
+ catch {
526
+ /* ignore watcher close errors */
527
+ }
528
+ }
529
+ sidecarServer?.close();
530
+ await resolvedPorts.release();
531
+ }
532
+ let resolveSession;
533
+ let rejectSession;
534
+ const sessionDone = new Promise((resolve, reject) => {
535
+ resolveSession = resolve;
536
+ rejectSession = reject;
537
+ });
538
+ async function settleSession(error) {
539
+ if (sessionSettled)
540
+ return;
541
+ sessionSettled = true;
542
+ await releaseSessionResources();
543
+ if (error) {
544
+ rejectSession(error);
545
+ }
546
+ else {
547
+ resolveSession();
548
+ }
549
+ }
550
+ const onConfigChange = (label) => {
551
+ if (configDebounce)
552
+ clearTimeout(configDebounce);
553
+ configDebounce = setTimeout(() => {
554
+ console.log();
555
+ console.log(chalk.blue('🔄'), `${label} changed — reloading config...`);
556
+ void rebundleConfig(projectDir, configPath).then(async () => {
557
+ // Schema destructive change detection + provider change migration
558
+ // Returns true if wrangler was already restarted (reset/migration action)
559
+ const handled = await checkSchemaChanges(projectDir, configPath, wranglerProcess, resolvedPorts.port, persistence.persistTo, (data) => {
560
+ pendingDevRestore = data;
561
+ }, () => {
562
+ restartRequested = true;
563
+ });
564
+ // Restart wrangler to pick up new config (if not already restarted by reset)
565
+ if (!handled && wranglerProcess) {
566
+ console.log(chalk.yellow('♻️'), 'Restarting wrangler dev...');
567
+ restartRequested = true;
568
+ wranglerProcess.kill('SIGTERM');
569
+ }
570
+ });
571
+ }, 500);
572
+ };
573
+ let configPathDebounce = null;
574
+ try {
575
+ const watcher = watch(configPath, () => {
576
+ if (configPathDebounce)
577
+ clearTimeout(configPathDebounce);
578
+ configPathDebounce = setTimeout(() => {
579
+ const nextSignature = getPathSignature(configPath);
580
+ if (!nextSignature || nextSignature === lastConfigSignature)
581
+ return;
582
+ lastConfigSignature = nextSignature;
583
+ onConfigChange('edgebase.config.ts');
584
+ }, 150);
585
+ });
586
+ cleanupHandles.push(watcher);
587
+ console.log(chalk.dim(' 👀 Watching edgebase.config.ts for changes'));
588
+ }
589
+ catch {
590
+ // file watch error — non-fatal
591
+ }
592
+ if (existsSync(configDir)) {
593
+ for (const filePath of listConfigWatchFiles(configDir)) {
594
+ configFileSignatures.set(filePath, getPathSignature(filePath));
595
+ }
596
+ const configDirDebounces = new Map();
597
+ try {
598
+ const watcher = watch(configDir, { recursive: true }, (_eventType, filename) => {
599
+ if (!filename || !/\.(ts|js|mts|cts|mjs|cjs)$/.test(filename))
600
+ return;
601
+ const changedPath = join(configDir, filename);
602
+ const existingDebounce = configDirDebounces.get(changedPath);
603
+ if (existingDebounce)
604
+ clearTimeout(existingDebounce);
605
+ configDirDebounces.set(changedPath, setTimeout(() => {
606
+ configDirDebounces.delete(changedPath);
607
+ const nextSignature = getPathSignature(changedPath);
608
+ const previousSignature = configFileSignatures.get(changedPath) ?? null;
609
+ if (nextSignature === previousSignature)
610
+ return;
611
+ if (!nextSignature && previousSignature === null)
612
+ return;
613
+ configFileSignatures.set(changedPath, nextSignature);
614
+ onConfigChange(`config/${filename}`);
615
+ }, 150));
616
+ });
617
+ cleanupHandles.push(watcher);
618
+ console.log(chalk.dim(' 👀 Watching config/ for changes'));
619
+ }
620
+ catch {
621
+ // file watch error — non-fatal
622
+ }
623
+ }
624
+ const localEnvWatchPath = existsSync(join(projectDir, '.env.development'))
625
+ ? join(projectDir, '.env.development')
626
+ : join(projectDir, '.dev.vars');
627
+ let envPathDebounce = null;
628
+ let lastEnvSignature = getPathSignature(localEnvWatchPath);
629
+ try {
630
+ const watcher = watch(localEnvWatchPath, () => {
631
+ if (envPathDebounce)
632
+ clearTimeout(envPathDebounce);
633
+ envPathDebounce = setTimeout(() => {
634
+ const nextSignature = getPathSignature(localEnvWatchPath);
635
+ if (!nextSignature || nextSignature === lastEnvSignature)
636
+ return;
637
+ lastEnvSignature = nextSignature;
638
+ if (localEnvWatchPath.endsWith('.env.development')) {
639
+ syncEnvDevelopmentToDevVars(projectDir, false);
640
+ }
641
+ onConfigChange(localEnvWatchPath.endsWith('.env.development') ? '.env.development' : '.dev.vars');
642
+ }, 150);
643
+ });
644
+ cleanupHandles.push(watcher);
645
+ console.log(chalk.dim(` 👀 Watching ${relative(projectDir, localEnvWatchPath) || '.env.development'} for changes`));
646
+ }
647
+ catch {
648
+ // file watch error — non-fatal
649
+ }
650
+ console.log();
651
+ // ─── Sync .env.development → .dev.vars ───
652
+ syncEnvDevelopmentToDevVars(projectDir, true);
653
+ // ─── Start Schema Editor Sidecar ───
654
+ const sidecarPort = resolvedPorts.sidecarPort;
655
+ try {
656
+ const { startSidecar, parseDevVars } = await import('../lib/dev-sidecar.js');
657
+ const devVars = parseDevVars(projectDir);
658
+ const adminSecret = devVars.JWT_ADMIN_SECRET;
659
+ if (adminSecret) {
660
+ sidecarServer = startSidecar({
661
+ port: sidecarPort,
662
+ workerPort: resolvedPorts.port,
663
+ configPath,
664
+ projectDir,
665
+ adminSecret,
666
+ });
667
+ }
668
+ else {
669
+ console.log(chalk.dim(' 📐 Schema Editor sidecar skipped (no JWT_ADMIN_SECRET in .env.development or .dev.vars)'));
670
+ }
671
+ }
672
+ catch (err) {
673
+ console.log(chalk.dim(' 📐 Schema Editor sidecar skipped:'), err.message);
674
+ }
675
+ // ─── Start wrangler dev (with auto-restart) ───
676
+ function refreshTempWrangler() {
677
+ cleanupTempWrangler();
678
+ const wranglerPath = join(projectDir, 'wrangler.toml');
679
+ if (!existsSync(wranglerPath))
680
+ return;
681
+ let config;
682
+ try {
683
+ syncDevEnvToProcess(projectDir);
684
+ config = loadConfigSafe(configPath, projectDir, FULL_CONFIG_EVAL);
685
+ }
686
+ catch (err) {
687
+ const detail = err.message ?? String(err);
688
+ const firstLine = detail.split('\n')[0];
689
+ throw new Error(`Failed to evaluate edgebase.config.ts for dev runtime:\n ${firstLine}` +
690
+ (detail.includes('\n') ? '\n ' + detail.split('\n').slice(1, 5).map(l => l.trim()).filter(Boolean).join('\n ') : ''));
691
+ }
692
+ tempWranglerPath = generateTempWranglerToml(wranglerPath, {
693
+ bindings: resolveLocalDevBindings(config),
694
+ triggerMode: 'preserve',
695
+ rateLimitBindings: resolveRateLimitBindings(config),
696
+ });
697
+ }
698
+ function startWrangler() {
699
+ syncEnvDevelopmentToDevVars(projectDir, false);
700
+ try {
701
+ refreshTempWrangler();
702
+ }
703
+ catch (err) {
704
+ void settleSession(Object.assign(new Error(err.message), {
705
+ edgebaseCode: 'dev_runtime_config_failed',
706
+ }));
707
+ return;
708
+ }
709
+ const wranglerTool = resolveWranglerCommand();
710
+ const wranglerArgs = [...wranglerTool.argsPrefix, 'dev', '--port', String(resolvedPorts.port)];
711
+ if (options.host) {
712
+ wranglerArgs.push('--ip', options.host);
713
+ }
714
+ if (options.inspectorPort) {
715
+ wranglerArgs.push('--inspector-port', String(resolvedPorts.inspectorPort));
716
+ }
717
+ else {
718
+ wranglerArgs.push('--inspector-port', String(resolvedPorts.inspectorPort));
719
+ }
720
+ // Pass internal-only runtime vars to the worker.
721
+ for (const binding of resolveWorkerVarBindings(sidecarServer ? sidecarPort : undefined)) {
722
+ wranglerArgs.push('--var', binding);
723
+ }
724
+ if (persistence.persistTo) {
725
+ wranglerArgs.push('--persist-to', persistence.persistTo);
726
+ }
727
+ if (tempWranglerPath) {
728
+ wranglerArgs.push('--config', tempWranglerPath);
729
+ }
730
+ wranglerProcess = spawn(wranglerTool.command, wranglerArgs, {
731
+ cwd: projectDir,
732
+ stdio: 'inherit',
733
+ });
734
+ // Wrap wrangler process errors with EdgeBase-specific guidance so users
735
+ // don't have to decipher raw wrangler/Miniflare output on their own.
736
+ wranglerProcess.on('error', (err) => {
737
+ void settleSession(Object.assign(new Error(err.message), {
738
+ edgebaseCode: 'wrangler_dev_start_failed',
739
+ }));
740
+ });
741
+ wranglerProcess.on('exit', (code) => {
742
+ const shouldRespawn = restartRequested;
743
+ restartRequested = false;
744
+ cleanupTempWrangler();
745
+ if (shouldRespawn) {
746
+ // Config change triggered restart → respawn wrangler
747
+ setTimeout(() => startWrangler(), 500);
748
+ }
749
+ else {
750
+ if (keepRunning) {
751
+ if (code && code !== 0) {
752
+ void settleSession(Object.assign(new Error(`Wrangler exited with code ${code}.`), {
753
+ edgebaseCode: 'wrangler_dev_exit',
754
+ exitCode: code,
755
+ }));
756
+ return;
757
+ }
758
+ void settleSession();
759
+ }
760
+ else {
761
+ void settleSession(interruptionSignal
762
+ ? new CommanderError(130, interruptionSignal, '')
763
+ : undefined);
764
+ }
765
+ }
766
+ });
767
+ // Post-restart migration restore
768
+ if (pendingDevRestore) {
769
+ const restore = pendingDevRestore;
770
+ pendingDevRestore = null;
771
+ const port = resolvedPorts.port;
772
+ // Wait for the new server, then restore
773
+ void waitForServer(port, 30).then(async (ready) => {
774
+ if (!ready) {
775
+ console.error(chalk.red('✗ Server did not start in time. Migration restore skipped.'));
776
+ console.error(chalk.dim(' You can retry with: npx edgebase migrate'));
777
+ return;
778
+ }
779
+ console.log();
780
+ console.log(chalk.blue('📦 Continuing database block migration after the worker restart...'));
781
+ if (restore.scope === 'data' || restore.scope === 'all') {
782
+ const namespaces = restore.namespaces ?? [];
783
+ const blockLabel = namespaces.length > 0
784
+ ? namespaces.join(', ')
785
+ : 'all configured data blocks';
786
+ console.log(chalk.dim(` DB blocks: ${blockLabel}`));
787
+ console.log(chalk.dim(' Every table in each migrated block will be restored to the new provider.'));
788
+ }
789
+ // Read service key from .dev.vars or .env.development
790
+ let serviceKey = '';
791
+ const devVarsPath = join(projectDir, '.dev.vars');
792
+ const envDevPath = join(projectDir, '.env.development');
793
+ for (const envPath of [devVarsPath, envDevPath]) {
794
+ if (existsSync(envPath)) {
795
+ const content = readFileSync(envPath, 'utf-8');
796
+ const match = content.match(/^SERVICE_KEY\s*=\s*(.+)$/m);
797
+ if (match) {
798
+ serviceKey = match[1].trim().replace(/^["']|["']$/g, '');
799
+ break;
800
+ }
801
+ }
802
+ }
803
+ if (!serviceKey) {
804
+ console.error(chalk.red('✗ SERVICE_KEY not found in .dev.vars or .env.development'));
805
+ console.error(chalk.dim(' Add SERVICE_KEY=<your-key> to continue migration.'));
806
+ return;
807
+ }
808
+ try {
809
+ await restoreToNewProvider({
810
+ scope: restore.scope,
811
+ namespaces: restore.namespaces,
812
+ serverUrl: `http://localhost:${port}`,
813
+ serviceKey,
814
+ dryRun: false,
815
+ }, restore.dumped);
816
+ console.log();
817
+ console.log(chalk.green('✓ Database block migration complete!'));
818
+ }
819
+ catch (err) {
820
+ console.error();
821
+ console.error(chalk.red('✗ Migration restore failed:'), err.message);
822
+ console.error(chalk.dim(' You can retry with: npx edgebase migrate'));
823
+ }
824
+ });
825
+ }
826
+ }
827
+ startWrangler();
828
+ // Auto-open admin dashboard when server is ready
829
+ if (options.open) {
830
+ waitForServer(resolvedPorts.port).then((ready) => {
831
+ if (ready) {
832
+ const url = `http://localhost:${resolvedPorts.port}/admin`;
833
+ console.log(chalk.green('✓'), `Opening ${chalk.cyan(url)}`);
834
+ openBrowser(url);
835
+ }
836
+ });
837
+ }
838
+ // Forward signals → clean shutdown
839
+ const handleSigint = () => {
840
+ keepRunning = false;
841
+ interruptionSignal = 'SIGINT';
842
+ restartRequested = false;
843
+ if (wranglerProcess) {
844
+ wranglerProcess.kill('SIGINT');
845
+ }
846
+ else {
847
+ void settleSession(new CommanderError(130, 'SIGINT', ''));
848
+ }
849
+ };
850
+ const handleSigterm = () => {
851
+ keepRunning = false;
852
+ interruptionSignal = 'SIGTERM';
853
+ restartRequested = false;
854
+ if (wranglerProcess) {
855
+ wranglerProcess.kill('SIGTERM');
856
+ }
857
+ else {
858
+ void settleSession(new CommanderError(130, 'SIGTERM', ''));
859
+ }
860
+ };
861
+ process.on('SIGINT', handleSigint);
862
+ process.on('SIGTERM', handleSigterm);
863
+ try {
864
+ await sessionDone;
865
+ }
866
+ catch (error) {
867
+ process.off('SIGINT', handleSigint);
868
+ process.off('SIGTERM', handleSigterm);
869
+ if (error instanceof CommanderError || isCliStructuredError(error))
870
+ throw error;
871
+ const devError = error;
872
+ if (devError.edgebaseCode === 'dev_runtime_config_failed') {
873
+ raiseCliError({
874
+ code: 'dev_runtime_config_failed',
875
+ message: devError.message,
876
+ hint: 'Check edgebase.config.ts and the generated wrangler runtime config, then retry.',
877
+ });
878
+ }
879
+ if (devError.edgebaseCode === 'wrangler_dev_start_failed') {
880
+ raiseCliError({
881
+ code: 'wrangler_dev_start_failed',
882
+ message: `Failed to start wrangler dev: ${devError.message}`,
883
+ hint: devError.message.includes('ENOENT')
884
+ ? 'Wrangler binary not found. Run `npm install` and ensure wrangler is installed.'
885
+ : 'Check your wrangler.toml and edgebase.config.ts, then retry `npx edgebase dev`.',
886
+ });
887
+ }
888
+ if (devError.edgebaseCode === 'wrangler_dev_exit') {
889
+ raiseCliError({
890
+ code: 'wrangler_dev_exit',
891
+ message: devError.message,
892
+ hint: 'Common causes are syntax errors in edgebase.config.ts or wrangler.toml, missing D1 bindings, or port conflicts.',
893
+ }, devError.exitCode ?? 1);
894
+ }
895
+ throw error;
896
+ }
897
+ process.off('SIGINT', handleSigint);
898
+ process.off('SIGTERM', handleSigterm);
899
+ });
900
+ // ─── Helper: Functions Registry Rebuild ───
901
+ function rebuildFunctionsRegistry(functionsDir, registryPath) {
902
+ try {
903
+ const functions = existsSync(functionsDir) ? scanFunctions(functionsDir) : [];
904
+ // Plugin functions are registered at runtime from config.plugins[] (Explicit Import Pattern).
905
+ validateRouteNames(functions);
906
+ generateFunctionRegistry(functions, registryPath, {
907
+ configImportPath: './generated-config.js',
908
+ functionsImportBasePath: relative(dirname(registryPath), functionsDir).replace(/\\/g, '/'),
909
+ });
910
+ if (functions.length > 0) {
911
+ console.log(chalk.green('✓'), `Registry rebuilt: ${functions.length} function(s) →`, functions.map((f) => chalk.cyan(f.name)).join(', '));
912
+ }
913
+ else {
914
+ console.log(chalk.green('✓'), 'Registry rebuilt: 0 user function(s) — plugin functions remain available');
915
+ }
916
+ }
917
+ catch (err) {
918
+ console.error(chalk.red('✗'), 'Failed to rebuild functions registry:', err.message);
919
+ }
920
+ }
921
+ function getPathSignature(path) {
922
+ try {
923
+ const content = readFileSync(path);
924
+ return createHash('sha1').update(content).digest('hex');
925
+ }
926
+ catch {
927
+ return null;
928
+ }
929
+ }
930
+ function listConfigWatchFiles(configDir) {
931
+ const files = [];
932
+ const queue = [configDir];
933
+ while (queue.length > 0) {
934
+ const currentDir = queue.pop();
935
+ if (!currentDir)
936
+ continue;
937
+ for (const entry of readdirSync(currentDir, { withFileTypes: true })) {
938
+ const fullPath = join(currentDir, entry.name);
939
+ if (entry.isDirectory()) {
940
+ queue.push(fullPath);
941
+ continue;
942
+ }
943
+ if (/\.(ts|js|mts|cts|mjs|cjs)$/.test(entry.name)) {
944
+ files.push(fullPath);
945
+ }
946
+ }
947
+ }
948
+ return files;
949
+ }
950
+ export const _devInternals = {
951
+ rebuildFunctionsRegistry,
952
+ getPathSignature,
953
+ listConfigWatchFiles,
954
+ checkSchemaChanges,
955
+ parsePort,
956
+ getPortReservationPath,
957
+ findAndReservePort,
958
+ findAvailablePort,
959
+ isPortAvailable,
960
+ resolveDevPorts,
961
+ reserveDevPorts,
962
+ resolveDevPersistence,
963
+ resolveWorkerVarBindings,
964
+ sanitizeIsolationName,
965
+ ensureDevJwtSecrets,
966
+ };
967
+ // ─── Helper: Config Sync ───
968
+ /**
969
+ * Hidden runtime imports edgebase.config.ts directly.
970
+ * Preserve .dev.vars secrets copied from .env.development.
971
+ */
972
+ async function rebundleConfig(projectDir, configPath) {
973
+ const envValues = syncDevEnvToProcess(projectDir);
974
+ try {
975
+ writeRuntimeConfigShim(projectDir, envValues);
976
+ }
977
+ catch (err) {
978
+ console.error(chalk.red('✗'), 'Config sync failed:', err.message?.split('\n')[0] ?? 'Unknown error');
979
+ console.log(chalk.dim(` → Check that ${configPath} is readable and .dev.vars is writable.`));
980
+ }
981
+ }
982
+ /**
983
+ * Check for destructive schema changes and provider changes after config rebundle.
984
+ * In dev mode, snapshot is NOT updated (§5) — only reset action updates it.
985
+ * Returns true if wrangler was restarted (reset/migration action), false otherwise.
986
+ */
987
+ async function checkSchemaChanges(projectDir, configPath, wranglerProcess, port, persistTo, setPendingRestore, requestRestart) {
988
+ try {
989
+ syncDevEnvToProcess(projectDir);
990
+ const configJson = loadConfigSafe(configPath, projectDir, FULL_CONFIG_EVAL);
991
+ const databases = extractDatabases(configJson);
992
+ if (!databases || Object.keys(databases).length === 0)
993
+ return false;
994
+ const authProvider = configJson.auth?.provider;
995
+ const currentSnapshot = buildSnapshot(databases, authProvider);
996
+ const savedSnapshot = loadSnapshot(projectDir);
997
+ if (!savedSnapshot)
998
+ return false; // No saved snapshot — nothing to compare
999
+ // ─── Destructive Change Detection ───
1000
+ let changes = detectDestructiveChanges(savedSnapshot, currentSnapshot);
1001
+ changes = filterAutoPassChanges(changes, savedSnapshot, currentSnapshot);
1002
+ if (changes.length > 0) {
1003
+ // Dev mode is always release: false
1004
+ const result = await handleDestructiveChanges(changes, false, !isNonInteractive());
1005
+ if (result.action === 'reset') {
1006
+ resetLocalDoState(projectDir, persistTo);
1007
+ saveSnapshot(projectDir, currentSnapshot);
1008
+ console.log(chalk.green('✓'), 'Schema snapshot updated after DB reset');
1009
+ // Restart wrangler to pick up clean state
1010
+ if (wranglerProcess) {
1011
+ console.log(chalk.yellow('♻️'), 'Restarting wrangler dev...');
1012
+ requestRestart();
1013
+ wranglerProcess.kill('SIGTERM');
1014
+ }
1015
+ return true;
1016
+ }
1017
+ if (result.action === 'migration_guide') {
1018
+ return true;
1019
+ }
1020
+ }
1021
+ // ─── Provider Change Detection + Migration ───
1022
+ const providerChanges = detectProviderChanges(savedSnapshot, currentSnapshot);
1023
+ const authChange = detectAuthProviderChange(savedSnapshot, currentSnapshot);
1024
+ const allChanges = [...providerChanges];
1025
+ if (authChange)
1026
+ allChanges.push(authChange);
1027
+ if (allChanges.length > 0 && (process.stdin.isTTY || isNonInteractive())) {
1028
+ const answer = await promptMigration(allChanges);
1029
+ if (answer === 'migrate') {
1030
+ // Read service key from .dev.vars or .env.development
1031
+ let serviceKey = '';
1032
+ const devVarsPath = join(projectDir, '.dev.vars');
1033
+ const envDevPath = join(projectDir, '.env.development');
1034
+ for (const envPath of [devVarsPath, envDevPath]) {
1035
+ if (existsSync(envPath)) {
1036
+ const content = readFileSync(envPath, 'utf-8');
1037
+ const match = content.match(/^SERVICE_KEY\s*=\s*(.+)$/m);
1038
+ if (match) {
1039
+ serviceKey = match[1].trim().replace(/^["']|["']$/g, '');
1040
+ break;
1041
+ }
1042
+ }
1043
+ }
1044
+ if (!serviceKey) {
1045
+ console.error(chalk.red('✗ SERVICE_KEY not found in .dev.vars or .env.development'));
1046
+ console.error(chalk.dim(' Add SERVICE_KEY=<your-key> to enable migration.'));
1047
+ }
1048
+ else {
1049
+ const serverUrl = `http://localhost:${port}`;
1050
+ const dataNamespaces = providerChanges.map((pc) => pc.namespace);
1051
+ const scope = authChange
1052
+ ? dataNamespaces.length > 0
1053
+ ? 'all'
1054
+ : 'auth'
1055
+ : 'data';
1056
+ console.log();
1057
+ console.log(chalk.blue('📦 Dumping data from current provider...'));
1058
+ try {
1059
+ const dumped = await dumpCurrentData({
1060
+ scope,
1061
+ namespaces: dataNamespaces.length > 0 ? dataNamespaces : undefined,
1062
+ serverUrl,
1063
+ serviceKey,
1064
+ dryRun: false,
1065
+ });
1066
+ // Schedule restore after wrangler restarts
1067
+ setPendingRestore({
1068
+ dumped,
1069
+ scope,
1070
+ namespaces: dataNamespaces.length > 0 ? dataNamespaces : undefined,
1071
+ });
1072
+ console.log(chalk.green('✓'), 'Data dumped. Restarting with new config...');
1073
+ }
1074
+ catch (err) {
1075
+ console.error(chalk.red('✗ Data dump failed:'), err.message);
1076
+ console.error(chalk.dim(' Migration skipped. You can retry with: npx edgebase migrate'));
1077
+ }
1078
+ }
1079
+ // Save snapshot so provider change is recorded
1080
+ saveSnapshot(projectDir, currentSnapshot);
1081
+ // Restart wrangler to pick up new provider config
1082
+ if (wranglerProcess) {
1083
+ console.log(chalk.yellow('♻️'), 'Restarting wrangler dev...');
1084
+ requestRestart();
1085
+ wranglerProcess.kill('SIGTERM');
1086
+ }
1087
+ return true;
1088
+ }
1089
+ else {
1090
+ // User chose to skip — save snapshot to not prompt again
1091
+ saveSnapshot(projectDir, currentSnapshot);
1092
+ }
1093
+ }
1094
+ }
1095
+ catch (err) {
1096
+ if (isCliStructuredError(err))
1097
+ throw err;
1098
+ console.error(chalk.red('✗'), `Config reload failed: ${err.message}`);
1099
+ console.log(chalk.dim(' Fix edgebase.config.ts, then restart `npx edgebase dev`.'));
1100
+ }
1101
+ return false;
1102
+ }
1103
+ // ─── Helper: Auto-open browser ───
1104
+ /** Poll the dev server health endpoint until it responds. */
1105
+ async function waitForServer(port, maxRetries = 30) {
1106
+ for (let i = 0; i < maxRetries; i++) {
1107
+ try {
1108
+ const res = await fetch(`http://localhost:${port}/api/health`);
1109
+ if (res.ok)
1110
+ return true;
1111
+ }
1112
+ catch {
1113
+ // Server not ready yet
1114
+ }
1115
+ await new Promise((r) => setTimeout(r, 1000));
1116
+ }
1117
+ return false;
1118
+ }
1119
+ /** Open a URL in the default browser (cross-platform). */
1120
+ function openBrowser(url) {
1121
+ if (process.platform === 'darwin') {
1122
+ spawn('open', [url], { stdio: 'ignore', detached: true }).unref();
1123
+ }
1124
+ else if (process.platform === 'win32') {
1125
+ spawn('cmd', ['/c', 'start', url], { stdio: 'ignore', detached: true }).unref();
1126
+ }
1127
+ else {
1128
+ spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();
1129
+ }
1130
+ }
1131
+ //# sourceMappingURL=dev.js.map