@indigoai-us/hq-cli 5.269.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.
- package/CHANGELOG.md +21 -0
- package/dist/command-catalog.generated.d.ts +4 -4
- package/dist/command-catalog.generated.js +4 -4
- package/dist/commands/files.js +6 -3
- package/dist/commands/lanes.js +3 -3
- package/dist/lib/billing/plan-limit-upgrade-url.d.ts +1 -4
- package/dist/lib/billing/plan-limit-upgrade-url.js +7 -18
- package/dist/lib/doctor/checks/sync-runtime.d.ts +1 -1
- package/dist/lib/doctor/checks/sync-runtime.js +69 -19
- package/dist/lib/plan-limit-nag.js +2 -4
- package/dist/utils/feedback-logs.d.ts +14 -9
- package/dist/utils/feedback-logs.js +43 -14
- package/dist/utils/local-files-overview.d.ts +4 -2
- package/dist/utils/local-files-overview.js +47 -6
- package/dist/utils/sync-log-sources.d.ts +15 -0
- package/dist/utils/sync-log-sources.js +36 -0
- package/dist/utils/version-gate.d.ts +1 -0
- package/dist/utils/version-gate.js +6 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.270.0] — 2026-09-28
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Company billing links in CLI plan-limit notices no longer depend on the
|
|
10
|
+
rollout flag.
|
|
11
|
+
|
|
12
|
+
### Removed
|
|
13
|
+
|
|
14
|
+
- Removed stale gate language from hire-grant command help. Enforcement remains
|
|
15
|
+
unconditional; backfill remains dry-run only.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- `hq files unshare --with` now accepts the same person UID principals as direct grants.
|
|
20
|
+
- The update gate no longer retries failed Windows installs through `sudo -n`.
|
|
21
|
+
|
|
5
22
|
## [5.269.0] — 2026-09-28
|
|
6
23
|
|
|
7
24
|
### Fixed
|
|
@@ -334,6 +351,10 @@
|
|
|
334
351
|
- Removed the unused `cli.network.force-ipv4` hq-flags lookup and forced IPv4
|
|
335
352
|
startup path. The existing Node family-attempt timeout remains unchanged.
|
|
336
353
|
- `hq auth login` continues when it cannot read or save the remembered provider. It prints a dim warning with the state path and error code; a save failure does not turn a successful sign-in into a login failure.
|
|
354
|
+
- `hq doctor`, client health metadata, and feedback attachments now use the
|
|
355
|
+
newest sync-log source from the desktop app or configured daemon directory.
|
|
356
|
+
Daemon `HQ_DAEMON_DIR` overrides and log rotations are supported, and
|
|
357
|
+
numeric runner error levels count in health metadata.
|
|
337
358
|
|
|
338
359
|
## [5.242.0] — 2026-09-27
|
|
339
360
|
|
|
@@ -1701,7 +1701,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
1701
1701
|
}];
|
|
1702
1702
|
readonly options: readonly [{
|
|
1703
1703
|
readonly flags: "--with <principal>";
|
|
1704
|
-
readonly description: "
|
|
1704
|
+
readonly description: "Person uid, email address, group id, or '@all' to remove the grant";
|
|
1705
1705
|
}];
|
|
1706
1706
|
readonly subcommands: readonly [];
|
|
1707
1707
|
}, {
|
|
@@ -7755,7 +7755,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7755
7755
|
readonly description: "Non-negative safe integer hire grant";
|
|
7756
7756
|
}, {
|
|
7757
7757
|
readonly flags: "--grant-context <base64url>";
|
|
7758
|
-
readonly description: "Primary-authorized hire hierarchy
|
|
7758
|
+
readonly description: "Primary-authorized hire hierarchy context";
|
|
7759
7759
|
}, {
|
|
7760
7760
|
readonly flags: "--json";
|
|
7761
7761
|
readonly description: "JSON on stdout (always)";
|
|
@@ -7807,7 +7807,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7807
7807
|
readonly subcommands: readonly [];
|
|
7808
7808
|
}, {
|
|
7809
7809
|
readonly name: "backfill-grants";
|
|
7810
|
-
readonly description: "Preview
|
|
7810
|
+
readonly description: "Preview missing hire grants for alive manager lanes; writes are disabled";
|
|
7811
7811
|
readonly aliases: readonly [];
|
|
7812
7812
|
readonly hidden: false;
|
|
7813
7813
|
readonly usage: "[options]";
|
|
@@ -7817,7 +7817,7 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7817
7817
|
readonly description: "Print proposed values without writing (default)";
|
|
7818
7818
|
}, {
|
|
7819
7819
|
readonly flags: "--apply";
|
|
7820
|
-
readonly description: "
|
|
7820
|
+
readonly description: "Retained for compatibility; writes are disabled";
|
|
7821
7821
|
}, {
|
|
7822
7822
|
readonly flags: "--json";
|
|
7823
7823
|
readonly description: "JSON on stdout (always)";
|
|
@@ -2170,7 +2170,7 @@ export const COMMAND_CATALOG = [
|
|
|
2170
2170
|
"options": [
|
|
2171
2171
|
{
|
|
2172
2172
|
"flags": "--with <principal>",
|
|
2173
|
-
"description": "
|
|
2173
|
+
"description": "Person uid, email address, group id, or '@all' to remove the grant"
|
|
2174
2174
|
}
|
|
2175
2175
|
],
|
|
2176
2176
|
"subcommands": []
|
|
@@ -10026,7 +10026,7 @@ export const COMMAND_CATALOG = [
|
|
|
10026
10026
|
},
|
|
10027
10027
|
{
|
|
10028
10028
|
"flags": "--grant-context <base64url>",
|
|
10029
|
-
"description": "Primary-authorized hire hierarchy
|
|
10029
|
+
"description": "Primary-authorized hire hierarchy context"
|
|
10030
10030
|
},
|
|
10031
10031
|
{
|
|
10032
10032
|
"flags": "--json",
|
|
@@ -10095,7 +10095,7 @@ export const COMMAND_CATALOG = [
|
|
|
10095
10095
|
},
|
|
10096
10096
|
{
|
|
10097
10097
|
"name": "backfill-grants",
|
|
10098
|
-
"description": "Preview
|
|
10098
|
+
"description": "Preview missing hire grants for alive manager lanes; writes are disabled",
|
|
10099
10099
|
"aliases": [],
|
|
10100
10100
|
"hidden": false,
|
|
10101
10101
|
"usage": "[options]",
|
|
@@ -10107,7 +10107,7 @@ export const COMMAND_CATALOG = [
|
|
|
10107
10107
|
},
|
|
10108
10108
|
{
|
|
10109
10109
|
"flags": "--apply",
|
|
10110
|
-
"description": "
|
|
10110
|
+
"description": "Retained for compatibility; writes are disabled"
|
|
10111
10111
|
},
|
|
10112
10112
|
{
|
|
10113
10113
|
"flags": "--json",
|
package/dist/commands/files.js
CHANGED
|
@@ -262,7 +262,7 @@ export function registerFilesCommand(program) {
|
|
|
262
262
|
files
|
|
263
263
|
.command("unshare <prefix>")
|
|
264
264
|
.description("Remove a file access grant. A trailing slash targets the private-folder row exactly.")
|
|
265
|
-
.requiredOption("--with <principal>", "
|
|
265
|
+
.requiredOption("--with <principal>", "Person uid, email address, group id, or '@all' to remove the grant")
|
|
266
266
|
.action(async (prefix, opts) => {
|
|
267
267
|
try {
|
|
268
268
|
// Unshare MUTATES access with no preview, and it resolves the target
|
|
@@ -278,7 +278,8 @@ export function registerFilesCommand(program) {
|
|
|
278
278
|
const isAll = principal === "@all";
|
|
279
279
|
const isEmail = !isAll && EMAIL_PATTERN.test(principal);
|
|
280
280
|
const isGroup = !isAll && GROUP_ID_PATTERN.test(principal);
|
|
281
|
-
|
|
281
|
+
const isPerson = !isAll && !isEmail && !isGroup && PERSON_UID_PATTERN.test(principal);
|
|
282
|
+
if (!isAll && !isEmail && !isGroup && !isPerson) {
|
|
282
283
|
console.error(chalk.red(`Invalid principal '${principal}': must be '@all', an email address, or a group id matching grp_<alphanumeric>`));
|
|
283
284
|
process.exit(1);
|
|
284
285
|
}
|
|
@@ -286,7 +287,9 @@ export function registerFilesCommand(program) {
|
|
|
286
287
|
? "company-wide"
|
|
287
288
|
: isEmail
|
|
288
289
|
? "email"
|
|
289
|
-
:
|
|
290
|
+
: isGroup
|
|
291
|
+
? "group"
|
|
292
|
+
: "person";
|
|
290
293
|
const granteeId = isAll
|
|
291
294
|
? ""
|
|
292
295
|
: isEmail
|
package/dist/commands/lanes.js
CHANGED
|
@@ -7002,7 +7002,7 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7002
7002
|
.command("grant-target", { hidden: true })
|
|
7003
7003
|
.argument("<lane-id>", "Lane id")
|
|
7004
7004
|
.option("--hire <n>", "Non-negative safe integer hire grant")
|
|
7005
|
-
.option("--grant-context <base64url>", "Primary-authorized hire hierarchy
|
|
7005
|
+
.option("--grant-context <base64url>", "Primary-authorized hire hierarchy context")
|
|
7006
7006
|
.action(wrap(async (laneId, opts) => {
|
|
7007
7007
|
await runGrantOwnerTarget(ctx, laneId, opts);
|
|
7008
7008
|
}));
|
|
@@ -7027,9 +7027,9 @@ export function registerLanesCommand(parent, deps = {}) {
|
|
|
7027
7027
|
}));
|
|
7028
7028
|
lanes
|
|
7029
7029
|
.command("backfill-grants")
|
|
7030
|
-
.description("Preview
|
|
7030
|
+
.description("Preview missing hire grants for alive manager lanes; writes are disabled")
|
|
7031
7031
|
.option("--dry-run", "Print proposed values without writing (default)")
|
|
7032
|
-
.option("--apply", "
|
|
7032
|
+
.option("--apply", "Retained for compatibility; writes are disabled")
|
|
7033
7033
|
.action(wrap(async (opts) => {
|
|
7034
7034
|
await runBackfillGrants(ctx, opts);
|
|
7035
7035
|
}));
|
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
export declare const PLAN_LIMIT_UPGRADE_FALLBACK_FLAG = "cli.plan-limit-upgrade-fallback";
|
|
2
|
-
export declare const PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT = false;
|
|
3
1
|
export interface PlanLimitUpgradeUrlChoices {
|
|
4
2
|
topLevelUrl?: string | null;
|
|
5
3
|
nestedUrl?: string | null;
|
|
6
4
|
legacyUrl: string;
|
|
7
5
|
}
|
|
8
|
-
export declare function isPlanLimitUpgradeFallbackEnabled(): boolean;
|
|
9
6
|
/** Resolve and attribute the final selected company billing destination. */
|
|
10
7
|
export declare function selectPlanLimitUpgradeUrl(companySlug: string | undefined, choices: PlanLimitUpgradeUrlChoices): string;
|
|
11
8
|
export declare function withCliLimitEntrySurface(value: string): string;
|
|
12
|
-
/**
|
|
9
|
+
/** Select the standard upgrade destination and label company limit links. */
|
|
13
10
|
export declare function planLimitUpgradeFallbackUrl(companySlug: string | undefined, previousFallbackUrl: string): string;
|
|
14
11
|
//# sourceMappingURL=plan-limit-upgrade-url.d.ts.map
|
|
@@ -1,11 +1,5 @@
|
|
|
1
|
-
import { resolveProcessFlagGate } from "../flag-registry.js";
|
|
2
1
|
import { DEFAULT_VAULT_API_URL } from "../../utils/cognito-session.js";
|
|
3
2
|
import { consoleOrigin } from "../../utils/console-origin.js";
|
|
4
|
-
export const PLAN_LIMIT_UPGRADE_FALLBACK_FLAG = "cli.plan-limit-upgrade-fallback";
|
|
5
|
-
export const PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT = false;
|
|
6
|
-
export function isPlanLimitUpgradeFallbackEnabled() {
|
|
7
|
-
return resolveProcessFlagGate(PLAN_LIMIT_UPGRADE_FALLBACK_FLAG, { fallback: PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT }, () => PLAN_LIMIT_UPGRADE_FALLBACK_DEFAULT);
|
|
8
|
-
}
|
|
9
3
|
function companyBillingUrl(companySlug) {
|
|
10
4
|
const controlPlaneUrl = process.env.HQ_VAULT_API_URL?.trim() || DEFAULT_VAULT_API_URL;
|
|
11
5
|
const origin = consoleOrigin(controlPlaneUrl);
|
|
@@ -19,20 +13,15 @@ function companyBillingUrl(companySlug) {
|
|
|
19
13
|
}
|
|
20
14
|
/** Resolve and attribute the final selected company billing destination. */
|
|
21
15
|
export function selectPlanLimitUpgradeUrl(companySlug, choices) {
|
|
16
|
+
const topLevelUrl = choices.topLevelUrl?.trim() || undefined;
|
|
17
|
+
const nestedUrl = choices.nestedUrl?.trim() || undefined;
|
|
18
|
+
const generatedUrl = companyBillingUrl(companySlug);
|
|
22
19
|
let selected;
|
|
23
|
-
if (
|
|
24
|
-
selected = choices.legacyUrl;
|
|
20
|
+
if (companySlug?.trim()) {
|
|
21
|
+
selected = generatedUrl ?? topLevelUrl ?? nestedUrl ?? choices.legacyUrl;
|
|
25
22
|
}
|
|
26
23
|
else {
|
|
27
|
-
|
|
28
|
-
const nestedUrl = choices.nestedUrl?.trim() || undefined;
|
|
29
|
-
const generatedUrl = companyBillingUrl(companySlug);
|
|
30
|
-
if (companySlug?.trim()) {
|
|
31
|
-
selected = generatedUrl ?? topLevelUrl ?? nestedUrl ?? choices.legacyUrl;
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
selected = topLevelUrl ?? nestedUrl ?? generatedUrl ?? choices.legacyUrl;
|
|
35
|
-
}
|
|
24
|
+
selected = topLevelUrl ?? nestedUrl ?? generatedUrl ?? choices.legacyUrl;
|
|
36
25
|
}
|
|
37
26
|
return withCliLimitEntrySurface(selected);
|
|
38
27
|
}
|
|
@@ -68,7 +57,7 @@ export function withCliLimitEntrySurface(value) {
|
|
|
68
57
|
}
|
|
69
58
|
return value;
|
|
70
59
|
}
|
|
71
|
-
/**
|
|
60
|
+
/** Select the standard upgrade destination and label company limit links. */
|
|
72
61
|
export function planLimitUpgradeFallbackUrl(companySlug, previousFallbackUrl) {
|
|
73
62
|
return selectPlanLimitUpgradeUrl(companySlug, {
|
|
74
63
|
legacyUrl: previousFallbackUrl,
|
|
@@ -7,5 +7,5 @@ export interface SyncRuntimeOptions {
|
|
|
7
7
|
/** Classify the last hour of sync log lines into privacy-safe results. */
|
|
8
8
|
export declare function analyzeSyncRuntimeLog(lines: readonly string[], options: SyncRuntimeOptions): CheckResult[];
|
|
9
9
|
/** Read the machine's sync logs and client health file. */
|
|
10
|
-
export declare function defaultSyncRuntimeResults(now: Date, hqHome?: string): CheckResult[];
|
|
10
|
+
export declare function defaultSyncRuntimeResults(now: Date, hqHome?: string, env?: NodeJS.ProcessEnv): CheckResult[];
|
|
11
11
|
//# sourceMappingURL=sync-runtime.d.ts.map
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as os from "node:os";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
+
import { currentSyncLogFamily, SYNC_LOG_GENERATIONS, syncLogBasePaths, syncLogGenerationPath, } from "../../../utils/sync-log-sources.js";
|
|
12
13
|
export const SYNC_RUNTIME_WINDOW_MS = 60 * 60_000;
|
|
13
14
|
const LAG_SECONDS = 15 * 60;
|
|
14
15
|
const LAG_MIN_RECEIPTS = 3;
|
|
@@ -32,6 +33,32 @@ function parseRecord(rest) {
|
|
|
32
33
|
return null;
|
|
33
34
|
}
|
|
34
35
|
}
|
|
36
|
+
function timestampFromRecord(record) {
|
|
37
|
+
if (!record)
|
|
38
|
+
return null;
|
|
39
|
+
for (const key of ["time", "timestamp", "ts"]) {
|
|
40
|
+
const value = record[key];
|
|
41
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
42
|
+
const millis = value < 100_000_000_000 ? value * 1000 : value;
|
|
43
|
+
if (Number.isFinite(millis))
|
|
44
|
+
return millis;
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === "string") {
|
|
47
|
+
const parsed = Date.parse(value);
|
|
48
|
+
if (Number.isFinite(parsed))
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
function lineTimestamp(line) {
|
|
55
|
+
const stamp = TIMESTAMP.exec(line);
|
|
56
|
+
if (stamp) {
|
|
57
|
+
const parsed = Date.parse(stamp[1]);
|
|
58
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
59
|
+
}
|
|
60
|
+
return timestampFromRecord(parseRecord(line));
|
|
61
|
+
}
|
|
35
62
|
/** Classify the last hour of sync log lines into privacy-safe results. */
|
|
36
63
|
export function analyzeSyncRuntimeLog(lines, options) {
|
|
37
64
|
const since = options.now - SYNC_RUNTIME_WINDOW_MS;
|
|
@@ -40,13 +67,13 @@ export function analyzeSyncRuntimeLog(lines, options) {
|
|
|
40
67
|
let starts = 0;
|
|
41
68
|
let deviceLimited = 0;
|
|
42
69
|
for (const line of lines) {
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
70
|
+
const time = lineTimestamp(line);
|
|
71
|
+
if (time === null)
|
|
45
72
|
continue;
|
|
46
|
-
const time = Date.parse(stamp[1]);
|
|
47
73
|
if (!Number.isFinite(time) || time < since || time > options.now)
|
|
48
74
|
continue;
|
|
49
|
-
const
|
|
75
|
+
const stamp = TIMESTAMP.exec(line);
|
|
76
|
+
const rest = stamp ? line.slice(stamp[0].length) : line;
|
|
50
77
|
if (DEVICE_LIMIT.test(rest))
|
|
51
78
|
deviceLimited += 1;
|
|
52
79
|
const record = parseRecord(rest);
|
|
@@ -61,7 +88,8 @@ export function analyzeSyncRuntimeLog(lines, options) {
|
|
|
61
88
|
else if (event === "receiver.start") {
|
|
62
89
|
starts += 1;
|
|
63
90
|
}
|
|
64
|
-
if ((record.type === "receiver" || record.type === "error"
|
|
91
|
+
if ((record.type === "receiver" || record.type === "error" ||
|
|
92
|
+
(typeof event === "string" && event.startsWith("receiver."))) &&
|
|
65
93
|
PERMISSION.test(JSON.stringify(record.err ?? record.message ?? ""))) {
|
|
66
94
|
denied += 1;
|
|
67
95
|
}
|
|
@@ -80,7 +108,7 @@ export function analyzeSyncRuntimeLog(lines, options) {
|
|
|
80
108
|
checkId: `sync.runtime.${check.id}`,
|
|
81
109
|
reasonCode: check.reasonCode,
|
|
82
110
|
message: `${check.count} ${check.label} in the last hour.`,
|
|
83
|
-
remediation: "Run `hq sync status` and
|
|
111
|
+
remediation: "Run `hq sync status` and inspect the selected sync log under ~/.hq/logs/, ~/.hq/daemon/logs/, or the configured HQ_DAEMON_DIR/logs/.",
|
|
84
112
|
}
|
|
85
113
|
: {
|
|
86
114
|
status: "PASS",
|
|
@@ -88,7 +116,6 @@ export function analyzeSyncRuntimeLog(lines, options) {
|
|
|
88
116
|
message: `${check.count} ${check.label} in the last hour.`,
|
|
89
117
|
});
|
|
90
118
|
}
|
|
91
|
-
const LOG_GENERATIONS = 4;
|
|
92
119
|
function readLogTail(file) {
|
|
93
120
|
const fd = fs.openSync(file, "r");
|
|
94
121
|
try {
|
|
@@ -104,9 +131,9 @@ function readLogTail(file) {
|
|
|
104
131
|
}
|
|
105
132
|
function firstTimestamp(lines) {
|
|
106
133
|
for (const line of lines) {
|
|
107
|
-
const
|
|
108
|
-
if (
|
|
109
|
-
return
|
|
134
|
+
const timestamp = lineTimestamp(line);
|
|
135
|
+
if (timestamp !== null)
|
|
136
|
+
return timestamp;
|
|
110
137
|
}
|
|
111
138
|
return null;
|
|
112
139
|
}
|
|
@@ -114,15 +141,21 @@ function firstTimestamp(lines) {
|
|
|
114
141
|
* Lines from the active log and as many rotated generations as needed to
|
|
115
142
|
* cover the window. Throws when a present log cannot be read.
|
|
116
143
|
*/
|
|
117
|
-
function readWindowLines(logFile, since) {
|
|
144
|
+
function readWindowLines(logFile, since, daemonLog) {
|
|
118
145
|
const generations = [];
|
|
119
|
-
for (let n = 0; n <
|
|
120
|
-
const file =
|
|
146
|
+
for (let n = 0; n < SYNC_LOG_GENERATIONS; n += 1) {
|
|
147
|
+
const file = syncLogGenerationPath(logFile, n);
|
|
121
148
|
if (!fs.existsSync(file))
|
|
122
|
-
|
|
149
|
+
continue;
|
|
123
150
|
const lines = readLogTail(file);
|
|
124
|
-
|
|
125
|
-
const
|
|
151
|
+
const fallbackTime = daemonLog ? fs.statSync(file).mtimeMs : null;
|
|
152
|
+
const timestampedLines = fallbackTime === null
|
|
153
|
+
? lines
|
|
154
|
+
: lines.map((line) => lineTimestamp(line) === null
|
|
155
|
+
? `${new Date(fallbackTime).toISOString()} ${line}`
|
|
156
|
+
: line);
|
|
157
|
+
generations.unshift(timestampedLines);
|
|
158
|
+
const first = firstTimestamp(timestampedLines);
|
|
126
159
|
if (first !== null && first <= since)
|
|
127
160
|
break;
|
|
128
161
|
}
|
|
@@ -139,15 +172,32 @@ function readConflictCount(file) {
|
|
|
139
172
|
}
|
|
140
173
|
}
|
|
141
174
|
/** Read the machine's sync logs and client health file. */
|
|
142
|
-
export function defaultSyncRuntimeResults(now, hqHome = path.join(os.homedir(), ".hq")) {
|
|
175
|
+
export function defaultSyncRuntimeResults(now, hqHome = path.join(os.homedir(), ".hq"), env = process.env) {
|
|
143
176
|
const options = {
|
|
144
177
|
now: now.getTime(),
|
|
145
178
|
conflictCount: readConflictCount(path.join(hqHome, "client-health.json")),
|
|
146
179
|
};
|
|
147
|
-
const
|
|
180
|
+
const paths = syncLogBasePaths(hqHome, env);
|
|
181
|
+
const familyModifiedMs = (family) => {
|
|
182
|
+
let newest = null;
|
|
183
|
+
for (let generation = 0; generation < SYNC_LOG_GENERATIONS; generation += 1) {
|
|
184
|
+
try {
|
|
185
|
+
const stat = fs.lstatSync(syncLogGenerationPath(paths[family], generation));
|
|
186
|
+
if (!stat.isFile() && !stat.isDirectory())
|
|
187
|
+
continue;
|
|
188
|
+
newest = newest === null ? stat.mtimeMs : Math.max(newest, stat.mtimeMs);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
// Missing or inaccessible generations do not make a source family newer.
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return newest;
|
|
195
|
+
};
|
|
196
|
+
const family = currentSyncLogFamily(familyModifiedMs("desktop"), familyModifiedMs("daemon"));
|
|
197
|
+
const logFile = family === null ? paths.desktop : paths[family];
|
|
148
198
|
let lines;
|
|
149
199
|
try {
|
|
150
|
-
lines = readWindowLines(logFile, options.now - SYNC_RUNTIME_WINDOW_MS);
|
|
200
|
+
lines = readWindowLines(logFile, options.now - SYNC_RUNTIME_WINDOW_MS, family === "daemon");
|
|
151
201
|
}
|
|
152
202
|
catch {
|
|
153
203
|
return [
|
|
@@ -37,7 +37,7 @@ import * as fs from "node:fs";
|
|
|
37
37
|
import * as os from "node:os";
|
|
38
38
|
import * as path from "node:path";
|
|
39
39
|
import { resolveProcessFlagCompanyContext } from "./flag-registry.js";
|
|
40
|
-
import {
|
|
40
|
+
import { planLimitUpgradeFallbackUrl, selectPlanLimitUpgradeUrl, } from "./billing/plan-limit-upgrade-url.js";
|
|
41
41
|
export const PLAN_LIMIT_UPGRADE_URL = "https://hq.computer/billing/upgrade";
|
|
42
42
|
/**
|
|
43
43
|
* Cadence of the over-limit per-turn line (US-036). Was once a day; the nag
|
|
@@ -230,9 +230,7 @@ export function recordPlanLimitStatus(body, opts = {}) {
|
|
|
230
230
|
: null;
|
|
231
231
|
const topLevelUpgradeUrl = parseUpgradeUrl(bodyRecord?.upgradeUrl);
|
|
232
232
|
const nestedUpgradeUrl = parseNestedUpgradeUrl(body);
|
|
233
|
-
const companySlug =
|
|
234
|
-
? resolveProcessFlagCompanyContext().companySlug
|
|
235
|
-
: undefined;
|
|
233
|
+
const companySlug = resolveProcessFlagCompanyContext().companySlug;
|
|
236
234
|
resolvedUpgradeUrl = selectPlanLimitUpgradeUrl(companySlug, {
|
|
237
235
|
topLevelUrl: topLevelUpgradeUrl,
|
|
238
236
|
nestedUrl: nestedUpgradeUrl,
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* (`hq-sync.log.1`, `.2`, `.3`) are eligible too — just after a rotation
|
|
14
14
|
* the active file is nearly empty and the history lives in `.1`.
|
|
15
15
|
* `logs/mcp-registry.log` is the CLI's own append-only MCP audit trail.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* The daemon runner's `daemon/logs/sync.log` and its three rotations are
|
|
17
|
+
* eligible too. When both sync families exist, only the newer family is
|
|
18
|
+
* attached.
|
|
18
19
|
*
|
|
19
20
|
* - STATE SNAPSHOTS (`state`) — small JSON files that describe where sync
|
|
20
21
|
* and client health currently stand: the in-flight sync phase and conflict
|
|
@@ -36,8 +37,9 @@
|
|
|
36
37
|
*
|
|
37
38
|
* 1. ALLOWLIST, NOT A SCAN. `~/.hq` also holds `cognito-tokens.json`,
|
|
38
39
|
* `deploy-passwords.json`, and `secrets-cache/`. Only `*.log` files
|
|
39
|
-
* directly inside `~/.hq/logs
|
|
40
|
-
*
|
|
40
|
+
* directly inside `~/.hq/logs/`, the exact daemon sync-log generations
|
|
41
|
+
* under the configured daemon root, and the two known root-level boot
|
|
42
|
+
* logs are eligible. A recursive walk over `~/.hq` is a credential leak
|
|
41
43
|
* and must never be introduced here.
|
|
42
44
|
*
|
|
43
45
|
* 2. NO SYMLINKS. Eligibility is decided with `lstat`, and a candidate must
|
|
@@ -235,12 +237,13 @@ export declare function feedbackLogsEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
|
235
237
|
* Enumerate eligible log files under `hqDir`.
|
|
236
238
|
*
|
|
237
239
|
* Eligibility is the allowlist described in the module header: `*.log` files
|
|
238
|
-
* directly inside `hqDir/logs/`,
|
|
239
|
-
* the root. Every candidate must be a
|
|
240
|
-
* and, critically, symlinks are rejected.
|
|
241
|
-
* the most relevant log is offered
|
|
240
|
+
* directly inside `hqDir/logs/`, the exact daemon sync-log generations, plus
|
|
241
|
+
* {@link FEEDBACK_ROOT_LOG_FILENAMES} at the root. Every candidate must be a
|
|
242
|
+
* REGULAR file by `lstat` — directories and, critically, symlinks are rejected.
|
|
243
|
+
* Returned newest-modified first so the most relevant log is offered budget
|
|
244
|
+
* before older ones.
|
|
242
245
|
*/
|
|
243
|
-
export declare function discoverLogFiles(hqDir: string): LogCandidate[];
|
|
246
|
+
export declare function discoverLogFiles(hqDir: string, env?: NodeJS.ProcessEnv): LogCandidate[];
|
|
244
247
|
/**
|
|
245
248
|
* Read the last `maxBytes` of a file.
|
|
246
249
|
*
|
|
@@ -314,6 +317,8 @@ export interface CollectFeedbackLogsOptions {
|
|
|
314
317
|
hqDir?: string;
|
|
315
318
|
/** Override the home directory used to derive `~/.hq` (tests). */
|
|
316
319
|
homeDir?: string;
|
|
320
|
+
/** Override process environment for configured log paths (tests). */
|
|
321
|
+
env?: NodeJS.ProcessEnv;
|
|
317
322
|
}
|
|
318
323
|
/**
|
|
319
324
|
* Collect redacted `~/.hq` evidence within `budgetBytes`.
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* (`hq-sync.log.1`, `.2`, `.3`) are eligible too — just after a rotation
|
|
14
14
|
* the active file is nearly empty and the history lives in `.1`.
|
|
15
15
|
* `logs/mcp-registry.log` is the CLI's own append-only MCP audit trail.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* The daemon runner's `daemon/logs/sync.log` and its three rotations are
|
|
17
|
+
* eligible too. When both sync families exist, only the newer family is
|
|
18
|
+
* attached.
|
|
18
19
|
*
|
|
19
20
|
* - STATE SNAPSHOTS (`state`) — small JSON files that describe where sync
|
|
20
21
|
* and client health currently stand: the in-flight sync phase and conflict
|
|
@@ -36,8 +37,9 @@
|
|
|
36
37
|
*
|
|
37
38
|
* 1. ALLOWLIST, NOT A SCAN. `~/.hq` also holds `cognito-tokens.json`,
|
|
38
39
|
* `deploy-passwords.json`, and `secrets-cache/`. Only `*.log` files
|
|
39
|
-
* directly inside `~/.hq/logs
|
|
40
|
-
*
|
|
40
|
+
* directly inside `~/.hq/logs/`, the exact daemon sync-log generations
|
|
41
|
+
* under the configured daemon root, and the two known root-level boot
|
|
42
|
+
* logs are eligible. A recursive walk over `~/.hq` is a credential leak
|
|
41
43
|
* and must never be introduced here.
|
|
42
44
|
*
|
|
43
45
|
* 2. NO SYMLINKS. Eligibility is decided with `lstat`, and a candidate must
|
|
@@ -58,6 +60,7 @@
|
|
|
58
60
|
import * as fs from "node:fs";
|
|
59
61
|
import * as os from "node:os";
|
|
60
62
|
import * as path from "node:path";
|
|
63
|
+
import { currentSyncLogFamily, SYNC_LOG_RELATIVE_PATHS, SYNC_LOG_GENERATIONS, syncLogBasePaths, syncLogFamilyForRelativePath, syncLogGenerationPath, } from "./sync-log-sources.js";
|
|
61
64
|
/**
|
|
62
65
|
* Ceiling on the total redacted log text attached to one submission.
|
|
63
66
|
*
|
|
@@ -279,15 +282,15 @@ function lstatOrNull(absPath) {
|
|
|
279
282
|
* Enumerate eligible log files under `hqDir`.
|
|
280
283
|
*
|
|
281
284
|
* Eligibility is the allowlist described in the module header: `*.log` files
|
|
282
|
-
* directly inside `hqDir/logs/`,
|
|
283
|
-
* the root. Every candidate must be a
|
|
284
|
-
* and, critically, symlinks are rejected.
|
|
285
|
-
* the most relevant log is offered
|
|
285
|
+
* directly inside `hqDir/logs/`, the exact daemon sync-log generations, plus
|
|
286
|
+
* {@link FEEDBACK_ROOT_LOG_FILENAMES} at the root. Every candidate must be a
|
|
287
|
+
* REGULAR file by `lstat` — directories and, critically, symlinks are rejected.
|
|
288
|
+
* Returned newest-modified first so the most relevant log is offered budget
|
|
289
|
+
* before older ones.
|
|
286
290
|
*/
|
|
287
|
-
export function discoverLogFiles(hqDir) {
|
|
291
|
+
export function discoverLogFiles(hqDir, env = process.env) {
|
|
288
292
|
const candidates = [];
|
|
289
|
-
const consider = (relName) => {
|
|
290
|
-
const absPath = path.join(hqDir, relName);
|
|
293
|
+
const consider = (relName, absPath = path.join(hqDir, relName)) => {
|
|
291
294
|
const st = lstatOrNull(absPath);
|
|
292
295
|
// isFile() is false for both directories and symlinks under lstat. A
|
|
293
296
|
// symlink here would be the credential-exfiltration path (see header).
|
|
@@ -313,11 +316,37 @@ export function discoverLogFiles(hqDir) {
|
|
|
313
316
|
continue;
|
|
314
317
|
consider(`${FEEDBACK_LOGS_DIRNAME}/${entry}`);
|
|
315
318
|
}
|
|
319
|
+
const daemonLogPath = syncLogBasePaths(hqDir, env).daemon;
|
|
320
|
+
for (let generation = 0; generation < SYNC_LOG_GENERATIONS; generation += 1) {
|
|
321
|
+
const relName = syncLogGenerationPath(SYNC_LOG_RELATIVE_PATHS.daemon, generation);
|
|
322
|
+
const absPath = syncLogGenerationPath(daemonLogPath, generation);
|
|
323
|
+
consider(relName, absPath);
|
|
324
|
+
}
|
|
316
325
|
for (const filename of FEEDBACK_ROOT_LOG_FILENAMES) {
|
|
317
326
|
consider(filename);
|
|
318
327
|
}
|
|
319
|
-
|
|
320
|
-
|
|
328
|
+
let desktopModifiedMs = null;
|
|
329
|
+
let daemonModifiedMs = null;
|
|
330
|
+
for (const candidate of candidates) {
|
|
331
|
+
const family = syncLogFamilyForRelativePath(candidate.name);
|
|
332
|
+
if (family === "desktop") {
|
|
333
|
+
desktopModifiedMs = desktopModifiedMs === null
|
|
334
|
+
? candidate.modifiedMs
|
|
335
|
+
: Math.max(desktopModifiedMs, candidate.modifiedMs);
|
|
336
|
+
}
|
|
337
|
+
else if (family === "daemon") {
|
|
338
|
+
daemonModifiedMs = daemonModifiedMs === null
|
|
339
|
+
? candidate.modifiedMs
|
|
340
|
+
: Math.max(daemonModifiedMs, candidate.modifiedMs);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const selectedFamily = currentSyncLogFamily(desktopModifiedMs, daemonModifiedMs);
|
|
344
|
+
const selected = candidates.filter((candidate) => {
|
|
345
|
+
const family = syncLogFamilyForRelativePath(candidate.name);
|
|
346
|
+
return family === null || family === selectedFamily;
|
|
347
|
+
});
|
|
348
|
+
selected.sort((a, b) => b.modifiedMs - a.modifiedMs);
|
|
349
|
+
return selected;
|
|
321
350
|
}
|
|
322
351
|
/**
|
|
323
352
|
* Read the last `maxBytes` of a file.
|
|
@@ -546,7 +575,7 @@ export function collectFeedbackLogs(opts = {}) {
|
|
|
546
575
|
let logCandidates;
|
|
547
576
|
let stateCandidates;
|
|
548
577
|
try {
|
|
549
|
-
logCandidates = discoverLogFiles(hqDir);
|
|
578
|
+
logCandidates = discoverLogFiles(hqDir, opts.env);
|
|
550
579
|
// Order matters: root status documents first, then the lock/claim files
|
|
551
580
|
// (highest signal for a hang), then scheduled-job status. Under a tight
|
|
552
581
|
// budget the earlier ones win, and that is the intended priority.
|
|
@@ -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.
|
|
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
|
|
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).
|
|
@@ -42,6 +43,7 @@ export declare function countRecentErrorLines(filePath: string, maxBytes?: numbe
|
|
|
42
43
|
export interface CollectLocalFilesOverviewDeps {
|
|
43
44
|
now?: () => Date;
|
|
44
45
|
homeDir?: () => string;
|
|
46
|
+
env?: NodeJS.ProcessEnv;
|
|
45
47
|
journals?: () => readonly {
|
|
46
48
|
path: string;
|
|
47
49
|
}[];
|
|
@@ -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.
|
|
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
|
|
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
|
|
137
|
-
const
|
|
138
|
-
const
|
|
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
|
-
|
|
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)
|