@flowscripter/dynamic-cli-framework 7.0.10 → 7.1.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 (52) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -0
  4. package/dist/src/cli/BaseCLI.d.ts.map +1 -1
  5. package/dist/src/cli/BaseCLI.js +24 -22
  6. package/dist/src/cli/BaseCLIFeatureOptions.d.ts +5 -0
  7. package/dist/src/cli/BaseCLIFeatureOptions.d.ts.map +1 -1
  8. package/dist/src/cli/DefaultRuntimeCLI.js +1 -1
  9. package/dist/src/runtime/lifecycle/priorities.d.ts +50 -0
  10. package/dist/src/runtime/lifecycle/priorities.d.ts.map +1 -0
  11. package/dist/src/runtime/lifecycle/priorities.js +49 -0
  12. package/dist/src/runtime/runner.d.ts +10 -1
  13. package/dist/src/runtime/runner.d.ts.map +1 -1
  14. package/dist/src/runtime/runner.js +7 -1
  15. package/dist/src/service/configuration/DefaultKeyValueService.d.ts +9 -1
  16. package/dist/src/service/configuration/DefaultKeyValueService.d.ts.map +1 -1
  17. package/dist/src/service/configuration/DefaultKeyValueService.js +16 -1
  18. package/dist/src/service/configuration/KeyValueServiceProvider.d.ts.map +1 -1
  19. package/dist/src/service/configuration/KeyValueServiceProvider.js +15 -9
  20. package/dist/src/service/fetch/DefaultFetchService.d.ts.map +1 -1
  21. package/dist/src/service/fetch/DefaultFetchService.js +2 -1
  22. package/dist/src/service/printer/PrinterServiceProvider.d.ts.map +1 -1
  23. package/dist/src/service/printer/PrinterServiceProvider.js +2 -1
  24. package/dist/src/service/shutdown/ShutdownServiceProvider.js +3 -3
  25. package/dist/src/service/spawn/DefaultSpawnService.d.ts.map +1 -1
  26. package/dist/src/service/spawn/DefaultSpawnService.js +3 -1
  27. package/dist/src/service/upgrade/DefaultUpgradeService.d.ts +30 -2
  28. package/dist/src/service/upgrade/DefaultUpgradeService.d.ts.map +1 -1
  29. package/dist/src/service/upgrade/DefaultUpgradeService.js +99 -6
  30. package/dist/src/service/upgrade/UpgradeServiceProvider.d.ts +32 -2
  31. package/dist/src/service/upgrade/UpgradeServiceProvider.d.ts.map +1 -1
  32. package/dist/src/service/upgrade/UpgradeServiceProvider.js +82 -14
  33. package/dist/src/startup/banner/bannerStartupTask.d.ts +5 -5
  34. package/dist/src/startup/banner/bannerStartupTask.d.ts.map +1 -1
  35. package/dist/src/startup/banner/bannerStartupTask.js +14 -10
  36. package/index.ts +20 -0
  37. package/package.json +2 -2
  38. package/src/cli/BaseCLI.ts +67 -21
  39. package/src/cli/BaseCLIFeatureOptions.ts +5 -0
  40. package/src/cli/DefaultRuntimeCLI.ts +1 -1
  41. package/src/runtime/lifecycle/priorities.ts +67 -0
  42. package/src/runtime/runner.ts +17 -2
  43. package/src/service/configuration/DefaultKeyValueService.ts +22 -1
  44. package/src/service/configuration/KeyValueServiceProvider.ts +20 -9
  45. package/src/service/fetch/DefaultFetchService.ts +2 -1
  46. package/src/service/printer/PrinterServiceProvider.ts +2 -1
  47. package/src/service/shutdown/ShutdownServiceProvider.ts +3 -3
  48. package/src/service/spawn/DefaultSpawnService.ts +3 -1
  49. package/src/service/upgrade/DefaultUpgradeService.ts +112 -5
  50. package/src/service/upgrade/UpgradeServiceProvider.ts +112 -20
  51. package/src/service/upgrade/command/UpgradeSubCommand.ts +2 -2
  52. package/src/startup/banner/bannerStartupTask.ts +16 -10
@@ -1,6 +1,7 @@
1
1
  import process from "node:process";
