gh-coderakr 0.0.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/app.d.ts +6 -0
- package/dist/app.js +103 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +21 -0
- package/dist/components/error-message.d.ts +7 -0
- package/dist/components/error-message.js +10 -0
- package/dist/components/layout.d.ts +9 -0
- package/dist/components/layout.js +11 -0
- package/dist/components/repo-details.d.ts +7 -0
- package/dist/components/repo-details.js +86 -0
- package/dist/components/spinner.d.ts +6 -0
- package/dist/components/spinner.js +19 -0
- package/dist/github.d.ts +22 -0
- package/dist/github.js +29 -0
- package/package.json +65 -0
- package/readme.md +25 -0
package/dist/app.d.ts
ADDED
package/dist/app.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import TextInput from 'ink-text-input';
|
|
4
|
+
import fetch from 'node-fetch';
|
|
5
|
+
import { AppLayout } from './components/layout.js';
|
|
6
|
+
import { Spinner } from './components/spinner.js';
|
|
7
|
+
import { ErrorMessage } from './components/error-message.js';
|
|
8
|
+
import { RepoDetails } from './components/repo-details.js';
|
|
9
|
+
import { normalizeRepoInput } from './github.js';
|
|
10
|
+
export default function App({ repo }) {
|
|
11
|
+
const initialTarget = repo ? normalizeRepoInput(repo) : '';
|
|
12
|
+
const [input, setInput] = useState('');
|
|
13
|
+
const [targetRepo, setTargetRepo] = useState(initialTarget);
|
|
14
|
+
const [data, setData] = useState();
|
|
15
|
+
const [error, setError] = useState();
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
const [reloadToken, setReloadToken] = useState(0);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!targetRepo || targetRepo.trim() === '') {
|
|
20
|
+
setLoading(false);
|
|
21
|
+
setData(undefined);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
let didCancel = false;
|
|
25
|
+
async function load() {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError(undefined);
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(`https://api.github.com/repos/${targetRepo}`, {
|
|
30
|
+
headers: {
|
|
31
|
+
Accept: 'application/vnd.github+json',
|
|
32
|
+
'User-Agent': 'inkcli',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const message = response.status === 404 ? 'Repository not found.' : `GitHub API error: ${response.status} ${response.statusText}`;
|
|
37
|
+
if (!didCancel) {
|
|
38
|
+
setError(message);
|
|
39
|
+
setData(undefined);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const json = (await response.json());
|
|
44
|
+
if (!didCancel) {
|
|
45
|
+
setData(json);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error_) {
|
|
49
|
+
if (!didCancel) {
|
|
50
|
+
setError(error_ instanceof Error ? error_.message : 'Unknown error while fetching repository.');
|
|
51
|
+
setData(undefined);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
if (!didCancel) {
|
|
56
|
+
setLoading(false);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
load();
|
|
61
|
+
return () => {
|
|
62
|
+
didCancel = true;
|
|
63
|
+
};
|
|
64
|
+
}, [targetRepo, reloadToken]);
|
|
65
|
+
const handleSubmit = (value) => {
|
|
66
|
+
const raw = value || input;
|
|
67
|
+
const normalized = normalizeRepoInput(raw);
|
|
68
|
+
if (!normalized) {
|
|
69
|
+
setError('Please enter a GitHub URL or owner/repo.');
|
|
70
|
+
setData(undefined);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
setError(undefined);
|
|
74
|
+
setTargetRepo(normalized);
|
|
75
|
+
};
|
|
76
|
+
useInput((inputKey, key) => {
|
|
77
|
+
if (key.escape || inputKey === 'q') {
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
if (inputKey === 'r') {
|
|
81
|
+
if (targetRepo && targetRepo.trim() !== '') {
|
|
82
|
+
setReloadToken(previous => previous + 1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return (React.createElement(AppLayout, { title: "GitHub Repo Viewer", subtitle: "Enter URL or owner/repo, then press Enter", footer: React.createElement(Text, { color: "gray" }, "Keys: Enter=load r=reload q/Esc=quit Ctrl+C=force quit") },
|
|
87
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
88
|
+
React.createElement(Text, { color: "gray" },
|
|
89
|
+
"Input:",
|
|
90
|
+
' '),
|
|
91
|
+
React.createElement(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit, showCursor: false, placeholder: "owner/repo or GitHub URL" })),
|
|
92
|
+
targetRepo && targetRepo.trim() !== '' && (React.createElement(Box, { marginBottom: 1 },
|
|
93
|
+
React.createElement(Text, { color: "gray" },
|
|
94
|
+
"Current:",
|
|
95
|
+
' '),
|
|
96
|
+
React.createElement(Text, { color: "greenBright" }, targetRepo))),
|
|
97
|
+
React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
|
|
98
|
+
loading && (React.createElement(Box, { marginBottom: 1 },
|
|
99
|
+
React.createElement(Spinner, { label: `Fetching ${targetRepo} from GitHub...` }))),
|
|
100
|
+
error && React.createElement(ErrorMessage, { targetRepo: targetRepo, error: error }),
|
|
101
|
+
!loading && !error && data && React.createElement(RepoDetails, { repo: data }),
|
|
102
|
+
!loading && !error && !data && (React.createElement(Text, { color: "gray" }, "Paste a GitHub URL or owner/repo above and press Enter to load details.")))));
|
|
103
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import meow from 'meow';
|
|
5
|
+
import App from './app.js';
|
|
6
|
+
const cli = meow(`
|
|
7
|
+
Usage
|
|
8
|
+
$ inkcli <owner>/<repo>|<github-url>
|
|
9
|
+
|
|
10
|
+
Examples
|
|
11
|
+
$ inkcli kubernetes/kubernetes
|
|
12
|
+
$ inkcli coderakr/CarRental
|
|
13
|
+
$ inkcli https://github.com/kubernetes/kubernetes
|
|
14
|
+
$ inkcli https://api.github.com/repos/kubernetes/kubernetes
|
|
15
|
+
|
|
16
|
+
If no argument is provided, it defaults to kubernetes/kubernetes.
|
|
17
|
+
`, {
|
|
18
|
+
importMeta: import.meta,
|
|
19
|
+
});
|
|
20
|
+
const [repo] = cli.input;
|
|
21
|
+
render(React.createElement(App, { repo: repo }));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function ErrorMessage({ targetRepo, error }) {
|
|
4
|
+
return (React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
5
|
+
React.createElement(Text, { color: "redBright" }, "\u2716 Failed to load repository"),
|
|
6
|
+
targetRepo && (React.createElement(Text, { color: "gray" },
|
|
7
|
+
"Target: ",
|
|
8
|
+
targetRepo)),
|
|
9
|
+
React.createElement(Text, { color: "red" }, error)));
|
|
10
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React, { type ReactNode } from 'react';
|
|
2
|
+
type AppLayoutProps = {
|
|
3
|
+
title: string;
|
|
4
|
+
subtitle?: string;
|
|
5
|
+
footer?: ReactNode;
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
};
|
|
8
|
+
export declare function AppLayout({ title, subtitle, footer, children }: AppLayoutProps): React.JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function AppLayout({ title, subtitle, footer, children }) {
|
|
4
|
+
return (React.createElement(Box, { paddingX: 1, paddingY: 1 },
|
|
5
|
+
React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyanBright", paddingX: 2, paddingY: 1 },
|
|
6
|
+
React.createElement(Box, { marginBottom: 1, justifyContent: "space-between" },
|
|
7
|
+
React.createElement(Text, { color: "cyanBright" }, title),
|
|
8
|
+
subtitle && (React.createElement(Text, { color: "gray" }, subtitle))),
|
|
9
|
+
React.createElement(Box, { flexDirection: "column" }, children),
|
|
10
|
+
footer && (React.createElement(Box, { marginTop: 1 }, footer)))));
|
|
11
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
export function RepoDetails({ repo }) {
|
|
4
|
+
const createdAt = new Date(repo.created_at);
|
|
5
|
+
const updatedAt = new Date(repo.updated_at);
|
|
6
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
7
|
+
React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
8
|
+
React.createElement(Text, null,
|
|
9
|
+
React.createElement(Text, { color: "gray" },
|
|
10
|
+
"Name:",
|
|
11
|
+
' '),
|
|
12
|
+
React.createElement(Text, { color: "greenBright" }, repo.full_name)),
|
|
13
|
+
React.createElement(Text, null,
|
|
14
|
+
React.createElement(Text, { color: "gray" },
|
|
15
|
+
"Owner:",
|
|
16
|
+
' '),
|
|
17
|
+
React.createElement(Text, { color: "magentaBright" }, repo.owner.login)),
|
|
18
|
+
repo.description && (React.createElement(Text, { wrap: "truncate" },
|
|
19
|
+
React.createElement(Text, { color: "gray" },
|
|
20
|
+
"Description:",
|
|
21
|
+
' '),
|
|
22
|
+
repo.description))),
|
|
23
|
+
React.createElement(Box, { flexDirection: "row", marginBottom: 1, flexWrap: "wrap" },
|
|
24
|
+
React.createElement(Box, { flexDirection: "column", marginRight: 4 },
|
|
25
|
+
React.createElement(Text, { color: "yellowBright" }, "\u2605 Stars:"),
|
|
26
|
+
React.createElement(Text, null, repo.stargazers_count)),
|
|
27
|
+
React.createElement(Box, { flexDirection: "column", marginRight: 4 },
|
|
28
|
+
React.createElement(Text, { color: "cyanBright" }, "\u2442 Forks:"),
|
|
29
|
+
React.createElement(Text, null, repo.forks_count)),
|
|
30
|
+
React.createElement(Box, { flexDirection: "column", marginRight: 4 },
|
|
31
|
+
React.createElement(Text, { color: "redBright" }, "\u26A0 Issues:"),
|
|
32
|
+
React.createElement(Text, null, repo.open_issues_count)),
|
|
33
|
+
React.createElement(Box, { flexDirection: "column" },
|
|
34
|
+
React.createElement(Text, { color: "blueBright" }, "\uD83D\uDC41 Watchers:"),
|
|
35
|
+
React.createElement(Text, null, repo.watchers_count))),
|
|
36
|
+
React.createElement(Box, { flexDirection: "row", marginBottom: 1, flexWrap: "wrap" },
|
|
37
|
+
React.createElement(Box, { marginRight: 4 },
|
|
38
|
+
React.createElement(Text, null,
|
|
39
|
+
React.createElement(Text, { color: "gray" },
|
|
40
|
+
"Language:",
|
|
41
|
+
' '),
|
|
42
|
+
repo.language ?? 'N/A')),
|
|
43
|
+
React.createElement(Box, { marginRight: 4 },
|
|
44
|
+
React.createElement(Text, null,
|
|
45
|
+
React.createElement(Text, { color: "gray" },
|
|
46
|
+
"License:",
|
|
47
|
+
' '),
|
|
48
|
+
repo.license?.name ?? 'Unlicensed')),
|
|
49
|
+
React.createElement(Box, null,
|
|
50
|
+
React.createElement(Text, null,
|
|
51
|
+
React.createElement(Text, { color: "gray" },
|
|
52
|
+
"Default branch:",
|
|
53
|
+
' '),
|
|
54
|
+
repo.default_branch))),
|
|
55
|
+
React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
56
|
+
React.createElement(Text, null,
|
|
57
|
+
React.createElement(Text, { color: "gray" },
|
|
58
|
+
"Created:",
|
|
59
|
+
' '),
|
|
60
|
+
createdAt.toLocaleString()),
|
|
61
|
+
React.createElement(Text, null,
|
|
62
|
+
React.createElement(Text, { color: "gray" },
|
|
63
|
+
"Last updated:",
|
|
64
|
+
' '),
|
|
65
|
+
updatedAt.toLocaleString())),
|
|
66
|
+
React.createElement(Box, { flexDirection: "column", marginBottom: 1 },
|
|
67
|
+
React.createElement(Text, null,
|
|
68
|
+
React.createElement(Text, { color: "gray" },
|
|
69
|
+
"GitHub:",
|
|
70
|
+
' '),
|
|
71
|
+
React.createElement(Text, { color: "blueBright" }, repo.html_url)),
|
|
72
|
+
repo.homepage && (React.createElement(Text, null,
|
|
73
|
+
React.createElement(Text, { color: "gray" },
|
|
74
|
+
"Homepage:",
|
|
75
|
+
' '),
|
|
76
|
+
React.createElement(Text, { color: "greenBright" }, repo.homepage)))),
|
|
77
|
+
repo.topics && repo.topics.length > 0 && (React.createElement(Box, { flexDirection: "row", flexWrap: "wrap" },
|
|
78
|
+
React.createElement(Text, { color: "gray" },
|
|
79
|
+
"Topics:",
|
|
80
|
+
' '),
|
|
81
|
+
React.createElement(Box, { marginLeft: 1, flexDirection: "row", flexWrap: "wrap" }, repo.topics.map(topic => (React.createElement(Box, { key: topic, marginRight: 1 },
|
|
82
|
+
React.createElement(Text, { color: "black", backgroundColor: "gray" },
|
|
83
|
+
' ',
|
|
84
|
+
topic,
|
|
85
|
+
' ')))))))));
|
|
86
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
4
|
+
export function Spinner({ label }) {
|
|
5
|
+
const [frame, setFrame] = useState(0);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const timer = setInterval(() => {
|
|
8
|
+
setFrame(previous => (previous + 1) % SPINNER_FRAMES.length);
|
|
9
|
+
}, 80);
|
|
10
|
+
return () => {
|
|
11
|
+
clearInterval(timer);
|
|
12
|
+
};
|
|
13
|
+
}, []);
|
|
14
|
+
return (React.createElement(Box, null,
|
|
15
|
+
React.createElement(Text, { color: "cyan" },
|
|
16
|
+
SPINNER_FRAMES[frame],
|
|
17
|
+
" ",
|
|
18
|
+
label)));
|
|
19
|
+
}
|
package/dist/github.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type GithubRepo = {
|
|
2
|
+
full_name: string;
|
|
3
|
+
description: string | null;
|
|
4
|
+
html_url: string;
|
|
5
|
+
homepage: string | null;
|
|
6
|
+
language: string | null;
|
|
7
|
+
stargazers_count: number;
|
|
8
|
+
forks_count: number;
|
|
9
|
+
open_issues_count: number;
|
|
10
|
+
watchers_count: number;
|
|
11
|
+
created_at: string;
|
|
12
|
+
updated_at: string;
|
|
13
|
+
default_branch: string;
|
|
14
|
+
license: {
|
|
15
|
+
name: string;
|
|
16
|
+
} | null;
|
|
17
|
+
owner: {
|
|
18
|
+
login: string;
|
|
19
|
+
};
|
|
20
|
+
topics?: string[];
|
|
21
|
+
};
|
|
22
|
+
export declare function normalizeRepoInput(raw?: string): string;
|
package/dist/github.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function normalizeRepoInput(raw) {
|
|
2
|
+
if (!raw) {
|
|
3
|
+
return '';
|
|
4
|
+
}
|
|
5
|
+
const input = raw.trim();
|
|
6
|
+
if (input === '') {
|
|
7
|
+
return '';
|
|
8
|
+
}
|
|
9
|
+
if (input.startsWith('http://') || input.startsWith('https://')) {
|
|
10
|
+
try {
|
|
11
|
+
const url = new URL(input);
|
|
12
|
+
const host = url.hostname.toLowerCase();
|
|
13
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
14
|
+
if (host === 'github.com' && segments.length >= 2) {
|
|
15
|
+
return `${segments[0]}/${segments[1]}`;
|
|
16
|
+
}
|
|
17
|
+
if (host === 'api.github.com') {
|
|
18
|
+
const startIndex = segments[0] === 'repos' ? 1 : 0;
|
|
19
|
+
if (segments.length >= startIndex + 2) {
|
|
20
|
+
return `${segments[startIndex]}/${segments[startIndex + 1]}`;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Ignore malformed URLs and fall through.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return input;
|
|
29
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Beautiful GitHub repo CLI built with Ink",
|
|
3
|
+
"author": "Ayush Kumar",
|
|
4
|
+
"keywords": [
|
|
5
|
+
"cli",
|
|
6
|
+
"ink",
|
|
7
|
+
"github",
|
|
8
|
+
"tui"
|
|
9
|
+
],
|
|
10
|
+
"name": "gh-coderakr",
|
|
11
|
+
"version": "0.0.0",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"bin": "dist/cli.js",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=16"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"test": "prettier --check . && xo && ava"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"ink": "^4.1.0",
|
|
28
|
+
"meow": "^11.0.0",
|
|
29
|
+
"react": "^18.2.0",
|
|
30
|
+
"node-fetch": "^3.3.2",
|
|
31
|
+
"ink-text-input": "^5.0.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@sindresorhus/tsconfig": "^3.0.1",
|
|
35
|
+
"@types/react": "^18.0.32",
|
|
36
|
+
"@vdemedes/prettier-config": "^2.0.1",
|
|
37
|
+
"ava": "^5.2.0",
|
|
38
|
+
"chalk": "^5.2.0",
|
|
39
|
+
"eslint-config-xo-react": "^0.27.0",
|
|
40
|
+
"eslint-plugin-react": "^7.32.2",
|
|
41
|
+
"eslint-plugin-react-hooks": "^4.6.0",
|
|
42
|
+
"ink-testing-library": "^3.0.0",
|
|
43
|
+
"prettier": "^2.8.7",
|
|
44
|
+
"ts-node": "^10.9.1",
|
|
45
|
+
"typescript": "^5.0.3",
|
|
46
|
+
"xo": "^0.53.1"
|
|
47
|
+
},
|
|
48
|
+
"ava": {
|
|
49
|
+
"extensions": {
|
|
50
|
+
"ts": "module",
|
|
51
|
+
"tsx": "module"
|
|
52
|
+
},
|
|
53
|
+
"nodeArguments": [
|
|
54
|
+
"--loader=ts-node/esm"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"xo": {
|
|
58
|
+
"extends": "xo-react",
|
|
59
|
+
"prettier": true,
|
|
60
|
+
"rules": {
|
|
61
|
+
"react/prop-types": "off"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"prettier": "@vdemedes/prettier-config"
|
|
65
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# inkcli
|
|
2
|
+
|
|
3
|
+
> This readme is automatically generated by [create-ink-app](https://github.com/vadimdemedes/create-ink-app)
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
$ npm install --global inkcli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## CLI
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
$ inkcli --help
|
|
15
|
+
|
|
16
|
+
Usage
|
|
17
|
+
$ inkcli
|
|
18
|
+
|
|
19
|
+
Options
|
|
20
|
+
--name Your name
|
|
21
|
+
|
|
22
|
+
Examples
|
|
23
|
+
$ inkcli --name=Jane
|
|
24
|
+
Hello, Jane
|
|
25
|
+
```
|