@flowscripter/dynamic-cli-framework 2.3.3 → 2.5.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.
Files changed (48) hide show
  1. package/dist/index.d.ts +4 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -0
  4. package/dist/src/cli/BaseCLI.d.ts +1 -0
  5. package/dist/src/cli/BaseCLI.d.ts.map +1 -1
  6. package/dist/src/cli/BaseCLI.js +6 -0
  7. package/dist/src/cli/BaseCLIFeatureOptions.d.ts +1 -0
  8. package/dist/src/cli/BaseCLIFeatureOptions.d.ts.map +1 -1
  9. package/dist/src/command/VersionCommand.js +1 -1
  10. package/dist/src/service/banner/BannerServiceProvider.js +1 -1
  11. package/dist/src/service/fetch/DefaultFetchService.d.ts +7 -0
  12. package/dist/src/service/fetch/DefaultFetchService.d.ts.map +1 -0
  13. package/dist/src/service/fetch/DefaultFetchService.js +44 -0
  14. package/dist/src/service/fetch/FetchServiceProvider.d.ts +10 -0
  15. package/dist/src/service/fetch/FetchServiceProvider.d.ts.map +1 -0
  16. package/dist/src/service/fetch/FetchServiceProvider.js +22 -0
  17. package/dist/src/service/plugin/DefaultPluginServiceProvider.d.ts +2 -1
  18. package/dist/src/service/plugin/DefaultPluginServiceProvider.d.ts.map +1 -1
  19. package/dist/src/service/plugin/DefaultPluginServiceProvider.js +23 -8
  20. package/dist/src/service/plugin/FetchInterfaceAdapter.d.ts +17 -0
  21. package/dist/src/service/plugin/FetchInterfaceAdapter.d.ts.map +1 -0
  22. package/dist/src/service/plugin/FetchInterfaceAdapter.js +16 -0
  23. package/dist/src/service/plugin/SpawnInterfaceAdapter.d.ts.map +1 -1
  24. package/dist/src/service/plugin/SpawnInterfaceAdapter.js +6 -3
  25. package/dist/src/service/spawn/DefaultSpawnService.d.ts.map +1 -1
  26. package/dist/src/service/spawn/DefaultSpawnService.js +38 -16
  27. package/dist/src/service/upgrade/DefaultUpgradeService.d.ts +4 -2
  28. package/dist/src/service/upgrade/DefaultUpgradeService.d.ts.map +1 -1
  29. package/dist/src/service/upgrade/DefaultUpgradeService.js +62 -75
  30. package/dist/src/service/upgrade/UpgradeServiceProvider.d.ts.map +1 -1
  31. package/dist/src/service/upgrade/UpgradeServiceProvider.js +14 -10
  32. package/dist/src/service/upgrade/command/UpgradeSubCommand.d.ts.map +1 -1
  33. package/dist/src/service/upgrade/command/UpgradeSubCommand.js +3 -1
  34. package/index.ts +5 -0
  35. package/package.json +3 -3
  36. package/src/cli/BaseCLI.ts +7 -0
  37. package/src/cli/BaseCLIFeatureOptions.ts +1 -0
  38. package/src/command/VersionCommand.ts +1 -1
  39. package/src/service/banner/BannerServiceProvider.ts +1 -1
  40. package/src/service/fetch/DefaultFetchService.ts +55 -0
  41. package/src/service/fetch/FetchServiceProvider.ts +33 -0
  42. package/src/service/plugin/DefaultPluginServiceProvider.ts +29 -12
  43. package/src/service/plugin/FetchInterfaceAdapter.ts +28 -0
  44. package/src/service/plugin/SpawnInterfaceAdapter.ts +6 -3
  45. package/src/service/spawn/DefaultSpawnService.ts +45 -16
  46. package/src/service/upgrade/DefaultUpgradeService.ts +107 -102
  47. package/src/service/upgrade/UpgradeServiceProvider.ts +16 -10
  48. package/src/service/upgrade/command/UpgradeSubCommand.ts +4 -1
@@ -45,7 +45,7 @@ export default class SpawnInterfaceAdapter implements SpawnInterface {
45
45
 
46
46
  const result = await this.#spawnService.spawn(command, {
47
47
  cwd: options.cwd,
48
- stdio: "wrapped",
48
+ mode: "wrapped",
49
49
  onOutput,
50
50
  });
