wasibase 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 +69 -0
- package/bin/wasibase.js +5 -0
- package/package.json +54 -0
- package/src/config.js +11 -0
- package/src/index.js +54 -0
- package/src/storage.js +262 -0
- package/src/ui/backup.js +248 -0
- package/src/ui/graph.js +21 -0
- package/src/ui/manage.js +320 -0
- package/src/ui/note.js +449 -0
- package/src/ui/search.js +21 -0
- package/src/ui/sync.js +348 -0
- package/src/utils.js +104 -0
- package/src/web/graphServer.js +897 -0
- package/src/web/server.js +2132 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Wasibase
|
|
2
|
+
|
|
3
|
+
> Dein Second Brain. Terminal-basiert. Mit Backlinks.
|
|
4
|
+
|
|
5
|
+
Ein terminal-basiertes Notizen-System mit Markdown-Unterstuetzung, Backlinks und Graph-Visualisierung.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g wasibase
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Verwendung
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Hauptmenu oeffnen
|
|
17
|
+
wasibase
|
|
18
|
+
|
|
19
|
+
# Neue Note erstellen oder bearbeiten
|
|
20
|
+
wasibase note
|
|
21
|
+
|
|
22
|
+
# Notes durchsuchen
|
|
23
|
+
wasibase search
|
|
24
|
+
|
|
25
|
+
# Wissens-Graph anzeigen
|
|
26
|
+
wasibase graph
|
|
27
|
+
|
|
28
|
+
# Backup erstellen/wiederherstellen
|
|
29
|
+
wasibase backup
|
|
30
|
+
|
|
31
|
+
# Mit Cloud synchronisieren (Proton Drive, Dropbox, iCloud)
|
|
32
|
+
wasibase sync
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Markdown Notes** - Schreibe deine Notizen in Markdown mit Live-Vorschau
|
|
38
|
+
- **Backlinks** - Verknuepfe Wissen mit `[[Backlinks]]` wie in Obsidian
|
|
39
|
+
- **Graph View** - Visualisiere dein Wissen als interaktiven Graph
|
|
40
|
+
- **Schnelle Suche** - Finde jede Note sofort
|
|
41
|
+
- **Cloud Backup** - Automatisches Backup zu Proton Drive, Dropbox oder iCloud
|
|
42
|
+
- **Terminal First** - Schnell und effizient direkt aus dem Terminal
|
|
43
|
+
|
|
44
|
+
## Struktur
|
|
45
|
+
|
|
46
|
+
Notes werden in `~/.wasibase/notes/` gespeichert:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
~/.wasibase/
|
|
50
|
+
├── notes/
|
|
51
|
+
│ ├── Oberkategorie/
|
|
52
|
+
│ │ └── Unterkategorie/
|
|
53
|
+
│ │ └── Note.md
|
|
54
|
+
├── backups/
|
|
55
|
+
└── config.json
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Systemanforderungen
|
|
59
|
+
|
|
60
|
+
- Node.js 18+
|
|
61
|
+
- macOS, Linux oder Windows
|
|
62
|
+
|
|
63
|
+
## Lizenz
|
|
64
|
+
|
|
65
|
+
MIT
|
|
66
|
+
|
|
67
|
+
## Autor
|
|
68
|
+
|
|
69
|
+
[Wasili](https://github.com/wasuli)
|
package/bin/wasibase.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wasibase",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Second Brain - Terminal-basiertes Notizen-System mit Backlinks und Graph-Visualisierung",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"wasibase": "./bin/wasibase.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node bin/wasibase.js",
|
|
12
|
+
"note": "node bin/wasibase.js note",
|
|
13
|
+
"search": "node bin/wasibase.js search",
|
|
14
|
+
"graph": "node bin/wasibase.js graph",
|
|
15
|
+
"backup": "node bin/wasibase.js backup",
|
|
16
|
+
"sync": "node bin/wasibase.js sync"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"notes",
|
|
20
|
+
"second-brain",
|
|
21
|
+
"backlinks",
|
|
22
|
+
"markdown",
|
|
23
|
+
"cli",
|
|
24
|
+
"terminal",
|
|
25
|
+
"knowledge-base",
|
|
26
|
+
"zettelkasten",
|
|
27
|
+
"note-taking",
|
|
28
|
+
"graph"
|
|
29
|
+
],
|
|
30
|
+
"author": "Wasili",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/wasuli/wasibase.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/wasuli/wasibase/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://wasuli.github.io/wasibase",
|
|
40
|
+
"files": [
|
|
41
|
+
"bin",
|
|
42
|
+
"src"
|
|
43
|
+
],
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"chalk": "^5.6.2",
|
|
46
|
+
"express": "^5.2.1",
|
|
47
|
+
"inquirer": "^9.3.8",
|
|
48
|
+
"marked": "^17.0.1",
|
|
49
|
+
"open": "^11.0.0"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=18.0.0"
|
|
53
|
+
}
|
|
54
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
|
|
4
|
+
export const CONFIG = {
|
|
5
|
+
baseDir: path.join(os.homedir(), '.wasibase'),
|
|
6
|
+
notesDir: path.join(os.homedir(), '.wasibase', 'notes'),
|
|
7
|
+
tempDir: path.join(os.homedir(), '.wasibase', 'temp'),
|
|
8
|
+
configFile: path.join(os.homedir(), '.wasibase', 'config.json'),
|
|
9
|
+
backupDir: path.join(os.homedir(), '.wasibase', 'backups'),
|
|
10
|
+
editor: process.env.EDITOR || 'vim'
|
|
11
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { mainMenu } from './ui/manage.js';
|
|
2
|
+
import { noteMenu } from './ui/note.js';
|
|
3
|
+
import { searchMenu } from './ui/search.js';
|
|
4
|
+
import { graphMenu } from './ui/graph.js';
|
|
5
|
+
import { backupMenu } from './ui/backup.js';
|
|
6
|
+
import { syncMenu } from './ui/sync.js';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
export async function main() {
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const command = args[0];
|
|
12
|
+
|
|
13
|
+
switch (command) {
|
|
14
|
+
case 'note':
|
|
15
|
+
case 'n':
|
|
16
|
+
await noteMenu();
|
|
17
|
+
break;
|
|
18
|
+
case 'search':
|
|
19
|
+
case 's':
|
|
20
|
+
await searchMenu();
|
|
21
|
+
break;
|
|
22
|
+
case 'graph':
|
|
23
|
+
case 'g':
|
|
24
|
+
await graphMenu();
|
|
25
|
+
break;
|
|
26
|
+
case 'backup':
|
|
27
|
+
case 'b':
|
|
28
|
+
await backupMenu();
|
|
29
|
+
break;
|
|
30
|
+
case 'sync':
|
|
31
|
+
await syncMenu();
|
|
32
|
+
break;
|
|
33
|
+
case 'help':
|
|
34
|
+
case 'h':
|
|
35
|
+
showHelp();
|
|
36
|
+
break;
|
|
37
|
+
default:
|
|
38
|
+
await mainMenu();
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function showHelp() {
|
|
44
|
+
console.clear();
|
|
45
|
+
console.log(chalk.cyan.bold('\n WASIBASE\n'));
|
|
46
|
+
console.log(chalk.bold(' Befehle:\n'));
|
|
47
|
+
console.log(chalk.gray(' wasibase ') + chalk.bold('Verwaltung (Kategorien)'));
|
|
48
|
+
console.log(chalk.gray(' wasibase note ') + chalk.bold('Note erstellen/bearbeiten'));
|
|
49
|
+
console.log(chalk.gray(' wasibase search ') + chalk.bold('Notes durchsuchen'));
|
|
50
|
+
console.log(chalk.gray(' wasibase graph ') + chalk.bold('Verknuepfungen visualisieren'));
|
|
51
|
+
console.log(chalk.gray(' wasibase backup ') + chalk.bold('Backup erstellen/wiederherstellen'));
|
|
52
|
+
console.log(chalk.gray(' wasibase sync ') + chalk.bold('Mit Cloud synchronisieren'));
|
|
53
|
+
console.log(chalk.gray(' wasibase help ') + chalk.bold('Diese Hilfe\n'));
|
|
54
|
+
}
|
package/src/storage.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Module - File system operations for Wasibase
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { CONFIG } from './config.js';
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// Directory Operations
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
export function ensureDir(dir) {
|
|
14
|
+
if (!fs.existsSync(dir)) {
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Category Operations
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
export function getOberkategorien() {
|
|
24
|
+
ensureDir(CONFIG.notesDir);
|
|
25
|
+
return fs.readdirSync(CONFIG.notesDir, { withFileTypes: true })
|
|
26
|
+
.filter(d => d.isDirectory())
|
|
27
|
+
.map(d => d.name)
|
|
28
|
+
.sort();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getUnterkategorien(oberkategorie) {
|
|
32
|
+
const dir = path.join(CONFIG.notesDir, oberkategorie);
|
|
33
|
+
if (!fs.existsSync(dir)) return [];
|
|
34
|
+
return fs.readdirSync(dir, { withFileTypes: true })
|
|
35
|
+
.filter(d => d.isDirectory())
|
|
36
|
+
.map(d => d.name)
|
|
37
|
+
.sort();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createOberkategorie(name) {
|
|
41
|
+
const dir = path.join(CONFIG.notesDir, name);
|
|
42
|
+
ensureDir(dir);
|
|
43
|
+
return dir;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createUnterkategorie(oberkategorie, name) {
|
|
47
|
+
const dir = path.join(CONFIG.notesDir, oberkategorie, name);
|
|
48
|
+
ensureDir(dir);
|
|
49
|
+
return dir;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function deleteOberkategorie(name) {
|
|
53
|
+
const dir = path.join(CONFIG.notesDir, name);
|
|
54
|
+
if (fs.existsSync(dir)) {
|
|
55
|
+
fs.rmSync(dir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function deleteUnterkategorie(oberkategorie, name) {
|
|
60
|
+
const dir = path.join(CONFIG.notesDir, oberkategorie, name);
|
|
61
|
+
if (fs.existsSync(dir)) {
|
|
62
|
+
fs.rmSync(dir, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// =============================================================================
|
|
67
|
+
// Note Operations
|
|
68
|
+
// =============================================================================
|
|
69
|
+
|
|
70
|
+
export function getNotes(oberkategorie, unterkategorie) {
|
|
71
|
+
const dir = path.join(CONFIG.notesDir, oberkategorie, unterkategorie);
|
|
72
|
+
if (!fs.existsSync(dir)) return [];
|
|
73
|
+
return fs.readdirSync(dir)
|
|
74
|
+
.filter(f => f.endsWith('.md'))
|
|
75
|
+
.map(f => f.replace('.md', ''))
|
|
76
|
+
.sort();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getNotePath(oberkategorie, unterkategorie, thema) {
|
|
80
|
+
return path.join(CONFIG.notesDir, oberkategorie, unterkategorie, `${thema}.md`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function noteExists(oberkategorie, unterkategorie, thema) {
|
|
84
|
+
return fs.existsSync(getNotePath(oberkategorie, unterkategorie, thema));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function readNote(oberkategorie, unterkategorie, thema) {
|
|
88
|
+
const filePath = getNotePath(oberkategorie, unterkategorie, thema);
|
|
89
|
+
if (!fs.existsSync(filePath)) return null;
|
|
90
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function saveNote(oberkategorie, unterkategorie, thema, content) {
|
|
94
|
+
const dir = path.join(CONFIG.notesDir, oberkategorie, unterkategorie);
|
|
95
|
+
ensureDir(dir);
|
|
96
|
+
const filePath = path.join(dir, `${thema}.md`);
|
|
97
|
+
fs.writeFileSync(filePath, content);
|
|
98
|
+
return filePath;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function deleteNote(oberkategorie, unterkategorie, thema) {
|
|
102
|
+
const filePath = getNotePath(oberkategorie, unterkategorie, thema);
|
|
103
|
+
if (fs.existsSync(filePath)) {
|
|
104
|
+
fs.unlinkSync(filePath);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get all notes across all categories
|
|
110
|
+
*/
|
|
111
|
+
export function getAllNotes() {
|
|
112
|
+
const allNotes = [];
|
|
113
|
+
|
|
114
|
+
for (const ober of getOberkategorien()) {
|
|
115
|
+
for (const unter of getUnterkategorien(ober)) {
|
|
116
|
+
for (const thema of getNotes(ober, unter)) {
|
|
117
|
+
allNotes.push({ oberkategorie: ober, unterkategorie: unter, thema });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return allNotes;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get statistics about the notes collection
|
|
127
|
+
*/
|
|
128
|
+
export function getStats() {
|
|
129
|
+
const oberkategorien = getOberkategorien();
|
|
130
|
+
let unterCount = 0;
|
|
131
|
+
let noteCount = 0;
|
|
132
|
+
|
|
133
|
+
for (const ober of oberkategorien) {
|
|
134
|
+
const unters = getUnterkategorien(ober);
|
|
135
|
+
unterCount += unters.length;
|
|
136
|
+
for (const unter of unters) {
|
|
137
|
+
noteCount += getNotes(ober, unter).length;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
oberkategorien: oberkategorien.length,
|
|
143
|
+
unterkategorien: unterCount,
|
|
144
|
+
notes: noteCount
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// =============================================================================
|
|
149
|
+
// Search
|
|
150
|
+
// =============================================================================
|
|
151
|
+
|
|
152
|
+
export function searchNotes(query) {
|
|
153
|
+
const results = [];
|
|
154
|
+
const queryLower = query.toLowerCase();
|
|
155
|
+
|
|
156
|
+
for (const { oberkategorie, unterkategorie, thema } of getAllNotes()) {
|
|
157
|
+
const content = readNote(oberkategorie, unterkategorie, thema);
|
|
158
|
+
if (!content) continue;
|
|
159
|
+
|
|
160
|
+
const contentLower = content.toLowerCase();
|
|
161
|
+
const themaLower = thema.toLowerCase();
|
|
162
|
+
|
|
163
|
+
if (themaLower.includes(queryLower) || contentLower.includes(queryLower)) {
|
|
164
|
+
let preview = '';
|
|
165
|
+
const idx = contentLower.indexOf(queryLower);
|
|
166
|
+
|
|
167
|
+
if (idx !== -1) {
|
|
168
|
+
const start = Math.max(0, idx - 50);
|
|
169
|
+
const end = Math.min(content.length, idx + query.length + 50);
|
|
170
|
+
preview = (start > 0 ? '...' : '') +
|
|
171
|
+
content.substring(start, idx) +
|
|
172
|
+
'<mark>' + content.substring(idx, idx + query.length) + '</mark>' +
|
|
173
|
+
content.substring(idx + query.length, end) +
|
|
174
|
+
(end < content.length ? '...' : '');
|
|
175
|
+
} else {
|
|
176
|
+
preview = content.substring(0, 100) + (content.length > 100 ? '...' : '');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
results.push({
|
|
180
|
+
oberkategorie,
|
|
181
|
+
unterkategorie,
|
|
182
|
+
thema,
|
|
183
|
+
preview: preview.replace(/\n/g, ' ')
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return results;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// =============================================================================
|
|
192
|
+
// Backup & Restore
|
|
193
|
+
// =============================================================================
|
|
194
|
+
|
|
195
|
+
export function createBackup() {
|
|
196
|
+
return {
|
|
197
|
+
version: 1,
|
|
198
|
+
created: new Date().toISOString(),
|
|
199
|
+
notes: getAllNotes().map(note => ({
|
|
200
|
+
...note,
|
|
201
|
+
content: readNote(note.oberkategorie, note.unterkategorie, note.thema) || ''
|
|
202
|
+
}))
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function saveBackupToFile(filePath) {
|
|
207
|
+
const backup = createBackup();
|
|
208
|
+
ensureDir(path.dirname(filePath));
|
|
209
|
+
fs.writeFileSync(filePath, JSON.stringify(backup, null, 2));
|
|
210
|
+
return backup.notes.length;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function loadBackupFromFile(filePath) {
|
|
214
|
+
if (!fs.existsSync(filePath)) return null;
|
|
215
|
+
try {
|
|
216
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function restoreFromBackup(backupData) {
|
|
223
|
+
let restored = 0;
|
|
224
|
+
for (const note of backupData.notes) {
|
|
225
|
+
saveNote(note.oberkategorie, note.unterkategorie, note.thema, note.content);
|
|
226
|
+
restored++;
|
|
227
|
+
}
|
|
228
|
+
return restored;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// =============================================================================
|
|
232
|
+
// Configuration
|
|
233
|
+
// =============================================================================
|
|
234
|
+
|
|
235
|
+
export function loadConfig() {
|
|
236
|
+
if (!fs.existsSync(CONFIG.configFile)) return {};
|
|
237
|
+
try {
|
|
238
|
+
return JSON.parse(fs.readFileSync(CONFIG.configFile, 'utf-8'));
|
|
239
|
+
} catch {
|
|
240
|
+
return {};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function saveConfig(config) {
|
|
245
|
+
ensureDir(CONFIG.baseDir);
|
|
246
|
+
fs.writeFileSync(CONFIG.configFile, JSON.stringify(config, null, 2));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function getSyncPath() {
|
|
250
|
+
return loadConfig().syncPath || null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function setSyncPath(syncPath) {
|
|
254
|
+
const config = loadConfig();
|
|
255
|
+
config.syncPath = syncPath;
|
|
256
|
+
saveConfig(config);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function syncToPath(syncPath) {
|
|
260
|
+
const backupFile = path.join(syncPath, 'wasibase-backup.json');
|
|
261
|
+
return saveBackupToFile(backupFile);
|
|
262
|
+
}
|
package/src/ui/backup.js
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import * as storage from '../storage.js';
|
|
6
|
+
import { CONFIG } from '../config.js';
|
|
7
|
+
|
|
8
|
+
function clear() {
|
|
9
|
+
console.clear();
|
|
10
|
+
console.log('');
|
|
11
|
+
console.log(chalk.bgGreen.white.bold(' WASIBASE BACKUP '));
|
|
12
|
+
console.log('');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatSize(bytes) {
|
|
16
|
+
if (bytes < 1024) return bytes + ' B';
|
|
17
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
18
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function backupMenu() {
|
|
22
|
+
clear();
|
|
23
|
+
|
|
24
|
+
const oberkategorien = storage.getOberkategorien();
|
|
25
|
+
let totalNotes = 0;
|
|
26
|
+
|
|
27
|
+
for (const ober of oberkategorien) {
|
|
28
|
+
const unters = storage.getUnterkategorien(ober);
|
|
29
|
+
for (const unter of unters) {
|
|
30
|
+
totalNotes += storage.getNotes(ober, unter).length;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(chalk.gray(` ${totalNotes} Notes in ${oberkategorien.length} Kategorien\n`));
|
|
35
|
+
|
|
36
|
+
const { action } = await inquirer.prompt([{
|
|
37
|
+
type: 'list',
|
|
38
|
+
name: 'action',
|
|
39
|
+
message: chalk.blue('Aktion'),
|
|
40
|
+
prefix: chalk.blue('◆'),
|
|
41
|
+
choices: [
|
|
42
|
+
{ name: chalk.green(' ↓ ') + chalk.bold('Backup erstellen'), value: 'create' },
|
|
43
|
+
{ name: chalk.yellow(' ↑ ') + chalk.bold('Backup wiederherstellen'), value: 'restore' },
|
|
44
|
+
{ name: chalk.gray(' < Zurueck'), value: 'exit' }
|
|
45
|
+
]
|
|
46
|
+
}]);
|
|
47
|
+
|
|
48
|
+
if (action === 'exit') return;
|
|
49
|
+
|
|
50
|
+
if (action === 'create') {
|
|
51
|
+
return createBackup();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (action === 'restore') {
|
|
55
|
+
return restoreBackup();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function createBackup() {
|
|
60
|
+
clear();
|
|
61
|
+
|
|
62
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
63
|
+
const defaultName = `wasibase-backup-${timestamp}.json`;
|
|
64
|
+
const defaultPath = path.join(CONFIG.backupDir, defaultName);
|
|
65
|
+
|
|
66
|
+
console.log(chalk.gray(' Standard Speicherort:'));
|
|
67
|
+
console.log(chalk.cyan(` ${CONFIG.backupDir}\n`));
|
|
68
|
+
|
|
69
|
+
const { location } = await inquirer.prompt([{
|
|
70
|
+
type: 'list',
|
|
71
|
+
name: 'location',
|
|
72
|
+
message: chalk.blue('Wo speichern?'),
|
|
73
|
+
prefix: chalk.blue('◆'),
|
|
74
|
+
choices: [
|
|
75
|
+
{ name: chalk.cyan(' → ') + chalk.bold('Standard (~/.wasibase/backups/)'), value: 'default' },
|
|
76
|
+
{ name: chalk.cyan(' → ') + chalk.bold('Eigener Pfad angeben'), value: 'custom' },
|
|
77
|
+
{ name: chalk.gray(' < Zurueck'), value: 'back' }
|
|
78
|
+
]
|
|
79
|
+
}]);
|
|
80
|
+
|
|
81
|
+
if (location === 'back') return backupMenu();
|
|
82
|
+
|
|
83
|
+
let filePath = defaultPath;
|
|
84
|
+
|
|
85
|
+
if (location === 'custom') {
|
|
86
|
+
const { customPath } = await inquirer.prompt([{
|
|
87
|
+
type: 'input',
|
|
88
|
+
name: 'customPath',
|
|
89
|
+
message: chalk.green('Pfad (mit Dateiname):'),
|
|
90
|
+
prefix: chalk.green('+'),
|
|
91
|
+
default: path.join(process.env.HOME, 'Downloads', defaultName)
|
|
92
|
+
}]);
|
|
93
|
+
|
|
94
|
+
if (!customPath.trim()) return backupMenu();
|
|
95
|
+
filePath = customPath.trim();
|
|
96
|
+
|
|
97
|
+
if (!filePath.endsWith('.json')) {
|
|
98
|
+
filePath = path.join(filePath, defaultName);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log(chalk.gray(' Erstelle Backup...'));
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const noteCount = storage.saveBackupToFile(filePath);
|
|
107
|
+
const stats = fs.statSync(filePath);
|
|
108
|
+
|
|
109
|
+
console.log('');
|
|
110
|
+
console.log(chalk.green.bold(' ✓ Backup erstellt!'));
|
|
111
|
+
console.log('');
|
|
112
|
+
console.log(chalk.gray(' Datei: ') + chalk.bold(filePath));
|
|
113
|
+
console.log(chalk.gray(' Notes: ') + chalk.bold(noteCount));
|
|
114
|
+
console.log(chalk.gray(' Groesse: ') + chalk.bold(formatSize(stats.size)));
|
|
115
|
+
console.log('');
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.log(chalk.red(`\n ✕ Fehler: ${err.message}\n`));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await inquirer.prompt([{
|
|
121
|
+
type: 'input',
|
|
122
|
+
name: 'continue',
|
|
123
|
+
message: chalk.gray('Enter zum Fortfahren...'),
|
|
124
|
+
prefix: ''
|
|
125
|
+
}]);
|
|
126
|
+
|
|
127
|
+
return backupMenu();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function restoreBackup() {
|
|
131
|
+
clear();
|
|
132
|
+
|
|
133
|
+
storage.ensureDir(CONFIG.backupDir);
|
|
134
|
+
const backupFiles = fs.existsSync(CONFIG.backupDir)
|
|
135
|
+
? fs.readdirSync(CONFIG.backupDir).filter(f => f.endsWith('.json')).sort().reverse()
|
|
136
|
+
: [];
|
|
137
|
+
|
|
138
|
+
if (backupFiles.length === 0) {
|
|
139
|
+
console.log(chalk.yellow(' Keine Backups gefunden.\n'));
|
|
140
|
+
console.log(chalk.gray(' Du kannst einen eigenen Pfad angeben.\n'));
|
|
141
|
+
} else {
|
|
142
|
+
console.log(chalk.gray(` ${backupFiles.length} Backup(s) gefunden\n`));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const choices = [
|
|
146
|
+
...backupFiles.slice(0, 10).map(f => {
|
|
147
|
+
const filePath = path.join(CONFIG.backupDir, f);
|
|
148
|
+
const stats = fs.statSync(filePath);
|
|
149
|
+
return {
|
|
150
|
+
name: chalk.cyan(' → ') + chalk.bold(f) + chalk.gray(` (${formatSize(stats.size)})`),
|
|
151
|
+
value: filePath,
|
|
152
|
+
short: f
|
|
153
|
+
};
|
|
154
|
+
}),
|
|
155
|
+
{ name: chalk.yellow(' ? ') + chalk.yellow('Eigenen Pfad angeben'), value: 'custom' },
|
|
156
|
+
{ name: chalk.gray(' < Zurueck'), value: 'back' }
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
if (backupFiles.length > 0) {
|
|
160
|
+
choices.splice(backupFiles.slice(0, 10).length, 0, new inquirer.Separator(chalk.gray('─'.repeat(40))));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const { selection } = await inquirer.prompt([{
|
|
164
|
+
type: 'list',
|
|
165
|
+
name: 'selection',
|
|
166
|
+
message: chalk.blue('Backup auswaehlen'),
|
|
167
|
+
prefix: chalk.blue('◆'),
|
|
168
|
+
choices,
|
|
169
|
+
pageSize: 15
|
|
170
|
+
}]);
|
|
171
|
+
|
|
172
|
+
if (selection === 'back') return backupMenu();
|
|
173
|
+
|
|
174
|
+
let filePath = selection;
|
|
175
|
+
|
|
176
|
+
if (selection === 'custom') {
|
|
177
|
+
const { customPath } = await inquirer.prompt([{
|
|
178
|
+
type: 'input',
|
|
179
|
+
name: 'customPath',
|
|
180
|
+
message: chalk.green('Pfad zur Backup-Datei:'),
|
|
181
|
+
prefix: chalk.green('?')
|
|
182
|
+
}]);
|
|
183
|
+
|
|
184
|
+
if (!customPath.trim()) return restoreBackup();
|
|
185
|
+
filePath = customPath.trim();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!fs.existsSync(filePath)) {
|
|
189
|
+
console.log(chalk.red(`\n ✕ Datei nicht gefunden: ${filePath}\n`));
|
|
190
|
+
await inquirer.prompt([{
|
|
191
|
+
type: 'input',
|
|
192
|
+
name: 'continue',
|
|
193
|
+
message: chalk.gray('Enter zum Fortfahren...'),
|
|
194
|
+
prefix: ''
|
|
195
|
+
}]);
|
|
196
|
+
return restoreBackup();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const backup = storage.loadBackupFromFile(filePath);
|
|
201
|
+
|
|
202
|
+
if (!backup || !backup.notes) {
|
|
203
|
+
console.log(chalk.red('\n ✕ Ungueltige Backup-Datei\n'));
|
|
204
|
+
await inquirer.prompt([{
|
|
205
|
+
type: 'input',
|
|
206
|
+
name: 'continue',
|
|
207
|
+
message: chalk.gray('Enter zum Fortfahren...'),
|
|
208
|
+
prefix: ''
|
|
209
|
+
}]);
|
|
210
|
+
return restoreBackup();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log('');
|
|
214
|
+
console.log(chalk.yellow(' Backup-Info:'));
|
|
215
|
+
console.log(chalk.gray(' Erstellt: ') + chalk.bold(backup.created || 'Unbekannt'));
|
|
216
|
+
console.log(chalk.gray(' Notes: ') + chalk.bold(backup.notes.length));
|
|
217
|
+
console.log('');
|
|
218
|
+
|
|
219
|
+
const { confirm } = await inquirer.prompt([{
|
|
220
|
+
type: 'confirm',
|
|
221
|
+
name: 'confirm',
|
|
222
|
+
message: chalk.yellow('Wiederherstellen? (Ueberschreibt bestehende Notes)'),
|
|
223
|
+
default: false
|
|
224
|
+
}]);
|
|
225
|
+
|
|
226
|
+
if (!confirm) return backupMenu();
|
|
227
|
+
|
|
228
|
+
console.log('');
|
|
229
|
+
console.log(chalk.gray(' Stelle wieder her...'));
|
|
230
|
+
|
|
231
|
+
const restored = storage.restoreFromBackup(backup);
|
|
232
|
+
|
|
233
|
+
console.log('');
|
|
234
|
+
console.log(chalk.green.bold(' ✓ Wiederhergestellt!'));
|
|
235
|
+
console.log(chalk.gray(` ${restored} Notes wurden importiert.\n`));
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.log(chalk.red(`\n ✕ Fehler: ${err.message}\n`));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await inquirer.prompt([{
|
|
241
|
+
type: 'input',
|
|
242
|
+
name: 'continue',
|
|
243
|
+
message: chalk.gray('Enter zum Fortfahren...'),
|
|
244
|
+
prefix: ''
|
|
245
|
+
}]);
|
|
246
|
+
|
|
247
|
+
return backupMenu();
|
|
248
|
+
}
|