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,231 @@
1
+ import type { DockerPublishOptions } from '../builtins/publish-targets/docker/docker.target.js';
2
+ import type { RmanApplication } from '../core/application.js';
3
+ import { type RmanConfig } from '../interfaces/rman-config.interface.js';
4
+ /** Hoisted for `ArgsOf` - see `version.command.ts` and `RmanConfig.ArgsOf` for why. */
5
+ declare const COMMAND: "publish";
6
+ /**
7
+ * **The options that belong to publishing, rather than to any one registry.**
8
+ *
9
+ * Everything npm-shaped that used to sit beside them - `--access`, `--tag`, `--otp`, `--registry`,
10
+ * `--userconfig`, `--contents`, `--package-manager` - is declared by the `npm` target now, and
11
+ * `--docker-namespace` by the `docker` one. They are merged in at build time (see below), so
12
+ * `rman publish --help` lists exactly the flags the targets this repository actually has.
13
+ */
14
+ declare const config: {
15
+ yes: {
16
+ target: "cli";
17
+ alias: string;
18
+ describe: string;
19
+ type: "boolean";
20
+ };
21
+ dryRun: {
22
+ target: "cli";
23
+ cliName: string;
24
+ describe: string;
25
+ type: "boolean";
26
+ };
27
+ json: {
28
+ target: "cli";
29
+ alias: string;
30
+ describe: string;
31
+ type: "boolean";
32
+ };
33
+ ignoreDirty: {
34
+ target: "cli";
35
+ cliName: string;
36
+ describe: string;
37
+ type: "boolean";
38
+ };
39
+ /**
40
+ * `target: 'both'` because it is genuinely both: `--target docker` for one run, `publish.target`
41
+ * in a `.rmanrc` for a package's standing answer. Its `choices` cannot be written here - which
42
+ * targets exist is whatever the repository's plugins contribute - so they are filled in from the
43
+ * registry where the command is built.
44
+ */
45
+ target: {
46
+ target: "both";
47
+ describe: string;
48
+ type: "array";
49
+ };
50
+ skip: {
51
+ target: "config";
52
+ describe: string;
53
+ type: "boolean";
54
+ };
55
+ allowBranch: {
56
+ target: "cli";
57
+ cliName: string;
58
+ describe: string;
59
+ type: "string";
60
+ };
61
+ ignoreBranch: {
62
+ target: "cli";
63
+ cliName: string;
64
+ describe: string;
65
+ type: "string";
66
+ };
67
+ scope: {
68
+ target: "cli";
69
+ describe: string;
70
+ type: "string";
71
+ };
72
+ ignore: {
73
+ target: "cli";
74
+ describe: string;
75
+ type: "string";
76
+ };
77
+ platform: {
78
+ target: "cli";
79
+ describe: string;
80
+ type: "string";
81
+ };
82
+ deps: {
83
+ target: "cli";
84
+ describe: string;
85
+ type: "boolean";
86
+ };
87
+ dependents: {
88
+ target: "cli";
89
+ describe: string;
90
+ type: "boolean";
91
+ };
92
+ };
93
+ /**
94
+ * The rest of `publish.*`: **one block per target, contributed by the target**.
95
+ *
96
+ * `PublishTargetConfigs` is an empty interface on purpose - an extension point rather than a list.
97
+ * `docker` below is the core's own target declaring its block; `rman-node` adds `directory` from
98
+ * its own package the same way. Neither could be written here: which targets exist is whatever the
99
+ * repository's plugins contribute.
100
+ *
101
+ * **It had to be a slot, not two declarations of `publish`.** A key that arrives from two places is
102
+ * `Interface 'RmanConfig' cannot simultaneously extend types ... Named property 'publish' of types
103
+ * ... are not identical` (measured) - so `publish` is contributed once, here, and everything under
104
+ * it merges into this one interface first.
105
+ */
106
+ export interface PublishExtraKeys extends PublishTargetConfigs {
107
+ }
108
+ /** Where a target declares its own `publish.<target>` config block. Augmented, never edited. */
109
+ export interface PublishTargetConfigs {
110
+ /** Required once `"docker"` is one of a package's `publish.target`s - `publish --target docker`
111
+ * errors clearly on a package that opts in here but leaves this out. */
112
+ docker?: DockerPublishOptions;
113
+ }
114
+ type Args = RmanConfig.ArgsOf<typeof config, typeof COMMAND>;
115
+ /**
116
+ * `rman publish` - the repository's half of question B: which packages' current version is not out
117
+ * there yet, and pushing the ones that are not.
118
+ *
119
+ * **This was `rman-node`'s command until the targets became contributions**, and that had the
120
+ * ownership backwards. Everything here is about a repository - which packages are candidates, what
121
+ * order to push them in (dependencies first), the plan/confirm/apply shape, `--dry-run`, the JSON a
122
+ * CI gate reads - and none of it is npm's. What *is* npm's is one answer to "is this version on the
123
+ * registry, and how do I push it", which is `PublishTarget`. The measured consequence of the old
124
+ * arrangement: `publish --target docker` in a repository with no JavaScript in it required
125
+ * installing a Node plugin to reach a Docker push the core had implemented all along.
126
+ *
127
+ * Never looks at whether `version` ran - it inspects what is on disk and on each registry, so it
128
+ * behaves the same right after a bump or days later, and re-running is safe.
129
+ */
130
+ declare const publishCommand: (app: RmanApplication) => {
131
+ command: "publish";
132
+ describe: string;
133
+ /** `group` is not read here, but `allowBranch`/`ignoreBranch` are - through the shared
134
+ * branch-guard group, which only exposes them as flags. */
135
+ configKeys: string[];
136
+ config: {
137
+ /** Filled in from the registry: `--target` can only offer what this repository installed. */
138
+ target: {
139
+ choices: string[];
140
+ target: "both";
141
+ describe: string;
142
+ type: "array";
143
+ };
144
+ yes: {
145
+ target: "cli";
146
+ alias: string;
147
+ describe: string;
148
+ type: "boolean";
149
+ };
150
+ dryRun: {
151
+ target: "cli";
152
+ cliName: string;
153
+ describe: string;
154
+ type: "boolean";
155
+ };
156
+ json: {
157
+ target: "cli";
158
+ alias: string;
159
+ describe: string;
160
+ type: "boolean";
161
+ };
162
+ ignoreDirty: {
163
+ target: "cli";
164
+ cliName: string;
165
+ describe: string;
166
+ type: "boolean";
167
+ };
168
+ skip: {
169
+ target: "config";
170
+ describe: string;
171
+ type: "boolean";
172
+ };
173
+ allowBranch: {
174
+ target: "cli";
175
+ cliName: string;
176
+ describe: string;
177
+ type: "string";
178
+ };
179
+ ignoreBranch: {
180
+ target: "cli";
181
+ cliName: string;
182
+ describe: string;
183
+ type: "string";
184
+ };
185
+ scope: {
186
+ target: "cli";
187
+ describe: string;
188
+ type: "string";
189
+ };
190
+ ignore: {
191
+ target: "cli";
192
+ describe: string;
193
+ type: "string";
194
+ };
195
+ platform: {
196
+ target: "cli";
197
+ describe: string;
198
+ type: "string";
199
+ };
200
+ deps: {
201
+ target: "cli";
202
+ describe: string;
203
+ type: "boolean";
204
+ };
205
+ dependents: {
206
+ target: "cli";
207
+ describe: string;
208
+ type: "boolean";
209
+ };
210
+ };
211
+ examples: {
212
+ command: string;
213
+ description: string;
214
+ }[];
215
+ handler: (args: Args) => Promise<void>;
216
+ };
217
+ export default publishCommand;
218
+ /**
219
+ * `publish`'s own keys on `RmanConfig`: `target` and `skip` derived from the `config` block, plus
220
+ * every target's own block through `PublishExtraKeys`.
221
+ *
222
+ * So a target contributes its config type as well as its flags now - `publish.docker.*` is declared
223
+ * by the core's docker target and `publish.npm.directory` by `rman-node`'s npm one, each from its own
224
+ * package, and neither can collide with the other or with what the command derives.
225
+ */
226
+ declare module '../interfaces/rman-config.interface.js' {
227
+ namespace RmanConfig {
228
+ interface CommandConfigs extends RmanConfig.CommandContribution<ReturnType<typeof publishCommand>, PublishExtraKeys> {
229
+ }
230
+ }
231
+ }
@@ -0,0 +1,304 @@
1
+ import readline from 'node:readline/promises';
2
+ import colors from 'ansi-colors';
3
+ import { unknownTargets } from '../core/interfaces/publish-target.js';
4
+ import { registerCommand } from '../interfaces/rman-config.interface.js';
5
+ import { assertAllowedBranch, branchGuardOptions, readBranchGuardOptions } from '../utils/branch-guard.js';
6
+ import { packageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
7
+ /** Hoisted for `ArgsOf` - see `version.command.ts` and `RmanConfig.ArgsOf` for why. */
8
+ const COMMAND = 'publish';
9
+ /**
10
+ * **The options that belong to publishing, rather than to any one registry.**
11
+ *
12
+ * Everything npm-shaped that used to sit beside them - `--access`, `--tag`, `--otp`, `--registry`,
13
+ * `--userconfig`, `--contents`, `--package-manager` - is declared by the `npm` target now, and
14
+ * `--docker-namespace` by the `docker` one. They are merged in at build time (see below), so
15
+ * `rman publish --help` lists exactly the flags the targets this repository actually has.
16
+ */
17
+ const config = {
18
+ ...packageFilterOptions,
19
+ ...branchGuardOptions,
20
+ yes: {
21
+ target: 'cli',
22
+ alias: 'y',
23
+ describe: 'Skip the confirmation prompt and publish immediately',
24
+ type: 'boolean',
25
+ },
26
+ dryRun: {
27
+ target: 'cli',
28
+ cliName: 'dry-run',
29
+ describe: 'Only show the plan - never publishes, regardless of --yes',
30
+ type: 'boolean',
31
+ },
32
+ json: {
33
+ target: 'cli',
34
+ alias: 'j',
35
+ describe: 'Print the plan as JSON instead of text - one entry per package and target. Combine with ' +
36
+ '--dry-run to ask "is there anything to publish?" without publishing (e.g. a CI release gate).',
37
+ type: 'boolean',
38
+ },
39
+ ignoreDirty: {
40
+ target: 'cli',
41
+ cliName: 'ignore-dirty',
42
+ describe: 'Exclude a package with uncommitted local changes instead of aborting the whole run',
43
+ type: 'boolean',
44
+ },
45
+ /**
46
+ * `target: 'both'` because it is genuinely both: `--target docker` for one run, `publish.target`
47
+ * in a `.rmanrc` for a package's standing answer. Its `choices` cannot be written here - which
48
+ * targets exist is whatever the repository's plugins contribute - so they are filled in from the
49
+ * registry where the command is built.
50
+ */
51
+ target: {
52
+ target: 'both',
53
+ describe: 'Restrict this run to just these publish target(s) (repeatable) - default: every target ' +
54
+ 'each package itself is configured for (.rmanrc "publish.target"), or the targets that claim ' +
55
+ "it when it configures none. A package that opts into a target but leaves out that target's " +
56
+ 'own config errors clearly instead of being silently skipped.',
57
+ type: 'array',
58
+ },
59
+ skip: {
60
+ target: 'config',
61
+ describe: 'Excludes this package from publish entirely (every target), regardless of "target" or ' +
62
+ '"private" - a single, explicit "never published" statement. "changelog" honours it too.',
63
+ type: 'boolean',
64
+ },
65
+ };
66
+ /**
67
+ * `rman publish` - the repository's half of question B: which packages' current version is not out
68
+ * there yet, and pushing the ones that are not.
69
+ *
70
+ * **This was `rman-node`'s command until the targets became contributions**, and that had the
71
+ * ownership backwards. Everything here is about a repository - which packages are candidates, what
72
+ * order to push them in (dependencies first), the plan/confirm/apply shape, `--dry-run`, the JSON a
73
+ * CI gate reads - and none of it is npm's. What *is* npm's is one answer to "is this version on the
74
+ * registry, and how do I push it", which is `PublishTarget`. The measured consequence of the old
75
+ * arrangement: `publish --target docker` in a repository with no JavaScript in it required
76
+ * installing a Node plugin to reach a Docker push the core had implemented all along.
77
+ *
78
+ * Never looks at whether `version` ran - it inspects what is on disk and on each registry, so it
79
+ * behaves the same right after a bump or days later, and re-running is safe.
80
+ */
81
+ const publishCommand = registerCommand(app => {
82
+ const repository = app.repository;
83
+ const targets = app.publishTargets.all;
84
+ return {
85
+ command: COMMAND,
86
+ describe: 'Publishes every package to its configured target(s) - see .rmanrc "publish.target"',
87
+ /** `group` is not read here, but `allowBranch`/`ignoreBranch` are - through the shared
88
+ * branch-guard group, which only exposes them as flags. */
89
+ configKeys: ['allowBranch', 'ignoreBranch'],
90
+ config: {
91
+ ...config,
92
+ /** Filled in from the registry: `--target` can only offer what this repository installed. */
93
+ target: { ...config.target, choices: targets.map(t => t.name) },
94
+ ...targetOptions(targets),
95
+ },
96
+ examples: [
97
+ { command: '$0 publish', description: '# Show the plan, then ask for confirmation' },
98
+ { command: '$0 publish --yes', description: '# Publish immediately, no confirmation' },
99
+ { command: '$0 publish --dry-run', description: '# Only show the plan, never publish' },
100
+ ...(targets.length > 1
101
+ ? [
102
+ {
103
+ command: `$0 publish --target ${targets[targets.length - 1].name}`,
104
+ description: '# Only the packages configured for that one target',
105
+ },
106
+ ]
107
+ : []),
108
+ ],
109
+ handler: async (args) => {
110
+ await assertAllowedBranch(repository, readBranchGuardOptions(args));
111
+ const requested = args.target;
112
+ const selected = selectTargets(targets, requested);
113
+ const ctx = {
114
+ app,
115
+ repository,
116
+ options: { ...readPackageFilterOptions(args), ignoreDirty: args.ignoreDirty },
117
+ args: args,
118
+ };
119
+ assertDeclaredTargetsExist(app, repository.getPackages());
120
+ const plans = new Map();
121
+ for (const target of selected)
122
+ plans.set(target, await target.getPlan(ctx));
123
+ if (args.json) {
124
+ console.log(JSON.stringify(jsonPlan(plans), undefined, 2));
125
+ }
126
+ else {
127
+ for (const [target, plan] of plans)
128
+ printPlan(plan, target.name);
129
+ }
130
+ /** An explicitly requested target that matched nothing is a mistake worth reporting: the run
131
+ * asked for something by name and silently got an empty plan back. */
132
+ if (requested?.length) {
133
+ const empty = [...plans].filter(([, plan]) => !plan.length).map(([target]) => target.name);
134
+ if (empty.length) {
135
+ throw logged(`--target ${empty.join(', ')} was given, but no package ships there ` + '(see .rmanrc "publish.target").');
136
+ }
137
+ }
138
+ const all = [...plans.values()].flat();
139
+ const errors = all.filter(e => e.status === 'error');
140
+ if (errors.length) {
141
+ const allDirty = errors.every(e => e.reason === 'uncommitted local changes');
142
+ throw logged(allDirty
143
+ ? `${errors.length} package(s) have uncommitted local changes ` +
144
+ '(pass --ignore-dirty to exclude them instead of aborting)'
145
+ : `${errors.length} package(s) failed to prepare for publish - see the errors above`);
146
+ }
147
+ if (!all.some(e => e.status === 'publish')) {
148
+ if (!args.json)
149
+ console.log(colors.gray('Nothing to publish.'));
150
+ return;
151
+ }
152
+ if (args.dryRun)
153
+ return;
154
+ let proceed = !!args.yes;
155
+ if (!proceed) {
156
+ if (!process.stdout.isTTY) {
157
+ console.log(colors.gray('Not a TTY - refusing to prompt. Pass --yes to publish non-interactively.'));
158
+ return;
159
+ }
160
+ proceed = await confirm('Publish these packages?');
161
+ }
162
+ if (!proceed)
163
+ return;
164
+ let failed = false;
165
+ for (const [target, plan] of plans) {
166
+ const applied = await target.applyPlan(ctx, plan);
167
+ if (printApplied(applied, plan, target.name))
168
+ failed = true;
169
+ }
170
+ if (failed)
171
+ throw logged('"publish" failed');
172
+ },
173
+ };
174
+ });
175
+ export default publishCommand;
176
+ /**
177
+ * Every registered target's own flags, flattened into one block.
178
+ *
179
+ * **A collision is refused rather than resolved.** Two targets both wanting `--registry` is a real
180
+ * possibility (npm has one, so does a hypothetical Cargo), and any rule for picking a winner -
181
+ * registration order, prefixing, last wins - produces a flag that silently means the other
182
+ * target's thing. Naming it for the target (`--docker-namespace`) is the fix, and the error says so.
183
+ */
184
+ function targetOptions(targets) {
185
+ const merged = {};
186
+ const owners = new Map();
187
+ for (const target of targets) {
188
+ for (const [key, option] of Object.entries(target.options ?? {})) {
189
+ const owner = owners.get(key);
190
+ if (owner) {
191
+ throw new Error(`Publish targets "${owner}" and "${target.name}" both declare an option named "${key}" - ` +
192
+ 'name it for the target it belongs to (e.g. "docker-namespace").');
193
+ }
194
+ owners.set(key, target.name);
195
+ merged[key] = option;
196
+ }
197
+ }
198
+ return merged;
199
+ }
200
+ /** `--target` narrows the run; omitting it means every registered target, each of which then
201
+ * decides for itself which packages are its own. */
202
+ function selectTargets(targets, requested) {
203
+ if (!requested?.length)
204
+ return [...targets];
205
+ const known = new Map(targets.map(t => [t.name, t]));
206
+ return requested.map(name => {
207
+ const target = known.get(name);
208
+ if (!target) {
209
+ throw logged(`Unknown publish target "${name}" - this repository has: ${[...known.keys()].join(', ')}.`);
210
+ }
211
+ return target;
212
+ });
213
+ }
214
+ /**
215
+ * A package naming a target nothing implements.
216
+ *
217
+ * This used to be impossible to get wrong - `publish.target` was typed `'npm' | 'docker'` and
218
+ * `--target` had those two as `choices`. With targets contributed, the core cannot enumerate them,
219
+ * so the check moves to where the facts are. Silence here would mean a package quietly publishing
220
+ * nowhere, which is the failure mode `publish` exists to rule out.
221
+ */
222
+ function assertDeclaredTargetsExist(app, packages) {
223
+ const offenders = packages
224
+ .map(pkg => ({ pkg, unknown: unknownTargets(app, pkg) }))
225
+ .filter(({ unknown }) => unknown.length);
226
+ if (!offenders.length)
227
+ return;
228
+ const installed = app.publishTargets.all.map(t => t.name).join(', ') || '(none)';
229
+ throw logged(offenders.map(({ pkg, unknown }) => `"${pkg.name}" declares publish target(s) ${unknown.join(', ')}`).join('; ') +
230
+ ` - this repository has: ${installed}.`);
231
+ }
232
+ /** One `--json` row per package *and target*: a package shipping to two registries needs two, or a
233
+ * consumer cannot tell which of them still has something to do. */
234
+ function jsonPlan(plans) {
235
+ return [...plans].flatMap(([target, plan]) => plan.map(entry => ({
236
+ name: entry.package.name,
237
+ target: target.name,
238
+ status: entry.status,
239
+ version: entry.version,
240
+ detail: entry.detail,
241
+ reason: entry.reason,
242
+ })));
243
+ }
244
+ function printPlan(entries, label) {
245
+ const prefix = colors.gray(`[${label}] `);
246
+ for (const e of entries) {
247
+ const name = prefix + colors.cyan(e.package.name);
248
+ switch (e.status) {
249
+ case 'publish':
250
+ /** `detail` belongs on this line, not only on the "published" one after the fact: it is
251
+ * where a target says *where* the package is going - npm's dist-tag, docker's resolved
252
+ * `<namespace>/<image>` - and the plan is what the reader is being asked to confirm. */
253
+ console.log(colors.green('publish'), name, e.version, ...(e.detail ? [colors.cyan(e.detail)] : []), colors.gray(e.reason ?? ''));
254
+ break;
255
+ case 'up-to-date':
256
+ console.log(colors.gray('up-to-date'), name, e.version);
257
+ break;
258
+ case 'skip':
259
+ console.log(colors.cyan('skip'), name, colors.gray(e.reason ?? ''));
260
+ break;
261
+ case 'error':
262
+ console.log(colors.red('error'), name, colors.red(e.reason ?? ''));
263
+ break;
264
+ }
265
+ }
266
+ }
267
+ /**
268
+ * What one target actually did. Returns whether anything it was asked to publish failed.
269
+ *
270
+ * An `'error'` is only reported when the *plan* said `'publish'` for that package: an entry that
271
+ * was already an error before anything ran has been printed once by `printPlan`, and printing it
272
+ * again under "failed" would read as a push that was attempted and did not work.
273
+ */
274
+ function printApplied(applied, plan, label) {
275
+ const prefix = colors.gray(`[${label}] `);
276
+ let failed = false;
277
+ for (const entry of applied) {
278
+ if (entry.status === 'publish') {
279
+ console.log(colors.green('published'), prefix + colors.cyan(entry.package.name), entry.detail ?? entry.version);
280
+ }
281
+ else if (entry.status === 'error' && plan.find(e => e.package === entry.package)?.status === 'publish') {
282
+ failed = true;
283
+ console.log(colors.red('failed'), prefix + colors.cyan(entry.package.name), colors.red(entry.reason ?? ''));
284
+ }
285
+ }
286
+ return failed;
287
+ }
288
+ /** The `logged` convention: printed here, so `runCli`'s catch does not print it a second time. */
289
+ function logged(message) {
290
+ console.log(colors.red(message));
291
+ const err = new Error(message);
292
+ err.logged = true;
293
+ return err;
294
+ }
295
+ async function confirm(question) {
296
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
297
+ try {
298
+ const answer = await rl.question(`${question} (y/N) `);
299
+ return /^y(es)?$/i.test(answer.trim());
300
+ }
301
+ finally {
302
+ rl.close();
303
+ }
304
+ }