@pachca/cli 2026.10.3 → 2026.10.4

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +5 -5
  3. package/README.template.md +5 -5
  4. package/dist/base-command.js +68 -36
  5. package/dist/client.js +2 -1
  6. package/dist/commands/api.js +3 -3
  7. package/dist/commands/auth/login.js +12 -9
  8. package/dist/commands/bots/create.js +1 -1
  9. package/dist/commands/bots/list-company.js +4 -2
  10. package/dist/commands/bots/list-events.js +4 -2
  11. package/dist/commands/bots/list-tokens.js +4 -2
  12. package/dist/commands/bots/list.js +4 -2
  13. package/dist/commands/bots/update.js +4 -3
  14. package/dist/commands/chats/list-company.js +4 -2
  15. package/dist/commands/chats/list.js +4 -2
  16. package/dist/commands/doctor.js +60 -35
  17. package/dist/commands/drafts/create.js +5 -2
  18. package/dist/commands/drafts/list.js +4 -2
  19. package/dist/commands/drafts/update.js +6 -3
  20. package/dist/commands/group-tags/list-users.js +4 -2
  21. package/dist/commands/group-tags/list.js +4 -2
  22. package/dist/commands/important-messages/list.js +4 -2
  23. package/dist/commands/members/add.js +1 -1
  24. package/dist/commands/members/list.js +4 -2
  25. package/dist/commands/messages/create.js +5 -3
  26. package/dist/commands/messages/list.js +4 -2
  27. package/dist/commands/messages/update.js +4 -2
  28. package/dist/commands/reactions/list.js +4 -2
  29. package/dist/commands/read-member/list-readers.js +4 -2
  30. package/dist/commands/search/list-chats.js +4 -2
  31. package/dist/commands/search/list-messages.js +4 -2
  32. package/dist/commands/search/list-users.js +4 -2
  33. package/dist/commands/security/list.js +4 -2
  34. package/dist/commands/tasks/create.js +3 -1
  35. package/dist/commands/tasks/list.js +4 -2
  36. package/dist/commands/tasks/update.js +3 -1
  37. package/dist/commands/threads/list.js +4 -2
  38. package/dist/commands/upload.js +27 -8
  39. package/dist/commands/users/list.js +4 -2
  40. package/dist/commands/users/update.js +1 -1
  41. package/dist/commands/views/open.js +1 -0
  42. package/dist/data/changelog.json +111 -0
  43. package/dist/data/endpoints.json +1 -1
  44. package/dist/hooks/init.js +8 -4
  45. package/dist/lib/flags.js +25 -0
  46. package/dist/oauth.js +2 -1
  47. package/dist/profiles.js +22 -4
  48. package/oclif.manifest.json +1 -1
  49. package/package.json +2 -2
@@ -2,7 +2,7 @@ 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, getAuthMethod, getSecretStorage, isExpiring, isRefreshable, listProfiles, } from '../profiles.js';
5
+ import { getProfile, getConfigFilePath, getConfigFilePermissions, getAuthMethod, getSecretStorage, isExpiring, isRefreshable, listProfiles, profileFlag, tokenSource, } from '../profiles.js';
6
6
  import { isSecretStoreAvailable } from '../secret-store.js';
7
7
  import { getBaseUrl } from '../client.js';
