zephinax 1.0.0 → 1.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/README.md +20 -10
- package/bin/index.js +368 -65
- package/package.json +15 -3
package/README.md
CHANGED
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
<div align="center">
|
|
2
|
-
<
|
|
2
|
+
<img src="./logo.svg" alt="Zephinax Logo" width="100" />
|
|
3
|
+
<h1>Amirali Rajabi (Zephinax)</h1>
|
|
4
|
+
<p><em>Interdisciplinary engineer turning ideas into functional products.</em></p>
|
|
5
|
+
|
|
6
|
+
<p>
|
|
7
|
+
<a href="https://www.npmjs.com/package/zephinax"><img src="https://img.shields.io/npm/v/zephinax?color=006aff&label=CLI%20Version" alt="npm version" /></a>
|
|
8
|
+
<a href="https://zephinax.com"><img src="https://img.shields.io/badge/Website-zephinax.com-006aff" alt="Website" /></a>
|
|
9
|
+
<a href="mailto:zephinax@gmail.com"><img src="https://img.shields.io/badge/Email-zephinax%40gmail.com-006aff" alt="Email" /></a>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<pre><code>npx zephinax</code></pre>
|
|
13
|
+
<p><sub>Interactive by default in terminal. Use <code>--json</code> for machine-readable output or <code>--static</code> for non-interactive.</sub></p>
|
|
3
14
|
</div>
|
|
4
15
|
|
|
5
16
|
<br>
|
|
6
17
|
|
|
18
|
+
<div align="center">
|
|
19
|
+
<h2>Activity & Stats</h2>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
7
22
|
<p align="center">
|
|
8
|
-
<img alt="
|
|
23
|
+
<img alt="Streak Stats" src="https://streak-stats.demolab.com/?user=Zephinax&hide_border=true&theme=transparent"/>
|
|
9
24
|
</p>
|
|
10
25
|
|
|
11
|
-
|
|
12
|
-
<img
|
|
13
|
-
|
|
14
|
-
<img align="center" src="http://github-profile-summary-cards.vercel.app/api/cards/profile-details?username=Zephinax&theme=transparent" height="180em" alt="Commitments"/>
|
|
15
|
-
</div> -->
|
|
16
|
-
|
|
17
|
-
<img src="./profile-3d-contrib/profile-3d-contrib.svg" alt="profile" >
|
|
18
|
-
|
|
26
|
+
<p align="center">
|
|
27
|
+
<img src="./profile-3d-contrib/profile-3d-contrib.svg" alt="3D Profile Contributions" width="100%">
|
|
28
|
+
</p>
|
package/bin/index.js
CHANGED
|
@@ -2,57 +2,96 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const https = require("https");
|
|
5
|
+
const { exec, spawnSync } = require("child_process");
|
|
6
|
+
const readline = require("readline");
|
|
5
7
|
|
|
8
|
+
const PKG = require("../package.json");
|
|
6
9
|
const API_URL = process.env.ZEPHINAX_API_URL || "https://zephinax.com/api/user";
|
|
10
|
+
const CONTENT_MARGIN = 1;
|
|
11
|
+
|
|
12
|
+
const DEFAULT_SKILLS = {
|
|
13
|
+
Stack: "Next.js • React • Node.js • Express • MongoDB",
|
|
14
|
+
Systems: "Embedded C/C++ • Firmware • Mechatronics",
|
|
15
|
+
DevOps: "Docker • Docker Compose • Git • Linux"
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const FALLBACK_PROFILE = {
|
|
19
|
+
firstName: "Amirali",
|
|
20
|
+
lastName: "Rajabi",
|
|
21
|
+
displayName: "Zephinax",
|
|
22
|
+
username: "zephinax",
|
|
23
|
+
gender: "male",
|
|
24
|
+
pronouns: "he/him",
|
|
25
|
+
bio: "Interdisciplinary engineer turning ideas into functional products.",
|
|
26
|
+
address: "Tehran, Iran",
|
|
27
|
+
email: "zephinax@gmail.com",
|
|
28
|
+
website: "https://zephinax.com",
|
|
29
|
+
jobTitle: "Systems Engineer",
|
|
30
|
+
about: `I'm Amirali, an interdisciplinary engineer with a background in mechatronics and practical experience across software development, hardware engineering, product development, and system integration.
|
|
31
|
+
My primary focus is turning ideas into functional products across the entire lifecycle — from system architecture, PCB design, and embedded firmware to full-stack web applications and cloud deployments.
|
|
32
|
+
I specialize in building complete systems: combining microcontrollers and embedded devices (ESP32, Arduino, Raspberry Pi) with modern web platforms (Next.js, React, Express, MongoDB, Docker). I enjoy designing scalable architectures, bridging physical hardware with web services, and solving real-world operational challenges.
|
|
33
|
+
Currently completing my studies in Industrial Mechatronics while working as a Systems Engineer, continually refining practical engineering and technical leadership skills.`,
|
|
34
|
+
quote: "Zephinax represents the endless pursuit of innovation in the world of technology.",
|
|
35
|
+
skills: DEFAULT_SKILLS,
|
|
36
|
+
timeZone: "Asia/Tehran"
|
|
37
|
+
};
|
|
7
38
|
|
|
8
39
|
const color = {
|
|
9
40
|
cyan: (str) => `\x1b[36m${str}\x1b[0m`,
|
|
10
41
|
magenta: (str) => `\x1b[35m${str}\x1b[0m`,
|
|
11
42
|
yellow: (str) => `\x1b[33m${str}\x1b[0m`,
|
|
43
|
+
green: (str) => `\x1b[32m${str}\x1b[0m`,
|
|
44
|
+
blue: (str) => `\x1b[34m${str}\x1b[0m`,
|
|
12
45
|
bold: (str) => `\x1b[1m${str}\x1b[0m`,
|
|
13
|
-
dim: (str) => `\x1b[2m${str}\x1b[0m
|
|
46
|
+
dim: (str) => `\x1b[2m${str}\x1b[0m`,
|
|
47
|
+
italic: (str) => `\x1b[3m${str}\x1b[0m`
|
|
14
48
|
};
|
|
15
49
|
|
|
16
|
-
|
|
50
|
+
function hyperlink(text, url) {
|
|
51
|
+
if (!url) return text;
|
|
52
|
+
return `\u001b]8;;${url}\u0007${text}\u001b]8;;\u0007`;
|
|
53
|
+
}
|
|
17
54
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
55
|
+
function stripAnsi(str = "") {
|
|
56
|
+
return str
|
|
57
|
+
.replace(/\u001b\]8;;[^\u0007\x1b]*(\u0007|\u001b\\)/g, "")
|
|
58
|
+
.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, "")
|
|
59
|
+
.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, "");
|
|
60
|
+
}
|
|
24
61
|
|
|
25
|
-
|
|
26
|
-
return
|
|
27
|
-
https
|
|
28
|
-
.get(url, (res) => {
|
|
29
|
-
let data = "";
|
|
30
|
-
res.on("data", (chunk) => (data += chunk));
|
|
31
|
-
res.on("end", () => {
|
|
32
|
-
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
33
|
-
return reject(new Error(`Request failed with status ${res.statusCode}`));
|
|
34
|
-
}
|
|
35
|
-
try {
|
|
36
|
-
resolve(JSON.parse(data));
|
|
37
|
-
} catch (err) {
|
|
38
|
-
reject(err);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
})
|
|
42
|
-
.on("error", reject);
|
|
43
|
-
});
|
|
62
|
+
function visibleLength(str = "") {
|
|
63
|
+
return stripAnsi(str).length;
|
|
44
64
|
}
|
|
45
65
|
|
|
46
|
-
function
|
|
47
|
-
if (!text) return "";
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
66
|
+
function parseAboutAndQuote(text) {
|
|
67
|
+
if (!text) return { about: "", quote: "" };
|
|
68
|
+
const lines = text.split(/\r?\n/);
|
|
69
|
+
const quoteLines = [];
|
|
70
|
+
const regularLines = [];
|
|
71
|
+
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
const trimmed = line.trim();
|
|
74
|
+
if (trimmed.startsWith(">")) {
|
|
75
|
+
quoteLines.push(trimmed.replace(/^>\s*/, ""));
|
|
76
|
+
} else {
|
|
77
|
+
regularLines.push(line);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const cleanText = (str) =>
|
|
82
|
+
str
|
|
83
|
+
.replace(/\*\*(.*?)\*\*/g, "$1")
|
|
84
|
+
.replace(/^\s*also a\b/im, "Also a")
|
|
85
|
+
.replace(/\r?\n\s*/g, "\n")
|
|
86
|
+
.trim();
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
about: cleanText(regularLines.join("\n")),
|
|
90
|
+
quote: cleanText(quoteLines.join(" "))
|
|
91
|
+
};
|
|
53
92
|
}
|
|
54
93
|
|
|
55
|
-
function wrapText(text, width, indent) {
|
|
94
|
+
function wrapText(text, width, indent = 0) {
|
|
56
95
|
if (!text) return [];
|
|
57
96
|
const indentStr = " ".repeat(indent);
|
|
58
97
|
return text
|
|
@@ -66,7 +105,7 @@ function wrapText(text, width, indent) {
|
|
|
66
105
|
|
|
67
106
|
words.forEach((word) => {
|
|
68
107
|
const candidate = current ? `${current} ${word}` : word;
|
|
69
|
-
if (candidate
|
|
108
|
+
if (visibleLength(candidate) > width) {
|
|
70
109
|
if (current) lines.push(current);
|
|
71
110
|
current = word;
|
|
72
111
|
} else {
|
|
@@ -79,19 +118,7 @@ function wrapText(text, width, indent) {
|
|
|
79
118
|
}
|
|
80
119
|
|
|
81
120
|
function divider(char = "-", length = 60) {
|
|
82
|
-
return char.repeat(length);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function stripAnsi(str = "") {
|
|
86
|
-
return str.replace(
|
|
87
|
-
// eslint-disable-next-line no-control-regex
|
|
88
|
-
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
|
|
89
|
-
""
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function visibleLength(str = "") {
|
|
94
|
-
return stripAnsi(str).length;
|
|
121
|
+
return char.repeat(Math.max(0, length));
|
|
95
122
|
}
|
|
96
123
|
|
|
97
124
|
function boxTop(label, width) {
|
|
@@ -121,54 +148,330 @@ function boxDivider(label, width) {
|
|
|
121
148
|
return `├${tag}${divider("─", lineLen)}┤`;
|
|
122
149
|
}
|
|
123
150
|
|
|
124
|
-
function fieldLine(label, value, width) {
|
|
151
|
+
function fieldLine(label, value, width, linkUrl = null) {
|
|
125
152
|
if (!value) return null;
|
|
126
|
-
const labelText = `${label}:`.padEnd(
|
|
127
|
-
|
|
153
|
+
const labelText = `${label}:`.padEnd(10);
|
|
154
|
+
const formattedVal = linkUrl ? hyperlink(color.cyan(value), linkUrl) : value;
|
|
155
|
+
return boxLine(`${color.yellow(labelText)} ${formattedVal}`, width);
|
|
128
156
|
}
|
|
129
157
|
|
|
130
|
-
function render(profile) {
|
|
131
|
-
const about =
|
|
158
|
+
function render(profile, widthOverride) {
|
|
159
|
+
const { about, quote } = parseAboutAndQuote(profile.about);
|
|
160
|
+
const finalQuote = profile.quote || quote;
|
|
161
|
+
const skills = profile.skills || DEFAULT_SKILLS;
|
|
162
|
+
|
|
132
163
|
const header =
|
|
133
|
-
`${color.bold(profile.displayName || profile.username || "Zephinax")} ${profile.pronouns ? `(${profile.pronouns})` : ""}`.trim();
|
|
164
|
+
`${color.bold(profile.displayName || profile.username || "Zephinax")} ${profile.pronouns ? color.dim(`(${profile.pronouns})`) : ""}`.trim();
|
|
134
165
|
const fullName =
|
|
135
166
|
profile.firstName || profile.lastName
|
|
136
167
|
? `${profile.firstName || ""} ${profile.lastName || ""}`.trim()
|
|
137
168
|
: null;
|
|
138
169
|
|
|
139
|
-
const
|
|
170
|
+
const terminalCols = process.stdout.columns || 80;
|
|
171
|
+
const width = widthOverride || Math.max(68, Math.min(terminalCols, 96));
|
|
140
172
|
const contentWidth = Math.max(0, width - 4 - CONTENT_MARGIN * 2);
|
|
141
173
|
|
|
142
174
|
const primary = color.cyan(header);
|
|
143
175
|
const secondary = fullName ? color.dim(fullName) : null;
|
|
144
|
-
const
|
|
176
|
+
const tagline = profile.bio ? color.italic(`"${profile.bio}"`) : null;
|
|
177
|
+
const aboutLines = about ? wrapText(about, contentWidth, 0) : [];
|
|
178
|
+
const quoteLines = finalQuote ? wrapText(`"${finalQuote}"`, contentWidth, 0) : [];
|
|
179
|
+
|
|
180
|
+
const githubUrl = `https://github.com/${profile.username || "zephinax"}`;
|
|
145
181
|
|
|
146
182
|
const lines = [
|
|
147
183
|
boxTop("Profile", width),
|
|
148
184
|
boxLine(primary, width),
|
|
149
185
|
secondary ? boxLine(secondary, width) : null,
|
|
186
|
+
tagline ? boxLine(tagline, width) : null,
|
|
150
187
|
boxDivider("details", width),
|
|
151
188
|
fieldLine("Role", profile.jobTitle, width),
|
|
189
|
+
fieldLine("Location", profile.address, width),
|
|
152
190
|
fieldLine("Timezone", profile.timeZone, width),
|
|
153
191
|
boxDivider("contact", width),
|
|
154
|
-
fieldLine("
|
|
155
|
-
fieldLine("
|
|
192
|
+
fieldLine("Web", profile.website, width, profile.website),
|
|
193
|
+
fieldLine("GitHub", githubUrl, width, githubUrl),
|
|
194
|
+
fieldLine("Email", profile.email, width, profile.email ? `mailto:${profile.email}` : null),
|
|
195
|
+
boxDivider("skills", width),
|
|
196
|
+
...Object.entries(skills).map(([key, val]) => fieldLine(key, val, width)),
|
|
156
197
|
aboutLines.length ? boxDivider("about", width) : null,
|
|
157
198
|
...aboutLines.map((l) => boxLine(l, width)),
|
|
199
|
+
quoteLines.length ? boxDivider("philosophy", width) : null,
|
|
200
|
+
...quoteLines.map((l) => boxLine(color.italic(l), width)),
|
|
158
201
|
boxBottom(width)
|
|
159
202
|
].filter(Boolean);
|
|
160
203
|
|
|
161
204
|
return lines.join("\n");
|
|
162
205
|
}
|
|
163
206
|
|
|
164
|
-
|
|
207
|
+
function renderPlain(profile) {
|
|
208
|
+
const { about, quote } = parseAboutAndQuote(profile.about);
|
|
209
|
+
const finalQuote = profile.quote || quote;
|
|
210
|
+
const fullName = [profile.firstName, profile.lastName].filter(Boolean).join(" ");
|
|
211
|
+
const skills = profile.skills || DEFAULT_SKILLS;
|
|
212
|
+
const githubUrl = `https://github.com/${profile.username || "zephinax"}`;
|
|
213
|
+
|
|
214
|
+
const lines = [
|
|
215
|
+
`${profile.displayName || profile.username || "Zephinax"}${profile.pronouns ? ` (${profile.pronouns})` : ""}`,
|
|
216
|
+
fullName || null,
|
|
217
|
+
profile.bio ? `"${profile.bio}"` : null,
|
|
218
|
+
"",
|
|
219
|
+
"--- DETAILS ---",
|
|
220
|
+
profile.jobTitle ? `Role: ${profile.jobTitle}` : null,
|
|
221
|
+
profile.address ? `Location: ${profile.address}` : null,
|
|
222
|
+
profile.timeZone ? `Timezone: ${profile.timeZone}` : null,
|
|
223
|
+
"",
|
|
224
|
+
"--- CONTACT ---",
|
|
225
|
+
profile.website ? `Web: ${profile.website}` : null,
|
|
226
|
+
`GitHub: ${githubUrl}`,
|
|
227
|
+
profile.email ? `Email: ${profile.email}` : null,
|
|
228
|
+
"",
|
|
229
|
+
"--- SKILLS ---",
|
|
230
|
+
...Object.entries(skills).map(([key, val]) => `${(key + ":").padEnd(10)} ${val}`),
|
|
231
|
+
"",
|
|
232
|
+
about ? "--- ABOUT ---" : null,
|
|
233
|
+
about || null,
|
|
234
|
+
"",
|
|
235
|
+
finalQuote ? "--- PHILOSOPHY ---" : null,
|
|
236
|
+
finalQuote ? `"${finalQuote}"` : null
|
|
237
|
+
].filter((l) => l !== null);
|
|
238
|
+
|
|
239
|
+
return lines.join("\n");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function openUrl(url) {
|
|
243
|
+
const platform = process.platform;
|
|
244
|
+
let cmd;
|
|
245
|
+
if (platform === "darwin") {
|
|
246
|
+
cmd = `open "${url}"`;
|
|
247
|
+
} else if (platform === "win32") {
|
|
248
|
+
cmd = `start "" "${url}"`;
|
|
249
|
+
} else {
|
|
250
|
+
cmd = `xdg-open "${url}"`;
|
|
251
|
+
}
|
|
252
|
+
exec(cmd, () => {});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function copyToClipboard(text) {
|
|
256
|
+
const platform = process.platform;
|
|
165
257
|
try {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
258
|
+
if (platform === "darwin") {
|
|
259
|
+
spawnSync("pbcopy", { input: text });
|
|
260
|
+
return true;
|
|
261
|
+
} else if (platform === "win32") {
|
|
262
|
+
spawnSync("clip", { input: text });
|
|
263
|
+
return true;
|
|
264
|
+
} else {
|
|
265
|
+
const resXclip = spawnSync("which", ["xclip"]);
|
|
266
|
+
if (resXclip.status === 0) {
|
|
267
|
+
spawnSync("xclip", ["-selection", "clipboard"], { input: text });
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
const resWl = spawnSync("which", ["wl-copy"]);
|
|
271
|
+
if (resWl.status === 0) {
|
|
272
|
+
spawnSync("wl-copy", { input: text });
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function runInteractive(profile) {
|
|
283
|
+
return new Promise((resolve) => {
|
|
284
|
+
const email = profile.email || "zephinax@gmail.com";
|
|
285
|
+
const web = profile.website || "https://zephinax.com";
|
|
286
|
+
const github = `https://github.com/${profile.username || "zephinax"}`;
|
|
287
|
+
|
|
288
|
+
const rl = readline.createInterface({
|
|
289
|
+
input: process.stdin,
|
|
290
|
+
output: process.stdout
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
console.log("\n" + color.bold(color.cyan("Actions:")));
|
|
294
|
+
console.log(` ${color.yellow("[1]")} 🌐 Open Website ${color.dim(`(${web})`)}`);
|
|
295
|
+
console.log(` ${color.yellow("[2]")} 🐙 Open GitHub ${color.dim(`(${github})`)}`);
|
|
296
|
+
console.log(` ${color.yellow("[3]")} ✉️ Send Email ${color.dim(`(${email})`)}`);
|
|
297
|
+
console.log(` ${color.yellow("[4]")} 📋 Copy Email ${color.dim(`(${email})`)}`);
|
|
298
|
+
console.log(` ${color.yellow("[q]")} 🚪 Exit\n`);
|
|
299
|
+
|
|
300
|
+
const ask = () => {
|
|
301
|
+
rl.question(color.cyan("Select an option (1-4, or press Enter/q to exit): "), (answer) => {
|
|
302
|
+
const choice = answer.trim().toLowerCase();
|
|
303
|
+
switch (choice) {
|
|
304
|
+
case "1":
|
|
305
|
+
console.log(color.dim(`Opening ${web}...`));
|
|
306
|
+
openUrl(web);
|
|
307
|
+
ask();
|
|
308
|
+
break;
|
|
309
|
+
case "2":
|
|
310
|
+
console.log(color.dim(`Opening ${github}...`));
|
|
311
|
+
openUrl(github);
|
|
312
|
+
ask();
|
|
313
|
+
break;
|
|
314
|
+
case "3":
|
|
315
|
+
console.log(color.dim(`Opening email client...`));
|
|
316
|
+
openUrl(`mailto:${email}`);
|
|
317
|
+
ask();
|
|
318
|
+
break;
|
|
319
|
+
case "4": {
|
|
320
|
+
const ok = copyToClipboard(email);
|
|
321
|
+
if (ok) {
|
|
322
|
+
console.log(color.green(`✓ Email copied to clipboard!`));
|
|
323
|
+
} else {
|
|
324
|
+
console.log(color.yellow(`Clipboard unavailable. Email: ${email}`));
|
|
325
|
+
}
|
|
326
|
+
ask();
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
case "":
|
|
330
|
+
case "q":
|
|
331
|
+
case "quit":
|
|
332
|
+
case "exit":
|
|
333
|
+
console.log(color.dim("Thanks for stopping by!"));
|
|
334
|
+
rl.close();
|
|
335
|
+
resolve();
|
|
336
|
+
break;
|
|
337
|
+
default:
|
|
338
|
+
console.log(color.magenta("Invalid option. Please enter 1, 2, 3, 4, or q."));
|
|
339
|
+
ask();
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
rl.on("close", resolve);
|
|
346
|
+
ask();
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function fetchProfile(url, timeoutMs = 3000) {
|
|
351
|
+
let controller;
|
|
352
|
+
let timeoutId;
|
|
353
|
+
if (typeof AbortController !== "undefined") {
|
|
354
|
+
controller = new AbortController();
|
|
355
|
+
timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
if (typeof fetch === "function") {
|
|
360
|
+
const res = await fetch(url, {
|
|
361
|
+
signal: controller ? controller.signal : undefined,
|
|
362
|
+
headers: { "User-Agent": "zephinax-cli-cv" }
|
|
363
|
+
});
|
|
364
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
365
|
+
const json = await res.json();
|
|
366
|
+
return { profile: json.data || json, fromNetwork: true };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return await new Promise((resolve, reject) => {
|
|
370
|
+
const req = https.get(url, { timeout: timeoutMs }, (res) => {
|
|
371
|
+
let data = "";
|
|
372
|
+
res.on("data", (chunk) => (data += chunk));
|
|
373
|
+
res.on("end", () => {
|
|
374
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
375
|
+
return reject(new Error(`HTTP ${res.statusCode}`));
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const json = JSON.parse(data);
|
|
379
|
+
resolve({ profile: json.data || json, fromNetwork: true });
|
|
380
|
+
} catch (err) {
|
|
381
|
+
reject(err);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
req.on("error", reject);
|
|
386
|
+
req.on("timeout", () => {
|
|
387
|
+
req.destroy();
|
|
388
|
+
reject(new Error("Request timed out"));
|
|
389
|
+
});
|
|
390
|
+
});
|
|
169
391
|
} catch (err) {
|
|
170
|
-
|
|
171
|
-
|
|
392
|
+
return { profile: FALLBACK_PROFILE, fromNetwork: false, error: err.message };
|
|
393
|
+
} finally {
|
|
394
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function showHelp() {
|
|
399
|
+
console.log(`
|
|
400
|
+
${color.bold("Zephinax (Amirali Rajabi)")} - CLI Portfolio & CV
|
|
401
|
+
|
|
402
|
+
${color.yellow("Usage:")}
|
|
403
|
+
npx zephinax [options]
|
|
404
|
+
|
|
405
|
+
${color.yellow("Options:")}
|
|
406
|
+
-s, --static Print card without interactive action prompt
|
|
407
|
+
-j, --json Output profile data in raw JSON format
|
|
408
|
+
-p, --plain Output clean plain text without borders or ANSI colors
|
|
409
|
+
--offline Use embedded profile data without making network requests
|
|
410
|
+
-h, --help Show this help message
|
|
411
|
+
-v, --version Show version number
|
|
412
|
+
`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function main() {
|
|
416
|
+
const args = process.argv.slice(2);
|
|
417
|
+
|
|
418
|
+
if (args.includes("-h") || args.includes("--help")) {
|
|
419
|
+
showHelp();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (args.includes("-v") || args.includes("--version")) {
|
|
424
|
+
console.log(PKG.version);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const isJson = args.includes("-j") || args.includes("--json");
|
|
429
|
+
const isPlain = args.includes("-p") || args.includes("--plain");
|
|
430
|
+
const isOffline = args.includes("--offline");
|
|
431
|
+
const isStatic =
|
|
432
|
+
args.includes("-s") ||
|
|
433
|
+
args.includes("--static") ||
|
|
434
|
+
args.includes("--no-interactive");
|
|
435
|
+
|
|
436
|
+
let profile;
|
|
437
|
+
let fromNetwork = false;
|
|
438
|
+
|
|
439
|
+
if (isOffline) {
|
|
440
|
+
profile = FALLBACK_PROFILE;
|
|
441
|
+
} else {
|
|
442
|
+
const res = await fetchProfile(API_URL);
|
|
443
|
+
profile = { ...FALLBACK_PROFILE, ...res.profile };
|
|
444
|
+
fromNetwork = res.fromNetwork;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (isJson) {
|
|
448
|
+
const { about, quote } = parseAboutAndQuote(profile.about);
|
|
449
|
+
console.log(
|
|
450
|
+
JSON.stringify(
|
|
451
|
+
{
|
|
452
|
+
...profile,
|
|
453
|
+
skills: profile.skills || DEFAULT_SKILLS,
|
|
454
|
+
quote: profile.quote || quote,
|
|
455
|
+
about: about || profile.about,
|
|
456
|
+
source: fromNetwork ? "network" : "offline_cache"
|
|
457
|
+
},
|
|
458
|
+
null,
|
|
459
|
+
2
|
|
460
|
+
)
|
|
461
|
+
);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (isPlain) {
|
|
466
|
+
console.log(renderPlain(profile));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
console.log(render(profile));
|
|
471
|
+
|
|
472
|
+
const isInteractiveTerminal = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
473
|
+
if (isInteractiveTerminal && !isStatic) {
|
|
474
|
+
await runInteractive(profile);
|
|
172
475
|
}
|
|
173
476
|
}
|
|
174
477
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zephinax",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI portfolio
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Interactive CLI portfolio and CV for Amirali Rajabi (Zephinax).",
|
|
5
5
|
"bin": {
|
|
6
6
|
"zephinax": "bin/index.js"
|
|
7
7
|
},
|
|
@@ -11,10 +11,22 @@
|
|
|
11
11
|
"keywords": [
|
|
12
12
|
"portfolio",
|
|
13
13
|
"cli",
|
|
14
|
-
"
|
|
14
|
+
"cv",
|
|
15
|
+
"resume",
|
|
16
|
+
"zephinax",
|
|
17
|
+
"terminal-card",
|
|
18
|
+
"interactive"
|
|
15
19
|
],
|
|
16
20
|
"author": "Zephinax <zephinax@gmail.com>",
|
|
17
21
|
"license": "MIT",
|
|
22
|
+
"homepage": "https://zephinax.com",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/zephinax/zephinax.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/zephinax/zephinax/issues"
|
|
29
|
+
},
|
|
18
30
|
"engines": {
|
|
19
31
|
"node": ">=18"
|
|
20
32
|
}
|