@flowscripter/dynamic-cli-framework 7.0.6 → 7.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"Progress.d.ts","sourceRoot":"","sources":["../../../../../src/service/printer/terminal/Progress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,6BAA6B,CAAC;AACtD,OAAO,KAAK,QAAQ,MAAM,+BAA+B,CAAC;AAU1D,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAmBxE,MAAM,CAAC,OAAO,OAAO,QAAQ;;IAe3B,YAAmB,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAGpD;IAEM,GAAG,CACR,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EACjC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GACnC,MAAM,CA6BR;IA4BM,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAyBrE;IAEY,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe/C;IAEY,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAKpC;IAEY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAqBlC;IAEM,MAAM,IAAI,IAAI,CAapB;IAkID,IAAI,aAAa,CAAC,KAAK,EAAE,MAAM,EAE9B;IAED,IAAI,cAAc,CAAC,KAAK,EAAE,MAAM,EAE/B;IAED,IAAI,UAAU,CAAC,KAAK,EAAE,MAAM,EAE3B;IAED,IAAI,UAAU,CAAC,KAAK,EAAE,MAAM,EAE3B;IAED,IAAI,aAAa,CAAC,KAAK,EAAE,aAAa,EAErC;CAmDF"}
1
+ {"version":3,"file":"Progress.d.ts","sourceRoot":"","sources":["../../../../../src/service/printer/terminal/Progress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,6BAA6B,CAAC;AACtD,OAAO,KAAK,QAAQ,MAAM,+BAA+B,CAAC;AAe1D,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAoBxE,MAAM,CAAC,OAAO,OAAO,QAAQ;;IAe3B,YAAmB,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAGpD;IAEM,GAAG,CACR,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,EACjC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GACnC,MAAM,CAgCR;IA6BM,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAyBrE;IAEY,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe/C;IAEY,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAKpC;IAEY,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAqBlC;IAEM,MAAM,IAAI,IAAI,CAapB;IAkID,IAAI,aAAa,CAAC,KAAK,EAAE,MAAM,EAE9B;IAED,IAAI,cAAc,CAAC,KAAK,EAAE,MAAM,EAE/B;IAED,IAAI,UAAU,CAAC,KAAK,EAAE,MAAM,EAE3B;IAED,IAAI,UAAU,CAAC,KAAK,EAAE,MAAM,EAE3B;IAED,IAAI,aAAa,CAAC,KAAK,EAAE,aAAa,EAErC;CAmDF"}
@@ -1,6 +1,10 @@
1
1
  // Weight given to each new rate sample in the exponential moving average - time constant is
2
- // roughly 1/RATE_SMOOTHING_FACTOR * 100ms (the render interval).
2
+ // roughly 1/RATE_SMOOTHING_FACTOR * RATE_SAMPLE_INTERVAL_MILLIS.
3
3
  const RATE_SMOOTHING_FACTOR = 0.3;
4
+ // Minimum time between rate samples. Updates arriving more often than this only advance the
5
+ // current value; the next sample then measures the total progress made across the whole window,
6
+ // so rapid, fine-grained or coarsely stepped updates still yield an accurate rate.
7
+ const RATE_SAMPLE_INTERVAL_MILLIS = 100;
4
8
  // A rate this close to zero (units/s) produces a meaningless (and, past a certain point,
5
9
  // non-finite) "time remaining" estimate - treat it the same as no rate at all.
6
10
  const MAX_REMAINING_MILLIS = 999 * 24 * 60 * 60 * 1000;
@@ -38,13 +42,16 @@ export default class Progress {
38
42
  if (current > total) {
39
43
  current = total;
40
44
  }
45
+ const now = Date.now();
41
46
  this.#bars.set(this.#bars.size + 1, {
42
47
  name: message,
43
48
  current,
44
49
  total,
45
50
  format,
46
51
  formatRate,
47
- startMillis: Date.now(),
52
+ sampleMillis: now,
53
+ sampleCurrent: current,
54
+ startMillis: now,
48
55
  });
49
56
  // force render on next timeout
50
57
  this.#isDirty = true;
@@ -67,17 +74,17 @@ export default class Progress {
67
74
  }
68
75
  #updateRate(bar, current) {
69
76
  const now = Date.now();
