rman 2.0.0-beta.3 → 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 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, so nothing it contributes exists until a
98
- repository asks for it:
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
- plugins: ['node'] # by name
102
- platform: node # the same statement at a repository root, plus which technology its packages are
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
- Or say nothing: a repository that declares no technology gets the one its own files imply, announced
106
- on stderr rather than guessed silently. It used to take a second package (`rman-node`) and a
107
- `.rmanrc` before anything worked at all.
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. */
@@ -72,8 +72,14 @@ function preparePublishManifest(pkg, publishDir, packagesByName) {
72
72
  * builds a native module on install. The rest (`build`, `test`, `prepare`, ...) never reach the
73
73
  * consumer - `prepare` runs for a git dependency, which builds from the repository, not from this
74
74
  * tarball.
75
- * - `private` - rman refuses to publish a private package in the first place, so carrying the flag
76
- * into a manifest that is being published can only be wrong.
75
+ * - `private` - it is a statement about the **source tree**, not about the artifact derived from it,
76
+ * and a manifest being published is by definition not private. rman's own gate normally stops a
77
+ * private package before this runs, so the line looks unreachable; it is not, because `private`
78
+ * is also how a repository keeps a stray `npm publish` out of the package directory while
79
+ * publishing `build/` on purpose. That is a real configuration - this repository ran it until
80
+ * `rman publish` became its own release path and the gate started refusing the thing it ships.
81
+ * A narrower guard (`prepublishOnly`, which never reaches a consumer either) says the same thing
82
+ * without claiming the artifact is unpublishable.
77
83
  * - `publishConfig.directory` - it pointed *here*; kept, it would point one level deeper again.
78
84
  */
79
85
  function derivePublishManifest(pkg, packagesByName) {
@@ -201,14 +207,20 @@ export var PublishService;
201
207
  * out. Asking it kept proposing an already-published version until npm answered 403. */
202
208
  const published = !!view?.versions.includes(pkg.version);
203
209
  const distTag = distTagFor(pkg, options.tag).tag;
210
+ const staged = resolveStaged(pkg, options.staged);
204
211
  entries.set(pkg.name, {
205
212
  package: pkg,
206
213
  version: pkg.version,
207
214
  registryVersion,
208
215
  distTag,
216
+ staged,
209
217
  /** Printed beside the package in the plan, so where a version is going is something the
210
- * reader confirms rather than something they have to infer from the version string. */
211
- detail: distTag ? `-> dist-tag "${distTag}"` : undefined,
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,
212
224
  status: published ? 'up-to-date' : 'publish',
213
225
  reason: published
214
226
  ? `registry already has ${pkg.version}`
@@ -256,10 +268,14 @@ export var PublishService;
256
268
  const publishDir = resolvePublishDir(pkg, options.contents);
257
269
  const restore = preparePublishManifest(pkg, publishDir, packagesByName);
258
270
  try {
259
- /** The plan's own tag, not a recomputed one: `getPlan` decided where this package goes,
260
- * the reader confirmed that, and `applyPlan` is here to carry it out. `options.tag` is
261
- * the fallback only for a plan built by something other than `getPlan`. */
262
- await exec(buildPublishCommand(packageManager, { ...options, tag: entry.distTag ?? options.tag }), {
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
+ }), {
263
279
  cwd: publishDir,
264
280
  app: pkg.repository.app,
265
281
  stdio: 'inherit',
@@ -355,8 +371,17 @@ function distTagFor(pkg, tag) {
355
371
  'no --tag npm would put it on "latest" - pass --tag <name>',
356
372
  };
357
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. */
358
383
  function buildPublishCommand(packageManager, options) {
359
- const args = ['publish'];
384
+ const args = options.staged ? ['stage', 'publish'] : ['publish'];
360
385
  if (options.access)
361
386
  args.push('--access', options.access);
362
387
  if (options.tag)
@@ -369,3 +394,13 @@ function buildPublishCommand(packageManager, options) {
369
394
  args.push('--userconfig', options.userconfig);
370
395
  return `${packageManager} ${args.join(' ')}`;
371
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
+ }
@@ -49,6 +49,11 @@ export declare class NpmPublishTarget implements PublishTarget {
49
49
  describe: string;
50
50
  type: "string";
51
51
  };
52
+ staged: {
53
+ target: "cli";
54
+ describe: string;
55
+ type: "boolean";
56
+ };
52
57
  registry: {
53
58
  target: "cli";
54
59
  describe: string;
@@ -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 ${winner.file}.`);
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 to fall back on, so the metadata has to name itself - `checkCustomCommand` says
269
- * so when it does not. `"commands"` stands in for the file in that message. */
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: '"commands"', register: entry });
277
+ direct.push({ name: '', file: '', register: entry });
272
278
  }
273
279
  else {
274
- /** `checkCustomCommand` is what refuses a nameless one, with the message that names the
275
- * omission - reached here rather than at registration so an inline command is checked
276
- * the same way a plugin's used to be. */
277
- const custom = checkCustomCommand(entry, '"commands"');
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/commands.d.ts CHANGED
@@ -20,7 +20,6 @@ import './commands/exec.command.js';
20
20
  import './commands/github-release.command.js';
21
21
  import './commands/import.command.js';
22
22
  import './commands/info.command.js';
23
- import './commands/lint.command.js';
24
23
  import './commands/list.command.js';
25
24
  import './commands/publish.command.js';
26
25
  import './commands/run.command.js';
package/commands.js CHANGED
@@ -20,7 +20,6 @@ import './commands/exec.command.js';
20
20
  import './commands/github-release.command.js';
21
21
  import './commands/import.command.js';
22
22
  import './commands/info.command.js';
23
- import './commands/lint.command.js';
24
23
  import './commands/list.command.js';
25
24
  import './commands/publish.command.js';
26
25
  import './commands/run.command.js';
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '2.0.0-beta.3';
1
+ export const version = '2.0.0-beta.5';
@@ -95,13 +95,15 @@ export declare class Workspace {
95
95
  /** Root, then every directory down to `targetDir`, inclusive. A target outside the root is the
96
96
  * root alone. */
97
97
  protected _dirChain(targetDir: string): string[];
98
- /** A level's own settings - everything that is not a `"[...]"` block. */
98
+ /** One config object's own settings - everything that is not a `"[...]"` block. Shallow: a block
99
+ * is dropped whole, and what is inside it is the caller's business. */
99
100
  protected _stripSelectors(config: RmanConfig): RmanConfig;
100
101
  /**
101
- * The blocks in `config` that speak for this package, in declaration order.
102
+ * The layers in `config` that speak for this package, in the order they are merged.
102
103
  *
103
104
  * `"[/]"` matches the root and nothing else; a glob matches a package's selector and never the
104
- * root.
105
+ * root. **A block may hold further selector blocks**, which narrow it: a nested one applies only
106
+ * where its own audience and every audience it sits inside all match.
105
107
  */
106
108
  protected _matchingSelectors(config: RmanConfig, audience: Workspace.Audience): RmanConfig[];
107
109
  /**
@@ -290,24 +290,68 @@ export class Workspace {
290
290
  }
291
291
  return dirs;
292
292
  }
293
- /** A level's own settings - everything that is not a `"[...]"` block. */
293
+ /** One config object's own settings - everything that is not a `"[...]"` block. Shallow: a block
294
+ * is dropped whole, and what is inside it is the caller's business. */
295
+ /* **The symbols travel with it, and leaving them behind was a measured loss.** `mergeConfig` reads
296
+ * a key's origin and its `value` chain off the *source* object under `ORIGINS` / `PREVIOUS_VALUES`,
297
+ * so a copy that carries only string keys arrives with neither. Measured on one repository before
298
+ * this line existed: a bad expression in an unmarked key reported `Invalid expression in "group"`
299
+ * with no file, while the identical expression inside a `"[*]"` block reported
300
+ * `... ("group" (.rmanrc.yml))` - because a selector block was merged as itself and only the
301
+ * unmarked layer came through here. Nesting would have spread that to the blocks too. */
294
302
  _stripSelectors(config) {
295
303
  const result = {};
296
304
  for (const [key, value] of Object.entries(config))
297
305
  if (!ConfigReader.isSelectorKey(key))
298
306
  result[key] = value;
307
+ for (const symbol of Object.getOwnPropertySymbols(config)) {
308
+ Object.defineProperty(result, symbol, Object.getOwnPropertyDescriptor(config, symbol));
309
+ }
299
310
  return result;
300
311
  }
301
312
  /**
302
- * The blocks in `config` that speak for this package, in declaration order.
313
+ * The layers in `config` that speak for this package, in the order they are merged.
303
314
  *
304
315
  * `"[/]"` matches the root and nothing else; a glob matches a package's selector and never the
305
- * root.
316
+ * root. **A block may hold further selector blocks**, which narrow it: a nested one applies only
317
+ * where its own audience and every audience it sits inside all match.
306
318
  */
307
319
  /* **A glob never matching the root removes two traps at once**: `"[my-*]"` cannot quietly pick up
308
320
  * a repository whose root package is called `my-repo`, and `"[*]"` cannot hand a package-shaped
309
321
  * setting to a root with no build directory to apply it to. A root block needs no selector, which
310
- * is what makes `/` structural. */
322
+ * is what makes `/` structural.
323
+ *
324
+ * **Nesting is depth-first pre-order, which is the level's own rule applied again rather than a
325
+ * new one**: a block's own keys are its floor, then the blocks written inside it in declaration
326
+ * order, then the next block beside it. So the layer order for
327
+ * `"[platform:node]" { a, "[pkg-*]" { b } }, "[*]" { c }` is `a, b, c` - and the last word is
328
+ * still whatever was written last, *not* whatever is most specific. That is the same cost the
329
+ * dropped specificity ranking already documents (a catch-all written below a narrower block
330
+ * overrides it), and nesting neither adds to it nor asks the tiebreak question again: two blocks
331
+ * that both match are siblings whatever depth they sit at, and declaration order answers them.
332
+ *
333
+ * **A matching block is contributed *stripped*, not whole.** Merging it as it stands put its
334
+ * nested keys into the resolved config as literal `'[pkg-*]'` entries, which is precisely how the
335
+ * old no-op showed up in `rman config` looking as though it had worked.
336
+ *
337
+ * **A platform block speaks for the root too, and only a glob is held off it.** `"[platform:node]"`
338
+ * asks `pkg.platform.name` and the root is a package with a platform, so
339
+ * `"[platform:node]" > "[/]"` is *the root, when the root is a node package* and
340
+ * `"[platform:node]" > "[*]"` is *its packages* - which is the whole of a technology's shared
341
+ * config in one block, `vars` included.
342
+ *
343
+ * A narrower rule was tried first and measured wrong: let a chain reach the root only where it
344
+ * *names* `"[/]"`, keeping a plain `"[platform:node]"` off the root as before. It answers the pair
345
+ * correctly and still breaks the case it was written for - the nested `"[/]"` could not read the
346
+ * `vars` its parent declared (`vars is not defined`), because those are the parent's own keys.
347
+ * Narrowing an audience and hiding the enclosing block's settings from it are different things,
348
+ * and only the first is what nesting means.
349
+ *
350
+ * The cost, stated rather than hidden: a package-shaped setting written **directly** under a
351
+ * platform block now reaches the root as well - the trap that keeps globs off it. It is not the
352
+ * same trap, because neither half of that one applies here: `platform:node` is not a name and
353
+ * cannot match by accident, and it is not a catch-all. And with nesting the spelling for
354
+ * package-shaped settings is `"[platform:node]" > "[*]"`, which says so. */
311
355
  _matchingSelectors(config, audience) {
312
356
  const matches = [];
313
357
  for (const [key, value] of Object.entries(config)) {
@@ -316,7 +360,9 @@ export class Workspace {
316
360
  const { scope, test } = ConfigReader.parseSelector(key);
317
361
  if (!this._speaksFor(scope, test, audience))
318
362
  continue;
319
- matches.push(value);
363
+ const block = value;
364
+ matches.push(this._stripSelectors(block));
365
+ matches.push(...this._matchingSelectors(block, audience));
320
366
  }
321
367
  return matches;
322
368
  }
@@ -328,8 +374,12 @@ export class Workspace {
328
374
  */
329
375
  /* **A glob never matching the root removes two traps at once**: `"[my-*]"` cannot quietly pick up
330
376
  * a repository whose root package is called `my-repo`, and `"[*]"` cannot hand a package-shaped
331
- * setting to a root with no build directory to apply it to. A platform block is held to the same
332
- * line for consistency - the root has a platform, but it also has `"[/]"`.
377
+ * setting to a root with no build directory to apply it to. Neither trap exists for a platform
378
+ * block - `platform:node` is not a name and cannot match by accident, and it is not a catch-all -
379
+ * so **a platform link answers about the root like any other package**, and what keeps a plain
380
+ * `"[platform:node]"` off the root is `_matchingSelectors`' chain rule rather than a refusal here.
381
+ * Asking `isRoot` first was what made `"[platform:node]" > "[/]"` unanswerable: the pair says *the
382
+ * root, when it is a node package*, and the short-circuit never let the platform question be put.
333
383
  *
334
384
  * A package no technology claimed carries `basePlatform`, whose name is `''`, so it matches no
335
385
  * platform block at all - `parseSelector`'s test refuses an empty name rather than letting an
@@ -337,11 +387,9 @@ export class Workspace {
337
387
  _speaksFor(scope, test, audience) {
338
388
  if (scope === 'root')
339
389
  return audience.isRoot;
340
- if (audience.isRoot)
341
- return false;
342
390
  if (scope === 'platform')
343
391
  return test(audience.platform);
344
- return audience.selector !== undefined && test(audience.selector);
392
+ return !audience.isRoot && audience.selector !== undefined && test(audience.selector);
345
393
  }
346
394
  /**
347
395
  * The platform that claims a directory - the one its config names, or the first loaded platform
@@ -13,7 +13,8 @@ export declare class ConfigReader {
13
13
  */
14
14
  static isSelectorKey(key: string): boolean;
15
15
  /**
16
- * **Which packages a selector key speaks for.** Two audiences, and the second is a glob:
16
+ * **Which packages a selector key speaks for.** Three audiences, and what each is matched
17
+ * against:
17
18
  *
18
19
  * | | matches | `test` is given |
19
20
  * | --- | --- | --- |
@@ -58,7 +59,27 @@ export declare class ConfigReader {
58
59
  * default export when there is one, the module's own exports object otherwise.
59
60
  */
60
61
  protected _loadConfigModule(file: string): Promise<any>;
62
+ /**
63
+ * Checks every `"[...]"` key in a config: where it sits, what it carries, and - for a nested one -
64
+ * whether the pair could ever match a package.
65
+ */
61
66
  protected _assertSelectorBlocks(config: RmanConfig, file: string): void;
67
+ /**
68
+ * One walk, refusing a selector key under a *setting* and checking each legal one's contents.
69
+ *
70
+ * @param at the key path to `node`.
71
+ * @param enclosing the selector keys `node` sits inside, outermost first.
72
+ */
73
+ protected _assertSelectorKeys(node: unknown, file: string, at: string[], enclosing: string[]): void;
74
+ /** What one selector block may carry, and - given `outer` - whether it can match anything. */
75
+ protected _assertSelectorBlock(key: string, value: unknown, file: string, outer: string | undefined): void;
76
+ /**
77
+ * Refuses a nested pair that could never match a package together.
78
+ *
79
+ * @param outer the enclosing selector key.
80
+ * @param inner the key written inside it.
81
+ */
82
+ protected _assertNestable(outer: string, inner: string, file: string): void;
62
83
  protected _resolveExtends(context: ConfigReader.Context, config: RmanConfig, from: string, seen?: string[]): Promise<RmanConfig>;
63
84
  /**
64
85
  * Lays `options.presets` underneath this level's own config, exactly as an `extends` naming them
@@ -152,6 +173,8 @@ export declare namespace ConfigReader {
152
173
  /** Whether a package's selector is one this key speaks for. Always `true` for a root key, which
153
174
  * is matched by *being* the root rather than by name. */
154
175
  test(name: string): boolean;
176
+ /** The technologies a `"[platform:...]"` key names, and nothing for the other two scopes. */
177
+ names?: readonly string[];
155
178
  }
156
179
  interface Options {
157
180
  /** Plugins to start with - the application's. Optional like `platforms`, which it was not: a
@@ -6,6 +6,7 @@ import fastGlob from 'fast-glob';
6
6
  import * as yaml from 'js-yaml';
7
7
  import { presetNames, PRESETS_DIR } from '../../builtins/presets/index.js';
8
8
  import { declaredKind, isDeclared, isPlatform } from '../interfaces/plugin.js';
9
+ import { CODE_SUBTREES } from './config-paths.js';
9
10
  import { mergeConfig } from './merge-config.js';
10
11
  const requireConfigModule = createRequire(import.meta.url);
11
12
  const IDENTITY_KEYS = ['platform', 'name'];
@@ -21,6 +22,8 @@ const PRESET_EXTENSIONS = ['.js', '.ts'];
21
22
  /** What an error about a default preset names itself as - there is no config key to quote, since
22
23
  * these are laid down by the caller rather than written by anybody. */
23
24
  const PRESETS_LABEL = 'presets';
25
+ /** Free-form by contract - see `_assertSelectorKeys`. */
26
+ const VARS_KEY = 'vars';
24
27
  /** Keys that only an rman **config** has, for telling one from a contribution once it has already
25
28
  * been refused - see `_assertLoaded`. `name` is not among them: a plugin has one too. */
26
29
  const CONFIG_SHAPED = ['extends', PLUGINS_KEY, PLATFORMS_KEY, 'commands', 'publishTargets'];
@@ -46,7 +49,8 @@ export class ConfigReader {
46
49
  return key.length > 2 && key.startsWith('[') && key.endsWith(']');
47
50
  }
48
51
  /**
49
- * **Which packages a selector key speaks for.** Two audiences, and the second is a glob:
52
+ * **Which packages a selector key speaks for.** Three audiences, and what each is matched
53
+ * against:
50
54
  *
51
55
  * | | matches | `test` is given |
52
56
  * | --- | --- | --- |
@@ -82,7 +86,7 @@ export class ConfigReader {
82
86
  .split(',')
83
87
  .map(n => n.trim())
84
88
  .filter(Boolean);
85
- return { scope: 'platform', test: (name) => !!name && names.includes(name) };
89
+ return { scope: 'platform', test: (name) => !!name && names.includes(name), names };
86
90
  }
87
91
  const re = globToRegExp(stripWorkspacePrefix(inner));
88
92
  return { scope: 'package', test: (name) => re.test(name) };
@@ -292,38 +296,158 @@ export class ConfigReader {
292
296
  }
293
297
  return mod?.default ?? mod;
294
298
  }
299
+ /**
300
+ * Checks every `"[...]"` key in a config: where it sits, what it carries, and - for a nested one -
301
+ * whether the pair could ever match a package.
302
+ */
295
303
  _assertSelectorBlocks(config, file) {
296
- for (const [key, value] of Object.entries(config)) {
297
- if (!ConfigReader.isSelectorKey(key) || !value || typeof value !== 'object')
304
+ this._assertSelectorKeys(config, file, [], []);
305
+ }
306
+ /**
307
+ * One walk, refusing a selector key under a *setting* and checking each legal one's contents.
308
+ *
309
+ * @param at the key path to `node`.
310
+ * @param enclosing the selector keys `node` sits inside, outermost first.
311
+ */
312
+ /* **A selector key is legal at the top level of a config and directly inside another selector
313
+ * key, and nowhere else** - which is the whole of the placement rule, and `at.length >
314
+ * enclosing.length` is the whole of the test: the two arrays stay equal for as long as every
315
+ * ancestor is a selector, and `at` runs ahead the moment one is not.
316
+ *
317
+ * ```yaml
318
+ * "[platform:node]":
319
+ * "[pkg-*]": { group: x } # an intersection
320
+ * run:
321
+ * "[pkg-*]": { ... } # refused - a setting is not an audience
322
+ * ```
323
+ *
324
+ * **The nested form used to be refused outright and that was the wrong call**, on a reason that
325
+ * does not survive being written out: "selectors do not intersect, because specificity ranking was
326
+ * dropped". Ranking answers which of two *siblings* wins, and nesting asks nothing of the sort -
327
+ * a nested block is resolved depth-first in declaration order, which is the rule already in force
328
+ * one level up (`Workspace._matchingSelectors`). What the refusal cost was the only way to say
329
+ * "these packages, but only the ones that are also X" for a whole block: `if:` is per key and
330
+ * reaches neither `vars` nor any key that is not a run step.
331
+ *
332
+ * What the refusal *did* get right was that silence is unacceptable - before it, the inner block
333
+ * reached every package the outer one did, as a literal `'[pkg-*]'` key sitting in the resolved
334
+ * config where `rman config` showed it looking as though it had worked. Both halves are answered
335
+ * now: the shape works, and the shapes that could never work are refused here rather than
336
+ * quietly matching nothing.
337
+ *
338
+ * **`vars` and the contribution keys are skipped**, and both for the same reason: their contents
339
+ * are not config keys. `vars` is free-form by contract and `CODE_SUBTREES` hold plugins, commands
340
+ * and publish targets - arbitrary objects whose key space rman does not own. A bracketed name in
341
+ * either is data. */
342
+ _assertSelectorKeys(node, file, at, enclosing) {
343
+ if (!node || typeof node !== 'object' || Array.isArray(node))
344
+ return;
345
+ for (const [key, value] of Object.entries(node)) {
346
+ if (ConfigReader.isSelectorKey(key)) {
347
+ if (at.length > enclosing.length) {
348
+ throw new Error(`"${key}" in "${file}" is nested under "${at.join('.')}", which is a setting rather ` +
349
+ `than an audience - a selector is read at the top level of a config and inside ` +
350
+ `another selector, so this one would never be applied. Move it out, or use an ` +
351
+ `expression where the value itself depends on the package.`);
352
+ }
353
+ this._assertSelectorBlock(key, value, file, enclosing.at(-1));
354
+ this._assertSelectorKeys(value, file, [...at, key], [...enclosing, key]);
298
355
  continue;
299
- if (EXTENDS_KEY in value) {
300
- throw new Error(`"${key}" in "${file}" cannot use "extends" - it belongs at the top level, where it is a ` +
301
- `statement about this config rather than about the packages the selector names.`);
302
356
  }
303
- /**
304
- * **The two keys a selector cannot carry, because the selector is downstream of them.**
305
- *
306
- * A `"[glob]"` matches `Package.selector`, which comes from `name` - and which package a
307
- * directory even holds comes from `platform`. Both are read from the *unmarked* cascade, while
308
- * the packages are still being found, so a glob block setting either would need its own answer
309
- * in order to be matched at all. Typed as a whole `RmanConfig`, both look valid there and both
310
- * would simply never be read.
311
- *
312
- * `"[/]"` is exempt and keeps working: the root is addressed structurally - its directory *is*
313
- * the repository root - so a root block needs no selector and is applied during the walk.
314
- */
315
- if (ConfigReader.parseSelector(key).scope === 'root')
357
+ if (key === VARS_KEY || CODE_SUBTREES.includes(key))
316
358
  continue;
317
- for (const identity of IDENTITY_KEYS) {
318
- if (identity in value) {
319
- throw new Error(`"${key}" in "${file}" cannot set "${identity}" - a glob matches a package's selector, ` +
320
- `and "${identity}" is what the selector is derived from, so the block could never be ` +
321
- `matched in order to apply it. Write it unmarked, in the package's own ".rmanrc", or ` +
322
- `under "[/]" for the root package.`);
323
- }
359
+ this._assertSelectorKeys(value, file, [...at, key], enclosing);
360
+ }
361
+ }
362
+ /** What one selector block may carry, and - given `outer` - whether it can match anything. */
363
+ _assertSelectorBlock(key, value, file, outer) {
364
+ if (!value || typeof value !== 'object')
365
+ return;
366
+ if (outer !== undefined)
367
+ this._assertNestable(outer, key, file);
368
+ if (EXTENDS_KEY in value) {
369
+ throw new Error(`"${key}" in "${file}" cannot use "extends" - it belongs at the top level, where it is a ` +
370
+ `statement about this config rather than about the packages the selector names.`);
371
+ }
372
+ /**
373
+ * **The two keys a selector cannot carry, because the selector is downstream of them.**
374
+ *
375
+ * A `"[glob]"` matches `Package.selector`, which comes from `name` - and which package a
376
+ * directory even holds comes from `platform`. Both are read from the *unmarked* cascade, while
377
+ * the packages are still being found, so a glob block setting either would need its own answer
378
+ * in order to be matched at all. Typed as a whole `RmanConfig`, both look valid there and both
379
+ * would simply never be read.
380
+ *
381
+ * `"[/]"` is exempt and keeps working: the root is addressed structurally - its directory *is*
382
+ * the repository root - so a root block needs no selector and is applied during the walk.
383
+ */
384
+ if (ConfigReader.parseSelector(key).scope === 'root') {
385
+ /** **Except for `platform` in a *nested* `"[/]"`, which is read nowhere.**
386
+ * `Workspace._declaredPlatformName` looks at a level's own `platform` and at a **top-level**
387
+ * `"[/]"`, because which technology claims a directory has to be settled before any block is
388
+ * matched. Inside `"[platform:node]"` it is also circular - the answer decides whether the
389
+ * block that declares it applies at all. */
390
+ if (outer !== undefined && 'platform' in value) {
391
+ throw new Error(`"${key}" in "${file}" is nested inside "${outer}" and cannot set "platform" - which ` +
392
+ `technology claims a directory is settled before any selector is matched, so it is ` +
393
+ `read from the level's own keys or a top-level "[/]" and nowhere else. Write it there.`);
394
+ }
395
+ return;
396
+ }
397
+ for (const identity of IDENTITY_KEYS) {
398
+ if (identity in value) {
399
+ throw new Error(`"${key}" in "${file}" cannot set "${identity}" - a glob matches a package's selector, ` +
400
+ `and "${identity}" is what the selector is derived from, so the block could never be ` +
401
+ `matched in order to apply it. Write it unmarked, in the package's own ".rmanrc", or ` +
402
+ `under "[/]" for the root package.`);
324
403
  }
325
404
  }
326
405
  }
406
+ /**
407
+ * Refuses a nested pair that could never match a package together.
408
+ *
409
+ * @param outer the enclosing selector key.
410
+ * @param inner the key written inside it.
411
+ */
412
+ /* **Nesting is an AND, so a pair naming disjoint sets is a block that runs for nobody** - which is
413
+ * the silence worth refusing. Two pairs are decidable and both are:
414
+ *
415
+ * - **`"[/]"` paired with a glob**, either way round. A glob matches a package's name and the root
416
+ * is nobody's child, so it never matches the root whatever it is written beside; `"[/]"`
417
+ * matches nothing else. The pair is empty in both directions.
418
+ * - **Two platform blocks naming nothing in common.** A package carries one `platform.name`, so
419
+ * `"[platform:node]" > "[platform:cargo]"` is empty; `"[platform:node,cargo]" > "[platform:node]"`
420
+ * narrows and is fine.
421
+ *
422
+ * **`"[/]"` inside a platform block is the pair this deliberately allows**, and getting that wrong
423
+ * once is why the reasoning is written out. It reads as *the root, when the root is a node
424
+ * package*, which is a perfectly well-defined audience - it was unreachable only because
425
+ * `_speaksFor` asked `isRoot` before it asked the platform question. Refusing it took the natural
426
+ * shape of a technology's shared config away: a preset wanting `vars` over a whole node
427
+ * repository, the root's own settings, and the packages' settings had to hoist `vars` to the top
428
+ * level, where they also reach packages of every other technology.
429
+ *
430
+ * **A glob pair is deliberately not checked**, and the asymmetry is the point rather than an
431
+ * omission: whether two globs intersect is a real computation with a wrong answer available in
432
+ * both directions, while a platform set is `includes`. `"[pkg-*]" > "[lib-*]"` therefore loads and
433
+ * matches nothing - the same thing a top-level `"[lib-*]"` does in a repository with no such
434
+ * package, which nothing reports either. */
435
+ _assertNestable(outer, inner, file) {
436
+ const a = ConfigReader.parseSelector(outer);
437
+ const b = ConfigReader.parseSelector(inner);
438
+ const [root, glob] = a.scope === 'root' ? [outer, b.scope === 'package' && inner] : [inner, a.scope === 'package' && outer];
439
+ if ((a.scope === 'root' || b.scope === 'root') && glob) {
440
+ throw new Error(`"${inner}" in "${file}" is nested inside "${outer}", and "${glob}" can never match the ` +
441
+ `same package "${root}" does - a glob matches a package's name and the root is nobody's ` +
442
+ `child, so the pair is empty. A platform block nests with "[/]" ("the root, when it is ` +
443
+ `of that technology"); a glob does not.`);
444
+ }
445
+ if (a.scope === 'platform' && b.scope === 'platform' && !b.names?.some(name => a.test(name))) {
446
+ throw new Error(`"${inner}" in "${file}" is nested inside "${outer}", which names no technology in common ` +
447
+ `with it - a package carries one platform, so this block could never be applied. Nesting ` +
448
+ `narrows: name a technology the outer block already speaks for.`);
449
+ }
450
+ }
327
451
  async _resolveExtends(context, config, from, seen = []) {
328
452
  const declared = config[EXTENDS_KEY];
329
453
  if (declared === undefined)
@@ -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 file is fine, the *name* is the mistake, and there is no reading of
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 file is fine, the *name* is the mistake, and there is no reading of
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
- throw new Error(`"${path.relative(process.cwd(), clash.file)}" would shadow rman's built-in "${clash.name}" command.\n` +
125
- ` Rename the file, or give it its own name with \`command: '<name>'\`.`);
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
  }
@@ -20,6 +20,6 @@ export declare function checkCustomCommand<T extends {
20
20
  command?: string;
21
21
  describe?: unknown;
22
22
  handler?: unknown;
23
- }>(command: T, pluginName: string): T & {
23
+ }>(command: T, source: string): T & {
24
24
  command: string;
25
25
  };
@@ -37,17 +37,21 @@ export function registerPlugin(app, entry) {
37
37
  }
38
38
  app.plugins.add(entry);
39
39
  }
40
- export function checkCustomCommand(command, pluginName) {
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(`Plugin "${pluginName}" has a command with no "command" name - it cannot be registered.`);
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(`Plugin "${pluginName}" command "${declared}" has no "handler" function.`);
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(`Plugin "${pluginName}" command "${declared}" has no "describe" - \`rman --help\` would have ` +
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
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "2.0.0-beta.3",
4
+ "version": "2.0.0-beta.5",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
@@ -71,4 +71,4 @@
71
71
  "publishConfig": {
72
72
  "access": "public"
73
73
  }
74
- }
74
+ }
@@ -1,176 +0,0 @@
1
- import { type RmanConfig } from '../interfaces/rman-config.interface.js';
2
- declare const COMMAND: "lint";
3
- declare const config: {
4
- parallel: {
5
- target: "cli";
6
- describe: string;
7
- coerce: (v: unknown) => boolean | number | undefined;
8
- };
9
- bail: {
10
- target: "cli";
11
- describe: string;
12
- type: "boolean";
13
- };
14
- topo: {
15
- target: "cli";
16
- describe: string;
17
- type: "boolean";
18
- };
19
- progress: {
20
- target: "cli";
21
- describe: string;
22
- type: "boolean";
23
- };
24
- changed: {
25
- target: "cli";
26
- alias: string;
27
- describe: string;
28
- type: "boolean";
29
- };
30
- changedSince: {
31
- target: "cli";
32
- cliName: string;
33
- describe: string;
34
- type: "string";
35
- conflicts: string;
36
- };
37
- fromRoot: {
38
- target: "cli";
39
- cliName: string;
40
- alias: string;
41
- describe: string;
42
- type: "boolean";
43
- };
44
- allowBranch: {
45
- target: "cli";
46
- cliName: string;
47
- describe: string;
48
- type: "string";
49
- };
50
- ignoreBranch: {
51
- target: "cli";
52
- cliName: string;
53
- describe: string;
54
- type: "string";
55
- };
56
- scope: {
57
- target: "cli";
58
- describe: string;
59
- type: "string";
60
- };
61
- ignore: {
62
- target: "cli";
63
- describe: string;
64
- type: "string";
65
- };
66
- platform: {
67
- target: "cli";
68
- describe: string;
69
- type: "string";
70
- };
71
- deps: {
72
- target: "cli";
73
- describe: string;
74
- type: "boolean";
75
- };
76
- dependents: {
77
- target: "cli";
78
- describe: string;
79
- type: "boolean";
80
- };
81
- };
82
- type Args = RmanConfig.ArgsOf<typeof config, typeof COMMAND>;
83
- /**
84
- * `rman lint` - an alias for `rman run lint`, the third of the trio beside `build` and `test`.
85
- */
86
- declare const lintCommand: (app: import("../index.js").RmanApplication) => {
87
- command: "lint";
88
- describe: string;
89
- /** Read, not owned - see the note above. */
90
- configKeys: string[];
91
- config: {
92
- parallel: {
93
- target: "cli";
94
- describe: string;
95
- coerce: (v: unknown) => boolean | number | undefined;
96
- };
97
- bail: {
98
- target: "cli";
99
- describe: string;
100
- type: "boolean";
101
- };
102
- topo: {
103
- target: "cli";
104
- describe: string;
105
- type: "boolean";
106
- };
107
- progress: {
108
- target: "cli";
109
- describe: string;
110
- type: "boolean";
111
- };
112
- changed: {
113
- target: "cli";
114
- alias: string;
115
- describe: string;
116
- type: "boolean";
117
- };
118
- changedSince: {
119
- target: "cli";
120
- cliName: string;
121
- describe: string;
122
- type: "string";
123
- conflicts: string;
124
- };
125
- fromRoot: {
126
- target: "cli";
127
- cliName: string;
128
- alias: string;
129
- describe: string;
130
- type: "boolean";
131
- };
132
- allowBranch: {
133
- target: "cli";
134
- cliName: string;
135
- describe: string;
136
- type: "string";
137
- };
138
- ignoreBranch: {
139
- target: "cli";
140
- cliName: string;
141
- describe: string;
142
- type: "string";
143
- };
144
- scope: {
145
- target: "cli";
146
- describe: string;
147
- type: "string";
148
- };
149
- ignore: {
150
- target: "cli";
151
- describe: string;
152
- type: "string";
153
- };
154
- platform: {
155
- target: "cli";
156
- describe: string;
157
- type: "string";
158
- };
159
- deps: {
160
- target: "cli";
161
- describe: string;
162
- type: "boolean";
163
- };
164
- dependents: {
165
- target: "cli";
166
- describe: string;
167
- type: "boolean";
168
- };
169
- };
170
- examples: {
171
- command: string;
172
- description: string;
173
- }[];
174
- handler: (args: Args) => Promise<void>;
175
- };
176
- export default lintCommand;
@@ -1,34 +0,0 @@
1
- import { registerCommand } from '../interfaces/rman-config.interface.js';
2
- import { assertAllowedBranch, readBranchGuardOptions } from '../utils/branch-guard.js';
3
- import { readRunOptions, runOptions } from '../utils/run-options.js';
4
- const COMMAND = 'lint';
5
- const config = runOptions;
6
- /**
7
- * `rman lint` - an alias for `rman run lint`, the third of the trio beside `build` and `test`.
8
- */
9
- /* **Core, not the `node` preset, and the test is what the command *knows*.** It knows a script
10
- * name and nothing else: a Cargo repository declaring `lint: 'cargo clippy'` and a Go one
11
- * declaring `golangci-lint run` are served by exactly this file. That is the line `clean` is on the
12
- * other side of - everything `clean` knows how to delete is a TypeScript fact - and it is why
13
- * `build` and `test` are here too.
14
- *
15
- * **It owns no config key**, for the reason `build` documents: `lint` is `run lint` under another
16
- * name, so its settings are `run.lint`, which belongs to `run`. Two commands cannot contribute
17
- * under one top-level key - interface merging is not a deep merge - and these three never needed
18
- * to. */
19
- const lintCommand = registerCommand(app => {
20
- const repository = app.repository;
21
- return {
22
- command: COMMAND,
23
- describe: 'Alias for "run lint"',
24
- /** Read, not owned - see the note above. */
25
- configKeys: ['run.lint'],
26
- config,
27
- examples: [{ command: '$0 lint', description: '# Lints packages' }],
28
- handler: async (args) => {
29
- await assertAllowedBranch(repository, readBranchGuardOptions(args));
30
- await app.getService('run').runScript('lint', { ...readRunOptions(args), commandName: 'lint' });
31
- },
32
- };
33
- });
34
- export default lintCommand;