@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/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 pc5 from "picocolors";
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.1",
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-artifact.test.ts",
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.1.2",
64
+ giget: "^3.3.0",
65
65
  "nano-spawn": "^2.0.0",
66
- ora: "^9.3.0",
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": "catalog:",
72
- "@typescript/native-preview": "catalog:",
71
+ "@types/node": "24.10.13",
73
72
  "@types/validate-npm-package-name": "^4.0.2",
74
- tsup: "catalog:",
75
- tsx: "catalog:",
76
- vitest: "catalog:"
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 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);
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 = "./cli/create-cella/configs/default-config.ts.template";
102
+ var PLACEHOLDER_CONFIG = "configs/default-config.ts.template";
105
103
  async function generateEnvFromExample(examplePath, replacements) {
106
- const { readFile: readFile2 } = await import("fs/promises");
104
+ const { readFile: readFile3 } = await import("fs/promises");
107
105
  let content;
108
106
  try {
109
- content = await readFile2(examplePath, "utf8");
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 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) {
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.green("\u2192")} cd ${pc2.cyan(relativePath)}`);
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.green("\u2192")} ${pc2.cyan(`${packageManager2} docker`)} ${pc2.dim("&&")} ${pc2.cyan(`${packageManager2} seed`)} ${pc2.dim("&&")} ${pc2.cyan(`${packageManager2} dev`)}`
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
- console.info(`sign in: ${pc2.gray("admin-test@cellajs.com / 12345678")}`);
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(`enjoy building ${pc2.green(projectName)} with cella!`);
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 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);
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 = path.resolve(targetFolder, PLACEHOLDER_CONFIG);
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
- if (targetFolder.startsWith(sourcePath)) {
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
- await cp(sourcePath, targetFolder, {
585
- recursive: true,
586
- filter: (src) => !src.includes("node_modules") && !src.includes(".git")
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
- progress.step("downloading cella template");
590
- await downloadTemplate(template, {
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
- 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)) {
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
- progress.step("adding upstream remote");
617
- await addRemote({ targetFolder, silent: true });
618
- progress.done(`created ${projectName}`);
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
- process.exit(1);
685
+ throw error;
622
686
  }
623
687
  const needsCd = originalCwd !== targetFolder;
624
- const relativePath = relative(originalCwd, targetFolder);
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 join2 } from "path";
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 = dirname(targetFolder);
723
+ const parentDir = dirname3(targetFolder);
633
724
  const used = [];
634
725
  let siblings;
635
726
  try {
636
- siblings = await readdir(parentDir);
727
+ siblings = await readdir2(parentDir);
637
728
  } catch {
638
729
  return used;
639
730
  }
640
731
  for (const name of siblings) {
641
- const configPath = join2(parentDir, name, "shared/config/config.development.ts");
732
+ const configPath = join3(parentDir, name, "shared/config/config.development.ts");
642
733
  try {
643
- const content = await readFile(configPath, "utf8");
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 packageJsonUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/package.json`;
763
+ const apiUrl = `https://api.github.com/repos/${owner}/${repo}/contents/package.json?ref=${branch}`;
674
764
  try {
675
- const response = await axios.get(packageJsonUrl);
676
- const packageJson = response.data;
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 (error) {
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 readdir2 } from "fs/promises";
815
+ import { readdir as readdir3 } from "fs/promises";
685
816
  async function isEmptyDirectory(path2) {
686
- const files = await readdir2(path2);
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
- const adminEmail = await input(
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: `admin@${projectName}.com`,
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(console.error);
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
- 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();
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 presets = [0, 10, 20, 30].filter((o) => o !== suggested);
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: "Port offset (avoids conflicts with sibling forks)",
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 },