@andrzejchm/notion-cli 0.2.0 → 0.3.1
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 +45 -23
- package/dist/cli.js +2281 -1946
- package/dist/cli.js.map +1 -1
- package/docs/README.agents.md +11 -8
- package/docs/skills/using-notion-cli/SKILL.md +16 -26
- package/package.json +10 -2
package/dist/cli.js
CHANGED
|
@@ -1,112 +1,236 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { Command as Command20 } from "commander";
|
|
5
|
-
import { fileURLToPath } from "url";
|
|
6
|
-
import { dirname, join as join3 } from "path";
|
|
7
4
|
import { readFileSync } from "fs";
|
|
5
|
+
import { dirname, join as join3 } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { Command as Command19 } from "commander";
|
|
8
8
|
|
|
9
|
-
// src/
|
|
10
|
-
import {
|
|
11
|
-
var _colorForced = false;
|
|
12
|
-
function setColorForced(forced) {
|
|
13
|
-
_colorForced = forced;
|
|
14
|
-
}
|
|
15
|
-
function isColorEnabled() {
|
|
16
|
-
if (process.env.NO_COLOR) return false;
|
|
17
|
-
if (_colorForced) return true;
|
|
18
|
-
return Boolean(process.stderr.isTTY);
|
|
19
|
-
}
|
|
20
|
-
function createChalk() {
|
|
21
|
-
const level = isColorEnabled() ? void 0 : 0;
|
|
22
|
-
return new Chalk({ level });
|
|
23
|
-
}
|
|
24
|
-
function error(msg) {
|
|
25
|
-
return createChalk().red(msg);
|
|
26
|
-
}
|
|
27
|
-
function success(msg) {
|
|
28
|
-
return createChalk().green(msg);
|
|
29
|
-
}
|
|
30
|
-
function dim(msg) {
|
|
31
|
-
return createChalk().dim(msg);
|
|
32
|
-
}
|
|
33
|
-
function bold(msg) {
|
|
34
|
-
return createChalk().bold(msg);
|
|
35
|
-
}
|
|
9
|
+
// src/commands/append.ts
|
|
10
|
+
import { Command } from "commander";
|
|
36
11
|
|
|
37
|
-
// src/
|
|
38
|
-
var
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
12
|
+
// src/blocks/md-to-blocks.ts
|
|
13
|
+
var INLINE_RE = /(\*\*[^*]+\*\*|_[^_]+_|\*[^*]+\*|`[^`]+`|\[[^\]]+\]\([^)]+\)|[^*_`[]+)/g;
|
|
14
|
+
function parseInlineMarkdown(text) {
|
|
15
|
+
const result = [];
|
|
16
|
+
let match;
|
|
17
|
+
INLINE_RE.lastIndex = 0;
|
|
18
|
+
while ((match = INLINE_RE.exec(text)) !== null) {
|
|
19
|
+
const segment = match[0];
|
|
20
|
+
if (segment.startsWith("**") && segment.endsWith("**")) {
|
|
21
|
+
const content = segment.slice(2, -2);
|
|
22
|
+
result.push({
|
|
23
|
+
type: "text",
|
|
24
|
+
text: { content, link: null },
|
|
25
|
+
annotations: {
|
|
26
|
+
bold: true,
|
|
27
|
+
italic: false,
|
|
28
|
+
strikethrough: false,
|
|
29
|
+
underline: false,
|
|
30
|
+
code: false,
|
|
31
|
+
color: "default"
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (segment.startsWith("_") && segment.endsWith("_") || segment.startsWith("*") && segment.endsWith("*")) {
|
|
37
|
+
const content = segment.slice(1, -1);
|
|
38
|
+
result.push({
|
|
39
|
+
type: "text",
|
|
40
|
+
text: { content, link: null },
|
|
41
|
+
annotations: {
|
|
42
|
+
bold: false,
|
|
43
|
+
italic: true,
|
|
44
|
+
strikethrough: false,
|
|
45
|
+
underline: false,
|
|
46
|
+
code: false,
|
|
47
|
+
color: "default"
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (segment.startsWith("`") && segment.endsWith("`")) {
|
|
53
|
+
const content = segment.slice(1, -1);
|
|
54
|
+
result.push({
|
|
55
|
+
type: "text",
|
|
56
|
+
text: { content, link: null },
|
|
57
|
+
annotations: {
|
|
58
|
+
bold: false,
|
|
59
|
+
italic: false,
|
|
60
|
+
strikethrough: false,
|
|
61
|
+
underline: false,
|
|
62
|
+
code: true,
|
|
63
|
+
color: "default"
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const linkMatch = segment.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
69
|
+
if (linkMatch) {
|
|
70
|
+
const [, label, url] = linkMatch;
|
|
71
|
+
result.push({
|
|
72
|
+
type: "text",
|
|
73
|
+
text: { content: label, link: { url } },
|
|
74
|
+
annotations: {
|
|
75
|
+
bold: false,
|
|
76
|
+
italic: false,
|
|
77
|
+
strikethrough: false,
|
|
78
|
+
underline: false,
|
|
79
|
+
code: false,
|
|
80
|
+
color: "default"
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
result.push({
|
|
86
|
+
type: "text",
|
|
87
|
+
text: { content: segment, link: null },
|
|
88
|
+
annotations: {
|
|
89
|
+
bold: false,
|
|
90
|
+
italic: false,
|
|
91
|
+
strikethrough: false,
|
|
92
|
+
underline: false,
|
|
93
|
+
code: false,
|
|
94
|
+
color: "default"
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
67
99
|
}
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
const cap = getColumnCap(header);
|
|
71
|
-
const headerLen = header.length;
|
|
72
|
-
const maxRowLen = rows.reduce((max, row) => {
|
|
73
|
-
const cell = row[colIdx] ?? "";
|
|
74
|
-
return Math.max(max, cell.length);
|
|
75
|
-
}, 0);
|
|
76
|
-
return Math.min(Math.max(headerLen, maxRowLen), cap);
|
|
77
|
-
});
|
|
78
|
-
const sep = "\u2500";
|
|
79
|
-
const colSep = " ";
|
|
80
|
-
const headerRow = headers.map((h, i) => h.padEnd(colWidths[i])).join(colSep);
|
|
81
|
-
const separatorRow = colWidths.map((w) => sep.repeat(w)).join(colSep);
|
|
82
|
-
const dataRows = rows.map(
|
|
83
|
-
(row) => headers.map((_, i) => {
|
|
84
|
-
const cell = row[i] ?? "";
|
|
85
|
-
return truncate(cell, colWidths[i]).padEnd(colWidths[i]);
|
|
86
|
-
}).join(colSep)
|
|
87
|
-
);
|
|
88
|
-
return [headerRow, separatorRow, ...dataRows].join("\n");
|
|
100
|
+
function makeRichText(text) {
|
|
101
|
+
return parseInlineMarkdown(text);
|
|
89
102
|
}
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
function mdToBlocks(md) {
|
|
104
|
+
if (!md) return [];
|
|
105
|
+
const lines = md.split("\n");
|
|
106
|
+
const blocks = [];
|
|
107
|
+
let inFence = false;
|
|
108
|
+
let fenceLang = "";
|
|
109
|
+
const fenceLines = [];
|
|
110
|
+
for (const line of lines) {
|
|
111
|
+
if (!inFence && line.startsWith("```")) {
|
|
112
|
+
inFence = true;
|
|
113
|
+
fenceLang = line.slice(3).trim() || "plain text";
|
|
114
|
+
fenceLines.length = 0;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (inFence) {
|
|
118
|
+
if (line.startsWith("```")) {
|
|
119
|
+
const content = fenceLines.join("\n");
|
|
120
|
+
blocks.push({
|
|
121
|
+
type: "code",
|
|
122
|
+
code: {
|
|
123
|
+
rich_text: [
|
|
124
|
+
{
|
|
125
|
+
type: "text",
|
|
126
|
+
text: { content, link: null },
|
|
127
|
+
annotations: {
|
|
128
|
+
bold: false,
|
|
129
|
+
italic: false,
|
|
130
|
+
strikethrough: false,
|
|
131
|
+
underline: false,
|
|
132
|
+
code: false,
|
|
133
|
+
color: "default"
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
// biome-ignore lint/suspicious/noExplicitAny: Notion SDK language type is too narrow
|
|
138
|
+
language: fenceLang
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
inFence = false;
|
|
142
|
+
fenceLang = "";
|
|
143
|
+
fenceLines.length = 0;
|
|
144
|
+
} else {
|
|
145
|
+
fenceLines.push(line);
|
|
146
|
+
}
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (line.trim() === "") continue;
|
|
150
|
+
const h1 = line.match(/^# (.+)$/);
|
|
151
|
+
if (h1) {
|
|
152
|
+
blocks.push({
|
|
153
|
+
type: "heading_1",
|
|
154
|
+
heading_1: { rich_text: makeRichText(h1[1]) }
|
|
155
|
+
});
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const h2 = line.match(/^## (.+)$/);
|
|
159
|
+
if (h2) {
|
|
160
|
+
blocks.push({
|
|
161
|
+
type: "heading_2",
|
|
162
|
+
heading_2: { rich_text: makeRichText(h2[1]) }
|
|
163
|
+
});
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const h3 = line.match(/^### (.+)$/);
|
|
167
|
+
if (h3) {
|
|
168
|
+
blocks.push({
|
|
169
|
+
type: "heading_3",
|
|
170
|
+
heading_3: { rich_text: makeRichText(h3[1]) }
|
|
171
|
+
});
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const bullet = line.match(/^[-*] (.+)$/);
|
|
175
|
+
if (bullet) {
|
|
176
|
+
blocks.push({
|
|
177
|
+
type: "bulleted_list_item",
|
|
178
|
+
bulleted_list_item: { rich_text: makeRichText(bullet[1]) }
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const numbered = line.match(/^\d+\. (.+)$/);
|
|
183
|
+
if (numbered) {
|
|
184
|
+
blocks.push({
|
|
185
|
+
type: "numbered_list_item",
|
|
186
|
+
numbered_list_item: { rich_text: makeRichText(numbered[1]) }
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const quote = line.match(/^> (.+)$/);
|
|
191
|
+
if (quote) {
|
|
192
|
+
blocks.push({
|
|
193
|
+
type: "quote",
|
|
194
|
+
quote: { rich_text: makeRichText(quote[1]) }
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
blocks.push({
|
|
199
|
+
type: "paragraph",
|
|
200
|
+
paragraph: { rich_text: makeRichText(line) }
|
|
201
|
+
});
|
|
96
202
|
}
|
|
203
|
+
if (inFence && fenceLines.length > 0) {
|
|
204
|
+
const content = fenceLines.join("\n");
|
|
205
|
+
blocks.push({
|
|
206
|
+
type: "code",
|
|
207
|
+
code: {
|
|
208
|
+
rich_text: [
|
|
209
|
+
{
|
|
210
|
+
type: "text",
|
|
211
|
+
text: { content, link: null },
|
|
212
|
+
annotations: {
|
|
213
|
+
bold: false,
|
|
214
|
+
italic: false,
|
|
215
|
+
strikethrough: false,
|
|
216
|
+
underline: false,
|
|
217
|
+
code: false,
|
|
218
|
+
color: "default"
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
],
|
|
222
|
+
// biome-ignore lint/suspicious/noExplicitAny: Notion SDK language type is too narrow
|
|
223
|
+
language: fenceLang
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return blocks;
|
|
97
228
|
}
|
|
98
|
-
function printWithPager(text) {
|
|
99
|
-
process.stdout.write(text);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// src/commands/init.ts
|
|
103
|
-
import { Command } from "commander";
|
|
104
|
-
import { input, password, confirm } from "@inquirer/prompts";
|
|
105
229
|
|
|
106
230
|
// src/errors/cli-error.ts
|
|
107
231
|
var CliError = class extends Error {
|
|
108
|
-
constructor(code, message, suggestion) {
|
|
109
|
-
super(message);
|
|
232
|
+
constructor(code, message, suggestion, cause) {
|
|
233
|
+
super(message, { cause });
|
|
110
234
|
this.code = code;
|
|
111
235
|
this.suggestion = suggestion;
|
|
112
236
|
this.name = "CliError";
|
|
@@ -144,230 +268,42 @@ var ErrorCodes = {
|
|
|
144
268
|
UNKNOWN: "UNKNOWN"
|
|
145
269
|
};
|
|
146
270
|
|
|
147
|
-
// src/config/config.ts
|
|
148
|
-
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
149
|
-
import { parse, stringify } from "yaml";
|
|
150
|
-
|
|
151
|
-
// src/config/paths.ts
|
|
152
|
-
import { homedir } from "os";
|
|
153
|
-
import { join } from "path";
|
|
154
|
-
function getConfigDir() {
|
|
155
|
-
const xdgConfigHome = process.env["XDG_CONFIG_HOME"];
|
|
156
|
-
const base = xdgConfigHome ? xdgConfigHome : join(homedir(), ".config");
|
|
157
|
-
return join(base, "notion-cli");
|
|
158
|
-
}
|
|
159
|
-
function getConfigPath() {
|
|
160
|
-
return join(getConfigDir(), "config.yaml");
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// src/config/config.ts
|
|
164
|
-
async function readGlobalConfig() {
|
|
165
|
-
const configPath = getConfigPath();
|
|
166
|
-
let raw;
|
|
167
|
-
try {
|
|
168
|
-
raw = await readFile(configPath, "utf-8");
|
|
169
|
-
} catch (err) {
|
|
170
|
-
if (err.code === "ENOENT") {
|
|
171
|
-
return {};
|
|
172
|
-
}
|
|
173
|
-
throw new CliError(
|
|
174
|
-
ErrorCodes.CONFIG_READ_ERROR,
|
|
175
|
-
`Failed to read config file: ${configPath}`,
|
|
176
|
-
'Check file permissions or run "notion init" to create a new config'
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
try {
|
|
180
|
-
const parsed = parse(raw);
|
|
181
|
-
return parsed ?? {};
|
|
182
|
-
} catch {
|
|
183
|
-
throw new CliError(
|
|
184
|
-
ErrorCodes.CONFIG_READ_ERROR,
|
|
185
|
-
`Failed to parse config file: ${configPath}`,
|
|
186
|
-
'The config file may be corrupted. Delete it and run "notion init" to start fresh'
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
async function writeGlobalConfig(config) {
|
|
191
|
-
const configDir = getConfigDir();
|
|
192
|
-
const configPath = getConfigPath();
|
|
193
|
-
const tmpPath = `${configPath}.tmp`;
|
|
194
|
-
try {
|
|
195
|
-
await mkdir(configDir, { recursive: true, mode: 448 });
|
|
196
|
-
} catch {
|
|
197
|
-
throw new CliError(
|
|
198
|
-
ErrorCodes.CONFIG_WRITE_ERROR,
|
|
199
|
-
`Failed to create config directory: ${configDir}`,
|
|
200
|
-
"Check that you have write permissions to your home directory"
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
|
-
const content = stringify(config);
|
|
204
|
-
try {
|
|
205
|
-
await writeFile(tmpPath, content, { mode: 384 });
|
|
206
|
-
await rename(tmpPath, configPath);
|
|
207
|
-
} catch {
|
|
208
|
-
throw new CliError(
|
|
209
|
-
ErrorCodes.CONFIG_WRITE_ERROR,
|
|
210
|
-
`Failed to write config file: ${configPath}`,
|
|
211
|
-
"Check file permissions in the config directory"
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// src/notion/client.ts
|
|
217
|
-
import { Client, APIErrorCode, isNotionClientError } from "@notionhq/client";
|
|
218
|
-
async function validateToken(token) {
|
|
219
|
-
const notion = new Client({ auth: token });
|
|
220
|
-
try {
|
|
221
|
-
const me = await notion.users.me({});
|
|
222
|
-
const bot = me;
|
|
223
|
-
const workspaceName = bot.bot?.workspace_name ?? "Unknown Workspace";
|
|
224
|
-
const workspaceId = bot.bot?.workspace_id ?? "";
|
|
225
|
-
return { workspaceName, workspaceId };
|
|
226
|
-
} catch (error2) {
|
|
227
|
-
if (isNotionClientError(error2) && error2.code === APIErrorCode.Unauthorized) {
|
|
228
|
-
throw new CliError(
|
|
229
|
-
ErrorCodes.AUTH_INVALID,
|
|
230
|
-
"Invalid integration token.",
|
|
231
|
-
"Check your token at notion.so/profile/integrations/internal"
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
throw error2;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
function createNotionClient(token) {
|
|
238
|
-
return new Client({ auth: token, timeoutMs: 12e4 });
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// src/output/stderr.ts
|
|
242
|
-
function stderrWrite(msg) {
|
|
243
|
-
process.stderr.write(msg + "\n");
|
|
244
|
-
}
|
|
245
|
-
function reportTokenSource(source) {
|
|
246
|
-
stderrWrite(dim(`Using token from ${source}`));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// src/errors/error-handler.ts
|
|
250
|
-
function mapNotionErrorCode(code) {
|
|
251
|
-
switch (code) {
|
|
252
|
-
case "unauthorized":
|
|
253
|
-
return ErrorCodes.AUTH_INVALID;
|
|
254
|
-
case "rate_limited":
|
|
255
|
-
return ErrorCodes.API_RATE_LIMITED;
|
|
256
|
-
case "object_not_found":
|
|
257
|
-
return ErrorCodes.API_NOT_FOUND;
|
|
258
|
-
default:
|
|
259
|
-
return ErrorCodes.API_ERROR;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
function withErrorHandling(fn) {
|
|
263
|
-
return (async (...args) => {
|
|
264
|
-
try {
|
|
265
|
-
await fn(...args);
|
|
266
|
-
} catch (error2) {
|
|
267
|
-
if (error2 instanceof CliError) {
|
|
268
|
-
process.stderr.write(error2.format() + "\n");
|
|
269
|
-
process.exit(1);
|
|
270
|
-
}
|
|
271
|
-
const { isNotionClientError: isNotionClientError2 } = await import("@notionhq/client");
|
|
272
|
-
if (isNotionClientError2(error2)) {
|
|
273
|
-
const code = mapNotionErrorCode(error2.code);
|
|
274
|
-
const mappedError = new CliError(
|
|
275
|
-
code,
|
|
276
|
-
error2.message,
|
|
277
|
-
code === ErrorCodes.AUTH_INVALID ? 'Run "notion init" to reconfigure your integration token' : void 0
|
|
278
|
-
);
|
|
279
|
-
process.stderr.write(mappedError.format() + "\n");
|
|
280
|
-
process.exit(1);
|
|
281
|
-
}
|
|
282
|
-
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
283
|
-
process.stderr.write(`[${ErrorCodes.UNKNOWN}] ${message}
|
|
284
|
-
`);
|
|
285
|
-
process.exit(1);
|
|
286
|
-
}
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// src/commands/init.ts
|
|
291
|
-
function initCommand() {
|
|
292
|
-
const cmd = new Command("init");
|
|
293
|
-
cmd.description("authenticate with Notion and save a profile").action(withErrorHandling(async () => {
|
|
294
|
-
if (!process.stdin.isTTY) {
|
|
295
|
-
throw new CliError(
|
|
296
|
-
ErrorCodes.AUTH_NO_TOKEN,
|
|
297
|
-
"Cannot run interactive init in non-TTY mode.",
|
|
298
|
-
"Set NOTION_API_TOKEN environment variable or create .notion.yaml"
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
const profileName = await input({
|
|
302
|
-
message: "Profile name:",
|
|
303
|
-
default: "default"
|
|
304
|
-
});
|
|
305
|
-
const token = await password({
|
|
306
|
-
message: "Integration token (from notion.so/profile/integrations/internal):",
|
|
307
|
-
mask: "*"
|
|
308
|
-
});
|
|
309
|
-
stderrWrite("Validating token...");
|
|
310
|
-
const { workspaceName, workspaceId } = await validateToken(token);
|
|
311
|
-
stderrWrite(success(`\u2713 Connected to workspace: ${bold(workspaceName)}`));
|
|
312
|
-
const config = await readGlobalConfig();
|
|
313
|
-
if (config.profiles?.[profileName]) {
|
|
314
|
-
const replace = await confirm({
|
|
315
|
-
message: `Profile "${profileName}" already exists. Replace?`,
|
|
316
|
-
default: false
|
|
317
|
-
});
|
|
318
|
-
if (!replace) {
|
|
319
|
-
stderrWrite("Aborted.");
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
const profiles = config.profiles ?? {};
|
|
324
|
-
profiles[profileName] = {
|
|
325
|
-
token,
|
|
326
|
-
workspace_name: workspaceName,
|
|
327
|
-
workspace_id: workspaceId
|
|
328
|
-
};
|
|
329
|
-
await writeGlobalConfig({
|
|
330
|
-
...config,
|
|
331
|
-
profiles,
|
|
332
|
-
active_profile: profileName
|
|
333
|
-
});
|
|
334
|
-
stderrWrite(success(`Profile "${profileName}" saved and set as active.`));
|
|
335
|
-
stderrWrite(dim("Checking integration access..."));
|
|
336
|
-
try {
|
|
337
|
-
const notion = createNotionClient(token);
|
|
338
|
-
const probe = await notion.search({ page_size: 1 });
|
|
339
|
-
if (probe.results.length === 0) {
|
|
340
|
-
stderrWrite("");
|
|
341
|
-
stderrWrite("\u26A0\uFE0F Your integration has no pages connected.");
|
|
342
|
-
stderrWrite(" To grant access, open any Notion page or database:");
|
|
343
|
-
stderrWrite(" 1. Click \xB7\xB7\xB7 (three dots) in the top-right corner");
|
|
344
|
-
stderrWrite(' 2. Select "Connect to"');
|
|
345
|
-
stderrWrite(` 3. Choose "${workspaceName}"`);
|
|
346
|
-
stderrWrite(" Then re-run any notion command to confirm access.");
|
|
347
|
-
} else {
|
|
348
|
-
stderrWrite(success(`\u2713 Integration has access to content in ${bold(workspaceName)}.`));
|
|
349
|
-
}
|
|
350
|
-
} catch {
|
|
351
|
-
stderrWrite(dim("(Could not verify integration access \u2014 run `notion ls` to check)"));
|
|
352
|
-
}
|
|
353
|
-
stderrWrite("");
|
|
354
|
-
stderrWrite(dim("Write commands (comment, append, create-page) require additional"));
|
|
355
|
-
stderrWrite(dim("capabilities in your integration settings:"));
|
|
356
|
-
stderrWrite(dim(" notion.so/profile/integrations/internal \u2192 your integration \u2192"));
|
|
357
|
-
stderrWrite(dim(' Capabilities: enable "Read content", "Insert content", "Read comments", "Insert comments"'));
|
|
358
|
-
stderrWrite("");
|
|
359
|
-
stderrWrite(dim("To post comments and create pages attributed to your user account:"));
|
|
360
|
-
stderrWrite(dim(" notion auth login"));
|
|
361
|
-
}));
|
|
362
|
-
return cmd;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// src/commands/auth/login.ts
|
|
366
|
-
import { Command as Command2 } from "commander";
|
|
367
|
-
|
|
368
271
|
// src/oauth/oauth-client.ts
|
|
369
|
-
var
|
|
370
|
-
var
|
|
272
|
+
var _k = 90;
|
|
273
|
+
var _d = (parts) => Buffer.from(parts.join(""), "base64").toString().split("").map((c2) => String.fromCharCode(c2.charCodeAt(0) ^ _k)).join("");
|
|
274
|
+
var OAUTH_CLIENT_ID = _d([
|
|
275
|
+
"aWtu",
|
|
276
|
+
"PmJt",
|
|
277
|
+
"aDh3",
|
|
278
|
+
"b2Nu",
|
|
279
|
+
"OXdi",
|
|
280
|
+
"a2I+",
|
|
281
|
+
"d2I5",
|
|
282
|
+
"amh3",
|
|
283
|
+
"ampp",
|
|
284
|
+
"bTtj",
|
|
285
|
+
"Pm44",
|
|
286
|
+
"P2s8"
|
|
287
|
+
]);
|
|
288
|
+
var OAUTH_CLIENT_SECRET = _d([
|
|
289
|
+
"KT85",
|
|
290
|
+
"KD8u",
|
|
291
|
+
"BWMM",
|
|
292
|
+
"axcx",
|
|
293
|
+
"P28P",
|
|
294
|
+
"ahYp",
|
|
295
|
+
"MCti",
|
|
296
|
+
"MQtt",
|
|
297
|
+
"Hj4V",
|
|
298
|
+
"NywV",
|
|
299
|
+
"I2sp",
|
|
300
|
+
"bzlv",
|
|
301
|
+
"ECIK",
|
|
302
|
+
"NTAx",
|
|
303
|
+
"IGwA",
|
|
304
|
+
"ETU7",
|
|
305
|
+
"ahU="
|
|
306
|
+
]);
|
|
371
307
|
var OAUTH_REDIRECT_URI = "http://localhost:54321/oauth/callback";
|
|
372
308
|
function buildAuthUrl(state) {
|
|
373
309
|
const params = new URLSearchParams({
|
|
@@ -380,13 +316,15 @@ function buildAuthUrl(state) {
|
|
|
380
316
|
return `https://api.notion.com/v1/oauth/authorize?${params.toString()}`;
|
|
381
317
|
}
|
|
382
318
|
function basicAuth() {
|
|
383
|
-
return Buffer.from(`${OAUTH_CLIENT_ID}:${OAUTH_CLIENT_SECRET}`).toString(
|
|
319
|
+
return Buffer.from(`${OAUTH_CLIENT_ID}:${OAUTH_CLIENT_SECRET}`).toString(
|
|
320
|
+
"base64"
|
|
321
|
+
);
|
|
384
322
|
}
|
|
385
323
|
async function exchangeCode(code, redirectUri = OAUTH_REDIRECT_URI) {
|
|
386
324
|
const response = await fetch("https://api.notion.com/v1/oauth/token", {
|
|
387
325
|
method: "POST",
|
|
388
326
|
headers: {
|
|
389
|
-
|
|
327
|
+
Authorization: `Basic ${basicAuth()}`,
|
|
390
328
|
"Content-Type": "application/json",
|
|
391
329
|
"Notion-Version": "2022-06-28"
|
|
392
330
|
},
|
|
@@ -417,7 +355,7 @@ async function refreshAccessToken(refreshToken) {
|
|
|
417
355
|
const response = await fetch("https://api.notion.com/v1/oauth/token", {
|
|
418
356
|
method: "POST",
|
|
419
357
|
headers: {
|
|
420
|
-
|
|
358
|
+
Authorization: `Basic ${basicAuth()}`,
|
|
421
359
|
"Content-Type": "application/json",
|
|
422
360
|
"Notion-Version": "2022-06-28"
|
|
423
361
|
},
|
|
@@ -435,229 +373,86 @@ async function refreshAccessToken(refreshToken) {
|
|
|
435
373
|
} catch {
|
|
436
374
|
}
|
|
437
375
|
throw new CliError(
|
|
438
|
-
ErrorCodes.AUTH_INVALID,
|
|
439
|
-
errorMessage,
|
|
440
|
-
'Run "notion auth login" to re-authenticate'
|
|
376
|
+
ErrorCodes.AUTH_INVALID,
|
|
377
|
+
errorMessage,
|
|
378
|
+
'Run "notion auth login" to re-authenticate'
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const data = await response.json();
|
|
382
|
+
return data;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/config/config.ts
|
|
386
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
387
|
+
import { parse, stringify } from "yaml";
|
|
388
|
+
|
|
389
|
+
// src/config/paths.ts
|
|
390
|
+
import { homedir } from "os";
|
|
391
|
+
import { join } from "path";
|
|
392
|
+
function getConfigDir() {
|
|
393
|
+
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
|
394
|
+
const base = xdgConfigHome ? xdgConfigHome : join(homedir(), ".config");
|
|
395
|
+
return join(base, "notion-cli");
|
|
396
|
+
}
|
|
397
|
+
function getConfigPath() {
|
|
398
|
+
return join(getConfigDir(), "config.yaml");
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/config/config.ts
|
|
402
|
+
async function readGlobalConfig() {
|
|
403
|
+
const configPath = getConfigPath();
|
|
404
|
+
let raw;
|
|
405
|
+
try {
|
|
406
|
+
raw = await readFile(configPath, "utf-8");
|
|
407
|
+
} catch (err) {
|
|
408
|
+
if (err.code === "ENOENT") {
|
|
409
|
+
return {};
|
|
410
|
+
}
|
|
411
|
+
throw new CliError(
|
|
412
|
+
ErrorCodes.CONFIG_READ_ERROR,
|
|
413
|
+
`Failed to read config file: ${configPath}`,
|
|
414
|
+
'Check file permissions or run "notion init" to create a new config',
|
|
415
|
+
err
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
const parsed = parse(raw);
|
|
420
|
+
return parsed ?? {};
|
|
421
|
+
} catch (err) {
|
|
422
|
+
throw new CliError(
|
|
423
|
+
ErrorCodes.CONFIG_READ_ERROR,
|
|
424
|
+
`Failed to parse config file: ${configPath}`,
|
|
425
|
+
'The config file may be corrupted. Delete it and run "notion init" to start fresh',
|
|
426
|
+
err
|
|
441
427
|
);
|
|
442
428
|
}
|
|
443
|
-
const data = await response.json();
|
|
444
|
-
return data;
|
|
445
429
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
import { spawn } from "child_process";
|
|
451
|
-
import { createInterface } from "readline";
|
|
452
|
-
function openBrowser(url) {
|
|
453
|
-
const platform = process.platform;
|
|
454
|
-
let cmd;
|
|
455
|
-
let args;
|
|
456
|
-
if (platform === "darwin") {
|
|
457
|
-
cmd = "open";
|
|
458
|
-
args = [url];
|
|
459
|
-
} else if (platform === "win32") {
|
|
460
|
-
cmd = "cmd";
|
|
461
|
-
args = ["/c", "start", url];
|
|
462
|
-
} else {
|
|
463
|
-
cmd = "xdg-open";
|
|
464
|
-
args = [url];
|
|
465
|
-
}
|
|
430
|
+
async function writeGlobalConfig(config) {
|
|
431
|
+
const configDir = getConfigDir();
|
|
432
|
+
const configPath = getConfigPath();
|
|
433
|
+
const tmpPath = `${configPath}.tmp`;
|
|
466
434
|
try {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
435
|
+
await mkdir(configDir, { recursive: true, mode: 448 });
|
|
436
|
+
} catch (err) {
|
|
437
|
+
throw new CliError(
|
|
438
|
+
ErrorCodes.CONFIG_WRITE_ERROR,
|
|
439
|
+
`Failed to create config directory: ${configDir}`,
|
|
440
|
+
"Check that you have write permissions to your home directory",
|
|
441
|
+
err
|
|
442
|
+
);
|
|
475
443
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
input: process.stdin,
|
|
488
|
-
output: process.stderr,
|
|
489
|
-
terminal: false
|
|
490
|
-
});
|
|
491
|
-
return new Promise((resolve, reject) => {
|
|
492
|
-
rl.once("line", (line) => {
|
|
493
|
-
rl.close();
|
|
494
|
-
try {
|
|
495
|
-
const parsed = new URL(line.trim());
|
|
496
|
-
const code = parsed.searchParams.get("code");
|
|
497
|
-
const state = parsed.searchParams.get("state");
|
|
498
|
-
const errorParam = parsed.searchParams.get("error");
|
|
499
|
-
if (errorParam === "access_denied") {
|
|
500
|
-
reject(
|
|
501
|
-
new CliError(
|
|
502
|
-
ErrorCodes.AUTH_INVALID,
|
|
503
|
-
"Notion OAuth access was denied.",
|
|
504
|
-
'Run "notion auth login" to try again'
|
|
505
|
-
)
|
|
506
|
-
);
|
|
507
|
-
return;
|
|
508
|
-
}
|
|
509
|
-
if (!code || !state) {
|
|
510
|
-
reject(
|
|
511
|
-
new CliError(
|
|
512
|
-
ErrorCodes.AUTH_INVALID,
|
|
513
|
-
"Invalid redirect URL \u2014 missing code or state parameter.",
|
|
514
|
-
"Make sure you paste the full redirect URL from the browser address bar"
|
|
515
|
-
)
|
|
516
|
-
);
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
519
|
-
resolve({ code, state });
|
|
520
|
-
} catch {
|
|
521
|
-
reject(
|
|
522
|
-
new CliError(
|
|
523
|
-
ErrorCodes.AUTH_INVALID,
|
|
524
|
-
"Could not parse the pasted URL.",
|
|
525
|
-
"Make sure you paste the full redirect URL from the browser address bar"
|
|
526
|
-
)
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
});
|
|
530
|
-
rl.once("close", () => {
|
|
531
|
-
reject(
|
|
532
|
-
new CliError(
|
|
533
|
-
ErrorCodes.AUTH_INVALID,
|
|
534
|
-
"No redirect URL received.",
|
|
535
|
-
'Run "notion auth login" to try again'
|
|
536
|
-
)
|
|
537
|
-
);
|
|
538
|
-
});
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
async function runOAuthFlow(options) {
|
|
542
|
-
const state = randomBytes(16).toString("hex");
|
|
543
|
-
const authUrl = buildAuthUrl(state);
|
|
544
|
-
if (options?.manual) {
|
|
545
|
-
return manualFlow(authUrl);
|
|
444
|
+
const content = stringify(config);
|
|
445
|
+
try {
|
|
446
|
+
await writeFile(tmpPath, content, { mode: 384 });
|
|
447
|
+
await rename(tmpPath, configPath);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
throw new CliError(
|
|
450
|
+
ErrorCodes.CONFIG_WRITE_ERROR,
|
|
451
|
+
`Failed to write config file: ${configPath}`,
|
|
452
|
+
"Check file permissions in the config directory",
|
|
453
|
+
err
|
|
454
|
+
);
|
|
546
455
|
}
|
|
547
|
-
return new Promise((resolve, reject) => {
|
|
548
|
-
let settled = false;
|
|
549
|
-
let timeoutHandle = null;
|
|
550
|
-
const server = createServer((req, res) => {
|
|
551
|
-
if (settled) {
|
|
552
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
553
|
-
res.end("<html><body><h1>Already handled. You can close this tab.</h1></body></html>");
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
try {
|
|
557
|
-
const reqUrl = new URL(req.url ?? "/", `http://localhost:54321`);
|
|
558
|
-
const code = reqUrl.searchParams.get("code");
|
|
559
|
-
const returnedState = reqUrl.searchParams.get("state");
|
|
560
|
-
const errorParam = reqUrl.searchParams.get("error");
|
|
561
|
-
if (errorParam === "access_denied") {
|
|
562
|
-
settled = true;
|
|
563
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
564
|
-
res.end(
|
|
565
|
-
"<html><body><h1>Access Denied</h1><p>You cancelled the Notion OAuth request. You can close this tab.</p></body></html>"
|
|
566
|
-
);
|
|
567
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
568
|
-
server.close(() => {
|
|
569
|
-
reject(
|
|
570
|
-
new CliError(
|
|
571
|
-
ErrorCodes.AUTH_INVALID,
|
|
572
|
-
"Notion OAuth access was denied.",
|
|
573
|
-
'Run "notion auth login" to try again'
|
|
574
|
-
)
|
|
575
|
-
);
|
|
576
|
-
});
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
if (!code || !returnedState) {
|
|
580
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
581
|
-
res.end("<html><body><p>Waiting for OAuth callback...</p></body></html>");
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
if (returnedState !== state) {
|
|
585
|
-
settled = true;
|
|
586
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
587
|
-
res.end(
|
|
588
|
-
"<html><body><h1>Security Error</h1><p>State mismatch \u2014 possible CSRF attempt. You can close this tab.</p></body></html>"
|
|
589
|
-
);
|
|
590
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
591
|
-
server.close(() => {
|
|
592
|
-
reject(
|
|
593
|
-
new CliError(
|
|
594
|
-
ErrorCodes.AUTH_INVALID,
|
|
595
|
-
"OAuth state mismatch \u2014 possible CSRF attempt. Aborting.",
|
|
596
|
-
'Run "notion auth login" to start a fresh OAuth flow'
|
|
597
|
-
)
|
|
598
|
-
);
|
|
599
|
-
});
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
settled = true;
|
|
603
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
604
|
-
res.end(
|
|
605
|
-
"<html><body><h1>Authenticated!</h1><p>You can close this tab and return to the terminal.</p></body></html>"
|
|
606
|
-
);
|
|
607
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
608
|
-
server.close(() => {
|
|
609
|
-
resolve({ code, state: returnedState });
|
|
610
|
-
});
|
|
611
|
-
} catch {
|
|
612
|
-
res.writeHead(500, { "Content-Type": "text/html" });
|
|
613
|
-
res.end("<html><body><h1>Error processing callback</h1></body></html>");
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
server.on("error", (err) => {
|
|
617
|
-
if (settled) return;
|
|
618
|
-
settled = true;
|
|
619
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
620
|
-
reject(
|
|
621
|
-
new CliError(
|
|
622
|
-
ErrorCodes.AUTH_INVALID,
|
|
623
|
-
`Failed to start OAuth callback server: ${err.message}`,
|
|
624
|
-
"Make sure port 54321 is not in use, or use --manual flag"
|
|
625
|
-
)
|
|
626
|
-
);
|
|
627
|
-
});
|
|
628
|
-
server.listen(54321, "127.0.0.1", () => {
|
|
629
|
-
const browserOpened = openBrowser(authUrl);
|
|
630
|
-
if (!browserOpened) {
|
|
631
|
-
server.close();
|
|
632
|
-
settled = true;
|
|
633
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
634
|
-
manualFlow(authUrl).then(resolve, reject);
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
process.stderr.write(
|
|
638
|
-
`
|
|
639
|
-
Opening browser for Notion OAuth...
|
|
640
|
-
If your browser didn't open, visit:
|
|
641
|
-
${authUrl}
|
|
642
|
-
|
|
643
|
-
Waiting for callback (up to 120 seconds)...
|
|
644
|
-
`
|
|
645
|
-
);
|
|
646
|
-
timeoutHandle = setTimeout(() => {
|
|
647
|
-
if (settled) return;
|
|
648
|
-
settled = true;
|
|
649
|
-
server.close(() => {
|
|
650
|
-
reject(
|
|
651
|
-
new CliError(
|
|
652
|
-
ErrorCodes.AUTH_INVALID,
|
|
653
|
-
"OAuth login timed out after 120 seconds.",
|
|
654
|
-
'Run "notion auth login" to try again, or use --manual flag'
|
|
655
|
-
)
|
|
656
|
-
);
|
|
657
|
-
});
|
|
658
|
-
}, 12e4);
|
|
659
|
-
});
|
|
660
|
-
});
|
|
661
456
|
}
|
|
662
457
|
|
|
663
458
|
// src/oauth/token-store.ts
|
|
@@ -672,8 +467,12 @@ async function saveOAuthTokens(profileName, response) {
|
|
|
672
467
|
oauth_expiry_ms: Date.now() + OAUTH_EXPIRY_DURATION_MS,
|
|
673
468
|
workspace_id: response.workspace_id,
|
|
674
469
|
workspace_name: response.workspace_name,
|
|
675
|
-
...response.owner?.user?.id != null && {
|
|
676
|
-
|
|
470
|
+
...response.owner?.user?.id != null && {
|
|
471
|
+
oauth_user_id: response.owner.user.id
|
|
472
|
+
},
|
|
473
|
+
...response.owner?.user?.name != null && {
|
|
474
|
+
oauth_user_name: response.owner.user.name
|
|
475
|
+
}
|
|
677
476
|
};
|
|
678
477
|
config.profiles = {
|
|
679
478
|
...config.profiles,
|
|
@@ -700,610 +499,954 @@ async function clearOAuthTokens(profileName) {
|
|
|
700
499
|
await writeGlobalConfig(config);
|
|
701
500
|
}
|
|
702
501
|
|
|
703
|
-
// src/
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
502
|
+
// src/config/local-config.ts
|
|
503
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
504
|
+
import { join as join2 } from "path";
|
|
505
|
+
import { parse as parse2 } from "yaml";
|
|
506
|
+
async function readLocalConfig() {
|
|
507
|
+
const localConfigPath = join2(process.cwd(), ".notion.yaml");
|
|
508
|
+
let raw;
|
|
509
|
+
try {
|
|
510
|
+
raw = await readFile2(localConfigPath, "utf-8");
|
|
511
|
+
} catch (err) {
|
|
512
|
+
if (err.code === "ENOENT") {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
throw new CliError(
|
|
516
|
+
ErrorCodes.CONFIG_READ_ERROR,
|
|
517
|
+
`Failed to read local config: ${localConfigPath}`,
|
|
518
|
+
"Check file permissions",
|
|
519
|
+
err
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
let parsed;
|
|
523
|
+
try {
|
|
524
|
+
parsed = parse2(raw) ?? {};
|
|
525
|
+
} catch (err) {
|
|
526
|
+
throw new CliError(
|
|
527
|
+
ErrorCodes.CONFIG_INVALID,
|
|
528
|
+
`Failed to parse .notion.yaml`,
|
|
529
|
+
"Check that the file contains valid YAML",
|
|
530
|
+
err
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
if (parsed.profile !== void 0 && parsed.token !== void 0) {
|
|
534
|
+
throw new CliError(
|
|
535
|
+
ErrorCodes.CONFIG_INVALID,
|
|
536
|
+
'.notion.yaml cannot specify both "profile" and "token"',
|
|
537
|
+
'Use either "profile: <name>" to reference a saved profile, or "token: <value>" for a direct token'
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
return parsed;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// src/config/token.ts
|
|
544
|
+
function isOAuthExpired(profile) {
|
|
545
|
+
if (profile.oauth_expiry_ms == null) return false;
|
|
546
|
+
return Date.now() >= profile.oauth_expiry_ms;
|
|
547
|
+
}
|
|
548
|
+
async function resolveOAuthToken(profileName, profile) {
|
|
549
|
+
if (!profile.oauth_access_token) return null;
|
|
550
|
+
if (!isOAuthExpired(profile)) {
|
|
551
|
+
return profile.oauth_access_token;
|
|
552
|
+
}
|
|
553
|
+
if (!profile.oauth_refresh_token) {
|
|
554
|
+
await clearOAuthTokens(profileName);
|
|
555
|
+
throw new CliError(
|
|
556
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
557
|
+
"OAuth session expired and no refresh token is available.",
|
|
558
|
+
'Run "notion auth login" to re-authenticate'
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
try {
|
|
562
|
+
const refreshed = await refreshAccessToken(profile.oauth_refresh_token);
|
|
563
|
+
await saveOAuthTokens(profileName, refreshed);
|
|
564
|
+
return refreshed.access_token;
|
|
565
|
+
} catch (err) {
|
|
566
|
+
await clearOAuthTokens(profileName);
|
|
567
|
+
throw new CliError(
|
|
568
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
569
|
+
'OAuth session expired. Run "notion auth login" to re-authenticate.',
|
|
570
|
+
"Your session was revoked or the refresh token has expired",
|
|
571
|
+
err
|
|
572
|
+
);
|
|
573
|
+
}
|
|
730
574
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
575
|
+
async function resolveProfileToken(profileName, profile) {
|
|
576
|
+
const oauthToken = await resolveOAuthToken(profileName, profile);
|
|
577
|
+
if (oauthToken) {
|
|
578
|
+
return { token: oauthToken, source: "oauth" };
|
|
579
|
+
}
|
|
580
|
+
if (profile.token) {
|
|
581
|
+
return { token: profile.token, source: `profile: ${profileName}` };
|
|
582
|
+
}
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
async function resolveToken() {
|
|
586
|
+
const envToken = process.env.NOTION_API_TOKEN;
|
|
587
|
+
if (envToken) {
|
|
588
|
+
return { token: envToken, source: "NOTION_API_TOKEN" };
|
|
589
|
+
}
|
|
590
|
+
const localConfig = await readLocalConfig();
|
|
591
|
+
if (localConfig !== null) {
|
|
592
|
+
if (localConfig.token) {
|
|
593
|
+
return { token: localConfig.token, source: ".notion.yaml" };
|
|
594
|
+
}
|
|
595
|
+
if (localConfig.profile) {
|
|
596
|
+
const globalConfig2 = await readGlobalConfig();
|
|
597
|
+
const profile = globalConfig2.profiles?.[localConfig.profile];
|
|
598
|
+
if (profile) {
|
|
599
|
+
const result = await resolveProfileToken(localConfig.profile, profile);
|
|
600
|
+
if (result) return result;
|
|
748
601
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const globalConfig = await readGlobalConfig();
|
|
605
|
+
if (globalConfig.active_profile) {
|
|
606
|
+
const profile = globalConfig.profiles?.[globalConfig.active_profile];
|
|
607
|
+
if (profile) {
|
|
608
|
+
const result = await resolveProfileToken(
|
|
609
|
+
globalConfig.active_profile,
|
|
610
|
+
profile
|
|
755
611
|
);
|
|
756
|
-
|
|
612
|
+
if (result) return result;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
throw new CliError(
|
|
616
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
617
|
+
"No authentication token found.",
|
|
618
|
+
'Run "notion init" to set up a profile'
|
|
757
619
|
);
|
|
758
|
-
return cmd;
|
|
759
620
|
}
|
|
760
621
|
|
|
761
|
-
// src/
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
const expiryDate = new Date(profile.oauth_expiry_ms).toISOString();
|
|
784
|
-
stderrWrite(dim(` Access token expires: ${expiryDate}`));
|
|
785
|
-
}
|
|
786
|
-
} else {
|
|
787
|
-
stderrWrite(
|
|
788
|
-
` OAuth: ${error("\u2717")} Not logged in (run 'notion auth login')`
|
|
789
|
-
);
|
|
790
|
-
}
|
|
791
|
-
if (profile.token) {
|
|
792
|
-
const tokenPreview = profile.token.substring(0, 10) + "...";
|
|
793
|
-
stderrWrite(` Internal token: ${success("\u2713")} Configured (${tokenPreview})`);
|
|
794
|
-
} else {
|
|
795
|
-
stderrWrite(` Internal token: ${error("\u2717")} Not configured`);
|
|
622
|
+
// src/errors/error-handler.ts
|
|
623
|
+
function mapNotionErrorCode(code) {
|
|
624
|
+
switch (code) {
|
|
625
|
+
case "unauthorized":
|
|
626
|
+
return ErrorCodes.AUTH_INVALID;
|
|
627
|
+
case "rate_limited":
|
|
628
|
+
return ErrorCodes.API_RATE_LIMITED;
|
|
629
|
+
case "object_not_found":
|
|
630
|
+
return ErrorCodes.API_NOT_FOUND;
|
|
631
|
+
default:
|
|
632
|
+
return ErrorCodes.API_ERROR;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function withErrorHandling(fn) {
|
|
636
|
+
return (async (...args) => {
|
|
637
|
+
try {
|
|
638
|
+
await fn(...args);
|
|
639
|
+
} catch (error2) {
|
|
640
|
+
if (error2 instanceof CliError) {
|
|
641
|
+
process.stderr.write(`${error2.format()}
|
|
642
|
+
`);
|
|
643
|
+
process.exit(1);
|
|
796
644
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
645
|
+
const { isNotionClientError: isNotionClientError2 } = await import("@notionhq/client");
|
|
646
|
+
if (isNotionClientError2(error2)) {
|
|
647
|
+
const code = mapNotionErrorCode(error2.code);
|
|
648
|
+
const mappedError = new CliError(
|
|
649
|
+
code,
|
|
650
|
+
error2.message,
|
|
651
|
+
code === ErrorCodes.AUTH_INVALID ? 'Run "notion init" to reconfigure your integration token' : void 0
|
|
804
652
|
);
|
|
653
|
+
process.stderr.write(`${mappedError.format()}
|
|
654
|
+
`);
|
|
655
|
+
process.exit(1);
|
|
805
656
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
return cmd;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// src/commands/profile/list.ts
|
|
812
|
-
import { Command as Command5 } from "commander";
|
|
813
|
-
function profileListCommand() {
|
|
814
|
-
const cmd = new Command5("list");
|
|
815
|
-
cmd.description("list all authentication profiles").action(withErrorHandling(async () => {
|
|
816
|
-
const config = await readGlobalConfig();
|
|
817
|
-
const profiles = config.profiles ?? {};
|
|
818
|
-
const profileNames = Object.keys(profiles);
|
|
819
|
-
if (profileNames.length === 0) {
|
|
820
|
-
process.stdout.write("No profiles configured. Run `notion init` to get started.\n");
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
for (const name of profileNames) {
|
|
824
|
-
const profile = profiles[name];
|
|
825
|
-
const isActive = config.active_profile === name;
|
|
826
|
-
const marker = isActive ? bold("* ") : " ";
|
|
827
|
-
const activeLabel = isActive ? " (active)" : "";
|
|
828
|
-
const workspaceInfo = profile.workspace_name ? dim(` \u2014 ${profile.workspace_name}`) : "";
|
|
829
|
-
process.stdout.write(`${marker}${name}${activeLabel}${workspaceInfo}
|
|
657
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
658
|
+
process.stderr.write(`[${ErrorCodes.UNKNOWN}] ${message}
|
|
830
659
|
`);
|
|
660
|
+
process.exit(1);
|
|
831
661
|
}
|
|
832
|
-
})
|
|
833
|
-
return cmd;
|
|
662
|
+
});
|
|
834
663
|
}
|
|
835
664
|
|
|
836
|
-
// src/
|
|
837
|
-
import {
|
|
838
|
-
function
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
const
|
|
842
|
-
const
|
|
843
|
-
|
|
665
|
+
// src/notion/client.ts
|
|
666
|
+
import { APIErrorCode, Client, isNotionClientError } from "@notionhq/client";
|
|
667
|
+
async function validateToken(token) {
|
|
668
|
+
const notion = new Client({ auth: token });
|
|
669
|
+
try {
|
|
670
|
+
const me = await notion.users.me({});
|
|
671
|
+
const bot = me;
|
|
672
|
+
const workspaceName = bot.bot?.workspace_name ?? "Unknown Workspace";
|
|
673
|
+
const workspaceId = bot.bot?.workspace_id ?? "";
|
|
674
|
+
return { workspaceName, workspaceId };
|
|
675
|
+
} catch (error2) {
|
|
676
|
+
if (isNotionClientError(error2) && error2.code === APIErrorCode.Unauthorized) {
|
|
844
677
|
throw new CliError(
|
|
845
|
-
ErrorCodes.
|
|
846
|
-
|
|
847
|
-
|
|
678
|
+
ErrorCodes.AUTH_INVALID,
|
|
679
|
+
"Invalid integration token.",
|
|
680
|
+
"Check your token at notion.so/profile/integrations/internal",
|
|
681
|
+
error2
|
|
848
682
|
);
|
|
849
683
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
active_profile: name
|
|
853
|
-
});
|
|
854
|
-
stderrWrite(success(`Switched to profile "${name}".`));
|
|
855
|
-
}));
|
|
856
|
-
return cmd;
|
|
684
|
+
throw error2;
|
|
685
|
+
}
|
|
857
686
|
}
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
import { Command as Command7 } from "commander";
|
|
861
|
-
function profileRemoveCommand() {
|
|
862
|
-
const cmd = new Command7("remove");
|
|
863
|
-
cmd.description("remove an authentication profile").argument("<name>", "profile name to remove").action(withErrorHandling(async (name) => {
|
|
864
|
-
const config = await readGlobalConfig();
|
|
865
|
-
const profiles = { ...config.profiles ?? {} };
|
|
866
|
-
if (!profiles[name]) {
|
|
867
|
-
throw new CliError(
|
|
868
|
-
ErrorCodes.AUTH_PROFILE_NOT_FOUND,
|
|
869
|
-
`Profile "${name}" not found.`,
|
|
870
|
-
`Run "notion profile list" to see available profiles`
|
|
871
|
-
);
|
|
872
|
-
}
|
|
873
|
-
delete profiles[name];
|
|
874
|
-
const newActiveProfile = config.active_profile === name ? void 0 : config.active_profile;
|
|
875
|
-
await writeGlobalConfig({
|
|
876
|
-
...config,
|
|
877
|
-
profiles,
|
|
878
|
-
active_profile: newActiveProfile
|
|
879
|
-
});
|
|
880
|
-
stderrWrite(success(`Profile "${name}" removed.`));
|
|
881
|
-
}));
|
|
882
|
-
return cmd;
|
|
687
|
+
function createNotionClient(token) {
|
|
688
|
+
return new Client({ auth: token, timeoutMs: 12e4 });
|
|
883
689
|
}
|
|
884
690
|
|
|
885
|
-
// src/
|
|
886
|
-
|
|
887
|
-
var
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
case "$prev" in
|
|
896
|
-
notion)
|
|
897
|
-
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
898
|
-
return 0
|
|
899
|
-
;;
|
|
900
|
-
profile)
|
|
901
|
-
COMPREPLY=($(compgen -W "$profile_commands" -- "$cur"))
|
|
902
|
-
return 0
|
|
903
|
-
;;
|
|
904
|
-
completion)
|
|
905
|
-
COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))
|
|
906
|
-
return 0
|
|
907
|
-
;;
|
|
908
|
-
esac
|
|
909
|
-
|
|
910
|
-
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
691
|
+
// src/notion/url-parser.ts
|
|
692
|
+
var NOTION_ID_REGEX = /^[0-9a-f]{32}$/i;
|
|
693
|
+
var UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
694
|
+
var NOTION_URL_REGEX = /https?:\/\/(?:[a-zA-Z0-9-]+\.)?notion\.(?:so|site)\/.*?([0-9a-f]{32})(?:[?#]|$)/i;
|
|
695
|
+
function throwInvalidId(input2) {
|
|
696
|
+
throw new CliError(
|
|
697
|
+
ErrorCodes.INVALID_ID,
|
|
698
|
+
`Cannot parse Notion ID from: ${input2}`,
|
|
699
|
+
"Provide a valid Notion URL or page/database ID"
|
|
700
|
+
);
|
|
911
701
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
global_opts=(
|
|
929
|
-
'--help[display help]'
|
|
930
|
-
'--version[output version]'
|
|
931
|
-
'--verbose[show API requests/responses]'
|
|
932
|
-
'--color[force color output]'
|
|
933
|
-
)
|
|
934
|
-
|
|
935
|
-
if (( CURRENT == 2 )); then
|
|
936
|
-
_describe 'command' commands
|
|
937
|
-
_arguments $global_opts
|
|
938
|
-
return
|
|
939
|
-
fi
|
|
940
|
-
|
|
941
|
-
case $words[2] in
|
|
942
|
-
profile)
|
|
943
|
-
local -a profile_cmds
|
|
944
|
-
profile_cmds=(
|
|
945
|
-
'list:list all authentication profiles'
|
|
946
|
-
'use:switch the active profile'
|
|
947
|
-
'remove:remove an authentication profile'
|
|
948
|
-
)
|
|
949
|
-
_describe 'profile command' profile_cmds
|
|
950
|
-
;;
|
|
951
|
-
completion)
|
|
952
|
-
local -a shells
|
|
953
|
-
shells=('bash' 'zsh' 'fish')
|
|
954
|
-
_describe 'shell' shells
|
|
955
|
-
;;
|
|
956
|
-
esac
|
|
702
|
+
function parseNotionId(input2) {
|
|
703
|
+
if (!input2) throwInvalidId(input2);
|
|
704
|
+
if (NOTION_ID_REGEX.test(input2)) {
|
|
705
|
+
return input2.toLowerCase();
|
|
706
|
+
}
|
|
707
|
+
if (UUID_REGEX.test(input2)) {
|
|
708
|
+
return input2.replace(/-/g, "").toLowerCase();
|
|
709
|
+
}
|
|
710
|
+
const urlMatch = NOTION_URL_REGEX.exec(input2);
|
|
711
|
+
if (urlMatch) {
|
|
712
|
+
return urlMatch[1].toLowerCase();
|
|
713
|
+
}
|
|
714
|
+
throwInvalidId(input2);
|
|
715
|
+
}
|
|
716
|
+
function toUuid(id) {
|
|
717
|
+
return `${id.slice(0, 8)}-${id.slice(8, 12)}-${id.slice(12, 16)}-${id.slice(16, 20)}-${id.slice(20)}`;
|
|
957
718
|
}
|
|
958
719
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
var
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
720
|
+
// src/output/color.ts
|
|
721
|
+
import { Chalk } from "chalk";
|
|
722
|
+
var _colorForced = false;
|
|
723
|
+
function setColorForced(forced) {
|
|
724
|
+
_colorForced = forced;
|
|
725
|
+
}
|
|
726
|
+
function isColorEnabled() {
|
|
727
|
+
if (process.env.NO_COLOR) return false;
|
|
728
|
+
if (_colorForced) return true;
|
|
729
|
+
return Boolean(process.stderr.isTTY);
|
|
730
|
+
}
|
|
731
|
+
function createChalk() {
|
|
732
|
+
const level = isColorEnabled() ? void 0 : 0;
|
|
733
|
+
return new Chalk({ level });
|
|
734
|
+
}
|
|
735
|
+
function error(msg) {
|
|
736
|
+
return createChalk().red(msg);
|
|
737
|
+
}
|
|
738
|
+
function success(msg) {
|
|
739
|
+
return createChalk().green(msg);
|
|
740
|
+
}
|
|
741
|
+
function dim(msg) {
|
|
742
|
+
return createChalk().dim(msg);
|
|
743
|
+
}
|
|
744
|
+
function bold(msg) {
|
|
745
|
+
return createChalk().bold(msg);
|
|
746
|
+
}
|
|
971
747
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
748
|
+
// src/output/stderr.ts
|
|
749
|
+
function stderrWrite(msg) {
|
|
750
|
+
process.stderr.write(`${msg}
|
|
751
|
+
`);
|
|
752
|
+
}
|
|
753
|
+
function reportTokenSource(source) {
|
|
754
|
+
stderrWrite(dim(`Using token from ${source}`));
|
|
755
|
+
}
|
|
976
756
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
757
|
+
// src/services/write.service.ts
|
|
758
|
+
async function addComment(client, pageId, text, options = {}) {
|
|
759
|
+
await client.comments.create({
|
|
760
|
+
parent: { page_id: pageId },
|
|
761
|
+
rich_text: [
|
|
762
|
+
{
|
|
763
|
+
type: "text",
|
|
764
|
+
text: { content: text, link: null },
|
|
765
|
+
annotations: {
|
|
766
|
+
bold: false,
|
|
767
|
+
italic: false,
|
|
768
|
+
strikethrough: false,
|
|
769
|
+
underline: false,
|
|
770
|
+
code: false,
|
|
771
|
+
color: "default"
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
],
|
|
775
|
+
...options.asUser && { display_name: { type: "user" } }
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
async function appendBlocks(client, blockId, blocks) {
|
|
779
|
+
await client.blocks.children.append({
|
|
780
|
+
block_id: blockId,
|
|
781
|
+
children: blocks
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
async function createPage(client, parentId, title, blocks) {
|
|
785
|
+
const response = await client.pages.create({
|
|
786
|
+
parent: { type: "page_id", page_id: parentId },
|
|
787
|
+
properties: {
|
|
788
|
+
title: {
|
|
789
|
+
title: [{ type: "text", text: { content: title, link: null } }]
|
|
790
|
+
}
|
|
791
|
+
},
|
|
792
|
+
children: blocks
|
|
793
|
+
});
|
|
794
|
+
return response.url;
|
|
795
|
+
}
|
|
981
796
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
ErrorCodes.UNKNOWN,
|
|
1003
|
-
`Unknown shell: "${shell}".`,
|
|
1004
|
-
"Supported shells: bash, zsh, fish"
|
|
1005
|
-
);
|
|
1006
|
-
}
|
|
1007
|
-
}));
|
|
797
|
+
// src/commands/append.ts
|
|
798
|
+
function appendCommand() {
|
|
799
|
+
const cmd = new Command("append");
|
|
800
|
+
cmd.description("append markdown content to a Notion page").argument("<id/url>", "Notion page ID or URL").requiredOption("-m, --message <markdown>", "markdown content to append").action(
|
|
801
|
+
withErrorHandling(async (idOrUrl, opts) => {
|
|
802
|
+
const { token, source } = await resolveToken();
|
|
803
|
+
reportTokenSource(source);
|
|
804
|
+
const client = createNotionClient(token);
|
|
805
|
+
const pageId = parseNotionId(idOrUrl);
|
|
806
|
+
const uuid = toUuid(pageId);
|
|
807
|
+
const blocks = mdToBlocks(opts.message);
|
|
808
|
+
if (blocks.length === 0) {
|
|
809
|
+
process.stdout.write("Nothing to append.\n");
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
await appendBlocks(client, uuid, blocks);
|
|
813
|
+
process.stdout.write(`Appended ${blocks.length} block(s).
|
|
814
|
+
`);
|
|
815
|
+
})
|
|
816
|
+
);
|
|
1008
817
|
return cmd;
|
|
1009
818
|
}
|
|
1010
819
|
|
|
1011
|
-
// src/commands/
|
|
1012
|
-
|
|
1013
|
-
|
|
820
|
+
// src/commands/auth/index.ts
|
|
821
|
+
function authDefaultAction(authCmd2) {
|
|
822
|
+
return async () => {
|
|
823
|
+
authCmd2.help();
|
|
824
|
+
};
|
|
825
|
+
}
|
|
1014
826
|
|
|
1015
|
-
// src/
|
|
1016
|
-
import {
|
|
1017
|
-
import {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
827
|
+
// src/commands/auth/login.ts
|
|
828
|
+
import { select } from "@inquirer/prompts";
|
|
829
|
+
import { Command as Command3 } from "commander";
|
|
830
|
+
|
|
831
|
+
// src/oauth/oauth-flow.ts
|
|
832
|
+
import { spawn } from "child_process";
|
|
833
|
+
import { randomBytes } from "crypto";
|
|
834
|
+
import {
|
|
835
|
+
createServer
|
|
836
|
+
} from "http";
|
|
837
|
+
import { createInterface } from "readline";
|
|
838
|
+
function openBrowser(url) {
|
|
839
|
+
const platform = process.platform;
|
|
840
|
+
let cmd;
|
|
841
|
+
let args;
|
|
842
|
+
if (platform === "darwin") {
|
|
843
|
+
cmd = "open";
|
|
844
|
+
args = [url];
|
|
845
|
+
} else if (platform === "win32") {
|
|
846
|
+
cmd = "cmd";
|
|
847
|
+
args = ["/c", "start", url];
|
|
848
|
+
} else {
|
|
849
|
+
cmd = "xdg-open";
|
|
850
|
+
args = [url];
|
|
1033
851
|
}
|
|
1034
|
-
let parsed;
|
|
1035
852
|
try {
|
|
1036
|
-
|
|
853
|
+
const child = spawn(cmd, args, {
|
|
854
|
+
detached: true,
|
|
855
|
+
stdio: "ignore"
|
|
856
|
+
});
|
|
857
|
+
child.unref();
|
|
858
|
+
return true;
|
|
1037
859
|
} catch {
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
async function manualFlow(url) {
|
|
864
|
+
process.stderr.write(
|
|
865
|
+
`
|
|
866
|
+
Opening browser to:
|
|
867
|
+
${url}
|
|
868
|
+
|
|
869
|
+
Paste the full redirect URL here (${OAUTH_REDIRECT_URI}?code=...):
|
|
870
|
+
> `
|
|
871
|
+
);
|
|
872
|
+
const rl = createInterface({
|
|
873
|
+
input: process.stdin,
|
|
874
|
+
output: process.stderr,
|
|
875
|
+
terminal: false
|
|
876
|
+
});
|
|
877
|
+
return new Promise((resolve, reject) => {
|
|
878
|
+
rl.once("line", (line) => {
|
|
879
|
+
rl.close();
|
|
880
|
+
try {
|
|
881
|
+
const parsed = new URL(line.trim());
|
|
882
|
+
const code = parsed.searchParams.get("code");
|
|
883
|
+
const state = parsed.searchParams.get("state");
|
|
884
|
+
const errorParam = parsed.searchParams.get("error");
|
|
885
|
+
if (errorParam === "access_denied") {
|
|
886
|
+
reject(
|
|
887
|
+
new CliError(
|
|
888
|
+
ErrorCodes.AUTH_INVALID,
|
|
889
|
+
"Notion OAuth access was denied.",
|
|
890
|
+
'Run "notion auth login" to try again'
|
|
891
|
+
)
|
|
892
|
+
);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
if (!code || !state) {
|
|
896
|
+
reject(
|
|
897
|
+
new CliError(
|
|
898
|
+
ErrorCodes.AUTH_INVALID,
|
|
899
|
+
"Invalid redirect URL \u2014 missing code or state parameter.",
|
|
900
|
+
"Make sure you paste the full redirect URL from the browser address bar"
|
|
901
|
+
)
|
|
902
|
+
);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
resolve({ code, state });
|
|
906
|
+
} catch {
|
|
907
|
+
reject(
|
|
908
|
+
new CliError(
|
|
909
|
+
ErrorCodes.AUTH_INVALID,
|
|
910
|
+
"Could not parse the pasted URL.",
|
|
911
|
+
"Make sure you paste the full redirect URL from the browser address bar"
|
|
912
|
+
)
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
rl.once("close", () => {
|
|
917
|
+
reject(
|
|
918
|
+
new CliError(
|
|
919
|
+
ErrorCodes.AUTH_INVALID,
|
|
920
|
+
"No redirect URL received.",
|
|
921
|
+
'Run "notion auth login" to try again'
|
|
922
|
+
)
|
|
923
|
+
);
|
|
924
|
+
});
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
function closeServer(ctx, cb) {
|
|
928
|
+
for (const socket of ctx.sockets) {
|
|
929
|
+
socket.destroy();
|
|
930
|
+
}
|
|
931
|
+
ctx.sockets.clear();
|
|
932
|
+
ctx.server.close(cb);
|
|
933
|
+
}
|
|
934
|
+
function handleCallbackRequest(req, res, ctx) {
|
|
935
|
+
if (ctx.settled) {
|
|
936
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
937
|
+
res.end(
|
|
938
|
+
"<html><body><h1>Already handled. You can close this tab.</h1></body></html>"
|
|
1042
939
|
);
|
|
940
|
+
return;
|
|
1043
941
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
942
|
+
try {
|
|
943
|
+
const reqUrl = new URL(req.url ?? "/", "http://localhost:54321");
|
|
944
|
+
const code = reqUrl.searchParams.get("code");
|
|
945
|
+
const returnedState = reqUrl.searchParams.get("state");
|
|
946
|
+
const errorParam = reqUrl.searchParams.get("error");
|
|
947
|
+
if (errorParam === "access_denied") {
|
|
948
|
+
ctx.settled = true;
|
|
949
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
950
|
+
res.end(
|
|
951
|
+
"<html><body><h1>Access Denied</h1><p>You cancelled the Notion OAuth request. You can close this tab.</p></body></html>"
|
|
952
|
+
);
|
|
953
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
954
|
+
closeServer(ctx, () => {
|
|
955
|
+
ctx.reject(
|
|
956
|
+
new CliError(
|
|
957
|
+
ErrorCodes.AUTH_INVALID,
|
|
958
|
+
"Notion OAuth access was denied.",
|
|
959
|
+
'Run "notion auth login" to try again'
|
|
960
|
+
)
|
|
961
|
+
);
|
|
962
|
+
});
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
if (!code || !returnedState) {
|
|
966
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
967
|
+
res.end("<html><body><p>Waiting for OAuth callback...</p></body></html>");
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (returnedState !== ctx.expectedState) {
|
|
971
|
+
ctx.settled = true;
|
|
972
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
973
|
+
res.end(
|
|
974
|
+
"<html><body><h1>Security Error</h1><p>State mismatch \u2014 possible CSRF attempt. You can close this tab.</p></body></html>"
|
|
975
|
+
);
|
|
976
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
977
|
+
closeServer(ctx, () => {
|
|
978
|
+
ctx.reject(
|
|
979
|
+
new CliError(
|
|
980
|
+
ErrorCodes.AUTH_INVALID,
|
|
981
|
+
"OAuth state mismatch \u2014 possible CSRF attempt. Aborting.",
|
|
982
|
+
'Run "notion auth login" to start a fresh OAuth flow'
|
|
983
|
+
)
|
|
984
|
+
);
|
|
985
|
+
});
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
ctx.settled = true;
|
|
989
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
990
|
+
res.end(
|
|
991
|
+
"<html><body><h1>Authenticated!</h1><p>You can close this tab and return to the terminal.</p></body></html>"
|
|
1049
992
|
);
|
|
993
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
994
|
+
closeServer(ctx, () => {
|
|
995
|
+
ctx.resolve({ code, state: returnedState });
|
|
996
|
+
});
|
|
997
|
+
} catch {
|
|
998
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
999
|
+
res.end("<html><body><h1>Error processing callback</h1></body></html>");
|
|
1050
1000
|
}
|
|
1051
|
-
return parsed;
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
// src/config/token.ts
|
|
1055
|
-
function isOAuthExpired(profile) {
|
|
1056
|
-
if (profile.oauth_expiry_ms == null) return false;
|
|
1057
|
-
return Date.now() >= profile.oauth_expiry_ms;
|
|
1058
1001
|
}
|
|
1059
|
-
async function
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1002
|
+
async function runOAuthFlow(options) {
|
|
1003
|
+
const state = randomBytes(16).toString("hex");
|
|
1004
|
+
const authUrl = buildAuthUrl(state);
|
|
1005
|
+
if (options?.manual) {
|
|
1006
|
+
return manualFlow(authUrl);
|
|
1063
1007
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1008
|
+
return new Promise((resolve, reject) => {
|
|
1009
|
+
const ctx = {
|
|
1010
|
+
expectedState: state,
|
|
1011
|
+
settled: false,
|
|
1012
|
+
timeoutHandle: null,
|
|
1013
|
+
resolve,
|
|
1014
|
+
reject,
|
|
1015
|
+
// assigned below after server is created
|
|
1016
|
+
server: null,
|
|
1017
|
+
sockets: /* @__PURE__ */ new Set()
|
|
1018
|
+
};
|
|
1019
|
+
const server = createServer(
|
|
1020
|
+
(req, res) => handleCallbackRequest(req, res, ctx)
|
|
1070
1021
|
);
|
|
1022
|
+
ctx.server = server;
|
|
1023
|
+
server.on("connection", (socket) => {
|
|
1024
|
+
ctx.sockets.add(socket);
|
|
1025
|
+
socket.once("close", () => ctx.sockets.delete(socket));
|
|
1026
|
+
});
|
|
1027
|
+
server.on("error", (err) => {
|
|
1028
|
+
if (ctx.settled) return;
|
|
1029
|
+
ctx.settled = true;
|
|
1030
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
1031
|
+
reject(
|
|
1032
|
+
new CliError(
|
|
1033
|
+
ErrorCodes.AUTH_INVALID,
|
|
1034
|
+
`Failed to start OAuth callback server: ${err.message}`,
|
|
1035
|
+
"Make sure port 54321 is not in use, or use --manual flag"
|
|
1036
|
+
)
|
|
1037
|
+
);
|
|
1038
|
+
});
|
|
1039
|
+
server.listen(54321, "127.0.0.1", () => {
|
|
1040
|
+
const browserOpened = openBrowser(authUrl);
|
|
1041
|
+
if (!browserOpened) {
|
|
1042
|
+
server.close();
|
|
1043
|
+
ctx.settled = true;
|
|
1044
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
1045
|
+
manualFlow(authUrl).then(resolve, reject);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
process.stderr.write(
|
|
1049
|
+
`
|
|
1050
|
+
Opening browser for Notion OAuth...
|
|
1051
|
+
If your browser didn't open, visit:
|
|
1052
|
+
${authUrl}
|
|
1053
|
+
|
|
1054
|
+
Waiting for callback (up to 120 seconds)...
|
|
1055
|
+
`
|
|
1056
|
+
);
|
|
1057
|
+
ctx.timeoutHandle = setTimeout(() => {
|
|
1058
|
+
if (ctx.settled) return;
|
|
1059
|
+
ctx.settled = true;
|
|
1060
|
+
closeServer(ctx, () => {
|
|
1061
|
+
reject(
|
|
1062
|
+
new CliError(
|
|
1063
|
+
ErrorCodes.AUTH_INVALID,
|
|
1064
|
+
"OAuth login timed out after 120 seconds.",
|
|
1065
|
+
'Run "notion auth login" to try again, or use --manual flag'
|
|
1066
|
+
)
|
|
1067
|
+
);
|
|
1068
|
+
});
|
|
1069
|
+
}, 12e4);
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// src/commands/init.ts
|
|
1075
|
+
import { confirm, input, password } from "@inquirer/prompts";
|
|
1076
|
+
import { Command as Command2 } from "commander";
|
|
1077
|
+
async function runInitFlow() {
|
|
1078
|
+
const profileName = await input({
|
|
1079
|
+
message: "Profile name:",
|
|
1080
|
+
default: "default"
|
|
1081
|
+
});
|
|
1082
|
+
const token = await password({
|
|
1083
|
+
message: "Integration token (from notion.so/profile/integrations/internal):",
|
|
1084
|
+
mask: "*"
|
|
1085
|
+
});
|
|
1086
|
+
stderrWrite("Validating token...");
|
|
1087
|
+
const { workspaceName, workspaceId } = await validateToken(token);
|
|
1088
|
+
stderrWrite(success(`\u2713 Connected to workspace: ${bold(workspaceName)}`));
|
|
1089
|
+
const config = await readGlobalConfig();
|
|
1090
|
+
if (config.profiles?.[profileName]) {
|
|
1091
|
+
const replace = await confirm({
|
|
1092
|
+
message: `Profile "${profileName}" already exists. Replace?`,
|
|
1093
|
+
default: false
|
|
1094
|
+
});
|
|
1095
|
+
if (!replace) {
|
|
1096
|
+
stderrWrite("Aborted.");
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1071
1099
|
}
|
|
1100
|
+
const profiles = config.profiles ?? {};
|
|
1101
|
+
profiles[profileName] = {
|
|
1102
|
+
token,
|
|
1103
|
+
workspace_name: workspaceName,
|
|
1104
|
+
workspace_id: workspaceId
|
|
1105
|
+
};
|
|
1106
|
+
await writeGlobalConfig({
|
|
1107
|
+
...config,
|
|
1108
|
+
profiles,
|
|
1109
|
+
active_profile: profileName
|
|
1110
|
+
});
|
|
1111
|
+
stderrWrite(success(`Profile "${profileName}" saved and set as active.`));
|
|
1112
|
+
stderrWrite(dim("Checking integration access..."));
|
|
1072
1113
|
try {
|
|
1073
|
-
const
|
|
1074
|
-
await
|
|
1075
|
-
|
|
1114
|
+
const notion = createNotionClient(token);
|
|
1115
|
+
const probe = await notion.search({ page_size: 1 });
|
|
1116
|
+
if (probe.results.length === 0) {
|
|
1117
|
+
stderrWrite("");
|
|
1118
|
+
stderrWrite("\u26A0\uFE0F Your integration has no pages connected.");
|
|
1119
|
+
stderrWrite(" To grant access, open any Notion page or database:");
|
|
1120
|
+
stderrWrite(" 1. Click \xB7\xB7\xB7 (three dots) in the top-right corner");
|
|
1121
|
+
stderrWrite(' 2. Select "Connect to"');
|
|
1122
|
+
stderrWrite(` 3. Choose "${workspaceName}"`);
|
|
1123
|
+
stderrWrite(" Then re-run any notion command to confirm access.");
|
|
1124
|
+
} else {
|
|
1125
|
+
stderrWrite(
|
|
1126
|
+
success(
|
|
1127
|
+
`\u2713 Integration has access to content in ${bold(workspaceName)}.`
|
|
1128
|
+
)
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1076
1131
|
} catch {
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
ErrorCodes.AUTH_NO_TOKEN,
|
|
1080
|
-
'OAuth session expired. Run "notion auth login" to re-authenticate.',
|
|
1081
|
-
"Your session was revoked or the refresh token has expired"
|
|
1132
|
+
stderrWrite(
|
|
1133
|
+
dim("(Could not verify integration access \u2014 run `notion ls` to check)")
|
|
1082
1134
|
);
|
|
1083
1135
|
}
|
|
1136
|
+
stderrWrite("");
|
|
1137
|
+
stderrWrite(
|
|
1138
|
+
dim("Write commands (comment, append, create-page) require additional")
|
|
1139
|
+
);
|
|
1140
|
+
stderrWrite(dim("capabilities in your integration settings:"));
|
|
1141
|
+
stderrWrite(
|
|
1142
|
+
dim(" notion.so/profile/integrations/internal \u2192 your integration \u2192")
|
|
1143
|
+
);
|
|
1144
|
+
stderrWrite(
|
|
1145
|
+
dim(
|
|
1146
|
+
' Capabilities: enable "Read content", "Insert content", "Read comments", "Insert comments"'
|
|
1147
|
+
)
|
|
1148
|
+
);
|
|
1149
|
+
stderrWrite("");
|
|
1150
|
+
stderrWrite(
|
|
1151
|
+
dim("To post comments and create pages attributed to your user account:")
|
|
1152
|
+
);
|
|
1153
|
+
stderrWrite(dim(" notion auth login"));
|
|
1084
1154
|
}
|
|
1085
|
-
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
if (localConfig.profile) {
|
|
1096
|
-
const globalConfig2 = await readGlobalConfig();
|
|
1097
|
-
const profile = globalConfig2.profiles?.[localConfig.profile];
|
|
1098
|
-
if (profile) {
|
|
1099
|
-
const oauthToken = await resolveOAuthToken(localConfig.profile, profile);
|
|
1100
|
-
if (oauthToken) {
|
|
1101
|
-
return { token: oauthToken, source: "oauth" };
|
|
1102
|
-
}
|
|
1103
|
-
if (profile.token) {
|
|
1104
|
-
return { token: profile.token, source: `profile: ${localConfig.profile}` };
|
|
1105
|
-
}
|
|
1155
|
+
function initCommand() {
|
|
1156
|
+
const cmd = new Command2("init");
|
|
1157
|
+
cmd.description("authenticate with Notion and save a profile").action(
|
|
1158
|
+
withErrorHandling(async () => {
|
|
1159
|
+
if (!process.stdin.isTTY) {
|
|
1160
|
+
throw new CliError(
|
|
1161
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
1162
|
+
"Cannot run interactive init in non-TTY mode.",
|
|
1163
|
+
"Set NOTION_API_TOKEN environment variable or create .notion.yaml"
|
|
1164
|
+
);
|
|
1106
1165
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1166
|
+
await runInitFlow();
|
|
1167
|
+
})
|
|
1168
|
+
);
|
|
1169
|
+
return cmd;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/commands/auth/login.ts
|
|
1173
|
+
function loginCommand() {
|
|
1174
|
+
const cmd = new Command3("login");
|
|
1175
|
+
cmd.description("authenticate with Notion \u2014 choose OAuth or integration token").option("--profile <name>", "profile name to store credentials in").option(
|
|
1176
|
+
"--manual",
|
|
1177
|
+
"print auth URL instead of opening browser (for headless OAuth)"
|
|
1178
|
+
).action(
|
|
1179
|
+
withErrorHandling(async (opts) => {
|
|
1180
|
+
if (!process.stdin.isTTY && !opts.manual) {
|
|
1181
|
+
throw new CliError(
|
|
1182
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
1183
|
+
"Cannot run interactive login in non-TTY mode.",
|
|
1184
|
+
"Use --manual flag to get an auth URL you can open in a browser"
|
|
1185
|
+
);
|
|
1116
1186
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1187
|
+
const method = await select({
|
|
1188
|
+
message: "How do you want to authenticate with Notion?",
|
|
1189
|
+
choices: [
|
|
1190
|
+
{
|
|
1191
|
+
name: "OAuth user login (browser required)",
|
|
1192
|
+
value: "oauth",
|
|
1193
|
+
description: "Opens Notion in your browser. Comments and pages are attributed to your account. Tokens auto-refresh."
|
|
1194
|
+
},
|
|
1195
|
+
{
|
|
1196
|
+
name: "Internal integration token (CI/headless friendly)",
|
|
1197
|
+
value: "token",
|
|
1198
|
+
description: "Paste a token from notion.so/profile/integrations. No browser needed. Write ops attributed to integration bot."
|
|
1199
|
+
}
|
|
1200
|
+
]
|
|
1201
|
+
});
|
|
1202
|
+
if (method === "oauth") {
|
|
1203
|
+
let profileName = opts.profile;
|
|
1204
|
+
if (!profileName) {
|
|
1205
|
+
const config = await readGlobalConfig();
|
|
1206
|
+
profileName = config.active_profile ?? "default";
|
|
1207
|
+
}
|
|
1208
|
+
const result = await runOAuthFlow({ manual: opts.manual });
|
|
1209
|
+
const response = await exchangeCode(result.code);
|
|
1210
|
+
await saveOAuthTokens(profileName, response);
|
|
1211
|
+
const userName = response.owner?.user?.name ?? "unknown user";
|
|
1212
|
+
const workspaceName = response.workspace_name ?? "unknown workspace";
|
|
1213
|
+
stderrWrite(
|
|
1214
|
+
success(`\u2713 Logged in as ${userName} to workspace ${workspaceName}`)
|
|
1215
|
+
);
|
|
1216
|
+
stderrWrite(
|
|
1217
|
+
dim(
|
|
1218
|
+
"Your comments and pages will now be attributed to your Notion account."
|
|
1219
|
+
)
|
|
1220
|
+
);
|
|
1221
|
+
} else {
|
|
1222
|
+
await runInitFlow();
|
|
1119
1223
|
}
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
1122
|
-
throw new CliError(
|
|
1123
|
-
ErrorCodes.AUTH_NO_TOKEN,
|
|
1124
|
-
"No authentication token found.",
|
|
1125
|
-
'Run "notion init" to set up a profile'
|
|
1224
|
+
})
|
|
1126
1225
|
);
|
|
1226
|
+
return cmd;
|
|
1127
1227
|
}
|
|
1128
1228
|
|
|
1129
|
-
// src/commands/
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
return
|
|
1142
|
-
}
|
|
1143
|
-
function displayType(item) {
|
|
1144
|
-
return item.object === "data_source" ? "database" : item.object;
|
|
1229
|
+
// src/commands/auth/logout.ts
|
|
1230
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
1231
|
+
import { Command as Command4 } from "commander";
|
|
1232
|
+
function profileLabel(name, profile) {
|
|
1233
|
+
const parts = [];
|
|
1234
|
+
if (profile.oauth_access_token)
|
|
1235
|
+
parts.push(
|
|
1236
|
+
`OAuth${profile.oauth_user_name ? ` (${profile.oauth_user_name})` : ""}`
|
|
1237
|
+
);
|
|
1238
|
+
if (profile.token) parts.push("integration token");
|
|
1239
|
+
const authDesc = parts.length > 0 ? parts.join(" + ") : "no credentials";
|
|
1240
|
+
const workspace = profile.workspace_name ? dim(` \u2014 ${profile.workspace_name}`) : "";
|
|
1241
|
+
return `${bold(name)} ${dim(authDesc)}${workspace}`;
|
|
1145
1242
|
}
|
|
1146
|
-
function
|
|
1147
|
-
const cmd = new
|
|
1148
|
-
cmd.description("
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
const { token, source } = await resolveToken();
|
|
1159
|
-
reportTokenSource(source);
|
|
1160
|
-
const notion = createNotionClient(token);
|
|
1161
|
-
const response = await notion.search({
|
|
1162
|
-
query,
|
|
1163
|
-
filter: opts.type ? { property: "object", value: toSdkFilterValue(opts.type) } : void 0,
|
|
1164
|
-
start_cursor: opts.cursor,
|
|
1165
|
-
page_size: 20
|
|
1166
|
-
});
|
|
1167
|
-
const fullResults = response.results.filter((r) => isFullPageOrDataSource(r));
|
|
1168
|
-
if (fullResults.length === 0) {
|
|
1169
|
-
process.stdout.write(`No results found for "${query}"
|
|
1170
|
-
`);
|
|
1243
|
+
function logoutCommand() {
|
|
1244
|
+
const cmd = new Command4("logout");
|
|
1245
|
+
cmd.description("remove a profile and its credentials").option(
|
|
1246
|
+
"--profile <name>",
|
|
1247
|
+
"profile name to remove (skips interactive selector)"
|
|
1248
|
+
).action(
|
|
1249
|
+
withErrorHandling(async (opts) => {
|
|
1250
|
+
const config = await readGlobalConfig();
|
|
1251
|
+
const profiles = config.profiles ?? {};
|
|
1252
|
+
const profileNames = Object.keys(profiles);
|
|
1253
|
+
if (profileNames.length === 0) {
|
|
1254
|
+
stderrWrite("No profiles configured.");
|
|
1171
1255
|
return;
|
|
1172
1256
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1257
|
+
let profileName = opts.profile;
|
|
1258
|
+
if (!profileName) {
|
|
1259
|
+
if (!process.stdin.isTTY) {
|
|
1260
|
+
throw new CliError(
|
|
1261
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
1262
|
+
"Cannot run interactive logout in non-TTY mode.",
|
|
1263
|
+
"Use --profile <name> to specify the profile to remove"
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
profileName = await select2({
|
|
1267
|
+
message: "Which profile do you want to log out of?",
|
|
1268
|
+
choices: profileNames.map((name) => ({
|
|
1269
|
+
// biome-ignore lint/style/noNonNullAssertion: key is from Object.keys, always present
|
|
1270
|
+
name: profileLabel(name, profiles[name]),
|
|
1271
|
+
value: name
|
|
1272
|
+
}))
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
if (!profiles[profileName]) {
|
|
1276
|
+
throw new CliError(
|
|
1277
|
+
ErrorCodes.AUTH_PROFILE_NOT_FOUND,
|
|
1278
|
+
`Profile "${profileName}" not found.`,
|
|
1279
|
+
`Run "notion auth list" to see available profiles`
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
const updatedProfiles = { ...profiles };
|
|
1283
|
+
delete updatedProfiles[profileName];
|
|
1284
|
+
const newActiveProfile = config.active_profile === profileName ? void 0 : config.active_profile;
|
|
1285
|
+
await writeGlobalConfig({
|
|
1286
|
+
...config,
|
|
1287
|
+
profiles: updatedProfiles,
|
|
1288
|
+
active_profile: newActiveProfile
|
|
1289
|
+
});
|
|
1290
|
+
stderrWrite(success(`\u2713 Logged out of profile "${profileName}".`));
|
|
1291
|
+
if (newActiveProfile === void 0 && Object.keys(updatedProfiles).length > 0) {
|
|
1292
|
+
stderrWrite(
|
|
1293
|
+
dim(`Run "notion auth use <name>" to set a new active profile.`)
|
|
1294
|
+
);
|
|
1185
1295
|
}
|
|
1186
1296
|
})
|
|
1187
1297
|
);
|
|
1188
1298
|
return cmd;
|
|
1189
1299
|
}
|
|
1190
1300
|
|
|
1191
|
-
// src/commands/
|
|
1192
|
-
import { Command as
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
return item.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
1197
|
-
}
|
|
1198
|
-
const titleProp = Object.values(item.properties).find((p) => p.type === "title");
|
|
1199
|
-
if (titleProp?.type === "title") {
|
|
1200
|
-
return titleProp.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
1201
|
-
}
|
|
1202
|
-
return "(untitled)";
|
|
1203
|
-
}
|
|
1204
|
-
function displayType2(item) {
|
|
1205
|
-
return item.object === "data_source" ? "database" : item.object;
|
|
1206
|
-
}
|
|
1207
|
-
function lsCommand() {
|
|
1208
|
-
const cmd = new Command10("ls");
|
|
1209
|
-
cmd.description("list accessible Notion pages and databases").option("--type <type>", "filter by object type (page or database)", (val) => {
|
|
1210
|
-
if (val !== "page" && val !== "database") {
|
|
1211
|
-
throw new Error('--type must be "page" or "database"');
|
|
1212
|
-
}
|
|
1213
|
-
return val;
|
|
1214
|
-
}).option("--cursor <cursor>", "start from this pagination cursor (from a previous --next hint)").option("--json", "force JSON output").action(
|
|
1301
|
+
// src/commands/auth/status.ts
|
|
1302
|
+
import { Command as Command5 } from "commander";
|
|
1303
|
+
function statusCommand() {
|
|
1304
|
+
const cmd = new Command5("status");
|
|
1305
|
+
cmd.description("show authentication status for the active profile").option("--profile <name>", "profile name to check").action(
|
|
1215
1306
|
withErrorHandling(async (opts) => {
|
|
1216
|
-
|
|
1217
|
-
|
|
1307
|
+
let profileName = opts.profile;
|
|
1308
|
+
const config = await readGlobalConfig();
|
|
1309
|
+
if (!profileName) {
|
|
1310
|
+
profileName = config.active_profile ?? "default";
|
|
1311
|
+
}
|
|
1312
|
+
const profile = config.profiles?.[profileName];
|
|
1313
|
+
stderrWrite(`Profile: ${profileName}`);
|
|
1314
|
+
if (!profile) {
|
|
1315
|
+
stderrWrite(
|
|
1316
|
+
` ${error("\u2717")} No profile found (run 'notion init' to create one)`
|
|
1317
|
+
);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
if (profile.oauth_access_token) {
|
|
1321
|
+
const userName = profile.oauth_user_name ?? "unknown";
|
|
1322
|
+
const userId = profile.oauth_user_id ?? "unknown";
|
|
1323
|
+
stderrWrite(
|
|
1324
|
+
` OAuth: ${success("\u2713")} Logged in as ${userName} (user: ${userId})`
|
|
1325
|
+
);
|
|
1326
|
+
if (profile.oauth_expiry_ms != null) {
|
|
1327
|
+
const expiryDate = new Date(profile.oauth_expiry_ms).toISOString();
|
|
1328
|
+
stderrWrite(dim(` Access token expires: ${expiryDate}`));
|
|
1329
|
+
}
|
|
1330
|
+
} else {
|
|
1331
|
+
stderrWrite(
|
|
1332
|
+
` OAuth: ${error("\u2717")} Not logged in (run 'notion auth login')`
|
|
1333
|
+
);
|
|
1334
|
+
}
|
|
1335
|
+
if (profile.token) {
|
|
1336
|
+
const tokenPreview = `${profile.token.substring(0, 10)}...`;
|
|
1337
|
+
stderrWrite(
|
|
1338
|
+
` Internal token: ${success("\u2713")} Configured (${tokenPreview})`
|
|
1339
|
+
);
|
|
1340
|
+
} else {
|
|
1341
|
+
stderrWrite(` Internal token: ${error("\u2717")} Not configured`);
|
|
1342
|
+
}
|
|
1343
|
+
if (profile.oauth_access_token) {
|
|
1344
|
+
stderrWrite(` Active method: OAuth (user-attributed)`);
|
|
1345
|
+
} else if (profile.token) {
|
|
1346
|
+
stderrWrite(
|
|
1347
|
+
` Active method: Internal integration token (bot-attributed)`
|
|
1348
|
+
);
|
|
1349
|
+
} else {
|
|
1350
|
+
stderrWrite(
|
|
1351
|
+
dim(
|
|
1352
|
+
` Active method: None (run 'notion auth login' or 'notion init')`
|
|
1353
|
+
)
|
|
1354
|
+
);
|
|
1218
1355
|
}
|
|
1356
|
+
})
|
|
1357
|
+
);
|
|
1358
|
+
return cmd;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/commands/comment-add.ts
|
|
1362
|
+
import { Command as Command6 } from "commander";
|
|
1363
|
+
function commentAddCommand() {
|
|
1364
|
+
const cmd = new Command6("comment");
|
|
1365
|
+
cmd.description("add a comment to a Notion page").argument("<id/url>", "Notion page ID or URL").requiredOption("-m, --message <text>", "comment text to post").action(
|
|
1366
|
+
withErrorHandling(async (idOrUrl, opts) => {
|
|
1219
1367
|
const { token, source } = await resolveToken();
|
|
1220
1368
|
reportTokenSource(source);
|
|
1221
|
-
const
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1224
|
-
|
|
1369
|
+
const client = createNotionClient(token);
|
|
1370
|
+
const id = parseNotionId(idOrUrl);
|
|
1371
|
+
const uuid = toUuid(id);
|
|
1372
|
+
await addComment(client, uuid, opts.message, {
|
|
1373
|
+
asUser: source === "oauth"
|
|
1225
1374
|
});
|
|
1226
|
-
|
|
1227
|
-
if (opts.type) {
|
|
1228
|
-
const filterType = opts.type;
|
|
1229
|
-
items = items.filter(
|
|
1230
|
-
(r) => filterType === "database" ? r.object === "data_source" : r.object === filterType
|
|
1231
|
-
);
|
|
1232
|
-
}
|
|
1233
|
-
if (items.length === 0) {
|
|
1234
|
-
process.stdout.write("No accessible content found\n");
|
|
1235
|
-
return;
|
|
1236
|
-
}
|
|
1237
|
-
const headers = ["TYPE", "TITLE", "ID", "MODIFIED"];
|
|
1238
|
-
const rows = items.map((item) => [
|
|
1239
|
-
displayType2(item),
|
|
1240
|
-
getTitle2(item),
|
|
1241
|
-
item.id,
|
|
1242
|
-
item.last_edited_time.split("T")[0]
|
|
1243
|
-
]);
|
|
1244
|
-
printOutput(items, headers, rows);
|
|
1245
|
-
if (response.has_more && response.next_cursor) {
|
|
1246
|
-
process.stderr.write(`
|
|
1247
|
-
--next: notion ls --cursor ${response.next_cursor}
|
|
1248
|
-
`);
|
|
1249
|
-
}
|
|
1375
|
+
process.stdout.write("Comment added.\n");
|
|
1250
1376
|
})
|
|
1251
1377
|
);
|
|
1252
1378
|
return cmd;
|
|
1253
1379
|
}
|
|
1254
1380
|
|
|
1255
|
-
// src/commands/
|
|
1256
|
-
import {
|
|
1257
|
-
import { promisify } from "util";
|
|
1258
|
-
import { Command as Command11 } from "commander";
|
|
1381
|
+
// src/commands/comments.ts
|
|
1382
|
+
import { Command as Command7 } from "commander";
|
|
1259
1383
|
|
|
1260
|
-
// src/
|
|
1261
|
-
var
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
function throwInvalidId(input2) {
|
|
1265
|
-
throw new CliError(
|
|
1266
|
-
ErrorCodes.INVALID_ID,
|
|
1267
|
-
`Cannot parse Notion ID from: ${input2}`,
|
|
1268
|
-
"Provide a valid Notion URL or page/database ID"
|
|
1269
|
-
);
|
|
1384
|
+
// src/output/format.ts
|
|
1385
|
+
var _mode = "auto";
|
|
1386
|
+
function setOutputMode(mode) {
|
|
1387
|
+
_mode = mode;
|
|
1270
1388
|
}
|
|
1271
|
-
function
|
|
1272
|
-
|
|
1273
|
-
if (NOTION_ID_REGEX.test(input2)) {
|
|
1274
|
-
return input2.toLowerCase();
|
|
1275
|
-
}
|
|
1276
|
-
if (UUID_REGEX.test(input2)) {
|
|
1277
|
-
return input2.replace(/-/g, "").toLowerCase();
|
|
1278
|
-
}
|
|
1279
|
-
const urlMatch = NOTION_URL_REGEX.exec(input2);
|
|
1280
|
-
if (urlMatch) {
|
|
1281
|
-
return urlMatch[1].toLowerCase();
|
|
1282
|
-
}
|
|
1283
|
-
throwInvalidId(input2);
|
|
1389
|
+
function getOutputMode() {
|
|
1390
|
+
return _mode;
|
|
1284
1391
|
}
|
|
1285
|
-
function
|
|
1286
|
-
return
|
|
1392
|
+
function isatty() {
|
|
1393
|
+
return Boolean(process.stdout.isTTY);
|
|
1287
1394
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1395
|
+
function isHumanMode() {
|
|
1396
|
+
if (_mode === "json") return false;
|
|
1397
|
+
if (_mode === "md") return false;
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
function formatJSON(data) {
|
|
1401
|
+
return JSON.stringify(data, null, 2);
|
|
1402
|
+
}
|
|
1403
|
+
var COLUMN_CAPS = {
|
|
1404
|
+
TITLE: 50,
|
|
1405
|
+
ID: 36
|
|
1406
|
+
};
|
|
1407
|
+
var DEFAULT_MAX_COL_WIDTH = 40;
|
|
1408
|
+
function getColumnCap(header) {
|
|
1409
|
+
return COLUMN_CAPS[header.toUpperCase()] ?? DEFAULT_MAX_COL_WIDTH;
|
|
1410
|
+
}
|
|
1411
|
+
function truncate(str, maxLen) {
|
|
1412
|
+
if (str.length <= maxLen) return str;
|
|
1413
|
+
return `${str.slice(0, maxLen - 1)}\u2026`;
|
|
1414
|
+
}
|
|
1415
|
+
function formatTable(rows, headers) {
|
|
1416
|
+
const colWidths = headers.map((header, colIdx) => {
|
|
1417
|
+
const cap = getColumnCap(header);
|
|
1418
|
+
const headerLen = header.length;
|
|
1419
|
+
const maxRowLen = rows.reduce((max, row) => {
|
|
1420
|
+
const cell = row[colIdx] ?? "";
|
|
1421
|
+
return Math.max(max, cell.length);
|
|
1422
|
+
}, 0);
|
|
1423
|
+
return Math.min(Math.max(headerLen, maxRowLen), cap);
|
|
1424
|
+
});
|
|
1425
|
+
const sep = "\u2500";
|
|
1426
|
+
const colSep = " ";
|
|
1427
|
+
const headerRow = headers.map((h, i) => h.padEnd(colWidths[i])).join(colSep);
|
|
1428
|
+
const separatorRow = colWidths.map((w) => sep.repeat(w)).join(colSep);
|
|
1429
|
+
const dataRows = rows.map(
|
|
1430
|
+
(row) => headers.map((_, i) => {
|
|
1431
|
+
const cell = row[i] ?? "";
|
|
1432
|
+
return truncate(cell, colWidths[i]).padEnd(colWidths[i]);
|
|
1433
|
+
}).join(colSep)
|
|
1434
|
+
);
|
|
1435
|
+
return [headerRow, separatorRow, ...dataRows].join("\n");
|
|
1436
|
+
}
|
|
1437
|
+
function printOutput(data, tableHeaders, tableRows) {
|
|
1438
|
+
const mode = getOutputMode();
|
|
1439
|
+
if (mode === "json") {
|
|
1440
|
+
process.stdout.write(`${formatJSON(data)}
|
|
1300
1441
|
`);
|
|
1301
|
-
}))
|
|
1302
|
-
|
|
1442
|
+
} else if (isHumanMode() && tableHeaders && tableRows) {
|
|
1443
|
+
printWithPager(`${formatTable(tableRows, tableHeaders)}
|
|
1444
|
+
`);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
function printWithPager(text) {
|
|
1448
|
+
process.stdout.write(text);
|
|
1303
1449
|
}
|
|
1304
|
-
|
|
1305
|
-
// src/commands/users.ts
|
|
1306
|
-
import { Command as Command12 } from "commander";
|
|
1307
1450
|
|
|
1308
1451
|
// src/output/paginate.ts
|
|
1309
1452
|
async function paginateResults(fetcher) {
|
|
@@ -1319,264 +1462,337 @@ async function paginateResults(fetcher) {
|
|
|
1319
1462
|
return allResults;
|
|
1320
1463
|
}
|
|
1321
1464
|
|
|
1322
|
-
// src/commands/
|
|
1323
|
-
function
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
}));
|
|
1465
|
+
// src/commands/comments.ts
|
|
1466
|
+
function commentsCommand() {
|
|
1467
|
+
const cmd = new Command7("comments");
|
|
1468
|
+
cmd.description("list comments on a Notion page").argument("<id/url>", "Notion page ID or URL").option("--json", "output as JSON").action(
|
|
1469
|
+
withErrorHandling(async (idOrUrl, opts) => {
|
|
1470
|
+
if (opts.json) setOutputMode("json");
|
|
1471
|
+
const id = parseNotionId(idOrUrl);
|
|
1472
|
+
const uuid = toUuid(id);
|
|
1473
|
+
const { token, source } = await resolveToken();
|
|
1474
|
+
reportTokenSource(source);
|
|
1475
|
+
const notion = createNotionClient(token);
|
|
1476
|
+
const comments = await paginateResults(
|
|
1477
|
+
(cursor) => notion.comments.list({ block_id: uuid, start_cursor: cursor })
|
|
1478
|
+
);
|
|
1479
|
+
if (comments.length === 0) {
|
|
1480
|
+
process.stdout.write("No comments found on this page\n");
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
const rows = comments.map((comment) => {
|
|
1484
|
+
const text = comment.rich_text.map((t) => t.plain_text).join("");
|
|
1485
|
+
return [
|
|
1486
|
+
comment.created_time.split("T")[0],
|
|
1487
|
+
`${comment.created_by.id.slice(0, 8)}...`,
|
|
1488
|
+
text.slice(0, 80) + (text.length > 80 ? "\u2026" : "")
|
|
1489
|
+
];
|
|
1490
|
+
});
|
|
1491
|
+
printOutput(comments, ["DATE", "AUTHOR ID", "COMMENT"], rows);
|
|
1492
|
+
})
|
|
1493
|
+
);
|
|
1352
1494
|
return cmd;
|
|
1353
1495
|
}
|
|
1354
1496
|
|
|
1355
|
-
// src/commands/
|
|
1356
|
-
import { Command as
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
(
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1497
|
+
// src/commands/completion.ts
|
|
1498
|
+
import { Command as Command8 } from "commander";
|
|
1499
|
+
var BASH_COMPLETION = `# notion bash completion
|
|
1500
|
+
_notion_completion() {
|
|
1501
|
+
local cur prev words cword
|
|
1502
|
+
_init_completion || return
|
|
1503
|
+
|
|
1504
|
+
local commands="init profile completion --help --version --verbose --color"
|
|
1505
|
+
local profile_commands="list use remove"
|
|
1506
|
+
|
|
1507
|
+
case "$prev" in
|
|
1508
|
+
notion)
|
|
1509
|
+
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
1510
|
+
return 0
|
|
1511
|
+
;;
|
|
1512
|
+
profile)
|
|
1513
|
+
COMPREPLY=($(compgen -W "$profile_commands" -- "$cur"))
|
|
1514
|
+
return 0
|
|
1515
|
+
;;
|
|
1516
|
+
completion)
|
|
1517
|
+
COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))
|
|
1518
|
+
return 0
|
|
1519
|
+
;;
|
|
1520
|
+
esac
|
|
1521
|
+
|
|
1522
|
+
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
complete -F _notion_completion notion
|
|
1526
|
+
`;
|
|
1527
|
+
var ZSH_COMPLETION = `#compdef notion
|
|
1528
|
+
# notion zsh completion
|
|
1529
|
+
|
|
1530
|
+
_notion() {
|
|
1531
|
+
local -a commands
|
|
1532
|
+
|
|
1533
|
+
commands=(
|
|
1534
|
+
'init:authenticate with Notion and save a profile'
|
|
1535
|
+
'profile:manage authentication profiles'
|
|
1536
|
+
'completion:output shell completion script'
|
|
1537
|
+
)
|
|
1538
|
+
|
|
1539
|
+
local -a global_opts
|
|
1540
|
+
global_opts=(
|
|
1541
|
+
'--help[display help]'
|
|
1542
|
+
'--version[output version]'
|
|
1543
|
+
'--verbose[show API requests/responses]'
|
|
1544
|
+
'--color[force color output]'
|
|
1545
|
+
)
|
|
1546
|
+
|
|
1547
|
+
if (( CURRENT == 2 )); then
|
|
1548
|
+
_describe 'command' commands
|
|
1549
|
+
_arguments $global_opts
|
|
1550
|
+
return
|
|
1551
|
+
fi
|
|
1552
|
+
|
|
1553
|
+
case $words[2] in
|
|
1554
|
+
profile)
|
|
1555
|
+
local -a profile_cmds
|
|
1556
|
+
profile_cmds=(
|
|
1557
|
+
'list:list all authentication profiles'
|
|
1558
|
+
'use:switch the active profile'
|
|
1559
|
+
'remove:remove an authentication profile'
|
|
1560
|
+
)
|
|
1561
|
+
_describe 'profile command' profile_cmds
|
|
1562
|
+
;;
|
|
1563
|
+
completion)
|
|
1564
|
+
local -a shells
|
|
1565
|
+
shells=('bash' 'zsh' 'fish')
|
|
1566
|
+
_describe 'shell' shells
|
|
1567
|
+
;;
|
|
1568
|
+
esac
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
_notion "$@"
|
|
1572
|
+
`;
|
|
1573
|
+
var FISH_COMPLETION = `# notion fish completion
|
|
1574
|
+
|
|
1575
|
+
# Disable file completion by default
|
|
1576
|
+
complete -c notion -f
|
|
1577
|
+
|
|
1578
|
+
# Global options
|
|
1579
|
+
complete -c notion -l help -d 'display help'
|
|
1580
|
+
complete -c notion -l version -d 'output version'
|
|
1581
|
+
complete -c notion -l verbose -d 'show API requests/responses'
|
|
1582
|
+
complete -c notion -l color -d 'force color output'
|
|
1583
|
+
|
|
1584
|
+
# Top-level commands
|
|
1585
|
+
complete -c notion -n '__fish_use_subcommand' -a init -d 'authenticate with Notion and save a profile'
|
|
1586
|
+
complete -c notion -n '__fish_use_subcommand' -a profile -d 'manage authentication profiles'
|
|
1587
|
+
complete -c notion -n '__fish_use_subcommand' -a completion -d 'output shell completion script'
|
|
1588
|
+
|
|
1589
|
+
# profile subcommands
|
|
1590
|
+
complete -c notion -n '__fish_seen_subcommand_from profile' -a list -d 'list all authentication profiles'
|
|
1591
|
+
complete -c notion -n '__fish_seen_subcommand_from profile' -a use -d 'switch the active profile'
|
|
1592
|
+
complete -c notion -n '__fish_seen_subcommand_from profile' -a remove -d 'remove an authentication profile'
|
|
1593
|
+
|
|
1594
|
+
# completion shells
|
|
1595
|
+
complete -c notion -n '__fish_seen_subcommand_from completion' -a bash -d 'bash completion script'
|
|
1596
|
+
complete -c notion -n '__fish_seen_subcommand_from completion' -a zsh -d 'zsh completion script'
|
|
1597
|
+
complete -c notion -n '__fish_seen_subcommand_from completion' -a fish -d 'fish completion script'
|
|
1598
|
+
`;
|
|
1599
|
+
function completionCommand() {
|
|
1600
|
+
const cmd = new Command8("completion");
|
|
1601
|
+
cmd.description("output shell completion script").argument("<shell>", "shell type (bash, zsh, fish)").action(
|
|
1602
|
+
withErrorHandling(async (shell) => {
|
|
1603
|
+
switch (shell) {
|
|
1604
|
+
case "bash":
|
|
1605
|
+
process.stdout.write(BASH_COMPLETION);
|
|
1606
|
+
break;
|
|
1607
|
+
case "zsh":
|
|
1608
|
+
process.stdout.write(ZSH_COMPLETION);
|
|
1609
|
+
break;
|
|
1610
|
+
case "fish":
|
|
1611
|
+
process.stdout.write(FISH_COMPLETION);
|
|
1612
|
+
break;
|
|
1613
|
+
default:
|
|
1614
|
+
throw new CliError(
|
|
1615
|
+
ErrorCodes.UNKNOWN,
|
|
1616
|
+
`Unknown shell: "${shell}".`,
|
|
1617
|
+
"Supported shells: bash, zsh, fish"
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
})
|
|
1621
|
+
);
|
|
1383
1622
|
return cmd;
|
|
1384
1623
|
}
|
|
1385
1624
|
|
|
1386
|
-
// src/commands/
|
|
1387
|
-
import { Command as
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
async function fetchBlockTree(client, blockId, depth, maxDepth) {
|
|
1393
|
-
if (depth >= maxDepth) return [];
|
|
1394
|
-
const rawBlocks = await collectPaginatedAPI(client.blocks.children.list, {
|
|
1395
|
-
block_id: blockId
|
|
1396
|
-
});
|
|
1397
|
-
const blocks = rawBlocks.filter(isFullBlock);
|
|
1398
|
-
const SKIP_RECURSE = /* @__PURE__ */ new Set(["child_page", "child_database"]);
|
|
1399
|
-
const nodes = [];
|
|
1400
|
-
for (let i = 0; i < blocks.length; i += MAX_CONCURRENT_REQUESTS) {
|
|
1401
|
-
const batch = blocks.slice(i, i + MAX_CONCURRENT_REQUESTS);
|
|
1402
|
-
const batchNodes = await Promise.all(
|
|
1403
|
-
batch.map(async (block) => {
|
|
1404
|
-
const children = block.has_children && !SKIP_RECURSE.has(block.type) ? await fetchBlockTree(client, block.id, depth + 1, maxDepth) : [];
|
|
1405
|
-
return { block, children };
|
|
1406
|
-
})
|
|
1407
|
-
);
|
|
1408
|
-
nodes.push(...batchNodes);
|
|
1625
|
+
// src/commands/create-page.ts
|
|
1626
|
+
import { Command as Command9 } from "commander";
|
|
1627
|
+
async function readStdin() {
|
|
1628
|
+
const chunks = [];
|
|
1629
|
+
for await (const chunk of process.stdin) {
|
|
1630
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1409
1631
|
}
|
|
1410
|
-
return
|
|
1632
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
1411
1633
|
}
|
|
1412
|
-
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1634
|
+
function createPageCommand() {
|
|
1635
|
+
const cmd = new Command9("create-page");
|
|
1636
|
+
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(
|
|
1637
|
+
"-m, --message <markdown>",
|
|
1638
|
+
"inline markdown content for the page body"
|
|
1639
|
+
).action(
|
|
1640
|
+
withErrorHandling(
|
|
1641
|
+
async (opts) => {
|
|
1642
|
+
const { token, source } = await resolveToken();
|
|
1643
|
+
reportTokenSource(source);
|
|
1644
|
+
const client = createNotionClient(token);
|
|
1645
|
+
let markdown = "";
|
|
1646
|
+
if (opts.message) {
|
|
1647
|
+
markdown = opts.message;
|
|
1648
|
+
} else if (!process.stdin.isTTY) {
|
|
1649
|
+
markdown = await readStdin();
|
|
1650
|
+
}
|
|
1651
|
+
const blocks = mdToBlocks(markdown);
|
|
1652
|
+
const parentUuid = toUuid(parseNotionId(opts.parent));
|
|
1653
|
+
const url = await createPage(client, parentUuid, opts.title, blocks);
|
|
1654
|
+
process.stdout.write(`${url}
|
|
1655
|
+
`);
|
|
1656
|
+
}
|
|
1657
|
+
)
|
|
1658
|
+
);
|
|
1659
|
+
return cmd;
|
|
1416
1660
|
}
|
|
1417
1661
|
|
|
1418
|
-
// src/
|
|
1419
|
-
|
|
1420
|
-
return richText.map(segmentToMd).join("");
|
|
1421
|
-
}
|
|
1422
|
-
function segmentToMd(segment) {
|
|
1423
|
-
if (segment.type === "equation") {
|
|
1424
|
-
return `$${segment.equation.expression}$`;
|
|
1425
|
-
}
|
|
1426
|
-
if (segment.type === "mention") {
|
|
1427
|
-
const text = segment.plain_text;
|
|
1428
|
-
return segment.href ? `[${text}](${segment.href})` : text;
|
|
1429
|
-
}
|
|
1430
|
-
const annotated = applyAnnotations(segment.text.content, segment.annotations);
|
|
1431
|
-
return segment.text.link ? `[${annotated}](${segment.text.link.url})` : annotated;
|
|
1432
|
-
}
|
|
1433
|
-
function applyAnnotations(text, annotations) {
|
|
1434
|
-
let result = text;
|
|
1435
|
-
if (annotations.code) result = `\`${result}\``;
|
|
1436
|
-
if (annotations.strikethrough) result = `~~${result}~~`;
|
|
1437
|
-
if (annotations.italic) result = `_${result}_`;
|
|
1438
|
-
if (annotations.bold) result = `**${result}**`;
|
|
1439
|
-
return result;
|
|
1440
|
-
}
|
|
1662
|
+
// src/commands/db/query.ts
|
|
1663
|
+
import { Command as Command10 } from "commander";
|
|
1441
1664
|
|
|
1442
|
-
// src/
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
const b = block;
|
|
1464
|
-
return `### ${richTextToMd(b.heading_3.rich_text)}
|
|
1465
|
-
`;
|
|
1466
|
-
},
|
|
1467
|
-
bulleted_list_item(block, ctx) {
|
|
1468
|
-
const b = block;
|
|
1469
|
-
const text = richTextToMd(b.bulleted_list_item.rich_text);
|
|
1470
|
-
const header = `- ${text}
|
|
1471
|
-
`;
|
|
1472
|
-
if (ctx?.childrenMd) {
|
|
1473
|
-
return header + indentChildren(ctx.childrenMd);
|
|
1665
|
+
// src/services/database.service.ts
|
|
1666
|
+
import { isFullPage } from "@notionhq/client";
|
|
1667
|
+
async function fetchDatabaseSchema(client, dbId) {
|
|
1668
|
+
const ds = await client.dataSources.retrieve({ data_source_id: dbId });
|
|
1669
|
+
const title = "title" in ds ? ds.title.map((rt) => rt.plain_text).join("") || dbId : dbId;
|
|
1670
|
+
const properties = {};
|
|
1671
|
+
if ("properties" in ds) {
|
|
1672
|
+
for (const [name, prop] of Object.entries(ds.properties)) {
|
|
1673
|
+
const config = {
|
|
1674
|
+
id: prop.id,
|
|
1675
|
+
name,
|
|
1676
|
+
type: prop.type
|
|
1677
|
+
};
|
|
1678
|
+
if (prop.type === "select" && "select" in prop) {
|
|
1679
|
+
config.options = prop.select.options;
|
|
1680
|
+
} else if (prop.type === "status" && "status" in prop) {
|
|
1681
|
+
config.options = prop.status.options;
|
|
1682
|
+
} else if (prop.type === "multi_select" && "multi_select" in prop) {
|
|
1683
|
+
config.options = prop.multi_select.options;
|
|
1684
|
+
}
|
|
1685
|
+
properties[name] = config;
|
|
1474
1686
|
}
|
|
1475
|
-
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
const
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
return `\`\`\`${lang}
|
|
1494
|
-
${content}
|
|
1495
|
-
\`\`\`
|
|
1496
|
-
`;
|
|
1497
|
-
},
|
|
1498
|
-
quote(block) {
|
|
1499
|
-
const b = block;
|
|
1500
|
-
return `> ${richTextToMd(b.quote.rich_text)}
|
|
1501
|
-
`;
|
|
1502
|
-
},
|
|
1503
|
-
divider() {
|
|
1504
|
-
return "---\n";
|
|
1505
|
-
},
|
|
1506
|
-
callout(block) {
|
|
1507
|
-
const b = block;
|
|
1508
|
-
const text = richTextToMd(b.callout.rich_text);
|
|
1509
|
-
const icon = b.callout.icon;
|
|
1510
|
-
if (icon?.type === "emoji") {
|
|
1511
|
-
return `> ${icon.emoji} ${text}
|
|
1512
|
-
`;
|
|
1687
|
+
}
|
|
1688
|
+
return { id: dbId, title, properties };
|
|
1689
|
+
}
|
|
1690
|
+
async function queryDatabase(client, dbId, opts = {}) {
|
|
1691
|
+
const rawPages = await paginateResults(
|
|
1692
|
+
(cursor) => client.dataSources.query({
|
|
1693
|
+
data_source_id: dbId,
|
|
1694
|
+
filter: opts.filter,
|
|
1695
|
+
sorts: opts.sorts,
|
|
1696
|
+
start_cursor: cursor,
|
|
1697
|
+
page_size: 100
|
|
1698
|
+
})
|
|
1699
|
+
);
|
|
1700
|
+
return rawPages.filter(isFullPage).map((page) => {
|
|
1701
|
+
const propValues = {};
|
|
1702
|
+
for (const [name, prop] of Object.entries(page.properties)) {
|
|
1703
|
+
if (opts.columns && !opts.columns.includes(name)) continue;
|
|
1704
|
+
propValues[name] = displayPropertyValue(prop);
|
|
1513
1705
|
}
|
|
1514
|
-
return
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
if (
|
|
1522
|
-
|
|
1706
|
+
return { id: page.id, properties: propValues, raw: page };
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
function buildFilter(filterStrings, schema) {
|
|
1710
|
+
if (!filterStrings.length) return void 0;
|
|
1711
|
+
const filters = filterStrings.map((raw) => {
|
|
1712
|
+
const eqIdx = raw.indexOf("=");
|
|
1713
|
+
if (eqIdx === -1) {
|
|
1714
|
+
throw new CliError(
|
|
1715
|
+
ErrorCodes.INVALID_ARG,
|
|
1716
|
+
`Invalid filter syntax: "${raw}"`,
|
|
1717
|
+
'Use format: --filter "PropertyName=Value"'
|
|
1718
|
+
);
|
|
1523
1719
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1720
|
+
const propName = raw.slice(0, eqIdx).trim();
|
|
1721
|
+
const value = raw.slice(eqIdx + 1).trim();
|
|
1722
|
+
const propConfig = schema.properties[propName];
|
|
1723
|
+
if (!propConfig) {
|
|
1724
|
+
const available = Object.keys(schema.properties).join(", ");
|
|
1725
|
+
throw new CliError(
|
|
1726
|
+
ErrorCodes.INVALID_ARG,
|
|
1727
|
+
`Property "${propName}" not found`,
|
|
1728
|
+
`Available properties: ${available}`
|
|
1729
|
+
);
|
|
1534
1730
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
if (converter) {
|
|
1565
|
-
return converter(block, ctx);
|
|
1731
|
+
return buildPropertyFilter(propName, propConfig.type, value);
|
|
1732
|
+
});
|
|
1733
|
+
return filters.length === 1 ? filters[0] : { and: filters };
|
|
1734
|
+
}
|
|
1735
|
+
function buildPropertyFilter(property, type, value) {
|
|
1736
|
+
switch (type) {
|
|
1737
|
+
case "select":
|
|
1738
|
+
return { property, select: { equals: value } };
|
|
1739
|
+
case "status":
|
|
1740
|
+
return { property, status: { equals: value } };
|
|
1741
|
+
case "multi_select":
|
|
1742
|
+
return { property, multi_select: { contains: value } };
|
|
1743
|
+
case "checkbox":
|
|
1744
|
+
return { property, checkbox: { equals: value.toLowerCase() === "true" } };
|
|
1745
|
+
case "number":
|
|
1746
|
+
return { property, number: { equals: Number(value) } };
|
|
1747
|
+
case "title":
|
|
1748
|
+
return { property, title: { contains: value } };
|
|
1749
|
+
case "rich_text":
|
|
1750
|
+
return { property, rich_text: { contains: value } };
|
|
1751
|
+
case "url":
|
|
1752
|
+
return { property, url: { contains: value } };
|
|
1753
|
+
case "email":
|
|
1754
|
+
return { property, email: { contains: value } };
|
|
1755
|
+
default:
|
|
1756
|
+
throw new CliError(
|
|
1757
|
+
ErrorCodes.INVALID_ARG,
|
|
1758
|
+
`Filtering by property type "${type}" is not supported`
|
|
1759
|
+
);
|
|
1566
1760
|
}
|
|
1567
|
-
return `<!-- unsupported block: ${block.type} -->
|
|
1568
|
-
`;
|
|
1569
1761
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1762
|
+
function buildSorts(sortStrings) {
|
|
1763
|
+
return sortStrings.map((raw) => {
|
|
1764
|
+
const colonIdx = raw.lastIndexOf(":");
|
|
1765
|
+
if (colonIdx === -1) {
|
|
1766
|
+
return { property: raw.trim(), direction: "ascending" };
|
|
1767
|
+
}
|
|
1768
|
+
const property = raw.slice(0, colonIdx).trim();
|
|
1769
|
+
const dir = raw.slice(colonIdx + 1).trim().toLowerCase();
|
|
1770
|
+
return {
|
|
1771
|
+
property,
|
|
1772
|
+
direction: dir === "desc" || dir === "descending" ? "descending" : "ascending"
|
|
1773
|
+
};
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
function displayFormula(f) {
|
|
1777
|
+
if (f.type === "string") return f.string ?? "";
|
|
1778
|
+
if (f.type === "number")
|
|
1779
|
+
return f.number !== null && f.number !== void 0 ? String(f.number) : "";
|
|
1780
|
+
if (f.type === "boolean") return f.boolean ? "true" : "false";
|
|
1781
|
+
if (f.type === "date") return f.date?.start ?? "";
|
|
1782
|
+
return "";
|
|
1783
|
+
}
|
|
1784
|
+
function displayDate(date) {
|
|
1785
|
+
if (!date) return "";
|
|
1786
|
+
return date.end ? `${date.start} \u2192 ${date.end}` : date.start;
|
|
1787
|
+
}
|
|
1788
|
+
function displayPropertyValue(prop) {
|
|
1573
1789
|
switch (prop.type) {
|
|
1574
1790
|
case "title":
|
|
1575
|
-
return prop.title.map((
|
|
1791
|
+
return prop.title.map((r) => r.plain_text).join("").replace(/\n/g, " ");
|
|
1576
1792
|
case "rich_text":
|
|
1577
|
-
return prop.rich_text.map((
|
|
1793
|
+
return prop.rich_text.map((r) => r.plain_text).join("").replace(/\n/g, " ");
|
|
1578
1794
|
case "number":
|
|
1579
|
-
return prop.number !== null ? String(prop.number) : "";
|
|
1795
|
+
return prop.number !== null && prop.number !== void 0 ? String(prop.number) : "";
|
|
1580
1796
|
case "select":
|
|
1581
1797
|
return prop.select?.name ?? "";
|
|
1582
1798
|
case "status":
|
|
@@ -1584,10 +1800,9 @@ function formatPropertyValue(name, prop) {
|
|
|
1584
1800
|
case "multi_select":
|
|
1585
1801
|
return prop.multi_select.map((s) => s.name).join(", ");
|
|
1586
1802
|
case "date":
|
|
1587
|
-
|
|
1588
|
-
return prop.date.end ? `${prop.date.start} \u2192 ${prop.date.end}` : prop.date.start;
|
|
1803
|
+
return displayDate(prop.date);
|
|
1589
1804
|
case "checkbox":
|
|
1590
|
-
return prop.checkbox ? "
|
|
1805
|
+
return prop.checkbox ? "\u2713" : "\u2717";
|
|
1591
1806
|
case "url":
|
|
1592
1807
|
return prop.url ?? "";
|
|
1593
1808
|
case "email":
|
|
@@ -1597,35 +1812,13 @@ function formatPropertyValue(name, prop) {
|
|
|
1597
1812
|
case "people":
|
|
1598
1813
|
return prop.people.map((p) => "name" in p && p.name ? p.name : p.id).join(", ");
|
|
1599
1814
|
case "relation":
|
|
1600
|
-
return prop.relation.
|
|
1601
|
-
case "formula":
|
|
1602
|
-
|
|
1603
|
-
if (f.type === "string") return f.string ?? "";
|
|
1604
|
-
if (f.type === "number") return f.number !== null ? String(f.number) : "";
|
|
1605
|
-
if (f.type === "boolean") return String(f.boolean);
|
|
1606
|
-
if (f.type === "date") return f.date?.start ?? "";
|
|
1607
|
-
return "";
|
|
1608
|
-
}
|
|
1609
|
-
case "rollup": {
|
|
1610
|
-
const r = prop.rollup;
|
|
1611
|
-
if (r.type === "number") return r.number !== null ? String(r.number) : "";
|
|
1612
|
-
if (r.type === "date") return r.date?.start ?? "";
|
|
1613
|
-
if (r.type === "array") return `[${r.array.length} items]`;
|
|
1614
|
-
return "";
|
|
1615
|
-
}
|
|
1815
|
+
return prop.relation.length > 0 ? `[${prop.relation.length}]` : "";
|
|
1816
|
+
case "formula":
|
|
1817
|
+
return displayFormula(prop.formula);
|
|
1616
1818
|
case "created_time":
|
|
1617
1819
|
return prop.created_time;
|
|
1618
1820
|
case "last_edited_time":
|
|
1619
1821
|
return prop.last_edited_time;
|
|
1620
|
-
case "created_by":
|
|
1621
|
-
return "name" in prop.created_by ? prop.created_by.name ?? prop.created_by.id : prop.created_by.id;
|
|
1622
|
-
case "last_edited_by":
|
|
1623
|
-
return "name" in prop.last_edited_by ? prop.last_edited_by.name ?? prop.last_edited_by.id : prop.last_edited_by.id;
|
|
1624
|
-
case "files":
|
|
1625
|
-
return prop.files.map((f) => {
|
|
1626
|
-
if (f.type === "external") return f.external.url;
|
|
1627
|
-
return f.name;
|
|
1628
|
-
}).join(", ");
|
|
1629
1822
|
case "unique_id":
|
|
1630
1823
|
return prop.unique_id.prefix ? `${prop.unique_id.prefix}-${prop.unique_id.number}` : String(prop.unique_id.number ?? "");
|
|
1631
1824
|
default:
|
|
@@ -1633,295 +1826,452 @@ function formatPropertyValue(name, prop) {
|
|
|
1633
1826
|
}
|
|
1634
1827
|
}
|
|
1635
1828
|
|
|
1636
|
-
// src/
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1829
|
+
// src/commands/db/query.ts
|
|
1830
|
+
var SKIP_TYPES_IN_AUTO = /* @__PURE__ */ new Set(["relation", "rich_text", "people"]);
|
|
1831
|
+
function autoSelectColumns(schema, entries) {
|
|
1832
|
+
const termWidth = process.stdout.columns || 120;
|
|
1833
|
+
const COL_SEP = 2;
|
|
1834
|
+
const candidates = Object.values(schema.properties).filter((p) => !SKIP_TYPES_IN_AUTO.has(p.type)).map((p) => p.name);
|
|
1835
|
+
const widths = candidates.map((col) => {
|
|
1836
|
+
const header = col.toUpperCase().length;
|
|
1837
|
+
const maxData = entries.reduce(
|
|
1838
|
+
(max, e) => Math.max(max, (e.properties[col] ?? "").length),
|
|
1839
|
+
0
|
|
1840
|
+
);
|
|
1841
|
+
return Math.min(Math.max(header, maxData), 40);
|
|
1842
|
+
});
|
|
1843
|
+
const selected = [];
|
|
1844
|
+
let usedWidth = 0;
|
|
1845
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
1846
|
+
const needed = (selected.length > 0 ? COL_SEP : 0) + widths[i];
|
|
1847
|
+
if (usedWidth + needed > termWidth) break;
|
|
1848
|
+
selected.push(candidates[i]);
|
|
1849
|
+
usedWidth += needed;
|
|
1644
1850
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
}
|
|
1648
|
-
function renderBlockTree(blocks) {
|
|
1649
|
-
const parts = [];
|
|
1650
|
-
let listCounter = 0;
|
|
1651
|
-
for (const node of blocks) {
|
|
1652
|
-
if (node.block.type === "numbered_list_item") {
|
|
1653
|
-
listCounter++;
|
|
1654
|
-
} else {
|
|
1655
|
-
listCounter = 0;
|
|
1656
|
-
}
|
|
1657
|
-
const childrenMd = node.children.length > 0 ? renderBlockTree(node.children) : "";
|
|
1658
|
-
const md = blockToMd(node.block, {
|
|
1659
|
-
listNumber: node.block.type === "numbered_list_item" ? listCounter : void 0,
|
|
1660
|
-
childrenMd: childrenMd || void 0
|
|
1661
|
-
});
|
|
1662
|
-
parts.push(md);
|
|
1851
|
+
if (selected.length === 0 && candidates.length > 0) {
|
|
1852
|
+
selected.push(candidates[0]);
|
|
1663
1853
|
}
|
|
1664
|
-
return
|
|
1854
|
+
return selected;
|
|
1665
1855
|
}
|
|
1666
|
-
function
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1856
|
+
function dbQueryCommand() {
|
|
1857
|
+
return new Command10("query").description("Query database entries with optional filtering and sorting").argument("<id>", "Notion database ID or URL").option(
|
|
1858
|
+
"--filter <filter>",
|
|
1859
|
+
'Filter entries (repeatable): --filter "Status=Done"',
|
|
1860
|
+
collect,
|
|
1861
|
+
[]
|
|
1862
|
+
).option(
|
|
1863
|
+
"--sort <sort>",
|
|
1864
|
+
'Sort entries (repeatable): --sort "Name:asc"',
|
|
1865
|
+
collect,
|
|
1866
|
+
[]
|
|
1867
|
+
).option(
|
|
1868
|
+
"--columns <columns>",
|
|
1869
|
+
'Comma-separated list of columns to display: --columns "Title,Status"'
|
|
1870
|
+
).option("--json", "Output raw JSON").action(
|
|
1871
|
+
withErrorHandling(
|
|
1872
|
+
async (id, options) => {
|
|
1873
|
+
const { token } = await resolveToken();
|
|
1874
|
+
const client = createNotionClient(token);
|
|
1875
|
+
const dbId = parseNotionId(id);
|
|
1876
|
+
const schema = await fetchDatabaseSchema(client, dbId);
|
|
1877
|
+
const columns = options.columns ? options.columns.split(",").map((c2) => c2.trim()) : void 0;
|
|
1878
|
+
const filter = options.filter.length ? buildFilter(options.filter, schema) : void 0;
|
|
1879
|
+
const sorts = options.sort.length ? buildSorts(options.sort) : void 0;
|
|
1880
|
+
const entries = await queryDatabase(client, dbId, {
|
|
1881
|
+
filter,
|
|
1882
|
+
sorts,
|
|
1883
|
+
columns
|
|
1884
|
+
});
|
|
1885
|
+
if (options.json) {
|
|
1886
|
+
process.stdout.write(`${formatJSON(entries.map((e) => e.raw))}
|
|
1887
|
+
`);
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
if (entries.length === 0) {
|
|
1891
|
+
process.stdout.write("No entries found.\n");
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
const displayColumns = columns ?? autoSelectColumns(schema, entries);
|
|
1895
|
+
const headers = displayColumns.map((c2) => c2.toUpperCase());
|
|
1896
|
+
const rows = entries.map(
|
|
1897
|
+
(entry) => displayColumns.map((col) => entry.properties[col] ?? "")
|
|
1898
|
+
);
|
|
1899
|
+
process.stdout.write(`${formatTable(rows, headers)}
|
|
1900
|
+
`);
|
|
1901
|
+
process.stderr.write(`${entries.length} entries
|
|
1902
|
+
`);
|
|
1903
|
+
}
|
|
1904
|
+
)
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
function collect(value, previous) {
|
|
1908
|
+
return previous.concat([value]);
|
|
1670
1909
|
}
|
|
1671
1910
|
|
|
1672
|
-
// src/
|
|
1673
|
-
import {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
inFence = true;
|
|
1686
|
-
fenceLang = fenceMatch[1] ?? "";
|
|
1687
|
-
fenceLines = [];
|
|
1688
|
-
continue;
|
|
1689
|
-
}
|
|
1690
|
-
if (line === "```" && inFence) {
|
|
1691
|
-
inFence = false;
|
|
1692
|
-
const header = fenceLang ? c.dim(`[${fenceLang}]`) : "";
|
|
1693
|
-
if (header) out.push(header);
|
|
1694
|
-
for (const fl of fenceLines) {
|
|
1695
|
-
out.push(c.green(" " + fl));
|
|
1911
|
+
// src/commands/db/schema.ts
|
|
1912
|
+
import { Command as Command11 } from "commander";
|
|
1913
|
+
function dbSchemaCommand() {
|
|
1914
|
+
return new Command11("schema").description("Show database schema (property names, types, and options)").argument("<id>", "Notion database ID or URL").option("--json", "Output raw JSON").action(
|
|
1915
|
+
withErrorHandling(async (id, options) => {
|
|
1916
|
+
const { token } = await resolveToken();
|
|
1917
|
+
const client = createNotionClient(token);
|
|
1918
|
+
const dbId = parseNotionId(id);
|
|
1919
|
+
const schema = await fetchDatabaseSchema(client, dbId);
|
|
1920
|
+
if (options.json) {
|
|
1921
|
+
process.stdout.write(`${formatJSON(schema)}
|
|
1922
|
+
`);
|
|
1923
|
+
return;
|
|
1696
1924
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
if (line === "---") {
|
|
1736
|
-
out.push(c.dim("\u2500".repeat(40)));
|
|
1737
|
-
continue;
|
|
1738
|
-
}
|
|
1739
|
-
const propMatch = line.match(/^([A-Za-z_][A-Za-z0-9_ ]*): (.+)$/);
|
|
1740
|
-
if (propMatch) {
|
|
1741
|
-
out.push(c.dim(propMatch[1] + ": ") + c.white(propMatch[2]));
|
|
1742
|
-
continue;
|
|
1743
|
-
}
|
|
1744
|
-
const bulletMatch = line.match(/^(\s*)- (\[[ x]\] )?(.+)/);
|
|
1745
|
-
if (bulletMatch) {
|
|
1746
|
-
const indent = bulletMatch[1] ?? "";
|
|
1747
|
-
const checkbox = bulletMatch[2];
|
|
1748
|
-
const text = bulletMatch[3] ?? "";
|
|
1749
|
-
if (checkbox) {
|
|
1750
|
-
const checked = checkbox.trim() === "[x]";
|
|
1751
|
-
const box = checked ? c.green("\u2611") : c.dim("\u2610");
|
|
1752
|
-
out.push(indent + box + " " + renderInline(text));
|
|
1753
|
-
} else {
|
|
1754
|
-
out.push(indent + c.cyan("\u2022") + " " + renderInline(text));
|
|
1925
|
+
const headers = ["PROPERTY", "TYPE", "OPTIONS"];
|
|
1926
|
+
const rows = Object.values(schema.properties).map((prop) => [
|
|
1927
|
+
prop.name,
|
|
1928
|
+
prop.type,
|
|
1929
|
+
prop.options ? prop.options.map((o) => o.name).join(", ") : ""
|
|
1930
|
+
]);
|
|
1931
|
+
process.stdout.write(`${formatTable(rows, headers)}
|
|
1932
|
+
`);
|
|
1933
|
+
})
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// src/commands/ls.ts
|
|
1938
|
+
import { isFullPageOrDataSource } from "@notionhq/client";
|
|
1939
|
+
import { Command as Command12 } from "commander";
|
|
1940
|
+
function getTitle(item) {
|
|
1941
|
+
if (item.object === "data_source") {
|
|
1942
|
+
return item.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
1943
|
+
}
|
|
1944
|
+
const titleProp = Object.values(item.properties).find(
|
|
1945
|
+
(p) => p.type === "title"
|
|
1946
|
+
);
|
|
1947
|
+
if (titleProp?.type === "title") {
|
|
1948
|
+
return titleProp.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
1949
|
+
}
|
|
1950
|
+
return "(untitled)";
|
|
1951
|
+
}
|
|
1952
|
+
function displayType(item) {
|
|
1953
|
+
return item.object === "data_source" ? "database" : item.object;
|
|
1954
|
+
}
|
|
1955
|
+
function lsCommand() {
|
|
1956
|
+
const cmd = new Command12("ls");
|
|
1957
|
+
cmd.description("list accessible Notion pages and databases").option(
|
|
1958
|
+
"--type <type>",
|
|
1959
|
+
"filter by object type (page or database)",
|
|
1960
|
+
(val) => {
|
|
1961
|
+
if (val !== "page" && val !== "database") {
|
|
1962
|
+
throw new Error('--type must be "page" or "database"');
|
|
1755
1963
|
}
|
|
1756
|
-
|
|
1757
|
-
}
|
|
1758
|
-
const numMatch = line.match(/^(\s*)(\d+)\. (.+)/);
|
|
1759
|
-
if (numMatch) {
|
|
1760
|
-
const indent = numMatch[1] ?? "";
|
|
1761
|
-
const num = numMatch[2] ?? "";
|
|
1762
|
-
const text = numMatch[3] ?? "";
|
|
1763
|
-
out.push(indent + c.cyan(num + ".") + " " + renderInline(text));
|
|
1764
|
-
continue;
|
|
1964
|
+
return val;
|
|
1765
1965
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1966
|
+
).option(
|
|
1967
|
+
"--cursor <cursor>",
|
|
1968
|
+
"start from this pagination cursor (from a previous --next hint)"
|
|
1969
|
+
).option("--json", "force JSON output").action(
|
|
1970
|
+
withErrorHandling(
|
|
1971
|
+
async (opts) => {
|
|
1972
|
+
if (opts.json) {
|
|
1973
|
+
setOutputMode("json");
|
|
1974
|
+
}
|
|
1975
|
+
const { token, source } = await resolveToken();
|
|
1976
|
+
reportTokenSource(source);
|
|
1977
|
+
const notion = createNotionClient(token);
|
|
1978
|
+
const response = await notion.search({
|
|
1979
|
+
start_cursor: opts.cursor,
|
|
1980
|
+
page_size: 20
|
|
1981
|
+
});
|
|
1982
|
+
let items = response.results.filter(
|
|
1983
|
+
(r) => isFullPageOrDataSource(r)
|
|
1984
|
+
);
|
|
1985
|
+
if (opts.type) {
|
|
1986
|
+
const filterType = opts.type;
|
|
1987
|
+
items = items.filter(
|
|
1988
|
+
(r) => filterType === "database" ? r.object === "data_source" : r.object === filterType
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
if (items.length === 0) {
|
|
1992
|
+
process.stdout.write("No accessible content found\n");
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
const headers = ["TYPE", "TITLE", "ID", "MODIFIED"];
|
|
1996
|
+
const rows = items.map((item) => [
|
|
1997
|
+
displayType(item),
|
|
1998
|
+
getTitle(item),
|
|
1999
|
+
item.id,
|
|
2000
|
+
item.last_edited_time.split("T")[0]
|
|
2001
|
+
]);
|
|
2002
|
+
printOutput(items, headers, rows);
|
|
2003
|
+
if (response.has_more && response.next_cursor) {
|
|
2004
|
+
process.stderr.write(
|
|
2005
|
+
`
|
|
2006
|
+
--next: notion ls --cursor ${response.next_cursor}
|
|
2007
|
+
`
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
)
|
|
2012
|
+
);
|
|
2013
|
+
return cmd;
|
|
1769
2014
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2015
|
+
|
|
2016
|
+
// src/commands/open.ts
|
|
2017
|
+
import { exec } from "child_process";
|
|
2018
|
+
import { promisify } from "util";
|
|
2019
|
+
import { Command as Command13 } from "commander";
|
|
2020
|
+
var execAsync = promisify(exec);
|
|
2021
|
+
function openCommand() {
|
|
2022
|
+
const cmd = new Command13("open");
|
|
2023
|
+
cmd.description("open a Notion page in the default browser").argument("<id/url>", "Notion page ID or URL").action(
|
|
2024
|
+
withErrorHandling(async (idOrUrl) => {
|
|
2025
|
+
const id = parseNotionId(idOrUrl);
|
|
2026
|
+
const url = `https://www.notion.so/${id}`;
|
|
2027
|
+
const platform = process.platform;
|
|
2028
|
+
const opener = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
2029
|
+
await execAsync(`${opener} "${url}"`);
|
|
2030
|
+
process.stdout.write(`Opening ${url}
|
|
2031
|
+
`);
|
|
2032
|
+
})
|
|
2033
|
+
);
|
|
2034
|
+
return cmd;
|
|
1779
2035
|
}
|
|
1780
2036
|
|
|
1781
|
-
// src/commands/
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
const
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
2037
|
+
// src/commands/profile/list.ts
|
|
2038
|
+
import { Command as Command14 } from "commander";
|
|
2039
|
+
function profileListCommand() {
|
|
2040
|
+
const cmd = new Command14("list");
|
|
2041
|
+
cmd.description("list all authentication profiles").action(
|
|
2042
|
+
withErrorHandling(async () => {
|
|
2043
|
+
const config = await readGlobalConfig();
|
|
2044
|
+
const profiles = config.profiles ?? {};
|
|
2045
|
+
const profileNames = Object.keys(profiles);
|
|
2046
|
+
if (profileNames.length === 0) {
|
|
2047
|
+
process.stdout.write(
|
|
2048
|
+
"No profiles configured. Run `notion auth login` to get started.\n"
|
|
2049
|
+
);
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
for (const name of profileNames) {
|
|
2053
|
+
const profile = profiles[name];
|
|
2054
|
+
const isActive = config.active_profile === name;
|
|
2055
|
+
const marker = isActive ? bold("* ") : " ";
|
|
2056
|
+
const activeLabel = isActive ? " (active)" : "";
|
|
2057
|
+
const workspaceInfo = profile.workspace_name ? dim(` \u2014 ${profile.workspace_name}`) : "";
|
|
2058
|
+
process.stdout.write(
|
|
2059
|
+
`${marker}${name}${activeLabel}${workspaceInfo}
|
|
2060
|
+
`
|
|
2061
|
+
);
|
|
1798
2062
|
}
|
|
1799
2063
|
})
|
|
1800
2064
|
);
|
|
2065
|
+
return cmd;
|
|
1801
2066
|
}
|
|
1802
2067
|
|
|
1803
|
-
// src/commands/
|
|
2068
|
+
// src/commands/profile/use.ts
|
|
1804
2069
|
import { Command as Command15 } from "commander";
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
async
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
type: prop.type
|
|
1818
|
-
};
|
|
1819
|
-
if (prop.type === "select" && "select" in prop) {
|
|
1820
|
-
config.options = prop.select.options;
|
|
1821
|
-
} else if (prop.type === "status" && "status" in prop) {
|
|
1822
|
-
config.options = prop.status.options;
|
|
1823
|
-
} else if (prop.type === "multi_select" && "multi_select" in prop) {
|
|
1824
|
-
config.options = prop.multi_select.options;
|
|
2070
|
+
function profileUseCommand() {
|
|
2071
|
+
const cmd = new Command15("use");
|
|
2072
|
+
cmd.description("switch the active profile").argument("<name>", "profile name to activate").action(
|
|
2073
|
+
withErrorHandling(async (name) => {
|
|
2074
|
+
const config = await readGlobalConfig();
|
|
2075
|
+
const profiles = config.profiles ?? {};
|
|
2076
|
+
if (!profiles[name]) {
|
|
2077
|
+
throw new CliError(
|
|
2078
|
+
ErrorCodes.AUTH_PROFILE_NOT_FOUND,
|
|
2079
|
+
`Profile "${name}" not found.`,
|
|
2080
|
+
`Run "notion auth list" to see available profiles`
|
|
2081
|
+
);
|
|
1825
2082
|
}
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
}
|
|
1831
|
-
async function queryDatabase(client, dbId, opts = {}) {
|
|
1832
|
-
const rawPages = await paginateResults(
|
|
1833
|
-
(cursor) => client.dataSources.query({
|
|
1834
|
-
data_source_id: dbId,
|
|
1835
|
-
filter: opts.filter,
|
|
1836
|
-
sorts: opts.sorts,
|
|
1837
|
-
start_cursor: cursor,
|
|
1838
|
-
page_size: 100
|
|
2083
|
+
await writeGlobalConfig({
|
|
2084
|
+
...config,
|
|
2085
|
+
active_profile: name
|
|
2086
|
+
});
|
|
2087
|
+
stderrWrite(success(`Switched to profile "${name}".`));
|
|
1839
2088
|
})
|
|
1840
2089
|
);
|
|
1841
|
-
return
|
|
1842
|
-
const propValues = {};
|
|
1843
|
-
for (const [name, prop] of Object.entries(page.properties)) {
|
|
1844
|
-
if (opts.columns && !opts.columns.includes(name)) continue;
|
|
1845
|
-
propValues[name] = displayPropertyValue(prop);
|
|
1846
|
-
}
|
|
1847
|
-
return { id: page.id, properties: propValues, raw: page };
|
|
1848
|
-
});
|
|
2090
|
+
return cmd;
|
|
1849
2091
|
}
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
`Invalid filter syntax: "${raw}"`,
|
|
1858
|
-
'Use format: --filter "PropertyName=Value"'
|
|
1859
|
-
);
|
|
1860
|
-
}
|
|
1861
|
-
const propName = raw.slice(0, eqIdx).trim();
|
|
1862
|
-
const value = raw.slice(eqIdx + 1).trim();
|
|
1863
|
-
const propConfig = schema.properties[propName];
|
|
1864
|
-
if (!propConfig) {
|
|
1865
|
-
const available = Object.keys(schema.properties).join(", ");
|
|
1866
|
-
throw new CliError(
|
|
1867
|
-
ErrorCodes.INVALID_ARG,
|
|
1868
|
-
`Property "${propName}" not found`,
|
|
1869
|
-
`Available properties: ${available}`
|
|
1870
|
-
);
|
|
1871
|
-
}
|
|
1872
|
-
return buildPropertyFilter(propName, propConfig.type, value);
|
|
1873
|
-
});
|
|
1874
|
-
return filters.length === 1 ? filters[0] : { and: filters };
|
|
2092
|
+
|
|
2093
|
+
// src/commands/read.ts
|
|
2094
|
+
import { Command as Command16 } from "commander";
|
|
2095
|
+
|
|
2096
|
+
// src/blocks/rich-text.ts
|
|
2097
|
+
function richTextToMd(richText) {
|
|
2098
|
+
return richText.map(segmentToMd).join("");
|
|
1875
2099
|
}
|
|
1876
|
-
function
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
return { property, multi_select: { contains: value } };
|
|
1884
|
-
case "checkbox":
|
|
1885
|
-
return { property, checkbox: { equals: value.toLowerCase() === "true" } };
|
|
1886
|
-
case "number":
|
|
1887
|
-
return { property, number: { equals: Number(value) } };
|
|
1888
|
-
case "title":
|
|
1889
|
-
return { property, title: { contains: value } };
|
|
1890
|
-
case "rich_text":
|
|
1891
|
-
return { property, rich_text: { contains: value } };
|
|
1892
|
-
case "url":
|
|
1893
|
-
return { property, url: { contains: value } };
|
|
1894
|
-
case "email":
|
|
1895
|
-
return { property, email: { contains: value } };
|
|
1896
|
-
default:
|
|
1897
|
-
throw new CliError(
|
|
1898
|
-
ErrorCodes.INVALID_ARG,
|
|
1899
|
-
`Filtering by property type "${type}" is not supported`
|
|
1900
|
-
);
|
|
2100
|
+
function segmentToMd(segment) {
|
|
2101
|
+
if (segment.type === "equation") {
|
|
2102
|
+
return `$${segment.equation.expression}$`;
|
|
2103
|
+
}
|
|
2104
|
+
if (segment.type === "mention") {
|
|
2105
|
+
const text = segment.plain_text;
|
|
2106
|
+
return segment.href ? `[${text}](${segment.href})` : text;
|
|
1901
2107
|
}
|
|
2108
|
+
const annotated = applyAnnotations(segment.text.content, segment.annotations);
|
|
2109
|
+
return segment.text.link ? `[${annotated}](${segment.text.link.url})` : annotated;
|
|
1902
2110
|
}
|
|
1903
|
-
function
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2111
|
+
function applyAnnotations(text, annotations) {
|
|
2112
|
+
let result = text;
|
|
2113
|
+
if (annotations.code) result = `\`${result}\``;
|
|
2114
|
+
if (annotations.strikethrough) result = `~~${result}~~`;
|
|
2115
|
+
if (annotations.italic) result = `_${result}_`;
|
|
2116
|
+
if (annotations.bold) result = `**${result}**`;
|
|
2117
|
+
return result;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
// src/blocks/converters.ts
|
|
2121
|
+
function indentChildren(childrenMd) {
|
|
2122
|
+
return `${childrenMd.split("\n").filter(Boolean).map((line) => ` ${line}`).join("\n")}
|
|
2123
|
+
`;
|
|
2124
|
+
}
|
|
2125
|
+
var converters = {
|
|
2126
|
+
paragraph(block) {
|
|
2127
|
+
const b = block;
|
|
2128
|
+
return `${richTextToMd(b.paragraph.rich_text)}
|
|
2129
|
+
`;
|
|
2130
|
+
},
|
|
2131
|
+
heading_1(block) {
|
|
2132
|
+
const b = block;
|
|
2133
|
+
return `# ${richTextToMd(b.heading_1.rich_text)}
|
|
2134
|
+
`;
|
|
2135
|
+
},
|
|
2136
|
+
heading_2(block) {
|
|
2137
|
+
const b = block;
|
|
2138
|
+
return `## ${richTextToMd(b.heading_2.rich_text)}
|
|
2139
|
+
`;
|
|
2140
|
+
},
|
|
2141
|
+
heading_3(block) {
|
|
2142
|
+
const b = block;
|
|
2143
|
+
return `### ${richTextToMd(b.heading_3.rich_text)}
|
|
2144
|
+
`;
|
|
2145
|
+
},
|
|
2146
|
+
bulleted_list_item(block, ctx) {
|
|
2147
|
+
const b = block;
|
|
2148
|
+
const text = richTextToMd(b.bulleted_list_item.rich_text);
|
|
2149
|
+
const header = `- ${text}
|
|
2150
|
+
`;
|
|
2151
|
+
if (ctx?.childrenMd) {
|
|
2152
|
+
return header + indentChildren(ctx.childrenMd);
|
|
2153
|
+
}
|
|
2154
|
+
return header;
|
|
2155
|
+
},
|
|
2156
|
+
numbered_list_item(block, ctx) {
|
|
2157
|
+
const b = block;
|
|
2158
|
+
const num = ctx?.listNumber ?? 1;
|
|
2159
|
+
return `${num}. ${richTextToMd(b.numbered_list_item.rich_text)}
|
|
2160
|
+
`;
|
|
2161
|
+
},
|
|
2162
|
+
to_do(block) {
|
|
2163
|
+
const b = block;
|
|
2164
|
+
const checkbox = b.to_do.checked ? "[x]" : "[ ]";
|
|
2165
|
+
return `- ${checkbox} ${richTextToMd(b.to_do.rich_text)}
|
|
2166
|
+
`;
|
|
2167
|
+
},
|
|
2168
|
+
code(block) {
|
|
2169
|
+
const b = block;
|
|
2170
|
+
const lang = b.code.language === "plain text" ? "" : b.code.language;
|
|
2171
|
+
const content = richTextToMd(b.code.rich_text);
|
|
2172
|
+
return `\`\`\`${lang}
|
|
2173
|
+
${content}
|
|
2174
|
+
\`\`\`
|
|
2175
|
+
`;
|
|
2176
|
+
},
|
|
2177
|
+
quote(block) {
|
|
2178
|
+
const b = block;
|
|
2179
|
+
return `> ${richTextToMd(b.quote.rich_text)}
|
|
2180
|
+
`;
|
|
2181
|
+
},
|
|
2182
|
+
divider() {
|
|
2183
|
+
return "---\n";
|
|
2184
|
+
},
|
|
2185
|
+
callout(block) {
|
|
2186
|
+
const b = block;
|
|
2187
|
+
const text = richTextToMd(b.callout.rich_text);
|
|
2188
|
+
const icon = b.callout.icon;
|
|
2189
|
+
if (icon?.type === "emoji") {
|
|
2190
|
+
return `> ${icon.emoji} ${text}
|
|
2191
|
+
`;
|
|
2192
|
+
}
|
|
2193
|
+
return `> ${text}
|
|
2194
|
+
`;
|
|
2195
|
+
},
|
|
2196
|
+
toggle(block, ctx) {
|
|
2197
|
+
const b = block;
|
|
2198
|
+
const header = `**${richTextToMd(b.toggle.rich_text)}**
|
|
2199
|
+
`;
|
|
2200
|
+
if (ctx?.childrenMd) {
|
|
2201
|
+
return header + ctx.childrenMd;
|
|
2202
|
+
}
|
|
2203
|
+
return header;
|
|
2204
|
+
},
|
|
2205
|
+
image(block) {
|
|
2206
|
+
const b = block;
|
|
2207
|
+
const caption = richTextToMd(b.image.caption);
|
|
2208
|
+
if (b.image.type === "file") {
|
|
2209
|
+
const url2 = b.image.file.url;
|
|
2210
|
+
const expiry = b.image.file.expiry_time;
|
|
2211
|
+
return ` <!-- expires: ${expiry} -->
|
|
2212
|
+
`;
|
|
1908
2213
|
}
|
|
1909
|
-
const
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2214
|
+
const url = b.image.external.url;
|
|
2215
|
+
return `
|
|
2216
|
+
`;
|
|
2217
|
+
},
|
|
2218
|
+
bookmark(block) {
|
|
2219
|
+
const b = block;
|
|
2220
|
+
const caption = richTextToMd(b.bookmark.caption);
|
|
2221
|
+
const text = caption || b.bookmark.url;
|
|
2222
|
+
return `[${text}](${b.bookmark.url})
|
|
2223
|
+
`;
|
|
2224
|
+
},
|
|
2225
|
+
child_page(block) {
|
|
2226
|
+
const b = block;
|
|
2227
|
+
return `### ${b.child_page.title}
|
|
2228
|
+
`;
|
|
2229
|
+
},
|
|
2230
|
+
child_database(block) {
|
|
2231
|
+
const b = block;
|
|
2232
|
+
return `### ${b.child_database.title}
|
|
2233
|
+
`;
|
|
2234
|
+
},
|
|
2235
|
+
link_preview(block) {
|
|
2236
|
+
const b = block;
|
|
2237
|
+
return `[${b.link_preview.url}](${b.link_preview.url})
|
|
2238
|
+
`;
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
function blockToMd(block, ctx) {
|
|
2242
|
+
const converter = converters[block.type];
|
|
2243
|
+
if (converter) {
|
|
2244
|
+
return converter(block, ctx);
|
|
2245
|
+
}
|
|
2246
|
+
return `<!-- unsupported block: ${block.type} -->
|
|
2247
|
+
`;
|
|
1916
2248
|
}
|
|
1917
|
-
|
|
2249
|
+
|
|
2250
|
+
// src/blocks/properties.ts
|
|
2251
|
+
function formatFormula(f) {
|
|
2252
|
+
if (f.type === "string") return f.string ?? "";
|
|
2253
|
+
if (f.type === "number") return f.number !== null ? String(f.number) : "";
|
|
2254
|
+
if (f.type === "boolean") return String(f.boolean);
|
|
2255
|
+
if (f.type === "date") return f.date?.start ?? "";
|
|
2256
|
+
return "";
|
|
2257
|
+
}
|
|
2258
|
+
function formatRollup(r) {
|
|
2259
|
+
if (r.type === "number") return r.number !== null ? String(r.number) : "";
|
|
2260
|
+
if (r.type === "date") return r.date?.start ?? "";
|
|
2261
|
+
if (r.type === "array") return `[${r.array.length} items]`;
|
|
2262
|
+
return "";
|
|
2263
|
+
}
|
|
2264
|
+
function formatUser(p) {
|
|
2265
|
+
return "name" in p && p.name ? p.name : p.id;
|
|
2266
|
+
}
|
|
2267
|
+
function formatPropertyValue(_name, prop) {
|
|
1918
2268
|
switch (prop.type) {
|
|
1919
2269
|
case "title":
|
|
1920
|
-
return prop.title.map((
|
|
2270
|
+
return prop.title.map((rt) => rt.plain_text).join("");
|
|
1921
2271
|
case "rich_text":
|
|
1922
|
-
return prop.rich_text.map((
|
|
2272
|
+
return prop.rich_text.map((rt) => rt.plain_text).join("");
|
|
1923
2273
|
case "number":
|
|
1924
|
-
return prop.number !== null
|
|
2274
|
+
return prop.number !== null ? String(prop.number) : "";
|
|
1925
2275
|
case "select":
|
|
1926
2276
|
return prop.select?.name ?? "";
|
|
1927
2277
|
case "status":
|
|
@@ -1929,9 +2279,10 @@ function displayPropertyValue(prop) {
|
|
|
1929
2279
|
case "multi_select":
|
|
1930
2280
|
return prop.multi_select.map((s) => s.name).join(", ");
|
|
1931
2281
|
case "date":
|
|
1932
|
-
|
|
2282
|
+
if (!prop.date) return "";
|
|
2283
|
+
return prop.date.end ? `${prop.date.start} \u2192 ${prop.date.end}` : prop.date.start;
|
|
1933
2284
|
case "checkbox":
|
|
1934
|
-
return prop.checkbox ? "
|
|
2285
|
+
return prop.checkbox ? "true" : "false";
|
|
1935
2286
|
case "url":
|
|
1936
2287
|
return prop.url ?? "";
|
|
1937
2288
|
case "email":
|
|
@@ -1939,22 +2290,23 @@ function displayPropertyValue(prop) {
|
|
|
1939
2290
|
case "phone_number":
|
|
1940
2291
|
return prop.phone_number ?? "";
|
|
1941
2292
|
case "people":
|
|
1942
|
-
return prop.people.map(
|
|
2293
|
+
return prop.people.map(formatUser).join(", ");
|
|
1943
2294
|
case "relation":
|
|
1944
|
-
return prop.relation.
|
|
1945
|
-
case "formula":
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
return f.number !== null && f.number !== void 0 ? String(f.number) : "";
|
|
1950
|
-
if (f.type === "boolean") return f.boolean ? "true" : "false";
|
|
1951
|
-
if (f.type === "date") return f.date?.start ?? "";
|
|
1952
|
-
return "";
|
|
1953
|
-
}
|
|
2295
|
+
return prop.relation.map((r) => r.id).join(", ");
|
|
2296
|
+
case "formula":
|
|
2297
|
+
return formatFormula(prop.formula);
|
|
2298
|
+
case "rollup":
|
|
2299
|
+
return formatRollup(prop.rollup);
|
|
1954
2300
|
case "created_time":
|
|
1955
2301
|
return prop.created_time;
|
|
1956
2302
|
case "last_edited_time":
|
|
1957
2303
|
return prop.last_edited_time;
|
|
2304
|
+
case "created_by":
|
|
2305
|
+
return "name" in prop.created_by ? prop.created_by.name ?? prop.created_by.id : prop.created_by.id;
|
|
2306
|
+
case "last_edited_by":
|
|
2307
|
+
return "name" in prop.last_edited_by ? prop.last_edited_by.name ?? prop.last_edited_by.id : prop.last_edited_by.id;
|
|
2308
|
+
case "files":
|
|
2309
|
+
return prop.files.map((f) => f.type === "external" ? f.external.url : f.name).join(", ");
|
|
1958
2310
|
case "unique_id":
|
|
1959
2311
|
return prop.unique_id.prefix ? `${prop.unique_id.prefix}-${prop.unique_id.number}` : String(prop.unique_id.number ?? "");
|
|
1960
2312
|
default:
|
|
@@ -1962,375 +2314,354 @@ function displayPropertyValue(prop) {
|
|
|
1962
2314
|
}
|
|
1963
2315
|
}
|
|
1964
2316
|
|
|
1965
|
-
// src/
|
|
1966
|
-
function
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
if (options.json) {
|
|
1974
|
-
process.stdout.write(formatJSON(schema) + "\n");
|
|
1975
|
-
return;
|
|
1976
|
-
}
|
|
1977
|
-
const headers = ["PROPERTY", "TYPE", "OPTIONS"];
|
|
1978
|
-
const rows = Object.values(schema.properties).map((prop) => [
|
|
1979
|
-
prop.name,
|
|
1980
|
-
prop.type,
|
|
1981
|
-
prop.options ? prop.options.map((o) => o.name).join(", ") : ""
|
|
1982
|
-
]);
|
|
1983
|
-
process.stdout.write(formatTable(rows, headers) + "\n");
|
|
1984
|
-
})
|
|
1985
|
-
);
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
// src/commands/db/query.ts
|
|
1989
|
-
import { Command as Command16 } from "commander";
|
|
1990
|
-
var SKIP_TYPES_IN_AUTO = /* @__PURE__ */ new Set(["relation", "rich_text", "people"]);
|
|
1991
|
-
function autoSelectColumns(schema, entries) {
|
|
1992
|
-
const termWidth = process.stdout.columns || 120;
|
|
1993
|
-
const COL_SEP = 2;
|
|
1994
|
-
const candidates = Object.values(schema.properties).filter((p) => !SKIP_TYPES_IN_AUTO.has(p.type)).map((p) => p.name);
|
|
1995
|
-
const widths = candidates.map((col) => {
|
|
1996
|
-
const header = col.toUpperCase().length;
|
|
1997
|
-
const maxData = entries.reduce((max, e) => Math.max(max, (e.properties[col] ?? "").length), 0);
|
|
1998
|
-
return Math.min(Math.max(header, maxData), 40);
|
|
1999
|
-
});
|
|
2000
|
-
const selected = [];
|
|
2001
|
-
let usedWidth = 0;
|
|
2002
|
-
for (let i = 0; i < candidates.length; i++) {
|
|
2003
|
-
const needed = (selected.length > 0 ? COL_SEP : 0) + widths[i];
|
|
2004
|
-
if (usedWidth + needed > termWidth) break;
|
|
2005
|
-
selected.push(candidates[i]);
|
|
2006
|
-
usedWidth += needed;
|
|
2007
|
-
}
|
|
2008
|
-
if (selected.length === 0 && candidates.length > 0) {
|
|
2009
|
-
selected.push(candidates[0]);
|
|
2317
|
+
// src/blocks/render.ts
|
|
2318
|
+
function buildPropertiesHeader(page) {
|
|
2319
|
+
const lines = ["---"];
|
|
2320
|
+
for (const [name, prop] of Object.entries(page.properties)) {
|
|
2321
|
+
const value = formatPropertyValue(name, prop);
|
|
2322
|
+
if (value) {
|
|
2323
|
+
lines.push(`${name}: ${value}`);
|
|
2324
|
+
}
|
|
2010
2325
|
}
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
function dbQueryCommand() {
|
|
2014
|
-
return new Command16("query").description("Query database entries with optional filtering and sorting").argument("<id>", "Notion database ID or URL").option("--filter <filter>", 'Filter entries (repeatable): --filter "Status=Done"', collect, []).option("--sort <sort>", 'Sort entries (repeatable): --sort "Name:asc"', collect, []).option("--columns <columns>", 'Comma-separated list of columns to display: --columns "Title,Status"').option("--json", "Output raw JSON").action(
|
|
2015
|
-
withErrorHandling(
|
|
2016
|
-
async (id, options) => {
|
|
2017
|
-
const { token } = await resolveToken();
|
|
2018
|
-
const client = createNotionClient(token);
|
|
2019
|
-
const dbId = parseNotionId(id);
|
|
2020
|
-
const schema = await fetchDatabaseSchema(client, dbId);
|
|
2021
|
-
const columns = options.columns ? options.columns.split(",").map((c2) => c2.trim()) : void 0;
|
|
2022
|
-
const filter = options.filter.length ? buildFilter(options.filter, schema) : void 0;
|
|
2023
|
-
const sorts = options.sort.length ? buildSorts(options.sort) : void 0;
|
|
2024
|
-
const entries = await queryDatabase(client, dbId, { filter, sorts, columns });
|
|
2025
|
-
if (options.json) {
|
|
2026
|
-
process.stdout.write(formatJSON(entries.map((e) => e.raw)) + "\n");
|
|
2027
|
-
return;
|
|
2028
|
-
}
|
|
2029
|
-
if (entries.length === 0) {
|
|
2030
|
-
process.stdout.write("No entries found.\n");
|
|
2031
|
-
return;
|
|
2032
|
-
}
|
|
2033
|
-
const displayColumns = columns ?? autoSelectColumns(schema, entries);
|
|
2034
|
-
const headers = displayColumns.map((c2) => c2.toUpperCase());
|
|
2035
|
-
const rows = entries.map(
|
|
2036
|
-
(entry) => displayColumns.map((col) => entry.properties[col] ?? "")
|
|
2037
|
-
);
|
|
2038
|
-
process.stdout.write(formatTable(rows, headers) + "\n");
|
|
2039
|
-
process.stderr.write(`${entries.length} entries
|
|
2040
|
-
`);
|
|
2041
|
-
}
|
|
2042
|
-
)
|
|
2043
|
-
);
|
|
2044
|
-
}
|
|
2045
|
-
function collect(value, previous) {
|
|
2046
|
-
return previous.concat([value]);
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
|
-
// src/commands/comment-add.ts
|
|
2050
|
-
import { Command as Command17 } from "commander";
|
|
2051
|
-
|
|
2052
|
-
// src/services/write.service.ts
|
|
2053
|
-
async function addComment(client, pageId, text, options = {}) {
|
|
2054
|
-
await client.comments.create({
|
|
2055
|
-
parent: { page_id: pageId },
|
|
2056
|
-
rich_text: [
|
|
2057
|
-
{
|
|
2058
|
-
type: "text",
|
|
2059
|
-
text: { content: text, link: null },
|
|
2060
|
-
annotations: {
|
|
2061
|
-
bold: false,
|
|
2062
|
-
italic: false,
|
|
2063
|
-
strikethrough: false,
|
|
2064
|
-
underline: false,
|
|
2065
|
-
code: false,
|
|
2066
|
-
color: "default"
|
|
2067
|
-
}
|
|
2068
|
-
}
|
|
2069
|
-
],
|
|
2070
|
-
...options.asUser && { display_name: { type: "user" } }
|
|
2071
|
-
});
|
|
2072
|
-
}
|
|
2073
|
-
async function appendBlocks(client, blockId, blocks) {
|
|
2074
|
-
await client.blocks.children.append({
|
|
2075
|
-
block_id: blockId,
|
|
2076
|
-
children: blocks
|
|
2077
|
-
});
|
|
2078
|
-
}
|
|
2079
|
-
async function createPage(client, parentId, title, blocks) {
|
|
2080
|
-
const response = await client.pages.create({
|
|
2081
|
-
parent: { type: "page_id", page_id: parentId },
|
|
2082
|
-
properties: {
|
|
2083
|
-
title: {
|
|
2084
|
-
title: [{ type: "text", text: { content: title, link: null } }]
|
|
2085
|
-
}
|
|
2086
|
-
},
|
|
2087
|
-
children: blocks
|
|
2088
|
-
});
|
|
2089
|
-
return response.url;
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
// src/commands/comment-add.ts
|
|
2093
|
-
function commentAddCommand() {
|
|
2094
|
-
const cmd = new Command17("comment");
|
|
2095
|
-
cmd.description("add a comment to a Notion page").argument("<id/url>", "Notion page ID or URL").requiredOption("-m, --message <text>", "comment text to post").action(withErrorHandling(async (idOrUrl, opts) => {
|
|
2096
|
-
const { token, source } = await resolveToken();
|
|
2097
|
-
reportTokenSource(source);
|
|
2098
|
-
const client = createNotionClient(token);
|
|
2099
|
-
const id = parseNotionId(idOrUrl);
|
|
2100
|
-
const uuid = toUuid(id);
|
|
2101
|
-
await addComment(client, uuid, opts.message, { asUser: source === "oauth" });
|
|
2102
|
-
process.stdout.write("Comment added.\n");
|
|
2103
|
-
}));
|
|
2104
|
-
return cmd;
|
|
2326
|
+
lines.push("---", "");
|
|
2327
|
+
return lines.join("\n");
|
|
2105
2328
|
}
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
let match;
|
|
2115
|
-
INLINE_RE.lastIndex = 0;
|
|
2116
|
-
while ((match = INLINE_RE.exec(text)) !== null) {
|
|
2117
|
-
const segment = match[0];
|
|
2118
|
-
if (segment.startsWith("**") && segment.endsWith("**")) {
|
|
2119
|
-
const content = segment.slice(2, -2);
|
|
2120
|
-
result.push({
|
|
2121
|
-
type: "text",
|
|
2122
|
-
text: { content, link: null },
|
|
2123
|
-
annotations: { bold: true, italic: false, strikethrough: false, underline: false, code: false, color: "default" }
|
|
2124
|
-
});
|
|
2125
|
-
continue;
|
|
2126
|
-
}
|
|
2127
|
-
if (segment.startsWith("_") && segment.endsWith("_") || segment.startsWith("*") && segment.endsWith("*")) {
|
|
2128
|
-
const content = segment.slice(1, -1);
|
|
2129
|
-
result.push({
|
|
2130
|
-
type: "text",
|
|
2131
|
-
text: { content, link: null },
|
|
2132
|
-
annotations: { bold: false, italic: true, strikethrough: false, underline: false, code: false, color: "default" }
|
|
2133
|
-
});
|
|
2134
|
-
continue;
|
|
2135
|
-
}
|
|
2136
|
-
if (segment.startsWith("`") && segment.endsWith("`")) {
|
|
2137
|
-
const content = segment.slice(1, -1);
|
|
2138
|
-
result.push({
|
|
2139
|
-
type: "text",
|
|
2140
|
-
text: { content, link: null },
|
|
2141
|
-
annotations: { bold: false, italic: false, strikethrough: false, underline: false, code: true, color: "default" }
|
|
2142
|
-
});
|
|
2143
|
-
continue;
|
|
2329
|
+
function renderBlockTree(blocks) {
|
|
2330
|
+
const parts = [];
|
|
2331
|
+
let listCounter = 0;
|
|
2332
|
+
for (const node of blocks) {
|
|
2333
|
+
if (node.block.type === "numbered_list_item") {
|
|
2334
|
+
listCounter++;
|
|
2335
|
+
} else {
|
|
2336
|
+
listCounter = 0;
|
|
2144
2337
|
}
|
|
2145
|
-
const
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2338
|
+
const childrenMd = node.children.length > 0 ? renderBlockTree(node.children) : "";
|
|
2339
|
+
const md = blockToMd(node.block, {
|
|
2340
|
+
listNumber: node.block.type === "numbered_list_item" ? listCounter : void 0,
|
|
2341
|
+
childrenMd: childrenMd || void 0
|
|
2342
|
+
});
|
|
2343
|
+
parts.push(md);
|
|
2344
|
+
}
|
|
2345
|
+
return parts.join("");
|
|
2346
|
+
}
|
|
2347
|
+
function renderPageMarkdown({ page, blocks }) {
|
|
2348
|
+
const header = buildPropertiesHeader(page);
|
|
2349
|
+
const content = renderBlockTree(blocks);
|
|
2350
|
+
return header + content;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
// src/output/markdown.ts
|
|
2354
|
+
import { Chalk as Chalk2 } from "chalk";
|
|
2355
|
+
var c = new Chalk2({ level: 3 });
|
|
2356
|
+
function handleFenceLine(line, state, out) {
|
|
2357
|
+
const fenceMatch = line.match(/^```(\w*)$/);
|
|
2358
|
+
if (fenceMatch && !state.inFence) {
|
|
2359
|
+
state.inFence = true;
|
|
2360
|
+
state.fenceLang = fenceMatch[1] ?? "";
|
|
2361
|
+
state.fenceLines = [];
|
|
2362
|
+
return true;
|
|
2363
|
+
}
|
|
2364
|
+
if (line === "```" && state.inFence) {
|
|
2365
|
+
state.inFence = false;
|
|
2366
|
+
const header = state.fenceLang ? c.dim(`[${state.fenceLang}]`) : "";
|
|
2367
|
+
if (header) out.push(header);
|
|
2368
|
+
for (const fl of state.fenceLines) {
|
|
2369
|
+
out.push(c.green(` ${fl}`));
|
|
2154
2370
|
}
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
text: { content: segment, link: null },
|
|
2158
|
-
annotations: { bold: false, italic: false, strikethrough: false, underline: false, code: false, color: "default" }
|
|
2159
|
-
});
|
|
2371
|
+
out.push("");
|
|
2372
|
+
return true;
|
|
2160
2373
|
}
|
|
2161
|
-
return
|
|
2374
|
+
return false;
|
|
2162
2375
|
}
|
|
2163
|
-
function
|
|
2164
|
-
|
|
2376
|
+
function handleHeading(line, out) {
|
|
2377
|
+
const h1 = line.match(/^# (.+)/);
|
|
2378
|
+
if (h1) {
|
|
2379
|
+
out.push(`
|
|
2380
|
+
${c.bold.cyan(h1[1])}`);
|
|
2381
|
+
return true;
|
|
2382
|
+
}
|
|
2383
|
+
const h2 = line.match(/^## (.+)/);
|
|
2384
|
+
if (h2) {
|
|
2385
|
+
out.push(`
|
|
2386
|
+
${c.bold.blue(h2[1])}`);
|
|
2387
|
+
return true;
|
|
2388
|
+
}
|
|
2389
|
+
const h3 = line.match(/^### (.+)/);
|
|
2390
|
+
if (h3) {
|
|
2391
|
+
out.push(`
|
|
2392
|
+
${c.bold(h3[1])}`);
|
|
2393
|
+
return true;
|
|
2394
|
+
}
|
|
2395
|
+
const h4 = line.match(/^#### (.+)/);
|
|
2396
|
+
if (h4) {
|
|
2397
|
+
out.push(c.bold.underline(h4[1]));
|
|
2398
|
+
return true;
|
|
2399
|
+
}
|
|
2400
|
+
return false;
|
|
2401
|
+
}
|
|
2402
|
+
function handleListLine(line, out) {
|
|
2403
|
+
const bulletMatch = line.match(/^(\s*)- (\[[ x]\] )?(.+)/);
|
|
2404
|
+
if (bulletMatch) {
|
|
2405
|
+
const indent = bulletMatch[1] ?? "";
|
|
2406
|
+
const checkbox = bulletMatch[2];
|
|
2407
|
+
const text = bulletMatch[3] ?? "";
|
|
2408
|
+
if (checkbox) {
|
|
2409
|
+
const checked = checkbox.trim() === "[x]";
|
|
2410
|
+
const box = checked ? c.green("\u2611") : c.dim("\u2610");
|
|
2411
|
+
out.push(`${indent + box} ${renderInline(text)}`);
|
|
2412
|
+
} else {
|
|
2413
|
+
out.push(`${indent + c.cyan("\u2022")} ${renderInline(text)}`);
|
|
2414
|
+
}
|
|
2415
|
+
return true;
|
|
2416
|
+
}
|
|
2417
|
+
const numMatch = line.match(/^(\s*)(\d+)\. (.+)/);
|
|
2418
|
+
if (numMatch) {
|
|
2419
|
+
const indent = numMatch[1] ?? "";
|
|
2420
|
+
const num = numMatch[2] ?? "";
|
|
2421
|
+
const text = numMatch[3] ?? "";
|
|
2422
|
+
out.push(`${indent + c.cyan(`${num}.`)} ${renderInline(text)}`);
|
|
2423
|
+
return true;
|
|
2424
|
+
}
|
|
2425
|
+
return false;
|
|
2165
2426
|
}
|
|
2166
|
-
function
|
|
2167
|
-
if (!
|
|
2427
|
+
function renderMarkdown(md) {
|
|
2428
|
+
if (!isatty()) return md;
|
|
2168
2429
|
const lines = md.split("\n");
|
|
2169
|
-
const
|
|
2170
|
-
|
|
2171
|
-
let fenceLang = "";
|
|
2172
|
-
const fenceLines = [];
|
|
2430
|
+
const out = [];
|
|
2431
|
+
const fence = { inFence: false, fenceLang: "", fenceLines: [] };
|
|
2173
2432
|
for (const line of lines) {
|
|
2174
|
-
if (
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
fenceLines.length = 0;
|
|
2178
|
-
continue;
|
|
2179
|
-
}
|
|
2180
|
-
if (inFence) {
|
|
2181
|
-
if (line.startsWith("```")) {
|
|
2182
|
-
const content = fenceLines.join("\n");
|
|
2183
|
-
blocks.push({
|
|
2184
|
-
type: "code",
|
|
2185
|
-
code: {
|
|
2186
|
-
rich_text: [
|
|
2187
|
-
{
|
|
2188
|
-
type: "text",
|
|
2189
|
-
text: { content, link: null },
|
|
2190
|
-
annotations: { bold: false, italic: false, strikethrough: false, underline: false, code: false, color: "default" }
|
|
2191
|
-
}
|
|
2192
|
-
],
|
|
2193
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2194
|
-
language: fenceLang
|
|
2195
|
-
}
|
|
2196
|
-
});
|
|
2197
|
-
inFence = false;
|
|
2198
|
-
fenceLang = "";
|
|
2199
|
-
fenceLines.length = 0;
|
|
2200
|
-
} else {
|
|
2201
|
-
fenceLines.push(line);
|
|
2202
|
-
}
|
|
2203
|
-
continue;
|
|
2204
|
-
}
|
|
2205
|
-
if (line.trim() === "") continue;
|
|
2206
|
-
const h1 = line.match(/^# (.+)$/);
|
|
2207
|
-
if (h1) {
|
|
2208
|
-
blocks.push({
|
|
2209
|
-
type: "heading_1",
|
|
2210
|
-
heading_1: { rich_text: makeRichText(h1[1]) }
|
|
2211
|
-
});
|
|
2212
|
-
continue;
|
|
2213
|
-
}
|
|
2214
|
-
const h2 = line.match(/^## (.+)$/);
|
|
2215
|
-
if (h2) {
|
|
2216
|
-
blocks.push({
|
|
2217
|
-
type: "heading_2",
|
|
2218
|
-
heading_2: { rich_text: makeRichText(h2[1]) }
|
|
2219
|
-
});
|
|
2220
|
-
continue;
|
|
2221
|
-
}
|
|
2222
|
-
const h3 = line.match(/^### (.+)$/);
|
|
2223
|
-
if (h3) {
|
|
2224
|
-
blocks.push({
|
|
2225
|
-
type: "heading_3",
|
|
2226
|
-
heading_3: { rich_text: makeRichText(h3[1]) }
|
|
2227
|
-
});
|
|
2433
|
+
if (handleFenceLine(line, fence, out)) continue;
|
|
2434
|
+
if (fence.inFence) {
|
|
2435
|
+
fence.fenceLines.push(line);
|
|
2228
2436
|
continue;
|
|
2229
2437
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
blocks.push({
|
|
2233
|
-
type: "bulleted_list_item",
|
|
2234
|
-
bulleted_list_item: { rich_text: makeRichText(bullet[1]) }
|
|
2235
|
-
});
|
|
2438
|
+
if (line === "---") {
|
|
2439
|
+
out.push(c.dim("\u2500".repeat(40)));
|
|
2236
2440
|
continue;
|
|
2237
2441
|
}
|
|
2238
|
-
|
|
2239
|
-
if (
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
numbered_list_item: { rich_text: makeRichText(numbered[1]) }
|
|
2243
|
-
});
|
|
2442
|
+
if (/^<!--.*-->$/.test(line.trim())) continue;
|
|
2443
|
+
if (handleHeading(line, out)) continue;
|
|
2444
|
+
if (line.startsWith("> ")) {
|
|
2445
|
+
out.push(c.yellow("\u258E ") + renderInline(line.slice(2)));
|
|
2244
2446
|
continue;
|
|
2245
2447
|
}
|
|
2246
|
-
const
|
|
2247
|
-
if (
|
|
2248
|
-
|
|
2249
|
-
type: "quote",
|
|
2250
|
-
quote: { rich_text: makeRichText(quote[1]) }
|
|
2251
|
-
});
|
|
2448
|
+
const propMatch = line.match(/^([A-Za-z_][A-Za-z0-9_ ]*): (.+)$/);
|
|
2449
|
+
if (propMatch) {
|
|
2450
|
+
out.push(c.dim(`${propMatch[1]}: `) + c.white(propMatch[2]));
|
|
2252
2451
|
continue;
|
|
2253
2452
|
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
paragraph: { rich_text: makeRichText(line) }
|
|
2257
|
-
});
|
|
2453
|
+
if (handleListLine(line, out)) continue;
|
|
2454
|
+
out.push(renderInline(line));
|
|
2258
2455
|
}
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2456
|
+
return `${out.join("\n")}
|
|
2457
|
+
`;
|
|
2458
|
+
}
|
|
2459
|
+
function renderInline(text) {
|
|
2460
|
+
const codeSpans = [];
|
|
2461
|
+
let result = text.replace(/`([^`]+)`/g, (_, code) => {
|
|
2462
|
+
codeSpans.push(c.green(code));
|
|
2463
|
+
return `\0CODE${codeSpans.length - 1}\0`;
|
|
2464
|
+
});
|
|
2465
|
+
result = result.replace(
|
|
2466
|
+
/!\[([^\]]*)\]\([^)]+\)/g,
|
|
2467
|
+
(_, alt) => alt ? c.dim(`[image: ${alt}]`) : c.dim("[image]")
|
|
2468
|
+
).replace(
|
|
2469
|
+
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
2470
|
+
(_, t, url) => c.cyan.underline(t) + c.dim(` (${url})`)
|
|
2471
|
+
).replace(/\*\*\*(.+?)\*\*\*/g, (_, t) => c.bold.italic(t)).replace(/\*\*(.+?)\*\*/g, (_, t) => c.bold(t)).replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, (_, t) => c.italic(t)).replace(/(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/g, (_, t) => c.italic(t)).replace(/~~(.+?)~~/g, (_, t) => c.strikethrough(t));
|
|
2472
|
+
result = result.replace(
|
|
2473
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: sentinel chars are intentional
|
|
2474
|
+
/\x00CODE(\d+)\x00/g,
|
|
2475
|
+
(_, i) => codeSpans[Number(i)] ?? ""
|
|
2476
|
+
);
|
|
2477
|
+
return result;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
// src/services/page.service.ts
|
|
2481
|
+
import {
|
|
2482
|
+
collectPaginatedAPI,
|
|
2483
|
+
isFullBlock
|
|
2484
|
+
} from "@notionhq/client";
|
|
2485
|
+
var MAX_CONCURRENT_REQUESTS = 3;
|
|
2486
|
+
async function fetchBlockTree(client, blockId, depth, maxDepth) {
|
|
2487
|
+
if (depth >= maxDepth) return [];
|
|
2488
|
+
const rawBlocks = await collectPaginatedAPI(client.blocks.children.list, {
|
|
2489
|
+
block_id: blockId
|
|
2490
|
+
});
|
|
2491
|
+
const blocks = rawBlocks.filter(isFullBlock);
|
|
2492
|
+
const SKIP_RECURSE = /* @__PURE__ */ new Set(["child_page", "child_database"]);
|
|
2493
|
+
const nodes = [];
|
|
2494
|
+
for (let i = 0; i < blocks.length; i += MAX_CONCURRENT_REQUESTS) {
|
|
2495
|
+
const batch = blocks.slice(i, i + MAX_CONCURRENT_REQUESTS);
|
|
2496
|
+
const batchNodes = await Promise.all(
|
|
2497
|
+
batch.map(async (block) => {
|
|
2498
|
+
const children = block.has_children && !SKIP_RECURSE.has(block.type) ? await fetchBlockTree(client, block.id, depth + 1, maxDepth) : [];
|
|
2499
|
+
return { block, children };
|
|
2500
|
+
})
|
|
2501
|
+
);
|
|
2502
|
+
nodes.push(...batchNodes);
|
|
2503
|
+
}
|
|
2504
|
+
return nodes;
|
|
2505
|
+
}
|
|
2506
|
+
async function fetchPageWithBlocks(client, pageId) {
|
|
2507
|
+
const page = await client.pages.retrieve({
|
|
2508
|
+
page_id: pageId
|
|
2509
|
+
});
|
|
2510
|
+
const blocks = await fetchBlockTree(client, pageId, 0, 10);
|
|
2511
|
+
return { page, blocks };
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
// src/commands/read.ts
|
|
2515
|
+
function readCommand() {
|
|
2516
|
+
return new Command16("read").description("Read a Notion page as markdown").argument("<id>", "Notion page ID or URL").option("--json", "Output raw JSON instead of markdown").option("--md", "Output raw markdown (no terminal styling)").action(
|
|
2517
|
+
withErrorHandling(
|
|
2518
|
+
async (id, options) => {
|
|
2519
|
+
const { token } = await resolveToken();
|
|
2520
|
+
const client = createNotionClient(token);
|
|
2521
|
+
const pageId = parseNotionId(id);
|
|
2522
|
+
const pageWithBlocks = await fetchPageWithBlocks(client, pageId);
|
|
2523
|
+
if (options.json) {
|
|
2524
|
+
process.stdout.write(
|
|
2525
|
+
`${JSON.stringify(pageWithBlocks, null, 2)}
|
|
2526
|
+
`
|
|
2527
|
+
);
|
|
2528
|
+
} else {
|
|
2529
|
+
const markdown = renderPageMarkdown(pageWithBlocks);
|
|
2530
|
+
if (options.md || !isatty()) {
|
|
2531
|
+
process.stdout.write(markdown);
|
|
2532
|
+
} else {
|
|
2533
|
+
process.stdout.write(renderMarkdown(markdown));
|
|
2269
2534
|
}
|
|
2270
|
-
|
|
2271
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2272
|
-
language: fenceLang
|
|
2535
|
+
}
|
|
2273
2536
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
return blocks;
|
|
2537
|
+
)
|
|
2538
|
+
);
|
|
2277
2539
|
}
|
|
2278
2540
|
|
|
2279
|
-
// src/commands/
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2541
|
+
// src/commands/search.ts
|
|
2542
|
+
import { isFullPageOrDataSource as isFullPageOrDataSource2 } from "@notionhq/client";
|
|
2543
|
+
import { Command as Command17 } from "commander";
|
|
2544
|
+
function getTitle2(item) {
|
|
2545
|
+
if (item.object === "data_source") {
|
|
2546
|
+
return item.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
2547
|
+
}
|
|
2548
|
+
const titleProp = Object.values(item.properties).find(
|
|
2549
|
+
(p) => p.type === "title"
|
|
2550
|
+
);
|
|
2551
|
+
if (titleProp?.type === "title") {
|
|
2552
|
+
return titleProp.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
2553
|
+
}
|
|
2554
|
+
return "(untitled)";
|
|
2555
|
+
}
|
|
2556
|
+
function toSdkFilterValue(type) {
|
|
2557
|
+
return type === "database" ? "data_source" : "page";
|
|
2558
|
+
}
|
|
2559
|
+
function displayType2(item) {
|
|
2560
|
+
return item.object === "data_source" ? "database" : item.object;
|
|
2561
|
+
}
|
|
2562
|
+
function searchCommand() {
|
|
2563
|
+
const cmd = new Command17("search");
|
|
2564
|
+
cmd.description("search Notion workspace by keyword").argument("<query>", "search keyword").option(
|
|
2565
|
+
"--type <type>",
|
|
2566
|
+
"filter by object type (page or database)",
|
|
2567
|
+
(val) => {
|
|
2568
|
+
if (val !== "page" && val !== "database") {
|
|
2569
|
+
throw new Error('--type must be "page" or "database"');
|
|
2570
|
+
}
|
|
2571
|
+
return val;
|
|
2292
2572
|
}
|
|
2293
|
-
|
|
2294
|
-
|
|
2573
|
+
).option(
|
|
2574
|
+
"--cursor <cursor>",
|
|
2575
|
+
"start from this pagination cursor (from a previous --next hint)"
|
|
2576
|
+
).option("--json", "force JSON output").action(
|
|
2577
|
+
withErrorHandling(
|
|
2578
|
+
async (query, opts) => {
|
|
2579
|
+
if (opts.json) {
|
|
2580
|
+
setOutputMode("json");
|
|
2581
|
+
}
|
|
2582
|
+
const { token, source } = await resolveToken();
|
|
2583
|
+
reportTokenSource(source);
|
|
2584
|
+
const notion = createNotionClient(token);
|
|
2585
|
+
const response = await notion.search({
|
|
2586
|
+
query,
|
|
2587
|
+
filter: opts.type ? { property: "object", value: toSdkFilterValue(opts.type) } : void 0,
|
|
2588
|
+
start_cursor: opts.cursor,
|
|
2589
|
+
page_size: 20
|
|
2590
|
+
});
|
|
2591
|
+
const fullResults = response.results.filter(
|
|
2592
|
+
(r) => isFullPageOrDataSource2(r)
|
|
2593
|
+
);
|
|
2594
|
+
if (fullResults.length === 0) {
|
|
2595
|
+
process.stdout.write(`No results found for "${query}"
|
|
2295
2596
|
`);
|
|
2296
|
-
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
const headers = ["TYPE", "TITLE", "ID", "MODIFIED"];
|
|
2600
|
+
const rows = fullResults.map((item) => [
|
|
2601
|
+
displayType2(item),
|
|
2602
|
+
getTitle2(item),
|
|
2603
|
+
item.id,
|
|
2604
|
+
item.last_edited_time.split("T")[0]
|
|
2605
|
+
]);
|
|
2606
|
+
printOutput(fullResults, headers, rows);
|
|
2607
|
+
if (response.has_more && response.next_cursor) {
|
|
2608
|
+
process.stderr.write(
|
|
2609
|
+
`
|
|
2610
|
+
--next: notion search "${query}" --cursor ${response.next_cursor}
|
|
2611
|
+
`
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
)
|
|
2616
|
+
);
|
|
2297
2617
|
return cmd;
|
|
2298
2618
|
}
|
|
2299
2619
|
|
|
2300
|
-
// src/commands/
|
|
2301
|
-
import { Command as
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2620
|
+
// src/commands/users.ts
|
|
2621
|
+
import { Command as Command18 } from "commander";
|
|
2622
|
+
function getEmailOrWorkspace(user) {
|
|
2623
|
+
if (user.type === "person") {
|
|
2624
|
+
return user.person.email ?? "\u2014";
|
|
2306
2625
|
}
|
|
2307
|
-
|
|
2626
|
+
if (user.type === "bot") {
|
|
2627
|
+
const bot = user.bot;
|
|
2628
|
+
return "workspace_name" in bot && bot.workspace_name ? bot.workspace_name : "\u2014";
|
|
2629
|
+
}
|
|
2630
|
+
return "\u2014";
|
|
2308
2631
|
}
|
|
2309
|
-
function
|
|
2310
|
-
const cmd = new
|
|
2311
|
-
cmd.description("
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2632
|
+
function usersCommand() {
|
|
2633
|
+
const cmd = new Command18("users");
|
|
2634
|
+
cmd.description("list all users in the workspace").option("--json", "output as JSON").action(
|
|
2635
|
+
withErrorHandling(async (opts) => {
|
|
2636
|
+
if (opts.json) setOutputMode("json");
|
|
2637
|
+
const { token, source } = await resolveToken();
|
|
2638
|
+
reportTokenSource(source);
|
|
2639
|
+
const notion = createNotionClient(token);
|
|
2640
|
+
const allUsers = await paginateResults(
|
|
2641
|
+
(cursor) => notion.users.list({ start_cursor: cursor })
|
|
2642
|
+
);
|
|
2643
|
+
const users = allUsers.filter(
|
|
2644
|
+
(u) => u.name !== void 0
|
|
2645
|
+
);
|
|
2646
|
+
const rows = users.map((user) => [
|
|
2647
|
+
user.type,
|
|
2648
|
+
user.name ?? "(unnamed)",
|
|
2649
|
+
getEmailOrWorkspace(user),
|
|
2650
|
+
user.id
|
|
2651
|
+
]);
|
|
2652
|
+
printOutput(users, ["TYPE", "NAME", "EMAIL / WORKSPACE", "ID"], rows);
|
|
2653
|
+
})
|
|
2654
|
+
);
|
|
2326
2655
|
return cmd;
|
|
2327
2656
|
}
|
|
2328
2657
|
|
|
2329
2658
|
// src/cli.ts
|
|
2330
2659
|
var __filename = fileURLToPath(import.meta.url);
|
|
2331
2660
|
var __dirname = dirname(__filename);
|
|
2332
|
-
var pkg = JSON.parse(
|
|
2333
|
-
|
|
2661
|
+
var pkg = JSON.parse(
|
|
2662
|
+
readFileSync(join3(__dirname, "../package.json"), "utf-8")
|
|
2663
|
+
);
|
|
2664
|
+
var program = new Command19();
|
|
2334
2665
|
program.name("notion").description("Notion CLI \u2014 read Notion pages and databases from the terminal").version(pkg.version);
|
|
2335
2666
|
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");
|
|
2336
2667
|
program.configureOutput({
|
|
@@ -2351,17 +2682,21 @@ program.hook("preAction", (thisCommand) => {
|
|
|
2351
2682
|
setOutputMode("md");
|
|
2352
2683
|
}
|
|
2353
2684
|
});
|
|
2354
|
-
|
|
2355
|
-
|
|
2685
|
+
var authCmd = new Command19("auth").description("manage Notion authentication");
|
|
2686
|
+
authCmd.action(authDefaultAction(authCmd));
|
|
2356
2687
|
authCmd.addCommand(loginCommand());
|
|
2357
2688
|
authCmd.addCommand(logoutCommand());
|
|
2358
2689
|
authCmd.addCommand(statusCommand());
|
|
2690
|
+
authCmd.addCommand(profileListCommand());
|
|
2691
|
+
authCmd.addCommand(profileUseCommand());
|
|
2359
2692
|
program.addCommand(authCmd);
|
|
2360
|
-
|
|
2693
|
+
program.addCommand(initCommand(), { hidden: true });
|
|
2694
|
+
var profileCmd = new Command19("profile").description(
|
|
2695
|
+
"manage authentication profiles"
|
|
2696
|
+
);
|
|
2361
2697
|
profileCmd.addCommand(profileListCommand());
|
|
2362
2698
|
profileCmd.addCommand(profileUseCommand());
|
|
2363
|
-
|
|
2364
|
-
program.addCommand(profileCmd);
|
|
2699
|
+
program.addCommand(profileCmd, { hidden: true });
|
|
2365
2700
|
program.addCommand(searchCommand());
|
|
2366
2701
|
program.addCommand(lsCommand());
|
|
2367
2702
|
program.addCommand(openCommand());
|
|
@@ -2371,7 +2706,7 @@ program.addCommand(readCommand());
|
|
|
2371
2706
|
program.addCommand(commentAddCommand());
|
|
2372
2707
|
program.addCommand(appendCommand());
|
|
2373
2708
|
program.addCommand(createPageCommand());
|
|
2374
|
-
var dbCmd = new
|
|
2709
|
+
var dbCmd = new Command19("db").description("Database operations");
|
|
2375
2710
|
dbCmd.addCommand(dbSchemaCommand());
|
|
2376
2711
|
dbCmd.addCommand(dbQueryCommand());
|
|
2377
2712
|
program.addCommand(dbCmd);
|