51
51
 
@@ -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",
@@ -79,10 +95,10 @@ export default class DefaultSpawnService implements SpawnService {
79
95
  }
80
96
  const printerService = this.#printerService;
81
97
  const shutdownService = this.#shutdownService;
82
- const stdio = options.stdio ?? "inherit";
98
+ const mode = options.mode ?? "inherit";
83
99
  const longRunning = options.longRunning ?? true;
84
100
 
85
- if (stdio === "inherit") {
101
+ if (mode === "inherit") {
86
102
  await printerService.hideSpinner();
87
103
  await printerService.hideAllProgressBars();
88
104
  }
@@ -91,9 +107,9 @@ export default class DefaultSpawnService implements SpawnService {
91
107
  try {
92
108
  proc = Bun.spawn(resolveForPlatform(command), {
93
109
  cwd: options.cwd,
94
- stdin: stdio === "wrapped" ? "ignore" : "inherit",
95
- stdout: stdio === "wrapped" ? "pipe" : "inherit",
96
- stderr: stdio === "wrapped" ? "pipe" : "inherit",
110
+ stdin: mode === "inherit" ? "inherit" : "ignore",
111
+ stdout: mode === "wrapped" ? "pipe" : mode === "ignore" ? "ignore" : "inherit",
112
+ stderr: mode === "wrapped" ? "pipe" : mode === "ignore" ? "ignore" : "inherit",
97
113
  });
98
114
  } catch (error) {
99
115
  logger.debug(() => `Failed to launch command '${command.join(" ")}': ${error}`);
@@ -110,25 +126,38 @@ 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
- if (stdio === "wrapped" && options.onOutput) {
132
+ if (mode === "wrapped" && options.onOutput) {
126
133
  const onOutput = options.onOutput;
127
134
  void this.#pipeLines(proc.stdout, "stdout", onOutput);
128
135
  void this.#pipeLines(proc.stderr, "stderr", onOutput);
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 };
@@ -4,7 +4,8 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import type {
6
6
  CLIConfig,
7
- ShutdownService,
7
+ FetchService,
8
+ SpawnResult,
8
9
  SpawnService,
9
10
  UpgradeCheckResult,
10
11
  UpgradeResult,
@@ -22,18 +23,13 @@ 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;
27
-
28
- // SpawnService has no built-in cancellation, so a stalled `brew`/`winget` invocation (e.g. winget's
29
- // first-ever run on a machine prompting to accept Microsoft Store terms) can otherwise hang
30
- // checkForUpgrade() indefinitely. Racing against this bounds how long we wait for a result; the
31
- // underlying process is not killed, but the CLI stops blocking on it.
32
- async function withSpawnTimeout<T>(spawnPromise: Promise<T>, fallback: T): Promise<T> {
33
- return Promise.race([
34
- spawnPromise,
35
- new Promise<T>((resolve) => setTimeout(() => resolve(fallback), VERSION_CHECK_TIMEOUT_MS)),
36
- ]);
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}`);
37
33
  }
38
34
 
39
35
  const OS_LABELS: Record<SupportedOs, string> = {
@@ -44,7 +40,8 @@ const OS_LABELS: Record<SupportedOs, string> = {
44
40
 
45
41
  export default class DefaultUpgradeService implements UpgradeService {
46
42
  #spawnService: SpawnService | undefined;
47
- #shutdownService: ShutdownService | undefined;
43
+ #fetchService: FetchService | undefined;
44
+ #upgradeCheckPromise: Promise<UpgradeCheckResult | undefined> | undefined;
48
45
  readonly #config: UpgradeLocationsConfig;
49
46
  readonly #cliConfig: CLIConfig;
50
47
 
@@ -53,9 +50,27 @@ export default class DefaultUpgradeService implements UpgradeService {
53
50
  this.#cliConfig = cliConfig;
54
51
  }
55
52
 
56
- public setDependencies(spawnService: SpawnService, shutdownService?: ShutdownService): void {
53
+ public setDependencies(
54
+ spawnService: SpawnService | undefined,
55
+ fetchService: FetchService | undefined,
56
+ ): void {
57
57
  this.#spawnService = spawnService;
58
- this.#shutdownService = shutdownService;
58
+ this.#fetchService = fetchService;
59
+ }
60
+
61
+ public getUpgradeCheckResult(waitForResult = false): Promise<UpgradeCheckResult | undefined> {
62
+ if (!this.#upgradeCheckPromise) {
63
+ this.#upgradeCheckPromise = this.checkForUpgrade();
64
+ }
65
+ if (waitForResult) {
66
+ return this.#upgradeCheckPromise;
67
+ }
68
+ return Promise.race([
69
+ this.#upgradeCheckPromise,
70
+ new Promise<undefined>((resolve) =>
71
+ setTimeout(() => resolve(undefined), VERSION_CHECK_TIMEOUT_MS),
72
+ ),
73
+ ]);
59
74
  }
60
75
 
61
76
  public detectOs(): SupportedOs | undefined {
@@ -143,7 +158,11 @@ export default class DefaultUpgradeService implements UpgradeService {
143
158
  installMethodOverride?: InstallMethod,
144
159
  ): Promise<UpgradeResult> {
145
160
  const oldVersion = this.#cliConfig.version;
146
- const checkResult = await this.checkForUpgrade(osOverride, archOverride, installMethodOverride);
161
+ const hasOverride =
162
+ osOverride !== undefined || archOverride !== undefined || installMethodOverride !== undefined;
163
+ const checkResult = hasOverride
164
+ ? await this.checkForUpgrade(osOverride, archOverride, installMethodOverride)
165
+ : await this.getUpgradeCheckResult(true);
147
166
  if (!checkResult) {
148
167
  return {
149
168
  ok: false,
@@ -154,6 +173,9 @@ export default class DefaultUpgradeService implements UpgradeService {
154
173
  if (!this.#spawnService) {
155
174
  return { ok: false, oldVersion, error: new Error("SpawnService is not available") };
156
175
  }
176
+ if (checkResult.installMethod === InstallMethod.GITHUB_RELEASE && !this.#fetchService) {
177
+ return { ok: false, oldVersion, error: new Error("FetchService is not available") };
178
+ }
157
179
 
158
180
  try {
159
181
  switch (checkResult.installMethod) {
@@ -186,12 +208,9 @@ export default class DefaultUpgradeService implements UpgradeService {
186
208
  if (!this.#spawnService || !this.#config.homebrew) {
187
209
  return false;
188
210
  }
189
- const result = await withSpawnTimeout(
190
- this.#spawnService.spawn(["brew", "list", "--versions", this.#config.homebrew.formula], {
191
- stdio: "wrapped",
192
- longRunning: false,
193
- }),
194
- { ok: false } as const,
211
+ const result = await this.#spawnService.spawn(
212
+ ["brew", "list", "--versions", this.#config.homebrew.formula],
213
+ { mode: "ignore", longRunning: false, timeoutMs: VERSION_CHECK_TIMEOUT_MS },
195
214
  );
196
215
  return result.ok;
197
216
  }
@@ -200,12 +219,9 @@ export default class DefaultUpgradeService implements UpgradeService {
200
219
  if (!this.#spawnService || !this.#config.winget) {
201
220
  return false;
202
221
  }
203
- const result = await withSpawnTimeout(
204
- this.#spawnService.spawn(["winget", "list", "--id", this.#config.winget.packageId], {
205
- stdio: "wrapped",
206
- longRunning: false,
207
- }),
208
- { ok: false } as const,
222
+ const result = await this.#spawnService.spawn(
223
+ ["winget", "list", "--id", this.#config.winget.packageId],
224
+ { mode: "ignore", longRunning: false, timeoutMs: VERSION_CHECK_TIMEOUT_MS },
209
225
  );
210
226
  return result.ok;
211
227
  }
@@ -229,14 +245,14 @@ export default class DefaultUpgradeService implements UpgradeService {
229
245
  }
230
246
 
231
247
  async #getLatestGithubReleaseVersion(): Promise<string | undefined> {
232
- if (!this.#config.githubRelease) {
248
+ if (!this.#config.githubRelease || !this.#fetchService) {
233
249
  return undefined;
234
250
  }
235
251
  const { owner, repo } = this.#config.githubRelease;
236
252
  try {
237
- const response = await fetch(
253
+ const response = await this.#fetchService.fetch(
238
254
  `https://api.github.com/repos/${owner}/${repo}/releases/latest`,
239
- { signal: AbortSignal.timeout(VERSION_CHECK_TIMEOUT_MS) },
255
+ { timeoutMs: VERSION_CHECK_TIMEOUT_MS },
240
256
  );
241
257
  if (!response.ok) {
242
258
  return undefined;
@@ -250,7 +266,7 @@ export default class DefaultUpgradeService implements UpgradeService {
250
266
  }
251
267
 
252
268
  async #getLatestHomebrewVersion(): Promise<string | undefined> {
253
- if (!this.#config.homebrew) {
269
+ if (!this.#config.homebrew || !this.#fetchService) {
254
270
  return undefined;
255
271
  }
256
272
  const { tap, formula } = this.#config.homebrew;
@@ -259,9 +275,9 @@ export default class DefaultUpgradeService implements UpgradeService {
259
275
  return undefined;
260
276
  }
261
277
  try {
262
- const response = await fetch(
278
+ const response = await this.#fetchService.fetch(
263
279
  `https://raw.githubusercontent.com/${tapOwner}/homebrew-${tapName}/main/${formula}.rb`,
264
- { signal: AbortSignal.timeout(VERSION_CHECK_TIMEOUT_MS) },
280
+ { timeoutMs: VERSION_CHECK_TIMEOUT_MS },
265
281
  );
266
282
  if (!response.ok) {
267
283
  return undefined;
@@ -279,13 +295,14 @@ export default class DefaultUpgradeService implements UpgradeService {
279
295
  return undefined;
280
296
  }
281
297
  const lines: string[] = [];
282
- const result = await withSpawnTimeout(
283
- this.#spawnService.spawn(["winget", "show", "--id", this.#config.winget.packageId], {
284
- stdio: "wrapped",
298
+ const result = await this.#spawnService.spawn(
299
+ ["winget", "show", "--id", this.#config.winget.packageId],
300
+ {
301
+ mode: "wrapped",
285
302
  longRunning: false,
303
+ timeoutMs: VERSION_CHECK_TIMEOUT_MS,
286
304
  onOutput: (line) => lines.push(line),
287
- }),
288
- { ok: false } as const,
305
+ },
289
306
  );
290
307
  if (!result.ok) {
291
308
  return undefined;
@@ -301,39 +318,40 @@ export default class DefaultUpgradeService implements UpgradeService {
301
318
 
302
319
  async #upgradeViaLinuxScript(): Promise<void> {
303
320
  const { scriptUrl } = this.#config.linuxScript!;
304
- const result = await this.#spawnService!.spawn(["sh", "-c", `curl -fsSL ${scriptUrl} | sh`]);
321
+ const result = await this.#spawnService!.spawn(["sh", "-c", `curl -fsSL ${scriptUrl} | sh`], {
322
+ mode: "ignore",
323
+ });
305
324
  if (!result.ok) {
306
- throw new Error(
307
- `Install script failed: ${result.error?.message ?? `exit code ${result.exitCode}`}`,
308
- );
325
+ throw new Error(`Install script failed: ${describeSpawnFailure(result)}`);
309
326
  }
310
327
  }
311
328
 
312
329
  async #upgradeViaHomebrew(): Promise<void> {
313
330
  const { tap, formula } = this.#config.homebrew!;
314
- const result = await this.#spawnService!.spawn(["brew", "upgrade", `${tap}/${formula}`]);
331
+ const result = await this.#spawnService!.spawn(["brew", "upgrade", `${tap}/${formula}`], {
332
+ mode: "ignore",
333
+ });
315
334
  if (!result.ok) {
316
- throw new Error(
317
- `brew upgrade failed: ${result.error?.message ?? `exit code ${result.exitCode}`}`,
318
- );
335
+ throw new Error(`brew upgrade failed: ${describeSpawnFailure(result)}`);
319
336
  }
320
337
  }
321
338
 
322
339
  async #upgradeViaWinget(): Promise<void> {
323
340
  const { packageId } = this.#config.winget!;
324
- const result = await this.#spawnService!.spawn([
325
- "winget",
326
- "upgrade",
327
- "--id",
328
- packageId,
329
- "--silent",
330
- "--accept-package-agreements",
331
- "--accept-source-agreements",
332
- ]);
341
+ const result = await this.#spawnService!.spawn(
342
+ [
343
+ "winget",
344
+ "upgrade",
345
+ "--id",
346
+ packageId,
347
+ "--silent",
348
+ "--accept-package-agreements",
349
+ "--accept-source-agreements",
350
+ ],
351
+ { mode: "ignore" },
352
+ );
333
353
  if (!result.ok) {
334
- throw new Error(
335
- `winget upgrade failed: ${result.error?.message ?? `exit code ${result.exitCode}`}`,
336
- );
354
+ throw new Error(`winget upgrade failed: ${describeSpawnFailure(result)}`);
337
355
  }
