@indigoai-us/hq-cli 5.55.0 → 5.56.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/dist/commands/cloud.d.ts +9 -0
- package/dist/commands/cloud.js +47 -9
- package/dist/commands/meetings.js +12 -6
- package/package.json +1 -1
- package/src/commands/cloud.push-all.test.ts +29 -0
- package/src/commands/cloud.scope-excluded-warning.test.ts +22 -0
- package/src/commands/cloud.ts +60 -11
- package/src/commands/meetings.test.ts +79 -0
- package/src/commands/meetings.ts +16 -3
package/dist/commands/cloud.d.ts
CHANGED
|
@@ -15,6 +15,13 @@
|
|
|
15
15
|
import { Command } from "commander";
|
|
16
16
|
import { type ConflictStrategy, type MembershipSyncConfig, type SyncMode, type PullScope, type ExplicitGrant } from "@indigoai-us/hq-cloud";
|
|
17
17
|
import { type BannerLevel } from "../lib/narrow-hint-banner.js";
|
|
18
|
+
/**
|
|
19
|
+
* Build a loud, human-readable warning when a push dropped files because
|
|
20
|
+
* they fell outside the caller's granted write scope. Returns null when
|
|
21
|
+
* nothing was scope-excluded. Keeping this pure makes the "never silently
|
|
22
|
+
* succeed when files were dropped" guarantee unit-testable.
|
|
23
|
+
*/
|
|
24
|
+
export declare function scopeExcludedWarning(count: number): string | null;
|
|
18
25
|
export interface PullAllVaultClient {
|
|
19
26
|
listMyMemberships(): Promise<Array<{
|
|
20
27
|
companyUid: string;
|
|
@@ -140,6 +147,7 @@ export interface ShareCallResult {
|
|
|
140
147
|
bytesUploaded: number;
|
|
141
148
|
filesSkipped: number;
|
|
142
149
|
filesDeleted: number;
|
|
150
|
+
filesExcludedByScope: number;
|
|
143
151
|
conflictPaths: string[];
|
|
144
152
|
aborted: boolean;
|
|
145
153
|
}
|
|
@@ -169,6 +177,7 @@ export interface PushAllResult {
|
|
|
169
177
|
filesUploaded: number;
|
|
170
178
|
bytesUploaded: number;
|
|
171
179
|
filesDeleted: number;
|
|
180
|
+
filesExcludedByScope: number;
|
|
172
181
|
errors: Array<{
|
|
173
182
|
company: string;
|
|
174
183
|
message: string;
|
package/dist/commands/cloud.js
CHANGED
|
@@ -13,13 +13,27 @@
|
|
|
13
13
|
* hq sync status — show local journal summary
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
16
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="fa72780b-ae22-5125-98b6-4e7b866b7d17")}catch(e){}}();
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import * as fs from "fs";
|
|
19
19
|
import * as path from "path";
|
|
20
20
|
import { share, sync, getStateDir, listJournals, loadCachedTokens, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, resolvePullScope, } from "@indigoai-us/hq-cloud";
|
|
21
21
|
import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
22
22
|
import { emitNarrowHint, isStrictRefusal, resolveBannerLevel, } from "../lib/narrow-hint-banner.js";
|
|
23
|
+
/**
|
|
24
|
+
* Build a loud, human-readable warning when a push dropped files because
|
|
25
|
+
* they fell outside the caller's granted write scope. Returns null when
|
|
26
|
+
* nothing was scope-excluded. Keeping this pure makes the "never silently
|
|
27
|
+
* succeed when files were dropped" guarantee unit-testable.
|
|
28
|
+
*/
|
|
29
|
+
export function scopeExcludedWarning(count) {
|
|
30
|
+
if (count <= 0)
|
|
31
|
+
return null;
|
|
32
|
+
return (`⚠ ${count} file(s) were NOT uploaded — they fall outside the ` +
|
|
33
|
+
`prefixes you have write access to (company-wide or direct grants). ` +
|
|
34
|
+
`They were skipped, not synced. Re-run with --json to list them, or ` +
|
|
35
|
+
`ask an admin to grant you write on those paths.`);
|
|
36
|
+
}
|
|
23
37
|
/**
|
|
24
38
|
* Resolve the `propagateDeletePolicy` for share() calls.
|
|
25
39
|
*
|
|
@@ -251,6 +265,7 @@ export async function pushAll(options, deps) {
|
|
|
251
265
|
filesUploaded: 0,
|
|
252
266
|
bytesUploaded: 0,
|
|
253
267
|
filesDeleted: 0,
|
|
268
|
+
filesExcludedByScope: 0,
|
|
254
269
|
errors: [],
|
|
255
270
|
perCompany: [],
|
|
256
271
|
};
|
|
@@ -261,6 +276,7 @@ export async function pushAll(options, deps) {
|
|
|
261
276
|
result.filesUploaded += r.filesUploaded;
|
|
262
277
|
result.bytesUploaded += r.bytesUploaded;
|
|
263
278
|
result.filesDeleted += r.filesDeleted;
|
|
279
|
+
result.filesExcludedByScope += r.filesExcludedByScope;
|
|
264
280
|
result.perCompany.push({ slug: entry.slug, result: r });
|
|
265
281
|
}
|
|
266
282
|
catch (err) {
|
|
@@ -621,7 +637,14 @@ export function registerCloudCommands(program) {
|
|
|
621
637
|
log(chalk.yellow(`\n⚠ Push aborted (${result.filesUploaded} uploaded, ${result.filesSkipped} skipped)`));
|
|
622
638
|
process.exit(1);
|
|
623
639
|
}
|
|
624
|
-
|
|
640
|
+
if (result.filesExcludedByScope > 0) {
|
|
641
|
+
log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
|
|
642
|
+
`${result.filesSkipped} skipped, ${result.filesExcludedByScope} scope-excluded)`));
|
|
643
|
+
log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)));
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`));
|
|
647
|
+
}
|
|
625
648
|
}
|
|
626
649
|
catch (err) {
|
|
627
650
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1051,18 +1074,27 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal) {
|
|
|
1051
1074
|
}
|
|
1052
1075
|
else if (row.result) {
|
|
1053
1076
|
const r = row.result;
|
|
1054
|
-
const status = r.aborted
|
|
1077
|
+
const status = r.aborted || r.filesExcludedByScope > 0
|
|
1078
|
+
? chalk.yellow("⚠")
|
|
1079
|
+
: chalk.green("✓");
|
|
1055
1080
|
console.log(` ${status} ${row.slug}: ${r.filesUploaded} file(s), ` +
|
|
1056
1081
|
`${formatBytes(r.bytesUploaded)}, ${r.filesSkipped} skipped, ` +
|
|
1057
|
-
`${r.filesDeleted} deleted, ${r.
|
|
1082
|
+
`${r.filesDeleted} deleted, ${r.filesExcludedByScope} scope-excluded, ` +
|
|
1083
|
+
`${r.conflictPaths.length} conflict(s)` +
|
|
1058
1084
|
(r.aborted ? " — aborted" : ""));
|
|
1059
1085
|
}
|
|
1060
1086
|
}
|
|
1061
1087
|
const errored = result.errors.length;
|
|
1062
1088
|
const summary = `\nPushed ${result.filesUploaded} file(s) ` +
|
|
1063
1089
|
`(${formatBytes(result.bytesUploaded)}) across ${result.attempted} ` +
|
|
1064
|
-
`target(s); ${result.filesDeleted} deleted; ${errored} error(s)
|
|
1065
|
-
|
|
1090
|
+
`target(s); ${result.filesDeleted} deleted; ${errored} error(s); ` +
|
|
1091
|
+
`${result.filesExcludedByScope} scope-excluded`;
|
|
1092
|
+
console.log(errored > 0 || result.filesExcludedByScope > 0
|
|
1093
|
+
? chalk.yellow(summary)
|
|
1094
|
+
: chalk.green(summary));
|
|
1095
|
+
if (result.filesExcludedByScope > 0) {
|
|
1096
|
+
console.log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)));
|
|
1097
|
+
}
|
|
1066
1098
|
if (errored > 0)
|
|
1067
1099
|
process.exit(1);
|
|
1068
1100
|
}
|
|
@@ -1124,10 +1156,16 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1124
1156
|
...(journalSlug !== undefined ? { journalSlug } : {}),
|
|
1125
1157
|
...(author ? { author } : {}),
|
|
1126
1158
|
});
|
|
1127
|
-
|
|
1159
|
+
const pushStatus = pushResult.aborted || pushResult.filesExcludedByScope > 0
|
|
1160
|
+
? chalk.yellow("⚠")
|
|
1161
|
+
: chalk.green("✓");
|
|
1162
|
+
console.log(` ${pushStatus} ` +
|
|
1128
1163
|
`${pushResult.filesUploaded} uploaded, ${pushResult.filesSkipped} skipped, ` +
|
|
1129
|
-
`${pushResult.filesDeleted} deleted` +
|
|
1164
|
+
`${pushResult.filesDeleted} deleted, ${pushResult.filesExcludedByScope} scope-excluded` +
|
|
1130
1165
|
(pushResult.aborted ? " — aborted" : ""));
|
|
1166
|
+
if (pushResult.filesExcludedByScope > 0) {
|
|
1167
|
+
console.log(chalk.yellow(scopeExcludedWarning(pushResult.filesExcludedByScope)));
|
|
1168
|
+
}
|
|
1131
1169
|
if (pushResult.aborted) {
|
|
1132
1170
|
console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
|
|
1133
1171
|
process.exit(1);
|
|
@@ -1330,4 +1368,4 @@ function resolveUploadAuthorFromCache() {
|
|
|
1330
1368
|
}
|
|
1331
1369
|
}
|
|
1332
1370
|
//# sourceMappingURL=cloud.js.map
|
|
1333
|
-
//# debugId=
|
|
1371
|
+
//# debugId=fa72780b-ae22-5125-98b6-4e7b866b7d17
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="2640a391-a3e6-5830-b56f-03b5fab3a4bd")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
5
5
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
@@ -35,9 +35,14 @@ function statusBadge(status) {
|
|
|
35
35
|
async function resolveShortId(token, prefix, query) {
|
|
36
36
|
if (prefix.includes("-") && prefix.length > 8)
|
|
37
37
|
return prefix;
|
|
38
|
-
const res = await vaultApiFetch({
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const res = await vaultApiFetch({
|
|
39
|
+
token,
|
|
40
|
+
path: "/v1/meetings",
|
|
41
|
+
query: { ...query, limit: "500" },
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
throw new Error(`Could not resolve meeting ID "${prefix}" — failed to list meetings (${res.status}). Pass the full meeting id.`);
|
|
45
|
+
}
|
|
41
46
|
const data = (await res.json());
|
|
42
47
|
const matches = data.meetings.filter((m) => m.meetingId.startsWith(prefix));
|
|
43
48
|
if (matches.length === 1)
|
|
@@ -46,7 +51,8 @@ async function resolveShortId(token, prefix, query) {
|
|
|
46
51
|
console.error(chalk.red(`Ambiguous ID prefix "${prefix}" — matches ${matches.length} meetings. Use a longer prefix.`));
|
|
47
52
|
process.exit(1);
|
|
48
53
|
}
|
|
49
|
-
|
|
54
|
+
console.error(chalk.red(`No meeting matches ID "${prefix}". It may be older than the meetings shown by \`hq meetings list\`, still processing, or attributed to a different company. Pass the full meeting id, add --company <slug>, or widen the list with \`hq meetings list --limit <n>\`.`));
|
|
55
|
+
process.exit(1);
|
|
50
56
|
}
|
|
51
57
|
async function handleApiError(res) {
|
|
52
58
|
const body = (await res.json().catch(() => ({})));
|
|
@@ -426,4 +432,4 @@ export function registerMeetingsCommand(program) {
|
|
|
426
432
|
});
|
|
427
433
|
}
|
|
428
434
|
//# sourceMappingURL=meetings.js.map
|
|
429
|
-
//# debugId=
|
|
435
|
+
//# debugId=2640a391-a3e6-5830-b56f-03b5fab3a4bd
|
package/package.json
CHANGED
|
@@ -82,6 +82,7 @@ function makeShareSpy(
|
|
|
82
82
|
bytesUploaded: 0,
|
|
83
83
|
filesSkipped: 0,
|
|
84
84
|
filesDeleted: 0,
|
|
85
|
+
filesExcludedByScope: 0,
|
|
85
86
|
conflictPaths: [],
|
|
86
87
|
aborted: false,
|
|
87
88
|
...override,
|
|
@@ -307,6 +308,7 @@ describe("pushAll", () => {
|
|
|
307
308
|
filesUploaded: 0,
|
|
308
309
|
bytesUploaded: 0,
|
|
309
310
|
filesDeleted: 0,
|
|
311
|
+
filesExcludedByScope: 0,
|
|
310
312
|
errors: [],
|
|
311
313
|
perCompany: [],
|
|
312
314
|
});
|
|
@@ -379,6 +381,33 @@ describe("pushAll", () => {
|
|
|
379
381
|
expect(result.filesDeleted).toBe(4);
|
|
380
382
|
});
|
|
381
383
|
|
|
384
|
+
it("aggregates filesExcludedByScope across every successful target", async () => {
|
|
385
|
+
const hqRoot = makeHqRoot(["companies", "docs"]);
|
|
386
|
+
const vaultClient = makeVaultClient({
|
|
387
|
+
memberships: [{ companyUid: "cmp_acme" }],
|
|
388
|
+
persons: [
|
|
389
|
+
{
|
|
390
|
+
uid: "psn_alice",
|
|
391
|
+
type: "person",
|
|
392
|
+
slug: "alice",
|
|
393
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
394
|
+
},
|
|
395
|
+
],
|
|
396
|
+
entitiesBySlug: { cmp_acme: { slug: "acme" } },
|
|
397
|
+
});
|
|
398
|
+
const share = makeShareSpy({
|
|
399
|
+
cmp_acme: { filesExcludedByScope: 2 },
|
|
400
|
+
psn_alice: { filesExcludedByScope: 3 },
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const result = await pushAll(
|
|
404
|
+
{ hqRoot },
|
|
405
|
+
{ vaultClient, share: share.fn },
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
expect(result.filesExcludedByScope).toBe(5);
|
|
409
|
+
});
|
|
410
|
+
|
|
382
411
|
// ── skipPersonal — fanout omits the canonical-person leg ──────────────────
|
|
383
412
|
//
|
|
384
413
|
// Symmetric with the `pullAll` skipPersonal coverage in
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { scopeExcludedWarning } from "./cloud.js";
|
|
4
|
+
|
|
5
|
+
describe("scopeExcludedWarning", () => {
|
|
6
|
+
it("returns null for zero and negative counts", () => {
|
|
7
|
+
expect(scopeExcludedWarning(0)).toBeNull();
|
|
8
|
+
expect(scopeExcludedWarning(-1)).toBeNull();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns a loud warning for positive counts", () => {
|
|
12
|
+
const warning = scopeExcludedWarning(3);
|
|
13
|
+
|
|
14
|
+
expect(warning).toBeTruthy();
|
|
15
|
+
expect(warning).toContain("3");
|
|
16
|
+
// The warning must loudly state the files did NOT reach the vault and
|
|
17
|
+
// explain why (outside the caller's write access) — without leaning on
|
|
18
|
+
// the jargon word "scope" in user-facing copy.
|
|
19
|
+
expect(warning).toContain("NOT uploaded");
|
|
20
|
+
expect(warning?.toLowerCase()).toContain("write access");
|
|
21
|
+
});
|
|
22
|
+
});
|
package/src/commands/cloud.ts
CHANGED
|
@@ -50,6 +50,22 @@ import {
|
|
|
50
50
|
type BannerLevel,
|
|
51
51
|
} from "../lib/narrow-hint-banner.js";
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Build a loud, human-readable warning when a push dropped files because
|
|
55
|
+
* they fell outside the caller's granted write scope. Returns null when
|
|
56
|
+
* nothing was scope-excluded. Keeping this pure makes the "never silently
|
|
57
|
+
* succeed when files were dropped" guarantee unit-testable.
|
|
58
|
+
*/
|
|
59
|
+
export function scopeExcludedWarning(count: number): string | null {
|
|
60
|
+
if (count <= 0) return null;
|
|
61
|
+
return (
|
|
62
|
+
`⚠ ${count} file(s) were NOT uploaded — they fall outside the ` +
|
|
63
|
+
`prefixes you have write access to (company-wide or direct grants). ` +
|
|
64
|
+
`They were skipped, not synced. Re-run with --json to list them, or ` +
|
|
65
|
+
`ask an admin to grant you write on those paths.`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
53
69
|
/**
|
|
54
70
|
* Resolve the `propagateDeletePolicy` for share() calls.
|
|
55
71
|
*
|
|
@@ -234,6 +250,7 @@ export interface ShareCallResult {
|
|
|
234
250
|
bytesUploaded: number;
|
|
235
251
|
filesSkipped: number;
|
|
236
252
|
filesDeleted: number;
|
|
253
|
+
filesExcludedByScope: number;
|
|
237
254
|
conflictPaths: string[];
|
|
238
255
|
aborted: boolean;
|
|
239
256
|
}
|
|
@@ -267,6 +284,7 @@ export interface PushAllResult {
|
|
|
267
284
|
filesUploaded: number;
|
|
268
285
|
bytesUploaded: number;
|
|
269
286
|
filesDeleted: number;
|
|
287
|
+
filesExcludedByScope: number;
|
|
270
288
|
errors: Array<{ company: string; message: string }>;
|
|
271
289
|
perCompany: PushAllRow[];
|
|
272
290
|
}
|
|
@@ -513,6 +531,7 @@ export async function pushAll(
|
|
|
513
531
|
filesUploaded: 0,
|
|
514
532
|
bytesUploaded: 0,
|
|
515
533
|
filesDeleted: 0,
|
|
534
|
+
filesExcludedByScope: 0,
|
|
516
535
|
errors: [],
|
|
517
536
|
perCompany: [],
|
|
518
537
|
};
|
|
@@ -524,6 +543,7 @@ export async function pushAll(
|
|
|
524
543
|
result.filesUploaded += r.filesUploaded;
|
|
525
544
|
result.bytesUploaded += r.bytesUploaded;
|
|
526
545
|
result.filesDeleted += r.filesDeleted;
|
|
546
|
+
result.filesExcludedByScope += r.filesExcludedByScope;
|
|
527
547
|
result.perCompany.push({ slug: entry.slug, result: r });
|
|
528
548
|
} catch (err) {
|
|
529
549
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1016,11 +1036,21 @@ export function registerCloudCommands(program: Command): void {
|
|
|
1016
1036
|
process.exit(1);
|
|
1017
1037
|
}
|
|
1018
1038
|
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1039
|
+
if (result.filesExcludedByScope > 0) {
|
|
1040
|
+
log(
|
|
1041
|
+
chalk.yellow(
|
|
1042
|
+
`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
|
|
1043
|
+
`${result.filesSkipped} skipped, ${result.filesExcludedByScope} scope-excluded)`,
|
|
1044
|
+
),
|
|
1045
|
+
);
|
|
1046
|
+
log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)!));
|
|
1047
|
+
} else {
|
|
1048
|
+
log(
|
|
1049
|
+
chalk.green(
|
|
1050
|
+
`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`,
|
|
1051
|
+
),
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1024
1054
|
} catch (err) {
|
|
1025
1055
|
const message = err instanceof Error ? err.message : String(err);
|
|
1026
1056
|
if (jsonMode) {
|
|
@@ -1657,11 +1687,15 @@ async function runPushAll(
|
|
|
1657
1687
|
console.log(chalk.red(` ✗ ${row.slug}: ${row.error}`));
|
|
1658
1688
|
} else if (row.result) {
|
|
1659
1689
|
const r = row.result;
|
|
1660
|
-
const status =
|
|
1690
|
+
const status =
|
|
1691
|
+
r.aborted || r.filesExcludedByScope > 0
|
|
1692
|
+
? chalk.yellow("⚠")
|
|
1693
|
+
: chalk.green("✓");
|
|
1661
1694
|
console.log(
|
|
1662
1695
|
` ${status} ${row.slug}: ${r.filesUploaded} file(s), ` +
|
|
1663
1696
|
`${formatBytes(r.bytesUploaded)}, ${r.filesSkipped} skipped, ` +
|
|
1664
|
-
`${r.filesDeleted} deleted, ${r.
|
|
1697
|
+
`${r.filesDeleted} deleted, ${r.filesExcludedByScope} scope-excluded, ` +
|
|
1698
|
+
`${r.conflictPaths.length} conflict(s)` +
|
|
1665
1699
|
(r.aborted ? " — aborted" : ""),
|
|
1666
1700
|
);
|
|
1667
1701
|
}
|
|
@@ -1671,8 +1705,16 @@ async function runPushAll(
|
|
|
1671
1705
|
const summary =
|
|
1672
1706
|
`\nPushed ${result.filesUploaded} file(s) ` +
|
|
1673
1707
|
`(${formatBytes(result.bytesUploaded)}) across ${result.attempted} ` +
|
|
1674
|
-
`target(s); ${result.filesDeleted} deleted; ${errored} error(s)
|
|
1675
|
-
|
|
1708
|
+
`target(s); ${result.filesDeleted} deleted; ${errored} error(s); ` +
|
|
1709
|
+
`${result.filesExcludedByScope} scope-excluded`;
|
|
1710
|
+
console.log(
|
|
1711
|
+
errored > 0 || result.filesExcludedByScope > 0
|
|
1712
|
+
? chalk.yellow(summary)
|
|
1713
|
+
: chalk.green(summary),
|
|
1714
|
+
);
|
|
1715
|
+
if (result.filesExcludedByScope > 0) {
|
|
1716
|
+
console.log(chalk.yellow(scopeExcludedWarning(result.filesExcludedByScope)!));
|
|
1717
|
+
}
|
|
1676
1718
|
if (errored > 0) process.exit(1);
|
|
1677
1719
|
}
|
|
1678
1720
|
|
|
@@ -1747,12 +1789,19 @@ async function runNowSingle(
|
|
|
1747
1789
|
...(journalSlug !== undefined ? { journalSlug } : {}),
|
|
1748
1790
|
...(author ? { author } : {}),
|
|
1749
1791
|
});
|
|
1792
|
+
const pushStatus =
|
|
1793
|
+
pushResult.aborted || pushResult.filesExcludedByScope > 0
|
|
1794
|
+
? chalk.yellow("⚠")
|
|
1795
|
+
: chalk.green("✓");
|
|
1750
1796
|
console.log(
|
|
1751
|
-
` ${
|
|
1797
|
+
` ${pushStatus} ` +
|
|
1752
1798
|
`${pushResult.filesUploaded} uploaded, ${pushResult.filesSkipped} skipped, ` +
|
|
1753
|
-
`${pushResult.filesDeleted} deleted` +
|
|
1799
|
+
`${pushResult.filesDeleted} deleted, ${pushResult.filesExcludedByScope} scope-excluded` +
|
|
1754
1800
|
(pushResult.aborted ? " — aborted" : ""),
|
|
1755
1801
|
);
|
|
1802
|
+
if (pushResult.filesExcludedByScope > 0) {
|
|
1803
|
+
console.log(chalk.yellow(scopeExcludedWarning(pushResult.filesExcludedByScope)!));
|
|
1804
|
+
}
|
|
1756
1805
|
if (pushResult.aborted) {
|
|
1757
1806
|
console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
|
|
1758
1807
|
process.exit(1);
|
|
@@ -65,6 +65,85 @@ function jsonRes(body: unknown, status = 200): Response {
|
|
|
65
65
|
});
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
describe("meetings get — short id resolution", () => {
|
|
69
|
+
it("resolves an 8-char short id to the full id before calling the by-id endpoint", async () => {
|
|
70
|
+
const fullId = "589bfd78-aaaa-bbbb-cccc-1234567890ab";
|
|
71
|
+
vi.mocked(vaultApiFetch)
|
|
72
|
+
.mockResolvedValueOnce(
|
|
73
|
+
jsonRes({
|
|
74
|
+
meetings: [
|
|
75
|
+
{
|
|
76
|
+
meetingId: fullId,
|
|
77
|
+
title: "T",
|
|
78
|
+
status: "ready",
|
|
79
|
+
startTime: "2026-01-01T00:00:00Z",
|
|
80
|
+
endTime: "2026-01-01T00:00:00Z",
|
|
81
|
+
duration: 0,
|
|
82
|
+
participantCount: 0,
|
|
83
|
+
hasTranscript: false,
|
|
84
|
+
hasNotes: false,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
.mockResolvedValueOnce(
|
|
90
|
+
jsonRes({
|
|
91
|
+
meetingId: fullId,
|
|
92
|
+
title: "T",
|
|
93
|
+
startTime: "2026-01-01T00:00:00Z",
|
|
94
|
+
endTime: "2026-01-01T00:00:00Z",
|
|
95
|
+
duration: 0,
|
|
96
|
+
participants: [],
|
|
97
|
+
calendarEventId: null,
|
|
98
|
+
botProvider: "recall",
|
|
99
|
+
sourceApp: "calendar",
|
|
100
|
+
companyId: "cmp_acme",
|
|
101
|
+
status: "ready",
|
|
102
|
+
recallBotId: null,
|
|
103
|
+
isShared: false,
|
|
104
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
105
|
+
updatedAt: "2026-01-01T00:00:00Z",
|
|
106
|
+
documentUrl: "",
|
|
107
|
+
hasTranscript: false,
|
|
108
|
+
hasNotes: false,
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
const program = buildProgram();
|
|
113
|
+
await program.parseAsync(["node", "hq", "meetings", "get", "589bfd78"]);
|
|
114
|
+
|
|
115
|
+
expect(vaultApiFetch).toHaveBeenNthCalledWith(1, {
|
|
116
|
+
token: "test-token",
|
|
117
|
+
path: "/v1/meetings",
|
|
118
|
+
query: { limit: "500" },
|
|
119
|
+
});
|
|
120
|
+
expect(vaultApiFetch).toHaveBeenNthCalledWith(2, {
|
|
121
|
+
token: "test-token",
|
|
122
|
+
path: `/v1/meetings/${fullId}`,
|
|
123
|
+
query: {},
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("errors and never sends the truncated id when the short id matches no meeting", async () => {
|
|
128
|
+
vi.spyOn(process, "exit").mockImplementation((() => {
|
|
129
|
+
throw new Error("process.exit");
|
|
130
|
+
}) as never);
|
|
131
|
+
vi.mocked(vaultApiFetch).mockResolvedValueOnce(jsonRes({ meetings: [] }));
|
|
132
|
+
|
|
133
|
+
const program = buildProgram();
|
|
134
|
+
await expect(
|
|
135
|
+
program.parseAsync(["node", "hq", "meetings", "get", "589bfd78"]),
|
|
136
|
+
).rejects.toThrow("process.exit");
|
|
137
|
+
|
|
138
|
+
expect(errSpy).toHaveBeenCalledWith(
|
|
139
|
+
expect.stringContaining("No meeting matches ID \"589bfd78\""),
|
|
140
|
+
);
|
|
141
|
+
expect(vaultApiFetch).not.toHaveBeenCalledWith(
|
|
142
|
+
expect.objectContaining({ path: "/v1/meetings/589bfd78" }),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
68
147
|
describe("meetings set-company", () => {
|
|
69
148
|
it("POSTs the resolved company id and applies to the recurring series by default", async () => {
|
|
70
149
|
vi.mocked(vaultApiFetch).mockResolvedValueOnce(
|
package/src/commands/meetings.ts
CHANGED
|
@@ -105,8 +105,16 @@ async function resolveShortId(
|
|
|
105
105
|
query: Record<string, string>,
|
|
106
106
|
): Promise<string> {
|
|
107
107
|
if (prefix.includes("-") && prefix.length > 8) return prefix;
|
|
108
|
-
const res = await vaultApiFetch({
|
|
109
|
-
|
|
108
|
+
const res = await vaultApiFetch({
|
|
109
|
+
token,
|
|
110
|
+
path: "/v1/meetings",
|
|
111
|
+
query: { ...query, limit: "500" },
|
|
112
|
+
});
|
|
113
|
+
if (!res.ok) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`Could not resolve meeting ID "${prefix}" — failed to list meetings (${res.status}). Pass the full meeting id.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
110
118
|
const data = (await res.json()) as { meetings: MeetingListItem[] };
|
|
111
119
|
const matches = data.meetings.filter((m) => m.meetingId.startsWith(prefix));
|
|
112
120
|
if (matches.length === 1) return matches[0].meetingId;
|
|
@@ -114,7 +122,12 @@ async function resolveShortId(
|
|
|
114
122
|
console.error(chalk.red(`Ambiguous ID prefix "${prefix}" — matches ${matches.length} meetings. Use a longer prefix.`));
|
|
115
123
|
process.exit(1);
|
|
116
124
|
}
|
|
117
|
-
|
|
125
|
+
console.error(
|
|
126
|
+
chalk.red(
|
|
127
|
+
`No meeting matches ID "${prefix}". It may be older than the meetings shown by \`hq meetings list\`, still processing, or attributed to a different company. Pass the full meeting id, add --company <slug>, or widen the list with \`hq meetings list --limit <n>\`.`,
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
process.exit(1);
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
async function handleApiError(res: Response): Promise<never> {
|