@indigoai-us/hq-cli 5.50.1 → 5.51.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.
Files changed (61) hide show
  1. package/dist/bin/hq-auth-refresh.d.ts +1 -1
  2. package/dist/bin/hq-auth-refresh.js +5 -2
  3. package/dist/commands/members.d.ts +17 -0
  4. package/dist/commands/members.js +65 -28
  5. package/dist/commands/onboard-warning.d.ts +7 -0
  6. package/dist/commands/onboard-warning.js +14 -0
  7. package/dist/commands/onboard.js +5 -5
  8. package/dist/commands/pack-install.d.ts +12 -0
  9. package/dist/commands/pack-install.js +74 -3
  10. package/dist/commands/packs.js +17 -3
  11. package/dist/commands/people.d.ts +26 -1
  12. package/dist/commands/people.js +70 -7
  13. package/dist/commands/secrets-scope.d.ts +20 -0
  14. package/dist/commands/secrets-scope.js +19 -0
  15. package/dist/commands/secrets.js +21 -6
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.js +44 -14
  18. package/dist/node-preflight.d.ts +39 -0
  19. package/dist/node-preflight.js +55 -0
  20. package/dist/sentry.d.ts +12 -0
  21. package/dist/sentry.js +19 -3
  22. package/dist/types.d.ts +18 -0
  23. package/dist/utils/epipe.d.ts +8 -0
  24. package/dist/utils/epipe.js +30 -0
  25. package/dist/utils/intercepted-process-exit.d.ts +7 -0
  26. package/dist/utils/intercepted-process-exit.js +38 -0
  27. package/dist/utils/pack-contributions.d.ts +7 -0
  28. package/dist/utils/pack-contributions.js +12 -2
  29. package/dist/utils/version-gate.d.ts +40 -1
  30. package/dist/utils/version-gate.js +91 -20
  31. package/e2e/cli.test.ts +35 -0
  32. package/package.json +1 -1
  33. package/src/bin/hq-auth-refresh.ts +3 -0
  34. package/src/commands/members.test.ts +176 -0
  35. package/src/commands/members.ts +113 -28
  36. package/src/commands/onboard-warning.test.ts +26 -0
  37. package/src/commands/onboard-warning.ts +12 -0
  38. package/src/commands/onboard.ts +4 -7
  39. package/src/commands/pack-install.test.ts +144 -0
  40. package/src/commands/pack-install.ts +86 -1
  41. package/src/commands/packs.ts +19 -0
  42. package/src/commands/people.test.ts +212 -5
  43. package/src/commands/people.ts +141 -5
  44. package/src/commands/secrets-scope.test.ts +56 -0
  45. package/src/commands/secrets-scope.ts +32 -0
  46. package/src/commands/secrets.ts +24 -10
  47. package/src/index.ts +40 -12
  48. package/src/node-preflight.test.ts +60 -0
  49. package/src/node-preflight.ts +67 -0
  50. package/src/sentry-epipe.test.ts +37 -0
  51. package/src/sentry-release.test.ts +54 -0
  52. package/src/sentry.ts +21 -1
  53. package/src/types.ts +19 -1
  54. package/src/utils/epipe.test.ts +28 -0
  55. package/src/utils/epipe.ts +29 -0
  56. package/src/utils/intercepted-process-exit.test.ts +37 -0
  57. package/src/utils/intercepted-process-exit.ts +36 -0
  58. package/src/utils/pack-contributions.test.ts +53 -0
  59. package/src/utils/pack-contributions.ts +17 -0
  60. package/src/utils/version-gate.test.ts +122 -0
  61. package/src/utils/version-gate.ts +109 -13
@@ -10,5 +10,5 @@
10
10
  * machine-token mint). Exit 1 if no valid session could be ensured
11
11
  * non-interactively.
12
12
  */
13
- export {};
13
+ import "../node-preflight.js";
14
14
  //# sourceMappingURL=hq-auth-refresh.d.ts.map
@@ -10,8 +10,11 @@
10
10
  * machine-token mint). Exit 1 if no valid session could be ensured
11
11
  * non-interactively.
12
12
  */
13
+ // MUST be first: guard the Node version before any dependency that needs a
14
+ // Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
13
15
 
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]="03d2c531-f620-5d93-a87b-9a87264ad77b")}catch(e){}}();
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]="3c7e1b4e-66dc-5fe8-b828-cd3636df1fbb")}catch(e){}}();
17
+ import "../node-preflight.js";
15
18
  import { initSentry, Sentry } from "../sentry.js";
16
19
  import { refreshCachedSession } from "../utils/cognito-session.js";
17
20
  initSentry();
@@ -38,4 +41,4 @@ initSentry();
38
41
  }
39
42
  })();
40
43
  //# sourceMappingURL=hq-auth-refresh.js.map