338
356
  }
339
357
 
@@ -344,20 +362,13 @@ export default class DefaultUpgradeService implements UpgradeService {
344
362
  const assetName = assetPattern.replace("{os}", OS_LABELS[os]).replace("{arch}", archLabel);
345
363
  const url = `https://github.com/${owner}/${repo}/releases/latest/download/${assetName}`;
346
364
 
347
- // Downloading the release asset happens outside SpawnService (which enters long-running mode
348
- // for its own subprocesses by default), so bracket it here to get the same cooperative
349
- // Ctrl-C handling during what can be the slowest step of the upgrade.
350
- this.#shutdownService?.enterLongRunningMode();
351
- let archiveData: ArrayBuffer;
352
- try {
353
- const response = await fetch(url);
354
- if (!response.ok) {
355
- throw new Error(`Failed to download release asset '${assetName}': HTTP ${response.status}`);
356
- }
357
- archiveData = await response.arrayBuffer();
358
- } finally {
359
- this.#shutdownService?.leaveLongRunningMode();
365
+ // longRunning: true gets cooperative Ctrl-C handling during what can be the slowest step of
366
+ // the upgrade.
367
+ const response = await this.#fetchService!.fetch(url, { longRunning: true });
368
+ if (!response.ok) {
369
+ throw new Error(`Failed to download release asset '${assetName}': HTTP ${response.status}`);
360
370
  }
371
+ const archiveData = await response.arrayBuffer();
361
372
 
362
373
  const tmpDir = await mkdtemp(join(tmpdir(), "upgrade-"));
363
374
  const archivePath = join(tmpDir, assetName);
@@ -366,52 +377,46 @@ export default class DefaultUpgradeService implements UpgradeService {
366
377
  const currentExecutable = process.execPath;
367
378
 
368
379
  if (os === SupportedOs.WINDOWS) {
369
- const extractResult = await this.#spawnService!.spawn([
370
- "powershell",
371
- "-Command",
372
- `Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpDir}' -Force`,
373
- ]);
380
+ const extractResult = await this.#spawnService!.spawn(
381
+ [
382
+ "powershell",
383
+ "-Command",
384
+ `Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpDir}' -Force`,
385
+ ],
386
+ { mode: "ignore" },
387
+ );
374
388
  if (!extractResult.ok) {
375
389
  throw new Error("Failed to extract release archive");
376
390
  }
377
391
  const extractedBinary = join(tmpDir, `${this.#cliConfig.name}.exe`);
378
392
  const oldPath = `${currentExecutable}.old.exe`;
379
- const moveResult = await this.#spawnService!.spawn([
380
- "cmd",
381
- "/c",
382
- "move",
383
- "/y",
384
- currentExecutable,
385
- oldPath,
386
- ]);
393
+ const moveResult = await this.#spawnService!.spawn(
394
+ ["cmd", "/c", "move", "/y", currentExecutable, oldPath],
395
+ { mode: "ignore" },
396
+ );
387
397
  if (!moveResult.ok) {
388
398
  throw new Error("Failed to move current executable aside");
389
399
  }
