Skip to content

Commit 656ed89

Browse files
committed
feat: enhance snip stats command with JSON output and detailed metrics, refactoring its implementation into a dedicated file.
1 parent f36337e commit 656ed89

6 files changed

Lines changed: 659 additions & 489 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2020
- Pipe JSON as template values: `echo '{"host":"prod"}' | snip pipe deploy --json`
2121
- Stdin passthrough to snippet process
2222
- Zero-chrome output for composability
23+
- `snip stats --json` — machine-readable statistics output
2324
- `stdinData` support in exec engine for stdin passthrough to child processes
2425
- Pipeline integration row in comparison tables (README + docs site)
26+
- Codecov coverage badge in README
27+
- Demo screenshots in README (`snip_sc_1.png`, `snip_sc_2.png`)
2528

2629
### Changed
30+
- `snip stats` extracted to its own command file with brand-colored output, language bar chart, and top tags
31+
- Unified CLI color palette to brand orange `#ff4d00` across list, search, show, run, stats
2732
- README: added `snip pipe` to commands table, replaced "Pipe-Friendly" section with "Pipeline Mode", marked pipe as shipped in roadmap
2833
- docs/index.html: updated feature card to "Unix Pipeline Mode", added comparison row, bumped CLI commands count to 20+
29-
- CHANGELOG: restructured unreleased section
34+
- `jest.config.js`: added coverage config (collectCoverageFrom, thresholds, reporters)
35+
- `color_scheme.md`: rewritten to match actual unified brand palette
3036

3137
## [0.2.0] - 2026-02-27
3238

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ snip doctor # validates storage, editor, fzf, shell, gist
110110
| `snip mv <old> <new>` | Rename a snippet |
111111
| `snip cat <name>` | Print raw content to stdout |
112112
| `snip recent [n]` | Show last _n_ used snippets (default: 5) |
113-
| `snip stats` | Library statistics |
113+
| `snip stats` | Library statistics (`--json`, language chart, top tags) |
114114
| `snip grab <url>` | Import from URL or `github:user/repo/path` |
115115
| `snip fzf` | fzf search with live preview |
116116

docs/GTM_PLAYBOOK.md

Lines changed: 65 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,42 @@ Every dev has commands they re-type. snip is the only CLI snippet manager with *
3333
#### Reddit Post Template (r/commandline)
3434

3535
```
36-
Title: I built "snip" — a terminal snippet manager that actually executes code across any language
36+
Title: I built "snip" — a CLI snippet manager that runs code in any language, not just shell
3737
38-
I kept losing one-off deploy scripts, docker commands, and API calls in my shell
39-
history. So I built snip.
38+
I got tired of losing deploy scripts, docker commands, and one-off API calls
39+
in my shell history. So I built snip — it saves, searches, and executes
40+
code snippets directly from your terminal.
4041
41-
What makes it different from pet/navi/aliases:
42+
What makes it different from pet/navi/shell aliases:
4243
43-
- Runs snippets in any language (sh, python, js, ruby) — not just shell
44-
- Pipeline mode: `echo '{"host":"prod"}' | snip pipe deploy --json`
45-
- Dangerous command detection (warns on rm -rf, sudo)
46-
- Interactive TUI with syntax highlighting
47-
- Parameterized templates: `docker run {{image:ubuntu}} {{cmd:bash}}`
48-
- Zero config: `npm install -g snip-manager`
44+
• Runs any language — sh, python, js, ruby, php, perl (not just shell)
45+
• Pipeline mode: echo '{"host":"prod"}' | snip pipe deploy --json
46+
• Parameterized templates: docker run {{image:ubuntu}} {{cmd:bash}}
47+
• Dangerous command detection — warns on rm -rf, sudo, dd before running
48+
• Interactive TUI with syntax highlighting and split-pane preview
49+
• fzf integration with live preview pane
50+
• Shell aliases — eval "$(snip alias)" turns every snippet into a command
51+
• Ctrl+G shell widget — search and paste snippets inline without leaving your prompt
52+
• GitHub Gist sync — push/pull your library across machines
53+
• Health check — snip doctor verifies your entire setup
54+
• JSON + SQLite backends — start simple, scale when your library grows
55+
• 58 tests, 20+ commands, zero config
4956
57+
Quick demo:
58+
59+
$ echo 'const os = require("os"); console.log(os.hostname())' | snip add sys-info --lang js
60+
$ snip exec sys-info
61+
→ Bharaths-MacBook-Air.local
62+
63+
$ snip stats --json | jq .totalRuns
64+
→ 14
65+
66+
Install: npm install -g snip-manager
5067
GitHub: https://github.com/Bharath-code/snip
51-
npm: https://npmjs.com/package/snip-manager
68+
Docs: https://bharath-code.github.io/snip/
5269
53-
Would love feedback — especially on what commands you'd want.
70+
What workflow would you use this for? I'm curious what commands people
71+
lose the most.
5472
```
5573

