rman 2.0.0-beta.2 → 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 (164) hide show
  1. package/README.md +2 -12
  2. package/{plugins/node/augmentation/rman.augmentation.d.ts → builtins/platforms/node/augmentation/rmanrc.augmentation.d.ts} +4 -5
  3. package/{plugins → builtins/platforms}/node/augmentation/system-info.augmentation.js +1 -1
  4. package/{plugins → builtins/platforms}/node/commands/ci.command.d.ts +5 -3
  5. package/{plugins → builtins/platforms}/node/commands/ci.command.js +6 -5
  6. package/{plugins → builtins/platforms}/node/commands/clean.command.d.ts +6 -4
  7. package/{plugins → builtins/platforms}/node/commands/clean.command.js +6 -5
  8. package/builtins/platforms/node/index.d.ts +8 -0
  9. package/builtins/platforms/node/index.js +8 -0
  10. package/{plugins → builtins/platforms}/node/node-config.interface.d.ts +1 -2
  11. package/{plugins → builtins/platforms}/node/node-manifest.provider.d.ts +3 -3
  12. package/{plugins → builtins/platforms}/node/node-manifest.provider.js +3 -3
  13. package/{plugins → builtins/platforms}/node/node.platform.d.ts +3 -3
  14. package/{plugins → builtins/platforms}/node/services/ci.service.d.ts +3 -3
  15. package/{plugins → builtins/platforms}/node/services/ci.service.js +4 -4
  16. package/{plugins → builtins/platforms}/node/services/clean.service.d.ts +3 -3
  17. package/{plugins → builtins/platforms}/node/services/clean.service.js +3 -3
  18. package/{plugins → builtins/platforms}/node/services/publish.service.d.ts +4 -4
  19. package/{plugins → builtins/platforms}/node/services/publish.service.js +5 -5
  20. package/{plugins → builtins/platforms}/node/services/version-plan.service.d.ts +3 -3
  21. package/{plugins → builtins/platforms}/node/services/version-plan.service.js +2 -2
  22. package/builtins/presets/index.d.ts +15 -0
  23. package/builtins/presets/index.js +57 -0
  24. package/builtins/presets/node.d.ts +16 -0
  25. package/builtins/presets/node.js +57 -0
  26. package/{services → builtins/publish-targets/docker}/docker-publish.service.d.ts +6 -6
  27. package/{services → builtins/publish-targets/docker}/docker-publish.service.js +9 -9
  28. package/{targets → builtins/publish-targets/docker}/docker.target.d.ts +6 -5
  29. package/{targets → builtins/publish-targets/docker}/docker.target.js +1 -1
  30. package/builtins/publish-targets/docker/index.d.ts +2 -0
  31. package/builtins/publish-targets/docker/index.js +2 -0
  32. package/builtins/publish-targets/npm/index.d.ts +2 -0
  33. package/builtins/publish-targets/npm/index.js +2 -0
  34. package/{plugins/node → builtins/publish-targets/npm}/npm-publish-target.d.ts +4 -4
  35. package/{plugins/node → builtins/publish-targets/npm}/npm-publish-target.js +4 -5
  36. package/cli.d.ts +3 -0
  37. package/cli.js +16 -7
  38. package/commands/changelog.command.d.ts +106 -2
  39. package/commands/changelog.command.js +40 -0
  40. package/commands/config.command.js +23 -5
  41. package/commands/lint.command.d.ts +176 -0
  42. package/commands/lint.command.js +34 -0
  43. package/commands/list.command.js +1 -1
  44. package/commands/publish.command.d.ts +1 -1
  45. package/commands/publish.command.js +1 -1
  46. package/commands/run.command.js +1 -1
  47. package/commands/version.command.d.ts +16 -3
  48. package/commands/version.command.js +143 -9
  49. package/commands.d.ts +1 -1
  50. package/commands.js +1 -1
  51. package/constants.js +1 -1
  52. package/core/application.d.ts +10 -10
  53. package/core/application.js +28 -11
  54. package/core/{package.d.ts → classes/package.d.ts} +12 -4
  55. package/core/{package.js → classes/package.js} +17 -1
  56. package/core/{repository.d.ts → classes/repository.d.ts} +22 -36
  57. package/core/{repository.js → classes/repository.js} +112 -241
  58. package/core/{service.d.ts → classes/service.d.ts} +2 -2
  59. package/core/classes/workspace.d.ts +275 -0
  60. package/core/classes/workspace.js +537 -0
  61. package/core/command-builder.js +17 -1
  62. package/core/config/config-file-scope.d.ts +107 -0
  63. package/core/config/config-file-scope.js +226 -0
  64. package/core/config/config-interpolator.d.ts +164 -0
  65. package/core/config/config-interpolator.js +577 -0
  66. package/core/config/config-paths.d.ts +65 -0
  67. package/core/config/config-paths.js +78 -0
  68. package/core/config/config-reader.d.ts +192 -0
  69. package/core/config/config-reader.js +727 -0
  70. package/core/{merge-config.js → config/merge-config.js} +4 -8
  71. package/core/{custom-command.d.ts → interfaces/custom-command.d.ts} +5 -5
  72. package/core/{manifest.d.ts → interfaces/manifest.d.ts} +3 -3
  73. package/core/{manifest.js → interfaces/manifest.js} +2 -2
  74. package/core/{plugin.d.ts → interfaces/plugin.d.ts} +44 -27
  75. package/core/{plugin.js → interfaces/plugin.js} +48 -10
  76. package/core/{publish-target.d.ts → interfaces/publish-target.d.ts} +24 -12
  77. package/core/{publish-target.js → interfaces/publish-target.js} +16 -3
  78. package/core/{run-step.d.ts → interfaces/run-step.d.ts} +4 -4
  79. package/core/plugin-loader.d.ts +11 -51
  80. package/core/plugin-loader.js +21 -202
  81. package/index.d.ts +29 -28
  82. package/index.js +20 -46
  83. package/interfaces/config-scope.interface.d.ts +223 -0
  84. package/interfaces/config-scope.interface.js +1 -0
  85. package/interfaces/rman-config.interface.d.ts +100 -5
  86. package/interfaces/rman-config.interface.js +17 -0
  87. package/package.json +2 -1
  88. package/services/change-hash.service.d.ts +40 -6
  89. package/services/change-hash.service.js +90 -11
  90. package/services/changelog.service.d.ts +31 -3
  91. package/services/changelog.service.js +448 -65
  92. package/services/config-service.d.ts +8 -0
  93. package/services/config-service.js +3 -0
  94. package/services/exec.service.d.ts +2 -2
  95. package/services/exec.service.js +1 -1
  96. package/services/github-release.service.d.ts +3 -3
  97. package/services/github-release.service.js +3 -3
  98. package/services/import.service.d.ts +2 -2
  99. package/services/import.service.js +1 -1
  100. package/services/list.service.d.ts +9 -7
  101. package/services/list.service.js +2 -2
  102. package/services/run.service.d.ts +15 -11
  103. package/services/run.service.js +62 -18
  104. package/services/system-info.d.ts +1 -1
  105. package/services/version-plan.service.d.ts +8 -4
  106. package/services/version-plan.service.js +54 -21
  107. package/services/version.service.d.ts +5 -5
  108. package/services/version.service.js +7 -4
  109. package/services.d.ts +1 -1
  110. package/services.js +1 -1
  111. package/utils/branch-guard.d.ts +1 -1
  112. package/utils/color-yaml.d.ts +8 -0
  113. package/utils/color-yaml.js +86 -0
  114. package/utils/git.d.ts +26 -0
  115. package/utils/git.js +65 -0
  116. package/utils/logger.d.ts +1 -1
  117. package/utils/package-filter.d.ts +8 -1
  118. package/utils/package-filter.js +7 -0
  119. package/utils/printable-config.d.ts +8 -0
  120. package/utils/printable-config.js +45 -0
  121. package/utils/progress-panel.d.ts +9 -0
  122. package/utils/progress-panel.js +23 -1
  123. package/utils/release-version.d.ts +1 -1
  124. package/utils/run-options.js +1 -1
  125. package/utils/version-group.d.ts +31 -0
  126. package/utils/version-group.js +43 -0
  127. package/commands/changed.command.d.ts +0 -80
  128. package/commands/changed.command.js +0 -36
  129. package/core/config.d.ts +0 -530
  130. package/core/config.js +0 -1123
  131. package/core/core-services.d.ts +0 -14
  132. package/core/core-services.js +0 -30
  133. package/core/core-targets.d.ts +0 -14
  134. package/core/core-targets.js +0 -16
  135. package/core/extends-config.d.ts +0 -35
  136. package/core/extends-config.js +0 -129
  137. package/core/load-config-module.d.ts +0 -28
  138. package/core/load-config-module.js +0 -42
  139. package/core/workspace.d.ts +0 -119
  140. package/core/workspace.js +0 -124
  141. package/plugins/builtins.d.ts +0 -44
  142. package/plugins/builtins.js +0 -33
  143. package/plugins/detect.d.ts +0 -78
  144. package/plugins/detect.js +0 -70
  145. package/plugins/node/index.d.ts +0 -29
  146. package/plugins/node/index.js +0 -40
  147. /package/{core/run-step.js → builtins/platforms/node/augmentation/rmanrc.augmentation.js} +0 -0
  148. /package/{plugins → builtins/platforms}/node/augmentation/system-info.augmentation.d.ts +0 -0
  149. /package/{plugins → builtins/platforms}/node/node-config.interface.js +0 -0
  150. /package/{plugins → builtins/platforms}/node/node.platform.js +0 -0
  151. /package/{plugins → builtins/platforms}/node/utils/workspace-range.d.ts +0 -0
  152. /package/{plugins → builtins/platforms}/node/utils/workspace-range.js +0 -0
  153. /package/{plugins/node/utils → builtins/publish-targets/npm}/npm-view.d.ts +0 -0
  154. /package/{plugins/node/utils → builtins/publish-targets/npm}/npm-view.js +0 -0
  155. /package/core/{registry.d.ts → classes/registry.d.ts} +0 -0
  156. /package/core/{registry.js → classes/registry.js} +0 -0
  157. /package/core/{service.js → classes/service.js} +0 -0
  158. /package/core/{version-scheme.d.ts → classes/version-scheme.d.ts} +0 -0
  159. /package/core/{version-scheme.js → classes/version-scheme.js} +0 -0
  160. /package/core/{merge-config.d.ts → config/merge-config.d.ts} +0 -0
  161. /package/core/{resolve-target.d.ts → config/resolve-config-target.d.ts} +0 -0
  162. /package/core/{resolve-target.js → config/resolve-config-target.js} +0 -0
  163. /package/core/{custom-command.js → interfaces/custom-command.js} +0 -0
  164. /package/{plugins/node/augmentation/rman.augmentation.js → core/interfaces/run-step.js} +0 -0
