open-notepad 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.
Files changed (2) hide show
  1. package/bin/note.js +565 -0
  2. package/package.json +20 -0
package/bin/note.js ADDED
@@ -0,0 +1,565 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs/promises';
4
+ import { existsSync } from 'fs';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ import { spawn } from 'child_process';
8
+ import readline from 'readline';
9
+ import { Writable } from 'stream';
10
+
11
+ const CONFIG_PATH = path.join(os.homedir(), '.notepad-cli.json');
12
+
13
+ // Color helpers
14
+ const colors = {
15
+ reset: '\x1b[0m',
16
+ bold: '\x1b[1m',
17
+ dim: '\x1b[2m',
18
+ blue: '\x1b[94m',
19
+ green: '\x1b[92m',
20
+ yellow: '\x1b[93m',
21
+ cyan: '\x1b[96m',
22
+ red: '\x1b[91m',
23
+ bgBlue: '\x1b[44m',
24
+ white: '\x1b[97m'
25
+ };
26
+
27
+ // Console formatting utils
28
+ function log(msg) { console.log(msg); }
29
+ function error(msg) { console.error(`${colors.bold}${colors.red}Error:${colors.reset} ${msg}`); }
30
+ function success(msg) { log(`${colors.bold}${colors.green}Success:${colors.reset} ${msg}`); }
31
+ function warning(msg) { log(`${colors.bold}${colors.yellow}Warning:${colors.reset} ${msg}`); }
32
+ function info(msg) { log(`${colors.cyan}i${colors.reset} ${msg}`); }
33
+
34
+ function clearScreen() {
35
+ process.stdout.write('\x1B[2J\x1B[3J\x1B[H');
36
+ }
37
+
38
+ // Readline prompt helpers
39
+ function ask(query, defaultValue = '') {
40
+ const rl = readline.createInterface({
41
+ input: process.stdin,
42
+ output: process.stdout
43
+ });
44
+ const suffix = defaultValue ? ` (${colors.dim}${defaultValue}${colors.reset})` : '';
45
+ return new Promise((resolve) => {
46
+ rl.question(`${colors.bold}${query}${suffix}: ${colors.reset}`, (answer) => {
47
+ rl.close();
48
+ resolve(answer.trim() || defaultValue);
49
+ });
50
+ });
51
+ }
52
+
53
+ function askPassword(query) {
54
+ return new Promise((resolve) => {
55
+ const mutableStdout = new Writable({
56
+ write: function (chunk, encoding, callback) {
57
+ if (!this.muted) {
58
+ process.stdout.write(chunk, encoding);
59
+ }
60
+ callback();
61
+ }
62
+ });
63
+ mutableStdout.muted = false;
64
+ const rl = readline.createInterface({
65
+ input: process.stdin,
66
+ output: mutableStdout,
67
+ terminal: true
68
+ });
69
+ process.stdout.write(`${colors.bold}${query}: ${colors.reset}`);
70
+ mutableStdout.muted = true;
71
+ rl.question('', (value) => {
72
+ rl.close();
73
+ process.stdout.write('\n');
74
+ resolve(value);
75
+ });
76
+ });
77
+ }
78
+
79
+ // Config management
80
+ async function loadConfig() {
81
+ if (existsSync(CONFIG_PATH)) {
82
+ try {
83
+ const data = await fs.readFile(CONFIG_PATH, 'utf-8');
84
+ return JSON.parse(data);
85
+ } catch (e) {
86
+ error(`Failed to parse configuration: ${e.message}`);
87
+ }
88
+ }
89
+ return {
90
+ apiUrl: 'https://api-yuulabs-v1.notepad.web.id',
91
+ roomId: '',
92
+ apiKey: ''
93
+ };
94
+ }
95
+
96
+ async function saveConfig(config) {
97
+ try {
98
+ await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
99
+ success(`Configuration saved to ${CONFIG_PATH}`);
100
+ } catch (e) {
101
+ error(`Failed to save configuration: ${e.message}`);
102
+ }
103
+ }
104
+
105
+ // HTTP request wrappers
106
+ async function apiFetch(config, endpoint, options = {}) {
107
+ const url = `${config.apiUrl.replace(/\/$/, '')}${endpoint}`;
108
+
109
+ const headers = {
110
+ 'Content-Type': 'application/json',
111
+ ...options.headers
112
+ };
113
+
114
+ if (config.apiKey && !options.skipApiKey) {
115
+ headers['x-api-key'] = config.apiKey;
116
+ headers['Authorization'] = `Bearer ${config.apiKey}`;
117
+ }
118
+
119
+ try {
120
+ const res = await fetch(url, {
121
+ ...options,
122
+ headers
123
+ });
124
+
125
+ return res;
126
+ } catch (e) {
127
+ throw new Error(`Connection failed. Check your API URL (${config.apiUrl}). Error: ${e.message}`);
128
+ }
129
+ }
130
+
131
+ // Commands
132
+ async function handleLogin() {
133
+ clearScreen();
134
+ log(`${colors.bgBlue}${colors.white}${colors.bold} Configure Notepad CLI ${colors.reset}\n`);
135
+
136
+ const current = await loadConfig();
137
+
138
+ const apiUrl = await ask('Enter Server API URL', current.apiUrl);
139
+ const roomId = await ask('Enter Subdomain/Room ID (e.g. "saya")', current.roomId);
140
+ const apiKey = await ask('Enter Room API Token/Key', current.apiKey);
141
+
142
+ await saveConfig({ apiUrl, roomId, apiKey });
143
+ }
144
+
145
+ async function handleList() {
146
+ const config = await loadConfig();
147
+ if (!config.roomId) {
148
+ warning('Not configured. Please run `note login` first.');
149
+ return;
150
+ }
151
+
152
+ info(`Fetching notes for room ${colors.bold}${config.roomId}${colors.reset}...`);
153
+ try {
154
+ const res = await apiFetch(config, `/api/rooms/${config.roomId}/notes`);
155
+ if (res.status === 403) {
156
+ error('Access Denied. Check your API key.');
157
+ return;
158
+ }
159
+ if (!res.ok) {
160
+ error(`API returned error code ${res.status}`);
161
+ return;
162
+ }
163
+
164
+ const notes = await res.json();
165
+ clearScreen();
166
+ log(`${colors.blue}${colors.bold}Notes in /${config.roomId}:${colors.reset}`);
167
+ log(`${colors.dim}------------------------------------------------------------${colors.reset}`);
168
+
169
+ if (notes.length === 0) {
170
+ log('No notes found in this room room.');
171
+ } else {
172
+ for (const note of notes) {
173
+ const vis = note.is_public ? `${colors.green}Public${colors.reset}` : `${colors.red}Private${colors.reset}`;
174
+ const prot = note.has_password ? `${colors.yellow}Locked${colors.reset}` : 'Open';
175
+ const date = new Date(note.updated_at).toLocaleString();
176
+ log(` 📄 ${colors.bold}${colors.cyan}${note.code.padEnd(20)}${colors.reset} [${vis}] [${prot.padEnd(6)}] ${colors.dim}${date}${colors.reset}`);
177
+ }
178
+ }
179
+ log(`${colors.dim}------------------------------------------------------------${colors.reset}`);
180
+ } catch (e) {
181
+ error(e.message);
182
+ }
183
+ }
184
+
185
+ async function handleView(codeArg) {
186
+ const config = await loadConfig();
187
+ if (!config.roomId) {
188
+ warning('Not configured. Please run `note login` first.');
189
+ return;
190
+ }
191
+
192
+ const code = codeArg || await ask('Enter note code to view');
193
+ if (!code) return;
194
+
195
+ info(`Loading note ${colors.bold}/${config.roomId}/${code}${colors.reset}...`);
196
+
197
+ try {
198
+ // Attempt standard fetch with API key
199
+ let res = await apiFetch(config, `/api/notes/room/${config.roomId}/${code}`);
200
+
201
+ if (res.status === 404) {
202
+ error(`Note "${code}" not found.`);
203
+ return;
204
+ }
205
+
206
+ let noteData = await res.json();
207
+
208
+ // Check if the note needs a password
209
+ if (noteData.has_password && (!noteData.content || res.status === 403)) {
210
+ const pwd = await askPassword('Enter note password');
211
+
212
+ // Note: We bypass API Key header if providing note password for private notes
213
+ // because backend rejects (is_api = true && private_note = true) outright.
214
+ res = await apiFetch(config, `/api/notes/room/${config.roomId}/${code}`, {
215
+ headers: { 'x-note-password': pwd },
216
+ skipApiKey: true
217
+ });
218
+
219
+ if (!res.ok) {
220
+ error('Invalid note password or unauthorized access.');
221
+ return;
222
+ }
223
+ noteData = await res.json();
224
+ }
225
+
226
+ if (res.status === 403) {
227
+ error('Access Denied: Private note requires correct password.');
228
+ return;
229
+ }
230
+
231
+ clearScreen();
232
+ log(`${colors.bold}${colors.blue}Note: /${config.roomId}/${code}${colors.reset}`);
233
+ log(`${colors.dim}------------------------------------------------------------${colors.reset}`);
234
+ log(noteData.content || `${colors.dim}(Empty note)${colors.reset}`);
235
+ log(`${colors.dim}------------------------------------------------------------${colors.reset}`);
236
+ } catch (e) {
237
+ error(e.message);
238
+ }
239
+ }
240
+
241
+ async function handleCreate(codeArg) {
242
+ const config = await loadConfig();
243
+ if (!config.roomId) {
244
+ warning('Not configured. Please run `note login` first.');
245
+ return;
246
+ }
247
+
248
+ const code = codeArg || await ask('Enter new note code');
249
+ if (!code) return;
250
+
251
+ const isPublicStr = await ask('Make note public? (y/n)', 'y');
252
+ const isPublic = isPublicStr.toLowerCase().startsWith('y');
253
+
254
+ let password = '';
255
+ if (!isPublic) {
256
+ password = await askPassword('Set password lock (required for private notes)');
257
+ if (!password) {
258
+ error('Password is required for private notes.');
259
+ return;
260
+ }
261
+ } else {
262
+ const setPass = await ask('Set optional password lock? (y/n)', 'n');
263
+ if (setPass.toLowerCase().startsWith('y')) {
264
+ password = await askPassword('Enter note password');
265
+ }
266
+ }
267
+
268
+ info(`Creating note ${colors.bold}/${config.roomId}/${code}${colors.reset}...`);
269
+
270
+ try {
271
+ // 1. Create/Save note with empty content (must send API key)
272
+ let saveRes = await apiFetch(config, `/api/notes/room/${config.roomId}/${code}`, {
273
+ method: 'POST',
274
+ body: JSON.stringify({ content: '' })
275
+ });
276
+
277
+ if (!saveRes.ok) {
278
+ error(`Failed to create note (status: ${saveRes.status})`);
279
+ return;
280
+ }
281
+
282
+ // 2. Set visibility and password (calls settings endpoint)
283
+ const settingsRes = await apiFetch(config, `/api/notes/room/${config.roomId}/${code}/settings`, {
284
+ method: 'POST',
285
+ body: JSON.stringify({
286
+ is_public: isPublic,
287
+ password: password || ""
288
+ })
289
+ });
290
+
291
+ if (!settingsRes.ok) {
292
+ error(`Failed to set note settings (status: ${settingsRes.status})`);
293
+ return;
294
+ }
295
+
296
+ success(`Note "${code}" created successfully!`);
297
+
298
+ const editNow = await ask('Open editor to write content now? (y/n)', 'y');
299
+ if (editNow.toLowerCase().startsWith('y')) {
300
+ await handleEdit(code);
301
+ }
302
+ } catch (e) {
303
+ error(e.message);
304
+ }
305
+ }
306
+
307
+ async function handleEdit(codeArg) {
308
+ const config = await loadConfig();
309
+ if (!config.roomId) {
310
+ warning('Not configured. Please run `note login` first.');
311
+ return;
312
+ }
313
+
314
+ const code = codeArg || await ask('Enter note code to edit');
315
+ if (!code) return;
316
+
317
+ info(`Fetching current content of /${config.roomId}/${code}...`);
318
+
319
+ try {
320
+ let notePassword = '';
321
+ let res = await apiFetch(config, `/api/notes/room/${config.roomId}/${code}`);
322
+
323
+ // If it doesn't exist, offer to create it
324
+ if (res.status === 404) {
325
+ const create = await ask('Note does not exist. Create it? (y/n)', 'y');
326
+ if (create.toLowerCase().startsWith('y')) {
327
+ await handleCreate(code);
328
+ }
329
+ return;
330
+ }
331
+
332
+ let noteData = await res.json();
333
+
334
+ // If locked, request password
335
+ if (noteData.has_password && (!noteData.content || res.status === 403)) {
336
+ notePassword = await askPassword('Enter note password to edit');
337
+
338
+ res = await apiFetch(config, `/api/notes/room/${config.roomId}/${code}`, {
339
+ headers: { 'x-note-password': notePassword },
340
+ skipApiKey: true
341
+ });
342
+
343
+ if (!res.ok) {
344
+ error('Invalid note password or unauthorized access.');
345
+ return;
346
+ }
347
+ noteData = await res.json();
348
+ }
349
+
350
+ if (res.status === 403) {
351
+ error('Access Denied: Private note requires correct password.');
352
+ return;
353
+ }
354
+
355
+ const initialContent = noteData.content || '';
356
+
357
+ // Create temp file
358
+ const tempDir = os.tmpdir();
359
+ const tempFilePath = path.join(tempDir, `note_${config.roomId}_${code}.txt`);
360
+ await fs.writeFile(tempFilePath, initialContent, 'utf-8');
361
+
362
+ // Open editor
363
+ const editor = process.env.EDITOR || (process.platform === 'win32' ? 'notepad' : 'nano');
364
+ info(`Opening editor: ${colors.bold}${editor}${colors.reset} with note content...`);
365
+
366
+ const child = spawn(editor, [tempFilePath], { stdio: 'inherit', shell: true });
367
+
368
+ child.on('exit', async (exitCode) => {
369
+ try {
370
+ const updatedContent = await fs.readFile(tempFilePath, 'utf-8');
371
+
372
+ // Cleanup temp file
373
+ await fs.unlink(tempFilePath).catch(() => { });
374
+
375
+ if (updatedContent === initialContent) {
376
+ info('No changes made. Note not saved.');
377
+ return;
378
+ }
379
+
380
+ info('Saving changes back to server...');
381
+
382
+ // Save note
383
+ const saveRes = await apiFetch(config, `/api/notes/room/${config.roomId}/${code}`, {
384
+ method: 'POST',
385
+ headers: notePassword ? { 'x-note-password': notePassword } : {},
386
+ skipApiKey: !!notePassword, // Skip API key header if note password is used
387
+ body: JSON.stringify({ content: updatedContent })
388
+ });
389
+
390
+ if (saveRes.ok) {
391
+ success(`Saved /${config.roomId}/${code} successfully!`);
392
+ } else {
393
+ error(`Failed to save changes. Server returned status: ${saveRes.status}`);
394
+ }
395
+ } catch (e) {
396
+ error(`Failed to save: ${e.message}`);
397
+ }
398
+ });
399
+ } catch (e) {
400
+ error(e.message);
401
+ }
402
+ }
403
+
404
+ async function handleDelete(codeArg) {
405
+ const config = await loadConfig();
406
+ if (!config.roomId) {
407
+ warning('Not configured. Please run `note login` first.');
408
+ return;
409
+ }
410
+
411
+ const code = codeArg || await ask('Enter note code to delete');
412
+ if (!code) return;
413
+
414
+ const confirm = await ask(`Are you sure you want to delete /${config.roomId}/${code}? (y/n)`, 'n');
415
+ if (!confirm.toLowerCase().startsWith('y')) {
416
+ info('Delete canceled.');
417
+ return;
418
+ }
419
+
420
+ info(`Deleting note /${config.roomId}/${code}...`);
421
+ try {
422
+ const res = await apiFetch(config, `/api/rooms/${config.roomId}/notes/${code}`, {
423
+ method: 'DELETE'
424
+ });
425
+
426
+ if (res.ok) {
427
+ success(`Note "${code}" deleted successfully.`);
428
+ } else {
429
+ error(`Failed to delete note. Status: ${res.status}`);
430
+ }
431
+ } catch (e) {
432
+ error(e.message);
433
+ }
434
+ }
435
+
436
+ function handleHelp() {
437
+ log(`
438
+ ${colors.bold}${colors.cyan}Notepad CLI Help${colors.reset}
439
+ Command: ${colors.bold}note${colors.reset}
440
+
441
+ ${colors.bold}Commands:${colors.reset}
442
+ ${colors.bold}note login${colors.reset} Interactive setup config (Server URL, Room ID, API Token)
443
+ ${colors.bold}note list${colors.reset} List all notes in your room
444
+ ${colors.bold}note view [code]${colors.reset} View content of a note (prompts for password if locked)
445
+ ${colors.bold}note create [code]${colors.reset} Create a new note with custom visibility and password
446
+ ${colors.bold}note edit [code]${colors.reset} Edit a note interactively in your favorite editor
447
+ ${colors.bold}note delete [code]${colors.reset} Delete a note
448
+ ${colors.bold}note config${colors.reset} Show current configurations
449
+ ${colors.bold}note help${colors.reset} Show this help screen
450
+
451
+ If run without commands, opens a fully interactive menu.
452
+ `);
453
+ }
454
+
455
+ async function handleConfig() {
456
+ const config = await loadConfig();
457
+ log(`
458
+ ${colors.bold}${colors.blue}Current Notepad Config:${colors.reset}
459
+ ${colors.bold}Server API URL:${colors.reset} ${config.apiUrl}
460
+ ${colors.bold}Room ID: ${colors.reset} ${config.roomId || colors.dim + '(Not set)' + colors.reset}
461
+ ${colors.bold}API Token: ${colors.reset} ${config.apiKey ? config.apiKey.slice(0, 10) + '...' : colors.dim + '(Not set)' + colors.reset}
462
+ `);
463
+ }
464
+
465
+ // Interactive Dashboard Menu (If no arguments)
466
+ async function startMainMenu() {
467
+ while (true) {
468
+ clearScreen();
469
+ log(`${colors.bgBlue}${colors.white}${colors.bold} Notepad CLI Menu ${colors.reset}`);
470
+ const config = await loadConfig();
471
+ log(`${colors.dim}Namespace: ${colors.reset}${config.roomId ? colors.green + config.roomId : colors.red + '(Not logged in)'}${colors.reset}\n`);
472
+
473
+ log(`${colors.cyan}1. 📁 List all notes${colors.reset}`);
474
+ log(`${colors.cyan}2. 📖 View a note${colors.reset}`);
475
+ log(`${colors.cyan}3. ✍️ Create a new note${colors.reset}`);
476
+ log(`${colors.cyan}4. ✏️ Edit an existing note${colors.reset}`);
477
+ log(`${colors.cyan}5. ❌ Delete a note${colors.reset}`);
478
+ log(`${colors.cyan}6. 🔑 Configure / Login${colors.reset}`);
479
+ log(`${colors.cyan}7. 🚪 Exit${colors.reset}\n`);
480
+
481
+ const choice = await ask('Select option (1-7)');
482
+
483
+ switch (choice) {
484
+ case '1':
485
+ await handleList();
486
+ await ask('Press Enter to return to menu');
487
+ break;
488
+ case '2':
489
+ await handleView();
490
+ await ask('Press Enter to return to menu');
491
+ break;
492
+ case '3':
493
+ await handleCreate();
494
+ await ask('Press Enter to return to menu');
495
+ break;
496
+ case '4':
497
+ await handleEdit();
498
+ await ask('Press Enter to return to menu');
499
+ break;
500
+ case '5':
501
+ await handleDelete();
502
+ await ask('Press Enter to return to menu');
503
+ break;
504
+ case '6':
505
+ await handleLogin();
506
+ await ask('Press Enter to return to menu');
507
+ break;
508
+ case '7':
509
+ clearScreen();
510
+ log('Goodbye!');
511
+ process.exit(0);
512
+ default:
513
+ warning('Invalid option. Please choose 1-7.');
514
+ await new Promise(r => setTimeout(r, 1200));
515
+ }
516
+ }
517
+ }
518
+
519
+ // CLI Entrypoint Router
520
+ async function main() {
521
+ const args = process.argv.slice(2);
522
+ const command = args[0]?.toLowerCase();
523
+ const subArg = args[1];
524
+
525
+ if (!command) {
526
+ await startMainMenu();
527
+ } else {
528
+ switch (command) {
529
+ case 'login':
530
+ await handleLogin();
531
+ break;
532
+ case 'list':
533
+ await handleList();
534
+ break;
535
+ case 'view':
536
+ await handleView(subArg);
537
+ break;
538
+ case 'create':
539
+ await handleCreate(subArg);
540
+ break;
541
+ case 'edit':
542
+ await handleEdit(subArg);
543
+ break;
544
+ case 'delete':
545
+ await handleDelete(subArg);
546
+ break;
547
+ case 'config':
548
+ await handleConfig();
549
+ break;
550
+ case 'help':
551
+ case '-h':
552
+ case '--help':
553
+ handleHelp();
554
+ break;
555
+ default:
556
+ error(`Unknown command: "${command}". Run \`note help\` for usage.`);
557
+ process.exit(1);
558
+ }
559
+ }
560
+ }
561
+
562
+ main().catch(e => {
563
+ error(`Unhandled exception: ${e.message}`);
564
+ process.exit(1);
565
+ });
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "open-notepad",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool for notepad.web.id to access, edit, create, and list room notes interactively.",
5
+ "type": "module",
6
+ "bin": {
7
+ "note": "./bin/note.js"
8
+ },
9
+ "engines": {
10
+ "node": ">=18.0.0"
11
+ },
12
+ "keywords": [
13
+ "note",
14
+ "notepad",
15
+ "cli",
16
+ "interactive"
17
+ ],
18
+ "author": "",
19
+ "license": "ISC"
20
+ }