@flowscripter/dynamic-cli-framework 2.3.1 → 2.3.3

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,10 +1,10 @@
1
- import type { CLIConfig, SpawnService, UpgradeCheckResult, UpgradeResult } from "@flowscripter/dynamic-cli-framework-api";
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
4
  export default class DefaultUpgradeService implements UpgradeService {
5
5
  #private;
6
6
  constructor(config: UpgradeLocationsConfig, cliConfig: CLIConfig);
7
- setDependencies(spawnService: SpawnService): void;
7
+ setDependencies(spawnService: SpawnService, shutdownService?: ShutdownService): void;
8
8
  detectOs(): SupportedOs | undefined;
9
9
  detectArch(): SupportedArch | undefined;
10
10
  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,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;AAW1E,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,cAAc;;gBAK/C,MAAM,EAAE,sBAAsB,EAAE,SAAS,EAAE,SAAS;IAKhE,eAAe,CAAC,YAAY,EAAE,YAAY,GAAG,IAAI;IAIjD,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;CA6P1B"}
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;AA0B1E,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;CAiR1B"}
@@ -6,6 +6,19 @@ import { InstallMethod, SupportedArch, SupportedOs, } from "@flowscripter/dynami
6
6
  import semver from "semver";
7
7
  import getLogger from "../../util/logger.js";
8
8
  const logger = getLogger("DefaultUpgradeService");
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;
12
+ // SpawnService has no built-in cancellation, so a stalled `brew`/`winget` invocation (e.g. winget's
13
+ // first-ever run on a machine prompting to accept Microsoft Store terms) can otherwise hang
14
+ // checkForUpgrade() indefinitely. Racing against this bounds how long we wait for a result; the
15
+ // underlying process is not killed, but the CLI stops blocking on it.
16
+ async function withSpawnTimeout(spawnPromise, fallback) {
17
+ return Promise.race([
18
+ spawnPromise,
19
+ new Promise((resolve) => setTimeout(() => resolve(fallback), VERSION_CHECK_TIMEOUT_MS)),
20
+ ]);
21
+ }
9
22
  const OS_LABELS = {
10
23
  [SupportedOs.LINUX]: "Linux",
11
24
  [SupportedOs.MACOS]: "MacOS",
@@ -13,14 +26,16 @@ const OS_LABELS = {
13
26
  };
14
27
  export default class DefaultUpgradeService {
15
28
  #spawnService;
29
+ #shutdownService;
16
30
  #config;
17
31
  #cliConfig;
18
32
  constructor(config, cliConfig) {
19
33
  this.#config = config;
20
34
  this.#cliConfig = cliConfig;
21
35
  }
22
- setDependencies(spawnService) {
36
+ setDependencies(spawnService, shutdownService) {
23
37
  this.#spawnService = spawnService;
38
+ this.#shutdownService = shutdownService;
24
39
  }
25
40
  detectOs() {
26
41
  switch (process.platform) {
@@ -130,14 +145,20 @@ export default class DefaultUpgradeService {
130
145
  if (!this.#spawnService || !this.#config.homebrew) {
131
146
  return false;
132
147
  }
133
- const result = await this.#spawnService.spawn(["brew", "list", "--versions", this.#config.homebrew.formula], { stdio: "wrapped", longRunning: false });
148
+ const result = await withSpawnTimeout(this.#spawnService.spawn(["brew", "list", "--versions", this.#config.homebrew.formula], {
149
+ stdio: "wrapped",
150
+ longRunning: false,
151
+ }), { ok: false });
134
152
  return result.ok;
135
153
  }
136
154
  async #isWingetInstalled() {
137
155
  if (!this.#spawnService || !this.#config.winget) {
138
156
  return false;
139
157
  }
140
- const result = await this.#spawnService.spawn(["winget", "list", "--id", this.#config.winget.packageId], { stdio: "wrapped", longRunning: false });
158
+ const result = await withSpawnTimeout(this.#spawnService.spawn(["winget", "list", "--id", this.#config.winget.packageId], {
159
+ stdio: "wrapped",
160
+ longRunning: false,
161
+ }), { ok: false });
141
162
  return result.ok;
142
163
  }
143
164
  #isLinuxScriptInstall() {
@@ -162,7 +183,7 @@ export default class DefaultUpgradeService {
162
183
  }
163
184
  const { owner, repo } = this.#config.githubRelease;
164
185
  try {
165
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`);
186
+ const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`, { signal: AbortSignal.timeout(VERSION_CHECK_TIMEOUT_MS) });
166
187
  if (!response.ok) {
167
188
  return undefined;
168
189
  }
@@ -184,7 +205,7 @@ export default class DefaultUpgradeService {
184
205
  return undefined;
185
206
  }
186
207
  try {
187
- const response = await fetch(`https://raw.githubusercontent.com/${tapOwner}/homebrew-${tapName}/main/${formula}.rb`);
208
+ const response = await fetch(`https://raw.githubusercontent.com/${tapOwner}/homebrew-${tapName}/main/${formula}.rb`, { signal: AbortSignal.timeout(VERSION_CHECK_TIMEOUT_MS) });
188
209
  if (!response.ok) {
189
210
  return undefined;
190
211
  }
@@ -201,11 +222,11 @@ export default class DefaultUpgradeService {
201
222
  return undefined;
202
223
  }
203
224
  const lines = [];
204
- const result = await this.#spawnService.spawn(["winget", "show", "--id", this.#config.winget.packageId], {
225
+ const result = await withSpawnTimeout(this.#spawnService.spawn(["winget", "show", "--id", this.#config.winget.packageId], {
205
226
  stdio: "wrapped",
206
227
  longRunning: false,
207
228
  onOutput: (line) => lines.push(line),
208
- });
229
+ }), { ok: false });
209
230
  if (!result.ok) {
210
231
  return undefined;
211
232
  }
