@kazoottt/github-repo-cleaner 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KazooTTT
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # github-repo-cleaner
2
+
3
+ Local terminal UI for cleaning GitHub repositories.
4
+
5
+ The tool reuses GitHub CLI authentication, loads repositories for the active GitHub user, makes forks easy to identify, and supports archive/delete workflows with a page-level second confirmation.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @kazoottt/github-repo-cleaner
11
+ ```
12
+
13
+ Run either command:
14
+
15
+ ```bash
16
+ repo-cleaner
17
+ github-repo-cleaner
18
+ ```
19
+
20
+ ## Authentication
21
+
22
+ This tool expects `gh` to own credentials:
23
+
24
+ ```bash
25
+ gh auth login
26
+ gh auth refresh -h github.com -s delete_repo
27
+ ```
28
+
29
+ The app reads the active token from `gh auth token` and does not store PATs or GitHub tokens.
30
+
31
+ ## Usage
32
+
33
+ ```bash
34
+ repo-cleaner
35
+ repo-cleaner --owner KazooTTT
36
+ ```
37
+
38
+ Keys:
39
+
40
+ ```text
41
+ up/down move
42
+ space select repository
43
+ f toggle forks-only
44
+ s toggle sort oldest/newest
45
+ / search
46
+ a archive mode
47
+ d delete mode
48
+ enter review selected repositories
49
+ esc cancel review
50
+ q quit
51
+ ```
52
+
53
+ ## Development
54
+
55
+ ```bash
56
+ npm install
57
+ npm run dev
58
+ npm test
59
+ npm run typecheck
60
+ npm run build
61
+ ```
62
+
63
+ ## Release
64
+
65
+ Releases are created locally with `bumpp` and published by GitHub Actions when a `v*` tag is pushed.
66
+
67
+ ```bash
68
+ npm run release
69
+ ```
70
+
71
+ The release script runs tests, typecheck, and build before `bumpp` creates the version commit and tag.
72
+
73
+ ## npm trusted publishing setup
74
+
75
+ This project is configured for npm trusted publishing with provenance. No `NPM_TOKEN` secret is required.
76
+
77
+ Expected identities:
78
+
79
+ ```text
80
+ GitHub owner: KazooTTT
81
+ GitHub repository: github-repo-cleaner
82
+ npm package: @kazoottt/github-repo-cleaner
83
+ Workflow filename: publish.yml
84
+ ```
85
+
86
+ Setup steps:
87
+
88
+ 1. Push this repository to GitHub as `KazooTTT/github-repo-cleaner`.
89
+ 2. Sign in to npm as the owner of the `@kazoottt` scope.
90
+ 3. Open the npm package settings for `@kazoottt/github-repo-cleaner`.
91
+ 4. Add a trusted publisher with:
92
+ - Owner: `KazooTTT`
93
+ - Repository: `github-repo-cleaner`
94
+ - Workflow filename: `publish.yml`
95
+ - Environment name: leave empty unless the workflow is changed to use one
96
+ 5. For the first publish only, run `npm publish --access public` locally to create the package.
97
+ 6. Add the trusted publisher in the package settings.
98
+ 7. For later releases, run `npm run release` locally.
99
+ 8. Confirm the pushed tag triggers the `Publish` workflow in GitHub Actions.
100
+
101
+ After trusted publishing is configured, the publish workflow runs:
102
+
103
+ ```bash
104
+ npm ci
105
+ npm test
106
+ npm run typecheck
107
+ npm run build
108
+ npm publish --provenance --access public
109
+ ```
110
+
111
+ ## Issue Tracker
112
+
113
+ Local implementation issues live under `.scratch/issues/`.
package/dist/auth.js ADDED
@@ -0,0 +1,35 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ export class AuthError extends Error {
5
+ code;
6
+ constructor(code, message) {
7
+ super(message);
8
+ this.code = code;
9
+ }
10
+ }
11
+ export async function getGhToken(runCommand = execFileAsync) {
12
+ try {
13
+ await runCommand("gh", ["auth", "status", "--hostname", "github.com"]);
14
+ }
15
+ catch (error) {
16
+ const nodeError = error;
17
+ if (nodeError.code === "ENOENT") {
18
+ throw new AuthError("missing-gh", "GitHub CLI is not installed. Install gh, then run gh auth login.");
19
+ }
20
+ throw new AuthError("not-authenticated", "GitHub CLI is not authenticated. Run gh auth login.");
21
+ }
22
+ try {
23
+ const { stdout } = await runCommand("gh", ["auth", "token", "--hostname", "github.com"]);
24
+ const token = stdout.trim();
25
+ if (!token) {
26
+ throw new AuthError("token-unavailable", "GitHub CLI did not return a token.");
27
+ }
28
+ return token;
29
+ }
30
+ catch (error) {
31
+ if (error instanceof AuthError)
32
+ throw error;
33
+ throw new AuthError("token-unavailable", "Unable to read a GitHub token from gh. Try gh auth refresh -h github.com -s delete_repo.");
34
+ }
35
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { render } from "ink";
4
+ import { getGhToken, AuthError } from "./auth.js";
5
+ import { OctokitGitHubApi } from "./github.js";
6
+ import { App } from "./ui.js";
7
+ function parseOwner(argv) {
8
+ const ownerIndex = argv.indexOf("--owner");
9
+ if (ownerIndex === -1)
10
+ return undefined;
11
+ return argv[ownerIndex + 1];
12
+ }
13
+ try {
14
+ const token = await getGhToken();
15
+ const api = new OctokitGitHubApi(token);
16
+ render(_jsx(App, { api: api, owner: parseOwner(process.argv.slice(2)) }));
17
+ }
18
+ catch (error) {
19
+ if (error instanceof AuthError) {
20
+ console.error(error.message);
21
+ process.exitCode = 1;
22
+ }
23
+ else {
24
+ throw error;
25
+ }
26
+ }
package/dist/domain.js ADDED
@@ -0,0 +1,67 @@
1
+ export function sortByPushedAt(repositories, direction = "oldest") {
2
+ return [...repositories].sort((a, b) => {
3
+ const aTime = a.pushedAt ? Date.parse(a.pushedAt) : 0;
4
+ const bTime = b.pushedAt ? Date.parse(b.pushedAt) : 0;
5
+ return direction === "oldest" ? aTime - bTime : bTime - aTime;
6
+ });
7
+ }
8
+ export function formatPushedAt(value) {
9
+ if (!value)
10
+ return "never";
11
+ const date = new Date(value);
12
+ if (Number.isNaN(date.getTime()))
13
+ return value;
14
+ const formatter = new Intl.DateTimeFormat("sv-SE", {
15
+ year: "numeric",
16
+ month: "2-digit",
17
+ day: "2-digit",
18
+ hour: "2-digit",
19
+ minute: "2-digit",
20
+ hour12: false
21
+ });
22
+ return formatter.format(date);
23
+ }
24
+ export function filterRepositories(repositories, options) {
25
+ const normalizedQuery = options.query.trim().toLowerCase();
26
+ return repositories.filter((repository) => {
27
+ if (options.forksOnly && !repository.fork)
28
+ return false;
29
+ if (!normalizedQuery)
30
+ return true;
31
+ return repository.fullName.toLowerCase().includes(normalizedQuery);
32
+ });
33
+ }
34
+ export function baseRiskFlags(repositories) {
35
+ return repositories.flatMap((repository) => {
36
+ const flags = [];
37
+ if (repository.private) {
38
+ flags.push({
39
+ kind: "private",
40
+ repoFullName: repository.fullName,
41
+ message: `${repository.fullName} is private`
42
+ });
43
+ }
44
+ if (!repository.fork) {
45
+ flags.push({
46
+ kind: "non-fork",
47
+ repoFullName: repository.fullName,
48
+ message: `${repository.fullName} is not a fork`
49
+ });
50
+ }
51
+ if (repository.archived) {
52
+ flags.push({
53
+ kind: "archived",
54
+ repoFullName: repository.fullName,
55
+ message: `${repository.fullName} is already archived`
56
+ });
57
+ }
58
+ if (repository.fork && repository.upstreamMissing) {
59
+ flags.push({
60
+ kind: "upstream-missing",
61
+ repoFullName: repository.fullName,
62
+ message: `${repository.fullName} has missing upstream metadata`
63
+ });
64
+ }
65
+ return flags;
66
+ });
67
+ }
package/dist/github.js ADDED
@@ -0,0 +1,130 @@
1
+ import { Octokit } from "@octokit/rest";
2
+ export class OctokitGitHubApi {
3
+ octokit;
4
+ constructor(token) {
5
+ this.octokit = new Octokit({ auth: token, userAgent: "github-repo-cleaner" });
6
+ }
7
+ async getDefaultOwner() {
8
+ const { data } = await this.octokit.rest.users.getAuthenticated();
9
+ return data.login;
10
+ }
11
+ async listRepositories(owner) {
12
+ const defaultOwner = await this.getDefaultOwner();
13
+ const activeOwner = owner ?? defaultOwner;
14
+ const repositories = !owner || activeOwner.toLowerCase() === defaultOwner.toLowerCase()
15
+ ? await this.octokit.paginate(this.octokit.rest.repos.listForAuthenticatedUser, {
16
+ affiliation: "owner",
17
+ per_page: 100,
18
+ sort: "pushed",
19
+ direction: "desc"
20
+ })
21
+ : await this.listForExplicitOwner(activeOwner);
22
+ return {
23
+ owner: activeOwner,
24
+ repositories: repositories
25
+ .filter((repository) => repository.owner.login.toLowerCase() === activeOwner.toLowerCase())
26
+ .map(toRepositorySummary)
27
+ };
28
+ }
29
+ async countOpenPullRequests(repository) {
30
+ const { data } = await this.octokit.rest.search.issuesAndPullRequests({
31
+ q: `repo:${repository.fullName} is:pr is:open`,
32
+ per_page: 1
33
+ });
34
+ return data.total_count;
35
+ }
36
+ async archiveRepository(repository) {
37
+ return this.execute("archive", repository, async () => {
38
+ const response = await this.octokit.rest.repos.update({
39
+ owner: repository.owner,
40
+ repo: repository.name,
41
+ archived: true
42
+ });
43
+ return response.status;
44
+ });
45
+ }
46
+ async deleteRepository(repository) {
47
+ return this.execute("delete", repository, async () => {
48
+ const response = await this.octokit.rest.repos.delete({
49
+ owner: repository.owner,
50
+ repo: repository.name
51
+ });
52
+ return response.status;
53
+ });
54
+ }
55
+ async listForExplicitOwner(owner) {
56
+ const { data: user } = await this.octokit.rest.users.getByUsername({ username: owner });
57
+ if (user.type === "Organization") {
58
+ return this.octokit.paginate(this.octokit.rest.repos.listForOrg, {
59
+ org: owner,
60
+ type: "all",
61
+ per_page: 100,
62
+ sort: "pushed",
63
+ direction: "desc"
64
+ });
65
+ }
66
+ return this.octokit.paginate(this.octokit.rest.repos.listForUser, {
67
+ username: owner,
68
+ type: "owner",
69
+ per_page: 100,
70
+ sort: "pushed",
71
+ direction: "desc"
72
+ });
73
+ }
74
+ async execute(action, repository, call) {
75
+ try {
76
+ const httpStatus = await call();
77
+ return { fullName: repository.fullName, action, status: "success", httpStatus };
78
+ }
79
+ catch (error) {
80
+ const apiError = error;
81
+ return {
82
+ fullName: repository.fullName,
83
+ action,
84
+ status: "failed",
85
+ httpStatus: apiError.status,
86
+ message: formatGitHubApiErrorMessage(apiError)
87
+ };
88
+ }
89
+ }
90
+ }
91
+ function toRepositorySummary(repository) {
92
+ const parent = repository.parent;
93
+ return {
94
+ id: repository.id,
95
+ name: repository.name,
96
+ fullName: repository.full_name,
97
+ owner: repository.owner.login,
98
+ private: repository.private ?? false,
99
+ fork: repository.fork ?? false,
100
+ archived: repository.archived ?? false,
101
+ pushedAt: repository.pushed_at ?? null,
102
+ upstreamFullName: parent?.full_name ?? undefined,
103
+ upstreamMissing: (repository.fork ?? false) && !parent?.full_name
104
+ };
105
+ }
106
+ export async function computeRiskFlags(api, repositories, mode) {
107
+ const { baseRiskFlags } = await import("./domain.js");
108
+ const flags = baseRiskFlags(repositories);
109
+ if (mode !== "delete")
110
+ return flags;
111
+ const openPrCounts = await Promise.all(repositories
112
+ .filter((repository) => repository.fork)
113
+ .map(async (repository) => ({ repository, count: await api.countOpenPullRequests(repository) })));
114
+ for (const { repository, count } of openPrCounts) {
115
+ if (count > 0) {
116
+ flags.push({
117
+ kind: "open-prs",
118
+ repoFullName: repository.fullName,
119
+ message: `${repository.fullName} has ${count} open pull request${count === 1 ? "" : "s"}`
120
+ });
121
+ }
122
+ }
123
+ return flags;
124
+ }
125
+ export function formatGitHubApiErrorMessage(error) {
126
+ const permissionHint = error.status === 403
127
+ ? " Try gh auth refresh -h github.com -s delete_repo if this is a permission issue."
128
+ : "";
129
+ return `${error.message ?? "Unknown GitHub API error"}${permissionHint}`;
130
+ }
@@ -0,0 +1,34 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ export function defaultSessionLogDirectory() {
5
+ return join(homedir(), ".local", "state", "github-repo-cleaner", "sessions");
6
+ }
7
+ export async function writeSessionLog(options) {
8
+ const directory = options.directory ?? defaultSessionLogDirectory();
9
+ await mkdir(directory, { recursive: true });
10
+ const resultByName = new Map(options.results.map((result) => [result.fullName, result]));
11
+ const log = {
12
+ owner: options.owner,
13
+ mode: options.mode,
14
+ startedAt: options.startedAt.toISOString(),
15
+ completedAt: options.completedAt.toISOString(),
16
+ items: options.repositories.map((repository) => {
17
+ const result = resultByName.get(repository.fullName);
18
+ return {
19
+ fullName: repository.fullName,
20
+ fork: repository.fork,
21
+ private: repository.private,
22
+ archived: repository.archived,
23
+ action: options.mode,
24
+ status: result?.status ?? "failed",
25
+ httpStatus: result?.httpStatus,
26
+ message: result?.message
27
+ };
28
+ })
29
+ };
30
+ const fileName = `${options.startedAt.toISOString().replace(/[:.]/g, "-")}.json`;
31
+ const path = join(directory, fileName);
32
+ await writeFile(path, `${JSON.stringify(log, null, 2)}\n`, "utf8");
33
+ return path;
34
+ }
package/dist/ui.js ADDED
@@ -0,0 +1,170 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { Box, Text, useApp, useInput } from "ink";
4
+ import { computeRiskFlags } from "./github.js";
5
+ import { filterRepositories, formatPushedAt, sortByPushedAt } from "./domain.js";
6
+ import { writeSessionLog } from "./session-log.js";
7
+ export function App({ api, owner: requestedOwner, writeLog = writeSessionLog }) {
8
+ const { exit } = useApp();
9
+ const [view, setView] = useState("loading");
10
+ const [owner, setOwner] = useState(requestedOwner ?? "");
11
+ const [repositories, setRepositories] = useState([]);
12
+ const [cursor, setCursor] = useState(0);
13
+ const [selectedIds, setSelectedIds] = useState(new Set());
14
+ const [forksOnly, setForksOnly] = useState(false);
15
+ const [sortDirection, setSortDirection] = useState("oldest");
16
+ const [query, setQuery] = useState("");
17
+ const [searching, setSearching] = useState(false);
18
+ const [mode, setMode] = useState("archive");
19
+ const [riskFlags, setRiskFlags] = useState([]);
20
+ const [riskFlagsLoading, setRiskFlagsLoading] = useState(false);
21
+ const [results, setResults] = useState([]);
22
+ const [logPath, setLogPath] = useState();
23
+ const [error, setError] = useState();
24
+ useEffect(() => {
25
+ let active = true;
26
+ api
27
+ .listRepositories(requestedOwner)
28
+ .then((response) => {
29
+ if (!active)
30
+ return;
31
+ setOwner(response.owner);
32
+ setRepositories(response.repositories);
33
+ setView("list");
34
+ })
35
+ .catch((loadError) => {
36
+ if (!active)
37
+ return;
38
+ const message = loadError instanceof Error ? loadError.message : "Unable to load repositories";
39
+ setError(message);
40
+ setView("error");
41
+ });
42
+ return () => {
43
+ active = false;
44
+ };
45
+ }, [api, requestedOwner]);
46
+ const visibleRepositories = useMemo(() => filterRepositories(sortByPushedAt(repositories, sortDirection), { query, forksOnly }), [repositories, sortDirection, query, forksOnly]);
47
+ const selectedRepositories = useMemo(() => repositories.filter((repository) => selectedIds.has(repository.id)), [repositories, selectedIds]);
48
+ const selectedRepositoriesRef = useRef([]);
49
+ const viewRef = useRef(view);
50
+ const modeRef = useRef(mode);
51
+ const ownerRef = useRef(owner);
52
+ const riskFlagsLoadingRef = useRef(riskFlagsLoading);
53
+ useEffect(() => {
54
+ selectedRepositoriesRef.current = selectedRepositories;
55
+ viewRef.current = view;
56
+ modeRef.current = mode;
57
+ ownerRef.current = owner;
58
+ riskFlagsLoadingRef.current = riskFlagsLoading;
59
+ }, [selectedRepositories, view, mode, owner, riskFlagsLoading]);
60
+ useInput((input, key) => {
61
+ if (input === "q") {
62
+ exit();
63
+ return;
64
+ }
65
+ const isReturn = key.return || input === "\r" || input === "\n";
66
+ if (viewRef.current === "list") {
67
+ if (searching) {
68
+ if (isReturn) {
69
+ setSearching(false);
70
+ }
71
+ else if (key.backspace || key.delete) {
72
+ setQuery((current) => current.slice(0, -1));
73
+ }
74
+ else if (input && !key.ctrl && !key.meta) {
75
+ setQuery((current) => `${current}${input}`);
76
+ }
77
+ return;
78
+ }
79
+ if (key.upArrow)
80
+ setCursor((current) => Math.max(0, current - 1));
81
+ if (key.downArrow)
82
+ setCursor((current) => Math.min(visibleRepositories.length - 1, current + 1));
83
+ if (input === "f")
84
+ setForksOnly((current) => !current);
85
+ if (input === "s")
86
+ setSortDirection((current) => current === "oldest" ? "newest" : "oldest");
87
+ if (input === "/")
88
+ setSearching(true);
89
+ if (input === "a")
90
+ setMode("archive");
91
+ if (input === "d")
92
+ setMode("delete");
93
+ if (input === " " && visibleRepositories[cursor]) {
94
+ const repository = visibleRepositories[cursor];
95
+ setSelectedIds((current) => {
96
+ const next = new Set(current);
97
+ if (next.has(repository.id))
98
+ next.delete(repository.id);
99
+ else
100
+ next.add(repository.id);
101
+ return next;
102
+ });
103
+ }
104
+ if (isReturn && selectedRepositoriesRef.current.length > 0) {
105
+ setRiskFlags([]);
106
+ setRiskFlagsLoading(true);
107
+ setView("review");
108
+ void computeRiskFlags(api, selectedRepositoriesRef.current, modeRef.current).then((flags) => {
109
+ setRiskFlags(flags);
110
+ setRiskFlagsLoading(false);
111
+ });
112
+ }
113
+ return;
114
+ }
115
+ if (viewRef.current === "review") {
116
+ if (key.escape) {
117
+ setView("list");
118
+ return;
119
+ }
120
+ if (isReturn && !riskFlagsLoadingRef.current) {
121
+ void execute();
122
+ }
123
+ }
124
+ });
125
+ async function execute() {
126
+ const startedAt = new Date();
127
+ setView("executing");
128
+ const operationResults = [];
129
+ const repositoriesToExecute = selectedRepositoriesRef.current;
130
+ const modeToExecute = modeRef.current;
131
+ for (const repository of repositoriesToExecute) {
132
+ const result = modeToExecute === "archive" ? await api.archiveRepository(repository) : await api.deleteRepository(repository);
133
+ operationResults.push(result);
134
+ setResults([...operationResults]);
135
+ }
136
+ const completedAt = new Date();
137
+ const path = await writeLog({
138
+ owner: ownerRef.current,
139
+ mode: modeToExecute,
140
+ startedAt,
141
+ completedAt,
142
+ repositories: repositoriesToExecute,
143
+ results: operationResults
144
+ });
145
+ setLogPath(path);
146
+ setResults(operationResults);
147
+ setView("result");
148
+ }
149
+ if (view === "loading")
150
+ return _jsx(Text, { children: "Loading repositories..." });
151
+ if (view === "error")
152
+ return _jsx(Text, { color: "red", children: error });
153
+ if (view === "review")
154
+ return _jsx(ReviewView, { mode: mode, repositories: selectedRepositories, riskFlags: riskFlags, loading: riskFlagsLoading });
155
+ if (view === "executing")
156
+ return _jsxs(Text, { children: ["Executing ", mode, " for ", selectedRepositories.length, " repositories..."] });
157
+ if (view === "result")
158
+ return _jsx(ResultView, { results: results, logPath: logPath });
159
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["github-repo-cleaner owner:", owner, " mode:", mode, " sort:", sortDirection, " selected:", selectedIds.size] }), _jsxs(Text, { children: ["[up/down] move [space] select [f] forks only:", forksOnly ? "on" : "off", " [s] sort [/] search [a] archive [d] delete [enter] review [q] quit"] }), _jsxs(Text, { children: ["search:", searching ? `${query}_` : query || "-"] }), visibleRepositories.slice(Math.max(0, cursor - 5), cursor + 15).map((repository) => {
160
+ const active = visibleRepositories[cursor]?.id === repository.id;
161
+ const selected = selectedIds.has(repository.id);
162
+ return (_jsxs(Text, { color: active ? "cyan" : undefined, children: [active ? ">" : " ", " ", selected ? "[x]" : "[ ]", " ", repository.fork ? "fork" : "own ", " ", repository.fullName, " pushed:", formatPushedAt(repository.pushedAt), " ", repository.fork ? `upstream:${repository.upstreamFullName ?? "missing"}` : ""] }, repository.id));
163
+ }), visibleRepositories.length === 0 ? _jsx(Text, { children: "No repositories match the current filters." }) : null] }));
164
+ }
165
+ function ReviewView({ mode, repositories, riskFlags, loading }) {
166
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [mode.toUpperCase(), " ", repositories.length, " repositories"] }), repositories.slice(0, 12).map((repository) => _jsxs(Text, { children: ["- ", repository.fullName] }, repository.id)), repositories.length > 12 ? _jsxs(Text, { children: ["... ", repositories.length - 12, " more"] }) : null, _jsx(Text, { children: "Risk flags:" }), loading ? _jsx(Text, { children: "Checking risks..." }) : riskFlags.length === 0 ? _jsx(Text, { children: "None" }) : riskFlags.map((flag) => _jsxs(Text, { color: "yellow", children: ["! ", flag.message] }, `${flag.kind}:${flag.repoFullName}`)), _jsx(Text, { children: loading ? "[enter] waiting for risk check [esc] cancel" : "[enter] confirm [esc] cancel" })] }));
167
+ }
168
+ function ResultView({ results, logPath }) {
169
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "Results" }), results.map((result) => (_jsxs(Text, { color: result.status === "success" ? "green" : "red", children: [result.status === "success" ? "ok" : "fail", " ", result.action, " ", result.fullName, " ", result.httpStatus ?? "", " ", result.message ?? ""] }, result.fullName))), logPath ? _jsxs(Text, { children: ["Log: ", logPath] }) : null] }));
170
+ }
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@kazoottt/github-repo-cleaner",
3
+ "version": "0.1.0",
4
+ "description": "Local TUI for cleaning GitHub repositories with gh authentication.",
5
+ "type": "module",
6
+ "bin": {
7
+ "github-repo-cleaner": "dist/cli.js",
8
+ "repo-cleaner": "dist/cli.js"
9
+ },
10
+ "scripts": {
11
+ "dev": "tsx src/cli.tsx",
12
+ "build": "tsc -p tsconfig.json",
13
+ "typecheck": "tsc -p tsconfig.json --noEmit",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "prepack": "npm run build",
17
+ "release": "npm test && npm run typecheck && npm run build && bumpp --commit \"chore(release): v%s\" --tag \"v%s\" --push"
18
+ },
19
+ "dependencies": {
20
+ "@octokit/rest": "latest",
21
+ "ink": "latest",
22
+ "react": "latest"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "latest",
26
+ "@types/react": "latest",
27
+ "bumpp": "^11.1.0",
28
+ "ink-testing-library": "latest",
29
+ "tsx": "latest",
30
+ "typescript": "latest",
31
+ "vitest": "latest"
32
+ },
33
+ "engines": {
34
+ "node": ">=20"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/KazooTTT/github-repo-cleaner.git"
47
+ },
48
+ "homepage": "https://github.com/KazooTTT/github-repo-cleaner#readme",
49
+ "bugs": {
50
+ "url": "https://github.com/KazooTTT/github-repo-cleaner/issues"
51
+ },
52
+ "license": "MIT"
53
+ }