dsh-github-app 0.0.0-stage → 0.0.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/github.mjs ADDED
@@ -0,0 +1,659 @@
1
+ import { DEFAULT_HOSTNAME } from "./constants.mjs";
2
+ import { passphraseError } from "./passphrase.mjs";
3
+ import { decryptPrivateKey } from "./keyCrypto.mjs";
4
+ import { parseTimestamp } from "./time.mjs";
5
+ import { readSettings, writeSettings } from "./settings.mjs";
6
+ import { createPrivateKey, createSign } from "node:crypto";
7
+ import { existsSync, readFileSync } from "node:fs";
8
+ import { spawn } from "node:child_process";
9
+ // src/github.ts
10
+ function run(command, args, options = {}) {
11
+ return new Promise((resolve) => {
12
+ const env = {
13
+ ...process.env,
14
+ ...options.env || {}
15
+ };
16
+ for (const key of options.unset || []) delete env[key];
17
+ const child = spawn(command, args, {
18
+ cwd: options.cwd,
19
+ env,
20
+ stdio: [
21
+ "pipe",
22
+ "pipe",
23
+ "pipe"
24
+ ],
25
+ windowsHide: true
26
+ });
27
+ let stdout = "";
28
+ let stderr = "";
29
+ child.stdout.on("data", (chunk) => {
30
+ stdout += String(chunk);
31
+ });
32
+ child.stderr.on("data", (chunk) => {
33
+ stderr += String(chunk);
34
+ });
35
+ let settled = false;
36
+ let timedOut = false;
37
+ let timer = null;
38
+ const finish = (code, error) => {
39
+ if (settled) return;
40
+ settled = true;
41
+ if (timer) clearTimeout(timer);
42
+ resolve({
43
+ code: typeof code === "number" ? code : error ? 1 : 0,
44
+ stdout,
45
+ stderr,
46
+ error: error || (timedOut ? /* @__PURE__ */ new Error("Command timed out") : code !== 0 ? new Error(stderr || stdout || "Command failed") : null)
47
+ });
48
+ };
49
+ if (options.timeout && options.timeout > 0) timer = setTimeout(() => {
50
+ timedOut = true;
51
+ try {
52
+ child.kill("SIGTERM");
53
+ } catch {}
54
+ setTimeout(() => {
55
+ try {
56
+ child.kill("SIGKILL");
57
+ } catch {}
58
+ }, 5e3);
59
+ }, options.timeout);
60
+ child.on("error", (error) => finish(1, error));
61
+ child.on("close", (code) => finish(code, null));
62
+ child.stdin.on("error", () => {});
63
+ try {
64
+ if (options.input != null) child.stdin.write(options.input);
65
+ child.stdin.end();
66
+ } catch {}
67
+ });
68
+ }
69
+ /**
70
+ * Environment variables `gh` accepts as a token source.
71
+ *
72
+ * When any of them is set, `gh auth login` and `gh auth logout` refuse to
73
+ * touch gh's own credential store ("…first clear the value from the
74
+ * environment"), and `gh auth status` reports the environment token instead
75
+ * of the stored credentials. The plugin exports the installation token into
76
+ * its own process environment for the agent, so every auth-management command
77
+ * has to run without them — otherwise the second Connect in one server
78
+ * lifetime always fails, and Disconnect never removes the stored credentials.
79
+ */
80
+ const GH_TOKEN_ENV_KEYS = [
81
+ "GH_TOKEN",
82
+ "GITHUB_TOKEN",
83
+ "GH_ENTERPRISE_TOKEN",
84
+ "GITHUB_ENTERPRISE_TOKEN"
85
+ ];
86
+ /**
87
+ * Run a `gh auth` subcommand with the token environment removed, so it reads
88
+ * and writes gh's credential store rather than the ambient token.
89
+ *
90
+ * @param args `gh` arguments, starting after the binary name.
91
+ * @param options spawn options; `unset` entries are added, never replaced.
92
+ * @returns the command result.
93
+ */
94
+ function runGhAuth(args, options = {}) {
95
+ return run("gh", args, {
96
+ ...options,
97
+ unset: [...options.unset || [], ...GH_TOKEN_ENV_KEYS]
98
+ });
99
+ }
100
+ async function getToolInfo(command) {
101
+ const result = await run(command, ["--version"]);
102
+ if (result.code !== 0) return {
103
+ installed: false,
104
+ version: null
105
+ };
106
+ const pattern = new RegExp(`${command} version (\\S+)`);
107
+ const match = result.stdout.match(pattern) || result.stderr.match(pattern);
108
+ return {
109
+ installed: true,
110
+ version: match ? match[1] : result.stdout.trim() || result.stderr.trim()
111
+ };
112
+ }
113
+ function getGhInfo() {
114
+ return getToolInfo("gh");
115
+ }
116
+ function getGitInfo() {
117
+ return getToolInfo("git");
118
+ }
119
+ const AUTH_FAILURE_TEXT = /failed to log in|not logged in(to)?|is invalid|has expired|authentication failed|bad credentials|re-?authenticate/i;
120
+ const AUTH_SUCCESS_TEXT = /logged in to|logged in as/i;
121
+ const AUTH_FAILURE_MARKER = /^\s*[✗✘×]\s+/u;
122
+ const AUTH_FAILURE_MARKER_TEXT = /^\s*X\s+(failed|not logged|invalid|expired|bad credentials)/i;
123
+ const AUTH_SUCCESS_MARKER = /^\s*[✓✔]\s+.*logged in/iu;
124
+ const AUTH_ACTIVE_ACCOUNT = /^(?:[-*]\s*)?active account:\s*true$/i;
125
+ /**
126
+ * Classify a single `gh auth status` line as `'failed'`, `'ok'`, or `null` when
127
+ * the line carries no verdict of its own (host header, `- Token:` metadata...).
128
+ *
129
+ * @param line one line of `gh auth status` output.
130
+ * @returns the verdict carried by that line, if any.
131
+ */
132
+ function classifyAuthLine(line) {
133
+ if (AUTH_FAILURE_TEXT.test(line) || AUTH_FAILURE_MARKER.test(line) || AUTH_FAILURE_MARKER_TEXT.test(line)) return "failed";
134
+ if (AUTH_SUCCESS_TEXT.test(line) || AUTH_SUCCESS_MARKER.test(line)) return "ok";
135
+ return null;
136
+ }
137
+ /**
138
+ * Decide whether `gh auth status` output describes a usable authentication.
139
+ *
140
+ * `gh` exits 0 as soon as *one* configured account is usable, so judging the
141
+ * merged output as a whole reports a false "disconnected" whenever an unusable
142
+ * entry sits next to a working one. The entry marked `Active account: true` —
143
+ * the credential `gh` and git operations actually use — decides the verdict.
144
+ * Other entries are only consulted when no such marker is present; when the
145
+ * marker is present but its status line is unrecognisable (a future `gh`
146
+ * re-wording), the verdict stays `false` instead of falling back to another
147
+ * account, so an unknown format can never claim "Connected".
148
+ *
149
+ * @param output merged stdout+stderr of `gh auth status`.
150
+ * @returns true when the active (or only) account is authenticated.
151
+ */
152
+ function isLoggedInOutput(output) {
153
+ let pendingStatus = null;
154
+ let activeStatus = null;
155
+ let activeAccountSeen = false;
156
+ let sawOk = false;
157
+ let sawFailed = false;
158
+ for (const line of String(output).split(/\r?\n/)) {
159
+ const status = classifyAuthLine(line);
160
+ if (status === "ok") {
161
+ pendingStatus = "ok";
162
+ sawOk = true;
163
+ } else if (status === "failed") {
164
+ pendingStatus = "failed";
165
+ sawFailed = true;
166
+ }
167
+ if (AUTH_ACTIVE_ACCOUNT.test(line.trim())) {
168
+ activeAccountSeen = true;
169
+ activeStatus = pendingStatus;
170
+ }
171
+ }
172
+ if (activeAccountSeen) return activeStatus === "ok";
173
+ if (sawOk) return true;
174
+ if (sawFailed) return false;
175
+ return false;
176
+ }
177
+ async function getAuthStatus(hostname = DEFAULT_HOSTNAME) {
178
+ const result = await runGhAuth([
179
+ "auth",
180
+ "status",
181
+ "--hostname",
182
+ hostname
183
+ ]);
184
+ const output = `${result.stdout}\n${result.stderr}`.trim();
185
+ const loggedIn = isLoggedInOutput(output);
186
+ const gitCfg = await run("git", [
187
+ "config",
188
+ "--global",
189
+ "--get",
190
+ `credential.https://${hostname}.helper`
191
+ ]);
192
+ return {
193
+ loggedIn,
194
+ gitConfigured: gitCfg.code === 0 && /gh auth git-credential|gh/.test(gitCfg.stdout),
195
+ hostname,
196
+ output: loggedIn ? output : void 0,
197
+ error: loggedIn ? void 0 : output || "gh auth status failed"
198
+ };
199
+ }
200
+ /**
201
+ * Package managers tried in order, per platform.
202
+ *
203
+ * `winget` is preferred on Windows and Chocolatey is the fallback; on Linux the
204
+ * order covers Debian/Ubuntu, Fedora/RHEL, Arch and Alpine/openSUSE.
205
+ */
206
+ const PACKAGE_MANAGERS = {
207
+ darwin: [{
208
+ command: "brew",
209
+ probe: ["--version"],
210
+ update: null,
211
+ install: ["install"]
212
+ }],
213
+ win32: [{
214
+ command: "winget",
215
+ probe: ["--version"],
216
+ update: null,
217
+ install: [
218
+ "install",
219
+ "--source",
220
+ "winget",
221
+ "--accept-source-agreements",
222
+ "--accept-package-agreements",
223
+ "--silent",
224
+ "--id"
225
+ ],
226
+ packageName: {
227
+ gh: "GitHub.cli",
228
+ git: "Git.Git"
229
+ }
230
+ }, {
231
+ command: "choco",
232
+ probe: ["--version"],
233
+ update: null,
234
+ install: ["install", "-y"]
235
+ }],
236
+ linux: [
237
+ {
238
+ command: "apt-get",
239
+ probe: ["--version"],
240
+ update: ["update"],
241
+ install: ["install", "-y"]
242
+ },
243
+ {
244
+ command: "dnf",
245
+ probe: ["--version"],
246
+ update: null,
247
+ install: ["install", "-y"]
248
+ },
249
+ {
250
+ command: "yum",
251
+ probe: ["--version"],
252
+ update: null,
253
+ install: ["install", "-y"]
254
+ },
255
+ {
256
+ command: "pacman",
257
+ probe: ["--version"],
258
+ update: null,
259
+ install: ["-S", "--noconfirm"],
260
+ packageName: { gh: "github-cli" }
261
+ },
262
+ {
263
+ command: "zypper",
264
+ probe: ["--version"],
265
+ update: null,
266
+ install: ["--non-interactive", "install"]
267
+ },
268
+ {
269
+ command: "apk",
270
+ probe: ["--version"],
271
+ update: null,
272
+ install: ["add", "--no-cache"],
273
+ packageName: { gh: "github-cli" }
274
+ }
275
+ ]
276
+ };
277
+ /**
278
+ * Package managers available for a platform, in probe order.
279
+ *
280
+ * @param platform target platform; defaults to the host.
281
+ * @returns the manager list, empty when the platform has no installer.
282
+ */
283
+ function packageManagers(platform = process.platform) {
284
+ return PACKAGE_MANAGERS[platform] ?? [];
285
+ }
286
+ /**
287
+ * Commands needed to install one tool with a known package manager.
288
+ *
289
+ * @param manager manager to use.
290
+ * @param tool tool to install.
291
+ * @returns ordered commands; an install failure aborts the sequence.
292
+ */
293
+ function installCommands(manager, tool) {
294
+ const steps = [];
295
+ if (manager.update !== null) steps.push({
296
+ command: manager.command,
297
+ args: manager.update
298
+ });
299
+ steps.push({
300
+ command: manager.command,
301
+ args: [...manager.install, manager.packageName?.[tool] ?? tool]
302
+ });
303
+ return steps;
304
+ }
305
+ /**
306
+ * Probe the platform's managers in order and return the first usable one.
307
+ *
308
+ * @param platform target platform.
309
+ * @returns the manager, or null when none answers.
310
+ */
311
+ async function findPackageManager(platform = process.platform) {
312
+ for (const manager of packageManagers(platform)) if ((await run(manager.command, manager.probe, { timeout: 15e3 })).code === 0) return manager;
313
+ return null;
314
+ }
315
+ /**
316
+ * Human-readable fallback hint for a platform without an automatic installer.
317
+ *
318
+ * @param tool tool the user wanted to install.
319
+ * @returns a short instruction.
320
+ */
321
+ function installHint(tool) {
322
+ if (process.platform === "darwin") return `Install Homebrew (https://brew.sh) and retry, or download ${tool} from https://cli.github.com`;
323
+ if (process.platform === "win32") return `Install winget or Chocolatey and retry, or download ${tool} from https://cli.github.com`;
324
+ return `Install a supported package manager (apt, dnf, yum, pacman, zypper or apk), or see https://github.com/cli/cli#installation for ${tool}`;
325
+ }
326
+ /**
327
+ * Install one missing tool with the first available platform manager.
328
+ *
329
+ * @param tool `gh` or `git`.
330
+ * @returns the install outcome; `skipped` when the tool was already present.
331
+ */
332
+ async function installTool(tool) {
333
+ const before = tool === "gh" ? await getGhInfo() : await getGitInfo();
334
+ if (before.installed) return {
335
+ ok: true,
336
+ version: before.version,
337
+ skipped: true
338
+ };
339
+ const managers = packageManagers();
340
+ const manager = await findPackageManager();
341
+ if (manager === null) return {
342
+ ok: false,
343
+ error: managers.length === 0 ? `Automatic install is not supported on ${process.platform}. ${installHint(tool)}` : `No supported package manager found. ${installHint(tool)}`
344
+ };
345
+ for (const step of installCommands(manager, tool)) {
346
+ const result = await run(step.command, step.args, { timeout: 3e5 });
347
+ if (result.code !== 0) {
348
+ const output = (result.stderr || result.stdout || `${step.command} ${step.args.join(" ")} failed`).trim();
349
+ return {
350
+ ok: false,
351
+ error: `${output}\n${manager.command === "apt-get" && tool === "gh" && /unable to locate|has no installation candidate/i.test(output) ? "On Debian/Ubuntu, gh needs the GitHub CLI repository: https://github.com/cli/cli/blob/trunk/docs/install_linux.md" : installHint(tool)}`
352
+ };
353
+ }
354
+ }
355
+ const after = tool === "gh" ? await getGhInfo() : await getGitInfo();
356
+ return {
357
+ ok: after.installed,
358
+ version: after.version,
359
+ error: after.installed ? void 0 : `${tool} still not found after install`
360
+ };
361
+ }
362
+ async function installGit() {
363
+ return installTool("git");
364
+ }
365
+ async function installGh() {
366
+ const ghResult = await installTool("gh");
367
+ const gitResult = await installTool("git");
368
+ const ok = ghResult.ok && gitResult.ok;
369
+ return {
370
+ ok,
371
+ version: ghResult.version,
372
+ gh: ghResult,
373
+ git: gitResult,
374
+ error: ok ? void 0 : ghResult.error || gitResult.error
375
+ };
376
+ }
377
+ function base64UrlEncode(value) {
378
+ return (Buffer.isBuffer(value) ? value : Buffer.from(String(value), "utf8")).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
379
+ }
380
+ function createAppJwt(appId, privateKeyPem) {
381
+ const now = Math.floor(Date.now() / 1e3);
382
+ const header = {
383
+ alg: "RS256",
384
+ typ: "JWT"
385
+ };
386
+ const payload = {
387
+ iat: now - 60,
388
+ exp: now + 600,
389
+ iss: String(appId)
390
+ };
391
+ const signingInput = `${base64UrlEncode(JSON.stringify(header))}.${base64UrlEncode(JSON.stringify(payload))}`;
392
+ let key;
393
+ try {
394
+ key = createPrivateKey(privateKeyPem);
395
+ } catch (error) {
396
+ throw new Error(`Invalid private key: ${error instanceof Error && error.message ? error.message : error}`);
397
+ }
398
+ const signer = createSign("RSA-SHA256");
399
+ signer.update(signingInput);
400
+ return `${signingInput}.${base64UrlEncode(signer.sign(key))}`;
401
+ }
402
+ function apiBaseFor(hostname) {
403
+ return hostname === "github.com" ? "https://api.github.com" : `https://${hostname}/api/v3`;
404
+ }
405
+ async function fetchInstallationToken(jwt, installationId, hostname) {
406
+ const url = `${apiBaseFor(hostname)}/app/installations/${encodeURIComponent(String(installationId))}/access_tokens`;
407
+ let response;
408
+ try {
409
+ response = await fetch(url, {
410
+ method: "POST",
411
+ headers: {
412
+ Authorization: `Bearer ${jwt}`,
413
+ Accept: "application/vnd.github+json",
414
+ "X-GitHub-Api-Version": "2022-11-28",
415
+ "Content-Type": "application/json"
416
+ },
417
+ body: "{}"
418
+ });
419
+ } catch (error) {
420
+ return {
421
+ ok: false,
422
+ error: `Network error: ${error instanceof Error && error.message ? error.message : error}`
423
+ };
424
+ }
425
+ const text = await response.text();
426
+ if (!response.ok) return {
427
+ ok: false,
428
+ error: `GitHub API ${response.status}: ${text.trim()}`
429
+ };
430
+ let data;
431
+ try {
432
+ data = JSON.parse(text);
433
+ } catch {
434
+ return {
435
+ ok: false,
436
+ error: "Invalid JSON response from GitHub API"
437
+ };
438
+ }
439
+ if (data === null || typeof data !== "object" || !("token" in data) || typeof data.token !== "string" || data.token === "") return {
440
+ ok: false,
441
+ error: "No installation token in GitHub API response"
442
+ };
443
+ const rawExpiresAt = data.expires_at;
444
+ const expiresAt = typeof rawExpiresAt === "string" ? parseTimestamp(rawExpiresAt) : null;
445
+ return {
446
+ ok: true,
447
+ token: data.token,
448
+ expiresAt
449
+ };
450
+ }
451
+ /**
452
+ * Exchange the GitHub App private key for an installation token.
453
+ *
454
+ * The key file is encrypted at rest, so the passphrase is required on every
455
+ * call: the decrypted PEM lives only for the duration of this operation and is
456
+ * never cached in the server process.
457
+ *
458
+ * @param settings persisted App ID, installation ID and key location.
459
+ * @param passphrase passphrase protecting the key file.
460
+ * @returns the connection outcome and refreshed auth status.
461
+ */
462
+ async function connect(settings, passphrase) {
463
+ const { appId, installationId, keyPath } = settings || {};
464
+ if (!appId || !installationId || !keyPath) return {
465
+ ok: false,
466
+ error: "Missing appId, installationId or key file"
467
+ };
468
+ const invalidPassphrase = passphraseError(passphrase);
469
+ if (invalidPassphrase !== null) return {
470
+ ok: false,
471
+ error: invalidPassphrase
472
+ };
473
+ if (!existsSync(keyPath)) return {
474
+ ok: false,
475
+ error: "Private key file not found"
476
+ };
477
+ let envelope;
478
+ try {
479
+ envelope = readFileSync(keyPath, "utf8");
480
+ } catch (error) {
481
+ return {
482
+ ok: false,
483
+ error: `Cannot read private key: ${error instanceof Error && error.message ? error.message : error}`
484
+ };
485
+ }
486
+ let pem;
487
+ try {
488
+ pem = await decryptPrivateKey(envelope, passphrase);
489
+ } catch {
490
+ return {
491
+ ok: false,
492
+ error: "Cannot decrypt private key: wrong passphrase or corrupted key file."
493
+ };
494
+ }
495
+ let jwt;
496
+ try {
497
+ jwt = createAppJwt(appId, pem);
498
+ } catch (error) {
499
+ return {
500
+ ok: false,
501
+ error: error instanceof Error && error.message ? error.message : "Failed to create JWT"
502
+ };
503
+ } finally {
504
+ pem = "";
505
+ }
506
+ const tokenResult = await fetchInstallationToken(jwt, installationId, DEFAULT_HOSTNAME);
507
+ if (!tokenResult.ok) return {
508
+ ok: false,
509
+ error: tokenResult.error
510
+ };
511
+ const token = tokenResult.token;
512
+ const login = await runGhAuth([
513
+ "auth",
514
+ "login",
515
+ "--hostname",
516
+ DEFAULT_HOSTNAME,
517
+ "--with-token",
518
+ "--git-protocol",
519
+ "https"
520
+ ], {
521
+ timeout: 12e4,
522
+ input: `${token}\n`
523
+ });
524
+ if (login.code !== 0) return {
525
+ ok: false,
526
+ error: (login.stderr || login.stdout || "gh auth login failed").trim()
527
+ };
528
+ const issuedAt = Date.now();
529
+ const expiresAt = tokenResult.expiresAt ?? issuedAt + 36e5;
530
+ try {
531
+ writeSettings({
532
+ ...readSettings(),
533
+ tokenIssuedAt: new Date(issuedAt).toISOString(),
534
+ tokenExpiresAt: new Date(expiresAt).toISOString()
535
+ });
536
+ } catch {}
537
+ const setup = await runGhAuth(["auth", "setup-git"], { timeout: 12e4 });
538
+ if (setup.code !== 0) {
539
+ const msg = (setup.stderr || setup.stdout || "gh auth setup-git failed").trim();
540
+ await rollbackLogin();
541
+ return {
542
+ ok: false,
543
+ error: msg
544
+ };
545
+ }
546
+ process.env.GH_TOKEN = token;
547
+ process.env.GITHUB_TOKEN = token;
548
+ const auth = await getAuthStatus();
549
+ if (!auth.loggedIn) {
550
+ const msg = auth.error || "gh auth status did not confirm the login";
551
+ await rollbackLogin();
552
+ return {
553
+ ok: false,
554
+ error: msg
555
+ };
556
+ }
557
+ return {
558
+ ok: true,
559
+ auth
560
+ };
561
+ }
562
+ /**
563
+ * Undo a login that was written to gh's credential store when a later step of
564
+ * {@link connect} failed, so the reported state matches the store.
565
+ *
566
+ * @returns nothing; failures are best-effort.
567
+ */
568
+ async function rollbackLogin() {
569
+ await runGhAuth([
570
+ "auth",
571
+ "logout",
572
+ "--hostname",
573
+ DEFAULT_HOSTNAME
574
+ ], {
575
+ timeout: 6e4,
576
+ input: "y\n"
577
+ });
578
+ delete process.env.GH_TOKEN;
579
+ delete process.env.GITHUB_TOKEN;
580
+ try {
581
+ writeSettings({
582
+ ...readSettings(),
583
+ tokenIssuedAt: null,
584
+ tokenExpiresAt: null
585
+ });
586
+ } catch {}
587
+ }
588
+ async function disconnect() {
589
+ const result = await runGhAuth([
590
+ "auth",
591
+ "logout",
592
+ "--hostname",
593
+ DEFAULT_HOSTNAME
594
+ ], {
595
+ timeout: 6e4,
596
+ input: "y\n"
597
+ });
598
+ const output = `${result.stdout}\n${result.stderr}`.trim();
599
+ const loggedOut = result.code === 0 || /not logged in/i.test(output);
600
+ delete process.env.GH_TOKEN;
601
+ delete process.env.GITHUB_TOKEN;
602
+ try {
603
+ writeSettings({
604
+ ...readSettings(),
605
+ tokenIssuedAt: null,
606
+ tokenExpiresAt: null
607
+ });
608
+ } catch {}
609
+ return loggedOut ? { ok: true } : {
610
+ ok: false,
611
+ error: output || "gh auth logout failed"
612
+ };
613
+ }
614
+ async function getGitIdentity() {
615
+ const nameResult = await run("git", [
616
+ "config",
617
+ "--global",
618
+ "--get",
619
+ "user.name"
620
+ ]);
621
+ const emailResult = await run("git", [
622
+ "config",
623
+ "--global",
624
+ "--get",
625
+ "user.email"
626
+ ]);
627
+ return {
628
+ name: nameResult.code === 0 ? nameResult.stdout.trim() : "",
629
+ email: emailResult.code === 0 ? emailResult.stdout.trim() : ""
630
+ };
631
+ }
632
+ async function setGitIdentity(name, email) {
633
+ if (typeof name === "string" && name.trim() !== "") await run("git", [
634
+ "config",
635
+ "--global",
636
+ "user.name",
637
+ name.trim()
638
+ ]);
639
+ else await run("git", [
640
+ "config",
641
+ "--global",
642
+ "--unset",
643
+ "user.name"
644
+ ]);
645
+ if (typeof email === "string" && email.trim() !== "") await run("git", [
646
+ "config",
647
+ "--global",
648
+ "user.email",
649
+ email.trim()
650
+ ]);
651
+ else await run("git", [
652
+ "config",
653
+ "--global",
654
+ "--unset",
655
+ "user.email"
656
+ ]);
657
+ return getGitIdentity();
658
+ }
659
+ export { connect, disconnect, getAuthStatus, getGhInfo, getGitIdentity, getGitInfo, installCommands, installGh, installGit, isLoggedInOutput, packageManagers, setGitIdentity };
package/index.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import type { ApiHandler } from './api';
2
+ interface PluginContext {
3
+ baseUrl?: unknown;
4
+ webStartup?: {
5
+ trustedHosts?: unknown;
6
+ };
7
+ webServer: {
8
+ register(route: {
9
+ kind: 'prefix';
10
+ path: string;
11
+ handler: ApiHandler;
12
+ }): unknown;
13
+ };
14
+ effect(callback: () => unknown, label: string): unknown;
15
+ }
16
+ export declare const name = "github";
17
+ export declare const inject: string[];
18
+ export declare function apply(ctx: PluginContext): void;
19
+ export {};