rman 1.3.0 → 2.0.0-beta.3

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 (187) hide show
  1. package/README.md +65 -31
  2. package/builtins/platforms/node/augmentation/rmanrc.augmentation.d.ts +83 -0
  3. package/builtins/platforms/node/augmentation/system-info.augmentation.d.ts +26 -0
  4. package/builtins/platforms/node/augmentation/system-info.augmentation.js +79 -0
  5. package/builtins/platforms/node/commands/ci.command.d.ts +133 -0
  6. package/builtins/platforms/node/commands/ci.command.js +60 -0
  7. package/builtins/platforms/node/commands/clean.command.d.ts +185 -0
  8. package/builtins/platforms/node/commands/clean.command.js +74 -0
  9. package/builtins/platforms/node/index.d.ts +8 -0
  10. package/builtins/platforms/node/index.js +8 -0
  11. package/builtins/platforms/node/node-config.interface.d.ts +76 -0
  12. package/builtins/platforms/node/node-config.interface.js +7 -0
  13. package/builtins/platforms/node/node-manifest.provider.d.ts +68 -0
  14. package/builtins/platforms/node/node-manifest.provider.js +125 -0
  15. package/builtins/platforms/node/node.platform.d.ts +53 -0
  16. package/builtins/platforms/node/node.platform.js +134 -0
  17. package/builtins/platforms/node/services/ci.service.d.ts +47 -0
  18. package/builtins/platforms/node/services/ci.service.js +213 -0
  19. package/builtins/platforms/node/services/clean.service.d.ts +53 -0
  20. package/builtins/platforms/node/services/clean.service.js +237 -0
  21. package/builtins/platforms/node/services/publish.service.d.ts +114 -0
  22. package/builtins/platforms/node/services/publish.service.js +371 -0
  23. package/builtins/platforms/node/services/version-plan.service.d.ts +44 -0
  24. package/builtins/platforms/node/services/version-plan.service.js +58 -0
  25. package/builtins/platforms/node/utils/workspace-range.d.ts +26 -0
  26. package/builtins/platforms/node/utils/workspace-range.js +28 -0
  27. package/builtins/presets/index.d.ts +15 -0
  28. package/builtins/presets/index.js +57 -0
  29. package/builtins/presets/node.d.ts +16 -0
  30. package/builtins/presets/node.js +57 -0
  31. package/{services → builtins/publish-targets/docker}/docker-publish.service.d.ts +52 -31
  32. package/{services → builtins/publish-targets/docker}/docker-publish.service.js +49 -26
  33. package/builtins/publish-targets/docker/docker.target.d.ts +54 -0
  34. package/builtins/publish-targets/docker/docker.target.js +40 -0
  35. package/builtins/publish-targets/docker/index.d.ts +2 -0
  36. package/builtins/publish-targets/docker/index.js +2 -0
  37. package/builtins/publish-targets/npm/index.d.ts +2 -0
  38. package/builtins/publish-targets/npm/index.js +2 -0
  39. package/builtins/publish-targets/npm/npm-publish-target.d.ts +73 -0
  40. package/builtins/publish-targets/npm/npm-publish-target.js +95 -0
  41. package/builtins/publish-targets/npm/npm-view.d.ts +48 -0
  42. package/builtins/publish-targets/npm/npm-view.js +71 -0
  43. package/cli.d.ts +8 -0
  44. package/cli.js +228 -93
  45. package/commands/build.command.d.ts +177 -3
  46. package/commands/build.command.js +20 -10
  47. package/commands/changelog.command.d.ts +296 -3
  48. package/commands/changelog.command.js +127 -43
  49. package/commands/config.command.d.ts +44 -3
  50. package/commands/config.command.js +53 -24
  51. package/commands/diff.command.d.ts +37 -3
  52. package/commands/diff.command.js +25 -16
  53. package/commands/exec.command.d.ts +193 -3
  54. package/commands/exec.command.js +60 -58
  55. package/commands/github-release.command.d.ts +154 -3
  56. package/commands/github-release.command.js +67 -37
  57. package/commands/import.command.d.ts +36 -3
  58. package/commands/import.command.js +28 -20
  59. package/commands/info.command.d.ts +35 -7
  60. package/commands/info.command.js +36 -30
  61. package/commands/lint.command.d.ts +176 -0
  62. package/commands/lint.command.js +34 -0
  63. package/commands/list.command.d.ts +163 -3
  64. package/commands/list.command.js +109 -71
  65. package/commands/publish.command.d.ts +231 -0
  66. package/commands/publish.command.js +304 -0
  67. package/commands/run.command.d.ts +186 -6
  68. package/commands/run.command.js +26 -72
  69. package/commands/test.command.d.ts +173 -3
  70. package/commands/test.command.js +16 -10
  71. package/commands/version.command.d.ts +330 -3
  72. package/commands/version.command.js +292 -78
  73. package/commands.d.ts +32 -0
  74. package/commands.js +28 -0
  75. package/constants.js +1 -1
  76. package/core/application.d.ts +116 -0
  77. package/core/application.js +160 -0
  78. package/core/classes/package.d.ts +219 -0
  79. package/core/classes/package.js +214 -0
  80. package/core/classes/registry.d.ts +30 -0
  81. package/core/classes/registry.js +47 -0
  82. package/core/{repository.d.ts → classes/repository.d.ts} +69 -20
  83. package/core/{repository.js → classes/repository.js} +195 -75
  84. package/core/classes/service.d.ts +49 -0
  85. package/core/classes/service.js +40 -0
  86. package/core/{version-scheme.d.ts → classes/version-scheme.d.ts} +23 -1
  87. package/core/{version-scheme.js → classes/version-scheme.js} +29 -1
  88. package/core/classes/workspace.d.ts +275 -0
  89. package/core/classes/workspace.js +537 -0
  90. package/core/command-builder.d.ts +14 -0
  91. package/core/command-builder.js +94 -0
  92. package/core/config/config-file-scope.d.ts +107 -0
  93. package/core/config/config-file-scope.js +226 -0
  94. package/core/config/config-interpolator.d.ts +164 -0
  95. package/core/config/config-interpolator.js +577 -0
  96. package/core/config/config-paths.d.ts +65 -0
  97. package/core/config/config-paths.js +78 -0
  98. package/core/config/config-reader.d.ts +192 -0
  99. package/core/config/config-reader.js +727 -0
  100. package/core/{merge-config.d.ts → config/merge-config.d.ts} +33 -34
  101. package/core/config/merge-config.js +260 -0
  102. package/core/{resolve-target.d.ts → config/resolve-config-target.d.ts} +1 -1
  103. package/core/{resolve-target.js → config/resolve-config-target.js} +1 -1
  104. package/core/{custom-command.d.ts → interfaces/custom-command.d.ts} +46 -10
  105. package/core/{custom-command.js → interfaces/custom-command.js} +44 -17
  106. package/core/{manifest.d.ts → interfaces/manifest.d.ts} +49 -27
  107. package/core/interfaces/manifest.js +132 -0
  108. package/core/interfaces/plugin.d.ts +174 -0
  109. package/core/interfaces/plugin.js +124 -0
  110. package/core/interfaces/publish-target.d.ts +136 -0
  111. package/core/interfaces/publish-target.js +43 -0
  112. package/core/{run-step.d.ts → interfaces/run-step.d.ts} +4 -4
  113. package/core/interfaces/run-step.js +1 -0
  114. package/core/plugin-loader.d.ts +25 -0
  115. package/core/plugin-loader.js +53 -0
  116. package/index.d.ts +122 -24
  117. package/index.js +66 -16
  118. package/interfaces/config-scope.interface.d.ts +223 -0
  119. package/interfaces/config-scope.interface.js +1 -0
  120. package/interfaces/rman-config.interface.d.ts +821 -203
  121. package/interfaces/rman-config.interface.js +78 -1
  122. package/package.json +3 -1
  123. package/services/change-hash.service.d.ts +42 -8
  124. package/services/change-hash.service.js +92 -13
  125. package/services/changelog.service.d.ts +91 -52
  126. package/services/changelog.service.js +460 -74
  127. package/services/config-service.d.ts +8 -0
  128. package/services/config-service.js +3 -0
  129. package/services/exec.service.d.ts +23 -12
  130. package/services/exec.service.js +14 -9
  131. package/services/github-release.service.d.ts +45 -34
  132. package/services/github-release.service.js +15 -12
  133. package/services/import.service.d.ts +25 -14
  134. package/services/import.service.js +9 -5
  135. package/services/list.service.d.ts +67 -13
  136. package/services/list.service.js +62 -15
  137. package/services/run.service.d.ts +35 -22
  138. package/services/run.service.js +316 -233
  139. package/services/system-info.d.ts +1 -1
  140. package/services/version-plan.service.d.ts +35 -8
  141. package/services/version-plan.service.js +96 -36
  142. package/services/version.service.d.ts +34 -14
  143. package/services/version.service.js +35 -16
  144. package/services.d.ts +1 -1
  145. package/services.js +1 -1
  146. package/utils/bin-path.d.ts +6 -7
  147. package/utils/bin-path.js +7 -18
  148. package/utils/branch-guard.d.ts +30 -1
  149. package/utils/branch-guard.js +31 -0
  150. package/utils/color-yaml.d.ts +8 -0
  151. package/utils/color-yaml.js +86 -0
  152. package/utils/exec.d.ts +10 -0
  153. package/utils/exec.js +1 -1
  154. package/utils/git.d.ts +26 -0
  155. package/utils/git.js +65 -0
  156. package/utils/logger.d.ts +2 -2
  157. package/utils/logger.js +1 -1
  158. package/utils/package-filter.d.ts +135 -8
  159. package/utils/package-filter.js +204 -16
  160. package/utils/printable-config.d.ts +9 -1
  161. package/utils/printable-config.js +46 -1
  162. package/utils/progress-panel.d.ts +9 -0
  163. package/utils/progress-panel.js +23 -1
  164. package/utils/release-version.d.ts +1 -1
  165. package/utils/run-bin.d.ts +10 -0
  166. package/utils/run-bin.js +1 -1
  167. package/utils/run-options.d.ts +97 -0
  168. package/utils/run-options.js +81 -0
  169. package/utils/version-group.d.ts +31 -0
  170. package/utils/version-group.js +43 -0
  171. package/utils/version-stamp.d.ts +1 -1
  172. package/utils/version-stamp.js +1 -1
  173. package/commands/changed.command.d.ts +0 -3
  174. package/commands/changed.command.js +0 -29
  175. package/core/config.d.ts +0 -409
  176. package/core/config.js +0 -939
  177. package/core/extends-config.d.ts +0 -27
  178. package/core/extends-config.js +0 -89
  179. package/core/manifest.js +0 -150
  180. package/core/merge-config.js +0 -219
  181. package/core/package.d.ts +0 -83
  182. package/core/package.js +0 -106
  183. package/core/plugin.d.ts +0 -112
  184. package/core/plugin.js +0 -189
  185. package/core/workspace.d.ts +0 -68
  186. package/core/workspace.js +0 -83
  187. /package/{core/run-step.js → builtins/platforms/node/augmentation/rmanrc.augmentation.js} +0 -0
