@indigoai-us/hq-cli 5.345.170 → 5.345.171
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 +55 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
- package/dist/command-catalog.generated.d.ts +132 -5
- package/dist/command-catalog.generated.js +164 -6
- package/dist/commands/access.d.ts +32 -1
- package/dist/commands/access.js +112 -3
- package/dist/commands/agent-kit.d.ts +1 -1
- package/dist/commands/agent-kit.js +3 -1
- package/dist/commands/agents.js +2 -1
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +137 -14
- package/dist/commands/cloud.d.ts +58 -1
- package/dist/commands/cloud.js +219 -5
- package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
- package/dist/commands/core-company-index-scope-flag.js +9 -2
- package/dist/commands/core-delegate-w1b.js +5 -2
- package/dist/commands/core-dm-bind-native.js +19 -4
- package/dist/commands/core-rebuild-index-fast.js +24 -6
- package/dist/commands/core.js +18 -6
- package/dist/commands/dm.d.ts +17 -3
- package/dist/commands/dm.js +121 -26
- package/dist/commands/integrations-api.d.ts +4 -0
- package/dist/commands/integrations-api.js +18 -2
- package/dist/commands/integrations-connect.d.ts +10 -3
- package/dist/commands/integrations-connect.js +70 -6
- package/dist/commands/integrations-core.d.ts +23 -0
- package/dist/commands/integrations-core.js +100 -0
- package/dist/commands/integrations-manage.js +8 -1
- package/dist/commands/integrations.js +49 -5
- package/dist/commands/lanes-loop.d.ts +20 -0
- package/dist/commands/lanes-loop.js +69 -11
- package/dist/commands/lanes.js +9 -1
- package/dist/commands/rescue.d.ts +9 -0
- package/dist/commands/rescue.js +31 -0
- package/dist/commands/run.js +115 -40
- package/dist/commands/scan.d.ts +5 -1
- package/dist/commands/scan.js +16 -4
- package/dist/commands/secrets.js +50 -9
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2367 -2109
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
- package/dist/lib/agent-kit/inbox-state.js +41 -20
- package/dist/lib/agent-kit/kit-config.d.ts +2 -0
- package/dist/lib/agent-kit/kit-config.js +6 -0
- package/dist/lib/agent-kit/mcp/tools.js +12 -5
- package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
- package/dist/lib/agent-kit/run/heartbeat.js +6 -3
- package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
- package/dist/lib/agent-kit/run/inbox.js +42 -10
- package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
- package/dist/lib/agent-kit/run/request-timeout.js +51 -0
- package/dist/lib/bot/api.d.ts +1 -1
- package/dist/lib/bot/config.d.ts +10 -0
- package/dist/lib/bot/config.js +17 -1
- package/dist/lib/bot/fleet-local.d.ts +127 -0
- package/dist/lib/bot/fleet-local.js +638 -0
- package/dist/lib/bot/index.d.ts +1 -0
- package/dist/lib/bot/index.js +1 -0
- package/dist/lib/bot/local-config.d.ts +3 -2
- package/dist/lib/bot/local-config.js +5 -2
- package/dist/lib/bot/promote.d.ts +10 -0
- package/dist/lib/bot/promote.js +3 -2
- package/dist/lib/bot/runtime/index.js +8 -4
- package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
- package/dist/lib/bot/runtime/scratch-dir.js +41 -0
- package/dist/lib/caller-principal.d.ts +4 -0
- package/dist/lib/caller-principal.js +40 -0
- package/dist/lib/capacity/collectors/grok.js +8 -2
- package/dist/lib/cli-flag-keys.d.ts +2 -0
- package/dist/lib/cli-flag-keys.js +2 -0
- package/dist/lib/daemon/catalog.js +4 -1
- package/dist/lib/daemon/sync-core-canary.d.ts +3 -0
- package/dist/lib/daemon/sync-core-canary.js +8 -3
- package/dist/lib/doctor/checks/release-links.d.ts +62 -1
- package/dist/lib/doctor/checks/release-links.js +188 -2
- package/dist/lib/doctor/fix/apply.js +54 -0
- package/dist/lib/doctor/fix/remediation.d.ts +8 -1
- package/dist/lib/doctor/fix/remediation.js +22 -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/index-render/index.d.ts +9 -0
- package/dist/lib/index-render/index.js +65 -5
- package/dist/lib/index-render/session-scope.d.ts +69 -0
- package/dist/lib/index-render/session-scope.js +279 -0
- package/dist/lib/lanes/admission.js +7 -20
- package/dist/lib/lanes/reconcile.js +13 -15
- package/dist/lib/lanes/stale-lock.d.ts +50 -0
- package/dist/lib/lanes/stale-lock.js +105 -0
- package/dist/lib/lanes/store.js +11 -76
- package/dist/lib/notify-recipient.d.ts +10 -4
- package/dist/lib/notify-recipient.js +16 -4
- package/dist/lib/secret-input-link.d.ts +42 -0
- package/dist/lib/secret-input-link.js +93 -0
- package/dist/lib/secret-scan/range.d.ts +57 -0
- package/dist/lib/secret-scan/range.js +266 -0
- package/dist/lib/secret-scan/repo-config.d.ts +18 -3
- package/dist/lib/secret-scan/repo-config.js +135 -2
- package/dist/run/script-bound-schema.d.ts +27 -0
- package/dist/run/script-bound-schema.js +97 -0
- package/dist/secrets-env-action.d.ts +2 -0
- package/dist/secrets-env-action.js +7 -2
- package/dist/utils/cognito-session.d.ts +3 -30
- package/dist/utils/cognito-session.js +2 -100
- package/dist/utils/machine-principal-state.d.ts +38 -0
- package/dist/utils/machine-principal-state.js +110 -0
- package/dist/utils/safe-link.js +9 -0
- package/dist/utils/script-bound-exec.d.ts +158 -0
- package/dist/utils/script-bound-exec.js +568 -0
- package/dist/utils/sync-watchdog.d.ts +92 -0
- package/dist/utils/sync-watchdog.js +250 -0
- package/package.json +8 -8
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* checks the result before it leaves this machine: a bot's machine credentials
|
|
10
10
|
* live only in ~/.hq/bots/<name>/machine-creds.json and never travel here.
|
|
11
11
|
*/
|
|
12
|
-
import { botMemoryMode, effectiveBotCompanies, effectiveBotKind, isBotKind, isBotMemoryMode, isBotRuntimeId, } from "./config.js";
|
|
12
|
+
import { botMemoryMode, effectiveBotCompanies, effectiveBotKind, isBotKind, isBotEngineMetadata, isBotMemoryMode, isBotRuntimeId, } from "./config.js";
|
|
13
13
|
/** Same limit the server enforces on the stored value. */
|
|
14
14
|
const BOT_LOCAL_CONFIG_MAX_BYTES = 8 * 1024;
|
|
15
15
|
/** Key names the server rejects outright (case-insensitive, substring). */
|
|
@@ -41,7 +41,8 @@ export function buildBotLocalConfig(config) {
|
|
|
41
41
|
const companies = effectiveBotCompanies(config);
|
|
42
42
|
const local = {
|
|
43
43
|
v: 1,
|
|
44
|
-
runtime: config.runtime,
|
|
44
|
+
...(config.engine ? {} : { runtime: config.runtime }),
|
|
45
|
+
...(config.engine ? { engine: config.engine } : {}),
|
|
45
46
|
workerSource: config.workerSource ?? "scaffold",
|
|
46
47
|
...(config.workerId ? { workerId: config.workerId } : {}),
|
|
47
48
|
...(config.companySlug ? { companySlug: config.companySlug } : {}),
|
|
@@ -80,6 +81,8 @@ export function parseBotLocalConfig(value) {
|
|
|
80
81
|
const runtime = str("runtime");
|
|
81
82
|
if (isBotRuntimeId(runtime))
|
|
82
83
|
out.runtime = runtime;
|
|
84
|
+
if (isBotEngineMetadata(raw.engine))
|
|
85
|
+
out.engine = raw.engine;
|
|
83
86
|
const workerSource = str("workerSource");
|
|
84
87
|
if (workerSource === "worker" || workerSource === "scaffold")
|
|
85
88
|
out.workerSource = workerSource;
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { BotApi, BotPromotionState } from "./api.js";
|
|
2
2
|
import type { BotConfig } from "./config.js";
|
|
3
|
+
import type { BotMachineCreds } from "./creds.js";
|
|
4
|
+
/** A bot whose process and HQ machine credential live outside its state folder,
|
|
5
|
+
* such as an HQ Fleet Runtime bot: promotion stops its service through the
|
|
6
|
+
* runtime and reads the credential from the runtime's secure store.
|
|
7
|
+
*/
|
|
8
|
+
export interface LocalBotPromotionRuntime {
|
|
9
|
+
stop(): void;
|
|
10
|
+
machineCredential(): BotMachineCreds;
|
|
11
|
+
}
|
|
3
12
|
/** One recoverable local-to-cloud step, used by CLI and desktop. Never kills the
|
|
4
13
|
* local runtime, recopies provider login files, or changes another bot.
|
|
5
14
|
*/
|
|
@@ -9,6 +18,7 @@ export declare function promoteLocalBot(input: {
|
|
|
9
18
|
companyUid: string;
|
|
10
19
|
api: BotApi;
|
|
11
20
|
drainTimeoutMs?: number;
|
|
21
|
+
runtime?: LocalBotPromotionRuntime;
|
|
12
22
|
}): Promise<{
|
|
13
23
|
promotion: BotPromotionState;
|
|
14
24
|
pending: boolean;
|
package/dist/lib/bot/promote.js
CHANGED
|
@@ -41,7 +41,7 @@ function readPrivateJson(file, limit) {
|
|
|
41
41
|
* local runtime, recopies provider login files, or changes another bot.
|
|
42
42
|
*/
|
|
43
43
|
export async function promoteLocalBot(input) {
|
|
44
|
-
const { dir, config, companyUid, api } = input;
|
|
44
|
+
const { dir, config, companyUid, api, runtime } = input;
|
|
45
45
|
const existing = await api.promotionStatus(config.agentUid, companyUid);
|
|
46
46
|
const holdFile = path.join(dir, "promotion-hold.json");
|
|
47
47
|
const localHold = fs.existsSync(holdFile) ? readPrivateJson(holdFile, 2048) : null;
|
|
@@ -58,6 +58,7 @@ export async function promoteLocalBot(input) {
|
|
|
58
58
|
recordActivePromotion(dir, hold, existing);
|
|
59
59
|
return { promotion: existing, pending: false };
|
|
60
60
|
}
|
|
61
|
+
runtime?.stop();
|
|
61
62
|
await waitForPromotionDrain(dir, input.drainTimeoutMs ?? 60_000);
|
|
62
63
|
const file = path.join(dir, `promotion-snapshot-${operationId}.json`);
|
|
63
64
|
if (!fs.existsSync(file)) {
|
|
@@ -83,7 +84,7 @@ export async function promoteLocalBot(input) {
|
|
|
83
84
|
throw new Error("Frozen snapshot does not match the cloud operation");
|
|
84
85
|
let state = existing;
|
|
85
86
|
if (!state || state.phase === "preparing") {
|
|
86
|
-
const machine = readPrivateJson(botCredsPath(dir), 8192);
|
|
87
|
+
const machine = runtime ? runtime.machineCredential() : readPrivateJson(botCredsPath(dir), 8192);
|
|
87
88
|
if (!machine || typeof machine !== "object" || typeof machine.secret !== "string")
|
|
88
89
|
throw new Error("Invalid bot machine credential");
|
|
89
90
|
try {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { execFileSync, spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
|
-
import * as os from "node:os";
|
|
5
4
|
import * as path from "node:path";
|
|
6
5
|
import { planBotRuntimeSpawn } from "./windows.js";
|
|
7
6
|
import { GrokAcpClient } from "./grok-acp.js";
|
|
@@ -9,6 +8,7 @@ import { createClaudeRuntime } from "./claude.js";
|
|
|
9
8
|
import { createCodexRuntime } from "./codex.js";
|
|
10
9
|
import { createGrokRuntime, grokRetryArgsAfterInvalidOutputFormat, probeGrokOutputFormat } from "./grok.js";
|
|
11
10
|
import { stripControlTokens } from "./sanitize.js";
|
|
11
|
+
import { createTurnScratchDir } from "./scratch-dir.js";
|
|
12
12
|
import { isRuntimeVersionTooOld, versionTooOldModel } from "../runtime-failure.js";
|
|
13
13
|
import { parseJsonLine, RuntimeError, turnTimeoutMs, stderrExcerpt, MAX_MODEL_FALLBACK_RETRIES, } from "./types.js";
|
|
14
14
|
export * from "./types.js";
|
|
@@ -345,10 +345,13 @@ export function forgetModelFallbacks() {
|
|
|
345
345
|
*/
|
|
346
346
|
export async function runRuntimeTurn(runtime, input, deps = {}) {
|
|
347
347
|
const spawnImpl = deps.spawnImpl ?? defaultSpawn;
|
|
348
|
-
const
|
|
348
|
+
const turnEnv = input.env ?? deps.env ?? process.env;
|
|
349
|
+
const turnPlatform = deps.platform ?? input.platform ?? process.platform;
|
|
350
|
+
const scratch = deps.scratchDir ? { dir: deps.scratchDir } : createTurnScratchDir("hq-bot-turn-", { platform: turnPlatform });
|
|
351
|
+
const scratchDir = scratch.dir;
|
|
349
352
|
const outputFile = path.join(scratchDir, "last-message.txt");
|
|
350
353
|
const newSessionId = input.newSessionId ?? (deps.newSessionId ?? randomUUID)();
|
|
351
|
-
const turnInput = { ...input, platform:
|
|
354
|
+
const turnInput = { ...input, platform: turnPlatform };
|
|
352
355
|
const nowMs = deps.now ?? Date.now;
|
|
353
356
|
const memoKey = fallbackKey(runtime, input.model);
|
|
354
357
|
const memo = rememberedModelFallbacks.get(memoKey);
|
|
@@ -358,7 +361,8 @@ export async function runRuntimeTurn(runtime, input, deps = {}) {
|
|
|
358
361
|
const args = runtime.buildArgs(remembered ? { ...turnInput, model: remembered.to } : turnInput, { outputFile, newSessionId });
|
|
359
362
|
// Enforce at the model boundary too: old plists and direct `bot run` callers
|
|
360
363
|
// may lack the guard. A bot prompt must never quit the user's desktop app.
|
|
361
|
-
|
|
364
|
+
// When TMPDIR was unwritable, hand the runtime CLI the working temp root too.
|
|
365
|
+
const baseEnv = { ...turnEnv, ...(scratch.fallbackRoot ? { TMPDIR: scratch.fallbackRoot } : {}), HQ_NO_ENSURE_HQ_DESKTOP: "1" };
|
|
362
366
|
const env = { ...(runtime.runtimeEnv?.(baseEnv) ?? baseEnv), HQ_NO_ENSURE_HQ_DESKTOP: "1" };
|
|
363
367
|
const binary = runtime.resolveBinary?.(env, input.cwd, turnInput.platform) ?? runtime.binary;
|
|
364
368
|
const stdin = runtime.stdinInput?.(turnInput);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-turn scratch folder for the local bot runtime (feedback_f0131909).
|
|
3
|
+
*
|
|
4
|
+
* A bot started by launchd/systemd at login can inherit a TMPDIR it cannot
|
|
5
|
+
* write yet (an external volume that is not mounted or not yet readable by the
|
|
6
|
+
* agent), so `mkdtemp` failed with EPERM on every turn until `hq bot restart`
|
|
7
|
+
* re-launched it with a working TMPDIR. Try the configured folder first, then
|
|
8
|
+
* /tmp, then ~/.hq/tmp. When TMPDIR works nothing changes.
|
|
9
|
+
*/
|
|
10
|
+
export interface TurnScratchDir {
|
|
11
|
+
/** The created scratch folder. */
|
|
12
|
+
dir: string;
|
|
13
|
+
/** The temp root used, when it is not the configured one. */
|
|
14
|
+
fallbackRoot?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface TurnScratchDeps {
|
|
17
|
+
/** The configured temp root. Defaults to os.tmpdir(), exactly as before. */
|
|
18
|
+
tmpdir?: string;
|
|
19
|
+
platform?: NodeJS.Platform;
|
|
20
|
+
home?: string;
|
|
21
|
+
warn?: (line: string) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function createTurnScratchDir(prefix: string, deps?: TurnScratchDeps): TurnScratchDir;
|
|
24
|
+
//# sourceMappingURL=scratch-dir.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
/** Warn once per unwritable folder per bot process, not on every turn. */
|
|
5
|
+
const warned = new Set();
|
|
6
|
+
/** Errors that mean "this folder cannot be used"; anything else is rethrown. */
|
|
7
|
+
const UNUSABLE = new Set(["EPERM", "EACCES", "ENOENT", "ENOTDIR", "EROFS"]);
|
|
8
|
+
export function createTurnScratchDir(prefix, deps = {}) {
|
|
9
|
+
const platform = deps.platform ?? process.platform;
|
|
10
|
+
const home = deps.home ?? os.homedir();
|
|
11
|
+
const warn = deps.warn ?? ((line) => process.stderr.write(`${line}\n`));
|
|
12
|
+
const configured = deps.tmpdir ?? os.tmpdir();
|
|
13
|
+
const fallbacks = [];
|
|
14
|
+
if (platform !== "win32")
|
|
15
|
+
fallbacks.push("/tmp");
|
|
16
|
+
fallbacks.push(path.join(home, ".hq", "tmp"));
|
|
17
|
+
let firstError;
|
|
18
|
+
for (const root of [configured, ...fallbacks.filter((f) => f !== configured)]) {
|
|
19
|
+
try {
|
|
20
|
+
if (root !== configured)
|
|
21
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
22
|
+
const dir = fs.mkdtempSync(path.join(root, prefix));
|
|
23
|
+
if (root === configured) {
|
|
24
|
+
warned.delete(configured);
|
|
25
|
+
return { dir };
|
|
26
|
+
}
|
|
27
|
+
if (!warned.has(configured))
|
|
28
|
+
warn(`hq bot: temp folder ${configured} is not writable (${firstError?.code ?? "error"}); using ${root} instead. Fix TMPDIR or run hq bot restart.`);
|
|
29
|
+
warned.add(configured);
|
|
30
|
+
return { dir, fallbackRoot: root };
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const e = err;
|
|
34
|
+
if (!e.code || !UNUSABLE.has(e.code))
|
|
35
|
+
throw err;
|
|
36
|
+
firstError ??= e;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`hq bot cannot create a temporary folder: ${configured} is not writable (${firstError?.code ?? "error"}), and ${fallbacks.join(" and ")} did not work either. Fix TMPDIR or run hq bot restart.`);
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=scratch-dir.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function callerIsMachinePrincipal(env?: NodeJS.ProcessEnv): boolean;
|
|
2
|
+
/** True when the token names an agent (agt_) or outpost (out_) identity. */
|
|
3
|
+
export declare function tokenIsMachinePrincipal(token: string): boolean;
|
|
4
|
+
//# sourceMappingURL=caller-principal.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the current caller is a machine principal (a local bot, an
|
|
3
|
+
* agent-kit agent, a hosted agent or an outpost) or a person login.
|
|
4
|
+
*
|
|
5
|
+
* The answer comes from the credentials the request will actually use, never
|
|
6
|
+
* from a user flag:
|
|
7
|
+
* - CLI sends go through `ensureCognitoToken()`, which picks machine tokens
|
|
8
|
+
* exactly when `wantsMachineCognitoTokens()` is true (HQ_MACHINE_CREDS_FILE
|
|
9
|
+
* set and valid, or the default machine creds file on a box with no person
|
|
10
|
+
* session). This check reads local state only and makes no network call.
|
|
11
|
+
* - Callers that already hold a token (the agent-kit MCP tools) use
|
|
12
|
+
* `tokenIsMachinePrincipal`, which reads the token's identity claims.
|
|
13
|
+
*/
|
|
14
|
+
import { wantsMachineCognitoTokens } from "../utils/machine-principal-state.js";
|
|
15
|
+
import { peekIdToken } from "../utils/id-token.js";
|
|
16
|
+
export function callerIsMachinePrincipal(env = process.env) {
|
|
17
|
+
try {
|
|
18
|
+
return wantsMachineCognitoTokens({}, env);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const MACHINE_ENTITY_TYPES = new Set(["agent", "outpost", "company-outpost"]);
|
|
25
|
+
/** True when the token names an agent (agt_) or outpost (out_) identity. */
|
|
26
|
+
export function tokenIsMachinePrincipal(token) {
|
|
27
|
+
let claims;
|
|
28
|
+
try {
|
|
29
|
+
claims = peekIdToken(token);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const type = claims["custom:entityType"];
|
|
35
|
+
if (typeof type === "string" && MACHINE_ENTITY_TYPES.has(type))
|
|
36
|
+
return true;
|
|
37
|
+
const uid = claims["custom:entityUid"];
|
|
38
|
+
return typeof uid === "string" && /^(agt|out)_/.test(uid);
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=caller-principal.js.map
|
|
@@ -192,7 +192,10 @@ export function mapGrokBillingBody(body, options) {
|
|
|
192
192
|
export async function collectGrok(options = {}) {
|
|
193
193
|
const ledger = ledgerOptions(options);
|
|
194
194
|
const root = ledger.root ?? capacityRoot(ledger.env, ledger.home);
|
|
195
|
-
|
|
195
|
+
// Forward the injected clock so ledger retention and now.json use the same
|
|
196
|
+
// instant as observedAt; otherwise retention runs on the wall clock and can
|
|
197
|
+
// delete readings this call just wrote.
|
|
198
|
+
const scoped = { ...ledger, root, now: options.now ?? ledger.now };
|
|
196
199
|
if (readCollectorMark(scoped)?.enabled === false && !options.allowDisabled) {
|
|
197
200
|
return { readings: [], failures: [], view: null, skipped: "disabled", accountId: null };
|
|
198
201
|
}
|
|
@@ -474,7 +477,10 @@ function stampAccount(row, options, planType) {
|
|
|
474
477
|
async function collectEnumeratedGrok(options) {
|
|
475
478
|
const ledger = ledgerOptions(options);
|
|
476
479
|
const root = ledger.root ?? capacityRoot(ledger.env, ledger.home);
|
|
477
|
-
|
|
480
|
+
// Forward the injected clock so ledger retention and now.json use the same
|
|
481
|
+
// instant as observedAt; otherwise retention runs on the wall clock and can
|
|
482
|
+
// delete readings this call just wrote.
|
|
483
|
+
const scoped = { ...ledger, root, now: options.now ?? ledger.now };
|
|
478
484
|
const clock = options.now ?? ledger.now ?? (() => new Date());
|
|
479
485
|
const cliVersion = options.cliVersion ?? readInstalledGrokVersion(options.readVersion);
|
|
480
486
|
const groups = new Map();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** hq-flags keys used by CLI-wide routes. */
|
|
2
2
|
export declare const CLI_COMMAND_HELP_CATALOG_FLAG = "cli.command-help-catalog";
|
|
3
3
|
export declare const MULTI_COMPANY_SESSION_LOCK_FLAG = "hooks.multi-company-session-lock";
|
|
4
|
+
/** Kill switch (default on) for company-scoped `hq core rebuild-index` root indexes. */
|
|
5
|
+
export declare const CORE_COMPANY_INDEX_SCOPE_FLAG = "cli.rebuild-index-company-scope";
|
|
4
6
|
/**
|
|
5
7
|
* hq-flags key for HQ Storage (`hq storage` and Settings > Storage in the
|
|
6
8
|
* desktop app). Shared with hq-desktop-app, so it carries no `cli.` prefix,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** hq-flags keys used by CLI-wide routes. */
|
|
2
2
|
export const CLI_COMMAND_HELP_CATALOG_FLAG = "cli.command-help-catalog";
|
|
3
3
|
export const MULTI_COMPANY_SESSION_LOCK_FLAG = "hooks.multi-company-session-lock";
|
|
4
|
+
/** Kill switch (default on) for company-scoped `hq core rebuild-index` root indexes. */
|
|
5
|
+
export const CORE_COMPANY_INDEX_SCOPE_FLAG = "cli.rebuild-index-company-scope";
|
|
4
6
|
/**
|
|
5
7
|
* hq-flags key for HQ Storage (`hq storage` and Settings > Storage in the
|
|
6
8
|
* desktop app). Shared with hq-desktop-app, so it carries no `cli.` prefix,
|
|
@@ -185,7 +185,9 @@ function planService(service, ctx) {
|
|
|
185
185
|
return [unit];
|
|
186
186
|
const core = base(service, {
|
|
187
187
|
id: "sync-core-canary",
|
|
188
|
-
description:
|
|
188
|
+
description: ctx.syncCoreCanary.fullVault
|
|
189
|
+
? "keeps the personal vault in sync with sync-core"
|
|
190
|
+
: "keeps one personal-vault canary in sync with sync-core",
|
|
189
191
|
command: ctx.syncCoreCanary.binaryPath
|
|
190
192
|
? { type: "exec", file: ctx.syncCoreCanary.binaryPath, args: ["start", "--loop"] }
|
|
191
193
|
: null,
|
|
@@ -195,6 +197,7 @@ function planService(service, ctx) {
|
|
|
195
197
|
HQ_SYNC_PROTOCOL: "1",
|
|
196
198
|
...(ctx.syncCoreCanary.vaultUid ? { HQ_SYNC_CANARY_VAULT: ctx.syncCoreCanary.vaultUid } : {}),
|
|
197
199
|
...(ctx.syncCoreCanary.canaryPath ? { HQ_SYNC_CANARY_PATH: ctx.syncCoreCanary.canaryPath } : {}),
|
|
200
|
+
...(ctx.syncCoreCanary.fullVault ? { HQ_SYNC_FULL_VAULT: "1" } : {}),
|
|
198
201
|
},
|
|
199
202
|
heartbeatTimeoutMs: 180_000,
|
|
200
203
|
blocked: firstBlock([
|
|
@@ -2,7 +2,10 @@ import type { SyncCoreCanaryConfig } from "./config.js";
|
|
|
2
2
|
export interface SyncCoreCanaryPlan {
|
|
3
3
|
binaryPath?: string;
|
|
4
4
|
vaultUid?: string;
|
|
5
|
+
/** The one file the engine may change; absent when `fullVault` is set. */
|
|
5
6
|
canaryPath?: string;
|
|
7
|
+
/** The engine applies every planned small file in the vault (`sync.full_vault = true`). */
|
|
8
|
+
fullVault?: boolean;
|
|
6
9
|
blockedReason?: string;
|
|
7
10
|
}
|
|
8
11
|
export declare function inspectSyncCoreCanary(home: string, hqRoot: string | undefined, config: SyncCoreCanaryConfig, platform: NodeJS.Platform): SyncCoreCanaryPlan;
|
|
@@ -40,8 +40,13 @@ export function inspectSyncCoreCanary(home, hqRoot, config, platform) {
|
|
|
40
40
|
return blocked(config, "the sync-core config must use the daemon's HQ folder");
|
|
41
41
|
}
|
|
42
42
|
const canaryPath = typeof sync.canary_path === "string" ? sync.canary_path.trim() : "";
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const fullVault = sync.full_vault === true;
|
|
44
|
+
if (fullVault && canaryPath) {
|
|
45
|
+
return blocked(config, "the sync-core config must set either full_vault = true or canary_path, not both");
|
|
46
|
+
}
|
|
47
|
+
if (!fullVault && !canaryPath) {
|
|
48
|
+
return blocked(config, "the sync-core config must name one canary path or set full_vault = true");
|
|
49
|
+
}
|
|
45
50
|
const ownedPath = path.join(home, ".hq", "sync-core", "owned", `${config.vaultUid}.json`);
|
|
46
51
|
try {
|
|
47
52
|
const owned = record(JSON.parse(fs.readFileSync(ownedPath, "utf8")));
|
|
@@ -51,6 +56,6 @@ export function inspectSyncCoreCanary(home, hqRoot, config, platform) {
|
|
|
51
56
|
catch {
|
|
52
57
|
return blocked(config, `the sync-core ownership record for ${config.vaultUid} is missing or invalid`);
|
|
53
58
|
}
|
|
54
|
-
return { ...config, canaryPath };
|
|
59
|
+
return fullVault ? { ...config, fullVault: true } : { ...config, canaryPath };
|
|
55
60
|
}
|
|
56
61
|
//# sourceMappingURL=sync-core-canary.js.map
|
|
@@ -9,7 +9,9 @@ import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
|
|
|
9
9
|
export declare const RELEASE_LINKS_FAMILY_ID = "release-links";
|
|
10
10
|
export declare const RELEASE_LINKS_FAMILY_TITLE = "Release links";
|
|
11
11
|
export declare const RELEASE_LINKS_CHECK_ID = "release-links.resolve";
|
|
12
|
-
export declare const RELEASE_LINK_REMEDIATION = "Run `hq rescue -y`
|
|
12
|
+
export declare const RELEASE_LINK_REMEDIATION = "Run `hq doctor --fix` (or `hq rescue -y`) to restore the shipped link. A dangling link that points outside the HQ root and the temp folder is treated as your own and left alone. On Windows, a directory link is written as a junction and a file link as a copy when symlinks are not allowed.";
|
|
13
|
+
/** Message prefix for a link whose target is missing; `--fix` keys off it. */
|
|
14
|
+
export declare const DANGLING_RELEASE_LINK_PROBLEM = "does not resolve.";
|
|
13
15
|
interface ReleaseLink {
|
|
14
16
|
rel: string;
|
|
15
17
|
linkText: string;
|
|
@@ -19,5 +21,64 @@ interface ReleaseLink {
|
|
|
19
21
|
export declare const RELEASE_LINKS: readonly ReleaseLink[];
|
|
20
22
|
export declare const releaseLinksFamily: CheckFamily;
|
|
21
23
|
export declare function checkReleaseLinks(context: CheckContext): CheckResult[];
|
|
24
|
+
/** Where a rescue may stage its extraction folder. */
|
|
25
|
+
export interface RescueStagingDeps {
|
|
26
|
+
/** The system temp folder. Default `os.tmpdir()`. */
|
|
27
|
+
tmpDir?: string;
|
|
28
|
+
/** The rescue backup root. Default `$HQ_BACKUP_DIR` or `~/.hq/backups`. */
|
|
29
|
+
backupRoot?: string;
|
|
30
|
+
}
|
|
31
|
+
/** What {@link repairDanglingReleaseLinks} did to one release link. */
|
|
32
|
+
export interface ReleaseLinkRepair {
|
|
33
|
+
rel: string;
|
|
34
|
+
/**
|
|
35
|
+
* `repointed`: the link was rewritten (see `linkText`). `removed`: the
|
|
36
|
+
* shipped target is missing too, so the dangling link was deleted.
|
|
37
|
+
* `skipped`: the link points outside the HQ root and the temp folder, so it
|
|
38
|
+
* is treated as the user's own link and left alone (see `reason`).
|
|
39
|
+
* `failed`: the repair threw for this link only (see `reason`).
|
|
40
|
+
*/
|
|
41
|
+
action: "repointed" | "removed" | "skipped" | "failed";
|
|
42
|
+
/** The absolute target the link now resolves to (repointed only). */
|
|
43
|
+
target?: string;
|
|
44
|
+
/** The text written into the link (repointed only). */
|
|
45
|
+
linkText?: string;
|
|
46
|
+
reason?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface RepairReleaseLinksDeps {
|
|
49
|
+
platform?: NodeJS.Platform;
|
|
50
|
+
/** Repair only this release link (a `RELEASE_LINKS` rel). */
|
|
51
|
+
only?: string;
|
|
52
|
+
/** The temp folder a rescue extracts into. Default `os.tmpdir()`. */
|
|
53
|
+
tmpDir?: string;
|
|
54
|
+
/** The rescue backup root. Default `$HQ_BACKUP_DIR` or `~/.hq/backups`. */
|
|
55
|
+
backupRoot?: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Repair HQ-managed release links whose target no longer exists, such as a
|
|
59
|
+
* `.agents/skills` or `.codex/claude` link an earlier rescue pointed at its
|
|
60
|
+
* deleted temp extraction folder (feedback_2ab63e50).
|
|
61
|
+
*
|
|
62
|
+
* Only the fixed {@link RELEASE_LINKS} paths are considered, and only when the
|
|
63
|
+
* entry itself is a symlink or junction that does not resolve. A real file or
|
|
64
|
+
* directory is never touched, a link that resolves anywhere is left alone, and
|
|
65
|
+
* the link's parent folder must sit inside the HQ root, so a `.agents` folder
|
|
66
|
+
* that is itself a link out of the tree is skipped.
|
|
67
|
+
*
|
|
68
|
+
* Ownership rule: a dangling link is repaired only when its current target is
|
|
69
|
+
* under the HQ root or under the system temp folder (where a rescue extracts
|
|
70
|
+
* its staging copy). A dangling link anywhere else, such as `/Volumes/X`, is
|
|
71
|
+
* the user's own link to a drive that is not mounted; it is left alone and
|
|
72
|
+
* reported as `skipped`.
|
|
73
|
+
*
|
|
74
|
+
* The repaired link carries the text HQ ships (`../.claude/skills`), so the
|
|
75
|
+
* git-tracked link is unchanged. On Windows a directory link is an absolute
|
|
76
|
+
* junction and a file link an absolute symlink, falling back to a copy. When
|
|
77
|
+
* the shipped target is missing too, the dangling link is removed and nothing
|
|
78
|
+
* is created. The old entry is removed with {@link removeLinkOnly}, which never
|
|
79
|
+
* follows it. A failure on one link is reported as `failed` and the remaining
|
|
80
|
+
* links are still repaired.
|
|
81
|
+
*/
|
|
82
|
+
export declare function repairDanglingReleaseLinks(hqRoot: string, deps?: RepairReleaseLinksDeps): ReleaseLinkRepair[];
|
|
22
83
|
export {};
|
|
23
84
|
//# sourceMappingURL=release-links.d.ts.map
|
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
* user customization and is left alone.
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
9
10
|
import * as path from "node:path";
|
|
11
|
+
import { removeLinkOnly } from "../../../utils/safe-link.js";
|
|
10
12
|
export const RELEASE_LINKS_FAMILY_ID = "release-links";
|
|
11
13
|
export const RELEASE_LINKS_FAMILY_TITLE = "Release links";
|
|
12
14
|
export const RELEASE_LINKS_CHECK_ID = "release-links.resolve";
|
|
13
|
-
export const RELEASE_LINK_REMEDIATION = "Run `hq rescue -y`
|
|
15
|
+
export const RELEASE_LINK_REMEDIATION = "Run `hq doctor --fix` (or `hq rescue -y`) to restore the shipped link. A dangling link that points outside the HQ root and the temp folder is treated as your own and left alone. On Windows, a directory link is written as a junction and a file link as a copy when symlinks are not allowed.";
|
|
16
|
+
/** Message prefix for a link whose target is missing; `--fix` keys off it. */
|
|
17
|
+
export const DANGLING_RELEASE_LINK_PROBLEM = "does not resolve.";
|
|
14
18
|
const CYGWIN_SYMLINK_COOKIE = Buffer.from("!<symlink>", "latin1");
|
|
15
19
|
/** Paths git records as symlinks in the hq-core release. */
|
|
16
20
|
export const RELEASE_LINKS = [
|
|
@@ -80,7 +84,11 @@ function releaseLinkProblem(hqRoot, entry) {
|
|
|
80
84
|
resolved = fs.realpathSync.native(destination);
|
|
81
85
|
}
|
|
82
86
|
catch {
|
|
83
|
-
|
|
87
|
+
const current = readlinkAbsOrNull(destination);
|
|
88
|
+
if (current !== null && !isRepairableDanglingTarget(hqRoot, current)) {
|
|
89
|
+
return `points at ${current}, which is missing and outside this HQ root. It is treated as your own link and left alone; fix or remove it yourself.`;
|
|
90
|
+
}
|
|
91
|
+
return `${DANGLING_RELEASE_LINK_PROBLEM} Its target is missing, often because an update linked it at a staging folder that was then deleted.`;
|
|
84
92
|
}
|
|
85
93
|
if (!pathIsInside(hqRoot, resolved)) {
|
|
86
94
|
return "points outside this HQ root, so it does not resolve to the shipped target.";
|
|
@@ -108,6 +116,42 @@ function releaseLinkProblem(hqRoot, entry) {
|
|
|
108
116
|
// A real directory in place of a directory link is a user replacement.
|
|
109
117
|
return null;
|
|
110
118
|
}
|
|
119
|
+
function readlinkAbsOrNull(destination) {
|
|
120
|
+
try {
|
|
121
|
+
return path.resolve(path.dirname(destination), fs.readlinkSync(destination));
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/** Prefix of the folder hq-cloud's rescue creates with mkdtemp. */
|
|
128
|
+
const RESCUE_STAGING_SEGMENT_PREFIX = "hq-replace-rescue-";
|
|
129
|
+
/**
|
|
130
|
+
* The rescue staging roots, mirroring hq-cloud's `resolveRescueStagingRoot`:
|
|
131
|
+
* the system temp folder, or `tmp` beside the backup root (`~/.hq/tmp`, or
|
|
132
|
+
* next to `$HQ_BACKUP_DIR`) when the temp folder is on another filesystem.
|
|
133
|
+
*/
|
|
134
|
+
function rescueStagingRoots(deps) {
|
|
135
|
+
const backupRoot = deps.backupRoot ?? process.env.HQ_BACKUP_DIR ?? path.join(os.homedir(), ".hq", "backups");
|
|
136
|
+
return [
|
|
137
|
+
...tempRoots(deps.tmpDir ?? os.tmpdir()),
|
|
138
|
+
...tempRoots(path.join(path.dirname(path.resolve(backupRoot)), "tmp")),
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* A dangling link HQ may repair. Its current target is under the HQ root,
|
|
143
|
+
* under the system temp folder, under the rescue staging root beside the
|
|
144
|
+
* backup root, or has a path segment starting with `hq-replace-rescue-` (the
|
|
145
|
+
* rescue's mkdtemp folder, wherever it was staged). Anything else is the
|
|
146
|
+
* user's own link and is left alone.
|
|
147
|
+
*/
|
|
148
|
+
function isRepairableDanglingTarget(hqRoot, current, deps = {}) {
|
|
149
|
+
const segments = path.resolve(current).split(path.sep);
|
|
150
|
+
if (segments.some((segment) => segment.startsWith(RESCUE_STAGING_SEGMENT_PREFIX)))
|
|
151
|
+
return true;
|
|
152
|
+
const roots = [...tempRoots(path.resolve(hqRoot)), ...rescueStagingRoots(deps)];
|
|
153
|
+
return roots.some((root) => isUnder(root, current));
|
|
154
|
+
}
|
|
111
155
|
function containedTarget(hqRoot, entry) {
|
|
112
156
|
const destination = path.join(hqRoot, entry.rel);
|
|
113
157
|
const resolved = path.resolve(path.dirname(destination), entry.linkText);
|
|
@@ -149,4 +193,146 @@ function readFileOrNull(target) {
|
|
|
149
193
|
return null;
|
|
150
194
|
}
|
|
151
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Repair HQ-managed release links whose target no longer exists, such as a
|
|
198
|
+
* `.agents/skills` or `.codex/claude` link an earlier rescue pointed at its
|
|
199
|
+
* deleted temp extraction folder (feedback_2ab63e50).
|
|
200
|
+
*
|
|
201
|
+
* Only the fixed {@link RELEASE_LINKS} paths are considered, and only when the
|
|
202
|
+
* entry itself is a symlink or junction that does not resolve. A real file or
|
|
203
|
+
* directory is never touched, a link that resolves anywhere is left alone, and
|
|
204
|
+
* the link's parent folder must sit inside the HQ root, so a `.agents` folder
|
|
205
|
+
* that is itself a link out of the tree is skipped.
|
|
206
|
+
*
|
|
207
|
+
* Ownership rule: a dangling link is repaired only when its current target is
|
|
208
|
+
* under the HQ root or under the system temp folder (where a rescue extracts
|
|
209
|
+
* its staging copy). A dangling link anywhere else, such as `/Volumes/X`, is
|
|
210
|
+
* the user's own link to a drive that is not mounted; it is left alone and
|
|
211
|
+
* reported as `skipped`.
|
|
212
|
+
*
|
|
213
|
+
* The repaired link carries the text HQ ships (`../.claude/skills`), so the
|
|
214
|
+
* git-tracked link is unchanged. On Windows a directory link is an absolute
|
|
215
|
+
* junction and a file link an absolute symlink, falling back to a copy. When
|
|
216
|
+
* the shipped target is missing too, the dangling link is removed and nothing
|
|
217
|
+
* is created. The old entry is removed with {@link removeLinkOnly}, which never
|
|
218
|
+
* follows it. A failure on one link is reported as `failed` and the remaining
|
|
219
|
+
* links are still repaired.
|
|
220
|
+
*/
|
|
221
|
+
export function repairDanglingReleaseLinks(hqRoot, deps = {}) {
|
|
222
|
+
const platform = deps.platform ?? process.platform;
|
|
223
|
+
let rootReal;
|
|
224
|
+
try {
|
|
225
|
+
rootReal = fs.realpathSync.native(path.resolve(hqRoot));
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
const repairs = [];
|
|
231
|
+
for (const entry of RELEASE_LINKS) {
|
|
232
|
+
if (deps.only !== undefined && entry.rel !== deps.only)
|
|
233
|
+
continue;
|
|
234
|
+
const destination = path.join(rootReal, entry.rel);
|
|
235
|
+
const lst = lstatOrNull(destination);
|
|
236
|
+
if (!lst || !lst.isSymbolicLink())
|
|
237
|
+
continue;
|
|
238
|
+
if (resolvesOrNull(destination) !== null)
|
|
239
|
+
continue;
|
|
240
|
+
const parentReal = resolvesOrNull(path.dirname(destination));
|
|
241
|
+
if (parentReal === null || !isUnder(rootReal, parentReal))
|
|
242
|
+
continue;
|
|
243
|
+
const target = path.resolve(path.dirname(destination), entry.linkText);
|
|
244
|
+
if (!isUnder(rootReal, target))
|
|
245
|
+
continue;
|
|
246
|
+
let current;
|
|
247
|
+
try {
|
|
248
|
+
current = path.resolve(path.dirname(destination), fs.readlinkSync(destination));
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
repairs.push({ rel: entry.rel, action: "failed", reason: errorMessage(err) });
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (!isRepairableDanglingTarget(rootReal, current, { tmpDir: deps.tmpDir, backupRoot: deps.backupRoot })) {
|
|
255
|
+
repairs.push({
|
|
256
|
+
rel: entry.rel,
|
|
257
|
+
action: "skipped",
|
|
258
|
+
reason: `it points at ${current}, outside the HQ root, so it is left as your own link`,
|
|
259
|
+
});
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const targetStat = statOrNull(target);
|
|
263
|
+
const targetUsable = targetStat !== null &&
|
|
264
|
+
(entry.kind === "dir" ? targetStat.isDirectory() : targetStat.isFile()) &&
|
|
265
|
+
isUnder(rootReal, resolvesOrNull(target) ?? "");
|
|
266
|
+
try {
|
|
267
|
+
removeLinkOnly(destination);
|
|
268
|
+
if (!targetUsable) {
|
|
269
|
+
repairs.push({ rel: entry.rel, action: "removed" });
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const linkText = writeReleaseLink(entry, target, destination, platform);
|
|
273
|
+
repairs.push({ rel: entry.rel, action: "repointed", target, linkText });
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
repairs.push({ rel: entry.rel, action: "failed", reason: errorMessage(err) });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return repairs;
|
|
280
|
+
}
|
|
281
|
+
/** Write the link and return the text it carries. */
|
|
282
|
+
function writeReleaseLink(entry, target, destination, platform) {
|
|
283
|
+
if (platform !== "win32") {
|
|
284
|
+
fs.symlinkSync(entry.linkText, destination, entry.kind);
|
|
285
|
+
return entry.linkText;
|
|
286
|
+
}
|
|
287
|
+
if (entry.kind === "dir") {
|
|
288
|
+
fs.symlinkSync(target, destination, "junction");
|
|
289
|
+
return target;
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
fs.symlinkSync(target, destination, "file");
|
|
293
|
+
return target;
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
const code = err.code;
|
|
297
|
+
if (code !== "EPERM" && code !== "EACCES")
|
|
298
|
+
throw err;
|
|
299
|
+
// Windows without the symlink privilege: the check accepts a copy whose
|
|
300
|
+
// bytes equal the target.
|
|
301
|
+
fs.copyFileSync(target, destination);
|
|
302
|
+
return target;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function tempRoots(tmpDir) {
|
|
306
|
+
const roots = [path.resolve(tmpDir)];
|
|
307
|
+
const real = resolvesOrNull(tmpDir);
|
|
308
|
+
if (real && !roots.includes(real))
|
|
309
|
+
roots.push(real);
|
|
310
|
+
return roots;
|
|
311
|
+
}
|
|
312
|
+
function errorMessage(err) {
|
|
313
|
+
return err instanceof Error ? err.message : String(err);
|
|
314
|
+
}
|
|
315
|
+
function isUnder(rootReal, candidate) {
|
|
316
|
+
if (!candidate)
|
|
317
|
+
return false;
|
|
318
|
+
const abs = path.resolve(candidate);
|
|
319
|
+
const base = rootReal.endsWith(path.sep) ? rootReal.slice(0, -1) : rootReal;
|
|
320
|
+
return abs === base || abs.startsWith(base + path.sep);
|
|
321
|
+
}
|
|
322
|
+
function resolvesOrNull(target) {
|
|
323
|
+
try {
|
|
324
|
+
return fs.realpathSync.native(target);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function statOrNull(target) {
|
|
331
|
+
try {
|
|
332
|
+
return fs.statSync(target);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
152
338
|
//# sourceMappingURL=release-links.js.map
|