@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.
- package/dist/bin.mjs +2 -10
- package/dist/index.mjs +1 -18
- package/dist/packem_chunks/handler.mjs +1 -160
- package/dist/packem_chunks/handler10.mjs +1 -16
- package/dist/packem_chunks/handler11.mjs +1 -23
- package/dist/packem_chunks/handler12.mjs +2 -192
- package/dist/packem_chunks/handler13.mjs +2 -131
- package/dist/packem_chunks/handler14.mjs +1 -65
- package/dist/packem_chunks/handler15.mjs +1 -100
- package/dist/packem_chunks/handler16.mjs +1 -44
- package/dist/packem_chunks/handler17.mjs +1 -105
- package/dist/packem_chunks/handler18.mjs +3 -170
- package/dist/packem_chunks/handler19.mjs +1 -128
- package/dist/packem_chunks/handler2.mjs +2 -116
- package/dist/packem_chunks/handler20.mjs +1 -94
- package/dist/packem_chunks/handler21.mjs +2 -202
- package/dist/packem_chunks/handler3.mjs +2 -208
- package/dist/packem_chunks/handler4.mjs +1 -33
- package/dist/packem_chunks/handler5.mjs +1 -52
- package/dist/packem_chunks/handler6.mjs +1 -94
- package/dist/packem_chunks/handler7.mjs +1 -42
- package/dist/packem_chunks/handler8.mjs +1 -174
- package/dist/packem_chunks/handler9.mjs +8 -334
- package/dist/packem_chunks/planDevCommand.mjs +7 -1181
- package/dist/packem_chunks/runCodegenCommand.mjs +4 -53
- package/dist/packem_chunks/runDeployCommand.mjs +1 -619
- package/dist/packem_chunks/runInitCommand.mjs +51 -1411
- package/dist/packem_chunks/runMigrateGenerateCommand.mjs +9 -442
- package/dist/packem_chunks/runResetCommand.mjs +1 -41
- package/dist/packem_chunks/runRpcCommand.mjs +1 -69
- package/dist/packem_shared/COMMANDS-DRqlsGT2.mjs +3 -0
- package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-DL87PIEl.mjs +8 -0
- package/dist/packem_shared/admin-url-DfVehV3e.mjs +1 -0
- package/dist/packem_shared/api-spec-BENwiyUa.mjs +1 -0
- package/dist/packem_shared/buildRegistryIndex-DwySASBu.mjs +1 -0
- package/dist/packem_shared/codegen-error-DJN6pTH5.mjs +3 -0
- package/dist/packem_shared/command-Bjv4VmYA.mjs +1 -0
- package/dist/packem_shared/commands-BhX2Bo9B.mjs +19 -0
- package/dist/packem_shared/createLogger--C3x1FNE.mjs +2 -0
- package/dist/packem_shared/createRecordingSpawner-C8_7nffs.mjs +1 -0
- package/dist/packem_shared/detect-package-manager-B2eIuAiP.mjs +1 -0
- package/dist/packem_shared/diffSnapshots-9bDd9nOK.mjs +5 -0
- package/dist/packem_shared/docker-CKvYE9YF.mjs +1 -0
- package/dist/packem_shared/insertSchemaExtension-DZReBZ4_.mjs +8 -0
- package/dist/packem_shared/open-url-CTU_g9gr.mjs +1 -0
- package/dist/packem_shared/output-format-D7cKB-O5.mjs +2 -0
- package/dist/packem_shared/parseManifest-x3WsxKHz.mjs +1 -0
- package/dist/packem_shared/prompt-cancelled-C8Sj7OqI.mjs +1 -0
- package/dist/packem_shared/resolve-target-vxaXHmvg.mjs +1 -0
- package/dist/packem_shared/runAddCommand--TBYJQYi.mjs +1 -0
- package/dist/packem_shared/schema-drift-gate-CpqG-xP4.mjs +1 -0
- package/dist/packem_shared/schemaIrToSnapshot-Clwd2A8e.mjs +1 -0
- package/dist/packem_shared/storage-DOvAKGi4.mjs +1 -0
- package/dist/packem_shared/tui-prompts-CCt3jm1k.mjs +4 -0
- package/dist/packem_shared/wrangler-name-HIl0Z6yx.mjs +1 -0
- package/dist/packem_shared/wrangler-secrets-BMRJwKQU.mjs +1 -0
- package/package.json +6 -6
- package/dist/packem_shared/COMMANDS-BOJIDoVY.mjs +0 -1031
- package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-D0VOTerB.mjs +0 -249
- package/dist/packem_shared/admin-url-4UzT-CI4.mjs +0 -19
- package/dist/packem_shared/api-spec-Bx0iKbxA.mjs +0 -15
- package/dist/packem_shared/buildRegistryIndex-BS5ig822.mjs +0 -38
- package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +0 -31
- package/dist/packem_shared/command-lYnl4QyF.mjs +0 -19
- package/dist/packem_shared/commands-D5Yxt9VY.mjs +0 -991
- package/dist/packem_shared/createLogger-CIWSHrTL.mjs +0 -105
- package/dist/packem_shared/createRecordingSpawner-WuSn20kb.mjs +0 -64
- package/dist/packem_shared/detect-package-manager-v4hHpQd0.mjs +0 -62
- package/dist/packem_shared/diffSnapshots-BeDvvNiF.mjs +0 -161
- package/dist/packem_shared/docker-hMQ97KSQ.mjs +0 -21
- package/dist/packem_shared/insertSchemaExtension-DAqbfr9Z.mjs +0 -64
- package/dist/packem_shared/open-url-4PBLY9X0.mjs +0 -42
- package/dist/packem_shared/output-format-B4642rjE.mjs +0 -17
- package/dist/packem_shared/parseManifest-Dbp-Q2q3.mjs +0 -121
- package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +0 -9
- package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +0 -16
- package/dist/packem_shared/runAddCommand-D1hgfqFQ.mjs +0 -4
- package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +0 -79
- package/dist/packem_shared/schemaIrToSnapshot-DdsljJT-.mjs +0 -43
- package/dist/packem_shared/storage-BXU4ax4O.mjs +0 -84
- package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +0 -658
- package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +0 -12
- package/dist/packem_shared/wrangler-secrets-Coni-mER.mjs +0 -49
|
@@ -1,1181 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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};
|