@@ -0,0 +1,71 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ /**
4
+ * `npm view <name> version versions --json` - `undefined` for anything that is not an answer (never
5
+ * published, no network, private or restricted with no access). Catch-everything on purpose: every
6
+ * caller treats "no answer" as a legitimate state rather than a failure.
7
+ *
8
+ * **Two fields in one call, because the two questions are genuinely different and `publish` needs
9
+ * both.** Whether *this* version is already out there decides `up-to-date` vs `publish`, and only
10
+ * `versions` can answer it; what `latest` points at is what the plan *reports*, so a reader can see
11
+ * where the registry stands. Asking `latest` alone was the same approximation in both roles, and it
12
+ * is wrong in both directions: a prerelease on its own dist-tag never moves `latest`, so `publish`
13
+ * kept proposing an already-published version until npm answered 403; and a package whose local
14
+ * version is *behind* `latest` was proposed just as wrongly.
15
+ *
16
+ * **`versions` is normalized to an array defensively, and the reason is what happens if it is
17
+ * not.** `includes` exists on a string too, so a bare `"1.0.0"` would answer `versions.includes()`
18
+ * without any error at all - and answer it by substring, where `"1.0.10".includes("1.0.1")` is
19
+ * `true` (measured). npm's own `--json` output *is* a real array here on this npm (measured: a
20
+ * single-entry `maintainers` comes back as `[...]`, not unwrapped), so this guards a shape
21
+ * difference across npm versions rather than one I reproduced - but a silent wrong answer about
22
+ * whether a version is published is worth two lines.
23
+ */
24
+ export async function npmViewPackage(name, cwd, options = {}) {
25
+ const argv = ['view', name, 'version', 'versions', '--json'];
26
+ if (options.registry)
27
+ argv.push('--registry', options.registry);
28
+ if (options.userconfig)
29
+ argv.push('--userconfig', options.userconfig);
30
+ try {
31
+ const { stdout } = await execFileAsync('npm', argv, { cwd });
32
+ const text = stdout.trim();
33
+ if (!text)
34
+ return undefined;
35
+ const raw = JSON.parse(text);
36
+ return { latest: raw.version, versions: toVersionList(raw.versions) };
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ /**
43
+ * `npm view <name> version` - the **`latest` dist-tag's** version, or `undefined` for anything that
44
+ * is not an answer.
45
+ *
46
+ * `packageJsonManifest.publishedVersion` asks this so that `detectChangeHash` can guess a tag name
47
+ * for a package with no release tag at all, and `latest` is the right question for it: it wants the
48
+ * one version the ecosystem considers current, not the set of everything ever published. It passes
49
+ * no options - a bare `npm view` already picks up the repository's own `.npmrc` from `cwd`, which
50
+ * is what that path wants. `publish` asks `npmViewPackage` instead; see there for why.
51
+ */
52
+ export async function npmViewVersion(name, cwd, options = {}) {
53
+ const argv = ['view', name, 'version'];
54
+ if (options.registry)
55
+ argv.push('--registry', options.registry);
56
+ if (options.userconfig)
57
+ argv.push('--userconfig', options.userconfig);
58
+ try {
59
+ const { stdout } = await execFileAsync('npm', argv, { cwd });
60
+ return stdout.trim() || undefined;
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ }
66
+ function toVersionList(versions) {
67
+ if (Array.isArray(versions))
68
+ return versions;
69
+ return versions ? [versions] : [];
70
+ }
71
+ const execFileAsync = promisify(execFile);
package/cli.d.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ /** Every built-in command, registered by importing them - see `commands.ts` for why the list lives
3
+ * there rather than here. */
4
+ import './commands.js';
5
+ import { RmanApplication } from './core/application.js';
2
6
  export declare function runCli(options?: {
3
7
  argv?: string[];
4
8
  cwd?: string;
9
+ app?: RmanApplication;
10
+ /** Forwarded to {@link Repository.create} - see its `presets`. `[]` is a caller with an
11
+ * ecosystem of its own, which is what the core's own fixture is. */
12
+ presets?: readonly string[];
5
13
  }): Promise<void>;
package/cli.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ /** Every built-in command, registered by importing them - see `commands.ts` for why the list lives
3
+ * there rather than here. */
4
+ import './commands.js';
2
5
  import { realpathSync } from 'node:fs';
3
6
  import path from 'node:path';
4
7
  import { fileURLToPath } from 'node:url';
@@ -6,25 +9,17 @@ import colors from 'ansi-colors';
6
9
  import * as yaml from 'js-yaml';
7
10
  import yargs, {} from 'yargs';
8
11
  import { hideBin } from 'yargs/helpers';
9
- import * as buildCommand from './commands/build.command.js';
10
- import * as changedCommand from './commands/changed.command.js';
11
- import * as changelogCommand from './commands/changelog.command.js';
12
- import * as configCommand from './commands/config.command.js';
13
- import * as diffCommand from './commands/diff.command.js';
14
- import * as execCommand from './commands/exec.command.js';
15
- import * as githubReleaseCommand from './commands/github-release.command.js';
16
- import * as importCommand from './commands/import.command.js';
17
- import * as infoCommand from './commands/info.command.js';
18
- import * as listCommand from './commands/list.command.js';
19
- import * as runCommand from './commands/run.command.js';
20
- import * as testCommand from './commands/test.command.js';
21
- import * as versionCommand from './commands/version.command.js';
22
12
  import { version } from './constants.js';
23
- import { assertNoBuiltinShadowing, loadCustomCommands } from './core/custom-command.js';
24
- import { Repository } from './core/repository.js';
13
+ import { RmanApplication } from './core/application.js';
14
+ import { Repository } from './core/classes/repository.js';
15
+ import { commandName, toYargsCommand } from './core/command-builder.js';
16
+ import { assertNoBuiltinShadowing, defaultCommandGlobs, loadCustomCommands, } from './core/interfaces/custom-command.js';
17
+ import { checkCustomCommand } from './core/plugin-loader.js';
18
+ import { commandRegistry } from './interfaces/rman-config.interface.js';
19
+ import { colorYaml } from './utils/color-yaml.js';
25
20
  import { LOG_LEVELS, Logger, resolveRootLogLevel } from './utils/logger.js';
26
- import { filterPackages, readPackageFilterOptions, readRootOption } from './utils/package-filter.js';
27
- import { printableConfig } from './utils/printable-config.js';
21
+ import { filterPackages, readFromRootOption, readPackageFilterOptions } from './utils/package-filter.js';
22
+ import { printableConfig, withoutContributions } from './utils/printable-config.js';
28
23
  import { runBin } from './utils/run-bin.js';
29
24
  export async function runCli(options) {
30
25
  const _argv = options?.argv || hideBin(process.argv);
@@ -41,7 +36,10 @@ export async function runCli(options) {
41
36
  return;
42
37
  }
43
38
  try {
44
- const repository = await Repository.create(options?.cwd);
39
+ /** One application per run, made here so `--log-level` reaches its logger, and handed to
40
+ * `Repository.create` rather than found through a global. */
41
+ const app = options?.app ?? new RmanApplication();
42
+ const repository = await Repository.create(options?.cwd, { app, presets: options?.presets });
45
43
  const program = yargs(_argv)
46
44
  .scriptName('rman')
47
45
  .version(version)
@@ -81,7 +79,7 @@ export async function runCli(options) {
81
79
  * - but it still has to **throw**, not exit.
82
80
  *
83
81
  * This branch called `process.exit(1)`, and `runCli` is a library entry point: rman's own
84
- * bin calls it, so do `rman-node`'s fixtures and every spec. Exiting from in here took the
82
+ * bin calls it, so do the fixtures and every spec. Exiting from in here took the
85
83
  * whole process down before any caller could see the rejection - which in mocha meant the
86
84
  * first command that failed killed the run and the suite could not report a single result.
87
85
  * The exit belongs to the bin entry alone (see `isMain()` at the bottom), which already does
@@ -99,60 +97,94 @@ export async function runCli(options) {
99
97
  * quietly does nothing on whichever command forgot it is worse than no flag.
100
98
  */
101
99
  interceptConfigFlag(repository, program);
102
- infoCommand.initCli(repository, program);
103
- listCommand.initCli(repository, program);
104
- runCommand.initCli(repository, program);
105
- buildCommand.initCli(repository, program);
106
- changelogCommand.initCli(repository, program);
107
- testCommand.initCli(repository, program);
108
- versionCommand.initCli(repository, program);
109
- githubReleaseCommand.initCli(repository, program);
110
- execCommand.initCli(repository, program);
111
- changedCommand.initCli(repository, program);
112
- diffCommand.initCli(repository, program);
113
- configCommand.initCli(repository, program);
114
- importCommand.initCli(repository, program);
115
100
  /**
116
- * Commands that are not built in, from two places, registered after the built-ins so the clash
117
- * check below has the full list to compare against:
101
+ * **Built-ins come from `commandRegistry`, in the order their modules were imported.** Each
102
+ * entry is a register function that has not run yet - it runs here, with the repository, and
103
+ * returns the command's declaration; `toYargsCommand` is the only thing that knows how a
104
+ * declaration becomes a yargs registration.
118
105
  *
119
- * - **plugins** (`.rmanrc "plugins"`) - a *package* contributing commands, which is how
120
- * everything Node-specific lives outside rman's core;
121
- * - **`.rman/*.mjs`** - this one repository's own commands.
106
+ * The thirteen hand-written `initCli(repository, program)` calls this replaces were the second
107
+ * place a command had to be listed, and the list the shadow check guards with was a third.
108
+ */
109
+ const builtIns = commandRegistry.map(register => register(app));
110
+ for (const meta of builtIns)
111
+ program.command(toYargsCommand(meta));
112
+ /**
113
+ * Commands that are not built in, all from **one** key: `.rmanrc "commands"`, whose default
114
+ * value is `.rman/*.{js,mjs,cjs}`.
115
+ *
116
+ * An entry is a command or a glob naming modules that export one, so a package contributing
117
+ * commands (a built-in's, a plugin's) and a repository writing its own reach yargs by the same path - there
118
+ * is one source of non-built-in commands and one precedence slot. A plugin used to hand them
119
+ * over separately through `addCommand`, which is the step this replaces.
120
+ *
121
+ * Registered after the built-ins so the shadow check below has the full list to compare
122
+ * against. A command taking a *built-in's* name is refused outright.
123
+ */
124
+ const { globs, direct } = commandEntries(repository);
125
+ const { commands: loaded, errors } = await loadCustomCommands(globs);
126
+ /**
127
+ * The two authoring forms, resolved the same way - a declarative one's factory runs here,
128
+ * where `app.repository` exists, rather than when the config was read.
129
+ *
130
+ * **The file name is the fallback for `command`**, which is the convention a command loaded
131
+ * from a *file* has and one written straight into the config has not: `checkCustomCommand`
132
+ * refuses nameless metadata, and an inline command has nothing to fall back on, so it must say
133
+ * its own name. Spread *under* the factory's result, so metadata that does declare one -
134
+ * `deploy <stage>`, with its positionals - wins.
135
+ *
136
+ * The name comes from what the factory *returned*, not from the file: those differ exactly
137
+ * when the metadata declared one, and using the file name would leave the clash check
138
+ * comparing something yargs never registered.
139
+ */
140
+ const localModules = [...direct, ...loaded].map(c => {
141
+ if (!c.register)
142
+ return { name: c.name, file: c.file, module: toCustomModule(c.custom, repository, app) };
143
+ const declared = c.register(app);
144
+ const meta = checkCustomCommand({ ...declared, command: declared.command?.trim() || c.name }, c.file);
145
+ return { name: commandName(meta.command), file: c.file, module: toYargsCommand(meta) };
146
+ });
147
+ /**
148
+ * **One command per name, keeping the last** - which is the precedence that already applied,
149
+ * made visible instead of left to yargs.
122
150
  *
123
- * The repository wins a name clash with a plugin, and silently: it is the more specific
124
- * statement, the same way its own `.rmanrc` overrides an `extends` base. A plugin taking a
125
- * *built-in's* name is still refused outright.
151
+ * A repository's own `.rman/check.mjs` overriding a command its config contributed is the
152
+ * intended escape hatch, and it is deliberately not an error: the same precedence a package's
153
+ * own `.rmanrc` has over an `extends` base. But both were being *registered*, and what that
154
+ * cost was the help output - measured, `rman --help` listed `deploy` twice, once with each
155
+ * description, and nothing said which of the two would run.
156
+ *
157
+ * Only the listing was wrong; the override itself was already clean (`rman deploy --help`
158
+ * showed the winner's options alone, and the loser's flag was rejected), which is why this is
159
+ * a registration fix and not a change to how a clash resolves. Registered later wins because
160
+ * `loaded` follows `direct` - a `.rman/` file after a contributed command - and yargs took the
161
+ * last, so keeping the last keeps today's behaviour exactly.
162
+ *
163
+ * The survivor takes the *loser's* position in the list, since that is where the name was
164
+ * first seen. Help ordering only.
165
+ */
166
+ const byName = new Map();
167
+ for (const command of localModules)
168
+ byName.set(command.name, command);
169
+ const commands = [...byName.values()];
170
+ /**
171
+ * Said out loud, at `verbose`, because deduplicating silently is what would make this the
172
+ * trap it is warned about elsewhere: before, two rows at least hinted that something was
173
+ * doubled - after, the overridden command is simply absent, and "my plugin's command does
174
+ * nothing" has no thread to pull. Not a warning: an override is a correct thing to do, and a
175
+ * repository that does it on purpose should not be nagged on every invocation.
126
176
  */
127
- /** Already loaded: `Repository.create` had to, because a plugin's workspace provider is what
128
- * finds the packages. This is just what it brought back. */
129
- const pluginCommands = repository.pluginCommands;
130
- const { commands: localCommands, errors } = await loadCustomCommands(repository.dirname);
131
- const localNames = new Set(localCommands.map(c => c.name));
132
- const commands = [...pluginCommands.filter(c => !localNames.has(c.name)), ...localCommands];
133
- assertNoBuiltinShadowing(commands, BUILT_IN_COMMANDS);
134
- for (const custom of commands) {
135
- program.command({
136
- command: custom.command,
137
- describe: custom.describe,
138
- /** Forwarded, not dropped: this loop builds a *new* spec object, so anything the command
139
- * declared and is not copied here is silently lost - `--config` printed the whole config
140
- * for every plugin command until this line existed (measured, on `clean` and `ci`). */
141
- configKeys: custom.configKeys,
142
- builder: custom.builder ?? (y => y),
143
- handler: args => {
144
- /** Resolved per invocation, not once at registration: `--log-level` is only known now. */
145
- const logLevel = args.logLevel ?? resolveRootLogLevel(repository);
146
- const context = {
147
- repository,
148
- package: repository.currentPackage,
149
- runBin: (bin, argv, opts) => runBin(bin, argv, { cwd: repository.dirname, logLevel, ...opts }),
150
- logger: new Logger(logLevel),
151
- };
152
- return custom.handler(context, args);
153
- },
154
- });
177
+ if (commands.length < localModules.length) {
178
+ const logger = new Logger(argvLogLevel(_argv) ?? resolveRootLogLevel(repository));
179
+ const survivors = new Set(commands);
180
+ for (const lost of localModules.filter(c => !survivors.has(c))) {
181
+ const winner = byName.get(lost.name);
182
+ logger.verbose(`"${lost.name}" from ${lost.file} is overridden by ${winner.file}.`);
183
+ }
155
184
  }
185
+ assertNoBuiltinShadowing(commands, builtInNames(builtIns));
186
+ for (const { module } of commands)
187
+ program.command(module);
156
188
  /** Warned about, not thrown: one unparseable file must not take the other commands with it.
157
189
  * Loud enough not to be mistaken for success, and it names the file and the reason - "my
158
190
  * command isn't there" is otherwise a long afternoon. */
@@ -199,6 +231,56 @@ export async function runCli(options) {
199
231
  throw e;
200
232
  }
201
233
  }
234
+ /**
235
+ * Everything `.rmanrc "commands"` declares across the repository, split into the globs to load and
236
+ * the commands written straight into the config.
237
+ *
238
+ * **Collected from the root and from every package**, because `commands` is not a root-level key -
239
+ * a package's own `.rmanrc` may contribute one, and a glob was anchored to that file when it was
240
+ * read. The commands themselves are repository-wide; there is one command list, so a package
241
+ * declaring one is contributing it to the repository. (`plugins` and `publishTargets` cannot work
242
+ * this way: they are read before the packages exist, because a plugin is what finds them.)
243
+ *
244
+ * The cascade means a root-declared entry also appears in each package's resolved config, so the
245
+ * same one arrives many times over. Globs are de-duplicated here and by resolved *file* again in
246
+ * the loader - the second pass is the one that matters, since two different globs can name one
247
+ * file. Inline commands are de-duplicated by identity, which is what the cascade produces.
248
+ *
249
+ * The `.rman/` default is used only when nothing was declared anywhere. Declaring `commands` and
250
+ * still wanting `.rman/` scanned means naming it: the key appends to other layers, not to a
251
+ * built-in fallback, and a default that could never be turned off is not a default.
252
+ */
253
+ function commandEntries(repository) {
254
+ const globs = new Set();
255
+ const seen = new Set();
256
+ const direct = [];
257
+ for (const config of [repository.rootPackage.config, ...repository.getPackages().map(p => p.config)]) {
258
+ const value = config?.commands;
259
+ for (const entry of Array.isArray(value) ? value : value ? [value] : []) {
260
+ if (typeof entry === 'string') {
261
+ if (entry.trim())
262
+ globs.add(entry);
263
+ continue;
264
+ }
265
+ if (!entry || seen.has(entry))
266
+ continue;
267
+ seen.add(entry);
268
+ /** No file to fall back on, so the metadata has to name itself - `checkCustomCommand` says
269
+ * so when it does not. `"commands"` stands in for the file in that message. */
270
+ if (typeof entry === 'function') {
271
+ direct.push({ name: '', file: '"commands"', register: entry });
272
+ }
273
+ else {
274
+ /** `checkCustomCommand` is what refuses a nameless one, with the message that names the
275
+ * omission - reached here rather than at registration so an inline command is checked
276
+ * the same way a plugin's used to be. */
277
+ const custom = checkCustomCommand(entry, '"commands"');
278
+ direct.push({ name: commandName(custom.command), file: '"commands"', custom });
279
+ }
280
+ }
281
+ }
282
+ return { globs: globs.size ? [...globs] : defaultCommandGlobs(repository.dirname), direct };
283
+ }
202
284
  /**
203
285
  * Replaces `program.command` with a version that wraps every handler: with `--config` it prints
204
286
  * what the command would run with and returns, instead of running it.
@@ -257,9 +339,17 @@ function printCommandConfig(repository, spec, args) {
257
339
  if (!shown.some(p => p === repository.rootPackage))
258
340
  shown.push(repository.rootPackage);
259
341
  const config = {};
260
- for (const pkg of shown)
261
- config[pkg.name] = keys?.length ? pick(pkg.config, keys) : pkg.config;
262
- console.log(indent(yaml.dump(printableConfig(config), { noRefs: true, lineWidth: 100 }).trimEnd()));
342
+ /** `withoutContributions` for the same reason `rman config` uses it: `plugins`, `platforms`,
343
+ * `commands` and `publishTargets` are code, and a command's config section is about settings.
344
+ * Only the "in full" branch needs it - `pick` already narrows to the keys the command reads. */
345
+ for (const pkg of shown) {
346
+ config[pkg.name] = keys?.length ? pick(pkg.config, keys) : withoutContributions(pkg.config);
347
+ }
348
+ /** Coloured after indenting, not before: `indent` prefixes each line, and an escape sequence
349
+ * already sitting at the start of one is what it would be measuring. Gated on a terminal for the
350
+ * reason `rman config` documents - the section is YAML and has to survive being redirected. */
351
+ const body = indent(yaml.dump(printableConfig(config), { noRefs: true, lineWidth: 100 }).trimEnd());
352
+ console.log(process.stdout.isTTY ? colorYaml(body) : body);
263
353
  if (!targets.includes(repository.rootPackage)) {
264
354
  console.log(comment(`# "${repository.rootPackage.name}" is the root - listed because repo-wide keys are read there.`));
265
355
  }
@@ -279,10 +369,10 @@ function readOptions(args) {
279
369
  }
280
370
  /** The packages the command would act on - `filterPackages` with the command's own options, so
281
371
  * this is the same set the command will compute, `skip` included. Narrowed to the current package
282
- * for a command that scopes by directory, unless `--root` says otherwise. */
372
+ * for a command that scopes by directory, unless `--from-root` says otherwise. */
283
373
  function commandTargets(repository, args) {
284
374
  const current = repository.currentPackage;
285
- if (current && !readRootOption(args))
375
+ if (current && !readFromRootOption(args))
286
376
  return [current];
287
377
  return filterPackages(repository.getPackages(), readPackageFilterOptions(args));
288
378
  }
@@ -312,23 +402,68 @@ function isMain() {
312
402
  }
313
403
  if (isMain())
314
404
  runCli().catch(() => process.exit(1));
315
- /** Every name a built-in command answers to - what a `.rman/*.mjs` command may not take. Kept here
316
- * rather than read back out of yargs (which exposes no such list) and pinned by a test against the
317
- * `command:` strings in `src/commands/*.command.ts`, so adding a command can't quietly leave a
318
- * repository's own able to shadow it. */
319
- const BUILT_IN_COMMANDS = [
320
- 'build',
321
- 'changed',
322
- 'changelog',
323
- 'completion',
324
- 'config',
325
- 'diff',
326
- 'exec',
327
- 'github-release',
328
- 'import',
329
- 'info',
330
- 'list',
331
- 'run',
332
- 'test',
333
- 'version',
334
- ];
405
+ /**
406
+ * Every name a repository's own command may not take - **derived from what was actually
407
+ * registered**, not listed.
408
+ *
409
+ * It used to be a hand-maintained array, because yargs exposes no such list, and a spec had to pin
410
+ * it against the command sources so that adding a command could not quietly leave a repository's
411
+ * own able to shadow it. With the built-ins coming out of `commandRegistry` the list and the
412
+ * registrations cannot disagree: they are the same walk.
413
+ *
414
+ * Aliases count - `ls` is `list`, and shadowing it would be the same mistake. `completion` is
415
+ * yargs' own command rather than one of ours, so it is the one name still written here.
416
+ */
417
+ function builtInNames(metas) {
418
+ const names = metas.flatMap(meta => [commandName(meta.command), ...(meta.aliases ?? [])]);
419
+ return [...names, 'completion'];
420
+ }
421
+ /**
422
+ * A `CustomCommand` - a `.rman/*.mjs` command, or a plugin's written the older way - as the yargs
423
+ * registration it describes. `toYargsCommand` is the same function for a *declarative* command;
424
+ * this is the other authoring form, and both end at one `program.command`.
425
+ *
426
+ * **Every field has to be copied deliberately**, because this builds a new object: anything the
427
+ * command declared and this forgets is silently lost. `--config` printed the whole config for every
428
+ * plugin command until `configKeys` was on this list (measured, on `clean` and `ci`).
429
+ */
430
+ function toCustomModule(custom, repository, app) {
431
+ return {
432
+ command: custom.command,
433
+ describe: custom.describe,
434
+ configKeys: custom.configKeys,
435
+ builder: custom.builder ?? ((y) => y),
436
+ handler: (args) => {
437
+ /** Resolved per invocation, not once at registration: `--log-level` is only known now. */
438
+ const logLevel = args.logLevel ?? resolveRootLogLevel(repository);
439
+ const context = {
440
+ repository,
441
+ package: repository.currentPackage,
442
+ runBin: (bin, argv, opts) => runBin(bin, argv, { cwd: repository.dirname, logLevel, app, ...opts }),
443
+ logger: new Logger(logLevel),
444
+ };
445
+ return custom.handler(context, args);
446
+ },
447
+ };
448
+ }
449
+ /**
450
+ * `--log-level` read straight off argv, for a message printed **before** yargs parses anything.
451
+ *
452
+ * Commands are registered before `parseAsync` runs - registering them is what makes parsing
453
+ * possible - so a diagnostic emitted at registration time cannot come from `args.logLevel`.
454
+ * Measured: the override note in `runCli` was silent for `--log-level verbose` and appeared only
455
+ * when `.rmanrc` said so, which is the one spelling a reader would not reach for first.
456
+ *
457
+ * A peek, not a parser. Both spellings, first match wins, and a value that is not a level is left
458
+ * alone for yargs to reject in its own words - this must not become a second place where an
459
+ * invalid `--log-level` is diagnosed.
460
+ */
461
+ function argvLogLevel(argv) {
462
+ for (let i = 0; i < argv.length; i++) {
463
+ const arg = argv[i];
464
+ const value = arg === '--log-level' ? argv[i + 1] : arg.startsWith('--log-level=') ? arg.slice(12) : undefined;
465
+ if (value && LOG_LEVELS.includes(value))
466
+ return value;
467
+ }
468
+ return undefined;
469
+ }
@@ -1,3 +1,177 @@
1
- import type { Argv } from 'yargs';
2
- import type { Repository } from '../core/repository.js';
3
- export declare function initCli(repository: Repository, program: Argv): void;
1
+ import { type RmanConfig } from '../interfaces/rman-config.interface.js';
2
+ declare const COMMAND: "build";
3
+ declare const config: {
4
+ parallel: {
5
+ target: "cli";
6
+ describe: string;
7
+ coerce: (v: unknown) => boolean | number | undefined;
8
+ };
9
+ bail: {
10
+ target: "cli";
11
+ describe: string;
12
+ type: "boolean";
13
+ };
14
+ topo: {
15
+ target: "cli";
16
+ describe: string;
17
+ type: "boolean";
18
+ };
19
+ progress: {
20
+ target: "cli";
21
+ describe: string;
22
+ type: "boolean";
23
+ };
24
+ changed: {
25
+ target: "cli";
26
+ alias: string;
27
+ describe: string;
28
+ type: "boolean";
29
+ };
30
+ changedSince: {
31
+ target: "cli";
32
+ cliName: string;
33
+ describe: string;
34
+ type: "string";
35
+ conflicts: string;
36
+ };
37
+ fromRoot: {
38
+ target: "cli";
39
+ cliName: string;
40
+ alias: string;
41
+ describe: string;
42
+ type: "boolean";
43
+ };
44
+ allowBranch: {
45
+ target: "cli";
46
+ cliName: string;
47
+ describe: string;
48
+ type: "string";
49
+ };
50
+ ignoreBranch: {
51
+ target: "cli";
52
+ cliName: string;
53
+ describe: string;
54
+ type: "string";
55
+ };
56
+ scope: {
57
+ target: "cli";
58
+ describe: string;
59
+ type: "string";
60
+ };
61
+ ignore: {
62
+ target: "cli";
63
+ describe: string;
64
+ type: "string";
65
+ };
66
+ platform: {
67
+ target: "cli";
68
+ describe: string;
69
+ type: "string";
70
+ };
71
+ deps: {
72
+ target: "cli";
73
+ describe: string;
74
+ type: "boolean";
75
+ };
76
+ dependents: {
77
+ target: "cli";
78
+ describe: string;
79
+ type: "boolean";
80
+ };
81
+ };
82
+ type Args = RmanConfig.ArgsOf<typeof config, typeof COMMAND>;
83
+ declare const buildCommand: (app: import("../index.js").RmanApplication) => {
84
+ command: "build";
85
+ describe: string;
86
+ /**
87
+ * Read, not owned - and it owns nothing at all. `build` is `run build` under another name, so its
88
+ * settings live in `run.build`, which belongs to `run`. Two commands cannot contribute under one
89
+ * top-level key (interface merging is not a deep merge), and this is why they never needed to.
90
+ */
91
+ configKeys: string[];
92
+ config: {
93
+ parallel: {
94
+ target: "cli";
95
+ describe: string;
96
+ coerce: (v: unknown) => boolean | number | undefined;
97
+ };
98
+ bail: {
99
+ target: "cli";
100
+ describe: string;
101
+ type: "boolean";
102
+ };
103
+ topo: {
104
+ target: "cli";
105
+ describe: string;
106
+ type: "boolean";
107
+ };
108
+ progress: {
109
+ target: "cli";
110
+ describe: string;
111
+ type: "boolean";
112
+ };
113
+ changed: {
114
+ target: "cli";
115
+ alias: string;
116
+ describe: string;
117
+ type: "boolean";
118
+ };
119
+ changedSince: {
120
+ target: "cli";
121
+ cliName: string;
122
+ describe: string;
123
+ type: "string";
124
+ conflicts: string;
125
+ };
126
+ fromRoot: {
127
+ target: "cli";
128
+ cliName: string;
129
+ alias: string;
130
+ describe: string;
131
+ type: "boolean";
132
+ };
133
+ allowBranch: {
134
+ target: "cli";
135
+ cliName: string;
136
+ describe: string;
137
+ type: "string";
138
+ };
139
+ ignoreBranch: {
140
+ target: "cli";
141
+ cliName: string;
142
+ describe: string;
143
+ type: "string";
144
+ };
145
+ scope: {
146
+ target: "cli";
147
+ describe: string;
148
+ type: "string";
149
+ };
150
+ ignore: {
151
+ target: "cli";
152
+ describe: string;
153
+ type: "string";
154
+ };
155
+ platform: {
156
+ target: "cli";
157
+ describe: string;
158
+ type: "string";
159
+ };
160
+ deps: {
161
+ target: "cli";
162
+ describe: string;
163
+ type: "boolean";
164
+ };
165
+ dependents: {
166
+ target: "cli";
167
+ describe: string;
168
+ type: "boolean";
169
+ };
170
+ };
171
+ examples: {
172
+ command: string;
173
+ description: string;
174
+ }[];
175
+ handler: (args: Args) => Promise<void>;
176
+ };
177
+ export default buildCommand;