@flowscripter/dynamic-cli-framework 2.3.2 → 2.4.0

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.
@@ -12,7 +12,7 @@ export default class VersionCommand {
12
12
  let line = context.cliConfig.version;
13
13
  if (context.doesServiceExist(UPGRADE_SERVICE_ID)) {
14
14
  const upgradeService = context.getServiceById(UPGRADE_SERVICE_ID);
15
- const result = await upgradeService.checkForUpgrade().catch(() => undefined);
15
+ const result = await upgradeService.getUpgradeCheckResult().catch(() => undefined);
16
16
  if (result?.updateAvailable) {
17
17
  line += ` (${result.latestVersion} available, run '${context.cliConfig.name} upgrade')`;
18
18
  }
@@ -52,7 +52,7 @@ export default class BannerServiceProvider {
52
52
  }
53
53
  if (context.doesServiceExist(UPGRADE_SERVICE_ID)) {
54
54
  const upgradeService = context.getServiceById(UPGRADE_SERVICE_ID);
55
- const result = await upgradeService.checkForUpgrade().catch(() => undefined);
55
+ const result = await upgradeService.getUpgradeCheckResult().catch(() => undefined);
56
56
  if (result?.updateAvailable) {
57
57
  await printerService.info(` ${printerService.secondary(`(${result.latestVersion} available, run '${cliConfig.name} upgrade')`)}\n`);
58
58
  }
@@ -1 +1 @@
1
- {"version":3,"file":"SpawnInterfaceAdapter.d.ts","sourceRoot":"","sources":["../../../../src/service/plugin/SpawnInterfaceAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAC;AAE1F,MAAM,WAAW,4BAA4B;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC;AAED;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,cAAc;;gBAOhE,YAAY,EAAE,YAAY,EAC1B,cAAc,EAAE,cAAc,EAC9B,OAAO,GAAE,4BAAiC;IAQ/B,KAAK,CAChB,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAC9B,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GACvB,OAAO,CAAC,WAAW,CAAC;CA4BxB"}
1
+ {"version":3,"file":"SpawnInterfaceAdapter.d.ts","sourceRoot":"","sources":["../../../../src/service/plugin/SpawnInterfaceAdapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,wCAAwC,CAAC;AAE1F,MAAM,WAAW,4BAA4B;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wBAAwB,CAAC,EAAE,MAAM,CAAC;CACnC;AAED;;;;GAIG;AACH,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,cAAc;;gBAOhE,YAAY,EAAE,YAAY,EAC1B,cAAc,EAAE,cAAc,EAC9B,OAAO,GAAE,4BAAiC;IAQ/B,KAAK,CAChB,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAC9B,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GACvB,OAAO,CAAC,WAAW,CAAC;CA+BxB"}
@@ -33,8 +33,11 @@ export default class SpawnInterfaceAdapter {
33
33
  this.#printerService.endQuote();
34
34
  this.#printerService.endMark();
35
35
  await this.#printerService.clearMarked(this.#markMinimumDisplayTimeMs);
36
- return result.ok
37
- ? { ok: true, exitCode: result.exitCode }
36
+ if (result.ok) {
37
+ return { ok: true, exitCode: result.exitCode };
38
+ }
39
+ return "timedOut" in result
40
+ ? { ok: false, error: new Error("Command timed out") }
38
41
  : { ok: false, exitCode: result.exitCode, error: result.error };
39
42
  }
40
43
  }
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultSpawnService.d.ts","sourceRoot":"","sources":["../../../../src/service/spawn/DefaultSpawnService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,YAAY,EACZ,WAAW,EACX,YAAY,EACb,MAAM,yCAAyC,CAAC;AAOjD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAa3E;AAED,MAAM,CAAC,OAAO,OAAO,mBAAoB,YAAW,YAAY;;IAIvD,eAAe,CAAC,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,eAAe,GAAG,IAAI;IAkCjF,KAAK,CAChB,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAC9B,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,WAAW,CAAC;CAiExB"}
1
+ {"version":3,"file":"DefaultSpawnService.d.ts","sourceRoot":"","sources":["../../../../src/service/spawn/DefaultSpawnService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,YAAY,EACZ,WAAW,EACX,YAAY,EACb,MAAM,yCAAyC,CAAC;AAOjD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAa3E;AAED,MAAM,CAAC,OAAO,OAAO,mBAAoB,YAAW,YAAY;;IAIvD,eAAe,CAAC,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,eAAe,GAAG,IAAI;IAkDjF,KAAK,CAChB,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,EAC9B,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,WAAW,CAAC;CA8ExB"}
@@ -28,6 +28,18 @@ export default class DefaultSpawnService {
28
28
  this.#printerService = printerService;
29
29
  this.#shutdownService = shutdownService;
30
30
  }
31
+ async #terminate(proc, command) {
32
+ logger.debug(() => `Sending SIGTERM to spawned command '${command.join(" ")}'`);
33
+ proc.kill("SIGTERM");
34
+ const exitedInTime = await Promise.race([
35
+ proc.exited.then(() => true),
36
+ new Promise((resolve) => setTimeout(() => resolve(false), SHUTDOWN_GRACE_PERIOD_MS)),
37
+ ]);
38
+ if (!exitedInTime) {
39
+ logger.debug(() => `Sending SIGKILL to spawned command '${command.join(" ")}'`);
40
+ proc.kill("SIGKILL");
41
+ }
42
+ }
31
43
  async #pipeLines(stream, streamName, onOutput) {
32
44
  if (typeof stream !== "object" || stream === null) {
33
45
  return;
@@ -85,16 +97,7 @@ export default class DefaultSpawnService {
85
97
  if (settled) {
86
98
  return;
87
99
  }
88
- logger.debug(() => `Sending SIGTERM to spawned command '${command.join(" ")}'`);
89
- proc.kill("SIGTERM");
90
- await Promise.race([
91
- proc.exited.then(() => { }),
92
- new Promise((resolve) => setTimeout(resolve, SHUTDOWN_GRACE_PERIOD_MS)),
93
- ]);
94
- if (!settled) {
95
- logger.debug(() => `Sending SIGKILL to spawned command '${command.join(" ")}'`);
96
- proc.kill("SIGKILL");
97
- }
100
+ await this.#terminate(proc, command);
98
101
  });
99
102
  if (stdio === "wrapped" && options.onOutput) {
100
103
  const onOutput = options.onOutput;
@@ -102,6 +105,25 @@ export default class DefaultSpawnService {
102
105
  void this.#pipeLines(proc.stderr, "stderr", onOutput);
103
106
  }
104
107
  try {
108
+ if (options.timeoutMs === undefined) {
109
+ const exitCode = await proc.exited;
110
+ settled = true;
111
+ return exitCode === 0 ? { ok: true, exitCode } : { ok: false, exitCode };
112
+ }
113
+ let timeoutHandle;
114
+ const timedOut = await Promise.race([
115
+ proc.exited.then(() => false),
116
+ new Promise((resolve) => {
117
+ timeoutHandle = setTimeout(() => resolve(true), options.timeoutMs);
118
+ }),
119
+ ]);
120
+ clearTimeout(timeoutHandle);
121
+ if (timedOut) {
122
+ logger.debug(() => `Command '${command.join(" ")}' timed out after ${options.timeoutMs}ms`);
123
+ await this.#terminate(proc, command);
124
+ settled = true;
125
+ return { ok: false, timedOut: true };
126
+ }
105
127
  const exitCode = await proc.exited;
106
128
  settled = true;
107
129
  return exitCode === 0 ? { ok: true, exitCode } : { ok: false, exitCode };
@@ -1,10 +1,12 @@
1
1
  import type { CLIConfig, ShutdownService, SpawnService, UpgradeCheckResult, UpgradeResult } from "@flowscripter/dynamic-cli-framework-api";
2
2
  import { InstallMethod, SupportedArch, SupportedOs, type UpgradeService } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import type { UpgradeLocationsConfig } from "./UpgradeLocationsConfig.ts";
4
+ export declare const VERSION_CHECK_TIMEOUT_MS = 250;
4
5
  export default class DefaultUpgradeService implements UpgradeService {
5
6
  #private;
6
7
  constructor(config: UpgradeLocationsConfig, cliConfig: CLIConfig);
7
8
  setDependencies(spawnService: SpawnService, shutdownService?: ShutdownService): void;
9
+ getUpgradeCheckResult(waitForResult?: boolean): Promise<UpgradeCheckResult | undefined>;
8
10
  detectOs(): SupportedOs | undefined;
9
11
  detectArch(): SupportedArch | undefined;
10
12
  detectInstallMethod(os: SupportedOs): Promise<InstallMethod | undefined>;
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultUpgradeService.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/DefaultUpgradeService.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,SAAS,EACT,eAAe,EACf,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;AAe1E,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,cAAc;;gBAM/C,MAAM,EAAE,sBAAsB,EAAE,SAAS,EAAE,SAAS;IAKhE,eAAe,CAAC,YAAY,EAAE,YAAY,EAAE,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI;IAKpF,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,GAAG,SAAS,CAAC;IAmC7B,OAAO,CAClB,UAAU,CAAC,EAAE,WAAW,EACxB,YAAY,CAAC,EAAE,aAAa,EAC5B,qBAAqB,CAAC,EAAE,aAAa,GACpC,OAAO,CAAC,aAAa,CAAC;CA2Q1B"}
1
+ {"version":3,"file":"DefaultUpgradeService.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/DefaultUpgradeService.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,SAAS,EACT,eAAe,EAEf,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;AAO1E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAc5C,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,cAAc;;gBAO/C,MAAM,EAAE,sBAAsB,EAAE,SAAS,EAAE,SAAS;IAKhE,eAAe,CAAC,YAAY,EAAE,YAAY,EAAE,eAAe,CAAC,EAAE,eAAe,GAAG,IAAI;IAKpF,qBAAqB,CAAC,aAAa,UAAQ,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAerF,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,GAAG,SAAS,CAAC;IAmC7B,OAAO,CAClB,UAAU,CAAC,EAAE,WAAW,EACxB,YAAY,CAAC,EAAE,aAAa,EAC5B,qBAAqB,CAAC,EAAE,aAAa,GACpC,OAAO,CAAC,aAAa,CAAC;CAyQ1B"}
@@ -7,8 +7,13 @@ import semver from "semver";
7
7
  import getLogger from "../../util/logger.js";
8
8
  const logger = getLogger("DefaultUpgradeService");
9
9
  // checkForUpgrade() runs opportunistically on every CLI invocation (via BannerServiceProvider),
10
- // so its version-check network calls must never be allowed to stall CLI startup.
11
- const VERSION_CHECK_TIMEOUT_MS = 250;
10
+ // so its version-check network/spawn calls must never be allowed to stall CLI startup.
11
+ export const VERSION_CHECK_TIMEOUT_MS = 250;
12
+ function describeSpawnFailure(result) {
13
+ return "timedOut" in result
14
+ ? "timed out"
15
+ : (result.error?.message ?? `exit code ${result.exitCode}`);
16
+ }
12
17
  const OS_LABELS = {
13
18
  [SupportedOs.LINUX]: "Linux",
14
19
  [SupportedOs.MACOS]: "MacOS",
@@ -17,6 +22,7 @@ const OS_LABELS = {
17
22
  export default class DefaultUpgradeService {
18
23
  #spawnService;
19
24
  #shutdownService;
25
+ #upgradeCheckPromise;
20
26
  #config;
21
27
  #cliConfig;
22
28
  constructor(config, cliConfig) {
@@ -27,6 +33,18 @@ export default class DefaultUpgradeService {
27
33
  this.#spawnService = spawnService;
28
34
  this.#shutdownService = shutdownService;
29
35
  }
36
+ getUpgradeCheckResult(waitForResult = false) {
37
+ if (!this.#upgradeCheckPromise) {
38
+ this.#upgradeCheckPromise = this.checkForUpgrade();
39
+ }
40
+ if (waitForResult) {
41
+ return this.#upgradeCheckPromise;
42
+ }
43
+ return Promise.race([
44
+ this.#upgradeCheckPromise,
45
+ new Promise((resolve) => setTimeout(() => resolve(undefined), VERSION_CHECK_TIMEOUT_MS)),
46
+ ]);
47
+ }
30
48
  detectOs() {
31
49
  switch (process.platform) {
32
50
  case "linux":
@@ -96,7 +114,10 @@ export default class DefaultUpgradeService {
96
114
  }
97
115
  async upgrade(osOverride, archOverride, installMethodOverride) {
98
116
  const oldVersion = this.#cliConfig.version;
99
- const checkResult = await this.checkForUpgrade(osOverride, archOverride, installMethodOverride);
117
+ const hasOverride = osOverride !== undefined || archOverride !== undefined || installMethodOverride !== undefined;
118
+ const checkResult = hasOverride
119
+ ? await this.checkForUpgrade(osOverride, archOverride, installMethodOverride)
120
+ : await this.getUpgradeCheckResult(true);
100
121
  if (!checkResult) {
101
122
  return {
102
123
  ok: false,
@@ -135,14 +156,14 @@ export default class DefaultUpgradeService {
135
156
  if (!this.#spawnService || !this.#config.homebrew) {
136
157
  return false;
137
158
  }
138
- const result = await this.#spawnService.spawn(["brew", "list", "--versions", this.#config.homebrew.formula], { stdio: "wrapped", longRunning: false });
159
+ const result = await this.#spawnService.spawn(["brew", "list", "--versions", this.#config.homebrew.formula], { stdio: "wrapped", longRunning: false, timeoutMs: VERSION_CHECK_TIMEOUT_MS });
139
160
  return result.ok;
140
161
  }
141
162
  async #isWingetInstalled() {
142
163
  if (!this.#spawnService || !this.#config.winget) {
143
164
  return false;
144
165
  }
145
- const result = await this.#spawnService.spawn(["winget", "list", "--id", this.#config.winget.packageId], { stdio: "wrapped", longRunning: false });
166
+ const result = await this.#spawnService.spawn(["winget", "list", "--id", this.#config.winget.packageId], { stdio: "wrapped", longRunning: false, timeoutMs: VERSION_CHECK_TIMEOUT_MS });
146
167
  return result.ok;
147
168
  }
148
169
  #isLinuxScriptInstall() {
@@ -209,6 +230,7 @@ export default class DefaultUpgradeService {
209
230
  const result = await this.#spawnService.spawn(["winget", "show", "--id", this.#config.winget.packageId], {
210
231
  stdio: "wrapped",
211
232
  longRunning: false,
233
+ timeoutMs: VERSION_CHECK_TIMEOUT_MS,
212
234
  onOutput: (line) => lines.push(line),
213
235
  });
214
236
  if (!result.ok) {
@@ -226,14 +248,14 @@ export default class DefaultUpgradeService {
226
248
  const { scriptUrl } = this.#config.linuxScript;
227
249
  const result = await this.#spawnService.spawn(["sh", "-c", `curl -fsSL ${scriptUrl} | sh`]);
228
250
  if (!result.ok) {
229
- throw new Error(`Install script failed: ${result.error?.message ?? `exit code ${result.exitCode}`}`);
251
+ throw new Error(`Install script failed: ${describeSpawnFailure(result)}`);
230
252
  }
231
253
  }
232
254
  async #upgradeViaHomebrew() {
233
255
  const { tap, formula } = this.#config.homebrew;
234
256
  const result = await this.#spawnService.spawn(["brew", "upgrade", `${tap}/${formula}`]);
235
257
  if (!result.ok) {
236
- throw new Error(`brew upgrade failed: ${result.error?.message ?? `exit code ${result.exitCode}`}`);
258
+ throw new Error(`brew upgrade failed: ${describeSpawnFailure(result)}`);
237
259
  }
238
260
  }
239
261
  async #upgradeViaWinget() {
@@ -248,7 +270,7 @@ export default class DefaultUpgradeService {
248
270
  "--accept-source-agreements",
249
271
  ]);
250
272
  if (!result.ok) {
251
- throw new Error(`winget upgrade failed: ${result.error?.message ?? `exit code ${result.exitCode}`}`);
273
+ throw new Error(`winget upgrade failed: ${describeSpawnFailure(result)}`);
252
274
  }
253
275
  }
254
276
  async #upgradeViaGithubRelease(os, arch) {
@@ -256,8 +278,7 @@ export default class DefaultUpgradeService {
256
278
  const archLabel = os === SupportedOs.MACOS ? "aarch64" : arch === SupportedArch.X64 ? "x64" : "arm64";
257
279
  const assetName = assetPattern.replace("{os}", OS_LABELS[os]).replace("{arch}", archLabel);
258
280
  const url = `https://github.com/${owner}/${repo}/releases/latest/download/${assetName}`;
259
- // Downloading the release asset happens outside SpawnService (which enters long-running mode
260
- // for its own subprocesses by default), so bracket it here to get the same cooperative
281
+ // Downloading the release asset but enter long-running mode to get cooperative
261
282
  // Ctrl-C handling during what can be the slowest step of the upgrade.
262
283
  this.#shutdownService?.enterLongRunningMode();
263
284
  let archiveData;
@@ -1 +1 @@
1
- {"version":3,"file":"UpgradeServiceProvider.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/UpgradeServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAczE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAK1E,MAAM,CAAC,OAAO,OAAO,sBAAuB,YAAW,eAAe;;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAsB;IAChD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;gBAKd,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB;IAKnE,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;IASpD,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAgH1D"}
1
+ {"version":3,"file":"UpgradeServiceProvider.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/UpgradeServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAczE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAK1E,MAAM,CAAC,OAAO,OAAO,sBAAuB,YAAW,eAAe;;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAsB;IAChD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;gBAKd,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB;IAKnE,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;IASpD,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAkH1D"}
@@ -39,6 +39,7 @@ export default class UpgradeServiceProvider {
39
39
  else {
40
40
  logger.debug(() => "SpawnService not available, upgrade install methods will be unavailable");
41
41
  }
42
+ void upgradeService.getUpgradeCheckResult();
42
43
  if (!context.doesServiceExist(PROMPTER_SERVICE_ID)) {
43
44
  logger.debug(() => "PrompterService not available, skipping auto-upgrade");
44
45
  return;
@@ -101,7 +102,7 @@ export default class UpgradeServiceProvider {
101
102
  }
102
103
  async #checkAndUpgrade(context, upgradeService, cliConfig) {
103
104
  try {
104
- const checkResult = await upgradeService.checkForUpgrade();
105
+ const checkResult = await upgradeService.getUpgradeCheckResult();
105
106
  if (!checkResult?.updateAvailable) {
106
107
  return;
107
108
  }
@@ -1 +1 @@
1
- {"version":3,"file":"UpgradeSubCommand.d.ts","sourceRoot":"","sources":["../../../../../src/service/upgrade/command/UpgradeSubCommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yCAAyC,CAAC;AACtE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,EACL,KAAK,cAAc,EAEpB,MAAM,yCAAyC,CAAC;AASjD,OAAO,KAAK,qBAAqB,MAAM,6BAA6B,CAAC;AAErE,qBAAa,iBAAkB,YAAW,UAAU;;IAClD,QAAQ,CAAC,IAAI,aAAa;IAC1B,QAAQ,CAAC,WAAW,6CAA6C;IACjE,QAAQ,CAAC,mBAAmB,SAAS;IACrC,QAAQ,CAAC,WAAW,UAAM;IAE1B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAerC;gBAIiB,cAAc,EAAE,qBAAqB;IAI3C,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;CAsCtF"}
1
+ {"version":3,"file":"UpgradeSubCommand.d.ts","sourceRoot":"","sources":["../../../../../src/service/upgrade/command/UpgradeSubCommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yCAAyC,CAAC;AACtE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,EACL,KAAK,cAAc,EAEpB,MAAM,yCAAyC,CAAC;AASjD,OAAO,KAAK,qBAAqB,MAAM,6BAA6B,CAAC;AAErE,qBAAa,iBAAkB,YAAW,UAAU;;IAClD,QAAQ,CAAC,IAAI,aAAa;IAC1B,QAAQ,CAAC,WAAW,6CAA6C;IACjE,QAAQ,CAAC,mBAAmB,SAAS;IACrC,QAAQ,CAAC,WAAW,UAAM;IAE1B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAerC;gBAIiB,cAAc,EAAE,qBAAqB;IAI3C,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;CAyCtF"}
@@ -31,7 +31,9 @@ export class UpgradeSubCommand {
31
31
  const os = argumentValues.os;
32
32
  const arch = undefined;
33
33
  const installMethod = argumentValues["install-method"];
34
- const checkResult = await this.#upgradeService.checkForUpgrade(os, arch, installMethod);
34
+ const checkResult = os === undefined && installMethod === undefined
35
+ ? await this.#upgradeService.getUpgradeCheckResult(true)
36
+ : await this.#upgradeService.checkForUpgrade(os, arch, installMethod);
35
37
  if (!checkResult) {
36
38
  await printerService.error(`No upgrade location is configured for the detected or requested platform.\n`, Icon.FAILURE);
37
39
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "2.3.2",
3
+ "version": "2.4.0",
4
4
  "description": "A framework for developing CLI applications which supports dynamic discovery and installation of new commands",
5
5
  "keywords": [
6
6
  "bun",
@@ -48,7 +48,7 @@
48
48
  "build": "tsc -p tsconfig.build.json"
49
49
  },
50
50
  "dependencies": {
51
- "@flowscripter/dynamic-cli-framework-api": "^1.2.0",
51
+ "@flowscripter/dynamic-cli-framework-api": "^1.3.0",
52
52
  "@flowscripter/dynamic-plugin-framework": "1.11.1",
53
53
  "emphasize": "^7.0.0",
54
54
  "fastest-levenshtein": "^1.0.16",
@@ -19,7 +19,7 @@ export default class VersionCommand implements GlobalCommand {
19
19
  let line = context.cliConfig.version;
20
20
  if (context.doesServiceExist(UPGRADE_SERVICE_ID)) {
21
21
  const upgradeService = context.getServiceById(UPGRADE_SERVICE_ID) as UpgradeService;
22
- const result = await upgradeService.checkForUpgrade().catch(() => undefined);
22
+ const result = await upgradeService.getUpgradeCheckResult().catch(() => undefined);
23
23
  if (result?.updateAvailable) {
24
24
  line += ` (${result.latestVersion} available, run '${context.cliConfig.name} upgrade')`;
25
25
  }
@@ -72,7 +72,7 @@ export default class BannerServiceProvider implements ServiceProvider {
72
72
  }
73
73
  if (context.doesServiceExist(UPGRADE_SERVICE_ID)) {
74
74
  const upgradeService = context.getServiceById(UPGRADE_SERVICE_ID) as UpgradeService;
75
- const result = await upgradeService.checkForUpgrade().catch(() => undefined);
75
+ const result = await upgradeService.getUpgradeCheckResult().catch(() => undefined);
76
76
  if (result?.updateAvailable) {
77
77
  await printerService.info(
78
78
  ` ${printerService.secondary(
@@ -55,8 +55,11 @@ export default class SpawnInterfaceAdapter implements SpawnInterface {
55
55
  this.#printerService.endMark();
56
56
  await this.#printerService.clearMarked(this.#markMinimumDisplayTimeMs);
57
57
 
58
- return result.ok
59
- ? { ok: true, exitCode: result.exitCode }
58
+ if (result.ok) {
59
+ return { ok: true, exitCode: result.exitCode };
60
+ }
61
+ return "timedOut" in result
62
+ ? { ok: false, error: new Error("Command timed out") }
60
63
  : { ok: false, exitCode: result.exitCode, error: result.error };
61
64
  }
62
65
  }
@@ -41,6 +41,22 @@ export default class DefaultSpawnService implements SpawnService {
41
41
  this.#shutdownService = shutdownService;
42
42
  }
43
43
 
44
+ async #terminate(
45
+ proc: ReturnType<typeof Bun.spawn>,
46
+ command: ReadonlyArray<string>,
47
+ ): Promise<void> {
48
+ logger.debug(() => `Sending SIGTERM to spawned command '${command.join(" ")}'`);
49
+ proc.kill("SIGTERM");
50
+ const exitedInTime = await Promise.race([
51
+ proc.exited.then(() => true),
52
+ new Promise<boolean>((resolve) => setTimeout(() => resolve(false), SHUTDOWN_GRACE_PERIOD_MS)),
53
+ ]);
54
+ if (!exitedInTime) {
55
+ logger.debug(() => `Sending SIGKILL to spawned command '${command.join(" ")}'`);
56
+ proc.kill("SIGKILL");
57
+ }
58
+ }
59
+
44
60
  async #pipeLines(
45
61
  stream: ReadableStream<Uint8Array> | number | undefined,
46
62
  streamName: "stdout" | "stderr",
@@ -110,16 +126,7 @@ export default class DefaultSpawnService implements SpawnService {
110
126
  if (settled) {
111
127
  return;
112
128
  }
113
- logger.debug(() => `Sending SIGTERM to spawned command '${command.join(" ")}'`);
114
- proc.kill("SIGTERM");
115
- await Promise.race([
116
- proc.exited.then(() => {}),
117
- new Promise<void>((resolve) => setTimeout(resolve, SHUTDOWN_GRACE_PERIOD_MS)),
118
- ]);
119
- if (!settled) {
120
- logger.debug(() => `Sending SIGKILL to spawned command '${command.join(" ")}'`);
121
- proc.kill("SIGKILL");
122
- }
129
+ await this.#terminate(proc, command);
123
130
  });
124
131
 
125
132
  if (stdio === "wrapped" && options.onOutput) {
@@ -129,6 +136,28 @@ export default class DefaultSpawnService implements SpawnService {
129
136
  }
130
137
 
131
138
  try {
139
+ if (options.timeoutMs === undefined) {
140
+ const exitCode = await proc.exited;
141
+ settled = true;
142
+ return exitCode === 0 ? { ok: true, exitCode } : { ok: false, exitCode };
143
+ }
144
+
145
+ let timeoutHandle: ReturnType<typeof setTimeout>;
146
+ const timedOut = await Promise.race([
147
+ proc.exited.then(() => false),
148
+ new Promise<boolean>((resolve) => {
149
+ timeoutHandle = setTimeout(() => resolve(true), options.timeoutMs);
150
+ }),
151
+ ]);
152
+ clearTimeout(timeoutHandle!);
153
+
154
+ if (timedOut) {
155
+ logger.debug(() => `Command '${command.join(" ")}' timed out after ${options.timeoutMs}ms`);
156
+ await this.#terminate(proc, command);
157
+ settled = true;
158
+ return { ok: false, timedOut: true };
159
+ }
160
+
132
161
  const exitCode = await proc.exited;
133
162
  settled = true;
134
163
  return exitCode === 0 ? { ok: true, exitCode } : { ok: false, exitCode };
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import type {
6
6
  CLIConfig,
7
7
  ShutdownService,
8
+ SpawnResult,
8
9
  SpawnService,
9
10
  UpgradeCheckResult,
10
11
  UpgradeResult,
@@ -22,8 +23,14 @@ import getLogger from "../../util/logger.ts";
22
23
  const logger = getLogger("DefaultUpgradeService");
23
24
 
24
25
  // checkForUpgrade() runs opportunistically on every CLI invocation (via BannerServiceProvider),
25
- // so its version-check network calls must never be allowed to stall CLI startup.
26
- const VERSION_CHECK_TIMEOUT_MS = 250;
26
+ // so its version-check network/spawn calls must never be allowed to stall CLI startup.
27
+ export const VERSION_CHECK_TIMEOUT_MS = 250;
28
+
29
+ function describeSpawnFailure(result: Extract<SpawnResult, { ok: false }>): string {
30
+ return "timedOut" in result
31
+ ? "timed out"
32
+ : (result.error?.message ?? `exit code ${result.exitCode}`);
33
+ }
27
34
 
28
35
  const OS_LABELS: Record<SupportedOs, string> = {
29
36
  [SupportedOs.LINUX]: "Linux",
@@ -34,6 +41,7 @@ const OS_LABELS: Record<SupportedOs, string> = {
34
41
  export default class DefaultUpgradeService implements UpgradeService {
35
42
  #spawnService: SpawnService | undefined;
36
43
  #shutdownService: ShutdownService | undefined;
44
+ #upgradeCheckPromise: Promise<UpgradeCheckResult | undefined> | undefined;
37
45
  readonly #config: UpgradeLocationsConfig;
38
46
  readonly #cliConfig: CLIConfig;
39
47
 
@@ -47,6 +55,21 @@ export default class DefaultUpgradeService implements UpgradeService {
47
55
  this.#shutdownService = shutdownService;
48
56
  }
49
57
 
58
+ public getUpgradeCheckResult(waitForResult = false): Promise<UpgradeCheckResult | undefined> {
59
+ if (!this.#upgradeCheckPromise) {
60
+ this.#upgradeCheckPromise = this.checkForUpgrade();
61
+ }
62
+ if (waitForResult) {
63
+ return this.#upgradeCheckPromise;
64
+ }
65
+ return Promise.race([
66
+ this.#upgradeCheckPromise,
67
+ new Promise<undefined>((resolve) =>
68
+ setTimeout(() => resolve(undefined), VERSION_CHECK_TIMEOUT_MS),
69
+ ),
70
+ ]);
71
+ }
72
+
50
73
  public detectOs(): SupportedOs | undefined {
51
74
  switch (process.platform) {
52
75
  case "linux":
@@ -132,7 +155,11 @@ export default class DefaultUpgradeService implements UpgradeService {
132
155
  installMethodOverride?: InstallMethod,
133
156
  ): Promise<UpgradeResult> {
134
157
  const oldVersion = this.#cliConfig.version;
135
- const checkResult = await this.checkForUpgrade(osOverride, archOverride, installMethodOverride);
158
+ const hasOverride =
159
+ osOverride !== undefined || archOverride !== undefined || installMethodOverride !== undefined;
160
+ const checkResult = hasOverride
161
+ ? await this.checkForUpgrade(osOverride, archOverride, installMethodOverride)
162
+ : await this.getUpgradeCheckResult(true);
136
163
  if (!checkResult) {
137
164
  return {
138
165
  ok: false,
@@ -177,7 +204,7 @@ export default class DefaultUpgradeService implements UpgradeService {
177
204
  }
178
205
  const result = await this.#spawnService.spawn(
179
206
  ["brew", "list", "--versions", this.#config.homebrew.formula],
180
- { stdio: "wrapped", longRunning: false },
207
+ { stdio: "wrapped", longRunning: false, timeoutMs: VERSION_CHECK_TIMEOUT_MS },
181
208
  );
182
209
  return result.ok;
183
210
  }
@@ -188,7 +215,7 @@ export default class DefaultUpgradeService implements UpgradeService {
188
215
  }
189
216
  const result = await this.#spawnService.spawn(
190
217
  ["winget", "list", "--id", this.#config.winget.packageId],
191
- { stdio: "wrapped", longRunning: false },
218
+ { stdio: "wrapped", longRunning: false, timeoutMs: VERSION_CHECK_TIMEOUT_MS },
192
219
  );
193
220
  return result.ok;
194
221
  }
@@ -267,6 +294,7 @@ export default class DefaultUpgradeService implements UpgradeService {
267
294
  {
268
295
  stdio: "wrapped",
269
296
  longRunning: false,
297
+ timeoutMs: VERSION_CHECK_TIMEOUT_MS,
270
298
  onOutput: (line) => lines.push(line),
271
299
  },
272
300
  );
@@ -286,9 +314,7 @@ export default class DefaultUpgradeService implements UpgradeService {
286
314
  const { scriptUrl } = this.#config.linuxScript!;
287
315
  const result = await this.#spawnService!.spawn(["sh", "-c", `curl -fsSL ${scriptUrl} | sh`]);
288
316
  if (!result.ok) {
289
- throw new Error(
290
- `Install script failed: ${result.error?.message ?? `exit code ${result.exitCode}`}`,
291
- );
317
+ throw new Error(`Install script failed: ${describeSpawnFailure(result)}`);
292
318
  }
293
319
  }
294
320
 
@@ -296,9 +322,7 @@ export default class DefaultUpgradeService implements UpgradeService {
296
322
  const { tap, formula } = this.#config.homebrew!;
297
323
  const result = await this.#spawnService!.spawn(["brew", "upgrade", `${tap}/${formula}`]);
298
324
  if (!result.ok) {
299
- throw new Error(
300
- `brew upgrade failed: ${result.error?.message ?? `exit code ${result.exitCode}`}`,
301
- );
325
+ throw new Error(`brew upgrade failed: ${describeSpawnFailure(result)}`);
302
326
  }
303
327
  }
304
328
 
@@ -314,9 +338,7 @@ export default class DefaultUpgradeService implements UpgradeService {
314
338
  "--accept-source-agreements",
315
339
  ]);
316
340
  if (!result.ok) {
317
- throw new Error(
318
- `winget upgrade failed: ${result.error?.message ?? `exit code ${result.exitCode}`}`,
319
- );
341
+ throw new Error(`winget upgrade failed: ${describeSpawnFailure(result)}`);
320
342
  }
321
343
  }
322
344
 
@@ -327,8 +349,7 @@ export default class DefaultUpgradeService implements UpgradeService {
327
349
  const assetName = assetPattern.replace("{os}", OS_LABELS[os]).replace("{arch}", archLabel);
328
350
  const url = `https://github.com/${owner}/${repo}/releases/latest/download/${assetName}`;
329
351
 
330
- // Downloading the release asset happens outside SpawnService (which enters long-running mode
331
- // for its own subprocesses by default), so bracket it here to get the same cooperative
352
+ // Downloading the release asset but enter long-running mode to get cooperative
332
353
  // Ctrl-C handling during what can be the slowest step of the upgrade.
333
354
  this.#shutdownService?.enterLongRunningMode();
334
355
  let archiveData: ArrayBuffer;
@@ -54,6 +54,8 @@ export default class UpgradeServiceProvider implements ServiceProvider {
54
54
  logger.debug(() => "SpawnService not available, upgrade install methods will be unavailable");
55
55
  }
56
56
 
57
+ void upgradeService.getUpgradeCheckResult();
58
+
57
59
  if (!context.doesServiceExist(PROMPTER_SERVICE_ID)) {
58
60
  logger.debug(() => "PrompterService not available, skipping auto-upgrade");
59
61
  return;
@@ -128,7 +130,7 @@ export default class UpgradeServiceProvider implements ServiceProvider {
128
130
  cliConfig: CLIConfig,
129
131
  ): Promise<void> {
130
132
  try {
131
- const checkResult = await upgradeService.checkForUpgrade();
133
+ const checkResult = await upgradeService.getUpgradeCheckResult();
132
134
  if (!checkResult?.updateAvailable) {
133
135
  return;
134
136
  }
@@ -52,7 +52,10 @@ export class UpgradeSubCommand implements SubCommand {
52
52
  const arch = undefined as SupportedArch | undefined;
53
53
  const installMethod = argumentValues["install-method"] as InstallMethod | undefined;
54
54
 
55
- const checkResult = await this.#upgradeService.checkForUpgrade(os, arch, installMethod);
55
+ const checkResult =
56
+ os === undefined && installMethod === undefined
57
+ ? await this.#upgradeService.getUpgradeCheckResult(true)
58
+ : await this.#upgradeService.checkForUpgrade(os, arch, installMethod);
56
59
  if (!checkResult) {
57
60
  await printerService.error(
58
61
  `No upgrade location is configured for the detected or requested platform.\n`,