5674
> [!IMPORTANT]
@@ -63,22 +81,27 @@ Would love feedback — especially on what commands you'd want.
6381
**Post on a Tuesday or Wednesday, 10 AM ET.** This is when Show HN gets the most traffic.
6482

6583
```
66-
Title: Show HN: Snip – Terminal snippet manager with multi-lang execution and unix pipelines
84+
Title: Show HN: Snip – CLI snippet manager with multi-lang execution and unix pipelines
6785
6886
URL: https://github.com/Bharath-code/snip
6987
7088
First comment (post immediately):
7189
7290
Hi HN, I built snip because I kept losing deploy scripts and docker
7391
commands in my shell history. The existing tools (pet, navi) only run
74-
shell commands — snip runs JS, Python, Ruby, anything.
92+
shell — snip runs JS, Python, Ruby, anything.
7593
76-
The feature I'm most proud of: `snip pipe`. You can pipe JSON into a
77-
snippet to fill template variables — makes it composable with CI/CD
78-
and unix pipelines.
94+
Key differentiators:
95+
- Pipeline mode: pipe JSON into snippets as template values
96+
- Parameterized templates with {{var:default}} syntax
97+
- Dangerous command detection (rm -rf, sudo, dd, fork bombs)
98+
- Interactive TUI with syntax highlighting (blessed)
99+
- fzf integration, Ctrl+G shell widget, shell aliases
100+
- GitHub Gist sync for cross-machine portability
101+
- JSON or SQLite storage, zero external services
79102
80-
Built with Node.js, Commander.js, blessed for TUI, Fuse.js for fuzzy
81-
search. Zero external services, fully local-first.
103+
Tech stack: Node.js, Commander.js, Fuse.js (fuzzy search), blessed (TUI).
104+
58 tests, 20+ commands. Fully local-first, MIT license.
82105
83106
npm install -g snip-manager
84107
@@ -101,55 +124,63 @@ I built a terminal tool that replaces my 47 shell aliases.
101124
It runs snippets in any language, detects dangerous commands,
102125
and pipes JSON into templates.
103126
104-
Open source, zero config.
127+
Open source. Zero config. 20+ commands.
105128
106129
🧵 Here's what makes snip different:
107130
108131
Tweet 2:
109132
The problem:
110133
→ Aliases break across machines
111-
→ Shell history disappears
134+
→ Shell history disappears
112135
→ You can't run a Python snippet from a shell alias
113136
114137
snip stores code snippets and runs them in their native language.
138+
JS, Python, Ruby, PHP — not just shell.
115139
116-
`snip exec deploy-api` ← runs immediately
117-
`snip run deploy-api` ← previews first
140+
$ echo 'console.log(os.hostname())' | snip add sys-info --lang js
141+
$ snip exec sys-info
142+
→ my-macbook.local
118143
119144
Tweet 3:
120145
The killer feature: pipeline mode.
121146
122147
echo '{"host":"prod"}' | snip pipe deploy --json
123148
124149
Pipe JSON → template variables get filled → snippet runs.
125-
126150
No other snippet manager does this.
127151
128152
Tweet 4:
129153
Safety built in:
130154
131-
snip detects `rm -rf`, `sudo`, and destructive commands.
132-
Shows a warning before execution.
155+
snip detects rm -rf, sudo, dd, fork bombs, curl|bash,
156+
and 12 other destructive patterns.
157+
158+
Shows a warning box before execution.
159+
You type "yes" to confirm — not just Enter.
133160
134161
Because running the wrong deploy script at 3am shouldn't be easy.
135162
136163
Tweet 5:
137-
Template variables with defaults:
138-
139-
docker run {{image:ubuntu:24.04}} {{cmd:bash}}
164+
More features that ship out of the box:
140165
141-
When you run the snippet, snip prompts for each variable.
142-
Defaults are pre-filled. Press Enter to accept.
166+
→ fzf with live preview pane
167+
→ Interactive TUI with syntax highlighting
168+
→ Ctrl+G shell widget (search inline)
169+
→ Shell aliases: eval "$(snip alias)"
170+
→ GitHub Gist sync across machines
171+
→ snip doctor health check
172+
→ JSON + SQLite backends
173+
→ snip stats --json for metrics
143174
144175
Tweet 6:
145-
It's open source. MIT license.
176+
It's open source. MIT license. 58 tests.
146177
147178
npm install -g snip-manager
148179
149180
GitHub: github.com/Bharath-code/snip
150-
Website: bharath-code.github.io/snip/
181+
Docs: bharath-code.github.io/snip/
151182
152-
I'd love your feedback — what commands do you wish existed?
183+
What commands do you lose the most? I'm building what you need.
153184
```
154185

155186
**Hashtags for discoverability:** `#cli` `#devtools` `#opensource` `#webdev` `#terminal`

lib/cli.js

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -187,32 +187,12 @@ program
187187
.option('--lang <lang>', 'Language (auto-detected if omitted)')
188188
.action((url, opts) => grabCmd(url, opts));
189189

