@helpfeel/cosense-cli 1.4.4 → 1.4.5
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 +10 -0
- package/src/commands/browsePage.ts +11 -2
- package/src/commands/browsePageChanges.ts +239 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -7,6 +7,11 @@ import {
|
|
|
7
7
|
browsePageHelp,
|
|
8
8
|
browsePageSummary
|
|
9
9
|
} from './commands/browsePage.ts';
|
|
10
|
+
import {
|
|
11
|
+
browsePageChanges,
|
|
12
|
+
browsePageChangesHelp,
|
|
13
|
+
browsePageChangesSummary
|
|
14
|
+
} from './commands/browsePageChanges.ts';
|
|
10
15
|
import {
|
|
11
16
|
browseRelatedPages,
|
|
12
17
|
browseRelatedPagesHelp,
|
|
@@ -103,6 +108,11 @@ const commands: Record<string, CommandSpec> = {
|
|
|
103
108
|
summary: browsePageSummary,
|
|
104
109
|
help: browsePageHelp
|
|
105
110
|
},
|
|
111
|
+
browsePageChanges: {
|
|
112
|
+
handler: browsePageChanges,
|
|
113
|
+
summary: browsePageChangesSummary,
|
|
114
|
+
help: browsePageChangesHelp
|
|
115
|
+
},
|
|
106
116
|
browseRelatedPages: {
|
|
107
117
|
handler: browseRelatedPages,
|
|
108
118
|
summary: browseRelatedPagesSummary,
|
|
@@ -31,8 +31,11 @@ Usage:
|
|
|
31
31
|
# <title>
|
|
32
32
|
|
|
33
33
|
## メタデータ
|
|
34
|
-
タイトル /
|
|
35
|
-
views / snapshot / 行数 / 文字数 / 作成者 / 最終更新者 /
|
|
34
|
+
タイトル / pageId / commitId / 作成日時 / 最終更新日時 / 最終アクセス日時 /
|
|
35
|
+
被リンク数 / pageRank / views / snapshot / 行数 / 文字数 / 作成者 / 最終更新者 /
|
|
36
|
+
関わったユーザー
|
|
37
|
+
pageId はページの不変ID、commitId は閲覧時点の最新コミット。共同編集中に
|
|
38
|
+
ページを見失ったら、これらを browsePageChanges に渡して変更(リネーム含む)を辿れる
|
|
36
39
|
|
|
37
40
|
## 人間のアイコン記法
|
|
38
41
|
本文中の [name.icon] のうち、現メンバーまたは退去済みメンバー (memberSnapshots) の
|
|
@@ -79,6 +82,8 @@ interface UserRef {
|
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
interface PageData {
|
|
85
|
+
id?: string;
|
|
86
|
+
commitId?: string;
|
|
82
87
|
title?: string;
|
|
83
88
|
persistent?: boolean;
|
|
84
89
|
pageRank?: number;
|
|
@@ -155,6 +160,10 @@ const buildTelomere = (lines: PageLine[]): TelomereEntry[] => {
|
|
|
155
160
|
const renderMetadata = (page: PageData): string => {
|
|
156
161
|
const items: string[] = [];
|
|
157
162
|
if (typeof page.title === 'string') items.push(`- タイトル: ${page.title}`);
|
|
163
|
+
if (typeof page.id === 'string') items.push(`- pageId: ${page.id}`);
|
|
164
|
+
if (typeof page.commitId === 'string') {
|
|
165
|
+
items.push(`- commitId: ${page.commitId}`);
|
|
166
|
+
}
|
|
158
167
|
if (page.created) items.push(`- 作成日時: ${page.created}`);
|
|
159
168
|
if (page.updated) items.push(`- 最終更新日時: ${page.updated}`);
|
|
160
169
|
if (page.accessed) items.push(`- 最終アクセス日時: ${page.accessed}`);
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { encodeTitleForUrl } from '../lib/encodeTitle.ts';
|
|
2
|
+
import { formatTimestamp } from '../lib/formatTimestamp.ts';
|
|
3
|
+
import { parseProjectUrlStrict } from '../lib/parseUrl.ts';
|
|
4
|
+
import { requestJson } from '../lib/request.ts';
|
|
5
|
+
import { fetchUserMap, type UserMap } from '../lib/resolveUsers.ts';
|
|
6
|
+
import { resolveCredential } from '../lib/settings.ts';
|
|
7
|
+
|
|
8
|
+
export const browsePageChangesSummary =
|
|
9
|
+
'ページの編集履歴(commit)をpageId起点で取得し、誰がいつ何をどう変えたかを自然言語で説明する。タイトル変更(リネーム)も検出する';
|
|
10
|
+
|
|
11
|
+
export const browsePageChangesHelp = `browsePageChanges - ページの編集履歴(commit)をpageId起点で取得し自然言語で説明する
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
cosense browsePageChanges <projectUrl> <pageId> [--since <commitId>]
|
|
15
|
+
|
|
16
|
+
引数:
|
|
17
|
+
<projectUrl> プロジェクトのURL(例: https://scrapbox.io/shokai)
|
|
18
|
+
<pageId> ページの不変ID。browsePage / readPage の出力に含まれる
|
|
19
|
+
--since <commitId> 指定した commitId より後の変更だけを対象にする(カーソル)。
|
|
20
|
+
browsePage / readPage 出力の commitId を控えておき、それを渡すと
|
|
21
|
+
「前回読んだ後に何が変わったか」だけを取得できる。
|
|
22
|
+
省略すると全履歴を対象にする。
|
|
23
|
+
|
|
24
|
+
なぜpageId起点か:
|
|
25
|
+
ページタイトルはURLになるため、共同編集者がタイトルを変更すると旧タイトルでは読めなくなる。
|
|
26
|
+
ページの不変IDである pageId から編集履歴を辿れば、リネームされても変更を追える。
|
|
27
|
+
|
|
28
|
+
出力形式(Markdown plain text。JSONではない):
|
|
29
|
+
# ページ変更履歴
|
|
30
|
+
pageId / 範囲(--since より後か全履歴か)/ commit数 / title change(変更後タイトル、無ければ なし)
|
|
31
|
+
タイトルが変わっていた時のみ、新URLも出力する
|
|
32
|
+
## 変更
|
|
33
|
+
時系列の変更イベントを「<日時>\t<誰>が<何を>」の形(TAB区切り)で列挙する。
|
|
34
|
+
同一行への連続した編集は1件にまとめ、変更前→最終のテキストを示す。
|
|
35
|
+
本文編集に伴う派生メタデータ(links / icons / linesCount 等)は出力しない。
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
interface CommitLine {
|
|
39
|
+
id?: string;
|
|
40
|
+
text?: string;
|
|
41
|
+
origText?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface Change {
|
|
45
|
+
title?: string;
|
|
46
|
+
_insert?: string;
|
|
47
|
+
_update?: string;
|
|
48
|
+
_delete?: string;
|
|
49
|
+
lines?: CommitLine;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface Commit {
|
|
53
|
+
id?: string;
|
|
54
|
+
changes?: Change[];
|
|
55
|
+
userId?: string;
|
|
56
|
+
created?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface CommitsResponse {
|
|
60
|
+
commits?: Commit[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface ParsedArgs {
|
|
64
|
+
projectUrl: string;
|
|
65
|
+
pageId: string;
|
|
66
|
+
since: string | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const parseArgs = (args: string[]): ParsedArgs => {
|
|
70
|
+
const usage =
|
|
71
|
+
'Usage: cosense browsePageChanges <projectUrl> <pageId> [--since <commitId>]';
|
|
72
|
+
let since: string | undefined;
|
|
73
|
+
const positional: string[] = [];
|
|
74
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
75
|
+
const arg = args[i] as string;
|
|
76
|
+
if (arg === '--since') {
|
|
77
|
+
const value = args[i + 1];
|
|
78
|
+
if (value === undefined || value.startsWith('--')) {
|
|
79
|
+
throw new Error(`--since requires a commitId\n${usage}`);
|
|
80
|
+
}
|
|
81
|
+
since = value;
|
|
82
|
+
i += 1;
|
|
83
|
+
} else if (arg.startsWith('--')) {
|
|
84
|
+
throw new Error(`Unknown option: ${arg}\n${usage}`);
|
|
85
|
+
} else {
|
|
86
|
+
positional.push(arg);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (positional.length !== 2) {
|
|
90
|
+
throw new Error(usage);
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
projectUrl: positional[0] as string,
|
|
94
|
+
pageId: positional[1] as string,
|
|
95
|
+
since
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const resolveUserName = (
|
|
100
|
+
userId: string | undefined,
|
|
101
|
+
userMap: UserMap
|
|
102
|
+
): string => {
|
|
103
|
+
if (!userId) return '不明なユーザー';
|
|
104
|
+
const info = userMap.get(userId);
|
|
105
|
+
return info?.displayName ?? info?.name ?? userId;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const quote = (text: string | undefined): string => `「${text ?? ''}」`;
|
|
109
|
+
|
|
110
|
+
// 時系列の変更イベント。同一行への連続updateは1件に畳む
|
|
111
|
+
type ChangeEvent =
|
|
112
|
+
| { kind: 'title'; userIds: string[]; created?: number; title: string }
|
|
113
|
+
| { kind: 'insert'; userIds: string[]; created?: number; text: string }
|
|
114
|
+
| {
|
|
115
|
+
kind: 'update';
|
|
116
|
+
userIds: string[];
|
|
117
|
+
created?: number;
|
|
118
|
+
lineId: string;
|
|
119
|
+
origText: string;
|
|
120
|
+
text: string;
|
|
121
|
+
}
|
|
122
|
+
| { kind: 'delete'; userIds: string[]; created?: number; origText: string };
|
|
123
|
+
|
|
124
|
+
const buildEvents = (commits: Commit[]): ChangeEvent[] => {
|
|
125
|
+
const events: ChangeEvent[] = [];
|
|
126
|
+
for (const commit of commits) {
|
|
127
|
+
const userId = commit.userId ?? '';
|
|
128
|
+
const created = commit.created;
|
|
129
|
+
for (const change of commit.changes ?? []) {
|
|
130
|
+
if (typeof change.title === 'string') {
|
|
131
|
+
events.push({
|
|
132
|
+
kind: 'title',
|
|
133
|
+
userIds: [userId],
|
|
134
|
+
created,
|
|
135
|
+
title: change.title
|
|
136
|
+
});
|
|
137
|
+
} else if (typeof change._insert === 'string') {
|
|
138
|
+
events.push({
|
|
139
|
+
kind: 'insert',
|
|
140
|
+
userIds: [userId],
|
|
141
|
+
created,
|
|
142
|
+
text: change.lines?.text ?? ''
|
|
143
|
+
});
|
|
144
|
+
} else if (typeof change._update === 'string') {
|
|
145
|
+
const lineId = change._update;
|
|
146
|
+
const last = events[events.length - 1];
|
|
147
|
+
// 同一行への連続したupdateを畳む。origTextは最初の値を保ち、textを最新で上書き
|
|
148
|
+
if (last && last.kind === 'update' && last.lineId === lineId) {
|
|
149
|
+
last.text = change.lines?.text ?? '';
|
|
150
|
+
last.created = created;
|
|
151
|
+
if (userId && !last.userIds.includes(userId)) {
|
|
152
|
+
last.userIds.push(userId);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
events.push({
|
|
156
|
+
kind: 'update',
|
|
157
|
+
userIds: [userId],
|
|
158
|
+
created,
|
|
159
|
+
lineId,
|
|
160
|
+
origText: change.lines?.origText ?? '',
|
|
161
|
+
text: change.lines?.text ?? ''
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
} else if (typeof change._delete === 'string') {
|
|
165
|
+
events.push({
|
|
166
|
+
kind: 'delete',
|
|
167
|
+
userIds: [userId],
|
|
168
|
+
created,
|
|
169
|
+
origText: change.lines?.origText ?? ''
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
// links / icons / projectLinks / descriptions / linesCount / charsCount /
|
|
173
|
+
// helpfeels / infobox* などは本文編集に伴う派生メタなので無視する
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return events;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const renderEvent = (event: ChangeEvent, userMap: UserMap): string => {
|
|
180
|
+
const who = event.userIds.map(id => resolveUserName(id, userMap)).join('、');
|
|
181
|
+
const when = formatTimestamp(event.created);
|
|
182
|
+
const prefix = when ? `${when}\t` : '';
|
|
183
|
+
switch (event.kind) {
|
|
184
|
+
case 'title':
|
|
185
|
+
return `${prefix}${who}がタイトルを${quote(event.title)}に変更`;
|
|
186
|
+
case 'insert':
|
|
187
|
+
return `${prefix}${who}が行を追加 ${quote(event.text)}`;
|
|
188
|
+
case 'update':
|
|
189
|
+
return `${prefix}${who}が行を編集 ${quote(event.origText)}→${quote(event.text)}`;
|
|
190
|
+
case 'delete':
|
|
191
|
+
return `${prefix}${who}が行を削除 ${quote(event.origText)}`;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export const browsePageChanges = async (args: string[]): Promise<void> => {
|
|
196
|
+
const { projectUrl, pageId, since } = parseArgs(args);
|
|
197
|
+
const { origin, projectName } = parseProjectUrlStrict(projectUrl);
|
|
198
|
+
|
|
199
|
+
let apiUrl = `${origin}/api/commits/${projectName}/${pageId}`;
|
|
200
|
+
if (since) {
|
|
201
|
+
apiUrl += `?head=${encodeURIComponent(since)}`;
|
|
202
|
+
}
|
|
203
|
+
const credential = resolveCredential(origin, projectName);
|
|
204
|
+
|
|
205
|
+
const [data, userMap] = await Promise.all([
|
|
206
|
+
requestJson(apiUrl, { credential }) as Promise<CommitsResponse>,
|
|
207
|
+
fetchUserMap(origin, projectName)
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
const commits = data.commits ?? [];
|
|
211
|
+
const events = buildEvents(commits);
|
|
212
|
+
|
|
213
|
+
const titleEvents = events.filter(e => e.kind === 'title');
|
|
214
|
+
const latestTitle = titleEvents.at(-1)?.title;
|
|
215
|
+
|
|
216
|
+
const sections: string[] = ['# ページ変更履歴'];
|
|
217
|
+
|
|
218
|
+
const meta: string[] = [
|
|
219
|
+
`- pageId: ${pageId}`,
|
|
220
|
+
`- 範囲: ${since ? `commit ${since} より後` : '全履歴'}`,
|
|
221
|
+
`- commit数: ${commits.length}`,
|
|
222
|
+
`- title change: ${latestTitle !== undefined ? quote(latestTitle) : 'なし'}`
|
|
223
|
+
];
|
|
224
|
+
// タイトルが変わっていればURLも変わっている。新URLを添える
|
|
225
|
+
if (latestTitle !== undefined) {
|
|
226
|
+
const newUrl = `${origin}/${projectName}/${encodeTitleForUrl(latestTitle)}`;
|
|
227
|
+
meta.push(`- 新URL: ${newUrl}`);
|
|
228
|
+
}
|
|
229
|
+
sections.push(meta.join('\n'));
|
|
230
|
+
|
|
231
|
+
if (events.length === 0) {
|
|
232
|
+
sections.push('## 変更\n\nこの範囲に説明できる変更はありません');
|
|
233
|
+
} else {
|
|
234
|
+
const lines = events.map(event => renderEvent(event, userMap));
|
|
235
|
+
sections.push(`## 変更\n\n${lines.join('\n')}`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
process.stdout.write(`${sections.join('\n\n')}\n`);
|
|
239
|
+
};
|