@indigoai-us/hq-cli 5.268.0 → 5.270.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/command-catalog.generated.d.ts +77 -4
  3. package/dist/command-catalog.generated.js +98 -4
  4. package/dist/commands/files.js +6 -3
  5. package/dist/commands/lanes-hold.d.ts +2 -0
  6. package/dist/commands/lanes-hold.js +122 -310
  7. package/dist/commands/lanes-message.js +6 -4
  8. package/dist/commands/lanes-review.d.ts +10 -0
  9. package/dist/commands/lanes-review.js +56 -0
  10. package/dist/commands/lanes-shared.d.ts +1 -0
  11. package/dist/commands/lanes.d.ts +2 -0
  12. package/dist/commands/lanes.js +29 -4
  13. package/dist/lib/billing/plan-limit-upgrade-url.d.ts +1 -4
  14. package/dist/lib/billing/plan-limit-upgrade-url.js +7 -18
  15. package/dist/lib/doctor/checks/sync-runtime.d.ts +1 -1
  16. package/dist/lib/doctor/checks/sync-runtime.js +69 -19
  17. package/dist/lib/lanes/blocker-freshness.js +2 -1
  18. package/dist/lib/lanes/keep-alive.js +49 -20
  19. package/dist/lib/lanes/kr-freshness.js +3 -2
  20. package/dist/lib/lanes/lane-company.d.ts +3 -2
  21. package/dist/lib/lanes/lane-company.js +21 -4
  22. package/dist/lib/lanes/manager-status-gate.js +5 -3
  23. package/dist/lib/lanes/merge-command.d.ts +14 -0
  24. package/dist/lib/lanes/merge-command.js +363 -0
  25. package/dist/lib/lanes/reviews.d.ts +38 -0
  26. package/dist/lib/lanes/reviews.js +395 -0
  27. package/dist/lib/lanes/watcher.js +4 -2
  28. package/dist/lib/plan-limit-nag.js +2 -4
  29. package/dist/utils/feedback-logs.d.ts +14 -9
  30. package/dist/utils/feedback-logs.js +43 -14
  31. package/dist/utils/local-files-overview.d.ts +4 -2
  32. package/dist/utils/local-files-overview.js +47 -6
  33. package/dist/utils/sync-log-sources.d.ts +15 -0
  34. package/dist/utils/sync-log-sources.js +36 -0
  35. package/dist/utils/version-gate.d.ts +1 -0
  36. package/dist/utils/version-gate.js +6 -1
  37. package/package.json +1 -1
@@ -11,10 +11,11 @@
11
11
  * cross the wire; there is no string field on it for anything sensitive to
12
12
  * hide inside.
13
13
  *
14
- * Sources (metadata only, via `fs.stat` — full content is read only for the
14
+ * Sources (metadata only, via `fs.lstat` — full content is read only for the
15
15
  * bounded error-line count below, and even then only a capped tail window,
16
16
  * and even then only an integer count is ever returned):
17
- * - the hq-sync desktop app's log file, `~/.hq/logs/hq-sync.log`
17
+ * - the newest available desktop or daemon sync-log family and its rotations
18
+ * under `~/.hq/logs/` or `~/.hq/daemon/logs/`
18
19
  * - the CLI's own per-company sync journals, enumerated via `listJournals()`
19
20
  * (never a hand-reconstructed path — see client-health.ts's note on the
20
21
  * single-path-reconstruction regression this mirrors).
@@ -28,6 +29,7 @@ import * as fs from "node:fs";
28
29
  import * as os from "node:os";
29
30
  import * as path from "node:path";
30
31
  import { listJournals } from "@indigoai-us/hq-cloud";
32
+ import { currentSyncLogFamily, SYNC_LOG_GENERATIONS, syncLogBasePaths, syncLogGenerationPath, } from "./sync-log-sources.js";
31
33
  import { CLIENT_HEALTH_MAX_ERROR_LINE_COUNT, CLIENT_HEALTH_MAX_FILE_AGE_SECONDS, CLIENT_HEALTH_MAX_FILE_SIZE_BYTES, } from "./client-health-contract.js";
32
34
  /**
33
35
  * Only the last N bytes of the sync log are read for error-line counting —
@@ -37,7 +39,8 @@ import { CLIENT_HEALTH_MAX_ERROR_LINE_COUNT, CLIENT_HEALTH_MAX_FILE_AGE_SECONDS,
37
39
  export const LOCAL_FILES_OVERVIEW_TAIL_READ_BYTES = 2 * 1024 * 1024; // 2 MiB
38
40
  /** Case-insensitive marker matched against each line (or JSON-lines `level`). */
39
41
  const ERROR_MARKER = /error/i;
40
- const JSON_LEVEL_ERROR = /"level"\s*:\s*"error"/i;
42
+ const JSON_LEVEL_ERROR = /"level"\s*:\s*"error"\s*(?=[,}])/i;
43
+ const JSON_NUMERIC_LEVEL = /"level"\s*:\s*(-?\d+(?:\.\d+)?)\s*(?=[,}])/;
41
44
  const ABSENT = { exists: false, sizeBytes: 0, ageSeconds: 0 };