390
- const copyResult = await this.#spawnService!.spawn([
391
- "cmd",
392
- "/c",
393
- "copy",
394
- "/y",
395
- extractedBinary,
396
- currentExecutable,
397
- ]);
400
+ const copyResult = await this.#spawnService!.spawn(
401
+ ["cmd", "/c", "copy", "/y", extractedBinary, currentExecutable],
402
+ { mode: "ignore" },
403
+ );
398
404
  if (!copyResult.ok) {
399
405
  throw new Error("Failed to copy new executable into place");
400
406
  }
401
407
  } else {
402
- const extractResult = await this.#spawnService!.spawn([
403
- "unzip",
404
- "-o",
405
- archivePath,
406
- "-d",
407
- tmpDir,
408
- ]);
408
+ const extractResult = await this.#spawnService!.spawn(
409
+ ["unzip", "-o", archivePath, "-d", tmpDir],
410
+ {
411
+ mode: "ignore",
412
+ },
413
+ );
409
414
  if (!extractResult.ok) {
410
415
  throw new Error("Failed to extract release archive");
411
416
  }
412
417
  const extractedBinary = join(tmpDir, this.#cliConfig.name);
413
418
  await Bun.write(currentExecutable, Bun.file(extractedBinary));
414
- await this.#spawnService!.spawn(["chmod", "+x", currentExecutable]);
419
+ await this.#spawnService!.spawn(["chmod", "+x", currentExecutable], { mode: "ignore" });
415
420
  }
