@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.
- package/README.md +7 -9
- package/dist/index.js +130 -29
- 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
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
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
|
|
435
|
-
if (!
|
|
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 =
|
|
536
|
+
const page = findPageBySelectorOrThrow(state, pageSelector);
|
|
443
537
|
if (ctx.json) {
|
|
444
538
|
output(ctx.json, {
|
|
445
539
|
pageId: page.id,
|
|
446
|
-
title: page
|
|
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
|
|
548
|
+
const pageSelector = ctx.positionals[0];
|
|
455
549
|
const filePath = ctx.positionals[1];
|
|
456
|
-
if (!
|
|
457
|
-
throw new Error(
|
|
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 =
|
|
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
|
|
478
|
-
if (!
|
|
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((
|
|
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: ${
|
|
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: ${
|
|
591
|
+
{ event: "delete_success", pageId: page.id },
|
|
592
|
+
`Deleted page: ${page.id}`
|
|
496
593
|
);
|
|
497
594
|
}
|
|
498
595
|
async function commandShare(ctx) {
|
|
499
|
-
const
|
|
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 (
|
|
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 =
|
|
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;
|