@doki-land/nifty 0.0.0 → 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.
@@ -0,0 +1,44 @@
1
+ import type { Cli, ParsedOptions } from "@vmz/commander";
2
+
3
+ import { bootstrapFromOptions } from "./context.js";
4
+ import { uploadFromOptions } from "./upload.js";
5
+ import { cwdFrom, flag, str } from "./options.js";
6
+
7
+ export function registerUploadCommand(cli: Cli): void {
8
+ cli.command("upload", "cli.cmd.upload")
9
+ .option("--release", "cli.opt.release")
10
+ .option("--pages", "cli.opt.pages")
11
+ .option("--both", "cli.opt.both")
12
+ .option("-d, --dir <path>", "cli.opt.dir")
13
+ .option("-t, --tag <tag>", "cli.opt.tag")
14
+ .option("--repo <repo>", "cli.opt.repo")
15
+ .option("--token <token>", "cli.opt.token")
16
+ .option("--name <title>", "cli.opt.name")
17
+ .option("--notes <body>", "cli.opt.notes")
18
+ .option("--notes-file <path>", "cli.opt.notes-file")
19
+ .option("--draft", "cli.opt.draft")
20
+ .option("--no-generate-notes", "cli.opt.no-generate-notes")
21
+ .option("--github-action", "cli.opt.github-action")
22
+ .action(async (options) => cmdUpload(options));
23
+ }
24
+
25
+ export async function cmdUpload(options: ParsedOptions): Promise<number> {
26
+ await bootstrapFromOptions(options);
27
+ await uploadFromOptions({
28
+ release: flag(options, "release"),
29
+ pages: flag(options, "pages"),
30
+ both: flag(options, "both"),
31
+ dir: str(options, "dir") ?? "dist",
32
+ tag: str(options, "tag"),
33
+ repo: str(options, "repo"),
34
+ token: str(options, "token"),
35
+ name: str(options, "name"),
36
+ notes: str(options, "notes"),
37
+ notesFile: str(options, "notes-file"),
38
+ draft: flag(options, "draft"),
39
+ generateNotes: !flag(options, "no-generate-notes"),
40
+ githubAction: flag(options, "github-action"),
41
+ cwd: cwdFrom(options),
42
+ });
43
+ return 0;
44
+ }
package/src/cli/upload.ts CHANGED
@@ -1,280 +1,313 @@
1
- import { spawnSync } from "node:child_process";
2
- import { cpSync, existsSync, mkdtempSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { basename, join, relative, resolve } from "node:path";
5
-
6
- import { detectProjectLayout } from "../config/detectProject.js";
7
- import { loadNiftyNative } from "../native.js";
8
-
9
- const USER_AGENT = "nifty-upload";
10
-
11
- type UploadOptions = {
12
- release: boolean;
13
- pages: boolean;
14
- both: boolean;
15
- dir: string;
16
- tag?: string;
17
- repo?: string;
18
- token?: string;
19
- name?: string;
20
- notes?: string;
21
- notesFile?: string;
22
- draft: boolean;
23
- generateNotes: boolean;
24
- githubAction: boolean;
25
- cwd?: string;
26
- };
27
-
28
- type GithubRelease = {
29
- id: number;
30
- };
31
-
32
- export async function runUpload(argv: string[]): Promise<void> {
33
- const options = parseUploadArgs(argv);
34
- if (options.githubAction) {
35
- applyGithubActionDefaults(options);
36
- }
37
-
38
- const token = options.token ?? process.env.GITHUB_TOKEN;
39
- if (!token) {
40
- throw new Error("GitHub token is required (set --token or GITHUB_TOKEN)");
41
- }
42
-
43
- const cwd = options.cwd ?? process.cwd();
44
- const layout = detectProjectLayout(cwd);
45
- const native = loadNiftyNative();
46
- const repo =
47
- options.repo ??
48
- native.git["detect-github-repo"](layout.root) ??
49
- process.env.GITHUB_REPOSITORY;
50
- if (!repo) {
51
- throw new Error("could not detect GitHub repo (use --repo owner/name)");
52
- }
53
-
54
- const tag = options.tag ?? process.env.GITHUB_REF_NAME;
55
- if (!tag) {
56
- throw new Error("release tag is required (use --tag or push a git tag)");
57
- }
58
-
59
- const target = parseTarget(options);
60
- const notes = await resolveNotes(options, layout.root, tag, native);
61
-
62
- if (target === "release" || target === "both") {
63
- await uploadReleaseAssets(repo, token, tag, options, notes);
64
- }
65
- if (target === "pages" || target === "both") {
66
- await deployGithubPages(repo, token, options.dir);
67
- }
68
- }
69
-
70
- function parseUploadArgs(argv: string[]): UploadOptions {
71
- const options: UploadOptions = {
72
- release: false,
73
- pages: false,
74
- both: false,
75
- dir: "dist",
76
- draft: false,
77
- generateNotes: true,
78
- githubAction: false,
79
- };
80
-
81
- for (let i = 0; i < argv.length; i += 1) {
82
- const arg = argv[i];
83
- if (arg === "--release") options.release = true;
84
- else if (arg === "--pages") options.pages = true;
85
- else if (arg === "--both") options.both = true;
86
- else if (arg === "-d" || arg === "--dir") options.dir = argv[++i];
87
- else if (arg === "-t" || arg === "--tag") options.tag = argv[++i];
88
- else if (arg === "--repo") options.repo = argv[++i];
89
- else if (arg === "--token") options.token = argv[++i];
90
- else if (arg === "--name") options.name = argv[++i];
91
- else if (arg === "--notes") options.notes = argv[++i];
92
- else if (arg === "--notes-file") options.notesFile = argv[++i];
93
- else if (arg === "--draft") options.draft = true;
94
- else if (arg === "--no-generate-notes") options.generateNotes = false;
95
- else if (arg === "--github-action") options.githubAction = true;
96
- else if (arg === "-C" || arg === "--cwd") options.cwd = argv[++i];
97
- }
98
-
99
- return options;
100
- }
101
-
102
- function parseTarget(options: UploadOptions): "release" | "pages" | "both" {
103
- if (options.both) return "both";
104
- if (options.pages) return "pages";
105
- if (options.release) return "release";
106
- throw new Error("specify --release, --pages, or --both");
107
- }
108
-
109
- function applyGithubActionDefaults(options: UploadOptions): void {
110
- options.token ??= process.env.GITHUB_TOKEN;
111
- options.repo ??= process.env.GITHUB_REPOSITORY;
112
- options.tag ??= process.env.GITHUB_REF_NAME;
113
- options.cwd ??= process.env.GITHUB_WORKSPACE;
114
- }
115
-
116
- async function resolveNotes(
117
- options: UploadOptions,
118
- repoRoot: string,
119
- tag: string,
120
- native: ReturnType<typeof loadNiftyNative>,
121
- ): Promise<string> {
122
- if (options.notes) return options.notes;
123
- if (options.notesFile) return readFileSync(options.notesFile, "utf8");
124
- if (!options.generateNotes) return "";
125
- try {
126
- const range = native.git["resolve-range"](repoRoot, tag.replace(/^v/, ""), undefined, tag);
127
- const commits = native.git["collect-commits"](repoRoot, range.fromRef, range.toRef);
128
- return commits.map((commit) => `- ${commit.body}`).join("\n");
129
- } catch {
130
- return "";
131
- }
132
- }
133
-
134
- async function uploadReleaseAssets(
135
- repo: string,
136
- token: string,
137
- tag: string,
138
- options: UploadOptions,
139
- notes: string,
140
- ): Promise<void> {
141
- const [owner, name] = splitRepo(repo);
142
- let release = await apiReleaseByTag(owner, name, tag, token);
143
- if (!release) {
144
- release = await apiCreateRelease(owner, name, tag, options.name ?? tag, notes, options.draft, token);
145
- }
146
- const dir = resolve(options.cwd ?? process.cwd(), options.dir);
147
- for (const file of collectFiles(dir)) {
148
- const asset = relative(dir, file).split(/[/\\]/).join("-") || basename(file);
149
- await apiUploadAsset(owner, name, release.id, asset, file, token);
150
- console.log(`release: uploaded ${asset}`);
151
- }
152
- }
153
-
154
- async function deployGithubPages(repo: string, token: string, dir: string): Promise<void> {
155
- const [owner, name] = splitRepo(repo);
156
- const source = resolve(dir);
157
- const work = mkdtempSync(join(tmpdir(), "nifty-pages-"));
158
- cpSync(source, work, { recursive: true });
159
- const nojekyll = join(work, ".nojekyll");
160
- if (!existsSync(nojekyll)) {
161
- writeFileSync(nojekyll, "");
162
- }
163
- git(work, ["init"]);
164
- git(work, ["config", "user.name", "github-actions[bot]"]);
165
- git(work, ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"]);
166
- git(work, ["add", "."]);
167
- git(work, ["commit", "-m", "Deploy GitHub Pages"]);
168
- const remote = `https://x-access-token:${token}@github.com/${owner}/${name}.git`;
169
- git(work, ["remote", "add", "origin", remote]);
170
- git(work, ["push", "--force", "origin", "HEAD:gh-pages"]);
171
- console.log(`pages: pushed ${source} to gh-pages`);
172
- }
173
-
174
- function collectFiles(dir: string): string[] {
175
- const out: string[] = [];
176
- for (const entry of walk(dir)) {
177
- if (statSync(entry).isFile()) {
178
- out.push(entry);
179
- }
180
- }
181
- return out;
182
- }
183
-
184
- function walk(dir: string): string[] {
185
- const files: string[] = [];
186
- for (const entry of readdirSync(dir)) {
187
- const path = join(dir, entry);
188
- if (statSync(path).isDirectory()) {
189
- files.push(...walk(path));
190
- } else {
191
- files.push(path);
192
- }
193
- }
194
- return files;
195
- }
196
-
197
- function splitRepo(repo: string): [string, string] {
198
- const parts = repo.split("/");
199
- if (parts.length !== 2 || !parts[0] || !parts[1]) {
200
- throw new Error(`invalid repo ${repo}, expected owner/name`);
201
- }
202
- return [parts[0], parts[1]];
203
- }
204
-
205
- async function apiReleaseByTag(
206
- owner: string,
207
- name: string,
208
- tag: string,
209
- token: string,
210
- ): Promise<GithubRelease | null> {
211
- const response = await fetch(
212
- `https://api.github.com/repos/${owner}/${name}/releases/tags/${encodeURIComponent(tag)}`,
213
- { headers: githubHeaders(token) },
214
- );
215
- if (response.status === 404) return null;
216
- if (!response.ok) {
217
- throw new Error(`GitHub API ${response.status}: ${await response.text()}`);
218
- }
219
- return (await response.json()) as GithubRelease;
220
- }
221
-
222
- async function apiCreateRelease(
223
- owner: string,
224
- name: string,
225
- tag: string,
226
- title: string,
227
- body: string,
228
- draft: boolean,
229
- token: string,
230
- ): Promise<GithubRelease> {
231
- const response = await fetch(`https://api.github.com/repos/${owner}/${name}/releases`, {
232
- method: "POST",
233
- headers: { ...githubHeaders(token), "Content-Type": "application/json" },
234
- body: JSON.stringify({ tag_name: tag, name: title, body, draft }),
235
- });
236
- if (!response.ok) {
237
- throw new Error(`GitHub API ${response.status}: ${await response.text()}`);
238
- }
239
- return (await response.json()) as GithubRelease;
240
- }
241
-
242
- async function apiUploadAsset(
243
- owner: string,
244
- name: string,
245
- releaseId: number,
246
- assetName: string,
247
- filePath: string,
248
- token: string,
249
- ): Promise<void> {
250
- const url = `https://uploads.github.com/repos/${owner}/${name}/releases/${releaseId}/assets?name=${encodeURIComponent(assetName)}`;
251
- const body = readFileSync(filePath);
252
- const response = await fetch(url, {
253
- method: "POST",
254
- headers: {
255
- ...githubHeaders(token),
256
- "Content-Type": "application/octet-stream",
257
- "Content-Length": String(body.length),
258
- },
259
- body,
260
- });
261
- if (!response.ok) {
262
- throw new Error(`GitHub upload ${response.status}: ${await response.text()}`);
263
- }
264
- }
265
-
266
- function githubHeaders(token: string): Record<string, string> {
267
- return {
268
- Accept: "application/vnd.github+json",
269
- Authorization: `Bearer ${token}`,
270
- "User-Agent": USER_AGENT,
271
- "X-GitHub-Api-Version": "2022-11-28",
272
- };
273
- }
274
-
275
- function git(cwd: string, args: string[]): void {
276
- const result = spawnSync("git", args, { cwd, stdio: "inherit" });
277
- if (result.status !== 0) {
278
- throw new Error(`git ${args.join(" ")} failed`);
279
- }
280
- }
1
+ import { spawnSync } from "node:child_process";
2
+ import { cpSync, existsSync, mkdtempSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, join, relative, resolve } from "node:path";
5
+
6
+ import { detectProjectLayout } from "../config/detectProject.js";
7
+ import { loadNiftyNative } from "../native.js";
8
+
9
+ const USER_AGENT = "nifty-upload";
10
+
11
+ export type UploadOptions = {
12
+ release: boolean;
13
+ pages: boolean;
14
+ both: boolean;
15
+ dir: string;
16
+ tag?: string;
17
+ repo?: string;
18
+ token?: string;
19
+ name?: string;
20
+ notes?: string;
21
+ notesFile?: string;
22
+ draft: boolean;
23
+ generateNotes: boolean;
24
+ githubAction: boolean;
25
+ cwd?: string;
26
+ };
27
+
28
+ type GithubRelease = {
29
+ id: number;
30
+ };
31
+
32
+ export async function runUpload(argv: string[]): Promise<void> {
33
+ await uploadFromOptions(parseUploadArgs(argv));
34
+ }
35
+
36
+ export async function uploadFromOptions(options: UploadOptions): Promise<void> {
37
+ if (options.githubAction) {
38
+ applyGithubActionDefaults(options);
39
+ }
40
+
41
+ const token = options.token ?? process.env.GITHUB_TOKEN;
42
+ if (!token) {
43
+ throw new Error("GitHub token is required (set --token or GITHUB_TOKEN)");
44
+ }
45
+
46
+ const cwd = options.cwd ?? process.cwd();
47
+ const layout = detectProjectLayout(cwd);
48
+ const native = loadNiftyNative();
49
+ const repo =
50
+ options.repo ??
51
+ native.git["detect-github-repo"](layout.root) ??
52
+ process.env.GITHUB_REPOSITORY;
53
+ if (!repo) {
54
+ throw new Error("could not detect GitHub repo (use --repo owner/name)");
55
+ }
56
+
57
+ const tag = options.tag ?? process.env.GITHUB_REF_NAME;
58
+ if (!tag) {
59
+ throw new Error("release tag is required (use --tag or push a git tag)");
60
+ }
61
+
62
+ const target = parseTarget(options);
63
+ const notes = await resolveNotes(options, layout.root, tag, native);
64
+
65
+ if (target === "release" || target === "both") {
66
+ await uploadReleaseAssets(repo, token, tag, options, notes);
67
+ }
68
+ if (target === "pages" || target === "both") {
69
+ await deployGithubPages(repo, token, options.dir);
70
+ }
71
+ }
72
+
73
+ function parseUploadArgs(argv: string[]): UploadOptions {
74
+ const options: UploadOptions = {
75
+ release: false,
76
+ pages: false,
77
+ both: false,
78
+ dir: "dist",
79
+ draft: false,
80
+ generateNotes: true,
81
+ githubAction: false,
82
+ };
83
+
84
+ for (let i = 0; i < argv.length; i += 1) {
85
+ const arg = argv[i];
86
+ if (arg === "--release") options.release = true;
87
+ else if (arg === "--pages") options.pages = true;
88
+ else if (arg === "--both") options.both = true;
89
+ else if (arg === "-d" || arg === "--dir") options.dir = argv[++i];
90
+ else if (arg === "-h" || arg === "--help") {
91
+ printUploadHelp();
92
+ process.exit(0);
93
+ }
94
+ else if (arg === "-t" || arg === "--tag") options.tag = argv[++i];
95
+ else if (arg === "--repo") options.repo = argv[++i];
96
+ else if (arg === "--token") options.token = argv[++i];
97
+ else if (arg === "--name") options.name = argv[++i];
98
+ else if (arg === "--notes") options.notes = argv[++i];
99
+ else if (arg === "--notes-file") options.notesFile = argv[++i];
100
+ else if (arg === "--draft") options.draft = true;
101
+ else if (arg === "--no-generate-notes") options.generateNotes = false;
102
+ else if (arg === "--github-action") options.githubAction = true;
103
+ else if (arg === "-C" || arg === "--cwd") options.cwd = argv[++i];
104
+ }
105
+
106
+ return options;
107
+ }
108
+
109
+ function parseTarget(options: UploadOptions): "release" | "pages" | "both" {
110
+ if (options.both) return "both";
111
+ if (options.pages) return "pages";
112
+ if (options.release) return "release";
113
+ throw new Error("specify --release, --pages, or --both");
114
+ }
115
+
116
+ function applyGithubActionDefaults(options: UploadOptions): void {
117
+ options.token ??= process.env.GITHUB_TOKEN;
118
+ options.repo ??= process.env.GITHUB_REPOSITORY;
119
+ options.tag ??= process.env.GITHUB_REF_NAME;
120
+ options.cwd ??= process.env.GITHUB_WORKSPACE;
121
+ }
122
+
123
+ async function resolveNotes(
124
+ options: UploadOptions,
125
+ repoRoot: string,
126
+ tag: string,
127
+ native: ReturnType<typeof loadNiftyNative>,
128
+ ): Promise<string> {
129
+ if (options.notes) return options.notes;
130
+ if (options.notesFile) return readFileSync(options.notesFile, "utf8");
131
+ if (!options.generateNotes) return "";
132
+ try {
133
+ const range = native.git["resolve-range"](repoRoot, tag.replace(/^v/, ""), undefined, tag);
134
+ const commits = native.git["collect-commits"](repoRoot, range["from-ref"], range["to-ref"]);
135
+ return commits.map((commit) => `- ${commit.body}`).join("\n");
136
+ } catch {
137
+ return "";
138
+ }
139
+ }
140
+
141
+ async function uploadReleaseAssets(
142
+ repo: string,
143
+ token: string,
144
+ tag: string,
145
+ options: UploadOptions,
146
+ notes: string,
147
+ ): Promise<void> {
148
+ const [owner, name] = splitRepo(repo);
149
+ let release = await apiReleaseByTag(owner, name, tag, token);
150
+ if (!release) {
151
+ release = await apiCreateRelease(owner, name, tag, options.name ?? tag, notes, options.draft, token);
152
+ }
153
+ const cwd = options.cwd ?? process.cwd();
154
+ const files = collectFiles(resolve(cwd, options.dir));
155
+ for (const file of files) {
156
+ const asset =
157
+ relative(resolve(cwd, options.dir), file).split(/[/\\]/).join("-") || basename(file);
158
+ await apiUploadAsset(owner, name, release.id, asset, file, token);
159
+ console.log(`release: uploaded ${asset}`);
160
+ }
161
+ }
162
+
163
+ function printUploadHelp(): void {
164
+ console.log(`nifty upload — GitHub Release assets and/or GitHub Pages
165
+
166
+ Usage:
167
+ nifty upload --release [--dir dist] [--tag vX.Y.Z]
168
+ nifty upload --pages --dir dist
169
+ nifty upload --both --dir dist
170
+ nifty upload --release --github-action
171
+
172
+ Options:
173
+ --release upload files from --dir to the GitHub Release for --tag
174
+ --pages push --dir to gh-pages
175
+ --both release + pages
176
+ -d, --dir <path> directory tree for --release/--pages (default: dist)
177
+ --github-action read GITHUB_TOKEN, GITHUB_REPOSITORY, GITHUB_REF_NAME
178
+ `);
179
+ }
180
+
181
+ async function deployGithubPages(repo: string, token: string, dir: string): Promise<void> {
182
+ const [owner, name] = splitRepo(repo);
183
+ const source = resolve(dir);
184
+ const work = mkdtempSync(join(tmpdir(), "nifty-pages-"));
185
+ cpSync(source, work, { recursive: true });
186
+ const nojekyll = join(work, ".nojekyll");
187
+ if (!existsSync(nojekyll)) {
188
+ writeFileSync(nojekyll, "");
189
+ }
190
+ git(work, ["init"]);
191
+ git(work, ["config", "user.name", "github-actions[bot]"]);
192
+ git(work, ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"]);
193
+ git(work, ["add", "."]);
194
+ git(work, ["commit", "-m", "Deploy GitHub Pages"]);
195
+ const remote = `https://x-access-token:${token}@github.com/${owner}/${name}.git`;
196
+ git(work, ["remote", "add", "origin", remote]);
197
+ git(work, ["push", "--force", "origin", "HEAD:gh-pages"]);
198
+ console.log(`pages: pushed ${source} to gh-pages`);
199
+ }
200
+
201
+ function collectFiles(dir: string): string[] {
202
+ const out: string[] = [];
203
+ for (const entry of walk(dir)) {
204
+ if (statSync(entry).isFile()) {
205
+ out.push(entry);
206
+ }
207
+ }
208
+ return out;
209
+ }
210
+
211
+ function walk(dir: string): string[] {
212
+ const files: string[] = [];
213
+ for (const entry of readdirSync(dir)) {
214
+ const path = join(dir, entry);
215
+ if (statSync(path).isDirectory()) {
216
+ files.push(...walk(path));
217
+ } else {
218
+ files.push(path);
219
+ }
220
+ }
221
+ return files;
222
+ }
223
+
224
+ function splitRepo(repo: string): [string, string] {
225
+ const parts = repo.split("/");
226
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
227
+ throw new Error(`invalid repo ${repo}, expected owner/name`);
228
+ }
229
+ return [parts[0], parts[1]];
230
+ }
231
+
232
+ async function apiReleaseByTag(
233
+ owner: string,
234
+ name: string,
235
+ tag: string,
236
+ token: string,
237
+ ): Promise<GithubRelease | null> {
238
+ const response = await fetch(
239
+ `https://api.github.com/repos/${owner}/${name}/releases/tags/${encodeURIComponent(tag)}`,
240
+ { headers: githubHeaders(token) },
241
+ );
242
+ if (response.status === 404) return null;
243
+ if (!response.ok) {
244
+ throw new Error(`GitHub API ${response.status}: ${await response.text()}`);
245
+ }
246
+ return (await response.json()) as GithubRelease;
247
+ }
248
+
249
+ async function apiCreateRelease(
250
+ owner: string,
251
+ name: string,
252
+ tag: string,
253
+ title: string,
254
+ body: string,
255
+ draft: boolean,
256
+ token: string,
257
+ ): Promise<GithubRelease> {
258
+ const response = await fetch(`https://api.github.com/repos/${owner}/${name}/releases`, {
259
+ method: "POST",
260
+ headers: { ...githubHeaders(token), "Content-Type": "application/json" },
261
+ body: JSON.stringify({ tag_name: tag, name: title, body, draft }),
262
+ });
263
+ if (!response.ok) {
264
+ throw new Error(`GitHub API ${response.status}: ${await response.text()}`);
265
+ }
266
+ return (await response.json()) as GithubRelease;
267
+ }
268
+
269
+ async function apiUploadAsset(
270
+ owner: string,
271
+ name: string,
272
+ releaseId: number,
273
+ assetName: string,
274
+ filePath: string,
275
+ token: string,
276
+ ): Promise<void> {
277
+ const url = `https://uploads.github.com/repos/${owner}/${name}/releases/${releaseId}/assets?name=${encodeURIComponent(assetName)}`;
278
+ const body = readFileSync(filePath);
279
+ const response = await fetch(url, {
280
+ method: "POST",
281
+ headers: {
282
+ ...githubHeaders(token),
283
+ "Content-Type": "application/octet-stream",
284
+ "Content-Length": String(body.length),
285
+ },
286
+ body,
287
+ });
288
+ const text = await response.text();
289
+ if (response.ok) {
290
+ return;
291
+ }
292
+ if (response.status === 422 && text.includes("already_exists")) {
293
+ console.log(`release: skip ${assetName} (already uploaded)`);
294
+ return;
295
+ }
296
+ throw new Error(`GitHub upload ${response.status}: ${text}`);
297
+ }
298
+
299
+ function githubHeaders(token: string): Record<string, string> {
300
+ return {
301
+ Accept: "application/vnd.github+json",
302
+ Authorization: `Bearer ${token}`,
303
+ "User-Agent": USER_AGENT,
304
+ "X-GitHub-Api-Version": "2022-11-28",
305
+ };
306
+ }
307
+
308
+ function git(cwd: string, args: string[]): void {
309
+ const result = spawnSync("git", args, { cwd, stdio: "inherit" });
310
+ if (result.status !== 0) {
311
+ throw new Error(`git ${args.join(" ")} failed`);
312
+ }
313
+ }