@promptbook/cli 0.114.0-26 → 0.114.0-28

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.
Files changed (65) hide show
  1. package/esm/index.es.js +374 -60
  2. package/esm/index.es.js.map +1 -1
  3. package/esm/src/cli/cli-commands/common/harness/$applyHarnessInstallationStatus.d.ts +1 -1
  4. package/esm/src/cli/cli-commands/common/harness/$resolveHarnessCommandPath.d.ts +14 -0
  5. package/esm/src/cli/cli-commands/common/harness/$resolveHarnessInstallationOrigin.d.ts +10 -0
  6. package/esm/src/cli/cli-commands/common/harness/$runHarnessInstallationCommand.d.ts +43 -0
  7. package/esm/src/cli/cli-commands/common/harness/HarnessDefinition.d.ts +26 -0
  8. package/esm/src/cli/cli-commands/common/harness/HarnessInstallationOrigin.d.ts +36 -0
  9. package/esm/src/cli/cli-commands/common/harness/HarnessInstallationStatus.d.ts +5 -0
  10. package/esm/src/cli/cli-commands/common/harness/HarnessUpdatePlan.d.ts +23 -0
  11. package/esm/src/cli/cli-commands/common/harness/buildHarnessInstallCommand.d.ts +4 -1
  12. package/esm/src/cli/cli-commands/common/harness/formatHarnessManualUpdateInstruction.d.ts +8 -0
  13. package/esm/src/cli/cli-commands/common/harness/formatHarnessManualUpdateInstruction.test.d.ts +1 -0
  14. package/esm/src/cli/cli-commands/common/harness/resolveHarnessInstallationMethodFromPaths.d.ts +31 -0
  15. package/esm/src/cli/cli-commands/common/harness/resolveHarnessInstallationMethodFromPaths.test.d.ts +1 -0
  16. package/esm/src/cli/cli-commands/common/harness/resolveHarnessUpdatePlan.d.ts +12 -0
  17. package/esm/src/cli/cli-commands/common/harness/resolveHarnessUpdatePlan.test.d.ts +1 -0
  18. package/esm/src/cli/cli-commands/common/npm/$resolveNpmGlobalPrefixPath.d.ts +9 -0
  19. package/esm/src/version.d.ts +1 -1
  20. package/package.json +1 -1
  21. package/src/cli/cli-commands/coder/init.ts +1 -1
  22. package/src/cli/cli-commands/coder/ping.ts +1 -1
  23. package/src/cli/cli-commands/coder/run.ts +1 -1
  24. package/src/cli/cli-commands/coder/server.ts +1 -1
  25. package/src/cli/cli-commands/common/harness/$applyHarnessInstallationStatus.ts +57 -8
  26. package/src/cli/cli-commands/common/harness/$checkHarnessInstallation.ts +8 -1
  27. package/src/cli/cli-commands/common/harness/$resolveHarnessCommandPath.ts +46 -0
  28. package/src/cli/cli-commands/common/harness/$resolveHarnessInstallationOrigin.ts +36 -0
  29. package/src/cli/cli-commands/common/harness/$runHarnessInstallationCommand.ts +100 -0
  30. package/src/cli/cli-commands/common/harness/HarnessDefinition.ts +34 -0
  31. package/src/cli/cli-commands/common/harness/HarnessInstallationOrigin.ts +44 -0
  32. package/src/cli/cli-commands/common/harness/HarnessInstallationStatus.ts +6 -0
  33. package/src/cli/cli-commands/common/harness/HarnessUpdatePlan.ts +27 -0
  34. package/src/cli/cli-commands/common/harness/buildHarnessInstallCommand.ts +4 -1
  35. package/src/cli/cli-commands/common/harness/formatHarnessInstallationWarning.ts +28 -6
  36. package/src/cli/cli-commands/common/harness/formatHarnessManualUpdateInstruction.ts +41 -0
  37. package/src/cli/cli-commands/common/harness/resolveHarnessInstallationMethodFromPaths.ts +111 -0
  38. package/src/cli/cli-commands/common/harness/resolveHarnessUpdatePlan.ts +47 -0
  39. package/src/cli/cli-commands/common/harnessUpdateCliOptions.ts +1 -1
  40. package/src/cli/cli-commands/common/npm/$resolveNpmGlobalPrefixPath.ts +51 -0
  41. package/src/other/templates/getTemplatesPipelineCollection.ts +816 -870
  42. package/src/version.ts +2 -2
  43. package/src/versions.txt +2 -0
  44. package/umd/index.umd.js +374 -60
  45. package/umd/index.umd.js.map +1 -1
  46. package/umd/src/cli/cli-commands/common/harness/$applyHarnessInstallationStatus.d.ts +1 -1
  47. package/umd/src/cli/cli-commands/common/harness/$resolveHarnessCommandPath.d.ts +14 -0
  48. package/umd/src/cli/cli-commands/common/harness/$resolveHarnessInstallationOrigin.d.ts +10 -0
  49. package/umd/src/cli/cli-commands/common/harness/$runHarnessInstallationCommand.d.ts +43 -0
  50. package/umd/src/cli/cli-commands/common/harness/HarnessDefinition.d.ts +26 -0
  51. package/umd/src/cli/cli-commands/common/harness/HarnessInstallationOrigin.d.ts +36 -0
  52. package/umd/src/cli/cli-commands/common/harness/HarnessInstallationStatus.d.ts +5 -0
  53. package/umd/src/cli/cli-commands/common/harness/HarnessUpdatePlan.d.ts +23 -0
  54. package/umd/src/cli/cli-commands/common/harness/buildHarnessInstallCommand.d.ts +4 -1
  55. package/umd/src/cli/cli-commands/common/harness/formatHarnessManualUpdateInstruction.d.ts +8 -0
  56. package/umd/src/cli/cli-commands/common/harness/formatHarnessManualUpdateInstruction.test.d.ts +1 -0
  57. package/umd/src/cli/cli-commands/common/harness/resolveHarnessInstallationMethodFromPaths.d.ts +31 -0
  58. package/umd/src/cli/cli-commands/common/harness/resolveHarnessInstallationMethodFromPaths.test.d.ts +1 -0
  59. package/umd/src/cli/cli-commands/common/harness/resolveHarnessUpdatePlan.d.ts +12 -0
  60. package/umd/src/cli/cli-commands/common/harness/resolveHarnessUpdatePlan.test.d.ts +1 -0
  61. package/umd/src/cli/cli-commands/common/npm/$resolveNpmGlobalPrefixPath.d.ts +9 -0
  62. package/umd/src/version.d.ts +1 -1
  63. package/esm/src/cli/cli-commands/common/harness/$installHarness.d.ts +0 -13
  64. package/src/cli/cli-commands/common/harness/$installHarness.ts +0 -56
  65. package/umd/src/cli/cli-commands/common/harness/$installHarness.d.ts +0 -13
