@helpfeel/cosense-cli 1.3.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/README.md +30 -0
- package/bin/cosense +3 -0
- package/package.json +43 -0
- package/src/cli.ts +214 -0
- package/src/commands/browsePage.ts +324 -0
- package/src/commands/browseRelatedPages.ts +79 -0
- package/src/commands/list1hopLinks.ts +66 -0
- package/src/commands/list2hopLinks.ts +53 -0
- package/src/commands/listPages.ts +164 -0
- package/src/commands/listProjects.ts +67 -0
- package/src/commands/login.ts +102 -0
- package/src/commands/previewEdit.ts +349 -0
- package/src/commands/readPage.ts +148 -0
- package/src/commands/readProjectMembers.ts +89 -0
- package/src/commands/search1hopLinks.ts +43 -0
- package/src/commands/search2hopLinks.ts +43 -0
- package/src/commands/searchFullText.ts +85 -0
- package/src/commands/searchVector.ts +86 -0
- package/src/commands/submitEdit.ts +73 -0
- package/src/commands/whoami.ts +48 -0
- package/src/lib/annotateRelations.ts +93 -0
- package/src/lib/encodeTitle.ts +13 -0
- package/src/lib/enrichTimestamps.ts +21 -0
- package/src/lib/formatTimestamp.ts +55 -0
- package/src/lib/parseUrl.ts +60 -0
- package/src/lib/relatedPages.ts +15 -0
- package/src/lib/relatedPagesFormat.ts +77 -0
- package/src/lib/request.ts +72 -0
- package/src/lib/resolveUsers.ts +107 -0
- package/src/lib/settings.ts +258 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { fetchRelatedPagesWithRelations } from '../lib/annotateRelations.ts';
|
|
2
|
+
import { enrichTimestampsOf } from '../lib/enrichTimestamps.ts';
|
|
3
|
+
import { parsePageUrl } from '../lib/parseUrl.ts';
|
|
4
|
+
import { enrichPageUsers, fetchUserMap } from '../lib/resolveUsers.ts';
|
|
5
|
+
|
|
6
|
+
export const list1hopLinksSummary = '1-hop近傍を取得する';
|
|
7
|
+
|
|
8
|
+
export const list1hopLinksHelp = `list1hopLinks - 1-hop近傍を取得する
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
cosense list1hopLinks <pageUrl>
|
|
12
|
+
|
|
13
|
+
引数:
|
|
14
|
+
<pageUrl> 対象ページの完全なURL
|
|
15
|
+
|
|
16
|
+
戻り値(top-levelの主なkey):
|
|
17
|
+
links1hop Array<Link> 1-hop近傍のページ配列
|
|
18
|
+
charsCount number 対象ページの文字数
|
|
19
|
+
hasBackLinksOrIcons boolean 被リンク・アイコン参照があるか
|
|
20
|
+
kcsControlTagsLc string[] 制御タグ
|
|
21
|
+
pagination object ページネーション情報
|
|
22
|
+
|
|
23
|
+
各 Link の field:
|
|
24
|
+
id, title, titleLc, image, descriptions(冒頭数行),
|
|
25
|
+
linksLc(リンク記法のtitleLc配列), linked(被リンク数), pageRank,
|
|
26
|
+
infoboxDefinition, infoboxDisableLinks,
|
|
27
|
+
created, updated, accessed, lastAccessed (string), charsCount,
|
|
28
|
+
user (作成者 User), lastUpdateUser (最終更新者 User | null), users (更新者リスト Array<User>),
|
|
29
|
+
relation 'outgoing' | 'incoming' | 'bidirectional'
|
|
30
|
+
outgoing 対象ページが参照しているページ(正リンク)
|
|
31
|
+
incoming 対象ページを参照しているページ(逆リンク)
|
|
32
|
+
bidirectional 双方向リンク
|
|
33
|
+
|
|
34
|
+
User の field(user / lastUpdateUser / users[] で共通):
|
|
35
|
+
id string Cosense内部のID
|
|
36
|
+
name string? ログイン名(自己紹介ページのタイトルや、本文中のアイコン記法で使われる)
|
|
37
|
+
displayName string? 表示名
|
|
38
|
+
email string? メールアドレス
|
|
39
|
+
|
|
40
|
+
戻り値のJSON抜粋例:
|
|
41
|
+
{
|
|
42
|
+
"links1hop": [
|
|
43
|
+
{
|
|
44
|
+
"id": "...", "title": "...", "linked": 4, "pageRank": 14,
|
|
45
|
+
"descriptions": ["..."], "linksLc": ["..."], "relation": "outgoing"
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
export const list1hopLinks = async (args: string[]): Promise<void> => {
|
|
52
|
+
if (args.length !== 1) {
|
|
53
|
+
throw new Error('Usage: cosense list1hopLinks <pageUrl>');
|
|
54
|
+
}
|
|
55
|
+
const [url] = args as [string];
|
|
56
|
+
const { origin, projectName } = parsePageUrl(url);
|
|
57
|
+
const [data, userMap] = await Promise.all([
|
|
58
|
+
fetchRelatedPagesWithRelations(url),
|
|
59
|
+
fetchUserMap(origin, projectName)
|
|
60
|
+
]);
|
|
61
|
+
for (const page of (data as { links1hop?: unknown[] }).links1hop ?? []) {
|
|
62
|
+
enrichPageUsers(page as Record<string, unknown>, userMap);
|
|
63
|
+
enrichTimestampsOf(page as Record<string, unknown>);
|
|
64
|
+
}
|
|
65
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
66
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { enrichTimestampsOf } from '../lib/enrichTimestamps.ts';
|
|
2
|
+
import { parsePageUrl } from '../lib/parseUrl.ts';
|
|
3
|
+
import { fetchRelatedPages } from '../lib/relatedPages.ts';
|
|
4
|
+
import { enrichPageUsers, fetchUserMap } from '../lib/resolveUsers.ts';
|
|
5
|
+
|
|
6
|
+
export const list2hopLinksSummary = '2-hop近傍を取得する';
|
|
7
|
+
|
|
8
|
+
export const list2hopLinksHelp = `list2hopLinks - 2-hop近傍を取得する
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
cosense list2hopLinks <pageUrl>
|
|
12
|
+
|
|
13
|
+
引数:
|
|
14
|
+
<pageUrl> 対象ページの完全なURL
|
|
15
|
+
|
|
16
|
+
戻り値(top-levelの主なkey):
|
|
17
|
+
links2hop Array<Link> 2-hop近傍のページ配列
|
|
18
|
+
hiddenHeadwordsLc string[] 非表示のヘッドワード
|
|
19
|
+
pagination object ページネーション情報
|
|
20
|
+
|
|
21
|
+
各 Link の field:
|
|
22
|
+
id, title, titleLc, image, descriptions, linksLc, linked, pageRank,
|
|
23
|
+
infoboxDefinition, infoboxDisableLinks,
|
|
24
|
+
created, updated, accessed, lastAccessed (string), charsCount,
|
|
25
|
+
user (作成者 User), lastUpdateUser (最終更新者 User | null), users (更新者リスト Array<User>)
|
|
26
|
+
|
|
27
|
+
User の field(user / lastUpdateUser / users[] で共通):
|
|
28
|
+
id string Cosense内部のID
|
|
29
|
+
name string? ログイン名(自己紹介ページのタイトルや、本文中のアイコン記法で使われる)
|
|
30
|
+
displayName string? 表示名
|
|
31
|
+
email string? メールアドレス
|
|
32
|
+
|
|
33
|
+
注意:
|
|
34
|
+
- 直接の1-hop近傍は含まれない
|
|
35
|
+
- list1hopLinks と異なり、relation field は付与されない
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
export const list2hopLinks = async (args: string[]): Promise<void> => {
|
|
39
|
+
if (args.length !== 1) {
|
|
40
|
+
throw new Error('Usage: cosense list2hopLinks <pageUrl>');
|
|
41
|
+
}
|
|
42
|
+
const [url] = args as [string];
|
|
43
|
+
const { origin, projectName } = parsePageUrl(url);
|
|
44
|
+
const [data, userMap] = await Promise.all([
|
|
45
|
+
fetchRelatedPages(url, 2),
|
|
46
|
+
fetchUserMap(origin, projectName)
|
|
47
|
+
]);
|
|
48
|
+
for (const page of (data as { links2hop?: unknown[] }).links2hop ?? []) {
|
|
49
|
+
enrichPageUsers(page as Record<string, unknown>, userMap);
|
|
50
|
+
enrichTimestampsOf(page as Record<string, unknown>);
|
|
51
|
+
}
|
|
52
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
53
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { enrichTimestampsOf } from '../lib/enrichTimestamps.ts';
|
|
2
|
+
import { parseProjectUrl } from '../lib/parseUrl.ts';
|
|
3
|
+
import { requestJson } from '../lib/request.ts';
|
|
4
|
+
import { enrichPageUsers, fetchUserMap } from '../lib/resolveUsers.ts';
|
|
5
|
+
import { resolveCredential } from '../lib/settings.ts';
|
|
6
|
+
|
|
7
|
+
export const listPagesSummary = 'プロジェクトのページ一覧を取得する';
|
|
8
|
+
|
|
9
|
+
export const listPagesHelp = `listPages - プロジェクトのページ一覧を取得する
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
cosense listPages <projectUrl> [options]
|
|
13
|
+
|
|
14
|
+
引数:
|
|
15
|
+
<projectUrl> プロジェクトのURL(例: https://scrapbox.io/shokai/)
|
|
16
|
+
|
|
17
|
+
オプション:
|
|
18
|
+
--sort <name> ソート順(既定: updated)
|
|
19
|
+
updated 更新日時降順
|
|
20
|
+
created 作成日時降順
|
|
21
|
+
accessed 最終アクセス降順
|
|
22
|
+
linked 被リンク数降順
|
|
23
|
+
views 閲覧数降順
|
|
24
|
+
title タイトル昇順
|
|
25
|
+
pinned page は常に先頭に来る
|
|
26
|
+
--limit <N> 1リクエストで返るページ数(既定 100、最大 1000)
|
|
27
|
+
--skip <N> 先頭から N 件スキップして取得(既定 0)
|
|
28
|
+
--filter <name> リストされるページを絞り込む。
|
|
29
|
+
本文中に [name.icon] を持つページと
|
|
30
|
+
指定したnameを持つユーザーがこれまでに編集したページが返る
|
|
31
|
+
ユーザー名で絞り込む場合は user.displayName ではなく users.name を指定する
|
|
32
|
+
自分の名前は whoami コマンドで確認できる
|
|
33
|
+
|
|
34
|
+
戻り値(top-levelの主なkey):
|
|
35
|
+
projectName string プロジェクト名
|
|
36
|
+
count number 条件に一致する総ページ数
|
|
37
|
+
limit number 1リクエストで返るページ数の上限(既定 100)
|
|
38
|
+
skip number スキップ件数(オフセット)
|
|
39
|
+
pages Array<Page> ページのメタデータ配列
|
|
40
|
+
|
|
41
|
+
各 Page の主なfield:
|
|
42
|
+
id, title, image, descriptions,
|
|
43
|
+
user (作成者 User), lastUpdateUser (最終更新者 User | null), users (更新者リスト Array<User>),
|
|
44
|
+
pin (pinned page なら正の値), views, linked,
|
|
45
|
+
linesCount, charsCount,
|
|
46
|
+
created, updated, accessed, lastAccessed (string)
|
|
47
|
+
|
|
48
|
+
User の field(user / lastUpdateUser / users[] で共通):
|
|
49
|
+
id string Cosense内部のID
|
|
50
|
+
name string? ログイン名(自己紹介ページのタイトルや、本文中のアイコン記法で使われる)
|
|
51
|
+
displayName string? 表示名
|
|
52
|
+
email string? メールアドレス
|
|
53
|
+
|
|
54
|
+
注意:
|
|
55
|
+
- ページ本文(lines)は含まれない。本文が必要なら readPage を使う
|
|
56
|
+
- 1000件を超える件数を1リクエストで取ることはできない。--skip でページネーションする
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const ALLOWED_SORTS = new Set([
|
|
60
|
+
'updated',
|
|
61
|
+
'created',
|
|
62
|
+
'accessed',
|
|
63
|
+
'linked',
|
|
64
|
+
'views',
|
|
65
|
+
'title'
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const NON_NEGATIVE_INT = /^(?:0|[1-9]\d*)$/;
|
|
69
|
+
|
|
70
|
+
interface ParsedArgs {
|
|
71
|
+
url: string;
|
|
72
|
+
sort?: string;
|
|
73
|
+
limit?: string;
|
|
74
|
+
skip?: string;
|
|
75
|
+
filter?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const parseArgs = (args: string[]): ParsedArgs => {
|
|
79
|
+
const usage =
|
|
80
|
+
'Usage: cosense listPages <projectUrl> [--sort <name>] [--limit <N>] [--skip <N>] [--filter <name>]';
|
|
81
|
+
let url: string | undefined;
|
|
82
|
+
const parsed: Partial<ParsedArgs> = {};
|
|
83
|
+
for (let i = 0; i < args.length; i++) {
|
|
84
|
+
const arg = args[i] as string;
|
|
85
|
+
const next = (): string => {
|
|
86
|
+
const value = args[i + 1];
|
|
87
|
+
if (value === undefined) throw new Error(`${arg} requires a value`);
|
|
88
|
+
i += 1;
|
|
89
|
+
return value;
|
|
90
|
+
};
|
|
91
|
+
if (arg === '--sort') {
|
|
92
|
+
const value = next();
|
|
93
|
+
if (!ALLOWED_SORTS.has(value)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Unknown --sort value: ${value}. Allowed: ${[...ALLOWED_SORTS].join(', ')}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
parsed.sort = value;
|
|
99
|
+
} else if (arg === '--limit') {
|
|
100
|
+
const value = next();
|
|
101
|
+
if (!NON_NEGATIVE_INT.test(value)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`--limit must be a non-negative integer, got: ${value}`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
parsed.limit = value;
|
|
107
|
+
} else if (arg === '--skip') {
|
|
108
|
+
const value = next();
|
|
109
|
+
if (!NON_NEGATIVE_INT.test(value)) {
|
|
110
|
+
throw new Error(`--skip must be a non-negative integer, got: ${value}`);
|
|
111
|
+
}
|
|
112
|
+
parsed.skip = value;
|
|
113
|
+
} else if (arg === '--filter') {
|
|
114
|
+
const value = next();
|
|
115
|
+
if (value.trim() === '') {
|
|
116
|
+
throw new Error('--filter must not be empty');
|
|
117
|
+
}
|
|
118
|
+
parsed.filter = value;
|
|
119
|
+
} else if (arg.startsWith('--')) {
|
|
120
|
+
throw new Error(`Unknown option: ${arg}\n${usage}`);
|
|
121
|
+
} else if (!url) {
|
|
122
|
+
url = arg;
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error(`Unexpected positional argument: ${arg}\n${usage}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!url) throw new Error(usage);
|
|
128
|
+
return { url, ...parsed };
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
interface PageEntry {
|
|
132
|
+
user?: { id: string } | null;
|
|
133
|
+
lastUpdateUser?: { id: string } | null;
|
|
134
|
+
users?: { id: string }[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface ListPagesData {
|
|
138
|
+
pages?: PageEntry[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export const listPages = async (args: string[]): Promise<void> => {
|
|
142
|
+
const { url, sort, limit, skip, filter } = parseArgs(args);
|
|
143
|
+
const { origin, projectName } = parseProjectUrl(url);
|
|
144
|
+
const params = new URLSearchParams();
|
|
145
|
+
if (sort) params.set('sort', sort);
|
|
146
|
+
if (limit) params.set('limit', limit);
|
|
147
|
+
if (skip) params.set('skip', skip);
|
|
148
|
+
if (filter !== undefined) {
|
|
149
|
+
params.set('filterType', 'icon');
|
|
150
|
+
params.set('filterValue', filter);
|
|
151
|
+
}
|
|
152
|
+
const queryString = params.toString();
|
|
153
|
+
const apiUrl = `${origin}/api/pages/${projectName}/${queryString ? `?${queryString}` : ''}`;
|
|
154
|
+
const credential = resolveCredential(origin, projectName);
|
|
155
|
+
const data = (await requestJson(apiUrl, { credential })) as ListPagesData;
|
|
156
|
+
|
|
157
|
+
const userMap = await fetchUserMap(origin, projectName);
|
|
158
|
+
for (const page of data.pages ?? []) {
|
|
159
|
+
enrichPageUsers(page, userMap);
|
|
160
|
+
enrichTimestampsOf(page as Record<string, unknown>);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
164
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { enrichTimestampsOf } from '../lib/enrichTimestamps.ts';
|
|
2
|
+
import { parseOrigin } from '../lib/parseUrl.ts';
|
|
3
|
+
import { requestJson } from '../lib/request.ts';
|
|
4
|
+
import { resolveUserCredential } from '../lib/settings.ts';
|
|
5
|
+
|
|
6
|
+
export const listProjectsSummary = '自分が参加しているprojectの一覧を取得する';
|
|
7
|
+
|
|
8
|
+
export const listProjectsHelp = `listProjects - 自分が参加しているprojectの一覧を取得する
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
cosense listProjects <origin>
|
|
12
|
+
|
|
13
|
+
引数:
|
|
14
|
+
<origin> Cosenseサーバーのorigin(例: https://scrapbox.io)
|
|
15
|
+
|
|
16
|
+
戻り値(top-levelの主なkey):
|
|
17
|
+
projects Array<Project> updated降順
|
|
18
|
+
|
|
19
|
+
Project の field:
|
|
20
|
+
id string Cosense内部のID
|
|
21
|
+
name string project名(URLパス)
|
|
22
|
+
displayName string 表示名
|
|
23
|
+
publicVisible boolean 公開projectかどうか
|
|
24
|
+
loginStrategies Array<string> 認証方法(google / microsoft / saml / email 等)
|
|
25
|
+
plan string? 課金plan名
|
|
26
|
+
additionalPlans { [planName]: boolean } 追加plan
|
|
27
|
+
alert object? project内に表示される通知
|
|
28
|
+
usersCount number メンバー数
|
|
29
|
+
isMember boolean 自分がメンバーか
|
|
30
|
+
billingId string? billing ID
|
|
31
|
+
created string 作成時刻
|
|
32
|
+
updated string 更新時刻
|
|
33
|
+
isOwner boolean? 自分がownerか
|
|
34
|
+
isAdmin boolean? 自分がadminか
|
|
35
|
+
adminsCount number? admin数
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
export const listProjects = async (args: string[]): Promise<void> => {
|
|
39
|
+
const [originArg] = args;
|
|
40
|
+
if (!originArg) throw new Error('Usage: cosense listProjects <origin>');
|
|
41
|
+
if (args.length > 1) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Unexpected positional argument: ${args[1]}\nUsage: cosense listProjects <origin>`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const origin = parseOrigin(originArg);
|
|
47
|
+
const credential = resolveUserCredential(origin);
|
|
48
|
+
if (!credential) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`No Personal Access Token found for ${origin}. Run \`cosense login ${origin}\` to authenticate.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const data = (await requestJson(`${origin}/api/projects`, {
|
|
54
|
+
credential
|
|
55
|
+
})) as { projects?: Record<string, unknown>[] };
|
|
56
|
+
if (data.projects) {
|
|
57
|
+
data.projects.sort((a, b) => {
|
|
58
|
+
const ua = typeof a.updated === 'number' ? a.updated : 0;
|
|
59
|
+
const ub = typeof b.updated === 'number' ? b.updated : 0;
|
|
60
|
+
return ub - ua;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
for (const project of data.projects ?? []) {
|
|
64
|
+
enrichTimestampsOf(project);
|
|
65
|
+
}
|
|
66
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
67
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { parseOrigin } from '../lib/parseUrl.ts';
|
|
2
|
+
import { settingsPath, writeUserToken } from '../lib/settings.ts';
|
|
3
|
+
|
|
4
|
+
export const loginSummary = 'Personal Access Tokenを設定ファイルに保存する';
|
|
5
|
+
|
|
6
|
+
export const loginHelp = `login - Personal Access Tokenを設定ファイルに保存する
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
cosense login <origin>
|
|
10
|
+
|
|
11
|
+
引数:
|
|
12
|
+
<origin> Cosenseサーバーのorigin(例: https://scrapbox.io)
|
|
13
|
+
|
|
14
|
+
動作:
|
|
15
|
+
- PAT発行URL(<origin>/settings/personal-access-tokens)を出力し、PAT入力を求める
|
|
16
|
+
- 入力されたPATを ~/.cosense/settings.json の users[] に書き込む
|
|
17
|
+
- 同じoriginの既存entryは上書きされる
|
|
18
|
+
- 設定ファイルとディレクトリは存在しなければ作成する(dir 0700, file 0600)
|
|
19
|
+
- interactive terminal(TTY)でのみ動作する
|
|
20
|
+
|
|
21
|
+
環境変数:
|
|
22
|
+
COSENSE_PAT 設定されていれば、ファイルに保存された認証情報より優先される
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const readMaskedLine = async (): Promise<string> => {
|
|
26
|
+
if (!process.stdin.isTTY) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'cosense login must be run in an interactive terminal (TTY)'
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
process.stdin.setRawMode(true);
|
|
32
|
+
process.stdin.resume();
|
|
33
|
+
const chars: string[] = [];
|
|
34
|
+
const cleanup = (): void => {
|
|
35
|
+
process.stdin.setRawMode(false);
|
|
36
|
+
process.stdin.pause();
|
|
37
|
+
};
|
|
38
|
+
try {
|
|
39
|
+
for await (const chunk of process.stdin) {
|
|
40
|
+
const text = (chunk as Buffer).toString('utf8');
|
|
41
|
+
for (const ch of text) {
|
|
42
|
+
const code = ch.charCodeAt(0);
|
|
43
|
+
if (code === 13 || code === 10) {
|
|
44
|
+
process.stdout.write('\n');
|
|
45
|
+
return chars.join('');
|
|
46
|
+
}
|
|
47
|
+
if (code === 3 || code === 4) {
|
|
48
|
+
cleanup();
|
|
49
|
+
process.stdout.write('\n');
|
|
50
|
+
process.exit(130);
|
|
51
|
+
}
|
|
52
|
+
if (code === 127 || code === 8) {
|
|
53
|
+
if (chars.length > 0) {
|
|
54
|
+
chars.pop();
|
|
55
|
+
process.stdout.write('\b \b');
|
|
56
|
+
}
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (code < 32) continue;
|
|
60
|
+
chars.push(ch);
|
|
61
|
+
process.stdout.write('*');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
cleanup();
|
|
66
|
+
}
|
|
67
|
+
process.stdout.write('\n');
|
|
68
|
+
return chars.join('');
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const login = async (args: string[]): Promise<void> => {
|
|
72
|
+
const [originArg, ...rest] = args;
|
|
73
|
+
if (!originArg) throw new Error('Usage: cosense login <origin>');
|
|
74
|
+
if (rest.length > 0) {
|
|
75
|
+
throw new Error(`Unexpected argument: ${rest[0]}`);
|
|
76
|
+
}
|
|
77
|
+
const origin = parseOrigin(originArg);
|
|
78
|
+
|
|
79
|
+
if (!process.stdin.isTTY) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
'cosense login must be run in an interactive terminal (TTY)'
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
process.stdout.write(
|
|
86
|
+
[
|
|
87
|
+
`Personal Access Token (PAT) を発行する:`,
|
|
88
|
+
` ${origin}/settings/personal-access-tokens`,
|
|
89
|
+
``,
|
|
90
|
+
`発行したPATを以下に貼り付けてEnter(入力は * でマスクされる):`,
|
|
91
|
+
`PAT: `
|
|
92
|
+
].join('\n')
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const token = (await readMaskedLine()).trim();
|
|
96
|
+
if (!token) {
|
|
97
|
+
throw new Error('PAT is empty');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
writeUserToken(origin, token);
|
|
101
|
+
process.stdout.write(`Saved PAT for ${origin} to ${settingsPath}\n`);
|
|
102
|
+
};
|