@flowscripter/dynamic-cli-framework 5.0.4 → 5.0.6

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.
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultUpgradeService.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/DefaultUpgradeService.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,SAAS,EACT,YAAY,EAEZ,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACd,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,aAAa,EACb,aAAa,EACb,WAAW,EACX,KAAK,cAAc,EACpB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAM1E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAkB5C,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,cAAc;;gBAO/C,MAAM,EAAE,sBAAsB,EAAE,SAAS,EAAE,SAAS;IAKhE,eAAe,CACpB,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,YAAY,EAAE,YAAY,GAAG,SAAS,GACrC,IAAI;IAKA,qBAAqB,CAAC,aAAa,UAAQ,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA0BzE,QAAQ,IAAI,WAAW,GAAG,SAAS;IAanC,UAAU,IAAI,aAAa,GAAG,SAAS;IAWjC,mBAAmB,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAgBxE,eAAe,CAC1B,UAAU,CAAC,EAAE,WAAW,EACxB,YAAY,CAAC,EAAE,aAAa,EAC5B,qBAAqB,CAAC,EAAE,aAAa,GACpC,OAAO,CAAC,kBAAkB,CAAC;IAwCjB,OAAO,CAClB,UAAU,CAAC,EAAE,WAAW,EACxB,YAAY,CAAC,EAAE,aAAa,EAC5B,qBAAqB,CAAC,EAAE,aAAa,GACpC,OAAO,CAAC,aAAa,CAAC;CA4S1B"}
1
+ {"version":3,"file":"DefaultUpgradeService.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/DefaultUpgradeService.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,SAAS,EACT,YAAY,EAEZ,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACd,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,aAAa,EACb,aAAa,EACb,WAAW,EACX,KAAK,cAAc,EACpB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAM1E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAkB5C,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,cAAc;;gBAO/C,MAAM,EAAE,sBAAsB,EAAE,SAAS,EAAE,SAAS;IAKhE,eAAe,CACpB,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,YAAY,EAAE,YAAY,GAAG,SAAS,GACrC,IAAI;IAKA,qBAAqB,CAAC,aAAa,UAAQ,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA0BzE,QAAQ,IAAI,WAAW,GAAG,SAAS;IAanC,UAAU,IAAI,aAAa,GAAG,SAAS;IAWjC,mBAAmB,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;IAgBxE,eAAe,CAC1B,UAAU,CAAC,EAAE,WAAW,EACxB,YAAY,CAAC,EAAE,aAAa,EAC5B,qBAAqB,CAAC,EAAE,aAAa,GACpC,OAAO,CAAC,kBAAkB,CAAC;IAwCjB,OAAO,CAClB,UAAU,CAAC,EAAE,WAAW,EACxB,YAAY,CAAC,EAAE,aAAa,EAC5B,qBAAqB,CAAC,EAAE,aAAa,GACpC,OAAO,CAAC,aAAa,CAAC;CAsU1B"}
@@ -1,7 +1,7 @@
1
1
  import process from "node:process";
2
- import { mkdtemp } from "node:fs/promises";
2
+ import { mkdtemp, rename, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import { InstallMethod, SupportedArch, SupportedOs, } from "@flowscripter/dynamic-cli-framework-api";
6
6
  import semver from "semver";
7
7
  // checkForUpgrade() runs opportunistically on every CLI invocation (via BannerServiceProvider),
@@ -330,7 +330,9 @@ export default class DefaultUpgradeService {
330
330
  }
331
331
  async #upgradeViaGithubRelease(os, arch) {
332
332
  const { owner, repo, assetPattern } = this.#config.githubRelease;
333
- const archLabel = os === SupportedOs.MACOS ? "aarch64" : arch === SupportedArch.X64 ? "x64" : "arm64";
333
+ // macOS release assets use "aarch64" rather than "arm64" for the arm64 build; x64 (including
334
+ // Intel Macs) always uses "x64" regardless of os.
335
+ const archLabel = arch === SupportedArch.X64 ? "x64" : os === SupportedOs.MACOS ? "aarch64" : "arm64";
334
336
  const assetName = assetPattern.replace("{os}", OS_LABELS[os]).replace("{arch}", archLabel);
335
337
  const url = `https://github.com/${owner}/${repo}/releases/latest/download/${assetName}`;
336
338
  // longRunning: true gets cooperative Ctrl-C handling during what can be the slowest step of
@@ -355,6 +357,14 @@ export default class DefaultUpgradeService {
355
357
  }
356
358
  const extractedBinary = join(tmpDir, `${this.#cliConfig.name}.exe`);
357
359
  const oldPath = `${currentExecutable}.old.exe`;
360
+ // Best-effort cleanup of a stale "<exe>.old.exe" left behind by a *previous* upgrade run.
361
+ // Windows won't let us delete the just-renamed-aside exe while this process still has it
362
+ // open/mapped - that can only happen once we're no longer holding it, i.e. at the start of
363
+ // the NEXT invocation, before we move today's running exe aside. Ignore failures: the file
364
+ // may not exist, or may still be locked (e.g. another instance still running).
365
+ await this.#spawnService.spawn(["cmd", "/c", "del", "/f", "/q", oldPath], {
366
+ mode: "ignore",
367
+ });
358
368
  const moveResult = await this.#spawnService.spawn(["cmd", "/c", "move", "/y", currentExecutable, oldPath], { mode: "ignore" });
