@pachca/cli 2026.10.2 → 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 (107) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +29 -29
  3. package/README.template.md +19 -28
  4. package/dist/base-command.js +69 -37
  5. package/dist/client.js +58 -33
  6. package/dist/commands/api.js +4 -4
  7. package/dist/commands/auth/login.js +12 -9
  8. package/dist/commands/auth/logout.js +23 -12
  9. package/dist/commands/bots/create-token.js +0 -3
  10. package/dist/commands/bots/create.js +1 -5
  11. package/dist/commands/bots/delete.js +0 -3
  12. package/dist/commands/bots/get-stream.js +23 -0
  13. package/dist/commands/bots/list-company.js +4 -5
  14. package/dist/commands/bots/list-events.js +4 -5
  15. package/dist/commands/bots/list-scopes.js +0 -3
  16. package/dist/commands/bots/list-tokens.js +4 -2
  17. package/dist/commands/bots/list.js +4 -5
  18. package/dist/commands/bots/recreate-token-self.js +0 -3
  19. package/dist/commands/bots/recreate-token.js +0 -3
  20. package/dist/commands/bots/reissue-token.js +0 -3
  21. package/dist/commands/bots/remove-event.js +0 -3
  22. package/dist/commands/bots/update-webhook.js +0 -3
  23. package/dist/commands/bots/update.js +4 -8
  24. package/dist/commands/chats/archive.js +0 -4
  25. package/dist/commands/chats/create.js +3 -6
  26. package/dist/commands/chats/download-export.js +0 -3
  27. package/dist/commands/chats/list-company.js +4 -5
  28. package/dist/commands/chats/list.js +5 -8
  29. package/dist/commands/chats/remove-avatar.js +0 -3
  30. package/dist/commands/chats/request-export.js +0 -3
  31. package/dist/commands/chats/unarchive.js +0 -3
  32. package/dist/commands/chats/update-avatar.js +0 -3
  33. package/dist/commands/chats/update.js +0 -3
  34. package/dist/commands/custom-properties/list.js +0 -3
  35. package/dist/commands/doctor.js +60 -35
  36. package/dist/commands/drafts/create.js +6 -6
  37. package/dist/commands/drafts/delete.js +0 -3
  38. package/dist/commands/drafts/list.js +4 -2
  39. package/dist/commands/drafts/update.js +7 -4
  40. package/dist/commands/files/uploads.js +0 -3
  41. package/dist/commands/group-tags/create.js +0 -3
  42. package/dist/commands/group-tags/list-users.js +4 -5
  43. package/dist/commands/group-tags/list.js +4 -5
  44. package/dist/commands/important-messages/create.js +64 -0
  45. package/dist/commands/important-messages/delete.js +42 -0
  46. package/dist/commands/important-messages/list.js +91 -0
  47. package/dist/commands/members/add.js +1 -6
  48. package/dist/commands/members/leave.js +0 -3
  49. package/dist/commands/members/list.js +4 -6
  50. package/dist/commands/members/remove.js +0 -3
  51. package/dist/commands/members/update.js +0 -3
  52. package/dist/commands/messages/create.js +6 -9
  53. package/dist/commands/messages/delete.js +0 -3
  54. package/dist/commands/messages/get.js +0 -5
  55. package/dist/commands/messages/list.js +5 -7
  56. package/dist/commands/messages/pin.js +0 -3
  57. package/dist/commands/messages/unfurl.js +0 -3
  58. package/dist/commands/messages/unpin.js +0 -3
  59. package/dist/commands/messages/update.js +5 -8
  60. package/dist/commands/oauth/token-info.js +0 -3
  61. package/dist/commands/profile/delete-avatar.js +0 -3
  62. package/dist/commands/profile/delete-status.js +0 -3
  63. package/dist/commands/profile/get.js +0 -4
  64. package/dist/commands/profile/update-avatar.js +0 -3
  65. package/dist/commands/profile/update-status.js +1 -4
  66. package/dist/commands/reactions/add.js +3 -6
  67. package/dist/commands/reactions/list.js +4 -2
  68. package/dist/commands/reactions/remove.js +3 -6
  69. package/dist/commands/read-member/list-readers.js +4 -5
  70. package/dist/commands/search/list-chats.js +4 -7
  71. package/dist/commands/search/list-messages.js +4 -5
  72. package/dist/commands/search/list-users.js +4 -7
  73. package/dist/commands/security/list.js +4 -7
  74. package/dist/commands/tasks/create.js +4 -7
  75. package/dist/commands/tasks/delete.js +0 -3
  76. package/dist/commands/tasks/get.js +0 -3
  77. package/dist/commands/tasks/list.js +4 -7
  78. package/dist/commands/tasks/update.js +5 -7
  79. package/dist/commands/threads/add.js +0 -4
  80. package/dist/commands/threads/list.js +4 -6
  81. package/dist/commands/upload.js +27 -8
  82. package/dist/commands/users/create.js +0 -5
  83. package/dist/commands/users/delete.js +0 -3
  84. package/dist/commands/users/get-status.js +0 -3
  85. package/dist/commands/users/get.js +0 -3
  86. package/dist/commands/users/list.js +4 -7
  87. package/dist/commands/users/remove-avatar.js +0 -3
  88. package/dist/commands/users/remove-status.js +0 -3
  89. package/dist/commands/users/update-avatar.js +0 -3
  90. package/dist/commands/users/update-status.js +1 -4
  91. package/dist/commands/users/update.js +5 -5
  92. package/dist/commands/views/open.js +1 -5
  93. package/dist/commands/views/submit-response.js +0 -3
  94. package/dist/data/alternatives.json +5 -9
  95. package/dist/data/changelog.json +177 -0
  96. package/dist/data/endpoints.json +1 -1
  97. package/dist/help.js +0 -1
  98. package/dist/hooks/command-not-found.js +0 -1
  99. package/dist/hooks/init.js +13 -5
  100. package/dist/lib/flags.js +25 -0
  101. package/dist/oauth.js +11 -5
  102. package/dist/profiles.js +22 -4
  103. package/dist/token-refresh.js +61 -14
  104. package/oclif.manifest.json +1 -1
  105. package/package.json +2 -2
  106. package/dist/commands/guide.js +0 -90
  107. package/dist/data/workflows.json +0 -1058
