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
@@ -1,11 +1,118 @@
1
1
  import readline from 'node:readline/promises';
2
2
  import colors from 'ansi-colors';
3
3
  import EasyTable from 'easy-table';
4
- import { VersionService } from '../services/version.service.js';
4
+ import { registerCommand } from '../interfaces/rman-config.interface.js';
5
5
  import { VersionPlanService } from '../services/version-plan.service.js';
6
- import { applyBranchGuardOptions, assertAllowedBranch, readBranchGuardOptions } from '../utils/branch-guard.js';
7
- import { applyPackageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
8
- export function initCli(repository, program) {
6
+ import { assertAllowedBranch, branchGuardOptions, readBranchGuardOptions } from '../utils/branch-guard.js';
7
+ import { packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
8
+ /**
9
+ * Hoisted out of the metadata literal so the handler can be annotated against them - see
10
+ * `RmanConfig.ArgsOf` for why an inferred `argv` and the metadata's own typo checking cannot both
11
+ * work in one signature.
12
+ *
13
+ * `as const` on the command string is load-bearing twice over: the config key is derived from it
14
+ * (`'version'`), and so are the positional names it declares.
15
+ */
16
+ const COMMAND = 'version [bump]';
17
+ const config = {
18
+ /** The two shared groups, spread rather than applied - this is what
19
+ * `applyBranchGuardOptions(applyPackageFilterOptions(cmd))` used to say. Spread first, so an
20
+ * option deliberately overridden below wins. */
21
+ ...packageFilterOptions,
22
+ ...branchGuardOptions,
23
+ interactive: {
24
+ target: 'cli',
25
+ alias: 'i',
26
+ describe: 'Show the plan and ask for confirmation before applying (with or without an explicit bump)',
27
+ type: 'boolean',
28
+ },
29
+ show: {
30
+ target: 'cli',
31
+ describe: 'Show the resulting plan for the given bump without applying it - unlike omitting bump ' +
32
+ 'entirely, this still uses the given bump keyword/version to compute the plan, ' +
33
+ 'just never writes it.',
34
+ type: 'boolean',
35
+ conflicts: 'interactive',
36
+ },
37
+ yes: {
38
+ target: 'cli',
39
+ alias: 'y',
40
+ describe: 'Skip the confirmation prompt and apply the computed plan immediately - an auto-detected ' +
41
+ 'bump included, no explicit bump keyword required (same idea as "publish --yes").',
42
+ type: 'boolean',
43
+ conflicts: 'interactive',
44
+ },
45
+ json: {
46
+ target: 'cli',
47
+ alias: 'j',
48
+ describe: 'Print the plan as JSON and write nothing - the machine-readable form of --show, and what ' +
49
+ 'the removed "changed" command was for.',
50
+ type: 'boolean',
51
+ conflicts: ['interactive', 'yes'],
52
+ },
53
+ ignoreDirty: {
54
+ target: 'cli',
55
+ cliName: 'ignore-dirty',
56
+ describe: 'Exclude a package with uncommitted local changes instead of aborting the whole run',
57
+ type: 'boolean',
58
+ },
59
+ push: {
60
+ target: 'cli',
61
+ describe: 'Push the resulting commit(s) and tag(s) to the remote once applied',
62
+ type: 'boolean',
63
+ },
64
+ message: {
65
+ target: 'cli',
66
+ alias: 'm',
67
+ describe: 'Override the commit message for every group this run commits (default: .rmanrc ' +
68
+ 'version.commitMessage, or "chore(release): v{version}") - "{version}" is substituted ' +
69
+ "when a commit's own group shares one version.",
70
+ type: 'string',
71
+ },
72
+ changelog: {
73
+ target: 'both',
74
+ describe: "Also write each bumped package's CHANGELOG.md (same as running changelog --write " +
75
+ 'separately) and fold it into the same commit as its version bump. Default: .rmanrc ' +
76
+ '"version.changelog", or false - --no-changelog forces it off even when that\'s true.',
77
+ type: 'boolean',
78
+ },
79
+ preid: {
80
+ target: 'cli',
81
+ describe: 'Make the bump a prerelease with this identifier (e.g. "beta" -> 1.2.3-beta.0). ' +
82
+ 'Running again with the same --preid increments it (-> 1.2.3-beta.1); a different ' +
83
+ 'identifier starts a fresh prerelease line. Ignored when bump is an explicit version.',
84
+ type: 'string',
85
+ },
86
+ /**
87
+ * Config-only, from here down: `.rmanrc "version.*"` keys with no reason to be a flag.
88
+ * `--message` is `commitMessage`'s flag and is declared above; the rest are settings a repository
89
+ * states once, not things a single run overrides.
90
+ */
91
+ commitMessage: {
92
+ target: 'config',
93
+ describe: 'The commit message for each group this run commits - "{version}" is substituted when a ' +
94
+ 'commit\'s own group shares one version. Default "chore(release): v{version}".',
95
+ type: 'string',
96
+ },
97
+ releaseTagPattern: {
98
+ target: 'config',
99
+ describe: "Tag naming the repository's own release, as opposed to the per-package tags " +
100
+ '"changelog.tagPattern" names - only created when the root is on a calendar version. ' +
101
+ 'Root-level only. Default "release-*". Must **not** match any package\'s own tag pattern, or ' +
102
+ "that package's changelog boundary resolves to the repository release instead of its own.",
103
+ type: 'string',
104
+ },
105
+ stampDockerfile: {
106
+ target: 'config',
107
+ describe: "Keep this package's Dockerfile org.opencontainers.image.version label in step with the " +
108
+ 'version being written. Per-package cascaded. Default true - the label is by specification ' +
109
+ 'the version of the packaged software, so there is only one correct value and "version" is ' +
110
+ 'what knows it. Only ever rewrites a label the Dockerfile already declares.',
111
+ type: 'boolean',
112
+ },
113
+ };
114
+ const versionCommand = registerCommand(app => {
115
+ const repository = app.repository;
9
116
  /** `rman version <bump|version>` is validated by the root's scheme (see `getPlan`), so the help
10
117
  * has to name that scheme's own words rather than semver's - otherwise `--help` in a repository
11
118
  * numbering some other way documents keywords its own planner would reject. A four-part scheme
@@ -15,77 +122,89 @@ export function initCli(repository, program) {
15
122
  /** For the examples. Not `smallestBump`, which throws: `--help` must still render for a scheme
16
123
  * that declares none, and the placeholder says what to write there. */
17
124
  const smallest = bumpNames[0] ?? '<bump>';
18
- program.command({
19
- command: 'version [bump]',
20
- /** `changelog` too: `--changelog` folds one into the bump commit, and `group` is what decides
21
- * which packages move together. */
22
- configKeys: ['version', 'changelog', 'group'],
125
+ return {
126
+ /** `as const` so the config key stays derivable from it: `CommandMetadata['command']` is a
127
+ * plain `string`, so without the assertion inference widens `'version [bump]'` and there is
128
+ * nothing left to read `'version'` out of. */
129
+ command: COMMAND,
23
130
  describe: 'Bumps versions of changed packages (and their dependents), grouped via .rmanrc "group"',
24
- builder: cmd => applyBranchGuardOptions(applyPackageFilterOptions(cmd))
25
- .example(`$0 version ${smallest}`, `# Bump ${smallest} directly, applied immediately`)
26
- .example('$0 version', "# Auto-detect the bump from commits, show the plan, don't write anything")
27
- .example('$0 version --interactive', '# Show the plan either way, then ask for confirmation')
28
- .example(`$0 version ${smallest} --show`, `# Preview what an explicit ${smallest} would do, without applying it`)
29
- .positional('bump', {
30
- describe: `A bump keyword (${bumps}) or an explicit ${scheme} version. ` +
31
- 'Omit to auto-detect from commits and only preview the plan.',
32
- type: 'string',
33
- })
34
- .option('interactive', {
35
- alias: 'i',
36
- describe: 'Show the plan and ask for confirmation before applying (with or without an explicit bump)',
37
- type: 'boolean',
38
- })
39
- .option('show', {
40
- describe: 'Show the resulting plan for the given bump without applying it - unlike omitting bump ' +
41
- 'entirely, this still uses the given bump keyword/version to compute the plan, ' +
42
- 'just never writes it.',
43
- type: 'boolean',
44
- })
45
- .conflicts('show', 'interactive')
46
- .option('yes', {
47
- alias: 'y',
48
- describe: 'Skip the confirmation prompt and apply the computed plan immediately - an auto-detected ' +
49
- 'bump included, no explicit bump keyword required (same idea as "publish --yes").',
50
- type: 'boolean',
51
- })
52
- .conflicts('yes', 'interactive')
53
- .option('ignore-dirty', {
54
- describe: 'Exclude a package with uncommitted local changes instead of aborting the whole run',
55
- type: 'boolean',
56
- })
57
- .option('push', {
58
- describe: 'Push the resulting commit(s) and tag(s) to the remote once applied',
59
- type: 'boolean',
60
- })
61
- .option('message', {
62
- alias: 'm',
63
- describe: 'Override the commit message for every group this run commits (default: .rmanrc ' +
64
- 'version.commitMessage, or "chore(release): v{version}") - "{version}" is substituted ' +
65
- "when a commit's own group shares one version.",
66
- type: 'string',
67
- })
68
- .option('changelog', {
69
- describe: "Also write each bumped package's CHANGELOG.md (same as running changelog --write " +
70
- 'separately) and fold it into the same commit as its version bump. Default: .rmanrc ' +
71
- '"version.changelog", or false - --no-changelog forces it off even when that\'s true.',
72
- type: 'boolean',
73
- })
74
- .option('preid', {
75
- describe: 'Make the bump a prerelease with this identifier (e.g. "beta" -> 1.2.3-beta.0). ' +
76
- 'Running again with the same --preid increments it (-> 1.2.3-beta.1); a different ' +
77
- `identifier starts a fresh prerelease line. Ignored when bump is an explicit ${scheme} version.`,
78
- type: 'string',
79
- }),
131
+ /**
132
+ * Read, not owned - so they are named here rather than declared in `config`. `group` is a core
133
+ * per-package key that decides which packages move together, `changelog.*` is consulted when
134
+ * `--changelog` folds one into the bump commit, and `allowBranch`/`ignoreBranch` are repo-wide
135
+ * keys the shared branch-guard group only exposes as flags. `version` itself is not listed: it
136
+ * is this command's own key, derived from `command`.
137
+ */
138
+ configKeys: ['changelog', 'group', 'allowBranch', 'ignoreBranch'],
139
+ config,
140
+ examples: [
141
+ {
142
+ command: `$0 version ${smallest} --show`,
143
+ description: `# Bump ${smallest} directly, applied immediately`,
144
+ },
145
+ {
146
+ command: '$0 version',
147
+ description: "# Auto-detect the bump from commits, show the plan, don't write anything",
148
+ },
149
+ {
150
+ command: '$0 version --interactive',
151
+ description: '# Show the plan either way, then ask for confirmation',
152
+ },
153
+ {
154
+ command: `$0 version ${smallest} --show`,
155
+ description: `# Preview what an explicit ${smallest} would do, without applying it`,
156
+ },
157
+ ],
158
+ positionals: {
159
+ bump: {
160
+ describe: `A bump keyword (${bumps}) or an explicit ${scheme} version. ` +
161
+ 'Omit to auto-detect from commits and only preview the plan.',
162
+ type: 'string',
163
+ },
164
+ },
165
+ /** Annotated rather than inferred - `RmanConfig.ArgsOf` records why, and the casts this
166
+ * replaces are the reason it is worth two hoisted declarations. */
80
167
  handler: async (args) => {
81
168
  await assertAllowedBranch(repository, readBranchGuardOptions(args));
82
169
  const bump = args.bump;
83
- const plan = await VersionPlanService.getPlanner().getPlan(repository, {
170
+ const plan = await VersionPlanService.getPlanner(app).getPlan(repository, {
84
171
  ...readPackageFilterOptions(args),
85
172
  bump,
86
173
  ignoreDirty: args.ignoreDirty,
87
174
  preid: args.preid,
88
175
  });
176
+ /**
177
+ * **The machine-readable plan, and the whole of it.** This replaced `rman changed`, which
178
+ * was `getPlan` filtered to `status === 'bump'` - and that one-line filter produced two
179
+ * silent wrongs at once, measured together on a dirty tree: the entry it returned was the
180
+ * repository **root** (`buildRootEntry` reports `'bump'`, and the fact that it is
181
+ * informational lived only in `reason`), while `pkg-a`, the package that had actually
182
+ * changed, was dropped because a package with uncommitted changes is `'error'`. A CI script
183
+ * reading that array saw one name to release and it was the one name that must never be
184
+ * published.
185
+ *
186
+ * So nothing is filtered here. Every entry carries its own `status`, and `isRoot` is stated
187
+ * rather than left to be inferred from `group === "root"` - a consumer selects what it wants
188
+ * and can see what it is leaving out. Printed **before** the table and before the
189
+ * dirty-package throw, so stdout holds one JSON document and nothing else.
190
+ *
191
+ * **Exit 0 either way, deliberately.** `--show` exits 1 on a dirty package because a person
192
+ * needs stopping; here the same fact is in the data, and overloading the exit code would
193
+ * make a pipeline bail before it could read the very rows that explain why.
194
+ */
195
+ if (args.json) {
196
+ console.log(JSON.stringify(plan.map(e => ({
197
+ name: e.package.name,
198
+ selector: e.package.selector,
199
+ isRoot: e.package.isRoot,
200
+ group: e.group,
201
+ status: e.status,
202
+ from: e.from,
203
+ to: e.to,
204
+ reason: e.reason,
205
+ })), undefined, 2));
206
+ return;
207
+ }
89
208
  printPlan(plan);
90
209
  const errors = plan.filter(e => e.status === 'error');
91
210
  if (errors.length) {
@@ -115,15 +234,16 @@ export function initCli(repository, program) {
115
234
  if (!apply)
116
235
  return;
117
236
  const changelog = args.changelog ?? repository.config?.version?.changelog ?? false;
118
- const applied = await VersionService.applyPlan(repository, plan, {
237
+ const applied = await app.getService('version').applyPlan(plan, {
119
238
  push: args.push,
120
239
  message: args.message,
121
240
  changelog,
122
241
  });
123
242
  printApplied(applied);
124
243
  },
125
- });
126
- }
244
+ };
245
+ });
246
+ export default versionCommand;
127
247
  /**
128
248
  * What the run *did* - which is deliberately not the table again.
129
249
  *
@@ -156,18 +276,112 @@ function printApplied(result) {
156
276
  }
157
277
  function printPlan(entries) {
158
278
  const table = new EasyTable();
159
- for (const e of entries) {
160
- table.cell('Status', statusLabel(e.status));
161
- table.cell('Package', colors.cyan(e.package.name));
162
- table.cell('Group', colors.gray(`(${e.group})`));
163
- table.cell('From', e.from);
164
- table.cell('', e.status === 'bump' ? '->' : '');
165
- table.cell('To', e.status === 'bump' ? colors.yellow(e.to) : '');
166
- table.cell('Reason', e.status === 'error' ? colors.red(e.reason ?? '') : colors.gray(e.reason ?? ''));
167
- table.newRow();
279
+ let first = true;
280
+ for (const { members, shared } of planBlocks(entries)) {
281
+ if (!first)
282
+ table.pushDelimeter(PLAN_COLUMNS);
283
+ first = false;
284
+ for (const e of members)
285
+ printPlanRow(table, e, shared);
168
286
  }
169
287
  console.log(table.toString().trim());
170
288
  }
289
+ /**
290
+ * The plan, split into the blocks a delimiter is drawn between: the **repository root** first, then
291
+ * every shared version line with its members together, then the packages that share a line with
292
+ * nobody.
293
+ *
294
+ * `getPlan` returns entries in *package* order, which is the order the workspace found them in -
295
+ * so two members of one group are adjacent only by luck. That matters more than it looks: a group
296
+ * releases as one version line, so "these four numbers move together" is the single fact the table
297
+ * exists to convey, and scattering the members leaves the `Group` column as the only thing saying
298
+ * so. Read top to bottom instead, each block now *is* a release.
299
+ *
300
+ * **The root is first, and it is the one row that is not a release.** Its number is the
301
+ * repository's identity - what the GitHub Release is named after, and on a calendar version a
302
+ * number no package shares - so it is the heading the rest of the table sits under rather than a
303
+ * footnote to it. `buildRootEntry` appends it, which is why it is pulled out by hand here; leaving
304
+ * it where the plan put it made it the last singleton, indistinguishable from a package that
305
+ * happens to be grouped with no one.
306
+ *
307
+ * **Solo packages are one block, not one block each.** `group: false` makes every package its own
308
+ * group of one, so a delimiter per group would draw a line between every row in exactly the
309
+ * repository that has nothing to group - noise standing in for structure. They share a block
310
+ * because what they have in common is real: none of them is tied to anyone else.
311
+ *
312
+ * `shared` travels with each block because it is also what decides whether the `Group` column is
313
+ * written at all - see `printPlanRow`. One partition, asked once, answering both questions; working
314
+ * it out again at the cell would be a second rule that could disagree with the blocks beside it.
315
+ */
316
+ function planBlocks(entries) {
317
+ /**
318
+ * `package.isRoot` is by **directory**, which is the only reliable test - `Repository extends
319
+ * Package` while holding a separate `rootPackage` instance for the same directory, so an identity
320
+ * check answers `false` for one of the two objects that are both the root.
321
+ *
322
+ * In a single-package repository that one package *is* the root and `getPlan` pushes no root
323
+ * entry of its own, so it lands here and prints alone - one row, and no delimiter to draw.
324
+ */
325
+ const root = entries.filter(e => e.package.isRoot);
326
+ const members = entries.filter(e => !e.package.isRoot);
327
+ const byGroup = new Map();
328
+ for (const e of members) {
329
+ const group = byGroup.get(e.groupKey);
330
+ if (group)
331
+ group.push(e);
332
+ else
333
+ byGroup.set(e.groupKey, [e]);
334
+ }
335
+ const shared = [];
336
+ const solo = [];
337
+ for (const group of byGroup.values()) {
338
+ if (group.length > 1)
339
+ shared.push({ members: group, shared: true });
340
+ else
341
+ solo.push(group[0]);
342
+ }
343
+ return [
344
+ ...(root.length ? [{ members: root, shared: false }] : []),
345
+ ...shared,
346
+ ...(solo.length ? [{ members: solo, shared: false }] : []),
347
+ ];
348
+ }
349
+ /** `buildRootEntry`'s display label. Named here because `printPlanRow` has to recognize the one
350
+ * singleton whose group is worth printing, and a bare `'root'` in a condition reads as a guess. */
351
+ const ROOT_GROUP = 'root';
352
+ /** The column names in the order `printPlanRow` writes them - `pushDelimeter` has to be handed the
353
+ * same set, or the dashes appear under a column that does not exist yet. */
354
+ const PLAN_COLUMNS = ['Status', 'Package', 'Group', 'From', '', 'To', 'Reason'];
355
+ function printPlanRow(table, e, shared) {
356
+ table.cell('Status', statusLabel(e.status));
357
+ table.cell('Package', colors.cyan(e.package.name));
358
+ /**
359
+ * **Blank for a package that shares its version line with nobody.** `resolveGroupKey` gives an
360
+ * ungrouped package a group of one named after the package, so the cell printed the Package
361
+ * column again, one column to the right - a word that looks like information and carries none.
362
+ * Worse, it made `(default)` and `(@panates/tsconfig)` read as the same *kind* of answer when one
363
+ * names a line four packages move along and the other names nothing at all.
364
+ *
365
+ * The root keeps its label: `root` is not its package name but what the row *is*, and it is the
366
+ * only thing on the line saying that the number below is the repository's identity rather than a
367
+ * release. So the test is `shared`, plus that one entry - not "does this string repeat the name",
368
+ * which would blank a genuine group that happened to be called after one of its members.
369
+ */
370
+ table.cell('Group', shared || e.group === ROOT_GROUP ? colors.gray(`(${e.group})`) : '');
371
+ table.cell('From', e.from);
372
+ /**
373
+ * Gated on `to`, not on `status === 'bump'`, which is what kept the column blank on the one
374
+ * command whose whole job is to fill it: a dirty package aborts the run, and "you cannot
375
+ * release this" is only half of what the reader came for - the other half is which version it
376
+ * would have got. `getPlan` sets `to` on exactly the entries that earned one, so asking whether
377
+ * there is a version to show is the honest test. Dimmed rather than yellow when the entry is
378
+ * not a bump, because that number is a hypothetical and nothing is going to write it.
379
+ */
380
+ table.cell('', e.to ? '->' : '');
381
+ table.cell('To', e.to ? (e.status === 'bump' ? colors.yellow(e.to) : colors.gray(e.to)) : '');
382
+ table.cell('Reason', e.status === 'error' ? colors.red(e.reason ?? '') : colors.gray(e.reason ?? ''));
383
+ table.newRow();
384
+ }
171
385
  function statusLabel(status) {
172
386
  switch (status) {
173
387
  case 'bump':
package/commands.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * **Every built-in command, and the one place the list lives.**
3
+ *
4
+ * Imported for their side effect: each module calls `registerCommand`, which pushes its register
5
+ * function onto `commandRegistry`. Nothing reads the modules' exports - `runCli` walks the registry
6
+ * - so this import list *is* the built-in command list.
7
+ *
8
+ * **`index.ts` imports this too, and that is not tidiness.** Each module also carries a
9
+ * `declare module` augmentation contributing its own `.rmanrc` keys, and a type augmentation only
10
+ * applies where the module declaring it is part of the program. Reached only from `cli.ts`, those
11
+ * keys existed for rman itself and for nobody else: `rman-node` reading `pkg.config.publish` got
12
+ * `Property 'publish' does not exist on type 'RmanConfig'` (measured, the moment the keys stopped
13
+ * being hand-written centrally, in what is now `rman-config.interface.ts`).
14
+ */
15
+ import './commands/build.command.js';
16
+ import './commands/changelog.command.js';
17
+ import './commands/config.command.js';
18
+ import './commands/diff.command.js';
19
+ import './commands/exec.command.js';
20
+ import './commands/github-release.command.js';
21
+ import './commands/import.command.js';
22
+ import './commands/info.command.js';
23
+ import './commands/lint.command.js';
24
+ import './commands/list.command.js';
25
+ import './commands/publish.command.js';
26
+ import './commands/run.command.js';
27
+ import './commands/test.command.js';
28
+ import './commands/version.command.js';
29
+ /** The config shapes a command declares by hand, where an option cannot describe them - see
30
+ * `RmanConfig.CommandContribution`'s `Extra`. Exported so a plugin can name one. */
31
+ export type { PublishExtraKeys, PublishTargetConfigs } from './commands/publish.command.js';
32
+ export type { VersionExtraKeys, VersionStampEntry } from './commands/version.command.js';
package/commands.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * **Every built-in command, and the one place the list lives.**
3
+ *
4
+ * Imported for their side effect: each module calls `registerCommand`, which pushes its register
5
+ * function onto `commandRegistry`. Nothing reads the modules' exports - `runCli` walks the registry
6
+ * - so this import list *is* the built-in command list.
7
+ *
8
+ * **`index.ts` imports this too, and that is not tidiness.** Each module also carries a
9
+ * `declare module` augmentation contributing its own `.rmanrc` keys, and a type augmentation only
10
+ * applies where the module declaring it is part of the program. Reached only from `cli.ts`, those
11
+ * keys existed for rman itself and for nobody else: `rman-node` reading `pkg.config.publish` got
12
+ * `Property 'publish' does not exist on type 'RmanConfig'` (measured, the moment the keys stopped
13
+ * being hand-written centrally, in what is now `rman-config.interface.ts`).
14
+ */
15
+ import './commands/build.command.js';
16
+ import './commands/changelog.command.js';
17
+ import './commands/config.command.js';
18
+ import './commands/diff.command.js';
19
+ import './commands/exec.command.js';
20
+ import './commands/github-release.command.js';
21
+ import './commands/import.command.js';
22
+ import './commands/info.command.js';
23
+ import './commands/lint.command.js';
24
+ import './commands/list.command.js';
25
+ import './commands/publish.command.js';
26
+ import './commands/run.command.js';
27
+ import './commands/test.command.js';
28
+ import './commands/version.command.js';
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '1.3.0';
1
+ export const version = '2.0.0-beta.3';
@@ -0,0 +1,116 @@
1
+ import type { VersionPlanService } from '../services/version-plan.service.js';
2
+ import { Logger, type LogLevel } from '../utils/logger.js';
3
+ import { Registry } from './classes/registry.js';
4
+ import type { Repository } from './classes/repository.js';
5
+ import type { ServiceFactory, ServiceMap } from './classes/service.js';
6
+ import { type Platform, type Plugin } from './interfaces/plugin.js';
7
+ import type { PublishTarget } from './interfaces/publish-target.js';
8
+ /**
9
+ * **One rman invocation, and everything it holds.** Created before anything else, handed to every
10
+ * plugin's `init`, and the owner of every registry and service that used to be a module-level
11
+ * variable.
12
+ *
13
+ * The point is that nothing is process-global any more. `Manifest`, `Workspace`, `BinPath`,
14
+ * `RunService` and `VersionPlanService` each kept their contributions in a module-scope array, so
15
+ * two repositories in one process shared them - which the test suite could only survive with a
16
+ * root hook emptying five of them before every test, and the failure that hook prevented was:
17
+ * whichever spec ran first decided the answer for the rest, and the core appeared to work in tests
18
+ * that had registered nothing. An application starts empty and is thrown away whole, so the hook
19
+ * is gone.
20
+ */
21
+ export declare class RmanApplication {
22
+ /**
23
+ * The **technologies** this invocation knows about, in `plugins` declaration order.
24
+ *
25
+ * One registry rather than the four it replaces (`manifest`, `workspace`, `binPaths`,
26
+ * `runSteps`): a technology is a whole, and declaring part of one was never meaningful - see
27
+ * `Platform`.
28
+ *
29
+ * **Flattened out of the plugins**, because this is what every seam actually iterates - a
30
+ * manifest is read by a platform, a workspace is laid out by a platform, a PATH is contributed by
31
+ * a platform. A plugin providing two of them puts two entries here, and a plugin providing none
32
+ * puts none.
33
+ */
34
+ readonly platforms: Registry<Platform>;
35
+ /**
36
+ * The **plugins** themselves, in declaration order - what a config named, before the platforms
37
+ * were taken out of them.
38
+ *
39
+ * Kept apart from `platforms` because the two answer different questions: this is who contributed,
40
+ * that is what they contributed. `init` runs per plugin; nothing else reads this.
41
+ */
42
+ readonly plugins: Registry<Plugin>;
43
+ /**
44
+ * Where a package's artifact can ship, in registration order.
45
+ *
46
+ * A registry rather than a field because the answer is a *sum*: a package may ship to npm and
47
+ * Docker Hub at once, and `publish` runs every target a package declares. The core contributes
48
+ * `docker` (nobody's ecosystem), the `node` built-in contributes `npm`, and a plugin for any other
49
+ * technology adds its own without either of them changing - see `PublishTarget`.
50
+ */
51
+ readonly publishTargets: Registry<PublishTarget>;
52
+ /**
53
+ * **The planner that orchestrates a run**, not the one that answers for each package.
54
+ *
55
+ * One answer, not a sum - so a field rather than a registry, and last registration wins. What it
56
+ * decides is the shape of the whole plan: groups, the commit→size reading, the cross-group ripple
57
+ * and the root's release identity, none of which belongs to any one technology.
58
+ *
59
+ * The two decisions that *are* a technology's - `detectBoundary` and `cascade` - are asked of
60
+ * each package's own `Platform.versionPlanner` instead (`VersionPlanService.plannerFor`), so a
61
+ * polyglot repository no longer resolves both through whichever plugin registered last.
62
+ */
63
+ versionPlanner?: VersionPlanService;
64
+ readonly logger: Logger;
65
+ /** `info` until `--log-level` or `.rmanrc "logLevel"` is resolved - which cannot happen here,
66
+ * since reading the config is itself work the application does. */
67
+ constructor(options?: {
68
+ logLevel?: LogLevel;
69
+ });
70
+ /** Which platform claims a directory - the first whose manifest provider recognizes it, because
71
+ * before a package is read there is nothing else to go on. `basePlatform` when none does, so the
72
+ * caller needs no guard. */
73
+ platformFor(dir: string): Platform;
74
+ /**
75
+ * **Not a constructor field, and that is forced by the order things happen in.** Plugins are what
76
+ * *find* the packages - `Repository.create` loads them before it calls `Workspace.resolve` - so
77
+ * the application has to exist, and be handed to every `init`, while there is still no
78
+ * repository to put in it.
79
+ *
80
+ * A throwing getter rather than `undefined`: a plugin reaching for packages during `init` has
81
+ * made a real mistake, and `undefined` would let it write a check that silently does nothing.
82
+ */
83
+ get repository(): Repository;
84
+ /**
85
+ * The single instance of a service, built on first use.
86
+ *
87
+ * **Lazy, and for two reasons.** `rman info` has no business constructing the changelog, version
88
+ * and release services, which is the same laziness the config scope's `git` getter was measured
89
+ * to need (0 git reads with property descriptors, 1 with a spread). And services call each other -
90
+ * `VersionService` reaches for `RunService` and `ChangelogService`, which reaches for
91
+ * `ChangeHashService` - so resolving at call time is what keeps that from being a construction
92
+ * cycle.
93
+ */
94
+ getService<K extends keyof ServiceMap>(name: K): ServiceMap[K];
95
+ /**
96
+ * Registers how a service is built. A plugin replacing one of the core's - a version planner that
97
+ * knows its own ecosystem's dependency ranges - does it here, which is why that no longer needs a
98
+ * seam of its own on the plugin interface.
99
+ *
100
+ * Refused once the service has been built: something is already holding the old instance, and
101
+ * swapping the factory then would leave two answers to one question in play.
102
+ */
103
+ setService<K extends keyof ServiceMap>(name: K, factory: ServiceFactory<ServiceMap[K]>): void;
104
+ /**
105
+ * Called by `Repository.create`, after the plugins that find the packages have run.
106
+ *
107
+ * **One application, one repository**, and a second is refused. It was briefly "last one wins",
108
+ * because a single shared application made two repositories in one test collide; each
109
+ * `Repository.create` makes its own now, so the rule holds again and nothing in a process is
110
+ * shared by accident.
111
+ */
112
+ attachRepository(repository: Repository): void;
113
+ private _repository?;
114
+ private readonly services;
115
+ private readonly factories;
116
+ }