41
- //# debugId=03d2c531-f620-5d93-a87b-9a87264ad77b
44
+ //# debugId=3c7e1b4e-66dc-5fe8-b828-cd3636df1fbb
@@ -12,6 +12,22 @@ export interface PendingInvite {
12
12
  invitedBy: string;
13
13
  invitedAt: string;
14
14
  }
15
+ /**
16
+ * An ACTIVE member of a company, as returned by
17
+ * `GET /membership/company/{companyUid}`. The server filters to
18
+ * `status: "active"` and enriches each row with resolved person metadata
19
+ * (`personEmail` / `personName` / `personSlug`) when available.
20
+ */
21
+ export interface ActiveMember {
22
+ membershipKey: string;
23
+ personUid: string;
24
+ companyUid: string;
25
+ role: string;
26
+ status: string;
27
+ personEmail?: string;
28
+ personName?: string;
29
+ personSlug?: string;
30
+ }
15
31
  export interface InviteOptions {
16
32
  target: string;
17
33
  role: string;
@@ -114,6 +130,7 @@ export declare class InviteHttpError extends Error {
114
130
  }
115
131
  export declare function formatInviteHttpError(status: number, fallback: string, code?: string): string;
116
132
  export declare function listPendingInvites(token: string, companyUid: string): Promise<PendingInvite[]>;
133
+ export declare function listActiveMembers(token: string, companyUid: string): Promise<ActiveMember[]>;
117
134
  /**
118
135
  * Resolve a `revoke` CLI argument into the canonical `membershipKey` shape
119
136
  * the server requires. Accepts three input forms:
@@ -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]="40f5379c-742e-5cd5-aba2-931856dc7dd3")}catch(e){}}();
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]="8244d720-094e-5d4e-b813-75086ed0849d")}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";
@@ -196,6 +196,20 @@ export async function listPendingInvites(token, companyUid) {
196
196
  const data = (await res.json());
197
197
  return data?.pending ?? data?.invites ?? [];
198
198
  }
199
+ export async function listActiveMembers(token, companyUid) {
200
+ const res = await vaultApiFetch({
201
+ token,
202
+ path: `/membership/company/${encodeURIComponent(companyUid)}`,
203
+ });
204
+ if (!res.ok) {
205
+ const err = (await res.json().catch(() => ({})));
206
+ throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
207
+ }
208
+ // Server schema: `{ members: [...] }` — active members only, enriched with
209
+ // resolved person metadata (personEmail / personName / personSlug).
210
+ const data = (await res.json());
211
+ return data?.members ?? [];
212
+ }
199
213
  /**
200
214
  * Resolve a `revoke` CLI argument into the canonical `membershipKey` shape
201
215
  * the server requires. Accepts three input forms:
@@ -422,43 +436,66 @@ export function registerMembersCommand(program) {
422
436
  });
423
437
  members
424
438
  .command("list")
425
- .description("List pending invites for the company")
426
- .action(async () => {
439
+ .description("List the company's active members (use --pending for pending invites)")
440
+ .option("--pending", "List pending invites instead of active members")
441
+ .action(async (opts) => {
427
442
  try {
428
443
  const token = await ensureCognitoToken();
429
444
  const companySlug = members.opts().company;
430
445
  const companyUid = await getCompanyUid(token, companySlug);
431
- const invites = await listPendingInvites(token, companyUid);
432
- if (invites.length === 0) {
433
- console.log(chalk.gray("No pending invites for this company."));
446
+ if (opts.pending) {
447
+ // --pending: preserve the original pending-invites view verbatim.
448
+ const invites = await listPendingInvites(token, companyUid);
449
+ if (invites.length === 0) {
450
+ console.log(chalk.gray("No pending invites for this company."));
451
+ return;
452
+ }
453
+ const targetW = Math.max(6, ...invites.map((i) => (i.inviteeEmail ?? i.personUid ?? "").length));
454
+ const roleW = Math.max(4, ...invites.map((i) => i.role.length));
455
+ const byW = Math.max(10, ...invites.map((i) => i.invitedBy.length));
456
+ const keyW = Math.max(14, ...invites.map((i) => i.membershipKey.length));
457
+ console.log(chalk.bold([
458
+ "TARGET".padEnd(targetW),
459
+ "ROLE".padEnd(roleW),
460
+ "INVITED_BY".padEnd(byW),
461
+ "INVITED_AT",
462
+ "MEMBERSHIP_KEY".padEnd(keyW),
463
+ ].join(" ")));
464
+ for (const inv of invites) {
465
+ const target = inv.inviteeEmail ?? inv.personUid ?? "";
466
+ console.log([
467
+ target.padEnd(targetW),
468
+ inv.role.padEnd(roleW),
469
+ inv.invitedBy.padEnd(byW),
470
+ shortDate(inv.invitedAt),
471
+ inv.membershipKey.padEnd(keyW),
472
+ ].join(" "));
473
+ }
474
+ return;
475
+ }
476
+ // Default: list ACTIVE members so their emails drop straight into
477
+ // `hq secrets share <path> --with <email>`.
478
+ const activeMembers = await listActiveMembers(token, companyUid);
479
+ if (activeMembers.length === 0) {
480
+ console.log(chalk.gray("No active members found for this company."));
434
481
  return;
435
482
  }
436
- const targetW = Math.max(6, ...invites.map((i) => (i.inviteeEmail ?? i.personUid ?? "").length));
437
- const roleW = Math.max(4, ...invites.map((i) => i.role.length));
438
- const byW = Math.max(10, ...invites.map((i) => i.invitedBy.length));
439
- const keyW = Math.max(14, ...invites.map((i) => i.membershipKey.length));
440
- console.log(chalk.bold([
441
- "TARGET".padEnd(targetW),
442
- "ROLE".padEnd(roleW),
443
- "INVITED_BY".padEnd(byW),
444
- "INVITED_AT",
445
- "MEMBERSHIP_KEY".padEnd(keyW),
446
- ].join(" ")));
447
- for (const inv of invites) {
448
- const target = inv.inviteeEmail ?? inv.personUid ?? "";
449
- console.log([
450
- target.padEnd(targetW),
451
- inv.role.padEnd(roleW),
452
- inv.invitedBy.padEnd(byW),
453
- shortDate(inv.invitedAt),
454
- inv.membershipKey.padEnd(keyW),
455
- ].join(" "));
483
+ const emailW = Math.max(5, ...activeMembers.map((m) => (m.personEmail ?? m.personUid).length));
484
+ const roleW = Math.max(4, ...activeMembers.map((m) => m.role.length));
485
+ console.log(chalk.bold(["EMAIL".padEnd(emailW), "ROLE".padEnd(roleW), "NAME"].join(" ")));
486
+ for (const m of activeMembers) {
487
+ const email = m.personEmail ?? m.personUid;
488
+ const name = m.personName ?? m.personSlug ?? "";
489
+ console.log([email.padEnd(emailW), m.role.padEnd(roleW), name].join(" "));
456
490
  }
491
+ console.log(chalk.gray("Share a secret with a member: hq secrets share <path> --with <email>"));
457
492
  }
458
493
  catch (err) {
459
494
  if (err instanceof InviteHttpError) {
460
495
  const msg = err.status === 403
461
- ? "Not authorized — only admins and owners can list invites"
496
+ ? opts.pending
497
+ ? "Not authorized — only admins and owners can list invites"
498
+ : "Not authorized — only company members can list members"
462
499
  : formatInviteHttpError(err.status, err.message);
463
500
  console.error(chalk.red(msg));
464
501
  process.exit(1);
@@ -497,4 +534,4 @@ export function registerMembersCommand(program) {
497
534
  });
498
535
  }
499
536
  //# sourceMappingURL=members.js.map
500
- //# debugId=40f5379c-742e-5cd5-aba2-931856dc7dd3
537
+ //# debugId=8244d720-094e-5d4e-b813-75086ed0849d
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Builds the static create-time warning surfaced by both `create-company` and
3
+ * `dry-run`, because users may accidentally create duplicate companies instead
4
+ * of joining the existing company they were invited to.
5
+ */
6
+ export declare function buildCreateCompanyWarning(): string;
7
+ //# sourceMappingURL=onboard-warning.d.ts.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Builds the static create-time warning surfaced by both `create-company` and
3
+ * `dry-run`, because users may accidentally create duplicate companies instead
4
+ * of joining the existing company they were invited to.
5
+ */
6
+
7
+ !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]="853f43ae-d902-54b9-9279-8a4b1d5f7693")}catch(e){}}();
8
+ export function buildCreateCompanyWarning() {
9
+ return (" This creates a NEW company that you own — it does NOT join an existing one.\n" +
10
+ " If your company already uses HQ, do NOT create it again: ask your admin or owner to send you an invite, then accept it and run `hq sync` to pull the existing company.\n" +
11
+ " Creating a same-named company makes a separate one you'd be alone in.\n");
12
+ }
13
+ //# sourceMappingURL=onboard-warning.js.map
14
+ //# debugId=853f43ae-d902-54b9-9279-8a4b1d5f7693
@@ -19,13 +19,14 @@
19
19
  * browser-OAuth flow opens automatically.
