@cellajs/create-cella 0.1.7 → 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 -21
- package/src/add-remote.ts +17 -27
- package/src/constants.ts +77 -32
- 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 +5 -5
- 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 -16
- package/tsup.config.ts +6 -5
- package/vitest.config.ts +17 -0
- package/dist/index.js +0 -633
- package/dist/index.js.map +0 -1
- package/src/cli.ts +0 -106
- package/src/index.ts +0 -137
- package/src/utils/run-git-command.ts +0 -57
package/src/create.ts
CHANGED
|
@@ -1,193 +1,114 @@
|
|
|
1
|
-
import { mkdir } from 'node:fs/promises';
|
|
2
1
|
import { existsSync } from 'node:fs';
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
2
|
+
import { cp, mkdir } from 'node:fs/promises';
|
|
3
|
+
import { join, relative, resolve } from 'node:path';
|
|
5
4
|
import { downloadTemplate } from 'giget';
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
targetFolder: string;
|
|
18
|
-
newBranchName?: string | null;
|
|
19
|
-
skipInstall: boolean;
|
|
20
|
-
skipGit: boolean;
|
|
21
|
-
skipClean: boolean;
|
|
22
|
-
skipGenerate: boolean;
|
|
23
|
-
packageManager: string;
|
|
5
|
+
import { addRemote } from '#/add-remote';
|
|
6
|
+
import { getPortEdits, TEMPLATE_URL } from '#/constants';
|
|
7
|
+
import { type CreateOptions, showSuccess } from '#/modules/cli';
|
|
8
|
+
import { cleanTemplate } from '#/utils/clean-template';
|
|
9
|
+
import { gitAddAll, gitBranch, gitCheckout, gitCommit, gitInit } from '#/utils/git';
|
|
10
|
+
import { createProgress } from '#/utils/progress';
|
|
11
|
+
import { generate, install } from '#/utils/run-package-manager-command';
|
|
12
|
+
|
|
13
|
+
/** Check if a path is a local directory */
|
|
14
|
+
function isLocalPath(path: string): boolean {
|
|
15
|
+
return path.startsWith('/') || path.startsWith('./') || path.startsWith('../');
|
|
24
16
|
}
|
|
25
17
|
|
|
26
18
|
export async function create({
|
|
27
19
|
projectName,
|
|
28
20
|
targetFolder,
|
|
29
21
|
newBranchName,
|
|
30
|
-
skipInstall,
|
|
31
|
-
skipGit,
|
|
32
|
-
skipClean,
|
|
33
|
-
skipGenerate,
|
|
34
22
|
packageManager,
|
|
23
|
+
templateUrl,
|
|
24
|
+
portOffset = 0,
|
|
25
|
+
silent = false,
|
|
35
26
|
}: CreateOptions): Promise<void> {
|
|
36
27
|
// Save the original working directory
|
|
37
28
|
const originalCwd = process.cwd();
|
|
38
29
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
downloadSpinner.success('`cella` template downloaded');
|
|
64
|
-
|
|
65
|
-
// Clean the template if the skipClean flag is not set
|
|
66
|
-
if (!skipClean) {
|
|
67
|
-
const cleanSpinner = yoctoSpinner({
|
|
68
|
-
text: 'cleaning `cella` template',
|
|
69
|
-
}).start();
|
|
30
|
+
// Use custom template or default
|
|
31
|
+
const template = templateUrl || TEMPLATE_URL;
|
|
32
|
+
const isLocalTemplate = templateUrl && isLocalPath(templateUrl);
|
|
33
|
+
|
|
34
|
+
const progress = createProgress('creating project', silent);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// Create the target folder
|
|
38
|
+
progress.step('creating project folder');
|
|
39
|
+
await mkdir(targetFolder, { recursive: true });
|
|
40
|
+
process.chdir(targetFolder);
|
|
41
|
+
|
|
42
|
+
// Download or copy the template
|
|
43
|
+
if (isLocalTemplate) {
|
|
44
|
+
progress.step('copying local template');
|
|
45
|
+
const sourcePath = resolve(originalCwd, templateUrl);
|
|
46
|
+
|
|
47
|
+
// Check if target is inside source (would cause EINVAL)
|
|
48
|
+
if (targetFolder.startsWith(sourcePath)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Cannot create project inside the template directory.\n` +
|
|
51
|
+
` Run from outside: cd ~ && pnpm create @cellajs/cella ${projectName} --template ${templateUrl}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
70
54
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
55
|
+
await cp(sourcePath, targetFolder, {
|
|
56
|
+
recursive: true,
|
|
57
|
+
filter: (src) => !src.includes('node_modules') && !src.includes('.git'),
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
progress.step('downloading cella template');
|
|
61
|
+
await downloadTemplate(template, {
|
|
62
|
+
cwd: process.cwd(),
|
|
63
|
+
dir: targetFolder,
|
|
64
|
+
force: true,
|
|
65
|
+
provider: 'github',
|
|
75
66
|
});
|
|
76
|
-
cleanSpinner.success('`cella` template cleaned');
|
|
77
|
-
} catch (e) {
|
|
78
|
-
console.error(e);
|
|
79
|
-
cleanSpinner.error('Failed to clean `cella` template');
|
|
80
|
-
process.exit(1);
|
|
81
67
|
}
|
|
82
|
-
} else {
|
|
83
|
-
console.info(`${colors.yellow('⚠')} --skip-clean > Skip cleaning \`cella\` template`);
|
|
84
|
-
}
|
|
85
68
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
}).start();
|
|
91
|
-
|
|
92
|
-
try {
|
|
93
|
-
await install(packageManager);
|
|
94
|
-
installSpinner.success('Dependencies installed');
|
|
95
|
-
} catch (e) {
|
|
96
|
-
console.error(e);
|
|
97
|
-
installSpinner.error('Failed to install dependencies');
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
} else {
|
|
101
|
-
console.info(`${colors.yellow('⚠')} --skip-install > Skip installing dependencies`);
|
|
102
|
-
}
|
|
69
|
+
// Clean the template and apply port offsets
|
|
70
|
+
progress.step('cleaning template');
|
|
71
|
+
const extraEdits = getPortEdits(projectName, portOffset);
|
|
72
|
+
await cleanTemplate({ targetFolder, extraEdits });
|
|
103
73
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
text: 'generating SQL files',
|
|
108
|
-
}).start();
|
|
109
|
-
|
|
110
|
-
try {
|
|
111
|
-
await generate(packageManager);
|
|
112
|
-
generateSpinner.success('SQL files generated');
|
|
113
|
-
} catch (e) {
|
|
114
|
-
console.error(e);
|
|
115
|
-
generateSpinner.error('Failed to generate SQL files');
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
} else {
|
|
119
|
-
console.info(`${colors.yellow('⚠')} --skip-generate > Skip generating SQL files`);
|
|
120
|
-
}
|
|
74
|
+
// Install dependencies
|
|
75
|
+
progress.step('installing dependencies');
|
|
76
|
+
await install(packageManager);
|
|
121
77
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
text: 'initializing git repository',
|
|
126
|
-
}).start();
|
|
78
|
+
// Generate SQL files
|
|
79
|
+
progress.step('generating migrations');
|
|
80
|
+
await generate(packageManager);
|
|
127
81
|
|
|
82
|
+
// Initialize git repository
|
|
128
83
|
const gitFolderPath = join(targetFolder, '.git');
|
|
129
|
-
|
|
130
84
|
if (!existsSync(gitFolderPath)) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
await runGitCommand({ targetFolder, command: `checkout ${newBranchName}` });
|
|
141
|
-
gitSpinner.success(`Git repository initialized, initial commit created, and new branch ${newBranchName} created`);
|
|
142
|
-
} else {
|
|
143
|
-
gitSpinner.success('Git repository initialized and initial commit created');
|
|
144
|
-
}
|
|
145
|
-
} catch (e) {
|
|
146
|
-
console.error(e);
|
|
147
|
-
gitSpinner.error('Failed to initialize Git repository or create branch');
|
|
148
|
-
process.exit(1);
|
|
85
|
+
progress.step('initializing git');
|
|
86
|
+
await gitInit(targetFolder);
|
|
87
|
+
await gitAddAll(targetFolder);
|
|
88
|
+
await gitCommit(targetFolder, 'Initial commit');
|
|
89
|
+
|
|
90
|
+
if (newBranchName) {
|
|
91
|
+
progress.step(`creating branch '${newBranchName}'`);
|
|
92
|
+
await gitBranch(targetFolder, newBranchName);
|
|
93
|
+
await gitCheckout(targetFolder, newBranchName);
|
|
149
94
|
}
|
|
150
|
-
} else {
|
|
151
|
-
gitSpinner.warning('Git repository already initialized > Skip git init');
|
|
152
95
|
}
|
|
153
|
-
} else {
|
|
154
|
-
console.info(`${colors.yellow('⚠')} --skip-git > Skip git init`);
|
|
155
|
-
}
|
|
156
96
|
|
|
157
|
-
|
|
158
|
-
|
|
97
|
+
// Add upstream remote
|
|
98
|
+
progress.step('adding upstream remote');
|
|
99
|
+
await addRemote({ targetFolder, silent: true });
|
|
159
100
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
101
|
+
// Done
|
|
102
|
+
progress.done(`created ${projectName}`);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
progress.fail(error instanceof Error ? error.message : String(error));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
164
107
|
|
|
165
108
|
// Check if the working directory needs to be changed
|
|
166
109
|
const needsCd = originalCwd !== targetFolder;
|
|
167
110
|
const relativePath = relative(originalCwd, targetFolder);
|
|
168
111
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
console.info('now go to your project using:');
|
|
172
|
-
console.info(colors.cyan(` cd ${relativePath}`)); // Adding './' to make it clear it's a relative path
|
|
173
|
-
console.info();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
console.info(`${needsCd ? 'then ' : ''}quick start using pglite with:`);
|
|
177
|
-
console.info(colors.cyan(` ${packageManager} quick`));
|
|
178
|
-
console.info();
|
|
179
|
-
|
|
180
|
-
console.info('Already have docker installed? Then you can run a full setup:');
|
|
181
|
-
console.info(colors.cyan(` ${packageManager} docker`));
|
|
182
|
-
console.info(colors.cyan(` ${packageManager} dev`));
|
|
183
|
-
console.info(colors.cyan(` ${packageManager} seed`));
|
|
184
|
-
console.info();
|
|
185
|
-
|
|
186
|
-
console.info(`Once running, you can sign in using:`);
|
|
187
|
-
console.info(`email: ${colors.greenBright('admin-test@cellajs.com')}`);
|
|
188
|
-
console.info(`password: ${colors.greenBright('12345678')}`);
|
|
189
|
-
console.info();
|
|
190
|
-
console.info(`For more info, check out: ${relativePath}/README.md`);
|
|
191
|
-
console.info(`Enjoy building ${projectName} using cella! 🎉`);
|
|
192
|
-
console.info();
|
|
112
|
+
// Display final success message
|
|
113
|
+
showSuccess(projectName, targetFolder, relativePath, needsCd, packageManager);
|
|
193
114
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { basename, resolve } from 'node:path';
|
|
2
|
+
import { Command, InvalidArgumentError } from 'commander';
|
|
3
|
+
|
|
4
|
+
import { NAME, VERSION } from '#/constants';
|
|
5
|
+
import { validateProjectName } from '#/utils/validate-project-name';
|
|
6
|
+
import type { CLIConfig, CLIOptions } from './types';
|
|
7
|
+
|
|
8
|
+
// Initialize CLI variables
|
|
9
|
+
let directory: string | null = null;
|
|
10
|
+
let newBranchName: string | null = null;
|
|
11
|
+
const packageManager = 'pnpm';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Defines the root CLI command using Commander.
|
|
15
|
+
* This command accepts CLI options and validates user input.
|
|
16
|
+
*/
|
|
17
|
+
export const command = new Command(NAME)
|
|
18
|
+
.version(VERSION, '-v, --version', `output the current version of ${NAME}`)
|
|
19
|
+
.argument('[directory]', 'the directory name for the new project')
|
|
20
|
+
.usage('[directory] [options]')
|
|
21
|
+
.helpOption('-h, --help', 'display this help message')
|
|
22
|
+
.option('--template <path>', 'use a custom template (local path or github:user/repo)')
|
|
23
|
+
.action((name: string) => {
|
|
24
|
+
if (typeof name === 'string') {
|
|
25
|
+
name = name.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (name) {
|
|
29
|
+
const validation = validateProjectName(basename(resolve(name)));
|
|
30
|
+
|
|
31
|
+
if (!validation.valid) {
|
|
32
|
+
throw new InvalidArgumentError(`Invalid project name: ${validation.problems?.[0] ?? 'unknown error'}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
directory = name;
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.parse();
|
|
39
|
+
|
|
40
|
+
// Gather the CLI options and arguments
|
|
41
|
+
const options: CLIOptions = command.opts<CLIOptions>();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Runs the CLI and returns the parsed configuration.
|
|
45
|
+
* This function parses command line arguments and returns the CLI config.
|
|
46
|
+
*/
|
|
47
|
+
export function runCli(): CLIConfig {
|
|
48
|
+
return {
|
|
49
|
+
options,
|
|
50
|
+
args: command.args,
|
|
51
|
+
directory,
|
|
52
|
+
newBranchName,
|
|
53
|
+
packageManager,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Export CLI configuration for direct import
|
|
58
|
+
export const cli: CLIConfig = runCli();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
|
|
3
|
+
import { DESCRIPTION, DIVIDER, getHeaderLine } from '#/constants';
|
|
4
|
+
|
|
5
|
+
/** ASCII art logo for the CLI welcome screen. */
|
|
6
|
+
function showAscii(): void {
|
|
7
|
+
console.info(pc.cyan(' _ _ '));
|
|
8
|
+
console.info(pc.cyan('▒▓█████▓▒ ___ ___| | | __ _ '));
|
|
9
|
+
console.info(pc.cyan('▒▓█ █▓▒ / __/ _ \\ | |/ _` | '));
|
|
10
|
+
console.info(pc.cyan('▒▓█ █▓▒ | (_| __/ | | (_| | '));
|
|
11
|
+
console.info(pc.cyan('▒▓█████▓▒ \\___\\___|_|_|\\__,_| '));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Displays the compact CLI welcome header.
|
|
16
|
+
* @param templateVersion - The version of the cella template being used
|
|
17
|
+
*/
|
|
18
|
+
export function showWelcome(templateVersion: string): void {
|
|
19
|
+
console.info();
|
|
20
|
+
showAscii();
|
|
21
|
+
console.info();
|
|
22
|
+
console.info(pc.dim(DESCRIPTION));
|
|
23
|
+
console.info();
|
|
24
|
+
console.info(getHeaderLine(templateVersion));
|
|
25
|
+
console.info(DIVIDER);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Displays the final success message after project creation.
|
|
30
|
+
*/
|
|
31
|
+
export function showSuccess(
|
|
32
|
+
projectName: string,
|
|
33
|
+
_targetFolder: string,
|
|
34
|
+
relativePath: string,
|
|
35
|
+
needsCd: boolean,
|
|
36
|
+
packageManager: string,
|
|
37
|
+
): void {
|
|
38
|
+
console.info(DIVIDER);
|
|
39
|
+
console.info();
|
|
40
|
+
|
|
41
|
+
// Navigation instruction
|
|
42
|
+
if (needsCd) {
|
|
43
|
+
console.info(`${pc.green('→')} cd ${pc.cyan(relativePath)}`);
|
|
44
|
+
console.info();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Quick start options
|
|
48
|
+
console.info(`${pc.green('→')} ${pc.cyan(`${packageManager} quick`)} ${pc.gray('(pglite, no docker)')}`);
|
|
49
|
+
console.info();
|
|
50
|
+
console.info(pc.gray('or, for full setup:'));
|
|
51
|
+
console.info();
|
|
52
|
+
console.info(
|
|
53
|
+
`${pc.green('→')} ${pc.cyan(`${packageManager} docker`)} ${pc.dim('&&')} ${pc.cyan(`${packageManager} seed`)} ${pc.dim('&&')} ${pc.cyan(`${packageManager} dev`)}`,
|
|
54
|
+
);
|
|
55
|
+
console.info();
|
|
56
|
+
|
|
57
|
+
// Credentials
|
|
58
|
+
console.info(`sign in: ${pc.gray('admin-test@cellajs.com / 12345678')}`);
|
|
59
|
+
console.info();
|
|
60
|
+
console.info(`enjoy building ${pc.green(projectName)} with cella!`);
|
|
61
|
+
console.info();
|
|
62
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** CLI options parsed from command line arguments */
|
|
2
|
+
export interface CLIOptions {
|
|
3
|
+
template?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/** CLI configuration state */
|
|
7
|
+
export interface CLIConfig {
|
|
8
|
+
options: CLIOptions;
|
|
9
|
+
args: string[];
|
|
10
|
+
directory: string | null;
|
|
11
|
+
newBranchName: string | null;
|
|
12
|
+
packageManager: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Options for creating a new project */
|
|
16
|
+
export interface CreateOptions {
|
|
17
|
+
projectName: string;
|
|
18
|
+
targetFolder: string;
|
|
19
|
+
newBranchName?: string | null;
|
|
20
|
+
packageManager: string;
|
|
21
|
+
templateUrl?: string;
|
|
22
|
+
/** Port offset to avoid collisions with sibling forks (0 = default ports) */
|
|
23
|
+
portOffset?: number;
|
|
24
|
+
/** Suppress all output (for testing) */
|
|
25
|
+
silent?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Options for adding a remote to the repository */
|
|
29
|
+
export interface AddRemoteOptions {
|
|
30
|
+
targetFolder: string;
|
|
31
|
+
remoteUrl?: string;
|
|
32
|
+
remoteName?: string;
|
|
33
|
+
/** If true, don't throw on failure */
|
|
34
|
+
silent?: boolean;
|
|
35
|
+
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import
|
|
3
|
+
import pc from 'picocolors';
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { type FileEdit, TO_CLEAN, TO_COPY, TO_EDIT, TO_REMOVE } from '#/constants';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Cleans the specified template by removing designated folders and files.
|
|
9
|
-
* @param params - Parameters containing the target folder and
|
|
9
|
+
* @param params - Parameters containing the target folder and optional extra edits.
|
|
10
10
|
*/
|
|
11
11
|
export async function cleanTemplate({
|
|
12
12
|
targetFolder,
|
|
13
|
-
|
|
13
|
+
extraEdits = {},
|
|
14
14
|
}: {
|
|
15
15
|
targetFolder: string;
|
|
16
|
-
|
|
16
|
+
extraEdits?: Record<string, FileEdit[]>;
|
|
17
17
|
}): Promise<void> {
|
|
18
18
|
// Change the current working directory to targetFolder if not already set
|
|
19
19
|
if (process.cwd() !== targetFolder) {
|
|
@@ -34,7 +34,7 @@ export async function cleanTemplate({
|
|
|
34
34
|
TO_CLEAN.map((folderPath) => {
|
|
35
35
|
const absolutePath = path.resolve(targetFolder, folderPath);
|
|
36
36
|
return removeFolderContents(absolutePath);
|
|
37
|
-
})
|
|
37
|
+
}),
|
|
38
38
|
);
|
|
39
39
|
|
|
40
40
|
// Remove specified files and folders
|
|
@@ -42,14 +42,21 @@ export async function cleanTemplate({
|
|
|
42
42
|
TO_REMOVE.map((filePath) => {
|
|
43
43
|
const absolutePath = path.resolve(targetFolder, filePath);
|
|
44
44
|
return removeFileOrFolder(absolutePath);
|
|
45
|
-
})
|
|
45
|
+
}),
|
|
46
46
|
);
|
|
47
47
|
|
|
48
|
+
// Merge static edits with extra edits (e.g., port offsets)
|
|
49
|
+
const allEdits = { ...TO_EDIT };
|
|
50
|
+
for (const [filePath, edits] of Object.entries(extraEdits)) {
|
|
51
|
+
allEdits[filePath] = [...(allEdits[filePath] || []), ...edits];
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
// Edit specific files
|
|
49
|
-
await Promise.all(
|
|
55
|
+
await Promise.all(
|
|
56
|
+
Object.entries(allEdits).map(async ([filePath, edits]) => {
|
|
50
57
|
const absolutePath = path.resolve(targetFolder, filePath);
|
|
51
58
|
await editFile(absolutePath, edits);
|
|
52
|
-
})
|
|
59
|
+
}),
|
|
53
60
|
);
|
|
54
61
|
|
|
55
62
|
resolve();
|
|
@@ -81,7 +88,7 @@ export async function removeFolderContents(folderPath: string): Promise<void> {
|
|
|
81
88
|
// If it's a file, remove it
|
|
82
89
|
await fs.rm(filePath);
|
|
83
90
|
}
|
|
84
|
-
})
|
|
91
|
+
}),
|
|
85
92
|
);
|
|
86
93
|
}
|
|
87
94
|
|
|
@@ -110,7 +117,7 @@ export async function copyFile(src: string, dest: string): Promise<void> {
|
|
|
110
117
|
await fs.copyFile(src, dest);
|
|
111
118
|
} catch (err: any) {
|
|
112
119
|
if (err.code === 'ENOENT') {
|
|
113
|
-
console.info(`\n${
|
|
120
|
+
console.info(`\n${pc.yellow('⚠')} Source file "${src}" does not exist > Skip copy`);
|
|
114
121
|
} else {
|
|
115
122
|
throw err;
|
|
116
123
|
}
|
|
@@ -122,7 +129,10 @@ export async function copyFile(src: string, dest: string): Promise<void> {
|
|
|
122
129
|
* @param filePath - The path of the file to edit.
|
|
123
130
|
* @param edits - The list of edits to apply.
|
|
124
131
|
*/
|
|
125
|
-
export async function editFile(
|
|
132
|
+
export async function editFile(
|
|
133
|
+
filePath: string,
|
|
134
|
+
edits: Array<{ regexMatch: RegExp; replaceWith: string }>,
|
|
135
|
+
): Promise<void> {
|
|
126
136
|
try {
|
|
127
137
|
await fs.access(filePath);
|
|
128
138
|
|
|
@@ -139,12 +149,11 @@ export async function editFile(filePath: string, edits: Array<{regexMatch: RegEx
|
|
|
139
149
|
if (fileContent !== updatedContent) {
|
|
140
150
|
await fs.writeFile(filePath, updatedContent, 'utf8');
|
|
141
151
|
}
|
|
142
|
-
|
|
143
152
|
} catch (err: any) {
|
|
144
153
|
if (err.code === 'ENOENT') {
|
|
145
|
-
console.info(`\n${
|
|
154
|
+
console.info(`\n${pc.yellow('⚠')} Source file "${filePath}" does not exist > Skip edit`);
|
|
146
155
|
} else {
|
|
147
156
|
throw err;
|
|
148
157
|
}
|
|
149
158
|
}
|
|
150
|
-
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
interface UsedPorts {
|
|
5
|
+
project: string;
|
|
6
|
+
frontend: number;
|
|
7
|
+
backend: number;
|
|
8
|
+
offset: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Scan sibling directories for existing cella forks and extract their dev ports.
|
|
13
|
+
* Looks for `shared/development-config.ts` to identify cella-based projects.
|
|
14
|
+
*/
|
|
15
|
+
export async function detectUsedPorts(targetFolder: string): Promise<UsedPorts[]> {
|
|
16
|
+
const parentDir = dirname(targetFolder);
|
|
17
|
+
const used: UsedPorts[] = [];
|
|
18
|
+
|
|
19
|
+
let siblings: string[];
|
|
20
|
+
try {
|
|
21
|
+
siblings = await readdir(parentDir);
|
|
22
|
+
} catch {
|
|
23
|
+
return used;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const name of siblings) {
|
|
27
|
+
const configPath = join(parentDir, name, 'shared/development-config.ts');
|
|
28
|
+
try {
|
|
29
|
+
const content = await readFile(configPath, 'utf8');
|
|
30
|
+
const feMatch = content.match(/frontendUrl:\s*'http:\/\/localhost:(\d+)'/);
|
|
31
|
+
const beMatch = content.match(/backendUrl:\s*'http:\/\/localhost:(\d+)'/);
|
|
32
|
+
if (feMatch && beMatch) {
|
|
33
|
+
const frontend = Number(feMatch[1]);
|
|
34
|
+
const backend = Number(beMatch[1]);
|
|
35
|
+
used.push({
|
|
36
|
+
project: name,
|
|
37
|
+
frontend,
|
|
38
|
+
backend,
|
|
39
|
+
offset: frontend - 3000,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Not a cella fork, skip
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return used;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Find the next available offset (in steps of 10) that doesn't collide with existing forks. */
|
|
51
|
+
export function findNextOffset(usedPorts: UsedPorts[]): number {
|
|
52
|
+
const usedOffsets = new Set(usedPorts.map((p) => p.offset));
|
|
53
|
+
for (let offset = 0; offset <= 490; offset += 10) {
|
|
54
|
+
if (!usedOffsets.has(offset)) return offset;
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
@@ -3,15 +3,15 @@ import axios from 'axios';
|
|
|
3
3
|
/**
|
|
4
4
|
* Retrieves the version from the package.json file of a GitHub repository.
|
|
5
5
|
* If the package.json file is not found or an error occurs, it returns 'unknown'.
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* @param repositoryUrl {string} - The GitHub repository URL in the format 'github:user/repo'.
|
|
8
8
|
* @param branch {string} - The branch name (defaults to 'main').
|
|
9
9
|
* @returns {Promise<string>} - The version from the package.json file.
|
|
10
10
|
*/
|
|
11
|
-
export async function extractPackageJsonVersionFromUri(repositoryUrl: string, branch
|
|
11
|
+
export async function extractPackageJsonVersionFromUri(repositoryUrl: string, branch = 'main'): Promise<string> {
|
|
12
12
|
// Extract owner and repo from the URL
|
|
13
13
|
const [owner, repo] = repositoryUrl.replace('github:', '').split('/');
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
// Construct the URL for the package.json file in the provided branch
|
|
16
16
|
const packageJsonUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/package.json`;
|
|
17
17
|
|
|
@@ -19,11 +19,11 @@ export async function extractPackageJsonVersionFromUri(repositoryUrl: string, br
|
|
|
19
19
|
// Fetch the package.json file
|
|
20
20
|
const response = await axios.get(packageJsonUrl);
|
|
21
21
|
const packageJson = response.data;
|
|
22
|
-
|
|
22
|
+
|
|
23
23
|
// Return the version from the package.json, or 'unknown' if not found
|
|
24
24
|
return packageJson.version || 'unknown';
|
|
25
25
|
} catch (error) {
|
|
26
26
|
// If there's an error (file not found, etc.), return 'unknown'
|
|
27
27
|
return 'unknown';
|
|
28
28
|
}
|
|
29
|
-
}
|
|
29
|
+
}
|