@minij/cli 0.0.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/CLAUDE.md ADDED
@@ -0,0 +1,23 @@
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/README.md ADDED
@@ -0,0 +1,203 @@
1
+ # @minij/cli
2
+
3
+ minij 一键建站方案的本地触发端,负责本地项目初始化、模板替换,并协调 `minij-orchestrator-server` 完成 GitLab 仓库创建和 CI/CD 变量注入。
4
+
5
+ > **注意:需在minij 608 内网中使用。**
6
+ > orchestrator 服务固定部署在 `http://192.168.50.41:8888`,不在内网无法连接。
7
+
8
+ ---
9
+
10
+ ## 安装
11
+
12
+ ```bash
13
+ npm install -g @minij/cli
14
+ # 或
15
+ pnpm add -g @minij/cli
16
+ ```
17
+
18
+ ## 前置要求
19
+
20
+ - Node.js >= 18
21
+ - pnpm
22
+ - Git
23
+ - SSH 公钥已配置到 `git.minij.com`
24
+ - 处于minij 608 内网环境
25
+
26
+ ---
27
+
28
+ ## 命令
29
+
30
+ ### minij create \<name\>
31
+
32
+ 一键创建项目:克隆模板 → 替换项目名 → 创建 GitLab 仓库 → 注入 CI/CD 变量 → 初始化 git 并推送。
33
+
34
+ ```bash
35
+ minij create my-project
36
+ # 或指定 Node 端口(跳过交互提示)
37
+ minij create my-project --port 3002
38
+ ```
39
+
40
+ **选项**
41
+
42
+ | 选项 | 说明 |
43
+ |------|------|
44
+ | `-p, --port <port>` | Node 服务端口,范围 3000-3100,默认 3001 |
45
+
46
+ **执行流程**
47
+
48
+ 1. 从 `git@git.minij.com:dataverse/minij-bi-admin-template.git` 克隆模板
49
+ 2. 删除 `.git` 目录
50
+ 3. 全局替换模板中的 `minij-bi-admin-template` 为项目名
51
+ 4. 写入 `backend/.env` 的 `PORT` 值
52
+ 5. 调用 orchestrator `POST /api/create-repo` 创建 GitLab 私有仓库
53
+ 6. 调用 orchestrator `POST /api/setup-ci-variables` 注入 CI/CD 变量
54
+ 7. `git init → commit → push -u origin main`
55
+
56
+ **完成后输出**
57
+
58
+ ```
59
+ ✓ 项目 my-project 创建完成!
60
+
61
+ 目录: /path/to/my-project
62
+ 远程仓库: git@git.minij.com:dataverse/my-project.git
63
+ Node 端口: 3002
64
+
65
+ 访问地址(CI 部署完成后):
66
+ https://dataverse.minij.com/DataVerse/my-project/
67
+ ```
68
+
69
+ ---
70
+
71
+ ### minij init
72
+
73
+ 在当前目录生成 `CLAUDE.md`(AI 助手执行规则),并检查 SSH 到 GitLab 的连通���。
74
+
75
+ ```bash
76
+ minij init
77
+ ```
78
+
79
+ SSH 检查失败时会提示配置步骤:
80
+
81
+ ```bash
82
+ ssh-keygen -t ed25519 -C "your_email@example.com"
83
+ cat ~/.ssh/id_ed25519.pub
84
+ # 将公钥添加到 git.minij.com → Settings → SSH Keys
85
+ ```
86
+
87
+ ---
88
+
89
+ ### minij server-deploy \<name\>
90
+
91
+ 调用 orchestrator `POST /api/setup-router` 为项目分配端口并配置 Nginx 路由。
92
+
93
+ ```bash
94
+ minij server-deploy my-project
95
+ ```
96
+
97
+ > 当前 DataVerse 体系前端通过 CI/CD 直接部署,通常不需要单独调用此命令。
98
+
99
+ ---
100
+
101
+ ## 完整工作流
102
+
103
+ ```bash
104
+ # 1. 检查环境(首次使用)
105
+ minij init
106
+
107
+ # 2. 一键创建项目
108
+ minij create my-project --port 3002
109
+
110
+ # 3. 进入项目目录,开始开发
111
+ cd my-project
112
+
113
+ # 4. 推送代码触发 CI/CD 自动部署
114
+ git add .
115
+ git commit -m "feat: ..."
116
+ git push
117
+
118
+ # 5. 部署完成后访问
119
+ # https://dataverse.minij.com/DataVerse/my-project/
120
+ ```
121
+
122
+ ---
123
+
124
+ ## 本地开发测试
125
+
126
+ ### 1. 安装依赖并构建
127
+
128
+ ```bash
129
+ pnpm install
130
+ pnpm build
131
+ ```
132
+
133
+ ### 2. 本地挂载 CLI
134
+
135
+ 在 `minij-cli` 目录执行:
136
+
137
+ ```bash
138
+ pnpm link --global
139
+ ```
140
+
141
+ 挂载完成后,可直接在终端使用:
142
+
143
+ ```bash
144
+ minij --version
145
+ minij init
146
+ ```
147
+
148
+ ### 3. 测试创建项目全流程
149
+
150
+ > 需确保当前机器已连接minij 608 内网,且 `minij-orchestrator-server` 已部署在 `http://192.168.50.41:8888`。
151
+
152
+ ```bash
153
+ minij create test-demo --port 3002
154
+ ```
155
+
156
+ 该命令会验证以下完整链路:
157
+
158
+ 1. 克隆模板仓库
159
+ 2. 替换项目名
160
+ 3. 调用 orchestrator 创建 GitLab 仓库
161
+ 4. 注入 GitLab CI/CD 变量
162
+ 5. 初始化 git 并推送到远程仓库
163
+
164
+ ### 4. 修改代码后重新测试
165
+
166
+ 每次修改 CLI 源码后,重新执行:
167
+
168
+ ```bash
169
+ pnpm build
170
+ ```
171
+
172
+ 然后再次运行:
173
+
174
+ ```bash
175
+ minij create another-demo --port 3003
176
+ ```
177
+
178
+ ### 5. 如需取消全局挂载
179
+
180
+ ```bash
181
+ pnpm unlink --global @minij/cli
182
+ ```
183
+
184
+ ---
185
+
186
+ ## 配置
187
+
188
+ CLI 默认连接 `http://192.168.50.41:8888`,无需手动配置。
189
+
190
+ 如需覆盖,可编辑 `~/.minij/config.json`:
191
+
192
+ ```json
193
+ {
194
+ "ORCHESTRATOR_URL": "http://192.168.50.41:8888"
195
+ }
196
+ ```
197
+
198
+ ---
199
+
200
+ ## 项目名规范
201
+
202
+ - 只能包含字母、数字、中划线、下划线
203
+ - 示例:`my-project`、`data_dashboard`、`bi2025`
package/bin/minij.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import '../dist/index.js';
package/deploy.sh ADDED
@@ -0,0 +1,33 @@
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"
@@ -0,0 +1,14 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ cd "$(dirname "$0")"
6
+
7
+ echo "📦 构建项目..."
8
+ pnpm build
9
+
10
+ echo "🚀 部署到本地..."
11
+ pnpm link --global
12
+
13
+ echo "✅ 部署完成!"
14
+ echo "minij -h"
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@minij/cli",
3
+ "version": "0.0.1",
4
+ "description": "minij 一键建站方案 - 本地触发端",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "bin": {
8
+ "minij": "./bin/minij.js"
9
+ },
10
+ "dependencies": {
11
+ "axios": "^1.6.0",
12
+ "chalk": "^5.3.0",
13
+ "commander": "^11.1.0",
14
+ "fs-extra": "^11.2.0",
15
+ "ora": "^8.0.1",
16
+ "shelljs": "^0.8.5"
17
+ },
18
+ "devDependencies": {
19
+ "@types/fs-extra": "^11.0.4",
20
+ "@types/node": "^20.10.0",
21
+ "@types/shelljs": "^0.8.15",
22
+ "typescript": "^5.3.0"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "prepublish": "npm run build"
27
+ }
28
+ }
@@ -0,0 +1,94 @@
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
+ }
@@ -0,0 +1,118 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs-extra';
3
+ import { checkSshConnectivity } from '../utils/ssh.js';
4
+
5
+ const CLAUDE_MD_CONTENT = `# 小吉 AI 执行规则
6
+
7
+ 你是小吉建站系统的 AI 助手。
8
+
9
+ ## 总原则
10
+
11
+ - 本项目中的建站、创建仓库、初始化代码、推送代码、部署服务,**优先使用 \`minij\` CLI**。
12
+ - **不要手动执行** clone 模板、修改项目名、创建 GitLab 仓库、初始化 git、推送代码,除非用户明确要求你不要使用 CLI。
13
+ - 当用户表达的目标已经对应到现有 CLI 能力时,直接调用 CLI,不要自己拆步骤重做。
14
+
15
+ ## 环境要求
16
+
17
+ > **重要**:minij CLI 需在 **小吉 608 内网** 中使用。
18
+ > orchestrator 服务固定部署在 \`http://192.168.50.41:8888\`,不在内网无法连接。
19
+
20
+ ## 创建项目
21
+
22
+ 当用户提出以下任一需求时:
23
+ - 新建项目
24
+ - 创建项目
25
+ - 帮我建一个项目
26
+ - 帮我初始化一个新项目
27
+ - 帮我从模板创建项目
28
+
29
+ 你的处理规则:
30
+
31
+ 1. 如果用户**还没有提供项目英文名**,先询问项目英文名。
32
+ 2. 如果用户**已经提供项目英文名和 Node 端口**,直接执行:
33
+
34
+ \`\`\`bash
35
+ minij create <项目英文名> --port <Node端口>
36
+ \`\`\`
37
+
38
+ 3. 如果用户**只提供了项目英文名**,直接执行:
39
+
40
+ \`\`\`bash
41
+ minij create <项目英文名>
42
+ \`\`\`
43
+
44
+ 此时 CLI 会继续询问 Node 服务端口(3000-3100,默认 3001)。
45
+
46
+ 4. 执行 \`minij create\` 时,不要改用手动 git 命令替代。
47
+ 5. 项目显示名默认直接使用英文项目名,不再单独询问中文项目名。
48
+
49
+ ## 部署项目
50
+
51
+ 当用户提出以下任一需求时:
52
+ - 部署项目
53
+ - 发布项目
54
+ - 帮我部署
55
+ - 帮我发布服务
56
+
57
+ 直接执行:
58
+
59
+ \`\`\`bash
60
+ minij server-deploy <项目名>
61
+ \`\`\`
62
+
63
+ 如果用户没有提供项目名,先询问项目名。
64
+
65
+ ## 初始化环境
66
+
67
+ 当用户提出以下任一需求时:
68
+ - 初始化环境
69
+ - 检查环境
70
+ - 初始化小吉环境
71
+
72
+ 直接执行:
73
+
74
+ \`\`\`bash
75
+ minij init
76
+ \`\`\`
77
+
78
+ ## 禁止事项
79
+
80
+ 在以下场景中,禁止跳过 CLI 直接手动操作:
81
+ - 创建项目
82
+ - 从模板生成项目
83
+ - 创建 GitLab 仓库
84
+ - 初始化项目 git
85
+ - 首次推送代码
86
+ - 部署服务
87
+
88
+ 除非用户明确说:
89
+ - 不要用 minij
90
+ - 我想手动执行
91
+ - 只给我命令,不要帮我执行
92
+
93
+ ## 额外约束
94
+
95
+ - 项目英文名只能包含字母、数字、中划线、下划线
96
+ - Node 服务端口必须在 3000-3100 之间,默认 3001
97
+ - 必须在小吉 608 内网环境中执行,orchestrator 地址:\`http://192.168.50.41:8888\`
98
+ - 默认 GitLab 主机是 \`git.minij.com\`
99
+ - 默认创建仓库用户是 \`deployer-bot\`
100
+ `;
101
+
102
+ export async function initCommand(): Promise<void> {
103
+ await fs.writeFile('CLAUDE.md', CLAUDE_MD_CONTENT, 'utf-8');
104
+ console.log(chalk.green('✓ CLAUDE.md 文件已生成'));
105
+
106
+ const sshResult = await checkSshConnectivity();
107
+
108
+ if (sshResult.success) {
109
+ console.log(chalk.green(`✓ ${sshResult.message}`));
110
+ } else {
111
+ console.log(chalk.red(`✗ ${sshResult.message}`));
112
+ console.log(chalk.yellow('\n请按以下步骤配置 SSH 公钥:'));
113
+ console.log(chalk.cyan('1. 生成 SSH 密钥对:ssh-keygen -t ed25519 -C "your_email@example.com"'));
114
+ console.log(chalk.cyan('2. 查看公钥:cat ~/.ssh/id_ed25519.pub'));
115
+ console.log(chalk.cyan('3. 复制公钥到 GitLab -> Settings -> SSH Keys'));
116
+ process.exit(1);
117
+ }
118
+ }
@@ -0,0 +1,40 @@
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
+ }
@@ -0,0 +1,11 @@
1
+ // 模板地址配置
2
+ export const TEMPLATE_URL = 'git@git.minij.com:dataverse/minij-bi-admin-template.git';
3
+
4
+ // GitLab 配置
5
+ export const GITLAB_HOST = 'git.minij.com';
6
+
7
+ // 编排服务地址(固定部署在小吉 608 内网,需在该内网环境下使用)
8
+ export const DEFAULT_ORCHESTRATOR_URL = 'http://192.168.50.41:8888';
9
+
10
+ // 配置文件路径
11
+ export const CONFIG_PATH = `${process.env.HOME}/.minij/config.json`;
package/src/index.ts ADDED
@@ -0,0 +1,53 @@
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();
@@ -0,0 +1,94 @@
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
+ }
@@ -0,0 +1,28 @@
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
+ }
@@ -0,0 +1,25 @@
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
+ }
@@ -0,0 +1,47 @@
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
+ }
@@ -0,0 +1,84 @@
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 ADDED
@@ -0,0 +1,16 @@
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
+ }