@cellajs/create-cella 0.1.7 → 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 CHANGED
@@ -1,19 +1,18 @@
1
1
  #!/usr/bin/env tsx
2
2
 
3
- // src/index.ts
4
- import { basename as basename2, resolve as resolve2 } from "node:path";
5
- import { existsSync as existsSync2 } from "node:fs";
6
- import colors3 from "picocolors";
7
- import { input, confirm, select } from "@inquirer/prompts";
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
8
 
9
- // src/cli.ts
10
- import { basename, resolve } from "node:path";
11
- import { Command, InvalidArgumentError } from "commander";
9
+ // src/constants.ts
10
+ import pc from "picocolors";
12
11
 
13
12
  // package.json
14
13
  var package_default = {
15
14
  name: "@cellajs/create-cella",
16
- version: "0.1.7",
15
+ version: "0.2.1",
17
16
  private: false,
18
17
  license: "MIT",
19
18
  description: "Cella is a TypeScript template to create web apps with sync and offline capabilities.",
@@ -28,39 +27,59 @@ var package_default = {
28
27
  homepage: "https://cellajs.com",
29
28
  author: "CellaJS <info@cellajs.com>",
30
29
  engines: {
31
- node: ">=20.14.0"
30
+ node: "24.x"
32
31
  },
33
32
  type: "module",
34
- main: "./src/index.ts",
33
+ main: "./dist/index.js",
34
+ files: [
35
+ "configs",
36
+ "dist",
37
+ "index.js",
38
+ "README.md"
39
+ ],
40
+ imports: {
41
+ "#/*": "./src/*"
42
+ },
35
43
  bin: {
36
44
  "create-cella": "index.js"
37
45
  },
38
46
  scripts: {
39
- start: "tsx ./src/index.ts",
47
+ start: "tsx ./src/create-cella-cli.ts",
40
48
  clean: "rimraf ./dist",
41
49
  build: "tsup",
42
50
  "test-build": "pnpm run build && node index.js",
43
- prepublishOnly: "pnpm run build"
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"
44
59
  },
45
- packageManager: "pnpm@9.11.0",
46
60
  dependencies: {
47
- "@inquirer/prompts": "^6.0.1",
48
- axios: "^1.8.3",
49
- commander: "^12.1.0",
50
- "cross-spawn": "^7.0.3",
51
- giget: "^1.2.3",
52
- picocolors: "^1.1.0",
53
- "validate-npm-package-name": "^5.0.1",
54
- "yocto-spinner": "^0.1.0"
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"
55
69
  },
56
70
  devDependencies: {
57
- tsup: "^8.3.5",
58
- tsx: "^4.19.2"
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:"
59
77
  }
60
78
  };
61
79
 
62
80
  // src/constants.ts
63
- var NAME = "create-cella";
81
+ var NAME = "create cella";
82
+ var DIVIDER = "\u2500".repeat(60);
64
83
  var TEMPLATE_URL = "github:cellajs/cella";
65
84
  var CELLA_REMOTE_URL = "git@github.com:cellajs/cella.git";
66
85
  var DESCRIPTION = package_default.description;
@@ -68,41 +87,190 @@ var VERSION = package_default.version;
68
87
  var AUTHOR = package_default.author;
69
88
  var WEBSITE = package_default.homepage;
70
89
  var GITHUB = package_default.repository.url;
71
- var TO_REMOVE = [
72
- "info",
73
- "./cli/create-cella"
74
- ];
75
- var TO_CLEAN = [
76
- "./backend/drizzle"
77
- ];
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"];
78
100
  var TO_COPY = {
79
- "./backend/.env.example": "./backend/.env",
80
101
  "./frontend/.env.example": "./frontend/.env",
81
102
  "./info/QUICKSTART.md": "README.md"
82
103
  };
83
- var TO_EDIT = {
84
- "./config/default.ts": [
85
- {
86
- regexMatch: /enabledAuthenticationStrategies:\s*\[[^\]]+\]\s*as\s*const,/g,
87
- replaceWith: "enabledAuthenticationStrategies: ['password', 'passkey'] as const,"
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
+ }
88
149
  },
89
- {
90
- regexMatch: /uploadEnabled\:\s*(true|false),/g,
91
- replaceWith: "uploadEnabled: false,"
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
+ }
92
159
  },
