@lunora/cli 1.0.0-alpha.10 → 1.0.0-alpha.101
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/LICENSE.md +6 -0
- package/__assets__/package-og.svg +1 -1
- package/dist/bin.mjs +1 -1
- package/dist/index.d.mts +345 -320
- package/dist/index.d.ts +345 -320
- package/dist/index.mjs +8 -9
- package/dist/packem_chunks/handler.mjs +15 -5
- package/dist/packem_chunks/handler10.mjs +8 -14
- package/dist/packem_chunks/handler11.mjs +20 -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 +27 -6
- package/dist/packem_chunks/handler16.mjs +3 -2
- package/dist/packem_chunks/handler17.mjs +1 -1
- package/dist/packem_chunks/handler18.mjs +3 -3
- package/dist/packem_chunks/handler19.mjs +44 -5
- package/dist/packem_chunks/handler2.mjs +6 -4
- package/dist/packem_chunks/handler20.mjs +2 -2
- package/dist/packem_chunks/handler21.mjs +158 -267
- package/dist/packem_chunks/handler3.mjs +8 -4
- 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 +330 -12
- package/dist/packem_chunks/planDevCommand.mjs +762 -81
- package/dist/packem_chunks/runCodegenCommand.mjs +5 -4
- package/dist/packem_chunks/runDeployCommand.mjs +137 -22
- package/dist/packem_chunks/runInitCommand.mjs +1193 -98
- package/dist/packem_chunks/runMigrateGenerateCommand.mjs +94 -47
- package/dist/packem_chunks/runResetCommand.mjs +2 -2
- package/dist/packem_chunks/runRpcCommand.mjs +3 -2
- package/dist/packem_shared/{COMMANDS-D3h9Iwvl.mjs → COMMANDS-BOJIDoVY.mjs} +112 -25
- package/dist/packem_shared/{DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs → DEFAULT_IMPORT_BATCH_SIZE-D0VOTerB.mjs} +7 -2
- package/dist/packem_shared/{api-spec-CtA6ilu4.mjs → api-spec-Bx0iKbxA.mjs} +3 -1
- package/dist/packem_shared/{buildRegistryIndex-BcYe607_.mjs → buildRegistryIndex-BS5ig822.mjs} +1 -1
- package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +31 -0
- package/dist/packem_shared/{command-BC30oSBW.mjs → command-lYnl4QyF.mjs} +6 -1
- package/dist/packem_shared/{commands-DPKWlqqX.mjs → commands-D5Yxt9VY.mjs} +199 -20
- package/dist/packem_shared/{createLogger-B40gPzQo.mjs → createLogger-CIWSHrTL.mjs} +33 -6
- package/dist/packem_shared/{createRecordingSpawner-DxI3mebw.mjs → createRecordingSpawner-WuSn20kb.mjs} +23 -2
- package/dist/packem_shared/detect-package-manager-v4hHpQd0.mjs +62 -0
- 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-wUvAN6AL.mjs → output-format-B4642rjE.mjs} +1 -1
- package/dist/packem_shared/{parseManifest--vZf2FY1.mjs → parseManifest-Dbp-Q2q3.mjs} +36 -9
- package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +9 -0
- package/dist/packem_shared/runAddCommand-D1hgfqFQ.mjs +4 -0
- package/dist/packem_shared/{storage-2RJBhUC4.mjs → storage-BXU4ax4O.mjs} +1 -1
- package/dist/packem_shared/{tui-prompts-DEiPCKV-.mjs → tui-prompts-BjEN8XgP.mjs} +9 -12
- package/dist/packem_shared/wrangler-secrets-Coni-mER.mjs +49 -0
- package/package.json +19 -14
- package/skills/lunora-quickstart/SKILL.md +26 -5
- package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +0 -61
- package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +0 -56
- package/dist/packem_shared/runAddCommand-CTRA_JlL.mjs +0 -4
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
import { LunoraError } from '@lunora/errors';
|
|
2
|
+
|
|
1
3
|
const NEWLINE_PRESENT = /[\r\n]/u;
|
|
2
4
|
const VALID_ENV_NAME = /^[A-Za-z_]\w*$/u;
|
|
3
5
|
const VALID_ITEM_NAME = /^[A-Za-z0-9][\w-]*$/u;
|
|
6
|
+
const VALID_MODULE_PATH = /^[\w.-]+(?:\/[\w.-]+)*$/u;
|
|
4
7
|
const parseManifest = (raw, itemName) => {
|
|
5
8
|
if (typeof raw !== "object" || raw === null) {
|
|
6
|
-
throw new
|
|
9
|
+
throw new LunoraError("INTERNAL", `registry.json for "${itemName}" is not an object`);
|
|
7
10
|
}
|
|
8
11
|
const record = raw;
|
|
9
12
|
const { name } = record;
|
|
10
13
|
if (typeof name !== "string" || name.length === 0) {
|
|
11
|
-
throw new
|
|
14
|
+
throw new LunoraError("INTERNAL", `registry.json for "${itemName}" is missing a string "name"`);
|
|
12
15
|
}
|
|
13
16
|
if (!VALID_ITEM_NAME.test(name)) {
|
|
14
|
-
throw new
|
|
17
|
+
throw new LunoraError(
|
|
18
|
+
"INTERNAL",
|
|
15
19
|
`registry.json for "${itemName}": name "${name}" must match ${VALID_ITEM_NAME.source} (letters, digits, "-", "_"; no path separators, "..", or code)`
|
|
16
20
|
);
|
|
17
21
|
}
|
|
@@ -21,7 +25,7 @@ const parseManifest = (raw, itemName) => {
|
|
|
21
25
|
}
|
|
22
26
|
const files = filesRaw.map((entry, index) => {
|
|
23
27
|
if (typeof entry !== "object" || entry === null) {
|
|
24
|
-
throw new
|
|
28
|
+
throw new LunoraError("INTERNAL", `registry.json "${itemName}": files[${String(index)}] is not an object`);
|
|
25
29
|
}
|
|
26
30
|
const fileRecord = entry;
|
|
27
31
|
const { from } = fileRecord;
|
|
@@ -31,14 +35,17 @@ const parseManifest = (raw, itemName) => {
|
|
|
31
35
|
throw new TypeError(`registry.json "${itemName}": files[${String(index)}] needs string "from" and "to"`);
|
|
32
36
|
}
|
|
33
37
|
if (merge !== "create-or-skip" && merge !== "schema-extension") {
|
|
34
|
-
throw new
|
|
38
|
+
throw new LunoraError("INTERNAL", `registry.json "${itemName}": files[${String(index)}].merge must be "create-or-skip" or "schema-extension"`);
|
|
35
39
|
}
|
|
36
40
|
for (const [field, value] of [
|
|
37
41
|
["from", from],
|
|
38
42
|
["to", to]
|
|
39
43
|
]) {
|
|
40
44
|
if (value.includes("..") || value.startsWith("/")) {
|
|
41
|
-
throw new
|
|
45
|
+
throw new LunoraError(
|
|
46
|
+
"INTERNAL",
|
|
47
|
+
`registry.json "${itemName}": files[${String(index)}].${field} "${value}" must be a relative path without ".."`
|
|
48
|
+
);
|
|
42
49
|
}
|
|
43
50
|
}
|
|
44
51
|
return { from, merge, to };
|
|
@@ -54,20 +61,39 @@ const parseManifest = (raw, itemName) => {
|
|
|
54
61
|
const bindingRecord = value;
|
|
55
62
|
return Array.isArray(bindingRecord.path) && bindingRecord.path.every((segment) => typeof segment === "string");
|
|
56
63
|
}) : void 0;
|
|
64
|
+
const entrypointReexports = Array.isArray(record.entrypointReexports) ? record.entrypointReexports.filter(
|
|
65
|
+
(value) => typeof value === "object" && value !== null && typeof value.module === "string"
|
|
66
|
+
).map((entry) => {
|
|
67
|
+
if (entry.module.includes("..") || entry.module.startsWith("/") || !VALID_MODULE_PATH.test(entry.module)) {
|
|
68
|
+
throw new LunoraError(
|
|
69
|
+
"INTERNAL",
|
|
70
|
+
`registry.json "${itemName}": entrypointReexports[].module "${entry.module}" must be a safe relative module path without path traversal or unsafe characters`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const reexport = { module: entry.module };
|
|
74
|
+
if (typeof entry.comment === "string") {
|
|
75
|
+
if (NEWLINE_PRESENT.test(entry.comment)) {
|
|
76
|
+
throw new LunoraError("INTERNAL", `registry.json "${itemName}": entrypointReexports[].comment must not contain a newline`);
|
|
77
|
+
}
|
|
78
|
+
reexport.comment = entry.comment;
|
|
79
|
+
}
|
|
80
|
+
return reexport;
|
|
81
|
+
}) : void 0;
|
|
57
82
|
const envVariables = Array.isArray(record.envVars) ? record.envVars.filter(
|
|
58
83
|
(value) => typeof value === "object" && value !== null && typeof value.name === "string"
|
|
59
84
|
).map((entry) => {
|
|
60
85
|
const hasValue = typeof entry.value === "string";
|
|
61
86
|
if (!VALID_ENV_NAME.test(entry.name)) {
|
|
62
|
-
throw new
|
|
87
|
+
throw new LunoraError(
|
|
88
|
+
"INTERNAL",
|
|
63
89
|
`registry.json "${itemName}": envVars["${entry.name}"].name must match ${VALID_ENV_NAME.source} (letters, digits, underscore; no "=" or newline)`
|
|
64
90
|
);
|
|
65
91
|
}
|
|
66
92
|
if (hasValue && NEWLINE_PRESENT.test(entry.value)) {
|
|
67
|
-
throw new
|
|
93
|
+
throw new LunoraError("INTERNAL", `registry.json "${itemName}": envVars["${entry.name}"].value must not contain a newline`);
|
|
68
94
|
}
|
|
69
95
|
if (typeof entry.description === "string" && NEWLINE_PRESENT.test(entry.description)) {
|
|
70
|
-
throw new
|
|
96
|
+
throw new LunoraError("INTERNAL", `registry.json "${itemName}": envVars["${entry.name}"].description must not contain a newline`);
|
|
71
97
|
}
|
|
72
98
|
return {
|
|
73
99
|
...typeof entry.description === "string" ? { description: entry.description } : {},
|
|
@@ -83,6 +109,7 @@ const parseManifest = (raw, itemName) => {
|
|
|
83
109
|
description: typeof record.description === "string" ? record.description : void 0,
|
|
84
110
|
devDependencies,
|
|
85
111
|
docs: typeof record.docs === "string" ? record.docs : void 0,
|
|
112
|
+
entrypointReexports,
|
|
86
113
|
envVars: envVariables,
|
|
87
114
|
files,
|
|
88
115
|
name,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
-
import { isInteractive, ACCENT, LUNA_ART, LUNA_NAME, LUNA_SIGNOFF, badgeLead, padBadge, badgeWidth } from '@lunora/config';
|
|
2
|
+
import { isInteractive, ACCENT, LUNA_ART, LUNA_NAME, LUNA_SIGNOFF, BADGE_COLUMN_WIDTH, badgeLead, padBadge, badgeWidth } from '@lunora/config';
|
|
3
3
|
import { render } from '@visulima/tui';
|
|
4
4
|
import '@visulima/tui/components/big-text';
|
|
5
5
|
import { Box } from '@visulima/tui/components/box';
|
|
@@ -13,6 +13,7 @@ import { useApp } from '@visulima/tui/hooks/use-app';
|
|
|
13
13
|
import { useInput } from '@visulima/tui/hooks/use-input';
|
|
14
14
|
import { useInterval } from '@visulima/tui/hooks/use-interval';
|
|
15
15
|
import { useEffect, useState } from 'react';
|
|
16
|
+
import { P as PromptCancelledError } from './prompt-cancelled-APzX1Im-.mjs';
|
|
16
17
|
|
|
17
18
|
let listenerCapRaised = false;
|
|
18
19
|
const raiseListenerCap = () => {
|
|
@@ -24,12 +25,6 @@ const raiseListenerCap = () => {
|
|
|
24
25
|
process.setMaxListeners(64);
|
|
25
26
|
}
|
|
26
27
|
};
|
|
27
|
-
class PromptCancelledError extends Error {
|
|
28
|
-
constructor() {
|
|
29
|
-
super("cancelled");
|
|
30
|
-
this.name = "PromptCancelledError";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
28
|
const CtrlCGuard = ({ children, onCancel }) => {
|
|
34
29
|
const { exit } = useApp();
|
|
35
30
|
useInput((input, key) => {
|
|
@@ -57,7 +52,7 @@ const PromptHeader = ({ badge, message }) => badge === void 0 ? jsx(Text, { bold
|
|
|
57
52
|
jsx(Text, { bold: true, children: ` ${message}` })
|
|
58
53
|
] });
|
|
59
54
|
const BODY_INDENT = 2;
|
|
60
|
-
const HEADER_INDENT = " ".repeat(
|
|
55
|
+
const HEADER_INDENT = " ".repeat(BADGE_COLUMN_WIDTH - 1);
|
|
61
56
|
const badgeIndent = (badge) => " ".repeat(badgeWidth(badge) + 1);
|
|
62
57
|
const bodyIndent = (badge) => badge === void 0 ? BODY_INDENT : badgeWidth(badge) + 1;
|
|
63
58
|
const PromptShell = ({
|
|
@@ -449,7 +444,7 @@ const tuiNextSteps = async (badge, header, steps, help) => {
|
|
|
449
444
|
] })
|
|
450
445
|
);
|
|
451
446
|
};
|
|
452
|
-
const TASK_INDENT = " ".repeat(
|
|
447
|
+
const TASK_INDENT = " ".repeat(BADGE_COLUMN_WIDTH - 1);
|
|
453
448
|
const SPINNER_FIRST = "#a855f7";
|
|
454
449
|
const SPINNER_LAST = "#06b6d4";
|
|
455
450
|
const SPINNER_COLORS = [SPINNER_FIRST, "#9b51f6", "#8a5cf6", "#6f86f6", "#4aa6ef", "#2bb6dd", "#12bcd0", SPINNER_LAST];
|
|
@@ -465,6 +460,7 @@ const SPINNER_FRAMES = SPINNER_STRIP.map((_, offset) => {
|
|
|
465
460
|
const window = SPINNER_STRIP.slice(offset, offset + SPINNER_BAR_WIDTH);
|
|
466
461
|
return [...window, ...repeatColor(SPINNER_BAR_WIDTH - window.length, SPINNER_FIRST)];
|
|
467
462
|
});
|
|
463
|
+
const SPINNER_LEAD = " ".repeat(Math.max(0, BADGE_COLUMN_WIDTH - SPINNER_BAR_WIDTH));
|
|
468
464
|
const GradientSpinner = ({ label }) => {
|
|
469
465
|
const [index, setIndex] = useState(0);
|
|
470
466
|
useInterval(() => {
|
|
@@ -472,11 +468,12 @@ const GradientSpinner = ({ label }) => {
|
|
|
472
468
|
}, 90);
|
|
473
469
|
const colors = SPINNER_FRAMES[index % SPINNER_FRAMES.length] ?? [];
|
|
474
470
|
return jsxs(Text, { children: [
|
|
471
|
+
SPINNER_LEAD,
|
|
475
472
|
colors.map((color, blockIndex) => (
|
|
476
473
|
// eslint-disable-next-line react-x/no-array-index-key -- fixed-length gradient bar that never reorders.
|
|
477
474
|
jsx(Text, { color, children: "█" }, blockIndex)
|
|
478
475
|
)),
|
|
479
|
-
`
|
|
476
|
+
` ${label}`
|
|
480
477
|
] });
|
|
481
478
|
};
|
|
482
479
|
const TaskRow = ({ label, status }) => {
|
|
@@ -536,7 +533,7 @@ const TasksView = ({ end, onSettle, start, tasks }) => {
|
|
|
536
533
|
return jsxs(Box, { flexDirection: "column", children: [
|
|
537
534
|
allDone ? jsxs(Box, { children: [
|
|
538
535
|
jsx(Text, { bold: true, color: "green", children: `${HEADER_INDENT}✔` }),
|
|
539
|
-
jsx(Text, { bold: true, color: "green", children: `
|
|
536
|
+
jsx(Text, { bold: true, color: "green", children: ` ${end}` })
|
|
540
537
|
] }) : jsx(GradientSpinner, { label: start }),
|
|
541
538
|
tasks.map((task, index) => jsx(TaskRow, { label: task.label, status: statuses[index] ?? "pending" }, task.label))
|
|
542
539
|
] });
|
|
@@ -658,4 +655,4 @@ const withTuiBadgeProgress = async (badge, steps, done) => {
|
|
|
658
655
|
return outcome.values;
|
|
659
656
|
};
|
|
660
657
|
|
|
661
|
-
export {
|
|
658
|
+
export { tuiSelect as a, tuiConfirm as b, createTuiConfirm as c, tuiMascot as d, tuiStep as e, tuiMoonrise as f, tuiHeadline as g, tuiInfo as h, tuiNextSteps as i, tuiTasks as j, tuiMultiSelect as k, withTuiBadgeProgress as l, tuiText as t, withTuiSpinner as w };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { e as execArgsFor, d as detectPackageManager } from './detect-package-manager-v4hHpQd0.mjs';
|
|
3
|
+
|
|
4
|
+
const execCode = (error) => {
|
|
5
|
+
if (!error) {
|
|
6
|
+
return 0;
|
|
7
|
+
}
|
|
8
|
+
return typeof error.code === "number" ? error.code : 1;
|
|
9
|
+
};
|
|
10
|
+
const defaultRunner = (command, args, cwd) => new Promise((resolve) => {
|
|
11
|
+
execFile(command, [...args], { cwd }, (error, stdout, stderr) => {
|
|
12
|
+
resolve({ code: execCode(error), stderr, stdout });
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
const parseSecretNames = (stdout) => {
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = JSON.parse(stdout);
|
|
19
|
+
} catch {
|
|
20
|
+
return void 0;
|
|
21
|
+
}
|
|
22
|
+
if (!Array.isArray(parsed)) {
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
const names = parsed.map((entry) => entry !== null && typeof entry === "object" ? entry.name : void 0).filter((name) => typeof name === "string" && name.length > 0);
|
|
26
|
+
return [...names].toSorted((a, b) => a.localeCompare(b));
|
|
27
|
+
};
|
|
28
|
+
const listRemoteSecrets = async (inputs) => {
|
|
29
|
+
const wranglerArgs = ["secret", "list", "--format", "json"];
|
|
30
|
+
if (inputs.env !== void 0) {
|
|
31
|
+
wranglerArgs.push("--env", inputs.env);
|
|
32
|
+
}
|
|
33
|
+
if (inputs.temporary) {
|
|
34
|
+
wranglerArgs.push("--temporary");
|
|
35
|
+
}
|
|
36
|
+
const { args, command } = execArgsFor(detectPackageManager(inputs.cwd), "wrangler", wranglerArgs);
|
|
37
|
+
const runner = inputs.runner ?? defaultRunner;
|
|
38
|
+
const result = await runner(command, args, inputs.cwd);
|
|
39
|
+
if (result.code !== 0) {
|
|
40
|
+
return { error: result.stderr.trim() || `wrangler secret list exited ${String(result.code)}`, names: [], ok: false };
|
|
41
|
+
}
|
|
42
|
+
const names = parseSecretNames(result.stdout);
|
|
43
|
+
if (names === void 0) {
|
|
44
|
+
return { error: "could not parse `wrangler secret list --format json` output", names: [], ok: false };
|
|
45
|
+
}
|
|
46
|
+
return { names, ok: true };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export { listRemoteSecrets as l };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/cli",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.101",
|
|
4
4
|
"description": "The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-skills",
|
|
@@ -51,22 +51,27 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@bomb.sh/tab": "0.0.
|
|
55
|
-
"@lunora/
|
|
56
|
-
"@lunora/
|
|
57
|
-
"@lunora/
|
|
58
|
-
"@lunora/
|
|
59
|
-
"@lunora/
|
|
60
|
-
"@
|
|
61
|
-
"@
|
|
62
|
-
"@
|
|
63
|
-
"@visulima/
|
|
64
|
-
"@visulima/
|
|
65
|
-
"@visulima/
|
|
54
|
+
"@bomb.sh/tab": "0.0.19",
|
|
55
|
+
"@lunora/bindings": "1.0.0-alpha.11",
|
|
56
|
+
"@lunora/codegen": "1.0.0-alpha.55",
|
|
57
|
+
"@lunora/config": "1.0.0-alpha.81",
|
|
58
|
+
"@lunora/container": "1.0.0-alpha.16",
|
|
59
|
+
"@lunora/d1": "1.0.0-alpha.40",
|
|
60
|
+
"@lunora/errors": "1.0.0-alpha.8",
|
|
61
|
+
"@lunora/runtime": "1.0.0-alpha.37",
|
|
62
|
+
"@lunora/seed": "1.0.0-alpha.30",
|
|
63
|
+
"@visulima/cerebro": "3.0.0",
|
|
64
|
+
"@visulima/error": "6.0.0",
|
|
65
|
+
"@visulima/fs": "5.0.4",
|
|
66
|
+
"@visulima/package": "5.0.4",
|
|
67
|
+
"@visulima/pail": "4.0.0",
|
|
68
|
+
"@visulima/path": "3.0.0",
|
|
69
|
+
"@visulima/spinner": "1.0.0",
|
|
70
|
+
"@visulima/tui": "1.0.4",
|
|
66
71
|
"cfonts": "^3.3.1",
|
|
67
72
|
"giget": "3.3.0",
|
|
68
73
|
"jsonc-parser": "^3.3.1",
|
|
69
|
-
"magic-string": "^0.
|
|
74
|
+
"magic-string": "^1.0.0",
|
|
70
75
|
"react": "^19.2.7",
|
|
71
76
|
"react-reconciler": "^0.33.0",
|
|
72
77
|
"ts-morph": "^28.0.0"
|
|
@@ -31,9 +31,12 @@ Set up a working Lunora project as fast as possible.
|
|
|
31
31
|
Lunora into the current project.
|
|
32
32
|
4. Run `lunora codegen` to generate `lunora/_generated/` and typecheck the
|
|
33
33
|
schema + functions. This is the agent's feedback loop.
|
|
34
|
-
5. Start the dev loop
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
5. Start the dev loop. As an agent, run `lunora dev --background` — it starts
|
|
35
|
+
the server as a managed detached process, blocks until it accepts requests,
|
|
36
|
+
prints the URL + PID, and returns (under a detected AI agent, plain
|
|
37
|
+
`lunora dev` does this automatically, with JSON logs). Never leave a bare
|
|
38
|
+
`lunora dev` running in your own shell — it is long-running and does not
|
|
39
|
+
exit.
|
|
37
40
|
6. Verify a query/mutation round-trip works end to end.
|
|
38
41
|
|
|
39
42
|
## Path 1: New Project (Recommended)
|
|
@@ -54,6 +57,7 @@ pnpm install
|
|
|
54
57
|
| `vite` | React + Vite (the simplest full-stack starter) |
|
|
55
58
|
| `standalone` | Worker-only Lunora backend, no frontend |
|
|
56
59
|
| `astro` | Astro integration |
|
|
60
|
+
| `next` | Next.js (App Router, OpenNext on Cloudflare) |
|
|
57
61
|
| `nuxt` | Nuxt (Vue) |
|
|
58
62
|
| `sveltekit` | SvelteKit |
|
|
59
63
|
| `tanstack-start-react` | TanStack Start (React) |
|
|
@@ -87,7 +91,23 @@ not exit, so:
|
|
|
87
91
|
|
|
88
92
|
- **Local development (user at the keyboard):** ask the user to run `lunora dev`
|
|
89
93
|
in a terminal.
|
|
90
|
-
- **
|
|
94
|
+
- **Agents:** run `lunora dev --background`. It detaches the server, waits until
|
|
95
|
+
it answers HTTP, prints `Dev server running at <url> (pid <n>)`, and exits —
|
|
96
|
+
no orphaned shell, no PID bookkeeping. When Lunora detects an AI agent
|
|
97
|
+
(Claude Code, Cursor, Codex, …), plain `lunora dev` flips into this mode
|
|
98
|
+
automatically with JSON logs; `LUNORA_AGENT_MODE=0` opts out.
|
|
99
|
+
|
|
100
|
+
Manage the running server afterwards:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
lunora dev status --json # machine-readable: url, pid, uptime, logFile
|
|
104
|
+
lunora dev logs --lines 50 # tail the captured output (.lunora/dev.log)
|
|
105
|
+
lunora dev stop # idempotent — succeeds even if nothing runs
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A second `lunora dev` never double-starts: it reports the existing instance
|
|
109
|
+
(`.lunora/dev.json` is the lockfile). Probe readiness or liveness at
|
|
110
|
+
`GET /_lunora/status` (`{"ok":true}`).
|
|
91
111
|
|
|
92
112
|
Vite serves on `http://localhost:5173` by default; the Worker is served on the
|
|
93
113
|
same origin via `@cloudflare/vite-plugin`.
|
|
@@ -236,5 +256,6 @@ ids, `.dev.vars` secrets, and container exports.
|
|
|
236
256
|
- [ ] New project: scaffolded with `lunora init --template <t>`.
|
|
237
257
|
- [ ] Existing app: ran `lunora init --here` and wired `LunoraProvider`.
|
|
238
258
|
- [ ] Ran `lunora codegen`: `lunora/_generated/` exists and typecheck is clean.
|
|
239
|
-
- [ ]
|
|
259
|
+
- [ ] Dev server is running — user terminal, or `lunora dev --background`
|
|
260
|
+
(check with `lunora dev status`).
|
|
240
261
|
- [ ] Verified a query/mutation round-trip re-renders the client live.
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, join } from '@visulima/path';
|
|
4
|
-
|
|
5
|
-
const FALLBACK = "pnpm";
|
|
6
|
-
const KNOWN_MANAGERS = ["pnpm", "yarn", "npm", "bun"];
|
|
7
|
-
const INSTALL_PREFERENCE = ["pnpm", "bun", "yarn", "npm"];
|
|
8
|
-
const isManagerInstalled = (manager) => {
|
|
9
|
-
try {
|
|
10
|
-
return spawnSync(manager, ["--version"], { stdio: "ignore", timeout: 5e3 }).status === 0;
|
|
11
|
-
} catch {
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
const detectInstalledManagers = (probe = isManagerInstalled) => INSTALL_PREFERENCE.filter((manager) => probe(manager));
|
|
16
|
-
const installArgsFor = (manager) => {
|
|
17
|
-
return { args: ["install"], command: manager };
|
|
18
|
-
};
|
|
19
|
-
const parseDeclaredManager = (declared) => {
|
|
20
|
-
if (typeof declared !== "string") {
|
|
21
|
-
return void 0;
|
|
22
|
-
}
|
|
23
|
-
return KNOWN_MANAGERS.find((manager) => declared.startsWith(`${manager}@`));
|
|
24
|
-
};
|
|
25
|
-
const readDeclaredManager = (directory) => {
|
|
26
|
-
const candidate = join(directory, "package.json");
|
|
27
|
-
if (!existsSync(candidate)) {
|
|
28
|
-
return void 0;
|
|
29
|
-
}
|
|
30
|
-
try {
|
|
31
|
-
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
32
|
-
return parseDeclaredManager(parsed.packageManager);
|
|
33
|
-
} catch {
|
|
34
|
-
return void 0;
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
const detectPackageManager = (startDirectory) => {
|
|
38
|
-
let directory = startDirectory;
|
|
39
|
-
while (directory && directory !== dirname(directory)) {
|
|
40
|
-
const declared = readDeclaredManager(directory);
|
|
41
|
-
if (declared !== void 0) {
|
|
42
|
-
return declared;
|
|
43
|
-
}
|
|
44
|
-
directory = dirname(directory);
|
|
45
|
-
}
|
|
46
|
-
return FALLBACK;
|
|
47
|
-
};
|
|
48
|
-
const execArgsFor = (manager, command, args) => {
|
|
49
|
-
if (manager === "yarn") {
|
|
50
|
-
return { args: [command, ...args], command: "yarn" };
|
|
51
|
-
}
|
|
52
|
-
if (manager === "bun") {
|
|
53
|
-
return { args: ["x", command, ...args], command: "bun" };
|
|
54
|
-
}
|
|
55
|
-
if (manager === "npm") {
|
|
56
|
-
return { args: ["--", command, ...args], command: "npx" };
|
|
57
|
-
}
|
|
58
|
-
return { args: ["exec", command, ...args], command: "pnpm" };
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
export { detectInstalledManagers as a, detectPackageManager as d, execArgsFor as e, installArgsFor as i };
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
const consumeLongOption = (body, next, booleanFlags, accumulator) => {
|
|
2
|
-
const eqIndex = body.indexOf("=");
|
|
3
|
-
if (eqIndex !== -1) {
|
|
4
|
-
accumulator.options[body.slice(0, eqIndex)] = body.slice(eqIndex + 1);
|
|
5
|
-
return 1;
|
|
6
|
-
}
|
|
7
|
-
if (booleanFlags.has(body)) {
|
|
8
|
-
accumulator.flags[body] = true;
|
|
9
|
-
return 1;
|
|
10
|
-
}
|
|
11
|
-
if (next !== void 0 && !next.startsWith("-")) {
|
|
12
|
-
accumulator.options[body] = next;
|
|
13
|
-
return 2;
|
|
14
|
-
}
|
|
15
|
-
accumulator.flags[body] = true;
|
|
16
|
-
return 1;
|
|
17
|
-
};
|
|
18
|
-
const consumeShortOption = (body, next, accumulator) => {
|
|
19
|
-
if (body.length > 1) {
|
|
20
|
-
accumulator.options[body[0]] = body.slice(1);
|
|
21
|
-
return 1;
|
|
22
|
-
}
|
|
23
|
-
if (next !== void 0 && !next.startsWith("-")) {
|
|
24
|
-
accumulator.options[body] = next;
|
|
25
|
-
return 2;
|
|
26
|
-
}
|
|
27
|
-
accumulator.flags[body] = true;
|
|
28
|
-
return 1;
|
|
29
|
-
};
|
|
30
|
-
const parseArgs = (argv, booleanFlags = /* @__PURE__ */ new Set()) => {
|
|
31
|
-
const accumulator = { flags: {}, options: {}, positional: [] };
|
|
32
|
-
let index = 0;
|
|
33
|
-
let terminated = false;
|
|
34
|
-
while (index < argv.length) {
|
|
35
|
-
const token = argv[index];
|
|
36
|
-
if (token === void 0) {
|
|
37
|
-
index += 1;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
if (terminated || !token.startsWith("-") || token.length === 1) {
|
|
41
|
-
accumulator.positional.push(token);
|
|
42
|
-
index += 1;
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
if (token === "--") {
|
|
46
|
-
terminated = true;
|
|
47
|
-
index += 1;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
const next = argv[index + 1];
|
|
51
|
-
index += token.startsWith("--") ? consumeLongOption(token.slice(2), next, booleanFlags, accumulator) : consumeShortOption(token.slice(1), next, accumulator);
|
|
52
|
-
}
|
|
53
|
-
return { flags: accumulator.flags, options: accumulator.options, positional: accumulator.positional };
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export { parseArgs as default };
|