saveinme 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/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # 💾 saveinme
2
+
3
+ > Your personal terminal notebook — save anything, permanently.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ cd path/to/saveinme
9
+ npm install
10
+ npm link # makes `saveinme` available globally in any terminal
11
+ ```
12
+
13
+ ## Commands
14
+
15
+ | Command | What it does |
16
+ |---|---|
17
+ | `saveinme` | List all your saved notes |
18
+ | `saveinme -s` | Create a new note or edit an existing one |
19
+ | `saveinme -s <title>` | Jump straight to editing a note by title |
20
+ | `saveinme -v <title>` | View a note in full |
21
+ | `saveinme -d <title>` | Delete a note |
22
+ | `saveinme -p <title>` | Toggle pin (pinned notes appear at the top) |
23
+ | `saveinme -f <term>` | Search by title, content, tags, or category |
24
+ | `saveinme --path` | Show where your notes are stored |
25
+ | `saveinme -h` | Help |
26
+
27
+ ## What gets saved per note
28
+
29
+ - **Title** — a short name for the note
30
+ - **Content** — multi-line text, written right in the terminal
31
+ - **Category** — group notes together (e.g. `work`, `personal`)
32
+ - **Tags** — multiple tags for cross-cutting topics (e.g. `#todo`, `#idea`)
33
+ - **Priority** — Low / Medium / High
34
+ - **Pinned** — pinned notes always appear at the top of the list
35
+ - **Created date** — set automatically
36
+ - **Updated date** — updated automatically on every save
37
+ - **Word count** and **char count** — calculated automatically
38
+
39
+ ## In-terminal editor
40
+
41
+ When you run `saveinme -s`, you'll be guided through prompts. When you reach the content area:
42
+
43
+ ```
44
+ ❯ This is line one
45
+ ❯ This is line two
46
+ ❯ :save
47
+ ```
48
+
49
+ Type `:save` on a new blank line and press Enter to save.
50
+ Type `:cancel` to discard.
51
+ Type `:keep` (when editing) to keep the existing content unchanged.
52
+
53
+ Works on **Windows** (PowerShell / CMD) and **Linux/macOS** — no external editor opens.
54
+
55
+ ## Storage
56
+
57
+ Notes are stored permanently at:
58
+
59
+ ```
60
+ ~/.saveinme/notes.json
61
+ ```
62
+
63
+ This file persists across sessions and reboots. Back it up any time.
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getAllNotes,
4
+ getNoteByTitle,
5
+ deleteNoteById,
6
+ togglePin,
7
+ searchNotes,
8
+ getDataPath,
9
+ } from '../src/store.js';
10
+
11
+ import {
12
+ showNotesList,
13
+ showNote,
14
+ showSuccess,
15
+ showError,
16
+ showInfo,
17
+ showHelp,
18
+ } from '../src/display.js';
19
+
20
+ import { openEditor } from '../src/editor.js';
21
+
22
+ // ─── Parse args ───────────────────────────────────────────────────────────────
23
+
24
+ const argv = process.argv.slice(2);
25
+ const flag = argv[0];
26
+ const rest = argv.slice(1).join(' ').trim();
27
+
28
+ // ─── Main ─────────────────────────────────────────────────────────────────────
29
+
30
+ async function main() {
31
+ switch (flag) {
32
+ // ── List all ───────────────────────────────────────────────────────────
33
+ case undefined:
34
+ case '--list':
35
+ case '-l': {
36
+ showNotesList(getAllNotes());
37
+ break;
38
+ }
39
+
40
+ // ── Save / edit ────────────────────────────────────────────────────────
41
+ case '-s':
42
+ case '--save': {
43
+ await openEditor(rest || undefined);
44
+ break;
45
+ }
46
+
47
+ // ── View ───────────────────────────────────────────────────────────────
48
+ case '-v':
49
+ case '--view': {
50
+ if (!rest) { showError('Provide a title: saveinme -v <title>'); break; }
51
+ const note = getNoteByTitle(rest);
52
+ if (!note) showError(`No note found with title "${rest}".`);
53
+ else showNote(note);
54
+ break;
55
+ }
56
+
57
+ // ── Delete ─────────────────────────────────────────────────────────────
58
+ case '-d':
59
+ case '--delete': {
60
+ if (!rest) { showError('Provide a title: saveinme -d <title>'); break; }
61
+ const note = getNoteByTitle(rest);
62
+ if (!note) {
63
+ showError(`No note found with title "${rest}".`);
64
+ } else {
65
+ const ok = deleteNoteById(note.id);
66
+ if (ok) showSuccess(`Note "${rest}" deleted.`);
67
+ else showError('Delete failed.');
68
+ }
69
+ break;
70
+ }
71
+
72
+ // ── Pin / unpin ────────────────────────────────────────────────────────
73
+ case '-p':
74
+ case '--pin': {
75
+ if (!rest) { showError('Provide a title: saveinme -p <title>'); break; }
76
+ const note = getNoteByTitle(rest);
77
+ if (!note) {
78
+ showError(`No note found with title "${rest}".`);
79
+ } else {
80
+ const nowPinned = togglePin(note.id);
81
+ showSuccess(`Note "${rest}" ${nowPinned ? '📌 pinned' : 'unpinned'}.`);
82
+ }
83
+ break;
84
+ }
85
+
86
+ // ── Search ─────────────────────────────────────────────────────────────
87
+ case '-f':
88
+ case '--find':
89
+ case '--search': {
90
+ if (!rest) { showError('Provide a search term: saveinme -f <term>'); break; }
91
+ const results = searchNotes(rest);
92
+ if (results.length === 0) showError(`No notes matched "${rest}".`);
93
+ else showNotesList(results, `Search results for "${rest}"`);
94
+ break;
95
+ }
96
+
97
+ // ── Storage path ───────────────────────────────────────────────────────
98
+ case '--path': {
99
+ showInfo(`Notes stored at:\n ${getDataPath()}`);
100
+ break;
101
+ }
102
+
103
+ // ── Help ───────────────────────────────────────────────────────────────
104
+ case '-h':
105
+ case '-help':
106
+ case '--help': {
107
+ showHelp();
108
+ break;
109
+ }
110
+
111
+ // ── Unknown ────────────────────────────────────────────────────────────
112
+ default: {
113
+ showError(`Unknown flag "${flag}". Run saveinme -h for help.`);
114
+ process.exitCode = 1;
115
+ }
116
+ }
117
+ }
118
+
119
+ main().catch(err => {
120
+ // Suppress Ctrl+C / forced-exit noise from inquirer
121
+ if (err?.name === 'ExitPromptError' || err?.message === 'cancelled') {
122
+ process.stdout.write('\n');
123
+ process.exit(0);
124
+ }
125
+ console.error(err);
126
+ process.exit(1);
127
+ });
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "saveinme",
3
+ "version": "1.0.0",
4
+ "description": "Your personal terminal notebook — save anything, permanently.",
5
+ "type": "module",
6
+ "main": "./bin/saveinme.js",
7
+ "bin": {
8
+ "saveinme": "bin/saveinme.js"
9
+ },
10
+ "files": [
11
+ "bin/",
12
+ "src/",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "start": "node bin/saveinme.js"
17
+ },
18
+ "dependencies": {
19
+ "@inquirer/prompts": "^5.0.0",
20
+ "boxen": "^7.1.1",
21
+ "chalk": "^5.3.0",
22
+ "cli-table3": "^0.6.3"
23
+ },
24
+ "engines": {
25
+ "node": ">=18.0.0"
26
+ },
27
+ "keywords": [
28
+ "cli",
29
+ "notes",
30
+ "terminal",
31
+ "notebook",
32
+ "saveinme"
33
+ ],
34
+ "license": "MIT"
35
+ }
package/src/display.js ADDED
@@ -0,0 +1,262 @@
1
+ import chalk from 'chalk';
2
+ import boxen from 'boxen';
3
+ import Table from 'cli-table3';
4
+
5
+ // ─── Color palettes ────────────────────────────────────────────────────────────
6
+
7
+ const PRIORITY_BADGE = {
8
+ high: chalk.bgRed.white.bold(' HIGH '),
9
+ medium: chalk.bgYellow.black.bold(' MED '),
10
+ low: chalk.bgGreen.black.bold(' LOW '),
11
+ };
12
+
13
+ const PRIORITY_COLOR = {
14
+ high: chalk.red,
15
+ medium: chalk.yellow,
16
+ low: chalk.green,
17
+ };
18
+
19
+ const CAT_COLORS = [
20
+ chalk.cyan, chalk.magenta, chalk.blue,
21
+ chalk.green, chalk.yellow, chalk.redBright,
22
+ ];
23
+ const catColorCache = {};
24
+ let catColorIdx = 0;
25
+
26
+ function catColor(cat) {
27
+ if (!cat) return s => chalk.dim(s);
28
+ if (!catColorCache[cat]) {
29
+ catColorCache[cat] = CAT_COLORS[catColorIdx++ % CAT_COLORS.length];
30
+ }
31
+ return catColorCache[cat];
32
+ }
33
+
34
+ // ─── Date formatting ───────────────────────────────────────────────────────────
35
+
36
+ export function formatDate(iso) {
37
+ if (!iso) return '—';
38
+ const d = new Date(iso);
39
+ const diff = Date.now() - d.getTime();
40
+ if (diff < 60_000) return chalk.dim('just now');
41
+ if (diff < 3_600_000) return chalk.dim(`${Math.floor(diff / 60_000)}m ago`);
42
+ if (diff < 86_400_000) return chalk.dim(`${Math.floor(diff / 3_600_000)}h ago`);
43
+ if (diff < 604_800_000) return chalk.dim(`${Math.floor(diff / 86_400_000)}d ago`);
44
+ return chalk.dim(d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }));
45
+ }
46
+
47
+ // ─── Header ───────────────────────────────────────────────────────────────────
48
+
49
+ export function showHeader() {
50
+ const logo =
51
+ chalk.bold.cyanBright('💾 saveinme') +
52
+ chalk.dim(' v1.0.0') +
53
+ '\n' +
54
+ chalk.dim('your personal terminal notebook');
55
+
56
+ console.log(
57
+ boxen(logo, {
58
+ padding: { top: 0, bottom: 0, left: 2, right: 2 },
59
+ margin: { top: 1, bottom: 0 },
60
+ borderStyle: 'round',
61
+ borderColor: 'cyan',
62
+ })
63
+ );
64
+ }
65
+
66
+ // ─── Notes list ───────────────────────────────────────────────────────────────
67
+
68
+ export function showNotesList(notes, heading = null) {
69
+ showHeader();
70
+
71
+ if (heading) {
72
+ console.log('\n' + chalk.bold.yellow(` ${heading}`));
73
+ }
74
+
75
+ if (notes.length === 0) {
76
+ console.log(
77
+ boxen(
78
+ chalk.dim('No notes yet.\n') +
79
+ chalk.cyanBright('Run ') +
80
+ chalk.bold.white('saveinme -s') +
81
+ chalk.cyanBright(' to create your first note.'),
82
+ {
83
+ padding: 1,
84
+ margin: { top: 1, bottom: 1 },
85
+ borderStyle: 'round',
86
+ borderColor: 'gray',
87
+ }
88
+ )
89
+ );
90
+ return;
91
+ }
92
+
93
+ // Sort: pinned first, then by updatedAt desc
94
+ const sorted = [...notes].sort((a, b) => {
95
+ if (a.pinned && !b.pinned) return -1;
96
+ if (!a.pinned && b.pinned) return 1;
97
+ return new Date(b.updatedAt) - new Date(a.updatedAt);
98
+ });
99
+
100
+ console.log('');
101
+
102
+ const table = new Table({
103
+ head: [
104
+ chalk.bold.cyanBright('#'),
105
+ chalk.bold.cyanBright('Title'),
106
+ chalk.bold.cyanBright('Category'),
107
+ chalk.bold.cyanBright('Tags'),
108
+ chalk.bold.cyanBright('Priority'),
109
+ chalk.bold.cyanBright('Words'),
110
+ chalk.bold.cyanBright('Updated'),
111
+ ],
112
+ style: { head: [], border: ['cyan'] },
113
+ colWidths: [4, 26, 14, 26, 9, 7, 16],
114
+ wordWrap: true,
115
+ });
116
+
117
+ sorted.forEach((note, i) => {
118
+ const pin = note.pinned ? chalk.yellow('📌') : ' ';
119
+ const title = `${pin} ${chalk.bold(truncate(note.title, 20))}`;
120
+ const cat = note.category
121
+ ? catColor(note.category)(truncate(note.category, 11))
122
+ : chalk.dim('—');
123
+ const tags = (note.tags ?? []).length
124
+ ? note.tags.map(t => chalk.magentaBright(`#${t}`)).join(' ').slice(0, 24)
125
+ : chalk.dim('—');
126
+ const pri = PRIORITY_BADGE[note.priority] ?? chalk.dim('—');
127
+ const words = chalk.dim(String(note.wordCount ?? 0));
128
+ const upd = formatDate(note.updatedAt);
129
+
130
+ table.push([String(i + 1), title, cat, tags, pri, words, upd]);
131
+ });
132
+
133
+ console.log(table.toString());
134
+
135
+ console.log(
136
+ chalk.dim(
137
+ `\n ${notes.length} note${notes.length !== 1 ? 's' : ''} saved` +
138
+ ` · saveinme -s add/edit` +
139
+ ` · saveinme -v <title> view` +
140
+ ` · saveinme -f <term> search` +
141
+ ` · saveinme -h help\n`
142
+ )
143
+ );
144
+ }
145
+
146
+ // ─── Full note view ────────────────────────────────────────────────────────────
147
+
148
+ export function showNote(note) {
149
+ const cc = catColor(note.category);
150
+
151
+ const metaParts = [
152
+ note.category ? `📁 ${cc(note.category)}` : null,
153
+ (note.tags ?? []).length
154
+ ? note.tags.map(t => chalk.magentaBright(`#${t}`)).join(' ')
155
+ : null,
156
+ note.priority
157
+ ? `${PRIORITY_COLOR[note.priority] ?? chalk.white}(●) ${note.priority}`
158
+ : null,
159
+ note.pinned ? chalk.yellow('📌 pinned') : null,
160
+ ].filter(Boolean).join(' ');
161
+
162
+ const statsParts = [
163
+ chalk.dim(`${note.wordCount ?? 0} words`),
164
+ chalk.dim(`${note.charCount ?? 0} chars`),
165
+ chalk.dim(`created ${formatDate(note.createdAt)}`),
166
+ chalk.dim(`updated ${formatDate(note.updatedAt)}`),
167
+ ].join(' · ');
168
+
169
+ const header =
170
+ chalk.bold.white(note.title) +
171
+ (metaParts ? '\n' + metaParts : '') +
172
+ '\n' + statsParts;
173
+
174
+ console.log(
175
+ boxen(header, {
176
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
177
+ margin: { top: 1, bottom: 0 },
178
+ borderStyle: 'round',
179
+ borderColor: 'cyan',
180
+ })
181
+ );
182
+
183
+ console.log(
184
+ boxen(chalk.white(note.content), {
185
+ padding: 1,
186
+ margin: { top: 0, bottom: 1 },
187
+ borderStyle: 'round',
188
+ borderColor: 'gray',
189
+ })
190
+ );
191
+ }
192
+
193
+ // ─── Feedback boxes ───────────────────────────────────────────────────────────
194
+
195
+ export function showSuccess(msg) {
196
+ console.log(
197
+ boxen(chalk.greenBright('✔ ') + chalk.bold.white(msg), {
198
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
199
+ margin: { top: 1, bottom: 1 },
200
+ borderStyle: 'round',
201
+ borderColor: 'green',
202
+ })
203
+ );
204
+ }
205
+
206
+ export function showError(msg) {
207
+ console.log(
208
+ boxen(chalk.redBright('✖ ') + chalk.bold.white(msg), {
209
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
210
+ margin: { top: 1, bottom: 1 },
211
+ borderStyle: 'round',
212
+ borderColor: 'red',
213
+ })
214
+ );
215
+ }
216
+
217
+ export function showInfo(msg) {
218
+ console.log(
219
+ boxen(chalk.cyanBright('ℹ ') + chalk.white(msg), {
220
+ padding: { top: 0, bottom: 0, left: 1, right: 1 },
221
+ margin: { top: 1, bottom: 1 },
222
+ borderStyle: 'round',
223
+ borderColor: 'cyan',
224
+ })
225
+ );
226
+ }
227
+
228
+ // ─── Help ─────────────────────────────────────────────────────────────────────
229
+
230
+ export function showHelp() {
231
+ showHeader();
232
+ console.log(`
233
+ ${chalk.bold.cyanBright('Usage:')}
234
+
235
+ ${chalk.bold.white('saveinme')} List all saved notes
236
+ ${chalk.bold.white('saveinme -s')} Create or edit a note (interactive)
237
+ ${chalk.bold.white('saveinme -s')} ${chalk.italic('<title>')} Edit a specific note by title
238
+ ${chalk.bold.white('saveinme -v')} ${chalk.italic('<title>')} View a note in full
239
+ ${chalk.bold.white('saveinme -d')} ${chalk.italic('<title>')} Delete a note
240
+ ${chalk.bold.white('saveinme -p')} ${chalk.italic('<title>')} Toggle pin on a note
241
+ ${chalk.bold.white('saveinme -f')} ${chalk.italic('<term>')} Search by title, content, tags, category
242
+ ${chalk.bold.white('saveinme -h')} Show this help
243
+
244
+ ${chalk.bold.cyanBright('Note fields saved:')}
245
+
246
+ ${chalk.cyan('·')} Title ${chalk.cyan('·')} Content (multi-line)
247
+ ${chalk.cyan('·')} Category ${chalk.cyan('·')} Tags (multiple)
248
+ ${chalk.cyan('·')} Priority ${chalk.cyan('·')} Pinned
249
+ ${chalk.cyan('·')} Created date ${chalk.cyan('·')} Updated date
250
+ ${chalk.cyan('·')} Word count ${chalk.cyan('·')} Char count
251
+
252
+ ${chalk.bold.cyanBright('Storage:')}
253
+
254
+ ${chalk.dim('~/.saveinme/notes.json')} ${chalk.dim('(permanent, survives reboots)')}
255
+ `);
256
+ }
257
+
258
+ // ─── Utilities ────────────────────────────────────────────────────────────────
259
+
260
+ function truncate(str, n) {
261
+ return str.length > n ? str.slice(0, n - 1) + '…' : str;
262
+ }
package/src/editor.js ADDED
@@ -0,0 +1,170 @@
1
+ import { input, select, confirm } from '@inquirer/prompts';
2
+ import chalk from 'chalk';
3
+ import { getAllNotes, saveNote, getNoteByTitle } from './store.js';
4
+ import { showHeader, showSuccess, showError } from './display.js';
5
+ import { multilineEditor } from './multiline.js';
6
+
7
+ // ─── Main editor entry point ──────────────────────────────────────────────────
8
+
9
+ /**
10
+ * Opens the interactive note editor.
11
+ * Flow: Title → Content → (optional details) → Save
12
+ * @param {string|undefined} titleArg If provided, jump straight to editing that note.
13
+ */
14
+ export async function openEditor(titleArg) {
15
+ showHeader();
16
+ console.log('');
17
+
18
+ const notes = getAllNotes();
19
+ let existingNote = null;
20
+
21
+ // ── Resolve which note we're working on ───────────────────────────────────
22
+ if (titleArg) {
23
+ existingNote = getNoteByTitle(titleArg);
24
+ if (!existingNote) {
25
+ console.log(
26
+ chalk.yellow(` Note "${titleArg}" not found — creating a new note with this title.\n`)
27
+ );
28
+ }
29
+ } else if (notes.length > 0) {
30
+ const action = await select({
31
+ message: 'What would you like to do?',
32
+ choices: [
33
+ { name: '✨ Create a new note', value: 'new' },
34
+ { name: '✏️ Edit an existing note', value: 'edit' },
35
+ ],
36
+ });
37
+
38
+ if (action === 'edit') {
39
+ const sorted = [...notes].sort((a, b) => {
40
+ if (a.pinned && !b.pinned) return -1;
41
+ if (!a.pinned && b.pinned) return 1;
42
+ return new Date(b.updatedAt) - new Date(a.updatedAt);
43
+ });
44
+
45
+ const selectedId = await select({
46
+ message: 'Select a note to edit:',
47
+ choices: sorted.map(n => ({
48
+ name:
49
+ (n.pinned ? chalk.yellow('📌 ') : ' ') +
50
+ chalk.bold(n.title) +
51
+ chalk.dim(` (${n.category || 'uncategorized'} · ${n.wordCount}w)`),
52
+ value: n.id,
53
+ })),
54
+ });
55
+
56
+ existingNote = notes.find(n => n.id === selectedId) ?? null;
57
+ }
58
+ }
59
+
60
+ console.log('');
61
+
62
+ // ── 1. Title ──────────────────────────────────────────────────────────────
63
+ const title = await input({
64
+ message: 'Title:',
65
+ default: existingNote?.title ?? (titleArg || ''),
66
+ validate: v => v.trim().length > 0 || 'Title cannot be empty.',
67
+ });
68
+
69
+ // ── 2. Content (straight to the editor) ───────────────────────────────────
70
+ console.log('');
71
+ const content = await multilineEditor(existingNote?.content ?? '');
72
+
73
+ if (content === null) {
74
+ showError('Note discarded.');
75
+ return;
76
+ }
77
+
78
+ const finalContent =
79
+ content.trim() === '' && existingNote?.content
80
+ ? existingNote.content
81
+ : content;
82
+
83
+ if (!finalContent.trim()) {
84
+ showError('Content cannot be empty. Note not saved.');
85
+ return;
86
+ }
87
+
88
+ // ── 3. Optional details ───────────────────────────────────────────────────
89
+ console.log('');
90
+ const addDetails = await confirm({
91
+ message: chalk.dim('Add tags / category / priority / pin? (optional)'),
92
+ default: false,
93
+ });
94
+
95
+ let tags = existingNote?.tags ?? [];
96
+ let category = existingNote?.category ?? '';
97
+ let priority = existingNote?.priority ?? 'medium';
98
+ let pinned = existingNote?.pinned ?? false;
99
+
100
+ if (addDetails) {
101
+ console.log('');
102
+
103
+ // Category
104
+ const existingCats = [...new Set(notes.map(n => n.category).filter(Boolean))];
105
+ if (existingCats.length > 0) {
106
+ const catChoice = await select({
107
+ message: 'Category:',
108
+ choices: [
109
+ ...existingCats.map(c => ({ name: c, value: c })),
110
+ { name: chalk.cyan('+ New category…'), value: '__new__' },
111
+ { name: chalk.dim('— None'), value: '' },
112
+ ],
113
+ default: existingNote?.category || '',
114
+ });
115
+ category =
116
+ catChoice === '__new__'
117
+ ? await input({ message: 'New category name:' })
118
+ : catChoice;
119
+ } else {
120
+ category = await input({
121
+ message: 'Category (optional):',
122
+ default: existingNote?.category ?? '',
123
+ });
124
+ }
125
+
126
+ // Tags
127
+ const tagsRaw = await input({
128
+ message: 'Tags (comma-separated):',
129
+ default: (existingNote?.tags ?? []).join(', '),
130
+ });
131
+ tags = tagsRaw
132
+ .split(',')
133
+ .map(t => t.trim().toLowerCase().replace(/^#/, ''))
134
+ .filter(Boolean);
135
+
136
+ // Priority
137
+ priority = await select({
138
+ message: 'Priority:',
139
+ choices: [
140
+ { name: chalk.green('🟢 Low'), value: 'low' },
141
+ { name: chalk.yellow('🟡 Medium'), value: 'medium' },
142
+ { name: chalk.red('🔴 High'), value: 'high' },
143
+ ],
144
+ default: existingNote?.priority ?? 'medium',
145
+ });
146
+
147
+ // Pin
148
+ pinned = await confirm({
149
+ message: 'Pin this note?',
150
+ default: existingNote?.pinned ?? false,
151
+ });
152
+ }
153
+
154
+ // ── Persist ───────────────────────────────────────────────────────────────
155
+ saveNote({
156
+ id: existingNote?.id,
157
+ title: title.trim(),
158
+ content: finalContent,
159
+ tags,
160
+ category: category.trim(),
161
+ priority,
162
+ pinned,
163
+ });
164
+
165
+ const wc = finalContent.trim().split(/\s+/).filter(Boolean).length;
166
+ showSuccess(
167
+ `Note "${title.trim()}" saved!` +
168
+ chalk.dim(` (${wc} words)`)
169
+ );
170
+ }
@@ -0,0 +1,90 @@
1
+ import { createInterface } from 'readline';
2
+ import chalk from 'chalk';
3
+
4
+ /**
5
+ * Cross-platform multi-line in-terminal text editor.
6
+ *
7
+ * The user types line by line. Pressing Enter on a line with just
8
+ * ":save" saves the content. ":cancel" discards it.
9
+ *
10
+ * Works on Windows (PowerShell / CMD) and Linux/macOS terminals alike
11
+ * since it only uses readline — no raw-mode or OS-specific APIs needed.
12
+ *
13
+ * @param {string} [defaultContent=''] Pre-filled content when editing an existing note
14
+ * @returns {Promise<string|null>} The collected text, or null if cancelled
15
+ */
16
+ export async function multilineEditor(defaultContent = '') {
17
+ // Show existing content as a dimmed preview if editing
18
+ if (defaultContent) {
19
+ console.log(chalk.dim('\n ── current content ──────────────────'));
20
+ defaultContent.split('\n').forEach(line => {
21
+ console.log(chalk.dim(` │ ${line}`));
22
+ });
23
+ console.log(chalk.dim(' ─────────────────────────────────────'));
24
+ console.log(
25
+ chalk.dim(' Start typing to replace, or type ') +
26
+ chalk.bold.white(':keep') +
27
+ chalk.dim(' to keep as-is.\n')
28
+ );
29
+ }
30
+
31
+ console.log(
32
+ chalk.bold.cyanBright(' ╭─ Write your note ─────────────────────────────╮') + '\n' +
33
+ chalk.cyanBright(' │') + chalk.dim(' Press Enter for a new line ') + chalk.cyanBright('│') + '\n' +
34
+ chalk.cyanBright(' │') + chalk.dim(' Type ') + chalk.bold.white(':save') + chalk.dim(' on a new line → save ') + chalk.cyanBright('│') + '\n' +
35
+ chalk.cyanBright(' │') + chalk.dim(' Type ') + chalk.bold.white(':cancel') + chalk.dim(' on a new line → discard ') + chalk.cyanBright('│') + '\n' +
36
+ (defaultContent
37
+ ? chalk.cyanBright(' │') + chalk.dim(' Type ') + chalk.bold.white(':keep') + chalk.dim(' on a new line → keep original ') + chalk.cyanBright('│') + '\n'
38
+ : '') +
39
+ chalk.bold.cyanBright(' ╰────────────────────────────────────────────────╯') + '\n'
40
+ );
41
+
42
+ return new Promise(resolve => {
43
+ const rl = createInterface({
44
+ input: process.stdin,
45
+ output: process.stdout,
46
+ terminal: true,
47
+ });
48
+
49
+ const lines = [];
50
+
51
+ const prompt = () => process.stdout.write(chalk.cyanBright(' ❯ '));
52
+ prompt();
53
+
54
+ rl.on('line', line => {
55
+ const trimmed = line.trim();
56
+
57
+ if (trimmed === ':save') {
58
+ rl.close();
59
+ resolve(lines.join('\n'));
60
+ return;
61
+ }
62
+
63
+ if (trimmed === ':cancel') {
64
+ rl.close();
65
+ resolve(null);
66
+ return;
67
+ }
68
+
69
+ if (trimmed === ':keep' && defaultContent) {
70
+ rl.close();
71
+ resolve(defaultContent);
72
+ return;
73
+ }
74
+
75
+ lines.push(line);
76
+ prompt();
77
+ });
78
+
79
+ // Handle Ctrl+C gracefully — treat as cancel
80
+ rl.on('SIGINT', () => {
81
+ rl.close();
82
+ resolve(null);
83
+ });
84
+
85
+ // Handle Ctrl+D (EOF) on Linux/macOS — treat as save
86
+ rl.on('close', () => {
87
+ if (lines.length > 0) resolve(lines.join('\n'));
88
+ });
89
+ });
90
+ }
package/src/store.js ADDED
@@ -0,0 +1,110 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { randomUUID } from 'crypto';
5
+
6
+ const DATA_DIR = join(homedir(), '.saveinme');
7
+ const DATA_FILE = join(DATA_DIR, 'notes.json');
8
+
9
+ function ensureDir() {
10
+ if (!existsSync(DATA_DIR)) {
11
+ mkdirSync(DATA_DIR, { recursive: true });
12
+ }
13
+ }
14
+
15
+ function readAll() {
16
+ ensureDir();
17
+ if (!existsSync(DATA_FILE)) return [];
18
+ try {
19
+ return JSON.parse(readFileSync(DATA_FILE, 'utf-8'));
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ function writeAll(notes) {
26
+ ensureDir();
27
+ writeFileSync(DATA_FILE, JSON.stringify(notes, null, 2), 'utf-8');
28
+ }
29
+
30
+ function countWords(text) {
31
+ return text.trim().split(/\s+/).filter(Boolean).length;
32
+ }
33
+
34
+ export function getAllNotes() {
35
+ return readAll();
36
+ }
37
+
38
+ export function getNoteByTitle(title) {
39
+ const notes = readAll();
40
+ return notes.find(n => n.title.toLowerCase() === title.toLowerCase()) ?? null;
41
+ }
42
+
43
+ export function getNoteById(id) {
44
+ const notes = readAll();
45
+ return notes.find(n => n.id === id) ?? null;
46
+ }
47
+
48
+ export function saveNote(noteData) {
49
+ const notes = readAll();
50
+ const now = new Date().toISOString();
51
+ const existingIdx = notes.findIndex(n => n.id === noteData.id);
52
+
53
+ if (existingIdx !== -1) {
54
+ notes[existingIdx] = {
55
+ ...notes[existingIdx],
56
+ ...noteData,
57
+ updatedAt: now,
58
+ wordCount: countWords(noteData.content),
59
+ charCount: noteData.content.length,
60
+ };
61
+ } else {
62
+ notes.push({
63
+ id: randomUUID(),
64
+ title: noteData.title,
65
+ content: noteData.content,
66
+ tags: noteData.tags ?? [],
67
+ category: noteData.category ?? '',
68
+ priority: noteData.priority ?? 'medium',
69
+ pinned: noteData.pinned ?? false,
70
+ color: noteData.color ?? '',
71
+ createdAt: now,
72
+ updatedAt: now,
73
+ wordCount: countWords(noteData.content),
74
+ charCount: noteData.content.length,
75
+ });
76
+ }
77
+
78
+ writeAll(notes);
79
+ }
80
+
81
+ export function deleteNoteById(id) {
82
+ const notes = readAll();
83
+ const next = notes.filter(n => n.id !== id);
84
+ writeAll(next);
85
+ return next.length < notes.length;
86
+ }
87
+
88
+ export function togglePin(id) {
89
+ const notes = readAll();
90
+ const idx = notes.findIndex(n => n.id === id);
91
+ if (idx === -1) return false;
92
+ notes[idx].pinned = !notes[idx].pinned;
93
+ writeAll(notes);
94
+ return notes[idx].pinned;
95
+ }
96
+
97
+ export function searchNotes(term) {
98
+ const notes = readAll();
99
+ const q = term.toLowerCase();
100
+ return notes.filter(n =>
101
+ n.title.toLowerCase().includes(q) ||
102
+ n.content.toLowerCase().includes(q) ||
103
+ (n.tags ?? []).some(t => t.toLowerCase().includes(q)) ||
104
+ (n.category ?? '').toLowerCase().includes(q)
105
+ );
106
+ }
107
+
108
+ export function getDataPath() {
109
+ return DATA_FILE;
110
+ }