93
- {
94
- regexMatch: /enabledOauthProviders:\s*\[[^\]]+\]\s*as\s*const,/g,
95
- replaceWith: "enabledOauthProviders: [] as const,"
96
- }
97
- ]
98
- };
99
- var LOGO = `
100
- _ _
101
- \u2592\u2593\u2588\u2588\u2588\u2588\u2588\u2593\u2592 ___ ___| | | __ _
102
- \u2592\u2593\u2588 \u2588\u2593\u2592 / __/ _ \\ | |/ _\` |
103
- \u2592\u2593\u2588 \u2588\u2593\u2592 | (_| __/ | | (_| |
104
- \u2592\u2593\u2588\u2588\u2588\u2588\u2588\u2593\u2592 \\___\\___|_|_|\\__,_|
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>;
105
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";
106
274
 
107
275
  // src/utils/validate-project-name.ts
108
276
  import validate from "validate-npm-package-name";
@@ -113,180 +281,137 @@ function validateProjectName(name) {
113
281
  }
114
282
  return {
115
283
  valid: false,
116
- problems: [
117
- ...nameValidation.errors || [],
118
- ...nameValidation.warnings || []
119
- ]
284
+ problems: [...nameValidation.errors || [], ...nameValidation.warnings || []]
120
285
  };
121
286
  }
122
287
 
123
- // src/cli.ts
288
+ // src/modules/cli/commands.ts
124
289
  var directory = null;
125
290
  var newBranchName = null;
126
- var createNewBranch = null;
127
291
  var packageManager = "pnpm";
128
- var command = new Command(NAME).version(
129
- VERSION,
130
- "-v, --version",
131
- `Output the current version of ${NAME}.`
132
- ).argument("[directory]", "The directory name for the new project.").usage("[directory] [options]").helpOption("-h, --help", "Display this help message.").option("--skip-new-branch", "Skip creating a new branch during initialization.", false).option("--skip-install", "Skip the installation of packages.", false).option("--skip-generate", "Skip generating SQL files.", false).option("--skip-clean", "Skip cleaning the `cella` template.", false).option("--skip-git", "Skip initializing a git repository.", false).option(
133
- "--new-branch-name <name>",
134
- "Specify a new branch name to create and use.",
135
- (name) => {
136
- if (typeof name === "string") {
137
- name = name.trim();
138
- }
139
- if (name) {
140
- const validation = validateProjectName(basename(resolve(name)));
141
- if (!validation.valid) {
142
- throw new InvalidArgumentError(
143
- `Invalid branch name: ${validation.problems[0]}`
144
- );
145
- }
146
- createNewBranch = true;
147
- newBranchName = name;
148
- }
149
- }
150
- ).action((name) => {
151
- if (typeof name === "string") {
152
- name = name.trim();
153
- }
154
- if (name) {
155
- const validation = validateProjectName(basename(resolve(name)));
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)));
156
296
  if (!validation.valid) {
157
- throw new InvalidArgumentError(
158
- `Invalid project name: ${validation.problems[0]}`
159
- );
297
+ throw new InvalidArgumentError(`Invalid project name: ${validation.problems?.[0] ?? "unknown error"}`);
160
298
  }
161
- directory = name;
299
+ directory = trimmedName;
162
300
  }
163
301
  }).parse();
164
- var options = command.opts({
165
- skipNewBranch: false,
166
- skipClean: false,
167
- skipGit: false,
168
- skipInstall: false,
169
- skipGenerate: false
170
- });
171
- var cli = {
172
- options,
173
- args: command.args,
174
- directory,
175
- newBranchName,
176
- createNewBranch,
177
- packageManager
178
- };
179
-
180
- // src/utils/extract-package-json-version-from-uri.ts
181
- import axios from "axios";
182
- async function extractPackageJsonVersionFromUri(repositoryUrl, branch = "main") {
183
- const [owner, repo] = repositoryUrl.replace("github:", "").split("/");
184
- const packageJsonUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/package.json`;
185
- try {
186
- const response = await axios.get(packageJsonUrl);
187
- const packageJson = response.data;
188
- return packageJson.version || "unknown";
189
- } catch (error) {
190
- return "unknown";
191
- }
192
- }
193
-
194
- // src/utils/is-empty-directory.ts
195
- import { readdir } from "node:fs/promises";
196
- async function isEmptyDirectory(path2) {
197
- const files = await readdir(path2);
198
- return files.length === 0 || files.length === 1 && files[0] === ".git";
302
+ var options = command.opts();
303
+ function runCli() {
304
+ return {
305
+ options,
306
+ args: command.args,
307
+ directory,
308
+ newBranchName,
309
+ packageManager
310
+ };
199
311
  }
312
+ var cli = runCli();
200
313
 
201
- // src/create.ts
202
- import { mkdir } from "node:fs/promises";
203
- import { existsSync } from "node:fs";
204
- import { join, relative } from "node:path";
205
- import colors2 from "picocolors";
206
- import { downloadTemplate } from "giget";
207
- import yoctoSpinner2 from "yocto-spinner";
208
-
209
- // src/utils/run-package-manager-command.ts
210
- import spawn from "cross-spawn";
211
- async function runPackageManagerCommand(packageManager2, args, env = {}) {
212
- return new Promise((resolve3, reject) => {
213
- const child = spawn(packageManager2, args, {
214
- env: {
215
- ...process.env,
216
- ...env
217
- },
218
- stdio: ["pipe", "pipe", "pipe"]
219
- });
220
- let stderrBuffer = "";
221
- let stdoutBuffer = "";
222
- child.stderr?.on("data", (data) => {
223
- stderrBuffer += data.toString();
224
- });
225
- child.stdout?.on("data", (data) => {
226
- stdoutBuffer += data.toString();
227
- });
228
- child.on("close", (code) => {
229
- if (code !== 0) {
230
- reject(
231
- `"${packageManager2} ${args.join(" ")}" failed ${stdoutBuffer} ${stderrBuffer}`
232
- );
233
- return;
234
- }
235
- resolve3();
236
- });
237
- });
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 \\___\\___|_|_|\\__,_| "));
238
322
  }
239
- async function install(packageManager2) {
240
- return runPackageManagerCommand(packageManager2, ["install"], {
241
- NODE_ENV: "development"
242
- });
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);
243
331
  }
