@tmux-web/ext-gh-workflow 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/gh-client.d.ts +24 -0
- package/dist/gh-client.js +177 -0
- package/dist/gh-repo.d.ts +14 -0
- package/dist/gh-repo.js +58 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +35 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
2
|
+
type SpawnFn = typeof nodeSpawn;
|
|
3
|
+
/** @internal test hook */
|
|
4
|
+
export declare function __setExecFileForTests(fn: SpawnFn | null): void;
|
|
5
|
+
export type GhApiResult = {
|
|
6
|
+
status: number;
|
|
7
|
+
body: unknown;
|
|
8
|
+
};
|
|
9
|
+
export declare function parseGhIncludeOutput(stdout: string): GhApiResult;
|
|
10
|
+
export declare function runGh(args: string[], input?: string): Promise<{
|
|
11
|
+
stdout: string;
|
|
12
|
+
stderr: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function ghApi(endpoint: string, options?: {
|
|
15
|
+
method?: string;
|
|
16
|
+
body?: unknown;
|
|
17
|
+
}): Promise<GhApiResult>;
|
|
18
|
+
export declare function checkGhAuth(): Promise<{
|
|
19
|
+
ok: true;
|
|
20
|
+
} | {
|
|
21
|
+
ok: false;
|
|
22
|
+
reason: string;
|
|
23
|
+
}>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
2
|
+
let spawnImpl = nodeSpawn;
|
|
3
|
+
/** @internal test hook */
|
|
4
|
+
export function __setExecFileForTests(fn) {
|
|
5
|
+
spawnImpl = fn ?? nodeSpawn;
|
|
6
|
+
}
|
|
7
|
+
function ghEnv() {
|
|
8
|
+
const env = { ...process.env };
|
|
9
|
+
if (env.GITHUB_PAT && !env.GH_TOKEN && !env.GITHUB_TOKEN) {
|
|
10
|
+
env.GH_TOKEN = env.GITHUB_PAT;
|
|
11
|
+
}
|
|
12
|
+
return env;
|
|
13
|
+
}
|
|
14
|
+
function normalizeEndpoint(endpoint) {
|
|
15
|
+
return endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
|
|
16
|
+
}
|
|
17
|
+
export function parseGhIncludeOutput(stdout) {
|
|
18
|
+
if (!stdout)
|
|
19
|
+
return { status: 204, body: null };
|
|
20
|
+
const sep = stdout.includes('\r\n\r\n') ? '\r\n\r\n' : '\n\n';
|
|
21
|
+
const splitIdx = stdout.indexOf(sep);
|
|
22
|
+
if (splitIdx === -1) {
|
|
23
|
+
const statusLine = stdout.trim().split(/\r?\n/)[0] ?? '';
|
|
24
|
+
const statusMatch = statusLine.match(/\s(\d{3})\s/);
|
|
25
|
+
if (statusMatch) {
|
|
26
|
+
return { status: Number(statusMatch[1]), body: null };
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
return { status: 200, body: JSON.parse(stdout.trim()) };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { status: 200, body: stdout.trim() };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const headerBlock = stdout.slice(0, splitIdx);
|
|
36
|
+
const bodyText = stdout.slice(splitIdx + sep.length).trim();
|
|
37
|
+
const statusLine = headerBlock.split(/\r?\n/)[0] ?? '';
|
|
38
|
+
const statusMatch = statusLine.match(/\s(\d{3})\s/);
|
|
39
|
+
const status = statusMatch ? Number(statusMatch[1]) : 200;
|
|
40
|
+
if (!bodyText)
|
|
41
|
+
return { status, body: null };
|
|
42
|
+
try {
|
|
43
|
+
return { status, body: JSON.parse(bodyText) };
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return { status, body: bodyText };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function authErrorMessage(text) {
|
|
50
|
+
const lower = text.toLowerCase();
|
|
51
|
+
if (lower.includes('gh auth login')
|
|
52
|
+
|| lower.includes('not logged in')
|
|
53
|
+
|| lower.includes('authentication')
|
|
54
|
+
|| lower.includes('no oauth token')
|
|
55
|
+
|| lower.includes('gh_token')) {
|
|
56
|
+
return 'GitHub CLI is not authenticated. Run `gh auth login` or set GH_TOKEN/GITHUB_PAT.';
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
function ghNotFoundMessage(err) {
|
|
61
|
+
if (err.code === 'ENOENT') {
|
|
62
|
+
return {
|
|
63
|
+
status: 503,
|
|
64
|
+
body: { error: 'GitHub CLI (gh) not found in PATH. Install gh or set GH_TOKEN/GITHUB_PAT.' },
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
export async function runGh(args, input) {
|
|
70
|
+
const options = {
|
|
71
|
+
env: ghEnv(),
|
|
72
|
+
};
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const child = spawnImpl('gh', args, options);
|
|
75
|
+
const maxBuffer = 10 * 1024 * 1024;
|
|
76
|
+
let stdout = '';
|
|
77
|
+
let stderr = '';
|
|
78
|
+
let settled = false;
|
|
79
|
+
function finishWithError(err) {
|
|
80
|
+
if (settled)
|
|
81
|
+
return;
|
|
82
|
+
settled = true;
|
|
83
|
+
err.stdout = stdout;
|
|
84
|
+
err.stderr = stderr;
|
|
85
|
+
reject(err);
|
|
86
|
+
}
|
|
87
|
+
child.stdout.on('data', (chunk) => {
|
|
88
|
+
stdout += String(chunk);
|
|
89
|
+
if (stdout.length + stderr.length > maxBuffer) {
|
|
90
|
+
child.kill();
|
|
91
|
+
finishWithError(Object.assign(new Error('gh output exceeded max buffer'), { code: 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' }));
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
child.stderr.on('data', (chunk) => {
|
|
95
|
+
stderr += String(chunk);
|
|
96
|
+
if (stdout.length + stderr.length > maxBuffer) {
|
|
97
|
+
child.kill();
|
|
98
|
+
finishWithError(Object.assign(new Error('gh output exceeded max buffer'), { code: 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' }));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
child.on('error', (err) => finishWithError(err));
|
|
102
|
+
child.on('close', (code) => {
|
|
103
|
+
if (settled)
|
|
104
|
+
return;
|
|
105
|
+
settled = true;
|
|
106
|
+
if (code && code !== 0) {
|
|
107
|
+
const err = Object.assign(new Error(`gh exited with code ${code}`), {
|
|
108
|
+
code,
|
|
109
|
+
stdout,
|
|
110
|
+
stderr,
|
|
111
|
+
});
|
|
112
|
+
reject(err);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
resolve({ stdout, stderr });
|
|
116
|
+
});
|
|
117
|
+
child.stdin.end(input);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
export async function ghApi(endpoint, options) {
|
|
121
|
+
const args = ['api', '--include', normalizeEndpoint(endpoint)];
|
|
122
|
+
const method = options?.method?.toUpperCase();
|
|
123
|
+
if (method && method !== 'GET') {
|
|
124
|
+
args.push('-X', method);
|
|
125
|
+
}
|
|
126
|
+
const input = options?.body !== undefined ? JSON.stringify(options.body) : undefined;
|
|
127
|
+
if (input !== undefined) {
|
|
128
|
+
args.push('--input', '-');
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const { stdout } = await runGh(args, input);
|
|
132
|
+
return parseGhIncludeOutput(stdout);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const execErr = err;
|
|
136
|
+
const notFound = ghNotFoundMessage(execErr);
|
|
137
|
+
if (notFound)
|
|
138
|
+
return notFound;
|
|
139
|
+
const combined = `${execErr.stdout ?? ''}\n${execErr.stderr ?? ''}`.trim();
|
|
140
|
+
const authMsg = authErrorMessage(combined);
|
|
141
|
+
if (authMsg)
|
|
142
|
+
return { status: 401, body: { error: authMsg } };
|
|
143
|
+
if (execErr.stdout?.trim()) {
|
|
144
|
+
const parsed = parseGhIncludeOutput(execErr.stdout);
|
|
145
|
+
if (parsed.status >= 400)
|
|
146
|
+
return parsed;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const body = combined ? JSON.parse(combined) : { error: combined || 'GitHub CLI request failed' };
|
|
150
|
+
return { status: 502, body };
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return { status: 502, body: { error: combined || 'GitHub CLI request failed' } };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
export async function checkGhAuth() {
|
|
158
|
+
try {
|
|
159
|
+
await runGh(['auth', 'status']);
|
|
160
|
+
return { ok: true };
|
|
161
|
+
}
|
|
162
|
+
catch (err) {
|
|
163
|
+
const execErr = err;
|
|
164
|
+
const notFound = ghNotFoundMessage(execErr);
|
|
165
|
+
if (notFound)
|
|
166
|
+
return { ok: false, reason: String(notFound.body.error) };
|
|
167
|
+
if (process.env.GH_TOKEN || process.env.GITHUB_TOKEN || process.env.GITHUB_PAT) {
|
|
168
|
+
return { ok: true };
|
|
169
|
+
}
|
|
170
|
+
const combined = `${execErr.stdout ?? ''}\n${execErr.stderr ?? ''}`.trim();
|
|
171
|
+
const authMsg = authErrorMessage(combined);
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
reason: authMsg ?? (combined || 'GitHub CLI authentication check failed'),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { execFile as nodeExecFile } from 'node:child_process';
|
|
2
|
+
export interface GhRepoInfo {
|
|
3
|
+
nameWithOwner: string;
|
|
4
|
+
org: string;
|
|
5
|
+
repo: string;
|
|
6
|
+
}
|
|
7
|
+
type ExecFileFn = typeof nodeExecFile;
|
|
8
|
+
/** @internal test hook */
|
|
9
|
+
export declare function __setExecFileForRepoTests(fn: ExecFileFn | null): void;
|
|
10
|
+
/** Resolve GitHub repo info from a directory (uses git root + gh repo view). */
|
|
11
|
+
export declare function ghRepoView(cwd: string): Promise<GhRepoInfo | null>;
|
|
12
|
+
/** @deprecated use ghRepoView */
|
|
13
|
+
export declare const ghRepoViewFromDir: typeof ghRepoView;
|
|
14
|
+
export {};
|
package/dist/gh-repo.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { execFile as nodeExecFile } from 'node:child_process';
|
|
2
|
+
let execFileImpl = nodeExecFile;
|
|
3
|
+
/** @internal test hook */
|
|
4
|
+
export function __setExecFileForRepoTests(fn) {
|
|
5
|
+
execFileImpl = fn ?? nodeExecFile;
|
|
6
|
+
}
|
|
7
|
+
function gitRepoRoot(cwd) {
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
execFileImpl('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], { encoding: 'utf-8' }, (err, stdout) => {
|
|
10
|
+
if (err)
|
|
11
|
+
resolve(null);
|
|
12
|
+
else
|
|
13
|
+
resolve(stdout?.trim() || null);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function runGhFromDir(dir, args) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
execFileImpl('gh', args, { cwd: dir, encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
20
|
+
if (err)
|
|
21
|
+
reject(err);
|
|
22
|
+
else
|
|
23
|
+
resolve({
|
|
24
|
+
stdout: stdout == null ? '' : String(stdout),
|
|
25
|
+
stderr: stderr == null ? '' : String(stderr),
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/** Resolve GitHub repo info from a directory (uses git root + gh repo view). */
|
|
31
|
+
export async function ghRepoView(cwd) {
|
|
32
|
+
const repoRoot = await gitRepoRoot(cwd);
|
|
33
|
+
if (!repoRoot)
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
const { stdout } = await runGhFromDir(repoRoot, [
|
|
37
|
+
'repo', 'view',
|
|
38
|
+
'--json', 'nameWithOwner,name,owner',
|
|
39
|
+
]);
|
|
40
|
+
const jsonText = stdout.trim();
|
|
41
|
+
if (!jsonText)
|
|
42
|
+
return null;
|
|
43
|
+
const data = JSON.parse(jsonText);
|
|
44
|
+
const nameWithOwner = data.nameWithOwner
|
|
45
|
+
?? (data.owner?.login && data.name ? `${data.owner.login}/${data.name}` : null);
|
|
46
|
+
if (!nameWithOwner)
|
|
47
|
+
return null;
|
|
48
|
+
const [org, repo] = nameWithOwner.split('/');
|
|
49
|
+
if (!org || !repo)
|
|
50
|
+
return null;
|
|
51
|
+
return { nameWithOwner, org, repo };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** @deprecated use ghRepoView */
|
|
58
|
+
export const ghRepoViewFromDir = ghRepoView;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tmux-web/ext-gh-workflow",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared GitHub CLI helpers for tmux-web extensions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/ashutoshpw/tmux-web",
|
|
10
|
+
"directory": "packages/ext-gh-workflow"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist/"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.json",
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^25.8.0",
|
|
32
|
+
"typescript": "^5.7.0",
|
|
33
|
+
"vitest": "^2.1.9"
|
|
34
|
+
}
|
|
35
|
+
}
|