@m8t-stack/cli 0.2.57 → 0.2.59

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/cli.js CHANGED
@@ -1364,7 +1364,7 @@ var init_enable_hosted_brain = __esm({
1364
1364
  import { Builtins, Cli } from "clipanion";
1365
1365
 
1366
1366
  // src/lib/package-version.ts
1367
- var CLI_VERSION = "0.2.57";
1367
+ var CLI_VERSION = "0.2.59";
1368
1368
 
1369
1369
  // src/lib/render-error.ts
1370
1370
  init_errors();
@@ -15668,6 +15668,72 @@ async function closePullRequestViaApp(args) {
15668
15668
  if (!res.ok) throw new LocalCliError({ code: "GH_APP_PR_CLOSE_FAILED", message: `PATCH /pulls/${args.number.toString()} HTTP ${res.status.toString()}: ${(await res.text()).slice(0, 300)}` });
15669
15669
  }
15670
15670
 
15671
+ // src/lib/github-app-permissions.ts
15672
+ init_esm2();
15673
+ init_errors();
15674
+ var REQUIRED_INSTALLATION_PERMISSIONS = [
15675
+ { key: "administration", level: "write", why: "create the brain repository (POST /orgs/<org>/repos)" },
15676
+ { key: "contents", level: "write", why: "push the brain template and commit memory updates" },
15677
+ { key: "workflows", level: "write", why: "push the brain template's .github/workflows/ files" },
15678
+ { key: "metadata", level: "read", why: "read repository metadata" }
15679
+ ];
15680
+ var LEVEL_RANK = { read: 1, write: 2, admin: 3 };
15681
+ function findPermissionGaps(granted) {
15682
+ return REQUIRED_INSTALLATION_PERMISSIONS.flatMap((req2) => {
15683
+ const have = granted[req2.key];
15684
+ const haveRank = have ? LEVEL_RANK[have] ?? 0 : 0;
15685
+ const needRank = LEVEL_RANK[req2.level] ?? 0;
15686
+ return haveRank >= needRank ? [] : [{ key: req2.key, required: req2.level, granted: have ?? null, why: req2.why }];
15687
+ });
15688
+ }
15689
+ function buildPermissionRefusal(args) {
15690
+ const lines = args.gaps.map((g) => ` \u2022 ${g.key}: need ${g.required}, installation grants ${g.granted ?? "nothing"} \u2014 needed to ${g.why}`);
15691
+ const appLabel = args.slug ? `'${args.slug}'` : "the m8t brain App";
15692
+ return {
15693
+ message: `The GitHub App installed on '${args.org}' is missing permissions the brain flow needs:
15694
+ ${lines.join("\n")}`,
15695
+ hint: `An App's permissions are not applied to an installation until an org owner approves them, so editing the App is not enough. Either approve the pending request at https://github.com/organizations/${args.org}/settings/installations (open ${appLabel} \u2192 Review request), or create a fresh App with the right set: m8t brain app-create --org ${args.org}`
15696
+ };
15697
+ }
15698
+ async function readInstallationPermissions(args) {
15699
+ const doFetch = args.fetchImpl ?? fetch;
15700
+ let jwt;
15701
+ try {
15702
+ jwt = signAppJwt({ appId: args.appId, privateKeyPem: args.privateKeyPem });
15703
+ } catch (e) {
15704
+ return { granted: null, unreadable: `could not sign an App JWT: ${e.message}` };
15705
+ }
15706
+ try {
15707
+ const res = await doFetch(`https://api.github.com/app/installations/${args.installationId}`, {
15708
+ headers: {
15709
+ Authorization: `Bearer ${jwt}`,
15710
+ Accept: "application/vnd.github+json",
15711
+ "X-GitHub-Api-Version": "2022-11-28",
15712
+ "User-Agent": "m8t"
15713
+ }
15714
+ });
15715
+ if (!res.ok) return { granted: null, unreadable: `GET /app/installations/${args.installationId} \u2192 HTTP ${String(res.status)}` };
15716
+ const body = await res.json();
15717
+ if (!body.permissions || typeof body.permissions !== "object") {
15718
+ return { granted: null, unreadable: "the installation response carried no permissions object" };
15719
+ }
15720
+ return { granted: body.permissions, ...body.app_slug ? { slug: body.app_slug } : {} };
15721
+ } catch (e) {
15722
+ return { granted: null, unreadable: e.message };
15723
+ }
15724
+ }
15725
+ async function assertInstallationPermissions(args) {
15726
+ const check = await readInstallationPermissions(args);
15727
+ if (!check.granted) {
15728
+ args.onUnverified?.(`could not confirm the GitHub App's granted permissions (${check.unreadable ?? "unknown"}) \u2014 continuing`);
15729
+ return;
15730
+ }
15731
+ const gaps = findPermissionGaps(check.granted);
15732
+ if (gaps.length === 0) return;
15733
+ const { message, hint } = buildPermissionRefusal({ org: args.org, ...check.slug ? { slug: check.slug } : {}, gaps });
15734
+ throw new LocalCliError({ code: "GITHUB_APP_PERMISSIONS_INSUFFICIENT", message, hint });
15735
+ }
15736
+
15671
15737
  // src/lib/brain-repo-collision.ts
15672
15738
  init_errors();
15673
15739
  var DEFAULT_MAX_ATTEMPTS = 20;
@@ -15835,6 +15901,16 @@ var BrainCreateCommand = class extends M8tCommand {
15835
15901
  const appPemOpt = typeof this.appPem === "string" ? this.appPem : void 0;
15836
15902
  const installationIdOpt = typeof this.installationId === "string" ? this.installationId : void 0;
15837
15903
  const useAppPath = appIdOpt !== void 0 && appPemOpt !== void 0 && installationIdOpt !== void 0;
15904
+ if (useAppPath) {
15905
+ await assertInstallationPermissions({
15906
+ appId: appIdOpt,
15907
+ privateKeyPem: readFileSync6(appPemOpt, "utf8"),
15908
+ installationId: installationIdOpt,
15909
+ org: owner,
15910
+ onUnverified: (m) => this.context.stderr.write(` ${colors.dim(`warning: ${m}`)}
15911
+ `)
15912
+ });
15913
+ }
15838
15914
  if (!useAppPath) {
15839
15915
  if (!await isGhAuthed()) {
15840
15916
  this.context.stdout.write(`${colors.hint("note:")} gh not authed \u2014 running gh auth login --device --scopes "repo,read:org"
@@ -24754,7 +24830,7 @@ function evaluateRedirectUri(p) {
24754
24830
  // Deliberately NOT "m8t prereqs --fix": this command reports it and does not
24755
24831
  // write it. The app registration is shared across every deployment in the
24756
24832
  // tenant, so the write belongs to the install, not to whoever is diagnosing.
24757
- `Re-run 'm8t bootstrap finish' (it registers this), or ask a directory admin to add it \u2014 MERGE, never overwrite:
24833
+ `Re-run 'm8t bootstrap status --finalize' (it registers this), or ask a directory admin to add it \u2014 MERGE, never overwrite:
24758
24834
  az ad app update --id ${p.clientId ?? "<clientId>"} --set spa.redirectUris="['${want}']"`
24759
24835
  );
24760
24836
  }
@@ -27526,11 +27602,16 @@ function redactTranscripts(input) {
27526
27602
  var DreamRunCommand = class extends M8tCommand {
27527
27603
  static paths = [["dream", "run"]];
27528
27604
  static usage = Command50.Usage({
27529
- description: "Dry-run the brain consumption pipeline for one worker (no model call, no writes).",
27530
- details: "Builds AzureCliCredential + resolves the Foundry project, ledger table, and Log Analytics workspace, runs the consumption pipeline, and prints the skip-ledger, the partition invariant, and harvest stats. Transcripts are metadata-only unless --show-transcripts is passed."
27605
+ description: "Dry-run the brain consumption pipeline for one worker (no model call, no writes). Pass --live to actually run it.",
27606
+ details: "Builds AzureCliCredential + resolves the Foundry project, ledger table, and Log Analytics workspace, runs the consumption pipeline, and prints the skip-ledger, the partition invariant, and harvest stats. Transcripts are metadata-only unless --show-transcripts is passed.\n\nThe bare command is READ-ONLY. --live takes the other branch: a real model call and a commit to the worker's brain repo through a minted GitHub App token."
27531
27607
  });
27532
27608
  worker = Option47.String("--worker", { description: "Worker (canonical name) to harvest. Required." });
27533
- dryRun = Option47.Boolean("--dry-run", false, { description: "Read-only harvest; no model call, no writes." });
27609
+ // Opting IN to the side effects, rather than opting out of them. This command's
27610
+ // own help has always described a dry run, but the bare invocation used to take
27611
+ // the live branch — a real model call and a commit to the brain repo — so anyone
27612
+ // acting on `--help` got the opposite of what they read.
27613
+ live = Option47.Boolean("--live", false, { description: "Actually run it: model call + commit to the brain repo." });
27614
+ dryRun = Option47.Boolean("--dry-run", false, { description: "Read-only harvest. This is the default; the flag is accepted for compatibility." });
27534
27615
  since = Option47.String("--since", { description: "ISO-8601 start override (rejected if malformed or future)." });
27535
27616
  reset = Option47.Boolean("--reset", false, { description: "Ignore the stored cursor; read from the beginning." });
27536
27617
  showTranscripts = Option47.Boolean("--show-transcripts", false, {
@@ -27550,7 +27631,7 @@ var DreamRunCommand = class extends M8tCommand {
27550
27631
  const reset = this.reset === true;
27551
27632
  const outputFlag = this.output === "json" || this.output === "auto" || this.output === "pretty" ? this.output : "pretty";
27552
27633
  const mode = resolveOutputMode(outputFlag, this.context.stdout);
27553
- if (this.dryRun !== true) {
27634
+ if (this.live === true && this.dryRun !== true) {
27554
27635
  const liveDeps = this.deps ?? defaultDeps();
27555
27636
  const liveContext = await liveDeps.resolveContext({
27556
27637
  worker,
@@ -28517,6 +28598,7 @@ function buildAciCreateArgs(s) {
28517
28598
  if (s.gatewayImageRef) env.push(`GATEWAY_IMAGE_REF=${s.gatewayImageRef}`);
28518
28599
  if (s.foundryTracing) env.push(`FOUNDRY_TRACING=${s.foundryTracing}`);
28519
28600
  if (s.skipBrains) env.push("SKIP_BRAINS=true");
28601
+ if (s.founderObjectId) env.push(`FOUNDER_OBJECT_ID=${s.founderObjectId}`);
28520
28602
  if (s.updateChannelUrl) env.push(`M8T_UPDATE_CHANNEL_URL=${s.updateChannelUrl}`);
28521
28603
  if (s.enrollContactEmail) env.push(`M8T_ENROLL_CONTACT_EMAIL=${s.enrollContactEmail}`);
28522
28604
  if (s.enrollCompany) env.push(`M8T_ENROLL_COMPANY=${s.enrollCompany}`);
@@ -28776,14 +28858,14 @@ ${colors.error("\u2717")} The GitHub App on disk is installed on ${colors.field(
28776
28858
  // src/commands/bootstrap/launch.ts
28777
28859
  var DEFAULT_RG = "rg-m8t-stack";
28778
28860
  var DEFAULT_INSTALLER = "ghcr.io/m8t-labs/m8t-installer";
28779
- var DEFAULT_INSTALLER_TAG = "v0.1.50";
28861
+ var DEFAULT_INSTALLER_TAG = "v0.1.51";
28780
28862
  var ACI_NAME = "m8t-installer";
28781
28863
  var MI_NAME = "m8t-installer-mi";
28782
28864
  var BootstrapLaunchCommand = class extends M8tCommand {
28783
28865
  static paths = [["bootstrap", "launch"]];
28784
28866
  static usage = Command54.Usage({
28785
28867
  description: "Create + authorize the installer managed identity, then kick the cloud installer.",
28786
- details: "Step 2 of `m8t bootstrap` (run after `preflight`). Ensures the resource group, creates the m8t app registration (or uses --client-id), creates a user-assigned managed identity granted Owner at subscription scope, and launches the published m8t-installer image as an ACI run-to-completion job under that identity. Writes ~/.m8t/bootstrap.json for `status`/`reap`/`finish`.",
28868
+ details: "Step 2 of `m8t bootstrap` (run after `preflight`). Ensures the resource group, creates the m8t app registration (or uses --client-id), creates a user-assigned managed identity granted Owner at subscription scope, and launches the published m8t-installer image as an ACI run-to-completion job under that identity. Writes ~/.m8t/bootstrap.json for `status` and `reap`, and threads your Azure object id so the installer can grant you the platform's data-plane roles itself.",
28787
28869
  examples: [
28788
28870
  ["Launch in eastus2", "$0 bootstrap launch --location eastus2"],
28789
28871
  ["BYO app registration", "$0 bootstrap launch --location eastus2 --client-id <appId>"],
@@ -28934,6 +29016,12 @@ var BootstrapLaunchCommand = class extends M8tCommand {
28934
29016
  out("waiting for the installer identity to be usable (role propagation)\u2026");
28935
29017
  const miReady = await waitForMiToken({ clientId: mi.clientId, subscriptionId, onProgress: out });
28936
29018
  if (!miReady) out("identity not confirmed yet \u2014 launching anyway (the installer retries its own login)");
29019
+ let founderObjectId;
29020
+ try {
29021
+ founderObjectId = await getCallerObjectId();
29022
+ } catch (e) {
29023
+ out(`warning: could not determine your Azure object id (${e.message}) \u2014 the install will not grant you data-plane access; run 'm8t prereqs --fix' afterwards`);
29024
+ }
28937
29025
  out("launching the cloud installer (ACI)\u2026");
28938
29026
  await kickInstaller({
28939
29027
  aciName: ACI_NAME,
@@ -28947,6 +29035,7 @@ var BootstrapLaunchCommand = class extends M8tCommand {
28947
29035
  gatewayImageRef,
28948
29036
  foundryTracing: "skip",
28949
29037
  githubApp,
29038
+ ...founderObjectId ? { founderObjectId } : {},
28950
29039
  ...skipBrains ? { skipBrains: true } : {},
28951
29040
  // Opt-in only. The signed-in UPN was previously seeded here automatically;
28952
29041
  // an installation is identified by its random instance id, and contact
@@ -28965,7 +29054,7 @@ var BootstrapLaunchCommand = class extends M8tCommand {
28965
29054
  };
28966
29055
 
28967
29056
  // src/commands/bootstrap/status.ts
28968
- import { Command as Command55, Option as Option52 } from "clipanion";
29057
+ import { Command as Command56, Option as Option53 } from "clipanion";
28969
29058
  init_errors();
28970
29059
 
28971
29060
  // src/lib/bootstrap-aci-state.ts
@@ -28990,364 +29079,74 @@ async function getAciState(opts) {
28990
29079
  return { provisioningState: j.provisioningState, firstContainerState: state, exitCode: cs?.exitCode, terminated };
28991
29080
  }
28992
29081
 
28993
- // src/commands/bootstrap/status.ts
28994
- var BootstrapStatusCommand = class extends M8tCommand {
28995
- static paths = [["bootstrap", "status"]];
28996
- static usage = Command55.Usage({
28997
- description: "Show the cloud installer's live status (phase, progress, result).",
28998
- details: "Reads the durable status blob written by the installer. --watch polls until the install reaches done or failed.",
28999
- examples: [["One read", "$0 bootstrap status"], ["Watch to completion", "$0 bootstrap status --watch"]]
29000
- });
29001
- watch = Option52.Boolean("--watch", false);
29002
- output = Option52.String("--output");
29003
- async executeCommand() {
29004
- const state = await readBootstrapState();
29005
- if (!state) {
29006
- throw new LocalCliError({
29007
- code: "BOOTSTRAP_NO_STATE",
29008
- message: "No bootstrap is in flight (no ~/.m8t/bootstrap.json).",
29009
- hint: "Run 'm8t bootstrap launch --location <region>' first."
29010
- });
29011
- }
29012
- const mode = resolveOutputMode(
29013
- typeof this.output === "string" ? this.output : void 0,
29014
- this.context.stdout
29015
- );
29016
- const sleep5 = (ms) => new Promise((r) => setTimeout(r, ms));
29017
- const watch = this.watch === true;
29018
- for (; ; ) {
29019
- let doc;
29020
- try {
29021
- doc = await readStatusBlob({ saName: state.statusSaName, resourceGroup: state.resourceGroup, subscriptionId: state.subscriptionId });
29022
- } catch (e) {
29023
- if (watch && e instanceof LocalCliError && e.code === "BOOTSTRAP_STATUS_UNREADABLE") {
29024
- const early = await getAciState({
29025
- aciName: state.aciName,
29026
- resourceGroup: state.resourceGroup,
29027
- subscriptionId: state.subscriptionId
29028
- }).catch(() => null);
29029
- if (early?.terminated) {
29030
- this.context.stderr.write(
29031
- ` ${colors.error("\u2717")} the installer container has terminated (exit ${String(early.exitCode ?? "?")}) and never reported a status.
29032
- ${colors.hint("inspect:")} az container logs -n ${state.aciName} -g ${state.resourceGroup}
29033
- ${colors.hint("reap:")} m8t bootstrap reap --force
29034
- `
29035
- );
29036
- return 1;
29037
- }
29038
- if (mode === "pretty") this.context.stderr.write(` ${colors.dim("waiting for the installer to start\u2026")}
29039
- `);
29040
- await sleep5(1e4);
29041
- continue;
29042
- }
29043
- throw e;
29044
- }
29045
- if (!watch || doc.status !== "running") {
29046
- if (mode === "json") {
29047
- this.context.stdout.write(renderJson(doc) + "\n");
29048
- } else {
29049
- this.context.stdout.write(formatStatus(doc));
29050
- }
29051
- return doc.status === "failed" ? 1 : 0;
29052
- }
29053
- if (mode === "pretty") this.context.stderr.write(` ${colors.dim(`${doc.phase} (${String(doc.phaseIndex)}/${String(doc.phaseTotal)}) ${doc.detail ?? ""}`)}
29054
- `);
29055
- const aci = await getAciState({ aciName: state.aciName, resourceGroup: state.resourceGroup, subscriptionId: state.subscriptionId }).catch(() => null);
29056
- if (aci?.terminated) {
29057
- this.context.stderr.write(
29058
- ` ${colors.error("\u2717")} the installer container has terminated (exit ${String(aci.exitCode ?? "?")}) but the status is still '${doc.phase}'.
29059
- ${colors.hint("inspect:")} az container logs -n ${state.aciName} -g ${state.resourceGroup}
29060
- ${colors.hint("reap:")} m8t bootstrap reap --force
29061
- `
29062
- );
29063
- return 1;
29064
- }
29065
- await sleep5(1e4);
29066
- }
29067
- }
29068
- };
29069
- function formatStatus(d) {
29070
- const head = `${d.status.toUpperCase()} \u2014 ${d.phase} (${String(d.phaseIndex)}/${String(d.phaseTotal)})${d.detail ? ` \u2014 ${d.detail}` : ""}
29071
- `;
29072
- if (d.status === "done" && d.result) {
29073
- return colors.success("\u2713 ") + head + ` gateway: ${d.result.gatewayUrl ?? "-"}
29074
- foundry: ${d.result.foundryEndpoint ?? "-"}
29075
- ${colors.hint("next:")} m8t bootstrap reap && m8t bootstrap finish
29076
- `;
29077
- }
29078
- if (d.status === "failed" && d.error) {
29079
- return colors.error("\u2717 ") + head + ` ${colors.error(`error in ${d.error.phase}: ${d.error.message}`)}
29080
- ${colors.hint("inspect:")} az container logs
29081
- `;
29082
- }
29083
- return head;
29084
- }
29082
+ // src/lib/bootstrap-finalize.ts
29083
+ import * as fs34 from "fs/promises";
29084
+ import * as os18 from "os";
29085
+ import * as path38 from "path";
29085
29086
 
29086
- // src/commands/bootstrap/reap.ts
29087
- import { Command as Command56, Option as Option53 } from "clipanion";
29087
+ // src/lib/company-profile-seed.ts
29088
+ import { spawn as spawn6 } from "child_process";
29089
+ import { closeSync, openSync, readFileSync as readFileSync21 } from "fs";
29090
+ import * as os15 from "os";
29091
+ import * as path33 from "path";
29088
29092
  init_errors();
29089
29093
 
29090
- // src/lib/bootstrap-reap.ts
29091
- var ROLE_ASSIGNMENT_API = "2022-04-01";
29092
- var GATEWAY_SUBSCOPE_ROLES = ["Cost Management Reader", "Monitoring Reader"];
29093
- async function listSubscriptionAssignments(subscriptionId) {
29094
- return JSON.parse(
29095
- await runAz([
29096
- "role",
29097
- "assignment",
29098
- "list",
29099
- "--all",
29100
- "--subscription",
29101
- subscriptionId,
29102
- "-o",
29103
- "json"
29104
- ]).catch(() => "[]") || "[]"
29105
- );
29106
- }
29107
- async function deleteAssignmentsById(ids) {
29108
- for (const id of ids) {
29109
- await runAz([
29110
- "rest",
29111
- "--method",
29112
- "delete",
29113
- "--url",
29114
- `https://management.azure.com${id}?api-version=${ROLE_ASSIGNMENT_API}`
29115
- ]).catch(() => void 0);
29094
+ // src/lib/onboarding-profile.ts
29095
+ init_esm();
29096
+ function describeBlockRejection(reason) {
29097
+ switch (reason) {
29098
+ case "unknown-schema-version":
29099
+ return "the onboarding block's schema_version was missing or unrecognized";
29100
+ case "unexpected-key":
29101
+ return "the onboarding JSON carried a field it shouldn't have";
29102
+ case "missing-key":
29103
+ return "the onboarding block was missing a required field";
29104
+ case "non-string-value":
29105
+ return "the onboarding block held a value of the wrong type";
29106
+ case "disallowed-value":
29107
+ return "an onboarding block field held a value outside its allowed set";
29108
+ case "too-many-pending-requests":
29109
+ return "the onboarding block listed more pending requests than the one allowed";
29110
+ case "malformed-json":
29111
+ return "the onboarding block's JSON could not be safely parsed \u2014 check for duplicate keys, excessive nesting, or a syntax error";
29112
+ case "multiple-artifacts":
29113
+ return "more than one onboarding block was found in your onboarding conversation";
29114
+ case "unreadable-fences":
29115
+ return "the message's code fences could not be reliably delimited, so any onboarding block inside them could not be safely read";
29116
+ case "mistagged-fence":
29117
+ return "the onboarding block was inside a code fence that wasn't tagged json, so it could not be safely read";
29118
+ case "unfenced-artifact":
29119
+ return "the onboarding block appeared outside of any code fence, so it could not be safely read";
29120
+ case "no-artifact":
29121
+ return "no onboarding block was found";
29116
29122
  }
29117
29123
  }
29118
- async function sweepOrphanGatewaySubRoles(opts) {
29119
- const subScope = `/subscriptions/${opts.subscriptionId}`;
29120
- const all = await listSubscriptionAssignments(opts.subscriptionId);
29121
- const matched = all.filter((r) => {
29122
- if (r.principalName) return false;
29123
- if (r.principalType !== "ServicePrincipal") return false;
29124
- if (r.scope !== subScope) return false;
29125
- if (r.description === GATEWAY_SUBSCOPE_GRANT_DESCRIPTION) return true;
29126
- return GATEWAY_SUBSCOPE_ROLES.includes(r.roleDefinitionName ?? "");
29127
- });
29128
- if (opts.execute) await deleteAssignmentsById(matched.map((r) => r.id));
29129
- return matched.map((r) => ({ id: r.id, description: r.description, roleDefinitionName: r.roleDefinitionName, createdOn: r.createdOn }));
29130
- }
29131
- async function sweepOrphanOwnerAssignments(opts) {
29132
- const subScope = `/subscriptions/${opts.subscriptionId}`;
29133
- const all = await listSubscriptionAssignments(opts.subscriptionId);
29134
- const matched = all.filter((r) => {
29135
- if (r.description === INSTALLER_GRANT_DESCRIPTION) return true;
29136
- return !r.principalName && r.principalType === "ServicePrincipal" && r.roleDefinitionName === "Owner" && r.scope === subScope;
29137
- });
29138
- if (opts.execute) await deleteAssignmentsById(matched.map((r) => r.id));
29139
- return matched.map((r) => ({ id: r.id, description: r.description, createdOn: r.createdOn }));
29124
+ var COMPANY_PROFILE_PATH = "memory/company-profile.md";
29125
+ var DEFAULT_MEMORY_INDEX_HEADER = [
29126
+ `# Memory index`,
29127
+ ``,
29128
+ `> Your memories, newest first. The summary on each line is usually enough to answer \u2014 open the linked file only when you need full detail. Each path is repo-root; copy it verbatim, never invent one.`,
29129
+ ``
29130
+ ].join("\n");
29131
+ var ONBOARDING_BLOCK_KEYS = [
29132
+ "schema_version",
29133
+ "company_stage",
29134
+ "icp",
29135
+ "industry",
29136
+ "team_size",
29137
+ "context",
29138
+ "founder_name",
29139
+ "founder_email",
29140
+ "advisor_name",
29141
+ "advisor_email"
29142
+ ];
29143
+ function isRecord(value) {
29144
+ return typeof value === "object" && value !== null && !Array.isArray(value);
29140
29145
  }
29141
- async function reapInstaller(opts) {
29142
- const swallow = async (p) => {
29143
- try {
29144
- await p;
29145
- } catch {
29146
- }
29147
- };
29148
- await swallow(runAz(["container", "delete", "--name", opts.aciName, "--resource-group", opts.resourceGroup, "--subscription", opts.subscriptionId, "--yes", "--only-show-errors"]));
29149
- await swallow(runAz(["identity", "delete", "--name", opts.miName, "--resource-group", opts.resourceGroup, "--subscription", opts.subscriptionId]));
29150
- for (const id of opts.roleAssignmentIds) {
29151
- await swallow(runAz([
29152
- "rest",
29153
- "--method",
29154
- "delete",
29155
- "--url",
29156
- `https://management.azure.com${id}?api-version=${ROLE_ASSIGNMENT_API}`
29157
- ]));
29158
- }
29159
- const survivors = JSON.parse(
29160
- await runAz([
29161
- "role",
29162
- "assignment",
29163
- "list",
29164
- "--all",
29165
- "--subscription",
29166
- opts.subscriptionId,
29167
- "--query",
29168
- `[?principalId=='${opts.principalId}' && roleDefinitionName=='Owner'].id`,
29169
- "-o",
29170
- "json"
29171
- ]).catch(() => "[]") || "[]"
29172
- );
29173
- for (const id of survivors) {
29174
- await swallow(runAz(["rest", "--method", "delete", "--url", `https://management.azure.com${id}?api-version=${ROLE_ASSIGNMENT_API}`]));
29175
- }
29176
- }
29177
-
29178
- // src/commands/bootstrap/reap.ts
29179
- var BootstrapReapCommand = class extends M8tCommand {
29180
- static paths = [["bootstrap", "reap"]];
29181
- static usage = Command56.Usage({
29182
- description: "Tear down the installer scaffolding (ACI \u2192 MI \u2192 its role assignments) after a successful install.",
29183
- details: "Runs locally on the 'done' signal (the installer can't delete its own identity). The platform RG and the gateway's own assignments persist. A failed install is left intact for diagnosis unless --force.",
29184
- examples: [["Reap after done", "$0 bootstrap reap"]]
29185
- });
29186
- force = Option53.Boolean("--force", false);
29187
- sweepOrphans = Option53.Boolean("--sweep-orphans", false);
29188
- yes = Option53.Boolean("--yes", false);
29189
- async executeCommand() {
29190
- if (this.sweepOrphans === true) {
29191
- const { subscriptionId: sub } = await getAzAccount();
29192
- if (!sub) {
29193
- throw new LocalCliError({ code: "BOOTSTRAP_SWEEP_NO_SUB", message: "No active subscription.", hint: "Run 'az account set --subscription <id>'." });
29194
- }
29195
- const execute = this.yes === true;
29196
- const owner = await sweepOrphanOwnerAssignments({ subscriptionId: sub, execute });
29197
- const gateway = await sweepOrphanGatewaySubRoles({ subscriptionId: sub, execute });
29198
- const total = owner.length + gateway.length;
29199
- if (total === 0) {
29200
- this.context.stdout.write(`${colors.success("\u2713")} No orphaned installer Owner@sub or gateway sub-scope assignments found.
29201
- `);
29202
- return 0;
29203
- }
29204
- for (const f of owner) {
29205
- this.context.stdout.write(` ${colors.dim("Owner@sub")} ${f.id}${f.createdOn ? colors.dim(` (created ${f.createdOn})`) : ""}${f.description ? colors.dim(" [tagged]") : ""}
29206
- `);
29207
- }
29208
- for (const f of gateway) {
29209
- this.context.stdout.write(` ${colors.dim(f.roleDefinitionName ?? "gateway sub-scope")} ${f.id}${f.createdOn ? colors.dim(` (created ${f.createdOn})`) : ""}${f.description ? colors.dim(" [tagged]") : ""}
29210
- `);
29211
- }
29212
- const breakdown = `${String(owner.length)} Owner@sub, ${String(gateway.length)} gateway sub-scope`;
29213
- if (execute) {
29214
- this.context.stdout.write(`${colors.success("\u2713")} Removed ${String(total)} orphaned assignment(s) (${breakdown}).
29215
- `);
29216
- } else {
29217
- this.context.stdout.write(`
29218
- Found ${String(total)} orphaned assignment(s) (${breakdown}) (dry-run). ${colors.hint("Re-run with --sweep-orphans --yes to delete them.")}
29219
- `);
29220
- }
29221
- return 0;
29222
- }
29223
- const state = await readBootstrapState();
29224
- if (!state) {
29225
- if (this.force === true) {
29226
- const { subscriptionId: sub } = await getAzAccount();
29227
- if (!sub) {
29228
- throw new LocalCliError({ code: "BOOTSTRAP_SWEEP_NO_SUB", message: "No active subscription.", hint: "Run 'az account set --subscription <id>'." });
29229
- }
29230
- const owner = await sweepOrphanOwnerAssignments({ subscriptionId: sub, execute: true });
29231
- const gateway = await sweepOrphanGatewaySubRoles({ subscriptionId: sub, execute: true });
29232
- const total = owner.length + gateway.length;
29233
- this.context.stdout.write(
29234
- total === 0 ? `${colors.success("\u2713")} No bootstrap state and no orphaned installer Owner@sub or gateway sub-scope assignments. Nothing to reap.
29235
- ` : `${colors.success("\u2713")} No bootstrap state found; swept ${String(total)} orphaned assignment(s) (${String(owner.length)} Owner@sub, ${String(gateway.length)} gateway sub-scope).
29236
- `
29237
- );
29238
- return 0;
29239
- }
29240
- throw new LocalCliError({
29241
- code: "BOOTSTRAP_NO_STATE",
29242
- message: "No bootstrap state to reap.",
29243
- hint: "Nothing to do. If a launch failed and its RG was deleted it may have orphaned an installer Owner@sub grant or the gateway's sub-scope reader roles \u2014 run 'm8t bootstrap reap --force' (or 'm8t bootstrap reap --sweep-orphans --yes') to clean them."
29244
- });
29245
- }
29246
- if (this.force !== true) {
29247
- const doc = await readStatusBlob({ saName: state.statusSaName, resourceGroup: state.resourceGroup, subscriptionId: state.subscriptionId });
29248
- if (doc.status !== "done") {
29249
- if (doc.status === "failed") {
29250
- throw new LocalCliError({
29251
- code: "BOOTSTRAP_REAP_FAILED_INSTALL",
29252
- message: `The install failed at '${doc.error?.phase ?? doc.phase}'. Leaving the installer up for diagnosis.`,
29253
- hint: `Inspect: az container logs -n ${state.aciName} -g ${state.resourceGroup}. Reap anyway with --force.`
29254
- });
29255
- }
29256
- const aci = await getAciState({ aciName: state.aciName, resourceGroup: state.resourceGroup, subscriptionId: state.subscriptionId }).catch(() => null);
29257
- const deadHint = aci?.terminated ? `The installer container has terminated (exit ${String(aci.exitCode ?? "?")}) without reaching done \u2014 reap with --force.` : `Wait for 'm8t bootstrap status --watch' to reach done, or reap with --force if the install is stuck.`;
29258
- throw new LocalCliError({
29259
- code: "BOOTSTRAP_REAP_NOT_DONE",
29260
- message: `The install is not done yet (status: ${doc.status}, phase: ${doc.phase}).`,
29261
- hint: deadHint
29262
- });
29263
- }
29264
- }
29265
- await reapInstaller({
29266
- subscriptionId: state.subscriptionId,
29267
- resourceGroup: state.resourceGroup,
29268
- aciName: state.aciName,
29269
- miName: state.miName,
29270
- roleAssignmentIds: state.roleAssignmentIds,
29271
- principalId: state.miPrincipalId
29272
- });
29273
- this.context.stdout.write(`${colors.success("\u2713")} reaped the installer (ACI + identity + its role assignments). The platform stays.
29274
- `);
29275
- this.context.stdout.write(` ${colors.hint("next:")} m8t bootstrap finish
29276
- `);
29277
- return 0;
29278
- }
29279
- };
29280
-
29281
- // src/commands/bootstrap/finish.ts
29282
- import * as fs34 from "fs/promises";
29283
- import * as os18 from "os";
29284
- import * as path38 from "path";
29285
- import { Command as Command58, Option as Option55 } from "clipanion";
29286
- init_errors();
29287
-
29288
- // src/lib/company-profile-seed.ts
29289
- import { spawn as spawn6 } from "child_process";
29290
- import { closeSync, openSync, readFileSync as readFileSync21 } from "fs";
29291
- import * as os15 from "os";
29292
- import * as path33 from "path";
29293
- init_errors();
29294
-
29295
- // src/lib/onboarding-profile.ts
29296
- init_esm();
29297
- function describeBlockRejection(reason) {
29298
- switch (reason) {
29299
- case "unknown-schema-version":
29300
- return "the onboarding block's schema_version was missing or unrecognized";
29301
- case "unexpected-key":
29302
- return "the onboarding JSON carried a field it shouldn't have";
29303
- case "missing-key":
29304
- return "the onboarding block was missing a required field";
29305
- case "non-string-value":
29306
- return "the onboarding block held a value of the wrong type";
29307
- case "disallowed-value":
29308
- return "an onboarding block field held a value outside its allowed set";
29309
- case "too-many-pending-requests":
29310
- return "the onboarding block listed more pending requests than the one allowed";
29311
- case "malformed-json":
29312
- return "the onboarding block's JSON could not be safely parsed \u2014 check for duplicate keys, excessive nesting, or a syntax error";
29313
- case "multiple-artifacts":
29314
- return "more than one onboarding block was found in your onboarding conversation";
29315
- case "unreadable-fences":
29316
- return "the message's code fences could not be reliably delimited, so any onboarding block inside them could not be safely read";
29317
- case "mistagged-fence":
29318
- return "the onboarding block was inside a code fence that wasn't tagged json, so it could not be safely read";
29319
- case "unfenced-artifact":
29320
- return "the onboarding block appeared outside of any code fence, so it could not be safely read";
29321
- case "no-artifact":
29322
- return "no onboarding block was found";
29323
- }
29324
- }
29325
- var COMPANY_PROFILE_PATH = "memory/company-profile.md";
29326
- var DEFAULT_MEMORY_INDEX_HEADER = [
29327
- `# Memory index`,
29328
- ``,
29329
- `> Your memories, newest first. The summary on each line is usually enough to answer \u2014 open the linked file only when you need full detail. Each path is repo-root; copy it verbatim, never invent one.`,
29330
- ``
29331
- ].join("\n");
29332
- var ONBOARDING_BLOCK_KEYS = [
29333
- "schema_version",
29334
- "company_stage",
29335
- "icp",
29336
- "industry",
29337
- "team_size",
29338
- "context",
29339
- "founder_name",
29340
- "founder_email",
29341
- "advisor_name",
29342
- "advisor_email"
29343
- ];
29344
- function isRecord(value) {
29345
- return typeof value === "object" && value !== null && !Array.isArray(value);
29346
- }
29347
- function hasValidUniqueJsonKeys(source) {
29348
- let offset = 0;
29349
- const skipWhitespace = () => {
29350
- while (/\s/.test(source[offset] ?? "")) offset += 1;
29146
+ function hasValidUniqueJsonKeys(source) {
29147
+ let offset = 0;
29148
+ const skipWhitespace = () => {
29149
+ while (/\s/.test(source[offset] ?? "")) offset += 1;
29351
29150
  };
29352
29151
  const parseString = () => {
29353
29152
  if (source[offset] !== '"') return null;
@@ -30184,7 +29983,7 @@ function spawnDetachedSeedWatch() {
30184
29983
  closeSync(fd);
30185
29984
  }
30186
29985
  }
30187
- async function reactiveSeedOnFinish(args) {
29986
+ async function reactiveSeedOnInstallComplete(args) {
30188
29987
  const ctx = await resolveSeedContext({ endpointOverride: args.endpoint, appCredsOverride: args.appCredsOverride, subscriptionId: args.subscriptionId });
30189
29988
  if (!ctx) return;
30190
29989
  const token = await (args.getFoundryTokenImpl ?? getFoundryToken)();
@@ -30370,18 +30169,18 @@ async function walk(root, relative3 = "") {
30370
30169
  const childRelative = relative3 ? `${relative3}/${child.name}` : child.name;
30371
30170
  normalizedRelative(childRelative);
30372
30171
  const childPath = path34.join(directory, child.name);
30373
- const stat4 = await fs31.lstat(childPath);
30374
- if (stat4.isDirectory()) {
30172
+ const stat5 = await fs31.lstat(childPath);
30173
+ if (stat5.isDirectory()) {
30375
30174
  entries.push(...await walk(root, childRelative));
30376
- } else if (stat4.isFile()) {
30175
+ } else if (stat5.isFile()) {
30377
30176
  entries.push({
30378
30177
  type: "file",
30379
30178
  path: childRelative,
30380
- mode: stat4.mode & 511,
30381
- size: stat4.size,
30179
+ mode: stat5.mode & 511,
30180
+ size: stat5.size,
30382
30181
  sha256: await sha256File2(childPath)
30383
30182
  });
30384
- } else if (stat4.isSymbolicLink()) {
30183
+ } else if (stat5.isSymbolicLink()) {
30385
30184
  const target = await fs31.readlink(childPath);
30386
30185
  entries.push({
30387
30186
  type: "symlink",
@@ -30395,9 +30194,9 @@ async function walk(root, relative3 = "") {
30395
30194
  return entries;
30396
30195
  }
30397
30196
  async function ensureRealDirectory(root) {
30398
- const stat4 = await fs31.lstat(root);
30399
- if (stat4.isSymbolicLink()) throw new Error("Artifact root is a symbolic link");
30400
- if (!stat4.isDirectory()) throw new Error("Artifact root is not a directory");
30197
+ const stat5 = await fs31.lstat(root);
30198
+ if (stat5.isSymbolicLink()) throw new Error("Artifact root is a symbolic link");
30199
+ if (!stat5.isDirectory()) throw new Error("Artifact root is not a directory");
30401
30200
  }
30402
30201
  async function validateResolvedLinks(root, entries) {
30403
30202
  const realRoot = await fs31.realpath(root);
@@ -30837,8 +30636,8 @@ async function atomicWriteText(filePath, contents, mode) {
30837
30636
  }
30838
30637
  async function realRegularFile(filePath, executable) {
30839
30638
  const real = await fs32.realpath(filePath);
30840
- const stat4 = await fs32.stat(real);
30841
- if (!stat4.isFile()) throw new Error("Companion launch target is not a file");
30639
+ const stat5 = await fs32.stat(real);
30640
+ if (!stat5.isFile()) throw new Error("Companion launch target is not a file");
30842
30641
  await fs32.access(real, executable ? constants2.X_OK : constants2.R_OK);
30843
30642
  return real;
30844
30643
  }
@@ -30851,6 +30650,29 @@ function defaultLaunch(executable) {
30851
30650
  child.unref();
30852
30651
  return Promise.resolve();
30853
30652
  }
30653
+ async function companionIsRunning(platform, executable) {
30654
+ if (platform !== "win32") return false;
30655
+ let handle;
30656
+ try {
30657
+ handle = await fs32.open(executable, "r+");
30658
+ } catch (error) {
30659
+ const code = error.code;
30660
+ if (code === "ENOENT") return false;
30661
+ if (code === "EBUSY" || code === "EPERM" || code === "EACCES") return true;
30662
+ throw error;
30663
+ } finally {
30664
+ await handle?.close().catch(() => void 0);
30665
+ }
30666
+ return false;
30667
+ }
30668
+ async function assertCompanionNotRunning(options, executable) {
30669
+ const running = options.isRunning ?? ((owned) => companionIsRunning(options.platform, owned));
30670
+ if (await running(executable)) {
30671
+ throw new Error(
30672
+ "The desktop companions are running. Quit them from the tray, then run this again."
30673
+ );
30674
+ }
30675
+ }
30854
30676
  async function readInstalled(options) {
30855
30677
  const paths = companionInstallPaths(options);
30856
30678
  await assertNotSymlink(paths.installManifest, "Install manifest");
@@ -30979,6 +30801,15 @@ async function converge(options, force) {
30979
30801
  if (!force && current.state === "installed" && current.version === artifactManifest.version && current.artifactTreeSha256 === artifactManifest.treeSha256 && current.startAtLogin) {
30980
30802
  return current;
30981
30803
  }
30804
+ if (priorInstalled) {
30805
+ await assertCompanionNotRunning(
30806
+ options,
30807
+ path35.join(
30808
+ priorInstalled.paths.targetRoot,
30809
+ ...priorInstalled.install.entryRelativePath.split("/")
30810
+ )
30811
+ );
30812
+ }
30982
30813
  if (current.state === "not-installed") {
30983
30814
  try {
30984
30815
  await fs32.lstat(paths.targetRoot);
@@ -31129,6 +30960,7 @@ async function uninstallCompanion(options) {
31129
30960
  paths.targetRoot,
31130
30961
  ...install.entryRelativePath.split("/")
31131
30962
  );
30963
+ await assertCompanionNotRunning(options, executable);
31132
30964
  await (options.setStartAtLogin ?? (() => Promise.resolve()))(executable, false);
31133
30965
  const remove = options.removeOwnedPath ?? (async (ownedPath, recursive) => {
31134
30966
  await fs32.rm(ownedPath, { recursive, force: true });
@@ -31143,7 +30975,7 @@ async function uninstallCompanion(options) {
31143
30975
  // src/commands/companion/install.ts
31144
30976
  import * as os17 from "os";
31145
30977
  import * as path37 from "path";
31146
- import { Command as Command57, Option as Option54 } from "clipanion";
30978
+ import { Command as Command55, Option as Option52 } from "clipanion";
31147
30979
 
31148
30980
  // src/lib/companion-channel.ts
31149
30981
  async function readCompanionRelease(source = { url: CHANNEL_LATEST_URL }, deps = {}) {
@@ -31254,7 +31086,7 @@ function defaultLocalCompanionInstallOptions() {
31254
31086
  getStartAtLogin: (executable) => getCompanionStartAtLogin({ homeDirectory, platform, executable })
31255
31087
  };
31256
31088
  }
31257
- async function convergeCompanionFromChannel(converge2, stagedDirectory) {
31089
+ async function convergeCompanionFromChannel(converge2, stagedDirectory, resourceGroup) {
31258
31090
  let artifactManifestPath;
31259
31091
  let dispose = () => Promise.resolve();
31260
31092
  if (stagedDirectory !== void 0) {
@@ -31266,7 +31098,7 @@ async function convergeCompanionFromChannel(converge2, stagedDirectory) {
31266
31098
  artifactManifestPath = build.artifactManifestPath;
31267
31099
  dispose = build.dispose;
31268
31100
  }
31269
- const gateway = await resolveGatewayContext({ interactive: false });
31101
+ const gateway = await resolveGatewayContext({ interactive: false, resourceGroup });
31270
31102
  try {
31271
31103
  return await converge2({
31272
31104
  ...defaultLocalCompanionInstallOptions(),
@@ -31277,204 +31109,565 @@ async function convergeCompanionFromChannel(converge2, stagedDirectory) {
31277
31109
  await dispose();
31278
31110
  }
31279
31111
  }
31280
- async function runCompanionInstallCommand(stdout, install) {
31281
- const state = await install();
31282
- if (state.state === "not-released") {
31283
- stdout.write(
31284
- "The release this platform is pinned to carries no desktop companions.\nThey arrive with a platform release \u2014 run: m8t platform update\n"
31285
- );
31286
- return 1;
31287
- }
31288
- if (state.state !== "installed") {
31289
- stdout.write("Desktop companions need repair.\nRun: m8t companion repair\n");
31290
- return 1;
31112
+ async function runCompanionInstallCommand(stdout, install) {
31113
+ const state = await install();
31114
+ if (state.state === "not-released") {
31115
+ stdout.write(
31116
+ "The release this platform is pinned to carries no desktop companions.\nThey arrive with a platform release \u2014 run: m8t platform update\n"
31117
+ );
31118
+ return 1;
31119
+ }
31120
+ if (state.state !== "installed") {
31121
+ stdout.write("Desktop companions need repair.\nRun: m8t companion repair\n");
31122
+ return 1;
31123
+ }
31124
+ stdout.write(
31125
+ `Desktop companions installed: Stacey and Azzy are ready.
31126
+ Version: ${state.version}
31127
+ `
31128
+ );
31129
+ return 0;
31130
+ }
31131
+ var CompanionInstallCommand = class extends M8tCommand {
31132
+ static paths = [["companion", "install"]];
31133
+ static usage = Command55.Usage({
31134
+ description: "Install the desktop companions for this user from the release channel.",
31135
+ examples: [
31136
+ ["Install the released build", "$0 companion install"],
31137
+ [
31138
+ "Install one built from this clone",
31139
+ "$0 companion install --from apps/companion/dist-artifacts/darwin-arm64"
31140
+ ]
31141
+ ]
31142
+ });
31143
+ from = Option52.String("--from", {
31144
+ description: "A locally staged build directory instead of the released one."
31145
+ });
31146
+ resourceGroup = Option52.String("--resource-group", {
31147
+ description: "Which deployment to bind to, when the subscription holds more than one."
31148
+ });
31149
+ async executeCommand() {
31150
+ return runCompanionInstallCommand(
31151
+ this.context.stdout,
31152
+ () => convergeCompanionFromChannel(installCompanion, this.from, this.resourceGroup)
31153
+ );
31154
+ }
31155
+ };
31156
+
31157
+ // src/lib/bootstrap-finalize.ts
31158
+ function resolveRepoRootMarker(args) {
31159
+ if (args.explicit !== void 0) return args.explicit;
31160
+ if (args.cwdIsCheckout) return args.cwd;
31161
+ if (args.existing !== null && args.existing !== "") return args.existing;
31162
+ return args.cwd;
31163
+ }
31164
+ async function looksLikeCheckout(dir) {
31165
+ try {
31166
+ return (await fs34.stat(path38.join(dir, "brain-template"))).isDirectory();
31167
+ } catch {
31168
+ return false;
31169
+ }
31170
+ }
31171
+ var defaultDeps2 = {
31172
+ discoverGateway,
31173
+ writeConfig,
31174
+ ensureGatewayRedirectUri,
31175
+ runUsagePhase,
31176
+ reactiveSeed: reactiveSeedOnInstallComplete,
31177
+ companionsSupportHost,
31178
+ convergeCompanion: () => convergeCompanionFromChannel(installCompanion),
31179
+ homedir: () => os18.homedir()
31180
+ };
31181
+ async function finalizeInstall(args, deps = defaultDeps2) {
31182
+ const markerDir = path38.join(deps.homedir(), ".m8t");
31183
+ const markerPath = path38.join(markerDir, "repo-root");
31184
+ const cwd = process.cwd();
31185
+ const existing = await fs34.readFile(markerPath, "utf8").then((s) => s.trim()).catch(() => null);
31186
+ const repoRoot = resolveRepoRootMarker({
31187
+ ...args.repoRoot !== void 0 ? { explicit: args.repoRoot } : {},
31188
+ cwd,
31189
+ cwdIsCheckout: await looksLikeCheckout(cwd),
31190
+ existing
31191
+ });
31192
+ await fs34.mkdir(markerDir, { recursive: true });
31193
+ await fs34.writeFile(markerPath, `${repoRoot}
31194
+ `, "utf8");
31195
+ let webappUrl;
31196
+ try {
31197
+ const d = await deps.discoverGateway({ subscriptionId: args.subscriptionId, interactive: false, resourceGroup: args.resourceGroup });
31198
+ await deps.writeConfig({
31199
+ gatewayUrl: d.gatewayUrl,
31200
+ gatewayClientId: d.gatewayClientId,
31201
+ gatewayTenantId: d.gatewayTenantId,
31202
+ subscriptionId: d.subscriptionId,
31203
+ containerAppResourceId: d.containerAppResourceId,
31204
+ cachedAt: (/* @__PURE__ */ new Date()).toISOString()
31205
+ });
31206
+ webappUrl = d.gatewayUrl;
31207
+ args.stdout(
31208
+ `${colors.success("\u2705 The platform is live and your local tools are pointed at it.")}
31209
+ gateway: ${d.gatewayUrl}
31210
+ foundry: ${args.doc.result?.foundryEndpoint ?? "-"}
31211
+
31212
+ `
31213
+ );
31214
+ try {
31215
+ await deps.ensureGatewayRedirectUri(d.gatewayClientId, new URL(d.gatewayUrl).host);
31216
+ } catch (e) {
31217
+ const err = e;
31218
+ args.stderr(
31219
+ ` ${colors.error("\u26A0 could not authorize sign-in for your webapp:")} ${err.message}
31220
+ ${colors.hint(`Until this is fixed, opening ${d.gatewayUrl} will fail with AADSTS50011 (redirect URI mismatch).`)}
31221
+ ${colors.hint("Ask a directory admin to run:")}
31222
+ ${colors.hint(` m8t bootstrap status # (as an admin), or by hand:`)}
31223
+ ${colors.hint(` az ad app update --id ${d.gatewayClientId} --set spa.redirectUris="['${d.gatewayUrl}']" # (merge, do not overwrite)`)}
31224
+
31225
+ `
31226
+ );
31227
+ }
31228
+ try {
31229
+ const verdict = await deps.runUsagePhase(
31230
+ buildPrereqDeps({ subscription: args.subscriptionId, resourceGroup: args.resourceGroup }),
31231
+ { fix: true, onProgress: (m) => {
31232
+ args.stdout(colors.dim(` ${m}
31233
+ `));
31234
+ } }
31235
+ );
31236
+ const rows = verdict.results.filter((r) => r.slug !== "signin-redirect-uri");
31237
+ const unresolved = rows.filter((r) => r.status === "fail");
31238
+ if (unresolved.length === 0) {
31239
+ args.stdout(` ${colors.success("\u2713 your account can reach Foundry and read the platform Key Vault")}
31240
+
31241
+ `);
31242
+ }
31243
+ for (const r of unresolved) {
31244
+ args.stderr(
31245
+ ` ${colors.error("\u26A0 could not grant you access:")} ${r.detail}
31246
+ ` + (r.remedy ? ` ${colors.hint(`Fix it with: ${r.remedy}`)}
31247
+ ` : "") + "\n"
31248
+ );
31249
+ }
31250
+ } catch (e) {
31251
+ args.stderr(
31252
+ ` ${colors.error("\u26A0 could not check or grant your platform access:")} ${e.message}
31253
+ ${colors.hint("Run 'm8t prereqs --fix' when you can \u2014 until then your AI team will not load and worker deploys will fail.")}
31254
+
31255
+ `
31256
+ );
31257
+ }
31258
+ } catch (e) {
31259
+ const err = e;
31260
+ args.stdout(`${colors.success("\u2705 The platform is live.")}
31261
+
31262
+ `);
31263
+ args.stderr(
31264
+ ` ${colors.hint(`\u26A0 couldn't auto-point your local tools at the gateway: ${err.message}`)}
31265
+ ` + (err.hint ? ` ${colors.hint(err.hint)}
31266
+ ` : "") + "\n"
31267
+ );
31268
+ }
31269
+ if (webappUrl !== void 0 && deps.companionsSupportHost()) {
31270
+ try {
31271
+ const companion = await deps.convergeCompanion();
31272
+ if (companion.state === "installed") {
31273
+ args.stdout("Desktop companions installed: Stacey and Azzy are ready.\n\n");
31274
+ } else if (companion.state !== "not-released") {
31275
+ throw new Error("companion installation is not verified");
31276
+ }
31277
+ } catch (e) {
31278
+ const reason = e instanceof Error ? e.message : String(e);
31279
+ args.stdout(
31280
+ `Platform installation succeeded, but the desktop companions need repair.
31281
+ Run: m8t companion repair
31282
+ ${colors.hint(reason)}
31283
+
31284
+ `
31285
+ );
31286
+ }
31287
+ }
31288
+ let brainOrg = null;
31289
+ try {
31290
+ const credsRaw = await fs34.readFile(path38.join(markerDir, "github-app.json"), "utf8");
31291
+ const creds = JSON.parse(credsRaw);
31292
+ brainOrg = typeof creds.org === "string" ? creds.org : null;
31293
+ } catch {
31294
+ }
31295
+ args.stdout(renderInstallSummary({ webappUrl, brainOrg }) + "\n");
31296
+ args.stdout(
31297
+ `${colors.field("Optional local tooling for your coding agent:")}
31298
+ \u2022 Talk to your deployed workers from agent sessions (e.g. /stacey) \u2014 install the m8t plugin: guides/install/m8t-plugin.md.
31299
+ \u2022 Azure-capable MCP servers (microsoft/azure-skills, Microsoft Learn) \u2014 see the optional steps in guides/install.md.
31300
+ \u2022 Deploy workers conversationally with the opt-in persona skills \u2014 see guides/workers.md.
31301
+ \u2022 Make sure your m8t CLI is current: npm i -g @m8t-stack/cli@latest
31302
+
31303
+ ${colors.field("Then open a brand new chat/session")} \u2014 new skills + MCP servers load only on a fresh start.
31304
+ `
31305
+ );
31306
+ try {
31307
+ await deps.reactiveSeed({
31308
+ endpoint: args.doc.result?.foundryEndpoint,
31309
+ subscriptionId: args.subscriptionId,
31310
+ stdout: args.stdout
31311
+ });
31312
+ } catch (e) {
31313
+ args.stderr(
31314
+ ` ${colors.dim(`(company-profile seed deferred: ${e instanceof Error ? e.message : String(e)} \u2014 run 'm8t bootstrap seed-profile' later)`)}
31315
+ `
31316
+ );
31317
+ }
31318
+ }
31319
+
31320
+ // src/commands/bootstrap/status.ts
31321
+ var BootstrapStatusCommand = class extends M8tCommand {
31322
+ // `bootstrap finish` is a DEPRECATED ALIAS, not a second command. Everything it
31323
+ // used to do either moved into the cloud installer (the founder's data-plane
31324
+ // grant, now at entrypoint.sh step 3b) or into this command's own `done`
31325
+ // handling. The path stays registered because it is written down in guides,
31326
+ // runbooks and shakedown recipes that a founder may already be part-way
31327
+ // through — it prints a deprecation notice and does the right thing.
31328
+ static paths = [["bootstrap", "status"], ["bootstrap", "finish"]];
31329
+ static usage = Command56.Usage({
31330
+ description: "Show the cloud installer's live status (phase, progress, result) \u2014 and finish the local setup when it lands.",
31331
+ details: "Reads the durable status blob written by the installer. --watch polls until the install reaches done or failed.\n\nOn reaching done under --watch this also completes the local half of the install, which nothing else in the bootstrap path can do: it registers the webapp's sign-in redirect URI (the installer runs as a managed identity with no directory role, and at launch time the gateway FQDN did not exist yet), writes ~/.m8t/repo-root and the gateway discovery cache, and seeds your advisors' brains from the onboarding intake. Re-runnable \u2014 `m8t bootstrap finish` is a deprecated alias that does exactly this on an already-done install.",
31332
+ examples: [
31333
+ ["One read", "$0 bootstrap status"],
31334
+ ["Watch to completion, then finish the local setup", "$0 bootstrap status --watch"],
31335
+ ["Redo the local setup on a finished install", "$0 bootstrap status --finalize --repo-root /path/to/m8t"]
31336
+ ]
31337
+ });
31338
+ watch = Option53.Boolean("--watch", false);
31339
+ output = Option53.String("--output");
31340
+ repoRoot = Option53.String("--repo-root", { description: "The m8t clone to point local tools at (default: the current directory)." });
31341
+ finalize = Option53.Boolean("--finalize", false, { description: "Complete the local setup against an already-done install, without watching." });
31342
+ subscription = Option53.String("--subscription");
31343
+ resourceGroup = Option53.String("--resource-group");
31344
+ async executeCommand() {
31345
+ const state = await readBootstrapState();
31346
+ if (!state) {
31347
+ throw new LocalCliError({
31348
+ code: "BOOTSTRAP_NO_STATE",
31349
+ message: "No bootstrap is in flight (no ~/.m8t/bootstrap.json).",
31350
+ hint: "Run 'm8t bootstrap launch --location <region>' first."
31351
+ });
31352
+ }
31353
+ const mode = resolveOutputMode(
31354
+ typeof this.output === "string" ? this.output : void 0,
31355
+ this.context.stdout
31356
+ );
31357
+ const sleep5 = (ms) => new Promise((r) => setTimeout(r, ms));
31358
+ const watch = this.watch === true;
31359
+ const invokedPath = this.path ?? [];
31360
+ const viaFinishAlias = invokedPath[invokedPath.length - 1] === "finish";
31361
+ if (viaFinishAlias) {
31362
+ this.context.stderr.write(
31363
+ ` ${colors.hint("note:")} ${colors.dim("`m8t bootstrap finish` is deprecated \u2014 `m8t bootstrap status --watch` now finishes the local setup itself when the install lands.")}
31364
+ `
31365
+ );
31366
+ }
31367
+ const wantsFinalize = watch || viaFinishAlias || this.finalize === true;
31368
+ for (; ; ) {
31369
+ let doc;
31370
+ try {
31371
+ doc = await readStatusBlob({ saName: state.statusSaName, resourceGroup: state.resourceGroup, subscriptionId: state.subscriptionId });
31372
+ } catch (e) {
31373
+ if (watch && e instanceof LocalCliError && e.code === "BOOTSTRAP_STATUS_UNREADABLE") {
31374
+ const early = await getAciState({
31375
+ aciName: state.aciName,
31376
+ resourceGroup: state.resourceGroup,
31377
+ subscriptionId: state.subscriptionId
31378
+ }).catch(() => null);
31379
+ if (early?.terminated) {
31380
+ this.context.stderr.write(
31381
+ ` ${colors.error("\u2717")} the installer container has terminated (exit ${String(early.exitCode ?? "?")}) and never reported a status.
31382
+ ${colors.hint("inspect:")} az container logs -n ${state.aciName} -g ${state.resourceGroup}
31383
+ ${colors.hint("reap:")} m8t bootstrap reap --force
31384
+ `
31385
+ );
31386
+ return 1;
31387
+ }
31388
+ if (mode === "pretty") this.context.stderr.write(` ${colors.dim("waiting for the installer to start\u2026")}
31389
+ `);
31390
+ await sleep5(1e4);
31391
+ continue;
31392
+ }
31393
+ throw e;
31394
+ }
31395
+ if (!watch || doc.status !== "running") {
31396
+ if (mode === "json") {
31397
+ this.context.stdout.write(renderJson(doc) + "\n");
31398
+ } else {
31399
+ this.context.stdout.write(formatStatus(doc));
31400
+ }
31401
+ if (wantsFinalize) {
31402
+ if (doc.status === "done") {
31403
+ await this.runFinalize(doc, state.subscriptionId, state.resourceGroup, mode);
31404
+ } else if (this.finalize === true || viaFinishAlias) {
31405
+ throw new LocalCliError({
31406
+ code: "BOOTSTRAP_FINISH_NOT_DONE",
31407
+ message: `The platform is not live yet (status: ${doc.status}, phase: ${doc.phase}).`,
31408
+ hint: "Wait for 'm8t bootstrap status --watch' to reach done \u2014 it finishes the local setup itself."
31409
+ });
31410
+ }
31411
+ }
31412
+ return doc.status === "failed" ? 1 : 0;
31413
+ }
31414
+ if (mode === "pretty") this.context.stderr.write(` ${colors.dim(`${doc.phase} (${String(doc.phaseIndex)}/${String(doc.phaseTotal)}) ${doc.detail ?? ""}`)}
31415
+ `);
31416
+ const aci = await getAciState({ aciName: state.aciName, resourceGroup: state.resourceGroup, subscriptionId: state.subscriptionId }).catch(() => null);
31417
+ if (aci?.terminated) {
31418
+ this.context.stderr.write(
31419
+ ` ${colors.error("\u2717")} the installer container has terminated (exit ${String(aci.exitCode ?? "?")}) but the status is still '${doc.phase}'.
31420
+ ${colors.hint("inspect:")} az container logs -n ${state.aciName} -g ${state.resourceGroup}
31421
+ ${colors.hint("reap:")} m8t bootstrap reap --force
31422
+ `
31423
+ );
31424
+ return 1;
31425
+ }
31426
+ await sleep5(1e4);
31427
+ }
31428
+ }
31429
+ /**
31430
+ * Complete the local half of the install. Never throws: the platform is live,
31431
+ * and a failure to point one machine at it must not turn a successful install
31432
+ * into a non-zero exit — every step inside reports its own failure with the
31433
+ * remedy, and the outer catch covers the ones that cannot.
31434
+ *
31435
+ * In `--output json` mode stdout carries the status document and nothing else,
31436
+ * so all of finalize's prose is routed to stderr rather than corrupting it.
31437
+ */
31438
+ async runFinalize(doc, subscriptionId, resourceGroup, mode) {
31439
+ const toStderr = (s) => this.context.stderr.write(s);
31440
+ try {
31441
+ await finalizeInstall({
31442
+ doc,
31443
+ subscriptionId: (typeof this.subscription === "string" ? this.subscription : void 0) ?? subscriptionId,
31444
+ resourceGroup: (typeof this.resourceGroup === "string" ? this.resourceGroup : void 0) ?? resourceGroup,
31445
+ ...typeof this.repoRoot === "string" ? { repoRoot: this.repoRoot } : {},
31446
+ stdout: mode === "json" ? toStderr : (s) => this.context.stdout.write(s),
31447
+ stderr: toStderr
31448
+ });
31449
+ } catch (e) {
31450
+ this.context.stderr.write(
31451
+ ` ${colors.error("\u26A0 the platform is live, but the local setup did not complete:")} ${e.message}
31452
+ ${colors.hint("retry:")} m8t bootstrap status --finalize
31453
+ `
31454
+ );
31455
+ }
31456
+ }
31457
+ };
31458
+ function formatStatus(d) {
31459
+ const head = `${d.status.toUpperCase()} \u2014 ${d.phase} (${String(d.phaseIndex)}/${String(d.phaseTotal)})${d.detail ? ` \u2014 ${d.detail}` : ""}
31460
+ `;
31461
+ if (d.status === "done" && d.result) {
31462
+ return colors.success("\u2713 ") + head + ` gateway: ${d.result.gatewayUrl ?? "-"}
31463
+ foundry: ${d.result.foundryEndpoint ?? "-"}
31464
+ ${colors.hint("next:")} m8t bootstrap reap
31465
+ `;
31466
+ }
31467
+ if (d.status === "failed" && d.error) {
31468
+ return colors.error("\u2717 ") + head + ` ${colors.error(`error in ${d.error.phase}: ${d.error.message}`)}
31469
+ ${colors.hint("inspect:")} az container logs
31470
+ `;
31471
+ }
31472
+ return head;
31473
+ }
31474
+
31475
+ // src/commands/bootstrap/reap.ts
31476
+ import { Command as Command57, Option as Option54 } from "clipanion";
31477
+ init_errors();
31478
+
31479
+ // src/lib/bootstrap-reap.ts
31480
+ var ROLE_ASSIGNMENT_API = "2022-04-01";
31481
+ var GATEWAY_SUBSCOPE_ROLES = ["Cost Management Reader", "Monitoring Reader"];
31482
+ async function listSubscriptionAssignments(subscriptionId) {
31483
+ return JSON.parse(
31484
+ await runAz([
31485
+ "role",
31486
+ "assignment",
31487
+ "list",
31488
+ "--all",
31489
+ "--subscription",
31490
+ subscriptionId,
31491
+ "-o",
31492
+ "json"
31493
+ ]).catch(() => "[]") || "[]"
31494
+ );
31495
+ }
31496
+ async function deleteAssignmentsById(ids) {
31497
+ for (const id of ids) {
31498
+ await runAz([
31499
+ "rest",
31500
+ "--method",
31501
+ "delete",
31502
+ "--url",
31503
+ `https://management.azure.com${id}?api-version=${ROLE_ASSIGNMENT_API}`
31504
+ ]).catch(() => void 0);
31505
+ }
31506
+ }
31507
+ async function sweepOrphanGatewaySubRoles(opts) {
31508
+ const subScope = `/subscriptions/${opts.subscriptionId}`;
31509
+ const all = await listSubscriptionAssignments(opts.subscriptionId);
31510
+ const matched = all.filter((r) => {
31511
+ if (r.principalName) return false;
31512
+ if (r.principalType !== "ServicePrincipal") return false;
31513
+ if (r.scope !== subScope) return false;
31514
+ if (r.description === GATEWAY_SUBSCOPE_GRANT_DESCRIPTION) return true;
31515
+ return GATEWAY_SUBSCOPE_ROLES.includes(r.roleDefinitionName ?? "");
31516
+ });
31517
+ if (opts.execute) await deleteAssignmentsById(matched.map((r) => r.id));
31518
+ return matched.map((r) => ({ id: r.id, description: r.description, roleDefinitionName: r.roleDefinitionName, createdOn: r.createdOn }));
31519
+ }
31520
+ async function sweepOrphanOwnerAssignments(opts) {
31521
+ const subScope = `/subscriptions/${opts.subscriptionId}`;
31522
+ const all = await listSubscriptionAssignments(opts.subscriptionId);
31523
+ const matched = all.filter((r) => {
31524
+ if (r.description === INSTALLER_GRANT_DESCRIPTION) return true;
31525
+ return !r.principalName && r.principalType === "ServicePrincipal" && r.roleDefinitionName === "Owner" && r.scope === subScope;
31526
+ });
31527
+ if (opts.execute) await deleteAssignmentsById(matched.map((r) => r.id));
31528
+ return matched.map((r) => ({ id: r.id, description: r.description, createdOn: r.createdOn }));
31529
+ }
31530
+ async function reapInstaller(opts) {
31531
+ const swallow = async (p) => {
31532
+ try {
31533
+ await p;
31534
+ } catch {
31535
+ }
31536
+ };
31537
+ await swallow(runAz(["container", "delete", "--name", opts.aciName, "--resource-group", opts.resourceGroup, "--subscription", opts.subscriptionId, "--yes", "--only-show-errors"]));
31538
+ await swallow(runAz(["identity", "delete", "--name", opts.miName, "--resource-group", opts.resourceGroup, "--subscription", opts.subscriptionId]));
31539
+ for (const id of opts.roleAssignmentIds) {
31540
+ await swallow(runAz([
31541
+ "rest",
31542
+ "--method",
31543
+ "delete",
31544
+ "--url",
31545
+ `https://management.azure.com${id}?api-version=${ROLE_ASSIGNMENT_API}`
31546
+ ]));
31291
31547
  }
31292
- stdout.write(
31293
- `Desktop companions installed: Stacey and Azzy are ready.
31294
- Version: ${state.version}
31295
- `
31548
+ const survivors = JSON.parse(
31549
+ await runAz([
31550
+ "role",
31551
+ "assignment",
31552
+ "list",
31553
+ "--all",
31554
+ "--subscription",
31555
+ opts.subscriptionId,
31556
+ "--query",
31557
+ `[?principalId=='${opts.principalId}' && roleDefinitionName=='Owner'].id`,
31558
+ "-o",
31559
+ "json"
31560
+ ]).catch(() => "[]") || "[]"
31296
31561
  );
31297
- return 0;
31562
+ for (const id of survivors) {
31563
+ await swallow(runAz(["rest", "--method", "delete", "--url", `https://management.azure.com${id}?api-version=${ROLE_ASSIGNMENT_API}`]));
31564
+ }
31298
31565
  }
31299
- var CompanionInstallCommand = class extends M8tCommand {
31300
- static paths = [["companion", "install"]];
31566
+
31567
+ // src/commands/bootstrap/reap.ts
31568
+ var BootstrapReapCommand = class extends M8tCommand {
31569
+ static paths = [["bootstrap", "reap"]];
31301
31570
  static usage = Command57.Usage({
31302
- description: "Install the desktop companions for this user from the release channel.",
31571
+ description: "Tear down the installer scaffolding (ACI \u2192 MI \u2192 its role assignments) after a successful install.",
31572
+ details: "Runs locally on the 'done' signal (the installer can't delete its own identity). The platform RG and the gateway's own assignments persist. A failed install is left intact for diagnosis unless --force.\n\n--sweep-orphans is a DIFFERENT and much broader mode: instead of reaping this install, it scans the WHOLE SUBSCRIPTION for m8t role assignments left behind by installs whose resources are gone \u2014 orphaned installer Owner-at-subscription-scope grants and orphaned gateway subscription-scope roles. It is a dry run that only lists what it found unless you also pass --yes, which deletes them.",
31303
31573
  examples: [
31304
- ["Install the released build", "$0 companion install"],
31305
- [
31306
- "Install one built from this clone",
31307
- "$0 companion install --from apps/companion/dist-artifacts/darwin-arm64"
31308
- ]
31574
+ ["Reap after done", "$0 bootstrap reap"],
31575
+ ["Reap a failed install anyway", "$0 bootstrap reap --force"],
31576
+ ["List orphaned role assignments across the subscription", "$0 bootstrap reap --sweep-orphans"],
31577
+ ["\u2026and delete them", "$0 bootstrap reap --sweep-orphans --yes"]
31309
31578
  ]
31310
31579
  });
31311
- from = Option54.String("--from", {
31312
- description: "A locally staged build directory instead of the released one."
31313
- });
31314
- async executeCommand() {
31315
- return runCompanionInstallCommand(
31316
- this.context.stdout,
31317
- () => convergeCompanionFromChannel(installCompanion, this.from)
31318
- );
31319
- }
31320
- };
31321
-
31322
- // src/commands/bootstrap/finish.ts
31323
- var BootstrapFinishCommand = class extends M8tCommand {
31324
- static paths = [["bootstrap", "finish"]];
31325
- static usage = Command58.Usage({
31326
- description: "Point your local tools at the now-live platform (repo-root marker, discovery cache, next steps).",
31327
- details: "Final step of `m8t bootstrap` (after the install reaches done). Writes ~/.m8t/repo-root, points your local tools at the live gateway, and \u2014 if you completed the onboarding intake \u2014 seeds your brain-backed advisor's brain with your company profile (best-effort; never blocks finish). Run `m8t bootstrap seed-profile` to seed manually later.",
31328
- examples: [["Finish local", "$0 bootstrap finish --repo-root /path/to/m8t"]]
31329
- });
31330
- repoRoot = Option55.String("--repo-root");
31331
- subscription = Option55.String("--subscription");
31332
- resourceGroup = Option55.String("--resource-group");
31580
+ force = Option54.Boolean("--force", false, { description: "Reap even if the install failed or never reported a status." });
31581
+ sweepOrphans = Option54.Boolean("--sweep-orphans", false, { description: "Subscription-wide: find m8t role assignments orphaned by earlier installs. Lists only, unless --yes." });
31582
+ yes = Option54.Boolean("--yes", false, { description: "With --sweep-orphans, actually delete what the sweep found." });
31333
31583
  async executeCommand() {
31334
- const state = await readBootstrapState();
31335
- if (!state) {
31336
- throw new LocalCliError({ code: "BOOTSTRAP_NO_STATE", message: "No bootstrap state.", hint: "Run 'm8t bootstrap launch' first." });
31337
- }
31338
- const doc = await readStatusBlob({ saName: state.statusSaName, resourceGroup: state.resourceGroup, subscriptionId: state.subscriptionId });
31339
- if (doc.status !== "done") {
31340
- throw new LocalCliError({
31341
- code: "BOOTSTRAP_FINISH_NOT_DONE",
31342
- message: `The platform is not live yet (status: ${doc.status}, phase: ${doc.phase}).`,
31343
- hint: "Wait for 'm8t bootstrap status --watch' to reach done."
31344
- });
31345
- }
31346
- const repoRoot = (typeof this.repoRoot === "string" ? this.repoRoot : void 0) ?? process.cwd();
31347
- const markerDir = path38.join(os18.homedir(), ".m8t");
31348
- await fs34.mkdir(markerDir, { recursive: true });
31349
- await fs34.writeFile(path38.join(markerDir, "repo-root"), `${repoRoot}
31350
- `, "utf8");
31351
- const subscriptionId = (typeof this.subscription === "string" ? this.subscription : void 0) ?? state.subscriptionId;
31352
- const resourceGroup = (typeof this.resourceGroup === "string" ? this.resourceGroup : void 0) ?? state.resourceGroup;
31353
- let webappUrl;
31354
- try {
31355
- const d = await discoverGateway({ subscriptionId, interactive: false, resourceGroup });
31356
- await writeConfig({
31357
- gatewayUrl: d.gatewayUrl,
31358
- gatewayClientId: d.gatewayClientId,
31359
- gatewayTenantId: d.gatewayTenantId,
31360
- subscriptionId: d.subscriptionId,
31361
- containerAppResourceId: d.containerAppResourceId,
31362
- cachedAt: (/* @__PURE__ */ new Date()).toISOString()
31363
- });
31364
- webappUrl = d.gatewayUrl;
31365
- this.context.stdout.write(
31366
- `${colors.success("\u2705 The platform is live and your local tools are pointed at it.")}
31367
- gateway: ${d.gatewayUrl}
31368
- foundry: ${doc.result?.foundryEndpoint ?? "-"}
31369
-
31370
- `
31371
- );
31372
- try {
31373
- await ensureGatewayRedirectUri(d.gatewayClientId, new URL(d.gatewayUrl).host);
31374
- } catch (e) {
31375
- const err = e;
31376
- this.context.stderr.write(
31377
- ` ${colors.error("\u26A0 could not authorize sign-in for your webapp:")} ${err.message}
31378
- ${colors.hint(`Until this is fixed, opening ${d.gatewayUrl} will fail with AADSTS50011 (redirect URI mismatch).`)}
31379
- ${colors.hint("Ask a directory admin to run:")}
31380
- ${colors.hint(` m8t bootstrap finish --repo-root ${repoRoot} # (as an admin), or by hand:`)}
31381
- ${colors.hint(` az ad app update --id ${d.gatewayClientId} --set spa.redirectUris="['${d.gatewayUrl}']" # (merge, do not overwrite)`)}
31382
-
31383
- `
31384
- );
31584
+ if (this.sweepOrphans === true) {
31585
+ const { subscriptionId: sub } = await getAzAccount();
31586
+ if (!sub) {
31587
+ throw new LocalCliError({ code: "BOOTSTRAP_SWEEP_NO_SUB", message: "No active subscription.", hint: "Run 'az account set --subscription <id>'." });
31385
31588
  }
31386
- try {
31387
- const verdict = await runUsagePhase(
31388
- buildPrereqDeps({ subscription: subscriptionId, resourceGroup }),
31389
- { fix: true, onProgress: (m) => {
31390
- this.context.stdout.write(colors.dim(` ${m}
31391
- `));
31392
- } }
31393
- );
31394
- const rows = verdict.results.filter((r) => r.slug !== "signin-redirect-uri");
31395
- const unresolved = rows.filter((r) => r.status === "fail");
31396
- if (unresolved.length === 0) {
31397
- this.context.stdout.write(` ${colors.success("\u2713 your account can reach Foundry and read the platform Key Vault")}
31398
-
31589
+ const execute = this.yes === true;
31590
+ const owner = await sweepOrphanOwnerAssignments({ subscriptionId: sub, execute });
31591
+ const gateway = await sweepOrphanGatewaySubRoles({ subscriptionId: sub, execute });
31592
+ const total = owner.length + gateway.length;
31593
+ if (total === 0) {
31594
+ this.context.stdout.write(`${colors.success("\u2713")} No orphaned installer Owner@sub or gateway sub-scope assignments found.
31399
31595
  `);
31400
- }
31401
- for (const r of unresolved) {
31402
- this.context.stderr.write(
31403
- ` ${colors.error("\u26A0 could not grant you access:")} ${r.detail}
31404
- ` + (r.remedy ? ` ${colors.hint(`Fix it with: ${r.remedy}`)}
31405
- ` : "") + "\n"
31406
- );
31407
- }
31408
- } catch (e) {
31409
- this.context.stderr.write(
31410
- ` ${colors.error("\u26A0 could not check or grant your platform access:")} ${e.message}
31411
- ${colors.hint("Run 'm8t prereqs --fix' when you can \u2014 until then your AI team will not load and worker deploys will fail.")}
31412
-
31413
- `
31414
- );
31596
+ return 0;
31415
31597
  }
31416
- } catch (e) {
31417
- const err = e;
31418
- this.context.stdout.write(`${colors.success("\u2705 The platform is live.")}
31419
-
31598
+ for (const f of owner) {
31599
+ this.context.stdout.write(` ${colors.dim("Owner@sub")} ${f.id}${f.createdOn ? colors.dim(` (created ${f.createdOn})`) : ""}${f.description ? colors.dim(" [tagged]") : ""}
31420
31600
  `);
31421
- this.context.stderr.write(
31422
- ` ${colors.hint(`\u26A0 couldn't auto-point your local tools at the gateway: ${err.message}`)}
31423
- ` + (err.hint ? ` ${colors.hint(err.hint)}
31424
- ` : "") + "\n"
31425
- );
31601
+ }
31602
+ for (const f of gateway) {
31603
+ this.context.stdout.write(` ${colors.dim(f.roleDefinitionName ?? "gateway sub-scope")} ${f.id}${f.createdOn ? colors.dim(` (created ${f.createdOn})`) : ""}${f.description ? colors.dim(" [tagged]") : ""}
31604
+ `);
31605
+ }
31606
+ const breakdown = `${String(owner.length)} Owner@sub, ${String(gateway.length)} gateway sub-scope`;
31607
+ if (execute) {
31608
+ this.context.stdout.write(`${colors.success("\u2713")} Removed ${String(total)} orphaned assignment(s) (${breakdown}).
31609
+ `);
31610
+ } else {
31611
+ this.context.stdout.write(`
31612
+ Found ${String(total)} orphaned assignment(s) (${breakdown}) (dry-run). ${colors.hint("Re-run with --sweep-orphans --yes to delete them.")}
31613
+ `);
31614
+ }
31615
+ return 0;
31426
31616
  }
31427
- if (webappUrl !== void 0 && companionsSupportHost()) {
31428
- try {
31429
- const companion = await convergeCompanionFromChannel(installCompanion);
31430
- if (companion.state === "installed") {
31431
- this.context.stdout.write(
31432
- "Desktop companions installed: Stacey and Azzy are ready.\n\n"
31433
- );
31434
- } else if (companion.state !== "not-released") {
31435
- throw new Error("companion installation is not verified");
31617
+ const state = await readBootstrapState();
31618
+ if (!state) {
31619
+ if (this.force === true) {
31620
+ const { subscriptionId: sub } = await getAzAccount();
31621
+ if (!sub) {
31622
+ throw new LocalCliError({ code: "BOOTSTRAP_SWEEP_NO_SUB", message: "No active subscription.", hint: "Run 'az account set --subscription <id>'." });
31436
31623
  }
31437
- } catch (e) {
31438
- const reason = e instanceof Error ? e.message : String(e);
31624
+ const owner = await sweepOrphanOwnerAssignments({ subscriptionId: sub, execute: true });
31625
+ const gateway = await sweepOrphanGatewaySubRoles({ subscriptionId: sub, execute: true });
31626
+ const total = owner.length + gateway.length;
31439
31627
  this.context.stdout.write(
31440
- `Platform installation succeeded, but the desktop companions need repair.
31441
- Run: m8t companion repair
31442
- ${colors.hint(reason)}
31443
-
31628
+ total === 0 ? `${colors.success("\u2713")} No bootstrap state and no orphaned installer Owner@sub or gateway sub-scope assignments. Nothing to reap.
31629
+ ` : `${colors.success("\u2713")} No bootstrap state found; swept ${String(total)} orphaned assignment(s) (${String(owner.length)} Owner@sub, ${String(gateway.length)} gateway sub-scope).
31444
31630
  `
31445
31631
  );
31632
+ return 0;
31446
31633
  }
31447
- }
31448
- let brainOrg = null;
31449
- try {
31450
- const credsRaw = await fs34.readFile(path38.join(markerDir, "github-app.json"), "utf8");
31451
- const creds = JSON.parse(credsRaw);
31452
- brainOrg = typeof creds.org === "string" ? creds.org : null;
31453
- } catch {
31454
- }
31455
- this.context.stdout.write(renderInstallSummary({ webappUrl, brainOrg }) + "\n");
31456
- this.context.stdout.write(
31457
- `${colors.field("Optional local tooling for your coding agent:")}
31458
- \u2022 Talk to your deployed workers from agent sessions (e.g. /stacey) \u2014 install the m8t plugin: guides/install/m8t-plugin.md.
31459
- \u2022 Azure-capable MCP servers (microsoft/azure-skills, Microsoft Learn) \u2014 see the optional steps in guides/install.md.
31460
- \u2022 Deploy workers conversationally with the opt-in persona skills \u2014 see guides/workers.md.
31461
- \u2022 Make sure your m8t CLI is current: npm i -g @m8t-stack/cli@latest
31462
-
31463
- ${colors.field("Then open a brand new chat/session")} \u2014 new skills + MCP servers load only on a fresh start.
31464
- `
31465
- );
31466
- try {
31467
- await reactiveSeedOnFinish({
31468
- endpoint: doc.result?.foundryEndpoint,
31469
- subscriptionId,
31470
- stdout: (s) => this.context.stdout.write(s)
31634
+ throw new LocalCliError({
31635
+ code: "BOOTSTRAP_NO_STATE",
31636
+ message: "No bootstrap state to reap.",
31637
+ hint: "Nothing to do. If a launch failed and its RG was deleted it may have orphaned an installer Owner@sub grant or the gateway's sub-scope reader roles \u2014 run 'm8t bootstrap reap --force' (or 'm8t bootstrap reap --sweep-orphans --yes') to clean them."
31471
31638
  });
31472
- } catch (e) {
31473
- this.context.stderr.write(
31474
- ` ${colors.dim(`(company-profile seed deferred: ${e instanceof Error ? e.message : String(e)} \u2014 run 'm8t bootstrap seed-profile' later)`)}
31475
- `
31476
- );
31477
31639
  }
31640
+ if (this.force !== true) {
31641
+ const doc = await readStatusBlob({ saName: state.statusSaName, resourceGroup: state.resourceGroup, subscriptionId: state.subscriptionId });
31642
+ if (doc.status !== "done") {
31643
+ if (doc.status === "failed") {
31644
+ throw new LocalCliError({
31645
+ code: "BOOTSTRAP_REAP_FAILED_INSTALL",
31646
+ message: `The install failed at '${doc.error?.phase ?? doc.phase}'. Leaving the installer up for diagnosis.`,
31647
+ hint: `Inspect: az container logs -n ${state.aciName} -g ${state.resourceGroup}. Reap anyway with --force.`
31648
+ });
31649
+ }
31650
+ const aci = await getAciState({ aciName: state.aciName, resourceGroup: state.resourceGroup, subscriptionId: state.subscriptionId }).catch(() => null);
31651
+ const deadHint = aci?.terminated ? `The installer container has terminated (exit ${String(aci.exitCode ?? "?")}) without reaching done \u2014 reap with --force.` : `Wait for 'm8t bootstrap status --watch' to reach done, or reap with --force if the install is stuck.`;
31652
+ throw new LocalCliError({
31653
+ code: "BOOTSTRAP_REAP_NOT_DONE",
31654
+ message: `The install is not done yet (status: ${doc.status}, phase: ${doc.phase}).`,
31655
+ hint: deadHint
31656
+ });
31657
+ }
31658
+ }
31659
+ await reapInstaller({
31660
+ subscriptionId: state.subscriptionId,
31661
+ resourceGroup: state.resourceGroup,
31662
+ aciName: state.aciName,
31663
+ miName: state.miName,
31664
+ roleAssignmentIds: state.roleAssignmentIds,
31665
+ principalId: state.miPrincipalId
31666
+ });
31667
+ this.context.stdout.write(`${colors.success("\u2713")} reaped the installer (ACI + identity + its role assignments). The platform stays.
31668
+ `);
31669
+ this.context.stdout.write(` ${colors.hint("next:")} m8t open ${colors.dim("# your local setup was completed by 'm8t bootstrap status --watch'")}
31670
+ `);
31478
31671
  return 0;
31479
31672
  }
31480
31673
  };
@@ -31483,7 +31676,7 @@ ${colors.field("Then open a brand new chat/session")} \u2014 new skills + MCP se
31483
31676
  import * as fs36 from "fs";
31484
31677
  import * as os20 from "os";
31485
31678
  import * as path40 from "path";
31486
- import { Command as Command59, Option as Option56 } from "clipanion";
31679
+ import { Command as Command58, Option as Option55 } from "clipanion";
31487
31680
  import { DefaultAzureCredential as DefaultAzureCredential24 } from "@azure/identity";
31488
31681
  init_errors();
31489
31682
 
@@ -32291,7 +32484,7 @@ function renderDeployFailure(error) {
32291
32484
  }
32292
32485
  var BootstrapUiCommand = class extends M8tCommand {
32293
32486
  static paths = [["bootstrap", "ui"]];
32294
- static usage = Command59.Usage({
32487
+ static usage = Command58.Usage({
32295
32488
  description: "Deploy Azzy + start the local onboarding chat UI in the background (returns immediately).",
32296
32489
  details: [
32297
32490
  "Run after `m8t bootstrap launch`, in parallel with `status --watch`. Waits for the cloud",
@@ -32312,16 +32505,16 @@ var BootstrapUiCommand = class extends M8tCommand {
32312
32505
  ["Experimental: start the voice relay (no effect on the text-only intake)", "$0 bootstrap ui --repo-root /path/to/m8t --voice"]
32313
32506
  ]
32314
32507
  });
32315
- repoRoot = Option56.String("--repo-root");
32316
- port = Option56.String("--port", "3000");
32317
- endpoint = Option56.String("--endpoint", {
32508
+ repoRoot = Option55.String("--repo-root");
32509
+ port = Option55.String("--port", "3000");
32510
+ endpoint = Option55.String("--endpoint", {
32318
32511
  description: "Foundry project endpoint to target \u2014 disambiguates when the subscription has multiple projects."
32319
32512
  });
32320
- prepOnly = Option56.Boolean("--prep-only", false);
32321
- skipInstall = Option56.Boolean("--skip-install", false);
32322
- stop = Option56.Boolean("--stop", false);
32323
- foreground = Option56.Boolean("--foreground", false);
32324
- voice = Option56.Boolean("--voice", false, {
32513
+ prepOnly = Option55.Boolean("--prep-only", false);
32514
+ skipInstall = Option55.Boolean("--skip-install", false);
32515
+ stop = Option55.Boolean("--stop", false);
32516
+ foreground = Option55.Boolean("--foreground", false);
32517
+ voice = Option55.Boolean("--voice", false, {
32325
32518
  description: "Experimental: when serving, starts the voice relay and writes the intake voice env var. The onboarding intake is text-only and is unaffected by this flag \u2014 no voice worker is registered for it."
32326
32519
  });
32327
32520
  async executeCommand() {
@@ -32483,10 +32676,10 @@ var BootstrapUiCommand = class extends M8tCommand {
32483
32676
  };
32484
32677
 
32485
32678
  // src/commands/bootstrap/seed-profile.ts
32486
- import { Command as Command60, Option as Option57 } from "clipanion";
32679
+ import { Command as Command59, Option as Option56 } from "clipanion";
32487
32680
  var BootstrapSeedProfileCommand = class extends M8tCommand {
32488
32681
  static paths = [["bootstrap", "seed-profile"]];
32489
- static usage = Command60.Usage({
32682
+ static usage = Command59.Usage({
32490
32683
  description: "Seed your advisors' brains with the founder + company profile from the onboarding intake.",
32491
32684
  details: "Reads the latest onboarding conversation, renders memory/founder.md + memory/company-profile.md (+ their MEMORY.md index lines), and commits them to both <org>/stacey-brain and <org>/azzy-brain via the GitHub App. Idempotent. --watch polls until the founder finishes the intake.",
32492
32685
  examples: [
@@ -32494,11 +32687,11 @@ var BootstrapSeedProfileCommand = class extends M8tCommand {
32494
32687
  ["Wait for the founder to finish", "$0 bootstrap seed-profile --watch"]
32495
32688
  ]
32496
32689
  });
32497
- endpoint = Option57.String("--endpoint", { description: "Override the Foundry endpoint (else read from the install status)." });
32498
- brain = Option57.String("--brain", { description: "Seed only this one brain repo, instead of both <org>/stacey-brain and <org>/azzy-brain." });
32499
- watch = Option57.Boolean("--watch", false, { description: "Poll until the intake completes (or --timeout)." });
32500
- timeout = Option57.String("--timeout", { description: "Watch timeout in minutes (default 20)." });
32501
- githubAppCreds = Option57.String("--github-app-creds");
32690
+ endpoint = Option56.String("--endpoint", { description: "Override the Foundry endpoint (else read from the install status)." });
32691
+ brain = Option56.String("--brain", { description: "Seed only this one brain repo, instead of both <org>/stacey-brain and <org>/azzy-brain." });
32692
+ watch = Option56.Boolean("--watch", false, { description: "Poll until the intake completes (or --timeout)." });
32693
+ timeout = Option56.String("--timeout", { description: "Watch timeout in minutes (default 20)." });
32694
+ githubAppCreds = Option56.String("--github-app-creds");
32502
32695
  async executeCommand() {
32503
32696
  const ctx = await resolveSeedContext({
32504
32697
  endpointOverride: typeof this.endpoint === "string" ? this.endpoint : void 0,
@@ -32561,7 +32754,7 @@ var BootstrapSeedProfileCommand = class extends M8tCommand {
32561
32754
  import * as fs37 from "fs";
32562
32755
  import * as os21 from "os";
32563
32756
  import * as path41 from "path";
32564
- import { Command as Command61, Option as Option58 } from "clipanion";
32757
+ import { Command as Command60, Option as Option57 } from "clipanion";
32565
32758
  init_errors();
32566
32759
 
32567
32760
  // src/lib/telemetry-enroll.ts
@@ -32643,7 +32836,7 @@ async function resolveKeyVaultName(containerAppResourceId) {
32643
32836
  }
32644
32837
  var TelemetryEnrollCommand = class extends M8tCommand {
32645
32838
  static paths = [["telemetry", "enroll"]];
32646
- static usage = Command61.Usage({
32839
+ static usage = Command60.Usage({
32647
32840
  description: "Enroll this installation for operational telemetry (pre-existing installs).",
32648
32841
  details: "Generates an instance id + ingest key from the m8t telemetry ingest and stores the key in your platform Key Vault, the same place the installer writes it on a fresh install. Your installation is identified only by that random instance id \u2014 no contact, company, or subscription details are sent unless you pass them explicitly. Idempotent: refuses if a key already exists.",
32649
32842
  examples: [
@@ -32651,11 +32844,11 @@ var TelemetryEnrollCommand = class extends M8tCommand {
32651
32844
  ["Enroll and share a support contact", "$0 telemetry enroll --contact-email you@example.com --company 'Acme'"]
32652
32845
  ]
32653
32846
  });
32654
- company = Option58.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
32655
- contactEmail = Option58.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
32656
- subscription = Option58.String("--subscription", { description: "Azure subscription id used to find your deployment. Never sent to m8t." });
32657
- resourceGroup = Option58.String("--resource-group", { description: "Resource group to disambiguate discovery, if you have more than one m8t deployment." });
32658
- force = Option58.Boolean("--force", false, { description: "Enroll even when a key is already stored. Use only when m8t support asks you to." });
32847
+ company = Option57.String("--company", { description: "Share your company name with m8t support. Not sent unless you pass it." });
32848
+ contactEmail = Option57.String("--contact-email", { description: "Share a contact email with m8t support. Not sent unless you pass it." });
32849
+ subscription = Option57.String("--subscription", { description: "Azure subscription id used to find your deployment. Never sent to m8t." });
32850
+ resourceGroup = Option57.String("--resource-group", { description: "Resource group to disambiguate discovery, if you have more than one m8t deployment." });
32851
+ force = Option57.Boolean("--force", false, { description: "Enroll even when a key is already stored. Use only when m8t support asks you to." });
32659
32852
  async executeCommand() {
32660
32853
  const account = await getAzAccount();
32661
32854
  const subscriptionId = (typeof this.subscription === "string" ? this.subscription : void 0) ?? account.subscriptionId;
@@ -32705,7 +32898,7 @@ var TelemetryEnrollCommand = class extends M8tCommand {
32705
32898
  };
32706
32899
 
32707
32900
  // src/commands/companion/bridge.ts
32708
- import { Command as Command62 } from "clipanion";
32901
+ import { Command as Command61 } from "clipanion";
32709
32902
 
32710
32903
  // ../../packages/companion-bridge-contract/src/index.ts
32711
32904
  var COMPANION_MESSAGE_MAX_CODE_POINTS = 32768;
@@ -32939,7 +33132,7 @@ function serializeEvent(value) {
32939
33132
  }
32940
33133
 
32941
33134
  // src/lib/companion-chat-client.ts
32942
- import { stat as stat3 } from "fs/promises";
33135
+ import { stat as stat4 } from "fs/promises";
32943
33136
  var RESPONSE_MAX_BYTES = 1e6;
32944
33137
  var REQUEST_DEADLINE_MS = 12e4;
32945
33138
  var OPAQUE_ID = /^[A-Za-z0-9_-]{1,256}$/u;
@@ -33113,7 +33306,7 @@ function createDeadline(deps) {
33113
33306
  }
33114
33307
  async function readInstallEpoch() {
33115
33308
  try {
33116
- const stats = await stat3(getConfigPath());
33309
+ const stats = await stat4(getConfigPath());
33117
33310
  const candidates = [stats.birthtimeMs, stats.mtimeMs].filter(
33118
33311
  (value) => Number.isFinite(value) && value > 0
33119
33312
  );
@@ -33305,7 +33498,7 @@ function companionUpdateAvailable(update) {
33305
33498
  }
33306
33499
 
33307
33500
  // src/commands/companion/bridge.ts
33308
- var defaultDeps2 = {
33501
+ var defaultDeps3 = {
33309
33502
  roster: rosterCompanions,
33310
33503
  send: sendCompanionMessage,
33311
33504
  update: () => checkCompanionUpdate(defaultLocalCompanionInstallOptions())
@@ -33337,7 +33530,7 @@ function exitFor(terminal) {
33337
33530
  }
33338
33531
  return 2;
33339
33532
  }
33340
- async function runCompanionBridge(stdin, stdout, stderr, deps = defaultDeps2) {
33533
+ async function runCompanionBridge(stdin, stdout, stderr, deps = defaultDeps3) {
33341
33534
  let request;
33342
33535
  try {
33343
33536
  request = parseRequestLine(await readSingleRequest(stdin));
@@ -33362,7 +33555,7 @@ async function runCompanionBridge(stdin, stdout, stderr, deps = defaultDeps2) {
33362
33555
  return 3;
33363
33556
  }
33364
33557
  }
33365
- var CompanionBridgeCommand = class extends Command62 {
33558
+ var CompanionBridgeCommand = class extends Command61 {
33366
33559
  static paths = [["companion", "_bridge"]];
33367
33560
  async execute() {
33368
33561
  return runCompanionBridge(
@@ -33374,7 +33567,7 @@ var CompanionBridgeCommand = class extends Command62 {
33374
33567
  };
33375
33568
 
33376
33569
  // src/commands/companion/status.ts
33377
- import { Command as Command63 } from "clipanion";
33570
+ import { Command as Command62 } from "clipanion";
33378
33571
  async function runCompanionStatusCommand(stdout, status, update) {
33379
33572
  const state = await status();
33380
33573
  if (state.state === "not-installed") {
@@ -33404,7 +33597,7 @@ Run: m8t companion install
33404
33597
  }
33405
33598
  var CompanionStatusCommand = class extends M8tCommand {
33406
33599
  static paths = [["companion", "status"]];
33407
- static usage = Command63.Usage({
33600
+ static usage = Command62.Usage({
33408
33601
  description: "Verify the installed desktop companion without launching it."
33409
33602
  });
33410
33603
  async executeCommand() {
@@ -33418,7 +33611,7 @@ var CompanionStatusCommand = class extends M8tCommand {
33418
33611
  };
33419
33612
 
33420
33613
  // src/commands/companion/repair.ts
33421
- import { Command as Command64 } from "clipanion";
33614
+ import { Command as Command63, Option as Option58 } from "clipanion";
33422
33615
  async function runCompanionRepairCommand(stdout, repair) {
33423
33616
  const state = await repair();
33424
33617
  if (state.state === "not-released") {
@@ -33437,19 +33630,22 @@ async function runCompanionRepairCommand(stdout, repair) {
33437
33630
  }
33438
33631
  var CompanionRepairCommand = class extends M8tCommand {
33439
33632
  static paths = [["companion", "repair"]];
33440
- static usage = Command64.Usage({
33441
- description: "Restore the internal desktop companion and start-at-login state."
33633
+ static usage = Command63.Usage({
33634
+ description: "Restore the desktop companions and start-at-login state."
33635
+ });
33636
+ resourceGroup = Option58.String("--resource-group", {
33637
+ description: "Which deployment to bind to, when the subscription holds more than one."
33442
33638
  });
33443
33639
  async executeCommand() {
33444
33640
  return runCompanionRepairCommand(
33445
33641
  this.context.stdout,
33446
- () => convergeCompanionFromChannel(repairCompanion)
33642
+ () => convergeCompanionFromChannel(repairCompanion, void 0, this.resourceGroup)
33447
33643
  );
33448
33644
  }
33449
33645
  };
33450
33646
 
33451
33647
  // src/commands/companion/uninstall.ts
33452
- import { Command as Command65 } from "clipanion";
33648
+ import { Command as Command64 } from "clipanion";
33453
33649
  async function runCompanionUninstallCommand(stdout, uninstall) {
33454
33650
  const state = await uninstall();
33455
33651
  if (state.state !== "not-installed") {
@@ -33461,7 +33657,7 @@ async function runCompanionUninstallCommand(stdout, uninstall) {
33461
33657
  }
33462
33658
  var CompanionUninstallCommand = class extends M8tCommand {
33463
33659
  static paths = [["companion", "uninstall"]];
33464
- static usage = Command65.Usage({
33660
+ static usage = Command64.Usage({
33465
33661
  description: "Remove only this user's desktop companion installation."
33466
33662
  });
33467
33663
  async executeCommand() {
@@ -33535,7 +33731,6 @@ cli.register(BootstrapPreflightCommand);
33535
33731
  cli.register(BootstrapLaunchCommand);
33536
33732
  cli.register(BootstrapStatusCommand);
33537
33733
  cli.register(BootstrapReapCommand);
33538
- cli.register(BootstrapFinishCommand);
33539
33734
  cli.register(BootstrapUiCommand);
33540
33735
  cli.register(BootstrapSeedProfileCommand);
33541
33736
  cli.register(TelemetryEnrollCommand);