416
421
  }
417
422
  }
@@ -8,8 +8,8 @@ import { KEY_VALUE_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
8
8
  import type { KeyValueService } from "@flowscripter/dynamic-cli-framework-api";
9
9
  import { SPAWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
10
10
  import type { SpawnService } from "@flowscripter/dynamic-cli-framework-api";
11
- import { SHUTDOWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
12
- import type { ShutdownService } from "@flowscripter/dynamic-cli-framework-api";
11
+ import { FETCH_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
12
+ import type { FetchService } from "@flowscripter/dynamic-cli-framework-api";
13
13
  import { Icon, PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
14
14
  import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
15
15
  import DefaultUpgradeService from "./DefaultUpgradeService.ts";
@@ -44,15 +44,21 @@ export default class UpgradeServiceProvider implements ServiceProvider {
44
44
  const upgradeService = this.#upgradeService!;
45
45
  const cliConfig = this.#cliConfig!;
46
46
 
47
- if (context.doesServiceExist(SPAWN_SERVICE_ID)) {
48
- const spawnService = context.getServiceById(SPAWN_SERVICE_ID) as SpawnService;
49
- const shutdownService = context.doesServiceExist(SHUTDOWN_SERVICE_ID)
50
- ? (context.getServiceById(SHUTDOWN_SERVICE_ID) as ShutdownService)
51
- : undefined;
52
- upgradeService.setDependencies(spawnService, shutdownService);
53
- } else {
47
+ const spawnService = context.doesServiceExist(SPAWN_SERVICE_ID)
48
+ ? (context.getServiceById(SPAWN_SERVICE_ID) as SpawnService)
49
+ : undefined;
50
+ const fetchService = context.doesServiceExist(FETCH_SERVICE_ID)
51
+ ? (context.getServiceById(FETCH_SERVICE_ID) as FetchService)
52
+ : undefined;
53
+ if (spawnService === undefined) {
54
54
  logger.debug(() => "SpawnService not available, upgrade install methods will be unavailable");
55
55
  }
56
+ if (fetchService === undefined) {
57
+ logger.debug(() => "FetchService not available, upgrade version checks will be unavailable");
58
+ }
59
+ upgradeService.setDependencies(spawnService, fetchService);
60
+
61
+ void upgradeService.getUpgradeCheckResult();
56
62
 
57
63
  if (!context.doesServiceExist(PROMPTER_SERVICE_ID)) {
58
64
  logger.debug(() => "PrompterService not available, skipping auto-upgrade");
@@ -128,7 +134,7 @@ export default class UpgradeServiceProvider implements ServiceProvider {
128
134
  cliConfig: CLIConfig,
129
135
  ): Promise<void> {
130
136
  try {
131
- const checkResult = await upgradeService.checkForUpgrade();
137
+ const checkResult = await upgradeService.getUpgradeCheckResult();
132
138
  if (!checkResult?.updateAvailable) {
133
139
  return;
134
140
  }
@@ -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`,