@lunora/cli 1.0.0-alpha.100 → 1.0.0-alpha.102

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 (83) hide show
  1. package/dist/bin.mjs +2 -10
  2. package/dist/index.mjs +1 -18
  3. package/dist/packem_chunks/handler.mjs +1 -160
  4. package/dist/packem_chunks/handler10.mjs +1 -16
  5. package/dist/packem_chunks/handler11.mjs +1 -23
  6. package/dist/packem_chunks/handler12.mjs +2 -192
  7. package/dist/packem_chunks/handler13.mjs +2 -131
  8. package/dist/packem_chunks/handler14.mjs +1 -65
  9. package/dist/packem_chunks/handler15.mjs +1 -100
  10. package/dist/packem_chunks/handler16.mjs +1 -44
  11. package/dist/packem_chunks/handler17.mjs +1 -105
  12. package/dist/packem_chunks/handler18.mjs +3 -170
  13. package/dist/packem_chunks/handler19.mjs +1 -128
  14. package/dist/packem_chunks/handler2.mjs +2 -116
  15. package/dist/packem_chunks/handler20.mjs +1 -94
  16. package/dist/packem_chunks/handler21.mjs +2 -202
  17. package/dist/packem_chunks/handler3.mjs +2 -208
  18. package/dist/packem_chunks/handler4.mjs +1 -33
  19. package/dist/packem_chunks/handler5.mjs +1 -52
  20. package/dist/packem_chunks/handler6.mjs +1 -94
  21. package/dist/packem_chunks/handler7.mjs +1 -42
  22. package/dist/packem_chunks/handler8.mjs +1 -174
  23. package/dist/packem_chunks/handler9.mjs +8 -334
  24. package/dist/packem_chunks/planDevCommand.mjs +7 -1181
  25. package/dist/packem_chunks/runCodegenCommand.mjs +4 -53
  26. package/dist/packem_chunks/runDeployCommand.mjs +1 -619
  27. package/dist/packem_chunks/runInitCommand.mjs +51 -1411
  28. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +9 -442
  29. package/dist/packem_chunks/runResetCommand.mjs +1 -41
  30. package/dist/packem_chunks/runRpcCommand.mjs +1 -69
  31. package/dist/packem_shared/COMMANDS-DRqlsGT2.mjs +3 -0
  32. package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-DL87PIEl.mjs +8 -0
  33. package/dist/packem_shared/admin-url-DfVehV3e.mjs +1 -0
  34. package/dist/packem_shared/api-spec-BENwiyUa.mjs +1 -0
  35. package/dist/packem_shared/buildRegistryIndex-DwySASBu.mjs +1 -0
  36. package/dist/packem_shared/codegen-error-DJN6pTH5.mjs +3 -0
  37. package/dist/packem_shared/command-Bjv4VmYA.mjs +1 -0
  38. package/dist/packem_shared/commands-BhX2Bo9B.mjs +19 -0
  39. package/dist/packem_shared/createLogger--C3x1FNE.mjs +2 -0
  40. package/dist/packem_shared/createRecordingSpawner-C8_7nffs.mjs +1 -0
  41. package/dist/packem_shared/detect-package-manager-B2eIuAiP.mjs +1 -0
  42. package/dist/packem_shared/diffSnapshots-9bDd9nOK.mjs +5 -0
  43. package/dist/packem_shared/docker-CKvYE9YF.mjs +1 -0
  44. package/dist/packem_shared/insertSchemaExtension-DZReBZ4_.mjs +8 -0
  45. package/dist/packem_shared/open-url-CTU_g9gr.mjs +1 -0
  46. package/dist/packem_shared/output-format-D7cKB-O5.mjs +2 -0
  47. package/dist/packem_shared/parseManifest-x3WsxKHz.mjs +1 -0
  48. package/dist/packem_shared/prompt-cancelled-C8Sj7OqI.mjs +1 -0
  49. package/dist/packem_shared/resolve-target-vxaXHmvg.mjs +1 -0
  50. package/dist/packem_shared/runAddCommand--TBYJQYi.mjs +1 -0
  51. package/dist/packem_shared/schema-drift-gate-CpqG-xP4.mjs +1 -0
  52. package/dist/packem_shared/schemaIrToSnapshot-Clwd2A8e.mjs +1 -0
  53. package/dist/packem_shared/storage-DOvAKGi4.mjs +1 -0
  54. package/dist/packem_shared/tui-prompts-CCt3jm1k.mjs +4 -0
  55. package/dist/packem_shared/wrangler-name-HIl0Z6yx.mjs +1 -0
  56. package/dist/packem_shared/wrangler-secrets-BMRJwKQU.mjs +1 -0
  57. package/package.json +6 -6
  58. package/dist/packem_shared/COMMANDS-BOJIDoVY.mjs +0 -1031
  59. package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-D0VOTerB.mjs +0 -249
  60. package/dist/packem_shared/admin-url-4UzT-CI4.mjs +0 -19
  61. package/dist/packem_shared/api-spec-Bx0iKbxA.mjs +0 -15
  62. package/dist/packem_shared/buildRegistryIndex-BS5ig822.mjs +0 -38
  63. package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +0 -31
  64. package/dist/packem_shared/command-lYnl4QyF.mjs +0 -19
  65. package/dist/packem_shared/commands-D5Yxt9VY.mjs +0 -991
  66. package/dist/packem_shared/createLogger-CIWSHrTL.mjs +0 -105
  67. package/dist/packem_shared/createRecordingSpawner-WuSn20kb.mjs +0 -64
  68. package/dist/packem_shared/detect-package-manager-v4hHpQd0.mjs +0 -62
  69. package/dist/packem_shared/diffSnapshots-BeDvvNiF.mjs +0 -161
  70. package/dist/packem_shared/docker-hMQ97KSQ.mjs +0 -21
  71. package/dist/packem_shared/insertSchemaExtension-DAqbfr9Z.mjs +0 -64
  72. package/dist/packem_shared/open-url-4PBLY9X0.mjs +0 -42
  73. package/dist/packem_shared/output-format-B4642rjE.mjs +0 -17
  74. package/dist/packem_shared/parseManifest-Dbp-Q2q3.mjs +0 -121
  75. package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +0 -9
  76. package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +0 -16
  77. package/dist/packem_shared/runAddCommand-D1hgfqFQ.mjs +0 -4
  78. package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +0 -79
  79. package/dist/packem_shared/schemaIrToSnapshot-DdsljJT-.mjs +0 -43
  80. package/dist/packem_shared/storage-BXU4ax4O.mjs +0 -84
  81. package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +0 -658
  82. package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +0 -12
  83. package/dist/packem_shared/wrangler-secrets-Coni-mER.mjs +0 -49