20
20
  */
21
21
 
22
- !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]="05a49c9b-23bc-5b96-b5e1-cc2e573b11c3")}catch(e){}}();
22
+ !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]="235a4623-0834-5428-957c-0c8bbe96d089")}catch(e){}}();
23
23
  import chalk from "chalk";
24
24
  import { runOnboardCli, readCheckpoint } from "@indigoai-us/hq-onboarding";
25
25
  import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
26
26
  import { createDefaultVaultClient } from "./cloud-provision.js";
27
27
  import { detectOnboardingIdentityMismatch } from "./onboard-identity-guard.js";
28
28
  import { planOnboardJoin } from "./onboard-join.js";
29
+ import { buildCreateCompanyWarning } from "./onboard-warning.js";
29
30
  // ---------------------------------------------------------------------------
30
31
  // Command registration
31
32
  // ---------------------------------------------------------------------------
@@ -49,9 +50,7 @@ export function registerOnboardCommand(program) {
49
50
  console.log(` Company: ${options.name} (${options.slug})`);
50
51
  console.log(` Person: ${options.personName} <${options.email}>`);
51
52
  console.log(` HQ root: ${options.hqRoot}\n`);
52
- console.log(chalk.gray(" This creates a NEW company you own. Joining a teammate's existing\n" +
53
- " company? Stop — accept your invite, then run `hq sync` to pull it.\n" +
54
- " Creating a same-named company leaves you alone in a separate one.\n"));
53
+ console.log(chalk.gray(buildCreateCompanyWarning()));
55
54
  const accessToken = await ensureCognitoToken();
56
55
  const result = await runOnboardCli({
57
56
  mode: "create-company",
@@ -179,6 +178,7 @@ export function registerOnboardCommand(program) {
179
178
  .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
180
179
  .action(async (options) => {
181
180
  try {
181
+ console.log(chalk.gray(buildCreateCompanyWarning()));
182
182
  // No auth needed for dry-run — runOnboardCli handles this branch
183
183
  // without touching the vault-service.
184
184
  const result = await runOnboardCli({
@@ -198,4 +198,4 @@ export function registerOnboardCommand(program) {
198
198
  });
199
199
  }
200
200
  //# sourceMappingURL=onboard.js.map
201
- //# debugId=05a49c9b-23bc-5b96-b5e1-cc2e573b11c3
201
+ //# debugId=235a4623-0834-5428-957c-0c8bbe96d089
@@ -259,6 +259,18 @@ export declare function verifyArtifact(input: VerifyArtifactInput): void;
259
259
  */
260
260
  export declare function validateMcpManifest(payloadDir: string, item: string): void;
261
261
  export declare function validateManifest(payloadDir: string, hqVersion: string | null): PackManifest;
262
+ /**
263
+ * Enforce a pack's `requires.packs`: every named dependency MUST already be
264
+ * installed (and satisfy its optional semver RANGE) before we write anything.
265
+ *
266
+ * Installed packs are discovered by FILESYSTEM PRESENCE via `listInstalledPacks`
267
+ * — deliberately NOT `modules.yaml`, which `installPack` no longer writes under
268
+ * the v12+ layout (a modules.yaml-based check would silently ignore every modern
269
+ * pack). Throws on the first unmet dependency so the install aborts with NO
270
+ * partial state (it is called before installToPackages). No-op when
271
+ * `requires.packs` is absent/empty, keeping legacy packs unaffected.
272
+ */
273
+ export declare function assertPackDependencies(hqRoot: string, pkg: PackManifest): void;
262
274
  /**
263
275
  * Derive the safe, auto-generated "get started" line for a freshly installed
264
276
  * pack from its `initialization.entrypoint` ONLY. PHASE 1 deliberately ignores
@@ -34,7 +34,7 @@
34
34
  * from each pack's package.yaml; rationale lives in the layout-fix PR.)
35
35
  */
36
36
 
37
- !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]="fbfbcf69-d410-51cb-ae59-886c451b3a55")}catch(e){}}();
37
+ !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]="0ec07d3a-ee86-51fd-adbd-2608c620ae99")}catch(e){}}();
38
38
  import * as fs from 'fs';
