@jordancoin/notioncli 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +74 -0
- package/README.md +12 -2
- package/TECHNICAL.md +63 -3
- package/bin/notion.js +18 -1676
- package/commands/blocks.js +142 -0
- package/commands/comments.js +59 -0
- package/commands/config.js +328 -0
- package/commands/crud.js +196 -0
- package/commands/database.js +241 -0
- package/commands/import-export.js +162 -0
- package/commands/pages.js +203 -0
- package/commands/query.js +73 -0
- package/commands/search.js +45 -0
- package/commands/upload.js +84 -0
- package/commands/users.js +73 -0
- package/lib/context.js +359 -0
- package/lib/filters.js +55 -11
- package/lib/format.js +35 -2
- package/lib/helpers.js +4 -0
- package/lib/markdown.js +179 -38
- package/lib/paginate.js +74 -0
- package/lib/retry.js +76 -0
- package/package.json +1 -1
- package/skill/SKILL.md +7 -0
- package/skill/marketplace.json +2 -2
- package/test/unit.test.js +270 -0
package/bin/notion.js
CHANGED
|
@@ -1,1686 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
|
-
const {
|
|
5
|
-
const fs = require('fs');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const helpers = require('../lib/helpers');
|
|
8
|
-
|
|
9
|
-
// ─── Config file system ────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
const { CONFIG_DIR, CONFIG_PATH } = helpers.getConfigPaths();
|
|
12
|
-
|
|
13
|
-
function loadConfig() {
|
|
14
|
-
return helpers.loadConfig(CONFIG_PATH);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function saveConfig(config) {
|
|
18
|
-
helpers.saveConfig(config, CONFIG_DIR, CONFIG_PATH);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Get the active workspace name from --workspace flag or config.
|
|
23
|
-
*/
|
|
24
|
-
function getWorkspaceName() {
|
|
25
|
-
return program.opts().workspace || undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Get the active workspace config { apiKey, aliases, name }.
|
|
30
|
-
*/
|
|
31
|
-
function getWorkspaceConfig() {
|
|
32
|
-
const config = loadConfig();
|
|
33
|
-
const ws = helpers.resolveWorkspace(config, getWorkspaceName());
|
|
34
|
-
if (ws.error) {
|
|
35
|
-
console.error(`Error: ${ws.error}`);
|
|
36
|
-
if (ws.available && ws.available.length > 0) {
|
|
37
|
-
console.error(`Available workspaces: ${ws.available.join(', ')}`);
|
|
38
|
-
}
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
return ws;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Resolve API key: env var → workspace config → error with setup instructions
|
|
46
|
-
*/
|
|
47
|
-
function getApiKey() {
|
|
48
|
-
if (process.env.NOTION_API_KEY) return process.env.NOTION_API_KEY;
|
|
49
|
-
const ws = getWorkspaceConfig();
|
|
50
|
-
if (ws.apiKey) return ws.apiKey;
|
|
51
|
-
console.error('Error: No Notion API key found.');
|
|
52
|
-
console.error('');
|
|
53
|
-
console.error('Set it up with one of:');
|
|
54
|
-
console.error(' 1. notion init --key ntn_your_api_key');
|
|
55
|
-
console.error(' 2. export NOTION_API_KEY=ntn_your_api_key');
|
|
56
|
-
console.error('');
|
|
57
|
-
console.error('Get a key at: https://www.notion.so/profile/integrations');
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Resolve a user-given alias or UUID to { database_id, data_source_id }.
|
|
63
|
-
* If given a raw UUID, we use it for both IDs (the SDK figures it out).
|
|
64
|
-
*/
|
|
65
|
-
function resolveDb(aliasOrId) {
|
|
66
|
-
const ws = getWorkspaceConfig();
|
|
67
|
-
if (ws.aliases && ws.aliases[aliasOrId]) {
|
|
68
|
-
return ws.aliases[aliasOrId];
|
|
69
|
-
}
|
|
70
|
-
if (UUID_REGEX.test(aliasOrId)) {
|
|
71
|
-
return { database_id: aliasOrId, data_source_id: aliasOrId };
|
|
72
|
-
}
|
|
73
|
-
const aliasNames = ws.aliases ? Object.keys(ws.aliases) : [];
|
|
74
|
-
console.error(`Unknown database alias: "${aliasOrId}"`);
|
|
75
|
-
if (aliasNames.length > 0) {
|
|
76
|
-
console.error(`Available aliases: ${aliasNames.join(', ')}`);
|
|
77
|
-
} else {
|
|
78
|
-
console.error('No aliases configured. Add one with: notion alias add <name> <database-id>');
|
|
79
|
-
}
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Resolve alias + filter → page ID, or pass through a raw UUID.
|
|
85
|
-
* Used by update, delete, get, blocks, comments, comment, append.
|
|
86
|
-
*
|
|
87
|
-
* Returns { pageId, dbIds } where dbIds is non-null when resolved via alias.
|
|
88
|
-
*/
|
|
89
|
-
async function resolvePageId(aliasOrId, filterInput) {
|
|
90
|
-
// Normalize filter: accept string or array, extract first non-empty
|
|
91
|
-
const filterStr = Array.isArray(filterInput)
|
|
92
|
-
? (filterInput.length > 0 ? filterInput : null)
|
|
93
|
-
: filterInput;
|
|
94
|
-
const ws = getWorkspaceConfig();
|
|
95
|
-
if (ws.aliases && ws.aliases[aliasOrId]) {
|
|
96
|
-
if (!filterStr || (Array.isArray(filterStr) && filterStr.length === 0)) {
|
|
97
|
-
console.error('When using an alias, --filter is required to identify a specific page.');
|
|
98
|
-
console.error(`Example: notion update ${aliasOrId} --filter "Name=My Page" --prop "Status=Done"`);
|
|
99
|
-
process.exit(1);
|
|
100
|
-
}
|
|
101
|
-
const dbIds = ws.aliases[aliasOrId];
|
|
102
|
-
const notion = getNotion();
|
|
103
|
-
const filter = await buildFilter(dbIds, filterStr);
|
|
104
|
-
const res = await notion.dataSources.query({
|
|
105
|
-
data_source_id: dbIds.data_source_id,
|
|
106
|
-
filter,
|
|
107
|
-
page_size: 5,
|
|
108
|
-
});
|
|
109
|
-
if (res.results.length === 0) {
|
|
110
|
-
console.error('No matching page found.');
|
|
111
|
-
process.exit(1);
|
|
112
|
-
}
|
|
113
|
-
if (res.results.length > 1) {
|
|
114
|
-
console.error(`Multiple pages match (${res.results.length}). Use a more specific filter or pass a page ID directly.`);
|
|
115
|
-
const rows = pagesToRows(res.results);
|
|
116
|
-
const cols = Object.keys(rows[0]).slice(0, 4);
|
|
117
|
-
printTable(rows, cols);
|
|
118
|
-
process.exit(1);
|
|
119
|
-
}
|
|
120
|
-
return { pageId: res.results[0].id, dbIds };
|
|
121
|
-
}
|
|
122
|
-
// Check if it looks like a UUID — if not, it's probably a typo'd alias
|
|
123
|
-
if (!UUID_REGEX.test(aliasOrId)) {
|
|
124
|
-
const aliasNames = ws.aliases ? Object.keys(ws.aliases) : [];
|
|
125
|
-
console.error(`Unknown alias: "${aliasOrId}"`);
|
|
126
|
-
if (aliasNames.length > 0) {
|
|
127
|
-
console.error(`Available aliases: ${aliasNames.join(', ')}`);
|
|
128
|
-
} else {
|
|
129
|
-
console.error('No aliases configured. Run: notion init --key <your-api-key>');
|
|
130
|
-
}
|
|
131
|
-
process.exit(1);
|
|
132
|
-
}
|
|
133
|
-
// Treat as raw page ID
|
|
134
|
-
return { pageId: aliasOrId, dbIds: null };
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ─── Lazy Notion client ────────────────────────────────────────────────────────
|
|
138
|
-
|
|
139
|
-
let _notion = null;
|
|
140
|
-
function getNotion() {
|
|
141
|
-
if (!_notion) {
|
|
142
|
-
_notion = new Client({ auth: getApiKey() });
|
|
143
|
-
}
|
|
144
|
-
return _notion;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// ─── Helpers (imported from lib/helpers.js) ────────────────────────────────────
|
|
148
|
-
|
|
149
|
-
const {
|
|
150
|
-
richTextToPlain,
|
|
151
|
-
propValue,
|
|
152
|
-
buildPropValue,
|
|
153
|
-
printTable,
|
|
154
|
-
pagesToRows,
|
|
155
|
-
formatCsv,
|
|
156
|
-
formatYaml,
|
|
157
|
-
outputFormatted,
|
|
158
|
-
buildFilterFromSchema,
|
|
159
|
-
buildCompoundFilter,
|
|
160
|
-
markdownToBlocks,
|
|
161
|
-
blocksToMarkdown,
|
|
162
|
-
parseCsv,
|
|
163
|
-
kebabToProperty,
|
|
164
|
-
extractDynamicProps,
|
|
165
|
-
UUID_REGEX,
|
|
166
|
-
} = helpers;
|
|
167
|
-
|
|
168
|
-
/** Check if --json flag is set anywhere in the command chain */
|
|
169
|
-
function getGlobalJson(cmd) {
|
|
170
|
-
let c = cmd;
|
|
171
|
-
while (c) {
|
|
172
|
-
if (c.opts().json) return true;
|
|
173
|
-
c = c.parent;
|
|
174
|
-
}
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Wrap a command action with standard error handling.
|
|
180
|
-
* Reduces try/catch boilerplate across all commands.
|
|
181
|
-
*/
|
|
182
|
-
async function runCommand(name, fn) {
|
|
183
|
-
try {
|
|
184
|
-
await fn();
|
|
185
|
-
} catch (err) {
|
|
186
|
-
console.error(`${name} failed:`, err.message);
|
|
187
|
-
process.exit(1);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* If --json flag is set, output raw JSON and return true. Otherwise return false.
|
|
193
|
-
* Use: if (jsonOutput(cmd, result)) return;
|
|
194
|
-
*/
|
|
195
|
-
function jsonOutput(cmd, result) {
|
|
196
|
-
if (getGlobalJson(cmd)) {
|
|
197
|
-
console.log(JSON.stringify(result, null, 2));
|
|
198
|
-
return true;
|
|
199
|
-
}
|
|
200
|
-
return false;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Fetch data source schema — returns map of lowercase_name → { type, name }
|
|
205
|
-
* Uses dataSources.retrieve() which accepts the data_source_id.
|
|
206
|
-
*/
|
|
207
|
-
async function getDbSchema(dbIds) {
|
|
208
|
-
const notion = getNotion();
|
|
209
|
-
const dsId = dbIds.data_source_id;
|
|
210
|
-
const ds = await notion.dataSources.retrieve({ data_source_id: dsId });
|
|
211
|
-
const schema = {};
|
|
212
|
-
for (const [name, prop] of Object.entries(ds.properties)) {
|
|
213
|
-
schema[name.toLowerCase()] = { type: prop.type, name };
|
|
214
|
-
}
|
|
215
|
-
return schema;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** Build properties object from --prop key=value pairs using schema */
|
|
219
|
-
async function buildProperties(dbIds, props) {
|
|
220
|
-
const schema = await getDbSchema(dbIds);
|
|
221
|
-
const properties = {};
|
|
222
|
-
|
|
223
|
-
for (const kv of props) {
|
|
224
|
-
const eqIdx = kv.indexOf('=');
|
|
225
|
-
if (eqIdx === -1) {
|
|
226
|
-
console.error(`Invalid property format: ${kv} (expected key=value)`);
|
|
227
|
-
process.exit(1);
|
|
228
|
-
}
|
|
229
|
-
const key = kv.slice(0, eqIdx);
|
|
230
|
-
const value = kv.slice(eqIdx + 1);
|
|
231
|
-
|
|
232
|
-
const schemaEntry = schema[key.toLowerCase()];
|
|
233
|
-
if (!schemaEntry) {
|
|
234
|
-
console.error(`Property "${key}" not found in database schema.`);
|
|
235
|
-
console.error(`Available: ${Object.values(schema).map(s => s.name).join(', ')}`);
|
|
236
|
-
process.exit(1);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
properties[schemaEntry.name] = buildPropValue(schemaEntry.type, value);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return properties;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/** Parse filter string(s) into a Notion filter object. Accepts string or array. */
|
|
246
|
-
async function buildFilter(dbIds, filterInput) {
|
|
247
|
-
const schema = await getDbSchema(dbIds);
|
|
248
|
-
const filters = Array.isArray(filterInput) ? filterInput : [filterInput];
|
|
249
|
-
const result = buildCompoundFilter(schema, filters);
|
|
250
|
-
if (result.error) {
|
|
251
|
-
console.error(result.error);
|
|
252
|
-
if (result.available) {
|
|
253
|
-
console.error(`Available: ${result.available.join(', ')}`);
|
|
254
|
-
}
|
|
255
|
-
process.exit(1);
|
|
256
|
-
}
|
|
257
|
-
return result.filter;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ─── Commands ──────────────────────────────────────────────────────────────────
|
|
4
|
+
const { createContext } = require('../lib/context');
|
|
261
5
|
|
|
262
6
|
program
|
|
263
7
|
.name('notion')
|
|
264
8
|
.description('A powerful CLI for the Notion API — query databases, manage pages, and automate your workspace from the terminal.')
|
|
265
|
-
.version('1.3.
|
|
9
|
+
.version('1.3.1')
|
|
266
10
|
.option('--json', 'Output raw JSON instead of formatted tables')
|
|
267
11
|
.option('-w, --workspace <name>', 'Use a specific workspace profile');
|
|
268
12
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
console.error(' 3. Copy the API key (starts with ntn_)');
|
|
286
|
-
console.error(' 4. Share your databases with the integration');
|
|
287
|
-
console.error('');
|
|
288
|
-
console.error('Then run: notion init --key ntn_your_api_key');
|
|
289
|
-
console.error(' Or with workspace: notion init --workspace work --key ntn_your_api_key');
|
|
290
|
-
process.exit(1);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
if (!config.workspaces) config.workspaces = {};
|
|
294
|
-
if (!config.workspaces[wsName]) config.workspaces[wsName] = { aliases: {} };
|
|
295
|
-
config.workspaces[wsName].apiKey = apiKey;
|
|
296
|
-
config.activeWorkspace = wsName;
|
|
297
|
-
saveConfig(config);
|
|
298
|
-
console.log(`✅ API key saved to workspace "${wsName}" in ${CONFIG_PATH}`);
|
|
299
|
-
console.log('');
|
|
300
|
-
|
|
301
|
-
// Discover databases
|
|
302
|
-
const notion = new Client({ auth: apiKey });
|
|
303
|
-
try {
|
|
304
|
-
const res = await notion.search({
|
|
305
|
-
filter: { value: 'data_source', property: 'object' },
|
|
306
|
-
page_size: 100,
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
if (res.results.length === 0) {
|
|
310
|
-
console.log('No databases found. Make sure you\'ve shared databases with your integration.');
|
|
311
|
-
console.log('In Notion: open a database → ••• menu → Connections → Add your integration');
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const aliases = config.workspaces[wsName].aliases || {};
|
|
316
|
-
|
|
317
|
-
console.log(`Found ${res.results.length} database${res.results.length !== 1 ? 's' : ''}:\n`);
|
|
318
|
-
|
|
319
|
-
const added = [];
|
|
320
|
-
for (const db of res.results) {
|
|
321
|
-
const title = richTextToPlain(db.title) || '';
|
|
322
|
-
const dsId = db.id;
|
|
323
|
-
const dbId = (db.parent && db.parent.type === 'database_id' && db.parent.database_id) || db.database_id || dsId;
|
|
324
|
-
|
|
325
|
-
// Auto-generate a slug from the title
|
|
326
|
-
let slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 30);
|
|
327
|
-
if (!slug) slug = `db-${dsId.slice(0, 8)}`;
|
|
328
|
-
|
|
329
|
-
// Avoid collisions — append a number if needed
|
|
330
|
-
let finalSlug = slug;
|
|
331
|
-
let counter = 2;
|
|
332
|
-
while (aliases[finalSlug] && aliases[finalSlug].data_source_id !== dsId) {
|
|
333
|
-
finalSlug = `${slug}-${counter}`;
|
|
334
|
-
counter++;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
aliases[finalSlug] = {
|
|
338
|
-
database_id: dbId,
|
|
339
|
-
data_source_id: dsId,
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
console.log(` ✅ ${finalSlug.padEnd(25)} → ${title || '(untitled)'}`);
|
|
343
|
-
added.push(finalSlug);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
config.workspaces[wsName].aliases = aliases;
|
|
347
|
-
saveConfig(config);
|
|
348
|
-
console.log('');
|
|
349
|
-
console.log(`${added.length} alias${added.length !== 1 ? 'es' : ''} saved to workspace "${wsName}".`);
|
|
350
|
-
console.log('');
|
|
351
|
-
console.log('Ready! Try:');
|
|
352
|
-
if (added.length > 0) {
|
|
353
|
-
console.log(` notion query ${added[0]}`);
|
|
354
|
-
console.log(` notion add ${added[0]} --prop "Name=Hello World"`);
|
|
355
|
-
}
|
|
356
|
-
console.log('');
|
|
357
|
-
console.log('Manage aliases:');
|
|
358
|
-
console.log(' notion alias list — see all aliases');
|
|
359
|
-
console.log(' notion alias rename <old> <new> — rename an alias');
|
|
360
|
-
console.log(' notion alias remove <name> — remove an alias');
|
|
361
|
-
} catch (err) {
|
|
362
|
-
console.error(`Failed to discover databases: ${err.message}`);
|
|
363
|
-
console.error('Your API key was saved. You can add databases manually with: notion alias add <name> <id>');
|
|
364
|
-
}
|
|
365
|
-
}));
|
|
366
|
-
|
|
367
|
-
// ─── alias ─────────────────────────────────────────────────────────────────────
|
|
368
|
-
const alias = program
|
|
369
|
-
.command('alias')
|
|
370
|
-
.description('Manage database aliases for quick access');
|
|
371
|
-
|
|
372
|
-
alias
|
|
373
|
-
.command('add <name> <database-id>')
|
|
374
|
-
.description('Add a database alias (auto-discovers data_source_id)')
|
|
375
|
-
.action(async (name, databaseId) => runCommand('Alias add', async () => {
|
|
376
|
-
const config = loadConfig();
|
|
377
|
-
const wsName = getWorkspaceName() || config.activeWorkspace || 'default';
|
|
378
|
-
if (!config.workspaces[wsName]) config.workspaces[wsName] = { aliases: {} };
|
|
379
|
-
const aliases = config.workspaces[wsName].aliases || {};
|
|
380
|
-
|
|
381
|
-
// Try to discover the data_source_id by searching for this database
|
|
382
|
-
const notion = getNotion();
|
|
383
|
-
let dataSourceId = databaseId;
|
|
384
|
-
|
|
385
|
-
try {
|
|
386
|
-
const res = await notion.search({
|
|
387
|
-
filter: { value: 'data_source', property: 'object' },
|
|
388
|
-
page_size: 100,
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
const match = res.results.find(db => {
|
|
392
|
-
// Match by data_source_id or database_id
|
|
393
|
-
return db.id === databaseId ||
|
|
394
|
-
db.id.replace(/-/g, '') === databaseId.replace(/-/g, '');
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
if (match) {
|
|
398
|
-
dataSourceId = match.id;
|
|
399
|
-
// The database_id might differ from data_source_id — check parent
|
|
400
|
-
const dbId = (match.parent && match.parent.type === 'database_id' && match.parent.database_id) || match.database_id || databaseId;
|
|
401
|
-
aliases[name] = {
|
|
402
|
-
database_id: dbId,
|
|
403
|
-
data_source_id: dataSourceId,
|
|
404
|
-
};
|
|
405
|
-
const title = richTextToPlain(match.title) || '(untitled)';
|
|
406
|
-
console.log(`✅ Added alias "${name}" → ${title}`);
|
|
407
|
-
console.log(` database_id: ${dbId}`);
|
|
408
|
-
console.log(` data_source_id: ${dataSourceId}`);
|
|
409
|
-
} else {
|
|
410
|
-
// Couldn't find via search — use the ID for both
|
|
411
|
-
aliases[name] = {
|
|
412
|
-
database_id: databaseId,
|
|
413
|
-
data_source_id: databaseId,
|
|
414
|
-
};
|
|
415
|
-
console.log(`✅ Added alias "${name}" → ${databaseId}`);
|
|
416
|
-
console.log(' (Could not auto-discover data_source_id — using same ID for both)');
|
|
417
|
-
}
|
|
418
|
-
} catch (err) {
|
|
419
|
-
// Fallback: use same ID for both
|
|
420
|
-
aliases[name] = {
|
|
421
|
-
database_id: databaseId,
|
|
422
|
-
data_source_id: databaseId,
|
|
423
|
-
};
|
|
424
|
-
console.log(`✅ Added alias "${name}" → ${databaseId}`);
|
|
425
|
-
console.log(` (Auto-discovery failed: ${err.message})`);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
config.workspaces[wsName].aliases = aliases;
|
|
429
|
-
saveConfig(config);
|
|
430
|
-
}));
|
|
431
|
-
|
|
432
|
-
alias
|
|
433
|
-
.command('list')
|
|
434
|
-
.description('Show all configured database aliases')
|
|
435
|
-
.action(() => {
|
|
436
|
-
const ws = getWorkspaceConfig();
|
|
437
|
-
const aliases = ws.aliases || {};
|
|
438
|
-
const names = Object.keys(aliases);
|
|
439
|
-
|
|
440
|
-
if (names.length === 0) {
|
|
441
|
-
console.log(`No aliases in workspace "${ws.name}".`);
|
|
442
|
-
console.log('Add one with: notion alias add <name> <database-id>');
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
console.log(`Workspace: ${ws.name}\n`);
|
|
447
|
-
const rows = names.map(name => ({
|
|
448
|
-
alias: name,
|
|
449
|
-
database_id: aliases[name].database_id,
|
|
450
|
-
data_source_id: aliases[name].data_source_id,
|
|
451
|
-
}));
|
|
452
|
-
printTable(rows, ['alias', 'database_id', 'data_source_id']);
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
alias
|
|
456
|
-
.command('remove <name>')
|
|
457
|
-
.description('Remove a database alias')
|
|
458
|
-
.action((name) => {
|
|
459
|
-
const config = loadConfig();
|
|
460
|
-
const wsName = getWorkspaceName() || config.activeWorkspace || 'default';
|
|
461
|
-
const aliases = config.workspaces[wsName]?.aliases || {};
|
|
462
|
-
if (!aliases[name]) {
|
|
463
|
-
console.error(`Alias "${name}" not found in workspace "${wsName}".`);
|
|
464
|
-
const names = Object.keys(aliases);
|
|
465
|
-
if (names.length > 0) {
|
|
466
|
-
console.error(`Available: ${names.join(', ')}`);
|
|
467
|
-
}
|
|
468
|
-
process.exit(1);
|
|
469
|
-
}
|
|
470
|
-
delete aliases[name];
|
|
471
|
-
config.workspaces[wsName].aliases = aliases;
|
|
472
|
-
saveConfig(config);
|
|
473
|
-
console.log(`✅ Removed alias "${name}" from workspace "${wsName}"`);
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
alias
|
|
477
|
-
.command('rename <old-name> <new-name>')
|
|
478
|
-
.description('Rename a database alias')
|
|
479
|
-
.action((oldName, newName) => {
|
|
480
|
-
const config = loadConfig();
|
|
481
|
-
const wsName = getWorkspaceName() || config.activeWorkspace || 'default';
|
|
482
|
-
const aliases = config.workspaces[wsName]?.aliases || {};
|
|
483
|
-
if (!aliases[oldName]) {
|
|
484
|
-
console.error(`Alias "${oldName}" not found in workspace "${wsName}".`);
|
|
485
|
-
const names = Object.keys(aliases);
|
|
486
|
-
if (names.length > 0) {
|
|
487
|
-
console.error(`Available: ${names.join(', ')}`);
|
|
488
|
-
}
|
|
489
|
-
process.exit(1);
|
|
490
|
-
}
|
|
491
|
-
if (aliases[newName]) {
|
|
492
|
-
console.error(`Alias "${newName}" already exists. Remove it first or pick a different name.`);
|
|
493
|
-
process.exit(1);
|
|
494
|
-
}
|
|
495
|
-
aliases[newName] = aliases[oldName];
|
|
496
|
-
delete aliases[oldName];
|
|
497
|
-
config.workspaces[wsName].aliases = aliases;
|
|
498
|
-
saveConfig(config);
|
|
499
|
-
console.log(`✅ Renamed "${oldName}" → "${newName}" in workspace "${wsName}"`);
|
|
500
|
-
});
|
|
501
|
-
|
|
502
|
-
// ─── workspace ─────────────────────────────────────────────────────────────────
|
|
503
|
-
const workspace = program
|
|
504
|
-
.command('workspace')
|
|
505
|
-
.description('Manage workspace profiles (multiple Notion accounts)');
|
|
506
|
-
|
|
507
|
-
workspace
|
|
508
|
-
.command('add <name>')
|
|
509
|
-
.description('Add a new workspace profile')
|
|
510
|
-
.requiredOption('--key <api-key>', 'Notion API key for this workspace')
|
|
511
|
-
.action(async (name, opts) => runCommand('Workspace add', async () => {
|
|
512
|
-
const config = loadConfig();
|
|
513
|
-
if (config.workspaces[name]) {
|
|
514
|
-
console.error(`Workspace "${name}" already exists. Use "notion init --workspace ${name} --key ..." to update it.`);
|
|
515
|
-
process.exit(1);
|
|
516
|
-
}
|
|
517
|
-
config.workspaces[name] = { apiKey: opts.key, aliases: {} };
|
|
518
|
-
saveConfig(config);
|
|
519
|
-
console.log(`✅ Added workspace "${name}"`);
|
|
520
|
-
console.log('');
|
|
521
|
-
console.log(`Discover databases: notion init --workspace ${name}`);
|
|
522
|
-
console.log(`Or set as active: notion workspace use ${name}`);
|
|
523
|
-
}));
|
|
524
|
-
|
|
525
|
-
workspace
|
|
526
|
-
.command('list')
|
|
527
|
-
.description('List all workspace profiles')
|
|
528
|
-
.action(() => {
|
|
529
|
-
const config = loadConfig();
|
|
530
|
-
const names = Object.keys(config.workspaces || {});
|
|
531
|
-
if (names.length === 0) {
|
|
532
|
-
console.log('No workspaces configured. Run: notion init --key ntn_...');
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
for (const name of names) {
|
|
536
|
-
const ws = config.workspaces[name];
|
|
537
|
-
const active = name === config.activeWorkspace ? ' ← active' : '';
|
|
538
|
-
const aliasCount = Object.keys(ws.aliases || {}).length;
|
|
539
|
-
const keyPreview = ws.apiKey ? `${ws.apiKey.slice(0, 8)}...` : '(no key)';
|
|
540
|
-
console.log(` ${name}${active}`);
|
|
541
|
-
console.log(` Key: ${keyPreview} | Aliases: ${aliasCount}`);
|
|
542
|
-
}
|
|
543
|
-
});
|
|
544
|
-
|
|
545
|
-
workspace
|
|
546
|
-
.command('use <name>')
|
|
547
|
-
.description('Set the active workspace')
|
|
548
|
-
.action((name) => {
|
|
549
|
-
const config = loadConfig();
|
|
550
|
-
if (!config.workspaces[name]) {
|
|
551
|
-
console.error(`Workspace "${name}" not found.`);
|
|
552
|
-
const names = Object.keys(config.workspaces || {});
|
|
553
|
-
if (names.length > 0) {
|
|
554
|
-
console.error(`Available: ${names.join(', ')}`);
|
|
555
|
-
}
|
|
556
|
-
process.exit(1);
|
|
557
|
-
}
|
|
558
|
-
config.activeWorkspace = name;
|
|
559
|
-
saveConfig(config);
|
|
560
|
-
console.log(`✅ Active workspace: ${name}`);
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
workspace
|
|
564
|
-
.command('remove <name>')
|
|
565
|
-
.description('Remove a workspace profile')
|
|
566
|
-
.action((name) => {
|
|
567
|
-
const config = loadConfig();
|
|
568
|
-
if (!config.workspaces[name]) {
|
|
569
|
-
console.error(`Workspace "${name}" not found.`);
|
|
570
|
-
process.exit(1);
|
|
571
|
-
}
|
|
572
|
-
if (name === config.activeWorkspace) {
|
|
573
|
-
console.error(`Cannot remove the active workspace. Switch first: notion workspace use <other>`);
|
|
574
|
-
process.exit(1);
|
|
575
|
-
}
|
|
576
|
-
delete config.workspaces[name];
|
|
577
|
-
saveConfig(config);
|
|
578
|
-
console.log(`✅ Removed workspace "${name}"`);
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
// ─── search ────────────────────────────────────────────────────────────────────
|
|
582
|
-
program
|
|
583
|
-
.command('search <query>')
|
|
584
|
-
.description('Search across all pages and databases shared with your integration')
|
|
585
|
-
.action(async (query, opts, cmd) => runCommand('Search', async () => {
|
|
586
|
-
const notion = getNotion();
|
|
587
|
-
const res = await notion.search({ query, page_size: 20 });
|
|
588
|
-
if (jsonOutput(cmd, res)) return;
|
|
589
|
-
const rows = res.results.map(r => {
|
|
590
|
-
let title = '';
|
|
591
|
-
if (r.object === 'data_source' || r.object === 'database') {
|
|
592
|
-
title = richTextToPlain(r.title);
|
|
593
|
-
} else if (r.properties) {
|
|
594
|
-
for (const [, prop] of Object.entries(r.properties)) {
|
|
595
|
-
if (prop.type === 'title') {
|
|
596
|
-
title = propValue(prop);
|
|
597
|
-
break;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
return {
|
|
602
|
-
id: r.id,
|
|
603
|
-
type: r.object,
|
|
604
|
-
title: title || '(untitled)',
|
|
605
|
-
url: r.url || '',
|
|
606
|
-
};
|
|
607
|
-
});
|
|
608
|
-
printTable(rows, ['id', 'type', 'title', 'url']);
|
|
609
|
-
}));
|
|
610
|
-
|
|
611
|
-
// ─── query ─────────────────────────────────────────────────────────────────────
|
|
612
|
-
program
|
|
613
|
-
.command('query <database>')
|
|
614
|
-
.description('Query a database by alias or ID (e.g. notion query projects --filter Status=Active)')
|
|
615
|
-
.option('--filter <key=value...>', 'Filter by property — repeatable, supports operators: =, !=, >, <, >=, <= (e.g. --filter Status=Active --filter Day>5)', (v, prev) => prev.concat([v]), [])
|
|
616
|
-
.option('--sort <key:direction>', 'Sort by property (e.g. Date:desc)')
|
|
617
|
-
.option('--limit <n>', 'Max results (default: 100, max: 100)', '100')
|
|
618
|
-
.option('--output <format>', 'Output format: table, csv, json, yaml (default: table)')
|
|
619
|
-
.action(async (db, opts, cmd) => runCommand('Query', async () => {
|
|
620
|
-
const notion = getNotion();
|
|
621
|
-
const dbIds = resolveDb(db);
|
|
622
|
-
const params = {
|
|
623
|
-
data_source_id: dbIds.data_source_id,
|
|
624
|
-
page_size: Math.min(parseInt(opts.limit), 100),
|
|
625
|
-
};
|
|
626
|
-
|
|
627
|
-
if (opts.filter && opts.filter.length > 0) {
|
|
628
|
-
params.filter = await buildFilter(dbIds, opts.filter);
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
if (opts.sort) {
|
|
632
|
-
const [key, dir] = opts.sort.split(':');
|
|
633
|
-
const schema = await getDbSchema(dbIds);
|
|
634
|
-
const entry = schema[key.toLowerCase()];
|
|
635
|
-
if (!entry) {
|
|
636
|
-
console.error(`Sort property "${key}" not found.`);
|
|
637
|
-
console.error(`Available: ${Object.values(schema).map(s => s.name).join(', ')}`);
|
|
638
|
-
process.exit(1);
|
|
639
|
-
}
|
|
640
|
-
params.sorts = [{ property: entry.name, direction: dir === 'desc' ? 'descending' : 'ascending' }];
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const res = await notion.dataSources.query(params);
|
|
644
|
-
|
|
645
|
-
// Determine output format: --output takes precedence, --json is shorthand
|
|
646
|
-
const format = opts.output || (getGlobalJson(cmd) ? 'json' : 'table');
|
|
647
|
-
|
|
648
|
-
if (format === 'json') {
|
|
649
|
-
console.log(JSON.stringify(res, null, 2));
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
const rows = pagesToRows(res.results);
|
|
654
|
-
if (rows.length === 0) {
|
|
655
|
-
console.log('(no results)');
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
const columns = Object.keys(rows[0]);
|
|
659
|
-
outputFormatted(rows, columns, format);
|
|
660
|
-
}));
|
|
661
|
-
|
|
662
|
-
// ─── add ───────────────────────────────────────────────────────────────────────
|
|
663
|
-
program
|
|
664
|
-
.command('add <database>')
|
|
665
|
-
.description('Add a new page to a database (e.g. notion add tasks --name "Ship it" --status "Done")')
|
|
666
|
-
.option('--prop <key=value...>', 'Property value — repeatable (e.g. --prop "Name=Hello")', (v, prev) => prev.concat([v]), [])
|
|
667
|
-
.option('--from <file>', 'Import content from a .md file as page body')
|
|
668
|
-
.allowUnknownOption()
|
|
669
|
-
.allowExcessArguments()
|
|
670
|
-
.action(async (db, opts, cmd) => runCommand('Add', async () => {
|
|
671
|
-
const notion = getNotion();
|
|
672
|
-
const dbIds = resolveDb(db);
|
|
673
|
-
|
|
674
|
-
// Merge --prop flags with dynamic property flags (--name, --status, etc.)
|
|
675
|
-
const schema = await getDbSchema(dbIds);
|
|
676
|
-
const knownFlags = ['prop', 'from', 'json', 'workspace', 'w', 'filter', 'limit', 'sort', 'output'];
|
|
677
|
-
const dynamicProps = extractDynamicProps(process.argv, knownFlags, schema);
|
|
678
|
-
const allProps = [...(opts.prop || []), ...dynamicProps];
|
|
679
|
-
|
|
680
|
-
if (allProps.length === 0) {
|
|
681
|
-
console.error('No properties provided. Use property flags or --prop:');
|
|
682
|
-
console.error(` notion add ${db} --name "My Page" --status "Active"`);
|
|
683
|
-
console.error(` notion add ${db} --prop "Name=My Page" --prop "Status=Active"`);
|
|
684
|
-
const propNames = Object.values(schema).map(s => `--${s.name.toLowerCase().replace(/\s+/g, '-')}`);
|
|
685
|
-
console.error(`\nAvailable: ${propNames.join(', ')}`);
|
|
686
|
-
process.exit(1);
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
const properties = await buildProperties(dbIds, allProps);
|
|
690
|
-
const res = await notion.pages.create({
|
|
691
|
-
parent: { type: 'data_source_id', data_source_id: dbIds.data_source_id },
|
|
692
|
-
properties,
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
// If --from file, parse and append blocks
|
|
696
|
-
if (opts.from) {
|
|
697
|
-
const filePath = path.resolve(opts.from);
|
|
698
|
-
if (!fs.existsSync(filePath)) {
|
|
699
|
-
console.error(`File not found: ${filePath}`);
|
|
700
|
-
process.exit(1);
|
|
701
|
-
}
|
|
702
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
703
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
704
|
-
|
|
705
|
-
let blocks;
|
|
706
|
-
if (ext === '.md' || ext === '.markdown') {
|
|
707
|
-
blocks = markdownToBlocks(content);
|
|
708
|
-
} else {
|
|
709
|
-
// Treat as plain text
|
|
710
|
-
blocks = [{ object: 'block', type: 'paragraph', paragraph: { rich_text: [{ type: 'text', text: { content } }] } }];
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// Notion API limits to 100 blocks per append call
|
|
714
|
-
for (let i = 0; i < blocks.length; i += 100) {
|
|
715
|
-
await notion.blocks.children.append({
|
|
716
|
-
block_id: res.id,
|
|
717
|
-
children: blocks.slice(i, i + 100),
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
if (jsonOutput(cmd, res)) return;
|
|
723
|
-
console.log(`✅ Created page: ${res.id}`);
|
|
724
|
-
console.log(` URL: ${res.url}`);
|
|
725
|
-
if (opts.from) console.log(` Content imported from: ${opts.from}`);
|
|
726
|
-
}));
|
|
727
|
-
|
|
728
|
-
// ─── update ────────────────────────────────────────────────────────────────────
|
|
729
|
-
program
|
|
730
|
-
.command('update <page-or-alias>')
|
|
731
|
-
.description('Update a page\'s properties by ID or alias + filter (e.g. notion update tasks --filter "Name=Ship it" --status "Done")')
|
|
732
|
-
.option('--filter <key=value...>', 'Filter to find the page — repeatable for AND (required with alias)', (v, prev) => prev.concat([v]), [])
|
|
733
|
-
.option('--prop <key=value...>', 'Property value — repeatable', (v, prev) => prev.concat([v]), [])
|
|
734
|
-
.allowUnknownOption()
|
|
735
|
-
.allowExcessArguments()
|
|
736
|
-
.action(async (target, opts, cmd) => runCommand('Update', async () => {
|
|
737
|
-
const notion = getNotion();
|
|
738
|
-
const { pageId, dbIds: resolvedDbIds } = await resolvePageId(target, opts.filter);
|
|
739
|
-
let dbIds = resolvedDbIds;
|
|
740
|
-
if (!dbIds) {
|
|
741
|
-
const page = await notion.pages.retrieve({ page_id: pageId });
|
|
742
|
-
const dsId = page.parent?.data_source_id;
|
|
743
|
-
if (!dsId) {
|
|
744
|
-
console.error('Page is not in a database — cannot auto-detect property types.');
|
|
745
|
-
process.exit(1);
|
|
746
|
-
}
|
|
747
|
-
dbIds = { data_source_id: dsId, database_id: page.parent?.database_id || dsId };
|
|
748
|
-
}
|
|
749
|
-
// Merge --prop flags with dynamic property flags
|
|
750
|
-
const schema = await getDbSchema(dbIds);
|
|
751
|
-
const knownFlags = ['prop', 'filter', 'json', 'workspace', 'w', 'limit', 'sort', 'output'];
|
|
752
|
-
const dynamicProps = extractDynamicProps(process.argv, knownFlags, schema);
|
|
753
|
-
const allProps = [...(opts.prop || []), ...dynamicProps];
|
|
754
|
-
|
|
755
|
-
if (allProps.length === 0) {
|
|
756
|
-
console.error('No properties to update. Use property flags or --prop:');
|
|
757
|
-
console.error(` notion update ${target} --filter "Name=..." --status "Done"`);
|
|
758
|
-
process.exit(1);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
const properties = await buildProperties(dbIds, allProps);
|
|
762
|
-
const res = await notion.pages.update({ page_id: pageId, properties });
|
|
763
|
-
if (jsonOutput(cmd, res)) return;
|
|
764
|
-
console.log(`✅ Updated page: ${res.id}`);
|
|
765
|
-
}));
|
|
766
|
-
|
|
767
|
-
// ─── delete (archive) ──────────────────────────────────────────────────────────
|
|
768
|
-
program
|
|
769
|
-
.command('delete <page-or-alias>')
|
|
770
|
-
.description('Delete (archive) a page by ID or alias + filter')
|
|
771
|
-
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
772
|
-
.action(async (target, opts, cmd) => runCommand('Delete', async () => {
|
|
773
|
-
const notion = getNotion();
|
|
774
|
-
const { pageId } = await resolvePageId(target, opts.filter);
|
|
775
|
-
const res = await notion.pages.update({ page_id: pageId, archived: true });
|
|
776
|
-
if (jsonOutput(cmd, res)) return;
|
|
777
|
-
console.log(`🗑️ Archived page: ${res.id}`);
|
|
778
|
-
console.log(' (Restore it from the trash in Notion if needed)');
|
|
779
|
-
}));
|
|
780
|
-
|
|
781
|
-
// ─── get ───────────────────────────────────────────────────────────────────────
|
|
782
|
-
program
|
|
783
|
-
.command('get <page-or-alias>')
|
|
784
|
-
.description('Get a page\'s properties by ID or alias + filter')
|
|
785
|
-
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
786
|
-
.action(async (target, opts, cmd) => runCommand('Get', async () => {
|
|
787
|
-
const notion = getNotion();
|
|
788
|
-
const { pageId } = await resolvePageId(target, opts.filter);
|
|
789
|
-
const page = await notion.pages.retrieve({ page_id: pageId });
|
|
790
|
-
if (jsonOutput(cmd, page)) return;
|
|
791
|
-
console.log(`Page: ${page.id}`);
|
|
792
|
-
console.log(`URL: ${page.url}`);
|
|
793
|
-
console.log(`Created: ${page.created_time}`);
|
|
794
|
-
console.log(`Updated: ${page.last_edited_time}`);
|
|
795
|
-
console.log('');
|
|
796
|
-
console.log('Properties:');
|
|
797
|
-
for (const [name, prop] of Object.entries(page.properties)) {
|
|
798
|
-
if (prop.type === 'relation') {
|
|
799
|
-
const rels = prop.relation || [];
|
|
800
|
-
if (rels.length === 0) {
|
|
801
|
-
console.log(` ${name}: (none)`);
|
|
802
|
-
} else {
|
|
803
|
-
// Resolve relation titles
|
|
804
|
-
const titles = [];
|
|
805
|
-
for (const rel of rels) {
|
|
806
|
-
try {
|
|
807
|
-
const linked = await notion.pages.retrieve({ page_id: rel.id });
|
|
808
|
-
let t = '';
|
|
809
|
-
for (const [, p] of Object.entries(linked.properties)) {
|
|
810
|
-
if (p.type === 'title') { t = propValue(p); break; }
|
|
811
|
-
}
|
|
812
|
-
titles.push(t || rel.id.slice(0, 8) + '…');
|
|
813
|
-
} catch {
|
|
814
|
-
titles.push(rel.id.slice(0, 8) + '…');
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
console.log(` ${name}: ${titles.join(', ')}`);
|
|
818
|
-
}
|
|
819
|
-
} else if (prop.type === 'rollup') {
|
|
820
|
-
const r = prop.rollup;
|
|
821
|
-
if (!r) {
|
|
822
|
-
console.log(` ${name}: (empty)`);
|
|
823
|
-
} else if (r.type === 'number') {
|
|
824
|
-
console.log(` ${name}: ${r.number != null ? r.number : '(empty)'}`);
|
|
825
|
-
} else if (r.type === 'date') {
|
|
826
|
-
console.log(` ${name}: ${r.date ? r.date.start : '(empty)'}`);
|
|
827
|
-
} else if (r.type === 'array' && r.array) {
|
|
828
|
-
console.log(` ${name}: ${r.array.map(item => propValue(item)).join(', ')}`);
|
|
829
|
-
} else {
|
|
830
|
-
console.log(` ${name}: ${JSON.stringify(r)}`);
|
|
831
|
-
}
|
|
832
|
-
} else {
|
|
833
|
-
console.log(` ${name}: ${propValue(prop)}`);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
}));
|
|
837
|
-
|
|
838
|
-
// ─── blocks ────────────────────────────────────────────────────────────────────
|
|
839
|
-
program
|
|
840
|
-
.command('blocks <page-or-alias>')
|
|
841
|
-
.description('Get page content as rendered blocks by ID or alias + filter')
|
|
842
|
-
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
843
|
-
.option('--ids', 'Show block IDs alongside content (for editing/deleting)')
|
|
844
|
-
.action(async (target, opts, cmd) => runCommand('Blocks', async () => {
|
|
845
|
-
const notion = getNotion();
|
|
846
|
-
const { pageId } = await resolvePageId(target, opts.filter);
|
|
847
|
-
const res = await notion.blocks.children.list({ block_id: pageId, page_size: 100 });
|
|
848
|
-
if (jsonOutput(cmd, res)) return;
|
|
849
|
-
if (res.results.length === 0) {
|
|
850
|
-
console.log('(no blocks)');
|
|
851
|
-
return;
|
|
852
|
-
}
|
|
853
|
-
for (const block of res.results) {
|
|
854
|
-
const type = block.type;
|
|
855
|
-
const content = block[type];
|
|
856
|
-
let text = '';
|
|
857
|
-
if (content?.rich_text) {
|
|
858
|
-
text = richTextToPlain(content.rich_text);
|
|
859
|
-
} else if (content?.text) {
|
|
860
|
-
text = richTextToPlain(content.text);
|
|
861
|
-
}
|
|
862
|
-
const prefix = type === 'heading_1' ? '# '
|
|
863
|
-
: type === 'heading_2' ? '## '
|
|
864
|
-
: type === 'heading_3' ? '### '
|
|
865
|
-
: type === 'bulleted_list_item' ? '• '
|
|
866
|
-
: type === 'numbered_list_item' ? ' 1. '
|
|
867
|
-
: type === 'to_do' ? (content?.checked ? '☑ ' : '☐ ')
|
|
868
|
-
: type === 'code' ? '```\n'
|
|
869
|
-
: '';
|
|
870
|
-
const suffix = type === 'code' ? '\n```' : '';
|
|
871
|
-
const idTag = opts.ids ? `[${block.id.slice(0, 8)}] ` : '';
|
|
872
|
-
console.log(`${idTag}${prefix}${text}${suffix}`);
|
|
873
|
-
}
|
|
874
|
-
}));
|
|
875
|
-
|
|
876
|
-
// ─── block-edit ────────────────────────────────────────────────────────────────
|
|
877
|
-
program
|
|
878
|
-
.command('block-edit <block-id> <text>')
|
|
879
|
-
.description('Update a block\'s text content')
|
|
880
|
-
.action(async (blockId, text, opts, cmd) => runCommand('Block edit', async () => {
|
|
881
|
-
const notion = getNotion();
|
|
882
|
-
// First retrieve the block to know its type
|
|
883
|
-
const block = await notion.blocks.retrieve({ block_id: blockId });
|
|
884
|
-
const type = block.type;
|
|
885
|
-
|
|
886
|
-
// Build the update payload based on block type
|
|
887
|
-
const supportedTextTypes = [
|
|
888
|
-
'paragraph', 'heading_1', 'heading_2', 'heading_3',
|
|
889
|
-
'bulleted_list_item', 'numbered_list_item', 'quote', 'callout', 'toggle',
|
|
890
|
-
];
|
|
891
|
-
|
|
892
|
-
if (type === 'to_do') {
|
|
893
|
-
const res = await notion.blocks.update({
|
|
894
|
-
block_id: blockId,
|
|
895
|
-
to_do: {
|
|
896
|
-
rich_text: [{ text: { content: text } }],
|
|
897
|
-
checked: block.to_do?.checked || false,
|
|
898
|
-
},
|
|
899
|
-
});
|
|
900
|
-
if (jsonOutput(cmd, res)) return;
|
|
901
|
-
console.log(`✅ Updated ${type} block: ${blockId.slice(0, 8)}…`);
|
|
902
|
-
} else if (supportedTextTypes.includes(type)) {
|
|
903
|
-
const res = await notion.blocks.update({
|
|
904
|
-
block_id: blockId,
|
|
905
|
-
[type]: {
|
|
906
|
-
rich_text: [{ text: { content: text } }],
|
|
907
|
-
},
|
|
908
|
-
});
|
|
909
|
-
if (jsonOutput(cmd, res)) return;
|
|
910
|
-
console.log(`✅ Updated ${type} block: ${blockId.slice(0, 8)}…`);
|
|
911
|
-
} else if (type === 'code') {
|
|
912
|
-
const res = await notion.blocks.update({
|
|
913
|
-
block_id: blockId,
|
|
914
|
-
code: {
|
|
915
|
-
rich_text: [{ text: { content: text } }],
|
|
916
|
-
language: block.code?.language || 'plain text',
|
|
917
|
-
},
|
|
918
|
-
});
|
|
919
|
-
if (jsonOutput(cmd, res)) return;
|
|
920
|
-
console.log(`✅ Updated code block: ${blockId.slice(0, 8)}…`);
|
|
921
|
-
} else {
|
|
922
|
-
console.error(`Block type "${type}" doesn't support text editing.`);
|
|
923
|
-
console.error('Supported types: paragraph, headings, lists, to_do, quote, callout, toggle, code');
|
|
924
|
-
process.exit(1);
|
|
925
|
-
}
|
|
926
|
-
}));
|
|
927
|
-
|
|
928
|
-
// ─── block-delete ──────────────────────────────────────────────────────────────
|
|
929
|
-
program
|
|
930
|
-
.command('block-delete <block-id>')
|
|
931
|
-
.description('Delete a block from a page')
|
|
932
|
-
.action(async (blockId, opts, cmd) => runCommand('Block delete', async () => {
|
|
933
|
-
const notion = getNotion();
|
|
934
|
-
const res = await notion.blocks.delete({ block_id: blockId });
|
|
935
|
-
if (jsonOutput(cmd, res)) return;
|
|
936
|
-
console.log(`🗑️ Deleted block: ${blockId.slice(0, 8)}…`);
|
|
937
|
-
}));
|
|
938
|
-
|
|
939
|
-
// ─── relations ─────────────────────────────────────────────────────────────────
|
|
940
|
-
program
|
|
941
|
-
.command('relations <page-or-alias>')
|
|
942
|
-
.description('Show all relation and rollup properties with resolved titles')
|
|
943
|
-
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
944
|
-
.action(async (target, opts, cmd) => runCommand('Relations', async () => {
|
|
945
|
-
const notion = getNotion();
|
|
946
|
-
const { pageId } = await resolvePageId(target, opts.filter);
|
|
947
|
-
const page = await notion.pages.retrieve({ page_id: pageId });
|
|
948
|
-
|
|
949
|
-
if (jsonOutput(cmd, page)) return;
|
|
950
|
-
|
|
951
|
-
let found = false;
|
|
952
|
-
|
|
953
|
-
for (const [name, prop] of Object.entries(page.properties)) {
|
|
954
|
-
if (prop.type === 'relation') {
|
|
955
|
-
const rels = prop.relation || [];
|
|
956
|
-
if (rels.length === 0) {
|
|
957
|
-
console.log(`\n${name}: (no linked pages)`);
|
|
958
|
-
continue;
|
|
959
|
-
}
|
|
960
|
-
found = true;
|
|
961
|
-
console.log(`\n${name}: ${rels.length} linked page${rels.length !== 1 ? 's' : ''}`);
|
|
962
|
-
|
|
963
|
-
// Resolve each related page title
|
|
964
|
-
const rows = [];
|
|
965
|
-
for (const rel of rels) {
|
|
966
|
-
try {
|
|
967
|
-
const linked = await notion.pages.retrieve({ page_id: rel.id });
|
|
968
|
-
let title = '';
|
|
969
|
-
for (const [, p] of Object.entries(linked.properties)) {
|
|
970
|
-
if (p.type === 'title') {
|
|
971
|
-
title = propValue(p);
|
|
972
|
-
break;
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
rows.push({
|
|
976
|
-
id: rel.id.slice(0, 8) + '…',
|
|
977
|
-
title: title || '(untitled)',
|
|
978
|
-
url: linked.url || '',
|
|
979
|
-
});
|
|
980
|
-
} catch {
|
|
981
|
-
rows.push({ id: rel.id.slice(0, 8) + '…', title: '(access denied)', url: '' });
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
printTable(rows, ['id', 'title', 'url']);
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
if (prop.type === 'rollup') {
|
|
988
|
-
found = true;
|
|
989
|
-
const r = prop.rollup;
|
|
990
|
-
console.log(`\n${name} (rollup):`);
|
|
991
|
-
if (!r) {
|
|
992
|
-
console.log(' (empty)');
|
|
993
|
-
continue;
|
|
994
|
-
}
|
|
995
|
-
if (r.type === 'number') {
|
|
996
|
-
console.log(` ${r.function || 'value'}: ${r.number}`);
|
|
997
|
-
} else if (r.type === 'date') {
|
|
998
|
-
console.log(` ${r.date ? r.date.start : '(empty)'}`);
|
|
999
|
-
} else if (r.type === 'array' && r.array) {
|
|
1000
|
-
for (const item of r.array) {
|
|
1001
|
-
console.log(` • ${propValue(item)}`);
|
|
1002
|
-
}
|
|
1003
|
-
} else {
|
|
1004
|
-
console.log(` ${JSON.stringify(r)}`);
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
if (!found) {
|
|
1010
|
-
console.log('This page has no relation or rollup properties.');
|
|
1011
|
-
}
|
|
1012
|
-
}));
|
|
1013
|
-
|
|
1014
|
-
// ─── dbs ───────────────────────────────────────────────────────────────────────
|
|
1015
|
-
program
|
|
1016
|
-
.command('dbs')
|
|
1017
|
-
.description('List all databases shared with your integration')
|
|
1018
|
-
.action(async (opts, cmd) => runCommand('List databases', async () => {
|
|
1019
|
-
const notion = getNotion();
|
|
1020
|
-
const res = await notion.search({
|
|
1021
|
-
filter: { value: 'data_source', property: 'object' },
|
|
1022
|
-
page_size: 100,
|
|
1023
|
-
});
|
|
1024
|
-
if (jsonOutput(cmd, res)) return;
|
|
1025
|
-
const rows = res.results.map(db => ({
|
|
1026
|
-
id: db.id,
|
|
1027
|
-
title: richTextToPlain(db.title),
|
|
1028
|
-
url: db.url || '',
|
|
1029
|
-
}));
|
|
1030
|
-
if (rows.length === 0) {
|
|
1031
|
-
console.log('No databases found. Make sure you\'ve shared databases with your integration.');
|
|
1032
|
-
console.log('In Notion: open a database → ••• menu → Connections → Add your integration');
|
|
1033
|
-
return;
|
|
1034
|
-
}
|
|
1035
|
-
printTable(rows, ['id', 'title', 'url']);
|
|
1036
|
-
}));
|
|
1037
|
-
|
|
1038
|
-
// ─── users ─────────────────────────────────────────────────────────────────────
|
|
1039
|
-
program
|
|
1040
|
-
.command('users')
|
|
1041
|
-
.description('List all users in the workspace')
|
|
1042
|
-
.action(async (opts, cmd) => runCommand('Users', async () => {
|
|
1043
|
-
const notion = getNotion();
|
|
1044
|
-
const res = await notion.users.list({});
|
|
1045
|
-
if (jsonOutput(cmd, res)) return;
|
|
1046
|
-
const rows = res.results.map(u => ({
|
|
1047
|
-
id: u.id,
|
|
1048
|
-
name: u.name || '',
|
|
1049
|
-
type: u.type || '',
|
|
1050
|
-
email: (u.person && u.person.email) || '',
|
|
1051
|
-
}));
|
|
1052
|
-
printTable(rows, ['id', 'name', 'type', 'email']);
|
|
1053
|
-
}));
|
|
1054
|
-
|
|
1055
|
-
// ─── user ──────────────────────────────────────────────────────────────────────
|
|
1056
|
-
program
|
|
1057
|
-
.command('user <user-id>')
|
|
1058
|
-
.description('Get user details')
|
|
1059
|
-
.action(async (userId, opts, cmd) => runCommand('User', async () => {
|
|
1060
|
-
const notion = getNotion();
|
|
1061
|
-
const user = await notion.users.retrieve({ user_id: userId });
|
|
1062
|
-
if (jsonOutput(cmd, user)) return;
|
|
1063
|
-
console.log(`User: ${user.id}`);
|
|
1064
|
-
console.log(`Name: ${user.name || '(unnamed)'}`);
|
|
1065
|
-
console.log(`Type: ${user.type || ''}`);
|
|
1066
|
-
if (user.person && user.person.email) {
|
|
1067
|
-
console.log(`Email: ${user.person.email}`);
|
|
1068
|
-
}
|
|
1069
|
-
if (user.avatar_url) {
|
|
1070
|
-
console.log(`Avatar: ${user.avatar_url}`);
|
|
1071
|
-
}
|
|
1072
|
-
if (user.bot) {
|
|
1073
|
-
console.log(`Bot Owner: ${JSON.stringify(user.bot.owner || {})}`);
|
|
1074
|
-
}
|
|
1075
|
-
}));
|
|
1076
|
-
|
|
1077
|
-
// ─── comments ──────────────────────────────────────────────────────────────────
|
|
1078
|
-
program
|
|
1079
|
-
.command('comments <page-or-alias>')
|
|
1080
|
-
.description('List comments on a page by ID or alias + filter')
|
|
1081
|
-
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
1082
|
-
.action(async (target, opts, cmd) => runCommand('Comments', async () => {
|
|
1083
|
-
const notion = getNotion();
|
|
1084
|
-
const { pageId } = await resolvePageId(target, opts.filter);
|
|
1085
|
-
const res = await notion.comments.list({ block_id: pageId });
|
|
1086
|
-
if (jsonOutput(cmd, res)) return;
|
|
1087
|
-
if (res.results.length === 0) {
|
|
1088
|
-
console.log('(no comments)');
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
const rows = res.results.map(c => ({
|
|
1092
|
-
id: c.id,
|
|
1093
|
-
text: richTextToPlain(c.rich_text),
|
|
1094
|
-
created: c.created_time || '',
|
|
1095
|
-
author: c.created_by?.name || c.created_by?.id || '',
|
|
1096
|
-
}));
|
|
1097
|
-
printTable(rows, ['id', 'text', 'created', 'author']);
|
|
1098
|
-
}));
|
|
1099
|
-
|
|
1100
|
-
// ─── comment ───────────────────────────────────────────────────────────────────
|
|
1101
|
-
program
|
|
1102
|
-
.command('comment <page-or-alias> <text>')
|
|
1103
|
-
.description('Add a comment to a page by ID or alias + filter')
|
|
1104
|
-
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
1105
|
-
.action(async (target, text, opts, cmd) => runCommand('Comment', async () => {
|
|
1106
|
-
const notion = getNotion();
|
|
1107
|
-
const { pageId } = await resolvePageId(target, opts.filter);
|
|
1108
|
-
const res = await notion.comments.create({
|
|
1109
|
-
parent: { page_id: pageId },
|
|
1110
|
-
rich_text: [{ text: { content: text } }],
|
|
1111
|
-
});
|
|
1112
|
-
if (jsonOutput(cmd, res)) return;
|
|
1113
|
-
console.log(`✅ Comment added: ${res.id}`);
|
|
1114
|
-
}));
|
|
1115
|
-
|
|
1116
|
-
// ─── append ────────────────────────────────────────────────────────────────────
|
|
1117
|
-
program
|
|
1118
|
-
.command('append <page-or-alias> <text>')
|
|
1119
|
-
.description('Append a text block to a page by ID or alias + filter')
|
|
1120
|
-
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
1121
|
-
.action(async (target, text, opts, cmd) => runCommand('Append', async () => {
|
|
1122
|
-
const notion = getNotion();
|
|
1123
|
-
const { pageId } = await resolvePageId(target, opts.filter);
|
|
1124
|
-
const res = await notion.blocks.children.append({
|
|
1125
|
-
block_id: pageId,
|
|
1126
|
-
children: [{
|
|
1127
|
-
object: 'block',
|
|
1128
|
-
type: 'paragraph',
|
|
1129
|
-
paragraph: {
|
|
1130
|
-
rich_text: [{ text: { content: text } }],
|
|
1131
|
-
},
|
|
1132
|
-
}],
|
|
1133
|
-
});
|
|
1134
|
-
if (jsonOutput(cmd, res)) return;
|
|
1135
|
-
console.log(`✅ Appended text block to page ${pageId}`);
|
|
1136
|
-
}));
|
|
1137
|
-
|
|
1138
|
-
// ─── me ────────────────────────────────────────────────────────────────────────
|
|
1139
|
-
program
|
|
1140
|
-
.command('me')
|
|
1141
|
-
.description('Show details about the current integration/bot')
|
|
1142
|
-
.action(async (opts, cmd) => runCommand('Me', async () => {
|
|
1143
|
-
const notion = getNotion();
|
|
1144
|
-
const me = await notion.users.me({});
|
|
1145
|
-
if (jsonOutput(cmd, me)) return;
|
|
1146
|
-
console.log(`Bot: ${me.name || '(unnamed)'}`);
|
|
1147
|
-
console.log(`ID: ${me.id}`);
|
|
1148
|
-
console.log(`Type: ${me.type}`);
|
|
1149
|
-
if (me.bot?.owner) {
|
|
1150
|
-
const owner = me.bot.owner;
|
|
1151
|
-
console.log(`Owner: ${owner.type === 'workspace' ? 'Workspace' : owner.user?.name || owner.type}`);
|
|
1152
|
-
}
|
|
1153
|
-
if (me.avatar_url) {
|
|
1154
|
-
console.log(`Avatar: ${me.avatar_url}`);
|
|
1155
|
-
}
|
|
1156
|
-
}));
|
|
1157
|
-
|
|
1158
|
-
// ─── move ──────────────────────────────────────────────────────────────────────
|
|
1159
|
-
program
|
|
1160
|
-
.command('move <page-or-alias>')
|
|
1161
|
-
.description('Move a page to a new parent (page or database)')
|
|
1162
|
-
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
1163
|
-
.option('--to <parent-id-or-alias>', 'Destination parent (page ID, database alias, or database ID)')
|
|
1164
|
-
.action(async (target, opts, cmd) => runCommand('Move', async () => {
|
|
1165
|
-
if (!opts.to) {
|
|
1166
|
-
console.error('--to is required. Specify a parent page ID or database alias.');
|
|
1167
|
-
process.exit(1);
|
|
1168
|
-
}
|
|
1169
|
-
const notion = getNotion();
|
|
1170
|
-
const { pageId } = await resolvePageId(target, opts.filter);
|
|
1171
|
-
|
|
1172
|
-
// Resolve --to target
|
|
1173
|
-
let parent;
|
|
1174
|
-
const ws = getWorkspaceConfig();
|
|
1175
|
-
if (ws.aliases && ws.aliases[opts.to]) {
|
|
1176
|
-
const db = ws.aliases[opts.to];
|
|
1177
|
-
// pages.move() requires data_source_id parent, not database_id
|
|
1178
|
-
parent = { type: 'data_source_id', data_source_id: db.data_source_id };
|
|
1179
|
-
} else if (UUID_REGEX.test(opts.to)) {
|
|
1180
|
-
// Assume page ID — user can also pass a database_id
|
|
1181
|
-
parent = { type: 'page_id', page_id: opts.to };
|
|
1182
|
-
} else {
|
|
1183
|
-
console.error(`Unknown destination: "${opts.to}". Use a page ID or database alias.`);
|
|
1184
|
-
const aliasNames = ws.aliases ? Object.keys(ws.aliases) : [];
|
|
1185
|
-
if (aliasNames.length > 0) {
|
|
1186
|
-
console.error(`Available aliases: ${aliasNames.join(', ')}`);
|
|
1187
|
-
}
|
|
1188
|
-
process.exit(1);
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
const res = await notion.pages.move({ page_id: pageId, parent });
|
|
1192
|
-
if (jsonOutput(cmd, res)) return;
|
|
1193
|
-
console.log(`✅ Moved page: ${pageId.slice(0, 8)}…`);
|
|
1194
|
-
if (res.url) console.log(` URL: ${res.url}`);
|
|
1195
|
-
}));
|
|
1196
|
-
|
|
1197
|
-
// ─── templates ─────────────────────────────────────────────────────────────────
|
|
1198
|
-
program
|
|
1199
|
-
.command('templates <database>')
|
|
1200
|
-
.description('List page templates available for a database')
|
|
1201
|
-
.action(async (db, opts, cmd) => runCommand('Templates', async () => {
|
|
1202
|
-
const notion = getNotion();
|
|
1203
|
-
const dbIds = resolveDb(db);
|
|
1204
|
-
const res = await notion.dataSources.listTemplates({
|
|
1205
|
-
data_source_id: dbIds.data_source_id,
|
|
1206
|
-
});
|
|
1207
|
-
if (jsonOutput(cmd, res)) return;
|
|
1208
|
-
if (!res.results || res.results.length === 0) {
|
|
1209
|
-
console.log('No templates found for this database.');
|
|
1210
|
-
return;
|
|
1211
|
-
}
|
|
1212
|
-
const rows = res.results.map(t => {
|
|
1213
|
-
let title = '';
|
|
1214
|
-
if (t.properties) {
|
|
1215
|
-
for (const [, prop] of Object.entries(t.properties)) {
|
|
1216
|
-
if (prop.type === 'title') {
|
|
1217
|
-
title = propValue(prop);
|
|
1218
|
-
break;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
return {
|
|
1223
|
-
id: t.id,
|
|
1224
|
-
title: title || '(untitled)',
|
|
1225
|
-
url: t.url || '',
|
|
1226
|
-
};
|
|
1227
|
-
});
|
|
1228
|
-
printTable(rows, ['id', 'title', 'url']);
|
|
1229
|
-
}));
|
|
1230
|
-
|
|
1231
|
-
// ─── db-create ─────────────────────────────────────────────────────────────────
|
|
1232
|
-
program
|
|
1233
|
-
.command('db-create <parent-page-id> <title>')
|
|
1234
|
-
.description('Create a new database under a page')
|
|
1235
|
-
.option('--prop <name:type...>', 'Property definition — repeatable (e.g. --prop "Status:select" --prop "Priority:number")', (v, prev) => prev.concat([v]), [])
|
|
1236
|
-
.option('--alias <name>', 'Auto-create an alias for the new database')
|
|
1237
|
-
.action(async (parentPageId, title, opts, cmd) => runCommand('Database create', async () => {
|
|
1238
|
-
const notion = getNotion();
|
|
1239
|
-
|
|
1240
|
-
// Build properties — always include a title property
|
|
1241
|
-
const properties = {};
|
|
1242
|
-
let hasTitleProp = false;
|
|
1243
|
-
|
|
1244
|
-
for (const kv of opts.prop) {
|
|
1245
|
-
const colonIdx = kv.indexOf(':');
|
|
1246
|
-
if (colonIdx === -1) {
|
|
1247
|
-
console.error(`Invalid property format: ${kv} (expected name:type)`);
|
|
1248
|
-
console.error('Supported types: title, rich_text, number, select, multi_select, date, checkbox, url, email, phone_number, status');
|
|
1249
|
-
process.exit(1);
|
|
1250
|
-
}
|
|
1251
|
-
const name = kv.slice(0, colonIdx);
|
|
1252
|
-
const type = kv.slice(colonIdx + 1).toLowerCase();
|
|
1253
|
-
if (type === 'title') hasTitleProp = true;
|
|
1254
|
-
properties[name] = { [type]: {} };
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
// Ensure there's a title property
|
|
1258
|
-
if (!hasTitleProp) {
|
|
1259
|
-
properties['Name'] = { title: {} };
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
// 2025 API: databases.create() only handles title property reliably.
|
|
1263
|
-
// Non-title properties must be added via dataSources.update() after creation.
|
|
1264
|
-
const titleProps = {};
|
|
1265
|
-
const extraProps = {};
|
|
1266
|
-
for (const [name, prop] of Object.entries(properties)) {
|
|
1267
|
-
if (prop.title) {
|
|
1268
|
-
titleProps[name] = prop;
|
|
1269
|
-
} else {
|
|
1270
|
-
extraProps[name] = prop;
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
// Ensure title property exists in create call
|
|
1274
|
-
if (Object.keys(titleProps).length === 0) {
|
|
1275
|
-
titleProps['Name'] = { title: {} };
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
const res = await notion.databases.create({
|
|
1279
|
-
parent: { type: 'page_id', page_id: parentPageId },
|
|
1280
|
-
title: [{ text: { content: title } }],
|
|
1281
|
-
properties: titleProps,
|
|
1282
|
-
});
|
|
1283
|
-
|
|
1284
|
-
// Extract correct dual IDs from response
|
|
1285
|
-
const databaseId = res.id;
|
|
1286
|
-
const dataSourceId = (res.data_sources && res.data_sources[0])
|
|
1287
|
-
? res.data_sources[0].id
|
|
1288
|
-
: res.id;
|
|
1289
|
-
|
|
1290
|
-
// Add non-title properties via dataSources.update()
|
|
1291
|
-
if (Object.keys(extraProps).length > 0) {
|
|
1292
|
-
await notion.dataSources.update({
|
|
1293
|
-
data_source_id: dataSourceId,
|
|
1294
|
-
properties: extraProps,
|
|
1295
|
-
});
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
if (jsonOutput(cmd, res)) return;
|
|
1299
|
-
|
|
1300
|
-
console.log(`✅ Created database: ${databaseId.slice(0, 8)}…`);
|
|
1301
|
-
console.log(` Title: ${title}`);
|
|
1302
|
-
console.log(` Properties: ${Object.keys(properties).join(', ')}`);
|
|
1303
|
-
|
|
1304
|
-
// Auto-create alias if requested
|
|
1305
|
-
if (opts.alias) {
|
|
1306
|
-
const config = loadConfig();
|
|
1307
|
-
const wsName = getWorkspaceName() || config.activeWorkspace || 'default';
|
|
1308
|
-
if (!config.workspaces[wsName]) config.workspaces[wsName] = { aliases: {} };
|
|
1309
|
-
if (!config.workspaces[wsName].aliases) config.workspaces[wsName].aliases = {};
|
|
1310
|
-
config.workspaces[wsName].aliases[opts.alias] = {
|
|
1311
|
-
database_id: databaseId,
|
|
1312
|
-
data_source_id: dataSourceId,
|
|
1313
|
-
};
|
|
1314
|
-
saveConfig(config);
|
|
1315
|
-
console.log(` Alias: ${opts.alias}`);
|
|
1316
|
-
}
|
|
1317
|
-
}));
|
|
1318
|
-
|
|
1319
|
-
// ─── db-update ─────────────────────────────────────────────────────────────────
|
|
1320
|
-
program
|
|
1321
|
-
.command('db-update <database>')
|
|
1322
|
-
.description('Update a database title or add properties')
|
|
1323
|
-
.option('--title <text>', 'New database title')
|
|
1324
|
-
.option('--add-prop <name:type...>', 'Add a property (e.g. --add-prop "Priority:number")', (v, prev) => prev.concat([v]), [])
|
|
1325
|
-
.option('--remove-prop <name...>', 'Remove a property by name', (v, prev) => prev.concat([v]), [])
|
|
1326
|
-
.action(async (db, opts, cmd) => runCommand('Database update', async () => {
|
|
1327
|
-
const notion = getNotion();
|
|
1328
|
-
const dbIds = resolveDb(db);
|
|
1329
|
-
|
|
1330
|
-
// 2025 API: property changes go through dataSources.update(), NOT databases.update().
|
|
1331
|
-
// databases.update() silently ignores property modifications.
|
|
1332
|
-
// Title changes still go through databases.update().
|
|
1333
|
-
let canonicalId = dbIds.database_id;
|
|
1334
|
-
const dataSourceId = dbIds.data_source_id;
|
|
1335
|
-
|
|
1336
|
-
// Resolve canonical database_id if both IDs are the same
|
|
1337
|
-
if (canonicalId === dataSourceId) {
|
|
1338
|
-
try {
|
|
1339
|
-
const ds = await notion.dataSources.retrieve({ data_source_id: canonicalId });
|
|
1340
|
-
if (ds.parent && ds.parent.type === 'database_id') {
|
|
1341
|
-
canonicalId = ds.parent.database_id;
|
|
1342
|
-
}
|
|
1343
|
-
} catch (_) { /* fall through with what we have */ }
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
// Build property changes for dataSources.update()
|
|
1347
|
-
let propChanges = null;
|
|
1348
|
-
if (opts.addProp.length > 0 || opts.removeProp.length > 0) {
|
|
1349
|
-
propChanges = {};
|
|
1350
|
-
|
|
1351
|
-
for (const kv of opts.addProp) {
|
|
1352
|
-
const colonIdx = kv.indexOf(':');
|
|
1353
|
-
if (colonIdx === -1) {
|
|
1354
|
-
console.error(`Invalid property format: ${kv} (expected name:type)`);
|
|
1355
|
-
process.exit(1);
|
|
1356
|
-
}
|
|
1357
|
-
const name = kv.slice(0, colonIdx);
|
|
1358
|
-
const type = kv.slice(colonIdx + 1).toLowerCase();
|
|
1359
|
-
propChanges[name] = { [type]: {} };
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
for (const name of opts.removeProp) {
|
|
1363
|
-
propChanges[name] = null;
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
let res;
|
|
1368
|
-
|
|
1369
|
-
// Title changes go through databases.update()
|
|
1370
|
-
if (opts.title) {
|
|
1371
|
-
res = await notion.databases.update({
|
|
1372
|
-
database_id: canonicalId,
|
|
1373
|
-
title: [{ text: { content: opts.title } }],
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// Property changes go through dataSources.update()
|
|
1378
|
-
if (propChanges) {
|
|
1379
|
-
res = await notion.dataSources.update({
|
|
1380
|
-
data_source_id: dataSourceId,
|
|
1381
|
-
properties: propChanges,
|
|
1382
|
-
});
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
if (jsonOutput(cmd, res)) return;
|
|
1386
|
-
|
|
1387
|
-
console.log(`✅ Updated database: ${(dbIds.database_id || dbIds.data_source_id).slice(0, 8)}…`);
|
|
1388
|
-
if (opts.title) console.log(` Title: ${opts.title}`);
|
|
1389
|
-
if (opts.addProp.length > 0) console.log(` Added: ${opts.addProp.join(', ')}`);
|
|
1390
|
-
if (opts.removeProp.length > 0) console.log(` Removed: ${opts.removeProp.join(', ')}`);
|
|
1391
|
-
}));
|
|
1392
|
-
|
|
1393
|
-
// ─── upload ────────────────────────────────────────────────────────────────────
|
|
1394
|
-
program
|
|
1395
|
-
.command('upload <page-or-alias> <file-path>')
|
|
1396
|
-
.description('Upload a file to a page')
|
|
1397
|
-
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
1398
|
-
.action(async (target, filePath, opts, cmd) => runCommand('Upload', async () => {
|
|
1399
|
-
const notion = getNotion();
|
|
1400
|
-
const { pageId } = await resolvePageId(target, opts.filter);
|
|
1401
|
-
|
|
1402
|
-
// Resolve file path
|
|
1403
|
-
const absPath = path.resolve(filePath);
|
|
1404
|
-
if (!fs.existsSync(absPath)) {
|
|
1405
|
-
console.error(`File not found: ${absPath}`);
|
|
1406
|
-
process.exit(1);
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
const filename = path.basename(absPath);
|
|
1410
|
-
const fileData = fs.readFileSync(absPath);
|
|
1411
|
-
const fileSize = fileData.length;
|
|
1412
|
-
|
|
1413
|
-
// Detect MIME type from extension
|
|
1414
|
-
const MIME_MAP = {
|
|
1415
|
-
'.txt': 'text/plain', '.csv': 'text/csv', '.html': 'text/html',
|
|
1416
|
-
'.json': 'application/json', '.pdf': 'application/pdf',
|
|
1417
|
-
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
1418
|
-
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
1419
|
-
'.mp4': 'video/mp4', '.mp3': 'audio/mpeg', '.wav': 'audio/wav',
|
|
1420
|
-
'.zip': 'application/zip', '.doc': 'application/msword',
|
|
1421
|
-
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
1422
|
-
'.xls': 'application/vnd.ms-excel',
|
|
1423
|
-
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
1424
|
-
};
|
|
1425
|
-
const ext = path.extname(filename).toLowerCase();
|
|
1426
|
-
const mimeType = MIME_MAP[ext] || 'application/octet-stream';
|
|
1427
|
-
|
|
1428
|
-
// Step 1: Create file upload
|
|
1429
|
-
const upload = await notion.fileUploads.create({
|
|
1430
|
-
parent: { type: 'page_id', page_id: pageId },
|
|
1431
|
-
filename,
|
|
1432
|
-
});
|
|
1433
|
-
const uploadId = upload.id;
|
|
1434
|
-
|
|
1435
|
-
// Step 2: Send file data with correct content type
|
|
1436
|
-
await notion.fileUploads.send({
|
|
1437
|
-
file_upload_id: uploadId,
|
|
1438
|
-
file: { data: new Blob([fileData], { type: mimeType }), filename },
|
|
1439
|
-
part_number: '1',
|
|
1440
|
-
});
|
|
1441
|
-
|
|
1442
|
-
// Step 3: Append file block to page (no complete() needed — attach directly)
|
|
1443
|
-
await notion.blocks.children.append({
|
|
1444
|
-
block_id: pageId,
|
|
1445
|
-
children: [{
|
|
1446
|
-
object: 'block',
|
|
1447
|
-
type: 'file',
|
|
1448
|
-
file: {
|
|
1449
|
-
type: 'file_upload',
|
|
1450
|
-
file_upload: { id: uploadId },
|
|
1451
|
-
},
|
|
1452
|
-
}],
|
|
1453
|
-
});
|
|
1454
|
-
|
|
1455
|
-
if (jsonOutput(cmd, { upload_id: uploadId, filename, size: fileSize, page_id: pageId })) return;
|
|
1456
|
-
|
|
1457
|
-
const sizeStr = fileSize > 1024 * 1024
|
|
1458
|
-
? `${(fileSize / (1024 * 1024)).toFixed(1)} MB`
|
|
1459
|
-
: `${(fileSize / 1024).toFixed(1)} KB`;
|
|
1460
|
-
|
|
1461
|
-
console.log(`✅ Uploaded: ${filename} (${sizeStr})`);
|
|
1462
|
-
console.log(` Page: ${pageId.slice(0, 8)}…`);
|
|
1463
|
-
}));
|
|
1464
|
-
|
|
1465
|
-
// ─── props ─────────────────────────────────────────────────────────────────────
|
|
1466
|
-
program
|
|
1467
|
-
.command('props <page-or-alias>')
|
|
1468
|
-
.description('List all properties with full paginated values')
|
|
1469
|
-
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
1470
|
-
.action(async (target, opts, cmd) => runCommand('Props', async () => {
|
|
1471
|
-
const notion = getNotion();
|
|
1472
|
-
const { pageId } = await resolvePageId(target, opts.filter);
|
|
1473
|
-
const page = await notion.pages.retrieve({ page_id: pageId });
|
|
1474
|
-
|
|
1475
|
-
if (jsonOutput(cmd, page)) return;
|
|
1476
|
-
|
|
1477
|
-
console.log(`Page: ${page.id}`);
|
|
1478
|
-
console.log(`URL: ${page.url}\n`);
|
|
1479
|
-
|
|
1480
|
-
for (const [name, prop] of Object.entries(page.properties)) {
|
|
1481
|
-
// For paginated properties (relation, rollup, rich_text, title, people),
|
|
1482
|
-
// use the property retrieval endpoint to get full values
|
|
1483
|
-
const needsPagination = ['relation', 'rollup', 'rich_text', 'title', 'people'].includes(prop.type);
|
|
1484
|
-
|
|
1485
|
-
if (needsPagination && prop.id) {
|
|
1486
|
-
try {
|
|
1487
|
-
const fullProp = await notion.pages.properties.retrieve({
|
|
1488
|
-
page_id: pageId,
|
|
1489
|
-
property_id: prop.id,
|
|
1490
|
-
});
|
|
1491
|
-
|
|
1492
|
-
if (fullProp.results) {
|
|
1493
|
-
// Paginated property — collect all results
|
|
1494
|
-
const items = fullProp.results;
|
|
1495
|
-
if (prop.type === 'relation') {
|
|
1496
|
-
if (items.length === 0) {
|
|
1497
|
-
console.log(` ${name}: (none)`);
|
|
1498
|
-
} else {
|
|
1499
|
-
const titles = [];
|
|
1500
|
-
for (const item of items) {
|
|
1501
|
-
const relId = item.relation?.id;
|
|
1502
|
-
if (relId) {
|
|
1503
|
-
try {
|
|
1504
|
-
const linked = await notion.pages.retrieve({ page_id: relId });
|
|
1505
|
-
let t = '';
|
|
1506
|
-
for (const [, p] of Object.entries(linked.properties)) {
|
|
1507
|
-
if (p.type === 'title') { t = propValue(p); break; }
|
|
1508
|
-
}
|
|
1509
|
-
titles.push(t || relId.slice(0, 8) + '…');
|
|
1510
|
-
} catch {
|
|
1511
|
-
titles.push(relId.slice(0, 8) + '…');
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
console.log(` ${name}: ${titles.join(', ')}`);
|
|
1516
|
-
}
|
|
1517
|
-
} else if (prop.type === 'rich_text' || prop.type === 'title') {
|
|
1518
|
-
const text = items.map(i => i[prop.type]?.plain_text || '').join('');
|
|
1519
|
-
console.log(` ${name}: ${text}`);
|
|
1520
|
-
} else if (prop.type === 'people') {
|
|
1521
|
-
const people = items.map(i => i.people?.name || i.people?.id || '').join(', ');
|
|
1522
|
-
console.log(` ${name}: ${people}`);
|
|
1523
|
-
} else {
|
|
1524
|
-
console.log(` ${name}: ${JSON.stringify(items)}`);
|
|
1525
|
-
}
|
|
1526
|
-
} else {
|
|
1527
|
-
// Non-paginated response
|
|
1528
|
-
console.log(` ${name}: ${propValue(fullProp)}`);
|
|
1529
|
-
}
|
|
1530
|
-
} catch {
|
|
1531
|
-
// Fallback to basic propValue
|
|
1532
|
-
console.log(` ${name}: ${propValue(prop)}`);
|
|
1533
|
-
}
|
|
1534
|
-
} else {
|
|
1535
|
-
console.log(` ${name}: ${propValue(prop)}`);
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
}));
|
|
1539
|
-
|
|
1540
|
-
// ─── import ────────────────────────────────────────────────────────────────────
|
|
1541
|
-
program
|
|
1542
|
-
.command('import <file>')
|
|
1543
|
-
.description('Import data from a file (.csv/.json → database pages, .md → page content)')
|
|
1544
|
-
.option('--to <database>', 'Target database alias for CSV/JSON import')
|
|
1545
|
-
.option('--parent <page-id>', 'Parent page for markdown import')
|
|
1546
|
-
.option('--title <text>', 'Page title for markdown import')
|
|
1547
|
-
.action(async (file, opts, cmd) => runCommand('Import', async () => {
|
|
1548
|
-
const filePath = path.resolve(file);
|
|
1549
|
-
if (!fs.existsSync(filePath)) {
|
|
1550
|
-
console.error(`File not found: ${filePath}`);
|
|
1551
|
-
process.exit(1);
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
1555
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
1556
|
-
|
|
1557
|
-
if (ext === '.csv' || ext === '.json') {
|
|
1558
|
-
// Database import: CSV/JSON → pages
|
|
1559
|
-
if (!opts.to) {
|
|
1560
|
-
console.error('--to <database> is required for CSV/JSON import.');
|
|
1561
|
-
console.error('Example: notion import data.csv --to tasks');
|
|
1562
|
-
process.exit(1);
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
const notion = getNotion();
|
|
1566
|
-
const dbIds = resolveDb(opts.to);
|
|
1567
|
-
const schema = await getDbSchema(dbIds);
|
|
1568
|
-
|
|
1569
|
-
let rows;
|
|
1570
|
-
if (ext === '.csv') {
|
|
1571
|
-
rows = parseCsv(content);
|
|
1572
|
-
} else {
|
|
1573
|
-
const parsed = JSON.parse(content);
|
|
1574
|
-
rows = Array.isArray(parsed) ? parsed : [parsed];
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
if (rows.length === 0) {
|
|
1578
|
-
console.error('No data found in file.');
|
|
1579
|
-
process.exit(1);
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
console.log(`Importing ${rows.length} row${rows.length !== 1 ? 's' : ''} to ${opts.to}...`);
|
|
1583
|
-
|
|
1584
|
-
let created = 0;
|
|
1585
|
-
let failed = 0;
|
|
1586
|
-
for (const row of rows) {
|
|
1587
|
-
try {
|
|
1588
|
-
// Map row keys to schema properties
|
|
1589
|
-
const propStrs = [];
|
|
1590
|
-
for (const [key, value] of Object.entries(row)) {
|
|
1591
|
-
if (value === '' || value === null || value === undefined) continue;
|
|
1592
|
-
const schemaEntry = schema[key.toLowerCase()];
|
|
1593
|
-
if (schemaEntry) {
|
|
1594
|
-
propStrs.push(`${schemaEntry.name}=${value}`);
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
if (propStrs.length === 0) continue;
|
|
1598
|
-
|
|
1599
|
-
const properties = await buildProperties(dbIds, propStrs);
|
|
1600
|
-
await notion.pages.create({
|
|
1601
|
-
parent: { type: 'data_source_id', data_source_id: dbIds.data_source_id },
|
|
1602
|
-
properties,
|
|
1603
|
-
});
|
|
1604
|
-
created++;
|
|
1605
|
-
} catch (err) {
|
|
1606
|
-
failed++;
|
|
1607
|
-
if (failed <= 3) console.error(` Row failed: ${err.message}`);
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
console.log(`✅ Imported ${created} page${created !== 1 ? 's' : ''}${failed > 0 ? ` (${failed} failed)` : ''}`);
|
|
1612
|
-
|
|
1613
|
-
} else if (ext === '.md' || ext === '.markdown') {
|
|
1614
|
-
// Page import: Markdown → page with blocks
|
|
1615
|
-
const notion = getNotion();
|
|
1616
|
-
const title = opts.title || path.basename(filePath, ext);
|
|
1617
|
-
|
|
1618
|
-
let parentId = opts.parent;
|
|
1619
|
-
if (!parentId && opts.to) {
|
|
1620
|
-
// If --to is an alias, create as a database page
|
|
1621
|
-
const dbIds = resolveDb(opts.to);
|
|
1622
|
-
const properties = await buildProperties(dbIds, [`Name=${title}`]);
|
|
1623
|
-
const res = await notion.pages.create({
|
|
1624
|
-
parent: { type: 'data_source_id', data_source_id: dbIds.data_source_id },
|
|
1625
|
-
properties,
|
|
1626
|
-
});
|
|
1627
|
-
parentId = res.id;
|
|
1628
|
-
console.log(`✅ Created page: ${res.id}`);
|
|
1629
|
-
} else if (parentId) {
|
|
1630
|
-
// Create as a child page
|
|
1631
|
-
const res = await notion.pages.create({
|
|
1632
|
-
parent: { type: 'page_id', page_id: parentId },
|
|
1633
|
-
properties: { title: { title: [{ text: { content: title } }] } },
|
|
1634
|
-
});
|
|
1635
|
-
parentId = res.id;
|
|
1636
|
-
console.log(`✅ Created page: ${res.id}`);
|
|
1637
|
-
} else {
|
|
1638
|
-
console.error('Specify --to <database> or --parent <page-id> for markdown import.');
|
|
1639
|
-
process.exit(1);
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
// Parse markdown and append blocks
|
|
1643
|
-
const blocks = markdownToBlocks(content);
|
|
1644
|
-
for (let i = 0; i < blocks.length; i += 100) {
|
|
1645
|
-
await notion.blocks.children.append({
|
|
1646
|
-
block_id: parentId,
|
|
1647
|
-
children: blocks.slice(i, i + 100),
|
|
1648
|
-
});
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
console.log(` Imported ${blocks.length} block${blocks.length !== 1 ? 's' : ''} from ${path.basename(filePath)}`);
|
|
1652
|
-
} else {
|
|
1653
|
-
console.error(`Unsupported file type: ${ext}`);
|
|
1654
|
-
console.error('Supported: .csv, .json (→ database), .md (→ page)');
|
|
1655
|
-
process.exit(1);
|
|
1656
|
-
}
|
|
1657
|
-
}));
|
|
1658
|
-
|
|
1659
|
-
// ─── export ────────────────────────────────────────────────────────────────────
|
|
1660
|
-
program
|
|
1661
|
-
.command('export <page-or-alias>')
|
|
1662
|
-
.description('Export page content as markdown')
|
|
1663
|
-
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
1664
|
-
.action(async (target, opts, cmd) => runCommand('Export', async () => {
|
|
1665
|
-
const notion = getNotion();
|
|
1666
|
-
const { pageId } = await resolvePageId(target, opts.filter);
|
|
1667
|
-
|
|
1668
|
-
// Fetch all blocks
|
|
1669
|
-
let blocks = [];
|
|
1670
|
-
let cursor;
|
|
1671
|
-
do {
|
|
1672
|
-
const res = await notion.blocks.children.list({
|
|
1673
|
-
block_id: pageId,
|
|
1674
|
-
start_cursor: cursor,
|
|
1675
|
-
page_size: 100,
|
|
1676
|
-
});
|
|
1677
|
-
blocks = blocks.concat(res.results);
|
|
1678
|
-
cursor = res.has_more ? res.next_cursor : null;
|
|
1679
|
-
} while (cursor);
|
|
1680
|
-
|
|
1681
|
-
const md = blocksToMarkdown(blocks);
|
|
1682
|
-
console.log(md);
|
|
1683
|
-
}));
|
|
1684
|
-
|
|
1685
|
-
// ─── Run ───────────────────────────────────────────────────────────────────────
|
|
1686
|
-
program.parse();
|
|
13
|
+
const ctx = createContext(program);
|
|
14
|
+
|
|
15
|
+
// Register all command modules
|
|
16
|
+
require('../commands/config').register(program, ctx);
|
|
17
|
+
require('../commands/search').register(program, ctx);
|
|
18
|
+
require('../commands/query').register(program, ctx);
|
|
19
|
+
require('../commands/crud').register(program, ctx);
|
|
20
|
+
require('../commands/blocks').register(program, ctx);
|
|
21
|
+
require('../commands/database').register(program, ctx);
|
|
22
|
+
require('../commands/users').register(program, ctx);
|
|
23
|
+
require('../commands/comments').register(program, ctx);
|
|
24
|
+
require('../commands/pages').register(program, ctx);
|
|
25
|
+
require('../commands/import-export').register(program, ctx);
|
|
26
|
+
require('../commands/upload').register(program, ctx);
|
|
27
|
+
|
|
28
|
+
program.parseAsync(process.argv);
|