359
369
  if (!moveResult.ok) {
360
370
  throw new Error("Failed to move current executable aside");
@@ -372,8 +382,22 @@ export default class DefaultUpgradeService {
372
382
  throw new Error("Failed to extract release archive");
373
383
  }
374
384
  const extractedBinary = join(tmpDir, this.#cliConfig.name);
375
- await Bun.write(currentExecutable, Bun.file(extractedBinary));
376
- await this.#spawnService.spawn(["chmod", "+x", currentExecutable], { mode: "ignore" });
385
+ // Extract into a staging file in the SAME directory as the running executable (not
386
+ // os.tmpdir(), which may be a different filesystem/mount), then atomically rename it over
387
+ // currentExecutable. This avoids ETXTBSY: the kernel refuses to open-for-write the inode
388
+ // mapped as a running process's text segment, but rename() only swaps the directory entry
389
+ // to point at a different inode - the running process keeps executing from its original,
390
+ // now-unlinked-but-still-open inode until it next execs/restarts.
391
+ const stagingDir = await mkdtemp(join(dirname(currentExecutable), ".upgrade-"));
392
+ try {
393
+ const stagingBinary = join(stagingDir, this.#cliConfig.name);
394
+ await Bun.write(stagingBinary, Bun.file(extractedBinary));
395
+ await this.#spawnService.spawn(["chmod", "+x", stagingBinary], { mode: "ignore" });
396
+ await rename(stagingBinary, currentExecutable);
397
+ }
398
+ finally {
399
+ await rm(stagingDir, { recursive: true, force: true });
400
+ }
377
401
  }
378
402
  }
379
403
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "5.0.4",
3
+ "version": "5.0.6",
4
4
  "description": "A framework for developing CLI applications which supports dynamic discovery and installation of new commands",
5
5
  "keywords": [
6
6
  "bun",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@flowscripter/dynamic-cli-framework-api": "^2.0.0",
52
- "@flowscripter/dynamic-plugin-framework": "2.0.2",
52
+ "@flowscripter/dynamic-plugin-framework": "2.0.3",
53
53
  "emphasize": "^7.0.0",
54
54
  "fastest-levenshtein": "^1.0.16",
55
55
  "figlet": "^1.11.0",
@@ -1,7 +1,7 @@
1
1
  import process from "node:process";
2
- import { mkdtemp } from "node:fs/promises";
2
+ import { mkdtemp, rename, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import type {
6
6
  CLIConfig,
7
7
  FetchService,
@@ -413,8 +413,10 @@ export default class DefaultUpgradeService implements UpgradeService {
413
413
 
414
414
  async #upgradeViaGithubRelease(os: SupportedOs, arch: SupportedArch): Promise<void> {
415
415
  const { owner, repo, assetPattern } = this.#config.githubRelease!;
416
+ // macOS release assets use "aarch64" rather than "arm64" for the arm64 build; x64 (including
417
+ // Intel Macs) always uses "x64" regardless of os.
416
418
  const archLabel =
417
- os === SupportedOs.MACOS ? "aarch64" : arch === SupportedArch.X64 ? "x64" : "arm64";
419
+ arch === SupportedArch.X64 ? "x64" : os === SupportedOs.MACOS ? "aarch64" : "arm64";
418
420
  const assetName = assetPattern.replace("{os}", OS_LABELS[os]).replace("{arch}", archLabel);
419
421
  const url = `https://github.com/${owner}/${repo}/releases/latest/download/${assetName}`;
420
422
 
@@ -446,6 +448,16 @@ export default class DefaultUpgradeService implements UpgradeService {
446
448
  }
447
449
  const extractedBinary = join(tmpDir, `${this.#cliConfig.name}.exe`);
448
450
  const oldPath = `${currentExecutable}.old.exe`;
451
+
452
+ // Best-effort cleanup of a stale "<exe>.old.exe" left behind by a *previous* upgrade run.
453
+ // Windows won't let us delete the just-renamed-aside exe while this process still has it
454
+ // open/mapped - that can only happen once we're no longer holding it, i.e. at the start of
455
+ // the NEXT invocation, before we move today's running exe aside. Ignore failures: the file
456
+ // may not exist, or may still be locked (e.g. another instance still running).
457
+ await this.#spawnService!.spawn(["cmd", "/c", "del", "/f", "/q", oldPath], {
458
+ mode: "ignore",
459
+ });
460
+
449
461
  const moveResult = await this.#spawnService!.spawn(
450
462
  ["cmd", "/c", "move", "/y", currentExecutable, oldPath],
451
463
  { mode: "ignore" },
@@ -471,8 +483,22 @@ export default class DefaultUpgradeService implements UpgradeService {
471
483
  throw new Error("Failed to extract release archive");
472
484
  }
473
485
  const extractedBinary = join(tmpDir, this.#cliConfig.name);
474
- await Bun.write(currentExecutable, Bun.file(extractedBinary));
475
- await this.#spawnService!.spawn(["chmod", "+x", currentExecutable], { mode: "ignore" });
486
+
487
+ // Extract into a staging file in the SAME directory as the running executable (not
488
+ // os.tmpdir(), which may be a different filesystem/mount), then atomically rename it over
489
+ // currentExecutable. This avoids ETXTBSY: the kernel refuses to open-for-write the inode
490
+ // mapped as a running process's text segment, but rename() only swaps the directory entry
491
+ // to point at a different inode - the running process keeps executing from its original,
492
+ // now-unlinked-but-still-open inode until it next execs/restarts.
493
+ const stagingDir = await mkdtemp(join(dirname(currentExecutable), ".upgrade-"));
494
+ try {
495
+ const stagingBinary = join(stagingDir, this.#cliConfig.name);
496
+ await Bun.write(stagingBinary, Bun.file(extractedBinary));
497
+ await this.#spawnService!.spawn(["chmod", "+x", stagingBinary], { mode: "ignore" });
498
+ await rename(stagingBinary, currentExecutable);
499
+ } finally {
500
+ await rm(stagingDir, { recursive: true, force: true });
501
+ }
476
502
  }
477
503
  }
478
504
  }