@eightstate/escli 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/notion/block/trash.js +71 -0
- package/dist/commands/notion/comments/add.js +48 -0
- package/dist/commands/notion/comments/get.js +42 -0
- package/dist/commands/notion/comments/list.js +55 -0
- package/dist/commands/notion/comments/reply.js +45 -0
- package/dist/commands/notion/comments/thread.js +87 -0
- package/dist/commands/notion/db/create.js +555 -0
- package/dist/commands/notion/db/query.js +451 -0
- package/dist/commands/notion/db/row/create.js +74 -0
- package/dist/commands/notion/db/row/update.js +165 -0
- package/dist/commands/notion/ds/create.js +73 -0
- package/dist/commands/notion/enroll.js +302 -0
- package/dist/commands/notion/index.js +24 -0
- package/dist/commands/notion/page/edit.js +73 -0
- package/dist/commands/notion/page/move.js +59 -0
- package/dist/commands/notion/page/read.js +60 -0
- package/dist/commands/notion/page/replace-content.js +80 -0
- package/dist/commands/notion/page/replace-text.js +80 -0
- package/dist/commands/notion/page/replace.js +63 -0
- package/dist/commands/notion/page/trash.js +79 -0
- package/dist/commands/notion/search.js +207 -0
- package/dist/commands/notion/upload/attach.js +105 -0
- package/dist/commands/notion/upload/index.js +129 -0
- package/dist/commands/notion/upload/list.js +78 -0
- package/dist/commands/notion/upload/status.js +76 -0
- package/dist/commands/notion/view/create.js +78 -0
- package/dist/commands/notion/whoami.js +96 -0
- package/dist/commands/research.js +11 -0
- package/dist/entry.js +12 -6
- package/dist/io/render-kv.js +12 -0
- package/dist/io/render-labeled.js +16 -0
- package/dist/io/render-table.js +19 -0
- package/dist/io/render-trailer.js +7 -0
- package/dist/lib/manifest.js +1 -1
- package/dist/lib/notion/comments/shared.js +366 -0
- package/dist/lib/notion/db-row/common.js +367 -0
- package/dist/lib/notion/manifest-pass.js +4 -0
- package/dist/lib/notion/page/content-common.js +473 -0
- package/dist/lib/notion/trash-move/support.js +300 -0
- package/dist/lib/notion/upload/shared.js +372 -0
- package/dist/lib/registry.js +118 -25
- package/dist/services/notion.js +274 -0
- package/dist/services/research.js +31 -10
- package/oclif.manifest.json +4084 -1
- package/package.json +22 -17
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { Errors } from '@oclif/core';
|
|
3
|
+
import { ErrorCode } from '@eightstate/contracts/errors';
|
|
4
|
+
import { ExitCodes } from '@eightstate/contracts/exit-codes';
|
|
5
|
+
import { EscliError } from '../../escli-error.js';
|
|
6
|
+
import { NOTION_VERSION } from '../../../services/notion.js';
|
|
7
|
+
import { writeStdout, writeStderr } from '../../../io/io.js';
|
|
8
|
+
import { renderKv } from '../../../io/render-kv.js';
|
|
9
|
+
import { renderTable } from '../../../io/render-table.js';
|
|
10
|
+
export const dbWriteErrors = [
|
|
11
|
+
ErrorCode.UsageInvalid,
|
|
12
|
+
ErrorCode.UsageRequired,
|
|
13
|
+
ErrorCode.ValidationFailed,
|
|
14
|
+
ErrorCode.JsonInvalid,
|
|
15
|
+
ErrorCode.FileNotFound,
|
|
16
|
+
ErrorCode.FileReadFailed,
|
|
17
|
+
ErrorCode.AuthRequired,
|
|
18
|
+
ErrorCode.NotionUnauthorized,
|
|
19
|
+
ErrorCode.NotionEnrollmentRequired,
|
|
20
|
+
ErrorCode.NotionRestricted,
|
|
21
|
+
ErrorCode.NotionNotFound,
|
|
22
|
+
ErrorCode.NotionRateLimited,
|
|
23
|
+
ErrorCode.NotionValidation,
|
|
24
|
+
ErrorCode.NotionConflict,
|
|
25
|
+
ErrorCode.NotionTimeoutUncertain,
|
|
26
|
+
ErrorCode.NotionBatchPartial,
|
|
27
|
+
ErrorCode.GateInvalidResponse,
|
|
28
|
+
ErrorCode.GateCredentialUnavailable,
|
|
29
|
+
ErrorCode.ApiError,
|
|
30
|
+
ErrorCode.NetworkError,
|
|
31
|
+
ErrorCode.NetworkTimeout,
|
|
32
|
+
ErrorCode.ServiceUnavailable,
|
|
33
|
+
];
|
|
34
|
+
export function parseJsonObject(value, label) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(value);
|
|
37
|
+
if (!isRecord(parsed))
|
|
38
|
+
throw new Error(`${label} must be a JSON object`);
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
throw new EscliError(error instanceof Error ? error.message : `invalid ${label}`, {
|
|
43
|
+
code: ErrorCode.JsonInvalid,
|
|
44
|
+
exitCode: ExitCodes.Usage,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function readJsonObjectInput(value, label) {
|
|
49
|
+
if (!value) {
|
|
50
|
+
throw new EscliError(`${label} is required`, { code: ErrorCode.UsageRequired, exitCode: ExitCodes.Usage });
|
|
51
|
+
}
|
|
52
|
+
if (value === '-')
|
|
53
|
+
return parseJsonObject(await readStdin(), label);
|
|
54
|
+
if (value.startsWith('@'))
|
|
55
|
+
return parseJsonObject(await readTextFile(value.slice(1)), label);
|
|
56
|
+
return parseJsonObject(value, label);
|
|
57
|
+
}
|
|
58
|
+
export async function readIdsFileRows(path) {
|
|
59
|
+
const content = path === '-' ? await readStdin() : await readTextFile(path);
|
|
60
|
+
return content.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map((line, index) => {
|
|
61
|
+
const row = parseJsonObject(line, `ids-file line ${index + 1}`);
|
|
62
|
+
if (typeof row.id !== 'string' || row.id.length === 0) {
|
|
63
|
+
throw new EscliError(`ids-file line ${index + 1} requires string id`, { code: ErrorCode.ValidationFailed, exitCode: ExitCodes.Usage });
|
|
64
|
+
}
|
|
65
|
+
if (!isRecord(row.properties)) {
|
|
66
|
+
throw new EscliError(`ids-file line ${index + 1} requires properties object`, { code: ErrorCode.ValidationFailed, exitCode: ExitCodes.Usage });
|
|
67
|
+
}
|
|
68
|
+
return { id: row.id, properties: row.properties };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export function rawOutput(result) {
|
|
72
|
+
if (isRecord(result) && 'raw' in result)
|
|
73
|
+
return result.raw;
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
export function normalizeCreate(result, written) {
|
|
77
|
+
const page = pageRecord(result.data);
|
|
78
|
+
return {
|
|
79
|
+
action: 'created',
|
|
80
|
+
page: { id: stringValue(page.id), title: extractTitle(page), url: stringValue(page.url) },
|
|
81
|
+
parent: extractParent(page),
|
|
82
|
+
written: normalizeWritten(written),
|
|
83
|
+
bot: extractBot(page),
|
|
84
|
+
last_edited_time: stringValue(page.last_edited_time),
|
|
85
|
+
raw: result.raw,
|
|
86
|
+
meta: { next: null, notion_version: NOTION_VERSION },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function normalizeUpdate(result, beforeRaw, requested) {
|
|
90
|
+
const before = pageRecord(beforeRaw);
|
|
91
|
+
const after = pageRecord(result.data);
|
|
92
|
+
const changes = buildChanges(before, after, requested);
|
|
93
|
+
const changedNames = new Set(changes.map((change) => change.property));
|
|
94
|
+
return {
|
|
95
|
+
action: 'updated',
|
|
96
|
+
page: { id: stringValue(after.id), title: extractTitle(after), url: stringValue(after.url) },
|
|
97
|
+
changes,
|
|
98
|
+
unchanged: Object.keys(requested).filter((name) => !changedNames.has(name)),
|
|
99
|
+
last_edited_time: stringValue(after.last_edited_time),
|
|
100
|
+
bot: extractBot(after),
|
|
101
|
+
raw: { before: beforeRaw, after: result.raw },
|
|
102
|
+
meta: { next: null, notion_version: NOTION_VERSION },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function renderCreate(data) {
|
|
106
|
+
return renderKv([
|
|
107
|
+
['action', data.action],
|
|
108
|
+
['page_id', data.page.id],
|
|
109
|
+
['title', data.page.title],
|
|
110
|
+
['url', data.page.url],
|
|
111
|
+
['data_source_id', data.parent.data_source_id],
|
|
112
|
+
['database_id', data.parent.database_id ?? ''],
|
|
113
|
+
['written', formatPropertyList(data.written)],
|
|
114
|
+
['bot', data.bot],
|
|
115
|
+
['last_edited_time', data.last_edited_time],
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
export function renderUpdate(data) {
|
|
119
|
+
if ('rows' in data)
|
|
120
|
+
return renderBatch(data);
|
|
121
|
+
const lines = [renderKv([
|
|
122
|
+
['action', data.action],
|
|
123
|
+
['page_id', data.page.id],
|
|
124
|
+
['title', data.page.title],
|
|
125
|
+
['url', data.page.url],
|
|
126
|
+
])];
|
|
127
|
+
lines.push('changes:');
|
|
128
|
+
if (data.changes.length === 0)
|
|
129
|
+
lines.push(' none');
|
|
130
|
+
for (const change of data.changes)
|
|
131
|
+
lines.push(` ${change.property}: ${formatScalar(change.from)} → ${formatScalar(change.to)}${change.cleared ? ' (cleared)' : ''}`);
|
|
132
|
+
lines.push(`unchanged: ${data.unchanged.length > 0 ? data.unchanged.join(', ') : 'none'}`);
|
|
133
|
+
lines.push(renderKv([
|
|
134
|
+
['bot', data.bot],
|
|
135
|
+
['last_edited_time', data.last_edited_time],
|
|
136
|
+
]));
|
|
137
|
+
return lines.join('\n');
|
|
138
|
+
}
|
|
139
|
+
export function renderBatch(data) {
|
|
140
|
+
return [
|
|
141
|
+
`action: ${data.action}`,
|
|
142
|
+
`rows: ${data.rows_count} ok=${data.ok_count} err=${data.err_count}`,
|
|
143
|
+
renderTable({
|
|
144
|
+
header: ['#', 'id', 'status', 'reason'],
|
|
145
|
+
rows: data.rows.map((row) => [row.index, abbreviateId(row.id), row.status, row.reason]),
|
|
146
|
+
}),
|
|
147
|
+
`bot: ${data.bot}`,
|
|
148
|
+
].join('\n');
|
|
149
|
+
}
|
|
150
|
+
export function stripCommandFields(data) {
|
|
151
|
+
const rest = { ...data };
|
|
152
|
+
delete rest.raw;
|
|
153
|
+
delete rest.meta;
|
|
154
|
+
return rest;
|
|
155
|
+
}
|
|
156
|
+
export async function emitSuccessEnvelope(command, version, data, durationMs) {
|
|
157
|
+
await writeStdout(`${JSON.stringify({ ok: true, data, error: null, meta: notionEnvelopeMeta(command, version, durationMs) })}\n`);
|
|
158
|
+
}
|
|
159
|
+
export async function emitErrorEnvelope(command, version, error, durationMs, json) {
|
|
160
|
+
process.exitCode = error.exitCode;
|
|
161
|
+
if (json) {
|
|
162
|
+
await writeStdout(`${JSON.stringify({
|
|
163
|
+
ok: false,
|
|
164
|
+
data: null,
|
|
165
|
+
error: {
|
|
166
|
+
code: error.code,
|
|
167
|
+
message: error.message,
|
|
168
|
+
remediation: error.remediation,
|
|
169
|
+
details: error.details,
|
|
170
|
+
cause_code: error.causeCode,
|
|
171
|
+
},
|
|
172
|
+
meta: notionEnvelopeMeta(command, version, durationMs),
|
|
173
|
+
})}\n`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
await writeStderr(`${error.message}\n`);
|
|
177
|
+
}
|
|
178
|
+
export function toEscliError(error) {
|
|
179
|
+
if (error instanceof EscliError)
|
|
180
|
+
return error;
|
|
181
|
+
if (error instanceof Errors.CLIError) {
|
|
182
|
+
return new EscliError(cleanOclifMessage(error.message), { code: classifyOclifUsageError(error), exitCode: ExitCodes.Usage, details: { oclif_exit: error.oclif?.exit } });
|
|
183
|
+
}
|
|
184
|
+
if (error instanceof Error)
|
|
185
|
+
return new EscliError(error.message, { code: ErrorCode.Internal });
|
|
186
|
+
return new EscliError('unknown error', { code: ErrorCode.Unknown, details: error });
|
|
187
|
+
}
|
|
188
|
+
function classifyOclifUsageError(error) {
|
|
189
|
+
const name = error.constructor.name;
|
|
190
|
+
const message = error.message;
|
|
191
|
+
if (name === 'NonExistentFlagsError' || message.startsWith('Nonexistent flag') || message.startsWith('Unexpected flag'))
|
|
192
|
+
return ErrorCode.UsageUnknownFlag;
|
|
193
|
+
if (name === 'RequiredArgsError' || message.startsWith('Missing ') || message.includes('expects a value'))
|
|
194
|
+
return ErrorCode.UsageRequired;
|
|
195
|
+
return ErrorCode.UsageInvalid;
|
|
196
|
+
}
|
|
197
|
+
function cleanOclifMessage(message) {
|
|
198
|
+
return message.replace(/\nSee more help with --help$/u, '');
|
|
199
|
+
}
|
|
200
|
+
export function notionEnvelopeMeta(command, version, durationMs) {
|
|
201
|
+
return {
|
|
202
|
+
schema_version: '1',
|
|
203
|
+
escli_version: version,
|
|
204
|
+
command,
|
|
205
|
+
warnings: [],
|
|
206
|
+
duration_ms: durationMs,
|
|
207
|
+
next: null,
|
|
208
|
+
notion_version: NOTION_VERSION,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
export function isRecord(value) {
|
|
212
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
213
|
+
}
|
|
214
|
+
function pageRecord(value) {
|
|
215
|
+
return isRecord(value) ? value : {};
|
|
216
|
+
}
|
|
217
|
+
function extractParent(page) {
|
|
218
|
+
const parent = isRecord(page.parent) ? page.parent : {};
|
|
219
|
+
return { data_source_id: stringValue(parent.data_source_id), database_id: nullableString(parent.database_id) };
|
|
220
|
+
}
|
|
221
|
+
function extractBot(page) {
|
|
222
|
+
const lastEditedBy = isRecord(page.last_edited_by) ? page.last_edited_by : {};
|
|
223
|
+
const createdBy = isRecord(page.created_by) ? page.created_by : {};
|
|
224
|
+
return stringValue(lastEditedBy.name) || stringValue(createdBy.name) || stringValue(lastEditedBy.id) || stringValue(createdBy.id);
|
|
225
|
+
}
|
|
226
|
+
function extractTitle(page) {
|
|
227
|
+
const title = stringValue(page.title);
|
|
228
|
+
if (title)
|
|
229
|
+
return title;
|
|
230
|
+
const properties = isRecord(page.properties) ? page.properties : {};
|
|
231
|
+
for (const property of Object.values(properties)) {
|
|
232
|
+
if (!isRecord(property) || property.type !== 'title' || !Array.isArray(property.title))
|
|
233
|
+
continue;
|
|
234
|
+
const plain = property.title.map((item) => isRecord(item) ? stringValue(item.plain_text) : '').join('');
|
|
235
|
+
if (plain)
|
|
236
|
+
return plain;
|
|
237
|
+
}
|
|
238
|
+
return '';
|
|
239
|
+
}
|
|
240
|
+
function buildChanges(before, after, requested) {
|
|
241
|
+
const beforeProps = isRecord(before.properties) ? before.properties : {};
|
|
242
|
+
const afterProps = isRecord(after.properties) ? after.properties : {};
|
|
243
|
+
const changes = [];
|
|
244
|
+
for (const [name, requestedValue] of Object.entries(requested)) {
|
|
245
|
+
const beforeProperty = beforeProps[name];
|
|
246
|
+
const afterProperty = afterProps[name];
|
|
247
|
+
const type = propertyType(afterProperty) ?? propertyType(beforeProperty);
|
|
248
|
+
const from = simplifyProperty(beforeProperty);
|
|
249
|
+
const to = simplifyProperty(afterProperty, requestedValue);
|
|
250
|
+
if (!deepEqual(from, to))
|
|
251
|
+
changes.push({ property: name, type, from, to, ...(to === null ? { cleared: true } : {}) });
|
|
252
|
+
}
|
|
253
|
+
return changes;
|
|
254
|
+
}
|
|
255
|
+
function propertyType(value) {
|
|
256
|
+
return isRecord(value) && typeof value.type === 'string' ? value.type : undefined;
|
|
257
|
+
}
|
|
258
|
+
function simplifyProperty(property, fallback) {
|
|
259
|
+
if (!isRecord(property))
|
|
260
|
+
return fallback ?? null;
|
|
261
|
+
const type = propertyType(property);
|
|
262
|
+
switch (type) {
|
|
263
|
+
case 'title':
|
|
264
|
+
return richTextPlain(property.title);
|
|
265
|
+
case 'rich_text':
|
|
266
|
+
return richTextPlain(property.rich_text);
|
|
267
|
+
case 'select':
|
|
268
|
+
case 'status':
|
|
269
|
+
return isRecord(property[type]) ? stringValue(property[type].name) : null;
|
|
270
|
+
case 'multi_select':
|
|
271
|
+
return Array.isArray(property.multi_select) ? property.multi_select.map((item) => isRecord(item) ? stringValue(item.name) : '').filter(Boolean) : [];
|
|
272
|
+
case 'date':
|
|
273
|
+
return isRecord(property.date) ? stringValue(property.date.start) : null;
|
|
274
|
+
case 'people':
|
|
275
|
+
return Array.isArray(property.people) ? property.people.map((person) => isRecord(person) ? stringValue(person.name) || stringValue(person.id) : '').filter(Boolean) : [];
|
|
276
|
+
case 'files':
|
|
277
|
+
return Array.isArray(property.files) ? property.files.map((file) => isRecord(file) ? stringValue(file.name) : '').filter(Boolean) : [];
|
|
278
|
+
case 'relation':
|
|
279
|
+
return Array.isArray(property.relation) ? property.relation.map((relation) => isRecord(relation) ? stringValue(relation.id) : '').filter(Boolean) : [];
|
|
280
|
+
case 'number':
|
|
281
|
+
case 'checkbox':
|
|
282
|
+
case 'email':
|
|
283
|
+
case 'phone_number':
|
|
284
|
+
case 'url':
|
|
285
|
+
return property[type];
|
|
286
|
+
case 'verification':
|
|
287
|
+
return simplifyVerification(property.verification);
|
|
288
|
+
default:
|
|
289
|
+
return fallback ?? property;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function richTextPlain(value) {
|
|
293
|
+
return Array.isArray(value) ? value.map((item) => {
|
|
294
|
+
if (!isRecord(item))
|
|
295
|
+
return '';
|
|
296
|
+
if (typeof item.plain_text === 'string')
|
|
297
|
+
return item.plain_text;
|
|
298
|
+
const text = isRecord(item.text) ? item.text : {};
|
|
299
|
+
return stringValue(text.content);
|
|
300
|
+
}).join('') : '';
|
|
301
|
+
}
|
|
302
|
+
function simplifyVerification(value) {
|
|
303
|
+
if (!isRecord(value))
|
|
304
|
+
return value ?? null;
|
|
305
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== null && entry !== undefined));
|
|
306
|
+
}
|
|
307
|
+
function normalizeWritten(properties) {
|
|
308
|
+
return Object.fromEntries(Object.entries(properties).map(([name, value]) => [name, simplifyInputValue(value)]));
|
|
309
|
+
}
|
|
310
|
+
function simplifyInputValue(value) {
|
|
311
|
+
if (!isRecord(value))
|
|
312
|
+
return value;
|
|
313
|
+
const type = Object.keys(value)[0];
|
|
314
|
+
if (!type)
|
|
315
|
+
return value;
|
|
316
|
+
return simplifyProperty({ type, [type]: value[type] }, value);
|
|
317
|
+
}
|
|
318
|
+
function formatPropertyList(properties) {
|
|
319
|
+
return Object.entries(properties).map(([name, value]) => `${name}=${formatQuoted(value)}`).join('; ');
|
|
320
|
+
}
|
|
321
|
+
function formatQuoted(value) {
|
|
322
|
+
if (typeof value === 'string')
|
|
323
|
+
return `"${value}"`;
|
|
324
|
+
return formatScalar(value);
|
|
325
|
+
}
|
|
326
|
+
function formatScalar(value) {
|
|
327
|
+
if (value === null || value === undefined)
|
|
328
|
+
return 'null';
|
|
329
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
|
|
330
|
+
return String(value);
|
|
331
|
+
return JSON.stringify(value);
|
|
332
|
+
}
|
|
333
|
+
function abbreviateId(id) {
|
|
334
|
+
return id.length > 9 ? `${id.slice(0, 8)}…` : id;
|
|
335
|
+
}
|
|
336
|
+
function deepEqual(left, right) {
|
|
337
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
338
|
+
}
|
|
339
|
+
function stringValue(value) {
|
|
340
|
+
return typeof value === 'string' ? value : '';
|
|
341
|
+
}
|
|
342
|
+
function nullableString(value) {
|
|
343
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
344
|
+
}
|
|
345
|
+
async function readTextFile(path) {
|
|
346
|
+
try {
|
|
347
|
+
return await readFile(path, 'utf8');
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
throw new EscliError(`failed to read ${path}: ${error instanceof Error ? error.message : String(error)}`, {
|
|
351
|
+
code: ErrorCode.FileReadFailed,
|
|
352
|
+
exitCode: ExitCodes.Error,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async function readStdin() {
|
|
357
|
+
return new Promise((resolve, reject) => {
|
|
358
|
+
let data = '';
|
|
359
|
+
process.stdin.setEncoding('utf8');
|
|
360
|
+
process.stdin.on('data', (chunk) => {
|
|
361
|
+
data += chunk;
|
|
362
|
+
});
|
|
363
|
+
process.stdin.on('end', () => resolve(data));
|
|
364
|
+
process.stdin.on('error', reject);
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
//# sourceMappingURL=common.js.map
|