@cellajs/create-cella 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,798 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ // src/create-cella-cli.ts
4
+ import { existsSync as existsSync2 } from "fs";
5
+ import { basename as basename2, resolve as resolve3 } from "path";
6
+ import { input, select } from "@inquirer/prompts";
7
+ import pc5 from "picocolors";
8
+
9
+ // src/constants.ts
10
+ import pc from "picocolors";
11
+
12
+ // package.json
13
+ var package_default = {
14
+ name: "@cellajs/create-cella",
15
+ version: "0.2.1",
16
+ private: false,
17
+ license: "MIT",
18
+ description: "Cella is a TypeScript template to create web apps with sync and offline capabilities.",
19
+ publishConfig: {
20
+ access: "public"
21
+ },
22
+ repository: {
23
+ type: "git",
24
+ url: "https://github.com/cellajs/cella",
25
+ directory: "cli/create-cella"
26
+ },
27
+ homepage: "https://cellajs.com",
28
+ author: "CellaJS <info@cellajs.com>",
29
+ engines: {
30
+ node: "24.x"
31
+ },
32
+ type: "module",
33
+ main: "./dist/index.js",
34
+ files: [
35
+ "configs",
36
+ "dist",
37
+ "index.js",
38
+ "README.md"
39
+ ],
40
+ imports: {
41
+ "#/*": "./src/*"
42
+ },
43
+ bin: {
44
+ "create-cella": "index.js"
45
+ },
46
+ scripts: {
47
+ start: "tsx ./src/create-cella-cli.ts",
48
+ clean: "rimraf ./dist",
49
+ build: "tsup",
50
+ "test-build": "pnpm run build && node index.js",
51
+ prepublishOnly: "pnpm run test:release",
52
+ check: "pnpm ts && pnpm biome check --write .",
53
+ ts: "tsgo --pretty",
54
+ lint: "biome check .",
55
+ "lint:fix": "biome check --write .",
56
+ test: "vitest run",
57
+ "test:release": "pnpm run build && vitest run tests/release-artifact.test.ts",
58
+ "test:watch": "vitest"
59
+ },
60
+ dependencies: {
61
+ "@inquirer/prompts": "^8.5.2",
62
+ axios: "^1.17.0",
63
+ commander: "^15.0.0",
64
+ giget: "^3.1.2",
65
+ "nano-spawn": "^2.0.0",
66
+ ora: "^9.3.0",
67
+ picocolors: "^1.1.1",
68
+ "validate-npm-package-name": "^8.0.0"
69
+ },
70
+ devDependencies: {
71
+ "@types/node": "catalog:",
72
+ "@typescript/native-preview": "catalog:",
73
+ "@types/validate-npm-package-name": "^4.0.2",
74
+ tsup: "catalog:",
75
+ tsx: "catalog:",
76
+ vitest: "catalog:"
77
+ }
78
+ };
79
+
80
+ // src/constants.ts
81
+ var NAME = "create cella";
82
+ var DIVIDER = "\u2500".repeat(60);
83
+ var TEMPLATE_URL = "github:cellajs/cella";
84
+ var CELLA_REMOTE_URL = "git@github.com:cellajs/cella.git";
85
+ var DESCRIPTION = package_default.description;
86
+ var VERSION = package_default.version;
87
+ var AUTHOR = package_default.author;
88
+ var WEBSITE = package_default.homepage;
89
+ var GITHUB = package_default.repository.url;
90
+ function getHeaderLine(templateVersion) {
91
+ const leftText = `\u29C8 ${NAME} \xB7 v${VERSION} \xB7 cella v${templateVersion}`;
92
+ const rightText = package_default.homepage.replace("https://", "");
93
+ const left = `${pc.cyan(`\u29C8 ${NAME}`)} ${pc.dim(`\xB7 v${VERSION} \xB7 cella v${templateVersion}`)}`;
94
+ const right = pc.cyan(rightText);
95
+ const padding = Math.max(1, 60 - leftText.length - rightText.length);
96
+ return `${left}${" ".repeat(padding)}${right}`;
97
+ }
98
+ var TO_REMOVE = ["./cli/create", "./info/QUICKSTART.md"];
99
+ var TO_CLEAN = ["./backend/drizzle"];
100
+ var TO_COPY = {
101
+ "./frontend/.env.example": "./frontend/.env",
102
+ "./info/QUICKSTART.md": "README.md"
103
+ };
104
+ var PLACEHOLDER_CONFIG = "./cli/create-cella/configs/default-config.ts.template";
105
+ async function generateEnvFromExample(examplePath, replacements) {
106
+ const { readFile: readFile2 } = await import("fs/promises");
107
+ let content;
108
+ try {
109
+ content = await readFile2(examplePath, "utf8");
110
+ } catch {
111
+ return null;
112
+ }
113
+ return content.replace(/^([A-Z_][A-Z0-9_]*)=(.*)$/gm, (match, key, _value) => {
114
+ if (key in replacements) return `${key}=${replacements[key]}`;
115
+ return match;
116
+ });
117
+ }
118
+ function getRootEnvReplacements(slug, portOffset) {
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) {
126
+ const db = 5432 + portOffset;
127
+ return {
128
+ DATABASE_URL: `postgres://runtime_role:dev_password@0.0.0.0:${db}/postgres`,
129
+ DATABASE_ADMIN_URL: `postgres://postgres:postgres@0.0.0.0:${db}/postgres`,
130
+ DATABASE_CDC_URL: `postgres://admin_role:dev_password@0.0.0.0:${db}/postgres`,
131
+ ADMIN_EMAIL: adminEmail,
132
+ PORT: String(4e3 + portOffset)
133
+ };
134
+ }
135
+ function generateEnvConfigs(slug, name, portOffset) {
136
+ const fe = 3e3 + portOffset;
137
+ const be = 4e3 + portOffset;
138
+ const header = "import type { DeepPartial } from '../src/builder/types';\nimport type _default from './config.default';\n";
139
+ const envs = {
140
+ development: {
141
+ props: {
142
+ slug: `${slug}-development`,
143
+ debug: false,
144
+ domain: "",
145
+ frontendUrl: `http://localhost:${fe}`,
146
+ backendUrl: `http://localhost:${be}`,
147
+ backendAuthUrl: `http://localhost:${be}/auth`
148
+ }
149
+ },
150
+ staging: {
151
+ props: {
152
+ slug: `${slug}-staging`,
153
+ debug: false,
154
+ domain: `${slug}.example.com`,
155
+ frontendUrl: `https://staging.${slug}.example.com`,
156
+ backendUrl: `https://api-staging.${slug}.example.com`,
157
+ backendAuthUrl: `https://api-staging.${slug}.example.com/auth`
158
+ }
159
+ },
160
+ tunnel: {
161
+ props: {
162
+ frontendUrl: `https://localhost:${fe}`,
163
+ backendUrl: `https://${slug}.ngrok.dev`,
164
+ backendAuthUrl: `https://${slug}.ngrok.dev/auth`
165
+ }
166
+ },
167
+ test: {
168
+ imports: "import development from './config.development';\n",
169
+ props: {
170
+ debug: false,
171
+ domain: "",
172
+ frontendUrl: "=development.frontendUrl",
173
+ backendUrl: "=development.backendUrl",
174
+ backendAuthUrl: "=development.backendAuthUrl"
175
+ }
176
+ },
177
+ production: { props: { maintenance: false } }
178
+ };
179
+ const lit = (v) => {
180
+ if (typeof v === "boolean") return String(v);
181
+ if (v.startsWith("=")) return v.slice(1);
182
+ return `'${v}'`;
183
+ };
184
+ const result = {};
185
+ for (const [mode, { imports = "", props }] of Object.entries(envs)) {
186
+ const nameEntry = mode !== "production" ? ` name: '${name} ${mode.toUpperCase()}',
187
+ ` : "";
188
+ const body = Object.entries(props).map(([k, v]) => ` ${k}: ${lit(v)},`).join("\n");
189
+ result[`./shared/config/config.${mode}.ts`] = `${imports}${header}
190
+ export default {
191
+ mode: '${mode}',
192
+ ${nameEntry}${body}
193
+ } satisfies DeepPartial<typeof _default>;
194
+ `;
195
+ }
196
+ return result;
197
+ }
198
+
199
+ // src/create.ts
200
+ import { existsSync } from "fs";
201
+ import { cp, mkdir } from "fs/promises";
202
+ import { join, relative, resolve as resolve2 } from "path";
203
+ import { downloadTemplate } from "giget";
204
+
205
+ // src/utils/git/command.ts
206
+ import { execFile } from "child_process";
207
+ import { promisify } from "util";
208
+ var execFileAsync = promisify(execFile);
209
+ async function runGitCommand(args, repoPath, options2 = {}) {
210
+ const gitArgs = repoPath ? ["-C", repoPath, ...args] : args;
211
+ const env = {
212
+ ...process.env,
213
+ ...options2.skipEditor ? { GIT_EDITOR: "true" } : {}
214
+ };
215
+ const maxBuffer = options2.maxBuffer ?? 10 * 1024 * 1024;
216
+ const { stdout } = await execFileAsync("git", gitArgs, { env, maxBuffer });
217
+ return stdout.trim();
218
+ }
219
+ async function gitInit(repoPath) {
220
+ return runGitCommand(["init"], repoPath);
221
+ }
222
+ async function gitAddAll(repoPath) {
223
+ return runGitCommand(["add", "."], repoPath);
224
+ }
225
+ 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);
233
+ }
234
+ async function gitRemoteGetUrl(repoPath, remoteName) {
235
+ return runGitCommand(["remote", "get-url", remoteName], repoPath);
236
+ }
237
+ async function gitRemoteAdd(repoPath, remoteName, remoteUrl) {
238
+ return runGitCommand(["remote", "add", remoteName, remoteUrl], repoPath);
239
+ }
240
+ async function gitRemoteRemove(repoPath, remoteName) {
241
+ return runGitCommand(["remote", "remove", remoteName], repoPath);
242
+ }
243
+
244
+ // src/add-remote.ts
245
+ async function addRemote({
246
+ targetFolder,
247
+ remoteUrl = CELLA_REMOTE_URL,
248
+ remoteName = "upstream",
249
+ silent = false
250
+ }) {
251
+ try {
252
+ let remote = null;
253
+ try {
254
+ remote = await gitRemoteGetUrl(targetFolder, remoteName);
255
+ } catch {
256
+ remote = null;
257
+ }
258
+ if (!remote) {
259
+ await gitRemoteAdd(targetFolder, remoteName, remoteUrl);
260
+ } else if (remote !== remoteUrl) {
261
+ await gitRemoteRemove(targetFolder, remoteName);
262
+ await gitRemoteAdd(targetFolder, remoteName, remoteUrl);
263
+ }
264
+ } catch (error) {
265
+ if (!silent) {
266
+ throw error;
267
+ }
268
+ }
269
+ }
270
+
271
+ // src/modules/cli/commands.ts
272
+ import { basename, resolve } from "path";
273
+ import { Command, InvalidArgumentError } from "commander";
274
+
275
+ // src/utils/validate-project-name.ts
276
+ import validate from "validate-npm-package-name";
277
+ function validateProjectName(name) {
278
+ const nameValidation = validate(name);
279
+ if (nameValidation.validForNewPackages) {
280
+ return { valid: true };
281
+ }
282
+ return {
283
+ valid: false,
284
+ problems: [...nameValidation.errors || [], ...nameValidation.warnings || []]
285
+ };
286
+ }
287
+
288
+ // src/modules/cli/commands.ts
289
+ var directory = null;
290
+ var newBranchName = null;
291
+ 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) => {
293
+ const trimmedName = typeof name === "string" ? name.trim() : name;
294
+ if (trimmedName) {
295
+ const validation = validateProjectName(basename(resolve(trimmedName)));
296
+ if (!validation.valid) {
297
+ throw new InvalidArgumentError(`Invalid project name: ${validation.problems?.[0] ?? "unknown error"}`);
298
+ }
299
+ directory = trimmedName;
300
+ }
301
+ }).parse();
302
+ var options = command.opts();
303
+ function runCli() {
304
+ return {
305
+ options,
306
+ args: command.args,
307
+ directory,
308
+ newBranchName,
309
+ packageManager
310
+ };
311
+ }
312
+ var cli = runCli();
313
+
314
+ // src/modules/cli/display.ts
315
+ import pc2 from "picocolors";
316
+ function showAscii() {
317
+ console.info(pc2.cyan(" _ _ "));
318
+ console.info(pc2.cyan("\u2592\u2593\u2588\u2588\u2588\u2588\u2588\u2593\u2592 ___ ___| | | __ _ "));
319
+ console.info(pc2.cyan("\u2592\u2593\u2588 \u2588\u2593\u2592 / __/ _ \\ | |/ _` | "));
320
+ console.info(pc2.cyan("\u2592\u2593\u2588 \u2588\u2593\u2592 | (_| __/ | | (_| | "));
321
+ console.info(pc2.cyan("\u2592\u2593\u2588\u2588\u2588\u2588\u2588\u2593\u2592 \\___\\___|_|_|\\__,_| "));
322
+ }
323
+ function showWelcome(templateVersion) {
324
+ console.info();
325
+ showAscii();
326
+ console.info();
327
+ console.info(pc2.dim(DESCRIPTION));
328
+ console.info();
329
+ console.info(getHeaderLine(templateVersion));
330
+ console.info(DIVIDER);
331
+ }
332
+ function showSuccess(projectName, _targetFolder, relativePath, needsCd, packageManager2) {
333
+ console.info(DIVIDER);
334
+ console.info();
335
+ if (needsCd) {
336
+ console.info(`${pc2.green("\u2192")} cd ${pc2.cyan(relativePath)}`);
337
+ console.info();
338
+ }
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
+ console.info(
344
+ `${pc2.green("\u2192")} ${pc2.cyan(`${packageManager2} docker`)} ${pc2.dim("&&")} ${pc2.cyan(`${packageManager2} seed`)} ${pc2.dim("&&")} ${pc2.cyan(`${packageManager2} dev`)}`
345
+ );
346
+ console.info();
347
+ console.info(`sign in: ${pc2.gray("admin-test@cellajs.com / 12345678")}`);
348
+ console.info();
349
+ console.info(`enjoy building ${pc2.green(projectName)} with cella!`);
350
+ console.info();
351
+ }
352
+
353
+ // src/utils/clean-template.ts
354
+ import fs from "fs/promises";
355
+ import path from "path";
356
+ import pc3 from "picocolors";
357
+ var warningMark = pc3.yellow("\u26A0");
358
+ async function cleanTemplate({
359
+ targetFolder,
360
+ projectName,
361
+ displayName,
362
+ portOffset = 0,
363
+ adminEmail = `admin@${projectName}.example.com`
364
+ }) {
365
+ if (process.cwd() !== targetFolder) {
366
+ process.chdir(targetFolder);
367
+ }
368
+ return new Promise((resolve4, reject) => {
369
+ (async () => {
370
+ try {
371
+ for (const [src, dest] of Object.entries(TO_COPY)) {
372
+ const srcAbsolutePath = path.resolve(targetFolder, src);
373
+ const destAbsolutePath = path.resolve(targetFolder, dest);
374
+ await copyFile(srcAbsolutePath, destAbsolutePath);
375
+ }
376
+ await applyPlaceholderConfig(targetFolder, projectName);
377
+ const rootReplacements = getRootEnvReplacements(projectName, portOffset);
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);
385
+ const backendEnv = await generateEnvFromExample(
386
+ path.resolve(targetFolder, "backend/.env.example"),
387
+ backendReplacements
388
+ );
389
+ if (backendEnv) {
390
+ await fs.writeFile(path.resolve(targetFolder, "backend/.env"), backendEnv, "utf8");
391
+ }
392
+ const envConfigs = generateEnvConfigs(projectName, displayName, portOffset);
393
+ await Promise.all(
394
+ Object.entries(envConfigs).map(
395
+ ([filePath, content]) => fs.writeFile(path.resolve(targetFolder, filePath), content, "utf8")
396
+ )
397
+ );
398
+ await Promise.all(
399
+ TO_CLEAN.map((folderPath) => {
400
+ const absolutePath = path.resolve(targetFolder, folderPath);
401
+ return removeFolderContents(absolutePath);
402
+ })
403
+ );
404
+ await Promise.all(
405
+ TO_REMOVE.map((filePath) => {
406
+ const absolutePath = path.resolve(targetFolder, filePath);
407
+ return removeFileOrFolder(absolutePath);
408
+ })
409
+ );
410
+ resolve4();
411
+ } catch (err) {
412
+ reject(`Error during the cleaning process: ${err}`);
413
+ }
414
+ })();
415
+ });
416
+ }
417
+ async function removeFolderContents(folderPath) {
418
+ const files = await fs.readdir(folderPath);
419
+ await Promise.all(
420
+ files.map(async (file) => {
421
+ const filePath = path.join(folderPath, file);
422
+ const stat = await fs.lstat(filePath);
423
+ if (stat.isDirectory()) {
424
+ await fs.rm(filePath, { recursive: true, force: true });
425
+ } else {
426
+ await fs.rm(filePath);
427
+ }
428
+ })
429
+ );
430
+ }
431
+ async function removeFileOrFolder(pathToRemove) {
432
+ await fs.rm(pathToRemove, { recursive: true, force: true });
433
+ }
434
+ async function copyFile(src, dest) {
435
+ try {
436
+ await fs.access(src);
437
+ await fs.mkdir(path.dirname(dest), { recursive: true });
438
+ await fs.copyFile(src, dest);
439
+ } catch (err) {
440
+ if (err.code === "ENOENT") {
441
+ console.info(`
442
+ ${warningMark} Source file "${src}" does not exist > Skip copy`);
443
+ } else {
444
+ throw err;
445
+ }
446
+ }
447
+ }
448
+ async function applyPlaceholderConfig(targetFolder, projectName) {
449
+ const displayName = projectName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
450
+ const src = path.resolve(targetFolder, PLACEHOLDER_CONFIG);
451
+ const dest = path.resolve(targetFolder, "./shared/config/config.default.ts");
452
+ try {
453
+ let content = await fs.readFile(src, "utf8");
454
+ content = content.replaceAll("__project_name__", displayName);
455
+ content = content.replaceAll("__project_slug__", projectName);
456
+ await fs.writeFile(dest, content, "utf8");
457
+ } catch (err) {
458
+ if (err.code === "ENOENT") {
459
+ console.info(`
460
+ ${warningMark} Placeholder config "${src}" not found > Skip`);
461
+ } else {
462
+ throw err;
463
+ }
464
+ }
465
+ }
466
+
467
+ // src/utils/progress.ts
468
+ import ora from "ora";
469
+ import pc4 from "picocolors";
470
+ var activeSpinner = null;
471
+ function createProgress(title, silent = false) {
472
+ const completedSteps = [];
473
+ const spinner = ora({
474
+ text: title,
475
+ isSilent: silent
476
+ });
477
+ activeSpinner = spinner;
478
+ spinner.start();
479
+ const log = (msg) => {
480
+ if (!silent) console.info(msg);
481
+ };
482
+ return {
483
+ step: (message) => {
484
+ if (completedSteps.length > 0) {
485
+ spinner.stop();
486
+ log(`${pc4.green("\u2713")} ${completedSteps[completedSteps.length - 1]}`);
487
+ }
488
+ completedSteps.push(message);
489
+ spinner.text = message;
490
+ spinner.start();
491
+ },
492
+ done: (message) => {
493
+ if (completedSteps.length > 0) {
494
+ spinner.stop();
495
+ log(`${pc4.green("\u2713")} ${completedSteps[completedSteps.length - 1]}`);
496
+ } else {
497
+ spinner.stop();
498
+ }
499
+ activeSpinner = null;
500
+ if (message) {
501
+ log("");
502
+ log(`${pc4.green("\u2713")} ${message}`);
503
+ }
504
+ },
505
+ fail: (message) => {
506
+ spinner.stop();
507
+ activeSpinner = null;
508
+ 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
+ }
526
+ };
527
+ }
528
+
529
+ // src/utils/run-package-manager-command.ts
530
+ import spawn from "nano-spawn";
531
+ async function runPackageManagerCommand(packageManager2, args, env = {}) {
532
+ try {
533
+ await spawn(packageManager2, args, {
534
+ env: { ...process.env, ...env },
535
+ stdio: ["pipe", "pipe", "pipe"]
536
+ });
537
+ } catch (error) {
538
+ const err = error;
539
+ throw new Error(`"${packageManager2} ${args.join(" ")}" failed ${err.stdout || ""} ${err.stderr || ""}`);
540
+ }
541
+ }
542
+ async function install(packageManager2) {
543
+ return runPackageManagerCommand(packageManager2, ["install"], {
544
+ NODE_ENV: "development"
545
+ });
546
+ }
547
+ async function generate(packageManager2) {
548
+ return runPackageManagerCommand(packageManager2, ["generate"], {
549
+ NODE_ENV: "development"
550
+ });
551
+ }
552
+
553
+ // src/create.ts
554
+ function isLocalPath(path2) {
555
+ return path2.startsWith("/") || path2.startsWith("./") || path2.startsWith("../");
556
+ }
557
+ async function create({
558
+ projectName,
559
+ targetFolder,
560
+ newBranchName: newBranchName2,
561
+ packageManager: packageManager2,
562
+ templateUrl,
563
+ portOffset,
564
+ adminEmail,
565
+ silent = false
566
+ }) {
567
+ const originalCwd = process.cwd();
568
+ const template = templateUrl || TEMPLATE_URL;
569
+ const isLocalTemplate = templateUrl && isLocalPath(templateUrl);
570
+ const progress = createProgress("creating project", silent);
571
+ try {
572
+ progress.step("creating project folder");
573
+ await mkdir(targetFolder, { recursive: true });
574
+ process.chdir(targetFolder);
575
+ if (isLocalTemplate) {
576
+ progress.step("copying local template");
577
+ const sourcePath = resolve2(originalCwd, templateUrl);
578
+ if (targetFolder.startsWith(sourcePath)) {
579
+ throw new Error(
580
+ `Cannot create project inside the template directory.
581
+ Run from outside: cd ~ && pnpm create @cellajs/cella ${projectName} --template ${templateUrl}`
582
+ );
583
+ }
584
+ await cp(sourcePath, targetFolder, {
585
+ recursive: true,
586
+ filter: (src) => !src.includes("node_modules") && !src.includes(".git")
587
+ });
588
+ } else {
589
+ progress.step("downloading cella template");
590
+ await downloadTemplate(template, {
591
+ cwd: process.cwd(),
592
+ dir: targetFolder,
593
+ force: true,
594
+ provider: "github"
595
+ });
596
+ }
597
+ progress.step("cleaning template");
598
+ const displayName = projectName.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
599
+ await cleanTemplate({ targetFolder, projectName, displayName, portOffset, adminEmail });
600
+ progress.step("installing dependencies");
601
+ await install(packageManager2);
602
+ progress.step("generating migrations");
603
+ await generate(packageManager2);
604
+ const gitFolderPath = join(targetFolder, ".git");
605
+ if (!existsSync(gitFolderPath)) {
606
+ progress.step("initializing git");
607
+ await gitInit(targetFolder);
608
+ await gitAddAll(targetFolder);
609
+ 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
+ }
616
+ progress.step("adding upstream remote");
617
+ await addRemote({ targetFolder, silent: true });
618
+ progress.done(`created ${projectName}`);
619
+ } catch (error) {
620
+ progress.fail(error instanceof Error ? error.message : String(error));
621
+ process.exit(1);
622
+ }
623
+ const needsCd = originalCwd !== targetFolder;
624
+ const relativePath = relative(originalCwd, targetFolder);
625
+ showSuccess(projectName, targetFolder, relativePath, needsCd, packageManager2);
626
+ }
627
+
628
+ // src/utils/detect-used-ports.ts
629
+ import { readdir, readFile } from "fs/promises";
630
+ import { dirname, join as join2 } from "path";
631
+ async function detectUsedPorts(targetFolder) {
632
+ const parentDir = dirname(targetFolder);
633
+ const used = [];
634
+ let siblings;
635
+ try {
636
+ siblings = await readdir(parentDir);
637
+ } catch {
638
+ return used;
639
+ }
640
+ for (const name of siblings) {
641
+ const configPath = join2(parentDir, name, "shared/config/config.development.ts");
642
+ try {
643
+ const content = await readFile(configPath, "utf8");
644
+ const feMatch = content.match(/frontendUrl:\s*'http:\/\/localhost:(\d+)'/);
645
+ const beMatch = content.match(/backendUrl:\s*'http:\/\/localhost:(\d+)'/);
646
+ if (feMatch && beMatch) {
647
+ const frontend = Number(feMatch[1]);
648
+ const backend = Number(beMatch[1]);
649
+ used.push({
650
+ project: name,
651
+ frontend,
652
+ backend,
653
+ offset: frontend - 3e3
654
+ });
655
+ }
656
+ } catch {
657
+ }
658
+ }
659
+ return used;
660
+ }
661
+ function findNextOffset(usedPorts) {
662
+ const usedOffsets = new Set(usedPorts.map((p) => p.offset));
663
+ for (let offset = 0; offset <= 490; offset += 10) {
664
+ if (!usedOffsets.has(offset)) return offset;
665
+ }
666
+ return 0;
667
+ }
668
+
669
+ // src/utils/extract-package-json-version-from-uri.ts
670
+ import axios from "axios";
671
+ async function extractPackageJsonVersionFromUri(repositoryUrl, branch = "main") {
672
+ const [owner, repo] = repositoryUrl.replace("github:", "").split("/");
673
+ const packageJsonUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/package.json`;
674
+ try {
675
+ const response = await axios.get(packageJsonUrl);
676
+ const packageJson = response.data;
677
+ return packageJson.version || "unknown";
678
+ } catch (error) {
679
+ return "unknown";
680
+ }
681
+ }
682
+
683
+ // src/utils/is-empty-directory.ts
684
+ import { readdir as readdir2 } from "fs/promises";
685
+ async function isEmptyDirectory(path2) {
686
+ const files = await readdir2(path2);
687
+ return files.length === 0 || files.length === 1 && files[0] === ".git";
688
+ }
689
+
690
+ // src/create-cella-cli.ts
691
+ async function main() {
692
+ const templateVersion = await extractPackageJsonVersionFromUri(TEMPLATE_URL);
693
+ showWelcome(templateVersion);
694
+ const promptTheme = { prefix: "", style: { answer: (text) => text } };
695
+ const promptContext = { clearPromptOnDone: true };
696
+ if (!cli.directory) {
697
+ cli.directory = await input(
698
+ {
699
+ message: "Enter your project name",
700
+ default: "my-cella-app",
701
+ theme: promptTheme,
702
+ validate: (name) => {
703
+ const validation = validateProjectName(basename2(resolve3(name)));
704
+ return validation.valid ? true : `Invalid project name: ${validation.problems?.[0] ?? "unknown error"}`;
705
+ }
706
+ },
707
+ promptContext
708
+ );
709
+ }
710
+ if (!cli.newBranchName) {
711
+ cli.newBranchName = "development";
712
+ }
713
+ const targetFolder = resolve3(cli.directory);
714
+ const projectName = basename2(targetFolder)?.toLowerCase() || "project";
715
+ if (existsSync2(targetFolder) && !await isEmptyDirectory(targetFolder)) {
716
+ const dirName = cli.directory === "." ? "Current directory" : `Target directory "${targetFolder}"`;
717
+ const message = `${dirName} is not empty. Please choose how you would like to proceed:`;
718
+ const action = await select(
719
+ {
720
+ message,
721
+ theme: promptTheme,
722
+ choices: [
723
+ { name: "Cancel and exit", value: "cancel" },
724
+ { name: "Ignore existing files and continue", value: "ignore" }
725
+ ]
726
+ },
727
+ promptContext
728
+ );
729
+ if (action === "cancel") {
730
+ process.exit(1);
731
+ }
732
+ }
733
+ const portOffset = await promptPortOffset(targetFolder, promptTheme, promptContext);
734
+ const adminEmail = await input(
735
+ {
736
+ message: "Admin email for initial seed user",
737
+ default: `admin@${projectName}.com`,
738
+ theme: promptTheme
739
+ },
740
+ promptContext
741
+ );
742
+ const createOptions = {
743
+ projectName,
744
+ targetFolder,
745
+ newBranchName: cli.newBranchName,
746
+ packageManager: cli.packageManager,
747
+ templateUrl: cli.options.template,
748
+ portOffset,
749
+ adminEmail
750
+ };
751
+ await create(createOptions);
752
+ }
753
+ main().catch(console.error);
754
+ function formatOffset(o, suffix = "") {
755
+ return `${o} \u2192 :${3e3 + o} / :${4e3 + o} / :${5432 + o}${suffix}`;
756
+ }
757
+ async function promptPortOffset(targetFolder, theme, context) {
758
+ const usedPorts = await detectUsedPorts(targetFolder);
759
+ const suggested = findNextOffset(usedPorts);
760
+ if (usedPorts.length > 0) {
761
+ console.info(pc5.dim("\nDetected cella forks in sibling directories:"));
762
+ for (const p of usedPorts) {
763
+ console.info(pc5.dim(` ${p.project}: frontend :${p.frontend}, backend :${p.backend} (offset ${p.offset})`));
764
+ }
765
+ console.info();
766
+ }
767
+ const presets = [0, 10, 20, 30].filter((o) => o !== suggested);
768
+ const choice = await select(
769
+ {
770
+ message: "Port offset (avoids conflicts with sibling forks)",
771
+ theme,
772
+ choices: [
773
+ ...suggested > 0 ? [{ name: formatOffset(suggested, " (suggested)"), value: suggested }] : [],
774
+ { name: formatOffset(0, " (default)"), value: 0 },
775
+ ...presets.map((o) => ({ name: formatOffset(o), value: o })),
776
+ { name: "Custom offset", value: -1 }
777
+ ]
778
+ },
779
+ context
780
+ );
781
+ if (choice !== -1) return choice;
782
+ const custom = await input(
783
+ {
784
+ message: "Enter custom offset (0-490, multiples of 10)",
785
+ default: String(suggested),
786
+ theme,
787
+ validate: (val) => {
788
+ const n = Number(val);
789
+ if (Number.isNaN(n) || n < 0 || n > 490) return "Must be between 0 and 490";
790
+ if (n % 10 !== 0) return "Must be a multiple of 10";
791
+ return true;
792
+ }
793
+ },
794
+ context
795
+ );
796
+ return Number(custom);
797
+ }
798
+ //# sourceMappingURL=index.js.map