@indigoai-us/hq-cli 5.345.54 → 5.345.56
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 +31 -0
- package/assets/scaffold/core/scripts/derive-trigger-facts.sh +10 -1
- package/assets/scaffold/core/scripts/jobs-validate.sh +2 -1
- package/assets/scaffold/core/scripts/session-title.sh +18 -15
- package/dist/command-catalog.generated.d.ts +143 -2
- package/dist/command-catalog.generated.js +175 -2
- package/dist/command-registration-plan.d.ts +6 -1
- package/dist/command-registration-plan.js +2 -1
- package/dist/commands/cloud.d.ts +7 -0
- package/dist/commands/cloud.js +92 -16
- package/dist/commands/core-delegate-w1b.js +18 -5
- package/dist/commands/core-derive-trigger-facts-command.js +14 -0
- package/dist/commands/core-derive-trigger-facts-native.d.ts +1 -0
- package/dist/commands/core-derive-trigger-facts-native.js +20 -1
- package/dist/commands/daemon.js +16 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +15 -2
- package/dist/commands/import-scan.d.ts +24 -0
- package/dist/commands/import-scan.js +75 -0
- package/dist/commands/lanes-checks.d.ts +3 -0
- package/dist/commands/lanes-checks.js +34 -0
- package/dist/commands/lanes-session-start.d.ts +17 -0
- package/dist/commands/lanes-session-start.js +195 -0
- package/dist/commands/lanes-shared.d.ts +2 -1
- package/dist/commands/lanes-watch.d.ts +2 -0
- package/dist/commands/lanes-watch.js +2 -1
- package/dist/commands/lanes.d.ts +3 -1
- package/dist/commands/lanes.js +121 -18
- package/dist/commands/members.d.ts +4 -0
- package/dist/commands/members.js +23 -2
- package/dist/lanes-drain-hook.js +6 -2
- package/dist/lib/daemon/conflict-notices.d.ts +4 -0
- package/dist/lib/daemon/conflict-notices.js +80 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +2 -0
- package/dist/lib/doctor/checks/sync-health.js +47 -23
- package/dist/lib/import-scan/events.d.ts +135 -0
- package/dist/lib/import-scan/events.js +71 -0
- package/dist/lib/import-scan/run.d.ts +84 -0
- package/dist/lib/import-scan/run.js +308 -0
- package/dist/lib/import-scan/scan-event.schema.json +127 -0
- package/dist/lib/import-scan/validate.d.ts +31 -0
- package/dist/lib/import-scan/validate.js +111 -0
- package/dist/lib/lanes/dropbox.d.ts +4 -2
- package/dist/lib/lanes/dropbox.js +54 -9
- package/dist/lib/lanes/flag-keys.d.ts +10 -0
- package/dist/lib/lanes/flag-keys.js +10 -0
- package/dist/lib/lanes/hooks-install.d.ts +14 -11
- package/dist/lib/lanes/hooks-install.js +70 -50
- package/dist/lib/lanes/monitor.js +1 -1
- package/dist/lib/lanes/reaper.d.ts +58 -0
- package/dist/lib/lanes/reaper.js +403 -0
- package/dist/lib/lanes/registry-flag.d.ts +2 -0
- package/dist/lib/lanes/registry-flag.js +1 -1
- package/dist/lib/lanes/spawn.d.ts +2 -0
- package/dist/lib/lanes/spawn.js +32 -2
- package/dist/lib/lanes/status-fields.d.ts +12 -0
- package/dist/lib/lanes/status-fields.js +189 -0
- package/dist/lib/lanes/types.js +1 -1
- package/dist/lib/lanes/worker-continuity.d.ts +18 -0
- package/dist/lib/lanes/worker-continuity.js +81 -0
- package/dist/lib/lanes/worker-process.d.ts +4 -0
- package/dist/lib/lanes/worker-process.js +77 -0
- package/dist/main.js +9 -3
- package/dist/utils/cli-telemetry.d.ts +4 -1
- package/dist/utils/cli-telemetry.js +8 -0
- package/dist/utils/jq-windows-compat.d.ts +13 -0
- package/dist/utils/jq-windows-compat.js +48 -0
- package/dist/utils/run-bundled-script.d.ts +3 -0
- package/dist/utils/run-bundled-script.js +9 -80
- package/dist/utils/windows-bash.d.ts +5 -0
- package/dist/utils/windows-bash.js +75 -0
- package/package.json +1 -1
package/dist/commands/cloud.js
CHANGED
|
@@ -139,6 +139,9 @@ export async function withSyncHealthReport(run) {
|
|
|
139
139
|
* nothing was scope-excluded. Keeping this pure makes the "never silently
|
|
140
140
|
* succeed when files were dropped" guarantee unit-testable.
|
|
141
141
|
*/
|
|
142
|
+
export function scopeExcludedExitCode(count) {
|
|
143
|
+
return count > 0 ? 1 : 0;
|
|
144
|
+
}
|
|
142
145
|
export function scopeExcludedWarning(count) {
|
|
143
146
|
if (count <= 0)
|
|
144
147
|
return null;
|
|
@@ -147,6 +150,19 @@ export function scopeExcludedWarning(count) {
|
|
|
147
150
|
`They were skipped, not synced. Re-run with --json to list them, or ` +
|
|
148
151
|
`ask an admin to grant you write on those paths.`);
|
|
149
152
|
}
|
|
153
|
+
/** Actionable recovery for files refused because the vault baseline is stale. */
|
|
154
|
+
export function staleUploadRefusalLines(count, paths = []) {
|
|
155
|
+
if (count <= 0)
|
|
156
|
+
return [];
|
|
157
|
+
const lines = [
|
|
158
|
+
`⚠ ${count} file(s) were NOT uploaded because the vault copy changed after your edit. ` +
|
|
159
|
+
`Pull the current vault copy, merge your edit, and push again.`,
|
|
160
|
+
];
|
|
161
|
+
if (paths.length > 0) {
|
|
162
|
+
lines.push(" Not uploaded:", ...paths.map((filePath) => ` - ${filePath}`));
|
|
163
|
+
}
|
|
164
|
+
return lines;
|
|
165
|
+
}
|
|
150
166
|
/**
|
|
151
167
|
* Optional detail line listing the effective write prefixes when a push
|
|
152
168
|
* dropped files — helps debug ACL vs scope-excluded mismatches (feedback_7e9c535e).
|
|
@@ -176,7 +192,7 @@ export function scopeExcludedLines(count, prefixSet) {
|
|
|
176
192
|
`⚠ ${count} file(s) were NOT uploaded — the server refused them. ` +
|
|
177
193
|
`A common cause is a file someone else created in a private folder ` +
|
|
178
194
|
`(foo/): only its creator can change it. They were skipped, not ` +
|
|
179
|
-
`synced. Re-run with --json to list them.`,
|
|
195
|
+
`synced. Re-run with --json to list them, or ask its creator to grant access.`,
|
|
180
196
|
];
|
|
181
197
|
}
|
|
182
198
|
const lines = [scopeExcludedWarning(count)];
|
|
@@ -527,6 +543,12 @@ export async function pushAll(options, deps) {
|
|
|
527
543
|
result.bytesUploaded += r.bytesUploaded;
|
|
528
544
|
result.filesDeleted += r.filesDeleted;
|
|
529
545
|
result.filesExcludedByScope += r.filesExcludedByScope;
|
|
546
|
+
if ((r.filesRefusedStale ?? 0) > 0) {
|
|
547
|
+
result.filesRefusedStale =
|
|
548
|
+
(result.filesRefusedStale ?? 0) + (r.filesRefusedStale ?? 0);
|
|
549
|
+
result.filesRefusedStalePaths ??= [];
|
|
550
|
+
result.filesRefusedStalePaths.push(...(r.filesRefusedStalePaths ?? []));
|
|
551
|
+
}
|
|
530
552
|
result.perCompany.push({
|
|
531
553
|
slug: entry.slug,
|
|
532
554
|
result: r,
|
|
@@ -977,6 +999,17 @@ export function registerCloudCommands(program) {
|
|
|
977
999
|
...(pushPrefixSet !== undefined ? { prefixSet: pushPrefixSet } : {}),
|
|
978
1000
|
});
|
|
979
1001
|
const planLimitRefused = planLimitTracker.summary();
|
|
1002
|
+
const staleRefusalCount = result.filesRefusedStale ?? 0;
|
|
1003
|
+
const refusalRecovery = [
|
|
1004
|
+
result.filesExcludedByScope > 0
|
|
1005
|
+
? "Check write access for the refused path; ask its creator or a company admin to grant access."
|
|
1006
|
+
: null,
|
|
1007
|
+
staleRefusalCount > 0
|
|
1008
|
+
? "Pull the current vault copy, merge your edit, and push again."
|
|
1009
|
+
: null,
|
|
1010
|
+
]
|
|
1011
|
+
.filter((message) => message !== null)
|
|
1012
|
+
.join(" ");
|
|
980
1013
|
if (jsonMode) {
|
|
981
1014
|
// Synthetic terminal event so subprocess consumers can read final
|
|
982
1015
|
// counts without summing per-file events. Distinguished from
|
|
@@ -991,6 +1024,10 @@ export function registerCloudCommands(program) {
|
|
|
991
1024
|
filesSkipped: result.filesSkipped,
|
|
992
1025
|
conflictPaths: result.conflictPaths,
|
|
993
1026
|
aborted: result.aborted,
|
|
1027
|
+
filesExcludedByScope: result.filesExcludedByScope,
|
|
1028
|
+
filesRefusedStale: staleRefusalCount,
|
|
1029
|
+
filesRefusedStalePaths: result.filesRefusedStalePaths ?? [],
|
|
1030
|
+
...(refusalRecovery ? { refusalRecovery } : {}),
|
|
994
1031
|
...(planLimitRefused ? { planLimitRefused } : {}),
|
|
995
1032
|
});
|
|
996
1033
|
}
|
|
@@ -1019,22 +1056,32 @@ export function registerCloudCommands(program) {
|
|
|
1019
1056
|
for (const line of scopeExcludedLines(result.filesExcludedByScope, pushPrefixSet)) {
|
|
1020
1057
|
log(chalk.yellow(line));
|
|
1021
1058
|
}
|
|
1059
|
+
for (const line of staleUploadRefusalLines(result.filesRefusedStale ?? 0, result.filesRefusedStalePaths)) {
|
|
1060
|
+
log(chalk.yellow(line));
|
|
1061
|
+
}
|
|
1022
1062
|
await syncHealth.succeeded();
|
|
1023
|
-
|
|
1024
|
-
process.exit(1);
|
|
1063
|
+
process.exit(1);
|
|
1025
1064
|
return;
|
|
1026
1065
|
}
|
|
1027
|
-
if (result.filesExcludedByScope > 0) {
|
|
1066
|
+
if (result.filesExcludedByScope > 0 || staleRefusalCount > 0) {
|
|
1028
1067
|
log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
|
|
1029
|
-
`${result.filesSkipped} skipped, ${result.filesExcludedByScope}
|
|
1068
|
+
`${result.filesSkipped} skipped, ${result.filesExcludedByScope + staleRefusalCount} files not uploaded)`));
|
|
1030
1069
|
for (const line of scopeExcludedLines(result.filesExcludedByScope, pushPrefixSet)) {
|
|
1031
1070
|
log(chalk.yellow(line));
|
|
1032
1071
|
}
|
|
1072
|
+
for (const line of staleUploadRefusalLines(staleRefusalCount, result.filesRefusedStalePaths)) {
|
|
1073
|
+
log(chalk.yellow(line));
|
|
1074
|
+
}
|
|
1033
1075
|
}
|
|
1034
1076
|
else {
|
|
1035
1077
|
log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`));
|
|
1036
1078
|
}
|
|
1037
1079
|
await syncHealth.succeeded();
|
|
1080
|
+
const exitCode = scopeExcludedExitCode(result.filesExcludedByScope + staleRefusalCount);
|
|
1081
|
+
if (exitCode !== 0) {
|
|
1082
|
+
process.exit(exitCode);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1038
1085
|
}
|
|
1039
1086
|
catch (err) {
|
|
1040
1087
|
if (syncHealth)
|
|
@@ -1630,13 +1677,13 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
|
|
|
1630
1677
|
else if (row.result) {
|
|
1631
1678
|
const r = row.result;
|
|
1632
1679
|
const legRefused = r.planLimit ?? null;
|
|
1633
|
-
const status = r.aborted || r.filesExcludedByScope > 0 || legRefused
|
|
1680
|
+
const status = r.aborted || r.filesExcludedByScope > 0 || (r.filesRefusedStale ?? 0) > 0 || legRefused
|
|
1634
1681
|
? chalk.yellow("⚠")
|
|
1635
1682
|
: chalk.green("✓");
|
|
1636
1683
|
console.log(` ${status} ${row.slug}: ${r.filesUploaded} file(s), ` +
|
|
1637
1684
|
`${formatBytes(r.bytesUploaded)}, ${r.filesSkipped} skipped, ` +
|
|
1638
1685
|
`${r.filesDeleted} deleted, ${r.filesExcludedByScope} scope-excluded, ` +
|
|
1639
|
-
`${r.conflictPaths.length} conflict(s)` +
|
|
1686
|
+
`${r.filesRefusedStale ?? 0} stale-refused, ${r.conflictPaths.length} conflict(s)` +
|
|
1640
1687
|
(r.aborted ? " — aborted" : "") +
|
|
1641
1688
|
(legRefused
|
|
1642
1689
|
? ` — ${describeRefusedCount(legRefused)} refused by plan limit`
|
|
@@ -1652,7 +1699,8 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
|
|
|
1652
1699
|
const summary = `\nPushed ${result.filesUploaded} file(s) ` +
|
|
1653
1700
|
`(${formatBytes(result.bytesUploaded)}) across ${result.attempted} ` +
|
|
1654
1701
|
`target(s); ${result.filesDeleted} deleted; ${errored} error(s); ` +
|
|
1655
|
-
`${result.filesExcludedByScope} scope-excluded` +
|
|
1702
|
+
`${result.filesExcludedByScope} scope-excluded; ` +
|
|
1703
|
+
`${result.filesRefusedStale ?? 0} stale-refused` +
|
|
1656
1704
|
(refusedTargets > 0
|
|
1657
1705
|
? `; new files refused by a plan limit on ${refusedTargets} target(s)`
|
|
1658
1706
|
: "");
|
|
@@ -1673,10 +1721,24 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
|
|
|
1673
1721
|
for (const line of scopeExcludedLines(excluded, row.prefixSet)) {
|
|
1674
1722
|
console.log(chalk.yellow(`${row.slug}: ${line}`));
|
|
1675
1723
|
}
|
|
1724
|
+
const staleCount = row.result?.filesRefusedStale ?? 0;
|
|
1725
|
+
for (const line of staleUploadRefusalLines(staleCount, row.result?.filesRefusedStalePaths)) {
|
|
1726
|
+
console.log(chalk.yellow(`${row.slug}: ${line}`));
|
|
1727
|
+
}
|
|
1676
1728
|
}
|
|
1677
1729
|
if (errored > 0)
|
|
1678
1730
|
throw new SyncExitError(1);
|
|
1679
|
-
|
|
1731
|
+
const refusalSummary = {
|
|
1732
|
+
planLimitRefused: refused,
|
|
1733
|
+
filesExcludedByScope: result.filesExcludedByScope,
|
|
1734
|
+
filesRefusedStale: result.filesRefusedStale ?? 0,
|
|
1735
|
+
filesRefusedStalePaths: [...new Set(result.filesRefusedStalePaths ?? [])],
|
|
1736
|
+
};
|
|
1737
|
+
return (refusalSummary.planLimitRefused ||
|
|
1738
|
+
refusalSummary.filesExcludedByScope > 0 ||
|
|
1739
|
+
refusalSummary.filesRefusedStale > 0
|
|
1740
|
+
? refusalSummary
|
|
1741
|
+
: null);
|
|
1680
1742
|
}
|
|
1681
1743
|
async function runNowSingle(hqRoot, company, personal, message, onConflict, modeAllOverride, forceScopeShrink) {
|
|
1682
1744
|
console.log(chalk.bold("\nHQ Sync — Now"));
|
|
@@ -1763,7 +1825,10 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1763
1825
|
...(nowPushPrefixSet !== undefined ? { prefixSet: nowPushPrefixSet } : {}),
|
|
1764
1826
|
});
|
|
1765
1827
|
const pushRefused = planLimitTracker.summary();
|
|
1766
|
-
const pushStatus = pushResult.aborted ||
|
|
1828
|
+
const pushStatus = pushResult.aborted ||
|
|
1829
|
+
pushResult.filesExcludedByScope > 0 ||
|
|
1830
|
+
(pushResult.filesRefusedStale ?? 0) > 0 ||
|
|
1831
|
+
pushRefused
|
|
1767
1832
|
? chalk.yellow("⚠")
|
|
1768
1833
|
: chalk.green("✓");
|
|
1769
1834
|
console.log(` ${pushStatus} ` +
|
|
@@ -1780,6 +1845,9 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1780
1845
|
console.log(chalk.yellow(line));
|
|
1781
1846
|
}
|
|
1782
1847
|
}
|
|
1848
|
+
for (const line of staleUploadRefusalLines(pushResult.filesRefusedStale ?? 0, pushResult.filesRefusedStalePaths)) {
|
|
1849
|
+
console.log(chalk.yellow(line));
|
|
1850
|
+
}
|
|
1783
1851
|
if (pushResult.aborted) {
|
|
1784
1852
|
console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
|
|
1785
1853
|
throw new SyncExitError(1);
|
|
@@ -1900,6 +1968,11 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1900
1968
|
"refused by your plan limit (listed above)."));
|
|
1901
1969
|
throw new SyncExitError(1, { health: "succeeded" });
|
|
1902
1970
|
}
|
|
1971
|
+
if (pushResult.filesExcludedByScope > 0 ||
|
|
1972
|
+
(pushResult.filesRefusedStale ?? 0) > 0) {
|
|
1973
|
+
console.log(chalk.yellow("\n⚠ Sync now finished, but one or more requested uploads were refused."));
|
|
1974
|
+
throw new SyncExitError(1, { health: "succeeded" });
|
|
1975
|
+
}
|
|
1903
1976
|
console.log(chalk.green("\n✓ Sync now complete"));
|
|
1904
1977
|
if (!personalMode &&
|
|
1905
1978
|
nudgeResolvedCompanyUid &&
|
|
@@ -1943,13 +2016,16 @@ async function runNowAll(hqRoot, message, onConflict, modeAllOverride, skipPerso
|
|
|
1943
2016
|
// pull-side).
|
|
1944
2017
|
await runPullAll(hqRoot, onConflict, modeAllOverride, skipPersonal, forceScopeShrink);
|
|
1945
2018
|
if (refused) {
|
|
1946
|
-
// A
|
|
1947
|
-
//
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2019
|
+
// A refused push does not stop the pull, but the run must not end looking
|
|
2020
|
+
// like a clean sync. Its per-target details were printed above.
|
|
2021
|
+
if (refused.planLimitRefused) {
|
|
2022
|
+
console.log(chalk.yellow(`\n⚠ Sync finished, but ${describeRefusedFiles(refused.planLimitRefused)} refused ` +
|
|
2023
|
+
"by your plan limit (listed under push --all above)."));
|
|
2024
|
+
if (refused.planLimitRefused.upgradeUrl) {
|
|
2025
|
+
console.log(chalk.yellow(` Upgrade: ${refused.planLimitRefused.upgradeUrl}`));
|
|
2026
|
+
}
|
|
1952
2027
|
}
|
|
2028
|
+
console.log(chalk.yellow("\n⚠ Sync finished, but one or more requested uploads were refused."));
|
|
1953
2029
|
throw new SyncExitError(1, { health: "succeeded" });
|
|
1954
2030
|
}
|
|
1955
2031
|
}
|
|
@@ -11,6 +11,8 @@ import { commandExists, delegateDiagnosticPath, isRecord, jsonAt, jqFallback, jq
|
|
|
11
11
|
import { resolveLiveRoot } from "../utils/hq-roots.js";
|
|
12
12
|
import { ACL_SOURCE_DISPLAY_LABELS } from "../utils/files-browse-acl.js";
|
|
13
13
|
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
14
|
+
import { resolveWindowsBash } from "../utils/windows-bash.js";
|
|
15
|
+
import { createShellCompatibleJqInvoker } from "../utils/jq-windows-compat.js";
|
|
14
16
|
const FLAG = "core-native-delegate";
|
|
15
17
|
const ASSETS = {
|
|
16
18
|
bundle: "core/scripts/hq-delegate-bundle.sh",
|
|
@@ -367,6 +369,13 @@ function runBundle(args, context, onMalformedWhoami) {
|
|
|
367
369
|
function runHq(args, context, stdout = "capture") {
|
|
368
370
|
return subprocess("hq", args, context.cwd, { stdout });
|
|
369
371
|
}
|
|
372
|
+
const runJq = createShellCompatibleJqInvoker((args, input, cwd, options) => subprocess("jq", args, cwd, { input, ...options }), process.platform, process.env, (cwd) => {
|
|
373
|
+
const bash = resolveWindowsBash(process.env).executable;
|
|
374
|
+
if (!bash)
|
|
375
|
+
return undefined;
|
|
376
|
+
const result = subprocess(bash, ["-c", "printf '%s' \"$OSTYPE\""], cwd, { stderr: "ignore" });
|
|
377
|
+
return result.code === 0 ? result.stdout?.trim() : undefined;
|
|
378
|
+
});
|
|
370
379
|
function commandSubstitutionBytes(value) {
|
|
371
380
|
const bytes = value.stdoutBytes ?? Buffer.from(value.stdout ?? "");
|
|
372
381
|
let end = bytes.length;
|
|
@@ -377,11 +386,15 @@ function commandSubstitutionBytes(value) {
|
|
|
377
386
|
function emitJqParseDiagnostic(input, args, context) {
|
|
378
387
|
if (!commandExists("jq", context.cwd))
|
|
379
388
|
return;
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
389
|
+
// cmd.exe cannot pass CR, LF, or NUL inside an argument. jq treats line
|
|
390
|
+
// breaks between tokens as whitespace, and these fixed filters contain no
|
|
391
|
+
// multiline string literals, so send the program as one argument on Windows.
|
|
392
|
+
const jqArgs = [...args];
|
|
393
|
+
const programIndex = jqArgs.length - 1;
|
|
394
|
+
if (programIndex >= 0)
|
|
395
|
+
jqArgs[programIndex] = (jqArgs[programIndex] ?? "").replace(/[\r\n\0]+/gu, " ");
|
|
396
|
+
const result = runJq(jqArgs, input, context.cwd, { stdout: "ignore", stderr: "pipe" });
|
|
397
|
+
const diagnostic = result.stderrBytes ?? Buffer.from(result.stderr ?? "");
|
|
385
398
|
if (diagnostic.length)
|
|
386
399
|
err(diagnostic);
|
|
387
400
|
}
|
|
@@ -13,6 +13,20 @@ export function registerCoreDeriveTriggerFactsCommand(core) {
|
|
|
13
13
|
.helpOption(false)
|
|
14
14
|
.argument("[args...]")
|
|
15
15
|
.action(async (args = [], _options, commandContext) => {
|
|
16
|
+
if (process.env.HQ_CORE_STDIN_CLOSED === "1") {
|
|
17
|
+
const root = liveRoot();
|
|
18
|
+
const result = runBundledScript({
|
|
19
|
+
asset: DERIVE_TRIGGER_FACTS_ASSET,
|
|
20
|
+
args: commandContext.args.length ? commandContext.args : args,
|
|
21
|
+
cwd: root,
|
|
22
|
+
hqRoot: root,
|
|
23
|
+
msysScriptPath: process.platform === "win32",
|
|
24
|
+
closeStdin: true,
|
|
25
|
+
});
|
|
26
|
+
if (result.code !== 0)
|
|
27
|
+
process.exit(result.code);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
16
30
|
if (!await coreNativeHookHelpersEnabled()) {
|
|
17
31
|
const root = liveRoot();
|
|
18
32
|
const stdin = process.platform === "win32" ? await readStdin() : undefined;
|
|
@@ -20,4 +20,5 @@ export declare function commandOnPath(command: string, env: Record<string, strin
|
|
|
20
20
|
/** Run the native handler contract, retaining exact bytes for differential tests. */
|
|
21
21
|
export declare function runDeriveTriggerFacts(options: DeriveTriggerRunOptions): DeriveTriggerRunResult;
|
|
22
22
|
export declare function runDeriveTriggerFactsFast(argv: readonly string[], stdin: string, cwd: string, hqRoot: string | undefined): number;
|
|
23
|
+
export declare function runDeriveTriggerFactsFastResult(argv: readonly string[], stdin: string, cwd: string, hqRoot: string | undefined): DeriveTriggerRunResult;
|
|
23
24
|
//# sourceMappingURL=core-derive-trigger-facts-native.d.ts.map
|
|
@@ -583,11 +583,30 @@ export function runDeriveTriggerFacts(options) {
|
|
|
583
583
|
}
|
|
584
584
|
}
|
|
585
585
|
export function runDeriveTriggerFactsFast(argv, stdin, cwd, hqRoot) {
|
|
586
|
-
const result =
|
|
586
|
+
const result = runDeriveTriggerFactsFastResult(argv, stdin, cwd, hqRoot);
|
|
587
587
|
if (result.stdout.length)
|
|
588
588
|
process.stdout.write(result.stdout);
|
|
589
589
|
if (result.stderr.length)
|
|
590
590
|
process.stderr.write(result.stderr);
|
|
591
591
|
return result.code;
|
|
592
592
|
}
|
|
593
|
+
export function runDeriveTriggerFactsFastResult(argv, stdin, cwd, hqRoot) {
|
|
594
|
+
if (process.env.HQ_CORE_STDIN_CLOSED === "1") {
|
|
595
|
+
const result = runBundledScript({
|
|
596
|
+
asset: DERIVE_TRIGGER_FACTS_ASSET,
|
|
597
|
+
args: [...argv],
|
|
598
|
+
cwd,
|
|
599
|
+
hqRoot: hqRoot ?? cwd,
|
|
600
|
+
msysScriptPath: process.platform === "win32",
|
|
601
|
+
closeStdin: true,
|
|
602
|
+
captureOutput: true,
|
|
603
|
+
});
|
|
604
|
+
return {
|
|
605
|
+
code: result.code,
|
|
606
|
+
stdout: result.stdout ?? Buffer.alloc(0),
|
|
607
|
+
stderr: result.stderr ?? Buffer.alloc(0),
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
return runDeriveTriggerFacts({ argv, stdin, cwd, hqRoot: hqRoot ?? cwd });
|
|
611
|
+
}
|
|
593
612
|
//# sourceMappingURL=core-derive-trigger-facts-native.js.map
|
package/dist/commands/daemon.js
CHANGED
|
@@ -30,6 +30,7 @@ import { daemonPaths } from "../lib/daemon/paths.js";
|
|
|
30
30
|
import { processCommandLine } from "../lib/daemon/lock.js";
|
|
31
31
|
import { writeControlRequest } from "../lib/daemon/requests.js";
|
|
32
32
|
import { runDaemonCall } from "../lib/daemon/command-call.js";
|
|
33
|
+
import { acknowledgeConflictNotice, conflictNoticeStateDir } from "../lib/daemon/conflict-notices.js";
|
|
33
34
|
import { isSyncRunnerCommandLine, readWatchRunnerOwnerStatus, syncRunnerPathFromCommandLine, watchRunnerOwnerStatusPath } from "../lib/daemon/sync-runner.js";
|
|
34
35
|
import { stagedSyncRunnerVersion, syncRunnerBundleStageRoot } from "../lib/daemon/sync-runner-bundle.js";
|
|
35
36
|
import { getSyncModeForCompany, setSyncMode } from "./sync-mode.js";
|
|
@@ -959,6 +960,21 @@ export function registerDaemonCommand(program, over = {}) {
|
|
|
959
960
|
const sync = daemon
|
|
960
961
|
.command("sync")
|
|
961
962
|
.description("Inspect and control the daemon's company sync runner");
|
|
963
|
+
sync
|
|
964
|
+
.command("conflicts")
|
|
965
|
+
.description("Manage parked conflict copy notices")
|
|
966
|
+
.command("acknowledge <noticeId>")
|
|
967
|
+
.description("Acknowledge a parked conflict copy notice")
|
|
968
|
+
.action(wrap(async (d, noticeId) => {
|
|
969
|
+
const host = currentHost(d);
|
|
970
|
+
const managedHome = host.runAsUser ? d.userHome(host.runAsUser) : d.home;
|
|
971
|
+
const stateDir = conflictNoticeStateDir(d.env, managedHome);
|
|
972
|
+
acknowledgeConflictNotice(noticeId, stateDir);
|
|
973
|
+
if (host.runAsUser && d.isRoot) {
|
|
974
|
+
d.run("chown", [`${host.runAsUser}:`, path.join(stateDir, "conflict-notice-acks.jsonl")]);
|
|
975
|
+
}
|
|
976
|
+
d.out("Conflict notice acknowledged.");
|
|
977
|
+
}));
|
|
962
978
|
sync
|
|
963
979
|
.command("status")
|
|
964
980
|
.description("Show sync ownership, heartbeat, last pass and log path")
|
|
@@ -119,6 +119,7 @@ export interface RunDoctorResult {
|
|
|
119
119
|
* read; all other default tiers remain local unless explicitly opted in.
|
|
120
120
|
*/
|
|
121
121
|
export declare function runDoctor(options?: RunDoctorOptions): Promise<RunDoctorResult>;
|
|
122
|
+
export declare function createClientHealthReasonCodeCompanyResolver(hqRoot: string): (companySlug: string | null) => Promise<boolean>;
|
|
122
123
|
export declare function createClientHealthCheckStatusCompanyResolver(hqRoot: string): (companySlug: string | null) => Promise<boolean>;
|
|
123
124
|
/** Register the top-level `hq doctor` command so it appears in `hq --help`. */
|
|
124
125
|
export declare function registerDoctorCommand(program: Command): void;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -207,7 +207,14 @@ async function resolveClientHealthReasonCodesCheckIds(families, resolveForCompan
|
|
|
207
207
|
continue;
|
|
208
208
|
}
|
|
209
209
|
const scoped = /^sync\.(?:journal|manifest)\.(.+)$/.exec(result.checkId);
|
|
210
|
-
const
|
|
210
|
+
const companyUidScope = scoped
|
|
211
|
+
? /^company:(cmp_[A-Za-z0-9]{3,128})$/.exec(scoped[1])
|
|
212
|
+
: null;
|
|
213
|
+
const companySlug = companyUidScope
|
|
214
|
+
? `company:${companyUidScope[1]}`
|
|
215
|
+
: scoped && isCompanyJournalSlug(scoped[1])
|
|
216
|
+
? scoped[1]
|
|
217
|
+
: null;
|
|
211
218
|
const checkIds = checksByCompany.get(companySlug) ?? [];
|
|
212
219
|
checkIds.push(result.checkId);
|
|
213
220
|
checksByCompany.set(companySlug, checkIds);
|
|
@@ -254,12 +261,18 @@ async function resolveClientHealthCheckStatusCheckIds(families, resolveForCompan
|
|
|
254
261
|
}
|
|
255
262
|
return enabledChecks;
|
|
256
263
|
}
|
|
257
|
-
function createClientHealthReasonCodeCompanyResolver(hqRoot) {
|
|
264
|
+
export function createClientHealthReasonCodeCompanyResolver(hqRoot) {
|
|
258
265
|
const companies = readCompaniesManifestMap(hqRoot);
|
|
259
266
|
return async (companySlug) => {
|
|
260
267
|
if (companySlug === null) {
|
|
261
268
|
return isActiveCompanyClientHealthReasonCodesEnabled();
|
|
262
269
|
}
|
|
270
|
+
const scopedCompanyUid = /^company:(cmp_[A-Za-z0-9]{3,128})$/.exec(companySlug);
|
|
271
|
+
if (scopedCompanyUid) {
|
|
272
|
+
return clientHealthReasonCodesEnabledForCompany({
|
|
273
|
+
companyUid: scopedCompanyUid[1],
|
|
274
|
+
});
|
|
275
|
+
}
|
|
263
276
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(companySlug)) {
|
|
264
277
|
return false;
|
|
265
278
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq import` command group.
|
|
3
|
+
*
|
|
4
|
+
* hq import scan human-readable scan summary
|
|
5
|
+
* hq import scan --json only the final `done` event (one JSON line)
|
|
6
|
+
* hq import scan --json --stream every scan event as JSON Lines
|
|
7
|
+
*
|
|
8
|
+
* Runs the HQ tree's import-context scanner with `--progress-json` (local
|
|
9
|
+
* only, read-only apart from report.json). The HQ tree is `--hq-root`, else
|
|
10
|
+
* the one the HQ installer recorded in ~/.hq/menubar.json; this command does
|
|
11
|
+
* not guess from the current folder or $HQ_ROOT. Event contract and types:
|
|
12
|
+
* src/lib/import-scan/events.ts and docs/import-scan-stream.md.
|
|
13
|
+
*/
|
|
14
|
+
import { Command, type CommanderError } from "commander";
|
|
15
|
+
/** Exit code for every usage error of `hq import scan` (contract). */
|
|
16
|
+
export declare const IMPORT_SCAN_USAGE_EXIT_CODE = 2;
|
|
17
|
+
/** Exit code for a Commander error raised while parsing `hq import scan`. */
|
|
18
|
+
export declare function importScanExitCode(err: CommanderError): number;
|
|
19
|
+
export interface RegisterImportCommandDeps {
|
|
20
|
+
/** Ends the process (default process.exit). Tests pass one that throws. */
|
|
21
|
+
exit?: (code: number, err: CommanderError) => never;
|
|
22
|
+
}
|
|
23
|
+
export declare function registerImportCommand(program: Command, deps?: RegisterImportCommandDeps): Command;
|
|
24
|
+
//# sourceMappingURL=import-scan.d.ts.map
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq import` command group.
|
|
3
|
+
*
|
|
4
|
+
* hq import scan human-readable scan summary
|
|
5
|
+
* hq import scan --json only the final `done` event (one JSON line)
|
|
6
|
+
* hq import scan --json --stream every scan event as JSON Lines
|
|
7
|
+
*
|
|
8
|
+
* Runs the HQ tree's import-context scanner with `--progress-json` (local
|
|
9
|
+
* only, read-only apart from report.json). The HQ tree is `--hq-root`, else
|
|
10
|
+
* the one the HQ installer recorded in ~/.hq/menubar.json; this command does
|
|
11
|
+
* not guess from the current folder or $HQ_ROOT. Event contract and types:
|
|
12
|
+
* src/lib/import-scan/events.ts and docs/import-scan-stream.md.
|
|
13
|
+
*/
|
|
14
|
+
import { runImportScan } from "../lib/import-scan/run.js";
|
|
15
|
+
function collect(value, previous = []) {
|
|
16
|
+
return [...previous, value];
|
|
17
|
+
}
|
|
18
|
+
/** Exit code for every usage error of `hq import scan` (contract). */
|
|
19
|
+
export const IMPORT_SCAN_USAGE_EXIT_CODE = 2;
|
|
20
|
+
/** Commander error codes that mean "the command line was wrong". */
|
|
21
|
+
const COMMANDER_USAGE_ERROR_CODES = new Set([
|
|
22
|
+
"commander.unknownOption",
|
|
23
|
+
"commander.unknownCommand",
|
|
24
|
+
"commander.missingArgument",
|
|
25
|
+
"commander.optionMissingArgument",
|
|
26
|
+
"commander.missingMandatoryOptionValue",
|
|
27
|
+
"commander.conflictingOption",
|
|
28
|
+
"commander.excessArguments",
|
|
29
|
+
"commander.invalidArgument",
|
|
30
|
+
]);
|
|
31
|
+
/** Exit code for a Commander error raised while parsing `hq import scan`. */
|
|
32
|
+
export function importScanExitCode(err) {
|
|
33
|
+
if (err.exitCode !== 0 && COMMANDER_USAGE_ERROR_CODES.has(err.code))
|
|
34
|
+
return IMPORT_SCAN_USAGE_EXIT_CODE;
|
|
35
|
+
return err.exitCode;
|
|
36
|
+
}
|
|
37
|
+
export function registerImportCommand(program, deps = {}) {
|
|
38
|
+
const exit = deps.exit ?? ((code) => process.exit(code));
|
|
39
|
+
const importCmd = program
|
|
40
|
+
.command("import")
|
|
41
|
+
.description("Import existing AI context (Claude Code, Codex, repos) into HQ");
|
|
42
|
+
importCmd
|
|
43
|
+
.command("scan")
|
|
44
|
+
.description("Scan this machine for AI context (sessions, repos, companies, projects); local only")
|
|
45
|
+
.option("--json", "Print only the final done event as JSON")
|
|
46
|
+
.option("--stream", "With --json, print every scan event as JSON Lines while scanning")
|
|
47
|
+
.option("--hq-root <path>", "HQ folder (default: the folder the HQ installer recorded)")
|
|
48
|
+
.option("--output <path>", "Where to write report.json (default: <hq>/workspace/imports/<scan id>/report.json)")
|
|
49
|
+
.option("--scope <dir>", "Also scan this folder (repeatable)", collect, [])
|
|
50
|
+
.option("--claude-export <path>", "Include a claude.ai data export (unzipped folder or conversations.json)")
|
|
51
|
+
.option("--no-default-scopes", "Scan only --scope folders, not the default code folders")
|
|
52
|
+
.addHelpText("after", "\nThe desktop app runs: hq import scan --json --stream\n" +
|
|
53
|
+
"Events (v1): start, source, count, company, project, error, done. See docs/import-scan-stream.md.")
|
|
54
|
+
// Usage errors (unknown option, missing value, --stream without --json)
|
|
55
|
+
// exit 2 per the scan contract. Scoped to this command only; Commander
|
|
56
|
+
// would otherwise exit 1 for most of them.
|
|
57
|
+
.exitOverride((err) => exit(importScanExitCode(err), err))
|
|
58
|
+
.action(async (opts, cmd) => {
|
|
59
|
+
if (opts.stream && !opts.json) {
|
|
60
|
+
cmd.error("error: --stream requires --json", { exitCode: 2 });
|
|
61
|
+
}
|
|
62
|
+
const mode = opts.json ? (opts.stream ? "stream" : "final") : "human";
|
|
63
|
+
const code = await runImportScan({
|
|
64
|
+
mode,
|
|
65
|
+
hqRoot: opts.hqRoot,
|
|
66
|
+
output: opts.output,
|
|
67
|
+
scopes: opts.scope,
|
|
68
|
+
claudeExport: opts.claudeExport,
|
|
69
|
+
noDefaultScopes: opts.defaultScopes === false,
|
|
70
|
+
}, { stdout: process.stdout, stderr: process.stderr });
|
|
71
|
+
process.exitCode = code;
|
|
72
|
+
});
|
|
73
|
+
return importCmd;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=import-scan.js.map
|
|
@@ -2,12 +2,15 @@ import type { Command } from "commander";
|
|
|
2
2
|
import { type TrackerCheckTeam } from "../lib/lanes/tracker-checks.js";
|
|
3
3
|
import { type TrackerKind, type TrackerRecord } from "../lib/lanes/tracker-store.js";
|
|
4
4
|
import type { LaneRecord, LaneCompanyRef, QuestionRecord } from "../lib/lanes/types.js";
|
|
5
|
+
import { type ReapDeps } from "../lib/lanes/reaper.js";
|
|
5
6
|
export interface LaneChecksDeps {
|
|
6
7
|
hqRoot: () => string;
|
|
7
8
|
resolveCompany: (company?: string) => Promise<LaneCompanyRef>;
|
|
8
9
|
now: () => Date;
|
|
9
10
|
stopLane?: (laneId: string, reason: string) => Promise<void>;
|
|
10
11
|
sessionState?: (root: string, sessionId: string, now: Date) => string | null;
|
|
12
|
+
stopEnabled?: (lane: LaneRecord) => Promise<boolean> | boolean;
|
|
13
|
+
reaperDeps?: Partial<ReapDeps>;
|
|
11
14
|
}
|
|
12
15
|
type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
|
|
13
16
|
export interface ProjectMapping {
|
|
@@ -13,6 +13,7 @@ import { LANES_RUNS_REL } from "../lib/lanes/spawn.js";
|
|
|
13
13
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
14
14
|
import { checkCompanyPath } from "../lib/lanes/checked-company-path.js";
|
|
15
15
|
import { findOrphanedBareIdDropboxes } from "../lib/lanes/orphaned-dropbox.js";
|
|
16
|
+
import { parseReapMinAge, reapLocalLanes } from "../lib/lanes/reaper.js";
|
|
16
17
|
export function mappingIncludesStory(mapping, storyId) {
|
|
17
18
|
return (mapping.storyPrefix === undefined || storyId.startsWith(mapping.storyPrefix))
|
|
18
19
|
&& (mapping.excludePrefix === undefined || !storyId.startsWith(mapping.excludePrefix));
|
|
@@ -905,6 +906,39 @@ async function orphanReport(root, company, now, minAgeMinutes, userIdleHours, de
|
|
|
905
906
|
output("orphans.stop", stopped);
|
|
906
907
|
}
|
|
907
908
|
export function registerLaneChecksCommand(parent, deps, wrap) {
|
|
909
|
+
parent.command("reap")
|
|
910
|
+
// CL-5's SessionStart hook launches this alias detached and does not wait for it.
|
|
911
|
+
.alias("_reap")
|
|
912
|
+
.description("Classify local lanes whose session senior has ended; report only by default")
|
|
913
|
+
.option("--min-age <minutes>", "Minimum lane age in minutes (default: 120)")
|
|
914
|
+
.option("--apply-stale-state", "Remove stale worker and watcher pid markers")
|
|
915
|
+
.option("--stop", "Stop candidate process trees when the hq-flags stop key is enabled")
|
|
916
|
+
.option("--reason <text>", "Required audit reason when --stop is set")
|
|
917
|
+
.option("--json", "Print machine-readable JSON (default)")
|
|
918
|
+
.action(wrap(async (options) => {
|
|
919
|
+
const reason = options.reason?.trim();
|
|
920
|
+
if (options.stop && !reason)
|
|
921
|
+
fail("lane_reap_reason_required", "--stop requires a non-empty --reason", {});
|
|
922
|
+
if (!options.stop && options.reason !== undefined)
|
|
923
|
+
fail("lane_reap_reason_requires_stop", "--reason can only be used with --stop", {});
|
|
924
|
+
if (reason)
|
|
925
|
+
assertNoSecretText("stop reason", reason);
|
|
926
|
+
const minAgeMinutes = parseReapMinAge(options.minAge);
|
|
927
|
+
const report = await reapLocalLanes(deps.hqRoot(), {
|
|
928
|
+
minAgeMinutes,
|
|
929
|
+
stop: options.stop === true,
|
|
930
|
+
applyStaleState: options.applyStaleState === true,
|
|
931
|
+
reason: reason ?? "Orphaned lane: owning session is confirmed ended",
|
|
932
|
+
json: true,
|
|
933
|
+
}, {
|
|
934
|
+
...deps.reaperDeps,
|
|
935
|
+
now: deps.now,
|
|
936
|
+
sessionState: deps.sessionState,
|
|
937
|
+
stopEnabled: deps.stopEnabled,
|
|
938
|
+
stopLane: deps.stopLane,
|
|
939
|
+
});
|
|
940
|
+
console.log(JSON.stringify(report, null, 2));
|
|
941
|
+
}));
|
|
908
942
|
parent.command("checks")
|
|
909
943
|
.description("Evaluate tracker and lane health checks")
|
|
910
944
|
.option("--company <slug>", "Select the company by slug")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
export interface SessionStartOptions {
|
|
4
|
+
env?: NodeJS.ProcessEnv;
|
|
5
|
+
cwd?: string;
|
|
6
|
+
stdin?: NodeJS.ReadableStream;
|
|
7
|
+
stdout?: Pick<NodeJS.WriteStream, "write">;
|
|
8
|
+
stderr?: Pick<NodeJS.WriteStream, "write">;
|
|
9
|
+
resolveRoot?: (env: NodeJS.ProcessEnv, cwd?: string) => string;
|
|
10
|
+
startSweep?: (root: string, env: NodeJS.ProcessEnv) => void;
|
|
11
|
+
}
|
|
12
|
+
/** Starts the CL-4 read-only orphan report once per host, without holding SessionStart open. */
|
|
13
|
+
export declare function startSessionStartOrphanSweep(root: string, env?: NodeJS.ProcessEnv, spawnDetached?: typeof spawn): void;
|
|
14
|
+
/** SessionStart-compatible, fail-open conduct bootstrap. */
|
|
15
|
+
export declare function runLaneSessionStart(options?: SessionStartOptions): Promise<void>;
|
|
16
|
+
export declare function registerLaneSessionStartCommand(lanes: Command): void;
|
|
17
|
+
//# sourceMappingURL=lanes-session-start.d.ts.map
|