rman 1.2.5 → 2.0.0-beta.2

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 (153) hide show
  1. package/README.md +63 -19
  2. package/cli.d.ts +5 -0
  3. package/cli.js +214 -88
  4. package/commands/build.command.d.ts +177 -3
  5. package/commands/build.command.js +20 -10
  6. package/commands/changed.command.d.ts +80 -3
  7. package/commands/changed.command.js +19 -12
  8. package/commands/changelog.command.d.ts +192 -3
  9. package/commands/changelog.command.js +87 -43
  10. package/commands/config.command.d.ts +44 -3
  11. package/commands/config.command.js +30 -19
  12. package/commands/diff.command.d.ts +37 -3
  13. package/commands/diff.command.js +25 -16
  14. package/commands/exec.command.d.ts +193 -3
  15. package/commands/exec.command.js +60 -58
  16. package/commands/github-release.command.d.ts +154 -3
  17. package/commands/github-release.command.js +67 -37
  18. package/commands/import.command.d.ts +36 -3
  19. package/commands/import.command.js +28 -20
  20. package/commands/info.command.d.ts +35 -7
  21. package/commands/info.command.js +36 -30
  22. package/commands/list.command.d.ts +163 -3
  23. package/commands/list.command.js +109 -71
  24. package/commands/publish.command.d.ts +231 -0
  25. package/commands/publish.command.js +304 -0
  26. package/commands/run.command.d.ts +186 -6
  27. package/commands/run.command.js +26 -72
  28. package/commands/test.command.d.ts +173 -3
  29. package/commands/test.command.js +16 -10
  30. package/commands/version.command.d.ts +317 -3
  31. package/commands/version.command.js +149 -69
  32. package/commands.d.ts +32 -0
  33. package/commands.js +28 -0
  34. package/constants.js +1 -1
  35. package/core/application.d.ts +116 -0
  36. package/core/application.js +143 -0
  37. package/core/command-builder.d.ts +14 -0
  38. package/core/command-builder.js +78 -0
  39. package/core/config.d.ts +180 -50
  40. package/core/config.js +332 -153
  41. package/core/core-services.d.ts +14 -0
  42. package/core/core-services.js +30 -0
  43. package/core/core-targets.d.ts +14 -0
  44. package/core/core-targets.js +16 -0
  45. package/core/custom-command.d.ts +42 -6
  46. package/core/custom-command.js +44 -17
  47. package/core/extends-config.d.ts +13 -5
  48. package/core/extends-config.js +52 -12
  49. package/core/load-config-module.d.ts +28 -0
  50. package/core/load-config-module.js +42 -0
  51. package/core/manifest.d.ts +47 -25
  52. package/core/manifest.js +51 -69
  53. package/core/merge-config.d.ts +33 -34
  54. package/core/merge-config.js +138 -93
  55. package/core/package.d.ts +145 -17
  56. package/core/package.js +128 -36
  57. package/core/plugin-loader.d.ts +65 -0
  58. package/core/plugin-loader.js +234 -0
  59. package/core/plugin.d.ts +135 -90
  60. package/core/plugin.js +70 -173
  61. package/core/publish-target.d.ts +124 -0
  62. package/core/publish-target.js +30 -0
  63. package/core/registry.d.ts +30 -0
  64. package/core/registry.js +47 -0
  65. package/core/repository.d.ts +72 -9
  66. package/core/repository.js +293 -43
  67. package/core/resolve-target.d.ts +1 -1
  68. package/core/resolve-target.js +1 -1
  69. package/core/service.d.ts +49 -0
  70. package/core/service.js +40 -0
  71. package/core/version-scheme.d.ts +23 -1
  72. package/core/version-scheme.js +29 -1
  73. package/core/workspace.d.ts +84 -33
  74. package/core/workspace.js +63 -22
  75. package/index.d.ts +111 -14
  76. package/index.js +85 -9
  77. package/interfaces/rman-config.interface.d.ts +739 -212
  78. package/interfaces/rman-config.interface.js +61 -1
  79. package/package.json +2 -1
  80. package/plugins/builtins.d.ts +44 -0
  81. package/plugins/builtins.js +33 -0
  82. package/plugins/detect.d.ts +78 -0
  83. package/plugins/detect.js +70 -0
  84. package/plugins/node/augmentation/rman.augmentation.d.ts +84 -0
  85. package/plugins/node/augmentation/rman.augmentation.js +1 -0
  86. package/plugins/node/augmentation/system-info.augmentation.d.ts +26 -0
  87. package/plugins/node/augmentation/system-info.augmentation.js +79 -0
  88. package/plugins/node/commands/ci.command.d.ts +131 -0
  89. package/plugins/node/commands/ci.command.js +59 -0
  90. package/plugins/node/commands/clean.command.d.ts +183 -0
  91. package/plugins/node/commands/clean.command.js +73 -0
  92. package/plugins/node/index.d.ts +29 -0
  93. package/plugins/node/index.js +40 -0
  94. package/plugins/node/node-config.interface.d.ts +77 -0
  95. package/plugins/node/node-config.interface.js +7 -0
  96. package/plugins/node/node-manifest.provider.d.ts +68 -0
  97. package/plugins/node/node-manifest.provider.js +125 -0
  98. package/plugins/node/node.platform.d.ts +53 -0
  99. package/plugins/node/node.platform.js +134 -0
  100. package/plugins/node/npm-publish-target.d.ts +73 -0
  101. package/plugins/node/npm-publish-target.js +96 -0
  102. package/plugins/node/services/ci.service.d.ts +47 -0
  103. package/plugins/node/services/ci.service.js +213 -0
  104. package/plugins/node/services/clean.service.d.ts +53 -0
  105. package/plugins/node/services/clean.service.js +237 -0
  106. package/plugins/node/services/publish.service.d.ts +114 -0
  107. package/plugins/node/services/publish.service.js +371 -0
  108. package/plugins/node/services/version-plan.service.d.ts +44 -0
  109. package/plugins/node/services/version-plan.service.js +58 -0
  110. package/plugins/node/utils/npm-view.d.ts +48 -0
  111. package/plugins/node/utils/npm-view.js +71 -0
  112. package/plugins/node/utils/workspace-range.d.ts +26 -0
  113. package/plugins/node/utils/workspace-range.js +28 -0
  114. package/services/change-hash.service.d.ts +2 -2
  115. package/services/change-hash.service.js +2 -2
  116. package/services/changelog.service.d.ts +62 -51
  117. package/services/changelog.service.js +14 -11
  118. package/services/docker-publish.service.d.ts +50 -29
  119. package/services/docker-publish.service.js +43 -20
  120. package/services/exec.service.d.ts +23 -12
  121. package/services/exec.service.js +14 -9
  122. package/services/github-release.service.d.ts +44 -33
  123. package/services/github-release.service.js +13 -10
  124. package/services/import.service.d.ts +25 -14
  125. package/services/import.service.js +9 -5
  126. package/services/list.service.d.ts +62 -10
  127. package/services/list.service.js +62 -15
  128. package/services/run.service.d.ts +22 -13
  129. package/services/run.service.js +262 -223
  130. package/services/version-plan.service.d.ts +27 -4
  131. package/services/version-plan.service.js +42 -15
  132. package/services/version.service.d.ts +31 -11
  133. package/services/version.service.js +29 -13
  134. package/targets/docker.target.d.ts +53 -0
  135. package/targets/docker.target.js +40 -0
  136. package/utils/bin-path.d.ts +6 -7
  137. package/utils/bin-path.js +7 -18
  138. package/utils/branch-guard.d.ts +29 -0
  139. package/utils/branch-guard.js +31 -0
  140. package/utils/exec.d.ts +10 -0
  141. package/utils/exec.js +1 -1
  142. package/utils/logger.d.ts +1 -1
  143. package/utils/logger.js +1 -1
  144. package/utils/package-filter.d.ts +127 -7
  145. package/utils/package-filter.js +197 -16
  146. package/utils/printable-config.d.ts +1 -1
  147. package/utils/printable-config.js +1 -1
  148. package/utils/run-bin.d.ts +10 -0
  149. package/utils/run-bin.js +1 -1
  150. package/utils/run-options.d.ts +97 -0
  151. package/utils/run-options.js +81 -0
  152. package/utils/version-stamp.d.ts +1 -1
  153. package/utils/version-stamp.js +1 -1
