@indigoai-us/hq-cli 5.345.163 → 5.345.164
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 +28 -0
- package/dist/commands/billing-usage.d.ts +16 -2
- package/dist/commands/billing-usage.js +63 -9
- package/dist/commands/billing.d.ts +34 -0
- package/dist/commands/billing.js +108 -1
- package/dist/commands/deploy-domains.d.ts +2 -0
- package/dist/commands/deploy-domains.js +18 -3
- package/dist/lib/billing/billing-status-lines.d.ts +14 -0
- package/dist/lib/billing/billing-status-lines.js +43 -0
- package/dist/lib/billing/usage-only.d.ts +18 -0
- package/dist/lib/billing/usage-only.js +18 -1
- package/dist/lib/doctor/checks/sync-runtime.d.ts +34 -0
- package/dist/lib/doctor/checks/sync-runtime.js +217 -8
- package/dist/lib/doctor/fix/apply.js +1 -0
- package/dist/lib/doctor/fix/refresh-sync-outcome.d.ts +4 -1
- package/dist/lib/doctor/fix/refresh-sync-outcome.js +15 -0
- package/dist/lib/doctor/fix/refresh-sync-progress.d.ts +42 -0
- package/dist/lib/doctor/fix/refresh-sync-progress.js +100 -0
- package/dist/lib/doctor/json-output.d.ts +7 -0
- package/dist/lib/doctor/json-output.js +8 -1
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +1 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -1
- package/dist/lib/mesh/live/daemon/work-subscribe.js +121 -38
- package/dist/lib/mesh/live/paths.d.ts +1 -0
- package/dist/lib/mesh/live/paths.js +1 -0
- package/package.json +7 -7
|
@@ -4,46 +4,67 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
+
import { randomUUID } from "node:crypto";
|
|
7
8
|
import { createPersonalRealtimeFetcher, } from "./credentials.js";
|
|
8
9
|
import { presignIotWssUrl } from "./presign.js";
|
|
9
10
|
import { applyProjectViewPush, parseProjectViewPush } from "./live-cache.js";
|
|
10
11
|
import { backoffDelayMs, RECONNECT_MIN_DELAY_MS } from "./presence.js";
|
|
11
|
-
import { AGENT_INBOX_PUSH_STATE_BASENAME } from "../paths.js";
|
|
12
|
-
export { AGENT_INBOX_PUSH_STATE_BASENAME } from "../paths.js";
|
|
12
|
+
import { AGENT_INBOX_PUSH_STATE_BASENAME, SYNC_WAKE_STATE_BASENAME } from "../paths.js";
|
|
13
|
+
export { AGENT_INBOX_PUSH_STATE_BASENAME, SYNC_WAKE_STATE_BASENAME } from "../paths.js";
|
|
13
14
|
const INBOX_PUSH_HEARTBEAT_MS = 5_000;
|
|
14
|
-
function
|
|
15
|
-
const target = path.join(meshRoot,
|
|
15
|
+
function readWakeSequence(meshRoot, basename, label, onInfo) {
|
|
16
|
+
const target = path.join(meshRoot, basename);
|
|
16
17
|
try {
|
|
17
18
|
const info = fs.lstatSync(target);
|
|
18
19
|
if (!info.isFile() || info.isSymbolicLink()) {
|
|
19
|
-
onInfo?.(
|
|
20
|
-
return 0;
|
|
20
|
+
onInfo?.(`${label} state read ignored (not a regular file)`);
|
|
21
|
+
return { sequence: 0, writable: false };
|
|
21
22
|
}
|
|
22
23
|
const parsed = JSON.parse(fs.readFileSync(target, "utf8"));
|
|
23
24
|
if (Number.isSafeInteger(parsed.wakeSequence) && Number(parsed.wakeSequence) >= 0) {
|
|
24
|
-
return Number(parsed.wakeSequence);
|
|
25
|
+
return { sequence: Number(parsed.wakeSequence), writable: true };
|
|
25
26
|
}
|
|
26
|
-
onInfo?.(
|
|
27
|
-
return 0;
|
|
27
|
+
onInfo?.(`${label} state read ignored (invalid wake sequence)`);
|
|
28
|
+
return { sequence: 0, writable: false };
|
|
28
29
|
}
|
|
29
30
|
catch (error) {
|
|
30
31
|
if (error.code !== "ENOENT") {
|
|
31
32
|
const name = error instanceof Error ? error.name : "UnknownError";
|
|
32
|
-
onInfo?.(
|
|
33
|
+
onInfo?.(`${label} state read failed (${name})`);
|
|
33
34
|
}
|
|
34
|
-
return 0;
|
|
35
|
+
return { sequence: 0, writable: error.code === "ENOENT" };
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
|
-
function
|
|
38
|
+
function writeWakeState(meshRoot, basename, state, durable) {
|
|
38
39
|
fs.mkdirSync(meshRoot, { recursive: true, mode: 0o700 });
|
|
39
|
-
const
|
|
40
|
-
|
|
40
|
+
const rootInfo = fs.lstatSync(meshRoot);
|
|
41
|
+
if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink()) {
|
|
42
|
+
throw new Error("wake state root is not a regular directory");
|
|
43
|
+
}
|
|
44
|
+
const target = path.join(meshRoot, basename);
|
|
45
|
+
const temporary = path.join(meshRoot, `.${basename}.${process.pid}.${randomUUID()}.tmp`);
|
|
46
|
+
let file;
|
|
41
47
|
try {
|
|
42
|
-
fs.
|
|
43
|
-
fs.
|
|
48
|
+
file = fs.openSync(temporary, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW, 0o600);
|
|
49
|
+
fs.writeFileSync(file, `${JSON.stringify(state)}\n`);
|
|
50
|
+
if (durable)
|
|
51
|
+
fs.fsyncSync(file);
|
|
52
|
+
fs.closeSync(file);
|
|
53
|
+
file = undefined;
|
|
44
54
|
fs.renameSync(temporary, target);
|
|
55
|
+
if (durable && process.platform !== "win32") {
|
|
56
|
+
const directory = fs.openSync(meshRoot, fs.constants.O_RDONLY);
|
|
57
|
+
try {
|
|
58
|
+
fs.fsyncSync(directory);
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
fs.closeSync(directory);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
45
64
|
}
|
|
46
65
|
catch (error) {
|
|
66
|
+
if (file !== undefined)
|
|
67
|
+
fs.closeSync(file);
|
|
47
68
|
try {
|
|
48
69
|
fs.unlinkSync(temporary);
|
|
49
70
|
}
|
|
@@ -79,37 +100,46 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
79
100
|
let heartbeatTimer = null;
|
|
80
101
|
let generation = 0;
|
|
81
102
|
let inboxSubscribed = false;
|
|
82
|
-
let
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
103
|
+
let syncSubscribed = false;
|
|
104
|
+
let actorUid;
|
|
105
|
+
let inboxWakeSequence = readWakeSequence(opts.meshRoot, AGENT_INBOX_PUSH_STATE_BASENAME, "agent inbox push", opts.onInfo).sequence;
|
|
106
|
+
const syncWakeState = readWakeSequence(opts.meshRoot, SYNC_WAKE_STATE_BASENAME, "sync wake", opts.onInfo);
|
|
107
|
+
let syncWakeSequence = syncWakeState.sequence;
|
|
108
|
+
const syncStateWritable = syncWakeState.writable;
|
|
86
109
|
const clearInboxHeartbeat = () => {
|
|
87
110
|
if (heartbeatTimer !== null)
|
|
88
111
|
timers.clearTimeout(heartbeatTimer);
|
|
89
112
|
heartbeatTimer = null;
|
|
90
113
|
};
|
|
91
|
-
const
|
|
114
|
+
const persistState = (basename, label, connected, wakeSequence, identity, durable = true) => {
|
|
92
115
|
try {
|
|
93
|
-
|
|
116
|
+
writeWakeState(opts.meshRoot, basename, {
|
|
94
117
|
connected,
|
|
95
118
|
heartbeatAtMs: now().getTime(),
|
|
96
119
|
wakeSequence,
|
|
97
|
-
...(connected &&
|
|
98
|
-
});
|
|
120
|
+
...(connected && actorUid && identity ? { [identity]: actorUid } : {}),
|
|
121
|
+
}, durable);
|
|
99
122
|
}
|
|
100
123
|
catch (error) {
|
|
101
124
|
const name = error instanceof Error ? error.name : "UnknownError";
|
|
102
|
-
opts.onInfo?.(
|
|
125
|
+
opts.onInfo?.(`${label} state write failed (${name})`);
|
|
103
126
|
}
|
|
104
127
|
};
|
|
128
|
+
const persistInboxState = (connected, durable = true) => persistState(AGENT_INBOX_PUSH_STATE_BASENAME, "agent inbox push", connected, inboxWakeSequence, "agentUid", durable);
|
|
129
|
+
const persistSyncState = (connected, durable = true) => syncStateWritable
|
|
130
|
+
? persistState(SYNC_WAKE_STATE_BASENAME, "sync wake", connected, syncWakeSequence, undefined, durable)
|
|
131
|
+
: undefined;
|
|
105
132
|
const scheduleInboxHeartbeat = () => {
|
|
106
|
-
if (stopped || !inboxSubscribed)
|
|
133
|
+
if (stopped || (!inboxSubscribed && !syncSubscribed))
|
|
107
134
|
return;
|
|
108
135
|
clearInboxHeartbeat();
|
|
109
136
|
heartbeatTimer = timers.setTimeout(() => {
|
|
110
137
|
heartbeatTimer = null;
|
|
111
|
-
if (!stopped && inboxSubscribed) {
|
|
112
|
-
|
|
138
|
+
if (!stopped && (inboxSubscribed || syncSubscribed)) {
|
|
139
|
+
if (inboxSubscribed)
|
|
140
|
+
persistInboxState(true, false);
|
|
141
|
+
if (syncSubscribed)
|
|
142
|
+
persistSyncState(true, false);
|
|
113
143
|
scheduleInboxHeartbeat();
|
|
114
144
|
}
|
|
115
145
|
}, INBOX_PUSH_HEARTBEAT_MS);
|
|
@@ -118,9 +148,11 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
118
148
|
if (stopped)
|
|
119
149
|
return;
|
|
120
150
|
inboxSubscribed = false;
|
|
121
|
-
|
|
151
|
+
syncSubscribed = false;
|
|
152
|
+
actorUid = undefined;
|
|
122
153
|
clearInboxHeartbeat();
|
|
123
154
|
persistInboxState(false);
|
|
155
|
+
persistSyncState(false);
|
|
124
156
|
const gen = ++generation;
|
|
125
157
|
let bundle;
|
|
126
158
|
try {
|
|
@@ -137,14 +169,15 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
137
169
|
bundle.topics.inbox === `hq/${bundle.actorUid}/inbox`
|
|
138
170
|
? bundle.topics.inbox
|
|
139
171
|
: undefined;
|
|
140
|
-
|
|
141
|
-
|
|
172
|
+
const syncTopic = syncStateWritable ? `hq/${bundle.actorUid}/sync` : undefined;
|
|
173
|
+
actorUid = inboxTopic || syncTopic ? bundle.actorUid : undefined;
|
|
174
|
+
if (!topic && !inboxTopic && !syncTopic) {
|
|
142
175
|
opts.onInfo?.("work cache subscribe skipped: credentials have no supported topic");
|
|
143
176
|
return;
|
|
144
177
|
}
|
|
145
178
|
const url = presignIotWssUrl(bundle.credentials, bundle.iotEndpoint, bundle.region, now());
|
|
146
179
|
const next = opts.mqttConnect(url, {
|
|
147
|
-
clientId:
|
|
180
|
+
clientId: bundle.clientId,
|
|
148
181
|
clean: true,
|
|
149
182
|
reconnectPeriod: 0,
|
|
150
183
|
protocolVersion: 4,
|
|
@@ -154,7 +187,8 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
154
187
|
if (stopped || gen !== generation)
|
|
155
188
|
return;
|
|
156
189
|
reconnectLogged = false;
|
|
157
|
-
let pendingSubscriptions = Number(Boolean(topic)) + Number(Boolean(inboxTopic))
|
|
190
|
+
let pendingSubscriptions = Number(Boolean(topic)) + Number(Boolean(inboxTopic)) +
|
|
191
|
+
Number(Boolean(syncTopic));
|
|
158
192
|
let subscriptionFailed = false;
|
|
159
193
|
const finishSubscription = (err) => {
|
|
160
194
|
if (err) {
|
|
@@ -171,6 +205,13 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
171
205
|
if (pendingSubscriptions === 0)
|
|
172
206
|
attempt = 0;
|
|
173
207
|
};
|
|
208
|
+
const finishOptionalSubscription = () => {
|
|
209
|
+
if (subscriptionFailed)
|
|
210
|
+
return;
|
|
211
|
+
pendingSubscriptions -= 1;
|
|
212
|
+
if (pendingSubscriptions === 0)
|
|
213
|
+
attempt = 0;
|
|
214
|
+
};
|
|
174
215
|
if (topic) {
|
|
175
216
|
next.subscribe(topic, { qos: 1 }, (err) => finishSubscription(err));
|
|
176
217
|
}
|
|
@@ -180,7 +221,6 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
180
221
|
return;
|
|
181
222
|
if (err) {
|
|
182
223
|
inboxSubscribed = false;
|
|
183
|
-
inboxAgentUid = undefined;
|
|
184
224
|
clearInboxHeartbeat();
|
|
185
225
|
persistInboxState(false);
|
|
186
226
|
opts.onInfo?.("agent inbox push subscribe failed");
|
|
@@ -193,17 +233,37 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
193
233
|
finishSubscription();
|
|
194
234
|
});
|
|
195
235
|
}
|
|
236
|
+
if (syncTopic) {
|
|
237
|
+
next.subscribe(syncTopic, { qos: 1 }, (err) => {
|
|
238
|
+
if (stopped || gen !== generation)
|
|
239
|
+
return;
|
|
240
|
+
if (err) {
|
|
241
|
+
syncSubscribed = false;
|
|
242
|
+
persistSyncState(false);
|
|
243
|
+
opts.onInfo?.("sync wake subscribe failed");
|
|
244
|
+
finishOptionalSubscription();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
syncSubscribed = true;
|
|
248
|
+
persistSyncState(true);
|
|
249
|
+
scheduleInboxHeartbeat();
|
|
250
|
+
finishSubscription();
|
|
251
|
+
});
|
|
252
|
+
}
|
|
196
253
|
});
|
|
197
254
|
next.on("message", (_topic, payload) => {
|
|
198
255
|
if (stopped || gen !== generation)
|
|
199
256
|
return;
|
|
200
257
|
if (inboxTopic && _topic === inboxTopic && inboxSubscribed) {
|
|
201
|
-
|
|
202
|
-
// content; the authenticated inbox GET remains the source of truth.
|
|
203
|
-
wakeSequence += 1;
|
|
258
|
+
inboxWakeSequence += 1;
|
|
204
259
|
persistInboxState(true);
|
|
205
260
|
return;
|
|
206
261
|
}
|
|
262
|
+
if (syncTopic && _topic === syncTopic && syncSubscribed) {
|
|
263
|
+
syncWakeSequence += 1;
|
|
264
|
+
persistSyncState(true);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
207
267
|
if (_topic !== topic)
|
|
208
268
|
return;
|
|
209
269
|
const push = parseProjectViewPush(payload);
|
|
@@ -214,13 +274,33 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
214
274
|
opts.onInfo?.(`project-view cache cursor=${result.cursor} ${push.companyUid}/${push.projectId}`);
|
|
215
275
|
}
|
|
216
276
|
});
|
|
277
|
+
next.on("error", (_error) => {
|
|
278
|
+
if (stopped || gen !== generation)
|
|
279
|
+
return;
|
|
280
|
+
inboxSubscribed = false;
|
|
281
|
+
syncSubscribed = false;
|
|
282
|
+
actorUid = undefined;
|
|
283
|
+
clearInboxHeartbeat();
|
|
284
|
+
persistInboxState(false);
|
|
285
|
+
persistSyncState(false);
|
|
286
|
+
opts.onInfo?.("work cache mqtt error");
|
|
287
|
+
schedule();
|
|
288
|
+
try {
|
|
289
|
+
next.end(true);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
});
|
|
217
295
|
next.on("close", () => {
|
|
218
296
|
if (stopped || gen !== generation)
|
|
219
297
|
return;
|
|
220
298
|
inboxSubscribed = false;
|
|
221
|
-
|
|
299
|
+
syncSubscribed = false;
|
|
300
|
+
actorUid = undefined;
|
|
222
301
|
clearInboxHeartbeat();
|
|
223
302
|
persistInboxState(false);
|
|
303
|
+
persistSyncState(false);
|
|
224
304
|
schedule();
|
|
225
305
|
});
|
|
226
306
|
};
|
|
@@ -250,7 +330,10 @@ export async function startWorkCacheSubscriber(opts) {
|
|
|
250
330
|
timers.clearTimeout(timer);
|
|
251
331
|
clearInboxHeartbeat();
|
|
252
332
|
inboxSubscribed = false;
|
|
333
|
+
syncSubscribed = false;
|
|
334
|
+
actorUid = undefined;
|
|
253
335
|
persistInboxState(false);
|
|
336
|
+
persistSyncState(false);
|
|
254
337
|
const current = client;
|
|
255
338
|
client = null;
|
|
256
339
|
if (!current)
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
/** Content-free sequence file written by the mesh MQTT inbox subscriber. */
|
|
6
6
|
export declare const AGENT_INBOX_PUSH_STATE_BASENAME = "agent-inbox-push.json";
|
|
7
|
+
export declare const SYNC_WAKE_STATE_BASENAME = "sync-wake.json";
|
|
7
8
|
export declare function workMeshRoot(home?: string, env?: NodeJS.ProcessEnv): string;
|
|
8
9
|
export declare function workMeshSpoolPath(root?: string): string;
|
|
9
10
|
export declare function workMeshHeldPath(root?: string): string;
|
|
@@ -7,6 +7,7 @@ import * as path from "node:path";
|
|
|
7
7
|
const WORK_MESH_DIRNAME = "work-mesh";
|
|
8
8
|
/** Content-free sequence file written by the mesh MQTT inbox subscriber. */
|
|
9
9
|
export const AGENT_INBOX_PUSH_STATE_BASENAME = "agent-inbox-push.json";
|
|
10
|
+
export const SYNC_WAKE_STATE_BASENAME = "sync-wake.json";
|
|
10
11
|
export function workMeshRoot(home = os.homedir(), env = process.env) {
|
|
11
12
|
const override = env.HQ_WORK_MESH_ROOT?.trim();
|
|
12
13
|
if (override)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indigoai-us/hq-cli",
|
|
3
|
-
"version": "5.345.
|
|
3
|
+
"version": "5.345.164",
|
|
4
4
|
"description": "HQ by Indigo management CLI — modules and cloud sync",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -114,11 +114,11 @@
|
|
|
114
114
|
"*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
|
|
115
115
|
},
|
|
116
116
|
"optionalDependencies": {
|
|
117
|
-
"@indigoai-us/hq-hook-win32-x64": "5.345.
|
|
118
|
-
"@indigoai-us/hq-hook-win32-arm64": "5.345.
|
|
119
|
-
"@indigoai-us/hq-hook-darwin-x64": "5.345.
|
|
120
|
-
"@indigoai-us/hq-hook-darwin-arm64": "5.345.
|
|
121
|
-
"@indigoai-us/hq-hook-linux-x64": "5.345.
|
|
122
|
-
"@indigoai-us/hq-hook-linux-arm64": "5.345.
|
|
117
|
+
"@indigoai-us/hq-hook-win32-x64": "5.345.164",
|
|
118
|
+
"@indigoai-us/hq-hook-win32-arm64": "5.345.164",
|
|
119
|
+
"@indigoai-us/hq-hook-darwin-x64": "5.345.164",
|
|
120
|
+
"@indigoai-us/hq-hook-darwin-arm64": "5.345.164",
|
|
121
|
+
"@indigoai-us/hq-hook-linux-x64": "5.345.164",
|
|
122
|
+
"@indigoai-us/hq-hook-linux-arm64": "5.345.164"
|
|
123
123
|
}
|
|
124
124
|
}
|