@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,613 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
|
|
4
|
+
import { RyuuError } from '../../src/utils/errors.js';
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_GITHUB_CONFIG,
|
|
7
|
+
describeGitHubConfig,
|
|
8
|
+
githubPaths,
|
|
9
|
+
readGitHubConfig,
|
|
10
|
+
readGitHubState,
|
|
11
|
+
updateGitHubConfig,
|
|
12
|
+
validateConfigEntry,
|
|
13
|
+
writeGitHubConfig,
|
|
14
|
+
writeGitHubState,
|
|
15
|
+
} from './config.js';
|
|
16
|
+
import { clearCredentials, describeCredentials, loadCredentials, saveCredentials } from './credentials.js';
|
|
17
|
+
import { GitHubBackendClient } from './backend.js';
|
|
18
|
+
import { GitHubApi } from './api.js';
|
|
19
|
+
import { GitHubPluginError, GITHUB_CODES } from './errors.js';
|
|
20
|
+
import { describeLoginFailure, openBrowser } from './auth.js';
|
|
21
|
+
import { createGitHubTools } from './tools.js';
|
|
22
|
+
|
|
23
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Lifecycle plugin GitHub.
|
|
27
|
+
*
|
|
28
|
+
* Status yang dihormati sama seperti plugin Google Drive:
|
|
29
|
+
* enabled + authenticated → tool terdaftar dan bisa dipakai
|
|
30
|
+
* disabled + authenticated → tool tidak diberikan ke model
|
|
31
|
+
* enabled + unauthenticated → tool tidak diberikan, /github login disarankan
|
|
32
|
+
*
|
|
33
|
+
* Setiap handler tool memanggil `requireClient()`, yang memeriksa ulang status
|
|
34
|
+
* saat eksekusi. Model yang memegang daftar tool lama tidak bisa bertindak
|
|
35
|
+
* setelah `/github disable`.
|
|
36
|
+
*/
|
|
37
|
+
export class GitHubPlugin {
|
|
38
|
+
constructor({
|
|
39
|
+
logger = null,
|
|
40
|
+
workspace = null,
|
|
41
|
+
config = null,
|
|
42
|
+
paths = null,
|
|
43
|
+
backendUrl = undefined,
|
|
44
|
+
fetchImpl = null,
|
|
45
|
+
now = () => Date.now(),
|
|
46
|
+
pollIntervalMs = 2000,
|
|
47
|
+
sessionTimeoutMs = 300000,
|
|
48
|
+
} = {}) {
|
|
49
|
+
this.name = 'github';
|
|
50
|
+
this.version = '1.0.0';
|
|
51
|
+
this.command = 'github';
|
|
52
|
+
this.logger = logger;
|
|
53
|
+
this.workspace = workspace;
|
|
54
|
+
this.appConfig = config;
|
|
55
|
+
this.paths = paths || githubPaths();
|
|
56
|
+
this.now = now;
|
|
57
|
+
this.pollIntervalMs = pollIntervalMs;
|
|
58
|
+
this.sessionTimeoutMs = sessionTimeoutMs;
|
|
59
|
+
this.settings = null;
|
|
60
|
+
this.settingsMtime = null;
|
|
61
|
+
this.stored = null;
|
|
62
|
+
this.loggingIn = false;
|
|
63
|
+
this.lastCheck = null;
|
|
64
|
+
this.lastCheckOk = null;
|
|
65
|
+
|
|
66
|
+
this.backend = new GitHubBackendClient({
|
|
67
|
+
...(backendUrl ? { baseUrl: backendUrl } : {}),
|
|
68
|
+
getToken: () => this.stored?.cliToken || null,
|
|
69
|
+
fetchImpl,
|
|
70
|
+
logger,
|
|
71
|
+
});
|
|
72
|
+
this.api = new GitHubApi({ backend: this.backend, logger });
|
|
73
|
+
this.tools = createGitHubTools(this);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get pathsInfo() {
|
|
77
|
+
return { ...this.paths };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Konfigurasi di-cache dengan invalidasi mtime agar perubahan luar terlihat. */
|
|
81
|
+
async loadConfig({ force = false } = {}) {
|
|
82
|
+
const mtime = await fsp
|
|
83
|
+
.stat(this.paths.configFile)
|
|
84
|
+
.then((stat) => stat.mtimeMs)
|
|
85
|
+
.catch(() => null);
|
|
86
|
+
if (force || !this.settings || mtime !== this.settingsMtime) {
|
|
87
|
+
this.settings = await readGitHubConfig({ paths: this.paths });
|
|
88
|
+
this.settingsMtime = mtime;
|
|
89
|
+
}
|
|
90
|
+
return this.settings;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async loadCredentials({ force = false } = {}) {
|
|
94
|
+
if (!this.stored || force) this.stored = await loadCredentials({ paths: this.paths });
|
|
95
|
+
return this.stored;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Metadata kredensial; tidak pernah memuat cliToken. */
|
|
99
|
+
async authInfo() {
|
|
100
|
+
const stored = await this.loadCredentials();
|
|
101
|
+
return describeCredentials(stored);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async isAuthenticated() {
|
|
105
|
+
const info = await this.authInfo();
|
|
106
|
+
if (!info.connected) return false;
|
|
107
|
+
if (info.cliTokenExpiresAt && Date.parse(info.cliTokenExpiresAt) <= this.now()) return false;
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async isEnabled() {
|
|
112
|
+
const settings = await this.loadConfig();
|
|
113
|
+
return Boolean(settings.enabled);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Guard setiap tool dan operasi yang membutuhkan GitHub. */
|
|
117
|
+
async requireClient() {
|
|
118
|
+
const settings = await this.loadConfig();
|
|
119
|
+
if (!settings.enabled) {
|
|
120
|
+
throw new GitHubPluginError(
|
|
121
|
+
'Integrasi GitHub sedang nonaktif. Jalankan /github enable untuk mengaktifkannya.',
|
|
122
|
+
{ code: GITHUB_CODES.DISABLED },
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const info = await this.authInfo();
|
|
126
|
+
if (!info.connected) {
|
|
127
|
+
throw new GitHubPluginError('Belum ada akun GitHub yang terhubung. Jalankan /github login.', {
|
|
128
|
+
code: GITHUB_CODES.UNAUTHORIZED,
|
|
129
|
+
status: 401,
|
|
130
|
+
reconnect: true,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (info.cliTokenExpiresAt && Date.parse(info.cliTokenExpiresAt) <= this.now()) {
|
|
134
|
+
throw new GitHubPluginError('Sesi CLI GitHub sudah kedaluwarsa. Jalankan /github login lagi.', {
|
|
135
|
+
code: GITHUB_CODES.UNAUTHORIZED,
|
|
136
|
+
status: 401,
|
|
137
|
+
reconnect: true,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return this.api;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async saveState(patch) {
|
|
144
|
+
const current = await readGitHubState({ paths: this.paths });
|
|
145
|
+
return writeGitHubState({ ...current, ...patch }, { paths: this.paths });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ------------------------------------------------------------------
|
|
149
|
+
// Login
|
|
150
|
+
// ------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
/** Login browser (utama) atau device flow (fallback headless). */
|
|
153
|
+
async login({
|
|
154
|
+
print = () => {},
|
|
155
|
+
showCode = null,
|
|
156
|
+
confirmReplace = null,
|
|
157
|
+
openBrowser: shouldOpen = true,
|
|
158
|
+
device = false,
|
|
159
|
+
timeoutMs = null,
|
|
160
|
+
} = {}) {
|
|
161
|
+
if (this.loggingIn) {
|
|
162
|
+
throw new GitHubPluginError('Login GitHub sedang berjalan.', { code: GITHUB_CODES.BUSY });
|
|
163
|
+
}
|
|
164
|
+
const existing = await this.authInfo().catch(() => ({ connected: false }));
|
|
165
|
+
if (existing.connected && existing.login && confirmReplace) {
|
|
166
|
+
const replace = await confirmReplace(
|
|
167
|
+
`Akun GitHub @${existing.login} sudah terhubung. Ganti dengan akun lain? [y/N] `,
|
|
168
|
+
);
|
|
169
|
+
if (!replace) {
|
|
170
|
+
print('Login dibatalkan. Kredensial yang ada tidak diubah.');
|
|
171
|
+
return { connected: false, cancelled: true };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.loggingIn = true;
|
|
176
|
+
try {
|
|
177
|
+
const limit = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : this.sessionTimeoutMs;
|
|
178
|
+
if (device) return await this.#loginDevice({ print, timeoutMs: limit });
|
|
179
|
+
return await this.#loginBrowser({ print, showCode, shouldOpen, timeoutMs: limit });
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (error instanceof GitHubPluginError) throw error;
|
|
182
|
+
throw new GitHubPluginError(describeLoginFailure(error), {
|
|
183
|
+
code: error.code || GITHUB_CODES.SESSION,
|
|
184
|
+
status: error.status || 0,
|
|
185
|
+
retryable: Boolean(error.retryable),
|
|
186
|
+
});
|
|
187
|
+
} finally {
|
|
188
|
+
this.loggingIn = false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async #loginBrowser({ print, showCode, shouldOpen, timeoutMs }) {
|
|
193
|
+
const session = await this.backend.startSession({ label: this.#deviceLabel() });
|
|
194
|
+
let launched = false;
|
|
195
|
+
if (shouldOpen) launched = openBrowser(session.url);
|
|
196
|
+
|
|
197
|
+
const emitCode = typeof showCode === 'function' ? showCode : (block) => print(`\n${block.text}\n`);
|
|
198
|
+
print(
|
|
199
|
+
[
|
|
200
|
+
'GitHub Authentication',
|
|
201
|
+
'',
|
|
202
|
+
launched
|
|
203
|
+
? 'Halaman otorisasi GitHub dibuka di browser default.'
|
|
204
|
+
: 'Browser tidak tersedia di mesin ini — buka tautan di bawah secara manual.',
|
|
205
|
+
'Setujui akses untuk akun GitHub yang ingin dihubungkan.',
|
|
206
|
+
].join('\n'),
|
|
207
|
+
);
|
|
208
|
+
emitCode({ label: 'login url', text: session.url });
|
|
209
|
+
print(
|
|
210
|
+
[
|
|
211
|
+
`Callback : ${session.redirectUri || '(dikelola backend)'}`,
|
|
212
|
+
`Kedaluwarsa: ${session.expiresAt || '(10 menit)'}`,
|
|
213
|
+
'Client Secret dan token GitHub tidak pernah disimpan di CLI.',
|
|
214
|
+
'',
|
|
215
|
+
'Menunggu persetujuan pengguna…',
|
|
216
|
+
].join('\n'),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const result = await this.#waitForSession({
|
|
220
|
+
sessionId: session.sessionId,
|
|
221
|
+
pairingSecret: session.pairingSecret,
|
|
222
|
+
timeoutMs,
|
|
223
|
+
});
|
|
224
|
+
return this.#completeLogin(result, { print });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async #loginDevice({ print, timeoutMs }) {
|
|
228
|
+
const session = await this.backend.startDevice();
|
|
229
|
+
print(
|
|
230
|
+
[
|
|
231
|
+
'GitHub Device Login',
|
|
232
|
+
'',
|
|
233
|
+
`1. Buka ${session.verificationUri}`,
|
|
234
|
+
`2. Masukkan kode: ${session.userCode}`,
|
|
235
|
+
'3. Setujui akses untuk Ryuu CLI.',
|
|
236
|
+
'',
|
|
237
|
+
'Menunggu persetujuan pengguna…',
|
|
238
|
+
].join('\n'),
|
|
239
|
+
);
|
|
240
|
+
const deadline = this.now() + timeoutMs;
|
|
241
|
+
let interval = Math.max(5, Number(session.interval) || 5);
|
|
242
|
+
while (this.now() < deadline) {
|
|
243
|
+
const result = await this.backend.pollDevice({
|
|
244
|
+
sessionId: session.sessionId,
|
|
245
|
+
pairingSecret: session.pairingSecret,
|
|
246
|
+
});
|
|
247
|
+
if (result.status === 'authorized') return this.#completeLogin(result, { print });
|
|
248
|
+
if (result.status === 'error') {
|
|
249
|
+
throw new GitHubPluginError(result.message || 'Device flow gagal.', {
|
|
250
|
+
code: GITHUB_CODES.SESSION,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (Number.isFinite(result.interval)) interval = Math.max(5, Number(result.interval));
|
|
254
|
+
await sleep(interval * 1000);
|
|
255
|
+
}
|
|
256
|
+
throw new GitHubPluginError('Waktu login device flow habis. Jalankan /github login --device lagi.', {
|
|
257
|
+
code: GITHUB_CODES.LOGIN_TIMEOUT,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async #waitForSession({ sessionId, pairingSecret, timeoutMs }) {
|
|
262
|
+
const deadline = this.now() + timeoutMs;
|
|
263
|
+
while (this.now() < deadline) {
|
|
264
|
+
let result = null;
|
|
265
|
+
try {
|
|
266
|
+
result = await this.backend.pollSession({ sessionId, pairingSecret });
|
|
267
|
+
} catch (error) {
|
|
268
|
+
// Gangguan jaringan sesaat tidak membatalkan login; sesi tetap valid
|
|
269
|
+
// sampai masa berlakunya habis.
|
|
270
|
+
if (!error.retryable) throw error;
|
|
271
|
+
}
|
|
272
|
+
if (result?.status === 'authorized') return result;
|
|
273
|
+
if (result?.status === 'error') {
|
|
274
|
+
throw new GitHubPluginError(result.message || 'GitHub menolak otorisasi.', {
|
|
275
|
+
code: GITHUB_CODES.SESSION,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
await sleep(this.pollIntervalMs);
|
|
279
|
+
}
|
|
280
|
+
const minutes = Math.max(1, Math.round(timeoutMs / 60000));
|
|
281
|
+
throw new GitHubPluginError(`Waktu login habis (${minutes} menit). Jalankan /github login lagi.`, {
|
|
282
|
+
code: GITHUB_CODES.LOGIN_TIMEOUT,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async #completeLogin(result, { print }) {
|
|
287
|
+
if (!result?.cliToken) {
|
|
288
|
+
throw new GitHubPluginError('Backend tidak mengembalikan token akses CLI.', {
|
|
289
|
+
code: GITHUB_CODES.SESSION,
|
|
290
|
+
status: 502,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const settings = await this.loadConfig();
|
|
294
|
+
const account = result.account || {};
|
|
295
|
+
await saveCredentials(
|
|
296
|
+
{
|
|
297
|
+
cliToken: result.cliToken,
|
|
298
|
+
ryuuUserId: result.ryuuUserId || null,
|
|
299
|
+
cliTokenExpiresAt: result.cliTokenExpiresAt || null,
|
|
300
|
+
account,
|
|
301
|
+
backend: settings.backend,
|
|
302
|
+
},
|
|
303
|
+
{ paths: this.paths },
|
|
304
|
+
);
|
|
305
|
+
this.stored = null;
|
|
306
|
+
await this.loadCredentials({ force: true });
|
|
307
|
+
if (!settings.enabled) {
|
|
308
|
+
await updateGitHubConfig('enabled', true, { paths: this.paths });
|
|
309
|
+
this.settings = null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
let backendOk = true;
|
|
313
|
+
let backendError = null;
|
|
314
|
+
try {
|
|
315
|
+
await this.backend.status();
|
|
316
|
+
} catch (error) {
|
|
317
|
+
backendOk = false;
|
|
318
|
+
backendError = error.message;
|
|
319
|
+
}
|
|
320
|
+
this.lastCheck = new Date(this.now()).toISOString();
|
|
321
|
+
this.lastCheckOk = backendOk;
|
|
322
|
+
await this.saveState({
|
|
323
|
+
lastCheck: this.lastCheck,
|
|
324
|
+
backendOk,
|
|
325
|
+
...(account.login ? { lastLogin: account.login } : {}),
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
print(
|
|
329
|
+
[
|
|
330
|
+
'[✓] Autentikasi berhasil!',
|
|
331
|
+
'',
|
|
332
|
+
`Akun : @${account.login || '(tidak terbaca)'}`,
|
|
333
|
+
account.name ? `Nama : ${account.name}` : null,
|
|
334
|
+
account.type ? `Tipe akun : ${account.type}` : null,
|
|
335
|
+
account.scope ? `Scope : ${account.scope}` : null,
|
|
336
|
+
`Instalasi : ${Array.isArray(account.installations) ? account.installations.length : 0} terlihat`,
|
|
337
|
+
`Verifikasi : ${backendOk ? 'backend merespons' : `gagal (${backendError})`}`,
|
|
338
|
+
'',
|
|
339
|
+
`GitHub aktif. ${this.tools.length} tool tersedia untuk AI Agent.`,
|
|
340
|
+
]
|
|
341
|
+
.filter((line) => line !== null)
|
|
342
|
+
.join('\n'),
|
|
343
|
+
);
|
|
344
|
+
return { connected: true, account, enabled: true };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
#deviceLabel() {
|
|
348
|
+
const host = (() => {
|
|
349
|
+
try {
|
|
350
|
+
return os.hostname();
|
|
351
|
+
} catch {
|
|
352
|
+
return 'cli';
|
|
353
|
+
}
|
|
354
|
+
})();
|
|
355
|
+
return `ryuu-cli@${host}`.slice(0, 60);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ------------------------------------------------------------------
|
|
359
|
+
// Logout & lifecycle
|
|
360
|
+
// ------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
async logout({ print = () => {} } = {}) {
|
|
363
|
+
const stored = await loadCredentials({ paths: this.paths }).catch(() => null);
|
|
364
|
+
if (!stored?.cliToken) {
|
|
365
|
+
await clearCredentials({ paths: this.paths }).catch(() => {});
|
|
366
|
+
this.stored = null;
|
|
367
|
+
print('Belum ada akun GitHub yang terhubung di mesin ini.');
|
|
368
|
+
return { connected: false, remote: false };
|
|
369
|
+
}
|
|
370
|
+
let remote = null;
|
|
371
|
+
let remoteError = null;
|
|
372
|
+
try {
|
|
373
|
+
remote = await this.backend.disconnect();
|
|
374
|
+
} catch (error) {
|
|
375
|
+
remoteError = error.message;
|
|
376
|
+
}
|
|
377
|
+
await clearCredentials({ paths: this.paths });
|
|
378
|
+
this.stored = null;
|
|
379
|
+
const settings = await this.loadConfig();
|
|
380
|
+
if (settings.enabled) {
|
|
381
|
+
await updateGitHubConfig('enabled', false, { paths: this.paths });
|
|
382
|
+
this.settings = null;
|
|
383
|
+
}
|
|
384
|
+
print('Koneksi GitHub diputus di mesin ini. Kredensial lokal dihapus.');
|
|
385
|
+
print(
|
|
386
|
+
remote?.revoked
|
|
387
|
+
? 'Revocation token di GitHub: berhasil.'
|
|
388
|
+
: remote
|
|
389
|
+
? `Revocation token di GitHub: dilewati (${remote.reason || 'tidak ada token'}).`
|
|
390
|
+
: `Backend tidak dapat dihubungi (${remoteError}); token di server mungkin masih tersimpan.`,
|
|
391
|
+
);
|
|
392
|
+
print('Repository, issue, dan release di GitHub tidak diubah.');
|
|
393
|
+
return { connected: false, remote: Boolean(remote?.disconnected), revoked: Boolean(remote?.revoked) };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async enable() {
|
|
397
|
+
if (!(await this.isAuthenticated())) {
|
|
398
|
+
throw new GitHubPluginError('Belum ada akun GitHub yang terhubung. Jalankan /github login lebih dulu.', {
|
|
399
|
+
code: GITHUB_CODES.UNAUTHORIZED,
|
|
400
|
+
status: 401,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
const settings = await this.loadConfig();
|
|
404
|
+
if (!settings.clientId) {
|
|
405
|
+
throw new GitHubPluginError('Client ID GitHub tidak tersedia pada konfigurasi plugin.', {
|
|
406
|
+
code: GITHUB_CODES.CONFIG,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
await updateGitHubConfig('enabled', true, { paths: this.paths });
|
|
410
|
+
this.settings = null;
|
|
411
|
+
return { enabled: true, tools: this.tools.map((tool) => tool.name) };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async disable() {
|
|
415
|
+
await updateGitHubConfig('enabled', false, { paths: this.paths });
|
|
416
|
+
this.settings = null;
|
|
417
|
+
return { enabled: false };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
async setConfig(key, value) {
|
|
421
|
+
const parsed = validateConfigEntry(key, value);
|
|
422
|
+
await updateGitHubConfig(key, parsed, { paths: this.paths });
|
|
423
|
+
this.settings = null;
|
|
424
|
+
return parsed;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async configView() {
|
|
428
|
+
const settings = await this.loadConfig({ force: true });
|
|
429
|
+
return { settings, lines: describeGitHubConfig(settings) };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ------------------------------------------------------------------
|
|
433
|
+
// Status, akun, test
|
|
434
|
+
// ------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
/** Status lengkap tanpa token; kegagalan backend dilaporkan, tidak dilempar. */
|
|
437
|
+
async status() {
|
|
438
|
+
let settings = null;
|
|
439
|
+
let configError = null;
|
|
440
|
+
try {
|
|
441
|
+
settings = await this.loadConfig();
|
|
442
|
+
} catch (error) {
|
|
443
|
+
configError = error.message;
|
|
444
|
+
}
|
|
445
|
+
let auth = { connected: false };
|
|
446
|
+
let authError = null;
|
|
447
|
+
try {
|
|
448
|
+
auth = await this.authInfo();
|
|
449
|
+
} catch (error) {
|
|
450
|
+
authError = error.message;
|
|
451
|
+
}
|
|
452
|
+
const sessionExpired = Boolean(
|
|
453
|
+
auth.cliTokenExpiresAt && Date.parse(auth.cliTokenExpiresAt) <= this.now(),
|
|
454
|
+
);
|
|
455
|
+
const authenticated = Boolean(auth.connected) && !sessionExpired;
|
|
456
|
+
let remote = null;
|
|
457
|
+
let remoteError = null;
|
|
458
|
+
if (authenticated) {
|
|
459
|
+
try {
|
|
460
|
+
remote = await this.backend.status();
|
|
461
|
+
this.lastCheck = new Date(this.now()).toISOString();
|
|
462
|
+
this.lastCheckOk = true;
|
|
463
|
+
await this.saveState({ lastCheck: this.lastCheck, backendOk: true });
|
|
464
|
+
} catch (error) {
|
|
465
|
+
remoteError = error.message;
|
|
466
|
+
this.lastCheck = new Date(this.now()).toISOString();
|
|
467
|
+
this.lastCheckOk = false;
|
|
468
|
+
await this.saveState({ lastCheck: this.lastCheck, backendOk: false });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
enabled: Boolean(settings?.enabled),
|
|
473
|
+
authenticated,
|
|
474
|
+
sessionExpired,
|
|
475
|
+
auth,
|
|
476
|
+
authError,
|
|
477
|
+
configError,
|
|
478
|
+
remote,
|
|
479
|
+
remoteError,
|
|
480
|
+
toolCount: this.tools.length,
|
|
481
|
+
toolNames: this.tools.map((tool) => tool.name),
|
|
482
|
+
settings,
|
|
483
|
+
lastCheck: this.lastCheck,
|
|
484
|
+
lastCheckOk: this.lastCheckOk,
|
|
485
|
+
paths: this.paths,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async account() {
|
|
490
|
+
if (!(await this.isAuthenticated())) {
|
|
491
|
+
throw new GitHubPluginError('Belum ada akun GitHub yang terhubung. Jalankan /github login.', {
|
|
492
|
+
code: GITHUB_CODES.UNAUTHORIZED,
|
|
493
|
+
status: 401,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
const account = await this.backend.account();
|
|
497
|
+
if (account?.login) {
|
|
498
|
+
const stored = await this.loadCredentials();
|
|
499
|
+
if (stored?.cliToken) {
|
|
500
|
+
await saveCredentials(
|
|
501
|
+
{
|
|
502
|
+
cliToken: stored.cliToken,
|
|
503
|
+
ryuuUserId: stored.ryuuUserId,
|
|
504
|
+
cliTokenExpiresAt: stored.cliTokenExpiresAt,
|
|
505
|
+
account: { ...stored.account, ...account },
|
|
506
|
+
backend: stored.backend,
|
|
507
|
+
},
|
|
508
|
+
{ paths: this.paths },
|
|
509
|
+
);
|
|
510
|
+
this.stored = null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return account;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Diagnostik aman: konfigurasi, sesi, backend, GET /user, instalasi, token.
|
|
518
|
+
* Tidak membuat, mengubah, atau menghapus repository.
|
|
519
|
+
*/
|
|
520
|
+
async test() {
|
|
521
|
+
const report = [];
|
|
522
|
+
const push = (label, ok, detail = '') => report.push({ label, ok, detail });
|
|
523
|
+
|
|
524
|
+
let settings;
|
|
525
|
+
try {
|
|
526
|
+
settings = await this.loadConfig({ force: true });
|
|
527
|
+
push('Konfigurasi plugin', true, this.paths.configFile);
|
|
528
|
+
} catch (error) {
|
|
529
|
+
push('Konfigurasi plugin', false, error.message);
|
|
530
|
+
return { ok: false, report };
|
|
531
|
+
}
|
|
532
|
+
push('Client ID', Boolean(settings.clientId), settings.clientId || 'tidak tersedia');
|
|
533
|
+
push('Backend', Boolean(settings.backend), settings.backend);
|
|
534
|
+
push(
|
|
535
|
+
'Rahasia di CLI',
|
|
536
|
+
true,
|
|
537
|
+
'client secret/private key/access token tidak disimpan di CLI',
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
const info = await this.authInfo().catch((error) => ({ connected: false, error: error.message }));
|
|
541
|
+
if (!info.connected) {
|
|
542
|
+
push('Sesi CLI', false, 'belum login. Jalankan /github login.');
|
|
543
|
+
return { ok: false, report };
|
|
544
|
+
}
|
|
545
|
+
push('Sesi CLI', true, `akun @${info.login || '(tanpa login)'}`);
|
|
546
|
+
if (info.cliTokenExpiresAt) {
|
|
547
|
+
const expired = Date.parse(info.cliTokenExpiresAt) <= this.now();
|
|
548
|
+
push('Masa berlaku sesi', !expired, `${info.cliTokenExpiresAt}${expired ? ' (kedaluwarsa)' : ''}`);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
let remote = null;
|
|
552
|
+
try {
|
|
553
|
+
remote = await this.backend.status();
|
|
554
|
+
push('Respon backend', true, `terhubung${remote?.backend?.apiVersion ? ` (API ${remote.backend.apiVersion})` : ''}`);
|
|
555
|
+
} catch (error) {
|
|
556
|
+
push('Respon backend', false, error.message);
|
|
557
|
+
}
|
|
558
|
+
push('Status integrasi', settings.enabled, settings.enabled ? 'enabled' : 'disabled');
|
|
559
|
+
|
|
560
|
+
if (remote?.token) {
|
|
561
|
+
push(
|
|
562
|
+
'Access token',
|
|
563
|
+
!remote.token.expired,
|
|
564
|
+
remote.token.accessExpiresAt
|
|
565
|
+
? `berlaku sampai ${remote.token.accessExpiresAt}`
|
|
566
|
+
: 'tanpa masa berlaku (expiration off)',
|
|
567
|
+
);
|
|
568
|
+
push(
|
|
569
|
+
'Refresh token',
|
|
570
|
+
Boolean(remote.token.hasRefreshToken),
|
|
571
|
+
remote.token.hasRefreshToken ? 'tersedia di backend' : 'tidak tersedia',
|
|
572
|
+
);
|
|
573
|
+
push(
|
|
574
|
+
'Instalasi',
|
|
575
|
+
Array.isArray(remote.installations) && remote.installations.length > 0,
|
|
576
|
+
`${Array.isArray(remote.installations) ? remote.installations.length : 0} instalasi terlihat`,
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
const tested = await this.backend.test();
|
|
582
|
+
for (const row of tested?.report || []) push(`GitHub: ${row.label}`, row.ok, row.detail || '');
|
|
583
|
+
} catch (error) {
|
|
584
|
+
push('GitHub API', false, error.message);
|
|
585
|
+
}
|
|
586
|
+
push('Tool terdaftar', true, `${this.tools.length} tool didefinisikan`);
|
|
587
|
+
|
|
588
|
+
this.lastCheck = new Date(this.now()).toISOString();
|
|
589
|
+
this.lastCheckOk = report.every((row) => row.ok);
|
|
590
|
+
await this.saveState({ lastCheck: this.lastCheck, backendOk: this.lastCheckOk });
|
|
591
|
+
return { ok: this.lastCheckOk, report, account: remote?.github || null };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async refresh() {
|
|
595
|
+
const result = await this.backend.refresh();
|
|
596
|
+
this.lastCheck = new Date(this.now()).toISOString();
|
|
597
|
+
this.lastCheckOk = true;
|
|
598
|
+
await this.saveState({ lastCheck: this.lastCheck, backendOk: true });
|
|
599
|
+
return result;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Reset konfigurasi ke default (dipakai test). */
|
|
603
|
+
async resetConfigFile() {
|
|
604
|
+
await writeGitHubConfig({ ...DEFAULT_GITHUB_CONFIG }, { paths: this.paths });
|
|
605
|
+
this.settings = null;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export function createGitHubPlugin(options = {}) {
|
|
610
|
+
return new GitHubPlugin(options);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export { DEFAULT_GITHUB_CONFIG };
|