rman 1.0.12 → 1.2.1

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 (102) hide show
  1. package/README.md +90 -70
  2. package/cli.js +226 -14
  3. package/commands/build.command.js +1 -0
  4. package/commands/changed.command.js +2 -2
  5. package/commands/changelog.command.js +13 -15
  6. package/commands/config.command.js +61 -0
  7. package/commands/diff.command.js +9 -4
  8. package/commands/exec.command.js +2 -8
  9. package/commands/github-release.command.js +1 -0
  10. package/commands/info.command.d.ts +9 -0
  11. package/commands/info.command.js +12 -2
  12. package/commands/run.command.js +5 -8
  13. package/commands/test.command.js +1 -0
  14. package/commands/version.command.js +53 -14
  15. package/constants.js +1 -1
  16. package/core/config.d.ts +265 -17
  17. package/core/config.js +651 -76
  18. package/core/custom-command.d.ts +133 -0
  19. package/core/custom-command.js +99 -0
  20. package/core/extends-config.d.ts +27 -0
  21. package/core/extends-config.js +89 -0
  22. package/core/manifest.d.ts +222 -0
  23. package/core/manifest.js +150 -0
  24. package/core/merge-config.d.ts +70 -0
  25. package/core/merge-config.js +193 -0
  26. package/core/package.d.ts +73 -7
  27. package/core/package.js +86 -24
  28. package/core/plugin.d.ts +112 -0
  29. package/core/plugin.js +189 -0
  30. package/core/repository.d.ts +91 -1
  31. package/core/repository.js +277 -132
  32. package/core/resolve-target.d.ts +12 -0
  33. package/core/resolve-target.js +33 -0
  34. package/core/run-step.d.ts +75 -0
  35. package/core/run-step.js +1 -0
  36. package/core/version-scheme.d.ts +134 -0
  37. package/core/version-scheme.js +148 -0
  38. package/core/workspace.d.ts +68 -0
  39. package/core/workspace.js +83 -0
  40. package/index.d.ts +55 -8
  41. package/index.js +42 -7
  42. package/interfaces/rman-config.interface.d.ts +222 -46
  43. package/package.json +16 -7
  44. package/services/change-hash.service.d.ts +88 -0
  45. package/services/change-hash.service.js +112 -0
  46. package/services/changelog.service.d.ts +8 -13
  47. package/services/changelog.service.js +12 -11
  48. package/services/conventional-commits.service.d.ts +73 -0
  49. package/services/conventional-commits.service.js +116 -0
  50. package/services/docker-publish.service.js +1 -1
  51. package/services/exec.service.js +1 -1
  52. package/services/github-release.service.d.ts +2 -2
  53. package/services/github-release.service.js +10 -5
  54. package/services/list.service.js +5 -2
  55. package/services/run.service.d.ts +112 -6
  56. package/services/run.service.js +265 -89
  57. package/services/system-info.d.ts +22 -7
  58. package/services/system-info.js +8 -23
  59. package/services/version-plan.service.d.ts +244 -0
  60. package/services/version-plan.service.js +414 -0
  61. package/services/version.service.d.ts +102 -82
  62. package/services/version.service.js +226 -434
  63. package/services.d.ts +5 -3
  64. package/services.js +5 -3
  65. package/utils/bin-path.d.ts +59 -0
  66. package/utils/bin-path.js +82 -0
  67. package/utils/child-tracker.d.ts +16 -0
  68. package/utils/child-tracker.js +30 -0
  69. package/utils/exec.d.ts +13 -2
  70. package/utils/exec.js +17 -17
  71. package/utils/git.d.ts +9 -3
  72. package/utils/git.js +10 -2
  73. package/utils/package-filter.d.ts +33 -2
  74. package/utils/package-filter.js +47 -7
  75. package/utils/printable-config.d.ts +15 -0
  76. package/utils/printable-config.js +42 -0
  77. package/utils/release-version.js +3 -3
  78. package/utils/run-bin.d.ts +46 -0
  79. package/utils/run-bin.js +63 -0
  80. package/utils/version-stamp.d.ts +14 -6
  81. package/utils/version-stamp.js +25 -13
  82. package/commands/ci.command.js +0 -30
  83. package/commands/clean.command.d.ts +0 -3
  84. package/commands/clean.command.js +0 -36
  85. package/commands/publish.command.d.ts +0 -3
  86. package/commands/publish.command.js +0 -225
  87. package/rmanrc.schema.json +0 -392
  88. package/services/ci.service.d.ts +0 -40
  89. package/services/ci.service.js +0 -204
  90. package/services/clean.service.d.ts +0 -42
  91. package/services/clean.service.js +0 -226
  92. package/services/publish.service.d.ts +0 -79
  93. package/services/publish.service.js +0 -273
  94. package/utils/change-hash.d.ts +0 -68
  95. package/utils/change-hash.js +0 -98
  96. package/utils/conventional-commits.d.ts +0 -52
  97. package/utils/conventional-commits.js +0 -90
  98. package/utils/npm-run-path.d.ts +0 -67
  99. package/utils/npm-run-path.js +0 -63
  100. package/utils/workspace-range.d.ts +0 -17
  101. package/utils/workspace-range.js +0 -28
  102. /package/commands/{ci.command.d.ts → config.command.d.ts} +0 -0