39
39
  import * as os from 'os';
40
40
  import * as path from 'path';
@@ -48,7 +48,7 @@ import semverValid from 'semver/functions/valid.js';
48
48
  import semverValidRange from 'semver/ranges/valid.js';
49
49
  import semverGt from 'semver/functions/gt.js';
50
50
  import { findHqRoot } from '../utils/manifest.js';
51
- import { readHqVersion, routeContribution } from '../utils/pack-contributions.js';
51
+ import { readHqVersion, routeContribution, listInstalledPacks } from '../utils/pack-contributions.js';
52
52
  import { CONTRIBUTION_TABLE, payloadFor } from '../utils/contribution-table.js';
53
53
  import { safeExtractTarball } from './safe-extract.js';
54
54
  import { vaultApiFetchPublic } from '../utils/vault-api.js';
@@ -907,6 +907,32 @@ export function validateManifest(payloadDir, hqVersion) {
907
907
  if (hqVersion && !semverSatisfies(hqVersion, range, { includePrerelease: true })) {
908
908
  throw new Error(`Host hqCore ${hqVersion} does not satisfy pack requirement ${range}`);
909
909
  }
910
+ // 6b. requires.packs (M0) — OPTIONAL pack-to-pack dependencies. Absent → legacy
911
+ // behavior (hqCore is the only prerequisite). Present → a list of
912
+ // { name, version? } where `name` is a valid hq-pack name and `version` (if
913
+ // given) is a valid semver RANGE. SHAPE validation only (no filesystem) so a
914
+ // malformed dependency can't masquerade as valid; whether the named packs are
915
+ // actually INSTALLED is enforced at install time by assertPackDependencies
916
+ // (which needs hqRoot — a pure manifest validator doesn't have it).
917
+ const reqPacks = m.requires?.packs;
918
+ if (reqPacks !== undefined) {
919
+ if (!Array.isArray(reqPacks)) {
920
+ throw new Error('requires.packs must be a list of { name, version? } entries');
921
+ }
922
+ for (const dep of reqPacks) {
923
+ if (!dep || typeof dep !== 'object' || Array.isArray(dep)) {
924
+ throw new Error('requires.packs entries must be mappings with a name (and optional version)');
925
+ }
926
+ const d = dep;
927
+ if (typeof d.name !== 'string' || !/^hq-pack-[a-z0-9][a-z0-9-]*$/.test(d.name)) {
928
+ throw new Error(`requires.packs[].name "${d.name}" must match ^hq-pack-[a-z0-9][a-z0-9-]*$`);
929
+ }
930
+ if (d.version !== undefined &&
931
+ (typeof d.version !== 'string' || !semverValidRange(d.version))) {
932
+ throw new Error(`requires.packs entry for "${d.name}" has an invalid version range "${d.version}"`);
933
+ }
934
+ }
935
+ }
910
936
  // 7. contributes has at least one non-empty subfield
911
937
  const contributes = (m.contributes ?? {});
912
938
  const nonEmpty = Object.values(contributes).some((v) => Array.isArray(v) && v.length > 0);
@@ -1007,6 +1033,46 @@ export function validateManifest(payloadDir, hqVersion) {
1007
1033
  return m;
1008
1034
  }
1009
1035
  // ---------------------------------------------------------------------------
1036
+ // Pack-to-pack dependency pre-flight (M0)
1037
+ // ---------------------------------------------------------------------------
1038
+ /**
1039
+ * Enforce a pack's `requires.packs`: every named dependency MUST already be
1040
+ * installed (and satisfy its optional semver RANGE) before we write anything.
1041
+ *
1042
+ * Installed packs are discovered by FILESYSTEM PRESENCE via `listInstalledPacks`
1043
+ * — deliberately NOT `modules.yaml`, which `installPack` no longer writes under
1044
+ * the v12+ layout (a modules.yaml-based check would silently ignore every modern
1045
+ * pack). Throws on the first unmet dependency so the install aborts with NO
1046
+ * partial state (it is called before installToPackages). No-op when
1047
+ * `requires.packs` is absent/empty, keeping legacy packs unaffected.
1048
+ */
1049
+ export function assertPackDependencies(hqRoot, pkg) {
1050
+ const deps = pkg.requires?.packs ?? [];
1051
+ if (deps.length === 0)
1052
+ return;
1053
+ const installed = new Map();
1054
+ for (const p of listInstalledPacks(hqRoot)) {
1055
+ // Key on the manifest name when readable, else the directory name.
1056
+ installed.set(p.manifest?.name ?? p.name, p.manifest?.version);
1057
+ }
1058
+ for (const dep of deps) {
1059
+ if (dep.name === pkg.name) {
1060
+ throw new Error(`Pack ${pkg.name} cannot list itself in requires.packs.`);
1061
+ }
1062
+ if (!installed.has(dep.name)) {
1063
+ throw new Error(`Pack ${pkg.name} requires ${dep.name}, which is not installed. ` +
1064
+ `Install it first, e.g.: hq install marketplace:${dep.name}`);
1065
+ }
1066
+ if (dep.version) {
1067
+ const have = installed.get(dep.name);
1068
+ if (!have || !semverSatisfies(have, dep.version, { includePrerelease: true })) {
1069
+ throw new Error(`Pack ${pkg.name} requires ${dep.name} ${dep.version}, but ` +
1070
+ `${dep.name}${have ? ` ${have}` : ' (version unknown)'} is installed.`);
1071
+ }
1072
+ }
1073
+ }
1074
+ }
1075
+ // ---------------------------------------------------------------------------
1010
1076
  // Post-install get-started line (US-005)
1011
1077
  // ---------------------------------------------------------------------------
1012
1078
  /**
@@ -1482,6 +1548,11 @@ export async function installPack(source, opts = {}) {
1482
1548
  break;
1483
1549
  }
1484
1550
  const pkg = validateManifest(fetched.payloadDir, hqVersion);
1551
+ // M0 — pack-to-pack dependency pre-flight. Runs BEFORE any prompts or writes
1552
+ // so a missing/unsatisfied `requires.packs` aborts with no partial state, and
1553
+ // before we bother the operator with hook/MCP trust prompts for a pack that
1554
+ // can't install anyway.
1555
+ assertPackDependencies(hqRoot, pkg);
1485
1556
  if (pkg.conditional) {
1486
1557
  const allowed = await confirmConditional(pkg, opts.allowHooks ?? false);
1487
1558
  if (!allowed) {
@@ -1558,4 +1629,4 @@ export async function installPack(source, opts = {}) {
1558
1629
  }
1559
1630
  }
1560
1631
  //# sourceMappingURL=pack-install.js.map
1561
- //# debugId=fbfbcf69-d410-51cb-ae59-886c451b3a55
1632
+ //# debugId=0ec07d3a-ee86-51fd-adbd-2608c620ae99
@@ -18,7 +18,7 @@
18
18
  * Spec: knowledge/public/hq-core/package-yaml-spec.md.
19
19
  */
20
20
 
21
- !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]="570f7491-8d05-577c-8bae-6f6cf7dfb2ec")}catch(e){}}();
21
+ !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]="69746edd-f9d8-528e-8902-de799c58bd8a")}catch(e){}}();
22
22
  import * as fs from 'fs';