70
- if (bar.lastMillis === undefined) {
71
- bar.rate = ((current - bar.current) * 1000) / (now - bar.startMillis);
72
- }
73
- else {
74
- let currentRate = ((current - bar.current) * 1000) / (now - bar.lastMillis);
75
- if (!isFinite(currentRate)) {
76
- currentRate = 0;
77
- }
78
- bar.rate = currentRate * RATE_SMOOTHING_FACTOR + bar.rate * (1 - RATE_SMOOTHING_FACTOR);
77
+ const elapsed = now - bar.sampleMillis;
78
+ if (elapsed < RATE_SAMPLE_INTERVAL_MILLIS) {
79
+ return;
79
80
  }
80
- bar.lastMillis = now;
81
+ const sampleRate = ((current - bar.sampleCurrent) * 1000) / elapsed;
82
+ bar.rate =
83
+ bar.rate === undefined
84
+ ? sampleRate
85
+ : sampleRate * RATE_SMOOTHING_FACTOR + bar.rate * (1 - RATE_SMOOTHING_FACTOR);
86
+ bar.sampleMillis = now;
87
+ bar.sampleCurrent = current;
81
88
  }
82
89
  update(handle, current, message) {
83
90
  if (!this.#bars.has(handle)) {
@@ -14,6 +14,11 @@ export default class DefaultUpgradeService implements UpgradeService {
14
14
  * pick it up on a later invocation.
15
15
  */
16
16
  refreshUpgradeCheckCache(): Promise<UpgradeCheckResult>;
17
+ /**
18
+ * Return the result last persisted by {@link refreshUpgradeCheckCache}, without running a check.
19
+ * Returns `undefined` if nothing has been persisted yet or no KeyValueService is available.
20
+ */
21
+ getCachedUpgradeCheckResult(): Promise<UpgradeCheckResult | undefined>;
17
22
  detectOs(): SupportedOs | undefined;
18
23
  detectArch(): SupportedArch | undefined;
19
24
  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;AAuC1E,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;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,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"}
@@ -23,9 +23,10 @@ const LATEST_VERSION_CACHE_KEY_PREFIX = "latest-version:";
23
23
  const CACHE_TTL_MILLIS = 24 * 60 * 60 * 1000;
24
24
  // KeyValueService key holding the last checkForUpgrade() result, refreshed opportunistically by
25
25
  // UpgradeServiceProvider's background StartupTask and by the `upgrade` command - shared by both so
26
- // there's one cache, not two. The banner task only ever reads this key (kv.has()/kv.get()) - it
27
- // never calls checkForUpgrade()/getUpgradeCheckResult() live, since checking live would stall
28
- // startup on the network/spawn calls checkForUpgrade() makes.
26
+ // there's one cache, not two. The key lives in this service's own KeyValueService scope, so other
27
+ // consumers (e.g. the banner task) read it via getCachedUpgradeCheckResult() rather than their own
28
+ // scoped KeyValueService. The banner never calls checkForUpgrade()/getUpgradeCheckResult() live,
29
+ // since checking live would stall startup on the network/spawn calls checkForUpgrade() makes.
29
30
  export const UPGRADE_CHECK_CACHE_KEY = "upgrade-check-result";
30
31
  function describeSpawnFailure(result) {
31
32
  return "timedOut" in result
@@ -133,6 +134,22 @@ export default class DefaultUpgradeService {
133
134
  }
134
135
  return result;
135
136
  }
137
+ /**
138
+ * Return the result last persisted by {@link refreshUpgradeCheckCache}, without running a check.
139
+ * Returns `undefined` if nothing has been persisted yet or no KeyValueService is available.
140
+ */
141
+ async getCachedUpgradeCheckResult() {
142
+ const keyValueService = this.#keyValueService;
143
+ if (!keyValueService) {
144
+ return undefined;
145
+ }
146
+ const has = await this.#safeKeyValueCall(() => keyValueService.has(UPGRADE_CHECK_CACHE_KEY));
147
+ if (!has) {
148
+ return undefined;
149
+ }
150
+ const cached = await this.#safeKeyValueCall(() => keyValueService.get(UPGRADE_CHECK_CACHE_KEY));
151
+ return cached;
152
+ }
136
153
  detectOs() {
137
154
  switch (process.platform) {
138
155
  case "linux":
@@ -1 +1 @@
1
- {"version":3,"file":"bannerStartupTask.d.ts","sourceRoot":"","sources":["../../../../src/startup/banner/bannerStartupTask.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,WAAW,EAEZ,MAAM,yCAAyC,CAAC;AAYjD,eAAO,MAAM,sBAAsB,4DAA4D,CAAC;AAEhG;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,OAAO,UAAU,uBAAuB,CAC7C,QAAQ,EAAE,MAAM,EAChB,QAAQ,SAAa,GACpB,WAAW,CA2Db"}
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,5 +1,5 @@
1
- import { ASCII_BANNER_GENERATOR_SERVICE_ID, CONFIGURATION_SERVICE_ID, KEY_VALUE_SERVICE_ID, PRINTER_SERVICE_ID, } from "@flowscripter/dynamic-cli-framework-api";
2
- import { UPGRADE_CHECK_CACHE_KEY } from "../../service/upgrade/DefaultUpgradeService.js";
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
3
  import NoBannerCommand, {} from "./command/NoBannerCommand.js";
4
4
  export const BANNER_STARTUP_TASK_ID = "@flowscripter/dynamic-cli-framework/banner-startup-task";
5
5
  /**
@@ -41,14 +41,18 @@ export default function createBannerStartupTask(priority, fontName = "standard")
41
41
  let versionLine = "version: " + cliConfig.version;
42
42
  // Cheap cache read only - never calls checkForUpgrade()/getUpgradeCheckResult() live,
43
43
  // since that could stall startup on a network/spawn call. A first-ever run (no cached
44
- // key yet) simply shows no upgrade-available suffix - that's expected, not a bug.
45
- if (context.doesServiceExist(KEY_VALUE_SERVICE_ID)) {
46
- const keyValueService = context.getServiceById(KEY_VALUE_SERVICE_ID);
47
- if (await keyValueService.has(UPGRADE_CHECK_CACHE_KEY)) {
48
- const result = (await keyValueService.get(UPGRADE_CHECK_CACHE_KEY));
49
- if (result.status === "checked" && result.updateAvailable) {
50
- versionLine += ` (${result.latestVersion} available, run '${cliConfig.name} upgrade')`;
51
- }
44
+ // result yet) shows no upgrade-available suffix. The cached result is read through the
45
+ // upgrade service because it is stored in that service's own KeyValueService scope, and
46
+ // 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) {
51
+ const result = await upgradeService.getCachedUpgradeCheckResult();
52
+ if (result?.status === "checked" &&
53
+ result.updateAvailable &&
54
+ result.currentVersion === cliConfig.version) {
55
+ versionLine += ` (${result.latestVersion} available, run '${cliConfig.name} upgrade')`;
52
56
  }
53
57
  }
54
58
  await printerService.info(` ${printerService.secondary(versionLine)}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "7.0.6",
3
+ "version": "7.0.8",
4
4
  "description": "A framework for developing CLI applications which supports dynamic discovery and installation of new commands",
5
5
  "keywords": [
6
6
  "bun",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@flowscripter/dynamic-cli-framework-api": "^4.0.4",
52
- "@flowscripter/dynamic-plugin-framework": "^2.2.12",
52
+ "@flowscripter/dynamic-plugin-framework": "^2.2.14",
53
53
  "emphasize": "^7.0.0",
54
54
  "fastest-levenshtein": "^1.0.16",
55
55
  "figlet": "^1.12.0",
@@ -2,9 +2,14 @@ import type Styler from "../../../terminal/Styler.ts";
2
2
  import type Terminal from "../../../terminal/Terminal.ts";
3
3
 
4
4
  // Weight given to each new rate sample in the exponential moving average - time constant is
5
- // roughly 1/RATE_SMOOTHING_FACTOR * 100ms (the render interval).
5
+ // roughly 1/RATE_SMOOTHING_FACTOR * RATE_SAMPLE_INTERVAL_MILLIS.
6
6
  const RATE_SMOOTHING_FACTOR = 0.3;
7
7
 
8
+ // Minimum time between rate samples. Updates arriving more often than this only advance the
9
+ // current value; the next sample then measures the total progress made across the whole window,
10
+ // so rapid, fine-grained or coarsely stepped updates still yield an accurate rate.
11
+ const RATE_SAMPLE_INTERVAL_MILLIS = 100;
12
+
8
13
  // A rate this close to zero (units/s) produces a meaningless (and, past a certain point,
9
14
  // non-finite) "time remaining" estimate - treat it the same as no rate at all.
10
15
  const MAX_REMAINING_MILLIS = 999 * 24 * 60 * 60 * 1000;
@@ -23,8 +28,9 @@ interface Bar {
23
28
  total: number;
24
29
  format: (value: number) => string;
25
30
  formatRate: (rate: number) => string;
26
- lastMillis?: number;
27
- startMillis?: number;
31
+ sampleMillis: number;
32
+ sampleCurrent: number;
33
+ startMillis: number;
28
34
  endMillis?: number;
29
35
  rate?: number;
30
36
  }
@@ -66,13 +72,16 @@ export default class Progress {
66
72
  current = total;
67
73
  }
68
74
 
75
+ const now = Date.now();
69
76
  this.#bars.set(this.#bars.size + 1, {
70
77
  name: message,
71
78
  current,
72
79
  total,
73
80
  format,
74
81
  formatRate,
75
- startMillis: Date.now(),
82
+ sampleMillis: now,
83
+ sampleCurrent: current,
84
+ startMillis: now,
76
85
  });
77
86
 
78
87
  // force render on next timeout
@@ -100,16 +109,17 @@ export default class Progress {
100
109
 
101
110
  #updateRate(bar: Bar, current: number): void {
102
111
  const now = Date.now();
103
- if (bar.lastMillis === undefined) {
104
- bar.rate = ((current - bar.current) * 1000) / (now - bar.startMillis!);
105
- } else {
106
- let currentRate = ((current - bar.current) * 1000) / (now - bar.lastMillis!);
107
- if (!isFinite(currentRate)) {
108
- currentRate = 0;
109
- }
110
- bar.rate = currentRate * RATE_SMOOTHING_FACTOR + bar.rate! * (1 - RATE_SMOOTHING_FACTOR);
112
+ const elapsed = now - bar.sampleMillis;
113
+ if (elapsed < RATE_SAMPLE_INTERVAL_MILLIS) {
114
+ return;
111
115
  }
112
- bar.lastMillis = now;
116
+ const sampleRate = ((current - bar.sampleCurrent) * 1000) / elapsed;
117
+ bar.rate =
118
+ bar.rate === undefined
119
+ ? sampleRate
120
+ : sampleRate * RATE_SMOOTHING_FACTOR + bar.rate * (1 - RATE_SMOOTHING_FACTOR);
121
+ bar.sampleMillis = now;
122
+ bar.sampleCurrent = current;
113
123
  }
114
124
 
115
125
  public update(handle: number, current: number, message?: string): void {
@@ -244,7 +254,7 @@ export default class Progress {
244
254
 
245
255
  let suffix = `${this.#styler.colorText("]", this.#labColor)} ${percent} `;
246
256
  if (bar.current === bar.total) {
247
- const taken = this.#formatTime(bar.endMillis! - bar.startMillis!);
257
+ const taken = this.#formatTime(bar.endMillis! - bar.startMillis);
248
258
  const totalText = bar.format(bar.total);
249
259
  suffix += `${this.#styler.colorText(totalText, this.#valColor)}${this.#styler.colorText(
250
260
  ", rate:",
@@ -63,9 +63,10 @@ interface CachedLatestVersion {
63
63
 
64
64
  // KeyValueService key holding the last checkForUpgrade() result, refreshed opportunistically by
65
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.
66
+ // there's one cache, not two. The key lives in this service's own KeyValueService scope, so other
67
+ // consumers (e.g. the banner task) read it via getCachedUpgradeCheckResult() rather than their own
68
+ // scoped KeyValueService. The banner never calls checkForUpgrade()/getUpgradeCheckResult() live,
69
+ // since checking live would stall startup on the network/spawn calls checkForUpgrade() makes.
69
70
  export const UPGRADE_CHECK_CACHE_KEY = "upgrade-check-result";
70
71
 
71
72
  type VersionLookupResult =
@@ -196,6 +197,23 @@ export default class DefaultUpgradeService implements UpgradeService {
196
197
  return result;
197
198
  }
198
199
 
200
+ /**
201
+ * Return the result last persisted by {@link refreshUpgradeCheckCache}, without running a check.
202
+ * Returns `undefined` if nothing has been persisted yet or no KeyValueService is available.
203
+ */
204
+ public async getCachedUpgradeCheckResult(): Promise<UpgradeCheckResult | undefined> {
205
+ const keyValueService = this.#keyValueService;
206
+ if (!keyValueService) {
207
+ return undefined;
208
+ }
209
+ const has = await this.#safeKeyValueCall(() => keyValueService.has(UPGRADE_CHECK_CACHE_KEY));
210
+ if (!has) {
211
+ return undefined;
212
+ }
213
+ const cached = await this.#safeKeyValueCall(() => keyValueService.get(UPGRADE_CHECK_CACHE_KEY));
214
+ return cached as unknown as UpgradeCheckResult | undefined;
215
+ }
216
+
199
217
  public detectOs(): SupportedOs | undefined {
200
218
  switch (process.platform) {
201
219
  case "linux":
@@ -1,19 +1,13 @@
1
- import type {
2
- Context,
3
- KeyValueService,
4
- PrinterService,
5
- StartupTask,
6
- UpgradeCheckResult,
7
- } from "@flowscripter/dynamic-cli-framework-api";
1
+ import type { Context, PrinterService, StartupTask } from "@flowscripter/dynamic-cli-framework-api";
8
2
  import {
9
3
  ASCII_BANNER_GENERATOR_SERVICE_ID,
10
4
  CONFIGURATION_SERVICE_ID,
11
- KEY_VALUE_SERVICE_ID,
12
5
  PRINTER_SERVICE_ID,
6
+ UPGRADE_SERVICE_ID,
13
7
  } from "@flowscripter/dynamic-cli-framework-api";
14
8
  import type { AsciiBannerGeneratorService } from "@flowscripter/dynamic-cli-framework-api";
15
9
  import type { ConfigurationService } from "@flowscripter/dynamic-cli-framework-api";
16
- import { UPGRADE_CHECK_CACHE_KEY } from "../../service/upgrade/DefaultUpgradeService.ts";
10
+ import DefaultUpgradeService from "../../service/upgrade/DefaultUpgradeService.ts";
17
11
  import NoBannerCommand, { type BannerState } from "./command/NoBannerCommand.ts";
18
12
 
19
13
  export const BANNER_STARTUP_TASK_ID = "@flowscripter/dynamic-cli-framework/banner-startup-task";
@@ -67,16 +61,20 @@ export default function createBannerStartupTask(
67
61
  let versionLine = "version: " + cliConfig.version;
68
62
  // Cheap cache read only - never calls checkForUpgrade()/getUpgradeCheckResult() live,
69
63
  // since that could stall startup on a network/spawn call. A first-ever run (no cached
70
- // key yet) simply shows no upgrade-available suffix - that's expected, not a bug.
71
- if (context.doesServiceExist(KEY_VALUE_SERVICE_ID)) {
72
- const keyValueService = context.getServiceById(KEY_VALUE_SERVICE_ID) as KeyValueService;
73
- if (await keyValueService.has(UPGRADE_CHECK_CACHE_KEY)) {
74
- const result = (await keyValueService.get(
75
- UPGRADE_CHECK_CACHE_KEY,
76
- )) as unknown as UpgradeCheckResult;
77
- if (result.status === "checked" && result.updateAvailable) {
78
- versionLine += ` (${result.latestVersion} available, run '${cliConfig.name} upgrade')`;
79
- }
64
+ // result yet) shows no upgrade-available suffix. The cached result is read through the
65
+ // upgrade service because it is stored in that service's own KeyValueService scope, and
66
+ // it is ignored if it was recorded by a different version (e.g. before an upgrade).
67
+ const upgradeService = context.doesServiceExist(UPGRADE_SERVICE_ID)
68
+ ? context.getServiceById(UPGRADE_SERVICE_ID)
69
+ : undefined;
70
+ if (upgradeService instanceof DefaultUpgradeService) {
71
+ const result = await upgradeService.getCachedUpgradeCheckResult();
72
+ if (
73
+ result?.status === "checked" &&
74
+ result.updateAvailable &&
75
+ result.currentVersion === cliConfig.version
76
+ ) {
77
+ versionLine += ` (${result.latestVersion} available, run '${cliConfig.name} upgrade')`;
80
78
  }
81
79
  }
82
80
  await printerService.info(` ${printerService.secondary(versionLine)}\n`);