@cellajs/create-cella 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/configs/default-config.ts.template +404 -0
- package/dist/index.js +798 -0
- package/dist/index.js.map +1 -0
- package/package.json +29 -21
- package/LICENSE +0 -21
- package/src/add-remote.ts +0 -40
- package/src/constants.ts +0 -106
- package/src/create-cella-cli.ts +0 -141
- package/src/create.ts +0 -114
- package/src/modules/cli/commands.ts +0 -58
- package/src/modules/cli/display.ts +0 -62
- package/src/modules/cli/index.ts +0 -3
- package/src/modules/cli/types.ts +0 -35
- package/src/utils/clean-template.ts +0 -159
- package/src/utils/detect-used-ports.ts +0 -57
- package/src/utils/extract-package-json-version-from-uri.ts +0 -29
- package/src/utils/git/command.ts +0 -89
- package/src/utils/git/index.ts +0 -11
- package/src/utils/is-empty-directory.ts +0 -15
- package/src/utils/progress.ts +0 -118
- package/src/utils/run-package-manager-command.ts +0 -49
- package/src/utils/validate-project-name.ts +0 -19
- package/tests/e2e.test.ts +0 -108
- package/tests/validate-project-name.test.ts +0 -22
- package/tsconfig.json +0 -20
- package/tsup.config.ts +0 -23
- package/vitest.config.ts +0 -17
|
@@ -1,58 +0,0 @@
|
|
|
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();
|
|
@@ -1,62 +0,0 @@
|
|
|
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
|
-
}
|
package/src/modules/cli/index.ts
DELETED
package/src/modules/cli/types.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
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,159 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import pc from 'picocolors';
|
|
4
|
-
|
|
5
|
-
import { type FileEdit, TO_CLEAN, TO_COPY, TO_EDIT, TO_REMOVE } from '#/constants';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Cleans the specified template by removing designated folders and files.
|
|
9
|
-
* @param params - Parameters containing the target folder and optional extra edits.
|
|
10
|
-
*/
|
|
11
|
-
export async function cleanTemplate({
|
|
12
|
-
targetFolder,
|
|
13
|
-
extraEdits = {},
|
|
14
|
-
}: {
|
|
15
|
-
targetFolder: string;
|
|
16
|
-
extraEdits?: Record<string, FileEdit[]>;
|
|
17
|
-
}): Promise<void> {
|
|
18
|
-
// Change the current working directory to targetFolder if not already set
|
|
19
|
-
if (process.cwd() !== targetFolder) {
|
|
20
|
-
process.chdir(targetFolder);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return new Promise<void>(async (resolve, reject) => {
|
|
24
|
-
try {
|
|
25
|
-
// Copy specified files
|
|
26
|
-
for (const [src, dest] of Object.entries(TO_COPY)) {
|
|
27
|
-
const srcAbsolutePath = path.resolve(targetFolder, src);
|
|
28
|
-
const destAbsolutePath = path.resolve(targetFolder, dest);
|
|
29
|
-
await copyFile(srcAbsolutePath, destAbsolutePath);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Clean specified folder contents
|
|
33
|
-
await Promise.all(
|
|
34
|
-
TO_CLEAN.map((folderPath) => {
|
|
35
|
-
const absolutePath = path.resolve(targetFolder, folderPath);
|
|
36
|
-
return removeFolderContents(absolutePath);
|
|
37
|
-
}),
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
// Remove specified files and folders
|
|
41
|
-
await Promise.all(
|
|
42
|
-
TO_REMOVE.map((filePath) => {
|
|
43
|
-
const absolutePath = path.resolve(targetFolder, filePath);
|
|
44
|
-
return removeFileOrFolder(absolutePath);
|
|
45
|
-
}),
|
|
46
|
-
);
|
|
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
|
-
|
|
54
|
-
// Edit specific files
|
|
55
|
-
await Promise.all(
|
|
56
|
-
Object.entries(allEdits).map(async ([filePath, edits]) => {
|
|
57
|
-
const absolutePath = path.resolve(targetFolder, filePath);
|
|
58
|
-
await editFile(absolutePath, edits);
|
|
59
|
-
}),
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
resolve();
|
|
63
|
-
} catch (err) {
|
|
64
|
-
reject(`Error during the cleaning process: ${err}`);
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Removes all contents within a specified folder.
|
|
71
|
-
* @param folderPath - The path of the folder to clean.
|
|
72
|
-
*/
|
|
73
|
-
export async function removeFolderContents(folderPath: string): Promise<void> {
|
|
74
|
-
// List all files in the folder
|
|
75
|
-
const files = await fs.readdir(folderPath);
|
|
76
|
-
|
|
77
|
-
await Promise.all(
|
|
78
|
-
files.map(async (file) => {
|
|
79
|
-
const filePath = path.join(folderPath, file);
|
|
80
|
-
|
|
81
|
-
// Get the file or folder statistics
|
|
82
|
-
const stat = await fs.lstat(filePath);
|
|
83
|
-
|
|
84
|
-
// If it's a directory, remove it and all its contents
|
|
85
|
-
if (stat.isDirectory()) {
|
|
86
|
-
await fs.rm(filePath, { recursive: true, force: true });
|
|
87
|
-
} else {
|
|
88
|
-
// If it's a file, remove it
|
|
89
|
-
await fs.rm(filePath);
|
|
90
|
-
}
|
|
91
|
-
}),
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Removes a specified file or folder.
|
|
97
|
-
* @param pathToRemove - The path to the file or folder to remove.
|
|
98
|
-
*/
|
|
99
|
-
export async function removeFileOrFolder(pathToRemove: string): Promise<void> {
|
|
100
|
-
await fs.rm(pathToRemove, { recursive: true, force: true });
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Helper function to copy files if the source exists.
|
|
105
|
-
* @param src - The source file path.
|
|
106
|
-
* @param dest - The destination file path.
|
|
107
|
-
*/
|
|
108
|
-
export async function copyFile(src: string, dest: string): Promise<void> {
|
|
109
|
-
try {
|
|
110
|
-
// Check if the source file exists
|
|
111
|
-
await fs.access(src);
|
|
112
|
-
|
|
113
|
-
// Ensure the destination directory exists
|
|
114
|
-
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
115
|
-
|
|
116
|
-
// Copy the file
|
|
117
|
-
await fs.copyFile(src, dest);
|
|
118
|
-
} catch (err: any) {
|
|
119
|
-
if (err.code === 'ENOENT') {
|
|
120
|
-
console.info(`\n${pc.yellow('⚠')} Source file "${src}" does not exist > Skip copy`);
|
|
121
|
-
} else {
|
|
122
|
-
throw err;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Helper function edit a file by applying regex replacements.
|
|
129
|
-
* @param filePath - The path of the file to edit.
|
|
130
|
-
* @param edits - The list of edits to apply.
|
|
131
|
-
*/
|
|
132
|
-
export async function editFile(
|
|
133
|
-
filePath: string,
|
|
134
|
-
edits: Array<{ regexMatch: RegExp; replaceWith: string }>,
|
|
135
|
-
): Promise<void> {
|
|
136
|
-
try {
|
|
137
|
-
await fs.access(filePath);
|
|
138
|
-
|
|
139
|
-
// Read the existing file content
|
|
140
|
-
const fileContent = await fs.readFile(filePath, 'utf8');
|
|
141
|
-
let updatedContent = fileContent;
|
|
142
|
-
|
|
143
|
-
// Apply each edit to the content
|
|
144
|
-
edits.forEach(({ regexMatch, replaceWith }) => {
|
|
145
|
-
updatedContent = updatedContent.replace(regexMatch, replaceWith);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// Write the updated content back to the file
|
|
149
|
-
if (fileContent !== updatedContent) {
|
|
150
|
-
await fs.writeFile(filePath, updatedContent, 'utf8');
|
|
151
|
-
}
|
|
152
|
-
} catch (err: any) {
|
|
153
|
-
if (err.code === 'ENOENT') {
|
|
154
|
-
console.info(`\n${pc.yellow('⚠')} Source file "${filePath}" does not exist > Skip edit`);
|
|
155
|
-
} else {
|
|
156
|
-
throw err;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import axios from 'axios';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Retrieves the version from the package.json file of a GitHub repository.
|
|
5
|
-
* If the package.json file is not found or an error occurs, it returns 'unknown'.
|
|
6
|
-
*
|
|
7
|
-
* @param repositoryUrl {string} - The GitHub repository URL in the format 'github:user/repo'.
|
|
8
|
-
* @param branch {string} - The branch name (defaults to 'main').
|
|
9
|
-
* @returns {Promise<string>} - The version from the package.json file.
|
|
10
|
-
*/
|
|
11
|
-
export async function extractPackageJsonVersionFromUri(repositoryUrl: string, branch = 'main'): Promise<string> {
|
|
12
|
-
// Extract owner and repo from the URL
|
|
13
|
-
const [owner, repo] = repositoryUrl.replace('github:', '').split('/');
|
|
14
|
-
|
|
15
|
-
// Construct the URL for the package.json file in the provided branch
|
|
16
|
-
const packageJsonUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/package.json`;
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
// Fetch the package.json file
|
|
20
|
-
const response = await axios.get(packageJsonUrl);
|
|
21
|
-
const packageJson = response.data;
|
|
22
|
-
|
|
23
|
-
// Return the version from the package.json, or 'unknown' if not found
|
|
24
|
-
return packageJson.version || 'unknown';
|
|
25
|
-
} catch (error) {
|
|
26
|
-
// If there's an error (file not found, etc.), return 'unknown'
|
|
27
|
-
return 'unknown';
|
|
28
|
-
}
|
|
29
|
-
}
|
package/src/utils/git/command.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { promisify } from 'node:util';
|
|
3
|
-
|
|
4
|
-
const execFileAsync = promisify(execFile);
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Executes a Git command in a specific repository path and returns the trimmed stdout.
|
|
8
|
-
* Uses execFile for safety (no shell injection) aligned with sync package pattern.
|
|
9
|
-
*
|
|
10
|
-
* @param args - The Git command arguments to execute (e.g., ['status'], ['checkout', 'branch-name']).
|
|
11
|
-
* @param repoPath - The path to the Git repository.
|
|
12
|
-
* @param options - Optional settings for command execution.
|
|
13
|
-
* @returns The stdout of the Git command, trimmed of leading and trailing whitespace.
|
|
14
|
-
*/
|
|
15
|
-
export async function runGitCommand(
|
|
16
|
-
args: string[],
|
|
17
|
-
repoPath: string,
|
|
18
|
-
options: { skipEditor?: boolean; maxBuffer?: number } = {},
|
|
19
|
-
): Promise<string> {
|
|
20
|
-
const gitArgs = repoPath ? ['-C', repoPath, ...args] : args;
|
|
21
|
-
|
|
22
|
-
const env = {
|
|
23
|
-
...process.env,
|
|
24
|
-
...(options.skipEditor ? { GIT_EDITOR: 'true' } : {}),
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
// Default to 10MB buffer for typical outputs
|
|
28
|
-
const maxBuffer = options.maxBuffer ?? 10 * 1024 * 1024;
|
|
29
|
-
|
|
30
|
-
const { stdout } = await execFileAsync('git', gitArgs, { env, maxBuffer });
|
|
31
|
-
|
|
32
|
-
return stdout.trim();
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Initializes a new Git repository in the specified directory.
|
|
37
|
-
*/
|
|
38
|
-
export async function gitInit(repoPath: string): Promise<string> {
|
|
39
|
-
return runGitCommand(['init'], repoPath);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Stages all files in the repository.
|
|
44
|
-
*/
|
|
45
|
-
export async function gitAddAll(repoPath: string): Promise<string> {
|
|
46
|
-
return runGitCommand(['add', '.'], repoPath);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Creates a commit with the specified message.
|
|
51
|
-
*/
|
|
52
|
-
export async function gitCommit(repoPath: string, message: string): Promise<string> {
|
|
53
|
-
return runGitCommand(['commit', '-m', message], repoPath);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Creates a new branch with the specified name.
|
|
58
|
-
*/
|
|
59
|
-
export async function gitBranch(repoPath: string, branchName: string): Promise<string> {
|
|
60
|
-
return runGitCommand(['branch', branchName], repoPath);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Checks out the specified branch.
|
|
65
|
-
*/
|
|
66
|
-
export async function gitCheckout(repoPath: string, branchName: string): Promise<string> {
|
|
67
|
-
return runGitCommand(['checkout', branchName], repoPath);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Gets the URL of a remote.
|
|
72
|
-
*/
|
|
73
|
-
export async function gitRemoteGetUrl(repoPath: string, remoteName: string): Promise<string> {
|
|
74
|
-
return runGitCommand(['remote', 'get-url', remoteName], repoPath);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Adds a new remote.
|
|
79
|
-
*/
|
|
80
|
-
export async function gitRemoteAdd(repoPath: string, remoteName: string, remoteUrl: string): Promise<string> {
|
|
81
|
-
return runGitCommand(['remote', 'add', remoteName, remoteUrl], repoPath);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Removes a remote.
|
|
86
|
-
*/
|
|
87
|
-
export async function gitRemoteRemove(repoPath: string, remoteName: string): Promise<string> {
|
|
88
|
-
return runGitCommand(['remote', 'remove', remoteName], repoPath);
|
|
89
|
-
}
|
package/src/utils/git/index.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { readdir } from 'node:fs/promises';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Checks if a directory is empty or only contains a .git directory.
|
|
5
|
-
*
|
|
6
|
-
* @param path - The path of the directory to check.
|
|
7
|
-
* @returns Resolves to true if the directory is empty or contains only a .git folder, false otherwise.
|
|
8
|
-
* @throws Throws an error if the path is not a directory or if there's an issue reading the directory.
|
|
9
|
-
*/
|
|
10
|
-
export async function isEmptyDirectory(path: string): Promise<boolean> {
|
|
11
|
-
const files = await readdir(path);
|
|
12
|
-
|
|
13
|
-
// Check if directory is empty or contains only the .git directory
|
|
14
|
-
return files.length === 0 || (files.length === 1 && files[0] === '.git');
|
|
15
|
-
}
|
package/src/utils/progress.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Progress Tracker Utility
|
|
3
|
-
*
|
|
4
|
-
* Provides compact progress tracking for CLI operations.
|
|
5
|
-
* Uses a single spinner that updates in place.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import ora, { type Ora } from 'ora';
|
|
9
|
-
import pc from 'picocolors';
|
|
10
|
-
|
|
11
|
-
/** Global reference to the currently active spinner */
|
|
12
|
-
let activeSpinner: Ora | null = null;
|
|
13
|
-
|
|
14
|
-
/** Pause the active spinner (for interactive prompts) */
|
|
15
|
-
export function pauseSpinner(): void {
|
|
16
|
-
if (activeSpinner) {
|
|
17
|
-
activeSpinner.stop();
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Resume the active spinner after a prompt */
|
|
22
|
-
export function resumeSpinner(): void {
|
|
23
|
-
if (activeSpinner) {
|
|
24
|
-
activeSpinner.start();
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface ProgressTracker {
|
|
29
|
-
/** Update the current step (shown as spinner text) */
|
|
30
|
-
step: (message: string) => void;
|
|
31
|
-
/** Mark the tracker as complete with final message */
|
|
32
|
-
done: (message: string) => void;
|
|
33
|
-
/** Mark the tracker as failed with error message */
|
|
34
|
-
fail: (message: string) => void;
|
|
35
|
-
/** Wrap an async operation to auto-fail on error */
|
|
36
|
-
wrap: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Creates a progress tracker for a multi-step operation.
|
|
41
|
-
* Shows a single spinner that updates in place.
|
|
42
|
-
*
|
|
43
|
-
* @param title - The initial title for the spinner
|
|
44
|
-
* @returns A ProgressTracker instance
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* const progress = createProgress('creating project');
|
|
48
|
-
* progress.step('downloading template');
|
|
49
|
-
* progress.step('installing dependencies');
|
|
50
|
-
* progress.done('project created');
|
|
51
|
-
*/
|
|
52
|
-
export function createProgress(title: string, silent = false): ProgressTracker {
|
|
53
|
-
const completedSteps: string[] = [];
|
|
54
|
-
|
|
55
|
-
const spinner = ora({
|
|
56
|
-
text: title,
|
|
57
|
-
isSilent: silent,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
activeSpinner = spinner;
|
|
61
|
-
spinner.start();
|
|
62
|
-
|
|
63
|
-
// Helper to log (respects silent mode)
|
|
64
|
-
const log = (msg: string) => {
|
|
65
|
-
if (!silent) console.info(msg);
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
step: (message: string) => {
|
|
70
|
-
// Complete previous step with green check if there was one
|
|
71
|
-
if (completedSteps.length > 0) {
|
|
72
|
-
spinner.stop();
|
|
73
|
-
log(`${pc.green('✓')} ${completedSteps[completedSteps.length - 1]}`);
|
|
74
|
-
}
|
|
75
|
-
completedSteps.push(message);
|
|
76
|
-
spinner.text = message;
|
|
77
|
-
spinner.start();
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
done: (message: string) => {
|
|
81
|
-
// Complete the last step with green check
|
|
82
|
-
if (completedSteps.length > 0) {
|
|
83
|
-
spinner.stop();
|
|
84
|
-
log(`${pc.green('✓')} ${completedSteps[completedSteps.length - 1]}`);
|
|
85
|
-
} else {
|
|
86
|
-
spinner.stop();
|
|
87
|
-
}
|
|
88
|
-
activeSpinner = null;
|
|
89
|
-
if (message) {
|
|
90
|
-
log('');
|
|
91
|
-
log(`${pc.green('✓')} ${message}`);
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
fail: (message: string) => {
|
|
96
|
-
spinner.stop();
|
|
97
|
-
activeSpinner = null;
|
|
98
|
-
log(`${pc.red('✗')} ${pc.red(message)}`);
|
|
99
|
-
},
|
|
100
|
-
|
|
101
|
-
wrap: async <T>(fn: () => Promise<T>): Promise<T> => {
|
|
102
|
-
try {
|
|
103
|
-
return await fn();
|
|
104
|
-
} catch (error) {
|
|
105
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
106
|
-
// Stop spinner and show tree-style breakdown on failure
|
|
107
|
-
spinner.stop();
|
|
108
|
-
activeSpinner = null;
|
|
109
|
-
log(pc.cyan(`\n${title}`));
|
|
110
|
-
for (const step of completedSteps) {
|
|
111
|
-
log(pc.gray(` ├─ ${step}`));
|
|
112
|
-
}
|
|
113
|
-
log(pc.red(` └─ ✗ ${errorMessage}`));
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import spawn from 'nano-spawn';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Executes a command using the specified package manager (e.g., pnpm).
|
|
5
|
-
*
|
|
6
|
-
* @param packageManager - The package manager to use (e.g., 'pnpm').
|
|
7
|
-
* @param args - The arguments to pass to the package manager command.
|
|
8
|
-
* @param env - Additional environment variables to set during command execution.
|
|
9
|
-
* @returns A promise that resolves if the command executes successfully; otherwise, it rejects with an error message.
|
|
10
|
-
*/
|
|
11
|
-
export async function runPackageManagerCommand(
|
|
12
|
-
packageManager: string,
|
|
13
|
-
args: string[],
|
|
14
|
-
env: Record<string, string> = {},
|
|
15
|
-
): Promise<void> {
|
|
16
|
-
try {
|
|
17
|
-
await spawn(packageManager, args, {
|
|
18
|
-
env: { ...process.env, ...env },
|
|
19
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
20
|
-
});
|
|
21
|
-
} catch (error) {
|
|
22
|
-
const err = error as { stdout?: string; stderr?: string };
|
|
23
|
-
throw new Error(`"${packageManager} ${args.join(' ')}" failed ${err.stdout || ''} ${err.stderr || ''}`);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Installs dependencies using the specified package manager.
|
|
29
|
-
*
|
|
30
|
-
* @param packageManager - The package manager to use for installation (e.g., 'pnpm').
|
|
31
|
-
* @returns A promise that resolves if the installation completes successfully; otherwise, it rejects with an error.
|
|
32
|
-
*/
|
|
33
|
-
export async function install(packageManager: string): Promise<void> {
|
|
34
|
-
return runPackageManagerCommand(packageManager, ['install'], {
|
|
35
|
-
NODE_ENV: 'development',
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Generates SQL files using the specified package manager.
|
|
41
|
-
*
|
|
42
|
-
* @param packageManager - The package manager to use for generation (e.g., 'pnpm').
|
|
43
|
-
* @returns A promise that resolves if the generation completes successfully; otherwise, it rejects with an error.
|
|
44
|
-
*/
|
|
45
|
-
export async function generate(packageManager: string): Promise<void> {
|
|
46
|
-
return runPackageManagerCommand(packageManager, ['generate'], {
|
|
47
|
-
NODE_ENV: 'development',
|
|
48
|
-
});
|
|
49
|
-
}
|