244
- async function generate(packageManager2) {
245
- return runPackageManagerCommand(packageManager2, ["generate"], {
246
- NODE_ENV: "development"
247
- });
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();
248
351
  }
249
352
 
250
353
  // src/utils/clean-template.ts
251
- import fs from "node:fs/promises";
252
- import path from "node:path";
253
- import colors from "picocolors";
354
+ import fs from "fs/promises";
355
+ import path from "path";
356
+ import pc3 from "picocolors";
357
+ var warningMark = pc3.yellow("\u26A0");
254
358
  async function cleanTemplate({
255
359
  targetFolder,
256
- projectName
360
+ projectName,
361
+ displayName,
362
+ portOffset = 0,
363
+ adminEmail = `admin@${projectName}.example.com`
257
364
  }) {
258
365
  if (process.cwd() !== targetFolder) {
259
366
  process.chdir(targetFolder);
260
367
  }
261
- return new Promise(async (resolve3, reject) => {
262
- try {
263
- for (const [src, dest] of Object.entries(TO_COPY)) {
264
- const srcAbsolutePath = path.resolve(targetFolder, src);
265
- const destAbsolutePath = path.resolve(targetFolder, dest);
266
- await copyFile(srcAbsolutePath, destAbsolutePath);
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}`);
267
413
  }
268
- await Promise.all(
269
- TO_CLEAN.map((folderPath) => {
270
- const absolutePath = path.resolve(targetFolder, folderPath);
271
- return removeFolderContents(absolutePath);
272
- })
273
- );
274
- await Promise.all(
275
- TO_REMOVE.map((filePath) => {
276
- const absolutePath = path.resolve(targetFolder, filePath);
277
- return removeFileOrFolder(absolutePath);
278
- })
279
- );
280
- await Promise.all(
281
- Object.entries(TO_EDIT).map(async ([filePath, edits]) => {
282
- const absolutePath = path.resolve(targetFolder, filePath);
283
- await editFile(absolutePath, edits);
284
- })
285
- );
286
- resolve3();
287
- } catch (err) {
288
- reject(`Error during the cleaning process: ${err}`);
289
- }
414
+ })();
290
415
  });
291
416
  }
292
417
  async function removeFolderContents(folderPath) {
@@ -314,320 +439,360 @@ async function copyFile(src, dest) {
314
439
  } catch (err) {
315
440
  if (err.code === "ENOENT") {
316
441
  console.info(`
317
- ${colors.yellow("\u26A0")} Source file "${src}" does not exist > Skip copy`);
442
+ ${warningMark} Source file "${src}" does not exist > Skip copy`);
318
443
  } else {
319
444
  throw err;
320
445
  }
321
446
  }
322
447
  }