@@ -0,0 +1,100 @@
1
+ import colors from 'colors';
2
+ import { spaceTrim } from 'spacetrim';
3
+ import { assertsError } from '../../../../errors/assertsError';
4
+ import { $execCommand } from '../../../../utils/execCommand/$execCommand';
5
+ import type { HarnessDefinition } from './HarnessDefinition';
6
+
7
+ /**
8
+ * What one harness installation command does with the harness.
9
+ *
10
+ * @private internal utility of `promptbookCli`
11
+ */
12
+ export type HarnessInstallationOperation = 'install' | 'update';
13
+
14
+ /**
15
+ * Words describing each operation in the terminal output.
16
+ */
17
+ const HARNESS_INSTALLATION_OPERATION_WORDS: Readonly<
18
+ Record<HarnessInstallationOperation, { readonly running: string; readonly finished: string }>
19
+ > = {
20
+ install: { running: 'Installing', finished: 'installed' },
21
+ update: { running: 'Updating', finished: 'updated' },
22
+ };
23
+
24
+ /**
25
+ * Options of one command which installs or updates a CLI coding harness.
26
+ *
27
+ * @private internal utility of `$runHarnessInstallationCommand`
28
+ */
29
+ type HarnessInstallationCommandOptions = {
30
+ /**
31
+ * Harness which is installed or updated.
32
+ */
33
+ readonly definition: HarnessDefinition;
34
+
35
+ /**
36
+ * What the command does with the harness.
37
+ */
38
+ readonly operation: HarnessInstallationOperation;
39
+
40
+ /**
41
+ * Shell command which does it.
42
+ */
43
+ readonly command: string;
44
+
45
+ /**
46
+ * Extra environment variables required by the command.
47
+ */
48
+ readonly environment?: Readonly<Record<string, string>>;
49
+ };
50
+
51
+ /**
52
+ * Runs one command which installs or updates a CLI coding harness.
53
+ *
54
+ * A failed installation is reported but never aborts the running command, because the harness check
55
+ * is only an assistant - the command itself may still work with a manually managed harness.
56
+ *
57
+ * Note: `$` is used to indicate that this function is not a pure function - it installs software
58
+ *
59
+ * @returns `true` when the command succeeded, `false` when it failed
60
+ * @private internal utility of `promptbookCli`
61
+ */
62
+ export async function $runHarnessInstallationCommand(options: HarnessInstallationCommandOptions): Promise<boolean> {
63
+ const { definition, operation, command, environment } = options;
64
+ const operationWords = HARNESS_INSTALLATION_OPERATION_WORDS[operation];
65
+
66
+ console.info(colors.cyan(`${operationWords.running} ${definition.label} with \`${command}\`...`));
67
+
68
+ try {
69
+ await $execCommand({
70
+ command,
71
+ crashOnError: true,
72
+ isVerbose: true,
73
+ env: environment ? { ...environment } : undefined,
74
+ });
75
+ } catch (error) {
76
+ assertsError(error);
77
+ const installErrorMessage = error.message;
78
+
79
+ console.error(
80
+ colors.red(
81
+ spaceTrim(
82
+ (block) => `
83
+ Could not ${operation} **${definition.label}**.
84
+
85
+ Run \`${command}\` manually and run the command again.
86
+
87
+ ${block(installErrorMessage)}
88
+ `,
89
+ ),
90
+ ),
91
+ );
92
+
93
+ return false;
94
+ }
95
+
96
+ console.info(colors.green(`${definition.label} has been ${operationWords.finished}.`));
97
+ return true;
98
+ }
99
+
100
+ // Note: [🟡] Code for CLI harness installation commands [$runHarnessInstallationCommand](src/cli/cli-commands/common/harness/$runHarnessInstallationCommand.ts) should never be published outside of `@promptbook/cli`
@@ -2,6 +2,28 @@ import { spaceTrim } from 'spacetrim';
2
2
  import { UnexpectedError } from '../../../../errors/UnexpectedError';
