@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.
- package/README.md +8 -9
- package/dist/index.js +165 -29
- 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
|
|
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,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
|
|
435
|
-
if (!
|
|
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 =
|
|
567
|
+
const page = findPageBySelectorOrThrow(state, pageSelector);
|
|
443
568
|
if (ctx.json) {
|
|
444
569
|
output(ctx.json, {
|
|
445
570
|
pageId: page.id,
|
|
446
|
-
title: page
|
|
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
|
|
579
|
+
const pageSelector = ctx.positionals[0];
|
|
455
580
|
const filePath = ctx.positionals[1];
|
|
456
|
-
if (!
|
|
457
|
-
throw new Error(
|
|
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 =
|
|
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
|
|
478
|
-
if (!
|
|
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((
|
|
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: ${
|
|
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: ${
|
|
622
|
+
{ event: "delete_success", pageId: page.id },
|
|
623
|
+
`Deleted page: ${page.id}`
|
|
496
624
|
);
|
|
497
625
|
}
|
|
498
626
|
async function commandShare(ctx) {
|
|
499
|
-
const
|
|
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 (
|
|
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 =
|
|
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;
|