@cellajs/create-cella 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/configs/default-config.ts.template +79 -81
- package/dist/index.js +303 -133
- package/dist/index.js.map +1 -1
- package/package.json +22 -22
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.2.
|
|
15
|
+
version: "0.2.3",
|
|
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.",
|
|
@@ -45,6 +45,7 @@ var package_default = {
|
|
|
45
45
|
},
|
|
46
46
|
scripts: {
|
|
47
47
|
start: "tsx ./src/create-cella-cli.ts",
|
|
48
|
+
try: "tsx ./src/create-cella-cli.ts ../../../cella-try-app --template ../.. --skip-install",
|
|
48
49
|
clean: "rimraf ./dist",
|
|
49
50
|
build: "tsup",
|
|
50
51
|
"test-build": "pnpm run build && node index.js",
|
|
@@ -54,26 +55,26 @@ var package_default = {
|
|
|
54
55
|
lint: "biome check .",
|
|
55
56
|
"lint:fix": "biome check --write .",
|
|
56
57
|
test: "vitest run",
|
|
57
|
-
"test:release": "pnpm run build && vitest run tests/release-
|
|
58
|
+
"test:release": "pnpm run build && vitest run tests/release-smoke.test.ts",
|
|
58
59
|
"test:watch": "vitest"
|
|
59
60
|
},
|
|
60
61
|
dependencies: {
|
|
61
62
|
"@inquirer/prompts": "^8.5.2",
|
|
62
|
-
axios: "^1.17.0",
|
|
63
63
|
commander: "^15.0.0",
|
|
64
|
-
giget: "^3.
|
|
64
|
+
giget: "^3.3.0",
|
|
65
65
|
"nano-spawn": "^2.0.0",
|
|
66
|
-
ora: "^9.
|
|
66
|
+
ora: "^9.4.1",
|
|
67
67
|
picocolors: "^1.1.1",
|
|
68
68
|
"validate-npm-package-name": "^8.0.0"
|
|
69
69
|
},
|
|
70
70
|
devDependencies: {
|
|
71
|
-
"@types/node": "
|
|
72
|
-
"@typescript/native-preview": "catalog:",
|
|
71
|
+
"@types/node": "24.10.13",
|
|
73
72
|
"@types/validate-npm-package-name": "^4.0.2",
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
"@typescript/native-preview": "7.0.0-dev.20260628.1",
|
|
74
|
+
tsup: "^8.5.1",
|
|
75
|
+
tsx: "^4.22.4",
|
|
76
|
+
typescript: "^6.0.3",
|
|
77
|
+
vitest: "^4.1.9"
|
|
77
78
|
}
|
|
78
79
|
};
|
|
79
80
|
|
|
@@ -84,15 +85,12 @@ var TEMPLATE_URL = "github:cellajs/cella";
|
|
|
84
85
|
var CELLA_REMOTE_URL = "git@github.com:cellajs/cella.git";
|
|
85
86
|
var DESCRIPTION = package_default.description;
|
|
86
87
|
var VERSION = package_default.version;
|
|
87
|
-
var AUTHOR = package_default.author;
|
|
88
88
|
var WEBSITE = package_default.homepage;
|
|
89
|
-
var GITHUB = package_default.repository.url;
|
|
90
89
|
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);
|
|
90
|
+
const leftText = `\u29C8 ${NAME} \xB7 ${VERSION} \xB7 cella ${templateVersion}`;
|
|
91
|
+
const left = `${pc.cyan(`\u29C8 ${NAME}`)} ${pc.dim(`\xB7 ${VERSION} \xB7 cella ${templateVersion}`)}`;
|
|
92
|
+
const right = pc.cyan(WEBSITE);
|
|
93
|
+
const padding = Math.max(1, 60 - leftText.length - WEBSITE.length);
|
|
96
94
|
return `${left}${" ".repeat(padding)}${right}`;
|
|
97
95
|
}
|
|
98
96
|
var TO_REMOVE = ["./cli/create", "./info/QUICKSTART.md"];
|
|
@@ -101,12 +99,12 @@ var TO_COPY = {
|
|
|
101
99
|
"./frontend/.env.example": "./frontend/.env",
|
|
102
100
|
"./info/QUICKSTART.md": "README.md"
|
|
103
101
|
};
|
|
104
|
-
var PLACEHOLDER_CONFIG = "
|
|
102
|
+
var PLACEHOLDER_CONFIG = "configs/default-config.ts.template";
|
|
105
103
|
async function generateEnvFromExample(examplePath, replacements) {
|
|
106
|
-
const { readFile:
|
|
104
|
+
const { readFile: readFile3 } = await import("fs/promises");
|
|
107
105
|
let content;
|
|
108
106
|
try {
|
|
109
|
-
content = await
|
|
107
|
+
content = await readFile3(examplePath, "utf8");
|
|
110
108
|
} catch {
|
|
111
109
|
return null;
|
|
112
110
|
}
|
|
@@ -115,16 +113,12 @@ async function generateEnvFromExample(examplePath, replacements) {
|
|
|
115
113
|
return match;
|
|
116
114
|
});
|
|
117
115
|
}
|
|
118
|
-
function
|
|
119
|
-
return {
|
|
120
|
-
PROJECT_SLUG: slug,
|
|
121
|
-
DB_PORT: String(5432 + portOffset),
|
|
122
|
-
DB_TEST_PORT: String(5434 + portOffset)
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function getBackendEnvReplacements(adminEmail, portOffset) {
|
|
116
|
+
function getBackendEnvReplacements(slug, adminEmail, portOffset) {
|
|
126
117
|
const db = 5432 + portOffset;
|
|
127
118
|
return {
|
|
119
|
+
PROJECT_SLUG: slug,
|
|
120
|
+
DB_PORT: String(db),
|
|
121
|
+
DB_TEST_PORT: String(5434 + portOffset),
|
|
128
122
|
DATABASE_URL: `postgres://runtime_role:dev_password@0.0.0.0:${db}/postgres`,
|
|
129
123
|
DATABASE_ADMIN_URL: `postgres://postgres:postgres@0.0.0.0:${db}/postgres`,
|
|
130
124
|
DATABASE_CDC_URL: `postgres://admin_role:dev_password@0.0.0.0:${db}/postgres`,
|
|
@@ -135,12 +129,11 @@ function getBackendEnvReplacements(adminEmail, portOffset) {
|
|
|
135
129
|
function generateEnvConfigs(slug, name, portOffset) {
|
|
136
130
|
const fe = 3e3 + portOffset;
|
|
137
131
|
const be = 4e3 + portOffset;
|
|
138
|
-
const header = "import type { DeepPartial } from '../src/builder/types';\nimport type _default from './config.default';\n";
|
|
132
|
+
const header = "import type { DeepPartial } from '../src/config-builder/types';\nimport type _default from './config.default';\n";
|
|
139
133
|
const envs = {
|
|
140
134
|
development: {
|
|
141
135
|
props: {
|
|
142
136
|
slug: `${slug}-development`,
|
|
143
|
-
debug: false,
|
|
144
137
|
domain: "",
|
|
145
138
|
frontendUrl: `http://localhost:${fe}`,
|
|
146
139
|
backendUrl: `http://localhost:${be}`,
|
|
@@ -150,7 +143,6 @@ function generateEnvConfigs(slug, name, portOffset) {
|
|
|
150
143
|
staging: {
|
|
151
144
|
props: {
|
|
152
145
|
slug: `${slug}-staging`,
|
|
153
|
-
debug: false,
|
|
154
146
|
domain: `${slug}.example.com`,
|
|
155
147
|
frontendUrl: `https://staging.${slug}.example.com`,
|
|
156
148
|
backendUrl: `https://api-staging.${slug}.example.com`,
|
|
@@ -167,7 +159,6 @@ function generateEnvConfigs(slug, name, portOffset) {
|
|
|
167
159
|
test: {
|
|
168
160
|
imports: "import development from './config.development';\n",
|
|
169
161
|
props: {
|
|
170
|
-
debug: false,
|
|
171
162
|
domain: "",
|
|
172
163
|
frontendUrl: "=development.frontendUrl",
|
|
173
164
|
backendUrl: "=development.backendUrl",
|
|
@@ -197,10 +188,13 @@ ${nameEntry}${body}
|
|
|
197
188
|
}
|
|
198
189
|
|
|
199
190
|
// src/create.ts
|
|
191
|
+
import { execFileSync } from "child_process";
|
|
200
192
|
import { existsSync } from "fs";
|
|
201
|
-
import { cp, mkdir } from "fs/promises";
|
|
202
|
-
import { join, relative, resolve as resolve2 } from "path";
|
|
193
|
+
import { cp, mkdir, rm } from "fs/promises";
|
|
194
|
+
import { dirname as dirname2, isAbsolute, join as join2, relative as relative2, resolve as resolve2 } from "path";
|
|
195
|
+
import { checkbox } from "@inquirer/prompts";
|
|
203
196
|
import { downloadTemplate } from "giget";
|
|
197
|
+
import pc5 from "picocolors";
|
|
204
198
|
|
|
205
199
|
// src/utils/git/command.ts
|
|
206
200
|
import { execFile } from "child_process";
|
|
@@ -223,13 +217,7 @@ async function gitAddAll(repoPath) {
|
|
|
223
217
|
return runGitCommand(["add", "."], repoPath);
|
|
224
218
|
}
|
|
225
219
|
async function gitCommit(repoPath, message) {
|
|
226
|
-
return runGitCommand(["commit", "-m", message], repoPath);
|
|
227
|
-
}
|
|
228
|
-
async function gitBranch(repoPath, branchName) {
|
|
229
|
-
return runGitCommand(["branch", branchName], repoPath);
|
|
230
|
-
}
|
|
231
|
-
async function gitCheckout(repoPath, branchName) {
|
|
232
|
-
return runGitCommand(["checkout", branchName], repoPath);
|
|
220
|
+
return runGitCommand(["commit", "--quiet", "--no-verify", "-m", message], repoPath);
|
|
233
221
|
}
|
|
234
222
|
async function gitRemoteGetUrl(repoPath, remoteName) {
|
|
235
223
|
return runGitCommand(["remote", "get-url", remoteName], repoPath);
|
|
@@ -286,10 +274,19 @@ function validateProjectName(name) {
|
|
|
286
274
|
}
|
|
287
275
|
|
|
288
276
|
// src/modules/cli/commands.ts
|
|
277
|
+
function parsePortOffset(value) {
|
|
278
|
+
const parsed = Number(value);
|
|
279
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 490) {
|
|
280
|
+
throw new InvalidArgumentError("Port offset must be an integer between 0 and 490");
|
|
281
|
+
}
|
|
282
|
+
if (parsed % 10 !== 0) {
|
|
283
|
+
throw new InvalidArgumentError("Port offset must be a multiple of 10");
|
|
284
|
+
}
|
|
285
|
+
return parsed;
|
|
286
|
+
}
|
|
289
287
|
var directory = null;
|
|
290
|
-
var newBranchName = null;
|
|
291
288
|
var packageManager = "pnpm";
|
|
292
|
-
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)").action((name) => {
|
|
289
|
+
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").option("--skip-install", "scaffold only: skip dependency installation and migration generation").action((name) => {
|
|
293
290
|
const trimmedName = typeof name === "string" ? name.trim() : name;
|
|
294
291
|
if (trimmedName) {
|
|
295
292
|
const validation = validateProjectName(basename(resolve(trimmedName)));
|
|
@@ -305,7 +302,6 @@ function runCli() {
|
|
|
305
302
|
options,
|
|
306
303
|
args: command.args,
|
|
307
304
|
directory,
|
|
308
|
-
newBranchName,
|
|
309
305
|
packageManager
|
|
310
306
|
};
|
|
311
307
|
}
|
|
@@ -329,32 +325,47 @@ function showWelcome(templateVersion) {
|
|
|
329
325
|
console.info(getHeaderLine(templateVersion));
|
|
330
326
|
console.info(DIVIDER);
|
|
331
327
|
}
|
|
328
|
+
function confirmChoice(label, value) {
|
|
329
|
+
console.info(`${pc2.green("\u2713")} ${label} \xB7 ${value}`);
|
|
330
|
+
console.info();
|
|
331
|
+
}
|
|
332
332
|
function showSuccess(projectName, _targetFolder, relativePath, needsCd, packageManager2) {
|
|
333
|
+
console.info();
|
|
334
|
+
console.info(pc2.bold("Next steps"));
|
|
333
335
|
console.info(DIVIDER);
|
|
334
336
|
console.info();
|
|
335
337
|
if (needsCd) {
|
|
336
|
-
console.info(`${pc2.
|
|
337
|
-
console.info();
|
|
338
|
+
console.info(`${pc2.dim("$")} cd ${pc2.cyan(relativePath)}`);
|
|
338
339
|
}
|
|
339
|
-
console.info(`${pc2.green("\u2192")} ${pc2.cyan(`${packageManager2} quick`)} ${pc2.gray("(pglite, no docker)")}`);
|
|
340
|
-
console.info();
|
|
341
|
-
console.info(pc2.gray("or, for full setup:"));
|
|
342
|
-
console.info();
|
|
343
340
|
console.info(
|
|
344
|
-
`${pc2.
|
|
341
|
+
`${pc2.dim("$")} ${pc2.cyan(`${packageManager2} docker`)} ${pc2.dim("&&")} ${pc2.cyan(`${packageManager2} seed`)} ${pc2.dim("&&")} ${pc2.cyan(`${packageManager2} dev`)}`
|
|
345
342
|
);
|
|
346
343
|
console.info();
|
|
347
|
-
|
|
344
|
+
const readmePath = relativePath ? `${relativePath}/README.md` : "README.md";
|
|
345
|
+
console.info(`${pc2.gray("To get started building, please read:")} ${pc2.cyan(readmePath)}`);
|
|
348
346
|
console.info();
|
|
349
|
-
console.info(
|
|
347
|
+
console.info(`\u{1F973} enjoy building ${pc2.green(projectName)} with cella!`);
|
|
350
348
|
console.info();
|
|
351
349
|
}
|
|
352
350
|
|
|
353
351
|
// src/utils/clean-template.ts
|
|
354
352
|
import fs from "fs/promises";
|
|
355
353
|
import path from "path";
|
|
354
|
+
import { fileURLToPath } from "url";
|
|
356
355
|
import pc3 from "picocolors";
|
|
357
356
|
var warningMark = pc3.yellow("\u26A0");
|
|
357
|
+
async function resolvePlaceholderConfigPath() {
|
|
358
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
359
|
+
const candidates = [path.resolve(here, "../..", PLACEHOLDER_CONFIG), path.resolve(here, "..", PLACEHOLDER_CONFIG)];
|
|
360
|
+
for (const candidate of candidates) {
|
|
361
|
+
try {
|
|
362
|
+
await fs.access(candidate);
|
|
363
|
+
return candidate;
|
|
364
|
+
} catch {
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return candidates[0];
|
|
368
|
+
}
|
|
358
369
|
async function cleanTemplate({
|
|
359
370
|
targetFolder,
|
|
360
371
|
projectName,
|
|
@@ -374,14 +385,7 @@ async function cleanTemplate({
|
|
|
374
385
|
await copyFile(srcAbsolutePath, destAbsolutePath);
|
|
375
386
|
}
|
|
376
387
|
await applyPlaceholderConfig(targetFolder, projectName);
|
|
377
|
-
const
|
|
378
|
-
const rootEnv = await generateEnvFromExample(path.resolve(targetFolder, ".env.example"), rootReplacements);
|
|
379
|
-
await fs.writeFile(
|
|
380
|
-
path.resolve(targetFolder, ".env"),
|
|
381
|
-
rootEnv ?? Object.entries(rootReplacements).map(([k, v]) => `${k}=${v}`).join("\n"),
|
|
382
|
-
"utf8"
|
|
383
|
-
);
|
|
384
|
-
const backendReplacements = getBackendEnvReplacements(adminEmail, portOffset);
|
|
388
|
+
const backendReplacements = getBackendEnvReplacements(projectName, adminEmail, portOffset);
|
|
385
389
|
const backendEnv = await generateEnvFromExample(
|
|
386
390
|
path.resolve(targetFolder, "backend/.env.example"),
|
|
387
391
|
backendReplacements
|
|
@@ -447,7 +451,7 @@ ${warningMark} Source file "${src}" does not exist > Skip copy`);
|
|
|
447
451
|
}
|
|
448
452
|
async function applyPlaceholderConfig(targetFolder, projectName) {
|
|
449
453
|
const displayName = projectName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
450
|
-
const src =
|
|
454
|
+
const src = await resolvePlaceholderConfigPath();
|
|
451
455
|
const dest = path.resolve(targetFolder, "./shared/config/config.default.ts");
|
|
452
456
|
try {
|
|
453
457
|
let content = await fs.readFile(src, "utf8");
|
|
@@ -468,6 +472,16 @@ ${warningMark} Placeholder config "${src}" not found > Skip`);
|
|
|
468
472
|
import ora from "ora";
|
|
469
473
|
import pc4 from "picocolors";
|
|
470
474
|
var activeSpinner = null;
|
|
475
|
+
function pauseSpinner() {
|
|
476
|
+
if (activeSpinner) {
|
|
477
|
+
activeSpinner.stop();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function resumeSpinner() {
|
|
481
|
+
if (activeSpinner) {
|
|
482
|
+
activeSpinner.start();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
471
485
|
function createProgress(title, silent = false) {
|
|
472
486
|
const completedSteps = [];
|
|
473
487
|
const spinner = ora({
|
|
@@ -506,22 +520,6 @@ function createProgress(title, silent = false) {
|
|
|
506
520
|
spinner.stop();
|
|
507
521
|
activeSpinner = null;
|
|
508
522
|
log(`${pc4.red("\u2717")} ${pc4.red(message)}`);
|
|
509
|
-
},
|
|
510
|
-
wrap: async (fn) => {
|
|
511
|
-
try {
|
|
512
|
-
return await fn();
|
|
513
|
-
} catch (error) {
|
|
514
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
515
|
-
spinner.stop();
|
|
516
|
-
activeSpinner = null;
|
|
517
|
-
log(pc4.cyan(`
|
|
518
|
-
${title}`));
|
|
519
|
-
for (const step of completedSteps) {
|
|
520
|
-
log(pc4.gray(` \u251C\u2500 ${step}`));
|
|
521
|
-
}
|
|
522
|
-
log(pc4.red(` \u2514\u2500 \u2717 ${errorMessage}`));
|
|
523
|
-
throw error;
|
|
524
|
-
}
|
|
525
523
|
}
|
|
526
524
|
};
|
|
527
525
|
}
|
|
@@ -550,18 +548,62 @@ async function generate(packageManager2) {
|
|
|
550
548
|
});
|
|
551
549
|
}
|
|
552
550
|
|
|
551
|
+
// src/utils/scan-optional-modules.ts
|
|
552
|
+
import { readdir, readFile } from "fs/promises";
|
|
553
|
+
import { dirname, join, relative } from "path";
|
|
554
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".turbo", "coverage"]);
|
|
555
|
+
async function scanOptionalModules(targetFolder) {
|
|
556
|
+
const files = [];
|
|
557
|
+
await collectModuleFiles(targetFolder, files);
|
|
558
|
+
const modules = [];
|
|
559
|
+
for (const file of files) {
|
|
560
|
+
const src = await readFile(file, "utf8");
|
|
561
|
+
if (!/optional:\s*true/.test(src)) continue;
|
|
562
|
+
const name = src.match(/name:\s*['"]([^'"]+)['"]/)?.[1];
|
|
563
|
+
const description = src.match(/description:\s*['"]([^'"]+)['"]/)?.[1] ?? "";
|
|
564
|
+
if (!name) continue;
|
|
565
|
+
modules.push({ name, description, folder: relative(targetFolder, dirname(file)) });
|
|
566
|
+
}
|
|
567
|
+
return modules.sort((a, b) => a.name.localeCompare(b.name));
|
|
568
|
+
}
|
|
569
|
+
function optionalModuleFolders({ name, folder }) {
|
|
570
|
+
return [
|
|
571
|
+
folder,
|
|
572
|
+
`frontend/src/routes/_public/_${name}`,
|
|
573
|
+
`frontend/src/routes/_public/${name}`,
|
|
574
|
+
`frontend/src/routes/_app/_${name}`,
|
|
575
|
+
`frontend/src/routes/_app/${name}`,
|
|
576
|
+
`frontend/public/static/${name}`
|
|
577
|
+
];
|
|
578
|
+
}
|
|
579
|
+
async function collectModuleFiles(dir, out) {
|
|
580
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
581
|
+
for (const entry of entries) {
|
|
582
|
+
if (entry.isDirectory()) {
|
|
583
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
584
|
+
await collectModuleFiles(join(dir, entry.name), out);
|
|
585
|
+
} else if (entry.name.endsWith("-module.ts")) {
|
|
586
|
+
out.push(join(dir, entry.name));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
553
591
|
// src/create.ts
|
|
554
592
|
function isLocalPath(path2) {
|
|
555
593
|
return path2.startsWith("/") || path2.startsWith("./") || path2.startsWith("../");
|
|
556
594
|
}
|
|
595
|
+
function shouldSkipStep(name) {
|
|
596
|
+
return process.env[`CREATE_CELLA_SKIP_${name.toUpperCase()}`] === "true";
|
|
597
|
+
}
|
|
557
598
|
async function create({
|
|
558
599
|
projectName,
|
|
559
600
|
targetFolder,
|
|
560
|
-
newBranchName: newBranchName2,
|
|
561
601
|
packageManager: packageManager2,
|
|
562
602
|
templateUrl,
|
|
603
|
+
templateRef,
|
|
563
604
|
portOffset,
|
|
564
605
|
adminEmail,
|
|
606
|
+
skipInstall = false,
|
|
565
607
|
silent = false
|
|
566
608
|
}) {
|
|
567
609
|
const originalCwd = process.cwd();
|
|
@@ -575,72 +617,121 @@ async function create({
|
|
|
575
617
|
if (isLocalTemplate) {
|
|
576
618
|
progress.step("copying local template");
|
|
577
619
|
const sourcePath = resolve2(originalCwd, templateUrl);
|
|
578
|
-
|
|
620
|
+
const rel = relative2(sourcePath, targetFolder);
|
|
621
|
+
const targetInsideTemplate = rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
622
|
+
if (targetInsideTemplate) {
|
|
579
623
|
throw new Error(
|
|
580
624
|
`Cannot create project inside the template directory.
|
|
581
625
|
Run from outside: cd ~ && pnpm create @cellajs/cella ${projectName} --template ${templateUrl}`
|
|
582
626
|
);
|
|
583
627
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
628
|
+
let tracked = [];
|
|
629
|
+
try {
|
|
630
|
+
tracked = execFileSync("git", ["ls-files", "--cached", "--others", "--exclude-standard", "-z"], {
|
|
631
|
+
cwd: sourcePath
|
|
632
|
+
}).toString().split("\0").filter(Boolean);
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
if (tracked.length > 0) {
|
|
636
|
+
for (const rel2 of tracked) {
|
|
637
|
+
const src = join2(sourcePath, rel2);
|
|
638
|
+
if (!existsSync(src)) continue;
|
|
639
|
+
const dest = join2(targetFolder, rel2);
|
|
640
|
+
await mkdir(dirname2(dest), { recursive: true });
|
|
641
|
+
await cp(src, dest);
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
await cp(sourcePath, targetFolder, {
|
|
645
|
+
recursive: true,
|
|
646
|
+
filter: (src) => !src.includes("node_modules") && !src.includes(".git")
|
|
647
|
+
});
|
|
648
|
+
}
|
|
588
649
|
} else {
|
|
589
|
-
|
|
590
|
-
|
|
650
|
+
const downloadId = templateRef ? `${template}#${templateRef}` : template;
|
|
651
|
+
progress.step(`downloading cella template${templateRef ? ` (${templateRef})` : ""}`);
|
|
652
|
+
await downloadTemplate(downloadId, {
|
|
591
653
|
cwd: process.cwd(),
|
|
592
654
|
dir: targetFolder,
|
|
593
655
|
force: true,
|
|
594
656
|
provider: "github"
|
|
595
657
|
});
|
|
596
658
|
}
|
|
659
|
+
if (!silent) await promptOptionalModules(targetFolder);
|
|
597
660
|
progress.step("cleaning template");
|
|
598
661
|
const displayName = projectName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
599
662
|
await cleanTemplate({ targetFolder, projectName, displayName, portOffset, adminEmail });
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
663
|
+
if (!skipInstall && !shouldSkipStep("install")) {
|
|
664
|
+
progress.step("installing dependencies");
|
|
665
|
+
await install(packageManager2);
|
|
666
|
+
}
|
|
667
|
+
if (!skipInstall && !shouldSkipStep("generate")) {
|
|
668
|
+
progress.step("generating migrations");
|
|
669
|
+
await generate(packageManager2);
|
|
670
|
+
}
|
|
671
|
+
const gitFolderPath = join2(targetFolder, ".git");
|
|
672
|
+
if (!shouldSkipStep("git") && !existsSync(gitFolderPath)) {
|
|
606
673
|
progress.step("initializing git");
|
|
607
674
|
await gitInit(targetFolder);
|
|
608
675
|
await gitAddAll(targetFolder);
|
|
609
676
|
await gitCommit(targetFolder, "Initial commit");
|
|
610
|
-
if (newBranchName2) {
|
|
611
|
-
progress.step(`creating branch '${newBranchName2}'`);
|
|
612
|
-
await gitBranch(targetFolder, newBranchName2);
|
|
613
|
-
await gitCheckout(targetFolder, newBranchName2);
|
|
614
|
-
}
|
|
615
677
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
678
|
+
if (!shouldSkipStep("remote")) {
|
|
679
|
+
progress.step("adding upstream remote");
|
|
680
|
+
await addRemote({ targetFolder, silent: true });
|
|
681
|
+
}
|
|
682
|
+
progress.done(pc5.bold(`created ${projectName}`));
|
|
619
683
|
} catch (error) {
|
|
620
684
|
progress.fail(error instanceof Error ? error.message : String(error));
|
|
621
|
-
|
|
685
|
+
throw error;
|
|
622
686
|
}
|
|
623
687
|
const needsCd = originalCwd !== targetFolder;
|
|
624
|
-
const relativePath =
|
|
688
|
+
const relativePath = relative2(originalCwd, targetFolder);
|
|
625
689
|
showSuccess(projectName, targetFolder, relativePath, needsCd, packageManager2);
|
|
626
690
|
}
|
|
691
|
+
async function promptOptionalModules(targetFolder) {
|
|
692
|
+
const modules = await scanOptionalModules(targetFolder);
|
|
693
|
+
if (modules.length === 0) return;
|
|
694
|
+
if (!process.stdin.isTTY) return;
|
|
695
|
+
pauseSpinner();
|
|
696
|
+
console.info();
|
|
697
|
+
const keep = await checkbox(
|
|
698
|
+
{
|
|
699
|
+
message: "Optional modules to include",
|
|
700
|
+
// Leading space on the radio icons adds a gap after the cursor (inquirer renders
|
|
701
|
+
// `${cursor}${checkbox} ${name}` with no space between cursor and checkbox).
|
|
702
|
+
theme: { icon: { checked: ` ${pc5.green("\u25C9")}`, unchecked: " \u25EF" } },
|
|
703
|
+
choices: modules.map((m) => ({
|
|
704
|
+
name: `${m.name} ${pc5.dim(`\xB7 ${m.description}`)}`,
|
|
705
|
+
value: m.name,
|
|
706
|
+
checked: true
|
|
707
|
+
}))
|
|
708
|
+
},
|
|
709
|
+
// Clear the answered prompt so we can print a compact, non-bold summary that lists
|
|
710
|
+
// only the kept module names (not their descriptions).
|
|
711
|
+
{ clearPromptOnDone: true }
|
|
712
|
+
);
|
|
713
|
+
confirmChoice("Optional modules", keep.length ? keep.join(", ") : pc5.dim("none"));
|
|
714
|
+
resumeSpinner();
|
|
715
|
+
const folders = modules.filter((m) => !keep.includes(m.name)).flatMap(optionalModuleFolders);
|
|
716
|
+
await Promise.all(folders.map((f) => rm(join2(targetFolder, f), { recursive: true, force: true })));
|
|
717
|
+
}
|
|
627
718
|
|
|
628
719
|
// src/utils/detect-used-ports.ts
|
|
629
|
-
import { readdir, readFile } from "fs/promises";
|
|
630
|
-
import { dirname, join as
|
|
720
|
+
import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
|
|
721
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
631
722
|
async function detectUsedPorts(targetFolder) {
|
|
632
|
-
const parentDir =
|
|
723
|
+
const parentDir = dirname3(targetFolder);
|
|
633
724
|
const used = [];
|
|
634
725
|
let siblings;
|
|
635
726
|
try {
|
|
636
|
-
siblings = await
|
|
727
|
+
siblings = await readdir2(parentDir);
|
|
637
728
|
} catch {
|
|
638
729
|
return used;
|
|
639
730
|
}
|
|
640
731
|
for (const name of siblings) {
|
|
641
|
-
const configPath =
|
|
732
|
+
const configPath = join3(parentDir, name, "shared/config/config.development.ts");
|
|
642
733
|
try {
|
|
643
|
-
const content = await
|
|
734
|
+
const content = await readFile2(configPath, "utf8");
|
|
644
735
|
const feMatch = content.match(/frontendUrl:\s*'http:\/\/localhost:(\d+)'/);
|
|
645
736
|
const beMatch = content.match(/backendUrl:\s*'http:\/\/localhost:(\d+)'/);
|
|
646
737
|
if (feMatch && beMatch) {
|
|
@@ -667,23 +758,63 @@ function findNextOffset(usedPorts) {
|
|
|
667
758
|
}
|
|
668
759
|
|
|
669
760
|
// src/utils/extract-package-json-version-from-uri.ts
|
|
670
|
-
import axios from "axios";
|
|
671
761
|
async function extractPackageJsonVersionFromUri(repositoryUrl, branch = "main") {
|
|
672
762
|
const [owner, repo] = repositoryUrl.replace("github:", "").split("/");
|
|
673
|
-
const
|
|
763
|
+
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/package.json?ref=${branch}`;
|
|
674
764
|
try {
|
|
675
|
-
const response = await
|
|
676
|
-
|
|
765
|
+
const response = await fetch(apiUrl, {
|
|
766
|
+
headers: { Accept: "application/vnd.github.raw+json" }
|
|
767
|
+
});
|
|
768
|
+
if (!response.ok) return "unknown";
|
|
769
|
+
const packageJson = await response.json();
|
|
677
770
|
return packageJson.version || "unknown";
|
|
678
|
-
} catch
|
|
771
|
+
} catch {
|
|
679
772
|
return "unknown";
|
|
680
773
|
}
|
|
681
774
|
}
|
|
682
775
|
|
|
776
|
+
// src/utils/fetch-template-metadata.ts
|
|
777
|
+
function parseRepo(repositoryUrl) {
|
|
778
|
+
const [owner, repo] = repositoryUrl.replace("github:", "").split("/");
|
|
779
|
+
return [owner, repo];
|
|
780
|
+
}
|
|
781
|
+
function toDate(iso) {
|
|
782
|
+
return iso ? iso.slice(0, 10) : "unknown";
|
|
783
|
+
}
|
|
784
|
+
var GITHUB_HEADERS = { Accept: "application/vnd.github+json" };
|
|
785
|
+
async function fetchLatestRelease(repositoryUrl) {
|
|
786
|
+
const [owner, repo] = parseRepo(repositoryUrl);
|
|
787
|
+
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/releases?per_page=30`;
|
|
788
|
+
try {
|
|
789
|
+
const response = await fetch(apiUrl, { headers: GITHUB_HEADERS });
|
|
790
|
+
if (!response.ok) return null;
|
|
791
|
+
const releases = await response.json();
|
|
792
|
+
const release = releases.find((r) => !r.draft && !r.prerelease && /^v\d/.test(r.tag_name ?? ""));
|
|
793
|
+
if (!release?.tag_name) return null;
|
|
794
|
+
return { tag: release.tag_name, date: toDate(release.published_at) };
|
|
795
|
+
} catch {
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
async function fetchLatestCommit(repositoryUrl, branch = "main") {
|
|
800
|
+
const [owner, repo] = parseRepo(repositoryUrl);
|
|
801
|
+
const apiUrl = `https://api.github.com/repos/${owner}/${repo}/commits/${branch}`;
|
|
802
|
+
try {
|
|
803
|
+
const response = await fetch(apiUrl, { headers: GITHUB_HEADERS });
|
|
804
|
+
if (!response.ok) return null;
|
|
805
|
+
const data = await response.json();
|
|
806
|
+
if (!data.sha) return null;
|
|
807
|
+
const message = (data.commit?.message ?? "").split("\n")[0];
|
|
808
|
+
return { sha: data.sha, shortSha: data.sha.slice(0, 7), message, date: toDate(data.commit?.author?.date) };
|
|
809
|
+
} catch {
|
|
810
|
+
return null;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
683
814
|
// src/utils/is-empty-directory.ts
|
|
684
|
-
import { readdir as
|
|
815
|
+
import { readdir as readdir3 } from "fs/promises";
|
|
685
816
|
async function isEmptyDirectory(path2) {
|
|
686
|
-
const files = await
|
|
817
|
+
const files = await readdir3(path2);
|
|
687
818
|
return files.length === 0 || files.length === 1 && files[0] === ".git";
|
|
688
819
|
}
|
|
689
820
|
|
|
@@ -693,6 +824,7 @@ async function main() {
|
|
|
693
824
|
showWelcome(templateVersion);
|
|
694
825
|
const promptTheme = { prefix: "", style: { answer: (text) => text } };
|
|
695
826
|
const promptContext = { clearPromptOnDone: true };
|
|
827
|
+
const templateRef = cli.options.template ? void 0 : await promptTemplateRef(promptTheme, promptContext);
|
|
696
828
|
if (!cli.directory) {
|
|
697
829
|
cli.directory = await input(
|
|
698
830
|
{
|
|
@@ -707,9 +839,6 @@ async function main() {
|
|
|
707
839
|
promptContext
|
|
708
840
|
);
|
|
709
841
|
}
|
|
710
|
-
if (!cli.newBranchName) {
|
|
711
|
-
cli.newBranchName = "development";
|
|
712
|
-
}
|
|
713
842
|
const targetFolder = resolve3(cli.directory);
|
|
714
843
|
const projectName = basename2(targetFolder)?.toLowerCase() || "project";
|
|
715
844
|
if (existsSync2(targetFolder) && !await isEmptyDirectory(targetFolder)) {
|
|
@@ -730,45 +859,86 @@ async function main() {
|
|
|
730
859
|
process.exit(1);
|
|
731
860
|
}
|
|
732
861
|
}
|
|
733
|
-
const portOffset = await promptPortOffset(targetFolder, promptTheme, promptContext);
|
|
734
|
-
|
|
862
|
+
const portOffset = cli.options.portOffset ?? await promptPortOffset(targetFolder, promptTheme, promptContext);
|
|
863
|
+
confirmChoice("Port offset", formatOffset(portOffset));
|
|
864
|
+
const adminEmail = cli.options.adminEmail ?? await input(
|
|
735
865
|
{
|
|
736
866
|
message: "Admin email for initial seed user",
|
|
737
|
-
default:
|
|
867
|
+
default: "admin@example.com",
|
|
738
868
|
theme: promptTheme
|
|
739
869
|
},
|
|
740
870
|
promptContext
|
|
741
871
|
);
|
|
872
|
+
confirmChoice("Admin email", adminEmail);
|
|
742
873
|
const createOptions = {
|
|
743
874
|
projectName,
|
|
744
875
|
targetFolder,
|
|
745
|
-
newBranchName: cli.newBranchName,
|
|
746
876
|
packageManager: cli.packageManager,
|
|
747
877
|
templateUrl: cli.options.template,
|
|
878
|
+
templateRef,
|
|
748
879
|
portOffset,
|
|
749
|
-
adminEmail
|
|
880
|
+
adminEmail,
|
|
881
|
+
skipInstall: cli.options.skipInstall
|
|
750
882
|
};
|
|
751
883
|
await create(createOptions);
|
|
752
884
|
}
|
|
753
|
-
main().catch(
|
|
885
|
+
main().catch((error) => {
|
|
886
|
+
console.error(error);
|
|
887
|
+
process.exit(1);
|
|
888
|
+
});
|
|
889
|
+
async function promptTemplateRef(theme, context) {
|
|
890
|
+
const [release, commit] = await Promise.all([fetchLatestRelease(TEMPLATE_URL), fetchLatestCommit(TEMPLATE_URL)]);
|
|
891
|
+
if (!release && !commit) return void 0;
|
|
892
|
+
console.info(pc6.dim("You are about to install the Cella template."));
|
|
893
|
+
console.info();
|
|
894
|
+
if (release) console.info(` Latest release: ${release.tag} ${pc6.dim(`\xB7 ${release.date}`)}`);
|
|
895
|
+
if (commit) {
|
|
896
|
+
console.info(` Latest commit: ${commit.shortSha} ${pc6.dim(`\xB7 ${commit.message} \xB7 ${commit.date}`)}`);
|
|
897
|
+
const repo = TEMPLATE_URL.replace("github:", "");
|
|
898
|
+
console.info(` ${pc6.gray(`https://github.com/${repo}/commit/${commit.sha}`)}`);
|
|
899
|
+
}
|
|
900
|
+
console.info();
|
|
901
|
+
if (!release) return commit?.sha;
|
|
902
|
+
if (!commit) return release.tag;
|
|
903
|
+
const choice = await select(
|
|
904
|
+
{
|
|
905
|
+
message: "Start from",
|
|
906
|
+
theme,
|
|
907
|
+
choices: [
|
|
908
|
+
{ name: "Latest release", value: "release" },
|
|
909
|
+
{ name: "Latest commit", value: "commit" }
|
|
910
|
+
]
|
|
911
|
+
},
|
|
912
|
+
context
|
|
913
|
+
);
|
|
914
|
+
if (choice === "release") {
|
|
915
|
+
confirmChoice("Latest release", release.tag);
|
|
916
|
+
return release.tag;
|
|
917
|
+
}
|
|
918
|
+
confirmChoice("Latest commit", commit.shortSha);
|
|
919
|
+
return commit.sha;
|
|
920
|
+
}
|
|
754
921
|
function formatOffset(o, suffix = "") {
|
|
755
922
|
return `${o} \u2192 :${3e3 + o} / :${4e3 + o} / :${5432 + o}${suffix}`;
|
|
756
923
|
}
|
|
757
924
|
async function promptPortOffset(targetFolder, theme, context) {
|
|
758
925
|
const usedPorts = await detectUsedPorts(targetFolder);
|
|
759
926
|
const suggested = findNextOffset(usedPorts);
|
|
927
|
+
let message = `${pc6.bold("Port offset")} ${pc6.dim("\xB7 to avoid conflicts with other forks/clones")}`;
|
|
760
928
|
if (usedPorts.length > 0) {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
}
|
|
765
|
-
console.info();
|
|
929
|
+
const count = usedPorts.length;
|
|
930
|
+
message += pc6.dim(`
|
|
931
|
+
Detected ${count} sibling fork${count === 1 ? "" : "s"} \xB7 suggested offset ${suggested}`);
|
|
766
932
|
}
|
|
767
|
-
const
|
|
933
|
+
const portTheme = {
|
|
934
|
+
...theme,
|
|
935
|
+
style: { ...theme.style ?? {}, message: (text) => text }
|
|
936
|
+
};
|
|
937
|
+
const presets = [10, 20, 30].filter((o) => o !== suggested);
|
|
768
938
|
const choice = await select(
|
|
769
939
|
{
|
|
770
|
-
message
|
|
771
|
-
theme,
|
|
940
|
+
message,
|
|
941
|
+
theme: portTheme,
|
|
772
942
|
choices: [
|
|
773
943
|
...suggested > 0 ? [{ name: formatOffset(suggested, " (suggested)"), value: suggested }] : [],
|
|
774
944
|
{ name: formatOffset(0, " (default)"), value: 0 },
|