@pachca/cli 2026.8.0 → 2026.8.2
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/CHANGELOG.md +42 -13
- package/README.md +2 -0
- package/dist/base-command.js +63 -6
- package/dist/client.js +33 -5
- package/dist/commands/auth/list.js +3 -1
- package/dist/commands/auth/login.js +184 -63
- package/dist/commands/auth/logout.js +38 -4
- package/dist/commands/auth/status.js +90 -3
- package/dist/commands/auth/switch.js +18 -4
- package/dist/commands/bots/list-company.js +98 -0
- package/dist/commands/chats/list-company.js +99 -0
- package/dist/commands/chats/list.js +8 -1
- package/dist/commands/doctor.js +71 -15
- package/dist/commands/introspect.js +2 -1
- package/dist/commands/messages/create.js +1 -1
- package/dist/commands/security/list.js +1 -1
- package/dist/commands/tasks/list.js +24 -1
- package/dist/commands/upgrade.js +1 -1
- package/dist/data/alternatives.json +2 -0
- package/dist/data/changelog.json +124 -0
- package/dist/data/endpoints.json +1 -1
- package/dist/data/workflows.json +48 -4
- package/dist/help.js +21 -0
- package/dist/hooks/command-not-found.js +1 -1
- package/dist/hooks/init.js +35 -6
- package/dist/lib/manifest.js +15 -4
- package/dist/oauth.js +231 -0
- package/dist/profiles.js +148 -2
- package/dist/secret-store.js +129 -0
- package/dist/token-refresh.js +136 -0
- package/oclif.manifest.json +1 -1
- package/package.json +8 -3
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import * as clack from '@clack/prompts';
|
|
3
3
|
import { BaseCommand } from '../../base-command.js';
|
|
4
|
-
import { listProfiles, deleteProfile } from '../../profiles.js';
|
|
4
|
+
import { listProfiles, deleteProfile, getAuthMethod, getSecretStorage, getProfile } from '../../profiles.js';
|
|
5
|
+
import { revokeToken } from '../../oauth.js';
|
|
5
6
|
import { outputError } from '../../output.js';
|
|
6
7
|
export default class AuthLogout extends BaseCommand {
|
|
7
8
|
static description = 'Удаление сохранённого профиля';
|
|
@@ -69,12 +70,45 @@ export default class AuthLogout extends BaseCommand {
|
|
|
69
70
|
outputError({ error: `Profile "${profileName}" not found`, type: 'PACHCA_USAGE_ERROR', code: null }, format);
|
|
70
71
|
this.exit(2);
|
|
71
72
|
}
|
|
73
|
+
const authMethod = getAuthMethod(profiles[profileName]);
|
|
74
|
+
const storage = getSecretStorage(profiles[profileName]);
|
|
75
|
+
// Put the token out server-side before dropping the profile — afterwards the
|
|
76
|
+
// secret is gone and there is nothing left to revoke with. Only tokens this
|
|
77
|
+
// CLI issued can revoke themselves; a pasted one is not ours to cancel.
|
|
78
|
+
let revoked = false;
|
|
79
|
+
if (authMethod === 'oauth') {
|
|
80
|
+
const secret = getProfile(profileName)?.token;
|
|
81
|
+
if (secret)
|
|
82
|
+
revoked = await revokeToken(secret);
|
|
83
|
+
}
|
|
72
84
|
deleteProfile(profileName);
|
|
73
85
|
if (format === 'json') {
|
|
74
|
-
this.output({
|
|
86
|
+
this.output({
|
|
87
|
+
deleted_profile: profileName,
|
|
88
|
+
auth: authMethod,
|
|
89
|
+
storage,
|
|
90
|
+
token_revoked: revoked,
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
75
93
|
}
|
|
76
|
-
|
|
77
|
-
|
|
94
|
+
this.success(`Профиль ${profileName} удалён`);
|
|
95
|
+
// Never claim access is closed when it is not: believing a logout revoked a
|
|
96
|
+
// token when it did not is worse than an extra line here.
|
|
97
|
+
if (!flags.quiet) {
|
|
98
|
+
if (authMethod === 'oauth') {
|
|
99
|
+
process.stderr.write(revoked
|
|
100
|
+
? ` Токен отозван на сервере и удалён с этой машины.\n`
|
|
101
|
+
: ` Токен удалён с этой машины. Отозвать его не удалось — перестанет действовать по истечении срока.\n`);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
process.stderr.write(` Токен удалён с этой машины, но продолжает действовать.\n`);
|
|
105
|
+
process.stderr.write(` Отзовите его в настройках Пачки, если он больше не нужен.\n`);
|
|
106
|
+
}
|
|
107
|
+
// Storage is a property of the profile, not of how it was created: a pasted
|
|
108
|
+
// token lives in the store just the same, and its entry goes away too.
|
|
109
|
+
if (storage === 'keyring') {
|
|
110
|
+
process.stderr.write(` Запись в хранилище ключей ОС удалена.\n`);
|
|
111
|
+
}
|
|
78
112
|
}
|
|
79
113
|
}
|
|
80
114
|
}
|
|
@@ -1,6 +1,22 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
1
2
|
import { BaseCommand } from '../../base-command.js';
|
|
2
|
-
import { getActiveProfile, getProfile } from '../../profiles.js';
|
|
3
|
+
import { getActiveProfile, getAuthMethod, getProfile, getSecretStorage, updateProfileScopes, } from '../../profiles.js';
|
|
4
|
+
import { request } from '../../client.js';
|
|
3
5
|
import { outputError } from '../../output.js';
|
|
6
|
+
/** Human-readable time left, or null when the token has no expiry at all. */
|
|
7
|
+
function describeExpiry(profile) {
|
|
8
|
+
if (getAuthMethod(profile) === 'token')
|
|
9
|
+
return null;
|
|
10
|
+
if (!profile.expires_at)
|
|
11
|
+
return null;
|
|
12
|
+
const msLeft = Date.parse(profile.expires_at) - Date.now();
|
|
13
|
+
if (Number.isNaN(msLeft))
|
|
14
|
+
return null;
|
|
15
|
+
if (msLeft <= 0)
|
|
16
|
+
return 'истёк, обновится при следующей команде';
|
|
17
|
+
const minutes = Math.round(msLeft / 60_000);
|
|
18
|
+
return minutes < 60 ? `осталось ${minutes} мин` : `осталось ${Math.round(minutes / 60)} ч`;
|
|
19
|
+
}
|
|
4
20
|
export default class AuthStatus extends BaseCommand {
|
|
5
21
|
static description = 'Статус текущего профиля';
|
|
6
22
|
static examples = [
|
|
@@ -9,6 +25,10 @@ export default class AuthStatus extends BaseCommand {
|
|
|
9
25
|
];
|
|
10
26
|
static flags = {
|
|
11
27
|
...BaseCommand.baseFlags,
|
|
28
|
+
remote: Flags.boolean({
|
|
29
|
+
description: 'Спросить права у сервера, а не показывать сохранённые при входе',
|
|
30
|
+
default: false,
|
|
31
|
+
}),
|
|
12
32
|
};
|
|
13
33
|
async run() {
|
|
14
34
|
const { flags } = await this.parse(AuthStatus);
|
|
@@ -24,17 +44,84 @@ export default class AuthStatus extends BaseCommand {
|
|
|
24
44
|
outputError({ error: `Profile "${profileName}" not found`, type: 'PACHCA_USAGE_ERROR', code: null }, format);
|
|
25
45
|
this.exit(2);
|
|
26
46
|
}
|
|
47
|
+
const authMethod = getAuthMethod(profile);
|
|
48
|
+
// The stored scope list is frozen at login. A role change since then makes it
|
|
49
|
+
// stale, and this is the command people are sent to when a call is refused —
|
|
50
|
+
// so offer to ask the server what the token actually carries.
|
|
51
|
+
let scopes = profile.scopes;
|
|
52
|
+
let drifted = false;
|
|
53
|
+
// Whether the answer really came from the server. `--remote` alone is not
|
|
54
|
+
// enough: the request can fail, and labelling the cached list as server truth
|
|
55
|
+
// is worse than not asking at all.
|
|
56
|
+
let fromServer = false;
|
|
57
|
+
if (flags.remote && profile.token) {
|
|
58
|
+
const remote = await this.fetchRemoteScopes(profile.token);
|
|
59
|
+
if (remote) {
|
|
60
|
+
fromServer = true;
|
|
61
|
+
drifted = JSON.stringify([...(scopes ?? [])].sort()) !== JSON.stringify([...remote].sort());
|
|
62
|
+
scopes = remote;
|
|
63
|
+
if (drifted)
|
|
64
|
+
updateProfileScopes(profileName, remote);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
27
67
|
if (format === 'json') {
|
|
28
68
|
this.output({
|
|
29
69
|
profile: profileName,
|
|
30
70
|
type: profile.type,
|
|
31
71
|
user: profile.user,
|
|
32
72
|
email: profile.email || null,
|
|
73
|
+
auth: authMethod,
|
|
74
|
+
expires_at: profile.expires_at || null,
|
|
75
|
+
scopes: scopes || null,
|
|
76
|
+
scopes_source: fromServer ? 'server' : 'profile',
|
|
77
|
+
storage: getSecretStorage(profile),
|
|
78
|
+
// A keyring-backed profile with no token means the store did not answer.
|
|
79
|
+
// Surfaced here because this is the command people run to debug access.
|
|
80
|
+
secret_readable: !!profile.token,
|
|
33
81
|
});
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const emailStr = profile.email ? ` (${profile.email})` : '';
|
|
85
|
+
process.stderr.write(` Подключён как: ${profile.user}${emailStr} [${profile.type}, profile: ${profileName}]\n`);
|
|
86
|
+
const expiry = describeExpiry(profile);
|
|
87
|
+
if (authMethod === 'oauth') {
|
|
88
|
+
process.stderr.write(` Вход: через браузер${expiry ? `, ${expiry}` : ''}\n`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
process.stderr.write(` Вход: готовым токеном, без срока\n`);
|
|
92
|
+
}
|
|
93
|
+
if (getSecretStorage(profile) === 'keyring') {
|
|
94
|
+
process.stderr.write(profile.token
|
|
95
|
+
? ` Хранение: хранилище ключей ОС\n`
|
|
96
|
+
: ` Хранение: хранилище ключей ОС — не читается, войдите заново\n`);
|
|
34
97
|
}
|
|
35
98
|
else {
|
|
36
|
-
|
|
37
|
-
|
|
99
|
+
process.stderr.write(` Хранение: файл конфигурации\n`);
|
|
100
|
+
}
|
|
101
|
+
if (scopes?.length) {
|
|
102
|
+
const source = fromServer ? ' по данным сервера' : '';
|
|
103
|
+
process.stderr.write(` Права${source} (${scopes.length}): ${scopes.join(', ')}\n`);
|
|
104
|
+
}
|
|
105
|
+
else if (fromServer) {
|
|
106
|
+
// An empty set is an answer, not a missing one — and a token that carries
|
|
107
|
+
// no rights is exactly the failure worth naming out loud.
|
|
108
|
+
process.stderr.write(` Права по данным сервера: ни одного\n`);
|
|
109
|
+
}
|
|
110
|
+
if (drifted) {
|
|
111
|
+
process.stderr.write(` Список прав в профиле расходился с сервером и был обновлён\n`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Ask the server what the token really carries. Null when it cannot be asked. */
|
|
115
|
+
async fetchRemoteScopes(token) {
|
|
116
|
+
try {
|
|
117
|
+
const response = await request({ method: 'GET', path: '/oauth/token/info', token }, { quiet: true });
|
|
118
|
+
const info = response.data.data;
|
|
119
|
+
return info?.scopes ?? null;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Offline or a dead token: the local view is still worth printing.
|
|
123
|
+
process.stderr.write(` Спросить права у сервера не удалось — показаны сохранённые при входе\n`);
|
|
124
|
+
return null;
|
|
38
125
|
}
|
|
39
126
|
}
|
|
40
127
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Args } from '@oclif/core';
|
|
2
2
|
import { BaseCommand } from '../../base-command.js';
|
|
3
|
-
import { listProfiles, setActiveProfile } from '../../profiles.js';
|
|
3
|
+
import { listProfiles, setActiveProfile, getProfile } from '../../profiles.js';
|
|
4
4
|
import { outputError } from '../../output.js';
|
|
5
5
|
export default class AuthSwitch extends BaseCommand {
|
|
6
6
|
static description = 'Переключение активного профиля';
|
|
@@ -26,11 +26,25 @@ export default class AuthSwitch extends BaseCommand {
|
|
|
26
26
|
this.exit(2);
|
|
27
27
|
}
|
|
28
28
|
setActiveProfile(args.profile);
|
|
29
|
+
// Say right here if the profile cannot work, rather than letting the next
|
|
30
|
+
// unrelated command fail: switching is where the user can still act on it.
|
|
31
|
+
const profile = getProfile(args.profile);
|
|
32
|
+
const secretMissing = !profile?.token;
|
|
33
|
+
const expired = !!profile?.expires_at && new Date(profile.expires_at) <= new Date();
|
|
29
34
|
if (format === 'json') {
|
|
30
|
-
this.output({
|
|
35
|
+
this.output({
|
|
36
|
+
active_profile: args.profile,
|
|
37
|
+
secret_readable: !secretMissing,
|
|
38
|
+
expired,
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
31
41
|
}
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
this.success(`Активный профиль: ${args.profile}`);
|
|
43
|
+
if (secretMissing) {
|
|
44
|
+
process.stderr.write(` Секрет профиля не читается — войдите заново: pachca auth login --profile ${args.profile}\n`);
|
|
45
|
+
}
|
|
46
|
+
else if (expired) {
|
|
47
|
+
process.stderr.write(` Срок действия токена истёк — он будет обновлён при следующей команде\n`);
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Auto-generated from openapi.yaml — DO NOT EDIT
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base-command.js';
|
|
4
|
+
export default class BotsListCompany extends BaseCommand {
|
|
5
|
+
static description = "Список ботов пространства";
|
|
6
|
+
static examples = [
|
|
7
|
+
"Инвентаризация всех ботов пространства — Получи всех ботов пространства, а не только доступных для редактирования:\n $ pachca bots list-company"
|
|
8
|
+
];
|
|
9
|
+
static scope = "company_bots:read";
|
|
10
|
+
static plan = "corporation";
|
|
11
|
+
static apiMethod = "GET";
|
|
12
|
+
static apiPath = "/company/bots";
|
|
13
|
+
static defaultColumns = ["id", "webhook"];
|
|
14
|
+
static args = {};
|
|
15
|
+
static flags = {
|
|
16
|
+
...BaseCommand.baseFlags,
|
|
17
|
+
'query': Flags.string({
|
|
18
|
+
description: "Поисковая фраза для фильтрации ботов по имени",
|
|
19
|
+
}),
|
|
20
|
+
limit: Flags.integer({
|
|
21
|
+
description: 'Количество результатов на страницу',
|
|
22
|
+
}),
|
|
23
|
+
cursor: Flags.string({
|
|
24
|
+
description: 'Курсор для следующей страницы',
|
|
25
|
+
}),
|
|
26
|
+
all: Flags.boolean({
|
|
27
|
+
description: 'Загрузить все страницы автоматически',
|
|
28
|
+
default: false,
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
async run() {
|
|
32
|
+
const { args, flags } = await this.parse(BotsListCompany);
|
|
33
|
+
this.parsedFlags = flags;
|
|
34
|
+
if (flags.all) {
|
|
35
|
+
// Auto-pagination
|
|
36
|
+
const allData = [];
|
|
37
|
+
let nextCursor = undefined;
|
|
38
|
+
let pages = 0;
|
|
39
|
+
const seenCursors = new Set();
|
|
40
|
+
while (pages < 500) {
|
|
41
|
+
const query = {
|
|
42
|
+
query: flags['query'],
|
|
43
|
+
limit: flags.limit,
|
|
44
|
+
cursor: nextCursor,
|
|
45
|
+
};
|
|
46
|
+
const response = await this.apiRequest({ method: 'GET', path: '/company/bots', query });
|
|
47
|
+
const body = response.data;
|
|
48
|
+
const items = body.data;
|
|
49
|
+
if (items)
|
|
50
|
+
allData.push(...items);
|
|
51
|
+
const meta = body.meta;
|
|
52
|
+
const paginate = meta?.paginate;
|
|
53
|
+
nextCursor = paginate?.next_page;
|
|
54
|
+
pages++;
|
|
55
|
+
// Условие конца: списочные методы — has_next === false; методы поиска и /users?query= (без has_next) — пустой data
|
|
56
|
+
const hasNext = paginate?.has_next;
|
|
57
|
+
if (typeof hasNext === 'boolean') {
|
|
58
|
+
if (!hasNext)
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
else if (!items || items.length === 0) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
if (process.stderr.isTTY) {
|
|
65
|
+
const total = paginate?.total;
|
|
66
|
+
const progress = total ? `${allData.length} / ${total}` : String(allData.length);
|
|
67
|
+
process.stderr.write(`\r Загружено: ${progress}...`);
|
|
68
|
+
}
|
|
69
|
+
if (!nextCursor)
|
|
70
|
+
break;
|
|
71
|
+
if (seenCursors.has(nextCursor)) {
|
|
72
|
+
process.stderr.write('\n⚠ Обнаружен цикл пагинации, остановка.\n');
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
seenCursors.add(nextCursor);
|
|
76
|
+
}
|
|
77
|
+
if (pages >= 500) {
|
|
78
|
+
process.stderr.write('\n⚠ Достигнут лимит 500 страниц.\n');
|
|
79
|
+
}
|
|
80
|
+
if (process.stderr.isTTY)
|
|
81
|
+
process.stderr.write('\n');
|
|
82
|
+
this.output(allData);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const { data } = await this.apiRequest({
|
|
86
|
+
method: 'GET',
|
|
87
|
+
path: '/company/bots',
|
|
88
|
+
query: {
|
|
89
|
+
query: flags['query'],
|
|
90
|
+
limit: flags.limit,
|
|
91
|
+
cursor: flags.cursor,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const responseBody = (data ?? {});
|
|
95
|
+
const items = responseBody.data ?? responseBody;
|
|
96
|
+
this.output(items);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Auto-generated from openapi.yaml — DO NOT EDIT
|
|
2
|
+
import { Flags } from '@oclif/core';
|
|
3
|
+
import { BaseCommand } from '../../base-command.js';
|
|
4
|
+
export default class ChatsListCompany extends BaseCommand {
|
|
5
|
+
static description = "Список чатов пространства";
|
|
6
|
+
static examples = [
|
|
7
|
+
"Выгрузить все чаты пространства, включая закрытые — Получи беседы и каналы всего пространства, включая закрытые, где владелец токена не состоит:\n $ pachca chats list-company"
|
|
8
|
+
];
|
|
9
|
+
static scope = "company_chats:read";
|
|
10
|
+
static plan = "corporation";
|
|
11
|
+
static apiMethod = "GET";
|
|
12
|
+
static apiPath = "/company/chats";
|
|
13
|
+
static defaultColumns = ["id", "name", "created_at", "owner_id", "channel"];
|
|
14
|
+
static args = {};
|
|
15
|
+
static flags = {
|
|
16
|
+
...BaseCommand.baseFlags,
|
|
17
|
+
'activity': Flags.string({
|
|
18
|
+
description: "Состояние чатов. Если параметр не указан, возвращаются и активные, и архивные чаты.",
|
|
19
|
+
options: ["active", "archived"],
|
|
20
|
+
}),
|
|
21
|
+
limit: Flags.integer({
|
|
22
|
+
description: 'Количество результатов на страницу',
|
|
23
|
+
}),
|
|
24
|
+
cursor: Flags.string({
|
|
25
|
+
description: 'Курсор для следующей страницы',
|
|
26
|
+
}),
|
|
27
|
+
all: Flags.boolean({
|
|
28
|
+
description: 'Загрузить все страницы автоматически',
|
|
29
|
+
default: false,
|
|
30
|
+
}),
|
|
31
|
+
};
|
|
32
|
+
async run() {
|
|
33
|
+
const { args, flags } = await this.parse(ChatsListCompany);
|
|
34
|
+
this.parsedFlags = flags;
|
|
35
|
+
if (flags.all) {
|
|
36
|
+
// Auto-pagination
|
|
37
|
+
const allData = [];
|
|
38
|
+
let nextCursor = undefined;
|
|
39
|
+
let pages = 0;
|
|
40
|
+
const seenCursors = new Set();
|
|
41
|
+
while (pages < 500) {
|
|
42
|
+
const query = {
|
|
43
|
+
activity: flags['activity'],
|
|
44
|
+
limit: flags.limit,
|
|
45
|
+
cursor: nextCursor,
|
|
46
|
+
};
|
|
47
|
+
const response = await this.apiRequest({ method: 'GET', path: '/company/chats', query });
|
|
48
|
+
const body = response.data;
|
|
49
|
+
const items = body.data;
|
|
50
|
+
if (items)
|
|
51
|
+
allData.push(...items);
|
|
52
|
+
const meta = body.meta;
|
|
53
|
+
const paginate = meta?.paginate;
|
|
54
|
+
nextCursor = paginate?.next_page;
|
|
55
|
+
pages++;
|
|
56
|
+
// Условие конца: списочные методы — has_next === false; методы поиска и /users?query= (без has_next) — пустой data
|
|
57
|
+
const hasNext = paginate?.has_next;
|
|
58
|
+
if (typeof hasNext === 'boolean') {
|
|
59
|
+
if (!hasNext)
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
else if (!items || items.length === 0) {
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
if (process.stderr.isTTY) {
|
|
66
|
+
const total = paginate?.total;
|
|
67
|
+
const progress = total ? `${allData.length} / ${total}` : String(allData.length);
|
|
68
|
+
process.stderr.write(`\r Загружено: ${progress}...`);
|
|
69
|
+
}
|
|
70
|
+
if (!nextCursor)
|
|
71
|
+
break;
|
|
72
|
+
if (seenCursors.has(nextCursor)) {
|
|
73
|
+
process.stderr.write('\n⚠ Обнаружен цикл пагинации, остановка.\n');
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
seenCursors.add(nextCursor);
|
|
77
|
+
}
|
|
78
|
+
if (pages >= 500) {
|
|
79
|
+
process.stderr.write('\n⚠ Достигнут лимит 500 страниц.\n');
|
|
80
|
+
}
|
|
81
|
+
if (process.stderr.isTTY)
|
|
82
|
+
process.stderr.write('\n');
|
|
83
|
+
this.output(allData);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const { data } = await this.apiRequest({
|
|
87
|
+
method: 'GET',
|
|
88
|
+
path: '/company/chats',
|
|
89
|
+
query: {
|
|
90
|
+
activity: flags['activity'],
|
|
91
|
+
limit: flags.limit,
|
|
92
|
+
cursor: flags.cursor,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const responseBody = (data ?? {});
|
|
96
|
+
const items = responseBody.data ?? responseBody;
|
|
97
|
+
this.output(items);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -6,7 +6,7 @@ export default class ChatsList extends BaseCommand {
|
|
|
6
6
|
static examples = [
|
|
7
7
|
"Найти активные чаты за период — Получи чаты с активностью после указанной даты:\n $ pachca chats list",
|
|
8
8
|
"Найти и заархивировать неактивные чаты — Получи чаты без активности с нужной даты:\n $ pachca chats list",
|
|
9
|
-
"
|
|
9
|
+
"Выгрузить все чаты пространства, включая закрытые — Если метод ответил `403` — у токена нет скоупа `company_chats:read`, либо владелец токена не владелец пространства, либо у пространства нет тарифа «Корпорация». Тогда возьми открытые чаты обычным списком:\n $ pachca chats list"
|
|
10
10
|
];
|
|
11
11
|
static scope = "chats:read";
|
|
12
12
|
static apiMethod = "GET";
|
|
@@ -30,6 +30,11 @@ export default class ChatsList extends BaseCommand {
|
|
|
30
30
|
options: ["is_member", "public"],
|
|
31
31
|
default: "is_member",
|
|
32
32
|
}),
|
|
33
|
+
'archived': Flags.boolean({
|
|
34
|
+
description: "Добавляет к выдаче архивные чаты. По умолчанию возвращаются только активные. Работает при любом значении `availability`.",
|
|
35
|
+
default: false,
|
|
36
|
+
allowNo: true,
|
|
37
|
+
}),
|
|
33
38
|
'last-message-at-after': Flags.string({
|
|
34
39
|
description: "Фильтрация по времени создания последнего сообщения. Будут возвращены те чаты, время последнего созданного сообщения в которых не раньше чем указанное (в формате YYYY-MM-DDThh:mm:ss.sssZ).",
|
|
35
40
|
}),
|
|
@@ -65,6 +70,7 @@ export default class ChatsList extends BaseCommand {
|
|
|
65
70
|
sort: flags['sort'],
|
|
66
71
|
order: flags['order'],
|
|
67
72
|
availability: flags['availability'],
|
|
73
|
+
archived: flags['archived'],
|
|
68
74
|
'last_message_at_after': flags['last-message-at-after'],
|
|
69
75
|
'last_message_at_before': flags['last-message-at-before'],
|
|
70
76
|
personal: flags['personal'],
|
|
@@ -117,6 +123,7 @@ export default class ChatsList extends BaseCommand {
|
|
|
117
123
|
sort: flags['sort'],
|
|
118
124
|
order: flags['order'],
|
|
119
125
|
availability: flags['availability'],
|
|
126
|
+
archived: flags['archived'],
|
|
120
127
|
'last_message_at_after': flags['last-message-at-after'],
|
|
121
128
|
'last_message_at_before': flags['last-message-at-before'],
|
|
122
129
|
personal: flags['personal'],
|
package/dist/commands/doctor.js
CHANGED
|
@@ -2,7 +2,9 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as process from 'node:process';
|
|
3
3
|
import semver from 'semver';
|
|
4
4
|
import { BaseCommand } from '../base-command.js';
|
|
5
|
-
import { getActiveProfile, getProfile, getConfigFilePath, getConfigFilePermissions, } from '../profiles.js';
|
|
5
|
+
import { getActiveProfile, getProfile, getConfigFilePath, getConfigFilePermissions, getAuthMethod, getSecretStorage, isExpiring, isRefreshable, listProfiles, } from '../profiles.js';
|
|
6
|
+
import { isSecretStoreAvailable } from '../secret-store.js';
|
|
7
|
+
import { getBaseUrl } from '../client.js';
|
|
6
8
|
export default class Doctor extends BaseCommand {
|
|
7
9
|
static description = 'Диагностика окружения: Node.js, сеть, токен, конфигурация';
|
|
8
10
|
static examples = [
|
|
@@ -30,30 +32,39 @@ export default class Doctor extends BaseCommand {
|
|
|
30
32
|
checks.push({ name: 'node', status: 'error', version: nodeVersion, required: '>=20' });
|
|
31
33
|
this.printCheck('error', `Node.js v${nodeVersion} — требуется >=20`, format);
|
|
32
34
|
}
|
|
33
|
-
// 2. Network
|
|
35
|
+
// 2. Network. Check the host the CLI actually talks to: on-premise installs
|
|
36
|
+
// point elsewhere, and probing the cloud there reports a healthy network
|
|
37
|
+
// while the server the user works against is down.
|
|
38
|
+
const baseUrl = getBaseUrl();
|
|
39
|
+
const host = new URL(baseUrl).host;
|
|
34
40
|
try {
|
|
35
41
|
const start = Date.now();
|
|
36
42
|
const controller = new AbortController();
|
|
37
43
|
const timer = setTimeout(() => controller.abort(), 5000);
|
|
38
|
-
await fetch(
|
|
44
|
+
await fetch(`${baseUrl}/profile`, {
|
|
39
45
|
method: 'HEAD',
|
|
40
46
|
signal: controller.signal,
|
|
41
47
|
});
|
|
42
48
|
clearTimeout(timer);
|
|
43
49
|
const latency = Date.now() - start;
|
|
44
|
-
checks.push({ name: 'network', status: 'ok', host
|
|
45
|
-
this.printCheck('ok', `Сеть
|
|
50
|
+
checks.push({ name: 'network', status: 'ok', host, latency_ms: latency });
|
|
51
|
+
this.printCheck('ok', `Сеть ${host} доступен (${latency}ms)`, format);
|
|
46
52
|
networkOk = true;
|
|
47
53
|
}
|
|
48
54
|
catch {
|
|
49
|
-
checks.push({ name: 'network', status: 'error', host
|
|
50
|
-
this.printCheck('error', `Сеть
|
|
55
|
+
checks.push({ name: 'network', status: 'error', host });
|
|
56
|
+
this.printCheck('error', `Сеть ${host} недоступен (таймаут 5s)`, format);
|
|
51
57
|
}
|
|
52
|
-
// 3. Config file
|
|
58
|
+
// 3. Config file. File permissions only matter for profiles that keep the
|
|
59
|
+
// secret there — for a keyring profile the file holds no token at all, and
|
|
60
|
+
// reporting its mode as a security state would be misleading.
|
|
53
61
|
const configPath = getConfigFilePath();
|
|
62
|
+
const activeName = getActiveProfile();
|
|
63
|
+
const activeProfile = activeName ? listProfiles()[activeName] : undefined;
|
|
64
|
+
const storage = activeProfile ? getSecretStorage(activeProfile) : 'file';
|
|
54
65
|
if (fs.existsSync(configPath)) {
|
|
55
66
|
const permissions = getConfigFilePermissions();
|
|
56
|
-
if (permissions && Number.parseInt(permissions, 8) > 0o600) {
|
|
67
|
+
if (storage === 'file' && permissions && Number.parseInt(permissions, 8) > 0o600) {
|
|
57
68
|
checks.push({ name: 'config', status: 'warning', path: configPath, permissions, expected: '600' });
|
|
58
69
|
this.printCheck('warning', `Конфиг ${configPath} (права: ${permissions} — рекомендуется chmod 600)`, format);
|
|
59
70
|
}
|
|
@@ -66,13 +77,48 @@ export default class Doctor extends BaseCommand {
|
|
|
66
77
|
checks.push({ name: 'config', status: 'error', path: configPath });
|
|
67
78
|
this.printCheck('error', `Конфиг файл не найден (запустите pachca auth login)`, format);
|
|
68
79
|
}
|
|
80
|
+
// 3a. Secret store. A locked keyring is the most likely failure of the new
|
|
81
|
+
// login, and without this check it surfaces below as "token is invalid".
|
|
82
|
+
let secretReadable = true;
|
|
83
|
+
if (activeProfile && storage === 'keyring') {
|
|
84
|
+
const storeUp = isSecretStoreAvailable();
|
|
85
|
+
secretReadable = storeUp && !!getProfile(activeName)?.token;
|
|
86
|
+
if (secretReadable) {
|
|
87
|
+
checks.push({ name: 'secret_store', status: 'ok', storage });
|
|
88
|
+
this.printCheck('ok', `Хранилище хранилище ключей ОС, секрет читается`, format);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
checks.push({ name: 'secret_store', status: 'error', storage, available: storeUp });
|
|
92
|
+
this.printCheck('error', storeUp
|
|
93
|
+
? `Хранилище секрет профиля не найден — войдите заново: pachca auth login`
|
|
94
|
+
: `Хранилище хранилище ключей ОС недоступно — разблокируйте связку ключей`, format);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else if (activeProfile) {
|
|
98
|
+
checks.push({ name: 'secret_store', status: 'ok', storage: 'file' });
|
|
99
|
+
this.printCheck('ok', `Хранилище файл конфигурации`, format);
|
|
100
|
+
}
|
|
69
101
|
// 4. Profile
|
|
70
102
|
const profileName = getActiveProfile();
|
|
71
103
|
if (profileName) {
|
|
72
104
|
const profile = getProfile(profileName);
|
|
73
105
|
if (profile) {
|
|
74
|
-
|
|
75
|
-
|
|
106
|
+
const method = getAuthMethod(profile);
|
|
107
|
+
const expiry = method === 'oauth' && profile.expires_at
|
|
108
|
+
? `, истекает ${new Date(profile.expires_at).toLocaleString('ru-RU')}`
|
|
109
|
+
: method === 'token'
|
|
110
|
+
? ', бессрочный'
|
|
111
|
+
: '';
|
|
112
|
+
checks.push({
|
|
113
|
+
name: 'profile',
|
|
114
|
+
status: 'ok',
|
|
115
|
+
profile: profileName,
|
|
116
|
+
type: profile.type,
|
|
117
|
+
user: profile.user,
|
|
118
|
+
auth: method,
|
|
119
|
+
storage,
|
|
120
|
+
});
|
|
121
|
+
this.printCheck('ok', `Профиль ${profileName} (${profile.type}: ${profile.user}) — ${method === 'oauth' ? 'вход через браузер' : 'готовый токен'}${expiry}`, format);
|
|
76
122
|
profileOk = true;
|
|
77
123
|
}
|
|
78
124
|
else {
|
|
@@ -84,16 +130,22 @@ export default class Doctor extends BaseCommand {
|
|
|
84
130
|
checks.push({ name: 'profile', status: 'error' });
|
|
85
131
|
this.printCheck('error', `Профиль не настроен (запустите pachca auth login)`, format);
|
|
86
132
|
}
|
|
87
|
-
// 5. Token (skip if no network or
|
|
88
|
-
|
|
89
|
-
|
|
133
|
+
// 5. Token (skip if no network, no profile, or the secret is unreadable —
|
|
134
|
+
// asking the server with an empty Bearer would come back 401 and get
|
|
135
|
+
// reported as an invalid token, sending the user to log in for nothing)
|
|
136
|
+
if (!networkOk || !profileOk || !secretReadable) {
|
|
137
|
+
const reason = !networkOk ? 'нет сети' : !profileOk ? 'нет профиля' : 'секрет недоступен';
|
|
90
138
|
checks.push({ name: 'token', status: 'skipped', reason });
|
|
91
139
|
this.printCheck('skipped', `Токен пропущено (${reason})`, format);
|
|
92
140
|
}
|
|
93
141
|
else {
|
|
94
142
|
const profile = getProfile(profileName);
|
|
143
|
+
// An expired token that still has its refresh token is not a broken login:
|
|
144
|
+
// the next command renews it on the init hook. Reporting it as invalid sent
|
|
145
|
+
// people to log in again for nothing.
|
|
146
|
+
const willRenew = isRefreshable(profile) && isExpiring(profile);
|
|
95
147
|
try {
|
|
96
|
-
const response = await fetch(
|
|
148
|
+
const response = await fetch(`${baseUrl}/oauth/token/info`, {
|
|
97
149
|
headers: { Authorization: `Bearer ${profile.token}` },
|
|
98
150
|
});
|
|
99
151
|
if (response.ok) {
|
|
@@ -102,6 +154,10 @@ export default class Doctor extends BaseCommand {
|
|
|
102
154
|
checks.push({ name: 'token', status: 'ok', scopes_count: scopeCount });
|
|
103
155
|
this.printCheck('ok', `Токен действителен (${scopeCount} скоупов)`, format);
|
|
104
156
|
}
|
|
157
|
+
else if (willRenew) {
|
|
158
|
+
checks.push({ name: 'token', status: 'ok', renewable: true, http_status: response.status });
|
|
159
|
+
this.printCheck('ok', `Токен срок истёк — обновится при следующей команде`, format);
|
|
160
|
+
}
|
|
105
161
|
else {
|
|
106
162
|
checks.push({ name: 'token', status: 'error', http_status: response.status });
|
|
107
163
|
this.printCheck('error', `Токен недействителен (${response.status}) — обновите: pachca auth login`, format);
|
|
@@ -48,6 +48,7 @@ export default class Introspect extends BaseCommand {
|
|
|
48
48
|
}
|
|
49
49
|
// D3 single source: baseFlagNames derived from BaseCommand.baseFlags
|
|
50
50
|
const baseFlagNames = new Set(Object.keys(BaseCommand.baseFlags));
|
|
51
|
-
|
|
51
|
+
const baseFlagDescriptions = new Map(Object.entries(BaseCommand.baseFlags).map(([name, def]) => [name, def.description ?? '']));
|
|
52
|
+
this.output(normalizeCommand(commandId, cmdMeta, baseFlagNames, baseFlagDescriptions));
|
|
52
53
|
}
|
|
53
54
|
}
|
|
@@ -18,7 +18,7 @@ export default class MessagesCreate extends BaseCommand {
|
|
|
18
18
|
static flags = {
|
|
19
19
|
...BaseCommand.baseFlags,
|
|
20
20
|
'entity-type': Flags.string({
|
|
21
|
-
description: "Тип
|
|
21
|
+
description: "Тип сущности. Регистр значения не учитывается. Значение вне списка возвращает `400` с кодом `invalid`.",
|
|
22
22
|
}),
|
|
23
23
|
'entity-id': Flags.integer({
|
|
24
24
|
description: "Идентификатор сущности (pachca chats list | pachca users list)",
|
|
@@ -24,7 +24,7 @@ export default class SecurityList extends BaseCommand {
|
|
|
24
24
|
}),
|
|
25
25
|
'event-key': Flags.string({
|
|
26
26
|
description: "Фильтр по конкретному типу события",
|
|
27
|
-
options: ["user_login", "user_logout", "user_2fa_fail", "user_2fa_success", "user_created", "user_deleted", "user_role_changed", "user_updated", "tag_created", "tag_deleted", "user_added_to_tag", "user_removed_from_tag", "chat_created", "chat_renamed", "chat_permission_changed", "user_chat_join", "user_chat_leave", "tag_added_to_chat", "tag_removed_from_chat", "message_updated", "message_deleted", "message_created", "reaction_created", "reaction_deleted", "thread_created", "access_token_created", "access_token_updated", "access_token_destroy", "kms_encrypt", "kms_decrypt", "audit_events_accessed", "dlp_violation_detected", "search_users_api", "search_chats_api", "search_messages_api", "bot_scopes_updated", "bot_webhook_settings_updated", "bot_token_recreated", "bot_deleted", "video_call_started", "video_call_finished", "video_call_recording_ready"],
|
|
27
|
+
options: ["user_login", "user_logout", "user_2fa_fail", "user_2fa_success", "user_created", "user_deleted", "user_role_changed", "user_updated", "tag_created", "tag_deleted", "user_added_to_tag", "user_removed_from_tag", "chat_created", "chat_renamed", "chat_permission_changed", "user_chat_join", "user_chat_leave", "tag_added_to_chat", "tag_removed_from_chat", "message_updated", "message_deleted", "message_created", "reaction_created", "reaction_deleted", "thread_created", "access_token_created", "access_token_updated", "access_token_destroy", "kms_encrypt", "kms_decrypt", "audit_events_accessed", "company_chats_accessed", "company_bots_accessed", "dlp_violation_detected", "search_users_api", "search_chats_api", "search_messages_api", "bot_scopes_updated", "bot_webhook_settings_updated", "bot_token_recreated", "bot_deleted", "bot_oauth_client_updated", "oauth_authorization_granted", "oauth_authorization_revoked", "video_call_started", "video_call_finished", "video_call_recording_ready"],
|
|
28
28
|
}),
|
|
29
29
|
'actor-id': Flags.string({
|
|
30
30
|
description: "Идентификатор пользователя, выполнившего действие",
|