@flowscripter/dynamic-cli-framework 2.3.3 → 2.5.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/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/src/cli/BaseCLI.d.ts +1 -0
- package/dist/src/cli/BaseCLI.d.ts.map +1 -1
- package/dist/src/cli/BaseCLI.js +6 -0
- package/dist/src/cli/BaseCLIFeatureOptions.d.ts +1 -0
- package/dist/src/cli/BaseCLIFeatureOptions.d.ts.map +1 -1
- package/dist/src/command/VersionCommand.js +1 -1
- package/dist/src/service/banner/BannerServiceProvider.js +1 -1
- package/dist/src/service/fetch/DefaultFetchService.d.ts +7 -0
- package/dist/src/service/fetch/DefaultFetchService.d.ts.map +1 -0
- package/dist/src/service/fetch/DefaultFetchService.js +44 -0
- package/dist/src/service/fetch/FetchServiceProvider.d.ts +10 -0
- package/dist/src/service/fetch/FetchServiceProvider.d.ts.map +1 -0
- package/dist/src/service/fetch/FetchServiceProvider.js +22 -0
- package/dist/src/service/plugin/DefaultPluginServiceProvider.d.ts +2 -1
- package/dist/src/service/plugin/DefaultPluginServiceProvider.d.ts.map +1 -1
- package/dist/src/service/plugin/DefaultPluginServiceProvider.js +23 -8
- package/dist/src/service/plugin/FetchInterfaceAdapter.d.ts +17 -0
- package/dist/src/service/plugin/FetchInterfaceAdapter.d.ts.map +1 -0
- package/dist/src/service/plugin/FetchInterfaceAdapter.js +16 -0
- package/dist/src/service/plugin/SpawnInterfaceAdapter.d.ts.map +1 -1
- package/dist/src/service/plugin/SpawnInterfaceAdapter.js +6 -3
- package/dist/src/service/spawn/DefaultSpawnService.d.ts.map +1 -1
- package/dist/src/service/spawn/DefaultSpawnService.js +38 -16
- package/dist/src/service/upgrade/DefaultUpgradeService.d.ts +4 -2
- package/dist/src/service/upgrade/DefaultUpgradeService.d.ts.map +1 -1
- package/dist/src/service/upgrade/DefaultUpgradeService.js +62 -75
- package/dist/src/service/upgrade/UpgradeServiceProvider.d.ts.map +1 -1
- package/dist/src/service/upgrade/UpgradeServiceProvider.js +14 -10
- package/dist/src/service/upgrade/command/UpgradeSubCommand.d.ts.map +1 -1
- package/dist/src/service/upgrade/command/UpgradeSubCommand.js +3 -1
- package/index.ts +5 -0
- package/package.json +3 -3
- package/src/cli/BaseCLI.ts +7 -0
- package/src/cli/BaseCLIFeatureOptions.ts +1 -0
- package/src/command/VersionCommand.ts +1 -1
- package/src/service/banner/BannerServiceProvider.ts +1 -1
- package/src/service/fetch/DefaultFetchService.ts +55 -0
- package/src/service/fetch/FetchServiceProvider.ts +33 -0
- package/src/service/plugin/DefaultPluginServiceProvider.ts +29 -12
- package/src/service/plugin/FetchInterfaceAdapter.ts +28 -0
- package/src/service/plugin/SpawnInterfaceAdapter.ts +6 -3
- package/src/service/spawn/DefaultSpawnService.ts +45 -16
- package/src/service/upgrade/DefaultUpgradeService.ts +107 -102
- package/src/service/upgrade/UpgradeServiceProvider.ts +16 -10
- package/src/service/upgrade/command/UpgradeSubCommand.ts +4 -1
|
@@ -7,17 +7,12 @@ import semver from "semver";
|
|
|
7
7
|
import getLogger from "../../util/logger.js";
|
|
8
8
|
const logger = getLogger("DefaultUpgradeService");
|
|
9
9
|
// checkForUpgrade() runs opportunistically on every CLI invocation (via BannerServiceProvider),
|
|
10
|
-
// so its version-check network calls must never be allowed to stall CLI startup.
|
|
11
|
-
const VERSION_CHECK_TIMEOUT_MS = 250;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
async function withSpawnTimeout(spawnPromise, fallback) {
|
|
17
|
-
return Promise.race([
|
|
18
|
-
spawnPromise,
|
|
19
|
-
new Promise((resolve) => setTimeout(() => resolve(fallback), VERSION_CHECK_TIMEOUT_MS)),
|
|
20
|
-
]);
|
|
10
|
+
// so its version-check network/spawn calls must never be allowed to stall CLI startup.
|
|
11
|
+
export const VERSION_CHECK_TIMEOUT_MS = 250;
|
|
12
|
+
function describeSpawnFailure(result) {
|
|
13
|
+
return "timedOut" in result
|
|
14
|
+
? "timed out"
|
|
15
|
+
: (result.error?.message ?? `exit code ${result.exitCode}`);
|
|
21
16
|
}
|
|
22
17
|
const OS_LABELS = {
|
|
23
18
|
[SupportedOs.LINUX]: "Linux",
|
|
@@ -26,16 +21,29 @@ const OS_LABELS = {
|
|
|
26
21
|
};
|
|
27
22
|
export default class DefaultUpgradeService {
|
|
28
23
|
#spawnService;
|
|
29
|
-
#
|
|
24
|
+
#fetchService;
|
|
25
|
+
#upgradeCheckPromise;
|
|
30
26
|
#config;
|
|
31
27
|
#cliConfig;
|
|
32
28
|
constructor(config, cliConfig) {
|
|
33
29
|
this.#config = config;
|
|
34
30
|
this.#cliConfig = cliConfig;
|
|
35
31
|
}
|
|
36
|
-
setDependencies(spawnService,
|
|
32
|
+
setDependencies(spawnService, fetchService) {
|
|
37
33
|
this.#spawnService = spawnService;
|
|
38
|
-
this.#
|
|
34
|
+
this.#fetchService = fetchService;
|
|
35
|
+
}
|
|
36
|
+
getUpgradeCheckResult(waitForResult = false) {
|
|
37
|
+
if (!this.#upgradeCheckPromise) {
|
|
38
|
+
this.#upgradeCheckPromise = this.checkForUpgrade();
|
|
39
|
+
}
|
|
40
|
+
if (waitForResult) {
|
|
41
|
+
return this.#upgradeCheckPromise;
|
|
42
|
+
}
|
|
43
|
+
return Promise.race([
|
|
44
|
+
this.#upgradeCheckPromise,
|
|
45
|
+
new Promise((resolve) => setTimeout(() => resolve(undefined), VERSION_CHECK_TIMEOUT_MS)),
|
|
46
|
+
]);
|
|
39
47
|
}
|
|
40
48
|
detectOs() {
|
|
41
49
|
switch (process.platform) {
|
|
@@ -106,7 +114,10 @@ export default class DefaultUpgradeService {
|
|
|
106
114
|
}
|
|
107
115
|
async upgrade(osOverride, archOverride, installMethodOverride) {
|
|
108
116
|
const oldVersion = this.#cliConfig.version;
|
|
109
|
-
const
|
|
117
|
+
const hasOverride = osOverride !== undefined || archOverride !== undefined || installMethodOverride !== undefined;
|
|
118
|
+
const checkResult = hasOverride
|
|
119
|
+
? await this.checkForUpgrade(osOverride, archOverride, installMethodOverride)
|
|
120
|
+
: await this.getUpgradeCheckResult(true);
|
|
110
121
|
if (!checkResult) {
|
|
111
122
|
return {
|
|
112
123
|
ok: false,
|
|
@@ -117,6 +128,9 @@ export default class DefaultUpgradeService {
|
|
|
117
128
|
if (!this.#spawnService) {
|
|
118
129
|
return { ok: false, oldVersion, error: new Error("SpawnService is not available") };
|
|
119
130
|
}
|
|
131
|
+
if (checkResult.installMethod === InstallMethod.GITHUB_RELEASE && !this.#fetchService) {
|
|
132
|
+
return { ok: false, oldVersion, error: new Error("FetchService is not available") };
|
|
133
|
+
}
|
|
120
134
|
try {
|
|
121
135
|
switch (checkResult.installMethod) {
|
|
122
136
|
case InstallMethod.LINUX_SCRIPT:
|
|
@@ -145,20 +159,14 @@ export default class DefaultUpgradeService {
|
|
|
145
159
|
if (!this.#spawnService || !this.#config.homebrew) {
|
|
146
160
|
return false;
|
|
147
161
|
}
|
|
148
|
-
const result = await
|
|
149
|
-
stdio: "wrapped",
|
|
150
|
-
longRunning: false,
|
|
151
|
-
}), { ok: false });
|
|
162
|
+
const result = await this.#spawnService.spawn(["brew", "list", "--versions", this.#config.homebrew.formula], { mode: "ignore", longRunning: false, timeoutMs: VERSION_CHECK_TIMEOUT_MS });
|
|
152
163
|
return result.ok;
|
|
153
164
|
}
|
|
154
165
|
async #isWingetInstalled() {
|
|
155
166
|
if (!this.#spawnService || !this.#config.winget) {
|
|
156
167
|
return false;
|
|
157
168
|
}
|
|
158
|
-
const result = await
|
|
159
|
-
stdio: "wrapped",
|
|
160
|
-
longRunning: false,
|
|
161
|
-
}), { ok: false });
|
|
169
|
+
const result = await this.#spawnService.spawn(["winget", "list", "--id", this.#config.winget.packageId], { mode: "ignore", longRunning: false, timeoutMs: VERSION_CHECK_TIMEOUT_MS });
|
|
162
170
|
return result.ok;
|
|
163
171
|
}
|
|
164
172
|
#isLinuxScriptInstall() {
|
|
@@ -178,12 +186,12 @@ export default class DefaultUpgradeService {
|
|
|
178
186
|
}
|
|
179
187
|
}
|
|
180
188
|
async #getLatestGithubReleaseVersion() {
|
|
181
|
-
if (!this.#config.githubRelease) {
|
|
189
|
+
if (!this.#config.githubRelease || !this.#fetchService) {
|
|
182
190
|
return undefined;
|
|
183
191
|
}
|
|
184
192
|
const { owner, repo } = this.#config.githubRelease;
|
|
185
193
|
try {
|
|
186
|
-
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`, {
|
|
194
|
+
const response = await this.#fetchService.fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`, { timeoutMs: VERSION_CHECK_TIMEOUT_MS });
|
|
187
195
|
if (!response.ok) {
|
|
188
196
|
return undefined;
|
|
189
197
|
}
|
|
@@ -196,7 +204,7 @@ export default class DefaultUpgradeService {
|
|
|
196
204
|
}
|
|
197
205
|
}
|
|
198
206
|
async #getLatestHomebrewVersion() {
|
|
199
|
-
if (!this.#config.homebrew) {
|
|
207
|
+
if (!this.#config.homebrew || !this.#fetchService) {
|
|
200
208
|
return undefined;
|
|
201
209
|
}
|
|
202
210
|
const { tap, formula } = this.#config.homebrew;
|
|
@@ -205,7 +213,7 @@ export default class DefaultUpgradeService {
|
|
|
205
213
|
return undefined;
|
|
206
214
|
}
|
|
207
215
|
try {
|
|
208
|
-
const response = await fetch(`https://raw.githubusercontent.com/${tapOwner}/homebrew-${tapName}/main/${formula}.rb`, {
|
|
216
|
+
const response = await this.#fetchService.fetch(`https://raw.githubusercontent.com/${tapOwner}/homebrew-${tapName}/main/${formula}.rb`, { timeoutMs: VERSION_CHECK_TIMEOUT_MS });
|
|
209
217
|
if (!response.ok) {
|
|
210
218
|
return undefined;
|
|
211
219
|
}
|
|
@@ -222,11 +230,12 @@ export default class DefaultUpgradeService {
|
|
|
222
230
|
return undefined;
|
|
223
231
|
}
|
|
224
232
|
const lines = [];
|
|
225
|
-
const result = await
|
|
226
|
-
|
|
233
|
+
const result = await this.#spawnService.spawn(["winget", "show", "--id", this.#config.winget.packageId], {
|
|
234
|
+
mode: "wrapped",
|
|
227
235
|
longRunning: false,
|
|
236
|
+
timeoutMs: VERSION_CHECK_TIMEOUT_MS,
|
|
228
237
|
onOutput: (line) => lines.push(line),
|
|
229
|
-
})
|
|
238
|
+
});
|
|
230
239
|
if (!result.ok) {
|
|
231
240
|
return undefined;
|
|
232
241
|
}
|
|
@@ -240,16 +249,20 @@ export default class DefaultUpgradeService {
|
|
|
240
249
|
}
|
|
241
250
|
async #upgradeViaLinuxScript() {
|
|
242
251
|
const { scriptUrl } = this.#config.linuxScript;
|
|
243
|
-
const result = await this.#spawnService.spawn(["sh", "-c", `curl -fsSL ${scriptUrl} | sh`]
|
|
252
|
+
const result = await this.#spawnService.spawn(["sh", "-c", `curl -fsSL ${scriptUrl} | sh`], {
|
|
253
|
+
mode: "ignore",
|
|
254
|
+
});
|
|
244
255
|
if (!result.ok) {
|
|
245
|
-
throw new Error(`Install script failed: ${result
|
|
256
|
+
throw new Error(`Install script failed: ${describeSpawnFailure(result)}`);
|
|
246
257
|
}
|
|
247
258
|
}
|
|
248
259
|
async #upgradeViaHomebrew() {
|
|
249
260
|
const { tap, formula } = this.#config.homebrew;
|
|
250
|
-
const result = await this.#spawnService.spawn(["brew", "upgrade", `${tap}/${formula}`]
|
|
261
|
+
const result = await this.#spawnService.spawn(["brew", "upgrade", `${tap}/${formula}`], {
|
|
262
|
+
mode: "ignore",
|
|
263
|
+
});
|
|
251
264
|
if (!result.ok) {
|
|
252
|
-
throw new Error(`brew upgrade failed: ${result
|
|
265
|
+
throw new Error(`brew upgrade failed: ${describeSpawnFailure(result)}`);
|
|
253
266
|
}
|
|
254
267
|
}
|
|
255
268
|
async #upgradeViaWinget() {
|
|
@@ -262,9 +275,9 @@ export default class DefaultUpgradeService {
|
|
|
262
275
|
"--silent",
|
|
263
276
|
"--accept-package-agreements",
|
|
264
277
|
"--accept-source-agreements",
|
|
265
|
-
]);
|
|
278
|
+
], { mode: "ignore" });
|
|
266
279
|
if (!result.ok) {
|
|
267
|
-
throw new Error(`winget upgrade failed: ${result
|
|
280
|
+
throw new Error(`winget upgrade failed: ${describeSpawnFailure(result)}`);
|
|
268
281
|
}
|
|
269
282
|
}
|
|
270
283
|
async #upgradeViaGithubRelease(os, arch) {
|
|
@@ -272,21 +285,13 @@ export default class DefaultUpgradeService {
|
|
|
272
285
|
const archLabel = os === SupportedOs.MACOS ? "aarch64" : arch === SupportedArch.X64 ? "x64" : "arm64";
|
|
273
286
|
const assetName = assetPattern.replace("{os}", OS_LABELS[os]).replace("{arch}", archLabel);
|
|
274
287
|
const url = `https://github.com/${owner}/${repo}/releases/latest/download/${assetName}`;
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
const response = await fetch(url);
|
|
282
|
-
if (!response.ok) {
|
|
283
|
-
throw new Error(`Failed to download release asset '${assetName}': HTTP ${response.status}`);
|
|
284
|
-
}
|
|
285
|
-
archiveData = await response.arrayBuffer();
|
|
286
|
-
}
|
|
287
|
-
finally {
|
|
288
|
-
this.#shutdownService?.leaveLongRunningMode();
|
|
288
|
+
// longRunning: true gets cooperative Ctrl-C handling during what can be the slowest step of
|
|
289
|
+
// the upgrade.
|
|
290
|
+
const response = await this.#fetchService.fetch(url, { longRunning: true });
|
|
291
|
+
if (!response.ok) {
|
|
292
|
+
throw new Error(`Failed to download release asset '${assetName}': HTTP ${response.status}`);
|
|
289
293
|
}
|
|
294
|
+
const archiveData = await response.arrayBuffer();
|
|
290
295
|
const tmpDir = await mkdtemp(join(tmpdir(), "upgrade-"));
|
|
291
296
|
const archivePath = join(tmpDir, assetName);
|
|
292
297
|
await Bun.write(archivePath, archiveData);
|
|
@@ -296,49 +301,31 @@ export default class DefaultUpgradeService {
|
|
|
296
301
|
"powershell",
|
|
297
302
|
"-Command",
|
|
298
303
|
`Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpDir}' -Force`,
|
|
299
|
-
]);
|
|
304
|
+
], { mode: "ignore" });
|
|
300
305
|
if (!extractResult.ok) {
|
|
301
306
|
throw new Error("Failed to extract release archive");
|
|
302
307
|
}
|
|
303
308
|
const extractedBinary = join(tmpDir, `${this.#cliConfig.name}.exe`);
|
|
304
309
|
const oldPath = `${currentExecutable}.old.exe`;
|
|
305
|
-
const moveResult = await this.#spawnService.spawn([
|
|
306
|
-
"cmd",
|
|
307
|
-
"/c",
|
|
308
|
-
"move",
|
|
309
|
-
"/y",
|
|
310
|
-
currentExecutable,
|
|
311
|
-
oldPath,
|
|
312
|
-
]);
|
|
310
|
+
const moveResult = await this.#spawnService.spawn(["cmd", "/c", "move", "/y", currentExecutable, oldPath], { mode: "ignore" });
|
|
313
311
|
if (!moveResult.ok) {
|
|
314
312
|
throw new Error("Failed to move current executable aside");
|
|
315
313
|
}
|
|
316
|
-
const copyResult = await this.#spawnService.spawn([
|
|
317
|
-
"cmd",
|
|
318
|
-
"/c",
|
|
319
|
-
"copy",
|
|
320
|
-
"/y",
|
|
321
|
-
extractedBinary,
|
|
322
|
-
currentExecutable,
|
|
323
|
-
]);
|
|
314
|
+
const copyResult = await this.#spawnService.spawn(["cmd", "/c", "copy", "/y", extractedBinary, currentExecutable], { mode: "ignore" });
|
|
324
315
|
if (!copyResult.ok) {
|
|
325
316
|
throw new Error("Failed to copy new executable into place");
|
|
326
317
|
}
|
|
327
318
|
}
|
|
328
319
|
else {
|
|
329
|
-
const extractResult = await this.#spawnService.spawn([
|
|
330
|
-
"
|
|
331
|
-
|
|
332
|
-
archivePath,
|
|
333
|
-
"-d",
|
|
334
|
-
tmpDir,
|
|
335
|
-
]);
|
|
320
|
+
const extractResult = await this.#spawnService.spawn(["unzip", "-o", archivePath, "-d", tmpDir], {
|
|
321
|
+
mode: "ignore",
|
|
322
|
+
});
|
|
336
323
|
if (!extractResult.ok) {
|
|
337
324
|
throw new Error("Failed to extract release archive");
|
|
338
325
|
}
|
|
339
326
|
const extractedBinary = join(tmpDir, this.#cliConfig.name);
|
|
340
327
|
await Bun.write(currentExecutable, Bun.file(extractedBinary));
|
|
341
|
-
await this.#spawnService.spawn(["chmod", "+x", currentExecutable]);
|
|
328
|
+
await this.#spawnService.spawn(["chmod", "+x", currentExecutable], { mode: "ignore" });
|
|
342
329
|
}
|
|
343
330
|
}
|
|
344
331
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UpgradeServiceProvider.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/UpgradeServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAczE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAK1E,MAAM,CAAC,OAAO,OAAO,sBAAuB,YAAW,eAAe;;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAsB;IAChD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;gBAKd,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB;IAKnE,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;IASpD,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"UpgradeServiceProvider.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/UpgradeServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,yCAAyC,CAAC;AAC5F,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAczE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAK1E,MAAM,CAAC,OAAO,OAAO,sBAAuB,YAAW,eAAe;;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAsB;IAChD,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;gBAKd,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB;IAKnE,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC;IASpD,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;CAsH1D"}
|
|
@@ -2,7 +2,7 @@ import { UPGRADE_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
|
2
2
|
import { PROMPTER_SERVICE_ID, PromptType } from "@flowscripter/dynamic-cli-framework-api";
|
|
3
3
|
import { KEY_VALUE_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
4
4
|
import { SPAWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
5
|
-
import {
|
|
5
|
+
import { FETCH_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
6
6
|
import { Icon, PRINTER_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
7
7
|
import DefaultUpgradeService from "./DefaultUpgradeService.js";
|
|
8
8
|
import { UpgradeSubCommand } from "./command/UpgradeSubCommand.js";
|
|
@@ -29,16 +29,20 @@ export default class UpgradeServiceProvider {
|
|
|
29
29
|
async initService(context) {
|
|
30
30
|
const upgradeService = this.#upgradeService;
|
|
31
31
|
const cliConfig = this.#cliConfig;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
else {
|
|
32
|
+
const spawnService = context.doesServiceExist(SPAWN_SERVICE_ID)
|
|
33
|
+
? context.getServiceById(SPAWN_SERVICE_ID)
|
|
34
|
+
: undefined;
|
|
35
|
+
const fetchService = context.doesServiceExist(FETCH_SERVICE_ID)
|
|
36
|
+
? context.getServiceById(FETCH_SERVICE_ID)
|
|
37
|
+
: undefined;
|
|
38
|
+
if (spawnService === undefined) {
|
|
40
39
|
logger.debug(() => "SpawnService not available, upgrade install methods will be unavailable");
|
|
41
40
|
}
|
|
41
|
+
if (fetchService === undefined) {
|
|
42
|
+
logger.debug(() => "FetchService not available, upgrade version checks will be unavailable");
|
|
43
|
+
}
|
|
44
|
+
upgradeService.setDependencies(spawnService, fetchService);
|
|
45
|
+
void upgradeService.getUpgradeCheckResult();
|
|
42
46
|
if (!context.doesServiceExist(PROMPTER_SERVICE_ID)) {
|
|
43
47
|
logger.debug(() => "PrompterService not available, skipping auto-upgrade");
|
|
44
48
|
return;
|
|
@@ -101,7 +105,7 @@ export default class UpgradeServiceProvider {
|
|
|
101
105
|
}
|
|
102
106
|
async #checkAndUpgrade(context, upgradeService, cliConfig) {
|
|
103
107
|
try {
|
|
104
|
-
const checkResult = await upgradeService.
|
|
108
|
+
const checkResult = await upgradeService.getUpgradeCheckResult();
|
|
105
109
|
if (!checkResult?.updateAvailable) {
|
|
106
110
|
return;
|
|
107
111
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UpgradeSubCommand.d.ts","sourceRoot":"","sources":["../../../../../src/service/upgrade/command/UpgradeSubCommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yCAAyC,CAAC;AACtE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,EACL,KAAK,cAAc,EAEpB,MAAM,yCAAyC,CAAC;AASjD,OAAO,KAAK,qBAAqB,MAAM,6BAA6B,CAAC;AAErE,qBAAa,iBAAkB,YAAW,UAAU;;IAClD,QAAQ,CAAC,IAAI,aAAa;IAC1B,QAAQ,CAAC,WAAW,6CAA6C;IACjE,QAAQ,CAAC,mBAAmB,SAAS;IACrC,QAAQ,CAAC,WAAW,UAAM;IAE1B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAerC;gBAIiB,cAAc,EAAE,qBAAqB;IAI3C,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"UpgradeSubCommand.d.ts","sourceRoot":"","sources":["../../../../../src/service/upgrade/command/UpgradeSubCommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yCAAyC,CAAC;AACtE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,EACL,KAAK,cAAc,EAEpB,MAAM,yCAAyC,CAAC;AASjD,OAAO,KAAK,qBAAqB,MAAM,6BAA6B,CAAC;AAErE,qBAAa,iBAAkB,YAAW,UAAU;;IAClD,QAAQ,CAAC,IAAI,aAAa;IAC1B,QAAQ,CAAC,WAAW,6CAA6C;IACjE,QAAQ,CAAC,mBAAmB,SAAS;IACrC,QAAQ,CAAC,WAAW,UAAM;IAE1B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAerC;gBAIiB,cAAc,EAAE,qBAAqB;IAI3C,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;CAyCtF"}
|
|
@@ -31,7 +31,9 @@ export class UpgradeSubCommand {
|
|
|
31
31
|
const os = argumentValues.os;
|
|
32
32
|
const arch = undefined;
|
|
33
33
|
const installMethod = argumentValues["install-method"];
|
|
34
|
-
const checkResult =
|
|
34
|
+
const checkResult = os === undefined && installMethod === undefined
|
|
35
|
+
? await this.#upgradeService.getUpgradeCheckResult(true)
|
|
36
|
+
: await this.#upgradeService.checkForUpgrade(os, arch, installMethod);
|
|
35
37
|
if (!checkResult) {
|
|
36
38
|
await printerService.error(`No upgrade location is configured for the detected or requested platform.\n`, Icon.FAILURE);
|
|
37
39
|
return;
|
package/index.ts
CHANGED
|
@@ -78,6 +78,11 @@ export type { ShutdownService } from "@flowscripter/dynamic-cli-framework-api";
|
|
|
78
78
|
export { default as DefaultShutdownService } from "./src/service/shutdown/DefaultShutdownService.ts";
|
|
79
79
|
export { default as ShutdownServiceProvider } from "./src/service/shutdown/ShutdownServiceProvider.ts";
|
|
80
80
|
|
|
81
|
+
export { FETCH_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
82
|
+
export type { FetchOptions, FetchService } from "@flowscripter/dynamic-cli-framework-api";
|
|
83
|
+
export { default as DefaultFetchService } from "./src/service/fetch/DefaultFetchService.ts";
|
|
84
|
+
export { default as FetchServiceProvider } from "./src/service/fetch/FetchServiceProvider.ts";
|
|
85
|
+
|
|
81
86
|
export { SPAWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
82
87
|
export type {
|
|
83
88
|
SpawnOptions,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowscripter/dynamic-cli-framework",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "A framework for developing CLI applications which supports dynamic discovery and installation of new commands",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"build": "tsc -p tsconfig.build.json"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@flowscripter/dynamic-cli-framework-api": "^1.
|
|
52
|
-
"@flowscripter/dynamic-plugin-framework": "1.
|
|
51
|
+
"@flowscripter/dynamic-cli-framework-api": "^1.4.0",
|
|
52
|
+
"@flowscripter/dynamic-plugin-framework": "1.12.0",
|
|
53
53
|
"emphasize": "^7.0.0",
|
|
54
54
|
"fastest-levenshtein": "^1.0.16",
|
|
55
55
|
"figlet": "^1.11.0",
|
package/src/cli/BaseCLI.ts
CHANGED
|
@@ -52,6 +52,7 @@ import DefaultCompletionService from "../service/completion/DefaultCompletionSer
|
|
|
52
52
|
import CompletionServiceProvider from "../service/completion/CompletionServiceProvider.ts";
|
|
53
53
|
import ImagePrinterServiceProvider from "../service/imagePrinter/ImagePrinterServiceProvider.ts";
|
|
54
54
|
import SpawnServiceProvider from "../service/spawn/SpawnServiceProvider.ts";
|
|
55
|
+
import FetchServiceProvider from "../service/fetch/FetchServiceProvider.ts";
|
|
55
56
|
import UpgradeServiceProvider from "../service/upgrade/UpgradeServiceProvider.ts";
|
|
56
57
|
const logger = getLogger("BaseCLI");
|
|
57
58
|
|
|
@@ -72,6 +73,7 @@ const logger = getLogger("BaseCLI");
|
|
|
72
73
|
*
|
|
73
74
|
* * {@link ImagePrinterServiceProvider}
|
|
74
75
|
* * {@link SpawnServiceProvider}
|
|
76
|
+
* * {@link FetchServiceProvider}
|
|
75
77
|
* * {@link CompletionServiceProvider}
|
|
76
78
|
* * {@link UpgradeServiceProvider}
|
|
77
79
|
*
|
|
@@ -136,6 +138,7 @@ export default class BaseCLI implements CLI {
|
|
|
136
138
|
completionServiceEnabled: false,
|
|
137
139
|
imagePrinterServiceEnabled: false,
|
|
138
140
|
spawnServiceEnabled: false,
|
|
141
|
+
fetchServiceEnabled: false,
|
|
139
142
|
upgradeServiceEnabled: false,
|
|
140
143
|
upgradeLocationsConfig: { supportedPlatforms: [] },
|
|
141
144
|
validateAllCommands: false,
|
|
@@ -274,6 +277,10 @@ export default class BaseCLI implements CLI {
|
|
|
274
277
|
this.addServiceProvider(new SpawnServiceProvider(58));
|
|
275
278
|
}
|
|
276
279
|
|
|
280
|
+
if (this.#options.fetchServiceEnabled) {
|
|
281
|
+
this.addServiceProvider(new FetchServiceProvider(57));
|
|
282
|
+
}
|
|
283
|
+
|
|
277
284
|
if (this.#options.completionServiceEnabled) {
|
|
278
285
|
const completionService = new DefaultCompletionService();
|
|
279
286
|
this.addServiceProvider(
|
|
@@ -9,6 +9,7 @@ export default interface BaseCLIFeatureOptions {
|
|
|
9
9
|
readonly completionServiceEnabled?: boolean;
|
|
10
10
|
readonly imagePrinterServiceEnabled?: boolean;
|
|
11
11
|
readonly spawnServiceEnabled?: boolean;
|
|
12
|
+
readonly fetchServiceEnabled?: boolean;
|
|
12
13
|
readonly upgradeServiceEnabled?: boolean;
|
|
13
14
|
readonly upgradeLocationsConfig?: UpgradeLocationsConfig;
|
|
14
15
|
readonly validateAllCommands?: boolean;
|
|
@@ -19,7 +19,7 @@ export default class VersionCommand implements GlobalCommand {
|
|
|
19
19
|
let line = context.cliConfig.version;
|
|
20
20
|
if (context.doesServiceExist(UPGRADE_SERVICE_ID)) {
|
|
21
21
|
const upgradeService = context.getServiceById(UPGRADE_SERVICE_ID) as UpgradeService;
|
|
22
|
-
const result = await upgradeService.
|
|
22
|
+
const result = await upgradeService.getUpgradeCheckResult().catch(() => undefined);
|
|
23
23
|
if (result?.updateAvailable) {
|
|
24
24
|
line += ` (${result.latestVersion} available, run '${context.cliConfig.name} upgrade')`;
|
|
25
25
|
}
|
|
@@ -72,7 +72,7 @@ export default class BannerServiceProvider implements ServiceProvider {
|
|
|
72
72
|
}
|
|
73
73
|
if (context.doesServiceExist(UPGRADE_SERVICE_ID)) {
|
|
74
74
|
const upgradeService = context.getServiceById(UPGRADE_SERVICE_ID) as UpgradeService;
|
|
75
|
-
const result = await upgradeService.
|
|
75
|
+
const result = await upgradeService.getUpgradeCheckResult().catch(() => undefined);
|
|
76
76
|
if (result?.updateAvailable) {
|
|
77
77
|
await printerService.info(
|
|
78
78
|
` ${printerService.secondary(
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FetchOptions,
|
|
3
|
+
FetchService,
|
|
4
|
+
ShutdownService,
|
|
5
|
+
} from "@flowscripter/dynamic-cli-framework-api";
|
|
6
|
+
import getLogger from "../../util/logger.ts";
|
|
7
|
+
|
|
8
|
+
const logger = getLogger("DefaultFetchService");
|
|
9
|
+
|
|
10
|
+
export default class DefaultFetchService implements FetchService {
|
|
11
|
+
#shutdownService: ShutdownService | undefined;
|
|
12
|
+
|
|
13
|
+
public setDependencies(shutdownService: ShutdownService): void {
|
|
14
|
+
this.#shutdownService = shutdownService;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public async fetch(input: string | URL, options: FetchOptions = {}): Promise<Response> {
|
|
18
|
+
if (this.#shutdownService === undefined) {
|
|
19
|
+
throw new Error("DefaultFetchService.fetch() called before setDependencies()");
|
|
20
|
+
}
|
|
21
|
+
const shutdownService = this.#shutdownService;
|
|
22
|
+
const { timeoutMs, longRunning = false, signal: callerSignal, ...requestInit } = options;
|
|
23
|
+
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const signals: AbortSignal[] = [controller.signal];
|
|
26
|
+
if (callerSignal) {
|
|
27
|
+
signals.push(callerSignal);
|
|
28
|
+
}
|
|
29
|
+
if (timeoutMs !== undefined) {
|
|
30
|
+
signals.push(AbortSignal.timeout(timeoutMs));
|
|
31
|
+
}
|
|
32
|
+
const signal = signals.length === 1 ? signals[0]! : AbortSignal.any(signals);
|
|
33
|
+
|
|
34
|
+
let settled = false;
|
|
35
|
+
if (longRunning) {
|
|
36
|
+
shutdownService.enterLongRunningMode();
|
|
37
|
+
shutdownService.addShutdownListener(async () => {
|
|
38
|
+
if (settled) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
logger.debug(() => `Aborting fetch of '${input.toString()}' due to shutdown`);
|
|
42
|
+
controller.abort();
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
return await fetch(input, { ...requestInit, signal });
|
|
48
|
+
} finally {
|
|
49
|
+
settled = true;
|
|
50
|
+
if (longRunning) {
|
|
51
|
+
shutdownService.leaveLongRunningMode();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CLIConfig,
|
|
3
|
+
Context,
|
|
4
|
+
ServiceInfo,
|
|
5
|
+
ServiceProvider,
|
|
6
|
+
ShutdownService,
|
|
7
|
+
} from "@flowscripter/dynamic-cli-framework-api";
|
|
8
|
+
import { FETCH_SERVICE_ID, SHUTDOWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
9
|
+
import DefaultFetchService from "./DefaultFetchService.ts";
|
|
10
|
+
|
|
11
|
+
export default class FetchServiceProvider implements ServiceProvider {
|
|
12
|
+
readonly serviceId: string = FETCH_SERVICE_ID;
|
|
13
|
+
readonly servicePriority: number;
|
|
14
|
+
#fetchService: DefaultFetchService | undefined;
|
|
15
|
+
|
|
16
|
+
public constructor(servicePriority: number) {
|
|
17
|
+
this.servicePriority = servicePriority;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
|
|
21
|
+
this.#fetchService = new DefaultFetchService();
|
|
22
|
+
return Promise.resolve({
|
|
23
|
+
service: this.#fetchService,
|
|
24
|
+
commands: [],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public initService(context: Context): Promise<void> {
|
|
29
|
+
const shutdownService = context.getServiceById(SHUTDOWN_SERVICE_ID) as ShutdownService;
|
|
30
|
+
this.#fetchService!.setDependencies(shutdownService);
|
|
31
|
+
return Promise.resolve();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -3,7 +3,10 @@ import type { Context } from "@flowscripter/dynamic-cli-framework-api";
|
|
|
3
3
|
import type { CLIConfig } from "@flowscripter/dynamic-cli-framework-api";
|
|
4
4
|
import type { PrinterService, SpawnService } from "@flowscripter/dynamic-cli-framework-api";
|
|
5
5
|
import { PRINTER_SERVICE_ID, SPAWN_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
6
|
+
import type { FetchService } from "@flowscripter/dynamic-cli-framework-api";
|
|
7
|
+
import { FETCH_SERVICE_ID } from "@flowscripter/dynamic-cli-framework-api";
|
|
6
8
|
import type {
|
|
9
|
+
FetchCapable,
|
|
7
10
|
MarketplacePluginManager,
|
|
8
11
|
SpawnCapable,
|
|
9
12
|
} from "@flowscripter/dynamic-plugin-framework";
|
|
@@ -13,6 +16,9 @@ import { PluginGroupCommand } from "./command/PluginGroupCommand.ts";
|
|
|
13
16
|
import SpawnInterfaceAdapter, {
|
|
14
17
|
type SpawnInterfaceAdapterOptions,
|
|
15
18
|
} from "./SpawnInterfaceAdapter.ts";
|
|
19
|
+
import FetchInterfaceAdapter, {
|
|
20
|
+
type FetchInterfaceAdapterOptions,
|
|
21
|
+
} from "./FetchInterfaceAdapter.ts";
|
|
16
22
|
import getLogger from "../../util/logger.ts";
|
|
17
23
|
|
|
18
24
|
const logger = getLogger("DefaultPluginServiceProvider");
|
|
@@ -22,16 +28,19 @@ export default class DefaultPluginServiceProvider implements ServiceProvider {
|
|
|
22
28
|
readonly servicePriority: number;
|
|
23
29
|
readonly #pluginManager: MarketplacePluginManager;
|
|
24
30
|
readonly #spawnInterfaceAdapterOptions: SpawnInterfaceAdapterOptions;
|
|
31
|
+
readonly #fetchInterfaceAdapterOptions: FetchInterfaceAdapterOptions;
|
|
25
32
|
#pluginService: DefaultPluginService | undefined;
|
|
26
33
|
|
|
27
34
|
constructor(
|
|
28
35
|
pluginManager: MarketplacePluginManager,
|
|
29
36
|
servicePriority = 50,
|
|
30
37
|
spawnInterfaceAdapterOptions: SpawnInterfaceAdapterOptions = {},
|
|
38
|
+
fetchInterfaceAdapterOptions: FetchInterfaceAdapterOptions = {},
|
|
31
39
|
) {
|
|
32
40
|
this.#pluginManager = pluginManager;
|
|
33
41
|
this.servicePriority = servicePriority;
|
|
34
42
|
this.#spawnInterfaceAdapterOptions = spawnInterfaceAdapterOptions;
|
|
43
|
+
this.#fetchInterfaceAdapterOptions = fetchInterfaceAdapterOptions;
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
async getServiceInfo(_cliConfig: CLIConfig): Promise<ServiceInfo> {
|
|
@@ -45,21 +54,29 @@ export default class DefaultPluginServiceProvider implements ServiceProvider {
|
|
|
45
54
|
initService(context: Context): Promise<void> {
|
|
46
55
|
if (!context.doesServiceExist(SPAWN_SERVICE_ID)) {
|
|
47
56
|
logger.debug("SpawnService not available, plugin manager will spawn processes directly");
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
if (!("setSpawn" in this.#pluginManager)) {
|
|
57
|
+
} else if (!("setSpawn" in this.#pluginManager)) {
|
|
51
58
|
logger.debug("Plugin manager does not support SpawnCapable, skipping spawn injection");
|
|
52
|
-
|
|
59
|
+
} else {
|
|
60
|
+
const spawnService = context.getServiceById(SPAWN_SERVICE_ID) as SpawnService;
|
|
61
|
+
const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
|
|
62
|
+
const adapter = new SpawnInterfaceAdapter(
|
|
63
|
+
spawnService,
|
|
64
|
+
printerService,
|
|
65
|
+
this.#spawnInterfaceAdapterOptions,
|
|
66
|
+
);
|
|
67
|
+
(this.#pluginManager as unknown as SpawnCapable).setSpawn(adapter);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!context.doesServiceExist(FETCH_SERVICE_ID)) {
|
|
71
|
+
logger.debug("FetchService not available, plugin manager will fetch directly");
|
|
72
|
+
} else if (!("setFetch" in this.#pluginManager)) {
|
|
73
|
+
logger.debug("Plugin manager does not support FetchCapable, skipping fetch injection");
|
|
74
|
+
} else {
|
|
75
|
+
const fetchService = context.getServiceById(FETCH_SERVICE_ID) as FetchService;
|
|
76
|
+
const adapter = new FetchInterfaceAdapter(fetchService, this.#fetchInterfaceAdapterOptions);
|
|
77
|
+
(this.#pluginManager as unknown as FetchCapable).setFetch(adapter);
|
|
53
78
|
}
|
|
54
79
|
|
|
55
|
-
const spawnService = context.getServiceById(SPAWN_SERVICE_ID) as SpawnService;
|
|
56
|
-
const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
|
|
57
|
-
const adapter = new SpawnInterfaceAdapter(
|
|
58
|
-
spawnService,
|
|
59
|
-
printerService,
|
|
60
|
-
this.#spawnInterfaceAdapterOptions,
|
|
61
|
-
);
|
|
62
|
-
(this.#pluginManager as unknown as SpawnCapable).setSpawn(adapter);
|
|
63
80
|
return Promise.resolve();
|
|
64
81
|
}
|
|
65
82
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FetchService } from "@flowscripter/dynamic-cli-framework-api";
|
|
2
|
+
import type { FetchInterface } from "@flowscripter/dynamic-plugin-framework";
|
|
3
|
+
|
|
4
|
+
export interface FetchInterfaceAdapterOptions {
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Adapts a {@link FetchService} to dynamic-plugin-framework's {@link FetchInterface}, so plugin
|
|
10
|
+
* manager/repository fetches respect the host CLI's shutdown handling and default timeout.
|
|
11
|
+
*/
|
|
12
|
+
export default class FetchInterfaceAdapter implements FetchInterface {
|
|
13
|
+
readonly #fetchService: FetchService;
|
|
14
|
+
readonly #timeoutMs: number | undefined;
|
|
15
|
+
|
|
16
|
+
public constructor(fetchService: FetchService, options: FetchInterfaceAdapterOptions = {}) {
|
|
17
|
+
this.#fetchService = fetchService;
|
|
18
|
+
this.#timeoutMs = options.timeoutMs;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async fetch(
|
|
22
|
+
input: string,
|
|
23
|
+
init?: RequestInit & { timeoutMs?: number },
|
|
24
|
+
): Promise<Response> {
|
|
25
|
+
const { timeoutMs = this.#timeoutMs, ...requestInit } = init ?? {};
|
|
26
|
+
return this.#fetchService.fetch(input, { ...requestInit, timeoutMs });
|
|
27
|
+
}
|
|
28
|
+
}
|