@pachca/cli 2026.10.3 → 2026.11.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +35 -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 +13 -7
  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 +5 -3
  13. package/dist/commands/bots/update-webhook.js +8 -1
  14. package/dist/commands/bots/update.js +14 -7
  15. package/dist/commands/chats/list-company.js +4 -2
  16. package/dist/commands/chats/list.js +4 -2
  17. package/dist/commands/chats/update.js +1 -1
  18. package/dist/commands/doctor.js +60 -35
  19. package/dist/commands/drafts/create.js +6 -3
  20. package/dist/commands/drafts/list.js +4 -2
  21. package/dist/commands/drafts/update.js +7 -4
  22. package/dist/commands/group-tags/list-users.js +4 -2
  23. package/dist/commands/group-tags/list.js +4 -2
  24. package/dist/commands/important-messages/list.js +4 -2
  25. package/dist/commands/members/add.js +1 -1
  26. package/dist/commands/members/list.js +4 -2
  27. package/dist/commands/messages/create.js +7 -5
  28. package/dist/commands/messages/list.js +4 -2
  29. package/dist/commands/messages/update.js +16 -4
  30. package/dist/commands/profile/update-status.js +5 -4
  31. package/dist/commands/reactions/add.js +9 -2
  32. package/dist/commands/reactions/list.js +4 -2
  33. package/dist/commands/reactions/remove.js +1 -28
  34. package/dist/commands/read-member/list-readers.js +4 -2
  35. package/dist/commands/search/list-chats.js +5 -3
  36. package/dist/commands/search/list-messages.js +5 -3
  37. package/dist/commands/search/list-users.js +5 -3
  38. package/dist/commands/security/list.js +5 -3
  39. package/dist/commands/tasks/create.js +3 -29
  40. package/dist/commands/tasks/list.js +4 -2
  41. package/dist/commands/tasks/update.js +4 -2
  42. package/dist/commands/threads/list.js +4 -2
  43. package/dist/commands/upload.js +27 -8
  44. package/dist/commands/users/create.js +5 -2
  45. package/dist/commands/users/list.js +5 -3
  46. package/dist/commands/users/update-status.js +5 -4
  47. package/dist/commands/users/update.js +8 -5
  48. package/dist/commands/views/open.js +1 -0
  49. package/dist/data/changelog.json +150 -0
  50. package/dist/data/endpoints.json +1 -1
  51. package/dist/hooks/init.js +8 -4
  52. package/dist/lib/flags.js +25 -0
  53. package/dist/oauth.js +2 -1
  54. package/dist/profiles.js +22 -4
  55. package/oclif.manifest.json +1 -1
  56. package/package.json +2 -2
@@ -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 { nullableString } from '../../lib/flags.js';
4
5
  export default class BotsUpdate extends BaseCommand {
5
6
  static description = "Редактирование бота";
6
7
  static scope = "bots:write";
@@ -19,10 +20,10 @@ export default class BotsUpdate extends BaseCommand {
19
20
  description: "Имя бота (макс. 255 символов)",
20
21
  }),
21
22
  'nickname': Flags.string({
22
- description: "Никнейм бота. Должен заканчиваться на `_bot`. (макс. 255 символов)",
23
+ description: "Никнейм бота: латинские буквы, цифры и `_`, в конце `_bot`, свободный в пространстве. Ник не по формату отвечает `400` на поле `bot.nickname` с кодом `invalid`, а занятый ник — с кодом `taken`. (макс. 255 символов)",
23
24
  }),
24
25
  'outgoing-url': Flags.string({
25
- description: "URL исходящего вебхука",
26
+ description: "URL исходящего вебхука (макс. 255 символов)",
26
27
  }),