@@ -0,0 +1,223 @@
1
+ import type path from 'node:path';
2
+ import type semver from 'semver';
3
+ import type { ConfigFileScope } from '../core/config/config-file-scope.js';
4
+ /**
5
+ * **What a `${{ ... }}` expression can name**, and what a value function is handed.
6
+ *
7
+ * One file because these six describe one thing from different angles: `ConfigScope` is the whole
8
+ * of it, the others are its members, and `ConfigValueContext` is the same scope with `value` added
9
+ * for a function. They are `interfaces/` rather than `core/` for the reason the config interface
10
+ * already is - nothing here runs, and the audience is a config author.
11
+ */
12
+ /**
13
+ * What a `${{ ... }}` expression can see - the bindings of the fresh global it is evaluated in.
14
+ * Namespaced rather than a flat bag of loose names: one obvious place per fact, and room to add
15
+ * helpers to `pkg`/`repository` later without crowding the global.
16
+ *
17
+ * Alongside these, **the config's own top-level keys are bound bare** (`${{ changelog.filePath }}`,
18
+ * `${{ clean.include }}`) - see `interpolateConfig`. They are not listed here because they come
19
+ * from the config being interpolated, not from this object; a name here wins over a config key of
20
+ * the same name.
21
+ *
22
+ * **Trap: bare `${{ version }}` is the `version` *options block*, not the package's version
23
+ * string** - that is `${{ pkg.version }}`. Same word, two different things, and the plain one
24
+ * belongs to the config because every other config key is reachable that way.
25
+ */
26
+ export interface ConfigScope {
27
+ /** The package the config was resolved for - which is what lets one declaration at the root
28
+ * still say something package-specific. */
29
+ pkg: PackageScope;
30
+ repository: RepositoryScope;
31
+ /** Paths, resolved against the package the config was resolved for. */
32
+ file: FileScope;
33
+ /**
34
+ * The **contents** of a structured file - `${{ read('tsconfig.json').compilerOptions.outDir }}` -
35
+ * where `file` answers only where one is. Resolved against `pkg.dirname` like `file`, so a
36
+ * `"[*]"` block asks each package about its own; a repository-level file is reached through
37
+ * `read(path.join(repository.dirname, ...))`.
38
+ *
39
+ * `.json`, `.yml`/`.yaml` and `.ini` by extension, or name it for a file that does not say
40
+ * (`read('.npmrc', 'ini')`). **Throws** when the file is absent, as `file.resolve` does - compose
41
+ * with `file.exists` when its absence is a case to handle.
42
+ *
43
+ * **A manifest is `pkg.manifest`, not this.** `read('package.json')` works and is the wrong
44
+ * answer: which file a package's identity lives in belongs to the ecosystem, so that expression
45
+ * is already wrong in a Cargo package sitting beside a Node one.
46
+ *
47
+ * The result is **deeply frozen and shared** - see `readStructuredFile`. Spread it to change it.
48
+ */
49
+ read: ConfigFileScope.ReadFile;
50
+ env: Record<string, string | undefined>;
51
+ /** rman's own `semver`, for the arithmetic every release config eventually wants
52
+ * (`semver.major(pkg.version)`). */
53
+ semver: typeof semver;
54
+ /**
55
+ * Node's own `node:path` - `${{ path.join(pkg.dirname, 'LICENSE') }}`, rather than gluing
56
+ * strings with `+ "/" +` and getting a double separator or none.
57
+ *
58
+ * The platform's flavour, not `path.posix`, so a joined path is the one the shell on *this*
59
+ * machine understands; `path.posix` and `path.win32` are reachable through it when a config
60
+ * genuinely needs one of them (a Docker image path, say, which is always posix).
61
+ */
62
+ path: typeof path;
63
+ /**
64
+ * The checkout: branch, sha, whether the tree is dirty.
65
+ *
66
+ * **Top level, not `repository.git`** - which is where it used to be, and the move is the point.
67
+ * `repository` shares its shape with `pkg` because the repository root *is* a package, and its
68
+ * only other members (`monorepo`, `packages`, `package()`) say something about the repository as
69
+ * a container of packages. A branch name says nothing about any package; it describes the
70
+ * working tree every one of them happens to be sitting in - the same kind of ambient fact as
71
+ * `env`, and it belongs beside it.
72
+ *
73
+ * **Read from git only if an expression actually asks**, then remembered for the whole run: every
74
+ * command resolves config, and a repository that never mentions git must not pay for one. See
75
+ * `Repository.configScope` for the getter, and `interpolateConfig` for why the context is built
76
+ * from property descriptors rather than a spread - a spread would fire this getter on every
77
+ * command, which is exactly what moving it up here risked.
78
+ */
79
+ git: GitScope;
80
+ }
81
+ /** One package, as an expression sees it - the same shape for the package the config belongs to
82
+ * and for the repository itself, so `${{ repository.basename }}` reads the way `${{ pkg.basename }}` does. */
83
+ /**
84
+ * `file` in a `${{ ... }}` expression: what is actually on disk, resolved against **the package
85
+ * the config was resolved for** - so one declaration in a `"[*]"` block asks each package about
86
+ * its own directory.
87
+ *
88
+ * The pair exists because a config has two different questions about a path, and answering both
89
+ * with one function would mean picking a wrong default for the other:
90
+ *
91
+ * ```yaml
92
+ * "[*]":
93
+ * run:
94
+ * build:
95
+ * # first of these that exists, and an error naming the config path if none do
96
+ * exec: 'tsc -b ${{ file.exists("tsconfig-build.json") || file.resolve("tsconfig.json") }}'
97
+ * ```
98
+ *
99
+ * **Every member asks a question, and none may ever change anything - no `copy`, no `write`, no
100
+ * `mkdir`.** Not a matter of taste: this is evaluated when the config *resolves*, which every
101
+ * command does, so a member that acted would act on `rman list`, `rman info` and `rman config`.
102
+ *
103
+ * That has been tried, in the only way a missing function can be: a shared config reaching for a
104
+ * `file.copyMany(...)` that does not exist made **every** rman command exit 1 - and had it existed,
105
+ * the quieter outcome would have been files copied by `rman list`. Work belongs in a step
106
+ * (`run.<script>`'s slots, `version`'s hooks), which is the one place rman runs anything, and a
107
+ * step can now be a function - so there is nothing this would enable that is not already possible
108
+ * at the right moment.
109
+ */
110
+ export interface FileScope {
111
+ /**
112
+ * The absolute path if it exists, **`''` if it does not** - so `a || b || c` picks the first one
113
+ * present, and so a miss never reaches the "nullish inside a string" guard that `undefined` would
114
+ * trip. Accepts a relative path (against the package directory) or an absolute one.
115
+ *
116
+ * It returns a path rather than a boolean on purpose: the caller almost always wants the path,
117
+ * and a separate `file.path()` to fetch it after a boolean test would read the disk twice and
118
+ * invite the two calls to disagree.
119
+ */
120
+ exists(target: string): string;
121
+ /** The absolute path, or **throws** - for a file whose absence is a mistake rather than a case to
122
+ * handle. The error names the config path holding the expression, like any other. */
123
+ resolve(target: string): string;
124
+ /**
125
+ * The first of several that exists, or **throws** naming every candidate it tried:
126
+ *
127
+ * ```yaml
128
+ * exec: 'tsc -b ${{ file.resolveFirst("tsconfig-build.json", "tsconfig.build.json", "tsconfig.json") }}'
129
+ * ```
130
+ *
131
+ * The same thing an `exists() || exists() || resolve()` chain does, said once - and it cannot be
132
+ * got subtly wrong the way that chain can: ending it in `exists()` leaves `tsc -b ` with no
133
+ * argument when nothing matches, and tsc then silently falls back to the directory's default
134
+ * rather than reporting that the package has no build config.
135
+ */
136
+ resolveFirst(...targets: string[]): string;
137
+ }
138
+ export interface PackageScope {
139
+ /** The package's own name, scope included (`@sqb/builder`). */
140
+ name: string;
141
+ /** Just the scope (`@sqb`), or `undefined` for an unscoped package. */
142
+ scope: string | undefined;
143
+ /** The name with its scope stripped (`builder`). */
144
+ unscopedName: string;
145
+ version: string;
146
+ /** The package directory's last segment (`builder`) - not always the same as `unscopedName`,
147
+ * which is why both exist, and usually what a sibling path (`../../coverage/builder`) is keyed on. */
148
+ basename: string;
149
+ /** Absolute path to the package's own directory - named as rman's own `Package.dirname` is. */
150
+ dirname: string;
151
+ /** That directory relative to the repository root (`packages/builder`), which is what a command
152
+ * addressing another package from the root usually needs. Empty string for the root itself. */
153
+ relativeDir: string;
154
+ /**
155
+ * Which ecosystem this package belongs to - `'node'` for one the `node` built-in read, empty when no
156
+ * plugin claimed it. The same `Package.provider`, so one declaration can address a single
157
+ * ecosystem in a polyglot repository (`if: "${{ pkg.provider === 'node' }}"`).
158
+ */
159
+ provider: string;
160
+ /**
161
+ * The whole manifest, as a copy - so an expression can reach a field rman itself has no opinion
162
+ * about (`${{ pkg.manifest.engines.node }}`).
163
+ *
164
+ * Named `manifest`, not `json`: which file a package's identity lives in is the ecosystem's
165
+ * business now (see `Plugin`'s manifest members), and `json` was that assumption showing through the one
166
+ * remaining user-facing name. A config written against `${{ pkg.json... }}` needs the rename.
167
+ */
168
+ manifest: Record<string, unknown>;
169
+ /**
170
+ * The version this run is about to write - **bound only during `version`**, and only once its
171
+ * plan is computed. Reading it anywhere else throws rather than yielding `undefined`: no other
172
+ * command has a target version, so an expression asking for one has been put in the wrong place,
173
+ * and a config that quietly evaluates to "undefined" is the failure this evaluator exists to
174
+ * prevent.
175
+ */
176
+ targetVersion: string;
177
+ }
178
+ /** Facts about the repository, on top of the root package's own - because the repository root *is*
179
+ * a package (`repository.name` is what its `package.json` says, `repository.basename` the directory
180
+ * it sits in, and the two genuinely differ). Sharing `PackageScope`'s shape is what makes
181
+ * `repository.version` read the way `pkg.version` does. */
182
+ export interface RepositoryScope extends PackageScope {
183
+ monorepo: boolean;
184
+ /** Every package in the repository - the root included only when it *is* the one package. */
185
+ packages: PackageScope[];
186
+ /** One package by name, or `undefined` - for reaching a sibling's directory. */
187
+ package(name: string): PackageScope | undefined;
188
+ }
189
+ export interface GitScope {
190
+ branch: string | undefined;
191
+ sha: string | undefined;
192
+ shortSha: string | undefined;
193
+ /** Whether the working tree has uncommitted changes. */
194
+ dirty: boolean | undefined;
195
+ }
196
+ /**
197
+ * What a **value function** is handed - `clean: { include: ({ value, pkg }) => [...] }`.
198
+ *
199
+ * The same scope a `${{ }}` expression sees, plus `value`. There is no asymmetry between the two
200
+ * spellings and that is an invariant with a spec on it: the argument *is* the expression context
201
+ * with `value` on it, so a member defined straight onto the argument would split them silently and
202
+ * a config author would meet a name that works in one spelling and not the other.
203
+ *
204
+ * **The config's own top-level keys are bound bare too**, and they cannot be typed here - they come
205
+ * from the config being interpolated rather than from this object. Reach them through `pkg.config`
206
+ * when a type matters, or accept `any` from the bare name.
207
+ *
208
+ * **Not what a *step* function is handed.** `run.<script>.exec` and `version.<slot>` take a
209
+ * `RunStepFn`, which gets a `RunStepContext` (`pkg`, `repository`, `cwd`, `runBin`, `logger`) when
210
+ * its turn comes - a different object at a different time, which is the whole distinction the two
211
+ * forms exist to draw. Never widen a step key to accept this one.
212
+ */
213
+ export interface ConfigValueContext extends ConfigScope {
214
+ /**
215
+ * What this key resolved to in the layers **below** this one - the list form of it, so
216
+ * `[...value, 'x']` needs no guard. `any` rather than a generic: the key's own type is what the
217
+ * function must return, while `value` is whatever the layers underneath happened to produce, and
218
+ * a scalar underneath arrives as a one-element list. See `previousValue`.
219
+ */
220
+ value: any;
221
+ /** The config's own top-level keys, bound bare - `vars`, `publish`, `clean`, … */
222
+ [key: string]: any;
223
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,10 +1,11 @@
1
1
  import type * as yargs from 'yargs';
2
2
  import type { RmanApplication } from '../core/application.js';
3
- import type { ConfigValue } from '../core/config.js';
4
- import type { CustomCommand } from '../core/custom-command.js';
5
- import type { Plugin } from '../core/plugin.js';
6
- import type { PublishTarget } from '../core/publish-target.js';
7
- import type { RunConditionFn, RunStepValue } from '../core/run-step.js';
3
+ import type { CodeSubtree } from '../core/config/config-paths.js';
4
+ import type { CustomCommand } from '../core/interfaces/custom-command.js';
5
+ import type { Platform, Plugin } from '../core/interfaces/plugin.js';
6
+ import type { PublishTarget } from '../core/interfaces/publish-target.js';
7
+ import type { RunConditionFn, RunStepFn, RunStepValue } from '../core/interfaces/run-step.js';
8
+ import type { ConfigValueContext } from './config-scope.interface.js';
8
9
  /**
9
10
  * The shape of `.rmanrc`/`.rmanrc.yml`/`.rmanrc.cjs`/`.mjs`/`.js` (and `package.json`'s own
10
11
  * `"rman"` key) - see docs/rman.md#configuration-rmanrc-rmanrcyml for the full reference. Every
@@ -127,6 +128,25 @@ export interface RmanConfigKeys {
127
128
  * package's own `.rmanrc` is never seen.
128
129
  */
129
130
  plugins?: string | Plugin | (string | Plugin)[];
131
+ /**
132
+ * **The technologies available here** - how a directory's packages are recognized, where they
133
+ * are, how their versions are planned.
134
+ *
135
+ * Each entry is a `Platform`, or a **glob** naming `.js` modules that `export default` one - the
136
+ * same two forms `plugins`, `commands` and `publishTargets` take. It **appends**: naming one
137
+ * never means "and drop the ones my preset brought", so a polyglot repository writes
138
+ * `extends: ['node', 'cargo']` and holds both.
139
+ *
140
+ * ```js
141
+ * // presets/node.js
142
+ * export default defineConfig({ platforms: [new NodePlatform()], commands, publishTargets });
143
+ * ```
144
+ *
145
+ * **Not the same key as `platform`** (singular), which *selects* one for a directory rather than
146
+ * contributing it: this says what is available, that says which one a package is. A package-level
147
+ * setting meant for one technology goes under `"[platform:node]"`.
148
+ */
149
+ platforms?: string | Platform | (string | Platform)[];
130
150
  /**
131
151
  * **Which technology a package belongs to**, by `Platform.name`.
132
152
  *
@@ -457,6 +477,16 @@ export declare namespace RmanConfig {
457
477
  command: string;
458
478
  /** Other names the command answers to - `list` is also `ls`. */
459
479
  aliases?: string[];
480
+ /**
481
+ * The technologies this command is for, by `Platform.name` - `['node']` for `clean` and `ci`,
482
+ * which only mean anything in an npm package. Absent means **every** platform, which is what a
483
+ * command that speaks for no one in particular says.
484
+ *
485
+ * It narrows what the command acts on: in a polyglot repository `rman clean` sweeps the node
486
+ * packages and leaves the Cargo ones alone, with no `--platform` and no check inside the
487
+ * command.
488
+ */
489
+ platforms?: string[];
460
490
  /**
461
491
  * yargs parser switches this command needs. `exec` is the only one so far and needs two:
462
492
  * `populate--` to keep everything after `--` out of its own options, and
@@ -898,4 +928,69 @@ type RequiredPositionalsOf<C extends string> = C extends `${string}<${infer N}>$
898
928
  /** A variadic positional (`[command..]`) collects a list; every other one is a single value. The
899
929
  * command string is the only place that says which, since it is what yargs parses. */
900
930
  type PositionalValue<Cmd extends string, N extends string> = Cmd extends `${string}${'<' | '['}${N}..${string}` ? string[] : string;
931
+ /**
932
+ * Identity helper for authoring a `.rmanrc.cjs`/`.mjs`/`.js` config with full type-checking and
933
+ * autocomplete - the same `defineConfig` pattern Vite/Vitest use. Returns `config` completely
934
+ * unchanged; this exists purely so TypeScript can infer/check against `RmanConfig`, not for any
935
+ * runtime behavior:
936
+ *
937
+ * // .rmanrc.mjs
938
+ * import { defineConfig } from 'rman';
939
+ * export default defineConfig({ packageManager: 'pnpm' });
940
+ *
941
+ * // .rmanrc.cjs
942
+ * const { defineConfig } = require('rman');
943
+ * module.exports = defineConfig({ packageManager: 'pnpm' });
944
+ */
945
+ export declare function defineConfig(config: RmanConfig): RmanConfig;
946
+ /**
947
+ * A config value that may be **written as a function instead**, computed per package at the moment
948
+ * the config resolves.
949
+ *
950
+ * `T` is what the function has to return, so the same checking applies either way - measured, with
951
+ * a control: a typo inside a wrapped object is still caught, and so is one inside an object a value
952
+ * function *returns*.
953
+ *
954
+ * **Not for a step key.** `run.<script>.before`/`.exec`/`.after`, `run.<script>.if` and
955
+ * `version.<slot>` already take a function, and it means something else there - code for `run` to
956
+ * call in its own time, with its own context. Wrapping one of those would produce a type that
957
+ * accepts a value function where a step is what actually runs. The key path decides which a
958
+ * function is (`STEP_PATHS`, `CODE_SUBTREES`), and the type can only follow that split by hand.
959
+ */
960
+ export type ConfigValue<T> = T | ((ctx: ConfigValueContext) => T);
961
+ /**
962
+ * The same config **after** it resolves: every `ConfigValue<T>` is just `T`, because
963
+ * `interpolateConfig` has already called it.
964
+ *
965
+ * **This is the half that lets `RmanConfig` be the author's type.** One type cannot answer both
966
+ * "what may I write?" (a function is fine - rman calls it) and "what do I get?" (never a function -
967
+ * it was already called), so it used to answer only the second, and writing a value function was a
968
+ * compile error the docs themselves committed. Widening `RmanConfig` alone just moves the problem:
969
+ * measured, six read sites needed a cast. The author's type widens and the *reader's* is computed
970
+ * from it - one derived type, applied at `Package.config`, rather than a second one to keep in step
971
+ * by hand.
972
+ *
973
+ * **Two guards, and each was measured by leaving it out.**
974
+ *
975
+ * - **Steps are named first.** A value function is recognised by its parameter, and
976
+ * `ConfigValueContext` carries an index signature - so `RunStepFn` is assignable to it and a
977
+ * `run.build.exec` function collapsed to its *return type*, leaving `RunService` nothing to call.
978
+ * - **`CODE_SUBTREES` is skipped, at every level.** `plugins`/`commands`/`publishTargets` hold code
979
+ * all the way down, and the selector index (`[selector]: RmanConfig`) re-enters the config, so a
980
+ * top-level-only guard misses the copy inside a `"[*]"` block. Left out, the walk reached
981
+ * `Plugin.manifestProvider.versionScheme` and rewrote its **methods**: `smallestBump(): string`
982
+ * became `string`, `bumpFor`/`isValid`/`compare`/`next` became `{}`. A function with *fewer*
983
+ * parameters is assignable to one with more, so a zero-argument method matches the value-function
984
+ * pattern - which makes this transform unsafe over any object carrying methods, and the guard the
985
+ * only thing keeping one out of its way.
986
+ *
987
+ * Both lists are the runtime's own (`STEP_PATHS`' function types, `CODE_SUBTREES` itself), so the
988
+ * type follows the rule rather than restating it - the drift `ScopedVars` already demonstrated is
989
+ * not available here.
990
+ */
991
+ export type Resolved<T> = T extends RunStepFn | RunConditionFn ? T : T extends (ctx: ConfigValueContext) => infer R ? R : T extends object ? {
992
+ [K in keyof T]: K extends CodeSubtree ? T[K] : Resolved<T[K]>;
993
+ } : T;
994
+ /** `pkg.config`'s type: what every command reads, with the value functions already called. */
995
+ export type ResolvedConfig = Resolved<RmanConfig>;
901
996
  export {};
@@ -59,3 +59,20 @@ export function registerCommand(def) {
59
59
  export function declareCommand(def) {
60
60
  return def;
61
61
  }
62
+ /**
63
+ * Identity helper for authoring a `.rmanrc.cjs`/`.mjs`/`.js` config with full type-checking and
64
+ * autocomplete - the same `defineConfig` pattern Vite/Vitest use. Returns `config` completely
65
+ * unchanged; this exists purely so TypeScript can infer/check against `RmanConfig`, not for any
66
+ * runtime behavior:
67
+ *
68
+ * // .rmanrc.mjs
69
+ * import { defineConfig } from 'rman';
70
+ * export default defineConfig({ packageManager: 'pnpm' });
71
+ *
72
+ * // .rmanrc.cjs
73
+ * const { defineConfig } = require('rman');
74
+ * module.exports = defineConfig({ packageManager: 'pnpm' });
75
+ */
76
+ export function defineConfig(config) {
77
+ return config;
78
+ }
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.0.0-beta.2",
4
+ "version": "2.0.0-beta.3",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
8
8
  "@netlify/parse-npm-script": "^0.1.2",
9
9
  "@xmldom/xmldom": "^0.9.12",
10
+ "@jsopen/objects": "^2.3.4",
10
11
  "ansi-colors": "^4.1.3",
11
12
  "cross-dirname": "^0.1.0",
12
13
  "easy-table": "^1.2.0",
@@ -1,4 +1,4 @@
1
- import type { Package } from '../core/package.js';
1
+ import type { Package } from '../core/classes/package.js';
2
2
  import type { GitHelper } from '../utils/git.js';
3
3
  /**
4
4
  * Release boundaries and the tag names that mark them - **the single source for both directions**.
@@ -39,11 +39,33 @@ export declare namespace ChangeHashService {
39
39
  * the file doesn't exist. */
40
40
  catchUpFile?: string;
41
41
  }
42
- /** `.rmanrc changelog.tagPattern` (cascaded, per-package overridable) - a glob for this package's
43
- * release tags. `{name}` (if present) is replaced with the package's own name, e.g. `{name}@*`
44
- * for independent per-package versioning (`@scope/pkg@1.2.3`, the same scheme lerna/changesets
45
- * use - `@`/`/` are both fine in a git tag name). Without `{name}`, it's a single repo-wide tag
46
- * shared by every package (e.g. the default `v*`). */
42
+ /**
43
+ * `.rmanrc changelog.tagPattern` (cascaded, per-package overridable) - a glob for this package's
44
+ * release tags. `{name}` (if present) is replaced with the package's own name, e.g. `{name}@*`
45
+ * for independent per-package versioning (`@scope/pkg@1.2.3`, the same scheme lerna/changesets
46
+ * use - `@`/`/` are both fine in a git tag name). Without `{name}`, it's a single repo-wide tag
47
+ * shared by every package (`v*`).
48
+ *
49
+ * **Undeclared, the default is derived from how many version lines the repository has**, not
50
+ * fixed at `v*`. A repo-wide pattern has no `{name}`, so `findLatestTag` resolves it with `git
51
+ * describe --match` - the nearest tag HEAD descends from, whichever package it belongs to. That
52
+ * is exactly right while every package shares one line and **silently wrong** the moment they do
53
+ * not: measured on a two-line repository, releasing `pkg-a` put `v1.1.0` on HEAD, and `pkg-b` -
54
+ * which had a committed, unreleased `fix:` of its own sitting behind that tag - reported
55
+ * `no-change` and shipped nothing. Under `{name}@*` the same repository answers `bump 1.0.0 ->
56
+ * 1.0.1, changed since pkg-b@1.0.0`.
57
+ *
58
+ * So the choice is not a preference and should never have been one to remember: a repo-wide tag
59
+ * can only name a release that the whole repository shares. rman already decides the root's
60
+ * versioning scheme structurally from the same group count (`usesCalendarVersion`) - this is that
61
+ * rule applied to the other half of a release.
62
+ *
63
+ * **The root keeps `v*`.** It is never a member of any group, so the count says nothing about
64
+ * it, and the one caller that asks (`github-release`'s `releaseTagGlob`) only does so when the
65
+ * root is *not* on a calendar version - which is the single-line case, where `v*` is the answer
66
+ * anyway. With several lines the repository's release carries its own `version.releaseTagPattern`
67
+ * instead.
68
+ */
47
69
  function tagPattern(pkg: Package): string;
48
70
  /** This package's most recent release tag - the `{name}`-bearing pattern looks up that package's
49
71
  * *own* tags directly (newest by version sort); a repo-wide pattern instead finds the nearest tag
@@ -51,6 +73,18 @@ export declare namespace ChangeHashService {
51
73
  * tagged at all (a fresh package, or one that's never been released). Shared by `changelog`
52
74
  * (reading the last-documented version) and `version` (finding the boundary a bump measures
53
75
  * "since"). */
76
+ /**
77
+ * **Every pattern that may name this package's releases**, in precedence order - its own, and,
78
+ * only where the pattern was derived, the shared `v*` a repository used before it grew a second
79
+ * version line. One list, because two questions need it and must not disagree: `findLatestTag`
80
+ * picks the boundary off it, and `changelog`'s `splitByRelease` cuts the range at the tags it
81
+ * matches.
82
+ *
83
+ * They *did* disagree, for one commit: the boundary fell back to the shared tag while the split
84
+ * still looked only for `{name}@*`, which a repository mid-transition has none of - so a backfill
85
+ * reaching across twelve releases found no tag to cut at and rendered all of them as one.
86
+ */
87
+ function releaseTagPatterns(pkg: Package): string[];
54
88
  function findLatestTag(git: GitHelper, pkg: Package): Promise<string | undefined>;
55
89
  /** The forward direction of `findLatestTag`: expands `pkg`'s (cascaded) `.rmanrc
56
90
  * changelog.tagPattern` into the concrete tag name `version` belongs under - `{name}` becomes the
@@ -1,4 +1,5 @@
1
- import { Manifest } from '../core/manifest.js';
1
+ import { Manifest } from '../core/interfaces/manifest.js';
2
+ import { versionLineCount } from '../utils/version-group.js';
2
3
  /**
3
4
  * Release boundaries and the tag names that mark them - **the single source for both directions**.
4
5
  *
@@ -16,14 +17,35 @@ export var ChangeHashService;
16
17
  /** The one keyword `from` accepts instead of a ref: "work it out per package". Omitting `from`
17
18
  * means the same thing - this exists so a pipeline can say it explicitly. */
18
19
  ChangeHashService.AUTO = 'auto';
19
- /** `.rmanrc changelog.tagPattern` (cascaded, per-package overridable) - a glob for this package's
20
- * release tags. `{name}` (if present) is replaced with the package's own name, e.g. `{name}@*`
21
- * for independent per-package versioning (`@scope/pkg@1.2.3`, the same scheme lerna/changesets
22
- * use - `@`/`/` are both fine in a git tag name). Without `{name}`, it's a single repo-wide tag
23
- * shared by every package (e.g. the default `v*`). */
20
+ /**
21
+ * `.rmanrc changelog.tagPattern` (cascaded, per-package overridable) - a glob for this package's
22
+ * release tags. `{name}` (if present) is replaced with the package's own name, e.g. `{name}@*`
23
+ * for independent per-package versioning (`@scope/pkg@1.2.3`, the same scheme lerna/changesets
24
+ * use - `@`/`/` are both fine in a git tag name). Without `{name}`, it's a single repo-wide tag
25
+ * shared by every package (`v*`).
26
+ *
27
+ * **Undeclared, the default is derived from how many version lines the repository has**, not
28
+ * fixed at `v*`. A repo-wide pattern has no `{name}`, so `findLatestTag` resolves it with `git
29
+ * describe --match` - the nearest tag HEAD descends from, whichever package it belongs to. That
30
+ * is exactly right while every package shares one line and **silently wrong** the moment they do
31
+ * not: measured on a two-line repository, releasing `pkg-a` put `v1.1.0` on HEAD, and `pkg-b` -
32
+ * which had a committed, unreleased `fix:` of its own sitting behind that tag - reported
33
+ * `no-change` and shipped nothing. Under `{name}@*` the same repository answers `bump 1.0.0 ->
34
+ * 1.0.1, changed since pkg-b@1.0.0`.
35
+ *
36
+ * So the choice is not a preference and should never have been one to remember: a repo-wide tag
37
+ * can only name a release that the whole repository shares. rman already decides the root's
38
+ * versioning scheme structurally from the same group count (`usesCalendarVersion`) - this is that
39
+ * rule applied to the other half of a release.
40
+ *
41
+ * **The root keeps `v*`.** It is never a member of any group, so the count says nothing about
42
+ * it, and the one caller that asks (`github-release`'s `releaseTagGlob`) only does so when the
43
+ * root is *not* on a calendar version - which is the single-line case, where `v*` is the answer
44
+ * anyway. With several lines the repository's release carries its own `version.releaseTagPattern`
45
+ * instead.
46
+ */
24
47
  function tagPattern(pkg) {
25
- const cfg = pkg.config?.changelog;
26
- return typeof cfg?.tagPattern === 'string' && cfg.tagPattern ? cfg.tagPattern : DEFAULT_TAG_PATTERN;
48
+ return resolvePattern(pkg).pattern;
27
49
  }
28
50
  ChangeHashService.tagPattern = tagPattern;
29
51
  /** This package's most recent release tag - the `{name}`-bearing pattern looks up that package's
@@ -32,10 +54,49 @@ export var ChangeHashService;
32
54
  * tagged at all (a fresh package, or one that's never been released). Shared by `changelog`
33
55
  * (reading the last-documented version) and `version` (finding the boundary a bump measures
34
56
  * "since"). */
57
+ /**
58
+ * **Every pattern that may name this package's releases**, in precedence order - its own, and,
59
+ * only where the pattern was derived, the shared `v*` a repository used before it grew a second
60
+ * version line. One list, because two questions need it and must not disagree: `findLatestTag`
61
+ * picks the boundary off it, and `changelog`'s `splitByRelease` cuts the range at the tags it
62
+ * matches.
63
+ *
64
+ * They *did* disagree, for one commit: the boundary fell back to the shared tag while the split
65
+ * still looked only for `{name}@*`, which a repository mid-transition has none of - so a backfill
66
+ * reaching across twelve releases found no tag to cut at and rendered all of them as one.
67
+ */
68
+ function releaseTagPatterns(pkg) {
69
+ const { pattern, derived } = resolvePattern(pkg);
70
+ const expanded = pattern.replace('{name}', pkg.name);
71
+ return derived && expanded !== SHARED_TAG_PATTERN ? [expanded, SHARED_TAG_PATTERN] : [expanded];
72
+ }
73
+ ChangeHashService.releaseTagPatterns = releaseTagPatterns;
35
74
  async function findLatestTag(git, pkg) {
36
- const pattern = tagPattern(pkg);
75
+ const { pattern, derived } = resolvePattern(pkg);
37
76
  const expanded = pattern.replace('{name}', pkg.name);
38
- return pattern.includes('{name}') ? (await git.listTags(expanded))[0] : await git.describeTag(expanded);
77
+ if (!pattern.includes('{name}'))
78
+ return git.describeTag(expanded);
79
+ const own = (await git.listTags(expanded))[0];
80
+ if (own || !derived)
81
+ return own;
82
+ /**
83
+ * **The bridge across the default changing.** A repository that released under `v*` and has
84
+ * since grown a second version line has no `{name}` tag for this package yet - and reading the
85
+ * whole history instead would re-propose everything ever committed. Measured on a real
86
+ * four-package repository: every package came back on `unreleased commits` and three of them
87
+ * jumped a major, because some commit in the full history said `feat!:`.
88
+ *
89
+ * Falling back to the repo-wide tag gives exactly the boundary that *was* correct - before the
90
+ * split, every package genuinely shared it - so the first run after the split reads the same
91
+ * commits it would have read yesterday, and writes a `{name}` tag that every run after it
92
+ * finds directly. One transition, no manual tagging, no invented release.
93
+ *
94
+ * **Only when the pattern was derived.** A repository that asked for `{name}@*` in its own
95
+ * `.rmanrc` has said what names its tags; borrowing a `v*` tag it never asked about could hand
96
+ * a package a boundary belonging to something else entirely, and reading too little is the
97
+ * failure that ships nothing and says nothing.
98
+ */
99
+ return git.describeTag(SHARED_TAG_PATTERN);
39
100
  }
40
101
  ChangeHashService.findLatestTag = findLatestTag;
41
102
  /** The forward direction of `findLatestTag`: expands `pkg`'s (cascaded) `.rmanrc
@@ -109,4 +170,22 @@ export var ChangeHashService;
109
170
  }
110
171
  ChangeHashService.detect = detect;
111
172
  })(ChangeHashService || (ChangeHashService = {}));
112
- const DEFAULT_TAG_PATTERN = 'v*';
173
+ /** One tag for the whole repository - right while every package releases on one line. */
174
+ const SHARED_TAG_PATTERN = 'v*';
175
+ /** One tag per package - the scheme lerna and changesets use, and the only one that can name a
176
+ * release when a repository has more than one version line. */
177
+ const PER_PACKAGE_TAG_PATTERN = '{name}@*';
178
+ /**
179
+ * The pattern plus **where it came from**, which `findLatestTag` needs and callers do not: the
180
+ * bridge to a repo-wide tag applies only where rman changed the answer itself, never over a
181
+ * repository's own declaration.
182
+ */
183
+ function resolvePattern(pkg) {
184
+ const configured = pkg.config?.changelog?.tagPattern;
185
+ if (typeof configured === 'string' && configured)
186
+ return { pattern: configured, derived: false };
187
+ if (pkg.isRoot)
188
+ return { pattern: SHARED_TAG_PATTERN, derived: true };
189
+ const several = versionLineCount(pkg) > 1;
190
+ return { pattern: several ? PER_PACKAGE_TAG_PATTERN : SHARED_TAG_PATTERN, derived: true };
191
+ }
@@ -1,5 +1,5 @@
1
- import type { Package } from '../core/package.js';
2
- import { Service } from '../core/service.js';
1
+ import type { Package } from '../core/classes/package.js';
2
+ import { Service } from '../core/classes/service.js';
3
3
  import { type PackageFilterOptions } from '../utils/package-filter.js';
4
4
  /**
5
5
  * A service class - see `ListService` for the shape and `Service` for the three measured
@@ -89,6 +89,21 @@ export declare namespace ChangelogService {
89
89
  * the number and has to say so, otherwise every entry ends up labelled with the *previous*
90
90
  * release's version. */
91
91
  version?: string;
92
+ /**
93
+ * Set by `generateToFile`; there is no reason for a caller to pass it. It tells `getEntries`
94
+ * that the entries are about to be **appended to a file**, which is what makes the file's own
95
+ * record of where it stopped the right boundary - see `resolveBoundary`. A print run asks a
96
+ * different question and deliberately ignores all of it.
97
+ */
98
+ write?: boolean;
99
+ /** Where this package's changelog begins - a version or release tag, a `YYYY-MM-DD` date, or a
100
+ * commit. Releases below it are left out. `.rmanrc changelog.startingAt` when omitted; see
101
+ * `resolveStartingPoint` for how the three forms are told apart. */
102
+ startingAt?: string;
103
+ /** Whether the not-yet-released commits get an entry - `.rmanrc changelog.unreleased` when
104
+ * omitted, and `true` when neither says. A caller passing `version` always gets it, since that
105
+ * names the release being cut; see `resolveUnreleased`. */
106
+ unreleased?: boolean;
92
107
  }
