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,47 @@
1
+ /**
2
+ * An ordered collection of contributions, held **on the application** rather than in a module.
3
+ *
4
+ * Every registry rman had was a module-level array (`const providers: Provider[] = []` in
5
+ * `manifest.ts`, `workspace.ts`, `bin-path.ts`, `run.service.ts`), which made it process-global:
6
+ * two repositories in one process shared it. The test suite needed a root hook emptying five of
7
+ * them before every single test, and without it whichever spec ran first decided the answer for
8
+ * the rest - so the core appeared to work in tests that had registered nothing.
9
+ *
10
+ * An instance per `RmanApplication` closes the whole class: a new application starts empty, and
11
+ * nothing has to be cleaned up afterwards - the root hook is deleted.
12
+ *
13
+ * **Registry, not service.** The distinction is multiplicity: a registry is for a question whose
14
+ * answer is the *sum* of what was contributed (every provider's bin directories, the first provider
15
+ * that recognizes a directory). A question with exactly one answer is a service, replaceable
16
+ * through `RmanApplication.getService`.
17
+ */
18
+ export class Registry {
19
+ items = [];
20
+ /** Contributions in registration order, which for a plugin is `plugins` declaration order. */
21
+ get all() {
22
+ return this.items;
23
+ }
24
+ get size() {
25
+ return this.items.length;
26
+ }
27
+ /** Idempotent by identity: a plugin may both declare a contribution and register it itself, and
28
+ * the same function arriving twice would make the registry lie about what is in it. */
29
+ add(item) {
30
+ if (!this.items.includes(item))
31
+ this.items.push(item);
32
+ return this;
33
+ }
34
+ /** The first contribution `ask` gives an answer for - "first that recognizes it" resolution,
35
+ * which is how a directory finds the manifest reader and the workspace layout that claim it. */
36
+ first(ask) {
37
+ for (const item of this.items) {
38
+ const answer = ask(item);
39
+ if (answer !== undefined)
40
+ return answer;
41
+ }
42
+ return undefined;
43
+ }
44
+ [Symbol.iterator]() {
45
+ return this.items[Symbol.iterator]();
46
+ }
47
+ }
@@ -1,6 +1,8 @@
1
- import { type ConfigScope, type GitScope, type PackageScope, type RepositoryScope } from './config.js';
2
- import type { LoadedCommand } from './custom-command.js';
1
+ import { type ConfigScope, type GitScope, type PackageScope, type RepositoryScope } from '../../interfaces/config-scope.interface.js';
2
+ import { RmanApplication } from '../application.js';
3
+ import type { Platform } from '../interfaces/plugin.js';
3
4
  import { Package } from './package.js';