323
- async function editFile(filePath, edits) {
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");
324
452
  try {
325
- await fs.access(filePath);
326
- const fileContent = await fs.readFile(filePath, "utf8");
327
- let updatedContent = fileContent;
328
- edits.forEach(({ regexMatch, replaceWith }) => {
329
- updatedContent = updatedContent.replace(regexMatch, replaceWith);
330
- });
331
- if (fileContent !== updatedContent) {
332
- await fs.writeFile(filePath, updatedContent, "utf8");
333
- }
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");
334
457
  } catch (err) {
335
458
  if (err.code === "ENOENT") {
336
459
  console.info(`
337
- ${colors.yellow("\u26A0")} Source file "${filePath}" does not exist > Skip edit`);
460
+ ${warningMark} Placeholder config "${src}" not found > Skip`);
338
461
  } else {
339
462
  throw err;
340
463
  }
341
464
  }
342
465
  }
343
466
 
344
- // src/utils/run-git-command.ts
345
- import { spawn as spawn2 } from "node:child_process";
346
- function getGitCommandType(command2) {
347
- if (command2.startsWith("merge")) return "merge";
348
- if (command2.startsWith("diff")) return "diff";
349
- return "other";
350
- }
351
- function isGitCommandSuccess(gitCommand, code, errOutput) {
352
- if (gitCommand === "merge") return code === 0 || code === 1 && !errOutput;
353
- if (gitCommand === "diff") return code === 0 || code === 2 && !errOutput;
354
- return code === 0;
355
- }
356
- async function runGitCommand({ targetFolder, command: command2 }) {
357
- return new Promise((resolve3, reject) => {
358
- const gitCommand = getGitCommandType(command2);
359
- const child = spawn2(`git ${command2}`, [], { cwd: targetFolder, shell: true, timeout: 6e4 });
360
- let output = "";
361
- let errOutput = "";
362
- child.on("error", (error) => {
363
- reject(error);
364
- });
365
- child.on("exit", (code) => {
366
- if (isGitCommandSuccess(gitCommand, code, errOutput)) {
367
- resolve3(output.trim());
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]}`);
368
496
  } else {
369
- reject(
370
- `Git ${gitCommand} command failed with exit code ${code}, stderr: "${errOutput.trim()}", stdout: "${output.trim()}"`
371
- );
497
+ spinner.stop();
372
498
  }
373
- });
374
- child.stdout.on("data", (data) => {
375
- output += data.toString();
376
- });
377
- child.stderr.on("data", (data) => {
378
- errOutput += data.toString();
379
- });
380
- });
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
+ };
381
527
  }
