@flowscripter/dynamic-cli-framework 2.2.0 → 2.3.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 (33) hide show
  1. package/dist/src/cli/BaseCLI.d.ts +2 -0
  2. package/dist/src/cli/BaseCLI.d.ts.map +1 -1
  3. package/dist/src/cli/BaseCLI.js +8 -0
  4. package/dist/src/cli/BaseCLIFeatureOptions.d.ts +3 -0
  5. package/dist/src/cli/BaseCLIFeatureOptions.d.ts.map +1 -1
  6. package/dist/src/command/VersionCommand.d.ts.map +1 -1
  7. package/dist/src/command/VersionCommand.js +10 -1
  8. package/dist/src/service/banner/BannerServiceProvider.d.ts.map +1 -1
  9. package/dist/src/service/banner/BannerServiceProvider.js +8 -0
  10. package/dist/src/service/spawn/DefaultSpawnService.d.ts.map +1 -1
  11. package/dist/src/service/spawn/DefaultSpawnService.js +1 -0
  12. package/dist/src/service/upgrade/DefaultUpgradeService.d.ts +14 -0
  13. package/dist/src/service/upgrade/DefaultUpgradeService.d.ts.map +1 -0
  14. package/dist/src/service/upgrade/DefaultUpgradeService.js +312 -0
  15. package/dist/src/service/upgrade/UpgradeLocationsConfig.d.ts +51 -0
  16. package/dist/src/service/upgrade/UpgradeLocationsConfig.d.ts.map +1 -0
  17. package/dist/src/service/upgrade/UpgradeLocationsConfig.js +0 -0
  18. package/dist/src/service/upgrade/UpgradeServiceProvider.d.ts +13 -0
  19. package/dist/src/service/upgrade/UpgradeServiceProvider.d.ts.map +1 -0
  20. package/dist/src/service/upgrade/UpgradeServiceProvider.js +120 -0
  21. package/dist/src/service/upgrade/command/UpgradeSubCommand.d.ts +16 -0
  22. package/dist/src/service/upgrade/command/UpgradeSubCommand.d.ts.map +1 -0
  23. package/dist/src/service/upgrade/command/UpgradeSubCommand.js +50 -0
  24. package/package.json +4 -2
  25. package/src/cli/BaseCLI.ts +9 -0
  26. package/src/cli/BaseCLIFeatureOptions.ts +4 -0
  27. package/src/command/VersionCommand.ts +13 -1
  28. package/src/service/banner/BannerServiceProvider.ts +13 -0
  29. package/src/service/spawn/DefaultSpawnService.ts +1 -0
  30. package/src/service/upgrade/DefaultUpgradeService.ts +379 -0
  31. package/src/service/upgrade/UpgradeLocationsConfig.ts +53 -0
  32. package/src/service/upgrade/UpgradeServiceProvider.ts +150 -0
  33. package/src/service/upgrade/command/UpgradeSubCommand.ts +85 -0
