@andrzejchm/notion-cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -23
- package/dist/cli.js +2269 -1946
- package/dist/cli.js.map +1 -1
- package/docs/README.agents.md +10 -6
- 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,942 @@ 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 handleCallbackRequest(req, res, ctx) {
|
|
928
|
+
if (ctx.settled) {
|
|
929
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
930
|
+
res.end(
|
|
931
|
+
"<html><body><h1>Already handled. You can close this tab.</h1></body></html>"
|
|
1042
932
|
);
|
|
933
|
+
return;
|
|
1043
934
|
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
935
|
+
try {
|
|
936
|
+
const reqUrl = new URL(req.url ?? "/", "http://localhost:54321");
|
|
937
|
+
const code = reqUrl.searchParams.get("code");
|
|
938
|
+
const returnedState = reqUrl.searchParams.get("state");
|
|
939
|
+
const errorParam = reqUrl.searchParams.get("error");
|
|
940
|
+
if (errorParam === "access_denied") {
|
|
941
|
+
ctx.settled = true;
|
|
942
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
943
|
+
res.end(
|
|
944
|
+
"<html><body><h1>Access Denied</h1><p>You cancelled the Notion OAuth request. You can close this tab.</p></body></html>"
|
|
945
|
+
);
|
|
946
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
947
|
+
ctx.server.close(() => {
|
|
948
|
+
ctx.reject(
|
|
949
|
+
new CliError(
|
|
950
|
+
ErrorCodes.AUTH_INVALID,
|
|
951
|
+
"Notion OAuth access was denied.",
|
|
952
|
+
'Run "notion auth login" to try again'
|
|
953
|
+
)
|
|
954
|
+
);
|
|
955
|
+
});
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
if (!code || !returnedState) {
|
|
959
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
960
|
+
res.end("<html><body><p>Waiting for OAuth callback...</p></body></html>");
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
if (returnedState !== ctx.expectedState) {
|
|
964
|
+
ctx.settled = true;
|
|
965
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
966
|
+
res.end(
|
|
967
|
+
"<html><body><h1>Security Error</h1><p>State mismatch \u2014 possible CSRF attempt. You can close this tab.</p></body></html>"
|
|
968
|
+
);
|
|
969
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
970
|
+
ctx.server.close(() => {
|
|
971
|
+
ctx.reject(
|
|
972
|
+
new CliError(
|
|
973
|
+
ErrorCodes.AUTH_INVALID,
|
|
974
|
+
"OAuth state mismatch \u2014 possible CSRF attempt. Aborting.",
|
|
975
|
+
'Run "notion auth login" to start a fresh OAuth flow'
|
|
976
|
+
)
|
|
977
|
+
);
|
|
978
|
+
});
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
ctx.settled = true;
|
|
982
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
983
|
+
res.end(
|
|
984
|
+
"<html><body><h1>Authenticated!</h1><p>You can close this tab and return to the terminal.</p></body></html>"
|
|
1049
985
|
);
|
|
986
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
987
|
+
ctx.server.close(() => {
|
|
988
|
+
ctx.resolve({ code, state: returnedState });
|
|
989
|
+
});
|
|
990
|
+
} catch {
|
|
991
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
992
|
+
res.end("<html><body><h1>Error processing callback</h1></body></html>");
|
|
1050
993
|
}
|
|
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
994
|
}
|
|
1059
|
-
async function
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
995
|
+
async function runOAuthFlow(options) {
|
|
996
|
+
const state = randomBytes(16).toString("hex");
|
|
997
|
+
const authUrl = buildAuthUrl(state);
|
|
998
|
+
if (options?.manual) {
|
|
999
|
+
return manualFlow(authUrl);
|
|
1063
1000
|
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1001
|
+
return new Promise((resolve, reject) => {
|
|
1002
|
+
const ctx = {
|
|
1003
|
+
expectedState: state,
|
|
1004
|
+
settled: false,
|
|
1005
|
+
timeoutHandle: null,
|
|
1006
|
+
resolve,
|
|
1007
|
+
reject,
|
|
1008
|
+
// assigned below after server is created
|
|
1009
|
+
server: null
|
|
1010
|
+
};
|
|
1011
|
+
const server = createServer(
|
|
1012
|
+
(req, res) => handleCallbackRequest(req, res, ctx)
|
|
1070
1013
|
);
|
|
1014
|
+
ctx.server = server;
|
|
1015
|
+
server.on("error", (err) => {
|
|
1016
|
+
if (ctx.settled) return;
|
|
1017
|
+
ctx.settled = true;
|
|
1018
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
1019
|
+
reject(
|
|
1020
|
+
new CliError(
|
|
1021
|
+
ErrorCodes.AUTH_INVALID,
|
|
1022
|
+
`Failed to start OAuth callback server: ${err.message}`,
|
|
1023
|
+
"Make sure port 54321 is not in use, or use --manual flag"
|
|
1024
|
+
)
|
|
1025
|
+
);
|
|
1026
|
+
});
|
|
1027
|
+
server.listen(54321, "127.0.0.1", () => {
|
|
1028
|
+
const browserOpened = openBrowser(authUrl);
|
|
1029
|
+
if (!browserOpened) {
|
|
1030
|
+
server.close();
|
|
1031
|
+
ctx.settled = true;
|
|
1032
|
+
if (ctx.timeoutHandle) clearTimeout(ctx.timeoutHandle);
|
|
1033
|
+
manualFlow(authUrl).then(resolve, reject);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
process.stderr.write(
|
|
1037
|
+
`
|
|
1038
|
+
Opening browser for Notion OAuth...
|
|
1039
|
+
If your browser didn't open, visit:
|
|
1040
|
+
${authUrl}
|
|
1041
|
+
|
|
1042
|
+
Waiting for callback (up to 120 seconds)...
|
|
1043
|
+
`
|
|
1044
|
+
);
|
|
1045
|
+
ctx.timeoutHandle = setTimeout(() => {
|
|
1046
|
+
if (ctx.settled) return;
|
|
1047
|
+
ctx.settled = true;
|
|
1048
|
+
server.close(() => {
|
|
1049
|
+
reject(
|
|
1050
|
+
new CliError(
|
|
1051
|
+
ErrorCodes.AUTH_INVALID,
|
|
1052
|
+
"OAuth login timed out after 120 seconds.",
|
|
1053
|
+
'Run "notion auth login" to try again, or use --manual flag'
|
|
1054
|
+
)
|
|
1055
|
+
);
|
|
1056
|
+
});
|
|
1057
|
+
}, 12e4);
|
|
1058
|
+
});
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/commands/init.ts
|
|
1063
|
+
import { confirm, input, password } from "@inquirer/prompts";
|
|
1064
|
+
import { Command as Command2 } from "commander";
|
|
1065
|
+
async function runInitFlow() {
|
|
1066
|
+
const profileName = await input({
|
|
1067
|
+
message: "Profile name:",
|
|
1068
|
+
default: "default"
|
|
1069
|
+
});
|
|
1070
|
+
const token = await password({
|
|
1071
|
+
message: "Integration token (from notion.so/profile/integrations/internal):",
|
|
1072
|
+
mask: "*"
|
|
1073
|
+
});
|
|
1074
|
+
stderrWrite("Validating token...");
|
|
1075
|
+
const { workspaceName, workspaceId } = await validateToken(token);
|
|
1076
|
+
stderrWrite(success(`\u2713 Connected to workspace: ${bold(workspaceName)}`));
|
|
1077
|
+
const config = await readGlobalConfig();
|
|
1078
|
+
if (config.profiles?.[profileName]) {
|
|
1079
|
+
const replace = await confirm({
|
|
1080
|
+
message: `Profile "${profileName}" already exists. Replace?`,
|
|
1081
|
+
default: false
|
|
1082
|
+
});
|
|
1083
|
+
if (!replace) {
|
|
1084
|
+
stderrWrite("Aborted.");
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1071
1087
|
}
|
|
1088
|
+
const profiles = config.profiles ?? {};
|
|
1089
|
+
profiles[profileName] = {
|
|
1090
|
+
token,
|
|
1091
|
+
workspace_name: workspaceName,
|
|
1092
|
+
workspace_id: workspaceId
|
|
1093
|
+
};
|
|
1094
|
+
await writeGlobalConfig({
|
|
1095
|
+
...config,
|
|
1096
|
+
profiles,
|
|
1097
|
+
active_profile: profileName
|
|
1098
|
+
});
|
|
1099
|
+
stderrWrite(success(`Profile "${profileName}" saved and set as active.`));
|
|
1100
|
+
stderrWrite(dim("Checking integration access..."));
|
|
1072
1101
|
try {
|
|
1073
|
-
const
|
|
1074
|
-
await
|
|
1075
|
-
|
|
1102
|
+
const notion = createNotionClient(token);
|
|
1103
|
+
const probe = await notion.search({ page_size: 1 });
|
|
1104
|
+
if (probe.results.length === 0) {
|
|
1105
|
+
stderrWrite("");
|
|
1106
|
+
stderrWrite("\u26A0\uFE0F Your integration has no pages connected.");
|
|
1107
|
+
stderrWrite(" To grant access, open any Notion page or database:");
|
|
1108
|
+
stderrWrite(" 1. Click \xB7\xB7\xB7 (three dots) in the top-right corner");
|
|
1109
|
+
stderrWrite(' 2. Select "Connect to"');
|
|
1110
|
+
stderrWrite(` 3. Choose "${workspaceName}"`);
|
|
1111
|
+
stderrWrite(" Then re-run any notion command to confirm access.");
|
|
1112
|
+
} else {
|
|
1113
|
+
stderrWrite(
|
|
1114
|
+
success(
|
|
1115
|
+
`\u2713 Integration has access to content in ${bold(workspaceName)}.`
|
|
1116
|
+
)
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1076
1119
|
} 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"
|
|
1120
|
+
stderrWrite(
|
|
1121
|
+
dim("(Could not verify integration access \u2014 run `notion ls` to check)")
|
|
1082
1122
|
);
|
|
1083
1123
|
}
|
|
1124
|
+
stderrWrite("");
|
|
1125
|
+
stderrWrite(
|
|
1126
|
+
dim("Write commands (comment, append, create-page) require additional")
|
|
1127
|
+
);
|
|
1128
|
+
stderrWrite(dim("capabilities in your integration settings:"));
|
|
1129
|
+
stderrWrite(
|
|
1130
|
+
dim(" notion.so/profile/integrations/internal \u2192 your integration \u2192")
|
|
1131
|
+
);
|
|
1132
|
+
stderrWrite(
|
|
1133
|
+
dim(
|
|
1134
|
+
' Capabilities: enable "Read content", "Insert content", "Read comments", "Insert comments"'
|
|
1135
|
+
)
|
|
1136
|
+
);
|
|
1137
|
+
stderrWrite("");
|
|
1138
|
+
stderrWrite(
|
|
1139
|
+
dim("To post comments and create pages attributed to your user account:")
|
|
1140
|
+
);
|
|
1141
|
+
stderrWrite(dim(" notion auth login"));
|
|
1084
1142
|
}
|
|
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
|
-
}
|
|
1143
|
+
function initCommand() {
|
|
1144
|
+
const cmd = new Command2("init");
|
|
1145
|
+
cmd.description("authenticate with Notion and save a profile").action(
|
|
1146
|
+
withErrorHandling(async () => {
|
|
1147
|
+
if (!process.stdin.isTTY) {
|
|
1148
|
+
throw new CliError(
|
|
1149
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
1150
|
+
"Cannot run interactive init in non-TTY mode.",
|
|
1151
|
+
"Set NOTION_API_TOKEN environment variable or create .notion.yaml"
|
|
1152
|
+
);
|
|
1106
1153
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1154
|
+
await runInitFlow();
|
|
1155
|
+
})
|
|
1156
|
+
);
|
|
1157
|
+
return cmd;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// src/commands/auth/login.ts
|
|
1161
|
+
function loginCommand() {
|
|
1162
|
+
const cmd = new Command3("login");
|
|
1163
|
+
cmd.description("authenticate with Notion \u2014 choose OAuth or integration token").option("--profile <name>", "profile name to store credentials in").option(
|
|
1164
|
+
"--manual",
|
|
1165
|
+
"print auth URL instead of opening browser (for headless OAuth)"
|
|
1166
|
+
).action(
|
|
1167
|
+
withErrorHandling(async (opts) => {
|
|
1168
|
+
if (!process.stdin.isTTY && !opts.manual) {
|
|
1169
|
+
throw new CliError(
|
|
1170
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
1171
|
+
"Cannot run interactive login in non-TTY mode.",
|
|
1172
|
+
"Use --manual flag to get an auth URL you can open in a browser"
|
|
1173
|
+
);
|
|
1116
1174
|
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1175
|
+
const method = await select({
|
|
1176
|
+
message: "How do you want to authenticate with Notion?",
|
|
1177
|
+
choices: [
|
|
1178
|
+
{
|
|
1179
|
+
name: "OAuth user login (browser required)",
|
|
1180
|
+
value: "oauth",
|
|
1181
|
+
description: "Opens Notion in your browser. Comments and pages are attributed to your account. Tokens auto-refresh."
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
name: "Internal integration token (CI/headless friendly)",
|
|
1185
|
+
value: "token",
|
|
1186
|
+
description: "Paste a token from notion.so/profile/integrations. No browser needed. Write ops attributed to integration bot."
|
|
1187
|
+
}
|
|
1188
|
+
]
|
|
1189
|
+
});
|
|
1190
|
+
if (method === "oauth") {
|
|
1191
|
+
let profileName = opts.profile;
|
|
1192
|
+
if (!profileName) {
|
|
1193
|
+
const config = await readGlobalConfig();
|
|
1194
|
+
profileName = config.active_profile ?? "default";
|
|
1195
|
+
}
|
|
1196
|
+
const result = await runOAuthFlow({ manual: opts.manual });
|
|
1197
|
+
const response = await exchangeCode(result.code);
|
|
1198
|
+
await saveOAuthTokens(profileName, response);
|
|
1199
|
+
const userName = response.owner?.user?.name ?? "unknown user";
|
|
1200
|
+
const workspaceName = response.workspace_name ?? "unknown workspace";
|
|
1201
|
+
stderrWrite(
|
|
1202
|
+
success(`\u2713 Logged in as ${userName} to workspace ${workspaceName}`)
|
|
1203
|
+
);
|
|
1204
|
+
stderrWrite(
|
|
1205
|
+
dim(
|
|
1206
|
+
"Your comments and pages will now be attributed to your Notion account."
|
|
1207
|
+
)
|
|
1208
|
+
);
|
|
1209
|
+
} else {
|
|
1210
|
+
await runInitFlow();
|
|
1119
1211
|
}
|
|
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'
|
|
1212
|
+
})
|
|
1126
1213
|
);
|
|
1214
|
+
return cmd;
|
|
1127
1215
|
}
|
|
1128
1216
|
|
|
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;
|
|
1217
|
+
// src/commands/auth/logout.ts
|
|
1218
|
+
import { select as select2 } from "@inquirer/prompts";
|
|
1219
|
+
import { Command as Command4 } from "commander";
|
|
1220
|
+
function profileLabel(name, profile) {
|
|
1221
|
+
const parts = [];
|
|
1222
|
+
if (profile.oauth_access_token)
|
|
1223
|
+
parts.push(
|
|
1224
|
+
`OAuth${profile.oauth_user_name ? ` (${profile.oauth_user_name})` : ""}`
|
|
1225
|
+
);
|
|
1226
|
+
if (profile.token) parts.push("integration token");
|
|
1227
|
+
const authDesc = parts.length > 0 ? parts.join(" + ") : "no credentials";
|
|
1228
|
+
const workspace = profile.workspace_name ? dim(` \u2014 ${profile.workspace_name}`) : "";
|
|
1229
|
+
return `${bold(name)} ${dim(authDesc)}${workspace}`;
|
|
1145
1230
|
}
|
|
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
|
-
`);
|
|
1231
|
+
function logoutCommand() {
|
|
1232
|
+
const cmd = new Command4("logout");
|
|
1233
|
+
cmd.description("remove a profile and its credentials").option(
|
|
1234
|
+
"--profile <name>",
|
|
1235
|
+
"profile name to remove (skips interactive selector)"
|
|
1236
|
+
).action(
|
|
1237
|
+
withErrorHandling(async (opts) => {
|
|
1238
|
+
const config = await readGlobalConfig();
|
|
1239
|
+
const profiles = config.profiles ?? {};
|
|
1240
|
+
const profileNames = Object.keys(profiles);
|
|
1241
|
+
if (profileNames.length === 0) {
|
|
1242
|
+
stderrWrite("No profiles configured.");
|
|
1171
1243
|
return;
|
|
1172
1244
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1245
|
+
let profileName = opts.profile;
|
|
1246
|
+
if (!profileName) {
|
|
1247
|
+
if (!process.stdin.isTTY) {
|
|
1248
|
+
throw new CliError(
|
|
1249
|
+
ErrorCodes.AUTH_NO_TOKEN,
|
|
1250
|
+
"Cannot run interactive logout in non-TTY mode.",
|
|
1251
|
+
"Use --profile <name> to specify the profile to remove"
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
profileName = await select2({
|
|
1255
|
+
message: "Which profile do you want to log out of?",
|
|
1256
|
+
choices: profileNames.map((name) => ({
|
|
1257
|
+
// biome-ignore lint/style/noNonNullAssertion: key is from Object.keys, always present
|
|
1258
|
+
name: profileLabel(name, profiles[name]),
|
|
1259
|
+
value: name
|
|
1260
|
+
}))
|
|
1261
|
+
});
|
|
1262
|
+
}
|
|
1263
|
+
if (!profiles[profileName]) {
|
|
1264
|
+
throw new CliError(
|
|
1265
|
+
ErrorCodes.AUTH_PROFILE_NOT_FOUND,
|
|
1266
|
+
`Profile "${profileName}" not found.`,
|
|
1267
|
+
`Run "notion auth list" to see available profiles`
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
const updatedProfiles = { ...profiles };
|
|
1271
|
+
delete updatedProfiles[profileName];
|
|
1272
|
+
const newActiveProfile = config.active_profile === profileName ? void 0 : config.active_profile;
|
|
1273
|
+
await writeGlobalConfig({
|
|
1274
|
+
...config,
|
|
1275
|
+
profiles: updatedProfiles,
|
|
1276
|
+
active_profile: newActiveProfile
|
|
1277
|
+
});
|
|
1278
|
+
stderrWrite(success(`\u2713 Logged out of profile "${profileName}".`));
|
|
1279
|
+
if (newActiveProfile === void 0 && Object.keys(updatedProfiles).length > 0) {
|
|
1280
|
+
stderrWrite(
|
|
1281
|
+
dim(`Run "notion auth use <name>" to set a new active profile.`)
|
|
1282
|
+
);
|
|
1185
1283
|
}
|
|
1186
1284
|
})
|
|
1187
1285
|
);
|
|
1188
1286
|
return cmd;
|
|
1189
1287
|
}
|
|
1190
1288
|
|
|
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(
|
|
1289
|
+
// src/commands/auth/status.ts
|
|
1290
|
+
import { Command as Command5 } from "commander";
|
|
1291
|
+
function statusCommand() {
|
|
1292
|
+
const cmd = new Command5("status");
|
|
1293
|
+
cmd.description("show authentication status for the active profile").option("--profile <name>", "profile name to check").action(
|
|
1215
1294
|
withErrorHandling(async (opts) => {
|
|
1216
|
-
|
|
1217
|
-
|
|
1295
|
+
let profileName = opts.profile;
|
|
1296
|
+
const config = await readGlobalConfig();
|
|
1297
|
+
if (!profileName) {
|
|
1298
|
+
profileName = config.active_profile ?? "default";
|
|
1299
|
+
}
|
|
1300
|
+
const profile = config.profiles?.[profileName];
|
|
1301
|
+
stderrWrite(`Profile: ${profileName}`);
|
|
1302
|
+
if (!profile) {
|
|
1303
|
+
stderrWrite(
|
|
1304
|
+
` ${error("\u2717")} No profile found (run 'notion init' to create one)`
|
|
1305
|
+
);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
if (profile.oauth_access_token) {
|
|
1309
|
+
const userName = profile.oauth_user_name ?? "unknown";
|
|
1310
|
+
const userId = profile.oauth_user_id ?? "unknown";
|
|
1311
|
+
stderrWrite(
|
|
1312
|
+
` OAuth: ${success("\u2713")} Logged in as ${userName} (user: ${userId})`
|
|
1313
|
+
);
|
|
1314
|
+
if (profile.oauth_expiry_ms != null) {
|
|
1315
|
+
const expiryDate = new Date(profile.oauth_expiry_ms).toISOString();
|
|
1316
|
+
stderrWrite(dim(` Access token expires: ${expiryDate}`));
|
|
1317
|
+
}
|
|
1318
|
+
} else {
|
|
1319
|
+
stderrWrite(
|
|
1320
|
+
` OAuth: ${error("\u2717")} Not logged in (run 'notion auth login')`
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
if (profile.token) {
|
|
1324
|
+
const tokenPreview = `${profile.token.substring(0, 10)}...`;
|
|
1325
|
+
stderrWrite(
|
|
1326
|
+
` Internal token: ${success("\u2713")} Configured (${tokenPreview})`
|
|
1327
|
+
);
|
|
1328
|
+
} else {
|
|
1329
|
+
stderrWrite(` Internal token: ${error("\u2717")} Not configured`);
|
|
1330
|
+
}
|
|
1331
|
+
if (profile.oauth_access_token) {
|
|
1332
|
+
stderrWrite(` Active method: OAuth (user-attributed)`);
|
|
1333
|
+
} else if (profile.token) {
|
|
1334
|
+
stderrWrite(
|
|
1335
|
+
` Active method: Internal integration token (bot-attributed)`
|
|
1336
|
+
);
|
|
1337
|
+
} else {
|
|
1338
|
+
stderrWrite(
|
|
1339
|
+
dim(
|
|
1340
|
+
` Active method: None (run 'notion auth login' or 'notion init')`
|
|
1341
|
+
)
|
|
1342
|
+
);
|
|
1218
1343
|
}
|
|
1344
|
+
})
|
|
1345
|
+
);
|
|
1346
|
+
return cmd;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/commands/comment-add.ts
|
|
1350
|
+
import { Command as Command6 } from "commander";
|
|
1351
|
+
function commentAddCommand() {
|
|
1352
|
+
const cmd = new Command6("comment");
|
|
1353
|
+
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(
|
|
1354
|
+
withErrorHandling(async (idOrUrl, opts) => {
|
|
1219
1355
|
const { token, source } = await resolveToken();
|
|
1220
1356
|
reportTokenSource(source);
|
|
1221
|
-
const
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1224
|
-
|
|
1357
|
+
const client = createNotionClient(token);
|
|
1358
|
+
const id = parseNotionId(idOrUrl);
|
|
1359
|
+
const uuid = toUuid(id);
|
|
1360
|
+
await addComment(client, uuid, opts.message, {
|
|
1361
|
+
asUser: source === "oauth"
|
|
1225
1362
|
});
|
|
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
|
-
}
|
|
1363
|
+
process.stdout.write("Comment added.\n");
|
|
1250
1364
|
})
|
|
1251
1365
|
);
|
|
1252
1366
|
return cmd;
|
|
1253
1367
|
}
|
|
1254
1368
|
|
|
1255
|
-
// src/commands/
|
|
1256
|
-
import {
|
|
1257
|
-
import { promisify } from "util";
|
|
1258
|
-
import { Command as Command11 } from "commander";
|
|
1369
|
+
// src/commands/comments.ts
|
|
1370
|
+
import { Command as Command7 } from "commander";
|
|
1259
1371
|
|
|
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
|
-
);
|
|
1372
|
+
// src/output/format.ts
|
|
1373
|
+
var _mode = "auto";
|
|
1374
|
+
function setOutputMode(mode) {
|
|
1375
|
+
_mode = mode;
|
|
1270
1376
|
}
|
|
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);
|
|
1377
|
+
function getOutputMode() {
|
|
1378
|
+
return _mode;
|
|
1284
1379
|
}
|
|
1285
|
-
function
|
|
1286
|
-
return
|
|
1380
|
+
function isatty() {
|
|
1381
|
+
return Boolean(process.stdout.isTTY);
|
|
1287
1382
|
}
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1383
|
+
function isHumanMode() {
|
|
1384
|
+
if (_mode === "json") return false;
|
|
1385
|
+
if (_mode === "md") return false;
|
|
1386
|
+
return true;
|
|
1387
|
+
}
|
|
1388
|
+
function formatJSON(data) {
|
|
1389
|
+
return JSON.stringify(data, null, 2);
|
|
1390
|
+
}
|
|
1391
|
+
var COLUMN_CAPS = {
|
|
1392
|
+
TITLE: 50,
|
|
1393
|
+
ID: 36
|
|
1394
|
+
};
|
|
1395
|
+
var DEFAULT_MAX_COL_WIDTH = 40;
|
|
1396
|
+
function getColumnCap(header) {
|
|
1397
|
+
return COLUMN_CAPS[header.toUpperCase()] ?? DEFAULT_MAX_COL_WIDTH;
|
|
1398
|
+
}
|
|
1399
|
+
function truncate(str, maxLen) {
|
|
1400
|
+
if (str.length <= maxLen) return str;
|
|
1401
|
+
return `${str.slice(0, maxLen - 1)}\u2026`;
|
|
1402
|
+
}
|
|
1403
|
+
function formatTable(rows, headers) {
|
|
1404
|
+
const colWidths = headers.map((header, colIdx) => {
|
|
1405
|
+
const cap = getColumnCap(header);
|
|
1406
|
+
const headerLen = header.length;
|
|
1407
|
+
const maxRowLen = rows.reduce((max, row) => {
|
|
1408
|
+
const cell = row[colIdx] ?? "";
|
|
1409
|
+
return Math.max(max, cell.length);
|
|
1410
|
+
}, 0);
|
|
1411
|
+
return Math.min(Math.max(headerLen, maxRowLen), cap);
|
|
1412
|
+
});
|
|
1413
|
+
const sep = "\u2500";
|
|
1414
|
+
const colSep = " ";
|
|
1415
|
+
const headerRow = headers.map((h, i) => h.padEnd(colWidths[i])).join(colSep);
|
|
1416
|
+
const separatorRow = colWidths.map((w) => sep.repeat(w)).join(colSep);
|
|
1417
|
+
const dataRows = rows.map(
|
|
1418
|
+
(row) => headers.map((_, i) => {
|
|
1419
|
+
const cell = row[i] ?? "";
|
|
1420
|
+
return truncate(cell, colWidths[i]).padEnd(colWidths[i]);
|
|
1421
|
+
}).join(colSep)
|
|
1422
|
+
);
|
|
1423
|
+
return [headerRow, separatorRow, ...dataRows].join("\n");
|
|
1424
|
+
}
|
|
1425
|
+
function printOutput(data, tableHeaders, tableRows) {
|
|
1426
|
+
const mode = getOutputMode();
|
|
1427
|
+
if (mode === "json") {
|
|
1428
|
+
process.stdout.write(`${formatJSON(data)}
|
|
1300
1429
|
`);
|
|
1301
|
-
}))
|
|
1302
|
-
|
|
1430
|
+
} else if (isHumanMode() && tableHeaders && tableRows) {
|
|
1431
|
+
printWithPager(`${formatTable(tableRows, tableHeaders)}
|
|
1432
|
+
`);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
function printWithPager(text) {
|
|
1436
|
+
process.stdout.write(text);
|
|
1303
1437
|
}
|
|
1304
|
-
|
|
1305
|
-
// src/commands/users.ts
|
|
1306
|
-
import { Command as Command12 } from "commander";
|
|
1307
1438
|
|
|
1308
1439
|
// src/output/paginate.ts
|
|
1309
1440
|
async function paginateResults(fetcher) {
|
|
@@ -1319,264 +1450,337 @@ async function paginateResults(fetcher) {
|
|
|
1319
1450
|
return allResults;
|
|
1320
1451
|
}
|
|
1321
1452
|
|
|
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
|
-
}));
|
|
1453
|
+
// src/commands/comments.ts
|
|
1454
|
+
function commentsCommand() {
|
|
1455
|
+
const cmd = new Command7("comments");
|
|
1456
|
+
cmd.description("list comments on a Notion page").argument("<id/url>", "Notion page ID or URL").option("--json", "output as JSON").action(
|
|
1457
|
+
withErrorHandling(async (idOrUrl, opts) => {
|
|
1458
|
+
if (opts.json) setOutputMode("json");
|
|
1459
|
+
const id = parseNotionId(idOrUrl);
|
|
1460
|
+
const uuid = toUuid(id);
|
|
1461
|
+
const { token, source } = await resolveToken();
|
|
1462
|
+
reportTokenSource(source);
|
|
1463
|
+
const notion = createNotionClient(token);
|
|
1464
|
+
const comments = await paginateResults(
|
|
1465
|
+
(cursor) => notion.comments.list({ block_id: uuid, start_cursor: cursor })
|
|
1466
|
+
);
|
|
1467
|
+
if (comments.length === 0) {
|
|
1468
|
+
process.stdout.write("No comments found on this page\n");
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
const rows = comments.map((comment) => {
|
|
1472
|
+
const text = comment.rich_text.map((t) => t.plain_text).join("");
|
|
1473
|
+
return [
|
|
1474
|
+
comment.created_time.split("T")[0],
|
|
1475
|
+
`${comment.created_by.id.slice(0, 8)}...`,
|
|
1476
|
+
text.slice(0, 80) + (text.length > 80 ? "\u2026" : "")
|
|
1477
|
+
];
|
|
1478
|
+
});
|
|
1479
|
+
printOutput(comments, ["DATE", "AUTHOR ID", "COMMENT"], rows);
|
|
1480
|
+
})
|
|
1481
|
+
);
|
|
1352
1482
|
return cmd;
|
|
1353
1483
|
}
|
|
1354
1484
|
|
|
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
|
-
|
|
1485
|
+
// src/commands/completion.ts
|
|
1486
|
+
import { Command as Command8 } from "commander";
|
|
1487
|
+
var BASH_COMPLETION = `# notion bash completion
|
|
1488
|
+
_notion_completion() {
|
|
1489
|
+
local cur prev words cword
|
|
1490
|
+
_init_completion || return
|
|
1491
|
+
|
|
1492
|
+
local commands="init profile completion --help --version --verbose --color"
|
|
1493
|
+
local profile_commands="list use remove"
|
|
1494
|
+
|
|
1495
|
+
case "$prev" in
|
|
1496
|
+
notion)
|
|
1497
|
+
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
1498
|
+
return 0
|
|
1499
|
+
;;
|
|
1500
|
+
profile)
|
|
1501
|
+
COMPREPLY=($(compgen -W "$profile_commands" -- "$cur"))
|
|
1502
|
+
return 0
|
|
1503
|
+
;;
|
|
1504
|
+
completion)
|
|
1505
|
+
COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))
|
|
1506
|
+
return 0
|
|
1507
|
+
;;
|
|
1508
|
+
esac
|
|
1509
|
+
|
|
1510
|
+
COMPREPLY=($(compgen -W "$commands" -- "$cur"))
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
complete -F _notion_completion notion
|
|
1514
|
+
`;
|
|
1515
|
+
var ZSH_COMPLETION = `#compdef notion
|
|
1516
|
+
# notion zsh completion
|
|
1517
|
+
|
|
1518
|
+
_notion() {
|
|
1519
|
+
local -a commands
|
|
1520
|
+
|
|
1521
|
+
commands=(
|
|
1522
|
+
'init:authenticate with Notion and save a profile'
|
|
1523
|
+
'profile:manage authentication profiles'
|
|
1524
|
+
'completion:output shell completion script'
|
|
1525
|
+
)
|
|
1526
|
+
|
|
1527
|
+
local -a global_opts
|
|
1528
|
+
global_opts=(
|
|
1529
|
+
'--help[display help]'
|
|
1530
|
+
'--version[output version]'
|
|
1531
|
+
'--verbose[show API requests/responses]'
|
|
1532
|
+
'--color[force color output]'
|
|
1533
|
+
)
|
|
1534
|
+
|
|
1535
|
+
if (( CURRENT == 2 )); then
|
|
1536
|
+
_describe 'command' commands
|
|
1537
|
+
_arguments $global_opts
|
|
1538
|
+
return
|
|
1539
|
+
fi
|
|
1540
|
+
|
|
1541
|
+
case $words[2] in
|
|
1542
|
+
profile)
|
|
1543
|
+
local -a profile_cmds
|
|
1544
|
+
profile_cmds=(
|
|
1545
|
+
'list:list all authentication profiles'
|
|
1546
|
+
'use:switch the active profile'
|
|
1547
|
+
'remove:remove an authentication profile'
|
|
1548
|
+
)
|
|
1549
|
+
_describe 'profile command' profile_cmds
|
|
1550
|
+
;;
|
|
1551
|
+
completion)
|
|
1552
|
+
local -a shells
|
|
1553
|
+
shells=('bash' 'zsh' 'fish')
|
|
1554
|
+
_describe 'shell' shells
|
|
1555
|
+
;;
|
|
1556
|
+
esac
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
_notion "$@"
|
|
1560
|
+
`;
|
|
1561
|
+
var FISH_COMPLETION = `# notion fish completion
|
|
1562
|
+
|
|
1563
|
+
# Disable file completion by default
|
|
1564
|
+
complete -c notion -f
|
|
1565
|
+
|
|
1566
|
+
# Global options
|
|
1567
|
+
complete -c notion -l help -d 'display help'
|
|
1568
|
+
complete -c notion -l version -d 'output version'
|
|
1569
|
+
complete -c notion -l verbose -d 'show API requests/responses'
|
|
1570
|
+
complete -c notion -l color -d 'force color output'
|
|
1571
|
+
|
|
1572
|
+
# Top-level commands
|
|
1573
|
+
complete -c notion -n '__fish_use_subcommand' -a init -d 'authenticate with Notion and save a profile'
|
|
1574
|
+
complete -c notion -n '__fish_use_subcommand' -a profile -d 'manage authentication profiles'
|
|
1575
|
+
complete -c notion -n '__fish_use_subcommand' -a completion -d 'output shell completion script'
|
|
1576
|
+
|
|
1577
|
+
# profile subcommands
|
|
1578
|
+
complete -c notion -n '__fish_seen_subcommand_from profile' -a list -d 'list all authentication profiles'
|
|
1579
|
+
complete -c notion -n '__fish_seen_subcommand_from profile' -a use -d 'switch the active profile'
|
|
1580
|
+
complete -c notion -n '__fish_seen_subcommand_from profile' -a remove -d 'remove an authentication profile'
|
|
1581
|
+
|
|
1582
|
+
# completion shells
|
|
1583
|
+
complete -c notion -n '__fish_seen_subcommand_from completion' -a bash -d 'bash completion script'
|
|
1584
|
+
complete -c notion -n '__fish_seen_subcommand_from completion' -a zsh -d 'zsh completion script'
|
|
1585
|
+
complete -c notion -n '__fish_seen_subcommand_from completion' -a fish -d 'fish completion script'
|
|
1586
|
+
`;
|
|
1587
|
+
function completionCommand() {
|
|
1588
|
+
const cmd = new Command8("completion");
|
|
1589
|
+
cmd.description("output shell completion script").argument("<shell>", "shell type (bash, zsh, fish)").action(
|
|
1590
|
+
withErrorHandling(async (shell) => {
|
|
1591
|
+
switch (shell) {
|
|
1592
|
+
case "bash":
|
|
1593
|
+
process.stdout.write(BASH_COMPLETION);
|
|
1594
|
+
break;
|
|
1595
|
+
case "zsh":
|
|
1596
|
+
process.stdout.write(ZSH_COMPLETION);
|
|
1597
|
+
break;
|
|
1598
|
+
case "fish":
|
|
1599
|
+
process.stdout.write(FISH_COMPLETION);
|
|
1600
|
+
break;
|
|
1601
|
+
default:
|
|
1602
|
+
throw new CliError(
|
|
1603
|
+
ErrorCodes.UNKNOWN,
|
|
1604
|
+
`Unknown shell: "${shell}".`,
|
|
1605
|
+
"Supported shells: bash, zsh, fish"
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
})
|
|
1609
|
+
);
|
|
1383
1610
|
return cmd;
|
|
1384
1611
|
}
|
|
1385
1612
|
|
|
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);
|
|
1613
|
+
// src/commands/create-page.ts
|
|
1614
|
+
import { Command as Command9 } from "commander";
|
|
1615
|
+
async function readStdin() {
|
|
1616
|
+
const chunks = [];
|
|
1617
|
+
for await (const chunk of process.stdin) {
|
|
1618
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1409
1619
|
}
|
|
1410
|
-
return
|
|
1620
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
1411
1621
|
}
|
|
1412
|
-
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1622
|
+
function createPageCommand() {
|
|
1623
|
+
const cmd = new Command9("create-page");
|
|
1624
|
+
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(
|
|
1625
|
+
"-m, --message <markdown>",
|
|
1626
|
+
"inline markdown content for the page body"
|
|
1627
|
+
).action(
|
|
1628
|
+
withErrorHandling(
|
|
1629
|
+
async (opts) => {
|
|
1630
|
+
const { token, source } = await resolveToken();
|
|
1631
|
+
reportTokenSource(source);
|
|
1632
|
+
const client = createNotionClient(token);
|
|
1633
|
+
let markdown = "";
|
|
1634
|
+
if (opts.message) {
|
|
1635
|
+
markdown = opts.message;
|
|
1636
|
+
} else if (!process.stdin.isTTY) {
|
|
1637
|
+
markdown = await readStdin();
|
|
1638
|
+
}
|
|
1639
|
+
const blocks = mdToBlocks(markdown);
|
|
1640
|
+
const parentUuid = toUuid(parseNotionId(opts.parent));
|
|
1641
|
+
const url = await createPage(client, parentUuid, opts.title, blocks);
|
|
1642
|
+
process.stdout.write(`${url}
|
|
1643
|
+
`);
|
|
1644
|
+
}
|
|
1645
|
+
)
|
|
1646
|
+
);
|
|
1647
|
+
return cmd;
|
|
1416
1648
|
}
|
|
1417
1649
|
|
|
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
|
-
}
|
|
1650
|
+
// src/commands/db/query.ts
|
|
1651
|
+
import { Command as Command10 } from "commander";
|
|
1441
1652
|
|
|
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);
|
|
1653
|
+
// src/services/database.service.ts
|
|
1654
|
+
import { isFullPage } from "@notionhq/client";
|
|
1655
|
+
async function fetchDatabaseSchema(client, dbId) {
|
|
1656
|
+
const ds = await client.dataSources.retrieve({ data_source_id: dbId });
|
|
1657
|
+
const title = "title" in ds ? ds.title.map((rt) => rt.plain_text).join("") || dbId : dbId;
|
|
1658
|
+
const properties = {};
|
|
1659
|
+
if ("properties" in ds) {
|
|
1660
|
+
for (const [name, prop] of Object.entries(ds.properties)) {
|
|
1661
|
+
const config = {
|
|
1662
|
+
id: prop.id,
|
|
1663
|
+
name,
|
|
1664
|
+
type: prop.type
|
|
1665
|
+
};
|
|
1666
|
+
if (prop.type === "select" && "select" in prop) {
|
|
1667
|
+
config.options = prop.select.options;
|
|
1668
|
+
} else if (prop.type === "status" && "status" in prop) {
|
|
1669
|
+
config.options = prop.status.options;
|
|
1670
|
+
} else if (prop.type === "multi_select" && "multi_select" in prop) {
|
|
1671
|
+
config.options = prop.multi_select.options;
|
|
1672
|
+
}
|
|
1673
|
+
properties[name] = config;
|
|
1474
1674
|
}
|
|
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
|
-
`;
|
|
1675
|
+
}
|
|
1676
|
+
return { id: dbId, title, properties };
|
|
1677
|
+
}
|
|
1678
|
+
async function queryDatabase(client, dbId, opts = {}) {
|
|
1679
|
+
const rawPages = await paginateResults(
|
|
1680
|
+
(cursor) => client.dataSources.query({
|
|
1681
|
+
data_source_id: dbId,
|
|
1682
|
+
filter: opts.filter,
|
|
1683
|
+
sorts: opts.sorts,
|
|
1684
|
+
start_cursor: cursor,
|
|
1685
|
+
page_size: 100
|
|
1686
|
+
})
|
|
1687
|
+
);
|
|
1688
|
+
return rawPages.filter(isFullPage).map((page) => {
|
|
1689
|
+
const propValues = {};
|
|
1690
|
+
for (const [name, prop] of Object.entries(page.properties)) {
|
|
1691
|
+
if (opts.columns && !opts.columns.includes(name)) continue;
|
|
1692
|
+
propValues[name] = displayPropertyValue(prop);
|
|
1513
1693
|
}
|
|
1514
|
-
return
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
if (
|
|
1522
|
-
|
|
1694
|
+
return { id: page.id, properties: propValues, raw: page };
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
function buildFilter(filterStrings, schema) {
|
|
1698
|
+
if (!filterStrings.length) return void 0;
|
|
1699
|
+
const filters = filterStrings.map((raw) => {
|
|
1700
|
+
const eqIdx = raw.indexOf("=");
|
|
1701
|
+
if (eqIdx === -1) {
|
|
1702
|
+
throw new CliError(
|
|
1703
|
+
ErrorCodes.INVALID_ARG,
|
|
1704
|
+
`Invalid filter syntax: "${raw}"`,
|
|
1705
|
+
'Use format: --filter "PropertyName=Value"'
|
|
1706
|
+
);
|
|
1523
1707
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1708
|
+
const propName = raw.slice(0, eqIdx).trim();
|
|
1709
|
+
const value = raw.slice(eqIdx + 1).trim();
|
|
1710
|
+
const propConfig = schema.properties[propName];
|
|
1711
|
+
if (!propConfig) {
|
|
1712
|
+
const available = Object.keys(schema.properties).join(", ");
|
|
1713
|
+
throw new CliError(
|
|
1714
|
+
ErrorCodes.INVALID_ARG,
|
|
1715
|
+
`Property "${propName}" not found`,
|
|
1716
|
+
`Available properties: ${available}`
|
|
1717
|
+
);
|
|
1534
1718
|
}
|
|
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);
|
|
1719
|
+
return buildPropertyFilter(propName, propConfig.type, value);
|
|
1720
|
+
});
|
|
1721
|
+
return filters.length === 1 ? filters[0] : { and: filters };
|
|
1722
|
+
}
|
|
1723
|
+
function buildPropertyFilter(property, type, value) {
|
|
1724
|
+
switch (type) {
|
|
1725
|
+
case "select":
|
|
1726
|
+
return { property, select: { equals: value } };
|
|
1727
|
+
case "status":
|
|
1728
|
+
return { property, status: { equals: value } };
|
|
1729
|
+
case "multi_select":
|
|
1730
|
+
return { property, multi_select: { contains: value } };
|
|
1731
|
+
case "checkbox":
|
|
1732
|
+
return { property, checkbox: { equals: value.toLowerCase() === "true" } };
|
|
1733
|
+
case "number":
|
|
1734
|
+
return { property, number: { equals: Number(value) } };
|
|
1735
|
+
case "title":
|
|
1736
|
+
return { property, title: { contains: value } };
|
|
1737
|
+
case "rich_text":
|
|
1738
|
+
return { property, rich_text: { contains: value } };
|
|
1739
|
+
case "url":
|
|
1740
|
+
return { property, url: { contains: value } };
|
|
1741
|
+
case "email":
|
|
1742
|
+
return { property, email: { contains: value } };
|
|
1743
|
+
default:
|
|
1744
|
+
throw new CliError(
|
|
1745
|
+
ErrorCodes.INVALID_ARG,
|
|
1746
|
+
`Filtering by property type "${type}" is not supported`
|
|
1747
|
+
);
|
|
1566
1748
|
}
|
|
1567
|
-
return `<!-- unsupported block: ${block.type} -->
|
|
1568
|
-
`;
|
|
1569
1749
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1750
|
+
function buildSorts(sortStrings) {
|
|
1751
|
+
return sortStrings.map((raw) => {
|
|
1752
|
+
const colonIdx = raw.lastIndexOf(":");
|
|
1753
|
+
if (colonIdx === -1) {
|
|
1754
|
+
return { property: raw.trim(), direction: "ascending" };
|
|
1755
|
+
}
|
|
1756
|
+
const property = raw.slice(0, colonIdx).trim();
|
|
1757
|
+
const dir = raw.slice(colonIdx + 1).trim().toLowerCase();
|
|
1758
|
+
return {
|
|
1759
|
+
property,
|
|
1760
|
+
direction: dir === "desc" || dir === "descending" ? "descending" : "ascending"
|
|
1761
|
+
};
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
function displayFormula(f) {
|
|
1765
|
+
if (f.type === "string") return f.string ?? "";
|
|
1766
|
+
if (f.type === "number")
|
|
1767
|
+
return f.number !== null && f.number !== void 0 ? String(f.number) : "";
|
|
1768
|
+
if (f.type === "boolean") return f.boolean ? "true" : "false";
|
|
1769
|
+
if (f.type === "date") return f.date?.start ?? "";
|
|
1770
|
+
return "";
|
|
1771
|
+
}
|
|
1772
|
+
function displayDate(date) {
|
|
1773
|
+
if (!date) return "";
|
|
1774
|
+
return date.end ? `${date.start} \u2192 ${date.end}` : date.start;
|
|
1775
|
+
}
|
|
1776
|
+
function displayPropertyValue(prop) {
|
|
1573
1777
|
switch (prop.type) {
|
|
1574
1778
|
case "title":
|
|
1575
|
-
return prop.title.map((
|
|
1779
|
+
return prop.title.map((r) => r.plain_text).join("").replace(/\n/g, " ");
|
|
1576
1780
|
case "rich_text":
|
|
1577
|
-
return prop.rich_text.map((
|
|
1781
|
+
return prop.rich_text.map((r) => r.plain_text).join("").replace(/\n/g, " ");
|
|
1578
1782
|
case "number":
|
|
1579
|
-
return prop.number !== null ? String(prop.number) : "";
|
|
1783
|
+
return prop.number !== null && prop.number !== void 0 ? String(prop.number) : "";
|
|
1580
1784
|
case "select":
|
|
1581
1785
|
return prop.select?.name ?? "";
|
|
1582
1786
|
case "status":
|
|
@@ -1584,10 +1788,9 @@ function formatPropertyValue(name, prop) {
|
|
|
1584
1788
|
case "multi_select":
|
|
1585
1789
|
return prop.multi_select.map((s) => s.name).join(", ");
|
|
1586
1790
|
case "date":
|
|
1587
|
-
|
|
1588
|
-
return prop.date.end ? `${prop.date.start} \u2192 ${prop.date.end}` : prop.date.start;
|
|
1791
|
+
return displayDate(prop.date);
|
|
1589
1792
|
case "checkbox":
|
|
1590
|
-
return prop.checkbox ? "
|
|
1793
|
+
return prop.checkbox ? "\u2713" : "\u2717";
|
|
1591
1794
|
case "url":
|
|
1592
1795
|
return prop.url ?? "";
|
|
1593
1796
|
case "email":
|
|
@@ -1597,35 +1800,13 @@ function formatPropertyValue(name, prop) {
|
|
|
1597
1800
|
case "people":
|
|
1598
1801
|
return prop.people.map((p) => "name" in p && p.name ? p.name : p.id).join(", ");
|
|
1599
1802
|
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
|
-
}
|
|
1803
|
+
return prop.relation.length > 0 ? `[${prop.relation.length}]` : "";
|
|
1804
|
+
case "formula":
|
|
1805
|
+
return displayFormula(prop.formula);
|
|
1616
1806
|
case "created_time":
|
|
1617
1807
|
return prop.created_time;
|
|
1618
1808
|
case "last_edited_time":
|
|
1619
1809
|
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
1810
|
case "unique_id":
|
|
1630
1811
|
return prop.unique_id.prefix ? `${prop.unique_id.prefix}-${prop.unique_id.number}` : String(prop.unique_id.number ?? "");
|
|
1631
1812
|
default:
|
|
@@ -1633,295 +1814,452 @@ function formatPropertyValue(name, prop) {
|
|
|
1633
1814
|
}
|
|
1634
1815
|
}
|
|
1635
1816
|
|
|
1636
|
-
// src/
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1817
|
+
// src/commands/db/query.ts
|
|
1818
|
+
var SKIP_TYPES_IN_AUTO = /* @__PURE__ */ new Set(["relation", "rich_text", "people"]);
|
|
1819
|
+
function autoSelectColumns(schema, entries) {
|
|
1820
|
+
const termWidth = process.stdout.columns || 120;
|
|
1821
|
+
const COL_SEP = 2;
|
|
1822
|
+
const candidates = Object.values(schema.properties).filter((p) => !SKIP_TYPES_IN_AUTO.has(p.type)).map((p) => p.name);
|
|
1823
|
+
const widths = candidates.map((col) => {
|
|
1824
|
+
const header = col.toUpperCase().length;
|
|
1825
|
+
const maxData = entries.reduce(
|
|
1826
|
+
(max, e) => Math.max(max, (e.properties[col] ?? "").length),
|
|
1827
|
+
0
|
|
1828
|
+
);
|
|
1829
|
+
return Math.min(Math.max(header, maxData), 40);
|
|
1830
|
+
});
|
|
1831
|
+
const selected = [];
|
|
1832
|
+
let usedWidth = 0;
|
|
1833
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
1834
|
+
const needed = (selected.length > 0 ? COL_SEP : 0) + widths[i];
|
|
1835
|
+
if (usedWidth + needed > termWidth) break;
|
|
1836
|
+
selected.push(candidates[i]);
|
|
1837
|
+
usedWidth += needed;
|
|
1644
1838
|
}
|
|
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);
|
|
1839
|
+
if (selected.length === 0 && candidates.length > 0) {
|
|
1840
|
+
selected.push(candidates[0]);
|
|
1663
1841
|
}
|
|
1664
|
-
return
|
|
1842
|
+
return selected;
|
|
1665
1843
|
}
|
|
1666
|
-
function
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1844
|
+
function dbQueryCommand() {
|
|
1845
|
+
return new Command10("query").description("Query database entries with optional filtering and sorting").argument("<id>", "Notion database ID or URL").option(
|
|
1846
|
+
"--filter <filter>",
|
|
1847
|
+
'Filter entries (repeatable): --filter "Status=Done"',
|
|
1848
|
+
collect,
|
|
1849
|
+
[]
|
|
1850
|
+
).option(
|
|
1851
|
+
"--sort <sort>",
|
|
1852
|
+
'Sort entries (repeatable): --sort "Name:asc"',
|
|
1853
|
+
collect,
|
|
1854
|
+
[]
|
|
1855
|
+
).option(
|
|
1856
|
+
"--columns <columns>",
|
|
1857
|
+
'Comma-separated list of columns to display: --columns "Title,Status"'
|
|
1858
|
+
).option("--json", "Output raw JSON").action(
|
|
1859
|
+
withErrorHandling(
|
|
1860
|
+
async (id, options) => {
|
|
1861
|
+
const { token } = await resolveToken();
|
|
1862
|
+
const client = createNotionClient(token);
|
|
1863
|
+
const dbId = parseNotionId(id);
|
|
1864
|
+
const schema = await fetchDatabaseSchema(client, dbId);
|
|
1865
|
+
const columns = options.columns ? options.columns.split(",").map((c2) => c2.trim()) : void 0;
|
|
1866
|
+
const filter = options.filter.length ? buildFilter(options.filter, schema) : void 0;
|
|
1867
|
+
const sorts = options.sort.length ? buildSorts(options.sort) : void 0;
|
|
1868
|
+
const entries = await queryDatabase(client, dbId, {
|
|
1869
|
+
filter,
|
|
1870
|
+
sorts,
|
|
1871
|
+
columns
|
|
1872
|
+
});
|
|
1873
|
+
if (options.json) {
|
|
1874
|
+
process.stdout.write(`${formatJSON(entries.map((e) => e.raw))}
|
|
1875
|
+
`);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
if (entries.length === 0) {
|
|
1879
|
+
process.stdout.write("No entries found.\n");
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
const displayColumns = columns ?? autoSelectColumns(schema, entries);
|
|
1883
|
+
const headers = displayColumns.map((c2) => c2.toUpperCase());
|
|
1884
|
+
const rows = entries.map(
|
|
1885
|
+
(entry) => displayColumns.map((col) => entry.properties[col] ?? "")
|
|
1886
|
+
);
|
|
1887
|
+
process.stdout.write(`${formatTable(rows, headers)}
|
|
1888
|
+
`);
|
|
1889
|
+
process.stderr.write(`${entries.length} entries
|
|
1890
|
+
`);
|
|
1891
|
+
}
|
|
1892
|
+
)
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
function collect(value, previous) {
|
|
1896
|
+
return previous.concat([value]);
|
|
1670
1897
|
}
|
|
1671
1898
|
|
|
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));
|
|
1899
|
+
// src/commands/db/schema.ts
|
|
1900
|
+
import { Command as Command11 } from "commander";
|
|
1901
|
+
function dbSchemaCommand() {
|
|
1902
|
+
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(
|
|
1903
|
+
withErrorHandling(async (id, options) => {
|
|
1904
|
+
const { token } = await resolveToken();
|
|
1905
|
+
const client = createNotionClient(token);
|
|
1906
|
+
const dbId = parseNotionId(id);
|
|
1907
|
+
const schema = await fetchDatabaseSchema(client, dbId);
|
|
1908
|
+
if (options.json) {
|
|
1909
|
+
process.stdout.write(`${formatJSON(schema)}
|
|
1910
|
+
`);
|
|
1911
|
+
return;
|
|
1696
1912
|
}
|
|
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));
|
|
1913
|
+
const headers = ["PROPERTY", "TYPE", "OPTIONS"];
|
|
1914
|
+
const rows = Object.values(schema.properties).map((prop) => [
|
|
1915
|
+
prop.name,
|
|
1916
|
+
prop.type,
|
|
1917
|
+
prop.options ? prop.options.map((o) => o.name).join(", ") : ""
|
|
1918
|
+
]);
|
|
1919
|
+
process.stdout.write(`${formatTable(rows, headers)}
|
|
1920
|
+
`);
|
|
1921
|
+
})
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// src/commands/ls.ts
|
|
1926
|
+
import { isFullPageOrDataSource } from "@notionhq/client";
|
|
1927
|
+
import { Command as Command12 } from "commander";
|
|
1928
|
+
function getTitle(item) {
|
|
1929
|
+
if (item.object === "data_source") {
|
|
1930
|
+
return item.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
1931
|
+
}
|
|
1932
|
+
const titleProp = Object.values(item.properties).find(
|
|
1933
|
+
(p) => p.type === "title"
|
|
1934
|
+
);
|
|
1935
|
+
if (titleProp?.type === "title") {
|
|
1936
|
+
return titleProp.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
1937
|
+
}
|
|
1938
|
+
return "(untitled)";
|
|
1939
|
+
}
|
|
1940
|
+
function displayType(item) {
|
|
1941
|
+
return item.object === "data_source" ? "database" : item.object;
|
|
1942
|
+
}
|
|
1943
|
+
function lsCommand() {
|
|
1944
|
+
const cmd = new Command12("ls");
|
|
1945
|
+
cmd.description("list accessible Notion pages and databases").option(
|
|
1946
|
+
"--type <type>",
|
|
1947
|
+
"filter by object type (page or database)",
|
|
1948
|
+
(val) => {
|
|
1949
|
+
if (val !== "page" && val !== "database") {
|
|
1950
|
+
throw new Error('--type must be "page" or "database"');
|
|
1755
1951
|
}
|
|
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;
|
|
1952
|
+
return val;
|
|
1765
1953
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1954
|
+
).option(
|
|
1955
|
+
"--cursor <cursor>",
|
|
1956
|
+
"start from this pagination cursor (from a previous --next hint)"
|
|
1957
|
+
).option("--json", "force JSON output").action(
|
|
1958
|
+
withErrorHandling(
|
|
1959
|
+
async (opts) => {
|
|
1960
|
+
if (opts.json) {
|
|
1961
|
+
setOutputMode("json");
|
|
1962
|
+
}
|
|
1963
|
+
const { token, source } = await resolveToken();
|
|
1964
|
+
reportTokenSource(source);
|
|
1965
|
+
const notion = createNotionClient(token);
|
|
1966
|
+
const response = await notion.search({
|
|
1967
|
+
start_cursor: opts.cursor,
|
|
1968
|
+
page_size: 20
|
|
1969
|
+
});
|
|
1970
|
+
let items = response.results.filter(
|
|
1971
|
+
(r) => isFullPageOrDataSource(r)
|
|
1972
|
+
);
|
|
1973
|
+
if (opts.type) {
|
|
1974
|
+
const filterType = opts.type;
|
|
1975
|
+
items = items.filter(
|
|
1976
|
+
(r) => filterType === "database" ? r.object === "data_source" : r.object === filterType
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
if (items.length === 0) {
|
|
1980
|
+
process.stdout.write("No accessible content found\n");
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
const headers = ["TYPE", "TITLE", "ID", "MODIFIED"];
|
|
1984
|
+
const rows = items.map((item) => [
|
|
1985
|
+
displayType(item),
|
|
1986
|
+
getTitle(item),
|
|
1987
|
+
item.id,
|
|
1988
|
+
item.last_edited_time.split("T")[0]
|
|
1989
|
+
]);
|
|
1990
|
+
printOutput(items, headers, rows);
|
|
1991
|
+
if (response.has_more && response.next_cursor) {
|
|
1992
|
+
process.stderr.write(
|
|
1993
|
+
`
|
|
1994
|
+
--next: notion ls --cursor ${response.next_cursor}
|
|
1995
|
+
`
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
)
|
|
2000
|
+
);
|
|
2001
|
+
return cmd;
|
|
1769
2002
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2003
|
+
|
|
2004
|
+
// src/commands/open.ts
|
|
2005
|
+
import { exec } from "child_process";
|
|
2006
|
+
import { promisify } from "util";
|
|
2007
|
+
import { Command as Command13 } from "commander";
|
|
2008
|
+
var execAsync = promisify(exec);
|
|
2009
|
+
function openCommand() {
|
|
2010
|
+
const cmd = new Command13("open");
|
|
2011
|
+
cmd.description("open a Notion page in the default browser").argument("<id/url>", "Notion page ID or URL").action(
|
|
2012
|
+
withErrorHandling(async (idOrUrl) => {
|
|
2013
|
+
const id = parseNotionId(idOrUrl);
|
|
2014
|
+
const url = `https://www.notion.so/${id}`;
|
|
2015
|
+
const platform = process.platform;
|
|
2016
|
+
const opener = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
2017
|
+
await execAsync(`${opener} "${url}"`);
|
|
2018
|
+
process.stdout.write(`Opening ${url}
|
|
2019
|
+
`);
|
|
2020
|
+
})
|
|
2021
|
+
);
|
|
2022
|
+
return cmd;
|
|
1779
2023
|
}
|
|
1780
2024
|
|
|
1781
|
-
// src/commands/
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
const
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
2025
|
+
// src/commands/profile/list.ts
|
|
2026
|
+
import { Command as Command14 } from "commander";
|
|
2027
|
+
function profileListCommand() {
|
|
2028
|
+
const cmd = new Command14("list");
|
|
2029
|
+
cmd.description("list all authentication profiles").action(
|
|
2030
|
+
withErrorHandling(async () => {
|
|
2031
|
+
const config = await readGlobalConfig();
|
|
2032
|
+
const profiles = config.profiles ?? {};
|
|
2033
|
+
const profileNames = Object.keys(profiles);
|
|
2034
|
+
if (profileNames.length === 0) {
|
|
2035
|
+
process.stdout.write(
|
|
2036
|
+
"No profiles configured. Run `notion auth login` to get started.\n"
|
|
2037
|
+
);
|
|
2038
|
+
return;
|
|
2039
|
+
}
|
|
2040
|
+
for (const name of profileNames) {
|
|
2041
|
+
const profile = profiles[name];
|
|
2042
|
+
const isActive = config.active_profile === name;
|
|
2043
|
+
const marker = isActive ? bold("* ") : " ";
|
|
2044
|
+
const activeLabel = isActive ? " (active)" : "";
|
|
2045
|
+
const workspaceInfo = profile.workspace_name ? dim(` \u2014 ${profile.workspace_name}`) : "";
|
|
2046
|
+
process.stdout.write(
|
|
2047
|
+
`${marker}${name}${activeLabel}${workspaceInfo}
|
|
2048
|
+
`
|
|
2049
|
+
);
|
|
1798
2050
|
}
|
|
1799
2051
|
})
|
|
1800
2052
|
);
|
|
2053
|
+
return cmd;
|
|
1801
2054
|
}
|
|
1802
2055
|
|
|
1803
|
-
// src/commands/
|
|
2056
|
+
// src/commands/profile/use.ts
|
|
1804
2057
|
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;
|
|
2058
|
+
function profileUseCommand() {
|
|
2059
|
+
const cmd = new Command15("use");
|
|
2060
|
+
cmd.description("switch the active profile").argument("<name>", "profile name to activate").action(
|
|
2061
|
+
withErrorHandling(async (name) => {
|
|
2062
|
+
const config = await readGlobalConfig();
|
|
2063
|
+
const profiles = config.profiles ?? {};
|
|
2064
|
+
if (!profiles[name]) {
|
|
2065
|
+
throw new CliError(
|
|
2066
|
+
ErrorCodes.AUTH_PROFILE_NOT_FOUND,
|
|
2067
|
+
`Profile "${name}" not found.`,
|
|
2068
|
+
`Run "notion auth list" to see available profiles`
|
|
2069
|
+
);
|
|
1825
2070
|
}
|
|
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
|
|
2071
|
+
await writeGlobalConfig({
|
|
2072
|
+
...config,
|
|
2073
|
+
active_profile: name
|
|
2074
|
+
});
|
|
2075
|
+
stderrWrite(success(`Switched to profile "${name}".`));
|
|
1839
2076
|
})
|
|
1840
2077
|
);
|
|
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
|
-
});
|
|
2078
|
+
return cmd;
|
|
1849
2079
|
}
|
|
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 };
|
|
2080
|
+
|
|
2081
|
+
// src/commands/read.ts
|
|
2082
|
+
import { Command as Command16 } from "commander";
|
|
2083
|
+
|
|
2084
|
+
// src/blocks/rich-text.ts
|
|
2085
|
+
function richTextToMd(richText) {
|
|
2086
|
+
return richText.map(segmentToMd).join("");
|
|
1875
2087
|
}
|
|
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
|
-
);
|
|
2088
|
+
function segmentToMd(segment) {
|
|
2089
|
+
if (segment.type === "equation") {
|
|
2090
|
+
return `$${segment.equation.expression}$`;
|
|
2091
|
+
}
|
|
2092
|
+
if (segment.type === "mention") {
|
|
2093
|
+
const text = segment.plain_text;
|
|
2094
|
+
return segment.href ? `[${text}](${segment.href})` : text;
|
|
1901
2095
|
}
|
|
2096
|
+
const annotated = applyAnnotations(segment.text.content, segment.annotations);
|
|
2097
|
+
return segment.text.link ? `[${annotated}](${segment.text.link.url})` : annotated;
|
|
1902
2098
|
}
|
|
1903
|
-
function
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
2099
|
+
function applyAnnotations(text, annotations) {
|
|
2100
|
+
let result = text;
|
|
2101
|
+
if (annotations.code) result = `\`${result}\``;
|
|
2102
|
+
if (annotations.strikethrough) result = `~~${result}~~`;
|
|
2103
|
+
if (annotations.italic) result = `_${result}_`;
|
|
2104
|
+
if (annotations.bold) result = `**${result}**`;
|
|
2105
|
+
return result;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
// src/blocks/converters.ts
|
|
2109
|
+
function indentChildren(childrenMd) {
|
|
2110
|
+
return `${childrenMd.split("\n").filter(Boolean).map((line) => ` ${line}`).join("\n")}
|
|
2111
|
+
`;
|
|
2112
|
+
}
|
|
2113
|
+
var converters = {
|
|
2114
|
+
paragraph(block) {
|
|
2115
|
+
const b = block;
|
|
2116
|
+
return `${richTextToMd(b.paragraph.rich_text)}
|
|
2117
|
+
`;
|
|
2118
|
+
},
|
|
2119
|
+
heading_1(block) {
|
|
2120
|
+
const b = block;
|
|
2121
|
+
return `# ${richTextToMd(b.heading_1.rich_text)}
|
|
2122
|
+
`;
|
|
2123
|
+
},
|
|
2124
|
+
heading_2(block) {
|
|
2125
|
+
const b = block;
|
|
2126
|
+
return `## ${richTextToMd(b.heading_2.rich_text)}
|
|
2127
|
+
`;
|
|
2128
|
+
},
|
|
2129
|
+
heading_3(block) {
|
|
2130
|
+
const b = block;
|
|
2131
|
+
return `### ${richTextToMd(b.heading_3.rich_text)}
|
|
2132
|
+
`;
|
|
2133
|
+
},
|
|
2134
|
+
bulleted_list_item(block, ctx) {
|
|
2135
|
+
const b = block;
|
|
2136
|
+
const text = richTextToMd(b.bulleted_list_item.rich_text);
|
|
2137
|
+
const header = `- ${text}
|
|
2138
|
+
`;
|
|
2139
|
+
if (ctx?.childrenMd) {
|
|
2140
|
+
return header + indentChildren(ctx.childrenMd);
|
|
2141
|
+
}
|
|
2142
|
+
return header;
|
|
2143
|
+
},
|
|
2144
|
+
numbered_list_item(block, ctx) {
|
|
2145
|
+
const b = block;
|
|
2146
|
+
const num = ctx?.listNumber ?? 1;
|
|
2147
|
+
return `${num}. ${richTextToMd(b.numbered_list_item.rich_text)}
|
|
2148
|
+
`;
|
|
2149
|
+
},
|
|
2150
|
+
to_do(block) {
|
|
2151
|
+
const b = block;
|
|
2152
|
+
const checkbox = b.to_do.checked ? "[x]" : "[ ]";
|
|
2153
|
+
return `- ${checkbox} ${richTextToMd(b.to_do.rich_text)}
|
|
2154
|
+
`;
|
|
2155
|
+
},
|
|
2156
|
+
code(block) {
|
|
2157
|
+
const b = block;
|
|
2158
|
+
const lang = b.code.language === "plain text" ? "" : b.code.language;
|
|
2159
|
+
const content = richTextToMd(b.code.rich_text);
|
|
2160
|
+
return `\`\`\`${lang}
|
|
2161
|
+
${content}
|
|
2162
|
+
\`\`\`
|
|
2163
|
+
`;
|
|
2164
|
+
},
|
|
2165
|
+
quote(block) {
|
|
2166
|
+
const b = block;
|
|
2167
|
+
return `> ${richTextToMd(b.quote.rich_text)}
|
|
2168
|
+
`;
|
|
2169
|
+
},
|
|
2170
|
+
divider() {
|
|
2171
|
+
return "---\n";
|
|
2172
|
+
},
|
|
2173
|
+
callout(block) {
|
|
2174
|
+
const b = block;
|
|
2175
|
+
const text = richTextToMd(b.callout.rich_text);
|
|
2176
|
+
const icon = b.callout.icon;
|
|
2177
|
+
if (icon?.type === "emoji") {
|
|
2178
|
+
return `> ${icon.emoji} ${text}
|
|
2179
|
+
`;
|
|
2180
|
+
}
|
|
2181
|
+
return `> ${text}
|
|
2182
|
+
`;
|
|
2183
|
+
},
|
|
2184
|
+
toggle(block, ctx) {
|
|
2185
|
+
const b = block;
|
|
2186
|
+
const header = `**${richTextToMd(b.toggle.rich_text)}**
|
|
2187
|
+
`;
|
|
2188
|
+
if (ctx?.childrenMd) {
|
|
2189
|
+
return header + ctx.childrenMd;
|
|
2190
|
+
}
|
|
2191
|
+
return header;
|
|
2192
|
+
},
|
|
2193
|
+
image(block) {
|
|
2194
|
+
const b = block;
|
|
2195
|
+
const caption = richTextToMd(b.image.caption);
|
|
2196
|
+
if (b.image.type === "file") {
|
|
2197
|
+
const url2 = b.image.file.url;
|
|
2198
|
+
const expiry = b.image.file.expiry_time;
|
|
2199
|
+
return ` <!-- expires: ${expiry} -->
|
|
2200
|
+
`;
|
|
1908
2201
|
}
|
|
1909
|
-
const
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2202
|
+
const url = b.image.external.url;
|
|
2203
|
+
return `
|
|
2204
|
+
`;
|
|
2205
|
+
},
|
|
2206
|
+
bookmark(block) {
|
|
2207
|
+
const b = block;
|
|
2208
|
+
const caption = richTextToMd(b.bookmark.caption);
|
|
2209
|
+
const text = caption || b.bookmark.url;
|
|
2210
|
+
return `[${text}](${b.bookmark.url})
|
|
2211
|
+
`;
|
|
2212
|
+
},
|
|
2213
|
+
child_page(block) {
|
|
2214
|
+
const b = block;
|
|
2215
|
+
return `### ${b.child_page.title}
|
|
2216
|
+
`;
|
|
2217
|
+
},
|
|
2218
|
+
child_database(block) {
|
|
2219
|
+
const b = block;
|
|
2220
|
+
return `### ${b.child_database.title}
|
|
2221
|
+
`;
|
|
2222
|
+
},
|
|
2223
|
+
link_preview(block) {
|
|
2224
|
+
const b = block;
|
|
2225
|
+
return `[${b.link_preview.url}](${b.link_preview.url})
|
|
2226
|
+
`;
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2229
|
+
function blockToMd(block, ctx) {
|
|
2230
|
+
const converter = converters[block.type];
|
|
2231
|
+
if (converter) {
|
|
2232
|
+
return converter(block, ctx);
|
|
2233
|
+
}
|
|
2234
|
+
return `<!-- unsupported block: ${block.type} -->
|
|
2235
|
+
`;
|
|
1916
2236
|
}
|
|
1917
|
-
|
|
2237
|
+
|
|
2238
|
+
// src/blocks/properties.ts
|
|
2239
|
+
function formatFormula(f) {
|
|
2240
|
+
if (f.type === "string") return f.string ?? "";
|
|
2241
|
+
if (f.type === "number") return f.number !== null ? String(f.number) : "";
|
|
2242
|
+
if (f.type === "boolean") return String(f.boolean);
|
|
2243
|
+
if (f.type === "date") return f.date?.start ?? "";
|
|
2244
|
+
return "";
|
|
2245
|
+
}
|
|
2246
|
+
function formatRollup(r) {
|
|
2247
|
+
if (r.type === "number") return r.number !== null ? String(r.number) : "";
|
|
2248
|
+
if (r.type === "date") return r.date?.start ?? "";
|
|
2249
|
+
if (r.type === "array") return `[${r.array.length} items]`;
|
|
2250
|
+
return "";
|
|
2251
|
+
}
|
|
2252
|
+
function formatUser(p) {
|
|
2253
|
+
return "name" in p && p.name ? p.name : p.id;
|
|
2254
|
+
}
|
|
2255
|
+
function formatPropertyValue(_name, prop) {
|
|
1918
2256
|
switch (prop.type) {
|
|
1919
2257
|
case "title":
|
|
1920
|
-
return prop.title.map((
|
|
2258
|
+
return prop.title.map((rt) => rt.plain_text).join("");
|
|
1921
2259
|
case "rich_text":
|
|
1922
|
-
return prop.rich_text.map((
|
|
2260
|
+
return prop.rich_text.map((rt) => rt.plain_text).join("");
|
|
1923
2261
|
case "number":
|
|
1924
|
-
return prop.number !== null
|
|
2262
|
+
return prop.number !== null ? String(prop.number) : "";
|
|
1925
2263
|
case "select":
|
|
1926
2264
|
return prop.select?.name ?? "";
|
|
1927
2265
|
case "status":
|
|
@@ -1929,9 +2267,10 @@ function displayPropertyValue(prop) {
|
|
|
1929
2267
|
case "multi_select":
|
|
1930
2268
|
return prop.multi_select.map((s) => s.name).join(", ");
|
|
1931
2269
|
case "date":
|
|
1932
|
-
|
|
2270
|
+
if (!prop.date) return "";
|
|
2271
|
+
return prop.date.end ? `${prop.date.start} \u2192 ${prop.date.end}` : prop.date.start;
|
|
1933
2272
|
case "checkbox":
|
|
1934
|
-
return prop.checkbox ? "
|
|
2273
|
+
return prop.checkbox ? "true" : "false";
|
|
1935
2274
|
case "url":
|
|
1936
2275
|
return prop.url ?? "";
|
|
1937
2276
|
case "email":
|
|
@@ -1939,22 +2278,23 @@ function displayPropertyValue(prop) {
|
|
|
1939
2278
|
case "phone_number":
|
|
1940
2279
|
return prop.phone_number ?? "";
|
|
1941
2280
|
case "people":
|
|
1942
|
-
return prop.people.map(
|
|
2281
|
+
return prop.people.map(formatUser).join(", ");
|
|
1943
2282
|
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
|
-
}
|
|
2283
|
+
return prop.relation.map((r) => r.id).join(", ");
|
|
2284
|
+
case "formula":
|
|
2285
|
+
return formatFormula(prop.formula);
|
|
2286
|
+
case "rollup":
|
|
2287
|
+
return formatRollup(prop.rollup);
|
|
1954
2288
|
case "created_time":
|
|
1955
2289
|
return prop.created_time;
|
|
1956
2290
|
case "last_edited_time":
|
|
1957
2291
|
return prop.last_edited_time;
|
|
2292
|
+
case "created_by":
|
|
2293
|
+
return "name" in prop.created_by ? prop.created_by.name ?? prop.created_by.id : prop.created_by.id;
|
|
2294
|
+
case "last_edited_by":
|
|
2295
|
+
return "name" in prop.last_edited_by ? prop.last_edited_by.name ?? prop.last_edited_by.id : prop.last_edited_by.id;
|
|
2296
|
+
case "files":
|
|
2297
|
+
return prop.files.map((f) => f.type === "external" ? f.external.url : f.name).join(", ");
|
|
1958
2298
|
case "unique_id":
|
|
1959
2299
|
return prop.unique_id.prefix ? `${prop.unique_id.prefix}-${prop.unique_id.number}` : String(prop.unique_id.number ?? "");
|
|
1960
2300
|
default:
|
|
@@ -1962,375 +2302,354 @@ function displayPropertyValue(prop) {
|
|
|
1962
2302
|
}
|
|
1963
2303
|
}
|
|
1964
2304
|
|
|
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]);
|
|
2305
|
+
// src/blocks/render.ts
|
|
2306
|
+
function buildPropertiesHeader(page) {
|
|
2307
|
+
const lines = ["---"];
|
|
2308
|
+
for (const [name, prop] of Object.entries(page.properties)) {
|
|
2309
|
+
const value = formatPropertyValue(name, prop);
|
|
2310
|
+
if (value) {
|
|
2311
|
+
lines.push(`${name}: ${value}`);
|
|
2312
|
+
}
|
|
2010
2313
|
}
|
|
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;
|
|
2314
|
+
lines.push("---", "");
|
|
2315
|
+
return lines.join("\n");
|
|
2105
2316
|
}
|
|
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;
|
|
2317
|
+
function renderBlockTree(blocks) {
|
|
2318
|
+
const parts = [];
|
|
2319
|
+
let listCounter = 0;
|
|
2320
|
+
for (const node of blocks) {
|
|
2321
|
+
if (node.block.type === "numbered_list_item") {
|
|
2322
|
+
listCounter++;
|
|
2323
|
+
} else {
|
|
2324
|
+
listCounter = 0;
|
|
2144
2325
|
}
|
|
2145
|
-
const
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2326
|
+
const childrenMd = node.children.length > 0 ? renderBlockTree(node.children) : "";
|
|
2327
|
+
const md = blockToMd(node.block, {
|
|
2328
|
+
listNumber: node.block.type === "numbered_list_item" ? listCounter : void 0,
|
|
2329
|
+
childrenMd: childrenMd || void 0
|
|
2330
|
+
});
|
|
2331
|
+
parts.push(md);
|
|
2332
|
+
}
|
|
2333
|
+
return parts.join("");
|
|
2334
|
+
}
|
|
2335
|
+
function renderPageMarkdown({ page, blocks }) {
|
|
2336
|
+
const header = buildPropertiesHeader(page);
|
|
2337
|
+
const content = renderBlockTree(blocks);
|
|
2338
|
+
return header + content;
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
// src/output/markdown.ts
|
|
2342
|
+
import { Chalk as Chalk2 } from "chalk";
|
|
2343
|
+
var c = new Chalk2({ level: 3 });
|
|
2344
|
+
function handleFenceLine(line, state, out) {
|
|
2345
|
+
const fenceMatch = line.match(/^```(\w*)$/);
|
|
2346
|
+
if (fenceMatch && !state.inFence) {
|
|
2347
|
+
state.inFence = true;
|
|
2348
|
+
state.fenceLang = fenceMatch[1] ?? "";
|
|
2349
|
+
state.fenceLines = [];
|
|
2350
|
+
return true;
|
|
2351
|
+
}
|
|
2352
|
+
if (line === "```" && state.inFence) {
|
|
2353
|
+
state.inFence = false;
|
|
2354
|
+
const header = state.fenceLang ? c.dim(`[${state.fenceLang}]`) : "";
|
|
2355
|
+
if (header) out.push(header);
|
|
2356
|
+
for (const fl of state.fenceLines) {
|
|
2357
|
+
out.push(c.green(` ${fl}`));
|
|
2154
2358
|
}
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
text: { content: segment, link: null },
|
|
2158
|
-
annotations: { bold: false, italic: false, strikethrough: false, underline: false, code: false, color: "default" }
|
|
2159
|
-
});
|
|
2359
|
+
out.push("");
|
|
2360
|
+
return true;
|
|
2160
2361
|
}
|
|
2161
|
-
return
|
|
2362
|
+
return false;
|
|
2162
2363
|
}
|
|
2163
|
-
function
|
|
2164
|
-
|
|
2364
|
+
function handleHeading(line, out) {
|
|
2365
|
+
const h1 = line.match(/^# (.+)/);
|
|
2366
|
+
if (h1) {
|
|
2367
|
+
out.push(`
|
|
2368
|
+
${c.bold.cyan(h1[1])}`);
|
|
2369
|
+
return true;
|
|
2370
|
+
}
|
|
2371
|
+
const h2 = line.match(/^## (.+)/);
|
|
2372
|
+
if (h2) {
|
|
2373
|
+
out.push(`
|
|
2374
|
+
${c.bold.blue(h2[1])}`);
|
|
2375
|
+
return true;
|
|
2376
|
+
}
|
|
2377
|
+
const h3 = line.match(/^### (.+)/);
|
|
2378
|
+
if (h3) {
|
|
2379
|
+
out.push(`
|
|
2380
|
+
${c.bold(h3[1])}`);
|
|
2381
|
+
return true;
|
|
2382
|
+
}
|
|
2383
|
+
const h4 = line.match(/^#### (.+)/);
|
|
2384
|
+
if (h4) {
|
|
2385
|
+
out.push(c.bold.underline(h4[1]));
|
|
2386
|
+
return true;
|
|
2387
|
+
}
|
|
2388
|
+
return false;
|
|
2389
|
+
}
|
|
2390
|
+
function handleListLine(line, out) {
|
|
2391
|
+
const bulletMatch = line.match(/^(\s*)- (\[[ x]\] )?(.+)/);
|
|
2392
|
+
if (bulletMatch) {
|
|
2393
|
+
const indent = bulletMatch[1] ?? "";
|
|
2394
|
+
const checkbox = bulletMatch[2];
|
|
2395
|
+
const text = bulletMatch[3] ?? "";
|
|
2396
|
+
if (checkbox) {
|
|
2397
|
+
const checked = checkbox.trim() === "[x]";
|
|
2398
|
+
const box = checked ? c.green("\u2611") : c.dim("\u2610");
|
|
2399
|
+
out.push(`${indent + box} ${renderInline(text)}`);
|
|
2400
|
+
} else {
|
|
2401
|
+
out.push(`${indent + c.cyan("\u2022")} ${renderInline(text)}`);
|
|
2402
|
+
}
|
|
2403
|
+
return true;
|
|
2404
|
+
}
|
|
2405
|
+
const numMatch = line.match(/^(\s*)(\d+)\. (.+)/);
|
|
2406
|
+
if (numMatch) {
|
|
2407
|
+
const indent = numMatch[1] ?? "";
|
|
2408
|
+
const num = numMatch[2] ?? "";
|
|
2409
|
+
const text = numMatch[3] ?? "";
|
|
2410
|
+
out.push(`${indent + c.cyan(`${num}.`)} ${renderInline(text)}`);
|
|
2411
|
+
return true;
|
|
2412
|
+
}
|
|
2413
|
+
return false;
|
|
2165
2414
|
}
|
|
2166
|
-
function
|
|
2167
|
-
if (!
|
|
2415
|
+
function renderMarkdown(md) {
|
|
2416
|
+
if (!isatty()) return md;
|
|
2168
2417
|
const lines = md.split("\n");
|
|
2169
|
-
const
|
|
2170
|
-
|
|
2171
|
-
let fenceLang = "";
|
|
2172
|
-
const fenceLines = [];
|
|
2418
|
+
const out = [];
|
|
2419
|
+
const fence = { inFence: false, fenceLang: "", fenceLines: [] };
|
|
2173
2420
|
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
|
-
});
|
|
2421
|
+
if (handleFenceLine(line, fence, out)) continue;
|
|
2422
|
+
if (fence.inFence) {
|
|
2423
|
+
fence.fenceLines.push(line);
|
|
2228
2424
|
continue;
|
|
2229
2425
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
blocks.push({
|
|
2233
|
-
type: "bulleted_list_item",
|
|
2234
|
-
bulleted_list_item: { rich_text: makeRichText(bullet[1]) }
|
|
2235
|
-
});
|
|
2426
|
+
if (line === "---") {
|
|
2427
|
+
out.push(c.dim("\u2500".repeat(40)));
|
|
2236
2428
|
continue;
|
|
2237
2429
|
}
|
|
2238
|
-
|
|
2239
|
-
if (
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
numbered_list_item: { rich_text: makeRichText(numbered[1]) }
|
|
2243
|
-
});
|
|
2430
|
+
if (/^<!--.*-->$/.test(line.trim())) continue;
|
|
2431
|
+
if (handleHeading(line, out)) continue;
|
|
2432
|
+
if (line.startsWith("> ")) {
|
|
2433
|
+
out.push(c.yellow("\u258E ") + renderInline(line.slice(2)));
|
|
2244
2434
|
continue;
|
|
2245
2435
|
}
|
|
2246
|
-
const
|
|
2247
|
-
if (
|
|
2248
|
-
|
|
2249
|
-
type: "quote",
|
|
2250
|
-
quote: { rich_text: makeRichText(quote[1]) }
|
|
2251
|
-
});
|
|
2436
|
+
const propMatch = line.match(/^([A-Za-z_][A-Za-z0-9_ ]*): (.+)$/);
|
|
2437
|
+
if (propMatch) {
|
|
2438
|
+
out.push(c.dim(`${propMatch[1]}: `) + c.white(propMatch[2]));
|
|
2252
2439
|
continue;
|
|
2253
2440
|
}
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
paragraph: { rich_text: makeRichText(line) }
|
|
2257
|
-
});
|
|
2441
|
+
if (handleListLine(line, out)) continue;
|
|
2442
|
+
out.push(renderInline(line));
|
|
2258
2443
|
}
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2444
|
+
return `${out.join("\n")}
|
|
2445
|
+
`;
|
|
2446
|
+
}
|
|
2447
|
+
function renderInline(text) {
|
|
2448
|
+
const codeSpans = [];
|
|
2449
|
+
let result = text.replace(/`([^`]+)`/g, (_, code) => {
|
|
2450
|
+
codeSpans.push(c.green(code));
|
|
2451
|
+
return `\0CODE${codeSpans.length - 1}\0`;
|
|
2452
|
+
});
|
|
2453
|
+
result = result.replace(
|
|
2454
|
+
/!\[([^\]]*)\]\([^)]+\)/g,
|
|
2455
|
+
(_, alt) => alt ? c.dim(`[image: ${alt}]`) : c.dim("[image]")
|
|
2456
|
+
).replace(
|
|
2457
|
+
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
2458
|
+
(_, t, url) => c.cyan.underline(t) + c.dim(` (${url})`)
|
|
2459
|
+
).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));
|
|
2460
|
+
result = result.replace(
|
|
2461
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: sentinel chars are intentional
|
|
2462
|
+
/\x00CODE(\d+)\x00/g,
|
|
2463
|
+
(_, i) => codeSpans[Number(i)] ?? ""
|
|
2464
|
+
);
|
|
2465
|
+
return result;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
// src/services/page.service.ts
|
|
2469
|
+
import {
|
|
2470
|
+
collectPaginatedAPI,
|
|
2471
|
+
isFullBlock
|
|
2472
|
+
} from "@notionhq/client";
|
|
2473
|
+
var MAX_CONCURRENT_REQUESTS = 3;
|
|
2474
|
+
async function fetchBlockTree(client, blockId, depth, maxDepth) {
|
|
2475
|
+
if (depth >= maxDepth) return [];
|
|
2476
|
+
const rawBlocks = await collectPaginatedAPI(client.blocks.children.list, {
|
|
2477
|
+
block_id: blockId
|
|
2478
|
+
});
|
|
2479
|
+
const blocks = rawBlocks.filter(isFullBlock);
|
|
2480
|
+
const SKIP_RECURSE = /* @__PURE__ */ new Set(["child_page", "child_database"]);
|
|
2481
|
+
const nodes = [];
|
|
2482
|
+
for (let i = 0; i < blocks.length; i += MAX_CONCURRENT_REQUESTS) {
|
|
2483
|
+
const batch = blocks.slice(i, i + MAX_CONCURRENT_REQUESTS);
|
|
2484
|
+
const batchNodes = await Promise.all(
|
|
2485
|
+
batch.map(async (block) => {
|
|
2486
|
+
const children = block.has_children && !SKIP_RECURSE.has(block.type) ? await fetchBlockTree(client, block.id, depth + 1, maxDepth) : [];
|
|
2487
|
+
return { block, children };
|
|
2488
|
+
})
|
|
2489
|
+
);
|
|
2490
|
+
nodes.push(...batchNodes);
|
|
2491
|
+
}
|
|
2492
|
+
return nodes;
|
|
2493
|
+
}
|
|
2494
|
+
async function fetchPageWithBlocks(client, pageId) {
|
|
2495
|
+
const page = await client.pages.retrieve({
|
|
2496
|
+
page_id: pageId
|
|
2497
|
+
});
|
|
2498
|
+
const blocks = await fetchBlockTree(client, pageId, 0, 10);
|
|
2499
|
+
return { page, blocks };
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
// src/commands/read.ts
|
|
2503
|
+
function readCommand() {
|
|
2504
|
+
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(
|
|
2505
|
+
withErrorHandling(
|
|
2506
|
+
async (id, options) => {
|
|
2507
|
+
const { token } = await resolveToken();
|
|
2508
|
+
const client = createNotionClient(token);
|
|
2509
|
+
const pageId = parseNotionId(id);
|
|
2510
|
+
const pageWithBlocks = await fetchPageWithBlocks(client, pageId);
|
|
2511
|
+
if (options.json) {
|
|
2512
|
+
process.stdout.write(
|
|
2513
|
+
`${JSON.stringify(pageWithBlocks, null, 2)}
|
|
2514
|
+
`
|
|
2515
|
+
);
|
|
2516
|
+
} else {
|
|
2517
|
+
const markdown = renderPageMarkdown(pageWithBlocks);
|
|
2518
|
+
if (options.md || !isatty()) {
|
|
2519
|
+
process.stdout.write(markdown);
|
|
2520
|
+
} else {
|
|
2521
|
+
process.stdout.write(renderMarkdown(markdown));
|
|
2269
2522
|
}
|
|
2270
|
-
|
|
2271
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2272
|
-
language: fenceLang
|
|
2523
|
+
}
|
|
2273
2524
|
}
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
return blocks;
|
|
2525
|
+
)
|
|
2526
|
+
);
|
|
2277
2527
|
}
|
|
2278
2528
|
|
|
2279
|
-
// src/commands/
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2529
|
+
// src/commands/search.ts
|
|
2530
|
+
import { isFullPageOrDataSource as isFullPageOrDataSource2 } from "@notionhq/client";
|
|
2531
|
+
import { Command as Command17 } from "commander";
|
|
2532
|
+
function getTitle2(item) {
|
|
2533
|
+
if (item.object === "data_source") {
|
|
2534
|
+
return item.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
2535
|
+
}
|
|
2536
|
+
const titleProp = Object.values(item.properties).find(
|
|
2537
|
+
(p) => p.type === "title"
|
|
2538
|
+
);
|
|
2539
|
+
if (titleProp?.type === "title") {
|
|
2540
|
+
return titleProp.title.map((t) => t.plain_text).join("") || "(untitled)";
|
|
2541
|
+
}
|
|
2542
|
+
return "(untitled)";
|
|
2543
|
+
}
|
|
2544
|
+
function toSdkFilterValue(type) {
|
|
2545
|
+
return type === "database" ? "data_source" : "page";
|
|
2546
|
+
}
|
|
2547
|
+
function displayType2(item) {
|
|
2548
|
+
return item.object === "data_source" ? "database" : item.object;
|
|
2549
|
+
}
|
|
2550
|
+
function searchCommand() {
|
|
2551
|
+
const cmd = new Command17("search");
|
|
2552
|
+
cmd.description("search Notion workspace by keyword").argument("<query>", "search keyword").option(
|
|
2553
|
+
"--type <type>",
|
|
2554
|
+
"filter by object type (page or database)",
|
|
2555
|
+
(val) => {
|
|
2556
|
+
if (val !== "page" && val !== "database") {
|
|
2557
|
+
throw new Error('--type must be "page" or "database"');
|
|
2558
|
+
}
|
|
2559
|
+
return val;
|
|
2292
2560
|
}
|
|
2293
|
-
|
|
2294
|
-
|
|
2561
|
+
).option(
|
|
2562
|
+
"--cursor <cursor>",
|
|
2563
|
+
"start from this pagination cursor (from a previous --next hint)"
|
|
2564
|
+
).option("--json", "force JSON output").action(
|
|
2565
|
+
withErrorHandling(
|
|
2566
|
+
async (query, opts) => {
|
|
2567
|
+
if (opts.json) {
|
|
2568
|
+
setOutputMode("json");
|
|
2569
|
+
}
|
|
2570
|
+
const { token, source } = await resolveToken();
|
|
2571
|
+
reportTokenSource(source);
|
|
2572
|
+
const notion = createNotionClient(token);
|
|
2573
|
+
const response = await notion.search({
|
|
2574
|
+
query,
|
|
2575
|
+
filter: opts.type ? { property: "object", value: toSdkFilterValue(opts.type) } : void 0,
|
|
2576
|
+
start_cursor: opts.cursor,
|
|
2577
|
+
page_size: 20
|
|
2578
|
+
});
|
|
2579
|
+
const fullResults = response.results.filter(
|
|
2580
|
+
(r) => isFullPageOrDataSource2(r)
|
|
2581
|
+
);
|
|
2582
|
+
if (fullResults.length === 0) {
|
|
2583
|
+
process.stdout.write(`No results found for "${query}"
|
|
2295
2584
|
`);
|
|
2296
|
-
|
|
2585
|
+
return;
|
|
2586
|
+
}
|
|
2587
|
+
const headers = ["TYPE", "TITLE", "ID", "MODIFIED"];
|
|
2588
|
+
const rows = fullResults.map((item) => [
|
|
2589
|
+
displayType2(item),
|
|
2590
|
+
getTitle2(item),
|
|
2591
|
+
item.id,
|
|
2592
|
+
item.last_edited_time.split("T")[0]
|
|
2593
|
+
]);
|
|
2594
|
+
printOutput(fullResults, headers, rows);
|
|
2595
|
+
if (response.has_more && response.next_cursor) {
|
|
2596
|
+
process.stderr.write(
|
|
2597
|
+
`
|
|
2598
|
+
--next: notion search "${query}" --cursor ${response.next_cursor}
|
|
2599
|
+
`
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
)
|
|
2604
|
+
);
|
|
2297
2605
|
return cmd;
|
|
2298
2606
|
}
|
|
2299
2607
|
|
|
2300
|
-
// src/commands/
|
|
2301
|
-
import { Command as
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2608
|
+
// src/commands/users.ts
|
|
2609
|
+
import { Command as Command18 } from "commander";
|
|
2610
|
+
function getEmailOrWorkspace(user) {
|
|
2611
|
+
if (user.type === "person") {
|
|
2612
|
+
return user.person.email ?? "\u2014";
|
|
2306
2613
|
}
|
|
2307
|
-
|
|
2614
|
+
if (user.type === "bot") {
|
|
2615
|
+
const bot = user.bot;
|
|
2616
|
+
return "workspace_name" in bot && bot.workspace_name ? bot.workspace_name : "\u2014";
|
|
2617
|
+
}
|
|
2618
|
+
return "\u2014";
|
|
2308
2619
|
}
|
|
2309
|
-
function
|
|
2310
|
-
const cmd = new
|
|
2311
|
-
cmd.description("
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2620
|
+
function usersCommand() {
|
|
2621
|
+
const cmd = new Command18("users");
|
|
2622
|
+
cmd.description("list all users in the workspace").option("--json", "output as JSON").action(
|
|
2623
|
+
withErrorHandling(async (opts) => {
|
|
2624
|
+
if (opts.json) setOutputMode("json");
|
|
2625
|
+
const { token, source } = await resolveToken();
|
|
2626
|
+
reportTokenSource(source);
|
|
2627
|
+
const notion = createNotionClient(token);
|
|
2628
|
+
const allUsers = await paginateResults(
|
|
2629
|
+
(cursor) => notion.users.list({ start_cursor: cursor })
|
|
2630
|
+
);
|
|
2631
|
+
const users = allUsers.filter(
|
|
2632
|
+
(u) => u.name !== void 0
|
|
2633
|
+
);
|
|
2634
|
+
const rows = users.map((user) => [
|
|
2635
|
+
user.type,
|
|
2636
|
+
user.name ?? "(unnamed)",
|
|
2637
|
+
getEmailOrWorkspace(user),
|
|
2638
|
+
user.id
|
|
2639
|
+
]);
|
|
2640
|
+
printOutput(users, ["TYPE", "NAME", "EMAIL / WORKSPACE", "ID"], rows);
|
|
2641
|
+
})
|
|
2642
|
+
);
|
|
2326
2643
|
return cmd;
|
|
2327
2644
|
}
|
|
2328
2645
|
|
|
2329
2646
|
// src/cli.ts
|
|
2330
2647
|
var __filename = fileURLToPath(import.meta.url);
|
|
2331
2648
|
var __dirname = dirname(__filename);
|
|
2332
|
-
var pkg = JSON.parse(
|
|
2333
|
-
|
|
2649
|
+
var pkg = JSON.parse(
|
|
2650
|
+
readFileSync(join3(__dirname, "../package.json"), "utf-8")
|
|
2651
|
+
);
|
|
2652
|
+
var program = new Command19();
|
|
2334
2653
|
program.name("notion").description("Notion CLI \u2014 read Notion pages and databases from the terminal").version(pkg.version);
|
|
2335
2654
|
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
2655
|
program.configureOutput({
|
|
@@ -2351,17 +2670,21 @@ program.hook("preAction", (thisCommand) => {
|
|
|
2351
2670
|
setOutputMode("md");
|
|
2352
2671
|
}
|
|
2353
2672
|
});
|
|
2354
|
-
|
|
2355
|
-
|
|
2673
|
+
var authCmd = new Command19("auth").description("manage Notion authentication");
|
|
2674
|
+
authCmd.action(authDefaultAction(authCmd));
|
|
2356
2675
|
authCmd.addCommand(loginCommand());
|
|
2357
2676
|
authCmd.addCommand(logoutCommand());
|
|
2358
2677
|
authCmd.addCommand(statusCommand());
|
|
2678
|
+
authCmd.addCommand(profileListCommand());
|
|
2679
|
+
authCmd.addCommand(profileUseCommand());
|
|
2359
2680
|
program.addCommand(authCmd);
|
|
2360
|
-
|
|
2681
|
+
program.addCommand(initCommand(), { hidden: true });
|
|
2682
|
+
var profileCmd = new Command19("profile").description(
|
|
2683
|
+
"manage authentication profiles"
|
|
2684
|
+
);
|
|
2361
2685
|
profileCmd.addCommand(profileListCommand());
|
|
2362
2686
|
profileCmd.addCommand(profileUseCommand());
|
|
2363
|
-
|
|
2364
|
-
program.addCommand(profileCmd);
|
|
2687
|
+
program.addCommand(profileCmd, { hidden: true });
|
|
2365
2688
|
program.addCommand(searchCommand());
|
|
2366
2689
|
program.addCommand(lsCommand());
|
|
2367
2690
|
program.addCommand(openCommand());
|
|
@@ -2371,7 +2694,7 @@ program.addCommand(readCommand());
|
|
|
2371
2694
|
program.addCommand(commentAddCommand());
|
|
2372
2695
|
program.addCommand(appendCommand());
|
|
2373
2696
|
program.addCommand(createPageCommand());
|
|
2374
|
-
var dbCmd = new
|
|
2697
|
+
var dbCmd = new Command19("db").description("Database operations");
|
|
2375
2698
|
dbCmd.addCommand(dbSchemaCommand());
|
|
2376
2699
|
dbCmd.addCommand(dbQueryCommand());
|
|
2377
2700
|
program.addCommand(dbCmd);
|