brignano 1.1.0 → 2.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/LICENSE +15 -0
- package/dist/card.js +28 -19
- package/dist/data.js +4 -1
- package/dist/env.js +46 -9
- package/dist/live.js +4 -1
- package/dist/menu.js +13 -0
- package/package.json +21 -14
- package/readme.md +24 -4
- package/.github/workflows/ci.yml +0 -29
- package/.github/workflows/publish.yml +0 -32
- package/demo.gif +0 -0
- package/src/banner.ts +0 -66
- package/src/card.ts +0 -98
- package/src/cardBox.ts +0 -64
- package/src/data.ts +0 -38
- package/src/env.ts +0 -69
- package/src/live.ts +0 -58
- package/src/menu.ts +0 -84
- package/src/qrcode.ts +0 -9
- package/src/reveal.ts +0 -25
- package/src/types/qrcode-terminal.d.ts +0 -13
- package/src/vcard.ts +0 -41
- package/tsconfig.json +0 -15
- package/vhs/demo.tape +0 -40
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-2026 Anthony Brignano
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/dist/card.js
CHANGED
|
@@ -1,33 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import inquirer from 'inquirer';
|
|
4
3
|
import { animateBanner, renderBanner } from './banner.js';
|
|
5
4
|
import { renderCard } from './cardBox.js';
|
|
6
5
|
import { funFacts, pick, profile, taglines } from './data.js';
|
|
7
|
-
import { clearConsole, isInteractive, parseFlags, shouldAnimate, shouldFetchLive, } from './env.js';
|
|
6
|
+
import { clearConsole, isInteractive, parseFlags, readVersion, shouldAnimate, shouldFetchLive, unknownFlags, } from './env.js';
|
|
8
7
|
import { fetchLive } from './live.js';
|
|
9
|
-
import {
|
|
8
|
+
import { promptForAction, sendEmail, viewResume } from './menu.js';
|
|
10
9
|
import { renderQr } from './qrcode.js';
|
|
11
10
|
import { reveal, sleep } from './reveal.js';
|
|
12
11
|
import { writeAndOpenVCard } from './vcard.js';
|
|
13
12
|
function printHelp() {
|
|
14
13
|
console.log('\nUsage: npx brignano [options]\n');
|
|
15
14
|
console.log('Options:');
|
|
16
|
-
console.log(' -h, --help
|
|
17
|
-
console.log(' -v, --
|
|
18
|
-
console.log(' -
|
|
19
|
-
console.log('
|
|
20
|
-
console.log(' --
|
|
15
|
+
console.log(' -h, --help Show this help');
|
|
16
|
+
console.log(' -v, --version Print the version and exit');
|
|
17
|
+
console.log(' -r, --view Open my resume in your browser');
|
|
18
|
+
console.log(' -e, --email Open your mail client to email me');
|
|
19
|
+
console.log(' --qr Print a scannable QR code to my website');
|
|
20
|
+
console.log(' --vcard Save my contact card (.vcf) and open it');
|
|
21
21
|
console.log(' --no-animation Render instantly, skip the reveal animation');
|
|
22
|
-
console.log(' --no-prompt
|
|
23
|
-
console.log(' -d, --download
|
|
22
|
+
console.log(' --no-prompt Show the card only (non-interactive)');
|
|
23
|
+
console.log(' -d, --download Deprecated alias for --view\n');
|
|
24
24
|
}
|
|
25
25
|
async function main() {
|
|
26
26
|
const argv = process.argv.slice(2);
|
|
27
|
+
// Reject typos loudly rather than silently rendering the default card.
|
|
28
|
+
const unknown = unknownFlags(argv);
|
|
29
|
+
if (unknown.length > 0) {
|
|
30
|
+
console.error(`Unknown option: ${unknown.join(', ')}`);
|
|
31
|
+
console.error("Run 'npx brignano --help' to see the available options.");
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
27
35
|
const flags = parseFlags(argv);
|
|
28
36
|
// Direct-action flags short-circuit the full experience.
|
|
29
37
|
if (flags.help)
|
|
30
38
|
return printHelp();
|
|
39
|
+
if (flags.version) {
|
|
40
|
+
console.log(readVersion());
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
31
43
|
if (flags.view || flags.download) {
|
|
32
44
|
if (flags.download) {
|
|
33
45
|
console.warn('Note: --download is deprecated; opening the resume instead.');
|
|
@@ -66,18 +78,15 @@ async function main() {
|
|
|
66
78
|
console.log(chalk.gray(` ${pick(funFacts)}\n`));
|
|
67
79
|
if (!interactive)
|
|
68
80
|
return;
|
|
69
|
-
const
|
|
70
|
-
const { action } = await prompt([
|
|
71
|
-
{
|
|
72
|
-
type: 'list',
|
|
73
|
-
name: 'action',
|
|
74
|
-
message: 'What would you like to do?',
|
|
75
|
-
choices: buildChoices(),
|
|
76
|
-
},
|
|
77
|
-
]);
|
|
81
|
+
const action = await promptForAction();
|
|
78
82
|
await action();
|
|
79
83
|
}
|
|
80
84
|
main().catch((err) => {
|
|
85
|
+
// Ctrl+C / Esc at the menu is a normal way to leave, not a crash.
|
|
86
|
+
if (err instanceof Error && err.name === 'ExitPromptError') {
|
|
87
|
+
console.log('\nHave a great day!\n');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
81
90
|
const message = err instanceof Error ? err.message : String(err);
|
|
82
91
|
console.error(`Something went wrong: ${message}`);
|
|
83
92
|
process.exitCode = 1;
|
package/dist/data.js
CHANGED
|
@@ -29,6 +29,9 @@ export const funFacts = [
|
|
|
29
29
|
'The live stats above are pulled fresh from GitHub (with a strict timeout).',
|
|
30
30
|
];
|
|
31
31
|
// Deterministic-enough pick without Math.random (kept simple + dependency-free).
|
|
32
|
+
// The `?? items[0]` fallback is unreachable for a NonEmpty list, but it keeps the
|
|
33
|
+
// return type honest under `noUncheckedIndexedAccess` without an assertion.
|
|
32
34
|
export function pick(items, seed = Date.now()) {
|
|
33
|
-
|
|
35
|
+
const index = Math.floor(seed / 1000) % items.length;
|
|
36
|
+
return items[index] ?? items[0];
|
|
34
37
|
}
|
package/dist/env.js
CHANGED
|
@@ -1,22 +1,59 @@
|
|
|
1
1
|
// Environment + capability detection. Keeps the render logic free of scattered
|
|
2
2
|
// `process.stdout.isTTY` checks and guarantees CI / piped output never hangs.
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
// Every accepted token, grouped by the flag it sets. Also drives unknown-flag
|
|
7
|
+
// detection below, so adding a flag here is the only edit needed.
|
|
8
|
+
const FLAG_ALIASES = {
|
|
9
|
+
help: ['--help', '-h'],
|
|
10
|
+
version: ['--version', '-v', '-V'],
|
|
11
|
+
view: ['--view', '--resume', '-r'],
|
|
12
|
+
download: ['--download', '-d'],
|
|
13
|
+
email: ['--email', '-e'],
|
|
14
|
+
qr: ['--qr'],
|
|
15
|
+
vcard: ['--vcard', '--save-contact'],
|
|
16
|
+
noPrompt: ['--no-prompt', '--quiet'],
|
|
17
|
+
noAnimation: ['--no-animation'],
|
|
18
|
+
};
|
|
19
|
+
const KNOWN_TOKENS = new Set(Object.values(FLAG_ALIASES).flat());
|
|
3
20
|
export function hasFlag(argv, names) {
|
|
4
21
|
return names.some((n) => argv.includes(n));
|
|
5
22
|
}
|
|
6
23
|
export function parseFlags(argv) {
|
|
7
24
|
return {
|
|
8
|
-
help: hasFlag(argv,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
help: hasFlag(argv, FLAG_ALIASES.help),
|
|
26
|
+
version: hasFlag(argv, FLAG_ALIASES.version),
|
|
27
|
+
view: hasFlag(argv, FLAG_ALIASES.view),
|
|
28
|
+
download: hasFlag(argv, FLAG_ALIASES.download),
|
|
29
|
+
email: hasFlag(argv, FLAG_ALIASES.email),
|
|
30
|
+
qr: hasFlag(argv, FLAG_ALIASES.qr),
|
|
31
|
+
vcard: hasFlag(argv, FLAG_ALIASES.vcard),
|
|
32
|
+
noPrompt: hasFlag(argv, FLAG_ALIASES.noPrompt),
|
|
33
|
+
noAnimation: hasFlag(argv, FLAG_ALIASES.noAnimation),
|
|
16
34
|
};
|
|
17
35
|
}
|
|
36
|
+
// Anything that looks like a flag but isn't one we accept. Returned rather than
|
|
37
|
+
// thrown so the caller decides how loudly to complain.
|
|
38
|
+
export function unknownFlags(argv) {
|
|
39
|
+
return argv.filter((arg) => arg.startsWith('-') && !KNOWN_TOKENS.has(arg));
|
|
40
|
+
}
|
|
41
|
+
// Read the version straight from the published package.json. It isn't in the
|
|
42
|
+
// `files` list, but npm always ships package.json, and it sits one level above
|
|
43
|
+
// the compiled dist/ directory.
|
|
44
|
+
export function readVersion() {
|
|
45
|
+
try {
|
|
46
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
47
|
+
const raw = readFileSync(join(here, '..', 'package.json'), 'utf8');
|
|
48
|
+
const pkg = JSON.parse(raw);
|
|
49
|
+
return typeof pkg.version === 'string' ? pkg.version : 'unknown';
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return 'unknown';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
18
55
|
const isCI = () => Boolean(process.env.CI);
|
|
19
|
-
// True only when we can safely show an interactive
|
|
56
|
+
// True only when we can safely show an interactive prompt.
|
|
20
57
|
export function isInteractive(flags) {
|
|
21
58
|
return Boolean(process.stdout.isTTY && process.stdin.isTTY && !isCI() && !flags.noPrompt);
|
|
22
59
|
}
|
package/dist/live.js
CHANGED
|
@@ -16,7 +16,10 @@ export async function fetchLive(timeoutMs = 1500) {
|
|
|
16
16
|
headers: HEADERS,
|
|
17
17
|
signal: controller.signal,
|
|
18
18
|
}),
|
|
19
|
-
fetch(`${API}/users/${profile.username}/repos?sort=updated&per_page=1`, {
|
|
19
|
+
fetch(`${API}/users/${profile.username}/repos?sort=updated&per_page=1`, {
|
|
20
|
+
headers: HEADERS,
|
|
21
|
+
signal: controller.signal,
|
|
22
|
+
}),
|
|
20
23
|
]);
|
|
21
24
|
if (!userRes.ok || !reposRes.ok)
|
|
22
25
|
return null;
|
package/dist/menu.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { select } from '@inquirer/prompts';
|
|
1
2
|
import chalk from 'chalk';
|
|
2
3
|
import clipboard from 'clipboardy';
|
|
3
4
|
import open from 'open';
|
|
@@ -71,3 +72,15 @@ export function buildChoices() {
|
|
|
71
72
|
},
|
|
72
73
|
];
|
|
73
74
|
}
|
|
75
|
+
// Shows the action menu and returns the chosen handler. Ctrl+C / Esc throws an
|
|
76
|
+
// `ExitPromptError`, which the entry point treats as a normal exit.
|
|
77
|
+
export async function promptForAction() {
|
|
78
|
+
const choices = buildChoices();
|
|
79
|
+
return select({
|
|
80
|
+
message: 'What would you like to do?',
|
|
81
|
+
choices,
|
|
82
|
+
// Default page size is 7, which would hide the last entry behind a scroll.
|
|
83
|
+
// Show every action at once — the list is short and fixed.
|
|
84
|
+
pageSize: choices.length,
|
|
85
|
+
});
|
|
86
|
+
}
|
package/package.json
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brignano",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"main": "dist/card.js",
|
|
6
5
|
"engines": {
|
|
7
6
|
"node": ">=20"
|
|
8
7
|
},
|
|
9
8
|
"bin": {
|
|
10
9
|
"brignano": "dist/card.js"
|
|
11
10
|
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist"
|
|
13
|
+
],
|
|
12
14
|
"scripts": {
|
|
13
15
|
"build": "tsc",
|
|
14
|
-
"dev": "
|
|
16
|
+
"dev": "tsx src/card.ts",
|
|
15
17
|
"start": "node dist/card.js",
|
|
18
|
+
"lint": "biome check .",
|
|
19
|
+
"format": "biome check --write .",
|
|
16
20
|
"test": "node dist/card.js --no-prompt",
|
|
17
21
|
"prepare": "npm run build"
|
|
18
22
|
},
|
|
@@ -35,22 +39,25 @@
|
|
|
35
39
|
"vcard"
|
|
36
40
|
],
|
|
37
41
|
"description": "A personal card for Anthony Brignano (@brignano)",
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public",
|
|
44
|
+
"provenance": true
|
|
45
|
+
},
|
|
38
46
|
"dependencies": {
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
47
|
+
"@inquirer/prompts": "^8.6.0",
|
|
48
|
+
"boxen": "^8.0.1",
|
|
49
|
+
"chalk": "^6.0.0",
|
|
50
|
+
"clipboardy": "^5.3.2",
|
|
51
|
+
"figlet": "^1.11.4",
|
|
43
52
|
"gradient-string": "^3.0.0",
|
|
44
|
-
"
|
|
45
|
-
"open": "^8.4.0",
|
|
53
|
+
"open": "^11.0.1",
|
|
46
54
|
"qrcode-terminal": "^0.12.0"
|
|
47
55
|
},
|
|
48
56
|
"devDependencies": {
|
|
49
|
-
"
|
|
50
|
-
"typescript": "^5.1.6",
|
|
51
|
-
"@types/node": "^20.5.1",
|
|
52
|
-
"@types/inquirer": "^9.0.3",
|
|
57
|
+
"@biomejs/biome": "^2.5.10",
|
|
53
58
|
"@types/figlet": "^1.7.0",
|
|
54
|
-
"@types/
|
|
59
|
+
"@types/node": "^22.20.1",
|
|
60
|
+
"tsx": "^4.23.12",
|
|
61
|
+
"typescript": "^5.9.3"
|
|
55
62
|
}
|
|
56
63
|
}
|
package/readme.md
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
# npx-brignano
|
|
1
|
+
# npx-brignano
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/brignano)
|
|
4
|
+
[](https://www.npmjs.com/package/brignano)
|
|
5
|
+
[](https://github.com/brignano/npx-brignano/actions/workflows/ci.yml)
|
|
6
|
+
[](LICENSE)
|
|
2
7
|
|
|
3
8
|
An interactive terminal business card for Anthony Brignano. Run it with `npx` to get a gradient ASCII-art intro, a styled card with **live GitHub stats**, and quick actions — copy my email, open my socials, save my contact, or grab a scannable QR code.
|
|
4
9
|
|
|
@@ -33,7 +38,8 @@ Run `npx brignano --help` to see this list.
|
|
|
33
38
|
| Flag | Description |
|
|
34
39
|
|------|-------------|
|
|
35
40
|
| `-h, --help` | Show help and exit. |
|
|
36
|
-
| `-v, --
|
|
41
|
+
| `-v, --version` | Print the installed version and exit. |
|
|
42
|
+
| `-r, --view` / `--resume` | Open my resume in your default browser (`https://brignano.io/resume`). |
|
|
37
43
|
| `-e, --email` | Open your mail client with a new message to `hi@brignano.io`. |
|
|
38
44
|
| `--qr` | Print a scannable QR code to my website and exit. |
|
|
39
45
|
| `--vcard` / `--save-contact` | Save my contact card (`.vcf`) and open it. |
|
|
@@ -41,11 +47,19 @@ Run `npx brignano --help` to see this list.
|
|
|
41
47
|
| `--no-prompt` | Show the card only and exit (non-interactive). |
|
|
42
48
|
| `-d, --download` | Deprecated alias for `--view`. |
|
|
43
49
|
|
|
50
|
+
Unrecognized flags exit with a non-zero status instead of silently falling back
|
|
51
|
+
to the default card.
|
|
52
|
+
|
|
53
|
+
> **Note:** as of `2.0.0`, `-v` prints the version (the near-universal CLI
|
|
54
|
+
> convention). The resume now lives at `-r`, and the long `--view` flag is
|
|
55
|
+
> unchanged.
|
|
56
|
+
|
|
44
57
|
### Examples
|
|
45
58
|
|
|
46
59
|
```bash
|
|
47
60
|
npx brignano # full interactive experience
|
|
48
61
|
npx brignano --view # jump straight to my resume
|
|
62
|
+
npx brignano --version # print the version
|
|
49
63
|
npx brignano --qr # print a QR code to my site
|
|
50
64
|
npx brignano --vcard # save my contact card
|
|
51
65
|
npx brignano --no-prompt # card only (useful in CI/scripts)
|
|
@@ -61,7 +75,13 @@ npm run build
|
|
|
61
75
|
node dist/card.js
|
|
62
76
|
```
|
|
63
77
|
|
|
64
|
-
Source lives in `src/` (split into small modules — `card.ts` is the entry/`bin`
|
|
78
|
+
Source lives in `src/` (split into small modules — `card.ts` is the entry/`bin`
|
|
79
|
+
target) and compiles to `dist/` via `tsc`. `npm run dev` runs the TypeScript
|
|
80
|
+
directly through `tsx`; `npm run lint` and `npm run format` use
|
|
81
|
+
[Biome](https://biomejs.dev).
|
|
82
|
+
|
|
83
|
+
Only `dist/` is published — the tarball is ~8 kB, so `npx brignano` stays a fast
|
|
84
|
+
cold start.
|
|
65
85
|
|
|
66
86
|
## Prerequisites
|
|
67
87
|
|
|
@@ -78,4 +98,4 @@ Inspired by [anmol098/npx_card](https://github.com/anmol098/npx_card).
|
|
|
78
98
|
|
|
79
99
|
## License
|
|
80
100
|
|
|
81
|
-
|
|
101
|
+
[ISC](LICENSE) © Anthony Brignano
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
paths:
|
|
6
|
-
- '**/*.ts'
|
|
7
|
-
- '*.json'
|
|
8
|
-
- '.github/workflows/ci.yml'
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
build:
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
strategy:
|
|
14
|
-
matrix:
|
|
15
|
-
node-version: [20.x, 22.x, 24.x]
|
|
16
|
-
steps:
|
|
17
|
-
- uses: actions/checkout@v6
|
|
18
|
-
- name: Use Node.js ${{ matrix.node-version }}
|
|
19
|
-
uses: actions/setup-node@v6
|
|
20
|
-
with:
|
|
21
|
-
node-version: ${{ matrix.node-version }}
|
|
22
|
-
- name: Install dependencies
|
|
23
|
-
run: npm ci
|
|
24
|
-
- name: Build
|
|
25
|
-
run: npm run build
|
|
26
|
-
- name: Test (non-interactive)
|
|
27
|
-
run: npm test
|
|
28
|
-
env:
|
|
29
|
-
CI: true
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
|
2
|
-
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
|
|
3
|
-
|
|
4
|
-
name: Publish Package
|
|
5
|
-
|
|
6
|
-
on:
|
|
7
|
-
workflow_dispatch:
|
|
8
|
-
release:
|
|
9
|
-
types: [created]
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
publish:
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
steps:
|
|
15
|
-
- uses: actions/checkout@v6
|
|
16
|
-
- name: Use Node.js
|
|
17
|
-
uses: actions/setup-node@v6
|
|
18
|
-
with:
|
|
19
|
-
node-version: '22.x'
|
|
20
|
-
registry-url: 'https://registry.npmjs.org'
|
|
21
|
-
- name: Install & Build
|
|
22
|
-
run: |
|
|
23
|
-
npm ci
|
|
24
|
-
npm run build
|
|
25
|
-
- name: Test (non-interactive)
|
|
26
|
-
run: npm test
|
|
27
|
-
env:
|
|
28
|
-
CI: true
|
|
29
|
-
- name: Publish to npm
|
|
30
|
-
env:
|
|
31
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
32
|
-
run: npm publish --access public
|
package/demo.gif
DELETED
|
Binary file
|
package/src/banner.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import figlet from 'figlet';
|
|
2
|
-
import gradient from 'gradient-string';
|
|
3
|
-
import { sleep } from './reveal.js';
|
|
4
|
-
|
|
5
|
-
// Brand palette — matches brignano.io's electric indigo/violet accent
|
|
6
|
-
// (violet-700 → violet-500 → violet-400). Reads well on a dark terminal.
|
|
7
|
-
const BRAND = ['#6d28d9', '#8b5cf6', '#a78bfa'];
|
|
8
|
-
|
|
9
|
-
// A longer ramp with a bright crest (violet-300) used for the shimmer sweep.
|
|
10
|
-
const SHIMMER = [
|
|
11
|
-
'#5b21b6',
|
|
12
|
-
'#6d28d9',
|
|
13
|
-
'#7c3aed',
|
|
14
|
-
'#8b5cf6',
|
|
15
|
-
'#a78bfa',
|
|
16
|
-
'#c4b5fd',
|
|
17
|
-
'#a78bfa',
|
|
18
|
-
'#8b5cf6',
|
|
19
|
-
'#7c3aed',
|
|
20
|
-
'#6d28d9',
|
|
21
|
-
];
|
|
22
|
-
|
|
23
|
-
function bannerArt(text: string): string {
|
|
24
|
-
try {
|
|
25
|
-
return figlet.textSync(text, { font: 'ANSI Shadow' });
|
|
26
|
-
} catch {
|
|
27
|
-
return text.toUpperCase();
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function paint(colors: string[], art: string): string {
|
|
32
|
-
try {
|
|
33
|
-
return gradient(colors).multiline(art);
|
|
34
|
-
} catch {
|
|
35
|
-
return art;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Static gradient banner (used for the non-animated path).
|
|
40
|
-
export function renderBanner(text = 'brignano'): string {
|
|
41
|
-
return paint(BRAND, bannerArt(text));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Animated splash: a violet highlight sweeps across the letters, then settles on
|
|
45
|
-
// the static brand gradient. Redraws in place via ANSI cursor moves, so it must
|
|
46
|
-
// only be called on a TTY (the caller gates this behind `shouldAnimate`).
|
|
47
|
-
export async function animateBanner(
|
|
48
|
-
text = 'brignano',
|
|
49
|
-
frames = 14,
|
|
50
|
-
frameMs = 70,
|
|
51
|
-
): Promise<void> {
|
|
52
|
-
const art = bannerArt(text);
|
|
53
|
-
const lineCount = art.split('\n').length;
|
|
54
|
-
|
|
55
|
-
for (let i = 0; i < frames; i++) {
|
|
56
|
-
const offset = i % SHIMMER.length;
|
|
57
|
-
const rotated = SHIMMER.slice(offset).concat(SHIMMER.slice(0, offset));
|
|
58
|
-
if (i > 0) process.stdout.write(`\u001b[${lineCount}A`);
|
|
59
|
-
process.stdout.write(`\r${paint(rotated, art)}\n`);
|
|
60
|
-
await sleep(frameMs);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Settle on the canonical brand gradient.
|
|
64
|
-
process.stdout.write(`\u001b[${lineCount}A`);
|
|
65
|
-
process.stdout.write(`${renderBanner(text)}\n`);
|
|
66
|
-
}
|
package/src/card.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import inquirer from 'inquirer';
|
|
5
|
-
import { animateBanner, renderBanner } from './banner.js';
|
|
6
|
-
import { renderCard } from './cardBox.js';
|
|
7
|
-
import { funFacts, pick, profile, taglines } from './data.js';
|
|
8
|
-
import {
|
|
9
|
-
clearConsole,
|
|
10
|
-
isInteractive,
|
|
11
|
-
parseFlags,
|
|
12
|
-
shouldAnimate,
|
|
13
|
-
shouldFetchLive,
|
|
14
|
-
} from './env.js';
|
|
15
|
-
import { fetchLive } from './live.js';
|
|
16
|
-
import { buildChoices, sendEmail, viewResume } from './menu.js';
|
|
17
|
-
import { renderQr } from './qrcode.js';
|
|
18
|
-
import { reveal, sleep } from './reveal.js';
|
|
19
|
-
import { writeAndOpenVCard } from './vcard.js';
|
|
20
|
-
|
|
21
|
-
function printHelp(): void {
|
|
22
|
-
console.log('\nUsage: npx brignano [options]\n');
|
|
23
|
-
console.log('Options:');
|
|
24
|
-
console.log(' -h, --help Show this help');
|
|
25
|
-
console.log(' -v, --view Open my resume in your browser');
|
|
26
|
-
console.log(' -e, --email Open your mail client to email me');
|
|
27
|
-
console.log(' --qr Print a scannable QR code to my website');
|
|
28
|
-
console.log(' --vcard Save my contact card (.vcf) and open it');
|
|
29
|
-
console.log(' --no-animation Render instantly, skip the reveal animation');
|
|
30
|
-
console.log(' --no-prompt Show the card only (non-interactive)');
|
|
31
|
-
console.log(' -d, --download Deprecated alias for --view\n');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function main(): Promise<void> {
|
|
35
|
-
const argv = process.argv.slice(2);
|
|
36
|
-
const flags = parseFlags(argv);
|
|
37
|
-
|
|
38
|
-
// Direct-action flags short-circuit the full experience.
|
|
39
|
-
if (flags.help) return printHelp();
|
|
40
|
-
if (flags.view || flags.download) {
|
|
41
|
-
if (flags.download) {
|
|
42
|
-
console.warn('Note: --download is deprecated; opening the resume instead.');
|
|
43
|
-
}
|
|
44
|
-
return viewResume();
|
|
45
|
-
}
|
|
46
|
-
if (flags.email) return sendEmail();
|
|
47
|
-
if (flags.qr) {
|
|
48
|
-
console.log(await renderQr(profile.website));
|
|
49
|
-
console.log(chalk.gray(` ${profile.website}\n`));
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
if (flags.vcard) return writeAndOpenVCard();
|
|
53
|
-
|
|
54
|
-
const animate = shouldAnimate(flags);
|
|
55
|
-
const interactive = isInteractive(flags);
|
|
56
|
-
|
|
57
|
-
// Kick off the live fetch early (don't await yet) so its timeout budget
|
|
58
|
-
// overlaps the banner render. Skipped entirely when non-interactive/CI.
|
|
59
|
-
const livePromise = shouldFetchLive(flags) ? fetchLive() : Promise.resolve(null);
|
|
60
|
-
|
|
61
|
-
clearConsole();
|
|
62
|
-
|
|
63
|
-
// Banner first: an animated violet shimmer when we can animate, otherwise the
|
|
64
|
-
// static gradient.
|
|
65
|
-
if (animate) {
|
|
66
|
-
await animateBanner();
|
|
67
|
-
await sleep(120);
|
|
68
|
-
} else {
|
|
69
|
-
console.log(renderBanner());
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Card, enriched with whatever the (bounded) live fetch returned.
|
|
73
|
-
const live = await livePromise;
|
|
74
|
-
const tagline = pick(taglines);
|
|
75
|
-
await reveal(renderCard({ live, tagline }), 30, animate);
|
|
76
|
-
|
|
77
|
-
console.log(`Tip: ${chalk.cyanBright.bold('cmd/ctrl + click')} the links above.`);
|
|
78
|
-
console.log(chalk.gray(` ${pick(funFacts)}\n`));
|
|
79
|
-
|
|
80
|
-
if (!interactive) return;
|
|
81
|
-
|
|
82
|
-
const prompt = inquirer.createPromptModule();
|
|
83
|
-
const { action } = await prompt([
|
|
84
|
-
{
|
|
85
|
-
type: 'list',
|
|
86
|
-
name: 'action',
|
|
87
|
-
message: 'What would you like to do?',
|
|
88
|
-
choices: buildChoices(),
|
|
89
|
-
},
|
|
90
|
-
]);
|
|
91
|
-
await action();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
main().catch((err: unknown) => {
|
|
95
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
96
|
-
console.error(`Something went wrong: ${message}`);
|
|
97
|
-
process.exitCode = 1;
|
|
98
|
-
});
|
package/src/cardBox.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import boxen from 'boxen';
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { profile } from './data.js';
|
|
4
|
-
import type { LiveData } from './live.js';
|
|
5
|
-
|
|
6
|
-
// Right-aligns labels to a common width so the values line up cleanly.
|
|
7
|
-
const LABEL_WIDTH = 10;
|
|
8
|
-
function label(text: string): string {
|
|
9
|
-
return chalk.white.bold(text.padStart(LABEL_WIDTH) + ':');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface CardOptions {
|
|
13
|
-
live?: LiveData | null;
|
|
14
|
-
tagline: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Builds the boxen business card. `live` is optional — when absent the live
|
|
18
|
-
// line is simply omitted, so the card looks intentional with or without network.
|
|
19
|
-
export function renderCard({ live, tagline }: CardOptions): string {
|
|
20
|
-
// Violet accent matches brignano.io (violet-400 #a78bfa on dark).
|
|
21
|
-
const accent = chalk.hex('#a78bfa');
|
|
22
|
-
const lines = [
|
|
23
|
-
accent.bold(profile.fullName) + ' ' + chalk.gray(profile.handle),
|
|
24
|
-
'',
|
|
25
|
-
`${label('Work')} ${profile.title} ${chalk.gray('at')} ${chalk
|
|
26
|
-
.hex('#3A5A78')
|
|
27
|
-
.bold(profile.org)}`,
|
|
28
|
-
'',
|
|
29
|
-
`${label('GitHub')} ${chalk.gray('https://github.com/')}${chalk.green(
|
|
30
|
-
profile.username,
|
|
31
|
-
)}`,
|
|
32
|
-
`${label('LinkedIn')} ${chalk.gray('https://linkedin.com/in/')}${chalk.blue(
|
|
33
|
-
profile.username,
|
|
34
|
-
)}`,
|
|
35
|
-
`${label('Website')} ${chalk.cyan(profile.website)}`,
|
|
36
|
-
];
|
|
37
|
-
|
|
38
|
-
if (live) {
|
|
39
|
-
lines.push('');
|
|
40
|
-
const parts = [
|
|
41
|
-
`${chalk.yellow('★')} ${chalk.bold(live.followers)} followers`,
|
|
42
|
-
`${chalk.bold(live.publicRepos)} repos`,
|
|
43
|
-
];
|
|
44
|
-
if (live.latestRepo) {
|
|
45
|
-
parts.push(`latest: ${chalk.green(live.latestRepo.name)}`);
|
|
46
|
-
}
|
|
47
|
-
lines.push(`${label('Live')} ${chalk.gray(parts.join(' · '))}`);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
lines.push(
|
|
51
|
-
'',
|
|
52
|
-
`${label('Card')} ${chalk.red('npx')} ${chalk.white('brignano')}`,
|
|
53
|
-
'',
|
|
54
|
-
chalk.italic(tagline),
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
return boxen(lines.join('\n'), {
|
|
58
|
-
margin: 1,
|
|
59
|
-
float: 'center',
|
|
60
|
-
padding: 1,
|
|
61
|
-
borderStyle: 'round',
|
|
62
|
-
borderColor: 'gray',
|
|
63
|
-
});
|
|
64
|
-
}
|
package/src/data.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// Single source of truth for everything the card, vCard, QR and live modules need.
|
|
2
|
-
// Plain (uncolored) values live here; chalk styling is applied at render time.
|
|
3
|
-
|
|
4
|
-
export const profile = {
|
|
5
|
-
firstName: 'Anthony',
|
|
6
|
-
lastName: 'Brignano',
|
|
7
|
-
fullName: 'Anthony Brignano',
|
|
8
|
-
handle: '@brignano',
|
|
9
|
-
username: 'brignano',
|
|
10
|
-
title: 'Senior Staff Software Engineer',
|
|
11
|
-
org: 'The Hartford',
|
|
12
|
-
email: 'hi@brignano.io',
|
|
13
|
-
website: 'https://brignano.io',
|
|
14
|
-
resume: 'https://brignano.io/resume',
|
|
15
|
-
github: 'https://github.com/brignano',
|
|
16
|
-
linkedin: 'https://linkedin.com/in/brignano',
|
|
17
|
-
} as const;
|
|
18
|
-
|
|
19
|
-
// Rotating taglines — one is picked per run so repeat visitors see variety.
|
|
20
|
-
export const taglines: string[] = [
|
|
21
|
-
'My inbox is always open. Ask a question or just say hi — I read everything.',
|
|
22
|
-
'Building reliable systems, mentoring engineers, and shipping things that last.',
|
|
23
|
-
"If it's worth automating, it's worth a clean abstraction. Let's build.",
|
|
24
|
-
'Self-hoster, spec-driven, and unreasonably fond of a tidy terminal.',
|
|
25
|
-
'Good engineering is mostly good taste applied consistently. Say hello.',
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
// Small easter-egg facts, shown occasionally below the card.
|
|
29
|
-
export const funFacts: string[] = [
|
|
30
|
-
'This card is open source — `npx brignano --help` shows every trick.',
|
|
31
|
-
'Tip: scan the QR with your phone to save my contact instantly.',
|
|
32
|
-
'The live stats above are pulled fresh from GitHub (with a strict timeout).',
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
// Deterministic-enough pick without Math.random (kept simple + dependency-free).
|
|
36
|
-
export function pick<T>(items: readonly T[], seed = Date.now()): T {
|
|
37
|
-
return items[Math.floor(seed / 1000) % items.length];
|
|
38
|
-
}
|
package/src/env.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
// Environment + capability detection. Keeps the render logic free of scattered
|
|
2
|
-
// `process.stdout.isTTY` checks and guarantees CI / piped output never hangs.
|
|
3
|
-
|
|
4
|
-
export interface Flags {
|
|
5
|
-
help: boolean;
|
|
6
|
-
view: boolean;
|
|
7
|
-
download: boolean;
|
|
8
|
-
email: boolean;
|
|
9
|
-
qr: boolean;
|
|
10
|
-
vcard: boolean;
|
|
11
|
-
noPrompt: boolean;
|
|
12
|
-
noAnimation: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function hasFlag(argv: string[], names: string[]): boolean {
|
|
16
|
-
return names.some((n) => argv.includes(n));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function parseFlags(argv: string[]): Flags {
|
|
20
|
-
return {
|
|
21
|
-
help: hasFlag(argv, ['--help', '-h']),
|
|
22
|
-
view: hasFlag(argv, ['--view', '-v']),
|
|
23
|
-
download: hasFlag(argv, ['--download', '-d']),
|
|
24
|
-
email: hasFlag(argv, ['--email', '-e']),
|
|
25
|
-
qr: hasFlag(argv, ['--qr']),
|
|
26
|
-
vcard: hasFlag(argv, ['--vcard', '--save-contact']),
|
|
27
|
-
noPrompt: hasFlag(argv, ['--no-prompt', '--quiet']),
|
|
28
|
-
noAnimation: hasFlag(argv, ['--no-animation']),
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const isCI = (): boolean => Boolean(process.env.CI);
|
|
33
|
-
|
|
34
|
-
// True only when we can safely show an interactive inquirer prompt.
|
|
35
|
-
export function isInteractive(flags: Flags): boolean {
|
|
36
|
-
return Boolean(
|
|
37
|
-
process.stdout.isTTY && process.stdin.isTTY && !isCI() && !flags.noPrompt,
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// True only when the reveal animation should play.
|
|
42
|
-
export function shouldAnimate(flags: Flags): boolean {
|
|
43
|
-
return Boolean(
|
|
44
|
-
process.stdout.isTTY &&
|
|
45
|
-
!isCI() &&
|
|
46
|
-
!flags.noPrompt &&
|
|
47
|
-
!flags.noAnimation &&
|
|
48
|
-
!process.env.NO_COLOR,
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// True only when we should spend time on a live network fetch.
|
|
53
|
-
export function shouldFetchLive(flags: Flags): boolean {
|
|
54
|
-
return isInteractive(flags);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function clearConsole(): void {
|
|
58
|
-
const out = process.stdout as NodeJS.WriteStream & { clear?: () => void };
|
|
59
|
-
if (typeof out.clear === 'function') {
|
|
60
|
-
try {
|
|
61
|
-
out.clear();
|
|
62
|
-
} catch {
|
|
63
|
-
/* ignore */
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (process.stdout.isTTY) {
|
|
67
|
-
process.stdout.write('\u001b[2J\u001b[0f');
|
|
68
|
-
}
|
|
69
|
-
}
|
package/src/live.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { profile } from './data.js';
|
|
2
|
-
|
|
3
|
-
export interface LiveData {
|
|
4
|
-
followers: number;
|
|
5
|
-
publicRepos: number;
|
|
6
|
-
latestRepo?: { name: string; url: string };
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const API = 'https://api.github.com';
|
|
10
|
-
const HEADERS = {
|
|
11
|
-
'User-Agent': 'npx-brignano',
|
|
12
|
-
Accept: 'application/vnd.github+json',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
// Fetches live GitHub stats with a hard timeout. ANY failure (timeout, offline,
|
|
16
|
-
// non-200, rate-limit, parse error) resolves to null — this never throws and
|
|
17
|
-
// never blocks the card for longer than `timeoutMs`.
|
|
18
|
-
export async function fetchLive(timeoutMs = 1500): Promise<LiveData | null> {
|
|
19
|
-
const controller = new AbortController();
|
|
20
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
21
|
-
try {
|
|
22
|
-
const [userRes, reposRes] = await Promise.all([
|
|
23
|
-
fetch(`${API}/users/${profile.username}`, {
|
|
24
|
-
headers: HEADERS,
|
|
25
|
-
signal: controller.signal,
|
|
26
|
-
}),
|
|
27
|
-
fetch(
|
|
28
|
-
`${API}/users/${profile.username}/repos?sort=updated&per_page=1`,
|
|
29
|
-
{ headers: HEADERS, signal: controller.signal },
|
|
30
|
-
),
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
if (!userRes.ok || !reposRes.ok) return null;
|
|
34
|
-
|
|
35
|
-
const user = (await userRes.json()) as {
|
|
36
|
-
followers?: number;
|
|
37
|
-
public_repos?: number;
|
|
38
|
-
};
|
|
39
|
-
const repos = (await reposRes.json()) as Array<{
|
|
40
|
-
name?: string;
|
|
41
|
-
html_url?: string;
|
|
42
|
-
}>;
|
|
43
|
-
|
|
44
|
-
const latest = Array.isArray(repos) ? repos[0] : undefined;
|
|
45
|
-
return {
|
|
46
|
-
followers: user.followers ?? 0,
|
|
47
|
-
publicRepos: user.public_repos ?? 0,
|
|
48
|
-
latestRepo:
|
|
49
|
-
latest?.name && latest?.html_url
|
|
50
|
-
? { name: latest.name, url: latest.html_url }
|
|
51
|
-
: undefined,
|
|
52
|
-
};
|
|
53
|
-
} catch {
|
|
54
|
-
return null;
|
|
55
|
-
} finally {
|
|
56
|
-
clearTimeout(timer);
|
|
57
|
-
}
|
|
58
|
-
}
|
package/src/menu.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import clipboard from 'clipboardy';
|
|
3
|
-
import open from 'open';
|
|
4
|
-
import { profile } from './data.js';
|
|
5
|
-
import { renderQr } from './qrcode.js';
|
|
6
|
-
import { buildVCard, writeAndOpenVCard } from './vcard.js';
|
|
7
|
-
|
|
8
|
-
// --- Action helpers (kept as named functions so flags and the menu share them) ---
|
|
9
|
-
|
|
10
|
-
export async function copyEmail(): Promise<void> {
|
|
11
|
-
try {
|
|
12
|
-
await clipboard.write(profile.email);
|
|
13
|
-
console.log(`\n${chalk.green('Copied')} ${profile.email} to your clipboard.\n`);
|
|
14
|
-
} catch {
|
|
15
|
-
// Clipboard access fails in headless/SSH environments — degrade gracefully.
|
|
16
|
-
console.log(`\nMy email is ${chalk.green(profile.email)} — drop me a line!\n`);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function sendEmail(): Promise<void> {
|
|
21
|
-
try {
|
|
22
|
-
await open(`mailto:${profile.email}`);
|
|
23
|
-
console.log('\nOpened your mail client — see you in the inbox.\n');
|
|
24
|
-
} catch (err) {
|
|
25
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
26
|
-
console.error(`Failed to open mail client: ${message}`);
|
|
27
|
-
console.error(`Reach me directly at ${profile.email}`);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export async function openUrl(url: string, label: string): Promise<void> {
|
|
32
|
-
try {
|
|
33
|
-
await open(url);
|
|
34
|
-
console.log(`\nOpened ${label}: ${url}\n`);
|
|
35
|
-
} catch (err) {
|
|
36
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
37
|
-
console.error(`Failed to open ${label}: ${message}`);
|
|
38
|
-
console.error(`Here it is: ${url}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export async function viewResume(): Promise<void> {
|
|
43
|
-
await openUrl(profile.resume, 'resume');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function showContactQr(): Promise<void> {
|
|
47
|
-
const qr = await renderQr(buildVCard());
|
|
48
|
-
console.log('\nScan to save my contact:\n');
|
|
49
|
-
console.log(qr);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// --- Menu definition ---
|
|
53
|
-
|
|
54
|
-
export interface MenuChoice {
|
|
55
|
-
name: string;
|
|
56
|
-
value: () => void | Promise<void>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function buildChoices(): MenuChoice[] {
|
|
60
|
-
return [
|
|
61
|
-
{ name: `Copy my ${chalk.green.bold('email')} to clipboard`, value: copyEmail },
|
|
62
|
-
{ name: `Send me an ${chalk.green.bold('email')}`, value: sendEmail },
|
|
63
|
-
{
|
|
64
|
-
name: `Open my ${chalk.green.bold('GitHub')}`,
|
|
65
|
-
value: () => openUrl(profile.github, 'GitHub'),
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
name: `Open my ${chalk.blue.bold('LinkedIn')}`,
|
|
69
|
-
value: () => openUrl(profile.linkedin, 'LinkedIn'),
|
|
70
|
-
},
|
|
71
|
-
{ name: `View my ${chalk.magentaBright.bold('resume')}`, value: viewResume },
|
|
72
|
-
{
|
|
73
|
-
name: `Save my ${chalk.cyan.bold('contact')} (.vcf)`,
|
|
74
|
-
value: writeAndOpenVCard,
|
|
75
|
-
},
|
|
76
|
-
{ name: `Show a ${chalk.yellow.bold('contact QR')} code`, value: showContactQr },
|
|
77
|
-
{
|
|
78
|
-
name: 'Just quit',
|
|
79
|
-
value: () => {
|
|
80
|
-
console.log('\nHave a great day!\n');
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
];
|
|
84
|
-
}
|
package/src/qrcode.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import qrcode from 'qrcode-terminal';
|
|
2
|
-
|
|
3
|
-
// Promisified wrapper around qrcode-terminal's callback API. `small: true` keeps
|
|
4
|
-
// the code compact enough for most terminals.
|
|
5
|
-
export function renderQr(text: string): Promise<string> {
|
|
6
|
-
return new Promise((resolve) => {
|
|
7
|
-
qrcode.generate(text, { small: true }, (output: string) => resolve(output));
|
|
8
|
-
});
|
|
9
|
-
}
|
package/src/reveal.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// Tiny animation helpers — no dependencies. Line-by-line reveal is immune to the
|
|
2
|
-
// ANSI-escape-splitting problem a per-character typewriter would hit, and the
|
|
3
|
-
// total budget stays well under a second.
|
|
4
|
-
|
|
5
|
-
export function sleep(ms: number): Promise<void> {
|
|
6
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
// Prints `text` one line at a time, pausing `perLineMs` between lines.
|
|
10
|
-
// When `animate` is false, prints everything at once (CI / piped / --no-animation).
|
|
11
|
-
export async function reveal(
|
|
12
|
-
text: string,
|
|
13
|
-
perLineMs = 35,
|
|
14
|
-
animate = true,
|
|
15
|
-
): Promise<void> {
|
|
16
|
-
if (!animate) {
|
|
17
|
-
console.log(text);
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
const lines = text.split('\n');
|
|
21
|
-
for (const line of lines) {
|
|
22
|
-
console.log(line);
|
|
23
|
-
await sleep(perLineMs);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// qrcode-terminal ships no official types and is CJS. Minimal ambient shim.
|
|
2
|
-
declare module 'qrcode-terminal' {
|
|
3
|
-
interface GenerateOptions {
|
|
4
|
-
small?: boolean;
|
|
5
|
-
}
|
|
6
|
-
function generate(
|
|
7
|
-
input: string,
|
|
8
|
-
options?: GenerateOptions,
|
|
9
|
-
callback?: (output: string) => void,
|
|
10
|
-
): void;
|
|
11
|
-
const qrcode: { generate: typeof generate };
|
|
12
|
-
export default qrcode;
|
|
13
|
-
}
|
package/src/vcard.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { writeFileSync } from 'node:fs';
|
|
2
|
-
import { tmpdir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import open from 'open';
|
|
5
|
-
import { profile } from './data.js';
|
|
6
|
-
|
|
7
|
-
// Builds a valid vCard 3.0 string (broadest contact-app compatibility) with the
|
|
8
|
-
// spec-required CRLF line endings.
|
|
9
|
-
export function buildVCard(): string {
|
|
10
|
-
const lines = [
|
|
11
|
-
'BEGIN:VCARD',
|
|
12
|
-
'VERSION:3.0',
|
|
13
|
-
`N:${profile.lastName};${profile.firstName};;;`,
|
|
14
|
-
`FN:${profile.fullName}`,
|
|
15
|
-
`ORG:${profile.org}`,
|
|
16
|
-
`TITLE:${profile.title}`,
|
|
17
|
-
`EMAIL;TYPE=INTERNET,PREF:${profile.email}`,
|
|
18
|
-
`URL:${profile.website}`,
|
|
19
|
-
`X-SOCIALPROFILE;TYPE=github:${profile.github}`,
|
|
20
|
-
`X-SOCIALPROFILE;TYPE=linkedin:${profile.linkedin}`,
|
|
21
|
-
'NOTE:npx brignano',
|
|
22
|
-
'END:VCARD',
|
|
23
|
-
];
|
|
24
|
-
return lines.join('\r\n') + '\r\n';
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Writes the vCard to a temp file and asks the OS to open it (Contacts on
|
|
28
|
-
// macOS/iOS, the default handler elsewhere). On failure, prints the path so the
|
|
29
|
-
// user can open it manually — never throws.
|
|
30
|
-
export async function writeAndOpenVCard(): Promise<void> {
|
|
31
|
-
const file = join(tmpdir(), 'anthony-brignano.vcf');
|
|
32
|
-
try {
|
|
33
|
-
writeFileSync(file, buildVCard(), 'utf8');
|
|
34
|
-
await open(file);
|
|
35
|
-
console.log(`\nSaved my contact card to: ${file}\n`);
|
|
36
|
-
} catch (err) {
|
|
37
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
38
|
-
console.error(`\nCouldn't open the contact card automatically (${message}).`);
|
|
39
|
-
console.error(`It's saved here — open it manually: ${file}\n`);
|
|
40
|
-
}
|
|
41
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ES2020",
|
|
5
|
-
"declaration": false,
|
|
6
|
-
"outDir": "dist",
|
|
7
|
-
"rootDir": "src",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"moduleResolution": "node",
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true
|
|
13
|
-
},
|
|
14
|
-
"include": ["src/**/*"]
|
|
15
|
-
}
|
package/vhs/demo.tape
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# Demo recording for `npx brignano`.
|
|
2
|
-
# Regenerate with: vhs vhs/demo.tape (requires: brew install vhs)
|
|
3
|
-
# Renders the gradient banner, the live card, menu navigation, and a contact QR.
|
|
4
|
-
|
|
5
|
-
Output demo.gif
|
|
6
|
-
Require node
|
|
7
|
-
|
|
8
|
-
Set Shell "zsh"
|
|
9
|
-
Set FontSize 14
|
|
10
|
-
Set Width 1000
|
|
11
|
-
Set Height 1000
|
|
12
|
-
Set Padding 20
|
|
13
|
-
Set Theme "GitHub Dark"
|
|
14
|
-
Set TypingSpeed 55ms
|
|
15
|
-
|
|
16
|
-
# Launch the local build so the demo reflects this branch (not the published npm version).
|
|
17
|
-
Type "node dist/card.js"
|
|
18
|
-
Sleep 400ms
|
|
19
|
-
Enter
|
|
20
|
-
|
|
21
|
-
# Banner animation + card reveal + live GitHub stats.
|
|
22
|
-
Sleep 4s
|
|
23
|
-
|
|
24
|
-
# Walk down the menu to showcase the actions.
|
|
25
|
-
Down
|
|
26
|
-
Sleep 600ms
|
|
27
|
-
Down
|
|
28
|
-
Sleep 600ms
|
|
29
|
-
Down
|
|
30
|
-
Sleep 600ms
|
|
31
|
-
Down
|
|
32
|
-
Sleep 600ms
|
|
33
|
-
Down
|
|
34
|
-
Sleep 600ms
|
|
35
|
-
Down
|
|
36
|
-
Sleep 800ms
|
|
37
|
-
|
|
38
|
-
# Land on "Show a contact QR code" and select it.
|
|
39
|
-
Enter
|
|
40
|
-
Sleep 3s
|