rman 2.0.0-beta.4 → 2.0.0-beta.5
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.
- package/README.md +20 -7
- package/builtins/platforms/node/node-config.interface.d.ts +13 -0
- package/builtins/platforms/node/services/publish.service.d.ts +26 -0
- package/builtins/platforms/node/services/publish.service.js +36 -7
- package/builtins/publish-targets/npm/npm-publish-target.d.ts +5 -0
- package/builtins/publish-targets/npm/npm-publish-target.js +12 -0
- package/cli.js +16 -11
- package/constants.js +1 -1
- package/core/interfaces/custom-command.d.ts +8 -1
- package/core/interfaces/custom-command.js +22 -3
- package/core/plugin-loader.d.ts +1 -1
- package/core/plugin-loader.js +9 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,17 +94,30 @@ worked examples of every single command, see **[docs/cli-rman.md](https://github
|
|
|
94
94
|
| [`import <path>`](#rman-import-path) | Imports an external git repository as a new package, with history. |
|
|
95
95
|
|
|
96
96
|
**`ci` and `clean` come from the `node` built-in**, which ships inside this package - each is about
|
|
97
|
-
npm or TypeScript rather than about repositories
|
|
98
|
-
repository
|
|
97
|
+
npm or TypeScript rather than about repositories. It needs no declaration: rman lays its own `node`
|
|
98
|
+
preset under every repository, so a clone with no `.rmanrc` at all already has them, plus the `node`
|
|
99
|
+
technology itself and the npm publish target. It used to take a second package (`rman-node`) and a
|
|
100
|
+
`.rmanrc` before anything worked.
|
|
101
|
+
|
|
102
|
+
A preset is an ordinary rman config rather than a type of its own, so another technology arrives the
|
|
103
|
+
same way and a polyglot repository asks for both:
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
extends: ['rman:node', 'rman:cargo']
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**`plugins: ['node']` is not a way to name a technology and is refused**, with a message saying so.
|
|
110
|
+
That key takes a plugin instance or a glob naming modules that export one; a name is what `extends`
|
|
111
|
+
resolves. The keys that do exist:
|
|
99
112
|
|
|
100
113
|
```yaml
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
platform: node # which technology claims *this directory* - it does not load anything
|
|
115
|
+
platforms: [...] # contributes technologies: an instance, or a glob naming modules exporting one
|
|
103
116
|
```
|
|
104
117
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
118
|
+
Whatever a repository declares is asked about a directory **before** the default preset, so
|
|
119
|
+
`extends: 'rman:cargo'` in a root holding both a `Cargo.toml` and a tooling `package.json` resolves
|
|
120
|
+
to what it declared.
|
|
108
121
|
|
|
109
122
|
**`publish` is here, but *where* a package ships is a plugin's to say.** A **publish target** is
|
|
110
123
|
one answer to "is this version on the registry, and how do I push it" - rman ships `docker`
|
|
@@ -48,6 +48,19 @@ export declare namespace RmanNodeConfig {
|
|
|
48
48
|
* Publishing from such a directory means the manifest there is **generated by `publish`**,
|
|
49
49
|
* from the package's own - see `PublishService`. There is nothing to configure about it. */
|
|
50
50
|
directory?: ConfigValue<string>;
|
|
51
|
+
/**
|
|
52
|
+
* Publish into npm's **staging queue** (`npm stage publish`) instead of straight to the
|
|
53
|
+
* registry: the version is held pending until a maintainer approves it with `npm stage approve`,
|
|
54
|
+
* which is the step that carries the 2FA challenge.
|
|
55
|
+
*
|
|
56
|
+
* Per package like every other key here, so a repository can stage the one package that matters
|
|
57
|
+
* and publish the rest directly.
|
|
58
|
+
*
|
|
59
|
+
* Requires npm >= 11.15.0 and Node >= 22.14.0 on whatever runs the publish. rman does not check
|
|
60
|
+
* either: the version that matters is the runner's, not the one resolving the config, and npm's
|
|
61
|
+
* own "Unknown command: stage" is a better message than a guess made somewhere else.
|
|
62
|
+
*/
|
|
63
|
+
staged?: ConfigValue<boolean>;
|
|
51
64
|
}
|
|
52
65
|
}
|
|
53
66
|
/**
|
|
@@ -32,6 +32,17 @@ export declare namespace PublishService {
|
|
|
32
32
|
* a release pipeline gates on both show where a version is going.
|
|
33
33
|
*/
|
|
34
34
|
tag?: string;
|
|
35
|
+
/**
|
|
36
|
+
* `npm stage publish` instead of `npm publish` - the version goes into npm's staging queue and
|
|
37
|
+
* is not on the registry until a maintainer runs `npm stage approve`, which is where the 2FA
|
|
38
|
+
* challenge moves to. `false` forces a direct publish even where the config asks for staging;
|
|
39
|
+
* `undefined` leaves the decision to each package's `.rmanrc "publish.npm.staged"`.
|
|
40
|
+
*
|
|
41
|
+
* **A plan option, for the same reason `tag` is one**: what a run is about to do is the thing a
|
|
42
|
+
* reader confirms and a pipeline gates on, and "this release will not be live when the workflow
|
|
43
|
+
* goes green" is the most important sentence in the plan when it applies.
|
|
44
|
+
*/
|
|
45
|
+
staged?: boolean;
|
|
35
46
|
}
|
|
36
47
|
interface ApplyOptions extends Options {
|
|
37
48
|
packageManager?: CiService.PackageManager;
|
|
@@ -63,6 +74,21 @@ export declare namespace PublishService {
|
|
|
63
74
|
* says; recomputing at publish time would let the two disagree about where a package is going.
|
|
64
75
|
*/
|
|
65
76
|
distTag?: string;
|
|
77
|
+
/**
|
|
78
|
+
* Whether this entry goes into npm's staging queue rather than straight to the registry -
|
|
79
|
+
* `--staged`, or the package's own `.rmanrc "publish.npm.staged"`.
|
|
80
|
+
*
|
|
81
|
+
* Decided in the plan and read back by `applyPlan`, exactly as `distTag` is: a plan that said
|
|
82
|
+
* "staged" while the command published directly would be the one disagreement that cannot be
|
|
83
|
+
* undone, since a live version cannot be unpublished after 72 hours.
|
|
84
|
+
*
|
|
85
|
+
* **A staged entry still reads `publish`, not a status of its own**, and that is a limitation
|
|
86
|
+
* rather than a decision: rman's question is "is this version on the registry", and a pending
|
|
87
|
+
* version is not - `npm view` does not report the queue. So a second run proposes the same
|
|
88
|
+
* package again, and whether npm accepts a duplicate stage is npm's answer to give, not rman's
|
|
89
|
+
* to guess. Watch this if a run is ever repeated before an approval.
|
|
90
|
+
*/
|
|
91
|
+
staged?: boolean;
|
|
66
92
|
/** `PublishTarget.Entry`'s own field - what the core's `publish` prints beside the package
|
|
67
93
|
* name. Carries the dist-tag, so where a version is going is visible in the plan and in
|
|
68
94
|
* `--dry-run --json` without the core knowing anything about npm. */
|
|
@@ -207,14 +207,20 @@ export var PublishService;
|
|
|
207
207
|
* out. Asking it kept proposing an already-published version until npm answered 403. */
|
|
208
208
|
const published = !!view?.versions.includes(pkg.version);
|
|
209
209
|
const distTag = distTagFor(pkg, options.tag).tag;
|
|
210
|
+
const staged = resolveStaged(pkg, options.staged);
|
|
210
211
|
entries.set(pkg.name, {
|
|
211
212
|
package: pkg,
|
|
212
213
|
version: pkg.version,
|
|
213
214
|
registryVersion,
|
|
214
215
|
distTag,
|
|
216
|
+
staged,
|
|
215
217
|
/** Printed beside the package in the plan, so where a version is going is something the
|
|
216
|
-
* reader confirms rather than something they have to infer from the version string.
|
|
217
|
-
|
|
218
|
+
* reader confirms rather than something they have to infer from the version string.
|
|
219
|
+
* Staging is named first because it changes what the run *does* - a reader who reads
|
|
220
|
+
* only the dist-tag would take a staged entry for one that goes live. */
|
|
221
|
+
detail: [staged ? 'staged for approval' : undefined, distTag ? `-> dist-tag "${distTag}"` : undefined]
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.join(', ') || undefined,
|
|
218
224
|
status: published ? 'up-to-date' : 'publish',
|
|
219
225
|
reason: published
|
|
220
226
|
? `registry already has ${pkg.version}`
|
|
@@ -262,10 +268,14 @@ export var PublishService;
|
|
|
262
268
|
const publishDir = resolvePublishDir(pkg, options.contents);
|
|
263
269
|
const restore = preparePublishManifest(pkg, publishDir, packagesByName);
|
|
264
270
|
try {
|
|
265
|
-
/** The plan's own tag, not
|
|
266
|
-
* the reader confirmed that, and `applyPlan` is here to carry it out.
|
|
267
|
-
* the fallback only for a plan built by something other than `getPlan`. */
|
|
268
|
-
await exec(buildPublishCommand(packageManager, {
|
|
271
|
+
/** The plan's own tag and staging decision, not recomputed ones: `getPlan` decided where
|
|
272
|
+
* this package goes, the reader confirmed that, and `applyPlan` is here to carry it out.
|
|
273
|
+
* `options.*` is the fallback only for a plan built by something other than `getPlan`. */
|
|
274
|
+
await exec(buildPublishCommand(packageManager, {
|
|
275
|
+
...options,
|
|
276
|
+
tag: entry.distTag ?? options.tag,
|
|
277
|
+
staged: entry.staged ?? options.staged,
|
|
278
|
+
}), {
|
|
269
279
|
cwd: publishDir,
|
|
270
280
|
app: pkg.repository.app,
|
|
271
281
|
stdio: 'inherit',
|
|
@@ -361,8 +371,17 @@ function distTagFor(pkg, tag) {
|
|
|
361
371
|
'no --tag npm would put it on "latest" - pass --tag <name>',
|
|
362
372
|
};
|
|
363
373
|
}
|
|
374
|
+
/* **`stage publish`, not a `--staged` flag**, because that is npm's own spelling: `npm stage` is a
|
|
375
|
+
* command with `publish`/`list`/`view`/`download`/`approve`/`reject` under it, and rman only ever
|
|
376
|
+
* drives the first. The rest is a maintainer's, at a terminal with a 2FA prompt - which is the whole
|
|
377
|
+
* point of staging, and the reason rman must not grow an `approve`.
|
|
378
|
+
*
|
|
379
|
+
* The publish flags are passed through unchanged. `--otp` is the one that reads oddly beside it,
|
|
380
|
+
* since staging is what *defers* the 2FA challenge to approval time - it is passed anyway rather
|
|
381
|
+
* than refused, because whether npm accepts it there is npm's to answer and a guess here would be a
|
|
382
|
+
* rule rman invented. */
|
|
364
383
|
function buildPublishCommand(packageManager, options) {
|
|
365
|
-
const args = ['publish'];
|
|
384
|
+
const args = options.staged ? ['stage', 'publish'] : ['publish'];
|
|
366
385
|
if (options.access)
|
|
367
386
|
args.push('--access', options.access);
|
|
368
387
|
if (options.tag)
|
|
@@ -375,3 +394,13 @@ function buildPublishCommand(packageManager, options) {
|
|
|
375
394
|
args.push('--userconfig', options.userconfig);
|
|
376
395
|
return `${packageManager} ${args.join(' ')}`;
|
|
377
396
|
}
|
|
397
|
+
/** Whether this package stages: `--staged`/`--no-staged` when given, else its own
|
|
398
|
+
* `.rmanrc "publish.npm.staged"`. */
|
|
399
|
+
/* Per package rather than per run, like `directory` beside it - a repository may well want its one
|
|
400
|
+
* widely-depended-on package held for approval and the rest published directly, and the cascade
|
|
401
|
+
* already makes "all of them" a single `"[*]"` line. */
|
|
402
|
+
function resolveStaged(pkg, override) {
|
|
403
|
+
if (override !== undefined)
|
|
404
|
+
return override;
|
|
405
|
+
return !!pkg.config?.publish?.npm?.staged;
|
|
406
|
+
}
|
|
@@ -50,6 +50,13 @@ export class NpmPublishTarget {
|
|
|
50
50
|
describe: 'npm publish --otp <otp> - a 2FA one-time password, for registries that require it',
|
|
51
51
|
type: 'string',
|
|
52
52
|
},
|
|
53
|
+
staged: {
|
|
54
|
+
target: 'cli',
|
|
55
|
+
describe: "npm stage publish - hold each version in npm's staging queue instead of publishing it, " +
|
|
56
|
+
'until a maintainer runs "npm stage approve" with 2FA. --no-staged forces a direct publish ' +
|
|
57
|
+
'over .rmanrc "publish.npm.staged"',
|
|
58
|
+
type: 'boolean',
|
|
59
|
+
},
|
|
53
60
|
registry: {
|
|
54
61
|
target: 'cli',
|
|
55
62
|
describe: 'Registry to check against and publish to (default: whatever .npmrc already configures)',
|
|
@@ -90,6 +97,11 @@ export class NpmPublishTarget {
|
|
|
90
97
|
* decides the dist-tag and refuses the two cases with nothing to derive, so `--dry-run` and
|
|
91
98
|
* the JSON a pipeline gates on have to see the flag. */
|
|
92
99
|
tag: ctx.args.tag,
|
|
100
|
+
/** Plan-time for the same reason, and a stronger one: whether a run leaves the version live
|
|
101
|
+
* or waiting for an approval is the thing a reader is confirming. Left `undefined` when the
|
|
102
|
+
* flag was not given, so each package's own `publish.npm.staged` still decides - `false`
|
|
103
|
+
* here would mean `--no-staged` and silently overrule the config. */
|
|
104
|
+
staged: ctx.args.staged,
|
|
93
105
|
};
|
|
94
106
|
}
|
|
95
107
|
}
|
package/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import { version } from './constants.js';
|
|
|
13
13
|
import { RmanApplication } from './core/application.js';
|
|
14
14
|
import { Repository } from './core/classes/repository.js';
|
|
15
15
|
import { commandName, toYargsCommand } from './core/command-builder.js';
|
|
16
|
-
import { assertNoBuiltinShadowing, defaultCommandGlobs, loadCustomCommands, } from './core/interfaces/custom-command.js';
|
|
16
|
+
import { assertNoBuiltinShadowing, defaultCommandGlobs, describeCommandSource, loadCustomCommands, } from './core/interfaces/custom-command.js';
|
|
17
17
|
import { checkCustomCommand } from './core/plugin-loader.js';
|
|
18
18
|
import { commandRegistry } from './interfaces/rman-config.interface.js';
|
|
19
19
|
import { colorYaml } from './utils/color-yaml.js';
|
|
@@ -141,7 +141,7 @@ export async function runCli(options) {
|
|
|
141
141
|
if (!c.register)
|
|
142
142
|
return { name: c.name, file: c.file, module: toCustomModule(c.custom, repository, app) };
|
|
143
143
|
const declared = c.register(app);
|
|
144
|
-
const meta = checkCustomCommand({ ...declared, command: declared.command?.trim() || c.name }, c.file);
|
|
144
|
+
const meta = checkCustomCommand({ ...declared, command: declared.command?.trim() || c.name }, describeCommandSource(c.file));
|
|
145
145
|
return { name: commandName(meta.command), file: c.file, module: toYargsCommand(meta) };
|
|
146
146
|
});
|
|
147
147
|
/**
|
|
@@ -179,7 +179,8 @@ export async function runCli(options) {
|
|
|
179
179
|
const survivors = new Set(commands);
|
|
180
180
|
for (const lost of localModules.filter(c => !survivors.has(c))) {
|
|
181
181
|
const winner = byName.get(lost.name);
|
|
182
|
-
logger.verbose(`"${lost.name}" from ${lost.file} is overridden by
|
|
182
|
+
logger.verbose(`"${lost.name}" from ${describeCommandSource(lost.file)} is overridden by ` +
|
|
183
|
+
`${describeCommandSource(winner.file)}.`);
|
|
183
184
|
}
|
|
184
185
|
}
|
|
185
186
|
assertNoBuiltinShadowing(commands, builtInNames(builtIns));
|
|
@@ -265,17 +266,21 @@ function commandEntries(repository) {
|
|
|
265
266
|
if (!entry || seen.has(entry))
|
|
266
267
|
continue;
|
|
267
268
|
seen.add(entry);
|
|
268
|
-
/** No file
|
|
269
|
-
*
|
|
269
|
+
/** **No file, and `file: ''` says so** - `describeCommandSource` turns that into
|
|
270
|
+
* `.rmanrc "commands"` wherever a message needs to name the origin. It used to be the
|
|
271
|
+
* literal string `'"commands"'`, which is not a path: `path.relative` mangled it and its
|
|
272
|
+
* own quotes doubled in every message that added a pair.
|
|
273
|
+
*
|
|
274
|
+
* The metadata therefore has to name itself, since there is no basename to fall back on,
|
|
275
|
+
* and `checkCustomCommand` is what says so when it does not. */
|
|
270
276
|
if (typeof entry === 'function') {
|
|
271
|
-
direct.push({ name: '', file: '
|
|
277
|
+
direct.push({ name: '', file: '', register: entry });
|
|
272
278
|
}
|
|
273
279
|
else {
|
|
274
|
-
/**
|
|
275
|
-
*
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
direct.push({ name: commandName(custom.command), file: '"commands"', custom });
|
|
280
|
+
/** Reached here rather than at registration, so an inline command is checked the same way
|
|
281
|
+
* one loaded from a file is. */
|
|
282
|
+
const custom = checkCustomCommand(entry, describeCommandSource(''));
|
|
283
|
+
direct.push({ name: commandName(custom.command), file: '', custom });
|
|
279
284
|
}
|
|
280
285
|
}
|
|
281
286
|
}
|
package/constants.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '2.0.0-beta.
|
|
1
|
+
export const version = '2.0.0-beta.5';
|
|
@@ -157,9 +157,16 @@ export declare function loadCustomCommands(patterns: string[]): Promise<{
|
|
|
157
157
|
commands: LoadedCommand[];
|
|
158
158
|
errors: CommandLoadError[];
|
|
159
159
|
}>;
|
|
160
|
+
/**
|
|
161
|
+
* How to name a command's origin in a message: the file it came from, or the config key that
|
|
162
|
+
* declared it.
|
|
163
|
+
*
|
|
164
|
+
* @param file a `LoadedCommand.file` - `''` for a command written into `.rmanrc "commands"`.
|
|
165
|
+
*/
|
|
166
|
+
export declare function describeCommandSource(file: string): string;
|
|
160
167
|
/**
|
|
161
168
|
* Refuses a command that would take a built-in's name. Unlike a module that simply fails to load,
|
|
162
|
-
* this one is thrown: the
|
|
169
|
+
* this one is thrown: the source is fine, the *name* is the mistake, and there is no reading of
|
|
163
170
|
* `rman publish` that is safe to guess at. Silently preferring either one would leave whoever typed
|
|
164
171
|
* it unable to tell which ran.
|
|
165
172
|
*/
|
|
@@ -111,16 +111,35 @@ export async function loadCustomCommands(patterns) {
|
|
|
111
111
|
}
|
|
112
112
|
return { commands, errors };
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* How to name a command's origin in a message: the file it came from, or the config key that
|
|
116
|
+
* declared it.
|
|
117
|
+
*
|
|
118
|
+
* @param file a `LoadedCommand.file` - `''` for a command written into `.rmanrc "commands"`.
|
|
119
|
+
*/
|
|
120
|
+
/* **A command loaded from a config key has no file, and saying so beats inventing one.** `file` was
|
|
121
|
+
* `'"commands"'` for those - a string that is not a path, carrying its own quotes - and every
|
|
122
|
+
* message that formatted it got something wrong: `path.relative` mangled it, the quotes doubled
|
|
123
|
+
* (`""commands""` in the shadowing error), and `checkCustomCommand` announced it as a plugin name.
|
|
124
|
+
* One empty string and one helper is the whole fix. */
|
|
125
|
+
export function describeCommandSource(file) {
|
|
126
|
+
return file ? `"${path.relative(process.cwd(), file)}"` : '.rmanrc "commands"';
|
|
127
|
+
}
|
|
114
128
|
/**
|
|
115
129
|
* Refuses a command that would take a built-in's name. Unlike a module that simply fails to load,
|
|
116
|
-
* this one is thrown: the
|
|
130
|
+
* this one is thrown: the source is fine, the *name* is the mistake, and there is no reading of
|
|
117
131
|
* `rman publish` that is safe to guess at. Silently preferring either one would leave whoever typed
|
|
118
132
|
* it unable to tell which ran.
|
|
119
133
|
*/
|
|
134
|
+
/* The remedy differs by where the command came from, which is the other half of why the origin has
|
|
135
|
+
* to be honest: "rename the file" is the answer for a `.rman/*.mjs` and no answer at all for an
|
|
136
|
+
* entry in a config, where there is no file to rename. */
|
|
120
137
|
export function assertNoBuiltinShadowing(commands, builtins) {
|
|
121
138
|
const clash = commands.find(c => builtins.includes(c.name));
|
|
122
139
|
if (!clash)
|
|
123
140
|
return;
|
|
124
|
-
|
|
125
|
-
`
|
|
141
|
+
const remedy = clash.file
|
|
142
|
+
? `Rename the file, or give it its own name with \`command: '<name>'\`.`
|
|
143
|
+
: `Give it its own name with \`command: '<name>'\`.`;
|
|
144
|
+
throw new Error(`${describeCommandSource(clash.file)} would shadow rman's built-in "${clash.name}" command.\n ${remedy}`);
|
|
126
145
|
}
|
package/core/plugin-loader.d.ts
CHANGED
package/core/plugin-loader.js
CHANGED
|
@@ -37,17 +37,21 @@ export function registerPlugin(app, entry) {
|
|
|
37
37
|
}
|
|
38
38
|
app.plugins.add(entry);
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
/* **`source` is where the command came from, already worded for a message** - a file in quotes, or
|
|
41
|
+
* `.rmanrc "commands"` - and `describeCommandSource` is what produces it. It was called
|
|
42
|
+
* `pluginName` and the messages said `Plugin "..."`, which neither caller ever passed: both hand
|
|
43
|
+
* over a command's *origin*, so a repository's own `.rman/deploy.mjs` was announced as a plugin and
|
|
44
|
+
* a config entry came out as `Plugin """commands"""`. */
|
|
45
|
+
export function checkCustomCommand(command, source) {
|
|
41
46
|
const declared = command?.command?.trim();
|
|
42
47
|
if (!declared) {
|
|
43
|
-
throw new Error(
|
|
48
|
+
throw new Error(`${source} has a command with no "command" name - it cannot be registered.`);
|
|
44
49
|
}
|
|
45
50
|
if (typeof command.handler !== 'function') {
|
|
46
|
-
throw new Error(
|
|
51
|
+
throw new Error(`${source} command "${declared}" has no "handler" function.`);
|
|
47
52
|
}
|
|
48
53
|
if (typeof command.describe !== 'string' || !command.describe) {
|
|
49
|
-
throw new Error(
|
|
50
|
-
`nothing to list it by.`);
|
|
54
|
+
throw new Error(`${source} command "${declared}" has no "describe" - \`rman --help\` would have nothing to ` + `list it by.`);
|
|
51
55
|
}
|
|
52
56
|
return { ...command, command: declared };
|
|
53
57
|
}
|