@andrzejchm/notion-cli 0.5.0 → 0.7.0
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 +10 -0
- package/dist/cli.js +357 -61
- package/dist/cli.js.map +1 -1
- package/docs/FEATURE-PARITY.md +189 -0
- package/docs/README.agents.md +42 -0
- package/docs/skills/using-notion-cli/SKILL.md +52 -1
- package/docs/testing-setup.md +250 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -95,7 +95,10 @@ notion ls
|
|
|
95
95
|
| `notion comments <id\|url>` | Read page comments |
|
|
96
96
|
| `notion comment <id\|url> -m <text>` | Add a comment to a page |
|
|
97
97
|
| `notion append <id\|url> -m <markdown>` | Append markdown blocks to a page |
|
|
98
|
+
| `notion edit-page <id\|url> --find <old> --replace <new>` | Search-and-replace text on a page |
|
|
99
|
+
| `notion edit-page <id\|url> -m <markdown>` | Replace entire page content |
|
|
98
100
|
| `notion create-page --parent <id\|url> --title <title>` | Create a new page, prints URL |
|
|
101
|
+
| `notion update <id\|url> --prop "Name=Value"` | Update properties on a page |
|
|
99
102
|
| `notion completion bash\|zsh\|fish` | Install shell tab completion |
|
|
100
103
|
|
|
101
104
|
### `notion db query` flags
|
|
@@ -188,6 +191,7 @@ Write commands require additional capabilities — enable in your integration se
|
|
|
188
191
|
|---------|----------------------|
|
|
189
192
|
| `notion append` | Read content, Insert content |
|
|
190
193
|
| `notion create-page` | Read content, Insert content |
|
|
194
|
+
| `notion update` | Read content, Update content |
|
|
191
195
|
| `notion comment` | Read content, Insert content, Read comments, Insert comments |
|
|
192
196
|
|
|
193
197
|
---
|
|
@@ -208,6 +212,12 @@ Write commands require additional capabilities — enable in your integration se
|
|
|
208
212
|
|
|
209
213
|
---
|
|
210
214
|
|
|
215
|
+
## Roadmap & Feature Parity
|
|
216
|
+
|
|
217
|
+
See [docs/FEATURE-PARITY.md](docs/FEATURE-PARITY.md) for a detailed comparison of this CLI's capabilities against the official Notion MCP server, with prioritized gaps and planned additions.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
211
221
|
## License
|
|
212
222
|
|
|
213
223
|
MIT © [Andrzej Chmielewski](https://github.com/andrzejchm)
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { readFileSync } from "fs";
|
|
5
5
|
import { dirname, join as join3 } from "path";
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
7
|
-
import { Command as
|
|
7
|
+
import { Command as Command22 } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/commands/append.ts
|
|
10
10
|
import { Command } from "commander";
|
|
@@ -444,6 +444,12 @@ function withErrorHandling(fn) {
|
|
|
444
444
|
});
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
+
// src/errors/notion-errors.ts
|
|
448
|
+
var SELECTOR_HINT = 'Use an ellipsis selector matching page content, e.g. "## Section...end of section". Run `notion read <id>` to see the page content.';
|
|
449
|
+
function isNotionValidationError(error2) {
|
|
450
|
+
return typeof error2 === "object" && error2 !== null && "code" in error2 && error2.code === "validation_error";
|
|
451
|
+
}
|
|
452
|
+
|
|
447
453
|
// src/notion/client.ts
|
|
448
454
|
import { APIErrorCode, Client, isNotionClientError } from "@notionhq/client";
|
|
449
455
|
async function validateToken(token) {
|
|
@@ -557,19 +563,23 @@ async function addComment(client, pageId, text, options = {}) {
|
|
|
557
563
|
...options.asUser && { display_name: { type: "user" } }
|
|
558
564
|
});
|
|
559
565
|
}
|
|
560
|
-
async function appendMarkdown(client, pageId, markdown) {
|
|
566
|
+
async function appendMarkdown(client, pageId, markdown, options) {
|
|
561
567
|
await client.pages.updateMarkdown({
|
|
562
568
|
page_id: pageId,
|
|
563
569
|
type: "insert_content",
|
|
564
|
-
insert_content: {
|
|
570
|
+
insert_content: {
|
|
571
|
+
content: markdown,
|
|
572
|
+
...options?.after != null && { after: options.after }
|
|
573
|
+
}
|
|
565
574
|
});
|
|
566
575
|
}
|
|
567
576
|
function countOccurrences(text, sub) {
|
|
577
|
+
if (!sub) return 0;
|
|
568
578
|
let count = 0;
|
|
569
579
|
let pos = text.indexOf(sub, 0);
|
|
570
580
|
while (pos !== -1) {
|
|
571
581
|
count++;
|
|
572
|
-
pos = text.indexOf(sub, pos +
|
|
582
|
+
pos = text.indexOf(sub, pos + sub.length);
|
|
573
583
|
}
|
|
574
584
|
return count;
|
|
575
585
|
}
|
|
@@ -588,10 +598,46 @@ function buildContentRange(content) {
|
|
|
588
598
|
}
|
|
589
599
|
return content;
|
|
590
600
|
}
|
|
591
|
-
async function
|
|
601
|
+
async function searchAndReplace(client, pageId, updates, options) {
|
|
602
|
+
await client.pages.updateMarkdown({
|
|
603
|
+
page_id: pageId,
|
|
604
|
+
type: "update_content",
|
|
605
|
+
update_content: {
|
|
606
|
+
content_updates: updates.map((u) => ({
|
|
607
|
+
old_str: u.oldStr,
|
|
608
|
+
new_str: u.newStr,
|
|
609
|
+
...options?.replaceAll && { replace_all_matches: true }
|
|
610
|
+
})),
|
|
611
|
+
...options?.allowDeletingContent && { allow_deleting_content: true }
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
async function replacePageContent(client, pageId, newContent, options) {
|
|
616
|
+
await client.pages.updateMarkdown({
|
|
617
|
+
page_id: pageId,
|
|
618
|
+
type: "replace_content",
|
|
619
|
+
replace_content: {
|
|
620
|
+
new_str: newContent,
|
|
621
|
+
...options?.allowDeletingContent && { allow_deleting_content: true }
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
async function replaceMarkdown(client, pageId, newMarkdown, options) {
|
|
592
626
|
const current = await client.pages.retrieveMarkdown({ page_id: pageId });
|
|
593
627
|
const currentContent = current.markdown.trim();
|
|
628
|
+
if (current.truncated && !options?.range) {
|
|
629
|
+
throw new CliError(
|
|
630
|
+
ErrorCodes.API_ERROR,
|
|
631
|
+
"Page content is too large for full-page replace (markdown was truncated by the API).",
|
|
632
|
+
"Use --range to replace a specific section instead."
|
|
633
|
+
);
|
|
634
|
+
}
|
|
594
635
|
if (!currentContent) {
|
|
636
|
+
if (options?.range) {
|
|
637
|
+
process.stderr.write(
|
|
638
|
+
"Warning: page is empty, --range ignored, content inserted as-is.\n"
|
|
639
|
+
);
|
|
640
|
+
}
|
|
595
641
|
if (newMarkdown.trim()) {
|
|
596
642
|
await client.pages.updateMarkdown({
|
|
597
643
|
page_id: pageId,
|
|
@@ -601,13 +647,15 @@ async function replaceMarkdown(client, pageId, newMarkdown) {
|
|
|
601
647
|
}
|
|
602
648
|
return;
|
|
603
649
|
}
|
|
650
|
+
const contentRange = options?.range ?? buildContentRange(currentContent);
|
|
651
|
+
const allowDeletingContent = options?.allowDeletingContent ?? options?.range == null;
|
|
604
652
|
await client.pages.updateMarkdown({
|
|
605
653
|
page_id: pageId,
|
|
606
654
|
type: "replace_content_range",
|
|
607
655
|
replace_content_range: {
|
|
608
656
|
content: newMarkdown,
|
|
609
|
-
content_range:
|
|
610
|
-
allow_deleting_content:
|
|
657
|
+
content_range: contentRange,
|
|
658
|
+
allow_deleting_content: allowDeletingContent
|
|
611
659
|
}
|
|
612
660
|
});
|
|
613
661
|
}
|
|
@@ -621,10 +669,11 @@ async function createPage(client, parentId, title, markdown) {
|
|
|
621
669
|
},
|
|
622
670
|
...markdown.trim() ? { markdown } : {}
|
|
623
671
|
});
|
|
624
|
-
|
|
672
|
+
const url = "url" in response ? response.url : response.id;
|
|
673
|
+
return url;
|
|
625
674
|
}
|
|
626
675
|
|
|
627
|
-
// src/
|
|
676
|
+
// src/utils/stdin.ts
|
|
628
677
|
async function readStdin() {
|
|
629
678
|
const chunks = [];
|
|
630
679
|
for await (const chunk of process.stdin) {
|
|
@@ -632,34 +681,58 @@ async function readStdin() {
|
|
|
632
681
|
}
|
|
633
682
|
return Buffer.concat(chunks).toString("utf-8");
|
|
634
683
|
}
|
|
684
|
+
|
|
685
|
+
// src/commands/append.ts
|
|
635
686
|
function appendCommand() {
|
|
636
687
|
const cmd = new Command("append");
|
|
637
|
-
cmd.description("append markdown content to a Notion page").argument("<id/url>", "Notion page ID or URL").option("-m, --message <markdown>", "markdown content to append").
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
markdown =
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
688
|
+
cmd.description("append markdown content to a Notion page").argument("<id/url>", "Notion page ID or URL").option("-m, --message <markdown>", "markdown content to append").option(
|
|
689
|
+
"--after <selector>",
|
|
690
|
+
'insert after matched content \u2014 ellipsis selector, e.g. "## Section...end of section"'
|
|
691
|
+
).action(
|
|
692
|
+
withErrorHandling(
|
|
693
|
+
async (idOrUrl, opts) => {
|
|
694
|
+
const { token, source } = await resolveToken();
|
|
695
|
+
reportTokenSource(source);
|
|
696
|
+
const client = createNotionClient(token);
|
|
697
|
+
let markdown = "";
|
|
698
|
+
if (opts.message) {
|
|
699
|
+
markdown = opts.message;
|
|
700
|
+
} else if (!process.stdin.isTTY) {
|
|
701
|
+
markdown = await readStdin();
|
|
702
|
+
} else {
|
|
703
|
+
throw new CliError(
|
|
704
|
+
ErrorCodes.INVALID_ARG,
|
|
705
|
+
"No content to append.",
|
|
706
|
+
"Pass markdown via -m/--message or pipe it through stdin"
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
if (!markdown.trim()) {
|
|
710
|
+
process.stdout.write("Nothing to append.\n");
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const pageId = parseNotionId(idOrUrl);
|
|
714
|
+
const uuid = toUuid(pageId);
|
|
715
|
+
try {
|
|
716
|
+
await appendMarkdown(
|
|
717
|
+
client,
|
|
718
|
+
uuid,
|
|
719
|
+
markdown,
|
|
720
|
+
opts.after ? { after: opts.after } : void 0
|
|
721
|
+
);
|
|
722
|
+
} catch (error2) {
|
|
723
|
+
if (opts.after && isNotionValidationError(error2)) {
|
|
724
|
+
throw new CliError(
|
|
725
|
+
ErrorCodes.INVALID_ARG,
|
|
726
|
+
`Selector not found: "${opts.after}". ${error2.message}`,
|
|
727
|
+
SELECTOR_HINT,
|
|
728
|
+
error2
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
throw error2;
|
|
732
|
+
}
|
|
733
|
+
process.stdout.write("Appended.\n");
|
|
657
734
|
}
|
|
658
|
-
|
|
659
|
-
const uuid = toUuid(pageId);
|
|
660
|
-
await appendMarkdown(client, uuid, markdown);
|
|
661
|
-
process.stdout.write("Appended.\n");
|
|
662
|
-
})
|
|
735
|
+
)
|
|
663
736
|
);
|
|
664
737
|
return cmd;
|
|
665
738
|
}
|
|
@@ -1379,13 +1452,6 @@ function completionCommand() {
|
|
|
1379
1452
|
|
|
1380
1453
|
// src/commands/create-page.ts
|
|
1381
1454
|
import { Command as Command8 } from "commander";
|
|
1382
|
-
async function readStdin2() {
|
|
1383
|
-
const chunks = [];
|
|
1384
|
-
for await (const chunk of process.stdin) {
|
|
1385
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1386
|
-
}
|
|
1387
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
1388
|
-
}
|
|
1389
1455
|
function createPageCommand() {
|
|
1390
1456
|
const cmd = new Command8("create-page");
|
|
1391
1457
|
cmd.description("create a new Notion page under a parent page").requiredOption("--parent <id/url>", "parent page ID or URL").requiredOption("--title <title>", "page title").option(
|
|
@@ -1401,7 +1467,7 @@ function createPageCommand() {
|
|
|
1401
1467
|
if (opts.message) {
|
|
1402
1468
|
markdown = opts.message;
|
|
1403
1469
|
} else if (!process.stdin.isTTY) {
|
|
1404
|
-
markdown = await
|
|
1470
|
+
markdown = await readStdin();
|
|
1405
1471
|
}
|
|
1406
1472
|
const parentUuid = toUuid(parseNotionId(opts.parent));
|
|
1407
1473
|
const url = await createPage(
|
|
@@ -1695,47 +1761,101 @@ function dbSchemaCommand() {
|
|
|
1695
1761
|
|
|
1696
1762
|
// src/commands/edit-page.ts
|
|
1697
1763
|
import { Command as Command11 } from "commander";
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1702
|
-
}
|
|
1703
|
-
return Buffer.concat(chunks).toString("utf-8");
|
|
1764
|
+
function collect2(val, acc) {
|
|
1765
|
+
acc.push(val);
|
|
1766
|
+
return acc;
|
|
1704
1767
|
}
|
|
1705
1768
|
function editPageCommand() {
|
|
1706
1769
|
const cmd = new Command11("edit-page");
|
|
1707
1770
|
cmd.description(
|
|
1708
|
-
"replace
|
|
1771
|
+
"replace a Notion page's content \u2014 full page or a targeted section"
|
|
1709
1772
|
).argument("<id/url>", "Notion page ID or URL").option(
|
|
1710
1773
|
"-m, --message <markdown>",
|
|
1711
1774
|
"new markdown content for the page body"
|
|
1775
|
+
).option(
|
|
1776
|
+
"--find <text>",
|
|
1777
|
+
"text to find (repeatable, pair with --replace)",
|
|
1778
|
+
collect2,
|
|
1779
|
+
[]
|
|
1780
|
+
).option(
|
|
1781
|
+
"--replace <text>",
|
|
1782
|
+
"replacement text (repeatable, pair with --find)",
|
|
1783
|
+
collect2,
|
|
1784
|
+
[]
|
|
1785
|
+
).option("--all", "replace all matches of each --find pattern").option(
|
|
1786
|
+
"--range <selector>",
|
|
1787
|
+
'[deprecated] ellipsis selector to replace only a section, e.g. "## My Section...last line"'
|
|
1788
|
+
).option(
|
|
1789
|
+
"--allow-deleting-content",
|
|
1790
|
+
"allow deletion of child pages/databases"
|
|
1712
1791
|
).action(
|
|
1713
1792
|
withErrorHandling(async (idOrUrl, opts) => {
|
|
1714
1793
|
const { token, source } = await resolveToken();
|
|
1715
1794
|
reportTokenSource(source);
|
|
1716
1795
|
const client = createNotionClient(token);
|
|
1796
|
+
const pageId = parseNotionId(idOrUrl);
|
|
1797
|
+
const uuid = toUuid(pageId);
|
|
1798
|
+
if (opts.find.length > 0) {
|
|
1799
|
+
if (opts.find.length !== opts.replace.length) {
|
|
1800
|
+
throw new CliError(
|
|
1801
|
+
ErrorCodes.INVALID_ARG,
|
|
1802
|
+
`Mismatched --find/--replace: got ${opts.find.length} --find and ${opts.replace.length} --replace flags.`,
|
|
1803
|
+
"Provide the same number of --find and --replace flags, paired by position."
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
const updates = opts.find.map((oldStr, i) => ({
|
|
1807
|
+
oldStr,
|
|
1808
|
+
newStr: opts.replace[i]
|
|
1809
|
+
}));
|
|
1810
|
+
await searchAndReplace(client, uuid, updates, {
|
|
1811
|
+
replaceAll: opts.all ?? false,
|
|
1812
|
+
allowDeletingContent: opts.allowDeletingContent ?? false
|
|
1813
|
+
});
|
|
1814
|
+
process.stdout.write("Page content updated.\n");
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1717
1817
|
let markdown = "";
|
|
1718
1818
|
if (opts.message) {
|
|
1719
1819
|
markdown = opts.message;
|
|
1720
1820
|
} else if (!process.stdin.isTTY) {
|
|
1721
|
-
markdown = await
|
|
1821
|
+
markdown = await readStdin();
|
|
1722
1822
|
if (!markdown.trim()) {
|
|
1723
1823
|
throw new CliError(
|
|
1724
1824
|
ErrorCodes.INVALID_ARG,
|
|
1725
1825
|
"No content provided (stdin was empty).",
|
|
1726
|
-
"Pass
|
|
1826
|
+
"Pass content via -m/--message for full replacement, --find/--replace for targeted edits, or pipe content through stdin"
|
|
1727
1827
|
);
|
|
1728
1828
|
}
|
|
1729
1829
|
} else {
|
|
1730
1830
|
throw new CliError(
|
|
1731
1831
|
ErrorCodes.INVALID_ARG,
|
|
1732
1832
|
"No content provided.",
|
|
1733
|
-
"Pass
|
|
1833
|
+
"Pass content via -m/--message for full replacement, --find/--replace for targeted edits, or pipe content through stdin"
|
|
1734
1834
|
);
|
|
1735
1835
|
}
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1836
|
+
if (opts.range) {
|
|
1837
|
+
try {
|
|
1838
|
+
await replaceMarkdown(client, uuid, markdown, {
|
|
1839
|
+
range: opts.range,
|
|
1840
|
+
allowDeletingContent: opts.allowDeletingContent ?? false
|
|
1841
|
+
});
|
|
1842
|
+
} catch (error2) {
|
|
1843
|
+
if (isNotionValidationError(error2)) {
|
|
1844
|
+
throw new CliError(
|
|
1845
|
+
ErrorCodes.INVALID_ARG,
|
|
1846
|
+
`Selector not found: "${opts.range}". ${error2.message}`,
|
|
1847
|
+
SELECTOR_HINT,
|
|
1848
|
+
error2
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
throw error2;
|
|
1852
|
+
}
|
|
1853
|
+
process.stdout.write("Page content replaced.\n");
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
await replacePageContent(client, uuid, markdown, {
|
|
1857
|
+
allowDeletingContent: opts.allowDeletingContent ?? false
|
|
1858
|
+
});
|
|
1739
1859
|
process.stdout.write("Page content replaced.\n");
|
|
1740
1860
|
})
|
|
1741
1861
|
);
|
|
@@ -2268,8 +2388,183 @@ function searchCommand() {
|
|
|
2268
2388
|
return cmd;
|
|
2269
2389
|
}
|
|
2270
2390
|
|
|
2271
|
-
// src/commands/
|
|
2391
|
+
// src/commands/update.ts
|
|
2272
2392
|
import { Command as Command20 } from "commander";
|
|
2393
|
+
|
|
2394
|
+
// src/services/update.service.ts
|
|
2395
|
+
var UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
|
|
2396
|
+
"relation",
|
|
2397
|
+
"formula",
|
|
2398
|
+
"rollup",
|
|
2399
|
+
"created_time",
|
|
2400
|
+
"created_by",
|
|
2401
|
+
"last_edited_time",
|
|
2402
|
+
"last_edited_by",
|
|
2403
|
+
"files",
|
|
2404
|
+
"unique_id",
|
|
2405
|
+
"verification",
|
|
2406
|
+
"button"
|
|
2407
|
+
]);
|
|
2408
|
+
function buildPropertyUpdate(propName, propType, value) {
|
|
2409
|
+
if (UNSUPPORTED_TYPES.has(propType)) {
|
|
2410
|
+
throw new CliError(
|
|
2411
|
+
ErrorCodes.INVALID_ARG,
|
|
2412
|
+
`Property "${propName}" has type "${propType}" which cannot be set via the CLI.`,
|
|
2413
|
+
"Supported types: title, rich_text, select, status, multi_select, number, checkbox, url, email, phone_number, date"
|
|
2414
|
+
);
|
|
2415
|
+
}
|
|
2416
|
+
if (value === "") {
|
|
2417
|
+
return null;
|
|
2418
|
+
}
|
|
2419
|
+
switch (propType) {
|
|
2420
|
+
case "title":
|
|
2421
|
+
return { title: [{ type: "text", text: { content: value } }] };
|
|
2422
|
+
case "rich_text":
|
|
2423
|
+
return { rich_text: [{ type: "text", text: { content: value } }] };
|
|
2424
|
+
case "select":
|
|
2425
|
+
return { select: { name: value } };
|
|
2426
|
+
case "status":
|
|
2427
|
+
return { status: { name: value } };
|
|
2428
|
+
case "multi_select":
|
|
2429
|
+
return {
|
|
2430
|
+
multi_select: value.split(",").map((v) => v.trim()).filter(Boolean).map((v) => ({ name: v }))
|
|
2431
|
+
};
|
|
2432
|
+
case "number": {
|
|
2433
|
+
const n = Number(value);
|
|
2434
|
+
if (Number.isNaN(n)) {
|
|
2435
|
+
throw new CliError(
|
|
2436
|
+
ErrorCodes.INVALID_ARG,
|
|
2437
|
+
`Invalid number value "${value}" for property "${propName}".`,
|
|
2438
|
+
'Provide a numeric value, e.g. --prop "Count=42"'
|
|
2439
|
+
);
|
|
2440
|
+
}
|
|
2441
|
+
return { number: n };
|
|
2442
|
+
}
|
|
2443
|
+
case "checkbox": {
|
|
2444
|
+
const lower = value.toLowerCase();
|
|
2445
|
+
return { checkbox: lower === "true" || lower === "yes" };
|
|
2446
|
+
}
|
|
2447
|
+
case "url":
|
|
2448
|
+
return { url: value };
|
|
2449
|
+
case "email":
|
|
2450
|
+
return { email: value };
|
|
2451
|
+
case "phone_number":
|
|
2452
|
+
return { phone_number: value };
|
|
2453
|
+
case "date": {
|
|
2454
|
+
const parts = value.split(",");
|
|
2455
|
+
const start = parts[0].trim();
|
|
2456
|
+
const end = parts[1]?.trim();
|
|
2457
|
+
return { date: end ? { start, end } : { start } };
|
|
2458
|
+
}
|
|
2459
|
+
default:
|
|
2460
|
+
throw new CliError(
|
|
2461
|
+
ErrorCodes.INVALID_ARG,
|
|
2462
|
+
`Property "${propName}" has unsupported type "${propType}".`,
|
|
2463
|
+
"Supported types: title, rich_text, select, status, multi_select, number, checkbox, url, email, phone_number, date"
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
function buildPropertiesPayload(propStrings, page) {
|
|
2468
|
+
const result = {};
|
|
2469
|
+
for (const propString of propStrings) {
|
|
2470
|
+
const eqIdx = propString.indexOf("=");
|
|
2471
|
+
if (eqIdx === -1) {
|
|
2472
|
+
throw new CliError(
|
|
2473
|
+
ErrorCodes.INVALID_ARG,
|
|
2474
|
+
`Invalid --prop value: "${propString}". Expected format: "PropertyName=Value".`,
|
|
2475
|
+
'Example: --prop "Status=Done"'
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
const propName = propString.slice(0, eqIdx).trim();
|
|
2479
|
+
const value = propString.slice(eqIdx + 1);
|
|
2480
|
+
const schemaProp = page.properties[propName];
|
|
2481
|
+
if (!schemaProp) {
|
|
2482
|
+
const available = Object.keys(page.properties).join(", ");
|
|
2483
|
+
throw new CliError(
|
|
2484
|
+
ErrorCodes.INVALID_ARG,
|
|
2485
|
+
`Property "${propName}" not found on this page.`,
|
|
2486
|
+
`Available properties: ${available}`
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
const propType = schemaProp.type;
|
|
2490
|
+
const payload = buildPropertyUpdate(propName, propType, value);
|
|
2491
|
+
result[propName] = payload;
|
|
2492
|
+
}
|
|
2493
|
+
return result;
|
|
2494
|
+
}
|
|
2495
|
+
async function updatePageProperties(client, pageId, properties) {
|
|
2496
|
+
const response = await client.pages.update({
|
|
2497
|
+
page_id: pageId,
|
|
2498
|
+
properties
|
|
2499
|
+
});
|
|
2500
|
+
return response;
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
// src/commands/update.ts
|
|
2504
|
+
function collectProps(val, acc) {
|
|
2505
|
+
acc.push(val);
|
|
2506
|
+
return acc;
|
|
2507
|
+
}
|
|
2508
|
+
function updateCommand() {
|
|
2509
|
+
const cmd = new Command20("update");
|
|
2510
|
+
cmd.description("update properties on a Notion page").argument("<id/url>", "Notion page ID or URL").option(
|
|
2511
|
+
"--prop <property=value>",
|
|
2512
|
+
"set a property value (repeatable)",
|
|
2513
|
+
collectProps,
|
|
2514
|
+
[]
|
|
2515
|
+
).option("--title <title>", "set the page title").action(
|
|
2516
|
+
withErrorHandling(async (idOrUrl, opts) => {
|
|
2517
|
+
if (opts.title === void 0 && opts.prop.length === 0) {
|
|
2518
|
+
throw new CliError(
|
|
2519
|
+
ErrorCodes.INVALID_ARG,
|
|
2520
|
+
"No properties to update.",
|
|
2521
|
+
'Provide at least one --prop "Name=Value" or --title "New Title"'
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
const { token, source } = await resolveToken();
|
|
2525
|
+
reportTokenSource(source);
|
|
2526
|
+
const client = createNotionClient(token);
|
|
2527
|
+
const id = parseNotionId(idOrUrl);
|
|
2528
|
+
const uuid = toUuid(id);
|
|
2529
|
+
const page = await client.pages.retrieve({
|
|
2530
|
+
page_id: uuid
|
|
2531
|
+
});
|
|
2532
|
+
const properties = buildPropertiesPayload(opts.prop, page);
|
|
2533
|
+
if (opts.title !== void 0) {
|
|
2534
|
+
const titleEntry = Object.entries(page.properties).find(
|
|
2535
|
+
([, prop]) => prop.type === "title"
|
|
2536
|
+
);
|
|
2537
|
+
if (!titleEntry) {
|
|
2538
|
+
throw new CliError(
|
|
2539
|
+
ErrorCodes.INVALID_ARG,
|
|
2540
|
+
"This page has no title property.",
|
|
2541
|
+
"Use --prop to set properties by name instead"
|
|
2542
|
+
);
|
|
2543
|
+
}
|
|
2544
|
+
const [titlePropName] = titleEntry;
|
|
2545
|
+
properties[titlePropName] = {
|
|
2546
|
+
title: [{ type: "text", text: { content: opts.title } }]
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
const updatedPage = await updatePageProperties(
|
|
2550
|
+
client,
|
|
2551
|
+
uuid,
|
|
2552
|
+
properties
|
|
2553
|
+
);
|
|
2554
|
+
const mode = getOutputMode();
|
|
2555
|
+
if (mode === "json") {
|
|
2556
|
+
process.stdout.write(`${formatJSON(updatedPage)}
|
|
2557
|
+
`);
|
|
2558
|
+
} else {
|
|
2559
|
+
process.stdout.write("Page updated.\n");
|
|
2560
|
+
}
|
|
2561
|
+
})
|
|
2562
|
+
);
|
|
2563
|
+
return cmd;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
// src/commands/users.ts
|
|
2567
|
+
import { Command as Command21 } from "commander";
|
|
2273
2568
|
function getEmailOrWorkspace(user) {
|
|
2274
2569
|
if (user.type === "person") {
|
|
2275
2570
|
return user.person.email ?? "\u2014";
|
|
@@ -2281,7 +2576,7 @@ function getEmailOrWorkspace(user) {
|
|
|
2281
2576
|
return "\u2014";
|
|
2282
2577
|
}
|
|
2283
2578
|
function usersCommand() {
|
|
2284
|
-
const cmd = new
|
|
2579
|
+
const cmd = new Command21("users");
|
|
2285
2580
|
cmd.description("list all users in the workspace").option("--json", "output as JSON").action(
|
|
2286
2581
|
withErrorHandling(async (opts) => {
|
|
2287
2582
|
if (opts.json) setOutputMode("json");
|
|
@@ -2312,7 +2607,7 @@ var __dirname = dirname(__filename);
|
|
|
2312
2607
|
var pkg = JSON.parse(
|
|
2313
2608
|
readFileSync(join3(__dirname, "../package.json"), "utf-8")
|
|
2314
2609
|
);
|
|
2315
|
-
var program = new
|
|
2610
|
+
var program = new Command22();
|
|
2316
2611
|
program.name("notion").description("Notion CLI \u2014 read Notion pages and databases from the terminal").version(pkg.version);
|
|
2317
2612
|
program.option("--verbose", "show API requests/responses").option("--color", "force color output").option("--json", "force JSON output (overrides TTY detection)").option("--md", "force markdown output for page content");
|
|
2318
2613
|
program.configureOutput({
|
|
@@ -2333,7 +2628,7 @@ program.hook("preAction", (thisCommand) => {
|
|
|
2333
2628
|
setOutputMode("md");
|
|
2334
2629
|
}
|
|
2335
2630
|
});
|
|
2336
|
-
var authCmd = new
|
|
2631
|
+
var authCmd = new Command22("auth").description("manage Notion authentication");
|
|
2337
2632
|
authCmd.action(authDefaultAction(authCmd));
|
|
2338
2633
|
authCmd.addCommand(loginCommand());
|
|
2339
2634
|
authCmd.addCommand(logoutCommand());
|
|
@@ -2343,7 +2638,7 @@ authCmd.addCommand(profileUseCommand());
|
|
|
2343
2638
|
authCmd.addCommand(profileRemoveCommand());
|
|
2344
2639
|
program.addCommand(authCmd);
|
|
2345
2640
|
program.addCommand(initCommand(), { hidden: true });
|
|
2346
|
-
var profileCmd = new
|
|
2641
|
+
var profileCmd = new Command22("profile").description(
|
|
2347
2642
|
"manage authentication profiles"
|
|
2348
2643
|
);
|
|
2349
2644
|
profileCmd.addCommand(profileListCommand());
|
|
@@ -2360,7 +2655,8 @@ program.addCommand(commentAddCommand());
|
|
|
2360
2655
|
program.addCommand(appendCommand());
|
|
2361
2656
|
program.addCommand(createPageCommand());
|
|
2362
2657
|
program.addCommand(editPageCommand());
|
|
2363
|
-
|
|
2658
|
+
program.addCommand(updateCommand());
|
|
2659
|
+
var dbCmd = new Command22("db").description("Database operations");
|
|
2364
2660
|
dbCmd.addCommand(dbSchemaCommand());
|
|
2365
2661
|
dbCmd.addCommand(dbQueryCommand());
|
|
2366
2662
|
program.addCommand(dbCmd);
|