@indigoai-us/hq-cli 5.345.191 → 5.345.193

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 (122) hide show
  1. package/CHANGELOG.md +398 -0
  2. package/assets/bot-workers/setup/worker.yaml +91 -34
  3. package/dist/command-catalog.generated.d.ts +96 -19
  4. package/dist/command-catalog.generated.js +124 -21
  5. package/dist/commands/bot-jobs.d.ts +21 -18
  6. package/dist/commands/bot-jobs.js +127 -39
  7. package/dist/commands/bot.d.ts +4 -3
  8. package/dist/commands/bot.js +265 -140
  9. package/dist/commands/core-jobs-schedule.js +10 -0
  10. package/dist/commands/daemon.js +37 -0
  11. package/dist/commands/files.d.ts +41 -6
  12. package/dist/commands/files.js +108 -27
  13. package/dist/commands/pack-start-bot.d.ts +4 -6
  14. package/dist/commands/pack-start-bot.js +4 -8
  15. package/dist/commands/signals-run.d.ts +45 -0
  16. package/dist/commands/signals-run.js +175 -0
  17. package/dist/commands/signals.d.ts +1 -0
  18. package/dist/commands/signals.js +3 -0
  19. package/dist/commands/sources.d.ts +11 -0
  20. package/dist/commands/sources.js +137 -25
  21. package/dist/commands/staff/reset-test-account.d.ts +10 -0
  22. package/dist/commands/staff/reset-test-account.js +18 -2
  23. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +31171 -52512
  24. package/dist/hq-cloud-sync-runner/package.json +1 -1
  25. package/dist/lib/bot/api.d.ts +3 -2
  26. package/dist/lib/bot/config.d.ts +2 -0
  27. package/dist/lib/bot/fleet-cron.d.ts +121 -0
  28. package/dist/lib/bot/fleet-cron.js +319 -0
  29. package/dist/lib/bot/fleet-local.d.ts +93 -1
  30. package/dist/lib/bot/fleet-local.js +546 -34
  31. package/dist/lib/bot/fleet-owner-companies.d.ts +12 -0
  32. package/dist/lib/bot/fleet-owner-companies.js +30 -0
  33. package/dist/lib/bot/fleet-watch.d.ts +25 -0
  34. package/dist/lib/bot/fleet-watch.js +51 -0
  35. package/dist/lib/bot/index.d.ts +1 -1
  36. package/dist/lib/bot/index.js +1 -1
  37. package/dist/lib/bot/job-run.d.ts +5 -4
  38. package/dist/lib/bot/job-run.js +5 -4
  39. package/dist/lib/bot/jobs/hot-signal-sync.d.ts +57 -3
  40. package/dist/lib/bot/jobs/hot-signal-sync.js +163 -4
  41. package/dist/lib/bot/local-jobs.d.ts +13 -2
  42. package/dist/lib/bot/local-jobs.js +17 -4
  43. package/dist/lib/bot/progress.d.ts +14 -0
  44. package/dist/lib/bot/progress.js +47 -1
  45. package/dist/lib/bot/run.d.ts +55 -2
  46. package/dist/lib/bot/run.js +192 -10
  47. package/dist/lib/bot/setup-tool-offer.d.ts +1 -0
  48. package/dist/lib/bot/setup-tool-offer.js +16 -0
  49. package/dist/lib/bot/signals-engine-flag.d.ts +18 -0
  50. package/dist/lib/bot/signals-engine-flag.js +30 -0
  51. package/dist/lib/daemon/catalog.d.ts +4 -0
  52. package/dist/lib/daemon/catalog.js +29 -9
  53. package/dist/lib/daemon/legacy.d.ts +6 -0
  54. package/dist/lib/daemon/legacy.js +16 -10
  55. package/dist/lib/daemon/run.d.ts +1 -0
  56. package/dist/lib/daemon/run.js +10 -0
  57. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  58. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  59. package/dist/lib/flag-registry.d.ts +13 -1
  60. package/dist/lib/flag-registry.js +46 -0
  61. package/dist/lib/hot-channels/adapters/attio.d.ts +75 -2
  62. package/dist/lib/hot-channels/adapters/attio.js +367 -29
  63. package/dist/lib/hot-channels/adapters/gmail.d.ts +14 -0
  64. package/dist/lib/hot-channels/adapters/gmail.js +59 -2
  65. package/dist/lib/hot-channels/adapters/linear.d.ts +24 -0
  66. package/dist/lib/hot-channels/adapters/linear.js +71 -1
  67. package/dist/lib/hot-channels/adapters/meetings.js +4 -1
  68. package/dist/lib/hot-channels/adapters/slack.js +7 -3
  69. package/dist/lib/hot-channels/alert.js +1 -1
  70. package/dist/lib/hot-channels/contract.d.ts +11 -0
  71. package/dist/lib/hot-channels/contract.js +1 -0
  72. package/dist/lib/hot-channels/cursors.js +1 -1
  73. package/dist/lib/hot-channels/landing.d.ts +6 -2
  74. package/dist/lib/hot-channels/landing.js +13 -6
  75. package/dist/lib/hot-channels/ledger.js +2 -0
  76. package/dist/lib/hot-channels/loop.js +18 -4
  77. package/dist/lib/hot-channels/registry.js +9 -1
  78. package/dist/lib/hot-channels/run.d.ts +18 -0
  79. package/dist/lib/hot-channels/run.js +15 -4
  80. package/dist/lib/hot-channels/signals-pass.d.ts +1 -0
  81. package/dist/lib/hot-channels/signals-pass.js +7 -5
  82. package/dist/lib/hot-channels/sync-lock.js +22 -6
  83. package/dist/lib/mesh/live/daemon/install.d.ts +22 -0
  84. package/dist/lib/mesh/live/daemon/install.js +117 -30
  85. package/dist/lib/signals/adapters-catalog.d.ts +77 -0
  86. package/dist/lib/signals/adapters-catalog.js +194 -0
  87. package/dist/lib/signals/engine.d.ts +196 -0
  88. package/dist/lib/signals/engine.js +495 -0
  89. package/dist/lib/signals/failure-alert.d.ts +89 -0
  90. package/dist/lib/signals/failure-alert.js +175 -0
  91. package/dist/lib/signals/local-date.d.ts +3 -0
  92. package/dist/lib/signals/local-date.js +6 -0
  93. package/dist/lib/signals/projection.d.ts +42 -0
  94. package/dist/lib/signals/projection.js +67 -0
  95. package/dist/lib/signals/reply.d.ts +112 -0
  96. package/dist/lib/signals/reply.js +368 -0
  97. package/dist/lib/signals/run-lock.d.ts +36 -0
  98. package/dist/lib/signals/run-lock.js +148 -0
  99. package/dist/lib/signals/run-record.d.ts +24 -0
  100. package/dist/lib/signals/run-record.js +82 -0
  101. package/dist/lib/signals/sanitize.d.ts +105 -0
  102. package/dist/lib/signals/sanitize.js +439 -0
  103. package/dist/lib/signals/settings.d.ts +10 -0
  104. package/dist/lib/signals/settings.js +48 -0
  105. package/dist/lib/signals/steps/adapters.d.ts +14 -0
  106. package/dist/lib/signals/steps/adapters.js +156 -0
  107. package/dist/lib/signals/steps/entity-similarity.d.ts +10 -0
  108. package/dist/lib/signals/steps/entity-similarity.js +47 -0
  109. package/dist/lib/signals/steps/extract.d.ts +113 -0
  110. package/dist/lib/signals/steps/extract.js +765 -0
  111. package/dist/lib/signals/steps/garden.d.ts +24 -0
  112. package/dist/lib/signals/steps/garden.js +250 -0
  113. package/dist/lib/signals/steps/types.d.ts +112 -0
  114. package/dist/lib/signals/steps/types.js +4 -0
  115. package/dist/lib/signals/steps/upload.d.ts +43 -0
  116. package/dist/lib/signals/steps/upload.js +324 -0
  117. package/dist/lib/signals/write-guard.d.ts +63 -0
  118. package/dist/lib/signals/write-guard.js +287 -0
  119. package/dist/utils/resident-package-preload.js +16 -0
  120. package/package.json +9 -9
  121. package/dist/lib/bot/fleet-default-flag.d.ts +0 -31
  122. package/dist/lib/bot/fleet-default-flag.js +0 -127
