@indigoai-us/hq-cli 5.345.134 → 5.345.136
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 +48 -0
- package/assets/scaffold/core/scripts/worktree-gc.sh +98 -8
- package/dist/command-catalog.generated.d.ts +62 -6
- package/dist/command-catalog.generated.js +79 -7
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/core-bucket-c-handoff-knowledge-native.d.ts +3 -1
- package/dist/commands/core-bucket-c-handoff-knowledge-native.js +8 -4
- package/dist/commands/core-checkpoint-native.d.ts +10 -0
- package/dist/commands/core-checkpoint-native.js +60 -15
- package/dist/commands/core-handoff.d.ts +2 -0
- package/dist/commands/core-handoff.js +3 -1
- package/dist/commands/core-pipeline-workflow-runner-native.js +7 -5
- package/dist/commands/daemon.js +3 -1
- package/dist/commands/fingerprint.d.ts +10 -0
- package/dist/commands/fingerprint.js +191 -0
- package/dist/commands/integrations-core.d.ts +22 -0
- package/dist/commands/integrations-core.js +25 -0
- package/dist/commands/integrations-manage.d.ts +24 -1
- package/dist/commands/integrations-manage.js +92 -17
- package/dist/commands/integrations.d.ts +11 -0
- package/dist/commands/integrations.js +100 -19
- package/dist/commands/lanes-loop.d.ts +9 -0
- package/dist/commands/lanes-loop.js +9 -1
- package/dist/commands/lanes.js +78 -55
- package/dist/commands/meetings.d.ts +2 -0
- package/dist/commands/meetings.js +57 -14
- package/dist/commands/mesh.d.ts +19 -1
- package/dist/commands/mesh.js +158 -70
- package/dist/commands/reindex.js +80 -5
- package/dist/commands/storage-offload.d.ts +93 -28
- package/dist/commands/storage-offload.js +290 -34
- package/dist/commands/storage.js +60 -7
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2550 -1748
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/index.js +4 -0
- package/dist/lib/capacity/route.js +24 -0
- package/dist/lib/core-utils/session-company.d.ts +11 -0
- package/dist/lib/core-utils/session-company.js +34 -0
- package/dist/lib/core-utils/worktree-resume.js +44 -4
- package/dist/lib/core-utils/worktree-session-owner.d.ts +27 -0
- package/dist/lib/core-utils/worktree-session-owner.js +157 -0
- package/dist/lib/daemon/catalog.d.ts +2 -0
- package/dist/lib/daemon/catalog.js +3 -3
- package/dist/lib/daemon/hq-child-bootstrap-stage.d.ts +5 -0
- package/dist/lib/daemon/hq-child-bootstrap-stage.js +3 -11
- package/dist/lib/daemon/server.js +10 -1
- package/dist/lib/daemon/supervisor.js +6 -0
- package/dist/lib/daemon/sync-runner-bundle.d.ts +5 -0
- package/dist/lib/daemon/sync-runner-bundle.js +4 -12
- package/dist/lib/doctor/checks/claude-wiring.js +2 -1
- package/dist/lib/doctor/fix/apply.d.ts +20 -0
- package/dist/lib/doctor/fix/apply.js +63 -3
- package/dist/lib/doctor/fix/remediation.d.ts +1 -1
- package/dist/lib/doctor/fix/remediation.js +16 -0
- package/dist/lib/doctor/shared.d.ts +11 -1
- package/dist/lib/doctor/shared.js +16 -2
- package/dist/lib/fingerprint/fixtures.d.ts +19 -0
- package/dist/lib/fingerprint/fixtures.js +61 -0
- package/dist/lib/fingerprint/format.d.ts +50 -0
- package/dist/lib/fingerprint/format.js +105 -0
- package/dist/lib/fingerprint/offline.d.ts +16 -0
- package/dist/lib/fingerprint/offline.js +44 -0
- package/dist/lib/fingerprint/registry-client.d.ts +74 -0
- package/dist/lib/fingerprint/registry-client.js +141 -0
- package/dist/lib/fingerprint/resolve-local.d.ts +14 -0
- package/dist/lib/fingerprint/resolve-local.js +67 -0
- package/dist/lib/fingerprint/self-core.d.ts +139 -0
- package/dist/lib/fingerprint/self-core.js +371 -0
- package/dist/lib/fingerprint/self.d.ts +44 -0
- package/dist/lib/fingerprint/self.js +191 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/monitor.js +14 -2
- package/dist/lib/lanes/spawn.d.ts +19 -1
- package/dist/lib/lanes/spawn.js +50 -6
- package/dist/lib/lanes/story-ledger.d.ts +1 -0
- package/dist/lib/lanes/story-ledger.js +3 -1
- package/dist/lib/lanes/types.d.ts +12 -0
- package/dist/lib/lanes/types.js +12 -1
- package/dist/lib/mesh/live/daemon/install.d.ts +12 -0
- package/dist/lib/mesh/live/daemon/install.js +22 -0
- package/dist/lib/mesh/live/flush.js +106 -3
- package/dist/lib/mesh/live/session-events-client.d.ts +9 -0
- package/dist/lib/mesh/live/session-events-client.js +15 -0
- package/dist/lib/work-context/state.d.ts +10 -0
- package/dist/lib/work-context/state.js +5 -0
- package/dist/utils/cli-network-lifecycle.d.ts +2 -0
- package/dist/utils/cli-network-lifecycle.js +5 -1
- package/dist/utils/cognito-session.d.ts +2 -21
- package/dist/utils/cognito-session.js +3 -84
- package/dist/utils/fsync-directory.d.ts +17 -0
- package/dist/utils/fsync-directory.js +27 -0
- package/dist/utils/person-token-cache.d.ts +32 -0
- package/dist/utils/person-token-cache.js +95 -0
- package/dist/utils/vault-api.d.ts +21 -1
- package/dist/utils/vault-api.js +54 -5
- package/dist/utils/windows-hide-default.d.ts +42 -0
- package/dist/utils/windows-hide-default.js +98 -0
- package/package.json +2 -2
|
@@ -25,13 +25,14 @@ import * as os from "os";
|
|
|
25
25
|
import * as path from "path";
|
|
26
26
|
import * as yaml from "js-yaml";
|
|
27
27
|
import chalk from "chalk";
|
|
28
|
-
import {
|
|
28
|
+
import { isExpiring, refreshTokens, saveCachedTokens, browserLogin, detectHqCoreVersion, isMachineIdentity, getValidMachineTokens, CognitoRefreshError, accessTokenFingerprint, invalidateCachedTokensByFingerprint, } from "@indigoai-us/hq-cloud";
|
|
29
29
|
import { CLI_NAME, CLI_VERSION } from "../cli-version.js";
|
|
30
30
|
import { daemonDir } from "../lib/mesh/live/daemon/paths.js";
|
|
31
31
|
import { workMeshRoot } from "../lib/mesh/live/paths.js";
|
|
32
32
|
import { acquireLockFileExclusively } from "./exclusive-lock.js";
|
|
33
33
|
import { reportNoninteractivePersonRefreshRejection } from "./cognito-refresh-sentry.js";
|
|
34
|
-
import {
|
|
34
|
+
import { decodeJwtPayload, loadCachedTokens, personTokenCacheFile, readPersonTokenFile, readTokenFile, } from "./person-token-cache.js";
|
|
35
|
+
export { loadCachedTokens, personTokenCacheFile, readPersonTokenFile };
|
|
35
36
|
// Re-export the cached-session primitives for callers that must deliberately
|
|
36
37
|
// avoid the refresh/login behavior in the higher-level helpers below.
|
|
37
38
|
export { isExpiring, isMachineIdentity };
|
|
@@ -294,17 +295,6 @@ function withAlignedMachineCredsEnv(env, fn) {
|
|
|
294
295
|
}
|
|
295
296
|
}
|
|
296
297
|
}
|
|
297
|
-
/**
|
|
298
|
-
* On-disk person Cognito cache path for `env` (`HQ_STATE_DIR` or `~/.hq`).
|
|
299
|
-
* Deliberately does not use hq-cloud's `loadCachedTokens()`: after a machine
|
|
300
|
-
* mint in this process, that helper returns in-memory machine tokens when the
|
|
301
|
-
* person file is absent, which would falsely block the fresh-box fallback.
|
|
302
|
-
*/
|
|
303
|
-
export function personTokenCacheFile(home = os.homedir(), env = process.env) {
|
|
304
|
-
const override = env.HQ_STATE_DIR?.trim();
|
|
305
|
-
const stateDir = override && override.length > 0 ? override : path.join(home, ".hq");
|
|
306
|
-
return path.join(stateDir, "cognito-tokens.json");
|
|
307
|
-
}
|
|
308
298
|
/**
|
|
309
299
|
* True when a usable person Cognito cache file exists under the effective
|
|
310
300
|
* `HQ_STATE_DIR` (or `~/.hq`). Presence alone keeps human commands on the
|
|
@@ -396,23 +386,6 @@ export function wantsMachineCognitoTokens(options = {}, env = process.env) {
|
|
|
396
386
|
export function resolveCognitoTokenSource(env = process.env) {
|
|
397
387
|
return wantsMachineCognitoTokens({}, env) ? "machine" : "person";
|
|
398
388
|
}
|
|
399
|
-
function decodeJwtPayload(token) {
|
|
400
|
-
if (!token)
|
|
401
|
-
return null;
|
|
402
|
-
const parts = token.split(".");
|
|
403
|
-
if (parts.length < 2 || !parts[1])
|
|
404
|
-
return null;
|
|
405
|
-
try {
|
|
406
|
-
const json = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
407
|
-
const parsed = JSON.parse(json);
|
|
408
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
409
|
-
return null;
|
|
410
|
-
return parsed;
|
|
411
|
-
}
|
|
412
|
-
catch {
|
|
413
|
-
return null;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
389
|
/**
|
|
417
390
|
* Refresh wrote a different Cognito subject than the active cache.
|
|
418
391
|
* Callers must not fall through to browser login or machine mint — that
|
|
@@ -460,60 +433,6 @@ export function formatCognitoSubject(tokens) {
|
|
|
460
433
|
const fromAccess = cognitoSubjectFromToken(tokens.accessToken);
|
|
461
434
|
return `sub=${fromId.sub ?? fromAccess.sub ?? "-"} entityUid=${fromId.entityUid ?? fromAccess.entityUid ?? "-"}`;
|
|
462
435
|
}
|
|
463
|
-
function readTokenFile(file) {
|
|
464
|
-
try {
|
|
465
|
-
if (!fs.existsSync(file))
|
|
466
|
-
return null;
|
|
467
|
-
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
468
|
-
if (typeof raw.accessToken !== "string" || typeof raw.idToken !== "string")
|
|
469
|
-
return null;
|
|
470
|
-
return raw;
|
|
471
|
-
}
|
|
472
|
-
catch {
|
|
473
|
-
return null;
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
/**
|
|
477
|
-
* The person session from the shared macOS Keychain item (see
|
|
478
|
-
* ./token-keychain.ts), or null. Not read when the token file is missing:
|
|
479
|
-
* no file means signed out. Machine-identity and invalidated tokens are
|
|
480
|
-
* refused, the same as the file loader.
|
|
481
|
-
*/
|
|
482
|
-
function personKeychainTokens(file, home, env, keychain = {}) {
|
|
483
|
-
if (!fs.existsSync(file))
|
|
484
|
-
return null;
|
|
485
|
-
const tokens = readKeychainTokens({ env, home, ...keychain });
|
|
486
|
-
if (!tokens)
|
|
487
|
-
return null;
|
|
488
|
-
const claims = decodeJwtPayload(tokens.idToken);
|
|
489
|
-
const entityType = claims?.["custom:entityType"];
|
|
490
|
-
if (typeof entityType === "string" && entityType.trim().length > 0)
|
|
491
|
-
return null;
|
|
492
|
-
if (fs.existsSync(`${file}.invalid.${accessTokenFingerprint(tokens.accessToken)}`))
|
|
493
|
-
return null;
|
|
494
|
-
return tokens;
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* Person session for `env`: the Keychain item and the token file, the newer
|
|
498
|
-
* by `expiresAt` (see ./token-keychain.ts). Does not consult hq-cloud's
|
|
499
|
-
* in-memory fallback, which can surface a different session when the file
|
|
500
|
-
* is missing or unreadable.
|
|
501
|
-
*/
|
|
502
|
-
export function readPersonTokenFile(home = os.homedir(), env = process.env, keychain = {}) {
|
|
503
|
-
const file = personTokenCacheFile(home, env);
|
|
504
|
-
return pickNewerSession(personKeychainTokens(file, home, env, keychain), readTokenFile(file));
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* The person's cached session: the shared Keychain item and hq-cloud's file
|
|
508
|
-
* loader, the newer by `expiresAt`. Writes still go only to the file in this
|
|
509
|
-
* step.
|
|
510
|
-
*/
|
|
511
|
-
export function loadCachedTokens(env = process.env, keychain = {}) {
|
|
512
|
-
const home = os.homedir();
|
|
513
|
-
const file = personTokenCacheFile(home, env);
|
|
514
|
-
const fromFile = loadCachedTokensFromFile();
|
|
515
|
-
return pickNewerSession(personKeychainTokens(file, home, env, keychain), fromFile);
|
|
516
|
-
}
|
|
517
436
|
function restoreTokenFile(file, before) {
|
|
518
437
|
if (before) {
|
|
519
438
|
const dir = path.dirname(file);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Filesystem seam for {@link fsyncDirectory} tests. */
|
|
2
|
+
export interface DirectorySyncIo {
|
|
3
|
+
openSync: (file: string, flags: number) => number;
|
|
4
|
+
fsyncSync: (fd: number) => void;
|
|
5
|
+
closeSync: (fd: number) => void;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Flush a directory entry (a rename or a new child) to disk.
|
|
9
|
+
*
|
|
10
|
+
* Windows has no directory fsync: FlushFileBuffers on a directory handle
|
|
11
|
+
* fails, and Node reports `EPERM: operation not permitted, fsync`. NTFS
|
|
12
|
+
* journals the rename itself, so there is nothing to flush and the call is
|
|
13
|
+
* skipped on win32 (feedback_516e4427). On every other platform a failure
|
|
14
|
+
* still throws, so a POSIX stage keeps its durability check.
|
|
15
|
+
*/
|
|
16
|
+
export declare function fsyncDirectory(directory: string, platform?: NodeJS.Platform, io?: DirectorySyncIo): void;
|
|
17
|
+
//# sourceMappingURL=fsync-directory.d.ts.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
const defaultIo = {
|
|
3
|
+
openSync: (file, flags) => fs.openSync(file, flags),
|
|
4
|
+
fsyncSync: (fd) => fs.fsyncSync(fd),
|
|
5
|
+
closeSync: (fd) => fs.closeSync(fd),
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Flush a directory entry (a rename or a new child) to disk.
|
|
9
|
+
*
|
|
10
|
+
* Windows has no directory fsync: FlushFileBuffers on a directory handle
|
|
11
|
+
* fails, and Node reports `EPERM: operation not permitted, fsync`. NTFS
|
|
12
|
+
* journals the rename itself, so there is nothing to flush and the call is
|
|
13
|
+
* skipped on win32 (feedback_516e4427). On every other platform a failure
|
|
14
|
+
* still throws, so a POSIX stage keeps its durability check.
|
|
15
|
+
*/
|
|
16
|
+
export function fsyncDirectory(directory, platform = process.platform, io = defaultIo) {
|
|
17
|
+
if (platform === "win32")
|
|
18
|
+
return;
|
|
19
|
+
const fd = io.openSync(directory, fs.constants.O_RDONLY);
|
|
20
|
+
try {
|
|
21
|
+
io.fsyncSync(fd);
|
|
22
|
+
}
|
|
23
|
+
finally {
|
|
24
|
+
io.closeSync(fd);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=fsync-directory.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type CognitoTokens } from "@indigoai-us/hq-cloud";
|
|
2
|
+
import { type KeychainReadOptions } from "./token-keychain.js";
|
|
3
|
+
/**
|
|
4
|
+
* On-disk person Cognito cache path for `env` (`HQ_STATE_DIR` or `~/.hq`).
|
|
5
|
+
* Deliberately does not use hq-cloud's `loadCachedTokens()`: after a machine
|
|
6
|
+
* mint in this process, that helper returns in-memory machine tokens when the
|
|
7
|
+
* person file is absent, which would falsely block the fresh-box fallback.
|
|
8
|
+
*/
|
|
9
|
+
export declare function personTokenCacheFile(home?: string, env?: NodeJS.ProcessEnv): string;
|
|
10
|
+
export declare function decodeJwtPayload(token: string | undefined): Record<string, unknown> | null;
|
|
11
|
+
export declare function readTokenFile(file: string): CognitoTokens | null;
|
|
12
|
+
/**
|
|
13
|
+
* The person session from the shared macOS Keychain item (see
|
|
14
|
+
* ./token-keychain.ts), or null. Not read when the token file is missing:
|
|
15
|
+
* no file means signed out. Machine-identity and invalidated tokens are
|
|
16
|
+
* refused, the same as the file loader.
|
|
17
|
+
*/
|
|
18
|
+
export declare function personKeychainTokens(file: string, home: string, env: NodeJS.ProcessEnv, keychain?: KeychainReadOptions): CognitoTokens | null;
|
|
19
|
+
/**
|
|
20
|
+
* Person session for `env`: the Keychain item and the token file, the newer
|
|
21
|
+
* by `expiresAt` (see ./token-keychain.ts). Does not consult hq-cloud's
|
|
22
|
+
* in-memory fallback, which can surface a different session when the file
|
|
23
|
+
* is missing or unreadable.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readPersonTokenFile(home?: string, env?: NodeJS.ProcessEnv, keychain?: KeychainReadOptions): CognitoTokens | null;
|
|
26
|
+
/**
|
|
27
|
+
* The person's cached session: the shared Keychain item and hq-cloud's file
|
|
28
|
+
* loader, the newer by `expiresAt`. Writes still go only to the file in this
|
|
29
|
+
* step.
|
|
30
|
+
*/
|
|
31
|
+
export declare function loadCachedTokens(env?: NodeJS.ProcessEnv, keychain?: KeychainReadOptions): CognitoTokens | null;
|
|
32
|
+
//# sourceMappingURL=person-token-cache.d.ts.map
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The person's cached Cognito session, read from disk and the shared Keychain
|
|
3
|
+
* item without any network call. Split out of cognito-session.ts so offline
|
|
4
|
+
* callers (lane start and the handoff commits) can read the cached ID token
|
|
5
|
+
* without bundling the refresh and login clients. cognito-session.ts
|
|
6
|
+
* re-exports these readers.
|
|
7
|
+
*/
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { accessTokenFingerprint, loadCachedTokens as loadCachedTokensFromFile } from "@indigoai-us/hq-cloud";
|
|
12
|
+
import { pickNewerSession, readKeychainTokens } from "./token-keychain.js";
|
|
13
|
+
/**
|
|
14
|
+
* On-disk person Cognito cache path for `env` (`HQ_STATE_DIR` or `~/.hq`).
|
|
15
|
+
* Deliberately does not use hq-cloud's `loadCachedTokens()`: after a machine
|
|
16
|
+
* mint in this process, that helper returns in-memory machine tokens when the
|
|
17
|
+
* person file is absent, which would falsely block the fresh-box fallback.
|
|
18
|
+
*/
|
|
19
|
+
export function personTokenCacheFile(home = os.homedir(), env = process.env) {
|
|
20
|
+
const override = env.HQ_STATE_DIR?.trim();
|
|
21
|
+
const stateDir = override && override.length > 0 ? override : path.join(home, ".hq");
|
|
22
|
+
return path.join(stateDir, "cognito-tokens.json");
|
|
23
|
+
}
|
|
24
|
+
export function decodeJwtPayload(token) {
|
|
25
|
+
if (!token)
|
|
26
|
+
return null;
|
|
27
|
+
const parts = token.split(".");
|
|
28
|
+
if (parts.length < 2 || !parts[1])
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
const json = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
32
|
+
const parsed = JSON.parse(json);
|
|
33
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
34
|
+
return null;
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function readTokenFile(file) {
|
|
42
|
+
try {
|
|
43
|
+
if (!fs.existsSync(file))
|
|
44
|
+
return null;
|
|
45
|
+
const raw = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
46
|
+
if (typeof raw.accessToken !== "string" || typeof raw.idToken !== "string")
|
|
47
|
+
return null;
|
|
48
|
+
return raw;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* The person session from the shared macOS Keychain item (see
|
|
56
|
+
* ./token-keychain.ts), or null. Not read when the token file is missing:
|
|
57
|
+
* no file means signed out. Machine-identity and invalidated tokens are
|
|
58
|
+
* refused, the same as the file loader.
|
|
59
|
+
*/
|
|
60
|
+
export function personKeychainTokens(file, home, env, keychain = {}) {
|
|
61
|
+
if (!fs.existsSync(file))
|
|
62
|
+
return null;
|
|
63
|
+
const tokens = readKeychainTokens({ env, home, ...keychain });
|
|
64
|
+
if (!tokens)
|
|
65
|
+
return null;
|
|
66
|
+
const claims = decodeJwtPayload(tokens.idToken);
|
|
67
|
+
const entityType = claims?.["custom:entityType"];
|
|
68
|
+
if (typeof entityType === "string" && entityType.trim().length > 0)
|
|
69
|
+
return null;
|
|
70
|
+
if (fs.existsSync(`${file}.invalid.${accessTokenFingerprint(tokens.accessToken)}`))
|
|
71
|
+
return null;
|
|
72
|
+
return tokens;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Person session for `env`: the Keychain item and the token file, the newer
|
|
76
|
+
* by `expiresAt` (see ./token-keychain.ts). Does not consult hq-cloud's
|
|
77
|
+
* in-memory fallback, which can surface a different session when the file
|
|
78
|
+
* is missing or unreadable.
|
|
79
|
+
*/
|
|
80
|
+
export function readPersonTokenFile(home = os.homedir(), env = process.env, keychain = {}) {
|
|
81
|
+
const file = personTokenCacheFile(home, env);
|
|
82
|
+
return pickNewerSession(personKeychainTokens(file, home, env, keychain), readTokenFile(file));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The person's cached session: the shared Keychain item and hq-cloud's file
|
|
86
|
+
* loader, the newer by `expiresAt`. Writes still go only to the file in this
|
|
87
|
+
* step.
|
|
88
|
+
*/
|
|
89
|
+
export function loadCachedTokens(env = process.env, keychain = {}) {
|
|
90
|
+
const home = os.homedir();
|
|
91
|
+
const file = personTokenCacheFile(home, env);
|
|
92
|
+
const fromFile = loadCachedTokensFromFile();
|
|
93
|
+
return pickNewerSession(personKeychainTokens(file, home, env, keychain), fromFile);
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=person-token-cache.js.map
|
|
@@ -109,7 +109,17 @@ export type CallerIdentity = {
|
|
|
109
109
|
* {@link resolveCallerPersonUid} does. Use this where the server accepts an
|
|
110
110
|
* agent principal; use `resolveCallerPersonUid` where only a person can act.
|
|
111
111
|
*/
|
|
112
|
-
export declare function resolveCallerUid(token: string, baseUrl?: string
|
|
112
|
+
export declare function resolveCallerUid(token: string, baseUrl?: string, opts?: {
|
|
113
|
+
signal?: AbortSignal;
|
|
114
|
+
}): Promise<CallerIdentity>;
|
|
115
|
+
/**
|
|
116
|
+
* Thrown when the person lookup succeeds but the caller has no person entity.
|
|
117
|
+
* This is a definitive answer, unlike a network or HTTP failure, so callers
|
|
118
|
+
* that cache lookups may remember it.
|
|
119
|
+
*/
|
|
120
|
+
export declare class NoPersonEntityError extends Error {
|
|
121
|
+
constructor();
|
|
122
|
+
}
|
|
113
123
|
/** Thrown when a person-only command runs under an agent machine identity. */
|
|
114
124
|
export declare function agentIdentityUnsupportedError(agentUid: string, command?: string): Error;
|
|
115
125
|
/**
|
|
@@ -120,6 +130,16 @@ export declare function agentIdentityUnsupportedError(agentUid: string, command?
|
|
|
120
130
|
export declare function resolveCallerPersonUid(token: string, baseUrl?: string, opts?: {
|
|
121
131
|
command?: string;
|
|
122
132
|
}): Promise<string>;
|
|
133
|
+
/**
|
|
134
|
+
* A signal that aborts when any input signal aborts, with that signal's
|
|
135
|
+
* reason. It stands in for `AbortSignal.any`, which Node only added in 20.3,
|
|
136
|
+
* so the CLI keeps working on Node 20.0. Call `dispose` once the guarded work
|
|
137
|
+
* settles to remove the listeners from the inputs.
|
|
138
|
+
*/
|
|
139
|
+
export declare function anySignal(signals: AbortSignal[]): {
|
|
140
|
+
signal: AbortSignal;
|
|
141
|
+
dispose: () => void;
|
|
142
|
+
};
|
|
123
143
|
export declare function getEntityUid(token: string, opts: {
|
|
124
144
|
personal?: boolean;
|
|
125
145
|
companySlug?: string;
|
package/dist/utils/vault-api.js
CHANGED
|
@@ -735,11 +735,22 @@ export function agentUidFromToken(token) {
|
|
|
735
735
|
* {@link resolveCallerPersonUid} does. Use this where the server accepts an
|
|
736
736
|
* agent principal; use `resolveCallerPersonUid` where only a person can act.
|
|
737
737
|
*/
|
|
738
|
-
export async function resolveCallerUid(token, baseUrl) {
|
|
738
|
+
export async function resolveCallerUid(token, baseUrl, opts = {}) {
|
|
739
739
|
const agentUid = agentUidFromToken(token);
|
|
740
740
|
if (agentUid)
|
|
741
741
|
return { kind: 'agent', uid: agentUid };
|
|
742
|
-
return { kind: 'person', uid: await lookupCallerPersonUid(token, baseUrl) };
|
|
742
|
+
return { kind: 'person', uid: await lookupCallerPersonUid(token, baseUrl, opts.signal) };
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Thrown when the person lookup succeeds but the caller has no person entity.
|
|
746
|
+
* This is a definitive answer, unlike a network or HTTP failure, so callers
|
|
747
|
+
* that cache lookups may remember it.
|
|
748
|
+
*/
|
|
749
|
+
export class NoPersonEntityError extends Error {
|
|
750
|
+
constructor() {
|
|
751
|
+
super('No person entity found for the caller. Sign in to HQ once to provision one.');
|
|
752
|
+
this.name = 'NoPersonEntityError';
|
|
753
|
+
}
|
|
743
754
|
}
|
|
744
755
|
/** Thrown when a person-only command runs under an agent machine identity. */
|
|
745
756
|
export function agentIdentityUnsupportedError(agentUid, command) {
|
|
@@ -758,14 +769,52 @@ export async function resolveCallerPersonUid(token, baseUrl, opts = {}) {
|
|
|
758
769
|
throw agentIdentityUnsupportedError(agentUid, opts.command);
|
|
759
770
|
return lookupCallerPersonUid(token, baseUrl);
|
|
760
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* A signal that aborts when any input signal aborts, with that signal's
|
|
774
|
+
* reason. It stands in for `AbortSignal.any`, which Node only added in 20.3,
|
|
775
|
+
* so the CLI keeps working on Node 20.0. Call `dispose` once the guarded work
|
|
776
|
+
* settles to remove the listeners from the inputs.
|
|
777
|
+
*/
|
|
778
|
+
export function anySignal(signals) {
|
|
779
|
+
const controller = new AbortController();
|
|
780
|
+
const listeners = [];
|
|
781
|
+
const dispose = () => {
|
|
782
|
+
for (const [input, listener] of listeners.splice(0))
|
|
783
|
+
input.removeEventListener('abort', listener);
|
|
784
|
+
};
|
|
785
|
+
for (const input of signals) {
|
|
786
|
+
if (input.aborted) {
|
|
787
|
+
dispose();
|
|
788
|
+
controller.abort(input.reason);
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
const listener = () => {
|
|
792
|
+
dispose();
|
|
793
|
+
controller.abort(input.reason);
|
|
794
|
+
};
|
|
795
|
+
input.addEventListener('abort', listener, { once: true });
|
|
796
|
+
listeners.push([input, listener]);
|
|
797
|
+
}
|
|
798
|
+
return { signal: controller.signal, dispose };
|
|
799
|
+
}
|
|
761
800
|
// Same selection rule as the backend's `resolveCallerPersonUid`: ascending by
|
|
762
801
|
// createdAt, tie-break by uid ascending. Returns the `prs_*` UID.
|
|
763
|
-
async function lookupCallerPersonUid(token, baseUrl) {
|
|
802
|
+
async function lookupCallerPersonUid(token, baseUrl, signal) {
|
|
803
|
+
const timeout = AbortSignal.timeout(identityLookupTimeoutMs());
|
|
804
|
+
const combined = signal ? anySignal([timeout, signal]) : { signal: timeout, dispose: () => { } };
|
|
805
|
+
try {
|
|
806
|
+
return await lookupCallerPersonUidWith(token, baseUrl, combined.signal);
|
|
807
|
+
}
|
|
808
|
+
finally {
|
|
809
|
+
combined.dispose();
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
async function lookupCallerPersonUidWith(token, baseUrl, signal) {
|
|
764
813
|
const res = await vaultApiFetch({
|
|
765
814
|
token,
|
|
766
815
|
path: '/entity/by-type/person',
|
|
767
816
|
baseUrl,
|
|
768
|
-
signal
|
|
817
|
+
signal,
|
|
769
818
|
});
|
|
770
819
|
if (!res.ok) {
|
|
771
820
|
if (isUnavailableSecretLoadHttpStatus(res.status)) {
|
|
@@ -776,7 +825,7 @@ async function lookupCallerPersonUid(token, baseUrl) {
|
|
|
776
825
|
const data = (await res.json());
|
|
777
826
|
const persons = (data.entities ?? []).filter((e) => e.type === 'person');
|
|
778
827
|
if (persons.length === 0) {
|
|
779
|
-
throw new
|
|
828
|
+
throw new NoPersonEntityError();
|
|
780
829
|
}
|
|
781
830
|
persons.sort((a, b) => {
|
|
782
831
|
const ac = a.createdAt ?? '';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default `windowsHide: true` for child processes of a background hq on Windows.
|
|
3
|
+
*
|
|
4
|
+
* A console program (node, bash, git, cygpath, claude --version) started from
|
|
5
|
+
* a process that has no console of its own gets a new, visible console
|
|
6
|
+
* window. The HQ desktop app, the sync runner and hooks start hq with piped
|
|
7
|
+
* stdio and no console, so every child hq spawned flashed a window on screen,
|
|
8
|
+
* every minute or two, all day (feedback_351a40d5). Node only hides it when a
|
|
9
|
+
* call passes `windowsHide: true`, and hq-cli has a few hundred spawn sites.
|
|
10
|
+
*
|
|
11
|
+
* When this process is on Windows and none of its standard streams is a
|
|
12
|
+
* terminal, every child_process call that does not choose `windowsHide`
|
|
13
|
+
* itself gets `windowsHide: true`. An interactive hq (any standard stream is
|
|
14
|
+
* a TTY) is left alone, so a child that shares the user's console still does.
|
|
15
|
+
*/
|
|
16
|
+
type ChildProcessModule = typeof import("node:child_process");
|
|
17
|
+
/** child_process entry points whose options object accepts `windowsHide`. */
|
|
18
|
+
export declare const WINDOWS_HIDE_FUNCTIONS: readonly ["spawn", "spawnSync", "execFile", "execFileSync", "exec", "execSync", "fork"];
|
|
19
|
+
export type WindowsHideFunction = (typeof WINDOWS_HIDE_FUNCTIONS)[number];
|
|
20
|
+
/**
|
|
21
|
+
* Return `args` with `windowsHide: true` in the options object of a
|
|
22
|
+
* child_process call, unless the caller set `windowsHide` either way. The
|
|
23
|
+
* input array and the caller's options object are not mutated.
|
|
24
|
+
*/
|
|
25
|
+
export declare function applyWindowsHideDefault(fn: WindowsHideFunction, args: readonly unknown[]): unknown[];
|
|
26
|
+
/** True when none of stdin, stdout and stderr is a terminal. */
|
|
27
|
+
export declare function isBackgroundProcess(isTty?: (fd: number) => boolean): boolean;
|
|
28
|
+
export interface InstallWindowsHideDefaultOptions {
|
|
29
|
+
platform?: NodeJS.Platform;
|
|
30
|
+
background?: boolean;
|
|
31
|
+
/** child_process module to patch (tests pass a stand-in). */
|
|
32
|
+
childProcess?: ChildProcessModule;
|
|
33
|
+
/** Called after patching so ESM named imports see the wrapped functions. */
|
|
34
|
+
syncExports?: () => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Patch node:child_process once for this process. Returns true when the
|
|
38
|
+
* default is (now or already) in force.
|
|
39
|
+
*/
|
|
40
|
+
export declare function installWindowsHideDefault(options?: InstallWindowsHideDefaultOptions): boolean;
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=windows-hide-default.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default `windowsHide: true` for child processes of a background hq on Windows.
|
|
3
|
+
*
|
|
4
|
+
* A console program (node, bash, git, cygpath, claude --version) started from
|
|
5
|
+
* a process that has no console of its own gets a new, visible console
|
|
6
|
+
* window. The HQ desktop app, the sync runner and hooks start hq with piped
|
|
7
|
+
* stdio and no console, so every child hq spawned flashed a window on screen,
|
|
8
|
+
* every minute or two, all day (feedback_351a40d5). Node only hides it when a
|
|
9
|
+
* call passes `windowsHide: true`, and hq-cli has a few hundred spawn sites.
|
|
10
|
+
*
|
|
11
|
+
* When this process is on Windows and none of its standard streams is a
|
|
12
|
+
* terminal, every child_process call that does not choose `windowsHide`
|
|
13
|
+
* itself gets `windowsHide: true`. An interactive hq (any standard stream is
|
|
14
|
+
* a TTY) is left alone, so a child that shares the user's console still does.
|
|
15
|
+
*/
|
|
16
|
+
// Only node:module is imported at load time (index.ts already loads it). This
|
|
17
|
+
// module is imported on every hq start, including the hook-time fast routes,
|
|
18
|
+
// so node:tty and node:util are loaded lazily and only on Windows.
|
|
19
|
+
import { createRequire, syncBuiltinESMExports } from "node:module";
|
|
20
|
+
/** child_process entry points whose options object accepts `windowsHide`. */
|
|
21
|
+
export const WINDOWS_HIDE_FUNCTIONS = ["spawn", "spawnSync", "execFile", "execFileSync", "exec", "execSync", "fork"];
|
|
22
|
+
const INSTALLED = Symbol.for("hq.windowsHideDefault.installed");
|
|
23
|
+
/** Same symbol as util.promisify.custom (a registered symbol). */
|
|
24
|
+
const PROMISIFY_CUSTOM = Symbol.for("nodejs.util.promisify.custom");
|
|
25
|
+
const requireBuiltin = createRequire(import.meta.url);
|
|
26
|
+
function defaultIsatty(fd) {
|
|
27
|
+
return requireBuiltin("node:tty").isatty(fd);
|
|
28
|
+
}
|
|
29
|
+
function isOptionsObject(value) {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Return `args` with `windowsHide: true` in the options object of a
|
|
34
|
+
* child_process call, unless the caller set `windowsHide` either way. The
|
|
35
|
+
* input array and the caller's options object are not mutated.
|
|
36
|
+
*/
|
|
37
|
+
export function applyWindowsHideDefault(fn, args) {
|
|
38
|
+
const out = [...args];
|
|
39
|
+
let index;
|
|
40
|
+
if (fn === "exec" || fn === "execSync") {
|
|
41
|
+
index = 1;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// (file, args?, options?, callback?): the args array is optional and may
|
|
45
|
+
// be passed as undefined/null ahead of an options object.
|
|
46
|
+
index = Array.isArray(out[1]) || (out[1] == null && out.length > 2) ? 2 : 1;
|
|
47
|
+
}
|
|
48
|
+
const current = out[index];
|
|
49
|
+
if (isOptionsObject(current)) {
|
|
50
|
+
if ("windowsHide" in current)
|
|
51
|
+
return out;
|
|
52
|
+
out[index] = { ...current, windowsHide: true };
|
|
53
|
+
}
|
|
54
|
+
else if (current == null) {
|
|
55
|
+
out[index] = { windowsHide: true };
|
|
56
|
+
}
|
|
57
|
+
else if (typeof current === "function") {
|
|
58
|
+
out.splice(index, 0, { windowsHide: true });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/** True when none of stdin, stdout and stderr is a terminal. */
|
|
63
|
+
export function isBackgroundProcess(isTty = defaultIsatty) {
|
|
64
|
+
return !isTty(0) && !isTty(1) && !isTty(2);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Patch node:child_process once for this process. Returns true when the
|
|
68
|
+
* default is (now or already) in force.
|
|
69
|
+
*/
|
|
70
|
+
export function installWindowsHideDefault(options = {}) {
|
|
71
|
+
const platform = options.platform ?? process.platform;
|
|
72
|
+
if (platform !== "win32")
|
|
73
|
+
return false;
|
|
74
|
+
if (!(options.background ?? isBackgroundProcess()))
|
|
75
|
+
return false;
|
|
76
|
+
const childProcess = options.childProcess ?? requireBuiltin("node:child_process");
|
|
77
|
+
const target = childProcess;
|
|
78
|
+
if (target[INSTALLED])
|
|
79
|
+
return true;
|
|
80
|
+
for (const name of WINDOWS_HIDE_FUNCTIONS) {
|
|
81
|
+
const original = target[name];
|
|
82
|
+
if (typeof original !== "function")
|
|
83
|
+
continue;
|
|
84
|
+
const originalFn = original;
|
|
85
|
+
const wrapped = function (...args) {
|
|
86
|
+
return originalFn.apply(this, applyWindowsHideDefault(name, args));
|
|
87
|
+
};
|
|
88
|
+
const custom = originalFn[PROMISIFY_CUSTOM];
|
|
89
|
+
if (typeof custom === "function") {
|
|
90
|
+
wrapped[PROMISIFY_CUSTOM] = (...args) => custom(...applyWindowsHideDefault(name, args));
|
|
91
|
+
}
|
|
92
|
+
target[name] = wrapped;
|
|
93
|
+
}
|
|
94
|
+
target[INSTALLED] = true;
|
|
95
|
+
(options.syncExports ?? syncBuiltinESMExports)();
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=windows-hide-default.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.136",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@aws-sdk/client-iot-data-plane": "^3.1096.0",
|
|
49
49
|
"@aws-sdk/client-s3": "^3.1049.0",
|
|
50
50
|
"@aws-sdk/dsql-signer": "^3.1146.0",
|
|
51
|
-
"@indigoai-us/hq-cloud": "~6.18.
|
|
51
|
+
"@indigoai-us/hq-cloud": "~6.18.76",
|
|
52
52
|
"@indigoai-us/hq-flags-client": "^0.1.2",
|
|
53
53
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
54
54
|
"@sentry/node": "^10.49.0",
|