382
528
 
383
- // src/add-remote.ts
384
- import yoctoSpinner from "yocto-spinner";
385
- async function addRemote({
386
- targetFolder,
387
- remoteUrl = CELLA_REMOTE_URL,
388
- remoteName = "upstream"
389
- }) {
390
- const remoteSpinner = yoctoSpinner({
391
- text: "Adding remote"
392
- }).start();
529
+ // src/utils/run-package-manager-command.ts
530
+ import spawn from "nano-spawn";
531
+ async function runPackageManagerCommand(packageManager2, args, env = {}) {
393
532
  try {
394
- let remote = null;
395
- try {
396
- remote = await runGitCommand({ targetFolder, command: `remote get-url ${remoteName}` });
397
- } catch (error) {
398
- remote = null;
399
- }
400
- if (!remote) {
401
- await runGitCommand({ targetFolder, command: `remote add ${remoteName} ${remoteUrl}` });
402
- remoteSpinner.success("Remote added successfully.");
403
- } else if (remote !== remoteUrl) {
404
- await runGitCommand({ targetFolder, command: `remote remove ${remoteName}` });
405
- await runGitCommand({ targetFolder, command: `remote add ${remoteName} ${remoteUrl}` });
406
- remoteSpinner.success("Remote updated successfully.");
407
- } else {
408
- remoteSpinner.success("Remote is already configured correctly.");
409
- }
533
+ await spawn(packageManager2, args, {
534
+ env: { ...process.env, ...env },
535
+ stdio: ["pipe", "pipe", "pipe"]
536
+ });
410
537
  } catch (error) {
411
- console.error(error);
412
- remoteSpinner.error("Failed to add remote.");
413
- process.exit(1);
538
+ const err = error;
539
+ throw new Error(`"${packageManager2} ${args.join(" ")}" failed ${err.stdout || ""} ${err.stderr || ""}`);
414
540
  }
415
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
+ }
416
552
 
417
553
  // src/create.ts
554
+ function isLocalPath(path2) {
555
+ return path2.startsWith("/") || path2.startsWith("./") || path2.startsWith("../");
556
+ }
418
557
  async function create({
419
558
  projectName,
420
559
  targetFolder,
421
560
  newBranchName: newBranchName2,
422
- skipInstall,
423
- skipGit,
424
- skipClean,
425
- skipGenerate,
426
- packageManager: packageManager2
561
+ packageManager: packageManager2,
562
+ templateUrl,
563
+ portOffset,
564
+ adminEmail,
565
+ silent = false
427
566
  }) {
428
567
  const originalCwd = process.cwd();
429
- console.info();
430
- const createFolderSpinner = yoctoSpinner2({
431
- text: "Creating project folder"
432
- }).start();
433
- await mkdir(targetFolder, { recursive: true });
434
- process.chdir(targetFolder);
435
- createFolderSpinner.success("Project folder created");
436
- const downloadSpinner = yoctoSpinner2({
437
- text: "Downloading `cella` template"
438
- }).start();
439
- await downloadTemplate(TEMPLATE_URL, {
440
- cwd: process.cwd(),
441
- dir: targetFolder,
442
- force: true,
443
- provider: "github"
444
- });
445
- downloadSpinner.success("`cella` template downloaded");
446
- if (!skipClean) {
447
- const cleanSpinner = yoctoSpinner2({
448
- text: "cleaning `cella` template"
449
- }).start();
450
- try {
451
- await cleanTemplate({
452
- targetFolder,
453
- projectName
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"
454
595
  });
455
- cleanSpinner.success("`cella` template cleaned");
456
- } catch (e) {
457
- console.error(e);
458
- cleanSpinner.error("Failed to clean `cella` template");
459
- process.exit(1);
460
- }
461
- } else {
462
- console.info(`${colors2.yellow("\u26A0")} --skip-clean > Skip cleaning \`cella\` template`);
463
- }
464
- if (!skipInstall) {
465
- const installSpinner = yoctoSpinner2({
466
- text: "installing dependencies"
467
- }).start();
468
- try {
469
- await install(packageManager2);
470
- installSpinner.success("Dependencies installed");
471
- } catch (e) {
472
- console.error(e);
473
- installSpinner.error("Failed to install dependencies");
474
- process.exit(1);
475
- }
476
- } else {
477
- console.info(`${colors2.yellow("\u26A0")} --skip-install > Skip installing dependencies`);
478
- }
479
- if (!skipGenerate) {
480
- const generateSpinner = yoctoSpinner2({
481
- text: "generating SQL files"
482
- }).start();
483
- try {
484
- await generate(packageManager2);
485
- generateSpinner.success("SQL files generated");
486
- } catch (e) {
487
- console.error(e);
488
- generateSpinner.error("Failed to generate SQL files");
489
- process.exit(1);
490
596
  }
491
- } else {
492
- console.info(`${colors2.yellow("\u26A0")} --skip-generate > Skip generating SQL files`);
493
- }
494
- if (!skipGit) {
495
- const gitSpinner = yoctoSpinner2({
496
- text: "initializing git repository"
497
- }).start();
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);
498
604
  const gitFolderPath = join(targetFolder, ".git");
499
605
  if (!existsSync(gitFolderPath)) {
500
- try {
501
- await runGitCommand({ targetFolder, command: "init" });
502
- await runGitCommand({ targetFolder, command: "add ." });
503
- await runGitCommand({ targetFolder, command: 'commit -m "Initial commit"' });
504
- if (newBranchName2) {
505
- await runGitCommand({ targetFolder, command: `branch ${newBranchName2}` });
506
- await runGitCommand({ targetFolder, command: `checkout ${newBranchName2}` });
507
- gitSpinner.success(`Git repository initialized, initial commit created, and new branch ${newBranchName2} created`);
508
- } else {
509
- gitSpinner.success("Git repository initialized and initial commit created");
510
- }
511
- } catch (e) {
512
- console.error(e);
513
- gitSpinner.error("Failed to initialize Git repository or create branch");
514
- process.exit(1);
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);
515
614
  }
516
- } else {
517
- gitSpinner.warning("Git repository already initialized > Skip git init");
518
615
  }
519
- } else {
520
- console.info(`${colors2.yellow("\u26A0")} --skip-git > Skip git init`);
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);
521
622
  }
