@helpfeel/cosense-cli 1.5.2 → 1.6.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/commands/browsePage.ts +11 -3
- package/src/commands/browsePageChanges.ts +8 -2
- package/src/commands/readProjectMembers.ts +14 -2
- package/src/commands/search1hopLinks.ts +32 -10
- package/src/commands/search2hopLinks.ts +32 -10
- package/src/commands/searchFullText.ts +57 -7
- package/src/lib/annotateRelations.ts +4 -2
- package/src/lib/relatedPages.ts +4 -2
- package/src/lib/resolveUsers.ts +38 -0
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import { requestJson } from '../lib/request.ts';
|
|
|
11
11
|
import {
|
|
12
12
|
enrichPageUsers,
|
|
13
13
|
fetchUserMap,
|
|
14
|
+
serviceAccountSuffix,
|
|
14
15
|
type UserMap
|
|
15
16
|
} from '../lib/resolveUsers.ts';
|
|
16
17
|
import { resolveCredential } from '../lib/settings.ts';
|
|
@@ -85,6 +86,8 @@ interface UserRef {
|
|
|
85
86
|
id?: string;
|
|
86
87
|
name?: string;
|
|
87
88
|
displayName?: string;
|
|
89
|
+
isServiceAccount?: boolean;
|
|
90
|
+
isServiceAccountDeleted?: boolean;
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
interface InfoboxResult {
|
|
@@ -129,8 +132,12 @@ const extractFragment = (input: string): string | null => {
|
|
|
129
132
|
|
|
130
133
|
const formatUserDisplay = (u: UserRef | null | undefined): string | null => {
|
|
131
134
|
if (!u) return null;
|
|
132
|
-
|
|
133
|
-
|
|
135
|
+
const base =
|
|
136
|
+
u.displayName && u.name
|
|
137
|
+
? `${u.displayName} (${u.name})`
|
|
138
|
+
: (u.displayName ?? u.name ?? u.id ?? null);
|
|
139
|
+
if (base === null) return null;
|
|
140
|
+
return `${base}${serviceAccountSuffix(u)}`;
|
|
134
141
|
};
|
|
135
142
|
|
|
136
143
|
const formatDateYMD = (unixSec: number): string => {
|
|
@@ -232,7 +239,8 @@ const renderTelomere = (
|
|
|
232
239
|
if (entries.length === 0) return null;
|
|
233
240
|
const lines = entries.map(e => {
|
|
234
241
|
const info = userMap.get(e.userId);
|
|
235
|
-
const
|
|
242
|
+
const base = info?.displayName ?? info?.name ?? e.userId;
|
|
243
|
+
const name = info ? `${base}${serviceAccountSuffix(info)}` : base;
|
|
236
244
|
return `- ${name}\t更新期間 ${formatDateYMD(e.minUpdated)} 〜 ${formatDateYMD(e.maxUpdated)}\t${e.lineCount}行更新`;
|
|
237
245
|
});
|
|
238
246
|
return `## テロメアのサマリー\n\n${lines.join('\n')}`;
|
|
@@ -2,7 +2,11 @@ import { encodeTitleForUrl } from '../lib/encodeTitle.ts';
|
|
|
2
2
|
import { formatTimestamp } from '../lib/formatTimestamp.ts';
|
|
3
3
|
import { parseProjectUrlStrict } from '../lib/parseUrl.ts';
|
|
4
4
|
import { requestJson } from '../lib/request.ts';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
fetchUserMap,
|
|
7
|
+
serviceAccountSuffix,
|
|
8
|
+
type UserMap
|
|
9
|
+
} from '../lib/resolveUsers.ts';
|
|
6
10
|
import { resolveCredential } from '../lib/settings.ts';
|
|
7
11
|
|
|
8
12
|
export const browsePageChangesSummary =
|
|
@@ -102,7 +106,9 @@ const resolveUserName = (
|
|
|
102
106
|
): string => {
|
|
103
107
|
if (!userId) return '不明なユーザー';
|
|
104
108
|
const info = userMap.get(userId);
|
|
105
|
-
|
|
109
|
+
if (!info) return userId;
|
|
110
|
+
const base = info.displayName ?? info.name ?? userId;
|
|
111
|
+
return `${base}${serviceAccountSuffix(info)}`;
|
|
106
112
|
};
|
|
107
113
|
|
|
108
114
|
const quote = (text: string | undefined): string => `「${text ?? ''}」`;
|
|
@@ -14,8 +14,10 @@ Usage:
|
|
|
14
14
|
<projectUrl> プロジェクトのURL(例: https://scrapbox.io/shokai)
|
|
15
15
|
|
|
16
16
|
戻り値(top-levelの主なkey):
|
|
17
|
-
users
|
|
18
|
-
memberSnapshots
|
|
17
|
+
users Array<User> 現メンバー一覧
|
|
18
|
+
memberSnapshots Array<Snapshot>? 退去済みメンバーの記録
|
|
19
|
+
serviceAccounts Array<ServiceAccount>? Service Account一覧
|
|
20
|
+
serviceAccountSnapshots Array<ServiceAccount>? 削除済みService Accountの記録
|
|
19
21
|
|
|
20
22
|
User の field:
|
|
21
23
|
id string Cosense内部のID
|
|
@@ -34,6 +36,10 @@ Snapshot の field:
|
|
|
34
36
|
updated string 退去者snapshot更新時刻
|
|
35
37
|
data { id, name?, displayName?, email? } 退去時のユーザー情報
|
|
36
38
|
|
|
39
|
+
ServiceAccount の field:
|
|
40
|
+
id string Service AccountのID(このIDでページ・行の著者を同定できる)
|
|
41
|
+
usage string 用途ラベル(著者の表示名として使う)
|
|
42
|
+
|
|
37
43
|
戻り値のJSON抜粋例:
|
|
38
44
|
{
|
|
39
45
|
"users": [
|
|
@@ -60,6 +66,12 @@ Snapshot の field:
|
|
|
60
66
|
"email": "former@example.com"
|
|
61
67
|
}
|
|
62
68
|
}
|
|
69
|
+
],
|
|
70
|
+
"serviceAccounts": [
|
|
71
|
+
{ "id": "6a44...", "usage": "CLI test" }
|
|
72
|
+
],
|
|
73
|
+
"serviceAccountSnapshots": [
|
|
74
|
+
{ "id": "6a1a...", "usage": "Cosense CLI" }
|
|
63
75
|
]
|
|
64
76
|
}
|
|
65
77
|
`;
|
|
@@ -8,11 +8,17 @@ export const search1hopLinksSummary = '1-hop近傍を全文検索でフィルタ
|
|
|
8
8
|
export const search1hopLinksHelp = `search1hopLinks - 1-hop近傍を全文検索でフィルタする
|
|
9
9
|
|
|
10
10
|
Usage:
|
|
11
|
-
cosense search1hopLinks <pageUrl> <query>
|
|
11
|
+
cosense search1hopLinks <pageUrl> <query> [--or]
|
|
12
12
|
|
|
13
13
|
引数:
|
|
14
14
|
<pageUrl> 対象ページの完全なURL
|
|
15
|
-
<query>
|
|
15
|
+
<query> 全文検索クエリ(必須。空文字は弾かれる)
|
|
16
|
+
|
|
17
|
+
オプション:
|
|
18
|
+
--or 複数語のいずれかにマッチするページを返す(既定はAND)
|
|
19
|
+
|
|
20
|
+
例:
|
|
21
|
+
cosense search1hopLinks https://scrapbox.io/shokai/カレー "うどん ラーメン" --or
|
|
16
22
|
|
|
17
23
|
戻り値(top-levelの主なkey):
|
|
18
24
|
links1hop Array<Link> query を本文に含む1-hop近傍ページ
|
|
@@ -20,19 +26,35 @@ Usage:
|
|
|
20
26
|
|
|
21
27
|
各 Link の field(list1hopLinksに加えて):
|
|
22
28
|
search 検索ハイライト情報
|
|
23
|
-
|
|
24
|
-
検索の制約:
|
|
25
|
-
- OR検索不可
|
|
26
29
|
`;
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
const parseArgs = (
|
|
32
|
+
args: string[]
|
|
33
|
+
): { url: string; query: string; or: boolean } => {
|
|
34
|
+
const usage = 'Usage: cosense search1hopLinks <pageUrl> <query> [--or]';
|
|
35
|
+
let or = false;
|
|
36
|
+
const positional: string[] = [];
|
|
37
|
+
for (const arg of args) {
|
|
38
|
+
if (arg === '--or') {
|
|
39
|
+
or = true;
|
|
40
|
+
} else if (arg.startsWith('--')) {
|
|
41
|
+
throw new Error(`Unknown option: ${arg}\n${usage}`);
|
|
42
|
+
} else {
|
|
43
|
+
positional.push(arg);
|
|
44
|
+
}
|
|
32
45
|
}
|
|
46
|
+
const [url, query] = positional;
|
|
47
|
+
if (positional.length !== 2 || !url || !query || query.trim() === '') {
|
|
48
|
+
throw new Error(usage);
|
|
49
|
+
}
|
|
50
|
+
return { url, query, or };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const search1hopLinks = async (args: string[]): Promise<void> => {
|
|
54
|
+
const { url, query, or } = parseArgs(args);
|
|
33
55
|
const { origin, projectName } = parsePageUrl(url);
|
|
34
56
|
const [data, userMap] = await Promise.all([
|
|
35
|
-
fetchRelatedPagesWithRelations(url, query),
|
|
57
|
+
fetchRelatedPagesWithRelations(url, query, or),
|
|
36
58
|
fetchUserMap(origin, projectName)
|
|
37
59
|
]);
|
|
38
60
|
for (const page of (data as { links1hop?: unknown[] }).links1hop ?? []) {
|
|
@@ -8,11 +8,17 @@ export const search2hopLinksSummary = '2-hop近傍を全文検索でフィルタ
|
|
|
8
8
|
export const search2hopLinksHelp = `search2hopLinks - 2-hop近傍を全文検索でフィルタする
|
|
9
9
|
|
|
10
10
|
Usage:
|
|
11
|
-
cosense search2hopLinks <pageUrl> <query>
|
|
11
|
+
cosense search2hopLinks <pageUrl> <query> [--or]
|
|
12
12
|
|
|
13
13
|
引数:
|
|
14
14
|
<pageUrl> 対象ページの完全なURL
|
|
15
|
-
<query>
|
|
15
|
+
<query> 全文検索クエリ(必須。空文字は弾かれる)
|
|
16
|
+
|
|
17
|
+
オプション:
|
|
18
|
+
--or 複数語のいずれかにマッチするページを返す(既定はAND)
|
|
19
|
+
|
|
20
|
+
例:
|
|
21
|
+
cosense search2hopLinks https://scrapbox.io/shokai/カレー "うどん ラーメン" --or
|
|
16
22
|
|
|
17
23
|
戻り値(top-levelの主なkey):
|
|
18
24
|
links2hop Array<Link> query を本文に含む2-hop近傍ページ
|
|
@@ -20,19 +26,35 @@ Usage:
|
|
|
20
26
|
|
|
21
27
|
各 Link の field(list2hopLinksに加えて):
|
|
22
28
|
search 検索ハイライト情報
|
|
23
|
-
|
|
24
|
-
検索の制約:
|
|
25
|
-
- OR検索不可
|
|
26
29
|
`;
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
const parseArgs = (
|
|
32
|
+
args: string[]
|
|
33
|
+
): { url: string; query: string; or: boolean } => {
|
|
34
|
+
const usage = 'Usage: cosense search2hopLinks <pageUrl> <query> [--or]';
|
|
35
|
+
let or = false;
|
|
36
|
+
const positional: string[] = [];
|
|
37
|
+
for (const arg of args) {
|
|
38
|
+
if (arg === '--or') {
|
|
39
|
+
or = true;
|
|
40
|
+
} else if (arg.startsWith('--')) {
|
|
41
|
+
throw new Error(`Unknown option: ${arg}\n${usage}`);
|
|
42
|
+
} else {
|
|
43
|
+
positional.push(arg);
|
|
44
|
+
}
|
|
32
45
|
}
|
|
46
|
+
const [url, query] = positional;
|
|
47
|
+
if (positional.length !== 2 || !url || !query || query.trim() === '') {
|
|
48
|
+
throw new Error(usage);
|
|
49
|
+
}
|
|
50
|
+
return { url, query, or };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const search2hopLinks = async (args: string[]): Promise<void> => {
|
|
54
|
+
const { url, query, or } = parseArgs(args);
|
|
33
55
|
const { origin, projectName } = parsePageUrl(url);
|
|
34
56
|
const [data, userMap] = await Promise.all([
|
|
35
|
-
fetchRelatedPages(url, 2, query),
|
|
57
|
+
fetchRelatedPages(url, 2, query, or),
|
|
36
58
|
fetchUserMap(origin, projectName)
|
|
37
59
|
]);
|
|
38
60
|
for (const page of (data as { links2hop?: unknown[] }).links2hop ?? []) {
|
|
@@ -9,12 +9,19 @@ export const searchFullTextSummary = '本文全文を対象に検索する';
|
|
|
9
9
|
export const searchFullTextHelp = `searchFullText - 本文全文を対象に検索する
|
|
10
10
|
|
|
11
11
|
Usage:
|
|
12
|
-
cosense searchFullText <projectUrl> <query>
|
|
12
|
+
cosense searchFullText <projectUrl> <query> [--or] [--sort <pageRank|updated>]
|
|
13
13
|
|
|
14
14
|
引数:
|
|
15
15
|
<projectUrl> プロジェクトのURL(例: https://scrapbox.io/shokai/)
|
|
16
16
|
<query> 検索クエリ
|
|
17
17
|
|
|
18
|
+
オプション:
|
|
19
|
+
--or 複数語のいずれかにマッチするページを返す(既定はAND)
|
|
20
|
+
--sort <pageRank|updated> 並び順(既定はpageRank)
|
|
21
|
+
|
|
22
|
+
例:
|
|
23
|
+
cosense searchFullText https://scrapbox.io/shokai/ "デザイン 設計 design UI UX プロトタイプ" --or
|
|
24
|
+
|
|
18
25
|
戻り値(top-levelの主なkey):
|
|
19
26
|
projectName string プロジェクト名
|
|
20
27
|
searchQuery string 実行されたクエリ
|
|
@@ -63,13 +70,56 @@ interface SearchFullTextData {
|
|
|
63
70
|
}[];
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
interface ParsedArgs {
|
|
74
|
+
projectUrl: string;
|
|
75
|
+
query: string;
|
|
76
|
+
or: boolean;
|
|
77
|
+
sort?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const parseArgs = (args: string[]): ParsedArgs => {
|
|
81
|
+
const usage =
|
|
82
|
+
'Usage: cosense searchFullText <projectUrl> <query> [--or] [--sort <pageRank|updated>]';
|
|
83
|
+
let or = false;
|
|
84
|
+
let sort: string | undefined;
|
|
85
|
+
const positional: string[] = [];
|
|
86
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
87
|
+
const arg = args[i] as string;
|
|
88
|
+
if (arg === '--or') {
|
|
89
|
+
or = true;
|
|
90
|
+
} else if (arg === '--sort') {
|
|
91
|
+
const value = args[i + 1];
|
|
92
|
+
if (value === undefined || value.startsWith('--')) {
|
|
93
|
+
throw new Error(`--sort requires a value\n${usage}`);
|
|
94
|
+
}
|
|
95
|
+
if (value !== 'pageRank' && value !== 'updated') {
|
|
96
|
+
throw new Error(`--sort must be pageRank or updated\n${usage}`);
|
|
97
|
+
}
|
|
98
|
+
sort = value;
|
|
99
|
+
i += 1;
|
|
100
|
+
} else if (arg.startsWith('--')) {
|
|
101
|
+
throw new Error(`Unknown option: ${arg}\n${usage}`);
|
|
102
|
+
} else {
|
|
103
|
+
positional.push(arg);
|
|
104
|
+
}
|
|
70
105
|
}
|
|
71
|
-
|
|
72
|
-
|
|
106
|
+
if (positional.length !== 2 || !positional[1]) {
|
|
107
|
+
throw new Error(usage);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
projectUrl: positional[0] as string,
|
|
111
|
+
query: positional[1] as string,
|
|
112
|
+
or,
|
|
113
|
+
sort
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const searchFullText = async (args: string[]): Promise<void> => {
|
|
118
|
+
const { projectUrl, query, or, sort } = parseArgs(args);
|
|
119
|
+
const { origin, projectName } = parseProjectUrl(projectUrl);
|
|
120
|
+
let apiUrl = `${origin}/api/pages/${projectName}/search/query?q=${encodeURIComponent(query)}`;
|
|
121
|
+
if (or) apiUrl += '&op=or';
|
|
122
|
+
if (sort) apiUrl += `&sort=${sort}`;
|
|
73
123
|
const credential = resolveCredential(origin, projectName);
|
|
74
124
|
const data = (await requestJson(apiUrl, {
|
|
75
125
|
credential
|
|
@@ -41,10 +41,12 @@ const computeRelation = (
|
|
|
41
41
|
|
|
42
42
|
export const fetchRelatedPagesWithRelations = async (
|
|
43
43
|
url: string,
|
|
44
|
-
query?: string
|
|
44
|
+
query?: string,
|
|
45
|
+
or?: boolean
|
|
45
46
|
): Promise<RelatedPagesData> => {
|
|
46
47
|
const { origin, projectName, encodedTitle } = parsePageUrl(url);
|
|
47
|
-
|
|
48
|
+
let queryParam = query ? `?search=${encodeURIComponent(query)}` : '';
|
|
49
|
+
if (or) queryParam += queryParam ? '&op=or' : '?op=or';
|
|
48
50
|
const startPageUrl = `${origin}/api/pages/v2/${projectName}/${encodedTitle}`;
|
|
49
51
|
const relatedUrl = `${startPageUrl}/links1hop${queryParam}`;
|
|
50
52
|
const credential = resolveCredential(origin, projectName);
|
package/src/lib/relatedPages.ts
CHANGED
|
@@ -5,10 +5,12 @@ import { resolveCredential } from './settings.ts';
|
|
|
5
5
|
export const fetchRelatedPages = async (
|
|
6
6
|
url: string,
|
|
7
7
|
hop: 1 | 2,
|
|
8
|
-
query?: string
|
|
8
|
+
query?: string,
|
|
9
|
+
or?: boolean
|
|
9
10
|
): Promise<unknown> => {
|
|
10
11
|
const { origin, projectName, encodedTitle } = parsePageUrl(url);
|
|
11
|
-
|
|
12
|
+
let queryParam = query ? `?search=${encodeURIComponent(query)}` : '';
|
|
13
|
+
if (or) queryParam += queryParam ? '&op=or' : '?op=or';
|
|
12
14
|
const apiUrl = `${origin}/api/pages/v2/${projectName}/${encodedTitle}/links${hop}hop${queryParam}`;
|
|
13
15
|
const credential = resolveCredential(origin, projectName);
|
|
14
16
|
return requestJson(apiUrl, { credential });
|
package/src/lib/resolveUsers.ts
CHANGED
|
@@ -5,6 +5,8 @@ interface UserInfo {
|
|
|
5
5
|
name?: string;
|
|
6
6
|
displayName?: string;
|
|
7
7
|
email?: string;
|
|
8
|
+
isServiceAccount?: boolean;
|
|
9
|
+
isServiceAccountDeleted?: boolean;
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export type UserMap = Map<string, UserInfo>;
|
|
@@ -20,9 +22,16 @@ interface MemberSnapshotEntry {
|
|
|
20
22
|
data?: UserEntry;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
interface ServiceAccountEntry {
|
|
26
|
+
id?: unknown;
|
|
27
|
+
usage?: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
interface UsersResponse {
|
|
24
31
|
users?: UserEntry[];
|
|
25
32
|
memberSnapshots?: MemberSnapshotEntry[];
|
|
33
|
+
serviceAccounts?: ServiceAccountEntry[];
|
|
34
|
+
serviceAccountSnapshots?: ServiceAccountEntry[];
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
const cache = new Map<string, UserMap>();
|
|
@@ -30,6 +39,18 @@ const cache = new Map<string, UserMap>();
|
|
|
30
39
|
const stringOrUndefined = (value: unknown): string | undefined =>
|
|
31
40
|
typeof value === 'string' && value !== '' ? value : undefined;
|
|
32
41
|
|
|
42
|
+
// Service Account著者の表示名末尾に付ける注記。名前を文字列整形するコマンド
|
|
43
|
+
// (browsePage / browsePageChanges) で共用し、usageが人間名に見えるのを防ぐ
|
|
44
|
+
export const serviceAccountSuffix = (info: {
|
|
45
|
+
isServiceAccount?: boolean;
|
|
46
|
+
isServiceAccountDeleted?: boolean;
|
|
47
|
+
}): string => {
|
|
48
|
+
if (!info.isServiceAccount) return '';
|
|
49
|
+
return info.isServiceAccountDeleted
|
|
50
|
+
? ' (deleted service account)'
|
|
51
|
+
: ' (service account)';
|
|
52
|
+
};
|
|
53
|
+
|
|
33
54
|
const buildUserInfo = (entry: UserEntry): UserInfo => {
|
|
34
55
|
const info: UserInfo = {};
|
|
35
56
|
const name = stringOrUndefined(entry.name);
|
|
@@ -66,6 +87,23 @@ export const fetchUserMap = async (
|
|
|
66
87
|
if (!id || map.has(id)) continue;
|
|
67
88
|
map.set(id, buildUserInfo(entry));
|
|
68
89
|
}
|
|
90
|
+
for (const entry of data.serviceAccounts ?? []) {
|
|
91
|
+
const id = stringOrUndefined(entry.id);
|
|
92
|
+
if (!id || map.has(id)) continue;
|
|
93
|
+
map.set(id, {
|
|
94
|
+
displayName: stringOrUndefined(entry.usage),
|
|
95
|
+
isServiceAccount: true
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
for (const entry of data.serviceAccountSnapshots ?? []) {
|
|
99
|
+
const id = stringOrUndefined(entry.id);
|
|
100
|
+
if (!id || map.has(id)) continue;
|
|
101
|
+
map.set(id, {
|
|
102
|
+
displayName: stringOrUndefined(entry.usage),
|
|
103
|
+
isServiceAccount: true,
|
|
104
|
+
isServiceAccountDeleted: true
|
|
105
|
+
});
|
|
106
|
+
}
|
|
69
107
|
|
|
70
108
|
cache.set(cacheKey, map);
|
|
71
109
|
return map;
|