3
3
  import type { PromptRunnerHarnessName } from '../promptRunnerCliOptions';
4
4
 
5
+ /**
6
+ * Description of a standalone *(self-managed)* installation of one CLI coding harness.
7
+ *
8
+ * Some harnesses are also distributed by their own installer which puts the harness outside of npm and
9
+ * updates it in place. Such an installation must never be updated with `npm install -g`, because that
10
+ * installs a second copy of the harness instead of updating the one which is really used.
11
+ *
12
+ * @private internal utility of `promptbookCli`
13
+ */
14
+ export type HarnessStandaloneInstallation = {
15
+ /**
16
+ * Directory names which appear in the resolved path of a standalone installation of the harness,
17
+ * for example `.codex` in `~/.codex/packages/standalone/current/bin/codex`.
18
+ */
19
+ readonly directoryNames: ReadonlyArray<string>;
20
+
21
+ /**
22
+ * Command which updates the standalone installation in place.
23
+ */
24
+ readonly updateCommand: string;
25
+ };
26
+
5
27
  /**
6
28
  * Static description of one supported CLI coding harness.
7
29
  *
@@ -32,6 +54,13 @@ export type HarnessDefinition = {
32
54
  * Extra environment variables required by the global npm installation of this harness.
33
55
  */
34
56
  readonly npmInstallEnvironment?: Readonly<Record<string, string>>;
