@flutry/cli 1.0.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/README.md +157 -0
- package/assets/logo.png +0 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.js +154 -0
- package/dist/commands/new.d.ts +17 -0
- package/dist/commands/new.js +190 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +14 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Flutry-HQ/cli/main/assets/logo.png" alt="Flurty CLI logo" width="180">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# Flurty CLI
|
|
6
|
+
|
|
7
|
+
A small, reliable command-line tool for creating and extending Flutry projects.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- Interactive Flutry project creation
|
|
12
|
+
- Secure `.env` secret generation
|
|
13
|
+
- Optional database configuration with MySQL or MariaDB
|
|
14
|
+
- Route and service generation
|
|
15
|
+
- Sequelize model generation
|
|
16
|
+
- npm, Yarn, and pnpm support
|
|
17
|
+
- Animated and colored terminal feedback
|
|
18
|
+
- Safe protection against overwriting existing files
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Install the package globally:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g @flutry/cli
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or run it directly with `npx`:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx @flutry/cli new
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Create A Project
|
|
35
|
+
|
|
36
|
+
Start the interactive project generator:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
flurty new
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The generator asks for:
|
|
43
|
+
|
|
44
|
+
- Project folder and package name
|
|
45
|
+
- Database activation and connection settings
|
|
46
|
+
- Package manager
|
|
47
|
+
- Dependency installation
|
|
48
|
+
|
|
49
|
+
After creation, the project includes `.env` and `.env.example` files.
|
|
50
|
+
|
|
51
|
+
## Generate A Route
|
|
52
|
+
|
|
53
|
+
Run this command from the root of an existing Flutry project:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
flurty generate route user
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This creates:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
src/routes/user/user.route.ts
|
|
63
|
+
src/routes/user/user.service.ts
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The generated route includes a basic `GET /user` endpoint that returns an `Ok` response.
|
|
67
|
+
|
|
68
|
+
## Generate A Model
|
|
69
|
+
|
|
70
|
+
Create a Sequelize model with:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
flurty generate model user
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This creates:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
src/models/user.model.ts
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The generated model contains an `id` field, a required unique `message` field, and a Sequelize initializer.
|
|
83
|
+
|
|
84
|
+
## Short Alias
|
|
85
|
+
|
|
86
|
+
The `generate` command also has a short `g` alias:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
flurty g route user
|
|
90
|
+
flurty g model user
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Generators only run inside a Flutry project. They check for `package.json` and the Flutry router before creating files.
|
|
94
|
+
|
|
95
|
+
## Environment Configuration
|
|
96
|
+
|
|
97
|
+
The generator creates a complete environment template, including database values even when the database is initially disabled:
|
|
98
|
+
|
|
99
|
+
```env
|
|
100
|
+
PORT=1337
|
|
101
|
+
HOST=0.0.0.0
|
|
102
|
+
PREFIX_API=
|
|
103
|
+
DB=false
|
|
104
|
+
DB_NAME=
|
|
105
|
+
DB_USER=
|
|
106
|
+
DB_PASS=
|
|
107
|
+
DB_HOST=
|
|
108
|
+
DB_PORT=3306
|
|
109
|
+
DB_TYPE=mariadb
|
|
110
|
+
SECRET_KEY=
|
|
111
|
+
SECRET_SALT=
|
|
112
|
+
JWT_SECRET_KEY=
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The real `.env` file receives generated secrets. The `.env.example` file never receives database credentials or generated secrets.
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
Install dependencies:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
yarn install
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Build the CLI:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
yarn build
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Run the compiled CLI:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
yarn start
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Watch TypeScript files during development:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
yarn dev
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Project Structure
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
src/
|
|
147
|
+
commands/
|
|
148
|
+
generate.ts
|
|
149
|
+
new.ts
|
|
150
|
+
types/
|
|
151
|
+
degit.d.ts
|
|
152
|
+
index.ts
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
MIT
|
package/assets/logo.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.generateRoute = generateRoute;
|
|
7
|
+
exports.generateModel = generateModel;
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
const ora_1 = __importDefault(require("ora"));
|
|
12
|
+
function normalizeName(name) {
|
|
13
|
+
const fileName = name.trim().toLowerCase();
|
|
14
|
+
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(fileName)) {
|
|
15
|
+
throw new Error('Name must contain lowercase letters, numbers, or hyphens and start with a letter.');
|
|
16
|
+
}
|
|
17
|
+
const className = fileName
|
|
18
|
+
.split('-')
|
|
19
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
20
|
+
.join('');
|
|
21
|
+
return { fileName, className };
|
|
22
|
+
}
|
|
23
|
+
function routeTemplate(className, fileName) {
|
|
24
|
+
return [
|
|
25
|
+
"import Router from '../../package/router/router';",
|
|
26
|
+
'',
|
|
27
|
+
`export default class ${className}Route extends Router {`,
|
|
28
|
+
' constructor() {',
|
|
29
|
+
' super();',
|
|
30
|
+
` //! [GET] /${fileName} || Responsed OK`,
|
|
31
|
+
" this.get('/', async (ctx) => {",
|
|
32
|
+
" return ctx.send({ message: 'Ok' });",
|
|
33
|
+
' });',
|
|
34
|
+
' }',
|
|
35
|
+
'}',
|
|
36
|
+
'',
|
|
37
|
+
].join('\n');
|
|
38
|
+
}
|
|
39
|
+
function serviceTemplate(className) {
|
|
40
|
+
return `export default class ${className}Service {}\n`;
|
|
41
|
+
}
|
|
42
|
+
function modelTemplate(className, fileName) {
|
|
43
|
+
return [
|
|
44
|
+
"import { Model, DataTypes, Sequelize } from 'sequelize';",
|
|
45
|
+
'',
|
|
46
|
+
`export type ${className}ModelType = {`,
|
|
47
|
+
' id: string;',
|
|
48
|
+
' message: string;',
|
|
49
|
+
'};',
|
|
50
|
+
'',
|
|
51
|
+
`export default class ${className} extends Model {`,
|
|
52
|
+
' static initialize(sequelize: Sequelize) {',
|
|
53
|
+
` ${className}.init(`,
|
|
54
|
+
' {',
|
|
55
|
+
' id: {',
|
|
56
|
+
' type: DataTypes.STRING,',
|
|
57
|
+
' primaryKey: true,',
|
|
58
|
+
' },',
|
|
59
|
+
' message: {',
|
|
60
|
+
' type: DataTypes.STRING,',
|
|
61
|
+
' allowNull: false,',
|
|
62
|
+
' },',
|
|
63
|
+
' },',
|
|
64
|
+
' {',
|
|
65
|
+
' sequelize,',
|
|
66
|
+
` tableName: '${fileName}',`,
|
|
67
|
+
' timestamps: false,',
|
|
68
|
+
' indexes: [',
|
|
69
|
+
' {',
|
|
70
|
+
' unique: true,',
|
|
71
|
+
" fields: ['message'],",
|
|
72
|
+
' },',
|
|
73
|
+
' ],',
|
|
74
|
+
' },',
|
|
75
|
+
' );',
|
|
76
|
+
' }',
|
|
77
|
+
'}',
|
|
78
|
+
'',
|
|
79
|
+
].join('\n');
|
|
80
|
+
}
|
|
81
|
+
async function writeNewFile(filePath, content) {
|
|
82
|
+
try {
|
|
83
|
+
await fs_1.promises.access(filePath);
|
|
84
|
+
throw new Error(`File already exists: ${path_1.default.relative(process.cwd(), filePath)}`);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error instanceof Error && error.message.startsWith('File already exists:'))
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
await fs_1.promises.mkdir(path_1.default.dirname(filePath), { recursive: true });
|
|
91
|
+
await fs_1.promises.writeFile(filePath, content, 'utf8');
|
|
92
|
+
}
|
|
93
|
+
async function isFlutryProject(projectRoot) {
|
|
94
|
+
try {
|
|
95
|
+
await Promise.all([
|
|
96
|
+
fs_1.promises.access(path_1.default.join(projectRoot, 'package.json')),
|
|
97
|
+
fs_1.promises.access(path_1.default.join(projectRoot, 'src', 'package', 'router', 'router.ts')),
|
|
98
|
+
]);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function generate(type, name) {
|
|
106
|
+
const projectRoot = process.cwd();
|
|
107
|
+
if (!(await isFlutryProject(projectRoot))) {
|
|
108
|
+
console.error('This command can only be used inside a Flutry project.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const { fileName, className } = normalizeName(name);
|
|
112
|
+
const sourceRoot = path_1.default.join(projectRoot, 'src');
|
|
113
|
+
const spinner = (0, ora_1.default)({
|
|
114
|
+
text: type === 'route' ? chalk_1.default.cyan(`Generating route ${fileName}`) : chalk_1.default.magenta(`Generating model ${fileName}`),
|
|
115
|
+
spinner: 'dots12',
|
|
116
|
+
color: type === 'route' ? 'cyan' : 'magenta',
|
|
117
|
+
}).start();
|
|
118
|
+
try {
|
|
119
|
+
if (type === 'route') {
|
|
120
|
+
const routeDirectory = path_1.default.join(sourceRoot, 'routes', fileName);
|
|
121
|
+
spinner.text = chalk_1.default.cyan(`Creating ${fileName}.route.ts`);
|
|
122
|
+
await writeNewFile(path_1.default.join(routeDirectory, `${fileName}.route.ts`), routeTemplate(className, fileName));
|
|
123
|
+
spinner.text = chalk_1.default.blue(`Creating ${fileName}.service.ts`);
|
|
124
|
+
await writeNewFile(path_1.default.join(routeDirectory, `${fileName}.service.ts`), serviceTemplate(className));
|
|
125
|
+
spinner.succeed(chalk_1.default.greenBright(`✦ Route generated: src/routes/${fileName}/`));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
spinner.text = chalk_1.default.magenta(`Creating ${fileName}.model.ts`);
|
|
129
|
+
await writeNewFile(path_1.default.join(sourceRoot, 'models', `${fileName}.model.ts`), modelTemplate(className, fileName));
|
|
130
|
+
spinner.succeed(chalk_1.default.greenBright(`✦ Model generated: src/models/${fileName}.model.ts`));
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
spinner.fail(chalk_1.default.red(`Could not generate ${type} ${fileName}`));
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function generateRoute(name) {
|
|
138
|
+
try {
|
|
139
|
+
await generate('route', name);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
console.error(`${chalk_1.default.redBright('✖')} ${error instanceof Error ? error.message : 'Route generation failed.'}`);
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async function generateModel(name) {
|
|
147
|
+
try {
|
|
148
|
+
await generate('model', name);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
console.error(`${chalk_1.default.redBright('✖')} ${error instanceof Error ? error.message : 'Model generation failed.'}`);
|
|
152
|
+
process.exitCode = 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type PackageManager = 'npm' | 'yarn' | 'pnpm';
|
|
2
|
+
type Answers = {
|
|
3
|
+
folderName: string;
|
|
4
|
+
projectName: string;
|
|
5
|
+
database: boolean;
|
|
6
|
+
dbName: string;
|
|
7
|
+
dbUser: string;
|
|
8
|
+
dbPass: string;
|
|
9
|
+
dbHost: string;
|
|
10
|
+
dbPort: string;
|
|
11
|
+
dbType: 'mysql' | 'mariadb';
|
|
12
|
+
packageManager: PackageManager;
|
|
13
|
+
installNow: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare function promptQuestions(): Promise<Answers>;
|
|
16
|
+
export default function newCommand(): Promise<void>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.promptQuestions = promptQuestions;
|
|
7
|
+
exports.default = newCommand;
|
|
8
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
13
|
+
const degit_1 = __importDefault(require("degit"));
|
|
14
|
+
const ora_1 = __importDefault(require("ora"));
|
|
15
|
+
const prompts_1 = require("@inquirer/prompts");
|
|
16
|
+
const DEFAULTS = {
|
|
17
|
+
port: '1337',
|
|
18
|
+
host: '0.0.0.0',
|
|
19
|
+
dbPort: '3306',
|
|
20
|
+
dbType: 'mariadb',
|
|
21
|
+
};
|
|
22
|
+
async function promptQuestions() {
|
|
23
|
+
console.log(`\n${chalk_1.default.cyanBright('╭────────────────────────────────────╮')}`);
|
|
24
|
+
console.log(`${chalk_1.default.cyanBright('│')} ${chalk_1.default.bold.white('✦ FLUTRY')} ${chalk_1.default.gray('project generator')} ${chalk_1.default.cyanBright('│')}`);
|
|
25
|
+
console.log(`${chalk_1.default.cyanBright('╰────────────────────────────────────╯')}`);
|
|
26
|
+
console.log(chalk_1.default.gray(' Create a clean project with a few focused choices.\n'));
|
|
27
|
+
const folderName = await (0, prompts_1.input)({
|
|
28
|
+
message: `${chalk_1.default.cyan('📁')} Project folder:`,
|
|
29
|
+
validate: (value) => validateFolderName(value) || 'Enter a folder name without / or \\.',
|
|
30
|
+
});
|
|
31
|
+
const projectName = await (0, prompts_1.input)({
|
|
32
|
+
message: `${chalk_1.default.blue('📦')} Package name:`,
|
|
33
|
+
default: folderName.toLowerCase().replace(/\s+/g, '-'),
|
|
34
|
+
validate: validatePackageName,
|
|
35
|
+
});
|
|
36
|
+
const database = await (0, prompts_1.confirm)({ message: `${chalk_1.default.yellow('🗄️')} Enable database?`, default: false });
|
|
37
|
+
const databaseAnswers = database
|
|
38
|
+
? {
|
|
39
|
+
dbName: await (0, prompts_1.input)({ message: `${chalk_1.default.yellow('◈')} Database name:`, default: 'flutrydb' }),
|
|
40
|
+
dbUser: await (0, prompts_1.input)({ message: `${chalk_1.default.yellow('◈')} Database user:`, default: 'root' }),
|
|
41
|
+
dbPass: await (0, prompts_1.password)({ message: `${chalk_1.default.yellow('◈')} Database password:`, mask: '*' }),
|
|
42
|
+
dbHost: await (0, prompts_1.input)({ message: `${chalk_1.default.yellow('◈')} Database host:`, default: 'localhost' }),
|
|
43
|
+
dbPort: await (0, prompts_1.input)({ message: `${chalk_1.default.yellow('◈')} Database port:`, default: DEFAULTS.dbPort, validate: validatePort }),
|
|
44
|
+
dbType: await (0, prompts_1.select)({
|
|
45
|
+
message: `${chalk_1.default.yellow('◈')} Database type:`,
|
|
46
|
+
choices: [
|
|
47
|
+
{ name: 'MariaDB', value: 'mariadb' },
|
|
48
|
+
{ name: 'MySQL', value: 'mysql' },
|
|
49
|
+
],
|
|
50
|
+
default: DEFAULTS.dbType,
|
|
51
|
+
}),
|
|
52
|
+
}
|
|
53
|
+
: {
|
|
54
|
+
dbName: '',
|
|
55
|
+
dbUser: '',
|
|
56
|
+
dbPass: '',
|
|
57
|
+
dbHost: '',
|
|
58
|
+
dbPort: '',
|
|
59
|
+
dbType: DEFAULTS.dbType,
|
|
60
|
+
};
|
|
61
|
+
const packageManager = await (0, prompts_1.select)({
|
|
62
|
+
message: `${chalk_1.default.magenta('⚙')} Package manager:`,
|
|
63
|
+
choices: [
|
|
64
|
+
{ name: 'npm', value: 'npm' },
|
|
65
|
+
{ name: 'Yarn', value: 'yarn' },
|
|
66
|
+
{ name: 'pnpm', value: 'pnpm' },
|
|
67
|
+
],
|
|
68
|
+
default: 'npm',
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
folderName,
|
|
72
|
+
projectName,
|
|
73
|
+
database,
|
|
74
|
+
...databaseAnswers,
|
|
75
|
+
packageManager,
|
|
76
|
+
installNow: await (0, prompts_1.confirm)({ message: `${chalk_1.default.green('⚡')} Install dependencies now?`, default: true }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function buildEnv(answers, example) {
|
|
80
|
+
const secret = (name) => (example ? `your_${name.toLowerCase()}` : crypto_1.default.randomBytes(64).toString('hex'));
|
|
81
|
+
return [
|
|
82
|
+
'# Server',
|
|
83
|
+
`PORT=${DEFAULTS.port}`,
|
|
84
|
+
`HOST=${DEFAULTS.host}`,
|
|
85
|
+
'PREFIX_API=',
|
|
86
|
+
'# Database',
|
|
87
|
+
`DB=${answers.database ? 'true' : 'false'}`,
|
|
88
|
+
`DB_NAME=${example ? '' : answers.dbName}`,
|
|
89
|
+
`DB_USER=${example ? '' : answers.dbUser}`,
|
|
90
|
+
`DB_PASS=${example ? '' : answers.dbPass}`,
|
|
91
|
+
`DB_HOST=${example ? '' : answers.dbHost}`,
|
|
92
|
+
`DB_PORT=${example ? '' : answers.dbPort}`,
|
|
93
|
+
`DB_TYPE=${answers.dbType}`,
|
|
94
|
+
'# Secrets',
|
|
95
|
+
`SECRET_KEY=${secret('SECRET_KEY')}`,
|
|
96
|
+
`SECRET_SALT=${secret('SECRET_SALT')}`,
|
|
97
|
+
`JWT_SECRET_KEY=${secret('JWT_SECRET_KEY')}`,
|
|
98
|
+
,
|
|
99
|
+
].join('\n');
|
|
100
|
+
}
|
|
101
|
+
async function removeIfPresent(filePath) {
|
|
102
|
+
await fs_1.promises.rm(filePath, { recursive: true, force: true });
|
|
103
|
+
}
|
|
104
|
+
async function createProject(answers) {
|
|
105
|
+
const targetDir = path_1.default.resolve(process.cwd(), answers.folderName);
|
|
106
|
+
const spinner = (0, ora_1.default)({ text: chalk_1.default.cyan('Creating project'), spinner: 'dots12', color: 'cyan' }).start();
|
|
107
|
+
try {
|
|
108
|
+
if (await exists(targetDir))
|
|
109
|
+
throw new Error(`Folder "${answers.folderName}" already exists.`);
|
|
110
|
+
spinner.text = chalk_1.default.blue('Downloading project template');
|
|
111
|
+
await (0, degit_1.default)('https://github.com/Flutry-HQ/Flutry.git', { cache: false, force: true }).clone(targetDir);
|
|
112
|
+
spinner.text = chalk_1.default.magenta('Applying project settings');
|
|
113
|
+
const packageJsonPath = path_1.default.join(targetDir, 'package.json');
|
|
114
|
+
const packageJson = JSON.parse(await fs_1.promises.readFile(packageJsonPath, 'utf8'));
|
|
115
|
+
packageJson.name = answers.projectName;
|
|
116
|
+
await fs_1.promises.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
|
|
117
|
+
await Promise.all([
|
|
118
|
+
fs_1.promises.writeFile(path_1.default.join(targetDir, '.env'), buildEnv(answers, false), 'utf8'),
|
|
119
|
+
fs_1.promises.writeFile(path_1.default.join(targetDir, '.env.example'), buildEnv(answers, true), 'utf8'),
|
|
120
|
+
removeIfPresent(path_1.default.join(targetDir, '.gitattributes')),
|
|
121
|
+
]);
|
|
122
|
+
await fs_1.promises.writeFile(path_1.default.join(targetDir, 'README.md'), `# ${answers.folderName}\n\nGenerated by Flutry CLI.\n`, 'utf8');
|
|
123
|
+
spinner.succeed(chalk_1.default.greenBright('Project created successfully'));
|
|
124
|
+
return targetDir;
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
spinner.fail(chalk_1.default.red('Project creation failed'));
|
|
128
|
+
await removeIfPresent(targetDir);
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function installPackages(targetDir, packageManager) {
|
|
133
|
+
const spinner = (0, ora_1.default)({ text: chalk_1.default.yellow(`Installing dependencies with ${packageManager}`), spinner: 'arc', color: 'yellow' }).start();
|
|
134
|
+
await new Promise((resolve, reject) => {
|
|
135
|
+
const executable = process.platform === 'win32' ? `${packageManager}.cmd` : packageManager;
|
|
136
|
+
const command = process.platform === 'win32' ? (process.env.ComSpec ?? 'cmd.exe') : executable;
|
|
137
|
+
const commandArgs = process.platform === 'win32' ? ['/d', '/s', '/c', executable, 'install'] : ['install'];
|
|
138
|
+
const child = (0, child_process_1.spawn)(command, commandArgs, { cwd: targetDir, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
139
|
+
let errorOutput = '';
|
|
140
|
+
child.stderr.on('data', (data) => {
|
|
141
|
+
errorOutput += data.toString();
|
|
142
|
+
});
|
|
143
|
+
child.on('error', reject);
|
|
144
|
+
child.on('close', (code) => {
|
|
145
|
+
if (code === 0)
|
|
146
|
+
resolve();
|
|
147
|
+
else
|
|
148
|
+
reject(new Error(`${packageManager} install failed${errorOutput ? `: ${errorOutput.trim()}` : ''}`));
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
spinner.succeed(chalk_1.default.greenBright('Dependencies installed'));
|
|
152
|
+
}
|
|
153
|
+
async function exists(filePath) {
|
|
154
|
+
try {
|
|
155
|
+
await fs_1.promises.access(filePath);
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function validateFolderName(value) {
|
|
163
|
+
return value.trim().length > 0 && value !== '.' && value !== '..' && !/[\\/]/.test(value);
|
|
164
|
+
}
|
|
165
|
+
function validatePort(value) {
|
|
166
|
+
return /^\d{1,5}$/.test(value) && Number(value) > 0 && Number(value) <= 65535 ? true : 'Enter a valid port (1-65535).';
|
|
167
|
+
}
|
|
168
|
+
function validatePackageName(name) {
|
|
169
|
+
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name) ? true : 'Use lowercase letters, numbers and hyphens only.';
|
|
170
|
+
}
|
|
171
|
+
async function newCommand() {
|
|
172
|
+
try {
|
|
173
|
+
const answers = await promptQuestions();
|
|
174
|
+
const targetDir = await createProject(answers);
|
|
175
|
+
if (answers.installNow)
|
|
176
|
+
await installPackages(targetDir, answers.packageManager);
|
|
177
|
+
console.log(`\n${chalk_1.default.greenBright('╭─')} ${chalk_1.default.bold.green('✓ Project ready')} ${chalk_1.default.greenBright('─'.repeat(19))}`);
|
|
178
|
+
console.log(`${chalk_1.default.greenBright('│')} ${chalk_1.default.gray('Location:')} ${chalk_1.default.cyan(`./${answers.folderName}`)}`);
|
|
179
|
+
console.log(`${chalk_1.default.greenBright('│')} ${chalk_1.default.gray('Next:')}`);
|
|
180
|
+
console.log(`${chalk_1.default.greenBright('│')} ${chalk_1.default.cyan(`cd ${answers.folderName}`)}`);
|
|
181
|
+
if (!answers.installNow)
|
|
182
|
+
console.log(`${chalk_1.default.greenBright('│')} ${chalk_1.default.yellow(`${answers.packageManager} install`)}`);
|
|
183
|
+
console.log(`${chalk_1.default.greenBright('│')} ${chalk_1.default.green(`${answers.packageManager} dev`)}`);
|
|
184
|
+
console.log(`${chalk_1.default.greenBright('╰────────────────────────────────────╯')}\n`);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
console.error(`\n${chalk_1.default.redBright('✖')} ${error instanceof Error ? error.message : 'Command failed.'}`);
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
}
|
|
190
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const new_1 = __importDefault(require("./commands/new"));
|
|
9
|
+
const generate_1 = require("./commands/generate");
|
|
10
|
+
commander_1.program.command('new').description('Create a new Flutry project').action(new_1.default);
|
|
11
|
+
const generate = commander_1.program.command('generate').alias('g').description('Generate a route or model');
|
|
12
|
+
generate.command('route <name>').description('Generate a route and service').action(generate_1.generateRoute);
|
|
13
|
+
generate.command('model <name>').description('Generate a Sequelize model').action(generate_1.generateModel);
|
|
14
|
+
commander_1.program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flutry/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Flurty project scaffolding CLI",
|
|
5
|
+
"bin": {
|
|
6
|
+
"flurty": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"assets",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"repository": "https://github.com/Flutry-HQ/cli.git",
|
|
16
|
+
"author": "HEDI <admin@otamoon.hu>",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"dev": "tsc --watch"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^26.2.0",
|
|
25
|
+
"typescript": "^7.0.2"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@inquirer/prompts": "^7.8.4",
|
|
29
|
+
"chalk": "^4.1.2",
|
|
30
|
+
"commander": "^15.0.0",
|
|
31
|
+
"degit": "^2.8.4",
|
|
32
|
+
"ora": "^9.4.1"
|
|
33
|
+
}
|
|
34
|
+
}
|