@indigoai-us/hq-cli 5.345.161 → 5.345.163
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 +17 -0
- package/dist/command-catalog.generated.d.ts +385 -1
- package/dist/command-catalog.generated.js +475 -1
- package/dist/command-registration-plan.d.ts +12 -0
- package/dist/command-registration-plan.js +2 -0
- package/dist/commands/agent-inbox.js +6 -2
- package/dist/commands/deploy-domains.d.ts +51 -0
- package/dist/commands/deploy-domains.js +787 -0
- package/dist/commands/feedback-triage.d.ts +67 -0
- package/dist/commands/feedback-triage.js +294 -0
- package/dist/commands/feedback.d.ts +13 -1
- package/dist/commands/feedback.js +32 -8
- package/dist/commands/lanes-loop.js +7 -7
- package/dist/commands/lanes-questions.js +54 -4
- package/dist/commands/lanes.js +2 -1
- package/dist/commands/sync-held.d.ts +87 -0
- package/dist/commands/sync-held.js +191 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3095 -2769
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +5 -4
- package/dist/lib/agent-kit/inbox-state.js +24 -3
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/lanes/loop.d.ts +17 -0
- package/dist/lib/lanes/loop.js +235 -5
- package/dist/lib/lanes/questions.d.ts +1 -0
- package/package.json +8 -8
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq sync held` and `hq sync release-held` (feedback_3bf773c5).
|
|
3
|
+
*
|
|
4
|
+
* After a different person signs in on the same computer, hq-cloud holds every
|
|
5
|
+
* upload and cloud delete for each sync scope (the personal vault and each
|
|
6
|
+
* company) until the signed-in person reviews what is held and releases it.
|
|
7
|
+
* These commands are the person's way to see the hold and make that decision.
|
|
8
|
+
*
|
|
9
|
+
* The hq-cloud exports (`listPushIdentityFences`, `releasePushIdentityFences`)
|
|
10
|
+
* ship after the hq-cloud version this CLI pins, so they are feature-detected
|
|
11
|
+
* on the module namespace, the same way `lib/hq-cloud-manifest.ts` does it. A
|
|
12
|
+
* static named import of a missing ESM export would stop every command that
|
|
13
|
+
* loads this file.
|
|
14
|
+
*/
|
|
15
|
+
import { Command } from "commander";
|
|
16
|
+
export interface HeldScope {
|
|
17
|
+
slug: string;
|
|
18
|
+
uid: string;
|
|
19
|
+
sinceMs: number;
|
|
20
|
+
held?: {
|
|
21
|
+
count: number;
|
|
22
|
+
samplePaths: string[];
|
|
23
|
+
at: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
interface FenceExports {
|
|
27
|
+
listPushIdentityFences: (stateDir: string) => HeldScope[];
|
|
28
|
+
releasePushIdentityFences: (stateDir: string, identities: readonly string[], slugs?: readonly string[]) => string[];
|
|
29
|
+
}
|
|
30
|
+
export declare const HELD_UNAVAILABLE_MESSAGE = "this HQ build cannot list or release held uploads yet; update HQ and try again";
|
|
31
|
+
export declare function fenceExports(mod?: Record<string, unknown>): FenceExports | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* One authenticated snapshot of who is signed in: the Cognito sub of the
|
|
34
|
+
* session token and, when asked for, the personal vault uid resolved with that
|
|
35
|
+
* same token. The sub never comes from the upload-author label, which needs an
|
|
36
|
+
* email; hq-cloud#1040 identifies email-less sessions by sub alone.
|
|
37
|
+
*/
|
|
38
|
+
export interface HeldIdentity {
|
|
39
|
+
sub: string;
|
|
40
|
+
personalUid?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface SyncHeldDeps {
|
|
43
|
+
exports: FenceExports | undefined;
|
|
44
|
+
stateDir: string;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the signed-in identity from one token. `interactive` allows a
|
|
47
|
+
* sign-in or refresh; the result is taken after it, never mixed with an
|
|
48
|
+
* identity read before it.
|
|
49
|
+
*/
|
|
50
|
+
identity: (options: {
|
|
51
|
+
needPersonal: boolean;
|
|
52
|
+
interactive: boolean;
|
|
53
|
+
}) => Promise<HeldIdentity>;
|
|
54
|
+
confirm: (question: string) => Promise<boolean>;
|
|
55
|
+
log: (line: string) => void;
|
|
56
|
+
}
|
|
57
|
+
export declare function scopeLabel(slug: string): string;
|
|
58
|
+
export declare function runSyncHeld(deps: SyncHeldDeps, json?: boolean): Promise<number>;
|
|
59
|
+
export interface ReleaseHeldOptions {
|
|
60
|
+
company?: string;
|
|
61
|
+
personal?: boolean;
|
|
62
|
+
yes?: boolean;
|
|
63
|
+
json?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export declare function runReleaseHeld(deps: SyncHeldDeps, options: ReleaseHeldOptions): Promise<number>;
|
|
66
|
+
/**
|
|
67
|
+
* The signed-in identity from the session token `ensureCognitoToken` selects
|
|
68
|
+
* (person or machine, the same choice every vault call makes). The personal
|
|
69
|
+
* vault uid is resolved with that same token: an agent principal uses its own
|
|
70
|
+
* entity uid, as the sync runner does; a person uses the canonical person
|
|
71
|
+
* entity.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveHeldIdentity(options: {
|
|
74
|
+
needPersonal: boolean;
|
|
75
|
+
interactive: boolean;
|
|
76
|
+
}, io: {
|
|
77
|
+
ensureToken: (interactive: boolean) => Promise<string>;
|
|
78
|
+
listPersons: (token: string) => Promise<Array<{
|
|
79
|
+
uid: string;
|
|
80
|
+
type: string;
|
|
81
|
+
createdAt: string;
|
|
82
|
+
}>>;
|
|
83
|
+
}): Promise<HeldIdentity>;
|
|
84
|
+
export declare function createDefaultSyncHeldDeps(): SyncHeldDeps;
|
|
85
|
+
export declare function registerSyncHeldCommands(syncCmd: Command): void;
|
|
86
|
+
export {};
|
|
87
|
+
//# sourceMappingURL=sync-held.d.ts.map
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `hq sync held` and `hq sync release-held` (feedback_3bf773c5).
|
|
3
|
+
*
|
|
4
|
+
* After a different person signs in on the same computer, hq-cloud holds every
|
|
5
|
+
* upload and cloud delete for each sync scope (the personal vault and each
|
|
6
|
+
* company) until the signed-in person reviews what is held and releases it.
|
|
7
|
+
* These commands are the person's way to see the hold and make that decision.
|
|
8
|
+
*
|
|
9
|
+
* The hq-cloud exports (`listPushIdentityFences`, `releasePushIdentityFences`)
|
|
10
|
+
* ship after the hq-cloud version this CLI pins, so they are feature-detected
|
|
11
|
+
* on the module namespace, the same way `lib/hq-cloud-manifest.ts` does it. A
|
|
12
|
+
* static named import of a missing ESM export would stop every command that
|
|
13
|
+
* loads this file.
|
|
14
|
+
*/
|
|
15
|
+
import * as hqCloud from "@indigoai-us/hq-cloud";
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
import * as readline from "node:readline/promises";
|
|
18
|
+
import { VaultClient, PERSONAL_VAULT_JOURNAL_SLUG, getStateDir } from "@indigoai-us/hq-cloud";
|
|
19
|
+
import { buildVaultConfig, cognitoSubjectFromToken, ensureCognitoToken } from "../utils/cognito-session.js";
|
|
20
|
+
import { decodeJwtPayload } from "../utils/person-token-cache.js";
|
|
21
|
+
import { resolveCanonicalPersonUid } from "./cloud.js";
|
|
22
|
+
export const HELD_UNAVAILABLE_MESSAGE = "this HQ build cannot list or release held uploads yet; update HQ and try again";
|
|
23
|
+
export function fenceExports(mod = hqCloud) {
|
|
24
|
+
const list = mod.listPushIdentityFences;
|
|
25
|
+
const release = mod.releasePushIdentityFences;
|
|
26
|
+
if (typeof list !== "function" || typeof release !== "function")
|
|
27
|
+
return undefined;
|
|
28
|
+
return {
|
|
29
|
+
listPushIdentityFences: list,
|
|
30
|
+
releasePushIdentityFences: release,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function scopeLabel(slug) {
|
|
34
|
+
return slug === PERSONAL_VAULT_JOURNAL_SLUG || slug === "personal" ? "personal vault" : `company ${slug}`;
|
|
35
|
+
}
|
|
36
|
+
function describe(scope) {
|
|
37
|
+
const since = new Date(scope.sinceMs).toISOString();
|
|
38
|
+
const lines = [` ${scopeLabel(scope.slug)}: held since ${since}`];
|
|
39
|
+
if (scope.held) {
|
|
40
|
+
lines.push(` ${scope.held.count} file${scope.held.count === 1 ? "" : "s"} waiting to upload (last checked ${scope.held.at})`);
|
|
41
|
+
for (const sample of scope.held.samplePaths.slice(0, 20))
|
|
42
|
+
lines.push(` ${sample}`);
|
|
43
|
+
if (scope.held.count > Math.min(20, scope.held.samplePaths.length))
|
|
44
|
+
lines.push(" ...");
|
|
45
|
+
}
|
|
46
|
+
return lines;
|
|
47
|
+
}
|
|
48
|
+
export async function runSyncHeld(deps, json = false) {
|
|
49
|
+
if (!deps.exports) {
|
|
50
|
+
deps.log(`${chalk.red("✗")} ${HELD_UNAVAILABLE_MESSAGE}`);
|
|
51
|
+
return 1;
|
|
52
|
+
}
|
|
53
|
+
const scopes = deps.exports.listPushIdentityFences(deps.stateDir);
|
|
54
|
+
if (json) {
|
|
55
|
+
deps.log(JSON.stringify({ scopes }));
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
if (scopes.length === 0) {
|
|
59
|
+
deps.log("No uploads are held on this computer.");
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
deps.log("Someone else was signed in on this computer, so these uploads are held:");
|
|
63
|
+
for (const scope of scopes)
|
|
64
|
+
for (const line of describe(scope))
|
|
65
|
+
deps.log(line);
|
|
66
|
+
deps.log("");
|
|
67
|
+
deps.log("Nothing has been uploaded or deleted in the cloud. If these files are yours, run `hq sync release-held`.");
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
export async function runReleaseHeld(deps, options) {
|
|
71
|
+
if (!deps.exports) {
|
|
72
|
+
deps.log(`${chalk.red("✗")} ${HELD_UNAVAILABLE_MESSAGE}`);
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
let scopes = deps.exports.listPushIdentityFences(deps.stateDir);
|
|
76
|
+
if (options.personal)
|
|
77
|
+
scopes = scopes.filter((scope) => scope.slug === PERSONAL_VAULT_JOURNAL_SLUG || scope.slug === "personal");
|
|
78
|
+
if (options.company)
|
|
79
|
+
scopes = scopes.filter((scope) => scope.slug === options.company);
|
|
80
|
+
if (scopes.length === 0) {
|
|
81
|
+
deps.log(options.json ? JSON.stringify({ released: [] }) : "No held uploads match.");
|
|
82
|
+
return 0;
|
|
83
|
+
}
|
|
84
|
+
const needPersonal = scopes.some((scope) => scope.slug === PERSONAL_VAULT_JOURNAL_SLUG || scope.slug === "personal");
|
|
85
|
+
const snapshot = await deps.identity({ needPersonal, interactive: true });
|
|
86
|
+
const identities = [snapshot.sub, ...(snapshot.personalUid ? [snapshot.personalUid] : [])];
|
|
87
|
+
const mine = scopes.filter((scope) => identities.includes(scope.uid));
|
|
88
|
+
if (mine.length === 0) {
|
|
89
|
+
deps.log(options.json
|
|
90
|
+
? JSON.stringify({ released: [], reason: "not-yours" })
|
|
91
|
+
: "These held uploads belong to a different signed-in person. Sign in as that person to release them.");
|
|
92
|
+
return 1;
|
|
93
|
+
}
|
|
94
|
+
if (!options.json) {
|
|
95
|
+
deps.log("Releasing these uploads sends the files below to the cloud as yours:");
|
|
96
|
+
for (const scope of mine)
|
|
97
|
+
for (const line of describe(scope))
|
|
98
|
+
deps.log(line);
|
|
99
|
+
}
|
|
100
|
+
if (!options.yes) {
|
|
101
|
+
const ok = await deps.confirm("Are these files yours to upload? [y/N] ");
|
|
102
|
+
if (!ok) {
|
|
103
|
+
deps.log(options.json ? JSON.stringify({ released: [] }) : "Nothing released.");
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Re-read the signed-in identity right before releasing, without a sign-in.
|
|
108
|
+
// If the account changed since the snapshot (including during the prompt),
|
|
109
|
+
// release nothing: the scopes were judged against the earlier account.
|
|
110
|
+
const current = await deps.identity({ needPersonal, interactive: false });
|
|
111
|
+
if (current.sub !== snapshot.sub || current.personalUid !== snapshot.personalUid) {
|
|
112
|
+
deps.log(options.json
|
|
113
|
+
? JSON.stringify({ released: [], reason: "identity-changed" })
|
|
114
|
+
: "The signed-in account changed while releasing. Nothing was released; run the command again.");
|
|
115
|
+
return 1;
|
|
116
|
+
}
|
|
117
|
+
const released = deps.exports.releasePushIdentityFences(deps.stateDir, identities, mine.map((scope) => scope.slug));
|
|
118
|
+
deps.log(options.json
|
|
119
|
+
? JSON.stringify({ released })
|
|
120
|
+
: `Released ${released.map(scopeLabel).join(", ")}. The next sync uploads these files.`);
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
async function confirmOnTty(question) {
|
|
124
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true)
|
|
125
|
+
return false;
|
|
126
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
127
|
+
try {
|
|
128
|
+
const answer = await rl.question(question);
|
|
129
|
+
return /^y(es)?$/i.test(answer.trim());
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
rl.close();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The signed-in identity from the session token `ensureCognitoToken` selects
|
|
137
|
+
* (person or machine, the same choice every vault call makes). The personal
|
|
138
|
+
* vault uid is resolved with that same token: an agent principal uses its own
|
|
139
|
+
* entity uid, as the sync runner does; a person uses the canonical person
|
|
140
|
+
* entity.
|
|
141
|
+
*/
|
|
142
|
+
export async function resolveHeldIdentity(options, io) {
|
|
143
|
+
const token = await io.ensureToken(options.interactive);
|
|
144
|
+
const { sub, entityUid } = cognitoSubjectFromToken(token);
|
|
145
|
+
if (!sub)
|
|
146
|
+
throw new Error("could not read the signed-in account from the session; run `hq login` and try again");
|
|
147
|
+
if (!options.needPersonal)
|
|
148
|
+
return { sub };
|
|
149
|
+
const entityType = decodeJwtPayload(token)?.["custom:entityType"];
|
|
150
|
+
if (entityType === "agent" && entityUid)
|
|
151
|
+
return { sub, personalUid: entityUid };
|
|
152
|
+
const personalUid = await resolveCanonicalPersonUid({
|
|
153
|
+
listMyMemberships: async () => [],
|
|
154
|
+
listPersonEntities: () => io.listPersons(token),
|
|
155
|
+
getEntity: async () => null,
|
|
156
|
+
});
|
|
157
|
+
return { sub, personalUid };
|
|
158
|
+
}
|
|
159
|
+
export function createDefaultSyncHeldDeps() {
|
|
160
|
+
const tty = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
161
|
+
return {
|
|
162
|
+
exports: fenceExports(),
|
|
163
|
+
stateDir: getStateDir(),
|
|
164
|
+
identity: (options) => resolveHeldIdentity({ needPersonal: options.needPersonal, interactive: options.interactive && tty }, {
|
|
165
|
+
ensureToken: (interactive) => ensureCognitoToken({ interactive }),
|
|
166
|
+
listPersons: async (token) => (await new VaultClient(buildVaultConfig(token)).entity.listByType("person")),
|
|
167
|
+
}),
|
|
168
|
+
confirm: confirmOnTty,
|
|
169
|
+
log: (line) => console.log(line),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export function registerSyncHeldCommands(syncCmd) {
|
|
173
|
+
syncCmd
|
|
174
|
+
.command("held")
|
|
175
|
+
.description("Show uploads held because someone else was signed in on this computer")
|
|
176
|
+
.option("--json", "Print the held scopes as JSON")
|
|
177
|
+
.action(async (options) => {
|
|
178
|
+
process.exitCode = await runSyncHeld(createDefaultSyncHeldDeps(), options.json === true);
|
|
179
|
+
});
|
|
180
|
+
syncCmd
|
|
181
|
+
.command("release-held")
|
|
182
|
+
.description("Confirm held files are yours and let sync upload them")
|
|
183
|
+
.option("--company <slug>", "Release only this company")
|
|
184
|
+
.option("--personal", "Release only the personal vault")
|
|
185
|
+
.option("--yes", "Do not ask for confirmation")
|
|
186
|
+
.option("--json", "Print the result as JSON")
|
|
187
|
+
.action(async (options) => {
|
|
188
|
+
process.exitCode = await runReleaseHeld(createDefaultSyncHeldDeps(), options);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=sync-held.js.map
|