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/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
+ '&': '&amp;',
57
+ '<': '&lt;',
58
+ '>': '&gt;',
59
+ '"': '&quot;',
60
+ "'": '&#039;'
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
+ }