@m8t-stack/cli 0.2.62 → 0.2.64

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.62";
1367
+ var CLI_VERSION = "0.2.64";
1368
1368
 
1369
1369
  // src/lib/render-error.ts
1370
1370
  init_errors();
@@ -19006,15 +19006,35 @@ import { DefaultAzureCredential as DefaultAzureCredential16 } from "@azure/ident
19006
19006
  init_errors();
19007
19007
  var DEFAULT_IMAGE_REPO = "ghcr.io/m8t-labs/m8t";
19008
19008
  function parseImageRef2(ref) {
19009
- const lastColon = ref.lastIndexOf(":");
19010
- const lastSlash = ref.lastIndexOf("/");
19011
- let name = ref;
19009
+ let rest = ref;
19010
+ let digest;
19011
+ const at = rest.lastIndexOf("@");
19012
+ if (at !== -1) {
19013
+ digest = rest.slice(at + 1) || void 0;
19014
+ rest = rest.slice(0, at);
19015
+ }
19016
+ const lastColon = rest.lastIndexOf(":");
19017
+ const lastSlash = rest.lastIndexOf("/");
19018
+ let name = rest;
19012
19019
  let tag;
19013
19020
  if (lastColon > lastSlash) {
19014
- name = ref.slice(0, lastColon);
19015
- tag = ref.slice(lastColon + 1) || void 0;
19021
+ name = rest.slice(0, lastColon);
19022
+ tag = rest.slice(lastColon + 1) || void 0;
19016
19023
  }
19017
- return { registry: name.split("/")[0], repo: name, tag };
19024
+ return { registry: name.split("/")[0], repo: name, tag, digest };
19025
+ }
19026
+ function assertGatewayDigest(c) {
19027
+ if (!c.digest?.startsWith("sha256:")) {
19028
+ throw new LocalCliError({
19029
+ code: "PLATFORM_GATEWAY_DIGEST_MISSING",
19030
+ message: `The release manifest's gateway pin (${c.ref}:${c.tag}) carries no usable image digest.`,
19031
+ hint: "Re-fetch the release channel, or target a release whose manifest pins a gateway digest. Deploying by tag instead is not offered: a mutable tag cannot establish which bytes run."
19032
+ });
19033
+ }
19034
+ return c.digest;
19035
+ }
19036
+ function gatewayRefFromComponent(c) {
19037
+ return `${c.ref}@${assertGatewayDigest(c)}`;
19018
19038
  }
19019
19039
  var SEMVER_TAG = /^v(\d+)\.(\d+)\.(\d+)$/;
