fangge 1.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/README.md +23 -0
- package/dist/cli.js +237 -0
- package/package.json +40 -0
package/README.md
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// source/cli.tsx
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
|
|
6
|
+
// source/app.tsx
|
|
7
|
+
import { Box as Box4, Text as Text4, useApp, useInput as useInput2 } from "ink";
|
|
8
|
+
|
|
9
|
+
// source/data.ts
|
|
10
|
+
var profile = {
|
|
11
|
+
name: "Fango",
|
|
12
|
+
fullName: { first: "Fangge", last: "Zhao" },
|
|
13
|
+
title: "Solo Builder & AI Engineer",
|
|
14
|
+
tagline: "Building ListenHub \u2014 turn anything into podcasts, speech & video."
|
|
15
|
+
};
|
|
16
|
+
var experiences = [
|
|
17
|
+
{
|
|
18
|
+
company: "MarsWave AI",
|
|
19
|
+
location: "Beijing",
|
|
20
|
+
title: "Full Stack Engineer",
|
|
21
|
+
from: "2025",
|
|
22
|
+
to: "Present"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
company: "TikTok",
|
|
26
|
+
location: "Beijing",
|
|
27
|
+
title: "Sr. Frontend Engineer",
|
|
28
|
+
from: "2024",
|
|
29
|
+
to: "2025"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
company: "DiDi",
|
|
33
|
+
location: "Beijing",
|
|
34
|
+
title: "Sr. Frontend Engineer",
|
|
35
|
+
from: "2021",
|
|
36
|
+
to: "2023"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
company: "SF Express",
|
|
40
|
+
location: "Beijing",
|
|
41
|
+
title: "Frontend Engineer",
|
|
42
|
+
from: "2018",
|
|
43
|
+
to: "2021"
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
var projects = [
|
|
47
|
+
{ name: "ListenHub", description: "AI audio platform", href: "https://listenhub.ai" },
|
|
48
|
+
{ name: "LH Skills", description: "AI skills for Claude Code", href: "https://github.com/marswaveai/skills" },
|
|
49
|
+
{ name: "LH SDK", description: "TypeScript SDK", href: "https://github.com/marswaveai/listenhub-sdk" },
|
|
50
|
+
{ name: "fango-cli", description: "This terminal card", href: "https://github.com/0xFANGO/fango-cli" }
|
|
51
|
+
];
|
|
52
|
+
var education = {
|
|
53
|
+
school: "Beijing Univ. of Posts & Telecom",
|
|
54
|
+
degree: "B.S.",
|
|
55
|
+
from: "2014",
|
|
56
|
+
to: "2018"
|
|
57
|
+
};
|
|
58
|
+
var links = [
|
|
59
|
+
{ label: "ListenHub", href: "https://listenhub.ai", display: "listenhub.ai" },
|
|
60
|
+
{ label: "LH Skills", href: "https://github.com/marswaveai/skills", display: "github.com/marswaveai/skills" },
|
|
61
|
+
{ label: "LH SDK", href: "https://github.com/marswaveai/listenhub-sdk", display: "github.com/marswaveai/listenhub-sdk" },
|
|
62
|
+
{ label: "GitHub", href: "https://github.com/0xFANGO", display: "github.com/0xFANGO" },
|
|
63
|
+
{ label: "Email", href: "mailto:silencerichard@163.com", display: "silencerichard@163.com" },
|
|
64
|
+
{ label: "Website", href: "https://fango.blog", display: "fango.blog" }
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
// source/components/experience.tsx
|
|
68
|
+
import { Box, Text } from "ink";
|
|
69
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
70
|
+
function truncate(str, max) {
|
|
71
|
+
return str.length > max ? str.slice(0, max - 1) + "\u2026" : str;
|
|
72
|
+
}
|
|
73
|
+
var Experience = ({ experiences: experiences2 }) => /* @__PURE__ */ jsxs(
|
|
74
|
+
Box,
|
|
75
|
+
{
|
|
76
|
+
flexDirection: "column",
|
|
77
|
+
borderStyle: "round",
|
|
78
|
+
paddingX: 1,
|
|
79
|
+
paddingY: 1,
|
|
80
|
+
children: [
|
|
81
|
+
/* @__PURE__ */ jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, color: "red", children: "Experience" }) }),
|
|
82
|
+
experiences2.map((exp) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
|
|
83
|
+
/* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
|
|
84
|
+
/* @__PURE__ */ jsx(Text, { bold: true, color: "white", children: truncate(exp.company, 20) }),
|
|
85
|
+
/* @__PURE__ */ jsx(Text, { children: truncate(exp.title, 22) })
|
|
86
|
+
] }),
|
|
87
|
+
/* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
|
|
88
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: exp.location }),
|
|
89
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
90
|
+
exp.from,
|
|
91
|
+
" \u2013 ",
|
|
92
|
+
exp.to
|
|
93
|
+
] })
|
|
94
|
+
] })
|
|
95
|
+
] }, `${exp.company}-${exp.from}`))
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// source/components/projects.tsx
|
|
101
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
102
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
103
|
+
function truncate2(str, max) {
|
|
104
|
+
return str.length > max ? str.slice(0, max - 1) + "\u2026" : str;
|
|
105
|
+
}
|
|
106
|
+
var Projects = ({ projects: projects2 }) => /* @__PURE__ */ jsxs2(
|
|
107
|
+
Box2,
|
|
108
|
+
{
|
|
109
|
+
flexDirection: "column",
|
|
110
|
+
borderStyle: "round",
|
|
111
|
+
paddingX: 1,
|
|
112
|
+
paddingY: 1,
|
|
113
|
+
children: [
|
|
114
|
+
/* @__PURE__ */ jsx2(Box2, { marginBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { bold: true, color: "red", children: "Projects" }) }),
|
|
115
|
+
projects2.map((proj) => /* @__PURE__ */ jsxs2(Box2, { gap: 1, children: [
|
|
116
|
+
/* @__PURE__ */ jsx2(Text2, { bold: true, color: "white", children: proj.name }),
|
|
117
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\xB7" }),
|
|
118
|
+
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: truncate2(proj.description, 30) })
|
|
119
|
+
] }, proj.name))
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// source/components/links.tsx
|
|
125
|
+
import { Box as Box3, Text as Text3, useInput } from "ink";
|
|
126
|
+
import { useState, useCallback, useRef } from "react";
|
|
127
|
+
import open from "open";
|
|
128
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
129
|
+
function truncate3(str, max) {
|
|
130
|
+
return str.length > max ? str.slice(0, max - 1) + "\u2026" : str;
|
|
131
|
+
}
|
|
132
|
+
var Links = ({ links: links2 }) => {
|
|
133
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
134
|
+
const [successHint, setSuccessHint] = useState(false);
|
|
135
|
+
const [failedUrl, setFailedUrl] = useState(null);
|
|
136
|
+
const lastOpenTime = useRef(0);
|
|
137
|
+
const handleSelect = useCallback(async () => {
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
if (now - lastOpenTime.current < 500) return;
|
|
140
|
+
lastOpenTime.current = now;
|
|
141
|
+
const link = links2[selectedIndex];
|
|
142
|
+
try {
|
|
143
|
+
await open(link.href);
|
|
144
|
+
setSuccessHint(true);
|
|
145
|
+
setFailedUrl(null);
|
|
146
|
+
setTimeout(() => setSuccessHint(false), 1e3);
|
|
147
|
+
} catch {
|
|
148
|
+
setSuccessHint(false);
|
|
149
|
+
setFailedUrl(link.href);
|
|
150
|
+
}
|
|
151
|
+
}, [links2, selectedIndex]);
|
|
152
|
+
useInput((input, key) => {
|
|
153
|
+
if (key.upArrow) {
|
|
154
|
+
setSelectedIndex((i) => i > 0 ? i - 1 : links2.length - 1);
|
|
155
|
+
setSuccessHint(false);
|
|
156
|
+
} else if (key.downArrow) {
|
|
157
|
+
setSelectedIndex((i) => i < links2.length - 1 ? i + 1 : 0);
|
|
158
|
+
setSuccessHint(false);
|
|
159
|
+
} else if (key.return) {
|
|
160
|
+
void handleSelect();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", paddingTop: 1, children: [
|
|
164
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Use \u2191\u2193 arrows to select, Enter to open:" }),
|
|
165
|
+
/* @__PURE__ */ jsx3(Box3, { flexDirection: "column", paddingTop: 1, children: links2.map((link, index) => {
|
|
166
|
+
const isSelected = index === selectedIndex;
|
|
167
|
+
return /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
|
|
168
|
+
/* @__PURE__ */ jsx3(Text3, { color: isSelected ? "cyan" : void 0, children: isSelected ? "\u276F" : " " }),
|
|
169
|
+
/* @__PURE__ */ jsx3(Text3, { bold: isSelected, color: isSelected ? "cyan" : void 0, children: link.label }),
|
|
170
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: truncate3(link.display, 28) }),
|
|
171
|
+
isSelected && successHint && /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: " Opened!" })
|
|
172
|
+
] }, link.label);
|
|
173
|
+
}) }),
|
|
174
|
+
failedUrl && /* @__PURE__ */ jsxs3(Box3, { paddingTop: 1, children: [
|
|
175
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Could not open \u2014 copy this URL: " }),
|
|
176
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: failedUrl })
|
|
177
|
+
] })
|
|
178
|
+
] });
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// source/app.tsx
|
|
182
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
183
|
+
var App = () => {
|
|
184
|
+
const { exit } = useApp();
|
|
185
|
+
useInput2((input) => {
|
|
186
|
+
if (input === "q") {
|
|
187
|
+
exit();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return /* @__PURE__ */ jsxs4(
|
|
191
|
+
Box4,
|
|
192
|
+
{
|
|
193
|
+
flexDirection: "column",
|
|
194
|
+
borderStyle: "round",
|
|
195
|
+
paddingX: 2,
|
|
196
|
+
paddingY: 1,
|
|
197
|
+
width: 54,
|
|
198
|
+
children: [
|
|
199
|
+
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
200
|
+
/* @__PURE__ */ jsxs4(Text4, { bold: true, color: "white", children: [
|
|
201
|
+
"\u266A ",
|
|
202
|
+
profile.fullName.first,
|
|
203
|
+
" "
|
|
204
|
+
] }),
|
|
205
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, color: "red", children: profile.fullName.last }),
|
|
206
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
207
|
+
" (",
|
|
208
|
+
profile.name,
|
|
209
|
+
")"
|
|
210
|
+
] })
|
|
211
|
+
] }),
|
|
212
|
+
/* @__PURE__ */ jsx4(Text4, { color: "cyan", children: profile.title }),
|
|
213
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: profile.tagline }),
|
|
214
|
+
/* @__PURE__ */ jsx4(Experience, { experiences }),
|
|
215
|
+
/* @__PURE__ */ jsx4(Projects, { projects }),
|
|
216
|
+
/* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingTop: 1, children: [
|
|
217
|
+
/* @__PURE__ */ jsx4(Text4, { bold: true, color: "red", children: "Education" }),
|
|
218
|
+
/* @__PURE__ */ jsxs4(Box4, { justifyContent: "space-between", children: [
|
|
219
|
+
/* @__PURE__ */ jsx4(Text4, { children: education.school }),
|
|
220
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
221
|
+
education.degree,
|
|
222
|
+
" ",
|
|
223
|
+
education.from,
|
|
224
|
+
"\u2013",
|
|
225
|
+
education.to
|
|
226
|
+
] })
|
|
227
|
+
] })
|
|
228
|
+
] }),
|
|
229
|
+
/* @__PURE__ */ jsx4(Links, { links })
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
// source/cli.tsx
|
|
236
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
237
|
+
render(/* @__PURE__ */ jsx5(App, {}));
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fangge",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Fango's terminal business card — run npx fango",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fangge": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsup",
|
|
17
|
+
"dev": "tsx source/cli.tsx",
|
|
18
|
+
"test": "echo 'no tests'",
|
|
19
|
+
"prepublishOnly": "pnpm build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"cli",
|
|
23
|
+
"card",
|
|
24
|
+
"terminal",
|
|
25
|
+
"npx"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"ink": "^5.2.1",
|
|
30
|
+
"ink-link": "^4.1.0",
|
|
31
|
+
"open": "^10.2.0",
|
|
32
|
+
"react": "^18.3.1"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/react": "^18.3.28",
|
|
36
|
+
"tsup": "^8.5.1",
|
|
37
|
+
"tsx": "^4.21.0",
|
|
38
|
+
"typescript": "^5.9.3"
|
|
39
|
+
}
|
|
40
|
+
}
|