@blankdotpage/cli 0.1.2 → 0.1.3

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 +7 -9
  2. package/dist/index.js +130 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -19,21 +19,19 @@ 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]
31
26
  blankpage whoami
27
+ blankpage list
32
28
  blankpage create <file.md>
33
- blankpage view <page-id>
34
- blankpage update <page-id> <file.md>
35
- blankpage delete <page-id>
29
+ blankpage view <page-id-or-title>
30
+ blankpage update <page-id-or-title> <file.md>
31
+ blankpage delete <page-id-or-title>
36
32
  blankpage share <file.md> [--title "..."]
37
- blankpage share --page-id <page-id> [--title "..."]
33
+ blankpage share --page-id <page-id-or-title> [--title "..."]
38
34
  blankpage unshare <link-id-or-url>
39
35
  ```
36
+
37
+ 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,17 @@ async function commandWhoAmI(ctx) {
403
486
  `${whoami.email}${whoami.handle ? ` (@${whoami.handle})` : ""}`
404
487
  );
405
488
  }
489
+ async function commandList(ctx) {
490
+ if (ctx.positionals.length > 0) {
491
+ throw new Error("Usage: blankpage list");
492
+ }
493
+ const config = await loadConfig();
494
+ const token = requireToken(config.token);
495
+ const client = new ApiClient({ appUrl: ctx.appUrl, token });
496
+ const state = await fetchRemoteState(client);
497
+ const pages = toListedPages(state);
498
+ output(ctx.json, { pages }, formatPagesTable(pages));
499
+ }
406
500
  async function commandCreate(ctx) {
407
501
  const filePath = ctx.positionals[0];
408
502
  if (!filePath) {
@@ -431,19 +525,19 @@ async function commandCreate(ctx) {
431
525
  );
432
526
  }
433
527
  async function commandView(ctx) {
434
- const pageId = ctx.positionals[0];
435
- if (!pageId) {
436
- throw new Error("Usage: blankpage view <page-id>");
528
+ const pageSelector = ctx.positionals[0];
529
+ if (!pageSelector) {
530
+ throw new Error("Usage: blankpage view <page-id-or-title>");
437
531
  }
438
532
  const config = await loadConfig();
439
533
  const token = requireToken(config.token);
440
534
  const client = new ApiClient({ appUrl: ctx.appUrl, token });
441
535
  const state = await fetchRemoteState(client);
442
- const page = findPageOrThrow(state, pageId);
536
+ const page = findPageBySelectorOrThrow(state, pageSelector);
443
537
  if (ctx.json) {
444
538
  output(ctx.json, {
445
539
  pageId: page.id,
446
- title: page.title,
540
+ title: getPageTitle(page),
447
541
  content: page.content
448
542
  });
449
543
  return;
@@ -451,17 +545,19 @@ async function commandView(ctx) {
451
545
  printText(page.content);
452
546
  }
453
547
  async function commandUpdate(ctx) {
454
- const pageId = ctx.positionals[0];
548
+ const pageSelector = ctx.positionals[0];
455
549
  const filePath = ctx.positionals[1];
456
- if (!pageId || !filePath) {
457
- throw new Error("Usage: blankpage update <page-id> <markdown-file>");
550
+ if (!pageSelector || !filePath) {
551
+ throw new Error(
552
+ "Usage: blankpage update <page-id-or-title> <markdown-file>"
553
+ );
458
554
  }
459
555
  const config = await loadConfig();
460
556
  const token = requireToken(config.token);
461
557
  const client = new ApiClient({ appUrl: ctx.appUrl, token });
462
558
  const content = await readMarkdownFile(filePath);
463
559
  const state = await fetchRemoteState(client);
464
- const page = findPageOrThrow(state, pageId);
560
+ const page = findPageBySelectorOrThrow(state, pageSelector);
465
561
  const now = Date.now();
466
562
  page.content = content;
467
563
  page.lastUpdatedAt = now;
@@ -474,29 +570,30 @@ async function commandUpdate(ctx) {
474
570
  );
475
571
  }
476
572
  async function commandDelete(ctx) {
477
- const pageId = ctx.positionals[0];
478
- if (!pageId) {
479
- throw new Error("Usage: blankpage delete <page-id>");
573
+ const pageSelector = ctx.positionals[0];
574
+ if (!pageSelector) {
575
+ throw new Error("Usage: blankpage delete <page-id-or-title>");
480
576
  }
481
577
  const config = await loadConfig();
482
578
  const token = requireToken(config.token);
483
579
  const client = new ApiClient({ appUrl: ctx.appUrl, token });
484
580
  const state = await fetchRemoteState(client);
581
+ const page = findPageBySelectorOrThrow(state, pageSelector);
485
582
  const before = state.pages.length;
486
- state.pages = state.pages.filter((page) => page.id !== pageId);
583
+ state.pages = state.pages.filter((entry) => entry.id !== page.id);
487
584
  if (state.pages.length === before) {
488
- throw new Error(`Page not found: ${pageId}`);
585
+ throw new Error(`Page not found: ${pageSelector}`);
489
586
  }
490
587
  state.lastUpdatedAt = Date.now();
491
588
  await pushRemoteState(client, state);
492
589
  output(
493
590
  ctx.json,
494
- { event: "delete_success", pageId },
495
- `Deleted page: ${pageId}`
591
+ { event: "delete_success", pageId: page.id },
592
+ `Deleted page: ${page.id}`
496
593
  );
497
594
  }
498
595
  async function commandShare(ctx) {
499
- const pageIdFlag = stringFlag(ctx.flags, "page-id");
596
+ const pageSelectorFlag = stringFlag(ctx.flags, "page-id");
500
597
  const title = stringFlag(ctx.flags, "title");
501
598
  const config = await loadConfig();
502
599
  const token = requireToken(config.token);
@@ -504,20 +601,20 @@ async function commandShare(ctx) {
504
601
  const state = await fetchRemoteState(client);
505
602
  let pageId;
506
603
  let content;
507
- if (pageIdFlag) {
604
+ if (pageSelectorFlag) {
508
605
  if (ctx.positionals.length > 0) {
509
606
  throw new Error(
510
607
  "When --page-id is used, do not pass a markdown file path."
511
608
  );
512
609
  }
513
- const page = findPageOrThrow(state, pageIdFlag);
610
+ const page = findPageBySelectorOrThrow(state, pageSelectorFlag);
514
611
  pageId = page.id;
515
612
  content = page.content;
516
613
  } else {
517
614
  const filePath = ctx.positionals[0];
518
615
  if (!filePath) {
519
616
  throw new Error(
520
- "Usage: blankpage share <markdown-file> [--title ...] or blankpage share --page-id <id>"
617
+ "Usage: blankpage share <markdown-file> [--title ...] or blankpage share --page-id <page-id-or-title>"
521
618
  );
522
619
  }
523
620
  content = await readMarkdownFile(filePath);
@@ -603,12 +700,13 @@ function printHelp() {
603
700
  Commands:
604
701
  blankpage login [--app-url <url>] [--no-browser] [--json]
605
702
  blankpage whoami [--app-url <url>] [--json]
703
+ blankpage list [--app-url <url>] [--json]
606
704
  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]
705
+ blankpage update <page-id-or-title> <markdown-file> [--app-url <url>] [--json]
706
+ blankpage view <page-id-or-title> [--app-url <url>] [--json]
707
+ blankpage delete <page-id-or-title> [--app-url <url>] [--json]
610
708
  blankpage share <markdown-file> [--title "..."] [--app-url <url>] [--json]
611
- blankpage share --page-id <page-id> [--title "..."] [--app-url <url>] [--json]
709
+ blankpage share --page-id <page-id-or-title> [--title "..."] [--app-url <url>] [--json]
612
710
  blankpage unshare <link-id-or-url> [--app-url <url>] [--json]
613
711
  `);
614
712
  }
@@ -633,6 +731,9 @@ async function main() {
633
731
  case "whoami":
634
732
  await commandWhoAmI(ctx);
635
733
  return;
734
+ case "list":
735
+ await commandList(ctx);
736
+ return;
636
737
  case "create":
637
738
  await commandCreate(ctx);
638
739
  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.3",
4
4
  "description": "CLI for Blank Page",
5
5
  "type": "module",
6
6
  "files": [