57
+
58
+ /**
59
+ * How a standalone installation of this harness is recognized and updated.
60
+ *
61
+ * Harnesses which are distributed only through npm leave this undefined.
62
+ */
63
+ readonly standaloneInstallation?: HarnessStandaloneInstallation;
35
64
  };
36
65
 
37
66
  /**
@@ -45,6 +74,11 @@ export const HARNESS_DEFINITIONS: Readonly<Record<PromptRunnerHarnessName, Harne
45
74
  label: 'OpenAI Codex',
46
75
  commandName: 'codex',
47
76
  npmPackageName: '@openai/codex',
77
+ // Note: The official standalone installer keeps its package in `~/.codex` and links it onto the `PATH`
78
+ standaloneInstallation: {
79
+ directoryNames: ['.codex'],
80
+ updateCommand: 'codex update',
81
+ },
48
82
  },
49
83
  'github-copilot': {
50
84
  harnessName: 'github-copilot',
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Way in which the globally available harness command got onto the current machine.
3
+ *
4
+ * - `npm-global` the command comes from a global npm installation, so `npm install -g` really updates it
5
+ * - `standalone` the command was installed by the own installer of the harness and updates itself in place
6
+ * - `homebrew` the command is managed by Homebrew, so only `brew` may update it
7
+ * - `unknown` the command lives somewhere else or could not be located at all
8
+ *
9
+ * @private internal utility of `promptbookCli`
10
+ */
11
+ export type HarnessInstallationMethod = 'npm-global' | 'standalone' | 'homebrew' | 'unknown';
12
+
13
+ /**
14
+ * Place where the harness command really lives together with the way it was installed there.
15
+ *
16
+ * Knowing the origin is what keeps an update from installing a **second** copy of a harness which is
17
+ * managed outside of npm, for example the standalone installation of OpenAI Codex.
18
+ *
19
+ * @private internal utility of `promptbookCli`
20
+ */
21
+ export type HarnessInstallationOrigin = {
22
+ /**
23
+ * Resolved path of the harness command with every symbolic link followed
24
+ * or `null` when the command could not be located.
25
+ */
26
+ readonly commandPath: string | null;
27
+
28
+ /**
29
+ * Way in which the harness command was installed.
30
+ */
31
+ readonly installationMethod: HarnessInstallationMethod;
32
+ };
33
+
34
+ /**
35
+ * Origin used when the harness command was not looked up at all, for example when the update check is disabled.
36
+ *
37
+ * @private internal utility of `promptbookCli`
38
+ */
39
+ export const UNKNOWN_HARNESS_INSTALLATION_ORIGIN: HarnessInstallationOrigin = {
40
+ commandPath: null,
41
+ installationMethod: 'unknown',
42
+ };
43
+
44
+ // Note: [🟡] Code for CLI harness installation origin [HarnessInstallationOrigin](src/cli/cli-commands/common/harness/HarnessInstallationOrigin.ts) should never be published outside of `@promptbook/cli`
@@ -1,4 +1,5 @@
1
1
  import type { HarnessDefinition } from './HarnessDefinition';
2
+ import type { HarnessInstallationOrigin } from './HarnessInstallationOrigin';
2
3
 
3
4
  /**
4
5
  * Result of comparing the globally installed harness version with the newest published one.
@@ -38,6 +39,11 @@ export type HarnessInstallationStatus = {
38
39
  * Newest version published to npm or `null` when it could not be resolved.
39
40
  */
40
41
  readonly latestVersion: string | null;
42
+
43
+ /**
44
+ * Place where the harness command lives together with the way it was installed there.
45
+ */
46
+ readonly installationOrigin: HarnessInstallationOrigin;
41
47
  };
42
48
 
43
49
  // Note: [🟡] Code for CLI harness installation status [HarnessInstallationStatus](src/cli/cli-commands/common/harness/HarnessInstallationStatus.ts) should never be published outside of `@promptbook/cli`
@@ -0,0 +1,27 @@
1
+ /**
2
+ * How an outdated CLI coding harness can be brought up to date where it is really installed.
3
+ *
4
+ * @private internal utility of `promptbookCli`
5
+ */
6
+ export type HarnessUpdatePlan = {
7
+ /**
8
+ * Command which updates the harness in place
9
+ * or `null` when the installation is not understood well enough to name one.
10
+ */
11
+ readonly command: string | null;
12
+
13
+ /**
14
+ * Whether Promptbook may run the command itself instead of only printing it.
15
+ *
16
+ * Only installations which Promptbook can update without any side effect are updated automatically,
17
+ * everything else is left to the user together with the instructions.
18
+ */
19
+ readonly isRunnableByPromptbook: boolean;
20
+
21
+ /**
22
+ * Extra environment variables required by the update command.
23
+ */
24
+ readonly environment?: Readonly<Record<string, string>>;
25
+ };
26
+
27
+ // Note: [🟡] Code for CLI harness update plan [HarnessUpdatePlan](src/cli/cli-commands/common/harness/HarnessUpdatePlan.ts) should never be published outside of `@promptbook/cli`
@@ -4,9 +4,12 @@ import type { HarnessDefinition } from './HarnessDefinition';
4
4
  /**
5
5
  * Builds the shell command which installs or updates one CLI coding harness globally.
6
6
  *
7
- * The very same command is executed by `$installHarness` and printed as the manual fallback,
7
+ * The very same command is executed by `$runHarnessInstallationCommand` and printed as the manual fallback,
8
8
  * so the user always sees exactly what Promptbook would run.
9
9
  *
10
+ * Note: This command installs the harness through npm, so it may only update a harness which npm really owns,
11
+ * see `resolveHarnessUpdatePlan`.
12
+ *
10
13
  * @private internal utility of `promptbookCli`
11
14
  */
12
15
  export function buildHarnessInstallCommand(definition: HarnessDefinition): string {
@@ -1,13 +1,24 @@
1
1
  import { spaceTrim } from 'spacetrim';
2
+ import type { HarnessInstallationMethod } from './HarnessInstallationOrigin';
2
3
  import type { HarnessInstallationStatus } from './HarnessInstallationStatus';
3
4
 
5
+ /**
6
+ * Human-readable description of each way a harness can be installed.
7
+ */
8
+ const HARNESS_INSTALLATION_METHOD_DESCRIPTIONS: Readonly<Record<HarnessInstallationMethod, string>> = {
9
+ 'npm-global': 'global npm installation',
10
+ standalone: 'standalone installation',
11
+ homebrew: 'Homebrew installation',
12
+ unknown: 'unknown installation method',
13
+ };
14
+
4
15
  /**
5
16
  * Formats the warning shown when a harness is missing or outdated.
6
17
  *
7
18
  * @private internal utility of `promptbookCli`
8
19
  */
9
20
  export function formatHarnessInstallationWarning(status: HarnessInstallationStatus): string {
10
- const { definition, installedVersion, latestVersion } = status;
21
+ const { definition, installedVersion, latestVersion, installationOrigin } = status;
11
22
 
12
23
  if (status.installationState === 'not-installed') {
13
24
  return spaceTrim(`
@@ -17,12 +28,23 @@ export function formatHarnessInstallationWarning(status: HarnessInstallationStat
17
28
  `);
18
29
  }
19
30
 
20
- return spaceTrim(`
21
- **${definition.label}** is outdated.
31
+ const warningLines = [
32
+ `**${definition.label}** is outdated.`,
33
+ '',
34
+ `Installed version: \`${installedVersion}\``,
35
+ `Newest version: \`${latestVersion}\``,
36
+ ];
37
+
38
+ if (installationOrigin.commandPath !== null) {
39
+ const installationMethodDescription =
40
+ HARNESS_INSTALLATION_METHOD_DESCRIPTIONS[installationOrigin.installationMethod];
41
+
42
+ warningLines.push(
43
+ `Installed command: \`${installationOrigin.commandPath}\` *(${installationMethodDescription})*`,
44
+ );
45
+ }
22
46
 
23
- Installed version: \`${installedVersion}\`
24
- Newest version: \`${latestVersion}\`
25
- `);
47
+ return spaceTrim(warningLines.join('\n'));
26
48
  }
27
49
 
28
50
  // Note: [🟡] Code for CLI harness installation warning [formatHarnessInstallationWarning](src/cli/cli-commands/common/harness/formatHarnessInstallationWarning.ts) should never be published outside of `@promptbook/cli`
@@ -0,0 +1,41 @@
1
+ import { spaceTrim } from 'spacetrim';
2
+ import { buildHarnessInstallCommand } from './buildHarnessInstallCommand';
3
+ import type { HarnessInstallationStatus } from './HarnessInstallationStatus';
4
+ import type { HarnessUpdatePlan } from './HarnessUpdatePlan';
5
+
6
+ /**
7
+ * Formats the instruction shown when an outdated harness must be updated by the user.
8
+ *
9
+ * @private internal utility of `promptbookCli`
10
+ */
11
+ export function formatHarnessManualUpdateInstruction(
12
+ status: HarnessInstallationStatus,
13
+ updatePlan: HarnessUpdatePlan,
14
+ ): string {
15
+ const { definition, installationOrigin } = status;
16
+
17
+ const commandLocation =
18
+ installationOrigin.commandPath === null
19
+ ? `The \`${definition.commandName}\` command`
20
+ : `The \`${definition.commandName}\` command at \`${installationOrigin.commandPath}\``;
21
+
22
+ if (updatePlan.command !== null) {
23
+ return spaceTrim(`
24
+ ${commandLocation} is not managed by npm, so Promptbook leaves it alone.
25
+
26
+ Update it manually with \`${updatePlan.command}\`.
27
+ `);
28
+ }
29
+
30
+ const unknownInstallationReason =
31
+ installationOrigin.commandPath === null ? 'could not be located' : 'is not inside the global npm prefix';
32
+ const npmInstallCommand = buildHarnessInstallCommand(definition);
33
+
34
+ return spaceTrim(`
35
+ ${commandLocation} ${unknownInstallationReason}, so Promptbook cannot tell how it was installed.
36
+
37
+ Update it the same way it was installed - \`${npmInstallCommand}\` would install a **second** copy of ${definition.label} instead of updating the one which is really used.
38
+ `);
39
+ }
40
+
41
+ // Note: [🟡] Code for CLI harness manual update instruction [formatHarnessManualUpdateInstruction](src/cli/cli-commands/common/harness/formatHarnessManualUpdateInstruction.ts) should never be published outside of `@promptbook/cli`
@@ -0,0 +1,111 @@
1
+ import type { HarnessDefinition } from './HarnessDefinition';
2
+ import type { HarnessInstallationMethod } from './HarnessInstallationOrigin';
3
+
4
+ /**
5
+ * Directory which holds npm packages, so it appears in the resolved path of every globally installed npm binary
6
+ * on systems where the global binary is only a symbolic link into the package.
7
+ *
8
+ * Note: Written in lower case because it is compared against normalized paths.
9
+ */
10
+ const NODE_MODULES_DIRECTORY_NAME = 'node_modules';
11
+
12
+ /**
13
+ * Directory into which Homebrew unpacks every formula, so it appears in the resolved path of
14
+ * everything Homebrew manages and in nothing else.
15
+ *
16
+ * Note: Written in lower case because it is compared against normalized paths.
17
+ */
18
+ const HOMEBREW_DIRECTORY_NAME = 'cellar';
19
+
20
+ /**
21
+ * Options describing where the harness command was found.
22
+ *
23
+ * @private internal utility of `resolveHarnessInstallationMethodFromPaths`
24
+ */
25
+ type HarnessInstallationPaths = {
26
+ /**
27
+ * Harness whose command was located.
28
+ */
29
+ readonly definition: HarnessDefinition;
30
+
31
+ /**
32
+ * Resolved path of the harness command with every symbolic link followed.
33
+ */
34
+ readonly commandPath: string;
35
+
36
+ /**
37
+ * Root directory of global npm installations or `null` when npm could not be asked for it.
38
+ */
39
+ readonly npmGlobalPrefixPath: string | null;
40
+ };
41
+
42
+ /**
43
+ * Decides how a harness command was installed from the place where it really lives.
44
+ *
45
+ * Only a command which npm owns may be updated with `npm install -g`, every other command has to be
46
+ * updated where it is installed, otherwise the update silently adds a second copy of the harness.
47
+ *
48
+ * @private internal utility of `promptbookCli`
49
+ */
50
+ export function resolveHarnessInstallationMethodFromPaths(paths: HarnessInstallationPaths): HarnessInstallationMethod {
51
+ const { definition, commandPath, npmGlobalPrefixPath } = paths;
52
+
53
+ const normalizedCommandPath = normalizeInstallationPath(commandPath);
54
+ const commandPathDirectoryNames = normalizedCommandPath.split('/');
55
+
56
+ // Note: A Homebrew formula may bundle its own `node_modules` and may live under the npm prefix of a
57
+ // Homebrew-installed Node, so Homebrew has to be recognized before anything npm-shaped
58
+ if (commandPathDirectoryNames.includes(HOMEBREW_DIRECTORY_NAME)) {
59
+ return 'homebrew';
60
+ }
61
+
62
+ const standaloneDirectoryNames = definition.standaloneInstallation?.directoryNames ?? [];
63
+
64
+ if (
65
+ standaloneDirectoryNames.some((directoryName) =>
66
+ commandPathDirectoryNames.includes(normalizeInstallationPath(directoryName)),
67
+ )
68
+ ) {
69
+ return 'standalone';
70
+ }
71
+
72
+ // Note: npm is not always reachable, but its own package directory is a reliable fallback marker
73
+ if (
74
+ normalizedCommandPath.includes(
75
+ `/${NODE_MODULES_DIRECTORY_NAME}/${normalizeInstallationPath(definition.npmPackageName)}/`,
76
+ )
77
+ ) {
78
+ return 'npm-global';
79
+ }
80
+
81
+ if (isPathInsideDirectory(normalizedCommandPath, npmGlobalPrefixPath)) {
82
+ return 'npm-global';
83
+ }
84
+
85
+ return 'unknown';
86
+ }
87
+
88
+ /**
89
+ * Normalizes one path so that paths coming from different tools and platforms can be compared.
90
+ *
91
+ * Note: The comparison is case-insensitive because Windows paths are, and two harness paths which differ
92
+ * only in letter casing do not exist in practice.
93
+ */
94
+ function normalizeInstallationPath(path: string): string {
95
+ return path.trim().replace(/\\/gu, '/').replace(/\/+$/u, '').toLowerCase();
96
+ }
97
+
98
+ /**
99
+ * Checks whether one path lies inside a directory.
100
+ */
101
+ function isPathInsideDirectory(normalizedPath: string, directoryPath: string | null): boolean {
102
+ if (directoryPath === null) {
103
+ return false;
104
+ }
105
+
106
+ const normalizedDirectoryPath = normalizeInstallationPath(directoryPath);
107
+
108
+ return normalizedDirectoryPath !== '' && normalizedPath.startsWith(`${normalizedDirectoryPath}/`);
109
+ }
110
+
111
+ // Note: [🟡] Code for CLI harness installation method detection [resolveHarnessInstallationMethodFromPaths](src/cli/cli-commands/common/harness/resolveHarnessInstallationMethodFromPaths.ts) should never be published outside of `@promptbook/cli`
@@ -0,0 +1,47 @@
1
+ import { buildHarnessInstallCommand } from './buildHarnessInstallCommand';
2
+ import type { HarnessDefinition } from './HarnessDefinition';
3
+ import type { HarnessInstallationMethod } from './HarnessInstallationOrigin';
4
+ import type { HarnessUpdatePlan } from './HarnessUpdatePlan';
5
+
6
+ /**
7
+ * Decides how one outdated CLI coding harness should be updated.
8
+ *
9
+ * The plan follows the way the harness was installed, because `npm install -g` updates only a harness
10
+ * which npm owns and installs a **second** copy of every other one.
11
+ *
12
+ * @private internal utility of `promptbookCli`
13
+ */
14
+ export function resolveHarnessUpdatePlan(
15
+ definition: HarnessDefinition,
16
+ installationMethod: HarnessInstallationMethod,
17
+ ): HarnessUpdatePlan {
18
+ if (installationMethod === 'npm-global') {
19
+ return {
20
+ command: buildHarnessInstallCommand(definition),
21
+ isRunnableByPromptbook: true,
22
+ environment: definition.npmInstallEnvironment,
23
+ };
24
+ }
25
+
26
+ if (installationMethod === 'standalone' && definition.standaloneInstallation !== undefined) {
27
+ return {
28
+ command: definition.standaloneInstallation.updateCommand,
29
+ isRunnableByPromptbook: true,
30
+ };
31
+ }
32
+
33
+ if (installationMethod === 'homebrew') {
34
+ // Note: The formula may be named differently than the command, so the update is only suggested
35
+ return {
36
+ command: `brew upgrade ${definition.commandName}`,
37
+ isRunnableByPromptbook: false,
38
+ };
39
+ }
40
+
41
+ return {
42
+ command: null,
43
+ isRunnableByPromptbook: false,
44
+ };
45
+ }
46
+
47
+ // Note: [🟡] Code for CLI harness update planning [resolveHarnessUpdatePlan](src/cli/cli-commands/common/harness/resolveHarnessUpdatePlan.ts) should never be published outside of `@promptbook/cli`
@@ -26,7 +26,7 @@ export type NormalizedHarnessUpdateCliOptions = {
26
26
  * @private internal utility of `promptbookCli`
27
27
  */
28
28
  export function addHarnessUpdateOption(command: Program): void {
29
- command.option('--no-harness-update', 'Skip checking whether globally installed coding harnesses are up to date');
29
+ command.option('--no-harness-update', 'Skip checking whether the installed coding harnesses are up to date');
30
30
  }
31
31
 
32
32
  /**
@@ -0,0 +1,51 @@
1
+ import { $execCommand } from '../../../../utils/execCommand/$execCommand';
2
+
3
+ /**
4
+ * Time limit for asking npm where it installs global packages.
5
+ */
6
+ const NPM_GLOBAL_PREFIX_COMMAND_TIMEOUT_MS = 30 * 1000;
7
+
8
+ /**
9
+ * Cached lookup, because the global npm prefix cannot change while one command runs and several
10
+ * harnesses are checked at the same time.
11
+ */
12
+ let npmGlobalPrefixPathPromise: Promise<string | null> | null = null;
13
+
14
+ /**
15
+ * Reads the root directory under which npm installs global packages.
16
+ *
17
+ * Note: `$` is used to indicate that this function is not a pure function - it runs npm
18
+ *
19
+ * @returns The global npm prefix or `null` when npm could not be asked for it
20
+ * @private internal utility of `promptbookCli`
21
+ */
22
+ export function $resolveNpmGlobalPrefixPath(): Promise<string | null> {
23
+ if (npmGlobalPrefixPathPromise === null) {
24
+ npmGlobalPrefixPathPromise = $readNpmGlobalPrefixPath();
25
+ }
26
+
27
+ return npmGlobalPrefixPathPromise;
28
+ }
29
+
30
+ /**
31
+ * Asks npm for the global prefix once.
32
+ */
33
+ async function $readNpmGlobalPrefixPath(): Promise<string | null> {
34
+ const output: string = await $execCommand({
35
+ command: 'npm prefix --global --loglevel=error',
36
+ crashOnError: true,
37
+ timeout: NPM_GLOBAL_PREFIX_COMMAND_TIMEOUT_MS,
38
+ isVerbose: false,
39
+ }).catch(() => '');
40
+
41
+ // Note: npm prints the prefix as the last line, warnings emitted before it are ignored
42
+ const prefixPath = output
43
+ .split('\n')
44
+ .map((line) => line.trim())
45
+ .filter((line) => line !== '')
46
+ .pop();
47
+
48
+ return prefixPath ?? null;
49
+ }
50
+
51
+ // Note: [🟡] Code for CLI npm global prefix lookup [$resolveNpmGlobalPrefixPath](src/cli/cli-commands/common/npm/$resolveNpmGlobalPrefixPath.ts) should never be published outside of `@promptbook/cli`