@blankdotpage/cli 0.1.2 → 0.1.4

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 (3) hide show
  1. package/README.md +8 -9
  2. package/dist/index.js +165 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,21 +19,20 @@ By default, CLI commands target:
19
19
 
20
20
  - `https://blank.page`
21
21
 
22
- Override target endpoint with either:
23
-
24
- - `--app-url https://your-preview.fly.dev`
25
- - `BLANKPAGE_APP_URL=https://your-preview.fly.dev`
26
-
27
22
  ## Commands
28
23
 
29
24
  ```bash
30
25
  blankpage login [--app-url <url>] [--no-browser]
26
+ blankpage logout [--app-url <url>]
31
27
  blankpage whoami
28
+ blankpage list
32
29
  blankpage create <file.md>
33
- blankpage view <page-id>
34
- blankpage update <page-id> <file.md>
35
- blankpage delete <page-id>
30
+ blankpage view <page-id-or-title>
31
+ blankpage update <page-id-or-title> <file.md>
32
+ blankpage delete <page-id-or-title>
36
33
  blankpage share <file.md> [--title "..."]
37
- blankpage share --page-id <page-id> [--title "..."]
34
+ blankpage share --page-id <page-id-or-title> [--title "..."]
38
35
  blankpage unshare <link-id-or-url>
39
36
  ```
