@m8t-stack/cli 0.2.90 → 0.2.92

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
@@ -921,8 +921,8 @@ async function mirrorBrainYaml(args) {
921
921
  Accept: "application/vnd.github+json",
922
922
  "X-GitHub-Api-Version": "2022-11-28"
923
923
  };
924
- const MAX_ATTEMPTS2 = 3;
925
- for (let attempt = 1; attempt <= MAX_ATTEMPTS2; attempt++) {
924
+ const MAX_ATTEMPTS3 = 3;
925
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS3; attempt++) {
926
926
  const getRes = await fetch(url, { headers });
927
927
  let sha;
928
928
  let existing;
@@ -953,8 +953,8 @@ async function mirrorBrainYaml(args) {
953
953
  if (putRes.ok) return;
954
954
  const text = await putRes.text();
955
955
  const retryable = putRes.status === 409 || putRes.status === 422;
956
- if (retryable && attempt < MAX_ATTEMPTS2) continue;
957
- const suffix = retryable ? ` after ${MAX_ATTEMPTS2.toString()} attempts` : "";
956
+ if (retryable && attempt < MAX_ATTEMPTS3) continue;
957
+ const suffix = retryable ? ` after ${MAX_ATTEMPTS3.toString()} attempts` : "";
958
958
  throw new LocalCliError({
959
959
  code: "BRAIN_YAML_MIRROR_FAILED",
960
960
  message: `PUT .m8t/brain.yaml: HTTP ${putRes.status.toString()}${suffix}
@@ -1487,7 +1487,7 @@ function installFoundryDnsShim() {
1487
1487
  }
1488
1488
 
1489
1489
  // src/lib/package-version.ts
1490
- var CLI_VERSION = "0.2.90";
1490
+ var CLI_VERSION = "0.2.92";
1491
1491
 
1492
1492
  // src/lib/render-error.ts
1493
1493
  init_errors();
@@ -24829,7 +24829,7 @@ function reportWhatIf(r, opts) {
24829
24829
  const exitCode = r.unexpected.length === 0 ? 0 : 1;
24830
24830
  if (opts.redact) {
24831
24831
  const output = opts.json ? JSON.stringify({ summary: redactForReport(r), exitCode }, null, 2) : redactForLog(r);
24832
- return { output, exitCode };
24832
+ return { output, exitCode, ...opts.json ? { diagnostic: redactForLog(r) } : {} };
24833
24833
  }
24834
24834
  if (opts.json) {
24835
24835
  return { output: JSON.stringify({ summary: { noise: r.noise.length, pending: r.pending.length, unexpected: r.unexpected.length }, pending: r.pending, unexpected: r.unexpected, exitCode }, null, 2), exitCode };
@@ -25096,8 +25096,9 @@ var DeployCommand = class extends M8tCommand {
25096
25096
  });
25097
25097
  const changes = await runWhatIf({ resourceGroup: this.resourceGroup, repoRoot: repoRoot2, params: params2 });
25098
25098
  const classified = classifyWhatIf(changes);
25099
- const { output, exitCode } = reportWhatIf(classified, { json: mode === "json", redact: this.redact });
25099
+ const { output, exitCode, diagnostic } = reportWhatIf(classified, { json: mode === "json", redact: this.redact });
25100
25100
  this.context.stdout.write(output + "\n");
25101
+ if (diagnostic) this.context.stderr.write(diagnostic + "\n");
25101
25102
  return exitCode;
25102
25103
  } catch (e) {
25103
25104
  const { output, exitCode } = reportWhatIfFailure(e, { json: mode === "json", redact: this.redact });
@@ -31792,6 +31793,7 @@ async function getAciState(opts) {
31792
31793
  }
31793
31794
 
31794
31795
  // src/lib/bootstrap-finalize.ts
31796
+ init_errors();
31795
31797
  import * as fs39 from "fs/promises";
31796
31798
  import * as os20 from "os";
31797
31799
  import * as path44 from "path";
@@ -32708,10 +32710,10 @@ var DEFAULT_WORKERS = [
32708
32710
  { name: "ezra", label: "Ezra - Azure Expert", brain: "ezra-brain" }
32709
32711
  ];
32710
32712
  function renderInstallSummary(args) {
32711
- const open3 = args.webappUrl ? `${args.webappUrl} (or run: m8t open)` : "run: m8t open";
32713
+ const open4 = args.webappUrl ? `${args.webappUrl} (or run: m8t open)` : "run: m8t open";
32712
32714
  const lines = [
32713
32715
  "\u2705 Your m8t platform is ready to use.",
32714
- ` Open it: ${open3}`,
32716
+ ` Open it: ${open4}`,
32715
32717
  ""
32716
32718
  ];
32717
32719
  if (args.brainOrg) {
@@ -32735,10 +32737,10 @@ function renderInstallSummary(args) {
32735
32737
 
32736
32738
  // src/lib/companion-install.ts
32737
32739
  import { constants as constants2 } from "fs";
32738
- import * as fs37 from "fs/promises";
32739
- import * as path41 from "path";
32740
+ import * as fs38 from "fs/promises";
32741
+ import * as path42 from "path";
32740
32742
  import { randomUUID as randomUUID3 } from "crypto";
32741
- import { execFile, spawn as spawn7 } from "child_process";
32743
+ import { execFile as execFile2, spawn as spawn7 } from "child_process";
32742
32744
 
32743
32745
  // src/lib/companion-artifact.ts
32744
32746
  import { createHash as createHash7 } from "crypto";
@@ -33016,6 +33018,265 @@ async function copyArtifactPayload(sourceRoot, targetRoot, manifest) {
33016
33018
  await verifyArtifactPayload(targetRoot, manifest);
33017
33019
  }
33018
33020
 
33021
+ // src/lib/companion-download.ts
33022
+ import { createHash as createHash8 } from "crypto";
33023
+ import { execFile } from "child_process";
33024
+ import { createReadStream } from "fs";
33025
+ import * as fs37 from "fs/promises";
33026
+ import * as os18 from "os";
33027
+ import * as path41 from "path";
33028
+ init_errors();
33029
+ var MAX_ASSET_BYTES = 512 * 1024 * 1024;
33030
+ var MAX_ATTEMPTS2 = 3;
33031
+ var BACKOFF_MS = [1e3, 3e3];
33032
+ function megabytes(bytes) {
33033
+ return `${Math.round(bytes / 1e6).toString()} MB`;
33034
+ }
33035
+ function extractArchive(archive, into) {
33036
+ return new Promise((resolve6, reject) => {
33037
+ execFile("tar", ["-xzf", archive, "-C", into], { windowsHide: true }, (error) => {
33038
+ if (error) reject(new Error(`Could not unpack the companion build: ${error.message}`));
33039
+ else resolve6();
33040
+ });
33041
+ });
33042
+ }
33043
+ function companionDownloadDirectory(homeDirectory) {
33044
+ return path41.join(homeDirectory, ".m8t", "companion", "downloads");
33045
+ }
33046
+ function companionAssetFor(component, platform = process.platform, architecture = process.arch) {
33047
+ const target = companionTargetFor(platform, architecture);
33048
+ if (target === null) return null;
33049
+ const pinned = component.targets[target];
33050
+ return pinned ? { target, asset: pinned.asset, sha256: pinned.sha256 } : null;
33051
+ }
33052
+ function tooLarge(asset) {
33053
+ return new LocalCliError({
33054
+ code: "COMPANION_ASSET_TOO_LARGE",
33055
+ message: `${asset} is larger than this command will accept.`
33056
+ });
33057
+ }
33058
+ function parseContentRange(value) {
33059
+ const matched = /^bytes (\d+)-(\d+)\/(\d+|\*)$/u.exec((value ?? "").trim());
33060
+ if (!matched?.[1] || !matched[3]) return null;
33061
+ return {
33062
+ start: Number(matched[1]),
33063
+ total: matched[3] === "*" ? null : Number(matched[3])
33064
+ };
33065
+ }
33066
+ function declaredLength(response) {
33067
+ const raw = response.headers.get("content-length");
33068
+ if (raw === null) return null;
33069
+ const value = Number(raw);
33070
+ return Number.isSafeInteger(value) && value >= 0 ? value : null;
33071
+ }
33072
+ async function usableBytes(partial) {
33073
+ const status = await fs37.lstat(partial).catch(() => null);
33074
+ if (status === null) return 0;
33075
+ if (!status.isFile()) {
33076
+ await fs37.rm(partial, { recursive: true, force: true }).catch(() => void 0);
33077
+ if (await fs37.lstat(partial).then(() => true, () => false)) {
33078
+ throw new LocalCliError({
33079
+ code: "COMPANION_DOWNLOAD_PATH_OCCUPIED",
33080
+ message: `${partial} is not a file and could not be cleared.`,
33081
+ hint: "Something that is not a download is sitting where the companion build goes. Remove it, then run this again."
33082
+ });
33083
+ }
33084
+ return 0;
33085
+ }
33086
+ return status.size;
33087
+ }
33088
+ async function sweepForeignPartials(directory, keep) {
33089
+ const entries = await fs37.readdir(directory).catch(() => []);
33090
+ await Promise.all(
33091
+ entries.filter((name) => name.endsWith(".part") && name !== keep).map((name) => fs37.rm(path41.join(directory, name), { recursive: true, force: true }))
33092
+ );
33093
+ }
33094
+ function sha256File3(filePath) {
33095
+ return new Promise((resolve6, reject) => {
33096
+ const hash = createHash8("sha256");
33097
+ const stream = createReadStream(filePath);
33098
+ stream.on("error", reject);
33099
+ stream.on("data", (chunk) => hash.update(chunk));
33100
+ stream.on("end", () => {
33101
+ resolve6(hash.digest("hex"));
33102
+ });
33103
+ });
33104
+ }
33105
+ async function transferToDisk(input) {
33106
+ let lastError;
33107
+ for (let attempt = 0; attempt < MAX_ATTEMPTS2; attempt++) {
33108
+ let existing = await usableBytes(input.partial);
33109
+ if (existing > input.maxAssetBytes) {
33110
+ await fs37.rm(input.partial, { force: true }).catch(() => void 0);
33111
+ existing = 0;
33112
+ }
33113
+ let received = existing;
33114
+ let answered = false;
33115
+ try {
33116
+ const controller = new AbortController();
33117
+ const response = await input.fetchImpl(input.url, {
33118
+ signal: controller.signal,
33119
+ ...existing > 0 ? { headers: { Range: `bytes=${existing.toString()}-` } } : {}
33120
+ });
33121
+ answered = true;
33122
+ if (response.status === 416) {
33123
+ await response.body?.cancel().catch(() => void 0);
33124
+ if (existing > 0 && await sha256File3(input.partial).catch(() => null) === input.expectedDigest) {
33125
+ return;
33126
+ }
33127
+ await fs37.rm(input.partial, { force: true }).catch(() => void 0);
33128
+ lastError = new Error("the interrupted download no longer fits the published asset");
33129
+ continue;
33130
+ }
33131
+ if (!response.ok) {
33132
+ await response.body?.cancel().catch(() => void 0);
33133
+ const failed = new LocalCliError({
33134
+ code: "COMPANION_ASSET_FETCH_FAILED",
33135
+ message: `GET ${input.url} returned HTTP ${response.status.toString()}.`,
33136
+ hint: "The release the platform is pinned to should carry this file. Try again, or run 'm8t companion repair' once the network is back."
33137
+ });
33138
+ if (response.status < 500) throw failed;
33139
+ lastError = failed;
33140
+ if (attempt + 1 < MAX_ATTEMPTS2) {
33141
+ input.onEvent({ kind: "retrying", received: existing, attempt: attempt + 1, cause: "server-error" });
33142
+ await input.delay(BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]);
33143
+ }
33144
+ continue;
33145
+ }
33146
+ let offset;
33147
+ let total;
33148
+ if (response.status === 206) {
33149
+ const range = parseContentRange(response.headers.get("content-range"));
33150
+ if (range?.start !== existing) {
33151
+ await response.body?.cancel().catch(() => void 0);
33152
+ await fs37.rm(input.partial, { force: true }).catch(() => void 0);
33153
+ lastError = new Error("the server answered a different range than the one requested");
33154
+ continue;
33155
+ }
33156
+ offset = existing;
33157
+ total = range.total;
33158
+ } else {
33159
+ offset = 0;
33160
+ total = declaredLength(response);
33161
+ }
33162
+ const declaredBody = declaredLength(response);
33163
+ if (total !== null && total > input.maxAssetBytes || declaredBody !== null && declaredBody > input.maxAssetBytes) {
33164
+ await response.body?.cancel().catch(() => void 0);
33165
+ await fs37.rm(input.partial, { force: true }).catch(() => void 0);
33166
+ throw tooLarge(input.asset);
33167
+ }
33168
+ if (offset > 0) input.onEvent({ kind: "resumed", received: offset, total });
33169
+ received = offset;
33170
+ const body = response.body;
33171
+ if (body === null) throw new Error("the response carried no body");
33172
+ const handle = await fs37.open(input.partial, offset > 0 ? "a" : "w", 384);
33173
+ try {
33174
+ for await (const chunk of body) {
33175
+ received += chunk.byteLength;
33176
+ if (received > input.maxAssetBytes) {
33177
+ controller.abort();
33178
+ throw tooLarge(input.asset);
33179
+ }
33180
+ await handle.write(chunk);
33181
+ input.onEvent({ kind: "progress", received, total });
33182
+ }
33183
+ } finally {
33184
+ await handle.close();
33185
+ }
33186
+ if (total !== null && received < total) {
33187
+ throw new Error(
33188
+ `the transfer stopped at ${received.toString()} of ${total.toString()} bytes`
33189
+ );
33190
+ }
33191
+ return;
33192
+ } catch (error) {
33193
+ if (error instanceof LocalCliError) {
33194
+ if (error.code === "COMPANION_ASSET_TOO_LARGE") {
33195
+ await fs37.rm(input.partial, { force: true }).catch(() => void 0);
33196
+ }
33197
+ throw error;
33198
+ }
33199
+ lastError = error;
33200
+ if (attempt + 1 < MAX_ATTEMPTS2) {
33201
+ input.onEvent({
33202
+ kind: "retrying",
33203
+ received,
33204
+ attempt: attempt + 1,
33205
+ cause: answered ? "dropped" : "not-connected"
33206
+ });
33207
+ await input.delay(BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]);
33208
+ }
33209
+ }
33210
+ }
33211
+ const kept = await fs37.lstat(input.partial).then(
33212
+ (status) => status.isFile() ? status.size : 0,
33213
+ () => 0
33214
+ );
33215
+ const keptLabel = megabytes(kept);
33216
+ throw new LocalCliError({
33217
+ code: "COMPANION_DOWNLOAD_INCOMPLETE",
33218
+ message: "The companion build could not be downloaded.",
33219
+ ...keptLabel === "0 MB" ? {} : { hint: `The ${keptLabel} already downloaded is kept. Run it again to carry on from there.` },
33220
+ cause: lastError
33221
+ });
33222
+ }
33223
+ async function downloadCompanionArtifact(component, deps = {}) {
33224
+ const platform = deps.platform ?? process.platform;
33225
+ const architecture = deps.architecture ?? process.arch;
33226
+ const pinned = companionAssetFor(component, platform, architecture);
33227
+ if (pinned === null) {
33228
+ throw new LocalCliError({
33229
+ code: "COMPANION_NO_BUILD_FOR_HOST",
33230
+ message: `Release ${component.version} carries no desktop companion for ${platform}-${architecture}.`,
33231
+ hint: "The companions are built for macOS and Windows on arm64 and x64."
33232
+ });
33233
+ }
33234
+ const url = releaseAssetUrl(component.tag, pinned.asset);
33235
+ const directory = deps.downloadDirectory ?? companionDownloadDirectory(os18.homedir());
33236
+ const partialName = `${pinned.sha256}.part`;
33237
+ const partial = path41.join(directory, partialName);
33238
+ await fs37.mkdir(directory, { recursive: true, mode: 448 });
33239
+ await sweepForeignPartials(directory, partialName);
33240
+ await transferToDisk({
33241
+ url,
33242
+ asset: pinned.asset,
33243
+ partial,
33244
+ fetchImpl: deps.fetchImpl ?? fetch,
33245
+ onEvent: deps.onEvent ?? (() => void 0),
33246
+ delay: deps.delay ?? ((ms) => new Promise((resolve6) => setTimeout(resolve6, ms))),
33247
+ maxAssetBytes: deps.maxAssetBytes ?? MAX_ASSET_BYTES,
33248
+ expectedDigest: pinned.sha256
33249
+ });
33250
+ const digest = await sha256File3(partial);
33251
+ if (digest !== pinned.sha256) {
33252
+ await fs37.rm(partial, { force: true }).catch(() => void 0);
33253
+ throw new LocalCliError({
33254
+ code: "COMPANION_ASSET_DIGEST_MISMATCH",
33255
+ message: `${pinned.asset} does not match the digest the release pins for it.`,
33256
+ hint: "Nothing was unpacked. This is what a corrupted download or a substituted file looks like \u2014 retry, and report it if it persists."
33257
+ });
33258
+ }
33259
+ const root = await (deps.makeTemporaryDirectory ?? (() => fs37.mkdtemp(path41.join(os18.tmpdir(), "m8t-companion-"))))();
33260
+ const dispose = async () => {
33261
+ await fs37.rm(root, { recursive: true, force: true }).catch(() => void 0);
33262
+ };
33263
+ try {
33264
+ const unpacked = path41.join(root, "unpacked");
33265
+ await fs37.mkdir(unpacked, { recursive: false, mode: 448 });
33266
+ await (deps.extract ?? extractArchive)(partial, unpacked);
33267
+ return {
33268
+ version: component.version,
33269
+ artifactManifestPath: path41.join(unpacked, "artifact-v1.json"),
33270
+ dispose
33271
+ };
33272
+ } catch (error) {
33273
+ await dispose();
33274
+ throw error;
33275
+ } finally {
33276
+ await fs37.rm(partial, { force: true }).catch(() => void 0);
33277
+ }
33278
+ }
33279
+
33019
33280
  // src/lib/companion-install.ts
33020
33281
  function xmlEscape(value) {
33021
33282
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
@@ -33028,7 +33289,7 @@ function commandError(error, message) {
33028
33289
  }
33029
33290
  function execFileAsync(file2, args) {
33030
33291
  return new Promise((resolve6, reject) => {
33031
- execFile(file2, [...args], { windowsHide: true }, (error) => {
33292
+ execFile2(file2, [...args], { windowsHide: true }, (error) => {
33032
33293
  if (error) reject(commandError(error, "Login-item command failed"));
33033
33294
  else resolve6();
33034
33295
  });
@@ -33036,7 +33297,7 @@ function execFileAsync(file2, args) {
33036
33297
  }
33037
33298
  function execFileOutput(file2, args) {
33038
33299
  return new Promise((resolve6, reject) => {
33039
- execFile(
33300
+ execFile2(
33040
33301
  file2,
33041
33302
  [...args],
33042
33303
  { windowsHide: true },
@@ -33049,17 +33310,17 @@ function execFileOutput(file2, args) {
33049
33310
  }
33050
33311
  async function setCompanionStartAtLogin(input) {
33051
33312
  if (input.platform === "darwin") {
33052
- const launchAgents = path41.join(input.homeDirectory, "Library", "LaunchAgents");
33053
- const registration = path41.join(
33313
+ const launchAgents = path42.join(input.homeDirectory, "Library", "LaunchAgents");
33314
+ const registration = path42.join(
33054
33315
  launchAgents,
33055
33316
  "com.m8t.companion.plist"
33056
33317
  );
33057
33318
  await assertNotSymlink(registration, "Start-at-login registration");
33058
33319
  if (!input.enabled) {
33059
- await fs37.rm(registration, { force: true });
33320
+ await fs38.rm(registration, { force: true });
33060
33321
  return;
33061
33322
  }
33062
- await fs37.mkdir(launchAgents, { recursive: true, mode: 448 });
33323
+ await fs38.mkdir(launchAgents, { recursive: true, mode: 448 });
33063
33324
  const plist = '<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>com.m8t.companion</string><key>ProgramArguments</key><array><string>' + xmlEscape(input.executable) + "</string></array><key>RunAtLoad</key><true/></dict></plist>\n";
33064
33325
  await atomicWriteText(registration, plist, 384);
33065
33326
  return;
@@ -33111,7 +33372,7 @@ async function setCompanionStartAtLogin(input) {
33111
33372
  }
33112
33373
  async function getCompanionStartAtLogin(input) {
33113
33374
  if (input.platform === "darwin") {
33114
- const registration = path41.join(
33375
+ const registration = path42.join(
33115
33376
  input.homeDirectory,
33116
33377
  "Library",
33117
33378
  "LaunchAgents",
@@ -33173,26 +33434,29 @@ function assertSupported(options) {
33173
33434
  }
33174
33435
  function companionInstallPaths(options) {
33175
33436
  assertSupported(options);
33176
- const companionState = path41.join(
33437
+ const companionState = path42.join(
33177
33438
  options.homeDirectory,
33178
33439
  ".m8t",
33179
33440
  "companion"
33180
33441
  );
33181
- const targetRoot = options.platform === "darwin" ? path41.join(
33442
+ const targetRoot = options.platform === "darwin" ? path42.join(
33182
33443
  options.homeDirectory,
33183
33444
  "Applications",
33184
33445
  "m8t Companion.app"
33185
- ) : path41.join(
33186
- options.localAppData ?? path41.join(options.homeDirectory, "AppData", "Local"),
33446
+ ) : path42.join(
33447
+ options.localAppData ?? path42.join(options.homeDirectory, "AppData", "Local"),
33187
33448
  "m8t",
33188
33449
  "companion",
33189
33450
  "app"
33190
33451
  );
33191
33452
  return {
33192
33453
  targetRoot,
33193
- installManifest: path41.join(companionState, "install-v1.json"),
33194
- runtimeBinding: path41.join(companionState, "runtime-v1.json"),
33195
- preferences: path41.join(companionState, "preferences-v1.json")
33454
+ installManifest: path42.join(companionState, "install-v1.json"),
33455
+ runtimeBinding: path42.join(companionState, "runtime-v1.json"),
33456
+ preferences: path42.join(companionState, "preferences-v1.json"),
33457
+ // Asked of the downloader rather than rebuilt here. Two modules computing
33458
+ // one path is how an uninstall comes to sweep a directory nothing writes.
33459
+ downloads: companionDownloadDirectory(options.homeDirectory)
33196
33460
  };
33197
33461
  }
33198
33462
  function exactKeys2(value, keys) {
@@ -33203,7 +33467,7 @@ function parseInstallManifest(value) {
33203
33467
  throw new Error("Install manifest schema is invalid");
33204
33468
  }
33205
33469
  const record = value;
33206
- if (!exactKeys2(record, INSTALL_KEYS) || record.schemaVersion !== 1 || typeof record.version !== "string" || record.platform !== "darwin" && record.platform !== "win32" || record.architecture !== "arm64" && record.architecture !== "x64" || typeof record.entryRelativePath !== "string" || typeof record.artifactTreeSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(record.artifactTreeSha256) || typeof record.ownedTargetRoot !== "string" || !path41.isAbsolute(record.ownedTargetRoot)) {
33470
+ if (!exactKeys2(record, INSTALL_KEYS) || record.schemaVersion !== 1 || typeof record.version !== "string" || record.platform !== "darwin" && record.platform !== "win32" || record.architecture !== "arm64" && record.architecture !== "x64" || typeof record.entryRelativePath !== "string" || typeof record.artifactTreeSha256 !== "string" || !/^[a-f0-9]{64}$/u.test(record.artifactTreeSha256) || typeof record.ownedTargetRoot !== "string" || !path42.isAbsolute(record.ownedTargetRoot)) {
33207
33471
  throw new Error("Install manifest schema is invalid");
33208
33472
  }
33209
33473
  return record;
@@ -33213,7 +33477,7 @@ function parseRuntimeBinding(value, platform) {
33213
33477
  throw new Error("Runtime binding schema is invalid");
33214
33478
  }
33215
33479
  const record = value;
33216
- const paths = platform === "win32" ? path41.win32 : path41.posix;
33480
+ const paths = platform === "win32" ? path42.win32 : path42.posix;
33217
33481
  if (!exactKeys2(record, RUNTIME_KEYS) || record.schemaVersion !== 1 || typeof record.nodeExecutable !== "string" || !paths.isAbsolute(record.nodeExecutable) || typeof record.cliEntry !== "string" || !paths.isAbsolute(record.cliEntry) || typeof record.gatewayOrigin !== "string") {
33218
33482
  throw new Error("Runtime binding schema is invalid");
33219
33483
  }
@@ -33238,7 +33502,7 @@ function validateGatewayOrigin(value) {
33238
33502
  }
33239
33503
  async function assertNotSymlink(filePath, kind) {
33240
33504
  try {
33241
- if ((await fs37.lstat(filePath)).isSymbolicLink()) {
33505
+ if ((await fs38.lstat(filePath)).isSymbolicLink()) {
33242
33506
  throw new Error(`${kind} is a symbolic link`);
33243
33507
  }
33244
33508
  } catch (error) {
@@ -33247,20 +33511,20 @@ async function assertNotSymlink(filePath, kind) {
33247
33511
  }
33248
33512
  }
33249
33513
  async function assertOwnedDirectoryChain(anchor, targetDirectory) {
33250
- const relative4 = path41.relative(anchor, targetDirectory);
33251
- if (relative4 === ".." || relative4.startsWith(`..${path41.sep}`) || path41.isAbsolute(relative4)) {
33514
+ const relative4 = path42.relative(anchor, targetDirectory);
33515
+ if (relative4 === ".." || relative4.startsWith(`..${path42.sep}`) || path42.isAbsolute(relative4)) {
33252
33516
  throw new Error("Companion owned directory escapes its trusted anchor");
33253
33517
  }
33254
- const segments = relative4.split(path41.sep).filter(Boolean);
33518
+ const segments = relative4.split(path42.sep).filter(Boolean);
33255
33519
  let current = anchor;
33256
- const anchorStatus = await fs37.lstat(anchor);
33520
+ const anchorStatus = await fs38.lstat(anchor);
33257
33521
  if (anchorStatus.isSymbolicLink() || !anchorStatus.isDirectory()) {
33258
33522
  throw new Error("Companion owned directory anchor is unsafe");
33259
33523
  }
33260
33524
  for (const segment of segments) {
33261
- current = path41.join(current, segment);
33525
+ current = path42.join(current, segment);
33262
33526
  try {
33263
- const status = await fs37.lstat(current);
33527
+ const status = await fs38.lstat(current);
33264
33528
  if (status.isSymbolicLink() || !status.isDirectory()) {
33265
33529
  throw new Error("Companion owned directory contains a symbolic link");
33266
33530
  }
@@ -33273,19 +33537,19 @@ async function assertOwnedDirectoryChain(anchor, targetDirectory) {
33273
33537
  async function assertOwnedParents(options, paths) {
33274
33538
  await assertOwnedDirectoryChain(
33275
33539
  options.homeDirectory,
33276
- path41.dirname(paths.installManifest)
33540
+ path42.dirname(paths.installManifest)
33277
33541
  );
33278
- const targetAnchor = options.platform === "win32" ? options.localAppData ?? path41.join(options.homeDirectory, "AppData", "Local") : options.homeDirectory;
33279
- await assertOwnedDirectoryChain(targetAnchor, path41.dirname(paths.targetRoot));
33542
+ const targetAnchor = options.platform === "win32" ? options.localAppData ?? path42.join(options.homeDirectory, "AppData", "Local") : options.homeDirectory;
33543
+ await assertOwnedDirectoryChain(targetAnchor, path42.dirname(paths.targetRoot));
33280
33544
  }
33281
33545
  async function readRegularText(filePath, maxBytes) {
33282
- const before = await fs37.lstat(filePath);
33546
+ const before = await fs38.lstat(filePath);
33283
33547
  if (before.isSymbolicLink() || !before.isFile()) {
33284
33548
  throw new Error("Companion state file is not a regular file");
33285
33549
  }
33286
33550
  let handle;
33287
33551
  try {
33288
- handle = await fs37.open(
33552
+ handle = await fs38.open(
33289
33553
  filePath,
33290
33554
  constants2.O_RDONLY | constants2.O_NOFOLLOW
33291
33555
  );
@@ -33308,27 +33572,27 @@ async function atomicWriteJson(filePath, value) {
33308
33572
  }
33309
33573
  async function atomicWriteText(filePath, contents, mode) {
33310
33574
  await assertNotSymlink(filePath, "Companion state file");
33311
- await fs37.mkdir(path41.dirname(filePath), {
33575
+ await fs38.mkdir(path42.dirname(filePath), {
33312
33576
  recursive: true,
33313
33577
  mode: 448
33314
33578
  });
33315
33579
  const temporary = `${filePath}.${randomUUID3()}.tmp`;
33316
33580
  try {
33317
- await fs37.writeFile(temporary, contents, {
33581
+ await fs38.writeFile(temporary, contents, {
33318
33582
  mode,
33319
33583
  flag: "wx"
33320
33584
  });
33321
- await fs37.rename(temporary, filePath);
33322
- await fs37.chmod(filePath, mode).catch(() => void 0);
33585
+ await fs38.rename(temporary, filePath);
33586
+ await fs38.chmod(filePath, mode).catch(() => void 0);
33323
33587
  } finally {
33324
- await fs37.rm(temporary, { force: true }).catch(() => void 0);
33588
+ await fs38.rm(temporary, { force: true }).catch(() => void 0);
33325
33589
  }
33326
33590
  }
33327
33591
  async function realRegularFile(filePath, executable) {
33328
- const real = await fs37.realpath(filePath);
33329
- const stat5 = await fs37.stat(real);
33592
+ const real = await fs38.realpath(filePath);
33593
+ const stat5 = await fs38.stat(real);
33330
33594
  if (!stat5.isFile()) throw new Error("Companion launch target is not a file");
33331
- await fs37.access(real, executable ? constants2.X_OK : constants2.R_OK);
33595
+ await fs38.access(real, executable ? constants2.X_OK : constants2.R_OK);
33332
33596
  return real;
33333
33597
  }
33334
33598
  function defaultLaunch(executable) {
@@ -33344,7 +33608,7 @@ async function companionIsRunning(platform, executable) {
33344
33608
  if (platform !== "win32") return false;
33345
33609
  let handle;
33346
33610
  try {
33347
- handle = await fs37.open(executable, "r+");
33611
+ handle = await fs38.open(executable, "r+");
33348
33612
  } catch (error) {
33349
33613
  const code = error.code;
33350
33614
  if (code === "ENOENT") return false;
@@ -33425,7 +33689,7 @@ async function statusCompanion(options) {
33425
33689
  realRegularFile(runtime.nodeExecutable, options.platform !== "win32"),
33426
33690
  realRegularFile(runtime.cliEntry, false)
33427
33691
  ]);
33428
- const executable = path41.join(
33692
+ const executable = path42.join(
33429
33693
  paths.targetRoot,
33430
33694
  ...install.entryRelativePath.split("/")
33431
33695
  );
@@ -33458,7 +33722,7 @@ async function snapshotFile(filePath) {
33458
33722
  }
33459
33723
  async function restoreFile(filePath, bytes) {
33460
33724
  if (bytes === null) {
33461
- await fs37.rm(filePath, { force: true });
33725
+ await fs38.rm(filePath, { force: true });
33462
33726
  } else {
33463
33727
  await atomicWriteJson(filePath, JSON.parse(bytes.toString("utf8")));
33464
33728
  }
@@ -33473,8 +33737,8 @@ async function converge(options, force) {
33473
33737
  if (artifactManifest.platform !== options.platform || artifactManifest.architecture !== options.architecture) {
33474
33738
  throw new Error("Companion artifact does not match this OS and architecture");
33475
33739
  }
33476
- const payloadRoot = path41.join(
33477
- path41.dirname(options.artifactManifestPath),
33740
+ const payloadRoot = path42.join(
33741
+ path42.dirname(options.artifactManifestPath),
33478
33742
  "payload"
33479
33743
  );
33480
33744
  await verifyArtifactSource(payloadRoot, artifactManifest);
@@ -33494,7 +33758,7 @@ async function converge(options, force) {
33494
33758
  if (priorInstalled) {
33495
33759
  await assertCompanionNotRunning(
33496
33760
  options,
33497
- path41.join(
33761
+ path42.join(
33498
33762
  priorInstalled.paths.targetRoot,
33499
33763
  ...priorInstalled.install.entryRelativePath.split("/")
33500
33764
  )
@@ -33502,7 +33766,7 @@ async function converge(options, force) {
33502
33766
  }
33503
33767
  if (current.state === "not-installed") {
33504
33768
  try {
33505
- await fs37.lstat(paths.targetRoot);
33769
+ await fs38.lstat(paths.targetRoot);
33506
33770
  throw new Error(
33507
33771
  "The fixed companion target exists without an owned install manifest"
33508
33772
  );
@@ -33513,7 +33777,7 @@ async function converge(options, force) {
33513
33777
  const stage = `${paths.targetRoot}.m8t-stage-${randomUUID3()}`;
33514
33778
  const backup = `${paths.targetRoot}.m8t-backup-${randomUUID3()}`;
33515
33779
  const copy = options.copyPayload ?? copyArtifactPayload;
33516
- await fs37.mkdir(path41.dirname(paths.targetRoot), {
33780
+ await fs38.mkdir(path42.dirname(paths.targetRoot), {
33517
33781
  recursive: true,
33518
33782
  mode: 448
33519
33783
  });
@@ -33530,12 +33794,12 @@ async function converge(options, force) {
33530
33794
  try {
33531
33795
  await copy(payloadRoot, stage, artifactManifest);
33532
33796
  try {
33533
- await fs37.rename(paths.targetRoot, backup);
33797
+ await fs38.rename(paths.targetRoot, backup);
33534
33798
  movedPrior = true;
33535
33799
  } catch (error) {
33536
33800
  if (error.code !== "ENOENT") throw error;
33537
33801
  }
33538
- await fs37.rename(stage, paths.targetRoot);
33802
+ await fs38.rename(stage, paths.targetRoot);
33539
33803
  installedStage = true;
33540
33804
  const nodeExecutable = await realRegularFile(
33541
33805
  options.nodeExecutable,
@@ -33577,7 +33841,7 @@ async function converge(options, force) {
33577
33841
  await readClosedJson(paths.runtimeBinding),
33578
33842
  options.platform
33579
33843
  );
33580
- const executable = path41.join(
33844
+ const executable = path42.join(
33581
33845
  paths.targetRoot,
33582
33846
  ...artifactManifest.entryRelativePath.split("/")
33583
33847
  );
@@ -33587,7 +33851,7 @@ async function converge(options, force) {
33587
33851
  platform: options.platform,
33588
33852
  executable: ownedExecutable
33589
33853
  }));
33590
- priorLoginExecutable = priorInstalled ? path41.join(
33854
+ priorLoginExecutable = priorInstalled ? path42.join(
33591
33855
  priorInstalled.paths.targetRoot,
33592
33856
  ...priorInstalled.install.entryRelativePath.split("/")
33593
33857
  ) : executable;
@@ -33598,7 +33862,7 @@ async function converge(options, force) {
33598
33862
  );
33599
33863
  loginChanged = true;
33600
33864
  await (options.launch ?? defaultLaunch)(executable);
33601
- await fs37.rm(backup, { recursive: true, force: true });
33865
+ await fs38.rm(backup, { recursive: true, force: true });
33602
33866
  return {
33603
33867
  state: "installed",
33604
33868
  version: artifactManifest.version,
@@ -33612,15 +33876,15 @@ async function converge(options, force) {
33612
33876
  () => void 0
33613
33877
  );
33614
33878
  }
33615
- await fs37.rm(stage, { recursive: true, force: true }).catch(() => void 0);
33879
+ await fs38.rm(stage, { recursive: true, force: true }).catch(() => void 0);
33616
33880
  if (installedStage) {
33617
- await fs37.rm(paths.targetRoot, {
33881
+ await fs38.rm(paths.targetRoot, {
33618
33882
  recursive: true,
33619
33883
  force: true
33620
33884
  }).catch(() => void 0);
33621
33885
  }
33622
33886
  if (movedPrior) {
33623
- await fs37.rename(backup, paths.targetRoot).catch(() => void 0);
33887
+ await fs38.rename(backup, paths.targetRoot).catch(() => void 0);
33624
33888
  }
33625
33889
  await Promise.all([
33626
33890
  restoreFile(paths.installManifest, snapshots[0]),
@@ -33640,25 +33904,29 @@ async function uninstallCompanion(options) {
33640
33904
  assertSupported(options);
33641
33905
  const expectedPaths = companionInstallPaths(options);
33642
33906
  await assertOwnedParents(options, expectedPaths);
33907
+ const remove = options.removeOwnedPath ?? (async (ownedPath, recursive) => {
33908
+ await fs38.rm(ownedPath, { recursive, force: true });
33909
+ });
33643
33910
  const installed2 = await readInstalled(options);
33644
- if (!installed2) return { state: "not-installed" };
33911
+ if (!installed2) {
33912
+ await remove(expectedPaths.downloads, true);
33913
+ return { state: "not-installed" };
33914
+ }
33645
33915
  const { paths, install } = installed2;
33646
33916
  if (install.ownedTargetRoot !== paths.targetRoot) {
33647
33917
  throw new Error("Refusing to remove a non-owned companion target");
33648
33918
  }
33649
- const executable = path41.join(
33919
+ const executable = path42.join(
33650
33920
  paths.targetRoot,
33651
33921
  ...install.entryRelativePath.split("/")
33652
33922
  );
33653
33923
  await assertCompanionNotRunning(options, executable);
33654
33924
  await (options.setStartAtLogin ?? (() => Promise.resolve()))(executable, false);
33655
- const remove = options.removeOwnedPath ?? (async (ownedPath, recursive) => {
33656
- await fs37.rm(ownedPath, { recursive, force: true });
33657
- });
33658
33925
  await remove(paths.targetRoot, true);
33659
33926
  await remove(paths.installManifest, false);
33660
33927
  await remove(paths.runtimeBinding, false);
33661
33928
  await remove(paths.preferences, false);
33929
+ await remove(paths.downloads, true);
33662
33930
  return { state: "not-installed" };
33663
33931
  }
33664
33932
 
@@ -33679,84 +33947,55 @@ async function readCompanionRelease(source = { url: CHANNEL_LATEST_URL }, deps =
33679
33947
  };
33680
33948
  }
33681
33949
 
33682
- // src/lib/companion-download.ts
33683
- import { createHash as createHash8 } from "crypto";
33684
- import { execFile as execFile2 } from "child_process";
33685
- import * as fs38 from "fs/promises";
33686
- import * as os18 from "os";
33687
- import * as path42 from "path";
33688
- init_errors();
33689
- var MAX_ASSET_BYTES = 512 * 1024 * 1024;
33690
- function extractArchive(archive, into) {
33691
- return new Promise((resolve6, reject) => {
33692
- execFile2("tar", ["-xzf", archive, "-C", into], { windowsHide: true }, (error) => {
33693
- if (error) reject(new Error(`Could not unpack the companion build: ${error.message}`));
33694
- else resolve6();
33695
- });
33696
- });
33697
- }
33698
- function companionAssetFor(component, platform = process.platform, architecture = process.arch) {
33699
- const target = companionTargetFor(platform, architecture);
33700
- if (target === null) return null;
33701
- const pinned = component.targets[target];
33702
- return pinned ? { target, asset: pinned.asset, sha256: pinned.sha256 } : null;
33703
- }
33704
- async function downloadCompanionArtifact(component, deps = {}) {
33705
- const platform = deps.platform ?? process.platform;
33706
- const architecture = deps.architecture ?? process.arch;
33707
- const pinned = companionAssetFor(component, platform, architecture);
33708
- if (pinned === null) {
33709
- throw new LocalCliError({
33710
- code: "COMPANION_NO_BUILD_FOR_HOST",
33711
- message: `Release ${component.version} carries no desktop companion for ${platform}-${architecture}.`,
33712
- hint: "The companions are built for macOS and Windows on arm64 and x64."
33713
- });
33714
- }
33715
- const url = releaseAssetUrl(component.tag, pinned.asset);
33716
- const fetchImpl = deps.fetchImpl ?? fetch;
33717
- const response = await fetchImpl(url);
33718
- if (!response.ok) {
33719
- throw new LocalCliError({
33720
- code: "COMPANION_ASSET_FETCH_FAILED",
33721
- message: `GET ${url} returned HTTP ${response.status.toString()}.`,
33722
- hint: "The release the platform is pinned to should carry this file. Try again, or run 'm8t companion repair' once the network is back."
33723
- });
33724
- }
33725
- const bytes = Buffer.from(await response.arrayBuffer());
33726
- if (bytes.byteLength > MAX_ASSET_BYTES) {
33727
- throw new LocalCliError({
33728
- code: "COMPANION_ASSET_TOO_LARGE",
33729
- message: `${pinned.asset} is larger than this command will accept.`
33730
- });
33731
- }
33732
- const digest = createHash8("sha256").update(bytes).digest("hex");
33733
- if (digest !== pinned.sha256) {
33734
- throw new LocalCliError({
33735
- code: "COMPANION_ASSET_DIGEST_MISMATCH",
33736
- message: `${pinned.asset} does not match the digest the release pins for it.`,
33737
- hint: "Nothing was unpacked. This is what a corrupted download or a substituted file looks like \u2014 retry, and report it if it persists."
33738
- });
33739
- }
33740
- const root = await (deps.makeTemporaryDirectory ?? (() => fs38.mkdtemp(path42.join(os18.tmpdir(), "m8t-companion-"))))();
33741
- const dispose = async () => {
33742
- await fs38.rm(root, { recursive: true, force: true }).catch(() => void 0);
33950
+ // src/lib/companion-download-progress.ts
33951
+ var STEP_FRACTION = 0.25;
33952
+ var INTERVAL_MS = 3e4;
33953
+ function downloadProgressLines(options) {
33954
+ const now = options.now ?? Date.now;
33955
+ let lastStep = 0;
33956
+ let lastReceived = 0;
33957
+ let lastEmitAt = null;
33958
+ const emit = (line2) => {
33959
+ lastEmitAt = now();
33960
+ options.write(line2);
33961
+ };
33962
+ return (event) => {
33963
+ if (event.kind === "retrying") {
33964
+ emit(
33965
+ event.cause === "not-connected" ? " Couldn't connect \u2014 retrying.\n" : event.cause === "server-error" ? " The download service answered with an error \u2014 retrying.\n" : ` The connection dropped at ${megabytes(event.received)} \u2014 retrying.
33966
+ `
33967
+ );
33968
+ return;
33969
+ }
33970
+ if (event.kind === "resumed") {
33971
+ if (event.total !== null && event.total > 0) {
33972
+ lastStep = Math.floor(event.received / event.total / STEP_FRACTION);
33973
+ }
33974
+ lastReceived = event.received;
33975
+ emit(` Resuming an interrupted download from ${megabytes(event.received)}.
33976
+ `);
33977
+ return;
33978
+ }
33979
+ lastEmitAt ??= now();
33980
+ if (event.received < lastReceived) lastStep = 0;
33981
+ lastReceived = event.received;
33982
+ const total = event.total !== null && event.total > 0 ? event.total : null;
33983
+ if (total === null) {
33984
+ if (now() - lastEmitAt < INTERVAL_MS) return;
33985
+ emit(` ${megabytes(event.received)} downloaded.
33986
+ `);
33987
+ return;
33988
+ }
33989
+ const fraction = event.received / total;
33990
+ if (fraction >= 1) return;
33991
+ const step = Math.floor(fraction / STEP_FRACTION);
33992
+ if (step <= lastStep && now() - lastEmitAt < INTERVAL_MS) return;
33993
+ lastStep = step;
33994
+ emit(
33995
+ ` ${Math.floor(fraction * 100).toString()}% \xB7 ${megabytes(event.received)} of ${megabytes(total)}
33996
+ `
33997
+ );
33743
33998
  };
33744
- try {
33745
- const archive = path42.join(root, pinned.asset);
33746
- await fs38.writeFile(archive, bytes, { mode: 384 });
33747
- const unpacked = path42.join(root, "unpacked");
33748
- await fs38.mkdir(unpacked, { recursive: false, mode: 448 });
33749
- await (deps.extract ?? extractArchive)(archive, unpacked);
33750
- await fs38.rm(archive, { force: true });
33751
- return {
33752
- version: component.version,
33753
- artifactManifestPath: path42.join(unpacked, "artifact-v1.json"),
33754
- dispose
33755
- };
33756
- } catch (error) {
33757
- await dispose();
33758
- throw error;
33759
- }
33760
33999
  }
33761
34000
 
33762
34001
  // src/commands/companion/install.ts
@@ -33777,7 +34016,7 @@ function defaultLocalCompanionInstallOptions() {
33777
34016
  };
33778
34017
  }
33779
34018
  async function convergeCompanionFromChannel(converge2, options = {}) {
33780
- const { from: stagedDirectory, resourceGroup, platformVersion } = options;
34019
+ const { from: stagedDirectory, resourceGroup, platformVersion, onDownloadEvent } = options;
33781
34020
  let artifactManifestPath;
33782
34021
  let dispose = () => Promise.resolve();
33783
34022
  if (stagedDirectory !== void 0) {
@@ -33787,7 +34026,9 @@ async function convergeCompanionFromChannel(converge2, options = {}) {
33787
34026
  platformVersion !== void 0 ? { channel: true, version: platformTag(platformVersion) } : { url: CHANNEL_LATEST_URL }
33788
34027
  );
33789
34028
  if (release === null) return { state: "not-released" };
33790
- const build = await downloadCompanionArtifact(release.component);
34029
+ const build = await downloadCompanionArtifact(release.component, {
34030
+ ...onDownloadEvent !== void 0 ? { onEvent: onDownloadEvent } : {}
34031
+ });
33791
34032
  artifactManifestPath = build.artifactManifestPath;
33792
34033
  dispose = build.dispose;
33793
34034
  }
@@ -33840,11 +34081,13 @@ var CompanionInstallCommand = class extends M8tCommand {
33840
34081
  description: "Which deployment to bind to, when the subscription holds more than one."
33841
34082
  });
33842
34083
  async executeCommand() {
34084
+ const stdout = this.context.stdout;
33843
34085
  return runCompanionInstallCommand(
33844
- this.context.stdout,
34086
+ stdout,
33845
34087
  () => convergeCompanionFromChannel(installCompanion, {
33846
34088
  ...this.from !== void 0 ? { from: this.from } : {},
33847
- ...this.resourceGroup !== void 0 ? { resourceGroup: this.resourceGroup } : {}
34089
+ ...this.resourceGroup !== void 0 ? { resourceGroup: this.resourceGroup } : {},
34090
+ onDownloadEvent: downloadProgressLines({ write: (line2) => stdout.write(line2) })
33848
34091
  })
33849
34092
  );
33850
34093
  }
@@ -33864,7 +34107,7 @@ async function looksLikeCheckout(dir2) {
33864
34107
  return false;
33865
34108
  }
33866
34109
  }
33867
- var defaultDeps3 = {
34110
+ var defaultFinalizeDeps = Object.freeze({
33868
34111
  discoverGateway,
33869
34112
  writeConfig,
33870
34113
  ensureGatewayRedirectUri,
@@ -33872,12 +34115,13 @@ var defaultDeps3 = {
33872
34115
  reactiveSeed: reactiveSeedOnInstallComplete,
33873
34116
  companionsSupportHost,
33874
34117
  companionStatus: () => statusCompanion(defaultLocalCompanionInstallOptions()),
33875
- convergeCompanion: (platformVersion) => convergeCompanionFromChannel(installCompanion, {
33876
- ...platformVersion !== void 0 ? { platformVersion } : {}
34118
+ convergeCompanion: (platformVersion, onDownloadEvent) => convergeCompanionFromChannel(installCompanion, {
34119
+ ...platformVersion !== void 0 ? { platformVersion } : {},
34120
+ ...onDownloadEvent !== void 0 ? { onDownloadEvent } : {}
33877
34121
  }),
33878
34122
  homedir: () => os20.homedir()
33879
- };
33880
- async function finalizeInstall(args, deps = defaultDeps3) {
34123
+ });
34124
+ async function finalizeInstall(args, deps = defaultFinalizeDeps) {
33881
34125
  const markerDir = path44.join(deps.homedir(), ".m8t");
33882
34126
  const markerPath = path44.join(markerDir, "repo-root");
33883
34127
  const cwd = process.cwd();
@@ -34015,7 +34259,10 @@ ${colors.field("Then open a brand new chat/session")} \u2014 new skills + MCP se
34015
34259
  `
34016
34260
  );
34017
34261
  try {
34018
- const companion = await deps.convergeCompanion(pinned);
34262
+ const companion = await deps.convergeCompanion(
34263
+ pinned,
34264
+ downloadProgressLines({ write: args.stdout })
34265
+ );
34019
34266
  if (companion.state === "installed") {
34020
34267
  args.stdout("Desktop companions installed - they are at the edge of your screen.\n\n");
34021
34268
  } else if (companion.state === "not-released") {
@@ -34031,7 +34278,7 @@ They arrive with a platform release \u2014 run: m8t platform update
34031
34278
  );
34032
34279
  }
34033
34280
  } catch (e) {
34034
- const reason = e instanceof Error ? e.message : String(e);
34281
+ const reason = e instanceof LocalCliError && e.code === "COMPANION_DOWNLOAD_INCOMPLETE" && e.hint !== void 0 ? e.hint : e instanceof Error ? e.message : String(e);
34035
34282
  args.stdout(
34036
34283
  `Platform installation succeeded, but the desktop companions need repair.
34037
34284
  Run: m8t companion repair
@@ -34680,9 +34927,9 @@ async function openChatInvite(deps = {}) {
34680
34927
  if (!invite.ok) return CHAT_UNAVAILABLE_LINE;
34681
34928
  const print = deps.print === true;
34682
34929
  if (!print) {
34683
- const open3 = deps.open ?? tryOpenUrl;
34930
+ const open4 = deps.open ?? tryOpenUrl;
34684
34931
  try {
34685
- await open3(invite.url);
34932
+ await open4(invite.url);
34686
34933
  } catch {
34687
34934
  }
34688
34935
  }
@@ -36449,7 +36696,7 @@ async function runCompanionBridgeServe(stdin, stdout, stderr, deps = {}) {
36449
36696
  }
36450
36697
 
36451
36698
  // src/commands/companion/bridge.ts
36452
- var defaultDeps4 = {
36699
+ var defaultDeps3 = {
36453
36700
  roster: rosterCompanions,
36454
36701
  send: sendCompanionMessage,
36455
36702
  converse: converseCompanionMessage,
@@ -36487,7 +36734,7 @@ function exitFor(terminal) {
36487
36734
  }
36488
36735
  return 2;
36489
36736
  }
36490
- async function runCompanionBridge(stdin, stdout, stderr, deps = defaultDeps4) {
36737
+ async function runCompanionBridge(stdin, stdout, stderr, deps = defaultDeps3) {
36491
36738
  let request;
36492
36739
  try {
36493
36740
  request = parseRequestLine(await readSingleRequest(stdin));
@@ -36659,10 +36906,14 @@ var CompanionRepairCommand = class extends M8tCommand {
36659
36906
  description: "Which deployment to bind to, when the subscription holds more than one."
36660
36907
  });
36661
36908
  async executeCommand() {
36909
+ const stdout = this.context.stdout;
36662
36910
  return runCompanionRepairCommand(
36663
- this.context.stdout,
36911
+ stdout,
36664
36912
  () => convergeCompanionFromChannel(repairCompanion, {
36665
- ...this.resourceGroup !== void 0 ? { resourceGroup: this.resourceGroup } : {}
36913
+ ...this.resourceGroup !== void 0 ? { resourceGroup: this.resourceGroup } : {},
36914
+ // Repair is the command a founder reaches for after an interrupted
36915
+ // install, so it is the one most likely to pick up a partial download.
36916
+ onDownloadEvent: downloadProgressLines({ write: (line2) => stdout.write(line2) })
36666
36917
  })
36667
36918
  );
36668
36919
  }