190+
const statsCmd = require('./commands/stats');
190191
program
191192
.command('stats')
192193
.description('Show snippet library statistics')
193-
.action(() => {
194-
const storage = require('./storage');
195-
const all = storage.listSnippets();
196-
const langMap = {};
197-
let totalUsage = 0;
198-
let mostUsed = null;
199-
for (const s of all) {
200-
const lang = s.language || 'unknown';
201-
langMap[lang] = (langMap[lang] || 0) + 1;
202-
totalUsage += s.usageCount || 0;
203-
if (!mostUsed || (s.usageCount || 0) > (mostUsed.usageCount || 0)) mostUsed = s;
204-
}
205-
console.log(`\n Snippets: ${all.length}`);
206-
console.log(` Total runs: ${totalUsage}`);
207-
if (mostUsed && mostUsed.usageCount) {
208-
console.log(` Most used: ${mostUsed.name} (${mostUsed.usageCount} runs)`);
209-
}
210-
const langs = Object.entries(langMap).sort((a, b) => b[1] - a[1]);
211-
if (langs.length) {
212-
console.log(` Languages: ${langs.map(([l, c]) => `${l} (${c})`).join(', ')}`);
213-
}
214-
console.log('');
215-
});
194+
.option('--json', 'Output as JSON')
195+
.action((opts) => statsCmd(opts));
216196

217197
program
218198
.command('cp <source> <dest>')

lib/commands/stats.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* snip stats — library statistics.
3+
*
4+
* Usage:
5+
* snip stats # colored terminal output
6+
* snip stats --json # machine-readable JSON
7+
*/
8+
9+
const storage = require('../storage');
10+
11+
// Brand-consistent colors (graceful fallback)
12+
let chalk = null;
13+
try {
14+
const m = require('chalk');
15+
chalk = (m && m.default) ? m.default : m;
16+
} catch (_) { }
17+
const c = {
18+
accent: (t) => chalk ? chalk.hex('#ff4d00').bold(t) : t,
19+
val: (t) => chalk ? chalk.hex('#ff7a33')(t) : t,
20+
tag: (t) => chalk ? chalk.hex('#F5A623')(t) : t,
21+
muted: (t) => chalk ? chalk.hex('#6C7086')(t) : t,
22+
dim: (t) => chalk ? chalk.dim(t) : t,
23+
};
24+
25+
function statsCmd(opts = {}) {
26+
const all = storage.listSnippets();
27+
const langMap = {};
28+
const tagMap = {};
29+
let totalUsage = 0;
30+
let mostUsed = null;
31+
let leastUsed = null;
32+
33+
for (const s of all) {
34+
const lang = s.language || 'unknown';
35+
langMap[lang] = (langMap[lang] || 0) + 1;
36+
totalUsage += s.usageCount || 0;
37+
38+
for (const tag of (s.tags || [])) {
39+
tagMap[tag] = (tagMap[tag] || 0) + 1;
40+
}
41+
42+
if (!mostUsed || (s.usageCount || 0) > (mostUsed.usageCount || 0)) mostUsed = s;
43+
if (!leastUsed || (s.usageCount || 0) < (leastUsed.usageCount || 0)) leastUsed = s;
44+
}
45+
46+
const languages = Object.entries(langMap).sort((a, b) => b[1] - a[1]);
47+
const topTags = Object.entries(tagMap).sort((a, b) => b[1] - a[1]).slice(0, 10);
48+
49+
// --json: machine-readable output
50+
if (opts.json) {
51+
console.log(JSON.stringify({
52+
total: all.length,
53+
totalRuns: totalUsage,
54+
mostUsed: mostUsed && mostUsed.usageCount ? { name: mostUsed.name, runs: mostUsed.usageCount } : null,
55+
languages: Object.fromEntries(languages),
56+
topTags: Object.fromEntries(topTags),
57+
}, null, 2));
58+
return;
59+
}
60+
61+
// Colored terminal output
62+
console.log('');
63+
console.log(c.accent(' ─── snip stats ───'));
64+
console.log('');
65+
console.log(` Snippets ${c.val(all.length)}`);
66+
console.log(` Total runs ${c.val(totalUsage)}`);
67+
68+
if (mostUsed && mostUsed.usageCount) {
69+
console.log(` Most used ${c.val(mostUsed.name)} ${c.muted(`(${mostUsed.usageCount} runs)`)}`);
70+
}
71+
72+
if (languages.length) {
73+
console.log('');
74+
console.log(c.dim(' Languages'));
75+
for (const [lang, count] of languages) {
76+
const bar = '█'.repeat(Math.max(1, Math.round((count / all.length) * 20)));
77+
console.log(` ${c.val(lang.padEnd(14))} ${c.accent(bar)} ${c.muted(count)}`);
78+
}
79+
}
80+
81+
if (topTags.length) {
82+
console.log('');
83+
console.log(c.dim(' Top tags'));
84+
console.log(` ${topTags.map(([t, n]) => c.tag(`${t}(${n})`)).join(' ')}`);
85+
}
86+
87+
console.log('');
88+
}
89+
90+
module.exports = statsCmd;

0 commit comments

Comments
 (0)