@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 +245 -37
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
19010
|
-
|
|
19011
|
-
|
|
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 =
|
|
19015
|
-
tag =
|
|
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}
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
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
|
|
20424
|
+
async probeLive(gatewayResourceId, gatewayUrl, stampGatewayTag, stampGatewayDigest) {
|
|
20425
|
+
let liveImage;
|
|
20352
20426
|
try {
|
|
20353
20427
|
const { resourceGroup, name } = parseContainerAppResourceId(gatewayResourceId);
|
|
20354
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20384
|
-
|
|
20385
|
-
|
|
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:
|
|
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: "
|
|
24578
|
-
checkedBy: "
|
|
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
|
|
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
|
|
28454
|
-
this.context.stdout.write(
|
|
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.
|
|
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 {
|