saveinme 1.3.4 → 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/README.md +64 -6
- package/bin/saveinme.js +99 -24
- package/package.json +2 -1
- package/src/ai.js +447 -0
- package/src/display.js +48 -10
- package/src/editor.js +226 -30
- package/src/store.js +16 -8
- package/src/sync.js +165 -0
- package/src/tui.js +336 -0
package/src/editor.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { input, select, confirm } from '@inquirer/prompts';
|
|
1
|
+
import { input, select, confirm, checkbox } from '@inquirer/prompts';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
-
import { getAllNotes, saveNote, getNoteByTitle, getDefaultNotebook } from './store.js';
|
|
4
|
-
import { showHeader, showSuccess, showError, showInfo } from './display.js';
|
|
3
|
+
import { getAllNotes, saveNote, getNoteByTitle, getDefaultNotebook, setDefaultNotebook, deleteNoteById } from './store.js';
|
|
4
|
+
import { showHeader, showSuccess, showError, showInfo, showNote } from './display.js';
|
|
5
5
|
import { multilineEditor } from './multiline.js';
|
|
6
6
|
import { getClipboardText } from './clipboard.js';
|
|
7
|
+
import { triggerAutoSync } from './sync.js';
|
|
7
8
|
|
|
8
9
|
// Helper for generating unique, readable default titles
|
|
9
10
|
function getTimestamp() {
|
|
@@ -49,30 +50,56 @@ export async function saveFromClipboard(titleArg, notebookOverride) {
|
|
|
49
50
|
}
|
|
50
51
|
console.log(chalk.dim(' └──────────────────────────────────────────────────\n'));
|
|
51
52
|
|
|
52
|
-
// Resolve title
|
|
53
|
-
const title = (titleArg && titleArg.trim()) || `Clipboard Note - ${getTimestamp()}`;
|
|
54
|
-
const existingNote = getNoteByTitle(title);
|
|
55
|
-
|
|
56
53
|
// Resolve notebook target
|
|
57
54
|
const defaultNotebook = getDefaultNotebook();
|
|
58
|
-
const
|
|
55
|
+
const tempCategory = (notebookOverride !== null) ? notebookOverride.trim() : defaultNotebook;
|
|
56
|
+
|
|
57
|
+
// Resolve title
|
|
58
|
+
let title = (titleArg && titleArg.trim()) || '';
|
|
59
|
+
let isAppend = false;
|
|
60
|
+
|
|
61
|
+
if (!title) {
|
|
62
|
+
if (tempCategory) {
|
|
63
|
+
title = tempCategory;
|
|
64
|
+
isAppend = true;
|
|
65
|
+
} else {
|
|
66
|
+
title = `Clipboard Note - ${getTimestamp()}`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const existingNoteResolve = getNoteByTitle(title);
|
|
71
|
+
const category = (notebookOverride !== null) ? notebookOverride.trim() : (existingNoteResolve?.category ?? defaultNotebook);
|
|
72
|
+
let finalContent = clipboardContent;
|
|
73
|
+
|
|
74
|
+
if (existingNoteResolve && isAppend) {
|
|
75
|
+
finalContent = existingNoteResolve.content + '\n' + clipboardContent;
|
|
76
|
+
}
|
|
59
77
|
|
|
60
78
|
saveNote({
|
|
61
|
-
id:
|
|
79
|
+
id: existingNoteResolve?.id, // updates if title matches exactly
|
|
62
80
|
title: title.trim(),
|
|
63
|
-
content:
|
|
81
|
+
content: finalContent,
|
|
64
82
|
category: category.trim(),
|
|
65
|
-
tags:
|
|
66
|
-
priority:
|
|
67
|
-
pinned:
|
|
83
|
+
tags: existingNoteResolve?.tags ?? [],
|
|
84
|
+
priority: existingNoteResolve?.priority ?? 'medium',
|
|
85
|
+
pinned: existingNoteResolve?.pinned ?? false,
|
|
68
86
|
});
|
|
87
|
+
triggerAutoSync();
|
|
69
88
|
|
|
70
89
|
const wc = clipboardContent.split(/\s+/).filter(Boolean).length;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
90
|
+
if (existingNoteResolve && isAppend) {
|
|
91
|
+
showSuccess(
|
|
92
|
+
`Appended clipboard text to note "${title.trim()}"` +
|
|
93
|
+
(category ? ` in notebook "${category.trim()}"` : '') +
|
|
94
|
+
chalk.dim(` (+${wc} words)`)
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
showSuccess(
|
|
98
|
+
`Clipboard note "${title.trim()}" saved!` +
|
|
99
|
+
(category ? ` [Notebook: ${category.trim()}]` : '') +
|
|
100
|
+
chalk.dim(` (${wc} words)`)
|
|
101
|
+
);
|
|
102
|
+
}
|
|
76
103
|
}
|
|
77
104
|
|
|
78
105
|
// ─── Save piped stdin directly ────────────────────────────────────────────────
|
|
@@ -96,25 +123,50 @@ export async function savePipedInput(inputContent, titleArg, notebookOverride) {
|
|
|
96
123
|
const category = (notebookOverride !== null) ? notebookOverride.trim() : defaultNotebook;
|
|
97
124
|
|
|
98
125
|
// Resolve title
|
|
99
|
-
|
|
100
|
-
|
|
126
|
+
let title = (titleArg && titleArg.trim()) || '';
|
|
127
|
+
let isAppend = false;
|
|
128
|
+
|
|
129
|
+
if (!title) {
|
|
130
|
+
if (category) {
|
|
131
|
+
title = category;
|
|
132
|
+
isAppend = true;
|
|
133
|
+
} else {
|
|
134
|
+
title = `Piped Note - ${getTimestamp()}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const existingNoteResolve = getNoteByTitle(title);
|
|
139
|
+
let finalContent = content;
|
|
140
|
+
|
|
141
|
+
if (existingNoteResolve && isAppend) {
|
|
142
|
+
finalContent = existingNoteResolve.content + '\n' + content;
|
|
143
|
+
}
|
|
101
144
|
|
|
102
145
|
saveNote({
|
|
103
|
-
id:
|
|
146
|
+
id: existingNoteResolve?.id,
|
|
104
147
|
title: title.trim(),
|
|
105
|
-
content:
|
|
148
|
+
content: finalContent,
|
|
106
149
|
category: category,
|
|
107
|
-
tags:
|
|
108
|
-
priority:
|
|
109
|
-
pinned:
|
|
150
|
+
tags: existingNoteResolve?.tags ?? [],
|
|
151
|
+
priority: existingNoteResolve?.priority ?? 'medium',
|
|
152
|
+
pinned: existingNoteResolve?.pinned ?? false,
|
|
110
153
|
});
|
|
154
|
+
triggerAutoSync();
|
|
111
155
|
|
|
112
156
|
const wc = content.split(/\s+/).filter(Boolean).length;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
)
|
|
157
|
+
if (existingNoteResolve && isAppend) {
|
|
158
|
+
showSuccess(
|
|
159
|
+
`Appended piped text to note "${title.trim()}"` +
|
|
160
|
+
(category ? ` in notebook "${category}"` : '') +
|
|
161
|
+
chalk.dim(` (+${wc} words)`)
|
|
162
|
+
);
|
|
163
|
+
} else {
|
|
164
|
+
showSuccess(
|
|
165
|
+
`Saved piped note "${title.trim()}"` +
|
|
166
|
+
(category ? ` to notebook "${category}"` : '') +
|
|
167
|
+
chalk.dim(` (${wc} words)`)
|
|
168
|
+
);
|
|
169
|
+
}
|
|
118
170
|
}
|
|
119
171
|
|
|
120
172
|
// ─── Main editor entry point ──────────────────────────────────────────────────
|
|
@@ -302,6 +354,7 @@ export async function openEditor(titleArg, notebookOverride) {
|
|
|
302
354
|
priority,
|
|
303
355
|
pinned,
|
|
304
356
|
});
|
|
357
|
+
triggerAutoSync();
|
|
305
358
|
|
|
306
359
|
const wc = finalContent.trim().split(/\s+/).filter(Boolean).length;
|
|
307
360
|
showSuccess(
|
|
@@ -310,3 +363,146 @@ export async function openEditor(titleArg, notebookOverride) {
|
|
|
310
363
|
chalk.dim(` (${wc} words)`)
|
|
311
364
|
);
|
|
312
365
|
}
|
|
366
|
+
|
|
367
|
+
// ─── Interactive Group Delete ─────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Renders a checkbox menu to select and delete multiple notes at once.
|
|
371
|
+
*/
|
|
372
|
+
export async function interactiveGroupDelete() {
|
|
373
|
+
const notes = getAllNotes();
|
|
374
|
+
if (notes.length === 0) {
|
|
375
|
+
showError('No notes saved to delete.');
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Sort notes: pinned first, then by last modified
|
|
380
|
+
const sorted = [...notes].sort((a, b) => {
|
|
381
|
+
if (a.pinned && !b.pinned) return -1;
|
|
382
|
+
if (!a.pinned && b.pinned) return 1;
|
|
383
|
+
return new Date(b.updatedAt) - new Date(a.updatedAt);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
const selectedIds = await checkbox({
|
|
387
|
+
message: 'Select notes to remove (Space to select, Enter to confirm):',
|
|
388
|
+
choices: sorted.map(n => ({
|
|
389
|
+
name: `${n.pinned ? '📌 ' : ''}${chalk.bold(n.title)} ${chalk.dim(`(${n.category || 'uncategorized'} · ${n.wordCount}w)`)}`,
|
|
390
|
+
value: n.id,
|
|
391
|
+
})),
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
if (selectedIds.length === 0) {
|
|
395
|
+
showInfo('No notes selected. Nothing deleted.');
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const confirmDelete = await confirm({
|
|
400
|
+
message: `Are you sure you want to delete ${selectedIds.length} note(s)?`,
|
|
401
|
+
default: false,
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (!confirmDelete) {
|
|
405
|
+
showInfo('Deletion cancelled.');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
let count = 0;
|
|
410
|
+
for (const id of selectedIds) {
|
|
411
|
+
if (deleteNoteById(id)) {
|
|
412
|
+
count++;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (count > 0) {
|
|
417
|
+
triggerAutoSync();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
showSuccess(`Successfully removed ${count} note(s).`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Renders a select menu to set the default notebook target.
|
|
425
|
+
*/
|
|
426
|
+
export async function interactiveSetDefaultNotebook() {
|
|
427
|
+
const notes = getAllNotes();
|
|
428
|
+
const current = getDefaultNotebook();
|
|
429
|
+
const existingCats = [...new Set(notes.map(n => n.category).filter(Boolean))];
|
|
430
|
+
|
|
431
|
+
const choices = [];
|
|
432
|
+
if (existingCats.length > 0) {
|
|
433
|
+
choices.push(...existingCats.map(c => ({ name: `📁 ${c}`, value: c })));
|
|
434
|
+
}
|
|
435
|
+
choices.push(
|
|
436
|
+
{ name: chalk.cyan('+ Create new notebook…'), value: '__new__' },
|
|
437
|
+
{ name: chalk.red('❌ Clear default notebook'), value: '__clear__' },
|
|
438
|
+
{ name: chalk.dim('↩️ Keep current default'), value: '__keep__' }
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
console.log('');
|
|
442
|
+
if (current) {
|
|
443
|
+
console.log(chalk.cyan(` Current default notebook: "${current}"`));
|
|
444
|
+
} else {
|
|
445
|
+
console.log(chalk.dim(' No default notebook currently set.'));
|
|
446
|
+
}
|
|
447
|
+
console.log('');
|
|
448
|
+
|
|
449
|
+
const choice = await select({
|
|
450
|
+
message: 'Select default notebook:',
|
|
451
|
+
choices,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
if (choice === '__keep__') {
|
|
455
|
+
showInfo('No changes made to default notebook.');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (choice === '__clear__') {
|
|
460
|
+
setDefaultNotebook('');
|
|
461
|
+
showSuccess('Default notebook cleared.');
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
let notebookName = choice;
|
|
466
|
+
if (choice === '__new__') {
|
|
467
|
+
const typed = await input({
|
|
468
|
+
message: 'New notebook name:',
|
|
469
|
+
validate: v => v.trim().length > 0 || 'Notebook name cannot be empty.',
|
|
470
|
+
});
|
|
471
|
+
notebookName = typed.trim();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
setDefaultNotebook(notebookName);
|
|
475
|
+
showSuccess(`Target notebook set to "${notebookName}".\n New notes will default to this notebook.`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Renders a select menu to pick a note to view.
|
|
480
|
+
*/
|
|
481
|
+
export async function interactiveViewNote() {
|
|
482
|
+
const notes = getAllNotes();
|
|
483
|
+
if (notes.length === 0) {
|
|
484
|
+
showError('No notes saved to view.');
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const sorted = [...notes].sort((a, b) => {
|
|
489
|
+
if (a.pinned && !b.pinned) return -1;
|
|
490
|
+
if (!a.pinned && b.pinned) return 1;
|
|
491
|
+
return new Date(b.updatedAt) - new Date(a.updatedAt);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
const choices = sorted.map(n => ({
|
|
495
|
+
name: `${n.pinned ? '📌 ' : ' '}${chalk.bold(n.title)} ${chalk.dim(`(${n.category || 'uncategorized'} · ${n.wordCount}w)`)}`,
|
|
496
|
+
value: n.id,
|
|
497
|
+
}));
|
|
498
|
+
|
|
499
|
+
const selectedId = await select({
|
|
500
|
+
message: 'Select a note to view:',
|
|
501
|
+
choices,
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const note = notes.find(n => n.id === selectedId);
|
|
505
|
+
if (note) {
|
|
506
|
+
showNote(note);
|
|
507
|
+
}
|
|
508
|
+
}
|
package/src/store.js
CHANGED
|
@@ -111,19 +111,27 @@ export function getDataPath() {
|
|
|
111
111
|
return DATA_FILE;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
export function
|
|
114
|
+
export function getConfig() {
|
|
115
115
|
ensureDir();
|
|
116
|
-
if (!existsSync(CONFIG_FILE)) return
|
|
116
|
+
if (!existsSync(CONFIG_FILE)) return {};
|
|
117
117
|
try {
|
|
118
|
-
|
|
119
|
-
return config.defaultNotebook ?? '';
|
|
118
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8')) ?? {};
|
|
120
119
|
} catch {
|
|
121
|
-
return
|
|
120
|
+
return {};
|
|
122
121
|
}
|
|
123
122
|
}
|
|
124
123
|
|
|
125
|
-
export function
|
|
124
|
+
export function saveConfig(updates) {
|
|
126
125
|
ensureDir();
|
|
127
|
-
const
|
|
128
|
-
|
|
126
|
+
const current = getConfig();
|
|
127
|
+
const next = { ...current, ...updates };
|
|
128
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(next, null, 2), 'utf-8');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function getDefaultNotebook() {
|
|
132
|
+
return getConfig().defaultNotebook ?? '';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function setDefaultNotebook(name) {
|
|
136
|
+
saveConfig({ defaultNotebook: name.trim() });
|
|
129
137
|
}
|
package/src/sync.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import { existsSync, writeFileSync } from 'fs';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { confirm, input } from '@inquirer/prompts';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { getDataPath, getConfig, saveConfig } from './store.js';
|
|
7
|
+
import { showSuccess, showError, showInfo } from './display.js';
|
|
8
|
+
|
|
9
|
+
// Get base directory of saveinme database (~/.saveinme)
|
|
10
|
+
const DATA_DIR = dirname(getDataPath());
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Runs a git command inside the saveinme directory.
|
|
14
|
+
* @param {string} cmd Git subcommand
|
|
15
|
+
* @returns {string|null} command output or null if failed
|
|
16
|
+
*/
|
|
17
|
+
function runGit(cmd) {
|
|
18
|
+
try {
|
|
19
|
+
return execSync(`git ${cmd}`, { cwd: DATA_DIR, stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
20
|
+
} catch (err) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Checks if Git is installed on the system.
|
|
27
|
+
*/
|
|
28
|
+
function isGitInstalled() {
|
|
29
|
+
try {
|
|
30
|
+
execSync('git --version', { stdio: 'ignore' });
|
|
31
|
+
return true;
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Initializes Git repository in the database directory if not already initialized.
|
|
39
|
+
*/
|
|
40
|
+
function ensureGitRepo() {
|
|
41
|
+
if (!existsSync(join(DATA_DIR, '.git'))) {
|
|
42
|
+
showInfo('Initializing local Git repository...');
|
|
43
|
+
runGit('init');
|
|
44
|
+
|
|
45
|
+
// Create .gitignore to prevent uploading local configs (like API keys)
|
|
46
|
+
const gitignorePath = join(DATA_DIR, '.gitignore');
|
|
47
|
+
writeFileSync(gitignorePath, 'config.json\n', 'utf-8');
|
|
48
|
+
runGit('add .gitignore');
|
|
49
|
+
runGit('commit -m "Initial commit: setup gitignore"');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Main Git Sync function. Pulls latest notes, commits local changes, and pushes them.
|
|
55
|
+
* @param {boolean} forceSetup If true, prompt for repository configuration even if configured.
|
|
56
|
+
*/
|
|
57
|
+
export async function syncNotes(forceSetup = false) {
|
|
58
|
+
if (!isGitInstalled()) {
|
|
59
|
+
showError('Git is not installed or not in your PATH. Please install Git first.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
ensureGitRepo();
|
|
64
|
+
|
|
65
|
+
// Check if remote is configured
|
|
66
|
+
let remoteUrl = runGit('remote get-url origin');
|
|
67
|
+
const config = getConfig();
|
|
68
|
+
|
|
69
|
+
if (!remoteUrl || forceSetup) {
|
|
70
|
+
console.log(chalk.yellow('\n🌐 Git Sync Remote Setup'));
|
|
71
|
+
const setupRemote = await confirm({
|
|
72
|
+
message: 'Would you like to configure a remote Git repository (e.g. on GitHub) to sync notes?',
|
|
73
|
+
default: true,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!setupRemote) {
|
|
77
|
+
showInfo('Git Sync cancelled.');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const url = await input({
|
|
82
|
+
message: 'Enter remote repository URL (SSH preferred, e.g. git@github.com:username/notes-repo.git):',
|
|
83
|
+
validate: v => v.trim().length > 0 || 'Repository URL cannot be empty.',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if (remoteUrl) {
|
|
87
|
+
runGit('remote remove origin');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const added = runGit(`remote add origin ${url.trim()}`);
|
|
91
|
+
if (added === null) {
|
|
92
|
+
showError('Failed to configure remote repository URL.');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
remoteUrl = url.trim();
|
|
96
|
+
|
|
97
|
+
// Enable auto-sync by default if remote is set up
|
|
98
|
+
saveConfig({ autoSync: true });
|
|
99
|
+
showSuccess('Remote repository configured successfully. Auto-sync enabled.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Get current active branch (default to main/master)
|
|
103
|
+
let branch = runGit('branch --show-current');
|
|
104
|
+
if (!branch) {
|
|
105
|
+
// Check if there are branches, or default to main
|
|
106
|
+
branch = 'main';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
showInfo(`Syncing notes with ${chalk.cyan(remoteUrl)} [branch: ${chalk.cyan(branch)}]...`);
|
|
110
|
+
|
|
111
|
+
// Stage changes
|
|
112
|
+
runGit('add notes.json');
|
|
113
|
+
|
|
114
|
+
// Check if there are staged changes to commit
|
|
115
|
+
const status = runGit('status --porcelain');
|
|
116
|
+
if (status && status.includes('notes.json')) {
|
|
117
|
+
runGit('commit -m "sync: update notes"');
|
|
118
|
+
showInfo('Committed local changes.');
|
|
119
|
+
} else {
|
|
120
|
+
showInfo('No local changes to commit.');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Try to pull remote changes
|
|
124
|
+
showInfo('Pulling latest changes from remote...');
|
|
125
|
+
// Use --rebase to keep note history clean.
|
|
126
|
+
const pullResult = runGit(`pull --rebase origin ${branch}`);
|
|
127
|
+
if (pullResult === null) {
|
|
128
|
+
showError('Failed to pull from remote. You may have merge conflicts.');
|
|
129
|
+
showInfo('Aborting rebase...');
|
|
130
|
+
runGit('rebase --abort');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Push local changes to remote
|
|
135
|
+
showInfo('Pushing local updates to remote...');
|
|
136
|
+
const pushResult = runGit(`push -u origin ${branch}`);
|
|
137
|
+
if (pushResult === null) {
|
|
138
|
+
showError('Failed to push to remote repository.');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
showSuccess('Sync complete! All notes are up to date.');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Triggers background/silent auto-sync when notes are modified.
|
|
147
|
+
*/
|
|
148
|
+
export function triggerAutoSync() {
|
|
149
|
+
const config = getConfig();
|
|
150
|
+
if (!config.autoSync) return;
|
|
151
|
+
|
|
152
|
+
if (!isGitInstalled() || !existsSync(join(DATA_DIR, '.git'))) return;
|
|
153
|
+
|
|
154
|
+
// Run auto-sync asynchronously so it doesn't block the CLI interface
|
|
155
|
+
import('child_process').then(({ exec }) => {
|
|
156
|
+
let branch = runGit('branch --show-current') || 'main';
|
|
157
|
+
exec(`git add notes.json && git commit -m "sync: auto-update notes" && git pull --rebase origin ${branch} && git push origin ${branch}`, {
|
|
158
|
+
cwd: DATA_DIR,
|
|
159
|
+
}, (error) => {
|
|
160
|
+
if (error) {
|
|
161
|
+
// Fail silently in background to avoid disrupting user workflow
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|