@lunora/cli 0.0.0 → 1.0.0-alpha.2
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 +105 -0
- package/README.md +109 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/bin.mjs +11 -0
- package/dist/index.d.mts +852 -0
- package/dist/index.d.ts +852 -0
- package/dist/index.mjs +19 -0
- package/dist/packem_chunks/handler.mjs +76 -0
- package/dist/packem_chunks/handler10.mjs +22 -0
- package/dist/packem_chunks/handler11.mjs +192 -0
- package/dist/packem_chunks/handler12.mjs +131 -0
- package/dist/packem_chunks/handler13.mjs +65 -0
- package/dist/packem_chunks/handler14.mjs +58 -0
- package/dist/packem_chunks/handler15.mjs +79 -0
- package/dist/packem_chunks/handler16.mjs +41 -0
- package/dist/packem_chunks/handler17.mjs +105 -0
- package/dist/packem_chunks/handler18.mjs +172 -0
- package/dist/packem_chunks/handler19.mjs +89 -0
- package/dist/packem_chunks/handler2.mjs +114 -0
- package/dist/packem_chunks/handler20.mjs +94 -0
- package/dist/packem_chunks/handler21.mjs +311 -0
- package/dist/packem_chunks/handler3.mjs +204 -0
- package/dist/packem_chunks/handler4.mjs +33 -0
- package/dist/packem_chunks/handler5.mjs +49 -0
- package/dist/packem_chunks/handler6.mjs +91 -0
- package/dist/packem_chunks/handler7.mjs +42 -0
- package/dist/packem_chunks/handler8.mjs +174 -0
- package/dist/packem_chunks/handler9.mjs +16 -0
- package/dist/packem_chunks/planDevCommand.mjs +543 -0
- package/dist/packem_chunks/runCodegenCommand.mjs +52 -0
- package/dist/packem_chunks/runDeployCommand.mjs +504 -0
- package/dist/packem_chunks/runInitCommand.mjs +652 -0
- package/dist/packem_chunks/runMigrateGenerateCommand.mjs +397 -0
- package/dist/packem_chunks/runResetCommand.mjs +41 -0
- package/dist/packem_chunks/runRpcCommand.mjs +68 -0
- package/dist/packem_shared/COMMANDS-1V_KEx35.mjs +905 -0
- package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs +244 -0
- package/dist/packem_shared/admin-url-4UzT-CI4.mjs +19 -0
- package/dist/packem_shared/api-spec-CtA6ilu4.mjs +13 -0
- package/dist/packem_shared/buildRegistryIndex-BcYe607_.mjs +38 -0
- package/dist/packem_shared/command-BDXcJCCJ.mjs +14 -0
- package/dist/packem_shared/createLogger-CHPNjFw2.mjs +73 -0
- package/dist/packem_shared/defaultSpawner-DxI3mebw.mjs +43 -0
- package/dist/packem_shared/diffSnapshots-RR2ZE8Ya.mjs +161 -0
- package/dist/packem_shared/docker-hMQ97KSQ.mjs +21 -0
- package/dist/packem_shared/features-ocSSpZtS.mjs +24 -0
- package/dist/packem_shared/insertSchemaExtension-BuzF6-t2.mjs +59 -0
- package/dist/packem_shared/open-url-Dfq6fAyT.mjs +41 -0
- package/dist/packem_shared/output-format-7gyGR3h8.mjs +17 -0
- package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +56 -0
- package/dist/packem_shared/parseManifest--vZf2FY1.mjs +94 -0
- package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +16 -0
- package/dist/packem_shared/runAddCommand-BZGkRnBs.mjs +693 -0
- package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +79 -0
- package/dist/packem_shared/schemaIrToSnapshot-aBTo7TM5.mjs +43 -0
- package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +12 -0
- package/package.json +61 -18
- package/skills/README.md +29 -0
- package/skills/lunora/SKILL.md +83 -0
- package/skills/lunora-create-package/SKILL.md +129 -0
- package/skills/lunora-deploy/SKILL.md +150 -0
- package/skills/lunora-functions/SKILL.md +182 -0
- package/skills/lunora-migration-helper/SKILL.md +194 -0
- package/skills/lunora-performance-audit/SKILL.md +143 -0
- package/skills/lunora-quickstart/SKILL.md +240 -0
- package/skills/lunora-realtime/SKILL.md +177 -0
- package/skills/lunora-setup-auth/SKILL.md +170 -0
- package/skills/lunora-setup-hyperdrive/SKILL.md +154 -0
- package/skills/lunora-setup-hyperdrive-global/SKILL.md +171 -0
- package/skills/lunora-setup-mail/SKILL.md +151 -0
- package/skills/lunora-setup-scheduler/SKILL.md +157 -0
- package/skills/lunora-setup-storage/SKILL.md +154 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { createWriteStream, createReadStream } from 'node:fs';
|
|
2
|
+
import { unlink, stat } from 'node:fs/promises';
|
|
3
|
+
import { r as resolveAdminBaseUrl } from './admin-url-4UzT-CI4.mjs';
|
|
4
|
+
|
|
5
|
+
const EXPORT_ENDPOINT_PATH = "/_lunora/admin/export";
|
|
6
|
+
const IMPORT_ENDPOINT_PATH = "/_lunora/admin/import";
|
|
7
|
+
const DEFAULT_IMPORT_BATCH_SIZE = 500;
|
|
8
|
+
const resolveTables = (raw) => {
|
|
9
|
+
if (raw === void 0) {
|
|
10
|
+
return void 0;
|
|
11
|
+
}
|
|
12
|
+
const tables = raw.split(",").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
13
|
+
return tables.length > 0 ? tables : void 0;
|
|
14
|
+
};
|
|
15
|
+
const writeWithBackpressure = async (sink, line) => {
|
|
16
|
+
if (!sink.write(line)) {
|
|
17
|
+
await new Promise((resolve) => {
|
|
18
|
+
sink.once("drain", resolve);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const streamNdjsonToSink = async (body, sink) => {
|
|
23
|
+
const reader = body.getReader();
|
|
24
|
+
const decoder = new TextDecoder();
|
|
25
|
+
let bytes = 0;
|
|
26
|
+
let rows = 0;
|
|
27
|
+
let leftover = "";
|
|
28
|
+
let done = false;
|
|
29
|
+
try {
|
|
30
|
+
while (!done) {
|
|
31
|
+
const read = await reader.read();
|
|
32
|
+
done = read.done;
|
|
33
|
+
if (read.value === void 0) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
bytes += read.value.length;
|
|
37
|
+
leftover += decoder.decode(read.value, { stream: true });
|
|
38
|
+
let newlineIndex = leftover.indexOf("\n");
|
|
39
|
+
while (newlineIndex !== -1) {
|
|
40
|
+
rows += 1;
|
|
41
|
+
const line = `${leftover.slice(0, newlineIndex)}
|
|
42
|
+
`;
|
|
43
|
+
await writeWithBackpressure(sink, line);
|
|
44
|
+
leftover = leftover.slice(newlineIndex + 1);
|
|
45
|
+
newlineIndex = leftover.indexOf("\n");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (leftover.length > 0) {
|
|
49
|
+
rows += 1;
|
|
50
|
+
await writeWithBackpressure(sink, `${leftover}
|
|
51
|
+
`);
|
|
52
|
+
}
|
|
53
|
+
return { bytes, rows };
|
|
54
|
+
} finally {
|
|
55
|
+
reader.releaseLock();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const discardPartialExport = async (sink, outPath) => {
|
|
59
|
+
if (outPath === void 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
sink.destroy();
|
|
63
|
+
try {
|
|
64
|
+
await unlink(outPath);
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const runExportCommand = async (options) => {
|
|
69
|
+
if (options.prod && options.url === void 0) {
|
|
70
|
+
options.logger.error("--prod requires an explicit --url (refusing to export from the implicit localhost worker)");
|
|
71
|
+
return { bytes: 0, code: 1, rows: 0 };
|
|
72
|
+
}
|
|
73
|
+
const token = options.token ?? process.env["LUNORA_ADMIN_TOKEN"];
|
|
74
|
+
if (!token) {
|
|
75
|
+
options.logger.error("admin token required — pass --token or set LUNORA_ADMIN_TOKEN");
|
|
76
|
+
return { bytes: 0, code: 1, rows: 0 };
|
|
77
|
+
}
|
|
78
|
+
const baseUrl = resolveAdminBaseUrl(options.url, options.logger);
|
|
79
|
+
if (baseUrl === void 0) {
|
|
80
|
+
return { bytes: 0, code: 1, rows: 0 };
|
|
81
|
+
}
|
|
82
|
+
const requestUrl = `${baseUrl}${EXPORT_ENDPOINT_PATH}`;
|
|
83
|
+
const tables = resolveTables(options.tables);
|
|
84
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
85
|
+
if (typeof fetchImpl !== "function") {
|
|
86
|
+
throw new TypeError("no fetch implementation available — pass fetchImpl or run on Node >= 18");
|
|
87
|
+
}
|
|
88
|
+
options.logger.info(`POST ${requestUrl} -> export${tables ? ` (tables: ${tables.join(",")})` : ""}`);
|
|
89
|
+
const response = await fetchImpl(requestUrl, {
|
|
90
|
+
body: JSON.stringify(tables ? { tables } : {}),
|
|
91
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
92
|
+
method: "POST"
|
|
93
|
+
});
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
const errorText = await response.text();
|
|
96
|
+
options.logger.error(`export failed: HTTP ${String(response.status)}: ${errorText}`);
|
|
97
|
+
return { bytes: 0, code: 1, rows: 0 };
|
|
98
|
+
}
|
|
99
|
+
if (!response.body) {
|
|
100
|
+
options.logger.error("export response carried no body");
|
|
101
|
+
return { bytes: 0, code: 1, rows: 0 };
|
|
102
|
+
}
|
|
103
|
+
const outPath = options.out === void 0 || options.out === "-" ? void 0 : options.out;
|
|
104
|
+
const sink = outPath === void 0 ? process.stdout : createWriteStream(outPath, { encoding: "utf8" });
|
|
105
|
+
let bytes;
|
|
106
|
+
let rows;
|
|
107
|
+
try {
|
|
108
|
+
({ bytes, rows } = await streamNdjsonToSink(response.body, sink));
|
|
109
|
+
} catch (error) {
|
|
110
|
+
await discardPartialExport(sink, outPath);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
if (outPath !== void 0) {
|
|
114
|
+
await new Promise((resolve, reject) => {
|
|
115
|
+
sink.end((error) => {
|
|
116
|
+
if (error) {
|
|
117
|
+
reject(error);
|
|
118
|
+
} else {
|
|
119
|
+
resolve();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
options.logger.success(`wrote ${String(rows)} rows to ${outPath} (${String(bytes)} bytes)`);
|
|
124
|
+
}
|
|
125
|
+
return { bytes, code: 0, rows };
|
|
126
|
+
};
|
|
127
|
+
const resolveImportRequest = async (options) => {
|
|
128
|
+
if (options.prod && options.url === void 0) {
|
|
129
|
+
options.logger.error("--prod requires an explicit --url (refusing to import to the implicit localhost worker)");
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
const token = options.token ?? process.env["LUNORA_ADMIN_TOKEN"];
|
|
133
|
+
if (!token) {
|
|
134
|
+
options.logger.error("admin token required — pass --token or set LUNORA_ADMIN_TOKEN");
|
|
135
|
+
return void 0;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const stats = await stat(options.file);
|
|
139
|
+
if (!stats.isFile()) {
|
|
140
|
+
options.logger.error(`not a file: ${options.file}`);
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
145
|
+
options.logger.error(`failed to stat ${options.file}: ${message}`);
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
const baseUrl = resolveAdminBaseUrl(options.url, options.logger);
|
|
149
|
+
if (baseUrl === void 0) {
|
|
150
|
+
return void 0;
|
|
151
|
+
}
|
|
152
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
153
|
+
if (typeof fetchImpl !== "function") {
|
|
154
|
+
throw new TypeError("no fetch implementation available — pass fetchImpl or run on Node >= 18");
|
|
155
|
+
}
|
|
156
|
+
return { fetchImpl, requestUrl: `${baseUrl}${IMPORT_ENDPOINT_PATH}`, token };
|
|
157
|
+
};
|
|
158
|
+
const runImportCommand = async (options) => {
|
|
159
|
+
const request = await resolveImportRequest(options);
|
|
160
|
+
if (request === void 0) {
|
|
161
|
+
return { body: void 0, code: 1, inserted: 0 };
|
|
162
|
+
}
|
|
163
|
+
const { fetchImpl, requestUrl, token } = request;
|
|
164
|
+
const batchSize = options.batchSize ?? DEFAULT_IMPORT_BATCH_SIZE;
|
|
165
|
+
options.logger.info(`POST ${requestUrl} -> import ${options.file}`);
|
|
166
|
+
const stream = createReadStream(options.file, { encoding: "utf8" });
|
|
167
|
+
const inserted = {};
|
|
168
|
+
const errors = [];
|
|
169
|
+
let conflicts = 0;
|
|
170
|
+
let buffer = "";
|
|
171
|
+
let batch = [];
|
|
172
|
+
let lineNumber = 0;
|
|
173
|
+
const flush = async () => {
|
|
174
|
+
if (batch.length === 0) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const body2 = batch.join("\n");
|
|
178
|
+
batch = [];
|
|
179
|
+
const response = await fetchImpl(requestUrl, {
|
|
180
|
+
body: body2,
|
|
181
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/x-ndjson" },
|
|
182
|
+
method: "POST"
|
|
183
|
+
});
|
|
184
|
+
if (!response.ok) {
|
|
185
|
+
const text = await response.text().catch(() => "<no body>");
|
|
186
|
+
throw new Error(`import batch failed (HTTP ${String(response.status)}): ${text}`);
|
|
187
|
+
}
|
|
188
|
+
const json = await response.json();
|
|
189
|
+
if (json.inserted) {
|
|
190
|
+
for (const [table, count] of Object.entries(json.inserted)) {
|
|
191
|
+
inserted[table] = (inserted[table] ?? 0) + count;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (Array.isArray(json.errors)) {
|
|
195
|
+
errors.push(...json.errors);
|
|
196
|
+
}
|
|
197
|
+
if (typeof json.conflicts === "number") {
|
|
198
|
+
conflicts += json.conflicts;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
const processLine = (line) => {
|
|
202
|
+
const trimmed = line.trim();
|
|
203
|
+
if (trimmed.length === 0) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
lineNumber += 1;
|
|
207
|
+
if (options.table === void 0) {
|
|
208
|
+
batch.push(trimmed);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
let parsedDocument;
|
|
212
|
+
try {
|
|
213
|
+
parsedDocument = JSON.parse(trimmed);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
216
|
+
throw new Error(`invalid JSON on line ${String(lineNumber)}: ${message}`, { cause: error });
|
|
217
|
+
}
|
|
218
|
+
batch.push(JSON.stringify({ doc: parsedDocument, table: options.table }));
|
|
219
|
+
};
|
|
220
|
+
for await (const chunk of stream) {
|
|
221
|
+
const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
222
|
+
buffer += text;
|
|
223
|
+
let newlineIndex = buffer.indexOf("\n");
|
|
224
|
+
while (newlineIndex !== -1) {
|
|
225
|
+
processLine(buffer.slice(0, newlineIndex));
|
|
226
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
227
|
+
newlineIndex = buffer.indexOf("\n");
|
|
228
|
+
if (batch.length >= batchSize) {
|
|
229
|
+
await flush();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (buffer.length > 0) {
|
|
234
|
+
processLine(buffer);
|
|
235
|
+
}
|
|
236
|
+
await flush();
|
|
237
|
+
const insertedTotal = Object.values(inserted).reduce((a, b) => a + b, 0);
|
|
238
|
+
const body = { conflicts, errors, inserted };
|
|
239
|
+
options.logger.info(JSON.stringify(body, void 0, 2));
|
|
240
|
+
options.logger.success(`imported ${String(insertedTotal)} rows (${String(conflicts)} conflicts, ${String(errors.length)} errors)`);
|
|
241
|
+
return { body, code: errors.length > 0 ? 1 : 0, inserted: insertedTotal };
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
export { DEFAULT_IMPORT_BATCH_SIZE, runExportCommand, runImportCommand };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "[::1]", "localhost"]);
|
|
2
|
+
const TRAILING_SLASH = /\/$/u;
|
|
3
|
+
const resolveAdminBaseUrl = (rawUrl, logger) => {
|
|
4
|
+
const candidate = rawUrl ?? "http://localhost:8787";
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = new URL(candidate);
|
|
8
|
+
} catch {
|
|
9
|
+
logger.error(`invalid --url: ${candidate}`);
|
|
10
|
+
return void 0;
|
|
11
|
+
}
|
|
12
|
+
if (!LOOPBACK_HOSTS.has(parsed.hostname) && parsed.protocol !== "https:") {
|
|
13
|
+
logger.error(`refusing to send the admin bearer over ${parsed.protocol}// to ${parsed.hostname} — use https for non-localhost targets`);
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
return candidate.replace(TRAILING_SLASH, "");
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export { resolveAdminBaseUrl as r };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const API_SPEC_VALUES = ["both", "none", "openapi", "openrpc"];
|
|
2
|
+
const API_SPEC_HELP = API_SPEC_VALUES.join(" | ");
|
|
3
|
+
const parseApiSpec = (value) => {
|
|
4
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
5
|
+
return void 0;
|
|
6
|
+
}
|
|
7
|
+
if (API_SPEC_VALUES.includes(value)) {
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
throw new Error(`invalid --api-spec "${value}" — expected one of: ${API_SPEC_HELP}`);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export { API_SPEC_HELP as A, parseApiSpec as p };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from '@visulima/path';
|
|
3
|
+
import parseManifest from './parseManifest--vZf2FY1.mjs';
|
|
4
|
+
|
|
5
|
+
const CONTROL_CHARS = /[\u0000-\u001F\u007F-\u009F]/gu;
|
|
6
|
+
const stripControlChars = (value) => value === void 0 ? void 0 : value.replaceAll(CONTROL_CHARS, "");
|
|
7
|
+
const listItemDirectories = (root) => readdirSync(root).filter((entry) => {
|
|
8
|
+
const full = join(root, entry);
|
|
9
|
+
return statSync(full).isDirectory() && existsSync(join(full, "registry.json"));
|
|
10
|
+
});
|
|
11
|
+
const collectCatalog = (root) => {
|
|
12
|
+
const indexPath = join(root, "index.json");
|
|
13
|
+
if (existsSync(indexPath)) {
|
|
14
|
+
const parsed = JSON.parse(readFileSync(indexPath, "utf8"));
|
|
15
|
+
if (Array.isArray(parsed.items)) {
|
|
16
|
+
return parsed.items.filter((entry) => typeof entry === "object" && entry !== null && typeof entry.name === "string").map((entry) => {
|
|
17
|
+
return { description: stripControlChars(entry.description), name: stripControlChars(entry.name) ?? entry.name };
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return listItemDirectories(root).map((name) => {
|
|
22
|
+
const raw = JSON.parse(readFileSync(join(root, name, "registry.json"), "utf8"));
|
|
23
|
+
return { description: stripControlChars(raw.description), name };
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
const buildRegistryIndex = (root) => {
|
|
27
|
+
const items = listItemDirectories(root).map((name) => {
|
|
28
|
+
const manifest = parseManifest(JSON.parse(readFileSync(join(root, name, "registry.json"), "utf8")), name);
|
|
29
|
+
return {
|
|
30
|
+
...manifest.description === void 0 ? {} : { description: manifest.description },
|
|
31
|
+
name: manifest.name,
|
|
32
|
+
...manifest.title === void 0 ? {} : { title: manifest.title }
|
|
33
|
+
};
|
|
34
|
+
}).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
35
|
+
return { items };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export { buildRegistryIndex, collectCatalog, listItemDirectories };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createLogger } from './createLogger-CHPNjFw2.mjs';
|
|
2
|
+
|
|
3
|
+
const defineHandler = (body) => async (toolbox) => {
|
|
4
|
+
const logger = createLogger();
|
|
5
|
+
try {
|
|
6
|
+
const { code } = await body({ argument: toolbox.argument, cwd: toolbox.process.cwd, logger, options: toolbox.options });
|
|
7
|
+
toolbox.process.exit(code);
|
|
8
|
+
} catch (error) {
|
|
9
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
10
|
+
toolbox.process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export { defineHandler as d };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { JsonReporter } from '@visulima/pail/reporter/json';
|
|
2
|
+
import { PrettyReporter } from '@visulima/pail/reporter/pretty';
|
|
3
|
+
import { createPail } from '@visulima/pail/server';
|
|
4
|
+
|
|
5
|
+
const wantJson = () => {
|
|
6
|
+
const flag = process.env.LUNORA_LOG_JSON;
|
|
7
|
+
return flag === "1" || flag === "true";
|
|
8
|
+
};
|
|
9
|
+
const buildReporter = () => {
|
|
10
|
+
const Reporter = wantJson() ? JsonReporter : PrettyReporter;
|
|
11
|
+
return new Reporter();
|
|
12
|
+
};
|
|
13
|
+
let sharedPail;
|
|
14
|
+
const getPail = () => {
|
|
15
|
+
sharedPail ??= createPail({
|
|
16
|
+
reporters: [buildReporter()],
|
|
17
|
+
scope: ["lunora"],
|
|
18
|
+
stderr: process.stderr,
|
|
19
|
+
stdout: process.stdout
|
|
20
|
+
});
|
|
21
|
+
return sharedPail;
|
|
22
|
+
};
|
|
23
|
+
const createLogger = () => {
|
|
24
|
+
return {
|
|
25
|
+
debug: (message) => {
|
|
26
|
+
getPail().debug(message);
|
|
27
|
+
},
|
|
28
|
+
error: (message) => {
|
|
29
|
+
getPail().error(message);
|
|
30
|
+
},
|
|
31
|
+
info: (message) => {
|
|
32
|
+
getPail().info(message);
|
|
33
|
+
},
|
|
34
|
+
success: (message) => {
|
|
35
|
+
getPail().success(message);
|
|
36
|
+
},
|
|
37
|
+
warn: (message) => {
|
|
38
|
+
getPail().warn(message);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
const createStderrLogger = () => {
|
|
43
|
+
const write = (tag, message) => {
|
|
44
|
+
process.stderr.write(`${tag} ${message}
|
|
45
|
+
`);
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
debug: (message) => {
|
|
49
|
+
write("debug", message);
|
|
50
|
+
},
|
|
51
|
+
error: (message) => {
|
|
52
|
+
write("error", message);
|
|
53
|
+
},
|
|
54
|
+
info: (message) => {
|
|
55
|
+
write("info ", message);
|
|
56
|
+
},
|
|
57
|
+
success: (message) => {
|
|
58
|
+
write("ok ", message);
|
|
59
|
+
},
|
|
60
|
+
warn: (message) => {
|
|
61
|
+
write("warn ", message);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
const pail = /* @__PURE__ */ new Proxy({}, {
|
|
66
|
+
get(_target, property) {
|
|
67
|
+
const instance = getPail();
|
|
68
|
+
const value = instance[property];
|
|
69
|
+
return typeof value === "function" ? value.bind(instance) : value;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
export { createLogger, createStderrLogger, getPail, pail };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const defaultSpawner = (descriptor) => new Promise((resolve, reject) => {
|
|
4
|
+
const hasInput = typeof descriptor.input === "string";
|
|
5
|
+
const wantCapture = descriptor.captureStdout === true;
|
|
6
|
+
let stdout = "inherit";
|
|
7
|
+
if (wantCapture) {
|
|
8
|
+
stdout = "pipe";
|
|
9
|
+
} else if (descriptor.stdoutToStderr) {
|
|
10
|
+
stdout = 2;
|
|
11
|
+
}
|
|
12
|
+
const child = spawn(descriptor.command, [...descriptor.args], {
|
|
13
|
+
cwd: descriptor.cwd ?? process.cwd(),
|
|
14
|
+
env: descriptor.env ? { ...process.env, ...descriptor.env } : process.env,
|
|
15
|
+
stdio: [hasInput ? "pipe" : "inherit", stdout, "inherit"]
|
|
16
|
+
});
|
|
17
|
+
let captured = "";
|
|
18
|
+
if (wantCapture && child.stdout) {
|
|
19
|
+
child.stdout.on("data", (chunk) => {
|
|
20
|
+
captured += chunk.toString("utf8");
|
|
21
|
+
process.stdout.write(chunk);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
child.on("error", (error) => {
|
|
25
|
+
reject(error);
|
|
26
|
+
});
|
|
27
|
+
child.on("exit", (code, signal) => {
|
|
28
|
+
resolve({ code: code ?? (signal ? 1 : 0), stdout: wantCapture ? captured : void 0 });
|
|
29
|
+
});
|
|
30
|
+
if (hasInput && child.stdin) {
|
|
31
|
+
child.stdin.end(descriptor.input);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const createRecordingSpawner = (exitCode = 0) => {
|
|
35
|
+
const calls = [];
|
|
36
|
+
const spawner = (descriptor) => {
|
|
37
|
+
calls.push({ descriptor });
|
|
38
|
+
return Promise.resolve({ code: exitCode });
|
|
39
|
+
};
|
|
40
|
+
return { calls, spawner };
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export { createRecordingSpawner, defaultSpawner };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { quoteIdentifier, columnRef, physicalIndexName, frameworkColumnDdl, sqlAffinityForKind } from '@lunora/d1/dialect';
|
|
2
|
+
|
|
3
|
+
const validatorKindToSqlType = (kind) => sqlAffinityForKind(kind);
|
|
4
|
+
const renderColumnDefinition = (name, column) => {
|
|
5
|
+
const parts = [quoteIdentifier(name), column.sqlType];
|
|
6
|
+
if (!column.nullable) {
|
|
7
|
+
parts.push("NOT NULL");
|
|
8
|
+
}
|
|
9
|
+
return parts.join(" ");
|
|
10
|
+
};
|
|
11
|
+
const renderCreateTable = (table) => {
|
|
12
|
+
const columns = Object.entries(table.columns).map(([columnName, column]) => ` ${renderColumnDefinition(columnName, column)}`);
|
|
13
|
+
const lines = [...frameworkColumnDdl().map((column) => ` ${column}`), ...columns].join(",\n");
|
|
14
|
+
return `CREATE TABLE IF NOT EXISTS ${quoteIdentifier(table.name)} (
|
|
15
|
+
${lines}
|
|
16
|
+
);`;
|
|
17
|
+
};
|
|
18
|
+
const renderDropTable = (tableName) => `DROP TABLE IF EXISTS ${quoteIdentifier(tableName)};`;
|
|
19
|
+
const renderAddColumn = (tableName, columnName, column) => `ALTER TABLE ${quoteIdentifier(tableName)} ADD COLUMN ${renderColumnDefinition(columnName, column)};`;
|
|
20
|
+
const renderCreateIndex = (tableName, index) => {
|
|
21
|
+
const fields = index.fields.map((field) => columnRef(field)).join(", ");
|
|
22
|
+
const uniqueClause = index.unique ? "UNIQUE " : "";
|
|
23
|
+
return `CREATE ${uniqueClause}INDEX IF NOT EXISTS ${physicalIndexName(tableName, index.name)} ON ${quoteIdentifier(tableName)} (${fields});`;
|
|
24
|
+
};
|
|
25
|
+
const renderDropIndex = (tableName, indexName) => `DROP INDEX IF EXISTS ${physicalIndexName(tableName, indexName)};`;
|
|
26
|
+
const diffExistingColumn = (tableName, columnName, old, column, unsupported) => {
|
|
27
|
+
if (old.sqlType !== column.sqlType) {
|
|
28
|
+
unsupported.push({
|
|
29
|
+
kind: "columnTypeChange",
|
|
30
|
+
summary: `column type change on ${tableName}.${columnName}: ${old.sqlType} → ${column.sqlType} (write SQL manually)`
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
if (old.nullable !== column.nullable) {
|
|
34
|
+
unsupported.push({
|
|
35
|
+
kind: "columnTypeChange",
|
|
36
|
+
summary: `nullability change on ${tableName}.${columnName}: ${old.nullable ? "NULL" : "NOT NULL"} → ${column.nullable ? "NULL" : "NOT NULL"} (write SQL manually)`
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const diffColumns = (tableName, previous, next, entries, unsupported) => {
|
|
41
|
+
for (const [columnName, column] of Object.entries(next)) {
|
|
42
|
+
const old = previous[columnName];
|
|
43
|
+
if (old === void 0) {
|
|
44
|
+
entries.push({
|
|
45
|
+
kind: "addColumn",
|
|
46
|
+
sql: renderAddColumn(tableName, columnName, column),
|
|
47
|
+
summary: `ADD COLUMN ${tableName}.${columnName}`
|
|
48
|
+
});
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
diffExistingColumn(tableName, columnName, old, column, unsupported);
|
|
52
|
+
}
|
|
53
|
+
for (const columnName of Object.keys(previous)) {
|
|
54
|
+
if (next[columnName] === void 0) {
|
|
55
|
+
unsupported.push({
|
|
56
|
+
kind: "dropColumn",
|
|
57
|
+
summary: `DROP COLUMN ${tableName}.${columnName} (SQLite drop-column requires careful migration — write SQL manually)`
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const diffIndexes = (tableName, previous, next, entries, unsupported) => {
|
|
63
|
+
for (const [indexName, index] of Object.entries(next)) {
|
|
64
|
+
const old = previous[indexName];
|
|
65
|
+
if (old === void 0) {
|
|
66
|
+
entries.push({
|
|
67
|
+
kind: "createIndex",
|
|
68
|
+
sql: renderCreateIndex(tableName, index),
|
|
69
|
+
summary: `CREATE INDEX ${indexName} ON ${tableName}`
|
|
70
|
+
});
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const fieldsEqual = old.fields.length === index.fields.length && old.fields.every((field, i) => field === index.fields[i]);
|
|
74
|
+
if (!fieldsEqual || old.unique !== index.unique) {
|
|
75
|
+
unsupported.push({
|
|
76
|
+
kind: "indexRename",
|
|
77
|
+
summary: `index ${indexName} changed on ${tableName} — drop+create manually if intentional`
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const indexName of Object.keys(previous)) {
|
|
82
|
+
if (next[indexName] === void 0) {
|
|
83
|
+
entries.push({
|
|
84
|
+
kind: "dropIndex",
|
|
85
|
+
sql: renderDropIndex(tableName, indexName),
|
|
86
|
+
summary: `DROP INDEX ${indexName}`
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const diffNewTable = (tableName, table, entries) => {
|
|
92
|
+
entries.push({
|
|
93
|
+
kind: "createTable",
|
|
94
|
+
sql: renderCreateTable(table),
|
|
95
|
+
summary: `CREATE TABLE ${tableName}`
|
|
96
|
+
});
|
|
97
|
+
for (const index of Object.values(table.indexes)) {
|
|
98
|
+
entries.push({
|
|
99
|
+
kind: "createIndex",
|
|
100
|
+
sql: renderCreateIndex(tableName, index),
|
|
101
|
+
summary: `CREATE INDEX ${index.name} ON ${tableName}`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const diffSnapshots = (previous, next) => {
|
|
106
|
+
const previousTables = previous?.tables ?? {};
|
|
107
|
+
const entries = [];
|
|
108
|
+
const unsupported = [];
|
|
109
|
+
for (const [tableName, table] of Object.entries(next.tables)) {
|
|
110
|
+
const old = previousTables[tableName];
|
|
111
|
+
if (old === void 0) {
|
|
112
|
+
diffNewTable(tableName, table, entries);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
diffColumns(tableName, old.columns, table.columns, entries, unsupported);
|
|
116
|
+
diffIndexes(tableName, old.indexes, table.indexes, entries, unsupported);
|
|
117
|
+
}
|
|
118
|
+
for (const [tableName] of Object.entries(previousTables)) {
|
|
119
|
+
if (next.tables[tableName] === void 0) {
|
|
120
|
+
entries.push({
|
|
121
|
+
kind: "dropTable",
|
|
122
|
+
sql: renderDropTable(tableName),
|
|
123
|
+
summary: `DROP TABLE ${tableName}`
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
empty: entries.length === 0 && unsupported.length === 0,
|
|
129
|
+
entries,
|
|
130
|
+
unsupported
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
const renderMigrationFile = (name, diff, generatedAt) => {
|
|
134
|
+
const lines = [
|
|
135
|
+
`-- Lunora migration: ${name}`,
|
|
136
|
+
`-- Generated at ${generatedAt}`,
|
|
137
|
+
"-- This file was produced by `lunora migrate generate`. Review carefully before applying.",
|
|
138
|
+
""
|
|
139
|
+
];
|
|
140
|
+
for (const entry of diff.entries) {
|
|
141
|
+
lines.push(`-- ${entry.summary}`, entry.sql, "");
|
|
142
|
+
}
|
|
143
|
+
if (diff.unsupported.length > 0) {
|
|
144
|
+
lines.push(
|
|
145
|
+
"-- ---------------------------------------------------------------",
|
|
146
|
+
"-- The following deltas are NOT auto-generated in v0.1.",
|
|
147
|
+
"-- Write the appropriate SQL below by hand:",
|
|
148
|
+
"--"
|
|
149
|
+
);
|
|
150
|
+
for (const entry of diff.unsupported) {
|
|
151
|
+
lines.push(`-- * ${entry.summary}`);
|
|
152
|
+
}
|
|
153
|
+
lines.push("-- ---------------------------------------------------------------", "");
|
|
154
|
+
}
|
|
155
|
+
if (diff.empty) {
|
|
156
|
+
lines.push("-- No changes detected. Re-running `lunora migrate generate` will overwrite this file.", "");
|
|
157
|
+
}
|
|
158
|
+
return lines.join("\n");
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
export { diffSnapshots, renderAddColumn, renderCreateIndex, renderCreateTable, renderDropIndex, renderDropTable, renderMigrationFile, validatorKindToSqlType };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const isDockerAvailable = () => {
|
|
4
|
+
try {
|
|
5
|
+
return spawnSync("docker", ["info"], { stdio: "ignore" }).status === 0;
|
|
6
|
+
} catch {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
const isRailpackAvailable = () => {
|
|
11
|
+
if (typeof process.env.BUILDKIT_HOST !== "string" || process.env.BUILDKIT_HOST.length === 0) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return spawnSync("railpack", ["--version"], { stdio: "ignore" }).status === 0;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export { isRailpackAvailable as a, isDockerAvailable as i };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const AUTH_PROVIDER_OPTIONS = [
|
|
2
|
+
{ description: "Email + password on better-auth (default)", label: "Email & password", value: "auth" },
|
|
3
|
+
{ description: "Clerk-hosted auth", label: "Clerk", value: "auth-clerk" },
|
|
4
|
+
{ description: "Auth0 (OIDC)", label: "Auth0", value: "auth-auth0" }
|
|
5
|
+
];
|
|
6
|
+
const DEFAULT_AUTH_ITEM = "auth";
|
|
7
|
+
const promptAuthProvider = async (select) => await select("Which auth provider?", AUTH_PROVIDER_OPTIONS, { default: DEFAULT_AUTH_ITEM }) ?? DEFAULT_AUTH_ITEM;
|
|
8
|
+
const EMAIL_ITEM = "mail";
|
|
9
|
+
const normalizeFeature = (raw) => {
|
|
10
|
+
const value = raw.trim();
|
|
11
|
+
if (value === "") {
|
|
12
|
+
return void 0;
|
|
13
|
+
}
|
|
14
|
+
const lower = value.toLowerCase();
|
|
15
|
+
if (lower === "auth") {
|
|
16
|
+
return { kind: "auth" };
|
|
17
|
+
}
|
|
18
|
+
if (lower === "email" || lower === "mail") {
|
|
19
|
+
return { kind: "email" };
|
|
20
|
+
}
|
|
21
|
+
return { item: lower, kind: "item" };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export { AUTH_PROVIDER_OPTIONS as A, DEFAULT_AUTH_ITEM as D, EMAIL_ITEM as E, normalizeFeature as n, promptAuthProvider as p };
|