23
23
  import * as path from 'path';
24
24
  import * as readline from 'readline';
@@ -27,7 +27,7 @@ import chalk from 'chalk';
27
27
  import semverSatisfies from 'semver/functions/satisfies.js';
28
28
  import { findHqRoot } from '../utils/manifest.js';
29
29
  import { classify, resolveLatest, resolveLatestMarketplace, runScanPackages, installPack, } from './pack-install.js';
30
- import { contributionLinks, linkStatus, listInstalledPacks, readPackManifest, unwirePack, unwirePackMcp, readHqVersion, readRecommendedPackages, packagesDir, } from '../utils/pack-contributions.js';
30
+ import { contributionLinks, linkStatus, listInstalledPacks, findDependentPacks, readPackManifest, unwirePack, unwirePackMcp, readHqVersion, readRecommendedPackages, packagesDir, } from '../utils/pack-contributions.js';
31
31
  function resolveRoot(opts) {
32
32
  return opts.hqRoot ? path.resolve(opts.hqRoot) : findHqRoot();
33
33
  }
@@ -278,6 +278,19 @@ async function runUninstall(name, opts) {
278
278
  if (!manifest) {
279
279
  warnings.push('package.yaml unreadable -- host symlinks could not be computed precisely; ran a re-scan to reconcile.');
280
280
  }
281
+ // 0. Dependents guard (M0): refuse to remove a pack that another installed pack
282
+ // lists in its `requires.packs`, unless --force. Filesystem presence is the
283
+ // source of truth (listInstalledPacks), consistent with the install-time
284
+ // assertPackDependencies check. Runs BEFORE any un-wiring so a blocked uninstall
285
+ // leaves the pack fully intact.
286
+ if (!opts.force) {
287
+ const dependents = findDependentPacks(listInstalledPacks(hqRoot), name);
288
+ if (dependents.length > 0) {
289
+ const who = dependents.map((p) => p.manifest?.name ?? p.name).join(', ');
290
+ throw new Error(`Cannot uninstall "${name}": required by ${who}. ` +
291
+ `Uninstall the dependent pack(s) first, or pass --force to override.`);
292
+ }
293
+ }
281
294
  // 1. Un-wire only our symlinks.
282
295
  const { unlinked, skipped } = unwirePack(hqRoot, packDir, contributes);
283
296
  // 1b. Un-register the pack's MCP (`wire: 'merge'`) servers — invisible to the
@@ -417,6 +430,7 @@ export function registerPacksCommand(parent) {
417
430
  .option('--hq-root <path>', 'HQ root (default: auto-detect)')
418
431
  .option('-y, --yes', 'Skip confirmation')
419
432
  .option('--no-archive', 'Delete instead of archiving')
433
+ .option('--force', 'Uninstall even if other installed packs require this one')
420
434
  .action(async (name, opts) => {
421
435
  try {
422
436
  if (!opts.yes) {
@@ -449,4 +463,4 @@ export function registerPacksCommand(parent) {
449
463
  });
450
464
  }
451
465
  //# sourceMappingURL=packs.js.map
452
- //# debugId=570f7491-8d05-577c-8bae-6f6cf7dfb2ec
466
+ //# debugId=69746edd-f9d8-528e-8902-de799c58bd8a
@@ -11,6 +11,16 @@
11
11
  * named by `--company`); nothing reads across company boundaries.
12
12
  */
13
13
  import { Command } from "commander";
14
+ import { resolveNameToEmail, type PersonRecord } from "../utils/people.js";
15
+ export type RefreshPeopleRoster = (hqRoot: string, companySlug: string) => Promise<void>;
16
+ interface PeopleCommandDeps {
17
+ refreshRoster?: RefreshPeopleRoster;
18
+ }
19
+ interface PeopleLookupOpts {
20
+ localOnly?: boolean;
21
+ json?: boolean;
22
+ }
23
+ export declare function refreshPeopleRosterFromCloud(hqRoot: string, companySlug: string): Promise<void>;
14
24
  /**
15
25
  * Resolve the single company to operate on. Explicit `--company` always wins
16
26
  * (after a path-safety check). Otherwise the active company is inferred from
@@ -18,5 +28,20 @@ import { Command } from "commander";
18
28
  * do, the caller must disambiguate with `--company`.
19
29
  */
20
30
  export declare function resolveCompanySlug(hqRoot: string, explicit: string | undefined): string;
21
- export declare function registerPeopleCommand(program: Command): void;
31
+ export declare function resolvePersonWithRosterFallback(input: {
32
+ hqRoot: string;
33
+ slug: string;
34
+ name: string;
35
+ opts?: PeopleLookupOpts;
36
+ refreshRoster?: RefreshPeopleRoster;
37
+ }): Promise<ReturnType<typeof resolveNameToEmail>>;
38
+ export declare function searchPeopleWithRosterFallback(input: {
39
+ hqRoot: string;
40
+ slug: string;
41
+ keyword: string;
42
+ opts?: PeopleLookupOpts;
43
+ refreshRoster?: RefreshPeopleRoster;
44
+ }): Promise<PersonRecord[]>;
45
+ export declare function registerPeopleCommand(program: Command, deps?: PeopleCommandDeps): void;
46
+ export {};
22
47
  //# sourceMappingURL=people.d.ts.map
@@ -11,14 +11,31 @@
11
11
  * named by `--company`); nothing reads across company boundaries.
12
12
  */
13
13
 
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]="076b7f64-6626-5733-8992-ab9361a3e31d")}catch(e){}}();
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]="79abc1f5-4f5a-5c95-b29b-10f04417355f")}catch(e){}}();
15
15
  import * as fs from "fs";