@@ -38,12 +38,15 @@ import { createClaudeSignalsPass } from "../lib/hot-channels/signals-pass.js";
38
38
  import { assertSafeSegment } from "../lib/hot-channels/landing.js";
39
39
  import { acquireSyncLock, SYNC_LOCK_BUSY_REASON } from "../lib/hot-channels/sync-lock.js";
40
40
  import { createBuiltinHotChannelRegistry } from "../lib/hot-channels/adapters/index.js";
41
+ import { companyAdapterFlagReader, gateAdapters, readSignalsAdaptersCatalog, } from "../lib/signals/adapters-catalog.js";
41
42
  import { createSelectionLoader } from "../lib/hot-channels/selection.js";
42
43
  import { createPersonVaultReader } from "../lib/hot-channels/person-vault-reader.js";
43
44
  import { S3Client } from "@aws-sdk/client-s3";
44
45
  import { resolveCanonicalPersonUid } from "./cloud.js";
45
46
  import { botsRoot as defaultBotsRoot } from "../lib/bot/paths.js";
46
47
  import { ensureHotSignalSyncJob } from "../lib/bot/jobs/hot-signal-sync.js";
48
+ import { companyOfHotSignalSyncJob, fleetCronOps } from "../lib/bot/fleet-cron.js";
49
+ import { HQ_FLEET_RUNTIME_ENGINE_ID, readBotConfig } from "../lib/bot/config.js";
47
50
  import { findHotSignalSyncJobs, lastRunsPerAdapter, parseDailyAt, renderScheduleReport, rescheduleDaily, toJobView, } from "../lib/hot-channels/schedule.js";
