@mutmutco/installer-launcher 1.3.0 → 1.3.1

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/README.md CHANGED
@@ -245,7 +245,7 @@ atomically only after convergence succeeds. On failure or interrupted startup it
245
245
  consumer rollback before allowing another install or forwarding. Failed rollback stays pending.
246
246
  A crash after shared commit completes the receipt instead of reverting the committed product.
247
247
  Host registration is not atomic: failures can leave host references to retained candidates.
248
- Candidates are deliberately retained until product-owned reference evidence can permit cleanup.
248
+ After a committed promotion the launcher removes candidate folders `state.json` no longer references (the active one is kept; symlinks and junctions are skipped; a failed delete is logged). A failed activation keeps its candidate.
249
249
  An orphan lock-recovery guard fails closed and needs operator diagnosis.
250
250
 
251
251
  For already-shipped old launchers, copy the exact file exported by
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 existsSync5, 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, readdirSync, lstatSync, rmSync as rmSync4 } 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
@@ -617,6 +617,28 @@ async function recoverAcquisition(dir, run2, env) {
617
617
  else if (existsSync5(statePath(dir))) unlinkSync(statePath(dir));
618
618
  unlinkSync(pendingPath(dir));
619
619
  }
620
+ function reapCandidates(dir) {
621
+ const keep = readState(dir)?.acquired?.candidate;
622
+ if (!keep) return;
623
+ const base = join4(dir, "candidates");
624
+ let names;
625
+ try {
626
+ names = readdirSync(base);
627
+ } catch {
628
+ return;
629
+ }
630
+ for (const name of names) {
631
+ if (name === keep || !/^[a-zA-Z0-9_-]+$/.test(name)) continue;
632
+ try {
633
+ const path = join4(base, name);
634
+ const info = lstatSync(path);
635
+ if (!info.isDirectory() || info.isSymbolicLink()) continue;
636
+ rmSync4(path, { recursive: true, force: true });
637
+ } catch (error) {
638
+ console.error(`launcher: could not remove old candidate ${name}: ${error.message}`);
639
+ }
640
+ }
641
+ }
620
642
  async function installAcquisition(dir, candidate, version, acquisition, options) {
621
643
  if (!acquisition.platforms.includes(`${process.platform}-${process.arch}`)) throw new Error("this product does not support this platform");
622
644
  const root = candidateRoot(dir, candidate);
@@ -673,11 +695,12 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
673
695
  throw error;
674
696
  }
675
697
  unlinkSync(pendingPath(dir));
698
+ reapCandidates(dir);
676
699
  }
677
700
 
678
701
  // src/index.ts
679
702
  import { spawn as spawn2, spawnSync as spawnSync4 } from "node:child_process";