522
- await addRemote({ targetFolder });
523
- console.info();
524
- console.info(`${colors2.green("Success")} Created ${projectName} at ${targetFolder}`);
525
- console.info();
526
623
  const needsCd = originalCwd !== targetFolder;
527
624
  const relativePath = relative(originalCwd, targetFolder);
528
- if (needsCd) {
529
- console.info("now go to your project using:");
530
- console.info(colors2.cyan(` cd ${relativePath}`));
531
- console.info();
532
- }
533
- console.info(`${needsCd ? "then " : ""}quick start using pglite with:`);
534
- console.info(colors2.cyan(` ${packageManager2} quick`));
535
- console.info();
536
- console.info("Already have docker installed? Then you can run a full setup:");
537
- console.info(colors2.cyan(` ${packageManager2} docker`));
538
- console.info(colors2.cyan(` ${packageManager2} dev`));
539
- console.info(colors2.cyan(` ${packageManager2} seed`));
540
- console.info();
541
- console.info(`Once running, you can sign in using:`);
542
- console.info(`email: ${colors2.greenBright("admin-test@cellajs.com")}`);
543
- console.info(`password: ${colors2.greenBright("12345678")}`);
544
- console.info();
545
- console.info(`For more info, check out: ${relativePath}/README.md`);
546
- console.info(`Enjoy building ${projectName} using cella! \u{1F389}`);
547
- console.info();
625
+ showSuccess(projectName, targetFolder, relativePath, needsCd, packageManager2);
548
626
  }
