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/src/ui/sync.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import * as storage from '../storage.js';
|
|
6
|
+
|
|
7
|
+
function clear() {
|
|
8
|
+
console.clear();
|
|
9
|
+
console.log('');
|
|
10
|
+
console.log(chalk.bgMagenta.white.bold(' WASIBASE SYNC '));
|
|
11
|
+
console.log('');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatSize(bytes) {
|
|
15
|
+
if (bytes < 1024) return bytes + ' B';
|
|
16
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
17
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function detectCloudServices() {
|
|
21
|
+
const services = [];
|
|
22
|
+
const cloudStorageDir = path.join(process.env.HOME, 'Library', 'CloudStorage');
|
|
23
|
+
|
|
24
|
+
if (fs.existsSync(cloudStorageDir)) {
|
|
25
|
+
const dirs = fs.readdirSync(cloudStorageDir, { withFileTypes: true });
|
|
26
|
+
|
|
27
|
+
for (const dir of dirs) {
|
|
28
|
+
if (!dir.isDirectory()) continue;
|
|
29
|
+
|
|
30
|
+
const fullPath = path.join(cloudStorageDir, dir.name);
|
|
31
|
+
|
|
32
|
+
if (dir.name.startsWith('ProtonDrive-')) {
|
|
33
|
+
services.push({
|
|
34
|
+
name: 'Proton Drive',
|
|
35
|
+
path: fullPath,
|
|
36
|
+
icon: '🔒',
|
|
37
|
+
detected: dir.name
|
|
38
|
+
});
|
|
39
|
+
} else if (dir.name.startsWith('Dropbox')) {
|
|
40
|
+
services.push({
|
|
41
|
+
name: 'Dropbox',
|
|
42
|
+
path: fullPath,
|
|
43
|
+
icon: '📦',
|
|
44
|
+
detected: dir.name
|
|
45
|
+
});
|
|
46
|
+
} else if (dir.name.startsWith('GoogleDrive')) {
|
|
47
|
+
services.push({
|
|
48
|
+
name: 'Google Drive',
|
|
49
|
+
path: fullPath,
|
|
50
|
+
icon: '🔷',
|
|
51
|
+
detected: dir.name
|
|
52
|
+
});
|
|
53
|
+
} else if (dir.name.startsWith('OneDrive')) {
|
|
54
|
+
services.push({
|
|
55
|
+
name: 'OneDrive',
|
|
56
|
+
path: fullPath,
|
|
57
|
+
icon: '☁️',
|
|
58
|
+
detected: dir.name
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check iCloud
|
|
65
|
+
const iCloudPath = path.join(process.env.HOME, 'Library', 'Mobile Documents', 'com~apple~CloudDocs');
|
|
66
|
+
if (fs.existsSync(iCloudPath)) {
|
|
67
|
+
services.push({
|
|
68
|
+
name: 'iCloud Drive',
|
|
69
|
+
path: iCloudPath,
|
|
70
|
+
icon: '☁️',
|
|
71
|
+
detected: 'iCloud'
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return services;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function syncMenu() {
|
|
79
|
+
clear();
|
|
80
|
+
|
|
81
|
+
const currentPath = storage.getSyncPath();
|
|
82
|
+
|
|
83
|
+
if (currentPath) {
|
|
84
|
+
console.log(chalk.gray(' Sync-Ordner konfiguriert:'));
|
|
85
|
+
console.log(chalk.cyan(` ${currentPath}`));
|
|
86
|
+
|
|
87
|
+
if (fs.existsSync(currentPath)) {
|
|
88
|
+
console.log(chalk.green(' ✓ Ordner existiert'));
|
|
89
|
+
const backupFile = path.join(currentPath, 'wasibase-backup.json');
|
|
90
|
+
if (fs.existsSync(backupFile)) {
|
|
91
|
+
const stats = fs.statSync(backupFile);
|
|
92
|
+
const modified = new Date(stats.mtime).toLocaleString('de-DE');
|
|
93
|
+
console.log(chalk.gray(` Letztes Backup: ${modified} (${formatSize(stats.size)})`));
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
console.log(chalk.yellow(' ! Ordner nicht gefunden'));
|
|
97
|
+
}
|
|
98
|
+
console.log('');
|
|
99
|
+
} else {
|
|
100
|
+
console.log(chalk.yellow(' Noch kein Sync-Ordner konfiguriert.\n'));
|
|
101
|
+
console.log(chalk.gray(' Tipp: Verwende deinen Proton Drive / Dropbox / iCloud Ordner\n'));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const choices = [];
|
|
105
|
+
|
|
106
|
+
if (currentPath && fs.existsSync(currentPath)) {
|
|
107
|
+
choices.push({ name: chalk.green(' ↑ ') + chalk.bold('Jetzt synchronisieren'), value: 'sync' });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
choices.push(
|
|
111
|
+
{ name: chalk.cyan(' ⚙ ') + chalk.bold('Sync-Ordner ' + (currentPath ? 'aendern' : 'einrichten')), value: 'configure' }
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (currentPath) {
|
|
115
|
+
choices.push({ name: chalk.red(' ✕ ') + chalk.gray('Sync deaktivieren'), value: 'disable' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
choices.push({ name: chalk.gray(' < Zurueck'), value: 'exit' });
|
|
119
|
+
|
|
120
|
+
const { action } = await inquirer.prompt([{
|
|
121
|
+
type: 'list',
|
|
122
|
+
name: 'action',
|
|
123
|
+
message: chalk.blue('Aktion'),
|
|
124
|
+
prefix: chalk.blue('◆'),
|
|
125
|
+
choices
|
|
126
|
+
}]);
|
|
127
|
+
|
|
128
|
+
if (action === 'exit') return;
|
|
129
|
+
|
|
130
|
+
if (action === 'sync') {
|
|
131
|
+
return doSync(currentPath);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (action === 'configure') {
|
|
135
|
+
return configureSyncPath();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (action === 'disable') {
|
|
139
|
+
return disableSync();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function configureSyncPath() {
|
|
144
|
+
clear();
|
|
145
|
+
|
|
146
|
+
const cloudServices = detectCloudServices();
|
|
147
|
+
|
|
148
|
+
if (cloudServices.length > 0) {
|
|
149
|
+
console.log(chalk.green.bold(' Cloud-Dienste erkannt:\n'));
|
|
150
|
+
|
|
151
|
+
const choices = cloudServices.map(service => {
|
|
152
|
+
const wasibasePath = path.join(service.path, 'Wasibase');
|
|
153
|
+
const exists = fs.existsSync(wasibasePath);
|
|
154
|
+
return {
|
|
155
|
+
name: chalk.cyan(` ${service.icon} `) + chalk.bold(service.name) +
|
|
156
|
+
chalk.gray(exists ? ' (Wasibase Ordner existiert)' : ' (Ordner wird erstellt)'),
|
|
157
|
+
value: { service, wasibasePath },
|
|
158
|
+
short: service.name
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
choices.push(
|
|
163
|
+
new inquirer.Separator(chalk.gray('─'.repeat(40))),
|
|
164
|
+
{ name: chalk.yellow(' ? ') + chalk.yellow('Eigenen Pfad angeben'), value: 'custom' },
|
|
165
|
+
{ name: chalk.gray(' < Zurueck'), value: 'back' }
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const { selection } = await inquirer.prompt([{
|
|
169
|
+
type: 'list',
|
|
170
|
+
name: 'selection',
|
|
171
|
+
message: chalk.blue('Wo sollen deine Notes gesichert werden?'),
|
|
172
|
+
prefix: chalk.blue('◆'),
|
|
173
|
+
choices,
|
|
174
|
+
pageSize: 12
|
|
175
|
+
}]);
|
|
176
|
+
|
|
177
|
+
if (selection === 'back') return syncMenu();
|
|
178
|
+
|
|
179
|
+
if (selection !== 'custom') {
|
|
180
|
+
const { service, wasibasePath } = selection;
|
|
181
|
+
|
|
182
|
+
if (!fs.existsSync(wasibasePath)) {
|
|
183
|
+
console.log('');
|
|
184
|
+
const { create } = await inquirer.prompt([{
|
|
185
|
+
type: 'confirm',
|
|
186
|
+
name: 'create',
|
|
187
|
+
message: chalk.green(`Ordner "Wasibase" in ${service.name} erstellen?`),
|
|
188
|
+
default: true
|
|
189
|
+
}]);
|
|
190
|
+
|
|
191
|
+
if (!create) return configureSyncPath();
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
fs.mkdirSync(wasibasePath, { recursive: true });
|
|
195
|
+
console.log(chalk.green(`\n ✓ Ordner erstellt!\n`));
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.log(chalk.red(`\n ✕ Fehler: ${err.message}\n`));
|
|
198
|
+
await inquirer.prompt([{
|
|
199
|
+
type: 'input',
|
|
200
|
+
name: 'continue',
|
|
201
|
+
message: chalk.gray('Enter zum Fortfahren...'),
|
|
202
|
+
prefix: ''
|
|
203
|
+
}]);
|
|
204
|
+
return configureSyncPath();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
storage.setSyncPath(wasibasePath);
|
|
209
|
+
console.log(chalk.green(`\n ✓ ${service.name} konfiguriert!\n`));
|
|
210
|
+
console.log(chalk.gray(` Pfad: ${wasibasePath}\n`));
|
|
211
|
+
|
|
212
|
+
const { syncNow } = await inquirer.prompt([{
|
|
213
|
+
type: 'confirm',
|
|
214
|
+
name: 'syncNow',
|
|
215
|
+
message: chalk.blue('Jetzt synchronisieren?'),
|
|
216
|
+
default: true
|
|
217
|
+
}]);
|
|
218
|
+
|
|
219
|
+
if (syncNow) {
|
|
220
|
+
return doSync(wasibasePath);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return syncMenu();
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
console.log(chalk.yellow(' Keine Cloud-Dienste erkannt.\n'));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
console.log(chalk.gray(' Gib den Pfad zu deinem Cloud-Ordner an.\n'));
|
|
230
|
+
|
|
231
|
+
const currentPath = storage.getSyncPath();
|
|
232
|
+
|
|
233
|
+
const { syncPath } = await inquirer.prompt([{
|
|
234
|
+
type: 'input',
|
|
235
|
+
name: 'syncPath',
|
|
236
|
+
message: chalk.green('Sync-Ordner:'),
|
|
237
|
+
prefix: chalk.green('◆'),
|
|
238
|
+
default: currentPath || ''
|
|
239
|
+
}]);
|
|
240
|
+
|
|
241
|
+
if (!syncPath.trim()) return syncMenu();
|
|
242
|
+
|
|
243
|
+
let resolvedPath = syncPath.trim();
|
|
244
|
+
if (resolvedPath.startsWith('~')) {
|
|
245
|
+
resolvedPath = path.join(process.env.HOME, resolvedPath.slice(1));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
249
|
+
console.log('');
|
|
250
|
+
const { create } = await inquirer.prompt([{
|
|
251
|
+
type: 'confirm',
|
|
252
|
+
name: 'create',
|
|
253
|
+
message: chalk.yellow('Ordner existiert nicht. Erstellen?'),
|
|
254
|
+
default: true
|
|
255
|
+
}]);
|
|
256
|
+
|
|
257
|
+
if (create) {
|
|
258
|
+
try {
|
|
259
|
+
fs.mkdirSync(resolvedPath, { recursive: true });
|
|
260
|
+
console.log(chalk.green(`\n ✓ Ordner erstellt: ${resolvedPath}\n`));
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.log(chalk.red(`\n ✕ Konnte Ordner nicht erstellen: ${err.message}\n`));
|
|
263
|
+
await inquirer.prompt([{
|
|
264
|
+
type: 'input',
|
|
265
|
+
name: 'continue',
|
|
266
|
+
message: chalk.gray('Enter zum Fortfahren...'),
|
|
267
|
+
prefix: ''
|
|
268
|
+
}]);
|
|
269
|
+
return configureSyncPath();
|
|
270
|
+
}
|
|
271
|
+
} else {
|
|
272
|
+
return configureSyncPath();
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
storage.setSyncPath(resolvedPath);
|
|
277
|
+
console.log(chalk.green('\n ✓ Sync-Ordner gespeichert!\n'));
|
|
278
|
+
|
|
279
|
+
const { syncNow } = await inquirer.prompt([{
|
|
280
|
+
type: 'confirm',
|
|
281
|
+
name: 'syncNow',
|
|
282
|
+
message: chalk.blue('Jetzt synchronisieren?'),
|
|
283
|
+
default: true
|
|
284
|
+
}]);
|
|
285
|
+
|
|
286
|
+
if (syncNow) {
|
|
287
|
+
return doSync(resolvedPath);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return syncMenu();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function doSync(syncPath) {
|
|
294
|
+
clear();
|
|
295
|
+
|
|
296
|
+
console.log(chalk.gray(' Synchronisiere...\n'));
|
|
297
|
+
console.log(chalk.gray(' Ziel: ') + chalk.cyan(syncPath));
|
|
298
|
+
console.log('');
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const noteCount = storage.syncToPath(syncPath);
|
|
302
|
+
const backupFile = path.join(syncPath, 'wasibase-backup.json');
|
|
303
|
+
const stats = fs.statSync(backupFile);
|
|
304
|
+
|
|
305
|
+
console.log(chalk.green.bold(' ✓ Sync abgeschlossen!'));
|
|
306
|
+
console.log('');
|
|
307
|
+
console.log(chalk.gray(' Notes: ') + chalk.bold(noteCount));
|
|
308
|
+
console.log(chalk.gray(' Groesse: ') + chalk.bold(formatSize(stats.size)));
|
|
309
|
+
console.log(chalk.gray(' Datei: ') + chalk.bold('wasibase-backup.json'));
|
|
310
|
+
console.log('');
|
|
311
|
+
} catch (err) {
|
|
312
|
+
console.log(chalk.red(` ✕ Fehler: ${err.message}\n`));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await inquirer.prompt([{
|
|
316
|
+
type: 'input',
|
|
317
|
+
name: 'continue',
|
|
318
|
+
message: chalk.gray('Enter zum Fortfahren...'),
|
|
319
|
+
prefix: ''
|
|
320
|
+
}]);
|
|
321
|
+
|
|
322
|
+
return syncMenu();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function disableSync() {
|
|
326
|
+
clear();
|
|
327
|
+
|
|
328
|
+
const { confirm } = await inquirer.prompt([{
|
|
329
|
+
type: 'confirm',
|
|
330
|
+
name: 'confirm',
|
|
331
|
+
message: chalk.red('Sync wirklich deaktivieren?'),
|
|
332
|
+
default: false
|
|
333
|
+
}]);
|
|
334
|
+
|
|
335
|
+
if (confirm) {
|
|
336
|
+
storage.setSyncPath(null);
|
|
337
|
+
console.log(chalk.yellow('\n ○ Sync deaktiviert.\n'));
|
|
338
|
+
|
|
339
|
+
await inquirer.prompt([{
|
|
340
|
+
type: 'input',
|
|
341
|
+
name: 'continue',
|
|
342
|
+
message: chalk.gray('Enter zum Fortfahren...'),
|
|
343
|
+
prefix: ''
|
|
344
|
+
}]);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return syncMenu();
|
|
348
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for Wasibase
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import net from 'net';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Find an available port starting from the given port
|
|
9
|
+
*/
|
|
10
|
+
export async function findAvailablePort(startPort = 3333) {
|
|
11
|
+
const checkPort = (port) => {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
const server = net.createServer();
|
|
14
|
+
server.listen(port, () => {
|
|
15
|
+
server.close(() => resolve(port));
|
|
16
|
+
});
|
|
17
|
+
server.on('error', () => resolve(null));
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
for (let port = startPort; port < startPort + 100; port++) {
|
|
22
|
+
const available = await checkPort(port);
|
|
23
|
+
if (available) return available;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return startPort + Math.floor(Math.random() * 100);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Format bytes to human readable string
|
|
31
|
+
*/
|
|
32
|
+
export function formatBytes(bytes) {
|
|
33
|
+
if (bytes < 1024) return bytes + ' B';
|
|
34
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
35
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Format date to German locale string
|
|
40
|
+
*/
|
|
41
|
+
export function formatDate(date) {
|
|
42
|
+
return new Date(date).toLocaleDateString('de-DE', {
|
|
43
|
+
day: '2-digit',
|
|
44
|
+
month: '2-digit',
|
|
45
|
+
year: 'numeric',
|
|
46
|
+
hour: '2-digit',
|
|
47
|
+
minute: '2-digit'
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Escape HTML special characters
|
|
53
|
+
*/
|
|
54
|
+
export function escapeHtml(text) {
|
|
55
|
+
const map = {
|
|
56
|
+
'&': '&',
|
|
57
|
+
'<': '<',
|
|
58
|
+
'>': '>',
|
|
59
|
+
'"': '"',
|
|
60
|
+
"'": '''
|
|
61
|
+
};
|
|
62
|
+
return text.replace(/[&<>"']/g, m => map[m]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extract backlinks from markdown content
|
|
67
|
+
*/
|
|
68
|
+
export function extractBacklinks(content) {
|
|
69
|
+
const regex = /\[\[([^\]]+)\]\]/g;
|
|
70
|
+
const links = [];
|
|
71
|
+
let match;
|
|
72
|
+
|
|
73
|
+
while ((match = regex.exec(content)) !== null) {
|
|
74
|
+
links.push(match[1].trim());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return [...new Set(links)];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Strip YAML frontmatter from markdown
|
|
82
|
+
*/
|
|
83
|
+
export function stripFrontmatter(content) {
|
|
84
|
+
return content.replace(/^---[\s\S]*?---\n?/, '').trim();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get text preview from markdown content
|
|
89
|
+
*/
|
|
90
|
+
export function getPreview(content, maxLength = 80) {
|
|
91
|
+
if (!content) return '';
|
|
92
|
+
|
|
93
|
+
const cleaned = stripFrontmatter(content)
|
|
94
|
+
.replace(/^#+\s*/gm, '')
|
|
95
|
+
.replace(/\*\*/g, '')
|
|
96
|
+
.replace(/\*/g, '')
|
|
97
|
+
.replace(/\n/g, ' ')
|
|
98
|
+
.trim();
|
|
99
|
+
|
|
100
|
+
if (cleaned.length > maxLength) {
|
|
101
|
+
return cleaned.substring(0, maxLength) + '...';
|
|
102
|
+
}
|
|
103
|
+
return cleaned;
|
|
104
|
+
}
|