@minij/cli 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/create.d.ts +4 -0
- package/dist/commands/create.js +77 -0
- package/dist/commands/gitlab-setup.d.ts +3 -0
- package/dist/commands/gitlab-setup.js +80 -0
- package/dist/commands/init-local.d.ts +1 -0
- package/dist/commands/init-local.js +56 -0
- package/dist/commands/init.d.ts +1 -0
- package/{src/commands/init.ts → dist/commands/init.js} +15 -18
- package/dist/commands/server-deploy.d.ts +1 -0
- package/dist/commands/server-deploy.js +28 -0
- package/dist/constants.d.ts +4 -0
- package/{src/constants.ts → dist/constants.js} +0 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +49 -0
- package/dist/services/gitlab.d.ts +7 -0
- package/dist/services/gitlab.js +72 -0
- package/dist/utils/config.d.ts +6 -0
- package/dist/utils/config.js +22 -0
- package/dist/utils/loader.d.ts +3 -0
- package/dist/utils/loader.js +20 -0
- package/dist/utils/ssh.d.ts +5 -0
- package/dist/utils/ssh.js +35 -0
- package/dist/utils/template-replacer.d.ts +1 -0
- package/dist/utils/template-replacer.js +50 -0
- package/package.json +5 -1
- package/CLAUDE.md +0 -23
- package/deploy.sh +0 -33
- package/local_deploy.sh +0 -14
- package/src/commands/create.ts +0 -94
- package/src/commands/server-deploy.ts +0 -40
- package/src/index.ts +0 -53
- package/src/services/gitlab.ts +0 -94
- package/src/utils/config.ts +0 -28
- package/src/utils/loader.ts +0 -25
- package/src/utils/ssh.ts +0 -47
- package/src/utils/template-replacer.ts +0 -84
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import shelljs from 'shelljs';
|
|
3
|
+
const shell = shelljs;
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import readline from 'node:readline/promises';
|
|
7
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
8
|
+
import { TEMPLATE_URL } from '../constants.js';
|
|
9
|
+
import { withLoader } from '../utils/loader.js';
|
|
10
|
+
import { applyTemplateReplacements } from '../utils/template-replacer.js';
|
|
11
|
+
import { createGitlabRepo, initLocalGitRepo, setupCiVariables } from '../services/gitlab.js';
|
|
12
|
+
export async function createCommand(name, options = {}) {
|
|
13
|
+
validateProjectName(name);
|
|
14
|
+
const projectPath = path.resolve(name);
|
|
15
|
+
if (await fs.pathExists(projectPath)) {
|
|
16
|
+
throw new Error(`目录已存在: ${projectPath}`);
|
|
17
|
+
}
|
|
18
|
+
const backendPort = await resolveBackendPort(options.port);
|
|
19
|
+
await withLoader(`正在克隆模板到 ${name}...`, async () => {
|
|
20
|
+
const result = shell.exec(`git clone ${TEMPLATE_URL} ${name}`, { silent: true });
|
|
21
|
+
if (result.code !== 0) {
|
|
22
|
+
throw new Error(`克隆失败: ${result.stderr}`);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
await withLoader('正在清理 .git 目录...', async () => {
|
|
26
|
+
await fs.remove(path.join(projectPath, '.git'));
|
|
27
|
+
});
|
|
28
|
+
await withLoader('正在替换模板内容...', async () => {
|
|
29
|
+
await applyTemplateReplacements(projectPath, name, backendPort);
|
|
30
|
+
});
|
|
31
|
+
const { sshUrl, projectId } = await createGitlabRepo(name);
|
|
32
|
+
await setupCiVariables(projectId, name, backendPort);
|
|
33
|
+
await initLocalGitRepo(projectPath, sshUrl);
|
|
34
|
+
const url = `https://dataverse.minij.com/DataVerse/${name}/`;
|
|
35
|
+
console.log(chalk.green(`\n✓ 项目 ${name} 创建完成!`));
|
|
36
|
+
console.log(chalk.white('\n项目信息:'));
|
|
37
|
+
console.log(chalk.gray(`目录: ${projectPath}`));
|
|
38
|
+
console.log(chalk.gray(`远程仓库: ${sshUrl}`));
|
|
39
|
+
console.log(chalk.gray(`Node 端口: ${backendPort}`));
|
|
40
|
+
console.log(chalk.yellow('\n提示:GitLab CI/CD 正在构建,约 20S后可访问'));
|
|
41
|
+
console.log(chalk.white('\n访问地址:'));
|
|
42
|
+
console.log(chalk.bold.blue(`\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\`));
|
|
43
|
+
console.log(chalk.gray('\n下一步:'));
|
|
44
|
+
console.log(chalk.gray(`cd ${name}`));
|
|
45
|
+
console.log(chalk.gray(`minij server-deploy ${name}`));
|
|
46
|
+
}
|
|
47
|
+
function validateProjectName(name) {
|
|
48
|
+
const validNamePattern = /^[a-zA-Z0-9_-]+$/;
|
|
49
|
+
if (!validNamePattern.test(name)) {
|
|
50
|
+
throw new Error('项目名仅支持字母、数字、中划线和下划线');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function resolveBackendPort(inputPort) {
|
|
54
|
+
if (inputPort) {
|
|
55
|
+
validateBackendPort(inputPort);
|
|
56
|
+
return inputPort;
|
|
57
|
+
}
|
|
58
|
+
const rl = readline.createInterface({ input, output });
|
|
59
|
+
try {
|
|
60
|
+
const answer = await rl.question('请输入 Node 服务端口号(3000-3100,默认 3001): ');
|
|
61
|
+
const backendPort = answer.trim() || '3001';
|
|
62
|
+
validateBackendPort(backendPort);
|
|
63
|
+
return backendPort;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
rl.close();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function validateBackendPort(port) {
|
|
70
|
+
if (!/^\d+$/.test(port)) {
|
|
71
|
+
throw new Error('Node 服务端口号必须是数字,范围 3000-3100,默认 3001');
|
|
72
|
+
}
|
|
73
|
+
const numericPort = Number(port);
|
|
74
|
+
if (numericPort < 3000 || numericPort > 3100) {
|
|
75
|
+
throw new Error('Node 服务端口号必须在 3000 到 3100 之间,默认 3001');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import shelljs from 'shelljs';
|
|
3
|
+
const shell = shelljs;
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import { withLoader } from '../utils/loader.js';
|
|
6
|
+
import { getOrchestratorUrl } from '../utils/config.js';
|
|
7
|
+
export async function createGitlabRepo(name) {
|
|
8
|
+
const orchestratorUrl = await getOrchestratorUrl();
|
|
9
|
+
return withLoader('正在创建 GitLab 仓库...', async () => {
|
|
10
|
+
try {
|
|
11
|
+
const response = await axios.post(`${orchestratorUrl}/api/create-repo`, {
|
|
12
|
+
projectName: name,
|
|
13
|
+
userName: 'deployer-bot'
|
|
14
|
+
});
|
|
15
|
+
if (!response.data.sshUrl) {
|
|
16
|
+
throw new Error('接口未返回 SSH 地址');
|
|
17
|
+
}
|
|
18
|
+
return response.data.sshUrl;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (axios.isAxiosError(error)) {
|
|
22
|
+
throw new Error(`API 请求失败: ${error.message}`);
|
|
23
|
+
}
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function initLocalGitRepo(projectPath, sshUrl) {
|
|
29
|
+
await withLoader('正在初始化 Git 仓库...', async () => {
|
|
30
|
+
const initResult = shell.exec('git init', {
|
|
31
|
+
silent: true,
|
|
32
|
+
cwd: projectPath,
|
|
33
|
+
});
|
|
34
|
+
if (initResult.code !== 0) {
|
|
35
|
+
throw new Error(`git init 失败: ${initResult.stderr}`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
await withLoader('正在添加远程仓库...', async () => {
|
|
39
|
+
const remoteResult = shell.exec(`git remote add origin ${sshUrl}`, {
|
|
40
|
+
silent: true,
|
|
41
|
+
cwd: projectPath,
|
|
42
|
+
});
|
|
43
|
+
if (remoteResult.code !== 0) {
|
|
44
|
+
throw new Error(`添加远程仓库失败: ${remoteResult.stderr}`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
await withLoader('正在提交初始代码...', async () => {
|
|
48
|
+
const addResult = shell.exec('git add .', {
|
|
49
|
+
silent: true,
|
|
50
|
+
cwd: projectPath,
|
|
51
|
+
});
|
|
52
|
+
if (addResult.code !== 0) {
|
|
53
|
+
throw new Error(`git add 失败: ${addResult.stderr}`);
|
|
54
|
+
}
|
|
55
|
+
const commitResult = shell.exec('git commit -m "chore: init project"', {
|
|
56
|
+
silent: true,
|
|
57
|
+
cwd: projectPath,
|
|
58
|
+
});
|
|
59
|
+
if (commitResult.code !== 0) {
|
|
60
|
+
throw new Error(`git commit 失败: ${commitResult.stderr}`);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
await withLoader('正在推送初始代码...', async () => {
|
|
64
|
+
const pushResult = shell.exec('git push -u origin main', {
|
|
65
|
+
silent: true,
|
|
66
|
+
cwd: projectPath,
|
|
67
|
+
});
|
|
68
|
+
if (pushResult.code !== 0) {
|
|
69
|
+
throw new Error(`git push 失败: ${pushResult.stderr}`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export async function gitlabSetupCommand(name) {
|
|
74
|
+
const sshUrl = await createGitlabRepo(name);
|
|
75
|
+
const projectPath = process.cwd();
|
|
76
|
+
await initLocalGitRepo(projectPath, sshUrl);
|
|
77
|
+
console.log(chalk.green(`\n✓ GitLab 仓库 ${name} 创建完成!`));
|
|
78
|
+
console.log(chalk.cyan(`\n远程仓库地址:`));
|
|
79
|
+
console.log(chalk.gray(sshUrl));
|
|
80
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initLocalCommand(name: string): Promise<void>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import shelljs from 'shelljs';
|
|
3
|
+
const shell = shelljs;
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { withLoader } from '../utils/loader.js';
|
|
7
|
+
import { TEMPLATE_URL } from '../constants.js';
|
|
8
|
+
export async function initLocalCommand(name) {
|
|
9
|
+
// 1. 克隆模板
|
|
10
|
+
await withLoader(`正在克隆模板到 ${name}...`, async () => {
|
|
11
|
+
const result = shell.exec(`git clone ${TEMPLATE_URL} ${name}`, { silent: true });
|
|
12
|
+
if (result.code !== 0) {
|
|
13
|
+
throw new Error(`克隆失败: ${result.stderr}`);
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
// 2. 清理 .git 文件夹
|
|
17
|
+
const projectPath = path.resolve(name);
|
|
18
|
+
const gitPath = path.join(projectPath, '.git');
|
|
19
|
+
await withLoader('正在清理 .git 目录...', async () => {
|
|
20
|
+
await fs.remove(gitPath);
|
|
21
|
+
});
|
|
22
|
+
// 3. 修改 package.json
|
|
23
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
24
|
+
await withLoader('正在修改 package.json...', async () => {
|
|
25
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
26
|
+
packageJson.name = name;
|
|
27
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
28
|
+
});
|
|
29
|
+
// 4. 修改 vite.config.ts 中的 base 字段
|
|
30
|
+
const viteConfigPath = path.join(projectPath, 'vite.config.ts');
|
|
31
|
+
if (await fs.pathExists(viteConfigPath)) {
|
|
32
|
+
await withLoader('正在修改 vite.config.ts...', async () => {
|
|
33
|
+
const viteConfigContent = await fs.readFile(viteConfigPath, 'utf-8');
|
|
34
|
+
// 正则匹配 base: '/xxx' 或 base: "/xxx" 或 base: '/', base: "/" 等多种情况
|
|
35
|
+
const basePattern = /base:\s*['"][^'"]*['"]/g;
|
|
36
|
+
const newBase = `base: '/assest/${name}/'`;
|
|
37
|
+
const modifiedContent = viteConfigContent.replace(basePattern, newBase);
|
|
38
|
+
await fs.writeFile(viteConfigPath, modifiedContent, 'utf-8');
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
// 5. 修改 src/router/index.ts 中的 createWebHistory
|
|
42
|
+
const routerPath = path.join(projectPath, 'src/router/index.ts');
|
|
43
|
+
if (await fs.pathExists(routerPath)) {
|
|
44
|
+
await withLoader('正在修改 router 配置...', async () => {
|
|
45
|
+
const routerContent = await fs.readFile(routerPath, 'utf-8');
|
|
46
|
+
// 匹配 createWebHistory(\`xxx\`) 或 createWebHistory('xxx') 或 createWebHistory("/xxx")
|
|
47
|
+
const routerPattern = /createWebHistory\([^)]*\)/g;
|
|
48
|
+
const newHistory = `createWebHistory('/assest/${name}/')`;
|
|
49
|
+
const modifiedContent = routerContent.replace(routerPattern, newHistory);
|
|
50
|
+
await fs.writeFile(routerPath, modifiedContent, 'utf-8');
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
console.log(chalk.green(`\n✓ 项目 ${name} 初始化完成!`));
|
|
54
|
+
console.log(chalk.cyan(`\n进入项目目录:`));
|
|
55
|
+
console.log(chalk.gray(`cd ${name}`));
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initCommand(): Promise<void>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { checkSshConnectivity } from '../utils/ssh.js';
|
|
4
|
-
|
|
5
4
|
const CLAUDE_MD_CONTENT = `# 小吉 AI 执行规则
|
|
6
5
|
|
|
7
6
|
你是小吉建站系统的 AI 助手。
|
|
@@ -98,21 +97,19 @@ minij init
|
|
|
98
97
|
- 默认 GitLab 主机是 \`git.minij.com\`
|
|
99
98
|
- 默认创建仓库用户是 \`deployer-bot\`
|
|
100
99
|
`;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
100
|
+
export async function initCommand() {
|
|
101
|
+
await fs.writeFile('CLAUDE.md', CLAUDE_MD_CONTENT, 'utf-8');
|
|
102
|
+
console.log(chalk.green('✓ CLAUDE.md 文件已生成'));
|
|
103
|
+
const sshResult = await checkSshConnectivity();
|
|
104
|
+
if (sshResult.success) {
|
|
105
|
+
console.log(chalk.green(`✓ ${sshResult.message}`));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
console.log(chalk.red(`✗ ${sshResult.message}`));
|
|
109
|
+
console.log(chalk.yellow('\n请按以下步骤配置 SSH 公钥:'));
|
|
110
|
+
console.log(chalk.cyan('1. 生成 SSH 密钥对:ssh-keygen -t ed25519 -C "your_email@example.com"'));
|
|
111
|
+
console.log(chalk.cyan('2. 查看公钥:cat ~/.ssh/id_ed25519.pub'));
|
|
112
|
+
console.log(chalk.cyan('3. 复制公钥到 GitLab -> Settings -> SSH Keys'));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
118
115
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function serverDeployCommand(name: string): Promise<void>;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
import { withLoader } from '../utils/loader.js';
|
|
4
|
+
import { getOrchestratorUrl } from '../utils/config.js';
|
|
5
|
+
export async function serverDeployCommand(name) {
|
|
6
|
+
const orchestratorUrl = await getOrchestratorUrl();
|
|
7
|
+
// 调用远程接口设置路由
|
|
8
|
+
const resultUrl = await withLoader('正在部署服务...', async () => {
|
|
9
|
+
try {
|
|
10
|
+
const response = await axios.post(`${orchestratorUrl}/api/setup-router`, {
|
|
11
|
+
projectName: name
|
|
12
|
+
});
|
|
13
|
+
if (!response.data.success) {
|
|
14
|
+
throw new Error('部署失败');
|
|
15
|
+
}
|
|
16
|
+
return response.data.url;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (axios.isAxiosError(error)) {
|
|
20
|
+
throw new Error(`API 请求失败: ${error.message}`);
|
|
21
|
+
}
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
console.log(chalk.green(`\n✓ 服务部署完成!`));
|
|
26
|
+
console.log(chalk.cyan(`\n访问地址:`));
|
|
27
|
+
console.log(chalk.bold.underline(resultUrl));
|
|
28
|
+
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
// 模板地址配置
|
|
2
2
|
export const TEMPLATE_URL = 'git@git.minij.com:dataverse/minij-bi-admin-template.git';
|
|
3
|
-
|
|
4
3
|
// GitLab 配置
|
|
5
4
|
export const GITLAB_HOST = 'git.minij.com';
|
|
6
|
-
|
|
7
5
|
// 编排服务地址(固定部署在小吉 608 内网,需在该内网环境下使用)
|
|
8
6
|
export const DEFAULT_ORCHESTRATOR_URL = 'http://192.168.50.41:8888';
|
|
9
|
-
|
|
10
7
|
// 配置文件路径
|
|
11
8
|
export const CONFIG_PATH = `${process.env.HOME}/.minij/config.json`;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { initCommand } from './commands/init.js';
|
|
5
|
+
import { serverDeployCommand } from './commands/server-deploy.js';
|
|
6
|
+
import { createCommand } from './commands/create.js';
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name('minij')
|
|
10
|
+
.description('minij一键建站方案 - 本地触发端')
|
|
11
|
+
.version('1.0.0');
|
|
12
|
+
program
|
|
13
|
+
.command('create <name>')
|
|
14
|
+
.description('一键创建项目:克隆模板、替换项目名、创建 GitLab 仓库、初始化 git 并推送')
|
|
15
|
+
.option('-p, --port <port>', 'Node 服务端口号(3000-3100,默认 3001)')
|
|
16
|
+
.action(async (name, options) => {
|
|
17
|
+
try {
|
|
18
|
+
await createCommand(name, options);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error(chalk.red(`\n错误: ${error.message}`));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
program
|
|
26
|
+
.command('init')
|
|
27
|
+
.description('初始化项目环境,生成 CLAUDE.md 并检查 SSH 配置')
|
|
28
|
+
.action(async () => {
|
|
29
|
+
try {
|
|
30
|
+
await initCommand();
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.error(chalk.red(`\n错误: ${error.message}`));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
program
|
|
38
|
+
.command('server-deploy <name>')
|
|
39
|
+
.description('调用远程接口部署服务')
|
|
40
|
+
.action(async (name) => {
|
|
41
|
+
try {
|
|
42
|
+
await serverDeployCommand(name);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error(chalk.red(`\n错误: ${error.message}`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
program.parse();
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface CreateGitlabRepoResult {
|
|
2
|
+
sshUrl: string;
|
|
3
|
+
projectId: number;
|
|
4
|
+
}
|
|
5
|
+
export declare function createGitlabRepo(name: string): Promise<CreateGitlabRepoResult>;
|
|
6
|
+
export declare function setupCiVariables(projectId: number, name: string, backendPort: string): Promise<void>;
|
|
7
|
+
export declare function initLocalGitRepo(projectPath: string, sshUrl: string): Promise<void>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import shelljs from 'shelljs';
|
|
2
|
+
const shell = shelljs;
|
|
3
|
+
import axios from 'axios';
|
|
4
|
+
import { withLoader } from '../utils/loader.js';
|
|
5
|
+
import { getOrchestratorUrl } from '../utils/config.js';
|
|
6
|
+
export async function createGitlabRepo(name) {
|
|
7
|
+
const orchestratorUrl = await getOrchestratorUrl();
|
|
8
|
+
return withLoader('正在创建 GitLab 仓库...', async () => {
|
|
9
|
+
try {
|
|
10
|
+
const response = await axios.post(`${orchestratorUrl}/api/create-repo`, { projectName: name, userName: 'deployer-bot' });
|
|
11
|
+
if (!response.data.sshUrl || !response.data.projectId) {
|
|
12
|
+
throw new Error('接口未返回完整的仓库信息');
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
sshUrl: response.data.sshUrl,
|
|
16
|
+
projectId: response.data.projectId,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (axios.isAxiosError(error)) {
|
|
21
|
+
throw new Error(`API 请求失败: ${error.message}`);
|
|
22
|
+
}
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export async function setupCiVariables(projectId, name, backendPort) {
|
|
28
|
+
const orchestratorUrl = await getOrchestratorUrl();
|
|
29
|
+
await withLoader('正在绑定 CI/CD 变量...', async () => {
|
|
30
|
+
try {
|
|
31
|
+
await axios.post(`${orchestratorUrl}/api/setup-ci-variables`, {
|
|
32
|
+
projectId,
|
|
33
|
+
name,
|
|
34
|
+
backendPort,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (axios.isAxiosError(error)) {
|
|
39
|
+
throw new Error(`API 请求失败: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export async function initLocalGitRepo(projectPath, sshUrl) {
|
|
46
|
+
await withLoader('正在初始化 Git 仓库...', async () => {
|
|
47
|
+
const result = shell.exec('git init', { silent: true, cwd: projectPath });
|
|
48
|
+
if (result.code !== 0)
|
|
49
|
+
throw new Error(`git init 失败: ${result.stderr}`);
|
|
50
|
+
const branchResult = shell.exec('git branch -M main', { silent: true, cwd: projectPath });
|
|
51
|
+
if (branchResult.code !== 0)
|
|
52
|
+
throw new Error(`git branch 失败: ${branchResult.stderr}`);
|
|
53
|
+
});
|
|
54
|
+
await withLoader('正在添加远程仓库...', async () => {
|
|
55
|
+
const result = shell.exec(`git remote add origin ${sshUrl}`, { silent: true, cwd: projectPath });
|
|
56
|
+
if (result.code !== 0)
|
|
57
|
+
throw new Error(`添加远程仓库失败: ${result.stderr}`);
|
|
58
|
+
});
|
|
59
|
+
await withLoader('正在提交初始代码...', async () => {
|
|
60
|
+
const addResult = shell.exec('git add .', { silent: true, cwd: projectPath });
|
|
61
|
+
if (addResult.code !== 0)
|
|
62
|
+
throw new Error(`git add 失败: ${addResult.stderr}`);
|
|
63
|
+
const commitResult = shell.exec('git commit -m "chore: init project"', { silent: true, cwd: projectPath });
|
|
64
|
+
if (commitResult.code !== 0)
|
|
65
|
+
throw new Error(`git commit 失败: ${commitResult.stderr}`);
|
|
66
|
+
});
|
|
67
|
+
await withLoader('正在推送初始代码...', async () => {
|
|
68
|
+
const result = shell.exec('git push -u origin main', { silent: true, cwd: projectPath });
|
|
69
|
+
if (result.code !== 0)
|
|
70
|
+
throw new Error(`git push 失败: ${result.stderr}`);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { CONFIG_PATH, DEFAULT_ORCHESTRATOR_URL } from '../constants.js';
|
|
4
|
+
export async function loadConfig() {
|
|
5
|
+
try {
|
|
6
|
+
const config = await fs.readJson(CONFIG_PATH);
|
|
7
|
+
return config;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
// 配置文件不存在,返回默认值
|
|
11
|
+
return { ORCHESTRATOR_URL: DEFAULT_ORCHESTRATOR_URL };
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function saveConfig(config) {
|
|
15
|
+
await fs.ensureFile(CONFIG_PATH);
|
|
16
|
+
await fs.writeJson(CONFIG_PATH, config, { spaces: 2 });
|
|
17
|
+
}
|
|
18
|
+
export async function getOrchestratorUrl() {
|
|
19
|
+
const config = await loadConfig();
|
|
20
|
+
console.log(chalk.gray(`提示:minij CLI 需在小吉 608 内网中使用,服务地址:${config.ORCHESTRATOR_URL}`));
|
|
21
|
+
return config.ORCHESTRATOR_URL;
|
|
22
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
export function createLoader(text) {
|
|
3
|
+
return ora({
|
|
4
|
+
text,
|
|
5
|
+
color: 'cyan'
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
export async function withLoader(text, fn) {
|
|
9
|
+
const spinner = createLoader(text);
|
|
10
|
+
spinner.start();
|
|
11
|
+
try {
|
|
12
|
+
const result = await fn();
|
|
13
|
+
spinner.succeed();
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
spinner.fail();
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import shelljs from 'shelljs';
|
|
2
|
+
const { exec } = shelljs;
|
|
3
|
+
import { GITLAB_HOST } from '../constants.js';
|
|
4
|
+
export async function checkSshConnectivity() {
|
|
5
|
+
return new Promise((resolve) => {
|
|
6
|
+
const command = `ssh -T git@${GITLAB_HOST} 2>&1`;
|
|
7
|
+
exec(command, { silent: true }, (code, stdout, stderr) => {
|
|
8
|
+
const output = stdout + stderr;
|
|
9
|
+
const lowerOutput = output.toLowerCase();
|
|
10
|
+
// 成功标志:GitLab 系列或 Gitee 的欢迎语,或包含用户名提示
|
|
11
|
+
const isSuccess = lowerOutput.includes('welcome to gitlab') ||
|
|
12
|
+
lowerOutput.includes('welcome to gitee') ||
|
|
13
|
+
lowerOutput.includes('you\'ve successfully authenticated') ||
|
|
14
|
+
// 私有部署的 GitLab 通常也会包含用户名
|
|
15
|
+
lowerOutput.includes('hi ');
|
|
16
|
+
// 明确失败标志
|
|
17
|
+
const isFail = lowerOutput.includes('permission denied') ||
|
|
18
|
+
lowerOutput.includes('publickey') ||
|
|
19
|
+
lowerOutput.includes('no such host') ||
|
|
20
|
+
lowerOutput.includes('connection refused');
|
|
21
|
+
if (isSuccess || (!isFail && output.length > 0)) {
|
|
22
|
+
resolve({
|
|
23
|
+
success: true,
|
|
24
|
+
message: 'SSH 连接成功'
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
resolve({
|
|
29
|
+
success: false,
|
|
30
|
+
message: 'SSH 连接失败,请先配置公钥'
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function applyTemplateReplacements(projectPath: string, name: string, backendPort: string): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const TEMPLATE_NAME = 'minij-bi-admin-template';
|
|
4
|
+
export async function applyTemplateReplacements(projectPath, name, backendPort) {
|
|
5
|
+
await replaceJsonField(path.join(projectPath, 'package.json'), (pkg) => {
|
|
6
|
+
pkg.name = name;
|
|
7
|
+
return pkg;
|
|
8
|
+
});
|
|
9
|
+
await replaceInFile(path.join(projectPath, 'frontend/index.html'), /<title>[^<]*<\/title>/, `<title>${name}</title>`);
|
|
10
|
+
await replaceAllInFile(path.join(projectPath, 'frontend/vite.config.ts'), TEMPLATE_NAME, name);
|
|
11
|
+
await replaceInFile(path.join(projectPath, 'frontend/src/router/index.ts'), /createWebHistory\([^)]*\)/g, `createWebHistory('/DataVerse/${name}/')`);
|
|
12
|
+
await replaceAllInFile(path.join(projectPath, 'frontend/src/router/guards.ts'), TEMPLATE_NAME, name);
|
|
13
|
+
await replaceAllInFile(path.join(projectPath, 'frontend/src/layouts/AdminLayout.vue'), TEMPLATE_NAME, name);
|
|
14
|
+
await replaceAllInFile(path.join(projectPath, 'frontend/src/views/home/HomeView.vue'), TEMPLATE_NAME, name);
|
|
15
|
+
await replaceAllInFile(path.join(projectPath, '.gitlab-ci.yml'), TEMPLATE_NAME, name);
|
|
16
|
+
await replaceAllInFile(path.join(projectPath, 'README.md'), TEMPLATE_NAME, name);
|
|
17
|
+
await replaceEnvValue(path.join(projectPath, 'backend/.env'), 'PORT', backendPort);
|
|
18
|
+
// 替换前端生产环境 API 地址
|
|
19
|
+
await replaceEnvValue(path.join(projectPath, 'frontend/.env.production'), 'VITE_API_BASE_URL', `https://node.minij.com/DataVerse/${name}`);
|
|
20
|
+
}
|
|
21
|
+
async function replaceInFile(filePath, pattern, replacement) {
|
|
22
|
+
if (!(await fs.pathExists(filePath)))
|
|
23
|
+
return;
|
|
24
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
25
|
+
await fs.writeFile(filePath, content.replace(pattern, replacement), 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
async function replaceAllInFile(filePath, search, replacement) {
|
|
28
|
+
if (!(await fs.pathExists(filePath)))
|
|
29
|
+
return;
|
|
30
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
31
|
+
if (!content.includes(search))
|
|
32
|
+
return;
|
|
33
|
+
await fs.writeFile(filePath, content.split(search).join(replacement), 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
async function replaceEnvValue(filePath, key, value) {
|
|
36
|
+
if (!(await fs.pathExists(filePath)))
|
|
37
|
+
return;
|
|
38
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
39
|
+
const pattern = new RegExp(`^${key}=.*$`, 'm');
|
|
40
|
+
const modified = pattern.test(content)
|
|
41
|
+
? content.replace(pattern, `${key}=${value}`)
|
|
42
|
+
: `${content.trimEnd()}\n${key}=${value}\n`;
|
|
43
|
+
await fs.writeFile(filePath, modified, 'utf-8');
|
|
44
|
+
}
|
|
45
|
+
async function replaceJsonField(filePath, transform) {
|
|
46
|
+
if (!(await fs.pathExists(filePath)))
|
|
47
|
+
return;
|
|
48
|
+
const obj = await fs.readJson(filePath);
|
|
49
|
+
await fs.writeJson(filePath, transform(obj), { spaces: 2 });
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minij/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "minij 一键建站方案 - 本地触发端",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"minij": "./bin/minij.js"
|
|
9
9
|
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"bin"
|
|
13
|
+
],
|
|
10
14
|
"dependencies": {
|
|
11
15
|
"axios": "^1.6.0",
|
|
12
16
|
"chalk": "^5.3.0",
|
package/CLAUDE.md
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# 小吉 AI 执行规范 (High Performance)
|
|
2
|
-
|
|
3
|
-
## 核心原则
|
|
4
|
-
- **优先 CLI**:所有操作(建站、建仓、初始化、部署)必须通过 `minij` CLI 完成。
|
|
5
|
-
- **环境隔离**:仅限 608 内网。API: `http://192.168.50.41:8888`。
|
|
6
|
-
- **禁止手动**:禁止使用 git clone/init/push 替代 CLI,除非用户明确要求。
|
|
7
|
-
|
|
8
|
-
## 指令映射表
|
|
9
|
-
| 需求场景 | 动作 / 预设 | 命令 |
|
|
10
|
-
| :--- | :--- | :--- |
|
|
11
|
-
| **新建项目** | 需确认 <英文名> | `minij create <name> [--port <port>]` |
|
|
12
|
-
| **部署/发布** | 需确认 <项目名> | `minij server-deploy <name>` |
|
|
13
|
-
| **初始化环境** | 直接执行 | `minij init` |
|
|
14
|
-
|
|
15
|
-
## 业务逻辑约束
|
|
16
|
-
1. **参数校验**:
|
|
17
|
-
- 英文名:仅限字母、数字、-、_。
|
|
18
|
-
- 端口:3000-3100 (默认 3001)。
|
|
19
|
-
2. **缺省策略**:
|
|
20
|
-
- 无英文名:主动询问。
|
|
21
|
-
- 无端口:直接执行 `minij create <name>` 由 CLI 接管提示。
|
|
22
|
-
- 中文名:默认使用英文名,无需确认。
|
|
23
|
-
3. **基础设施**:GitLab (`git.minij.com`),用户 (`deployer-bot`)。
|
package/deploy.sh
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
set -e
|
|
4
|
-
|
|
5
|
-
cd "$(dirname "$0")"
|
|
6
|
-
|
|
7
|
-
echo "📦 构建项目..."
|
|
8
|
-
pnpm build
|
|
9
|
-
|
|
10
|
-
echo "🔍 检查 Git 工作区..."
|
|
11
|
-
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
12
|
-
if [ -n "$(git status --porcelain)" ]; then
|
|
13
|
-
echo "❌ 检测到未提交的 Git 变更,已取消发布。"
|
|
14
|
-
echo ""
|
|
15
|
-
echo "当前变更:"
|
|
16
|
-
git status --short
|
|
17
|
-
echo ""
|
|
18
|
-
echo "请先处理后再重试,你可以选择:"
|
|
19
|
-
echo "1. 提交改动后重新执行 bash deploy.sh"
|
|
20
|
-
echo "2. 临时 stash 改动后重新执行 bash deploy.sh"
|
|
21
|
-
echo "3. 如确需跳过检查,手动执行:pnpm publish --no-git-checks --registry=https://registry.npmjs.org/"
|
|
22
|
-
exit 1
|
|
23
|
-
fi
|
|
24
|
-
echo "✅ Git 工作区干净"
|
|
25
|
-
else
|
|
26
|
-
echo "⚠️ 当前目录不在 Git 仓库中,跳过 Git 状态检查"
|
|
27
|
-
fi
|
|
28
|
-
|
|
29
|
-
echo "🚀 发布到 npm..."
|
|
30
|
-
pnpm publish --tag beta --access public --registry=https://registry.npmjs.org/
|
|
31
|
-
|
|
32
|
-
echo "✅ 发布完成!"
|
|
33
|
-
echo "查看: https://www.npmjs.com/package/@minij/cli"
|
package/local_deploy.sh
DELETED
package/src/commands/create.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import shelljs from 'shelljs';
|
|
3
|
-
const shell = shelljs;
|
|
4
|
-
import fs from 'fs-extra';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import readline from 'node:readline/promises';
|
|
7
|
-
import { stdin as input, stdout as output } from 'node:process';
|
|
8
|
-
import { TEMPLATE_URL } from '../constants.js';
|
|
9
|
-
import { withLoader } from '../utils/loader.js';
|
|
10
|
-
import { applyTemplateReplacements } from '../utils/template-replacer.js';
|
|
11
|
-
import { createGitlabRepo, initLocalGitRepo, setupCiVariables } from '../services/gitlab.js';
|
|
12
|
-
|
|
13
|
-
export interface CreateCommandOptions {
|
|
14
|
-
port?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function createCommand(name: string, options: CreateCommandOptions = {}): Promise<void> {
|
|
18
|
-
validateProjectName(name);
|
|
19
|
-
|
|
20
|
-
const projectPath = path.resolve(name);
|
|
21
|
-
if (await fs.pathExists(projectPath)) {
|
|
22
|
-
throw new Error(`目录已存在: ${projectPath}`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const backendPort = await resolveBackendPort(options.port);
|
|
26
|
-
|
|
27
|
-
await withLoader(`正在克隆模板到 ${name}...`, async () => {
|
|
28
|
-
const result = shell.exec(`git clone ${TEMPLATE_URL} ${name}`, { silent: true });
|
|
29
|
-
if (result.code !== 0) {
|
|
30
|
-
throw new Error(`克隆失败: ${result.stderr}`);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
await withLoader('正在清理 .git 目录...', async () => {
|
|
35
|
-
await fs.remove(path.join(projectPath, '.git'));
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
await withLoader('正在替换模板内容...', async () => {
|
|
39
|
-
await applyTemplateReplacements(projectPath, name, backendPort);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const { sshUrl, projectId } = await createGitlabRepo(name);
|
|
43
|
-
await setupCiVariables(projectId, name, backendPort);
|
|
44
|
-
await initLocalGitRepo(projectPath, sshUrl);
|
|
45
|
-
|
|
46
|
-
const url = `https://dataverse.minij.com/DataVerse/${name}/`;
|
|
47
|
-
|
|
48
|
-
console.log(chalk.green(`\n✓ 项目 ${name} 创建完成!`));
|
|
49
|
-
console.log(chalk.white('\n项目信息:'));
|
|
50
|
-
console.log(chalk.gray(`目录: ${projectPath}`));
|
|
51
|
-
console.log(chalk.gray(`远程仓库: ${sshUrl}`));
|
|
52
|
-
console.log(chalk.gray(`Node 端口: ${backendPort}`));
|
|
53
|
-
console.log(chalk.yellow('\n提示:GitLab CI/CD 正在构建,约 20S后可访问'));
|
|
54
|
-
console.log(chalk.white('\n访问地址:'));
|
|
55
|
-
console.log(chalk.bold.blue(`\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\`));
|
|
56
|
-
console.log(chalk.gray('\n下一步:'));
|
|
57
|
-
console.log(chalk.gray(`cd ${name}`));
|
|
58
|
-
console.log(chalk.gray(`minij server-deploy ${name}`));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function validateProjectName(name: string): void {
|
|
62
|
-
const validNamePattern = /^[a-zA-Z0-9_-]+$/;
|
|
63
|
-
if (!validNamePattern.test(name)) {
|
|
64
|
-
throw new Error('项目名仅支持字母、数字、中划线和下划线');
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function resolveBackendPort(inputPort?: string): Promise<string> {
|
|
69
|
-
if (inputPort) {
|
|
70
|
-
validateBackendPort(inputPort);
|
|
71
|
-
return inputPort;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const rl = readline.createInterface({ input, output });
|
|
75
|
-
try {
|
|
76
|
-
const answer = await rl.question('请输入 Node 服务端口号(3000-3100,默认 3001): ');
|
|
77
|
-
const backendPort = answer.trim() || '3001';
|
|
78
|
-
validateBackendPort(backendPort);
|
|
79
|
-
return backendPort;
|
|
80
|
-
} finally {
|
|
81
|
-
rl.close();
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function validateBackendPort(port: string): void {
|
|
86
|
-
if (!/^\d+$/.test(port)) {
|
|
87
|
-
throw new Error('Node 服务端口号必须是数字,范围 3000-3100,默认 3001');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const numericPort = Number(port);
|
|
91
|
-
if (numericPort < 3000 || numericPort > 3100) {
|
|
92
|
-
throw new Error('Node 服务端口号必须在 3000 到 3100 之间,默认 3001');
|
|
93
|
-
}
|
|
94
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import axios from 'axios';
|
|
3
|
-
import { withLoader } from '../utils/loader.js';
|
|
4
|
-
import { getOrchestratorUrl } from '../utils/config.js';
|
|
5
|
-
|
|
6
|
-
interface SetupRouterResponse {
|
|
7
|
-
success: boolean;
|
|
8
|
-
url: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export async function serverDeployCommand(name: string): Promise<void> {
|
|
12
|
-
const orchestratorUrl = await getOrchestratorUrl();
|
|
13
|
-
|
|
14
|
-
// 调用远程接口设置路由
|
|
15
|
-
const resultUrl = await withLoader('正在部署服务...', async () => {
|
|
16
|
-
try {
|
|
17
|
-
const response = await axios.post<SetupRouterResponse>(
|
|
18
|
-
`${orchestratorUrl}/api/setup-router`,
|
|
19
|
-
{
|
|
20
|
-
projectName: name
|
|
21
|
-
}
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
if (!response.data.success) {
|
|
25
|
-
throw new Error('部署失败');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return response.data.url;
|
|
29
|
-
} catch (error) {
|
|
30
|
-
if (axios.isAxiosError(error)) {
|
|
31
|
-
throw new Error(`API 请求失败: ${error.message}`);
|
|
32
|
-
}
|
|
33
|
-
throw error;
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
console.log(chalk.green(`\n✓ 服务部署完成!`));
|
|
38
|
-
console.log(chalk.cyan(`\n访问地址:`));
|
|
39
|
-
console.log(chalk.bold.underline(resultUrl));
|
|
40
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { initCommand } from './commands/init.js';
|
|
6
|
-
import { serverDeployCommand } from './commands/server-deploy.js';
|
|
7
|
-
import { createCommand } from './commands/create.js';
|
|
8
|
-
|
|
9
|
-
const program = new Command();
|
|
10
|
-
|
|
11
|
-
program
|
|
12
|
-
.name('minij')
|
|
13
|
-
.description('minij一键建站方案 - 本地触发端')
|
|
14
|
-
.version('1.0.0');
|
|
15
|
-
|
|
16
|
-
program
|
|
17
|
-
.command('create <name>')
|
|
18
|
-
.description('一键创建项目:克隆模板、替换项目名、创建 GitLab 仓库、初始化 git 并推送')
|
|
19
|
-
.option('-p, --port <port>', 'Node 服务端口号(3000-3100,默认 3001)')
|
|
20
|
-
.action(async (name: string, options: { port?: string }) => {
|
|
21
|
-
try {
|
|
22
|
-
await createCommand(name, options);
|
|
23
|
-
} catch (error) {
|
|
24
|
-
console.error(chalk.red(`\n错误: ${(error as Error).message}`));
|
|
25
|
-
process.exit(1);
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
program
|
|
30
|
-
.command('init')
|
|
31
|
-
.description('初始化项目环境,生成 CLAUDE.md 并检查 SSH 配置')
|
|
32
|
-
.action(async () => {
|
|
33
|
-
try {
|
|
34
|
-
await initCommand();
|
|
35
|
-
} catch (error) {
|
|
36
|
-
console.error(chalk.red(`\n错误: ${(error as Error).message}`));
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
program
|
|
42
|
-
.command('server-deploy <name>')
|
|
43
|
-
.description('调用远程接口部署服务')
|
|
44
|
-
.action(async (name: string) => {
|
|
45
|
-
try {
|
|
46
|
-
await serverDeployCommand(name);
|
|
47
|
-
} catch (error) {
|
|
48
|
-
console.error(chalk.red(`\n错误: ${(error as Error).message}`));
|
|
49
|
-
process.exit(1);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
program.parse();
|
package/src/services/gitlab.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import shelljs from 'shelljs';
|
|
2
|
-
const shell = shelljs;
|
|
3
|
-
import axios from 'axios';
|
|
4
|
-
import { withLoader } from '../utils/loader.js';
|
|
5
|
-
import { getOrchestratorUrl } from '../utils/config.js';
|
|
6
|
-
|
|
7
|
-
interface CreateRepoResponse {
|
|
8
|
-
success?: boolean;
|
|
9
|
-
sshUrl: string;
|
|
10
|
-
projectId: number;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface CreateGitlabRepoResult {
|
|
14
|
-
sshUrl: string;
|
|
15
|
-
projectId: number;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function createGitlabRepo(name: string): Promise<CreateGitlabRepoResult> {
|
|
19
|
-
const orchestratorUrl = await getOrchestratorUrl();
|
|
20
|
-
|
|
21
|
-
return withLoader('正在创建 GitLab 仓库...', async () => {
|
|
22
|
-
try {
|
|
23
|
-
const response = await axios.post<CreateRepoResponse>(
|
|
24
|
-
`${orchestratorUrl}/api/create-repo`,
|
|
25
|
-
{ projectName: name, userName: 'deployer-bot' }
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
if (!response.data.sshUrl || !response.data.projectId) {
|
|
29
|
-
throw new Error('接口未返回完整的仓库信息');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
sshUrl: response.data.sshUrl,
|
|
34
|
-
projectId: response.data.projectId,
|
|
35
|
-
};
|
|
36
|
-
} catch (error) {
|
|
37
|
-
if (axios.isAxiosError(error)) {
|
|
38
|
-
throw new Error(`API 请求失败: ${error.message}`);
|
|
39
|
-
}
|
|
40
|
-
throw error;
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function setupCiVariables(
|
|
46
|
-
projectId: number,
|
|
47
|
-
name: string,
|
|
48
|
-
backendPort: string
|
|
49
|
-
): Promise<void> {
|
|
50
|
-
const orchestratorUrl = await getOrchestratorUrl();
|
|
51
|
-
|
|
52
|
-
await withLoader('正在绑定 CI/CD 变量...', async () => {
|
|
53
|
-
try {
|
|
54
|
-
await axios.post(`${orchestratorUrl}/api/setup-ci-variables`, {
|
|
55
|
-
projectId,
|
|
56
|
-
name,
|
|
57
|
-
backendPort,
|
|
58
|
-
});
|
|
59
|
-
} catch (error) {
|
|
60
|
-
if (axios.isAxiosError(error)) {
|
|
61
|
-
throw new Error(`API 请求失败: ${error.message}`);
|
|
62
|
-
}
|
|
63
|
-
throw error;
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function initLocalGitRepo(projectPath: string, sshUrl: string): Promise<void> {
|
|
69
|
-
await withLoader('正在初始化 Git 仓库...', async () => {
|
|
70
|
-
const result = shell.exec('git init', { silent: true, cwd: projectPath });
|
|
71
|
-
if (result.code !== 0) throw new Error(`git init 失败: ${result.stderr}`);
|
|
72
|
-
|
|
73
|
-
const branchResult = shell.exec('git branch -M main', { silent: true, cwd: projectPath });
|
|
74
|
-
if (branchResult.code !== 0) throw new Error(`git branch 失败: ${branchResult.stderr}`);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
await withLoader('正在添加远程仓库...', async () => {
|
|
78
|
-
const result = shell.exec(`git remote add origin ${sshUrl}`, { silent: true, cwd: projectPath });
|
|
79
|
-
if (result.code !== 0) throw new Error(`添加远程仓库失败: ${result.stderr}`);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
await withLoader('正在提交初始代码...', async () => {
|
|
83
|
-
const addResult = shell.exec('git add .', { silent: true, cwd: projectPath });
|
|
84
|
-
if (addResult.code !== 0) throw new Error(`git add 失败: ${addResult.stderr}`);
|
|
85
|
-
|
|
86
|
-
const commitResult = shell.exec('git commit -m "chore: init project"', { silent: true, cwd: projectPath });
|
|
87
|
-
if (commitResult.code !== 0) throw new Error(`git commit 失败: ${commitResult.stderr}`);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
await withLoader('正在推送初始代码...', async () => {
|
|
91
|
-
const result = shell.exec('git push -u origin main', { silent: true, cwd: projectPath });
|
|
92
|
-
if (result.code !== 0) throw new Error(`git push 失败: ${result.stderr}`);
|
|
93
|
-
});
|
|
94
|
-
}
|
package/src/utils/config.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { CONFIG_PATH, DEFAULT_ORCHESTRATOR_URL } from '../constants.js';
|
|
4
|
-
|
|
5
|
-
export interface MinijConfig {
|
|
6
|
-
ORCHESTRATOR_URL: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export async function loadConfig(): Promise<MinijConfig> {
|
|
10
|
-
try {
|
|
11
|
-
const config = await fs.readJson(CONFIG_PATH);
|
|
12
|
-
return config as MinijConfig;
|
|
13
|
-
} catch {
|
|
14
|
-
// 配置文件不存在,返回默认值
|
|
15
|
-
return { ORCHESTRATOR_URL: DEFAULT_ORCHESTRATOR_URL };
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function saveConfig(config: MinijConfig): Promise<void> {
|
|
20
|
-
await fs.ensureFile(CONFIG_PATH);
|
|
21
|
-
await fs.writeJson(CONFIG_PATH, config, { spaces: 2 });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function getOrchestratorUrl(): Promise<string> {
|
|
25
|
-
const config = await loadConfig();
|
|
26
|
-
console.log(chalk.gray(`提示:minij CLI 需在小吉 608 内网中使用,服务地址:${config.ORCHESTRATOR_URL}`));
|
|
27
|
-
return config.ORCHESTRATOR_URL;
|
|
28
|
-
}
|
package/src/utils/loader.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import ora, { Ora } from 'ora';
|
|
2
|
-
|
|
3
|
-
export function createLoader(text: string): Ora {
|
|
4
|
-
return ora({
|
|
5
|
-
text,
|
|
6
|
-
color: 'cyan'
|
|
7
|
-
});
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function withLoader<T>(
|
|
11
|
-
text: string,
|
|
12
|
-
fn: () => Promise<T>
|
|
13
|
-
): Promise<T> {
|
|
14
|
-
const spinner = createLoader(text);
|
|
15
|
-
spinner.start();
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const result = await fn();
|
|
19
|
-
spinner.succeed();
|
|
20
|
-
return result;
|
|
21
|
-
} catch (error) {
|
|
22
|
-
spinner.fail();
|
|
23
|
-
throw error;
|
|
24
|
-
}
|
|
25
|
-
}
|
package/src/utils/ssh.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import shelljs from 'shelljs';
|
|
2
|
-
const { exec } = shelljs;
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { GITLAB_HOST } from '../constants.js';
|
|
5
|
-
|
|
6
|
-
export interface SshCheckResult {
|
|
7
|
-
success: boolean;
|
|
8
|
-
message: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export async function checkSshConnectivity(): Promise<SshCheckResult> {
|
|
12
|
-
return new Promise((resolve) => {
|
|
13
|
-
const command = `ssh -T git@${GITLAB_HOST} 2>&1`;
|
|
14
|
-
|
|
15
|
-
exec(command, { silent: true }, (code, stdout, stderr) => {
|
|
16
|
-
const output = stdout + stderr;
|
|
17
|
-
const lowerOutput = output.toLowerCase();
|
|
18
|
-
|
|
19
|
-
// 成功标志:GitLab 系列或 Gitee 的欢迎语,或包含用户名提示
|
|
20
|
-
const isSuccess =
|
|
21
|
-
lowerOutput.includes('welcome to gitlab') ||
|
|
22
|
-
lowerOutput.includes('welcome to gitee') ||
|
|
23
|
-
lowerOutput.includes('you\'ve successfully authenticated') ||
|
|
24
|
-
// 私有部署的 GitLab 通常也会包含用户名
|
|
25
|
-
lowerOutput.includes('hi ');
|
|
26
|
-
|
|
27
|
-
// 明确失败标志
|
|
28
|
-
const isFail =
|
|
29
|
-
lowerOutput.includes('permission denied') ||
|
|
30
|
-
lowerOutput.includes('publickey') ||
|
|
31
|
-
lowerOutput.includes('no such host') ||
|
|
32
|
-
lowerOutput.includes('connection refused');
|
|
33
|
-
|
|
34
|
-
if (isSuccess || (!isFail && output.length > 0)) {
|
|
35
|
-
resolve({
|
|
36
|
-
success: true,
|
|
37
|
-
message: 'SSH 连接成功'
|
|
38
|
-
});
|
|
39
|
-
} else {
|
|
40
|
-
resolve({
|
|
41
|
-
success: false,
|
|
42
|
-
message: 'SSH 连接失败,请先配置公钥'
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
}
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import fs from 'fs-extra';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
const TEMPLATE_NAME = 'minij-bi-admin-template';
|
|
5
|
-
|
|
6
|
-
export async function applyTemplateReplacements(
|
|
7
|
-
projectPath: string,
|
|
8
|
-
name: string,
|
|
9
|
-
backendPort: string
|
|
10
|
-
): Promise<void> {
|
|
11
|
-
await replaceJsonField(path.join(projectPath, 'package.json'), (pkg) => {
|
|
12
|
-
pkg.name = name;
|
|
13
|
-
return pkg;
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
await replaceInFile(
|
|
17
|
-
path.join(projectPath, 'frontend/index.html'),
|
|
18
|
-
/<title>[^<]*<\/title>/,
|
|
19
|
-
`<title>${name}</title>`
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
await replaceAllInFile(path.join(projectPath, 'frontend/vite.config.ts'), TEMPLATE_NAME, name);
|
|
23
|
-
|
|
24
|
-
await replaceInFile(
|
|
25
|
-
path.join(projectPath, 'frontend/src/router/index.ts'),
|
|
26
|
-
/createWebHistory\([^)]*\)/g,
|
|
27
|
-
`createWebHistory('/DataVerse/${name}/')`
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
await replaceAllInFile(path.join(projectPath, 'frontend/src/router/guards.ts'), TEMPLATE_NAME, name);
|
|
31
|
-
await replaceAllInFile(path.join(projectPath, 'frontend/src/layouts/AdminLayout.vue'), TEMPLATE_NAME, name);
|
|
32
|
-
await replaceAllInFile(path.join(projectPath, 'frontend/src/views/home/HomeView.vue'), TEMPLATE_NAME, name);
|
|
33
|
-
await replaceAllInFile(path.join(projectPath, '.gitlab-ci.yml'), TEMPLATE_NAME, name);
|
|
34
|
-
await replaceAllInFile(path.join(projectPath, 'README.md'), TEMPLATE_NAME, name);
|
|
35
|
-
|
|
36
|
-
await replaceEnvValue(path.join(projectPath, 'backend/.env'), 'PORT', backendPort);
|
|
37
|
-
|
|
38
|
-
// 替换前端生产环境 API 地址
|
|
39
|
-
await replaceEnvValue(
|
|
40
|
-
path.join(projectPath, 'frontend/.env.production'),
|
|
41
|
-
'VITE_API_BASE_URL',
|
|
42
|
-
`https://node.minij.com/DataVerse/${name}`
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
async function replaceInFile(
|
|
47
|
-
filePath: string,
|
|
48
|
-
pattern: RegExp | string,
|
|
49
|
-
replacement: string
|
|
50
|
-
): Promise<void> {
|
|
51
|
-
if (!(await fs.pathExists(filePath))) return;
|
|
52
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
53
|
-
await fs.writeFile(filePath, content.replace(pattern, replacement), 'utf-8');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function replaceAllInFile(
|
|
57
|
-
filePath: string,
|
|
58
|
-
search: string,
|
|
59
|
-
replacement: string
|
|
60
|
-
): Promise<void> {
|
|
61
|
-
if (!(await fs.pathExists(filePath))) return;
|
|
62
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
63
|
-
if (!content.includes(search)) return;
|
|
64
|
-
await fs.writeFile(filePath, content.split(search).join(replacement), 'utf-8');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function replaceEnvValue(filePath: string, key: string, value: string): Promise<void> {
|
|
68
|
-
if (!(await fs.pathExists(filePath))) return;
|
|
69
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
70
|
-
const pattern = new RegExp(`^${key}=.*$`, 'm');
|
|
71
|
-
const modified = pattern.test(content)
|
|
72
|
-
? content.replace(pattern, `${key}=${value}`)
|
|
73
|
-
: `${content.trimEnd()}\n${key}=${value}\n`;
|
|
74
|
-
await fs.writeFile(filePath, modified, 'utf-8');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function replaceJsonField(
|
|
78
|
-
filePath: string,
|
|
79
|
-
transform: (obj: Record<string, unknown>) => Record<string, unknown>
|
|
80
|
-
): Promise<void> {
|
|
81
|
-
if (!(await fs.pathExists(filePath))) return;
|
|
82
|
-
const obj = await fs.readJson(filePath);
|
|
83
|
-
await fs.writeJson(filePath, transform(obj), { spaces: 2 });
|
|
84
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"outDir": "./dist",
|
|
7
|
-
"rootDir": "./src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"skipLibCheck": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true,
|
|
12
|
-
"declaration": true
|
|
13
|
-
},
|
|
14
|
-
"include": ["src/**/*"],
|
|
15
|
-
"exclude": ["node_modules", "dist"]
|
|
16
|
-
}
|