@indigoai-us/hq-cli 5.57.0 → 5.58.1
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/people.d.ts +12 -8
- package/dist/commands/people.js +87 -41
- package/dist/commands/secrets.js +26 -33
- package/dist/utils/sandbox-runner-client.d.ts +4 -9
- package/dist/utils/sandbox-runner-client.js +4 -11
- package/package.json +1 -1
- package/src/commands/people.test.ts +217 -52
- package/src/commands/people.ts +114 -64
- package/src/commands/secrets.test.ts +84 -78
- package/src/commands/secrets.ts +26 -32
- package/src/utils/sandbox-runner-client.test.ts +21 -23
- package/src/utils/sandbox-runner-client.ts +6 -19
|
@@ -20,10 +20,13 @@ import {
|
|
|
20
20
|
type PersonRecord,
|
|
21
21
|
} from "../utils/people.js";
|
|
22
22
|
import {
|
|
23
|
+
activeMemberToPersonRecord,
|
|
24
|
+
mergePeople,
|
|
23
25
|
registerPeopleCommand,
|
|
24
26
|
resolveCompanySlug,
|
|
25
|
-
type
|
|
27
|
+
type FetchPeopleRoster,
|
|
26
28
|
} from "./people.js";
|
|
29
|
+
import type { ActiveMember } from "./members.js";
|
|
27
30
|
|
|
28
31
|
// ---------------------------------------------------------------------------
|
|
29
32
|
// Filesystem fixture helpers
|
|
@@ -335,11 +338,119 @@ describe("companyPeopleDir", () => {
|
|
|
335
338
|
});
|
|
336
339
|
});
|
|
337
340
|
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Membership roster fallback helpers
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
function rosterPerson(overrides: Partial<PersonRecord>): PersonRecord {
|
|
346
|
+
return {
|
|
347
|
+
slug: "ada-lovelace",
|
|
348
|
+
name: "Ada Lovelace",
|
|
349
|
+
email: "ada@acme.com",
|
|
350
|
+
type: "internal",
|
|
351
|
+
source: "test roster",
|
|
352
|
+
...overrides,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
describe("activeMemberToPersonRecord", () => {
|
|
357
|
+
const base: ActiveMember = {
|
|
358
|
+
membershipKey: "m_1",
|
|
359
|
+
personUid: "prs_123",
|
|
360
|
+
companyUid: "co_123",
|
|
361
|
+
role: "admin",
|
|
362
|
+
status: "active",
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
it("maps membership person fields into an internal person record", () => {
|
|
366
|
+
expect(
|
|
367
|
+
activeMemberToPersonRecord({
|
|
368
|
+
...base,
|
|
369
|
+
personName: " Ada Lovelace ",
|
|
370
|
+
personEmail: " ada@acme.com ",
|
|
371
|
+
personSlug: " ada ",
|
|
372
|
+
}),
|
|
373
|
+
).toEqual({
|
|
374
|
+
slug: "ada",
|
|
375
|
+
name: "Ada Lovelace",
|
|
376
|
+
email: "ada@acme.com",
|
|
377
|
+
role: "admin",
|
|
378
|
+
type: "internal",
|
|
379
|
+
source: "hq-pro membership: /membership/company/co_123",
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("falls back from name to email to slug, and slugifies names", () => {
|
|
384
|
+
expect(
|
|
385
|
+
activeMemberToPersonRecord({
|
|
386
|
+
...base,
|
|
387
|
+
personName: "Grace Hopper",
|
|
388
|
+
personEmail: "grace@acme.com",
|
|
389
|
+
}),
|
|
390
|
+
).toMatchObject({ name: "Grace Hopper", slug: "grace-hopper" });
|
|
391
|
+
|
|
392
|
+
expect(
|
|
393
|
+
activeMemberToPersonRecord({
|
|
394
|
+
...base,
|
|
395
|
+
personEmail: "only-email@acme.com",
|
|
396
|
+
}),
|
|
397
|
+
).toMatchObject({ name: "only-email@acme.com", slug: "only-email-acme-com" });
|
|
398
|
+
|
|
399
|
+
expect(
|
|
400
|
+
activeMemberToPersonRecord({
|
|
401
|
+
...base,
|
|
402
|
+
personSlug: "only-slug",
|
|
403
|
+
}),
|
|
404
|
+
).toMatchObject({ name: "only-slug", slug: "only-slug" });
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it("returns null when all name-like fields are empty", () => {
|
|
408
|
+
expect(
|
|
409
|
+
activeMemberToPersonRecord({
|
|
410
|
+
...base,
|
|
411
|
+
personName: " ",
|
|
412
|
+
personEmail: "",
|
|
413
|
+
personSlug: " ",
|
|
414
|
+
}),
|
|
415
|
+
).toBeNull();
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
describe("mergePeople", () => {
|
|
420
|
+
it("deduplicates by email, slug, then name with primary records winning", () => {
|
|
421
|
+
const primary = [
|
|
422
|
+
rosterPerson({
|
|
423
|
+
slug: "local-email",
|
|
424
|
+
name: "Local Email",
|
|
425
|
+
email: "same@acme.com",
|
|
426
|
+
role: "CEO",
|
|
427
|
+
}),
|
|
428
|
+
rosterPerson({ slug: "same-slug", name: "Local Slug", email: undefined }),
|
|
429
|
+
rosterPerson({ slug: "", name: "Same Name", email: undefined }),
|
|
430
|
+
];
|
|
431
|
+
const extra = [
|
|
432
|
+
rosterPerson({
|
|
433
|
+
slug: "remote-email",
|
|
434
|
+
name: "Remote Email",
|
|
435
|
+
email: "SAME@acme.com",
|
|
436
|
+
}),
|
|
437
|
+
rosterPerson({ slug: "SAME-SLUG", name: "Remote Slug", email: undefined }),
|
|
438
|
+
rosterPerson({ slug: "", name: "same name", email: undefined }),
|
|
439
|
+
rosterPerson({ slug: "new-person", name: "New Person", email: undefined }),
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
expect(mergePeople(primary, extra)).toEqual([
|
|
443
|
+
...primary,
|
|
444
|
+
rosterPerson({ slug: "new-person", name: "New Person", email: undefined }),
|
|
445
|
+
]);
|
|
446
|
+
});
|
|
447
|
+
});
|
|
448
|
+
|
|
338
449
|
// ---------------------------------------------------------------------------
|
|
339
450
|
// Wired CLI surface (list / search / resolve)
|
|
340
451
|
// ---------------------------------------------------------------------------
|
|
341
452
|
|
|
342
|
-
function buildProgram(deps?: {
|
|
453
|
+
function buildProgram(deps?: { fetchRoster?: FetchPeopleRoster }): Command {
|
|
343
454
|
const program = new Command();
|
|
344
455
|
program.exitOverride(); // throw instead of process.exit on commander errors
|
|
345
456
|
registerPeopleCommand(program, deps);
|
|
@@ -348,8 +459,8 @@ function buildProgram(deps?: { refreshRoster?: RefreshPeopleRoster }): Command {
|
|
|
348
459
|
|
|
349
460
|
async function run(
|
|
350
461
|
args: string[],
|
|
351
|
-
deps: {
|
|
352
|
-
|
|
462
|
+
deps: { fetchRoster?: FetchPeopleRoster } = {
|
|
463
|
+
fetchRoster: async () => [],
|
|
353
464
|
},
|
|
354
465
|
): Promise<void> {
|
|
355
466
|
await buildProgram(deps).parseAsync(["node", "hq", ...args]);
|
|
@@ -389,6 +500,68 @@ describe("hq people (wired)", () => {
|
|
|
389
500
|
]);
|
|
390
501
|
});
|
|
391
502
|
|
|
503
|
+
it("list includes roster people, sorts them, and deduplicates with local winning", async () => {
|
|
504
|
+
const fetchRoster = vi.fn<FetchPeopleRoster>(async () => [
|
|
505
|
+
rosterPerson({
|
|
506
|
+
slug: "remote-jane",
|
|
507
|
+
name: "Remote Jane",
|
|
508
|
+
email: "JANE@acme.com",
|
|
509
|
+
role: "member",
|
|
510
|
+
}),
|
|
511
|
+
rosterPerson({
|
|
512
|
+
slug: "ada-lovelace",
|
|
513
|
+
name: "Ada Lovelace",
|
|
514
|
+
email: "ada@acme.com",
|
|
515
|
+
role: "admin",
|
|
516
|
+
}),
|
|
517
|
+
]);
|
|
518
|
+
|
|
519
|
+
await run(
|
|
520
|
+
["people", "list", "--company", "acme", "--hq-root", tmpRoot, "--json"],
|
|
521
|
+
{ fetchRoster },
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
expect(fetchRoster).toHaveBeenCalledTimes(1);
|
|
525
|
+
expect(fetchRoster).toHaveBeenCalledWith(tmpRoot, "acme");
|
|
526
|
+
const out = JSON.parse(logSpy.mock.calls[0][0] as string);
|
|
527
|
+
expect(out.map((p: PersonRecord) => p.name)).toEqual([
|
|
528
|
+
"Ada Lovelace",
|
|
529
|
+
"Jane Smith",
|
|
530
|
+
"John Doe",
|
|
531
|
+
]);
|
|
532
|
+
expect(out.find((p: PersonRecord) => p.email === "jane@acme.com")).toMatchObject({
|
|
533
|
+
name: "Jane Smith",
|
|
534
|
+
role: "CEO",
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("list --local-only skips roster fetch", async () => {
|
|
539
|
+
const fetchRoster = vi.fn<FetchPeopleRoster>(async () => [
|
|
540
|
+
rosterPerson({ slug: "ada-lovelace", name: "Ada Lovelace" }),
|
|
541
|
+
]);
|
|
542
|
+
|
|
543
|
+
await run(
|
|
544
|
+
[
|
|
545
|
+
"people",
|
|
546
|
+
"list",
|
|
547
|
+
"--company",
|
|
548
|
+
"acme",
|
|
549
|
+
"--hq-root",
|
|
550
|
+
tmpRoot,
|
|
551
|
+
"--json",
|
|
552
|
+
"--local-only",
|
|
553
|
+
],
|
|
554
|
+
{ fetchRoster },
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
expect(fetchRoster).not.toHaveBeenCalled();
|
|
558
|
+
const out = JSON.parse(logSpy.mock.calls[0][0] as string);
|
|
559
|
+
expect(out.map((p: PersonRecord) => p.name).sort()).toEqual([
|
|
560
|
+
"Jane Smith",
|
|
561
|
+
"John Doe",
|
|
562
|
+
]);
|
|
563
|
+
});
|
|
564
|
+
|
|
392
565
|
it("search narrows to the matching person", async () => {
|
|
393
566
|
await run([
|
|
394
567
|
"people",
|
|
@@ -405,8 +578,8 @@ describe("hq people (wired)", () => {
|
|
|
405
578
|
expect(out[0].email).toBe("jane@acme.com");
|
|
406
579
|
});
|
|
407
580
|
|
|
408
|
-
it("search local hit does not
|
|
409
|
-
const
|
|
581
|
+
it("search local hit does not fetch the roster", async () => {
|
|
582
|
+
const fetchRoster = vi.fn<FetchPeopleRoster>();
|
|
410
583
|
await run(
|
|
411
584
|
[
|
|
412
585
|
"people",
|
|
@@ -418,19 +591,15 @@ describe("hq people (wired)", () => {
|
|
|
418
591
|
tmpRoot,
|
|
419
592
|
"--json",
|
|
420
593
|
],
|
|
421
|
-
{
|
|
594
|
+
{ fetchRoster },
|
|
422
595
|
);
|
|
423
|
-
expect(
|
|
596
|
+
expect(fetchRoster).not.toHaveBeenCalled();
|
|
424
597
|
});
|
|
425
598
|
|
|
426
|
-
it("search
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
email: "ada@acme.com",
|
|
431
|
-
type: "internal",
|
|
432
|
-
});
|
|
433
|
-
});
|
|
599
|
+
it("search finds a teammate from the roster on a local miss", async () => {
|
|
600
|
+
const fetchRoster = vi.fn<FetchPeopleRoster>(async () => [
|
|
601
|
+
rosterPerson({ slug: "ada-lovelace", name: "Ada Lovelace" }),
|
|
602
|
+
]);
|
|
434
603
|
|
|
435
604
|
await run(
|
|
436
605
|
[
|
|
@@ -443,11 +612,11 @@ describe("hq people (wired)", () => {
|
|
|
443
612
|
tmpRoot,
|
|
444
613
|
"--json",
|
|
445
614
|
],
|
|
446
|
-
{
|
|
615
|
+
{ fetchRoster },
|
|
447
616
|
);
|
|
448
617
|
|
|
449
|
-
expect(
|
|
450
|
-
expect(
|
|
618
|
+
expect(fetchRoster).toHaveBeenCalledTimes(1);
|
|
619
|
+
expect(fetchRoster).toHaveBeenCalledWith(tmpRoot, "acme");
|
|
451
620
|
const out = JSON.parse(logSpy.mock.calls[0][0] as string);
|
|
452
621
|
expect(out).toHaveLength(1);
|
|
453
622
|
expect(out[0]).toMatchObject({
|
|
@@ -456,8 +625,8 @@ describe("hq people (wired)", () => {
|
|
|
456
625
|
});
|
|
457
626
|
});
|
|
458
627
|
|
|
459
|
-
it("search local miss with
|
|
460
|
-
const
|
|
628
|
+
it("search local miss with fetch failure reports the original empty JSON and logs a note", async () => {
|
|
629
|
+
const fetchRoster = vi.fn<FetchPeopleRoster>(async () => {
|
|
461
630
|
throw new Error("offline");
|
|
462
631
|
});
|
|
463
632
|
|
|
@@ -472,18 +641,18 @@ describe("hq people (wired)", () => {
|
|
|
472
641
|
tmpRoot,
|
|
473
642
|
"--json",
|
|
474
643
|
],
|
|
475
|
-
{
|
|
644
|
+
{ fetchRoster },
|
|
476
645
|
);
|
|
477
646
|
|
|
478
|
-
expect(
|
|
647
|
+
expect(fetchRoster).toHaveBeenCalledTimes(1);
|
|
479
648
|
expect(JSON.parse(logSpy.mock.calls[0][0] as string)).toEqual([]);
|
|
480
649
|
expect(errSpy).toHaveBeenCalledWith(
|
|
481
|
-
expect.stringContaining("Could not
|
|
650
|
+
expect.stringContaining("Could not fetch people roster"),
|
|
482
651
|
);
|
|
483
652
|
});
|
|
484
653
|
|
|
485
|
-
it("search --local-only skips
|
|
486
|
-
const
|
|
654
|
+
it("search --local-only skips fetch on a miss", async () => {
|
|
655
|
+
const fetchRoster = vi.fn<FetchPeopleRoster>();
|
|
487
656
|
await run(
|
|
488
657
|
[
|
|
489
658
|
"people",
|
|
@@ -496,10 +665,10 @@ describe("hq people (wired)", () => {
|
|
|
496
665
|
"--json",
|
|
497
666
|
"--local-only",
|
|
498
667
|
],
|
|
499
|
-
{
|
|
668
|
+
{ fetchRoster },
|
|
500
669
|
);
|
|
501
670
|
|
|
502
|
-
expect(
|
|
671
|
+
expect(fetchRoster).not.toHaveBeenCalled();
|
|
503
672
|
expect(JSON.parse(logSpy.mock.calls[0][0] as string)).toEqual([]);
|
|
504
673
|
});
|
|
505
674
|
|
|
@@ -516,8 +685,8 @@ describe("hq people (wired)", () => {
|
|
|
516
685
|
expect(logSpy).toHaveBeenCalledWith("jane@acme.com");
|
|
517
686
|
});
|
|
518
687
|
|
|
519
|
-
it("resolve local hit does not
|
|
520
|
-
const
|
|
688
|
+
it("resolve local hit does not fetch the roster", async () => {
|
|
689
|
+
const fetchRoster = vi.fn<FetchPeopleRoster>();
|
|
521
690
|
await run(
|
|
522
691
|
[
|
|
523
692
|
"people",
|
|
@@ -528,19 +697,15 @@ describe("hq people (wired)", () => {
|
|
|
528
697
|
"--hq-root",
|
|
529
698
|
tmpRoot,
|
|
530
699
|
],
|
|
531
|
-
{
|
|
700
|
+
{ fetchRoster },
|
|
532
701
|
);
|
|
533
|
-
expect(
|
|
702
|
+
expect(fetchRoster).not.toHaveBeenCalled();
|
|
534
703
|
});
|
|
535
704
|
|
|
536
|
-
it("resolve
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
email: "ada@acme.com",
|
|
541
|
-
type: "internal",
|
|
542
|
-
});
|
|
543
|
-
});
|
|
705
|
+
it("resolve finds a teammate that exists only in the fetched roster", async () => {
|
|
706
|
+
const fetchRoster = vi.fn<FetchPeopleRoster>(async () => [
|
|
707
|
+
rosterPerson({ slug: "ada-lovelace", name: "Ada Lovelace" }),
|
|
708
|
+
]);
|
|
544
709
|
|
|
545
710
|
await run(
|
|
546
711
|
[
|
|
@@ -552,16 +717,16 @@ describe("hq people (wired)", () => {
|
|
|
552
717
|
"--hq-root",
|
|
553
718
|
tmpRoot,
|
|
554
719
|
],
|
|
555
|
-
{
|
|
720
|
+
{ fetchRoster },
|
|
556
721
|
);
|
|
557
722
|
|
|
558
|
-
expect(
|
|
559
|
-
expect(
|
|
723
|
+
expect(fetchRoster).toHaveBeenCalledTimes(1);
|
|
724
|
+
expect(fetchRoster).toHaveBeenCalledWith(tmpRoot, "acme");
|
|
560
725
|
expect(logSpy).toHaveBeenCalledWith("ada@acme.com");
|
|
561
726
|
});
|
|
562
727
|
|
|
563
|
-
it("resolve local miss with
|
|
564
|
-
const
|
|
728
|
+
it("resolve local miss with fetch failure reports unchanged not_found JSON and logs a note", async () => {
|
|
729
|
+
const fetchRoster = vi.fn<FetchPeopleRoster>(async () => {
|
|
565
730
|
throw new Error("not signed in");
|
|
566
731
|
});
|
|
567
732
|
|
|
@@ -577,22 +742,22 @@ describe("hq people (wired)", () => {
|
|
|
577
742
|
tmpRoot,
|
|
578
743
|
"--json",
|
|
579
744
|
],
|
|
580
|
-
{
|
|
745
|
+
{ fetchRoster },
|
|
581
746
|
),
|
|
582
747
|
).rejects.toThrow("__exit__");
|
|
583
748
|
|
|
584
|
-
expect(
|
|
749
|
+
expect(fetchRoster).toHaveBeenCalledTimes(1);
|
|
585
750
|
expect(JSON.parse(logSpy.mock.calls[0][0] as string)).toEqual({
|
|
586
751
|
status: "not_found",
|
|
587
752
|
});
|
|
588
753
|
expect(errSpy).toHaveBeenCalledWith(
|
|
589
|
-
expect.stringContaining("Could not
|
|
754
|
+
expect.stringContaining("Could not fetch people roster"),
|
|
590
755
|
);
|
|
591
756
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
592
757
|
});
|
|
593
758
|
|
|
594
|
-
it("resolve --local-only skips
|
|
595
|
-
const
|
|
759
|
+
it("resolve --local-only skips fetch on a miss", async () => {
|
|
760
|
+
const fetchRoster = vi.fn<FetchPeopleRoster>();
|
|
596
761
|
await expect(
|
|
597
762
|
run(
|
|
598
763
|
[
|
|
@@ -606,11 +771,11 @@ describe("hq people (wired)", () => {
|
|
|
606
771
|
"--json",
|
|
607
772
|
"--local-only",
|
|
608
773
|
],
|
|
609
|
-
{
|
|
774
|
+
{ fetchRoster },
|
|
610
775
|
),
|
|
611
776
|
).rejects.toThrow("__exit__");
|
|
612
777
|
|
|
613
|
-
expect(
|
|
778
|
+
expect(fetchRoster).not.toHaveBeenCalled();
|
|
614
779
|
expect(JSON.parse(logSpy.mock.calls[0][0] as string)).toEqual({
|
|
615
780
|
status: "not_found",
|
|
616
781
|
});
|