2
2
  import { PRINTER_SERVICE_ID, SHUTDOWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import getLogger from "../../util/logger.js";
4
+ import { CLEANUP_SHUTDOWN_TASK_PRIORITY } from "../../runtime/lifecycle/priorities.js";
4
5
  const logger = getLogger("DefaultSpawnService");
5
6
  const SHUTDOWN_GRACE_PERIOD_MS = 5000;
6
7
  /**
@@ -79,6 +80,7 @@ export default class DefaultSpawnService {
79
80
  try {
80
81
  proc = Bun.spawn(resolveForPlatform(command), {
81
82
  cwd: options.cwd,
83
+ env: options.env === undefined ? undefined : { ...process.env, ...options.env },
82
84
  stdin: mode === "inherit" ? "inherit" : "ignore",
83
85
  stdout: mode === "wrapped" ? "pipe" : mode === "ignore" ? "ignore" : "inherit",
84
86
  stderr: mode === "wrapped" ? "pipe" : mode === "ignore" ? "ignore" : "inherit",
@@ -94,7 +96,7 @@ export default class DefaultSpawnService {
94
96
  }
95
97
  shutdownService.registerTask({
96
98
  id: `spawn:${command.join(" ")}`,
97
- priority: 0,
99
+ priority: CLEANUP_SHUTDOWN_TASK_PRIORITY,
98
100
  run: async () => {
99
101
  if (settled) {
100
102
  return;
@@ -1,10 +1,22 @@
1
- import type { CLIConfig, Context, UpgradeCheckResult, UpgradeResult } from "@flowscripter/dynamic-cli-framework-api";
2
- import { InstallMethod, SupportedArch, SupportedOs, type UpgradeService } from "@flowscripter/dynamic-cli-framework-api";
1
+ import type { CLIConfig, Context, SpawnResult, UpgradeCheckResult, UpgradeResult } from "@flowscripter/dynamic-cli-framework-api";
2
+ import { InstallMethod, RunState, SupportedArch, SupportedOs, type UpgradeService } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import type { UpgradeLocationsConfig } from "./UpgradeLocationsConfig.ts";
4
4
  export declare const UPGRADE_CHECK_CACHE_KEY = "upgrade-check-result";
5
5
  export declare function describeUpgradeCheckResult(result: UpgradeCheckResult): string;
6
+ /**
7
+ * Map the result of spawning a restarted CLI to the {@link RunState} it ended with.
8
+ *
9
+ * The child is the same framework CLI, so it normally exits with a {@link RunState} value, which is
10
+ * returned as is (this includes 130 for SIGINT and 143 for SIGTERM). Termination by any other
11
+ * signal, or a timeout, maps to {@link RunState.TERMINATED}, and any other exit code to
12
+ * {@link RunState.RUNTIME_ERROR}.
13
+ *
14
+ * @return the {@link RunState}, or `undefined` if the child could not be started.
15
+ */
16
+ export declare function toRunState(result: SpawnResult): RunState | undefined;
6
17
  export default class DefaultUpgradeService implements UpgradeService {
7
18
  #private;
19
+ readonly restartedFromVersion: string | undefined;
8
20
  constructor(config: UpgradeLocationsConfig, cliConfig: CLIConfig);
9
21
  setContext(context: Context): void;
10
22
  getUpgradeCheckResult(): Promise<UpgradeCheckResult>;
@@ -19,6 +31,22 @@ export default class DefaultUpgradeService implements UpgradeService {
19
31
  * Returns `undefined` if nothing has been persisted yet or no KeyValueService is available.
20
32
  */
21
33
  getCachedUpgradeCheckResult(): Promise<UpgradeCheckResult | undefined>;
34
+ /**
35
+ * Resolve the executable to restart into after a successful upgrade via `installMethod`.
36
+ *
37
+ * @return the executable path, or `undefined` if it cannot be resolved (e.g. for winget, or a
38
+ * homebrew install whose `opt` link is missing).
39
+ */
40
+ resolveUpgradedExecutable(installMethod: InstallMethod): string | undefined;
41
+ /**
42
+ * Run `executable` with `args` in place of this process, with terminal input and output
43
+ * inherited, and wait for it to exit. The child process sees the current version as its
44
+ * {@link restartedFromVersion}.
45
+ *
46
+ * @return the {@link RunState} the child ended with (see {@link toRunState}), or `undefined` if
47
+ * it could not be started.
48
+ */
49
+ restart(executable: string, args: ReadonlyArray<string>): Promise<RunState | undefined>;
22
50
  detectOs(): SupportedOs | undefined;
23
51
  detectArch(): SupportedArch | undefined;
24
52
  detectInstallMethod(os: SupportedOs): Promise<InstallMethod | undefined>;
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultUpgradeService.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/DefaultUpgradeService.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,SAAS,EACT,OAAO,EAOP,kBAAkB,EAClB,aAAa,EAEd,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAEL,aAAa,EAIb,aAAa,EACb,WAAW,EACX,KAAK,cAAc,EACpB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAwC1E,eAAO,MAAM,uBAAuB,yBAAyB,CAAC;AAY9D,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAI7E;AAQD,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,cAAc;;IAMlE,YAAmB,MAAM,EAAE,sBAAsB,EAAE,SAAS,EAAE,SAAS,EAGtE;IAEM,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAExC;IA4DM,qBAAqB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAe1D;IAED;;;;OAIG;IACU,wBAAwB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CASnE;IAED;;;OAGG;IACU,2BAA2B,IAAI,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAWlF;IAEM,QAAQ,IAAI,WAAW,GAAG,SAAS,CAWzC;IAEM,UAAU,IAAI,aAAa,GAAG,SAAS,CAS7C;IAEY,mBAAmB,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAsCpF;IA2CY,eAAe,CAC1B,UAAU,CAAC,EAAE,WAAW,EACxB,YAAY,CAAC,EAAE,aAAa,EAC5B,qBAAqB,CAAC,EAAE,aAAa,GACpC,OAAO,CAAC,kBAAkB,CAAC,CAsC7B;IAEY,OAAO,CAClB,UAAU,CAAC,EAAE,WAAW,EACxB,YAAY,CAAC,EAAE,aAAa,EAC5B,qBAAqB,CAAC,EAAE,aAAa,GACpC,OAAO,CAAC,aAAa,CAAC,CAkDxB;CAwTF"}
1
+ {"version":3,"file":"DefaultUpgradeService.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/DefaultUpgradeService.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EACV,SAAS,EACT,OAAO,EAKP,WAAW,EAEX,kBAAkB,EAClB,aAAa,EAEd,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAEL,aAAa,EAGb,QAAQ,EAER,aAAa,EACb,WAAW,EACX,KAAK,cAAc,EACpB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAwC1E,eAAO,MAAM,uBAAuB,yBAAyB,CAAC;AAY9D,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAI7E;AAaD;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAiBpE;AAQD,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,cAAc;;IAKlE,SAAgB,oBAAoB,EAAE,MAAM,GAAG,SAAS,CAAC;IAEzD,YAAmB,MAAM,EAAE,sBAAsB,EAAE,SAAS,EAAE,SAAS,EAItE;IAEM,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAExC;IA4DM,qBAAqB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAe1D;IAED;;;;OAIG;IACU,wBAAwB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CASnE;IAED;;;OAGG;IACU,2BAA2B,IAAI,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAWlF;IAED;;;;;OAKG;IACI,yBAAyB,CAAC,aAAa,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAUjF;IAoBD;;;;;;;OAOG;IACU,OAAO,CAClB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAC1B,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAY/B;IAEM,QAAQ,IAAI,WAAW,GAAG,SAAS,CAWzC;IAEM,UAAU,IAAI,aAAa,GAAG,SAAS,CAS7C;IAEY,mBAAmB,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAsCpF;IA2CY,eAAe,CAC1B,UAAU,CAAC,EAAE,WAAW,EACxB,YAAY,CAAC,EAAE,aAAa,EAC5B,qBAAqB,CAAC,EAAE,aAAa,GACpC,OAAO,CAAC,kBAAkB,CAAC,CAsC7B;IAEY,OAAO,CAClB,UAAU,CAAC,EAAE,WAAW,EACxB,YAAY,CAAC,EAAE,aAAa,EAC5B,qBAAqB,CAAC,EAAE,aAAa,GACpC,OAAO,CAAC,aAAa,CAAC,CAkDxB;CA2TF"}
@@ -1,9 +1,9 @@
1
1
  import process from "node:process";
2
- import { realpathSync } from "node:fs";
2
+ import { existsSync, realpathSync } from "node:fs";
3
3
  import { mkdtemp, rename, rm } from "node:fs/promises";
4
4
  import { tmpdir } from "node:os";
5
- import { dirname, join } from "node:path";
6
- import { FETCH_SERVICE_ID, InstallMethod, KEY_VALUE_SERVICE_ID, PRINTER_SERVICE_ID, SPAWN_SERVICE_ID, SupportedArch, SupportedOs, } from "@flowscripter/dynamic-cli-framework-api";
5
+ import { basename, dirname, join } from "node:path";
6
+ import { FETCH_SERVICE_ID, InstallMethod, KEY_VALUE_SERVICE_ID, PRINTER_SERVICE_ID, RunState, SPAWN_SERVICE_ID, SupportedArch, SupportedOs, } from "@flowscripter/dynamic-cli-framework-api";
7
7
  import semver from "semver";
8
8
  import getLogger from "../../util/logger.js";
9
9
  const logger = getLogger("DefaultUpgradeService");
@@ -38,6 +38,40 @@ export function describeUpgradeCheckResult(result) {
38
38
  ? `Upgrade check result: failed - ${result.error.message}`
39
39
  : `Upgrade check result: ${JSON.stringify(result)}`;
40
40
  }
41
+ // Environment variable set on a process restarted after an automatic upgrade, holding the version
42
+ // which restarted it (see DefaultUpgradeService.restart() and restartedFromVersion).
43
+ const RESTARTED_FROM_VERSION_ENV_VAR = "DYNAMIC_CLI_FRAMEWORK_REEXEC_FROM";
44
+ // Exit codes from 129 upwards are reported for a child terminated by a signal (128 + signal number).
45
+ const SIGNAL_EXIT_CODE_BASE = 128;
46
+ const RUN_STATE_VALUES = new Set(Object.values(RunState).filter((value) => typeof value === "number"));
47
+ /**
48
+ * Map the result of spawning a restarted CLI to the {@link RunState} it ended with.
49
+ *
50
+ * The child is the same framework CLI, so it normally exits with a {@link RunState} value, which is
51
+ * returned as is (this includes 130 for SIGINT and 143 for SIGTERM). Termination by any other
52
+ * signal, or a timeout, maps to {@link RunState.TERMINATED}, and any other exit code to
53
+ * {@link RunState.RUNTIME_ERROR}.
54
+ *
55
+ * @return the {@link RunState}, or `undefined` if the child could not be started.
56
+ */
57
+ export function toRunState(result) {
58
+ if (result.ok) {
59
+ return RunState.SUCCESS;
60
+ }
61
+ if ("timedOut" in result) {
62
+ return RunState.TERMINATED;
63
+ }
64
+ if (result.exitCode === undefined) {
65
+ return undefined;
66
+ }
67
+ if (RUN_STATE_VALUES.has(result.exitCode)) {
68
+ return result.exitCode;
69
+ }
70
+ if (result.exitCode > SIGNAL_EXIT_CODE_BASE) {
71
+ return RunState.TERMINATED;
72
+ }
73
+ return RunState.RUNTIME_ERROR;
74
+ }
41
75
  const OS_LABELS = {
42
76
  [SupportedOs.LINUX]: "Linux",
43
77
  [SupportedOs.MACOS]: "MacOS",
@@ -48,9 +82,11 @@ export default class DefaultUpgradeService {
48
82
  #upgradeCheckPromise;
49
83
  #config;
50
84
  #cliConfig;
85
+ restartedFromVersion;
51
86
  constructor(config, cliConfig) {
52
87
  this.#config = config;
53
88
  this.#cliConfig = cliConfig;
89
+ this.restartedFromVersion = process.env[RESTARTED_FROM_VERSION_ENV_VAR] || undefined;
54
90
  }
55
91
  setContext(context) {
56
92
  this.#context = context;
@@ -150,6 +186,61 @@ export default class DefaultUpgradeService {
150
186
  const cached = await this.#safeKeyValueCall(() => keyValueService.get(UPGRADE_CHECK_CACHE_KEY));
151
187
  return cached;
152
188
  }
189
+ /**
190
+ * Resolve the executable to restart into after a successful upgrade via `installMethod`.
191
+ *
192
+ * @return the executable path, or `undefined` if it cannot be resolved (e.g. for winget, or a
193
+ * homebrew install whose `opt` link is missing).
194
+ */
195
+ resolveUpgradedExecutable(installMethod) {
196
+ switch (installMethod) {
197
+ case InstallMethod.GITHUB_RELEASE:
198
+ case InstallMethod.LINUX_SCRIPT:
199
+ return process.execPath;
200
+ case InstallMethod.HOMEBREW:
201
+ return this.#resolveHomebrewOptExecutable();
202
+ case InstallMethod.WINGET:
203
+ return undefined;
204
+ }
205
+ }
206
+ // The running executable lives in a version-specific Cellar directory, e.g.
207
+ // "<prefix>/Cellar/<formula>/<version>/bin/<exe>", while "<prefix>/opt/<formula>" links to the
208
+ // installed version, so the upgraded executable is "<prefix>/opt/<formula>/bin/<exe>".
209
+ #resolveHomebrewOptExecutable() {
210
+ if (!this.#config.homebrew) {
211
+ return undefined;
212
+ }
213
+ const { formula } = this.#config.homebrew;
214
+ const realExecutable = this.#resolveRealExecutable();
215
+ const cellarIndex = realExecutable.indexOf(`/Cellar/${formula}/`);
216
+ if (cellarIndex === -1) {
217
+ return undefined;
218
+ }
219
+ const prefix = realExecutable.slice(0, cellarIndex);
220
+ const executable = join(prefix, "opt", formula, "bin", basename(realExecutable));
221
+ return existsSync(executable) ? executable : undefined;
222
+ }
223
+ /**
224
+ * Run `executable` with `args` in place of this process, with terminal input and output
225
+ * inherited, and wait for it to exit. The child process sees the current version as its
226
+ * {@link restartedFromVersion}.
227
+ *
228
+ * @return the {@link RunState} the child ended with (see {@link toRunState}), or `undefined` if
229
+ * it could not be started.
230
+ */
231
+ async restart(executable, args) {
232
+ const spawnService = this.#spawnService;
233
+ if (!spawnService) {
234
+ return undefined;
235
+ }
236
+ const result = await spawnService.spawn([executable, ...args], {
237
+ mode: "inherit",
238
+ // Ctrl-C reaches the child directly from the terminal, so this process only notes it
239
+ longRunning: true,
240
+ env: { [RESTARTED_FROM_VERSION_ENV_VAR]: this.#cliConfig.version },
241
+ });
242
+ return toRunState(result);
243
+ }
153
244
  detectOs() {
154
245
  switch (process.platform) {
155
246
  case "linux":
@@ -331,15 +422,17 @@ export default class DefaultUpgradeService {
331
422
  // `brew`, which has a slow cold start - avoiding it keeps the background upgrade-check
332
423
  // StartupTask (see UpgradeServiceProvider) fast even though it runs to completion.
333
424
  #isRunningFromHomebrewCellar(formula) {
334
- let realExecutable = process.execPath;
425
+ return this.#resolveRealExecutable().includes(`/Cellar/${formula}/`);
426
+ }
427
+ #resolveRealExecutable() {
335
428
  try {
336
- realExecutable = realpathSync(process.execPath);
429
+ return realpathSync(process.execPath);
337
430
  }
338
431
  catch {
339
432
  // process.execPath may not resolve on disk (e.g. a fabricated path in tests) - fall back to
340
433
  // the unresolved path rather than treating that as "not installed".
434
+ return process.execPath;
341
435
  }
342
- return realExecutable.includes(`/Cellar/${formula}/`);
343
436
  }
344
437
  async #isHomebrewInstalled() {
345
438
  if (!this.#spawnService || !this.#config.homebrew) {
@@ -3,7 +3,7 @@ import type { Context } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import DefaultUpgradeService from "./DefaultUpgradeService.ts";
5
5
  import type { UpgradeLocationsConfig } from "./UpgradeLocationsConfig.ts";
6
- import type { StartupTask } from "@flowscripter/dynamic-cli-framework-api";
6
+ import type { StartupTask, StartupTaskOutcome } from "@flowscripter/dynamic-cli-framework-api";
7
7
  /**
8
8
  * Build the background {@link StartupTask} which opportunistically refreshes the upgrade-check
9
9
  * cache (see {@link DefaultUpgradeService.refreshUpgradeCheckCache}) that the banner task reads
@@ -11,13 +11,43 @@ import type { StartupTask } from "@flowscripter/dynamic-cli-framework-api";
11
11
  * regular blocking init task) so it can run in `"background"` mode.
12
12
  */
13
13
  export declare function createUpgradeCheckStartupTask(upgradeService: DefaultUpgradeService, priority: number): StartupTask;
14
+ /**
15
+ * Build the blocking {@link StartupTask} which offers to enable automatic upgrades and, when they
16
+ * are enabled, checks for and installs a newer version, optionally restarting into it (see
17
+ * {@link UpgradeServiceProvider.runAutoUpgrade}). It is registered separately from the provider's
18
+ * own init task so it can run at a lower priority, after consumer startup tasks such as the banner.
19
+ */
20
+ export declare function createAutoUpgradeStartupTask(provider: UpgradeServiceProvider, priority: number): StartupTask;
14
21
  export default class UpgradeServiceProvider implements ServiceProvider {
15
22
  #private;
16
23
  readonly serviceId: string;
17
24
  readonly servicePriority: number;
18
- constructor(servicePriority: number, config: UpgradeLocationsConfig);
25
+ /**
26
+ * @param servicePriority the priority of the service.
27
+ * @param config the upgrade locations.
28
+ * @param restartAfterAutoUpgrade whether to run the upgraded executable, with the arguments from
29
+ * {@link setRestartArgs}, after a successful automatic upgrade and end the run with its result.
30
+ */
31
+ constructor(servicePriority: number, config: UpgradeLocationsConfig, restartAfterAutoUpgrade?: boolean);
32
+ /**
33
+ * Set the CLI arguments to run the upgraded executable with when restarting after an automatic
34
+ * upgrade. These are kept internal to the framework and never exposed to startup tasks.
35
+ */
36
+ setRestartArgs(args: ReadonlyArray<string>): void;
19
37
  get upgradeService(): DefaultUpgradeService | undefined;
20
38
  getServiceInfo(cliConfig: CLIConfig): Promise<ServiceInfo>;
21
39
  initService(context: Context): Promise<void>;
40
+ /**
41
+ * Offer to enable automatic upgrades (storing the answer as `upgrade-status` in the upgrade
42
+ * service's scope) and, when they are enabled, check for and install a newer version. Must be
43
+ * called after {@link initService}.
44
+ *
45
+ * After a successful upgrade, when restarting is enabled, the upgraded executable is run with the
46
+ * arguments from {@link setRestartArgs} and its result is returned as an `exitRequest`. Does
47
+ * nothing in a process which was itself started by such a restart.
48
+ *
49
+ * @param context the startup task's context, used to look up the Prompter and Printer services.
50
+ */
51
+ runAutoUpgrade(context: Context): Promise<void | StartupTaskOutcome>;
22
52
  }
23
53
  //# sourceMappingURL=UpgradeServiceProvider.d.ts.map
@@ -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,qBAAqB,MAAM,4BAA4B,CAAC;AAE/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAE1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAI3E;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,cAAc,EAAE,qBAAqB,EACrC,QAAQ,EAAE,MAAM,GACf,WAAW,CASb;AAED,MAAM,CAAC,OAAO,OAAO,sBAAuB,YAAW,eAAe;;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAsB;IAChD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IAKjC,YAAmB,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB,EAGzE;IAED,IAAW,cAAc,IAAI,qBAAqB,GAAG,SAAS,CAE7D;IAEM,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAOhE;IAEY,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAqFxD;CAoCF"}
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,qBAAqB,MAAM,4BAA4B,CAAC;AAE/D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAE1E,OAAO,KAAK,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAI/F;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAC3C,cAAc,EAAE,qBAAqB,EACrC,QAAQ,EAAE,MAAM,GACf,WAAW,CASb;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,sBAAsB,EAChC,QAAQ,EAAE,MAAM,GACf,WAAW,CAOb;AAED,MAAM,CAAC,OAAO,OAAO,sBAAuB,YAAW,eAAe;;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAsB;IAChD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IAQjC;;;;;OAKG;IACH,YACE,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,sBAAsB,EAC9B,uBAAuB,UAAQ,EAKhC;IAED;;;OAGG;IACI,cAAc,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,IAAI,CAEvD;IAED,IAAW,cAAc,IAAI,qBAAqB,GAAG,SAAS,CAE7D;IAEM,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAOhE;IAEM,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAsBlD;IAED;;;;;;;;;;OAUG;IACU,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,GAAG,kBAAkB,CAAC,CA4EhF;CA8DF"}
@@ -24,15 +24,46 @@ export function createUpgradeCheckStartupTask(upgradeService, priority) {
24
24
  },
25
25
  };
26
26
  }
27
+ /**
28
+ * Build the blocking {@link StartupTask} which offers to enable automatic upgrades and, when they
29
+ * are enabled, checks for and installs a newer version, optionally restarting into it (see
30
+ * {@link UpgradeServiceProvider.runAutoUpgrade}). It is registered separately from the provider's
31
+ * own init task so it can run at a lower priority, after consumer startup tasks such as the banner.
32
+ */
33
+ export function createAutoUpgradeStartupTask(provider, priority) {
34
+ return {
35
+ id: `${UPGRADE_SERVICE_ID}-auto-upgrade`,
36
+ priority,
37
+ mode: "blocking",
38
+ run: (context) => provider.runAutoUpgrade(context),
39
+ };
40
+ }
27
41
  export default class UpgradeServiceProvider {
28
42
  serviceId = UPGRADE_SERVICE_ID;
29
43
  servicePriority;
30
44
  #config;
31
45
  #upgradeService;
32
46
  #cliConfig;
33
- constructor(servicePriority, config) {
47
+ #keyValueService;
48
+ #restartAfterAutoUpgrade;
49
+ #restartArgs = [];
50
+ /**
51
+ * @param servicePriority the priority of the service.
52
+ * @param config the upgrade locations.
53
+ * @param restartAfterAutoUpgrade whether to run the upgraded executable, with the arguments from
54
+ * {@link setRestartArgs}, after a successful automatic upgrade and end the run with its result.
55
+ */
56
+ constructor(servicePriority, config, restartAfterAutoUpgrade = false) {
34
57
  this.servicePriority = servicePriority;
35
58
  this.#config = config;
59
+ this.#restartAfterAutoUpgrade = restartAfterAutoUpgrade;
60
+ }
61
+ /**
62
+ * Set the CLI arguments to run the upgraded executable with when restarting after an automatic
63
+ * upgrade. These are kept internal to the framework and never exposed to startup tasks.
64
+ */
65
+ setRestartArgs(args) {
66
+ this.#restartArgs = args;
36
67
  }
37
68
  get upgradeService() {
38
69
  return this.#upgradeService;
@@ -45,9 +76,8 @@ export default class UpgradeServiceProvider {
45
76
  commands: [new UpgradeSubCommand()],
46
77
  });
47
78
  }
48
- async initService(context) {
79
+ initService(context) {
49
80
  const upgradeService = this.#upgradeService;
50
- const cliConfig = this.#cliConfig;
51
81
  const spawnService = context.doesServiceExist(SPAWN_SERVICE_ID)
52
82
  ? context.getServiceById(SPAWN_SERVICE_ID)
53
83
  : undefined;
@@ -60,10 +90,33 @@ export default class UpgradeServiceProvider {
60
90
  if (fetchService === undefined) {
61
91
  logger.debug(() => "FetchService not available, upgrade version checks will be unavailable");
62
92
  }
63
- const keyValueService = context.doesServiceExist(KEY_VALUE_SERVICE_ID)
93
+ // the KeyValueService from this context is bound to the upgrade service's own scope, so it is
94
+ // kept for runAutoUpgrade(), whose task context is scoped to the task ID instead
95
+ this.#keyValueService = context.doesServiceExist(KEY_VALUE_SERVICE_ID)
64
96
  ? context.getServiceById(KEY_VALUE_SERVICE_ID)
65
97
  : undefined;
66
98
  upgradeService.setContext(context);
99
+ return Promise.resolve();
100
+ }
101
+ /**
102
+ * Offer to enable automatic upgrades (storing the answer as `upgrade-status` in the upgrade
103
+ * service's scope) and, when they are enabled, check for and install a newer version. Must be
104
+ * called after {@link initService}.
105
+ *
106
+ * After a successful upgrade, when restarting is enabled, the upgraded executable is run with the
107
+ * arguments from {@link setRestartArgs} and its result is returned as an `exitRequest`. Does
108
+ * nothing in a process which was itself started by such a restart.
109
+ *
110
+ * @param context the startup task's context, used to look up the Prompter and Printer services.
111
+ */
112
+ async runAutoUpgrade(context) {
113
+ const upgradeService = this.#upgradeService;
114
+ const cliConfig = this.#cliConfig;
115
+ const keyValueService = this.#keyValueService;
116
+ if (upgradeService.restartedFromVersion !== undefined) {
117
+ logger.debug(() => `Restarted from version ${upgradeService.restartedFromVersion}, skipping auto-upgrade`);
118
+ return;
119
+ }
67
120
  if (!context.doesServiceExist(PROMPTER_SERVICE_ID)) {
68
121
  logger.debug(() => "PrompterService not available, skipping auto-upgrade");
69
122
  return;
@@ -79,8 +132,7 @@ export default class UpgradeServiceProvider {
79
132
  return;
80
133
  }
81
134
  if (status === "enabled") {
82
- await this.#checkAndUpgrade(context, upgradeService, cliConfig);
83
- return;
135
+ return this.#checkAndUpgrade(context, upgradeService, cliConfig, keyValueService);
84
136
  }
85
137
  }
86
138
  const prompterService = context.getServiceById(PROMPTER_SERVICE_ID);
@@ -121,9 +173,9 @@ export default class UpgradeServiceProvider {
121
173
  return;
122
174
  }
123
175
  await keyValueService.set("upgrade-status", "enabled");
124
- await this.#checkAndUpgrade(context, upgradeService, cliConfig);
176
+ return this.#checkAndUpgrade(context, upgradeService, cliConfig, keyValueService);
125
177
  }
126
- async #checkAndUpgrade(context, upgradeService, cliConfig) {
178
+ async #checkAndUpgrade(context, upgradeService, cliConfig, keyValueService) {
127
179
  try {
128
180
  const checkResult = await upgradeService.getUpgradeCheckResult();
129
181
  if (checkResult.status === "failed") {
@@ -134,16 +186,32 @@ export default class UpgradeServiceProvider {
134
186
  return;
135
187
  }
136
188
  const upgradeResult = await upgradeService.upgrade();
137
- if (!context.doesServiceExist(PRINTER_SERVICE_ID)) {
189
+ const printerService = context.doesServiceExist(PRINTER_SERVICE_ID)
190
+ ? context.getServiceById(PRINTER_SERVICE_ID)
191
+ : undefined;
192
+ if (!upgradeResult.ok) {
193
+ await printerService?.error(`Auto-upgrade failed: ${upgradeResult.error?.message ?? "unknown error"}\n`, Icon.FAILURE);
138
194
  return;
139
195
  }
140
- const printerService = context.getServiceById(PRINTER_SERVICE_ID);
141
- if (upgradeResult.ok) {
142
- await printerService.info(`${cliConfig.name} upgraded (${upgradeResult.oldVersion} -> ${upgradeResult.newVersion})\n`, Icon.SUCCESS);
196
+ await printerService?.info(`${cliConfig.name} upgraded (${upgradeResult.oldVersion} -> ${upgradeResult.newVersion})\n`, Icon.SUCCESS);
197
+ if (!this.#restartAfterAutoUpgrade) {
198
+ return;
199
+ }
200
+ const executable = upgradeService.resolveUpgradedExecutable(checkResult.installMethod);
201
+ if (executable === undefined) {
202
+ await printerService?.info(`Restart ${cliConfig.name} to use ${upgradeResult.newVersion}\n`, Icon.INFORMATION);
203
+ return;
143
204
  }
144
- else {
145
- await printerService.error(`Auto-upgrade failed: ${upgradeResult.error?.message ?? "unknown error"}\n`, Icon.FAILURE);
205
+ // the cache write from the background upgrade check lands before the flush, so the shutdown
206
+ // flush has nothing left to write over the restarted process's configuration
207
+ await upgradeService.refreshUpgradeCheckCache();
208
+ // written now so the restarted process sees the stored upgrade-status
209
+ await keyValueService.flush();
210
+ const runState = await upgradeService.restart(executable, this.#restartArgs);
211
+ if (runState !== undefined) {
212
+ return { exitRequest: { runState } };
146
213
  }
214
+ await printerService?.warn(`${cliConfig.name} upgraded to ${upgradeResult.newVersion}; restart it to use the new version\n`, Icon.ALERT);
147
215
  }
148
216
  catch (error) {
149
217
  logger.debug(() => `Auto-upgrade check failed: ${error}`);
@@ -1,15 +1,15 @@
1
1
  import type { StartupTask } from "@flowscripter/dynamic-cli-framework-api";
2
2
  export declare const BANNER_STARTUP_TASK_ID = "@flowscripter/dynamic-cli-framework/banner-startup-task";
3
3
  /**
4
- * Build the ascii banner {@link StartupTask}, replacing the old `BannerServiceProvider`.
4
+ * Build the ascii banner {@link StartupTask}.
5
5
  *
6
6
  * A consumer opts in by passing the result of this function in the `serviceProviders` array
7
7
  * argument of {@link launchMultiCommandCLI}/{@link launchSingleCommandCLI} (or via
8
- * `BaseCLI.addStartupTask`), the same way `new BannerServiceProvider(priority)` used to be passed
9
- * in that array - there's no default/automatic banner registration.
8
+ * `BaseCLI.addStartupTask`) - there's no default/automatic banner registration.
10
9
  *
11
- * @param priority the priority of the task (higher runs earlier - the same semantics
12
- * `BannerServiceProvider.servicePriority` had).
10
+ * @param priority the priority of the task (higher runs earlier). It must be above
11
+ * `AUTO_UPGRADE_STARTUP_TASK_PRIORITY` (10) for the banner to print before the automatic upgrade
12
+ * prompt and upgrade.
13
13
  * @param fontName an optional [FIGlet](http://www.figlet.org) font name to use for the banner,
14
14
  * defaults to "standard".
15
15
  */
@@ -1 +1 @@
1
- {"version":3,"file":"bannerStartupTask.d.ts","sourceRoot":"","sources":["../../../../src/startup/banner/bannerStartupTask.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA2B,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAYpG,eAAO,MAAM,sBAAsB,4DAA4D,CAAC;AAEhG;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,OAAO,UAAU,uBAAuB,CAC7C,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAAa,GACpB,WAAW,CA+Db"}
1
+ {"version":3,"file":"bannerStartupTask.d.ts","sourceRoot":"","sources":["../../../../src/startup/banner/bannerStartupTask.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA2B,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAYpG,eAAO,MAAM,sBAAsB,4DAA4D,CAAC;AAEhG;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,OAAO,UAAU,uBAAuB,CAC7C,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAAa,GACpB,WAAW,CAqEb"}
@@ -1,17 +1,16 @@
1
1
  import { ASCII_BANNER_GENERATOR_SERVICE_ID, CONFIGURATION_SERVICE_ID, PRINTER_SERVICE_ID, UPGRADE_SERVICE_ID, } from "@flowscripter/dynamic-cli-framework-api";
2
- import DefaultUpgradeService from "../../service/upgrade/DefaultUpgradeService.js";
3
2
  import NoBannerCommand, {} from "./command/NoBannerCommand.js";
4
3
  export const BANNER_STARTUP_TASK_ID = "@flowscripter/dynamic-cli-framework/banner-startup-task";
5
4
  /**
6
- * Build the ascii banner {@link StartupTask}, replacing the old `BannerServiceProvider`.
5
+ * Build the ascii banner {@link StartupTask}.
7
6
  *
8
7
  * A consumer opts in by passing the result of this function in the `serviceProviders` array
9
8
  * argument of {@link launchMultiCommandCLI}/{@link launchSingleCommandCLI} (or via
10
- * `BaseCLI.addStartupTask`), the same way `new BannerServiceProvider(priority)` used to be passed
11
- * in that array - there's no default/automatic banner registration.
9
+ * `BaseCLI.addStartupTask`) - there's no default/automatic banner registration.
12
10
  *
13
- * @param priority the priority of the task (higher runs earlier - the same semantics
14
- * `BannerServiceProvider.servicePriority` had).
11
+ * @param priority the priority of the task (higher runs earlier). It must be above
12
+ * `AUTO_UPGRADE_STARTUP_TASK_PRIORITY` (10) for the banner to print before the automatic upgrade
13
+ * prompt and upgrade.
15
14
  * @param fontName an optional [FIGlet](http://www.figlet.org) font name to use for the banner,
16
15
  * defaults to "standard".
17
16
  */
@@ -26,6 +25,14 @@ export default function createBannerStartupTask(priority, fontName = "standard")
26
25
  if (!state.printBanner) {
27
26
  return;
28
27
  }
28
+ const upgradeService = context.doesServiceExist(UPGRADE_SERVICE_ID)
29
+ ? context.getServiceById(UPGRADE_SERVICE_ID)
30
+ : undefined;
31
+ // a process restarted after an automatic upgrade follows the banner already printed by the
32
+ // process which restarted it
33
+ if (upgradeService?.restartedFromVersion !== undefined) {
34
+ return;
35
+ }
29
36
  const printerService = context.getServiceById(PRINTER_SERVICE_ID);
30
37
  const asciiBannerGeneratorService = context.getServiceById(ASCII_BANNER_GENERATOR_SERVICE_ID);
31
38
  const { cliConfig } = context;
@@ -44,10 +51,7 @@ export default function createBannerStartupTask(priority, fontName = "standard")
44
51
  // result yet) shows no upgrade-available suffix. The cached result is read through the
45
52
  // upgrade service because it is stored in that service's own KeyValueService scope, and
46
53
  // it is ignored if it was recorded by a different version (e.g. before an upgrade).
47
- const upgradeService = context.doesServiceExist(UPGRADE_SERVICE_ID)
48
- ? context.getServiceById(UPGRADE_SERVICE_ID)
49
- : undefined;
50
- if (upgradeService instanceof DefaultUpgradeService) {
54
+ if (upgradeService) {
51
55
  const result = await upgradeService.getCachedUpgradeCheckResult();
52
56
  if (result?.status === "checked" &&
53
57
  result.updateAvailable &&
package/index.ts CHANGED
@@ -207,6 +207,26 @@ export { default as PluginServiceProvider } from "./src/service/plugin/PluginSer
207
207
 
208
208
  // Core CLI
209
209
  export { default as BaseCLI } from "./src/cli/BaseCLI.ts";
210
+ export {
211
+ SHUTDOWN_SERVICE_PRIORITY,
212
+ STARTUP_SERVICE_PRIORITY,
213
+ CONFIGURATION_SERVICE_PRIORITY,
214
+ KEY_VALUE_SERVICE_PRIORITY,
215
+ PRINTER_SERVICE_PRIORITY,
216
+ PROMPTER_SERVICE_PRIORITY,
217
+ TABLE_GENERATOR_SERVICE_PRIORITY,
218
+ ARGUMENT_PROMPTER_SERVICE_PRIORITY,
219
+ SPAWN_SERVICE_PRIORITY,
220
+ FETCH_SERVICE_PRIORITY,
221
+ UPGRADE_SERVICE_PRIORITY,
222
+ UPGRADE_CHECK_STARTUP_TASK_PRIORITY,
223
+ IMAGE_PRINTER_SERVICE_PRIORITY,
224
+ PLUGIN_SERVICE_PRIORITY,
225
+ AUTO_UPGRADE_STARTUP_TASK_PRIORITY,
226
+ COMPLETION_SERVICE_PRIORITY,
227
+ CLEANUP_SHUTDOWN_TASK_PRIORITY,
228
+ KEY_VALUE_FLUSH_SHUTDOWN_TASK_PRIORITY,
229
+ } from "./src/runtime/lifecycle/priorities.ts";
210
230
  export { default as DefaultRuntimeCLI } from "./src/cli/DefaultRuntimeCLI.ts";
211
231
 
212
232
  // Terminal API
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "7.0.10",
3
+ "version": "7.1.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": "^4.0.6",
51
+ "@flowscripter/dynamic-cli-framework-api": "^4.1.0",
52
52
  "@flowscripter/dynamic-plugin-framework": "^2.2.14",
53
53
  "emphasize": "^7.0.0",
54
54
  "fastest-levenshtein": "^1.0.16",