@korajs/cli 0.1.14 → 0.2.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/README.md +49 -0
- package/dist/bin.cjs +2517 -332
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +12 -10
- package/dist/bin.js.map +1 -1
- package/dist/chunk-E4JG7THU.js +1550 -0
- package/dist/chunk-E4JG7THU.js.map +1 -0
- package/dist/chunk-KTSRAPSE.js +752 -0
- package/dist/chunk-KTSRAPSE.js.map +1 -0
- package/dist/chunk-ZGYRDYXS.js +609 -0
- package/dist/chunk-ZGYRDYXS.js.map +1 -0
- package/dist/create.cjs +974 -159
- package/dist/create.cjs.map +1 -1
- package/dist/create.js +2 -2
- package/dist/index.cjs +2103 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +617 -1
- package/dist/index.d.ts +617 -1
- package/dist/index.js +91 -5
- package/package.json +19 -13
- package/dist/chunk-N36PFOSA.js +0 -103
- package/dist/chunk-N36PFOSA.js.map +0 -1
- package/dist/chunk-SYOFLJLB.js +0 -437
- package/dist/chunk-SYOFLJLB.js.map +0 -1
- package/dist/chunk-VGOOQ56H.js +0 -103
- package/dist/chunk-VGOOQ56H.js.map +0 -1
package/dist/create.cjs
CHANGED
|
@@ -1,4 +1,26 @@
|
|
|
1
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
|
+
));
|
|
2
24
|
|
|
3
25
|
// src/create.ts
|
|
4
26
|
var import_citty2 = require("citty");
|
|
@@ -6,7 +28,7 @@ var import_citty2 = require("citty");
|
|
|
6
28
|
// src/commands/create/create-command.ts
|
|
7
29
|
var import_node_child_process2 = require("child_process");
|
|
8
30
|
var import_node_fs2 = require("fs");
|
|
9
|
-
var
|
|
31
|
+
var import_node_path5 = require("path");
|
|
10
32
|
var import_node_url2 = require("url");
|
|
11
33
|
var import_citty = require("citty");
|
|
12
34
|
|
|
@@ -25,6 +47,209 @@ var ProjectExistsError = class extends import_core.KoraError {
|
|
|
25
47
|
directory;
|
|
26
48
|
};
|
|
27
49
|
|
|
50
|
+
// src/prompts/preferences.ts
|
|
51
|
+
var import_conf = __toESM(require("conf"), 1);
|
|
52
|
+
var DEFAULT_PREFERENCES = {
|
|
53
|
+
framework: "react",
|
|
54
|
+
tailwind: true,
|
|
55
|
+
sync: true,
|
|
56
|
+
db: "sqlite",
|
|
57
|
+
dbProvider: "none",
|
|
58
|
+
auth: "none",
|
|
59
|
+
packageManager: "pnpm"
|
|
60
|
+
};
|
|
61
|
+
var PREFERENCES_KEY = "create.defaults";
|
|
62
|
+
var PreferenceStore = class {
|
|
63
|
+
store;
|
|
64
|
+
constructor() {
|
|
65
|
+
this.store = new import_conf.default({
|
|
66
|
+
projectName: "korajs-cli"
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
getCreatePreferences() {
|
|
70
|
+
return this.store.get(PREFERENCES_KEY) ?? null;
|
|
71
|
+
}
|
|
72
|
+
saveCreatePreferences(preferences) {
|
|
73
|
+
this.store.set(PREFERENCES_KEY, preferences);
|
|
74
|
+
}
|
|
75
|
+
clearCreatePreferences() {
|
|
76
|
+
this.store.delete(PREFERENCES_KEY);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
function getCreatePreferencesOrDefault(store) {
|
|
80
|
+
return store.getCreatePreferences() ?? DEFAULT_PREFERENCES;
|
|
81
|
+
}
|
|
82
|
+
function getDefaultCreatePreferences() {
|
|
83
|
+
return { ...DEFAULT_PREFERENCES };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/prompts/prompt-client.ts
|
|
87
|
+
var import_prompts = require("@clack/prompts");
|
|
88
|
+
|
|
89
|
+
// src/utils/prompt.ts
|
|
90
|
+
var import_node_readline = require("readline");
|
|
91
|
+
function promptText(message, defaultValue, options) {
|
|
92
|
+
return new Promise((resolve5) => {
|
|
93
|
+
const rl = createReadline(options);
|
|
94
|
+
const suffix = defaultValue !== void 0 ? ` (${defaultValue})` : "";
|
|
95
|
+
rl.question(` ? ${message}${suffix}: `, (answer) => {
|
|
96
|
+
rl.close();
|
|
97
|
+
const trimmed = answer.trim();
|
|
98
|
+
resolve5(trimmed || defaultValue || "");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
function promptSelect(message, choices, options) {
|
|
103
|
+
return new Promise((resolve5) => {
|
|
104
|
+
const rl = createReadline(options);
|
|
105
|
+
const out = options?.output ?? process.stdout;
|
|
106
|
+
out.write(` ? ${message}
|
|
107
|
+
`);
|
|
108
|
+
for (let i = 0; i < choices.length; i++) {
|
|
109
|
+
const choice = choices[i];
|
|
110
|
+
if (choice) {
|
|
111
|
+
out.write(` ${i + 1}) ${choice.label}
|
|
112
|
+
`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const ask = () => {
|
|
116
|
+
rl.question(" > ", (answer) => {
|
|
117
|
+
const index = Number.parseInt(answer.trim(), 10) - 1;
|
|
118
|
+
const selected = choices[index];
|
|
119
|
+
if (selected) {
|
|
120
|
+
rl.close();
|
|
121
|
+
resolve5(selected.value);
|
|
122
|
+
} else {
|
|
123
|
+
out.write(` Please enter a number between 1 and ${choices.length}
|
|
124
|
+
`);
|
|
125
|
+
ask();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
ask();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function promptConfirm(message, defaultValue = false, options) {
|
|
133
|
+
return new Promise((resolve5) => {
|
|
134
|
+
const rl = createReadline(options);
|
|
135
|
+
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
136
|
+
const ask = () => {
|
|
137
|
+
rl.question(` ? ${message} (${suffix}): `, (answer) => {
|
|
138
|
+
const normalized = answer.trim().toLowerCase();
|
|
139
|
+
if (normalized.length === 0) {
|
|
140
|
+
rl.close();
|
|
141
|
+
resolve5(defaultValue);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (normalized === "y" || normalized === "yes") {
|
|
145
|
+
rl.close();
|
|
146
|
+
resolve5(true);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (normalized === "n" || normalized === "no") {
|
|
150
|
+
rl.close();
|
|
151
|
+
resolve5(false);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
(options?.output ?? process.stdout).write(" Please answer with y or n\n");
|
|
155
|
+
ask();
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
ask();
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function createReadline(options) {
|
|
162
|
+
return (0, import_node_readline.createInterface)({
|
|
163
|
+
input: options?.input ?? process.stdin,
|
|
164
|
+
output: options?.output ?? process.stdout
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/prompts/prompt-client.ts
|
|
169
|
+
var PromptCancelledError = class extends Error {
|
|
170
|
+
constructor(message = "Prompt cancelled by user") {
|
|
171
|
+
super(message);
|
|
172
|
+
this.name = "PromptCancelledError";
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var ReadlinePromptClient = class {
|
|
176
|
+
async text(message, defaultValue) {
|
|
177
|
+
return promptText(message, defaultValue);
|
|
178
|
+
}
|
|
179
|
+
async select(message, options) {
|
|
180
|
+
return promptSelect(
|
|
181
|
+
message,
|
|
182
|
+
options.filter((option) => option.disabled !== true).map((option) => ({ label: option.label, value: option.value }))
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
async confirm(message, defaultValue = false) {
|
|
186
|
+
return promptConfirm(message, defaultValue);
|
|
187
|
+
}
|
|
188
|
+
intro(message) {
|
|
189
|
+
void message;
|
|
190
|
+
}
|
|
191
|
+
outro(message) {
|
|
192
|
+
void message;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
function createPromptClient() {
|
|
196
|
+
const canUseInteractiveClack = typeof process !== "undefined" && process.stdin.isTTY && process.stdout.isTTY;
|
|
197
|
+
if (canUseInteractiveClack) {
|
|
198
|
+
return new ClackPromptClient();
|
|
199
|
+
}
|
|
200
|
+
return new ReadlinePromptClient();
|
|
201
|
+
}
|
|
202
|
+
var ClackPromptClient = class {
|
|
203
|
+
async text(message, defaultValue) {
|
|
204
|
+
const result = await (0, import_prompts.text)({
|
|
205
|
+
message,
|
|
206
|
+
placeholder: defaultValue,
|
|
207
|
+
defaultValue
|
|
208
|
+
});
|
|
209
|
+
if ((0, import_prompts.isCancel)(result)) {
|
|
210
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
211
|
+
throw new PromptCancelledError();
|
|
212
|
+
}
|
|
213
|
+
const value = result.trim();
|
|
214
|
+
if (value.length > 0) return value;
|
|
215
|
+
return defaultValue ?? "";
|
|
216
|
+
}
|
|
217
|
+
async select(message, options) {
|
|
218
|
+
const mappedOptions = options.map((option) => ({
|
|
219
|
+
label: option.label,
|
|
220
|
+
value: option.value,
|
|
221
|
+
hint: option.hint,
|
|
222
|
+
disabled: option.disabled
|
|
223
|
+
}));
|
|
224
|
+
const result = await (0, import_prompts.select)({
|
|
225
|
+
message,
|
|
226
|
+
options: mappedOptions
|
|
227
|
+
});
|
|
228
|
+
if ((0, import_prompts.isCancel)(result)) {
|
|
229
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
230
|
+
throw new PromptCancelledError();
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
async confirm(message, defaultValue = false) {
|
|
235
|
+
const result = await (0, import_prompts.confirm)({
|
|
236
|
+
message,
|
|
237
|
+
initialValue: defaultValue
|
|
238
|
+
});
|
|
239
|
+
if ((0, import_prompts.isCancel)(result)) {
|
|
240
|
+
(0, import_prompts.cancel)("Operation cancelled.");
|
|
241
|
+
throw new PromptCancelledError();
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
intro(message) {
|
|
246
|
+
(0, import_prompts.intro)(message);
|
|
247
|
+
}
|
|
248
|
+
outro(message) {
|
|
249
|
+
(0, import_prompts.outro)(message);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
28
253
|
// src/types.ts
|
|
29
254
|
var PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
|
|
30
255
|
var TEMPLATES = [
|
|
@@ -33,28 +258,6 @@ var TEMPLATES = [
|
|
|
33
258
|
"react-sync",
|
|
34
259
|
"react-basic"
|
|
35
260
|
];
|
|
36
|
-
var TEMPLATE_INFO = [
|
|
37
|
-
{
|
|
38
|
-
name: "react-tailwind-sync",
|
|
39
|
-
label: "React + Tailwind (with sync)",
|
|
40
|
-
description: "Polished dark-themed app with Tailwind CSS and sync server (Recommended)"
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
name: "react-tailwind",
|
|
44
|
-
label: "React + Tailwind (local-only)",
|
|
45
|
-
description: "Polished dark-themed app with Tailwind CSS \u2014 no sync server"
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: "react-sync",
|
|
49
|
-
label: "React + CSS (with sync)",
|
|
50
|
-
description: "Clean CSS app with sync server included"
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
name: "react-basic",
|
|
54
|
-
label: "React + CSS (local-only)",
|
|
55
|
-
description: "Clean CSS app \u2014 no sync server"
|
|
56
|
-
}
|
|
57
|
-
];
|
|
58
261
|
|
|
59
262
|
// src/utils/fs-helpers.ts
|
|
60
263
|
var import_promises = require("fs/promises");
|
|
@@ -128,60 +331,421 @@ function getRunDevCommand(pm) {
|
|
|
128
331
|
return `${pm} dev`;
|
|
129
332
|
}
|
|
130
333
|
|
|
131
|
-
// src/
|
|
132
|
-
var
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
}
|
|
334
|
+
// src/commands/create/environment-detection.ts
|
|
335
|
+
var import_promises2 = require("fs/promises");
|
|
336
|
+
var import_node_path2 = require("path");
|
|
337
|
+
function detectEditorFromEnvironment(env = process.env) {
|
|
338
|
+
const termProgram = String(env.TERM_PROGRAM ?? "").toLowerCase();
|
|
339
|
+
const editorValue = `${String(env.EDITOR ?? "")} ${String(env.VISUAL ?? "")}`.toLowerCase();
|
|
340
|
+
if (termProgram.includes("cursor") || env.CURSOR_TRACE_ID !== void 0 || env.CURSOR_SESSION_ID !== void 0 || editorValue.includes("cursor")) {
|
|
341
|
+
return { editor: "cursor", source: "env" };
|
|
342
|
+
}
|
|
343
|
+
if (termProgram.includes("windsurf") || env.WINDSURF_SESSION_ID !== void 0 || editorValue.includes("windsurf")) {
|
|
344
|
+
return { editor: "windsurf", source: "env" };
|
|
345
|
+
}
|
|
346
|
+
if (termProgram.includes("vscode") || env.VSCODE_GIT_IPC_HANDLE !== void 0 || env.VSCODE_IPC_HOOK !== void 0 || env.VSCODE_PID !== void 0 || editorValue.includes("code")) {
|
|
347
|
+
return { editor: "vscode", source: "env" };
|
|
348
|
+
}
|
|
349
|
+
if (termProgram.includes("zed") || env.ZED_TERM !== void 0 || editorValue.includes("zed")) {
|
|
350
|
+
return { editor: "zed", source: "env" };
|
|
351
|
+
}
|
|
352
|
+
return { editor: "unknown", source: "none" };
|
|
143
353
|
}
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
354
|
+
async function detectGitContext(startDir) {
|
|
355
|
+
const root = await findNearestAncestorWithEntry(startDir, ".git");
|
|
356
|
+
return {
|
|
357
|
+
hasRepository: root !== null,
|
|
358
|
+
repositoryRoot: root
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
async function detectMonorepoContext(startDir) {
|
|
362
|
+
let current = (0, import_node_path2.resolve)(startDir);
|
|
363
|
+
for (; ; ) {
|
|
364
|
+
if (await fileExists((0, import_node_path2.join)(current, "pnpm-workspace.yaml"))) {
|
|
365
|
+
return { isMonorepo: true, root: current, kind: "pnpm-workspace" };
|
|
156
366
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
ask();
|
|
367
|
+
if (await fileExists((0, import_node_path2.join)(current, "turbo.json"))) {
|
|
368
|
+
return { isMonorepo: true, root: current, kind: "turborepo" };
|
|
369
|
+
}
|
|
370
|
+
const packageJsonPath = (0, import_node_path2.join)(current, "package.json");
|
|
371
|
+
if (await fileExists(packageJsonPath)) {
|
|
372
|
+
try {
|
|
373
|
+
const packageJsonRaw = await (0, import_promises2.readFile)(packageJsonPath, "utf-8");
|
|
374
|
+
const parsed = JSON.parse(packageJsonRaw);
|
|
375
|
+
if (Array.isArray(parsed.workspaces) || isNpmWorkspaceObject(parsed.workspaces)) {
|
|
376
|
+
return { isMonorepo: true, root: current, kind: "npm-workspaces" };
|
|
168
377
|
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const parent = (0, import_node_path2.dirname)(current);
|
|
382
|
+
if (parent === current) {
|
|
383
|
+
return { isMonorepo: false, root: null, kind: "none" };
|
|
384
|
+
}
|
|
385
|
+
current = parent;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async function applyEditorWorkspacePreset(params) {
|
|
389
|
+
const { targetDir, editor } = params;
|
|
390
|
+
if (editor !== "vscode" && editor !== "cursor" && editor !== "windsurf") {
|
|
391
|
+
return { applied: false, filePath: null };
|
|
392
|
+
}
|
|
393
|
+
const vscodeDir = (0, import_node_path2.join)(targetDir, ".vscode");
|
|
394
|
+
const extensionsPath = (0, import_node_path2.join)(vscodeDir, "extensions.json");
|
|
395
|
+
await (0, import_promises2.mkdir)(vscodeDir, { recursive: true });
|
|
396
|
+
const recommendations = ["korajs.kora-devtools"];
|
|
397
|
+
const existing = await readJsonObject(extensionsPath);
|
|
398
|
+
const existingRecommendations = Array.isArray(existing?.recommendations) ? existing.recommendations.filter((item) => typeof item === "string") : [];
|
|
399
|
+
const mergedRecommendations = dedupeStrings([...existingRecommendations, ...recommendations]);
|
|
400
|
+
const next = {
|
|
401
|
+
recommendations: mergedRecommendations
|
|
402
|
+
};
|
|
403
|
+
await (0, import_promises2.writeFile)(extensionsPath, `${JSON.stringify(next, null, 2)}
|
|
404
|
+
`, "utf-8");
|
|
405
|
+
return { applied: true, filePath: extensionsPath };
|
|
406
|
+
}
|
|
407
|
+
function resolveMonorepoTargetDirectory(monorepoRoot, projectName) {
|
|
408
|
+
return (0, import_node_path2.join)(monorepoRoot, "packages", projectName);
|
|
409
|
+
}
|
|
410
|
+
function dedupeStrings(values) {
|
|
411
|
+
return Array.from(new Set(values));
|
|
412
|
+
}
|
|
413
|
+
async function readJsonObject(path) {
|
|
414
|
+
try {
|
|
415
|
+
const content = await (0, import_promises2.readFile)(path, "utf-8");
|
|
416
|
+
const parsed = JSON.parse(content);
|
|
417
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
418
|
+
return parsed;
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
} catch {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
function isNpmWorkspaceObject(value) {
|
|
426
|
+
if (typeof value !== "object" || value === null) return false;
|
|
427
|
+
const record = value;
|
|
428
|
+
return Array.isArray(record.packages);
|
|
429
|
+
}
|
|
430
|
+
async function fileExists(path) {
|
|
431
|
+
try {
|
|
432
|
+
await (0, import_promises2.access)(path);
|
|
433
|
+
return true;
|
|
434
|
+
} catch {
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async function findNearestAncestorWithEntry(startDir, entryName) {
|
|
439
|
+
let current = (0, import_node_path2.resolve)(startDir);
|
|
440
|
+
for (; ; ) {
|
|
441
|
+
const candidate = (0, import_node_path2.join)(current, entryName);
|
|
442
|
+
try {
|
|
443
|
+
await (0, import_promises2.stat)(candidate);
|
|
444
|
+
return current;
|
|
445
|
+
} catch {
|
|
446
|
+
}
|
|
447
|
+
const parent = (0, import_node_path2.dirname)(current);
|
|
448
|
+
if (parent === current) {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
current = parent;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/commands/create/options.ts
|
|
456
|
+
function determineTemplateFromSelections(input) {
|
|
457
|
+
const shouldSync = input.sync && input.db !== "none";
|
|
458
|
+
if (input.tailwind && shouldSync) return "react-tailwind-sync";
|
|
459
|
+
if (input.tailwind && !shouldSync) return "react-tailwind";
|
|
460
|
+
if (!input.tailwind && shouldSync) return "react-sync";
|
|
461
|
+
return "react-basic";
|
|
462
|
+
}
|
|
463
|
+
function isFrameworkValue(value) {
|
|
464
|
+
return value === "react" || value === "vue" || value === "svelte" || value === "solid";
|
|
465
|
+
}
|
|
466
|
+
function isAuthValue(value) {
|
|
467
|
+
return value === "none" || value === "email-password" || value === "oauth";
|
|
468
|
+
}
|
|
469
|
+
function isDatabaseValue(value) {
|
|
470
|
+
return value === "none" || value === "sqlite" || value === "postgres";
|
|
471
|
+
}
|
|
472
|
+
function isDatabaseProviderValue(value) {
|
|
473
|
+
return value === "none" || value === "local" || value === "supabase" || value === "neon" || value === "railway" || value === "vercel-postgres" || value === "custom";
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/commands/create/preferences-flow.ts
|
|
477
|
+
async function resolveCreatePreferencesFlow(params) {
|
|
478
|
+
const { flags, prompts, store } = params;
|
|
479
|
+
const stored = store.getCreatePreferences();
|
|
480
|
+
const base = getCreatePreferencesOrDefault(store);
|
|
481
|
+
const hasExplicitFlags = hasExplicitPreferenceFlags(flags);
|
|
482
|
+
const canOfferStored = !flags.useDefaults && !hasExplicitFlags && stored !== null && promptSupportsRichOptions();
|
|
483
|
+
let effective = { ...base };
|
|
484
|
+
let usedStoredPreferences = false;
|
|
485
|
+
if (flags.useDefaults) {
|
|
486
|
+
effective = getDefaultCreatePreferences();
|
|
487
|
+
} else if (canOfferStored && stored !== null) {
|
|
488
|
+
const reuseStored = await prompts.select("Welcome back! Choose setup mode:", [
|
|
489
|
+
{ label: formatStoredPreferenceLabel(stored), value: "reuse" },
|
|
490
|
+
{ label: "Customize", value: "customize" }
|
|
491
|
+
]);
|
|
492
|
+
if (reuseStored === "reuse") {
|
|
493
|
+
effective = { ...stored };
|
|
494
|
+
usedStoredPreferences = true;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (flags.framework !== void 0) {
|
|
498
|
+
if (!isFrameworkValue(flags.framework)) {
|
|
499
|
+
throw new Error(
|
|
500
|
+
`Invalid --framework value "${flags.framework}". Expected one of: react, vue, svelte, solid.`
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
effective.framework = flags.framework;
|
|
504
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
505
|
+
effective.framework = await prompts.select("UI framework:", [
|
|
506
|
+
{ label: "React", value: "react" },
|
|
507
|
+
{ label: "Vue (coming soon)", value: "vue", disabled: true },
|
|
508
|
+
{ label: "Svelte (coming soon)", value: "svelte", disabled: true },
|
|
509
|
+
{ label: "Solid (coming soon)", value: "solid", disabled: true }
|
|
510
|
+
]);
|
|
511
|
+
}
|
|
512
|
+
if (flags.auth !== void 0) {
|
|
513
|
+
if (!isAuthValue(flags.auth)) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
`Invalid --auth value "${flags.auth}". Expected one of: none, email-password, oauth.`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
effective.auth = flags.auth;
|
|
519
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
520
|
+
effective.auth = await prompts.select("Authentication:", [
|
|
521
|
+
{ label: "None", value: "none" },
|
|
522
|
+
{ label: "Email + Password (coming soon)", value: "email-password", disabled: true },
|
|
523
|
+
{ label: "OAuth (coming soon)", value: "oauth", disabled: true }
|
|
524
|
+
]);
|
|
525
|
+
}
|
|
526
|
+
if (flags.tailwind !== void 0) {
|
|
527
|
+
effective.tailwind = flags.tailwind;
|
|
528
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
529
|
+
effective.tailwind = await prompts.confirm("Use Tailwind CSS?", true);
|
|
530
|
+
}
|
|
531
|
+
if (flags.sync !== void 0) {
|
|
532
|
+
effective.sync = flags.sync;
|
|
533
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
534
|
+
effective.sync = await prompts.confirm("Enable multi-device sync?", true);
|
|
535
|
+
}
|
|
536
|
+
if (flags.db !== void 0) {
|
|
537
|
+
if (!isDatabaseValue(flags.db)) {
|
|
538
|
+
throw new Error(`Invalid --db value "${flags.db}". Expected one of: none, sqlite, postgres.`);
|
|
539
|
+
}
|
|
540
|
+
effective.db = flags.db;
|
|
541
|
+
} else if (!effective.sync) {
|
|
542
|
+
effective.db = "none";
|
|
543
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
544
|
+
effective.db = await prompts.select("Server-side database:", [
|
|
545
|
+
{ label: "SQLite (zero-config)", value: "sqlite" },
|
|
546
|
+
{ label: "PostgreSQL (production-scale)", value: "postgres" }
|
|
547
|
+
]);
|
|
548
|
+
}
|
|
549
|
+
if (effective.db !== "postgres") {
|
|
550
|
+
effective.dbProvider = "none";
|
|
551
|
+
} else if (flags.dbProvider !== void 0) {
|
|
552
|
+
if (!isDatabaseProviderValue(flags.dbProvider)) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`Invalid --db-provider value "${flags.dbProvider}". Expected one of: none, local, supabase, neon, railway, vercel-postgres, custom.`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
effective.dbProvider = flags.dbProvider;
|
|
558
|
+
} else if (!flags.useDefaults && !usedStoredPreferences) {
|
|
559
|
+
effective.dbProvider = await prompts.select("Database provider:", [
|
|
560
|
+
{ label: "Local Postgres", value: "local" },
|
|
561
|
+
{ label: "Supabase", value: "supabase" },
|
|
562
|
+
{ label: "Neon", value: "neon" },
|
|
563
|
+
{ label: "Railway", value: "railway" },
|
|
564
|
+
{ label: "Vercel Postgres", value: "vercel-postgres" },
|
|
565
|
+
{ label: "Custom connection string", value: "custom" }
|
|
566
|
+
]);
|
|
567
|
+
}
|
|
568
|
+
const template = determineTemplateFromSelections({
|
|
569
|
+
tailwind: effective.tailwind,
|
|
570
|
+
sync: effective.sync,
|
|
571
|
+
db: effective.db
|
|
172
572
|
});
|
|
573
|
+
return {
|
|
574
|
+
framework: effective.framework,
|
|
575
|
+
auth: effective.auth,
|
|
576
|
+
db: effective.db,
|
|
577
|
+
dbProvider: effective.dbProvider,
|
|
578
|
+
tailwind: effective.tailwind,
|
|
579
|
+
sync: effective.sync,
|
|
580
|
+
template,
|
|
581
|
+
usedStoredPreferences
|
|
582
|
+
};
|
|
173
583
|
}
|
|
174
|
-
function
|
|
175
|
-
return
|
|
176
|
-
|
|
177
|
-
|
|
584
|
+
function shouldSavePreferences(flags) {
|
|
585
|
+
return !flags.useDefaults;
|
|
586
|
+
}
|
|
587
|
+
function saveResolvedPreferences(store, resolution) {
|
|
588
|
+
store.saveCreatePreferences({
|
|
589
|
+
framework: resolution.framework,
|
|
590
|
+
tailwind: resolution.tailwind,
|
|
591
|
+
sync: resolution.sync,
|
|
592
|
+
db: resolution.db,
|
|
593
|
+
dbProvider: resolution.dbProvider,
|
|
594
|
+
auth: resolution.auth,
|
|
595
|
+
packageManager: resolution.packageManager
|
|
178
596
|
});
|
|
179
597
|
}
|
|
598
|
+
function hasExplicitPreferenceFlags(flags) {
|
|
599
|
+
return flags.framework !== void 0 || flags.auth !== void 0 || flags.db !== void 0 || flags.dbProvider !== void 0 || flags.tailwind !== void 0 || flags.sync !== void 0;
|
|
600
|
+
}
|
|
601
|
+
function promptSupportsRichOptions() {
|
|
602
|
+
return typeof process !== "undefined" && process.stdin.isTTY && process.stdout.isTTY;
|
|
603
|
+
}
|
|
604
|
+
function formatStoredPreferenceLabel(preferences) {
|
|
605
|
+
const syncLabel = preferences.sync ? `sync/${preferences.db}` : "local-only";
|
|
606
|
+
const styleLabel = preferences.tailwind ? "tailwind" : "css";
|
|
607
|
+
return `Use previous settings (${preferences.framework} + ${styleLabel} + ${syncLabel} + ${preferences.packageManager})`;
|
|
608
|
+
}
|
|
180
609
|
|
|
181
|
-
// src/commands/create/
|
|
610
|
+
// src/commands/create/project-name.ts
|
|
611
|
+
var import_validate_npm_package_name = __toESM(require("validate-npm-package-name"), 1);
|
|
612
|
+
function validateProjectName(name) {
|
|
613
|
+
const trimmedName = name.trim();
|
|
614
|
+
if (trimmedName.length === 0) {
|
|
615
|
+
return {
|
|
616
|
+
valid: false,
|
|
617
|
+
issues: ["Project name cannot be empty."]
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
const validation = (0, import_validate_npm_package_name.default)(trimmedName);
|
|
621
|
+
const issues = [...validation.errors ?? [], ...validation.warnings ?? []];
|
|
622
|
+
if (!validation.validForNewPackages && issues.length === 0) {
|
|
623
|
+
return {
|
|
624
|
+
valid: false,
|
|
625
|
+
issues: ["Project name is not a valid npm package name."]
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
return {
|
|
629
|
+
valid: validation.validForNewPackages,
|
|
630
|
+
issues
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/commands/create/sync-provider-preset.ts
|
|
635
|
+
var import_promises3 = require("fs/promises");
|
|
636
|
+
var import_node_path3 = require("path");
|
|
637
|
+
async function applySyncProviderPreset(options) {
|
|
638
|
+
if (!isSyncTemplate(options.template)) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (options.db !== "postgres") {
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const providerName = getProviderDisplayName(options.dbProvider);
|
|
645
|
+
const providerConnectionString = getProviderConnectionStringExample(options.dbProvider);
|
|
646
|
+
const serverPath = (0, import_node_path3.join)(options.targetDir, "server.ts");
|
|
647
|
+
const envPath = (0, import_node_path3.join)(options.targetDir, ".env.example");
|
|
648
|
+
const readmePath = (0, import_node_path3.join)(options.targetDir, "README.md");
|
|
649
|
+
const serverTemplate = [
|
|
650
|
+
"import { createPostgresServerStore, createProductionServer } from '@korajs/server'",
|
|
651
|
+
"",
|
|
652
|
+
`// PostgreSQL provider preset: ${providerName}`,
|
|
653
|
+
"// Ensure DATABASE_URL is set in your environment.",
|
|
654
|
+
"",
|
|
655
|
+
"async function start(): Promise<void> {",
|
|
656
|
+
" const connectionString = process.env.DATABASE_URL || ''",
|
|
657
|
+
" if (connectionString.length === 0) {",
|
|
658
|
+
" throw new Error('DATABASE_URL is required for PostgreSQL sync server store.')",
|
|
659
|
+
" }",
|
|
660
|
+
"",
|
|
661
|
+
" const store = await createPostgresServerStore({ connectionString })",
|
|
662
|
+
" const server = createProductionServer({",
|
|
663
|
+
" store,",
|
|
664
|
+
" port: Number(process.env.PORT) || 3001,",
|
|
665
|
+
" staticDir: './dist',",
|
|
666
|
+
" syncPath: '/kora-sync',",
|
|
667
|
+
" })",
|
|
668
|
+
"",
|
|
669
|
+
" const url = await server.start()",
|
|
670
|
+
" console.log(`Kora app running at ${url}`)",
|
|
671
|
+
"}",
|
|
672
|
+
"",
|
|
673
|
+
"void start()",
|
|
674
|
+
""
|
|
675
|
+
].join("\n");
|
|
676
|
+
const envTemplate = [
|
|
677
|
+
"# Kora Sync Server",
|
|
678
|
+
"# WebSocket URL for the sync server (used by the client)",
|
|
679
|
+
"VITE_SYNC_URL=ws://localhost:3001",
|
|
680
|
+
"",
|
|
681
|
+
"# Sync server port",
|
|
682
|
+
"PORT=3001",
|
|
683
|
+
"",
|
|
684
|
+
`# PostgreSQL connection string (${providerName})`,
|
|
685
|
+
`# Example: ${providerConnectionString}`,
|
|
686
|
+
"DATABASE_URL=",
|
|
687
|
+
""
|
|
688
|
+
].join("\n");
|
|
689
|
+
const existingReadme = await (0, import_promises3.readFile)(readmePath, "utf-8");
|
|
690
|
+
const trimmedReadme = existingReadme.trimEnd();
|
|
691
|
+
const readmeSuffix = [
|
|
692
|
+
"",
|
|
693
|
+
"## PostgreSQL Provider Preset",
|
|
694
|
+
"",
|
|
695
|
+
`Selected DB provider: ${options.dbProvider}`,
|
|
696
|
+
"",
|
|
697
|
+
"This scaffold uses `createPostgresServerStore` in `server.ts` and reads `DATABASE_URL` from the environment. See `.env.example` for provider-specific examples.",
|
|
698
|
+
""
|
|
699
|
+
].join("\n");
|
|
700
|
+
const readmeTemplate = `${trimmedReadme}${readmeSuffix}`;
|
|
701
|
+
await (0, import_promises3.writeFile)(serverPath, serverTemplate, "utf-8");
|
|
702
|
+
await (0, import_promises3.writeFile)(envPath, envTemplate, "utf-8");
|
|
703
|
+
await (0, import_promises3.writeFile)(readmePath, readmeTemplate, "utf-8");
|
|
704
|
+
}
|
|
705
|
+
function isSyncTemplate(template) {
|
|
706
|
+
return template === "react-sync" || template === "react-tailwind-sync";
|
|
707
|
+
}
|
|
708
|
+
function getProviderDisplayName(provider) {
|
|
709
|
+
switch (provider) {
|
|
710
|
+
case "supabase":
|
|
711
|
+
return "Supabase";
|
|
712
|
+
case "neon":
|
|
713
|
+
return "Neon";
|
|
714
|
+
case "railway":
|
|
715
|
+
return "Railway";
|
|
716
|
+
case "vercel-postgres":
|
|
717
|
+
return "Vercel Postgres";
|
|
718
|
+
case "custom":
|
|
719
|
+
return "Custom";
|
|
720
|
+
case "local":
|
|
721
|
+
return "Local Postgres";
|
|
722
|
+
case "none":
|
|
723
|
+
return "PostgreSQL";
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function getProviderConnectionStringExample(provider) {
|
|
727
|
+
switch (provider) {
|
|
728
|
+
case "supabase":
|
|
729
|
+
return "postgresql://postgres:<password>@db.<project-ref>.supabase.co:5432/postgres?sslmode=require";
|
|
730
|
+
case "neon":
|
|
731
|
+
return "postgresql://<user>:<password>@<branch>.<project>.neon.tech/neondb?sslmode=require";
|
|
732
|
+
case "railway":
|
|
733
|
+
return "postgresql://postgres:<password>@<host>.railway.app:<port>/railway?sslmode=require";
|
|
734
|
+
case "vercel-postgres":
|
|
735
|
+
return "postgresql://<user>:<password>@<host>.pooler.vercel-storage.com:5432/verceldb?sslmode=require";
|
|
736
|
+
case "custom":
|
|
737
|
+
return "postgresql://<user>:<password>@<host>:5432/<database>";
|
|
738
|
+
case "local":
|
|
739
|
+
return "postgresql://postgres:postgres@localhost:5432/kora";
|
|
740
|
+
case "none":
|
|
741
|
+
return "postgresql://postgres:postgres@localhost:5432/kora";
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// src/templates/composer.ts
|
|
182
746
|
var import_node_fs = require("fs");
|
|
183
|
-
var
|
|
184
|
-
var
|
|
747
|
+
var import_promises4 = require("fs/promises");
|
|
748
|
+
var import_node_path4 = require("path");
|
|
185
749
|
var import_node_url = require("url");
|
|
186
750
|
var import_meta = {};
|
|
187
751
|
function substituteVariables(template, context) {
|
|
@@ -191,43 +755,113 @@ function substituteVariables(template, context) {
|
|
|
191
755
|
});
|
|
192
756
|
}
|
|
193
757
|
function getTemplatePath(templateName) {
|
|
194
|
-
let dir = (0,
|
|
195
|
-
for (let i = 0; i <
|
|
196
|
-
|
|
197
|
-
|
|
758
|
+
let dir = (0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
759
|
+
for (let i = 0; i < 7; i++) {
|
|
760
|
+
const candidate = (0, import_node_path4.resolve)(dir, "templates", templateName);
|
|
761
|
+
if ((0, import_node_fs.existsSync)(candidate)) {
|
|
762
|
+
return candidate;
|
|
198
763
|
}
|
|
199
|
-
dir = (0,
|
|
764
|
+
dir = (0, import_node_path4.dirname)(dir);
|
|
200
765
|
}
|
|
201
|
-
const currentDir = (0,
|
|
202
|
-
return (0,
|
|
766
|
+
const currentDir = (0, import_node_path4.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
|
|
767
|
+
return (0, import_node_path4.resolve)(currentDir, "..", "..", "..", "templates", templateName);
|
|
203
768
|
}
|
|
204
|
-
|
|
205
|
-
const
|
|
769
|
+
function createCompatibilityLayerPlan(templateName) {
|
|
770
|
+
const baseLayer = { category: "base", name: "base", sourceTemplate: "react-basic" };
|
|
771
|
+
const uiLayer = { category: "ui", name: "react", sourceTemplate: null };
|
|
772
|
+
const authLayer = { category: "auth", name: "none", sourceTemplate: null };
|
|
773
|
+
switch (templateName) {
|
|
774
|
+
case "react-basic":
|
|
775
|
+
return {
|
|
776
|
+
compatibilityTarget: templateName,
|
|
777
|
+
layers: [
|
|
778
|
+
baseLayer,
|
|
779
|
+
uiLayer,
|
|
780
|
+
{ category: "style", name: "plain", sourceTemplate: null },
|
|
781
|
+
{ category: "sync", name: "disabled", sourceTemplate: null },
|
|
782
|
+
{ category: "db", name: "none", sourceTemplate: null },
|
|
783
|
+
authLayer
|
|
784
|
+
]
|
|
785
|
+
};
|
|
786
|
+
case "react-tailwind":
|
|
787
|
+
return {
|
|
788
|
+
compatibilityTarget: templateName,
|
|
789
|
+
layers: [
|
|
790
|
+
baseLayer,
|
|
791
|
+
uiLayer,
|
|
792
|
+
{ category: "style", name: "tailwind", sourceTemplate: "react-tailwind" },
|
|
793
|
+
{ category: "sync", name: "disabled", sourceTemplate: null },
|
|
794
|
+
{ category: "db", name: "none", sourceTemplate: null },
|
|
795
|
+
authLayer
|
|
796
|
+
]
|
|
797
|
+
};
|
|
798
|
+
case "react-sync":
|
|
799
|
+
return {
|
|
800
|
+
compatibilityTarget: templateName,
|
|
801
|
+
layers: [
|
|
802
|
+
baseLayer,
|
|
803
|
+
uiLayer,
|
|
804
|
+
{ category: "style", name: "plain", sourceTemplate: null },
|
|
805
|
+
{ category: "sync", name: "enabled", sourceTemplate: "react-sync" },
|
|
806
|
+
{ category: "db", name: "sqlite", sourceTemplate: null },
|
|
807
|
+
authLayer
|
|
808
|
+
]
|
|
809
|
+
};
|
|
810
|
+
case "react-tailwind-sync":
|
|
811
|
+
return {
|
|
812
|
+
compatibilityTarget: templateName,
|
|
813
|
+
layers: [
|
|
814
|
+
baseLayer,
|
|
815
|
+
uiLayer,
|
|
816
|
+
{ category: "style", name: "tailwind", sourceTemplate: "react-tailwind" },
|
|
817
|
+
{ category: "sync", name: "enabled", sourceTemplate: "react-tailwind-sync" },
|
|
818
|
+
{ category: "db", name: "sqlite", sourceTemplate: null },
|
|
819
|
+
authLayer
|
|
820
|
+
]
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async function composeTemplateLayers(plan, targetDir, context) {
|
|
206
825
|
const vars = {
|
|
207
826
|
projectName: context.projectName,
|
|
208
827
|
packageManager: context.packageManager,
|
|
209
|
-
koraVersion: context.koraVersion
|
|
828
|
+
koraVersion: context.koraVersion,
|
|
829
|
+
dbProvider: context.dbProvider ?? "none"
|
|
210
830
|
};
|
|
211
|
-
|
|
831
|
+
for (const layer of plan.layers) {
|
|
832
|
+
if (layer.sourceTemplate === null) {
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
const sourceDir = getTemplatePath(layer.sourceTemplate);
|
|
836
|
+
await copyDirectory(sourceDir, targetDir, vars);
|
|
837
|
+
}
|
|
212
838
|
}
|
|
213
839
|
async function copyDirectory(src, dest, vars) {
|
|
214
|
-
await (0,
|
|
215
|
-
const entries = await (0,
|
|
840
|
+
await (0, import_promises4.mkdir)(dest, { recursive: true });
|
|
841
|
+
const entries = await (0, import_promises4.readdir)(src);
|
|
216
842
|
for (const entry of entries) {
|
|
217
|
-
const srcPath = (0,
|
|
218
|
-
const srcStat = await (0,
|
|
843
|
+
const srcPath = (0, import_node_path4.join)(src, entry);
|
|
844
|
+
const srcStat = await (0, import_promises4.stat)(srcPath);
|
|
219
845
|
if (srcStat.isDirectory()) {
|
|
220
|
-
await copyDirectory(srcPath, (0,
|
|
221
|
-
|
|
222
|
-
|
|
846
|
+
await copyDirectory(srcPath, (0, import_node_path4.join)(dest, entry), vars);
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (entry.endsWith(".hbs")) {
|
|
850
|
+
const content = await (0, import_promises4.readFile)(srcPath, "utf-8");
|
|
223
851
|
const outputName = entry.slice(0, -4);
|
|
224
|
-
await (0,
|
|
225
|
-
|
|
226
|
-
await (0, import_promises2.copyFile)(srcPath, (0, import_node_path2.join)(dest, entry));
|
|
852
|
+
await (0, import_promises4.writeFile)((0, import_node_path4.join)(dest, outputName), substituteVariables(content, vars), "utf-8");
|
|
853
|
+
continue;
|
|
227
854
|
}
|
|
855
|
+
await (0, import_promises4.copyFile)(srcPath, (0, import_node_path4.join)(dest, entry));
|
|
228
856
|
}
|
|
229
857
|
}
|
|
230
858
|
|
|
859
|
+
// src/commands/create/template-engine.ts
|
|
860
|
+
async function scaffoldTemplate(templateName, targetDir, context) {
|
|
861
|
+
const plan = createCompatibilityLayerPlan(templateName);
|
|
862
|
+
await composeTemplateLayers(plan, targetDir, context);
|
|
863
|
+
}
|
|
864
|
+
|
|
231
865
|
// src/commands/create/create-command.ts
|
|
232
866
|
var import_meta2 = {};
|
|
233
867
|
var createCommand = (0, import_citty.defineCommand)({
|
|
@@ -249,6 +883,22 @@ var createCommand = (0, import_citty.defineCommand)({
|
|
|
249
883
|
type: "string",
|
|
250
884
|
description: "Package manager (pnpm, npm, yarn, bun)"
|
|
251
885
|
},
|
|
886
|
+
framework: {
|
|
887
|
+
type: "string",
|
|
888
|
+
description: "UI framework (react, vue, svelte, solid)"
|
|
889
|
+
},
|
|
890
|
+
db: {
|
|
891
|
+
type: "string",
|
|
892
|
+
description: "Database backend for sync templates (sqlite, postgres)"
|
|
893
|
+
},
|
|
894
|
+
"db-provider": {
|
|
895
|
+
type: "string",
|
|
896
|
+
description: "Database provider for postgres (local, supabase, neon, railway, vercel-postgres, custom)"
|
|
897
|
+
},
|
|
898
|
+
auth: {
|
|
899
|
+
type: "string",
|
|
900
|
+
description: "Authentication mode (none, email-password, oauth)"
|
|
901
|
+
},
|
|
252
902
|
"skip-install": {
|
|
253
903
|
type: "boolean",
|
|
254
904
|
description: "Skip installing dependencies",
|
|
@@ -271,69 +921,182 @@ var createCommand = (0, import_citty.defineCommand)({
|
|
|
271
921
|
},
|
|
272
922
|
async run({ args }) {
|
|
273
923
|
const logger = createLogger();
|
|
924
|
+
const prompts = createPromptClient();
|
|
925
|
+
const preferenceStore = new PreferenceStore();
|
|
274
926
|
logger.banner();
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
process.exitCode = 1;
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
let template;
|
|
283
|
-
if (args.template && isValidTemplate(args.template)) {
|
|
284
|
-
template = args.template;
|
|
285
|
-
} else if (useDefaults) {
|
|
286
|
-
template = "react-tailwind-sync";
|
|
287
|
-
} else if (args.tailwind !== void 0 || args.sync !== void 0) {
|
|
288
|
-
template = resolveTemplateFromFlags(args.tailwind, args.sync);
|
|
289
|
-
} else {
|
|
290
|
-
template = await promptSelect(
|
|
291
|
-
"Select a template:",
|
|
292
|
-
TEMPLATE_INFO.map((t) => ({ label: `${t.label} \u2014 ${t.description}`, value: t.name }))
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
let pm;
|
|
296
|
-
if (args.pm && isValidPackageManager(args.pm)) {
|
|
297
|
-
pm = args.pm;
|
|
298
|
-
} else if (useDefaults) {
|
|
299
|
-
pm = detectPackageManager();
|
|
300
|
-
} else {
|
|
301
|
-
const detected = detectPackageManager();
|
|
302
|
-
pm = await promptSelect(
|
|
303
|
-
"Package manager:",
|
|
304
|
-
PACKAGE_MANAGERS.map((p) => ({
|
|
305
|
-
label: p === detected ? `${p} (detected)` : p,
|
|
306
|
-
value: p
|
|
307
|
-
}))
|
|
308
|
-
);
|
|
309
|
-
}
|
|
310
|
-
const targetDir = (0, import_node_path3.resolve)(process.cwd(), projectName);
|
|
311
|
-
if (await directoryExists(targetDir)) {
|
|
312
|
-
throw new ProjectExistsError(projectName);
|
|
313
|
-
}
|
|
314
|
-
const koraVersion = resolveKoraVersion();
|
|
315
|
-
logger.step(`Creating ${projectName} with ${template} template...`);
|
|
316
|
-
await scaffoldTemplate(template, targetDir, {
|
|
317
|
-
projectName,
|
|
318
|
-
packageManager: pm,
|
|
319
|
-
koraVersion
|
|
320
|
-
});
|
|
321
|
-
logger.success("Project scaffolded");
|
|
322
|
-
if (!args["skip-install"]) {
|
|
323
|
-
logger.step("Installing dependencies...");
|
|
324
|
-
try {
|
|
325
|
-
(0, import_node_child_process2.execSync)(getInstallCommand(pm), { cwd: targetDir, stdio: "inherit" });
|
|
326
|
-
logger.success("Dependencies installed");
|
|
327
|
-
} catch {
|
|
328
|
-
logger.warn("Failed to install dependencies. Run install manually.");
|
|
927
|
+
try {
|
|
928
|
+
const useDefaults = args.yes === true;
|
|
929
|
+
if (!useDefaults) {
|
|
930
|
+
prompts.intro("Kora.js \u2014 Offline-first application framework");
|
|
329
931
|
}
|
|
932
|
+
const projectName = args.name || (useDefaults ? "my-kora-app" : await prompts.text("Project name", "my-kora-app"));
|
|
933
|
+
if (!projectName) {
|
|
934
|
+
logger.error("Project name is required");
|
|
935
|
+
process.exitCode = 1;
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const nameValidation = validateProjectName(projectName);
|
|
939
|
+
if (!nameValidation.valid) {
|
|
940
|
+
logger.error("Invalid project name.");
|
|
941
|
+
for (const issue of nameValidation.issues) {
|
|
942
|
+
logger.step(`- ${issue}`);
|
|
943
|
+
}
|
|
944
|
+
if (!useDefaults) {
|
|
945
|
+
prompts.outro("Project creation aborted.");
|
|
946
|
+
}
|
|
947
|
+
process.exitCode = 1;
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const preferenceFlags = {
|
|
951
|
+
framework: typeof args.framework === "string" ? args.framework : void 0,
|
|
952
|
+
auth: typeof args.auth === "string" ? args.auth : void 0,
|
|
953
|
+
db: typeof args.db === "string" ? args.db : void 0,
|
|
954
|
+
dbProvider: typeof args["db-provider"] === "string" ? args["db-provider"] : void 0,
|
|
955
|
+
tailwind: args.tailwind,
|
|
956
|
+
sync: args.sync,
|
|
957
|
+
useDefaults
|
|
958
|
+
};
|
|
959
|
+
const selection = await resolveCreatePreferencesFlow({
|
|
960
|
+
flags: preferenceFlags,
|
|
961
|
+
prompts,
|
|
962
|
+
store: preferenceStore
|
|
963
|
+
});
|
|
964
|
+
if (selection.framework !== "react") {
|
|
965
|
+
logger.error(`Framework "${selection.framework}" is not available yet. Use "react".`);
|
|
966
|
+
if (!useDefaults) {
|
|
967
|
+
prompts.outro("Project creation aborted.");
|
|
968
|
+
}
|
|
969
|
+
process.exitCode = 1;
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (selection.auth !== "none") {
|
|
973
|
+
logger.error(`Auth mode "${selection.auth}" is not available yet. Use "none".`);
|
|
974
|
+
if (!useDefaults) {
|
|
975
|
+
prompts.outro("Project creation aborted.");
|
|
976
|
+
}
|
|
977
|
+
process.exitCode = 1;
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
const template = args.template && isValidTemplate(args.template) ? args.template : selection.template;
|
|
981
|
+
let pm;
|
|
982
|
+
if (args.pm && isValidPackageManager(args.pm)) {
|
|
983
|
+
pm = args.pm;
|
|
984
|
+
} else if (useDefaults) {
|
|
985
|
+
pm = detectPackageManager();
|
|
986
|
+
} else if (selection.usedStoredPreferences) {
|
|
987
|
+
pm = preferenceStore.getCreatePreferences()?.packageManager ?? detectPackageManager();
|
|
988
|
+
} else {
|
|
989
|
+
const detected = detectPackageManager();
|
|
990
|
+
pm = await prompts.select(
|
|
991
|
+
"Package manager:",
|
|
992
|
+
PACKAGE_MANAGERS.map((p) => ({
|
|
993
|
+
label: p === detected ? `${p} (detected)` : p,
|
|
994
|
+
value: p
|
|
995
|
+
}))
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
const editorDetection = detectEditorFromEnvironment();
|
|
999
|
+
const gitContext = await detectGitContext(process.cwd());
|
|
1000
|
+
const monorepoContext = await detectMonorepoContext(process.cwd());
|
|
1001
|
+
let targetDir = (0, import_node_path5.resolve)(process.cwd(), projectName);
|
|
1002
|
+
if (!useDefaults && monorepoContext.isMonorepo && monorepoContext.root !== null) {
|
|
1003
|
+
const useWorkspaceTarget = await prompts.confirm(
|
|
1004
|
+
`Detected ${formatMonorepoKind(monorepoContext.kind)} at ${monorepoContext.root}. Create app under packages/${projectName}?`,
|
|
1005
|
+
true
|
|
1006
|
+
);
|
|
1007
|
+
if (useWorkspaceTarget) {
|
|
1008
|
+
targetDir = resolveMonorepoTargetDirectory(monorepoContext.root, projectName);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
if (await directoryExists(targetDir)) {
|
|
1012
|
+
throw new ProjectExistsError(projectName);
|
|
1013
|
+
}
|
|
1014
|
+
const koraVersion = resolveKoraVersion();
|
|
1015
|
+
logger.step(`Creating ${projectName} with ${template} template...`);
|
|
1016
|
+
await scaffoldTemplate(template, targetDir, {
|
|
1017
|
+
projectName,
|
|
1018
|
+
packageManager: pm,
|
|
1019
|
+
koraVersion,
|
|
1020
|
+
dbProvider: selection.dbProvider
|
|
1021
|
+
});
|
|
1022
|
+
await applySyncProviderPreset({
|
|
1023
|
+
targetDir,
|
|
1024
|
+
template,
|
|
1025
|
+
db: selection.db,
|
|
1026
|
+
dbProvider: selection.dbProvider
|
|
1027
|
+
});
|
|
1028
|
+
if (!useDefaults && editorDetection.editor !== "unknown") {
|
|
1029
|
+
const shouldApplyEditorPreset = await prompts.confirm(
|
|
1030
|
+
`Detected ${formatEditor(editorDetection.editor)}. Add workspace recommendations for ${formatEditor(editorDetection.editor)}?`,
|
|
1031
|
+
true
|
|
1032
|
+
);
|
|
1033
|
+
if (shouldApplyEditorPreset) {
|
|
1034
|
+
const appliedPreset = await applyEditorWorkspacePreset({
|
|
1035
|
+
targetDir,
|
|
1036
|
+
editor: editorDetection.editor
|
|
1037
|
+
});
|
|
1038
|
+
if (appliedPreset.applied) {
|
|
1039
|
+
logger.step(
|
|
1040
|
+
`Added editor recommendations at ${relativeToTarget(targetDir, appliedPreset.filePath)}`
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (selection.db === "postgres" && isSyncTemplate2(template)) {
|
|
1046
|
+
logger.info(
|
|
1047
|
+
`Applied PostgreSQL sync preset (${formatDbProviderForLog(selection.dbProvider)}). Update DATABASE_URL in .env.example before running the sync server.`
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
if (gitContext.hasRepository) {
|
|
1051
|
+
logger.step(
|
|
1052
|
+
`Detected existing git repository at ${gitContext.repositoryRoot}. Skipping git initialization.`
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
logger.success("Project scaffolded");
|
|
1056
|
+
if (shouldSavePreferences(preferenceFlags)) {
|
|
1057
|
+
saveResolvedPreferences(preferenceStore, {
|
|
1058
|
+
framework: selection.framework,
|
|
1059
|
+
auth: selection.auth,
|
|
1060
|
+
db: selection.db,
|
|
1061
|
+
dbProvider: selection.dbProvider,
|
|
1062
|
+
tailwind: selection.tailwind,
|
|
1063
|
+
sync: selection.sync,
|
|
1064
|
+
packageManager: pm
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
if (!args["skip-install"]) {
|
|
1068
|
+
logger.step("Installing dependencies...");
|
|
1069
|
+
try {
|
|
1070
|
+
(0, import_node_child_process2.execSync)(getInstallCommand(pm), { cwd: targetDir, stdio: "inherit" });
|
|
1071
|
+
logger.success("Dependencies installed");
|
|
1072
|
+
} catch {
|
|
1073
|
+
logger.warn("Failed to install dependencies. Run install manually.");
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
logger.blank();
|
|
1077
|
+
logger.info("Done! Next steps:");
|
|
1078
|
+
logger.blank();
|
|
1079
|
+
logger.step(` cd ${targetDir}`);
|
|
1080
|
+
logger.step(` ${getRunDevCommand(pm)}`);
|
|
1081
|
+
logger.blank();
|
|
1082
|
+
if (!useDefaults) {
|
|
1083
|
+
prompts.outro("Project ready. Happy building with Kora!");
|
|
1084
|
+
}
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
if (error instanceof PromptCancelledError) {
|
|
1087
|
+
process.exitCode = 1;
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (error instanceof Error && error.message.startsWith("Invalid --")) {
|
|
1091
|
+
logger.error(error.message);
|
|
1092
|
+
if (!args.yes) {
|
|
1093
|
+
prompts.outro("Project creation aborted.");
|
|
1094
|
+
}
|
|
1095
|
+
process.exitCode = 1;
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
throw error;
|
|
330
1099
|
}
|
|
331
|
-
logger.blank();
|
|
332
|
-
logger.info("Done! Next steps:");
|
|
333
|
-
logger.blank();
|
|
334
|
-
logger.step(` cd ${projectName}`);
|
|
335
|
-
logger.step(` ${getRunDevCommand(pm)}`);
|
|
336
|
-
logger.blank();
|
|
337
1100
|
}
|
|
338
1101
|
});
|
|
339
1102
|
function isValidTemplate(value) {
|
|
@@ -342,19 +1105,71 @@ function isValidTemplate(value) {
|
|
|
342
1105
|
function isValidPackageManager(value) {
|
|
343
1106
|
return PACKAGE_MANAGERS.includes(value);
|
|
344
1107
|
}
|
|
345
|
-
function
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
1108
|
+
function isSyncTemplate2(template) {
|
|
1109
|
+
return template === "react-sync" || template === "react-tailwind-sync";
|
|
1110
|
+
}
|
|
1111
|
+
function formatDbProviderForLog(dbProvider) {
|
|
1112
|
+
switch (dbProvider) {
|
|
1113
|
+
case "supabase":
|
|
1114
|
+
return "Supabase";
|
|
1115
|
+
case "neon":
|
|
1116
|
+
return "Neon";
|
|
1117
|
+
case "railway":
|
|
1118
|
+
return "Railway";
|
|
1119
|
+
case "vercel-postgres":
|
|
1120
|
+
return "Vercel Postgres";
|
|
1121
|
+
case "custom":
|
|
1122
|
+
return "Custom";
|
|
1123
|
+
case "local":
|
|
1124
|
+
return "Local Postgres";
|
|
1125
|
+
case "none":
|
|
1126
|
+
return "PostgreSQL";
|
|
1127
|
+
default:
|
|
1128
|
+
return dbProvider;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
function formatEditor(editor) {
|
|
1132
|
+
switch (editor) {
|
|
1133
|
+
case "vscode":
|
|
1134
|
+
return "VS Code";
|
|
1135
|
+
case "cursor":
|
|
1136
|
+
return "Cursor";
|
|
1137
|
+
case "windsurf":
|
|
1138
|
+
return "Windsurf";
|
|
1139
|
+
case "zed":
|
|
1140
|
+
return "Zed";
|
|
1141
|
+
default:
|
|
1142
|
+
return editor;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
function formatMonorepoKind(kind) {
|
|
1146
|
+
switch (kind) {
|
|
1147
|
+
case "pnpm-workspace":
|
|
1148
|
+
return "pnpm workspace";
|
|
1149
|
+
case "npm-workspaces":
|
|
1150
|
+
return "npm workspace";
|
|
1151
|
+
case "turborepo":
|
|
1152
|
+
return "Turborepo";
|
|
1153
|
+
default:
|
|
1154
|
+
return "monorepo";
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
function relativeToTarget(targetDir, filePath) {
|
|
1158
|
+
if (filePath === null) return ".";
|
|
1159
|
+
const normalizedTarget = targetDir.endsWith("/") ? targetDir : `${targetDir}/`;
|
|
1160
|
+
if (filePath.startsWith(normalizedTarget)) {
|
|
1161
|
+
return filePath.slice(normalizedTarget.length);
|
|
1162
|
+
}
|
|
1163
|
+
if (filePath === targetDir) {
|
|
1164
|
+
return ".";
|
|
1165
|
+
}
|
|
1166
|
+
return filePath;
|
|
352
1167
|
}
|
|
353
1168
|
function resolveKoraVersion() {
|
|
354
1169
|
try {
|
|
355
|
-
let dir = (0,
|
|
1170
|
+
let dir = (0, import_node_path5.dirname)((0, import_node_url2.fileURLToPath)(import_meta2.url));
|
|
356
1171
|
for (let i = 0; i < 5; i++) {
|
|
357
|
-
const pkgPath = (0,
|
|
1172
|
+
const pkgPath = (0, import_node_path5.resolve)(dir, "package.json");
|
|
358
1173
|
if ((0, import_node_fs2.existsSync)(pkgPath)) {
|
|
359
1174
|
const pkg = JSON.parse((0, import_node_fs2.readFileSync)(pkgPath, "utf-8"));
|
|
360
1175
|
if (pkg.name === "@korajs/cli") {
|
|
@@ -363,7 +1178,7 @@ function resolveKoraVersion() {
|
|
|
363
1178
|
return `^${parts[0]}.${parts[1]}.0`;
|
|
364
1179
|
}
|
|
365
1180
|
}
|
|
366
|
-
dir = (0,
|
|
1181
|
+
dir = (0, import_node_path5.dirname)(dir);
|
|
367
1182
|
}
|
|
368
1183
|
return "latest";
|
|
369
1184
|
} catch {
|