@@ -0,0 +1,120 @@
1
+ import { UPGRADE_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
2
+ import { PROMPTER_SERVICE_ID, PromptType } from "@flowscripter/dynamic-cli-framework-api";
3
+ import { KEY_VALUE_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
4
+ import { SPAWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
5
+ import { Icon, PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
6
+ import DefaultUpgradeService from "./DefaultUpgradeService.js";
7
+ import { UpgradeSubCommand } from "./command/UpgradeSubCommand.js";
8
+ import getLogger from "../../util/logger.js";
9
+ const logger = getLogger("UpgradeServiceProvider");
10
+ export default class UpgradeServiceProvider {
11
+ serviceId = UPGRADE_SERVICE_ID;
12
+ servicePriority;
13
+ #config;
14
+ #upgradeService;
15
+ #cliConfig;
16
+ constructor(servicePriority, config) {
17
+ this.servicePriority = servicePriority;
18
+ this.#config = config;
19
+ }
20
+ getServiceInfo(cliConfig) {
21
+ this.#cliConfig = cliConfig;
22
+ this.#upgradeService = new DefaultUpgradeService(this.#config, cliConfig);
23
+ return Promise.resolve({
24
+ service: this.#upgradeService,
25
+ commands: [new UpgradeSubCommand(this.#upgradeService)],
26
+ });
27
+ }
28
+ async initService(context) {
29
+ const upgradeService = this.#upgradeService;
30
+ const cliConfig = this.#cliConfig;
31
+ if (context.doesServiceExist(SPAWN_SERVICE_ID)) {
32
+ const spawnService = context.getServiceById(SPAWN_SERVICE_ID);
33
+ upgradeService.setDependencies(spawnService);
34
+ }
35
+ else {
36
+ logger.debug(() => "SpawnService not available, upgrade install methods will be unavailable");
37
+ }
38
+ if (!context.doesServiceExist(PROMPTER_SERVICE_ID)) {
39
+ logger.debug(() => "PrompterService not available, skipping auto-upgrade");
40
+ return;
41
+ }
42
+ if (!context.doesServiceExist(KEY_VALUE_SERVICE_ID)) {
43
+ logger.debug(() => "KeyValueService not available, skipping auto-upgrade");
44
+ return;
45
+ }
46
+ const keyValueService = context.getServiceById(KEY_VALUE_SERVICE_ID);
47
+ if (await keyValueService.hasKey("upgrade-status")) {
48
+ const status = await keyValueService.getKey("upgrade-status");
49
+ if (status === "declined") {
50
+ logger.debug(() => "Auto-upgrade previously declined, skipping");
51
+ return;
52
+ }
53
+ if (status === "enabled") {
54
+ await this.#checkAndUpgrade(context, upgradeService, cliConfig);
55
+ return;
56
+ }
57
+ }
58
+ const prompterService = context.getServiceById(PROMPTER_SERVICE_ID);
59
+ if (!prompterService.promptEnabled) {
60
+ logger.debug(() => "Prompting is disabled, skipping auto-upgrade prompt");
61
+ return;
62
+ }
63
+ const os = upgradeService.detectOs();
64
+ const arch = upgradeService.detectArch();
65
+ if (!os || !arch) {
66
+ logger.debug(() => "Unsupported OS/arch, skipping auto-upgrade prompt");
67
+ return;
68
+ }
69
+ const installMethod = await upgradeService.detectInstallMethod(os);
70
+ if (!installMethod) {
71
+ logger.debug(() => "No supported install method detected, skipping auto-upgrade prompt");
72
+ return;
73
+ }
74
+ let enableResult;
75
+ try {
76
+ enableResult = await prompterService.prompt({
77
+ name: "enable-upgrade",
78
+ promptText: "Would you like to enable automatic upgrades on startup?",
79
+ description: "This will check for and install newer versions of " +
80
+ `${cliConfig.name} before it runs each time.`,
81
+ type: PromptType.TOGGLE,
82
+ options: [
83
+ { displayValue: "Yes", returnedValue: true },
84
+ { displayValue: "No", returnedValue: false },
85
+ ],
86
+ });
87
+ }
88
+ catch {
89
+ return;
90
+ }
91
+ if (enableResult.value !== true) {
92
+ await keyValueService.setKey("upgrade-status", "declined");
93
+ return;
94
+ }
95
+ await keyValueService.setKey("upgrade-status", "enabled");
96
+ await this.#checkAndUpgrade(context, upgradeService, cliConfig);
97
+ }
98
+ async #checkAndUpgrade(context, upgradeService, cliConfig) {
99
+ try {
100
+ const checkResult = await upgradeService.checkForUpgrade();
101
+ if (!checkResult?.updateAvailable) {
102
+ return;
103
+ }
104
+ const upgradeResult = await upgradeService.upgrade();
105
+ if (!context.doesServiceExist(PRINTER_SERVICE_ID)) {
106
+ return;
107
+ }
108
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID);
109
+ if (upgradeResult.ok) {
110
+ await printerService.info(`${cliConfig.name} upgraded (${upgradeResult.oldVersion} -> ${upgradeResult.newVersion})\n`, Icon.SUCCESS);
111
+ }
112
+ else {
113
+ await printerService.error(`Auto-upgrade failed: ${upgradeResult.error?.message ?? "unknown error"}\n`, Icon.FAILURE);
114
+ }
115
+ }
116
+ catch (error) {
117
+ logger.debug(() => `Auto-upgrade check failed: ${error}`);
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,16 @@
1
+ import type { SubCommand } from "@flowscripter/dynamic-cli-framework-api";
2
+ import type { Option } from "@flowscripter/dynamic-cli-framework-api";
3
+ import type { Context } from "@flowscripter/dynamic-cli-framework-api";
4
+ import { type ArgumentValues } from "@flowscripter/dynamic-cli-framework-api";
5
+ import type DefaultUpgradeService from "../DefaultUpgradeService.ts";
6
+ export declare class UpgradeSubCommand implements SubCommand {
7
+ #private;
8
+ readonly name = "upgrade";
9
+ readonly description = "Upgrade to the latest available version";
10
+ readonly enableConfiguration = false;
11
+ readonly positionals: never[];
12
+ readonly options: ReadonlyArray<Option>;
13
+ constructor(upgradeService: DefaultUpgradeService);
14
+ execute(context: Context, argumentValues: ArgumentValues): Promise<void>;
15
+ }
16
+ //# sourceMappingURL=UpgradeSubCommand.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,50 @@
1
+ import { ArgumentValueTypeName, } from "@flowscripter/dynamic-cli-framework-api";
2
+ import { Icon, InstallMethod, PRINTER_SERVICE_ID, SupportedArch, SupportedOs, } from "@flowscripter/dynamic-cli-framework-api";
3
+ export class UpgradeSubCommand {
4
+ name = "upgrade";
5
+ description = "Upgrade to the latest available version";
6
+ enableConfiguration = false;
7
+ positionals = [];
8
+ options = [
9
+ {
10
+ name: "os",
11
+ type: ArgumentValueTypeName.STRING,
12
+ isOptional: true,
13
+ allowableValues: Object.values(SupportedOs),
14
+ description: "Override the detected operating system",
15
+ },
16
+ {
17
+ name: "install-method",
18
+ type: ArgumentValueTypeName.STRING,
19
+ isOptional: true,
20
+ allowableValues: Object.values(InstallMethod),
21
+ description: "Override the detected install method",
22
+ },
23
+ ];
24
+ #upgradeService;
25
+ constructor(upgradeService) {
26
+ this.#upgradeService = upgradeService;
27
+ }
28
+ async execute(context, argumentValues) {
29
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID);
30
+ const cliName = context.cliConfig.name;
31
+ const os = argumentValues.os;
32
+ const arch = undefined;
33
+ const installMethod = argumentValues["install-method"];
34
+ const checkResult = await this.#upgradeService.checkForUpgrade(os, arch, installMethod);
35
+ if (!checkResult) {
36
+ await printerService.error(`No upgrade location is configured for the detected or requested platform.\n`, Icon.FAILURE);
37
+ return;
38
+ }
39
+ if (!checkResult.updateAvailable) {
40
+ await printerService.print(`${cliName} is already up to date (${checkResult.currentVersion}).\n`);
41
+ return;
42
+ }
43
+ const upgradeResult = await this.#upgradeService.upgrade(os, arch, installMethod);
44
+ if (!upgradeResult.ok) {
45
+ await printerService.error(`Failed to upgrade ${cliName}: ${upgradeResult.error?.message ?? "unknown error"}\n`, Icon.FAILURE);
46
+ return;
47
+ }
48
+ await printerService.print(`${cliName} upgraded (${upgradeResult.oldVersion} -> ${upgradeResult.newVersion})\n`, Icon.SUCCESS);
49
+ }
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "2.2.0",
3
+ "version": "2.3.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,19 +48,21 @@
48
48
  "build": "tsc -p tsconfig.build.json"
49
49
  },
50
50
  "dependencies": {
51
- "@flowscripter/dynamic-cli-framework-api": "^1.1.1",
51
+ "@flowscripter/dynamic-cli-framework-api": "^1.2.0",
52
52
  "@flowscripter/dynamic-plugin-framework": "1.11.0",
53
53
  "emphasize": "^7.0.0",
54
54
  "fastest-levenshtein": "^1.0.16",
55
55
  "figlet": "^1.11.0",
56
56
  "highlight.js": "^11.11.1",
57
57
  "prettier": "^3.9.4",
58
+ "semver": "^7.7.3",
58
59
  "supports-color": "^10.2.2",
59
60
  "supports-terminal-graphics": "^0.1.0"
60
61
  },
61
62
  "devDependencies": {
62
63
  "@types/bun": "^1.3.14",
63
64
  "@types/node": "^26.1.0",
65
+ "@types/semver": "^7.7.1",
64
66
  "oxfmt": "0.57.0",
65
67
  "oxlint": "1.72.0"
66
68
  },
@@ -52,6 +52,7 @@ import DefaultCompletionService from "../service/completion/DefaultCompletionSer
52
52
  import CompletionServiceProvider from "../service/completion/CompletionServiceProvider.ts";
53
53
  import ImagePrinterServiceProvider from "../service/imagePrinter/ImagePrinterServiceProvider.ts";
54
54
  import SpawnServiceProvider from "../service/spawn/SpawnServiceProvider.ts";
55
+ import UpgradeServiceProvider from "../service/upgrade/UpgradeServiceProvider.ts";
55
56
  const logger = getLogger("BaseCLI");
56
57
 
57
58
  /**
@@ -71,6 +72,8 @@ const logger = getLogger("BaseCLI");
71
72
  *
72
73
  * * {@link ImagePrinterServiceProvider}
73
74
  * * {@link SpawnServiceProvider}
75
+ * * {@link CompletionServiceProvider}
76
+ * * {@link UpgradeServiceProvider}
74
77
  *
75
78
  * `keyReader` is optional. If omitted, or if the stderr {@link Terminal} is not a TTY, prompting is
76
79
  * unavailable: {@link PrompterServiceProvider} (and {@link ArgumentPrompterServiceProvider}, if
@@ -133,6 +136,8 @@ export default class BaseCLI implements CLI {
133
136
  completionServiceEnabled: false,
134
137
  imagePrinterServiceEnabled: false,
135
138
  spawnServiceEnabled: false,
139
+ upgradeServiceEnabled: false,
140
+ upgradeLocationsConfig: { supportedPlatforms: [] },
136
141
  validateAllCommands: false,
137
142
  promptingEnabled: true,
138
143
  ...options,
@@ -276,6 +281,10 @@ export default class BaseCLI implements CLI {
276
281
  );
277
282
  }
278
283
 
284
+ if (this.#options.upgradeServiceEnabled) {
285
+ this.addServiceProvider(new UpgradeServiceProvider(6, this.#options.upgradeLocationsConfig));
286
+ }
287
+
279
288
  const configurationServiceProvider = new ConfigurationServiceProvider(
280
289
  90,
281
290
  this.#options.envVarsSupportEnabled,
@@ -1,3 +1,5 @@
1
+ import type { UpgradeLocationsConfig } from "../service/upgrade/UpgradeLocationsConfig.ts";
2
+
1
3
  export default interface BaseCLIFeatureOptions {
2
4
  readonly configFileSupportEnabled?: boolean;
3
5
  readonly envVarsSupportEnabled?: boolean;
@@ -7,6 +9,8 @@ export default interface BaseCLIFeatureOptions {
7
9
  readonly completionServiceEnabled?: boolean;
8
10
  readonly imagePrinterServiceEnabled?: boolean;
9
11
  readonly spawnServiceEnabled?: boolean;
12
+ readonly upgradeServiceEnabled?: boolean;
13
+ readonly upgradeLocationsConfig?: UpgradeLocationsConfig;
10
14
  readonly validateAllCommands?: boolean;
11
15
  readonly promptingEnabled?: boolean;
12
16
  }
@@ -2,6 +2,8 @@ import type { Context } from "@flowscripter/dynamic-cli-framework-api";
2
2
  import type { GlobalCommand } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
4
4
  import { PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
5
+ import { UPGRADE_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
6
+ import type { UpgradeService } from "@flowscripter/dynamic-cli-framework-api";
5
7
 
6
8
  /**
7
9
  * Implementation of a {@link GlobalCommand} which outputs the version of the CLI.
@@ -13,6 +15,16 @@ export default class VersionCommand implements GlobalCommand {
13
15
 
14
16
  public async execute(context: Context): Promise<void> {
15
17
  const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
16
- await printerService.print(`${context.cliConfig.version}\n`);
18
+
19
+ let line = context.cliConfig.version;
20
+ if (context.doesServiceExist(UPGRADE_SERVICE_ID)) {
21
+ const upgradeService = context.getServiceById(UPGRADE_SERVICE_ID) as UpgradeService;
22
+ const result = await upgradeService.checkForUpgrade().catch(() => undefined);
23
+ if (result?.updateAvailable) {
24
+ line += ` (${result.latestVersion} available, run '${context.cliConfig.name} upgrade')`;
25
+ }
26
+ }
27
+
28
+ await printerService.print(`${line}\n`);
17
29
  }
18
30
  }
@@ -7,6 +7,8 @@ import type ConfigurationServiceProvider from "../configuration/ConfigurationSer
7
7
  import type { AsciiBannerGeneratorService } from "@flowscripter/dynamic-cli-framework-api";
8
8
  import { ASCII_BANNER_GENERATOR_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
9
9
  import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
10
+ import { UPGRADE_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
11
+ import type { UpgradeService } from "@flowscripter/dynamic-cli-framework-api";
10
12
  export const BANNER_SERVICE_ID = "@flowscripter/dynamic-cli-framework/banner-service";
11
13
 
12
14
  /**
@@ -68,6 +70,17 @@ export default class BannerServiceProvider implements ServiceProvider {
68
70
  if (cliConfig.version.length > 0) {
69
71
  await printerService.info(` ${printerService.secondary("version: " + cliConfig.version)}\n`);
70
72
  }
73
+ if (context.doesServiceExist(UPGRADE_SERVICE_ID)) {
74
+ const upgradeService = context.getServiceById(UPGRADE_SERVICE_ID) as UpgradeService;
75
+ const result = await upgradeService.checkForUpgrade().catch(() => undefined);
76
+ if (result?.updateAvailable) {
77
+ await printerService.info(
78
+ ` ${printerService.secondary(
79
+ `(${result.latestVersion} available, run '${cliConfig.name} upgrade')`,
80
+ )}\n`,
81
+ );
82
+ }
83
+ }
71
84
  if (this.#configurationServiceProvider) {
72
85
  const configLocation = this.#configurationServiceProvider.configLocation;
73
86
  if (configLocation && configLocation.length > 0) {
@@ -70,6 +70,7 @@ export default class DefaultSpawnService implements SpawnService {
70
70
  try {
71
71
  proc = Bun.spawn([...command], {
72
72
  cwd: options.cwd,
73
+ stdin: stdio === "wrapped" ? "ignore" : "inherit",
73
74
  stdout: stdio === "wrapped" ? "pipe" : "inherit",
74
75
  stderr: stdio === "wrapped" ? "pipe" : "inherit",
75
76
  });