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.
Files changed (3) hide show
  1. package/README.md +23 -0
  2. package/dist/cli.js +237 -0
  3. package/package.json +40 -0
package/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # fango
2
+
3
+ My terminal business card.
4
+
5
+ ## Usage
6
+
7
+ ```
8
+ npx fango
9
+ ```
10
+
11
+ ## Development
12
+
13
+ ```bash
14
+ pnpm install
15
+ pnpm dev
16
+ ```
17
+
18
+ ## Build
19
+
20
+ ```bash
21
+ pnpm build
22
+ node dist/cli.js
23
+ ```
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
+ }