@cellajs/create-cella 0.2.2 → 0.3.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 +19 -8
- package/configs/default-config.ts.template +32 -8
- package/dist/index.js +351 -172
- package/dist/index.js.map +1 -1
- package/package.json +53 -24
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { existsSync as existsSync2 } from "fs";
|
|
5
5
|
import { basename as basename2, resolve as resolve3 } from "path";
|
|
6
6
|
import { input, select } from "@inquirer/prompts";
|
|
7
|
-
import
|
|
7
|
+
import pc6 from "picocolors";
|
|
8
8
|
|
|
9
9
|
// src/constants.ts
|
|
10
10
|
import pc from "picocolors";
|
|
@@ -12,7 +12,7 @@ import pc from "picocolors";
|
|
|
12
12
|
// package.json
|
|
13
13
|
var package_default = {
|
|
14
14
|
name: "@cellajs/create-cella",
|
|
15
|
-
version: "0.
|
|
15
|
+
version: "0.3.0",
|
|
16
16
|
private: false,
|
|
17
17
|
license: "MIT",
|
|
18
18
|
description: "Cella is a TypeScript template to create web apps with sync and offline capabilities.",
|
|
@@ -21,8 +21,7 @@ var package_default = {
|
|
|
21
21
|
},
|
|
22
22
|
repository: {
|
|
23
23
|
type: "git",
|
|
24
|
-
url: "https://github.com/cellajs/cella"
|
|
25
|
-
directory: "cli/create-cella"
|
|
24
|
+
url: "https://github.com/cellajs/create-cella"
|
|
26
25
|
},
|
|
27
26
|
homepage: "https://cellajs.com",
|
|
28
27
|
author: "CellaJS <info@cellajs.com>",
|
|
@@ -45,6 +44,7 @@ var package_default = {
|
|
|
45
44
|
},
|
|
46
45
|
scripts: {
|
|
47
46
|
start: "tsx ./src/create-cella-cli.ts",
|
|
47
|
+
try: "tsx ./src/create-cella-cli.ts ../cella-try-app",
|
|
48
48
|
clean: "rimraf ./dist",
|
|
49
49
|
build: "tsup",
|
|
50
50
|
"test-build": "pnpm run build && node index.js",
|
|
@@ -54,26 +54,58 @@ var package_default = {
|
|
|
54
54
|
lint: "biome check .",
|
|
55
55
|
"lint:fix": "biome check --write .",
|
|
56
56
|
test: "vitest run",
|
|
57
|
-
"test:release": "pnpm run build && vitest run
|
|
58
|
-
"test:watch": "vitest"
|
|
57
|
+
"test:release": "pnpm run build && vitest run --config vitest.release.config.ts",
|
|
58
|
+
"test:watch": "vitest",
|
|
59
|
+
prepare: "lefthook install"
|
|
59
60
|
},
|
|
60
61
|
dependencies: {
|
|
61
62
|
"@inquirer/prompts": "^8.5.2",
|
|
62
|
-
axios: "^1.17.0",
|
|
63
63
|
commander: "^15.0.0",
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
|
|
64
|
+
ignore: "^7.0.5",
|
|
65
|
+
"isomorphic-git": "^1.38.5",
|
|
66
|
+
nanotar: "^0.3.0",
|
|
67
|
+
ora: "^9.4.1",
|
|
67
68
|
picocolors: "^1.1.1",
|
|
68
69
|
"validate-npm-package-name": "^8.0.0"
|
|
69
70
|
},
|
|
70
71
|
devDependencies: {
|
|
71
|
-
"@
|
|
72
|
-
"@
|
|
72
|
+
"@biomejs/biome": "^2.5.1",
|
|
73
|
+
"@commitlint/cli": "^21.1.0",
|
|
74
|
+
"@commitlint/config-conventional": "^21.1.0",
|
|
75
|
+
"@evilmartians/lefthook": "^2.1.9",
|
|
76
|
+
"@types/node": "24.10.13",
|
|
73
77
|
"@types/validate-npm-package-name": "^4.0.2",
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
"@typescript/native-preview": "7.0.0-dev.20260628.1",
|
|
79
|
+
rimraf: "^6.1.3",
|
|
80
|
+
tsup: "^8.5.1",
|
|
81
|
+
tsx: "^4.22.4",
|
|
82
|
+
typescript: "^6.0.3",
|
|
83
|
+
vitest: "^4.1.9"
|
|
84
|
+
},
|
|
85
|
+
packageManager: "pnpm@11.8.0",
|
|
86
|
+
commitlint: {
|
|
87
|
+
extends: [
|
|
88
|
+
"@commitlint/config-conventional"
|
|
89
|
+
],
|
|
90
|
+
rules: {
|
|
91
|
+
"type-enum": [
|
|
92
|
+
2,
|
|
93
|
+
"always",
|
|
94
|
+
[
|
|
95
|
+
"feat",
|
|
96
|
+
"fix",
|
|
97
|
+
"perf",
|
|
98
|
+
"refactor",
|
|
99
|
+
"revert",
|
|
100
|
+
"docs",
|
|
101
|
+
"chore",
|
|
102
|
+
"build",
|
|
103
|
+
"ci",
|
|
104
|
+
"style",
|
|
105
|
+
"test"
|
|
106
|
+
]
|
|
107
|
+
]
|
|
108
|
+
}
|
|
77
109
|
}
|
|
78
110
|
};
|
|
79
111
|
|
|
@@ -84,15 +116,12 @@ var TEMPLATE_URL = "github:cellajs/cella";
|
|
|
84
116
|
var CELLA_REMOTE_URL = "git@github.com:cellajs/cella.git";
|
|
85
117
|
var DESCRIPTION = package_default.description;
|
|
86
118
|
var VERSION = package_default.version;
|
|
87
|
-
var AUTHOR = package_default.author;
|
|
88
119
|
var WEBSITE = package_default.homepage;
|
|
89
|
-
var GITHUB = package_default.repository.url;
|
|
90
120
|
function getHeaderLine(templateVersion) {
|
|
91
|
-
const leftText = `\u29C8 ${NAME} \xB7
|
|
92
|
-
const
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
const padding = Math.max(1, 60 - leftText.length - rightText.length);
|
|
121
|
+
const leftText = `\u29C8 ${NAME} \xB7 ${VERSION} \xB7 cella ${templateVersion}`;
|
|
122
|
+
const left = `${pc.cyan(`\u29C8 ${NAME}`)} ${pc.dim(`\xB7 ${VERSION} \xB7 cella ${templateVersion}`)}`;
|
|
123
|
+
const right = pc.cyan(WEBSITE);
|
|
124
|
+
const padding = Math.max(1, 60 - leftText.length - WEBSITE.length);
|
|
96
125
|
return `${left}${" ".repeat(padding)}${right}`;
|
|
97
126
|
}
|
|
98
127
|
var TO_REMOVE = ["./cli/create", "./info/QUICKSTART.md"];
|
|
@@ -103,10 +132,10 @@ var TO_COPY = {
|
|
|
103
132
|
};
|
|
104
133
|
var PLACEHOLDER_CONFIG = "configs/default-config.ts.template";
|
|
105
134
|
async function generateEnvFromExample(examplePath, replacements) {
|
|
106
|
-
const { readFile:
|
|
135
|
+
const { readFile: readFile4 } = await import("fs/promises");
|
|
107
136
|
let content;
|
|
108
137
|
try {
|
|
109
|
-
content = await
|
|
138
|
+
content = await readFile4(examplePath, "utf8");
|
|
110
139
|
} catch {
|
|
111
140
|
return null;
|
|
112
141
|
}
|
|
@@ -191,47 +220,35 @@ ${nameEntry}${body}
|
|
|
191
220
|
|
|
192
221
|
// src/create.ts
|
|
193
222
|
import { existsSync } from "fs";
|
|
194
|
-
import { cp, mkdir } from "fs/promises";
|
|
195
|
-
import { join, relative, resolve as resolve2 } from "path";
|
|
196
|
-
import {
|
|
223
|
+
import { cp, mkdir as mkdir2, rm } from "fs/promises";
|
|
224
|
+
import { dirname as dirname3, isAbsolute, join as join4, relative as relative2, resolve as resolve2 } from "path";
|
|
225
|
+
import { checkbox } from "@inquirer/prompts";
|
|
226
|
+
import pc5 from "picocolors";
|
|
197
227
|
|
|
198
228
|
// src/utils/git/command.ts
|
|
199
|
-
import
|
|
200
|
-
import
|
|
201
|
-
var
|
|
202
|
-
async function runGitCommand(args, repoPath, options2 = {}) {
|
|
203
|
-
const gitArgs = repoPath ? ["-C", repoPath, ...args] : args;
|
|
204
|
-
const env = {
|
|
205
|
-
...process.env,
|
|
206
|
-
...options2.skipEditor ? { GIT_EDITOR: "true" } : {}
|
|
207
|
-
};
|
|
208
|
-
const maxBuffer = options2.maxBuffer ?? 10 * 1024 * 1024;
|
|
209
|
-
const { stdout } = await execFileAsync("git", gitArgs, { env, maxBuffer });
|
|
210
|
-
return stdout.trim();
|
|
211
|
-
}
|
|
229
|
+
import fs from "fs";
|
|
230
|
+
import git from "isomorphic-git";
|
|
231
|
+
var INITIAL_AUTHOR = { name: "cella", email: "info@cellajs.com" };
|
|
212
232
|
async function gitInit(repoPath) {
|
|
213
|
-
|
|
233
|
+
await git.init({ fs, dir: repoPath, defaultBranch: "main" });
|
|
214
234
|
}
|
|
215
235
|
async function gitAddAll(repoPath) {
|
|
216
|
-
|
|
236
|
+
await git.add({ fs, dir: repoPath, filepath: "." });
|
|
217
237
|
}
|
|
218
238
|
async function gitCommit(repoPath, message) {
|
|
219
|
-
return
|
|
220
|
-
}
|
|
221
|
-
async function gitBranch(repoPath, branchName) {
|
|
222
|
-
return runGitCommand(["branch", branchName], repoPath);
|
|
223
|
-
}
|
|
224
|
-
async function gitCheckout(repoPath, branchName) {
|
|
225
|
-
return runGitCommand(["checkout", branchName], repoPath);
|
|
239
|
+
return git.commit({ fs, dir: repoPath, message, author: INITIAL_AUTHOR });
|
|
226
240
|
}
|
|
227
241
|
async function gitRemoteGetUrl(repoPath, remoteName) {
|
|
228
|
-
|
|
242
|
+
const remotes = await git.listRemotes({ fs, dir: repoPath });
|
|
243
|
+
const match = remotes.find((r) => r.remote === remoteName);
|
|
244
|
+
if (!match) throw new Error(`No such remote '${remoteName}'`);
|
|
245
|
+
return match.url;
|
|
229
246
|
}
|
|
230
247
|
async function gitRemoteAdd(repoPath, remoteName, remoteUrl) {
|
|
231
|
-
|
|
248
|
+
await git.addRemote({ fs, dir: repoPath, remote: remoteName, url: remoteUrl });
|
|
232
249
|
}
|
|
233
250
|
async function gitRemoteRemove(repoPath, remoteName) {
|
|
234
|
-
|
|
251
|
+
await git.deleteRemote({ fs, dir: repoPath, remote: remoteName });
|
|
235
252
|
}
|
|
236
253
|
|
|
237
254
|
// src/add-remote.ts
|
|
@@ -290,7 +307,6 @@ function parsePortOffset(value) {
|
|
|
290
307
|
return parsed;
|
|
291
308
|
}
|
|
292
309
|
var directory = null;
|
|
293
|
-
var newBranchName = null;
|
|
294
310
|
var packageManager = "pnpm";
|
|
295
311
|
var command = new Command(NAME).version(VERSION, "-v, --version", `output the current version of ${NAME}`).argument("[directory]", "the directory name for the new project").usage("[directory] [options]").helpOption("-h, --help", "display this help message").option("--template <path>", "use a custom template (local path or github:user/repo)").option("--port-offset <number>", "set the port offset (0-490 in steps of 10)", parsePortOffset).option("--admin-email <email>", "set the admin email for the initial seed user").action((name) => {
|
|
296
312
|
const trimmedName = typeof name === "string" ? name.trim() : name;
|
|
@@ -308,7 +324,6 @@ function runCli() {
|
|
|
308
324
|
options,
|
|
309
325
|
args: command.args,
|
|
310
326
|
directory,
|
|
311
|
-
newBranchName,
|
|
312
327
|
packageManager
|
|
313
328
|
};
|
|
314
329
|
}
|
|
@@ -332,29 +347,33 @@ function showWelcome(templateVersion) {
|
|
|
332
347
|
console.info(getHeaderLine(templateVersion));
|
|
333
348
|
console.info(DIVIDER);
|
|
334
349
|
}
|
|
350
|
+
function confirmChoice(label, value) {
|
|
351
|
+
console.info(`${pc2.green("\u2713")} ${label} \xB7 ${value}`);
|
|
352
|
+
console.info();
|
|
353
|
+
}
|
|
335
354
|
function showSuccess(projectName, _targetFolder, relativePath, needsCd, packageManager2) {
|
|
355
|
+
console.info();
|
|
356
|
+
console.info(pc2.bold("Next steps"));
|
|
336
357
|
console.info(DIVIDER);
|
|
337
358
|
console.info();
|
|
338
359
|
if (needsCd) {
|
|
339
|
-
console.info(`${pc2.
|
|
340
|
-
console.info();
|
|
360
|
+
console.info(`${pc2.dim("$")} cd ${pc2.cyan(relativePath)}`);
|
|
341
361
|
}
|
|
342
|
-
console.info(`${pc2.
|
|
343
|
-
console.info();
|
|
344
|
-
console.info(pc2.gray("or, for full setup:"));
|
|
345
|
-
console.info();
|
|
362
|
+
console.info(`${pc2.dim("$")} ${pc2.cyan(`${packageManager2} install`)}`);
|
|
363
|
+
console.info(`${pc2.dim("$")} ${pc2.cyan(`${packageManager2} generate`)}`);
|
|
346
364
|
console.info(
|
|
347
|
-
`${pc2.
|
|
365
|
+
`${pc2.dim("$")} ${pc2.cyan(`${packageManager2} docker`)} ${pc2.dim("&&")} ${pc2.cyan(`${packageManager2} seed`)} ${pc2.dim("&&")} ${pc2.cyan(`${packageManager2} dev`)}`
|
|
348
366
|
);
|
|
349
367
|
console.info();
|
|
350
|
-
|
|
368
|
+
const readmePath = relativePath ? `${relativePath}/README.md` : "README.md";
|
|
369
|
+
console.info(`${pc2.gray("To get started building, please read:")} ${pc2.cyan(readmePath)}`);
|
|
351
370
|
console.info();
|
|
352
|
-
console.info(
|
|
371
|
+
console.info(`\u{1F973} enjoy building ${pc2.green(projectName)} with cella!`);
|
|
353
372
|
console.info();
|
|
354
373
|
}
|
|
355
374
|
|
|
356
375
|
// src/utils/clean-template.ts
|
|
357
|
-
import
|
|
376
|
+
import fs2 from "fs/promises";
|
|
358
377
|
import path from "path";
|
|
359
378
|
import { fileURLToPath } from "url";
|
|
360
379
|
import pc3 from "picocolors";
|
|
@@ -364,7 +383,7 @@ async function resolvePlaceholderConfigPath() {
|
|
|
364
383
|
const candidates = [path.resolve(here, "../..", PLACEHOLDER_CONFIG), path.resolve(here, "..", PLACEHOLDER_CONFIG)];
|
|
365
384
|
for (const candidate of candidates) {
|
|
366
385
|
try {
|
|
367
|
-
await
|
|
386
|
+
await fs2.access(candidate);
|
|
368
387
|
return candidate;
|
|
369
388
|
} catch {
|
|
370
389
|
}
|
|
@@ -396,12 +415,12 @@ async function cleanTemplate({
|
|
|
396
415
|
backendReplacements
|
|
397
416
|
);
|
|
398
417
|
if (backendEnv) {
|
|
399
|
-
await
|
|
418
|
+
await fs2.writeFile(path.resolve(targetFolder, "backend/.env"), backendEnv, "utf8");
|
|
400
419
|
}
|
|
401
420
|
const envConfigs = generateEnvConfigs(projectName, displayName, portOffset);
|
|
402
421
|
await Promise.all(
|
|
403
422
|
Object.entries(envConfigs).map(
|
|
404
|
-
([filePath, content]) =>
|
|
423
|
+
([filePath, content]) => fs2.writeFile(path.resolve(targetFolder, filePath), content, "utf8")
|
|
405
424
|
)
|
|
406
425
|
);
|
|
407
426
|
await Promise.all(
|
|
@@ -424,27 +443,27 @@ async function cleanTemplate({
|
|
|
424
443
|
});
|
|
425
444
|
}
|
|
426
445
|
async function removeFolderContents(folderPath) {
|
|
427
|
-
const files = await
|
|
446
|
+
const files = await fs2.readdir(folderPath);
|
|
428
447
|
await Promise.all(
|
|
429
448
|
files.map(async (file) => {
|
|
430
449
|
const filePath = path.join(folderPath, file);
|
|
431
|
-
const stat = await
|
|
450
|
+
const stat = await fs2.lstat(filePath);
|
|
432
451
|
if (stat.isDirectory()) {
|
|
433
|
-
await
|
|
452
|
+
await fs2.rm(filePath, { recursive: true, force: true });
|
|
434
453
|
} else {
|
|
435
|
-
await
|
|
454
|
+
await fs2.rm(filePath);
|
|
436
455
|
}
|
|
437
456
|
})
|
|
438
457
|
);
|
|
439
458
|
}
|
|
440
459
|
async function removeFileOrFolder(pathToRemove) {
|
|
441
|
-
await
|
|
460
|
+
await fs2.rm(pathToRemove, { recursive: true, force: true });
|
|
442
461
|
}
|
|
443
462
|
async function copyFile(src, dest) {
|
|
444
463
|
try {
|
|
445
|
-
await
|
|
446
|
-
await
|
|
447
|
-
await
|
|
464
|
+
await fs2.access(src);
|
|
465
|
+
await fs2.mkdir(path.dirname(dest), { recursive: true });
|
|
466
|
+
await fs2.copyFile(src, dest);
|
|
448
467
|
} catch (err) {
|
|
449
468
|
if (err.code === "ENOENT") {
|
|
450
469
|
console.info(`
|
|
@@ -459,10 +478,10 @@ async function applyPlaceholderConfig(targetFolder, projectName) {
|
|
|
459
478
|
const src = await resolvePlaceholderConfigPath();
|
|
460
479
|
const dest = path.resolve(targetFolder, "./shared/config/config.default.ts");
|
|
461
480
|
try {
|
|
462
|
-
let content = await
|
|
481
|
+
let content = await fs2.readFile(src, "utf8");
|
|
463
482
|
content = content.replaceAll("__project_name__", displayName);
|
|
464
483
|
content = content.replaceAll("__project_slug__", projectName);
|
|
465
|
-
await
|
|
484
|
+
await fs2.writeFile(dest, content, "utf8");
|
|
466
485
|
} catch (err) {
|
|
467
486
|
if (err.code === "ENOENT") {
|
|
468
487
|
console.info(`
|
|
@@ -473,10 +492,75 @@ ${warningMark} Placeholder config "${src}" not found > Skip`);
|
|
|
473
492
|
}
|
|
474
493
|
}
|
|
475
494
|
|
|
495
|
+
// src/utils/download-template.ts
|
|
496
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
497
|
+
import { dirname, join } from "path";
|
|
498
|
+
import { parseTarGzip } from "nanotar";
|
|
499
|
+
function parseGithubRepo(template) {
|
|
500
|
+
const [owner, repo] = template.replace(/^github:/, "").split("/");
|
|
501
|
+
if (!owner || !repo) {
|
|
502
|
+
throw new Error(`Invalid template '${template}'. Expected 'github:owner/repo'.`);
|
|
503
|
+
}
|
|
504
|
+
return { owner, repo };
|
|
505
|
+
}
|
|
506
|
+
async function downloadGithubTemplate(template, ref, targetFolder) {
|
|
507
|
+
const { owner, repo } = parseGithubRepo(template);
|
|
508
|
+
const url = `https://codeload.github.com/${owner}/${repo}/tar.gz/${ref}`;
|
|
509
|
+
const response = await fetch(url);
|
|
510
|
+
if (!response.ok) {
|
|
511
|
+
throw new Error(`Failed to download template ${owner}/${repo}@${ref}: ${response.status} ${response.statusText}`);
|
|
512
|
+
}
|
|
513
|
+
const archive = new Uint8Array(await response.arrayBuffer());
|
|
514
|
+
const entries = await parseTarGzip(archive);
|
|
515
|
+
for (const entry of entries) {
|
|
516
|
+
if (entry.type !== "file" || !entry.data) continue;
|
|
517
|
+
const relativePath = entry.name.replace(/^[^/]+\//, "");
|
|
518
|
+
if (!relativePath) continue;
|
|
519
|
+
const dest = join(targetFolder, relativePath);
|
|
520
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
521
|
+
const mode = entry.attrs?.mode ? Number.parseInt(entry.attrs.mode, 8) : void 0;
|
|
522
|
+
await writeFile(dest, entry.data, mode ? { mode } : void 0);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/utils/list-template-files.ts
|
|
527
|
+
import { readdir, readFile } from "fs/promises";
|
|
528
|
+
import { join as join2 } from "path";
|
|
529
|
+
import ignore from "ignore";
|
|
530
|
+
async function listTemplateFiles(root) {
|
|
531
|
+
const ig = ignore().add([".git", "node_modules"]);
|
|
532
|
+
try {
|
|
533
|
+
ig.add(await readFile(join2(root, ".gitignore"), "utf8"));
|
|
534
|
+
} catch {
|
|
535
|
+
}
|
|
536
|
+
const files = [];
|
|
537
|
+
async function walk(relDir) {
|
|
538
|
+
const entries = await readdir(relDir ? join2(root, relDir) : root, { withFileTypes: true });
|
|
539
|
+
for (const entry of entries) {
|
|
540
|
+
const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
541
|
+
if (ig.ignores(entry.isDirectory() ? `${rel}/` : rel)) continue;
|
|
542
|
+
if (entry.isDirectory()) await walk(rel);
|
|
543
|
+
else if (entry.isFile()) files.push(rel);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
await walk("");
|
|
547
|
+
return files;
|
|
548
|
+
}
|
|
549
|
+
|
|
476
550
|
// src/utils/progress.ts
|
|
477
551
|
import ora from "ora";
|
|
478
552
|
import pc4 from "picocolors";
|
|
479
553
|
var activeSpinner = null;
|
|
554
|
+
function pauseSpinner() {
|
|
555
|
+
if (activeSpinner) {
|
|
556
|
+
activeSpinner.stop();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function resumeSpinner() {
|
|
560
|
+
if (activeSpinner) {
|
|
561
|
+
activeSpinner.start();
|
|
562
|
+
}
|
|
563
|
+
}
|
|
480
564
|
function createProgress(title, silent = false) {
|
|
481
565
|
const completedSteps = [];
|
|
482
566
|
const spinner = ora({
|
|
@@ -515,48 +599,48 @@ function createProgress(title, silent = false) {
|
|
|
515
599
|
spinner.stop();
|
|
516
600
|
activeSpinner = null;
|
|
517
601
|
log(`${pc4.red("\u2717")} ${pc4.red(message)}`);
|
|
518
|
-
},
|
|
519
|
-
wrap: async (fn) => {
|
|
520
|
-
try {
|
|
521
|
-
return await fn();
|
|
522
|
-
} catch (error) {
|
|
523
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
524
|
-
spinner.stop();
|
|
525
|
-
activeSpinner = null;
|
|
526
|
-
log(pc4.cyan(`
|
|
527
|
-
${title}`));
|
|
528
|
-
for (const step of completedSteps) {
|
|
529
|
-
log(pc4.gray(` \u251C\u2500 ${step}`));
|
|
530
|
-
}
|
|
531
|
-
log(pc4.red(` \u2514\u2500 \u2717 ${errorMessage}`));
|
|
532
|
-
throw error;
|
|
533
|
-
}
|
|
534
602
|
}
|
|
535
603
|
};
|
|
536
604
|
}
|
|
537
605
|
|
|
538
|
-
// src/utils/
|
|
539
|
-
import
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
|
|
606
|
+
// src/utils/scan-optional-modules.ts
|
|
607
|
+
import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
|
|
608
|
+
import { dirname as dirname2, join as join3, relative } from "path";
|
|
609
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".turbo", "coverage"]);
|
|
610
|
+
async function scanOptionalModules(targetFolder) {
|
|
611
|
+
const files = [];
|
|
612
|
+
await collectModuleFiles(targetFolder, files);
|
|
613
|
+
const modules = [];
|
|
614
|
+
for (const file of files) {
|
|
615
|
+
const src = await readFile2(file, "utf8");
|
|
616
|
+
if (!/optional:\s*true/.test(src)) continue;
|
|
617
|
+
const name = src.match(/name:\s*['"]([^'"]+)['"]/)?.[1];
|
|
618
|
+
const description = src.match(/description:\s*['"]([^'"]+)['"]/)?.[1] ?? "";
|
|
619
|
+
if (!name) continue;
|
|
620
|
+
modules.push({ name, description, folder: relative(targetFolder, dirname2(file)) });
|
|
621
|
+
}
|
|
622
|
+
return modules.sort((a, b) => a.name.localeCompare(b.name));
|
|
623
|
+
}
|
|
624
|
+
function optionalModuleFolders({ name, folder }) {
|
|
625
|
+
return [
|
|
626
|
+
folder,
|
|
627
|
+
`frontend/src/routes/_public/_${name}`,
|
|
628
|
+
`frontend/src/routes/_public/${name}`,
|
|
629
|
+
`frontend/src/routes/_app/_${name}`,
|
|
630
|
+
`frontend/src/routes/_app/${name}`,
|
|
631
|
+
`frontend/public/static/${name}`
|
|
632
|
+
];
|
|
633
|
+
}
|
|
634
|
+
async function collectModuleFiles(dir, out) {
|
|
635
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
636
|
+
for (const entry of entries) {
|
|
637
|
+
if (entry.isDirectory()) {
|
|
638
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
639
|
+
await collectModuleFiles(join3(dir, entry.name), out);
|
|
640
|
+
} else if (entry.name.endsWith("-module.ts")) {
|
|
641
|
+
out.push(join3(dir, entry.name));
|
|
642
|
+
}
|
|
549
643
|
}
|
|
550
|
-
}
|
|
551
|
-
async function install(packageManager2) {
|
|
552
|
-
return runPackageManagerCommand(packageManager2, ["install"], {
|
|
553
|
-
NODE_ENV: "development"
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
async function generate(packageManager2) {
|
|
557
|
-
return runPackageManagerCommand(packageManager2, ["generate"], {
|
|
558
|
-
NODE_ENV: "development"
|
|
559
|
-
});
|
|
560
644
|
}
|
|
561
645
|
|
|
562
646
|
// src/create.ts
|
|
@@ -569,9 +653,9 @@ function shouldSkipStep(name) {
|
|
|
569
653
|
async function create({
|
|
570
654
|
projectName,
|
|
571
655
|
targetFolder,
|
|
572
|
-
newBranchName: newBranchName2,
|
|
573
656
|
packageManager: packageManager2,
|
|
574
657
|
templateUrl,
|
|
658
|
+
templateRef,
|
|
575
659
|
portOffset,
|
|
576
660
|
adminEmail,
|
|
577
661
|
silent = false
|
|
@@ -582,83 +666,100 @@ async function create({
|
|
|
582
666
|
const progress = createProgress("creating project", silent);
|
|
583
667
|
try {
|
|
584
668
|
progress.step("creating project folder");
|
|
585
|
-
await
|
|
669
|
+
await mkdir2(targetFolder, { recursive: true });
|
|
586
670
|
process.chdir(targetFolder);
|
|
587
671
|
if (isLocalTemplate) {
|
|
588
672
|
progress.step("copying local template");
|
|
589
673
|
const sourcePath = resolve2(originalCwd, templateUrl);
|
|
590
|
-
|
|
674
|
+
const rel = relative2(sourcePath, targetFolder);
|
|
675
|
+
const targetInsideTemplate = rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
676
|
+
if (targetInsideTemplate) {
|
|
591
677
|
throw new Error(
|
|
592
678
|
`Cannot create project inside the template directory.
|
|
593
679
|
Run from outside: cd ~ && pnpm create @cellajs/cella ${projectName} --template ${templateUrl}`
|
|
594
680
|
);
|
|
595
681
|
}
|
|
596
|
-
await
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
682
|
+
const tracked = await listTemplateFiles(sourcePath);
|
|
683
|
+
for (const rel2 of tracked) {
|
|
684
|
+
const src = join4(sourcePath, rel2);
|
|
685
|
+
if (!existsSync(src)) continue;
|
|
686
|
+
const dest = join4(targetFolder, rel2);
|
|
687
|
+
await mkdir2(dirname3(dest), { recursive: true });
|
|
688
|
+
await cp(src, dest);
|
|
689
|
+
}
|
|
600
690
|
} else {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
dir: targetFolder,
|
|
605
|
-
force: true,
|
|
606
|
-
provider: "github"
|
|
607
|
-
});
|
|
691
|
+
const ref = templateRef ?? "main";
|
|
692
|
+
progress.step(`downloading cella template${templateRef ? ` (${templateRef})` : ""}`);
|
|
693
|
+
await downloadGithubTemplate(template, ref, targetFolder);
|
|
608
694
|
}
|
|
695
|
+
if (!silent) await promptOptionalModules(targetFolder);
|
|
609
696
|
progress.step("cleaning template");
|
|
610
697
|
const displayName = projectName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
611
698
|
await cleanTemplate({ targetFolder, projectName, displayName, portOffset, adminEmail });
|
|
612
|
-
|
|
613
|
-
progress.step("installing dependencies");
|
|
614
|
-
await install(packageManager2);
|
|
615
|
-
}
|
|
616
|
-
if (!shouldSkipStep("generate")) {
|
|
617
|
-
progress.step("generating migrations");
|
|
618
|
-
await generate(packageManager2);
|
|
619
|
-
}
|
|
620
|
-
const gitFolderPath = join(targetFolder, ".git");
|
|
699
|
+
const gitFolderPath = join4(targetFolder, ".git");
|
|
621
700
|
if (!shouldSkipStep("git") && !existsSync(gitFolderPath)) {
|
|
622
701
|
progress.step("initializing git");
|
|
623
702
|
await gitInit(targetFolder);
|
|
624
703
|
await gitAddAll(targetFolder);
|
|
625
704
|
await gitCommit(targetFolder, "Initial commit");
|
|
626
|
-
if (newBranchName2) {
|
|
627
|
-
progress.step(`creating branch '${newBranchName2}'`);
|
|
628
|
-
await gitBranch(targetFolder, newBranchName2);
|
|
629
|
-
await gitCheckout(targetFolder, newBranchName2);
|
|
630
|
-
}
|
|
631
705
|
}
|
|
632
706
|
if (!shouldSkipStep("remote")) {
|
|
633
707
|
progress.step("adding upstream remote");
|
|
634
708
|
await addRemote({ targetFolder, silent: true });
|
|
635
709
|
}
|
|
636
|
-
progress.done(`created ${projectName}`);
|
|
710
|
+
progress.done(pc5.bold(`created ${projectName}`));
|
|
637
711
|
} catch (error) {
|
|
638
712
|
progress.fail(error instanceof Error ? error.message : String(error));
|
|
639
|
-
|
|
713
|
+
throw error;
|
|
640
714
|
}
|
|
641
715
|
const needsCd = originalCwd !== targetFolder;
|
|
642
|
-
const relativePath =
|
|
716
|
+
const relativePath = relative2(originalCwd, targetFolder);
|
|
643
717
|
showSuccess(projectName, targetFolder, relativePath, needsCd, packageManager2);
|
|
644
718
|
}
|
|
719
|
+
async function promptOptionalModules(targetFolder) {
|
|
720
|
+
const modules = await scanOptionalModules(targetFolder);
|
|
721
|
+
if (modules.length === 0) return;
|
|
722
|
+
if (!process.stdin.isTTY) return;
|
|
723
|
+
pauseSpinner();
|
|
724
|
+
console.info();
|
|
725
|
+
const keep = await checkbox(
|
|
726
|
+
{
|
|
727
|
+
message: "Optional modules to include",
|
|
728
|
+
// Leading space on the radio icons adds a gap after the cursor (inquirer renders
|
|
729
|
+
// `${cursor}${checkbox} ${name}` with no space between cursor and checkbox).
|
|
730
|
+
theme: { icon: { checked: ` ${pc5.green("\u25C9")}`, unchecked: " \u25EF" } },
|
|
731
|
+
choices: modules.map((m) => ({
|
|
732
|
+
name: `${m.name} ${pc5.dim(`\xB7 ${m.description}`)}`,
|
|
733
|
+
value: m.name,
|
|
734
|
+
checked: true
|
|
735
|
+
}))
|
|
736
|
+
},
|
|
737
|
+
// Clear the answered prompt so we can print a compact, non-bold summary that lists
|
|
738
|
+
// only the kept module names (not their descriptions).
|
|
739
|
+
{ clearPromptOnDone: true }
|
|
740
|
+
);
|
|
741
|
+
confirmChoice("Optional modules", keep.length ? keep.join(", ") : pc5.dim("none"));
|
|
742
|
+
resumeSpinner();
|
|
743
|
+
const folders = modules.filter((m) => !keep.includes(m.name)).flatMap(optionalModuleFolders);
|
|
744
|
+
await Promise.all(folders.map((f) => rm(join4(targetFolder, f), { recursive: true, force: true })));
|
|
745
|
+
}
|
|
645
746
|
|
|
646
747
|
// src/utils/detect-used-ports.ts
|
|
647
|
-
import { readdir, readFile } from "fs/promises";
|
|
648
|
-
import { dirname, join as
|
|
748
|
+
import { readdir as readdir3, readFile as readFile3 } from "fs/promises";
|
|
749
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
649
750
|
async function detectUsedPorts(targetFolder) {
|
|
650
|
-
const parentDir =
|
|
751
|
+
const parentDir = dirname4(targetFolder);
|
|
651
752
|
const used = [];
|
|
652
753
|
let siblings;
|
|
653
754
|
try {
|
|
654
|
-
siblings = await
|
|
755
|
+
siblings = await readdir3(parentDir);
|
|
655
756
|
} catch {
|
|
656
757
|
return used;
|
|
657
758
|
}
|
|
658
759
|
for (const name of siblings) {
|
|
659
|
-
const configPath =
|
|
760
|
+
const configPath = join5(parentDir, name, "shared/config/config.development.ts");
|
|
660
761
|
try {
|
|
661
|
-
const content = await
|
|
762
|
+
const content = await readFile3(configPath, "utf8");
|
|
662
763
|
const feMatch = content.match(/frontendUrl:\s*'http:\/\/localhost:(\d+)'/);
|
|
663
764
|
const beMatch = content.match(/backendUrl:\s*'http:\/\/localhost:(\d+)'/);
|
|
664
765
|
if (feMatch && beMatch) {
|
|
@@ -685,23 +786,63 @@ function findNextOffset(usedPorts) {
|
|
|
685
786
|
}
|
|
686
787
|
|
|
687
788
|
// src/utils/extract-package-json-version-from-uri.ts
|
|
688
|
-
import axios from "axios";
|
|
689
789
|
async function extractPackageJsonVersionFromUri(repositoryUrl, branch = "main") {
|
|
690
790
|
const [owner, repo] = repositoryUrl.replace("github:", "").split("/");
|
|
691
|
-
const
|
|
791
|
+
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/package.json?ref=${branch}`;
|
|
692
792
|
try {
|
|
693
|
-
const response = await
|
|
694
|
-
|
|
793
|
+
const response = await fetch(apiUrl, {
|
|
794
|
+
headers: { Accept: "application/vnd.github.raw+json" }
|
|
795
|
+
});
|
|
796
|
+
if (!response.ok) return "unknown";
|
|
797
|
+
const packageJson = await response.json();
|
|
695
798
|
return packageJson.version || "unknown";
|
|
696
|
-
} catch
|
|
799
|
+
} catch {
|
|
697
800
|
return "unknown";
|
|
698
801
|
}
|
|
699
802
|
}
|
|
700
803
|
|
|
804
|
+
// src/utils/fetch-template-metadata.ts
|
|
805
|
+
function parseRepo(repositoryUrl) {
|
|
806
|
+
const [owner, repo] = repositoryUrl.replace("github:", "").split("/");
|
|
807
|
+
return [owner, repo];
|
|
808
|
+
}
|
|
809
|
+
function toDate(iso) {
|
|
810
|
+
return iso ? iso.slice(0, 10) : "unknown";
|
|
811
|
+
}
|
|
812
|
+
var GITHUB_HEADERS = { Accept: "application/vnd.github+json" };
|
|
813
|
+
async function fetchLatestRelease(repositoryUrl) {
|
|
814
|
+
const [owner, repo] = parseRepo(repositoryUrl);
|
|
815
|
+
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/releases?per_page=30`;
|
|
816
|
+
try {
|
|
817
|
+
const response = await fetch(apiUrl, { headers: GITHUB_HEADERS });
|
|
818
|
+
if (!response.ok) return null;
|
|
819
|
+
const releases = await response.json();
|
|
820
|
+
const release = releases.find((r) => !r.draft && !r.prerelease && /^v\d/.test(r.tag_name ?? ""));
|
|
821
|
+
if (!release?.tag_name) return null;
|
|
822
|
+
return { tag: release.tag_name, date: toDate(release.published_at) };
|
|
823
|
+
} catch {
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
async function fetchLatestCommit(repositoryUrl, branch = "main") {
|
|
828
|
+
const [owner, repo] = parseRepo(repositoryUrl);
|
|
829
|
+
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/commits/${branch}`;
|
|
830
|
+
try {
|
|
831
|
+
const response = await fetch(apiUrl, { headers: GITHUB_HEADERS });
|
|
832
|
+
if (!response.ok) return null;
|
|
833
|
+
const data = await response.json();
|
|
834
|
+
if (!data.sha) return null;
|
|
835
|
+
const message = (data.commit?.message ?? "").split("\n")[0];
|
|
836
|
+
return { sha: data.sha, shortSha: data.sha.slice(0, 7), message, date: toDate(data.commit?.author?.date) };
|
|
837
|
+
} catch {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
701
842
|
// src/utils/is-empty-directory.ts
|
|
702
|
-
import { readdir as
|
|
843
|
+
import { readdir as readdir4 } from "fs/promises";
|
|
703
844
|
async function isEmptyDirectory(path2) {
|
|
704
|
-
const files = await
|
|
845
|
+
const files = await readdir4(path2);
|
|
705
846
|
return files.length === 0 || files.length === 1 && files[0] === ".git";
|
|
706
847
|
}
|
|
707
848
|
|
|
@@ -711,6 +852,7 @@ async function main() {
|
|
|
711
852
|
showWelcome(templateVersion);
|
|
712
853
|
const promptTheme = { prefix: "", style: { answer: (text) => text } };
|
|
713
854
|
const promptContext = { clearPromptOnDone: true };
|
|
855
|
+
const templateRef = cli.options.template ? void 0 : await promptTemplateRef(promptTheme, promptContext);
|
|
714
856
|
if (!cli.directory) {
|
|
715
857
|
cli.directory = await input(
|
|
716
858
|
{
|
|
@@ -725,9 +867,6 @@ async function main() {
|
|
|
725
867
|
promptContext
|
|
726
868
|
);
|
|
727
869
|
}
|
|
728
|
-
if (!cli.newBranchName) {
|
|
729
|
-
cli.newBranchName = "development";
|
|
730
|
-
}
|
|
731
870
|
const targetFolder = resolve3(cli.directory);
|
|
732
871
|
const projectName = basename2(targetFolder)?.toLowerCase() || "project";
|
|
733
872
|
if (existsSync2(targetFolder) && !await isEmptyDirectory(targetFolder)) {
|
|
@@ -749,44 +888,84 @@ async function main() {
|
|
|
749
888
|
}
|
|
750
889
|
}
|
|
751
890
|
const portOffset = cli.options.portOffset ?? await promptPortOffset(targetFolder, promptTheme, promptContext);
|
|
891
|
+
confirmChoice("Port offset", formatOffset(portOffset));
|
|
752
892
|
const adminEmail = cli.options.adminEmail ?? await input(
|
|
753
893
|
{
|
|
754
894
|
message: "Admin email for initial seed user",
|
|
755
|
-
default:
|
|
895
|
+
default: "admin@example.com",
|
|
756
896
|
theme: promptTheme
|
|
757
897
|
},
|
|
758
898
|
promptContext
|
|
759
899
|
);
|
|
900
|
+
confirmChoice("Admin email", adminEmail);
|
|
760
901
|
const createOptions = {
|
|
761
902
|
projectName,
|
|
762
903
|
targetFolder,
|
|
763
|
-
newBranchName: cli.newBranchName,
|
|
764
904
|
packageManager: cli.packageManager,
|
|
765
905
|
templateUrl: cli.options.template,
|
|
906
|
+
templateRef,
|
|
766
907
|
portOffset,
|
|
767
908
|
adminEmail
|
|
768
909
|
};
|
|
769
910
|
await create(createOptions);
|
|
770
911
|
}
|
|
771
|
-
main().catch(
|
|
912
|
+
main().catch((error) => {
|
|
913
|
+
console.error(error);
|
|
914
|
+
process.exit(1);
|
|
915
|
+
});
|
|
916
|
+
async function promptTemplateRef(theme, context) {
|
|
917
|
+
const [release, commit] = await Promise.all([fetchLatestRelease(TEMPLATE_URL), fetchLatestCommit(TEMPLATE_URL)]);
|
|
918
|
+
if (!release && !commit) return void 0;
|
|
919
|
+
console.info(pc6.dim("You are about to install the Cella template."));
|
|
920
|
+
console.info();
|
|
921
|
+
if (release) console.info(` Latest release: ${release.tag} ${pc6.dim(`\xB7 ${release.date}`)}`);
|
|
922
|
+
if (commit) {
|
|
923
|
+
console.info(` Latest commit: ${commit.shortSha} ${pc6.dim(`\xB7 ${commit.message} \xB7 ${commit.date}`)}`);
|
|
924
|
+
const repo = TEMPLATE_URL.replace("github:", "");
|
|
925
|
+
console.info(` ${pc6.gray(`https://github.com/${repo}/commit/${commit.sha}`)}`);
|
|
926
|
+
}
|
|
927
|
+
console.info();
|
|
928
|
+
if (!release) return commit?.sha;
|
|
929
|
+
if (!commit) return release.tag;
|
|
930
|
+
const choice = await select(
|
|
931
|
+
{
|
|
932
|
+
message: "Start from",
|
|
933
|
+
theme,
|
|
934
|
+
choices: [
|
|
935
|
+
{ name: "Latest release", value: "release" },
|
|
936
|
+
{ name: "Latest commit", value: "commit" }
|
|
937
|
+
]
|
|
938
|
+
},
|
|
939
|
+
context
|
|
940
|
+
);
|
|
941
|
+
if (choice === "release") {
|
|
942
|
+
confirmChoice("Latest release", release.tag);
|
|
943
|
+
return release.tag;
|
|
944
|
+
}
|
|
945
|
+
confirmChoice("Latest commit", commit.shortSha);
|
|
946
|
+
return commit.sha;
|
|
947
|
+
}
|
|
772
948
|
function formatOffset(o, suffix = "") {
|
|
773
949
|
return `${o} \u2192 :${3e3 + o} / :${4e3 + o} / :${5432 + o}${suffix}`;
|
|
774
950
|
}
|
|
775
951
|
async function promptPortOffset(targetFolder, theme, context) {
|
|
776
952
|
const usedPorts = await detectUsedPorts(targetFolder);
|
|
777
953
|
const suggested = findNextOffset(usedPorts);
|
|
954
|
+
let message = `${pc6.bold("Port offset")} ${pc6.dim("\xB7 to avoid conflicts with other forks/clones")}`;
|
|
778
955
|
if (usedPorts.length > 0) {
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
}
|
|
783
|
-
console.info();
|
|
956
|
+
const count = usedPorts.length;
|
|
957
|
+
message += pc6.dim(`
|
|
958
|
+
Detected ${count} sibling fork${count === 1 ? "" : "s"} \xB7 suggested offset ${suggested}`);
|
|
784
959
|
}
|
|
785
|
-
const
|
|
960
|
+
const portTheme = {
|
|
961
|
+
...theme,
|
|
962
|
+
style: { ...theme.style ?? {}, message: (text) => text }
|
|
963
|
+
};
|
|
964
|
+
const presets = [10, 20, 30].filter((o) => o !== suggested);
|
|
786
965
|
const choice = await select(
|
|
787
966
|
{
|
|
788
|
-
message
|
|
789
|
-
theme,
|
|
967
|
+
message,
|
|
968
|
+
theme: portTheme,
|
|
790
969
|
choices: [
|
|
791
970
|
...suggested > 0 ? [{ name: formatOffset(suggested, " (suggested)"), value: suggested }] : [],
|
|
792
971
|
{ name: formatOffset(0, " (default)"), value: 0 },
|