42
45
  function clampInt(value, max) {
43
46
  if (!Number.isFinite(value) || value < 0)
@@ -102,6 +105,12 @@ export function countRecentErrorLines(filePath, maxBytes = LOCAL_FILES_OVERVIEW_
102
105
  for (const line of text.split("\n")) {
103
106
  if (count >= cap)
104
107
  break;
108
+ const numericLevel = JSON_NUMERIC_LEVEL.exec(line)?.[1];
109
+ if (numericLevel !== undefined) {
110
+ if (Number(numericLevel) >= 50)
111
+ count += 1;
112
+ continue;
113
+ }
105
114
  if (ERROR_MARKER.test(line) || JSON_LEVEL_ERROR.test(line))
106
115
  count += 1;
107
116
  }
@@ -133,9 +142,41 @@ export function collectLocalFilesOverview(deps = {}) {
133
142
  const nowMs = now.getTime();
134
143
  const homeDir = (deps.homeDir ?? os.homedir)();
135
144
  const countErrorLines = deps.countErrorLines ?? countRecentErrorLines;
136
- const syncLogPath = path.join(homeDir, ".hq", "logs", "hq-sync.log");
137
- const syncLogFacts = statFacts(syncLogPath, nowMs);
138
- const recentErrorLineCount = syncLogFacts.exists ? countErrorLines(syncLogPath) : 0;
145
+ const hqDir = path.join(homeDir, ".hq");
146
+ const syncLogPaths = syncLogBasePaths(hqDir, deps.env);
147
+ const generations = {
148
+ desktop: [],
149
+ daemon: [],
150
+ };
151
+ const familyModifiedMs = (family) => {
152
+ let newest = null;
153
+ for (let generation = 0; generation < SYNC_LOG_GENERATIONS; generation += 1) {
154
+ const file = syncLogGenerationPath(syncLogPaths[family], generation);
155
+ try {
156
+ const stat = fs.lstatSync(file);
157
+ if (!stat.isFile())
158
+ continue;
159
+ generations[family].push({ file, modifiedMs: stat.mtimeMs });
160
+ newest = newest === null ? stat.mtimeMs : Math.max(newest, stat.mtimeMs);
161
+ }
162
+ catch {
163
+ // Missing, denied, or raced generations are absent from this overview.
164
+ }
165
+ }
166
+ return newest;
167
+ };
168
+ const family = currentSyncLogFamily(familyModifiedMs("desktop"), familyModifiedMs("daemon"));
169
+ const selectedFamily = family ?? "desktop";
170
+ const selectedFiles = generations[selectedFamily];
171
+ const activePath = syncLogPaths[selectedFamily];
172
+ const activeFacts = statFacts(activePath, nowMs);
173
+ const freshestGeneration = [...selectedFiles].sort((a, b) => b.modifiedMs - a.modifiedMs)[0];
174
+ const syncLogPath = activeFacts.exists ? activePath : freshestGeneration?.file;
175
+ const syncLogFacts = syncLogPath ? statFacts(syncLogPath, nowMs) : ABSENT;
176
+ let recentErrorLineCount = 0;
177
+ for (const generation of selectedFiles) {
178
+ recentErrorLineCount = Math.min(CLIENT_HEALTH_MAX_ERROR_LINE_COUNT, recentErrorLineCount + countErrorLines(generation.file));
179
+ }
139
180
  let journalEntries;
140
181
  try {
141
182
  journalEntries = (deps.journals ?? listJournals)();
@@ -0,0 +1,15 @@
1
+ /** Stable relative names used for feedback attachments. */
2
+ export declare const SYNC_LOG_RELATIVE_PATHS: {
3
+ readonly desktop: "logs/hq-sync.log";
4
+ readonly daemon: "daemon/logs/sync.log";
5
+ };
6
+ export type SyncLogFamily = keyof typeof SYNC_LOG_RELATIVE_PATHS;
7
+ /** Read the active file plus the three retained rotations. */
8
+ export declare const SYNC_LOG_GENERATIONS = 4;
9
+ /** Resolve both sync-log families, honoring the daemon's configured root. */
10
+ export declare function syncLogBasePaths(hqDir: string, env?: NodeJS.ProcessEnv): Record<SyncLogFamily, string>;
11
+ export declare function syncLogGenerationPath(basePath: string, generation: number): string;
12
+ export declare function syncLogFamilyForRelativePath(name: string): SyncLogFamily | null;
13
+ /** Choose the most recently written family; desktop wins a timestamp tie. */
14
+ export declare function currentSyncLogFamily(desktopModifiedMs: number | null, daemonModifiedMs: number | null): SyncLogFamily | null;
15
+ //# sourceMappingURL=sync-log-sources.d.ts.map
@@ -0,0 +1,36 @@
1
+ import * as path from "node:path";
2
+ import { daemonPaths } from "../lib/daemon/paths.js";
3
+ /** Stable relative names used for feedback attachments. */
4
+ export const SYNC_LOG_RELATIVE_PATHS = {
5
+ desktop: "logs/hq-sync.log",
6
+ daemon: "daemon/logs/sync.log",
7
+ };
8
+ /** Read the active file plus the three retained rotations. */
9
+ export const SYNC_LOG_GENERATIONS = 4;
10
+ /** Resolve both sync-log families, honoring the daemon's configured root. */
11
+ export function syncLogBasePaths(hqDir, env = process.env) {
12
+ const daemonLogDir = daemonPaths(path.dirname(hqDir), env).logsDir;
13
+ return {
14
+ desktop: path.join(hqDir, SYNC_LOG_RELATIVE_PATHS.desktop),
15
+ daemon: path.join(daemonLogDir, "sync.log"),
16
+ };
17
+ }
18
+ export function syncLogGenerationPath(basePath, generation) {
19
+ return generation === 0 ? basePath : `${basePath}.${generation}`;
20
+ }
21
+ export function syncLogFamilyForRelativePath(name) {
22
+ if (/^logs\/hq-sync\.log(?:\.\d+)?$/.test(name))
23
+ return "desktop";
24
+ if (/^daemon\/logs\/sync\.log(?:\.\d+)?$/.test(name))
25
+ return "daemon";
26
+ return null;
27
+ }
28
+ /** Choose the most recently written family; desktop wins a timestamp tie. */
29
+ export function currentSyncLogFamily(desktopModifiedMs, daemonModifiedMs) {
30
+ if (desktopModifiedMs === null)
31
+ return daemonModifiedMs === null ? null : "daemon";
32
+ if (daemonModifiedMs === null)
33
+ return "desktop";
34
+ return daemonModifiedMs > desktopModifiedMs ? "daemon" : "desktop";
35
+ }
36
+ //# sourceMappingURL=sync-log-sources.js.map
@@ -390,6 +390,7 @@ interface EnforceUpdateDeps {
390
390
  performUpdateString?: (command: string) => UpdateResult;
391
391
  resolveInstall?: () => RunningInstall;
392
392
  runner?: UpdateRunner;
393
+ platform?: NodeJS.Platform;
393
394
  cleanStale?: (prefix: string) => string[];
394
395
  acquireLock?: () => UpdateLockHandle | null;
395
396
  /**
@@ -1224,7 +1224,12 @@ function attemptRequiredUpdate(decision, deps, install) {
1224
1224
  // Never for pnpm or Bun: elevation changes the package manager's global home,
1225
1225
  // leaving the user's shim untouched while reporting success. A failed
1226
1226
  // non-npm update must surface instead.
1227
- if (!result.ok && result.code !== "ETIMEDOUT" && primaryCmd && !isManagedOutsideNpm) {
1227
+ const platform = deps.platform ?? process.platform;
1228
+ if (platform !== "win32" &&
1229
+ !result.ok &&
1230
+ result.code !== "ETIMEDOUT" &&
1231
+ primaryCmd &&
1232
+ !isManagedOutsideNpm) {
1228
1233
  console.error(chalk.dim(` Update failed unprivileged; retrying with: sudo -n ${primaryCmd} ${primaryArgs.join(" ")}`));
1229
1234
  const sudoResult = performUpdateCommand("sudo", ["-n", primaryCmd, ...primaryArgs], runner);
1230
1235
  if (sudoResult.ok)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.268.0",
3
+ "version": "5.270.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {