@flowscripter/dynamic-cli-framework 5.3.5 → 5.4.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.
@@ -1 +1 @@
1
- {"version":3,"file":"PluginAddSubCommand.d.ts","sourceRoot":"","sources":["../../../../../src/service/plugin/command/PluginAddSubCommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yCAAyC,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AASxE,qBAAa,mBAAoB,YAAW,UAAU;IACpD,QAAQ,CAAC,IAAI,SAAS;IACtB,QAAQ,CAAC,WAAW,6BAA6B;IACjD,QAAQ,CAAC,mBAAmB,SAAS;IACrC,QAAQ,CAAC,OAAO,UAAM;IACtB,QAAQ,CAAC,WAAW;;;;QAMlB;IAEI,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA8ErE;CACF"}
1
+ {"version":3,"file":"PluginAddSubCommand.d.ts","sourceRoot":"","sources":["../../../../../src/service/plugin/command/PluginAddSubCommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yCAAyC,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AASxE,qBAAa,mBAAoB,YAAW,UAAU;IACpD,QAAQ,CAAC,IAAI,SAAS;IACtB,QAAQ,CAAC,WAAW,6BAA6B;IACjD,QAAQ,CAAC,mBAAmB,SAAS;IACrC,QAAQ,CAAC,OAAO,UAAM;IACtB,QAAQ,CAAC,WAAW;;;;QAMlB;IAEI,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgFrE;CACF"}
@@ -20,7 +20,7 @@ export class PluginAddSubCommand {
20
20
  const pluginService = context.getServiceById(PLUGIN_SERVICE_ID);
21
21
  const { pluginId, version } = parsePluginSpecifier(argumentValues["pluginId"]);
22
22
  const searchLabel = version ? `${pluginId}@${version}` : pluginId;
23
- await printerService.info(`Searching for plugin: ${searchLabel}\n`, Icon.INFORMATION);
23
+ await printerService.showSpinner(`Searching for plugin: ${searchLabel}`);
24
24
  let descriptor;
25
25
  for await (const d of pluginService.search({ text: pluginId })) {
26
26
  if (getPluginId(d) === pluginId || d.pluginId === pluginId) {
@@ -28,6 +28,7 @@ export class PluginAddSubCommand {
28
28
  break;
29
29
  }
30
30
  }
31
+ await printerService.hideSpinner();
31
32
  if (descriptor && version) {
32
33
  // search only ever returns the latest version - substitute the explicitly requested
33
34
  // version so install() is asked for the correct one.
@@ -62,7 +63,7 @@ export class PluginAddSubCommand {
62
63
  ? `Version ${versionToCheck} of plugin ${descriptor.pluginId} was not found in the configured plugin registry`
63
64
  : `Plugin ${descriptor.pluginId} was not found in the configured plugin registry`);
64
65
  }
65
- await printerService.info(`Installing ${installLabel}...\n`, Icon.INFORMATION);
66
+ await printerService.showSpinner(`Installing ${installLabel}...`);
66
67
  await pluginService.install(descriptor);
67
68
  // Look up the actually-installed version rather than trusting `descriptor.version`: when no
68
69
  // version was requested and the plugin wasn't found via search (direct-install fallback),
@@ -74,6 +75,7 @@ export class PluginAddSubCommand {
74
75
  break;
75
76
  }
76
77
  }
78
+ await printerService.hideSpinner();
77
79
  await printerService.print(`Plugin ${descriptor.pluginId}@${installedVersion} installed.\n`, Icon.SUCCESS);
78
80
  }
79
81
  }
@@ -1 +1 @@
1
- {"version":3,"file":"PluginRemoveSubCommand.d.ts","sourceRoot":"","sources":["../../../../../src/service/plugin/command/PluginRemoveSubCommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yCAAyC,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAMxE,qBAAa,sBAAuB,YAAW,UAAU;IACvD,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,WAAW,uCAAuC;IAC3D,QAAQ,CAAC,mBAAmB,SAAS;IACrC,QAAQ,CAAC,OAAO,UAAM;IACtB,QAAQ,CAAC,WAAW;;;;QAMlB;IAEI,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQrE;CACF"}
1
+ {"version":3,"file":"PluginRemoveSubCommand.d.ts","sourceRoot":"","sources":["../../../../../src/service/plugin/command/PluginRemoveSubCommand.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yCAAyC,CAAC;AAC1E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yCAAyC,CAAC;AACvE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yCAAyC,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,yCAAyC,CAAC;AAMxE,qBAAa,sBAAuB,YAAW,UAAU;IACvD,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,WAAW,uCAAuC;IAC3D,QAAQ,CAAC,mBAAmB,SAAS;IACrC,QAAQ,CAAC,OAAO,UAAM;IACtB,QAAQ,CAAC,WAAW;;;;QAMlB;IAEI,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASrE;CACF"}
@@ -17,8 +17,9 @@ export class PluginRemoveSubCommand {
17
17
  const printerService = context.getServiceById(PRINTER_SERVICE_ID);
18
18
  const pluginService = context.getServiceById(PLUGIN_SERVICE_ID);
19
19
  const pluginId = argumentValues["pluginId"];
20
- await printerService.info(`Removing plugin: ${pluginId}...\n`, Icon.INFORMATION);
20
+ await printerService.showSpinner(`Removing plugin: ${pluginId}...`);
21
21
  await pluginService.uninstall(pluginId);
22
+ await printerService.hideSpinner();
22
23
  await printerService.print(`Plugin ${pluginId} removed.\n`, Icon.SUCCESS);
23
24
  }
24
25
  }
@@ -1,11 +1,12 @@
1
- import type { CLIConfig, FetchService, SpawnService, UpgradeCheckResult, UpgradeResult } from "@flowscripter/dynamic-cli-framework-api";
1
+ import type { CLIConfig, FetchService, PrinterService, SpawnService, UpgradeCheckResult, UpgradeResult } from "@flowscripter/dynamic-cli-framework-api";
2
2
  import { InstallMethod, SupportedArch, SupportedOs, type UpgradeService } from "@flowscripter/dynamic-cli-framework-api";
3
3
  import type { UpgradeLocationsConfig } from "./UpgradeLocationsConfig.ts";
4
4
  export declare const VERSION_CHECK_TIMEOUT_MS = 250;
5
+ export declare function describeUpgradeCheckResult(result: UpgradeCheckResult): string;
5
6
  export default class DefaultUpgradeService implements UpgradeService {
6
7
  #private;
7
8
  constructor(config: UpgradeLocationsConfig, cliConfig: CLIConfig);
8
- setDependencies(spawnService: SpawnService | undefined, fetchService: FetchService | undefined): void;
9
+ setDependencies(spawnService: SpawnService | undefined, fetchService: FetchService | undefined, printerService: PrinterService | undefined): void;
9
10
  getUpgradeCheckResult(waitForResult?: boolean): Promise<UpgradeCheckResult>;
10
11
  detectOs(): SupportedOs | undefined;
11
12
  detectArch(): SupportedArch | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultUpgradeService.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/DefaultUpgradeService.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,SAAS,EACT,YAAY,EAEZ,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACd,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,aAAa,EACb,aAAa,EACb,WAAW,EACX,KAAK,cAAc,EACpB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAS1E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAkB5C,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,cAAc;;IAOlE,YAAmB,MAAM,EAAE,sBAAsB,EAAE,SAAS,EAAE,SAAS,EAGtE;IAEM,eAAe,CACpB,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,YAAY,EAAE,YAAY,GAAG,SAAS,GACrC,IAAI,CAGN;IAEM,qBAAqB,CAAC,aAAa,UAAQ,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA0B/E;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,CAcpF;IAEY,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,CA+CxB;CAuRF"}
1
+ {"version":3,"file":"DefaultUpgradeService.d.ts","sourceRoot":"","sources":["../../../../src/service/upgrade/DefaultUpgradeService.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,SAAS,EACT,YAAY,EACZ,cAAc,EAEd,YAAY,EACZ,kBAAkB,EAClB,aAAa,EACd,MAAM,yCAAyC,CAAC;AACjD,OAAO,EACL,aAAa,EACb,aAAa,EACb,WAAW,EACX,KAAK,cAAc,EACpB,MAAM,yCAAyC,CAAC;AAEjD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAS1E,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAY5C,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAI7E;AAQD,MAAM,CAAC,OAAO,OAAO,qBAAsB,YAAW,cAAc;;IAQlE,YAAmB,MAAM,EAAE,sBAAsB,EAAE,SAAS,EAAE,SAAS,EAGtE;IAEM,eAAe,CACpB,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,YAAY,EAAE,YAAY,GAAG,SAAS,EACtC,cAAc,EAAE,cAAc,GAAG,SAAS,GACzC,IAAI,CAIN;IAoCM,qBAAqB,CAAC,aAAa,UAAQ,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA0B/E;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,CAcpF;IAEY,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,CAsDxB;CAiRF"}
@@ -16,6 +16,11 @@ function describeSpawnFailure(result) {
16
16
  ? "timed out"
17
17
  : (result.error?.message ?? `exit code ${result.exitCode}`);
18
18
  }
19
+ export function describeUpgradeCheckResult(result) {
20
+ return result.status === "failed"
21
+ ? `Upgrade check result: failed - ${result.error.message}`
22
+ : `Upgrade check result: ${JSON.stringify(result)}`;
23
+ }
19
24
  const OS_LABELS = {
20
25
  [SupportedOs.LINUX]: "Linux",
21
26
  [SupportedOs.MACOS]: "MacOS",
@@ -24,6 +29,7 @@ const OS_LABELS = {
24
29
  export default class DefaultUpgradeService {
25
30
  #spawnService;
26
31
  #fetchService;
32
+ #printerService;
27
33
  #upgradeCheckPromise;
28
34
  #config;
29
35
  #cliConfig;
@@ -31,9 +37,40 @@ export default class DefaultUpgradeService {
31
37
  this.#config = config;
32
38
  this.#cliConfig = cliConfig;
33
39
  }
34
- setDependencies(spawnService, fetchService) {
40
+ setDependencies(spawnService, fetchService, printerService) {
35
41
  this.#spawnService = spawnService;
36
42
  this.#fetchService = fetchService;
43
+ this.#printerService = printerService;
44
+ }
45
+ // Mirrors SpawnInterfaceAdapter's plugin:add/plugin:remove pattern: wrap a spawned command's
46
+ // output in a quoted, marked block that's cleared on success (so a clean install stays quiet)
47
+ // but left on screen on failure (so the diagnostic output remains visible).
48
+ async #spawnQuoted(command) {
49
+ const spawnService = this.#spawnService;
50
+ const printerService = this.#printerService;
51
+ if (!printerService) {
52
+ return spawnService.spawn(command, { mode: "ignore" });
53
+ }
54
+ printerService.startQuote();
55
+ printerService.startMark();
56
+ // onOutput is synchronous and may be called concurrently for stdout/stderr lines, but
57
+ // printerService.info() is async and must not be invoked concurrently with itself - queue
58
+ // writes so they're applied one at a time, in call order.
59
+ let writeQueue = Promise.resolve();
60
+ const onOutput = (line) => {
61
+ writeQueue = writeQueue.then(() => printerService.info(`${line}\n`));
62
+ };
63
+ const result = await spawnService.spawn(command, { mode: "wrapped", onOutput });
64
+ await writeQueue;
65
+ printerService.endQuote();
66
+ printerService.endMark();
67
+ if (result.ok) {
68
+ await printerService.clearMarked();
69
+ }
70
+ else {
71
+ printerService.discardMark();
72
+ }
73
+ return result;
37
74
  }
38
75
  getUpgradeCheckResult(waitForResult = false) {
39
76
  if (!this.#upgradeCheckPromise) {
@@ -45,7 +82,7 @@ export default class DefaultUpgradeService {
45
82
  // waiting via waitForResult=true) any chance of a fresh attempt for the rest of this
46
83
  // process's lifetime.
47
84
  this.#upgradeCheckPromise = this.checkForUpgrade().then((result) => {
48
- logger.debug(() => `Upgrade check result: ${JSON.stringify(result)}`);
85
+ logger.debug(() => describeUpgradeCheckResult(result));
49
86
  if (result.status === "failed") {
50
87
  this.#upgradeCheckPromise = undefined;
51
88
  }
@@ -156,7 +193,13 @@ export default class DefaultUpgradeService {
156
193
  if (checkResult.installMethod === InstallMethod.GITHUB_RELEASE && !this.#fetchService) {
157
194
  return { ok: false, oldVersion, error: new Error("FetchService is not available") };
158
195
  }
196
+ if (this.#printerService) {
197
+ await this.#printerService.showSpinner(`Installing version ${checkResult.latestVersion}...`);
198
+ }
159
199
  try {
200
+ if (this.#printerService) {
201
+ await this.#printerService.hideSpinner();
202
+ }
160
203
  switch (checkResult.installMethod) {
161
204
  case InstallMethod.LINUX_SCRIPT:
162
205
  await this.#upgradeViaLinuxScript();
@@ -301,25 +344,21 @@ export default class DefaultUpgradeService {
301
344
  }
302
345
  async #upgradeViaLinuxScript() {
303
346
  const { scriptUrl } = this.#config.linuxScript;
304
- const result = await this.#spawnService.spawn(["sh", "-c", `curl -fsSL ${scriptUrl} | sh`], {
305
- mode: "ignore",
306
- });
347
+ const result = await this.#spawnQuoted(["sh", "-c", `curl -fsSL ${scriptUrl} | sh`]);
307
348
  if (!result.ok) {
308
349
  throw new Error(`Install script failed: ${describeSpawnFailure(result)}`);
309
350
  }
310
351
  }
311
352
  async #upgradeViaHomebrew() {
312
353
  const { tap, formula } = this.#config.homebrew;
313
- const result = await this.#spawnService.spawn(["brew", "upgrade", `${tap}/${formula}`], {
314
- mode: "ignore",
315
- });
354
+ const result = await this.#spawnQuoted(["brew", "upgrade", `${tap}/${formula}`]);
316
355
  if (!result.ok) {
317
356
  throw new Error(`brew upgrade failed: ${describeSpawnFailure(result)}`);
318
357
  }
319
358
  }
320
359
  async #upgradeViaWinget() {
321
360
  const { packageId } = this.#config.winget;
322
- const result = await this.#spawnService.spawn([
361
+ const result = await this.#spawnQuoted([
323
362
  "winget",
324
363
  "upgrade",
325
364
  "--id",
@@ -327,7 +366,7 @@ export default class DefaultUpgradeService {
327
366
  "--silent",
328
367
  "--accept-package-agreements",
329
368
  "--accept-source-agreements",
330
- ], { mode: "ignore" });
369
+ ]);
331
370
  if (!result.ok) {
332
371
  throw new Error(`winget upgrade failed: ${describeSpawnFailure(result)}`);
333
372
  }
@@ -351,11 +390,11 @@ export default class DefaultUpgradeService {
351
390
  await Bun.write(archivePath, archiveData);
352
391
  const currentExecutable = process.execPath;
353
392
  if (os === SupportedOs.WINDOWS) {
354
- const extractResult = await this.#spawnService.spawn([
393
+ const extractResult = await this.#spawnQuoted([
355
394
  "powershell",
356
395
  "-Command",
357
396
  `Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpDir}' -Force`,
358
- ], { mode: "ignore" });
397
+ ]);
359
398
  if (!extractResult.ok) {
360
399
  throw new Error("Failed to extract release archive");
361
400
  }
@@ -365,23 +404,36 @@ export default class DefaultUpgradeService {
365
404
  // Windows won't let us delete the just-renamed-aside exe while this process still has it
366
405
  // open/mapped - that can only happen once we're no longer holding it, i.e. at the start of
367
406
  // the NEXT invocation, before we move today's running exe aside. Ignore failures: the file
368
- // may not exist, or may still be locked (e.g. another instance still running).
407
+ // may not exist, or may still be locked (e.g. another instance still running). Not quoted -
408
+ // this is expected to fail silently, there's nothing worth showing the user.
369
409
  await this.#spawnService.spawn(["cmd", "/c", "del", "/f", "/q", oldPath], {
370
410
  mode: "ignore",
371
411
  });
372
- const moveResult = await this.#spawnService.spawn(["cmd", "/c", "move", "/y", currentExecutable, oldPath], { mode: "ignore" });
412
+ const moveResult = await this.#spawnQuoted([
413
+ "cmd",
414
+ "/c",
415
+ "move",
416
+ "/y",
417
+ currentExecutable,
418
+ oldPath,
419
+ ]);
373
420
  if (!moveResult.ok) {
374
421
  throw new Error("Failed to move current executable aside");
375
422
  }
376
- const copyResult = await this.#spawnService.spawn(["cmd", "/c", "copy", "/y", extractedBinary, currentExecutable], { mode: "ignore" });
423
+ const copyResult = await this.#spawnQuoted([
424
+ "cmd",
425
+ "/c",
426
+ "copy",
427
+ "/y",
428
+ extractedBinary,
429
+ currentExecutable,
430
+ ]);
377
431
  if (!copyResult.ok) {
378
432
  throw new Error("Failed to copy new executable into place");
379
433
  }
380
434
  }
381
435
  else {
382
- const extractResult = await this.#spawnService.spawn(["unzip", "-o", archivePath, "-d", tmpDir], {
383
- mode: "ignore",
384
- });
436
+ const extractResult = await this.#spawnQuoted(["unzip", "-o", archivePath, "-d", tmpDir]);
385
437
  if (!extractResult.ok) {
386
438
  throw new Error("Failed to extract release archive");
387
439
  }
@@ -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;IAKjC,YAAmB,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB,EAGzE;IAEM,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAOhE;IAEY,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAsFxD;CAoCF"}
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;IAKjC,YAAmB,eAAe,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB,EAGzE;IAEM,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,CAAC,CAOhE;IAEY,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAuFxD;CAoCF"}
@@ -41,7 +41,8 @@ export default class UpgradeServiceProvider {
41
41
  if (fetchService === undefined) {
42
42
  logger.debug(() => "FetchService not available, upgrade version checks will be unavailable");
43
43
  }
44
- upgradeService.setDependencies(spawnService, fetchService);
44
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID);
45
+ upgradeService.setDependencies(spawnService, fetchService, printerService);
45
46
  void upgradeService.getUpgradeCheckResult();
46
47
  if (!context.doesServiceExist(PROMPTER_SERVICE_ID)) {
47
48
  logger.debug(() => "PrompterService not available, skipping auto-upgrade");
@@ -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,EAAE,KAAK,MAAM,EAAiB,MAAM,yCAAyC,CAAC;AASrF,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;IAIF,YAAmB,cAAc,EAAE,qBAAqB,EAEvD;IAEY,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsD5E;CACF"}
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,EAAE,KAAK,MAAM,EAAiB,MAAM,yCAAyC,CAAC;AASrF,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;IAIF,YAAmB,cAAc,EAAE,qBAAqB,EAEvD;IAEY,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4D5E;CACF"}
@@ -28,12 +28,15 @@ export class UpgradeSubCommand {
28
28
  async execute(context, argumentValues) {
29
29
  const printerService = context.getServiceById(PRINTER_SERVICE_ID);
30
30
  const cliName = context.cliConfig.name;
31
+ const currentVersion = context.cliConfig.version;
31
32
  const os = argumentValues.os;
32
33
  const arch = undefined;
33
34
  const installMethod = argumentValues["install-method"];
35
+ await printerService.showSpinner(`Looking for version newer than ${currentVersion}`);
34
36
  const checkResult = os === undefined && installMethod === undefined
35
37
  ? await this.#upgradeService.getUpgradeCheckResult(true)
36
38
  : await this.#upgradeService.checkForUpgrade(os, arch, installMethod);
39
+ await printerService.hideSpinner();
37
40
  if (checkResult.status === "unsupported") {
38
41
  await printerService.error(`No upgrade location is configured for the detected or requested platform.\n`, Icon.FAILURE);
39
42
  return;
@@ -48,7 +51,7 @@ export class UpgradeSubCommand {
48
51
  return;
49
52
  }
50
53
  if (!checkResult.updateAvailable) {
51
- await printerService.print(`${cliName} is already up to date (${checkResult.currentVersion}).\n`);
54
+ await printerService.print(`${cliName} is already up to date: ${checkResult.currentVersion}\n`, Icon.INFORMATION);
52
55
  return;
53
56
  }
54
57
  const upgradeResult = await this.#upgradeService.upgrade(os, arch, installMethod);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowscripter/dynamic-cli-framework",
3
- "version": "5.3.5",
3
+ "version": "5.4.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",
@@ -29,7 +29,7 @@ export class PluginAddSubCommand implements SubCommand {
29
29
 
30
30
  const { pluginId, version } = parsePluginSpecifier(argumentValues["pluginId"] as string);
31
31
  const searchLabel = version ? `${pluginId}@${version}` : pluginId;
32
- await printerService.info(`Searching for plugin: ${searchLabel}\n`, Icon.INFORMATION);
32
+ await printerService.showSpinner(`Searching for plugin: ${searchLabel}`);
33
33
 
34
34
  let descriptor: VersionedPluginDescriptor | undefined;
35
35
  for await (const d of pluginService.search({ text: pluginId })) {
@@ -38,6 +38,7 @@ export class PluginAddSubCommand implements SubCommand {
38
38
  break;
39
39
  }
40
40
  }
41
+ await printerService.hideSpinner();
41
42
 
42
43
  if (descriptor && version) {
43
44
  // search only ever returns the latest version - substitute the explicitly requested
@@ -83,7 +84,7 @@ export class PluginAddSubCommand implements SubCommand {
83
84
  );
84
85
  }
85
86
 
86
- await printerService.info(`Installing ${installLabel}...\n`, Icon.INFORMATION);
87
+ await printerService.showSpinner(`Installing ${installLabel}...`);
87
88
  await pluginService.install(descriptor);
88
89
 
89
90
  // Look up the actually-installed version rather than trusting `descriptor.version`: when no
@@ -96,6 +97,7 @@ export class PluginAddSubCommand implements SubCommand {
96
97
  break;
97
98
  }
98
99
  }
100
+ await printerService.hideSpinner();
99
101
 
100
102
  await printerService.print(
101
103
  `Plugin ${descriptor.pluginId}@${installedVersion} installed.\n`,
@@ -25,8 +25,9 @@ export class PluginRemoveSubCommand implements SubCommand {
25
25
  const pluginService = context.getServiceById(PLUGIN_SERVICE_ID) as PluginService;
26
26
 
27
27
  const pluginId = argumentValues["pluginId"] as string;
28
- await printerService.info(`Removing plugin: ${pluginId}...\n`, Icon.INFORMATION);
28
+ await printerService.showSpinner(`Removing plugin: ${pluginId}...`);
29
29
  await pluginService.uninstall(pluginId);
30
+ await printerService.hideSpinner();
30
31
  await printerService.print(`Plugin ${pluginId} removed.\n`, Icon.SUCCESS);
31
32
  }
32
33
  }
@@ -5,6 +5,7 @@ import { dirname, join } from "node:path";
5
5
  import type {
6
6
  CLIConfig,
7
7
  FetchService,
8
+ PrinterService,
8
9
  SpawnResult,
9
10
  SpawnService,
10
11
  UpgradeCheckResult,
@@ -38,6 +39,12 @@ function describeSpawnFailure(result: Extract<SpawnResult, { ok: false }>): stri
38
39
  : (result.error?.message ?? `exit code ${result.exitCode}`);
39
40
  }
40
41
 
42
+ export function describeUpgradeCheckResult(result: UpgradeCheckResult): string {
43
+ return result.status === "failed"
44
+ ? `Upgrade check result: failed - ${result.error.message}`
45
+ : `Upgrade check result: ${JSON.stringify(result)}`;
46
+ }
47
+
41
48
  const OS_LABELS: Record<SupportedOs, string> = {
42
49
  [SupportedOs.LINUX]: "Linux",
43
50
  [SupportedOs.MACOS]: "MacOS",
@@ -47,6 +54,7 @@ const OS_LABELS: Record<SupportedOs, string> = {
47
54
  export default class DefaultUpgradeService implements UpgradeService {
48
55
  #spawnService: SpawnService | undefined;
49
56
  #fetchService: FetchService | undefined;
57
+ #printerService: PrinterService | undefined;
50
58
  #upgradeCheckPromise: Promise<UpgradeCheckResult> | undefined;
51
59
  readonly #config: UpgradeLocationsConfig;
52
60
  readonly #cliConfig: CLIConfig;
@@ -59,9 +67,45 @@ export default class DefaultUpgradeService implements UpgradeService {
59
67
  public setDependencies(
60
68
  spawnService: SpawnService | undefined,
61
69
  fetchService: FetchService | undefined,
70
+ printerService: PrinterService | undefined,
62
71
  ): void {
63
72
  this.#spawnService = spawnService;
64
73
  this.#fetchService = fetchService;
74
+ this.#printerService = printerService;
75
+ }
76
+
77
+ // Mirrors SpawnInterfaceAdapter's plugin:add/plugin:remove pattern: wrap a spawned command's
78
+ // output in a quoted, marked block that's cleared on success (so a clean install stays quiet)
79
+ // but left on screen on failure (so the diagnostic output remains visible).
80
+ async #spawnQuoted(command: ReadonlyArray<string>): Promise<SpawnResult> {
81
+ const spawnService = this.#spawnService!;
82
+ const printerService = this.#printerService;
83
+ if (!printerService) {
84
+ return spawnService.spawn(command, { mode: "ignore" });
85
+ }
86
+
87
+ printerService.startQuote();
88
+ printerService.startMark();
89
+
90
+ // onOutput is synchronous and may be called concurrently for stdout/stderr lines, but
91
+ // printerService.info() is async and must not be invoked concurrently with itself - queue
92
+ // writes so they're applied one at a time, in call order.
93
+ let writeQueue: Promise<void> = Promise.resolve();
94
+ const onOutput = (line: string): void => {
95
+ writeQueue = writeQueue.then(() => printerService.info(`${line}\n`));
96
+ };
97
+
98
+ const result = await spawnService.spawn(command, { mode: "wrapped", onOutput });
99
+ await writeQueue;
100
+
101
+ printerService.endQuote();
102
+ printerService.endMark();
103
+ if (result.ok) {
104
+ await printerService.clearMarked();
105
+ } else {
106
+ printerService.discardMark();
107
+ }
108
+ return result;
65
109
  }
66
110
 
67
111
  public getUpgradeCheckResult(waitForResult = false): Promise<UpgradeCheckResult> {
@@ -74,7 +118,7 @@ export default class DefaultUpgradeService implements UpgradeService {
74
118
  // waiting via waitForResult=true) any chance of a fresh attempt for the rest of this
75
119
  // process's lifetime.
76
120
  this.#upgradeCheckPromise = this.checkForUpgrade().then((result) => {
77
- logger.debug(() => `Upgrade check result: ${JSON.stringify(result)}`);
121
+ logger.debug(() => describeUpgradeCheckResult(result));
78
122
  if (result.status === "failed") {
79
123
  this.#upgradeCheckPromise = undefined;
80
124
  }
@@ -208,7 +252,14 @@ export default class DefaultUpgradeService implements UpgradeService {
208
252
  return { ok: false, oldVersion, error: new Error("FetchService is not available") };
209
253
  }
210
254
 
255
+ if (this.#printerService) {
256
+ await this.#printerService.showSpinner(`Installing version ${checkResult.latestVersion}...`);
257
+ }
258
+
211
259
  try {
260
+ if (this.#printerService) {
261
+ await this.#printerService.hideSpinner();
262
+ }
212
263
  switch (checkResult.installMethod) {
213
264
  case InstallMethod.LINUX_SCRIPT:
214
265
  await this.#upgradeViaLinuxScript();
@@ -379,9 +430,7 @@ export default class DefaultUpgradeService implements UpgradeService {
379
430
 
380
431
  async #upgradeViaLinuxScript(): Promise<void> {
381
432
  const { scriptUrl } = this.#config.linuxScript!;
382
- const result = await this.#spawnService!.spawn(["sh", "-c", `curl -fsSL ${scriptUrl} | sh`], {
383
- mode: "ignore",
384
- });
433
+ const result = await this.#spawnQuoted(["sh", "-c", `curl -fsSL ${scriptUrl} | sh`]);
385
434
  if (!result.ok) {
386
435
  throw new Error(`Install script failed: ${describeSpawnFailure(result)}`);
387
436
  }
@@ -389,9 +438,7 @@ export default class DefaultUpgradeService implements UpgradeService {
389
438
 
390
439
  async #upgradeViaHomebrew(): Promise<void> {
391
440
  const { tap, formula } = this.#config.homebrew!;
392
- const result = await this.#spawnService!.spawn(["brew", "upgrade", `${tap}/${formula}`], {
393
- mode: "ignore",
394
- });
441
+ const result = await this.#spawnQuoted(["brew", "upgrade", `${tap}/${formula}`]);
395
442
  if (!result.ok) {
396
443
  throw new Error(`brew upgrade failed: ${describeSpawnFailure(result)}`);
397
444
  }
@@ -399,18 +446,15 @@ export default class DefaultUpgradeService implements UpgradeService {
399
446
 
400
447
  async #upgradeViaWinget(): Promise<void> {
401
448
  const { packageId } = this.#config.winget!;
402
- const result = await this.#spawnService!.spawn(
403
- [
404
- "winget",
405
- "upgrade",
406
- "--id",
407
- packageId,
408
- "--silent",
409
- "--accept-package-agreements",
410
- "--accept-source-agreements",
411
- ],
412
- { mode: "ignore" },
413
- );
449
+ const result = await this.#spawnQuoted([
450
+ "winget",
451
+ "upgrade",
452
+ "--id",
453
+ packageId,
454
+ "--silent",
455
+ "--accept-package-agreements",
456
+ "--accept-source-agreements",
457
+ ]);
414
458
  if (!result.ok) {
415
459
  throw new Error(`winget upgrade failed: ${describeSpawnFailure(result)}`);
416
460
  }
@@ -440,14 +484,11 @@ export default class DefaultUpgradeService implements UpgradeService {
440
484
  const currentExecutable = process.execPath;
441
485
 
442
486
  if (os === SupportedOs.WINDOWS) {
443
- const extractResult = await this.#spawnService!.spawn(
444
- [
445
- "powershell",
446
- "-Command",
447
- `Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpDir}' -Force`,
448
- ],
449
- { mode: "ignore" },
450
- );
487
+ const extractResult = await this.#spawnQuoted([
488
+ "powershell",
489
+ "-Command",
490
+ `Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpDir}' -Force`,
491
+ ]);
451
492
  if (!extractResult.ok) {
452
493
  throw new Error("Failed to extract release archive");
453
494
  }
@@ -458,32 +499,36 @@ export default class DefaultUpgradeService implements UpgradeService {
458
499
  // Windows won't let us delete the just-renamed-aside exe while this process still has it
459
500
  // open/mapped - that can only happen once we're no longer holding it, i.e. at the start of
460
501
  // the NEXT invocation, before we move today's running exe aside. Ignore failures: the file
461
- // may not exist, or may still be locked (e.g. another instance still running).
502
+ // may not exist, or may still be locked (e.g. another instance still running). Not quoted -
503
+ // this is expected to fail silently, there's nothing worth showing the user.
462
504
  await this.#spawnService!.spawn(["cmd", "/c", "del", "/f", "/q", oldPath], {
463
505
  mode: "ignore",
464
506
  });
465
507
 
466
- const moveResult = await this.#spawnService!.spawn(
467
- ["cmd", "/c", "move", "/y", currentExecutable, oldPath],
468
- { mode: "ignore" },
469
- );
508
+ const moveResult = await this.#spawnQuoted([
509
+ "cmd",
510
+ "/c",
511
+ "move",
512
+ "/y",
513
+ currentExecutable,
514
+ oldPath,
515
+ ]);
470
516
  if (!moveResult.ok) {
471
517
  throw new Error("Failed to move current executable aside");
472
518
  }
473
- const copyResult = await this.#spawnService!.spawn(
474
- ["cmd", "/c", "copy", "/y", extractedBinary, currentExecutable],
475
- { mode: "ignore" },
476
- );
519
+ const copyResult = await this.#spawnQuoted([
520
+ "cmd",
521
+ "/c",
522
+ "copy",
523
+ "/y",
524
+ extractedBinary,
525
+ currentExecutable,
526
+ ]);
477
527
  if (!copyResult.ok) {
478
528
  throw new Error("Failed to copy new executable into place");
479
529
  }
480
530
  } else {
481
- const extractResult = await this.#spawnService!.spawn(
482
- ["unzip", "-o", archivePath, "-d", tmpDir],
483
- {
484
- mode: "ignore",
485
- },
486
- );
531
+ const extractResult = await this.#spawnQuoted(["unzip", "-o", archivePath, "-d", tmpDir]);
487
532
  if (!extractResult.ok) {
488
533
  throw new Error("Failed to extract release archive");
489
534
  }
@@ -56,7 +56,8 @@ export default class UpgradeServiceProvider implements ServiceProvider {
56
56
  if (fetchService === undefined) {
57
57
  logger.debug(() => "FetchService not available, upgrade version checks will be unavailable");
58
58
  }
59
- upgradeService.setDependencies(spawnService, fetchService);
59
+ const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
60
+ upgradeService.setDependencies(spawnService, fetchService, printerService);
60
61
 
61
62
  void upgradeService.getUpgradeCheckResult();
62
63
 
@@ -44,15 +44,20 @@ export class UpgradeSubCommand implements SubCommand {
44
44
  public async execute(context: Context, argumentValues: Values): Promise<void> {
45
45
  const printerService = context.getServiceById(PRINTER_SERVICE_ID) as PrinterService;
46
46
  const cliName = context.cliConfig.name;
47
+ const currentVersion = context.cliConfig.version;
47
48
 
48
49
  const os = argumentValues.os as SupportedOs | undefined;
49
50
  const arch = undefined as SupportedArch | undefined;
50
51
  const installMethod = argumentValues["install-method"] as InstallMethod | undefined;
51
52
 
53
+ await printerService.showSpinner(`Looking for version newer than ${currentVersion}`);
54
+
52
55
  const checkResult =
53
56
  os === undefined && installMethod === undefined
54
57
  ? await this.#upgradeService.getUpgradeCheckResult(true)
55
58
  : await this.#upgradeService.checkForUpgrade(os, arch, installMethod);
59
+
60
+ await printerService.hideSpinner();
56
61
  if (checkResult.status === "unsupported") {
57
62
  await printerService.error(
58
63
  `No upgrade location is configured for the detected or requested platform.\n`,
@@ -77,7 +82,8 @@ export class UpgradeSubCommand implements SubCommand {
77
82
 
78
83
  if (!checkResult.updateAvailable) {
79
84
  await printerService.print(
80
- `${cliName} is already up to date (${checkResult.currentVersion}).\n`,
85
+ `${cliName} is already up to date: ${checkResult.currentVersion}\n`,
86
+ Icon.INFORMATION,
81
87
  );
82
88
  return;
83
89
  }