8
8
  export default class Doctor extends BaseCommand {
@@ -20,7 +20,6 @@ export default class Doctor extends BaseCommand {
20
20
  const format = this.getOutputFormat();
21
21
  const checks = [];
22
22
  let networkOk = false;
23
- let profileOk = false;
24
23
  // 1. Node.js version
25
24
  const nodeVersion = process.version.replace('v', '');
26
25
  const nodeMajor = Number.parseInt(nodeVersion.split('.')[0], 10);
@@ -55,13 +54,19 @@ export default class Doctor extends BaseCommand {
55
54
  checks.push({ name: 'network', status: 'error', host });
56
55
  this.printCheck('error', `Сеть ${host} недоступен (таймаут 5s)`, format);
57
56
  }
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.
57
+ // The token comes from the same place as for any other command, in the same
58
+ // order: --token, --profile, PACHCA_TOKEN, PACHCA_PROFILE, the active profile.
59
+ // In CI the token usually comes from PACHCA_TOKEN alone, with no config at all.
60
+ const source = tokenSource({ token: flags.token, profile: flags.profile });
61
+ const sourceLabel = source.kind === 'flag' ? '--token' : source.kind === 'env' ? 'PACHCA_TOKEN' : undefined;
62
+ const profileName = source.kind === 'profile' ? source.profileName : undefined;
63
+ const storedProfile = profileName ? listProfiles()[profileName] : undefined;
64
+ const relogin = `pachca auth login${profileFlag(profileName)}`;
65
+ // 3. Config file. File permissions only matter when a secret lives there: a
66
+ // keyring profile keeps no token in the file, and reporting its mode as a
67
+ // security state would be misleading.
61
68
  const configPath = getConfigFilePath();
62
- const activeName = getActiveProfile();
63
- const activeProfile = activeName ? listProfiles()[activeName] : undefined;
64
- const storage = activeProfile ? getSecretStorage(activeProfile) : 'file';
69
+ const storage = storedProfile ? getSecretStorage(storedProfile) : 'file';
65
70
  if (fs.existsSync(configPath)) {
66
71
  const permissions = getConfigFilePermissions();
67
72
  if (storage === 'file' && permissions && Number.parseInt(permissions, 8) > 0o600) {
@@ -73,16 +78,20 @@ export default class Doctor extends BaseCommand {
73
78
  this.printCheck('ok', `Конфиг ${configPath} (права: ${permissions || '600'})`, format);
74
79
  }
75
80
  }
81
+ else if (sourceLabel) {
82
+ checks.push({ name: 'config', status: 'skipped', path: configPath, reason: `токен из ${sourceLabel}` });
83
+ this.printCheck('skipped', `Конфиг не нужен: токен из ${sourceLabel}`, format);
84
+ }
76
85
  else {
77
86
  checks.push({ name: 'config', status: 'error', path: configPath });
78
- this.printCheck('error', `Конфиг файл не найден (запустите pachca auth login)`, format);
87
+ this.printCheck('error', `Конфиг ${configPath} не найден (запустите pachca auth login или задайте PACHCA_TOKEN)`, format);
79
88
  }
80
89
  // 3a. Secret store. A locked keyring is the most likely failure of the new
81
90
  // login, and without this check it surfaces below as "token is invalid".
82
91
  let secretReadable = true;
83
- if (activeProfile && storage === 'keyring') {
92
+ if (storedProfile && storage === 'keyring') {
84
93
  const storeUp = isSecretStoreAvailable();
85
- secretReadable = storeUp && !!getProfile(activeName)?.token;
94
+ secretReadable = storeUp && !!getProfile(profileName)?.token;
86
95
  if (secretReadable) {
87
96
  checks.push({ name: 'secret_store', status: 'ok', storage });
88
97
  this.printCheck('ok', `Хранилище хранилище ключей ОС, секрет читается`, format);
@@ -90,63 +99,81 @@ export default class Doctor extends BaseCommand {
90
99
  else {
91
100
  checks.push({ name: 'secret_store', status: 'error', storage, available: storeUp });
92
101
  this.printCheck('error', storeUp
93
- ? `Хранилище секрет профиля не найден — войдите заново: pachca auth login`
102
+ ? `Хранилище секрет профиля не найден — войдите заново: ${relogin}`
94
103
  : `Хранилище хранилище ключей ОС недоступно — разблокируйте связку ключей`, format);
95
104
  }
96
105
  }
97
- else if (activeProfile) {
106
+ else if (storedProfile) {
98
107
  checks.push({ name: 'secret_store', status: 'ok', storage: 'file' });
99
108
  this.printCheck('ok', `Хранилище файл конфигурации`, format);
100
109
  }
101
110
  // 4. Profile
102
- const profileName = getActiveProfile();
103
- if (profileName) {
104
- const profile = getProfile(profileName);
111
+ let token;
112
+ let profile;
113
+ if (sourceLabel) {
114
+ token = source.kind === 'flag' ? flags.token : process.env.PACHCA_TOKEN;
115
+ checks.push({ name: 'profile', status: 'skipped', source: sourceLabel, reason: `токен из ${sourceLabel}` });
116
+ this.printCheck('skipped', `Профиль не используется: токен из ${sourceLabel}`, format);
117
+ }
118
+ else if (source.kind === 'profile') {
119
+ profile = getProfile(source.profileName);
105
120
  if (profile) {
121
+ token = profile.token;
106
122
  const method = getAuthMethod(profile);
107
123
  const expiry = method === 'oauth' && profile.expires_at
108
124
  ? `, истекает ${new Date(profile.expires_at).toLocaleString('ru-RU')}`
109
125
  : method === 'token'
110
126
  ? ', бессрочный'
111
127
  : '';
128
+ const via = source.via === 'active_profile' ? '' : ` из ${source.via}`;
112
129
  checks.push({
113
130
  name: 'profile',
114
131
  status: 'ok',
115
- profile: profileName,
132
+ profile: source.profileName,
133
+ source: source.via,
116
134
  type: profile.type,
117
135
  user: profile.user,
118
136
  auth: method,
119
137
  storage,
120
138
  });
121
- this.printCheck('ok', `Профиль ${profileName} (${profile.type}: ${profile.user}) — ${method === 'oauth' ? 'вход через браузер' : 'готовый токен'}${expiry}`, format);
122
- profileOk = true;
139
+ this.printCheck('ok', `Профиль ${source.profileName}${via} (${profile.type}: ${profile.user}) — ${method === 'oauth' ? 'вход через браузер' : 'готовый токен'}${expiry}`, format);
123
140
  }
124
141
  else {
125
- checks.push({ name: 'profile', status: 'error', profile: profileName });
126
- this.printCheck('error', `Профиль ${profileName} — профиль не найден в конфиге`, format);
142
+ checks.push({ name: 'profile', status: 'error', profile: source.profileName, source: source.via });
143
+ this.printCheck('error', `Профиль ${source.profileName} — профиль не найден в конфиге (список: pachca auth list)`, format);
127
144
  }
128
145
  }
129
146
  else {
130
147
  checks.push({ name: 'profile', status: 'error' });
131
- this.printCheck('error', `Профиль не настроен (запустите pachca auth login)`, format);
148
+ this.printCheck('error', `Профиль не настроен (запустите pachca auth login или задайте PACHCA_TOKEN)`, format);
132
149
  }
133
- // 5. Token (skip if no network, no profile, or the secret is unreadable —
150
+ // 5. Token (skip if no network, no token, or the secret is unreadable —
134
151
  // asking the server with an empty Bearer would come back 401 and get
135
152
  // reported as an invalid token, sending the user to log in for nothing)
136
- if (!networkOk || !profileOk || !secretReadable) {
137
- const reason = !networkOk ? 'нет сети' : !profileOk ? 'нет профиля' : 'секрет недоступен';
153
+ if (!networkOk || !token || !secretReadable) {
154
+ const reason = !networkOk
155
+ ? 'нет сети'
156
+ : !secretReadable
157
+ ? 'секрет недоступен'
158
+ : !sourceLabel && !profile
159
+ ? 'нет профиля'
160
+ : 'нет токена';
138
161
  checks.push({ name: 'token', status: 'skipped', reason });
139
162
  this.printCheck('skipped', `Токен пропущено (${reason})`, format);
140
163
  }
141
164
  else {
142
- const profile = getProfile(profileName);
143
165
  // An expired token that still has its refresh token is not a broken login:
144
166
  // the next command renews it on the init hook. Reporting it as invalid sent
145
167
  // people to log in again for nothing.
146
- const willRenew = isRefreshable(profile) && isExpiring(profile);
168
+ const willRenew = !!profile && isRefreshable(profile) && isExpiring(profile);
169
+ const fix = sourceLabel
170
+ ? `проверьте ${sourceLabel}`
171
+ : profile && getAuthMethod(profile) === 'oauth'
172
+ ? `войдите заново: ${relogin}`
173
+ : `сохраните новый: ${relogin} --token <токен>`;
147
174
  try {
148
175
  const response = await fetch(`${baseUrl}/oauth/token/info`, {
149
- headers: { Authorization: `Bearer ${profile.token}` },
176
+ headers: { Authorization: `Bearer ${token}` },
150
177
  });
151
178
  if (response.ok) {
152
179
  const body = (await response.json());
@@ -160,7 +187,7 @@ export default class Doctor extends BaseCommand {
160
187
  }
161
188
  else {
162
189
  checks.push({ name: 'token', status: 'error', http_status: response.status });
163
- this.printCheck('error', `Токен недействителен (${response.status}) — обновите: pachca auth login`, format);
190
+ this.printCheck('error', `Токен недействителен (${response.status}) — ${fix}`, format);
164
191
  }
165
192
  }
166
193
  catch {
@@ -201,17 +228,15 @@ export default class Doctor extends BaseCommand {
201
228
  this.printCheck('ok', `CLI v${currentVersion}`, format);
202
229
  }
203
230
  }
204
- // Output JSON if requested
231
+ // A failed check exits with 1 in every format, so a CI step can rely on the
232
+ // exit code alone; `status` of each check says which one failed.
233
+ const hasErrors = checks.some((c) => c.status === 'error');
205
234
  if (format === 'json') {
206
235
  this.output({ checks });
207
- return;
208
236
  }
209
- // Summary
210
- const hasErrors = checks.some((c) => c.status === 'error');
211
- if (!hasErrors) {
237
+ else if (!hasErrors) {
212
238
  process.stderr.write('\nВсё в порядке.\n');
213
239
  }
214
- // Exit with error if any check failed
215
240
  if (hasErrors) {
216
241
  this.exit(1);
217
242
  }
@@ -2,6 +2,7 @@
2
2
  import { Flags } from '@oclif/core';
3
3
  import { BaseCommand } from '../../base-command.js';
4
4
  import * as clack from '@clack/prompts';
5
+ import { nullableInteger } from '../../lib/flags.js';
5
6
  export default class DraftsCreate extends BaseCommand {
6
7
  static description = "Новый черновик";
7
8
  static scope = "drafts:write";
@@ -21,7 +22,7 @@ export default class DraftsCreate extends BaseCommand {
21
22
  'content': Flags.string({
22
23
  description: "Текст черновика. Можно не передавать, если есть вложения, но пустым черновик быть не может. До 40 000 байт в UTF-8.",
23
24
  }),
24
- 'parent-message-id': Flags.integer({
25
+ 'parent-message-id': nullableInteger({
25
26
  description: "Идентификатор сообщения, ответом на которое уйдёт черновик. Сообщение должно быть в том же чате.",
26
27
  }),
27
28
  'files': Flags.string({
@@ -40,7 +41,9 @@ export default class DraftsCreate extends BaseCommand {
40
41
  for await (const chunk of process.stdin) {
41
42
  chunks.push(chunk);
42
43
  }
43
- flags['content'] = Buffer.concat(chunks).toString('utf-8').trimEnd();
44
+ const piped = Buffer.concat(chunks).toString('utf-8').trimEnd();
45
+ if (piped)
46
+ flags['content'] = piped;
44
47
  }
45
48
  const missingRequired = [
46
49
  { flag: 'entity-type', label: "Куда пишется черновик: в беседу или канал, в тред либо в личную переписку с сотрудником", type: 'string' },
@@ -42,6 +42,7 @@ export default class DraftsList extends BaseCommand {
42
42
  let nextCursor = undefined;
43
43
  let pages = 0;
44
44
  const seenCursors = new Set();
45
+ let total;
45
46
  while (pages < 500) {
46
47
  const query = {
47
48
  type: flags['type'],
@@ -59,7 +60,7 @@ export default class DraftsList extends BaseCommand {
59
60
  const paginate = meta?.paginate;
60
61
  nextCursor = paginate?.next_page;
61
62
  pages++;
62
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
63
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
63
64
  const hasNext = paginate?.has_next;
64
65
  if (typeof hasNext === 'boolean') {
65
66
  if (!hasNext)
@@ -69,7 +70,8 @@ export default class DraftsList extends BaseCommand {
69
70
  break;
70
71
  }
71
72
  if (process.stderr.isTTY) {
72
- const total = paginate?.total;
73
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
74
+ total ??= meta?.total;
73
75
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
74
76
  process.stderr.write(`\r Загружено: ${progress}...`);
75
77
  }
@@ -1,6 +1,7 @@
1
1
  // Auto-generated from openapi.yaml — DO NOT EDIT
2
2
  import { Args, Flags } from '@oclif/core';
3
3
  import { BaseCommand } from '../../base-command.js';
4
+ import { nullableInteger } from '../../lib/flags.js';
4
5
  export default class DraftsUpdate extends BaseCommand {
5
6
  static description = "Редактирование черновика";
6
7
  static scope = "drafts:write";
@@ -18,8 +19,8 @@ export default class DraftsUpdate extends BaseCommand {
18
19
  'content': Flags.string({
19
20
  description: "Текст черновика. До 40 000 байт в UTF-8.",
20
21
  }),
21
- 'parent-message-id': Flags.integer({
22
- description: "Идентификатор сообщения, ответом на которое уйдёт черновик. `null` снимает ответ.",
22
+ 'parent-message-id': nullableInteger({
23
+ description: "Идентификатор сообщения, ответом на которое уйдёт черновик. `null` снимает ответ. Чтобы передать null, укажите `--parent-message-id null`.",
23
24
  }),
24
25
  'files': Flags.string({
25
26
  description: "Прикрепляемые файлы, не больше десяти. Переданный массив заменяет прежний состав: пустой снимает все вложения, а чтобы сохранить уже прикреплённый файл, передайте его с `id`. Без этого поля вложения остаются как были.",
@@ -37,7 +38,9 @@ export default class DraftsUpdate extends BaseCommand {
37
38
  for await (const chunk of process.stdin) {
38
39
  chunks.push(chunk);
39
40
  }
40
- flags['content'] = Buffer.concat(chunks).toString('utf-8').trimEnd();
41
+ const piped = Buffer.concat(chunks).toString('utf-8').trimEnd();
42
+ if (piped)
43
+ flags['content'] = piped;
41
44
  }
42
45
  const body = { draft: {
43
46
  content: flags['content'],
@@ -35,6 +35,7 @@ export default class GroupTagsListUsers extends BaseCommand {
35
35
  let nextCursor = undefined;
36
36
  let pages = 0;
37
37
  const seenCursors = new Set();
38
+ let total;
38
39
  while (pages < 500) {
39
40
  const query = {
40
41
  limit: flags.limit,
@@ -49,7 +50,7 @@ export default class GroupTagsListUsers extends BaseCommand {
49
50
  const paginate = meta?.paginate;
50
51
  nextCursor = paginate?.next_page;
51
52
  pages++;
52
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
53
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
53
54
  const hasNext = paginate?.has_next;
54
55
  if (typeof hasNext === 'boolean') {
55
56
  if (!hasNext)
@@ -59,7 +60,8 @@ export default class GroupTagsListUsers extends BaseCommand {
59
60
  break;
60
61
  }
61
62
  if (process.stderr.isTTY) {
62
- const total = paginate?.total;
63
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
64
+ total ??= meta?.total;
63
65
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
64
66
  process.stderr.write(`\r Загружено: ${progress}...`);
65
67
  }
@@ -33,6 +33,7 @@ export default class GroupTagsList extends BaseCommand {
33
33
  let nextCursor = undefined;
34
34
  let pages = 0;
35
35
  const seenCursors = new Set();
36
+ let total;
36
37
  while (pages < 500) {
37
38
  const query = {
38
39
  'names[]': flags['names']?.split(','),
@@ -48,7 +49,7 @@ export default class GroupTagsList extends BaseCommand {
48
49
  const paginate = meta?.paginate;
49
50
  nextCursor = paginate?.next_page;
50
51
  pages++;
51
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
52
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
52
53
  const hasNext = paginate?.has_next;
53
54
  if (typeof hasNext === 'boolean') {
54
55
  if (!hasNext)
@@ -58,7 +59,8 @@ export default class GroupTagsList extends BaseCommand {
58
59
  break;
59
60
  }
60
61
  if (process.stderr.isTTY) {
61
- const total = paginate?.total;
62
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
63
+ total ??= meta?.total;
62
64
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
63
65
  process.stderr.write(`\r Загружено: ${progress}...`);
64
66
  }
@@ -30,6 +30,7 @@ export default class ImportantMessagesList extends BaseCommand {
30
30
  let nextCursor = undefined;
31
31
  let pages = 0;
32
32
  const seenCursors = new Set();
33
+ let total;
33
34
  while (pages < 500) {
34
35
  const query = {
35
36
  limit: flags.limit,
@@ -44,7 +45,7 @@ export default class ImportantMessagesList extends BaseCommand {
44
45
  const paginate = meta?.paginate;
45
46
  nextCursor = paginate?.next_page;
46
47
  pages++;
47
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
48
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
48
49
  const hasNext = paginate?.has_next;
49
50
  if (typeof hasNext === 'boolean') {
50
51
  if (!hasNext)
@@ -54,7 +55,8 @@ export default class ImportantMessagesList extends BaseCommand {
54
55
  break;
55
56
  }
56
57
  if (process.stderr.isTTY) {
57
- const total = paginate?.total;
58
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
59
+ total ??= meta?.total;
58
60
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
59
61
  process.stderr.write(`\r Загружено: ${progress}...`);
60
62
  }
@@ -10,7 +10,7 @@ export default class MembersAdd extends BaseCommand {
10
10
  static requiredFlags = ["member-ids"];
11
11
  static args = {
12
12
  id: Args.integer({
13
- description: "Идентификатор чата (беседа, канал или чат треда)",
13
+ description: "Идентификатор чата (беседа, канал или чат треда). В тред метод добавляет всех обычными участниками и без уведомления, поэтому упомяните добавленных в сообщении треда. Сотрудников не из родительского чата он добавляет туда при любой настройке «Использовать сквозные треды».",
14
14
  required: true,
15
15
  }),
16
16
  };
@@ -40,6 +40,7 @@ export default class MembersList extends BaseCommand {
40
40
  let nextCursor = undefined;
41
41
  let pages = 0;
42
42
  const seenCursors = new Set();
43
+ let total;
43
44
  while (pages < 500) {
44
45
  const query = {
45
46
  role: flags['role'],
@@ -55,7 +56,7 @@ export default class MembersList extends BaseCommand {
55
56
  const paginate = meta?.paginate;
56
57
  nextCursor = paginate?.next_page;
57
58
  pages++;
58
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
59
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
59
60
  const hasNext = paginate?.has_next;
60
61
  if (typeof hasNext === 'boolean') {
61
62
  if (!hasNext)
@@ -65,7 +66,8 @@ export default class MembersList extends BaseCommand {
65
66
  break;
66
67
  }
67
68
  if (process.stderr.isTTY) {
68
- const total = paginate?.total;
69
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
70
+ total ??= meta?.total;
69
71
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
70
72
  process.stderr.write(`\r Загружено: ${progress}...`);
71
73
  }
@@ -19,7 +19,7 @@ export default class MessagesCreate extends BaseCommand {
19
19
  description: "Идентификатор того, что названо в `entity_type`: беседы или канала, треда либо пользователя. У треда их два: `id` треда указывают с `entity_type: \"thread\"`, а его `chat_id` — с `discussion`, как у любого чата. (pachca chats list | pachca users list)",
20
20
  }),
21
21
  'content': Flags.string({
22
- description: "Текст сообщения. Обязателен, если к сообщению не приложены файлы. Поддерживает упоминания: `@nickname` или `<@user_id>` (будет автоматически преобразовано в `@nickname`). `@all` упоминает всех участников чата, `@here` — тех, кто сейчас в сети, а `@` с названием тега без пробелов — сотрудников этого тега. Имя без `@` и упоминание внутри строчного кода или блока кода упоминанием не считаются. У мульти-гостя `<@user_id>` недоступного ему сотрудника остаётся в тексте как есть. До 40 000 байт в UTF-8, для кириллицы это около 20 000 символов. Сообщение уходит под именем владельца токена, у бота — с бейджем «Бот».",
22
+ description: "Текст сообщения. Обязателен, если к сообщению не приложены файлы. Поддерживает упоминания: `@nickname` или `<@user_id>` (будет автоматически преобразовано в `@nickname`). Ник берут из ответа метода, а `user_id` — из данных: ник, составленный из имени, никого не упоминает. `@all` упоминает всех участников чата, `@here` — тех, кто сейчас в сети, а `@` с названием тега без пробелов — сотрудников этого тега. Имя без `@` и упоминание внутри строчного кода или блока кода упоминанием не считаются. У мульти-гостя `<@user_id>` недоступного ему сотрудника остаётся в тексте как есть. До 40 000 байт в UTF-8, для кириллицы это около 20 000 символов. Сообщение уходит под именем владельца токена, у бота — с бейджем «Бот».",
23
23
  }),
24
24
  'files': Flags.string({
25
25
  description: "Прикрепляемые файлы",
@@ -37,7 +37,7 @@ export default class MessagesCreate extends BaseCommand {
37
37
  description: "Полное специальное имя отправителя для этого сообщения. Вместе с `display_avatar_url` бот подписывает сообщение чужим именем и аватаром, и только это сообщение. Бейдж «Бот» остаётся, а в профиле отправителя видно настоящего бота. Так показывают автора из другой системы, когда переносят в Пачку чужую переписку. С личным токеном поле недоступно, ответ `403`. Подробности — в разделе [Боты](https://dev.pachca.com/guides/bots/overview). (макс. 255 символов)",
38
38
  }),
39
39
  'skip-invite-mentions': Flags.boolean({
40
- description: "Пропуск добавления упомянутых сотрудников в тред. В треде упоминание работает ещё и как приглашение: упомянутого сотрудника, которого в треде нет, Пачка туда добавляет, и он получает доступ к треду и родительскому сообщению. Если в пространстве включена настройка «Использовать сквозные треды» (так по умолчанию), упоминание добавляет и сотрудника не из родительского чата, а с `true` в этом поле упоминание никого не добавляет. Упоминание от гостя или мульти-гостя при этом добавляет только тех, с кем у автора есть общий чат. Если настройка выключена, упоминание добавляет только участников родительского чата, и это поле ничего не меняет. В беседе и канале упоминание никого не добавляет, и этот параметр там ничего не меняет.",
40
+ description: "Пропуск добавления упомянутых сотрудников в тред. В треде упоминание работает ещё и как приглашение: упомянутого сотрудника, которого в треде нет, Пачка туда добавляет, и он получает доступ к треду и родительскому сообщению. Если в пространстве включена настройка «Использовать сквозные треды» (так по умолчанию), упоминание добавляет и сотрудника не из родительского чата, а с `true` в этом поле упоминание никого не добавляет, и тот, кого в треде нет, не получает и уведомления. Упоминание от гостя или мульти-гостя при этом добавляет только тех, с кем у автора есть общий чат. Если настройка выключена, упоминание добавляет только участников родительского чата, и это поле ничего не меняет. В беседе и канале упоминание никого не добавляет, и этот параметр там ничего не меняет.",
41
41
  allowNo: true,
42
42
  }),
43
43
  'link-preview': Flags.boolean({
@@ -54,7 +54,9 @@ export default class MessagesCreate extends BaseCommand {
54
54
  for await (const chunk of process.stdin) {
55
55
  chunks.push(chunk);
56
56
  }
57
- flags['content'] = Buffer.concat(chunks).toString('utf-8').trimEnd();
57
+ const piped = Buffer.concat(chunks).toString('utf-8').trimEnd();
58
+ if (piped)
59
+ flags['content'] = piped;
58
60
  }
59
61
  const missingRequired = [
60
62
  { flag: 'entity-id', label: "Идентификатор того, что названо в `entity_type`: беседы или канала, треда либо пользователя. У треда их два: `id` треда указывают с `entity_type: \"thread\"`, а его `chat_id` — с `discussion`, как у любого чата.", type: 'integer' },
@@ -79,6 +79,7 @@ export default class MessagesList extends BaseCommand {
79
79
  let nextCursor = undefined;
80
80
  let pages = 0;
81
81
  const seenCursors = new Set();
82
+ let total;
82
83
  while (pages < 500) {
83
84
  const query = {
84
85
  'chat_id': flags['chat-id'],
@@ -96,7 +97,7 @@ export default class MessagesList extends BaseCommand {
96
97
  const paginate = meta?.paginate;
97
98
  nextCursor = paginate?.next_page;
98
99
  pages++;
99
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
100
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
100
101
  const hasNext = paginate?.has_next;
101
102
  if (typeof hasNext === 'boolean') {
102
103
  if (!hasNext)
@@ -106,7 +107,8 @@ export default class MessagesList extends BaseCommand {
106
107
  break;
107
108
  }
108
109
  if (process.stderr.isTTY) {
109
- const total = paginate?.total;
110
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
111
+ total ??= meta?.total;
110
112
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
111
113
  process.stderr.write(`\r Загружено: ${progress}...`);
112
114
  }
@@ -16,7 +16,7 @@ export default class MessagesUpdate extends BaseCommand {
16
16
  static flags = {
17
17
  ...BaseCommand.baseFlags,
18
18
  'content': Flags.string({
19
- description: "Текст сообщения. Поддерживает упоминания: `@nickname` или `<@user_id>` (будет автоматически преобразовано в `@nickname`). У мульти-гостя `<@user_id>` недоступного ему сотрудника остаётся в тексте как есть. До 40 000 байт в UTF-8, для кириллицы это около 20 000 символов.",
19
+ description: "Текст сообщения. Поддерживает упоминания: `@nickname` или `<@user_id>` (будет автоматически преобразовано в `@nickname`). Ник берут из ответа метода, а `user_id` — из данных: ник, составленный из имени, никого не упоминает. У мульти-гостя `<@user_id>` недоступного ему сотрудника остаётся в тексте как есть. До 40 000 байт в UTF-8, для кириллицы это около 20 000 символов.",
20
20
  }),
21
21
  'files': Flags.string({
22
22
  description: "Прикрепляемые файлы. Переданный набор заменяет прежний: чтобы оставить уже приложенный файл, передайте его снова с тем же `key`. Такой файл остаётся прежним вложением с тем же `id` и названием, а его поля из запроса не применяются. Чтобы показать файл под другим названием, загрузите его заново.",
@@ -40,7 +40,9 @@ export default class MessagesUpdate extends BaseCommand {
40
40
  for await (const chunk of process.stdin) {
41
41
  chunks.push(chunk);
42
42
  }
43
- flags['content'] = Buffer.concat(chunks).toString('utf-8').trimEnd();
43
+ const piped = Buffer.concat(chunks).toString('utf-8').trimEnd();
44
+ if (piped)
45
+ flags['content'] = piped;
44
46
  }
45
47
  const body = { message: {
46
48
  content: flags['content'],
@@ -35,6 +35,7 @@ export default class ReactionsList extends BaseCommand {
35
35
  let nextCursor = undefined;
36
36
  let pages = 0;
37
37
  const seenCursors = new Set();
38
+ let total;
38
39
  while (pages < 500) {
39
40
  const query = {
40
41
  limit: flags.limit,
@@ -49,7 +50,7 @@ export default class ReactionsList extends BaseCommand {
49
50
  const paginate = meta?.paginate;
50
51
  nextCursor = paginate?.next_page;
51
52
  pages++;
52
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
53
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
53
54
  const hasNext = paginate?.has_next;
54
55
  if (typeof hasNext === 'boolean') {
55
56
  if (!hasNext)
@@ -59,7 +60,8 @@ export default class ReactionsList extends BaseCommand {
59
60
  break;
60
61
  }
61
62
  if (process.stderr.isTTY) {
62
- const total = paginate?.total;
63
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
64
+ total ??= meta?.total;
63
65
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
64
66
  process.stderr.write(`\r Загружено: ${progress}...`);
65
67
  }
@@ -34,6 +34,7 @@ export default class ReadMemberListReaders extends BaseCommand {
34
34
  let nextCursor = undefined;
35
35
  let pages = 0;
36
36
  const seenCursors = new Set();
37
+ let total;
37
38
  while (pages < 500) {
38
39
  const query = {
39
40
  limit: flags.limit,
@@ -48,7 +49,7 @@ export default class ReadMemberListReaders extends BaseCommand {
48
49
  const paginate = meta?.paginate;
49
50
  nextCursor = paginate?.next_page;
50
51
  pages++;
51
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
52
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
52
53
  const hasNext = paginate?.has_next;
53
54
  if (typeof hasNext === 'boolean') {
54
55
  if (!hasNext)
@@ -58,7 +59,8 @@ export default class ReadMemberListReaders extends BaseCommand {
58
59
  break;
59
60
  }
60
61
  if (process.stderr.isTTY) {
61
- const total = paginate?.total;
62
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
63
+ total ??= meta?.total;
62
64
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
63
65
  process.stderr.write(`\r Загружено: ${progress}...`);
64
66
  }
@@ -55,6 +55,7 @@ export default class SearchListChats extends BaseCommand {
55
55
  let nextCursor = undefined;
56
56
  let pages = 0;
57
57
  const seenCursors = new Set();
58
+ let total;
58
59
  while (pages < 500) {
59
60
  const query = {
60
61
  query: flags['query'],
@@ -76,7 +77,7 @@ export default class SearchListChats extends BaseCommand {
76
77
  const paginate = meta?.paginate;
77
78
  nextCursor = paginate?.next_page;
78
79
  pages++;
79
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
80
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
80
81
  const hasNext = paginate?.has_next;
81
82
  if (typeof hasNext === 'boolean') {
82
83
  if (!hasNext)
@@ -86,7 +87,8 @@ export default class SearchListChats extends BaseCommand {
86
87
  break;
87
88
  }
88
89
  if (process.stderr.isTTY) {
89
- const total = paginate?.total;
90
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
91
+ total ??= meta?.total;
90
92
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
91
93
  process.stderr.write(`\r Загружено: ${progress}...`);
92
94
  }
@@ -58,6 +58,7 @@ export default class SearchListMessages extends BaseCommand {
58
58
  let nextCursor = undefined;
59
59
  let pages = 0;
60
60
  const seenCursors = new Set();
61
+ let total;
61
62
  while (pages < 500) {
62
63
  const query = {
63
64
  query: flags['query'],
@@ -80,7 +81,7 @@ export default class SearchListMessages extends BaseCommand {
80
81
  const paginate = meta?.paginate;
81
82
  nextCursor = paginate?.next_page;
82
83
  pages++;
83
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
84
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
84
85
  const hasNext = paginate?.has_next;
85
86
  if (typeof hasNext === 'boolean') {
86
87
  if (!hasNext)
@@ -90,7 +91,8 @@ export default class SearchListMessages extends BaseCommand {
90
91
  break;
91
92
  }
92
93
  if (process.stderr.isTTY) {
93
- const total = paginate?.total;
94
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
95
+ total ??= meta?.total;
94
96
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
95
97
  process.stderr.write(`\r Загружено: ${progress}...`);
96
98
  }