@jordancoin/notioncli 1.2.1 → 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 +118 -420
- package/TECHNICAL.md +185 -0
- package/bin/notion.js +18 -1611
- 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/config.js +68 -0
- package/lib/context.js +359 -0
- package/lib/filters.js +257 -0
- package/lib/format.js +258 -0
- package/lib/helpers.js +13 -334
- package/lib/markdown.js +488 -0
- package/lib/paginate.js +74 -0
- package/lib/retry.js +76 -0
- package/package.json +1 -1
- package/skill/SKILL.md +51 -10
- package/skill/marketplace.json +2 -2
- package/test/unit.test.js +662 -0
- package/test/debug-parent.js +0 -32
- package/test/live-relations-test.js +0 -309
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
register(program, ctx) {
|
|
3
|
+
const {
|
|
4
|
+
getNotion,
|
|
5
|
+
resolvePageId,
|
|
6
|
+
paginate,
|
|
7
|
+
jsonOutput,
|
|
8
|
+
richTextToPlain,
|
|
9
|
+
runCommand,
|
|
10
|
+
} = ctx;
|
|
11
|
+
|
|
12
|
+
// ─── blocks ───────────────────────────────────────────────────────────────
|
|
13
|
+
program
|
|
14
|
+
.command('blocks <page-or-alias>')
|
|
15
|
+
.description('Get page content as rendered blocks by ID or alias + filter')
|
|
16
|
+
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
17
|
+
.option('--ids', 'Show block IDs alongside content (for editing/deleting)')
|
|
18
|
+
.action(async (target, opts, cmd) => runCommand('Blocks', async () => {
|
|
19
|
+
const notion = getNotion();
|
|
20
|
+
const { pageId } = await resolvePageId(target, opts.filter);
|
|
21
|
+
const { results, response } = await paginate(
|
|
22
|
+
({ start_cursor, page_size }) => notion.blocks.children.list({
|
|
23
|
+
block_id: pageId,
|
|
24
|
+
start_cursor,
|
|
25
|
+
page_size,
|
|
26
|
+
}),
|
|
27
|
+
{ pageSizeLimit: 100 },
|
|
28
|
+
);
|
|
29
|
+
if (jsonOutput(cmd, response)) return;
|
|
30
|
+
if (results.length === 0) {
|
|
31
|
+
console.log('(no blocks)');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
for (const block of results) {
|
|
35
|
+
const type = block.type;
|
|
36
|
+
const content = block[type];
|
|
37
|
+
let text = '';
|
|
38
|
+
if (content?.rich_text) {
|
|
39
|
+
text = richTextToPlain(content.rich_text);
|
|
40
|
+
} else if (content?.text) {
|
|
41
|
+
text = richTextToPlain(content.text);
|
|
42
|
+
}
|
|
43
|
+
const prefix = type === 'heading_1' ? '# '
|
|
44
|
+
: type === 'heading_2' ? '## '
|
|
45
|
+
: type === 'heading_3' ? '### '
|
|
46
|
+
: type === 'bulleted_list_item' ? '• '
|
|
47
|
+
: type === 'numbered_list_item' ? ' 1. '
|
|
48
|
+
: type === 'to_do' ? (content?.checked ? '☑ ' : '☐ ')
|
|
49
|
+
: type === 'code' ? '```\n'
|
|
50
|
+
: '';
|
|
51
|
+
const suffix = type === 'code' ? '\n```' : '';
|
|
52
|
+
const idTag = opts.ids ? `[${block.id.slice(0, 8)}] ` : '';
|
|
53
|
+
console.log(`${idTag}${prefix}${text}${suffix}`);
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
// ─── block-edit ───────────────────────────────────────────────────────────
|
|
58
|
+
program
|
|
59
|
+
.command('block-edit <block-id> <text>')
|
|
60
|
+
.description('Update a block\'s text content')
|
|
61
|
+
.action(async (blockId, text, opts, cmd) => runCommand('Block edit', async () => {
|
|
62
|
+
const notion = getNotion();
|
|
63
|
+
// First retrieve the block to know its type
|
|
64
|
+
const block = await notion.blocks.retrieve({ block_id: blockId });
|
|
65
|
+
const type = block.type;
|
|
66
|
+
|
|
67
|
+
// Build the update payload based on block type
|
|
68
|
+
const supportedTextTypes = [
|
|
69
|
+
'paragraph', 'heading_1', 'heading_2', 'heading_3',
|
|
70
|
+
'bulleted_list_item', 'numbered_list_item', 'quote', 'callout', 'toggle',
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
if (type === 'to_do') {
|
|
74
|
+
const res = await notion.blocks.update({
|
|
75
|
+
block_id: blockId,
|
|
76
|
+
to_do: {
|
|
77
|
+
rich_text: [{ text: { content: text } }],
|
|
78
|
+
checked: block.to_do?.checked || false,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
if (jsonOutput(cmd, res)) return;
|
|
82
|
+
console.log(`✅ Updated ${type} block: ${blockId.slice(0, 8)}…`);
|
|
83
|
+
} else if (supportedTextTypes.includes(type)) {
|
|
84
|
+
const res = await notion.blocks.update({
|
|
85
|
+
block_id: blockId,
|
|
86
|
+
[type]: {
|
|
87
|
+
rich_text: [{ text: { content: text } }],
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
if (jsonOutput(cmd, res)) return;
|
|
91
|
+
console.log(`✅ Updated ${type} block: ${blockId.slice(0, 8)}…`);
|
|
92
|
+
} else if (type === 'code') {
|
|
93
|
+
const res = await notion.blocks.update({
|
|
94
|
+
block_id: blockId,
|
|
95
|
+
code: {
|
|
96
|
+
rich_text: [{ text: { content: text } }],
|
|
97
|
+
language: block.code?.language || 'plain text',
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
if (jsonOutput(cmd, res)) return;
|
|
101
|
+
console.log(`✅ Updated code block: ${blockId.slice(0, 8)}…`);
|
|
102
|
+
} else {
|
|
103
|
+
console.error(`Block type "${type}" doesn't support text editing.`);
|
|
104
|
+
console.error('Supported types: paragraph, headings, lists, to_do, quote, callout, toggle, code');
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
// ─── block-delete ─────────────────────────────────────────────────────────
|
|
110
|
+
program
|
|
111
|
+
.command('block-delete <block-id>')
|
|
112
|
+
.description('Delete a block from a page')
|
|
113
|
+
.action(async (blockId, opts, cmd) => runCommand('Block delete', async () => {
|
|
114
|
+
const notion = getNotion();
|
|
115
|
+
const res = await notion.blocks.delete({ block_id: blockId });
|
|
116
|
+
if (jsonOutput(cmd, res)) return;
|
|
117
|
+
console.log(`🗑️ Deleted block: ${blockId.slice(0, 8)}…`);
|
|
118
|
+
}));
|
|
119
|
+
|
|
120
|
+
// ─── append ──────────────────────────────────────────────────────────────
|
|
121
|
+
program
|
|
122
|
+
.command('append <page-or-alias> <text>')
|
|
123
|
+
.description('Append a text block to a page by ID or alias + filter')
|
|
124
|
+
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
125
|
+
.action(async (target, text, opts, cmd) => runCommand('Append', async () => {
|
|
126
|
+
const notion = getNotion();
|
|
127
|
+
const { pageId } = await resolvePageId(target, opts.filter);
|
|
128
|
+
const res = await notion.blocks.children.append({
|
|
129
|
+
block_id: pageId,
|
|
130
|
+
children: [{
|
|
131
|
+
object: 'block',
|
|
132
|
+
type: 'paragraph',
|
|
133
|
+
paragraph: {
|
|
134
|
+
rich_text: [{ text: { content: text } }],
|
|
135
|
+
},
|
|
136
|
+
}],
|
|
137
|
+
});
|
|
138
|
+
if (jsonOutput(cmd, res)) return;
|
|
139
|
+
console.log(`✅ Appended text block to page ${pageId}`);
|
|
140
|
+
}));
|
|
141
|
+
},
|
|
142
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
register(program, ctx) {
|
|
3
|
+
const {
|
|
4
|
+
getNotion,
|
|
5
|
+
resolvePageId,
|
|
6
|
+
paginate,
|
|
7
|
+
jsonOutput,
|
|
8
|
+
richTextToPlain,
|
|
9
|
+
printTable,
|
|
10
|
+
runCommand,
|
|
11
|
+
} = ctx;
|
|
12
|
+
|
|
13
|
+
// ─── comments ────────────────────────────────────────────────────────────
|
|
14
|
+
program
|
|
15
|
+
.command('comments <page-or-alias>')
|
|
16
|
+
.description('List comments on a page by ID or alias + filter')
|
|
17
|
+
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
18
|
+
.action(async (target, opts, cmd) => runCommand('Comments', async () => {
|
|
19
|
+
const notion = getNotion();
|
|
20
|
+
const { pageId } = await resolvePageId(target, opts.filter);
|
|
21
|
+
const { results, response } = await paginate(
|
|
22
|
+
({ start_cursor, page_size }) => notion.comments.list({
|
|
23
|
+
block_id: pageId,
|
|
24
|
+
start_cursor,
|
|
25
|
+
page_size,
|
|
26
|
+
}),
|
|
27
|
+
{ pageSizeLimit: 100 },
|
|
28
|
+
);
|
|
29
|
+
if (jsonOutput(cmd, response)) return;
|
|
30
|
+
if (results.length === 0) {
|
|
31
|
+
console.log('(no comments)');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const rows = results.map(c => ({
|
|
35
|
+
id: c.id,
|
|
36
|
+
text: richTextToPlain(c.rich_text),
|
|
37
|
+
created: c.created_time || '',
|
|
38
|
+
author: c.created_by?.name || c.created_by?.id || '',
|
|
39
|
+
}));
|
|
40
|
+
printTable(rows, ['id', 'text', 'created', 'author']);
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
// ─── comment ─────────────────────────────────────────────────────────────
|
|
44
|
+
program
|
|
45
|
+
.command('comment <page-or-alias> <text>')
|
|
46
|
+
.description('Add a comment to a page by ID or alias + filter')
|
|
47
|
+
.option('--filter <key=value>', 'Filter to find the page (required when using an alias)')
|
|
48
|
+
.action(async (target, text, opts, cmd) => runCommand('Comment', async () => {
|
|
49
|
+
const notion = getNotion();
|
|
50
|
+
const { pageId } = await resolvePageId(target, opts.filter);
|
|
51
|
+
const res = await notion.comments.create({
|
|
52
|
+
parent: { page_id: pageId },
|
|
53
|
+
rich_text: [{ text: { content: text } }],
|
|
54
|
+
});
|
|
55
|
+
if (jsonOutput(cmd, res)) return;
|
|
56
|
+
console.log(`✅ Comment added: ${res.id}`);
|
|
57
|
+
}));
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
register(program, ctx) {
|
|
3
|
+
const {
|
|
4
|
+
CONFIG_PATH,
|
|
5
|
+
loadConfig,
|
|
6
|
+
saveConfig,
|
|
7
|
+
getWorkspaceName,
|
|
8
|
+
getWorkspaceConfig,
|
|
9
|
+
getNotion,
|
|
10
|
+
createNotionClient,
|
|
11
|
+
richTextToPlain,
|
|
12
|
+
printTable,
|
|
13
|
+
runCommand,
|
|
14
|
+
} = ctx;
|
|
15
|
+
|
|
16
|
+
// ─── init ──────────────────────────────────────────────────────────────────
|
|
17
|
+
program
|
|
18
|
+
.command('init')
|
|
19
|
+
.description('Initialize notioncli with your API key and discover databases')
|
|
20
|
+
.option('--key <api-key>', 'Notion integration API key (starts with ntn_)')
|
|
21
|
+
.action(async (opts) => runCommand('Init', async () => {
|
|
22
|
+
const config = loadConfig();
|
|
23
|
+
const wsName = getWorkspaceName() || config.activeWorkspace || 'default';
|
|
24
|
+
const apiKey = opts.key || process.env.NOTION_API_KEY;
|
|
25
|
+
|
|
26
|
+
if (!apiKey) {
|
|
27
|
+
console.error('Error: Provide an API key with --key or set NOTION_API_KEY env var.');
|
|
28
|
+
console.error('');
|
|
29
|
+
console.error('To create an integration:');
|
|
30
|
+
console.error(' 1. Go to https://www.notion.so/profile/integrations');
|
|
31
|
+
console.error(' 2. Click "New integration"');
|
|
32
|
+
console.error(' 3. Copy the API key (starts with ntn_)');
|
|
33
|
+
console.error(' 4. Share your databases with the integration');
|
|
34
|
+
console.error('');
|
|
35
|
+
console.error('Then run: notion init --key ntn_your_api_key');
|
|
36
|
+
console.error(' Or with workspace: notion init --workspace work --key ntn_your_api_key');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!config.workspaces) config.workspaces = {};
|
|
41
|
+
if (!config.workspaces[wsName]) config.workspaces[wsName] = { aliases: {} };
|
|
42
|
+
config.workspaces[wsName].apiKey = apiKey;
|
|
43
|
+
config.activeWorkspace = wsName;
|
|
44
|
+
saveConfig(config);
|
|
45
|
+
console.log(`✅ API key saved to workspace "${wsName}" in ${CONFIG_PATH}`);
|
|
46
|
+
console.log('');
|
|
47
|
+
|
|
48
|
+
// Discover databases
|
|
49
|
+
const notion = createNotionClient(apiKey);
|
|
50
|
+
try {
|
|
51
|
+
const res = await notion.search({
|
|
52
|
+
filter: { value: 'data_source', property: 'object' },
|
|
53
|
+
page_size: 100,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (res.results.length === 0) {
|
|
57
|
+
console.log('No databases found. Make sure you\'ve shared databases with your integration.');
|
|
58
|
+
console.log('In Notion: open a database → ••• menu → Connections → Add your integration');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const aliases = config.workspaces[wsName].aliases || {};
|
|
63
|
+
|
|
64
|
+
console.log(`Found ${res.results.length} database${res.results.length !== 1 ? 's' : ''}:\n`);
|
|
65
|
+
|
|
66
|
+
const added = [];
|
|
67
|
+
for (const db of res.results) {
|
|
68
|
+
const title = richTextToPlain(db.title) || '';
|
|
69
|
+
const dsId = db.id;
|
|
70
|
+
const dbId = (db.parent && db.parent.type === 'database_id' && db.parent.database_id) || db.database_id || dsId;
|
|
71
|
+
|
|
72
|
+
// Auto-generate a slug from the title
|
|
73
|
+
let slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 30);
|
|
74
|
+
if (!slug) slug = `db-${dsId.slice(0, 8)}`;
|
|
75
|
+
|
|
76
|
+
// Avoid collisions — append a number if needed
|
|
77
|
+
let finalSlug = slug;
|
|
78
|
+
let counter = 2;
|
|
79
|
+
while (aliases[finalSlug] && aliases[finalSlug].data_source_id !== dsId) {
|
|
80
|
+
finalSlug = `${slug}-${counter}`;
|
|
81
|
+
counter++;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
aliases[finalSlug] = {
|
|
85
|
+
database_id: dbId,
|
|
86
|
+
data_source_id: dsId,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
console.log(` ✅ ${finalSlug.padEnd(25)} → ${title || '(untitled)'}`);
|
|
90
|
+
added.push(finalSlug);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
config.workspaces[wsName].aliases = aliases;
|
|
94
|
+
saveConfig(config);
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log(`${added.length} alias${added.length !== 1 ? 'es' : ''} saved to workspace "${wsName}".`);
|
|
97
|
+
console.log('');
|
|
98
|
+
console.log('Ready! Try:');
|
|
99
|
+
if (added.length > 0) {
|
|
100
|
+
console.log(` notion query ${added[0]}`);
|
|
101
|
+
console.log(` notion add ${added[0]} --prop "Name=Hello World"`);
|
|
102
|
+
}
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log('Manage aliases:');
|
|
105
|
+
console.log(' notion alias list — see all aliases');
|
|
106
|
+
console.log(' notion alias rename <old> <new> — rename an alias');
|
|
107
|
+
console.log(' notion alias remove <name> — remove an alias');
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(`Failed to discover databases: ${err.message}`);
|
|
110
|
+
console.error('Your API key was saved. You can add databases manually with: notion alias add <name> <id>');
|
|
111
|
+
}
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
// ─── alias ─────────────────────────────────────────────────────────────────
|
|
115
|
+
const alias = program
|
|
116
|
+
.command('alias')
|
|
117
|
+
.description('Manage database aliases for quick access');
|
|
118
|
+
|
|
119
|
+
alias
|
|
120
|
+
.command('add <name> <database-id>')
|
|
121
|
+
.description('Add a database alias (auto-discovers data_source_id)')
|
|
122
|
+
.action(async (name, databaseId) => runCommand('Alias add', async () => {
|
|
123
|
+
const config = loadConfig();
|
|
124
|
+
const wsName = getWorkspaceName() || config.activeWorkspace || 'default';
|
|
125
|
+
if (!config.workspaces[wsName]) config.workspaces[wsName] = { aliases: {} };
|
|
126
|
+
const aliases = config.workspaces[wsName].aliases || {};
|
|
127
|
+
|
|
128
|
+
// Try to discover the data_source_id by searching for this database
|
|
129
|
+
const notion = getNotion();
|
|
130
|
+
let dataSourceId = databaseId;
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const res = await notion.search({
|
|
134
|
+
filter: { value: 'data_source', property: 'object' },
|
|
135
|
+
page_size: 100,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const match = res.results.find(db => {
|
|
139
|
+
// Match by data_source_id or database_id
|
|
140
|
+
return db.id === databaseId ||
|
|
141
|
+
db.id.replace(/-/g, '') === databaseId.replace(/-/g, '');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (match) {
|
|
145
|
+
dataSourceId = match.id;
|
|
146
|
+
// The database_id might differ from data_source_id — check parent
|
|
147
|
+
const dbId = (match.parent && match.parent.type === 'database_id' && match.parent.database_id) || match.database_id || databaseId;
|
|
148
|
+
aliases[name] = {
|
|
149
|
+
database_id: dbId,
|
|
150
|
+
data_source_id: dataSourceId,
|
|
151
|
+
};
|
|
152
|
+
const title = richTextToPlain(match.title) || '(untitled)';
|
|
153
|
+
console.log(`✅ Added alias "${name}" → ${title}`);
|
|
154
|
+
console.log(` database_id: ${dbId}`);
|
|
155
|
+
console.log(` data_source_id: ${dataSourceId}`);
|
|
156
|
+
} else {
|
|
157
|
+
// Couldn't find via search — use the ID for both
|
|
158
|
+
aliases[name] = {
|
|
159
|
+
database_id: databaseId,
|
|
160
|
+
data_source_id: databaseId,
|
|
161
|
+
};
|
|
162
|
+
console.log(`✅ Added alias "${name}" → ${databaseId}`);
|
|
163
|
+
console.log(' (Could not auto-discover data_source_id — using same ID for both)');
|
|
164
|
+
}
|
|
165
|
+
} catch (err) {
|
|
166
|
+
// Fallback: use same ID for both
|
|
167
|
+
aliases[name] = {
|
|
168
|
+
database_id: databaseId,
|
|
169
|
+
data_source_id: databaseId,
|
|
170
|
+
};
|
|
171
|
+
console.log(`✅ Added alias "${name}" → ${databaseId}`);
|
|
172
|
+
console.log(` (Auto-discovery failed: ${err.message})`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
config.workspaces[wsName].aliases = aliases;
|
|
176
|
+
saveConfig(config);
|
|
177
|
+
}));
|
|
178
|
+
|
|
179
|
+
alias
|
|
180
|
+
.command('list')
|
|
181
|
+
.description('Show all configured database aliases')
|
|
182
|
+
.action(() => {
|
|
183
|
+
const ws = getWorkspaceConfig();
|
|
184
|
+
const aliases = ws.aliases || {};
|
|
185
|
+
const names = Object.keys(aliases);
|
|
186
|
+
|
|
187
|
+
if (names.length === 0) {
|
|
188
|
+
console.log(`No aliases in workspace "${ws.name}".`);
|
|
189
|
+
console.log('Add one with: notion alias add <name> <database-id>');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log(`Workspace: ${ws.name}\n`);
|
|
194
|
+
const rows = names.map(name => ({
|
|
195
|
+
alias: name,
|
|
196
|
+
database_id: aliases[name].database_id,
|
|
197
|
+
data_source_id: aliases[name].data_source_id,
|
|
198
|
+
}));
|
|
199
|
+
printTable(rows, ['alias', 'database_id', 'data_source_id']);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
alias
|
|
203
|
+
.command('remove <name>')
|
|
204
|
+
.description('Remove a database alias')
|
|
205
|
+
.action((name) => {
|
|
206
|
+
const config = loadConfig();
|
|
207
|
+
const wsName = getWorkspaceName() || config.activeWorkspace || 'default';
|
|
208
|
+
const aliases = config.workspaces[wsName]?.aliases || {};
|
|
209
|
+
if (!aliases[name]) {
|
|
210
|
+
console.error(`Alias "${name}" not found in workspace "${wsName}".`);
|
|
211
|
+
const names = Object.keys(aliases);
|
|
212
|
+
if (names.length > 0) {
|
|
213
|
+
console.error(`Available: ${names.join(', ')}`);
|
|
214
|
+
}
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
delete aliases[name];
|
|
218
|
+
config.workspaces[wsName].aliases = aliases;
|
|
219
|
+
saveConfig(config);
|
|
220
|
+
console.log(`✅ Removed alias "${name}" from workspace "${wsName}"`);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
alias
|
|
224
|
+
.command('rename <old-name> <new-name>')
|
|
225
|
+
.description('Rename a database alias')
|
|
226
|
+
.action((oldName, newName) => {
|
|
227
|
+
const config = loadConfig();
|
|
228
|
+
const wsName = getWorkspaceName() || config.activeWorkspace || 'default';
|
|
229
|
+
const aliases = config.workspaces[wsName]?.aliases || {};
|
|
230
|
+
if (!aliases[oldName]) {
|
|
231
|
+
console.error(`Alias "${oldName}" not found in workspace "${wsName}".`);
|
|
232
|
+
const names = Object.keys(aliases);
|
|
233
|
+
if (names.length > 0) {
|
|
234
|
+
console.error(`Available: ${names.join(', ')}`);
|
|
235
|
+
}
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
if (aliases[newName]) {
|
|
239
|
+
console.error(`Alias "${newName}" already exists. Remove it first or pick a different name.`);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
aliases[newName] = aliases[oldName];
|
|
243
|
+
delete aliases[oldName];
|
|
244
|
+
config.workspaces[wsName].aliases = aliases;
|
|
245
|
+
saveConfig(config);
|
|
246
|
+
console.log(`✅ Renamed "${oldName}" → "${newName}" in workspace "${wsName}"`);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// ─── workspace ─────────────────────────────────────────────────────────────
|
|
250
|
+
const workspace = program
|
|
251
|
+
.command('workspace')
|
|
252
|
+
.description('Manage workspace profiles (multiple Notion accounts)');
|
|
253
|
+
|
|
254
|
+
workspace
|
|
255
|
+
.command('add <name>')
|
|
256
|
+
.description('Add a new workspace profile')
|
|
257
|
+
.requiredOption('--key <api-key>', 'Notion API key for this workspace')
|
|
258
|
+
.action(async (name, opts) => runCommand('Workspace add', async () => {
|
|
259
|
+
const config = loadConfig();
|
|
260
|
+
if (config.workspaces[name]) {
|
|
261
|
+
console.error(`Workspace "${name}" already exists. Use "notion init --workspace ${name} --key ..." to update it.`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
config.workspaces[name] = { apiKey: opts.key, aliases: {} };
|
|
265
|
+
saveConfig(config);
|
|
266
|
+
console.log(`✅ Added workspace "${name}"`);
|
|
267
|
+
console.log('');
|
|
268
|
+
console.log(`Discover databases: notion init --workspace ${name}`);
|
|
269
|
+
console.log(`Or set as active: notion workspace use ${name}`);
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
workspace
|
|
273
|
+
.command('list')
|
|
274
|
+
.description('List all workspace profiles')
|
|
275
|
+
.action(() => {
|
|
276
|
+
const config = loadConfig();
|
|
277
|
+
const names = Object.keys(config.workspaces || {});
|
|
278
|
+
if (names.length === 0) {
|
|
279
|
+
console.log('No workspaces configured. Run: notion init --key ntn_...');
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
for (const name of names) {
|
|
283
|
+
const ws = config.workspaces[name];
|
|
284
|
+
const active = name === config.activeWorkspace ? ' ← active' : '';
|
|
285
|
+
const aliasCount = Object.keys(ws.aliases || {}).length;
|
|
286
|
+
const keyPreview = ws.apiKey ? `${ws.apiKey.slice(0, 8)}...` : '(no key)';
|
|
287
|
+
console.log(` ${name}${active}`);
|
|
288
|
+
console.log(` Key: ${keyPreview} | Aliases: ${aliasCount}`);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
workspace
|
|
293
|
+
.command('use <name>')
|
|
294
|
+
.description('Set the active workspace')
|
|
295
|
+
.action((name) => {
|
|
296
|
+
const config = loadConfig();
|
|
297
|
+
if (!config.workspaces[name]) {
|
|
298
|
+
console.error(`Workspace "${name}" not found.`);
|
|
299
|
+
const names = Object.keys(config.workspaces || {});
|
|
300
|
+
if (names.length > 0) {
|
|
301
|
+
console.error(`Available: ${names.join(', ')}`);
|
|
302
|
+
}
|
|
303
|
+
process.exit(1);
|
|
304
|
+
}
|
|
305
|
+
config.activeWorkspace = name;
|
|
306
|
+
saveConfig(config);
|
|
307
|
+
console.log(`✅ Active workspace: ${name}`);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
workspace
|
|
311
|
+
.command('remove <name>')
|
|
312
|
+
.description('Remove a workspace profile')
|
|
313
|
+
.action((name) => {
|
|
314
|
+
const config = loadConfig();
|
|
315
|
+
if (!config.workspaces[name]) {
|
|
316
|
+
console.error(`Workspace "${name}" not found.`);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
if (name === config.activeWorkspace) {
|
|
320
|
+
console.error('Cannot remove the active workspace. Switch first: notion workspace use <other>');
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
delete config.workspaces[name];
|
|
324
|
+
saveConfig(config);
|
|
325
|
+
console.log(`✅ Removed workspace "${name}"`);
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
};
|