@indigoai-us/hq-cli 5.345.169 → 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 +69 -0
- package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
- package/dist/command-catalog.generated.d.ts +181 -9
- package/dist/command-catalog.generated.js +224 -10
- package/dist/commands/access.d.ts +32 -1
- package/dist/commands/access.js +119 -5
- 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 +141 -14
- package/dist/commands/cloud.d.ts +78 -1
- package/dist/commands/cloud.js +274 -9
- 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/feedback-triage.d.ts +43 -1
- package/dist/commands/feedback-triage.js +123 -23
- package/dist/commands/feedback.d.ts +40 -1
- package/dist/commands/feedback.js +184 -4
- package/dist/commands/files.d.ts +42 -0
- package/dist/commands/files.js +223 -30
- 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/mesh.js +6 -0
- 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/commands/sync-manifest.d.ts +16 -0
- package/dist/commands/sync-manifest.js +17 -1
- 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/decisions.d.ts +62 -0
- package/dist/lib/bot/decisions.js +228 -0
- 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 +2 -0
- package/dist/lib/bot/index.js +2 -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/run.d.ts +7 -0
- package/dist/lib/bot/run.js +104 -1
- 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.d.ts +4 -0
- package/dist/lib/daemon/catalog.js +63 -29
- package/dist/lib/daemon/config.d.ts +7 -0
- package/dist/lib/daemon/config.js +32 -0
- package/dist/lib/daemon/run.d.ts +3 -2
- package/dist/lib/daemon/run.js +23 -3
- package/dist/lib/daemon/supervisor.d.ts +1 -0
- package/dist/lib/daemon/supervisor.js +4 -0
- package/dist/lib/daemon/sync-core-canary.d.ts +12 -0
- package/dist/lib/daemon/sync-core-canary.js +61 -0
- 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/checks/sync-health.d.ts +32 -0
- package/dist/lib/doctor/checks/sync-health.js +54 -1
- package/dist/lib/doctor/fix/apply.d.ts +26 -4
- package/dist/lib/doctor/fix/apply.js +149 -11
- package/dist/lib/doctor/fix/remediation.d.ts +8 -1
- package/dist/lib/doctor/fix/remediation.js +22 -0
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +1 -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/mesh/live/daemon/work-subscribe.js +31 -0
- 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/lib/work-context/organize.d.ts +7 -0
- package/dist/lib/work-context/organize.js +21 -1
- 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/sentry-before-send.js +1 -0
- 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
package/dist/lib/daemon/run.d.ts
CHANGED
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
* legacy units, the desktop app), so enabling a service, creating a bot, or
|
|
6
6
|
* signing in takes effect without restarting the daemon.
|
|
7
7
|
*/
|
|
8
|
-
import { type UnitPlan } from "./catalog.js";
|
|
8
|
+
import { type PlanContext, type ServiceName, type UnitPlan } from "./catalog.js";
|
|
9
9
|
import { type LegacyUnit } from "./legacy.js";
|
|
10
10
|
import { type DaemonLockResult } from "./lock.js";
|
|
11
11
|
import { type MachineType } from "./machine-type.js";
|
|
12
12
|
import { type PickedHqCloud } from "./hq-cloud.js";
|
|
13
|
-
import { type HostApp } from "./supervisor.js";
|
|
13
|
+
import { type HostApp, type Supervisor } from "./supervisor.js";
|
|
14
14
|
export declare const LOG_ROTATE_BYTES: number;
|
|
15
15
|
/** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
|
|
16
16
|
export declare const RESTART_EXIT_CODE = 75;
|
|
17
|
+
export declare function planUnitsWithSyncCoreInterlock(services: readonly ServiceName[], context: PlanContext, supervisor: Pick<Supervisor, "runningPlanKey"> | null | undefined): UnitPlan[];
|
|
17
18
|
/** A bounded shutdown deadline that can account for sync push persistence. */
|
|
18
19
|
export declare function createShutdownCap(): {
|
|
19
20
|
promise: Promise<"timeout">;
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -22,6 +22,7 @@ import { readBotConfig } from "../bot/config.js";
|
|
|
22
22
|
import { botsRoot } from "../bot/paths.js";
|
|
23
23
|
import { CLAUDE_RUNNER_SCRIPT, CODEX_RUNNER_SCRIPT, enabledServices, planUnits } from "./catalog.js";
|
|
24
24
|
import { effectiveSyncOwner, readDaemonConfig } from "./config.js";
|
|
25
|
+
import { inspectSyncCoreCanary } from "./sync-core-canary.js";
|
|
25
26
|
import { assertPersonalServiceSessionCapability, readRemoteControlPoolsConfig, remoteControlPoolSecretsEnv, } from "./remote-control-pools.js";
|
|
26
27
|
import { loadLanesConfig } from "../lanes/config.js";
|
|
27
28
|
import { daemonLanesReconcileHandler } from "../lanes/daemon-reconcile.js";
|
|
@@ -54,7 +55,7 @@ import { stageHqChildBootstrap } from "./hq-child-bootstrap-stage.js";
|
|
|
54
55
|
import { runnerSessionName } from "./remote-control.js";
|
|
55
56
|
import { adoptChild, createChildTracker, finishSuccession, handoffEligible, readProcStart, stopOrphans, successionPaths, systemdKillMode, takeSuccession, writeHandoff, } from "./succession.js";
|
|
56
57
|
import { listSyncRunnerProcesses, readWatchRunnerOwnerStatus, syncHeartbeatTimeoutMs, syncRunnerNodeOptions, watchRunnerOwnerStatusPath, } from "./sync-runner.js";
|
|
57
|
-
import { STOP_ESCALATION_MS, SYNC_RUNNER_PERSIST_GRACE_MS, createSupervisor, } from "./supervisor.js";
|
|
58
|
+
import { STOP_ESCALATION_MS, SYNC_RUNNER_PERSIST_GRACE_MS, createSupervisor, planKey, } from "./supervisor.js";
|
|
58
59
|
import { userToolDirs } from "./tools.js";
|
|
59
60
|
import { logOffset, readLogSince, readUnitHolds, writeUnitHolds } from "./unit-holds.js";
|
|
60
61
|
import { daemonRunGuard } from "./lanes-reconcile-guard.js";
|
|
@@ -63,6 +64,19 @@ export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
|
|
|
63
64
|
export const RESTART_EXIT_CODE = 75;
|
|
64
65
|
const NON_SYNC_SHUTDOWN_CAP_MS = STOP_ESCALATION_MS + 5_000;
|
|
65
66
|
const SYNC_RUNNER_SHUTDOWN_CAP_MS = STOP_ESCALATION_MS + SYNC_RUNNER_PERSIST_GRACE_MS + 5_000;
|
|
67
|
+
export function planUnitsWithSyncCoreInterlock(services, context, supervisor) {
|
|
68
|
+
const syncCoreCanaryRunning = supervisor?.runningPlanKey("sync-core-canary") !== undefined;
|
|
69
|
+
const transitionContext = {
|
|
70
|
+
...context,
|
|
71
|
+
syncCoreCanaryRunning,
|
|
72
|
+
legacySyncReadyForCore: false,
|
|
73
|
+
};
|
|
74
|
+
const waiting = planUnits(services, transitionContext);
|
|
75
|
+
const legacy = waiting.find((unit) => unit.id === "sync");
|
|
76
|
+
const legacySyncReadyForCore = legacy !== undefined
|
|
77
|
+
&& supervisor?.runningPlanKey("sync") === planKey(legacy, CLI_VERSION);
|
|
78
|
+
return planUnits(services, { ...transitionContext, legacySyncReadyForCore });
|
|
79
|
+
}
|
|
66
80
|
/** A bounded shutdown deadline that can account for sync push persistence. */
|
|
67
81
|
export function createShutdownCap() {
|
|
68
82
|
const startedAt = Date.now();
|
|
@@ -939,7 +953,7 @@ export async function runDaemon(opts) {
|
|
|
939
953
|
legacy,
|
|
940
954
|
appRunning: () => desktopAppRunning(platform),
|
|
941
955
|
});
|
|
942
|
-
const
|
|
956
|
+
const context = {
|
|
943
957
|
machineType,
|
|
944
958
|
platform,
|
|
945
959
|
home,
|
|
@@ -964,6 +978,11 @@ export async function runDaemon(opts) {
|
|
|
964
978
|
return switches;
|
|
965
979
|
})(),
|
|
966
980
|
syncOwner,
|
|
981
|
+
syncCoreCanary: config.syncCoreCanaryError
|
|
982
|
+
? { blockedReason: config.syncCoreCanaryError }
|
|
983
|
+
: config.syncCoreCanary
|
|
984
|
+
? inspectSyncCoreCanary(home, hqRoot, config.syncCoreCanary, platform)
|
|
985
|
+
: undefined,
|
|
967
986
|
desktopAppManagesSync: appManagesSync,
|
|
968
987
|
syncRunnerProcesses,
|
|
969
988
|
syncRunnerOwnerPid,
|
|
@@ -991,7 +1010,8 @@ export async function runDaemon(opts) {
|
|
|
991
1010
|
invalidPoolServiceSessions,
|
|
992
1011
|
availablePoolFrontends,
|
|
993
1012
|
unverifiablePoolFrontends,
|
|
994
|
-
}
|
|
1013
|
+
};
|
|
1014
|
+
const plans = planUnitsWithSyncCoreInterlock(services, context, supervisor);
|
|
995
1015
|
if (startupPlanOpen) {
|
|
996
1016
|
const replaceablePackageRoots = cliPackageRoot ? [cliPackageRoot] : [];
|
|
997
1017
|
if (hqCloud?.source === "bundled")
|
|
@@ -179,6 +179,7 @@ export interface Supervisor {
|
|
|
179
179
|
stop(): Promise<void>;
|
|
180
180
|
reconcile(): void;
|
|
181
181
|
state(): DaemonState;
|
|
182
|
+
runningPlanKey(unitId: string): string | undefined;
|
|
182
183
|
/** True while a live sync runner has reported that it is persisting pushes. */
|
|
183
184
|
hasStoppingSyncRunner(): boolean;
|
|
184
185
|
/** Stop supervising without signalling any child; returns the long-running units left running. */
|
|
@@ -1399,6 +1399,10 @@ export function createSupervisor(deps) {
|
|
|
1399
1399
|
},
|
|
1400
1400
|
reconcile,
|
|
1401
1401
|
state: snapshot,
|
|
1402
|
+
runningPlanKey(unitId) {
|
|
1403
|
+
const slot = slots.get(unitId);
|
|
1404
|
+
return slot?.child ? slot.childKey : undefined;
|
|
1405
|
+
},
|
|
1402
1406
|
hasStoppingSyncRunner: () => [...liveOwners.values()].some(syncRunnerIsStopping),
|
|
1403
1407
|
release() {
|
|
1404
1408
|
const released = [];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SyncCoreCanaryConfig } from "./config.js";
|
|
2
|
+
export interface SyncCoreCanaryPlan {
|
|
3
|
+
binaryPath?: string;
|
|
4
|
+
vaultUid?: string;
|
|
5
|
+
/** The one file the engine may change; absent when `fullVault` is set. */
|
|
6
|
+
canaryPath?: string;
|
|
7
|
+
/** The engine applies every planned small file in the vault (`sync.full_vault = true`). */
|
|
8
|
+
fullVault?: boolean;
|
|
9
|
+
blockedReason?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function inspectSyncCoreCanary(home: string, hqRoot: string | undefined, config: SyncCoreCanaryConfig, platform: NodeJS.Platform): SyncCoreCanaryPlan;
|
|
12
|
+
//# sourceMappingURL=sync-core-canary.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { parse as parseToml } from "smol-toml";
|
|
4
|
+
function blocked(config, blockedReason) {
|
|
5
|
+
return { ...config, blockedReason };
|
|
6
|
+
}
|
|
7
|
+
function record(value) {
|
|
8
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
9
|
+
? value
|
|
10
|
+
: undefined;
|
|
11
|
+
}
|
|
12
|
+
export function inspectSyncCoreCanary(home, hqRoot, config, platform) {
|
|
13
|
+
if (platform !== "darwin" && platform !== "linux") {
|
|
14
|
+
return blocked(config, "the sync-core canary requires macOS or Linux");
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
if (!fs.statSync(config.binaryPath).isFile())
|
|
18
|
+
return blocked(config, "the sync-core binary is missing");
|
|
19
|
+
fs.accessSync(config.binaryPath, fs.constants.X_OK);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return blocked(config, "the sync-core binary is missing or not executable");
|
|
23
|
+
}
|
|
24
|
+
const configPath = path.join(home, ".hq", "sync-core", "config.toml");
|
|
25
|
+
let sync;
|
|
26
|
+
try {
|
|
27
|
+
sync = record(record(parseToml(fs.readFileSync(configPath, "utf8")))?.sync);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return blocked(config, `the sync-core config at ${configPath} is missing or invalid`);
|
|
31
|
+
}
|
|
32
|
+
if (sync?.engine !== "rust-test" || sync.mode !== "full") {
|
|
33
|
+
return blocked(config, "the sync-core config must select engine rust-test in full mode");
|
|
34
|
+
}
|
|
35
|
+
const vaults = sync.vaults;
|
|
36
|
+
if (!Array.isArray(vaults) || vaults.length !== 1 || vaults[0] !== config.vaultUid) {
|
|
37
|
+
return blocked(config, `the sync-core config must name exactly ${config.vaultUid}`);
|
|
38
|
+
}
|
|
39
|
+
if (!hqRoot || typeof sync.hq_root !== "string" || path.resolve(sync.hq_root) !== path.resolve(hqRoot)) {
|
|
40
|
+
return blocked(config, "the sync-core config must use the daemon's HQ folder");
|
|
41
|
+
}
|
|
42
|
+
const canaryPath = typeof sync.canary_path === "string" ? sync.canary_path.trim() : "";
|
|
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
|
+
}
|
|
50
|
+
const ownedPath = path.join(home, ".hq", "sync-core", "owned", `${config.vaultUid}.json`);
|
|
51
|
+
try {
|
|
52
|
+
const owned = record(JSON.parse(fs.readFileSync(ownedPath, "utf8")));
|
|
53
|
+
if (owned?.vault !== config.vaultUid)
|
|
54
|
+
throw new Error("mismatch");
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return blocked(config, `the sync-core ownership record for ${config.vaultUid} is missing or invalid`);
|
|
58
|
+
}
|
|
59
|
+
return fullVault ? { ...config, fullVault: true } : { ...config, canaryPath };
|
|
60
|
+
}
|
|
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
|
|
@@ -61,7 +61,39 @@ export interface SyncManifestUploadStatus {
|
|
|
61
61
|
baseUsable: boolean;
|
|
62
62
|
/** Local company slug for company-scoped remediation; absent for personal. */
|
|
63
63
|
companySlug?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Last failed pass, from hq-cloud >= 6.18.102 (`readManifestUploadStatus`,
|
|
66
|
+
* hq-cloud #1050). Fixed tokens only. Absent on older hq-cloud and after a
|
|
67
|
+
* later pass succeeded.
|
|
68
|
+
*/
|
|
69
|
+
lastAttemptAt?: string;
|
|
70
|
+
consecutiveFailures?: number;
|
|
71
|
+
lastFailureReason?: string;
|
|
72
|
+
lastFailureKind?: string;
|
|
73
|
+
lastFailureStatus?: number;
|
|
64
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* The last failed pass for a stale scope, when hq-cloud recorded one after the
|
|
77
|
+
* last successful upload. Undefined means "no evidence a pass ran", which is
|
|
78
|
+
* the only case where "the daily audit pass is not running" is accurate.
|
|
79
|
+
*/
|
|
80
|
+
export declare function manifestLastFailure(entry: SyncManifestUploadStatus): {
|
|
81
|
+
reason: string;
|
|
82
|
+
status?: number;
|
|
83
|
+
attempts: number;
|
|
84
|
+
at: string;
|
|
85
|
+
} | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Failures a fresh `hq sync manifest` from this machine cannot clear: the
|
|
88
|
+
* upload route is not deployed (404 soft skip), the server rejected this
|
|
89
|
+
* identity, or it refused access (403). A 401 is not listed: it can be an
|
|
90
|
+
* expired token that a fresh run refreshes. Offering `--fix` for these only makes the
|
|
91
|
+
* health hook report the same row as "repair did not clear" every day.
|
|
92
|
+
*/
|
|
93
|
+
export declare function manifestFailureNotFixable(failure: {
|
|
94
|
+
reason: string;
|
|
95
|
+
status?: number;
|
|
96
|
+
}): boolean;
|
|
65
97
|
/** Injectable dependencies — defaults are the real collectors. */
|
|
66
98
|
export interface SyncHealthDeps {
|
|
67
99
|
/** Runner log and conflict signals; omitted in unit tests of other checks. */
|
|
@@ -52,6 +52,45 @@ const SYNC_FAMILY_TITLE = "Versions & sync";
|
|
|
52
52
|
export const STALE_JOURNAL_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000;
|
|
53
53
|
/** The offline update cache written by the check-hq-update SessionStart hook. */
|
|
54
54
|
export const UPDATE_CACHE_RELPATH = path.join("workspace", ".hq-update-check", "last-check.json");
|
|
55
|
+
/** Failure tokens hq-cloud stores, restricted the same way hq-cloud reads them. */
|
|
56
|
+
const MANIFEST_FAILURE_TOKEN = /^[a-z][a-z0-9_]{0,39}$/;
|
|
57
|
+
/**
|
|
58
|
+
* The last failed pass for a stale scope, when hq-cloud recorded one after the
|
|
59
|
+
* last successful upload. Undefined means "no evidence a pass ran", which is
|
|
60
|
+
* the only case where "the daily audit pass is not running" is accurate.
|
|
61
|
+
*/
|
|
62
|
+
export function manifestLastFailure(entry) {
|
|
63
|
+
const reason = entry.lastFailureReason;
|
|
64
|
+
const at = entry.lastAttemptAt;
|
|
65
|
+
if (!reason || !MANIFEST_FAILURE_TOKEN.test(reason) || !at)
|
|
66
|
+
return undefined;
|
|
67
|
+
const attempts = entry.consecutiveFailures ?? 0;
|
|
68
|
+
if (!Number.isFinite(attempts) || attempts < 1)
|
|
69
|
+
return undefined;
|
|
70
|
+
const atMs = Date.parse(at);
|
|
71
|
+
if (!Number.isFinite(atMs))
|
|
72
|
+
return undefined;
|
|
73
|
+
const uploadedMs = entry.lastUploadAt ? Date.parse(entry.lastUploadAt) : Number.NaN;
|
|
74
|
+
if (Number.isFinite(uploadedMs) && atMs <= uploadedMs)
|
|
75
|
+
return undefined;
|
|
76
|
+
const status = typeof entry.lastFailureStatus === "number" && Number.isInteger(entry.lastFailureStatus)
|
|
77
|
+
? entry.lastFailureStatus
|
|
78
|
+
: undefined;
|
|
79
|
+
return { reason, attempts: Math.floor(attempts), at, ...(status !== undefined ? { status } : {}) };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Failures a fresh `hq sync manifest` from this machine cannot clear: the
|
|
83
|
+
* upload route is not deployed (404 soft skip), the server rejected this
|
|
84
|
+
* identity, or it refused access (403). A 401 is not listed: it can be an
|
|
85
|
+
* expired token that a fresh run refreshes. Offering `--fix` for these only makes the
|
|
86
|
+
* health hook report the same row as "repair did not clear" every day.
|
|
87
|
+
*/
|
|
88
|
+
export function manifestFailureNotFixable(failure) {
|
|
89
|
+
if (failure.reason === "soft_skip" || failure.reason === "rejected_identity")
|
|
90
|
+
return true;
|
|
91
|
+
// 401 is left fixable: an expired token is refreshed by the next run.
|
|
92
|
+
return failure.status === 403 || failure.status === 404;
|
|
93
|
+
}
|
|
55
94
|
/** Fill in the optional dependencies so call sites never branch on undefined. */
|
|
56
95
|
function withDefaults(deps, hqRoot) {
|
|
57
96
|
return {
|
|
@@ -782,7 +821,21 @@ function manifestResults(deps, journals) {
|
|
|
782
821
|
}
|
|
783
822
|
}
|
|
784
823
|
const command = `hq sync manifest${scope ? ` --scope ${scope}` : ""}`;
|
|
785
|
-
const
|
|
824
|
+
const failure = manifestLastFailure(entry);
|
|
825
|
+
const age = `Scope '${entry.scopeKey}' last uploaded a sync manifest ${days} day${days === 1 ? "" : "s"} ago (${entry.lastUploadAt})`;
|
|
826
|
+
// A pass that ran and failed is not "not running": name the stored reason.
|
|
827
|
+
const message = failure
|
|
828
|
+
? `${age}. The last pass (${failure.at}) failed with ${failure.reason}${failure.status !== undefined ? ` (HTTP ${failure.status})` : ""}, ${failure.attempts} failed pass${failure.attempts === 1 ? "" : "es"} in a row.`
|
|
829
|
+
: `${age} — the daily audit pass is not running.`;
|
|
830
|
+
if (failure && manifestFailureNotFixable(failure)) {
|
|
831
|
+
return {
|
|
832
|
+
status: "WARN",
|
|
833
|
+
checkId,
|
|
834
|
+
reasonCode: "manifest-upload-rejected",
|
|
835
|
+
message,
|
|
836
|
+
remediation: `--fix cannot clear this from this machine. Check you are signed in to the right HQ account (/hq-whoami), then run \`${command}\` and read its error.`,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
786
839
|
// With the kill switch set the upload exits 0 and sends nothing, so the
|
|
787
840
|
// row carries no `--fix` target and names the switch instead.
|
|
788
841
|
if (deps.manifestUploadsDisabled()) {
|
|
@@ -157,8 +157,13 @@ export interface RefreshManifestTarget {
|
|
|
157
157
|
}
|
|
158
158
|
/** Outcome of one {@link ApplyFixesOptions.refreshManifest} call. */
|
|
159
159
|
export interface RefreshManifestResult {
|
|
160
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* Whether the upload command exited 0 and, when it printed a `--json`
|
|
162
|
+
* outcome, that outcome was `uploaded`. The re-check is the real verdict.
|
|
163
|
+
*/
|
|
161
164
|
ok: boolean;
|
|
165
|
+
/** The `hq sync manifest --json` status token, when the pass did not upload. */
|
|
166
|
+
passStatus?: string;
|
|
162
167
|
/** One-line summary. */
|
|
163
168
|
message: string;
|
|
164
169
|
/**
|
|
@@ -333,11 +338,28 @@ export interface RefreshSyncSpawnDeps {
|
|
|
333
338
|
export declare const REFRESH_SYNC_LOCK_TIMEOUT_SEC = 30;
|
|
334
339
|
/**
|
|
335
340
|
* Default `refresh-manifest` repair: a bounded `hq sync manifest --scope <s>`
|
|
336
|
-
* against the tree `--fix` is repairing
|
|
337
|
-
*
|
|
338
|
-
*
|
|
341
|
+
* against the tree `--fix` is repairing, with `--json` so a pass that exits 0
|
|
342
|
+
* without uploading (soft_skipped, throttled, disabled, locked,
|
|
343
|
+
* resend_full_scheduled) is reported as not uploaded. A `resend_full` answer
|
|
344
|
+
* is retried once with `--full`. The post-fix re-check still decides whether
|
|
345
|
+
* the row cleared. A timeout or non-zero exit is reported, not thrown.
|
|
339
346
|
*/
|
|
340
347
|
export declare function defaultRefreshManifest(target: RefreshManifestTarget): Promise<RefreshManifestResult>;
|
|
348
|
+
type ManifestSpawnResult = Pick<SpawnSyncReturns<string>, "error" | "status" | "signal"> & Partial<Pick<SpawnSyncReturns<string>, "stdout" | "stderr">>;
|
|
349
|
+
/**
|
|
350
|
+
* Run one upload and, when the server answered `resend_full` (it dropped or
|
|
351
|
+
* refused the delta base), run it once more with `--full`. Without the retry
|
|
352
|
+
* the scope stays stale until the next day's --fix run, and the health hook
|
|
353
|
+
* reports it as unrepaired in between (feedback_84d7a862).
|
|
354
|
+
*/
|
|
355
|
+
export declare function refreshManifestWithRetry(scope: string, run: (full: boolean, json: boolean) => ManifestSpawnResult): RefreshManifestResult;
|
|
356
|
+
/** Commander's rejection of `--json` by a binary that predates it. */
|
|
357
|
+
export declare function manifestJsonUnsupported(result: ManifestSpawnResult): boolean;
|
|
358
|
+
/**
|
|
359
|
+
* The status token from the `hq sync manifest --json` line, or undefined when
|
|
360
|
+
* the output has none (an older binary, or a pass that printed only a summary).
|
|
361
|
+
*/
|
|
362
|
+
export declare function manifestPassStatus(stdout: string | undefined): string | undefined;
|
|
341
363
|
/**
|
|
342
364
|
* Map one `hq sync manifest` spawn result to a {@link RefreshManifestResult}.
|
|
343
365
|
* A spawn killed by the timeout has a null status, so it is reported as a
|