@cellajs/create-cella 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +57 -0
- package/index.js +1 -1
- package/package.json +32 -20
- package/src/add-remote.ts +17 -27
- package/src/constants.ts +77 -33
- package/src/create-cella-cli.ts +141 -0
- package/src/create.ts +82 -161
- package/src/modules/cli/commands.ts +58 -0
- package/src/modules/cli/display.ts +62 -0
- package/src/modules/cli/index.ts +3 -0
- package/src/modules/cli/types.ts +35 -0
- package/src/utils/clean-template.ts +24 -15
- package/src/utils/detect-used-ports.ts +57 -0
- package/src/utils/extract-package-json-version-from-uri.ts +29 -0
- package/src/utils/git/command.ts +89 -0
- package/src/utils/git/index.ts +11 -0
- package/src/utils/is-empty-directory.ts +1 -1
- package/src/utils/progress.ts +118 -0
- package/src/utils/run-package-manager-command.ts +12 -37
- package/src/utils/validate-project-name.ts +1 -4
- package/tests/e2e.test.ts +108 -0
- package/tests/validate-project-name.test.ts +22 -0
- package/tsconfig.json +19 -14
- package/tsup.config.ts +6 -5
- package/vitest.config.ts +17 -0
- package/dist/index.js +0 -617
- package/dist/index.js.map +0 -1
- package/src/cli.ts +0 -106
- package/src/index.ts +0 -132
- package/src/utils/run-git-command.ts +0 -57
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 CellaJS
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @cellajs/create-cella
|
|
2
|
+
|
|
3
|
+
CLI tool to scaffold a new Cella project from the template.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This CLI creates a new Cella project by downloading the latest template, setting up your development environment, and configuring git with upstream tracking for future syncs.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm create @cellajs/cella my-app
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Running without arguments starts interactive mode, prompting for:
|
|
16
|
+
|
|
17
|
+
1. **Project name** – Directory name and package name
|
|
18
|
+
2. **New branch** – Optionally create a dev branch alongside `main`
|
|
19
|
+
3. **Directory conflict** – If target exists, choose to cancel or continue
|
|
20
|
+
|
|
21
|
+
## CLI Options
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm create @cellajs/cella [directory] [options]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## What It Does
|
|
28
|
+
|
|
29
|
+
1. Downloads latest Cella template via [giget](https://github.com/unjs/giget)
|
|
30
|
+
2. Cleans template files (removes cella-specific docs, configs)
|
|
31
|
+
3. Installs dependencies with `pnpm install`
|
|
32
|
+
4. Generates initial database migrations
|
|
33
|
+
5. Initializes git repository with initial commit
|
|
34
|
+
6. Creates optional working branch
|
|
35
|
+
7. Adds Cella as upstream remote for future syncs
|
|
36
|
+
|
|
37
|
+
## Development
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd cli/create-cella
|
|
41
|
+
|
|
42
|
+
# Type check
|
|
43
|
+
pnpm ts
|
|
44
|
+
|
|
45
|
+
# Lint
|
|
46
|
+
pnpm lint:fix
|
|
47
|
+
|
|
48
|
+
# Run tests
|
|
49
|
+
pnpm test
|
|
50
|
+
|
|
51
|
+
# Run locally
|
|
52
|
+
pnpm start
|
|
53
|
+
|
|
54
|
+
# Build for npm publish
|
|
55
|
+
pnpm build
|
|
56
|
+
```
|
|
57
|
+
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cellajs/create-cella",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Cella is a TypeScript template to create web apps with sync and offline capabilities.",
|
|
@@ -15,32 +15,44 @@
|
|
|
15
15
|
"homepage": "https://cellajs.com",
|
|
16
16
|
"author": "CellaJS <info@cellajs.com>",
|
|
17
17
|
"engines": {
|
|
18
|
-
"node": "
|
|
18
|
+
"node": "24.x"
|
|
19
19
|
},
|
|
20
20
|
"type": "module",
|
|
21
|
-
"main": "./src/
|
|
21
|
+
"main": "./src/create-cella-cli.ts",
|
|
22
|
+
"imports": {
|
|
23
|
+
"#/*": "./src/*"
|
|
24
|
+
},
|
|
22
25
|
"bin": {
|
|
23
26
|
"create-cella": "index.js"
|
|
24
27
|
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@inquirer/prompts": "^8.3.0",
|
|
30
|
+
"axios": "^1.13.6",
|
|
31
|
+
"commander": "^14.0.3",
|
|
32
|
+
"giget": "^3.1.2",
|
|
33
|
+
"nano-spawn": "^2.0.0",
|
|
34
|
+
"ora": "^9.3.0",
|
|
35
|
+
"picocolors": "^1.1.1",
|
|
36
|
+
"validate-npm-package-name": "^7.0.2"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "24.10.13",
|
|
40
|
+
"@types/validate-npm-package-name": "^4.0.2",
|
|
41
|
+
"tsup": "^8.5.1",
|
|
42
|
+
"tsx": "^4.21.0"
|
|
43
|
+
},
|
|
25
44
|
"scripts": {
|
|
26
|
-
"start": "tsx ./src/
|
|
45
|
+
"start": "tsx ./src/create-cella-cli.ts",
|
|
27
46
|
"clean": "rimraf ./dist",
|
|
28
47
|
"build": "tsup",
|
|
29
48
|
"test-build": "pnpm run build && node index.js",
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"picocolors": "^1.1.0",
|
|
39
|
-
"validate-npm-package-name": "^5.0.1",
|
|
40
|
-
"yocto-spinner": "^0.1.0"
|
|
41
|
-
},
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"tsup": "^8.3.5",
|
|
44
|
-
"tsx": "^4.19.2"
|
|
49
|
+
"check": "pnpm ts && pnpm biome check --write .",
|
|
50
|
+
"check:old": "pnpm ts:old && pnpm biome check --write .",
|
|
51
|
+
"ts": "tsgo --pretty",
|
|
52
|
+
"ts:old": "tsc --pretty",
|
|
53
|
+
"lint": "biome check .",
|
|
54
|
+
"lint:fix": "biome check --write .",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:watch": "vitest"
|
|
45
57
|
}
|
|
46
|
-
}
|
|
58
|
+
}
|
package/src/add-remote.ts
CHANGED
|
@@ -1,50 +1,40 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
interface AddRemoteOptions {
|
|
6
|
-
targetFolder: string;
|
|
7
|
-
remoteUrl?: string; // Optional, defaults to CELLA_REMOTE_URL
|
|
8
|
-
remoteName?: string; // Optional, defaults to 'upstream'
|
|
9
|
-
}
|
|
1
|
+
import { CELLA_REMOTE_URL } from '#/constants';
|
|
2
|
+
import type { AddRemoteOptions } from '#/modules/cli';
|
|
3
|
+
import { gitRemoteAdd, gitRemoteGetUrl, gitRemoteRemove } from '#/utils/git';
|
|
10
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Adds or updates the upstream remote for the Cella template.
|
|
7
|
+
* @param options - Configuration options
|
|
8
|
+
* @param options.silent - If true, don't throw on failure (used when called from progress tracker)
|
|
9
|
+
*/
|
|
11
10
|
export async function addRemote({
|
|
12
11
|
targetFolder,
|
|
13
12
|
remoteUrl = CELLA_REMOTE_URL,
|
|
14
13
|
remoteName = 'upstream',
|
|
14
|
+
silent = false,
|
|
15
15
|
}: AddRemoteOptions): Promise<void> {
|
|
16
|
-
|
|
17
|
-
// Spinner for adding remote
|
|
18
|
-
const remoteSpinner = yoctoSpinner({
|
|
19
|
-
text: 'Adding remote',
|
|
20
|
-
}).start();
|
|
21
|
-
|
|
22
16
|
try {
|
|
23
17
|
// Check if the remote exists
|
|
24
18
|
let remote: string | null = null;
|
|
25
19
|
|
|
26
20
|
try {
|
|
27
|
-
remote = await
|
|
28
|
-
} catch
|
|
21
|
+
remote = await gitRemoteGetUrl(targetFolder, remoteName);
|
|
22
|
+
} catch {
|
|
29
23
|
// If the remote doesn't exist, it throws a fatal error
|
|
30
24
|
remote = null;
|
|
31
25
|
}
|
|
32
26
|
|
|
33
27
|
// Add or update the remote if it doesn't exist or differs from `remoteUrl`
|
|
34
28
|
if (!remote) {
|
|
35
|
-
await
|
|
36
|
-
remoteSpinner.success('Remote added successfully.');
|
|
29
|
+
await gitRemoteAdd(targetFolder, remoteName, remoteUrl);
|
|
37
30
|
} else if (remote !== remoteUrl) {
|
|
38
31
|
// Remove existing remote and set the new URL
|
|
39
|
-
await
|
|
40
|
-
await
|
|
41
|
-
remoteSpinner.success('Remote updated successfully.');
|
|
42
|
-
} else {
|
|
43
|
-
remoteSpinner.success('Remote is already configured correctly.');
|
|
32
|
+
await gitRemoteRemove(targetFolder, remoteName);
|
|
33
|
+
await gitRemoteAdd(targetFolder, remoteName, remoteUrl);
|
|
44
34
|
}
|
|
45
35
|
} catch (error) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
if (!silent) {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
49
39
|
}
|
|
50
40
|
}
|
package/src/constants.ts
CHANGED
|
@@ -1,62 +1,106 @@
|
|
|
1
|
-
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import packageJson from '../package.json' with { type: 'json' };
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
export const
|
|
4
|
+
/** Name of this CLI tool */
|
|
5
|
+
export const NAME = 'create cella';
|
|
6
|
+
|
|
7
|
+
/** Thin line divider for console output (60 chars wide) */
|
|
8
|
+
export const DIVIDER = '─'.repeat(60);
|
|
5
9
|
|
|
6
|
-
|
|
7
|
-
export const
|
|
10
|
+
/** URL of the template repository */
|
|
11
|
+
export const TEMPLATE_URL = 'github:cellajs/cella';
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
/** URL to the repository */
|
|
14
|
+
export const CELLA_REMOTE_URL = 'git@github.com:cellajs/cella.git';
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
/** Export details from package.json */
|
|
13
17
|
export const DESCRIPTION: string = packageJson.description;
|
|
14
18
|
export const VERSION: string = packageJson.version;
|
|
15
19
|
export const AUTHOR: string = packageJson.author;
|
|
16
20
|
export const WEBSITE: string = packageJson.homepage;
|
|
17
21
|
export const GITHUB: string = packageJson.repository.url;
|
|
18
22
|
|
|
23
|
+
export function getHeaderLine(templateVersion?: string): string {
|
|
24
|
+
const leftText = `⧈ ${NAME} · v${VERSION} · cella v${templateVersion}`;
|
|
25
|
+
const rightText = packageJson.homepage.replace('https://', '');
|
|
26
|
+
const left = `${pc.cyan(`⧈ ${NAME}`)} ${pc.dim(`· v${VERSION} · cella v${templateVersion}`)}`;
|
|
27
|
+
const right = pc.cyan(rightText);
|
|
28
|
+
const padding = Math.max(1, 60 - leftText.length - rightText.length);
|
|
29
|
+
return `${left}${' '.repeat(padding)}${right}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
19
32
|
// Files or folders to be removed from the template after downloading
|
|
20
|
-
export const TO_REMOVE: string[] = [
|
|
21
|
-
'info',
|
|
22
|
-
'./cli/create-cella',
|
|
23
|
-
];
|
|
33
|
+
export const TO_REMOVE: string[] = ['./cli/create', './info/QUICKSTART.md'];
|
|
24
34
|
|
|
25
35
|
// Specific folder contents to be cleaned out from the template
|
|
26
|
-
export const TO_CLEAN: string[] = [
|
|
27
|
-
'./backend/drizzle',
|
|
28
|
-
];
|
|
36
|
+
export const TO_CLEAN: string[] = ['./backend/drizzle'];
|
|
29
37
|
|
|
30
38
|
// Files to copy/paste after downloading
|
|
31
39
|
export const TO_COPY: Record<string, string> = {
|
|
32
40
|
'./backend/.env.example': './backend/.env',
|
|
33
41
|
'./frontend/.env.example': './frontend/.env',
|
|
34
|
-
'./tus/.env.example': './tus/.env',
|
|
35
42
|
'./info/QUICKSTART.md': 'README.md',
|
|
36
43
|
};
|
|
37
44
|
|
|
38
|
-
|
|
39
|
-
export
|
|
40
|
-
|
|
45
|
+
/** Type for file edit operations */
|
|
46
|
+
export type FileEdit = { regexMatch: RegExp; replaceWith: string };
|
|
47
|
+
|
|
48
|
+
// Files to be edited after downloading
|
|
49
|
+
export const TO_EDIT: Record<string, FileEdit[]> = {
|
|
50
|
+
'./shared/default-config.ts': [
|
|
41
51
|
{
|
|
42
|
-
regexMatch: /
|
|
43
|
-
replaceWith: "
|
|
52
|
+
regexMatch: /enabledAuthStrategies:\s*\[[^\]]+\]\s*as\s*const,/g,
|
|
53
|
+
replaceWith: "enabledAuthStrategies: ['password', 'passkey', 'totp'] as const,",
|
|
44
54
|
},
|
|
45
55
|
{
|
|
46
|
-
regexMatch: /
|
|
47
|
-
replaceWith:
|
|
56
|
+
regexMatch: /uploadEnabled:\s*(true|false),/g,
|
|
57
|
+
replaceWith: 'uploadEnabled: false,',
|
|
48
58
|
},
|
|
49
59
|
{
|
|
50
|
-
regexMatch: /
|
|
51
|
-
replaceWith:
|
|
60
|
+
regexMatch: /enabledOAuthProviders:\s*\[[^\]]+\]\s*as\s*const,/g,
|
|
61
|
+
replaceWith: 'enabledOAuthProviders: [] as const,',
|
|
52
62
|
},
|
|
53
63
|
],
|
|
54
64
|
};
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Generate file edits to apply a port offset to a new fork.
|
|
68
|
+
* All dev ports are shifted by the given offset to avoid collisions with sibling forks.
|
|
69
|
+
*
|
|
70
|
+
* Only 3 files need editing — all other services derive ports from these:
|
|
71
|
+
* - development-config.ts → frontend/backend URLs (read by Vite, backend, CDC, studio, tests)
|
|
72
|
+
* - .env → backend PORT + database connection strings
|
|
73
|
+
* - compose.yaml → Docker container names + host port mappings
|
|
74
|
+
*
|
|
75
|
+
* Default ports: frontend=3000, backend=4000, db=5432, dbTest=5434
|
|
76
|
+
*/
|
|
77
|
+
export function getPortEdits(projectName: string, offset: number): Record<string, FileEdit[]> {
|
|
78
|
+
if (offset === 0) return {};
|
|
79
|
+
|
|
80
|
+
const fe = 3000 + offset;
|
|
81
|
+
const be = 4000 + offset;
|
|
82
|
+
const db = 5432 + offset;
|
|
83
|
+
const dbTest = 5434 + offset;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
'./shared/development-config.ts': [
|
|
87
|
+
{ regexMatch: /frontendUrl:\s*'http:\/\/localhost:\d+'/g, replaceWith: `frontendUrl: 'http://localhost:${fe}'` },
|
|
88
|
+
{ regexMatch: /backendUrl:\s*'http:\/\/localhost:\d+'/g, replaceWith: `backendUrl: 'http://localhost:${be}'` },
|
|
89
|
+
{
|
|
90
|
+
regexMatch: /backendAuthUrl:\s*'http:\/\/localhost:\d+\/auth'/g,
|
|
91
|
+
replaceWith: `backendAuthUrl: 'http://localhost:${be}/auth'`,
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
'./backend/.env': [
|
|
95
|
+
{ regexMatch: /PORT=\d+/g, replaceWith: `PORT=${be}` },
|
|
96
|
+
{ regexMatch: /@0\.0\.0\.0:5432\//g, replaceWith: `@0.0.0.0:${db}/` },
|
|
97
|
+
],
|
|
98
|
+
'./compose.yaml': [
|
|
99
|
+
{ regexMatch: /name: cella/g, replaceWith: `name: ${projectName}` },
|
|
100
|
+
{ regexMatch: /container_name: cella_db\b/g, replaceWith: `container_name: ${projectName}_db` },
|
|
101
|
+
{ regexMatch: /container_name: cella_db_test/g, replaceWith: `container_name: ${projectName}_db_test` },
|
|
102
|
+
{ regexMatch: /- 5432:5432/g, replaceWith: `- ${db}:5432` },
|
|
103
|
+
{ regexMatch: /- 5434:5432/g, replaceWith: `- ${dbTest}:5432` },
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { basename, resolve } from 'node:path';
|
|
5
|
+
import { input, select } from '@inquirer/prompts';
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
|
|
8
|
+
import { TEMPLATE_URL } from '#/constants';
|
|
9
|
+
import { create } from '#/create';
|
|
10
|
+
import { type CreateOptions, cli, showWelcome } from '#/modules/cli';
|
|
11
|
+
import { detectUsedPorts, findNextOffset } from '#/utils/detect-used-ports';
|
|
12
|
+
import { extractPackageJsonVersionFromUri } from '#/utils/extract-package-json-version-from-uri';
|
|
13
|
+
import { isEmptyDirectory } from '#/utils/is-empty-directory';
|
|
14
|
+
import { validateProjectName } from '#/utils/validate-project-name';
|
|
15
|
+
|
|
16
|
+
async function main(): Promise<void> {
|
|
17
|
+
// Get the latest version of the template
|
|
18
|
+
const templateVersion = await extractPackageJsonVersionFromUri(TEMPLATE_URL);
|
|
19
|
+
|
|
20
|
+
// Display CLI welcome banner
|
|
21
|
+
showWelcome(templateVersion);
|
|
22
|
+
|
|
23
|
+
// Shared theme to clear prompts after answering
|
|
24
|
+
const promptTheme = { prefix: '', style: { answer: (text: string) => text } };
|
|
25
|
+
const promptContext = { clearPromptOnDone: true };
|
|
26
|
+
|
|
27
|
+
// Prompt for project name if not provided
|
|
28
|
+
if (!cli.directory) {
|
|
29
|
+
cli.directory = await input(
|
|
30
|
+
{
|
|
31
|
+
message: 'Enter your project name',
|
|
32
|
+
default: 'my-cella-app',
|
|
33
|
+
theme: promptTheme,
|
|
34
|
+
validate: (name) => {
|
|
35
|
+
const validation = validateProjectName(basename(resolve(name)));
|
|
36
|
+
return validation.valid ? true : `Invalid project name: ${validation.problems?.[0] ?? 'unknown error'}`;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
promptContext,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Default to creating a 'development' working branch
|
|
44
|
+
if (!cli.newBranchName) {
|
|
45
|
+
cli.newBranchName = 'development';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const targetFolder = resolve(cli.directory);
|
|
49
|
+
const projectName = basename(targetFolder);
|
|
50
|
+
|
|
51
|
+
// Check if the target folder exists and is not empty
|
|
52
|
+
if (existsSync(targetFolder) && !(await isEmptyDirectory(targetFolder))) {
|
|
53
|
+
const dirName = cli.directory === '.' ? 'Current directory' : `Target directory "${targetFolder}"`;
|
|
54
|
+
const message = `${dirName} is not empty. Please choose how you would like to proceed:`;
|
|
55
|
+
|
|
56
|
+
const action = await select(
|
|
57
|
+
{
|
|
58
|
+
message,
|
|
59
|
+
theme: promptTheme,
|
|
60
|
+
choices: [
|
|
61
|
+
{ name: 'Cancel and exit', value: 'cancel' },
|
|
62
|
+
{ name: 'Ignore existing files and continue', value: 'ignore' },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
promptContext,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (action === 'cancel') {
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Scan sibling directories and prompt for port offset
|
|
74
|
+
const portOffset = await promptPortOffset(targetFolder, promptTheme, promptContext);
|
|
75
|
+
|
|
76
|
+
// Proceed with the project creation
|
|
77
|
+
const createOptions: CreateOptions = {
|
|
78
|
+
projectName,
|
|
79
|
+
targetFolder,
|
|
80
|
+
newBranchName: cli.newBranchName,
|
|
81
|
+
packageManager: cli.packageManager,
|
|
82
|
+
templateUrl: cli.options.template,
|
|
83
|
+
portOffset,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
await create(createOptions);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
main().catch(console.error);
|
|
90
|
+
|
|
91
|
+
/** Format an offset as a port overview string, e.g. "10 → :3010 / :4010 / :5442" */
|
|
92
|
+
function formatOffset(o: number, suffix = ''): string {
|
|
93
|
+
return `${o} → :${3000 + o} / :${4000 + o} / :${5432 + o}${suffix}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Scan sibling forks and prompt the user to pick a port offset */
|
|
97
|
+
async function promptPortOffset(targetFolder: string, theme: object, context: object): Promise<number> {
|
|
98
|
+
const usedPorts = await detectUsedPorts(targetFolder);
|
|
99
|
+
const suggested = findNextOffset(usedPorts);
|
|
100
|
+
|
|
101
|
+
if (usedPorts.length > 0) {
|
|
102
|
+
console.info(pc.dim('\nDetected cella forks in sibling directories:'));
|
|
103
|
+
for (const p of usedPorts) {
|
|
104
|
+
console.info(pc.dim(` ${p.project}: frontend :${p.frontend}, backend :${p.backend} (offset ${p.offset})`));
|
|
105
|
+
}
|
|
106
|
+
console.info();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const presets = [0, 10, 20, 30].filter((o) => o !== suggested);
|
|
110
|
+
const choice = await select(
|
|
111
|
+
{
|
|
112
|
+
message: 'Port offset (avoids conflicts with sibling forks)',
|
|
113
|
+
theme,
|
|
114
|
+
choices: [
|
|
115
|
+
...(suggested > 0 ? [{ name: formatOffset(suggested, ' (suggested)'), value: suggested }] : []),
|
|
116
|
+
{ name: formatOffset(0, ' (default)'), value: 0 },
|
|
117
|
+
...presets.map((o) => ({ name: formatOffset(o), value: o })),
|
|
118
|
+
{ name: 'Custom offset', value: -1 },
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
context,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (choice !== -1) return choice;
|
|
125
|
+
|
|
126
|
+
const custom = await input(
|
|
127
|
+
{
|
|
128
|
+
message: 'Enter custom offset (0-490, multiples of 10)',
|
|
129
|
+
default: String(suggested),
|
|
130
|
+
theme,
|
|
131
|
+
validate: (val) => {
|
|
132
|
+
const n = Number(val);
|
|
133
|
+
if (Number.isNaN(n) || n < 0 || n > 490) return 'Must be between 0 and 490';
|
|
134
|
+
if (n % 10 !== 0) return 'Must be a multiple of 10';
|
|
135
|
+
return true;
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
context,
|
|
139
|
+
);
|
|
140
|
+
return Number(custom);
|
|
141
|
+
}
|