rman 1.3.0 → 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 +140 -19
  40. package/core/config.js +258 -74
  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 +137 -92
  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 -44
  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 +725 -202
  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
package/core/config.js CHANGED
@@ -2,13 +2,13 @@ import { DOMParser } from '@xmldom/xmldom';
2
2
  import fs from 'fs';
3
3
  import ini from 'ini';
4
4
  import * as yaml from 'js-yaml';
5
- import { createRequire } from 'module';
6
5
  import path from 'path';
7
6
  import semver from 'semver';
8
- import { pathToFileURL } from 'url';
9
7
  import vm from 'vm';
10
- import { assertNoSelectorExtends, EXTENDS_KEY, resolveExtends } from './extends-config.js';
11
- import { finalizeConfig, mergeConfig, ORIGINS, PREVIOUS_VALUES } from './merge-config.js';
8
+ import { DETECTED_BUILTIN } from '../plugins/detect.js';
9
+ import { assertSelectorBlocks, EXTENDS_KEY, resolveExtends } from './extends-config.js';
10
+ import { loadConfigModule } from './load-config-module.js';
11
+ import { mergeConfig, ORIGINS, PREVIOUS_VALUES } from './merge-config.js';
12
12
  /**
13
13
  * Identity helper for authoring a `.rmanrc.cjs`/`.mjs`/`.js` config with full type-checking and
14
14
  * autocomplete - the same `defineConfig` pattern Vite/Vitest use. Returns `config` completely
@@ -31,35 +31,18 @@ export function defineConfig(config) {
31
31
  * or a `.js` under a `"type": "commonjs"` package.json) and native ESM (`.mjs`, or a `.js` under
32
32
  * `"type": "module"`) are supported - the reason `readDirConfig`/`resolveConfig` are async at all. */
33
33
  const JS_CONFIG_FILES = ['.rmanrc.cjs', '.rmanrc.mjs', '.rmanrc.js'];
34
- const requireJsConfig = createRequire(import.meta.url);
35
- /**
36
- * Loads `file`'s config object. Tries `require()` first - not just an optimization: a CommonJS
37
- * module's `module.exports` is more reliably observed this way than through dynamic `import()`'s
38
- * CJS-interop synthesis, which some ESM loader hooks (e.g. ts-node/swc-node-style transpilers
39
- * registered via `--import`) can end up short-circuiting into an empty object. `require()` throws
40
- * `ERR_REQUIRE_ESM` for a genuinely-ESM file (`.mjs`, or `.js` under `"type": "module"`) - only
41
- * then does this fall back to `import()`, the one case that actually needs it. Either path can
42
- * hand back an ES module namespace instead of a plain object (Node's `require(esm)` support does
43
- * this too, not just `import()`), so `.default` is preferred whenever present.
44
- */
45
- async function loadJsConfig(file) {
46
- let mod;
47
- try {
48
- mod = requireJsConfig(file);
49
- }
50
- catch (e) {
51
- if (e?.code !== 'ERR_REQUIRE_ESM')
52
- throw e;
53
- mod = await import(pathToFileURL(file).href);
54
- }
55
- return mod?.default ?? mod;
56
- }
57
34
  /**
58
35
  * Reads the rman configuration defined at a single directory level, merging
59
36
  * (in increasing precedence): `package.json#rman`, `.rmanrc.yml`, `.rmanrc`,
60
37
  * then `.rmanrc.cjs`/`.rmanrc.mjs`/`.rmanrc.js` (whichever exist, in that order).
38
+ *
39
+ * `options.inject` supplies a built-in for a repository that declared no technology - **already
40
+ * decided**, rather than a "please detect" flag. The decision needs the application (a programmatic
41
+ * caller or a spec may have registered a technology without writing it in a config), which this
42
+ * function has no business knowing about; `Repository.create` makes it once and hands the answer
43
+ * to every read that has to agree with it. See `detectBuiltin`.
61
44
  */