19020
19040
  function isSemverTag(tag) {
@@ -19058,7 +19078,7 @@ function parseContainerAppResourceId(id) {
19058
19078
  }
19059
19079
  function planUpdate(opts) {
19060
19080
  const cur = parseImageRef2(opts.currentImage);
19061
- const currentTag = cur.tag ?? "(none)";
19081
+ const currentTag = cur.tag ?? cur.digest ?? "(none)";
19062
19082
  if (cur.repo !== opts.imageRepo) {
19063
19083
  return { kind: "refuse-byoc", current: currentTag, currentRepo: cur.repo, trackedRepo: opts.imageRepo };
19064
19084
  }
@@ -19067,6 +19087,9 @@ function planUpdate(opts) {
19067
19087
  }
19068
19088
  const available = opts.to ?? pickNewestSemver(opts.availableTags);
19069
19089
  if (!available) return { kind: "no-versions", current: currentTag };
19090
+ if (cur.digest && opts.toDigest && cur.digest === opts.toDigest) {
19091
+ return { kind: "noop", current: currentTag, available };
19092
+ }
19070
19093
  if (cur.tag === available) return { kind: "noop", current: currentTag, available };
19071
19094
  if (!opts.to) {
19072
19095
  const currentIsSemver = cur.tag ? isSemverTag(cur.tag) : false;
@@ -20057,14 +20080,14 @@ async function applyGatewayImage(a, ctx, gatewayResourceId) {
20057
20080
  const { resourceGroup, name } = parseContainerAppResourceId(gatewayResourceId);
20058
20081
  const current = (await runAz(["containerapp", "show", "-g", resourceGroup, "-n", name, "--query", "properties.template.containers[0].image", "-o", "tsv"])).trim();
20059
20082
  const tags = await fetchPublicTags(DEFAULT_IMAGE_REPO);
20060
- const plan = planUpdate({ currentImage: current, availableTags: tags, imageRepo: DEFAULT_IMAGE_REPO, to: a.to });
20061
20083
  const digest = ctx.manifest.components.gateway.digest;
20084
+ const plan = planUpdate({ currentImage: current, availableTags: tags, imageRepo: DEFAULT_IMAGE_REPO, to: a.to, toDigest: digest });
20062
20085
  switch (plan.kind) {
20063
20086
  case "refuse-byoc":
20064
20087
  ctx.onProgress?.(`gateway is BYOC (${plan.currentRepo}) \u2014 marking externally managed.`);
20065
20088
  return { tag: a.to, digest, state: "external" };
20066
20089
  case "roll":
20067
- await runAz(["containerapp", "update", "-n", name, "-g", resourceGroup, "--image", `${plan.repo}:${plan.target}`]);
20090
+ await runAz(["containerapp", "update", "-n", name, "-g", resourceGroup, "--image", `${plan.repo}@${assertGatewayDigest(ctx.manifest.components.gateway)}`]);
20068
20091
  return { tag: a.to, digest, state: "managed" };
20069
20092
  case "noop":
20070
20093
  return { tag: a.to, digest, state: "managed" };
@@ -20107,7 +20130,7 @@ async function resolveBicepParamsForConverge(ctx, opts = {}) {
20107
20130
  const location = opts.location ?? (await runAz(["resource", "list", "-g", ctx.resourceGroup, "--query", "[0].location", "-o", "tsv"])).trim();
20108
20131
  const foundryResourceId = await resolveFoundryResourceId(opts.foundryEndpoint ?? "", opts.foundryResourceId);
20109
20132
  const acrPullIdentityResourceId = resolveAcrPullIdentity({ explicit: opts.acrPullIdentity });
20110
- const imageRef = `${ctx.manifest.components.gateway.ref}:${ctx.manifest.components.gateway.tag}`;
20133
+ const imageRef = gatewayRefFromComponent(ctx.manifest.components.gateway);
20111
20134
  const acrResourceId = await resolveAcrResourceId({ imageRef, explicit: opts.acrResourceId });
20112
20135
  return {
20113
20136
  location,
@@ -20243,6 +20266,57 @@ async function applyPersona(a, ctx, opts) {
20243
20266
  return { treeSha: a.to, foundryVersion };
20244
20267
  }
20245
20268
 
20269
+ // src/lib/gateway-verify.ts
20270
+ function compareGatewayBinding(args) {
20271
+ const base = { stampTag: args.stampTag, stampDigest: args.stampDigest };
20272
+ if (!args.liveImage) {
20273
+ return { ...base, live: { kind: "unread" }, comparedBy: "none", match: "unknown" };
20274
+ }
20275
+ const parsed = parseImageRef2(args.liveImage);
20276
+ if (parsed.digest) {
20277
+ const comparable = Boolean(args.stampDigest);
20278
+ return {
20279
+ ...base,
20280
+ live: { kind: "digest", digest: parsed.digest },
20281
+ comparedBy: comparable ? "digest" : "none",
20282
+ match: comparable ? parsed.digest === args.stampDigest : "unknown"
20283
+ };
20284
+ }
20285
+ if (parsed.tag) {
20286
+ const comparable = args.stampTag !== "unknown" && args.stampTag !== "";
20287
+ return {
20288
+ ...base,
20289
+ live: { kind: "tag", tag: parsed.tag },
20290
+ comparedBy: comparable ? "tag" : "none",
20291
+ match: comparable ? parsed.tag === args.stampTag : "unknown"
20292
+ };
20293
+ }
20294
+ return { ...base, live: { kind: "unread" }, comparedBy: "none", match: "unknown" };
20295
+ }
20296
+ var LABEL = (s) => ` ${s}`.padEnd(22, " ");
20297
+ function renderLiveVerify(v, apiVersion, fmt2 = { ok: (s) => s, warn: (s) => s }) {
20298
+ const stamp = v.stampDigest ? `${v.stampTag} \xB7 ${v.stampDigest}` : v.stampTag;
20299
+ let live;
20300
+ switch (v.live.kind) {
20301
+ case "digest":
20302
+ live = v.live.digest;
20303
+ break;
20304
+ case "tag":
20305
+ live = `${v.live.tag} (tag-pinned \u2014 bytes not verified)`;
20306
+ break;
20307
+ default:
20308
+ live = "unknown";
20309
+ }
20310
+ const verdict = v.match === "unknown" ? fmt2.warn("unknown") : v.match ? fmt2.ok("yes") : fmt2.warn("no");
20311
+ const how = v.comparedBy === "none" ? "" : ` (compared by ${v.comparedBy})`;
20312
+ return [
20313
+ `${LABEL("stamp gateway:")}${stamp}`,
20314
+ `${LABEL("live gateway:")}${live}`,
20315
+ `${LABEL("match:")}${verdict}${how}`,
20316
+ `${LABEL("/api/version:")}${apiVersion}`
20317
+ ];
20318
+ }
20319
+
20246
20320
  // src/commands/platform/status.ts
20247
20321
  var PlatformStatusCommand = class extends M8tCommand {
20248
20322
  static paths = [["platform", "status"]];
@@ -20298,7 +20372,12 @@ var PlatformStatusCommand = class extends M8tCommand {
20298
20372
  const rows = statusRows(manifest, stamp, tree);
20299
20373
  let verify;
20300
20374
  if (this.verify) {
20301
- verify = await this.probeLive(gw.containerAppResourceId, gw.gatewayUrl, stamp?.components.gateway.tag);
20375
+ verify = await this.probeLive(
20376
+ gw.containerAppResourceId,
20377
+ gw.gatewayUrl,
20378
+ stamp?.components.gateway.tag,
20379
+ stamp?.components.gateway.digest
20380
+ );
20302
20381
  }
20303
20382
  if (mode === "json") {
20304
20383
  this.context.stdout.write(
@@ -20318,12 +20397,7 @@ var PlatformStatusCommand = class extends M8tCommand {
20318
20397
  if (verify) {
20319
20398
  log("");
20320
20399
  log(colors.field("live verify:"));
20321
- log(` stamp gateway tag: ${verify.stampGatewayTag}`);
20322
- log(` live gateway tag: ${verify.liveGatewayTag}`);
20323
- log(
20324
- ` match: ${verify.gatewayTagMatch === "unknown" ? colors.warn("unknown") : verify.gatewayTagMatch ? colors.success("yes") : colors.warn("no")}`
20325
- );
20326
- log(` /api/version: ${verify.apiVersion}`);
20400
+ for (const line2 of renderLiveVerify(verify.gateway, verify.apiVersion, { ok: colors.success, warn: colors.warn })) log(line2);
20327
20401
  }
20328
20402
  return 0;
20329
20403
  }
@@ -20347,11 +20421,11 @@ var PlatformStatusCommand = class extends M8tCommand {
20347
20421
  * error, etc.) degrades the affected field to "unknown" rather than
20348
20422
  * throwing — a verify probe must never crash the read-only status command.
20349
20423
  */
20350
- async probeLive(gatewayResourceId, gatewayUrl, stampGatewayTag) {
20351
- let liveGatewayTag = "unknown";
20424
+ async probeLive(gatewayResourceId, gatewayUrl, stampGatewayTag, stampGatewayDigest) {
20425
+ let liveImage;
20352
20426
  try {
20353
20427
  const { resourceGroup, name } = parseContainerAppResourceId(gatewayResourceId);
20354
- const image = (await runAz([
20428
+ liveImage = (await runAz([
20355
20429
  "containerapp",
20356
20430
  "show",
20357
20431
  "-g",
@@ -20362,11 +20436,9 @@ var PlatformStatusCommand = class extends M8tCommand {
20362
20436
  "properties.template.containers[0].image",
20363
20437
  "-o",
20364
20438
  "tsv"
20365
- ])).trim();
20366
- const tag = image.split(":").pop();
20367
- if (tag) liveGatewayTag = tag;
20439
+ ])).trim() || void 0;
20368
20440
  } catch {
20369
- liveGatewayTag = "unknown";
20441
+ liveImage = void 0;
20370
20442
  }
20371
20443
  let apiVersion = "unknown";
20372
20444
  try {
@@ -20380,9 +20452,17 @@ var PlatformStatusCommand = class extends M8tCommand {
20380
20452
  } catch {
20381
20453
  apiVersion = "unknown";
20382
20454
  }
20383
- const stampTag = stampGatewayTag ?? "unknown";
20384
- const gatewayTagMatch = liveGatewayTag === "unknown" || stampTag === "unknown" ? "unknown" : liveGatewayTag === stampTag;
20385
- return { liveGatewayTag, stampGatewayTag: stampTag, gatewayTagMatch, apiVersion };
20455
+ return {
20456
+ gateway: compareGatewayBinding({
20457
+ stampTag: stampGatewayTag ?? "unknown",
20458
+ // A stamp records "" for a component whose digest it never established, so
20459
+ // empty and absent are the SAME claim here. `??` would preserve "" and let a
20460
+ // caller treat it as a recorded value.
20461
+ stampDigest: stampGatewayDigest === "" ? void 0 : stampGatewayDigest,
20462
+ liveImage
20463
+ }),
20464
+ apiVersion
20465
+ };
20386
20466
  }
20387
20467
  };
20388
20468
 
@@ -20973,7 +21053,7 @@ async function buildConvergeDeps(args) {
20973
21053
  ...existing,
20974
21054
  bicep: {
20975
21055
  ...existing.bicep,
20976
- imageRef: `${ctx.manifest.components.gateway.ref}:${ctx.manifest.components.gateway.tag}`
21056
+ imageRef: gatewayRefFromComponent(ctx.manifest.components.gateway)
20977
21057
  },
20978
21058
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
20979
21059
  }
@@ -24010,6 +24090,46 @@ async function probeModelQuota(region, model) {
24010
24090
  const { verdict, quotad } = modelQuotaVerdict(usages, model);
24011
24091
  return { verdict, quotad, model, region, usagesReadable: usages.length > 0 };
24012
24092
  }
24093
+ function failureReason(e) {
24094
+ const raw = e instanceof Error ? e.message : String(e);
24095
+ const line2 = raw.split("\n").find((l) => l.trim().length > 0)?.trim() ?? "unknown error";
24096
+ return line2.length > 300 ? `${line2.slice(0, 297)}\u2026` : line2;
24097
+ }
24098
+ async function probeSoftDeletedAccounts(subscriptionId) {
24099
+ const args = ["cognitiveservices", "account", "list-deleted", "-o", "json"];
24100
+ if (subscriptionId) args.push("--subscription", subscriptionId);
24101
+ let out;
24102
+ try {
24103
+ out = await runAz(args);
24104
+ } catch (e) {
24105
+ return { readable: false, accounts: [], reason: failureReason(e) };
24106
+ }
24107
+ try {
24108
+ const raw = JSON.parse(out);
24109
+ const fromId = (id, segment) => new RegExp(`/${segment}/([^/]+)`, "i").exec(id)?.[1] ?? null;
24110
+ const accounts = [];
24111
+ for (const a of raw) {
24112
+ const id = a.id ?? "";
24113
+ const name = a.name ?? fromId(id, "deletedAccounts");
24114
+ const location = a.location ?? fromId(id, "locations");
24115
+ if (!name || !location) {
24116
+ return { readable: false, accounts: [], reason: `a deleted-account entry carried no usable name or location (id: ${id || "absent"})` };
24117
+ }
24118
+ accounts.push({
24119
+ name,
24120
+ location,
24121
+ // Kind decides whether the account can hold MODEL quota at all. A
24122
+ // soft-deleted SpeechServices account holds none; refusing on it would
24123
+ // block the install and point a purge command at an unrelated resource.
24124
+ kind: a.kind ?? "",
24125
+ resourceGroup: a.resourceGroup ?? fromId(id, "resourceGroups")
24126
+ });
24127
+ }
24128
+ return { readable: true, accounts };
24129
+ } catch (e) {
24130
+ return { readable: false, accounts: [], reason: failureReason(e) };
24131
+ }
24132
+ }
24013
24133
  async function bearer(credential2, scope) {
24014
24134
  try {
24015
24135
  const t = await credential2.getToken(scope);
@@ -24452,6 +24572,7 @@ function buildPrereqDeps(opts = {}) {
24452
24572
  probeProviders,
24453
24573
  registerProviders,
24454
24574
  probeModelQuota,
24575
+ probeSoftDeletedAccounts,
24455
24576
  discoverPlatform: async () => {
24456
24577
  const cfg = await readFoundryConfig();
24457
24578
  const d = await discoverGateway({
@@ -24574,8 +24695,10 @@ var PREREQS = [
24574
24695
  phase: "install",
24575
24696
  title: "No soft-deleted Cognitive Services account still holding the model quota",
24576
24697
  breaks: "Quota looks free but the model deployment fails; the quota is held by an account you already deleted.",
24577
- coverage: "unhandled",
24578
- checkedBy: "none",
24698
+ coverage: "refuses",
24699
+ checkedBy: "prereqs",
24700
+ // Reported, never repaired. Purging is irreversible and the account may be
24701
+ // one the founder means to recover — `--fix` names the command instead.
24579
24702
  fixable: false
24580
24703
  },
24581
24704
  {
@@ -24696,6 +24819,72 @@ function evaluateProviders(states) {
24696
24819
  }
24697
24820
  return result("resource-providers", "pass", `all ${states.length.toString()} required providers registered`);
24698
24821
  }
24822
+ var MODEL_HOSTING_KINDS = /* @__PURE__ */ new Set(["aiservices", "openai", "cognitiveservices"]);
24823
+ function holdsModelQuota(a) {
24824
+ return MODEL_HOSTING_KINDS.has(a.kind.trim().toLowerCase());
24825
+ }
24826
+ function sameRegion(a, b) {
24827
+ const norm = (s) => s.replace(/\s+/g, "").toLowerCase();
24828
+ return norm(a) === norm(b);
24829
+ }
24830
+ function softDeletedRemedy(accounts) {
24831
+ const purge = accounts.map((a) => `az cognitiveservices account purge -n ${a.name} -g ${a.resourceGroup ?? "<resource-group-it-was-in>"} -l ${a.location}`).join("\n ");
24832
+ const it = accounts.length === 1 ? "it" : "them";
24833
+ return `Purge ${it} \u2014 permanent, the account and its model deployments cannot be recovered afterwards:
24834
+ ${purge}
24835
+ Meant to keep one? 'az cognitiveservices account recover' takes the same -n/-g/-l and restores it (its quota then reads honestly again).
24836
+ Or install into a region with no soft-deleted account.`;
24837
+ }
24838
+ function asideNote(others, where) {
24839
+ if (others.length === 0) return "";
24840
+ const kinds = [...new Set(others.map((a) => a.kind || "unknown kind"))].join(", ");
24841
+ return ` (${others.length.toString()} other soft-deleted ${others.length === 1 ? "account" : "accounts"} ${where} \u2014 ${kinds} \u2014 ${others.length === 1 ? "cannot hold" : "cannot hold"} model quota, so ${others.length === 1 ? "it is" : "they are"} not judged here)`;
24842
+ }
24843
+ function evaluateSoftDeletedAccounts(e) {
24844
+ if (!e.readable) {
24845
+ return result(
24846
+ "soft-deleted-account",
24847
+ "skipped",
24848
+ `could not list this subscription's soft-deleted Cognitive Services accounts \u2014 proceeding unverified (${e.reason ?? "no reason reported"})`
24849
+ );
24850
+ }
24851
+ const mechanism = "A soft-deleted account keeps its model quota until it is purged";
24852
+ const quotaBearing = e.accounts.filter(holdsModelQuota);
24853
+ const others = e.accounts.filter((a) => !holdsModelQuota(a));
24854
+ if (e.region) {
24855
+ const region = e.region;
24856
+ const here = quotaBearing.filter((a) => sameRegion(a.location, region));
24857
+ if (here.length === 0) {
24858
+ return result(
24859
+ "soft-deleted-account",
24860
+ "pass",
24861
+ `no soft-deleted model-hosting accounts in ${region}` + asideNote(others.filter((a) => sameRegion(a.location, region)), "there")
24862
+ );
24863
+ }
24864
+ const noun2 = here.length === 1 ? "account" : "accounts";
24865
+ return result(
24866
+ "soft-deleted-account",
24867
+ "fail",
24868
+ `${here.length.toString()} soft-deleted Cognitive Services ${noun2} in ${region}: ${here.map((a) => `${a.name} (${a.kind || "unknown kind"})`).join(", ")}. ${mechanism}, so quota can read free here and the model deployment still fail.`,
24869
+ softDeletedRemedy(here)
24870
+ );
24871
+ }
24872
+ if (quotaBearing.length === 0) {
24873
+ return result(
24874
+ "soft-deleted-account",
24875
+ "pass",
24876
+ "no soft-deleted model-hosting accounts anywhere in this subscription" + asideNote(others, "in this subscription")
24877
+ );
24878
+ }
24879
+ const regions = [...new Set(quotaBearing.map((a) => a.location.replace(/\s+/g, "").toLowerCase()))];
24880
+ const noun = quotaBearing.length === 1 ? "account" : "accounts";
24881
+ return result(
24882
+ "soft-deleted-account",
24883
+ "warn",
24884
+ `no region given, so this cannot be judged against your target. This subscription has ${quotaBearing.length.toString()} soft-deleted Cognitive Services ${noun}: ${quotaBearing.map((a) => `${a.name} (${a.location})`).join(", ")}. ${mechanism} \u2014 installing into ${regions.length === 1 ? regions[0] : "one of those regions"} would let quota read free while the model deployment still fails.`,
24885
+ softDeletedRemedy(quotaBearing)
24886
+ );
24887
+ }
24699
24888
  function evaluateModelQuota(e) {
24700
24889
  if (!e.usagesReadable) {
24701
24890
  return result("model-quota", "skipped", `could not read model quota in ${e.region} \u2014 proceeding unverified`);
@@ -24883,6 +25072,11 @@ async function runInstallPhase(deps, opts) {
24883
25072
  }
24884
25073
  }
24885
25074
  results.push(evaluateProviders(await deps.probeProviders(REQUIRED_PROVIDERS, subscriptionId)));
25075
+ const softDeleted = await deps.probeSoftDeletedAccounts(subscriptionId).catch((e) => {
25076
+ const raw = e instanceof Error ? e.message : String(e);
25077
+ return { readable: false, accounts: [], reason: raw.length > 300 ? `${raw.slice(0, 297)}\u2026` : raw };
25078
+ });
25079
+ results.push(evaluateSoftDeletedAccounts({ ...softDeleted, ...opts.region ? { region: opts.region } : {} }));
24886
25080
  if (opts.region && opts.model) {
24887
25081
  results.push(evaluateModelQuota(await deps.probeModelQuota(opts.region, opts.model)));
24888
25082
  }
@@ -25000,7 +25194,7 @@ var PrereqsCommand = class extends M8tCommand {
25000
25194
  static paths = [["prereqs"]];
25001
25195
  static usage = Command47.Usage({
25002
25196
  description: "Check (and optionally fix) everything m8t needs \u2014 to install, or for you to use it.",
25003
- details: "Runs one of two phases. With no platform discoverable it checks INSTALL prerequisites: your Azure sign-in, subscription and directory rights, resource-provider registrations, and model quota. With a platform live it checks USAGE prerequisites for the signed-in account: the sign-in redirect URI, Foundry data-plane access, and Key Vault secrets access. Pass --fix to repair what is repairable, and --fix --for <upn> to set a teammate up (usage phase only). Read-only without --fix.",
25197
+ details: "Runs one of two phases. With no platform discoverable it checks INSTALL prerequisites: your Azure sign-in, subscription and directory rights, resource-provider registrations, soft-deleted Cognitive Services accounts (they hold their model quota until purged), and model quota. With a platform live it checks USAGE prerequisites for the signed-in account: the sign-in redirect URI, Foundry data-plane access, and Key Vault secrets access. Pass --fix to repair what is repairable, and --fix --for <upn> to set a teammate up (usage phase only). Read-only without --fix.",
25004
25198
  examples: [
25005
25199
  ["Before installing", "$0 prereqs"],
25006
25200
  ["Register anything missing, then install", "$0 prereqs --fix"],
@@ -25012,7 +25206,7 @@ var PrereqsCommand = class extends M8tCommand {
25012
25206
  fix = Option44.Boolean("--fix", false, { description: "Repair what is repairable. Without it the command only reports." });
25013
25207
  for_ = Option44.String("--for", { description: "UPN or object id of another person. Usage phase only." });
25014
25208
  phase = Option44.String("--phase", { description: "Force 'install' or 'usage' instead of auto-detecting." });
25015
- region = Option44.String("--region", { description: "Target region for the install-phase quota check." });
25209
+ region = Option44.String("--region", { description: "Target region. Enables the install-phase quota check, and judges soft-deleted accounts against the region you will install into (without it, they only warn)." });
25016
25210
  model = Option44.String("--model", { description: `Model whose quota the install needs (default ${INSTALL_REASONING_MODEL}).` });
25017
25211
  clientId = Option44.String("--client-id", { description: "A ready app registration, in place of directory-admin rights." });
25018
25212
  subscription = Option44.String("--subscription");
@@ -28390,11 +28584,15 @@ var SPEND_DISCLOSURE = [
28390
28584
  ].join("\n");
28391
28585
 
28392
28586
  // src/commands/bootstrap/preflight.ts
28587
+ var PREFLIGHT_SELF_REPORTED = ["az-signed-in", "subscription-admin", "app-registration"];
28588
+ function preflightRenderable(results) {
28589
+ return results.filter((r) => !PREFLIGHT_SELF_REPORTED.includes(r.slug));
28590
+ }
28393
28591
  var BootstrapPreflightCommand = class extends M8tCommand {
28394
28592
  static paths = [["bootstrap", "preflight"]];
28395
28593
  static usage = Command53.Usage({
28396
28594
  description: "Loudly verify you can install m8t (Owner/UAA + directory admin) and hard-stop if not.",
28397
- details: "Step 1 of `m8t bootstrap`. Prints an unmissable admin-credentials notice, then checks: Owner or User Access Administrator at subscription scope (hard requirement), directory-admin capability to register the app (or pass --client-id), every Azure resource provider the install uses (registering any that are missing), and \u2014 with --location \u2014 model quota in the target region. Exits non-zero with the exact failing check + remedy. `m8t prereqs` runs the same substrate checks on their own.",
28595
+ details: "Step 1 of `m8t bootstrap`. Prints an unmissable admin-credentials notice, then checks: Owner or User Access Administrator at subscription scope (hard requirement), directory-admin capability to register the app (or pass --client-id), every Azure resource provider the install uses (registering any that are missing), soft-deleted Cognitive Services accounts (they keep their model quota until purged, so quota can read free and the model deployment still fail), and \u2014 with --location \u2014 model quota in the target region. Exits non-zero with the exact failing check + remedy. `m8t prereqs` runs the same substrate checks on their own.",
28398
28596
  examples: [
28399
28597
  ["Run the preflight", "$0 bootstrap preflight"],
28400
28598
  ["Check quota for the region you will install into", "$0 bootstrap preflight --location eastus2"],
@@ -28450,8 +28648,12 @@ ${colors.dim(DISCLOSURE_TIER1)}
28450
28648
  `));
28451
28649
  }
28452
28650
  });
28453
- for (const r of verdict.results.filter((x) => x.slug === "resource-providers" || x.slug === "model-quota")) {
28454
- this.context.stdout.write(line(r.status !== "fail", r.title, r.detail));
28651
+ for (const r of preflightRenderable(verdict.results)) {
28652
+ this.context.stdout.write(statusLine(r.status, r.title, r.detail));
28653
+ if ((r.status === "warn" || r.status === "skipped") && r.remedy) {
28654
+ this.context.stdout.write(` ${colors.hint("if this is your region:")} ${r.remedy}
28655
+ `);
28656
+ }
28455
28657
  if (r.status === "fail") {
28456
28658
  this.context.stderr.write(failBlock(r.detail, r.remedy ?? "See guides/prerequisites.md."));
28457
28659
  throw new LocalCliError({ code: `BOOTSTRAP_PREREQ_${r.slug.toUpperCase().replace(/-/g, "_")}`, message: r.detail, hint: r.remedy });
@@ -28479,6 +28681,12 @@ function line(ok, label, note) {
28479
28681
  return ` ${mark} ${label.padEnd(50)} ${colors.dim(`(${note})`)}
28480
28682
  `;
28481
28683
  }
28684
+ function statusLine(status, label, note) {
28685
+ if (status === "fail") return line(false, label, note);
28686
+ if (status === "pass" || status === "fixed") return line(true, label, note);
28687
+ return ` ${colors.hint("!")} ${label.padEnd(50)} ${colors.dim(`(${note})`)}
28688
+ `;
28689
+ }
28482
28690
  function failBlock(why, remedy) {
28483
28691
  return `
28484
28692
  ${colors.error("\u26D4 CANNOT PROCEED")}
@@ -28858,7 +29066,7 @@ ${colors.error("\u2717")} The GitHub App on disk is installed on ${colors.field(
28858
29066
  // src/commands/bootstrap/launch.ts
28859
29067
  var DEFAULT_RG = "rg-m8t-stack";
28860
29068
  var DEFAULT_INSTALLER = "ghcr.io/m8t-labs/m8t-installer";
28861
- var DEFAULT_INSTALLER_TAG = "v0.1.53";
29069
+ var DEFAULT_INSTALLER_TAG = "v0.1.55";
28862
29070
  var ACI_NAME = "m8t-installer";
28863
29071
  var MI_NAME = "m8t-installer-mi";
28864
29072
  var BootstrapLaunchCommand = class extends M8tCommand {