@korajs/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.cjs +2170 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/bin.js +1640 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-N36PFOSA.js +103 -0
- package/dist/chunk-N36PFOSA.js.map +1 -0
- package/dist/chunk-REOTYAM6.js +370 -0
- package/dist/chunk-REOTYAM6.js.map +1 -0
- package/dist/chunk-ZVB4HAB3.js +88 -0
- package/dist/chunk-ZVB4HAB3.js.map +1 -0
- package/dist/create.cjs +313 -0
- package/dist/create.cjs.map +1 -0
- package/dist/create.js +10 -0
- package/dist/create.js.map +1 -0
- package/dist/index.cjs +218 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +72 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/templates/react-basic/index.html.hbs +12 -0
- package/templates/react-basic/kora.config.ts +13 -0
- package/templates/react-basic/package.json.hbs +26 -0
- package/templates/react-basic/src/App.tsx +48 -0
- package/templates/react-basic/src/main.tsx +16 -0
- package/templates/react-basic/src/schema.ts +15 -0
- package/templates/react-basic/tsconfig.json +14 -0
- package/templates/react-basic/vite.config.ts +6 -0
- package/templates/react-sync/index.html.hbs +12 -0
- package/templates/react-sync/kora.config.ts +17 -0
- package/templates/react-sync/package.json.hbs +28 -0
- package/templates/react-sync/server.ts +11 -0
- package/templates/react-sync/src/App.tsx +50 -0
- package/templates/react-sync/src/main.tsx +24 -0
- package/templates/react-sync/src/schema.ts +15 -0
- package/templates/react-sync/tsconfig.json +14 -0
- package/templates/react-sync/vite.config.ts +6 -0
package/dist/bin.cjs
ADDED
|
@@ -0,0 +1,2170 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// src/bin.ts
|
|
26
|
+
var import_citty5 = require("citty");
|
|
27
|
+
|
|
28
|
+
// src/commands/create/create-command.ts
|
|
29
|
+
var import_node_child_process2 = require("child_process");
|
|
30
|
+
var import_node_fs = require("fs");
|
|
31
|
+
var import_node_path3 = require("path");
|
|
32
|
+
var import_node_url2 = require("url");
|
|
33
|
+
var import_citty = require("citty");
|
|
34
|
+
|
|
35
|
+
// src/errors.ts
|
|
36
|
+
var import_core = require("@korajs/core");
|
|
37
|
+
var ProjectExistsError = class extends import_core.KoraError {
|
|
38
|
+
constructor(directory) {
|
|
39
|
+
super(
|
|
40
|
+
`Directory "${directory}" already exists. Choose a different name or remove the existing directory.`,
|
|
41
|
+
"PROJECT_EXISTS",
|
|
42
|
+
{ directory }
|
|
43
|
+
);
|
|
44
|
+
this.directory = directory;
|
|
45
|
+
this.name = "ProjectExistsError";
|
|
46
|
+
}
|
|
47
|
+
directory;
|
|
48
|
+
};
|
|
49
|
+
var SchemaNotFoundError = class extends import_core.KoraError {
|
|
50
|
+
constructor(searchedPaths) {
|
|
51
|
+
super(
|
|
52
|
+
`Could not find a schema file. Searched: ${searchedPaths.join(", ")}. Create a schema file using defineSchema() from @korajs/core.`,
|
|
53
|
+
"SCHEMA_NOT_FOUND",
|
|
54
|
+
{ searchedPaths }
|
|
55
|
+
);
|
|
56
|
+
this.searchedPaths = searchedPaths;
|
|
57
|
+
this.name = "SchemaNotFoundError";
|
|
58
|
+
}
|
|
59
|
+
searchedPaths;
|
|
60
|
+
};
|
|
61
|
+
var InvalidProjectError = class extends import_core.KoraError {
|
|
62
|
+
constructor(directory) {
|
|
63
|
+
super(
|
|
64
|
+
`"${directory}" is not a valid Kora project. No package.json with a kora dependency found. Run this command from inside a Kora project.`,
|
|
65
|
+
"INVALID_PROJECT",
|
|
66
|
+
{ directory }
|
|
67
|
+
);
|
|
68
|
+
this.directory = directory;
|
|
69
|
+
this.name = "InvalidProjectError";
|
|
70
|
+
}
|
|
71
|
+
directory;
|
|
72
|
+
};
|
|
73
|
+
var DevServerError = class extends import_core.KoraError {
|
|
74
|
+
constructor(binary, searchPath) {
|
|
75
|
+
super(
|
|
76
|
+
`Could not find required binary "${binary}" at ${searchPath}. Install project dependencies and try again.`,
|
|
77
|
+
"DEV_SERVER_ERROR",
|
|
78
|
+
{ binary, searchPath }
|
|
79
|
+
);
|
|
80
|
+
this.binary = binary;
|
|
81
|
+
this.searchPath = searchPath;
|
|
82
|
+
this.name = "DevServerError";
|
|
83
|
+
}
|
|
84
|
+
binary;
|
|
85
|
+
searchPath;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// src/types.ts
|
|
89
|
+
var PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
|
|
90
|
+
var TEMPLATES = ["react-basic", "react-sync"];
|
|
91
|
+
var TEMPLATE_INFO = [
|
|
92
|
+
{
|
|
93
|
+
name: "react-basic",
|
|
94
|
+
label: "React (basic)",
|
|
95
|
+
description: "Local-only React app with Kora \u2014 no sync server"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "react-sync",
|
|
99
|
+
label: "React (with sync)",
|
|
100
|
+
description: "React app with Kora sync server included"
|
|
101
|
+
}
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// src/utils/fs-helpers.ts
|
|
105
|
+
var import_promises = require("fs/promises");
|
|
106
|
+
var import_node_path = require("path");
|
|
107
|
+
async function directoryExists(path) {
|
|
108
|
+
try {
|
|
109
|
+
await (0, import_promises.access)(path);
|
|
110
|
+
return true;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function findProjectRoot(startDir) {
|
|
116
|
+
let current = (0, import_node_path.resolve)(startDir ?? process.cwd());
|
|
117
|
+
for (; ; ) {
|
|
118
|
+
const pkgPath = (0, import_node_path.join)(current, "package.json");
|
|
119
|
+
try {
|
|
120
|
+
const content = await (0, import_promises.readFile)(pkgPath, "utf-8");
|
|
121
|
+
const pkg = JSON.parse(content);
|
|
122
|
+
if (isKoraProject(pkg)) {
|
|
123
|
+
return current;
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
const parent = (0, import_node_path.dirname)(current);
|
|
128
|
+
if (parent === current) break;
|
|
129
|
+
current = parent;
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
async function findSchemaFile(projectRoot) {
|
|
134
|
+
const candidates = [
|
|
135
|
+
(0, import_node_path.join)(projectRoot, "src", "schema.ts"),
|
|
136
|
+
(0, import_node_path.join)(projectRoot, "schema.ts"),
|
|
137
|
+
(0, import_node_path.join)(projectRoot, "src", "schema.js"),
|
|
138
|
+
(0, import_node_path.join)(projectRoot, "schema.js")
|
|
139
|
+
];
|
|
140
|
+
for (const candidate of candidates) {
|
|
141
|
+
try {
|
|
142
|
+
await (0, import_promises.access)(candidate);
|
|
143
|
+
return candidate;
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
async function resolveProjectBinary(projectRoot, binaryName) {
|
|
150
|
+
const binaryPath = (0, import_node_path.join)(projectRoot, "node_modules", ".bin", binaryName);
|
|
151
|
+
try {
|
|
152
|
+
await (0, import_promises.access)(binaryPath);
|
|
153
|
+
return binaryPath;
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function isKoraProject(pkg) {
|
|
159
|
+
if (typeof pkg !== "object" || pkg === null) return false;
|
|
160
|
+
const record = pkg;
|
|
161
|
+
return hasKoraDep(record.dependencies) || hasKoraDep(record.devDependencies);
|
|
162
|
+
}
|
|
163
|
+
function hasKoraDep(deps) {
|
|
164
|
+
if (typeof deps !== "object" || deps === null) return false;
|
|
165
|
+
return Object.keys(deps).some((key) => key === "kora" || key.startsWith("@korajs/"));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/utils/logger.ts
|
|
169
|
+
var RESET = "\x1B[0m";
|
|
170
|
+
var BOLD = "\x1B[1m";
|
|
171
|
+
var DIM = "\x1B[2m";
|
|
172
|
+
var GREEN = "\x1B[32m";
|
|
173
|
+
var YELLOW = "\x1B[33m";
|
|
174
|
+
var RED = "\x1B[31m";
|
|
175
|
+
var CYAN = "\x1B[36m";
|
|
176
|
+
function createLogger(options) {
|
|
177
|
+
const colorDisabled = options?.noColor === true || process.env.NO_COLOR !== void 0 || !process.stdout.isTTY;
|
|
178
|
+
function color(code, text) {
|
|
179
|
+
return colorDisabled ? text : `${code}${text}${RESET}`;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
info(message) {
|
|
183
|
+
console.log(color(CYAN, message));
|
|
184
|
+
},
|
|
185
|
+
success(message) {
|
|
186
|
+
console.log(color(GREEN, ` \u2713 ${message}`));
|
|
187
|
+
},
|
|
188
|
+
warn(message) {
|
|
189
|
+
console.warn(color(YELLOW, ` \u26A0 ${message}`));
|
|
190
|
+
},
|
|
191
|
+
error(message) {
|
|
192
|
+
console.error(color(RED, ` \u2717 ${message}`));
|
|
193
|
+
},
|
|
194
|
+
step(message) {
|
|
195
|
+
console.log(color(DIM, ` ${message}`));
|
|
196
|
+
},
|
|
197
|
+
blank() {
|
|
198
|
+
console.log();
|
|
199
|
+
},
|
|
200
|
+
banner() {
|
|
201
|
+
console.log();
|
|
202
|
+
console.log(
|
|
203
|
+
color(BOLD + CYAN, " Kora.js") + color(DIM, " \u2014 Offline-first application framework")
|
|
204
|
+
);
|
|
205
|
+
console.log();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/utils/package-manager.ts
|
|
211
|
+
var import_node_child_process = require("child_process");
|
|
212
|
+
function detectPackageManager() {
|
|
213
|
+
const userAgent = process.env.npm_config_user_agent;
|
|
214
|
+
if (!userAgent) return "npm";
|
|
215
|
+
if (userAgent.startsWith("pnpm/")) return "pnpm";
|
|
216
|
+
if (userAgent.startsWith("yarn/")) return "yarn";
|
|
217
|
+
if (userAgent.startsWith("bun/")) return "bun";
|
|
218
|
+
return "npm";
|
|
219
|
+
}
|
|
220
|
+
function getInstallCommand(pm) {
|
|
221
|
+
return pm === "yarn" ? "yarn" : `${pm} install`;
|
|
222
|
+
}
|
|
223
|
+
function getRunDevCommand(pm) {
|
|
224
|
+
if (pm === "npm") return "npm run dev";
|
|
225
|
+
return `${pm} dev`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/utils/prompt.ts
|
|
229
|
+
var import_node_readline = require("readline");
|
|
230
|
+
function promptText(message, defaultValue, options) {
|
|
231
|
+
return new Promise((resolve7) => {
|
|
232
|
+
const rl = createReadline(options);
|
|
233
|
+
const suffix = defaultValue !== void 0 ? ` (${defaultValue})` : "";
|
|
234
|
+
rl.question(` ? ${message}${suffix}: `, (answer) => {
|
|
235
|
+
rl.close();
|
|
236
|
+
const trimmed = answer.trim();
|
|
237
|
+
resolve7(trimmed || defaultValue || "");
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function promptSelect(message, choices, options) {
|
|
242
|
+
return new Promise((resolve7) => {
|
|
243
|
+
const rl = createReadline(options);
|
|
244
|
+
const out = options?.output ?? process.stdout;
|
|
245
|
+
out.write(` ? ${message}
|
|
246
|
+
`);
|
|
247
|
+
for (let i = 0; i < choices.length; i++) {
|
|
248
|
+
const choice = choices[i];
|
|
249
|
+
if (choice) {
|
|
250
|
+
out.write(` ${i + 1}) ${choice.label}
|
|
251
|
+
`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const ask = () => {
|
|
255
|
+
rl.question(" > ", (answer) => {
|
|
256
|
+
const index = Number.parseInt(answer.trim(), 10) - 1;
|
|
257
|
+
const selected = choices[index];
|
|
258
|
+
if (selected) {
|
|
259
|
+
rl.close();
|
|
260
|
+
resolve7(selected.value);
|
|
261
|
+
} else {
|
|
262
|
+
out.write(` Please enter a number between 1 and ${choices.length}
|
|
263
|
+
`);
|
|
264
|
+
ask();
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
ask();
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
function promptConfirm(message, defaultValue = false, options) {
|
|
272
|
+
return new Promise((resolve7) => {
|
|
273
|
+
const rl = createReadline(options);
|
|
274
|
+
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
275
|
+
const ask = () => {
|
|
276
|
+
rl.question(` ? ${message} (${suffix}): `, (answer) => {
|
|
277
|
+
const normalized = answer.trim().toLowerCase();
|
|
278
|
+
if (normalized.length === 0) {
|
|
279
|
+
rl.close();
|
|
280
|
+
resolve7(defaultValue);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (normalized === "y" || normalized === "yes") {
|
|
284
|
+
rl.close();
|
|
285
|
+
resolve7(true);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (normalized === "n" || normalized === "no") {
|
|
289
|
+
rl.close();
|
|
290
|
+
resolve7(false);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
(options?.output ?? process.stdout).write(" Please answer with y or n\n");
|
|
294
|
+
ask();
|
|
295
|
+
});
|
|
296
|
+
};
|
|
297
|
+
ask();
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function createReadline(options) {
|
|
301
|
+
return (0, import_node_readline.createInterface)({
|
|
302
|
+
input: options?.input ?? process.stdin,
|
|
303
|
+
output: options?.output ?? process.stdout
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/commands/create/template-engine.ts
|
|
308
|
+
var import_promises2 = require("fs/promises");
|
|
309
|
+
var import_node_path2 = require("path");
|
|
310
|
+
var import_node_url = require("url");
|
|
311
|
+
var import_meta = {};
|
|
312
|
+
function substituteVariables(template, context) {
|
|
313
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
|
314
|
+
const value = context[key];
|
|
315
|
+
return value !== void 0 ? value : `{{${key}}}`;
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function getTemplatePath(templateName) {
|
|
319
|
+
const currentDir = (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
320
|
+
return (0, import_node_path2.resolve)(currentDir, "..", "..", "..", "templates", templateName);
|
|
321
|
+
}
|
|
322
|
+
async function scaffoldTemplate(templateName, targetDir, context) {
|
|
323
|
+
const templateDir = getTemplatePath(templateName);
|
|
324
|
+
const vars = {
|
|
325
|
+
projectName: context.projectName,
|
|
326
|
+
packageManager: context.packageManager,
|
|
327
|
+
koraVersion: context.koraVersion
|
|
328
|
+
};
|
|
329
|
+
await copyDirectory(templateDir, targetDir, vars);
|
|
330
|
+
}
|
|
331
|
+
async function copyDirectory(src, dest, vars) {
|
|
332
|
+
await (0, import_promises2.mkdir)(dest, { recursive: true });
|
|
333
|
+
const entries = await (0, import_promises2.readdir)(src);
|
|
334
|
+
for (const entry of entries) {
|
|
335
|
+
const srcPath = (0, import_node_path2.join)(src, entry);
|
|
336
|
+
const srcStat = await (0, import_promises2.stat)(srcPath);
|
|
337
|
+
if (srcStat.isDirectory()) {
|
|
338
|
+
await copyDirectory(srcPath, (0, import_node_path2.join)(dest, entry), vars);
|
|
339
|
+
} else if (entry.endsWith(".hbs")) {
|
|
340
|
+
const content = await (0, import_promises2.readFile)(srcPath, "utf-8");
|
|
341
|
+
const outputName = entry.slice(0, -4);
|
|
342
|
+
await (0, import_promises2.writeFile)((0, import_node_path2.join)(dest, outputName), substituteVariables(content, vars), "utf-8");
|
|
343
|
+
} else {
|
|
344
|
+
await (0, import_promises2.copyFile)(srcPath, (0, import_node_path2.join)(dest, entry));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/commands/create/create-command.ts
|
|
350
|
+
var import_meta2 = {};
|
|
351
|
+
var createCommand = (0, import_citty.defineCommand)({
|
|
352
|
+
meta: {
|
|
353
|
+
name: "create",
|
|
354
|
+
description: "Create a new Kora application"
|
|
355
|
+
},
|
|
356
|
+
args: {
|
|
357
|
+
name: {
|
|
358
|
+
type: "positional",
|
|
359
|
+
description: "Project directory name",
|
|
360
|
+
required: false
|
|
361
|
+
},
|
|
362
|
+
template: {
|
|
363
|
+
type: "string",
|
|
364
|
+
description: "Project template (react-basic, react-sync)"
|
|
365
|
+
},
|
|
366
|
+
pm: {
|
|
367
|
+
type: "string",
|
|
368
|
+
description: "Package manager (pnpm, npm, yarn, bun)"
|
|
369
|
+
},
|
|
370
|
+
"skip-install": {
|
|
371
|
+
type: "boolean",
|
|
372
|
+
description: "Skip installing dependencies",
|
|
373
|
+
default: false
|
|
374
|
+
}
|
|
375
|
+
},
|
|
376
|
+
async run({ args }) {
|
|
377
|
+
const logger = createLogger();
|
|
378
|
+
logger.banner();
|
|
379
|
+
const projectName = args.name || await promptText("Project name", "my-kora-app");
|
|
380
|
+
if (!projectName) {
|
|
381
|
+
logger.error("Project name is required");
|
|
382
|
+
process.exitCode = 1;
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
let template;
|
|
386
|
+
if (args.template && isValidTemplate(args.template)) {
|
|
387
|
+
template = args.template;
|
|
388
|
+
} else {
|
|
389
|
+
template = await promptSelect(
|
|
390
|
+
"Select a template:",
|
|
391
|
+
TEMPLATE_INFO.map((t) => ({ label: `${t.label} \u2014 ${t.description}`, value: t.name }))
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
let pm;
|
|
395
|
+
if (args.pm && isValidPackageManager(args.pm)) {
|
|
396
|
+
pm = args.pm;
|
|
397
|
+
} else {
|
|
398
|
+
const detected = detectPackageManager();
|
|
399
|
+
pm = await promptSelect(
|
|
400
|
+
"Package manager:",
|
|
401
|
+
PACKAGE_MANAGERS.map((p) => ({
|
|
402
|
+
label: p === detected ? `${p} (detected)` : p,
|
|
403
|
+
value: p
|
|
404
|
+
}))
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
const targetDir = (0, import_node_path3.resolve)(process.cwd(), projectName);
|
|
408
|
+
if (await directoryExists(targetDir)) {
|
|
409
|
+
throw new ProjectExistsError(projectName);
|
|
410
|
+
}
|
|
411
|
+
const koraVersion = resolveKoraVersion();
|
|
412
|
+
logger.step(`Creating ${projectName} with ${template} template...`);
|
|
413
|
+
await scaffoldTemplate(template, targetDir, {
|
|
414
|
+
projectName,
|
|
415
|
+
packageManager: pm,
|
|
416
|
+
koraVersion
|
|
417
|
+
});
|
|
418
|
+
logger.success("Project scaffolded");
|
|
419
|
+
if (!args["skip-install"]) {
|
|
420
|
+
logger.step("Installing dependencies...");
|
|
421
|
+
try {
|
|
422
|
+
(0, import_node_child_process2.execSync)(getInstallCommand(pm), { cwd: targetDir, stdio: "inherit" });
|
|
423
|
+
logger.success("Dependencies installed");
|
|
424
|
+
} catch {
|
|
425
|
+
logger.warn("Failed to install dependencies. Run install manually.");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
logger.blank();
|
|
429
|
+
logger.info("Done! Next steps:");
|
|
430
|
+
logger.blank();
|
|
431
|
+
logger.step(` cd ${projectName}`);
|
|
432
|
+
logger.step(` ${getRunDevCommand(pm)}`);
|
|
433
|
+
logger.blank();
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
function isValidTemplate(value) {
|
|
437
|
+
return TEMPLATES.includes(value);
|
|
438
|
+
}
|
|
439
|
+
function isValidPackageManager(value) {
|
|
440
|
+
return PACKAGE_MANAGERS.includes(value);
|
|
441
|
+
}
|
|
442
|
+
function resolveKoraVersion() {
|
|
443
|
+
try {
|
|
444
|
+
const cliDir = (0, import_node_path3.resolve)((0, import_node_path3.dirname)((0, import_node_url2.fileURLToPath)(import_meta2.url)), "..", "..", "..");
|
|
445
|
+
const pkg = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path3.resolve)(cliDir, "package.json"), "utf-8"));
|
|
446
|
+
return pkg.version === "0.0.0" ? "latest" : `^${pkg.version}`;
|
|
447
|
+
} catch {
|
|
448
|
+
return "latest";
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/commands/dev/dev-command.ts
|
|
453
|
+
var import_promises4 = require("fs/promises");
|
|
454
|
+
var import_node_path5 = require("path");
|
|
455
|
+
var import_node_path6 = require("path");
|
|
456
|
+
var import_citty2 = require("citty");
|
|
457
|
+
|
|
458
|
+
// src/commands/dev/kora-config.ts
|
|
459
|
+
var import_node_child_process3 = require("child_process");
|
|
460
|
+
var import_promises3 = require("fs/promises");
|
|
461
|
+
var import_node_path4 = require("path");
|
|
462
|
+
var import_node_url3 = require("url");
|
|
463
|
+
var CONFIG_CANDIDATES = [
|
|
464
|
+
"kora.config.ts",
|
|
465
|
+
"kora.config.mts",
|
|
466
|
+
"kora.config.cts",
|
|
467
|
+
"kora.config.js",
|
|
468
|
+
"kora.config.mjs",
|
|
469
|
+
"kora.config.cjs"
|
|
470
|
+
];
|
|
471
|
+
async function loadKoraConfig(projectRoot) {
|
|
472
|
+
const configPath = await findKoraConfigFile(projectRoot);
|
|
473
|
+
if (!configPath) return null;
|
|
474
|
+
const ext = (0, import_node_path4.extname)(configPath);
|
|
475
|
+
if (ext === ".ts" || ext === ".mts" || ext === ".cts") {
|
|
476
|
+
const loaded2 = await loadTypeScriptConfig(configPath, projectRoot);
|
|
477
|
+
return toConfigObject(loaded2);
|
|
478
|
+
}
|
|
479
|
+
const loaded = await import((0, import_node_url3.pathToFileURL)(configPath).href);
|
|
480
|
+
return toConfigObject(loaded);
|
|
481
|
+
}
|
|
482
|
+
async function findKoraConfigFile(projectRoot) {
|
|
483
|
+
for (const file of CONFIG_CANDIDATES) {
|
|
484
|
+
const candidate = (0, import_node_path4.join)(projectRoot, file);
|
|
485
|
+
try {
|
|
486
|
+
await (0, import_promises3.access)(candidate);
|
|
487
|
+
return candidate;
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
async function loadTypeScriptConfig(configPath, projectRoot) {
|
|
494
|
+
const tsxBinary = await resolveProjectBinary(projectRoot, "tsx");
|
|
495
|
+
if (!tsxBinary) {
|
|
496
|
+
throw new Error(
|
|
497
|
+
`Found TypeScript config at ${configPath}, but "tsx" is not installed in this project. Install tsx or use kora.config.js.`
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
const script = [
|
|
501
|
+
"import { pathToFileURL } from 'node:url'",
|
|
502
|
+
"const configPath = process.argv[process.argv.length - 1]",
|
|
503
|
+
"const mod = await import(pathToFileURL(configPath).href)",
|
|
504
|
+
"const value = mod.default ?? mod",
|
|
505
|
+
"process.stdout.write(JSON.stringify(value))"
|
|
506
|
+
].join(";");
|
|
507
|
+
const output = await runCommand(tsxBinary, ["--eval", script, configPath], projectRoot);
|
|
508
|
+
try {
|
|
509
|
+
return JSON.parse(output);
|
|
510
|
+
} catch {
|
|
511
|
+
throw new Error(`Failed to parse ${configPath} output as JSON.`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
async function runCommand(command, args, cwd) {
|
|
515
|
+
return await new Promise((resolve7, reject) => {
|
|
516
|
+
const child = (0, import_node_child_process3.spawn)(command, args, {
|
|
517
|
+
cwd,
|
|
518
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
519
|
+
env: process.env
|
|
520
|
+
});
|
|
521
|
+
let stdout = "";
|
|
522
|
+
let stderr = "";
|
|
523
|
+
child.stdout?.on("data", (chunk) => {
|
|
524
|
+
stdout += chunk.toString("utf-8");
|
|
525
|
+
});
|
|
526
|
+
child.stderr?.on("data", (chunk) => {
|
|
527
|
+
stderr += chunk.toString("utf-8");
|
|
528
|
+
});
|
|
529
|
+
child.on("error", (error) => {
|
|
530
|
+
reject(error);
|
|
531
|
+
});
|
|
532
|
+
child.on("exit", (code) => {
|
|
533
|
+
if (code === 0) {
|
|
534
|
+
resolve7(stdout.trim());
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
reject(new Error(`Failed to load kora config (exit ${code ?? "unknown"}): ${stderr.trim()}`));
|
|
538
|
+
});
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
function toConfigObject(mod) {
|
|
542
|
+
if (typeof mod !== "object" || mod === null) {
|
|
543
|
+
throw new Error("kora config must export an object.");
|
|
544
|
+
}
|
|
545
|
+
const value = mod.default ?? mod;
|
|
546
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
547
|
+
throw new Error("kora config must export an object.");
|
|
548
|
+
}
|
|
549
|
+
return value;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/commands/dev/process-manager.ts
|
|
553
|
+
var import_node_child_process4 = require("child_process");
|
|
554
|
+
var ProcessManager = class {
|
|
555
|
+
processes = /* @__PURE__ */ new Map();
|
|
556
|
+
spawn(config) {
|
|
557
|
+
const options = {
|
|
558
|
+
cwd: config.cwd,
|
|
559
|
+
env: { ...process.env, ...config.env },
|
|
560
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
561
|
+
};
|
|
562
|
+
const child = (0, import_node_child_process4.spawn)(config.command, config.args, options);
|
|
563
|
+
let resolveExit;
|
|
564
|
+
const runningProcess = {
|
|
565
|
+
child,
|
|
566
|
+
exitPromise: new Promise((resolve7) => {
|
|
567
|
+
resolveExit = resolve7;
|
|
568
|
+
}),
|
|
569
|
+
stdoutBuffer: "",
|
|
570
|
+
stderrBuffer: ""
|
|
571
|
+
};
|
|
572
|
+
this.processes.set(config.label, runningProcess);
|
|
573
|
+
child.stdout?.on("data", (chunk) => {
|
|
574
|
+
runningProcess.stdoutBuffer = this.writeChunk(
|
|
575
|
+
config.label,
|
|
576
|
+
runningProcess.stdoutBuffer,
|
|
577
|
+
chunk,
|
|
578
|
+
false
|
|
579
|
+
);
|
|
580
|
+
});
|
|
581
|
+
child.stderr?.on("data", (chunk) => {
|
|
582
|
+
runningProcess.stderrBuffer = this.writeChunk(
|
|
583
|
+
config.label,
|
|
584
|
+
runningProcess.stderrBuffer,
|
|
585
|
+
chunk,
|
|
586
|
+
true
|
|
587
|
+
);
|
|
588
|
+
});
|
|
589
|
+
child.on("exit", (code, signal) => {
|
|
590
|
+
this.flushBuffer(config.label, runningProcess.stdoutBuffer, false);
|
|
591
|
+
this.flushBuffer(config.label, runningProcess.stderrBuffer, true);
|
|
592
|
+
this.processes.delete(config.label);
|
|
593
|
+
config.onExit?.(code, signal);
|
|
594
|
+
resolveExit?.();
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
hasRunning() {
|
|
598
|
+
return this.processes.size > 0;
|
|
599
|
+
}
|
|
600
|
+
async shutdownAll() {
|
|
601
|
+
const running = Array.from(this.processes.values());
|
|
602
|
+
if (running.length === 0) return;
|
|
603
|
+
for (const processEntry of running) {
|
|
604
|
+
processEntry.child.kill("SIGTERM");
|
|
605
|
+
}
|
|
606
|
+
await Promise.race([Promise.all(running.map((entry) => entry.exitPromise)), delay(5e3)]);
|
|
607
|
+
const remaining = Array.from(this.processes.values());
|
|
608
|
+
if (remaining.length === 0) return;
|
|
609
|
+
for (const processEntry of remaining) {
|
|
610
|
+
processEntry.child.kill("SIGKILL");
|
|
611
|
+
}
|
|
612
|
+
await Promise.all(remaining.map((entry) => entry.exitPromise));
|
|
613
|
+
}
|
|
614
|
+
writeChunk(label, buffer, chunk, isError) {
|
|
615
|
+
const combined = `${buffer}${chunk.toString("utf-8")}`;
|
|
616
|
+
const lines = combined.split(/\r?\n/);
|
|
617
|
+
const remaining = lines.pop() ?? "";
|
|
618
|
+
for (const line of lines) {
|
|
619
|
+
this.writeLine(label, line, isError);
|
|
620
|
+
}
|
|
621
|
+
return remaining;
|
|
622
|
+
}
|
|
623
|
+
flushBuffer(label, buffer, isError) {
|
|
624
|
+
if (!buffer) return;
|
|
625
|
+
this.writeLine(label, buffer, isError);
|
|
626
|
+
}
|
|
627
|
+
writeLine(label, line, isError) {
|
|
628
|
+
const stream = isError ? process.stderr : process.stdout;
|
|
629
|
+
stream.write(`[${label}] ${line}
|
|
630
|
+
`);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
function delay(ms) {
|
|
634
|
+
return new Promise((resolve7) => {
|
|
635
|
+
setTimeout(resolve7, ms);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/commands/dev/schema-watcher.ts
|
|
640
|
+
var import_node_child_process5 = require("child_process");
|
|
641
|
+
var import_node_fs2 = require("fs");
|
|
642
|
+
var SchemaWatcher = class {
|
|
643
|
+
constructor(config) {
|
|
644
|
+
this.config = config;
|
|
645
|
+
this.debounceMs = config.debounceMs ?? 300;
|
|
646
|
+
}
|
|
647
|
+
config;
|
|
648
|
+
debounceMs;
|
|
649
|
+
watcher = null;
|
|
650
|
+
debounceTimer = null;
|
|
651
|
+
start() {
|
|
652
|
+
if (this.watcher) return;
|
|
653
|
+
this.watcher = (0, import_node_fs2.watch)(this.config.schemaPath, () => {
|
|
654
|
+
this.scheduleRegeneration();
|
|
655
|
+
});
|
|
656
|
+
this.watcher.on("error", (error) => {
|
|
657
|
+
this.config.onError?.(toError(error));
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
stop() {
|
|
661
|
+
if (this.debounceTimer) {
|
|
662
|
+
clearTimeout(this.debounceTimer);
|
|
663
|
+
this.debounceTimer = null;
|
|
664
|
+
}
|
|
665
|
+
this.watcher?.close();
|
|
666
|
+
this.watcher = null;
|
|
667
|
+
}
|
|
668
|
+
async regenerate() {
|
|
669
|
+
const koraBinary = await resolveProjectBinary(this.config.projectRoot, "kora");
|
|
670
|
+
if (!koraBinary) {
|
|
671
|
+
throw new Error('Could not find project binary "kora" in node_modules/.bin.');
|
|
672
|
+
}
|
|
673
|
+
const tsxBinary = await resolveProjectBinary(this.config.projectRoot, "tsx");
|
|
674
|
+
if (!tsxBinary) {
|
|
675
|
+
process.stderr.write('[kora] Could not find "tsx" binary. Falling back to node.\n');
|
|
676
|
+
}
|
|
677
|
+
const command = tsxBinary ?? process.execPath;
|
|
678
|
+
const args = [koraBinary, "generate", "types", "--schema", this.config.schemaPath];
|
|
679
|
+
await spawnCommand(command, args, this.config.projectRoot);
|
|
680
|
+
this.config.onRegenerate?.();
|
|
681
|
+
}
|
|
682
|
+
scheduleRegeneration() {
|
|
683
|
+
if (this.debounceTimer) {
|
|
684
|
+
clearTimeout(this.debounceTimer);
|
|
685
|
+
}
|
|
686
|
+
this.debounceTimer = setTimeout(() => {
|
|
687
|
+
this.debounceTimer = null;
|
|
688
|
+
void this.regenerate().catch((error) => {
|
|
689
|
+
this.config.onError?.(toError(error));
|
|
690
|
+
});
|
|
691
|
+
}, this.debounceMs);
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
async function spawnCommand(command, args, cwd) {
|
|
695
|
+
await new Promise((resolve7, reject) => {
|
|
696
|
+
const child = (0, import_node_child_process5.spawn)(command, args, {
|
|
697
|
+
cwd,
|
|
698
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
699
|
+
env: process.env
|
|
700
|
+
});
|
|
701
|
+
child.stdout?.on("data", (chunk) => {
|
|
702
|
+
writePrefixedLines(chunk, false);
|
|
703
|
+
});
|
|
704
|
+
child.stderr?.on("data", (chunk) => {
|
|
705
|
+
writePrefixedLines(chunk, true);
|
|
706
|
+
});
|
|
707
|
+
child.on("error", (error) => {
|
|
708
|
+
reject(error);
|
|
709
|
+
});
|
|
710
|
+
child.on("exit", (code) => {
|
|
711
|
+
if (code === 0) {
|
|
712
|
+
resolve7();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
reject(new Error(`Type generation exited with code ${code ?? "unknown"}.`));
|
|
716
|
+
});
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
function writePrefixedLines(chunk, isError) {
|
|
720
|
+
const text = chunk.toString("utf-8");
|
|
721
|
+
const lines = text.split(/\r?\n/).filter((line) => line.length > 0);
|
|
722
|
+
const stream = isError ? process.stderr : process.stdout;
|
|
723
|
+
for (const line of lines) {
|
|
724
|
+
stream.write(`[kora] ${line}
|
|
725
|
+
`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function toError(error) {
|
|
729
|
+
if (error instanceof Error) return error;
|
|
730
|
+
return new Error(String(error));
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// src/commands/dev/dev-command.ts
|
|
734
|
+
var devCommand = (0, import_citty2.defineCommand)({
|
|
735
|
+
meta: {
|
|
736
|
+
name: "dev",
|
|
737
|
+
description: "Start the Kora development environment"
|
|
738
|
+
},
|
|
739
|
+
args: {
|
|
740
|
+
port: {
|
|
741
|
+
type: "string",
|
|
742
|
+
description: "Vite dev server port"
|
|
743
|
+
},
|
|
744
|
+
"sync-port": {
|
|
745
|
+
type: "string",
|
|
746
|
+
description: "Kora sync server port"
|
|
747
|
+
},
|
|
748
|
+
"no-sync": {
|
|
749
|
+
type: "boolean",
|
|
750
|
+
description: "Disable sync server startup",
|
|
751
|
+
default: false
|
|
752
|
+
},
|
|
753
|
+
"no-watch": {
|
|
754
|
+
type: "boolean",
|
|
755
|
+
description: "Disable schema file watching",
|
|
756
|
+
default: false
|
|
757
|
+
}
|
|
758
|
+
},
|
|
759
|
+
async run({ args }) {
|
|
760
|
+
const logger = createLogger();
|
|
761
|
+
const projectRoot = await findProjectRoot();
|
|
762
|
+
if (!projectRoot) {
|
|
763
|
+
throw new InvalidProjectError(process.cwd());
|
|
764
|
+
}
|
|
765
|
+
const config = await loadKoraConfig(projectRoot);
|
|
766
|
+
const vitePort = typeof args.port === "string" ? args.port : String(config?.dev?.port ?? 5173);
|
|
767
|
+
const syncPortFromConfig = typeof config?.dev?.sync === "object" && typeof config.dev.sync.port === "number" ? config.dev.sync.port : 3001;
|
|
768
|
+
const syncPort = typeof args["sync-port"] === "string" ? args["sync-port"] : String(syncPortFromConfig);
|
|
769
|
+
const configSyncEnabled = config?.dev?.sync === void 0 || config.dev.sync === true || typeof config.dev.sync === "object" && config.dev.sync.enabled !== false;
|
|
770
|
+
const configWatchEnabled = config?.dev?.watch === void 0 || config.dev.watch === true || typeof config.dev.watch === "object" && config.dev.watch.enabled !== false;
|
|
771
|
+
const watchDebounceMs = typeof config?.dev?.watch === "object" && typeof config.dev.watch.debounceMs === "number" ? config.dev.watch.debounceMs : 300;
|
|
772
|
+
const viteBinary = await resolveProjectBinary(projectRoot, "vite");
|
|
773
|
+
if (!viteBinary) {
|
|
774
|
+
throw new DevServerError("vite", (0, import_node_path5.join)(projectRoot, "node_modules", ".bin", "vite"));
|
|
775
|
+
}
|
|
776
|
+
const syncServerFile = await findSyncServerFile(projectRoot);
|
|
777
|
+
let managedSyncStore = normalizeManagedSyncStore(config, projectRoot);
|
|
778
|
+
const postgresEnvRequested = isPostgresEnvRequested(config);
|
|
779
|
+
const syncAllowed = args["no-sync"] !== true && configSyncEnabled;
|
|
780
|
+
let shouldStartSync = syncAllowed && (syncServerFile !== null || managedSyncStore !== null);
|
|
781
|
+
let syncBinary = null;
|
|
782
|
+
if (shouldStartSync && syncServerFile !== null) {
|
|
783
|
+
syncBinary = await resolveProjectBinary(projectRoot, "tsx");
|
|
784
|
+
if (!syncBinary) {
|
|
785
|
+
logger.warn('Sync server detected, but local "tsx" binary was not found. Skipping sync.');
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (shouldStartSync && syncServerFile === null && managedSyncStore) {
|
|
789
|
+
const hasServerPackage = await fileExists(
|
|
790
|
+
(0, import_node_path5.join)(projectRoot, "node_modules", "@kora", "server", "package.json")
|
|
791
|
+
);
|
|
792
|
+
if (!hasServerPackage) {
|
|
793
|
+
logger.warn(
|
|
794
|
+
"Managed sync is configured, but @korajs/server is not installed. Install it or add server.ts."
|
|
795
|
+
);
|
|
796
|
+
managedSyncStore = null;
|
|
797
|
+
shouldStartSync = syncAllowed && (syncServerFile !== null || managedSyncStore !== null);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
if (syncAllowed && syncServerFile === null && managedSyncStore === null && postgresEnvRequested) {
|
|
801
|
+
logger.warn(
|
|
802
|
+
"Managed postgres sync requested but no connection string found. Set dev.sync.store.connectionString or DATABASE_URL."
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
let configuredSchemaPath = null;
|
|
806
|
+
if (typeof config?.schema === "string") {
|
|
807
|
+
const candidate = (0, import_node_path6.resolve)(projectRoot, config.schema);
|
|
808
|
+
if (await fileExists(candidate)) {
|
|
809
|
+
configuredSchemaPath = candidate;
|
|
810
|
+
} else {
|
|
811
|
+
logger.warn(`Configured schema file not found: ${candidate}. Falling back to auto-detection.`);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
const schemaPath = configuredSchemaPath ?? await findSchemaFile(projectRoot);
|
|
815
|
+
const watchEnabled = args["no-watch"] !== true && configWatchEnabled && schemaPath !== null;
|
|
816
|
+
const processManager = new ProcessManager();
|
|
817
|
+
let schemaWatcher = null;
|
|
818
|
+
let shuttingDown = false;
|
|
819
|
+
let resolveFinished;
|
|
820
|
+
const finished = new Promise((resolve7) => {
|
|
821
|
+
resolveFinished = resolve7;
|
|
822
|
+
});
|
|
823
|
+
const onManagedProcessExit = () => {
|
|
824
|
+
if (!processManager.hasRunning() && !shuttingDown) {
|
|
825
|
+
resolveFinished?.();
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
const shutdown = async () => {
|
|
829
|
+
if (shuttingDown) return;
|
|
830
|
+
shuttingDown = true;
|
|
831
|
+
schemaWatcher?.stop();
|
|
832
|
+
await processManager.shutdownAll();
|
|
833
|
+
resolveFinished?.();
|
|
834
|
+
};
|
|
835
|
+
const onSigInt = () => {
|
|
836
|
+
void shutdown();
|
|
837
|
+
};
|
|
838
|
+
const onSigTerm = () => {
|
|
839
|
+
void shutdown();
|
|
840
|
+
};
|
|
841
|
+
process.on("SIGINT", onSigInt);
|
|
842
|
+
process.on("SIGTERM", onSigTerm);
|
|
843
|
+
logger.banner();
|
|
844
|
+
logger.info("Starting development environment:");
|
|
845
|
+
logger.blank();
|
|
846
|
+
logger.step(` Vite dev server on port ${vitePort}`);
|
|
847
|
+
if (shouldStartSync && syncBinary && syncServerFile) {
|
|
848
|
+
logger.step(` Sync server on port ${syncPort}`);
|
|
849
|
+
} else if (shouldStartSync && syncServerFile === null && managedSyncStore !== null) {
|
|
850
|
+
logger.step(` Managed sync server on port ${syncPort} (${managedSyncStore.type})`);
|
|
851
|
+
} else if (syncAllowed && syncServerFile === null) {
|
|
852
|
+
logger.step(" Sync server configured but no server.ts/server.js or managed store found");
|
|
853
|
+
} else if (!syncAllowed) {
|
|
854
|
+
logger.step(" Sync server disabled via --no-sync");
|
|
855
|
+
}
|
|
856
|
+
if (watchEnabled && schemaPath) {
|
|
857
|
+
logger.step(` Schema watcher enabled (${schemaPath})`);
|
|
858
|
+
} else if (args["no-watch"] === true) {
|
|
859
|
+
logger.step(" Schema watcher disabled via --no-watch");
|
|
860
|
+
} else {
|
|
861
|
+
logger.step(" Schema watcher disabled (schema.ts not found)");
|
|
862
|
+
}
|
|
863
|
+
logger.blank();
|
|
864
|
+
processManager.spawn({
|
|
865
|
+
label: "vite",
|
|
866
|
+
command: viteBinary,
|
|
867
|
+
args: ["--port", String(vitePort)],
|
|
868
|
+
cwd: projectRoot,
|
|
869
|
+
onExit: onManagedProcessExit
|
|
870
|
+
});
|
|
871
|
+
if (shouldStartSync && syncBinary && syncServerFile) {
|
|
872
|
+
processManager.spawn({
|
|
873
|
+
label: "sync",
|
|
874
|
+
command: syncBinary,
|
|
875
|
+
args: [syncServerFile],
|
|
876
|
+
cwd: projectRoot,
|
|
877
|
+
env: {
|
|
878
|
+
PORT: String(syncPort),
|
|
879
|
+
KORA_SYNC_PORT: String(syncPort)
|
|
880
|
+
},
|
|
881
|
+
onExit: onManagedProcessExit
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
if (shouldStartSync && syncServerFile === null && managedSyncStore !== null) {
|
|
885
|
+
processManager.spawn({
|
|
886
|
+
label: "sync",
|
|
887
|
+
command: process.execPath,
|
|
888
|
+
args: ["--input-type=module", "--eval", MANAGED_SYNC_BOOTSTRAP_SCRIPT],
|
|
889
|
+
cwd: projectRoot,
|
|
890
|
+
env: {
|
|
891
|
+
KORA_DEV_SYNC_CONFIG: JSON.stringify({
|
|
892
|
+
port: Number(syncPort),
|
|
893
|
+
store: managedSyncStore
|
|
894
|
+
})
|
|
895
|
+
},
|
|
896
|
+
onExit: onManagedProcessExit
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
if (watchEnabled && schemaPath) {
|
|
900
|
+
schemaWatcher = new SchemaWatcher({
|
|
901
|
+
schemaPath,
|
|
902
|
+
projectRoot,
|
|
903
|
+
debounceMs: watchDebounceMs,
|
|
904
|
+
onRegenerate: () => {
|
|
905
|
+
logger.success("Regenerated types from schema changes");
|
|
906
|
+
},
|
|
907
|
+
onError: (error) => {
|
|
908
|
+
logger.error(`Schema watcher error: ${error.message}`);
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
schemaWatcher.start();
|
|
912
|
+
}
|
|
913
|
+
await finished;
|
|
914
|
+
process.off("SIGINT", onSigInt);
|
|
915
|
+
process.off("SIGTERM", onSigTerm);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
async function findSyncServerFile(projectRoot) {
|
|
919
|
+
const candidates = [(0, import_node_path5.join)(projectRoot, "server.ts"), (0, import_node_path5.join)(projectRoot, "server.js")];
|
|
920
|
+
for (const candidate of candidates) {
|
|
921
|
+
try {
|
|
922
|
+
await (0, import_promises4.access)(candidate);
|
|
923
|
+
return candidate;
|
|
924
|
+
} catch {
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
async function fileExists(path) {
|
|
930
|
+
try {
|
|
931
|
+
await (0, import_promises4.access)(path);
|
|
932
|
+
return true;
|
|
933
|
+
} catch {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
function normalizeManagedSyncStore(config, projectRoot) {
|
|
938
|
+
const sync = config?.dev?.sync;
|
|
939
|
+
if (typeof sync !== "object" || sync === null) return null;
|
|
940
|
+
const store = sync.store;
|
|
941
|
+
if (store === void 0) return { type: "memory" };
|
|
942
|
+
if (store === "memory") return { type: "memory" };
|
|
943
|
+
if (store === "sqlite") return { type: "sqlite", filename: (0, import_node_path5.join)(projectRoot, "kora-sync.db") };
|
|
944
|
+
if (store === "postgres") {
|
|
945
|
+
const connectionString = process.env.DATABASE_URL;
|
|
946
|
+
if (!connectionString) return null;
|
|
947
|
+
return { type: "postgres", connectionString };
|
|
948
|
+
}
|
|
949
|
+
if (typeof store === "object" && store !== null) {
|
|
950
|
+
if (store.type === "memory") return { type: "memory" };
|
|
951
|
+
if (store.type === "sqlite") {
|
|
952
|
+
const filename = typeof store.filename === "string" && store.filename.length > 0 ? (0, import_node_path6.resolve)(projectRoot, store.filename) : (0, import_node_path5.join)(projectRoot, "kora-sync.db");
|
|
953
|
+
return { type: "sqlite", filename };
|
|
954
|
+
}
|
|
955
|
+
if (store.type === "postgres" && typeof store.connectionString === "string") {
|
|
956
|
+
return { type: "postgres", connectionString: store.connectionString };
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
function isPostgresEnvRequested(config) {
|
|
962
|
+
const sync = config?.dev?.sync;
|
|
963
|
+
if (typeof sync !== "object" || sync === null) return false;
|
|
964
|
+
if (sync.store === "postgres") return true;
|
|
965
|
+
if (typeof sync.store === "object" && sync.store !== null && sync.store.type === "postgres") return true;
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
var MANAGED_SYNC_BOOTSTRAP_SCRIPT = `
|
|
969
|
+
const config = JSON.parse(process.env.KORA_DEV_SYNC_CONFIG ?? '{}');
|
|
970
|
+
const {
|
|
971
|
+
createKoraServer,
|
|
972
|
+
MemoryServerStore,
|
|
973
|
+
createSqliteServerStore,
|
|
974
|
+
createPostgresServerStore,
|
|
975
|
+
} = await import('@korajs/server');
|
|
976
|
+
const storeConfig = config.store ?? { type: 'memory' };
|
|
977
|
+
let store;
|
|
978
|
+
if (storeConfig.type === 'memory') {
|
|
979
|
+
store = new MemoryServerStore();
|
|
980
|
+
} else if (storeConfig.type === 'sqlite') {
|
|
981
|
+
const filename = typeof storeConfig.filename === 'string' && storeConfig.filename.length > 0
|
|
982
|
+
? storeConfig.filename
|
|
983
|
+
: './kora-sync.db';
|
|
984
|
+
store = createSqliteServerStore({ filename });
|
|
985
|
+
} else if (storeConfig.type === 'postgres') {
|
|
986
|
+
if (typeof storeConfig.connectionString !== 'string' || storeConfig.connectionString.length === 0) {
|
|
987
|
+
throw new Error('Managed postgres sync requires a connectionString');
|
|
988
|
+
}
|
|
989
|
+
store = await createPostgresServerStore({
|
|
990
|
+
connectionString: storeConfig.connectionString,
|
|
991
|
+
});
|
|
992
|
+
} else {
|
|
993
|
+
throw new Error('Unsupported managed sync store type: ' + String(storeConfig.type));
|
|
994
|
+
}
|
|
995
|
+
const server = createKoraServer({ store, port: Number(config.port ?? 3001) });
|
|
996
|
+
const shutdown = async () => {
|
|
997
|
+
try {
|
|
998
|
+
await server.stop();
|
|
999
|
+
} catch {
|
|
1000
|
+
}
|
|
1001
|
+
process.exit(0);
|
|
1002
|
+
};
|
|
1003
|
+
process.on('SIGINT', () => {
|
|
1004
|
+
void shutdown();
|
|
1005
|
+
});
|
|
1006
|
+
process.on('SIGTERM', () => {
|
|
1007
|
+
void shutdown();
|
|
1008
|
+
});
|
|
1009
|
+
await server.start();
|
|
1010
|
+
process.stdout.write('Managed sync server running on ws://localhost:' + String(config.port ?? 3001) + '\\n');
|
|
1011
|
+
await new Promise(() => {});
|
|
1012
|
+
`;
|
|
1013
|
+
|
|
1014
|
+
// src/commands/generate/generate-command.ts
|
|
1015
|
+
var import_promises5 = require("fs/promises");
|
|
1016
|
+
var import_node_path7 = require("path");
|
|
1017
|
+
var import_citty3 = require("citty");
|
|
1018
|
+
|
|
1019
|
+
// src/commands/generate/type-generator.ts
|
|
1020
|
+
function generateTypes(schema) {
|
|
1021
|
+
const lines = [
|
|
1022
|
+
"// Auto-generated by @korajs/cli \u2014 do not edit manually",
|
|
1023
|
+
`// Generated from schema version ${String(schema.version)}`,
|
|
1024
|
+
""
|
|
1025
|
+
];
|
|
1026
|
+
const collectionNames = Object.keys(schema.collections);
|
|
1027
|
+
if (collectionNames.length === 0) {
|
|
1028
|
+
return lines.join("\n");
|
|
1029
|
+
}
|
|
1030
|
+
for (const [name, collection] of Object.entries(schema.collections)) {
|
|
1031
|
+
const pascal = toPascalCase(name);
|
|
1032
|
+
const fields = collection.fields;
|
|
1033
|
+
lines.push(`export interface ${pascal}Record {`);
|
|
1034
|
+
lines.push(" readonly id: string");
|
|
1035
|
+
for (const [fieldName, descriptor] of Object.entries(fields)) {
|
|
1036
|
+
const tsType = fieldKindToTypeScript(descriptor);
|
|
1037
|
+
const optional = !descriptor.required && !descriptor.auto ? "?" : "";
|
|
1038
|
+
lines.push(` readonly ${fieldName}${optional}: ${tsType}`);
|
|
1039
|
+
}
|
|
1040
|
+
lines.push("}");
|
|
1041
|
+
lines.push("");
|
|
1042
|
+
lines.push(`export interface ${pascal}InsertInput {`);
|
|
1043
|
+
for (const [fieldName, descriptor] of Object.entries(fields)) {
|
|
1044
|
+
if (descriptor.auto) continue;
|
|
1045
|
+
const tsType = fieldKindToTypeScript(descriptor);
|
|
1046
|
+
const optional = !descriptor.required || descriptor.defaultValue !== void 0 ? "?" : "";
|
|
1047
|
+
lines.push(` ${fieldName}${optional}: ${tsType}`);
|
|
1048
|
+
}
|
|
1049
|
+
lines.push("}");
|
|
1050
|
+
lines.push("");
|
|
1051
|
+
lines.push(`export interface ${pascal}UpdateInput {`);
|
|
1052
|
+
for (const [fieldName, descriptor] of Object.entries(fields)) {
|
|
1053
|
+
if (descriptor.auto) continue;
|
|
1054
|
+
const tsType = fieldKindToTypeScript(descriptor);
|
|
1055
|
+
lines.push(` ${fieldName}?: ${tsType}`);
|
|
1056
|
+
}
|
|
1057
|
+
lines.push("}");
|
|
1058
|
+
lines.push("");
|
|
1059
|
+
}
|
|
1060
|
+
return lines.join("\n");
|
|
1061
|
+
}
|
|
1062
|
+
function fieldKindToTypeScript(descriptor) {
|
|
1063
|
+
switch (descriptor.kind) {
|
|
1064
|
+
case "string":
|
|
1065
|
+
return "string";
|
|
1066
|
+
case "number":
|
|
1067
|
+
return "number";
|
|
1068
|
+
case "boolean":
|
|
1069
|
+
return "boolean";
|
|
1070
|
+
case "timestamp":
|
|
1071
|
+
return "number";
|
|
1072
|
+
case "richtext":
|
|
1073
|
+
return "string";
|
|
1074
|
+
case "enum": {
|
|
1075
|
+
if (descriptor.enumValues && descriptor.enumValues.length > 0) {
|
|
1076
|
+
return descriptor.enumValues.map((v) => `'${v}'`).join(" | ");
|
|
1077
|
+
}
|
|
1078
|
+
return "string";
|
|
1079
|
+
}
|
|
1080
|
+
case "array": {
|
|
1081
|
+
const itemType = itemKindToTypeScript(descriptor.itemKind);
|
|
1082
|
+
return `Array<${itemType}>`;
|
|
1083
|
+
}
|
|
1084
|
+
default:
|
|
1085
|
+
return "unknown";
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
function itemKindToTypeScript(kind) {
|
|
1089
|
+
switch (kind) {
|
|
1090
|
+
case "string":
|
|
1091
|
+
return "string";
|
|
1092
|
+
case "number":
|
|
1093
|
+
return "number";
|
|
1094
|
+
case "boolean":
|
|
1095
|
+
return "boolean";
|
|
1096
|
+
case "timestamp":
|
|
1097
|
+
return "number";
|
|
1098
|
+
case "richtext":
|
|
1099
|
+
return "string";
|
|
1100
|
+
case "enum":
|
|
1101
|
+
return "string";
|
|
1102
|
+
case "array":
|
|
1103
|
+
return "unknown[]";
|
|
1104
|
+
default:
|
|
1105
|
+
return "unknown";
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function toPascalCase(name) {
|
|
1109
|
+
return name.split(/[_-]/).map((part) => {
|
|
1110
|
+
if (part.length === 0) return "";
|
|
1111
|
+
const first = part[0];
|
|
1112
|
+
return first ? first.toUpperCase() + part.slice(1) : "";
|
|
1113
|
+
}).join("");
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// src/commands/generate/generate-command.ts
|
|
1117
|
+
var generateCommand = (0, import_citty3.defineCommand)({
|
|
1118
|
+
meta: {
|
|
1119
|
+
name: "generate",
|
|
1120
|
+
description: "Generate code from your Kora schema"
|
|
1121
|
+
},
|
|
1122
|
+
subCommands: {
|
|
1123
|
+
types: (0, import_citty3.defineCommand)({
|
|
1124
|
+
meta: {
|
|
1125
|
+
name: "types",
|
|
1126
|
+
description: "Generate TypeScript types from your schema"
|
|
1127
|
+
},
|
|
1128
|
+
args: {
|
|
1129
|
+
schema: {
|
|
1130
|
+
type: "string",
|
|
1131
|
+
description: "Path to schema file"
|
|
1132
|
+
},
|
|
1133
|
+
output: {
|
|
1134
|
+
type: "string",
|
|
1135
|
+
description: "Output file path",
|
|
1136
|
+
default: "kora/generated/types.ts"
|
|
1137
|
+
}
|
|
1138
|
+
},
|
|
1139
|
+
async run({ args }) {
|
|
1140
|
+
const logger = createLogger();
|
|
1141
|
+
const projectRoot = await findProjectRoot();
|
|
1142
|
+
if (!projectRoot) {
|
|
1143
|
+
throw new InvalidProjectError(process.cwd());
|
|
1144
|
+
}
|
|
1145
|
+
let schemaPath;
|
|
1146
|
+
if (args.schema && typeof args.schema === "string") {
|
|
1147
|
+
schemaPath = (0, import_node_path7.resolve)(args.schema);
|
|
1148
|
+
} else {
|
|
1149
|
+
const found = await findSchemaFile(projectRoot);
|
|
1150
|
+
if (!found) {
|
|
1151
|
+
throw new SchemaNotFoundError([
|
|
1152
|
+
"src/schema.ts",
|
|
1153
|
+
"schema.ts",
|
|
1154
|
+
"src/schema.js",
|
|
1155
|
+
"schema.js"
|
|
1156
|
+
]);
|
|
1157
|
+
}
|
|
1158
|
+
schemaPath = found;
|
|
1159
|
+
}
|
|
1160
|
+
logger.step(`Reading schema from ${schemaPath}...`);
|
|
1161
|
+
const schemaModule = await import(schemaPath);
|
|
1162
|
+
const schema = extractSchema(schemaModule);
|
|
1163
|
+
if (!schema) {
|
|
1164
|
+
logger.error("Schema file must export a SchemaDefinition as the default export.");
|
|
1165
|
+
process.exitCode = 1;
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
const output = generateTypes(schema);
|
|
1169
|
+
const outputFile = typeof args.output === "string" ? args.output : "kora/generated/types.ts";
|
|
1170
|
+
const outputPath = (0, import_node_path7.resolve)(projectRoot, outputFile);
|
|
1171
|
+
await (0, import_promises5.mkdir)((0, import_node_path7.dirname)(outputPath), { recursive: true });
|
|
1172
|
+
await (0, import_promises5.writeFile)(outputPath, output, "utf-8");
|
|
1173
|
+
logger.success(`Generated types at ${outputPath}`);
|
|
1174
|
+
}
|
|
1175
|
+
})
|
|
1176
|
+
}
|
|
1177
|
+
});
|
|
1178
|
+
function extractSchema(mod) {
|
|
1179
|
+
if (typeof mod !== "object" || mod === null) return null;
|
|
1180
|
+
const record = mod;
|
|
1181
|
+
const candidate = record.default ?? record;
|
|
1182
|
+
if (isSchemaDefinition(candidate)) return candidate;
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
function isSchemaDefinition(value) {
|
|
1186
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1187
|
+
const obj = value;
|
|
1188
|
+
return typeof obj.version === "number" && typeof obj.collections === "object" && obj.collections !== null;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// src/commands/migrate/migrate-command.ts
|
|
1192
|
+
var import_promises6 = require("fs/promises");
|
|
1193
|
+
var import_node_path9 = require("path");
|
|
1194
|
+
var import_citty4 = require("citty");
|
|
1195
|
+
|
|
1196
|
+
// src/commands/migrate/migration-generator.ts
|
|
1197
|
+
var import_core2 = require("@korajs/core");
|
|
1198
|
+
|
|
1199
|
+
// src/commands/migrate/schema-differ.ts
|
|
1200
|
+
function diffSchemas(previous, current) {
|
|
1201
|
+
const changes = [];
|
|
1202
|
+
const previousCollections = new Set(Object.keys(previous.collections));
|
|
1203
|
+
const currentCollections = new Set(Object.keys(current.collections));
|
|
1204
|
+
for (const collection of currentCollections) {
|
|
1205
|
+
if (!previousCollections.has(collection)) {
|
|
1206
|
+
changes.push({ type: "collection-added", collection });
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
for (const collection of previousCollections) {
|
|
1210
|
+
if (!currentCollections.has(collection)) {
|
|
1211
|
+
changes.push({ type: "collection-removed", collection });
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
for (const collection of currentCollections) {
|
|
1215
|
+
if (!previousCollections.has(collection)) continue;
|
|
1216
|
+
const previousDef = previous.collections[collection];
|
|
1217
|
+
const currentDef = current.collections[collection];
|
|
1218
|
+
if (!previousDef || !currentDef) continue;
|
|
1219
|
+
const previousFields = previousDef.fields;
|
|
1220
|
+
const currentFields = currentDef.fields;
|
|
1221
|
+
for (const [fieldName, currentField] of Object.entries(currentFields)) {
|
|
1222
|
+
const previousField = previousFields[fieldName];
|
|
1223
|
+
if (!previousField) {
|
|
1224
|
+
changes.push({
|
|
1225
|
+
type: "field-added",
|
|
1226
|
+
collection,
|
|
1227
|
+
field: fieldName,
|
|
1228
|
+
descriptor: currentField
|
|
1229
|
+
});
|
|
1230
|
+
continue;
|
|
1231
|
+
}
|
|
1232
|
+
if (!fieldDescriptorsEqual(previousField, currentField)) {
|
|
1233
|
+
changes.push({
|
|
1234
|
+
type: "field-changed",
|
|
1235
|
+
collection,
|
|
1236
|
+
field: fieldName,
|
|
1237
|
+
before: previousField,
|
|
1238
|
+
after: currentField
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
for (const [fieldName, previousField] of Object.entries(previousFields)) {
|
|
1243
|
+
if (!(fieldName in currentFields)) {
|
|
1244
|
+
changes.push({
|
|
1245
|
+
type: "field-removed",
|
|
1246
|
+
collection,
|
|
1247
|
+
field: fieldName,
|
|
1248
|
+
descriptor: previousField
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
const previousIndexes = new Set(previousDef.indexes);
|
|
1253
|
+
const currentIndexes = new Set(currentDef.indexes);
|
|
1254
|
+
for (const index of currentIndexes) {
|
|
1255
|
+
if (!previousIndexes.has(index)) {
|
|
1256
|
+
changes.push({ type: "index-added", collection, index });
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
for (const index of previousIndexes) {
|
|
1260
|
+
if (!currentIndexes.has(index)) {
|
|
1261
|
+
changes.push({ type: "index-removed", collection, index });
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
changes.sort(compareChanges);
|
|
1266
|
+
return {
|
|
1267
|
+
fromVersion: previous.version,
|
|
1268
|
+
toVersion: current.version,
|
|
1269
|
+
changes,
|
|
1270
|
+
hasChanges: changes.length > 0,
|
|
1271
|
+
hasBreakingChanges: changes.some(isBreakingChange)
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
function getChangedCollections(diff) {
|
|
1275
|
+
const collections = /* @__PURE__ */ new Set();
|
|
1276
|
+
for (const change of diff.changes) {
|
|
1277
|
+
collections.add(change.collection);
|
|
1278
|
+
}
|
|
1279
|
+
return [...collections].sort();
|
|
1280
|
+
}
|
|
1281
|
+
function isBreakingChange(change) {
|
|
1282
|
+
if (change.type === "collection-removed" || change.type === "field-removed") return true;
|
|
1283
|
+
if (change.type === "field-changed") {
|
|
1284
|
+
if (change.before.kind !== change.after.kind) return true;
|
|
1285
|
+
if (change.before.itemKind !== change.after.itemKind) return true;
|
|
1286
|
+
if (serializeEnum(change.before.enumValues) !== serializeEnum(change.after.enumValues)) return true;
|
|
1287
|
+
if (change.before.required !== change.after.required && change.after.required) return true;
|
|
1288
|
+
return false;
|
|
1289
|
+
}
|
|
1290
|
+
if (change.type === "field-added") {
|
|
1291
|
+
const descriptor = change.descriptor;
|
|
1292
|
+
return descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto;
|
|
1293
|
+
}
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
function fieldDescriptorsEqual(left, right) {
|
|
1297
|
+
return left.kind === right.kind && left.required === right.required && left.defaultValue === right.defaultValue && left.auto === right.auto && left.itemKind === right.itemKind && serializeEnum(left.enumValues) === serializeEnum(right.enumValues);
|
|
1298
|
+
}
|
|
1299
|
+
function serializeEnum(values) {
|
|
1300
|
+
if (!values) return "";
|
|
1301
|
+
return values.join("|");
|
|
1302
|
+
}
|
|
1303
|
+
function compareChanges(left, right) {
|
|
1304
|
+
if (left.collection < right.collection) return -1;
|
|
1305
|
+
if (left.collection > right.collection) return 1;
|
|
1306
|
+
if (left.type < right.type) return -1;
|
|
1307
|
+
if (left.type > right.type) return 1;
|
|
1308
|
+
const leftKey = "field" in left ? left.field : "index" in left ? left.index : "";
|
|
1309
|
+
const rightKey = "field" in right ? right.field : "index" in right ? right.index : "";
|
|
1310
|
+
if (leftKey < rightKey) return -1;
|
|
1311
|
+
if (leftKey > rightKey) return 1;
|
|
1312
|
+
return 0;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/commands/migrate/migration-generator.ts
|
|
1316
|
+
function generateMigration(previous, current, diff) {
|
|
1317
|
+
const up = [];
|
|
1318
|
+
const down = [];
|
|
1319
|
+
for (const change of diff.changes) {
|
|
1320
|
+
if (change.type === "collection-added") {
|
|
1321
|
+
const collectionDef = current.collections[change.collection];
|
|
1322
|
+
if (!collectionDef) continue;
|
|
1323
|
+
up.push(...(0, import_core2.generateSQL)(change.collection, collectionDef));
|
|
1324
|
+
down.push(...dropCollectionStatements(change.collection));
|
|
1325
|
+
}
|
|
1326
|
+
if (change.type === "collection-removed") {
|
|
1327
|
+
const collectionDef = previous.collections[change.collection];
|
|
1328
|
+
up.push(...dropCollectionStatements(change.collection));
|
|
1329
|
+
if (collectionDef) {
|
|
1330
|
+
down.push(...(0, import_core2.generateSQL)(change.collection, collectionDef));
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const changedCollections = getChangedCollections(diff).filter(
|
|
1335
|
+
(collection) => collection in previous.collections && collection in current.collections && diff.changes.some(
|
|
1336
|
+
(change) => change.collection === collection && (change.type === "field-added" || change.type === "field-removed" || change.type === "field-changed" || change.type === "index-added" || change.type === "index-removed")
|
|
1337
|
+
)
|
|
1338
|
+
);
|
|
1339
|
+
for (const collection of changedCollections) {
|
|
1340
|
+
const previousDef = previous.collections[collection];
|
|
1341
|
+
const currentDef = current.collections[collection];
|
|
1342
|
+
if (!previousDef || !currentDef) continue;
|
|
1343
|
+
validateRebuildSafety(collection, previousDef, currentDef);
|
|
1344
|
+
up.push(...generateRebuildStatements(collection, previousDef, currentDef));
|
|
1345
|
+
down.push(...generateRebuildStatements(collection, currentDef, previousDef));
|
|
1346
|
+
}
|
|
1347
|
+
down.reverse();
|
|
1348
|
+
return {
|
|
1349
|
+
up,
|
|
1350
|
+
down,
|
|
1351
|
+
summary: diff.changes.map(formatChange),
|
|
1352
|
+
containsBreakingChanges: diff.hasBreakingChanges
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function generateRebuildStatements(collection, from, to) {
|
|
1356
|
+
const table = quoteIdentifier(collection);
|
|
1357
|
+
const tempTable = quoteIdentifier(`_kora_mig_${collection}_new`);
|
|
1358
|
+
const targetColumns = [
|
|
1359
|
+
"id TEXT PRIMARY KEY NOT NULL",
|
|
1360
|
+
...Object.entries(to.fields).map(([field, descriptor]) => columnDefinition(field, descriptor)),
|
|
1361
|
+
"_created_at INTEGER NOT NULL",
|
|
1362
|
+
"_updated_at INTEGER NOT NULL",
|
|
1363
|
+
"_deleted INTEGER NOT NULL DEFAULT 0"
|
|
1364
|
+
];
|
|
1365
|
+
const statements = [];
|
|
1366
|
+
statements.push(`CREATE TABLE ${tempTable} (
|
|
1367
|
+
${targetColumns.join(",\n ")}
|
|
1368
|
+
)`);
|
|
1369
|
+
const toFields = Object.keys(to.fields);
|
|
1370
|
+
const columns = ["id", ...toFields, "_created_at", "_updated_at", "_deleted"];
|
|
1371
|
+
const selectExpressions = columns.map(
|
|
1372
|
+
(column) => projectionForColumn(column, from.fields, to.fields[column] ?? null)
|
|
1373
|
+
);
|
|
1374
|
+
statements.push(
|
|
1375
|
+
`INSERT INTO ${tempTable} (${columns.map(quoteIdentifier).join(", ")}) SELECT ${selectExpressions.join(", ")} FROM ${table}`
|
|
1376
|
+
);
|
|
1377
|
+
statements.push(`DROP TABLE ${table}`);
|
|
1378
|
+
statements.push(`ALTER TABLE ${tempTable} RENAME TO ${table}`);
|
|
1379
|
+
for (const indexField of to.indexes) {
|
|
1380
|
+
statements.push(
|
|
1381
|
+
`CREATE INDEX IF NOT EXISTS idx_${collection}_${indexField} ON ${table} (${quoteIdentifier(indexField)})`
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
return statements;
|
|
1385
|
+
}
|
|
1386
|
+
function validateRebuildSafety(collection, from, to) {
|
|
1387
|
+
for (const [fieldName, descriptor] of Object.entries(to.fields)) {
|
|
1388
|
+
if (fieldName in from.fields) continue;
|
|
1389
|
+
if (descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto) {
|
|
1390
|
+
throw new Error(
|
|
1391
|
+
`Cannot auto-migrate collection "${collection}": added required field "${fieldName}" has no default value.`
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
for (const [fieldName, targetDescriptor] of Object.entries(to.fields)) {
|
|
1396
|
+
const sourceDescriptor = from.fields[fieldName];
|
|
1397
|
+
if (!sourceDescriptor) continue;
|
|
1398
|
+
if (canTransformField(sourceDescriptor, targetDescriptor)) continue;
|
|
1399
|
+
if (targetDescriptor.required && targetDescriptor.defaultValue === void 0 && !targetDescriptor.auto) {
|
|
1400
|
+
throw new Error(
|
|
1401
|
+
`Cannot auto-migrate collection "${collection}": changed required field "${fieldName}" from ${sourceDescriptor.kind} to ${targetDescriptor.kind} without a safe transform/default.`
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
function projectionForColumn(column, fromFields, targetDescriptor) {
|
|
1407
|
+
if (column === "id" || column === "_created_at" || column === "_updated_at" || column === "_deleted") {
|
|
1408
|
+
return quoteIdentifier(column);
|
|
1409
|
+
}
|
|
1410
|
+
const sourceDescriptor = fromFields[column];
|
|
1411
|
+
if (sourceDescriptor && targetDescriptor) {
|
|
1412
|
+
return projectionForFieldTransform(column, sourceDescriptor, targetDescriptor);
|
|
1413
|
+
}
|
|
1414
|
+
if (sourceDescriptor) {
|
|
1415
|
+
return quoteIdentifier(column);
|
|
1416
|
+
}
|
|
1417
|
+
if (!targetDescriptor) {
|
|
1418
|
+
return "NULL";
|
|
1419
|
+
}
|
|
1420
|
+
if (targetDescriptor.auto && targetDescriptor.kind === "timestamp") {
|
|
1421
|
+
return "CAST(strftime('%s','now') AS INTEGER) * 1000";
|
|
1422
|
+
}
|
|
1423
|
+
if (targetDescriptor.defaultValue !== void 0) {
|
|
1424
|
+
return sqlLiteral(targetDescriptor.defaultValue);
|
|
1425
|
+
}
|
|
1426
|
+
return "NULL";
|
|
1427
|
+
}
|
|
1428
|
+
function projectionForFieldTransform(column, source, target) {
|
|
1429
|
+
const sourceColumn = quoteIdentifier(column);
|
|
1430
|
+
if (source.kind === target.kind && source.itemKind === target.itemKind) {
|
|
1431
|
+
if (target.kind === "enum" && target.enumValues && target.enumValues.length > 0) {
|
|
1432
|
+
const allowed = target.enumValues.map((value) => sqlLiteral(value)).join(", ");
|
|
1433
|
+
const fallback = target.defaultValue !== void 0 ? sqlLiteral(target.defaultValue) : sourceColumn;
|
|
1434
|
+
return `CASE WHEN ${sourceColumn} IN (${allowed}) THEN ${sourceColumn} ELSE ${fallback} END`;
|
|
1435
|
+
}
|
|
1436
|
+
return sourceColumn;
|
|
1437
|
+
}
|
|
1438
|
+
if (target.kind === "string") {
|
|
1439
|
+
return `CAST(${sourceColumn} AS TEXT)`;
|
|
1440
|
+
}
|
|
1441
|
+
if (target.kind === "number" || target.kind === "timestamp") {
|
|
1442
|
+
if (source.kind === "string" || source.kind === "enum" || source.kind === "number" || source.kind === "timestamp" || source.kind === "boolean") {
|
|
1443
|
+
const castType = target.kind === "number" ? "REAL" : "INTEGER";
|
|
1444
|
+
return `CASE WHEN ${sourceColumn} IS NULL THEN NULL ELSE CAST(${sourceColumn} AS ${castType}) END`;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
if (target.kind === "boolean") {
|
|
1448
|
+
if (source.kind === "number" || source.kind === "timestamp" || source.kind === "boolean") {
|
|
1449
|
+
return `CASE WHEN ${sourceColumn} IS NULL THEN NULL WHEN CAST(${sourceColumn} AS REAL) = 0 THEN 0 ELSE 1 END`;
|
|
1450
|
+
}
|
|
1451
|
+
if (source.kind === "string" || source.kind === "enum") {
|
|
1452
|
+
return `CASE WHEN ${sourceColumn} IS NULL THEN NULL WHEN LOWER(TRIM(CAST(${sourceColumn} AS TEXT))) IN ('1','true','t','yes','y','on') THEN 1 WHEN LOWER(TRIM(CAST(${sourceColumn} AS TEXT))) IN ('0','false','f','no','n','off') THEN 0 ELSE ${projectionFallback(target)} END`;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
if (target.kind === "enum" && target.enumValues && target.enumValues.length > 0) {
|
|
1456
|
+
if (source.kind === "string" || source.kind === "enum") {
|
|
1457
|
+
const allowed = target.enumValues.map((value) => sqlLiteral(value)).join(", ");
|
|
1458
|
+
return `CASE WHEN ${sourceColumn} IN (${allowed}) THEN ${sourceColumn} ELSE ${projectionFallback(target)} END`;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
if (target.kind === "array" && source.kind === "array" && source.itemKind === target.itemKind) {
|
|
1462
|
+
return sourceColumn;
|
|
1463
|
+
}
|
|
1464
|
+
if (target.auto && target.kind === "timestamp") {
|
|
1465
|
+
return "CAST(strftime('%s','now') AS INTEGER) * 1000";
|
|
1466
|
+
}
|
|
1467
|
+
return projectionFallback(target);
|
|
1468
|
+
}
|
|
1469
|
+
function canTransformField(source, target) {
|
|
1470
|
+
if (source.kind === target.kind && source.itemKind === target.itemKind) {
|
|
1471
|
+
return true;
|
|
1472
|
+
}
|
|
1473
|
+
if (target.kind === "string") {
|
|
1474
|
+
return true;
|
|
1475
|
+
}
|
|
1476
|
+
if (target.kind === "number" || target.kind === "timestamp") {
|
|
1477
|
+
return source.kind === "string" || source.kind === "enum" || source.kind === "number" || source.kind === "timestamp" || source.kind === "boolean";
|
|
1478
|
+
}
|
|
1479
|
+
if (target.kind === "boolean") {
|
|
1480
|
+
return source.kind === "number" || source.kind === "timestamp" || source.kind === "boolean" || source.kind === "string" || source.kind === "enum";
|
|
1481
|
+
}
|
|
1482
|
+
if (target.kind === "enum") {
|
|
1483
|
+
return source.kind === "string" || source.kind === "enum";
|
|
1484
|
+
}
|
|
1485
|
+
if (target.kind === "array") {
|
|
1486
|
+
return source.kind === "array" && source.itemKind === target.itemKind;
|
|
1487
|
+
}
|
|
1488
|
+
if (target.kind === "richtext") {
|
|
1489
|
+
return source.kind === "richtext";
|
|
1490
|
+
}
|
|
1491
|
+
return false;
|
|
1492
|
+
}
|
|
1493
|
+
function projectionFallback(target) {
|
|
1494
|
+
if (target.auto && target.kind === "timestamp") {
|
|
1495
|
+
return "CAST(strftime('%s','now') AS INTEGER) * 1000";
|
|
1496
|
+
}
|
|
1497
|
+
if (target.defaultValue !== void 0) {
|
|
1498
|
+
return sqlLiteral(target.defaultValue);
|
|
1499
|
+
}
|
|
1500
|
+
return "NULL";
|
|
1501
|
+
}
|
|
1502
|
+
function dropCollectionStatements(collection) {
|
|
1503
|
+
const table = quoteIdentifier(collection);
|
|
1504
|
+
const opsTable = quoteIdentifier(`_kora_ops_${collection}`);
|
|
1505
|
+
return [`DROP TABLE IF EXISTS ${table}`, `DROP TABLE IF EXISTS ${opsTable}`];
|
|
1506
|
+
}
|
|
1507
|
+
function columnDefinition(fieldName, descriptor) {
|
|
1508
|
+
const sqlType = mapFieldType(descriptor);
|
|
1509
|
+
const parts = [quoteIdentifier(fieldName), sqlType];
|
|
1510
|
+
if (descriptor.required && descriptor.defaultValue === void 0 && !descriptor.auto) {
|
|
1511
|
+
parts.push("NOT NULL");
|
|
1512
|
+
}
|
|
1513
|
+
if (descriptor.defaultValue !== void 0) {
|
|
1514
|
+
parts.push(`DEFAULT ${sqlLiteral(descriptor.defaultValue)}`);
|
|
1515
|
+
}
|
|
1516
|
+
if (descriptor.kind === "enum" && descriptor.enumValues) {
|
|
1517
|
+
const values = descriptor.enumValues.map((value) => sqlLiteral(value)).join(", ");
|
|
1518
|
+
parts.push(`CHECK (${quoteIdentifier(fieldName)} IN (${values}))`);
|
|
1519
|
+
}
|
|
1520
|
+
return parts.join(" ");
|
|
1521
|
+
}
|
|
1522
|
+
function mapFieldType(descriptor) {
|
|
1523
|
+
switch (descriptor.kind) {
|
|
1524
|
+
case "string":
|
|
1525
|
+
return "TEXT";
|
|
1526
|
+
case "number":
|
|
1527
|
+
return "REAL";
|
|
1528
|
+
case "boolean":
|
|
1529
|
+
return "INTEGER";
|
|
1530
|
+
case "enum":
|
|
1531
|
+
return "TEXT";
|
|
1532
|
+
case "timestamp":
|
|
1533
|
+
return "INTEGER";
|
|
1534
|
+
case "array":
|
|
1535
|
+
return "TEXT";
|
|
1536
|
+
case "richtext":
|
|
1537
|
+
return "BLOB";
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
function sqlLiteral(value) {
|
|
1541
|
+
if (value === null) return "NULL";
|
|
1542
|
+
if (typeof value === "number") return String(value);
|
|
1543
|
+
if (typeof value === "boolean") return value ? "1" : "0";
|
|
1544
|
+
if (typeof value === "string") return `'${value.replaceAll("'", "''")}'`;
|
|
1545
|
+
return `'${JSON.stringify(value).replaceAll("'", "''")}'`;
|
|
1546
|
+
}
|
|
1547
|
+
function quoteIdentifier(identifier) {
|
|
1548
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(identifier)) {
|
|
1549
|
+
throw new Error(`Invalid SQL identifier: ${identifier}`);
|
|
1550
|
+
}
|
|
1551
|
+
return identifier;
|
|
1552
|
+
}
|
|
1553
|
+
function formatChange(change) {
|
|
1554
|
+
switch (change.type) {
|
|
1555
|
+
case "collection-added":
|
|
1556
|
+
return `+ collection ${change.collection}`;
|
|
1557
|
+
case "collection-removed":
|
|
1558
|
+
return `- collection ${change.collection}`;
|
|
1559
|
+
case "field-added":
|
|
1560
|
+
return `+ ${change.collection}.${change.field}`;
|
|
1561
|
+
case "field-removed":
|
|
1562
|
+
return `- ${change.collection}.${change.field}`;
|
|
1563
|
+
case "field-changed":
|
|
1564
|
+
return `~ ${change.collection}.${change.field}`;
|
|
1565
|
+
case "index-added":
|
|
1566
|
+
return `+ index ${change.collection}.${change.index}`;
|
|
1567
|
+
case "index-removed":
|
|
1568
|
+
return `- index ${change.collection}.${change.index}`;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/commands/migrate/migration-runner.ts
|
|
1573
|
+
var import_meta3 = {};
|
|
1574
|
+
var MigrationApplyError = class extends Error {
|
|
1575
|
+
constructor(message, backend, report) {
|
|
1576
|
+
super(message);
|
|
1577
|
+
this.backend = backend;
|
|
1578
|
+
this.report = report;
|
|
1579
|
+
this.name = "MigrationApplyError";
|
|
1580
|
+
}
|
|
1581
|
+
backend;
|
|
1582
|
+
report;
|
|
1583
|
+
};
|
|
1584
|
+
async function runMigration(options) {
|
|
1585
|
+
const report = { backends: [] };
|
|
1586
|
+
const migrationId = options.migrationId ?? `migration-${Date.now()}`;
|
|
1587
|
+
const fromVersion = options.fromVersion ?? 0;
|
|
1588
|
+
const toVersion = options.toVersion ?? 0;
|
|
1589
|
+
if (options.sqlitePath) {
|
|
1590
|
+
try {
|
|
1591
|
+
const sqliteReport = await runSqliteMigration(
|
|
1592
|
+
options.sqlitePath,
|
|
1593
|
+
options.upStatements,
|
|
1594
|
+
migrationId,
|
|
1595
|
+
fromVersion,
|
|
1596
|
+
toVersion,
|
|
1597
|
+
options.projectRoot,
|
|
1598
|
+
options.sqliteDriver
|
|
1599
|
+
);
|
|
1600
|
+
report.backends.push(sqliteReport);
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
throw new MigrationApplyError(error.message, "sqlite", report);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (options.postgresConnectionString) {
|
|
1606
|
+
try {
|
|
1607
|
+
const postgresReport = await runPostgresMigration(
|
|
1608
|
+
options.postgresConnectionString,
|
|
1609
|
+
options.upStatements,
|
|
1610
|
+
migrationId,
|
|
1611
|
+
fromVersion,
|
|
1612
|
+
toVersion,
|
|
1613
|
+
options.postgresClientFactory
|
|
1614
|
+
);
|
|
1615
|
+
report.backends.push(postgresReport);
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
throw new MigrationApplyError(error.message, "postgres", report);
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
return report;
|
|
1621
|
+
}
|
|
1622
|
+
async function runSqliteMigration(path, statements, migrationId, fromVersion, toVersion, projectRoot, driverOverride) {
|
|
1623
|
+
const driver = driverOverride ?? await loadSqliteDriver(projectRoot);
|
|
1624
|
+
const db = driver.open(path);
|
|
1625
|
+
let statementsApplied = 0;
|
|
1626
|
+
try {
|
|
1627
|
+
db.exec("BEGIN");
|
|
1628
|
+
db.exec(
|
|
1629
|
+
"CREATE TABLE IF NOT EXISTS _kora_migrations (id TEXT PRIMARY KEY NOT NULL, from_version INTEGER NOT NULL, to_version INTEGER NOT NULL, applied_at INTEGER NOT NULL)"
|
|
1630
|
+
);
|
|
1631
|
+
const alreadyApplied = typeof db.isMigrationApplied === "function" ? db.isMigrationApplied(migrationId) : false;
|
|
1632
|
+
if (alreadyApplied) {
|
|
1633
|
+
db.exec("COMMIT");
|
|
1634
|
+
return {
|
|
1635
|
+
backend: "sqlite",
|
|
1636
|
+
statementsApplied: 0,
|
|
1637
|
+
historyRecorded: true,
|
|
1638
|
+
skipped: true
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
for (const statement of statements) {
|
|
1642
|
+
db.exec(statement);
|
|
1643
|
+
statementsApplied++;
|
|
1644
|
+
}
|
|
1645
|
+
db.exec(
|
|
1646
|
+
`INSERT OR REPLACE INTO _kora_migrations (id, from_version, to_version, applied_at) VALUES (${sqlLiteral2(migrationId)}, ${fromVersion}, ${toVersion}, ${Date.now()})`
|
|
1647
|
+
);
|
|
1648
|
+
db.exec("COMMIT");
|
|
1649
|
+
return {
|
|
1650
|
+
backend: "sqlite",
|
|
1651
|
+
statementsApplied,
|
|
1652
|
+
historyRecorded: true,
|
|
1653
|
+
skipped: false
|
|
1654
|
+
};
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
try {
|
|
1657
|
+
db.exec("ROLLBACK");
|
|
1658
|
+
} catch {
|
|
1659
|
+
}
|
|
1660
|
+
throw error;
|
|
1661
|
+
} finally {
|
|
1662
|
+
if (typeof db.close === "function") {
|
|
1663
|
+
db.close();
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
async function loadSqliteDriver(projectRoot) {
|
|
1668
|
+
try {
|
|
1669
|
+
const { createRequire } = await import("module");
|
|
1670
|
+
const requireFrom = createRequire(
|
|
1671
|
+
projectRoot ? `${projectRoot}/package.json` : import_meta3.url
|
|
1672
|
+
);
|
|
1673
|
+
const Database = requireFrom("better-sqlite3");
|
|
1674
|
+
return {
|
|
1675
|
+
open(path) {
|
|
1676
|
+
const db = new Database(path);
|
|
1677
|
+
return {
|
|
1678
|
+
exec(sql) {
|
|
1679
|
+
db.exec(sql);
|
|
1680
|
+
},
|
|
1681
|
+
isMigrationApplied(id) {
|
|
1682
|
+
const row = db.prepare("SELECT COUNT(*) AS count FROM _kora_migrations WHERE id = ?").get(id);
|
|
1683
|
+
return (row?.count ?? 0) > 0;
|
|
1684
|
+
},
|
|
1685
|
+
close() {
|
|
1686
|
+
db.close();
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
} catch {
|
|
1692
|
+
throw new Error(
|
|
1693
|
+
'SQLite migration apply requires the "better-sqlite3" package in the target project dependencies.'
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
async function runPostgresMigration(connectionString, statements, migrationId, fromVersion, toVersion, clientFactoryOverride) {
|
|
1698
|
+
const sql = typeof clientFactoryOverride === "function" ? clientFactoryOverride(connectionString) : (await loadPostgresModule()).default(connectionString);
|
|
1699
|
+
let statementsApplied = 0;
|
|
1700
|
+
try {
|
|
1701
|
+
await sql.unsafe("BEGIN");
|
|
1702
|
+
await sql.unsafe(
|
|
1703
|
+
"CREATE TABLE IF NOT EXISTS _kora_migrations (id TEXT PRIMARY KEY, from_version INTEGER NOT NULL, to_version INTEGER NOT NULL, applied_at BIGINT NOT NULL)"
|
|
1704
|
+
);
|
|
1705
|
+
const existing = await sql.unsafe(
|
|
1706
|
+
`SELECT COUNT(*)::int AS count FROM _kora_migrations WHERE id = ${sqlLiteral2(migrationId)}`
|
|
1707
|
+
);
|
|
1708
|
+
if ((existing[0]?.count ?? 0) > 0) {
|
|
1709
|
+
await sql.unsafe("COMMIT");
|
|
1710
|
+
return {
|
|
1711
|
+
backend: "postgres",
|
|
1712
|
+
statementsApplied: 0,
|
|
1713
|
+
historyRecorded: true,
|
|
1714
|
+
skipped: true
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
for (const statement of statements) {
|
|
1718
|
+
await sql.unsafe(statement);
|
|
1719
|
+
statementsApplied++;
|
|
1720
|
+
}
|
|
1721
|
+
await sql.unsafe(
|
|
1722
|
+
`INSERT INTO _kora_migrations (id, from_version, to_version, applied_at) VALUES (${sqlLiteral2(migrationId)}, ${fromVersion}, ${toVersion}, ${Date.now()}) ON CONFLICT (id) DO UPDATE SET from_version = EXCLUDED.from_version, to_version = EXCLUDED.to_version, applied_at = EXCLUDED.applied_at`
|
|
1723
|
+
);
|
|
1724
|
+
await sql.unsafe("COMMIT");
|
|
1725
|
+
return {
|
|
1726
|
+
backend: "postgres",
|
|
1727
|
+
statementsApplied,
|
|
1728
|
+
historyRecorded: true,
|
|
1729
|
+
skipped: false
|
|
1730
|
+
};
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
try {
|
|
1733
|
+
await sql.unsafe("ROLLBACK");
|
|
1734
|
+
} catch {
|
|
1735
|
+
}
|
|
1736
|
+
throw error;
|
|
1737
|
+
} finally {
|
|
1738
|
+
if (typeof sql.end === "function") {
|
|
1739
|
+
await sql.end();
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
function sqlLiteral2(value) {
|
|
1744
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
1745
|
+
}
|
|
1746
|
+
async function loadPostgresModule() {
|
|
1747
|
+
try {
|
|
1748
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
1749
|
+
const mod = await dynamicImport("postgres");
|
|
1750
|
+
if (typeof mod === "object" && mod !== null && "default" in mod) {
|
|
1751
|
+
return mod;
|
|
1752
|
+
}
|
|
1753
|
+
throw new Error("Invalid postgres module");
|
|
1754
|
+
} catch {
|
|
1755
|
+
throw new Error(
|
|
1756
|
+
'PostgreSQL migration apply requires the "postgres" package in the target project dependencies.'
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
// src/commands/migrate/schema-loader.ts
|
|
1762
|
+
var import_node_child_process6 = require("child_process");
|
|
1763
|
+
var import_node_path8 = require("path");
|
|
1764
|
+
var import_node_url4 = require("url");
|
|
1765
|
+
async function loadSchemaDefinition(schemaPath, projectRoot) {
|
|
1766
|
+
const ext = (0, import_node_path8.extname)(schemaPath);
|
|
1767
|
+
const moduleValue = ext === ".ts" || ext === ".mts" || ext === ".cts" ? await loadTypeScriptModule(schemaPath, projectRoot) : await import(`${(0, import_node_url4.pathToFileURL)(schemaPath).href}?t=${Date.now()}-${Math.random()}`);
|
|
1768
|
+
return extractSchema2(moduleValue);
|
|
1769
|
+
}
|
|
1770
|
+
function extractSchema2(value) {
|
|
1771
|
+
if (typeof value !== "object" || value === null) {
|
|
1772
|
+
throw new Error("Schema module must export an object.");
|
|
1773
|
+
}
|
|
1774
|
+
const moduleRecord = value;
|
|
1775
|
+
const candidate = moduleRecord.default ?? moduleRecord;
|
|
1776
|
+
if (!isSchemaDefinition2(candidate)) {
|
|
1777
|
+
throw new Error("Schema module must export a valid SchemaDefinition as default export.");
|
|
1778
|
+
}
|
|
1779
|
+
return candidate;
|
|
1780
|
+
}
|
|
1781
|
+
function isSchemaDefinition2(value) {
|
|
1782
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1783
|
+
const object = value;
|
|
1784
|
+
return typeof object.version === "number" && typeof object.collections === "object" && object.collections !== null && typeof object.relations === "object" && object.relations !== null;
|
|
1785
|
+
}
|
|
1786
|
+
async function loadTypeScriptModule(schemaPath, projectRoot) {
|
|
1787
|
+
const tsxBinary = await resolveProjectBinary(projectRoot, "tsx");
|
|
1788
|
+
if (!tsxBinary) {
|
|
1789
|
+
throw new Error(
|
|
1790
|
+
`Schema file is TypeScript (${schemaPath}) but local "tsx" was not found. Install tsx in the project.`
|
|
1791
|
+
);
|
|
1792
|
+
}
|
|
1793
|
+
const script = [
|
|
1794
|
+
"import { pathToFileURL } from 'node:url'",
|
|
1795
|
+
"const modulePath = process.argv[process.argv.length - 1]",
|
|
1796
|
+
"const mod = await import(pathToFileURL(modulePath).href)",
|
|
1797
|
+
"const value = mod.default ?? mod",
|
|
1798
|
+
"process.stdout.write(JSON.stringify(value))"
|
|
1799
|
+
].join(";");
|
|
1800
|
+
const output = await runCommand2(tsxBinary, ["--eval", script, schemaPath], projectRoot);
|
|
1801
|
+
try {
|
|
1802
|
+
return JSON.parse(output);
|
|
1803
|
+
} catch {
|
|
1804
|
+
throw new Error(`Failed to parse schema module output for ${schemaPath}`);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
async function runCommand2(command, args, cwd) {
|
|
1808
|
+
return await new Promise((resolve7, reject) => {
|
|
1809
|
+
const child = (0, import_node_child_process6.spawn)(command, args, {
|
|
1810
|
+
cwd,
|
|
1811
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1812
|
+
env: process.env
|
|
1813
|
+
});
|
|
1814
|
+
let stdout = "";
|
|
1815
|
+
let stderr = "";
|
|
1816
|
+
child.stdout?.on("data", (chunk) => {
|
|
1817
|
+
stdout += chunk.toString("utf-8");
|
|
1818
|
+
});
|
|
1819
|
+
child.stderr?.on("data", (chunk) => {
|
|
1820
|
+
stderr += chunk.toString("utf-8");
|
|
1821
|
+
});
|
|
1822
|
+
child.on("error", (error) => {
|
|
1823
|
+
reject(error);
|
|
1824
|
+
});
|
|
1825
|
+
child.on("exit", (code) => {
|
|
1826
|
+
if (code === 0) {
|
|
1827
|
+
resolve7(stdout.trim());
|
|
1828
|
+
return;
|
|
1829
|
+
}
|
|
1830
|
+
reject(
|
|
1831
|
+
new Error(`Failed to load TypeScript schema (exit ${code ?? "unknown"}): ${stderr.trim()}`)
|
|
1832
|
+
);
|
|
1833
|
+
});
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
// src/commands/migrate/migrate-command.ts
|
|
1838
|
+
var SNAPSHOT_PATH = "kora/schema.snapshot.json";
|
|
1839
|
+
var MIGRATIONS_DIR = "kora/migrations";
|
|
1840
|
+
var migrateCommand = (0, import_citty4.defineCommand)({
|
|
1841
|
+
meta: {
|
|
1842
|
+
name: "migrate",
|
|
1843
|
+
description: "Detect schema changes and generate/apply migrations"
|
|
1844
|
+
},
|
|
1845
|
+
args: {
|
|
1846
|
+
apply: {
|
|
1847
|
+
type: "boolean",
|
|
1848
|
+
description: "Apply migration to configured database backends",
|
|
1849
|
+
default: false
|
|
1850
|
+
},
|
|
1851
|
+
schema: {
|
|
1852
|
+
type: "string",
|
|
1853
|
+
description: "Path to schema file"
|
|
1854
|
+
},
|
|
1855
|
+
db: {
|
|
1856
|
+
type: "string",
|
|
1857
|
+
description: "SQLite database path for --apply (overrides config)"
|
|
1858
|
+
},
|
|
1859
|
+
"output-dir": {
|
|
1860
|
+
type: "string",
|
|
1861
|
+
description: "Migration output directory",
|
|
1862
|
+
default: MIGRATIONS_DIR
|
|
1863
|
+
},
|
|
1864
|
+
"dry-run": {
|
|
1865
|
+
type: "boolean",
|
|
1866
|
+
description: "Preview migration changes without writing files",
|
|
1867
|
+
default: false
|
|
1868
|
+
},
|
|
1869
|
+
force: {
|
|
1870
|
+
type: "boolean",
|
|
1871
|
+
description: "Skip breaking-change confirmation prompts",
|
|
1872
|
+
default: false
|
|
1873
|
+
}
|
|
1874
|
+
},
|
|
1875
|
+
async run({ args }) {
|
|
1876
|
+
const logger = createLogger();
|
|
1877
|
+
const projectRoot = await findProjectRoot();
|
|
1878
|
+
if (!projectRoot) {
|
|
1879
|
+
throw new InvalidProjectError(process.cwd());
|
|
1880
|
+
}
|
|
1881
|
+
const config = await loadKoraConfig(projectRoot);
|
|
1882
|
+
const resolvedSchemaPath = typeof args.schema === "string" ? (0, import_node_path9.resolve)(projectRoot, args.schema) : typeof config?.schema === "string" ? (0, import_node_path9.resolve)(projectRoot, config.schema) : await findSchemaFile(projectRoot);
|
|
1883
|
+
if (!resolvedSchemaPath) {
|
|
1884
|
+
throw new SchemaNotFoundError(["src/schema.ts", "schema.ts", "src/schema.js", "schema.js"]);
|
|
1885
|
+
}
|
|
1886
|
+
const currentSchema = await loadSchemaDefinition(resolvedSchemaPath, projectRoot);
|
|
1887
|
+
const snapshotFile = (0, import_node_path9.join)(projectRoot, SNAPSHOT_PATH);
|
|
1888
|
+
const previousSchema = await readSchemaSnapshot(snapshotFile);
|
|
1889
|
+
if (!previousSchema) {
|
|
1890
|
+
if (args["dry-run"] === true) {
|
|
1891
|
+
logger.info("No schema snapshot found. Dry run: baseline snapshot would be created.");
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
await writeSchemaSnapshot(snapshotFile, currentSchema);
|
|
1895
|
+
logger.success(`Initialized schema snapshot at ${snapshotFile}`);
|
|
1896
|
+
logger.step("Run `kora migrate` again after schema changes to generate migrations.");
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
const diff = diffSchemas(previousSchema, currentSchema);
|
|
1900
|
+
const outputDir = typeof args["output-dir"] === "string" ? (0, import_node_path9.resolve)(projectRoot, args["output-dir"]) : (0, import_node_path9.resolve)(projectRoot, MIGRATIONS_DIR);
|
|
1901
|
+
if (!diff.hasChanges) {
|
|
1902
|
+
logger.success("No schema changes detected.");
|
|
1903
|
+
if (args.apply === true) {
|
|
1904
|
+
const sqlitePath = resolveSqliteApplyPath(args.db, projectRoot, config);
|
|
1905
|
+
const postgresConnectionString = resolvePostgresConnectionString(config);
|
|
1906
|
+
const pending = await listMigrationManifests(outputDir);
|
|
1907
|
+
if (pending.length === 0) {
|
|
1908
|
+
logger.step("No migration files found to apply.");
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
for (const manifest of pending) {
|
|
1912
|
+
const report = await runMigration({
|
|
1913
|
+
upStatements: manifest.up,
|
|
1914
|
+
migrationId: manifest.id,
|
|
1915
|
+
fromVersion: manifest.fromVersion,
|
|
1916
|
+
toVersion: manifest.toVersion,
|
|
1917
|
+
sqlitePath,
|
|
1918
|
+
postgresConnectionString,
|
|
1919
|
+
projectRoot
|
|
1920
|
+
});
|
|
1921
|
+
for (const backend of report.backends) {
|
|
1922
|
+
logger.step(
|
|
1923
|
+
` ${manifest.id} -> ${backend.backend}: applied=${backend.statementsApplied}, skipped=${backend.skipped}`
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
const generated = generateMigration(previousSchema, currentSchema, diff);
|
|
1931
|
+
logger.banner();
|
|
1932
|
+
logger.info(`Detected schema change: v${diff.fromVersion} \u2192 v${diff.toVersion}`);
|
|
1933
|
+
logger.blank();
|
|
1934
|
+
logger.info("Changes:");
|
|
1935
|
+
for (const line of generated.summary) {
|
|
1936
|
+
logger.step(` ${line}`);
|
|
1937
|
+
}
|
|
1938
|
+
if (diff.hasBreakingChanges && args["dry-run"] !== true) {
|
|
1939
|
+
logger.blank();
|
|
1940
|
+
logger.warn("Breaking schema changes detected.");
|
|
1941
|
+
const shouldContinue = await confirmBreakingChanges(args.force === true);
|
|
1942
|
+
if (!shouldContinue) {
|
|
1943
|
+
logger.warn("Migration generation aborted.");
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
if (args["dry-run"] === true) {
|
|
1948
|
+
logger.blank();
|
|
1949
|
+
logger.warn("Dry run enabled: no files written, no migrations applied.");
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
await (0, import_promises6.mkdir)(outputDir, { recursive: true });
|
|
1953
|
+
const migrationPath = await writeMigrationFile(outputDir, diff.fromVersion, diff.toVersion, generated);
|
|
1954
|
+
await writeSchemaSnapshot(snapshotFile, currentSchema);
|
|
1955
|
+
logger.blank();
|
|
1956
|
+
logger.success(`Generated migration: ${migrationPath}`);
|
|
1957
|
+
if (args.apply === true) {
|
|
1958
|
+
const sqlitePath = resolveSqliteApplyPath(args.db, projectRoot, config);
|
|
1959
|
+
const postgresConnectionString = resolvePostgresConnectionString(config);
|
|
1960
|
+
const pending = await listMigrationManifests(outputDir);
|
|
1961
|
+
for (const manifest of pending) {
|
|
1962
|
+
const report = await runMigration({
|
|
1963
|
+
upStatements: manifest.up,
|
|
1964
|
+
migrationId: manifest.id,
|
|
1965
|
+
fromVersion: manifest.fromVersion,
|
|
1966
|
+
toVersion: manifest.toVersion,
|
|
1967
|
+
sqlitePath,
|
|
1968
|
+
postgresConnectionString,
|
|
1969
|
+
projectRoot
|
|
1970
|
+
});
|
|
1971
|
+
for (const backend of report.backends) {
|
|
1972
|
+
logger.step(
|
|
1973
|
+
` ${manifest.id} -> ${backend.backend}: applied=${backend.statementsApplied}, skipped=${backend.skipped}, history=${backend.historyRecorded}`
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
logger.success("Applied pending migrations successfully.");
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
});
|
|
1981
|
+
async function readSchemaSnapshot(path) {
|
|
1982
|
+
try {
|
|
1983
|
+
const content = await (0, import_promises6.readFile)(path, "utf-8");
|
|
1984
|
+
return JSON.parse(content);
|
|
1985
|
+
} catch {
|
|
1986
|
+
return null;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
async function confirmBreakingChanges(force) {
|
|
1990
|
+
if (force) {
|
|
1991
|
+
return true;
|
|
1992
|
+
}
|
|
1993
|
+
if (!isInteractiveTerminal()) {
|
|
1994
|
+
throw new Error(
|
|
1995
|
+
"Breaking schema changes require confirmation. Re-run with --force to continue or --dry-run to preview."
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
return await promptConfirm("Continue and generate a breaking migration?", false);
|
|
1999
|
+
}
|
|
2000
|
+
function isInteractiveTerminal() {
|
|
2001
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
2002
|
+
}
|
|
2003
|
+
async function writeSchemaSnapshot(path, schema) {
|
|
2004
|
+
await (0, import_promises6.mkdir)((0, import_node_path9.dirname)(path), { recursive: true });
|
|
2005
|
+
await (0, import_promises6.writeFile)(path, `${JSON.stringify(schema, null, 2)}
|
|
2006
|
+
`, "utf-8");
|
|
2007
|
+
}
|
|
2008
|
+
async function writeMigrationFile(outputDir, fromVersion, toVersion, generated) {
|
|
2009
|
+
const existing = await (0, import_promises6.readdir)(outputDir).catch(() => []);
|
|
2010
|
+
const sequence = existing.filter((file) => /^\d{3}-/.test(file)).length + 1;
|
|
2011
|
+
const filename = `${String(sequence).padStart(3, "0")}-v${fromVersion}-to-v${toVersion}.ts`;
|
|
2012
|
+
const path = (0, import_node_path9.join)(outputDir, filename);
|
|
2013
|
+
const migrationId = filename.replace(/\.ts$/, "");
|
|
2014
|
+
const fileContent = [
|
|
2015
|
+
`export const up = ${JSON.stringify(generated.up, null, 2)} as const`,
|
|
2016
|
+
"",
|
|
2017
|
+
`export const down = ${JSON.stringify(generated.down, null, 2)} as const`,
|
|
2018
|
+
"",
|
|
2019
|
+
`export const summary = ${JSON.stringify(generated.summary, null, 2)} as const`,
|
|
2020
|
+
"",
|
|
2021
|
+
`export const containsBreakingChanges = ${generated.containsBreakingChanges}`,
|
|
2022
|
+
""
|
|
2023
|
+
].join("\n");
|
|
2024
|
+
await (0, import_promises6.writeFile)(path, fileContent, "utf-8");
|
|
2025
|
+
await writeMigrationManifest((0, import_node_path9.join)(outputDir, `${migrationId}.json`), {
|
|
2026
|
+
id: migrationId,
|
|
2027
|
+
fromVersion,
|
|
2028
|
+
toVersion,
|
|
2029
|
+
up: generated.up,
|
|
2030
|
+
down: generated.down,
|
|
2031
|
+
summary: generated.summary,
|
|
2032
|
+
containsBreakingChanges: generated.containsBreakingChanges
|
|
2033
|
+
});
|
|
2034
|
+
return path;
|
|
2035
|
+
}
|
|
2036
|
+
async function writeMigrationManifest(path, manifest) {
|
|
2037
|
+
await (0, import_promises6.writeFile)(path, `${JSON.stringify(manifest, null, 2)}
|
|
2038
|
+
`, "utf-8");
|
|
2039
|
+
}
|
|
2040
|
+
async function listMigrationManifests(outputDir) {
|
|
2041
|
+
const files = await (0, import_promises6.readdir)(outputDir).catch(() => []);
|
|
2042
|
+
const migrationFiles = files.filter((file) => /^\d{3}-.*\.ts$/.test(file)).sort((left, right) => left.localeCompare(right));
|
|
2043
|
+
const manifests = [];
|
|
2044
|
+
for (const file of migrationFiles) {
|
|
2045
|
+
const id = file.replace(/\.ts$/, "");
|
|
2046
|
+
const manifestPath = (0, import_node_path9.join)(outputDir, `${id}.json`);
|
|
2047
|
+
const jsonManifest = await readMigrationManifest(manifestPath);
|
|
2048
|
+
if (jsonManifest) {
|
|
2049
|
+
manifests.push({ ...jsonManifest, id });
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
const migrationPath = (0, import_node_path9.join)(outputDir, file);
|
|
2053
|
+
const sourceManifest = await readMigrationManifestFromSource(migrationPath, id);
|
|
2054
|
+
manifests.push(sourceManifest);
|
|
2055
|
+
}
|
|
2056
|
+
return manifests;
|
|
2057
|
+
}
|
|
2058
|
+
async function readMigrationManifest(path) {
|
|
2059
|
+
try {
|
|
2060
|
+
const content = await (0, import_promises6.readFile)(path, "utf-8");
|
|
2061
|
+
return JSON.parse(content);
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
const code = error.code;
|
|
2064
|
+
if (code === "ENOENT") {
|
|
2065
|
+
return null;
|
|
2066
|
+
}
|
|
2067
|
+
throw error;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
async function readMigrationManifestFromSource(path, id) {
|
|
2071
|
+
const content = await (0, import_promises6.readFile)(path, "utf-8");
|
|
2072
|
+
const versions = parseVersionsFromMigrationId(id);
|
|
2073
|
+
return {
|
|
2074
|
+
id,
|
|
2075
|
+
fromVersion: versions.fromVersion,
|
|
2076
|
+
toVersion: versions.toVersion,
|
|
2077
|
+
up: parseStringArrayExport(content, "up"),
|
|
2078
|
+
down: parseStringArrayExport(content, "down"),
|
|
2079
|
+
summary: parseStringArrayExport(content, "summary"),
|
|
2080
|
+
containsBreakingChanges: parseBooleanExport(content, "containsBreakingChanges")
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
function parseVersionsFromMigrationId(id) {
|
|
2084
|
+
const match = id.match(/-v(\d+)-to-v(\d+)$/);
|
|
2085
|
+
if (!match) {
|
|
2086
|
+
throw new Error(`Migration id "${id}" does not include a vX-to-vY version suffix.`);
|
|
2087
|
+
}
|
|
2088
|
+
return {
|
|
2089
|
+
fromVersion: Number.parseInt(match[1], 10),
|
|
2090
|
+
toVersion: Number.parseInt(match[2], 10)
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
function parseStringArrayExport(source, exportName) {
|
|
2094
|
+
const expression = parseExportExpression(source, exportName);
|
|
2095
|
+
const parsed = JSON.parse(expression);
|
|
2096
|
+
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
|
|
2097
|
+
throw new Error(`Migration export "${exportName}" must be a string array.`);
|
|
2098
|
+
}
|
|
2099
|
+
return parsed;
|
|
2100
|
+
}
|
|
2101
|
+
function parseBooleanExport(source, exportName) {
|
|
2102
|
+
const expression = parseLiteralExport(source, exportName);
|
|
2103
|
+
if (expression === "true") return true;
|
|
2104
|
+
if (expression === "false") return false;
|
|
2105
|
+
throw new Error(`Migration export "${exportName}" must be a boolean literal.`);
|
|
2106
|
+
}
|
|
2107
|
+
function parseExportExpression(source, exportName) {
|
|
2108
|
+
const escapedName = exportName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2109
|
+
const regex = new RegExp(`export const ${escapedName} = ([\\s\\S]*?) as const`);
|
|
2110
|
+
const match = source.match(regex);
|
|
2111
|
+
if (!match || !match[1]) {
|
|
2112
|
+
throw new Error(`Failed to read migration export "${exportName}".`);
|
|
2113
|
+
}
|
|
2114
|
+
return match[1].trim();
|
|
2115
|
+
}
|
|
2116
|
+
function parseLiteralExport(source, exportName) {
|
|
2117
|
+
const escapedName = exportName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2118
|
+
const regex = new RegExp(`export const ${escapedName} = ([^
|
|
2119
|
+
\r]+)`);
|
|
2120
|
+
const match = source.match(regex);
|
|
2121
|
+
if (!match || !match[1]) {
|
|
2122
|
+
throw new Error(`Failed to read migration export "${exportName}".`);
|
|
2123
|
+
}
|
|
2124
|
+
return match[1].trim();
|
|
2125
|
+
}
|
|
2126
|
+
function resolveSqliteApplyPath(dbArg, projectRoot, config) {
|
|
2127
|
+
if (typeof dbArg === "string") {
|
|
2128
|
+
return (0, import_node_path9.resolve)(projectRoot, dbArg);
|
|
2129
|
+
}
|
|
2130
|
+
const sync = config?.dev?.sync;
|
|
2131
|
+
if (typeof sync === "object" && sync !== null) {
|
|
2132
|
+
if (sync.store === "sqlite") {
|
|
2133
|
+
return (0, import_node_path9.join)(projectRoot, "kora-sync.db");
|
|
2134
|
+
}
|
|
2135
|
+
if (typeof sync.store === "object" && sync.store !== null && sync.store.type === "sqlite") {
|
|
2136
|
+
if (typeof sync.store.filename === "string" && sync.store.filename.length > 0) {
|
|
2137
|
+
return (0, import_node_path9.resolve)(projectRoot, sync.store.filename);
|
|
2138
|
+
}
|
|
2139
|
+
return (0, import_node_path9.join)(projectRoot, "kora-sync.db");
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return void 0;
|
|
2143
|
+
}
|
|
2144
|
+
function resolvePostgresConnectionString(config) {
|
|
2145
|
+
const sync = config?.dev?.sync;
|
|
2146
|
+
if (typeof sync !== "object" || sync === null) return void 0;
|
|
2147
|
+
if (sync.store === "postgres") {
|
|
2148
|
+
return process.env.DATABASE_URL;
|
|
2149
|
+
}
|
|
2150
|
+
if (typeof sync.store === "object" && sync.store !== null && sync.store.type === "postgres") {
|
|
2151
|
+
return sync.store.connectionString;
|
|
2152
|
+
}
|
|
2153
|
+
return void 0;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
// src/bin.ts
|
|
2157
|
+
var main = (0, import_citty5.defineCommand)({
|
|
2158
|
+
meta: {
|
|
2159
|
+
name: "kora",
|
|
2160
|
+
description: "Kora.js \u2014 Offline-first application framework"
|
|
2161
|
+
},
|
|
2162
|
+
subCommands: {
|
|
2163
|
+
create: createCommand,
|
|
2164
|
+
dev: devCommand,
|
|
2165
|
+
generate: generateCommand,
|
|
2166
|
+
migrate: migrateCommand
|
|
2167
|
+
}
|
|
2168
|
+
});
|
|
2169
|
+
(0, import_citty5.runMain)(main);
|
|
2170
|
+
//# sourceMappingURL=bin.cjs.map
|