@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.
- package/CHANGELOG.md +41 -0
- package/README.md +29 -29
- package/README.template.md +19 -28
- package/dist/base-command.js +69 -37
- package/dist/client.js +58 -33
- package/dist/commands/api.js +4 -4
- package/dist/commands/auth/login.js +12 -9
- package/dist/commands/auth/logout.js +23 -12
- package/dist/commands/bots/create-token.js +0 -3
- package/dist/commands/bots/create.js +1 -5
- package/dist/commands/bots/delete.js +0 -3
- package/dist/commands/bots/get-stream.js +23 -0
- package/dist/commands/bots/list-company.js +4 -5
- package/dist/commands/bots/list-events.js +4 -5
- package/dist/commands/bots/list-scopes.js +0 -3
- package/dist/commands/bots/list-tokens.js +4 -2
- package/dist/commands/bots/list.js +4 -5
- package/dist/commands/bots/recreate-token-self.js +0 -3
- package/dist/commands/bots/recreate-token.js +0 -3
- package/dist/commands/bots/reissue-token.js +0 -3
- package/dist/commands/bots/remove-event.js +0 -3
- package/dist/commands/bots/update-webhook.js +0 -3
- package/dist/commands/bots/update.js +4 -8
- package/dist/commands/chats/archive.js +0 -4
- package/dist/commands/chats/create.js +3 -6
- package/dist/commands/chats/download-export.js +0 -3
- package/dist/commands/chats/list-company.js +4 -5
- package/dist/commands/chats/list.js +5 -8
- package/dist/commands/chats/remove-avatar.js +0 -3
- package/dist/commands/chats/request-export.js +0 -3
- package/dist/commands/chats/unarchive.js +0 -3
- package/dist/commands/chats/update-avatar.js +0 -3
- package/dist/commands/chats/update.js +0 -3
- package/dist/commands/custom-properties/list.js +0 -3
- package/dist/commands/doctor.js +60 -35
- package/dist/commands/drafts/create.js +6 -6
- package/dist/commands/drafts/delete.js +0 -3
- package/dist/commands/drafts/list.js +4 -2
- package/dist/commands/drafts/update.js +7 -4
- package/dist/commands/files/uploads.js +0 -3
- package/dist/commands/group-tags/create.js +0 -3
- package/dist/commands/group-tags/list-users.js +4 -5
- package/dist/commands/group-tags/list.js +4 -5
- package/dist/commands/important-messages/create.js +64 -0
- package/dist/commands/important-messages/delete.js +42 -0
- package/dist/commands/important-messages/list.js +91 -0
- package/dist/commands/members/add.js +1 -6
- package/dist/commands/members/leave.js +0 -3
- package/dist/commands/members/list.js +4 -6
- package/dist/commands/members/remove.js +0 -3
- package/dist/commands/members/update.js +0 -3
- package/dist/commands/messages/create.js +6 -9
- package/dist/commands/messages/delete.js +0 -3
- package/dist/commands/messages/get.js +0 -5
- package/dist/commands/messages/list.js +5 -7
- package/dist/commands/messages/pin.js +0 -3
- package/dist/commands/messages/unfurl.js +0 -3
- package/dist/commands/messages/unpin.js +0 -3
- package/dist/commands/messages/update.js +5 -8
- package/dist/commands/oauth/token-info.js +0 -3
- package/dist/commands/profile/delete-avatar.js +0 -3
- package/dist/commands/profile/delete-status.js +0 -3
- package/dist/commands/profile/get.js +0 -4
- package/dist/commands/profile/update-avatar.js +0 -3
- package/dist/commands/profile/update-status.js +1 -4
- package/dist/commands/reactions/add.js +3 -6
- package/dist/commands/reactions/list.js +4 -2
- package/dist/commands/reactions/remove.js +3 -6
- package/dist/commands/read-member/list-readers.js +4 -5
- package/dist/commands/search/list-chats.js +4 -7
- package/dist/commands/search/list-messages.js +4 -5
- package/dist/commands/search/list-users.js +4 -7
- package/dist/commands/security/list.js +4 -7
- package/dist/commands/tasks/create.js +4 -7
- package/dist/commands/tasks/delete.js +0 -3
- package/dist/commands/tasks/get.js +0 -3
- package/dist/commands/tasks/list.js +4 -7
- package/dist/commands/tasks/update.js +5 -7
- package/dist/commands/threads/add.js +0 -4
- package/dist/commands/threads/list.js +4 -6
- package/dist/commands/upload.js +27 -8
- package/dist/commands/users/create.js +0 -5
- package/dist/commands/users/delete.js +0 -3
- package/dist/commands/users/get-status.js +0 -3
- package/dist/commands/users/get.js +0 -3
- package/dist/commands/users/list.js +4 -7
- package/dist/commands/users/remove-avatar.js +0 -3
- package/dist/commands/users/remove-status.js +0 -3
- package/dist/commands/users/update-avatar.js +0 -3
- package/dist/commands/users/update-status.js +1 -4
- package/dist/commands/users/update.js +5 -5
- package/dist/commands/views/open.js +1 -5
- package/dist/commands/views/submit-response.js +0 -3
- package/dist/data/alternatives.json +5 -9
- package/dist/data/changelog.json +177 -0
- package/dist/data/endpoints.json +1 -1
- package/dist/help.js +0 -1
- package/dist/hooks/command-not-found.js +0 -1
- package/dist/hooks/init.js +13 -5
- package/dist/lib/flags.js +25 -0
- package/dist/oauth.js +11 -5
- package/dist/profiles.js +22 -4
- package/dist/token-refresh.js +61 -14
- package/oclif.manifest.json +1 -1
- package/package.json +2 -2
- package/dist/commands/guide.js +0 -90
- package/dist/data/workflows.json +0 -1058
package/dist/help.js
CHANGED
package/dist/hooks/init.js
CHANGED
|
@@ -4,10 +4,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import semver from 'semver';
|
|
6
6
|
import ansis from 'ansis';
|
|
7
|
-
import { getDefaults, resolveToken, TokenNotFoundError, ProfileNotFoundError, } from '../profiles.js';
|
|
7
|
+
import { getDefaults, profileFlag, resolveToken, TokenNotFoundError, ProfileNotFoundError, } from '../profiles.js';
|
|
8
8
|
import { ensureFreshToken, isSessionExpired } from '../token-refresh.js';
|
|
9
9
|
import { outputError } from '../output.js';
|
|
10
10
|
import { defaultOutputFormat, isInteractive } from '../utils.js';
|
|
11
|
+
import { detectIntrospectMode } from '../api-introspect.js';
|
|
11
12
|
/**
|
|
12
13
|
* Resolve output format from argv before flags are parsed by oclif.
|
|
13
14
|
*/
|
|
@@ -40,7 +41,6 @@ const SKIP_AUTH = new Set([
|
|
|
40
41
|
'doctor',
|
|
41
42
|
'autocomplete',
|
|
42
43
|
'commands',
|
|
43
|
-
'guide',
|
|
44
44
|
'introspect',
|
|
45
45
|
'changelog',
|
|
46
46
|
'upgrade',
|
|
@@ -102,6 +102,10 @@ const hook = async function (opts) {
|
|
|
102
102
|
// Skip auth for exempt commands
|
|
103
103
|
if (SKIP_AUTH.has(commandId))
|
|
104
104
|
return;
|
|
105
|
+
// `api ls` and `api <METHOD> <path> --describe|--spec|--docs` read the bundled
|
|
106
|
+
// spec and never call the API, so an agent can study the API before signing in.
|
|
107
|
+
if (commandId === 'api' && (opts.argv[0] === 'ls' || detectIntrospectMode(opts.argv)))
|
|
108
|
+
return;
|
|
105
109
|
// Skip auth when --help, --version, or --dry-run flag is passed
|
|
106
110
|
if (process.argv.includes('--help') || process.argv.includes('-h'))
|
|
107
111
|
return;
|
|
@@ -142,18 +146,22 @@ const hook = async function (opts) {
|
|
|
142
146
|
// runs with the current token and reports the real API error itself.
|
|
143
147
|
if (!isSessionExpired(error))
|
|
144
148
|
return;
|
|
149
|
+
// The server no longer renews this login: it was revoked (in the «Токены CLI»
|
|
150
|
+
// list, by an admin) or ran out. Name the profile, like the 401 hint does,
|
|
151
|
+
// so the command can be pasted as is.
|
|
152
|
+
const relogin = `pachca auth login${profileFlag(profileName)}`;
|
|
145
153
|
const format = resolveOutputFromArgv();
|
|
146
154
|
if (format === 'json' || !process.stderr.isTTY) {
|
|
147
155
|
outputError({
|
|
148
156
|
error: 'Session expired',
|
|
149
157
|
type: 'PACHCA_AUTH_ERROR',
|
|
150
158
|
code: null,
|
|
151
|
-
hint:
|
|
159
|
+
hint: relogin,
|
|
152
160
|
}, format);
|
|
153
161
|
}
|
|
154
162
|
else {
|
|
155
|
-
process.stderr.write(`✗
|
|
156
|
-
process.stderr.write(`
|
|
163
|
+
process.stderr.write(`✗ Вход завершён или отозван. Войдите заново:\n\n`);
|
|
164
|
+
process.stderr.write(` ${relogin}\n`);
|
|
157
165
|
}
|
|
158
166
|
process.exit(3);
|
|
159
167
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Errors, Flags } from '@oclif/core';
|
|
2
|
+
/**
|
|
3
|
+
* Flags for body fields the spec marks nullable.
|
|
4
|
+
*
|
|
5
|
+
* In such a field `null` carries meaning of its own: `--kind null` deletes a
|
|
6
|
+
* bot's incoming webhook, `--parent-message-id null` takes the reply off a
|
|
7
|
+
* draft. A plain string flag would send the text "null", which the API refuses,
|
|
8
|
+
* and an integer flag would not accept the word at all. These flags read the
|
|
9
|
+
* literal `null` as JSON null and pass every other value through as before.
|
|
10
|
+
*/
|
|
11
|
+
export const nullableString = Flags.custom({
|
|
12
|
+
async parse(input) {
|
|
13
|
+
return input === 'null' ? null : input;
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
export const nullableInteger = Flags.custom({
|
|
17
|
+
async parse(input) {
|
|
18
|
+
if (input === 'null')
|
|
19
|
+
return null;
|
|
20
|
+
if (!/^-?\d+$/.test(input)) {
|
|
21
|
+
throw new Errors.CLIError(`Expected an integer or null but received: ${input}`);
|
|
22
|
+
}
|
|
23
|
+
return Number.parseInt(input, 10);
|
|
24
|
+
},
|
|
25
|
+
});
|
package/dist/oauth.js
CHANGED
|
@@ -12,7 +12,8 @@ function delay(ms) {
|
|
|
12
12
|
*
|
|
13
13
|
* The CLI is a public client: it ships no secret and identifies itself with a
|
|
14
14
|
* fixed `client_id`. Protection comes from the code never leaving the user's
|
|
15
|
-
* own screen
|
|
15
|
+
* own screen: the CLI prints the address and the code apart and never a link
|
|
16
|
+
* with the code in it.
|
|
16
17
|
*/
|
|
17
18
|
export const CLI_CLIENT_ID = 'pachca_cli';
|
|
18
19
|
const DEVICE_CODE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code';
|
|
@@ -162,10 +163,15 @@ export async function pollForToken(grant) {
|
|
|
162
163
|
/**
|
|
163
164
|
* Put out an access token server-side (RFC 7009).
|
|
164
165
|
*
|
|
165
|
-
* The endpoint only accepts self-revocation: the same token must arrive
|
|
166
|
-
* the body and as the bearer, and it must belong to the CLI's public
|
|
167
|
-
*
|
|
168
|
-
*
|
|
166
|
+
* The endpoint only accepts self-revocation: the same access token must arrive
|
|
167
|
+
* both in the body and as the bearer, and it must belong to the CLI's public
|
|
168
|
+
* client. A refresh token is refused here: the CLI does not know the client's
|
|
169
|
+
* real `client_id`, and self-revocation compares the body with the bearer.
|
|
170
|
+
*
|
|
171
|
+
* `true` proves the revocation only for a live token. The server answers `200`
|
|
172
|
+
* to an expired one too, yet leaves its record and refresh token untouched.
|
|
173
|
+
* Pass a token known to be live — `revokeSession` in `token-refresh.ts` does.
|
|
174
|
+
* Returns false when the server declined or could not be reached.
|
|
169
175
|
*/
|
|
170
176
|
export async function revokeToken(token) {
|
|
171
177
|
try {
|
package/dist/profiles.js
CHANGED
|
@@ -9,6 +9,10 @@ export function getSecretStorage(profile) {
|
|
|
9
9
|
export function getAuthMethod(profile) {
|
|
10
10
|
return profile.auth === 'oauth' ? 'oauth' : 'token';
|
|
11
11
|
}
|
|
12
|
+
/** ` --profile <name>` to paste into a suggested command; empty for the default profile. */
|
|
13
|
+
export function profileFlag(profileName) {
|
|
14
|
+
return !profileName || profileName === 'default' ? '' : ` --profile ${profileName}`;
|
|
15
|
+
}
|
|
12
16
|
/** True when the profile holds a refreshable OAuth session. */
|
|
13
17
|
export function isRefreshable(profile) {
|
|
14
18
|
return getAuthMethod(profile) === 'oauth' && !!profile.refresh_token;
|
|
@@ -262,19 +266,20 @@ export function setConfigValue(key, value) {
|
|
|
262
266
|
* command silently wrote to the wrong workspace with the wrong token.
|
|
263
267
|
*/
|
|
264
268
|
export function resolveToken(flags) {
|
|
269
|
+
const source = tokenSource(flags);
|
|
265
270
|
// 1. --token flag (highest priority)
|
|
266
|
-
if (
|
|
271
|
+
if (source.kind === 'flag') {
|
|
267
272
|
return { token: flags.token };
|
|
268
273
|
}
|
|
269
274
|
// 2. PACHCA_TOKEN env — unless a profile was named explicitly
|
|
270
|
-
if (
|
|
275
|
+
if (source.kind === 'env') {
|
|
271
276
|
return { token: process.env.PACHCA_TOKEN };
|
|
272
277
|
}
|
|
273
278
|
// 3. --profile flag or PACHCA_PROFILE env
|
|
274
|
-
|
|
275
|
-
if (!profileName) {
|
|
279
|
+
if (source.kind === 'none') {
|
|
276
280
|
throw new TokenNotFoundError();
|
|
277
281
|
}
|
|
282
|
+
const { profileName } = source;
|
|
278
283
|
const profile = getProfile(profileName);
|
|
279
284
|
if (!profile) {
|
|
280
285
|
throw new ProfileNotFoundError(profileName);
|
|
@@ -284,6 +289,19 @@ export function resolveToken(flags) {
|
|
|
284
289
|
}
|
|
285
290
|
return { token: profile.token, profileName, profile };
|
|
286
291
|
}
|
|
292
|
+
export function tokenSource(flags) {
|
|
293
|
+
if (flags.token)
|
|
294
|
+
return { kind: 'flag' };
|
|
295
|
+
if (!flags.profile && process.env.PACHCA_TOKEN)
|
|
296
|
+
return { kind: 'env' };
|
|
297
|
+
if (flags.profile)
|
|
298
|
+
return { kind: 'profile', profileName: flags.profile, via: '--profile' };
|
|
299
|
+
if (process.env.PACHCA_PROFILE) {
|
|
300
|
+
return { kind: 'profile', profileName: process.env.PACHCA_PROFILE, via: 'PACHCA_PROFILE' };
|
|
301
|
+
}
|
|
302
|
+
const active = getActiveProfile();
|
|
303
|
+
return active ? { kind: 'profile', profileName: active, via: 'active_profile' } : { kind: 'none' };
|
|
304
|
+
}
|
|
287
305
|
/**
|
|
288
306
|
* Refresh just the cached scope list from the server's answer.
|
|
289
307
|
*
|
package/dist/token-refresh.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { getConfigDirPath, getProfile, isExpiring, isRefreshable, updateProfileTokens, expiresAtFromNow, } from './profiles.js';
|
|
4
|
-
import { OAuthError, refreshAccessToken } from './oauth.js';
|
|
4
|
+
import { OAuthError, refreshAccessToken, revokeToken } from './oauth.js';
|
|
5
5
|
const LOCK_FILENAME = 'refresh.lock';
|
|
6
6
|
/** A lock older than this belonged to a process that died mid-refresh. */
|
|
7
7
|
const LOCK_STALE_MS = 30_000;
|
|
@@ -65,46 +65,46 @@ function releaseLock(fd) {
|
|
|
65
65
|
* Returns the profile to use. Throws `OAuthError` only when the session is truly
|
|
66
66
|
* dead and the user has to log in again.
|
|
67
67
|
*/
|
|
68
|
-
export async function ensureFreshToken(profileName, profile) {
|
|
69
|
-
if (!isRefreshable(profile) || !
|
|
68
|
+
export async function ensureFreshToken(profileName, profile, needsRefresh = isExpiring) {
|
|
69
|
+
if (!isRefreshable(profile) || !needsRefresh(profile))
|
|
70
70
|
return profile;
|
|
71
71
|
const fd = acquireLock();
|
|
72
72
|
if (fd === null) {
|
|
73
|
-
const waited = await waitForOtherProcess(profileName);
|
|
73
|
+
const waited = await waitForOtherProcess(profileName, needsRefresh);
|
|
74
74
|
if (waited)
|
|
75
75
|
return waited;
|
|
76
76
|
// The holder gave up or is stuck — refresh ourselves rather than fail.
|
|
77
|
-
return refreshAndStore(profileName, profile);
|
|
77
|
+
return refreshAndStore(profileName, profile, needsRefresh);
|
|
78
78
|
}
|
|
79
79
|
try {
|
|
80
80
|
// Re-read: the previous holder may have refreshed while we queued for the lock.
|
|
81
81
|
const current = getProfile(profileName) ?? profile;
|
|
82
|
-
if (!
|
|
82
|
+
if (!needsRefresh(current))
|
|
83
83
|
return current;
|
|
84
|
-
return await refreshAndStore(profileName, current);
|
|
84
|
+
return await refreshAndStore(profileName, current, needsRefresh);
|
|
85
85
|
}
|
|
86
86
|
finally {
|
|
87
87
|
releaseLock(fd);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
async function waitForOtherProcess(profileName) {
|
|
90
|
+
async function waitForOtherProcess(profileName, needsRefresh) {
|
|
91
91
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
92
92
|
while (Date.now() < deadline) {
|
|
93
93
|
await sleep(LOCK_POLL_MS);
|
|
94
94
|
const current = getProfile(profileName);
|
|
95
|
-
if (current && !
|
|
95
|
+
if (current && !needsRefresh(current))
|
|
96
96
|
return current;
|
|
97
97
|
if (!fs.existsSync(lockPath())) {
|
|
98
98
|
// The holder finished between our read of the profile and this check. Look
|
|
99
99
|
// once more before refreshing ourselves: their new token is already on disk,
|
|
100
100
|
// and ours has just been spent.
|
|
101
101
|
const after = getProfile(profileName);
|
|
102
|
-
return after && !
|
|
102
|
+
return after && !needsRefresh(after) ? after : undefined;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
return undefined;
|
|
106
106
|
}
|
|
107
|
-
async function refreshAndStore(profileName, profile) {
|
|
107
|
+
async function refreshAndStore(profileName, profile, needsRefresh) {
|
|
108
108
|
let grant;
|
|
109
109
|
try {
|
|
110
110
|
grant = await refreshAccessToken(profile.refresh_token);
|
|
@@ -116,21 +116,68 @@ async function refreshAndStore(profileName, profile) {
|
|
|
116
116
|
// Without this check a lost race logs the user out for no reason.
|
|
117
117
|
if (error instanceof OAuthError && error.code === 'invalid_grant') {
|
|
118
118
|
const current = getProfile(profileName);
|
|
119
|
-
if (current?.token && !
|
|
119
|
+
if (current?.token && !needsRefresh(current))
|
|
120
120
|
return current;
|
|
121
121
|
}
|
|
122
122
|
throw error;
|
|
123
123
|
}
|
|
124
|
-
const
|
|
124
|
+
const tokens = {
|
|
125
125
|
token: grant.access_token,
|
|
126
126
|
refresh_token: grant.refresh_token,
|
|
127
127
|
expires_at: grant.expires_in ? expiresAtFromNow(grant.expires_in) : undefined,
|
|
128
128
|
scopes: grant.scope ? grant.scope.split(' ').filter(Boolean) : undefined,
|
|
129
|
+
};
|
|
130
|
+
const updated = updateProfileTokens(profileName, tokens);
|
|
131
|
+
// The profile can vanish from the config mid-refresh. The new token is still
|
|
132
|
+
// the one in force: hand it back with its expiry so a caller can revoke it.
|
|
133
|
+
return (updated ?? {
|
|
134
|
+
...profile,
|
|
135
|
+
token: tokens.token,
|
|
136
|
+
refresh_token: tokens.refresh_token ?? profile.refresh_token,
|
|
137
|
+
expires_at: tokens.expires_at ?? profile.expires_at,
|
|
129
138
|
});
|
|
130
|
-
return updated ?? { ...profile, token: grant.access_token };
|
|
131
139
|
}
|
|
132
140
|
/** True when the failure means the session is gone for good, not a hiccup. */
|
|
133
141
|
export function isSessionExpired(error) {
|
|
134
142
|
return (error instanceof OAuthError &&
|
|
135
143
|
['invalid_grant', 'invalid_request', 'invalid_client', 'unauthorized_client'].includes(error.code));
|
|
136
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* True only when the access token will outlive the request about to use it.
|
|
147
|
+
* Unknown expiry counts as not live: revoking is only proof when the token is.
|
|
148
|
+
*/
|
|
149
|
+
function hasLiveToken(profile) {
|
|
150
|
+
if (!profile.token || !profile.expires_at)
|
|
151
|
+
return false;
|
|
152
|
+
if (Number.isNaN(Date.parse(profile.expires_at)))
|
|
153
|
+
return false;
|
|
154
|
+
return !isExpiring(profile);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Close the profile's OAuth login on the server.
|
|
158
|
+
*
|
|
159
|
+
* The revoke endpoint answers `200` even when it skipped the token: an expired
|
|
160
|
+
* access token is not revocable, so the record stays as it was, refresh token
|
|
161
|
+
* included, and that refresh token can still mint new access tokens. Revoking
|
|
162
|
+
* by the refresh token itself is not open to the CLI: self-revocation takes only
|
|
163
|
+
* the access token that also came as the bearer. So the token sent is always a
|
|
164
|
+
* live one — refreshed first when the saved one has run out. The refresh retires
|
|
165
|
+
* the old record on the server, and the revoke then closes the new one.
|
|
166
|
+
*/
|
|
167
|
+
export async function revokeSession(profileName, profile) {
|
|
168
|
+
let current;
|
|
169
|
+
try {
|
|
170
|
+
current = await ensureFreshToken(profileName, profile, (p) => !hasLiveToken(p));
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
// `invalid_grant` on refresh: the token is revoked or gone on the server.
|
|
174
|
+
if (error instanceof OAuthError && error.code === 'invalid_grant')
|
|
175
|
+
return 'closed';
|
|
176
|
+
return 'open';
|
|
177
|
+
}
|
|
178
|
+
// No refresh token and the access token has run out, or it never came back
|
|
179
|
+
// from the OS store: there is nothing to revoke with that would prove anything.
|
|
180
|
+
if (!hasLiveToken(current))
|
|
181
|
+
return 'open';
|
|
182
|
+
return (await revokeToken(current.token)) ? 'revoked' : 'open';
|
|
183
|
+
}
|