open-notepad 1.0.0 → 1.0.2

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 +143 -42
  2. package/package.json +1 -1
package/bin/note.js CHANGED
@@ -87,7 +87,7 @@ async function loadConfig() {
87
87
  }
88
88
  }
89
89
  return {
90
- apiUrl: 'https://api-yuulabs-v1.notepad.web.id',
90
+ apiUrl: 'https://notepad.web.id',
91
91
  roomId: '',
92
92
  apiKey: ''
93
93
  };
@@ -102,6 +102,17 @@ async function saveConfig(config) {
102
102
  }
103
103
  }
104
104
 
105
+ function resolveTarget(code, config) {
106
+ let targetRoom = config.roomId;
107
+ let targetCode = code;
108
+ if (code && code.includes(':')) {
109
+ const parts = code.split(':');
110
+ targetRoom = parts[0];
111
+ targetCode = parts[1];
112
+ }
113
+ return { targetRoom, targetCode };
114
+ }
115
+
105
116
  // HTTP request wrappers
106
117
  async function apiFetch(config, endpoint, options = {}) {
107
118
  const url = `${config.apiUrl.replace(/\/$/, '')}${endpoint}`;
@@ -136,6 +147,92 @@ async function handleLogin() {
136
147
  const current = await loadConfig();
137
148
 
138
149
  const apiUrl = await ask('Enter Server API URL', current.apiUrl);
150
+
151
+ // Generate a random session code
152
+ const sessionCode = 'cli_' + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
153
+
154
+ // Try browser-based login
155
+ info('Registering CLI session...');
156
+ try {
157
+ const regRes = await fetch(`${apiUrl.replace(/\/$/, '')}/api/cli/session`, {
158
+ method: 'POST',
159
+ headers: { 'Content-Type': 'application/json' },
160
+ body: JSON.stringify({ code: sessionCode })
161
+ });
162
+
163
+ if (!regRes.ok) {
164
+ throw new Error(`Server returned ${regRes.status}`);
165
+ }
166
+
167
+ // Open browser
168
+ const authPageUrl = `${apiUrl.replace(/\/$/, '')}/auth-cli-20260628dontuseforyounote?session=${sessionCode}`;
169
+ info(`Opening browser for authentication...`);
170
+ log(`\n${colors.dim}If browser does not open automatically, visit:${colors.reset}`);
171
+ log(`${colors.bold}${colors.cyan}${authPageUrl}${colors.reset}\n`);
172
+
173
+ // Platform-specific browser open
174
+ const openCmd = process.platform === 'win32' ? 'start'
175
+ : process.platform === 'darwin' ? 'open'
176
+ : 'xdg-open';
177
+ const child = spawn(openCmd, [authPageUrl], { shell: true, stdio: 'ignore', detached: true });
178
+ child.unref();
179
+
180
+ // Poll for completion
181
+ info('Waiting for browser authorization...');
182
+ const maxAttempts = 120; // 2 minutes (polling every 1s)
183
+ let attempts = 0;
184
+
185
+ while (attempts < maxAttempts) {
186
+ await new Promise(r => setTimeout(r, 1500));
187
+ attempts++;
188
+
189
+ try {
190
+ const pollRes = await fetch(`${apiUrl.replace(/\/$/, '')}/api/cli/session/${sessionCode}`);
191
+
192
+ if (pollRes.status === 404) {
193
+ // Session expired or not found
194
+ if (attempts > 10) {
195
+ error('Session expired. Please try again.');
196
+ return;
197
+ }
198
+ continue;
199
+ }
200
+
201
+ if (pollRes.ok) {
202
+ const data = await pollRes.json();
203
+
204
+ if (data.status === 'completed' && data.room_id && data.api_key) {
205
+ clearScreen();
206
+ log(`${colors.bgBlue}${colors.white}${colors.bold} CLI Connected Successfully! ${colors.reset}\n`);
207
+ await saveConfig({
208
+ apiUrl,
209
+ roomId: data.room_id,
210
+ apiKey: data.api_key
211
+ });
212
+ success(`Linked to room: ${colors.bold}${data.room_id}.notepad.web.id${colors.reset}`);
213
+ info('You can now use all CLI commands. Try: note list');
214
+ return;
215
+ }
216
+ }
217
+ } catch (pollErr) {
218
+ // Silently continue polling
219
+ }
220
+
221
+ // Progress indicator
222
+ if (attempts % 4 === 0) {
223
+ process.stdout.write(`\r${colors.dim} Polling... (${attempts}s elapsed, waiting for browser auth)${colors.reset} `);
224
+ }
225
+ }
226
+
227
+ process.stdout.write('\n');
228
+ warning('Browser authorization timed out.');
229
+ info('Falling back to manual configuration...\n');
230
+ } catch (e) {
231
+ warning(`Browser login unavailable: ${e.message}`);
232
+ info('Using manual configuration instead...\n');
233
+ }
234
+
235
+ // Fallback: manual configuration
139
236
  const roomId = await ask('Enter Subdomain/Room ID (e.g. "saya")', current.roomId);
140
237
  const apiKey = await ask('Enter Room API Token/Key', current.apiKey);
141
238
 
@@ -184,22 +281,23 @@ async function handleList() {
184
281
 
185
282
  async function handleView(codeArg) {
186
283
  const config = await loadConfig();
187
- if (!config.roomId) {
188
- warning('Not configured. Please run `note login` first.');
189
- return;
190
- }
191
-
192
284
  const code = codeArg || await ask('Enter note code to view');
193
285
  if (!code) return;
194
286
 
195
- info(`Loading note ${colors.bold}/${config.roomId}/${code}${colors.reset}...`);
287
+ const { targetRoom, targetCode } = resolveTarget(code, config);
288
+ if (!targetRoom) {
289
+ warning('No room specified. Please run `note login` or specify room:code.');
290
+ return;
291
+ }
292
+
293
+ info(`Loading note ${colors.bold}/${targetRoom}/${targetCode}${colors.reset}...`);
196
294
 
197
295
  try {
198
296
  // Attempt standard fetch with API key
199
- let res = await apiFetch(config, `/api/notes/room/${config.roomId}/${code}`);
297
+ let res = await apiFetch(config, `/api/notes/room/${targetRoom}/${targetCode}`);
200
298
 
201
299
  if (res.status === 404) {
202
- error(`Note "${code}" not found.`);
300
+ error(`Note "${targetCode}" not found in room "${targetRoom}".`);
203
301
  return;
204
302
  }
205
303
 
@@ -211,7 +309,7 @@ async function handleView(codeArg) {
211
309
 
212
310
  // Note: We bypass API Key header if providing note password for private notes
213
311
  // because backend rejects (is_api = true && private_note = true) outright.
214
- res = await apiFetch(config, `/api/notes/room/${config.roomId}/${code}`, {
312
+ res = await apiFetch(config, `/api/notes/room/${targetRoom}/${targetCode}`, {
215
313
  headers: { 'x-note-password': pwd },
216
314
  skipApiKey: true
217
315
  });
@@ -229,7 +327,7 @@ async function handleView(codeArg) {
229
327
  }
230
328
 
231
329
  clearScreen();
232
- log(`${colors.bold}${colors.blue}Note: /${config.roomId}/${code}${colors.reset}`);
330
+ log(`${colors.bold}${colors.blue}Note: /${targetRoom}/${targetCode}${colors.reset}`);
233
331
  log(`${colors.dim}------------------------------------------------------------${colors.reset}`);
234
332
  log(noteData.content || `${colors.dim}(Empty note)${colors.reset}`);
235
333
  log(`${colors.dim}------------------------------------------------------------${colors.reset}`);
@@ -240,14 +338,15 @@ async function handleView(codeArg) {
240
338
 
241
339
  async function handleCreate(codeArg) {
242
340
  const config = await loadConfig();
243
- if (!config.roomId) {
244
- warning('Not configured. Please run `note login` first.');
245
- return;
246
- }
247
-
248
341
  const code = codeArg || await ask('Enter new note code');
249
342
  if (!code) return;
250
343
 
344
+ const { targetRoom, targetCode } = resolveTarget(code, config);
345
+ if (!targetRoom) {
346
+ warning('No room specified. Please run `note login` or specify room:code.');
347
+ return;
348
+ }
349
+
251
350
  const isPublicStr = await ask('Make note public? (y/n)', 'y');
252
351
  const isPublic = isPublicStr.toLowerCase().startsWith('y');
253
352
 
@@ -265,11 +364,11 @@ async function handleCreate(codeArg) {
265
364
  }
266
365
  }
267
366
 
268
- info(`Creating note ${colors.bold}/${config.roomId}/${code}${colors.reset}...`);
367
+ info(`Creating note ${colors.bold}/${targetRoom}/${targetCode}${colors.reset}...`);
269
368
 
270
369
  try {
271
370
  // 1. Create/Save note with empty content (must send API key)
272
- let saveRes = await apiFetch(config, `/api/notes/room/${config.roomId}/${code}`, {
371
+ let saveRes = await apiFetch(config, `/api/notes/room/${targetRoom}/${targetCode}`, {
273
372
  method: 'POST',
274
373
  body: JSON.stringify({ content: '' })
275
374
  });
@@ -280,7 +379,7 @@ async function handleCreate(codeArg) {
280
379
  }
281
380
 
282
381
  // 2. Set visibility and password (calls settings endpoint)
283
- const settingsRes = await apiFetch(config, `/api/notes/room/${config.roomId}/${code}/settings`, {
382
+ const settingsRes = await apiFetch(config, `/api/notes/room/${targetRoom}/${targetCode}/settings`, {
284
383
  method: 'POST',
285
384
  body: JSON.stringify({
286
385
  is_public: isPublic,
@@ -293,11 +392,11 @@ async function handleCreate(codeArg) {
293
392
  return;
294
393
  }
295
394
 
296
- success(`Note "${code}" created successfully!`);
395
+ success(`Note "${targetCode}" created successfully in room "${targetRoom}"!`);
297
396
 
298
397
  const editNow = await ask('Open editor to write content now? (y/n)', 'y');
299
398
  if (editNow.toLowerCase().startsWith('y')) {
300
- await handleEdit(code);
399
+ await handleEdit(`${targetRoom}:${targetCode}`);
301
400
  }
302
401
  } catch (e) {
303
402
  error(e.message);
@@ -306,25 +405,26 @@ async function handleCreate(codeArg) {
306
405
 
307
406
  async function handleEdit(codeArg) {
308
407
  const config = await loadConfig();
309
- if (!config.roomId) {
310
- warning('Not configured. Please run `note login` first.');
311
- return;
312
- }
313
-
314
408
  const code = codeArg || await ask('Enter note code to edit');
315
409
  if (!code) return;
316
410
 
317
- info(`Fetching current content of /${config.roomId}/${code}...`);
411
+ const { targetRoom, targetCode } = resolveTarget(code, config);
412
+ if (!targetRoom) {
413
+ warning('No room specified. Please run `note login` or specify room:code.');
414
+ return;
415
+ }
416
+
417
+ info(`Fetching current content of /${targetRoom}/${targetCode}...`);
318
418
 
319
419
  try {
320
420
  let notePassword = '';
321
- let res = await apiFetch(config, `/api/notes/room/${config.roomId}/${code}`);
421
+ let res = await apiFetch(config, `/api/notes/room/${targetRoom}/${targetCode}`);
322
422
 
323
423
  // If it doesn't exist, offer to create it
324
424
  if (res.status === 404) {
325
425
  const create = await ask('Note does not exist. Create it? (y/n)', 'y');
326
426
  if (create.toLowerCase().startsWith('y')) {
327
- await handleCreate(code);
427
+ await handleCreate(`${targetRoom}:${targetCode}`);
328
428
  }
329
429
  return;
330
430
  }
@@ -335,7 +435,7 @@ async function handleEdit(codeArg) {
335
435
  if (noteData.has_password && (!noteData.content || res.status === 403)) {
336
436
  notePassword = await askPassword('Enter note password to edit');
337
437
 
338
- res = await apiFetch(config, `/api/notes/room/${config.roomId}/${code}`, {
438
+ res = await apiFetch(config, `/api/notes/room/${targetRoom}/${targetCode}`, {
339
439
  headers: { 'x-note-password': notePassword },
340
440
  skipApiKey: true
341
441
  });
@@ -356,7 +456,7 @@ async function handleEdit(codeArg) {
356
456
 
357
457
  // Create temp file
358
458
  const tempDir = os.tmpdir();
359
- const tempFilePath = path.join(tempDir, `note_${config.roomId}_${code}.txt`);
459
+ const tempFilePath = path.join(tempDir, `note_${targetRoom}_${targetCode}.txt`);
360
460
  await fs.writeFile(tempFilePath, initialContent, 'utf-8');
361
461
 
362
462
  // Open editor
@@ -380,7 +480,7 @@ async function handleEdit(codeArg) {
380
480
  info('Saving changes back to server...');
381
481
 
382
482
  // Save note
383
- const saveRes = await apiFetch(config, `/api/notes/room/${config.roomId}/${code}`, {
483
+ const saveRes = await apiFetch(config, `/api/notes/room/${targetRoom}/${targetCode}`, {
384
484
  method: 'POST',
385
485
  headers: notePassword ? { 'x-note-password': notePassword } : {},
386
486
  skipApiKey: !!notePassword, // Skip API key header if note password is used
@@ -388,7 +488,7 @@ async function handleEdit(codeArg) {
388
488
  });
389
489
 
390
490
  if (saveRes.ok) {
391
- success(`Saved /${config.roomId}/${code} successfully!`);
491
+ success(`Saved /${targetRoom}/${targetCode} successfully!`);
392
492
  } else {
393
493
  error(`Failed to save changes. Server returned status: ${saveRes.status}`);
394
494
  }
@@ -403,28 +503,29 @@ async function handleEdit(codeArg) {
403
503
 
404
504
  async function handleDelete(codeArg) {
405
505
  const config = await loadConfig();
406
- if (!config.roomId) {
407
- warning('Not configured. Please run `note login` first.');
408
- return;
409
- }
410
-
411
506
  const code = codeArg || await ask('Enter note code to delete');
412
507
  if (!code) return;
413
508
 
414
- const confirm = await ask(`Are you sure you want to delete /${config.roomId}/${code}? (y/n)`, 'n');
509
+ const { targetRoom, targetCode } = resolveTarget(code, config);
510
+ if (!targetRoom) {
511
+ warning('No room specified. Please run `note login` or specify room:code.');
512
+ return;
513
+ }
514
+
515
+ const confirm = await ask(`Are you sure you want to delete /${targetRoom}/${targetCode}? (y/n)`, 'n');
415
516
  if (!confirm.toLowerCase().startsWith('y')) {
416
517
  info('Delete canceled.');
417
518
  return;
418
519
  }
419
520
 
420
- info(`Deleting note /${config.roomId}/${code}...`);
521
+ info(`Deleting note /${targetRoom}/${targetCode}...`);
421
522
  try {
422
- const res = await apiFetch(config, `/api/rooms/${config.roomId}/notes/${code}`, {
523
+ const res = await apiFetch(config, `/api/rooms/${targetRoom}/notes/${targetCode}`, {
423
524
  method: 'DELETE'
424
525
  });
425
526
 
426
527
  if (res.ok) {
427
- success(`Note "${code}" deleted successfully.`);
528
+ success(`Note "${targetCode}" deleted successfully from room "${targetRoom}".`);
428
529
  } else {
429
530
  error(`Failed to delete note. Status: ${res.status}`);
430
531
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "open-notepad",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "CLI tool for notepad.web.id to access, edit, create, and list room notes interactively.",
5
5
  "type": "module",
6
6
  "bin": {