@m8t-stack/cli 0.2.1 → 0.2.3

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
@@ -1109,7 +1109,7 @@ var init_enable_hosted_brain = __esm({
1109
1109
  import { Builtins, Cli } from "clipanion";
1110
1110
 
1111
1111
  // src/lib/package-version.ts
1112
- var CLI_VERSION = "0.2.1";
1112
+ var CLI_VERSION = "0.2.3";
1113
1113
 
1114
1114
  // src/lib/render-error.ts
1115
1115
  init_errors();
@@ -3135,7 +3135,7 @@ async function resolveFoundryProject(opts) {
3135
3135
  throw new LocalCliError({
3136
3136
  code: "FOUNDRY_PROJECT_ZERO",
3137
3137
  message: `No AIServices Foundry project found in subscription ${opts.subscriptionId}.`,
3138
- hint: "Switch subscription with 'az account set --subscription <id>' or pass --endpoint."
3138
+ hint: "Create one with 'm8t foundry create --resource-group <rg> --location <region>', switch subscription with 'az account set --subscription <id>', or pass --endpoint."
3139
3139
  });
3140
3140
  }
3141
3141
  if (candidates.length === 1) return candidates[0];
@@ -3367,7 +3367,7 @@ import * as fs6 from "fs";
3367
3367
  // src/lib/persona-render.ts
3368
3368
  init_errors();
3369
3369
  import * as fs5 from "fs";
3370
- function renderPersonaBody(personaPath, fillableFieldValues) {
3370
+ function renderPersonaBody(personaPath, fillableFieldValues, instanceName) {
3371
3371
  let raw;
3372
3372
  try {
3373
3373
  raw = fs5.readFileSync(personaPath, "utf8");
@@ -3392,7 +3392,18 @@ function renderPersonaBody(personaPath, fillableFieldValues) {
3392
3392
  hint: `Add these to fillableFieldValues in ~/.m8t-stack/foundry/<agent>.yaml, or re-deploy the agent via the architect to repopulate.`
3393
3393
  });
3394
3394
  }
3395
- return rendered.replace(/\s+$/, "");
3395
+ const out = rendered.replace(/\s+$/, "");
3396
+ return instanceName ? injectInstanceIdentity(out, instanceName) : out;
3397
+ }
3398
+ function injectInstanceIdentity(body, instanceName) {
3399
+ const display = instanceName.charAt(0).toUpperCase() + instanceName.slice(1);
3400
+ const line = `**You are ${display}.** Introduce yourself as ${display} on first contact.`;
3401
+ const title = /^(#[^\n]*\n)/.exec(body);
3402
+ return title ? `${title[0]}
3403
+ ${line}
3404
+ ${body.slice(title[0].length)}` : `${line}
3405
+
3406
+ ${body}`;
3396
3407
  }
3397
3408
 
3398
3409
  // src/lib/brain-loader-block.ts
@@ -3508,7 +3519,7 @@ async function linkBrain(args) {
3508
3519
  if (personaPath) {
3509
3520
  progress("Re-rendering persona body with brain loader\u2026");
3510
3521
  const resolved = personaPath.startsWith("/") ? personaPath : path5.join(args.repoRoot, personaPath);
3511
- base = renderPersonaBody(resolved, yaml?.fillableFieldValues ?? {});
3522
+ base = renderPersonaBody(resolved, yaml?.fillableFieldValues ?? {}, args.agentName);
3512
3523
  } else {
3513
3524
  progress("No local persona yaml \u2014 basing instructions on the deployed agent\u2026");
3514
3525
  base = current.definition.instructions ?? "";
@@ -4230,7 +4241,7 @@ async function unlinkBrain(args) {
4230
4241
  }
4231
4242
  personaPath = path8.join(fs9.readFileSync(marker, "utf8").trim(), personaPath);
4232
4243
  }
4233
- strippedInstructions = renderPersonaBody(personaPath, yaml.fillableFieldValues ?? {});
4244
+ strippedInstructions = renderPersonaBody(personaPath, yaml.fillableFieldValues ?? {}, args.agentName);
4234
4245
  } else {
4235
4246
  progress("No persona yaml \u2014 stripping the loader from the deployed instructions\u2026");
4236
4247
  strippedInstructions = stripBrainLoader(current.definition.instructions ?? "");
@@ -5319,7 +5330,10 @@ function parseImageRef(image) {
5319
5330
  const host = hostAndPath.slice(0, slash);
5320
5331
  const repo = hostAndPath.slice(slash + 1);
5321
5332
  const acrName = host.replace(/\.azurecr\.io$/i, "");
5322
- return { acrName, repo, tag: tag || "" };
5333
+ return { host, acrName, repo, tag: tag || "" };
5334
+ }
5335
+ function isAcrHost(host) {
5336
+ return /\.azurecr\.io$/i.test(host);
5323
5337
  }
5324
5338
  function acrScope(subscriptionId, projectAccountScope, acrName) {
5325
5339
  const rg = /\/resourceGroups\/([^/]+)\//.exec(projectAccountScope)?.[1] ?? "";
@@ -5334,36 +5348,40 @@ async function checkPreconditions(args) {
5334
5348
  hint: `Hosted agents need one of: ${HOSTED_AGENT_REGIONS.join(", ")}. Deploy against a project in a supported region.`
5335
5349
  });
5336
5350
  }
5337
- const { acrName, repo, tag } = parseImageRef(args.image);
5338
- const tagCheck = await imageTagExists(acrName, repo, tag);
5339
- if (tagCheck === "absent") {
5340
- throw new LocalCliError({
5341
- code: "IMAGE_TAG_NOT_FOUND",
5342
- message: `Image '${repo}:${tag}' was not found in ACR '${acrName}'.`,
5343
- hint: `Build + push it first: cd agents/coding-agent && docker buildx build --platform linux/amd64 -t ${acrName}.azurecr.io/${repo}:${tag} --push . (see workers.md).`
5344
- });
5345
- }
5346
- if (tagCheck === "unknown") {
5347
- warnings.push(`could not verify image '${repo}:${tag}' in ACR '${acrName}' (az acr check failed) \u2014 proceeding; a bad image surfaces as a failed version.`);
5348
- }
5349
- if (args.project.projectPrincipalId) {
5350
- const scope = acrScope(args.subscriptionId, args.project.accountScope, acrName);
5351
- const hasPull = await principalHasAcrPull({
5352
- credential: args.credential,
5353
- acrScope: scope,
5354
- principalId: args.project.projectPrincipalId
5355
- });
5356
- if (!hasPull) {
5357
- await grantAcrPull({
5351
+ const { host, acrName, repo, tag } = parseImageRef(args.image);
5352
+ if (isAcrHost(host)) {
5353
+ const tagCheck = await imageTagExists(acrName, repo, tag);
5354
+ if (tagCheck === "absent") {
5355
+ throw new LocalCliError({
5356
+ code: "IMAGE_TAG_NOT_FOUND",
5357
+ message: `Image '${repo}:${tag}' was not found in ACR '${acrName}'.`,
5358
+ hint: `Build + push it first: cd agents/coding-agent && docker buildx build --platform linux/amd64 -t ${acrName}.azurecr.io/${repo}:${tag} --push . (see workers.md).`
5359
+ });
5360
+ }
5361
+ if (tagCheck === "unknown") {
5362
+ warnings.push(`could not verify image '${repo}:${tag}' in ACR '${acrName}' (az acr check failed) \u2014 proceeding; a bad image surfaces as a failed version.`);
5363
+ }
5364
+ if (args.project.projectPrincipalId) {
5365
+ const scope = acrScope(args.subscriptionId, args.project.accountScope, acrName);
5366
+ const hasPull = await principalHasAcrPull({
5358
5367
  credential: args.credential,
5359
- subscriptionId: args.subscriptionId,
5360
5368
  acrScope: scope,
5361
5369
  principalId: args.project.projectPrincipalId
5362
5370
  });
5363
- warnings.push(`granted AcrPull to the project managed identity on '${acrName}' (was missing).`);
5371
+ if (!hasPull) {
5372
+ await grantAcrPull({
5373
+ credential: args.credential,
5374
+ subscriptionId: args.subscriptionId,
5375
+ acrScope: scope,
5376
+ principalId: args.project.projectPrincipalId
5377
+ });
5378
+ warnings.push(`granted AcrPull to the project managed identity on '${acrName}' (was missing).`);
5379
+ }
5380
+ } else {
5381
+ warnings.push("could not resolve the project managed identity to verify AcrPull \u2014 ensure it can pull from the ACR.");
5364
5382
  }
5365
5383
  } else {
5366
- warnings.push("could not resolve the project managed identity to verify AcrPull \u2014 ensure it can pull from the ACR.");
5384
+ warnings.push(`public image host '${host}' \u2014 skipping the ACR tag check and AcrPull grant (anonymous pull, no registry credentials needed).`);
5367
5385
  }
5368
5386
  const canAssign = await callerCanAssignRoles({
5369
5387
  credential: args.credential,
@@ -5522,6 +5540,9 @@ function usageModelName(usageName) {
5522
5540
  return usageName.split(".").slice(2).join(".");
5523
5541
  }
5524
5542
  var REASONING_USAGE_RE = /gpt-5|o[1-9]/i;
5543
+ function isReasoningModel(modelDeployment) {
5544
+ return REASONING_USAGE_RE.test(modelDeployment);
5545
+ }
5525
5546
  function modelQuotaVerdict(usages, modelDeployment) {
5526
5547
  const token = modelDeployment.trim().toLowerCase();
5527
5548
  const matches = usages.filter((u) => usageModelName(u.name).toLowerCase() === token);
@@ -5539,16 +5560,16 @@ var SIZE_PRESETS = {
5539
5560
  medium: { cpu: "1", memory: "2Gi" },
5540
5561
  large: { cpu: "2", memory: "4Gi" }
5541
5562
  };
5542
- var DEFAULT_ACR = "m8tacrxn42jrnx.azurecr.io";
5563
+ var DEFAULT_REGISTRY = "ghcr.io/m8t-run";
5543
5564
  var DEFAULT_IMAGE = "m8t-coding-agent";
5544
- var DEFAULT_TAG = "v20260604-996b7ac";
5565
+ var DEFAULT_TAG = "v0.1.0";
5545
5566
  var DEFAULT_MODEL = "gpt-4.1-mini";
5546
5567
  var NAME_RE = /^[a-z0-9-]+$/;
5547
5568
  var CoderDeployCommand = class extends M8tCommand {
5548
5569
  static paths = [["coder", "deploy"]];
5549
5570
  static usage = Command26.Usage({
5550
5571
  description: "Deploy the curated coding agent as a hosted Foundry worker.",
5551
- details: "Creates a hosted agent version from the ACR image, tags it as an m8t worker (kind: hosted + persona), grants its identity the Foundry User role, and polls to active. The image must already be pushed (see workers.md). Re-running creates a new version (idempotent).",
5572
+ details: "Creates a hosted agent version from a container image (defaults to the public GHCR image ghcr.io/m8t-run/m8t-coding-agent, pulled anonymously \u2014 no local build needed), tags it as an m8t worker (kind: hosted + persona), grants its identity the Foundry User role, and polls to active. Override with --image/--image-tag for a bring-your-own-registry image (e.g. a private ACR ref, which must be pushed first). Re-running creates a new version (idempotent).",
5552
5573
  examples: [
5553
5574
  ["Deploy a coder with defaults", "$0 coder deploy my-coder"],
5554
5575
  ["Larger sandbox + a custom model", "$0 coder deploy my-coder --size large --model-deployment gpt-4.1"],
@@ -5603,7 +5624,7 @@ var CoderDeployCommand = class extends M8tCommand {
5603
5624
  }
5604
5625
  const persona = this.persona ?? "coding-agent";
5605
5626
  const imageInput = this.image ?? DEFAULT_IMAGE;
5606
- const repoRef = imageInput.includes("/") ? imageInput : `${DEFAULT_ACR}/${imageInput}`;
5627
+ const repoRef = imageInput.includes("/") ? imageInput : `${DEFAULT_REGISTRY}/${imageInput}`;
5607
5628
  const image = `${repoRef}:${this.imageTag ?? DEFAULT_TAG}`;
5608
5629
  const mode = resolveOutputMode(
5609
5630
  this.output,
@@ -5843,21 +5864,66 @@ import { Command as Command28, Option as Option26 } from "clipanion";
5843
5864
  import { DefaultAzureCredential as DefaultAzureCredential14 } from "@azure/identity";
5844
5865
  init_errors();
5845
5866
  init_rbac();
5867
+
5868
+ // src/lib/acs-provision.ts
5869
+ var MANAGED_DOMAIN = "AzureManagedDomain";
5870
+ async function provisionAcsEmail(args) {
5871
+ const { client, subscriptionId, resourceGroup, name, dataLocation, onProgress } = args;
5872
+ const acsName = `acs-${name}`;
5873
+ const emailName = `${name}-email`;
5874
+ onProgress?.(`creating ACS resource ${acsName}\u2026`);
5875
+ const acs = await client.communicationServices.beginCreateOrUpdateAndWait(
5876
+ resourceGroup,
5877
+ acsName,
5878
+ { location: "global", dataLocation }
5879
+ );
5880
+ onProgress?.(`creating Email Communication Service ${emailName}\u2026`);
5881
+ await client.emailServices.beginCreateOrUpdateAndWait(resourceGroup, emailName, {
5882
+ location: "global",
5883
+ dataLocation
5884
+ });
5885
+ onProgress?.("provisioning the Azure-managed domain\u2026");
5886
+ const domain = await client.domains.beginCreateOrUpdateAndWait(
5887
+ resourceGroup,
5888
+ emailName,
5889
+ MANAGED_DOMAIN,
5890
+ { location: "global", domainManagement: "AzureManaged" }
5891
+ );
5892
+ const domainResourceId = `/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Communication/emailServices/${emailName}/domains/${MANAGED_DOMAIN}`;
5893
+ onProgress?.("linking the domain to the ACS resource\u2026");
5894
+ await client.communicationServices.beginCreateOrUpdateAndWait(resourceGroup, acsName, {
5895
+ location: "global",
5896
+ dataLocation,
5897
+ linkedDomains: [domainResourceId]
5898
+ });
5899
+ const hostName = acs.hostName ?? `${acsName}.communication.azure.com`;
5900
+ const fromSenderDomain = domain.fromSenderDomain;
5901
+ if (!fromSenderDomain) {
5902
+ throw new Error("ACS managed domain did not report a fromSenderDomain");
5903
+ }
5904
+ return {
5905
+ endpoint: `https://${hostName}/`,
5906
+ sender: `DoNotReply@${fromSenderDomain}`
5907
+ };
5908
+ }
5909
+
5910
+ // src/commands/azure-exec/deploy.ts
5911
+ import { CommunicationServiceManagementClient } from "@azure/arm-communication";
5846
5912
  var SIZE_PRESETS2 = {
5847
5913
  small: { cpu: "0.5", memory: "1Gi" },
5848
5914
  medium: { cpu: "1", memory: "2Gi" },
5849
5915
  large: { cpu: "2", memory: "4Gi" }
5850
5916
  };
5851
- var DEFAULT_ACR2 = "m8tacrxn42jrnx.azurecr.io";
5917
+ var DEFAULT_REGISTRY2 = "ghcr.io/m8t-run";
5852
5918
  var DEFAULT_IMAGE2 = "m8t-azure-executor";
5853
- var DEFAULT_TAG2 = "dev";
5919
+ var DEFAULT_TAG2 = "v0.1.0";
5854
5920
  var DEFAULT_MODEL2 = "gpt-5-mini";
5855
5921
  var NAME_RE2 = /^[a-z0-9-]+$/;
5856
5922
  var AzureExecDeployCommand = class extends M8tCommand {
5857
5923
  static paths = [["azure-exec", "deploy"]];
5858
5924
  static usage = Command28.Usage({
5859
5925
  description: "Deploy the Azure executor as a hosted Foundry worker (az CLI + tiered ops).",
5860
- details: "Creates a hosted agent version from the executor image, grants its identity Foundry User + Contributor (at --scope) + Key Vault Secrets User (brain KV), polls to active, and a2a-enables it as a target. The image must already be pushed. Contributor scope is REQUIRED \u2014 pass --scope or --resource-group. Pass --grant-access-admin to additionally grant User Access Administrator (enables human-approved Tier-2 role/delete ops).",
5926
+ details: "Creates a hosted agent version from a container image (defaults to the public GHCR image ghcr.io/m8t-run/m8t-azure-executor, pulled anonymously \u2014 no local build needed), grants its identity Foundry User + Contributor (at --scope) + Key Vault Secrets User (brain KV), polls to active, and a2a-enables it as a target. Override with --image/--image-tag for a bring-your-own-registry image (e.g. a private ACR ref, which must be pushed first). Contributor scope is REQUIRED \u2014 pass --scope or --resource-group. Pass --grant-access-admin to additionally grant User Access Administrator (enables human-approved Tier-2 role/delete ops).",
5861
5927
  examples: [
5862
5928
  [
5863
5929
  "Deploy scoped to a resource group",
@@ -5881,6 +5947,7 @@ var AzureExecDeployCommand = class extends M8tCommand {
5881
5947
  output = Option26.String("--output");
5882
5948
  skipQuotaCheck = Option26.Boolean("--skip-quota-check", false);
5883
5949
  grantAccessAdmin = Option26.Boolean("--grant-access-admin", false);
5950
+ enableEmail = Option26.Boolean("--enable-email", false);
5884
5951
  async executeCommand() {
5885
5952
  if (!NAME_RE2.test(this.name)) {
5886
5953
  throw new LocalCliError({
@@ -5925,8 +5992,15 @@ var AzureExecDeployCommand = class extends M8tCommand {
5925
5992
  });
5926
5993
  }
5927
5994
  }
5995
+ if (this.enableEmail === true && (typeof this.resourceGroup !== "string" || this.resourceGroup.length === 0)) {
5996
+ throw new LocalCliError({
5997
+ code: "USAGE",
5998
+ message: "--enable-email requires --resource-group (the ACS resource lands in the Contributor-granted RG so the executor's identity can send).",
5999
+ hint: "Pass --resource-group <rg> (the same scope the executor is granted Contributor on)."
6000
+ });
6001
+ }
5928
6002
  const imageInput = this.image ?? DEFAULT_IMAGE2;
5929
- const repoRef = imageInput.includes("/") ? imageInput : `${DEFAULT_ACR2}/${imageInput}`;
6003
+ const repoRef = imageInput.includes("/") ? imageInput : `${DEFAULT_REGISTRY2}/${imageInput}`;
5930
6004
  const image = `${repoRef}:${this.imageTag ?? DEFAULT_TAG2}`;
5931
6005
  const project = await resolveFoundryProject({
5932
6006
  credential: credential2,
@@ -5955,11 +6029,27 @@ var AzureExecDeployCommand = class extends M8tCommand {
5955
6029
  privateKeyPem,
5956
6030
  write: (s) => this.context.stdout.write(s)
5957
6031
  });
6032
+ let acsEnv = {};
6033
+ if (this.enableEmail === true && typeof this.resourceGroup === "string") {
6034
+ const resourceGroup = this.resourceGroup;
6035
+ onProgress?.("provisioning ACS Email (managed domain)\u2026");
6036
+ const acsClient = new CommunicationServiceManagementClient(credential2, subscriptionId);
6037
+ const acs = await provisionAcsEmail({
6038
+ client: acsClient,
6039
+ subscriptionId,
6040
+ resourceGroup,
6041
+ name: this.name,
6042
+ dataLocation: "United States",
6043
+ onProgress
6044
+ });
6045
+ acsEnv = { M8T_ACS_ENDPOINT: acs.endpoint, M8T_ACS_SENDER: acs.sender };
6046
+ }
5958
6047
  const env = {
5959
6048
  MODEL_DEPLOYMENT_NAME: this.modelDeployment ?? DEFAULT_MODEL2,
5960
6049
  REASONING_EFFORT: "low",
5961
6050
  GITHUB_APP_INSTALLATION_ID: installationId,
5962
- AZURE_KEYVAULT_URI: kvUri
6051
+ AZURE_KEYVAULT_URI: kvUri,
6052
+ ...acsEnv
5963
6053
  };
5964
6054
  for (const pair of this.env ?? []) {
5965
6055
  const eq = pair.indexOf("=");
@@ -9570,6 +9660,13 @@ function renderDigestMarkdown(digest, opts) {
9570
9660
  case "codified":
9571
9661
  push("## Codified", `- \`${e.slug}\`${e.pr ? ` \u2014 PR ${e.pr}` : ""}${e.rejected ? ` \u2014 rejected: ${e.rejected}` : ""}`);
9572
9662
  break;
9663
+ case "promotion": {
9664
+ const icon = e.outcome === "auto-merged" ? "\u2705" : e.outcome === "rejected" ? "\u274C" : "\u{1F6D1}";
9665
+ const scoreStr = typeof e.score === "number" ? `, score ${String(e.score)}` : "";
9666
+ const tags = e.defaulted ? ` [SAFETY explicit: ${e.defaulted.safety ? "no" : "yes"}, COLLISION explicit: ${e.defaulted.collision ? "no" : "yes"}]` : "";
9667
+ push("## Promotion", `- ${icon} \`${e.slug}\` \u2014 **${e.outcome.toUpperCase()}** (${e.decision}/${e.judgeStatus}${scoreStr}, origin: ${e.origin})${tags}${e.pr ? ` \u2014 PR ${e.pr}` : ""} \u2014 ${e.reason}`);
9668
+ break;
9669
+ }
9573
9670
  default: {
9574
9671
  const unknown = e;
9575
9672
  const detail = unknown.detail ?? unknown.path ?? "";
@@ -9591,6 +9688,7 @@ function renderDigestMarkdown(digest, opts) {
9591
9688
  "## Health",
9592
9689
  "## Archived",
9593
9690
  "## Codified",
9691
+ "## Promotion",
9594
9692
  "## Other"
9595
9693
  ];
9596
9694
  for (const header of order) {
@@ -10389,6 +10487,9 @@ var ZERO_WIDTH_RE = new RegExp("\u200B|\u200C|\u200D|\uFEFF", "g");
10389
10487
  // ../../packages/brain/engine/dist/esm/librarian/janitor/frontmatter.js
10390
10488
  import { parse as parseYaml9, stringify as stringifyYaml5 } from "yaml";
10391
10489
 
10490
+ // ../../packages/brain/engine/dist/esm/librarian/config.js
10491
+ import { parse as parseYaml10 } from "yaml";
10492
+
10392
10493
  // src/lib/dream-discovery.ts
10393
10494
  init_http();
10394
10495
  init_errors();
@@ -10864,6 +10965,324 @@ function defaultDeps(overrides) {
10864
10965
  return deps;
10865
10966
  }
10866
10967
 
10968
+ // src/commands/foundry/create.ts
10969
+ import { Command as Command41, Option as Option39 } from "clipanion";
10970
+
10971
+ // src/lib/foundry-create.ts
10972
+ init_errors();
10973
+ import { createHash as createHash3 } from "crypto";
10974
+ function rethrowIfAzUnavailable(e) {
10975
+ if (e instanceof LocalCliError && (e.code === "AUTH_NOT_SIGNED_IN" || e.code === "AZ_NOT_INSTALLED")) {
10976
+ throw e;
10977
+ }
10978
+ }
10979
+ function deriveAccountName(subscriptionId) {
10980
+ const h = createHash3("sha256").update(subscriptionId).digest("hex").slice(0, 12);
10981
+ return `m8t${h}`;
10982
+ }
10983
+ function assertHostedRegion(location) {
10984
+ if (isHostedRegionSupported(location)) return;
10985
+ throw new LocalCliError({
10986
+ code: "FOUNDRY_REGION_NOT_ELIGIBLE",
10987
+ message: `Region '${location}' is not eligible for hosted Foundry agents.`,
10988
+ hint: `Use a hosted-eligible region, e.g. ${HOSTED_AGENT_REGIONS.slice(0, 4).join(", ")}.`
10989
+ });
10990
+ }
10991
+ async function checkModelQuota2(region, model) {
10992
+ const usages = await listModelQuota(region);
10993
+ const { verdict, quotad } = modelQuotaVerdict(usages, model);
10994
+ if (verdict === "no_quota") {
10995
+ throw new LocalCliError({
10996
+ code: "FOUNDRY_MODEL_NO_QUOTA",
10997
+ message: `Model '${model}' has 0 quota in ${region}.`,
10998
+ hint: `Pick a quota'd model (--model ${quotad[0] ?? "gpt-4.1-mini"}) or request quota in ${region}.`
10999
+ });
11000
+ }
11001
+ return verdict === "ok" ? "ok" : "unknown";
11002
+ }
11003
+ async function ensureAccount(args) {
11004
+ let existing = null;
11005
+ try {
11006
+ existing = JSON.parse(
11007
+ await runAz(["cognitiveservices", "account", "show", "--name", args.account, "--resource-group", args.resourceGroup, "--output", "json"])
11008
+ );
11009
+ } catch (e) {
11010
+ rethrowIfAzUnavailable(e);
11011
+ existing = null;
11012
+ }
11013
+ if (existing?.properties?.customSubDomainName && existing.properties.allowProjectManagement) {
11014
+ return { created: false, accountScope: existing.id ?? "", subdomain: existing.properties.customSubDomainName };
11015
+ }
11016
+ let createdOut;
11017
+ try {
11018
+ createdOut = JSON.parse(
11019
+ await runAz([
11020
+ "cognitiveservices",
11021
+ "account",
11022
+ "create",
11023
+ "--name",
11024
+ args.account,
11025
+ "--resource-group",
11026
+ args.resourceGroup,
11027
+ "--location",
11028
+ args.location,
11029
+ "--kind",
11030
+ "AIServices",
11031
+ "--sku",
11032
+ "S0",
11033
+ "--custom-domain",
11034
+ args.account,
11035
+ "--allow-project-management",
11036
+ "--yes",
11037
+ "--output",
11038
+ "json"
11039
+ ])
11040
+ );
11041
+ } catch (e) {
11042
+ const msg = e instanceof Error ? e.message : String(e);
11043
+ if (/subdomain|custom.?domain|already in use|already exists/i.test(msg)) {
11044
+ throw new LocalCliError({
11045
+ code: "FOUNDRY_ACCOUNT_SUBDOMAIN_CONFLICT",
11046
+ message: `The custom subdomain '${args.account}' is unavailable (globally taken).`,
11047
+ hint: "Pass --account <globally-unique-name>."
11048
+ });
11049
+ }
11050
+ throw e;
11051
+ }
11052
+ if (!createdOut.properties?.customSubDomainName || !createdOut.properties.allowProjectManagement) {
11053
+ throw new LocalCliError({
11054
+ code: "FOUNDRY_ACCOUNT_NOT_CONFORMING",
11055
+ message: `Account '${args.account}' exists but lacks a custom subdomain or project management, and could not be converged.`,
11056
+ hint: "Use a fresh --account name or resource group."
11057
+ });
11058
+ }
11059
+ return { created: !existing, accountScope: createdOut.id ?? "", subdomain: createdOut.properties.customSubDomainName };
11060
+ }
11061
+ function readProjectEndpoint(show) {
11062
+ const ep = show.properties?.endpoints?.["AI Foundry API"];
11063
+ if (!ep) {
11064
+ throw new LocalCliError({
11065
+ code: "FOUNDRY_PROJECT_NO_ENDPOINT",
11066
+ message: "Project created but no 'AI Foundry API' endpoint was returned.",
11067
+ hint: "Re-run after a moment, or check 'az cognitiveservices account project show'."
11068
+ });
11069
+ }
11070
+ return ep;
11071
+ }
11072
+ var projectShowArgs = (a) => [
11073
+ "cognitiveservices",
11074
+ "account",
11075
+ "project",
11076
+ "show",
11077
+ "--name",
11078
+ a.account,
11079
+ "--resource-group",
11080
+ a.resourceGroup,
11081
+ "--project-name",
11082
+ a.project,
11083
+ "--output",
11084
+ "json"
11085
+ ];
11086
+ async function ensureProject(args) {
11087
+ let show = null;
11088
+ try {
11089
+ show = JSON.parse(await runAz(projectShowArgs(args)));
11090
+ } catch (e) {
11091
+ rethrowIfAzUnavailable(e);
11092
+ show = null;
11093
+ }
11094
+ if (show) {
11095
+ return { created: false, endpoint: readProjectEndpoint(show), projectPrincipalId: show.identity?.principalId ?? null };
11096
+ }
11097
+ await runAz([
11098
+ "cognitiveservices",
11099
+ "account",
11100
+ "project",
11101
+ "create",
11102
+ "--name",
11103
+ args.account,
11104
+ "--resource-group",
11105
+ args.resourceGroup,
11106
+ "--project-name",
11107
+ args.project,
11108
+ "--location",
11109
+ args.location,
11110
+ "--output",
11111
+ "json"
11112
+ ]);
11113
+ const created = JSON.parse(await runAz(projectShowArgs(args)));
11114
+ return { created: true, endpoint: readProjectEndpoint(created), projectPrincipalId: created.identity?.principalId ?? null };
11115
+ }
11116
+ async function ensureDeployment(args) {
11117
+ let current = null;
11118
+ try {
11119
+ const show = JSON.parse(
11120
+ await runAz(["cognitiveservices", "account", "deployment", "show", "--name", args.account, "--resource-group", args.resourceGroup, "--deployment-name", args.model, "--output", "json"])
11121
+ );
11122
+ current = show.sku?.capacity ?? null;
11123
+ } catch (e) {
11124
+ rethrowIfAzUnavailable(e);
11125
+ current = null;
11126
+ }
11127
+ if (current !== null && current >= args.capacity) return { created: false };
11128
+ if (current !== null && current < args.capacity) args.onProgress?.(`scaling ${args.model} ${String(current)}\u2192${String(args.capacity)}`);
11129
+ await runAz([
11130
+ "cognitiveservices",
11131
+ "account",
11132
+ "deployment",
11133
+ "create",
11134
+ "--name",
11135
+ args.account,
11136
+ "--resource-group",
11137
+ args.resourceGroup,
11138
+ "--deployment-name",
11139
+ args.model,
11140
+ "--model-name",
11141
+ args.model,
11142
+ "--model-version",
11143
+ args.modelVersion,
11144
+ "--model-format",
11145
+ "OpenAI",
11146
+ "--sku-name",
11147
+ "GlobalStandard",
11148
+ "--sku-capacity",
11149
+ String(args.capacity),
11150
+ "--output",
11151
+ "json"
11152
+ ]);
11153
+ return { created: current === null };
11154
+ }
11155
+ var REASONING_MIN_CAPACITY = 250;
11156
+ async function createFoundryProject(args) {
11157
+ assertHostedRegion(args.location);
11158
+ if (!args.skipQuotaCheck) {
11159
+ const verdict = await checkModelQuota2(args.location, args.model);
11160
+ if (verdict === "unknown") {
11161
+ args.onProgress?.(`could not confirm quota for '${args.model}' in ${args.location}; proceeding`);
11162
+ }
11163
+ }
11164
+ if (isReasoningModel(args.model) && args.capacity < REASONING_MIN_CAPACITY) {
11165
+ args.onProgress?.(`note: reasoning model '${args.model}' at capacity ${String(args.capacity)} may 429; consider --capacity ${String(REASONING_MIN_CAPACITY)}`);
11166
+ }
11167
+ args.onProgress?.(`ensuring Foundry account ${args.account}\u2026`);
11168
+ const acct = await ensureAccount({ account: args.account, resourceGroup: args.resourceGroup, location: args.location });
11169
+ args.onProgress?.(`ensuring project ${args.project}\u2026`);
11170
+ const proj = await ensureProject({ account: args.account, resourceGroup: args.resourceGroup, project: args.project, location: args.location });
11171
+ args.onProgress?.(`ensuring model deployment ${args.model} (capacity ${String(args.capacity)})\u2026`);
11172
+ const dep = await ensureDeployment({
11173
+ account: args.account,
11174
+ resourceGroup: args.resourceGroup,
11175
+ model: args.model,
11176
+ modelVersion: args.modelVersion,
11177
+ capacity: args.capacity,
11178
+ onProgress: args.onProgress
11179
+ });
11180
+ return {
11181
+ endpoint: proj.endpoint,
11182
+ accountName: args.account,
11183
+ accountScope: acct.accountScope,
11184
+ projectName: args.project,
11185
+ region: args.location,
11186
+ projectPrincipalId: proj.projectPrincipalId,
11187
+ model: args.model,
11188
+ capacity: args.capacity,
11189
+ created: { account: acct.created, project: proj.created, deployment: dep.created }
11190
+ };
11191
+ }
11192
+
11193
+ // src/commands/foundry/create.ts
11194
+ init_errors();
11195
+ var FoundryCreateCommand = class extends M8tCommand {
11196
+ static paths = [["foundry", "create"]];
11197
+ static usage = Command41.Usage({
11198
+ description: "Create an AI Foundry (AIServices) account + project + model deployment from scratch.",
11199
+ details: "Non-interactive and idempotent. Creates the AIServices account (custom subdomain + project management), a project, and a model deployment (default gpt-4.1-mini @ capacity 50). Region must be hosted-agent-eligible. Emits the project endpoint as structured output. Re-run is a clean no-op (account/project skipped if present; deployment capacity converges UP, never down).",
11200
+ examples: [
11201
+ ["Create from zero in a resource group", "$0 foundry create --resource-group rg-m8t-stack --location eastus2"],
11202
+ ["Pick names + model", "$0 foundry create --resource-group rg-m8t-stack --location eastus2 --account myfoundry --project m8t --model gpt-4.1-mini"],
11203
+ ["Higher capacity for a reasoning model", "$0 foundry create --resource-group rg-m8t-stack --location eastus2 --model gpt-5-mini --capacity 250"]
11204
+ ]
11205
+ });
11206
+ resourceGroup = Option39.String("--resource-group");
11207
+ location = Option39.String("--location");
11208
+ account = Option39.String("--account");
11209
+ project = Option39.String("--project", "m8t");
11210
+ model = Option39.String("--model", "gpt-4.1-mini");
11211
+ modelVersion = Option39.String("--model-version", "2025-04-14");
11212
+ capacity = Option39.String("--capacity", "50");
11213
+ subscription = Option39.String("--subscription");
11214
+ skipQuotaCheck = Option39.Boolean("--skip-quota-check", false);
11215
+ output = Option39.String("--output");
11216
+ async executeCommand() {
11217
+ const mode = resolveOutputMode(
11218
+ this.output,
11219
+ this.context.stdout
11220
+ );
11221
+ const resourceGroup = typeof this.resourceGroup === "string" ? this.resourceGroup : void 0;
11222
+ const location = typeof this.location === "string" ? this.location : void 0;
11223
+ if (!resourceGroup) {
11224
+ throw new LocalCliError({ code: "USAGE", message: "--resource-group is required.", hint: "Example: m8t foundry create --resource-group rg-m8t-stack --location eastus2" });
11225
+ }
11226
+ if (!location) {
11227
+ throw new LocalCliError({ code: "USAGE", message: "--location is required.", hint: "Example: m8t foundry create --resource-group rg-m8t-stack --location eastus2" });
11228
+ }
11229
+ const subscriptionOpt = typeof this.subscription === "string" ? this.subscription : void 0;
11230
+ if (subscriptionOpt) await runAz(["account", "set", "--subscription", subscriptionOpt]);
11231
+ const account = await getAzAccount();
11232
+ const subscriptionId = subscriptionOpt ?? account.subscriptionId;
11233
+ const accountName = typeof this.account === "string" ? this.account : deriveAccountName(subscriptionId);
11234
+ const project = typeof this.project === "string" ? this.project : "m8t";
11235
+ const model = typeof this.model === "string" ? this.model : "gpt-4.1-mini";
11236
+ const modelVersion = typeof this.modelVersion === "string" ? this.modelVersion : "2025-04-14";
11237
+ const capacity = typeof this.capacity === "string" ? Number(this.capacity) : 50;
11238
+ const onProgress = mode === "pretty" ? (m) => this.context.stderr.write(` ${colors.dim(m)}
11239
+ `) : void 0;
11240
+ const result = await createFoundryProject({
11241
+ resourceGroup,
11242
+ location,
11243
+ account: accountName,
11244
+ project,
11245
+ model,
11246
+ modelVersion,
11247
+ capacity,
11248
+ // `=== true`: clipanion's Option descriptor is truthy until argv parse, so a
11249
+ // bare check would force skipQuotaCheck on under test construction.
11250
+ skipQuotaCheck: this.skipQuotaCheck === true,
11251
+ onProgress
11252
+ });
11253
+ if (mode === "json") {
11254
+ this.context.stdout.write(
11255
+ renderJson({
11256
+ endpoint: result.endpoint,
11257
+ accountName: result.accountName,
11258
+ accountResourceId: result.accountScope,
11259
+ projectName: result.projectName,
11260
+ region: result.region,
11261
+ model: result.model,
11262
+ capacity: result.capacity,
11263
+ created: result.created
11264
+ }) + "\n"
11265
+ );
11266
+ return 0;
11267
+ }
11268
+ this.context.stdout.write(
11269
+ renderKeyValueBlock([
11270
+ { key: "endpoint", value: result.endpoint },
11271
+ { key: "account", value: result.accountName },
11272
+ { key: "project", value: result.projectName },
11273
+ { key: "region", value: result.region },
11274
+ { key: "model", value: `${result.model} (capacity ${String(result.capacity)})` }
11275
+ ]) + "\n"
11276
+ );
11277
+ const made = Object.entries(result.created).filter(([, v]) => v).map(([k]) => k);
11278
+ this.context.stdout.write(colors.dim(`created: ${made.length ? made.join(", ") : "nothing (all present)"}
11279
+ `));
11280
+ this.context.stdout.write(colors.dim(`next: 'm8t deploy --foundry-endpoint ${result.endpoint}' to deploy the gateway.
11281
+ `));
11282
+ return 0;
11283
+ }
11284
+ };
11285
+
10867
11286
  // src/cli.ts
10868
11287
  var cli = new Cli({
10869
11288
  binaryName: "m8t",
@@ -10910,6 +11329,7 @@ cli.register(A2aDisableCommand);
10910
11329
  cli.register(EvalSkillCommand);
10911
11330
  cli.register(EvalExamCommand);
10912
11331
  cli.register(DreamRunCommand);
11332
+ cli.register(FoundryCreateCommand);
10913
11333
  async function main() {
10914
11334
  try {
10915
11335
  return await cli.run(process.argv.slice(2));