@flowscripter/dynamic-cli-framework 9.0.3 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cli/BaseCLI.d.ts.map +1 -1
- package/dist/src/cli/BaseCLI.js +2 -0
- package/dist/src/runtime/lifecycle/BackgroundTasks.d.ts +21 -0
- package/dist/src/runtime/lifecycle/BackgroundTasks.d.ts.map +1 -0
- package/dist/src/runtime/lifecycle/BackgroundTasks.js +69 -0
- package/dist/src/runtime/runner.d.ts +2 -2
- package/dist/src/runtime/runner.d.ts.map +1 -1
- package/dist/src/runtime/runner.js +4 -5
- package/dist/src/service/upgrade/DefaultUpgradeService.d.ts.map +1 -1
- package/dist/src/service/upgrade/DefaultUpgradeService.js +18 -330
- package/dist/src/service/upgrade/method/githubRelease.d.ts +6 -0
- package/dist/src/service/upgrade/method/githubRelease.d.ts.map +1 -0
- package/dist/src/service/upgrade/method/githubRelease.js +132 -0
- package/dist/src/service/upgrade/method/homebrew.d.ts +11 -0
- package/dist/src/service/upgrade/method/homebrew.d.ts.map +1 -0
- package/dist/src/service/upgrade/method/homebrew.js +132 -0
- package/dist/src/service/upgrade/method/linuxScript.d.ts +5 -0
- package/dist/src/service/upgrade/method/linuxScript.d.ts.map +1 -0
- package/dist/src/service/upgrade/method/linuxScript.js +15 -0
- package/dist/src/service/upgrade/method/shared.d.ts +13 -0
- package/dist/src/service/upgrade/method/shared.d.ts.map +1 -0
- package/dist/src/service/upgrade/method/shared.js +33 -0
- package/dist/src/service/upgrade/method/winget.d.ts +7 -0
- package/dist/src/service/upgrade/method/winget.d.ts.map +1 -0
- package/dist/src/service/upgrade/method/winget.js +49 -0
- package/package.json +1 -1
- package/src/cli/BaseCLI.ts +2 -0
- package/src/runtime/lifecycle/BackgroundTasks.ts +96 -0
- package/src/runtime/runner.ts +4 -5
- package/src/service/upgrade/DefaultUpgradeService.ts +38 -377
- package/src/service/upgrade/method/githubRelease.ts +163 -0
- package/src/service/upgrade/method/homebrew.ts +165 -0
- package/src/service/upgrade/method/linuxScript.ts +23 -0
- package/src/service/upgrade/method/shared.ts +51 -0
- package/src/service/upgrade/method/winget.ts +64 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BaseCLI.d.ts","sourceRoot":"","sources":["../../../src/cli/BaseCLI.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,yCAAyC,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAGvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAsBzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"BaseCLI.d.ts","sourceRoot":"","sources":["../../../src/cli/BaseCLI.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,yCAAyC,CAAC;AACnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAGvE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC/E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAsBzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yCAAyC,CAAC;AAU3E,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,QAAQ,MAAM,yBAAyB,CAAC;AAGpD,OAAO,KAAK,MAAM,MAAM,uBAAuB,CAAC;AAChD,OAAO,KAAK,SAAS,MAAM,0BAA0B,CAAC;AACtD,OAAO,KAAK,qBAAqB,MAAM,4BAA4B,CAAC;AAqCpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,MAAM,CAAC,OAAO,OAAO,OAAQ,YAAW,GAAG;;IAazC,YACE,SAAS,EAAE,SAAS,EACpB,oBAAoB,EAAE,cAAc,EACpC,oBAAoB,EAAE,cAAc,EACpC,aAAa,EAAE,OAAO,EACtB,aAAa,EAAE,OAAO,EACtB,cAAc,EAAE,QAAQ,EACxB,cAAc,EAAE,QAAQ,EACxB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,SAAS,GAAG,SAAS,EAChC,OAAO,CAAC,EAAE,qBAAqB,EAmEhC;IAED;;;;;;;OAOG;IACI,kBAAkB,CAAC,eAAe,EAAE,eAAe,QAEzD;IAED;;;;;;;;OAQG;IACH;;;;;OAKG;IACI,cAAc,CAAC,IAAI,EAAE,WAAW,QAEtC;IAEM,UAAU,CAAC,OAAO,EAAE,OAAO,QAQjC;IAEK,GAAG,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,SAAS,CAAC,CAqQzD;CACF"}
|
package/dist/src/cli/BaseCLI.js
CHANGED
|
@@ -17,6 +17,7 @@ import KeyValueServiceProvider from "../service/configuration/KeyValueServicePro
|
|
|
17
17
|
import PrinterServiceProvider from "../service/printer/PrinterServiceProvider.js";
|
|
18
18
|
import TableGeneratorServiceProvider from "../service/tableGenerator/TableGeneratorServiceProvider.js";
|
|
19
19
|
import { run } from "../runtime/runner.js";
|
|
20
|
+
import { awaitBackgroundTasks } from "../runtime/lifecycle/BackgroundTasks.js";
|
|
20
21
|
import { printRuntimeError } from "../util/runnerHelper.js";
|
|
21
22
|
import { WritableStream } from "node:stream/web";
|
|
22
23
|
import DefaultPrinterService from "../service/printer/DefaultPrinterService.js";
|
|
@@ -322,6 +323,7 @@ export default class BaseCLI {
|
|
|
322
323
|
};
|
|
323
324
|
}
|
|
324
325
|
finally {
|
|
326
|
+
await awaitBackgroundTasks();
|
|
325
327
|
await ShutdownServiceProvider.shutdown();
|
|
326
328
|
}
|
|
327
329
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long, at shutdown, to wait for in-flight "background" startup tasks before abandoning them.
|
|
3
|
+
*/
|
|
4
|
+
export declare const BACKGROUND_TASK_GRACE_PERIOD_MS = 1000;
|
|
5
|
+
/**
|
|
6
|
+
* Start a background startup task without awaiting it, and track it so that
|
|
7
|
+
* {@link awaitBackgroundTasks} can give it a chance to finish at shutdown. A rejection is
|
|
8
|
+
* logged (at debug level) and never propagated.
|
|
9
|
+
*
|
|
10
|
+
* @param id the id of the startup task.
|
|
11
|
+
* @param start starts the task.
|
|
12
|
+
*/
|
|
13
|
+
export declare function trackBackgroundTask(id: string, start: () => Promise<unknown>): void;
|
|
14
|
+
/**
|
|
15
|
+
* Wait for the tracked background startup tasks, for at most `gracePeriodMs`. Tasks still running
|
|
16
|
+
* once it expires are abandoned. Returns immediately if no tasks are in flight. Never rejects.
|
|
17
|
+
*
|
|
18
|
+
* @param gracePeriodMs the maximum time to wait.
|
|
19
|
+
*/
|
|
20
|
+
export declare function awaitBackgroundTasks(gracePeriodMs?: number): Promise<void>;
|
|
21
|
+
//# sourceMappingURL=BackgroundTasks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BackgroundTasks.d.ts","sourceRoot":"","sources":["../../../../src/runtime/lifecycle/BackgroundTasks.ts"],"names":[],"mappings":"AAIA;;GAEG;AACH,eAAO,MAAM,+BAA+B,OAAO,CAAC;AAcpD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAkBnF;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,aAAa,GAAE,MAAwC,GACtD,OAAO,CAAC,IAAI,CAAC,CAsCf"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import getLogger from "../../util/logger.js";
|
|
2
|
+
const logger = getLogger("BackgroundTasks");
|
|
3
|
+
/**
|
|
4
|
+
* How long, at shutdown, to wait for in-flight "background" startup tasks before abandoning them.
|
|
5
|
+
*/
|
|
6
|
+
export const BACKGROUND_TASK_GRACE_PERIOD_MS = 1000;
|
|
7
|
+
const inFlightTasks = new Set();
|
|
8
|
+
function elapsedMs(task) {
|
|
9
|
+
return Math.round(performance.now() - task.startedAt);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Start a background startup task without awaiting it, and track it so that
|
|
13
|
+
* {@link awaitBackgroundTasks} can give it a chance to finish at shutdown. A rejection is
|
|
14
|
+
* logged (at debug level) and never propagated.
|
|
15
|
+
*
|
|
16
|
+
* @param id the id of the startup task.
|
|
17
|
+
* @param start starts the task.
|
|
18
|
+
*/
|
|
19
|
+
export function trackBackgroundTask(id, start) {
|
|
20
|
+
const task = { id, startedAt: performance.now(), settled: Promise.resolve() };
|
|
21
|
+
logger.debug(() => `Background startup task '${id}' started`);
|
|
22
|
+
task.settled = new Promise((resolve) => resolve(start()))
|
|
23
|
+
.then(() => {
|
|
24
|
+
logger.debug(() => `Background startup task '${id}' completed in ${elapsedMs(task)}ms`);
|
|
25
|
+
}, (error) => {
|
|
26
|
+
logger.debug(() => `Background startup task '${id}' failed after ${elapsedMs(task)}ms: ${error}`);
|
|
27
|
+
})
|
|
28
|
+
.finally(() => {
|
|
29
|
+
inFlightTasks.delete(task);
|
|
30
|
+
});
|
|
31
|
+
inFlightTasks.add(task);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Wait for the tracked background startup tasks, for at most `gracePeriodMs`. Tasks still running
|
|
35
|
+
* once it expires are abandoned. Returns immediately if no tasks are in flight. Never rejects.
|
|
36
|
+
*
|
|
37
|
+
* @param gracePeriodMs the maximum time to wait.
|
|
38
|
+
*/
|
|
39
|
+
export async function awaitBackgroundTasks(gracePeriodMs = BACKGROUND_TASK_GRACE_PERIOD_MS) {
|
|
40
|
+
if (inFlightTasks.size === 0) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const pending = [...inFlightTasks];
|
|
44
|
+
logger.debug(() => `Waiting up to ${gracePeriodMs}ms for ${pending.length} in-flight background startup task(s)`);
|
|
45
|
+
for (const task of pending) {
|
|
46
|
+
logger.debug(() => `Background startup task '${task.id}' still in flight after ${elapsedMs(task)}ms`);
|
|
47
|
+
}
|
|
48
|
+
let timer;
|
|
49
|
+
const expired = new Promise((resolve) => {
|
|
50
|
+
timer = setTimeout(resolve, gracePeriodMs);
|
|
51
|
+
// must not keep the process alive
|
|
52
|
+
timer.unref?.();
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
await Promise.race([Promise.all(pending.map((task) => task.settled)), expired]);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// settled promises never reject; nothing to do
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
}
|
|
63
|
+
for (const task of pending) {
|
|
64
|
+
if (inFlightTasks.has(task)) {
|
|
65
|
+
inFlightTasks.delete(task);
|
|
66
|
+
logger.debug(() => `Background startup task '${task.id}' cancelled after ${elapsedMs(task)}ms: still running when the ${gracePeriodMs}ms grace period expired`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -39,8 +39,8 @@ export type RunnerResult = RunResult & {
|
|
|
39
39
|
* - Give the task a Context whose KeyValueService is bound permanently to its own scope
|
|
40
40
|
* (keyed by task.id), via KeyValueServiceProvider if provided.
|
|
41
41
|
* - Run the task: awaited before continuing if its mode is "blocking" (the default, and how
|
|
42
|
-
* every ServiceProvider's initService() behaves);
|
|
43
|
-
* (errors logged, not propagated).
|
|
42
|
+
* every ServiceProvider's initService() behaves); started without awaiting if "background"
|
|
43
|
+
* (errors logged, not propagated; the CLI gives it a short grace period to finish at shutdown).
|
|
44
44
|
* - Return immediately if a "blocking" task returned an exitRequest, using its runState and
|
|
45
45
|
* skipping the remaining tasks and the command.
|
|
46
46
|
* 2. Scan arguments for any GlobalModifierCommand clauses not provided by a StartupTask
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/runtime/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAiB/E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAEvF,OAAO,KAAK,4BAA4B,MAAM,0DAA0D,CAAC;AACzG,OAAO,KAAK,uBAAuB,MAAM,qDAAqD,CAAC;AAK/F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAKvE,OAAO,qBAAqB,MAAM,6CAA6C,CAAC;
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/runtime/runner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAEzE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAiB/E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yCAAyC,CAAC;AAEvF,OAAO,KAAK,4BAA4B,MAAM,0DAA0D,CAAC;AACzG,OAAO,KAAK,uBAAuB,MAAM,qDAAqD,CAAC;AAK/F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AAKvE,OAAO,qBAAqB,MAAM,6CAA6C,CAAC;AAMhF;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG;IAAE,QAAQ,CAAC,aAAa,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC;AAkfzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2EG;AACH,wBAAsB,GAAG,CACvB,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,EAC3B,eAAe,EAAE,eAAe,EAChC,uBAAuB,EAAE,uBAAuB,EAChD,4BAA4B,EAAE,4BAA4B,GAAG,SAAS,EACtE,uBAAuB,EAAE,uBAAuB,GAAG,SAAS,EAC5D,OAAO,EAAE,OAAO,EAChB,cAAc,CAAC,EAAE,OAAO,EACxB,cAAc,CAAC,EAAE,qBAAqB,GACrC,OAAO,CAAC,YAAY,CAAC,CAkKvB"}
|
|
@@ -7,6 +7,7 @@ import { parseGlobalCommandClause, parseSubCommandClause } from "./parser.js";
|
|
|
7
7
|
import { ARGUMENT_PROMPTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
8
8
|
import DefaultStartupService from "../service/startup/DefaultStartupService.js";
|
|
9
9
|
import { sortByPriority } from "./lifecycle/PriorityTaskList.js";
|
|
10
|
+
import { trackBackgroundTask } from "./lifecycle/BackgroundTasks.js";
|
|
10
11
|
const logger = getLogger("runner");
|
|
11
12
|
/**
|
|
12
13
|
* Wrap a {@link ServiceProvider}'s `initService()` call as a {@link StartupTask} so it can be
|
|
@@ -387,8 +388,8 @@ async function findAndExecuteDefaultNonModifierCommand(availableArgs, unusedArgs
|
|
|
387
388
|
* - Give the task a Context whose KeyValueService is bound permanently to its own scope
|
|
388
389
|
* (keyed by task.id), via KeyValueServiceProvider if provided.
|
|
389
390
|
* - Run the task: awaited before continuing if its mode is "blocking" (the default, and how
|
|
390
|
-
* every ServiceProvider's initService() behaves);
|
|
391
|
-
* (errors logged, not propagated).
|
|
391
|
+
* every ServiceProvider's initService() behaves); started without awaiting if "background"
|
|
392
|
+
* (errors logged, not propagated; the CLI gives it a short grace period to finish at shutdown).
|
|
392
393
|
* - Return immediately if a "blocking" task returned an exitRequest, using its runState and
|
|
393
394
|
* skipping the remaining tasks and the command.
|
|
394
395
|
* 2. Scan arguments for any GlobalModifierCommand clauses not provided by a StartupTask
|
|
@@ -486,9 +487,7 @@ export async function run(args, commandRegistry, serviceProviderRegistry, config
|
|
|
486
487
|
}
|
|
487
488
|
}
|
|
488
489
|
else {
|
|
489
|
-
|
|
490
|
-
logger.debug(() => `Background startup task '${task.id}' failed: ${error}`);
|
|
491
|
-
});
|
|
490
|
+
trackBackgroundTask(task.id, () => task.run(taskContext));
|
|
492
491
|
}
|
|
493
492
|
}
|
|
494
493
|
// using the GlobalModifierCommands instances which are NOT provided by a ServiceProvider...
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DefaultUpgradeService.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/DefaultUpgradeService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"DefaultUpgradeService.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/DefaultUpgradeService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,OAAO,EAKP,WAAW,EAEX,kBAAkB,EAClB,aAAa,EAEd,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAEL,aAAa,EAGb,QAAQ,EAER,aAAa,EACb,WAAW,EACX,KAAK,cAAc,EACpB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAmD1E,eAAO,MAAM,uBAAuB,yBAAyB,CAAC;AAE9D,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAI7E;AAaD;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAiBpE;AAED,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,cAAc;;IAKlE,SAAgB,oBAAoB,EAAE,MAAM,GAAG,SAAS,CAAC;IAEzD,YAAmB,MAAM,EAAE,sBAAsB,EAAE,SAAS,EAAE,SAAS,EAItE;IAEM,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAExC;IA0BM,qBAAqB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAe1D;IAED;;;;OAIG;IACU,wBAAwB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CASnE;IAED;;;OAGG;IACU,2BAA2B,IAAI,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAWlF;IAED;;;;;OAKG;IACI,yBAAyB,CAAC,aAAa,EAAE,aAAa,GAAG,MAAM,GAAG,SAAS,CAUjF;IAED;;;;;;;OAOG;IACU,OAAO,CAClB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAC1B,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAY/B;IAEM,QAAQ,IAAI,WAAW,GAAG,SAAS,CAWzC;IAEM,UAAU,IAAI,aAAa,GAAG,SAAS,CAQ7C;IAEY,mBAAmB,CAAC,EAAE,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAoDpF;IA2CY,eAAe,CAAC,qBAAqB,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAsC/F;IAEY,OAAO,CAAC,qBAAqB,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAgElF;CA2CF"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
|
-
import { existsSync, realpathSync } from "node:fs";
|
|
3
|
-
import { mkdtemp, rename, rm } from "node:fs/promises";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { basename, dirname, join } from "node:path";
|
|
6
2
|
import { FETCH_SERVICE_ID, InstallMethod, KEY_VALUE_SERVICE_ID, PRINTER_SERVICE_ID, RunState, SPAWN_SERVICE_ID, SupportedArch, SupportedOs, } from "@flowscripter/dynamic-cli-framework-api";
|
|
7
3
|
import semver from "semver";
|
|
4
|
+
import { getLatestGithubReleaseVersion, upgradeViaGithubRelease } from "./method/githubRelease.js";
|
|
5
|
+
import { getLatestHomebrewVersion, isHomebrewInstalled, isRunningFromHomebrewCellar, resolveHomebrewOptExecutable, upgradeViaHomebrew, } from "./method/homebrew.js";
|
|
6
|
+
import { isLinuxScriptInstall, upgradeViaLinuxScript } from "./method/linuxScript.js";
|
|
7
|
+
import { getLatestWingetVersion, isWingetInstalled, upgradeViaWinget } from "./method/winget.js";
|
|
8
8
|
import getLogger from "../../util/logger.js";
|
|
9
9
|
const logger = getLogger("DefaultUpgradeService");
|
|
10
10
|
// KeyValueService key caching the resolved InstallMethod (see detectInstallMethod()). Stored
|
|
@@ -28,11 +28,6 @@ const CACHE_TTL_MILLIS = 24 * 60 * 60 * 1000;
|
|
|
28
28
|
// scoped KeyValueService. The banner never calls checkForUpgrade()/getUpgradeCheckResult() live,
|
|
29
29
|
// since checking live would stall startup on the network/spawn calls checkForUpgrade() makes.
|
|
30
30
|
export const UPGRADE_CHECK_CACHE_KEY = "upgrade-check-result";
|
|
31
|
-
function describeSpawnFailure(result) {
|
|
32
|
-
return "timedOut" in result
|
|
33
|
-
? "timed out"
|
|
34
|
-
: (result.error?.message ?? `exit code ${result.exitCode}`);
|
|
35
|
-
}
|
|
36
31
|
export function describeUpgradeCheckResult(result) {
|
|
37
32
|
return result.status === "failed"
|
|
38
33
|
? `Upgrade check result: failed - ${result.error.message}`
|
|
@@ -72,11 +67,6 @@ export function toRunState(result) {
|
|
|
72
67
|
}
|
|
73
68
|
return RunState.RUNTIME_ERROR;
|
|
74
69
|
}
|
|
75
|
-
const OS_LABELS = {
|
|
76
|
-
[SupportedOs.LINUX]: "Linux",
|
|
77
|
-
[SupportedOs.MACOS]: "MacOS",
|
|
78
|
-
[SupportedOs.WINDOWS]: "Windows",
|
|
79
|
-
};
|
|
80
70
|
export default class DefaultUpgradeService {
|
|
81
71
|
#context;
|
|
82
72
|
#upgradeCheckPromise;
|
|
@@ -111,36 +101,6 @@ export default class DefaultUpgradeService {
|
|
|
111
101
|
? this.#context.getServiceById(KEY_VALUE_SERVICE_ID)
|
|
112
102
|
: undefined;
|
|
113
103
|
}
|
|
114
|
-
// Mirrors SpawnInterfaceAdapter's plugin:add/plugin:remove pattern: wrap a spawned command's
|
|
115
|
-
// output in a quoted, marked block that's cleared on success (so a clean install stays quiet)
|
|
116
|
-
// but left on screen on failure (so the diagnostic output remains visible).
|
|
117
|
-
async #spawnQuoted(command) {
|
|
118
|
-
const spawnService = this.#spawnService;
|
|
119
|
-
const printerService = this.#printerService;
|
|
120
|
-
if (!printerService) {
|
|
121
|
-
return spawnService.spawn(command, { mode: "ignore" });
|
|
122
|
-
}
|
|
123
|
-
printerService.startQuote();
|
|
124
|
-
printerService.startMark();
|
|
125
|
-
// onOutput is synchronous and may be called concurrently for stdout/stderr lines, but
|
|
126
|
-
// printerService.info() is async and must not be invoked concurrently with itself - queue
|
|
127
|
-
// writes so they're applied one at a time, in call order.
|
|
128
|
-
let writeQueue = Promise.resolve();
|
|
129
|
-
const onOutput = (line) => {
|
|
130
|
-
writeQueue = writeQueue.then(() => printerService.info(`${line}\n`));
|
|
131
|
-
};
|
|
132
|
-
const result = await spawnService.spawn(command, { mode: "wrapped", onOutput });
|
|
133
|
-
await writeQueue;
|
|
134
|
-
printerService.endQuote();
|
|
135
|
-
printerService.endMark();
|
|
136
|
-
if (result.ok) {
|
|
137
|
-
await printerService.clearMarked();
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
printerService.discardMark();
|
|
141
|
-
}
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
104
|
getUpgradeCheckResult() {
|
|
145
105
|
if (!this.#upgradeCheckPromise) {
|
|
146
106
|
logger.debug(() => "Starting upgrade check");
|
|
@@ -198,28 +158,11 @@ export default class DefaultUpgradeService {
|
|
|
198
158
|
case InstallMethod.LINUX_SCRIPT:
|
|
199
159
|
return process.execPath;
|
|
200
160
|
case InstallMethod.HOMEBREW:
|
|
201
|
-
return this.#
|
|
161
|
+
return resolveHomebrewOptExecutable(this.#config.homebrew);
|
|
202
162
|
case InstallMethod.WINGET:
|
|
203
163
|
return undefined;
|
|
204
164
|
}
|
|
205
165
|
}
|
|
206
|
-
// The running executable lives in a version-specific Cellar directory, e.g.
|
|
207
|
-
// "<prefix>/Cellar/<formula>/<version>/bin/<exe>", while "<prefix>/opt/<formula>" links to the
|
|
208
|
-
// installed version, so the upgraded executable is "<prefix>/opt/<formula>/bin/<exe>".
|
|
209
|
-
#resolveHomebrewOptExecutable() {
|
|
210
|
-
if (!this.#config.homebrew) {
|
|
211
|
-
return undefined;
|
|
212
|
-
}
|
|
213
|
-
const { formula } = this.#config.homebrew;
|
|
214
|
-
const realExecutable = this.#resolveRealExecutable();
|
|
215
|
-
const cellarIndex = realExecutable.indexOf(`/Cellar/${formula}/`);
|
|
216
|
-
if (cellarIndex === -1) {
|
|
217
|
-
return undefined;
|
|
218
|
-
}
|
|
219
|
-
const prefix = realExecutable.slice(0, cellarIndex);
|
|
220
|
-
const executable = join(prefix, "opt", formula, "bin", basename(realExecutable));
|
|
221
|
-
return existsSync(executable) ? executable : undefined;
|
|
222
|
-
}
|
|
223
166
|
/**
|
|
224
167
|
* Run `executable` with `args` in place of this process, with terminal input and output
|
|
225
168
|
* inherited, and wait for it to exit. The child process sees the current version as its
|
|
@@ -270,13 +213,13 @@ export default class DefaultUpgradeService {
|
|
|
270
213
|
if (process.platform === "darwin" &&
|
|
271
214
|
os === SupportedOs.MACOS &&
|
|
272
215
|
this.#config.homebrew &&
|
|
273
|
-
|
|
216
|
+
isRunningFromHomebrewCellar(this.#config.homebrew.formula)) {
|
|
274
217
|
return InstallMethod.HOMEBREW;
|
|
275
218
|
}
|
|
276
219
|
if (process.platform === "linux" &&
|
|
277
220
|
os === SupportedOs.LINUX &&
|
|
278
221
|
this.#config.linuxScript &&
|
|
279
|
-
|
|
222
|
+
isLinuxScriptInstall()) {
|
|
280
223
|
return InstallMethod.LINUX_SCRIPT;
|
|
281
224
|
}
|
|
282
225
|
// Everything else needs an external process spawn (`brew list`, `winget list`) to confirm -
|
|
@@ -289,13 +232,13 @@ export default class DefaultUpgradeService {
|
|
|
289
232
|
if (process.platform === "darwin" &&
|
|
290
233
|
os === SupportedOs.MACOS &&
|
|
291
234
|
this.#config.homebrew &&
|
|
292
|
-
(await this.#
|
|
235
|
+
(await isHomebrewInstalled(this.#spawnService, this.#config.homebrew))) {
|
|
293
236
|
detected = InstallMethod.HOMEBREW;
|
|
294
237
|
}
|
|
295
238
|
else if (process.platform === "win32" &&
|
|
296
239
|
os === SupportedOs.WINDOWS &&
|
|
297
240
|
this.#config.winget &&
|
|
298
|
-
(await this.#
|
|
241
|
+
(await isWingetInstalled(this.#spawnService, this.#config.winget))) {
|
|
299
242
|
detected = InstallMethod.WINGET;
|
|
300
243
|
}
|
|
301
244
|
else if (this.#config.githubRelease) {
|
|
@@ -402,18 +345,20 @@ export default class DefaultUpgradeService {
|
|
|
402
345
|
if (this.#printerService) {
|
|
403
346
|
await this.#printerService.hideSpinner();
|
|
404
347
|
}
|
|
348
|
+
const spawnService = this.#spawnService;
|
|
349
|
+
const printerService = this.#printerService;
|
|
405
350
|
switch (checkResult.installMethod) {
|
|
406
351
|
case InstallMethod.LINUX_SCRIPT:
|
|
407
|
-
await this.#
|
|
352
|
+
await upgradeViaLinuxScript(spawnService, printerService, this.#config.linuxScript);
|
|
408
353
|
break;
|
|
409
354
|
case InstallMethod.HOMEBREW:
|
|
410
|
-
await this.#
|
|
355
|
+
await upgradeViaHomebrew(spawnService, printerService, this.#config.homebrew, checkResult.latestVersion);
|
|
411
356
|
break;
|
|
412
357
|
case InstallMethod.WINGET:
|
|
413
|
-
await this.#
|
|
358
|
+
await upgradeViaWinget(spawnService, printerService, this.#config.winget);
|
|
414
359
|
break;
|
|
415
360
|
case InstallMethod.GITHUB_RELEASE:
|
|
416
|
-
await this.#
|
|
361
|
+
await upgradeViaGithubRelease(spawnService, this.#fetchService, printerService, this.#config.githubRelease, this.#cliConfig.name, checkResult.os, checkResult.arch);
|
|
417
362
|
break;
|
|
418
363
|
}
|
|
419
364
|
return { ok: true, oldVersion, newVersion: checkResult.latestVersion };
|
|
@@ -425,40 +370,6 @@ export default class DefaultUpgradeService {
|
|
|
425
370
|
#isPlatformSupported(os, arch) {
|
|
426
371
|
return this.#config.supportedPlatforms.some((platform) => platform.os === os && platform.arch === arch);
|
|
427
372
|
}
|
|
428
|
-
// Homebrew relinks a formula's installed binary from its Cellar directory into a `bin/` symlink,
|
|
429
|
-
// so resolving the running executable's real path confirms a homebrew install without spawning
|
|
430
|
-
// `brew`, which has a slow cold start - avoiding it keeps the background upgrade-check
|
|
431
|
-
// StartupTask (see UpgradeServiceProvider) fast even though it runs to completion.
|
|
432
|
-
#isRunningFromHomebrewCellar(formula) {
|
|
433
|
-
return this.#resolveRealExecutable().includes(`/Cellar/${formula}/`);
|
|
434
|
-
}
|
|
435
|
-
#resolveRealExecutable() {
|
|
436
|
-
try {
|
|
437
|
-
return realpathSync(process.execPath);
|
|
438
|
-
}
|
|
439
|
-
catch {
|
|
440
|
-
// process.execPath may not resolve on disk (e.g. a fabricated path in tests) - fall back to
|
|
441
|
-
// the unresolved path rather than treating that as "not installed".
|
|
442
|
-
return process.execPath;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
async #isHomebrewInstalled() {
|
|
446
|
-
if (!this.#spawnService || !this.#config.homebrew) {
|
|
447
|
-
return false;
|
|
448
|
-
}
|
|
449
|
-
const result = await this.#spawnService.spawn(["brew", "list", "--versions", this.#config.homebrew.formula], { mode: "ignore", longRunning: false });
|
|
450
|
-
return result.ok;
|
|
451
|
-
}
|
|
452
|
-
async #isWingetInstalled() {
|
|
453
|
-
if (!this.#spawnService || !this.#config.winget) {
|
|
454
|
-
return false;
|
|
455
|
-
}
|
|
456
|
-
const result = await this.#spawnService.spawn(["winget", "list", "--id", this.#config.winget.packageId], { mode: "ignore", longRunning: false });
|
|
457
|
-
return result.ok;
|
|
458
|
-
}
|
|
459
|
-
#isLinuxScriptInstall() {
|
|
460
|
-
return process.execPath.startsWith("/usr/local/bin/");
|
|
461
|
-
}
|
|
462
373
|
async #getLatestVersion(installMethod) {
|
|
463
374
|
const cacheKey = `${LATEST_VERSION_CACHE_KEY_PREFIX}${installMethod}`;
|
|
464
375
|
const keyValueService = this.#keyValueService;
|
|
@@ -481,234 +392,11 @@ export default class DefaultUpgradeService {
|
|
|
481
392
|
switch (installMethod) {
|
|
482
393
|
case InstallMethod.GITHUB_RELEASE:
|
|
483
394
|
case InstallMethod.LINUX_SCRIPT:
|
|
484
|
-
return this.#
|
|
395
|
+
return getLatestGithubReleaseVersion(this.#fetchService, this.#config.githubRelease);
|
|
485
396
|
case InstallMethod.HOMEBREW:
|
|
486
|
-
return this.#
|
|
397
|
+
return getLatestHomebrewVersion(this.#fetchService, this.#config.homebrew);
|
|
487
398
|
case InstallMethod.WINGET:
|
|
488
|
-
return this.#
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
async #getLatestGithubReleaseVersion() {
|
|
492
|
-
if (!this.#config.githubRelease) {
|
|
493
|
-
return { ok: false, error: new Error("No githubRelease location configured") };
|
|
494
|
-
}
|
|
495
|
-
if (!this.#fetchService) {
|
|
496
|
-
return { ok: false, error: new Error("FetchService is not available") };
|
|
497
|
-
}
|
|
498
|
-
const { owner, repo } = this.#config.githubRelease;
|
|
499
|
-
try {
|
|
500
|
-
// Uses the plain web redirect rather than the api.github.com REST endpoint, since the
|
|
501
|
-
// latter's unauthenticated rate limit (60 requests/hour/IP) is easily exhausted, e.g. by
|
|
502
|
-
// CI runners sharing an IP pool.
|
|
503
|
-
const response = await this.#fetchService.fetch(`https://github.com/${owner}/${repo}/releases/latest`, { redirect: "manual" });
|
|
504
|
-
const location = response.headers.get("location");
|
|
505
|
-
const version = location ? /\/releases\/tag\/v?([^/]+)$/.exec(location)?.[1] : undefined;
|
|
506
|
-
if (!version) {
|
|
507
|
-
return {
|
|
508
|
-
ok: false,
|
|
509
|
-
error: new Error(`Unexpected response resolving latest release for ${owner}/${repo}: HTTP ${response.status}`),
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
return { ok: true, version };
|
|
513
|
-
}
|
|
514
|
-
catch (error) {
|
|
515
|
-
return {
|
|
516
|
-
ok: false,
|
|
517
|
-
error: new Error(`Failed to fetch latest GitHub release for ${owner}/${repo}: ${error}`),
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
async #getLatestHomebrewVersion() {
|
|
522
|
-
if (!this.#config.homebrew) {
|
|
523
|
-
return { ok: false, error: new Error("No homebrew location configured") };
|
|
524
|
-
}
|
|
525
|
-
if (!this.#fetchService) {
|
|
526
|
-
return { ok: false, error: new Error("FetchService is not available") };
|
|
527
|
-
}
|
|
528
|
-
const { tap, formula } = this.#config.homebrew;
|
|
529
|
-
const [tapOwner, tapName] = tap.split("/");
|
|
530
|
-
if (!tapOwner || !tapName) {
|
|
531
|
-
return { ok: false, error: new Error(`Invalid homebrew tap '${tap}'`) };
|
|
532
|
-
}
|
|
533
|
-
try {
|
|
534
|
-
const response = await this.#fetchService.fetch(`https://raw.githubusercontent.com/${tapOwner}/homebrew-${tapName}/main/${formula}.rb`);
|
|
535
|
-
if (!response.ok) {
|
|
536
|
-
return {
|
|
537
|
-
ok: false,
|
|
538
|
-
error: new Error(`Failed to fetch homebrew formula for ${tap}/${formula}: HTTP ${response.status}`),
|
|
539
|
-
};
|
|
540
|
-
}
|
|
541
|
-
const text = await response.text();
|
|
542
|
-
const version = /version\s+"v?([^"]+)"/.exec(text)?.[1];
|
|
543
|
-
if (!version) {
|
|
544
|
-
return {
|
|
545
|
-
ok: false,
|
|
546
|
-
error: new Error(`Could not parse version from homebrew formula ${tap}/${formula}`),
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
return { ok: true, version };
|
|
550
|
-
}
|
|
551
|
-
catch (error) {
|
|
552
|
-
return {
|
|
553
|
-
ok: false,
|
|
554
|
-
error: new Error(`Failed to fetch homebrew formula for ${tap}/${formula}: ${error}`),
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
async #getLatestWingetVersion() {
|
|
559
|
-
if (!this.#config.winget) {
|
|
560
|
-
return { ok: false, error: new Error("No winget location configured") };
|
|
561
|
-
}
|
|
562
|
-
if (!this.#spawnService) {
|
|
563
|
-
return { ok: false, error: new Error("SpawnService is not available") };
|
|
564
|
-
}
|
|
565
|
-
const lines = [];
|
|
566
|
-
const result = await this.#spawnService.spawn(["winget", "show", "--id", this.#config.winget.packageId], {
|
|
567
|
-
mode: "wrapped",
|
|
568
|
-
longRunning: false,
|
|
569
|
-
onOutput: (line) => lines.push(line),
|
|
570
|
-
});
|
|
571
|
-
if (!result.ok) {
|
|
572
|
-
return { ok: false, error: new Error(`winget show failed: ${describeSpawnFailure(result)}`) };
|
|
573
|
-
}
|
|
574
|
-
for (const line of lines) {
|
|
575
|
-
const match = /Version:\s*(\S+)/.exec(line);
|
|
576
|
-
if (match?.[1]) {
|
|
577
|
-
return { ok: true, version: match[1] };
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return { ok: false, error: new Error("Could not parse version from winget output") };
|
|
581
|
-
}
|
|
582
|
-
async #upgradeViaLinuxScript() {
|
|
583
|
-
const { scriptUrl } = this.#config.linuxScript;
|
|
584
|
-
const result = await this.#spawnQuoted(["sh", "-c", `curl -fsSL ${scriptUrl} | sh`]);
|
|
585
|
-
if (!result.ok) {
|
|
586
|
-
throw new Error(`Install script failed: ${describeSpawnFailure(result)}`);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
// The latest version is read from the tap's formula on GitHub, but `brew upgrade` only sees the
|
|
590
|
-
// local tap clone, which is stale until `brew update` runs. A stale tap makes `brew upgrade`
|
|
591
|
-
// exit 0 with "already installed", so update first and then confirm the installed version.
|
|
592
|
-
async #upgradeViaHomebrew(expectedVersion) {
|
|
593
|
-
const { tap, formula } = this.#config.homebrew;
|
|
594
|
-
const updateResult = await this.#spawnQuoted(["brew", "update"]);
|
|
595
|
-
if (!updateResult.ok) {
|
|
596
|
-
throw new Error(`brew update failed: ${describeSpawnFailure(updateResult)}`);
|
|
597
|
-
}
|
|
598
|
-
const result = await this.#spawnQuoted(["brew", "upgrade", `${tap}/${formula}`]);
|
|
599
|
-
if (!result.ok) {
|
|
600
|
-
throw new Error(`brew upgrade failed: ${describeSpawnFailure(result)}`);
|
|
601
|
-
}
|
|
602
|
-
const lines = [];
|
|
603
|
-
const listResult = await this.#spawnService.spawn(["brew", "list", "--versions", formula], {
|
|
604
|
-
mode: "wrapped",
|
|
605
|
-
longRunning: false,
|
|
606
|
-
onOutput: (line) => lines.push(line),
|
|
607
|
-
});
|
|
608
|
-
const installed = lines.join(" ").trim();
|
|
609
|
-
const installedVersions = installed.split(/\s+/).slice(1);
|
|
610
|
-
if (!listResult.ok || !installedVersions.includes(expectedVersion)) {
|
|
611
|
-
throw new Error(`brew upgrade completed but version ${expectedVersion} is not installed (installed: ${installed || "unknown"})`);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
async #upgradeViaWinget() {
|
|
615
|
-
const { packageId } = this.#config.winget;
|
|
616
|
-
const result = await this.#spawnQuoted([
|
|
617
|
-
"winget",
|
|
618
|
-
"upgrade",
|
|
619
|
-
"--id",
|
|
620
|
-
packageId,
|
|
621
|
-
"--silent",
|
|
622
|
-
"--accept-package-agreements",
|
|
623
|
-
"--accept-source-agreements",
|
|
624
|
-
]);
|
|
625
|
-
if (!result.ok) {
|
|
626
|
-
throw new Error(`winget upgrade failed: ${describeSpawnFailure(result)}`);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
async #upgradeViaGithubRelease(os, arch) {
|
|
630
|
-
const { owner, repo, assetPattern } = this.#config.githubRelease;
|
|
631
|
-
// macOS release assets use "aarch64" rather than "arm64" for the arm64 build; x64 (including
|
|
632
|
-
// Intel Macs) always uses "x64" regardless of os.
|
|
633
|
-
const archLabel = arch === SupportedArch.X64 ? "x64" : os === SupportedOs.MACOS ? "aarch64" : "arm64";
|
|
634
|
-
const assetName = assetPattern.replace("{os}", OS_LABELS[os]).replace("{arch}", archLabel);
|
|
635
|
-
const url = `https://github.com/${owner}/${repo}/releases/latest/download/${assetName}`;
|
|
636
|
-
// longRunning: true gets cooperative Ctrl-C handling during what can be the slowest step of
|
|
637
|
-
// the upgrade.
|
|
638
|
-
const response = await this.#fetchService.fetch(url, { longRunning: true });
|
|
639
|
-
if (!response.ok) {
|
|
640
|
-
throw new Error(`Failed to download release asset '${assetName}': HTTP ${response.status}`);
|
|
641
|
-
}
|
|
642
|
-
const archiveData = await response.arrayBuffer();
|
|
643
|
-
const tmpDir = await mkdtemp(join(tmpdir(), "upgrade-"));
|
|
644
|
-
const archivePath = join(tmpDir, assetName);
|
|
645
|
-
await Bun.write(archivePath, archiveData);
|
|
646
|
-
const currentExecutable = process.execPath;
|
|
647
|
-
if (os === SupportedOs.WINDOWS) {
|
|
648
|
-
const extractResult = await this.#spawnQuoted([
|
|
649
|
-
"powershell",
|
|
650
|
-
"-Command",
|
|
651
|
-
`Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpDir}' -Force`,
|
|
652
|
-
]);
|
|
653
|
-
if (!extractResult.ok) {
|
|
654
|
-
throw new Error("Failed to extract release archive");
|
|
655
|
-
}
|
|
656
|
-
const extractedBinary = join(tmpDir, `${this.#cliConfig.name}.exe`);
|
|
657
|
-
const oldPath = `${currentExecutable}.old.exe`;
|
|
658
|
-
// Best-effort cleanup of a stale "<exe>.old.exe" left behind by a *previous* upgrade run.
|
|
659
|
-
// Windows won't let us delete the just-renamed-aside exe while this process still has it
|
|
660
|
-
// open/mapped - that can only happen once we're no longer holding it, i.e. at the start of
|
|
661
|
-
// the NEXT invocation, before we move today's running exe aside. Ignore failures: the file
|
|
662
|
-
// may not exist, or may still be locked (e.g. another instance still running). Not quoted -
|
|
663
|
-
// this is expected to fail silently, there's nothing worth showing the user.
|
|
664
|
-
await this.#spawnService.spawn(["cmd", "/c", "del", "/f", "/q", oldPath], {
|
|
665
|
-
mode: "ignore",
|
|
666
|
-
});
|
|
667
|
-
const moveResult = await this.#spawnQuoted([
|
|
668
|
-
"cmd",
|
|
669
|
-
"/c",
|
|
670
|
-
"move",
|
|
671
|
-
"/y",
|
|
672
|
-
currentExecutable,
|
|
673
|
-
oldPath,
|
|
674
|
-
]);
|
|
675
|
-
if (!moveResult.ok) {
|
|
676
|
-
throw new Error("Failed to move current executable aside");
|
|
677
|
-
}
|
|
678
|
-
const copyResult = await this.#spawnQuoted([
|
|
679
|
-
"cmd",
|
|
680
|
-
"/c",
|
|
681
|
-
"copy",
|
|
682
|
-
"/y",
|
|
683
|
-
extractedBinary,
|
|
684
|
-
currentExecutable,
|
|
685
|
-
]);
|
|
686
|
-
if (!copyResult.ok) {
|
|
687
|
-
throw new Error("Failed to copy new executable into place");
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
else {
|
|
691
|
-
const extractResult = await this.#spawnQuoted(["unzip", "-o", archivePath, "-d", tmpDir]);
|
|
692
|
-
if (!extractResult.ok) {
|
|
693
|
-
throw new Error("Failed to extract release archive");
|
|
694
|
-
}
|
|
695
|
-
const extractedBinary = join(tmpDir, this.#cliConfig.name);
|
|
696
|
-
// Extract into a staging file in the SAME directory as the running executable (not
|
|
697
|
-
// os.tmpdir(), which may be a different filesystem/mount), then atomically rename it over
|
|
698
|
-
// currentExecutable. This avoids ETXTBSY: the kernel refuses to open-for-write the inode
|
|
699
|
-
// mapped as a running process's text segment, but rename() only swaps the directory entry
|
|
700
|
-
// to point at a different inode - the running process keeps executing from its original,
|
|
701
|
-
// now-unlinked-but-still-open inode until it next execs/restarts.
|
|
702
|
-
const stagingDir = await mkdtemp(join(dirname(currentExecutable), ".upgrade-"));
|
|
703
|
-
try {
|
|
704
|
-
const stagingBinary = join(stagingDir, this.#cliConfig.name);
|
|
705
|
-
await Bun.write(stagingBinary, Bun.file(extractedBinary));
|
|
706
|
-
await this.#spawnService.spawn(["chmod", "+x", stagingBinary], { mode: "ignore" });
|
|
707
|
-
await rename(stagingBinary, currentExecutable);
|
|
708
|
-
}
|
|
709
|
-
finally {
|
|
710
|
-
await rm(stagingDir, { recursive: true, force: true });
|
|
711
|
-
}
|
|
399
|
+
return getLatestWingetVersion(this.#spawnService, this.#config.winget);
|
|
712
400
|
}
|
|
713
401
|
}
|
|
714
402
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type FetchService, type PrinterService, type SpawnService, SupportedArch, SupportedOs } from "@flowscripter/dynamic-cli-framework-api";
|
|
2
|
+
import type { GithubReleaseLocation } from "../UpgradeLocationsConfig.ts";
|
|
3
|
+
import { type VersionLookupResult } from "./shared.ts";
|
|
4
|
+
export declare function getLatestGithubReleaseVersion(fetchService: FetchService | undefined, location: GithubReleaseLocation | undefined): Promise<VersionLookupResult>;
|
|
5
|
+
export declare function upgradeViaGithubRelease(spawnService: SpawnService, fetchService: FetchService, printerService: PrinterService | undefined, location: GithubReleaseLocation, cliName: string, os: SupportedOs, arch: SupportedArch): Promise<void>;
|
|
6
|
+
//# sourceMappingURL=githubRelease.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"githubRelease.d.ts","sourceRoot":"","sources":["../../../../../src/service/upgrade/method/githubRelease.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,aAAa,EACb,WAAW,EACZ,MAAM,yCAAyC,CAAC;AACjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAe,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAQpE,wBAAsB,6BAA6B,CACjD,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,QAAQ,EAAE,qBAAqB,GAAG,SAAS,GAC1C,OAAO,CAAC,mBAAmB,CAAC,CAiC9B;AAED,wBAAsB,uBAAuB,CAC3C,YAAY,EAAE,YAAY,EAC1B,YAAY,EAAE,YAAY,EAC1B,cAAc,EAAE,cAAc,GAAG,SAAS,EAC1C,QAAQ,EAAE,qBAAqB,EAC/B,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,WAAW,EACf,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAgGf"}
|