@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
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface ApiResponse {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
data?: any;
|
|
4
|
+
error?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class GitimClient {
|
|
7
|
+
private socketPath;
|
|
8
|
+
constructor(repoRoot: string);
|
|
9
|
+
request(method: string, params?: Record<string, any>): Promise<ApiResponse>;
|
|
10
|
+
status(): Promise<ApiResponse>;
|
|
11
|
+
send(channel: string, body: string, author?: string, replyTo?: number): Promise<ApiResponse>;
|
|
12
|
+
read(channel: string, limit?: number, since?: number): Promise<ApiResponse>;
|
|
13
|
+
listChannels(): Promise<ApiResponse>;
|
|
14
|
+
listUsers(): Promise<ApiResponse>;
|
|
15
|
+
getThread(channel: string, lineNumber: number): Promise<ApiResponse>;
|
|
16
|
+
registerUser(handler: string, displayName: string, role?: string, introduction?: string): Promise<ApiResponse>;
|
|
17
|
+
onboard(gitServer: string, auth: Record<string, string>, admin?: boolean, guest?: boolean): Promise<ApiResponse>;
|
|
18
|
+
joinChannel(channel: string, targets?: string[]): Promise<ApiResponse>;
|
|
19
|
+
leaveChannel(channel: string, targets?: string[]): Promise<ApiResponse>;
|
|
20
|
+
createChannel(name: string, displayName?: string, introduction?: string): Promise<ApiResponse>;
|
|
21
|
+
archiveChannel(channel: string): Promise<ApiResponse>;
|
|
22
|
+
listArchivedChannels(): Promise<ApiResponse>;
|
|
23
|
+
stop(): Promise<ApiResponse>;
|
|
24
|
+
poll(since?: string): Promise<ApiResponse>;
|
|
25
|
+
search(params: {
|
|
26
|
+
query?: string;
|
|
27
|
+
author?: string;
|
|
28
|
+
channel?: string;
|
|
29
|
+
channel_type?: string;
|
|
30
|
+
limit?: number;
|
|
31
|
+
offset?: number;
|
|
32
|
+
}): Promise<ApiResponse>;
|
|
33
|
+
reindex(): Promise<ApiResponse>;
|
|
34
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import readline from 'node:readline';
|
|
4
|
+
export class GitimClient {
|
|
5
|
+
socketPath;
|
|
6
|
+
constructor(repoRoot) {
|
|
7
|
+
this.socketPath = path.join(repoRoot, '.gitim', 'run', 'gitim.sock');
|
|
8
|
+
}
|
|
9
|
+
async request(method, params = {}) {
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const socket = net.createConnection(this.socketPath);
|
|
12
|
+
const payload = JSON.stringify({ method, ...params }) + '\n';
|
|
13
|
+
socket.on('connect', () => {
|
|
14
|
+
socket.write(payload);
|
|
15
|
+
});
|
|
16
|
+
const rl = readline.createInterface({ input: socket });
|
|
17
|
+
rl.on('error', () => { }); // socket error is already handled below
|
|
18
|
+
rl.on('line', (line) => {
|
|
19
|
+
try {
|
|
20
|
+
resolve(JSON.parse(line));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
reject(new Error(`Invalid response: ${line}`));
|
|
24
|
+
}
|
|
25
|
+
socket.end();
|
|
26
|
+
});
|
|
27
|
+
socket.on('error', (err) => {
|
|
28
|
+
reject(new Error(`Cannot connect to daemon: ${err.message}`));
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async status() {
|
|
33
|
+
return this.request('status');
|
|
34
|
+
}
|
|
35
|
+
async send(channel, body, author, replyTo) {
|
|
36
|
+
return this.request('send', { channel, body, author: author ?? null, reply_to: replyTo ?? null });
|
|
37
|
+
}
|
|
38
|
+
async read(channel, limit, since) {
|
|
39
|
+
return this.request('read', { channel, limit: limit ?? null, since: since ?? null });
|
|
40
|
+
}
|
|
41
|
+
async listChannels() {
|
|
42
|
+
return this.request('channels');
|
|
43
|
+
}
|
|
44
|
+
async listUsers() {
|
|
45
|
+
return this.request('users');
|
|
46
|
+
}
|
|
47
|
+
async getThread(channel, lineNumber) {
|
|
48
|
+
return this.request('thread', { channel, line_number: lineNumber });
|
|
49
|
+
}
|
|
50
|
+
async registerUser(handler, displayName, role, introduction) {
|
|
51
|
+
return this.request('register_user', {
|
|
52
|
+
handler,
|
|
53
|
+
display_name: displayName,
|
|
54
|
+
role: role ?? 'member',
|
|
55
|
+
introduction: introduction ?? 'GitIM user',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async onboard(gitServer, auth, admin, guest) {
|
|
59
|
+
return this.request('onboard', { git_server: gitServer, auth, admin: admin ?? false, guest: guest ?? false });
|
|
60
|
+
}
|
|
61
|
+
async joinChannel(channel, targets) {
|
|
62
|
+
return this.request('join_channel', { channel, targets: targets ?? [] });
|
|
63
|
+
}
|
|
64
|
+
async leaveChannel(channel, targets) {
|
|
65
|
+
return this.request('leave_channel', { channel, targets: targets ?? [] });
|
|
66
|
+
}
|
|
67
|
+
async createChannel(name, displayName, introduction) {
|
|
68
|
+
return this.request('create_channel', {
|
|
69
|
+
name,
|
|
70
|
+
display_name: displayName,
|
|
71
|
+
introduction,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async archiveChannel(channel) {
|
|
75
|
+
return this.request('archive_channel', { channel });
|
|
76
|
+
}
|
|
77
|
+
async listArchivedChannels() {
|
|
78
|
+
return this.request('archived_channels');
|
|
79
|
+
}
|
|
80
|
+
async stop() {
|
|
81
|
+
return this.request('stop');
|
|
82
|
+
}
|
|
83
|
+
async poll(since) {
|
|
84
|
+
return this.request('poll', { since: since ?? null });
|
|
85
|
+
}
|
|
86
|
+
async search(params) {
|
|
87
|
+
return this.request('search', {
|
|
88
|
+
query: params.query ?? null,
|
|
89
|
+
author: params.author ?? null,
|
|
90
|
+
channel: params.channel ?? null,
|
|
91
|
+
channel_type: params.channel_type ?? null,
|
|
92
|
+
limit: params.limit ?? 50,
|
|
93
|
+
offset: params.offset ?? 0,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async reindex() {
|
|
97
|
+
return this.request('reindex');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function archiveChannelCommand(name: string): Promise<void>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { findRepoRoot, ensureDaemon } from '../daemon.js';
|
|
2
|
+
import { GitimClient } from '../client.js';
|
|
3
|
+
export async function archiveChannelCommand(name) {
|
|
4
|
+
const repoRoot = findRepoRoot();
|
|
5
|
+
if (!repoRoot) {
|
|
6
|
+
console.error('Not in a GitIM repository');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
await ensureDaemon(repoRoot);
|
|
10
|
+
const client = new GitimClient(repoRoot);
|
|
11
|
+
const res = await client.archiveChannel(name);
|
|
12
|
+
if (res.ok) {
|
|
13
|
+
console.log(`频道 #${name} 已归档`);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.error(`归档失败: ${res.error}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function archivedChannelsCommand(): Promise<void>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { findRepoRoot, ensureDaemon } from '../daemon.js';
|
|
2
|
+
import { GitimClient } from '../client.js';
|
|
3
|
+
export async function archivedChannelsCommand() {
|
|
4
|
+
const repoRoot = findRepoRoot();
|
|
5
|
+
if (!repoRoot) {
|
|
6
|
+
console.error('Not in a GitIM repository');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
await ensureDaemon(repoRoot);
|
|
10
|
+
const client = new GitimClient(repoRoot);
|
|
11
|
+
const res = await client.listArchivedChannels();
|
|
12
|
+
if (res.ok) {
|
|
13
|
+
const channels = res.data.channels;
|
|
14
|
+
if (channels.length === 0) {
|
|
15
|
+
console.log('暂无已归档频道');
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
for (const ch of channels) {
|
|
19
|
+
console.log(`#${ch.name}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.error('Error:', res.error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function channelsCommand(): Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { findRepoRoot, ensureDaemon } from '../daemon.js';
|
|
2
|
+
import { GitimClient } from '../client.js';
|
|
3
|
+
export async function channelsCommand() {
|
|
4
|
+
const repoRoot = findRepoRoot();
|
|
5
|
+
if (!repoRoot) {
|
|
6
|
+
console.error('Not in a GitIM repository');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
await ensureDaemon(repoRoot);
|
|
10
|
+
const client = new GitimClient(repoRoot);
|
|
11
|
+
const res = await client.listChannels();
|
|
12
|
+
if (res.ok) {
|
|
13
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.error('Error:', res.error);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { findRepoRoot, ensureDaemon } from '../daemon.js';
|
|
2
|
+
import { GitimClient } from '../client.js';
|
|
3
|
+
export async function createChannelCommand(name, options) {
|
|
4
|
+
const repoRoot = findRepoRoot();
|
|
5
|
+
if (!repoRoot) {
|
|
6
|
+
console.error('Not in a GitIM repository');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
await ensureDaemon(repoRoot);
|
|
10
|
+
const client = new GitimClient(repoRoot);
|
|
11
|
+
const res = await client.createChannel(name, options.displayName, options.introduction);
|
|
12
|
+
if (res.ok) {
|
|
13
|
+
console.log(`频道 #${name} 创建成功`);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.error(`创建失败: ${res.error}`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function dmSendCommand(handler: string, body: string, options: {
|
|
2
|
+
author?: string;
|
|
3
|
+
replyTo?: string;
|
|
4
|
+
}): Promise<void>;
|
|
5
|
+
export declare function dmReadCommand(handler: string, options: {
|
|
6
|
+
author?: string;
|
|
7
|
+
limit?: string;
|
|
8
|
+
since?: string;
|
|
9
|
+
}): Promise<void>;
|
|
10
|
+
export declare function dmListCommand(): Promise<void>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { findRepoRoot, ensureDaemon } from '../daemon.js';
|
|
4
|
+
import { GitimClient } from '../client.js';
|
|
5
|
+
function resolveAuthor(repoRoot, explicit) {
|
|
6
|
+
if (explicit)
|
|
7
|
+
return explicit;
|
|
8
|
+
const mePath = path.join(repoRoot, '.gitim', 'me.json');
|
|
9
|
+
if (fs.existsSync(mePath)) {
|
|
10
|
+
const me = JSON.parse(fs.readFileSync(mePath, 'utf-8'));
|
|
11
|
+
return me.handler;
|
|
12
|
+
}
|
|
13
|
+
console.error('Error: 未配置身份,请先运行 gitim onboard');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
export async function dmSendCommand(handler, body, options) {
|
|
17
|
+
const repoRoot = findRepoRoot();
|
|
18
|
+
if (!repoRoot) {
|
|
19
|
+
console.error('Not in a GitIM repository');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const author = resolveAuthor(repoRoot, options.author);
|
|
23
|
+
await ensureDaemon(repoRoot);
|
|
24
|
+
const client = new GitimClient(repoRoot);
|
|
25
|
+
const [h1, h2] = [author, handler].sort();
|
|
26
|
+
const channel = `dm:${h1},${h2}`;
|
|
27
|
+
const replyTo = options.replyTo ? parseInt(options.replyTo, 10) : undefined;
|
|
28
|
+
const res = await client.send(channel, body, author, replyTo);
|
|
29
|
+
if (res.ok) {
|
|
30
|
+
console.log('DM sent.');
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
console.error('Error:', res.error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function dmReadCommand(handler, options) {
|
|
37
|
+
const repoRoot = findRepoRoot();
|
|
38
|
+
if (!repoRoot) {
|
|
39
|
+
console.error('Not in a GitIM repository');
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const author = resolveAuthor(repoRoot, options.author);
|
|
43
|
+
await ensureDaemon(repoRoot);
|
|
44
|
+
const client = new GitimClient(repoRoot);
|
|
45
|
+
const [h1, h2] = [author, handler].sort();
|
|
46
|
+
const channel = `dm:${h1},${h2}`;
|
|
47
|
+
const limit = options.limit ? parseInt(options.limit, 10) : undefined;
|
|
48
|
+
const since = options.since ? parseInt(options.since, 10) : undefined;
|
|
49
|
+
const res = await client.read(channel, limit, since);
|
|
50
|
+
if (res.ok) {
|
|
51
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.error('Error:', res.error);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function dmListCommand() {
|
|
58
|
+
const repoRoot = findRepoRoot();
|
|
59
|
+
if (!repoRoot) {
|
|
60
|
+
console.error('Not in a GitIM repository');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
// List DM files by scanning dm/ directory
|
|
64
|
+
const fs = await import('node:fs');
|
|
65
|
+
const path = await import('node:path');
|
|
66
|
+
const dmDir = path.join(repoRoot, 'dm');
|
|
67
|
+
if (!fs.existsSync(dmDir)) {
|
|
68
|
+
console.log('No DM conversations.');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const files = fs.readdirSync(dmDir).filter((f) => f.endsWith('.thread'));
|
|
72
|
+
const conversations = files.map((f) => f.replace('.thread', ''));
|
|
73
|
+
if (conversations.length === 0) {
|
|
74
|
+
console.log('No DM conversations.');
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
for (const conv of conversations) {
|
|
78
|
+
console.log(conv);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initRepo(dir?: string): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function initRepo(dir = process.cwd()) {
|
|
4
|
+
const dirs = [
|
|
5
|
+
path.join(dir, '.gitim'),
|
|
6
|
+
path.join(dir, 'users'),
|
|
7
|
+
path.join(dir, 'channels'),
|
|
8
|
+
];
|
|
9
|
+
for (const d of dirs) {
|
|
10
|
+
fs.mkdirSync(d, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
const configPath = path.join(dir, '.gitim', 'config.yaml');
|
|
13
|
+
if (!fs.existsSync(configPath)) {
|
|
14
|
+
fs.writeFileSync(configPath, 'version: 1\n');
|
|
15
|
+
}
|
|
16
|
+
const gitignorePath = path.join(dir, '.gitignore');
|
|
17
|
+
const gitignoreContent = fs.existsSync(gitignorePath)
|
|
18
|
+
? fs.readFileSync(gitignorePath, 'utf-8')
|
|
19
|
+
: '';
|
|
20
|
+
if (!gitignoreContent.includes('.gitim/run/')) {
|
|
21
|
+
fs.appendFileSync(gitignorePath, '\n.gitim/run/\n');
|
|
22
|
+
}
|
|
23
|
+
console.log('GitIM repository initialized.');
|
|
24
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { findRepoRoot, ensureDaemon } from '../daemon.js';
|
|
2
|
+
import { GitimClient } from '../client.js';
|
|
3
|
+
export async function joinChannelCommand(channel, options) {
|
|
4
|
+
const repoRoot = findRepoRoot();
|
|
5
|
+
if (!repoRoot) {
|
|
6
|
+
console.error('Not in a GitIM repository');
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
await ensureDaemon(repoRoot);
|
|
10
|
+
const client = new GitimClient(repoRoot);
|
|
11
|
+
const res = await client.joinChannel(channel, options.targets);
|
|
12
|
+
if (res.ok) {
|
|
13
|
+
const who = options.targets?.length ? options.targets.join(', ') : '你';
|
|
14
|
+
console.log(`${who} 已加入 #${channel}`);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
console.error(`加入失败: ${res.error}`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
type GitServer = 'git' | 'github' | 'gitea' | 'gitlab';
|
|
2
|
+
interface OnboardOptions {
|
|
3
|
+
gitServer: GitServer;
|
|
4
|
+
token?: string;
|
|
5
|
+
handler?: string;
|
|
6
|
+
displayName?: string;
|
|
7
|
+
url?: string;
|
|
8
|
+
refresh?: boolean;
|
|
9
|
+
debugHttp?: boolean;
|
|
10
|
+
withWebui?: boolean;
|
|
11
|
+
webuiPort?: string;
|
|
12
|
+
webuiDev?: boolean;
|
|
13
|
+
admin?: boolean;
|
|
14
|
+
guest?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare function onboardCommand(repoName: string | undefined, org: string | undefined, options: OnboardOptions): Promise<void>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { ensureDaemon, isDaemonRunning } from '../daemon.js';
|
|
5
|
+
import { GitimClient } from '../client.js';
|
|
6
|
+
import { startServer } from '../webui/server.js';
|
|
7
|
+
function buildAuth(gitServer, options) {
|
|
8
|
+
if (gitServer === 'git') {
|
|
9
|
+
return {
|
|
10
|
+
handler: options.handler,
|
|
11
|
+
display_name: options.displayName,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const auth = { token: options.token };
|
|
15
|
+
if ((gitServer === 'gitea' || gitServer === 'gitlab') && options.url) {
|
|
16
|
+
auth.url = options.url;
|
|
17
|
+
}
|
|
18
|
+
return auth;
|
|
19
|
+
}
|
|
20
|
+
function ensureConfigDebugHttp(repoDir, enabled) {
|
|
21
|
+
const configPath = path.join(repoDir, '.gitim', 'config.yaml');
|
|
22
|
+
if (fs.existsSync(configPath)) {
|
|
23
|
+
let content = fs.readFileSync(configPath, 'utf-8');
|
|
24
|
+
if (content.includes('debug_http:')) {
|
|
25
|
+
content = content.replace(/debug_http:\s*(true|false)/, `debug_http: ${enabled}`);
|
|
26
|
+
}
|
|
27
|
+
else if (content.includes('daemon:')) {
|
|
28
|
+
content = content.replace(/daemon:/, `daemon:\n debug_http: ${enabled}`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
content += `\ndaemon:\n debug_http: ${enabled}\n`;
|
|
32
|
+
}
|
|
33
|
+
fs.writeFileSync(configPath, content);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
fs.mkdirSync(path.join(repoDir, '.gitim'), { recursive: true });
|
|
37
|
+
fs.writeFileSync(configPath, `version: 1\ndaemon:\n debug_http: ${enabled}\n`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function validateParams(gitServer, options) {
|
|
41
|
+
if (gitServer === 'git') {
|
|
42
|
+
if (!options.handler) {
|
|
43
|
+
console.error('Error: git 本地模式需要 --handler');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
if (!options.displayName) {
|
|
47
|
+
console.error('Error: git 本地模式需要 --display-name');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
if (!options.token) {
|
|
53
|
+
console.error(`Error: ${gitServer} 模式需要 --token`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
if ((gitServer === 'gitea' || gitServer === 'gitlab') && !options.url) {
|
|
57
|
+
console.error(`Error: ${gitServer} 模式需要 --url(服务地址)`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function cloneOrCreateRepo(repoName, org, gitServer, options) {
|
|
63
|
+
const targetDir = path.resolve(repoName);
|
|
64
|
+
// Determine repo URL (not applicable for plain git local mode)
|
|
65
|
+
if (gitServer === 'git') {
|
|
66
|
+
// Local mode: just create directory + git init
|
|
67
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
68
|
+
try {
|
|
69
|
+
execFileSync('git', ['init'], { cwd: targetDir, stdio: 'ignore' });
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
console.error('Error: git init 失败');
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
return targetDir;
|
|
76
|
+
}
|
|
77
|
+
// Try clone first, then create if needed
|
|
78
|
+
let cloneSucceeded = false;
|
|
79
|
+
if (gitServer === 'github') {
|
|
80
|
+
// GitHub: use gh CLI which resolves owner automatically
|
|
81
|
+
const ghTarget = org ? `${org}/${repoName}` : repoName;
|
|
82
|
+
try {
|
|
83
|
+
execFileSync('gh', ['repo', 'clone', ghTarget, targetDir], { stdio: 'ignore' });
|
|
84
|
+
cloneSucceeded = true;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
cloneSucceeded = false;
|
|
88
|
+
}
|
|
89
|
+
if (!cloneSucceeded) {
|
|
90
|
+
try {
|
|
91
|
+
execFileSync('gh', ['repo', 'create', ghTarget, '--private', '--clone'], {
|
|
92
|
+
cwd: path.dirname(targetDir),
|
|
93
|
+
stdio: 'ignore',
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
console.error(`Error: 无法创建仓库 ${ghTarget}`);
|
|
98
|
+
console.error(' → 请确认 gh 已认证且 Token 有仓库创建权限');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
// Gitea / GitLab: org is required for URL construction
|
|
105
|
+
if (!org) {
|
|
106
|
+
console.error(`Error: ${gitServer} 模式需要指定 org(作为 URL 中的 owner)`);
|
|
107
|
+
console.error(' → 用法: gitim onboard <repo> <org> --git-server gitea --url ...');
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
const baseUrl = options.url;
|
|
111
|
+
const repoUrl = `${baseUrl}/${org}/${repoName}.git`;
|
|
112
|
+
try {
|
|
113
|
+
execFileSync('git', ['clone', repoUrl, targetDir], { stdio: 'ignore' });
|
|
114
|
+
cloneSucceeded = true;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
cloneSucceeded = false;
|
|
118
|
+
}
|
|
119
|
+
if (!cloneSucceeded) {
|
|
120
|
+
if (gitServer === 'gitlab') {
|
|
121
|
+
console.error('Error: GitLab 不支持自动创建仓库,请先在 GitLab 上手动创建');
|
|
122
|
+
console.error(` → 创建后再运行: gitim onboard ${repoName} ${org} --git-server gitlab --url ${baseUrl} --token ...`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
// Gitea: create via API then clone
|
|
126
|
+
const token = options.token;
|
|
127
|
+
const createUrl = `${baseUrl}/api/v1/orgs/${org}/repos`;
|
|
128
|
+
try {
|
|
129
|
+
execFileSync('curl', [
|
|
130
|
+
'-sf', '-X', 'POST',
|
|
131
|
+
'-H', `Authorization: token ${token}`,
|
|
132
|
+
'-H', 'Content-Type: application/json',
|
|
133
|
+
'-d', JSON.stringify({ name: repoName, private: true }),
|
|
134
|
+
createUrl,
|
|
135
|
+
], { stdio: 'ignore' });
|
|
136
|
+
execFileSync('git', ['clone', repoUrl, targetDir], { stdio: 'ignore' });
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
console.error(`Error: 无法创建 Gitea 仓库 ${repoName}`);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return targetDir;
|
|
145
|
+
}
|
|
146
|
+
export async function onboardCommand(repoName, org, options) {
|
|
147
|
+
const gitServer = (options.gitServer || 'github');
|
|
148
|
+
// --guest 和 --admin 互斥
|
|
149
|
+
if (options.guest && options.admin) {
|
|
150
|
+
console.error('Error: --guest 和 --admin 不能同时使用');
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
// --refresh mode: send Onboard request to running daemon
|
|
154
|
+
if (options.refresh) {
|
|
155
|
+
if (!options.guest) {
|
|
156
|
+
validateParams(gitServer, options);
|
|
157
|
+
}
|
|
158
|
+
const cwd = process.cwd();
|
|
159
|
+
const gitimDir = path.join(cwd, '.gitim');
|
|
160
|
+
if (!fs.existsSync(gitimDir)) {
|
|
161
|
+
console.error('不在 GitIM 仓库中,无法 --refresh');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
// If --debug-http is set, update config and restart daemon
|
|
165
|
+
if (options.debugHttp) {
|
|
166
|
+
ensureConfigDebugHttp(cwd, true);
|
|
167
|
+
if (isDaemonRunning(cwd)) {
|
|
168
|
+
const oldClient = new GitimClient(cwd);
|
|
169
|
+
await oldClient.stop().catch(() => { });
|
|
170
|
+
// Wait briefly for daemon to exit
|
|
171
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
await ensureDaemon(cwd);
|
|
175
|
+
const client = new GitimClient(cwd);
|
|
176
|
+
const auth = options.guest ? {} : buildAuth(gitServer, options);
|
|
177
|
+
const res = await client.onboard(gitServer, auth, options.admin, options.guest);
|
|
178
|
+
if (!res.ok) {
|
|
179
|
+
console.error(`身份刷新失败:${res.error}`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
if (options.guest) {
|
|
183
|
+
console.log('游客模式已刷新');
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
const adminTag = options.admin ? ' [ADMIN]' : '';
|
|
187
|
+
console.log(`身份已刷新:@${res.data?.handler}${adminTag}`);
|
|
188
|
+
}
|
|
189
|
+
if (options.withWebui) {
|
|
190
|
+
await launchWebui(cwd, options);
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (!repoName) {
|
|
195
|
+
console.error('请指定仓库名称: gitim onboard <repo_name> [org]');
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
// 1. Validate params
|
|
199
|
+
if (!options.guest) {
|
|
200
|
+
validateParams(gitServer, options);
|
|
201
|
+
}
|
|
202
|
+
// 2. Clone or create repo
|
|
203
|
+
const repoDir = cloneOrCreateRepo(repoName, org, gitServer, options);
|
|
204
|
+
// 3. Ensure .gitim/ directory exists
|
|
205
|
+
fs.mkdirSync(path.join(repoDir, '.gitim'), { recursive: true });
|
|
206
|
+
// 3.5. Write config with debug_http if requested (before daemon starts)
|
|
207
|
+
if (options.debugHttp) {
|
|
208
|
+
ensureConfigDebugHttp(repoDir, true);
|
|
209
|
+
}
|
|
210
|
+
// 4. Start daemon
|
|
211
|
+
await ensureDaemon(repoDir);
|
|
212
|
+
// 5. Send Onboard request
|
|
213
|
+
const client = new GitimClient(repoDir);
|
|
214
|
+
const auth = options.guest ? {} : buildAuth(gitServer, options);
|
|
215
|
+
const res = await client.onboard(gitServer, auth, options.admin, options.guest);
|
|
216
|
+
if (!res.ok) {
|
|
217
|
+
console.error(`Onboard 失败:${res.error}`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
// 6. Report result
|
|
221
|
+
if (options.guest) {
|
|
222
|
+
console.log(`游客模式已启动 @ ${repoName}`);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const handler = res.data?.handler ?? '(unknown)';
|
|
226
|
+
const created = res.data?.created ? '(新建)' : '(已加入)';
|
|
227
|
+
const adminTag = options.admin ? ' [ADMIN]' : '';
|
|
228
|
+
console.log(`成功 ${created}:@${handler}${adminTag} @ ${repoName}`);
|
|
229
|
+
}
|
|
230
|
+
// 7. Optional: start WebUI
|
|
231
|
+
if (options.withWebui) {
|
|
232
|
+
await launchWebui(repoDir, options);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
async function launchWebui(repoDir, options) {
|
|
236
|
+
const port = parseInt(options.webuiPort || '6868', 10);
|
|
237
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
238
|
+
console.error('错误:--webui-port 必须是 1-65535 之间的数字');
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
const dev = options.webuiDev || false;
|
|
242
|
+
try {
|
|
243
|
+
await startServer({ repoRoot: repoDir, port, dev });
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
247
|
+
if (msg.includes('already in use')) {
|
|
248
|
+
console.error(`错误:端口 ${port} 已被占用`);
|
|
249
|
+
console.error(' → 使用 --webui-port <port> 指定其他端口');
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
console.error(`错误:无法启动 WebUI — ${msg}`);
|
|
253
|
+
}
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
// HTTP server 保持进程运行,Ctrl+C 干净退出
|
|
257
|
+
process.on('SIGINT', () => {
|
|
258
|
+
console.log('\n正在关闭 WebUI...');
|
|
259
|
+
process.exit(0);
|
|
260
|
+
});
|
|
261
|
+
}
|