@indigoai-us/hq-cli 5.345.32 → 5.345.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/command-catalog.generated.d.ts +92 -3
- package/dist/command-catalog.generated.js +111 -3
- package/dist/commands/cloud-provision.d.ts +2 -0
- package/dist/commands/db-dump.d.ts +8 -0
- package/dist/commands/db-dump.js +50 -0
- package/dist/commands/db-provision.d.ts +3 -10
- package/dist/commands/db-provision.js +26 -47
- package/dist/commands/db-remote-deps.d.ts +18 -0
- package/dist/commands/db-remote-deps.js +59 -0
- package/dist/commands/db-sql.d.ts +2 -1
- package/dist/commands/db-sql.js +30 -5
- package/dist/commands/db-usage.d.ts +7 -0
- package/dist/commands/db-usage.js +28 -0
- package/dist/commands/db.js +4 -0
- package/dist/commands/files.js +33 -5
- package/dist/commands/lanes-message.js +40 -8
- package/dist/commands/lanes-questions.d.ts +6 -1
- package/dist/commands/lanes-questions.js +19 -7
- package/dist/commands/lanes-resume.js +17 -6
- package/dist/commands/lanes-shared.d.ts +6 -1
- package/dist/commands/lanes.js +92 -15
- package/dist/commands/mesh.js +150 -26
- package/dist/commands/reindex.js +11 -8
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +68 -604
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/daemon/remote-control.js +62 -3
- package/dist/lib/db/control-plane.d.ts +32 -0
- package/dist/lib/db/control-plane.js +50 -0
- package/dist/lib/db/redact.d.ts +4 -0
- package/dist/lib/db/redact.js +17 -0
- package/dist/lib/db/remote-engine.d.ts +67 -0
- package/dist/lib/db/remote-engine.js +165 -0
- package/dist/lib/db/usage-format.d.ts +4 -0
- package/dist/lib/db/usage-format.js +41 -0
- package/dist/lib/index-render/shared.d.ts +3 -1
- package/dist/lib/index-render/shared.js +5 -2
- package/dist/lib/lanes/depth-cap.d.ts +7 -0
- package/dist/lib/lanes/depth-cap.js +41 -0
- package/dist/lib/lanes/keep-alive.d.ts +12 -0
- package/dist/lib/lanes/keep-alive.js +25 -6
- package/dist/lib/lanes/reconcile.d.ts +1 -1
- package/dist/lib/lanes/reconcile.js +16 -5
- package/dist/lib/mesh/api.d.ts +2 -0
- package/dist/lib/mesh/live/daemon/install.d.ts +7 -4
- package/dist/lib/mesh/live/daemon/install.js +144 -47
- package/dist/lib/mesh/project-board.d.ts +17 -0
- package/dist/lib/mesh/project-board.js +28 -0
- package/dist/lib/mesh/story-status.d.ts +16 -0
- package/dist/lib/mesh/story-status.js +32 -7
- package/dist/utils/self-update.d.ts +2 -0
- package/dist/utils/self-update.js +61 -23
- package/dist/utils/version-check.d.ts +1 -1
- package/dist/utils/version-check.js +2 -2
- package/dist/utils/version-gate.d.ts +21 -14
- package/dist/utils/version-gate.js +119 -28
- package/package.json +4 -1
|
@@ -187,6 +187,10 @@ function retiredLaneRegistryPath(hqRoot) {
|
|
|
187
187
|
}
|
|
188
188
|
/** Missing registry means no retired lanes; every present invalid file fails closed. */
|
|
189
189
|
export function loadRetiredLaneIds(hqRoot) {
|
|
190
|
+
return new Set(loadRetiredLaneEntries(hqRoot).map((entry) => entry.lane));
|
|
191
|
+
}
|
|
192
|
+
/** Preserve registry metadata for diagnostics while validating the same shape as the eligibility reader. */
|
|
193
|
+
export function loadRetiredLaneEntries(hqRoot) {
|
|
190
194
|
const filePath = retiredLaneRegistryPath(hqRoot);
|
|
191
195
|
let text;
|
|
192
196
|
try {
|
|
@@ -198,7 +202,7 @@ export function loadRetiredLaneIds(hqRoot) {
|
|
|
198
202
|
}
|
|
199
203
|
catch (err) {
|
|
200
204
|
if (err.code === "ENOENT")
|
|
201
|
-
return
|
|
205
|
+
return [];
|
|
202
206
|
throw new LanesError("keep_alive_retired_list_invalid", `Cannot read retired lane list ${filePath}: ${err instanceof Error ? err.message : String(err)}`, { path: filePath });
|
|
203
207
|
}
|
|
204
208
|
let parsed;
|
|
@@ -213,21 +217,26 @@ export function loadRetiredLaneIds(hqRoot) {
|
|
|
213
217
|
}
|
|
214
218
|
const retired = parsed.retired;
|
|
215
219
|
if (retired === undefined)
|
|
216
|
-
return
|
|
220
|
+
return [];
|
|
217
221
|
if (!Array.isArray(retired))
|
|
218
222
|
throw invalidRetiredList(filePath, "retired must be an array");
|
|
219
|
-
const
|
|
223
|
+
const entries = [];
|
|
220
224
|
for (const [index, entry] of retired.entries()) {
|
|
221
225
|
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
222
226
|
throw invalidRetiredList(filePath, `retired[${index}] must be an object with a lane id`);
|
|
223
227
|
}
|
|
224
|
-
const
|
|
228
|
+
const row = entry;
|
|
229
|
+
const laneId = row.lane;
|
|
225
230
|
if (typeof laneId !== "string" || laneId.trim() === "") {
|
|
226
231
|
throw invalidRetiredList(filePath, `retired[${index}].lane must be a non-empty string`);
|
|
227
232
|
}
|
|
228
|
-
|
|
233
|
+
entries.push({
|
|
234
|
+
lane: laneId.trim(),
|
|
235
|
+
index,
|
|
236
|
+
...(typeof row.retired_at === "string" ? { retired_at: row.retired_at } : {}),
|
|
237
|
+
});
|
|
229
238
|
}
|
|
230
|
-
return
|
|
239
|
+
return entries;
|
|
231
240
|
}
|
|
232
241
|
function invalidRetiredList(filePath, message) {
|
|
233
242
|
return new LanesError("keep_alive_retired_list_invalid", `Invalid retired lane list ${filePath}: ${message}`, { path: filePath });
|
|
@@ -242,6 +251,16 @@ function isRetiredLaneId(laneId, retiredLaneIds) {
|
|
|
242
251
|
}
|
|
243
252
|
return false;
|
|
244
253
|
}
|
|
254
|
+
/** Return the matching registry row using the same bare/host-suffixed ID semantics as keepAliveExclusion. */
|
|
255
|
+
export function findRetiredLaneEntry(hqRoot, laneId) {
|
|
256
|
+
const entry = loadRetiredLaneEntries(hqRoot).find((candidate) => matchesRetiredLaneEntry(candidate, laneId));
|
|
257
|
+
return entry === undefined ? undefined : { ...entry, path: retiredLaneRegistryPath(hqRoot) };
|
|
258
|
+
}
|
|
259
|
+
export function matchesRetiredLaneEntry(entry, laneId) {
|
|
260
|
+
return entry.lane === laneId ||
|
|
261
|
+
entry.lane.startsWith(`${laneId}_`) ||
|
|
262
|
+
laneId.startsWith(`${entry.lane}_`);
|
|
263
|
+
}
|
|
245
264
|
export function keepAliveSupersededBy(lane, lanes, lineageAware = LANE_SUPERSESSION_LINEAGE_AWARE_DEFAULT_ENABLED, companyForLane) {
|
|
246
265
|
const createdAt = Date.parse(lane.created_at);
|
|
247
266
|
if (!Number.isFinite(createdAt))
|
|
@@ -132,7 +132,7 @@ export declare function timeoutPath(hqRoot: string, laneId: string): string;
|
|
|
132
132
|
export declare function laneRunsHere(lane: LaneRecord, host: ReconcileHost): boolean;
|
|
133
133
|
export declare function agedAdvanceCount(question: QuestionRecord): number;
|
|
134
134
|
export declare function currentHopStartedAtMs(question: QuestionRecord): number;
|
|
135
|
-
export declare function shouldAgeQuestion(question: QuestionRecord, audience: SeniorRef, nowMs: number, ageWindowMs?: number): boolean;
|
|
135
|
+
export declare function shouldAgeQuestion(question: QuestionRecord, audience: SeniorRef, nowMs: number, ageWindowMs?: number, maxAgedAdvances?: number): boolean;
|
|
136
136
|
export declare function shouldAdvanceAudienceEnded(audience: SeniorRef, liveness: LivenessState | null): boolean;
|
|
137
137
|
export declare function readTimeoutDeadline(hqRoot: string, laneId: string): {
|
|
138
138
|
deadline_at: string;
|
|
@@ -14,6 +14,7 @@ import { effectiveAccountHome } from "./account-home.js";
|
|
|
14
14
|
import { enforceRegistryApprovalGate } from "./approval-gate.js";
|
|
15
15
|
import { loadLanesConfig, providerQuotaDefaultHoldMs } from "./config.js";
|
|
16
16
|
import { MAX_LANE_DEPTH } from "./constants.js";
|
|
17
|
+
import { laneDepthCap } from "./depth-cap.js";
|
|
17
18
|
import { AccountResolutionError, LanesError, isLaneVersionConflict } from "./errors.js";
|
|
18
19
|
import { loadWorkerProfile } from "./profile.js";
|
|
19
20
|
import { terminalChildResumeHireGrantError, withHireGrantAdmission, } from "./grants.js";
|
|
@@ -155,12 +156,12 @@ export function currentHopStartedAtMs(question) {
|
|
|
155
156
|
const ms = Date.parse(raw);
|
|
156
157
|
return Number.isFinite(ms) ? ms : Number.NEGATIVE_INFINITY;
|
|
157
158
|
}
|
|
158
|
-
export function shouldAgeQuestion(question, audience, nowMs, ageWindowMs = QUESTION_AGE_MS) {
|
|
159
|
+
export function shouldAgeQuestion(question, audience, nowMs, ageWindowMs = QUESTION_AGE_MS, maxAgedAdvances = MAX_AGED_ADVANCES) {
|
|
159
160
|
if (question.status !== "pending")
|
|
160
161
|
return false;
|
|
161
162
|
if (audience.kind === "user")
|
|
162
163
|
return false;
|
|
163
|
-
if (agedAdvanceCount(question) >=
|
|
164
|
+
if (agedAdvanceCount(question) >= maxAgedAdvances)
|
|
164
165
|
return false;
|
|
165
166
|
const started = currentHopStartedAtMs(question);
|
|
166
167
|
if (started === Number.NEGATIVE_INFINITY)
|
|
@@ -1121,16 +1122,26 @@ export async function runPassBody(hqRoot, deps) {
|
|
|
1121
1122
|
}
|
|
1122
1123
|
continue;
|
|
1123
1124
|
}
|
|
1124
|
-
|
|
1125
|
+
let maxAgedAdvances;
|
|
1126
|
+
try {
|
|
1127
|
+
maxAgedAdvances = laneDepthCap(hqRoot, question.company?.slug).max * 2;
|
|
1128
|
+
}
|
|
1129
|
+
catch (err) {
|
|
1130
|
+
if (!(err instanceof LanesError))
|
|
1131
|
+
throw err;
|
|
1132
|
+
maxAgedAdvances = MAX_AGED_ADVANCES;
|
|
1133
|
+
process.stderr.write(`hq lanes reconcile: lane depth config failed for company=${JSON.stringify(question.company?.slug)}; using default ${MAX_LANE_DEPTH}: ${err.message}\n`);
|
|
1134
|
+
}
|
|
1135
|
+
if (shouldAgeQuestion(question, audience, nowMs, questionAgeWindowMs(audience), maxAgedAdvances)) {
|
|
1125
1136
|
const nextHop = question.hop + 1;
|
|
1126
1137
|
const nextAudience = resolveQuestionAudience({ ...question, hop: nextHop }, readLaneSenior, readSessionSenior);
|
|
1127
1138
|
const result = casQuestionIf(hqRoot, question.question_id, (current) => {
|
|
1128
1139
|
if (current.status !== "pending" ||
|
|
1129
|
-
agedAdvanceCount(current) >=
|
|
1140
|
+
agedAdvanceCount(current) >= maxAgedAdvances) {
|
|
1130
1141
|
return false;
|
|
1131
1142
|
}
|
|
1132
1143
|
const currentAudience = resolveQuestionAudience(current, readLaneSenior, readSessionSenior);
|
|
1133
|
-
return shouldAgeQuestion(current, currentAudience, nowMs, questionAgeWindowMs(currentAudience));
|
|
1144
|
+
return shouldAgeQuestion(current, currentAudience, nowMs, questionAgeWindowMs(currentAudience), maxAgedAdvances);
|
|
1134
1145
|
}, (current) => {
|
|
1135
1146
|
const entry = {
|
|
1136
1147
|
hop: current.hop + 1,
|
package/dist/lib/mesh/api.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ export interface MeshProjectStoryInput {
|
|
|
51
51
|
description?: string;
|
|
52
52
|
acceptanceCriteria?: string[];
|
|
53
53
|
status?: string;
|
|
54
|
+
/** Source ledger state preserved for Boards whose status enum has four columns. */
|
|
55
|
+
ledgerState?: string;
|
|
54
56
|
passes?: boolean;
|
|
55
57
|
priority?: number;
|
|
56
58
|
assigneeUid?: string;
|
|
@@ -55,6 +55,8 @@ export interface InstallPaths {
|
|
|
55
55
|
* host has no conhost (the task then runs node directly).
|
|
56
56
|
*/
|
|
57
57
|
windowsHeadlessLauncher?: string;
|
|
58
|
+
/** Windows command processor used to execute the stable npm .cmd shim. */
|
|
59
|
+
windowsCommandProcessor?: string;
|
|
58
60
|
/** When true, render the root/ec2-user system unit instead of user unit. */
|
|
59
61
|
systemUser?: string;
|
|
60
62
|
/** Verified HQ root to persist and pass to the installed service. */
|
|
@@ -102,7 +104,6 @@ export declare function restartDaemonServiceIfInstalled(opts?: {
|
|
|
102
104
|
env?: NodeJS.ProcessEnv;
|
|
103
105
|
existsSync?: typeof fs.existsSync;
|
|
104
106
|
readFileSync?: typeof fs.readFileSync;
|
|
105
|
-
writeFileSync?: typeof fs.writeFileSync;
|
|
106
107
|
run?: (command: string, args: string[]) => {
|
|
107
108
|
status: number | null;
|
|
108
109
|
stdout?: string;
|
|
@@ -119,7 +120,7 @@ export declare function detectPlatform(platform?: NodeJS.Platform): DaemonInstal
|
|
|
119
120
|
* dies with MODULE_NOT_FOUND on the next restart. Prefer the live npm-global
|
|
120
121
|
* binary when both exist.
|
|
121
122
|
*/
|
|
122
|
-
export declare function preferStableHqBinary(entry: string): string;
|
|
123
|
+
export declare function preferStableHqBinary(entry: string, platform?: DaemonInstallPlatform, existsSync?: typeof fs.existsSync): string;
|
|
123
124
|
/**
|
|
124
125
|
* Absolute path of the CLI entry that is executing this process.
|
|
125
126
|
* realpath of argv[1] only — never a PATH lookup of `hq`.
|
|
@@ -153,7 +154,6 @@ export declare function installDaemonIfStale(opts: {
|
|
|
153
154
|
run?: RunCommand;
|
|
154
155
|
uid?: number;
|
|
155
156
|
sleep?: SleepFn;
|
|
156
|
-
writeFileSync?: typeof fs.writeFileSync;
|
|
157
157
|
mkdirSync?: typeof fs.mkdirSync;
|
|
158
158
|
reloadSystemd?: () => {
|
|
159
159
|
status: number | null;
|
|
@@ -193,6 +193,10 @@ export declare function buildInstallPaths(opts: {
|
|
|
193
193
|
windowsUser?: string;
|
|
194
194
|
/** Windows headless launcher override (tests); defaults to %SystemRoot%\System32\conhost.exe when present. */
|
|
195
195
|
windowsHeadlessLauncher?: string | null;
|
|
196
|
+
/** Platform override for unit path and command rendering tests. */
|
|
197
|
+
platform?: DaemonInstallPlatform;
|
|
198
|
+
/** Filesystem seam for stable hq shim resolution tests. */
|
|
199
|
+
existsSync?: typeof fs.existsSync;
|
|
196
200
|
}): InstallPaths;
|
|
197
201
|
/** `DOMAIN\user` for the current account, as Task Scheduler expects. */
|
|
198
202
|
export declare function resolveWindowsUser(env?: NodeJS.ProcessEnv): string;
|
|
@@ -277,7 +281,6 @@ export declare function installDaemonService(opts: {
|
|
|
277
281
|
paths?: InstallPaths;
|
|
278
282
|
/** When set, write the system unit template (does not sudo-install). */
|
|
279
283
|
systemUser?: string;
|
|
280
|
-
writeFileSync?: typeof fs.writeFileSync;
|
|
281
284
|
mkdirSync?: typeof fs.mkdirSync;
|
|
282
285
|
/** Optional: actually load the service (disabled in tests). */
|
|
283
286
|
activate?: (platform: DaemonInstallPlatform, dest: string) => void;
|
|
@@ -16,6 +16,7 @@ import * as os from "node:os";
|
|
|
16
16
|
import * as path from "node:path";
|
|
17
17
|
import { spawnSync } from "node:child_process";
|
|
18
18
|
import semver from "semver";
|
|
19
|
+
import { atomicWrite } from "../../../index-render/shared.js";
|
|
19
20
|
import { resolveDaemonAssetDir } from "./log.js";
|
|
20
21
|
import { LAUNCHD_LABEL, SYSTEMD_UNIT_NAME, WINDOWS_TASK_NAME, daemonDir, daemonLogPath, daemonPidPath, } from "./paths.js";
|
|
21
22
|
import { isInstallableHqRoot, readDaemonConfig, registerTrustedHqRoot, verifyHqRoot } from "./config.js";
|
|
@@ -123,17 +124,25 @@ function migrateLegacyDaemonUnit(opts) {
|
|
|
123
124
|
// The rendered command only has to name a binary; install already resolved one.
|
|
124
125
|
}
|
|
125
126
|
if (platform === "darwin") {
|
|
126
|
-
|
|
127
|
+
replaceUnitFile(unitPath, renderLaunchdPlist(paths), 0o600, () => {
|
|
128
|
+
activateLaunchd(unitPath, asRunCommand(opts.run), opts.uid, { sleep: opts.sleep });
|
|
129
|
+
});
|
|
127
130
|
return;
|
|
128
131
|
}
|
|
129
132
|
if (platform === "linux") {
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
replaceUnitFile(unitPath, renderSystemdUserUnit(paths), 0o600, () => {
|
|
134
|
+
const result = opts.run("systemctl", ["--user", "daemon-reload"]);
|
|
135
|
+
if (result.status !== 0)
|
|
136
|
+
throw new Error("systemctl --user daemon-reload failed while repairing the mesh unit");
|
|
137
|
+
});
|
|
132
138
|
return;
|
|
133
139
|
}
|
|
134
140
|
const rendered = renderWindowsTaskXml(paths);
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
replaceUnitFile(unitPath, encodeWindowsTaskXml(rendered), 0o600, () => {
|
|
142
|
+
const result = opts.run("schtasks.exe", ["/Create", "/TN", WINDOWS_TASK_NAME, "/XML", unitPath, "/F"]);
|
|
143
|
+
if (result.status !== 0)
|
|
144
|
+
throw new Error("schtasks /Create failed while repairing the mesh unit");
|
|
145
|
+
});
|
|
137
146
|
}
|
|
138
147
|
/**
|
|
139
148
|
* Restart the installed user daemon after a CLI upgrade. This is deliberately
|
|
@@ -155,7 +164,6 @@ export function restartDaemonServiceIfInstalled(opts = {}) {
|
|
|
155
164
|
const home = opts.home ?? os.homedir();
|
|
156
165
|
const existsSync = opts.existsSync ?? fs.existsSync;
|
|
157
166
|
const readFileSync = opts.readFileSync ?? fs.readFileSync;
|
|
158
|
-
const writeFileSync = opts.writeFileSync ?? fs.writeFileSync;
|
|
159
167
|
const run = opts.run ?? ((command, args) => {
|
|
160
168
|
const result = spawnSync(command, args, { encoding: "utf8" });
|
|
161
169
|
return {
|
|
@@ -165,7 +173,17 @@ export function restartDaemonServiceIfInstalled(opts = {}) {
|
|
|
165
173
|
};
|
|
166
174
|
});
|
|
167
175
|
try {
|
|
168
|
-
migrateLegacyDaemonUnit({
|
|
176
|
+
migrateLegacyDaemonUnit({
|
|
177
|
+
platform,
|
|
178
|
+
home,
|
|
179
|
+
hqRoot: opts.hqRoot,
|
|
180
|
+
env: opts.env,
|
|
181
|
+
existsSync,
|
|
182
|
+
readFileSync,
|
|
183
|
+
run,
|
|
184
|
+
uid: opts.uid,
|
|
185
|
+
sleep: opts.sleep,
|
|
186
|
+
});
|
|
169
187
|
if (platform === "darwin") {
|
|
170
188
|
const plist = path.join(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
171
189
|
if (!existsSync(plist))
|
|
@@ -239,14 +257,46 @@ export function detectPlatform(platform = process.platform) {
|
|
|
239
257
|
* dies with MODULE_NOT_FOUND on the next restart. Prefer the live npm-global
|
|
240
258
|
* binary when both exist.
|
|
241
259
|
*/
|
|
242
|
-
export function preferStableHqBinary(entry) {
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
260
|
+
export function preferStableHqBinary(entry, platform = detectPlatform(), existsSync = fs.existsSync) {
|
|
261
|
+
const api = platform === "win32" ? path.win32 : path;
|
|
262
|
+
const sep = api.sep;
|
|
263
|
+
const stale = `${sep}toolchain${sep}node${sep}bin${sep}`;
|
|
264
|
+
const stable = `${sep}toolchain${sep}npm-global${sep}bin${sep}`;
|
|
265
|
+
if (entry.includes(stale)) {
|
|
266
|
+
const candidate = entry.replace(stale, stable);
|
|
267
|
+
if (candidate !== entry && existsSync(candidate))
|
|
268
|
+
return api.resolve(candidate);
|
|
269
|
+
}
|
|
270
|
+
const normalized = entry.replace(/\\/g, "/");
|
|
271
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
272
|
+
let packageAt = -1;
|
|
273
|
+
for (let i = 0; i < parts.length - 2; i += 1) {
|
|
274
|
+
if (parts[i]?.toLowerCase() === "node_modules"
|
|
275
|
+
&& parts[i + 1]?.toLowerCase() === "@indigoai-us"
|
|
276
|
+
&& parts[i + 2]?.toLowerCase() === "hq-cli") {
|
|
277
|
+
packageAt = i;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (packageAt >= 0) {
|
|
282
|
+
const prefixParts = parts.slice(0, packageAt);
|
|
283
|
+
let prefix;
|
|
284
|
+
if (platform === "win32") {
|
|
285
|
+
const root = path.win32.parse(entry).root;
|
|
286
|
+
const rootParts = root.replace(/\\+$/, "").split(/[\\/]+/).filter(Boolean);
|
|
287
|
+
prefix = path.win32.resolve(root, ...prefixParts.slice(rootParts.length));
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
if (prefixParts.at(-1) === "lib")
|
|
291
|
+
prefixParts.pop();
|
|
292
|
+
prefix = path.posix.resolve(`/${prefixParts.join("/")}`);
|
|
293
|
+
}
|
|
294
|
+
const shim = platform === "win32"
|
|
295
|
+
? path.win32.join(prefix, "hq.cmd")
|
|
296
|
+
: path.posix.join(prefix, "bin", "hq");
|
|
297
|
+
if (existsSync(shim))
|
|
298
|
+
return api.resolve(shim);
|
|
299
|
+
}
|
|
250
300
|
return entry;
|
|
251
301
|
}
|
|
252
302
|
/**
|
|
@@ -346,7 +396,6 @@ export function installDaemonIfStale(opts) {
|
|
|
346
396
|
platform,
|
|
347
397
|
paths: opts.paths,
|
|
348
398
|
systemUser: opts.systemUser,
|
|
349
|
-
writeFileSync: opts.writeFileSync,
|
|
350
399
|
mkdirSync: opts.mkdirSync,
|
|
351
400
|
reloadSystemd: opts.reloadSystemd,
|
|
352
401
|
schtasks: opts.schtasks,
|
|
@@ -357,9 +406,6 @@ export function installDaemonIfStale(opts) {
|
|
|
357
406
|
uid: opts.uid,
|
|
358
407
|
sleep: opts.sleep,
|
|
359
408
|
});
|
|
360
|
-
if (platform === "linux" && !opts.systemUser && !opts.paths.systemUser) {
|
|
361
|
-
activateSystemd(SYSTEMD_UNIT_NAME, run);
|
|
362
|
-
}
|
|
363
409
|
return {
|
|
364
410
|
reinstalled: true,
|
|
365
411
|
message: `${installed.message} Restarted with ${opts.paths.nodeBinary} ${opts.paths.hqBinary}.`,
|
|
@@ -413,7 +459,8 @@ export function buildInstallPaths(opts) {
|
|
|
413
459
|
const home = opts.home ?? os.homedir();
|
|
414
460
|
const env = opts.env ?? process.env;
|
|
415
461
|
const ddir = daemonDir(opts.meshRoot, home, env);
|
|
416
|
-
const
|
|
462
|
+
const platform = opts.platform ?? detectPlatform();
|
|
463
|
+
const hqBinary = preferStableHqBinary(opts.hqBinary ?? resolveHqBinary(opts.argv, env), platform, opts.existsSync ?? fs.existsSync);
|
|
417
464
|
const nodeBinary = opts.nodeBinary ?? resolveNodeBinary(opts.execPath, env);
|
|
418
465
|
return {
|
|
419
466
|
home,
|
|
@@ -430,6 +477,7 @@ export function buildInstallPaths(opts) {
|
|
|
430
477
|
windowsHeadlessLauncher: opts.windowsHeadlessLauncher === undefined
|
|
431
478
|
? resolveWindowsHeadlessLauncher(env)
|
|
432
479
|
: (opts.windowsHeadlessLauncher ?? undefined),
|
|
480
|
+
windowsCommandProcessor: path.win32.join(env.SystemRoot ?? "C:\\Windows", "System32", "cmd.exe"),
|
|
433
481
|
systemUser: opts.systemUser,
|
|
434
482
|
hqRoot: opts.hqRoot,
|
|
435
483
|
hqRootSource: opts.hqRootSource,
|
|
@@ -480,7 +528,12 @@ export function windowsLogonNotice(paths) {
|
|
|
480
528
|
function describeWindowsLaunchMode(paths) {
|
|
481
529
|
return windowsLaunchMode(paths) === "conhost-headless"
|
|
482
530
|
? `mode: conhost --headless via ${paths.windowsHeadlessLauncher}`
|
|
483
|
-
:
|
|
531
|
+
: isWindowsCommandShim(paths.hqBinary)
|
|
532
|
+
? "mode: direct command processor (conhost --headless unavailable on this host; a console window may appear at logon)"
|
|
533
|
+
: "mode: direct node (conhost --headless unavailable on this host; a console window may appear at logon)";
|
|
534
|
+
}
|
|
535
|
+
function isWindowsCommandShim(file) {
|
|
536
|
+
return path.win32.extname(file).toLowerCase() === ".cmd";
|
|
484
537
|
}
|
|
485
538
|
function safeUsername() {
|
|
486
539
|
try {
|
|
@@ -601,13 +654,19 @@ WantedBy=multi-user.target
|
|
|
601
654
|
*/
|
|
602
655
|
export function renderWindowsTaskXml(paths) {
|
|
603
656
|
const user = escapeXml(paths.windowsUser);
|
|
604
|
-
const
|
|
657
|
+
const stableShim = isWindowsCommandShim(paths.hqBinary);
|
|
605
658
|
const hqArg = windowsArgQuote(paths.hqBinary);
|
|
606
|
-
const
|
|
659
|
+
const commandProcessor = paths.windowsCommandProcessor ?? path.win32.join("C:\\Windows", "System32", "cmd.exe");
|
|
660
|
+
const command = stableShim ? (paths.windowsHeadlessLauncher ?? commandProcessor) : (paths.windowsHeadlessLauncher ?? paths.nodeBinary);
|
|
607
661
|
const rootArgs = paths.hqRoot ? ` --hq-root ${windowsArgQuote(paths.hqRoot)}` : "";
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
662
|
+
const commandLine = `${hqArg} mesh daemon run${rootArgs}`;
|
|
663
|
+
const args = stableShim
|
|
664
|
+
? paths.windowsHeadlessLauncher
|
|
665
|
+
? `--headless ${windowsArgQuote(commandProcessor)} /d /s /c "${commandLine}"`
|
|
666
|
+
: `/d /s /c "${commandLine}"`
|
|
667
|
+
: paths.windowsHeadlessLauncher
|
|
668
|
+
? `--headless ${windowsArgQuote(paths.nodeBinary)} ${hqArg} mesh daemon run${rootArgs}`
|
|
669
|
+
: `${hqArg} mesh daemon run${rootArgs}`;
|
|
611
670
|
return `<?xml version="1.0" encoding="UTF-16"?>
|
|
612
671
|
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
613
672
|
<RegistrationInfo>
|
|
@@ -769,7 +828,7 @@ export function parseWindowsTaskHqRoot(xml) {
|
|
|
769
828
|
if (quoted)
|
|
770
829
|
return quoted[1].replace(/\\"/g, '"');
|
|
771
830
|
const bare = /--hq-root (\S+)/.exec(args);
|
|
772
|
-
return bare ? bare[1] : null;
|
|
831
|
+
return bare ? bare[1].replace(/"+$/, "") : null;
|
|
773
832
|
}
|
|
774
833
|
/**
|
|
775
834
|
* The HQ root the installed Windows task actually carries: the task XML on
|
|
@@ -812,11 +871,40 @@ function shellQuote(value) {
|
|
|
812
871
|
return value;
|
|
813
872
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
814
873
|
}
|
|
874
|
+
function replaceUnitFile(file, content, mode, reload) {
|
|
875
|
+
let previous = null;
|
|
876
|
+
let previousMode = mode;
|
|
877
|
+
try {
|
|
878
|
+
previous = fs.readFileSync(file);
|
|
879
|
+
previousMode = fs.statSync(file).mode & 0o777;
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
if (error.code !== "ENOENT")
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
885
|
+
try {
|
|
886
|
+
atomicWrite(file, content, { mode });
|
|
887
|
+
reload?.();
|
|
888
|
+
}
|
|
889
|
+
catch (error) {
|
|
890
|
+
try {
|
|
891
|
+
if (previous) {
|
|
892
|
+
atomicWrite(file, previous, { mode: previousMode });
|
|
893
|
+
}
|
|
894
|
+
else {
|
|
895
|
+
fs.rmSync(file, { force: true });
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
catch (rollbackError) {
|
|
899
|
+
throw new AggregateError([error, rollbackError], `Unit replacement and rollback failed: ${file}`);
|
|
900
|
+
}
|
|
901
|
+
throw error;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
815
904
|
export function installDaemonService(opts) {
|
|
816
905
|
const platform = opts.platform ?? detectPlatform();
|
|
817
906
|
const paths = opts.paths ??
|
|
818
907
|
buildInstallPaths({ systemUser: opts.systemUser });
|
|
819
|
-
const writeFileSync = opts.writeFileSync ?? fs.writeFileSync;
|
|
820
908
|
const mkdirSync = opts.mkdirSync ?? fs.mkdirSync;
|
|
821
909
|
mkdirSync(paths.daemonDir, { recursive: true, mode: 0o700 });
|
|
822
910
|
registerTrustedHqRoot(paths.daemonDir, paths.hqRoot);
|
|
@@ -832,13 +920,12 @@ export function installDaemonService(opts) {
|
|
|
832
920
|
if (platform === "darwin") {
|
|
833
921
|
const rendered = renderLaunchdPlist(paths);
|
|
834
922
|
mkdirSync(paths.launchAgentsDir, { recursive: true });
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
}
|
|
923
|
+
replaceUnitFile(paths.plistPath, rendered, 0o600, () => {
|
|
924
|
+
if (opts.run)
|
|
925
|
+
activateLaunchd(paths.plistPath, opts.run, opts.uid, { sleep: opts.sleep });
|
|
926
|
+
else
|
|
927
|
+
opts.activate?.(platform, paths.plistPath);
|
|
928
|
+
});
|
|
842
929
|
return {
|
|
843
930
|
platform,
|
|
844
931
|
action: "install",
|
|
@@ -856,7 +943,7 @@ export function installDaemonService(opts) {
|
|
|
856
943
|
});
|
|
857
944
|
// Write under daemon dir as a template the operator can sudo-install.
|
|
858
945
|
const dest = path.join(paths.daemonDir, SYSTEMD_UNIT_NAME);
|
|
859
|
-
|
|
946
|
+
replaceUnitFile(dest, rendered, 0o644);
|
|
860
947
|
return {
|
|
861
948
|
platform,
|
|
862
949
|
action: "install",
|
|
@@ -870,9 +957,18 @@ export function installDaemonService(opts) {
|
|
|
870
957
|
}
|
|
871
958
|
const rendered = renderSystemdUserUnit(paths);
|
|
872
959
|
mkdirSync(paths.systemdUserDir, { recursive: true });
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
960
|
+
replaceUnitFile(paths.systemdUnitPath, rendered, 0o600, () => {
|
|
961
|
+
const result = (opts.reloadSystemd ?? (() => {
|
|
962
|
+
const reload = spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
963
|
+
return { status: reload.status };
|
|
964
|
+
}))();
|
|
965
|
+
if (result.status !== 0)
|
|
966
|
+
throw new Error("systemctl --user daemon-reload failed while installing the mesh unit");
|
|
967
|
+
if (opts.run)
|
|
968
|
+
activateSystemd(SYSTEMD_UNIT_NAME, opts.run);
|
|
969
|
+
else
|
|
970
|
+
opts.activate?.(platform, paths.systemdUnitPath);
|
|
971
|
+
});
|
|
876
972
|
return {
|
|
877
973
|
platform,
|
|
878
974
|
action: "install",
|
|
@@ -885,14 +981,15 @@ export function installDaemonService(opts) {
|
|
|
885
981
|
if (platform === "win32") {
|
|
886
982
|
const schtasks = opts.schtasks ?? defaultSchtasks;
|
|
887
983
|
const rendered = renderWindowsTaskXml(paths);
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
984
|
+
replaceUnitFile(paths.windowsTaskXmlPath, encodeWindowsTaskXml(rendered), 0o600, () => {
|
|
985
|
+
const created = schtasks([
|
|
986
|
+
"/Create", "/TN", WINDOWS_TASK_NAME, "/XML", paths.windowsTaskXmlPath, "/F",
|
|
987
|
+
]);
|
|
988
|
+
if (created.status !== 0) {
|
|
989
|
+
throw new Error(`schtasks /Create failed for ${WINDOWS_TASK_NAME} (exit ${created.status ?? "signal"}): ` +
|
|
990
|
+
`${(created.stderr || created.stdout).trim()}`);
|
|
991
|
+
}
|
|
992
|
+
});
|
|
896
993
|
// /F replaced any previous registration without ending its instance; stop
|
|
897
994
|
// the recorded daemon so the fresh task instance can take the pid lock.
|
|
898
995
|
schtasks(["/End", "/TN", WINDOWS_TASK_NAME]);
|
|
@@ -4,6 +4,23 @@ export declare function mergeProjectStories(current: MeshProjectStoryInput[], pl
|
|
|
4
4
|
resetStatus?: boolean;
|
|
5
5
|
}): MeshProjectStoryInput[];
|
|
6
6
|
export declare function boardStoryStatus(story: MeshProjectStoryInput): string;
|
|
7
|
+
export interface ProjectStoryReconciliation {
|
|
8
|
+
storyId: string;
|
|
9
|
+
ledgerState: string | null;
|
|
10
|
+
expectedBoardStatus: string | null;
|
|
11
|
+
boardStatus: string | null;
|
|
12
|
+
boardLedgerState: string | null;
|
|
13
|
+
stateDrift: boolean;
|
|
14
|
+
missingOnBoard: boolean;
|
|
15
|
+
extraOnBoard: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ProjectStoryReconciliationReport {
|
|
18
|
+
mismatches: ProjectStoryReconciliation[];
|
|
19
|
+
ledgerStateUnsupported: string[];
|
|
20
|
+
stateDriftCount: number;
|
|
21
|
+
}
|
|
22
|
+
/** Compare one PRD ledger with one Board snapshot without changing either. */
|
|
23
|
+
export declare function reconcileProjectStories(ledger: readonly MeshProjectStoryInput[], board: readonly MeshProjectStoryInput[]): ProjectStoryReconciliationReport;
|
|
7
24
|
/**
|
|
8
25
|
* Keep only prd-sync task_status events whose Board status actually differs.
|
|
9
26
|
* Missing Board rows stay (ensure will add them). Same-status replays of
|
|
@@ -36,6 +36,34 @@ export function boardStoryStatus(story) {
|
|
|
36
36
|
return status;
|
|
37
37
|
return story.passes === true ? "done" : "queued";
|
|
38
38
|
}
|
|
39
|
+
/** Compare one PRD ledger with one Board snapshot without changing either. */
|
|
40
|
+
export function reconcileProjectStories(ledger, board) {
|
|
41
|
+
const ledgerById = new Map(ledger.map((story) => [story.id, story]));
|
|
42
|
+
const boardById = new Map(board.map((story) => [story.id, story]));
|
|
43
|
+
const ids = [...ledgerById.keys(), ...[...boardById.keys()].filter((id) => !ledgerById.has(id))];
|
|
44
|
+
const ledgerStateUnsupported = [];
|
|
45
|
+
const mismatches = ids.flatMap((storyId) => {
|
|
46
|
+
const source = ledgerById.get(storyId);
|
|
47
|
+
const current = boardById.get(storyId);
|
|
48
|
+
const expectedBoardStatus = source ? boardStoryStatus(source) : null;
|
|
49
|
+
const boardStatus = current ? boardStoryStatus(current) : null;
|
|
50
|
+
const ledgerState = source?.ledgerState ?? null;
|
|
51
|
+
const boardLedgerState = current?.ledgerState ?? null;
|
|
52
|
+
const missingOnBoard = Boolean(source && !current);
|
|
53
|
+
const extraOnBoard = Boolean(current && !source);
|
|
54
|
+
if (source && ledgerState && current && boardLedgerState === null)
|
|
55
|
+
ledgerStateUnsupported.push(storyId);
|
|
56
|
+
const stateDrift = Boolean(source && ledgerState && boardLedgerState !== null && boardLedgerState !== ledgerState);
|
|
57
|
+
if (!missingOnBoard && !extraOnBoard && boardStatus === expectedBoardStatus && !stateDrift)
|
|
58
|
+
return [];
|
|
59
|
+
return [{ storyId, ledgerState, expectedBoardStatus, boardStatus, boardLedgerState, stateDrift, missingOnBoard, extraOnBoard }];
|
|
60
|
+
});
|
|
61
|
+
return {
|
|
62
|
+
mismatches,
|
|
63
|
+
ledgerStateUnsupported,
|
|
64
|
+
stateDriftCount: mismatches.filter((mismatch) => mismatch.stateDrift).length,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
39
67
|
/**
|
|
40
68
|
* Keep only prd-sync task_status events whose Board status actually differs.
|
|
41
69
|
* Missing Board rows stay (ensure will add them). Same-status replays of
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
declare const BOARD_STORY_STATUSES: readonly ["queued", "in_progress", "review", "done"];
|
|
2
2
|
export type BoardStoryStatus = (typeof BOARD_STORY_STATUSES)[number];
|
|
3
|
+
/**
|
|
4
|
+
* The ledger has more states than the Board. `blocked` and the review states
|
|
5
|
+
* need attention, while `dropped` is terminal; the Board has only one terminal
|
|
6
|
+
* column (`done`).
|
|
7
|
+
*/
|
|
8
|
+
export declare const LEDGER_STATES: readonly ["todo", "in_progress", "review", "waiting", "blocked", "done", "dropped"];
|
|
9
|
+
export declare const ACCEPTED_LEDGER_STATES: readonly ["todo", "in_progress", "review", "waiting", "blocked", "done", "dropped", "in_review", "verification"];
|
|
10
|
+
export declare class UnknownProjectStoryStateError extends Error {
|
|
11
|
+
readonly source: string;
|
|
12
|
+
readonly storyId: string;
|
|
13
|
+
readonly value: unknown;
|
|
14
|
+
readonly code = "PROJECT_STORY_STATE_INVALID";
|
|
15
|
+
constructor(source: string, storyId: string, value: unknown);
|
|
16
|
+
}
|
|
3
17
|
export declare function mapSourceStoryStatus(story: {
|
|
18
|
+
id?: unknown;
|
|
4
19
|
status?: unknown;
|
|
5
20
|
state?: unknown;
|
|
6
21
|
}, source: string): BoardStoryStatus | undefined;
|
|
@@ -12,6 +27,7 @@ export declare function mapSourceStoryStatus(story: {
|
|
|
12
27
|
export declare function mapReaderStoryStatus(story: {
|
|
13
28
|
status?: unknown;
|
|
14
29
|
state?: unknown;
|
|
30
|
+
ledgerState?: unknown;
|
|
15
31
|
}): string | undefined;
|
|
16
32
|
export {};
|
|
17
33
|
//# sourceMappingURL=story-status.d.ts.map
|