@m8t-stack/cli 0.2.23 → 0.2.25

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
@@ -604,6 +604,22 @@ async function authedJsonResult(args) {
604
604
  async function authedJson(args) {
605
605
  return (await authedJsonResult(args)).data;
606
606
  }
607
+ async function authedJsonPagedValues(args) {
608
+ const maxPages = args.maxPages ?? 100;
609
+ const items = [];
610
+ let url = args.url;
611
+ for (let page = 0; url && page < maxPages; page++) {
612
+ const body = await authedJson({
613
+ credential: args.credential,
614
+ scope: args.scope,
615
+ method: "GET",
616
+ url
617
+ });
618
+ items.push(...body?.value ?? []);
619
+ url = body?.nextLink;
620
+ }
621
+ return items;
622
+ }
607
623
  var init_http = __esm({
608
624
  "src/lib/http.ts"() {
609
625
  "use strict";
@@ -1231,7 +1247,7 @@ var init_enable_hosted_brain = __esm({
1231
1247
  import { Builtins, Cli } from "clipanion";
1232
1248
 
1233
1249
  // src/lib/package-version.ts
1234
- var CLI_VERSION = "0.2.23";
1250
+ var CLI_VERSION = "0.2.25";
1235
1251
 
1236
1252
  // src/lib/render-error.ts
1237
1253
  init_errors();
@@ -13663,23 +13679,22 @@ var ACCOUNTS_API = "2024-10-01";
13663
13679
  var PROJECTS_API = "2025-04-01-preview";
13664
13680
  async function listProjectsForAccount(credential2, accountId) {
13665
13681
  const url = `${ARM}${accountId}/projects?api-version=${PROJECTS_API}`;
13666
- return await authedJson({ credential: credential2, scope: ARM_SCOPE2, method: "GET", url }) ?? {};
13682
+ return await authedJsonPagedValues({ credential: credential2, scope: ARM_SCOPE2, url });
13667
13683
  }
13668
13684
  function formatProjectCandidate(c) {
13669
13685
  return `${c.projectName} (account=${c.accountName}, ${c.region})`;
13670
13686
  }
13671
13687
  async function resolveFoundryProject(opts) {
13672
13688
  const candidates = [];
13673
- const accounts = await authedJson({
13689
+ const accounts = await authedJsonPagedValues({
13674
13690
  credential: opts.credential,
13675
13691
  scope: ARM_SCOPE2,
13676
- method: "GET",
13677
13692
  url: `${ARM}/subscriptions/${opts.subscriptionId}/providers/Microsoft.CognitiveServices/accounts?api-version=${ACCOUNTS_API}`
13678
- }) ?? {};
13679
- for (const acc of accounts.value ?? []) {
13693
+ });
13694
+ for (const acc of accounts) {
13680
13695
  if (acc.kind !== "AIServices" || !acc.name || !acc.id) continue;
13681
13696
  const projects = await listProjectsForAccount(opts.credential, acc.id);
13682
- for (const proj of projects.value ?? []) {
13697
+ for (const proj of projects) {
13683
13698
  if (!proj.name) continue;
13684
13699
  const projectName = proj.name.includes("/") ? proj.name.slice(proj.name.lastIndexOf("/") + 1) : proj.name;
13685
13700
  candidates.push({
@@ -14297,6 +14312,14 @@ function run(cmd, cmdArgs, cwd, env) {
14297
14312
  c.stderr.on("data", (d) => {
14298
14313
  err += d.toString();
14299
14314
  });
14315
+ c.on("error", (e) => {
14316
+ reject(
14317
+ new LocalCliError({
14318
+ code: "GH_APP_PUSH_FAILED",
14319
+ message: `${cmd} ${cmdArgs.join(" ")} could not run: ${e.message}`
14320
+ })
14321
+ );
14322
+ });
14300
14323
  c.on("close", (code) => {
14301
14324
  if (code === 0) {
14302
14325
  resolve3();
@@ -14627,6 +14650,19 @@ async function promptCollision(ctx, info) {
14627
14650
  rl.close();
14628
14651
  }
14629
14652
  }
14653
+ function resolveScaffoldCommit(repoRoot, env) {
14654
+ const baked = env.M8T_BUILD_SHA?.trim();
14655
+ if (baked) return baked;
14656
+ try {
14657
+ return execFileSync("git", ["rev-parse", "HEAD"], {
14658
+ cwd: repoRoot,
14659
+ encoding: "utf8",
14660
+ stdio: ["ignore", "pipe", "ignore"]
14661
+ }).trim() || "(scaffold)";
14662
+ } catch {
14663
+ return "(scaffold)";
14664
+ }
14665
+ }
14630
14666
  function stageAsGitRepo(dir) {
14631
14667
  const steps = [
14632
14668
  { args: ["init", "-b", "main"], label: "git init" },
@@ -14770,7 +14806,7 @@ var BrainCreateCommand = class extends M8tCommand {
14770
14806
  if (needsPush) {
14771
14807
  materializeBrainTree({ templateSrc, destDir: tmpDir, seedDir });
14772
14808
  if (seedName && seedDir) {
14773
- const headSha = execFileSync("git", ["rev-parse", "HEAD"], { cwd: repoRoot, encoding: "utf8" }).trim();
14809
+ const headSha = resolveScaffoldCommit(repoRoot, env);
14774
14810
  writeSeedMarker({ destDir: tmpDir, seedName, seedDir, refreshedToCommit: headSha, version: "(scaffold)" });
14775
14811
  }
14776
14812
  this.context.stdout.write(
@@ -24397,13 +24433,12 @@ var STORAGE_API2 = "2023-05-01";
24397
24433
  var LA_API = "2023-09-01";
24398
24434
  async function discoverLedgerResources(opts) {
24399
24435
  const { credential: credential2, subscriptionId, resourceGroup } = opts;
24400
- const storage = await authedJson({
24436
+ const storage = await authedJsonPagedValues({
24401
24437
  credential: credential2,
24402
24438
  scope: ARM_SCOPE8,
24403
- method: "GET",
24404
24439
  url: `${ARM4}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.Storage/storageAccounts?api-version=${STORAGE_API2}`
24405
- }) ?? {};
24406
- const accounts = (storage.value ?? []).filter((a) => a.properties?.primaryEndpoints?.table);
24440
+ });
24441
+ const accounts = storage.filter((a) => a.properties?.primaryEndpoints?.table);
24407
24442
  const ledgerAcct = accounts.find((a) => (a.tags?.["m8t-stack:role"] ?? a.properties?.tags?.["m8t-stack:role"]) === "ledger") ?? (accounts.length === 1 ? accounts[0] : void 0);
24408
24443
  const ledgerTableEndpoint = ledgerAcct?.properties?.primaryEndpoints?.table?.replace(/\/$/, "");
24409
24444
  if (!ledgerTableEndpoint) {
@@ -24413,13 +24448,11 @@ async function discoverLedgerResources(opts) {
24413
24448
  hint: `Found: ${accounts.map((a) => a.name).join(", ") || "(none)"}. Tag the ledger account 'm8t-stack:role=ledger'.`
24414
24449
  });
24415
24450
  }
24416
- const laList = await authedJson({
24451
+ const ws = await authedJsonPagedValues({
24417
24452
  credential: credential2,
24418
24453
  scope: ARM_SCOPE8,
24419
- method: "GET",
24420
24454
  url: `${ARM4}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/providers/Microsoft.OperationalInsights/workspaces?api-version=${LA_API}`
24421
- }) ?? {};
24422
- const ws = laList.value ?? [];
24455
+ });
24423
24456
  const workspaceId = (ws.length === 1 ? ws[0] : ws.find((w) => w.properties?.customerId))?.properties?.customerId;
24424
24457
  if (!workspaceId) {
24425
24458
  throw new LocalCliError({
@@ -25388,6 +25421,7 @@ init_errors();
25388
25421
 
25389
25422
  // src/lib/bootstrap-mi.ts
25390
25423
  var INSTALLER_GRANT_DESCRIPTION = "m8t-installer auto-reap (bootstrap)";
25424
+ var GATEWAY_SUBSCOPE_GRANT_DESCRIPTION = "m8t-gateway auto-reap (bootstrap)";
25391
25425
  async function ensureResourceGroup2(opts) {
25392
25426
  const existing = (await runAz(["group", "show", "--name", opts.name, "--subscription", opts.subscriptionId, "-o", "json"]).catch(() => "")).trim();
25393
25427
  if (existing) return;
@@ -25631,7 +25665,7 @@ async function writeBootstrapState(state, home = os13.homedir()) {
25631
25665
  // src/commands/bootstrap/launch.ts
25632
25666
  var DEFAULT_RG = "rg-m8t-stack";
25633
25667
  var DEFAULT_INSTALLER = "ghcr.io/m8t-labs/m8t-installer";
25634
- var DEFAULT_INSTALLER_TAG = "v0.1.30";
25668
+ var DEFAULT_INSTALLER_TAG = "v0.1.32";
25635
25669
  var ACI_NAME = "m8t-installer";
25636
25670
  var MI_NAME = "m8t-installer-mi";
25637
25671
  var BootstrapLaunchCommand = class extends M8tCommand {
@@ -25642,8 +25676,8 @@ var BootstrapLaunchCommand = class extends M8tCommand {
25642
25676
  examples: [
25643
25677
  ["Launch in eastus2", "$0 bootstrap launch --location eastus2"],
25644
25678
  ["BYO app registration", "$0 bootstrap launch --location eastus2 --client-id <appId>"],
25645
- ["Pin a specific installer tag", "$0 bootstrap launch --location eastus2 --installer-tag v0.1.30"],
25646
- ["Override the full installer image ref", "$0 bootstrap launch --location eastus2 --installer-image ghcr.io/m8t-labs/m8t-installer:v0.1.30"]
25679
+ ["Pin a specific installer tag", "$0 bootstrap launch --location eastus2 --installer-tag v0.1.32"],
25680
+ ["Override the full installer image ref", "$0 bootstrap launch --location eastus2 --installer-image ghcr.io/m8t-labs/m8t-installer:v0.1.32"]
25647
25681
  ]
25648
25682
  });
25649
25683
  location = Option46.String("--location");
@@ -25869,35 +25903,53 @@ init_errors();
25869
25903
 
25870
25904
  // src/lib/bootstrap-reap.ts
25871
25905
  var ROLE_ASSIGNMENT_API = "2022-04-01";
25872
- async function sweepOrphanOwnerAssignments(opts) {
25873
- const subScope = `/subscriptions/${opts.subscriptionId}`;
25874
- const all = JSON.parse(
25906
+ var GATEWAY_SUBSCOPE_ROLES = ["Cost Management Reader", "Monitoring Reader"];
25907
+ async function listSubscriptionAssignments(subscriptionId) {
25908
+ return JSON.parse(
25875
25909
  await runAz([
25876
25910
  "role",
25877
25911
  "assignment",
25878
25912
  "list",
25879
25913
  "--all",
25880
25914
  "--subscription",
25881
- opts.subscriptionId,
25915
+ subscriptionId,
25882
25916
  "-o",
25883
25917
  "json"
25884
25918
  ]).catch(() => "[]") || "[]"
25885
25919
  );
25920
+ }
25921
+ async function deleteAssignmentsById(ids) {
25922
+ for (const id of ids) {
25923
+ await runAz([
25924
+ "rest",
25925
+ "--method",
25926
+ "delete",
25927
+ "--url",
25928
+ `https://management.azure.com${id}?api-version=${ROLE_ASSIGNMENT_API}`
25929
+ ]).catch(() => void 0);
25930
+ }
25931
+ }
25932
+ async function sweepOrphanGatewaySubRoles(opts) {
25933
+ const subScope = `/subscriptions/${opts.subscriptionId}`;
25934
+ const all = await listSubscriptionAssignments(opts.subscriptionId);
25935
+ const matched = all.filter((r) => {
25936
+ if (r.principalName) return false;
25937
+ if (r.principalType !== "ServicePrincipal") return false;
25938
+ if (r.scope !== subScope) return false;
25939
+ if (r.description === GATEWAY_SUBSCOPE_GRANT_DESCRIPTION) return true;
25940
+ return GATEWAY_SUBSCOPE_ROLES.includes(r.roleDefinitionName ?? "");
25941
+ });
25942
+ if (opts.execute) await deleteAssignmentsById(matched.map((r) => r.id));
25943
+ return matched.map((r) => ({ id: r.id, description: r.description, roleDefinitionName: r.roleDefinitionName, createdOn: r.createdOn }));
25944
+ }
25945
+ async function sweepOrphanOwnerAssignments(opts) {
25946
+ const subScope = `/subscriptions/${opts.subscriptionId}`;
25947
+ const all = await listSubscriptionAssignments(opts.subscriptionId);
25886
25948
  const matched = all.filter((r) => {
25887
25949
  if (r.description === INSTALLER_GRANT_DESCRIPTION) return true;
25888
25950
  return !r.principalName && r.principalType === "ServicePrincipal" && r.roleDefinitionName === "Owner" && r.scope === subScope;
25889
25951
  });
25890
- if (opts.execute) {
25891
- for (const r of matched) {
25892
- await runAz([
25893
- "rest",
25894
- "--method",
25895
- "delete",
25896
- "--url",
25897
- `https://management.azure.com${r.id}?api-version=${ROLE_ASSIGNMENT_API}`
25898
- ]).catch(() => void 0);
25899
- }
25900
- }
25952
+ if (opts.execute) await deleteAssignmentsById(matched.map((r) => r.id));
25901
25953
  return matched.map((r) => ({ id: r.id, description: r.description, createdOn: r.createdOn }));
25902
25954
  }
25903
25955
  async function reapInstaller(opts) {
@@ -25954,22 +26006,30 @@ var BootstrapReapCommand = class extends M8tCommand {
25954
26006
  if (!sub) {
25955
26007
  throw new LocalCliError({ code: "BOOTSTRAP_SWEEP_NO_SUB", message: "No active subscription.", hint: "Run 'az account set --subscription <id>'." });
25956
26008
  }
25957
- const found = await sweepOrphanOwnerAssignments({ subscriptionId: sub, execute: this.yes === true });
25958
- if (found.length === 0) {
25959
- this.context.stdout.write(`${colors.success("\u2713")} No orphaned installer Owner@sub assignments found.
26009
+ const execute = this.yes === true;
26010
+ const owner = await sweepOrphanOwnerAssignments({ subscriptionId: sub, execute });
26011
+ const gateway = await sweepOrphanGatewaySubRoles({ subscriptionId: sub, execute });
26012
+ const total = owner.length + gateway.length;
26013
+ if (total === 0) {
26014
+ this.context.stdout.write(`${colors.success("\u2713")} No orphaned installer Owner@sub or gateway sub-scope assignments found.
25960
26015
  `);
25961
26016
  return 0;
25962
26017
  }
25963
- for (const f of found) {
25964
- this.context.stdout.write(` ${f.id}${f.createdOn ? colors.dim(` (created ${f.createdOn})`) : ""}${f.description ? colors.dim(" [tagged]") : ""}
26018
+ for (const f of owner) {
26019
+ this.context.stdout.write(` ${colors.dim("Owner@sub")} ${f.id}${f.createdOn ? colors.dim(` (created ${f.createdOn})`) : ""}${f.description ? colors.dim(" [tagged]") : ""}
26020
+ `);
26021
+ }
26022
+ for (const f of gateway) {
26023
+ 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]") : ""}
25965
26024
  `);
25966
26025
  }
25967
- if (this.yes === true) {
25968
- this.context.stdout.write(`${colors.success("\u2713")} Removed ${String(found.length)} orphaned Owner@sub assignment(s).
26026
+ const breakdown = `${String(owner.length)} Owner@sub, ${String(gateway.length)} gateway sub-scope`;
26027
+ if (execute) {
26028
+ this.context.stdout.write(`${colors.success("\u2713")} Removed ${String(total)} orphaned assignment(s) (${breakdown}).
25969
26029
  `);
25970
26030
  } else {
25971
26031
  this.context.stdout.write(`
25972
- Found ${String(found.length)} orphaned Owner@sub assignment(s) (dry-run). ${colors.hint("Re-run with --sweep-orphans --yes to delete them.")}
26032
+ Found ${String(total)} orphaned assignment(s) (${breakdown}) (dry-run). ${colors.hint("Re-run with --sweep-orphans --yes to delete them.")}
25973
26033
  `);
25974
26034
  }
25975
26035
  return 0;
@@ -25981,10 +26041,12 @@ Found ${String(found.length)} orphaned Owner@sub assignment(s) (dry-run). ${colo
25981
26041
  if (!sub) {
25982
26042
  throw new LocalCliError({ code: "BOOTSTRAP_SWEEP_NO_SUB", message: "No active subscription.", hint: "Run 'az account set --subscription <id>'." });
25983
26043
  }
25984
- const found = await sweepOrphanOwnerAssignments({ subscriptionId: sub, execute: true });
26044
+ const owner = await sweepOrphanOwnerAssignments({ subscriptionId: sub, execute: true });
26045
+ const gateway = await sweepOrphanGatewaySubRoles({ subscriptionId: sub, execute: true });
26046
+ const total = owner.length + gateway.length;
25985
26047
  this.context.stdout.write(
25986
- found.length === 0 ? `${colors.success("\u2713")} No bootstrap state and no orphaned installer Owner@sub assignments. Nothing to reap.
25987
- ` : `${colors.success("\u2713")} No bootstrap state found; swept ${String(found.length)} orphaned installer Owner@sub assignment(s).
26048
+ total === 0 ? `${colors.success("\u2713")} No bootstrap state and no orphaned installer Owner@sub or gateway sub-scope assignments. Nothing to reap.
26049
+ ` : `${colors.success("\u2713")} No bootstrap state found; swept ${String(total)} orphaned assignment(s) (${String(owner.length)} Owner@sub, ${String(gateway.length)} gateway sub-scope).
25988
26050
  `
25989
26051
  );
25990
26052
  return 0;
@@ -25992,7 +26054,7 @@ Found ${String(found.length)} orphaned Owner@sub assignment(s) (dry-run). ${colo
25992
26054
  throw new LocalCliError({
25993
26055
  code: "BOOTSTRAP_NO_STATE",
25994
26056
  message: "No bootstrap state to reap.",
25995
- hint: "Nothing to do. If a launch failed early it may have orphaned an installer Owner@sub grant \u2014 run 'm8t bootstrap reap --force' (or 'm8t bootstrap reap --sweep-orphans --yes') to clean it."
26057
+ 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."
25996
26058
  });
25997
26059
  }
25998
26060
  if (this.force !== true) {