@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,473 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { stdin } from 'node:process';
|
|
4
|
+
import { Errors } from '@oclif/core';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { ErrorCode } from '@eightstate/contracts/errors';
|
|
7
|
+
import { ExitCodes } from '@eightstate/contracts/exit-codes';
|
|
8
|
+
import { BaseCommand } from '../../../base-command.js';
|
|
9
|
+
import { EscliError } from '../../../lib/escli-error.js';
|
|
10
|
+
import { writeStderr, writeStdout } from '../../../io/io.js';
|
|
11
|
+
import { renderKv } from '../../../io/render-kv.js';
|
|
12
|
+
import { renderTable } from '../../../io/render-table.js';
|
|
13
|
+
import { NOTION_VERSION } from '../../../services/notion.js';
|
|
14
|
+
import { hasManifestFlag } from '../manifest-pass.js';
|
|
15
|
+
export const PAGE_CONTENT_ERROR_CODES = [
|
|
16
|
+
ErrorCode.AuthRequired,
|
|
17
|
+
ErrorCode.NotionEnrollmentRequired,
|
|
18
|
+
ErrorCode.NotionUnauthorized,
|
|
19
|
+
ErrorCode.NotionRestricted,
|
|
20
|
+
ErrorCode.NotionNotFound,
|
|
21
|
+
ErrorCode.NotionRateLimited,
|
|
22
|
+
ErrorCode.NotionValidation,
|
|
23
|
+
ErrorCode.NotionConflict,
|
|
24
|
+
ErrorCode.NotionTimeoutUncertain,
|
|
25
|
+
ErrorCode.GateInvalidResponse,
|
|
26
|
+
ErrorCode.NetworkError,
|
|
27
|
+
ErrorCode.NetworkTimeout,
|
|
28
|
+
ErrorCode.ServiceUnavailable,
|
|
29
|
+
ErrorCode.ApiError,
|
|
30
|
+
ErrorCode.UsageInvalid,
|
|
31
|
+
ErrorCode.UsageRequired,
|
|
32
|
+
ErrorCode.FileNotFound,
|
|
33
|
+
ErrorCode.FileReadFailed,
|
|
34
|
+
];
|
|
35
|
+
export const PageRefArg = {
|
|
36
|
+
description: 'Notion page id or URL.',
|
|
37
|
+
required: true,
|
|
38
|
+
};
|
|
39
|
+
export const RawNotionBodySchema = z.unknown();
|
|
40
|
+
export const NotionPageContentPageSchema = z.object({
|
|
41
|
+
id: z.string(),
|
|
42
|
+
title: z.string().optional(),
|
|
43
|
+
url: z.string().optional(),
|
|
44
|
+
});
|
|
45
|
+
export const PageContentReadDataSchema = z.object({
|
|
46
|
+
surface: z.literal('page-content'),
|
|
47
|
+
command: z.literal('read'),
|
|
48
|
+
page: NotionPageContentPageSchema,
|
|
49
|
+
content: z.object({
|
|
50
|
+
format: z.literal('notion-enhanced-markdown'),
|
|
51
|
+
chars_total: z.number().int().nonnegative(),
|
|
52
|
+
preview: z.string(),
|
|
53
|
+
notion_truncated: z.boolean(),
|
|
54
|
+
unknown_block_ids: z.array(z.string()),
|
|
55
|
+
}),
|
|
56
|
+
raw: RawNotionBodySchema.optional(),
|
|
57
|
+
}).passthrough();
|
|
58
|
+
export const PageContentEditDataSchema = z.object({
|
|
59
|
+
surface: z.literal('page-content'),
|
|
60
|
+
command: z.literal('edit'),
|
|
61
|
+
page: NotionPageContentPageSchema,
|
|
62
|
+
status: z.string(),
|
|
63
|
+
ops_requested: z.number().int().nonnegative(),
|
|
64
|
+
ops_applied: z.number().int().nonnegative(),
|
|
65
|
+
replace_all: z.boolean(),
|
|
66
|
+
allow_deleting_content: z.boolean(),
|
|
67
|
+
ops: z.array(z.object({ old: z.string(), new: z.string(), status: z.string() })),
|
|
68
|
+
after_excerpt: z.string(),
|
|
69
|
+
raw: RawNotionBodySchema.optional(),
|
|
70
|
+
}).passthrough();
|
|
71
|
+
export const PageContentReplaceDataSchema = z.object({
|
|
72
|
+
surface: z.literal('page-content'),
|
|
73
|
+
command: z.literal('replace'),
|
|
74
|
+
page: NotionPageContentPageSchema,
|
|
75
|
+
status: z.string(),
|
|
76
|
+
allow_deleting_content: z.boolean(),
|
|
77
|
+
submitted: z.object({ format: z.literal('notion-enhanced-markdown'), chars: z.number().int().nonnegative(), preview: z.string() }),
|
|
78
|
+
raw: RawNotionBodySchema.optional(),
|
|
79
|
+
}).passthrough();
|
|
80
|
+
export class RawBypassCommand extends BaseCommand {
|
|
81
|
+
async run() {
|
|
82
|
+
if (hasManifestFlag(this.argv)) {
|
|
83
|
+
await super.run();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const start = Date.now();
|
|
87
|
+
let flags = {};
|
|
88
|
+
try {
|
|
89
|
+
flags = await this.parseFlags(this.ctor);
|
|
90
|
+
const data = await this.execute();
|
|
91
|
+
if (flags.raw === true || flags.json !== true)
|
|
92
|
+
return;
|
|
93
|
+
this.humanOutputHandled = true;
|
|
94
|
+
await writeStdout(`${JSON.stringify({ ok: true, data: withoutRaw(data), error: null, meta: notionCommandMeta() })}\n`);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
const escliError = normalizeError(error);
|
|
98
|
+
process.exitCode = escliError.exitCode;
|
|
99
|
+
const command = this.id ?? 'notion page';
|
|
100
|
+
if (flags.json === true) {
|
|
101
|
+
// --json contract: JSON envelope on stdout is the sole surface.
|
|
102
|
+
// No human card, no next-step text on stderr — automation parses
|
|
103
|
+
// exactly one stable stream.
|
|
104
|
+
await writeStdout(`${JSON.stringify({ ok: false, data: null, error: errorObject(escliError), meta: notionCommandMeta() })}\n`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
await writeStdout(`${renderErrorCard(command, escliError)}\n`);
|
|
108
|
+
await writeStderr(`${nextStep(escliError)}\n`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
const _durationMs = Date.now() - start;
|
|
113
|
+
void _durationMs;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
async parseFlags(command) {
|
|
118
|
+
const flags = await super.parseFlags(command);
|
|
119
|
+
if (flags.raw === true)
|
|
120
|
+
flags.json = false;
|
|
121
|
+
return flags;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function notionCommandMeta() {
|
|
125
|
+
return { next: null, notion_version: NOTION_VERSION };
|
|
126
|
+
}
|
|
127
|
+
function normalizeError(error) {
|
|
128
|
+
if (error instanceof EscliError)
|
|
129
|
+
return error;
|
|
130
|
+
if (error instanceof z.ZodError)
|
|
131
|
+
return new EscliError('input validation failed', { code: ErrorCode.ValidationFailed, exitCode: ExitCodes.Usage, details: error.issues });
|
|
132
|
+
if (error instanceof Errors.ExitError)
|
|
133
|
+
return new EscliError(error.message, { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
|
|
134
|
+
if (error instanceof Errors.CLIError)
|
|
135
|
+
return new EscliError(error.message.replace(/\nSee more help with --help$/u, ''), { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
|
|
136
|
+
if (error instanceof Error)
|
|
137
|
+
return new EscliError(error.message, { code: ErrorCode.Internal, exitCode: ExitCodes.Error });
|
|
138
|
+
return new EscliError('unknown error', { code: ErrorCode.Unknown, exitCode: ExitCodes.Error, details: error });
|
|
139
|
+
}
|
|
140
|
+
function errorObject(error) {
|
|
141
|
+
const requestId = requestIdFromDetails(error.details);
|
|
142
|
+
return {
|
|
143
|
+
code: error.code,
|
|
144
|
+
message: error.message,
|
|
145
|
+
next: nextStep(error),
|
|
146
|
+
exit: error.exitCode,
|
|
147
|
+
...(requestId ? { request_id: requestId } : {}),
|
|
148
|
+
...(error.causeCode ? { cause_code: error.causeCode } : {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function renderErrorCard(command, error) {
|
|
152
|
+
const cause = error.causeCode ?? error.code;
|
|
153
|
+
const requestId = requestIdFromDetails(error.details);
|
|
154
|
+
return renderKv([
|
|
155
|
+
['error', `${command} failed (${cause})`],
|
|
156
|
+
['reason', error.message],
|
|
157
|
+
['next', nextStep(error)],
|
|
158
|
+
['exit', error.exitCode],
|
|
159
|
+
...(requestId ? [['request_id', requestId]] : []),
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
162
|
+
function nextStep(error) {
|
|
163
|
+
if (error.remediation?.hint)
|
|
164
|
+
return error.remediation.hint;
|
|
165
|
+
if (error.remediation?.command)
|
|
166
|
+
return `run ${error.remediation.command}`;
|
|
167
|
+
switch (error.code) {
|
|
168
|
+
case ErrorCode.NotionUnauthorized:
|
|
169
|
+
case ErrorCode.NotionEnrollmentRequired:
|
|
170
|
+
case ErrorCode.AuthRequired:
|
|
171
|
+
return 'run escli notion enroll or re-enroll this workspace';
|
|
172
|
+
case ErrorCode.NotionRestricted:
|
|
173
|
+
case ErrorCode.NotionNotFound:
|
|
174
|
+
return 'share the target with the Notion connection and grant the required capability';
|
|
175
|
+
case ErrorCode.NotionValidation:
|
|
176
|
+
case ErrorCode.UsageInvalid:
|
|
177
|
+
case ErrorCode.UsageRequired:
|
|
178
|
+
case ErrorCode.ValidationFailed:
|
|
179
|
+
return 'fix input, markdown, target type, or destructive guard flag before retrying';
|
|
180
|
+
case ErrorCode.NotionRateLimited:
|
|
181
|
+
case ErrorCode.ServiceUnavailable:
|
|
182
|
+
case ErrorCode.NetworkError:
|
|
183
|
+
case ErrorCode.NetworkTimeout:
|
|
184
|
+
return 'retry reads after delay; verify write outcome before repeating writes';
|
|
185
|
+
default:
|
|
186
|
+
return 'retry once, then file bug with request id if it repeats';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function requestIdFromDetails(details) {
|
|
190
|
+
const record = recordValue(details);
|
|
191
|
+
return stringValue(record?.request_id) ?? stringValue(recordValue(record?.details)?.request_id);
|
|
192
|
+
}
|
|
193
|
+
export function pageIdFromRef(input) {
|
|
194
|
+
const trimmed = input.trim();
|
|
195
|
+
const uuid = trimmed.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/iu)?.[0];
|
|
196
|
+
if (uuid)
|
|
197
|
+
return uuid;
|
|
198
|
+
const compact = trimmed.match(/[0-9a-f]{32}/iu)?.[0];
|
|
199
|
+
if (compact)
|
|
200
|
+
return compact.replace(/^(.{8})(.{4})(.{4})(.{4})(.{12})$/u, '$1-$2-$3-$4-$5');
|
|
201
|
+
return trimmed;
|
|
202
|
+
}
|
|
203
|
+
export function pageTitle(page) {
|
|
204
|
+
return page.title && page.title.length > 0 ? page.title : 'Untitled';
|
|
205
|
+
}
|
|
206
|
+
export function pageLabel(page) {
|
|
207
|
+
return `${pageTitle(page)} (${page.id})`;
|
|
208
|
+
}
|
|
209
|
+
export function extractPageInfo(raw, fallbackId) {
|
|
210
|
+
const record = recordValue(raw);
|
|
211
|
+
const id = stringValue(record?.id) ?? fallbackId;
|
|
212
|
+
return {
|
|
213
|
+
id,
|
|
214
|
+
title: stringValue(record?.title) ?? titleFromProperties(record?.properties) ?? titleFromMarkdown(stringValue(record?.markdown) ?? ''),
|
|
215
|
+
url: stringValue(record?.url),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
export function normalizeMarkdownBody(result, fallbackId) {
|
|
219
|
+
const data = recordValue(result.data) ?? {};
|
|
220
|
+
return {
|
|
221
|
+
object: stringValue(data.object),
|
|
222
|
+
id: stringValue(data.id) ?? fallbackId,
|
|
223
|
+
title: stringValue(data.title) ?? titleFromProperties(data.properties) ?? titleFromMarkdown(stringValue(data.markdown) ?? ''),
|
|
224
|
+
url: stringValue(data.url),
|
|
225
|
+
markdown: stringValue(data.markdown) ?? '',
|
|
226
|
+
truncated: booleanValue(data.truncated) ?? false,
|
|
227
|
+
unknown_block_ids: stringArrayValue(data.unknown_block_ids),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
export function renderRead(data) {
|
|
231
|
+
const markdown = data.content.preview;
|
|
232
|
+
const trailer = `— page ${data.page.id} · title "${escapeTrailerValue(pageTitle(data.page))}" · preview chars ${data.content.preview.length}/${data.content.chars_total} · Notion truncated ${data.content.notion_truncated} · unknown blocks ${data.content.unknown_block_ids.length}`;
|
|
233
|
+
return markdown.length > 0 ? `${markdown}\n${trailer}` : trailer;
|
|
234
|
+
}
|
|
235
|
+
export function renderEdit(data) {
|
|
236
|
+
return [
|
|
237
|
+
renderKv([
|
|
238
|
+
['ok', true],
|
|
239
|
+
['command', 'page-content edit'],
|
|
240
|
+
['page', pageLabel(data.page)],
|
|
241
|
+
['status', data.status],
|
|
242
|
+
['ops-requested', data.ops_requested],
|
|
243
|
+
['ops-applied', data.ops_applied],
|
|
244
|
+
['replace-all', data.replace_all],
|
|
245
|
+
['allow-deleting-content', data.allow_deleting_content],
|
|
246
|
+
]),
|
|
247
|
+
'ops:',
|
|
248
|
+
renderTable({
|
|
249
|
+
header: ['#', 'old', 'new', 'status'],
|
|
250
|
+
rows: data.ops.map((operation, index) => [index + 1, operation.old, operation.new, operation.status]),
|
|
251
|
+
}),
|
|
252
|
+
'after excerpt:',
|
|
253
|
+
data.after_excerpt,
|
|
254
|
+
].join('\n\n');
|
|
255
|
+
}
|
|
256
|
+
export function renderReplace(data) {
|
|
257
|
+
return [
|
|
258
|
+
renderKv([
|
|
259
|
+
['ok', true],
|
|
260
|
+
['command', 'page-content replace'],
|
|
261
|
+
['page', pageLabel(data.page)],
|
|
262
|
+
['status', data.status],
|
|
263
|
+
['allow-deleting-content', data.allow_deleting_content],
|
|
264
|
+
['submitted', `${data.submitted.format}, ${data.submitted.chars} chars`],
|
|
265
|
+
]),
|
|
266
|
+
'replacement preview:',
|
|
267
|
+
data.submitted.preview,
|
|
268
|
+
].join('\n\n');
|
|
269
|
+
}
|
|
270
|
+
export async function emitRaw(raw) {
|
|
271
|
+
await writeStdout(`${JSON.stringify(redactAuthUrls(raw), null, 2)}\n`);
|
|
272
|
+
}
|
|
273
|
+
export function withoutRaw(data) {
|
|
274
|
+
const { raw: _raw, ...rest } = data;
|
|
275
|
+
void _raw;
|
|
276
|
+
return rest;
|
|
277
|
+
}
|
|
278
|
+
export async function readMarkdownSource(flags) {
|
|
279
|
+
const sources = [flags.markdown !== undefined, flags.file !== undefined, flags.stdin === true].filter(Boolean).length;
|
|
280
|
+
if (sources !== 1) {
|
|
281
|
+
throw new EscliError('provide exactly one content source: --markdown, --file, or --stdin', {
|
|
282
|
+
code: ErrorCode.UsageRequired,
|
|
283
|
+
exitCode: ExitCodes.Usage,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (flags.markdown !== undefined)
|
|
287
|
+
return flags.markdown;
|
|
288
|
+
if (flags.file !== undefined) {
|
|
289
|
+
try {
|
|
290
|
+
return await readFile(flags.file, 'utf8');
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
const nodeError = error;
|
|
294
|
+
throw new EscliError(`failed to read markdown file: ${nodeError.message}`, {
|
|
295
|
+
code: nodeError.code === 'ENOENT' ? ErrorCode.FileNotFound : ErrorCode.FileReadFailed,
|
|
296
|
+
exitCode: nodeError.code === 'ENOENT' ? ExitCodes.NotFound : ExitCodes.Error,
|
|
297
|
+
details: { path: flags.file },
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return readAllStdin();
|
|
302
|
+
}
|
|
303
|
+
export function parseEditOperations(flags) {
|
|
304
|
+
if (flags.ops !== undefined) {
|
|
305
|
+
if (flags.old !== undefined || flags.new !== undefined) {
|
|
306
|
+
throw new EscliError('use either --ops or --old/--new pairs, not both', { code: ErrorCode.UsageInvalid, exitCode: ExitCodes.Usage });
|
|
307
|
+
}
|
|
308
|
+
return parseOps(flags.ops);
|
|
309
|
+
}
|
|
310
|
+
const oldValues = flags.old ?? [];
|
|
311
|
+
const newValues = flags.new ?? [];
|
|
312
|
+
if (oldValues.length === 0 || newValues.length === 0 || oldValues.length !== newValues.length) {
|
|
313
|
+
throw new EscliError('provide matching --old and --new pairs, or --ops', { code: ErrorCode.UsageRequired, exitCode: ExitCodes.Usage });
|
|
314
|
+
}
|
|
315
|
+
return oldValues.map((oldValue, index) => ({
|
|
316
|
+
old_str: oldValue,
|
|
317
|
+
new_str: newValues[index] ?? '',
|
|
318
|
+
...(flags['replace-all'] ? { replace_all_matches: true } : {}),
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
export function buildEditBody(ops, allowDeletingContent) {
|
|
322
|
+
return {
|
|
323
|
+
type: 'update_content',
|
|
324
|
+
update_content: { content_updates: ops },
|
|
325
|
+
...(allowDeletingContent ? { allow_deleting_content: true } : {}),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
export function buildReplaceBody(markdown, allowDeletingContent) {
|
|
329
|
+
return {
|
|
330
|
+
type: 'replace_content',
|
|
331
|
+
replace_content: { markdown },
|
|
332
|
+
...(allowDeletingContent ? { allow_deleting_content: true } : {}),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
export function excerptMarkdown(markdown, maxLines = 9, maxChars = 900) {
|
|
336
|
+
const byLines = markdown.split(/\r?\n/u).slice(0, maxLines).join('\n');
|
|
337
|
+
return byLines.length > maxChars ? `${byLines.slice(0, maxChars).trimEnd()}…` : byLines;
|
|
338
|
+
}
|
|
339
|
+
export function previewMarkdown(markdown, maxLines = 80, maxChars = 4000) {
|
|
340
|
+
const byLines = markdown.split(/\r?\n/u).slice(0, maxLines).join('\n');
|
|
341
|
+
return byLines.length > maxChars ? byLines.slice(0, maxChars).trimEnd() : byLines;
|
|
342
|
+
}
|
|
343
|
+
export function operationStatuses(ops, after) {
|
|
344
|
+
return ops.map((operation) => ({
|
|
345
|
+
old: operation.old_str,
|
|
346
|
+
new: operation.new_str,
|
|
347
|
+
status: after.includes(operation.new_str) ? 'applied' : 'submitted',
|
|
348
|
+
}));
|
|
349
|
+
}
|
|
350
|
+
export function parentFromFlag(parent) {
|
|
351
|
+
const id = pageIdFromRef(parent);
|
|
352
|
+
const lower = parent.toLowerCase();
|
|
353
|
+
if (lower.includes('data_source') || lower.includes('data-source'))
|
|
354
|
+
return { data_source_id: id };
|
|
355
|
+
if (lower.includes('database'))
|
|
356
|
+
return { database_id: id };
|
|
357
|
+
return { page_id: id };
|
|
358
|
+
}
|
|
359
|
+
export function parseProperties(input) {
|
|
360
|
+
if (input === undefined)
|
|
361
|
+
return {};
|
|
362
|
+
const json = readJsonText(input);
|
|
363
|
+
if (json.trim().length === 0)
|
|
364
|
+
return {};
|
|
365
|
+
try {
|
|
366
|
+
return JSON.parse(json);
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
throw new EscliError(`invalid properties JSON: ${error instanceof Error ? error.message : String(error)}`, {
|
|
370
|
+
code: ErrorCode.ValidationFailed,
|
|
371
|
+
exitCode: ExitCodes.Usage,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async function readAllStdin() {
|
|
376
|
+
const chunks = [];
|
|
377
|
+
for await (const chunk of stdin)
|
|
378
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
379
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
380
|
+
}
|
|
381
|
+
function parseOps(input) {
|
|
382
|
+
const parsed = parseJsonOrAtFile(input);
|
|
383
|
+
const array = Array.isArray(parsed) ? parsed : recordValue(parsed)?.content_updates;
|
|
384
|
+
if (!Array.isArray(array)) {
|
|
385
|
+
throw new EscliError('--ops must be a JSON array or an object with content_updates', { code: ErrorCode.ValidationFailed, exitCode: ExitCodes.Usage });
|
|
386
|
+
}
|
|
387
|
+
return array.map((entry, index) => {
|
|
388
|
+
const record = recordValue(entry);
|
|
389
|
+
const oldValue = stringValue(record?.old_str ?? record?.old);
|
|
390
|
+
const newValue = stringValue(record?.new_str ?? record?.new);
|
|
391
|
+
if (oldValue === undefined || newValue === undefined) {
|
|
392
|
+
throw new EscliError(`invalid --ops entry at index ${index}: expected old_str/new_str`, { code: ErrorCode.ValidationFailed, exitCode: ExitCodes.Usage });
|
|
393
|
+
}
|
|
394
|
+
const replaceAll = booleanValue(record?.replace_all_matches);
|
|
395
|
+
return { old_str: oldValue, new_str: newValue, ...(replaceAll === undefined ? {} : { replace_all_matches: replaceAll }) };
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function parseJsonOrAtFile(input) {
|
|
399
|
+
return JSON.parse(readJsonText(input));
|
|
400
|
+
}
|
|
401
|
+
function readJsonText(input) {
|
|
402
|
+
if (!input.startsWith('@'))
|
|
403
|
+
return input;
|
|
404
|
+
const path = input.slice(1);
|
|
405
|
+
try {
|
|
406
|
+
return readFileSync(path, 'utf8');
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
const nodeError = error;
|
|
410
|
+
throw new EscliError(`failed to read JSON file: ${nodeError.message}`, {
|
|
411
|
+
code: nodeError.code === 'ENOENT' ? ErrorCode.FileNotFound : ErrorCode.FileReadFailed,
|
|
412
|
+
exitCode: nodeError.code === 'ENOENT' ? ExitCodes.NotFound : ExitCodes.Error,
|
|
413
|
+
details: { path },
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function titleFromMarkdown(markdown) {
|
|
418
|
+
return markdown.match(/^#\s+(.+)$/mu)?.[1]?.trim();
|
|
419
|
+
}
|
|
420
|
+
function titleFromProperties(properties) {
|
|
421
|
+
const props = recordValue(properties);
|
|
422
|
+
if (!props)
|
|
423
|
+
return undefined;
|
|
424
|
+
for (const value of Object.values(props)) {
|
|
425
|
+
const property = recordValue(value);
|
|
426
|
+
const title = arrayValue(property?.title)
|
|
427
|
+
.map((item) => stringValue(recordValue(item)?.plain_text) ?? stringValue(recordValue(recordValue(item)?.text)?.content))
|
|
428
|
+
.filter((item) => item !== undefined)
|
|
429
|
+
.join('');
|
|
430
|
+
if (title.length > 0)
|
|
431
|
+
return title;
|
|
432
|
+
}
|
|
433
|
+
return undefined;
|
|
434
|
+
}
|
|
435
|
+
function redactAuthUrls(value) {
|
|
436
|
+
if (Array.isArray(value))
|
|
437
|
+
return value.map(redactAuthUrls);
|
|
438
|
+
const record = recordValue(value);
|
|
439
|
+
if (!record)
|
|
440
|
+
return value;
|
|
441
|
+
return Object.fromEntries(Object.entries(record).map(([key, item]) => [key, key === 'upload_url' || key === 'complete_url' ? redactUrlQuery(item) : redactAuthUrls(item)]));
|
|
442
|
+
}
|
|
443
|
+
function redactUrlQuery(value) {
|
|
444
|
+
if (typeof value !== 'string')
|
|
445
|
+
return value;
|
|
446
|
+
try {
|
|
447
|
+
const url = new URL(value);
|
|
448
|
+
url.search = '';
|
|
449
|
+
return url.toString();
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return value.replace(/\?.*$/u, '');
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function escapeTrailerValue(value) {
|
|
456
|
+
return value.replaceAll('"', '\\"');
|
|
457
|
+
}
|
|
458
|
+
function recordValue(value) {
|
|
459
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
460
|
+
}
|
|
461
|
+
function arrayValue(value) {
|
|
462
|
+
return Array.isArray(value) ? value : [];
|
|
463
|
+
}
|
|
464
|
+
function stringArrayValue(value) {
|
|
465
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
|
|
466
|
+
}
|
|
467
|
+
function stringValue(value) {
|
|
468
|
+
return typeof value === 'string' ? value : undefined;
|
|
469
|
+
}
|
|
470
|
+
function booleanValue(value) {
|
|
471
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
472
|
+
}
|
|
473
|
+
//# sourceMappingURL=content-common.js.map
|