@flowscripter/dynamic-cli-framework 2.1.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 (53) hide show
  1. package/dist/index.d.ts +5 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +4 -1
  4. package/dist/src/cli/BaseCLI.d.ts +3 -0
  5. package/dist/src/cli/BaseCLI.d.ts.map +1 -1
  6. package/dist/src/cli/BaseCLI.js +14 -0
  7. package/dist/src/cli/BaseCLIFeatureOptions.d.ts +4 -0
  8. package/dist/src/cli/BaseCLIFeatureOptions.d.ts.map +1 -1
  9. package/dist/src/command/VersionCommand.d.ts.map +1 -1
  10. package/dist/src/command/VersionCommand.js +10 -1
  11. package/dist/src/service/banner/BannerServiceProvider.d.ts.map +1 -1
  12. package/dist/src/service/banner/BannerServiceProvider.js +8 -0
  13. package/dist/src/service/plugin/DefaultPluginService.d.ts +1 -1
  14. package/dist/src/service/plugin/DefaultPluginService.d.ts.map +1 -1
  15. package/dist/src/service/plugin/DefaultPluginServiceProvider.d.ts +3 -2
  16. package/dist/src/service/plugin/DefaultPluginServiceProvider.d.ts.map +1 -1
  17. package/dist/src/service/plugin/DefaultPluginServiceProvider.js +22 -2
  18. package/dist/src/service/plugin/SpawnInterfaceAdapter.d.ts +19 -0
  19. package/dist/src/service/plugin/SpawnInterfaceAdapter.d.ts.map +1 -0
  20. package/dist/src/service/plugin/SpawnInterfaceAdapter.js +40 -0
  21. package/dist/src/service/spawn/DefaultSpawnService.d.ts +7 -0
  22. package/dist/src/service/spawn/DefaultSpawnService.d.ts.map +1 -0
  23. package/dist/src/service/spawn/DefaultSpawnService.js +95 -0
  24. package/dist/src/service/spawn/SpawnServiceProvider.d.ts +12 -0
  25. package/dist/src/service/spawn/SpawnServiceProvider.d.ts.map +1 -0
  26. package/dist/src/service/spawn/SpawnServiceProvider.js +24 -0
  27. package/dist/src/service/upgrade/DefaultUpgradeService.d.ts +14 -0
  28. package/dist/src/service/upgrade/DefaultUpgradeService.d.ts.map +1 -0
  29. package/dist/src/service/upgrade/DefaultUpgradeService.js +312 -0
  30. package/dist/src/service/upgrade/UpgradeLocationsConfig.d.ts +51 -0
  31. package/dist/src/service/upgrade/UpgradeLocationsConfig.d.ts.map +1 -0
  32. package/dist/src/service/upgrade/UpgradeLocationsConfig.js +0 -0
  33. package/dist/src/service/upgrade/UpgradeServiceProvider.d.ts +13 -0
  34. package/dist/src/service/upgrade/UpgradeServiceProvider.d.ts.map +1 -0
  35. package/dist/src/service/upgrade/UpgradeServiceProvider.js +120 -0
  36. package/dist/src/service/upgrade/command/UpgradeSubCommand.d.ts +16 -0
  37. package/dist/src/service/upgrade/command/UpgradeSubCommand.d.ts.map +1 -0
  38. package/dist/src/service/upgrade/command/UpgradeSubCommand.js +50 -0
  39. package/index.ts +10 -1
  40. package/package.json +5 -3
  41. package/src/cli/BaseCLI.ts +16 -0
  42. package/src/cli/BaseCLIFeatureOptions.ts +5 -0
  43. package/src/command/VersionCommand.ts +13 -1
  44. package/src/service/banner/BannerServiceProvider.ts +13 -0
  45. package/src/service/plugin/DefaultPluginService.ts +1 -1
  46. package/src/service/plugin/DefaultPluginServiceProvider.ts +39 -3
  47. package/src/service/plugin/SpawnInterfaceAdapter.ts +62 -0
  48. package/src/service/spawn/DefaultSpawnService.ts +120 -0
  49. package/src/service/spawn/SpawnServiceProvider.ts +32 -0
  50. package/src/service/upgrade/DefaultUpgradeService.ts +379 -0
  51. package/src/service/upgrade/UpgradeLocationsConfig.ts +53 -0
  52. package/src/service/upgrade/UpgradeServiceProvider.ts +150 -0
  53. package/src/service/upgrade/command/UpgradeSubCommand.ts +85 -0
