@indigoai-us/hq-cli 5.103.21 → 5.103.23
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 +15 -0
- package/dist/commands/auth.d.ts +1 -1
- package/dist/commands/auth.js +2 -2
- package/dist/commands/integrations-api.d.ts +13 -0
- package/dist/commands/integrations-api.js +35 -0
- package/dist/commands/integrations-connect.js +79 -42
- package/dist/commands/integrations-core.d.ts +20 -0
- package/dist/commands/integrations-core.js +59 -5
- package/dist/commands/integrations-manage.d.ts +1 -1
- package/dist/commands/integrations-manage.js +159 -4
- package/dist/commands/integrations.d.ts +2 -0
- package/dist/commands/integrations.js +84 -5
- package/dist/commands/skill.d.ts +44 -142
- package/dist/commands/skill.js +214 -521
- package/dist/main.js +2 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.103.23] — 2026-08-26
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- The bundled sync engine now requires `@indigoai-us/hq-cloud` 6.15.71, so new
|
|
10
|
+
company skills are registered and stamped with a stable skill UID before
|
|
11
|
+
their first upload while comment-only skill proposals remain available from
|
|
12
|
+
the CLI.
|
|
13
|
+
|
|
14
|
+
## [5.103.22] — 2026-08-25
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `hq auth login` now points account signup guidance at the canonical `onboarding.hq.computer` host instead of the retired Indigo onboarding domain.
|
|
19
|
+
|
|
5
20
|
## [5.103.21] — 2026-08-24
|
|
6
21
|
|
|
7
22
|
### Fixed
|
package/dist/commands/auth.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* hq auth status — show whether a valid session is cached + expiry
|
|
9
9
|
*
|
|
10
10
|
* Sign-up is owned by the onboarding web app at
|
|
11
|
-
* https://onboarding.
|
|
11
|
+
* https://onboarding.hq.computer. `hq auth login` signs an existing account
|
|
12
12
|
* into this machine by writing ~/.hq/cognito-tokens.json; once cached, the
|
|
13
13
|
* session is kept valid by `hq auth refresh` / `hq-auth-refresh` and consumed
|
|
14
14
|
* by the deploy + sync skills.
|
package/dist/commands/auth.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* hq auth status — show whether a valid session is cached + expiry
|
|
9
9
|
*
|
|
10
10
|
* Sign-up is owned by the onboarding web app at
|
|
11
|
-
* https://onboarding.
|
|
11
|
+
* https://onboarding.hq.computer. `hq auth login` signs an existing account
|
|
12
12
|
* into this machine by writing ~/.hq/cognito-tokens.json; once cached, the
|
|
13
13
|
* session is kept valid by `hq auth refresh` / `hq-auth-refresh` and consumed
|
|
14
14
|
* by the deploy + sync skills.
|
|
@@ -96,7 +96,7 @@ export function registerAuthCommands(program) {
|
|
|
96
96
|
console.error(chalk.dim(callbackPortCollisionGuidance(DEFAULT_COGNITO.port ?? 8765)));
|
|
97
97
|
}
|
|
98
98
|
else {
|
|
99
|
-
console.error(chalk.dim(" If you do not have an account, sign up at https://onboarding.
|
|
99
|
+
console.error(chalk.dim(" If you do not have an account, sign up at https://onboarding.hq.computer"));
|
|
100
100
|
}
|
|
101
101
|
process.exit(1);
|
|
102
102
|
}
|
|
@@ -156,6 +156,14 @@ export declare function uninstallIntegration(token: string, companyUid: string,
|
|
|
156
156
|
installationId: string;
|
|
157
157
|
connectionId: string;
|
|
158
158
|
}>;
|
|
159
|
+
/**
|
|
160
|
+
* Permanently remove a revoked connection tombstone. The admin purge endpoint
|
|
161
|
+
* deliberately accepts only the connection id: it derives the company and
|
|
162
|
+
* enforces owner authorization from the authenticated connection record.
|
|
163
|
+
*/
|
|
164
|
+
export declare function purgeConnection(token: string, companyUid: string, connectionId: string): Promise<{
|
|
165
|
+
connectionId: string;
|
|
166
|
+
}>;
|
|
159
167
|
export interface OAuthStartResult {
|
|
160
168
|
provider: string;
|
|
161
169
|
displayName: string;
|
|
@@ -197,6 +205,11 @@ export declare function updateGovernance(token: string, companyUid: string, inpu
|
|
|
197
205
|
writePolicy?: WritePolicy;
|
|
198
206
|
writeAllowlist: WriteAllowlistGrant[];
|
|
199
207
|
}>;
|
|
208
|
+
/**
|
|
209
|
+
* Mark or unmark one tool as read-safe. The service owns tool classification:
|
|
210
|
+
* it refuses attempts to mark write/destructive tools read-safe.
|
|
211
|
+
*/
|
|
212
|
+
export declare function setReadSafe(token: string, companyUid: string, connectionId: string, toolName: string, readSafe: boolean): Promise<void>;
|
|
200
213
|
export interface ConnectionAccess {
|
|
201
214
|
connectionId: string;
|
|
202
215
|
provider: string;
|
|
@@ -74,6 +74,27 @@ export async function uninstallIntegration(token, companyUid, installationId) {
|
|
|
74
74
|
await raiseForResponse(res, "Failed to disconnect the app");
|
|
75
75
|
return (await res.json());
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Permanently remove a revoked connection tombstone. The admin purge endpoint
|
|
79
|
+
* deliberately accepts only the connection id: it derives the company and
|
|
80
|
+
* enforces owner authorization from the authenticated connection record.
|
|
81
|
+
*/
|
|
82
|
+
export async function purgeConnection(token, companyUid, connectionId) {
|
|
83
|
+
// Keep the company in this client's call signature with the other
|
|
84
|
+
// connection mutations. It is resolved before the target connection so a
|
|
85
|
+
// caller cannot use a slug from a different company, but the server's purge
|
|
86
|
+
// contract intentionally takes only connectionId in its body.
|
|
87
|
+
void companyUid;
|
|
88
|
+
const res = await vaultApiFetch({
|
|
89
|
+
token,
|
|
90
|
+
path: "/v1/integrations/admin/purge",
|
|
91
|
+
method: "POST",
|
|
92
|
+
body: { connectionId },
|
|
93
|
+
});
|
|
94
|
+
if (!res.ok)
|
|
95
|
+
await raiseForResponse(res, "Failed to purge the revoked connection");
|
|
96
|
+
return (await res.json());
|
|
97
|
+
}
|
|
77
98
|
export async function startOAuth(token, companyUid, input) {
|
|
78
99
|
const res = await vaultApiFetch({
|
|
79
100
|
token,
|
|
@@ -110,6 +131,20 @@ export async function updateGovernance(token, companyUid, input) {
|
|
|
110
131
|
await raiseForResponse(res, "Failed to update the app's settings");
|
|
111
132
|
return (await res.json());
|
|
112
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Mark or unmark one tool as read-safe. The service owns tool classification:
|
|
136
|
+
* it refuses attempts to mark write/destructive tools read-safe.
|
|
137
|
+
*/
|
|
138
|
+
export async function setReadSafe(token, companyUid, connectionId, toolName, readSafe) {
|
|
139
|
+
const res = await vaultApiFetch({
|
|
140
|
+
token,
|
|
141
|
+
path: "/v1/integrations/admin/read-safe",
|
|
142
|
+
method: "POST",
|
|
143
|
+
body: { companyUid, connectionId, toolName, readSafe },
|
|
144
|
+
});
|
|
145
|
+
if (!res.ok)
|
|
146
|
+
await raiseForResponse(res, "Failed to update the tool's read-safe setting");
|
|
147
|
+
}
|
|
113
148
|
/**
|
|
114
149
|
* Live open-approval list, read from hq-pro's confirm-queue state (not the
|
|
115
150
|
* audit feed). Unlike the audit-derived reconstruction this replaces, a call
|
|
@@ -20,7 +20,7 @@ import chalk from "chalk";
|
|
|
20
20
|
import open from "open";
|
|
21
21
|
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
22
22
|
import { getCompanyUid } from "../utils/vault-api.js";
|
|
23
|
-
import { IntegrationsCliError, bareProvider, printJson, resolveConnection, } from "./integrations-core.js";
|
|
23
|
+
import { IntegrationsCliError, bareProvider, connectionDomain, printJson, revokedConnectionDetails, resolveConnection, } from "./integrations-core.js";
|
|
24
24
|
import { completeOAuth, discoverDocs, installIntegration, listCatalog, pullBlueprint, startOAuth, } from "./integrations-api.js";
|
|
25
25
|
import { startLoopbackListener } from "./integrations-oauth.js";
|
|
26
26
|
/** hq-pro's machine code for "this endpoint needs a browser sign-in". */
|
|
@@ -183,7 +183,7 @@ async function resolveKey(opts, appLabel) {
|
|
|
183
183
|
* most-specific-first so an explicit flag always wins over the positional
|
|
184
184
|
* argument's heuristics.
|
|
185
185
|
*/
|
|
186
|
-
async function resolveTarget(token, companyUid, app, opts) {
|
|
186
|
+
async function resolveTarget(token, companyUid, app, opts, preserveDomain = false) {
|
|
187
187
|
if (opts.docsUrl) {
|
|
188
188
|
const found = await discoverDocs(token, companyUid, opts.docsUrl);
|
|
189
189
|
if (!found.discovery || !found.discoveryReceiptId) {
|
|
@@ -222,6 +222,17 @@ async function resolveTarget(token, companyUid, app, opts) {
|
|
|
222
222
|
// authClass, both of which make the connect cleaner than a raw domain
|
|
223
223
|
// lookup. Missing it is fine — the domain path still works.
|
|
224
224
|
const match = await findCatalogEntry(token, companyUid, app);
|
|
225
|
+
if (preserveDomain) {
|
|
226
|
+
// Reviving a revoked connection is keyed by its canonical domain in
|
|
227
|
+
// hq-pro. A catalog entry id is useful auth metadata, but replacing the
|
|
228
|
+
// domain with it can create a distinct connection instead of reviving
|
|
229
|
+
// the original acct_ row.
|
|
230
|
+
return {
|
|
231
|
+
ref: { domain: app },
|
|
232
|
+
...(match?.authClass ? { authClass: match.authClass } : {}),
|
|
233
|
+
label: match?.name || app,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
225
236
|
return catalogEntryToTarget(match, { ref: { domain: app }, label: app });
|
|
226
237
|
}
|
|
227
238
|
// A bare name (`notion`, `atlassian`) is how the catalog reads to a person —
|
|
@@ -424,14 +435,15 @@ async function completeCredentialIfNeeded(token, companyUid, target, opts, resul
|
|
|
424
435
|
});
|
|
425
436
|
}
|
|
426
437
|
/** Print the outcome of a successful connect. */
|
|
427
|
-
function reportInstall(result, opts) {
|
|
438
|
+
function reportInstall(result, opts, expectedRevivedConnectionId) {
|
|
428
439
|
if (opts.json) {
|
|
429
440
|
printJson(result);
|
|
430
441
|
return;
|
|
431
442
|
}
|
|
432
443
|
const { installation, connection } = result;
|
|
433
444
|
const toolCount = result.mcp?.tools?.length;
|
|
434
|
-
|
|
445
|
+
const revived = connection.id === expectedRevivedConnectionId;
|
|
446
|
+
console.log(chalk.green(`${revived ? "Revived" : "Connected"} ${chalk.bold(installation.displayName)}`) +
|
|
435
447
|
(typeof toolCount === "number" ? chalk.dim(` — ${toolCount} tools available`) : ""));
|
|
436
448
|
console.log(chalk.dim(` connection: ${connection.id}`));
|
|
437
449
|
if (installation.status === "needs_credentials") {
|
|
@@ -439,6 +451,49 @@ function reportInstall(result, opts) {
|
|
|
439
451
|
}
|
|
440
452
|
console.log(chalk.dim(` Try it: hq integrations tools --provider ${bareProvider(connection.provider)}`));
|
|
441
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* The shared `connect <domain>` execution path. Reconnect's revoked fallback
|
|
456
|
+
* intentionally comes through here rather than re-installing its saved MCP
|
|
457
|
+
* URL: hq-pro recognizes the domain and revives the revoked row in place.
|
|
458
|
+
*/
|
|
459
|
+
async function connectApp(token, companyUid, app, opts, expectedRevivedConnectionId) {
|
|
460
|
+
const target = await resolveTarget(token, companyUid, app, opts, expectedRevivedConnectionId !== undefined);
|
|
461
|
+
const authMode = opts.auth ?? target.authClass;
|
|
462
|
+
if (authMode === "oauth") {
|
|
463
|
+
const result = await connectViaOAuth(token, companyUid, target, opts);
|
|
464
|
+
if (result)
|
|
465
|
+
reportInstall(result, opts, expectedRevivedConnectionId);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
// A key is only collected when something already says one is needed, or
|
|
469
|
+
// the caller supplied one — otherwise a no-auth app would pointlessly
|
|
470
|
+
// prompt.
|
|
471
|
+
const wantsKey = authMode === "key" || Boolean(opts.token || opts.tokenStdin);
|
|
472
|
+
const bearerToken = wantsKey ? await resolveKey(opts, target.label) : undefined;
|
|
473
|
+
try {
|
|
474
|
+
const result = await installIntegration(token, companyUid, {
|
|
475
|
+
...target.ref,
|
|
476
|
+
...(bearerToken
|
|
477
|
+
? { authMode: "bearer", bearerToken }
|
|
478
|
+
: authMode === "none"
|
|
479
|
+
? { authMode: "none" }
|
|
480
|
+
: {}),
|
|
481
|
+
});
|
|
482
|
+
reportInstall(await completeCredentialIfNeeded(token, companyUid, target, opts, result), opts, expectedRevivedConnectionId);
|
|
483
|
+
}
|
|
484
|
+
catch (err) {
|
|
485
|
+
// Server-authoritative detection: the endpoint turned out to be
|
|
486
|
+
// OAuth-protected, so run the browser flow instead of making the
|
|
487
|
+
// caller re-issue the command with --auth oauth.
|
|
488
|
+
if (isOAuthRequiredError(err)) {
|
|
489
|
+
const result = await connectViaOAuth(token, companyUid, target, opts);
|
|
490
|
+
if (result)
|
|
491
|
+
reportInstall(result, opts, expectedRevivedConnectionId);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
throw err;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
442
497
|
export function registerConnectCommands(integrations) {
|
|
443
498
|
integrations
|
|
444
499
|
.command("catalog [query]")
|
|
@@ -561,46 +616,11 @@ export function registerConnectCommands(integrations) {
|
|
|
561
616
|
assertAuthMode(opts.auth);
|
|
562
617
|
const token = await ensureCognitoIdToken();
|
|
563
618
|
const companyUid = await getCompanyUid(token, opts.company);
|
|
564
|
-
|
|
565
|
-
const authMode = opts.auth ?? target.authClass;
|
|
566
|
-
if (authMode === "oauth") {
|
|
567
|
-
const result = await connectViaOAuth(token, companyUid, target, opts);
|
|
568
|
-
if (result)
|
|
569
|
-
reportInstall(result, opts);
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
// A key is only collected when something already says one is needed, or
|
|
573
|
-
// the caller supplied one — otherwise a no-auth app would pointlessly
|
|
574
|
-
// prompt.
|
|
575
|
-
const wantsKey = authMode === "key" || Boolean(opts.token || opts.tokenStdin);
|
|
576
|
-
const bearerToken = wantsKey ? await resolveKey(opts, target.label) : undefined;
|
|
577
|
-
try {
|
|
578
|
-
const result = await installIntegration(token, companyUid, {
|
|
579
|
-
...target.ref,
|
|
580
|
-
...(bearerToken
|
|
581
|
-
? { authMode: "bearer", bearerToken }
|
|
582
|
-
: authMode === "none"
|
|
583
|
-
? { authMode: "none" }
|
|
584
|
-
: {}),
|
|
585
|
-
});
|
|
586
|
-
reportInstall(await completeCredentialIfNeeded(token, companyUid, target, opts, result), opts);
|
|
587
|
-
}
|
|
588
|
-
catch (err) {
|
|
589
|
-
// Server-authoritative detection: the endpoint turned out to be
|
|
590
|
-
// OAuth-protected, so run the browser flow instead of making the
|
|
591
|
-
// caller re-issue the command with --auth oauth.
|
|
592
|
-
if (isOAuthRequiredError(err)) {
|
|
593
|
-
const result = await connectViaOAuth(token, companyUid, target, opts);
|
|
594
|
-
if (result)
|
|
595
|
-
reportInstall(result, opts);
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
throw err;
|
|
599
|
-
}
|
|
619
|
+
await connectApp(token, companyUid, app, opts);
|
|
600
620
|
});
|
|
601
621
|
integrations
|
|
602
622
|
.command("reconnect [app]")
|
|
603
|
-
.description("Re-authenticate a connected app
|
|
623
|
+
.description("Re-authenticate a connected app; use --connect to re-add a revoked app")
|
|
604
624
|
.option("--company <slug>", "Company slug, e.g. indigo")
|
|
605
625
|
.option("--provider <slug>", "Connected app (e.g. linear)")
|
|
606
626
|
.option("--connection <id>", "Connection id (acct_…)")
|
|
@@ -609,6 +629,7 @@ export function registerConnectCommands(integrations) {
|
|
|
609
629
|
.option("--auth <mode>", "Force the auth mode: none, key, or oauth (default: detect)")
|
|
610
630
|
.option("--no-browser", "Print the sign-in URL instead of opening a browser")
|
|
611
631
|
.option("--timeout <seconds>", "How long to wait for a browser sign-in (default 300)")
|
|
632
|
+
.option("--connect", "For a revoked row, run `connect <domain>` to re-add and revive it")
|
|
612
633
|
.option("--json", "Machine-readable output")
|
|
613
634
|
.action(async (app, opts) => {
|
|
614
635
|
// Same validation as `connect`. Without it a typo like `--auth oauth2`
|
|
@@ -618,7 +639,23 @@ export function registerConnectCommands(integrations) {
|
|
|
618
639
|
assertAuthMode(opts.auth);
|
|
619
640
|
const token = await ensureCognitoIdToken();
|
|
620
641
|
const companyUid = await getCompanyUid(token, opts.company);
|
|
621
|
-
const connection = await resolveConnection(token, companyUid, app, opts
|
|
642
|
+
const connection = await resolveConnection(token, companyUid, app, opts, {
|
|
643
|
+
allowSingleRevoked: Boolean(opts.connect),
|
|
644
|
+
});
|
|
645
|
+
if (connection.status === "revoked") {
|
|
646
|
+
const details = revokedConnectionDetails(connection, opts.company);
|
|
647
|
+
if (!opts.connect) {
|
|
648
|
+
if (opts.json)
|
|
649
|
+
printJson(details);
|
|
650
|
+
else {
|
|
651
|
+
console.log(chalk.yellow(details.reason));
|
|
652
|
+
console.log(chalk.yellow(`Re-add it with: ${details.fixPath}`));
|
|
653
|
+
}
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
await connectApp(token, companyUid, connectionDomain(connection), opts, connection.id);
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
622
659
|
const url = connection.installation?.surface?.url;
|
|
623
660
|
if (!url) {
|
|
624
661
|
throw new IntegrationsCliError(`${bareProvider(connection.provider)} was not installed through the app catalog, so it cannot be reconnected from here.`, { expected: true });
|
|
@@ -74,6 +74,16 @@ export interface AdminConnection {
|
|
|
74
74
|
};
|
|
75
75
|
installation?: FactoryInstallation | null;
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The actionable state returned instead of attempting to use a revoked
|
|
79
|
+
* connection. Keep this machine-readable so commands and scripts get the same
|
|
80
|
+
* recovery path instead of treating a listed row as absent.
|
|
81
|
+
*/
|
|
82
|
+
export interface RevokedConnectionDetails {
|
|
83
|
+
status: "revoked";
|
|
84
|
+
reason: string;
|
|
85
|
+
fixPath: string;
|
|
86
|
+
}
|
|
77
87
|
export interface AdminAuditEvent {
|
|
78
88
|
timestamp: string;
|
|
79
89
|
memberOrAgent: string;
|
|
@@ -210,6 +220,14 @@ export declare function selectConnection(connections: AdminConnection[], opts: {
|
|
|
210
220
|
connection?: string;
|
|
211
221
|
provider?: string;
|
|
212
222
|
}): AdminConnection;
|
|
223
|
+
/**
|
|
224
|
+
* The hostname re-add needs. Prefer the server's canonical installation domain;
|
|
225
|
+
* an older row may only retain its MCP URL, and provider is the last-resort
|
|
226
|
+
* human-safe query when neither was stored.
|
|
227
|
+
*/
|
|
228
|
+
export declare function connectionDomain(connection: AdminConnection): string;
|
|
229
|
+
/** A revoked row is still addressable, but it cannot make a live MCP call. */
|
|
230
|
+
export declare function revokedConnectionDetails(connection: AdminConnection, companySlug?: string): RevokedConnectionDetails;
|
|
213
231
|
/**
|
|
214
232
|
* Resolve a connection the caller named positionally OR through the
|
|
215
233
|
* `--provider` / `--connection` flags. Every management verb takes an optional
|
|
@@ -219,6 +237,8 @@ export declare function selectConnection(connections: AdminConnection[], opts: {
|
|
|
219
237
|
export declare function resolveConnection(token: string, companyUid: string, app: string | undefined, opts: {
|
|
220
238
|
provider?: string;
|
|
221
239
|
connection?: string;
|
|
240
|
+
}, recoveryOpts?: {
|
|
241
|
+
allowSingleRevoked?: boolean;
|
|
222
242
|
}): Promise<AdminConnection>;
|
|
223
243
|
export declare function callGateway(token: string, params: Record<string, unknown>): Promise<GatewayMessage>;
|
|
224
244
|
/**
|
|
@@ -256,14 +256,26 @@ export function selectConnection(connections, opts) {
|
|
|
256
256
|
if (opts.provider) {
|
|
257
257
|
const want = opts.provider.trim().toLowerCase();
|
|
258
258
|
const wantHumanSlug = humanSlug(opts.provider);
|
|
259
|
-
|
|
259
|
+
// A reconnect normally leaves a revoked historical row alongside its new
|
|
260
|
+
// active connection. Prefer the active inventory before applying the
|
|
261
|
+
// provider/name/alias precedence below; fall back to history only when no
|
|
262
|
+
// active row matches this selector at all. `--connection` above remains
|
|
263
|
+
// the explicit way to inspect or act on a particular historical row.
|
|
264
|
+
const activeMatches = connections.filter((c) => c.status !== "revoked" &&
|
|
265
|
+
(bareProvider(c.provider).toLowerCase() === want ||
|
|
266
|
+
c.provider.toLowerCase() === want ||
|
|
267
|
+
c.installation?.displayName?.trim().toLowerCase() === want ||
|
|
268
|
+
(wantHumanSlug !== "" &&
|
|
269
|
+
humanSlug(c.installation?.displayName ?? "") === wantHumanSlug)));
|
|
270
|
+
const candidates = activeMatches.length > 0 ? activeMatches : connections;
|
|
271
|
+
const providerMatch = candidates.find((c) => bareProvider(c.provider).toLowerCase() === want || c.provider.toLowerCase() === want);
|
|
260
272
|
if (providerMatch)
|
|
261
273
|
return providerMatch;
|
|
262
|
-
const displayNameMatch =
|
|
274
|
+
const displayNameMatch = candidates.find((c) => c.installation?.displayName?.trim().toLowerCase() === want);
|
|
263
275
|
if (displayNameMatch)
|
|
264
276
|
return displayNameMatch;
|
|
265
277
|
const aliasMatches = wantHumanSlug
|
|
266
|
-
?
|
|
278
|
+
? candidates.filter((c) => {
|
|
267
279
|
const displayName = c.installation?.displayName;
|
|
268
280
|
const displayNameSlug = displayName ? humanSlug(displayName) : "";
|
|
269
281
|
return displayNameSlug !== "" && displayNameSlug === wantHumanSlug;
|
|
@@ -274,7 +286,7 @@ export function selectConnection(connections, opts) {
|
|
|
274
286
|
if (aliasMatches.length > 1) {
|
|
275
287
|
throw new IntegrationsCliError(`Display-name alias '${opts.provider}' matches multiple connected apps. Use --connection to choose one.`, { expected: true });
|
|
276
288
|
}
|
|
277
|
-
const available =
|
|
289
|
+
const available = connections.map((c) => bareProvider(c.provider)).join(", ");
|
|
278
290
|
throw new IntegrationsCliError(`No connected app matches '${opts.provider}'.` +
|
|
279
291
|
(available ? ` Connected: ${available}.` : " Nothing is connected yet — connect apps with `hq integrations connect <app>`."), { expected: true });
|
|
280
292
|
}
|
|
@@ -286,17 +298,59 @@ export function selectConnection(connections, opts) {
|
|
|
286
298
|
throw new IntegrationsCliError(`Multiple apps are connected — pick one with --provider:\n` +
|
|
287
299
|
active.map((c) => ` --provider ${bareProvider(c.provider)}`).join("\n"), { expected: true });
|
|
288
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* The hostname re-add needs. Prefer the server's canonical installation domain;
|
|
303
|
+
* an older row may only retain its MCP URL, and provider is the last-resort
|
|
304
|
+
* human-safe query when neither was stored.
|
|
305
|
+
*/
|
|
306
|
+
export function connectionDomain(connection) {
|
|
307
|
+
const domain = connection.installation?.domain?.trim();
|
|
308
|
+
if (domain)
|
|
309
|
+
return domain;
|
|
310
|
+
const url = connection.installation?.surface?.url;
|
|
311
|
+
if (url) {
|
|
312
|
+
try {
|
|
313
|
+
const host = new URL(url).hostname;
|
|
314
|
+
if (host)
|
|
315
|
+
return host;
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// The saved endpoint is advisory here. A malformed legacy URL must not
|
|
319
|
+
// prevent recovery when the provider slug can still be re-added.
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return bareProvider(connection.provider);
|
|
323
|
+
}
|
|
324
|
+
/** A revoked row is still addressable, but it cannot make a live MCP call. */
|
|
325
|
+
export function revokedConnectionDetails(connection, companySlug) {
|
|
326
|
+
const domain = connectionDomain(connection);
|
|
327
|
+
return {
|
|
328
|
+
status: "revoked",
|
|
329
|
+
reason: "This connection was revoked and cannot be used until it is re-added.",
|
|
330
|
+
fixPath: `hq integrations connect ${domain}` +
|
|
331
|
+
(companySlug ? ` --company ${companySlug}` : ""),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
289
334
|
/**
|
|
290
335
|
* Resolve a connection the caller named positionally OR through the
|
|
291
336
|
* `--provider` / `--connection` flags. Every management verb takes an optional
|
|
292
337
|
* `<app>` argument for ergonomics (`hq integrations policy linear …`), which is
|
|
293
338
|
* matched exactly like `--provider` unless it looks like a connection id.
|
|
294
339
|
*/
|
|
295
|
-
export async function resolveConnection(token, companyUid, app, opts) {
|
|
340
|
+
export async function resolveConnection(token, companyUid, app, opts, recoveryOpts = {}) {
|
|
296
341
|
const connections = await fetchConnections(token, companyUid);
|
|
297
342
|
if (app && !opts.provider && !opts.connection) {
|
|
298
343
|
return selectConnection(connections, app.startsWith("acct_") ? { connection: app } : { provider: app });
|
|
299
344
|
}
|
|
345
|
+
// Normal management verbs deliberately ignore revoked rows for implicit
|
|
346
|
+
// selection. Reconnect's explicit --connect recovery is the one exception:
|
|
347
|
+
// a sole revoked row is unambiguous and needs its saved domain to revive.
|
|
348
|
+
if (recoveryOpts.allowSingleRevoked &&
|
|
349
|
+
!opts.provider &&
|
|
350
|
+
!opts.connection &&
|
|
351
|
+
connections.length === 1) {
|
|
352
|
+
return connections[0];
|
|
353
|
+
}
|
|
300
354
|
return selectConnection(connections, opts);
|
|
301
355
|
}
|
|
302
356
|
export async function callGateway(token, params) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `hq integrations show | policy | grants | grant | ungrant | access | share |
|
|
3
|
-
* unshare | audit | pending | disconnect`.
|
|
3
|
+
* unshare | audit | pending | disconnect | purge`.
|
|
4
4
|
*
|
|
5
5
|
* The govern-and-remove half of the lifecycle. Two different permission
|
|
6
6
|
* surfaces live here and are easy to confuse, so they get separate verbs:
|