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/plugin.d.ts CHANGED
@@ -1,112 +1,157 @@
1
- import type { RmanConfig } from '../interfaces/rman-config.interface.js';
2
- import { RunService } from '../services/run.service.js';
3
- import { VersionPlanService } from '../services/version-plan.service.js';
4
- import { BinPath } from '../utils/bin-path.js';
5
- import type { CustomCommand, LoadedCommand } from './custom-command.js';
6
- import { type ManifestProvider } from './manifest.js';
7
- import { Workspace } from './workspace.js';
8
- /** The `.rmanrc` key naming plugin packages to load. */
9
- export declare const PLUGINS_KEY = "plugins";
1
+ import type { RunService } from '../services/run.service.js';
2
+ import type { VersionPlanService } from '../services/version-plan.service.js';
3
+ import type { BinPath } from '../utils/bin-path.js';
4
+ import type { RmanApplication } from './application.js';
5
+ import type { ManifestProvider } from './manifest.js';
6
+ import type { Workspace } from './workspace.js';
10
7
  /**
11
- * What a plugin package hands rman.
8
+ * **One technology, as a whole** - Node, Cargo, Maven. Everything rman needs in order to treat a
9
+ * directory as a package of that technology.
12
10
  *
13
- * An object rather than a bare array of commands, for the reason `CommandContext` is one: a plugin
14
- * will eventually contribute more than commands (config defaults, publish targets, a package
15
- * provider for a non-Node repository), and nothing written against this should have to change when
16
- * it does.
11
+ * **This was called `Plugin`, and the name was a lie.** `manifestProvider` is required, which makes
12
+ * the type a *platform* by definition: a package that only ships commands never touches it, because
13
+ * commands are a config key. So the narrow thing took the narrow name, and `Plugin` became the
14
+ * broad one that may carry platforms - a plugin *provides* platforms, and may provide more than
15
+ * one (a package shipping both `maven` and `gradle` is one plugin, two platforms).
16
+ *
17
+ * These were six independent fields on a plugin, each with its own registry, and declaring one
18
+ * without the others type-checked. It is not a shape that admits sense: `getRunSteps` reads
19
+ * `pkg.manifest.raw?.scripts`, so contributing npm's step source without npm's manifest reader
20
+ * leaves it parsing whatever another technology produced. The coupling was already real; only the
21
+ * type failed to say so.
22
+ *
23
+ * **`RmanPlugin` and `TechStack` used to be two types and are now one.** The plugin existed solely to
24
+ * *register* the stack - its whole `init` was `ctx.addTechStack(...)` plus a command or two - and
25
+ * once a config carries commands and publish targets itself, that registration step has nothing
26
+ * left to do. What remains of a plugin is the technology, so that is what the type is.
27
+ *
28
+ * **`manifestProvider` stays grouped rather than flattened in here**, and it was tried the other
29
+ * way first: nine members about one file - reading it, writing it, what it declares, how its
30
+ * versions are numbered, stamped and looked up on a registry - are more legible as a named group
31
+ * than as nine siblings of `getBinPaths`. The name also keeps its distance from `Package.manifest`,
32
+ * which is the *data* this produces rather than the reader.
33
+ *
34
+ * **Everything optional is answered by its absence**, never by a default rman invented - see
35
+ * `basePlatform`.
17
36
  */
