@flowscripter/dynamic-cli-framework 6.0.0 → 7.0.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.
- package/README.md +2 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/src/cli/BaseCLI.d.ts +8 -0
- package/dist/src/cli/BaseCLI.d.ts.map +1 -1
- package/dist/src/cli/BaseCLI.js +37 -4
- package/dist/src/launcher.d.ts +3 -3
- package/dist/src/launcher.d.ts.map +1 -1
- package/dist/src/launcher.js +6 -4
- package/dist/src/runtime/lifecycle/PriorityTaskList.d.ts +22 -0
- package/dist/src/runtime/lifecycle/PriorityTaskList.d.ts.map +1 -0
- package/dist/src/runtime/lifecycle/PriorityTaskList.js +23 -0
- package/dist/src/runtime/runner.d.ts +15 -7
- package/dist/src/runtime/runner.d.ts.map +1 -1
- package/dist/src/runtime/runner.js +82 -53
- package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts +26 -82
- package/dist/src/service/configuration/ConfigurationServiceProvider.d.ts.map +1 -1
- package/dist/src/service/configuration/ConfigurationServiceProvider.js +68 -163
- package/dist/src/service/configuration/DefaultKeyValueService.d.ts +6 -3
- package/dist/src/service/configuration/DefaultKeyValueService.d.ts.map +1 -1
- package/dist/src/service/configuration/DefaultKeyValueService.js +7 -24
- package/dist/src/service/configuration/DefaultSecretService.d.ts +1 -3
- package/dist/src/service/configuration/DefaultSecretService.d.ts.map +1 -1
- package/dist/src/service/configuration/DefaultSecretService.js +2 -10
- package/dist/src/service/configuration/KeyValueServiceProvider.d.ts +105 -0
- package/dist/src/service/configuration/KeyValueServiceProvider.d.ts.map +1 -0
- package/dist/src/service/configuration/KeyValueServiceProvider.js +201 -0
- package/dist/src/service/configuration/command/ConfigCommand.d.ts +4 -5
- package/dist/src/service/configuration/command/ConfigCommand.d.ts.map +1 -1
- package/dist/src/service/configuration/command/ConfigCommand.js +7 -6
- package/dist/src/service/configuration/command/DumpConfigCommand.d.ts +7 -3
- package/dist/src/service/configuration/command/DumpConfigCommand.d.ts.map +1 -1
- package/dist/src/service/configuration/command/DumpConfigCommand.js +10 -5
- package/dist/src/service/fetch/DefaultFetchService.d.ts +2 -2
- package/dist/src/service/fetch/DefaultFetchService.d.ts.map +1 -1
- package/dist/src/service/fetch/DefaultFetchService.js +17 -12
- package/dist/src/service/fetch/FetchServiceProvider.d.ts.map +1 -1
- package/dist/src/service/fetch/FetchServiceProvider.js +2 -3
- package/dist/src/service/printer/PrinterServiceProvider.d.ts.map +1 -1
- package/dist/src/service/printer/PrinterServiceProvider.js +11 -7
- package/dist/src/service/printer/command/DarkModeCommand.d.ts +3 -5
- package/dist/src/service/printer/command/DarkModeCommand.d.ts.map +1 -1
- package/dist/src/service/printer/command/DarkModeCommand.js +6 -6
- package/dist/src/service/printer/command/LogLevelCommand.d.ts +3 -5
- package/dist/src/service/printer/command/LogLevelCommand.d.ts.map +1 -1
- package/dist/src/service/printer/command/LogLevelCommand.js +9 -9
- package/dist/src/service/printer/command/NoColorCommand.d.ts +3 -5
- package/dist/src/service/printer/command/NoColorCommand.d.ts.map +1 -1
- package/dist/src/service/printer/command/NoColorCommand.js +6 -6
- package/dist/src/service/printer/command/NoHyperlinksCommand.d.ts +2 -4
- package/dist/src/service/printer/command/NoHyperlinksCommand.d.ts.map +1 -1
- package/dist/src/service/printer/command/NoHyperlinksCommand.js +5 -5
- package/dist/src/service/prompter/PrompterServiceProvider.js +1 -1
- package/dist/src/service/prompter/command/NoPromptCommand.d.ts +2 -4
- package/dist/src/service/prompter/command/NoPromptCommand.d.ts.map +1 -1
- package/dist/src/service/prompter/command/NoPromptCommand.js +5 -5
- package/dist/src/service/shutdown/DefaultShutdownService.d.ts +4 -3
- package/dist/src/service/shutdown/DefaultShutdownService.d.ts.map +1 -1
- package/dist/src/service/shutdown/DefaultShutdownService.js +4 -3
- package/dist/src/service/shutdown/ShutdownServiceProvider.js +2 -2
- package/dist/src/service/spawn/DefaultSpawnService.d.ts +2 -2
- package/dist/src/service/spawn/DefaultSpawnService.d.ts.map +1 -1
- package/dist/src/service/spawn/DefaultSpawnService.js +17 -14
- package/dist/src/service/spawn/SpawnServiceProvider.d.ts.map +1 -1
- package/dist/src/service/spawn/SpawnServiceProvider.js +1 -4
- package/dist/src/service/startup/DefaultStartupService.d.ts +10 -0
- package/dist/src/service/startup/DefaultStartupService.d.ts.map +1 -0
- package/dist/src/service/startup/DefaultStartupService.js +13 -0
- package/dist/src/service/startup/StartupServiceProvider.d.ts +17 -0
- package/dist/src/service/startup/StartupServiceProvider.d.ts.map +1 -0
- package/dist/src/service/startup/StartupServiceProvider.js +26 -0
- package/dist/src/service/upgrade/DefaultUpgradeService.d.ts +10 -4
- package/dist/src/service/upgrade/DefaultUpgradeService.d.ts.map +1 -1
- package/dist/src/service/upgrade/DefaultUpgradeService.js +156 -37
- package/dist/src/service/upgrade/UpgradeServiceProvider.d.ts +10 -0
- package/dist/src/service/upgrade/UpgradeServiceProvider.d.ts.map +1 -1
- package/dist/src/service/upgrade/UpgradeServiceProvider.js +25 -6
- package/dist/src/service/upgrade/command/UpgradeSubCommand.d.ts +0 -3
- package/dist/src/service/upgrade/command/UpgradeSubCommand.d.ts.map +1 -1
- package/dist/src/service/upgrade/command/UpgradeSubCommand.js +5 -13
- package/dist/src/startup/banner/bannerStartupTask.d.ts +17 -0
- package/dist/src/startup/banner/bannerStartupTask.d.ts.map +1 -0
- package/dist/src/startup/banner/bannerStartupTask.js +66 -0
- package/dist/src/{service → startup}/banner/command/NoBannerCommand.d.ts +8 -2
- package/dist/src/startup/banner/command/NoBannerCommand.d.ts.map +1 -0
- package/dist/src/{service → startup}/banner/command/NoBannerCommand.js +4 -4
- package/index.ts +19 -3
- package/package.json +7 -7
- package/src/cli/BaseCLI.ts +49 -3
- package/src/launcher.ts +7 -2
- package/src/runtime/lifecycle/PriorityTaskList.ts +34 -0
- package/src/runtime/runner.ts +106 -67
- package/src/service/configuration/ConfigurationServiceProvider.ts +80 -180
- package/src/service/configuration/DefaultKeyValueService.ts +8 -27
- package/src/service/configuration/DefaultSecretService.ts +3 -13
- package/src/service/configuration/KeyValueServiceProvider.ts +246 -0
- package/src/service/configuration/command/ConfigCommand.ts +12 -11
- package/src/service/configuration/command/DumpConfigCommand.ts +10 -9
- package/src/service/fetch/DefaultFetchService.ts +18 -12
- package/src/service/fetch/FetchServiceProvider.ts +2 -4
- package/src/service/printer/PrinterServiceProvider.ts +11 -7
- package/src/service/printer/command/DarkModeCommand.ts +7 -8
- package/src/service/printer/command/LogLevelCommand.ts +10 -11
- package/src/service/printer/command/NoColorCommand.ts +7 -8
- package/src/service/printer/command/NoHyperlinksCommand.ts +6 -7
- package/src/service/prompter/PrompterServiceProvider.ts +1 -1
- package/src/service/prompter/command/NoPromptCommand.ts +6 -7
- package/src/service/shutdown/DefaultShutdownService.ts +5 -4
- package/src/service/shutdown/ShutdownServiceProvider.ts +2 -2
- package/src/service/spawn/DefaultSpawnService.ts +18 -14
- package/src/service/spawn/SpawnServiceProvider.ts +1 -5
- package/src/service/startup/DefaultStartupService.ts +17 -0
- package/src/service/startup/StartupServiceProvider.ts +36 -0
- package/src/service/upgrade/DefaultUpgradeService.ts +202 -42
- package/src/service/upgrade/UpgradeServiceProvider.ts +31 -8
- package/src/service/upgrade/command/UpgradeSubCommand.ts +5 -15
- package/src/startup/banner/bannerStartupTask.ts +96 -0
- package/src/{service → startup}/banner/command/NoBannerCommand.ts +12 -5
- package/dist/src/service/banner/BannerServiceProvider.d.ts +0 -29
- package/dist/src/service/banner/BannerServiceProvider.d.ts.map +0 -1
- package/dist/src/service/banner/BannerServiceProvider.js +0 -69
- package/dist/src/service/banner/command/NoBannerCommand.d.ts.map +0 -1
- package/src/service/banner/BannerServiceProvider.ts +0 -89
|
@@ -57,8 +57,8 @@ export default class ShutdownServiceProvider implements ServiceProvider {
|
|
|
57
57
|
process.removeListener("beforeExit", ShutdownServiceProvider.shutdown);
|
|
58
58
|
process.removeListener("SIGINT", ShutdownServiceProvider.onInterrupt);
|
|
59
59
|
process.removeListener("SIGTERM", ShutdownServiceProvider.onTerminate);
|
|
60
|
-
for
|
|
61
|
-
await
|
|
60
|
+
for (const task of DefaultShutdownService.taskList.sorted()) {
|
|
61
|
+
await task.run();
|
|
62
62
|
}
|
|
63
63
|
} catch (error) {
|
|
64
64
|
logger.error("shutdown error: %s", (error as Error).message);
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
import type {
|
|
3
|
+
Context,
|
|
3
4
|
PrinterService,
|
|
4
5
|
ShutdownService,
|
|
5
6
|
SpawnOptions,
|
|
6
7
|
SpawnResult,
|
|
7
8
|
SpawnService,
|
|
8
9
|
} from "@flowscripter/dynamic-cli-framework-api";
|
|
10
|
+
import { PRINTER_SERVICE_ID, SHUTDOWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
9
11
|
import getLogger from "../../util/logger.ts";
|
|
10
12
|
|
|
11
13
|
const logger = getLogger("DefaultSpawnService");
|
|
@@ -33,12 +35,10 @@ export function resolveForPlatform(command: ReadonlyArray<string>): string[] {
|
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
export default class DefaultSpawnService implements SpawnService {
|
|
36
|
-
#
|
|
37
|
-
#shutdownService: ShutdownService | undefined;
|
|
38
|
+
#context: Context | undefined;
|
|
38
39
|
|
|
39
|
-
public
|
|
40
|
-
this.#
|
|
41
|
-
this.#shutdownService = shutdownService;
|
|
40
|
+
public setContext(context: Context): void {
|
|
41
|
+
this.#context = context;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
async #terminate(
|
|
@@ -90,11 +90,11 @@ export default class DefaultSpawnService implements SpawnService {
|
|
|
90
90
|
command: ReadonlyArray<string>,
|
|
91
91
|
options: SpawnOptions = {},
|
|
92
92
|
): Promise<SpawnResult> {
|
|
93
|
-
if (this.#
|
|
94
|
-
throw new Error("DefaultSpawnService.spawn() called before
|
|
93
|
+
if (this.#context === undefined) {
|
|
94
|
+
throw new Error("DefaultSpawnService.spawn() called before setContext()");
|
|
95
95
|
}
|
|
96
|
-
const printerService = this.#
|
|
97
|
-
const shutdownService = this.#
|
|
96
|
+
const printerService = this.#context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
|
|
97
|
+
const shutdownService = this.#context.getServiceById(SHUTDOWN_SERVICE_ID) as ShutdownService;
|
|
98
98
|
const mode = options.mode ?? "inherit";
|
|
99
99
|
const longRunning = options.longRunning ?? true;
|
|
100
100
|
|
|
@@ -122,11 +122,15 @@ export default class DefaultSpawnService implements SpawnService {
|
|
|
122
122
|
shutdownService.enterLongRunningMode();
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
shutdownService.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
shutdownService.registerTask({
|
|
126
|
+
id: `spawn:${command.join(" ")}`,
|
|
127
|
+
priority: 0,
|
|
128
|
+
run: async () => {
|
|
129
|
+
if (settled) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
await this.#terminate(proc, command);
|
|
133
|
+
},
|
|
130
134
|
});
|
|
131
135
|
|
|
132
136
|
// These are intentionally not awaited here - they only complete once the child's stdout/
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { ServiceInfo, ServiceProvider } from "@flowscripter/dynamic-cli-framework-api";
|
|
2
2
|
import type { Context } from "@flowscripter/dynamic-cli-framework-api";
|
|
3
3
|
import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
|
|
4
|
-
import type { PrinterService, ShutdownService } from "@flowscripter/dynamic-cli-framework-api";
|
|
5
|
-
import { PRINTER_SERVICE_ID, SHUTDOWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
6
4
|
import { SPAWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
7
5
|
import DefaultSpawnService from "./DefaultSpawnService.ts";
|
|
8
6
|
|
|
@@ -24,9 +22,7 @@ export default class SpawnServiceProvider implements ServiceProvider {
|
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
public initService(context: Context): Promise<void> {
|
|
27
|
-
|
|
28
|
-
const shutdownService = context.getServiceById(SHUTDOWN_SERVICE_ID) as ShutdownService;
|
|
29
|
-
this.#spawnService!.setDependencies(printerService, shutdownService);
|
|
25
|
+
this.#spawnService!.setContext(context);
|
|
30
26
|
return Promise.resolve();
|
|
31
27
|
}
|
|
32
28
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { StartupService, StartupTask } from "@flowscripter/dynamic-cli-framework-api";
|
|
2
|
+
import PriorityTaskList from "../../runtime/lifecycle/PriorityTaskList.ts";
|
|
3
|
+
|
|
4
|
+
export default class DefaultStartupService implements StartupService {
|
|
5
|
+
readonly #taskList = new PriorityTaskList<StartupTask>();
|
|
6
|
+
|
|
7
|
+
public registerTask(task: StartupTask): void {
|
|
8
|
+
this.#taskList.add(task);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Return all registered {@link StartupTask} instances, in descending priority order.
|
|
13
|
+
*/
|
|
14
|
+
public getTasks(): ReadonlyArray<StartupTask> {
|
|
15
|
+
return this.#taskList.sorted();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CLIConfig,
|
|
3
|
+
Context,
|
|
4
|
+
ServiceInfo,
|
|
5
|
+
ServiceProvider,
|
|
6
|
+
} from "@flowscripter/dynamic-cli-framework-api";
|
|
7
|
+
import { STARTUP_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
8
|
+
import DefaultStartupService from "./DefaultStartupService.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Minimal internal {@link ServiceProvider} which registers a {@link DefaultStartupService} in the
|
|
12
|
+
* {@link Context}. There is no natural per-provider work for this service to do on init - tasks
|
|
13
|
+
* registered against it are run by the {@link run} orchestration loop directly, not via
|
|
14
|
+
* {@link initService}.
|
|
15
|
+
*/
|
|
16
|
+
export default class StartupServiceProvider implements ServiceProvider {
|
|
17
|
+
readonly serviceId: string = STARTUP_SERVICE_ID;
|
|
18
|
+
readonly servicePriority: number;
|
|
19
|
+
readonly startupService: DefaultStartupService;
|
|
20
|
+
|
|
21
|
+
public constructor(servicePriority: number) {
|
|
22
|
+
this.servicePriority = servicePriority;
|
|
23
|
+
this.startupService = new DefaultStartupService();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
|
|
27
|
+
return Promise.resolve({
|
|
28
|
+
service: this.startupService,
|
|
29
|
+
commands: [],
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public initService(_context: Context): Promise<void> {
|
|
34
|
+
return Promise.resolve();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,18 +1,27 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
2
3
|
import { mkdtemp, rename, rm } from "node:fs/promises";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import type {
|
|
6
7
|
CLIConfig,
|
|
8
|
+
Context,
|
|
7
9
|
FetchService,
|
|
10
|
+
KeyValueService,
|
|
8
11
|
PrinterService,
|
|
12
|
+
SettableValueNode,
|
|
9
13
|
SpawnResult,
|
|
10
14
|
SpawnService,
|
|
11
15
|
UpgradeCheckResult,
|
|
12
16
|
UpgradeResult,
|
|
17
|
+
ValueNode,
|
|
13
18
|
} from "@flowscripter/dynamic-cli-framework-api";
|
|
14
19
|
import {
|
|
20
|
+
FETCH_SERVICE_ID,
|
|
15
21
|
InstallMethod,
|
|
22
|
+
KEY_VALUE_SERVICE_ID,
|
|
23
|
+
PRINTER_SERVICE_ID,
|
|
24
|
+
SPAWN_SERVICE_ID,
|
|
16
25
|
SupportedArch,
|
|
17
26
|
SupportedOs,
|
|
18
27
|
type UpgradeService,
|
|
@@ -23,11 +32,41 @@ import getLogger from "../../util/logger.ts";
|
|
|
23
32
|
|
|
24
33
|
const logger = getLogger("DefaultUpgradeService");
|
|
25
34
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
35
|
+
// KeyValueService key caching the resolved InstallMethod (see detectInstallMethod()). Stored
|
|
36
|
+
// value shape: { method: InstallMethod, checkedAt: number } where checkedAt is Date.now() at
|
|
37
|
+
// detection time - refreshed on the same CACHE_TTL_MILLIS cycle as the latest-version cache below,
|
|
38
|
+
// rather than trusted indefinitely, so a reinstall via a different method is eventually picked up
|
|
39
|
+
// even outside the no-spawn signals (Cellar path, linux script prefix) that self-heal immediately.
|
|
40
|
+
const INSTALL_METHOD_CACHE_KEY = "install-method";
|
|
41
|
+
|
|
42
|
+
// KeyValueService key prefix caching a #getLatestVersion() lookup per install method, e.g.
|
|
43
|
+
// "latest-version:homebrew". Stored value shape: { version: string, checkedAt: number } where
|
|
44
|
+
// checkedAt is Date.now() at lookup time - see CACHE_TTL_MILLIS.
|
|
45
|
+
const LATEST_VERSION_CACHE_KEY_PREFIX = "latest-version:";
|
|
46
|
+
|
|
47
|
+
// How long a cached install-method or latest-version lookup is trusted before a fresh
|
|
48
|
+
// spawn/network lookup runs again - long enough to remove the cost from routine invocations,
|
|
49
|
+
// short enough that a genuine reinstall or new release surfaces within about a day.
|
|
50
|
+
const CACHE_TTL_MILLIS = 24 * 60 * 60 * 1000;
|
|
51
|
+
|
|
52
|
+
interface CachedInstallMethod {
|
|
53
|
+
method: InstallMethod;
|
|
54
|
+
checkedAt: number;
|
|
55
|
+
[key: string]: ValueNode;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface CachedLatestVersion {
|
|
59
|
+
version: string;
|
|
60
|
+
checkedAt: number;
|
|
61
|
+
[key: string]: ValueNode;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// KeyValueService key holding the last checkForUpgrade() result, refreshed opportunistically by
|
|
65
|
+
// UpgradeServiceProvider's background StartupTask and by the `upgrade` command - shared by both so
|
|
66
|
+
// there's one cache, not two. The banner task only ever reads this key (kv.has()/kv.get()) - it
|
|
67
|
+
// never calls checkForUpgrade()/getUpgradeCheckResult() live, since checking live would stall
|
|
68
|
+
// startup on the network/spawn calls checkForUpgrade() makes.
|
|
69
|
+
export const UPGRADE_CHECK_CACHE_KEY = "upgrade-check-result";
|
|
31
70
|
|
|
32
71
|
type VersionLookupResult =
|
|
33
72
|
| { readonly ok: true; readonly version: string }
|
|
@@ -52,9 +91,7 @@ const OS_LABELS: Record<SupportedOs, string> = {
|
|
|
52
91
|
};
|
|
53
92
|
|
|
54
93
|
export default class DefaultUpgradeService implements UpgradeService {
|
|
55
|
-
#
|
|
56
|
-
#fetchService: FetchService | undefined;
|
|
57
|
-
#printerService: PrinterService | undefined;
|
|
94
|
+
#context: Context | undefined;
|
|
58
95
|
#upgradeCheckPromise: Promise<UpgradeCheckResult> | undefined;
|
|
59
96
|
readonly #config: UpgradeLocationsConfig;
|
|
60
97
|
readonly #cliConfig: CLIConfig;
|
|
@@ -64,14 +101,32 @@ export default class DefaultUpgradeService implements UpgradeService {
|
|
|
64
101
|
this.#cliConfig = cliConfig;
|
|
65
102
|
}
|
|
66
103
|
|
|
67
|
-
public
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
):
|
|
72
|
-
this.#
|
|
73
|
-
|
|
74
|
-
|
|
104
|
+
public setContext(context: Context): void {
|
|
105
|
+
this.#context = context;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
get #spawnService(): SpawnService | undefined {
|
|
109
|
+
return this.#context?.doesServiceExist(SPAWN_SERVICE_ID)
|
|
110
|
+
? (this.#context.getServiceById(SPAWN_SERVICE_ID) as SpawnService)
|
|
111
|
+
: undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
get #fetchService(): FetchService | undefined {
|
|
115
|
+
return this.#context?.doesServiceExist(FETCH_SERVICE_ID)
|
|
116
|
+
? (this.#context.getServiceById(FETCH_SERVICE_ID) as FetchService)
|
|
117
|
+
: undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get #printerService(): PrinterService | undefined {
|
|
121
|
+
return this.#context?.doesServiceExist(PRINTER_SERVICE_ID)
|
|
122
|
+
? (this.#context.getServiceById(PRINTER_SERVICE_ID) as PrinterService)
|
|
123
|
+
: undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
get #keyValueService(): KeyValueService | undefined {
|
|
127
|
+
return this.#context?.doesServiceExist(KEY_VALUE_SERVICE_ID)
|
|
128
|
+
? (this.#context.getServiceById(KEY_VALUE_SERVICE_ID) as KeyValueService)
|
|
129
|
+
: undefined;
|
|
75
130
|
}
|
|
76
131
|
|
|
77
132
|
// Mirrors SpawnInterfaceAdapter's plugin:add/plugin:remove pattern: wrap a spawned command's
|
|
@@ -108,15 +163,12 @@ export default class DefaultUpgradeService implements UpgradeService {
|
|
|
108
163
|
return result;
|
|
109
164
|
}
|
|
110
165
|
|
|
111
|
-
public getUpgradeCheckResult(
|
|
166
|
+
public getUpgradeCheckResult(): Promise<UpgradeCheckResult> {
|
|
112
167
|
if (!this.#upgradeCheckPromise) {
|
|
113
168
|
logger.debug(() => "Starting upgrade check");
|
|
114
169
|
// Only cache a non-"failed" result. A "failed" result can come from a transient issue
|
|
115
|
-
// (e.g. a network blip
|
|
116
|
-
//
|
|
117
|
-
// permanently deny a later, deliberate caller (e.g. the `upgrade` command explicitly
|
|
118
|
-
// waiting via waitForResult=true) any chance of a fresh attempt for the rest of this
|
|
119
|
-
// process's lifetime.
|
|
170
|
+
// (e.g. a network blip) - caching that would permanently deny a later caller any chance of
|
|
171
|
+
// a fresh attempt for the rest of this process's lifetime.
|
|
120
172
|
this.#upgradeCheckPromise = this.checkForUpgrade().then((result) => {
|
|
121
173
|
logger.debug(() => describeUpgradeCheckResult(result));
|
|
122
174
|
if (result.status === "failed") {
|
|
@@ -125,15 +177,23 @@ export default class DefaultUpgradeService implements UpgradeService {
|
|
|
125
177
|
return result;
|
|
126
178
|
});
|
|
127
179
|
}
|
|
128
|
-
|
|
129
|
-
|
|
180
|
+
return this.#upgradeCheckPromise;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Run (or reuse an in-flight/cached) {@link getUpgradeCheckResult}, then persist a "checked" or
|
|
185
|
+
* "unsupported" result to {@link UPGRADE_CHECK_CACHE_KEY} so the banner task's cheap KV read can
|
|
186
|
+
* pick it up on a later invocation.
|
|
187
|
+
*/
|
|
188
|
+
public async refreshUpgradeCheckCache(): Promise<UpgradeCheckResult> {
|
|
189
|
+
const result = await this.getUpgradeCheckResult();
|
|
190
|
+
if (this.#keyValueService && (result.status === "checked" || result.status === "unsupported")) {
|
|
191
|
+
const keyValueService = this.#keyValueService;
|
|
192
|
+
await this.#safeKeyValueCall(() =>
|
|
193
|
+
keyValueService.set(UPGRADE_CHECK_CACHE_KEY, result as unknown as SettableValueNode),
|
|
194
|
+
);
|
|
130
195
|
}
|
|
131
|
-
return
|
|
132
|
-
this.#upgradeCheckPromise,
|
|
133
|
-
new Promise<UpgradeCheckResult>((resolve) =>
|
|
134
|
-
setTimeout(() => resolve({ status: "pending" }), VERSION_CHECK_TIMEOUT_MS),
|
|
135
|
-
),
|
|
136
|
-
]);
|
|
196
|
+
return result;
|
|
137
197
|
}
|
|
138
198
|
|
|
139
199
|
public detectOs(): SupportedOs | undefined {
|
|
@@ -161,19 +221,84 @@ export default class DefaultUpgradeService implements UpgradeService {
|
|
|
161
221
|
}
|
|
162
222
|
|
|
163
223
|
public async detectInstallMethod(os: SupportedOs): Promise<InstallMethod | undefined> {
|
|
164
|
-
|
|
224
|
+
// Cheap, no-spawn signals are checked first and always win over the cache below, so a fresh
|
|
225
|
+
// install is picked up immediately rather than waiting on a stale cached method.
|
|
226
|
+
if (
|
|
227
|
+
os === SupportedOs.MACOS &&
|
|
228
|
+
this.#config.homebrew &&
|
|
229
|
+
this.#isRunningFromHomebrewCellar(this.#config.homebrew.formula)
|
|
230
|
+
) {
|
|
165
231
|
return InstallMethod.HOMEBREW;
|
|
166
232
|
}
|
|
167
|
-
if (os === SupportedOs.WINDOWS && this.#config.winget && (await this.#isWingetInstalled())) {
|
|
168
|
-
return InstallMethod.WINGET;
|
|
169
|
-
}
|
|
170
233
|
if (os === SupportedOs.LINUX && this.#config.linuxScript && this.#isLinuxScriptInstall()) {
|
|
171
234
|
return InstallMethod.LINUX_SCRIPT;
|
|
172
235
|
}
|
|
173
|
-
|
|
174
|
-
|
|
236
|
+
|
|
237
|
+
// Everything else needs an external process spawn (`brew list`, `winget list`) to confirm -
|
|
238
|
+
// cache the resolved method so that spawn only ever happens once per keystore.
|
|
239
|
+
const cached = await this.#getCachedInstallMethod();
|
|
240
|
+
if (cached !== undefined) {
|
|
241
|
+
return cached;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let detected: InstallMethod | undefined;
|
|
245
|
+
if (os === SupportedOs.MACOS && this.#config.homebrew && (await this.#isHomebrewInstalled())) {
|
|
246
|
+
detected = InstallMethod.HOMEBREW;
|
|
247
|
+
} else if (
|
|
248
|
+
os === SupportedOs.WINDOWS &&
|
|
249
|
+
this.#config.winget &&
|
|
250
|
+
(await this.#isWingetInstalled())
|
|
251
|
+
) {
|
|
252
|
+
detected = InstallMethod.WINGET;
|
|
253
|
+
} else if (this.#config.githubRelease) {
|
|
254
|
+
detected = InstallMethod.GITHUB_RELEASE;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (detected !== undefined) {
|
|
258
|
+
await this.#cacheInstallMethod(detected);
|
|
259
|
+
}
|
|
260
|
+
return detected;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// The opportunistic startup check (see getUpgradeCheckResult()) runs detached from
|
|
264
|
+
// UpgradeServiceProvider.initService()'s own await chain, so it can still be running after that
|
|
265
|
+
// scoped KeyValueService access window has closed - a KeyValueService call landing after that
|
|
266
|
+
// point throws (or could land under a different service's scope entirely). Treat any such
|
|
267
|
+
// failure as "caching unavailable right now" rather than letting it fail the whole check.
|
|
268
|
+
async #safeKeyValueCall<T>(op: () => Promise<T>): Promise<T | undefined> {
|
|
269
|
+
try {
|
|
270
|
+
return await op();
|
|
271
|
+
} catch (error) {
|
|
272
|
+
logger.debug(() => `KeyValueService access failed, skipping cache: ${error}`);
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async #getCachedInstallMethod(): Promise<InstallMethod | undefined> {
|
|
278
|
+
if (!this.#keyValueService) {
|
|
279
|
+
return undefined;
|
|
280
|
+
}
|
|
281
|
+
const keyValueService = this.#keyValueService;
|
|
282
|
+
const has = await this.#safeKeyValueCall(() => keyValueService.has(INSTALL_METHOD_CACHE_KEY));
|
|
283
|
+
if (!has) {
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
const cached = await this.#safeKeyValueCall(() =>
|
|
287
|
+
keyValueService.get<CachedInstallMethod>(INSTALL_METHOD_CACHE_KEY),
|
|
288
|
+
);
|
|
289
|
+
if (!cached || Date.now() - cached.checkedAt >= CACHE_TTL_MILLIS) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
return cached.method;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async #cacheInstallMethod(method: InstallMethod): Promise<void> {
|
|
296
|
+
if (this.#keyValueService) {
|
|
297
|
+
const keyValueService = this.#keyValueService;
|
|
298
|
+
await this.#safeKeyValueCall(() =>
|
|
299
|
+
keyValueService.set(INSTALL_METHOD_CACHE_KEY, { method, checkedAt: Date.now() }),
|
|
300
|
+
);
|
|
175
301
|
}
|
|
176
|
-
return undefined;
|
|
177
302
|
}
|
|
178
303
|
|
|
179
304
|
public async checkForUpgrade(
|
|
@@ -230,7 +355,7 @@ export default class DefaultUpgradeService implements UpgradeService {
|
|
|
230
355
|
osOverride !== undefined || archOverride !== undefined || installMethodOverride !== undefined;
|
|
231
356
|
const checkResult = hasOverride
|
|
232
357
|
? await this.checkForUpgrade(osOverride, archOverride, installMethodOverride)
|
|
233
|
-
: await this.getUpgradeCheckResult(
|
|
358
|
+
: await this.getUpgradeCheckResult();
|
|
234
359
|
if (checkResult.status === "unsupported") {
|
|
235
360
|
return {
|
|
236
361
|
ok: false,
|
|
@@ -241,10 +366,6 @@ export default class DefaultUpgradeService implements UpgradeService {
|
|
|
241
366
|
if (checkResult.status === "failed") {
|
|
242
367
|
return { ok: false, oldVersion, error: checkResult.error };
|
|
243
368
|
}
|
|
244
|
-
if (checkResult.status === "pending") {
|
|
245
|
-
// Unreachable: checkForUpgrade() and getUpgradeCheckResult(true) always run to completion.
|
|
246
|
-
return { ok: false, oldVersion, error: new Error("Upgrade check did not complete") };
|
|
247
|
-
}
|
|
248
369
|
if (!this.#spawnService) {
|
|
249
370
|
return { ok: false, oldVersion, error: new Error("SpawnService is not available") };
|
|
250
371
|
}
|
|
@@ -286,6 +407,21 @@ export default class DefaultUpgradeService implements UpgradeService {
|
|
|
286
407
|
);
|
|
287
408
|
}
|
|
288
409
|
|
|
410
|
+
// Homebrew relinks a formula's installed binary from its Cellar directory into a `bin/` symlink,
|
|
411
|
+
// so resolving the running executable's real path confirms a homebrew install without spawning
|
|
412
|
+
// `brew`, which has a slow cold start - avoiding it keeps the background upgrade-check
|
|
413
|
+
// StartupTask (see UpgradeServiceProvider) fast even though it now runs to completion.
|
|
414
|
+
#isRunningFromHomebrewCellar(formula: string): boolean {
|
|
415
|
+
let realExecutable = process.execPath;
|
|
416
|
+
try {
|
|
417
|
+
realExecutable = realpathSync(process.execPath);
|
|
418
|
+
} catch {
|
|
419
|
+
// process.execPath may not resolve on disk (e.g. a fabricated path in tests) - fall back to
|
|
420
|
+
// the unresolved path rather than treating that as "not installed".
|
|
421
|
+
}
|
|
422
|
+
return realExecutable.includes(`/Cellar/${formula}/`);
|
|
423
|
+
}
|
|
424
|
+
|
|
289
425
|
async #isHomebrewInstalled(): Promise<boolean> {
|
|
290
426
|
if (!this.#spawnService || !this.#config.homebrew) {
|
|
291
427
|
return false;
|
|
@@ -313,6 +449,30 @@ export default class DefaultUpgradeService implements UpgradeService {
|
|
|
313
449
|
}
|
|
314
450
|
|
|
315
451
|
async #getLatestVersion(installMethod: InstallMethod): Promise<VersionLookupResult> {
|
|
452
|
+
const cacheKey = `${LATEST_VERSION_CACHE_KEY_PREFIX}${installMethod}`;
|
|
453
|
+
const keyValueService = this.#keyValueService;
|
|
454
|
+
if (keyValueService) {
|
|
455
|
+
const has = await this.#safeKeyValueCall(() => keyValueService.has(cacheKey));
|
|
456
|
+
if (has) {
|
|
457
|
+
const cached = await this.#safeKeyValueCall(() =>
|
|
458
|
+
keyValueService.get<CachedLatestVersion>(cacheKey),
|
|
459
|
+
);
|
|
460
|
+
if (cached && Date.now() - cached.checkedAt < CACHE_TTL_MILLIS) {
|
|
461
|
+
return { ok: true, version: cached.version };
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const result = await this.#lookupLatestVersion(installMethod);
|
|
467
|
+
if (result.ok && keyValueService) {
|
|
468
|
+
await this.#safeKeyValueCall(() =>
|
|
469
|
+
keyValueService.set(cacheKey, { version: result.version, checkedAt: Date.now() }),
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async #lookupLatestVersion(installMethod: InstallMethod): Promise<VersionLookupResult> {
|
|
316
476
|
switch (installMethod) {
|
|
317
477
|
case InstallMethod.GITHUB_RELEASE:
|
|
318
478
|
case InstallMethod.LINUX_SCRIPT:
|
|
@@ -16,9 +16,30 @@ import DefaultUpgradeService from "./DefaultUpgradeService.ts";
|
|
|
16
16
|
import { UpgradeSubCommand } from "./command/UpgradeSubCommand.ts";
|
|
17
17
|
import type { UpgradeLocationsConfig } from "./UpgradeLocationsConfig.ts";
|
|
18
18
|
import getLogger from "../../util/logger.ts";
|
|
19
|
+
import type { StartupTask } from "@flowscripter/dynamic-cli-framework-api";
|
|
19
20
|
|
|
20
21
|
const logger = getLogger("UpgradeServiceProvider");
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Build the background {@link StartupTask} which opportunistically refreshes the upgrade-check
|
|
25
|
+
* cache (see {@link DefaultUpgradeService.refreshUpgradeCheckCache}) that the banner task reads
|
|
26
|
+
* from. Registered separately from {@link UpgradeServiceProvider} itself (which still runs as a
|
|
27
|
+
* regular blocking init task) so it can run in `"background"` mode.
|
|
28
|
+
*/
|
|
29
|
+
export function createUpgradeCheckStartupTask(
|
|
30
|
+
upgradeService: DefaultUpgradeService,
|
|
31
|
+
priority: number,
|
|
32
|
+
): StartupTask {
|
|
33
|
+
return {
|
|
34
|
+
id: `${UPGRADE_SERVICE_ID}-check`,
|
|
35
|
+
priority,
|
|
36
|
+
mode: "background",
|
|
37
|
+
run: async () => {
|
|
38
|
+
await upgradeService.refreshUpgradeCheckCache();
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
export default class UpgradeServiceProvider implements ServiceProvider {
|
|
23
44
|
readonly serviceId: string = UPGRADE_SERVICE_ID;
|
|
24
45
|
readonly servicePriority: number;
|
|
@@ -31,12 +52,16 @@ export default class UpgradeServiceProvider implements ServiceProvider {
|
|
|
31
52
|
this.#config = config;
|
|
32
53
|
}
|
|
33
54
|
|
|
55
|
+
public get upgradeService(): DefaultUpgradeService | undefined {
|
|
56
|
+
return this.#upgradeService;
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
public getServiceInfo(cliConfig: CLIConfig): Promise<ServiceInfo> {
|
|
35
60
|
this.#cliConfig = cliConfig;
|
|
36
61
|
this.#upgradeService = new DefaultUpgradeService(this.#config, cliConfig);
|
|
37
62
|
return Promise.resolve({
|
|
38
63
|
service: this.#upgradeService,
|
|
39
|
-
commands: [new UpgradeSubCommand(
|
|
64
|
+
commands: [new UpgradeSubCommand()],
|
|
40
65
|
});
|
|
41
66
|
}
|
|
42
67
|
|
|
@@ -56,22 +81,20 @@ export default class UpgradeServiceProvider implements ServiceProvider {
|
|
|
56
81
|
if (fetchService === undefined) {
|
|
57
82
|
logger.debug(() => "FetchService not available, upgrade version checks will be unavailable");
|
|
58
83
|
}
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
84
|
+
const keyValueService = context.doesServiceExist(KEY_VALUE_SERVICE_ID)
|
|
85
|
+
? (context.getServiceById(KEY_VALUE_SERVICE_ID) as KeyValueService)
|
|
86
|
+
: undefined;
|
|
87
|
+
upgradeService.setContext(context);
|
|
63
88
|
|
|
64
89
|
if (!context.doesServiceExist(PROMPTER_SERVICE_ID)) {
|
|
65
90
|
logger.debug(() => "PrompterService not available, skipping auto-upgrade");
|
|
66
91
|
return;
|
|
67
92
|
}
|
|
68
|
-
if (!
|
|
93
|
+
if (!keyValueService) {
|
|
69
94
|
logger.debug(() => "KeyValueService not available, skipping auto-upgrade");
|
|
70
95
|
return;
|
|
71
96
|
}
|
|
72
97
|
|
|
73
|
-
const keyValueService = context.getServiceById(KEY_VALUE_SERVICE_ID) as KeyValueService;
|
|
74
|
-
|
|
75
98
|
if (await keyValueService.has("upgrade-status")) {
|
|
76
99
|
const status = await keyValueService.get("upgrade-status");
|
|
77
100
|
if (status === "declined") {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
PRINTER_SERVICE_ID,
|
|
9
9
|
SupportedArch,
|
|
10
10
|
SupportedOs,
|
|
11
|
+
UPGRADE_SERVICE_ID,
|
|
11
12
|
} from "@flowscripter/dynamic-cli-framework-api";
|
|
12
13
|
import type { PrinterService } from "@flowscripter/dynamic-cli-framework-api";
|
|
13
14
|
import type DefaultUpgradeService from "../DefaultUpgradeService.ts";
|
|
@@ -35,14 +36,9 @@ export class UpgradeSubCommand implements SubCommand {
|
|
|
35
36
|
},
|
|
36
37
|
];
|
|
37
38
|
|
|
38
|
-
readonly #upgradeService: DefaultUpgradeService;
|
|
39
|
-
|
|
40
|
-
public constructor(upgradeService: DefaultUpgradeService) {
|
|
41
|
-
this.#upgradeService = upgradeService;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
39
|
public async execute(context: Context, argumentValues: Values): Promise<void> {
|
|
45
40
|
const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
|
|
41
|
+
const upgradeService = context.getServiceById(UPGRADE_SERVICE_ID) as DefaultUpgradeService;
|
|
46
42
|
const cliName = context.cliConfig.name;
|
|
47
43
|
const currentVersion = context.cliConfig.version;
|
|
48
44
|
|
|
@@ -54,8 +50,8 @@ export class UpgradeSubCommand implements SubCommand {
|
|
|
54
50
|
|
|
55
51
|
const checkResult =
|
|
56
52
|
os === undefined && installMethod === undefined
|
|
57
|
-
? await
|
|
58
|
-
: await
|
|
53
|
+
? await upgradeService.refreshUpgradeCheckCache()
|
|
54
|
+
: await upgradeService.checkForUpgrade(os, arch, installMethod);
|
|
59
55
|
|
|
60
56
|
await printerService.hideSpinner();
|
|
61
57
|
if (checkResult.status === "unsupported") {
|
|
@@ -74,12 +70,6 @@ export class UpgradeSubCommand implements SubCommand {
|
|
|
74
70
|
return;
|
|
75
71
|
}
|
|
76
72
|
|
|
77
|
-
if (checkResult.status === "pending") {
|
|
78
|
-
// Unreachable: getUpgradeCheckResult(true) and checkForUpgrade() always run to completion.
|
|
79
|
-
await printerService.error(`Failed to check for updates.\n`, Icon.FAILURE);
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
73
|
if (!checkResult.updateAvailable) {
|
|
84
74
|
await printerService.print(
|
|
85
75
|
`${cliName} is already up to date: ${checkResult.currentVersion}\n`,
|
|
@@ -88,7 +78,7 @@ export class UpgradeSubCommand implements SubCommand {
|
|
|
88
78
|
return;
|
|
89
79
|
}
|
|
90
80
|
|
|
91
|
-
const upgradeResult = await
|
|
81
|
+
const upgradeResult = await upgradeService.upgrade(os, arch, installMethod);
|
|
92
82
|
if (!upgradeResult.ok) {
|
|
93
83
|
await printerService.error(
|
|
94
84
|
`Failed to upgrade ${cliName}: ${upgradeResult.error?.message ?? "unknown error"}\n`,
|