@helpfeel/cosense-cli 1.4.6 → 1.5.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/package.json +6 -6
- package/src/commands/previewEdit.ts +81 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helpfeel/cosense-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Cosense (旧Scrapbox) のページを読み・調べ・編集するAgent Skill用のCLI",
|
|
5
5
|
"homepage": "https://github.com/helpfeel/cosense-cli",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
"format": "oxfmt"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"tsx": "4.
|
|
27
|
+
"tsx": "4.22.4"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/node": "24.
|
|
30
|
+
"@types/node": "24.13.2",
|
|
31
31
|
"npm-run-all": "4.1.5",
|
|
32
|
-
"oxfmt": "0.
|
|
33
|
-
"oxlint": "1.
|
|
34
|
-
"typescript": "6.0.
|
|
32
|
+
"oxfmt": "0.54.0",
|
|
33
|
+
"oxlint": "1.70.0",
|
|
34
|
+
"typescript": "6.0.3"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
37
|
"node": ">=24"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
2
3
|
import { parseProjectUrlStrict } from '../lib/parseUrl.ts';
|
|
3
4
|
import { requestJson } from '../lib/request.ts';
|
|
4
5
|
import { resolveUserCredential } from '../lib/settings.ts';
|
|
@@ -11,6 +12,8 @@ export const previewEditHelp = `previewEdit - ページ編集opsをdry-runして
|
|
|
11
12
|
Usage:
|
|
12
13
|
cosense previewEdit <projectUrl> <pageId> < ops.json 既存ページの編集 (stdinはops JSON)
|
|
13
14
|
cosense previewEdit --new <projectUrl> < body.txt 新規ページ作成 (stdinはプレーンテキスト本文)
|
|
15
|
+
cosense previewEdit --input-file ops.json <projectUrl> <pageId>
|
|
16
|
+
cosense previewEdit --new --input-file body.txt <projectUrl>
|
|
14
17
|
printf '%s' '<opsJSON>' | cosense previewEdit <projectUrl> <pageId>
|
|
15
18
|
printf '%s' '<text>' | cosense previewEdit --new <projectUrl>
|
|
16
19
|
|
|
@@ -22,6 +25,8 @@ Usage:
|
|
|
22
25
|
オプション:
|
|
23
26
|
--new stdin をプレーンテキスト本文として受け取り、 新規ページを作る。 改行で複数行に分割され、
|
|
24
27
|
1行目が page title、 2行目以降が本文として扱われる。 ops JSON を組み立てる必要は無い
|
|
28
|
+
--input-file <path>
|
|
29
|
+
stdin の代わりに UTF-8 テキストファイルから入力を読む。 指定時は stdin を読まない
|
|
25
30
|
|
|
26
31
|
stdinから受け取る入力形式(既存ページ編集モード, JSON):
|
|
27
32
|
{
|
|
@@ -234,23 +239,75 @@ const readStdin = async (): Promise<string> => {
|
|
|
234
239
|
return Buffer.concat(chunks).toString('utf8');
|
|
235
240
|
};
|
|
236
241
|
|
|
242
|
+
// --input-file はバイト列を読んで UTF-8 として厳格に decode する。 TextDecoder の既定は
|
|
243
|
+
// ignoreBOM: false なので先頭 BOM を除去し、 fatal: true で BOM付き UTF-16 や不正バイトを
|
|
244
|
+
// 例外で弾く。 ただし BOM なし UTF-16LE は各バイトが偶然 valid UTF-8 になり fatal をすり抜けて
|
|
245
|
+
// NUL 混じり文字列になる事があるので、 decode 後に NUL を弾く。 stdin 経由 (readStdin の
|
|
246
|
+
// toString('utf8')) は不正バイトを置換して通すが、 こちらは「文字化けしたまま書き込み成功」を
|
|
247
|
+
// 防ぐため早期に失敗させる。
|
|
248
|
+
const readInputFileUtf8 = async (path: string): Promise<string> => {
|
|
249
|
+
let bytes: Buffer;
|
|
250
|
+
try {
|
|
251
|
+
bytes = await readFile(path);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`--input-file: failed to read "${path}": ${err instanceof Error ? err.message : String(err)}`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
let text: string;
|
|
258
|
+
try {
|
|
259
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
260
|
+
} catch {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`--input-file: "${path}" is not valid UTF-8. Rewrite the file as UTF-8 (not UTF-16) and retry.`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (text.includes('\u0000')) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`--input-file: "${path}" contains NUL, which suggests UTF-16 or binary, not UTF-8 text. Rewrite the file as UTF-8 (not UTF-16) and retry.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return text;
|
|
271
|
+
};
|
|
272
|
+
|
|
237
273
|
interface ParsedArgs {
|
|
238
274
|
isNew: boolean;
|
|
239
275
|
projectUrl: string;
|
|
240
276
|
pageId: string | undefined;
|
|
277
|
+
inputFile: string | undefined;
|
|
241
278
|
}
|
|
242
279
|
|
|
243
280
|
const parseArgs = (args: string[]): ParsedArgs => {
|
|
244
281
|
const usage =
|
|
245
282
|
'Usage: cosense previewEdit <projectUrl> <pageId> < ops.json (use --new <projectUrl> < body.txt for new pages)';
|
|
246
283
|
let isNew = false;
|
|
284
|
+
let inputFile: string | undefined;
|
|
247
285
|
const positional: string[] = [];
|
|
248
|
-
for (
|
|
286
|
+
for (let i = 0; i < args.length; i++) {
|
|
287
|
+
const arg = args[i] as string;
|
|
249
288
|
if (arg === '--new') {
|
|
250
289
|
if (isNew) {
|
|
251
290
|
throw new Error(`Duplicate option: --new\n${usage}`);
|
|
252
291
|
}
|
|
253
292
|
isNew = true;
|
|
293
|
+
} else if (arg === '--input-file') {
|
|
294
|
+
if (inputFile !== undefined) {
|
|
295
|
+
throw new Error(`Duplicate option: --input-file\n${usage}`);
|
|
296
|
+
}
|
|
297
|
+
// 空文字を素通しすると後段の truthy 判定 (!inputFile / inputFile ? ...) で
|
|
298
|
+
// 「未指定」と同じ扱いになり、 stdin を読んでしまう。 値欠落として弾く
|
|
299
|
+
const value = args[++i];
|
|
300
|
+
if (value === undefined || value === '') {
|
|
301
|
+
throw new Error(`Missing value for --input-file\n${usage}`);
|
|
302
|
+
}
|
|
303
|
+
// 次トークンが別オプション (--new 等) の時はパスの書き忘れとみなし、 黙って
|
|
304
|
+
// ファイル名として消費しない。 `--` 始まりの実ファイルは ./--name で渡せる
|
|
305
|
+
if (value.startsWith('--')) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`--input-file expects a file path, but got "${value}". A path must immediately follow --input-file.\n${usage}`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
inputFile = value;
|
|
254
311
|
} else if (arg.startsWith('--')) {
|
|
255
312
|
throw new Error(`Unknown option: ${arg}\n${usage}`);
|
|
256
313
|
} else {
|
|
@@ -263,7 +320,12 @@ const parseArgs = (args: string[]): ParsedArgs => {
|
|
|
263
320
|
'Usage: cosense previewEdit --new <projectUrl> < body.txt'
|
|
264
321
|
);
|
|
265
322
|
}
|
|
266
|
-
return {
|
|
323
|
+
return {
|
|
324
|
+
isNew,
|
|
325
|
+
projectUrl: positional[0] as string,
|
|
326
|
+
pageId: undefined,
|
|
327
|
+
inputFile
|
|
328
|
+
};
|
|
267
329
|
}
|
|
268
330
|
if (positional.length !== 2) {
|
|
269
331
|
throw new Error(usage);
|
|
@@ -271,28 +333,32 @@ const parseArgs = (args: string[]): ParsedArgs => {
|
|
|
271
333
|
return {
|
|
272
334
|
isNew,
|
|
273
335
|
projectUrl: positional[0] as string,
|
|
274
|
-
pageId: positional[1] as string
|
|
336
|
+
pageId: positional[1] as string,
|
|
337
|
+
inputFile
|
|
275
338
|
};
|
|
276
339
|
};
|
|
277
340
|
|
|
278
341
|
export const previewEdit = async (args: string[]): Promise<void> => {
|
|
279
|
-
const { isNew, projectUrl, pageId } = parseArgs(args);
|
|
342
|
+
const { isNew, projectUrl, pageId, inputFile } = parseArgs(args);
|
|
280
343
|
|
|
281
|
-
if (process.stdin.isTTY) {
|
|
344
|
+
if (!inputFile && process.stdin.isTTY) {
|
|
282
345
|
throw new Error(
|
|
283
346
|
isNew
|
|
284
|
-
? 'previewEdit --new reads plain text body from stdin. Pipe it in, e.g. `printf "Title\\nbody\\n" | cosense previewEdit --new <projectUrl
|
|
285
|
-
: 'previewEdit reads ops JSON from stdin. Pipe it in, e.g. `cosense previewEdit <projectUrl> <pageId> < ops.json`.'
|
|
347
|
+
? 'previewEdit --new reads plain text body from stdin or --input-file. Pipe it in, e.g. `printf "Title\\nbody\\n" | cosense previewEdit --new <projectUrl>`, or pass a UTF-8 file with `--input-file body.txt`.'
|
|
348
|
+
: 'previewEdit reads ops JSON from stdin or --input-file. Pipe it in, e.g. `cosense previewEdit <projectUrl> <pageId> < ops.json`, or pass a UTF-8 file with `--input-file ops.json`.'
|
|
286
349
|
);
|
|
287
350
|
}
|
|
288
351
|
|
|
289
352
|
const { origin, projectName } = parseProjectUrlStrict(projectUrl);
|
|
290
|
-
const
|
|
291
|
-
|
|
353
|
+
const rawInput = inputFile
|
|
354
|
+
? await readInputFileUtf8(inputFile)
|
|
355
|
+
: await readStdin();
|
|
356
|
+
if (!rawInput.trim()) {
|
|
357
|
+
const source = inputFile ? `input file "${inputFile}"` : 'stdin';
|
|
292
358
|
throw new Error(
|
|
293
359
|
isNew
|
|
294
|
-
?
|
|
295
|
-
:
|
|
360
|
+
? `${source} is empty. Provide page body (plain text).`
|
|
361
|
+
: `${source} is empty. Provide ops JSON.`
|
|
296
362
|
);
|
|
297
363
|
}
|
|
298
364
|
|
|
@@ -302,15 +368,16 @@ export const previewEdit = async (args: string[]): Promise<void> => {
|
|
|
302
368
|
// 慣習で末尾の単一改行 (LF または CRLF) だけ取り除く
|
|
303
369
|
let ops: unknown;
|
|
304
370
|
if (isNew) {
|
|
305
|
-
const body =
|
|
371
|
+
const body = rawInput.replace(/\r?\n$/, '');
|
|
306
372
|
ops = [{ insertBefore: '_end', text: body }];
|
|
307
373
|
} else {
|
|
308
374
|
let parsed: unknown;
|
|
309
375
|
try {
|
|
310
|
-
parsed = JSON.parse(
|
|
376
|
+
parsed = JSON.parse(rawInput);
|
|
311
377
|
} catch (err) {
|
|
378
|
+
const source = inputFile ? `input file "${inputFile}"` : 'stdin';
|
|
312
379
|
throw new Error(
|
|
313
|
-
|
|
380
|
+
`${source} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
314
381
|
);
|
|
315
382
|
}
|
|
316
383
|
ops = (parsed as { ops?: unknown }).ops;
|