cli-snip-tool 1.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 +21 -0
- package/README.md +506 -0
- package/dist/commands.js +440 -0
- package/dist/db.js +260 -0
- package/dist/edit.js +171 -0
- package/dist/editor.js +23 -0
- package/dist/index.js +64 -0
- package/dist/schema.js +88 -0
- package/dist/utils.js +5 -0
- package/package.json +50 -0
package/dist/db.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSnippet = createSnippet;
|
|
4
|
+
exports.getSnippetById = getSnippetById;
|
|
5
|
+
exports.searchSnippets = searchSnippets;
|
|
6
|
+
exports.updateSnippet = updateSnippet;
|
|
7
|
+
exports.deleteSnippet = deleteSnippet;
|
|
8
|
+
exports.incrementUseCount = incrementUseCount;
|
|
9
|
+
exports.getTopSnippets = getTopSnippets;
|
|
10
|
+
exports.getAllTags = getAllTags;
|
|
11
|
+
exports.getAllLanguages = getAllLanguages;
|
|
12
|
+
exports.exportAll = exportAll;
|
|
13
|
+
exports.importSnippets = importSnippets;
|
|
14
|
+
const schema_1 = require("./schema");
|
|
15
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
16
|
+
/** Fetch tag names for a snippet id */
|
|
17
|
+
function getTagsForSnippet(snippetId) {
|
|
18
|
+
const db = (0, schema_1.getDb)();
|
|
19
|
+
const rows = db
|
|
20
|
+
.prepare(`SELECT t.name FROM tags t
|
|
21
|
+
JOIN snippet_tags st ON st.tag_id = t.id
|
|
22
|
+
WHERE st.snippet_id = ?
|
|
23
|
+
ORDER BY t.name`)
|
|
24
|
+
.all(snippetId);
|
|
25
|
+
return rows.map((r) => r.name);
|
|
26
|
+
}
|
|
27
|
+
/** Upsert tags and link them to a snippet (replaces existing links) */
|
|
28
|
+
function setTagsForSnippet(snippetId, tags) {
|
|
29
|
+
const db = (0, schema_1.getDb)();
|
|
30
|
+
// Remove old links
|
|
31
|
+
db.prepare('DELETE FROM snippet_tags WHERE snippet_id = ?').run(snippetId);
|
|
32
|
+
for (const rawTag of tags) {
|
|
33
|
+
const name = rawTag.trim().toLowerCase();
|
|
34
|
+
if (!name)
|
|
35
|
+
continue;
|
|
36
|
+
// Insert tag if it doesn't exist, ignore if it does
|
|
37
|
+
db.prepare('INSERT OR IGNORE INTO tags (name) VALUES (?)').run(name);
|
|
38
|
+
const tag = db
|
|
39
|
+
.prepare('SELECT id FROM tags WHERE name = ?')
|
|
40
|
+
.get(name);
|
|
41
|
+
db.prepare('INSERT OR IGNORE INTO snippet_tags (snippet_id, tag_id) VALUES (?, ?)').run(snippetId, tag.id);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Attach tags array to a SnippetRow → Snippet */
|
|
45
|
+
function hydrate(row) {
|
|
46
|
+
return { ...row, tags: getTagsForSnippet(row.id) };
|
|
47
|
+
}
|
|
48
|
+
// ─── Public API ──────────────────────────────────────────────────────────────
|
|
49
|
+
/**
|
|
50
|
+
* Insert a new snippet and return it with its auto-assigned id.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const s = createSnippet({
|
|
54
|
+
* title: 'Docker exec into container',
|
|
55
|
+
* code: 'docker exec -it <container> /bin/bash',
|
|
56
|
+
* language: 'bash',
|
|
57
|
+
* tags: ['docker', 'devops'],
|
|
58
|
+
* });
|
|
59
|
+
*/
|
|
60
|
+
function createSnippet(input) {
|
|
61
|
+
const db = (0, schema_1.getDb)();
|
|
62
|
+
const result = db
|
|
63
|
+
.prepare(`INSERT INTO snippets (title, code, language, description)
|
|
64
|
+
VALUES (@title, @code, @language, @description)`)
|
|
65
|
+
.run({
|
|
66
|
+
title: input.title,
|
|
67
|
+
code: input.code,
|
|
68
|
+
language: input.language ?? 'plaintext',
|
|
69
|
+
description: input.description ?? null,
|
|
70
|
+
});
|
|
71
|
+
const id = Number(result.lastInsertRowid);
|
|
72
|
+
if (input.tags?.length) {
|
|
73
|
+
setTagsForSnippet(id, input.tags);
|
|
74
|
+
}
|
|
75
|
+
return getSnippetById(id);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Fetch a single snippet by id. Returns undefined if not found.
|
|
79
|
+
*/
|
|
80
|
+
function getSnippetById(id) {
|
|
81
|
+
const db = (0, schema_1.getDb)();
|
|
82
|
+
const row = db
|
|
83
|
+
.prepare('SELECT * FROM snippets WHERE id = ?')
|
|
84
|
+
.get(id);
|
|
85
|
+
return row ? hydrate(row) : undefined;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Search snippets by full-text query, tag, or language.
|
|
89
|
+
* If no filters are provided, returns all snippets ordered by updated_at desc.
|
|
90
|
+
*
|
|
91
|
+
* FTS query supports:
|
|
92
|
+
* - plain terms: "docker exec"
|
|
93
|
+
* - prefix: "dock*"
|
|
94
|
+
* - phrase: '"exec bash"'
|
|
95
|
+
*/
|
|
96
|
+
function searchSnippets(opts = {}) {
|
|
97
|
+
const db = (0, schema_1.getDb)();
|
|
98
|
+
const { query, tag, language, limit = 50, offset = 0 } = opts;
|
|
99
|
+
// Build WHERE clauses dynamically
|
|
100
|
+
const conditions = [];
|
|
101
|
+
const params = { limit, offset };
|
|
102
|
+
if (query) {
|
|
103
|
+
// Use FTS for text search — rank by BM25 relevance
|
|
104
|
+
conditions.push(`s.id IN (
|
|
105
|
+
SELECT rowid FROM snippets_fts
|
|
106
|
+
WHERE snippets_fts MATCH @query
|
|
107
|
+
ORDER BY rank
|
|
108
|
+
)`);
|
|
109
|
+
// FTS5 needs the query wrapped — escape any special chars
|
|
110
|
+
params.query = query.replace(/["*]/g, (c) => `"${c}"`);
|
|
111
|
+
}
|
|
112
|
+
if (tag) {
|
|
113
|
+
conditions.push(`s.id IN (
|
|
114
|
+
SELECT st.snippet_id FROM snippet_tags st
|
|
115
|
+
JOIN tags t ON t.id = st.tag_id
|
|
116
|
+
WHERE t.name = @tag COLLATE NOCASE
|
|
117
|
+
)`);
|
|
118
|
+
params.tag = tag.trim().toLowerCase();
|
|
119
|
+
}
|
|
120
|
+
if (language) {
|
|
121
|
+
conditions.push('s.language = @language COLLATE NOCASE');
|
|
122
|
+
params.language = language;
|
|
123
|
+
}
|
|
124
|
+
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
125
|
+
// Sort: FTS results by relevance, otherwise most recently updated first
|
|
126
|
+
const orderBy = query
|
|
127
|
+
? `ORDER BY (
|
|
128
|
+
SELECT rank FROM snippets_fts
|
|
129
|
+
WHERE snippets_fts MATCH @query AND rowid = s.id
|
|
130
|
+
LIMIT 1
|
|
131
|
+
)`
|
|
132
|
+
: 'ORDER BY s.updated_at DESC';
|
|
133
|
+
const rows = db
|
|
134
|
+
.prepare(`SELECT s.* FROM snippets s
|
|
135
|
+
${where}
|
|
136
|
+
${orderBy}
|
|
137
|
+
LIMIT @limit OFFSET @offset`)
|
|
138
|
+
.all(params);
|
|
139
|
+
return rows.map(hydrate);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Update a snippet's fields. Only provided fields are changed.
|
|
143
|
+
* Passing an empty tags array clears all tags.
|
|
144
|
+
*/
|
|
145
|
+
function updateSnippet(id, input) {
|
|
146
|
+
const db = (0, schema_1.getDb)();
|
|
147
|
+
const existing = getSnippetById(id);
|
|
148
|
+
if (!existing)
|
|
149
|
+
return undefined;
|
|
150
|
+
// Build a partial SET clause from only the keys the caller provided
|
|
151
|
+
const setClauses = ['updated_at = datetime(\'now\')'];
|
|
152
|
+
const params = { id };
|
|
153
|
+
if (input.title !== undefined) {
|
|
154
|
+
setClauses.push('title = @title');
|
|
155
|
+
params.title = input.title;
|
|
156
|
+
}
|
|
157
|
+
if (input.code !== undefined) {
|
|
158
|
+
setClauses.push('code = @code');
|
|
159
|
+
params.code = input.code;
|
|
160
|
+
}
|
|
161
|
+
if (input.language !== undefined) {
|
|
162
|
+
setClauses.push('language = @language');
|
|
163
|
+
params.language = input.language;
|
|
164
|
+
}
|
|
165
|
+
if (input.description !== undefined) {
|
|
166
|
+
setClauses.push('description = @description');
|
|
167
|
+
params.description = input.description;
|
|
168
|
+
}
|
|
169
|
+
db.prepare(`UPDATE snippets SET ${setClauses.join(', ')} WHERE id = @id`).run(params);
|
|
170
|
+
if (input.tags !== undefined) {
|
|
171
|
+
setTagsForSnippet(id, input.tags);
|
|
172
|
+
}
|
|
173
|
+
return getSnippetById(id);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Delete a snippet and its tag links (cascade handles snippet_tags).
|
|
177
|
+
* Returns true if a row was actually deleted.
|
|
178
|
+
*/
|
|
179
|
+
function deleteSnippet(id) {
|
|
180
|
+
const db = (0, schema_1.getDb)();
|
|
181
|
+
const result = db
|
|
182
|
+
.prepare('DELETE FROM snippets WHERE id = ?')
|
|
183
|
+
.run(id);
|
|
184
|
+
return result.changes > 0;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Increment use_count when a snippet is copied to clipboard.
|
|
188
|
+
* Call this every time the user runs `snip copy <id>`.
|
|
189
|
+
*/
|
|
190
|
+
function incrementUseCount(id) {
|
|
191
|
+
(0, schema_1.getDb)()
|
|
192
|
+
.prepare('UPDATE snippets SET use_count = use_count + 1 WHERE id = ?')
|
|
193
|
+
.run(id);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Return the top N most-copied snippets.
|
|
197
|
+
* Powers the `snip top` command.
|
|
198
|
+
*/
|
|
199
|
+
function getTopSnippets(limit = 10) {
|
|
200
|
+
const db = (0, schema_1.getDb)();
|
|
201
|
+
const rows = db
|
|
202
|
+
.prepare(`SELECT * FROM snippets
|
|
203
|
+
WHERE use_count > 0
|
|
204
|
+
ORDER BY use_count DESC
|
|
205
|
+
LIMIT ?`)
|
|
206
|
+
.all(limit);
|
|
207
|
+
return rows.map(hydrate);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Return all distinct tag names in alphabetical order.
|
|
211
|
+
* Useful for autocomplete in `snip list --tag`.
|
|
212
|
+
*/
|
|
213
|
+
function getAllTags() {
|
|
214
|
+
const rows = (0, schema_1.getDb)()
|
|
215
|
+
.prepare('SELECT name FROM tags ORDER BY name')
|
|
216
|
+
.all();
|
|
217
|
+
return rows.map((r) => r.name);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Return all distinct language names — useful for `--language` autocomplete.
|
|
221
|
+
*/
|
|
222
|
+
function getAllLanguages() {
|
|
223
|
+
const rows = (0, schema_1.getDb)()
|
|
224
|
+
.prepare(`SELECT DISTINCT language FROM snippets
|
|
225
|
+
ORDER BY language`)
|
|
226
|
+
.all();
|
|
227
|
+
return rows.map((r) => r.language);
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Export every snippet (with tags) for backup/import.
|
|
231
|
+
*/
|
|
232
|
+
function exportAll() {
|
|
233
|
+
const db = (0, schema_1.getDb)();
|
|
234
|
+
const rows = db
|
|
235
|
+
.prepare('SELECT * FROM snippets ORDER BY id')
|
|
236
|
+
.all();
|
|
237
|
+
return rows.map(hydrate);
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Bulk-insert snippets from an import file.
|
|
241
|
+
* Skips duplicates (same title + language).
|
|
242
|
+
* Returns count of actually inserted snippets.
|
|
243
|
+
*/
|
|
244
|
+
function importSnippets(snippets) {
|
|
245
|
+
let count = 0;
|
|
246
|
+
const db = (0, schema_1.getDb)();
|
|
247
|
+
const insertMany = db.transaction((items) => {
|
|
248
|
+
for (const item of items) {
|
|
249
|
+
const exists = db
|
|
250
|
+
.prepare('SELECT id FROM snippets WHERE title = ? AND language = ?')
|
|
251
|
+
.get(item.title, item.language ?? 'plaintext');
|
|
252
|
+
if (!exists) {
|
|
253
|
+
createSnippet(item);
|
|
254
|
+
count++;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
insertMany(snippets);
|
|
259
|
+
return count;
|
|
260
|
+
}
|
package/dist/edit.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerEdit = registerEdit;
|
|
7
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
const db_1 = require("./db");
|
|
10
|
+
const utils_1 = require("./utils");
|
|
11
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
12
|
+
/** Show a compact one-line summary of a snippet */
|
|
13
|
+
function printSummary(s) {
|
|
14
|
+
const tags = s.tags.length ? ' ' + s.tags.map(t => chalk_1.default.green(`#${t}`)).join(' ') : '';
|
|
15
|
+
console.log(chalk_1.default.bold.cyan(`#${s.id}`) + ' ' +
|
|
16
|
+
chalk_1.default.bold(s.title) + ' ' +
|
|
17
|
+
chalk_1.default.yellow(`[${s.language}]`) +
|
|
18
|
+
tags);
|
|
19
|
+
}
|
|
20
|
+
// ─── snip edit ───────────────────────────────────────────────────────────────
|
|
21
|
+
function registerEdit(program) {
|
|
22
|
+
program
|
|
23
|
+
.command('edit <id>')
|
|
24
|
+
.description('Edit an existing snippet in your $EDITOR')
|
|
25
|
+
.option('--title', 'Edit title only')
|
|
26
|
+
.option('--tags', 'Edit tags only')
|
|
27
|
+
.option('--description', 'Edit description only')
|
|
28
|
+
.action(async (rawId, opts) => {
|
|
29
|
+
// ── Resolve snippet ───────────────────────────────────────────────────
|
|
30
|
+
const id = parseInt(rawId, 10);
|
|
31
|
+
if (isNaN(id)) {
|
|
32
|
+
console.error(chalk_1.default.red(`Invalid id: "${rawId}". Must be a number.`));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const snippet = (0, db_1.getSnippetById)(id);
|
|
36
|
+
if (!snippet) {
|
|
37
|
+
console.error(chalk_1.default.red(`No snippet found with id ${id}.`));
|
|
38
|
+
console.error(chalk_1.default.dim('Run snip list to see all snippet ids.'));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(chalk_1.default.bold('Editing:'));
|
|
43
|
+
printSummary(snippet);
|
|
44
|
+
console.log();
|
|
45
|
+
const changes = {};
|
|
46
|
+
// ── Decide what to edit ───────────────────────────────────────────────
|
|
47
|
+
// If no specific flag → ask what the user wants to change
|
|
48
|
+
const editingAll = !opts.title && !opts.tags && !opts.description;
|
|
49
|
+
let editCode = editingAll;
|
|
50
|
+
let editTitle = opts.title || editingAll;
|
|
51
|
+
let editTags = opts.tags || editingAll;
|
|
52
|
+
let editDescription = opts.description || editingAll;
|
|
53
|
+
if (editingAll) {
|
|
54
|
+
// Let the user choose which fields to actually touch
|
|
55
|
+
const { fields } = await inquirer_1.default.prompt([{
|
|
56
|
+
type: 'checkbox',
|
|
57
|
+
name: 'fields',
|
|
58
|
+
message: 'What do you want to edit? (Space to toggle)',
|
|
59
|
+
choices: [
|
|
60
|
+
{ name: 'Code (opens editor)', value: 'code', checked: true },
|
|
61
|
+
{ name: 'Title', value: 'title', checked: false },
|
|
62
|
+
{ name: 'Tags', value: 'tags', checked: false },
|
|
63
|
+
{ name: 'Description', value: 'description', checked: false },
|
|
64
|
+
],
|
|
65
|
+
}]);
|
|
66
|
+
editCode = fields.includes('code');
|
|
67
|
+
editTitle = fields.includes('title');
|
|
68
|
+
editTags = fields.includes('tags');
|
|
69
|
+
editDescription = fields.includes('description');
|
|
70
|
+
}
|
|
71
|
+
// ── Edit title ────────────────────────────────────────────────────────
|
|
72
|
+
if (editTitle) {
|
|
73
|
+
const { title } = await inquirer_1.default.prompt([{
|
|
74
|
+
type: 'input',
|
|
75
|
+
name: 'title',
|
|
76
|
+
message: 'Title:',
|
|
77
|
+
default: snippet.title,
|
|
78
|
+
validate: (v) => v.trim().length > 0 || 'Title cannot be empty',
|
|
79
|
+
}]);
|
|
80
|
+
if (title.trim() !== snippet.title) {
|
|
81
|
+
changes.title = title.trim();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// ── Edit description ──────────────────────────────────────────────────
|
|
85
|
+
if (editDescription) {
|
|
86
|
+
const { description } = await inquirer_1.default.prompt([{
|
|
87
|
+
type: 'input',
|
|
88
|
+
name: 'description',
|
|
89
|
+
message: 'Description:',
|
|
90
|
+
default: snippet.description ?? '',
|
|
91
|
+
}]);
|
|
92
|
+
const cleaned = description.trim() || null;
|
|
93
|
+
if (cleaned !== snippet.description) {
|
|
94
|
+
changes.description = cleaned ?? undefined;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// ── Edit tags ─────────────────────────────────────────────────────────
|
|
98
|
+
if (editTags) {
|
|
99
|
+
const allTags = (0, db_1.getAllTags)();
|
|
100
|
+
// Pre-check the snippet's current tags
|
|
101
|
+
const { selectedTags } = await inquirer_1.default.prompt([{
|
|
102
|
+
type: 'checkbox',
|
|
103
|
+
name: 'selectedTags',
|
|
104
|
+
message: 'Tags:',
|
|
105
|
+
choices: allTags.map(t => ({
|
|
106
|
+
name: t,
|
|
107
|
+
value: t,
|
|
108
|
+
checked: snippet.tags.includes(t),
|
|
109
|
+
})),
|
|
110
|
+
pageSize: 10,
|
|
111
|
+
}]);
|
|
112
|
+
const { newTags } = await inquirer_1.default.prompt([{
|
|
113
|
+
type: 'input',
|
|
114
|
+
name: 'newTags',
|
|
115
|
+
message: 'Add new tags (comma-separated, or Enter to skip):',
|
|
116
|
+
}]);
|
|
117
|
+
const merged = [
|
|
118
|
+
...selectedTags,
|
|
119
|
+
...newTags.split(',').map((t) => t.trim()).filter(Boolean),
|
|
120
|
+
];
|
|
121
|
+
// Only mark as changed if different from current
|
|
122
|
+
const currentSorted = [...snippet.tags].sort().join(',');
|
|
123
|
+
const proposedSorted = [...new Set(merged)].sort().join(',');
|
|
124
|
+
if (currentSorted !== proposedSorted) {
|
|
125
|
+
changes.tags = [...new Set(merged)];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// ── Edit code (opens $EDITOR) ─────────────────────────────────────────
|
|
129
|
+
if (editCode) {
|
|
130
|
+
console.log(chalk_1.default.dim('Opening editor — save and close when done…'));
|
|
131
|
+
const updatedCode = (0, utils_1.openInEditor)(snippet.code);
|
|
132
|
+
if (updatedCode.trim() === '') {
|
|
133
|
+
console.log(chalk_1.default.red('Editor returned empty content. Code not changed.'));
|
|
134
|
+
}
|
|
135
|
+
else if (updatedCode !== snippet.code) {
|
|
136
|
+
changes.code = updatedCode;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
console.log(chalk_1.default.dim('Code unchanged.'));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// ── Nothing to save ───────────────────────────────────────────────────
|
|
143
|
+
if (Object.keys(changes).length === 0) {
|
|
144
|
+
console.log(chalk_1.default.yellow('\nNo changes made.\n'));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
// ── Confirm & save ────────────────────────────────────────────────────
|
|
148
|
+
const summary = Object.keys(changes)
|
|
149
|
+
.map(k => chalk_1.default.cyan(k))
|
|
150
|
+
.join(', ');
|
|
151
|
+
const { confirm } = await inquirer_1.default.prompt([{
|
|
152
|
+
type: 'confirm',
|
|
153
|
+
name: 'confirm',
|
|
154
|
+
message: `Save changes to ${summary}?`,
|
|
155
|
+
default: true,
|
|
156
|
+
}]);
|
|
157
|
+
if (!confirm) {
|
|
158
|
+
console.log(chalk_1.default.yellow('Cancelled. No changes saved.\n'));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const updated = (0, db_1.updateSnippet)(id, changes);
|
|
162
|
+
if (!updated) {
|
|
163
|
+
console.error(chalk_1.default.red('Something went wrong saving the snippet.'));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
console.log();
|
|
167
|
+
console.log(chalk_1.default.green('✔ Snippet updated!'));
|
|
168
|
+
printSummary(updated);
|
|
169
|
+
console.log();
|
|
170
|
+
});
|
|
171
|
+
}
|
package/dist/editor.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openInEditor = openInEditor;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const fs_1 = require("fs");
|
|
6
|
+
const os_1 = require("os");
|
|
7
|
+
const path_1 = require("path");
|
|
8
|
+
/** Open $EDITOR with a temp file; return whatever the user saved. */
|
|
9
|
+
function openInEditor(placeholder = '') {
|
|
10
|
+
let editor = process.env.EDITOR || process.env.VISUAL;
|
|
11
|
+
if (!editor) {
|
|
12
|
+
editor = process.platform === 'win32' ? 'notepad' : 'nano';
|
|
13
|
+
}
|
|
14
|
+
const tmpFile = (0, path_1.join)((0, os_1.tmpdir)(), `snip-${Date.now()}.tmp`);
|
|
15
|
+
(0, fs_1.writeFileSync)(tmpFile, placeholder, 'utf8');
|
|
16
|
+
const result = (0, child_process_1.spawnSync)(editor, [tmpFile], { stdio: 'inherit' });
|
|
17
|
+
if (result.error) {
|
|
18
|
+
throw new Error(`Could not open editor "${editor}": ${result.error.message}`);
|
|
19
|
+
}
|
|
20
|
+
const content = (0, fs_1.readFileSync)(tmpFile, 'utf8').trim();
|
|
21
|
+
(0, fs_1.unlinkSync)(tmpFile);
|
|
22
|
+
return content;
|
|
23
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
|
+
};
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const commands_1 = require("./commands");
|
|
9
|
+
const edit_1 = require("./edit");
|
|
10
|
+
const update_notifier_1 = __importDefault(require("update-notifier"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
// Retrieve package.json at runtime to prevent typescript compiler boundaries issues
|
|
13
|
+
const pkg = require(path_1.default.join(__dirname, '../package.json'));
|
|
14
|
+
// Initialize background update checks
|
|
15
|
+
(0, update_notifier_1.default)({ pkg }).notify();
|
|
16
|
+
const program = new commander_1.Command();
|
|
17
|
+
program
|
|
18
|
+
.name('snip')
|
|
19
|
+
.description('A fast CLI snippet manager')
|
|
20
|
+
.version(pkg.version || '1.0.0');
|
|
21
|
+
(0, commands_1.registerAdd)(program);
|
|
22
|
+
(0, commands_1.registerFind)(program);
|
|
23
|
+
(0, edit_1.registerEdit)(program);
|
|
24
|
+
(0, commands_1.registerList)(program);
|
|
25
|
+
(0, commands_1.registerCopy)(program);
|
|
26
|
+
(0, commands_1.registerDelete)(program);
|
|
27
|
+
(0, commands_1.registerExport)(program);
|
|
28
|
+
// Add custom help text with examples
|
|
29
|
+
program.addHelpText('after', `
|
|
30
|
+
Examples:
|
|
31
|
+
# Add a snippet interactively
|
|
32
|
+
$ snip add
|
|
33
|
+
|
|
34
|
+
# Add a snippet with tags and language, piping code from stdin
|
|
35
|
+
$ echo 'cat file.txt' | snip add --stdin -t "Cat command" -l bash --tags shell,utils
|
|
36
|
+
|
|
37
|
+
# Search snippets interactively
|
|
38
|
+
$ snip find
|
|
39
|
+
|
|
40
|
+
# Search and copy the top result directly to clipboard
|
|
41
|
+
$ snip find "docker exec" --copy
|
|
42
|
+
|
|
43
|
+
# List all snippets in a table
|
|
44
|
+
$ snip list
|
|
45
|
+
|
|
46
|
+
# Copy a specific snippet to clipboard
|
|
47
|
+
$ snip copy 3
|
|
48
|
+
|
|
49
|
+
# Edit a snippet's fields or code
|
|
50
|
+
$ snip edit 3
|
|
51
|
+
|
|
52
|
+
# Delete a snippet by ID
|
|
53
|
+
$ snip delete 3
|
|
54
|
+
|
|
55
|
+
# Export all snippets as Markdown
|
|
56
|
+
$ snip export --format markdown
|
|
57
|
+
`);
|
|
58
|
+
// Helpful error on unknown commands
|
|
59
|
+
program.on('command:*', () => {
|
|
60
|
+
console.error(`Unknown command: ${program.args.join(' ')}`);
|
|
61
|
+
console.error('Run snip --help to see available commands.');
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
64
|
+
program.parse(process.argv);
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getDb = getDb;
|
|
7
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
// Resolve DB path: ~/.config/snip/snip.db
|
|
12
|
+
const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.config', 'snip');
|
|
13
|
+
const DB_PATH = path_1.default.join(CONFIG_DIR, 'snip.db');
|
|
14
|
+
let _db = null;
|
|
15
|
+
function getDb() {
|
|
16
|
+
if (_db)
|
|
17
|
+
return _db;
|
|
18
|
+
// Create config dir if it doesn't exist
|
|
19
|
+
fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
20
|
+
_db = new better_sqlite3_1.default(DB_PATH);
|
|
21
|
+
// WAL mode: faster writes, safer concurrent reads
|
|
22
|
+
_db.pragma('journal_mode = WAL');
|
|
23
|
+
_db.pragma('foreign_keys = ON');
|
|
24
|
+
initSchema(_db);
|
|
25
|
+
return _db;
|
|
26
|
+
}
|
|
27
|
+
function initSchema(db) {
|
|
28
|
+
db.exec(`
|
|
29
|
+
-- Main snippets table
|
|
30
|
+
CREATE TABLE IF NOT EXISTS snippets (
|
|
31
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
32
|
+
title TEXT NOT NULL,
|
|
33
|
+
code TEXT NOT NULL,
|
|
34
|
+
language TEXT NOT NULL DEFAULT 'plaintext',
|
|
35
|
+
description TEXT,
|
|
36
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
37
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
38
|
+
use_count INTEGER NOT NULL DEFAULT 0
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
-- Tags table (many-to-many with snippets)
|
|
42
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
43
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
44
|
+
name TEXT NOT NULL UNIQUE COLLATE NOCASE
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE TABLE IF NOT EXISTS snippet_tags (
|
|
48
|
+
snippet_id INTEGER NOT NULL REFERENCES snippets(id) ON DELETE CASCADE,
|
|
49
|
+
tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
50
|
+
PRIMARY KEY (snippet_id, tag_id)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
-- FTS5 virtual table for full-text search across title, code, description
|
|
54
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS snippets_fts USING fts5(
|
|
55
|
+
title,
|
|
56
|
+
code,
|
|
57
|
+
description,
|
|
58
|
+
content = 'snippets',
|
|
59
|
+
content_rowid = 'id',
|
|
60
|
+
tokenize = 'porter unicode61'
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
-- Keep FTS index in sync with the snippets table
|
|
64
|
+
CREATE TRIGGER IF NOT EXISTS snippets_fts_insert
|
|
65
|
+
AFTER INSERT ON snippets BEGIN
|
|
66
|
+
INSERT INTO snippets_fts(rowid, title, code, description)
|
|
67
|
+
VALUES (new.id, new.title, new.code, new.description);
|
|
68
|
+
END;
|
|
69
|
+
|
|
70
|
+
CREATE TRIGGER IF NOT EXISTS snippets_fts_update
|
|
71
|
+
AFTER UPDATE ON snippets BEGIN
|
|
72
|
+
UPDATE snippets_fts
|
|
73
|
+
SET title = new.title, code = new.code, description = new.description
|
|
74
|
+
WHERE rowid = new.id;
|
|
75
|
+
END;
|
|
76
|
+
|
|
77
|
+
CREATE TRIGGER IF NOT EXISTS snippets_fts_delete
|
|
78
|
+
AFTER DELETE ON snippets BEGIN
|
|
79
|
+
DELETE FROM snippets_fts WHERE rowid = old.id;
|
|
80
|
+
END;
|
|
81
|
+
|
|
82
|
+
-- Index for fast tag lookups
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_snippet_tags_tag ON snippet_tags(tag_id);
|
|
84
|
+
|
|
85
|
+
-- Index for sorting by use_count (snip top command)
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_snippets_use_count ON snippets(use_count DESC);
|
|
87
|
+
`);
|
|
88
|
+
}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openInEditor = void 0;
|
|
4
|
+
var editor_1 = require("./editor");
|
|
5
|
+
Object.defineProperty(exports, "openInEditor", { enumerable: true, get: function () { return editor_1.openInEditor; } });
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "cli-snip-tool",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A fast CLI snippet manager to save, search, and copy code snippets directly from the terminal.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"snip": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"cli",
|
|
20
|
+
"snippet",
|
|
21
|
+
"manager",
|
|
22
|
+
"code-snippet",
|
|
23
|
+
"sqlite",
|
|
24
|
+
"fts5",
|
|
25
|
+
"terminal",
|
|
26
|
+
"developer-tools"
|
|
27
|
+
],
|
|
28
|
+
"author": "satyamsinghs408",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"type": "commonjs",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
33
|
+
"@types/inquirer": "^8.2.5",
|
|
34
|
+
"@types/node": "^26.0.0",
|
|
35
|
+
"@types/update-notifier": "^5.1.0",
|
|
36
|
+
"tsx": "^4.22.4",
|
|
37
|
+
"typescript": "^6.0.3"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"better-sqlite3": "^12.11.1",
|
|
41
|
+
"chalk": "^4.1.2",
|
|
42
|
+
"cli-highlight": "^2.1.11",
|
|
43
|
+
"cli-table3": "^0.6.5",
|
|
44
|
+
"clipboardy": "^2.3.0",
|
|
45
|
+
"commander": "^15.0.0",
|
|
46
|
+
"inquirer": "^8.2.5",
|
|
47
|
+
"ora": "^5.4.1",
|
|
48
|
+
"update-notifier": "^5.1.0"
|
|
49
|
+
}
|
|
50
|
+
}
|