snip-manager 0.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/lib/cli.js ADDED
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ const { program } = require('commander');
3
+ const pkg = require('../package.json');
4
+ const config = require('./config');
5
+ const addCmd = require('./commands/add');
6
+ const listCmd = require('./commands/list');
7
+ const searchCmd = require('./commands/search');
8
+ const showCmd = require('./commands/show');
9
+ const runCmd = require('./commands/run');
10
+ const cfgCmd = require('./commands/config');
11
+
12
+ program.name('snip').version(pkg.version).description(pkg.description);
13
+
14
+ program
15
+ .command('add <name>')
16
+ .description('Add a new snippet')
17
+ .option('--lang <lang>')
18
+ .option('--tags <tags>')
19
+ .action((name, opts) => addCmd(name, opts));
20
+
21
+ program
22
+ .command('list')
23
+ .description('List snippets')
24
+ .option('-t, --tag <tag>')
25
+ .option('--lang <lang>')
26
+ .option('--sort <sort>', 'Sort by: name | usage | recent', 'name')
27
+ .action((opts) => listCmd(opts));
28
+
29
+ program
30
+ .command('search <query>')
31
+ .description('Fuzzy search snippets')
32
+ .action((q) => searchCmd(q));
33
+
34
+ program
35
+ .command('show <idOrName>')
36
+ .description('Show snippet content')
37
+ .option('--edit', 'Open in editor')
38
+ .action((idOrName, opts) => showCmd(idOrName, opts));
39
+
40
+ program
41
+ .command('run <idOrName>')
42
+ .description('Run a snippet (preview + confirm)')
43
+ .option('--dry-run', 'Print but do not execute')
44
+ .option('--confirm', 'Skip confirmation prompt; allow running dangerous snippets (use with care)')
45
+ .action((idOrName, opts) => runCmd(idOrName, opts));
46
+
47
+ program
48
+ .command('config <action> [key] [value]')
49
+ .description('Get or set config values')
50
+ .action((action, key, value) => cfgCmd(action, key, value));
51
+
52
+ // additional commands
53
+ const editCmd = require('./commands/edit');
54
+ const rmCmd = require('./commands/rm');
55
+ const exportCmd = require('./commands/export');
56
+ const importCmd = require('./commands/import');
57
+ const syncCmd = require('./commands/sync');
58
+
59
+ program
60
+ .command('edit <idOrName>')
61
+ .description('Edit snippet in editor')
62
+ .action((idOrName) => editCmd(idOrName));
63
+
64
+ program
65
+ .command('rm <idOrName>')
66
+ .description('Remove a snippet')
67
+ .action((idOrName) => rmCmd(idOrName));
68
+
69
+ program
70
+ .command('export [path]')
71
+ .description('Export snippets to file (JSON)')
72
+ .action((path) => exportCmd(path));
73
+
74
+ program
75
+ .command('import <file>')
76
+ .description('Import snippets from file')
77
+ .action((file) => importCmd(file));
78
+
79
+ program
80
+ .command('sync <action> [id]')
81
+ .description('Sync snippets with GitHub Gists: push|pull')
82
+ .action((action, id) => syncCmd(action, id));
83
+
84
+ const uiCmd = require('./commands/ui');
85
+ program
86
+ .command('ui')
87
+ .description('Interactive TUI: j/k + Ctrl+d/u navigation, Enter show, c copy, r run, t tag filter, / search')
88
+ .action(() => uiCmd());
89
+
90
+ const seedCmd = require('../scripts/seed-examples.js');
91
+ program
92
+ .command('seed')
93
+ .description('Clear all snippet data (JSON + SQLite) and add 10 example snippets')
94
+ .action(() => seedCmd.main());
95
+
96
+ const fs = require('fs');
97
+ const cfg = config.loadConfig();
98
+
99
+ // First-run onboarding: when no commands provided and DB missing
100
+ if (process.argv.length <= 2) {
101
+ try {
102
+ if (!fs.existsSync(cfg.dbPath)) {
103
+ console.log('\nWelcome to snip — your terminal snippet manager!');
104
+ console.log('\nQuick start:\n 1) Set your editor: snip config set editor "code --wait"\n 2) Add a snippet: echo "echo \'hello\'" | snip add hello --lang sh --tags demo\n 3) List snippets: snip list\n 4) Search and run: snip search hello && snip run hello --dry-run');
105
+ console.log('\nTip: install locally with `npm link` to use the `snip` command.\n');
106
+ }
107
+ } catch (e) {}
108
+ }
109
+
110
+ program.on('--help', () => {
111
+ console.log('\nQuick start:\n echo \'echo "hello"\' | snip add test --lang sh --tags demo\n snip list\n snip search hello\n snip run test --dry-run\n');
112
+ });
113
+
114
+ program.parse(process.argv);
@@ -0,0 +1,37 @@
1
+ const { spawnSync } = require('child_process');
2
+
3
+ function getClipboardCommands(platform = process.platform) {
4
+ if (platform === 'darwin') {
5
+ return [['pbcopy', []]];
6
+ }
7
+ if (platform === 'win32') {
8
+ return [['clip', []], ['powershell', ['-NoProfile', '-Command', 'Set-Clipboard']]];
9
+ }
10
+ return [
11
+ ['wl-copy', []],
12
+ ['xclip', ['-selection', 'clipboard']],
13
+ ['xsel', ['--clipboard', '--input']]
14
+ ];
15
+ }
16
+
17
+ function copyText(text, platform = process.platform, runner = spawnSync) {
18
+ const payload = String(text == null ? '' : text);
19
+ const commands = getClipboardCommands(platform);
20
+ let lastError = null;
21
+
22
+ for (const [cmd, args] of commands) {
23
+ const res = runner(cmd, args, {
24
+ input: payload,
25
+ encoding: 'utf8',
26
+ stdio: ['pipe', 'ignore', 'ignore']
27
+ });
28
+ if (!res.error && res.status === 0) {
29
+ return { ok: true, command: cmd };
30
+ }
31
+ lastError = res.error || new Error(`${cmd} exited with status ${res.status}`);
32
+ }
33
+
34
+ return { ok: false, error: lastError };
35
+ }
36
+
37
+ module.exports = { copyText, getClipboardCommands };
@@ -0,0 +1,36 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const { spawnSync } = require('child_process');
5
+ const storage = require('../storage');
6
+ const config = require('../config');
7
+
8
+ function readStdin() {
9
+ return new Promise((resolve, reject) => {
10
+ let data = '';
11
+ process.stdin.setEncoding('utf8');
12
+ process.stdin.on('data', chunk => data += chunk);
13
+ process.stdin.on('end', () => resolve(data));
14
+ process.stdin.on('error', reject);
15
+ });
16
+ }
17
+
18
+ async function add(name, opts) {
19
+ const cfg = config.loadConfig();
20
+ let content = '';
21
+ if (!process.stdin.isTTY) {
22
+ content = await readStdin();
23
+ } else {
24
+ const tmp = path.join(os.tmpdir(), `snip-${Date.now()}.tmp`);
25
+ fs.writeFileSync(tmp, `# Snippet: ${name}\n\n`);
26
+ const editor = cfg.editor.split(' ');
27
+ spawnSync(editor[0], editor.slice(1).concat([tmp]), { stdio: 'inherit' });
28
+ content = fs.readFileSync(tmp, 'utf8');
29
+ fs.unlinkSync(tmp);
30
+ }
31
+ const tags = opts.tags ? opts.tags.split(',').map(t => t.trim()) : [];
32
+ const snippet = storage.addSnippet({ name, content, language: opts.lang, tags });
33
+ console.log(`Added snippet ${snippet.name} (${snippet.id})`);
34
+ }
35
+
36
+ module.exports = add;
@@ -0,0 +1,19 @@
1
+ const cfg = require('../config');
2
+
3
+ function run(action, key, value) {
4
+ if (action === 'get') {
5
+ const c = cfg.loadConfig();
6
+ if (!key) return console.log(JSON.stringify(c, null, 2));
7
+ return console.log((c && c[key]) || '');
8
+ }
9
+ if (action === 'set') {
10
+ if (!key) return console.error('Key required for set');
11
+ const obj = {};
12
+ obj[key] = value;
13
+ cfg.saveConfig(obj);
14
+ return console.log('OK');
15
+ }
16
+ console.error('Unknown action; use get/set');
17
+ }
18
+
19
+ module.exports = run;
@@ -0,0 +1,26 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const storage = require('../storage');
5
+ const config = require('../config');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ function edit(idOrName) {
9
+ const s = storage.getSnippetByIdOrName(idOrName);
10
+ if (!s) return console.error('Snippet not found');
11
+ const content = storage.readSnippetContent(s);
12
+ const editor = (config.loadConfig().editor || process.env.EDITOR || 'vi').split(' ');
13
+ const fileToEdit = s.path || path.join(os.tmpdir(), `snip-edit-${s.id}.tmp`);
14
+ if (!s.path) fs.writeFileSync(fileToEdit, content, 'utf8');
15
+ spawnSync(editor[0], editor.slice(1).concat([fileToEdit]), { stdio: 'inherit' });
16
+ const newContent = fs.readFileSync(fileToEdit, 'utf8');
17
+ if (!s.path) try { fs.unlinkSync(fileToEdit); } catch (e) { }
18
+ if (newContent !== content) {
19
+ storage.updateSnippetContent(s.id, newContent);
20
+ console.log(`Updated "${s.name}"`);
21
+ } else {
22
+ console.log('No changes.');
23
+ }
24
+ }
25
+
26
+ module.exports = edit;
@@ -0,0 +1,23 @@
1
+ const fs = require('fs');
2
+ const storage = require('../storage');
3
+
4
+ function exportCmd(pathArg) {
5
+ const items = storage.listSnippets().map(s => ({
6
+ id: s.id,
7
+ name: s.name,
8
+ language: s.language,
9
+ tags: s.tags,
10
+ createdAt: s.createdAt,
11
+ updatedAt: s.updatedAt,
12
+ content: storage.readSnippetContent(s)
13
+ }));
14
+ const out = JSON.stringify({ exportedAt: new Date().toISOString(), snippets: items }, null, 2);
15
+ if (!pathArg) {
16
+ console.log(out);
17
+ return;
18
+ }
19
+ fs.writeFileSync(pathArg, out, 'utf8');
20
+ console.log('Exported to', pathArg);
21
+ }
22
+
23
+ module.exports = exportCmd;
@@ -0,0 +1,18 @@
1
+ const fs = require('fs');
2
+ const storage = require('../storage');
3
+
4
+ function importCmd(file) {
5
+ try {
6
+ const raw = fs.readFileSync(file, 'utf8');
7
+ const parsed = JSON.parse(raw);
8
+ const list = parsed.snippets || parsed;
9
+ list.forEach(s => {
10
+ storage.addSnippet({ name: s.name || 'imported', content: s.content || '', language: s.language, tags: s.tags });
11
+ });
12
+ console.log('Imported', list.length, 'snippets');
13
+ } catch (e) {
14
+ console.error('Import failed', e.message);
15
+ }
16
+ }
17
+
18
+ module.exports = importCmd;
@@ -0,0 +1,81 @@
1
+ const storage = require('../storage');
2
+ let chalk = null;
3
+ try {
4
+ const chalkModule = require('chalk');
5
+ chalk = (chalkModule && chalkModule.default) ? chalkModule.default : chalkModule;
6
+ } catch (e) { }
7
+
8
+ // Helpers — gracefully degrade to plain text when chalk is unavailable
9
+ const c = {
10
+ name: (t) => chalk ? chalk.hex('#89B4FA').bold(t) : t,
11
+ tag: (t) => chalk ? chalk.hex('#94E2D5')(t) : t,
12
+ muted: (t) => chalk ? chalk.hex('#6C7086')(t) : t,
13
+ badge: (t) => chalk ? chalk.hex('#F5C2E7')(t) : t,
14
+ dim: (t) => chalk ? chalk.dim(t) : t,
15
+ };
16
+
17
+ const SORTERS = {
18
+ name: (a, b) => String(a.name || '').localeCompare(String(b.name || '')),
19
+ usage: (a, b) => {
20
+ const diff = (b.usageCount || 0) - (a.usageCount || 0);
21
+ if (diff !== 0) return diff;
22
+ return String(a.name || '').localeCompare(String(b.name || ''));
23
+ },
24
+ recent: (a, b) => {
25
+ const aTs = Date.parse(a.lastUsedAt || a.updatedAt || a.createdAt || 0) || 0;
26
+ const bTs = Date.parse(b.lastUsedAt || b.updatedAt || b.createdAt || 0) || 0;
27
+ if (bTs !== aTs) return bTs - aTs;
28
+ return String(a.name || '').localeCompare(String(b.name || ''));
29
+ }
30
+ };
31
+
32
+ function normalizeSort(sort) {
33
+ const key = String(sort || 'name').trim().toLowerCase();
34
+ return SORTERS[key] ? key : 'name';
35
+ }
36
+
37
+ function list(opts) {
38
+ const items = storage.listSnippets();
39
+ const filtered = items.filter(s => {
40
+ if (opts.tag && (!s.tags || !s.tags.includes(opts.tag))) return false;
41
+ if (opts.lang && s.language !== opts.lang) return false;
42
+ return true;
43
+ });
44
+ const sortBy = normalizeSort(opts.sort);
45
+ filtered.sort(SORTERS[sortBy]);
46
+ if (filtered.length === 0) return console.log(c.muted('No snippets found.'));
47
+
48
+ // Column widths
49
+ const nameW = 28;
50
+ const langW = 10;
51
+ const tagsW = 30;
52
+ const usageW = 6;
53
+
54
+ // Header
55
+ console.log(
56
+ c.dim(' ' +
57
+ 'NAME'.padEnd(nameW) +
58
+ 'LANG'.padEnd(langW) +
59
+ 'TAGS'.padEnd(tagsW) +
60
+ 'USED'.padStart(usageW))
61
+ );
62
+ console.log(c.dim(' ' + '─'.repeat(nameW + langW + tagsW + usageW)));
63
+
64
+ // Rows
65
+ filtered.forEach(s => {
66
+ const name = c.name(String(s.name || 'untitled').slice(0, nameW - 2).padEnd(nameW));
67
+ const lang = c.muted((s.language || '–').padEnd(langW));
68
+ const tags = (s.tags || []).length
69
+ ? c.tag((s.tags || []).join(', ').slice(0, tagsW - 2).padEnd(tagsW))
70
+ : c.muted('–'.padEnd(tagsW));
71
+ const usage = s.usageCount
72
+ ? c.badge(String(s.usageCount).padStart(usageW))
73
+ : c.muted('0'.padStart(usageW));
74
+ console.log(` ${name}${lang}${tags}${usage}`);
75
+ });
76
+
77
+ console.log(c.dim(`\n ${filtered.length} snippet${filtered.length === 1 ? '' : 's'}`));
78
+ }
79
+
80
+ module.exports = list;
81
+
@@ -0,0 +1,10 @@
1
+ const storage = require('../storage');
2
+
3
+ function remove(idOrName) {
4
+ const s = storage.getSnippetByIdOrName(idOrName);
5
+ if (!s) return console.error('Snippet not found');
6
+ storage.deleteSnippetById(s.id);
7
+ console.log('Removed', s.id);
8
+ }
9
+
10
+ module.exports = remove;
@@ -0,0 +1,34 @@
1
+ const storage = require('../storage');
2
+ const exec = require('../exec');
3
+ const config = require('../config');
4
+
5
+ function run(idOrName, opts) {
6
+ const s = storage.getSnippetByIdOrName(idOrName);
7
+ if (!s) return console.error('Snippet not found');
8
+ const content = storage.readSnippetContent(s);
9
+ const cfg = config.loadConfig();
10
+ const runner = exec.resolveRunner(s.language, cfg.defaultShell);
11
+ console.log(`--- Preview (${runner.command}) ---`);
12
+ console.log(content);
13
+ const doConfirm = cfg.confirmRun && !opts.confirm;
14
+ const safety = require('../safety');
15
+ if (safety.isDangerous(content)) {
16
+ console.error('Warning: snippet contains potentially dangerous commands. Aborting. Use --confirm to override.');
17
+ if (!opts.confirm) return process.exitCode = 2;
18
+ }
19
+ if (opts['dryRun']) return exec.runSnippetContent(content, { dryRun: true });
20
+ if (doConfirm) {
21
+ const readline = require('readline-sync');
22
+ const ans = readline.question('Run snippet? (y/N): ');
23
+ if (!['y', 'Y', 'yes'].includes(ans)) return console.log('Aborted');
24
+ }
25
+ const status = exec.runSnippetContent(content, {
26
+ dryRun: false,
27
+ shell: cfg.defaultShell,
28
+ language: s.language
29
+ });
30
+ if (status === 0) storage.touchUsage(s);
31
+ process.exitCode = status;
32
+ }
33
+
34
+ module.exports = run;
@@ -0,0 +1,13 @@
1
+ const search = require('../search');
2
+ const storage = require('../storage');
3
+
4
+ function run(query) {
5
+ const results = search.search(query, 15);
6
+ if (!results.length) return console.log('No results');
7
+ results.forEach((r, i) => {
8
+ const snip = storage.getSnippetByIdOrName(r.id);
9
+ console.log(`${i+1}. ${snip.name} (${snip.id}) [${(snip.tags||[]).join(', ')}]`);
10
+ });
11
+ }
12
+
13
+ module.exports = run;
@@ -0,0 +1,29 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const storage = require('../storage');
5
+ const { spawnSync } = require('child_process');
6
+
7
+ function show(idOrName, opts) {
8
+ const s = storage.getSnippetByIdOrName(idOrName);
9
+ if (!s) return console.error('Snippet not found');
10
+ const content = storage.readSnippetContent(s);
11
+ if (opts.edit) {
12
+ const editor = (require('../config').loadConfig().editor || 'vi').split(' ');
13
+ const fileToEdit = s.path || path.join(os.tmpdir(), `snip-show-${s.id}.tmp`);
14
+ if (!s.path) fs.writeFileSync(fileToEdit, content, 'utf8');
15
+ spawnSync(editor[0], editor.slice(1).concat([fileToEdit]), { stdio: 'inherit' });
16
+ if (!s.path) {
17
+ const newContent = fs.readFileSync(fileToEdit, 'utf8');
18
+ fs.unlinkSync(fileToEdit);
19
+ storage.updateSnippetContent(s.id, newContent);
20
+ } else {
21
+ storage.updateSnippetUpdatedAt(s.id);
22
+ }
23
+ return;
24
+ }
25
+ console.log('--- ' + s.name + ' ---');
26
+ console.log(content);
27
+ }
28
+
29
+ module.exports = show;
@@ -0,0 +1,27 @@
1
+ const gist = require('../sync/gist');
2
+ const cfg = require('../config');
3
+
4
+ async function run(action, id) {
5
+ const config = cfg.loadConfig();
6
+ const token = config.gist_token;
7
+ try {
8
+ if (action === 'push') {
9
+ if (!id) return console.error('Usage: snip sync push <snippetId|name>');
10
+ if (!token) return console.error('No gist token found. Set SNIP_GIST_TOKEN env var or: snip config set gist_token <token>');
11
+ const res = await gist.pushSnippet(id, token);
12
+ console.log('Pushed to gist:', res.html_url || res.id);
13
+ return;
14
+ }
15
+ if (action === 'pull') {
16
+ if (!id) return console.error('Usage: snip sync pull <gistId>');
17
+ const imported = await gist.pullGist(id, token);
18
+ console.log('Imported', imported.length, 'files from gist', id);
19
+ return;
20
+ }
21
+ console.error('Unknown action. Use push|pull');
22
+ } catch (e) {
23
+ console.error('Sync failed:', e.message);
24
+ }
25
+ }
26
+
27
+ module.exports = run;