@@ -0,0 +1,133 @@
1
+ import type { ArgumentsCamelCase, Argv } from 'yargs';
2
+ import type { Logger } from '../utils/logger.js';
3
+ import type { RunBinOptions, RunBinResult } from '../utils/run-bin.js';
4
+ import type { Package } from './package.js';
5
+ import type { Repository } from './repository.js';
6
+ /** Where a repository keeps its own commands - one module per command, named after it. */
7
+ export declare const CUSTOM_COMMAND_DIR = ".rman";
8
+ /**
9
+ * What a repository's own command is handed. An object rather than loose parameters so later
10
+ * additions don't break every command already written against it.
11
+ */
12
+ export interface CommandContext {
13
+ repository: Repository;
14
+ /**
15
+ * The package whose directory rman was invoked from, or `undefined` at the repository root (and
16
+ * in a single-package repository, which is always "at the root") - the same
17
+ * `Repository.currentPackage` the built-in commands scope themselves by. A command that only
18
+ * makes sense inside a package should say so itself rather than assume.
19
+ */
20
+ package: Package | undefined;
21
+ /**
22
+ * Runs one of the repository's locally installed binaries - `runBin` (see
23
+ * `../utils/run-bin.ts`), already carrying **this run's** settings: `cwd` defaults to the
24
+ * repository root, and `logLevel` to the level resolved from `--log-level` and `.rmanrc
25
+ * logLevel`. Either can still be overridden per call.
26
+ *
27
+ * Handed over here rather than left to be imported, because those settings are the whole point:
28
+ * importing `runBin` straight from `'rman'` gets a helper that knows neither, so
29
+ * `--log-level silent` would quietly not apply to the one part of the command that produces
30
+ * output. Anything else a run turns out to carry is added here the same way, and no command
31
+ * written against this breaks.
32
+ */
33
+ runBin: (bin: string, argv: string[], options?: RunBinOptions) => Promise<RunBinResult>;
34
+ /** Logger at this run's resolved level, for a command's own narration. */
35
+ logger: Logger;
36
+ }
37
+ /**
38
+ * Which `.rmanrc` keys a command reads, for `--config` to print instead of running it - a dotted
39
+ * path each (`'run.build'`, `'publish.docker'`), or a function of the parsed argv when the answer
40
+ * depends on it (`run <script>` reads `run.<script>`).
41
+ *
42
+ * **Declared beside the command rather than in a list somewhere central**, so it cannot drift out
43
+ * of step with the code that does the reading, and so a plugin's command or a `.rman/*.mjs` one can
44
+ * say it too. Omitted, `--config` prints the whole effective config - the honest answer when
45
+ * nothing has said which part matters.
46
+ */
47
+ export type ConfigKeys = string[] | ((args: ArgumentsCamelCase) => string[]);
48
+ export interface CustomCommand {
49
+ /** yargs command string, for a command taking positionals (`'deploy <stage>'`). Defaults to the
50
+ * module's own file name, which is the whole point of the directory. */
51
+ command?: string;
52
+ /** Required: without it `rman --help` has nothing to list the command by. */
53
+ describe: string;
54
+ builder?: (argv: Argv) => Argv;
55
+ handler: (context: CommandContext, args: ArgumentsCamelCase) => void | Promise<void>;
56
+ /** See `ConfigKeys` - what `rman <this command> --config` narrows its output to. */
57
+ configKeys?: ConfigKeys;
58
+ }
59
+ /**
60
+ * The same field on **yargs's own** command object, which is what the built-in commands pass.
61
+ *
62
+ * By augmentation rather than a wrapper type of ours: `program.command({ ... })` takes a literal,
63
+ * and TypeScript's excess-property check fires on a literal however the parameter is typed - so a
64
+ * field yargs does not know about is a compile error even though it is ignored at runtime
65
+ * (measured, on nine commands at once). One block, here, beside `ConfigKeys` itself.
66
+ */
67
+ declare module 'yargs' {
68
+ interface CommandModule<T = {}, U = {}> {
69
+ configKeys?: ConfigKeys;
70
+ }
71
+ }
72
+ /**
73
+ * Identity helper for authoring a `.rman/<name>.mjs` command with full type-checking and
74
+ * autocomplete - the same `defineConfig` pattern, for the same reason. Returns `command`
75
+ * unchanged.
76
+ *
77
+ * ```js
78
+ * // .rman/deploy.mjs
79
+ * import { defineCommand, VersionService } from 'rman';
80
+ *
81
+ * export default defineCommand({
82
+ * describe: 'Ships what was just published to the staging cluster',
83
+ * builder: y => y.option('stage', { choices: ['dev', 'prod'], demandOption: true }),
84
+ * async handler({ repository, runBin, logger }, args) {
85
+ * const plan = await VersionService.getPlan(repository);
86
+ * for (const entry of plan.filter(e => e.status === 'bump')) {
87
+ * logger.info(`${entry.package.name} -> ${args.stage}`);
88
+ * }
89
+ * await runBin('helm', ['upgrade', '--install', args.stage, './chart']);
90
+ * },
91
+ * });
92
+ * ```
93
+ *
94
+ * Take `runBin` from the context rather than importing it: the one on the context already carries
95
+ * this run's `cwd` (the repository root) and log level.
96
+ *
97
+ * This is for **one repository-level operation with logic of its own** - branching, its own CLI
98
+ * options, rman's services. Running a shell step across every package is what `.rmanrc
99
+ * "run.<script>"` already does, with the scheduling, topological order, `bail` and progress panel
100
+ * that come with it; reimplementing that loop here would only lose them.
101
+ */
102
+ export declare function defineCommand(command: CustomCommand): CustomCommand;
103
+ export interface LoadedCommand extends CustomCommand {
104
+ /** The command's name - its file's basename, or the first word of an explicit `command`. */
105
+ name: string;
106
+ file: string;
107
+ }
108
+ /** A module that couldn't be loaded or doesn't look like a command. Reported, never thrown: one
109
+ * unparseable file must not take `rman publish` down with it. */
110
+ export interface CommandLoadError {
111
+ file: string;
112
+ reason: string;
113
+ }
114
+ /**
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.
118
+ *
119
+ * Each failure is collected rather than thrown, so the rest of the CLI keeps working; the caller
120
+ * warns about them. What is *not* tolerated is a module that would shadow a built-in - see
121
+ * `assertNoBuiltinShadowing`.
122
+ */
123
+ export declare function loadCustomCommands(rootDir: string): Promise<{
124
+ commands: LoadedCommand[];
125
+ errors: CommandLoadError[];
126
+ }>;
127
+ /**
128
+ * Refuses a command that would take a built-in's name. Unlike a module that simply fails to load,
129
+ * this one is thrown: the file is fine, the *name* is the mistake, and there is no reading of
130
+ * `rman publish` that is safe to guess at. Silently preferring either one would leave whoever typed
131
+ * it unable to tell which ran.
132
+ */
133
+ export declare function assertNoBuiltinShadowing(commands: LoadedCommand[], builtins: readonly string[]): void;
@@ -0,0 +1,99 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ /** Where a repository keeps its own commands - one module per command, named after it. */
5
+ export const CUSTOM_COMMAND_DIR = '.rman';
6
+ /** Loadable module forms, matching what a `.rmanrc.cjs`/`.mjs`/`.js` config already accepts. A
7
+ * `.ts` command would need a loader registered in rman's own process, which is a separate
8
+ * question from this one. */
9
+ const EXTENSIONS = ['.js', '.mjs', '.cjs'];
10
+ /**
11
+ * Identity helper for authoring a `.rman/<name>.mjs` command with full type-checking and
12
+ * autocomplete - the same `defineConfig` pattern, for the same reason. Returns `command`
13
+ * unchanged.
14
+ *
15
+ * ```js
16
+ * // .rman/deploy.mjs
17
+ * import { defineCommand, VersionService } from 'rman';
18
+ *
19
+ * export default defineCommand({
20
+ * describe: 'Ships what was just published to the staging cluster',
21
+ * builder: y => y.option('stage', { choices: ['dev', 'prod'], demandOption: true }),
22
+ * async handler({ repository, runBin, logger }, args) {
23
+ * const plan = await VersionService.getPlan(repository);
24
+ * for (const entry of plan.filter(e => e.status === 'bump')) {
25
+ * logger.info(`${entry.package.name} -> ${args.stage}`);
26
+ * }
27
+ * await runBin('helm', ['upgrade', '--install', args.stage, './chart']);
28
+ * },
29
+ * });
30
+ * ```
31
+ *
32
+ * Take `runBin` from the context rather than importing it: the one on the context already carries
33
+ * this run's `cwd` (the repository root) and log level.
34
+ *
35
+ * This is for **one repository-level operation with logic of its own** - branching, its own CLI
36
+ * options, rman's services. Running a shell step across every package is what `.rmanrc
37
+ * "run.<script>"` already does, with the scheduling, topological order, `bail` and progress panel
38
+ * that come with it; reimplementing that loop here would only lose them.
39
+ */
40
+ export function defineCommand(command) {
41
+ return command;
42
+ }
43
+ /**
44
+ * Loads every command module in `<root>/.rman`. A repository without that directory pays nothing -
45
+ * no scan, no imports - which matters because this runs on *every* rman invocation, `info`
46
+ * included.
47
+ *
48
+ * Each failure is collected rather than thrown, so the rest of the CLI keeps working; the caller
49
+ * warns about them. What is *not* tolerated is a module that would shadow a built-in - see
50
+ * `assertNoBuiltinShadowing`.
51
+ */
52
+ export async function loadCustomCommands(rootDir) {
53
+ const dir = path.join(rootDir, CUSTOM_COMMAND_DIR);
54
+ const commands = [];
55
+ const errors = [];
56
+ if (!fs.existsSync(dir))
57
+ return { commands, errors };
58
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
59
+ if (!entry.isFile() || !EXTENSIONS.includes(path.extname(entry.name)))
60
+ continue;
61
+ const file = path.join(dir, entry.name);
62
+ try {
63
+ const mod = await import(pathToFileURL(file).href);
64
+ const command = mod?.default ?? mod?.command;
65
+ if (!command || typeof command !== 'object') {
66
+ throw new Error('no default export - end the module with `export default defineCommand({ ... })`');
67
+ }
68
+ if (typeof command.handler !== 'function')
69
+ throw new Error('"handler" is missing, or is not a function');
70
+ if (typeof command.describe !== 'string' || !command.describe) {
71
+ throw new Error('"describe" is missing - `rman --help` has nothing to list the command by without it');
72
+ }
73
+ const declared = command.command?.trim();
74
+ commands.push({
75
+ ...command,
76
+ command: declared || path.basename(entry.name, path.extname(entry.name)),
77
+ name: (declared || path.basename(entry.name, path.extname(entry.name))).split(/\s+/)[0],
78
+ file,
79
+ });
80
+ }
81
+ catch (e) {
82
+ errors.push({ file, reason: e?.message ?? String(e) });
83
+ }
84
+ }
85
+ return { commands, errors };
86
+ }
87
+ /**
88
+ * Refuses a command that would take a built-in's name. Unlike a module that simply fails to load,
89
+ * this one is thrown: the file is fine, the *name* is the mistake, and there is no reading of
90
+ * `rman publish` that is safe to guess at. Silently preferring either one would leave whoever typed
91
+ * it unable to tell which ran.
92
+ */
93
+ export function assertNoBuiltinShadowing(commands, builtins) {
94
+ const clash = commands.find(c => builtins.includes(c.name));
95
+ if (!clash)
96
+ return;
97
+ throw new Error(`"${path.relative(process.cwd(), clash.file)}" would shadow rman's built-in "${clash.name}" command.\n` +
98
+ ` Rename the file, or give it its own name with \`command: '<name>'\`.`);
99
+ }
@@ -0,0 +1,27 @@
1
+ import type { RmanConfig } from '../interfaces/rman-config.interface.js';
2
+ /** The key naming configs to inherit from, the way eslint and tsconfig spell it. */
3
+ export declare const EXTENDS_KEY = "extends";
4
+ /**
5
+ * Resolves `config`'s own `extends` into it: every named config is loaded, merged in declaration
6
+ * order, and `config`'s own keys land on top. Returns a new object with no `extends` left in it.
7
+ *
8
+ * The point is a shared package - `extends: "@panates/rman-monorepo"` - so a repository declares
9
+ * its house rules once instead of restating them. `+key` is what makes that liveable (see
10
+ * `mergeConfig`): without it, adding one step to a base's list means copying the list.
11
+ *
12
+ * `from` is the file the `extends` was written in, and everything resolves relative to **it**: a
13
+ * bare specifier through that file's own `node_modules`, a relative path against its directory.
14
+ * Resolving from rman's own location instead would look in rman's dependencies, where a
15
+ * repository's shared config has no reason to be.
16
+ *
17
+ * `seen` carries the chain being resolved, so a config that extends its way back to itself is
18
+ * reported rather than recursed into forever.
19
+ */
20
+ export declare function resolveExtends(config: RmanConfig, from: string, seen?: string[]): Promise<RmanConfig>;
21
+ /**
22
+ * Refuses `extends` inside a `"[selector]"` block. A selector block is typed as a whole
23
+ * `RmanConfig`, so writing one there looks valid and would simply never be resolved - and a config
24
+ * that quietly does nothing is worse than one that won't load. Inheritance is a statement about
25
+ * the file, not about the packages it happens to name.
26
+ */
27
+ export declare function assertNoSelectorExtends(config: RmanConfig, file: string): void;
@@ -0,0 +1,89 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import * as yaml from 'js-yaml';
5
+ import { isSelectorKey } from './config.js';
6
+ import { mergeConfig } from './merge-config.js';
7
+ import { resolveConfigTarget } from './resolve-target.js';
8
+ /** The key naming configs to inherit from, the way eslint and tsconfig spell it. */
9
+ export const EXTENDS_KEY = 'extends';
10
+ /**
11
+ * Resolves `config`'s own `extends` into it: every named config is loaded, merged in declaration
12
+ * order, and `config`'s own keys land on top. Returns a new object with no `extends` left in it.
13
+ *
14
+ * The point is a shared package - `extends: "@panates/rman-monorepo"` - so a repository declares
15
+ * its house rules once instead of restating them. `+key` is what makes that liveable (see
16
+ * `mergeConfig`): without it, adding one step to a base's list means copying the list.
17
+ *
18
+ * `from` is the file the `extends` was written in, and everything resolves relative to **it**: a
19
+ * bare specifier through that file's own `node_modules`, a relative path against its directory.
20
+ * Resolving from rman's own location instead would look in rman's dependencies, where a
21
+ * repository's shared config has no reason to be.
22
+ *
23
+ * `seen` carries the chain being resolved, so a config that extends its way back to itself is
24
+ * reported rather than recursed into forever.
25
+ */
26
+ export async function resolveExtends(config, from, seen = []) {
27
+ const declared = config[EXTENDS_KEY];
28
+ if (declared === undefined)
29
+ return config;
30
+ const targets = Array.isArray(declared) ? declared : [declared];
31
+ for (const target of targets) {
32
+ if (typeof target !== 'string' || !target.trim()) {
33
+ throw new Error(`"extends" in "${from}" must be a config name or path, or an array of them`);
34
+ }
35
+ }
36
+ const base = {};
37
+ for (const target of targets) {
38
+ const file = resolveConfigTarget(target, from, EXTENDS_KEY);
39
+ if (seen.includes(file)) {
40
+ throw new Error(`"extends" forms a cycle: ${[...seen, file].map(f => path.basename(f)).join(' -> ')}`);
41
+ }
42
+ const loaded = await loadConfigFile(file);
43
+ assertNoSelectorExtends(loaded, file);
44
+ // Recursive: a shared config may itself be built on another.
45
+ mergeConfig(base, await resolveExtends(loaded, file, [...seen, file]));
46
+ }
47
+ const own = { ...config };
48
+ delete own[EXTENDS_KEY];
49
+ return mergeConfig(base, own);
50
+ }
51
+ /**
52
+ * Refuses `extends` inside a `"[selector]"` block. A selector block is typed as a whole
53
+ * `RmanConfig`, so writing one there looks valid and would simply never be resolved - and a config
54
+ * that quietly does nothing is worse than one that won't load. Inheritance is a statement about
55
+ * the file, not about the packages it happens to name.
56
+ */
57
+ export function assertNoSelectorExtends(config, file) {
58
+ for (const [key, value] of Object.entries(config)) {
59
+ if (!isSelectorKey(key) || !value || typeof value !== 'object')
60
+ continue;
61
+ if (EXTENDS_KEY in value) {
62
+ throw new Error(`"${key}" in "${file}" cannot use "extends" - it belongs at the top level, where it is a ` +
63
+ `statement about this config rather than about the packages the selector names.`);
64
+ }
65
+ }
66
+ }
67
+ /** Loads one resolved target. YAML and JSON are read directly; anything else goes through the
68
+ * module loader, so a shared config can be a `defineConfig` module with real logic in it. */
69
+ async function loadConfigFile(file) {
70
+ const ext = path.extname(file);
71
+ if (ext === '.yml' || ext === '.yaml') {
72
+ const obj = yaml.load(fs.readFileSync(file, 'utf-8'));
73
+ return asConfig(obj, file);
74
+ }
75
+ if (ext === '.json')
76
+ return asConfig(JSON.parse(fs.readFileSync(file, 'utf-8')), file);
77
+ const mod = await import(pathToFileURL(file).href);
78
+ return asConfig(mod?.default ?? mod, file);
79
+ }
80
+ function asConfig(value, file) {
81
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
82
+ throw new Error(`"${file}" does not export an rman config object`);
83
+ }
84
+ const config = { ...value };
85
+ // Editor tooling only - meaningless once merged, and `additionalProperties` would reject it
86
+ // wherever it ended up.
87
+ delete config.$schema;
88
+ return config;
89
+ }
@@ -0,0 +1,222 @@
1
+ import type { Package } from './package.js';
2
+ import { type VersionScheme } from './version-scheme.js';
3
+ /**
4
+ * A package's identity, however its ecosystem happens to record it.
5
+ *
6
+ * Three fields rman genuinely needs of every package, plus the raw document for the commands that
7
+ * know what ecosystem they are in. Everything rman's core does - grouping, tagging, changelogs,
8
+ * release identity - is expressed in terms of these three and nothing else.
9
+ */
10
+ export interface Manifest {
11
+ name: string;
12
+ version: string;
13
+ /** Excluded from publishing by the package's own declaration (`package.json#private`). Not the
14
+ * same as `.rmanrc "publish.skip"`, which is the *repository's* declaration about it. */
15
+ private?: boolean;
16
+ /** The document as the ecosystem wrote it. `rman-node`'s own commands read `package.json` fields
17
+ * the core has no opinion about (`scripts`, `publishConfig`, `engines`) off this. */
18
+ raw: any;
19
+ }
20
+ /**
21
+ * Where a package's identity is written, and how to change it.
22
+ *
23
+ * **The core has no provider.** "The name and version live in a `package.json`" is true of npm and
24
+ * of nothing else - a `Cargo.toml`, a `pyproject.toml` and a `go.mod` each say the same thing
25
+ * differently, and the version is not even in the same *kind* of place in all of them.
26
+ * `rman-node` contributes the `package.json` one.
27
+ *
28
+ * Paired with `VersionScheme` on purpose: the ecosystem that decides *where* a version is written
29
+ * is the one that decides *how* it is numbered, so a provider supplies both and a package gets a
30
+ * matching pair rather than a `pyproject.toml` read with semver's arithmetic.
31
+ */
32
+ export interface ManifestProvider {
33
+ /**
34
+ * **The ecosystem this provider speaks for**, surfaced on every package it reads as
35
+ * `Package.provider` - `'node'` for `rman-node`. Short and about the technology, not about the
36
+ * file: `fileName` already says `package.json`, and a name repeating it would tell a caller
37
+ * nothing it did not have.
38
+ *
39
+ * This is what lets code that *does* know one ecosystem check before acting on a package -
40
+ * `if (pkg.provider === 'node')` - which matters most in a repository holding more than one,
41
+ * since `read` is asked per directory and two packages can legitimately answer to different
42
+ * providers.
43
+ */
44
+ readonly name: string;
45
+ /** The file this looks for, relative to a package directory - `'package.json'`. Used to report
46
+ * what was missing, and by `import` when grafting an external repository in. */
47
+ readonly fileName: string;
48
+ /** `undefined` when this directory holds no package of this kind, so another provider gets a
49
+ * turn rather than this one having to throw. */
50
+ read(dir: string): Manifest | undefined;
51
+ /** Writes `manifest` back. Only ever called with a manifest this provider produced. */
52
+ write(dir: string, manifest: Manifest): void;
53
+ /** How this ecosystem numbers versions. Defaults to semver when a provider has no opinion -
54
+ * which is right for Cargo and Go, and wrong for PEP 440, whose provider should say so. */
55
+ readonly versionScheme?: VersionScheme;
56
+ /**
57
+ * The version of `pkg` this ecosystem's registry currently reports, or `undefined` for anything
58
+ * that is not an answer (never published, no network, private with no access).
59
+ *
60
+ * **Read for exactly one purpose, and it is not "has this been published"**: `detectChangeHash`
61
+ * borrows the version string to *guess a tag name*, and uses it only if a tag by that name
62
+ * actually exists in git. The case it covers is rman being adopted onto a repository whose
63
+ * releases predate it - a tag exists but is not in HEAD's ancestry (cut on another branch,
64
+ * rewritten history, a shallow clone), so `git describe` cannot see it. Never compare this
65
+ * against the local manifest version; that is `publish`'s question, and mixing the two is the
66
+ * measured bug the A/B split exists to prevent.
67
+ *
68
+ * Here rather than behind a repository-wide hook because a registry belongs to an *ecosystem*: a
69
+ * polyglot repository asks npm about its `node` packages and crates.io about its `cargo` ones, and
70
+ * only a per-package provider can do that. Optional - an ecosystem with no registry to ask (or a
71
+ * repository that would rather not reach the network) simply leaves git tags as the only source,
72
+ * which is the honest answer rather than a diminished one.
73
+ */
74
+ publishedVersion?(pkg: Package): Promise<string | undefined>;
75
+ /**
76
+ * Rewrites `content` so the version it hard-codes reads `version`, for a file `.rmanrc
77
+ * "version.stamp"` lists - returning `undefined` when there is nothing to change, so the caller
78
+ * writes nothing and can report a file that matched nothing.
79
+ *
80
+ * **Here because how a version is *declared* is the language's**, and this provider is already
81
+ * the ecosystem's representative for exactly that: `write` says where the manifest keeps it,
82
+ * `versionScheme` says how it is numbered, and this says what it looks like in source. Measured
83
+ * before it moved: the core's one pattern stamped a Go `const version = "…"` and a Gradle
84
+ * `version = "…"` but silently missed `const Version`, `__version__`, Rust's
85
+ * `pub const VERSION: &str = "…"` and `pom.xml`'s `<version>` - and "silently" is the part that
86
+ * mattered, since a listed file that matches nothing looked exactly like a file with no version
87
+ * in it.
88
+ *
89
+ * `file` is the absolute path, for a provider that keys off the extension (a `.ts` constant and a
90
+ * `Chart.yaml` are both npm-adjacent and are not the same rewrite). `options.constant` is the
91
+ * identifier the repository says holds it, from the config - a provider whose format has no
92
+ * identifier ignores it.
93
+ *
94
+ * `stampVersionConstant` is exported for the common case; delegating to it is one line.
95
+ */
96
+ stampVersion?(file: string, content: string, version: string, options?: {
97
+ constant?: string;
98
+ }): string | undefined;
99
+ /**
100
+ * Which of `candidates` this package declares a dependency on.
101
+ *
102
+ * The *field names* are the ecosystem's: npm spreads four of them
103
+ * (`dependencies`/`devDependencies`/`peerDependencies`/`optionalDependencies`), Cargo has
104
+ * `[dev-dependencies]` and `[build-dependencies]`, `go.mod` has one `require` block. rman only
105
+ * wants the edges of the graph, so the provider reads its own manifest and returns the packages.
106
+ *
107
+ * Returning **packages rather than names** for the same reason `Package.dependencies` holds
108
+ * them: a name identifies a package only where the ecosystem guarantees uniqueness, and the
109
+ * provider is the only thing that knows how its own ecosystem refers to a dependency.
110
+ *
111
+ * Omit it and a package has no declared dependencies beyond `.rmanrc "dependencies"`.
112
+ */
113
+ dependencies?(manifest: Manifest, candidates: readonly Package[]): Package[];
114
+ /**
115
+ * Splits a package name into the parts a config expression can ask for - `${{ pkg.scope }}` and
116
+ * `${{ pkg.unscopedName }}`.
117
+ *
118
+ * npm's `@scope/name` is a convention, not a universal: a Go module path (`github.com/x/y`)
119
+ * split on `/` would report a scope of `github.com/x`, and a Cargo crate has no such notion at
120
+ * all. Omit it and a name has no scope and is its own unscoped form, which is the honest answer
121
+ * for an ecosystem without the concept.
122
+ */
123
+ splitName?(name: string): {
124
+ scope?: string;
125
+ unscopedName: string;
126
+ };
127
+ /**
128
+ * Rewrites this manifest's references to in-repo packages that just got a new version.
129
+ *
130
+ * `bumped` maps a package to the version it is being given. What a "reference" looks like is
131
+ * entirely the ecosystem's: npm has four dependency fields and a `"workspace:"` protocol whose
132
+ * bare selectors resolve at publish time and must *not* be rewritten; Cargo has `path`
133
+ * dependencies that carry no version at all. rman only knows that a bump may leave siblings
134
+ * pointing at the old number.
135
+ *
136
+ * Mutates `manifest.raw` in place - the caller writes it out afterwards, in the same pass that
137
+ * wrote the new version, so there is one file write rather than two.
138
+ *
139
+ * Omit it and nothing is rewritten, which is correct for an ecosystem that references siblings
140
+ * by path.
141
+ */
142
+ updateDependencyVersions?(manifest: Manifest, bumped: ReadonlyMap<Package, string>): void;
143
+ }
144
+ /**
145
+ * The registry, merged onto the `Manifest` interface so one name carries both the shape and the
146
+ * operations - `Manifest.read(dir)` returns a `Manifest`.
147
+ *
148
+ * A namespace rather than loose `addManifestProvider`/`readManifest` functions because a namespace
149
+ * is what a plugin can *augment*: `declare module 'rman' { namespace Manifest { ... } }` is how
150
+ * `rman-node` already adds to `SystemInfo`, and anything this seam grows later can arrive the
151
+ * same way instead of as another top-level export.
152
+ */
153
+ export declare namespace Manifest {
154
+ /** Registers a provider. Called by `loadPlugins` for each plugin's `manifest`, in `plugins`
155
+ * declaration order - so which one answers is a function of the repository's own config. */
156
+ function addProvider(provider: ManifestProvider): void;
157
+ /** For tests, which would otherwise leak a provider into every later case in the process. */
158
+ function clearProviders(): void;
159
+ /**
160
+ * Reads `dir`'s manifest through the first provider that recognizes it, with the scheme that
161
+ * provider brings.
162
+ *
163
+ * **With no provider registered, or none recognizing the directory**, the fallback is a package
164
+ * named after its own directory at version `0.0.0`. That is deliberately the least it can claim:
165
+ * the directory name is a fact, and `0.0.0` is the version a thing has when nothing says
166
+ * otherwise. The alternative - refusing to construct a package at all - would make `rman info` and
167
+ * `rman list` fail in a repository whose `.rmanrc` simply names no plugin yet, which is exactly
168
+ * when someone needs to run them.
169
+ */
170
+ function read(dir: string): {
171
+ manifest: Manifest;
172
+ versionScheme: VersionScheme;
173
+ fileName: string;
174
+ provider: string;
175
+ };
176
+ /** Writes through whichever provider recognizes `dir`. Throws when none does: a write that lands
177
+ * nowhere is worse than one that fails, since the caller has already decided the new version. */
178
+ function write(dir: string, manifest: Manifest): void;
179
+ /**
180
+ * The packages `pkg` declares a dependency on, via whichever provider recognizes it.
181
+ *
182
+ * No provider, or one with no opinion, means no declared dependencies - `.rmanrc "dependencies"`
183
+ * is then the only source, which is exactly right: a repository rman cannot read the manifests
184
+ * of can still describe its own graph by hand.
185
+ */
186
+ function dependenciesOf(pkg: Package, candidates: readonly Package[]): Package[];
187
+ /** `${{ pkg.scope }}`/`${{ pkg.unscopedName }}`, by whichever provider recognizes `dir` - and
188
+ * "no scope, the name is its own unscoped form" when none has an opinion. */
189
+ function splitName(dir: string, name: string): {
190
+ scope?: string;
191
+ unscopedName: string;
192
+ };
193
+ /**
194
+ * Refreshes `pkg`'s references to the packages in `bumped`, via whichever provider recognizes it.
195
+ *
196
+ * No provider, or one without an opinion, means nothing to rewrite - a repository whose packages
197
+ * reference each other by path has nothing here to go stale.
198
+ */
199
+ function updateDependencyVersions(pkg: Package, bumped: ReadonlyMap<Package, string>): void;
200
+ /**
201
+ * Rewrites a hard-coded version in `content` through `pkg`'s own ecosystem - see
202
+ * `ManifestProvider.stampVersion`.
203
+ *
204
+ * `undefined` covers three cases the caller has to tell apart from each other, and cannot: no
205
+ * provider claimed the package, the provider has no opinion about stamping, or it looked and found
206
+ * nothing to change. All three mean "this file was not stamped", which is what `version` reports.
207
+ */
208
+ function stampVersion(pkg: Package, file: string, content: string, version: string, options?: {
209
+ constant?: string;
210
+ }): string | undefined;
211
+ /**
212
+ * What `pkg`'s own ecosystem's registry says its current version is - see
213
+ * `ManifestProvider.publishedVersion` for the one thing this is for and the one thing it must
214
+ * never be used for.
215
+ *
216
+ * `undefined` when the provider has no opinion, which includes every repository that names no
217
+ * plugin: git tags then answer the boundary question alone.
218
+ */
219
+ function publishedVersion(pkg: Package): Promise<string | undefined>;
220
+ /** The file names providers look for, for an error message that can say what was expected. */
221
+ function fileNames(): string[];
222
+ }