@mutmutco/installer-launcher 0.1.14 → 0.1.17

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/launcher.js CHANGED
@@ -6,7 +6,7 @@ import { randomUUID as randomUUID4 } from "node:crypto";
6
6
 
7
7
  // src/acquisition.ts
8
8
  import { createHash as createHash3, randomUUID as randomUUID3 } from "node:crypto";
9
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync, realpathSync as realpathSync2 } from "node:fs";
9
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4, unlinkSync, realpathSync as realpathSync2 } from "node:fs";
10
10
  import { join as join4, resolve, relative as relative2, isAbsolute as isAbsolute2, dirname as dirname2 } from "node:path";
11
11
 
12
12
  // src/runtime.ts
@@ -194,7 +194,7 @@ function wipeProductDir(dir) {
194
194
 
195
195
  // src/payload.ts
196
196
  import { createHash as createHash2, createPublicKey, verify } from "node:crypto";
197
- import { mkdirSync as mkdirSync3, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
197
+ import { cpSync, existsSync as existsSync4, mkdirSync as mkdirSync3, renameSync as renameSync3, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
198
198
  import { tmpdir as tmpdir2 } from "node:os";
199
199
  import { dirname, join as join3 } from "node:path";
200
200
 
@@ -403,6 +403,17 @@ async function fetchVerifiedManifest(config, accessToken, fetchImpl = fetch) {
403
403
  }
404
404
  return manifest;
405
405
  }
406
+ async function fetchReleaseVersion(config, fetchImpl = fetch) {
407
+ try {
408
+ const response = await fetchImpl(`${config.host}/release/version`);
409
+ if (!response.ok) return null;
410
+ const json = await response.json();
411
+ const version = typeof json?.version === "string" ? json.version.trim() : "";
412
+ return version || null;
413
+ } catch {
414
+ return null;
415
+ }
416
+ }
406
417
  async function fetchFileBytes(config, accessToken, path, fetchImpl, onProgress, expectedSize) {
407
418
  const encoded = path.split("/").map(encodeURIComponent).join("/");
408
419
  const response = await fetchImpl(`${config.host}/release/${encoded}`, {
@@ -414,7 +425,36 @@ async function fetchFileBytes(config, accessToken, path, fetchImpl, onProgress,
414
425
  }
415
426
  return readDownload(response, onProgress, expectedSize);
416
427
  }
428
+ var VERSION_CACHE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
429
+ function versionCacheRoot(dir, version) {
430
+ return VERSION_CACHE_RE.test(version) ? join3(dir, "payload-cache", version) : "";
431
+ }
432
+ function cachedPayloadForVersion(dir, version) {
433
+ const root = versionCacheRoot(dir, version);
434
+ if (!root) return null;
435
+ const payload = join3(root, "payload");
436
+ return existsSync4(payload) ? payload : null;
437
+ }
438
+ function rememberPayloadVersion(dir, version, payload) {
439
+ try {
440
+ const root = versionCacheRoot(dir, version);
441
+ if (!root) return;
442
+ const dest = join3(root, "payload");
443
+ if (existsSync4(dest)) return;
444
+ mkdirSync3(root, { recursive: true });
445
+ cpSync(payload, dest, { recursive: true });
446
+ } catch {
447
+ }
448
+ }
417
449
  async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
450
+ const target = join3(dir, "payload");
451
+ const cached = options.cacheDir ? cachedPayloadForVersion(options.cacheDir, manifest.version) : null;
452
+ if (cached) {
453
+ mkdirSync3(dir, { recursive: true });
454
+ rmSync3(target, { force: true, recursive: true });
455
+ cpSync(cached, target, { recursive: true });
456
+ return manifest.version;
457
+ }
418
458
  const fetchImpl = options.fetchImpl ?? fetch;
419
459
  const staging = join3(tmpdir2(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
420
460
  mkdirSync3(staging, { recursive: true });
@@ -431,7 +471,6 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
431
471
  mkdirSync3(dirname(dest), { recursive: true });
432
472
  writeFileSync3(dest, bytes);
433
473
  }
434
- const target = join3(dir, "payload");
435
474
  mkdirSync3(dir, { recursive: true });
436
475
  rmSync3(target, { force: true, recursive: true });
437
476
  renameSync3(staging, target);
@@ -439,6 +478,7 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
439
478
  rmSync3(staging, { force: true, recursive: true });
440
479
  throw error;
441
480
  }
481
+ if (options.cacheDir) rememberPayloadVersion(options.cacheDir, manifest.version, target);
442
482
  return manifest.version;
443
483
  }
444
484
 
@@ -448,7 +488,7 @@ function reusableCandidate(dir, manifest) {
448
488
  const saved = JSON.parse(readFileSync4(join4(dir, "acquisition-cache.json"), "utf8"));
449
489
  if (saved.signature !== manifest.signature) return null;
450
490
  const root = candidateRoot(dir, saved.candidate);
451
- if (!existsSync4(join4(root, "acquired.json"))) return null;
491
+ if (!existsSync5(join4(root, "acquired.json"))) return null;
452
492
  for (const file of manifest.files) {
453
493
  if (!safePath(file.path) || !verifyFileBytes(file, readFileSync4(join4(root, "payload", file.path)))) return null;
454
494
  }
@@ -555,7 +595,7 @@ function acquisitionRepairCommand(dir, version) {
555
595
  return argv(realpathSync2(state.acquired.node), productRoot, acquisition.runEntry, acquisition.repairArgs, prefix, version);
556
596
  }
557
597
  async function recoverAcquisition(dir, run2, env) {
558
- if (!existsSync4(pendingPath(dir))) return;
598
+ if (!existsSync5(pendingPath(dir))) return;
559
599
  const pending = JSON.parse(readFileSync4(pendingPath(dir), "utf8"));
560
600
  if (pending.schema !== 1 || typeof pending.version !== "string" || typeof pending.node !== "string") throw new Error("invalid pending acquisition receipt");
561
601
  const root = candidateRoot(dir, pending.candidate);
@@ -569,7 +609,7 @@ async function recoverAcquisition(dir, run2, env) {
569
609
  const command2 = argv(pending.node, packageRoot(prefix, acquisition), acquisition.rollbackEntry, acquisition.rollbackArgs, prefix, pending.version);
570
610
  if (!await run2(command2, root, runtimeEnvironment(pending.node, env))) throw new Error("installation recovery is pending; product rollback did not finish");
571
611
  if (pending.previous) writeState(dir, pending.previous);
572
- else if (existsSync4(statePath(dir))) unlinkSync(statePath(dir));
612
+ else if (existsSync5(statePath(dir))) unlinkSync(statePath(dir));
573
613
  unlinkSync(pendingPath(dir));
574
614
  }
575
615
  async function installAcquisition(dir, candidate, version, acquisition, options) {
@@ -579,7 +619,7 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
579
619
  const prefix = join4(root, "prefix");
580
620
  const prepared = join4(root, "acquired.json");
581
621
  const archiveHash = createHash3("sha256").update(readFileSync4(join4(payload, acquisition.archive))).digest("hex");
582
- const reused = existsSync4(prepared);
622
+ const reused = existsSync5(prepared);
583
623
  if (reused && JSON.parse(readFileSync4(prepared, "utf8")).archiveHash !== archiveHash) throw new Error("cached acquisition archive changed");
584
624
  if (!reused) mkdirSync4(prefix);
585
625
  const runtime = await (options.runtime ?? acquireRuntime)(dir, options.fetchImpl, options.onProgress);
@@ -632,7 +672,7 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
632
672
 
633
673
  // src/index.ts
634
674
  import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
635
- import { existsSync as existsSync6, readFileSync as readFileSync7, statSync, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, rmdirSync, mkdirSync as mkdirSync6, renameSync as renameSync4, rmSync as rmSync5 } from "node:fs";
675
+ import { existsSync as existsSync7, readFileSync as readFileSync7, statSync, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, rmdirSync, mkdirSync as mkdirSync6, renameSync as renameSync4, rmSync as rmSync5 } from "node:fs";
636
676
  import { tmpdir as tmpdir4 } from "node:os";
637
677
  import { dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
638
678
  import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
@@ -759,15 +799,24 @@ function createFace({ product, color = false, columns, env = process.env, operat
759
799
  const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
760
800
  if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
761
801
  if (kind === "running" && typeof measure === "string") record.measure = measure;
762
- try {
763
- if (progressFile) appendFileSync(progressFile, `${JSON.stringify(record)}
764
- `);
765
- else writeSync(progressFd, `${JSON.stringify(record)}
766
- `);
767
- return true;
768
- } catch {
769
- return false;
802
+ const line = `${JSON.stringify(record)}
803
+ `;
804
+ if (progressFd !== null) {
805
+ try {
806
+ writeSync(progressFd, line);
807
+ return true;
808
+ } catch {
809
+ }
810
+ }
811
+ if (progressFile) {
812
+ try {
813
+ appendFileSync(progressFile, line);
814
+ return true;
815
+ } catch {
816
+ return false;
817
+ }
770
818
  }
819
+ return false;
771
820
  };
772
821
  const welcome = () => continuesFace || nested ? [] : [
773
822
  `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
@@ -781,7 +830,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
781
830
  };
782
831
  const step = (title, measure = null, kind = "ok") => {
783
832
  if (emitMilestone(title, measure, kind)) return "";
784
- const glyph = title === "Armed hourly updates" && kind === "ok" ? GLYPH.clock : kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
833
+ const glyph = title === "Armed hourly updates" && kind === "ok" ? GLYPH.clock : kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(identity.accent, GLYPH.check);
785
834
  const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
786
835
  const time = measured;
787
836
  const column = Math.min(44, Math.max(0, width - 8));
@@ -802,7 +851,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
802
851
  const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
803
852
  const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
804
853
  const frame = (left, right) => `${paint(identity.accent, left)}${rule}${paint(identity.accent, right)}`;
805
- const bodyRow = (line) => line.startsWith(GLYPH.check) ? `${paint(PALETTE.green, GLYPH.check)}${line.slice(1)}` : line;
854
+ const bodyRow = (line) => line.startsWith(GLYPH.check) ? `${paint(identity.accent, GLYPH.check)}${line.slice(1)}` : line;
806
855
  return [
807
856
  frame(GLYPH.boxTop, GLYPH.boxTopEnd),
808
857
  ...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${bodyRow(line)}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
@@ -831,7 +880,41 @@ var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
831
880
  // ../face/src/spinner.ts
832
881
  import { appendFileSync as appendFileSync2, writeSync as writeSync2 } from "node:fs";
833
882
  import { Worker } from "node:worker_threads";
834
- var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
883
+
884
+ // ../face/src/encoding.ts
885
+ import { execFileSync } from "node:child_process";
886
+ var UNICODE_SPINNER_FRAMES = Object.freeze(["\u25D2", "\u25D0", "\u25D3", "\u25D1"]);
887
+ var ASCII_SPINNER_FRAMES = Object.freeze(["-", "\\", "|", "/"]);
888
+ var cachedCodePage;
889
+ function consoleOutputCodePage() {
890
+ if (cachedCodePage !== void 0) return cachedCodePage;
891
+ cachedCodePage = null;
892
+ try {
893
+ const out = execFileSync("chcp.com", { windowsHide: true, encoding: "utf8", timeout: 5e3 });
894
+ const pages = [...String(out).matchAll(/(\d{3,5})/gu)].map((m) => Number(m[1]));
895
+ cachedCodePage = pages.length ? pages[pages.length - 1] : null;
896
+ } catch {
897
+ cachedCodePage = null;
898
+ }
899
+ return cachedCodePage;
900
+ }
901
+ function consoleRendersUnicode(env = process.env, platform = process.platform, codePage = platform === "win32" ? consoleOutputCodePage() : null) {
902
+ if (env.MM_FACE_ASCII === "1" || env.NO_UNICODE === "1") return false;
903
+ const locale = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
904
+ if (locale === "C" || locale === "POSIX" || /^C\./u.test(locale) || /^POSIX\./u.test(locale)) return false;
905
+ if (/cp437|ibm437|\b437\b/iu.test(locale)) return false;
906
+ if (/utf-?8/iu.test(locale)) return true;
907
+ if (platform === "win32") {
908
+ if (env.WT_SESSION || env.ConEmuANSI === "ON") return true;
909
+ return codePage === 65001;
910
+ }
911
+ return true;
912
+ }
913
+ function spinnerFramesFor(env = process.env, platform = process.platform) {
914
+ return consoleRendersUnicode(env, platform) ? UNICODE_SPINNER_FRAMES : ASCII_SPINNER_FRAMES;
915
+ }
916
+
917
+ // ../face/src/spinner.ts
835
918
  var WORKER_SOURCE = `
836
919
  const { parentPort, workerData } = require('node:worker_threads');
837
920
  const { writeSync, appendFileSync } = require('node:fs');
@@ -856,8 +939,10 @@ parentPort.on('message', (next) => {
856
939
  });
857
940
  setInterval(draw, workerData.intervalMs);
858
941
  `;
859
- function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath }) {
942
+ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath, env = process.env, platform = process.platform }) {
860
943
  animate = animate && !face.nested && !face.emitsProgress;
944
+ const glyphs = spinnerFramesFor(env, platform);
945
+ const ascii = glyphs[0] === ASCII_SPINNER_FRAMES[0];
861
946
  let timer = null;
862
947
  let worker = null;
863
948
  let control = null;
@@ -875,7 +960,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
875
960
  };
876
961
  const render = (text, measure) => {
877
962
  const line = face.step(text, measure ?? "__elapsed__", "note").split("\n")[0];
878
- frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
963
+ const prepared = line && ascii ? line.replaceAll(GLYPH.bar, "|") : line;
964
+ frames = prepared ? glyphs.map((glyph) => `\r\x1B[2K${prepared.replace(GLYPH.dot, glyph)}`) : [];
879
965
  };
880
966
  const draw = () => {
881
967
  if (animate && !suspended && frames.length) write(frames[frame++ % frames.length].replace("__elapsed__", `${Math.floor((Date.now() - started) / 1e3)}s`));
@@ -952,7 +1038,6 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
952
1038
  }
953
1039
  };
954
1040
  }
955
- var SPINNER_FRAMES = Object.freeze([...FRAMES]);
956
1041
 
957
1042
  // ../face/src/conformance.ts
958
1043
  var ALLOWED = new Set(Object.values(GLYPH));
@@ -1056,6 +1141,14 @@ var PHASES = {
1056
1141
  doctor: ["Checking health", "Checked health"],
1057
1142
  rollback: ["Restoring the previous version", "Restored the previous version"]
1058
1143
  };
1144
+ var UPDATE_CHECK_PHASES = /* @__PURE__ */ new Set([
1145
+ "preflight",
1146
+ "resolve",
1147
+ "verify-release",
1148
+ "download",
1149
+ "sign-in",
1150
+ "check"
1151
+ ]);
1059
1152
  function createInstallerRun(value, options = {}) {
1060
1153
  const declaration = validateInstallerProduct(value);
1061
1154
  const env = options.env ?? process.env;
@@ -1091,6 +1184,7 @@ function createInstallerRun(value, options = {}) {
1091
1184
  };
1092
1185
  const spinner = createSpinner(face, {
1093
1186
  animate: tty && env.TERM !== "dumb" && (options.animate ?? Boolean(process.stderr.isTTY)),
1187
+ env,
1094
1188
  ...options.write ? { stream: { write: (text) => {
1095
1189
  emit(String(text), "spinner");
1096
1190
  return true;
@@ -1102,6 +1196,7 @@ function createInstallerRun(value, options = {}) {
1102
1196
  let chunkColumn = 0;
1103
1197
  const trailingCR = /* @__PURE__ */ new Set();
1104
1198
  let deferredActivity;
1199
+ const drawnPhaseCompletions = /* @__PURE__ */ new Set();
1105
1200
  const closeChunk = () => {
1106
1201
  if (chunkOpen) emit("\n", chunkChannel);
1107
1202
  chunkOpen = false;
@@ -1109,6 +1204,7 @@ function createInstallerRun(value, options = {}) {
1109
1204
  };
1110
1205
  let started = false;
1111
1206
  let finished = false;
1207
+ let operation = options.operation;
1112
1208
  const start = () => {
1113
1209
  if (started || finished) return;
1114
1210
  started = true;
@@ -1116,7 +1212,7 @@ function createInstallerRun(value, options = {}) {
1116
1212
  const welcome = face.welcome();
1117
1213
  if (tty) lines(welcome);
1118
1214
  else if (welcome.length) {
1119
- const warm = options.operation === "install" ? face.identity.installWarm : face.identity.warm;
1215
+ const warm = operation === "install" ? face.identity.installWarm : face.identity.warm;
1120
1216
  lines([`${face.identity.name} - Mutatis Mutandis`, warm.replaceAll("\u2014", "-").replaceAll("\u2026", "...")]);
1121
1217
  }
1122
1218
  };
@@ -1132,13 +1228,20 @@ function createInstallerRun(value, options = {}) {
1132
1228
  get errors() {
1133
1229
  return [...errors];
1134
1230
  },
1231
+ get operation() {
1232
+ return operation;
1233
+ },
1234
+ set operation(value2) {
1235
+ operation = value2;
1236
+ },
1135
1237
  start,
1136
1238
  phase(id, facts = {}) {
1137
1239
  if (finished) throw new Error("installer run already finished");
1138
1240
  if (!Object.hasOwn(PHASES, id)) throw new Error("installer run: unknown phase");
1139
1241
  start();
1140
1242
  const state = facts.state ?? "ok";
1141
- const title = PHASES[id][state === "ok" ? 1 : 0];
1243
+ const fold = operation === "update" && UPDATE_CHECK_PHASES.has(id) && state !== "fail";
1244
+ const title = PHASES[fold ? "check" : id][state === "ok" ? 1 : 0];
1142
1245
  if (state === "running") {
1143
1246
  if (face.progress(title, facts.measure ?? null)) return;
1144
1247
  if (activeTitle === title) spinner.say(title, facts.measure ?? null);
@@ -1148,7 +1251,14 @@ function createInstallerRun(value, options = {}) {
1148
1251
  }
1149
1252
  return;
1150
1253
  }
1151
- if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
1254
+ if (fold && id !== "check") {
1255
+ if (facts.detail) run2.relay(facts.detail);
1256
+ return;
1257
+ }
1258
+ if (!face.continues(id, state)) {
1259
+ durable(title, facts.seconds ?? facts.measure ?? null, state);
1260
+ if (state === "ok") drawnPhaseCompletions.add(title);
1261
+ }
1152
1262
  if (facts.detail) run2.relay(facts.detail);
1153
1263
  },
1154
1264
  surface(facts) {
@@ -1179,7 +1289,9 @@ function createInstallerRun(value, options = {}) {
1179
1289
  }
1180
1290
  return;
1181
1291
  }
1292
+ if (state !== "fail" && drawnPhaseCompletions.has(step)) return;
1182
1293
  durable(step, ms === void 0 ? null : ms / 1e3, state);
1294
+ if (state === "ok") drawnPhaseCompletions.add(step);
1183
1295
  },
1184
1296
  signIn({ url, code }) {
1185
1297
  start();
@@ -1286,7 +1398,7 @@ function createInstallerRun(value, options = {}) {
1286
1398
 
1287
1399
  // src/autoupdate.ts
1288
1400
  import { spawnSync as spawnSync2 } from "node:child_process";
1289
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
1401
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
1290
1402
  import { tmpdir as tmpdir3 } from "node:os";
1291
1403
  import { join as join5 } from "node:path";
1292
1404
  function schedulePlatform(override) {
@@ -1398,11 +1510,11 @@ function querySchedule(config, options = {}) {
1398
1510
  }
1399
1511
  if (platform === "darwin") {
1400
1512
  const plist = join5(home, "Library", "LaunchAgents", `${scheduleLabel(config)}.plist`);
1401
- if (!existsSync5(plist)) return { supported: true, enabled: false };
1513
+ if (!existsSync6(plist)) return { supported: true, enabled: false };
1402
1514
  return { supported: true, enabled: true, cadence: "hourly" };
1403
1515
  }
1404
1516
  const timer = join5(home, ".config", "systemd", "user", `${scheduleLabel(config)}.timer`);
1405
- if (!existsSync5(timer)) return { supported: true, enabled: false };
1517
+ if (!existsSync6(timer)) return { supported: true, enabled: false };
1406
1518
  const state = { supported: true, enabled: true, cadence: "hourly" };
1407
1519
  const shown = exec("systemctl", ["--user", "show", `${scheduleLabel(config)}.service`, "-p", "ExecMainStartTimestamp", "--value"]);
1408
1520
  const stamp = (shown.stdout ?? "").trim();
@@ -1694,7 +1806,7 @@ function page(title, body) {
1694
1806
  }
1695
1807
 
1696
1808
  // src/index.ts
1697
- var LAUNCHER_VERSION = true ? "0.1.14" : readVersionFromPackage();
1809
+ var LAUNCHER_VERSION = true ? "0.1.17" : readVersionFromPackage();
1698
1810
  function defaultPrint(message) {
1699
1811
  process.stdout.write(`${message}
1700
1812
  `);
@@ -1783,7 +1895,7 @@ async function runFile(file, args, printErr) {
1783
1895
  return 1;
1784
1896
  }
1785
1897
  const abs = resolve2(process.cwd(), file);
1786
- if (!existsSync6(abs)) {
1898
+ if (!existsSync7(abs)) {
1787
1899
  printErr(`cannot run ${file}: no such file.`);
1788
1900
  return 1;
1789
1901
  }
@@ -1915,7 +2027,7 @@ function resolveEntry(entry2) {
1915
2027
  function needsShell(command2) {
1916
2028
  if (process.platform !== "win32") return false;
1917
2029
  if (/\.(cmd|bat)$/i.test(command2)) return true;
1918
- return !existsSync6(command2);
2030
+ return !existsSync7(command2);
1919
2031
  }
1920
2032
  function quoteForShell(arg) {
1921
2033
  return /[\s"&|<>^%]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg;
@@ -1995,8 +2107,27 @@ async function installOrUpdate(config, dir, options, print, update) {
1995
2107
  let version = readState(dir)?.version ?? "unknown";
1996
2108
  installer.start();
1997
2109
  try {
2110
+ const current = readState(dir);
2111
+ const payloadPresent = existsSync7(payloadDir(dir));
1998
2112
  let started = Date.now();
2113
+ installer.phase("resolve", { state: "running" });
2114
+ const pointer = await fetchReleaseVersion(config, fetchImpl);
2115
+ if (pointer && current?.version === pointer && payloadPresent) {
2116
+ version = pointer;
2117
+ installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
2118
+ installer.surface({ id: config.product, from: version, to: version, state: "current" });
2119
+ return await finishLastMile(
2120
+ config,
2121
+ dir,
2122
+ options,
2123
+ installer,
2124
+ version,
2125
+ true,
2126
+ current.acquired ? acquisitionRepairCommand(dir, version) : void 0
2127
+ );
2128
+ }
1999
2129
  installer.phase("sign-in", { state: "running" });
2130
+ started = Date.now();
2000
2131
  const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
2001
2132
  installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
2002
2133
  installer.phase("resolve", { state: "running" });
@@ -2004,8 +2135,7 @@ async function installOrUpdate(config, dir, options, print, update) {
2004
2135
  const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
2005
2136
  version = manifest.version;
2006
2137
  installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
2007
- const current = readState(dir);
2008
- const unchanged = current?.version === version && existsSync6(payloadDir(dir));
2138
+ const unchanged = current?.version === version && existsSync7(payloadDir(dir));
2009
2139
  if (unchanged && current.acquired) {
2010
2140
  installer.surface({ id: config.product, from: version, to: version, state: "current" });
2011
2141
  return await finishLastMile(config, dir, options, installer, version, true, acquisitionRepairCommand(dir, version));
@@ -2020,8 +2150,8 @@ async function installOrUpdate(config, dir, options, print, update) {
2020
2150
  state: "running",
2021
2151
  measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${done}/${total} B` : `${done} B`
2022
2152
  });
2023
- if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress });
2024
- const acquisition = existsSync6(join6(candidateDir, "payload", "payload.json")) ? readAcquisition(join6(candidateDir, "payload")) : null;
2153
+ if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress, cacheDir: dir });
2154
+ const acquisition = existsSync7(join6(candidateDir, "payload", "payload.json")) ? readAcquisition(join6(candidateDir, "payload")) : null;
2025
2155
  if (acquisition) {
2026
2156
  rememberCandidate(dir, candidate, manifest);
2027
2157
  installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
@@ -2090,7 +2220,7 @@ function payloadFileAsCommand(entry2, payload) {
2090
2220
  if (!first || first === "$self") return null;
2091
2221
  if (first.includes("/") || first.includes("\\")) return null;
2092
2222
  const candidate = join6(payload, first);
2093
- if (!existsSync6(candidate)) return null;
2223
+ if (!existsSync7(candidate)) return null;
2094
2224
  if (NEVER_A_PROGRAM.test(first)) return first;
2095
2225
  if (process.platform === "win32") return null;
2096
2226
  try {
@@ -2207,7 +2337,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2207
2337
  const pollProgress = () => {
2208
2338
  if (progressError) return;
2209
2339
  try {
2210
- if (!existsSync6(progressFile)) return;
2340
+ if (!existsSync7(progressFile)) return;
2211
2341
  const bytes = readFileSync7(progressFile);
2212
2342
  receive(progressDecoder.write(bytes.subarray(progressOffset)));
2213
2343
  progressOffset = bytes.length;
@@ -2241,13 +2371,13 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2241
2371
  return { ...result, ok: false, code: result.code || 1, error: "installer outcome: invalid child result" };
2242
2372
  }
2243
2373
  } finally {
2244
- if (existsSync6(progressFile)) unlinkSync2(progressFile);
2245
- if (existsSync6(outcomeFile)) unlinkSync2(outcomeFile);
2374
+ if (existsSync7(progressFile)) unlinkSync2(progressFile);
2375
+ if (existsSync7(outcomeFile)) unlinkSync2(outcomeFile);
2246
2376
  rmdirSync(outcomeDir);
2247
2377
  }
2248
2378
  }
2249
2379
  function doForward(config, dir, options, command2, argv2, print, unlock) {
2250
- if (command2 && existsSync6(command2)) {
2380
+ if (command2 && existsSync7(command2)) {
2251
2381
  print(`${command2} is a file, not a command \u2014 did you mean \`--run ${command2}\`?`);
2252
2382
  return 2;
2253
2383
  }
@@ -2327,7 +2457,7 @@ function doDoctor(config, dir, options, print, unlock) {
2327
2457
  function doLauncherDoctor(config, dir, options, print) {
2328
2458
  const tokens = readTokens(dir);
2329
2459
  const state = readState(dir);
2330
- const payloadPresent = existsSync6(payloadDir(dir));
2460
+ const payloadPresent = existsSync7(payloadDir(dir));
2331
2461
  print(`product: ${config.product}`);
2332
2462
  print(`host: ${config.host}`);
2333
2463
  print(`login: ${config.loginKind}`);
@@ -435,6 +435,17 @@ async function fetchVerifiedManifest(config, accessToken, fetchImpl = fetch) {
435
435
  }
436
436
  return manifest;
437
437
  }
438
+ async function fetchReleaseVersion(config, fetchImpl = fetch) {
439
+ try {
440
+ const response = await fetchImpl(`${config.host}/release/version`);
441
+ if (!response.ok) return null;
442
+ const json = await response.json();
443
+ const version = typeof json?.version === "string" ? json.version.trim() : "";
444
+ return version || null;
445
+ } catch {
446
+ return null;
447
+ }
448
+ }
438
449
  async function fetchFileBytes(config, accessToken, path, fetchImpl, onProgress, expectedSize) {
439
450
  const encoded = path.split("/").map(encodeURIComponent).join("/");
440
451
  const response = await fetchImpl(`${config.host}/release/${encoded}`, {
@@ -446,7 +457,36 @@ async function fetchFileBytes(config, accessToken, path, fetchImpl, onProgress,
446
457
  }
447
458
  return readDownload(response, onProgress, expectedSize);
448
459
  }
460
+ var VERSION_CACHE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
461
+ function versionCacheRoot(dir, version) {
462
+ return VERSION_CACHE_RE.test(version) ? (0, import_node_path3.join)(dir, "payload-cache", version) : "";
463
+ }
464
+ function cachedPayloadForVersion(dir, version) {
465
+ const root = versionCacheRoot(dir, version);
466
+ if (!root) return null;
467
+ const payload = (0, import_node_path3.join)(root, "payload");
468
+ return (0, import_node_fs4.existsSync)(payload) ? payload : null;
469
+ }
470
+ function rememberPayloadVersion(dir, version, payload) {
471
+ try {
472
+ const root = versionCacheRoot(dir, version);
473
+ if (!root) return;
474
+ const dest = (0, import_node_path3.join)(root, "payload");
475
+ if ((0, import_node_fs4.existsSync)(dest)) return;
476
+ (0, import_node_fs4.mkdirSync)(root, { recursive: true });
477
+ (0, import_node_fs4.cpSync)(payload, dest, { recursive: true });
478
+ } catch {
479
+ }
480
+ }
449
481
  async function downloadAndUnpack(config, dir, manifest, accessToken, options = {}) {
482
+ const target = (0, import_node_path3.join)(dir, "payload");
483
+ const cached = options.cacheDir ? cachedPayloadForVersion(options.cacheDir, manifest.version) : null;
484
+ if (cached) {
485
+ (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
486
+ (0, import_node_fs4.rmSync)(target, { force: true, recursive: true });
487
+ (0, import_node_fs4.cpSync)(cached, target, { recursive: true });
488
+ return manifest.version;
489
+ }
450
490
  const fetchImpl = options.fetchImpl ?? fetch;
451
491
  const staging = (0, import_node_path3.join)((0, import_node_os2.tmpdir)(), `launcher-${config.product}-${Date.now()}-${Math.floor(Math.random() * 1e6)}`);
452
492
  (0, import_node_fs4.mkdirSync)(staging, { recursive: true });
@@ -463,7 +503,6 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
463
503
  (0, import_node_fs4.mkdirSync)((0, import_node_path3.dirname)(dest), { recursive: true });
464
504
  (0, import_node_fs4.writeFileSync)(dest, bytes);
465
505
  }
466
- const target = (0, import_node_path3.join)(dir, "payload");
467
506
  (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
468
507
  (0, import_node_fs4.rmSync)(target, { force: true, recursive: true });
469
508
  (0, import_node_fs4.renameSync)(staging, target);
@@ -471,6 +510,7 @@ async function downloadAndUnpack(config, dir, manifest, accessToken, options = {
471
510
  (0, import_node_fs4.rmSync)(staging, { force: true, recursive: true });
472
511
  throw error;
473
512
  }
513
+ if (options.cacheDir) rememberPayloadVersion(options.cacheDir, manifest.version, target);
474
514
  return manifest.version;
475
515
  }
476
516
 
@@ -663,7 +703,7 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
663
703
  }
664
704
 
665
705
  // src/index.ts
666
- var import_node_child_process4 = require("node:child_process");
706
+ var import_node_child_process5 = require("node:child_process");
667
707
  var import_node_fs11 = require("node:fs");
668
708
  var import_node_os4 = require("node:os");
669
709
  var import_node_path6 = require("node:path");
@@ -791,15 +831,24 @@ function createFace({ product, color = false, columns, env = process.env, operat
791
831
  const record = { v: PROGRESS_PROTOCOL, step: stripColor(title), state: kind };
792
832
  if (typeof measure === "number") record.ms = Math.max(0, Math.round(measure * 1e3));
793
833
  if (kind === "running" && typeof measure === "string") record.measure = measure;
794
- try {
795
- if (progressFile) (0, import_node_fs6.appendFileSync)(progressFile, `${JSON.stringify(record)}
796
- `);
797
- else (0, import_node_fs6.writeSync)(progressFd, `${JSON.stringify(record)}
798
- `);
799
- return true;
800
- } catch {
801
- return false;
834
+ const line = `${JSON.stringify(record)}
835
+ `;
836
+ if (progressFd !== null) {
837
+ try {
838
+ (0, import_node_fs6.writeSync)(progressFd, line);
839
+ return true;
840
+ } catch {
841
+ }
802
842
  }
843
+ if (progressFile) {
844
+ try {
845
+ (0, import_node_fs6.appendFileSync)(progressFile, line);
846
+ return true;
847
+ } catch {
848
+ return false;
849
+ }
850
+ }
851
+ return false;
803
852
  };
804
853
  const welcome = () => continuesFace || nested ? [] : [
805
854
  `${paint(identity.accent, GLYPH.diamond)} ${paint(identity.accent, identity.name)} \u2014 Mutatis Mutandis`,
@@ -813,7 +862,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
813
862
  };
814
863
  const step = (title, measure = null, kind = "ok") => {
815
864
  if (emitMilestone(title, measure, kind)) return "";
816
- const glyph = title === "Armed hourly updates" && kind === "ok" ? GLYPH.clock : kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(PALETTE.green, GLYPH.check);
865
+ const glyph = title === "Armed hourly updates" && kind === "ok" ? GLYPH.clock : kind === "fail" ? paint(identity.accent, GLYPH.cross) : kind === "note" ? paint(PALETTE.muted, GLYPH.dot) : paint(identity.accent, GLYPH.check);
817
866
  const measured = measure === null || measure === void 0 ? "" : paint(PALETTE.muted, typeof measure === "number" ? `${Math.max(0, Math.round(measure))}s` : String(measure).trim());
818
867
  const time = measured;
819
868
  const column = Math.min(44, Math.max(0, width - 8));
@@ -834,7 +883,7 @@ function createFace({ product, color = false, columns, env = process.env, operat
834
883
  const content = Math.min(width - 6, Math.max(...body.map(visibleWidth)));
835
884
  const rule = paint(identity.accent, GLYPH.rule.repeat(content + 4));
836
885
  const frame = (left, right) => `${paint(identity.accent, left)}${rule}${paint(identity.accent, right)}`;
837
- const bodyRow = (line) => line.startsWith(GLYPH.check) ? `${paint(PALETTE.green, GLYPH.check)}${line.slice(1)}` : line;
886
+ const bodyRow = (line) => line.startsWith(GLYPH.check) ? `${paint(identity.accent, GLYPH.check)}${line.slice(1)}` : line;
838
887
  return [
839
888
  frame(GLYPH.boxTop, GLYPH.boxTopEnd),
840
889
  ...body.map((line) => `${paint(identity.accent, GLYPH.bar)} ${bodyRow(line)}${" ".repeat(Math.max(0, content - visibleWidth(line)))} ${paint(identity.accent, GLYPH.bar)}`),
@@ -863,7 +912,41 @@ var RELAY_INDENT = " ".repeat(TITLE_COLUMN - 1);
863
912
  // ../face/src/spinner.ts
864
913
  var import_node_fs7 = require("node:fs");
865
914
  var import_node_worker_threads = require("node:worker_threads");
866
- var FRAMES = ["\u25D2", "\u25D0", "\u25D3", "\u25D1"];
915
+
916
+ // ../face/src/encoding.ts
917
+ var import_node_child_process2 = require("node:child_process");
918
+ var UNICODE_SPINNER_FRAMES = Object.freeze(["\u25D2", "\u25D0", "\u25D3", "\u25D1"]);
919
+ var ASCII_SPINNER_FRAMES = Object.freeze(["-", "\\", "|", "/"]);
920
+ var cachedCodePage;
921
+ function consoleOutputCodePage() {
922
+ if (cachedCodePage !== void 0) return cachedCodePage;
923
+ cachedCodePage = null;
924
+ try {
925
+ const out = (0, import_node_child_process2.execFileSync)("chcp.com", { windowsHide: true, encoding: "utf8", timeout: 5e3 });
926
+ const pages = [...String(out).matchAll(/(\d{3,5})/gu)].map((m) => Number(m[1]));
927
+ cachedCodePage = pages.length ? pages[pages.length - 1] : null;
928
+ } catch {
929
+ cachedCodePage = null;
930
+ }
931
+ return cachedCodePage;
932
+ }
933
+ function consoleRendersUnicode(env = process.env, platform = process.platform, codePage = platform === "win32" ? consoleOutputCodePage() : null) {
934
+ if (env.MM_FACE_ASCII === "1" || env.NO_UNICODE === "1") return false;
935
+ const locale = env.LC_ALL || env.LC_CTYPE || env.LANG || "";
936
+ if (locale === "C" || locale === "POSIX" || /^C\./u.test(locale) || /^POSIX\./u.test(locale)) return false;
937
+ if (/cp437|ibm437|\b437\b/iu.test(locale)) return false;
938
+ if (/utf-?8/iu.test(locale)) return true;
939
+ if (platform === "win32") {
940
+ if (env.WT_SESSION || env.ConEmuANSI === "ON") return true;
941
+ return codePage === 65001;
942
+ }
943
+ return true;
944
+ }
945
+ function spinnerFramesFor(env = process.env, platform = process.platform) {
946
+ return consoleRendersUnicode(env, platform) ? UNICODE_SPINNER_FRAMES : ASCII_SPINNER_FRAMES;
947
+ }
948
+
949
+ // ../face/src/spinner.ts
867
950
  var WORKER_SOURCE = `
868
951
  const { parentPort, workerData } = require('node:worker_threads');
869
952
  const { writeSync, appendFileSync } = require('node:fs');
@@ -888,8 +971,10 @@ parentPort.on('message', (next) => {
888
971
  });
889
972
  setInterval(draw, workerData.intervalMs);
890
973
  `;
891
- function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath }) {
974
+ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath, env = process.env, platform = process.platform }) {
892
975
  animate = animate && !face.nested && !face.emitsProgress;
976
+ const glyphs = spinnerFramesFor(env, platform);
977
+ const ascii = glyphs[0] === ASCII_SPINNER_FRAMES[0];
893
978
  let timer = null;
894
979
  let worker = null;
895
980
  let control = null;
@@ -907,7 +992,8 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
907
992
  };
908
993
  const render = (text, measure) => {
909
994
  const line = face.step(text, measure ?? "__elapsed__", "note").split("\n")[0];
910
- frames = line ? FRAMES.map((glyph) => `\r\x1B[2K${line.replace(GLYPH.dot, glyph)}`) : [];
995
+ const prepared = line && ascii ? line.replaceAll(GLYPH.bar, "|") : line;
996
+ frames = prepared ? glyphs.map((glyph) => `\r\x1B[2K${prepared.replace(GLYPH.dot, glyph)}`) : [];
911
997
  };
912
998
  const draw = () => {
913
999
  if (animate && !suspended && frames.length) write(frames[frame++ % frames.length].replace("__elapsed__", `${Math.floor((Date.now() - started) / 1e3)}s`));
@@ -984,7 +1070,6 @@ function createSpinner(face, { animate, stream, intervalMs = 90, transcriptPath
984
1070
  }
985
1071
  };
986
1072
  }
987
- var SPINNER_FRAMES = Object.freeze([...FRAMES]);
988
1073
 
989
1074
  // ../face/src/conformance.ts
990
1075
  var ALLOWED = new Set(Object.values(GLYPH));
@@ -1088,6 +1173,14 @@ var PHASES = {
1088
1173
  doctor: ["Checking health", "Checked health"],
1089
1174
  rollback: ["Restoring the previous version", "Restored the previous version"]
1090
1175
  };
1176
+ var UPDATE_CHECK_PHASES = /* @__PURE__ */ new Set([
1177
+ "preflight",
1178
+ "resolve",
1179
+ "verify-release",
1180
+ "download",
1181
+ "sign-in",
1182
+ "check"
1183
+ ]);
1091
1184
  function createInstallerRun(value, options = {}) {
1092
1185
  const declaration = validateInstallerProduct(value);
1093
1186
  const env = options.env ?? process.env;
@@ -1123,6 +1216,7 @@ function createInstallerRun(value, options = {}) {
1123
1216
  };
1124
1217
  const spinner = createSpinner(face, {
1125
1218
  animate: tty && env.TERM !== "dumb" && (options.animate ?? Boolean(process.stderr.isTTY)),
1219
+ env,
1126
1220
  ...options.write ? { stream: { write: (text) => {
1127
1221
  emit(String(text), "spinner");
1128
1222
  return true;
@@ -1134,6 +1228,7 @@ function createInstallerRun(value, options = {}) {
1134
1228
  let chunkColumn = 0;
1135
1229
  const trailingCR = /* @__PURE__ */ new Set();
1136
1230
  let deferredActivity;
1231
+ const drawnPhaseCompletions = /* @__PURE__ */ new Set();
1137
1232
  const closeChunk = () => {
1138
1233
  if (chunkOpen) emit("\n", chunkChannel);
1139
1234
  chunkOpen = false;
@@ -1141,6 +1236,7 @@ function createInstallerRun(value, options = {}) {
1141
1236
  };
1142
1237
  let started = false;
1143
1238
  let finished = false;
1239
+ let operation = options.operation;
1144
1240
  const start = () => {
1145
1241
  if (started || finished) return;
1146
1242
  started = true;
@@ -1148,7 +1244,7 @@ function createInstallerRun(value, options = {}) {
1148
1244
  const welcome = face.welcome();
1149
1245
  if (tty) lines(welcome);
1150
1246
  else if (welcome.length) {
1151
- const warm = options.operation === "install" ? face.identity.installWarm : face.identity.warm;
1247
+ const warm = operation === "install" ? face.identity.installWarm : face.identity.warm;
1152
1248
  lines([`${face.identity.name} - Mutatis Mutandis`, warm.replaceAll("\u2014", "-").replaceAll("\u2026", "...")]);
1153
1249
  }
1154
1250
  };
@@ -1164,13 +1260,20 @@ function createInstallerRun(value, options = {}) {
1164
1260
  get errors() {
1165
1261
  return [...errors];
1166
1262
  },
1263
+ get operation() {
1264
+ return operation;
1265
+ },
1266
+ set operation(value2) {
1267
+ operation = value2;
1268
+ },
1167
1269
  start,
1168
1270
  phase(id, facts = {}) {
1169
1271
  if (finished) throw new Error("installer run already finished");
1170
1272
  if (!Object.hasOwn(PHASES, id)) throw new Error("installer run: unknown phase");
1171
1273
  start();
1172
1274
  const state = facts.state ?? "ok";
1173
- const title = PHASES[id][state === "ok" ? 1 : 0];
1275
+ const fold = operation === "update" && UPDATE_CHECK_PHASES.has(id) && state !== "fail";
1276
+ const title = PHASES[fold ? "check" : id][state === "ok" ? 1 : 0];
1174
1277
  if (state === "running") {
1175
1278
  if (face.progress(title, facts.measure ?? null)) return;
1176
1279
  if (activeTitle === title) spinner.say(title, facts.measure ?? null);
@@ -1180,7 +1283,14 @@ function createInstallerRun(value, options = {}) {
1180
1283
  }
1181
1284
  return;
1182
1285
  }
1183
- if (!face.continues(id, state)) durable(title, facts.seconds ?? facts.measure ?? null, state);
1286
+ if (fold && id !== "check") {
1287
+ if (facts.detail) run2.relay(facts.detail);
1288
+ return;
1289
+ }
1290
+ if (!face.continues(id, state)) {
1291
+ durable(title, facts.seconds ?? facts.measure ?? null, state);
1292
+ if (state === "ok") drawnPhaseCompletions.add(title);
1293
+ }
1184
1294
  if (facts.detail) run2.relay(facts.detail);
1185
1295
  },
1186
1296
  surface(facts) {
@@ -1211,7 +1321,9 @@ function createInstallerRun(value, options = {}) {
1211
1321
  }
1212
1322
  return;
1213
1323
  }
1324
+ if (state !== "fail" && drawnPhaseCompletions.has(step)) return;
1214
1325
  durable(step, ms === void 0 ? null : ms / 1e3, state);
1326
+ if (state === "ok") drawnPhaseCompletions.add(step);
1215
1327
  },
1216
1328
  signIn({ url, code }) {
1217
1329
  start();
@@ -1317,7 +1429,7 @@ function createInstallerRun(value, options = {}) {
1317
1429
  }
1318
1430
 
1319
1431
  // src/autoupdate.ts
1320
- var import_node_child_process2 = require("node:child_process");
1432
+ var import_node_child_process3 = require("node:child_process");
1321
1433
  var import_node_fs10 = require("node:fs");
1322
1434
  var import_node_os3 = require("node:os");
1323
1435
  var import_node_path5 = require("node:path");
@@ -1327,7 +1439,7 @@ function schedulePlatform(override) {
1327
1439
  return null;
1328
1440
  }
1329
1441
  function defaultExec(command2, args) {
1330
- const result = (0, import_node_child_process2.spawnSync)(command2, args, { encoding: "utf8", windowsHide: true });
1442
+ const result = (0, import_node_child_process3.spawnSync)(command2, args, { encoding: "utf8", windowsHide: true });
1331
1443
  if (result.error) return { code: 1, stdout: "", stderr: result.error.message };
1332
1444
  return {
1333
1445
  code: typeof result.status === "number" ? result.status : 1,
@@ -1492,14 +1604,14 @@ function firstLine(text) {
1492
1604
  }
1493
1605
 
1494
1606
  // src/login-github.ts
1495
- var import_node_child_process3 = require("node:child_process");
1607
+ var import_node_child_process4 = require("node:child_process");
1496
1608
  var realSleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
1497
1609
  function openBrowser(url) {
1498
1610
  if (process.env.LAUNCHER_NO_OPEN === "1") return;
1499
1611
  const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "rundll32" : "xdg-open";
1500
1612
  const args = process.platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
1501
1613
  try {
1502
- const child = (0, import_node_child_process3.spawn)(opener, args, { detached: true, stdio: "ignore", windowsHide: true });
1614
+ const child = (0, import_node_child_process4.spawn)(opener, args, { detached: true, stdio: "ignore", windowsHide: true });
1503
1615
  child.on("error", () => {
1504
1616
  });
1505
1617
  child.unref();
@@ -1726,7 +1838,7 @@ function page(title, body) {
1726
1838
  }
1727
1839
 
1728
1840
  // src/index.ts
1729
- var LAUNCHER_VERSION = true ? "0.1.14" : readVersionFromPackage();
1841
+ var LAUNCHER_VERSION = true ? "0.1.17" : readVersionFromPackage();
1730
1842
  function defaultPrint(message) {
1731
1843
  process.stdout.write(`${message}
1732
1844
  `);
@@ -1979,7 +2091,7 @@ function defaultRunEntry(entry2, cwd, env) {
1979
2091
  const [command2, ...args] = entry2;
1980
2092
  const shell = needsShell(command2);
1981
2093
  const commandLine = shell ? [command2, ...args].map(quoteForShell).join(" ") : command2;
1982
- const spawnEntry = (progress2) => (0, import_node_child_process4.spawnSync)(commandLine, shell ? [] : args, {
2094
+ const spawnEntry = (progress2) => (0, import_node_child_process5.spawnSync)(commandLine, shell ? [] : args, {
1983
2095
  cwd,
1984
2096
  stdio: progress2 ? ["inherit", "inherit", "inherit", "pipe"] : "inherit",
1985
2097
  shell,
@@ -2027,8 +2139,27 @@ async function installOrUpdate(config, dir, options, print, update) {
2027
2139
  let version = readState(dir)?.version ?? "unknown";
2028
2140
  installer.start();
2029
2141
  try {
2142
+ const current = readState(dir);
2143
+ const payloadPresent = (0, import_node_fs11.existsSync)(payloadDir(dir));
2030
2144
  let started = Date.now();
2145
+ installer.phase("resolve", { state: "running" });
2146
+ const pointer = await fetchReleaseVersion(config, fetchImpl);
2147
+ if (pointer && current?.version === pointer && payloadPresent) {
2148
+ version = pointer;
2149
+ installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
2150
+ installer.surface({ id: config.product, from: version, to: version, state: "current" });
2151
+ return await finishLastMile(
2152
+ config,
2153
+ dir,
2154
+ options,
2155
+ installer,
2156
+ version,
2157
+ true,
2158
+ current.acquired ? acquisitionRepairCommand(dir, version) : void 0
2159
+ );
2160
+ }
2031
2161
  installer.phase("sign-in", { state: "running" });
2162
+ started = Date.now();
2032
2163
  const accessToken = await fetchAccessTokenOrLogin(config, dir, options, print, installer);
2033
2164
  installer.phase("sign-in", { seconds: (Date.now() - started) / 1e3 });
2034
2165
  installer.phase("resolve", { state: "running" });
@@ -2036,7 +2167,6 @@ async function installOrUpdate(config, dir, options, print, update) {
2036
2167
  const manifest = await fetchVerifiedManifest(config, accessToken, fetchImpl);
2037
2168
  version = manifest.version;
2038
2169
  installer.phase("resolve", { seconds: (Date.now() - started) / 1e3 });
2039
- const current = readState(dir);
2040
2170
  const unchanged = current?.version === version && (0, import_node_fs11.existsSync)(payloadDir(dir));
2041
2171
  if (unchanged && current.acquired) {
2042
2172
  installer.surface({ id: config.product, from: version, to: version, state: "current" });
@@ -2052,7 +2182,7 @@ async function installOrUpdate(config, dir, options, print, update) {
2052
2182
  state: "running",
2053
2183
  measure: total ? `${Math.floor(done / total * 100)}% \xB7 ${done}/${total} B` : `${done} B`
2054
2184
  });
2055
- if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress });
2185
+ if (!cached) await downloadAndUnpack(config, candidateDir, manifest, accessToken, { fetchImpl, onProgress, cacheDir: dir });
2056
2186
  const acquisition = (0, import_node_fs11.existsSync)((0, import_node_path6.join)(candidateDir, "payload", "payload.json")) ? readAcquisition((0, import_node_path6.join)(candidateDir, "payload")) : null;
2057
2187
  if (acquisition) {
2058
2188
  rememberCandidate(dir, candidate, manifest);
@@ -2202,7 +2332,7 @@ async function runInstallEntry(entry2, cwd, env, installer, tokens = null) {
2202
2332
  const redact = (text) => secrets.reduce((safe, value) => safe.replaceAll(value, "[redacted]"), text);
2203
2333
  try {
2204
2334
  const result = await new Promise((resolve3) => {
2205
- const child = (0, import_node_child_process4.spawn)(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
2335
+ const child = (0, import_node_child_process5.spawn)(shell ? [command2, ...args].map(quoteForShell).join(" ") : command2, shell ? [] : args, {
2206
2336
  cwd,
2207
2337
  shell,
2208
2338
  windowsHide: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/installer-launcher",
3
- "version": "0.1.14",
3
+ "version": "0.1.17",
4
4
  "description": "Single-executable (SEA) launcher: sign-in, gated payload fetch, self-update. One copy ships per product.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",