rman 1.1.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,18 @@ export declare class Repository extends Package {
24
24
  * An internal cache has no business being walked anyway.
25
25
  */
26
26
  private _repoScope?;
27
+ /** Cached `${{ git.* }}` facts - see `_gitScope`. Non-enumerable for the same reason as above,
28
+ * and because reading it is a subprocess: a deep walk of a package must not spawn one. */
29
+ private _git?;
30
+ /**
31
+ * Files `${{ read(...) }}` has parsed, shared by every package's scope and keyed by the identity
32
+ * of the bytes - see `readStructuredFile`.
33
+ *
34
+ * **On the repository rather than per scope, and that is the whole point of it**: `configScope`
35
+ * is built once per package, so a cache living there would re-read a repository-level file once
36
+ * for every package that mentions it.
37
+ */
38
+ private readonly _readCache;
27
39
  protected constructor(dirname: string, monorepo: boolean, packages: Package[],
28
40
  /** The directory `Repository.create()` was actually invoked from - unlike `dirname` (the
29
41
  * resolved repository root, possibly several levels up), this is where the user's shell
@@ -64,6 +76,8 @@ export declare class Repository extends Package {
64
76
  configScope(pkg: Package, options?: {
65
77
  targetVersion?: string;
66
78
  }): ConfigScope;
79
+ /** `${{ git.* }}`, read at most once per repository per process. */
80
+ protected _gitScope(): GitScope;
67
81
  /**
68
82
  * Resolves the effective rman config for the repository root and every package, cascading
69
83
  * root -> intermediate directories -> package directory, so a `.rmanrc` placed anywhere along
@@ -104,7 +118,7 @@ export declare class Repository extends Package {
104
118
  */
105
119
  protected _resolveDeclaredPackage(entry: string): Package | undefined;
106
120
  protected _updateDependencies(): void;
107
- /** `git` facts for a `${{ repository.git.* }}` expression. Synchronous on purpose: it backs a lazy
121
+ /** `git` facts for a `${{ git.* }}` expression. Synchronous on purpose: it backs a lazy
108
122
  * getter, and a getter cannot await. Everything is `undefined` outside a git checkout - not an
109
123
  * error, just a repository without one. */
110
124
  protected _readGitScope(dirname: string): GitScope;
@@ -2,7 +2,7 @@ import { execFileSync } from 'node:child_process';
2
2
  import path from 'path';
3
3
  import semver from 'semver';
4
4
  import { GitHelper } from '../utils/git.js';
5
- import { createFileScope, DEFERRED_PATHS, interpolateConfig, readDirConfig, resolveConfig, } from './config.js';
5
+ import { createFileScope, createReadScope, DEFERRED_PATHS, interpolateConfig, readDirConfig, resolveConfig, } from './config.js';
6
6
  import { Manifest } from './manifest.js';
7
7
  import { Package } from './package.js';
8
8
  import { loadPlugins } from './plugin.js';
@@ -27,6 +27,18 @@ export class Repository extends Package {
27
27
  * An internal cache has no business being walked anyway.
28
28
  */
29
29
  _repoScope;
30
+ /** Cached `${{ git.* }}` facts - see `_gitScope`. Non-enumerable for the same reason as above,
31
+ * and because reading it is a subprocess: a deep walk of a package must not spawn one. */
32
+ _git;
33
+ /**
34
+ * Files `${{ read(...) }}` has parsed, shared by every package's scope and keyed by the identity
35
+ * of the bytes - see `readStructuredFile`.
36
+ *
37
+ * **On the repository rather than per scope, and that is the whole point of it**: `configScope`
38
+ * is built once per package, so a cache living there would re-read a repository-level file once
39
+ * for every package that mentions it.
40
+ */
41
+ _readCache = new Map();
30
42
  constructor(dirname, monorepo, packages,
31
43
  /** The directory `Repository.create()` was actually invoked from - unlike `dirname` (the
32
44
  * resolved repository root, possibly several levels up), this is where the user's shell
@@ -113,17 +125,43 @@ export class Repository extends Package {
113
125
  * seeing a value that means nothing to them.
114
126
  */
115
127
  configScope(pkg, options) {
128
+ const _this = this;
116
129
  return {
117
130
  pkg: this._packageScope(pkg, options?.targetVersion),
118
131
  repository: this._repositoryScope(),
119
132
  /** `pkg.dirname`, not the repository root: a `"[*]"` block asking whether
120
133
  * `tsconfig-build.json` exists has to be answered per package. */
121
134
  file: createFileScope(pkg.dirname),
135
+ /** Same base directory as `file`, so one `"[*]"` declaration reads each package's own copy -
136
+ * and the cache is the repository's, so a file they *share* is parsed once. */
137
+ read: createReadScope(pkg.dirname, this._readCache),
122
138
  env: { ...process.env },
123
139
  semver,
124
140
  path,
141
+ /**
142
+ * A getter, and cached on the **repository** rather than in this closure: `configScope` is
143
+ * called once per package, so a per-scope cache would still shell out once per package in a
144
+ * monorepo that mentions git at all.
145
+ *
146
+ * Enumerable, unlike `pkg.targetVersion` - it has a real answer everywhere, so nothing needs
147
+ * hiding. What keeps it lazy is `interpolateConfig` building its context from property
148
+ * descriptors instead of spreading; see the note there.
149
+ */
150
+ get git() {
151
+ return _this._gitScope();
152
+ },
125
153
  };
126
154
  }
155
+ /** `${{ git.* }}`, read at most once per repository per process. */
156
+ _gitScope() {
157
+ if (this._git)
158
+ return this._git;
159
+ const built = this._readGitScope(this.dirname);
160
+ /** Defined rather than assigned, so the cache stays out of every enumeration of the repository
161
+ * - the same reason `_repoScope` is defined this way. */
162
+ Object.defineProperty(this, '_git', { value: built, enumerable: false, writable: true });
163
+ return built;
164
+ }
127
165
  /**
128
166
  * Resolves the effective rman config for the repository root and every package, cascading
129
167
  * root -> intermediate directories -> package directory, so a `.rmanrc` placed anywhere along
@@ -239,17 +277,11 @@ export class Repository extends Package {
239
277
  if (this._repoScope)
240
278
  return this._repoScope;
241
279
  const packageScopes = this.packages.map(p => this._packageScope(p));
242
- const repoDir = this.dirname;
243
- let gitScope;
244
- const _this = this;
245
280
  const built = {
246
281
  ...this._packageScope(this.rootPackage),
247
282
  monorepo: this.monorepo,
248
283
  packages: packageScopes,
249
284
  package: (name) => packageScopes.find(p => p.name === name),
250
- get git() {
251
- return (gitScope ??= _this._readGitScope(repoDir));
252
- },
253
285
  };
254
286
  /** Defined rather than assigned, so the cache stays out of every enumeration of this object -
255
287
  * see the field's own doc for what walked into it. */
@@ -315,7 +347,7 @@ export class Repository extends Package {
315
347
  deepFindDependencies(pkg, pkg.dependencies);
316
348
  }
317
349
  }
318
- /** `git` facts for a `${{ repository.git.* }}` expression. Synchronous on purpose: it backs a lazy
350
+ /** `git` facts for a `${{ git.* }}` expression. Synchronous on purpose: it backs a lazy
319
351
  * getter, and a getter cannot await. Everything is `undefined` outside a git checkout - not an
320
352
  * error, just a repository without one. */
321
353
  _readGitScope(dirname) {
@@ -0,0 +1,75 @@
1
+ import type { Logger } from '../utils/logger.js';
2
+ import type { RunBinOptions, RunBinResult } from '../utils/run-bin.js';
3
+ import type { Package } from './package.js';
4
+ import type { Repository } from './repository.js';
5
+ /**
6
+ * What a **function step** is handed - a `run.<script>.before`/`.exec`/`.after` or a
7
+ * `version.<slot>` written as JavaScript instead of a shell command, and the `if` deciding whether
8
+ * a script runs at all.
9
+ *
10
+ * An object rather than loose parameters, for the reason `CommandContext` is one: a member added
11
+ * later must not break every step already written against it.
12
+ *
13
+ * **The whole point of the function form is *when* it runs.** A `${{ }}` expression is evaluated
14
+ * while the repository's config resolves - which every command does, `rman list` included - so it
15
+ * can only ever answer questions about the state the config was loaded in, and anything it *did*
16
+ * would happen on every invocation. A function step runs when its turn comes, with the real
17
+ * objects, in the package's own directory. Reach for it exactly when that difference matters;
18
+ * a shell command is still the right shape for a shell command.
19
+ */
20
+ export interface RunStepContext {
21
+ /**
22
+ * The package this step is running for - `pkg`, matching `${{ pkg }}` in an expression rather
23
+ * than `CommandContext.package`. `package` is a reserved word, so that spelling forces every
24
+ * author to rename it while destructuring (`{ package: current }`), which is friction paid at
25
+ * every call site for no benefit.
26
+ *
27
+ * Always set, unlike `CommandContext.package`: a step belongs to a package by construction, and
28
+ * the repo-wide bookend belongs to the root package.
29
+ */
30
+ pkg: Package;
31
+ repository: Repository;
32
+ /**
33
+ * The directory this step is *about* - the package's own, or the repository root for a monorepo's
34
+ * bookend. The same directory a shell step in this slot is spawned in.
35
+ *
36
+ * **`process.cwd()` is NOT changed, and cannot be.** A shell step gets a real working directory
37
+ * because it is a child process; a function step runs inside rman's own, and `run` executes
38
+ * packages **concurrently** - one step calling `process.chdir()` would move the ground under
39
+ * every other step running at that moment. So a relative path resolves against wherever rman was
40
+ * invoked, which is almost never what the step meant:
41
+ *
42
+ * ```js
43
+ * fs.writeFileSync('out.txt', data) // the repository root. Measured, and wrong.
44
+ * fs.writeFileSync(path.join(ctx.cwd, 'out.txt'), data) // the package
45
+ * ```
46
+ *
47
+ * `ctx.runBin` is already bound to this directory, so a binary run through it needs no such care.
48
+ */
49
+ cwd: string;
50
+ /**
51
+ * The repository's locally installed binaries, already carrying this run's `cwd` and log level -
52
+ * handed over rather than imported, for the reason `CommandContext.runBin` is.
53
+ */
54
+ runBin: (bin: string, argv: string[], options?: RunBinOptions) => Promise<RunBinResult>;
55
+ /** Logger at this run's resolved level. **Prefer it to `console`**: with the live progress panel
56
+ * on, a direct write lands beside the panel rather than in the step's own log. */
57
+ logger: Logger;
58
+ }
59
+ /**
60
+ * A step written as JavaScript. **Failure is a throw** - the return value means nothing, exactly as
61
+ * a non-zero exit is what fails a shell step and what `runBin` rejects on. A step that can report
62
+ * trouble only by returning something nobody reads is a step that passes while doing nothing.
63
+ */
64
+ export type RunStepFn = (context: RunStepContext) => void | Promise<void>;
65
+ /** One entry of a `before`/`exec`/`after` slot: a shell command, or a function. A list of them runs
66
+ * in sequence, and the two forms mix freely within one list. */
67
+ export type RunStepValue = string | RunStepFn;
68
+ /**
69
+ * A `run.<script>.if` written as JavaScript, deciding whether the script runs for this package.
70
+ *
71
+ * The string form is a small closed grammar (`changed and not private`) which cannot express an
72
+ * arbitrary condition, and a `${{ }}` one is frozen at config-load time. This is evaluated per
73
+ * package, when the run reaches it - the same context a step gets.
74
+ */
75
+ export type RunConditionFn = (context: RunStepContext) => boolean | Promise<boolean>;
@@ -0,0 +1 @@
1
+ export {};
package/index.d.ts CHANGED
@@ -25,6 +25,7 @@ export { Package } from './core/package.js';
25
25
  export type { RmanPlugin } from './core/plugin.js';
26
26
  export { definePlugin } from './core/plugin.js';
27
27
  export { Repository } from './core/repository.js';
28
+ export type { RunConditionFn, RunStepContext, RunStepFn, RunStepValue } from './core/run-step.js';
28
29
  export type { ChangeKind } from './core/version-scheme.js';
29
30
  /** The numbering seam. `VersionScheme` is abstract - `highestVersion`/`highestBump`/`smallestBump`
30
31
  * are implemented from the members around them, so a scheme states only what it must and still
@@ -1,4 +1,5 @@
1
1
  import type { RmanPlugin } from '../core/plugin.js';
2
+ import type { RunConditionFn, RunStepValue } from '../core/run-step.js';
2
3
  /**
3
4
  * Adds a `+key` alongside every key of `T`, which **appends** to whatever that key already resolved
4
5
  * to instead of replacing it - see `mergeConfig`.
@@ -9,6 +10,23 @@ import type { RmanPlugin } from '../core/plugin.js';
9
10
  export type WithAppend<T> = {
10
11
  [K in keyof T as `+${K & string}`]?: T[K];
11
12
  };
13
+ /**
14
+ * The one key every nested config node may carry: `vars` scoping that node's subtree - a fresh copy
15
+ * per level, merged per key over the level above. See `withScopedVars` in `core/config.ts` for what
16
+ * it does at resolution time, and docs/rman.md#scoped-vars for how it reads.
17
+ *
18
+ * **Every nested options interface extends this**, and a new one has to remember to - which is the
19
+ * cost of the runtime rule being general (any object node scopes) while a type can only say it one
20
+ * interface at a time. TypeScript has no way to state "and every object below this may also carry
21
+ * `vars`" without a recursive remap that would wreck the error messages.
22
+ *
23
+ * Extended by the `XOptions` interface rather than declared on `XOptionsKeys`, so `WithAppend` does
24
+ * not generate a `+vars`: appending to `vars` means nothing, since objects merge either way.
25
+ */
26
+ export interface ScopedVars {
27
+ /** Values for `${{ vars.* }}` to read, for this node and everything under it. */
28
+ vars?: Record<string, unknown>;
29
+ }
12
30
  /**
13
31
  * The shape of `.rmanrc`/`.rmanrc.yml`/`.rmanrc.cjs`/`.mjs`/`.js` (and `package.json`'s own
14
32
  * `"rman"` key) - see docs/rman.md#configuration-rmanrc-rmanrcyml for the full reference. Every
@@ -120,8 +138,8 @@ export interface RmanConfigKeys {
120
138
  githubRelease?: RmanConfig.GithubReleaseOptions;
121
139
  /** Keyed by npm script name (e.g. `"build"`, `"lint"`, `"test"`). A bare string (or array of
122
140
  * them) is shorthand for `{ exec: ... }` - `test: "mocha"` and `test: { exec: "mocha" }` mean
123
- * exactly the same thing. */
124
- run?: Record<string, string | string[] | RmanConfig.RunScriptOptions>;
141
+ * exactly the same thing, and a bare function is the same shorthand for a function step. */
142
+ run?: RmanConfig.RunConfig;
125
143
  /**
126
144
  * In-repo packages this one depends on **beyond what its own manifest declares** - purely for
127
145
  * rman's own dependency graph (topo-sort, `--deps`/`--dependents`, `run`'s scheduling, the version
@@ -190,7 +208,7 @@ export declare namespace RmanConfig {
190
208
  file: string;
191
209
  constant?: string;
192
210
  };
193
- interface VersionOptions extends VersionOptionsKeys, WithAppend<VersionOptionsKeys> {
211
+ interface VersionOptions extends VersionOptionsKeys, WithAppend<VersionOptionsKeys>, ScopedVars {
194
212
  }
195
213
  interface VersionOptionsKeys {
196
214
  commitMessage?: string;
@@ -225,14 +243,17 @@ export declare namespace RmanConfig {
225
243
  * that slot of its own (`version` in a Node repository's `package.json#scripts`, whatever a
226
244
  * plugin's step source answers elsewhere - the package's own declaration wins, as in `run`).
227
245
  * An array runs them in sequence. `${{ pkg.targetVersion }}` is bound here and in the two
228
- * below, and nowhere else. */
229
- exec?: string | string[];
246
+ * below, and nowhere else.
247
+ *
248
+ * A `RunStepFn` runs in place of a shell command - but note that `${{ pkg.targetVersion }}` is
249
+ * a *string* substitution, so a function reads the written version off `pkg` instead. */
250
+ exec?: RunStepValue | RunStepValue[];
230
251
  /** Same, before the write (`preversion` in a Node repository). */
231
- before?: string | string[];
252
+ before?: RunStepValue | RunStepValue[];
232
253
  /** Same, after it (`postversion` in a Node repository). */
233
- after?: string | string[];
254
+ after?: RunStepValue | RunStepValue[];
234
255
  }
235
- interface ChangelogOptions extends ChangelogOptionsKeys, WithAppend<ChangelogOptionsKeys> {
256
+ interface ChangelogOptions extends ChangelogOptionsKeys, WithAppend<ChangelogOptionsKeys>, ScopedVars {
236
257
  }
237
258
  interface ChangelogOptionsKeys {
238
259
  ignoreTypes?: string[];
@@ -240,7 +261,23 @@ export declare namespace RmanConfig {
240
261
  filePath?: string;
241
262
  tagPattern?: string;
242
263
  }
243
- interface RunScriptOptions extends RunScriptOptionsKeys, WithAppend<RunScriptOptionsKeys> {
264
+ /**
265
+ * The `run` block: scripts by name.
266
+ *
267
+ * **`run.vars` works at runtime but is deliberately not in this type**, and the reason is a
268
+ * measured trade rather than an oversight. `run` is keyed by script name, so any encoding that
269
+ * lets `vars` through has to widen the index signature's value type to something object-shaped -
270
+ * and TypeScript then stops excess-property-checking *every* script's options. Measured on the
271
+ * same file: with the widened index, `run: { build: { exce: 'tsc' } }` compiles clean.
272
+ *
273
+ * Catching that typo across every script is worth more than typing one key, so a typed JS config
274
+ * writing `run.vars` needs a cast (`run: { vars: { x: 2 }, build: ... } as RmanConfig['run']`).
275
+ * YAML and JSON configs are unchecked anyway and simply work. A key-remapped index signature
276
+ * (`{ [K in string as K extends 'vars' ? never : K]: ... }`) was tried and does not help - the
277
+ * remap still produces an index signature that claims `vars`.
278
+ */
279
+ type RunConfig = Record<string, RunStepValue | RunStepValue[] | RunScriptOptions>;
280
+ interface RunScriptOptions extends RunScriptOptionsKeys, WithAppend<RunScriptOptionsKeys>, ScopedVars {
244
281
  }
245
282
  interface RunScriptOptionsKeys {
246
283
  concurrency?: number;
@@ -250,17 +287,21 @@ export declare namespace RmanConfig {
250
287
  logLevel?: 'silent' | 'error' | 'info' | 'verbose';
251
288
  changedSince?: string;
252
289
  skip?: boolean;
253
- if?: string;
290
+ /** Whether this script runs for a package at all - the small `changed and not private` grammar,
291
+ * or a `RunConditionFn` for a condition it cannot express. Both are evaluated per package when
292
+ * the run reaches it; a `${{ }}` expression here is not, having been resolved when the config
293
+ * loaded. */
294
+ if?: string | RunConditionFn;
254
295
  /** Command(s) to run as this script itself, when the package's `package.json` doesn't define
255
- * it. An array runs them in sequence. */
256
- exec?: string | string[];
296
+ * it. An array runs them in sequence, and may mix shell commands with functions. */
297
+ exec?: RunStepValue | RunStepValue[];
257
298
  /** Same, for this script's `pre<script>` hook. */
258
- before?: string | string[];
299
+ before?: RunStepValue | RunStepValue[];
259
300
  /** Same, for its `post<script>` hook. */
260
- after?: string | string[];
301
+ after?: RunStepValue | RunStepValue[];
261
302
  override?: boolean;
262
303
  }
263
- interface PublishOptions extends PublishOptionsKeys, WithAppend<PublishOptionsKeys> {
304
+ interface PublishOptions extends PublishOptionsKeys, WithAppend<PublishOptionsKeys>, ScopedVars {
264
305
  }
265
306
  interface PublishOptionsKeys {
266
307
  /** Which **registry** `publish` ships this package to - default `['npm']` (every existing repo
@@ -286,7 +327,7 @@ export declare namespace RmanConfig {
286
327
  type PublishTarget = 'npm' | 'docker';
287
328
  /** Required once `"docker"` is one of this package's `publish.target`s - `publish --target
288
329
  * docker` errors clearly on a package that opts in here but leaves this out. */
289
- interface DockerPublishOptions extends DockerPublishOptionsKeys, WithAppend<DockerPublishOptionsKeys> {
330
+ interface DockerPublishOptions extends DockerPublishOptionsKeys, WithAppend<DockerPublishOptionsKeys>, ScopedVars {
290
331
  }
291
332
  interface DockerPublishOptionsKeys {
292
333
  /** DockerHub image name/repository - bare (e.g. `"my-app"`) to be prefixed with
@@ -314,7 +355,7 @@ export declare namespace RmanConfig {
314
355
  * (which tag, which repository, what the notes say) already has a sensible source. Nothing here
315
356
  * decides *whether* a release is cut: a release records that the repository shipped, so it is
316
357
  * always cut, and these are only details about how. */
317
- interface GithubReleaseOptions extends GithubReleaseOptionsKeys, WithAppend<GithubReleaseOptionsKeys> {
358
+ interface GithubReleaseOptions extends GithubReleaseOptionsKeys, WithAppend<GithubReleaseOptionsKeys>, ScopedVars {
318
359
  }
319
360
  interface GithubReleaseOptionsKeys {
320
361
  /** Files to attach to the release, as glob patterns relative to the package's own directory
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "1.1.1",
4
+ "version": "1.2.2",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
8
+ "@xmldom/xmldom": "^0.9.12",
8
9
  "ansi-colors": "^4.1.3",
9
10
  "cross-dirname": "^0.1.0",
10
11
  "easy-table": "^1.2.0",
@@ -1,9 +1,10 @@
1
1
  import type { Package } from '../core/package.js';
2
2
  import type { Repository } from '../core/repository.js';
3
+ import type { RunStepContext, RunStepFn, RunStepValue } from '../core/run-step.js';
3
4
  import { type LogLevel } from '../utils/logger.js';
4
5
  import { type PackageFilterOptions } from '../utils/package-filter.js';
5
6
  export declare namespace RunService {
6
- interface Options extends PackageFilterOptions {
7
+ export interface Options extends PackageFilterOptions {
7
8
  /** Max packages built at once: `true`/omitted = CPU count, a number = that many, `false` = serial (1). */
8
9
  parallel?: boolean | number;
9
10
  /** Respect the package dependency graph: a package waits for its dependencies and is skipped
@@ -52,17 +53,36 @@ export declare namespace RunService {
52
53
  * run:
53
54
  * test: mocha # same as test: { exec: mocha }
54
55
  */
55
- /** One command of a script, with the label the progress panel and the per-step log show. */
56
- interface ScriptStep {
56
+ /**
57
+ * One step of a script - a shell command, or a function ([`RunStepFn`](../core/run-step.ts)).
58
+ *
59
+ * A union rather than one shape with two optional fields, so every consumer is made to say which
60
+ * it is handling: the executor that forgets is the one that silently runs nothing, which is
61
+ * exactly the bug this type replaces (a function in `after` used to be dropped by
62
+ * `normalizeScriptValue` and reported as a step that succeeded).
63
+ */
64
+ export type ScriptStep = CommandStep | FunctionStep;
65
+ interface StepBase {
66
+ /** The slot it came from - `before`/`exec`/`after`, which is what the log line shows. */
57
67
  name: string;
68
+ /** What the progress panel and the per-step log print: the command itself, or the function's
69
+ * own name. */
70
+ label: string;
71
+ }
72
+ export interface CommandStep extends StepBase {
58
73
  command: string;
74
+ run?: undefined;
59
75
  }
60
- /** The three slots a script is made of, each one command or several run in sequence. The same
61
- * three names a `.rmanrc "run.<script>"` block uses, because they are the same three things. */
62
- interface ScriptSlots {
63
- before?: string[];
64
- exec?: string[];
65
- after?: string[];
76
+ export interface FunctionStep extends StepBase {
77
+ run: RunStepFn;
78
+ command?: undefined;
79
+ }
80
+ /** The three slots a script is made of, each one step or several run in sequence. The same three
81
+ * names a `.rmanrc "run.<script>"` block uses, because they are the same three things. */
82
+ export interface ScriptSlots {
83
+ before?: RunStepValue[];
84
+ exec?: RunStepValue[];
85
+ after?: RunStepValue[];
66
86
  }
67
87
  /**
68
88
  * Where a package's steps can come from besides its `.rmanrc`.
@@ -75,15 +95,15 @@ export declare namespace RunService {
75
95
  * Returns `undefined` for "this package declares nothing", not empty slots - the difference
76
96
  * decides whether the config's value applies.
77
97
  */
78
- type StepSource = (pkg: Package, script: string) => ScriptSlots | undefined;
98
+ export type StepSource = (pkg: Package, script: string) => ScriptSlots | undefined;
79
99
  /**
80
100
  * Registers a source. Called by `loadPlugins` for each plugin's `runSteps`, in `plugins`
81
101
  * declaration order - never as an import side effect, so what is registered is exactly what the
82
102
  * repository's `.rmanrc` asked for.
83
103
  */
84
- function addStepSource(source: StepSource): void;
104
+ export function addStepSource(source: StepSource): void;
85
105
  /** For tests, which would otherwise leak a source into every later case in the process. */
86
- function clearStepSources(): void;
106
+ export function clearStepSources(): void;
87
107
  /**
88
108
  * What the *package itself* declares for the lifecycle `script`, from the contributed sources
89
109
  * alone - no `.rmanrc` involved. `undefined` when it declares nothing.
@@ -95,7 +115,7 @@ export declare namespace RunService {
95
115
  * and npm's version lifecycle keeps working with no second seam and no extra line in any plugin.
96
116
  * A plugin for another ecosystem gets its own lifecycle hooks the moment it contributes steps.
97
117
  */
98
- function contributedSlots(pkg: Package, script: string): ScriptSlots | undefined;
118
+ export function contributedSlots(pkg: Package, script: string): ScriptSlots | undefined;
99
119
  /**
100
120
  * Runs one slot of a lifecycle belonging to some operation other than `run` itself - `version`'s
101
121
  * hooks around the version write are the only one so far.
@@ -106,13 +126,38 @@ export declare namespace RunService {
106
126
  * the copies would sit in the file that writes versions - which now runs no command of its own at
107
127
  * all.
108
128
  *
109
- * `fallback` is the caller's own configured command, **already evaluated**: `version`'s three
129
+ * `fallback` is the caller's own configured step(s), **already evaluated**: `version`'s three
110
130
  * paths are in `DEFERRED_PATHS` precisely because only the caller can bind
111
131
  * `${{ pkg.targetVersion }}`, so interpolating here would either be too early or need a scope this
112
132
  * service has no business holding.
133
+ *
134
+ * **A list, not one joined string.** `VersionService` used to `join(' && ')` an array into a
135
+ * single shell line, which a function step cannot be part of - and which quietly changed the
136
+ * semantics of the shell case too, since `cd x && y` in one process is not the same as two.
137
+ */
138
+ export function runLifecycleSlot(pkg: Package, script: string, slot: keyof ScriptSlots, fallback?: RunStepValue[]): Promise<void>;
139
+ export function getConfig(pkg: Package, script: string): Record<string, unknown>;
140
+ /**
141
+ * The context a function step or `if` is handed - see [`RunStepContext`](../core/run-step.ts).
142
+ *
143
+ * `runBin` and `logger` are bound to *this run* rather than left to be imported, which is the
144
+ * whole reason they are handed over: an imported `runBin` knows neither the cwd nor the resolved
145
+ * log level.
146
+ */
147
+ /**
148
+ * A `run.<script>.before`/`.exec`/`.after` (or `version.<slot>`) value: one step, or several to
149
+ * run in sequence. A shell command or a function, and a list may mix them.
150
+ *
151
+ * **Anything else throws, naming the path.** It used to `return []`, which meant a value rman did
152
+ * not recognize was dropped with no trace: writing a function here - the obvious guess, and now
153
+ * the supported form - produced `1 succeeded, 0 failed` with the step never run (measured). A
154
+ * configuration mistake has to be loud; silence here reads as success.
155
+ *
156
+ * Exported, and the only implementation: `VersionService` used to carry a second one that behaved
157
+ * differently, which is how `version.<slot>` came to join its array with `' && '`.
113
158
  */
114
- function runLifecycleSlot(pkg: Package, script: string, slot: keyof ScriptSlots, fallback?: string): Promise<void>;
115
- function getConfig(pkg: Package, script: string): Record<string, unknown>;
159
+ export function normalizeScriptValue(value: unknown, at: string): RunStepValue[];
160
+ export function createStepContext(pkg: Package, cwd: string): RunStepContext;
116
161
  /**
117
162
  * `.rmanrc` conditional execution, GitHub Actions-`if`-flavored but a small closed grammar
118
163
  * instead of a full expression language (less to get wrong, still covers what's asked for) -
@@ -126,7 +171,7 @@ export declare namespace RunService {
126
171
  * if: changed = {CHANGE_HASH} # {NAME} -> process.env.NAME first
127
172
  * if: (changed or dirty) and not committed
128
173
  */
129
- type IfNode = {
174
+ export type IfNode = {
130
175
  kind: 'atom';
131
176
  name: string;
132
177
  value?: string;
@@ -144,13 +189,14 @@ export declare namespace RunService {
144
189
  };
145
190
  /** Recursive-descent parser over `tokenizeIf`'s output: expr := or ; or := and ('or' and)* ;
146
191
  * and := unary ('and' unary)* ; unary := 'not' unary | GROUP | NAME ['=' VALUE] */
147
- function parseIfExpr(raw: unknown): IfNode | undefined;
192
+ export function parseIfExpr(raw: unknown): IfNode | undefined;
148
193
  /** Evaluates a parsed `if` expression for one package. `statusCache` avoids repeat `git` calls
149
194
  * for the same reference hash across packages/scripts in a single run. */
150
- function evaluateIf(repository: Repository, pkg: Package, node: IfNode, statusCache: Map<string, Record<string, Repository.PackageStatus>>): Promise<boolean>;
151
- function runScript(repository: Repository, script: string, options?: Options & {
195
+ export function evaluateIf(repository: Repository, pkg: Package, node: IfNode, statusCache: Map<string, Record<string, Repository.PackageStatus>>): Promise<boolean>;
196
+ export function runScript(repository: Repository, script: string, options?: Options & {
152
197
  commandName?: string;
153
198
  }): Promise<void>;
199
+ export {};
154
200
  }
155
201
  /** Resolution order: explicit CLI flag > the package's resolved `.rmanrc` > `fallback`. */
156
202
  export declare function resolveBool(cliValue: boolean | undefined, pkg: Package, script: string, key: string, fallback: boolean): boolean;