@gitim-runtime/cli 0.1.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/dist/client.d.ts +34 -0
- package/dist/client.js +99 -0
- package/dist/commands/archive-channel.d.ts +1 -0
- package/dist/commands/archive-channel.js +19 -0
- package/dist/commands/archived-channels.d.ts +1 -0
- package/dist/commands/archived-channels.js +26 -0
- package/dist/commands/channels.d.ts +1 -0
- package/dist/commands/channels.js +18 -0
- package/dist/commands/create-channel.d.ts +4 -0
- package/dist/commands/create-channel.js +19 -0
- package/dist/commands/dm.d.ts +10 -0
- package/dist/commands/dm.js +81 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +24 -0
- package/dist/commands/join-channel.d.ts +3 -0
- package/dist/commands/join-channel.js +20 -0
- package/dist/commands/onboard.d.ts +17 -0
- package/dist/commands/onboard.js +261 -0
- package/dist/commands/read.d.ts +4 -0
- package/dist/commands/read.js +20 -0
- package/dist/commands/reindex.d.ts +1 -0
- package/dist/commands/reindex.js +18 -0
- package/dist/commands/search.d.ts +7 -0
- package/dist/commands/search.js +41 -0
- package/dist/commands/send.d.ts +4 -0
- package/dist/commands/send.js +19 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +18 -0
- package/dist/commands/stop.d.ts +1 -0
- package/dist/commands/stop.js +21 -0
- package/dist/commands/tui.d.ts +1 -0
- package/dist/commands/tui.js +57 -0
- package/dist/commands/users.d.ts +1 -0
- package/dist/commands/users.js +18 -0
- package/dist/commands/webui.d.ts +5 -0
- package/dist/commands/webui.js +41 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +73 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +141 -0
- package/dist/tui/app.d.ts +43 -0
- package/dist/tui/app.js +461 -0
- package/dist/tui/channel-sidebar.d.ts +14 -0
- package/dist/tui/channel-sidebar.js +62 -0
- package/dist/tui/daemon-connection.d.ts +38 -0
- package/dist/tui/daemon-connection.js +118 -0
- package/dist/tui/mention-popup.d.ts +13 -0
- package/dist/tui/mention-popup.js +59 -0
- package/dist/tui/message-view.d.ts +38 -0
- package/dist/tui/message-view.js +166 -0
- package/dist/tui/split-layout.d.ts +17 -0
- package/dist/tui/split-layout.js +48 -0
- package/dist/tui/status-bar.d.ts +12 -0
- package/dist/tui/status-bar.js +39 -0
- package/dist/tui/themes.d.ts +22 -0
- package/dist/tui/themes.js +49 -0
- package/dist/tui/thread-view.d.ts +14 -0
- package/dist/tui/thread-view.js +72 -0
- package/dist/webui/assets/index-YH6ztb9g.css +1 -0
- package/dist/webui/assets/index-nFs-lh9F.js +49 -0
- package/dist/webui/index.html +13 -0
- package/dist/webui/server.d.ts +7 -0
- package/dist/webui/server.js +229 -0
- package/package.json +28 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>GitIM</title>
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-nFs-lh9F.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-YH6ztb9g.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
</body>
|
|
13
|
+
</html>
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { GitimClient } from '../client.js';
|
|
7
|
+
// Module-level client, initialized in startServer
|
|
8
|
+
let client;
|
|
9
|
+
let repoRoot;
|
|
10
|
+
// ---------- helpers ----------
|
|
11
|
+
function jsonResponse(res, status, body) {
|
|
12
|
+
const json = JSON.stringify(body);
|
|
13
|
+
res.writeHead(status, {
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
'Content-Length': Buffer.byteLength(json),
|
|
16
|
+
});
|
|
17
|
+
res.end(json);
|
|
18
|
+
}
|
|
19
|
+
function parseQuery(url) {
|
|
20
|
+
const idx = url.indexOf('?');
|
|
21
|
+
return new URLSearchParams(idx >= 0 ? url.slice(idx + 1) : '');
|
|
22
|
+
}
|
|
23
|
+
const MAX_BODY_BYTES = 64 * 1024;
|
|
24
|
+
async function readBody(req) {
|
|
25
|
+
const chunks = [];
|
|
26
|
+
let total = 0;
|
|
27
|
+
for await (const chunk of req) {
|
|
28
|
+
total += chunk.length;
|
|
29
|
+
if (total > MAX_BODY_BYTES)
|
|
30
|
+
throw new Error('Request body too large');
|
|
31
|
+
chunks.push(chunk);
|
|
32
|
+
}
|
|
33
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
34
|
+
}
|
|
35
|
+
const MIME_TYPES = {
|
|
36
|
+
'.html': 'text/html; charset=utf-8',
|
|
37
|
+
'.css': 'text/css; charset=utf-8',
|
|
38
|
+
'.js': 'application/javascript; charset=utf-8',
|
|
39
|
+
'.json': 'application/json; charset=utf-8',
|
|
40
|
+
'.svg': 'image/svg+xml',
|
|
41
|
+
'.png': 'image/png',
|
|
42
|
+
'.jpg': 'image/jpeg',
|
|
43
|
+
'.ico': 'image/x-icon',
|
|
44
|
+
'.woff': 'font/woff',
|
|
45
|
+
'.woff2': 'font/woff2',
|
|
46
|
+
};
|
|
47
|
+
// ---------- /api/me ----------
|
|
48
|
+
async function handleMe(res) {
|
|
49
|
+
const meJsonPath = path.join(repoRoot, '.gitim', 'me.json');
|
|
50
|
+
try {
|
|
51
|
+
const raw = await fs.readFile(meJsonPath, 'utf-8');
|
|
52
|
+
const me = JSON.parse(raw);
|
|
53
|
+
jsonResponse(res, 200, { ok: true, data: me });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
try {
|
|
57
|
+
const name = execSync('git config user.name', { cwd: repoRoot, encoding: 'utf-8' }).trim();
|
|
58
|
+
jsonResponse(res, 200, { ok: true, data: { handler: name.toLowerCase().replace(/\s+/g, '-'), display_name: name } });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
jsonResponse(res, 500, { ok: false, error: 'Cannot determine identity: no me.json and git config user.name not set' });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ---------- API routing ----------
|
|
66
|
+
async function handleApi(req, res) {
|
|
67
|
+
const url = req.url ?? '/';
|
|
68
|
+
const pathname = url.split('?')[0];
|
|
69
|
+
const query = parseQuery(url);
|
|
70
|
+
try {
|
|
71
|
+
if (pathname === '/api/me' && req.method === 'GET') {
|
|
72
|
+
await handleMe(res);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (pathname === '/api/poll' && req.method === 'GET') {
|
|
76
|
+
const since = query.get('since') ?? undefined;
|
|
77
|
+
const result = await client.poll(since);
|
|
78
|
+
jsonResponse(res, 200, result);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (pathname === '/api/channels' && req.method === 'GET') {
|
|
82
|
+
const result = await client.listChannels();
|
|
83
|
+
jsonResponse(res, 200, result);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (pathname === '/api/users' && req.method === 'GET') {
|
|
87
|
+
const result = await client.listUsers();
|
|
88
|
+
jsonResponse(res, 200, result);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (pathname === '/api/read' && req.method === 'GET') {
|
|
92
|
+
const channel = query.get('channel');
|
|
93
|
+
if (!channel) {
|
|
94
|
+
jsonResponse(res, 400, { ok: false, error: 'Missing required parameter: channel' });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const limitStr = query.get('limit');
|
|
98
|
+
const limit = limitStr ? parseInt(limitStr, 10) : undefined;
|
|
99
|
+
const result = await client.read(channel, limit);
|
|
100
|
+
jsonResponse(res, 200, result);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (pathname === '/api/thread' && req.method === 'GET') {
|
|
104
|
+
const channel = query.get('channel');
|
|
105
|
+
const lineStr = query.get('line');
|
|
106
|
+
if (!channel || !lineStr) {
|
|
107
|
+
jsonResponse(res, 400, { ok: false, error: 'Missing required parameters: channel, line' });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const line = parseInt(lineStr, 10);
|
|
111
|
+
const result = await client.getThread(channel, line);
|
|
112
|
+
jsonResponse(res, 200, result);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (pathname === '/api/send' && req.method === 'POST') {
|
|
116
|
+
const raw = await readBody(req);
|
|
117
|
+
let body;
|
|
118
|
+
try {
|
|
119
|
+
body = JSON.parse(raw);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
jsonResponse(res, 400, { ok: false, error: 'Invalid JSON body' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (!body.channel || !body.body) {
|
|
126
|
+
jsonResponse(res, 400, { ok: false, error: 'Missing required fields: channel, body' });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const result = await client.send(body.channel, body.body, body.author, body.reply_to);
|
|
130
|
+
jsonResponse(res, 200, result);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
jsonResponse(res, 404, { ok: false, error: `Unknown API endpoint: ${pathname}` });
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
137
|
+
jsonResponse(res, 502, { ok: false, error: `Daemon error: ${message}` });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// ---------- Static file serving (production) ----------
|
|
141
|
+
function serveStatic(req, res, staticDir) {
|
|
142
|
+
const url = req.url ?? '/';
|
|
143
|
+
const pathname = url.split('?')[0];
|
|
144
|
+
// Resolve and guard against path traversal
|
|
145
|
+
const requestedPath = path.resolve(staticDir, '.' + pathname);
|
|
146
|
+
if (!requestedPath.startsWith(staticDir)) {
|
|
147
|
+
res.writeHead(403);
|
|
148
|
+
res.end('Forbidden');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
let filePath = requestedPath;
|
|
152
|
+
// If requesting a directory, try index.html
|
|
153
|
+
if (filePath.endsWith('/') || !path.extname(filePath)) {
|
|
154
|
+
const asIndex = path.join(filePath, 'index.html');
|
|
155
|
+
if (existsSync(asIndex)) {
|
|
156
|
+
filePath = asIndex;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Try to serve the file; SPA fallback to index.html if not found
|
|
160
|
+
if (!existsSync(filePath) || statSync(filePath).isDirectory()) {
|
|
161
|
+
filePath = path.join(staticDir, 'index.html');
|
|
162
|
+
if (!existsSync(filePath)) {
|
|
163
|
+
res.writeHead(404);
|
|
164
|
+
res.end('Not Found');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const ext = path.extname(filePath);
|
|
169
|
+
const contentType = MIME_TYPES[ext] ?? 'application/octet-stream';
|
|
170
|
+
const content = readFileSync(filePath);
|
|
171
|
+
res.writeHead(200, {
|
|
172
|
+
'Content-Type': contentType,
|
|
173
|
+
'Content-Length': content.length,
|
|
174
|
+
});
|
|
175
|
+
res.end(content);
|
|
176
|
+
}
|
|
177
|
+
export async function startServer(options) {
|
|
178
|
+
repoRoot = options.repoRoot;
|
|
179
|
+
client = new GitimClient(repoRoot);
|
|
180
|
+
const staticDir = path.resolve(import.meta.dirname, '../../dist/webui');
|
|
181
|
+
const viteRoot = path.resolve(import.meta.dirname, '../../../webui');
|
|
182
|
+
// In dev mode, dynamically import vite and create middleware-mode server.
|
|
183
|
+
// Use string variable for import() to bypass TypeScript module resolution —
|
|
184
|
+
// vite is only a devDependency of the webui package, not of cli.
|
|
185
|
+
let vite;
|
|
186
|
+
if (options.dev) {
|
|
187
|
+
const vitePkg = 'vite';
|
|
188
|
+
const { createServer: createViteServer } = await import(/* webpackIgnore: true */ vitePkg);
|
|
189
|
+
vite = await createViteServer({
|
|
190
|
+
root: viteRoot,
|
|
191
|
+
server: { middlewareMode: true },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const server = http.createServer(async (req, res) => {
|
|
195
|
+
const url = req.url ?? '/';
|
|
196
|
+
// API routes
|
|
197
|
+
if (url.startsWith('/api/')) {
|
|
198
|
+
await handleApi(req, res);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Dev mode: proxy through Vite
|
|
202
|
+
if (vite) {
|
|
203
|
+
vite.middlewares(req, res);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// Production: serve static files
|
|
207
|
+
serveStatic(req, res, staticDir);
|
|
208
|
+
});
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
server.on('error', (err) => {
|
|
211
|
+
if (err.code === 'EADDRINUSE') {
|
|
212
|
+
reject(new Error(`Port ${options.port} is already in use`));
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
reject(err);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
server.listen(options.port, '127.0.0.1', () => {
|
|
219
|
+
console.log(`GitIM WebUI server listening on http://localhost:${options.port}`);
|
|
220
|
+
if (options.dev) {
|
|
221
|
+
console.log(' Mode: development (Vite HMR enabled)');
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
console.log(' Mode: production (serving static files)');
|
|
225
|
+
}
|
|
226
|
+
resolve(server);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gitim-runtime/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"gitim": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "tsx src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"optionalDependencies": {
|
|
16
|
+
"@gitim-runtime/daemon-darwin-arm64": "0.1.0"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@mariozechner/pi-tui": "^0.60.0",
|
|
20
|
+
"chalk": "^5.6.2",
|
|
21
|
+
"commander": "^13.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.0.0",
|
|
25
|
+
"tsx": "^4.19.0",
|
|
26
|
+
"typescript": "^5.7.0"
|
|
27
|
+
}
|
|
28
|
+
}
|