@surenkid/acli-hub 1.0.1 → 1.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/.env.example +8 -6
- package/README.md +23 -23
- package/package.json +1 -1
- package/server/index.js +14 -6
- package/server/routes/files.js +2 -2
- package/server/services/deps.js +94 -0
- package/server/services/deps.test.js +46 -0
package/.env.example
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
# Main server port (backend + built frontend in production)
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
ACLI_HUB_PORT=3000
|
|
3
|
+
# Bind address — default 127.0.0.1 (local only). Use 0.0.0.0 for LAN/remote access,
|
|
4
|
+
# and ALWAYS set ACLI_HUB_AUTH_PASSWORD when binding beyond loopback.
|
|
5
|
+
#ACLI_HUB_HOST=0.0.0.0
|
|
4
6
|
|
|
5
7
|
# Internal ttyd port range (localhost only, proxied through the main port)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
ACLI_HUB_TTYD_PORT_RANGE_START=7681
|
|
9
|
+
ACLI_HUB_TTYD_PORT_RANGE_END=7780
|
|
8
10
|
|
|
9
11
|
# Maximum size per uploaded file, in MiB. 0 = unlimited.
|
|
10
|
-
|
|
12
|
+
ACLI_HUB_UPLOAD_MAX_MB=500
|
|
11
13
|
|
|
12
14
|
# Login password for the web UI. Leave UNSET to disable authentication
|
|
13
15
|
# (local development). Generate a strong one with: openssl rand -base64 24
|
|
14
16
|
# When set, all pages/API/terminal/WebSocket access requires a 7-day
|
|
15
17
|
# login session (cookie).
|
|
16
|
-
|
|
18
|
+
ACLI_HUB_AUTH_PASSWORD=
|
|
17
19
|
|
|
18
20
|
# Config locations (cwd is never read): ~/.config/acli-hub/.env for the installed CLI
|
|
19
21
|
# ($XDG_CONFIG_HOME respected); the repo root .env when running from source.
|
package/README.md
CHANGED
|
@@ -56,26 +56,26 @@ brew install ttyd tmux
|
|
|
56
56
|
## 🚀 快速开始
|
|
57
57
|
|
|
58
58
|
```bash
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
cd frontend && npm install && cd ..
|
|
62
|
-
|
|
63
|
-
# 构建前端
|
|
64
|
-
npm run build
|
|
65
|
-
|
|
66
|
-
# 启动服务
|
|
67
|
-
npm start
|
|
59
|
+
npm install -g @surenkid/acli-hub
|
|
60
|
+
acli-hub
|
|
68
61
|
```
|
|
69
62
|
|
|
70
|
-
浏览器打开 `http://localhost:3000`
|
|
63
|
+
浏览器打开 `http://localhost:3000` 即可使用。登录密码等配置放在 `~/.config/acli-hub/.env`(模板见下方配置项)。
|
|
64
|
+
|
|
65
|
+
> 也可以免安装直接体验:`npx @surenkid/acli-hub`
|
|
71
66
|
|
|
72
|
-
## 👨💻
|
|
67
|
+
## 👨💻 从源码运行
|
|
73
68
|
|
|
74
69
|
```bash
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
git clone https://cnb.cool/surenkid/acli-hub.git
|
|
71
|
+
cd acli-hub
|
|
72
|
+
npm install
|
|
73
|
+
cd frontend && npm install && cd ..
|
|
77
74
|
|
|
78
|
-
|
|
75
|
+
npm run dev # 开发模式:后端 3000 + Vite 5173(热更新)
|
|
76
|
+
# 或构建生产版运行
|
|
77
|
+
npm run build && npm start
|
|
78
|
+
```
|
|
79
79
|
|
|
80
80
|
## ⚙️ 配置项
|
|
81
81
|
|
|
@@ -88,14 +88,14 @@ npm run dev
|
|
|
88
88
|
|
|
89
89
|
配置项:
|
|
90
90
|
|
|
91
|
-
| 变量
|
|
92
|
-
|
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
91
|
+
| 变量 | 说明 | 默认值 |
|
|
92
|
+
| ------------------------------- | ------------------------------------------ | --------- |
|
|
93
|
+
| `ACLI_HUB_PORT` | 服务监听端口 | `3000` |
|
|
94
|
+
| `ACLI_HUB_HOST` | 监听地址;默认仅本机,局域网访问设 `0.0.0.0`(务必同时设密码) | `127.0.0.1` |
|
|
95
|
+
| `ACLI_HUB_AUTH_PASSWORD` | 登录密码;不设置则关闭认证(仅限本地开发) | _(不设置)_ |
|
|
96
|
+
| `ACLI_HUB_TTYD_PORT_RANGE_START` | ttyd 内部端口范围起始 | `7681` |
|
|
97
|
+
| `ACLI_HUB_TTYD_PORT_RANGE_END` | ttyd 内部端口范围结束 | `7780` |
|
|
98
|
+
| `ACLI_HUB_UPLOAD_MAX_MB` | 单文件上传上限(MiB),0 为不限 | `500` |
|
|
99
99
|
|
|
100
100
|
### 📁 文件面板说明
|
|
101
101
|
|
|
@@ -105,7 +105,7 @@ npm run dev
|
|
|
105
105
|
|
|
106
106
|
### 🔐 公网暴露
|
|
107
107
|
|
|
108
|
-
设置 `
|
|
108
|
+
设置 `ACLI_HUB_AUTH_PASSWORD` 后,建议将 ACLI Hub 部署在 HTTPS 反向代理(如 Caddy / Nginx / frp)之后,只公开 443 端口。登录会话为 7 天有效的 HttpOnly Cookie,终端 iframe 与 WebSocket 均受同一凭据保护。
|
|
109
109
|
|
|
110
110
|
## 📄 License
|
|
111
111
|
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -18,11 +18,19 @@ const authRoute = require('./routes/auth');
|
|
|
18
18
|
const sessionsRoute = require('./routes/sessions');
|
|
19
19
|
const filesRoute = require('./routes/files');
|
|
20
20
|
const setupWebSocket = require('./ws');
|
|
21
|
+
const { checkDependencies, dependencyReport } = require('./services/deps');
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// Fail fast with actionable install hints instead of a half-broken UI.
|
|
24
|
+
const missingDependencies = checkDependencies();
|
|
25
|
+
if (missingDependencies.length) {
|
|
26
|
+
console.error(dependencyReport(missingDependencies));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const PORT = parseInt(process.env.ACLI_HUB_PORT || '3000', 10);
|
|
31
|
+
const HOST = process.env.ACLI_HUB_HOST || '127.0.0.1';
|
|
32
|
+
const TTYD_PORT_START = parseInt(process.env.ACLI_HUB_TTYD_PORT_RANGE_START || '7681', 10);
|
|
33
|
+
const TTYD_PORT_END = parseInt(process.env.ACLI_HUB_TTYD_PORT_RANGE_END || '7780', 10);
|
|
26
34
|
|
|
27
35
|
const app = express();
|
|
28
36
|
const server = http.createServer(app);
|
|
@@ -34,7 +42,7 @@ app.use((req, res, next) => {
|
|
|
34
42
|
});
|
|
35
43
|
|
|
36
44
|
const sessionManager = new SessionManager(TTYD_PORT_START, TTYD_PORT_END);
|
|
37
|
-
const auth = new AuthService(process.env.
|
|
45
|
+
const auth = new AuthService(process.env.ACLI_HUB_AUTH_PASSWORD);
|
|
38
46
|
|
|
39
47
|
function requireAuth(req, res, next) {
|
|
40
48
|
if (!auth.isEnabled()) return next();
|
|
@@ -130,7 +138,7 @@ server.listen(PORT, HOST, async () => {
|
|
|
130
138
|
if (auth.isEnabled()) {
|
|
131
139
|
console.log('Auth enabled: login required (sessions last 7 days)');
|
|
132
140
|
} else {
|
|
133
|
-
console.warn('WARNING:
|
|
141
|
+
console.warn('WARNING: ACLI_HUB_AUTH_PASSWORD is not set — running WITHOUT authentication. Do not expose this to the internet.');
|
|
134
142
|
}
|
|
135
143
|
const restored = await sessionManager.restore();
|
|
136
144
|
if (restored.length > 0) {
|
package/server/routes/files.js
CHANGED
|
@@ -69,10 +69,10 @@ function respondError(res, err) {
|
|
|
69
69
|
res.status(status).json({ error: err.message });
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
module.exports = function filesRoute({ platform = process.platform, home = os.homedir(), uploadMaxMb = process.env.
|
|
72
|
+
module.exports = function filesRoute({ platform = process.platform, home = os.homedir(), uploadMaxMb = process.env.ACLI_HUB_UPLOAD_MAX_MB ?? '500' } = {}) {
|
|
73
73
|
const router = express.Router();
|
|
74
74
|
const maximum = Number(uploadMaxMb);
|
|
75
|
-
if (!Number.isFinite(maximum) || maximum < 0) throw new Error('
|
|
75
|
+
if (!Number.isFinite(maximum) || maximum < 0) throw new Error('ACLI_HUB_UPLOAD_MAX_MB must be a non-negative number');
|
|
76
76
|
const uploadLimit = maximum * 1024 * 1024;
|
|
77
77
|
const trash = platform === 'darwin' ? 'mv-trash' : 'rm';
|
|
78
78
|
// Serialize app mutations so concurrent renames/trash moves cannot overwrite
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const { spawnSync } = require('child_process');
|
|
2
|
+
|
|
3
|
+
// Homebrew paths are not always on PATH when the process is launched from a
|
|
4
|
+
// GUI (Dock, Spotlight), so probe with the same augmentation SPAWN_ENV uses.
|
|
5
|
+
const EXTRA_PATH = `/usr/local/bin:/opt/homebrew/bin:${process.env.PATH || ''}`;
|
|
6
|
+
|
|
7
|
+
// Placeholder doc links — replace with the real FAQ pages when published.
|
|
8
|
+
const DOCS = 'https://acli-hub.uum.cc/faq';
|
|
9
|
+
|
|
10
|
+
const TOOLS = {
|
|
11
|
+
tmux: {
|
|
12
|
+
why: 'to keep terminal sessions alive across browser disconnects',
|
|
13
|
+
hints: [
|
|
14
|
+
'macOS: brew install tmux',
|
|
15
|
+
'Debian/Ubuntu: sudo apt install tmux',
|
|
16
|
+
'Fedora: sudo dnf install tmux',
|
|
17
|
+
'Arch: sudo pacman -S tmux',
|
|
18
|
+
'Alpine: sudo apk add tmux',
|
|
19
|
+
],
|
|
20
|
+
docs: `${DOCS}/install-tmux.html`,
|
|
21
|
+
},
|
|
22
|
+
ttyd: {
|
|
23
|
+
why: 'to stream the terminal into your browser',
|
|
24
|
+
hints: [
|
|
25
|
+
'macOS: brew install ttyd',
|
|
26
|
+
'Debian/Ubuntu: sudo apt install ttyd',
|
|
27
|
+
'any Linux: sudo snap install ttyd --classic',
|
|
28
|
+
' (or download a binary from https://github.com/tsl0922/ttyd/releases)',
|
|
29
|
+
],
|
|
30
|
+
docs: `${DOCS}/install-ttyd.html`,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// A non-zero exit code still proves the binary exists — only a spawn error
|
|
35
|
+
// (ENOENT, EACCES) counts as "not installed".
|
|
36
|
+
function defaultProbe(tool, env) {
|
|
37
|
+
const args = tool === 'tmux' ? ['-V'] : ['--version'];
|
|
38
|
+
return { ok: !spawnSync(tool, args, { env, stdio: 'ignore' }).error };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function checkDependencies({ platform = process.platform, env = { ...process.env, PATH: EXTRA_PATH }, probe = defaultProbe } = {}) {
|
|
42
|
+
const missing = [];
|
|
43
|
+
for (const tool of Object.keys(TOOLS)) {
|
|
44
|
+
if (!probe(tool, env).ok) missing.push({ tool });
|
|
45
|
+
}
|
|
46
|
+
return missing;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// tmux has no native Windows build (its ncurses dependency is POSIX-only),
|
|
50
|
+
// so any missing dependency on Windows gets one shared WSL walkthrough that
|
|
51
|
+
// also covers the shell: the Ubuntu that wsl --install provides ships bash.
|
|
52
|
+
function windowsReport() {
|
|
53
|
+
return [
|
|
54
|
+
'✖ Windows is not supported natively',
|
|
55
|
+
'',
|
|
56
|
+
' ACLI Hub is built on tmux + ttyd + a POSIX shell, and tmux has no',
|
|
57
|
+
' native Windows build. Run it inside WSL instead:',
|
|
58
|
+
'',
|
|
59
|
+
' 1. wsl --install (PowerShell as admin —',
|
|
60
|
+
' installs Ubuntu, which already includes bash)',
|
|
61
|
+
' 2. inside WSL: sudo apt install tmux ttyd',
|
|
62
|
+
' 3. inside WSL: npm install -g @surenkid/acli-hub && acli-hub',
|
|
63
|
+
'',
|
|
64
|
+
' Note: Git Bash alone is not enough — tmux cannot run outside WSL.',
|
|
65
|
+
'',
|
|
66
|
+
` Full Windows guide: ${DOCS}/install-windows.html`,
|
|
67
|
+
].join('\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Claude-Code-style actionable error: what is missing, why it matters, the
|
|
71
|
+
// shortest working command per platform, and where to read more. macOS users
|
|
72
|
+
// only see the brew line; Linux keeps the whole distro family (the distro
|
|
73
|
+
// itself is not probed, so all of apt/dnf/pacman/apk stay listed).
|
|
74
|
+
function relevantHints(hints, platform) {
|
|
75
|
+
return platform === 'darwin' ? hints.filter(line => line.startsWith('macOS:')) : hints.filter(line => !line.startsWith('macOS:'));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function dependencyReport(missing, platform = process.platform) {
|
|
79
|
+
if (platform === 'win32') return windowsReport();
|
|
80
|
+
return missing.map(({ tool }) => {
|
|
81
|
+
const spec = TOOLS[tool];
|
|
82
|
+
return [
|
|
83
|
+
`✖ ${tool} is not installed`,
|
|
84
|
+
'',
|
|
85
|
+
` ACLI Hub needs ${tool} ${spec.why}, but it wasn't found on your PATH.`,
|
|
86
|
+
'',
|
|
87
|
+
...relevantHints(spec.hints, platform).map(line => ` ${line}`),
|
|
88
|
+
'',
|
|
89
|
+
` Full install guide: ${spec.docs}`,
|
|
90
|
+
].join('\n');
|
|
91
|
+
}).join('\n\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = { checkDependencies, dependencyReport };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const test = require('node:test');
|
|
2
|
+
const assert = require('node:assert');
|
|
3
|
+
const { checkDependencies, dependencyReport } = require('./deps');
|
|
4
|
+
|
|
5
|
+
const found = () => ({ ok: true });
|
|
6
|
+
const notFound = () => ({ ok: false });
|
|
7
|
+
|
|
8
|
+
test('nothing is reported when both tools probe successfully', () => {
|
|
9
|
+
assert.deepEqual(checkDependencies({ probe: found }), []);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('a missing tool lists a complete set of per-platform quick installs', () => {
|
|
13
|
+
const missing = checkDependencies({ probe: tool => tool === 'ttyd' ? found() : notFound() });
|
|
14
|
+
assert.deepEqual(missing, [{ tool: 'tmux' }]);
|
|
15
|
+
|
|
16
|
+
const report = dependencyReport(missing, 'darwin');
|
|
17
|
+
assert.match(report, /✖ tmux is not installed/);
|
|
18
|
+
assert.match(report, /brew install tmux/);
|
|
19
|
+
assert.match(report, /Full install guide: https:\/\/acli-hub\.uum\.cc\/faq\/install-tmux\.html/);
|
|
20
|
+
assert.ok(!report.includes('apt'), 'darwin report should not suggest apt');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('linux reports cover apt/dnf/pacman/apk, snap and the release binaries', () => {
|
|
24
|
+
const report = dependencyReport(checkDependencies({ probe: notFound }), 'linux');
|
|
25
|
+
assert.match(report, /✖ tmux is not installed/);
|
|
26
|
+
assert.match(report, /sudo apt install tmux/);
|
|
27
|
+
assert.match(report, /sudo dnf install tmux/);
|
|
28
|
+
assert.match(report, /sudo pacman -S tmux/);
|
|
29
|
+
assert.match(report, /sudo apk add tmux/);
|
|
30
|
+
assert.match(report, /✖ ttyd is not installed/);
|
|
31
|
+
assert.match(report, /sudo apt install ttyd/);
|
|
32
|
+
assert.match(report, /snap install ttyd --classic/);
|
|
33
|
+
assert.match(report, /github\.com\/tsl0922\/ttyd\/releases/);
|
|
34
|
+
assert.match(report, /install-ttyd\.html/);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('any missing dependency on Windows collapses into one WSL walkthrough', () => {
|
|
38
|
+
const report = dependencyReport([{ tool: 'tmux' }], 'win32');
|
|
39
|
+
assert.match(report, /✖ Windows is not supported natively/);
|
|
40
|
+
assert.match(report, /wsl --install/);
|
|
41
|
+
assert.match(report, /sudo apt install tmux ttyd/);
|
|
42
|
+
assert.match(report, /includes bash/);
|
|
43
|
+
assert.match(report, /Git Bash alone is not enough/);
|
|
44
|
+
assert.match(report, /install-windows\.html/);
|
|
45
|
+
assert.ok(!report.includes('brew install'), 'windows report should not suggest brew');
|
|
46
|
+
});
|