549
627
 
550
- // src/index.ts
551
- async function main() {
552
- console.info(LOGO);
553
- const templateVersion = await extractPackageJsonVersionFromUri(TEMPLATE_URL);
554
- console.info();
555
- console.info(DESCRIPTION);
556
- console.info();
557
- console.info(`Cella version: ${colors3.green(templateVersion)}`);
558
- console.info(`Cli version ${colors3.green(VERSION)}`);
559
- console.info(`Created by ${AUTHOR}`);
560
- console.info(`${GITHUB} | ${WEBSITE}`);
561
- console.info();
562
- if (cli.options.skipNewBranch || cli.options.skipGit) {
563
- cli.createNewBranch = false;
564
- cli.newBranchName = null;
565
- }
566
- if (cli.options.skipGenerate === true) {
567
- cli.options.skipGenerate = true;
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;
568
639
  }
569
- if (cli.options.skipInstall === true) {
570
- cli.options.skipInstall = true;
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
+ }
571
658
  }
572
- if (cli.options.skipClean === true) {
573
- cli.options.skipClean = true;
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;
574
665
  }
575
- if (cli.options.skipGit === true) {
576
- cli.options.skipGit = true;
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";
577
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 };
578
696
  if (!cli.directory) {
579
- cli.directory = await input({
580
- message: "Enter your project name",
581
- default: "my-cella-app",
582
- validate: (name) => {
583
- const validation = validateProjectName(basename2(resolve2(name)));
584
- return validation.valid ? true : `Invalid project name: ${validation.problems[0]}`;
585
- }
586
- });
587
- }
588
- if (cli.createNewBranch === null) {
589
- cli.createNewBranch = await confirm({
590
- message: 'Would you like to create a new branch (besides "main")?',
591
- default: true
592
- });
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
+ );
593
709
  }
594
- if (!cli.newBranchName && cli.createNewBranch) {
595
- cli.newBranchName = await input({
596
- message: "Enter the new branch name",
597
- default: "development",
598
- validate: (name) => {
599
- const validation = validateProjectName(basename2(resolve2(name)));
600
- return validation.valid ? true : `Invalid branch name: ${validation.problems[0]}`;
601
- }
602
- });
710
+ if (!cli.newBranchName) {
711
+ cli.newBranchName = "development";
603
712
  }
604
- const targetFolder = resolve2(cli.directory);
605
- const projectName = basename2(targetFolder);
713
+ const targetFolder = resolve3(cli.directory);
714
+ const projectName = basename2(targetFolder)?.toLowerCase() || "project";
606
715
  if (existsSync2(targetFolder) && !await isEmptyDirectory(targetFolder)) {
607
716
  const dirName = cli.directory === "." ? "Current directory" : `Target directory "${targetFolder}"`;
608
717
  const message = `${dirName} is not empty. Please choose how you would like to proceed:`;
609
- const action = await select({
610
- message,
611
- choices: [
612
- { name: "Cancel and exit", value: "cancel" },
613
- { name: "Ignore existing files and continue", value: "ignore" }
614
- ]
615
- });
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
+ );
616
729
  if (action === "cancel") {
617
730
  process.exit(1);
618
731
  }
619
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
+ );
620
742
  const createOptions = {
621
743
  projectName,
622
744
  targetFolder,
623
745
  newBranchName: cli.newBranchName,
624
- skipInstall: cli.options.skipInstall,
625
- skipGit: cli.options.skipGit,
626
- skipClean: cli.options.skipClean,
627
- skipGenerate: cli.options.skipGenerate,
628
- packageManager: cli.packageManager
746
+ packageManager: cli.packageManager,
747
+ templateUrl: cli.options.template,
748
+ portOffset,
749
+ adminEmail
629
750
  };
630
751
  await create(createOptions);
631
752
  }
632
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
+ }
633
798
  //# sourceMappingURL=index.js.map