93
108
  /** One package's (root included) generated changelog entry - what `getEntries`/`generate`
94
109
  * return. */
@@ -100,6 +115,19 @@ export declare namespace ChangelogService {
100
115
  /** `options.version` when the caller gave one, otherwise resolved from git tags rather than
101
116
  * package.json - see `resolveVersion`. */
102
117
  version: string;
118
+ /** The last commit this entry covers. `generateToFile` writes it into the file as the marker
119
+ * the next run starts from - see `Entry.filePath`. */
120
+ documentedUpTo: string;
121
+ /** Every section this entry renders, in the order it renders them - the heading a repository
122
+ * chose through `.rmanrc changelog.titles`, and the lines under it. */
123
+ sections: {
124
+ title: string;
125
+ lines: string[];
126
+ }[];
127
+ /** The three buckets `Entry` carried before headings were configurable, derived from
128
+ * `sections` so they keep meaning what they meant: whatever `feat` and `fix` are listed under,
129
+ * and everything else together. A repository that adds a section of its own finds it in
130
+ * `other` - the only honest slot for it in a shape that has three. */
103
131
  features: string[];
104
132
  fixes: string[];
105
133
  other: string[];
@@ -110,7 +138,7 @@ export declare namespace ChangelogService {
110
138
  filePath: string;
111
139
  }
112
140
  }
113
- declare module '../core/service.js' {
141
+ declare module '../core/classes/service.js' {
114
142
  interface ServiceMap {
115
143
  changelog: ChangelogService;
116
144
  }