16
16
  import { Option } from "commander";
17
17
  import chalk from "chalk";
18
+ import { VaultClient } from "@indigoai-us/hq-cloud";
18
19
  import * as yaml from "js-yaml";
19
20
  import { findHqRoot } from "../utils/manifest.js";
20
21
  import { manifestPath } from "./cloud-provision.js";
22
+ import { DEFAULT_COGNITO, buildVaultConfig, ensureCognitoToken, } from "../utils/cognito-session.js";
23
+ import { getCompanyUid } from "../utils/vault-api.js";
24
+ import { createCompanyPresignClient, runGet } from "./files-browse.js";
21
25
  import { assertSafeCompanySlug, listCompanyPeople, searchPeople, resolveNameToEmail, companyPeopleDir, } from "../utils/people.js";
26
+ export async function refreshPeopleRosterFromCloud(hqRoot, companySlug) {
27
+ const accessToken = await ensureCognitoToken();
28
+ const client = new VaultClient(buildVaultConfig(accessToken));
29
+ await getCompanyUid(accessToken, companySlug);
30
+ await runGet({
31
+ path: `companies/${companySlug}/people/`,
32
+ hqRoot,
33
+ companySlug,
34
+ vaultClient: client,
35
+ companyClient: ({ companyUid }) => createCompanyPresignClient({ token: accessToken, companyUid }),
36
+ region: DEFAULT_COGNITO.region,
37
+ });
38
+ }
22
39
  /** Companies that still exist (anything not explicitly `status: archived`). */
