@helpfeel/cosense-cli 1.12.1 → 1.14.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 +1 -1
- package/src/cli.ts +40 -0
- package/src/commands/deleteFile.ts +81 -0
- package/src/commands/listPageSnapshots.ts +65 -0
- package/src/commands/previewDelete.ts +76 -0
- package/src/commands/readPageSnapshot.ts +138 -0
- package/src/commands/submitEdit.ts +21 -3
- package/src/lib/request.ts +1 -1
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -17,6 +17,11 @@ import {
|
|
|
17
17
|
browseRelatedPagesHelp,
|
|
18
18
|
browseRelatedPagesSummary
|
|
19
19
|
} from './commands/browseRelatedPages.ts';
|
|
20
|
+
import {
|
|
21
|
+
deleteFile,
|
|
22
|
+
deleteFileHelp,
|
|
23
|
+
deleteFileSummary
|
|
24
|
+
} from './commands/deleteFile.ts';
|
|
20
25
|
import {
|
|
21
26
|
downloadFile,
|
|
22
27
|
downloadFileHelp,
|
|
@@ -37,12 +42,22 @@ import {
|
|
|
37
42
|
listPagesHelp,
|
|
38
43
|
listPagesSummary
|
|
39
44
|
} from './commands/listPages.ts';
|
|
45
|
+
import {
|
|
46
|
+
listPageSnapshots,
|
|
47
|
+
listPageSnapshotsHelp,
|
|
48
|
+
listPageSnapshotsSummary
|
|
49
|
+
} from './commands/listPageSnapshots.ts';
|
|
40
50
|
import {
|
|
41
51
|
listProjects,
|
|
42
52
|
listProjectsHelp,
|
|
43
53
|
listProjectsSummary
|
|
44
54
|
} from './commands/listProjects.ts';
|
|
45
55
|
import { login, loginHelp, loginSummary } from './commands/login.ts';
|
|
56
|
+
import {
|
|
57
|
+
previewDelete,
|
|
58
|
+
previewDeleteHelp,
|
|
59
|
+
previewDeleteSummary
|
|
60
|
+
} from './commands/previewDelete.ts';
|
|
46
61
|
import {
|
|
47
62
|
previewEdit,
|
|
48
63
|
previewEditHelp,
|
|
@@ -58,6 +73,11 @@ import {
|
|
|
58
73
|
readPageHelp,
|
|
59
74
|
readPageSummary
|
|
60
75
|
} from './commands/readPage.ts';
|
|
76
|
+
import {
|
|
77
|
+
readPageSnapshot,
|
|
78
|
+
readPageSnapshotHelp,
|
|
79
|
+
readPageSnapshotSummary
|
|
80
|
+
} from './commands/readPageSnapshot.ts';
|
|
61
81
|
import {
|
|
62
82
|
readProjectMembers,
|
|
63
83
|
readProjectMembersHelp,
|
|
@@ -139,6 +159,11 @@ const commands: Record<string, CommandSpec> = {
|
|
|
139
159
|
help: browseRelatedPagesHelp
|
|
140
160
|
},
|
|
141
161
|
readPage: { handler: readPage, summary: readPageSummary, help: readPageHelp },
|
|
162
|
+
readPageSnapshot: {
|
|
163
|
+
handler: readPageSnapshot,
|
|
164
|
+
summary: readPageSnapshotSummary,
|
|
165
|
+
help: readPageSnapshotHelp
|
|
166
|
+
},
|
|
142
167
|
readFileInfo: {
|
|
143
168
|
handler: readFileInfo,
|
|
144
169
|
summary: readFileInfoSummary,
|
|
@@ -154,6 +179,11 @@ const commands: Record<string, CommandSpec> = {
|
|
|
154
179
|
summary: uploadFileSummary,
|
|
155
180
|
help: uploadFileHelp
|
|
156
181
|
},
|
|
182
|
+
deleteFile: {
|
|
183
|
+
handler: deleteFile,
|
|
184
|
+
summary: deleteFileSummary,
|
|
185
|
+
help: deleteFileHelp
|
|
186
|
+
},
|
|
157
187
|
readProjectMembers: {
|
|
158
188
|
handler: readProjectMembers,
|
|
159
189
|
summary: readProjectMembersSummary,
|
|
@@ -164,6 +194,11 @@ const commands: Record<string, CommandSpec> = {
|
|
|
164
194
|
summary: listPagesSummary,
|
|
165
195
|
help: listPagesHelp
|
|
166
196
|
},
|
|
197
|
+
listPageSnapshots: {
|
|
198
|
+
handler: listPageSnapshots,
|
|
199
|
+
summary: listPageSnapshotsSummary,
|
|
200
|
+
help: listPageSnapshotsHelp
|
|
201
|
+
},
|
|
167
202
|
list1hopLinks: {
|
|
168
203
|
handler: list1hopLinks,
|
|
169
204
|
summary: list1hopLinksSummary,
|
|
@@ -199,6 +234,11 @@ const commands: Record<string, CommandSpec> = {
|
|
|
199
234
|
summary: previewEditSummary,
|
|
200
235
|
help: previewEditHelp
|
|
201
236
|
},
|
|
237
|
+
previewDelete: {
|
|
238
|
+
handler: previewDelete,
|
|
239
|
+
summary: previewDeleteSummary,
|
|
240
|
+
help: previewDeleteHelp
|
|
241
|
+
},
|
|
202
242
|
submitEdit: {
|
|
203
243
|
handler: submitEdit,
|
|
204
244
|
summary: submitEditSummary,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { parseFileUrl } from '../lib/parseUrl.ts';
|
|
2
|
+
import { requestJson } from '../lib/request.ts';
|
|
3
|
+
import { resolveFileCredential } from '../lib/resolveFileCredential.ts';
|
|
4
|
+
|
|
5
|
+
export const deleteFileSummary = 'ファイルをprojectから削除する';
|
|
6
|
+
|
|
7
|
+
export const deleteFileHelp = `deleteFile - ファイルをprojectから削除する
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
cosense deleteFile <fileUrl> [--project <projectUrl>]
|
|
11
|
+
|
|
12
|
+
引数:
|
|
13
|
+
<fileUrl> ファイルのURL(例: https://scrapbox.io/files/5f151efbacbb17001a58f120.pdf)。query/hashは付けない
|
|
14
|
+
|
|
15
|
+
オプション:
|
|
16
|
+
--project <projectUrl> Service Account認証で削除する時に、ファイルが属するprojectのURLを指定する(例: https://scrapbox.io/example)。省略時はPersonal Access Tokenを使う
|
|
17
|
+
|
|
18
|
+
出力(JSON):
|
|
19
|
+
success boolean 削除に成功したらtrue
|
|
20
|
+
|
|
21
|
+
削除後、ファイルが所属するproject内でこのファイルを埋め込んでいるページの本文はサーバーが自動修正するため、クライアント側での本文修正は不要。
|
|
22
|
+
|
|
23
|
+
例:
|
|
24
|
+
cosense deleteFile 'https://scrapbox.io/files/5f151efbacbb17001a58f120.pdf'
|
|
25
|
+
|
|
26
|
+
HTTPエラー:
|
|
27
|
+
401/403: 認証・権限が無い。projectのmember権限(またはprojectのService Account)が必要
|
|
28
|
+
404: fileIdに対応するファイルが存在しない
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
interface ParsedArgs {
|
|
32
|
+
fileUrl: string;
|
|
33
|
+
project?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const parseArgs = (args: string[]): ParsedArgs => {
|
|
37
|
+
const usage = 'Usage: cosense deleteFile <fileUrl> [--project <projectUrl>]';
|
|
38
|
+
let project: string | undefined;
|
|
39
|
+
const positional: string[] = [];
|
|
40
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
41
|
+
const arg = args[i] as string;
|
|
42
|
+
if (arg === '--project') {
|
|
43
|
+
if (project !== undefined) {
|
|
44
|
+
throw new Error(`--project specified multiple times\n${usage}`);
|
|
45
|
+
}
|
|
46
|
+
const value = args[i + 1];
|
|
47
|
+
if (value === undefined || value.startsWith('--')) {
|
|
48
|
+
throw new Error(`--project requires a value\n${usage}`);
|
|
49
|
+
}
|
|
50
|
+
project = value;
|
|
51
|
+
i += 1;
|
|
52
|
+
} else if (arg.startsWith('--')) {
|
|
53
|
+
throw new Error(`Unknown option: ${arg}\n${usage}`);
|
|
54
|
+
} else {
|
|
55
|
+
positional.push(arg);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (positional.length !== 1) {
|
|
59
|
+
throw new Error(usage);
|
|
60
|
+
}
|
|
61
|
+
return { fileUrl: positional[0] as string, project };
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const deleteFile = async (args: string[]): Promise<void> => {
|
|
65
|
+
const { fileUrl, project } = parseArgs(args);
|
|
66
|
+
const { origin, fileId } = parseFileUrl(fileUrl);
|
|
67
|
+
const credential = resolveFileCredential(origin, project);
|
|
68
|
+
if (!credential) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`No credential found for ${origin}. Run \`cosense login ${origin}\` to authenticate.`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const data = (await requestJson(`${origin}/api/gcs/${fileId}`, {
|
|
74
|
+
credential,
|
|
75
|
+
method: 'DELETE'
|
|
76
|
+
})) as { success?: unknown };
|
|
77
|
+
if (data?.success !== true) {
|
|
78
|
+
throw new Error(`unexpected delete response: ${JSON.stringify(data)}`);
|
|
79
|
+
}
|
|
80
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
81
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { enrichTimestampsOf } from '../lib/enrichTimestamps.ts';
|
|
2
|
+
import { parseProjectUrlStrict } from '../lib/parseUrl.ts';
|
|
3
|
+
import { requestJson } from '../lib/request.ts';
|
|
4
|
+
import { resolveCredential } from '../lib/settings.ts';
|
|
5
|
+
|
|
6
|
+
export const listPageSnapshotsSummary =
|
|
7
|
+
'ページのsnapshot一覧(Page History)をpageId起点で取得する';
|
|
8
|
+
|
|
9
|
+
export const listPageSnapshotsHelp = `listPageSnapshots - ページのsnapshot一覧(Page History)をpageId起点で取得する
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
cosense listPageSnapshots <projectUrl> <pageId>
|
|
13
|
+
|
|
14
|
+
引数:
|
|
15
|
+
<projectUrl> プロジェクトのURL (例: https://scrapbox.io/shokai)。 末尾に余分なpathがあるとerror
|
|
16
|
+
<pageId> ページの不変ID。browsePage / readPage の出力に含まれる
|
|
17
|
+
|
|
18
|
+
戻り値(top-levelのkey):
|
|
19
|
+
pageId string 対象ページのID
|
|
20
|
+
timestamps Array<{ id, created }> snapshotの一覧。新しい順、最新100件まで
|
|
21
|
+
id string snapshotのID。readPageSnapshot に渡す
|
|
22
|
+
created string snapshot時点のページ最終更新時刻
|
|
23
|
+
|
|
24
|
+
戻り値のJSON抜粋例:
|
|
25
|
+
{
|
|
26
|
+
"pageId": "5803c5397ad353b0aee24341",
|
|
27
|
+
"timestamps": [
|
|
28
|
+
{
|
|
29
|
+
"id": "5cef4052dd15ed00447c1405",
|
|
30
|
+
"created": "2019-05-30T11:29+09:00 (7 years ago)"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
HTTPエラー:
|
|
36
|
+
HTTP 401 認証なし
|
|
37
|
+
HTTP 403 権限不足(projectのmemberでない 等)
|
|
38
|
+
HTTP 404 pageId に対応するpageが存在しない / pageId が不正な形式
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
interface ListPageSnapshotsData {
|
|
42
|
+
timestamps?: { id?: string; created?: number | string }[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const listPageSnapshots = async (args: string[]): Promise<void> => {
|
|
46
|
+
if (args.length !== 2) {
|
|
47
|
+
throw new Error('Usage: cosense listPageSnapshots <projectUrl> <pageId>');
|
|
48
|
+
}
|
|
49
|
+
const [projectUrl, pageId] = args as [string, string];
|
|
50
|
+
|
|
51
|
+
const { origin, projectName } = parseProjectUrlStrict(projectUrl);
|
|
52
|
+
const credential = resolveCredential(origin, projectName);
|
|
53
|
+
|
|
54
|
+
// APIはx-following-idレスポンスヘッダで100件ずつページングするが、最新100件のみ返し追跡しない
|
|
55
|
+
const apiUrl = `${origin}/api/page-snapshots/${projectName}/${pageId}`;
|
|
56
|
+
const data = (await requestJson(apiUrl, {
|
|
57
|
+
credential
|
|
58
|
+
})) as ListPageSnapshotsData;
|
|
59
|
+
|
|
60
|
+
for (const timestamp of data.timestamps ?? []) {
|
|
61
|
+
enrichTimestampsOf(timestamp as Record<string, unknown>, ['created']);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
65
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { parseProjectUrlStrict } from '../lib/parseUrl.ts';
|
|
2
|
+
import { requestJson } from '../lib/request.ts';
|
|
3
|
+
import { resolveCredential } from '../lib/settings.ts';
|
|
4
|
+
|
|
5
|
+
export const previewDeleteSummary =
|
|
6
|
+
'ページ削除をdry-runしてpreviewIdを取得する';
|
|
7
|
+
|
|
8
|
+
export const previewDeleteHelp = `previewDelete - ページ削除をdry-runしてpreviewIdを取得する
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
cosense previewDelete <projectUrl> <pageId>
|
|
12
|
+
|
|
13
|
+
引数:
|
|
14
|
+
<projectUrl> プロジェクトのURL (例: https://scrapbox.io/shokai)。 末尾に余分なpathがあるとerror
|
|
15
|
+
<pageId> 削除対象ページのID。 readPage 出力の top-level "id" field から取得する
|
|
16
|
+
|
|
17
|
+
戻り値(plain text):
|
|
18
|
+
previewId / expireAt / status (delete) / project / title のヘッダー + 削除されるpage全体。
|
|
19
|
+
preview は dry-run なのでこの段階では削除されない。
|
|
20
|
+
previewId を submitEdit に渡すと削除が確定する。5分で expire する。
|
|
21
|
+
|
|
22
|
+
HTTPエラー:
|
|
23
|
+
HTTP 401 認証なし
|
|
24
|
+
HTTP 403 権限不足(PAT利用時、projectのmemberでない 等)
|
|
25
|
+
HTTP 404 pageId に対応するpageが存在しない / pageId が不正な形式
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
interface PagePreview {
|
|
29
|
+
title?: string;
|
|
30
|
+
lines?: { id: string; text: string }[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PreviewResponse {
|
|
34
|
+
previewId: string;
|
|
35
|
+
expireAt: string;
|
|
36
|
+
pagePreview: PagePreview | null;
|
|
37
|
+
pageDelete?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const previewDelete = async (args: string[]): Promise<void> => {
|
|
41
|
+
if (args.length !== 2) {
|
|
42
|
+
throw new Error('Usage: cosense previewDelete <projectUrl> <pageId>');
|
|
43
|
+
}
|
|
44
|
+
const [projectUrl, pageId] = args as [string, string];
|
|
45
|
+
|
|
46
|
+
const { origin, projectName } = parseProjectUrlStrict(projectUrl);
|
|
47
|
+
// projectに紐づくService Accountがあればそれを、無ければPATを使う(読み取りと同じ)
|
|
48
|
+
const credential = resolveCredential(origin, projectName);
|
|
49
|
+
|
|
50
|
+
const apiUrl = `${origin}/api/pages/v2/${projectName}/page-edit-for-ai/preview`;
|
|
51
|
+
const response = (await requestJson(apiUrl, {
|
|
52
|
+
credential,
|
|
53
|
+
method: 'POST',
|
|
54
|
+
body: { pageId, changes: [{ deleted: true }] }
|
|
55
|
+
})) as PreviewResponse;
|
|
56
|
+
|
|
57
|
+
// 削除は破壊的操作なので、サーバーが削除previewとして受理した事を確認してから表示する
|
|
58
|
+
if (response.pageDelete !== true) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'server did not mark this preview as a page deletion. Do not submit the previewId.'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const lines: string[] = [];
|
|
65
|
+
lines.push(`previewId: ${response.previewId}`);
|
|
66
|
+
lines.push(`expireAt: ${response.expireAt}`);
|
|
67
|
+
lines.push('status: delete');
|
|
68
|
+
lines.push(`project: ${projectName}`);
|
|
69
|
+
lines.push(`title: ${response.pagePreview?.title ?? ''}`);
|
|
70
|
+
lines.push('');
|
|
71
|
+
lines.push('page (will be deleted):');
|
|
72
|
+
for (const line of response.pagePreview?.lines ?? []) {
|
|
73
|
+
lines.push(` ${line.text}`);
|
|
74
|
+
}
|
|
75
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
76
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { enrichTimestampsOf } from '../lib/enrichTimestamps.ts';
|
|
2
|
+
import { parseProjectUrlStrict } from '../lib/parseUrl.ts';
|
|
3
|
+
import { requestJson } from '../lib/request.ts';
|
|
4
|
+
import { enrichUser, fetchUserMap } from '../lib/resolveUsers.ts';
|
|
5
|
+
import { resolveCredential } from '../lib/settings.ts';
|
|
6
|
+
|
|
7
|
+
export const readPageSnapshotSummary =
|
|
8
|
+
'snapshotのIDを指定してPage History上の過去の本文を読む';
|
|
9
|
+
|
|
10
|
+
export const readPageSnapshotHelp = `readPageSnapshot - snapshotのIDを指定してPage History上の過去の本文を読む
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
cosense readPageSnapshot <projectUrl> <pageId> <snapshotId>
|
|
14
|
+
|
|
15
|
+
引数:
|
|
16
|
+
<projectUrl> プロジェクトのURL (例: https://scrapbox.io/shokai)。 末尾に余分なpathがあるとerror
|
|
17
|
+
<pageId> ページの不変ID。browsePage / readPage の出力に含まれる
|
|
18
|
+
<snapshotId> snapshotのID。listPageSnapshots の timestamps[].id から取得する
|
|
19
|
+
|
|
20
|
+
戻り値(top-levelのkey):
|
|
21
|
+
page object 現在のページのメタデータ。field構成は readPage のtop-levelから本文・リンク系
|
|
22
|
+
(lines / links / icons 等) を除いたもの。現在の本文は含まれない
|
|
23
|
+
snapshot object snapshot時点のページ
|
|
24
|
+
title string snapshot時点のページタイトル
|
|
25
|
+
created string snapshot時点のページ最終更新時刻
|
|
26
|
+
lines Array<Line> snapshot時点の本文。Line = { id, text, user, created, updated }
|
|
27
|
+
|
|
28
|
+
戻り値のJSON抜粋例:
|
|
29
|
+
{
|
|
30
|
+
"page": {
|
|
31
|
+
"id": "5803c5397ad353b0aee24341",
|
|
32
|
+
"title": "page1",
|
|
33
|
+
"commitId": "5cef401cfca37d0018dd9ead"
|
|
34
|
+
},
|
|
35
|
+
"snapshot": {
|
|
36
|
+
"title": "page1",
|
|
37
|
+
"created": "2019-05-30T11:29+09:00 (7 years ago)",
|
|
38
|
+
"lines": [
|
|
39
|
+
{
|
|
40
|
+
"id": "5803c5397ad353b0aee24341",
|
|
41
|
+
"text": "page1",
|
|
42
|
+
"user": { "id": "5803c4bd7ad353b0aee24328", "name": "shokai" },
|
|
43
|
+
"created": "2019-05-30T11:29+09:00 (7 years ago)",
|
|
44
|
+
"updated": "2019-05-30T11:29+09:00 (7 years ago)"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
絞り込み例(jqで欲しい部分だけ抜き出す):
|
|
51
|
+
snapshot時点の各行のテキストだけ:
|
|
52
|
+
cosense readPageSnapshot <projectUrl> <pageId> <snapshotId> | jq -r '.snapshot.lines[].text'
|
|
53
|
+
|
|
54
|
+
HTTPエラー:
|
|
55
|
+
HTTP 401 認証なし
|
|
56
|
+
HTTP 403 権限不足(projectのmemberでない 等)
|
|
57
|
+
HTTP 404 pageId に対応するpageが存在しない / snapshotId がこのページのsnapshotでない
|
|
58
|
+
HTTP 422 snapshotId が不正な形式
|
|
59
|
+
`;
|
|
60
|
+
|
|
61
|
+
interface UserRef {
|
|
62
|
+
id: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface SnapshotLine {
|
|
66
|
+
id?: string;
|
|
67
|
+
text?: string;
|
|
68
|
+
userId?: string;
|
|
69
|
+
user?: UserRef;
|
|
70
|
+
created?: number | string;
|
|
71
|
+
updated?: number | string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface PageMetadata {
|
|
75
|
+
user?: UserRef | null;
|
|
76
|
+
lastUpdateUser?: UserRef | null;
|
|
77
|
+
users?: UserRef[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ReadPageSnapshotData {
|
|
81
|
+
page?: PageMetadata;
|
|
82
|
+
snapshot?: {
|
|
83
|
+
created?: number | string;
|
|
84
|
+
lines?: SnapshotLine[];
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const readPageSnapshot = async (args: string[]): Promise<void> => {
|
|
89
|
+
if (args.length !== 3) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
'Usage: cosense readPageSnapshot <projectUrl> <pageId> <snapshotId>'
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const [projectUrl, pageId, snapshotId] = args as [string, string, string];
|
|
95
|
+
|
|
96
|
+
const { origin, projectName } = parseProjectUrlStrict(projectUrl);
|
|
97
|
+
const credential = resolveCredential(origin, projectName);
|
|
98
|
+
|
|
99
|
+
const apiUrl = `${origin}/api/page-snapshots/${projectName}/${pageId}/${snapshotId}`;
|
|
100
|
+
const data = (await requestJson(apiUrl, {
|
|
101
|
+
credential
|
|
102
|
+
})) as ReadPageSnapshotData;
|
|
103
|
+
|
|
104
|
+
const userMap = await fetchUserMap(origin, projectName);
|
|
105
|
+
const page = data.page;
|
|
106
|
+
if (page) {
|
|
107
|
+
enrichUser(page.user, userMap);
|
|
108
|
+
enrichUser(page.lastUpdateUser, userMap);
|
|
109
|
+
for (const editor of page.users ?? []) {
|
|
110
|
+
enrichUser(editor, userMap);
|
|
111
|
+
}
|
|
112
|
+
enrichTimestampsOf(page as Record<string, unknown>, [
|
|
113
|
+
'created',
|
|
114
|
+
'updated',
|
|
115
|
+
'accessed',
|
|
116
|
+
'snapshotCreated',
|
|
117
|
+
'lastAccessed'
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
const snapshot = data.snapshot;
|
|
121
|
+
if (snapshot) {
|
|
122
|
+
for (const line of snapshot.lines ?? []) {
|
|
123
|
+
const userId = line.userId;
|
|
124
|
+
if (typeof userId === 'string' && userId !== '') {
|
|
125
|
+
line.user = { id: userId };
|
|
126
|
+
delete line.userId;
|
|
127
|
+
enrichUser(line.user, userMap);
|
|
128
|
+
}
|
|
129
|
+
enrichTimestampsOf(line as Record<string, unknown>, [
|
|
130
|
+
'created',
|
|
131
|
+
'updated'
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
enrichTimestampsOf(snapshot as Record<string, unknown>, ['created']);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
138
|
+
};
|
|
@@ -4,16 +4,16 @@ import { requestJson } from '../lib/request.ts';
|
|
|
4
4
|
import { resolveCredential } from '../lib/settings.ts';
|
|
5
5
|
|
|
6
6
|
export const submitEditSummary =
|
|
7
|
-
'previewEditで取得したpreviewId
|
|
7
|
+
'previewEdit/previewDeleteで取得したpreviewIdを使ってページ編集・削除を確定する';
|
|
8
8
|
|
|
9
|
-
export const submitEditHelp = `submitEdit - previewEditで取得したpreviewId
|
|
9
|
+
export const submitEditHelp = `submitEdit - previewEdit/previewDeleteで取得したpreviewIdを使ってページ編集・削除を確定する
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
12
12
|
cosense submitEdit <projectUrl> <previewId>
|
|
13
13
|
|
|
14
14
|
引数:
|
|
15
15
|
<projectUrl> プロジェクトのURL (例: https://scrapbox.io/shokai)。 末尾に余分なpathがあるとerror
|
|
16
|
-
<previewId> previewEdit の戻り値の previewId
|
|
16
|
+
<previewId> previewEdit または previewDelete の戻り値の previewId
|
|
17
17
|
|
|
18
18
|
戻り値(plain text):
|
|
19
19
|
commitId: <生成されたcommitのID>
|
|
@@ -28,6 +28,11 @@ Usage:
|
|
|
28
28
|
titleChanged: "<変更前title>" -> "<変更後title>"
|
|
29
29
|
新旧タイトルはそれぞれJSON string
|
|
30
30
|
|
|
31
|
+
previewDelete の previewId を確定した時は、代わりに以下を出力する:
|
|
32
|
+
commitId: <生成されたcommitのID>
|
|
33
|
+
title: <削除されたpage title>
|
|
34
|
+
deleted: true
|
|
35
|
+
|
|
31
36
|
HTTPエラー:
|
|
32
37
|
HTTP 400 preview を生成した時と違う project の URL を渡している
|
|
33
38
|
HTTP 401 認証なし
|
|
@@ -36,6 +41,9 @@ HTTPエラー:
|
|
|
36
41
|
HTTP 409 {"error":"NotFastForward","latest":...}
|
|
37
42
|
preview生成後にページが更新された。最新stateを再取得して ops を作り直し、
|
|
38
43
|
previewEdit からやり直す必要がある
|
|
44
|
+
HTTP 409 {"error":"NotFastForward","latest":null}
|
|
45
|
+
対象ページが既に存在しない。削除の確定でこれが返った場合、ページは別経路で
|
|
46
|
+
削除済みであり、目的の状態は達成されている。リトライ不要
|
|
39
47
|
HTTP 409 {"error":"DuplicateTitle"}
|
|
40
48
|
preview→submit の間に他人が同名ページを作った (race condition)
|
|
41
49
|
|
|
@@ -45,6 +53,7 @@ previewId は1回限り (consume-on-submit)。submit 後・5分 expire 後・con
|
|
|
45
53
|
interface SubmitResponse {
|
|
46
54
|
commitId: string;
|
|
47
55
|
page: { title?: string } | null;
|
|
56
|
+
pageDeleted?: { title?: string };
|
|
48
57
|
titleChanged?: { from?: string; to?: string };
|
|
49
58
|
}
|
|
50
59
|
|
|
@@ -64,6 +73,15 @@ export const submitEdit = async (args: string[]): Promise<void> => {
|
|
|
64
73
|
body: { previewId }
|
|
65
74
|
})) as SubmitResponse;
|
|
66
75
|
|
|
76
|
+
// pageDeletedはページ削除commitの確定時に返る。削除されたページにURLは無い
|
|
77
|
+
const pageDeleted = response.pageDeleted;
|
|
78
|
+
if (pageDeleted) {
|
|
79
|
+
process.stdout.write(
|
|
80
|
+
`commitId: ${response.commitId}\ntitle: ${pageDeleted.title ?? ''}\ndeleted: true\n`
|
|
81
|
+
);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
67
85
|
const title = response.page?.title;
|
|
68
86
|
if (typeof title !== 'string') {
|
|
69
87
|
throw new Error(
|