18
- export interface RmanPlugin {
19
- /** For error messages and `--help` grouping. Conventionally the package's own name. */
20
- name: string;
21
- commands?: CustomCommand[];
37
+ export interface Platform {
22
38
  /**
23
- * Where `run` can find a package's steps besides its `.rmanrc` - `rman-node` contributes
24
- * `package.json#scripts` here, with npm's `pre`/`post` lifecycle.
39
+ * **The ecosystem this speaks for**, surfaced on every package it reads as `Package.platform` -
40
+ * `'node'` for the built-in of that name. Short and about the technology, not about the file:
41
+ * `manifestProvider.fileName` already says `package.json`, and a name repeating it would tell a
42
+ * caller nothing it did not have.
25
43
  *
26
- * Registered in `plugins` declaration order, and only for plugins the repository actually named:
27
- * what a script resolves to is then a function of the config rather than of what happened to be
28
- * imported.
44
+ * This is what lets code that *does* know one ecosystem check before acting on a package -
45
+ * `if (pkg.platform === 'node')` - which matters most in a repository holding more than one,
46
+ * since a manifest is read per directory and two packages can legitimately answer to different
47
+ * technologies. It is also the de-duplication key: registering twice under one name is refused,
48
+ * because it would define the same commands twice and yargs does not survive that.
29
49
  */
30
- runSteps?: RunService.StepSource;
31
- /**
32
- * How this ecosystem's repositories are laid out - `rman-node` reads `workspaces` from the root
33
- * `package.json` here.
34
- *
35
- * Loaded **before any package is known**, since this is what finds them: `Repository.create`
36
- * reads the root config, loads the plugins it names, and only then asks. A repository naming no
37
- * plugin therefore has no packages beyond itself.
38
- */
39
- workspace?: Workspace.Provider;
40
- /**
41
- * Where a package's name and version are written, and how it is numbered - `rman-node`
42
- * contributes `package.json` here.
43
- *
44
- * Registered before any package is constructed, since `Package` reads through it. A repository
45
- * naming no plugin therefore gets packages named after their own directories at version
46
- * `0.0.0` - see `readManifest`.
47
- */
48
- manifest?: ManifestProvider;
50
+ name: string;
51
+ /** Where this technology's packages keep their identity, and how to change it - reading and
52
+ * writing the manifest, what it declares as dependencies, how its versions are numbered,
53
+ * stamped, and looked up on a registry. */
54
+ manifestProvider: ManifestProvider;
55
+ /** Where this technology's packages are, given the repository root - npm reads `workspaces`,
56
+ * Cargo a `[workspace] members`. `undefined` when it does not recognize the root. */
57
+ getWorkspace?: Workspace.Provider;
58
+ /** Directories to put in front of a child process's PATH, so a command an author wrote
59
+ * (`eslint .`) runs the repository's pinned copy - `node_modules/.bin` walked up the tree, for
60
+ * npm. Every plugin contributes, unlike the seams above where the first answer wins: a PATH is
61
+ * a list, and a polyglot repository wants both ecosystems' binaries reachable. */
62
+ getBinPaths?: BinPath.Provider;
49
63
  /**
50
- * How a release is planned - which packages have changed since their last release and what
51
- * version each gets. `rman-node` contributes `NodeVersionPlanService` here.
52
- *
53
- * **`VersionPlanService` is abstract, so `version`/`changed` do not work without one** (they fail
54
- * naming this key). Unlike `manifest` and `workspace`, which degrade to honest defaults, a plan is
55
- * either right or it quietly releases the wrong set of packages - see `VersionPlanService`.
64
+ * What the *package itself* declares for a lifecycle script, from this technology's own files -
65
+ * npm's `package.json` `scripts`, with `pre<script>`/`<script>`/`post<script>`.
56
66
  *
57
- * Consulted when a command asks, not at load time, so this is declared and nothing else has to
58
- * happen as the plugin's module is imported.
67
+ * **A query, not a hook, and not build-specific** - which is why it is `getRunSteps` rather than
68
+ * anything beginning `on`. It returns what the package declares and rman decides what to do with
69
+ * it; `run`/`build`/`test` ask, and so does `version`, whose `preversion`/`version`/`postversion`
70
+ * are the same shape (`runLifecycleSlot` -> `contributedSlots`). A name mentioning `build` would
71
+ * have been wrong about half its callers.
59
72
  */
73
+ getRunSteps?: RunService.StepSource;
74
+ /** How this technology's releases are planned - where a package's change boundary comes from
75
+ * when it has no release tag, and how far into its group a bump reaches. */
60
76
  versionPlanner?: VersionPlanService;
61
- /**
62
- * Where this ecosystem keeps a repository's locally installed executables - `rman-node`
63
- * contributes npm's `node_modules/.bin`, walked up the directory chain.
64
- *
65
- * Prepended to PATH for every `exec`/`runBin` child process, so a command an author wrote runs
66
- * against the repository's own pinned tools. **Every plugin's entries are used**, not just the
67
- * first - see `BinPath`.
68
- */
69
- binPaths?: BinPath.Provider;
70
77
  }
71
78
  /**
72
- * Identity helper for authoring a plugin with full type-checking - the `defineConfig`/
73
- * `defineCommand` pattern again, for the same reason. Returns `plugin` unchanged.
74
- *
75
- * **A plugin package's entry point exports a *config*, not this**, so that a package is an
76
- * `.rmanrc` like any other and can carry a second plugin later without changing shape:
79
+ * **A plugin is whatever a package contributes; a platform is one of the things it can contribute.**
77
80
  *
78
- * ```js
79
- * // rman-node's entry point
80
- * import { defineConfig, definePlugin } from 'rman';
81
- * import publishCommand from './commands/publish.js';
81
+ * The broad half of the split. `Platform` is a technology and nothing else - required manifest
82
+ * reader, workspace layout, bin paths, steps, version planning. What is left over is this: an
83
+ * `init` for whatever the seams do not name yet, and the **declaration** that this plugin provides
84
+ * platforms.
82
85
  *
83
- * export const nodePlugin = definePlugin({ name: 'rman-node', commands: [publishCommand] });
84
- * export default defineConfig({ plugins: [nodePlugin] });
85
- * ```
86
+ * **`platforms` is a list, and that is not symmetry.** One package can speak for more than one
87
+ * technology - `maven` and `gradle` are the same toolchain family and a single plugin shipping both
88
+ * is the natural shape. A singular field would have made the second one a separate package for no
89
+ * reason.
86
90
  *
87
- * **A module exporting the plugin itself is refused**, with a message saying so. Accepting both
88
- * would mean telling a plugin from a config at runtime, and `name` is a key either may have - the
89
- * test would be a guess, and guessing "plugin" registers nothing while reporting success.
91
+ * A bare `Platform` is accepted anywhere a `Plugin` is, as sugar for `{ name, platforms: [it] }` -
92
+ * which is what almost every entry is.
90
93
  */
91
- export declare function definePlugin(plugin: RmanPlugin): RmanPlugin;
94
+ export interface Plugin {
95
+ /** How this plugin is named in messages, and the de-duplication key: registering twice under one
96
+ * name is refused. A platform contributed by it keeps its *own* name, which is what a package
97
+ * reports as `pkg.platform`. */
98
+ name: string;
99
+ /** The technologies this plugin provides, if any. */
100
+ platforms?: Platform[];
101
+ /**
102
+ * Anything this plugin contributes that is not a platform, run once when it is registered.
103
+ *
104
+ * **The escape hatch, not the front door.** Commands and publish targets are `.rmanrc` keys
105
+ * (`commands`, `publishTargets`), so a plugin contributing only those declares them in its own
106
+ * config and needs no `init` at all. What is left for `init` is whatever the seams do not name
107
+ * yet, reached through `ctx.app`.
108
+ *
109
+ * **It runs inside `Repository.create`**, before any package is known, so `ctx.app.repository`
110
+ * throws there. Anything wanting the repository belongs in a command's factory instead, which
111
+ * runs once there is one.
112
+ */
113
+ init?(ctx: PluginContext): void | Promise<void>;
114
+ }
92
115
  /**
93
- * Loads every plugin the repository's `.rmanrc "plugins"` declares, in declaration order.
116
+ * What `Plugin.init` is handed.
94
117
  *
95
- * This is what lets a *package* contribute commands. `.rman/*.mjs` covers one repository's own
96
- * commands (see `loadCustomCommands`); a plugin covers a whole class of repository - `rman-node`
97
- * carrying everything that only means something because the repository is a Node one, so rman's
98
- * core does not have to.
118
+ * Only the application today. An object rather than a bare parameter so a member added later
119
+ * breaks nothing already written against it - the same reason `CommandContext` is one, which has
120
+ * already paid for itself twice.
121
+ */
122
+ export interface PluginContext {
123
+ app: RmanApplication;
124
+ }
125
+ /** Whether `value` came from `definePlatform` or `definePlugin`. */
126
+ export declare function isDeclared(value: unknown): boolean;
127
+ /** Declares a **platform** - one technology, whole. Returns it unchanged apart from the mark. */
128
+ export declare function definePlatform(platform: Platform): Platform;
129
+ /** Declares a **plugin** - whatever a package contributes, platforms included. Returns it unchanged
130
+ * apart from the mark. */
131
+ export declare function definePlugin(plugin: Plugin): Plugin;
132
+ /** Whether `value` is a platform rather than the broader plugin - `manifestProvider` is what makes
133
+ * one, and it is the only required member either type has beyond `name`. */
134
+ export declare function isPlatform(value: Plugin | Platform): value is Platform;
135
+ /**
136
+ * The platform a package gets when **none** claimed its directory - a repository naming no
137
+ * platform, or a directory none of the named ones recognized.
99
138
  *
100
- * An entry is a package name, a path, or a plugin object - and a named package's entry point
101
- * exports a **config**, whose own `plugins` are then loaded the same way (see `loadInto`).
139
+ * It exists so `Package.platform` need not be optional. `Package.provider` was an empty string for
140
+ * exactly this case, and every reader had to know that; an object with an empty `name` says the
141
+ * same thing without a guard, and `pkg.provider === 'node'` - the check CLAUDE.md prescribes -
142
+ * reads the same either way.
102
143
  *
103
- * Names resolve relative to the repository root's own `node_modules`, the way `extends` resolves -
104
- * a plugin is the repository's dependency, not rman's.
144
+ * **Every documented behaviour of "no plugin" survives unchanged**, because the absences are the
145
+ * behaviour:
105
146
  *
106
- * **A plugin that cannot be loaded throws**, unlike a broken `.rman/*.mjs` file, which is warned
107
- * about and skipped. The two differ because the consequence does: a skipped local command affects
108
- * only itself, while a missing plugin silently removes commands the repository is built around -
109
- * `rman publish` would simply not exist, and "not a known command" sends the reader looking in the
110
- * wrong place entirely.
147
+ * - a reader that recognizes nothing -> `Manifest.read` falls through to its own empty manifest,
148
+ * exactly as it does when no plugin answers;
149
+ * - no `getWorkspace` -> a repository naming no plugin has no packages beyond itself, which is the
150
+ * boundary working rather than failing (`workspaces` in a `package.json` is npm's idea);
151
+ * - no `getBinPaths` -> nothing is prepended to a child process's PATH, so the inherited one
152
+ * stands on its own rather than being guessed at;
153
+ * - no `getRunSteps` -> a package's steps come from its `.rmanrc` alone, the core's only source;
154
+ * - no `versionPlanner` -> `version`/`changed` fail naming the key, rather than releasing a
155
+ * plausible but untrue set of packages from a default nobody chose.
111
156
  */
112
- export declare function loadPlugins(rootDir: string, rootConfig: RmanConfig): Promise<LoadedCommand[]>;
157
+ export declare const basePlatform: Platform;
package/core/plugin.js CHANGED
@@ -1,189 +1,86 @@
1
- import path from 'node:path';
2
- import { pathToFileURL } from 'node:url';
3
- import { RunService } from '../services/run.service.js';
4
- import { VersionPlanService } from '../services/version-plan.service.js';
5
- import { BinPath } from '../utils/bin-path.js';
6
- import { Manifest } from './manifest.js';
7
- import { resolveConfigTarget } from './resolve-target.js';
8
- import { Workspace } from './workspace.js';
9
- /** The `.rmanrc` key naming plugin packages to load. */
10
- export const PLUGINS_KEY = 'plugins';
11
- /**
12
- * Identity helper for authoring a plugin with full type-checking - the `defineConfig`/
13
- * `defineCommand` pattern again, for the same reason. Returns `plugin` unchanged.
14
- *
15
- * **A plugin package's entry point exports a *config*, not this**, so that a package is an
16
- * `.rmanrc` like any other and can carry a second plugin later without changing shape:
17
- *
18
- * ```js
19
- * // rman-node's entry point
20
- * import { defineConfig, definePlugin } from 'rman';
21
- * import publishCommand from './commands/publish.js';
22
- *
23
- * export const nodePlugin = definePlugin({ name: 'rman-node', commands: [publishCommand] });
24
- * export default defineConfig({ plugins: [nodePlugin] });
25
- * ```
26
- *
27
- * **A module exporting the plugin itself is refused**, with a message saying so. Accepting both
28
- * would mean telling a plugin from a config at runtime, and `name` is a key either may have - the
29
- * test would be a guess, and guessing "plugin" registers nothing while reporting success.
30
- */
1
+ /** Whether `value` came from `definePlatform` or `definePlugin`. */
2
+ export function isDeclared(value) {
3
+ return !!value && typeof value === 'object' && value[declaredKey()] === true;
4
+ }
5
+ /** Declares a **platform** - one technology, whole. Returns it unchanged apart from the mark. */
6
+ export function definePlatform(platform) {
7
+ return brand(platform);
8
+ }
9
+ /** Declares a **plugin** - whatever a package contributes, platforms included. Returns it unchanged
10
+ * apart from the mark. */
31
11
  export function definePlugin(plugin) {
32
- return plugin;
12
+ return brand(plugin);
13
+ }
14
+ /** Whether `value` is a platform rather than the broader plugin - `manifestProvider` is what makes
15
+ * one, and it is the only required member either type has beyond `name`. */
16
+ export function isPlatform(value) {
17
+ return !!value.manifestProvider;
33
18
  }
34
19
  /**
35
- * Loads every plugin the repository's `.rmanrc "plugins"` declares, in declaration order.
20
+ * The platform a package gets when **none** claimed its directory - a repository naming no
21
+ * platform, or a directory none of the named ones recognized.
36
22
  *
37
- * This is what lets a *package* contribute commands. `.rman/*.mjs` covers one repository's own
38
- * commands (see `loadCustomCommands`); a plugin covers a whole class of repository - `rman-node`
39
- * carrying everything that only means something because the repository is a Node one, so rman's
40
- * core does not have to.
23
+ * It exists so `Package.platform` need not be optional. `Package.provider` was an empty string for
24
+ * exactly this case, and every reader had to know that; an object with an empty `name` says the
25
+ * same thing without a guard, and `pkg.provider === 'node'` - the check CLAUDE.md prescribes -
26
+ * reads the same either way.
41
27
  *
42
- * An entry is a package name, a path, or a plugin object - and a named package's entry point
43
- * exports a **config**, whose own `plugins` are then loaded the same way (see `loadInto`).
28
+ * **Every documented behaviour of "no plugin" survives unchanged**, because the absences are the
29
+ * behaviour:
44
30
  *
45
- * Names resolve relative to the repository root's own `node_modules`, the way `extends` resolves -
46
- * a plugin is the repository's dependency, not rman's.
47
- *
48
- * **A plugin that cannot be loaded throws**, unlike a broken `.rman/*.mjs` file, which is warned
49
- * about and skipped. The two differ because the consequence does: a skipped local command affects
50
- * only itself, while a missing plugin silently removes commands the repository is built around -
51
- * `rman publish` would simply not exist, and "not a known command" sends the reader looking in the
52
- * wrong place entirely.
31
+ * - a reader that recognizes nothing -> `Manifest.read` falls through to its own empty manifest,
32
+ * exactly as it does when no plugin answers;
33
+ * - no `getWorkspace` -> a repository naming no plugin has no packages beyond itself, which is the
34
+ * boundary working rather than failing (`workspaces` in a `package.json` is npm's idea);
35
+ * - no `getBinPaths` -> nothing is prepended to a child process's PATH, so the inherited one
36
+ * stands on its own rather than being guessed at;
37
+ * - no `getRunSteps` -> a package's steps come from its `.rmanrc` alone, the core's only source;
38
+ * - no `versionPlanner` -> `version`/`changed` fail naming the key, rather than releasing a
39
+ * plausible but untrue set of packages from a default nobody chose.
53
40
  */
54
- export async function loadPlugins(rootDir, rootConfig) {
55
- const commands = [];
56
- /** Resolved against the repository root, where the `.rmanrc` declaring them lives. */
57
- await loadInto(commands, rootConfig, path.join(rootDir, '.rmanrc'), { files: new Set(), names: new Set() });
58
- return commands;
59
- }
41
+ export const basePlatform = definePlatform({
42
+ name: '',
43
+ manifestProvider: {
44
+ name: '',
45
+ fileName: '',
46
+ /** Recognizes nothing, which is the point: `Manifest.read` falls through to its own empty
47
+ * manifest exactly as it does when no plugin answers. */
48
+ read: () => undefined,
49
+ write: () => undefined,
50
+ },
51
+ });
60
52
  /**
61
- * One config's `plugins`, in declaration order.
53
+ * **Marks an object as declared through one of the factories below**, so `loadPlugins` can tell a
54
+ * 2.x contribution from anything else that happens to have the same shape.
62
55
  *
63
- * Recursive because a plugin package **exports a config**, not a plugin: `rman-node`'s entry point
64
- * is `export default defineConfig({ plugins: [ ... ] })`, so resolving a name lands on another
65
- * config whose own `plugins` are the ones to register. That also means a plugin package can name a
66
- * plugin of its own and it simply works.
56
+ * It exists for one measured case, and there is no structural check that could replace it: an rman
57
+ * **1.x plugin was `{ name, init }`**, and under this split a 2.x plugin that only runs an `init`
58
+ * is *also* `{ name, init }`. They are indistinguishable by shape. The guard used to be
59
+ * `manifestProvider` being required - which only worked while the one type was both halves.
67
60
  *
68
- * `from` is the file the entries are resolved against, and it changes as it descends - an entry in
69
- * `rman-node`'s config resolves through *its* `node_modules`, not the repository's, the same rule
70
- * `extends` follows.
61
+ * Non-enumerable, so it never reaches `JSON.stringify`, `rman config` or a `toEqual` diff, the same
62
+ * way `ORIGINS` and `PREVIOUS_VALUES` travel.
71
63
  *
72
- * **Only `plugins` is read out of an imported config.** Its other keys are not merged: a config's
73
- * way into a repository is `extends`, which is the key that says "merge this underneath mine".
74
- * Reading them here would make a plugin able to configure a repository by being installed.
75
- */
76
- async function loadInto(commands, config, from, seen) {
77
- const declared = config?.[PLUGINS_KEY];
78
- if (declared === undefined)
79
- return;
80
- for (const entry of Array.isArray(declared) ? declared : [declared]) {
81
- /**
82
- * The object form: a JS config handing a plugin over directly, and what a plugin package's own
83
- * config holds. Nothing to resolve or import.
84
- *
85
- * An object here **is** a plugin - it is not guessed at. The one thing checked is that it has a
86
- * `name`, because everything downstream (the registration guard, `--help` grouping, every error
87
- * message) is keyed by it.
88
- */
89
- if (isPlainObject(entry)) {
90
- if (typeof entry.name !== 'string' || !entry.name) {
91
- throw new Error(`A plugin object in "${PLUGINS_KEY}" has no "name" - every other message is keyed by it.`);
92
- }
93
- register(commands, entry, entry.name, from, seen);
94
- continue;
95
- }
96
- if (typeof entry !== 'string' || !entry.trim()) {
97
- throw new Error(`"${PLUGINS_KEY}" takes a package name, a path, or a plugin object - not ${JSON.stringify(entry)}`);
98
- }
99
- const file = resolveConfigTarget(entry, from, PLUGINS_KEY);
100
- /** A config naming itself, or two naming each other, would otherwise recurse forever. Keyed by
101
- * resolved file, so the same package reached by two names is still loaded once. */
102
- if (seen.files.has(file))
103
- continue;
104
- seen.files.add(file);
105
- const mod = await import(pathToFileURL(file).href);
106
- const exported = mod?.default ?? mod?.plugin;
107
- /**
108
- * **A module exports one thing: an rman config.** Not a plugin, and not either-or.
109
- *
110
- * Accepting both meant having to *tell them apart*, and there is no reliable way to - `name` is
111
- * a key a config may have as well, so the test came down to "a name plus at least one of the
112
- * things a plugin contributes", which is a guess. Guess wrong in the direction of "plugin" and
113
- * nothing is registered while the command reports success, which is the worst outcome on offer.
114
- * One shape, one rule, one error.
115
- */
116
- if (!isPlainObject(exported) || exported.plugins === undefined) {
117
- throw new Error(`Plugin "${entry}" must export an rman config - \`export default defineConfig({ plugins: [ ... ] })\`. ` +
118
- describeExport(exported));
119
- }
120
- await loadInto(commands, exported, file, seen);
121
- }
122
- }
123
- /**
124
- * Everything a plugin contributes, in one place - so the object and the imported forms cannot drift
125
- * apart in what they support.
64
+ * The cost, stated rather than hidden: a plain object literal in `plugins` is no longer accepted.
65
+ * `definePlatform`/`definePlugin` is one import and one call, and it is the explicit declaration
66
+ * that a runtime check needs - a type cannot reach a JavaScript config.
126
67
  *
127
- * **One registration per plugin name.** `plugins` appends at every layer now, so the same plugin
128
- * arriving twice is an ordinary consequence of `extends` rather than a mistake to report - and
129
- * registering it twice would define its commands twice, which yargs does not survive. The config
130
- * merge already drops an identical entry; this catches the rest, including two objects claiming one
131
- * name and an object that duplicates a named package.
132
- */
133
- function register(commands, plugin, label, specifier, seen) {
134
- if (seen.names.has(plugin.name))
135
- return;
136
- seen.names.add(plugin.name);
137
- if (plugin.runSteps)
138
- RunService.addStepSource(plugin.runSteps);
139
- if (plugin.manifest)
140
- Manifest.addProvider(plugin.manifest);
141
- if (plugin.workspace)
142
- Workspace.addProvider(plugin.workspace);
143
- if (plugin.binPaths)
144
- BinPath.addProvider(plugin.binPaths);
145
- if (plugin.versionPlanner)
146
- VersionPlanService.setPlanner(plugin.versionPlanner);
147
- for (const command of plugin.commands ?? []) {
148
- commands.push(toLoadedCommand(command, label || specifier, specifier));
149
- }
150
- }
151
- /**
152
- * The second half of the error above - what the module *did* export, so the author can see how far
153
- * off it was.
68
+ * **A function rather than a `const`, and that is not a preference.** The file layout puts private
69
+ * declarations below the exported ones, and `basePlatform` is an exported const whose initializer
70
+ * *calls* `definePlatform` - so a `const DECLARED` below it sits in its temporal dead zone while the
71
+ * module is still evaluating. Measured: the whole suite failed to load with
72
+ * `ReferenceError: Cannot access 'DECLARED' before initialization`. A function declaration hoists,
73
+ * so the key is resolved when it is asked for instead of when the module reaches this line.
154
74
  *
155
- * It recognizes a plugin object only to **say so in a message**. That is the one safe use for this
156
- * shape test: it decides nothing, so a wrong guess costs a slightly less helpful sentence rather
157
- * than a plugin that silently does not load.
75
+ * **`Symbol.for`, so two copies of rman in one process agree about the mark.** A per-module symbol
76
+ * would have a plugin declared against one copy refused by the other - with a message about rman
77
+ * 1.x plugins, which is the one explanation guaranteed to be wrong.
158
78
  */
159
- function describeExport(exported) {
160
- if (exported === undefined)
161
- return 'It has no default export.';
162
- if (!isPlainObject(exported))
163
- return `Its default export is a ${typeof exported}.`;
164
- const looksLikePlugin = PLUGIN_SEAMS.some(seam => exported[seam] !== undefined);
165
- return looksLikePlugin
166
- ? `Its default export looks like the plugin itself - put it in a config's "${PLUGINS_KEY}".`
167
- : `Its default export has no "${PLUGINS_KEY}".`;
168
- }
169
- const PLUGIN_SEAMS = ['commands', 'runSteps', 'workspace', 'manifest', 'versionPlanner', 'binPaths'];
170
- /** A config object, as opposed to an array or anything with its own prototype. */
171
- function isPlainObject(value) {
172
- return !!value && typeof value === 'object' && !Array.isArray(value);
79
+ function declaredKey() {
80
+ return Symbol.for('rman.declared');
173
81
  }
174
- /** A plugin's command, checked the same way a `.rman/*.mjs` one is - the name it answers to comes
175
- * from its own `command` string, since a plugin has no file name to fall back on. */
176
- function toLoadedCommand(command, pluginName, specifier) {
177
- const declared = command?.command?.trim();
178
- if (!declared) {
179
- throw new Error(`Plugin "${pluginName}" has a command with no "command" name - it cannot be registered.`);
180
- }
181
- if (typeof command.handler !== 'function') {
182
- throw new Error(`Plugin "${pluginName}" command "${declared}" has no "handler" function.`);
183
- }
184
- if (typeof command.describe !== 'string' || !command.describe) {
185
- throw new Error(`Plugin "${pluginName}" command "${declared}" has no "describe" - \`rman --help\` would have ` +
186
- `nothing to list it by.`);
187
- }
188
- return { ...command, command: declared, name: declared.split(/\s+/)[0], file: specifier };
82
+ /** Stamps the mark, non-enumerably, and hands the object back. */
83
+ function brand(value) {
84
+ Object.defineProperty(value, declaredKey(), { value: true, enumerable: false, configurable: true });
85
+ return value;
189
86
  }