@indigoai-us/hq-cli 5.58.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/package.json +1 -1
- package/src/commands/people.test.ts +217 -52
- package/src/commands/people.ts +114 -64
|
@@ -6,21 +6,25 @@
|
|
|
6
6
|
* hq people search <keyword> [--company <slug>] [--json]
|
|
7
7
|
* hq people resolve <name> [--company <slug>] [--json]
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Local records from `companies/<company>/people/<person>/meta.yaml` are the
|
|
10
|
+
* curated primary source. On misses, commands may fall back to the membership
|
|
11
|
+
* roster for exactly ONE company (the active company, or the one named by
|
|
12
|
+
* `--company`); nothing reads across company boundaries.
|
|
12
13
|
*/
|
|
13
14
|
import { Command } from "commander";
|
|
15
|
+
import { type ActiveMember } from "./members.js";
|
|
14
16
|
import { resolveNameToEmail, type PersonRecord } from "../utils/people.js";
|
|
15
|
-
export type
|
|
17
|
+
export type FetchPeopleRoster = (hqRoot: string, companySlug: string) => Promise<PersonRecord[]>;
|
|
16
18
|
interface PeopleCommandDeps {
|
|
17
|
-
|
|
19
|
+
fetchRoster?: FetchPeopleRoster;
|
|
18
20
|
}
|
|
19
21
|
interface PeopleLookupOpts {
|
|
20
22
|
localOnly?: boolean;
|
|
21
23
|
json?: boolean;
|
|
22
24
|
}
|
|
23
|
-
export declare function
|
|
25
|
+
export declare function activeMemberToPersonRecord(m: ActiveMember): PersonRecord | null;
|
|
26
|
+
export declare function fetchMembershipRoster(hqRoot: string, companySlug: string): Promise<PersonRecord[]>;
|
|
27
|
+
export declare function mergePeople(primary: PersonRecord[], extra: PersonRecord[]): PersonRecord[];
|
|
24
28
|
/**
|
|
25
29
|
* Resolve the single company to operate on. Explicit `--company` always wins
|
|
26
30
|
* (after a path-safety check). Otherwise the active company is inferred from
|
|
@@ -33,14 +37,14 @@ export declare function resolvePersonWithRosterFallback(input: {
|
|
|
33
37
|
slug: string;
|
|
34
38
|
name: string;
|
|
35
39
|
opts?: PeopleLookupOpts;
|
|
36
|
-
|
|
40
|
+
fetchRoster?: FetchPeopleRoster;
|
|
37
41
|
}): Promise<ReturnType<typeof resolveNameToEmail>>;
|
|
38
42
|
export declare function searchPeopleWithRosterFallback(input: {
|
|
39
43
|
hqRoot: string;
|
|
40
44
|
slug: string;
|
|
41
45
|
keyword: string;
|
|
42
46
|
opts?: PeopleLookupOpts;
|
|
43
|
-
|
|
47
|
+
fetchRoster?: FetchPeopleRoster;
|
|
44
48
|
}): Promise<PersonRecord[]>;
|
|
45
49
|
export declare function registerPeopleCommand(program: Command, deps?: PeopleCommandDeps): void;
|
|
46
50
|
export {};
|
package/dist/commands/people.js
CHANGED
|
@@ -6,35 +6,73 @@
|
|
|
6
6
|
* hq people search <keyword> [--company <slug>] [--json]
|
|
7
7
|
* hq people resolve <name> [--company <slug>] [--json]
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Local records from `companies/<company>/people/<person>/meta.yaml` are the
|
|
10
|
+
* curated primary source. On misses, commands may fall back to the membership
|
|
11
|
+
* roster for exactly ONE company (the active company, or the one named by
|
|
12
|
+
* `--company`); nothing reads across company boundaries.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
|
-
!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]="
|
|
15
|
+
!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]="555055d9-98e2-5fcd-b736-4b1df12daf4d")}catch(e){}}();
|
|
15
16
|
import * as fs from "fs";
|
|
16
17
|
import { Option } from "commander";
|
|
17
18
|
import chalk from "chalk";
|
|
18
|
-
import { VaultClient } from "@indigoai-us/hq-cloud";
|
|
19
19
|
import * as yaml from "js-yaml";
|
|
20
20
|
import { findHqRoot } from "../utils/manifest.js";
|
|
21
21
|
import { manifestPath } from "./cloud-provision.js";
|
|
22
|
-
import {
|
|
22
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
23
23
|
import { getCompanyUid } from "../utils/vault-api.js";
|
|
24
|
-
import {
|
|
24
|
+
import { listActiveMembers } from "./members.js";
|
|
25
25
|
import { assertSafeCompanySlug, listCompanyPeople, searchPeople, resolveNameToEmail, companyPeopleDir, } from "../utils/people.js";
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
26
|
+
function slugify(value) {
|
|
27
|
+
return value
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
30
|
+
.replace(/-+/g, "-")
|
|
31
|
+
.replace(/^-|-$/g, "");
|
|
32
|
+
}
|
|
33
|
+
export function activeMemberToPersonRecord(m) {
|
|
34
|
+
const name = m.personName?.trim() ||
|
|
35
|
+
m.personEmail?.trim() ||
|
|
36
|
+
m.personSlug?.trim() ||
|
|
37
|
+
"";
|
|
38
|
+
if (!name)
|
|
39
|
+
return null;
|
|
40
|
+
const email = m.personEmail?.trim() || undefined;
|
|
41
|
+
const slug = m.personSlug?.trim() || slugify(name) || m.personUid;
|
|
42
|
+
return {
|
|
43
|
+
slug,
|
|
44
|
+
name,
|
|
45
|
+
email,
|
|
46
|
+
role: m.role || undefined,
|
|
47
|
+
type: "internal",
|
|
48
|
+
source: `hq-pro membership: /membership/company/${m.companyUid}`,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export async function fetchMembershipRoster(hqRoot, companySlug) {
|
|
52
|
+
void hqRoot;
|
|
53
|
+
const token = await ensureCognitoToken();
|
|
54
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
55
|
+
const members = await listActiveMembers(token, companyUid);
|
|
56
|
+
return members
|
|
57
|
+
.map(activeMemberToPersonRecord)
|
|
58
|
+
.filter((r) => r !== null);
|
|
59
|
+
}
|
|
60
|
+
function personIdentityKey(person) {
|
|
61
|
+
return (person.email?.toLowerCase() ||
|
|
62
|
+
person.slug?.toLowerCase() ||
|
|
63
|
+
person.name.toLowerCase());
|
|
64
|
+
}
|
|
65
|
+
export function mergePeople(primary, extra) {
|
|
66
|
+
const seen = new Set(primary.map(personIdentityKey));
|
|
67
|
+
const merged = [...primary];
|
|
68
|
+
for (const person of extra) {
|
|
69
|
+
const key = personIdentityKey(person);
|
|
70
|
+
if (seen.has(key))
|
|
71
|
+
continue;
|
|
72
|
+
seen.add(key);
|
|
73
|
+
merged.push(person);
|
|
74
|
+
}
|
|
75
|
+
return merged;
|
|
38
76
|
}
|
|
39
77
|
/** Companies that still exist (anything not explicitly `status: archived`). */
|
|
40
78
|
function activeCompanySlugs(manifest) {
|
|
@@ -103,37 +141,38 @@ function fail(message) {
|
|
|
103
141
|
console.error(chalk.red(message));
|
|
104
142
|
process.exit(1);
|
|
105
143
|
}
|
|
106
|
-
function
|
|
144
|
+
function logRosterFetchFailure(companySlug, err) {
|
|
107
145
|
const message = err instanceof Error ? err.message : String(err);
|
|
108
|
-
console.error(chalk.dim(` Could not
|
|
146
|
+
console.error(chalk.dim(` Could not fetch people roster for '${companySlug}': ${message}`));
|
|
109
147
|
}
|
|
110
|
-
async function
|
|
148
|
+
async function tryFetchRoster(fetchRoster, hqRoot, slug) {
|
|
111
149
|
try {
|
|
112
|
-
await
|
|
113
|
-
return true;
|
|
150
|
+
return await fetchRoster(hqRoot, slug);
|
|
114
151
|
}
|
|
115
152
|
catch (err) {
|
|
116
|
-
|
|
117
|
-
return
|
|
153
|
+
logRosterFetchFailure(slug, err);
|
|
154
|
+
return null;
|
|
118
155
|
}
|
|
119
156
|
}
|
|
120
157
|
export async function resolvePersonWithRosterFallback(input) {
|
|
121
|
-
const
|
|
158
|
+
const localPeople = listCompanyPeople(input.hqRoot, input.slug);
|
|
159
|
+
const local = resolveNameToEmail(localPeople, input.name);
|
|
122
160
|
if (local.status !== "not_found" || input.opts?.localOnly)
|
|
123
161
|
return local;
|
|
124
|
-
const
|
|
125
|
-
if (!
|
|
162
|
+
const roster = await tryFetchRoster(input.fetchRoster ?? fetchMembershipRoster, input.hqRoot, input.slug);
|
|
163
|
+
if (!roster)
|
|
126
164
|
return local;
|
|
127
|
-
return resolveNameToEmail(
|
|
165
|
+
return resolveNameToEmail(mergePeople(localPeople, roster), input.name);
|
|
128
166
|
}
|
|
129
167
|
export async function searchPeopleWithRosterFallback(input) {
|
|
130
|
-
const
|
|
168
|
+
const localPeople = listCompanyPeople(input.hqRoot, input.slug);
|
|
169
|
+
const local = searchPeople(localPeople, input.keyword);
|
|
131
170
|
if (local.length > 0 || input.opts?.localOnly)
|
|
132
171
|
return local;
|
|
133
|
-
const
|
|
134
|
-
if (!
|
|
172
|
+
const roster = await tryFetchRoster(input.fetchRoster ?? fetchMembershipRoster, input.hqRoot, input.slug);
|
|
173
|
+
if (!roster)
|
|
135
174
|
return local;
|
|
136
|
-
return searchPeople(
|
|
175
|
+
return searchPeople(mergePeople(localPeople, roster), input.keyword);
|
|
137
176
|
}
|
|
138
177
|
export function registerPeopleCommand(program, deps = {}) {
|
|
139
178
|
const people = program
|
|
@@ -147,12 +186,19 @@ export function registerPeopleCommand(program, deps = {}) {
|
|
|
147
186
|
.command("list")
|
|
148
187
|
.description("List all people recorded for the company")
|
|
149
188
|
.option("--json", "Output JSON instead of a table")
|
|
150
|
-
.
|
|
189
|
+
.option("--local-only", "Skip network fallback; list only the local people roster")
|
|
190
|
+
.action(async (opts) => {
|
|
151
191
|
try {
|
|
152
192
|
const scope = people.opts();
|
|
153
193
|
const hqRoot = resolveHqRoot(scope);
|
|
154
194
|
const slug = resolveCompanySlug(hqRoot, scope.company);
|
|
155
|
-
|
|
195
|
+
let records = listCompanyPeople(hqRoot, slug);
|
|
196
|
+
if (!opts.localOnly) {
|
|
197
|
+
const roster = await tryFetchRoster(deps.fetchRoster ?? fetchMembershipRoster, hqRoot, slug);
|
|
198
|
+
if (roster)
|
|
199
|
+
records = mergePeople(records, roster);
|
|
200
|
+
}
|
|
201
|
+
records = records.sort((a, b) => a.name.localeCompare(b.name));
|
|
156
202
|
if (opts.json) {
|
|
157
203
|
console.log(JSON.stringify(records, null, 2));
|
|
158
204
|
return;
|
|
@@ -171,7 +217,7 @@ export function registerPeopleCommand(program, deps = {}) {
|
|
|
171
217
|
.command("search <keyword>")
|
|
172
218
|
.description("Keyword search over people names and emails")
|
|
173
219
|
.option("--json", "Output JSON instead of a table")
|
|
174
|
-
.option("--local-only", "Skip
|
|
220
|
+
.option("--local-only", "Skip network fallback; search only the local people roster")
|
|
175
221
|
.action(async (keyword, opts) => {
|
|
176
222
|
try {
|
|
177
223
|
const scope = people.opts();
|
|
@@ -182,7 +228,7 @@ export function registerPeopleCommand(program, deps = {}) {
|
|
|
182
228
|
slug,
|
|
183
229
|
keyword,
|
|
184
230
|
opts,
|
|
185
|
-
|
|
231
|
+
fetchRoster: deps.fetchRoster,
|
|
186
232
|
});
|
|
187
233
|
if (opts.json) {
|
|
188
234
|
console.log(JSON.stringify(matches, null, 2));
|
|
@@ -202,7 +248,7 @@ export function registerPeopleCommand(program, deps = {}) {
|
|
|
202
248
|
.command("resolve <name>")
|
|
203
249
|
.description("Resolve a person name to their email address")
|
|
204
250
|
.option("--json", "Output JSON instead of plain text")
|
|
205
|
-
.option("--local-only", "Skip
|
|
251
|
+
.option("--local-only", "Skip network fallback; resolve only from the local people roster")
|
|
206
252
|
.action(async (name, opts) => {
|
|
207
253
|
try {
|
|
208
254
|
const scope = people.opts();
|
|
@@ -213,7 +259,7 @@ export function registerPeopleCommand(program, deps = {}) {
|
|
|
213
259
|
slug,
|
|
214
260
|
name,
|
|
215
261
|
opts,
|
|
216
|
-
|
|
262
|
+
fetchRoster: deps.fetchRoster,
|
|
217
263
|
});
|
|
218
264
|
if (opts.json) {
|
|
219
265
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -247,4 +293,4 @@ export function registerPeopleCommand(program, deps = {}) {
|
|
|
247
293
|
});
|
|
248
294
|
}
|
|
249
295
|
//# sourceMappingURL=people.js.map
|
|
250
|
-
//# debugId=
|
|
296
|
+
//# debugId=555055d9-98e2-5fcd-b736-4b1df12daf4d
|
package/package.json
CHANGED
|
@@ -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
|
});
|
package/src/commands/people.ts
CHANGED
|
@@ -6,25 +6,21 @@
|
|
|
6
6
|
* hq people search <keyword> [--company <slug>] [--json]
|
|
7
7
|
* hq people resolve <name> [--company <slug>] [--json]
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Local records from `companies/<company>/people/<person>/meta.yaml` are the
|
|
10
|
+
* curated primary source. On misses, commands may fall back to the membership
|
|
11
|
+
* roster for exactly ONE company (the active company, or the one named by
|
|
12
|
+
* `--company`); nothing reads across company boundaries.
|
|
12
13
|
*/
|
|
13
14
|
|
|
14
15
|
import * as fs from "fs";
|
|
15
16
|
import { Command, Option } from "commander";
|
|
16
17
|
import chalk from "chalk";
|
|
17
|
-
import { VaultClient } from "@indigoai-us/hq-cloud";
|
|
18
18
|
import * as yaml from "js-yaml";
|
|
19
19
|
import { findHqRoot } from "../utils/manifest.js";
|
|
20
20
|
import { manifestPath, type ManifestDoc } from "./cloud-provision.js";
|
|
21
|
-
import {
|
|
22
|
-
DEFAULT_COGNITO,
|
|
23
|
-
buildVaultConfig,
|
|
24
|
-
ensureCognitoToken,
|
|
25
|
-
} from "../utils/cognito-session.js";
|
|
21
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
26
22
|
import { getCompanyUid } from "../utils/vault-api.js";
|
|
27
|
-
import {
|
|
23
|
+
import { listActiveMembers, type ActiveMember } from "./members.js";
|
|
28
24
|
import {
|
|
29
25
|
assertSafeCompanySlug,
|
|
30
26
|
listCompanyPeople,
|
|
@@ -39,13 +35,13 @@ interface PeopleScopeOpts {
|
|
|
39
35
|
hqRoot?: string;
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
export type
|
|
38
|
+
export type FetchPeopleRoster = (
|
|
43
39
|
hqRoot: string,
|
|
44
40
|
companySlug: string,
|
|
45
|
-
) => Promise<
|
|
41
|
+
) => Promise<PersonRecord[]>;
|
|
46
42
|
|
|
47
43
|
interface PeopleCommandDeps {
|
|
48
|
-
|
|
44
|
+
fetchRoster?: FetchPeopleRoster;
|
|
49
45
|
}
|
|
50
46
|
|
|
51
47
|
interface PeopleLookupOpts {
|
|
@@ -53,23 +49,71 @@ interface PeopleLookupOpts {
|
|
|
53
49
|
json?: boolean;
|
|
54
50
|
}
|
|
55
51
|
|
|
56
|
-
|
|
52
|
+
function slugify(value: string): string {
|
|
53
|
+
return value
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
56
|
+
.replace(/-+/g, "-")
|
|
57
|
+
.replace(/^-|-$/g, "");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function activeMemberToPersonRecord(
|
|
61
|
+
m: ActiveMember,
|
|
62
|
+
): PersonRecord | null {
|
|
63
|
+
const name =
|
|
64
|
+
m.personName?.trim() ||
|
|
65
|
+
m.personEmail?.trim() ||
|
|
66
|
+
m.personSlug?.trim() ||
|
|
67
|
+
"";
|
|
68
|
+
if (!name) return null;
|
|
69
|
+
|
|
70
|
+
const email = m.personEmail?.trim() || undefined;
|
|
71
|
+
const slug = m.personSlug?.trim() || slugify(name) || m.personUid;
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
slug,
|
|
75
|
+
name,
|
|
76
|
+
email,
|
|
77
|
+
role: m.role || undefined,
|
|
78
|
+
type: "internal",
|
|
79
|
+
source: `hq-pro membership: /membership/company/${m.companyUid}`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function fetchMembershipRoster(
|
|
57
84
|
hqRoot: string,
|
|
58
85
|
companySlug: string,
|
|
59
|
-
): Promise<
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
await getCompanyUid(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
86
|
+
): Promise<PersonRecord[]> {
|
|
87
|
+
void hqRoot;
|
|
88
|
+
const token = await ensureCognitoToken();
|
|
89
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
90
|
+
const members = await listActiveMembers(token, companyUid);
|
|
91
|
+
return members
|
|
92
|
+
.map(activeMemberToPersonRecord)
|
|
93
|
+
.filter((r): r is PersonRecord => r !== null);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function personIdentityKey(person: PersonRecord): string {
|
|
97
|
+
return (
|
|
98
|
+
person.email?.toLowerCase() ||
|
|
99
|
+
person.slug?.toLowerCase() ||
|
|
100
|
+
person.name.toLowerCase()
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function mergePeople(
|
|
105
|
+
primary: PersonRecord[],
|
|
106
|
+
extra: PersonRecord[],
|
|
107
|
+
): PersonRecord[] {
|
|
108
|
+
const seen = new Set(primary.map(personIdentityKey));
|
|
109
|
+
const merged = [...primary];
|
|
110
|
+
for (const person of extra) {
|
|
111
|
+
const key = personIdentityKey(person);
|
|
112
|
+
if (seen.has(key)) continue;
|
|
113
|
+
seen.add(key);
|
|
114
|
+
merged.push(person);
|
|
115
|
+
}
|
|
116
|
+
return merged;
|
|
73
117
|
}
|
|
74
118
|
|
|
75
119
|
/** Companies that still exist (anything not explicitly `status: archived`). */
|
|
@@ -159,26 +203,25 @@ function fail(message: string): never {
|
|
|
159
203
|
process.exit(1);
|
|
160
204
|
}
|
|
161
205
|
|
|
162
|
-
function
|
|
206
|
+
function logRosterFetchFailure(companySlug: string, err: unknown): void {
|
|
163
207
|
const message = err instanceof Error ? err.message : String(err);
|
|
164
208
|
console.error(
|
|
165
209
|
chalk.dim(
|
|
166
|
-
` Could not
|
|
210
|
+
` Could not fetch people roster for '${companySlug}': ${message}`,
|
|
167
211
|
),
|
|
168
212
|
);
|
|
169
213
|
}
|
|
170
214
|
|
|
171
|
-
async function
|
|
172
|
-
|
|
215
|
+
async function tryFetchRoster(
|
|
216
|
+
fetchRoster: FetchPeopleRoster,
|
|
173
217
|
hqRoot: string,
|
|
174
218
|
slug: string,
|
|
175
|
-
): Promise<
|
|
219
|
+
): Promise<PersonRecord[] | null> {
|
|
176
220
|
try {
|
|
177
|
-
await
|
|
178
|
-
return true;
|
|
221
|
+
return await fetchRoster(hqRoot, slug);
|
|
179
222
|
} catch (err) {
|
|
180
|
-
|
|
181
|
-
return
|
|
223
|
+
logRosterFetchFailure(slug, err);
|
|
224
|
+
return null;
|
|
182
225
|
}
|
|
183
226
|
}
|
|
184
227
|
|
|
@@ -188,25 +231,20 @@ export async function resolvePersonWithRosterFallback(
|
|
|
188
231
|
slug: string;
|
|
189
232
|
name: string;
|
|
190
233
|
opts?: PeopleLookupOpts;
|
|
191
|
-
|
|
234
|
+
fetchRoster?: FetchPeopleRoster;
|
|
192
235
|
},
|
|
193
236
|
): Promise<ReturnType<typeof resolveNameToEmail>> {
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
input.name,
|
|
197
|
-
);
|
|
237
|
+
const localPeople = listCompanyPeople(input.hqRoot, input.slug);
|
|
238
|
+
const local = resolveNameToEmail(localPeople, input.name);
|
|
198
239
|
if (local.status !== "not_found" || input.opts?.localOnly) return local;
|
|
199
240
|
|
|
200
|
-
const
|
|
201
|
-
input.
|
|
241
|
+
const roster = await tryFetchRoster(
|
|
242
|
+
input.fetchRoster ?? fetchMembershipRoster,
|
|
202
243
|
input.hqRoot,
|
|
203
244
|
input.slug,
|
|
204
245
|
);
|
|
205
|
-
if (!
|
|
206
|
-
return resolveNameToEmail(
|
|
207
|
-
listCompanyPeople(input.hqRoot, input.slug),
|
|
208
|
-
input.name,
|
|
209
|
-
);
|
|
246
|
+
if (!roster) return local;
|
|
247
|
+
return resolveNameToEmail(mergePeople(localPeople, roster), input.name);
|
|
210
248
|
}
|
|
211
249
|
|
|
212
250
|
export async function searchPeopleWithRosterFallback(input: {
|
|
@@ -214,21 +252,19 @@ export async function searchPeopleWithRosterFallback(input: {
|
|
|
214
252
|
slug: string;
|
|
215
253
|
keyword: string;
|
|
216
254
|
opts?: PeopleLookupOpts;
|
|
217
|
-
|
|
255
|
+
fetchRoster?: FetchPeopleRoster;
|
|
218
256
|
}): Promise<PersonRecord[]> {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
input.keyword,
|
|
222
|
-
);
|
|
257
|
+
const localPeople = listCompanyPeople(input.hqRoot, input.slug);
|
|
258
|
+
const local = searchPeople(localPeople, input.keyword);
|
|
223
259
|
if (local.length > 0 || input.opts?.localOnly) return local;
|
|
224
260
|
|
|
225
|
-
const
|
|
226
|
-
input.
|
|
261
|
+
const roster = await tryFetchRoster(
|
|
262
|
+
input.fetchRoster ?? fetchMembershipRoster,
|
|
227
263
|
input.hqRoot,
|
|
228
264
|
input.slug,
|
|
229
265
|
);
|
|
230
|
-
if (!
|
|
231
|
-
return searchPeople(
|
|
266
|
+
if (!roster) return local;
|
|
267
|
+
return searchPeople(mergePeople(localPeople, roster), input.keyword);
|
|
232
268
|
}
|
|
233
269
|
|
|
234
270
|
export function registerPeopleCommand(
|
|
@@ -257,12 +293,26 @@ export function registerPeopleCommand(
|
|
|
257
293
|
.command("list")
|
|
258
294
|
.description("List all people recorded for the company")
|
|
259
295
|
.option("--json", "Output JSON instead of a table")
|
|
260
|
-
.
|
|
296
|
+
.option(
|
|
297
|
+
"--local-only",
|
|
298
|
+
"Skip network fallback; list only the local people roster",
|
|
299
|
+
)
|
|
300
|
+
.action(async (opts: PeopleLookupOpts) => {
|
|
261
301
|
try {
|
|
262
302
|
const scope = people.opts() as PeopleScopeOpts;
|
|
263
303
|
const hqRoot = resolveHqRoot(scope);
|
|
264
304
|
const slug = resolveCompanySlug(hqRoot, scope.company);
|
|
265
|
-
|
|
305
|
+
let records = listCompanyPeople(hqRoot, slug);
|
|
306
|
+
|
|
307
|
+
if (!opts.localOnly) {
|
|
308
|
+
const roster = await tryFetchRoster(
|
|
309
|
+
deps.fetchRoster ?? fetchMembershipRoster,
|
|
310
|
+
hqRoot,
|
|
311
|
+
slug,
|
|
312
|
+
);
|
|
313
|
+
if (roster) records = mergePeople(records, roster);
|
|
314
|
+
}
|
|
315
|
+
records = records.sort((a, b) => a.name.localeCompare(b.name));
|
|
266
316
|
|
|
267
317
|
if (opts.json) {
|
|
268
318
|
console.log(JSON.stringify(records, null, 2));
|
|
@@ -288,7 +338,7 @@ export function registerPeopleCommand(
|
|
|
288
338
|
.option("--json", "Output JSON instead of a table")
|
|
289
339
|
.option(
|
|
290
340
|
"--local-only",
|
|
291
|
-
"Skip
|
|
341
|
+
"Skip network fallback; search only the local people roster",
|
|
292
342
|
)
|
|
293
343
|
.action(async (keyword: string, opts: PeopleLookupOpts) => {
|
|
294
344
|
try {
|
|
@@ -300,7 +350,7 @@ export function registerPeopleCommand(
|
|
|
300
350
|
slug,
|
|
301
351
|
keyword,
|
|
302
352
|
opts,
|
|
303
|
-
|
|
353
|
+
fetchRoster: deps.fetchRoster,
|
|
304
354
|
});
|
|
305
355
|
|
|
306
356
|
if (opts.json) {
|
|
@@ -323,7 +373,7 @@ export function registerPeopleCommand(
|
|
|
323
373
|
.option("--json", "Output JSON instead of plain text")
|
|
324
374
|
.option(
|
|
325
375
|
"--local-only",
|
|
326
|
-
"Skip
|
|
376
|
+
"Skip network fallback; resolve only from the local people roster",
|
|
327
377
|
)
|
|
328
378
|
.action(async (name: string, opts: PeopleLookupOpts) => {
|
|
329
379
|
try {
|
|
@@ -335,7 +385,7 @@ export function registerPeopleCommand(
|
|
|
335
385
|
slug,
|
|
336
386
|
name,
|
|
337
387
|
opts,
|
|
338
|
-
|
|
388
|
+
fetchRoster: deps.fetchRoster,
|
|
339
389
|
});
|
|
340
390
|
|
|
341
391
|
if (opts.json) {
|