680
- import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync2, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, rmdirSync, mkdirSync as mkdirSync6, renameSync as renameSync5, rmSync as rmSync6, writeFileSync as writeFileSync8 } from "node:fs";
703
+ import { existsSync as existsSync8, readFileSync as readFileSync8, statSync as statSync2, mkdtempSync as mkdtempSync2, unlinkSync as unlinkSync2, rmdirSync, mkdirSync as mkdirSync6, renameSync as renameSync5, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "node:fs";
681
704
  import { tmpdir as tmpdir4 } from "node:os";
682
705
  import { dirname as dirname4, join as join7, resolve as resolve3 } from "node:path";
683
706
  import { isSea } from "node:sea";
@@ -1554,7 +1577,7 @@ var rowTitle = (row) => `${row.surface}${row.aspect ? ` ${row.aspect}` : ""}${ro
1554
1577
 
1555
1578
  // src/autoupdate.ts
1556
1579
  import { spawnSync as spawnSync2 } from "node:child_process";
1557
- import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "node:fs";
1580
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "node:fs";
1558
1581
  import { tmpdir as tmpdir3 } from "node:os";
1559
1582
  import { join as join5 } from "node:path";
1560
1583
  function schedulePlatform(override) {
@@ -1649,20 +1672,20 @@ function disableSchedule(config, options = {}) {
1649
1672
  if (result.code !== 0 && !/cannot find|does not exist/i.test(`${result.stdout} ${result.stderr}`)) {
1650
1673
  throw new Error(`Could not turn auto-update off: ${firstLine(result.stdout || result.stderr) || `exit ${result.code}`}`);
1651
1674
  }
1652
- rmSync4(join5(options.stateDir ?? defaultProductDir(config.product), HIDDEN_SCRIPT), { force: true });
1675
+ rmSync5(join5(options.stateDir ?? defaultProductDir(config.product), HIDDEN_SCRIPT), { force: true });
1653
1676
  return;
1654
1677
  }
1655
1678
  if (platform === "darwin") {
1656
1679
  const label2 = scheduleLabel(config);
1657
1680
  exec("launchctl", ["bootout", `gui/${process.getuid?.() ?? 501}/${label2}`]);
1658
- rmSync4(join5(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
1681
+ rmSync5(join5(home, "Library", "LaunchAgents", `${label2}.plist`), { force: true });
1659
1682
  return;
1660
1683
  }
1661
1684
  const label = scheduleLabel(config);
1662
1685
  const dir = join5(home, ".config", "systemd", "user");
1663
1686
  exec("systemctl", ["--user", "disable", "--now", `${label}.timer`]);
1664
- rmSync4(join5(dir, `${label}.service`), { force: true });
1665
- rmSync4(join5(dir, `${label}.timer`), { force: true });
1687
+ rmSync5(join5(dir, `${label}.service`), { force: true });
1688
+ rmSync5(join5(dir, `${label}.timer`), { force: true });
1666
1689
  }
1667
1690
  function querySchedule(config, options = {}) {
1668
1691
  const platform = schedulePlatform(options.platform);
@@ -2041,7 +2064,7 @@ async function ensureFreshToken(config, dir, fetchImpl = fetch) {
2041
2064
 
2042
2065
  // src/refresh.ts
2043
2066
  import { execFile } from "node:child_process";
2044
- import { accessSync, chmodSync, constants, existsSync as existsSync7, lstatSync, readdirSync, readFileSync as readFileSync7, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync5, statSync, writeFileSync as writeFileSync7 } from "node:fs";
2067
+ import { accessSync, chmodSync, constants, existsSync as existsSync7, lstatSync as lstatSync2, readdirSync as readdirSync2, readFileSync as readFileSync7, realpathSync as realpathSync3, renameSync as renameSync4, rmSync as rmSync6, statSync, writeFileSync as writeFileSync7 } from "node:fs";
2045
2068
  import { homedir } from "node:os";
2046
2069
  import { basename, dirname as dirname3, join as join6, resolve as resolve2 } from "node:path";
2047
2070
  var LAUNCHER_RECORD_FILE = "launcher.json";
@@ -2157,14 +2180,14 @@ function removeStale(file) {
2157
2180
  const prefix = `${basename(file)}.old-`;
2158
2181
  let names;
2159
2182
  try {
2160
- names = readdirSync(dirname3(file));
2183
+ names = readdirSync2(dirname3(file));
2161
2184
  } catch {
2162
2185
  return;
2163
2186
  }
2164
2187
  for (const name of names) {
2165
2188
  if (!name.startsWith(prefix)) continue;
2166
2189
  try {
2167
- rmSync5(join6(dirname3(file), name), { force: true });
2190
+ rmSync6(join6(dirname3(file), name), { force: true });
2168
2191
  } catch {
2169
2192
  }
2170
2193
  }
@@ -2177,7 +2200,7 @@ async function refreshLauncher(target, options = {}) {
2177
2200
  const productDir = options.productDir ?? defaultProductDir(target.product);
2178
2201
  const recorded = recordedPath(productDir);
2179
2202
  const file = options.path ?? (recorded && existsSync7(recorded) ? recorded : installedPath(target, productDir, platform));
2180
- if (!existsSync7(file) || lstatSync(file).isSymbolicLink()) return null;
2203
+ if (!existsSync7(file) || lstatSync2(file).isSymbolicLink()) return null;
2181
2204
  if (!options.dryRun) removeStale(file);
2182
2205
  const from = await (options.readVersion ?? ((path) => runVersion(path, target.bin)))(file);
2183
2206
  if (!from) return null;
@@ -2226,7 +2249,7 @@ async function refreshLauncher(target, options = {}) {
2226
2249
  if (moved && !existsSync7(file)) renameSync4(moved, file);
2227
2250
  } catch {
2228
2251
  }
2229
- rmSync5(next, { force: true });
2252
+ rmSync6(next, { force: true });
2230
2253
  return arm("fail", from, "The launcher could not be replaced.");
2231
2254
  }
2232
2255
  return arm("ok", record.version, "");
@@ -2236,7 +2259,7 @@ async function refreshLauncher(target, options = {}) {
2236
2259
  }
2237
2260
 
2238
2261
  // src/index.ts
2239
- var LAUNCHER_VERSION = true ? "1.3.0" : readVersionFromPackage();
2262
+ var LAUNCHER_VERSION = true ? "1.3.1" : readVersionFromPackage();
2240
2263
  function defaultPrint(message) {
2241
2264
  process.stdout.write(`${message}
2242
2265
  `);
@@ -2758,7 +2781,7 @@ async function installOrUpdateRun(config, dir, options, print, update, flags2, m
2758
2781
  }
2759
2782
  const target = join7(dir, "payload");
2760
2783
  mkdirSync6(dir, { recursive: true });
2761
- rmSync6(target, { recursive: true, force: true });
2784
+ rmSync7(target, { recursive: true, force: true });
2762
2785
  renameSync5(join7(candidateDir, "payload"), target);
2763
2786
  writeState(dir, { version, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
2764
2787
  installer.phase("download", { seconds: (Date.now() - started) / 1e3 });
@@ -650,6 +650,28 @@ async function recoverAcquisition(dir, run2, env) {
650
650
  else if ((0, import_node_fs5.existsSync)(statePath(dir))) (0, import_node_fs5.unlinkSync)(statePath(dir));
651
651
  (0, import_node_fs5.unlinkSync)(pendingPath(dir));
652
652
  }
653
+ function reapCandidates(dir) {
654
+ const keep = readState(dir)?.acquired?.candidate;
655
+ if (!keep) return;
656
+ const base = (0, import_node_path4.join)(dir, "candidates");
657
+ let names;
658
+ try {
659
+ names = (0, import_node_fs5.readdirSync)(base);
660
+ } catch {
661
+ return;
662
+ }
663
+ for (const name of names) {
664
+ if (name === keep || !/^[a-zA-Z0-9_-]+$/.test(name)) continue;
665
+ try {
666
+ const path = (0, import_node_path4.join)(base, name);
667
+ const info = (0, import_node_fs5.lstatSync)(path);
668
+ if (!info.isDirectory() || info.isSymbolicLink()) continue;
669
+ (0, import_node_fs5.rmSync)(path, { recursive: true, force: true });
670
+ } catch (error) {
671
+ console.error(`launcher: could not remove old candidate ${name}: ${error.message}`);
672
+ }
673
+ }
674
+ }
653
675
  async function installAcquisition(dir, candidate, version, acquisition, options) {
654
676
  if (!acquisition.platforms.includes(`${process.platform}-${process.arch}`)) throw new Error("this product does not support this platform");
655
677
  const root = candidateRoot(dir, candidate);
@@ -706,6 +728,7 @@ async function installAcquisition(dir, candidate, version, acquisition, options)
706
728
  throw error;
707
729
  }
708
730
  (0, import_node_fs5.unlinkSync)(pendingPath(dir));
731
+ reapCandidates(dir);
709
732
  }
710
733
 
711
734
  // src/index.ts
@@ -2269,7 +2292,7 @@ async function refreshLauncher(target, options = {}) {
2269
2292
  }
2270
2293
 
2271
2294
  // src/index.ts
2272
- var LAUNCHER_VERSION = true ? "1.3.0" : readVersionFromPackage();
2295
+ var LAUNCHER_VERSION = true ? "1.3.1" : readVersionFromPackage();
2273
2296
  function defaultPrint(message) {
2274
2297
  process.stdout.write(`${message}
2275
2298
  `);
package/dist/refresh.js CHANGED
@@ -1,12 +1,12 @@
1
1
  // src/refresh.ts
2
2
  import { execFile } from "node:child_process";
3
- import { accessSync, chmodSync, constants, existsSync as existsSync2, lstatSync, readdirSync, readFileSync as readFileSync2, realpathSync as realpathSync2, renameSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "node:fs";
3
+ import { accessSync, chmodSync, constants, existsSync as existsSync2, lstatSync as lstatSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2, renameSync, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { basename, dirname as dirname2, join as join3, resolve as resolve2 } from "node:path";
6
6
 
7
7
  // src/acquisition.ts
8
8
  import { createHash as createHash2, randomUUID } from "node:crypto";
9
- import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, realpathSync } from "node:fs";
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, realpathSync, readdirSync, lstatSync, rmSync } from "node:fs";
10
10
  import { join as join2, resolve, relative, isAbsolute, dirname } from "node:path";
11
11
 
12
12
  // src/download.ts
@@ -290,14 +290,14 @@ function removeStale(file) {
290
290
  const prefix = `${basename(file)}.old-`;
291
291
  let names;
292
292
  try {
293
- names = readdirSync(dirname2(file));
293
+ names = readdirSync2(dirname2(file));
294
294
  } catch {
295
295
  return;
296
296
  }
297
297
  for (const name of names) {
298
298
  if (!name.startsWith(prefix)) continue;
299
299
  try {
300
- rmSync(join3(dirname2(file), name), { force: true });
300
+ rmSync2(join3(dirname2(file), name), { force: true });
301
301
  } catch {
302
302
  }
303
303
  }
@@ -310,7 +310,7 @@ async function refreshLauncher(target, options = {}) {
310
310
  const productDir = options.productDir ?? defaultProductDir(target.product);
311
311
  const recorded = recordedPath(productDir);
312
312
  const file = options.path ?? (recorded && existsSync2(recorded) ? recorded : installedPath(target, productDir, platform));
313
- if (!existsSync2(file) || lstatSync(file).isSymbolicLink()) return null;
313
+ if (!existsSync2(file) || lstatSync2(file).isSymbolicLink()) return null;
314
314
  if (!options.dryRun) removeStale(file);
315
315
  const from = await (options.readVersion ?? ((path) => runVersion(path, target.bin)))(file);
316
316
  if (!from) return null;
@@ -359,7 +359,7 @@ async function refreshLauncher(target, options = {}) {
359
359
  if (moved && !existsSync2(file)) renameSync(moved, file);
360
360
  } catch {
361
361
  }
362
- rmSync(next, { force: true });
362
+ rmSync2(next, { force: true });
363
363
  return arm("fail", from, "The launcher could not be replaced.");
364
364
  }
365
365
  return arm("ok", record.version, "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/installer-launcher",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
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",