@ryuu-reinzz/ryuu-cli 0.4.3 → 0.5.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/CHANGELOG.md +100 -0
- package/README.md +26 -0
- package/docs/drive.md +484 -0
- package/docs/github.md +464 -0
- package/package.json +4 -1
- package/plugins/github/README.md +151 -0
- package/plugins/github/api.js +78 -0
- package/plugins/github/auth.js +45 -0
- package/plugins/github/backend.js +191 -0
- package/plugins/github/command.js +307 -0
- package/plugins/github/config.js +209 -0
- package/plugins/github/credentials.js +161 -0
- package/plugins/github/errors.js +49 -0
- package/plugins/github/index.js +41 -0
- package/plugins/github/manager.js +613 -0
- package/plugins/github/tool-helpers.js +259 -0
- package/plugins/github/tools-account.js +64 -0
- package/plugins/github/tools-actions.js +126 -0
- package/plugins/github/tools-issues.js +214 -0
- package/plugins/github/tools-pulls.js +267 -0
- package/plugins/github/tools-releases.js +87 -0
- package/plugins/github/tools-repos.js +392 -0
- package/plugins/github/tools.js +25 -0
- package/plugins/google-drive/auth.js +57 -0
- package/plugins/google-drive/backup.js +282 -0
- package/plugins/google-drive/command.js +266 -0
- package/plugins/google-drive/config.js +252 -0
- package/plugins/google-drive/credentials.js +171 -0
- package/plugins/google-drive/drive-api.js +304 -0
- package/plugins/google-drive/index.js +26 -0
- package/plugins/google-drive/manager.js +507 -0
- package/plugins/google-drive/oauth-backend.js +161 -0
- package/plugins/google-drive/secret.js +50 -0
- package/plugins/google-drive/token-client.js +80 -0
- package/plugins/google-drive/tool-helpers.js +46 -0
- package/plugins/google-drive/tools-backup.js +204 -0
- package/plugins/google-drive/tools-files.js +315 -0
- package/plugins/google-drive/tools.js +13 -0
- package/plugins/google-drive/zip-reader.js +217 -0
- package/plugins/google-drive/zip-writer.js +204 -0
- package/plugins/index.js +58 -0
- package/src/agent/agent.js +12 -2
- package/src/cli/app.js +16 -0
- package/src/cli/commands.js +44 -0
- package/src/cli/markdown.js +10 -5
- package/src/cli/renderer.js +38 -5
- package/src/cli/tui.js +80 -9
- package/src/tools/registry.js +13 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { defineTool } from '../../src/tools/registry.js';
|
|
2
|
+
import {
|
|
3
|
+
assertDestructiveAllowed,
|
|
4
|
+
contentPath,
|
|
5
|
+
limitItems,
|
|
6
|
+
pageQuery,
|
|
7
|
+
pickBranch,
|
|
8
|
+
pickCommit,
|
|
9
|
+
pickRepo,
|
|
10
|
+
refSegment,
|
|
11
|
+
repoBase,
|
|
12
|
+
text,
|
|
13
|
+
} from './tool-helpers.js';
|
|
14
|
+
|
|
15
|
+
const REPO_PROPS = {
|
|
16
|
+
owner: { type: 'string', description: 'Pemilik repository (username atau organisasi GitHub).' },
|
|
17
|
+
repo: { type: 'string', description: 'Nama repository tanpa owner.' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function createRepoTools(plugin) {
|
|
21
|
+
return [
|
|
22
|
+
defineTool({
|
|
23
|
+
name: 'github_list_repositories',
|
|
24
|
+
description:
|
|
25
|
+
'Daftar repository yang dapat diakses akun terhubung (milik sendiri, collaborator, atau organisasi). Membutuhkan permission Metadata read; repository privat hanya muncul bila instalasi aplikasi mencakupnya.',
|
|
26
|
+
risk: 'read',
|
|
27
|
+
schema: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
visibility: { type: 'string', enum: ['all', 'public', 'private'], description: 'Filter visibilitas. Default all.' },
|
|
31
|
+
affiliation: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'owner, collaborator, organization_member (boleh digabung dengan koma). Default semua.',
|
|
34
|
+
},
|
|
35
|
+
sort: { type: 'string', enum: ['updated', 'pushed', 'full_name', 'created'], description: 'Urutan hasil.' },
|
|
36
|
+
direction: { type: 'string', enum: ['asc', 'desc'], description: 'Arah urutan.' },
|
|
37
|
+
per_page: { type: 'integer', minimum: 1, maximum: 100, description: 'Jumlah per halaman (default 30).' },
|
|
38
|
+
page: { type: 'integer', minimum: 1, description: 'Halaman (default 1).' },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
async handler(args) {
|
|
42
|
+
const api = await plugin.requireClient();
|
|
43
|
+
const query = {
|
|
44
|
+
...pageQuery(args),
|
|
45
|
+
...(args.visibility ? { visibility: args.visibility } : {}),
|
|
46
|
+
...(args.affiliation ? { affiliation: args.affiliation } : {}),
|
|
47
|
+
...(args.sort ? { sort: args.sort } : {}),
|
|
48
|
+
...(args.direction ? { direction: args.direction } : {}),
|
|
49
|
+
};
|
|
50
|
+
const repos = await api.paginate('/user/repos', { query, perPage: query.per_page, maxPages: 2, maxItems: 100 });
|
|
51
|
+
return { ok: true, count: repos.length, repositories: limitItems(repos, 100).map(pickRepo) };
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
|
|
55
|
+
defineTool({
|
|
56
|
+
name: 'github_get_repository',
|
|
57
|
+
description:
|
|
58
|
+
'Ambil detail satu repository: default branch, visibilitas, bahasa, statistik, permission akun, dan status arsip.',
|
|
59
|
+
risk: 'read',
|
|
60
|
+
schema: { type: 'object', properties: REPO_PROPS, required: ['owner', 'repo'] },
|
|
61
|
+
async handler(args) {
|
|
62
|
+
const api = await plugin.requireClient();
|
|
63
|
+
const repo = await api.get(repoBase(args));
|
|
64
|
+
return { ok: true, repository: pickRepo(repo) };
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
|
|
68
|
+
defineTool({
|
|
69
|
+
name: 'github_create_repository',
|
|
70
|
+
description:
|
|
71
|
+
'Buat repository baru untuk akun yang terhubung. Membutuhkan permission Administration write atau scope public_repo/repo. Operasi ini mengubah data dan selalu meminta approval.',
|
|
72
|
+
risk: 'write',
|
|
73
|
+
schema: {
|
|
74
|
+
type: 'object',
|
|
75
|
+
properties: {
|
|
76
|
+
name: { type: 'string', description: 'Nama repository baru.' },
|
|
77
|
+
description: { type: 'string', description: 'Deskripsi singkat.' },
|
|
78
|
+
private: { type: 'boolean', description: 'Repository privat bila true.' },
|
|
79
|
+
auto_init: { type: 'boolean', description: 'Inisialisasi dengan README sehingga branch default langsung ada.' },
|
|
80
|
+
gitignore_template: { type: 'string', description: 'Template .gitignore, contoh: Node.' },
|
|
81
|
+
license_template: { type: 'string', description: 'Template lisensi, contoh: mit.' },
|
|
82
|
+
homepage: { type: 'string', description: 'URL homepage repository.' },
|
|
83
|
+
},
|
|
84
|
+
required: ['name'],
|
|
85
|
+
},
|
|
86
|
+
async handler(args) {
|
|
87
|
+
const api = await plugin.requireClient();
|
|
88
|
+
const name = String(args.name || '').trim();
|
|
89
|
+
if (!/^[A-Za-z0-9._-]{1,100}$/.test(name)) throw new Error('Nama repository tidak valid.');
|
|
90
|
+
const body = { name };
|
|
91
|
+
for (const key of ['description', 'auto_init', 'gitignore_template', 'license_template', 'homepage']) {
|
|
92
|
+
if (args[key] !== undefined) body[key] = args[key];
|
|
93
|
+
}
|
|
94
|
+
if (args.private !== undefined) body.private = Boolean(args.private);
|
|
95
|
+
const repo = await api.post('/user/repos', body);
|
|
96
|
+
return { ok: true, repository: pickRepo(repo) };
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
|
|
100
|
+
defineTool({
|
|
101
|
+
name: 'github_update_repository',
|
|
102
|
+
description:
|
|
103
|
+
'Ubah metadata repository (deskripsi, homepage, visibilitas, arsip, topik). Membutuhkan permission Administration write. Operasi ini selalu meminta approval.',
|
|
104
|
+
risk: 'write',
|
|
105
|
+
schema: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
...REPO_PROPS,
|
|
109
|
+
name: { type: 'string', description: 'Nama baru repository.' },
|
|
110
|
+
description: { type: 'string', description: 'Deskripsi baru.' },
|
|
111
|
+
homepage: { type: 'string', description: 'URL homepage baru.' },
|
|
112
|
+
private: { type: 'boolean', description: 'Ubah visibilitas privat/publik.' },
|
|
113
|
+
archived: { type: 'boolean', description: 'Arsipkan atau aktifkan kembali repository.' },
|
|
114
|
+
has_issues: { type: 'boolean', description: 'Aktifkan/nonaktifkan Issues.' },
|
|
115
|
+
has_wiki: { type: 'boolean', description: 'Aktifkan/nonaktifkan Wiki.' },
|
|
116
|
+
default_branch: { type: 'string', description: 'Branch default baru (harus sudah ada).' },
|
|
117
|
+
},
|
|
118
|
+
required: ['owner', 'repo'],
|
|
119
|
+
},
|
|
120
|
+
async handler(args) {
|
|
121
|
+
const api = await plugin.requireClient();
|
|
122
|
+
const body = {};
|
|
123
|
+
for (const key of ['name', 'description', 'homepage', 'private', 'archived', 'has_issues', 'has_wiki', 'default_branch']) {
|
|
124
|
+
if (args[key] !== undefined) body[key] = args[key];
|
|
125
|
+
}
|
|
126
|
+
if (Object.keys(body).length === 0) throw new Error('Tidak ada perubahan yang diminta.');
|
|
127
|
+
const repo = await api.patch(repoBase(args), body);
|
|
128
|
+
return { ok: true, repository: pickRepo(repo) };
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
131
|
+
|
|
132
|
+
defineTool({
|
|
133
|
+
name: 'github_list_branches',
|
|
134
|
+
description: 'Daftar branch pada repository beserta commit SHA terakhir dan status proteksi.',
|
|
135
|
+
risk: 'read',
|
|
136
|
+
schema: {
|
|
137
|
+
type: 'object',
|
|
138
|
+
properties: {
|
|
139
|
+
...REPO_PROPS,
|
|
140
|
+
protected: { type: 'boolean', description: 'Filter hanya branch terproteksi (true) atau tidak (false).' },
|
|
141
|
+
per_page: { type: 'integer', minimum: 1, maximum: 100, description: 'Jumlah per halaman (default 30).' },
|
|
142
|
+
page: { type: 'integer', minimum: 1, description: 'Halaman (default 1).' },
|
|
143
|
+
},
|
|
144
|
+
required: ['owner', 'repo'],
|
|
145
|
+
},
|
|
146
|
+
async handler(args) {
|
|
147
|
+
const api = await plugin.requireClient();
|
|
148
|
+
const branches = await api.paginate(`${repoBase(args)}/branches`, {
|
|
149
|
+
query: {
|
|
150
|
+
...pageQuery(args),
|
|
151
|
+
...(args.protected !== undefined ? { protected: String(args.protected) } : {}),
|
|
152
|
+
},
|
|
153
|
+
maxPages: 2,
|
|
154
|
+
maxItems: 100,
|
|
155
|
+
});
|
|
156
|
+
return { ok: true, count: branches.length, branches: limitItems(branches, 100).map(pickBranch) };
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
|
|
160
|
+
defineTool({
|
|
161
|
+
name: 'github_get_branch',
|
|
162
|
+
description: 'Ambil satu branch berdasarkan nama, termasuk SHA commit terakhir dan status proteksinya.',
|
|
163
|
+
risk: 'read',
|
|
164
|
+
schema: {
|
|
165
|
+
type: 'object',
|
|
166
|
+
properties: { ...REPO_PROPS, branch: { type: 'string', description: 'Nama branch, contoh: main.' } },
|
|
167
|
+
required: ['owner', 'repo', 'branch'],
|
|
168
|
+
},
|
|
169
|
+
async handler(args) {
|
|
170
|
+
const api = await plugin.requireClient();
|
|
171
|
+
const branch = await api.get(`${repoBase(args)}/branches/${refSegment(args.branch, 'branch')}`);
|
|
172
|
+
return { ok: true, branch: pickBranch(branch) };
|
|
173
|
+
},
|
|
174
|
+
}),
|
|
175
|
+
|
|
176
|
+
defineTool({
|
|
177
|
+
name: 'github_list_commits',
|
|
178
|
+
description:
|
|
179
|
+
'Daftar commit pada branch atau path tertentu. Dapat difilter berdasarkan author dan rentang tanggal ISO 8601.',
|
|
180
|
+
risk: 'read',
|
|
181
|
+
schema: {
|
|
182
|
+
type: 'object',
|
|
183
|
+
properties: {
|
|
184
|
+
...REPO_PROPS,
|
|
185
|
+
sha: { type: 'string', description: 'Branch atau SHA commit awal (default branch default repository).' },
|
|
186
|
+
path: { type: 'string', description: 'Hanya commit yang menyentuh path ini.' },
|
|
187
|
+
author: { type: 'string', description: 'Username GitHub atau email author.' },
|
|
188
|
+
since: { type: 'string', description: 'Batas awal ISO 8601, contoh 2026-01-01T00:00:00Z.' },
|
|
189
|
+
until: { type: 'string', description: 'Batas akhir ISO 8601.' },
|
|
190
|
+
per_page: { type: 'integer', minimum: 1, maximum: 100, description: 'Jumlah per halaman (default 30).' },
|
|
191
|
+
page: { type: 'integer', minimum: 1, description: 'Halaman (default 1).' },
|
|
192
|
+
},
|
|
193
|
+
required: ['owner', 'repo'],
|
|
194
|
+
},
|
|
195
|
+
async handler(args) {
|
|
196
|
+
const api = await plugin.requireClient();
|
|
197
|
+
const query = {
|
|
198
|
+
...pageQuery(args),
|
|
199
|
+
...(args.sha ? { sha: args.sha } : {}),
|
|
200
|
+
...(args.path ? { path: args.path } : {}),
|
|
201
|
+
...(args.author ? { author: args.author } : {}),
|
|
202
|
+
...(args.since ? { since: args.since } : {}),
|
|
203
|
+
...(args.until ? { until: args.until } : {}),
|
|
204
|
+
};
|
|
205
|
+
const commits = await api.paginate(`${repoBase(args)}/commits`, { query, maxPages: 2, maxItems: 100 });
|
|
206
|
+
return { ok: true, count: commits.length, commits: limitItems(commits, 100).map(pickCommit) };
|
|
207
|
+
},
|
|
208
|
+
}),
|
|
209
|
+
|
|
210
|
+
defineTool({
|
|
211
|
+
name: 'github_get_commit',
|
|
212
|
+
description: 'Ambil satu commit lengkap dengan pesan, author, parent, dan statistik perubahan file.',
|
|
213
|
+
risk: 'read',
|
|
214
|
+
schema: {
|
|
215
|
+
type: 'object',
|
|
216
|
+
properties: { ...REPO_PROPS, ref: { type: 'string', description: 'SHA commit, branch, atau tag.' } },
|
|
217
|
+
required: ['owner', 'repo', 'ref'],
|
|
218
|
+
},
|
|
219
|
+
async handler(args) {
|
|
220
|
+
const api = await plugin.requireClient();
|
|
221
|
+
const commit = await api.get(`${repoBase(args)}/commits/${refSegment(args.ref)}`);
|
|
222
|
+
return {
|
|
223
|
+
ok: true,
|
|
224
|
+
commit: pickCommit(commit),
|
|
225
|
+
files: limitItems(commit.files || [], 50).map((file) => ({
|
|
226
|
+
filename: file.filename,
|
|
227
|
+
status: file.status,
|
|
228
|
+
additions: file.additions,
|
|
229
|
+
deletions: file.deletions,
|
|
230
|
+
changes: file.changes,
|
|
231
|
+
})),
|
|
232
|
+
};
|
|
233
|
+
},
|
|
234
|
+
}),
|
|
235
|
+
|
|
236
|
+
defineTool({
|
|
237
|
+
name: 'github_get_file_contents',
|
|
238
|
+
description:
|
|
239
|
+
'Baca isi file atau daftar isi folder pada repository. Untuk file teks, isi dikembalikan sudah didekode; file biner atau lebih dari 200 KB hanya mengembalikan metadata.',
|
|
240
|
+
risk: 'read',
|
|
241
|
+
schema: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties: {
|
|
244
|
+
...REPO_PROPS,
|
|
245
|
+
path: { type: 'string', description: 'Path file/folder relatif, contoh: src/index.js.' },
|
|
246
|
+
ref: { type: 'string', description: 'Branch, tag, atau SHA. Default branch default repository.' },
|
|
247
|
+
},
|
|
248
|
+
required: ['owner', 'repo', 'path'],
|
|
249
|
+
},
|
|
250
|
+
async handler(args) {
|
|
251
|
+
const api = await plugin.requireClient();
|
|
252
|
+
const data = await api.get(`${repoBase(args)}/contents/${contentPath(args.path)}`, args.ref ? { ref: args.ref } : null);
|
|
253
|
+
if (Array.isArray(data)) {
|
|
254
|
+
return {
|
|
255
|
+
ok: true,
|
|
256
|
+
type: 'directory',
|
|
257
|
+
count: data.length,
|
|
258
|
+
entries: limitItems(data, 100).map((entry) => ({
|
|
259
|
+
name: entry.name,
|
|
260
|
+
path: entry.path,
|
|
261
|
+
type: entry.type,
|
|
262
|
+
size: entry.size ?? null,
|
|
263
|
+
sha: entry.sha,
|
|
264
|
+
})),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
const base = {
|
|
268
|
+
ok: true,
|
|
269
|
+
type: 'file',
|
|
270
|
+
name: data.name,
|
|
271
|
+
path: data.path,
|
|
272
|
+
sha: data.sha,
|
|
273
|
+
size: data.size,
|
|
274
|
+
html_url: data.html_url || null,
|
|
275
|
+
download_url: data.download_url || null,
|
|
276
|
+
};
|
|
277
|
+
if (data.encoding !== 'base64' || typeof data.content !== 'string') return base;
|
|
278
|
+
if (Number(data.size) > 200_000) {
|
|
279
|
+
return { ...base, truncated: true, note: 'File lebih dari 200 KB; isi tidak didekode.' };
|
|
280
|
+
}
|
|
281
|
+
const buffer = Buffer.from(data.content, 'base64');
|
|
282
|
+
if (buffer.includes(0)) {
|
|
283
|
+
return { ...base, binary: true, note: 'File tampak biner; isi tidak ditampilkan.' };
|
|
284
|
+
}
|
|
285
|
+
return { ...base, content: text(buffer.toString('utf8'), 100_000) };
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
|
|
289
|
+
defineTool({
|
|
290
|
+
name: 'github_create_or_update_file',
|
|
291
|
+
description:
|
|
292
|
+
'Buat file baru atau perbarui file yang ada pada satu commit. Untuk memperbarui file, sertakan SHA file saat ini (dari github_get_file_contents). Membutuhkan permission Contents write. Selalu meminta approval.',
|
|
293
|
+
risk: 'write',
|
|
294
|
+
schema: {
|
|
295
|
+
type: 'object',
|
|
296
|
+
properties: {
|
|
297
|
+
...REPO_PROPS,
|
|
298
|
+
path: { type: 'string', description: 'Path file, contoh: docs/readme.md.' },
|
|
299
|
+
message: { type: 'string', description: 'Pesan commit.' },
|
|
300
|
+
content: { type: 'string', description: 'Isi file sebagai teks biasa (UTF-8).' },
|
|
301
|
+
sha: { type: 'string', description: 'SHA file yang sedang diperbarui; kosongkan untuk file baru.' },
|
|
302
|
+
branch: { type: 'string', description: 'Branch tujuan. Default branch default repository.' },
|
|
303
|
+
},
|
|
304
|
+
required: ['owner', 'repo', 'path', 'message', 'content'],
|
|
305
|
+
},
|
|
306
|
+
async handler(args) {
|
|
307
|
+
const api = await plugin.requireClient();
|
|
308
|
+
const body = {
|
|
309
|
+
message: String(args.message),
|
|
310
|
+
content: Buffer.from(String(args.content), 'utf8').toString('base64'),
|
|
311
|
+
...(args.sha ? { sha: String(args.sha) } : {}),
|
|
312
|
+
...(args.branch ? { branch: String(args.branch) } : {}),
|
|
313
|
+
};
|
|
314
|
+
const result = await api.put(`${repoBase(args)}/contents/${contentPath(args.path)}`, body);
|
|
315
|
+
return {
|
|
316
|
+
ok: true,
|
|
317
|
+
content: result?.content
|
|
318
|
+
? { path: result.content.path, sha: result.content.sha, html_url: result.content.html_url }
|
|
319
|
+
: null,
|
|
320
|
+
commit: result?.commit ? { sha: result.commit.sha, message: text(result.commit.message, 500) } : null,
|
|
321
|
+
};
|
|
322
|
+
},
|
|
323
|
+
}),
|
|
324
|
+
|
|
325
|
+
defineTool({
|
|
326
|
+
name: 'github_delete_file',
|
|
327
|
+
description:
|
|
328
|
+
'Hapus satu file dari repository dengan commit baru. Membutuhkan SHA file saat ini dan permission Contents write. Operasi destruktif: selalu meminta approval dan dapat diblokir oleh konfigurasi confirmDestructive.',
|
|
329
|
+
risk: 'destructive',
|
|
330
|
+
requiresApproval: true,
|
|
331
|
+
schema: {
|
|
332
|
+
type: 'object',
|
|
333
|
+
properties: {
|
|
334
|
+
...REPO_PROPS,
|
|
335
|
+
path: { type: 'string', description: 'Path file yang dihapus.' },
|
|
336
|
+
message: { type: 'string', description: 'Pesan commit penghapusan.' },
|
|
337
|
+
sha: { type: 'string', description: 'SHA file saat ini (wajib).' },
|
|
338
|
+
branch: { type: 'string', description: 'Branch tujuan. Default branch default repository.' },
|
|
339
|
+
},
|
|
340
|
+
required: ['owner', 'repo', 'path', 'message', 'sha'],
|
|
341
|
+
},
|
|
342
|
+
async handler(args) {
|
|
343
|
+
await assertDestructiveAllowed(plugin, 'Penghapusan file');
|
|
344
|
+
const api = await plugin.requireClient();
|
|
345
|
+
const result = await api.delete(`${repoBase(args)}/contents/${contentPath(args.path)}`, {
|
|
346
|
+
message: String(args.message),
|
|
347
|
+
sha: String(args.sha),
|
|
348
|
+
...(args.branch ? { branch: String(args.branch) } : {}),
|
|
349
|
+
});
|
|
350
|
+
return {
|
|
351
|
+
ok: true,
|
|
352
|
+
commit: result?.commit ? { sha: result.commit.sha, message: text(result.commit.message, 500) } : null,
|
|
353
|
+
};
|
|
354
|
+
},
|
|
355
|
+
}),
|
|
356
|
+
|
|
357
|
+
defineTool({
|
|
358
|
+
name: 'github_compare_commits',
|
|
359
|
+
description:
|
|
360
|
+
'Bandingkan dua commit/branch (base...head) dan tampilkan status ahead/behind serta daftar file yang berubah.',
|
|
361
|
+
risk: 'read',
|
|
362
|
+
schema: {
|
|
363
|
+
type: 'object',
|
|
364
|
+
properties: {
|
|
365
|
+
...REPO_PROPS,
|
|
366
|
+
base: { type: 'string', description: 'Branch/tag/SHA awal.' },
|
|
367
|
+
head: { type: 'string', description: 'Branch/tag/SHA akhir.' },
|
|
368
|
+
},
|
|
369
|
+
required: ['owner', 'repo', 'base', 'head'],
|
|
370
|
+
},
|
|
371
|
+
async handler(args) {
|
|
372
|
+
const api = await plugin.requireClient();
|
|
373
|
+
const ref = `${refSegment(args.base, 'base')}...${refSegment(args.head, 'head')}`;
|
|
374
|
+
const result = await api.get(`${repoBase(args)}/compare/${ref}`);
|
|
375
|
+
return {
|
|
376
|
+
ok: true,
|
|
377
|
+
status: result.status,
|
|
378
|
+
ahead_by: result.ahead_by,
|
|
379
|
+
behind_by: result.behind_by,
|
|
380
|
+
total_commits: result.total_commits,
|
|
381
|
+
commits: limitItems(result.commits || [], 50).map(pickCommit),
|
|
382
|
+
files: limitItems(result.files || [], 100).map((file) => ({
|
|
383
|
+
filename: file.filename,
|
|
384
|
+
status: file.status,
|
|
385
|
+
additions: file.additions,
|
|
386
|
+
deletions: file.deletions,
|
|
387
|
+
})),
|
|
388
|
+
};
|
|
389
|
+
},
|
|
390
|
+
}),
|
|
391
|
+
];
|
|
392
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { createAccountTools } from './tools-account.js';
|
|
2
|
+
import { createRepoTools } from './tools-repos.js';
|
|
3
|
+
import { createIssueTools } from './tools-issues.js';
|
|
4
|
+
import { createPullTools } from './tools-pulls.js';
|
|
5
|
+
import { createReleaseTools } from './tools-releases.js';
|
|
6
|
+
import { createActionTools } from './tools-actions.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Seluruh tool AI plugin GitHub.
|
|
10
|
+
*
|
|
11
|
+
* Hanya tool yang benar-benar diimplementasikan yang didaftarkan. Setiap tool
|
|
12
|
+
* memakai kontrak registry Ryuu (name, description, schema, risk, handler),
|
|
13
|
+
* sehingga approval, validasi schema, dan rendering berjalan tanpa jalur
|
|
14
|
+
* khusus.
|
|
15
|
+
*/
|
|
16
|
+
export function createGitHubTools(plugin) {
|
|
17
|
+
return [
|
|
18
|
+
...createAccountTools(plugin),
|
|
19
|
+
...createRepoTools(plugin),
|
|
20
|
+
...createIssueTools(plugin),
|
|
21
|
+
...createPullTools(plugin),
|
|
22
|
+
...createReleaseTools(plugin),
|
|
23
|
+
...createActionTools(plugin),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Helper autentikasi sisi CLI — TANPA kredensial.
|
|
5
|
+
*
|
|
6
|
+
* Semua langkah OAuth (authorization URL, PKCE verifier, penukaran code,
|
|
7
|
+
* refresh token, revoke) dikerjakan backend `website/api`, yang satu-satunya
|
|
8
|
+
* memegang Client ID & Client Secret. Modul ini hanya menyisakan hal yang
|
|
9
|
+
* memang milik CLI: membuka browser dan menerjemahkan pesan kegagalan.
|
|
10
|
+
*
|
|
11
|
+
* Tidak ada loopback callback server di sini: redirect Google menuju route
|
|
12
|
+
* publik backend (`/admin/tools/drive-callback`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Best-effort browser launch; returns false when no opener is available. */
|
|
16
|
+
export function openBrowser(url) {
|
|
17
|
+
const target = String(url);
|
|
18
|
+
const attempts =
|
|
19
|
+
process.platform === 'darwin'
|
|
20
|
+
? [['open', [target]]]
|
|
21
|
+
: process.platform === 'win32'
|
|
22
|
+
? [['cmd', ['/c', 'start', '', target]]]
|
|
23
|
+
: [['xdg-open', [target]], ['sensible-browser', [target]], ['gio', ['open', target]]];
|
|
24
|
+
return attempts.some(([command, args]) => {
|
|
25
|
+
try {
|
|
26
|
+
const child = spawn(command, args, { stdio: 'ignore', detached: true });
|
|
27
|
+
child.on('error', () => {});
|
|
28
|
+
child.unref();
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Turn a Google/network failure into an Indonesian hint. The message never
|
|
38
|
+
* contains a token: only the OAuth error code and a plain-language reason.
|
|
39
|
+
*/
|
|
40
|
+
export function describeOAuthFailure(error) {
|
|
41
|
+
const data = error?.response?.data || {};
|
|
42
|
+
const reason = String(data.error || error?.code || 'unknown');
|
|
43
|
+
const detail = String(data.error_description || error?.message || '').slice(0, 200);
|
|
44
|
+
if (reason === 'invalid_grant' || /invalid_grant/.test(detail)) {
|
|
45
|
+
return 'refresh token sudah tidak berlaku. Jalankan /drive login untuk menghubungkan ulang.';
|
|
46
|
+
}
|
|
47
|
+
if (reason === 'invalid_client' || /invalid_client/.test(detail)) {
|
|
48
|
+
return 'backend OAuth menolak kredensial Google. Hubungi admin layanan API.';
|
|
49
|
+
}
|
|
50
|
+
if (reason === 'access_denied') return 'akses ditolak oleh pengguna di halaman persetujuan Google.';
|
|
51
|
+
if (reason === 'invalid_request') return `permintaan OAuth tidak valid${detail ? ` (${detail})` : ''}.`;
|
|
52
|
+
if (/backend OAuth/i.test(detail)) return detail;
|
|
53
|
+
if (error?.code === 'ENOTFOUND' || error?.code === 'ECONNREFUSED' || error?.code === 'ETIMEDOUT') {
|
|
54
|
+
return 'tidak dapat menghubungi backend OAuth (masalah jaringan).';
|
|
55
|
+
}
|
|
56
|
+
return detail || reason;
|
|
57
|
+
}
|