@@ -1,1181 +1,7 @@
1
- import { spawnSync, spawn } from 'node:child_process';
2
- import { existsSync, watch, readFileSync, statSync, openSync, readSync, closeSync, mkdirSync } from 'node:fs';
3
- import { join, dirname } from 'node:path';
4
- import { runCodegen } from '@lunora/codegen';
5
- import { detectAgentRules, claimDevServerState, clearDevServerState, readDevServerState, readLiveDevServerState, detectFramework, readProjectDependencyNames, DEV_HANDOFF_ENV, isRecordedProcessCurrent, DEV_LOG_FILE, DEV_LOG_FILE_ENV, DEV_DAEMON_ENV, updateDevServerState, detectAiAgent, resolveRemoteEnabled, readProjectRemotePreference, inferLunoraBindings, packageNamesFromBindings, ensureDevVarsExample, ensureDevVariables, isInteractive, DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, fillDevSecrets, claimAgentRulesHint, AGENT_RULES_HINT, findWranglerFile, readWranglerJsonc, discoverContainerInfo, streamContainerLogs, materializeRemoteWranglerConfig, formatLunoraEvent } from '@lunora/config';
6
- import { p as parseApiSpec } from '../packem_shared/api-spec-Bx0iKbxA.mjs';
7
- import { r as renderCodegenFailure } from '../packem_shared/codegen-error-DJG-ghs_.mjs';
8
- import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs';
9
- import { d as detectPackageManager, e as execArgsFor, r as runScriptCommand } from '../packem_shared/detect-package-manager-v4hHpQd0.mjs';
10
- import { createServer, connect } from 'node:net';
11
- import { forceJsonLogging } from '../packem_shared/createLogger-CIWSHrTL.mjs';
12
- import { networkInterfaces } from 'node:os';
13
- import { spawnShellCompat } from '../packem_shared/createRecordingSpawner-WuSn20kb.mjs';
14
- import { createServer as createServer$1, request } from 'node:http';
15
- import { loadStudioAssets, studioAssetsStamp, renderStudioHtml, resolveAdminToken, SCHEMA_EDIT_ENDPOINT, POLICY_SCAFFOLD_ENDPOINT, SEED_ENDPOINT, serveJsonHandler, isStandaloneModulePath, readStandaloneAsset, assetContentType, handleSchemaEditRequest, handlePolicyScaffoldRequest, handleSeedRequest } from '@lunora/config/studio-host';
16
- import { c as createTuiConfirm } from '../packem_shared/tui-prompts-BjEN8XgP.mjs';
17
- import { p as printJson } from '../packem_shared/output-format-B4642rjE.mjs';
18
-
19
- const DEFAULT_DEBOUNCE_MS = 100;
20
- const PATH_SEGMENT_SEPARATOR = /[/\\]/u;
21
- const runOnce = (projectRoot, lunoraDirectory, apiSpec, logger, reason) => {
22
- try {
23
- runCodegen({ apiSpec, lunoraDirectory, projectRoot });
24
- logger.success(`codegen: wrote ${lunoraDirectory}/_generated (${reason})`);
25
- } catch (error) {
26
- logger.error(renderCodegenFailure(error, reason));
27
- }
28
- };
29
- const startCodegenWatch = (options) => {
30
- const lunoraDirectory = options.lunoraDirectory ?? "lunora";
31
- const watchDirectory = join(options.projectRoot, lunoraDirectory);
32
- const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
33
- const { apiSpec } = options;
34
- runOnce(options.projectRoot, lunoraDirectory, apiSpec, options.logger, "startup");
35
- let timer;
36
- let watcher;
37
- if (!existsSync(watchDirectory)) {
38
- options.logger.warn(
39
- `codegen watch unavailable (no such directory: ${watchDirectory}) — schema edits will NOT auto-regenerate. Run \`lunora codegen\` manually after each edit.`
40
- );
41
- return {
42
- close: () => {
43
- },
44
- watchAvailable: false
45
- };
46
- }
47
- try {
48
- watcher = watch(watchDirectory, { recursive: true }, (_event, filename) => {
49
- if (typeof filename === "string" && filename.split(PATH_SEGMENT_SEPARATOR).includes("_generated")) {
50
- return;
51
- }
52
- if (timer) {
53
- clearTimeout(timer);
54
- }
55
- timer = setTimeout(runOnce, debounceMs, options.projectRoot, lunoraDirectory, apiSpec, options.logger, `change: ${filename ?? "?"}`);
56
- });
57
- } catch (error) {
58
- options.logger.warn(
59
- `codegen watch unavailable (${error instanceof Error ? error.message : String(error)}) — schema edits will NOT auto-regenerate. Run \`lunora codegen\` manually after each edit.`
60
- );
61
- return {
62
- close: () => {
63
- },
64
- watchAvailable: false
65
- };
66
- }
67
- const liveWatcher = watcher;
68
- return {
69
- close: () => {
70
- if (timer) {
71
- clearTimeout(timer);
72
- }
73
- liveWatcher.close();
74
- },
75
- watchAvailable: true
76
- };
77
- };
78
-
79
- const DEFAULT_PROBE_ATTEMPTS = 64;
80
- const LOOPBACK_HOST = "127.0.0.1";
81
- const MAX_PORT = 65535;
82
- const isPortFree = (port, host = LOOPBACK_HOST) => new Promise((resolve) => {
83
- const server = createServer();
84
- server.once("error", () => {
85
- resolve(false);
86
- });
87
- server.once("listening", () => {
88
- server.close(() => {
89
- resolve(true);
90
- });
91
- });
92
- server.listen(port, host);
93
- });
94
- const findAvailablePort = async (preferred, host = LOOPBACK_HOST, attempts = DEFAULT_PROBE_ATTEMPTS) => {
95
- for (let port = preferred; port < preferred + attempts && port <= MAX_PORT; port += 1) {
96
- if (await isPortFree(port, host)) {
97
- return port;
98
- }
99
- }
100
- return preferred;
101
- };
102
-
103
- const hasIpv6Loopback = (readInterfaces = networkInterfaces) => Object.values(readInterfaces()).some((addresses) => addresses?.some((address) => address.internal && address.address === "::1"));
104
-
105
- const PROXY_PREFIX = "/_lunora";
106
- const pathnameOf = (url) => {
107
- const queryIndex = url.indexOf("?");
108
- return queryIndex === -1 ? url : url.slice(0, queryIndex);
109
- };
110
- const LOOPBACK_HOST_NAMES = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "[::1]", "localhost"]);
111
- const isLoopbackHost = (hostHeader) => {
112
- if (hostHeader === void 0 || hostHeader === "") {
113
- return false;
114
- }
115
- const withoutPort = hostHeader.startsWith("[") ? hostHeader.slice(0, hostHeader.indexOf("]") + 1) : hostHeader.split(":")[0] ?? hostHeader;
116
- return LOOPBACK_HOST_NAMES.has(withoutPort);
117
- };
118
- const proxyHttp = (request$1, response, worker) => {
119
- const upstream = request(
120
- {
121
- headers: { ...request$1.headers, host: worker.host },
122
- hostname: worker.hostname,
123
- method: request$1.method,
124
- path: request$1.url,
125
- port: worker.port
126
- },
127
- (upstreamResponse) => {
128
- response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.headers);
129
- upstreamResponse.pipe(response);
130
- }
131
- );
132
- upstream.on("error", (error) => {
133
- response.statusCode = 502;
134
- response.end(`lunora dev: worker unreachable (${error.message})`);
135
- });
136
- request$1.pipe(upstream);
137
- };
138
- const UPGRADE_CONNECT_TIMEOUT_MS = 5e3;
139
- const proxyUpgrade = (request, clientSocket, head, worker) => {
140
- const upstream = connect({ host: worker.hostname, port: Number(worker.port) }, () => {
141
- upstream.setTimeout(0);
142
- const lines = [`${request.method ?? "GET"} ${request.url ?? "/"} HTTP/1.1`];
143
- for (const [key, value] of Object.entries(request.headers)) {
144
- if (key === "host") {
145
- continue;
146
- }
147
- for (const item of Array.isArray(value) ? value : [value]) {
148
- if (item !== void 0) {
149
- lines.push(`${key}: ${item}`);
150
- }
151
- }
152
- }
153
- lines.push(`host: ${worker.host}`, "", "");
154
- upstream.write(lines.join("\r\n"));
155
- if (head.length > 0) {
156
- upstream.write(head);
157
- }
158
- upstream.pipe(clientSocket);
159
- clientSocket.pipe(upstream);
160
- });
161
- upstream.setTimeout(UPGRADE_CONNECT_TIMEOUT_MS, () => upstream.destroy());
162
- upstream.on("error", () => clientSocket.destroy());
163
- clientSocket.on("error", () => upstream.destroy());
164
- };
165
- const startStudioServer = async (options) => {
166
- const host = options.host ?? "127.0.0.1";
167
- const isLoopback = host === "127.0.0.1" || host === "localhost" || host === "::1";
168
- const worker = new URL(options.workerOrigin);
169
- let assets = loadStudioAssets(options.logger, import.meta.url);
170
- let assetsStamp = studioAssetsStamp(import.meta.url);
171
- const html = renderStudioHtml({
172
- // The admin token grants full read/write over the local worker. Only embed
173
- // it for a loopback bind: a non-loopback (LAN/0.0.0.0) bind would otherwise
174
- // hand the token to any network client that GETs `/`.
175
- adminToken: isLoopback ? resolveAdminToken(options.cwd) : void 0,
176
- basePath: "/",
177
- dataEditable: isLoopback,
178
- rulesInstalled: detectAgentRules(options.cwd).installed,
179
- runAsIdentity: isLoopback,
180
- schemaEditable: isLoopback,
181
- scriptSrc: "/studio.js",
182
- styleHref: "/styles.css"
183
- });
184
- const sendAsset = (response, body, contentType) => {
185
- response.statusCode = 200;
186
- response.setHeader("Content-Type", contentType);
187
- response.end(body);
188
- };
189
- const serveLoopbackOnly = (request, response, handle, deniedMessage) => {
190
- if (isLoopback) {
191
- serveJsonHandler(request, response, handle, options.cwd);
192
- return;
193
- }
194
- response.statusCode = 403;
195
- response.setHeader("Content-Type", "text/plain");
196
- response.end(deniedMessage);
197
- };
198
- const serveStaticAsset = (pathname, response) => {
199
- const isStyle = pathname === "/styles.css";
200
- const isModule = isStandaloneModulePath(pathname);
201
- if (!isStyle && !isModule) {
202
- return false;
203
- }
204
- const stamp = studioAssetsStamp(import.meta.url);
205
- if (stamp !== assetsStamp) {
206
- assets = loadStudioAssets(options.logger, import.meta.url);
207
- assetsStamp = stamp;
208
- }
209
- if (assets === void 0) {
210
- response.statusCode = 501;
211
- response.setHeader("Content-Type", "text/plain");
212
- response.end("Lunora studio assets not found — install and build @lunora/studio.");
213
- return true;
214
- }
215
- if (isStyle) {
216
- sendAsset(response, assets.styles, "text/css; charset=utf-8");
217
- return true;
218
- }
219
- const fileName = pathname.slice(pathname.lastIndexOf("/") + 1);
220
- const bytes = readStandaloneAsset(fileName, import.meta.url);
221
- if (bytes === void 0) {
222
- response.statusCode = 404;
223
- response.setHeader("Content-Type", "text/plain");
224
- response.end("Not found");
225
- return true;
226
- }
227
- sendAsset(response, bytes, assetContentType(fileName));
228
- return true;
229
- };
230
- const document = Buffer.from(html);
231
- const server = createServer$1((request, response) => {
232
- const pathname = pathnameOf(request.url ?? "/");
233
- if (isLoopback && !isLoopbackHost(request.headers.host)) {
234
- response.statusCode = 403;
235
- response.setHeader("Content-Type", "text/plain");
236
- response.end("Lunora studio: refusing a request whose Host is not a loopback literal (DNS-rebinding guard).");
237
- return;
238
- }
239
- if (pathname.startsWith(PROXY_PREFIX)) {
240
- if (!isLoopback) {
241
- response.statusCode = 403;
242
- response.setHeader("Content-Type", "text/plain");
243
- response.end("Lunora studio: the worker admin proxy is only available on a loopback bind.");
244
- return;
245
- }
246
- proxyHttp(request, response, worker);
247
- return;
248
- }
249
- if (pathname === SCHEMA_EDIT_ENDPOINT) {
250
- serveLoopbackOnly(request, response, handleSchemaEditRequest, "Lunora schema editing is only available on loopback hosts in dev.");
251
- return;
252
- }
253
- if (pathname === POLICY_SCAFFOLD_ENDPOINT) {
254
- serveLoopbackOnly(request, response, handlePolicyScaffoldRequest, "Lunora policy scaffolding is only available on loopback hosts in dev.");
255
- return;
256
- }
257
- if (pathname === SEED_ENDPOINT) {
258
- serveLoopbackOnly(request, response, handleSeedRequest, "Lunora data seeding is only available on loopback hosts in dev.");
259
- return;
260
- }
261
- if (serveStaticAsset(pathname, response)) {
262
- return;
263
- }
264
- sendAsset(response, document, "text/html; charset=utf-8");
265
- });
266
- server.on("upgrade", (request, socket, head) => {
267
- if (!isLoopback || !isLoopbackHost(request.headers.host)) {
268
- socket.destroy();
269
- return;
270
- }
271
- if (pathnameOf(request.url ?? "").startsWith(PROXY_PREFIX)) {
272
- proxyUpgrade(request, socket, head, worker);
273
- } else {
274
- socket.destroy();
275
- }
276
- });
277
- await new Promise((resolve, reject) => {
278
- server.once("error", (error) => {
279
- if (error.code === "EADDRINUSE") {
280
- reject(
281
- new Error(`studio port ${String(options.port)} is already in use — pass a different port with --port, or stop the process using it`, {
282
- cause: error
283
- })
284
- );
285
- } else {
286
- reject(error);
287
- }
288
- });
289
- server.listen(options.port, host, () => {
290
- resolve();
291
- });
292
- });
293
- return {
294
- close: () => new Promise((resolve) => {
295
- server.close(() => {
296
- resolve();
297
- });
298
- }),
299
- url: `http://${host === "0.0.0.0" || host === "::" ? "localhost" : host}:${String(options.port)}`
300
- };
301
- };
302
-
303
- const DEFAULT_READY_TIMEOUT_MS = 12e4;
304
- const POLL_INTERVAL_MS = 250;
305
- const STOP_GRACE_MS = 1e4;
306
- const DEFAULT_LOG_LINES = 100;
307
- const FAILURE_LOG_TAIL_LINES = 40;
308
- const LOG_TAIL_MAX_BYTES = 256 * 1024;
309
- const READY_TIMEOUT_ENV = "LUNORA_DEV_READY_TIMEOUT_MS";
310
- const SIDECAR_FRAMEWORKS = /* @__PURE__ */ new Set(["nuxt", "sveltekit"]);
311
- const detectDevFlavor = (cwd) => {
312
- if (SIDECAR_FRAMEWORKS.has(detectFramework(cwd).framework)) {
313
- return "framework-worker";
314
- }
315
- return readProjectDependencyNames(cwd).has("@lunora/vite") ? "vite" : "wrangler";
316
- };
317
- const viteDevCommand = (cwd) => {
318
- const manager = detectPackageManager(cwd);
319
- let script;
320
- try {
321
- const raw = readFileSync(join(cwd, "package.json"), "utf8");
322
- script = JSON.parse(raw).scripts?.dev;
323
- } catch {
324
- }
325
- if (script === void 0 || script.trim() === "" || script.includes("lunora")) {
326
- const exec = execArgsFor(manager, "vite", ["dev"]);
327
- return { args: exec.args, command: exec.command };
328
- }
329
- return { args: ["run", "dev"], command: manager };
330
- };
331
- const sleep = (ms) => new Promise((resolve) => {
332
- setTimeout(resolve, ms);
333
- });
334
- const defaultProbe = async (origin) => {
335
- try {
336
- await fetch(new URL("/_lunora/status", origin), { signal: AbortSignal.timeout(1e3) });
337
- return true;
338
- } catch {
339
- return false;
340
- }
341
- };
342
- const defaultDetachedSpawner = (descriptor) => {
343
- mkdirSync(dirname(descriptor.logPath), { recursive: true });
344
- const logFd = openSync(descriptor.logPath, "w", 384);
345
- let child;
346
- const exec = spawnShellCompat(descriptor.command, descriptor.args);
347
- try {
348
- child = spawn(exec.command, exec.args, {
349
- cwd: descriptor.cwd,
350
- detached: true,
351
- env: descriptor.env,
352
- shell: exec.shell,
353
- stdio: ["ignore", logFd, logFd],
354
- windowsHide: true
355
- });
356
- } finally {
357
- closeSync(logFd);
358
- }
359
- child.unref();
360
- return {
361
- exited: new Promise((resolve) => {
362
- child.on("error", () => {
363
- resolve(1);
364
- });
365
- child.on("exit", (code, signal) => {
366
- resolve(code ?? (signal ? 1 : 0));
367
- });
368
- }),
369
- pid: child.pid
370
- };
371
- };
372
- const readLogWindow = (path) => {
373
- try {
374
- const { size } = statSync(path);
375
- if (size <= LOG_TAIL_MAX_BYTES) {
376
- return readFileSync(path, "utf8");
377
- }
378
- const fd = openSync(path, "r");
379
- try {
380
- const buffer = Buffer.alloc(LOG_TAIL_MAX_BYTES);
381
- const read = readSync(fd, buffer, 0, LOG_TAIL_MAX_BYTES, size - LOG_TAIL_MAX_BYTES);
382
- const text = buffer.toString("utf8", 0, read);
383
- return text.slice(text.indexOf("\n") + 1);
384
- } finally {
385
- closeSync(fd);
386
- }
387
- } catch {
388
- return void 0;
389
- }
390
- };
391
- const readLogTail = (path, count) => {
392
- const text = readLogWindow(path);
393
- if (text === void 0) {
394
- return [];
395
- }
396
- const lines = text.split("\n");
397
- if (lines.at(-1) === "") {
398
- lines.pop();
399
- }
400
- return count > 0 ? lines.slice(-count) : lines;
401
- };
402
- const resolveLogPath = (cwd, state) => state?.logFile ?? join(cwd, DEV_LOG_FILE);
403
- const printLifecycleHints = (logger) => {
404
- logger.info(" Stop: lunora dev stop");
405
- logger.info(" Status: lunora dev status");
406
- logger.info(" Logs: lunora dev logs");
407
- };
408
- const reportExistingServer = (logger, existing) => {
409
- logger.warn(`Dev server already running at ${existing.url} (pid ${String(existing.pid)})`);
410
- printLifecycleHints(logger);
411
- };
412
- const printBackgroundBanner = (logger, state) => {
413
- logger.success(`Dev server running at ${state.url} (pid ${String(state.pid)})`);
414
- if (state.studioUrl !== void 0) {
415
- logger.info(` Studio: ${state.studioUrl}`);
416
- }
417
- printLifecycleHints(logger);
418
- };
419
- const waitUntilReady = async (parameters) => {
420
- const { child, cwd, deadline, pollIntervalMs, probe } = parameters;
421
- const exited = child.exited.then((code) => {
422
- return { exitCode: code };
423
- });
424
- while (Date.now() < deadline) {
425
- const state = readLiveDevServerState(cwd);
426
- if (state !== void 0 && state.pid !== process.pid && await probe(state.url)) {
427
- return { state, status: "ready" };
428
- }
429
- const raced = await Promise.race([exited, sleep(pollIntervalMs)]);
430
- if (raced !== void 0) {
431
- return { ...raced, status: "exited" };
432
- }
433
- }
434
- return { status: "timeout" };
435
- };
436
- const runDevBackground = async (options) => {
437
- const { cwd, logger } = options;
438
- const logPath = resolveLogPath(cwd, void 0);
439
- const environmentTimeout = Number(process.env[READY_TIMEOUT_ENV] ?? "");
440
- const timeout = options.readyTimeoutMs ?? (Number.isFinite(environmentTimeout) && environmentTimeout > 0 ? environmentTimeout : DEFAULT_READY_TIMEOUT_MS);
441
- const child = (options.spawnDetached ?? defaultDetachedSpawner)({
442
- args: options.command.args,
443
- command: options.command.command,
444
- cwd,
445
- env: {
446
- ...process.env,
447
- ...options.env,
448
- [DEV_DAEMON_ENV]: "1",
449
- [DEV_LOG_FILE_ENV]: logPath,
450
- // The capture log is read back by `lunora dev logs` (and often an
451
- // agent) — ANSI colour codes would only garble it.
452
- ...process.env.FORCE_COLOR === void 0 ? { NO_COLOR: "1" } : {},
453
- ...options.json ? { LUNORA_LOG_JSON: "1" } : {}
454
- },
455
- logPath
456
- });
457
- const outcome = await waitUntilReady({
458
- child,
459
- cwd,
460
- deadline: Date.now() + timeout,
461
- pollIntervalMs: options.pollIntervalMs ?? POLL_INTERVAL_MS,
462
- probe: options.probe ?? defaultProbe
463
- });
464
- if (outcome.status === "ready") {
465
- printBackgroundBanner(logger, outcome.state);
466
- return { code: 0 };
467
- }
468
- if (outcome.status === "exited") {
469
- logger.error(`dev server exited before becoming ready (exit code ${String(outcome.exitCode)}). Last output:`);
470
- for (const line of readLogTail(logPath, FAILURE_LOG_TAIL_LINES)) {
471
- logger.error(` ${line}`);
472
- }
473
- clearDevServerState(cwd, child.pid ?? process.pid);
474
- return { code: outcome.exitCode === 0 ? 1 : outcome.exitCode };
475
- }
476
- if (child.pid !== void 0) {
477
- const current = readDevServerState(cwd);
478
- if (current?.pid === process.pid) {
479
- updateDevServerState(cwd, { logFile: logPath, pid: child.pid });
480
- }
481
- }
482
- const pidHint = child.pid === void 0 ? "" : ` (pid ${String(child.pid)})`;
483
- logger.warn(
484
- `dev server did not confirm ready within ${String(Math.round(timeout / 1e3))}s — it may still be compiling${pidHint}. Check \`lunora dev status\` and \`lunora dev logs\`; \`lunora dev stop\` shuts it down.`
485
- );
486
- return { code: 1 };
487
- };
488
- const daemonArguments = (options, remote) => {
489
- const args = ["dev"];
490
- if (options.apiSpec !== void 0) {
491
- args.push("--api-spec", options.apiSpec);
492
- }
493
- if (options.port !== void 0) {
494
- args.push("--port", String(options.port));
495
- }
496
- if (options.workerPort !== void 0) {
497
- args.push("--worker-port", String(options.workerPort));
498
- }
499
- if (options.codegen === false) {
500
- args.push("--no-codegen");
501
- }
502
- if (options.studio === false) {
503
- args.push("--no-studio");
504
- }
505
- if (remote) {
506
- args.push("--remote");
507
- }
508
- return args;
509
- };
510
- const startBackground = async (context) => {
511
- const { cwd, jsonLogs, logger, options, remote } = context;
512
- const run = context.run ?? runDevBackground;
513
- const flavor = detectDevFlavor(cwd);
514
- const url = flavor === "wrangler" ? `http://localhost:${String(options.workerPort ?? 8787)}` : "http://localhost:5173";
515
- const claim = claimDevServerState(cwd, {
516
- background: true,
517
- mode: "cli",
518
- pid: process.pid,
519
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
520
- url
521
- });
522
- if (!claim.ok) {
523
- if (claim.existing !== void 0) {
524
- reportExistingServer(logger, claim.existing);
525
- }
526
- return { code: 0 };
527
- }
528
- const handoff = { [DEV_HANDOFF_ENV]: String(process.pid) };
529
- try {
530
- if (flavor === "vite") {
531
- return await run({
532
- command: viteDevCommand(cwd),
533
- cwd,
534
- env: { ...remote ? { LUNORA_REMOTE: "1" } : {}, ...handoff },
535
- json: jsonLogs,
536
- logger
537
- });
538
- }
539
- return await run({
540
- command: { args: [process.argv[1] ?? "lunora", ...daemonArguments(options, remote)], command: process.execPath },
541
- cwd,
542
- env: handoff,
543
- json: jsonLogs,
544
- logger
545
- });
546
- } finally {
547
- clearDevServerState(cwd, process.pid);
548
- }
549
- };
550
- const processGroupId = (pid) => {
551
- try {
552
- const result = spawnSync("ps", ["-o", "pgid=", "-p", String(pid)], { encoding: "utf8" });
553
- const pgid = Number.parseInt(result.stdout.trim(), 10);
554
- return Number.isInteger(pgid) && pgid > 0 ? pgid : void 0;
555
- } catch {
556
- return void 0;
557
- }
558
- };
559
- const forceKillRecordedServer = (state, signal, platform = process.platform, spawnSyncImpl = spawnSync) => {
560
- if (platform === "win32") {
561
- try {
562
- spawnSyncImpl("taskkill", ["/pid", String(state.pid), "/T", "/F"], { stdio: "ignore" });
563
- } catch {
564
- }
565
- return;
566
- }
567
- if (state.background === true) {
568
- try {
569
- signal(-(processGroupId(state.pid) ?? state.pid), "SIGKILL");
570
- return;
571
- } catch {
572
- }
573
- }
574
- try {
575
- signal(state.pid, "SIGKILL");
576
- } catch {
577
- }
578
- };
579
- const reportSurvivedForceKill = (state, options) => {
580
- if (options.json) {
581
- printJson({ pid: state.pid, stopped: false });
582
- } else {
583
- options.logger.error(
584
- `dev server (pid ${String(state.pid)}) survived the force-kill — record kept; retry \`lunora dev stop\` or kill the process manually.`
585
- );
586
- }
587
- return { code: 1 };
588
- };
589
- const runDevStop = async (options) => {
590
- const { cwd, logger } = options;
591
- const state = readDevServerState(cwd);
592
- const alive = options.alive ?? ((pid) => pid === state?.pid && isRecordedProcessCurrent(state));
593
- const signal = options.signal ?? ((pid, sig) => {
594
- process.kill(pid, sig);
595
- });
596
- const pollInterval = options.pollIntervalMs ?? POLL_INTERVAL_MS;
597
- const grace = options.stopGraceMs ?? STOP_GRACE_MS;
598
- const platform = options.platform ?? process.platform;
599
- const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync;
600
- if (state === void 0 || !alive(state.pid)) {
601
- if (state !== void 0) {
602
- clearDevServerState(cwd, state.pid);
603
- }
604
- if (options.json) {
605
- printJson({ running: false, stopped: false });
606
- } else {
607
- logger.info("No dev server running.");
608
- }
609
- return { code: 0 };
610
- }
611
- try {
612
- signal(state.pid, "SIGTERM");
613
- } catch {
614
- }
615
- const deadline = Date.now() + grace;
616
- while (alive(state.pid) && Date.now() < deadline) {
617
- await sleep(pollInterval);
618
- }
619
- if (alive(state.pid)) {
620
- forceKillRecordedServer(state, signal, platform, spawnSyncImpl);
621
- await sleep(pollInterval);
622
- if (alive(state.pid)) {
623
- return reportSurvivedForceKill(state, options);
624
- }
625
- }
626
- clearDevServerState(cwd, state.pid);
627
- if (options.json) {
628
- printJson({ pid: state.pid, stopped: true });
629
- } else {
630
- logger.success(`Stopped dev server (pid ${String(state.pid)}).`);
631
- }
632
- return { code: 0 };
633
- };
634
- const runDevStatus = (options) => {
635
- const { cwd, logger } = options;
636
- const state = readLiveDevServerState(cwd);
637
- if (state === void 0) {
638
- if (options.json) {
639
- printJson({ running: false });
640
- } else {
641
- logger.info("No dev server running.");
642
- }
643
- return { code: 0 };
644
- }
645
- const now = options.now ?? Date.now;
646
- const startedAtMs = state.startedAt === void 0 ? Number.NaN : Date.parse(state.startedAt);
647
- const uptimeSeconds = Number.isFinite(startedAtMs) ? Math.max(0, Math.round((now() - startedAtMs) / 1e3)) : void 0;
648
- if (options.json) {
649
- printJson({
650
- background: state.background === true,
651
- logFile: state.logFile,
652
- mode: state.mode,
653
- pid: state.pid,
654
- running: true,
655
- startedAt: state.startedAt,
656
- studioUrl: state.studioUrl,
657
- uptimeSeconds,
658
- url: state.url
659
- });
660
- return { code: 0 };
661
- }
662
- const details = [
663
- `pid ${String(state.pid)}`,
664
- ...uptimeSeconds === void 0 ? [] : [`uptime ${String(uptimeSeconds)}s`],
665
- state.background === true ? "background" : "foreground"
666
- ];
667
- logger.success(`Dev server running at ${state.url} (${details.join(", ")})`);
668
- if (state.studioUrl !== void 0) {
669
- logger.info(` Studio: ${state.studioUrl}`);
670
- }
671
- if (state.logFile !== void 0) {
672
- logger.info(` Logs: lunora dev logs (${state.logFile})`);
673
- }
674
- return { code: 0 };
675
- };
676
- const runDevLogs = (options) => {
677
- const { cwd, logger } = options;
678
- const state = readDevServerState(cwd);
679
- const logPath = resolveLogPath(cwd, state);
680
- if (!existsSync(logPath)) {
681
- logger.info("No dev server logs found — output is captured when the server runs in background mode (`lunora dev --background`).");
682
- return { code: 0 };
683
- }
684
- const tail = readLogTail(logPath, options.lines ?? DEFAULT_LOG_LINES);
685
- if (tail.length > 0) {
686
- process.stdout.write(`${tail.join("\n")}
687
- `);
688
- }
689
- return { code: 0 };
690
- };
691
- const runLifecycleSubcommand = (parameters) => {
692
- const { cwd, json, lines, logger, subcommand } = parameters;
693
- if (subcommand === "stop") {
694
- return runDevStop({ cwd, json, logger });
695
- }
696
- if (subcommand === "status") {
697
- return runDevStatus({ cwd, json, logger });
698
- }
699
- if (subcommand === "logs") {
700
- return runDevLogs({ cwd, lines, logger });
701
- }
702
- if (subcommand !== void 0) {
703
- logger.error(`dev: unknown subcommand "${subcommand}" — expected stop | status | logs (or no subcommand to start the dev server)`);
704
- return { code: 1 };
705
- }
706
- return void 0;
707
- };
708
-
709
- const DEV_WRANGLER_CONFIG = "wrangler.dev.jsonc";
710
- const DEFAULT_STUDIO_PORT = 6173;
711
- const DEFAULT_WORKER_PORT = 8787;
712
- const DEFAULT_VITE_PORT = 5173;
713
- const SIGINT_GRACE_MS = 5e3;
714
- const resolveRemotePlan = (options, cwd) => {
715
- const noopCleanup = () => {
716
- };
717
- if (!options.remote) {
718
- return { args: [], plan: { bindings: [], cleanup: noopCleanup, enabled: false } };
719
- }
720
- const materialize = options.materializeRemote ?? materializeRemoteWranglerConfig;
721
- const result = materialize({ enabled: true, projectRoot: cwd });
722
- const bindings = result.remoteBindings.map((binding) => `${binding.binding} (${binding.kind})`);
723
- const { cleanup } = result;
724
- if (result.configPath === void 0) {
725
- return { args: [], plan: { bindings, cleanup, enabled: true, reason: result.reason } };
726
- }
727
- return { args: ["--config", result.configPath], plan: { bindings, cleanup, enabled: true } };
728
- };
729
- const readDevIp = (wranglerPath) => readWranglerJsonc(wranglerPath).parsed?.dev?.ip;
730
- const resolveLoopbackArgs = (cwd, hasLoopback, sidecarConfigFile) => {
731
- if (sidecarConfigFile !== void 0) {
732
- const sidecarConfigPath = join(cwd, sidecarConfigFile);
733
- if (existsSync(sidecarConfigPath) && readDevIp(sidecarConfigPath) !== void 0) {
734
- return [];
735
- }
736
- }
737
- const wranglerPath = findWranglerFile(cwd);
738
- if (wranglerPath !== void 0 && readDevIp(wranglerPath) !== void 0) {
739
- return [];
740
- }
741
- return hasLoopback() ? [] : ["--ip", "127.0.0.1"];
742
- };
743
- const resolveWorkerPort = async (options, cwd) => {
744
- if (options.workerPort !== void 0) {
745
- return options.workerPort;
746
- }
747
- const wranglerPath = findWranglerFile(cwd);
748
- if (wranglerPath !== void 0) {
749
- const { parsed } = readWranglerJsonc(wranglerPath);
750
- if (typeof parsed?.dev?.port === "number") {
751
- return parsed.dev.port;
752
- }
753
- }
754
- return (options.findFreePort ?? findAvailablePort)(DEFAULT_WORKER_PORT);
755
- };
756
- const planDevCommand = (options) => {
757
- const cwd = options.cwd ?? process.cwd();
758
- const manager = detectPackageManager(cwd);
759
- const flavor = options.flavor ?? detectDevFlavor(cwd);
760
- if (flavor === "vite" || flavor === "framework-worker") {
761
- const exec2 = viteDevCommand(cwd);
762
- let sidecar;
763
- if (flavor === "framework-worker") {
764
- const loopbackArgs2 = resolveLoopbackArgs(cwd, options.hasIpv6Loopback ?? hasIpv6Loopback, DEV_WRANGLER_CONFIG);
765
- const sidecarExec = execArgsFor(manager, "wrangler", ["dev", "--config", DEV_WRANGLER_CONFIG, ...loopbackArgs2, "--var", "WORKER_ENV:development"]);
766
- sidecar = { args: sidecarExec.args, command: sidecarExec.command, cwd, tag: "worker" };
767
- }
768
- return {
769
- codegenEnabled: false,
770
- flavor,
771
- ipv4LoopbackForced: false,
772
- remote: { bindings: [], cleanup: () => {
773
- }, enabled: options.remote === true },
774
- ...sidecar ? { sidecar } : {},
775
- studioEnabled: false,
776
- studioPort: options.port ?? DEFAULT_STUDIO_PORT,
777
- workerOrigin: `http://localhost:${String(DEFAULT_VITE_PORT)}`,
778
- workerPort: DEFAULT_VITE_PORT,
779
- wrangler: {
780
- args: exec2.args,
781
- command: exec2.command,
782
- cwd,
783
- ...options.remote === true ? { env: { LUNORA_REMOTE: "1" } } : {},
784
- tag: "vite"
785
- }
786
- };
787
- }
788
- const detection = detectFramework(cwd);
789
- const frameworkHint = detection.framework === "none" ? void 0 : `this project uses ${detection.framework} — the worker runs inside Vite there. run \`${runScriptCommand(manager, "dev")}\` for the full app (frontend + HMR); \`lunora dev\` starts only the worker.`;
790
- const workerPort = options.workerPort ?? DEFAULT_WORKER_PORT;
791
- const remote = resolveRemotePlan(options, cwd);
792
- const loopbackArgs = resolveLoopbackArgs(cwd, options.hasIpv6Loopback ?? hasIpv6Loopback);
793
- const exec = execArgsFor(manager, "wrangler", ["dev", "--port", String(workerPort), ...loopbackArgs, "--var", "WORKER_ENV:development", ...remote.args]);
794
- return {
795
- codegenEnabled: options.codegen !== false,
796
- flavor,
797
- frameworkHint,
798
- ipv4LoopbackForced: loopbackArgs.length > 0,
799
- remote: remote.plan,
800
- studioEnabled: options.studio !== false,
801
- studioPort: options.port ?? DEFAULT_STUDIO_PORT,
802
- workerOrigin: `http://localhost:${String(workerPort)}`,
803
- workerPort,
804
- wrangler: { args: exec.args, command: exec.command, cwd, tag: "wrangler" }
805
- };
806
- };
807
- const emitChildLine = (line, tag, kind, logger) => {
808
- if (line.length === 0) {
809
- return;
810
- }
811
- const formatted = formatLunoraEvent(line);
812
- if (formatted) {
813
- logger[formatted.level](`[lunora] ${formatted.text}`);
814
- return;
815
- }
816
- const prefixed = `[${tag}] ${line}`;
817
- if (kind === "stderr") {
818
- logger.warn(prefixed);
819
- } else {
820
- logger.info(prefixed);
821
- }
822
- };
823
- const pipeChildOutput = (child, tag, logger) => {
824
- const pumpStream = (stream, kind) => {
825
- if (!stream) {
826
- return;
827
- }
828
- let buffer = "";
829
- stream.on("data", (chunk) => {
830
- buffer += chunk.toString("utf8");
831
- const lines = buffer.split("\n");
832
- buffer = lines.pop() ?? "";
833
- for (const line of lines) {
834
- emitChildLine(line.trimEnd(), tag, kind, logger);
835
- }
836
- });
837
- stream.on("end", () => {
838
- emitChildLine(buffer.trimEnd(), tag, kind, logger);
839
- buffer = "";
840
- });
841
- };
842
- pumpStream(child.stdout, "stdout");
843
- pumpStream(child.stderr, "stderr");
844
- };
845
- const defaultWorkerSpawner = (descriptor, logger) => {
846
- const exec = spawnShellCompat(descriptor.command, descriptor.args);
847
- const child = spawn(exec.command, exec.args, {
848
- cwd: descriptor.cwd ?? process.cwd(),
849
- env: descriptor.env ? { ...process.env, ...descriptor.env } : process.env,
850
- shell: exec.shell,
851
- stdio: ["inherit", "pipe", "pipe"]
852
- });
853
- pipeChildOutput(child, descriptor.tag, logger);
854
- return {
855
- exited: new Promise((resolve) => {
856
- child.on("error", (error) => {
857
- logger.error(`[${descriptor.tag}] failed to start: ${error.message}`);
858
- resolve(1);
859
- });
860
- child.on("exit", (code) => {
861
- resolve(code ?? 0);
862
- });
863
- }),
864
- kill: (signal) => {
865
- try {
866
- child.kill(signal);
867
- } catch {
868
- }
869
- }
870
- };
871
- };
872
- const printBanner = (logger, plan, studioUrl) => {
873
- logger.info("");
874
- logger.success("Lunora dev");
875
- logger.info(` ➜ Worker: ${plan.workerOrigin}`);
876
- if (studioUrl !== void 0) {
877
- logger.info(` ➜ Studio: ${studioUrl}`);
878
- }
879
- if (plan.codegenEnabled) {
880
- logger.info(" ➜ Codegen: watching lunora/");
881
- }
882
- if (plan.remote.enabled) {
883
- if (plan.remote.bindings.length > 0) {
884
- logger.info(` ➜ Remote: ${plan.remote.bindings.join(", ")} → deployed worker`);
885
- } else {
886
- logger.warn(` ➜ Remote: requested but inactive (${plan.remote.reason ?? "no eligible bindings"}) — running fully local`);
887
- }
888
- }
889
- logger.info("");
890
- };
891
- const printAgentRulesHint = (logger, cwd) => {
892
- if (detectAgentRules(cwd).installed || !claimAgentRulesHint()) {
893
- return;
894
- }
895
- logger.info(` ⓘ ${AGENT_RULES_HINT}`);
896
- logger.info("");
897
- };
898
- const startContainerLogStreaming = (cwd, logger) => {
899
- if (process.env.LUNORA_CONTAINER_LOGS === "0") {
900
- return void 0;
901
- }
902
- const discovery = discoverContainerInfo(cwd, "lunora");
903
- const containers = discovery.containers.map((container) => {
904
- return { className: container.className, exportName: container.exportName };
905
- });
906
- if (containers.length === 0) {
907
- return void 0;
908
- }
909
- return streamContainerLogs({
910
- containers,
911
- onLine: (line) => {
912
- const tagged = `[container:${line.name}] ${line.text}`;
913
- if (line.level === "error") {
914
- logger.warn(tagged);
915
- } else {
916
- logger.info(tagged);
917
- }
918
- },
919
- onUnavailable: (message) => {
920
- logger.warn(`[container] Docker engine unreachable — container logs unavailable (${message})`);
921
- }
922
- });
923
- };
924
- const teardown = async (handles) => {
925
- handles.codegen?.close();
926
- handles.containerLogs?.close();
927
- await handles.studio?.close().catch(() => void 0);
928
- try {
929
- handles.remoteCleanup?.();
930
- } catch {
931
- }
932
- };
933
- const offerDevVariablesScaffold = async (options, cwd) => {
934
- try {
935
- const bindings = await inferLunoraBindings({ projectRoot: cwd });
936
- const packageNames = packageNamesFromBindings(bindings);
937
- const addedKeys = (options.ensureExample ?? ensureDevVarsExample)(cwd, packageNames);
938
- if (addedKeys.length > 0) {
939
- options.logger.info(`Updated .dev.vars.example with secrets for: ${packageNames.join(", ")} (${addedKeys.join(", ")})`);
940
- }
941
- } catch {
942
- }
943
- const result = await (options.ensureEnv ?? ensureDevVariables)({
944
- confirm: createTuiConfirm(),
945
- cwd,
946
- info: (message) => {
947
- options.logger.info(message);
948
- }
949
- });
950
- if (result.status === "declined" && !isInteractive()) {
951
- options.logger.info(
952
- `hint: ${DEV_VARS_FILE} was not scaffolded (non-interactive run). Copy ${DEV_VARS_EXAMPLE_FILE} → ${DEV_VARS_FILE} and fill in secrets, or run \`lunora dev\` in an interactive terminal to scaffold automatically.`
953
- );
954
- }
955
- try {
956
- (options.fillSecrets ?? fillDevSecrets)({
957
- cwd,
958
- info: (message) => {
959
- options.logger.info(message);
960
- }
961
- });
962
- } catch {
963
- }
964
- };
965
- const afterWorkerSpawn = (plan, cwd, logger, studioUrl) => {
966
- if (plan.flavor !== "wrangler") {
967
- return void 0;
968
- }
969
- if (studioUrl !== void 0) {
970
- updateDevServerState(cwd, { studioUrl });
971
- }
972
- let containerLogs;
973
- try {
974
- containerLogs = startContainerLogStreaming(cwd, logger);
975
- } catch {
976
- }
977
- printBanner(logger, plan, studioUrl);
978
- return containerLogs;
979
- };
980
- const claimStartRecord = (plan, cwd) => {
981
- const handoffPid = Number(process.env[DEV_HANDOFF_ENV]);
982
- const claim = claimDevServerState(
983
- cwd,
984
- {
985
- background: process.env[DEV_DAEMON_ENV] === "1",
986
- logFile: process.env[DEV_LOG_FILE_ENV],
987
- mode: "cli",
988
- pid: process.pid,
989
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
990
- url: plan.workerOrigin
991
- },
992
- Number.isInteger(handoffPid) && handoffPid > 0 ? { supersedePid: handoffPid } : void 0
993
- );
994
- return claim.ok ? void 0 : claim.existing;
995
- };
996
- const buildDevPlan = async (options) => {
997
- const cwd = options.cwd ?? process.cwd();
998
- const flavor = options.flavor ?? detectDevFlavor(cwd);
999
- const workerPort = flavor === "wrangler" ? await resolveWorkerPort(options, cwd) : options.workerPort;
1000
- return planDevCommand({ ...options, cwd, flavor, workerPort });
1001
- };
1002
- const superviseWorkers = async (worker, sidecar, logger) => {
1003
- let sigintCount = 0;
1004
- let escalationTimer;
1005
- const killChildren = (signal) => {
1006
- worker.kill(signal);
1007
- sidecar?.kill(signal);
1008
- };
1009
- const onSigint = () => {
1010
- sigintCount += 1;
1011
- if (sigintCount === 1) {
1012
- logger.info("received SIGINT — shutting down (press Ctrl-C again to force-kill)");
1013
- killChildren("SIGTERM");
1014
- escalationTimer = setTimeout(() => {
1015
- killChildren("SIGKILL");
1016
- }, SIGINT_GRACE_MS);
1017
- escalationTimer.unref();
1018
- } else {
1019
- killChildren("SIGKILL");
1020
- }
1021
- };
1022
- const onSigterm = () => {
1023
- killChildren("SIGTERM");
1024
- };
1025
- process.on("SIGINT", onSigint);
1026
- process.on("SIGTERM", onSigterm);
1027
- const exits = [
1028
- worker.exited.then((code) => {
1029
- return { code, who: "worker" };
1030
- })
1031
- ];
1032
- if (sidecar !== void 0) {
1033
- exits.push(
1034
- sidecar.exited.then((code) => {
1035
- return { code, who: "sidecar" };
1036
- })
1037
- );
1038
- }
1039
- const first = await Promise.race(exits);
1040
- if (sidecar !== void 0) {
1041
- if (first.who === "sidecar") {
1042
- logger.warn("[worker] the Lunora sidecar (wrangler dev) exited — shutting down the framework dev server");
1043
- worker.kill("SIGTERM");
1044
- } else {
1045
- sidecar.kill("SIGTERM");
1046
- }
1047
- await Promise.allSettled([worker.exited, sidecar.exited]);
1048
- }
1049
- if (escalationTimer) {
1050
- clearTimeout(escalationTimer);
1051
- }
1052
- process.off("SIGINT", onSigint);
1053
- process.off("SIGTERM", onSigterm);
1054
- return first.code;
1055
- };
1056
- const startStudioBestEffort = async (options, plan, cwd, logger) => {
1057
- if (!plan.studioEnabled) {
1058
- return void 0;
1059
- }
1060
- try {
1061
- return await (options.startStudio ?? startStudioServer)({
1062
- cwd,
1063
- logger: {
1064
- warnOnce: (message) => {
1065
- logger.warn(message);
1066
- }
1067
- },
1068
- port: plan.studioPort,
1069
- workerOrigin: plan.workerOrigin
1070
- });
1071
- } catch (error) {
1072
- logger.warn(`studio server failed to start (${error instanceof Error ? error.message : String(error)}) — continuing without it`);
1073
- return void 0;
1074
- }
1075
- };
1076
- const ensureSidecarGenerated = (plan, options, cwd, logger) => {
1077
- if (plan.sidecar === void 0) {
1078
- return;
1079
- }
1080
- try {
1081
- runCodegen({ apiSpec: options.apiSpec, lunoraDirectory: "lunora", projectRoot: cwd });
1082
- } catch (error) {
1083
- logger.warn(`codegen (pre-sidecar) failed: ${error instanceof Error ? error.message : String(error)} — the framework dev server will retry`);
1084
- }
1085
- };
1086
- const runDevCommand = async (options) => {
1087
- const plan = await buildDevPlan(options);
1088
- const { logger } = options;
1089
- const cwd = plan.wrangler.cwd ?? process.cwd();
1090
- const handles = { remoteCleanup: plan.remote.cleanup };
1091
- try {
1092
- const handoffPid = Number(process.env[DEV_HANDOFF_ENV]);
1093
- const existing = readLiveDevServerState(cwd);
1094
- if (existing !== void 0 && existing.pid !== process.pid && existing.pid !== handoffPid) {
1095
- reportExistingServer(logger, existing);
1096
- return { code: 0, plan };
1097
- }
1098
- const incumbent = claimStartRecord(plan, cwd);
1099
- if (incumbent !== void 0) {
1100
- reportExistingServer(logger, incumbent);
1101
- return { code: 0, plan };
1102
- }
1103
- if (plan.flavor === "vite" || plan.flavor === "framework-worker") {
1104
- plan.wrangler.env = { ...plan.wrangler.env, [DEV_HANDOFF_ENV]: String(process.pid) };
1105
- }
1106
- await offerDevVariablesScaffold(options, cwd);
1107
- logger.info(
1108
- plan.flavor === "vite" ? "starting vite dev (worker + studio + codegen run inside Vite via @lunora/vite)" : "starting wrangler dev + studio"
1109
- );
1110
- if (plan.ipv4LoopbackForced) {
1111
- logger.info(
1112
- "no IPv6 loopback (::1) on this host — binding the worker to 127.0.0.1 (--ip) so wrangler dev doesn't crash. Pin `dev.ip` in wrangler.jsonc to override."
1113
- );
1114
- }
1115
- if (plan.codegenEnabled) {
1116
- handles.codegen = (options.startCodegen ?? startCodegenWatch)({ apiSpec: options.apiSpec, logger, projectRoot: cwd });
1117
- }
1118
- handles.studio = await startStudioBestEffort(options, plan, cwd, logger);
1119
- const studioUrl = handles.studio?.url;
1120
- if (plan.frameworkHint !== void 0) {
1121
- logger.warn(plan.frameworkHint);
1122
- }
1123
- ensureSidecarGenerated(plan, options, cwd, logger);
1124
- const spawn = options.startWorker ?? defaultWorkerSpawner;
1125
- const worker = spawn(plan.wrangler, logger);
1126
- const sidecar = plan.sidecar === void 0 ? void 0 : spawn(plan.sidecar, logger);
1127
- handles.containerLogs = afterWorkerSpawn(plan, cwd, logger, studioUrl);
1128
- printAgentRulesHint(logger, cwd);
1129
- const code = await superviseWorkers(worker, sidecar, logger);
1130
- return { code, plan };
1131
- } finally {
1132
- clearDevServerState(cwd, process.pid);
1133
- await teardown(handles);
1134
- }
1135
- };
1136
- const execute = defineHandler(async ({ argument, cwd, logger, options }) => {
1137
- const json = options.json === true;
1138
- const dispatched = runLifecycleSubcommand({ cwd, json, lines: options.lines, logger, subcommand: argument[0] });
1139
- if (dispatched !== void 0) {
1140
- return dispatched;
1141
- }
1142
- const isDaemon = process.env[DEV_DAEMON_ENV] === "1";
1143
- const agent = isDaemon ? void 0 : detectAiAgent();
1144
- const jsonLogs = json || agent !== void 0;
1145
- if (jsonLogs) {
1146
- forceJsonLogging();
1147
- }
1148
- if (agent !== void 0 && options.background !== true) {
1149
- logger.info(
1150
- `AI agent detected (${agent.name} via ${agent.variable}) — starting the dev server in background mode with JSON logs. Set LUNORA_AGENT_MODE=0 to opt out.`
1151
- );
1152
- }
1153
- const remote = resolveRemoteEnabled({
1154
- configPreference: readProjectRemotePreference(cwd),
1155
- envValue: process.env["LUNORA_REMOTE"],
1156
- flag: options.remote
1157
- });
1158
- if (!isDaemon && (options.background === true || agent !== void 0)) {
1159
- const existing = readLiveDevServerState(cwd);
1160
- if (existing !== void 0) {
1161
- reportExistingServer(logger, existing);
1162
- return { code: 0 };
1163
- }
1164
- return startBackground({ cwd, jsonLogs, logger, options, remote });
1165
- }
1166
- return runDevCommand({
1167
- apiSpec: parseApiSpec(options.apiSpec),
1168
- // cerebro parses `--no-codegen`/`--no-studio` as the negation of the
1169
- // `codegen`/`studio` booleans (runtime key drops the `no-` prefix), so a
1170
- // passed flag arrives as `false`, absent as `true` (the option default).
1171
- codegen: options.codegen === false ? false : void 0,
1172
- cwd,
1173
- logger,
1174
- port: options.port,
1175
- remote,
1176
- studio: options.studio === false ? false : void 0,
1177
- workerPort: options.workerPort
1178
- });
1179
- });
1180
-
1181
- export { detectDevFlavor, execute, planDevCommand, resolveRemotePlan, resolveWorkerPort, runDevCommand };
1
+ import{spawnSync as N,spawn as J}from"node:child_process";import{existsSync as L,watch as me,readFileSync as K,statSync as fe,openSync as z,readSync as ve,closeSync as Y,mkdirSync as we}from"node:fs";import{join as k,dirname as he}from"node:path";import{runCodegen as X}from"@lunora/codegen";import{detectAgentRules as Z,claimDevServerState as Q,clearDevServerState as f,readDevServerState as R,readLiveDevServerState as y,detectFramework as ee,readProjectDependencyNames as Se,DEV_HANDOFF_ENV as h,isRecordedProcessCurrent as ke,DEV_LOG_FILE as ye,DEV_LOG_FILE_ENV as re,DEV_DAEMON_ENV as I,updateDevServerState as oe,detectAiAgent as be,resolveRemoteEnabled as $e,readProjectRemotePreference as Ee,inferLunoraBindings as Ne,packageNamesFromBindings as Le,ensureDevVarsExample as Re,ensureDevVariables as Ie,isInteractive as Oe,DEV_VARS_FILE as T,DEV_VARS_EXAMPLE_FILE as Pe,fillDevSecrets as xe,claimAgentRulesHint as De,AGENT_RULES_HINT as Te,findWranglerFile as te,readWranglerJsonc as ne,discoverContainerInfo as Ce,streamContainerLogs as _e,materializeRemoteWranglerConfig as Ae,formatLunoraEvent as je}from"@lunora/config";import{r as Fe}from"../packem_shared/api-spec-BENwiyUa.mjs";import{l as Me}from"../packem_shared/codegen-error-DJN6pTH5.mjs";import{i as Ge}from"../packem_shared/command-Bjv4VmYA.mjs";import{P as se,M as $,k as He}from"../packem_shared/detect-package-manager-B2eIuAiP.mjs";import{createServer as Ue,connect as Ve}from"node:net";import{forceJsonLogging as We}from"../packem_shared/createLogger--C3x1FNE.mjs";import{networkInterfaces as qe}from"node:os";import{spawnShellCompat as ie}from"../packem_shared/createRecordingSpawner-C8_7nffs.mjs";import{createServer as Be,request as Je}from"node:http";import{loadStudioAssets as C,studioAssetsStamp as _,renderStudioHtml as Ke,resolveAdminToken as ze,SCHEMA_EDIT_ENDPOINT as Ye,POLICY_SCAFFOLD_ENDPOINT as Xe,SEED_ENDPOINT as Ze,serveJsonHandler as Qe,isStandaloneModulePath as er,readStandaloneAsset as rr,assetContentType as or,handleSchemaEditRequest as tr,handlePolicyScaffoldRequest as nr,handleSeedRequest as sr}from"@lunora/config/studio-host";import{$ as ir}from"../packem_shared/tui-prompts-CCt3jm1k.mjs";import{d as v}from"../packem_shared/output-format-D7cKB-O5.mjs";const ar=100,dr=/[/\\]/u,A=(e,r,t,o,n)=>{try{X({apiSpec:t,lunoraDirectory:r,projectRoot:e}),o.success(`codegen: wrote ${r}/_generated (${n})`)}catch(s){o.error(Me(s,n))}},cr=e=>{const r=e.lunoraDirectory??"lunora",t=k(e.projectRoot,r),o=e.debounceMs??ar,{apiSpec:n}=e;A(e.projectRoot,r,n,e.logger,"startup");let s,i;if(!L(t))return e.logger.warn(`codegen watch unavailable (no such directory: ${t}) — schema edits will NOT auto-regenerate. Run \`lunora codegen\` manually after each edit.`),{close:()=>{},watchAvailable:!1};try{i=me(t,{recursive:!0},(c,u)=>{typeof u=="string"&&u.split(dr).includes("_generated")||(s&&clearTimeout(s),s=setTimeout(A,o,e.projectRoot,r,n,e.logger,`change: ${u??"?"}`))})}catch(c){return e.logger.warn(`codegen watch unavailable (${c instanceof Error?c.message:String(c)}) — schema edits will NOT auto-regenerate. Run \`lunora codegen\` manually after each edit.`),{close:()=>{},watchAvailable:!1}}const a=i;return{close:()=>{s&&clearTimeout(s),a.close()},watchAvailable:!0}},lr=64,ae="127.0.0.1",ur=65535,pr=(e,r=ae)=>new Promise(t=>{const o=Ue();o.once("error",()=>{t(!1)}),o.once("listening",()=>{o.close(()=>{t(!0)})}),o.listen(e,r)}),gr=async(e,r=ae,t=lr)=>{for(let o=e;o<e+t&&o<=ur;o+=1)if(await pr(o,r))return o;return e},j=(e=qe)=>Object.values(e()).some(r=>r?.some(t=>t.internal&&t.address==="::1")),F="/_lunora",M=e=>{const r=e.indexOf("?");return r===-1?e:e.slice(0,r)},mr=new Set(["127.0.0.1","::1","[::1]","localhost"]),G=e=>{if(e===void 0||e==="")return!1;const r=e.startsWith("[")?e.slice(0,e.indexOf("]")+1):e.split(":")[0]??e;return mr.has(r)},fr=(e,r,t)=>{const o=Je({headers:{...e.headers,host:t.host},hostname:t.hostname,method:e.method,path:e.url,port:t.port},n=>{r.writeHead(n.statusCode??502,n.headers),n.pipe(r)});o.on("error",n=>{r.statusCode=502,r.end(`lunora dev: worker unreachable (${n.message})`)}),e.pipe(o)},vr=5e3,wr=(e,r,t,o)=>{const n=Ve({host:o.hostname,port:Number(o.port)},()=>{n.setTimeout(0);const s=[`${e.method??"GET"} ${e.url??"/"} HTTP/1.1`];for(const[i,a]of Object.entries(e.headers))if(i!=="host")for(const c of Array.isArray(a)?a:[a])c!==void 0&&s.push(`${i}: ${c}`);s.push(`host: ${o.host}`,"",""),n.write(s.join(`\r
2
+ `)),t.length>0&&n.write(t),n.pipe(r),r.pipe(n)});n.setTimeout(vr,()=>n.destroy()),n.on("error",()=>r.destroy()),r.on("error",()=>n.destroy())},hr=async e=>{const r=e.host??"127.0.0.1",t=r==="127.0.0.1"||r==="localhost"||r==="::1",o=new URL(e.workerOrigin);let n=C(e.logger,import.meta.url),s=_(import.meta.url);const i=Ke({adminToken:t?ze(e.cwd):void 0,basePath:"/",dataEditable:t,rulesInstalled:Z(e.cwd).installed,runAsIdentity:t,schemaEditable:t,scriptSrc:"/studio.js",styleHref:"/styles.css"}),a=(l,d,g)=>{l.statusCode=200,l.setHeader("Content-Type",g),l.end(d)},c=(l,d,g,b)=>{if(t){Qe(l,d,g,e.cwd);return}d.statusCode=403,d.setHeader("Content-Type","text/plain"),d.end(b)},u=(l,d)=>{const g=l==="/styles.css",b=er(l);if(!g&&!b)return!1;const P=_(import.meta.url);if(P!==s&&(n=C(e.logger,import.meta.url),s=P),n===void 0)return d.statusCode=501,d.setHeader("Content-Type","text/plain"),d.end("Lunora studio assets not found — install and build @lunora/studio."),!0;if(g)return a(d,n.styles,"text/css; charset=utf-8"),!0;const x=l.slice(l.lastIndexOf("/")+1),D=rr(x,import.meta.url);return D===void 0?(d.statusCode=404,d.setHeader("Content-Type","text/plain"),d.end("Not found"),!0):(a(d,D,or(x)),!0)},p=Buffer.from(i),m=Be((l,d)=>{const g=M(l.url??"/");if(t&&!G(l.headers.host)){d.statusCode=403,d.setHeader("Content-Type","text/plain"),d.end("Lunora studio: refusing a request whose Host is not a loopback literal (DNS-rebinding guard).");return}if(g.startsWith(F)){if(!t){d.statusCode=403,d.setHeader("Content-Type","text/plain"),d.end("Lunora studio: the worker admin proxy is only available on a loopback bind.");return}fr(l,d,o);return}if(g===Ye){c(l,d,tr,"Lunora schema editing is only available on loopback hosts in dev.");return}if(g===Xe){c(l,d,nr,"Lunora policy scaffolding is only available on loopback hosts in dev.");return}if(g===Ze){c(l,d,sr,"Lunora data seeding is only available on loopback hosts in dev.");return}u(g,d)||a(d,p,"text/html; charset=utf-8")});return m.on("upgrade",(l,d,g)=>{if(!t||!G(l.headers.host)){d.destroy();return}M(l.url??"").startsWith(F)?wr(l,d,g,o):d.destroy()}),await new Promise((l,d)=>{m.once("error",g=>{g.code==="EADDRINUSE"?d(new Error(`studio port ${String(e.port)} is already in use — pass a different port with --port, or stop the process using it`,{cause:g})):d(g)}),m.listen(e.port,r,()=>{l()})}),{close:()=>new Promise(l=>{m.close(()=>{l()})}),url:`http://${r==="0.0.0.0"||r==="::"?"localhost":r}:${String(e.port)}`}},Sr=12e4,de=250,kr=1e4,yr=100,br=40,w=256*1024,$r="LUNORA_DEV_READY_TIMEOUT_MS",Er=new Set(["nuxt","sveltekit"]),O=e=>Er.has(ee(e).framework)?"framework-worker":Se(e).has("@lunora/vite")?"vite":"wrangler",ce=e=>{const r=se(e);let t;try{const o=K(k(e,"package.json"),"utf8");t=JSON.parse(o).scripts?.dev}catch{}if(t===void 0||t.trim()===""||t.includes("lunora")){const o=$(r,"vite",["dev"]);return{args:o.args,command:o.command}}return{args:["run","dev"],command:r}},E=e=>new Promise(r=>{setTimeout(r,e)}),Nr=async e=>{try{return await fetch(new URL("/_lunora/status",e),{signal:AbortSignal.timeout(1e3)}),!0}catch{return!1}},Lr=e=>{we(he(e.logPath),{recursive:!0});const r=z(e.logPath,"w",384);let t;const o=ie(e.command,e.args);try{t=J(o.command,o.args,{cwd:e.cwd,detached:!0,env:e.env,shell:o.shell,stdio:["ignore",r,r],windowsHide:!0})}finally{Y(r)}return t.unref(),{exited:new Promise(n=>{t.on("error",()=>{n(1)}),t.on("exit",(s,i)=>{n(s??(i?1:0))})}),pid:t.pid}},Rr=e=>{try{const{size:r}=fe(e);if(r<=w)return K(e,"utf8");const t=z(e,"r");try{const o=Buffer.alloc(w),n=ve(t,o,0,w,r-w),s=o.toString("utf8",0,n);return s.slice(s.indexOf(`
3
+ `)+1)}finally{Y(t)}}catch{return}},le=(e,r)=>{const t=Rr(e);if(t===void 0)return[];const o=t.split(`
4
+ `);return o.at(-1)===""&&o.pop(),r>0?o.slice(-r):o},ue=(e,r)=>r?.logFile??k(e,ye),pe=e=>{e.info(" Stop: lunora dev stop"),e.info(" Status: lunora dev status"),e.info(" Logs: lunora dev logs")},S=(e,r)=>{e.warn(`Dev server already running at ${r.url} (pid ${String(r.pid)})`),pe(e)},Ir=(e,r)=>{e.success(`Dev server running at ${r.url} (pid ${String(r.pid)})`),r.studioUrl!==void 0&&e.info(` Studio: ${r.studioUrl}`),pe(e)},Or=async e=>{const{child:r,cwd:t,deadline:o,pollIntervalMs:n,probe:s}=e,i=r.exited.then(a=>({exitCode:a}));for(;Date.now()<o;){const a=y(t);if(a!==void 0&&a.pid!==process.pid&&await s(a.url))return{state:a,status:"ready"};const c=await Promise.race([i,E(n)]);if(c!==void 0)return{...c,status:"exited"}}return{status:"timeout"}},Pr=async e=>{const{cwd:r,logger:t}=e,o=ue(r,void 0),n=Number(process.env[$r]??""),s=e.readyTimeoutMs??(Number.isFinite(n)&&n>0?n:Sr),i=(e.spawnDetached??Lr)({args:e.command.args,command:e.command.command,cwd:r,env:{...process.env,...e.env,[I]:"1",[re]:o,...process.env.FORCE_COLOR===void 0?{NO_COLOR:"1"}:{},...e.json?{LUNORA_LOG_JSON:"1"}:{}},logPath:o}),a=await Or({child:i,cwd:r,deadline:Date.now()+s,pollIntervalMs:e.pollIntervalMs??de,probe:e.probe??Nr});if(a.status==="ready")return Ir(t,a.state),{code:0};if(a.status==="exited"){t.error(`dev server exited before becoming ready (exit code ${String(a.exitCode)}). Last output:`);for(const u of le(o,br))t.error(` ${u}`);return f(r,i.pid??process.pid),{code:a.exitCode===0?1:a.exitCode}}i.pid!==void 0&&R(r)?.pid===process.pid&&oe(r,{logFile:o,pid:i.pid});const c=i.pid===void 0?"":` (pid ${String(i.pid)})`;return t.warn(`dev server did not confirm ready within ${String(Math.round(s/1e3))}s — it may still be compiling${c}. Check \`lunora dev status\` and \`lunora dev logs\`; \`lunora dev stop\` shuts it down.`),{code:1}},xr=(e,r)=>{const t=["dev"];return e.apiSpec!==void 0&&t.push("--api-spec",e.apiSpec),e.port!==void 0&&t.push("--port",String(e.port)),e.workerPort!==void 0&&t.push("--worker-port",String(e.workerPort)),e.codegen===!1&&t.push("--no-codegen"),e.studio===!1&&t.push("--no-studio"),r&&t.push("--remote"),t},Dr=async e=>{const{cwd:r,jsonLogs:t,logger:o,options:n,remote:s}=e,i=e.run??Pr,a=O(r),c=a==="wrangler"?`http://localhost:${String(n.workerPort??8787)}`:"http://localhost:5173",u=Q(r,{background:!0,mode:"cli",pid:process.pid,startedAt:new Date().toISOString(),url:c});if(!u.ok)return u.existing!==void 0&&S(o,u.existing),{code:0};const p={[h]:String(process.pid)};try{return a==="vite"?await i({command:ce(r),cwd:r,env:{...s?{LUNORA_REMOTE:"1"}:{},...p},json:t,logger:o}):await i({command:{args:[process.argv[1]??"lunora",...xr(n,s)],command:process.execPath},cwd:r,env:p,json:t,logger:o})}finally{f(r,process.pid)}},Tr=e=>{try{const r=N("ps",["-o","pgid=","-p",String(e)],{encoding:"utf8"}),t=Number.parseInt(r.stdout.trim(),10);return Number.isInteger(t)&&t>0?t:void 0}catch{return}},Cr=(e,r,t=process.platform,o=N)=>{if(t==="win32"){try{o("taskkill",["/pid",String(e.pid),"/T","/F"],{stdio:"ignore"})}catch{}return}if(e.background===!0)try{r(-(Tr(e.pid)??e.pid),"SIGKILL");return}catch{}try{r(e.pid,"SIGKILL")}catch{}},_r=(e,r)=>(r.json?v({pid:e.pid,stopped:!1}):r.logger.error(`dev server (pid ${String(e.pid)}) survived the force-kill — record kept; retry \`lunora dev stop\` or kill the process manually.`),{code:1}),Ar=async e=>{const{cwd:r,logger:t}=e,o=R(r),n=e.alive??(m=>m===o?.pid&&ke(o)),s=e.signal??((m,l)=>{process.kill(m,l)}),i=e.pollIntervalMs??de,a=e.stopGraceMs??kr,c=e.platform??process.platform,u=e.spawnSyncImpl??N;if(o===void 0||!n(o.pid))return o!==void 0&&f(r,o.pid),e.json?v({running:!1,stopped:!1}):t.info("No dev server running."),{code:0};try{s(o.pid,"SIGTERM")}catch{}const p=Date.now()+a;for(;n(o.pid)&&Date.now()<p;)await E(i);return n(o.pid)&&(Cr(o,s,c,u),await E(i),n(o.pid))?_r(o,e):(f(r,o.pid),e.json?v({pid:o.pid,stopped:!0}):t.success(`Stopped dev server (pid ${String(o.pid)}).`),{code:0})},jr=e=>{const{cwd:r,logger:t}=e,o=y(r);if(o===void 0)return e.json?v({running:!1}):t.info("No dev server running."),{code:0};const n=e.now??Date.now,s=o.startedAt===void 0?Number.NaN:Date.parse(o.startedAt),i=Number.isFinite(s)?Math.max(0,Math.round((n()-s)/1e3)):void 0;if(e.json)return v({background:o.background===!0,logFile:o.logFile,mode:o.mode,pid:o.pid,running:!0,startedAt:o.startedAt,studioUrl:o.studioUrl,uptimeSeconds:i,url:o.url}),{code:0};const a=[`pid ${String(o.pid)}`,...i===void 0?[]:[`uptime ${String(i)}s`],o.background===!0?"background":"foreground"];return t.success(`Dev server running at ${o.url} (${a.join(", ")})`),o.studioUrl!==void 0&&t.info(` Studio: ${o.studioUrl}`),o.logFile!==void 0&&t.info(` Logs: lunora dev logs (${o.logFile})`),{code:0}},Fr=e=>{const{cwd:r,logger:t}=e,o=R(r),n=ue(r,o);if(!L(n))return t.info("No dev server logs found — output is captured when the server runs in background mode (`lunora dev --background`)."),{code:0};const s=le(n,e.lines??yr);return s.length>0&&process.stdout.write(`${s.join(`
5
+ `)}
6
+ `),{code:0}},Mr=e=>{const{cwd:r,json:t,lines:o,logger:n,subcommand:s}=e;if(s==="stop")return Ar({cwd:r,json:t,logger:n});if(s==="status")return jr({cwd:r,json:t,logger:n});if(s==="logs")return Fr({cwd:r,lines:o,logger:n});if(s!==void 0)return n.error(`dev: unknown subcommand "${s}" expected stop | status | logs (or no subcommand to start the dev server)`),{code:1}},H="wrangler.dev.jsonc",U=6173,ge=8787,V=5173,Gr=5e3,Hr=(e,r)=>{const t=()=>{};if(!e.remote)return{args:[],plan:{bindings:[],cleanup:t,enabled:!1}};const o=(e.materializeRemote??Ae)({enabled:!0,projectRoot:r}),n=o.remoteBindings.map(i=>`${i.binding} (${i.kind})`),{cleanup:s}=o;return o.configPath===void 0?{args:[],plan:{bindings:n,cleanup:s,enabled:!0,reason:o.reason}}:{args:["--config",o.configPath],plan:{bindings:n,cleanup:s,enabled:!0}}},W=e=>ne(e).parsed?.dev?.ip,q=(e,r,t)=>{if(t!==void 0){const n=k(e,t);if(L(n)&&W(n)!==void 0)return[]}const o=te(e);return o!==void 0&&W(o)!==void 0?[]:r()?[]:["--ip","127.0.0.1"]},Ur=async(e,r)=>{if(e.workerPort!==void 0)return e.workerPort;const t=te(r);if(t!==void 0){const{parsed:o}=ne(t);if(typeof o?.dev?.port=="number")return o.dev.port}return(e.findFreePort??gr)(ge)},Vr=e=>{const r=e.cwd??process.cwd(),t=se(r),o=e.flavor??O(r);if(o==="vite"||o==="framework-worker"){const p=ce(r);let m;if(o==="framework-worker"){const l=q(r,e.hasIpv6Loopback??j,H),d=$(t,"wrangler",["dev","--config",H,...l,"--var","WORKER_ENV:development"]);m={args:d.args,command:d.command,cwd:r,tag:"worker"}}return{codegenEnabled:!1,flavor:o,ipv4LoopbackForced:!1,remote:{bindings:[],cleanup:()=>{},enabled:e.remote===!0},...m?{sidecar:m}:{},studioEnabled:!1,studioPort:e.port??U,workerOrigin:`http://localhost:${String(V)}`,workerPort:V,wrangler:{args:p.args,command:p.command,cwd:r,...e.remote===!0?{env:{LUNORA_REMOTE:"1"}}:{},tag:"vite"}}}const n=ee(r),s=n.framework==="none"?void 0:`this project uses ${n.framework} — the worker runs inside Vite there. run \`${He(t,"dev")}\` for the full app (frontend + HMR); \`lunora dev\` starts only the worker.`,i=e.workerPort??ge,a=Hr(e,r),c=q(r,e.hasIpv6Loopback??j),u=$(t,"wrangler",["dev","--port",String(i),...c,"--var","WORKER_ENV:development",...a.args]);return{codegenEnabled:e.codegen!==!1,flavor:o,frameworkHint:s,ipv4LoopbackForced:c.length>0,remote:a.plan,studioEnabled:e.studio!==!1,studioPort:e.port??U,workerOrigin:`http://localhost:${String(i)}`,workerPort:i,wrangler:{args:u.args,command:u.command,cwd:r,tag:"wrangler"}}},B=(e,r,t,o)=>{if(e.length===0)return;const n=je(e);if(n){o[n.level](`[lunora] ${n.text}`);return}const s=`[${r}] ${e}`;t==="stderr"?o.warn(s):o.info(s)},Wr=(e,r,t)=>{const o=(n,s)=>{if(!n)return;let i="";n.on("data",a=>{i+=a.toString("utf8");const c=i.split(`
7
+ `);i=c.pop()??"";for(const u of c)B(u.trimEnd(),r,s,t)}),n.on("end",()=>{B(i.trimEnd(),r,s,t),i=""})};o(e.stdout,"stdout"),o(e.stderr,"stderr")},qr=(e,r)=>{const t=ie(e.command,e.args),o=J(t.command,t.args,{cwd:e.cwd??process.cwd(),env:e.env?{...process.env,...e.env}:process.env,shell:t.shell,stdio:["inherit","pipe","pipe"]});return Wr(o,e.tag,r),{exited:new Promise(n=>{o.on("error",s=>{r.error(`[${e.tag}] failed to start: ${s.message}`),n(1)}),o.on("exit",s=>{n(s??0)})}),kill:n=>{try{o.kill(n)}catch{}}}},Br=(e,r,t)=>{e.info(""),e.success("Lunora dev"),e.info(` ➜ Worker: ${r.workerOrigin}`),t!==void 0&&e.info(` ➜ Studio: ${t}`),r.codegenEnabled&&e.info(" ➜ Codegen: watching lunora/"),r.remote.enabled&&(r.remote.bindings.length>0?e.info(` ➜ Remote: ${r.remote.bindings.join(", ")} → deployed worker`):e.warn(` ➜ Remote: requested but inactive (${r.remote.reason??"no eligible bindings"}) — running fully local`)),e.info("")},Jr=(e,r)=>{Z(r).installed||!De()||(e.info(` ⓘ ${Te}`),e.info(""))},Kr=(e,r)=>{if(process.env.LUNORA_CONTAINER_LOGS==="0")return;const t=Ce(e,"lunora").containers.map(o=>({className:o.className,exportName:o.exportName}));if(t.length!==0)return _e({containers:t,onLine:o=>{const n=`[container:${o.name}] ${o.text}`;o.level==="error"?r.warn(n):r.info(n)},onUnavailable:o=>{r.warn(`[container] Docker engine unreachable — container logs unavailable (${o})`)}})},zr=async e=>{e.codegen?.close(),e.containerLogs?.close(),await e.studio?.close().catch(()=>{});try{e.remoteCleanup?.()}catch{}},Yr=async(e,r)=>{try{const t=await Ne({projectRoot:r}),o=Le(t),n=(e.ensureExample??Re)(r,o);n.length>0&&e.logger.info(`Updated .dev.vars.example with secrets for: ${o.join(", ")} (${n.join(", ")})`)}catch{}(await(e.ensureEnv??Ie)({confirm:ir(),cwd:r,info:t=>{e.logger.info(t)}})).status==="declined"&&!Oe()&&e.logger.info(`hint: ${T} was not scaffolded (non-interactive run). Copy ${Pe} → ${T} and fill in secrets, or run \`lunora dev\` in an interactive terminal to scaffold automatically.`);try{(e.fillSecrets??xe)({cwd:r,info:t=>{e.logger.info(t)}})}catch{}},Xr=(e,r,t,o)=>{if(e.flavor!=="wrangler")return;o!==void 0&&oe(r,{studioUrl:o});let n;try{n=Kr(r,t)}catch{}return Br(t,e,o),n},Zr=(e,r)=>{const t=Number(process.env[h]),o=Q(r,{background:process.env[I]==="1",logFile:process.env[re],mode:"cli",pid:process.pid,startedAt:new Date().toISOString(),url:e.workerOrigin},Number.isInteger(t)&&t>0?{supersedePid:t}:void 0);return o.ok?void 0:o.existing},Qr=async e=>{const r=e.cwd??process.cwd(),t=e.flavor??O(r),o=t==="wrangler"?await Ur(e,r):e.workerPort;return Vr({...e,cwd:r,flavor:t,workerPort:o})},eo=async(e,r,t)=>{let o=0,n;const s=p=>{e.kill(p),r?.kill(p)},i=()=>{o+=1,o===1?(t.info("received SIGINT — shutting down (press Ctrl-C again to force-kill)"),s("SIGTERM"),n=setTimeout(()=>{s("SIGKILL")},Gr),n.unref()):s("SIGKILL")},a=()=>{s("SIGTERM")};process.on("SIGINT",i),process.on("SIGTERM",a);const c=[e.exited.then(p=>({code:p,who:"worker"}))];r!==void 0&&c.push(r.exited.then(p=>({code:p,who:"sidecar"})));const u=await Promise.race(c);return r!==void 0&&(u.who==="sidecar"?(t.warn("[worker] the Lunora sidecar (wrangler dev) exited — shutting down the framework dev server"),e.kill("SIGTERM")):r.kill("SIGTERM"),await Promise.allSettled([e.exited,r.exited])),n&&clearTimeout(n),process.off("SIGINT",i),process.off("SIGTERM",a),u.code},ro=async(e,r,t,o)=>{if(r.studioEnabled)try{return await(e.startStudio??hr)({cwd:t,logger:{warnOnce:n=>{o.warn(n)}},port:r.studioPort,workerOrigin:r.workerOrigin})}catch(n){o.warn(`studio server failed to start (${n instanceof Error?n.message:String(n)}) — continuing without it`);return}},oo=(e,r,t,o)=>{if(e.sidecar!==void 0)try{X({apiSpec:r.apiSpec,lunoraDirectory:"lunora",projectRoot:t})}catch(n){o.warn(`codegen (pre-sidecar) failed: ${n instanceof Error?n.message:String(n)} — the framework dev server will retry`)}},to=async e=>{const r=await Qr(e),{logger:t}=e,o=r.wrangler.cwd??process.cwd(),n={remoteCleanup:r.remote.cleanup};try{const s=Number(process.env[h]),i=y(o);if(i!==void 0&&i.pid!==process.pid&&i.pid!==s)return S(t,i),{code:0,plan:r};const a=Zr(r,o);if(a!==void 0)return S(t,a),{code:0,plan:r};(r.flavor==="vite"||r.flavor==="framework-worker")&&(r.wrangler.env={...r.wrangler.env,[h]:String(process.pid)}),await Yr(e,o),t.info(r.flavor==="vite"?"starting vite dev (worker + studio + codegen run inside Vite via @lunora/vite)":"starting wrangler dev + studio"),r.ipv4LoopbackForced&&t.info("no IPv6 loopback (::1) on this host — binding the worker to 127.0.0.1 (--ip) so wrangler dev doesn't crash. Pin `dev.ip` in wrangler.jsonc to override."),r.codegenEnabled&&(n.codegen=(e.startCodegen??cr)({apiSpec:e.apiSpec,logger:t,projectRoot:o})),n.studio=await ro(e,r,o,t);const c=n.studio?.url;r.frameworkHint!==void 0&&t.warn(r.frameworkHint),oo(r,e,o,t);const u=e.startWorker??qr,p=u(r.wrangler,t),m=r.sidecar===void 0?void 0:u(r.sidecar,t);return n.containerLogs=Xr(r,o,t,c),Jr(t,o),{code:await eo(p,m,t),plan:r}}finally{f(o,process.pid),await zr(n)}},bo=Ge(async({argument:e,cwd:r,logger:t,options:o})=>{const n=o.json===!0,s=Mr({cwd:r,json:n,lines:o.lines,logger:t,subcommand:e[0]});if(s!==void 0)return s;const i=process.env[I]==="1",a=i?void 0:be(),c=n||a!==void 0;c&&We(),a!==void 0&&o.background!==!0&&t.info(`AI agent detected (${a.name} via ${a.variable}) — starting the dev server in background mode with JSON logs. Set LUNORA_AGENT_MODE=0 to opt out.`);const u=$e({configPreference:Ee(r),envValue:process.env.LUNORA_REMOTE,flag:o.remote});if(!i&&(o.background===!0||a!==void 0)){const p=y(r);return p!==void 0?(S(t,p),{code:0}):Dr({cwd:r,jsonLogs:c,logger:t,options:o,remote:u})}return to({apiSpec:Fe(o.apiSpec),codegen:o.codegen===!1?!1:void 0,cwd:r,logger:t,port:o.port,remote:u,studio:o.studio===!1?!1:void 0,workerPort:o.workerPort})});export{O as detectDevFlavor,bo as execute,Vr as planDevCommand,Hr as resolveRemotePlan,Ur as resolveWorkerPort,to as runDevCommand};