@indigoai-us/hq-cli 5.273.0-alpha.0 → 5.274.0
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 +65 -2
- package/README.md +32 -1
- package/dist/command-catalog.generated.d.ts +83 -536
- package/dist/command-catalog.generated.js +129 -714
- package/dist/command-registration-plan.d.ts +12 -0
- package/dist/command-registration-plan.js +2 -0
- package/dist/commands/cloud.js +40 -0
- package/dist/commands/core-checkpoint.js +3 -1
- package/dist/commands/core-command-catalog.d.ts +32 -0
- package/dist/commands/core-command-catalog.js +58 -0
- package/dist/commands/core-ontology.js +9 -4
- package/dist/commands/core.d.ts +4 -0
- package/dist/commands/core.js +79 -75
- package/dist/commands/daemon.d.ts +0 -3
- package/dist/commands/daemon.js +1 -173
- package/dist/commands/deploy-comments.d.ts +3 -0
- package/dist/commands/deploy-comments.js +159 -0
- package/dist/commands/lanes-checks.d.ts +0 -1
- package/dist/commands/lanes-checks.js +217 -54
- package/dist/commands/lanes-hold.js +40 -2
- package/dist/commands/lanes-message.js +98 -14
- package/dist/commands/lanes-move.d.ts +12 -0
- package/dist/commands/lanes-move.js +342 -0
- package/dist/commands/lanes-resume.js +3 -1
- package/dist/commands/lanes-shared.d.ts +39 -3
- package/dist/commands/lanes-shared.js +59 -0
- package/dist/commands/lanes.d.ts +3 -5
- package/dist/commands/lanes.js +206 -478
- package/dist/commands/report.js +2 -14
- package/dist/commands/sync-mode.d.ts +0 -11
- package/dist/commands/sync-mode.js +0 -30
- package/dist/fast-core.js +1 -1
- package/dist/{commands/scaffold-fast.d.ts → fast-scaffold.d.ts} +1 -1
- package/dist/{commands/scaffold-fast.js → fast-scaffold.js} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/lib/checkpoint-ontology/apply.d.ts +8 -0
- package/dist/lib/checkpoint-ontology/apply.js +33 -0
- package/dist/lib/checkpoint-ontology/plan.d.ts +15 -0
- package/dist/lib/checkpoint-ontology/plan.js +15 -0
- package/dist/lib/daemon/catalog.d.ts +2 -18
- package/dist/lib/daemon/catalog.js +2 -18
- package/dist/lib/daemon/config.d.ts +0 -6
- package/dist/lib/daemon/config.js +2 -15
- package/dist/lib/daemon/requests.d.ts +0 -4
- package/dist/lib/daemon/requests.js +0 -2
- package/dist/lib/daemon/run.js +19 -78
- package/dist/lib/daemon/supervisor.d.ts +0 -8
- package/dist/lib/daemon/supervisor.js +1 -101
- package/dist/lib/deploy-api.d.ts +6 -0
- package/dist/lib/deploy-api.js +21 -0
- package/dist/lib/doctor/checks/scaffold-cli-floor.d.ts +7 -0
- package/dist/lib/doctor/checks/scaffold-cli-floor.js +73 -0
- package/dist/lib/doctor/fix/apply.d.ts +17 -2
- package/dist/lib/doctor/fix/apply.js +111 -57
- package/dist/lib/doctor/fix/json-output.d.ts +9 -1
- package/dist/lib/doctor/fix/json-output.js +11 -0
- package/dist/lib/doctor/registry.js +2 -0
- package/dist/lib/first-sync-next-step.d.ts +37 -0
- package/dist/lib/first-sync-next-step.js +189 -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/hire-grant-dispatch.d.ts +6 -1
- package/dist/lib/lanes/hire-grant-dispatch.js +78 -3
- package/dist/lib/lanes/keep-alive.d.ts +3 -2
- package/dist/lib/lanes/keep-alive.js +2 -2
- package/dist/lib/lanes/merge-command.d.ts +1 -0
- package/dist/lib/lanes/merge-command.js +299 -33
- package/dist/lib/lanes/orphans.d.ts +7 -0
- package/dist/lib/lanes/orphans.js +39 -4
- package/dist/lib/lanes/tracker-store.d.ts +2 -18
- package/dist/lib/lanes/tracker-store.js +1 -172
- package/dist/lib/scaffold-cli-floor.d.ts +9 -0
- package/dist/lib/scaffold-cli-floor.js +50 -0
- package/dist/main.js +4 -1
- package/dist/utils/cli-telemetry.d.ts +7 -1
- package/dist/utils/cli-telemetry.js +84 -8
- package/dist/utils/contribution-table.d.ts +1 -1
- package/dist/utils/scaffold-fast-import-guard.d.ts +3 -0
- package/dist/utils/scaffold-fast-import-guard.js +28 -0
- package/dist/utils/scaffold-packlist-guard.d.ts +7 -0
- package/dist/utils/scaffold-packlist-guard.js +23 -0
- package/dist/utils/scaffold-sibling-references.d.ts +21 -0
- package/dist/utils/scaffold-sibling-references.js +156 -0
- package/package.json +2 -2
- package/dist/commands/lanes-import.d.ts +0 -14
- package/dist/commands/lanes-import.js +0 -1210
- package/dist/commands/lanes-tracker.d.ts +0 -18
- package/dist/commands/lanes-tracker.js +0 -452
- package/dist/lib/daemon/sync-runner.d.ts +0 -58
- package/dist/lib/daemon/sync-runner.js +0 -277
|
@@ -20,16 +20,12 @@
|
|
|
20
20
|
import type { UnitPlan } from "./catalog.js";
|
|
21
21
|
import type { MachineType } from "./machine-type.js";
|
|
22
22
|
import type { ControlRequest } from "./requests.js";
|
|
23
|
-
import { syncPassResult } from "./sync-runner.js";
|
|
24
23
|
export declare const BACKOFF_BASE_MS = 1000;
|
|
25
24
|
export declare const BACKOFF_MAX_MS = 60000;
|
|
26
25
|
export declare const HEALTHY_RUN_MS = 60000;
|
|
27
26
|
export declare const STOP_ESCALATION_MS = 10000;
|
|
28
27
|
export interface SupervisedChild {
|
|
29
28
|
pid?: number;
|
|
30
|
-
stdout?: {
|
|
31
|
-
on(event: "data" | "end", handler: (chunk?: Buffer | string) => void): unknown;
|
|
32
|
-
};
|
|
33
29
|
on(event: "exit", handler: (code: number | null, signal: string | null) => void): unknown;
|
|
34
30
|
on(event: "error", handler: (err: Error) => void): unknown;
|
|
35
31
|
kill(signal?: NodeJS.Signals | number): boolean;
|
|
@@ -53,8 +49,6 @@ export interface UnitState {
|
|
|
53
49
|
lastRunAt?: number;
|
|
54
50
|
nextRunAt?: number;
|
|
55
51
|
lastResult?: "ok" | "failed";
|
|
56
|
-
lastHeartbeatAt?: string;
|
|
57
|
-
lastPassResult?: ReturnType<typeof syncPassResult>;
|
|
58
52
|
}
|
|
59
53
|
/** An app that runs the daemon as its own child process instead of an OS unit. */
|
|
60
54
|
export type HostApp = "desktop";
|
|
@@ -85,8 +79,6 @@ export interface SupervisorDeps {
|
|
|
85
79
|
clearTimeout?: (handle: unknown) => void;
|
|
86
80
|
reconcileMs?: number;
|
|
87
81
|
requestPollMs?: number;
|
|
88
|
-
/** Read hq-cloud's lease status after it exits with the lease-conflict code. */
|
|
89
|
-
syncOwnerBlockReason?: () => string | undefined;
|
|
90
82
|
}
|
|
91
83
|
export interface Supervisor {
|
|
92
84
|
start(): void;
|
|
@@ -17,8 +17,6 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Processes, time, and state output are injected; `run.ts` wires the real ones.
|
|
19
19
|
*/
|
|
20
|
-
import { StringDecoder } from "node:string_decoder";
|
|
21
|
-
import { SYNC_RUNNER_LEASE_CONFLICT_EXIT, SYNC_RUNNER_LEASE_RETRY_MS, parseSyncRunnerProtocolLine, syncPassResult, } from "./sync-runner.js";
|
|
22
20
|
export const BACKOFF_BASE_MS = 1_000;
|
|
23
21
|
export const BACKOFF_MAX_MS = 60_000;
|
|
24
22
|
export const HEALTHY_RUN_MS = 60_000;
|
|
@@ -27,15 +25,7 @@ export function backoffMs(attempt) {
|
|
|
27
25
|
return Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** Math.max(0, attempt));
|
|
28
26
|
}
|
|
29
27
|
function planKey(plan) {
|
|
30
|
-
return JSON.stringify([
|
|
31
|
-
plan.kind,
|
|
32
|
-
plan.restart,
|
|
33
|
-
plan.intervalSec,
|
|
34
|
-
plan.initialDelaySec,
|
|
35
|
-
plan.heartbeatTimeoutMs,
|
|
36
|
-
plan.command,
|
|
37
|
-
plan.env,
|
|
38
|
-
]);
|
|
28
|
+
return JSON.stringify([plan.kind, plan.restart, plan.intervalSec, plan.initialDelaySec, plan.command, plan.env]);
|
|
39
29
|
}
|
|
40
30
|
function errorMessage(err) {
|
|
41
31
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -78,11 +68,6 @@ export function createSupervisor(deps) {
|
|
|
78
68
|
clearT(slot.timer);
|
|
79
69
|
slot.timer = undefined;
|
|
80
70
|
};
|
|
81
|
-
const clearHeartbeatTimer = (slot) => {
|
|
82
|
-
if (slot.heartbeatTimer !== undefined)
|
|
83
|
-
clearT(slot.heartbeatTimer);
|
|
84
|
-
slot.heartbeatTimer = undefined;
|
|
85
|
-
};
|
|
86
71
|
const setBlocked = (slot) => {
|
|
87
72
|
const block = slot.plan.blocked;
|
|
88
73
|
slot.state.status = block.state;
|
|
@@ -93,7 +78,6 @@ export function createSupervisor(deps) {
|
|
|
93
78
|
/** Stop the slot's child (if any), then run `then` once it has exited. */
|
|
94
79
|
const terminate = (slot, then) => {
|
|
95
80
|
clearTimer(slot);
|
|
96
|
-
clearHeartbeatTimer(slot);
|
|
97
81
|
const child = slot.child;
|
|
98
82
|
if (!child) {
|
|
99
83
|
then?.();
|
|
@@ -138,69 +122,6 @@ export function createSupervisor(deps) {
|
|
|
138
122
|
const watch = (slot, child) => {
|
|
139
123
|
live.add(child);
|
|
140
124
|
let ended = false;
|
|
141
|
-
const timeoutMs = slot.plan.heartbeatTimeoutMs;
|
|
142
|
-
const decoder = timeoutMs ? new StringDecoder("utf8") : null;
|
|
143
|
-
let stdoutRemainder = "";
|
|
144
|
-
let lastHeartbeatPersistedAt = now() - 15_000;
|
|
145
|
-
const armHeartbeatWatchdog = () => {
|
|
146
|
-
if (!timeoutMs)
|
|
147
|
-
return;
|
|
148
|
-
clearHeartbeatTimer(slot);
|
|
149
|
-
slot.heartbeatTimer = setT(() => {
|
|
150
|
-
slot.heartbeatTimer = undefined;
|
|
151
|
-
if (stopped || slot.child !== child || slot.expectingExit)
|
|
152
|
-
return;
|
|
153
|
-
deps.log("warn", `${slot.plan.id} emitted no sync protocol heartbeat for ${timeoutMs}ms; restarting it`);
|
|
154
|
-
terminate(slot, () => scheduleRestart(slot));
|
|
155
|
-
emit();
|
|
156
|
-
}, timeoutMs);
|
|
157
|
-
};
|
|
158
|
-
const acceptProtocolLine = (line) => {
|
|
159
|
-
let event;
|
|
160
|
-
try {
|
|
161
|
-
event = parseSyncRunnerProtocolLine(line);
|
|
162
|
-
}
|
|
163
|
-
catch (err) {
|
|
164
|
-
deps.log("error", `${slot.plan.id} emitted invalid sync protocol output (${Buffer.byteLength(line)} bytes): ${errorMessage(err)}`);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (!event)
|
|
168
|
-
return;
|
|
169
|
-
const at = now();
|
|
170
|
-
slot.state.lastHeartbeatAt = new Date(at).toISOString();
|
|
171
|
-
const pass = syncPassResult(event, slot.state.lastHeartbeatAt);
|
|
172
|
-
if (pass)
|
|
173
|
-
slot.state.lastPassResult = pass;
|
|
174
|
-
armHeartbeatWatchdog();
|
|
175
|
-
if (pass || at - lastHeartbeatPersistedAt >= 15_000) {
|
|
176
|
-
lastHeartbeatPersistedAt = at;
|
|
177
|
-
emit();
|
|
178
|
-
}
|
|
179
|
-
};
|
|
180
|
-
if (decoder && child.stdout) {
|
|
181
|
-
child.stdout.on("data", (chunk) => {
|
|
182
|
-
if (chunk === undefined)
|
|
183
|
-
return;
|
|
184
|
-
stdoutRemainder += decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
185
|
-
let newline = stdoutRemainder.indexOf("\n");
|
|
186
|
-
while (newline >= 0) {
|
|
187
|
-
acceptProtocolLine(stdoutRemainder.slice(0, newline).trim());
|
|
188
|
-
stdoutRemainder = stdoutRemainder.slice(newline + 1);
|
|
189
|
-
newline = stdoutRemainder.indexOf("\n");
|
|
190
|
-
}
|
|
191
|
-
// A malformed or unexpectedly large line is not a heartbeat. Bound
|
|
192
|
-
// retained data so a broken child cannot grow daemon memory forever.
|
|
193
|
-
if (Buffer.byteLength(stdoutRemainder) > 1024 * 1024)
|
|
194
|
-
stdoutRemainder = "";
|
|
195
|
-
});
|
|
196
|
-
child.stdout.on("end", () => {
|
|
197
|
-
stdoutRemainder += decoder.end();
|
|
198
|
-
if (stdoutRemainder)
|
|
199
|
-
acceptProtocolLine(stdoutRemainder.trim());
|
|
200
|
-
stdoutRemainder = "";
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
armHeartbeatWatchdog();
|
|
204
125
|
const onEnd = (code, signal) => {
|
|
205
126
|
if (ended)
|
|
206
127
|
return;
|
|
@@ -211,7 +132,6 @@ export function createSupervisor(deps) {
|
|
|
211
132
|
if (slot.child !== child)
|
|
212
133
|
return;
|
|
213
134
|
slot.child = undefined;
|
|
214
|
-
clearHeartbeatTimer(slot);
|
|
215
135
|
if (slot.killTimer !== undefined) {
|
|
216
136
|
clearT(slot.killTimer);
|
|
217
137
|
slot.killTimer = undefined;
|
|
@@ -239,20 +159,6 @@ export function createSupervisor(deps) {
|
|
|
239
159
|
emit();
|
|
240
160
|
return;
|
|
241
161
|
}
|
|
242
|
-
if (slot.plan.service === "sync" && code === SYNC_RUNNER_LEASE_CONFLICT_EXIT) {
|
|
243
|
-
const reason = deps.syncOwnerBlockReason?.() ?? "sync owned by another runner";
|
|
244
|
-
slot.state.status = "waiting";
|
|
245
|
-
slot.state.reason = reason;
|
|
246
|
-
slot.state.nextRunAt = now() + SYNC_RUNNER_LEASE_RETRY_MS;
|
|
247
|
-
deps.log("info", `${reason}; retrying in ${SYNC_RUNNER_LEASE_RETRY_MS}ms`);
|
|
248
|
-
slot.timer = setT(() => {
|
|
249
|
-
slot.timer = undefined;
|
|
250
|
-
startLong(slot);
|
|
251
|
-
emit();
|
|
252
|
-
}, SYNC_RUNNER_LEASE_RETRY_MS);
|
|
253
|
-
emit();
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
162
|
deps.log("warn", `${slot.plan.id} exited (code=${code ?? "null"} signal=${signal ?? "none"})`);
|
|
257
163
|
if (ranFor >= HEALTHY_RUN_MS)
|
|
258
164
|
slot.attempts = 0;
|
|
@@ -463,11 +369,6 @@ export function createSupervisor(deps) {
|
|
|
463
369
|
emit();
|
|
464
370
|
};
|
|
465
371
|
const handleRequest = (req) => {
|
|
466
|
-
if (req.action === "reconcile") {
|
|
467
|
-
deps.log("info", "reconciling daemon configuration (requested)");
|
|
468
|
-
reconcile();
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
372
|
if (req.action === "restart-daemon") {
|
|
472
373
|
deps.log("info", "restarting the daemon (requested)");
|
|
473
374
|
if (deps.restartDaemon)
|
|
@@ -574,7 +475,6 @@ export function createSupervisor(deps) {
|
|
|
574
475
|
clearT(requestTimer);
|
|
575
476
|
for (const slot of slots.values()) {
|
|
576
477
|
clearTimer(slot);
|
|
577
|
-
clearHeartbeatTimer(slot);
|
|
578
478
|
slot.child = undefined;
|
|
579
479
|
if (slot.killTimer !== undefined)
|
|
580
480
|
clearT(slot.killTimer);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Shared authenticated request path for the hq-deploy control plane. */
|
|
2
|
+
export declare const DEPLOY_API_URL: string;
|
|
3
|
+
export declare function deployHeaders(token: string, json?: boolean, extra?: Record<string, string>): Record<string, string>;
|
|
4
|
+
export declare function deployApiUrl(route: string): string;
|
|
5
|
+
export declare function deployApiRequest(token: string, route: string, method: string, body?: unknown, extraHeaders?: Record<string, string>): Promise<Response>;
|
|
6
|
+
//# sourceMappingURL=deploy-api.d.ts.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Shared authenticated request path for the hq-deploy control plane. */
|
|
2
|
+
export const DEPLOY_API_URL = process.env.HQ_DEPLOY_API_URL?.trim() || "https://api.indigo-hq.com";
|
|
3
|
+
export function deployHeaders(token, json = true, extra = {}) {
|
|
4
|
+
return {
|
|
5
|
+
Authorization: `Bearer ${token}`,
|
|
6
|
+
Accept: "application/json",
|
|
7
|
+
...(json ? { "Content-Type": "application/json" } : {}),
|
|
8
|
+
...extra,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function deployApiUrl(route) {
|
|
12
|
+
return `${DEPLOY_API_URL.replace(/\/+$/, "")}${route}`;
|
|
13
|
+
}
|
|
14
|
+
export async function deployApiRequest(token, route, method, body, extraHeaders = {}) {
|
|
15
|
+
return fetch(deployApiUrl(route), {
|
|
16
|
+
method,
|
|
17
|
+
headers: deployHeaders(token, body !== undefined, extraHeaders),
|
|
18
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=deploy-api.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
|
|
2
|
+
export declare const SCAFFOLD_CLI_FLOOR_FAMILY_ID = "scaffold-cli-floor";
|
|
3
|
+
export declare const SCAFFOLD_CLI_FLOOR_FAMILY_TITLE = "Scaffold CLI compatibility";
|
|
4
|
+
export declare const SCAFFOLD_CLI_FLOOR_CHECK_ID = "scaffold-cli-floor.requires-hq-cli";
|
|
5
|
+
export declare const scaffoldCliFloorFamily: CheckFamily;
|
|
6
|
+
export declare function checkScaffoldCliFloor(context: CheckContext): CheckResult[];
|
|
7
|
+
//# sourceMappingURL=scaffold-cli-floor.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import * as yaml from "js-yaml";
|
|
4
|
+
import { CLI_VERSION } from "../../../cli-version.js";
|
|
5
|
+
import { compareScaffoldCliFloor } from "../../scaffold-cli-floor.js";
|
|
6
|
+
export const SCAFFOLD_CLI_FLOOR_FAMILY_ID = "scaffold-cli-floor";
|
|
7
|
+
export const SCAFFOLD_CLI_FLOOR_FAMILY_TITLE = "Scaffold CLI compatibility";
|
|
8
|
+
export const SCAFFOLD_CLI_FLOOR_CHECK_ID = "scaffold-cli-floor.requires-hq-cli";
|
|
9
|
+
export const scaffoldCliFloorFamily = {
|
|
10
|
+
id: SCAFFOLD_CLI_FLOOR_FAMILY_ID,
|
|
11
|
+
title: SCAFFOLD_CLI_FLOOR_FAMILY_TITLE,
|
|
12
|
+
run: (context) => Promise.resolve(checkScaffoldCliFloor(context)),
|
|
13
|
+
};
|
|
14
|
+
export function checkScaffoldCliFloor(context) {
|
|
15
|
+
const coreYamlPath = path.join(context.hqRoot, "core", "core.yaml");
|
|
16
|
+
if (!fs.existsSync(coreYamlPath)) {
|
|
17
|
+
return [skipped(coreYamlPath, "core/core.yaml is missing")];
|
|
18
|
+
}
|
|
19
|
+
let document;
|
|
20
|
+
try {
|
|
21
|
+
document = yaml.load(fs.readFileSync(coreYamlPath, "utf8"));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return [
|
|
25
|
+
{
|
|
26
|
+
status: "WARN",
|
|
27
|
+
checkId: SCAFFOLD_CLI_FLOOR_CHECK_ID,
|
|
28
|
+
target: coreYamlPath,
|
|
29
|
+
message: "core/core.yaml could not be parsed, so the hq-cli floor was not checked.",
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
}
|
|
33
|
+
if (!document || typeof document !== "object" || Array.isArray(document)) {
|
|
34
|
+
if (document === undefined || document === null) {
|
|
35
|
+
return [skipped(coreYamlPath, "core/core.yaml does not declare requiresHqCli")];
|
|
36
|
+
}
|
|
37
|
+
return [
|
|
38
|
+
{
|
|
39
|
+
status: "WARN",
|
|
40
|
+
checkId: SCAFFOLD_CLI_FLOOR_CHECK_ID,
|
|
41
|
+
target: coreYamlPath,
|
|
42
|
+
message: "core/core.yaml is not a mapping, so the hq-cli floor was not checked.",
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
const required = document.requiresHqCli;
|
|
47
|
+
const comparison = compareScaffoldCliFloor(required, CLI_VERSION);
|
|
48
|
+
if (comparison.status === "skip") {
|
|
49
|
+
return [skipped(coreYamlPath, "core/core.yaml does not declare requiresHqCli")];
|
|
50
|
+
}
|
|
51
|
+
return [
|
|
52
|
+
{
|
|
53
|
+
status: comparison.status === "pass"
|
|
54
|
+
? "PASS"
|
|
55
|
+
: comparison.status === "fail"
|
|
56
|
+
? "FAIL"
|
|
57
|
+
: "WARN",
|
|
58
|
+
checkId: SCAFFOLD_CLI_FLOOR_CHECK_ID,
|
|
59
|
+
target: coreYamlPath,
|
|
60
|
+
message: comparison.message,
|
|
61
|
+
...(comparison.remediation ? { remediation: comparison.remediation } : {}),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
function skipped(target, reason) {
|
|
66
|
+
return {
|
|
67
|
+
status: "NA",
|
|
68
|
+
checkId: SCAFFOLD_CLI_FLOOR_CHECK_ID,
|
|
69
|
+
target,
|
|
70
|
+
message: `Skipped: ${reason}; no hq-cli floor was checked.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=scaffold-cli-floor.js.map
|
|
@@ -101,6 +101,8 @@ export interface RefreshSyncResult {
|
|
|
101
101
|
export declare const REFRESH_SYNC_TIMEOUT_MS = 90000;
|
|
102
102
|
/** One applied (or attempted) repair, with its post-fix re-check status. */
|
|
103
103
|
export interface AppliedFix {
|
|
104
|
+
/** One-based repair pass in this `hq doctor --fix` invocation. */
|
|
105
|
+
pass: number;
|
|
104
106
|
/** The check id of the finding that was repaired. */
|
|
105
107
|
checkId: string;
|
|
106
108
|
/** The safe class applied. */
|
|
@@ -114,13 +116,24 @@ export interface AppliedFix {
|
|
|
114
116
|
/** Present for refresh-sync plans; contains no command output or path data. */
|
|
115
117
|
refreshSyncOutcome?: RefreshSyncOutcome;
|
|
116
118
|
}
|
|
119
|
+
/** Backup and restore information for one pass that wrote files. */
|
|
120
|
+
export interface AppliedFixBackup {
|
|
121
|
+
/** One-based repair pass that created this backup. */
|
|
122
|
+
pass: number;
|
|
123
|
+
/** Absolute path of the backup directory. */
|
|
124
|
+
backupDir: string;
|
|
125
|
+
/** Command that restores this pass's pre-write files. */
|
|
126
|
+
restoreCommand: string;
|
|
127
|
+
}
|
|
128
|
+
/** A newly unlocked safe repair can be picked up for at most this many passes. */
|
|
129
|
+
export declare const MAX_FIX_PASSES = 3;
|
|
117
130
|
/** The outcome of an {@link applyFixes} run. */
|
|
118
131
|
export interface ApplyFixesResult {
|
|
119
|
-
/** 0 on success or a clean no-op; 1 when
|
|
132
|
+
/** 0 on success, decline, or a clean no-op; 1 when a dirty tree is refused. */
|
|
120
133
|
exitCode: number;
|
|
121
134
|
/** Whether any file was written. */
|
|
122
135
|
wrote: boolean;
|
|
123
|
-
/** Why
|
|
136
|
+
/** Why no further repairs were applied, when the run stopped early. */
|
|
124
137
|
refused: "dirty-tree" | "declined" | null;
|
|
125
138
|
/** How many findings were auto-fixable. */
|
|
126
139
|
fixableCount: number;
|
|
@@ -128,6 +141,8 @@ export interface ApplyFixesResult {
|
|
|
128
141
|
backupDir: string | null;
|
|
129
142
|
/** The restore command, or null when nothing was written. */
|
|
130
143
|
restoreCommand: string | null;
|
|
144
|
+
/** Every backup made, in pass order. */
|
|
145
|
+
backups: AppliedFixBackup[];
|
|
131
146
|
/** The applied repairs with their post-fix status. */
|
|
132
147
|
applied: AppliedFix[];
|
|
133
148
|
}
|
|
@@ -45,6 +45,8 @@ import { classifyRefreshSyncOutcome, } from "./refresh-sync-outcome.js";
|
|
|
45
45
|
export const HOOK_CONFIG_DIRS = [".claude", ".codex", ".grok"];
|
|
46
46
|
/** Per-company bound for the default `hq sync pull` repair. */
|
|
47
47
|
export const REFRESH_SYNC_TIMEOUT_MS = 90_000;
|
|
48
|
+
/** A newly unlocked safe repair can be picked up for at most this many passes. */
|
|
49
|
+
export const MAX_FIX_PASSES = 3;
|
|
48
50
|
/**
|
|
49
51
|
* Apply every auto-fixable finding, honouring the dirty-tree refusal, the
|
|
50
52
|
* preview/confirmation gate, the pre-write backup, and the post-fix re-check.
|
|
@@ -62,6 +64,7 @@ export async function applyFixes(options) {
|
|
|
62
64
|
fixableCount: 0,
|
|
63
65
|
backupDir: null,
|
|
64
66
|
restoreCommand: null,
|
|
67
|
+
backups: [],
|
|
65
68
|
applied: [],
|
|
66
69
|
...over,
|
|
67
70
|
});
|
|
@@ -80,72 +83,123 @@ export async function applyFixes(options) {
|
|
|
80
83
|
const collect = options.runChecks ?? defaultRunChecks;
|
|
81
84
|
const refreshSync = options.refreshSync ?? defaultRefreshSync;
|
|
82
85
|
const rewriteBotNodePath = options.rewriteBotNodePath ?? defaultRewriteBotNodePath;
|
|
83
|
-
// 2. Collect findings
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
86
|
+
// 2. Collect findings, then plan each newly unlocked allowlisted repair.
|
|
87
|
+
// Identity is deliberately the check id + target pair: an unsuccessful
|
|
88
|
+
// repair must not be applied repeatedly in one invocation.
|
|
89
|
+
let findings = await collect(hqRoot);
|
|
90
|
+
const appliedKeys = new Set();
|
|
91
|
+
const planned = [];
|
|
92
|
+
const backups = [];
|
|
93
|
+
let fixableCount = 0;
|
|
94
|
+
let refused = null;
|
|
95
|
+
let lastBackupMs = Number.NEGATIVE_INFINITY;
|
|
96
|
+
const backupClock = () => {
|
|
97
|
+
const suppliedMs = (options.now ?? (() => new Date()))().getTime();
|
|
98
|
+
lastBackupMs = Math.max(suppliedMs, lastBackupMs + 1);
|
|
99
|
+
return new Date(lastBackupMs);
|
|
100
|
+
};
|
|
101
|
+
const buildPlans = (results) => {
|
|
102
|
+
const keysThisPass = new Set();
|
|
103
|
+
const plans = [];
|
|
104
|
+
for (const result of results) {
|
|
105
|
+
const rem = deriveRemediation(result);
|
|
106
|
+
if (!rem?.autoFixable)
|
|
107
|
+
continue;
|
|
108
|
+
const plan = planFix(hqRoot, result, rem, refreshSync, rewriteBotNodePath);
|
|
109
|
+
if (!plan)
|
|
110
|
+
continue;
|
|
111
|
+
const key = JSON.stringify([plan.checkId, plan.target]);
|
|
112
|
+
if (appliedKeys.has(key) || keysThisPass.has(key))
|
|
113
|
+
continue;
|
|
114
|
+
keysThisPass.add(key);
|
|
115
|
+
plans.push(plan);
|
|
116
|
+
}
|
|
117
|
+
return plans;
|
|
118
|
+
};
|
|
119
|
+
for (let pass = 1; pass <= MAX_FIX_PASSES; pass += 1) {
|
|
120
|
+
const plans = buildPlans(findings);
|
|
121
|
+
if (plans.length === 0)
|
|
122
|
+
break;
|
|
123
|
+
fixableCount += plans.length;
|
|
124
|
+
// 3. Preview and require confirmation for every pass unless --yes is set.
|
|
125
|
+
write(`Pass ${pass}/${MAX_FIX_PASSES}: hq doctor --fix will apply ${plans.length} repair${plans.length === 1 ? "" : "s"}:\n\n`);
|
|
126
|
+
for (const plan of plans)
|
|
127
|
+
write(plan.preview + "\n");
|
|
128
|
+
if (!options.yes) {
|
|
129
|
+
const confirmed = await Promise.resolve((options.confirm ?? declineByDefault)());
|
|
130
|
+
if (!confirmed) {
|
|
131
|
+
refused = "declined";
|
|
132
|
+
if (planned.length === 0) {
|
|
133
|
+
write("Aborted — no changes written.\n");
|
|
134
|
+
return empty({ refused, fixableCount });
|
|
135
|
+
}
|
|
136
|
+
write(`Pass ${pass} declined — no further changes written.\n`);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// 4. Back up this pass's files before its first write. The clock advances
|
|
141
|
+
// at least one millisecond per backup so fast consecutive passes cannot
|
|
142
|
+
// overwrite an earlier pass's restore point.
|
|
143
|
+
const affected = unique(plans.map((plan) => plan.relpath).filter((rel) => rel.length > 0));
|
|
144
|
+
const backup = affected.length > 0 ? createBackup(hqRoot, affected, backupClock) : null;
|
|
145
|
+
if (backup) {
|
|
146
|
+
backups.push({
|
|
147
|
+
pass,
|
|
148
|
+
backupDir: backup.dir,
|
|
149
|
+
restoreCommand: backup.restoreCommand,
|
|
150
|
+
});
|
|
151
|
+
write(`\nBacked up ${backup.files.length} file${backup.files.length === 1 ? "" : "s"} for pass ${pass} to ${backup.dir}\n`);
|
|
152
|
+
write(`To restore: ${backup.restoreCommand}\n`);
|
|
111
153
|
}
|
|
154
|
+
// 5. Apply each plan once. Later findings with this check id + target are
|
|
155
|
+
// excluded even if this repair did not make its check pass.
|
|
156
|
+
for (const plan of plans) {
|
|
157
|
+
appliedKeys.add(JSON.stringify([plan.checkId, plan.target]));
|
|
158
|
+
planned.push({ plan, pass });
|
|
159
|
+
await plan.apply();
|
|
160
|
+
}
|
|
161
|
+
// 6. Re-run checks. Newly surfaced repairs are considered by the next loop.
|
|
162
|
+
findings = await collect(hqRoot);
|
|
112
163
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const affected = unique(plans.map((plan) => plan.relpath).filter((rel) => rel.length > 0));
|
|
117
|
-
const backup = affected.length > 0 ? createBackup(hqRoot, affected, options.now) : null;
|
|
118
|
-
// 5. Apply. Each plan reads the current on-disk state, so multiple plans that
|
|
119
|
-
// touch the same file (two gate ids) compose correctly.
|
|
120
|
-
for (const plan of plans)
|
|
121
|
-
await plan.apply();
|
|
122
|
-
if (backup) {
|
|
123
|
-
write(`\nBacked up ${backup.files.length} file${backup.files.length === 1 ? "" : "s"} to ${backup.dir}\n`);
|
|
124
|
-
write(`To restore: ${backup.restoreCommand}\n`);
|
|
164
|
+
if (planned.length === 0) {
|
|
165
|
+
write("hq doctor --fix: nothing to repair — no auto-fixable findings.\n");
|
|
166
|
+
return empty({ refused, fixableCount });
|
|
125
167
|
}
|
|
126
|
-
//
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
: {}),
|
|
137
|
-
|
|
168
|
+
// Show each repair's final post-fix status, including work from earlier passes.
|
|
169
|
+
const applied = planned.map(({ plan, pass }) => {
|
|
170
|
+
const refreshSyncOutcome = plan.refreshSyncOutcome?.();
|
|
171
|
+
return {
|
|
172
|
+
pass,
|
|
173
|
+
checkId: plan.checkId,
|
|
174
|
+
fixClass: plan.fixClass,
|
|
175
|
+
target: plan.target,
|
|
176
|
+
summary: plan.summary,
|
|
177
|
+
postStatus: plan.postStatus(findings),
|
|
178
|
+
...(refreshSyncOutcome ? { refreshSyncOutcome } : {}),
|
|
179
|
+
};
|
|
180
|
+
});
|
|
138
181
|
write("\nPost-fix status:\n");
|
|
139
182
|
for (const fix of applied) {
|
|
140
|
-
write(` ${fix.postStatus ?? "UNKNOWN"} ${fix.checkId} [${fix.target}]\n`);
|
|
183
|
+
write(` ${fix.postStatus ?? "UNKNOWN"} ${fix.checkId} [${fix.target}] (pass ${fix.pass})\n`);
|
|
184
|
+
}
|
|
185
|
+
// Restore later pass snapshots first. This also returns every file to its
|
|
186
|
+
// pre-run state when multiple passes touched the same path.
|
|
187
|
+
const restoreCommand = backups.length > 0
|
|
188
|
+
? [...backups].reverse().map(({ restoreCommand: command }) => command).join(" && ")
|
|
189
|
+
: null;
|
|
190
|
+
if (restoreCommand)
|
|
191
|
+
write(`To restore all changes: ${restoreCommand}\n`);
|
|
192
|
+
if (planned.at(-1)?.pass === MAX_FIX_PASSES && buildPlans(findings).length > 0) {
|
|
193
|
+
write(`Stopped after ${MAX_FIX_PASSES} repair passes; new auto-fixable findings remain.\n`);
|
|
141
194
|
}
|
|
142
195
|
return {
|
|
143
196
|
exitCode: 0,
|
|
144
197
|
wrote: true,
|
|
145
|
-
refused
|
|
146
|
-
fixableCount
|
|
147
|
-
backupDir:
|
|
148
|
-
restoreCommand
|
|
198
|
+
refused,
|
|
199
|
+
fixableCount,
|
|
200
|
+
backupDir: backups[0]?.backupDir ?? null,
|
|
201
|
+
restoreCommand,
|
|
202
|
+
backups,
|
|
149
203
|
applied,
|
|
150
204
|
};
|
|
151
205
|
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import type { AppliedFix, ApplyFixesResult } from "./apply.js";
|
|
1
|
+
import type { AppliedFix, AppliedFixBackup, ApplyFixesResult } from "./apply.js";
|
|
2
2
|
export declare const DOCTOR_FIX_JSON_SCHEMA_VERSION = 1;
|
|
3
3
|
export interface DoctorFixJsonResult {
|
|
4
4
|
schemaVersion: number;
|
|
5
5
|
exitCode: number;
|
|
6
6
|
refused: ApplyFixesResult["refused"] | "outside-hq-tree";
|
|
7
|
+
applied: {
|
|
8
|
+
checkId: string;
|
|
9
|
+
fixClass: AppliedFix["fixClass"];
|
|
10
|
+
pass: number;
|
|
11
|
+
postStatus: AppliedFix["postStatus"];
|
|
12
|
+
}[];
|
|
13
|
+
backups: Pick<AppliedFixBackup, "pass" | "backupDir" | "restoreCommand">[];
|
|
7
14
|
refreshSync: {
|
|
8
15
|
checkId: string;
|
|
9
16
|
outcome: NonNullable<AppliedFix["refreshSyncOutcome"]>;
|
|
@@ -15,6 +22,7 @@ export declare function buildDoctorFixJson(input: {
|
|
|
15
22
|
exitCode: number;
|
|
16
23
|
refused: ApplyFixesResult["refused"] | "outside-hq-tree";
|
|
17
24
|
applied: readonly AppliedFix[];
|
|
25
|
+
backups?: readonly AppliedFixBackup[];
|
|
18
26
|
}): DoctorFixJsonResult;
|
|
19
27
|
export declare function renderDoctorFixJson(input: Parameters<typeof buildDoctorFixJson>[0]): string;
|
|
20
28
|
//# sourceMappingURL=json-output.d.ts.map
|
|
@@ -5,6 +5,17 @@ export function buildDoctorFixJson(input) {
|
|
|
5
5
|
schemaVersion: DOCTOR_FIX_JSON_SCHEMA_VERSION,
|
|
6
6
|
exitCode: input.exitCode,
|
|
7
7
|
refused: input.refused,
|
|
8
|
+
applied: input.applied.map(({ checkId, fixClass, pass, postStatus }) => ({
|
|
9
|
+
checkId,
|
|
10
|
+
fixClass,
|
|
11
|
+
pass,
|
|
12
|
+
postStatus,
|
|
13
|
+
})),
|
|
14
|
+
backups: (input.backups ?? []).map(({ pass, backupDir, restoreCommand }) => ({
|
|
15
|
+
pass,
|
|
16
|
+
backupDir,
|
|
17
|
+
restoreCommand,
|
|
18
|
+
})),
|
|
8
19
|
refreshSync: input.applied.flatMap((fix) => fix.fixClass === "refresh-sync" && fix.refreshSyncOutcome
|
|
9
20
|
? [
|
|
10
21
|
{
|
|
@@ -23,6 +23,7 @@ import { integrationsFamily } from "./checks/integrations.js";
|
|
|
23
23
|
import { syncHealthFamily } from "./checks/sync-health.js";
|
|
24
24
|
import { workContextFamily } from "./checks/work-context.js";
|
|
25
25
|
import { botsFamily } from "./checks/bots.js";
|
|
26
|
+
import { scaffoldCliFloorFamily } from "./checks/scaffold-cli-floor.js";
|
|
26
27
|
import { fixtureCoverageFamily } from "./fixtures/discover.js";
|
|
27
28
|
import { checkClaudeWiring } from "./checks/claude-wiring.js";
|
|
28
29
|
/**
|
|
@@ -236,6 +237,7 @@ function hasCommandHook(settings, event) {
|
|
|
236
237
|
export function createDefaultRegistry() {
|
|
237
238
|
const registry = new DoctorRegistry();
|
|
238
239
|
registry.register(hooksFamily);
|
|
240
|
+
registry.register(scaffoldCliFloorFamily);
|
|
239
241
|
// Fixture coverage (US-007): reports UNTESTED for every registered hook with
|
|
240
242
|
// no fixture and prints the tested/total coverage line. A separate family so
|
|
241
243
|
// it needs no change to the hooks tier, per the registry's extensibility
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const CLI_FIRST_SYNC_NEXT_STEP_FLAG = "cli.first-sync-next-step";
|
|
2
|
+
export declare const FIRST_SYNC_NEXT_STEP_COPY = "Save a file in your HQ folder, then run `hq sync push`.";
|
|
3
|
+
export declare const FIRST_SYNC_CONFIRMATION_COPY = "Your first work sync is complete.";
|
|
4
|
+
export type FirstSyncStepOutcome = "shown" | "followed";
|
|
5
|
+
export declare function shouldShowFirstSyncNextStep(input: {
|
|
6
|
+
flagEnabled: boolean;
|
|
7
|
+
interactive: boolean;
|
|
8
|
+
alreadyShown: boolean;
|
|
9
|
+
alreadyCompleted: boolean;
|
|
10
|
+
}): boolean;
|
|
11
|
+
export declare function shouldConfirmFirstWorkBearingSync(input: {
|
|
12
|
+
flagEnabled: boolean;
|
|
13
|
+
interactive: boolean;
|
|
14
|
+
alreadyCompleted: boolean;
|
|
15
|
+
workBearing: boolean;
|
|
16
|
+
}): boolean;
|
|
17
|
+
export declare function isWorkBearingSyncResult(result: Record<string, unknown>): boolean;
|
|
18
|
+
export declare function isHumanInteractiveCliSession(input: {
|
|
19
|
+
stdoutIsTTY: boolean;
|
|
20
|
+
argv: readonly string[];
|
|
21
|
+
env: NodeJS.ProcessEnv;
|
|
22
|
+
machineIdentity: boolean;
|
|
23
|
+
}): boolean;
|
|
24
|
+
export declare function firstSyncStepMarkerPaths(identity: string, home?: string): {
|
|
25
|
+
shown: string;
|
|
26
|
+
completed: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function claimFirstSyncStepMarker(kind: "shown" | "completed", identity: string, home?: string): boolean;
|
|
29
|
+
export declare function firstSyncStepTelemetryProperties(outcome: FirstSyncStepOutcome): Record<string, unknown>;
|
|
30
|
+
export declare function isFirstSyncNextStepEnabled(): boolean;
|
|
31
|
+
/** Called once after the first completed CLI action, before process finalization. */
|
|
32
|
+
export declare function maybeShowFirstSyncNextStep(): boolean;
|
|
33
|
+
/** Capture the first positive CLI sync result without changing transfer logic. */
|
|
34
|
+
export declare function recordFirstWorkBearingSync(result: Record<string, unknown>): boolean;
|
|
35
|
+
/** Drain outcomes once so each shown/followed event is sent at most once per run. */
|
|
36
|
+
export declare function takeFirstSyncStepOutcomes(): FirstSyncStepOutcome[];
|
|
37
|
+
//# sourceMappingURL=first-sync-next-step.d.ts.map
|