27
28
  'events': Flags.string({
28
29
  description: "События, на которые подписан бот. Список заменяется целиком.",
@@ -31,7 +32,7 @@ export default class BotsUpdate extends BaseCommand {
31
32
  description: "Условие срабатывания исходящего вебхука",
32
33
  }),
33
34
  'commands': Flags.string({
34
- description: "Команды бота (триггер-слова), на которые он реагирует при trigger_on = commands. Суммарная длина команд, объединённых через запятую, не должна превышать 255 символов.",
35
+ description: "Команды бота (триггер-слова), на которые он реагирует при trigger_on = commands. Событие о сообщении приходит боту, когда первое слово текста совпадает с одной из команд целиком, с учётом регистра. Команду можно задать и без слэша в начале. Суммарная длина команд вместе с разделителями `, ` (запятая и пробел) не должна превышать 255 символов.",
35
36
  }),
36
37
  'scopes': Flags.string({
37
38
  description: "Скоупы (права доступа) токена бота. Если не указано, бот получает набор по умолчанию. Боту доступны не все скоупы: часть из них разрешена только пользовательским ролям, и попытка назначить такой скоуп возвращает `400`. Служебные значения `bot` и `all` назначать нельзя.",
@@ -43,14 +44,14 @@ export default class BotsUpdate extends BaseCommand {
43
44
  description: "Шаблонизатор для обработки шаблона входящего вебхука",
44
45
  }),
45
46
  'challenge-key': Flags.string({
46
- description: "Название поля проверки для верификации входящего вебхука",
47
+ description: "Название поля проверки для верификации входящего вебхука (макс. 255 символов)",
47
48
  }),
48
49
  'link-preview-enabled': Flags.boolean({
49
50
  description: "Показывать превью ссылок в сообщениях входящего вебхука",
50
51
  allowNo: true,
51
52
  }),
52
53
  'ignore-self-messages': Flags.boolean({
53
- description: "Не присылать боту события о его собственных сообщениях и реакциях",
54
+ description: "Не присылать боту события о сообщениях, которые он отправил сам, и о реакциях на эти сообщения, кто бы их ни поставил или снял. Нажатия кнопок приходят всегда.",
54
55
  allowNo: true,
55
56
  }),
56
57
  'events-history-enabled': Flags.boolean({
@@ -67,8 +68,8 @@ export default class BotsUpdate extends BaseCommand {
67
68
  description: "Ограничивает бота одной беседой или каналом: `true` — бота можно добавить только в один такой чат, `false` — в несколько. Личные чаты и треды в ограничение не входят.",
68
69
  allowNo: true,
69
70
  }),
70
- 'kind': Flags.string({
71
- description: "Источник входящего вебхука. Если у бота входящего вебхука не было, он включается.",
71
+ 'kind': nullableString({
72
+ description: "Источник входящего вебхука. Если у бота входящего вебхука не было, он включается. `null` удаляет входящий вебхук: адрес перестаёт работать, шаблон и настройки входящего вебхука сбрасываются, а при следующем включении бот получает новый адрес. Чтобы передать null, укажите `--kind null`.",
72
73
  }),
73
74
  'unfurl-domains': Flags.string({
74
75
  description: "Домены, ссылки на которые бот разворачивает, не больше 5. Работают вместе с событием `message_link_shared`. Список заменяется целиком.",
@@ -90,6 +91,12 @@ export default class BotsUpdate extends BaseCommand {
90
91
  if (flags['nickname'] && String(flags['nickname']).length > 255) {
91
92
  validationErrors.push({ message: `--nickname: максимум 255 символов (передано: ${String(flags['nickname']).length})`, flag: 'nickname' });
92
93
  }
94
+ if (flags['outgoing-url'] && String(flags['outgoing-url']).length > 255) {
95
+ validationErrors.push({ message: `--outgoing-url: максимум 255 символов (передано: ${String(flags['outgoing-url']).length})`, flag: 'outgoing-url' });
96
+ }
97
+ if (flags['challenge-key'] && String(flags['challenge-key']).length > 255) {
98
+ validationErrors.push({ message: `--challenge-key: максимум 255 символов (передано: ${String(flags['challenge-key']).length})`, flag: 'challenge-key' });
99
+ }
93
100
  if (validationErrors.length > 0) {
94
101
  this.validationError(validationErrors);
95
102
  }
@@ -35,6 +35,7 @@ export default class ChatsListCompany 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
  activity: flags['activity'],
@@ -50,7 +51,7 @@ export default class ChatsListCompany extends BaseCommand {
50
51
  const paginate = meta?.paginate;
51
52
  nextCursor = paginate?.next_page;
52
53
  pages++;
53
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
54
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
54
55
  const hasNext = paginate?.has_next;
55
56
  if (typeof hasNext === 'boolean') {
56
57
  if (!hasNext)
@@ -60,7 +61,8 @@ export default class ChatsListCompany extends BaseCommand {
60
61
  break;
61
62
  }
62
63
  if (process.stderr.isTTY) {
63
- const total = paginate?.total;
64
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
65
+ total ??= meta?.total;
64
66
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
65
67
  process.stderr.write(`\r Загружено: ${progress}...`);
66
68
  }
@@ -60,6 +60,7 @@ export default class ChatsList extends BaseCommand {
60
60
  let nextCursor = undefined;
61
61
  let pages = 0;
62
62
  const seenCursors = new Set();
63
+ let total;
63
64
  while (pages < 500) {
64
65
  const query = {
65
66
  sort: flags['sort'],
@@ -81,7 +82,7 @@ export default class ChatsList extends BaseCommand {
81
82
  const paginate = meta?.paginate;
82
83
  nextCursor = paginate?.next_page;
83
84
  pages++;
84
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
85
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
85
86
  const hasNext = paginate?.has_next;
86
87
  if (typeof hasNext === 'boolean') {
87
88
  if (!hasNext)
@@ -91,7 +92,8 @@ export default class ChatsList extends BaseCommand {
91
92
  break;
92
93
  }
93
94
  if (process.stderr.isTTY) {
94
- const total = paginate?.total;
95
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
96
+ total ??= meta?.total;
95
97
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
96
98
  process.stderr.write(`\r Загружено: ${progress}...`);
97
99
  }
@@ -16,7 +16,7 @@ export default class ChatsUpdate extends BaseCommand {
16
16
  static flags = {
17
17
  ...BaseCommand.baseFlags,
18
18
  'name': Flags.string({
19
- description: "Название (макс. 255 символов)",
19
+ description: "Название беседы или канала. Пустое название или название из одних пробелов отвечает `400` с кодом `blank`. (макс. 255 символов)",
20
20
  }),
21
21
  'public': Flags.boolean({
22
22
  description: "Открытый доступ",
@@ -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";
@@ -19,9 +20,9 @@ export default class DraftsCreate extends BaseCommand {
19
20
  description: "Идентификатор того, что названо в `entity_type`: беседы или канала, треда либо сотрудника (pachca chats list | pachca users list)",
20
21
  }),
21
22
  'content': Flags.string({
22
- description: "Текст черновика. Можно не передавать, если есть вложения, но пустым черновик быть не может. До 40 000 байт в UTF-8.",
23
+ description: "Текст черновика. Можно не передавать, если есть вложения, но пустым черновик быть не может. Поддерживает упоминания, как текст в методе [Новое сообщение](POST /messages). `<@user_id>` остаётся в тексте черновика как передан, а при отправке становится `@nickname`. До 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";
@@ -16,10 +17,10 @@ export default class DraftsUpdate extends BaseCommand {
16
17
  static flags = {
17
18
  ...BaseCommand.baseFlags,
18
19
  'content': Flags.string({
19
- description: "Текст черновика. До 40 000 байт в UTF-8.",
20
+ description: "Текст черновика. Поддерживает упоминания, как текст в методе [Новое сообщение](POST /messages). `<@user_id>` остаётся в тексте черновика как передан, а при отправке становится `@nickname`. До 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: "Прикрепляемые файлы",
@@ -31,13 +31,13 @@ export default class MessagesCreate extends BaseCommand {
31
31
  description: "Идентификатор сообщения, на которое вы отвечаете с цитатой. Ответить можно на сообщение в любом чате, в том числе в треде: ответ отправляют в тот же чат или тред, где лежит это сообщение, иначе запрос вернёт ошибку `422`. Чтобы написать в тред под сообщением, укажите `entity_type: \"thread\"` и `id` треда в `entity_id`.",
32
32
  }),
33
33
  'display-avatar-url': Flags.string({
34
- description: "Ссылка на специальную аватарку отправителя для этого сообщения. Использование этого поля возможно только с access_token бота. (макс. 255 символов)",
34
+ description: "Ссылка на специальную аватарку отправителя для этого сообщения. Использование этого поля возможно только с access_token бота. До 255 байт в UTF-8. (макс. 255 символов)",
35
35
  }),
36
36
  'display-name': Flags.string({
37
- description: "Полное специальное имя отправителя для этого сообщения. Вместе с `display_avatar_url` бот подписывает сообщение чужим именем и аватаром, и только это сообщение. Бейдж «Бот» остаётся, а в профиле отправителя видно настоящего бота. Так показывают автора из другой системы, когда переносят в Пачку чужую переписку. С личным токеном поле недоступно, ответ `403`. Подробности — в разделе [Боты](https://dev.pachca.com/guides/bots/overview). (макс. 255 символов)",
37
+ description: "Полное специальное имя отправителя для этого сообщения. Вместе с `display_avatar_url` бот подписывает сообщение чужим именем и аватаром, и только это сообщение. Бейдж «Бот» остаётся, а в профиле отправителя видно настоящего бота. Так показывают автора из другой системы, когда переносят в Пачку чужую переписку. С личным токеном поле недоступно, ответ `403`. До 255 байт в UTF-8, для кириллицы это около 127 символов. Подробности — в разделе [Боты](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' },