@@ -5,9 +5,6 @@ import * as fs from 'node:fs';
5
5
  import * as path from 'node:path';
6
6
  export default class ChatsUpdateAvatar extends BaseCommand {
7
7
  static description = "Загрузка аватара чата";
8
- static examples = [
9
- "Поставить или убрать аватар чата — Загрузи аватар беседы или канала:\n $ pachca chats update-avatar"
10
- ];
11
8
  static scope = "chats:update";
12
9
  static apiMethod = "PUT";
13
10
  static apiPath = "/chats/{id}/avatar";
@@ -3,9 +3,6 @@ import { Args, Flags } from '@oclif/core';
3
3
  import { BaseCommand } from '../../base-command.js';
4
4
  export default class ChatsUpdate extends BaseCommand {
5
5
  static description = "Редактирование чата";
6
- static examples = [
7
- "Переименовать или обновить чат — Обнови чат:\n $ pachca chats update"
8
- ];
9
6
  static scope = "chats:update";
10
7
  static apiMethod = "PUT";
11
8
  static apiPath = "/chats/{id}";
@@ -4,9 +4,6 @@ import { BaseCommand } from '../../base-command.js';
4
4
  import * as clack from '@clack/prompts';
5
5
  export default class CustomPropertiesList extends BaseCommand {
6
6
  static description = "Список дополнительных полей";
7
- static examples = [
8
- "Получить кастомные поля профиля — Получи список дополнительных полей для сотрудников:\n $ pachca custom-properties list"
9
- ];
10
7
  static hiddenAliases = ["common:custom-properties"];
11
8
  static scope = "custom_properties:read";
12
9
  static apiMethod = "GET";
@@ -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,11 +2,9 @@
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
- static examples = [
8
- "Отправить сообщение позже или по расписанию — Создай отложенное сообщение: `schedule.start_date` — когда отправить, `schedule.repetition.interval` — как повторять (`once`, если один раз):\n $ pachca drafts create"
9
- ];
10
8
  static scope = "drafts:write";
11
9
  static apiMethod = "POST";
12
10
  static apiPath = "/drafts";
@@ -22,9 +20,9 @@ export default class DraftsCreate extends BaseCommand {
22
20
  description: "Идентификатор того, что названо в `entity_type`: беседы или канала, треда либо сотрудника (pachca chats list | pachca users list)",
23
21
  }),
24
22
  'content': Flags.string({
25
- description: "Текст черновика. Можно не передавать, если есть вложения, но пустым черновик быть не может. До 40 000 байт в UTF-8, а у отложенного сообщения в пространстве с продвинутым шифрованием — до 27 000.",
23
+ description: "Текст черновика. Можно не передавать, если есть вложения, но пустым черновик быть не может. До 40 000 байт в UTF-8.",
26
24
  }),
27
- 'parent-message-id': Flags.integer({
25
+ 'parent-message-id': nullableInteger({
28
26
  description: "Идентификатор сообщения, ответом на которое уйдёт черновик. Сообщение должно быть в том же чате.",
29
27
  }),
30
28
  'files': Flags.string({
@@ -43,7 +41,9 @@ export default class DraftsCreate extends BaseCommand {
43
41
  for await (const chunk of process.stdin) {
44
42
  chunks.push(chunk);
45
43
  }
46
- 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;
47
47
  }
48
48
  const missingRequired = [
49
49
  { flag: 'entity-type', label: "Куда пишется черновик: в беседу или канал, в тред либо в личную переписку с сотрудником", type: 'string' },
@@ -4,9 +4,6 @@ import { BaseCommand } from '../../base-command.js';
4
4
  import * as clack from '@clack/prompts';
5
5
  export default class DraftsDelete extends BaseCommand {
6
6
  static description = "Удаление черновика";
7
- static examples = [
8
- "Отправить сообщение позже или по расписанию — Чтобы отменить отправку — удали отложенное сообщение:\n $ pachca drafts delete"
9
- ];
10
7
  static scope = "drafts:write";
11
8
  static apiMethod = "DELETE";
12
9
  static apiPath = "/drafts/{id}";
@@ -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, а у отложенного сообщения в пространстве с продвинутым шифрованием — до 27 000.",
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'],
@@ -1,9 +1,6 @@
1
1
  import { BaseCommand } from '../../base-command.js';
2
2
  export default class FilesUploads extends BaseCommand {
3
3
  static description = "Получение подписи, ключа и других параметров";
4
- static examples = [
5
- "Изменить вложения сообщения — Если нужно добавить новый файл — загрузи его:\n $ pachca files uploads"
6
- ];
7
4
  static hiddenAliases = ["common:uploads"];
8
5
  static scope = "uploads:write";
9
6
  static apiMethod = "POST";
@@ -4,9 +4,6 @@ import { BaseCommand } from '../../base-command.js';
4
4
  import * as clack from '@clack/prompts';
5
5
  export default class GroupTagsCreate extends BaseCommand {
6
6
  static description = "Новый тег";
7
- static examples = [
8
- "Массовое создание сотрудников с тегами — Создай тег (если нужен):\n $ pachca group-tags create"
9
- ];
10
7
  static scope = "group_tags:write";
11
8
  static apiMethod = "POST";
12
9
  static apiPath = "/group_tags";
@@ -3,9 +3,6 @@ import { Args, Flags } from '@oclif/core';
3
3
  import { BaseCommand } from '../../base-command.js';
4
4
  export default class GroupTagsListUsers extends BaseCommand {
5
5
  static description = "Список сотрудников тега";
6
- static examples = [
7
- "Получить всех сотрудников тега/департамента — Получи всех участников тега:\n $ pachca group-tags list-users"
8
- ];
9
6
  static scope = "group_tags:read";
10
7
  static apiMethod = "GET";
11
8
  static apiPath = "/group_tags/{id}/users";
@@ -38,6 +35,7 @@ export default class GroupTagsListUsers extends BaseCommand {
38
35
  let nextCursor = undefined;
39
36
  let pages = 0;
40
37
  const seenCursors = new Set();
38
+ let total;
41
39
  while (pages < 500) {
42
40
  const query = {
43
41
  limit: flags.limit,
@@ -52,7 +50,7 @@ export default class GroupTagsListUsers extends BaseCommand {
52
50
  const paginate = meta?.paginate;
53
51
  nextCursor = paginate?.next_page;
54
52
  pages++;
55
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
53
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
56
54
  const hasNext = paginate?.has_next;
57
55
  if (typeof hasNext === 'boolean') {
58
56
  if (!hasNext)
@@ -62,7 +60,8 @@ export default class GroupTagsListUsers extends BaseCommand {
62
60
  break;
63
61
  }
64
62
  if (process.stderr.isTTY) {
65
- const total = paginate?.total;
63
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
64
+ total ??= meta?.total;
66
65
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
67
66
  process.stderr.write(`\r Загружено: ${progress}...`);
68
67
  }
@@ -3,9 +3,6 @@ import { Flags } from '@oclif/core';
3
3
  import { BaseCommand } from '../../base-command.js';
4
4
  export default class GroupTagsList extends BaseCommand {
5
5
  static description = "Список тегов";
6
- static examples = [
7
- "Получить всех сотрудников тега/департамента — Найди тег по названию, возьми `id`:\n $ pachca group-tags list"
8
- ];
9
6
  static scope = "group_tags:read";
10
7
  static apiMethod = "GET";
11
8
  static apiPath = "/group_tags";
@@ -36,6 +33,7 @@ export default class GroupTagsList extends BaseCommand {
36
33
  let nextCursor = undefined;
37
34
  let pages = 0;
38
35
  const seenCursors = new Set();
36
+ let total;
39
37
  while (pages < 500) {
40
38
  const query = {
41
39
  'names[]': flags['names']?.split(','),
@@ -51,7 +49,7 @@ export default class GroupTagsList extends BaseCommand {
51
49
  const paginate = meta?.paginate;
52
50
  nextCursor = paginate?.next_page;
53
51
  pages++;
54
- // Условие конца: списочные методы и /users?query= — has_next === false; методы поиска (без has_next) — пустой data
52
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
55
53
  const hasNext = paginate?.has_next;
56
54
  if (typeof hasNext === 'boolean') {
57
55
  if (!hasNext)
@@ -61,7 +59,8 @@ export default class GroupTagsList extends BaseCommand {
61
59
  break;
62
60
  }
63
61
  if (process.stderr.isTTY) {
64
- const total = paginate?.total;
62
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
63
+ total ??= meta?.total;
65
64
  const progress = total ? `${allData.length} / ${total}` : String(allData.length);
66
65
  process.stderr.write(`\r Загружено: ${progress}...`);
67
66
  }
@@ -0,0 +1,64 @@
1
+ // Auto-generated from openapi.yaml — DO NOT EDIT
2
+ import { Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../base-command.js';
4
+ import * as clack from '@clack/prompts';
5
+ export default class ImportantMessagesCreate extends BaseCommand {
6
+ static description = "Добавление сообщения в важные";
7
+ static scope = "important_messages:write";
8
+ static apiMethod = "POST";
9
+ static apiPath = "/important_messages";
10
+ static requiredFlags = ["message-id"];
11
+ static args = {};
12
+ static flags = {
13
+ ...BaseCommand.baseFlags,
14
+ 'message-id': Flags.integer({
15
+ description: "Идентификатор сообщения (pachca messages list)",
16
+ }),
17
+ };
18
+ async run() {
19
+ const { args, flags } = await this.parse(ImportantMessagesCreate);
20
+ this.parsedFlags = flags;
21
+ const missingRequired = [
22
+ { flag: 'message-id', label: "Идентификатор сообщения", type: 'integer' },
23
+ ].filter((f) => flags[f.flag] === undefined || flags[f.flag] === null);
24
+ if (missingRequired.length > 0) {
25
+ if (this.isInteractive()) {
26
+ for (const field of missingRequired) {
27
+ const value = await clack.text({ message: field.label, validate: (v) => v.length === 0 ? 'Обязательное поле' : undefined });
28
+ if (clack.isCancel(value)) {
29
+ process.stderr.write('Отменено.\n');
30
+ this.exit(0);
31
+ }
32
+ if (field.type === 'integer') {
33
+ flags[field.flag] = Number.parseInt(value, 10);
34
+ }
35
+ else if (field.type === 'boolean') {
36
+ flags[field.flag] = value === 'true';
37
+ }
38
+ else {
39
+ flags[field.flag] = value;
40
+ }
41
+ }
42
+ }
43
+ else {
44
+ this.validationError(missingRequired.map((f) => ({ message: `Обязательный флаг --${f.flag} не передан`, flag: f.flag })), { hint: "Обязательные: --message-id <integer>. pachca introspect important-messages create" });
45
+ }
46
+ }
47
+ const body = {
48
+ message_id: flags['message-id'],
49
+ };
50
+ // Clean undefined fields
51
+ for (const [k, v] of Object.entries(body)) {
52
+ if (v === undefined)
53
+ delete body[k];
54
+ }
55
+ const { data } = await this.apiRequest({
56
+ method: 'POST',
57
+ path: '/important_messages',
58
+ body,
59
+ });
60
+ const responseBody = (data ?? {});
61
+ const result = responseBody.data ?? responseBody;
62
+ this.output(result);
63
+ }
64
+ }
@@ -0,0 +1,42 @@
1
+ // Auto-generated from openapi.yaml — DO NOT EDIT
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { BaseCommand } from '../../base-command.js';
4
+ import * as clack from '@clack/prompts';
5
+ export default class ImportantMessagesDelete extends BaseCommand {
6
+ static description = "Удаление сообщения из важных";
7
+ static scope = "important_messages:write";
8
+ static apiMethod = "DELETE";
9
+ static apiPath = "/important_messages/{id}";
10
+ static args = {
11
+ id: Args.integer({
12
+ description: "Идентификатор сообщения",
13
+ required: true,
14
+ }),
15
+ };
16
+ static flags = {
17
+ ...BaseCommand.baseFlags,
18
+ force: Flags.boolean({
19
+ description: 'Пропустить подтверждение',
20
+ default: false,
21
+ }),
22
+ };
23
+ async run() {
24
+ const { args, flags } = await this.parse(ImportantMessagesDelete);
25
+ this.parsedFlags = flags;
26
+ if (!flags.force) {
27
+ if (!this.isInteractive()) {
28
+ this.validationError([{ message: 'Деструктивная операция требует флага --force', flag: 'force' }], { type: 'PACHCA_DESTRUCTIVE_OP_ERROR', hint: "pachca important-messages delete <id> --force" });
29
+ }
30
+ const confirm = await clack.confirm({ message: 'Вы уверены?' });
31
+ if (clack.isCancel(confirm) || !confirm) {
32
+ process.stderr.write('Отменено.\n');
33
+ this.exit(0);
34
+ }
35
+ }
36
+ const { data } = await this.apiRequest({
37
+ method: 'DELETE',
38
+ path: `/important_messages/${args.id}`,
39
+ });
40
+ this.success('Удалено');
41
+ }
42
+ }
@@ -0,0 +1,91 @@
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 ImportantMessagesList extends BaseCommand {
5
+ static description = "Список важных сообщений";
6
+ static scope = "important_messages:read";
7
+ static apiMethod = "GET";
8
+ static apiPath = "/important_messages";
9
+ static defaultColumns = ["id", "content", "created_at", "entity_type", "entity_id"];
10
+ static args = {};
11
+ static flags = {
12
+ ...BaseCommand.baseFlags,
13
+ limit: Flags.integer({
14
+ description: 'Количество результатов на страницу',
15
+ }),
16
+ cursor: Flags.string({
17
+ description: 'Курсор для следующей страницы',
18
+ }),
19
+ all: Flags.boolean({
20
+ description: 'Загрузить все страницы автоматически',
21
+ default: false,
22
+ }),
23
+ };
24
+ async run() {
25
+ const { args, flags } = await this.parse(ImportantMessagesList);
26
+ this.parsedFlags = flags;
27
+ if (flags.all) {
28
+ // Auto-pagination
29
+ const allData = [];
30
+ let nextCursor = undefined;
31
+ let pages = 0;
32
+ const seenCursors = new Set();
33
+ let total;
34
+ while (pages < 500) {
35
+ const query = {
36
+ limit: flags.limit,
37
+ cursor: nextCursor,
38
+ };
39
+ const response = await this.apiRequest({ method: 'GET', path: '/important_messages', query });
40
+ const body = response.data;
41
+ const items = body.data;
42
+ if (items)
43
+ allData.push(...items);
44
+ const meta = body.meta;
45
+ const paginate = meta?.paginate;
46
+ nextCursor = paginate?.next_page;
47
+ pages++;
48
+ // Условие конца: has_next === false, а в ответе без has_next — пустой data
49
+ const hasNext = paginate?.has_next;
50
+ if (typeof hasNext === 'boolean') {
51
+ if (!hasNext)
52
+ break;
53
+ }
54
+ else if (!items || items.length === 0) {
55
+ break;
56
+ }
57
+ if (process.stderr.isTTY) {
58
+ // total лежит в meta у методов поиска. Берём его с первой страницы: при поиске сообщений по дате следующие страницы считают только остаток
59
+ total ??= meta?.total;
60
+ const progress = total ? `${allData.length} / ${total}` : String(allData.length);
61
+ process.stderr.write(`\r Загружено: ${progress}...`);
62
+ }
63
+ if (!nextCursor)
64
+ break;
65
+ if (seenCursors.has(nextCursor)) {
66
+ process.stderr.write('\n⚠ Обнаружен цикл пагинации, остановка.\n');
67
+ break;
68
+ }
69
+ seenCursors.add(nextCursor);
70
+ }
71
+ if (pages >= 500) {
72
+ process.stderr.write('\n⚠ Достигнут лимит 500 страниц.\n');
73
+ }
74
+ if (process.stderr.isTTY)
75
+ process.stderr.write('\n');
76
+ this.output(allData);
77
+ return;
78
+ }
79
+ const { data } = await this.apiRequest({
80
+ method: 'GET',
81
+ path: '/important_messages',
82
+ query: {
83
+ limit: flags.limit,
84
+ cursor: flags.cursor,
85
+ },
86
+ });
87
+ const responseBody = (data ?? {});
88
+ const items = responseBody.data ?? responseBody;
89
+ this.output(items);
90
+ }
91
+ }