@lunora/cli 1.0.0-alpha.6 → 1.0.0-alpha.60
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/__assets__/package-og.svg +1 -1
- package/dist/bin.mjs +1 -1
- package/dist/index.d.mts +238 -115
- package/dist/index.d.ts +238 -115
- package/dist/index.mjs +8 -8
- package/dist/packem_chunks/handler.mjs +87 -6
- package/dist/packem_chunks/handler10.mjs +8 -14
- package/dist/packem_chunks/handler11.mjs +19 -189
- package/dist/packem_chunks/handler12.mjs +176 -115
- package/dist/packem_chunks/handler13.mjs +118 -52
- package/dist/packem_chunks/handler14.mjs +50 -43
- package/dist/packem_chunks/handler15.mjs +48 -67
- package/dist/packem_chunks/handler16.mjs +74 -37
- package/dist/packem_chunks/handler17.mjs +39 -100
- package/dist/packem_chunks/handler18.mjs +87 -152
- package/dist/packem_chunks/handler19.mjs +148 -67
- package/dist/packem_chunks/handler2.mjs +6 -4
- package/dist/packem_chunks/handler20.mjs +77 -75
- package/dist/packem_chunks/handler21.mjs +71 -288
- package/dist/packem_chunks/handler3.mjs +6 -3
- package/dist/packem_chunks/handler4.mjs +2 -2
- package/dist/packem_chunks/handler5.mjs +7 -4
- package/dist/packem_chunks/handler6.mjs +10 -7
- package/dist/packem_chunks/handler7.mjs +2 -2
- package/dist/packem_chunks/handler8.mjs +1 -1
- package/dist/packem_chunks/handler9.mjs +315 -12
- package/dist/packem_chunks/planDevCommand.mjs +561 -32
- package/dist/packem_chunks/runCodegenCommand.mjs +5 -4
- package/dist/packem_chunks/runDeployCommand.mjs +115 -20
- package/dist/packem_chunks/runInitCommand.mjs +1601 -172
- package/dist/packem_chunks/runMigrateGenerateCommand.mjs +5 -4
- package/dist/packem_chunks/runResetCommand.mjs +2 -2
- package/dist/packem_chunks/runRpcCommand.mjs +3 -2
- package/dist/packem_shared/{COMMANDS-Bn8luojF.mjs → COMMANDS-0BD3K1pb.mjs} +79 -21
- package/dist/packem_shared/{DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs → DEFAULT_IMPORT_BATCH_SIZE-CRQmmBZM.mjs} +3 -2
- package/dist/packem_shared/{api-spec-CtA6ilu4.mjs → api-spec-Bx0iKbxA.mjs} +3 -1
- package/dist/packem_shared/{buildRegistryIndex-BcYe607_.mjs → buildRegistryIndex-CqM5FwGI.mjs} +1 -1
- package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +31 -0
- package/dist/packem_shared/{command-BDXcJCCJ.mjs → command-lYnl4QyF.mjs} +6 -1
- package/dist/packem_shared/{commands-DqsEzojt.mjs → commands-vnRRkhNE.mjs} +123 -15
- package/dist/packem_shared/{createLogger-CHPNjFw2.mjs → createLogger-CIWSHrTL.mjs} +40 -8
- package/dist/packem_shared/{createRecordingSpawner-DxI3mebw.mjs → createRecordingSpawner-Cw5Iu73G.mjs} +12 -2
- package/dist/packem_shared/{detect-package-manager-DYp7n3mJ.mjs → detect-package-manager-Cxo6Tpyx.mjs} +10 -1
- package/dist/packem_shared/{insertSchemaExtension-BuzF6-t2.mjs → insertSchemaExtension-DAqbfr9Z.mjs} +15 -10
- package/dist/packem_shared/{open-url-Dfq6fAyT.mjs → open-url-4PBLY9X0.mjs} +3 -2
- package/dist/packem_shared/{output-format-7gyGR3h8.mjs → output-format-B4642rjE.mjs} +1 -1
- package/dist/packem_shared/{parseManifest--vZf2FY1.mjs → parseManifest-BBrXCg9V.mjs} +16 -9
- package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +9 -0
- package/dist/packem_shared/runAddCommand-BCcJzs_k.mjs +4 -0
- package/dist/packem_shared/storage-bGfgwOgF.mjs +84 -0
- package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +658 -0
- package/dist/packem_shared/wrangler-secrets-P2_ZUR-k.mjs +47 -0
- package/package.json +8 -6
- package/skills/lunora-quickstart/SKILL.md +25 -5
- package/skills/lunora-setup-storage/SKILL.md +7 -3
- package/dist/packem_shared/features-ocSSpZtS.mjs +0 -24
- package/dist/packem_shared/runAddCommand-G544_v6e.mjs +0 -4
- package/dist/packem_shared/tui-prompts-XHFxlOg5.mjs +0 -269
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { detectAgentRules, resolveRemoteEnabled, readProjectRemotePreference, inferLunoraBindings, packageNamesFromBindings, ensureDevVarsExample, ensureDevVariables, isInteractive, DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, claimAgentRulesHint, AGENT_RULES_HINT, materializeRemoteWranglerConfig, formatLunoraEvent } from '@lunora/config';
|
|
3
|
-
import { p as parseApiSpec } from '../packem_shared/api-spec-
|
|
4
|
-
import { existsSync, watch } from 'node:fs';
|
|
5
|
-
import { join } from 'node:path';
|
|
1
|
+
import { spawnSync, spawn } from 'node:child_process';
|
|
2
|
+
import { detectAgentRules, claimDevServerState, clearDevServerState, readDevServerState, readLiveDevServerState, readProjectDependencyNames, DEV_HANDOFF_ENV, isRecordedProcessCurrent, DEV_LOG_FILE, DEV_LOG_FILE_ENV, DEV_DAEMON_ENV, detectAiAgent, resolveRemoteEnabled, readProjectRemotePreference, detectFramework, inferLunoraBindings, packageNamesFromBindings, ensureDevVarsExample, ensureDevVariables, isInteractive, DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, fillDevSecrets, updateDevServerState, claimAgentRulesHint, AGENT_RULES_HINT, discoverContainerInfo, streamContainerLogs, materializeRemoteWranglerConfig, formatLunoraEvent } from '@lunora/config';
|
|
3
|
+
import { p as parseApiSpec } from '../packem_shared/api-spec-Bx0iKbxA.mjs';
|
|
4
|
+
import { existsSync, watch, readFileSync, statSync, openSync, readSync, closeSync, mkdirSync } from 'node:fs';
|
|
5
|
+
import { join, dirname } from 'node:path';
|
|
6
6
|
import { runCodegen } from '@lunora/codegen';
|
|
7
|
-
import {
|
|
8
|
-
import { d as
|
|
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-Cxo6Tpyx.mjs';
|
|
10
|
+
import { forceJsonLogging } from '../packem_shared/createLogger-CIWSHrTL.mjs';
|
|
11
|
+
import { spawnShellCompat } from '../packem_shared/createRecordingSpawner-Cw5Iu73G.mjs';
|
|
9
12
|
import { createServer, request } from 'node:http';
|
|
10
13
|
import { connect } from 'node:net';
|
|
11
|
-
import { loadStudioAssets, studioAssetsStamp, renderStudioHtml, resolveAdminToken, SCHEMA_EDIT_ENDPOINT, POLICY_SCAFFOLD_ENDPOINT, SEED_ENDPOINT, serveJsonHandler, handleSchemaEditRequest, handlePolicyScaffoldRequest, handleSeedRequest } from '@lunora/config/studio-host';
|
|
12
|
-
import { c as createTuiConfirm } from '../packem_shared/tui-prompts-
|
|
14
|
+
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';
|
|
15
|
+
import { c as createTuiConfirm } from '../packem_shared/tui-prompts-BjEN8XgP.mjs';
|
|
16
|
+
import { p as printJson } from '../packem_shared/output-format-B4642rjE.mjs';
|
|
13
17
|
|
|
14
18
|
const DEFAULT_DEBOUNCE_MS = 100;
|
|
15
19
|
const PATH_SEGMENT_SEPARATOR = /[/\\]/u;
|
|
@@ -18,7 +22,7 @@ const runOnce = (projectRoot, lunoraDirectory, apiSpec, logger, reason) => {
|
|
|
18
22
|
runCodegen({ apiSpec, lunoraDirectory, projectRoot });
|
|
19
23
|
logger.success(`codegen: wrote ${lunoraDirectory}/_generated (${reason})`);
|
|
20
24
|
} catch (error) {
|
|
21
|
-
logger.error(
|
|
25
|
+
logger.error(renderCodegenFailure(error, reason));
|
|
22
26
|
}
|
|
23
27
|
};
|
|
24
28
|
const startCodegenWatch = (options) => {
|
|
@@ -165,7 +169,9 @@ const startStudioServer = async (options) => {
|
|
|
165
169
|
response.end(deniedMessage);
|
|
166
170
|
};
|
|
167
171
|
const serveStaticAsset = (pathname, response) => {
|
|
168
|
-
|
|
172
|
+
const isStyle = pathname === "/styles.css";
|
|
173
|
+
const isModule = isStandaloneModulePath(pathname);
|
|
174
|
+
if (!isStyle && !isModule) {
|
|
169
175
|
return false;
|
|
170
176
|
}
|
|
171
177
|
const stamp = studioAssetsStamp(import.meta.url);
|
|
@@ -179,8 +185,19 @@ const startStudioServer = async (options) => {
|
|
|
179
185
|
response.end("Lunora studio assets not found — install and build @lunora/studio.");
|
|
180
186
|
return true;
|
|
181
187
|
}
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
if (isStyle) {
|
|
189
|
+
sendAsset(response, assets.styles, "text/css; charset=utf-8");
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
const fileName = pathname.slice(pathname.lastIndexOf("/") + 1);
|
|
193
|
+
const bytes = readStandaloneAsset(fileName, import.meta.url);
|
|
194
|
+
if (bytes === void 0) {
|
|
195
|
+
response.statusCode = 404;
|
|
196
|
+
response.setHeader("Content-Type", "text/plain");
|
|
197
|
+
response.end("Not found");
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
sendAsset(response, bytes, assetContentType(fileName));
|
|
184
201
|
return true;
|
|
185
202
|
};
|
|
186
203
|
const document = Buffer.from(html);
|
|
@@ -256,8 +273,386 @@ const startStudioServer = async (options) => {
|
|
|
256
273
|
};
|
|
257
274
|
};
|
|
258
275
|
|
|
276
|
+
const DEFAULT_READY_TIMEOUT_MS = 12e4;
|
|
277
|
+
const POLL_INTERVAL_MS = 250;
|
|
278
|
+
const STOP_GRACE_MS = 1e4;
|
|
279
|
+
const DEFAULT_LOG_LINES = 100;
|
|
280
|
+
const FAILURE_LOG_TAIL_LINES = 40;
|
|
281
|
+
const LOG_TAIL_MAX_BYTES = 256 * 1024;
|
|
282
|
+
const READY_TIMEOUT_ENV = "LUNORA_DEV_READY_TIMEOUT_MS";
|
|
283
|
+
const detectDevFlavor = (cwd) => readProjectDependencyNames(cwd).has("@lunora/vite") ? "vite" : "wrangler";
|
|
284
|
+
const viteDevCommand = (cwd) => {
|
|
285
|
+
const manager = detectPackageManager(cwd);
|
|
286
|
+
let script;
|
|
287
|
+
try {
|
|
288
|
+
const raw = readFileSync(join(cwd, "package.json"), "utf8");
|
|
289
|
+
script = JSON.parse(raw).scripts?.dev;
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
if (script === void 0 || script.trim() === "" || script.includes("lunora")) {
|
|
293
|
+
const exec = execArgsFor(manager, "vite", ["dev"]);
|
|
294
|
+
return { args: exec.args, command: exec.command };
|
|
295
|
+
}
|
|
296
|
+
return { args: ["run", "dev"], command: manager };
|
|
297
|
+
};
|
|
298
|
+
const sleep = (ms) => new Promise((resolve) => {
|
|
299
|
+
setTimeout(resolve, ms);
|
|
300
|
+
});
|
|
301
|
+
const defaultProbe = async (origin) => {
|
|
302
|
+
try {
|
|
303
|
+
await fetch(new URL("/_lunora/status", origin), { signal: AbortSignal.timeout(1e3) });
|
|
304
|
+
return true;
|
|
305
|
+
} catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const defaultDetachedSpawner = (descriptor) => {
|
|
310
|
+
mkdirSync(dirname(descriptor.logPath), { recursive: true });
|
|
311
|
+
const logFd = openSync(descriptor.logPath, "w", 384);
|
|
312
|
+
let child;
|
|
313
|
+
const exec = spawnShellCompat(descriptor.command, descriptor.args);
|
|
314
|
+
try {
|
|
315
|
+
child = spawn(exec.command, exec.args, {
|
|
316
|
+
cwd: descriptor.cwd,
|
|
317
|
+
detached: true,
|
|
318
|
+
env: descriptor.env,
|
|
319
|
+
shell: exec.shell,
|
|
320
|
+
stdio: ["ignore", logFd, logFd],
|
|
321
|
+
windowsHide: true
|
|
322
|
+
});
|
|
323
|
+
} finally {
|
|
324
|
+
closeSync(logFd);
|
|
325
|
+
}
|
|
326
|
+
child.unref();
|
|
327
|
+
return {
|
|
328
|
+
exited: new Promise((resolve) => {
|
|
329
|
+
child.on("error", () => {
|
|
330
|
+
resolve(1);
|
|
331
|
+
});
|
|
332
|
+
child.on("exit", (code, signal) => {
|
|
333
|
+
resolve(code ?? (signal ? 1 : 0));
|
|
334
|
+
});
|
|
335
|
+
}),
|
|
336
|
+
pid: child.pid
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
const readLogWindow = (path) => {
|
|
340
|
+
try {
|
|
341
|
+
const { size } = statSync(path);
|
|
342
|
+
if (size <= LOG_TAIL_MAX_BYTES) {
|
|
343
|
+
return readFileSync(path, "utf8");
|
|
344
|
+
}
|
|
345
|
+
const fd = openSync(path, "r");
|
|
346
|
+
try {
|
|
347
|
+
const buffer = Buffer.alloc(LOG_TAIL_MAX_BYTES);
|
|
348
|
+
const read = readSync(fd, buffer, 0, LOG_TAIL_MAX_BYTES, size - LOG_TAIL_MAX_BYTES);
|
|
349
|
+
const text = buffer.toString("utf8", 0, read);
|
|
350
|
+
return text.slice(text.indexOf("\n") + 1);
|
|
351
|
+
} finally {
|
|
352
|
+
closeSync(fd);
|
|
353
|
+
}
|
|
354
|
+
} catch {
|
|
355
|
+
return void 0;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
const readLogTail = (path, count) => {
|
|
359
|
+
const text = readLogWindow(path);
|
|
360
|
+
if (text === void 0) {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
const lines = text.split("\n");
|
|
364
|
+
if (lines.at(-1) === "") {
|
|
365
|
+
lines.pop();
|
|
366
|
+
}
|
|
367
|
+
return count > 0 ? lines.slice(-count) : lines;
|
|
368
|
+
};
|
|
369
|
+
const resolveLogPath = (cwd, state) => state?.logFile ?? join(cwd, DEV_LOG_FILE);
|
|
370
|
+
const printLifecycleHints = (logger) => {
|
|
371
|
+
logger.info(" Stop: lunora dev stop");
|
|
372
|
+
logger.info(" Status: lunora dev status");
|
|
373
|
+
logger.info(" Logs: lunora dev logs");
|
|
374
|
+
};
|
|
375
|
+
const reportExistingServer = (logger, existing) => {
|
|
376
|
+
logger.warn(`Dev server already running at ${existing.url} (pid ${String(existing.pid)})`);
|
|
377
|
+
printLifecycleHints(logger);
|
|
378
|
+
};
|
|
379
|
+
const printBackgroundBanner = (logger, state) => {
|
|
380
|
+
logger.success(`Dev server running at ${state.url} (pid ${String(state.pid)})`);
|
|
381
|
+
if (state.studioUrl !== void 0) {
|
|
382
|
+
logger.info(` Studio: ${state.studioUrl}`);
|
|
383
|
+
}
|
|
384
|
+
printLifecycleHints(logger);
|
|
385
|
+
};
|
|
386
|
+
const waitUntilReady = async (parameters) => {
|
|
387
|
+
const { child, cwd, deadline, pollIntervalMs, probe } = parameters;
|
|
388
|
+
const exited = child.exited.then((code) => {
|
|
389
|
+
return { exitCode: code };
|
|
390
|
+
});
|
|
391
|
+
while (Date.now() < deadline) {
|
|
392
|
+
const state = readLiveDevServerState(cwd);
|
|
393
|
+
if (state !== void 0 && state.pid !== process.pid && await probe(state.url)) {
|
|
394
|
+
return { state, status: "ready" };
|
|
395
|
+
}
|
|
396
|
+
const raced = await Promise.race([exited, sleep(pollIntervalMs)]);
|
|
397
|
+
if (raced !== void 0) {
|
|
398
|
+
return { ...raced, status: "exited" };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
return { status: "timeout" };
|
|
402
|
+
};
|
|
403
|
+
const runDevBackground = async (options) => {
|
|
404
|
+
const { cwd, logger } = options;
|
|
405
|
+
const logPath = resolveLogPath(cwd, void 0);
|
|
406
|
+
const environmentTimeout = Number(process.env[READY_TIMEOUT_ENV] ?? "");
|
|
407
|
+
const timeout = options.readyTimeoutMs ?? (Number.isFinite(environmentTimeout) && environmentTimeout > 0 ? environmentTimeout : DEFAULT_READY_TIMEOUT_MS);
|
|
408
|
+
const child = (options.spawnDetached ?? defaultDetachedSpawner)({
|
|
409
|
+
args: options.command.args,
|
|
410
|
+
command: options.command.command,
|
|
411
|
+
cwd,
|
|
412
|
+
env: {
|
|
413
|
+
...process.env,
|
|
414
|
+
...options.env,
|
|
415
|
+
[DEV_DAEMON_ENV]: "1",
|
|
416
|
+
[DEV_LOG_FILE_ENV]: logPath,
|
|
417
|
+
// The capture log is read back by `lunora dev logs` (and often an
|
|
418
|
+
// agent) — ANSI colour codes would only garble it.
|
|
419
|
+
...process.env.FORCE_COLOR === void 0 ? { NO_COLOR: "1" } : {},
|
|
420
|
+
...options.json ? { LUNORA_LOG_JSON: "1" } : {}
|
|
421
|
+
},
|
|
422
|
+
logPath
|
|
423
|
+
});
|
|
424
|
+
const outcome = await waitUntilReady({
|
|
425
|
+
child,
|
|
426
|
+
cwd,
|
|
427
|
+
deadline: Date.now() + timeout,
|
|
428
|
+
pollIntervalMs: options.pollIntervalMs ?? POLL_INTERVAL_MS,
|
|
429
|
+
probe: options.probe ?? defaultProbe
|
|
430
|
+
});
|
|
431
|
+
if (outcome.status === "ready") {
|
|
432
|
+
printBackgroundBanner(logger, outcome.state);
|
|
433
|
+
return { code: 0 };
|
|
434
|
+
}
|
|
435
|
+
if (outcome.status === "exited") {
|
|
436
|
+
logger.error(`dev server exited before becoming ready (exit code ${String(outcome.exitCode)}). Last output:`);
|
|
437
|
+
for (const line of readLogTail(logPath, FAILURE_LOG_TAIL_LINES)) {
|
|
438
|
+
logger.error(` ${line}`);
|
|
439
|
+
}
|
|
440
|
+
clearDevServerState(cwd, child.pid);
|
|
441
|
+
return { code: outcome.exitCode === 0 ? 1 : outcome.exitCode };
|
|
442
|
+
}
|
|
443
|
+
logger.warn(
|
|
444
|
+
`dev server did not confirm ready within ${String(Math.round(timeout / 1e3))}s — it may still be compiling. Check \`lunora dev status\` and \`lunora dev logs\`; \`lunora dev stop\` shuts it down.`
|
|
445
|
+
);
|
|
446
|
+
return { code: 1 };
|
|
447
|
+
};
|
|
448
|
+
const daemonArguments = (options, remote) => {
|
|
449
|
+
const args = ["dev"];
|
|
450
|
+
if (options.apiSpec !== void 0) {
|
|
451
|
+
args.push("--api-spec", options.apiSpec);
|
|
452
|
+
}
|
|
453
|
+
if (options.port !== void 0) {
|
|
454
|
+
args.push("--port", String(options.port));
|
|
455
|
+
}
|
|
456
|
+
if (options.workerPort !== void 0) {
|
|
457
|
+
args.push("--worker-port", String(options.workerPort));
|
|
458
|
+
}
|
|
459
|
+
if (options.codegen === false) {
|
|
460
|
+
args.push("--no-codegen");
|
|
461
|
+
}
|
|
462
|
+
if (options.studio === false) {
|
|
463
|
+
args.push("--no-studio");
|
|
464
|
+
}
|
|
465
|
+
if (remote) {
|
|
466
|
+
args.push("--remote");
|
|
467
|
+
}
|
|
468
|
+
return args;
|
|
469
|
+
};
|
|
470
|
+
const startBackground = async (context) => {
|
|
471
|
+
const { cwd, jsonLogs, logger, options, remote } = context;
|
|
472
|
+
const run = context.run ?? runDevBackground;
|
|
473
|
+
const flavor = detectDevFlavor(cwd);
|
|
474
|
+
const url = flavor === "vite" ? "http://localhost:5173" : `http://localhost:${String(options.workerPort ?? 8787)}`;
|
|
475
|
+
const claim = claimDevServerState(cwd, {
|
|
476
|
+
background: true,
|
|
477
|
+
mode: "cli",
|
|
478
|
+
pid: process.pid,
|
|
479
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
480
|
+
url
|
|
481
|
+
});
|
|
482
|
+
if (!claim.ok) {
|
|
483
|
+
if (claim.existing !== void 0) {
|
|
484
|
+
reportExistingServer(logger, claim.existing);
|
|
485
|
+
}
|
|
486
|
+
return { code: 0 };
|
|
487
|
+
}
|
|
488
|
+
const handoff = { [DEV_HANDOFF_ENV]: String(process.pid) };
|
|
489
|
+
try {
|
|
490
|
+
if (flavor === "vite") {
|
|
491
|
+
return await run({
|
|
492
|
+
command: viteDevCommand(cwd),
|
|
493
|
+
cwd,
|
|
494
|
+
env: { ...remote ? { LUNORA_REMOTE: "1" } : {}, ...handoff },
|
|
495
|
+
json: jsonLogs,
|
|
496
|
+
logger
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
return await run({
|
|
500
|
+
command: { args: [process.argv[1] ?? "lunora", ...daemonArguments(options, remote)], command: process.execPath },
|
|
501
|
+
cwd,
|
|
502
|
+
env: handoff,
|
|
503
|
+
json: jsonLogs,
|
|
504
|
+
logger
|
|
505
|
+
});
|
|
506
|
+
} finally {
|
|
507
|
+
clearDevServerState(cwd, process.pid);
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
const processGroupId = (pid) => {
|
|
511
|
+
try {
|
|
512
|
+
const result = spawnSync("ps", ["-o", "pgid=", "-p", String(pid)], { encoding: "utf8" });
|
|
513
|
+
const pgid = Number.parseInt(result.stdout.trim(), 10);
|
|
514
|
+
return Number.isInteger(pgid) && pgid > 0 ? pgid : void 0;
|
|
515
|
+
} catch {
|
|
516
|
+
return void 0;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
const forceKillRecordedServer = (state, signal) => {
|
|
520
|
+
if (process.platform === "win32") {
|
|
521
|
+
try {
|
|
522
|
+
spawnSync("taskkill", ["/pid", String(state.pid), "/T", "/F"], { stdio: "ignore" });
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
if (state.background === true) {
|
|
528
|
+
try {
|
|
529
|
+
signal(-(processGroupId(state.pid) ?? state.pid), "SIGKILL");
|
|
530
|
+
return;
|
|
531
|
+
} catch {
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
signal(state.pid, "SIGKILL");
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
const runDevStop = async (options) => {
|
|
540
|
+
const { cwd, logger } = options;
|
|
541
|
+
const state = readDevServerState(cwd);
|
|
542
|
+
const alive = options.alive ?? ((pid) => pid === state?.pid && isRecordedProcessCurrent(state));
|
|
543
|
+
const signal = options.signal ?? ((pid, sig) => {
|
|
544
|
+
process.kill(pid, sig);
|
|
545
|
+
});
|
|
546
|
+
const pollInterval = options.pollIntervalMs ?? POLL_INTERVAL_MS;
|
|
547
|
+
const grace = options.stopGraceMs ?? STOP_GRACE_MS;
|
|
548
|
+
if (state === void 0 || !alive(state.pid)) {
|
|
549
|
+
if (state !== void 0) {
|
|
550
|
+
clearDevServerState(cwd, state.pid);
|
|
551
|
+
}
|
|
552
|
+
if (options.json) {
|
|
553
|
+
printJson({ running: false, stopped: false });
|
|
554
|
+
} else {
|
|
555
|
+
logger.info("No dev server running.");
|
|
556
|
+
}
|
|
557
|
+
return { code: 0 };
|
|
558
|
+
}
|
|
559
|
+
try {
|
|
560
|
+
signal(state.pid, "SIGTERM");
|
|
561
|
+
} catch {
|
|
562
|
+
}
|
|
563
|
+
const deadline = Date.now() + grace;
|
|
564
|
+
while (alive(state.pid) && Date.now() < deadline) {
|
|
565
|
+
await sleep(pollInterval);
|
|
566
|
+
}
|
|
567
|
+
if (alive(state.pid)) {
|
|
568
|
+
forceKillRecordedServer(state, signal);
|
|
569
|
+
}
|
|
570
|
+
clearDevServerState(cwd, state.pid);
|
|
571
|
+
if (options.json) {
|
|
572
|
+
printJson({ pid: state.pid, stopped: true });
|
|
573
|
+
} else {
|
|
574
|
+
logger.success(`Stopped dev server (pid ${String(state.pid)}).`);
|
|
575
|
+
}
|
|
576
|
+
return { code: 0 };
|
|
577
|
+
};
|
|
578
|
+
const runDevStatus = (options) => {
|
|
579
|
+
const { cwd, logger } = options;
|
|
580
|
+
const state = readLiveDevServerState(cwd);
|
|
581
|
+
if (state === void 0) {
|
|
582
|
+
if (options.json) {
|
|
583
|
+
printJson({ running: false });
|
|
584
|
+
} else {
|
|
585
|
+
logger.info("No dev server running.");
|
|
586
|
+
}
|
|
587
|
+
return { code: 0 };
|
|
588
|
+
}
|
|
589
|
+
const now = options.now ?? Date.now;
|
|
590
|
+
const startedAtMs = state.startedAt === void 0 ? Number.NaN : Date.parse(state.startedAt);
|
|
591
|
+
const uptimeSeconds = Number.isFinite(startedAtMs) ? Math.max(0, Math.round((now() - startedAtMs) / 1e3)) : void 0;
|
|
592
|
+
if (options.json) {
|
|
593
|
+
printJson({
|
|
594
|
+
background: state.background === true,
|
|
595
|
+
logFile: state.logFile,
|
|
596
|
+
mode: state.mode,
|
|
597
|
+
pid: state.pid,
|
|
598
|
+
running: true,
|
|
599
|
+
startedAt: state.startedAt,
|
|
600
|
+
studioUrl: state.studioUrl,
|
|
601
|
+
uptimeSeconds,
|
|
602
|
+
url: state.url
|
|
603
|
+
});
|
|
604
|
+
return { code: 0 };
|
|
605
|
+
}
|
|
606
|
+
const details = [
|
|
607
|
+
`pid ${String(state.pid)}`,
|
|
608
|
+
...uptimeSeconds === void 0 ? [] : [`uptime ${String(uptimeSeconds)}s`],
|
|
609
|
+
state.background === true ? "background" : "foreground"
|
|
610
|
+
];
|
|
611
|
+
logger.success(`Dev server running at ${state.url} (${details.join(", ")})`);
|
|
612
|
+
if (state.studioUrl !== void 0) {
|
|
613
|
+
logger.info(` Studio: ${state.studioUrl}`);
|
|
614
|
+
}
|
|
615
|
+
if (state.logFile !== void 0) {
|
|
616
|
+
logger.info(` Logs: lunora dev logs (${state.logFile})`);
|
|
617
|
+
}
|
|
618
|
+
return { code: 0 };
|
|
619
|
+
};
|
|
620
|
+
const runDevLogs = (options) => {
|
|
621
|
+
const { cwd, logger } = options;
|
|
622
|
+
const state = readDevServerState(cwd);
|
|
623
|
+
const logPath = resolveLogPath(cwd, state);
|
|
624
|
+
if (!existsSync(logPath)) {
|
|
625
|
+
logger.info("No dev server logs found — output is captured when the server runs in background mode (`lunora dev --background`).");
|
|
626
|
+
return { code: 0 };
|
|
627
|
+
}
|
|
628
|
+
const tail = readLogTail(logPath, options.lines ?? DEFAULT_LOG_LINES);
|
|
629
|
+
if (tail.length > 0) {
|
|
630
|
+
process.stdout.write(`${tail.join("\n")}
|
|
631
|
+
`);
|
|
632
|
+
}
|
|
633
|
+
return { code: 0 };
|
|
634
|
+
};
|
|
635
|
+
const runLifecycleSubcommand = (parameters) => {
|
|
636
|
+
const { cwd, json, lines, logger, subcommand } = parameters;
|
|
637
|
+
if (subcommand === "stop") {
|
|
638
|
+
return runDevStop({ cwd, json, logger });
|
|
639
|
+
}
|
|
640
|
+
if (subcommand === "status") {
|
|
641
|
+
return runDevStatus({ cwd, json, logger });
|
|
642
|
+
}
|
|
643
|
+
if (subcommand === "logs") {
|
|
644
|
+
return runDevLogs({ cwd, lines, logger });
|
|
645
|
+
}
|
|
646
|
+
if (subcommand !== void 0) {
|
|
647
|
+
logger.error(`dev: unknown subcommand "${subcommand}" — expected stop | status | logs (or no subcommand to start the dev server)`);
|
|
648
|
+
return { code: 1 };
|
|
649
|
+
}
|
|
650
|
+
return void 0;
|
|
651
|
+
};
|
|
652
|
+
|
|
259
653
|
const DEFAULT_STUDIO_PORT = 6173;
|
|
260
654
|
const DEFAULT_WORKER_PORT = 8787;
|
|
655
|
+
const DEFAULT_VITE_PORT = 5173;
|
|
261
656
|
const SIGINT_GRACE_MS = 5e3;
|
|
262
657
|
const resolveRemotePlan = (options, cwd) => {
|
|
263
658
|
const noopCleanup = () => {
|
|
@@ -276,12 +671,37 @@ const resolveRemotePlan = (options, cwd) => {
|
|
|
276
671
|
};
|
|
277
672
|
const planDevCommand = (options) => {
|
|
278
673
|
const cwd = options.cwd ?? process.cwd();
|
|
279
|
-
const workerPort = options.workerPort ?? DEFAULT_WORKER_PORT;
|
|
280
674
|
const manager = detectPackageManager(cwd);
|
|
675
|
+
const flavor = options.flavor ?? detectDevFlavor(cwd);
|
|
676
|
+
if (flavor === "vite") {
|
|
677
|
+
const exec2 = viteDevCommand(cwd);
|
|
678
|
+
return {
|
|
679
|
+
codegenEnabled: false,
|
|
680
|
+
flavor,
|
|
681
|
+
remote: { bindings: [], cleanup: () => {
|
|
682
|
+
}, enabled: options.remote === true },
|
|
683
|
+
studioEnabled: false,
|
|
684
|
+
studioPort: options.port ?? DEFAULT_STUDIO_PORT,
|
|
685
|
+
workerOrigin: `http://localhost:${String(DEFAULT_VITE_PORT)}`,
|
|
686
|
+
workerPort: DEFAULT_VITE_PORT,
|
|
687
|
+
wrangler: {
|
|
688
|
+
args: exec2.args,
|
|
689
|
+
command: exec2.command,
|
|
690
|
+
cwd,
|
|
691
|
+
...options.remote === true ? { env: { LUNORA_REMOTE: "1" } } : {},
|
|
692
|
+
tag: "vite"
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
const detection = detectFramework(cwd);
|
|
697
|
+
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.`;
|
|
698
|
+
const workerPort = options.workerPort ?? DEFAULT_WORKER_PORT;
|
|
281
699
|
const remote = resolveRemotePlan(options, cwd);
|
|
282
700
|
const exec = execArgsFor(manager, "wrangler", ["dev", "--port", String(workerPort), "--var", "WORKER_ENV:development", ...remote.args]);
|
|
283
701
|
return {
|
|
284
702
|
codegenEnabled: options.codegen !== false,
|
|
703
|
+
flavor,
|
|
704
|
+
frameworkHint,
|
|
285
705
|
remote: remote.plan,
|
|
286
706
|
studioEnabled: options.studio !== false,
|
|
287
707
|
studioPort: options.port ?? DEFAULT_STUDIO_PORT,
|
|
@@ -329,9 +749,11 @@ const pipeChildOutput = (child, tag, logger) => {
|
|
|
329
749
|
pumpStream(child.stderr, "stderr");
|
|
330
750
|
};
|
|
331
751
|
const defaultWorkerSpawner = (descriptor, logger) => {
|
|
332
|
-
const
|
|
752
|
+
const exec = spawnShellCompat(descriptor.command, descriptor.args);
|
|
753
|
+
const child = spawn(exec.command, exec.args, {
|
|
333
754
|
cwd: descriptor.cwd ?? process.cwd(),
|
|
334
|
-
env: process.env,
|
|
755
|
+
env: descriptor.env ? { ...process.env, ...descriptor.env } : process.env,
|
|
756
|
+
shell: exec.shell,
|
|
335
757
|
stdio: ["inherit", "pipe", "pipe"]
|
|
336
758
|
});
|
|
337
759
|
pipeChildOutput(child, descriptor.tag, logger);
|
|
@@ -379,8 +801,35 @@ const printAgentRulesHint = (logger, cwd) => {
|
|
|
379
801
|
logger.info(` ⓘ ${AGENT_RULES_HINT}`);
|
|
380
802
|
logger.info("");
|
|
381
803
|
};
|
|
804
|
+
const startContainerLogStreaming = (cwd, logger) => {
|
|
805
|
+
if (process.env.LUNORA_CONTAINER_LOGS === "0") {
|
|
806
|
+
return void 0;
|
|
807
|
+
}
|
|
808
|
+
const discovery = discoverContainerInfo(cwd, "lunora");
|
|
809
|
+
const containers = discovery.containers.map((container) => {
|
|
810
|
+
return { className: container.className, exportName: container.exportName };
|
|
811
|
+
});
|
|
812
|
+
if (containers.length === 0) {
|
|
813
|
+
return void 0;
|
|
814
|
+
}
|
|
815
|
+
return streamContainerLogs({
|
|
816
|
+
containers,
|
|
817
|
+
onLine: (line) => {
|
|
818
|
+
const tagged = `[container:${line.name}] ${line.text}`;
|
|
819
|
+
if (line.level === "error") {
|
|
820
|
+
logger.warn(tagged);
|
|
821
|
+
} else {
|
|
822
|
+
logger.info(tagged);
|
|
823
|
+
}
|
|
824
|
+
},
|
|
825
|
+
onUnavailable: (message) => {
|
|
826
|
+
logger.warn(`[container] Docker engine unreachable — container logs unavailable (${message})`);
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
};
|
|
382
830
|
const teardown = async (handles) => {
|
|
383
831
|
handles.codegen?.close();
|
|
832
|
+
handles.containerLogs?.close();
|
|
384
833
|
await handles.studio?.close().catch(() => void 0);
|
|
385
834
|
try {
|
|
386
835
|
handles.remoteCleanup?.();
|
|
@@ -409,6 +858,46 @@ const offerDevVariablesScaffold = async (options, cwd) => {
|
|
|
409
858
|
`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.`
|
|
410
859
|
);
|
|
411
860
|
}
|
|
861
|
+
try {
|
|
862
|
+
(options.fillSecrets ?? fillDevSecrets)({
|
|
863
|
+
cwd,
|
|
864
|
+
info: (message) => {
|
|
865
|
+
options.logger.info(message);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
} catch {
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
const afterWorkerSpawn = (plan, cwd, logger, studioUrl) => {
|
|
872
|
+
if (plan.flavor !== "wrangler") {
|
|
873
|
+
return void 0;
|
|
874
|
+
}
|
|
875
|
+
if (studioUrl !== void 0) {
|
|
876
|
+
updateDevServerState(cwd, { studioUrl });
|
|
877
|
+
}
|
|
878
|
+
let containerLogs;
|
|
879
|
+
try {
|
|
880
|
+
containerLogs = startContainerLogStreaming(cwd, logger);
|
|
881
|
+
} catch {
|
|
882
|
+
}
|
|
883
|
+
printBanner(logger, plan, studioUrl);
|
|
884
|
+
return containerLogs;
|
|
885
|
+
};
|
|
886
|
+
const claimStartRecord = (plan, cwd) => {
|
|
887
|
+
const handoffPid = Number(process.env[DEV_HANDOFF_ENV]);
|
|
888
|
+
const claim = claimDevServerState(
|
|
889
|
+
cwd,
|
|
890
|
+
{
|
|
891
|
+
background: process.env[DEV_DAEMON_ENV] === "1",
|
|
892
|
+
logFile: process.env[DEV_LOG_FILE_ENV],
|
|
893
|
+
mode: "cli",
|
|
894
|
+
pid: process.pid,
|
|
895
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
896
|
+
url: plan.workerOrigin
|
|
897
|
+
},
|
|
898
|
+
Number.isInteger(handoffPid) && handoffPid > 0 ? { supersedePid: handoffPid } : void 0
|
|
899
|
+
);
|
|
900
|
+
return claim.ok ? void 0 : claim.existing;
|
|
412
901
|
};
|
|
413
902
|
const runDevCommand = async (options) => {
|
|
414
903
|
const plan = planDevCommand(options);
|
|
@@ -416,8 +905,23 @@ const runDevCommand = async (options) => {
|
|
|
416
905
|
const cwd = plan.wrangler.cwd ?? process.cwd();
|
|
417
906
|
const handles = { remoteCleanup: plan.remote.cleanup };
|
|
418
907
|
try {
|
|
908
|
+
const existing = readLiveDevServerState(cwd);
|
|
909
|
+
if (existing !== void 0 && existing.pid !== process.pid) {
|
|
910
|
+
reportExistingServer(logger, existing);
|
|
911
|
+
return { code: 0, plan };
|
|
912
|
+
}
|
|
913
|
+
const incumbent = claimStartRecord(plan, cwd);
|
|
914
|
+
if (incumbent !== void 0) {
|
|
915
|
+
reportExistingServer(logger, incumbent);
|
|
916
|
+
return { code: 0, plan };
|
|
917
|
+
}
|
|
918
|
+
if (plan.flavor === "vite") {
|
|
919
|
+
plan.wrangler.env = { ...plan.wrangler.env, [DEV_HANDOFF_ENV]: String(process.pid) };
|
|
920
|
+
}
|
|
419
921
|
await offerDevVariablesScaffold(options, cwd);
|
|
420
|
-
logger.info(
|
|
922
|
+
logger.info(
|
|
923
|
+
plan.flavor === "vite" ? "starting vite dev (worker + studio + codegen run inside Vite via @lunora/vite)" : "starting wrangler dev + studio"
|
|
924
|
+
);
|
|
421
925
|
if (plan.codegenEnabled) {
|
|
422
926
|
handles.codegen = (options.startCodegen ?? startCodegenWatch)({ apiSpec: options.apiSpec, logger, projectRoot: cwd });
|
|
423
927
|
}
|
|
@@ -439,8 +943,11 @@ const runDevCommand = async (options) => {
|
|
|
439
943
|
logger.warn(`studio server failed to start (${error instanceof Error ? error.message : String(error)}) — continuing without it`);
|
|
440
944
|
}
|
|
441
945
|
}
|
|
946
|
+
if (plan.frameworkHint !== void 0) {
|
|
947
|
+
logger.warn(plan.frameworkHint);
|
|
948
|
+
}
|
|
442
949
|
const worker = (options.startWorker ?? defaultWorkerSpawner)(plan.wrangler, logger);
|
|
443
|
-
|
|
950
|
+
handles.containerLogs = afterWorkerSpawn(plan, cwd, logger, studioUrl);
|
|
444
951
|
printAgentRulesHint(logger, cwd);
|
|
445
952
|
let sigintCount = 0;
|
|
446
953
|
let escalationTimer;
|
|
@@ -470,11 +977,41 @@ const runDevCommand = async (options) => {
|
|
|
470
977
|
process.off("SIGTERM", onSigterm);
|
|
471
978
|
return { code, plan };
|
|
472
979
|
} finally {
|
|
980
|
+
clearDevServerState(cwd, process.pid);
|
|
473
981
|
await teardown(handles);
|
|
474
982
|
}
|
|
475
983
|
};
|
|
476
|
-
const execute = defineHandler(
|
|
477
|
-
|
|
984
|
+
const execute = defineHandler(async ({ argument, cwd, logger, options }) => {
|
|
985
|
+
const json = options.json === true;
|
|
986
|
+
const dispatched = runLifecycleSubcommand({ cwd, json, lines: options.lines, logger, subcommand: argument[0] });
|
|
987
|
+
if (dispatched !== void 0) {
|
|
988
|
+
return dispatched;
|
|
989
|
+
}
|
|
990
|
+
const isDaemon = process.env[DEV_DAEMON_ENV] === "1";
|
|
991
|
+
const agent = isDaemon ? void 0 : detectAiAgent();
|
|
992
|
+
const jsonLogs = json || agent !== void 0;
|
|
993
|
+
if (jsonLogs) {
|
|
994
|
+
forceJsonLogging();
|
|
995
|
+
}
|
|
996
|
+
if (agent !== void 0 && options.background !== true) {
|
|
997
|
+
logger.info(
|
|
998
|
+
`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.`
|
|
999
|
+
);
|
|
1000
|
+
}
|
|
1001
|
+
const remote = resolveRemoteEnabled({
|
|
1002
|
+
configPreference: readProjectRemotePreference(cwd),
|
|
1003
|
+
envValue: process.env["LUNORA_REMOTE"],
|
|
1004
|
+
flag: options.remote
|
|
1005
|
+
});
|
|
1006
|
+
if (!isDaemon && (options.background === true || agent !== void 0)) {
|
|
1007
|
+
const existing = readLiveDevServerState(cwd);
|
|
1008
|
+
if (existing !== void 0) {
|
|
1009
|
+
reportExistingServer(logger, existing);
|
|
1010
|
+
return { code: 0 };
|
|
1011
|
+
}
|
|
1012
|
+
return startBackground({ cwd, jsonLogs, logger, options, remote });
|
|
1013
|
+
}
|
|
1014
|
+
return runDevCommand({
|
|
478
1015
|
apiSpec: parseApiSpec(options.apiSpec),
|
|
479
1016
|
// cerebro parses `--no-codegen`/`--no-studio` as the negation of the
|
|
480
1017
|
// `codegen`/`studio` booleans (runtime key drops the `no-` prefix), so a
|
|
@@ -483,18 +1020,10 @@ const execute = defineHandler(
|
|
|
483
1020
|
cwd,
|
|
484
1021
|
logger,
|
|
485
1022
|
port: options.port,
|
|
486
|
-
|
|
487
|
-
// flag wins, then `LUNORA_REMOTE` in the environment, then the `remote`
|
|
488
|
-
// key in the project's `lunora.json` (a project default). See
|
|
489
|
-
// `resolveRemoteEnabled` in @lunora/config.
|
|
490
|
-
remote: resolveRemoteEnabled({
|
|
491
|
-
configPreference: readProjectRemotePreference(cwd),
|
|
492
|
-
envValue: process.env["LUNORA_REMOTE"],
|
|
493
|
-
flag: options.remote
|
|
494
|
-
}),
|
|
1023
|
+
remote,
|
|
495
1024
|
studio: options.studio === false ? false : void 0,
|
|
496
1025
|
workerPort: options.workerPort
|
|
497
|
-
})
|
|
498
|
-
);
|
|
1026
|
+
});
|
|
1027
|
+
});
|
|
499
1028
|
|
|
500
|
-
export { execute, planDevCommand, resolveRemotePlan, runDevCommand };
|
|
1029
|
+
export { detectDevFlavor, execute, planDevCommand, resolveRemotePlan, runDevCommand };
|