@@ -251,13 +272,24 @@ export default class DefaultUpgradeService {
251
272
  const archLabel = os === SupportedOs.MACOS ? "aarch64" : arch === SupportedArch.X64 ? "x64" : "arm64";
252
273
  const assetName = assetPattern.replace("{os}", OS_LABELS[os]).replace("{arch}", archLabel);
253
274
  const url = `https://github.com/${owner}/${repo}/releases/latest/download/${assetName}`;
254
- const response = await fetch(url);
255
- if (!response.ok) {
256
- throw new Error(`Failed to download release asset '${assetName}': HTTP ${response.status}`);
275
+ // Downloading the release asset happens outside SpawnService (which enters long-running mode
276
+ // for its own subprocesses by default), so bracket it here to get the same cooperative
277
+ // Ctrl-C handling during what can be the slowest step of the upgrade.
278
+ this.#shutdownService?.enterLongRunningMode();
279
+ let archiveData;
280
+ try {
281
+ const response = await fetch(url);
282
+ if (!response.ok) {
283
+ throw new Error(`Failed to download release asset '${assetName}': HTTP ${response.status}`);
284
+ }
285
+ archiveData = await response.arrayBuffer();
286
+ }
287
+ finally {
288
+ this.#shutdownService?.leaveLongRunningMode();
257
289
  }
258
290
  const tmpDir = await mkdtemp(join(tmpdir(), "upgrade-"));
259
291
  const archivePath = join(tmpDir, assetName);
260
- await Bun.write(archivePath, await response.arrayBuffer());
292
+ await Bun.write(archivePath, archiveData);
261
293
  const currentExecutable = process.execPath;
262
294
  if (os === SupportedOs.WINDOWS) {
263
295
  const extractResult = await this.#spawnService.spawn([
@@ -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;AAYzE,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;CA6G1D"}
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"}
@@ -2,6 +2,7 @@ import { UPGRADE_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
2
2
  import { PROMPTER_SERVICE_ID, PromptType } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import { KEY_VALUE_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import { SPAWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
5
+ import { SHUTDOWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
5
6
  import { Icon, PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
6
7
  import DefaultUpgradeService from "./DefaultUpgradeService.js";
7
8
  import { UpgradeSubCommand } from "./command/UpgradeSubCommand.js";
@@ -30,7 +31,10 @@ export default class UpgradeServiceProvider {
30
31
  const cliConfig = this.#cliConfig;
31
32
  if (context.doesServiceExist(SPAWN_SERVICE_ID)) {
32
33
  const spawnService = context.getServiceById(SPAWN_SERVICE_ID);
33
- upgradeService.setDependencies(spawnService);
34
+ const shutdownService = context.doesServiceExist(SHUTDOWN_SERVICE_ID)
35
+ ? context.getServiceById(SHUTDOWN_SERVICE_ID)
36
+ : undefined;
37
+ upgradeService.setDependencies(spawnService, shutdownService);
34
38
  }
35
39
  else {
36
40
  logger.debug(() => "SpawnService not available, upgrade install methods will be unavailable");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "2.3.1",
3
+ "version": "2.3.3",
4
4
  "description": "A framework for developing CLI applications which supports dynamic discovery and installation of new commands",
5
5
  "keywords": [
6
6
  "bun",
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import type {
6
6
  CLIConfig,
7
+ ShutdownService,
7
8
  SpawnService,
8
9
  UpgradeCheckResult,
9
10
  UpgradeResult,
@@ -20,6 +21,21 @@ import getLogger from "../../util/logger.ts";
20
21
 
21
22
  const logger = getLogger("DefaultUpgradeService");
22
23
 
24
+ // 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
+ ]);
37
+ }
38
+
23
39
  const OS_LABELS: Record<SupportedOs, string> = {
24
40
  [SupportedOs.LINUX]: "Linux",
25
41
  [SupportedOs.MACOS]: "MacOS",
@@ -28,6 +44,7 @@ const OS_LABELS: Record<SupportedOs, string> = {
28
44
 
29
45
  export default class DefaultUpgradeService implements UpgradeService {
30
46
  #spawnService: SpawnService | undefined;
47
+ #shutdownService: ShutdownService | undefined;
31
48
  readonly #config: UpgradeLocationsConfig;
32
49
  readonly #cliConfig: CLIConfig;
33
50
 
@@ -36,8 +53,9 @@ export default class DefaultUpgradeService implements UpgradeService {
36
53
  this.#cliConfig = cliConfig;
37
54
  }
38
55
 
39
- public setDependencies(spawnService: SpawnService): void {
56
+ public setDependencies(spawnService: SpawnService, shutdownService?: ShutdownService): void {
40
57
  this.#spawnService = spawnService;
58
+ this.#shutdownService = shutdownService;
41
59
  }
42
60
 
43
61
  public detectOs(): SupportedOs | undefined {
@@ -168,9 +186,12 @@ export default class DefaultUpgradeService implements UpgradeService {
168
186
  if (!this.#spawnService || !this.#config.homebrew) {
169
187
  return false;
170
188
  }
171
- const result = await this.#spawnService.spawn(
172
- ["brew", "list", "--versions", this.#config.homebrew.formula],
173
- { stdio: "wrapped", longRunning: false },
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,
174
195
  );
175
196
  return result.ok;
176
197
  }
@@ -179,9 +200,12 @@ export default class DefaultUpgradeService implements UpgradeService {
179
200
  if (!this.#spawnService || !this.#config.winget) {
180
201
  return false;
181
202
  }
182
- const result = await this.#spawnService.spawn(
183
- ["winget", "list", "--id", this.#config.winget.packageId],
184
- { stdio: "wrapped", longRunning: false },
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,
185
209
  );
186
210
  return result.ok;
187
211
  }
@@ -210,7 +234,10 @@ export default class DefaultUpgradeService implements UpgradeService {
210
234
  }
211
235
  const { owner, repo } = this.#config.githubRelease;
212
236
  try {
213
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`);
237
+ const response = await fetch(
238
+ `https://api.github.com/repos/${owner}/${repo}/releases/latest`,
239
+ { signal: AbortSignal.timeout(VERSION_CHECK_TIMEOUT_MS) },
240
+ );
214
241
  if (!response.ok) {
215
242
  return undefined;
216
243
  }
@@ -234,6 +261,7 @@ export default class DefaultUpgradeService implements UpgradeService {
234
261
  try {
235
262
  const response = await fetch(
236
263
  `https://raw.githubusercontent.com/${tapOwner}/homebrew-${tapName}/main/${formula}.rb`,
264
+ { signal: AbortSignal.timeout(VERSION_CHECK_TIMEOUT_MS) },
237
265
  );
238
266
  if (!response.ok) {
239
267
  return undefined;
@@ -251,13 +279,13 @@ export default class DefaultUpgradeService implements UpgradeService {
251
279
  return undefined;
252
280
  }
253
281
  const lines: string[] = [];
254
- const result = await this.#spawnService.spawn(
255
- ["winget", "show", "--id", this.#config.winget.packageId],
256
- {
282
+ const result = await withSpawnTimeout(
283
+ this.#spawnService.spawn(["winget", "show", "--id", this.#config.winget.packageId], {
257
284
  stdio: "wrapped",
258
285
  longRunning: false,
259
286
  onOutput: (line) => lines.push(line),
260
- },
287
+ }),
288
+ { ok: false } as const,
261
289
  );
262
290
  if (!result.ok) {
263
291
  return undefined;
@@ -316,14 +344,24 @@ export default class DefaultUpgradeService implements UpgradeService {
316
344
  const assetName = assetPattern.replace("{os}", OS_LABELS[os]).replace("{arch}", archLabel);
317
345
  const url = `https://github.com/${owner}/${repo}/releases/latest/download/${assetName}`;
318
346
 
319
- const response = await fetch(url);
320
- if (!response.ok) {
321
- throw new Error(`Failed to download release asset '${assetName}': HTTP ${response.status}`);
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();
322
360
  }
323
361
 
324
362
  const tmpDir = await mkdtemp(join(tmpdir(), "upgrade-"));
325
363
  const archivePath = join(tmpDir, assetName);
326
- await Bun.write(archivePath, await response.arrayBuffer());
364
+ await Bun.write(archivePath, archiveData);
327
365
 
328
366
  const currentExecutable = process.execPath;
329
367
 
@@ -8,6 +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
13
  import { Icon, PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
12
14
  import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
13
15
  import DefaultUpgradeService from "./DefaultUpgradeService.ts";
@@ -44,7 +46,10 @@ export default class UpgradeServiceProvider implements ServiceProvider {
44
46
 
45
47
  if (context.doesServiceExist(SPAWN_SERVICE_ID)) {
46
48
  const spawnService = context.getServiceById(SPAWN_SERVICE_ID) as SpawnService;
47
- upgradeService.setDependencies(spawnService);
49
+ const shutdownService = context.doesServiceExist(SHUTDOWN_SERVICE_ID)
50
+ ? (context.getServiceById(SHUTDOWN_SERVICE_ID) as ShutdownService)
51
+ : undefined;
52
+ upgradeService.setDependencies(spawnService, shutdownService);
48
53
  } else {
49
54
  logger.debug(() => "SpawnService not available, upgrade install methods will be unavailable");
50
55
  }