@@ -0,0 +1,312 @@
1
+ import process from "node:process";
2
+ import { mkdtemp } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { InstallMethod, SupportedArch, SupportedOs, } from "@flowscripter/dynamic-cli-framework-api";
6
+ import semver from "semver";
7
+ import getLogger from "../../util/logger.js";
8
+ const logger = getLogger("DefaultUpgradeService");
9
+ const OS_LABELS = {
10
+ [SupportedOs.LINUX]: "Linux",
11
+ [SupportedOs.MACOS]: "MacOS",
12
+ [SupportedOs.WINDOWS]: "Windows",
13
+ };
14
+ export default class DefaultUpgradeService {
15
+ #spawnService;
16
+ #config;
17
+ #cliConfig;
18
+ constructor(config, cliConfig) {
19
+ this.#config = config;
20
+ this.#cliConfig = cliConfig;
21
+ }
22
+ setDependencies(spawnService) {
23
+ this.#spawnService = spawnService;
24
+ }
25
+ detectOs() {
26
+ switch (process.platform) {
27
+ case "linux":
28
+ return SupportedOs.LINUX;
29
+ case "darwin":
30
+ return SupportedOs.MACOS;
31
+ case "win32":
32
+ return SupportedOs.WINDOWS;
33
+ default:
34
+ return undefined;
35
+ }
36
+ }
37
+ detectArch() {
38
+ switch (process.arch) {
39
+ case "x64":
40
+ return SupportedArch.X64;
41
+ case "arm64":
42
+ return SupportedArch.ARM64;
43
+ default:
44
+ return undefined;
45
+ }
46
+ }
47
+ async detectInstallMethod(os) {
48
+ if (os === SupportedOs.MACOS && this.#config.homebrew && (await this.#isHomebrewInstalled())) {
49
+ return InstallMethod.HOMEBREW;
50
+ }
51
+ if (os === SupportedOs.WINDOWS && this.#config.winget && (await this.#isWingetInstalled())) {
52
+ return InstallMethod.WINGET;
53
+ }
54
+ if (os === SupportedOs.LINUX && this.#config.linuxScript && this.#isLinuxScriptInstall()) {
55
+ return InstallMethod.LINUX_SCRIPT;
56
+ }
57
+ if (this.#config.githubRelease) {
58
+ return InstallMethod.GITHUB_RELEASE;
59
+ }
60
+ return undefined;
61
+ }
62
+ async checkForUpgrade(osOverride, archOverride, installMethodOverride) {
63
+ const os = osOverride ?? this.detectOs();
64
+ const arch = archOverride ?? this.detectArch();
65
+ if (!os || !arch || !this.#isPlatformSupported(os, arch)) {
66
+ return undefined;
67
+ }
68
+ const installMethod = installMethodOverride ?? (await this.detectInstallMethod(os));
69
+ if (!installMethod) {
70
+ return undefined;
71
+ }
72
+ const latestVersion = await this.#getLatestVersion(installMethod);
73
+ if (!latestVersion) {
74
+ return undefined;
75
+ }
76
+ const currentVersion = this.#cliConfig.version;
77
+ const coercedCurrent = semver.coerce(currentVersion);
78
+ const coercedLatest = semver.coerce(latestVersion);
79
+ if (!coercedCurrent || !coercedLatest) {
80
+ logger.debug(() => `Unable to compare versions '${currentVersion}' and '${latestVersion}'`);
81
+ return undefined;
82
+ }
83
+ return {
84
+ currentVersion,
85
+ latestVersion,
86
+ updateAvailable: semver.gt(coercedLatest, coercedCurrent),
87
+ os,
88
+ arch,
89
+ installMethod,
90
+ };
91
+ }
92
+ async upgrade(osOverride, archOverride, installMethodOverride) {
93
+ const oldVersion = this.#cliConfig.version;
94
+ const checkResult = await this.checkForUpgrade(osOverride, archOverride, installMethodOverride);
95
+ if (!checkResult) {
96
+ return {
97
+ ok: false,
98
+ oldVersion,
99
+ error: new Error("No upgrade location configured for the detected or requested platform"),
100
+ };
101
+ }
102
+ if (!this.#spawnService) {
103
+ return { ok: false, oldVersion, error: new Error("SpawnService is not available") };
104
+ }
105
+ try {
106
+ switch (checkResult.installMethod) {
107
+ case InstallMethod.LINUX_SCRIPT:
108
+ await this.#upgradeViaLinuxScript();
109
+ break;
110
+ case InstallMethod.HOMEBREW:
111
+ await this.#upgradeViaHomebrew();
112
+ break;
113
+ case InstallMethod.WINGET:
114
+ await this.#upgradeViaWinget();
115
+ break;
116
+ case InstallMethod.GITHUB_RELEASE:
117
+ await this.#upgradeViaGithubRelease(checkResult.os, checkResult.arch);
118
+ break;
119
+ }
120
+ return { ok: true, oldVersion, newVersion: checkResult.latestVersion };
121
+ }
122
+ catch (error) {
123
+ return { ok: false, oldVersion, error: error };
124
+ }
125
+ }
126
+ #isPlatformSupported(os, arch) {
127
+ return this.#config.supportedPlatforms.some((platform) => platform.os === os && platform.arch === arch);
128
+ }
129
+ async #isHomebrewInstalled() {
130
+ if (!this.#spawnService || !this.#config.homebrew) {
131
+ return false;
132
+ }
133
+ const result = await this.#spawnService.spawn(["brew", "list", "--versions", this.#config.homebrew.formula], { stdio: "wrapped", longRunning: false });
134
+ return result.ok;
135
+ }
136
+ async #isWingetInstalled() {
137
+ if (!this.#spawnService || !this.#config.winget) {
138
+ return false;
139
+ }
140
+ const result = await this.#spawnService.spawn(["winget", "list", "--id", this.#config.winget.packageId], { stdio: "wrapped", longRunning: false });
141
+ return result.ok;
142
+ }
143
+ #isLinuxScriptInstall() {
144
+ return process.execPath.startsWith("/usr/local/bin/");
145
+ }
146
+ async #getLatestVersion(installMethod) {
147
+ switch (installMethod) {
148
+ case InstallMethod.GITHUB_RELEASE:
149
+ case InstallMethod.LINUX_SCRIPT:
150
+ return this.#getLatestGithubReleaseVersion();
151
+ case InstallMethod.HOMEBREW:
152
+ return this.#getLatestHomebrewVersion();
153
+ case InstallMethod.WINGET:
154
+ return this.#getLatestWingetVersion();
155
+ default:
156
+ return undefined;
157
+ }
158
+ }
159
+ async #getLatestGithubReleaseVersion() {
160
+ if (!this.#config.githubRelease) {
161
+ return undefined;
162
+ }
163
+ const { owner, repo } = this.#config.githubRelease;
164
+ try {
165
+ const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`);
166
+ if (!response.ok) {
167
+ return undefined;
168
+ }
169
+ const data = (await response.json());
170
+ return data.tag_name?.replace(/^v/, "");
171
+ }
172
+ catch (error) {
173
+ logger.debug(() => `Failed to fetch latest GitHub release for ${owner}/${repo}: ${error}`);
174
+ return undefined;
175
+ }
176
+ }
177
+ async #getLatestHomebrewVersion() {
178
+ if (!this.#config.homebrew) {
179
+ return undefined;
180
+ }
181
+ const { tap, formula } = this.#config.homebrew;
182
+ const [tapOwner, tapName] = tap.split("/");
183
+ if (!tapOwner || !tapName) {
184
+ return undefined;
185
+ }
186
+ try {
187
+ const response = await fetch(`https://raw.githubusercontent.com/${tapOwner}/homebrew-${tapName}/main/${formula}.rb`);
188
+ if (!response.ok) {
189
+ return undefined;
190
+ }
191
+ const text = await response.text();
192
+ return /version\s+"v?([^"]+)"/.exec(text)?.[1];
193
+ }
194
+ catch (error) {
195
+ logger.debug(() => `Failed to fetch homebrew formula for ${tap}/${formula}: ${error}`);
196
+ return undefined;
197
+ }
198
+ }
199
+ async #getLatestWingetVersion() {
200
+ if (!this.#spawnService || !this.#config.winget) {
201
+ return undefined;
202
+ }
203
+ const lines = [];
204
+ const result = await this.#spawnService.spawn(["winget", "show", "--id", this.#config.winget.packageId], {
205
+ stdio: "wrapped",
206
+ longRunning: false,
207
+ onOutput: (line) => lines.push(line),
208
+ });
209
+ if (!result.ok) {
210
+ return undefined;
211
+ }
212
+ for (const line of lines) {
213
+ const match = /Version:\s*(\S+)/.exec(line);
214
+ if (match?.[1]) {
215
+ return match[1];
216
+ }
217
+ }
218
+ return undefined;
219
+ }
220
+ async #upgradeViaLinuxScript() {
221
+ const { scriptUrl } = this.#config.linuxScript;
222
+ const result = await this.#spawnService.spawn(["sh", "-c", `curl -fsSL ${scriptUrl} | sh`]);
223
+ if (!result.ok) {
224
+ throw new Error(`Install script failed: ${result.error?.message ?? `exit code ${result.exitCode}`}`);
225
+ }
226
+ }
227
+ async #upgradeViaHomebrew() {
228
+ const { tap, formula } = this.#config.homebrew;
229
+ const result = await this.#spawnService.spawn(["brew", "upgrade", `${tap}/${formula}`]);
230
+ if (!result.ok) {
231
+ throw new Error(`brew upgrade failed: ${result.error?.message ?? `exit code ${result.exitCode}`}`);
232
+ }
233
+ }
234
+ async #upgradeViaWinget() {
235
+ const { packageId } = this.#config.winget;
236
+ const result = await this.#spawnService.spawn([
237
+ "winget",
238
+ "upgrade",
239
+ "--id",
240
+ packageId,
241
+ "--silent",
242
+ "--accept-package-agreements",
243
+ "--accept-source-agreements",
244
+ ]);
245
+ if (!result.ok) {
246
+ throw new Error(`winget upgrade failed: ${result.error?.message ?? `exit code ${result.exitCode}`}`);
247
+ }
248
+ }
249
+ async #upgradeViaGithubRelease(os, arch) {
250
+ const { owner, repo, assetPattern } = this.#config.githubRelease;
251
+ const archLabel = os === SupportedOs.MACOS ? "aarch64" : arch === SupportedArch.X64 ? "x64" : "arm64";
252
+ const assetName = assetPattern.replace("{os}", OS_LABELS[os]).replace("{arch}", archLabel);
253
+ 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}`);
257
+ }
258
+ const tmpDir = await mkdtemp(join(tmpdir(), "upgrade-"));
259
+ const archivePath = join(tmpDir, assetName);
260
+ await Bun.write(archivePath, await response.arrayBuffer());
261
+ const currentExecutable = process.execPath;
262
+ if (os === SupportedOs.WINDOWS) {
263
+ const extractResult = await this.#spawnService.spawn([
264
+ "powershell",
265
+ "-Command",
266
+ `Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpDir}' -Force`,
267
+ ]);
268
+ if (!extractResult.ok) {
269
+ throw new Error("Failed to extract release archive");
270
+ }
271
+ const extractedBinary = join(tmpDir, `${this.#cliConfig.name}.exe`);
272
+ const oldPath = `${currentExecutable}.old.exe`;
273
+ const moveResult = await this.#spawnService.spawn([
274
+ "cmd",
275
+ "/c",
276
+ "move",
277
+ "/y",
278
+ currentExecutable,
279
+ oldPath,
280
+ ]);
281
+ if (!moveResult.ok) {
282
+ throw new Error("Failed to move current executable aside");
283
+ }
284
+ const copyResult = await this.#spawnService.spawn([
285
+ "cmd",
286
+ "/c",
287
+ "copy",
288
+ "/y",
289
+ extractedBinary,
290
+ currentExecutable,
291
+ ]);
292
+ if (!copyResult.ok) {
293
+ throw new Error("Failed to copy new executable into place");
294
+ }
295
+ }
296
+ else {
297
+ const extractResult = await this.#spawnService.spawn([
298
+ "unzip",
299
+ "-o",
300
+ archivePath,
301
+ "-d",
302
+ tmpDir,
303
+ ]);
304
+ if (!extractResult.ok) {
305
+ throw new Error("Failed to extract release archive");
306
+ }
307
+ const extractedBinary = join(tmpDir, this.#cliConfig.name);
308
+ await Bun.write(currentExecutable, Bun.file(extractedBinary));
309
+ await this.#spawnService.spawn(["chmod", "+x", currentExecutable]);
310
+ }
311
+ }
312
+ }
@@ -0,0 +1,51 @@
1
+ import type { SupportedArch, SupportedOs } from "@flowscripter/dynamic-cli-framework-api";
2
+ /**
3
+ * Location of a manual GitHub release asset, and how to determine the latest available version.
4
+ */
5
+ export interface GithubReleaseLocation {
6
+ readonly owner: string;
7
+ readonly repo: string;
8
+ /**
9
+ * Asset filename pattern for a release, e.g. `"example-cli_{os}_{arch}.zip"`.
10
+ * `{os}` is substituted with `Linux`, `MacOS` or `Windows`; `{arch}` with `x64`, `arm64` or
11
+ * `aarch64` (macOS).
12
+ */
13
+ readonly assetPattern: string;
14
+ }
15
+ /**
16
+ * Location of a Linux install script, e.g. as documented for `curl -fsSL <scriptUrl> | sh`.
17
+ */
18
+ export interface LinuxScriptLocation {
19
+ readonly scriptUrl: string;
20
+ }
21
+ /**
22
+ * Location of a Homebrew tap formula, e.g. `brew install <tap>/<formula>`.
23
+ */
24
+ export interface HomebrewLocation {
25
+ readonly tap: string;
26
+ readonly formula: string;
27
+ }
28
+ /**
29
+ * Identifier of a Winget package, e.g. `winget install <packageId>`.
30
+ */
31
+ export interface WingetLocation {
32
+ readonly packageId: string;
33
+ }
34
+ /**
35
+ * CLI-author supplied configuration describing which os/arch combinations a CLI supports
36
+ * upgrading, and where to find the latest release for each supported install method.
37
+ *
38
+ * An install method is only usable for an os/arch combination if both its config block is
39
+ * present here AND that combination appears in {@link supportedPlatforms}.
40
+ */
41
+ export interface UpgradeLocationsConfig {
42
+ readonly supportedPlatforms: ReadonlyArray<{
43
+ os: SupportedOs;
44
+ arch: SupportedArch;
45
+ }>;
46
+ readonly githubRelease?: GithubReleaseLocation;
47
+ readonly linuxScript?: LinuxScriptLocation;
48
+ readonly homebrew?: HomebrewLocation;
49
+ readonly winget?: WingetLocation;
50
+ }
51
+ //# sourceMappingURL=UpgradeLocationsConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UpgradeLocationsConfig.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/UpgradeLocationsConfig.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAE1F;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,kBAAkB,EAAE,aAAa,CAAC;QAAE,EAAE,EAAE,WAAW,CAAC;QAAC,IAAI,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC;IACrF,QAAQ,CAAC,aAAa,CAAC,EAAE,qBAAqB,CAAC;IAC/C,QAAQ,CAAC,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;CAClC"}
@@ -0,0 +1,13 @@
1
+ import type { ServiceInfo, ServiceProvider } from "@flowscripter/dynamic-cli-framework-api";
2
+ import type { Context } from "@flowscripter/dynamic-cli-framework-api";
3
+ import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
4
+ import type { UpgradeLocationsConfig } from "./UpgradeLocationsConfig.ts";
5
+ export default class UpgradeServiceProvider implements ServiceProvider {
6
+ #private;
7
+ readonly serviceId: string;
8
+ readonly servicePriority: number;
9
+ constructor(servicePriority: number, config: UpgradeLocationsConfig);
10
+ getServiceInfo(cliConfig: CLIConfig): Promise<ServiceInfo>;
11
+ initService(context: Context): Promise<void>;
12
+ }
13
+ //# sourceMappingURL=UpgradeServiceProvider.d.ts.map
@@ -0,0 +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"}
@@ -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/index.ts CHANGED
@@ -78,6 +78,15 @@ export type { ShutdownService } from "@flowscripter/dynamic-cli-framework-api";
78
78
  export { default as DefaultShutdownService } from "./src/service/shutdown/DefaultShutdownService.ts";