48
51
  import { listActiveMembers } from "./members.js";
49
52
  import { runOnDemandSync, } from "../lib/hot-channels/run.js";
@@ -485,32 +488,78 @@ async function skippedReport(input, vaultRoot, personKey, personUid, now) {
485
488
  runId: line.runId,
486
489
  signInRequired: false,
487
490
  skipped: true,
491
+ personKey,
488
492
  outcomes: [],
489
493
  entries: [line],
490
494
  notices: [`Another hot-channel sync for ${input.company} is already running, so this one was skipped.`],
491
495
  exitCode: 0,
492
496
  };
493
497
  }
498
+ /**
499
+ * One `gated` outcome per adapter the local gate turned off (yaml or company
500
+ * flag). With `--adapter`, only that adapter's. Nothing is fetched for them.
501
+ */
502
+ function gatedAdapterOutcomes(registry, gating, wanted) {
503
+ const want = wanted?.toLowerCase();
504
+ const out = [];
505
+ for (const adapter of registry.list()) {
506
+ if (!(adapter.id in gating.skips))
507
+ continue;
508
+ const provider = adapter.tokenScope.provider || adapter.id;
509
+ if (want !== undefined && adapter.id !== want && provider.toLowerCase() !== want)
510
+ continue;
511
+ const flag = gating.offFlags[adapter.id];
512
+ out.push({
513
+ adapter: adapter.id,
514
+ provider,
515
+ connectionId: null,
516
+ appName: null,
517
+ status: "gated",
518
+ landed: 0,
519
+ duplicates: 0,
520
+ message: flag
521
+ ? `${adapter.id} is not enabled for this company (flag ${flag} is off)`
522
+ : `${adapter.id} is turned off in core/settings/signals-adapters.yaml`,
523
+ });
524
+ }
525
+ return out;
526
+ }
494
527
  async function runLockedCompanySync(input, vaultRoot, personKey, author, session, lockHeld) {
495
528
  const deps = gatewaySyncDeps(session.token, session.companyUid);
496
- // A fresh loader per run: selections are cached for this run only.
497
- const selections = input.createSelections ? input.createSelections() : defaultSelections();
498
- const report = await runOnDemandSync({
499
- ...(selections ? { selections } : {}),
500
- deps,
501
- registry: input.registry ??
502
- createBuiltinHotChannelRegistry({ vaultRoot, identities: await meetingIdentities(input, session, author.email) }),
503
- vaultRoot,
504
- personUid: author.userSub,
505
- personKey,
506
- ...(input.adapter !== undefined ? { adapter: input.adapter } : {}),
507
- ...(input.now ? { now: input.now } : {}),
508
- ...(input.trigger ? { trigger: input.trigger } : {}),
509
- ...(input.catchUp === undefined ? {} : { catchUp: input.catchUp }),
510
- ...(input.firstRun ? { firstRun: true } : {}),
511
- ...(input.signalsGenerated ? { signalsGenerated: input.signalsGenerated } : {}),
512
- beforeWrite: lockHeld,
513
- });
529
+ const registry = input.registry ??
530
+ createBuiltinHotChannelRegistry({ vaultRoot, identities: await meetingIdentities(input, session, author.email) });
531
+ // The adapter gate for every caller: signals-adapters.yaml plus the company
532
+ // flags, read through hq-flags.
533
+ const gating = await gateAdapters(registry.list().map((a) => a.id), readSignalsAdaptersCatalog(input.hqRoot), input.isFlagOn ??
534
+ companyAdapterFlagReader(session.companyUid, session.token, { hqRoot: input.hqRoot, company: input.company }));
535
+ const gated = gatedAdapterOutcomes(registry, gating, input.adapter);
536
+ const onlyAdapters = (input.onlyAdapters ?? gating.enabled).filter((id) => gating.enabled.includes(id));
537
+ let report;
538
+ if (input.adapter !== undefined && gated.length > 0) {
539
+ // `--adapter` named an adapter the gate turned off: there is nothing to run.
540
+ report = { runId: null, signInRequired: false, personKey, outcomes: [], entries: [], notices: [], exitCode: 0 };
541
+ }
542
+ else {
543
+ // A fresh loader per run: selections are cached for this run only.
544
+ const selections = input.createSelections ? input.createSelections() : defaultSelections();
545
+ report = await runOnDemandSync({
546
+ ...(selections ? { selections } : {}),
547
+ deps,
548
+ registry,
549
+ vaultRoot,
550
+ personUid: author.userSub,
551
+ personKey,
552
+ ...(input.adapter !== undefined ? { adapter: input.adapter } : {}),
553
+ ...(input.onlyAdapters !== undefined || Object.keys(gating.skips).length > 0 ? { onlyAdapters } : {}),
554
+ ...(input.now ? { now: input.now } : {}),
555
+ ...(input.trigger ? { trigger: input.trigger } : {}),
556
+ ...(input.catchUp === undefined ? {} : { catchUp: input.catchUp }),
557
+ ...(input.firstRun ? { firstRun: true } : {}),
558
+ ...(input.signalsGenerated ? { signalsGenerated: input.signalsGenerated } : {}),
559
+ beforeWrite: lockHeld,
560
+ });
561
+ }
562
+ report.outcomes.push(...gated);
514
563
  if (input.signalsAlert) {
515
564
  const postCompanyChannel = input.signalsAlert.postCompanyChannel ??
516
565
  createSignalsAlertChannelPoster({ hqRoot: input.hqRoot, company: input.company, deps });
@@ -593,6 +642,55 @@ function renderSyncTable(company, report) {
593
642
  }
594
643
  return lines.join("\n");
595
644
  }
645
+ function fleetBotsIn(root) {
646
+ let names;
647
+ try {
648
+ names = fs.readdirSync(root).sort();
649
+ }
650
+ catch {
651
+ return [];
652
+ }
653
+ const bots = [];
654
+ for (const botName of names) {
655
+ const config = readBotConfig(path.join(root, botName));
656
+ if (config && config.engine?.id === HQ_FLEET_RUNTIME_ENGINE_ID)
657
+ bots.push({ botName, agentUid: config.agentUid });
658
+ }
659
+ return bots;
660
+ }
661
+ function findFleetHotSignalSyncJob(root, company, opsFor) {
662
+ for (const bot of fleetBotsIn(root)) {
663
+ let ops;
664
+ let jobs;
665
+ try {
666
+ ops = opsFor(bot.agentUid);
667
+ jobs = ops.list();
668
+ }
669
+ catch {
670
+ continue;
671
+ }
672
+ const job = jobs.find((candidate) => companyOfHotSignalSyncJob(candidate) === company);
673
+ if (job)
674
+ return { botName: bot.botName, ops, job };
675
+ }
676
+ return null;
677
+ }
678
+ function fleetDailyAt(expr) {
679
+ const match = /^(\d{1,2}) (\d{1,2}) \* \* \*$/.exec((expr ?? "").trim());
680
+ return match ? `${match[2].padStart(2, "0")}:${match[1].padStart(2, "0")}` : null;
681
+ }
682
+ function fleetJobView(found) {
683
+ const expr = found.job.schedule?.expr ?? "";
684
+ return {
685
+ botName: found.botName,
686
+ jobId: found.job.id,
687
+ cron: expr,
688
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
689
+ dailyAt: fleetDailyAt(expr),
690
+ nextRunAt: found.job.next_run_at ?? null,
691
+ lastRunAt: found.job.last_run_at ?? null,
692
+ };
693
+ }
596
694
  export class NoHotSignalSyncJobError extends Error {
597
695
  company;
598
696
  constructor(company) {
@@ -618,7 +716,22 @@ export async function runSchedule(options, overrides = {}) {
618
716
  // Validate before touching any job file.
619
717
  if (options.at !== undefined)
620
718
  parseDailyAt(options.at);
621
- let found = findHotSignalSyncJobs(overrides.botsRoot ?? defaultBotsRoot(), company);
719
+ const botsRootDir = overrides.botsRoot ?? defaultBotsRoot();
720
+ const fleetFound = findFleetHotSignalSyncJob(botsRootDir, company, overrides.fleetOps ?? fleetCronOps);
721
+ if (fleetFound) {
722
+ if (options.at !== undefined) {
723
+ const { hour, minute } = parseDailyAt(options.at);
724
+ const updated = fleetFound.ops.reschedule(fleetFound.job.id, `${minute} ${hour} * * *`);
725
+ if (!updated)
726
+ throw new NoHotSignalSyncJobError(company);
727
+ fleetFound.job = updated;
728
+ }
729
+ return finishScheduleReport(options, overrides, company, fleetJobView(fleetFound), [], now);
730
+ }
731
+ if (options.at !== undefined && fleetBotsIn(botsRootDir).some((bot) => bot.botName === "setup")) {
732
+ throw new Error(`The setup bot runs on the HQ Fleet Runtime and has no hot-signal-sync job for ${company} yet. Start it once with \`hq bot start setup\` so it adds the job, then set the time again.`);
733
+ }
734
+ let found = findHotSignalSyncJobs(botsRootDir, company);
622
735
  if (options.at !== undefined) {
623
736
  if (found.length === 0) {
624
737
  // Setting a time turns the daily sync on: register it on the personal setup bot.
@@ -638,20 +751,19 @@ export async function runSchedule(options, overrides = {}) {
638
751
  throw new NoHotSignalSyncJobError(company);
639
752
  found = rescheduleDaily(found, options.at, now);
640
753
  }
754
+ return finishScheduleReport(options, overrides, company, found[0] ? toJobView(found[0], now) : null, found.slice(1).map((f) => f.botName), now);
755
+ }
756
+ async function finishScheduleReport(options, overrides, company, job, extraBots, now) {
641
757
  const email = overrides.email ?? resolveUploadAuthor()?.email;
642
758
  const lastRuns = email
643
759
  ? await lastRunsPerAdapter(path.join(options.hqRoot, "companies", company), audienceKey([email]), now)
644
760
  : [];
645
- const report = {
646
- company,
647
- job: found[0] ? toJobView(found[0], now) : null,
648
- lastRuns,
649
- };
761
+ const report = { company, job, lastRuns };
650
762
  if (options.json) {
651
763
  process.stdout.write(JSON.stringify(report, null, 2) + "\n");
652
764
  }
653
765
  else {
654
- process.stdout.write(renderScheduleReport(report, found.slice(1).map((f) => f.botName)) + "\n");
766
+ process.stdout.write(renderScheduleReport(report, extraBots) + "\n");
655
767
  if (!email)
656
768
  process.stdout.write("Sign in (hq login) to see recent runs.\n");
657
769
  }
@@ -24,6 +24,12 @@ interface PlanCompany {
24
24
  slug: string;
25
25
  memberCount: number;
26
26
  }
27
+ interface PlanCompanyHandoff {
28
+ companyUid: string;
29
+ companySlug: string;
30
+ toPersonUid: string;
31
+ toEmail: string;
32
+ }
27
33
  interface Plan {
28
34
  target: {
29
35
  input: string;
@@ -32,6 +38,8 @@ interface Plan {
32
38
  personUid: string | null;
33
39
  cognitoUsers: PlanCognitoUser[];
34
40
  ownedCompanies: PlanCompany[];
41
+ /** Present only when the request opted in to the handoff. */
42
+ sharedCompanyHandoffs?: PlanCompanyHandoff[];
35
43
  personalVault: {
36
44
  bucketName: string | null;
37
45
  kmsKeyAlias: string | null;
@@ -89,6 +97,8 @@ export interface RunResetTestAccountParams {
89
97
  execute: boolean;
90
98
  /** Skip the typed-confirmation prompt on --execute (for scripts). */
91
99
  yes?: boolean;
100
+ /** Keep a company with real members and hand it to one, instead of refusing. */
101
+ handoffSharedCompanies?: boolean;
92
102
  }
93
103
  export interface RunResetTestAccountDeps {
94
104
  prompt?: ConfirmFn;
@@ -76,6 +76,13 @@ export function renderPlan(plan) {
76
76
  }
77
77
  }
78
78
  lines.push("");
79
+ if (plan.sharedCompanyHandoffs && plan.sharedCompanyHandoffs.length > 0) {
80
+ lines.push(chalk.bold(" Companies kept and handed over"));
81
+ for (const h of plan.sharedCompanyHandoffs) {
82
+ lines.push(` handoff: ${h.companySlug} to ${h.toEmail}`);
83
+ }
84
+ lines.push("");
85
+ }
79
86
  lines.push(chalk.bold(" Personal vault"));
80
87
  lines.push(` bucket: ${plan.personalVault.bucketName ?? chalk.dim("(none)")}`);
81
88
  lines.push(` kms alias: ${plan.personalVault.kmsKeyAlias ?? chalk.dim("(none)")}`);
@@ -166,13 +173,17 @@ export async function runResetTestAccount(params, deps = {}) {
166
173
  throw new Error("reset-test-account requires a super-admin (staff) HQ session. " +
167
174
  "Sign in with an @getindigo.ai account and retry.");
168
175
  }
176
+ // Absent unless asked for, so the server's refusal stays the default.
177
+ const handoff = params.handoffSharedCompanies === true
178
+ ? { handoffSharedCompanies: true }
179
+ : {};
169
180
  // Dry-run first. The AC calls out "Without --execute, runs dry-run and
170
181
  // prints the plan" — so a bare invocation always ends here.
171
182
  const dryRunRes = await vaultApiFetch({
172
183
  token,
173
184
  path: "/v1/admin/test-account/reset",
174
185
  method: "POST",
175
- body: { email, dryRun: true },
186
+ body: { email, dryRun: true, ...handoff },
176
187
  });
177
188
  const dryRunBody = await readBody(dryRunRes);
178
189
  if (!dryRunRes.ok) {
@@ -221,7 +232,7 @@ export async function runResetTestAccount(params, deps = {}) {
221
232
  token,
222
233
  path: "/v1/admin/test-account/reset",
223
234
  method: "POST",
224
- body: { email, dryRun: false, confirmEmail },
235
+ body: { email, dryRun: false, confirmEmail, ...handoff },
225
236
  });
226
237
  const executeBody = await readBody(executeRes);
227
238
  // The handler answers 200 (all-ok) and 207 (best-effort partial). Anything
@@ -234,6 +245,9 @@ export async function runResetTestAccount(params, deps = {}) {
234
245
  throw new Error("server did not return an execute result");
235
246
  }
236
247
  console.log(renderExecuteSummary(result));
248
+ for (const h of plan.sharedCompanyHandoffs ?? []) {
249
+ console.log(` handoff: ${h.companySlug} to ${h.toEmail}`);
250
+ }
237
251
  if (!result.ok) {
238
252
  // Non-zero exit so a scripted caller can react. The summary line above
239
253
  // already shows which step failed.
@@ -253,12 +267,14 @@ export function registerResetTestAccountCommand(staff, deps = {}) {
253
267
  .argument("<email>", "Designated test-account email (must be in the registry)")
254
268
  .option("--execute", "Actually delete artifacts. Prompts for a typed-email confirmation.")
255
269
  .option("-y, --yes", "Skip the typed-email confirmation prompt (still sends confirmEmail).")
270
+ .option("--handoff-shared-companies", "Keep a company with real members and hand it to one, instead of refusing the reset.")
256
271
  .action(async (email, opts) => {
257
272
  try {
258
273
  await runResetTestAccount({
259
274
  email,
260
275
  execute: opts.execute === true,
261
276
  yes: opts.yes === true,
277
+ handoffSharedCompanies: opts.handoffSharedCompanies === true,
262
278
  }, deps);
263
279
  }
264
280
  catch (err) {