23
40
  function activeCompanySlugs(manifest) {
24
41
  const companies = manifest.companies ?? {};
@@ -86,7 +103,39 @@ function fail(message) {
86
103
  console.error(chalk.red(message));
87
104
  process.exit(1);
88
105
  }
89
- export function registerPeopleCommand(program) {
106
+ function logRefreshFailure(companySlug, err) {
107
+ const message = err instanceof Error ? err.message : String(err);
108
+ console.error(chalk.dim(` Could not refresh people roster for '${companySlug}': ${message}`));
109
+ }
110
+ async function tryRefreshRoster(refreshRoster, hqRoot, slug) {
111
+ try {
112
+ await refreshRoster(hqRoot, slug);
113
+ return true;
114
+ }
115
+ catch (err) {
116
+ logRefreshFailure(slug, err);
117
+ return false;
118
+ }
119
+ }
120
+ export async function resolvePersonWithRosterFallback(input) {
121
+ const local = resolveNameToEmail(listCompanyPeople(input.hqRoot, input.slug), input.name);
122
+ if (local.status !== "not_found" || input.opts?.localOnly)
123
+ return local;
124
+ const refreshed = await tryRefreshRoster(input.refreshRoster ?? refreshPeopleRosterFromCloud, input.hqRoot, input.slug);
125
+ if (!refreshed)
126
+ return local;
127
+ return resolveNameToEmail(listCompanyPeople(input.hqRoot, input.slug), input.name);
128
+ }
129
+ export async function searchPeopleWithRosterFallback(input) {
130
+ const local = searchPeople(listCompanyPeople(input.hqRoot, input.slug), input.keyword);
131
+ if (local.length > 0 || input.opts?.localOnly)
132
+ return local;
133
+ const refreshed = await tryRefreshRoster(input.refreshRoster ?? refreshPeopleRosterFromCloud, input.hqRoot, input.slug);
134
+ if (!refreshed)
135
+ return local;
136
+ return searchPeople(listCompanyPeople(input.hqRoot, input.slug), input.keyword);
137
+ }
138
+ export function registerPeopleCommand(program, deps = {}) {
90
139
  const people = program
91
140
  .command("people")
92
141
  .description("List, search, and resolve a company's people (from companies/<co>/people)")
@@ -122,12 +171,19 @@ export function registerPeopleCommand(program) {
122
171
  .command("search <keyword>")
123
172
  .description("Keyword search over people names and emails")
124
173
  .option("--json", "Output JSON instead of a table")
125
- .action((keyword, opts) => {
174
+ .option("--local-only", "Skip cloud fallback; search only the local people roster")
175
+ .action(async (keyword, opts) => {
126
176
  try {
127
177
  const scope = people.opts();
128
178
  const hqRoot = resolveHqRoot(scope);
129
179
  const slug = resolveCompanySlug(hqRoot, scope.company);
130
- const matches = searchPeople(listCompanyPeople(hqRoot, slug), keyword);
180
+ const matches = await searchPeopleWithRosterFallback({
181
+ hqRoot,
182
+ slug,
183
+ keyword,
184
+ opts,
185
+ refreshRoster: deps.refreshRoster,
186
+ });
131
187
  if (opts.json) {
132
188
  console.log(JSON.stringify(matches, null, 2));
133
189
  return;
@@ -146,12 +202,19 @@ export function registerPeopleCommand(program) {
146
202
  .command("resolve <name>")
147
203
  .description("Resolve a person name to their email address")
148
204
  .option("--json", "Output JSON instead of plain text")
149
- .action((name, opts) => {
205
+ .option("--local-only", "Skip cloud fallback; resolve only from the local people roster")
206
+ .action(async (name, opts) => {
150
207
  try {
151
208
  const scope = people.opts();
152
209
  const hqRoot = resolveHqRoot(scope);
153
210
  const slug = resolveCompanySlug(hqRoot, scope.company);
154
- const result = resolveNameToEmail(listCompanyPeople(hqRoot, slug), name);
211
+ const result = await resolvePersonWithRosterFallback({
212
+ hqRoot,
213
+ slug,
214
+ name,
215
+ opts,
216
+ refreshRoster: deps.refreshRoster,
217
+ });
155
218
  if (opts.json) {
156
219
  console.log(JSON.stringify(result, null, 2));
157
220
  if (result.status === "found")
@@ -184,4 +247,4 @@ export function registerPeopleCommand(program) {
184
247
  });
185
248
  }
186
249
  //# sourceMappingURL=people.js.map
187
- //# debugId=076b7f64-6626-5733-8992-ab9361a3e31d
250
+ //# debugId=79abc1f5-4f5a-5c95-b29b-10f04417355f
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Pure helpers describing WHICH secrets scope a command acted on, so `set` and
3
+ * `list` can echo it. Users were setting a secret in one scope (personal vs a
4
+ * company, or company A vs B) and listing another, then seeing "no secrets" with
5
+ * no indication the scopes differed (feedback_70e059da).
6
+ */
7
+ export interface SecretsScopeRef {
8
+ /** True when --personal was used (caller's personal vault). */
9
+ personal: boolean;
10
+ /** The slug the user passed via --company, if any. */
11
+ companySlug?: string;
12
+ /** Resolved entity uid: prs_* for personal, cmp_* for a company. */
13
+ companyUid: string;
14
+ }
15
+ /** Human label for a secrets scope: "your personal vault" or "company <slug-or-uid>". */
16
+ export declare function describeSecretsScope(ref: SecretsScopeRef): string;
17
+ export declare function formatSecretSaved(name: string, scope: string): string;
18
+ export declare function formatSecretsListHeader(scope: string): string;
19
+ export declare function formatSecretsListEmpty(scope: string): string;
20
+ //# sourceMappingURL=secrets-scope.d.ts.map
@@ -0,0 +1,19 @@
1
+ /** Human label for a secrets scope: "your personal vault" or "company <slug-or-uid>". */
2
+
3
+ !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]="718913a3-f80f-5bff-928e-a7aa9f51dd0d")}catch(e){}}();
4
+ export function describeSecretsScope(ref) {
5
+ if (ref.personal)
6
+ return "your personal vault";
7
+ return `company ${ref.companySlug ?? ref.companyUid}`;
8
+ }
9
+ export function formatSecretSaved(name, scope) {
10
+ return `Secret '${name}' saved to ${scope}.`;
11
+ }
12
+ export function formatSecretsListHeader(scope) {
13
+ return `Secrets for ${scope}:`;
14
+ }
15
+ export function formatSecretsListEmpty(scope) {
16
+ return `No secrets found for ${scope}.`;
17
+ }
18
+ //# sourceMappingURL=secrets-scope.js.map
19
+ //# debugId=718913a3-f80f-5bff-928e-a7aa9f51dd0d