79
79
  export { default as ShutdownServiceProvider } from "./src/service/shutdown/ShutdownServiceProvider.ts";
80
80
 
81
+ export { SPAWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
82
+ export type {
83
+ SpawnOptions,
84
+ SpawnResult,
85
+ SpawnService,
86
+ } from "@flowscripter/dynamic-cli-framework-api";
87
+ export { default as DefaultSpawnService } from "./src/service/spawn/DefaultSpawnService.ts";
88
+ export { default as SpawnServiceProvider } from "./src/service/spawn/SpawnServiceProvider.ts";
89
+
81
90
  export { SYNTAX_HIGHLIGHTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
82
91
  export type { SyntaxHighlighterService } from "@flowscripter/dynamic-cli-framework-api";
83
92
  export type { ColorScheme } from "@flowscripter/dynamic-cli-framework-api";
@@ -192,7 +201,7 @@ export { default as TtyKeyReader } from "./src/terminal/TtyKeyReader.ts";
192
201
 
193
202
  // Convenience functions
194
203
  export {
204
+ launchDynamicPluginMultiCommandCLI,
195
205
  launchMultiCommandCLI,
196
206
  launchSingleCommandCLI,
197
- launchDynamicPluginMultiCommandCLI,
198
207
  } from "./src/launcher.ts";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "2.1.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.0.1",
52
- "@flowscripter/dynamic-plugin-framework": "1.10.1",
51
+ "@flowscripter/dynamic-cli-framework-api": "^1.2.0",
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
  },