62
- export async function readDirConfig(dirname) {
45
+ export async function readDirConfig(dirname, options) {
63
46
  const result = {};
64
47
  /** The file an `extends` in this directory resolves relative to. The last form that actually
65
48
  * declared one wins, which matters only for the unusual directory holding several. */
@@ -68,7 +51,7 @@ export async function readDirConfig(dirname) {
68
51
  if (fs.existsSync(pkgJsonFile)) {
69
52
  const pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf-8'));
70
53
  if (pkgJson && typeof pkgJson.rman === 'object') {
71
- assertNoSelectorExtends(pkgJson.rman, pkgJsonFile);
54
+ assertSelectorBlocks(pkgJson.rman, pkgJsonFile);
72
55
  if (EXTENDS_KEY in pkgJson.rman)
73
56
  extendsFrom = pkgJsonFile;
74
57
  mergeConfig(result, pkgJson.rman, pkgJsonFile);
@@ -78,7 +61,7 @@ export async function readDirConfig(dirname) {
78
61
  if (fs.existsSync(ymlFile)) {
79
62
  const obj = yaml.load(fs.readFileSync(ymlFile, 'utf-8'));
80
63
  if (obj && typeof obj === 'object') {
81
- assertNoSelectorExtends(obj, ymlFile);
64
+ assertSelectorBlocks(obj, ymlFile);
82
65
  if (EXTENDS_KEY in obj)
83
66
  extendsFrom = ymlFile;
84
67
  mergeConfig(result, obj, ymlFile);
@@ -88,7 +71,7 @@ export async function readDirConfig(dirname) {
88
71
  if (fs.existsSync(rcFile)) {
89
72
  const obj = JSON.parse(fs.readFileSync(rcFile, 'utf-8'));
90
73
  if (obj && typeof obj === 'object') {
91
- assertNoSelectorExtends(obj, rcFile);
74
+ assertSelectorBlocks(obj, rcFile);
92
75
  if (EXTENDS_KEY in obj)
93
76
  extendsFrom = rcFile;
94
77
  mergeConfig(result, obj, rcFile);
@@ -97,9 +80,9 @@ export async function readDirConfig(dirname) {
97
80
  for (const jsFileName of JS_CONFIG_FILES) {
98
81
  const jsFile = path.join(dirname, jsFileName);
99
82
  if (fs.existsSync(jsFile)) {
100
- const obj = await loadJsConfig(jsFile);
83
+ const obj = await loadConfigModule(jsFile);
101
84
  if (obj && typeof obj === 'object') {
102
- assertNoSelectorExtends(obj, jsFile);
85
+ assertSelectorBlocks(obj, jsFile);
103
86
  if (EXTENDS_KEY in obj)
104
87
  extendsFrom = jsFile;
105
88
  mergeConfig(result, obj, jsFile);
@@ -110,7 +93,107 @@ export async function readDirConfig(dirname) {
110
93
  * one of them sits on, and the directory chain then layers on top as it always did. Each form
111
94
  * was checked for a misplaced `extends` as it was read, so that error can name the file holding
112
95
  * it rather than whichever form happened to declare the real one. */
113
- return resolveExtends(result, extendsFrom);
96
+ const resolved = await resolveExtends(result, extendsFrom);
97
+ /**
98
+ * **After `extends`, because a base may be what declares the technology** - a shared config
99
+ * naming `plugins` is a statement, and detecting on top of it would be guessing over an answer.
100
+ * `plugins` being *present* is what counts, so `plugins: []` is a repository saying "none".
101
+ */
102
+ const detected = options?.inject && resolved.plugins === undefined ? options.inject : undefined;
103
+ if (detected)
104
+ resolved.plugins = [detected.name];
105
+ const expanded = await expandBuiltinPlugins(resolved);
106
+ /**
107
+ * **Marked after the expansion, not before, because the expansion rebuilds the object.**
108
+ * `expandBuiltinPlugins` merges the built-in's config underneath and returns a *new* config, so a
109
+ * symbol set on the way in is simply gone on the way out - measured: detection worked and the
110
+ * "detected" line never printed. The same trap `PREVIOUS_VALUES` and `ORIGINS` document from the
111
+ * other side, where `mergeConfig` has to copy them across by hand.
112
+ */
113
+ if (detected) {
114
+ Object.defineProperty(expanded, DETECTED_BUILTIN, { value: detected, enumerable: false, configurable: true });
115
+ }
116
+ return expanded;
117
+ }
118
+ /**
119
+ * Turns a built-in **name** in `plugins` into what that built-in contributes - `['node']` into the
120
+ * node plugin, its two commands and its publish target.
121
+ *
122
+ * **Here, beside `extends`, because it is the same operation**: something named brings a config,
123
+ * and that config sits *underneath* the one naming it. Doing it anywhere later would not reach far
124
+ * enough - `commands` is read off the resolved root package by `cli.ts`, not off the raw config
125
+ * `Repository.create` hands to `loadPlugins`, so a built-in expanded only there would register its
126
+ * technology and silently lose its commands.
127
+ *
128
+ * **The name is consumed.** `plugins` always appends, so leaving the string beside the instance it
129
+ * expanded into would hand `loadPlugins` a glob that matches no file - the built-in would load and
130
+ * then the run would fail saying it did not.
131
+ *
132
+ * Runs per directory, like `extends`, but only the root's `plugins` is ever read (`loadPlugins`
133
+ * needs the technologies before any package exists). The cost of walking a key that is almost
134
+ * always absent is one `Array.isArray`.
135
+ */
136
+ async function expandBuiltinPlugins(config) {
137
+ const declared = config.plugins;
138
+ const own = Array.isArray(declared) ? declared : declared === undefined ? [] : [declared];
139
+ const platform = declaredPlatform(config);
140
+ if (!own.some(e => typeof e === 'string') && platform === undefined)
141
+ return config;
142
+ /**
143
+ * **Imported here rather than at the top, and that is a cycle rather than a style.** A built-in
144
+ * pulls in its commands and services, which read config - so a static import would have
145
+ * `config.ts` and the plugin subtree initialising each other, which in ESM half-works and fails
146
+ * silently. Node caches the module, so the cost is one resolution on a config that names one.
147
+ */
148
+ const { BUILTIN_PLUGINS, isBuiltinPlugin } = await import('../plugins/builtins.js');
149
+ /**
150
+ * **A `platform` naming a built-in puts it at the front of `plugins`.**
151
+ *
152
+ * Saying which technology this repository is *is* saying it has it, so making the author write
153
+ * both was a distinction only rman could see. Measured on the repository this was noticed in:
154
+ * `platform: 'node'` alone gave a working `rman list` with a `node` column and
155
+ * `Unknown arguments: clean`, because the technology had loaded and its commands had not.
156
+ *
157
+ * **At the front, not the back**, and that is what makes it a statement rather than an addition:
158
+ * `platformFor` takes the first registered platform that recognizes a directory, so the one this
159
+ * repository says it *is* should win over anything a shared config brought along.
160
+ *
161
+ * **Only a built-in**, checked after the import above for exactly this reason: a `platform` naming
162
+ * a third-party technology is answered by the `plugins` entry that loads it, and pushing the bare
163
+ * name in here would hand `loadPlugins` a glob matching no file - the failure would read as the
164
+ * plugin being missing when it is registered perfectly well.
165
+ *
166
+ * Already named, and nothing happens: `plugins` de-duplicates, and this keeps the author's own
167
+ * ordering rather than promoting an entry they placed deliberately.
168
+ */
169
+ const entries = platform !== undefined && isBuiltinPlugin(platform) && !own.includes(platform) ? [platform, ...own] : own;
170
+ const named = entries.filter((e) => typeof e === 'string' && isBuiltinPlugin(e));
171
+ if (!named.length)
172
+ return config;
173
+ const base = {};
174
+ /** De-duplicated first: two layers naming the same built-in is ordinary (a shared config and the
175
+ * repository that inherits it), and registering a plugin twice defines its commands twice. */
176
+ for (const name of [...new Set(named)])
177
+ mergeConfig(base, BUILTIN_PLUGINS[name].contribute());
178
+ const result = { ...config };
179
+ result.plugins = entries.filter(e => !(typeof e === 'string' && isBuiltinPlugin(e)));
180
+ return mergeConfig(base, result);
181
+ }
182
+ /**
183
+ * The **root's** declared platform, however it was spelled - unmarked, or inside a `"[/]"` block.
184
+ *
185
+ * Both, because both are the root saying what it is and a reader would not expect one to bring the
186
+ * built-in and the other not. `"[/]"` is the precise spelling (it speaks for the root package
187
+ * alone, where an unmarked key also cascades to every package below), so leaving it out would have
188
+ * punished the more careful author.
189
+ *
190
+ * A glob block is not consulted and cannot be: `assertSelectorBlocks` refuses `platform` there,
191
+ * since the glob matches a selector the key is upstream of.
192
+ */
193
+ function declaredPlatform(config) {
194
+ const root = config[`[${ROOT_SELECTOR_INNER}]`];
195
+ const declared = config.platform ?? (root && typeof root === 'object' ? root.platform : undefined);
196
+ return typeof declared === 'string' && declared.trim() ? declared.trim() : undefined;
114
197
  }
115
198
  /**
116
199
  * Resolves the effective config for the package at `targetDir`, cascading from `rootDir` down to
@@ -139,10 +222,27 @@ export async function readDirConfig(dirname) {
139
222
  * repo-wide bookend therefore belongs under `"[/]"`, where its audience is visible; that is the
140
223
  * migration this change asks for, and the only one that is not mechanical.
141
224
  *
142
- * `packageName` is what selectors match against; without it, selector blocks contribute nothing at
143
- * all. The root package passes its own, since `"[/]"` speaks to it.
225
+ * **`selector` is what a `"[glob]"` block matches** - `Package.selector`, which is the package's
226
+ * `.rmanrc "name"` if it declares one and its platform's answer otherwise. Without it, glob blocks
227
+ * contribute nothing: the walk resolves config for a directory *before* the package exists, since
228
+ * that is where `platform` and `name` are read from, and a glob has nothing to match against yet.
229
+ *
230
+ * **`"[/]"` needs no selector, and that is the documented rule rather than an exception.** The root
231
+ * is addressed structurally - its directory *is* the repository root - which is the whole reason it
232
+ * is `/` and not a name. So a root block applies whenever the target is the root, named or not, and
233
+ * `platform` under `"[/]"` therefore works during the walk. It did not until this was noticed: the
234
+ * gate was `if (packageName)`, so the walk skipped every selector block including that one, and the
235
+ * key documented as "keeps it on the root package alone" silently did nothing.
236
+ *
237
+ * It was called `packageName`, which was wrong twice over: a package is not guaranteed to have a
238
+ * name (that is an ecosystem's promise, not rman's), and what this matches is the selector, which a
239
+ * repository can assign itself.
144
240
  */
145
- export async function resolveConfig(rootDir, targetDir, cache = new Map(), packageName) {
241
+ export async function resolveConfig(rootDir, targetDir, cache = new Map(), selector,
242
+ /** The built-in `Repository.create` decided on, for a repository that declared no technology.
243
+ * Applied at the **root level only** - `plugins` is read nowhere else, and this is the read whose
244
+ * result becomes `pkg.config`, which is where `cli.ts` finds a built-in's `commands`. */
245
+ inject) {
146
246
  const result = {};
147
247
  const target = path.resolve(targetDir);
148
248
  /** The root *package* is the one whose directory is the repository root - no other test is
@@ -152,7 +252,9 @@ export async function resolveConfig(rootDir, targetDir, cache = new Map(), packa
152
252
  for (const dir of dirChain(rootDir, targetDir)) {
153
253
  let local = cache.get(dir);
154
254
  if (!local) {
155
- local = await readDirConfig(dir);
255
+ local = await readDirConfig(dir, {
256
+ inject: path.resolve(dir) === path.resolve(rootDir) ? inject : undefined,
257
+ });
156
258
  cache.set(dir, local);
157
259
  }
158
260
  /**
@@ -167,16 +269,14 @@ export async function resolveConfig(rootDir, targetDir, cache = new Map(), packa
167
269
  * layer that feeds the directories below it.
168
270
  */
169
271
  mergeConfig(result, stripSelectors(local));
170
- /** Then the selector blocks, **in the order they were written** - see `matchingSelectors`. */
171
- if (packageName) {
172
- for (const block of matchingSelectors(local, packageName, isRoot))
272
+ /** Then the selector blocks, **in the order they were written** - see `matchingSelectors`.
273
+ * Reached for the root even with no selector, since `"[/]"` is structural. */
274
+ if (selector !== undefined || isRoot) {
275
+ for (const block of matchingSelectors(local, selector, isRoot))
173
276
  mergeConfig(result, block);
174
277
  }
175
278
  }
176
- /** Every layer has had its turn, so an append still outstanding has nothing left to attach to
177
- * and becomes the value itself. Done here rather than per layer: until the chain is finished,
178
- * the key it appends to may still be coming. */
179
- return finalizeConfig(result);
279
+ return result;
180
280
  }
181
281
  /** A config key naming packages rather than settings: `"[*]"`, `"[/]"`, `"[pkg-a]"`. The
182
282
  * brackets are what keep this space from colliding with real config keys - no setting starts with
@@ -236,13 +336,15 @@ export function selectorToRegExp(key) {
236
336
  * The cost, which the docs state rather than hide: a catch-all written *below* a narrower block now
237
337
  * overrides it. Writing catch-alls first is a convention, not a rule - the file reads top to bottom.
238
338
  */
239
- function matchingSelectors(config, packageName, isRoot) {
339
+ function matchingSelectors(config, selector, isRoot) {
240
340
  const matches = [];
241
341
  for (const [key, value] of Object.entries(config)) {
242
342
  if (!isSelectorKey(key) || !value || typeof value !== 'object')
243
343
  continue;
244
344
  const { scope, test } = parseSelector(key);
245
- if (scope === 'root' ? !isRoot : isRoot || !test(packageName))
345
+ /** A root block asks only whether this *is* the root - no selector needed, which is what makes
346
+ * `/` structural. A glob has to have something to match, and during the walk it does not. */
347
+ if (scope === 'root' ? !isRoot : isRoot || selector === undefined || !test(selector))
246
348
  continue;
247
349
  matches.push(value);
248
350
  }
@@ -287,6 +389,11 @@ function dirChain(rootDir, targetDir) {
287
389
  }
288
390
  return dirs;
289
391
  }
392
+ /**
393
+ * **Returns `Resolved<T>`, not `T`, because resolving is what it does.** Calling every value
394
+ * function is half this function's job, so the type it hands back is the one where they are gone -
395
+ * which is what makes `pkg.config` a `ResolvedConfig` without a cast anywhere between.
396
+ */
290
397
  export function interpolateConfig(config, scope, options) {
291
398
  const skip = options?.skip ?? [];
292
399
  /**
@@ -313,8 +420,8 @@ export function interpolateConfig(config, scope, options) {
313
420
  if (!config || typeof config !== 'object' || Array.isArray(config))
314
421
  return walk(config, scope, context, base, skip);
315
422
  /**
316
- * The config's own top-level keys, readable bare: `${{ publish.directory }}`. So a value that
317
- * restates another - `after: "cp README.md ${{ publish.directory }}/"` - stops being a second
423
+ * The config's own top-level keys, readable bare: `${{ changelog.filePath }}`. So a value that
424
+ * restates another - `after: "cp README.md ${{ changelog.filePath }}/"` - stops being a second
318
425
  * copy that drifts when the first one changes.
319
426
  *
320
427
  * Resolved **on demand**, one key at a time, and memoized. Interpolating the config in tree order
@@ -374,6 +481,13 @@ export function interpolateConfig(config, scope, options) {
374
481
  const result = {};
375
482
  for (const key of Object.keys(config))
376
483
  result[key] = resolve(key);
484
+ /**
485
+ * **The one cast in the whole two-view split, and it is here rather than at every read.** No type
486
+ * can prove that a runtime walk turned `T` into `Resolved<T>`; this walk is what makes it true.
487
+ * Putting it at this single return is what keeps `pkg.config` honest without a cast in any of the
488
+ * commands - which is the arrangement the alternative (widening `RmanConfig` alone) gave up, six
489
+ * read sites at a time.
490
+ */
377
491
  return result;
378
492
  }
379
493
  /**
@@ -401,7 +515,7 @@ export const DEFERRED_PATHS = ['version.before', 'version.exec', 'version.after'
401
515
  * ```
402
516
  *
403
517
  * **The key decides, and it already did.** `run.build.exec: 'tsc -b'` is a shell command and
404
- * `publish.directory: 'build'` is a path - not because of anything about the strings, but because of
518
+ * `publish.npm.directory: 'build'` is a path - not because of anything about the strings, but because of
405
519
  * where they sit. A function inherits the same rule, so nothing new has to be learned and no marker
406
520
  * has to be remembered. The alternative was inspecting the function (arity, parameter names), which
407
521
  * is the kind of guess `loadPlugins` refuses to make about a module's export for the same reason:
@@ -429,16 +543,23 @@ export const STEP_PATHS = [
429
543
  ];
430
544
  /**
431
545
  * Keys whose **whole subtree** is code rather than config, so no function under them is a value to
432
- * compute. `plugins` is the only one, and it has to be here: an entry may be the plugin *object*
433
- * itself, and an `RmanPlugin` is almost entirely functions - `manifest.read`, `workspace.resolve`,
434
- * `versionPlanner`, `binPaths`, and every command's `builder` and `handler`.
435
- *
436
- * Measured, and it is why this exists: with `plugins` walked like any other key, resolving the
437
- * config of a repository that named a plugin called that plugin's yargs builder with the config
438
- * scope - `Config function in "plugins[0].commands[0].builder" failed: cmd.option is not a
439
- * function`. A `plugins` entry is loaded by `loadPlugins`, never read as a setting.
546
+ * compute.
547
+ *
548
+ * The three contribution keys, and each has to be here: an entry may be the *instance* itself, and
549
+ * a `Plugin` is almost entirely functions - `manifestProvider.read`, `getWorkspace`,
550
+ * `getBinPaths`, `versionPlanner` - while a command is often a bare factory and a publish target
551
+ * carries `getPlan`/`applyPlan`.
552
+ *
553
+ * Measured twice, once per shape. With `plugins` walked like any other key, resolving the config
554
+ * of a repository that named a plugin called that plugin's yargs builder with the config scope:
555
+ * `Config function in "plugins[0].commands[0].builder" failed: cmd.option is not a function`. And
556
+ * with `commands` left out of this list, a declarative command - which *is* a function - was
557
+ * invoked with the interpolation scope instead of the application, so its handler closed over a
558
+ * repository that was not one: `repository.getPackages is not a function`, from inside `clean`.
559
+ *
560
+ * These entries are loaded by `loadPlugins` and `cli.ts`, never read as settings.
440
561
  */
441
- export const CODE_SUBTREES = ['plugins'];
562
+ export const CODE_SUBTREES = ['plugins', 'commands', 'publishTargets'];
442
563
  const EXPRESSION = /\$\{\{([\s\S]*?)\}\}/g;
443
564
  /** A config key an expression could actually name. Anything else - a `"[selector]"` block, a
444
565
  * `"lint:fix"` - is unreachable as a bare identifier anyway, so it is not bound. */
@@ -656,13 +777,14 @@ function shortenOrigin(file) {
656
777
  *
657
778
  * **Always bound, even with nothing underneath.** Left unbound, an expression naming it fails with
658
779
  * V8's `value is not defined`, which reads as "there is no such thing" rather than "nothing below
659
- * this layer set it" - two different mistakes needing two different fixes.
780
+ * this layer set it" - two different mistakes needing two different fixes. With nothing underneath
781
+ * it is `unsetValue()` rather than `undefined` - see there.
660
782
  *
661
783
  * The chain resolves bottom-up, so a layer deriving from a layer that itself derived from something
662
784
  * is handed the finished value rather than a half-resolved expression.
663
785
  */
664
786
  function walkWithPrevious(item, previous, scope, context, at, skip) {
665
- const resolved = previous === undefined ? undefined : walkWithPrevious(previous.value, previous.previous, scope, context, at, skip);
787
+ const resolved = previousValue(previous === undefined ? undefined : walkWithPrevious(previous.value, previous.previous, scope, context, at, skip), at);
666
788
  const outer = Object.getOwnPropertyDescriptor(context, VALUE_KEY);
667
789
  /** A getter, so the catch below can tell whether the value **actually read `value`**: the hint is
668
790
  * irrelevant to any other failure, and attaching it anyway is the send-the-reader-to-the-wrong-
@@ -681,22 +803,18 @@ function walkWithPrevious(item, previous, scope, context, at, skip) {
681
803
  }
682
804
  catch (e) {
683
805
  /**
684
- * **`value` is `undefined` when no layer underneath set this key**, and a value written to
685
- * extend an inherited list is also the *first* layer in a repository that inherits nothing.
686
- * V8 reports that as `value is not iterable`, naming neither the key nor the reason.
687
- *
688
- * Here rather than in `callValueFn`, so the expression and the function spelling get the same
689
- * sentence from the same place. `rmanValueHint` keeps a rethrow from stacking it twice as the
690
- * error passes back up through the enclosing keys.
806
+ * **The hint is only about *reading* `value`**, so it is attached only when the value did -
807
+ * recorded through the getter above, never matched on V8's wording. Attaching it to any other
808
+ * failure is the send-the-reader-to-the-wrong-place mistake it exists to prevent.
691
809
  *
692
- * Not papered over by defaulting `value` to `[]`: that would be a guess about the key's type,
693
- * and wrong for every key that is not a list.
810
+ * The sentence itself comes from `unsetValue`, which knows the key and throws at the exact
811
+ * point of misuse; all this adds is the case the sentinel cannot catch, where a value reads
812
+ * `value` and fails for a reason of its own. `rmanValueHint` keeps a rethrow from stacking it
813
+ * twice as the error passes back up through the enclosing keys.
694
814
  */
695
- if (wasRead && resolved === undefined && !e?.rmanValueHint) {
815
+ if (wasRead && isUnsetValue(resolved) && !e?.rmanValueHint) {
696
816
  e.rmanValueHint = true;
697
- e.message =
698
- `${e.message}\n \`value\` is undefined here - nothing below this layer sets "${describeAt(at)}.` +
699
- `\n Write \`value ?? []\` (or \`?? ''\`) if it has to work as the first layer too.`;
817
+ e.message = `${e.message}\n Note: nothing below this layer sets "${describeAt(at)}, so \`value\` is empty.`;
700
818
  }
701
819
  throw e;
702
820
  }
@@ -937,3 +1055,69 @@ function deepFreeze(value) {
937
1055
  for (const item of Object.values(value))
938
1056
  deepFreeze(item);
939
1057
  }
1058
+ /**
1059
+ * What a layer is handed as `value`: **the list form of whatever is underneath it.**
1060
+ *
1061
+ * `value` exists for one job - extending what a closer layer inherited, the general form of `+key`,
1062
+ * and `+key` only ever meant append. So the shape a spread wants is the shape to hand over:
1063
+ * `[...value, 'x']` works with no guard whether the layers below said nothing, said `'build'`, or
1064
+ * said `['build']`.
1065
+ *
1066
+ * **Coercing a scalar into a one-element list is not a guess about the key's type.** Every key this
1067
+ * is reached for is declared `X | X[]` - `clean.include`, `clean.exclude`, `version.stamp`,
1068
+ * `version.before`/`.exec`/`.after` - where the list is the type and the scalar is *shorthand*.
1069
+ * `CleanService` and `RunService` already normalize it; doing it here as well decides nothing new.
1070
+ * And spreading a string into its characters, which is what handing the raw value over did, is not
1071
+ * something any key wants.
1072
+ *
1073
+ * It reads as the scalar wherever a scalar is what makes sense, through `Symbol.toPrimitive`:
1074
+ * `` `${value}-x` `` is `'build-x'` and `value + 1` is `6`. A *list* underneath refuses both, since
1075
+ * splicing `a,b` into a sentence is a mistake worth naming; so does nothing-underneath.
1076
+ *
1077
+ * **A boolean is handed over as itself**, the one carve-out, because it is never a list nor a
1078
+ * list's shorthand - and an object cannot be fixed up for it: `!value` and `value ? :` use
1079
+ * ToBoolean, which has no hook and answers `true` for every object, so a wrapped `false` would read
1080
+ * as `true`. Measured. `[...value]` on one then throws, which is right - spreading a boolean means
1081
+ * nothing.
1082
+ *
1083
+ * **The cost, stated rather than hidden: strict equality and string methods on an inherited
1084
+ * scalar.** `value === 'build'` is `false` and `value.includes('bui')` is `false` (an array's
1085
+ * `includes` matches elements, not substrings). `value == 'build'`, `` `${value}` === 'build' `` and
1086
+ * `String(value).includes('bui')` all work, and `value.length` is the number of layers' worth of
1087
+ * entries rather than a string's length. That is the trade for the append case never needing a
1088
+ * guard; `value` was introduced for the append case.
1089
+ */
1090
+ function previousValue(raw, at) {
1091
+ /** Never a list, and unfixable as one - see above. */
1092
+ if (typeof raw === 'boolean')
1093
+ return raw;
1094
+ const list = raw === undefined ? [] : Array.isArray(raw) ? [...raw] : [raw];
1095
+ Object.defineProperty(list, UNSET_MARKER, { value: raw === undefined });
1096
+ return Object.defineProperty(list, Symbol.toPrimitive, {
1097
+ value: (hint) => {
1098
+ if (raw === undefined) {
1099
+ throw unusable(at, 'nothing below this layer sets it, so it is empty', "`value ?? ''`, `value ?? 0`");
1100
+ }
1101
+ if (typeof raw !== 'string' && typeof raw !== 'number') {
1102
+ throw unusable(at, `the layer below it is ${Array.isArray(raw) ? 'a list' : 'an object'}`, '`value.join(", ")` for a list');
1103
+ }
1104
+ return hint === 'string' ? String(raw) : raw;
1105
+ },
1106
+ });
1107
+ }
1108
+ /** The one sentence both refusals share: what was asked for, why it cannot be done, what to write
1109
+ * instead. Marked `rmanValueHint` so `walkWithPrevious`'s catch leaves it alone - that note exists
1110
+ * to explain an empty `value` to an error that does not mention it, and this error *is* that
1111
+ * explanation. */
1112
+ function unusable(at, because, instead) {
1113
+ const error = new Error(`\`value\` cannot be used as a string or a number here - ${because}, for "${describeAt(at)}. ` +
1114
+ `It spreads as a list (\`[...value, x]\`); to use it as something else, say what it should be - ${instead}.`);
1115
+ error.rmanValueHint = true;
1116
+ return error;
1117
+ }
1118
+ /** Whether `value` stands for "no layer underneath set this key" - by the marker `previousValue`
1119
+ * puts on it, never by emptiness, since a layer may legitimately resolve to `[]`. */
1120
+ function isUnsetValue(value) {
1121
+ return Array.isArray(value) && value[UNSET_MARKER] === true;
1122
+ }
1123
+ const UNSET_MARKER = Symbol('rman.valueUnset');
@@ -0,0 +1,14 @@
1
+ import type { RmanApplication } from './application.js';
2
+ /**
3
+ * Every service the core brings, registered as an application is built.
4
+ *
5
+ * **Factories rather than instances**, so nothing is constructed until something asks: `rman info`
6
+ * has no business building the changelog or release services, and services reach each other through
7
+ * the application, so resolving at call time is also what keeps that from being a construction
8
+ * cycle.
9
+ *
10
+ * Here rather than inside `RmanApplication` so the composition list is one readable file, and so
11
+ * the application itself imports no service - only this does. A plugin adds its own the same way,
12
+ * from its own package, with `app.setService`.
13
+ */
14
+ export declare function registerCoreServices(app: RmanApplication): void;
@@ -0,0 +1,30 @@
1
+ import { ChangelogService } from '../services/changelog.service.js';
2
+ import { DockerPublishService } from '../services/docker-publish.service.js';
3
+ import { ExecService } from '../services/exec.service.js';
4
+ import { GithubReleaseService } from '../services/github-release.service.js';
5
+ import { ImportService } from '../services/import.service.js';
6
+ import { ListService } from '../services/list.service.js';
7
+ import { RunService } from '../services/run.service.js';
8
+ import { VersionService } from '../services/version.service.js';
9
+ /**
10
+ * Every service the core brings, registered as an application is built.
11
+ *
12
+ * **Factories rather than instances**, so nothing is constructed until something asks: `rman info`
13
+ * has no business building the changelog or release services, and services reach each other through
14
+ * the application, so resolving at call time is also what keeps that from being a construction
15
+ * cycle.
16
+ *
17
+ * Here rather than inside `RmanApplication` so the composition list is one readable file, and so
18
+ * the application itself imports no service - only this does. A plugin adds its own the same way,
19
+ * from its own package, with `app.setService`.
20
+ */
21
+ export function registerCoreServices(app) {
22
+ app.setService('changelog', a => new ChangelogService(a));
23
+ app.setService('dockerPublish', a => new DockerPublishService(a));
24
+ app.setService('githubRelease', a => new GithubReleaseService(a));
25
+ app.setService('exec', a => new ExecService(a));
26
+ app.setService('import', a => new ImportService(a));
27
+ app.setService('list', a => new ListService(a));
28
+ app.setService('run', a => new RunService(a));
29
+ app.setService('version', a => new VersionService(a));
30
+ }
@@ -0,0 +1,14 @@
1
+ import type { RmanApplication } from './application.js';
2
+ /**
3
+ * Every publish target the core brings, registered as an application is built - `registerCoreServices`
4
+ * for the other half of what a fresh application starts with.
5
+ *
6
+ * Exactly one today, and that is the point: `docker` is the target that belongs to no ecosystem, so
7
+ * it is the only one the core can honestly ship. `npm` arrives with the `node` built-in, and a Cargo or
8
+ * Maven target would arrive from its own plugin the same way.
9
+ *
10
+ * A separate file from `core-services.ts` rather than a second call inside it: a target is not a
11
+ * service (it is a contribution summed with others, not one replaceable answer), and a function
12
+ * called `registerCoreServices` that also registered targets would have to be read to be believed.
13
+ */
14
+ export declare function registerCoreTargets(app: RmanApplication): void;
@@ -0,0 +1,16 @@
1
+ import { dockerPublishTarget } from '../targets/docker.target.js';
2
+ /**
3
+ * Every publish target the core brings, registered as an application is built - `registerCoreServices`
4
+ * for the other half of what a fresh application starts with.
5
+ *
6
+ * Exactly one today, and that is the point: `docker` is the target that belongs to no ecosystem, so
7
+ * it is the only one the core can honestly ship. `npm` arrives with the `node` built-in, and a Cargo or
8
+ * Maven target would arrive from its own plugin the same way.
9
+ *
10
+ * A separate file from `core-services.ts` rather than a second call inside it: a target is not a
11
+ * service (it is a contribution summed with others, not one replaceable answer), and a function
12
+ * called `registerCoreServices` that also registered targets would have to be read to be believed.
13
+ */
14
+ export function registerCoreTargets(app) {
15
+ app.publishTargets.add(dockerPublishTarget);
16
+ }
@@ -1,4 +1,5 @@
1
1
  import type { ArgumentsCamelCase, Argv } from 'yargs';
2
+ import type { RmanConfig as CommandDeclaration } from '../interfaces/rman-config.interface.js';
2
3
  import type { Logger } from '../utils/logger.js';
3
4
  import type { RunBinOptions, RunBinResult } from '../utils/run-bin.js';
4
5
  import type { Package } from './package.js';
@@ -100,10 +101,25 @@ declare module 'yargs' {
100
101
  * that come with it; reimplementing that loop here would only lose them.
101
102
  */
102
103
  export declare function defineCommand(command: CustomCommand): CustomCommand;
103
- export interface LoadedCommand extends CustomCommand {
104
+ /**
105
+ * One command module that loaded, in whichever form it exported.
106
+ *
107
+ * Both forms are accepted, and the same pair is accepted for a command written straight into
108
+ * `.rmanrc "commands"` - one key, one set of rules. The declarative factory is what rman asks a
109
+ * command author to write; a repository's own command should not be stuck on the older object
110
+ * shape just because it lives in a file rather than in a config.
111
+ */
112
+ export interface LoadedCommand {
104
113
  /** The command's name - its file's basename, or the first word of an explicit `command`. */
105
114
  name: string;
106
115
  file: string;
116
+ /** The declarative form (`app => ({ ... })`). `cli.ts` runs it where a plugin's and a built-in's
117
+ * own factories run, because it wants `app.repository` and loading happens before one exists. */
118
+ register?: CommandDeclaration.CommandRegisterFunction;
119
+ /** The `defineCommand({ ... })` object form, already checked and named. */
120
+ custom?: CustomCommand & {
121
+ command: string;
122
+ };
107
123
  }
108
124
  /** A module that couldn't be loaded or doesn't look like a command. Reported, never thrown: one
109
125
  * unparseable file must not take `rman publish` down with it. */
@@ -112,15 +128,32 @@ export interface CommandLoadError {
112
128
  reason: string;
113
129
  }
114
130
  /**
115
- * Loads every command module in `<root>/.rman`. A repository without that directory pays nothing -
116
- * no scan, no imports - which matters because this runs on *every* rman invocation, `info`
117
- * included.
131
+ * The globs a repository's own commands are loaded from when it names none: `.rman/*.{js,mjs,cjs}`
132
+ * under the repository root.
133
+ *
134
+ * **`.rman/` is this default, not a second mechanism.** It used to be a hardcoded directory scan
135
+ * beside which `commands` would have been a third source of repository-level commands - and a
136
+ * third precedence question. Making it the default value instead leaves one source, one slot, and
137
+ * a zero-config path that behaves exactly as it did.
138
+ */
139
+ export declare function defaultCommandGlobs(rootDir: string): string[];
140
+ /**
141
+ * Loads every command module matching `patterns` - absolute globs, already anchored to whichever
142
+ * config file declared them (see `anchorContributions`).
143
+ *
144
+ * A repository matching nothing pays for one glob and no imports, which matters because this runs
145
+ * on *every* rman invocation, `info` included.
146
+ *
147
+ * **Deduplicated by resolved path**, because `commands` appends at every level and cascades: the
148
+ * root's glob reaches each package's resolved config too, so the same file is named more than once
149
+ * as a matter of course rather than as a mistake. Loading it twice would register the command
150
+ * twice, which yargs does not survive.
118
151
  *
119
152
  * Each failure is collected rather than thrown, so the rest of the CLI keeps working; the caller
120
153
  * warns about them. What is *not* tolerated is a module that would shadow a built-in - see
121
154
  * `assertNoBuiltinShadowing`.
122
155
  */
123
- export declare function loadCustomCommands(rootDir: string): Promise<{
156
+ export declare function loadCustomCommands(patterns: string[]): Promise<{
124
157
  commands: LoadedCommand[];
125
158
  errors: CommandLoadError[];
126
159
  }>;
@@ -130,4 +163,7 @@ export declare function loadCustomCommands(rootDir: string): Promise<{
130
163
  * `rman publish` that is safe to guess at. Silently preferring either one would leave whoever typed
131
164
  * it unable to tell which ran.
132
165
  */
133
- export declare function assertNoBuiltinShadowing(commands: LoadedCommand[], builtins: readonly string[]): void;
166
+ export declare function assertNoBuiltinShadowing(commands: readonly {
167
+ name: string;
168
+ file: string;
169
+ }[], builtins: readonly string[]): void;