37
+
38
+ Commands that accept page ids also accept page titles when the title is unique.
package/dist/index.js CHANGED
@@ -306,12 +306,95 @@ function output(json, payload, text) {
306
306
  async function readMarkdownFile(filePath) {
307
307
  return fs2.readFile(filePath, "utf8");
308
308
  }
309
- function findPageOrThrow(state, pageId) {
310
- const page = state.pages.find((entry) => entry.id === pageId);
311
- if (!page) {
312
- throw new Error(`Page not found: ${pageId}`);
309
+ function inferTitleFromMarkdown(content) {
310
+ const lines = content.split(/\r?\n/);
311
+ for (const line of lines) {
312
+ const trimmed = line.trim();
313
+ if (!trimmed) {
314
+ continue;
315
+ }
316
+ const headingMatch = trimmed.match(/^#{1,6}\s+(.+)$/);
317
+ if (headingMatch) {
318
+ const heading = headingMatch[1]?.trim();
319
+ if (heading) {
320
+ return heading;
321
+ }
322
+ }
323
+ return trimmed;
324
+ }
325
+ return null;
326
+ }
327
+ function getPageTitle(page) {
328
+ const explicit = page.title?.trim();
329
+ if (explicit) {
330
+ return explicit;
331
+ }
332
+ const inferred = inferTitleFromMarkdown(page.content);
333
+ if (inferred) {
334
+ return inferred;
335
+ }
336
+ return "(untitled)";
337
+ }
338
+ function normalizeSelector(value) {
339
+ return value.trim().toLowerCase();
340
+ }
341
+ function findPageBySelectorOrThrow(state, selector) {
342
+ const exactIdMatch = state.pages.find((entry) => entry.id === selector);
343
+ if (exactIdMatch) {
344
+ return exactIdMatch;
345
+ }
346
+ const normalizedSelector = normalizeSelector(selector);
347
+ const titleMatches = state.pages.filter(
348
+ (entry) => normalizeSelector(getPageTitle(entry)) === normalizedSelector
349
+ );
350
+ if (titleMatches.length === 1) {
351
+ return titleMatches[0];
352
+ }
353
+ if (titleMatches.length > 1) {
354
+ const matchesPreview = titleMatches.slice(0, 5).map((entry) => `${entry.id} (${getPageTitle(entry)})`).join(", ");
355
+ throw new Error(
356
+ `Multiple pages match "${selector}". Use page id instead: ${matchesPreview}`
357
+ );
358
+ }
359
+ throw new Error(`Page not found: ${selector}`);
360
+ }
361
+ function toListedPages(state) {
362
+ return [...state.pages].map((page) => ({
363
+ id: page.id,
364
+ title: getPageTitle(page),
365
+ lastUpdatedAt: page.lastUpdatedAt
366
+ })).sort((left, right) => right.lastUpdatedAt - left.lastUpdatedAt);
367
+ }
368
+ function truncateForTable(value, maxLength) {
369
+ if (value.length <= maxLength) {
370
+ return value;
371
+ }
372
+ if (maxLength <= 3) {
373
+ return value.slice(0, maxLength);
374
+ }
375
+ return `${value.slice(0, maxLength - 3)}...`;
376
+ }
377
+ function formatPagesTable(pages) {
378
+ if (pages.length === 0) {
379
+ return "No pages found.";
380
+ }
381
+ const idWidth = Math.max(
382
+ "PAGE ID".length,
383
+ ...pages.map((page) => page.id.length)
384
+ );
385
+ const titleWidth = Math.min(
386
+ 48,
387
+ Math.max("TITLE".length, ...pages.map((page) => page.title.length))
388
+ );
389
+ const lines = [
390
+ `${"PAGE ID".padEnd(idWidth)} ${"TITLE".padEnd(titleWidth)} UPDATED`
391
+ ];
392
+ for (const page of pages) {
393
+ lines.push(
394
+ `${page.id.padEnd(idWidth)} ${truncateForTable(page.title, titleWidth).padEnd(titleWidth)} ${new Date(page.lastUpdatedAt).toISOString()}`
395
+ );
313
396
  }
314
- return page;
397
+ return lines.join("\n");
315
398
  }
316
399
  async function commandLogin(ctx) {
317
400
  const noBrowser = booleanFlag(ctx.flags, "no-browser");
@@ -403,6 +486,48 @@ async function commandWhoAmI(ctx) {
403
486
  `${whoami.email}${whoami.handle ? ` (@${whoami.handle})` : ""}`
404
487
  );
405
488
  }
489
+ async function commandLogout(ctx) {
490
+ if (ctx.positionals.length > 0) {
491
+ throw new Error("Usage: blankpage logout");
492
+ }
493
+ const config = await loadConfig();
494
+ const token = config.token;
495
+ let revokedRemote = false;
496
+ if (token) {
497
+ const client = new ApiClient({ appUrl: ctx.appUrl, token });
498
+ try {
499
+ await client.post("/api/cli/logout", {});
500
+ revokedRemote = true;
501
+ } catch (error) {
502
+ if (!(error instanceof ApiError) || error.status >= 500) {
503
+ throw error;
504
+ }
505
+ }
506
+ }
507
+ await saveConfig({
508
+ appUrl: config.appUrl ?? ctx.appUrl
509
+ });
510
+ output(
511
+ ctx.json,
512
+ {
513
+ event: "logout_success",
514
+ revokedRemote,
515
+ hadToken: Boolean(token)
516
+ },
517
+ token ? "Logged out. CLI token removed." : "Already logged out. No CLI token found."
518
+ );
519
+ }
520
+ async function commandList(ctx) {
521
+ if (ctx.positionals.length > 0) {
522
+ throw new Error("Usage: blankpage list");
523
+ }
524
+ const config = await loadConfig();
525
+ const token = requireToken(config.token);
526
+ const client = new ApiClient({ appUrl: ctx.appUrl, token });
527
+ const state = await fetchRemoteState(client);
528
+ const pages = toListedPages(state);
529
+ output(ctx.json, { pages }, formatPagesTable(pages));
530
+ }
406
531
  async function commandCreate(ctx) {
407
532
  const filePath = ctx.positionals[0];
408
533
  if (!filePath) {
@@ -431,19 +556,19 @@ async function commandCreate(ctx) {
431
556
  );
432
557
  }
433
558
  async function commandView(ctx) {
434
- const pageId = ctx.positionals[0];
435
- if (!pageId) {
436
- throw new Error("Usage: blankpage view <page-id>");
559
+ const pageSelector = ctx.positionals[0];
560
+ if (!pageSelector) {
561
+ throw new Error("Usage: blankpage view <page-id-or-title>");
437
562
  }
438
563
  const config = await loadConfig();
439
564
  const token = requireToken(config.token);
440
565
  const client = new ApiClient({ appUrl: ctx.appUrl, token });
441
566
  const state = await fetchRemoteState(client);
442
- const page = findPageOrThrow(state, pageId);
567
+ const page = findPageBySelectorOrThrow(state, pageSelector);
443
568
  if (ctx.json) {
444
569
  output(ctx.json, {
445
570
  pageId: page.id,
446
- title: page.title,
571
+ title: getPageTitle(page),
447
572
  content: page.content
448
573
  });
449
574
  return;
@@ -451,17 +576,19 @@ async function commandView(ctx) {
451
576
  printText(page.content);
452
577
  }
453
578
  async function commandUpdate(ctx) {
454
- const pageId = ctx.positionals[0];
579
+ const pageSelector = ctx.positionals[0];
455
580
  const filePath = ctx.positionals[1];
456
- if (!pageId || !filePath) {
457
- throw new Error("Usage: blankpage update <page-id> <markdown-file>");
581
+ if (!pageSelector || !filePath) {
582
+ throw new Error(
583
+ "Usage: blankpage update <page-id-or-title> <markdown-file>"
584
+ );
458
585
  }
459
586
  const config = await loadConfig();
460
587
  const token = requireToken(config.token);
461
588
  const client = new ApiClient({ appUrl: ctx.appUrl, token });
462
589
  const content = await readMarkdownFile(filePath);
463
590
  const state = await fetchRemoteState(client);
464
- const page = findPageOrThrow(state, pageId);
591
+ const page = findPageBySelectorOrThrow(state, pageSelector);
465
592
  const now = Date.now();
466
593
  page.content = content;
467
594
  page.lastUpdatedAt = now;
@@ -474,29 +601,30 @@ async function commandUpdate(ctx) {
474
601
  );
475
602
  }
476
603
  async function commandDelete(ctx) {
477
- const pageId = ctx.positionals[0];
478
- if (!pageId) {
479
- throw new Error("Usage: blankpage delete <page-id>");
604
+ const pageSelector = ctx.positionals[0];
605
+ if (!pageSelector) {
606
+ throw new Error("Usage: blankpage delete <page-id-or-title>");
480
607
  }
481
608
  const config = await loadConfig();
482
609
  const token = requireToken(config.token);
483
610
  const client = new ApiClient({ appUrl: ctx.appUrl, token });
484
611
  const state = await fetchRemoteState(client);
612
+ const page = findPageBySelectorOrThrow(state, pageSelector);
485
613
  const before = state.pages.length;
486
- state.pages = state.pages.filter((page) => page.id !== pageId);
614
+ state.pages = state.pages.filter((entry) => entry.id !== page.id);
487
615
  if (state.pages.length === before) {
488
- throw new Error(`Page not found: ${pageId}`);
616
+ throw new Error(`Page not found: ${pageSelector}`);
489
617
  }
490
618
  state.lastUpdatedAt = Date.now();
491
619
  await pushRemoteState(client, state);
492
620
  output(
493
621
  ctx.json,
494
- { event: "delete_success", pageId },
495
- `Deleted page: ${pageId}`
622
+ { event: "delete_success", pageId: page.id },
623
+ `Deleted page: ${page.id}`
496
624
  );
497
625
  }
498
626
  async function commandShare(ctx) {
499
- const pageIdFlag = stringFlag(ctx.flags, "page-id");
627
+ const pageSelectorFlag = stringFlag(ctx.flags, "page-id");
500
628
  const title = stringFlag(ctx.flags, "title");
501
629
  const config = await loadConfig();
502
630
  const token = requireToken(config.token);
@@ -504,20 +632,20 @@ async function commandShare(ctx) {
504
632
  const state = await fetchRemoteState(client);
505
633
  let pageId;
506
634
  let content;
507
- if (pageIdFlag) {
635
+ if (pageSelectorFlag) {
508
636
  if (ctx.positionals.length > 0) {
509
637
  throw new Error(
510
638
  "When --page-id is used, do not pass a markdown file path."
511
639
  );
512
640
  }
513
- const page = findPageOrThrow(state, pageIdFlag);
641
+ const page = findPageBySelectorOrThrow(state, pageSelectorFlag);
514
642
  pageId = page.id;
515
643
  content = page.content;
516
644
  } else {
517
645
  const filePath = ctx.positionals[0];
518
646
  if (!filePath) {
519
647
  throw new Error(
520
- "Usage: blankpage share <markdown-file> [--title ...] or blankpage share --page-id <id>"
648
+ "Usage: blankpage share <markdown-file> [--title ...] or blankpage share --page-id <page-id-or-title>"
521
649
  );
522
650
  }
523
651
  content = await readMarkdownFile(filePath);
@@ -602,13 +730,15 @@ function printHelp() {
602
730
 
603
731
  Commands:
604
732
  blankpage login [--app-url <url>] [--no-browser] [--json]
733
+ blankpage logout [--app-url <url>] [--json]
605
734
  blankpage whoami [--app-url <url>] [--json]
735
+ blankpage list [--app-url <url>] [--json]
606
736
  blankpage create <markdown-file> [--app-url <url>] [--json]
607
- blankpage update <page-id> <markdown-file> [--app-url <url>] [--json]
608
- blankpage view <page-id> [--app-url <url>] [--json]
609
- blankpage delete <page-id> [--app-url <url>] [--json]
737
+ blankpage update <page-id-or-title> <markdown-file> [--app-url <url>] [--json]
738
+ blankpage view <page-id-or-title> [--app-url <url>] [--json]
739
+ blankpage delete <page-id-or-title> [--app-url <url>] [--json]
610
740
  blankpage share <markdown-file> [--title "..."] [--app-url <url>] [--json]
611
- blankpage share --page-id <page-id> [--title "..."] [--app-url <url>] [--json]
741
+ blankpage share --page-id <page-id-or-title> [--title "..."] [--app-url <url>] [--json]
612
742
  blankpage unshare <link-id-or-url> [--app-url <url>] [--json]
613
743
  `);
614
744
  }
@@ -630,9 +760,15 @@ async function main() {
630
760
  case "login":
631
761
  await commandLogin(ctx);
632
762
  return;
763
+ case "logout":
764
+ await commandLogout(ctx);
765
+ return;
633
766
  case "whoami":
634
767
  await commandWhoAmI(ctx);
635
768
  return;
769
+ case "list":
770
+ await commandList(ctx);
771
+ return;
636
772
  case "create":
637
773
  await commandCreate(ctx);
638
774
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blankdotpage/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "CLI for Blank Page",
5
5
  "type": "module",
6
6
  "files": [