5
+ import { Workspace } from './workspace.js';
4
6
  export declare class Repository extends Package {
5
7
  readonly dirname: string;
6
8
  readonly monorepo: boolean;
@@ -12,7 +14,6 @@ export declare class Repository extends Package {
12
14
  readonly rootPackage: Package;
13
15
  /** Commands the repository's plugins contributed, loaded during `create` because the workspace
14
16
  * providers they bring are needed before any package can be found. `cli.ts` registers them. */
15
- pluginCommands: LoadedCommand[];
16
17
  /**
17
18
  * Cached repository scope - see `_repositoryScope`.
18
19
  *
@@ -28,19 +29,33 @@ export declare class Repository extends Package {
28
29
  * and because reading it is a subprocess: a deep walk of a package must not spawn one. */
29
30
  private _git?;
30
31
  /**
31
- * Files `${{ read(...) }}` has parsed, shared by every package's scope and keyed by the identity
32
- * of the bytes - see `readStructuredFile`.
32
+ * What `${{ file... }}` and `${{ read(...) }}` answer from, for every package.
33
33
  *
34
- * **On the repository rather than per scope, and that is the whole point of it**: `configScope`
35
- * is built once per package, so a cache living there would re-read a repository-level file once
36
- * for every package that mentions it.
34
+ * **One per repository rather than one per scope, and that is the whole point of it**:
35
+ * `configScope` is built once per package, so a parse cache living there would re-read a
36
+ * repository-level file once for every package that mentions it. The cache inside is keyed by the
37
+ * identity of the bytes (`mtimeNs:size`), because rman writes JSON while it runs - `version`
38
+ * rewrites every bumped manifest and then re-interpolates its deferred hooks.
37
39
  */
38
- private readonly _readCache;
39
- protected constructor(dirname: string, monorepo: boolean, packages: Package[],
40
+ private readonly _files;
41
+ /**
42
+ * The application this repository belongs to - its services, its technologies, its logger.
43
+ *
44
+ * **Non-enumerable**, like `_repoScope` and `_git` beside it: the two things that walk a
45
+ * repository are `{...pkg}` spreads and the config scope, and an enumerable back-reference to the
46
+ * whole application would be dragged into both. A `Package` deliberately has no such field at
47
+ * all; a repository is never spread or serialized, which is what makes this one safe - measured,
48
+ * rather than assumed.
49
+ */
50
+ readonly app: RmanApplication;
51
+ protected constructor(app: RmanApplication, dirname: string, monorepo: boolean, packages: Package[],
40
52
  /** The directory `Repository.create()` was actually invoked from - unlike `dirname` (the
41
53
  * resolved repository root, possibly several levels up), this is where the user's shell
42
54
  * really was. Used by `currentPackage` to scope commands to "the package I'm standing in". */
43
- cwd?: string);
55
+ cwd?: string,
56
+ /** The root directory's own technology, from the same walk that found the packages - so the
57
+ * repository and its `rootPackage` agree without either searching the registry again. */
58
+ platform?: Platform);
44
59
  /**
45
60
  * The package whose own directory contains `cwd` (the deepest match, so a package nested
46
61
  * inside another's directory resolves to the innermost one) - or `undefined` when `cwd` *is*
@@ -60,9 +75,21 @@ export declare class Repository extends Package {
60
75
  * dirty, the reference point decides the rest: without `hash`, `committed`
61
76
  * means committed but not yet in the upstream branch; with `hash`, `changed`
62
77
  * means it differs from that commit. Otherwise a package is `clean`.
78
+ *
79
+ * **Keyed by `Package.selector`**, which is what addresses a package - it was `name`, and the two
80
+ * coincide wherever a technology names its packages. A repository whose does not had every such
81
+ * package answering to `""`, so one entry stood for all of them.
82
+ *
83
+ * **`includeRoot` is opt-in, and the reason is that the root's answer means something different.**
84
+ * Its directory contains every other package, so the same rule - "does a changed file fall under
85
+ * this directory" - reports `dirty` for the root whenever *anything* in the repository is dirty.
86
+ * That is the honest reading of the rule rather than a bug, and it is not what `run --changed`
87
+ * wants, so only a caller that asked for the root gets it (`rman list`'s table, which shows the
88
+ * root as the tree's own row).
63
89
  */
64
90
  listStatus(options?: {
65
91
  hash?: string;
92
+ includeRoot?: boolean;
66
93
  }): Promise<Record<string, Repository.PackageStatus>>;
67
94
  /**
68
95
  * The scope a `${{ ... }}` expression is evaluated against for `pkg`, optionally with the version
@@ -89,13 +116,29 @@ export declare class Repository extends Package {
89
116
  * nothing under `getPackages()` is the root, so only its own unmarked config applies.
90
117
  */
91
118
  /**
92
- * Gives every package its `repository` and `parent`, before any config is resolved - a config
93
- * expression or a provider may already want to navigate from a package outwards.
119
+ * Gives every package its `repository`, and hangs the `parent`/`children` tree off the walk that
120
+ * found them - before any config is resolved, since a config expression or a provider may already
121
+ * want to navigate from a package outwards.
122
+ *
123
+ * **The containment is read from the tree rather than recomputed from paths.** It used to be an
124
+ * O(n²) sweep comparing every package's directory against every other's and keeping the longest
125
+ * prefix - which is the same question `Workspace.walk` answers on the way down, asked again
126
+ * afterwards with the answer thrown away. Two places deriving one relationship is two places to
127
+ * disagree; there is one now.
128
+ *
129
+ * **`parent` is defined non-enumerably, `children` is a plain field**, which is the one asymmetry
130
+ * here and it is deliberate: a tree is serialized downwards, so `children` has to be walkable and
131
+ * `parent` must not be, or every `JSON.stringify` is a cycle. The same way `Repository.app` and
132
+ * `ORIGINS` travel.
94
133
  *
95
134
  * A repository's own `repository` is itself, which reads oddly and is the honest answer:
96
135
  * `Repository extends Package`, so the repository *is* a package of its own repository.
97
136
  */
98
- protected _linkPackages(): void;
137
+ protected _linkPackages(tree: Workspace.Node, nodes: Workspace.Node[], packages: Package[]): void;
138
+ /**
139
+ * Bakes every package's config: the raw one the workspace cascaded, with every `${{ }}` and value
140
+ * function resolved against that package's scope.
141
+ */
99
142
  protected _resolveConfigs(): Promise<void>;
100
143
  protected _topoSortPackages(packages: Package[]): void;
101
144
  protected _packageScope(pkg: Package, targetVersion?: string): PackageScope;
@@ -131,20 +174,26 @@ export declare class Repository extends Package {
131
174
  * otherwise: the plugins that know what a package is are named in the config file this step
132
175
  * is looking for.
133
176
  * 2. **Load the plugins** the root's config names, which registers their workspace providers
134
- * (and their commands, handed on via `pluginCommands` - `cli.ts` registers those).
177
+ * (their commands arrive through `.rmanrc "commands"`, which `cli.ts` reads).
135
178
  * 3. **Ask the providers** for the layout. None recognizing it means a repository that is itself
136
179
  * the one package.
137
180
  *
138
- * **A repository whose `.rmanrc` names no plugin has no packages beyond itself**, and that is the
181
+ * **A repository no technology recognizes has no packages beyond itself**, and that is the
139
182
  * boundary working rather than failing: `workspaces` in a `package.json` is npm's idea, so it
140
- * takes `plugins: ['rman-node']` to be read as one.
183
+ * takes a platform that reads one for the directory to be a workspace at all. rman's own presets
184
+ * are laid under every root (see `DEFAULT_PRESETS`), so a plain Node repository needs no config
185
+ * file to say so; anything else arrives through `extends` or `platforms`.
141
186
  */
142
187
  static create(root?: string, options?: {
143
188
  deep?: number;
189
+ app?: RmanApplication;
190
+ presets?: readonly string[];
144
191
  }): Promise<Repository>;
145
- /** Finishes constructing `repo` with the async work a constructor can't do itself - resolving
146
- * `.rmanrc`/`.rmanrc.yml`/`.rmanrc.cjs`/`.mjs`/`.js` config (which may need a dynamic `import()`)
147
- * before the dependency graph is built from it. */
192
+ /**
193
+ * Takes the workspace's packages as this repository's own - the back-references a `Package` needs
194
+ * and the parent/child edges the walk established.
195
+ */
196
+ protected _adopt(workspace: Workspace): void;
148
197
  private static _init;
149
198
  }
150
199
  export declare namespace Repository {
@@ -1,11 +1,14 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
  import path from 'path';
3
3
  import semver from 'semver';
4
- import { GitHelper } from '../utils/git.js';
5
- import { createFileScope, createReadScope, DEFERRED_PATHS, interpolateConfig, readDirConfig, resolveConfig, } from './config.js';
6
- import { Manifest } from './manifest.js';
4
+ import {} from '../../interfaces/config-scope.interface.js';
5
+ import { GitHelper } from '../../utils/git.js';
6
+ import { RmanApplication } from '../application.js';
7
+ import { ConfigFileScope } from '../config/config-file-scope.js';
8
+ import { ConfigInterpolator } from '../config/config-interpolator.js';
9
+ import { DEFERRED_PATHS } from '../config/config-paths.js';
10
+ import { Manifest } from '../interfaces/manifest.js';
7
11
  import { Package } from './package.js';
8
- import { loadPlugins } from './plugin.js';
9
12
  import { Workspace } from './workspace.js';
10
13
  export class Repository extends Package {
11
14
  dirname;
@@ -15,7 +18,6 @@ export class Repository extends Package {
15
18
  rootPackage;
16
19
  /** Commands the repository's plugins contributed, loaded during `create` because the workspace
17
20
  * providers they bring are needed before any package can be found. `cli.ts` registers them. */
18
- pluginCommands = [];
19
21
  /**
20
22
  * Cached repository scope - see `_repositoryScope`.
21
23
  *
@@ -31,25 +33,40 @@ export class Repository extends Package {
31
33
  * and because reading it is a subprocess: a deep walk of a package must not spawn one. */
32
34
  _git;
33
35
  /**
34
- * Files `${{ read(...) }}` has parsed, shared by every package's scope and keyed by the identity
35
- * of the bytes - see `readStructuredFile`.
36
+ * What `${{ file... }}` and `${{ read(...) }}` answer from, for every package.
36
37
  *
37
- * **On the repository rather than per scope, and that is the whole point of it**: `configScope`
38
- * is built once per package, so a cache living there would re-read a repository-level file once
39
- * for every package that mentions it.
38
+ * **One per repository rather than one per scope, and that is the whole point of it**:
39
+ * `configScope` is built once per package, so a parse cache living there would re-read a
40
+ * repository-level file once for every package that mentions it. The cache inside is keyed by the
41
+ * identity of the bytes (`mtimeNs:size`), because rman writes JSON while it runs - `version`
42
+ * rewrites every bumped manifest and then re-interpolates its deferred hooks.
40
43
  */
41
- _readCache = new Map();
42
- constructor(dirname, monorepo, packages,
44
+ _files = new ConfigFileScope();
45
+ /**
46
+ * The application this repository belongs to - its services, its technologies, its logger.
47
+ *
48
+ * **Non-enumerable**, like `_repoScope` and `_git` beside it: the two things that walk a
49
+ * repository are `{...pkg}` spreads and the config scope, and an enumerable back-reference to the
50
+ * whole application would be dragged into both. A `Package` deliberately has no such field at
51
+ * all; a repository is never spread or serialized, which is what makes this one safe - measured,
52
+ * rather than assumed.
53
+ */
54
+ app;
55
+ constructor(app, dirname, monorepo, packages,
43
56
  /** The directory `Repository.create()` was actually invoked from - unlike `dirname` (the
44
57
  * resolved repository root, possibly several levels up), this is where the user's shell
45
58
  * really was. Used by `currentPackage` to scope commands to "the package I'm standing in". */
46
- cwd = dirname) {
47
- super(dirname);
59
+ cwd = dirname,
60
+ /** The root directory's own technology, from the same walk that found the packages - so the
61
+ * repository and its `rootPackage` agree without either searching the registry again. */
62
+ platform) {
63
+ super(dirname, app, platform);
48
64
  this.dirname = dirname;
49
65
  this.monorepo = monorepo;
50
66
  this.packages = packages;
51
67
  this.cwd = cwd;
52
- this.rootPackage = new Package(dirname);
68
+ Object.defineProperty(this, 'app', { value: app, enumerable: false, writable: false });
69
+ this.rootPackage = new Package(dirname, app, platform);
53
70
  if (!monorepo)
54
71
  this.packages = [this.rootPackage];
55
72
  // Config resolution can load a `.rmanrc.cjs`/`.mjs`/`.js` module (dynamic `import()`, always
@@ -94,11 +111,22 @@ export class Repository extends Package {
94
111
  * dirty, the reference point decides the rest: without `hash`, `committed`
95
112
  * means committed but not yet in the upstream branch; with `hash`, `changed`
96
113
  * means it differs from that commit. Otherwise a package is `clean`.
114
+ *
115
+ * **Keyed by `Package.selector`**, which is what addresses a package - it was `name`, and the two
116
+ * coincide wherever a technology names its packages. A repository whose does not had every such
117
+ * package answering to `""`, so one entry stood for all of them.
118
+ *
119
+ * **`includeRoot` is opt-in, and the reason is that the root's answer means something different.**
120
+ * Its directory contains every other package, so the same rule - "does a changed file fall under
121
+ * this directory" - reports `dirty` for the root whenever *anything* in the repository is dirty.
122
+ * That is the honest reading of the rule rather than a bug, and it is not what `run --changed`
123
+ * wants, so only a caller that asked for the root gets it (`rman list`'s table, which shows the
124
+ * root as the tree's own row).
97
125
  */
98
126
  async listStatus(options) {
99
127
  const hash = options?.hash;
100
128
  const git = new GitHelper({ cwd: this.dirname });
101
- const packages = this.getPackages();
129
+ const packages = options?.includeRoot ? [this.rootPackage, ...this.getPackages()] : this.getPackages();
102
130
  const belongsTo = (p, files) => files.some(f => !path.relative(p.dirname, f).startsWith('..'));
103
131
  const [dirtyFiles, referenceFiles] = await Promise.all([
104
132
  git.listDirtyFiles({ absolute: true }),
@@ -107,11 +135,11 @@ export class Repository extends Package {
107
135
  const result = {};
108
136
  for (const p of packages) {
109
137
  if (belongsTo(p, dirtyFiles))
110
- result[p.name] = 'dirty';
138
+ result[p.selector] = 'dirty';
111
139
  else if (belongsTo(p, referenceFiles))
112
- result[p.name] = hash ? 'changed' : 'committed';
140
+ result[p.selector] = hash ? 'changed' : 'committed';
113
141
  else
114
- result[p.name] = 'clean';
142
+ result[p.selector] = 'clean';
115
143
  }
116
144
  return result;
117
145
  }
@@ -131,10 +159,10 @@ export class Repository extends Package {
131
159
  repository: this._repositoryScope(),
132
160
  /** `pkg.dirname`, not the repository root: a `"[*]"` block asking whether
133
161
  * `tsconfig-build.json` exists has to be answered per package. */
134
- file: createFileScope(pkg.dirname),
162
+ file: this._files.fileScope(pkg.dirname),
135
163
  /** Same base directory as `file`, so one `"[*]"` declaration reads each package's own copy -
136
164
  * and the cache is the repository's, so a file they *share* is parsed once. */
137
- read: createReadScope(pkg.dirname, this._readCache),
165
+ read: this._files.readScope(pkg.dirname),
138
166
  env: { ...process.env },
139
167
  semver,
140
168
  path,
@@ -173,52 +201,71 @@ export class Repository extends Package {
173
201
  * nothing under `getPackages()` is the root, so only its own unmarked config applies.
174
202
  */
175
203
  /**
176
- * Gives every package its `repository` and `parent`, before any config is resolved - a config
177
- * expression or a provider may already want to navigate from a package outwards.
204
+ * Gives every package its `repository`, and hangs the `parent`/`children` tree off the walk that
205
+ * found them - before any config is resolved, since a config expression or a provider may already
206
+ * want to navigate from a package outwards.
207
+ *
208
+ * **The containment is read from the tree rather than recomputed from paths.** It used to be an
209
+ * O(n²) sweep comparing every package's directory against every other's and keeping the longest
210
+ * prefix - which is the same question `Workspace.walk` answers on the way down, asked again
211
+ * afterwards with the answer thrown away. Two places deriving one relationship is two places to
212
+ * disagree; there is one now.
213
+ *
214
+ * **`parent` is defined non-enumerably, `children` is a plain field**, which is the one asymmetry
215
+ * here and it is deliberate: a tree is serialized downwards, so `children` has to be walkable and
216
+ * `parent` must not be, or every `JSON.stringify` is a cycle. The same way `Repository.app` and
217
+ * `ORIGINS` travel.
178
218
  *
179
219
  * A repository's own `repository` is itself, which reads oddly and is the honest answer:
180
220
  * `Repository extends Package`, so the repository *is* a package of its own repository.
181
221
  */
182
- _linkPackages() {
183
- this.repository = this;
184
- this.rootPackage.repository = this;
185
- for (const pkg of this.packages) {
186
- pkg.repository = this;
187
- /** The deepest package that strictly contains it - the root for an ordinary member, an
188
- * enclosing package for a nested one. Longest containing path wins, the same rule
189
- * `currentPackage` uses to resolve "the package I am standing in". */
190
- let parent = this.monorepo ? this.rootPackage : undefined;
191
- for (const other of this.packages) {
192
- if (other === pkg)
193
- continue;
194
- const rel = path.relative(other.dirname, pkg.dirname);
195
- const contains = !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
196
- if (contains && (!parent || other.dirname.length > parent.dirname.length))
197
- parent = other;
222
+ _linkPackages(tree, nodes, packages) {
223
+ /** Non-enumerable everywhere, including on the repository itself - see `Package.repository`.
224
+ * `writable` because `import` grafts an external repository's packages onto this one. */
225
+ const link = (pkg) => {
226
+ Object.defineProperty(pkg, 'repository', { value: this, enumerable: false, configurable: true, writable: true });
227
+ };
228
+ link(this);
229
+ link(this.rootPackage);
230
+ for (const pkg of this.packages)
231
+ link(pkg);
232
+ /** The walk visits a directory once, so one node is one package and this map is a bijection -
233
+ * which is what lets the edges below be read off the tree instead of guessed from paths. */
234
+ const byNode = new Map(nodes.map((node, i) => [node, packages[i]]));
235
+ byNode.set(tree, this.rootPackage);
236
+ for (const node of [tree, ...nodes]) {
237
+ const pkg = byNode.get(node);
238
+ for (const childNode of node.children) {
239
+ const child = byNode.get(childNode);
240
+ pkg.children.push(child);
241
+ Object.defineProperty(child, 'parent', { value: pkg, enumerable: false, configurable: true });
198
242
  }
199
- pkg.parent = pkg === this.rootPackage ? undefined : parent;
200
243
  }
244
+ /** `Repository extends Package` while holding a separate `rootPackage` for the same directory,
245
+ * so both are truthfully the root - they share the one array rather than each getting a copy
246
+ * that could drift. */
247
+ Object.defineProperty(this, 'children', { value: this.rootPackage.children, enumerable: true });
201
248
  }
249
+ /**
250
+ * Bakes every package's config: the raw one the workspace cascaded, with every `${{ }}` and value
251
+ * function resolved against that package's scope.
252
+ */
253
+ /* **The one step the workspace deliberately stops short of.** Resolving an expression needs
254
+ * `pkg`, `repository`, `file` and `git` - which belong to a repository rather than to its layout,
255
+ * and `configScope` is where the measured subtleties of building that scope live.
256
+ *
257
+ * `DEFERRED_PATHS` are left raw, so the resolved config is the only copy anyone needs - `version`
258
+ * reads its own hooks straight off it and interpolates them once `pkg.targetVersion` exists. */
202
259
  async _resolveConfigs() {
203
- const cache = new Map();
204
- /**
205
- * `DEFERRED_PATHS` are left *raw* in the result (see `interpolateConfig`'s `walk`), so the
206
- * resolved config is the only copy anyone needs - `version` reads its own hooks straight off
207
- * `config` and interpolates them itself once `pkg.targetVersion` exists. There used to be a
208
- * second `rawConfig` copy of every package's config for that one reader; measured identical.
209
- */
210
- /**
211
- * The root is resolved **with its name**, like every other package. Not because a glob could
212
- * match it - `"[*]"` and every other name pattern speak only to the packages below - but because
213
- * `resolveConfig` needs a name to run `matchingSelectors` at all, and `"[/]"` is a selector.
214
- * Passing none would silently drop the root's own block.
215
- */
216
- const rootRaw = await resolveConfig(this.dirname, this.dirname, cache, this.rootPackage.name);
217
- this.config = interpolateConfig(rootRaw, this.configScope(this.rootPackage), { skip: DEFERRED_PATHS });
218
- for (const pkg of this.packages) {
219
- const raw = await resolveConfig(this.dirname, pkg.dirname, cache, pkg.name);
220
- pkg.config = interpolateConfig(raw, this.configScope(pkg), { skip: DEFERRED_PATHS });
221
- }
260
+ const interpolator = new ConfigInterpolator();
261
+ const bake = (pkg) => interpolator.interpolate({
262
+ config: pkg.rawConfig,
263
+ scope: this.configScope(pkg),
264
+ skip: DEFERRED_PATHS,
265
+ });
266
+ this.config = bake(this.rootPackage);
267
+ for (const pkg of this.packages)
268
+ pkg.config = bake(pkg);
222
269
  if (this.monorepo)
223
270
  this.rootPackage.config = this.config;
224
271
  }
@@ -236,7 +283,7 @@ export class Repository extends Package {
236
283
  // than refusing it - so the scope has to survive one too.
237
284
  const name = pkg.name ?? '';
238
285
  /** Splitting `@scope/name` is npm's convention, not a universal - the provider decides. */
239
- const { scope: nameScope, unscopedName } = Manifest.splitName(pkg.dirname, name);
286
+ const { scope: nameScope, unscopedName } = Manifest.splitName(this.app, pkg.dirname, name);
240
287
  const scope = {
241
288
  name,
242
289
  scope: nameScope,
@@ -384,34 +431,107 @@ export class Repository extends Package {
384
431
  * otherwise: the plugins that know what a package is are named in the config file this step
385
432
  * is looking for.
386
433
  * 2. **Load the plugins** the root's config names, which registers their workspace providers
387
- * (and their commands, handed on via `pluginCommands` - `cli.ts` registers those).
434
+ * (their commands arrive through `.rmanrc "commands"`, which `cli.ts` reads).
388
435
  * 3. **Ask the providers** for the layout. None recognizing it means a repository that is itself
389
436
  * the one package.
390
437
  *
391
- * **A repository whose `.rmanrc` names no plugin has no packages beyond itself**, and that is the
438
+ * **A repository no technology recognizes has no packages beyond itself**, and that is the
392
439
  * boundary working rather than failing: `workspaces` in a `package.json` is npm's idea, so it
393
- * takes `plugins: ['rman-node']` to be read as one.
440
+ * takes a platform that reads one for the directory to be a workspace at all. rman's own presets
441
+ * are laid under every root (see `DEFAULT_PRESETS`), so a plain Node repository needs no config
442
+ * file to say so; anything else arrives through `extends` or `platforms`.
394
443
  */
444
+ /* **`presets` is an opt-out, and it exists for the specs that must not have one.** rman's own
445
+ * presets are laid under every root, so a caller that brought its own ecosystem would otherwise
446
+ * get the `node` technology, `clean`, `ci` and the npm publish target beside it. That is right
447
+ * for a repository and wrong for a synthetic one: measured, the core's `publish` specs ran a
448
+ * **real `npm publish` against registry.npmjs.org**, and only a 404 stopped it - the failure
449
+ * `useLocalBin`'s doc already records for `docker`, arriving by a new route. `test/_fixture.ts`
450
+ * passes `[]`. */
395
451
  static async create(root, options) {
396
452
  const from = root || process.cwd();
397
453
  const rootDir = Workspace.findRoot(from, options?.deep ?? 10);
398
- /** The root's own config, raw: `plugins` is a list of package names, so it needs neither the
399
- * package list (which does not exist yet) nor expression interpolation. */
400
- const rootConfig = await readDirConfig(rootDir);
401
- const pluginCommands = await loadPlugins(rootDir, rootConfig);
402
- const layout = Workspace.resolve(rootDir);
403
- const packages = (layout?.packageDirs ?? []).map(dir => new Package(dir));
404
- const repo = new Repository(layout?.root ?? rootDir, packages.length > 0, packages, from);
405
- repo.pluginCommands = pluginCommands;
406
- repo._linkPackages();
454
+ const app = options?.app ?? new RmanApplication();
455
+ /**
456
+ * **The whole of discovery and config reading is the workspace's.** It reads each level's
457
+ * config, loads whatever that level names, decides which technology claims each directory,
458
+ * assigns the selectors and cascades every package's **raw** config.
459
+ *
460
+ * Technologies already on the application are handed over: a programmatic caller - and every
461
+ * spec in this suite - registers one without writing a config, and the walk has to see it.
462
+ */
463
+ const workspace = await Workspace.create(rootDir, {
464
+ app,
465
+ deep: options?.deep,
466
+ platforms: [...app.platforms],
467
+ presets: options?.presets,
468
+ });
469
+ /** What the configs named, onto the application the commands read from. */
470
+ for (const platform of workspace.platforms)
471
+ app.platforms.add(platform);
472
+ for (const plugin of workspace.plugins) {
473
+ app.plugins.add(plugin);
474
+ await plugin.afterInitApplication?.({ app });
475
+ }
476
+ /** One loop, because `workspace.platforms` already holds every technology in play whatever
477
+ * route it arrived by. There used to be a second one over each plugin's own `platforms`, and
478
+ * by the end it was pure duplication of this. */
479
+ for (const platform of workspace.platforms) {
480
+ if (platform.versionPlanner)
481
+ app.versionPlanner = platform.versionPlanner;
482
+ }
483
+ /** **Publish targets the root config declared.** `publish` builds its `--target` choices from
484
+ * `app.publishTargets` when its factory runs, so they have to be there before the commands
485
+ * are. Read off the root's own level rather than a package's: a target is the repository's. */
486
+ for (const target of toList(workspace.rootPackage.rawConfig.publishTargets)) {
487
+ if (!target || typeof target !== 'object' || typeof target.name !== 'string') {
488
+ throw new Error(`"publishTargets" takes a target or a glob naming modules that export one - got ` +
489
+ `${target === null ? 'null' : typeof target}.`);
490
+ }
491
+ app.publishTargets.add(target);
492
+ }
493
+ const packages = workspace.packages;
494
+ const repo = new Repository(app, rootDir, packages.length > 0, packages, from, workspace.rootPackage.platform);
495
+ repo._adopt(workspace);
496
+ app.attachRepository(repo);
407
497
  return Repository._init(repo);
408
498
  }
409
- /** Finishes constructing `repo` with the async work a constructor can't do itself - resolving
410
- * `.rmanrc`/`.rmanrc.yml`/`.rmanrc.cjs`/`.mjs`/`.js` config (which may need a dynamic `import()`)
411
- * before the dependency graph is built from it. */
499
+ /**
500
+ * Takes the workspace's packages as this repository's own - the back-references a `Package` needs
501
+ * and the parent/child edges the walk established.
502
+ */
503
+ /* `Repository extends Package` while the workspace built its own root package for the same
504
+ * directory, so both are, truthfully, the root. This keeps the workspace's - it is the one the
505
+ * walk handed a platform and a selector - and leaves `Repository` itself pointing at the same
506
+ * children. */
507
+ _adopt(workspace) {
508
+ const link = (pkg) => void Object.defineProperty(pkg, 'repository', {
509
+ value: this,
510
+ enumerable: false,
511
+ configurable: true,
512
+ writable: true,
513
+ });
514
+ link(this);
515
+ this.rootPackage = workspace.rootPackage;
516
+ link(this.rootPackage);
517
+ for (const pkg of this.packages)
518
+ link(pkg);
519
+ if (!this.monorepo)
520
+ this.packages = [this.rootPackage];
521
+ Object.defineProperty(this, 'children', { value: this.rootPackage.children, enumerable: true });
522
+ }
412
523
  static async _init(repo) {
413
524
  await repo._resolveConfigs();
414
525
  repo._updateDependencies();
526
+ /** **Last, and that is the whole point of the second stage.** Every package is found, every
527
+ * config is baked and the dependency graph is linked - so a plugin handed this repository can
528
+ * read it rather than being told to wait for a command it may not contribute. */
529
+ for (const plugin of repo.app.plugins)
530
+ await plugin.afterInitRepository?.({ app: repo.app, repository: repo });
415
531
  return repo;
416
532
  }
417
533
  }
534
+ /** A key that takes one value or a list of them, as a list. */
535
+ function toList(value) {
536
+ return value === undefined ? [] : Array.isArray(value) ? value : [value];
537
+ }
@@ -0,0 +1,49 @@
1
+ import type { Logger } from '../../utils/logger.js';
2
+ import type { RmanApplication } from '../application.js';
3
+ import type { Repository } from './repository.js';
4
+ /**
5
+ * **The one service shape.** Everything that does work on a repository is a class extending this,
6
+ * constructed once per `RmanApplication` and reached through `app.getService(name)`.
7
+ *
8
+ * rman's services were `export namespace` blocks, and the split between those and the two classes
9
+ * (`VersionPlanService`, `VersionScheme`) was historical rather than principled: whatever needed to
10
+ * be *extended* became a class, and the rest stayed a namespace. Three consequences, all of them
11
+ * measured in this repository:
12
+ *
13
+ * - **A namespace that needs state can only have global state.** `RunService` is a namespace and
14
+ * acquired three module-level variables - `stepSources` and two "already warned" sets - which is
15
+ * why a warning printed for one repository was suppressed for the next one in the same process.
16
+ * - **A namespace cannot be extended.** The two seams a plugin has to specialize
17
+ * (`NodeVersionPlanService extends VersionPlanService`) are precisely the two that are classes.
18
+ * - **A namespace has to be stubbed by mutating the module**, and CLAUDE.md records that failing:
19
+ * a spec that captured a core function at module scope and restored it put the *un-augmented*
20
+ * version back for the rest of the process and broke a spec two files away. An instance is
21
+ * rebuilt per application, so a stub dies with it.
22
+ *
23
+ * **`repository` is no longer a parameter.** Nearly every service method took it first; it now
24
+ * comes from the application, which is what made the argument redundant in the first place.
25
+ *
26
+ * Pure functions of their arguments stay plain exported functions rather than becoming services -
27
+ * `ConventionalCommitsService.parseSubject('feat: x')` parses a string and will never hold state,
28
+ * and making it `app.getService('conventionalCommits').parseSubject(...)` would be ceremony that
29
+ * also puts an application between a caller and a parser. The line is whether it needs the
30
+ * repository.
31
+ */
32
+ export declare abstract class Service {
33
+ protected readonly app: RmanApplication;
34
+ constructor(app: RmanApplication);
35
+ protected get repository(): Repository;
36
+ protected get logger(): Logger;
37
+ }
38
+ /**
39
+ * Every service the application can hand out, by name.
40
+ *
41
+ * Declaration-merged, like `RmanConfigKeys`: the core declares its own here, and a plugin adds its
42
+ * own from its own package with `declare module 'rman'`. That is what keeps `getService` typed -
43
+ * a bare `Map<string, Service>` would need a cast at every call and would turn a misspelled name
44
+ * into a runtime failure instead of a compile error.
45
+ */
46
+ export interface ServiceMap {
47
+ }
48
+ /** How a service is built when the application first needs it. */
49
+ export type ServiceFactory<T> = (app: RmanApplication) => T;