@@ -1,6 +1,10 @@
1
+ import type { RmanConfig } from '../interfaces/rman-config.interface.js';
2
+ import { type DetectedBuiltin } from '../plugins/detect.js';
3
+ import { RmanApplication } from './application.js';
1
4
  import { type ConfigScope, type GitScope, type PackageScope, type RepositoryScope } from './config.js';
2
- import type { LoadedCommand } from './custom-command.js';
3
5
  import { Package } from './package.js';
6
+ import type { Platform } from './plugin.js';
7
+ import { Workspace } from './workspace.js';
4
8
  export declare class Repository extends Package {
5
9
  readonly dirname: string;
6
10
  readonly monorepo: boolean;
@@ -10,9 +14,10 @@ export declare class Repository extends Package {
10
14
  * really was. Used by `currentPackage` to scope commands to "the package I'm standing in". */
11
15
  readonly cwd: string;
12
16
  readonly rootPackage: Package;
17
+ /** See the `defineProperty` in the constructor - what detection supplied, when it did. */
18
+ readonly detectedBuiltin?: DetectedBuiltin;
13
19
  /** Commands the repository's plugins contributed, loaded during `create` because the workspace
14
20
  * providers they bring are needed before any package can be found. `cli.ts` registers them. */
15
- pluginCommands: LoadedCommand[];
16
21
  /**
17
22
  * Cached repository scope - see `_repositoryScope`.
18
23
  *
@@ -36,11 +41,24 @@ export declare class Repository extends Package {
36
41
  * for every package that mentions it.
37
42
  */
38
43
  private readonly _readCache;
39
- protected constructor(dirname: string, monorepo: boolean, packages: Package[],
44
+ /**
45
+ * The application this repository belongs to - its services, its technologies, its logger.
46
+ *
47
+ * **Non-enumerable**, like `_repoScope` and `_git` beside it: the two things that walk a
48
+ * repository are `{...pkg}` spreads and the config scope, and an enumerable back-reference to the
49
+ * whole application would be dragged into both. A `Package` deliberately has no such field at
50
+ * all; a repository is never spread or serialized, which is what makes this one safe - measured,
51
+ * rather than assumed.
52
+ */
53
+ readonly app: RmanApplication;
54
+ protected constructor(app: RmanApplication, dirname: string, monorepo: boolean, packages: Package[],
40
55
  /** The directory `Repository.create()` was actually invoked from - unlike `dirname` (the
41
56
  * resolved repository root, possibly several levels up), this is where the user's shell
42
57
  * really was. Used by `currentPackage` to scope commands to "the package I'm standing in". */
43
- cwd?: string);
58
+ cwd?: string,
59
+ /** The root directory's own technology, from the same walk that found the packages - so the
60
+ * repository and its `rootPackage` agree without either searching the registry again. */
61
+ platform?: Platform);
44
62
  /**
45
63
  * The package whose own directory contains `cwd` (the deepest match, so a package nested
46
64
  * inside another's directory resolves to the innermost one) - or `undefined` when `cwd` *is*
@@ -60,9 +78,21 @@ export declare class Repository extends Package {
60
78
  * dirty, the reference point decides the rest: without `hash`, `committed`
61
79
  * means committed but not yet in the upstream branch; with `hash`, `changed`
62
80
  * means it differs from that commit. Otherwise a package is `clean`.
81
+ *
82
+ * **Keyed by `Package.selector`**, which is what addresses a package - it was `name`, and the two
83
+ * coincide wherever a technology names its packages. A repository whose does not had every such
84
+ * package answering to `""`, so one entry stood for all of them.
85
+ *
86
+ * **`includeRoot` is opt-in, and the reason is that the root's answer means something different.**
87
+ * Its directory contains every other package, so the same rule - "does a changed file fall under
88
+ * this directory" - reports `dirty` for the root whenever *anything* in the repository is dirty.
89
+ * That is the honest reading of the rule rather than a bug, and it is not what `run --changed`
90
+ * wants, so only a caller that asked for the root gets it (`rman list`'s table, which shows the
91
+ * root as the tree's own row).
63
92
  */
64
93
  listStatus(options?: {
65
94
  hash?: string;
95
+ includeRoot?: boolean;
66
96
  }): Promise<Record<string, Repository.PackageStatus>>;
67
97
  /**
68
98
  * The scope a `${{ ... }}` expression is evaluated against for `pkg`, optionally with the version
@@ -89,13 +119,44 @@ export declare class Repository extends Package {
89
119
  * nothing under `getPackages()` is the root, so only its own unmarked config applies.
90
120
  */
91
121
  /**
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.
122
+ * Gives every package its `repository`, and hangs the `parent`/`children` tree off the walk that
123
+ * found them - before any config is resolved, since a config expression or a provider may already
124
+ * want to navigate from a package outwards.
125
+ *
126
+ * **The containment is read from the tree rather than recomputed from paths.** It used to be an
127
+ * O(n²) sweep comparing every package's directory against every other's and keeping the longest
128
+ * prefix - which is the same question `Workspace.walk` answers on the way down, asked again
129
+ * afterwards with the answer thrown away. Two places deriving one relationship is two places to
130
+ * disagree; there is one now.
131
+ *
132
+ * **`parent` is defined non-enumerably, `children` is a plain field**, which is the one asymmetry
133
+ * here and it is deliberate: a tree is serialized downwards, so `children` has to be walkable and
134
+ * `parent` must not be, or every `JSON.stringify` is a cycle. The same way `Repository.app` and
135
+ * `ORIGINS` travel.
94
136
  *
95
137
  * A repository's own `repository` is itself, which reads oddly and is the honest answer:
96
138
  * `Repository extends Package`, so the repository *is* a package of its own repository.
97
139
  */
98
- protected _linkPackages(): void;
140
+ protected _linkPackages(tree: Workspace.Node, nodes: Workspace.Node[], packages: Package[]): void;
141
+ /**
142
+ * Gives every package the selector a `"[glob]"` block and `--scope` match it by, and refuses two
143
+ * packages that would answer to the same one.
144
+ *
145
+ * **Its own step, before any config is resolved by a selector**, which is the ordering that makes
146
+ * the rest work: `_resolveConfigs` asks `resolveConfig` for each package *by selector*, so an
147
+ * address assigned afterwards would be applied to nothing.
148
+ *
149
+ * **`name` comes from the unmarked cascade only** - the same read `platform` gets, from the same
150
+ * cache, for the same reason. A `"[glob]"` block cannot set it (`assertSelectorBlocks` refuses
151
+ * one) because the glob matches the very thing the block would be setting.
152
+ *
153
+ * **Uniqueness is checked, and the cascade is the mistake it usually catches.** `name` cascades
154
+ * like every unmarked key, so one declaration above two packages gives both the same address -
155
+ * and the failure would otherwise be silent in the worst way: the config reaches both and
156
+ * `getPackage` returns whichever came first. The message names both directories, and says the
157
+ * cascade out loud when the two got it from one declaration.
158
+ */
159
+ protected _assignSelectors(rootDir: string, cache: Map<string, RmanConfig>): Promise<void>;
99
160
  protected _resolveConfigs(): Promise<void>;
100
161
  protected _topoSortPackages(packages: Package[]): void;
101
162
  protected _packageScope(pkg: Package, targetVersion?: string): PackageScope;
@@ -131,16 +192,18 @@ export declare class Repository extends Package {
131
192
  * otherwise: the plugins that know what a package is are named in the config file this step
132
193
  * is looking for.
133
194
  * 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).
195
+ * (their commands arrive through `.rmanrc "commands"`, which `cli.ts` reads).
135
196
  * 3. **Ask the providers** for the layout. None recognizing it means a repository that is itself
136
197
  * the one package.
137
198
  *
138
199
  * **A repository whose `.rmanrc` names no plugin has no packages beyond itself**, and that is the
139
200
  * 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.
201
+ * takes `plugins: ['node']` - or detection reading the directory as a Node one - for it to be
202
+ * read as a workspace at all.
141
203
  */
142
204
  static create(root?: string, options?: {
143
205
  deep?: number;
206
+ app?: RmanApplication;
144
207
  }): Promise<Repository>;
145
208
  /** Finishes constructing `repo` with the async work a constructor can't do itself - resolving
146
209
  * `.rmanrc`/`.rmanrc.yml`/`.rmanrc.cjs`/`.mjs`/`.js` config (which may need a dynamic `import()`)
@@ -1,11 +1,15 @@
1
1
  import { execFileSync } from 'node:child_process';
2
+ import colors from 'ansi-colors';
2
3
  import path from 'path';
3
4
  import semver from 'semver';
5
+ import { detectBuiltin } from '../plugins/detect.js';
4
6
  import { GitHelper } from '../utils/git.js';
7
+ import { RmanApplication } from './application.js';
5
8
  import { createFileScope, createReadScope, DEFERRED_PATHS, interpolateConfig, readDirConfig, resolveConfig, } from './config.js';
6
9
  import { Manifest } from './manifest.js';
10
+ import { ORIGINS } from './merge-config.js';
7
11
  import { Package } from './package.js';
8
- import { loadPlugins } from './plugin.js';
12
+ import { loadPlugins, registerPlugin } from './plugin-loader.js';
9
13
  import { Workspace } from './workspace.js';
10
14
  export class Repository extends Package {
11
15
  dirname;
@@ -13,9 +17,10 @@ export class Repository extends Package {
13
17
  packages;
14
18
  cwd;
15
19
  rootPackage;
20
+ /** See the `defineProperty` in the constructor - what detection supplied, when it did. */
21
+ detectedBuiltin;
16
22
  /** Commands the repository's plugins contributed, loaded during `create` because the workspace
17
23
  * providers they bring are needed before any package can be found. `cli.ts` registers them. */
18
- pluginCommands = [];
19
24
  /**
20
25
  * Cached repository scope - see `_repositoryScope`.
21
26
  *
@@ -39,17 +44,40 @@ export class Repository extends Package {
39
44
  * for every package that mentions it.
40
45
  */
41
46
  _readCache = new Map();
42
- constructor(dirname, monorepo, packages,
47
+ /**
48
+ * The application this repository belongs to - its services, its technologies, its logger.
49
+ *
50
+ * **Non-enumerable**, like `_repoScope` and `_git` beside it: the two things that walk a
51
+ * repository are `{...pkg}` spreads and the config scope, and an enumerable back-reference to the
52
+ * whole application would be dragged into both. A `Package` deliberately has no such field at
53
+ * all; a repository is never spread or serialized, which is what makes this one safe - measured,
54
+ * rather than assumed.
55
+ */
56
+ app;
57
+ constructor(app, dirname, monorepo, packages,
43
58
  /** The directory `Repository.create()` was actually invoked from - unlike `dirname` (the
44
59
  * resolved repository root, possibly several levels up), this is where the user's shell
45
60
  * really was. Used by `currentPackage` to scope commands to "the package I'm standing in". */
46
- cwd = dirname) {
47
- super(dirname);
61
+ cwd = dirname,
62
+ /** The root directory's own technology, from the same walk that found the packages - so the
63
+ * repository and its `rootPackage` agree without either searching the registry again. */
64
+ platform) {
65
+ super(dirname, app, platform);
48
66
  this.dirname = dirname;
49
67
  this.monorepo = monorepo;
50
68
  this.packages = packages;
51
69
  this.cwd = cwd;
52
- this.rootPackage = new Package(dirname);
70
+ Object.defineProperty(this, 'app', { value: app, enumerable: false, writable: false });
71
+ /**
72
+ * What detection decided for this repository, or `undefined` when it declared its own
73
+ * technology (or when there was nothing to detect). Carried here because `_resolveConfigs` runs
74
+ * later and every read of the root's config has to agree with the one decision `create` made.
75
+ *
76
+ * Non-enumerable, like `app` and for the same reason: `{...repository}` and `toEqual` both walk
77
+ * a repository, and bookkeeping that shows up there turns spec failures into diffs about it.
78
+ */
79
+ Object.defineProperty(this, 'detectedBuiltin', { value: undefined, enumerable: false, writable: true });
80
+ this.rootPackage = new Package(dirname, app, platform);
53
81
  if (!monorepo)
54
82
  this.packages = [this.rootPackage];
55
83
  // Config resolution can load a `.rmanrc.cjs`/`.mjs`/`.js` module (dynamic `import()`, always
@@ -94,11 +122,22 @@ export class Repository extends Package {
94
122
  * dirty, the reference point decides the rest: without `hash`, `committed`
95
123
  * means committed but not yet in the upstream branch; with `hash`, `changed`
96
124
  * means it differs from that commit. Otherwise a package is `clean`.
125
+ *
126
+ * **Keyed by `Package.selector`**, which is what addresses a package - it was `name`, and the two
127
+ * coincide wherever a technology names its packages. A repository whose does not had every such
128
+ * package answering to `""`, so one entry stood for all of them.
129
+ *
130
+ * **`includeRoot` is opt-in, and the reason is that the root's answer means something different.**
131
+ * Its directory contains every other package, so the same rule - "does a changed file fall under
132
+ * this directory" - reports `dirty` for the root whenever *anything* in the repository is dirty.
133
+ * That is the honest reading of the rule rather than a bug, and it is not what `run --changed`
134
+ * wants, so only a caller that asked for the root gets it (`rman list`'s table, which shows the
135
+ * root as the tree's own row).
97
136
  */
98
137
  async listStatus(options) {
99
138
  const hash = options?.hash;
100
139
  const git = new GitHelper({ cwd: this.dirname });
101
- const packages = this.getPackages();
140
+ const packages = options?.includeRoot ? [this.rootPackage, ...this.getPackages()] : this.getPackages();
102
141
  const belongsTo = (p, files) => files.some(f => !path.relative(p.dirname, f).startsWith('..'));
103
142
  const [dirtyFiles, referenceFiles] = await Promise.all([
104
143
  git.listDirtyFiles({ absolute: true }),
@@ -107,11 +146,11 @@ export class Repository extends Package {
107
146
  const result = {};
108
147
  for (const p of packages) {
109
148
  if (belongsTo(p, dirtyFiles))
110
- result[p.name] = 'dirty';
149
+ result[p.selector] = 'dirty';
111
150
  else if (belongsTo(p, referenceFiles))
112
- result[p.name] = hash ? 'changed' : 'committed';
151
+ result[p.selector] = hash ? 'changed' : 'committed';
113
152
  else
114
- result[p.name] = 'clean';
153
+ result[p.selector] = 'clean';
115
154
  }
116
155
  return result;
117
156
  }
@@ -173,30 +212,96 @@ export class Repository extends Package {
173
212
  * nothing under `getPackages()` is the root, so only its own unmarked config applies.
174
213
  */
175
214
  /**
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.
215
+ * Gives every package its `repository`, and hangs the `parent`/`children` tree off the walk that
216
+ * found them - before any config is resolved, since a config expression or a provider may already
217
+ * want to navigate from a package outwards.
218
+ *
219
+ * **The containment is read from the tree rather than recomputed from paths.** It used to be an
220
+ * O(n²) sweep comparing every package's directory against every other's and keeping the longest
221
+ * prefix - which is the same question `Workspace.walk` answers on the way down, asked again
222
+ * afterwards with the answer thrown away. Two places deriving one relationship is two places to
223
+ * disagree; there is one now.
224
+ *
225
+ * **`parent` is defined non-enumerably, `children` is a plain field**, which is the one asymmetry
226
+ * here and it is deliberate: a tree is serialized downwards, so `children` has to be walkable and
227
+ * `parent` must not be, or every `JSON.stringify` is a cycle. The same way `Repository.app` and
228
+ * `ORIGINS` travel.
178
229
  *
179
230
  * A repository's own `repository` is itself, which reads oddly and is the honest answer:
180
231
  * `Repository extends Package`, so the repository *is* a package of its own repository.
181
232
  */
182
- _linkPackages() {
183
- this.repository = this;
184
- this.rootPackage.repository = this;
233
+ _linkPackages(tree, nodes, packages) {
234
+ /** Non-enumerable everywhere, including on the repository itself - see `Package.repository`.
235
+ * `writable` because `import` grafts an external repository's packages onto this one. */
236
+ const link = (pkg) => {
237
+ Object.defineProperty(pkg, 'repository', { value: this, enumerable: false, configurable: true, writable: true });
238
+ };
239
+ link(this);
240
+ link(this.rootPackage);
241
+ for (const pkg of this.packages)
242
+ link(pkg);
243
+ /** The walk visits a directory once, so one node is one package and this map is a bijection -
244
+ * which is what lets the edges below be read off the tree instead of guessed from paths. */
245
+ const byNode = new Map(nodes.map((node, i) => [node, packages[i]]));
246
+ byNode.set(tree, this.rootPackage);
247
+ for (const node of [tree, ...nodes]) {
248
+ const pkg = byNode.get(node);
249
+ for (const childNode of node.children) {
250
+ const child = byNode.get(childNode);
251
+ pkg.children.push(child);
252
+ Object.defineProperty(child, 'parent', { value: pkg, enumerable: false, configurable: true });
253
+ }
254
+ }
255
+ /** `Repository extends Package` while holding a separate `rootPackage` for the same directory,
256
+ * so both are truthfully the root - they share the one array rather than each getting a copy
257
+ * that could drift. */
258
+ Object.defineProperty(this, 'children', { value: this.rootPackage.children, enumerable: true });
259
+ }
260
+ /**
261
+ * Gives every package the selector a `"[glob]"` block and `--scope` match it by, and refuses two
262
+ * packages that would answer to the same one.
263
+ *
264
+ * **Its own step, before any config is resolved by a selector**, which is the ordering that makes
265
+ * the rest work: `_resolveConfigs` asks `resolveConfig` for each package *by selector*, so an
266
+ * address assigned afterwards would be applied to nothing.
267
+ *
268
+ * **`name` comes from the unmarked cascade only** - the same read `platform` gets, from the same
269
+ * cache, for the same reason. A `"[glob]"` block cannot set it (`assertSelectorBlocks` refuses
270
+ * one) because the glob matches the very thing the block would be setting.
271
+ *
272
+ * **Uniqueness is checked, and the cascade is the mistake it usually catches.** `name` cascades
273
+ * like every unmarked key, so one declaration above two packages gives both the same address -
274
+ * and the failure would otherwise be silent in the worst way: the config reaches both and
275
+ * `getPackage` returns whichever came first. The message names both directories, and says the
276
+ * cascade out loud when the two got it from one declaration.
277
+ */
278
+ async _assignSelectors(rootDir, cache) {
279
+ const declaredBy = new Map();
280
+ for (const pkg of [this.rootPackage, ...this.packages]) {
281
+ const config = await resolveConfig(rootDir, pkg.dirname, cache, undefined);
282
+ const declared = config.name;
283
+ if (declared !== undefined && (typeof declared !== 'string' || !declared.trim())) {
284
+ throw new Error(`"name" takes the selector this package answers to - "${pkg.dirname}" gave ${typeof declared}.`);
285
+ }
286
+ pkg.selector = declared?.trim() || pkg.platformSelector();
287
+ declaredBy.set(pkg, declared !== undefined);
288
+ }
289
+ /** The root is left out: a glob never matches it and `"[/]"` needs no name, so it shares an
290
+ * address with nobody - see `Package.selector`. */
291
+ const bySelector = new Map();
185
292
  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;
293
+ const clash = bySelector.get(pkg.selector);
294
+ if (clash) {
295
+ const cascaded = declaredBy.get(pkg) && declaredBy.get(clash);
296
+ throw new Error(`Two packages answer to the selector "${pkg.selector}":\n ${clash.dirname}\n ${pkg.dirname}\n` +
297
+ ` A selector has to be unique - "[${pkg.selector}]" and \`--scope ${pkg.selector}\` ` +
298
+ `cannot mean two packages.` +
299
+ (cascaded
300
+ ? `\n Both got it from one cascading "name" declaration above them; declare it in ` +
301
+ `each package's own ".rmanrc" instead.`
302
+ : ''));
198
303
  }
199
- pkg.parent = pkg === this.rootPackage ? undefined : parent;
304
+ bySelector.set(pkg.selector, pkg);
200
305
  }
201
306
  }
202
307
  async _resolveConfigs() {
@@ -208,14 +313,18 @@ export class Repository extends Package {
208
313
  * second `rawConfig` copy of every package's config for that one reader; measured identical.
209
314
  */
210
315
  /**
211
- * The root is resolved **with its name**, like every other package, because selectors now speak
212
- * to it: `"[/]"` names it and `"[*]"` includes it. It used to be resolved without one, which is
213
- * what made `"[*]"` quietly mean "the workspace packages" - `"[ws:*]"` is that, spelled.
316
+ * **By selector, not by name** - `"[glob]"` matches `Package.selector`, which is the package's
317
+ * own `.rmanrc "name"` when it assigned one and its platform's answer otherwise. They coincide
318
+ * for every Node repository; they are not the same question, and `name` was the wrong one to
319
+ * ask, since a package having one at all is an ecosystem's promise rather than rman's.
320
+ *
321
+ * The root passes its own too, although no glob can match it: `"[/]"` is applied on the
322
+ * strength of the target *being* the root and needs no selector at all (see `resolveConfig`).
214
323
  */
215
- const rootRaw = await resolveConfig(this.dirname, this.dirname, cache, this.rootPackage.name);
324
+ const rootRaw = await resolveConfig(this.dirname, this.dirname, cache, this.rootPackage.selector, this.detectedBuiltin);
216
325
  this.config = interpolateConfig(rootRaw, this.configScope(this.rootPackage), { skip: DEFERRED_PATHS });
217
326
  for (const pkg of this.packages) {
218
- const raw = await resolveConfig(this.dirname, pkg.dirname, cache, pkg.name);
327
+ const raw = await resolveConfig(this.dirname, pkg.dirname, cache, pkg.selector, this.detectedBuiltin);
219
328
  pkg.config = interpolateConfig(raw, this.configScope(pkg), { skip: DEFERRED_PATHS });
220
329
  }
221
330
  if (this.monorepo)
@@ -235,7 +344,7 @@ export class Repository extends Package {
235
344
  // than refusing it - so the scope has to survive one too.
236
345
  const name = pkg.name ?? '';
237
346
  /** Splitting `@scope/name` is npm's convention, not a universal - the provider decides. */
238
- const { scope: nameScope, unscopedName } = Manifest.splitName(pkg.dirname, name);
347
+ const { scope: nameScope, unscopedName } = Manifest.splitName(this.app, pkg.dirname, name);
239
348
  const scope = {
240
349
  name,
241
350
  scope: nameScope,
@@ -383,26 +492,106 @@ export class Repository extends Package {
383
492
  * otherwise: the plugins that know what a package is are named in the config file this step
384
493
  * is looking for.
385
494
  * 2. **Load the plugins** the root's config names, which registers their workspace providers
386
- * (and their commands, handed on via `pluginCommands` - `cli.ts` registers those).
495
+ * (their commands arrive through `.rmanrc "commands"`, which `cli.ts` reads).
387
496
  * 3. **Ask the providers** for the layout. None recognizing it means a repository that is itself
388
497
  * the one package.
389
498
  *
390
499
  * **A repository whose `.rmanrc` names no plugin has no packages beyond itself**, and that is the
391
500
  * boundary working rather than failing: `workspaces` in a `package.json` is npm's idea, so it
392
- * takes `plugins: ['rman-node']` to be read as one.
501
+ * takes `plugins: ['node']` - or detection reading the directory as a Node one - for it to be
502
+ * read as a workspace at all.
393
503
  */
394
504
  static async create(root, options) {
395
505
  const from = root || process.cwd();
396
506
  const rootDir = Workspace.findRoot(from, options?.deep ?? 10);
507
+ /**
508
+ * One application per repository, made here unless the caller brought one.
509
+ *
510
+ * `runCli` passes its own so that `--log-level` reaches the logger; a spec that only wants a
511
+ * repository lets this make one, which is also what keeps two repositories in a single process
512
+ * from sharing anything - the thing that used to need five `clear*()` calls before every test.
513
+ */
514
+ const app = options?.app ?? new RmanApplication();
397
515
  /** The root's own config, raw: `plugins` is a list of package names, so it needs neither the
398
516
  * package list (which does not exist yet) nor expression interpolation. */
399
- const rootConfig = await readDirConfig(rootDir);
400
- const pluginCommands = await loadPlugins(rootDir, rootConfig);
401
- const layout = Workspace.resolve(rootDir);
402
- const packages = (layout?.packageDirs ?? []).map(dir => new Package(dir));
403
- const repo = new Repository(layout?.root ?? rootDir, packages.length > 0, packages, from);
404
- repo.pluginCommands = pluginCommands;
405
- repo._linkPackages();
517
+ const declared = await readDirConfig(rootDir);
518
+ /**
519
+ * **What this repository looks like, when nothing said** - the other half of shipping the
520
+ * built-ins in the box. See `detectBuiltin`.
521
+ *
522
+ * **Two conditions, and the second is the one that is easy to miss.** The config declaring no
523
+ * `plugins` is not the same as the *repository* having no technology: a programmatic caller -
524
+ * and every spec in this suite - registers one straight onto the application without writing a
525
+ * config at all. Guessing on top of that registers a second technology, and the first provider
526
+ * that recognizes a directory decides whether it holds a package. Measured with only the config
527
+ * condition: ten specs changed answer, seven of them about config cascading and three about
528
+ * `publish`'s flags.
529
+ *
530
+ * **`platforms`, not `plugins`, and the difference is what detection produces.** What would be
531
+ * added here is a *platform*, so what must not already be there is a platform - a plugin that
532
+ * only registers a command says nothing about which directories hold packages, and letting it
533
+ * suppress the guess would leave a Node repository undetected for having added a command.
534
+ *
535
+ * **A root `platform` counts as having said something**, like `plugins: []` does. Detection
536
+ * exists for a repository that stated nothing; one naming its technology has stated the very
537
+ * thing detection would be guessing at, and guessing anyway would register a *second* platform
538
+ * beside the declared one - which then competes for every directory the declaration did not
539
+ * cover.
540
+ *
541
+ * Decided **once**, here, and handed to every read that has to agree - `readDirConfig` has no
542
+ * business knowing about an application.
543
+ */
544
+ const saidSomething = declared.plugins !== undefined || declared.platform !== undefined;
545
+ const detected = !saidSomething && app.platforms.size === 0 ? await detectBuiltin(rootDir) : undefined;
546
+ const rootConfig = detected ? await readDirConfig(rootDir, { inject: detected }) : declared;
547
+ /**
548
+ * **Said out loud, because a guess the reader cannot see is one they cannot correct.**
549
+ *
550
+ * To **stderr**, not through `app.logger`: that writes with `console.log`, and `rman list
551
+ * --json` has to stay a parseable document on stdout - measured, its output is pure JSON, and
552
+ * one line of prose in front of it breaks every `| jq`. The same reason `--help`'s degradation
553
+ * notice goes to stderr. Not at `silent`, where the caller asked for no narration.
554
+ */
555
+ if (detected && app.logger.level !== 'silent') {
556
+ const line = `${detected.name} repository detected (${detected.because}) - ` +
557
+ `write \`plugins: ['${detected.name}']\` in .rmanrc to state it, or \`plugins: []\` for none.`;
558
+ console.error(process.stderr.isTTY ? colors.gray(line) : line);
559
+ }
560
+ await loadPlugins(app, rootConfig);
561
+ /**
562
+ * **Its own cache, deliberately not shared with `_resolveConfigs`'.**
563
+ *
564
+ * Both cache `readDirConfig` per directory, and the two reads of the *root* are not the same
565
+ * read: `_resolveConfigs` passes `detectedBuiltin` as `inject` and this one passes nothing,
566
+ * since a built-in's contribution has no bearing on which platform a directory declares. The
567
+ * cache is keyed by directory alone, so one shared map would hand whichever ran first to the
568
+ * other - and for the root that is a config with or without the whole node built-in merged in.
569
+ *
570
+ * The cost is one extra read per directory in the chain, on a repository that is about to read
571
+ * every one of them again per package anyway.
572
+ */
573
+ const platformCache = new Map();
574
+ /**
575
+ * **The walk**, which is where the package list comes from now - descending from the root,
576
+ * asking each directory's own technology where its children are. `Workspace.resolve` asked the
577
+ * root once, through whichever platform recognized it first; the tree is the shape discovery
578
+ * actually has, and it is what makes a nested package of another technology findable at all.
579
+ */
580
+ const tree = await Workspace.walk(app, rootDir, {
581
+ deep: options?.deep,
582
+ declared: dir => declaredPlatformAt(app, rootDir, dir, platformCache),
583
+ });
584
+ const nodes = Workspace.flatten(tree);
585
+ const packages = nodes.map(node => new Package(node.dirname, app, node.platform));
586
+ const repo = new Repository(app, tree.dirname, packages.length > 0, packages, from, tree.platform);
587
+ repo._linkPackages(tree, nodes, packages);
588
+ /** Before `_resolveConfigs`, because a `"[glob]"` block is matched against the selector - so
589
+ * the addresses have to be settled before anything is resolved by them. */
590
+ await repo._assignSelectors(rootDir, platformCache);
591
+ /** The application is what the plugins registered into a moment ago; from here on it can hand
592
+ * out services, which need the repository to work on. */
593
+ repo.detectedBuiltin = detected;
594
+ app.attachRepository(repo);
406
595
  return Repository._init(repo);
407
596
  }
408
597
  /** Finishes constructing `repo` with the async work a constructor can't do itself - resolving
@@ -414,3 +603,64 @@ export class Repository extends Package {
414
603
  return repo;
415
604
  }
416
605
  }
606
+ /**
607
+ * The platform `dir` declares in its cascaded `.rmanrc "platform"`, loaded if it has to be -
608
+ * `undefined` when the directory declares none, so the walk falls back to its guess.
609
+ *
610
+ * **The unmarked cascade only**, which is what `resolveConfig` gives with no package name: a
611
+ * selector matches a package *name*, and this runs while the packages are still being found - the
612
+ * name is not known yet, and it is read *from* a manifest whose reader this key decides. So
613
+ * `"[/]"` reaches the root (its directory is the repository root, which needs no name) and a glob
614
+ * block contributes nothing here, which is the honest answer rather than a half-applied one.
615
+ */
616
+ async function declaredPlatformAt(app, rootDir, dir, cache) {
617
+ const config = await resolveConfig(rootDir, dir, cache);
618
+ const declared = config.platform;
619
+ if (declared === undefined)
620
+ return undefined;
621
+ if (typeof declared !== 'string' || !declared.trim()) {
622
+ throw new Error(`"platform" takes a platform's name - ${originOf(config)} gave ${typeof declared}.`);
623
+ }
624
+ /**
625
+ * **An expression is refused rather than read as a literal.** `platform` is consulted before any
626
+ * package exists - it is what decides what a package *is* - so there is no `pkg` for an
627
+ * expression to be about, and `interpolateConfig` runs long afterwards. Passed through, a
628
+ * `${{ }}` here reached the lookup below as the raw text and failed as an unknown platform name,
629
+ * which sends the reader to check their `plugins`.
630
+ */
631
+ if (declared.includes('${{')) {
632
+ throw new Error(`"platform" cannot be an expression (${originOf(config)}) - it is read while ` +
633
+ `the packages are still being found, so there is no package for one to be about. Write the ` +
634
+ `name, and use a package's own ".rmanrc" where the answer differs.`);
635
+ }
636
+ const registered = [...app.platforms].find(p => p.name === declared);
637
+ if (registered)
638
+ return registered;
639
+ /**
640
+ * **A built-in is loaded on the strength of being named**, from any level - `platform: 'node'` is
641
+ * enough, and it is what a repository holding one Node package among others writes.
642
+ *
643
+ * `platform()` and not `contribute()`, and that split is why the two halves exist: what arrives
644
+ * is the technology alone. Commands and publish targets come from root `plugins`, because they
645
+ * are repository-wide - a Node package inside a Cargo repository wants npm's manifest read, not
646
+ * an `rman clean` that would sweep the whole tree.
647
+ */
648
+ const { BUILTIN_PLUGINS, builtinPluginNames } = await import('../plugins/builtins.js');
649
+ const builtin = BUILTIN_PLUGINS[declared];
650
+ if (builtin) {
651
+ const platform = builtin.platform();
652
+ registerPlugin(app, platform);
653
+ return platform;
654
+ }
655
+ const have = [...app.platforms].map(p => p.name).filter(Boolean);
656
+ throw new Error(`"platform" names "${declared}" (${originOf(config)}), which is not a platform ` +
657
+ `this repository has. ${have.length ? `Registered: ${have.join(', ')}. ` : 'None is registered. '}` +
658
+ `rman ships ${builtinPluginNames().join(', ')}; anything else arrives through "plugins".`);
659
+ }
660
+ /** Which file the `platform` key came from, for an error that can be acted on - `mergeConfig`
661
+ * records one per key under `ORIGINS`, and a config is merged from a directory's own four forms,
662
+ * an `extends` base and one layer per directory before anything reads it. */
663
+ function originOf(config) {
664
+ const origins = config[ORIGINS];
665
+ return origins?.platform ? `in "${origins.platform}"` : 'the "platform" key';
666
+ }
@@ -15,7 +15,7 @@ export declare function resolveConfigTarget(target: string, from: string, label:
15
15
  * repository cannot resolve it.
16
16
  *
17
17
  * A globally installed rman's siblings *are* the globally installed packages, so this is what makes
18
- * `rman ci` work on a fresh clone - the command comes from `rman-node`, and `ci` exists to create
18
+ * `rman ci` work on a fresh clone - the command came from `rman-node` then, and `ci` exists to create
19
19
  * the very `node_modules` the plugin would otherwise have to be found in. Measured: with both
20
20
  * installed globally, a clone answered `"plugins" target "rman-node" could not be resolved ... is
21
21
  * it installed in this repository?`, which was true and useless.
@@ -39,7 +39,7 @@ export function resolveConfigTarget(target, from, label) {
39
39
  * repository cannot resolve it.
40
40
  *
41
41
  * A globally installed rman's siblings *are* the globally installed packages, so this is what makes
42
- * `rman ci` work on a fresh clone - the command comes from `rman-node`, and `ci` exists to create
42
+ * `rman ci` work on a fresh clone - the command came from `rman-node` then, and `ci` exists to create
43
43
  * the very `node_modules` the plugin would otherwise have to be found in. Measured: with both
44
44
  * installed globally, a clone answered `"plugins" target "rman-node" could not be resolved ... is
45
45
  * it installed in this repository?`, which was true and useless.