rman 0.38.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +371 -21
  2. package/cli.js +51 -29
  3. package/commands/build.command.d.ts +3 -0
  4. package/commands/build.command.js +14 -0
  5. package/commands/changed.command.d.ts +3 -0
  6. package/commands/changed.command.js +29 -0
  7. package/commands/changelog.command.d.ts +3 -0
  8. package/commands/changelog.command.js +63 -0
  9. package/commands/ci.command.d.ts +3 -0
  10. package/commands/ci.command.js +30 -0
  11. package/commands/clean.command.d.ts +3 -0
  12. package/commands/clean.command.js +36 -0
  13. package/commands/diff.command.d.ts +3 -0
  14. package/commands/diff.command.js +51 -0
  15. package/commands/exec.command.d.ts +3 -0
  16. package/commands/exec.command.js +83 -0
  17. package/commands/import.command.d.ts +3 -0
  18. package/commands/import.command.js +27 -0
  19. package/commands/info.command.d.ts +3 -0
  20. package/commands/info.command.js +61 -0
  21. package/commands/list.command.d.ts +3 -0
  22. package/commands/list.command.js +113 -0
  23. package/commands/publish.command.d.ts +3 -0
  24. package/commands/publish.command.js +147 -0
  25. package/commands/run.command.d.ts +6 -0
  26. package/commands/run.command.js +80 -0
  27. package/commands/test.command.d.ts +3 -0
  28. package/commands/test.command.js +14 -0
  29. package/commands/version.command.d.ts +3 -0
  30. package/commands/version.command.js +125 -0
  31. package/constants.js +1 -1
  32. package/core/config.d.ts +13 -0
  33. package/core/config.js +61 -0
  34. package/core/package.d.ts +2 -0
  35. package/core/package.js +2 -0
  36. package/core/repository.d.ts +36 -3
  37. package/core/repository.js +90 -45
  38. package/index.d.ts +17 -1
  39. package/index.js +15 -1
  40. package/package.json +8 -4
  41. package/rmanrc.schema.json +202 -0
  42. package/services/changelog.service.d.ts +99 -0
  43. package/services/changelog.service.js +272 -0
  44. package/services/ci.service.d.ts +40 -0
  45. package/services/ci.service.js +204 -0
  46. package/services/clean.service.d.ts +42 -0
  47. package/services/clean.service.js +226 -0
  48. package/services/exec.service.d.ts +35 -0
  49. package/services/exec.service.js +114 -0
  50. package/services/import.service.d.ts +30 -0
  51. package/services/import.service.js +84 -0
  52. package/services/list.service.d.ts +27 -0
  53. package/services/list.service.js +25 -0
  54. package/services/publish.service.d.ts +79 -0
  55. package/services/publish.service.js +199 -0
  56. package/services/run.service.d.ts +99 -0
  57. package/services/run.service.js +432 -0
  58. package/services/system-info.d.ts +16 -0
  59. package/services/system-info.js +25 -0
  60. package/services/version.service.d.ts +95 -0
  61. package/services/version.service.js +450 -0
  62. package/services.d.ts +10 -0
  63. package/services.js +10 -0
  64. package/utils/branch-guard.d.ts +25 -0
  65. package/utils/branch-guard.js +61 -0
  66. package/utils/change-hash.d.ts +55 -0
  67. package/utils/change-hash.js +80 -0
  68. package/utils/conventional-commits.d.ts +39 -0
  69. package/utils/conventional-commits.js +43 -0
  70. package/utils/exec.d.ts +5 -6
  71. package/utils/exec.js +45 -59
  72. package/utils/git.d.ts +86 -0
  73. package/utils/git.js +276 -0
  74. package/utils/live-region.d.ts +16 -0
  75. package/utils/live-region.js +59 -0
  76. package/utils/logger.d.ts +24 -0
  77. package/utils/logger.js +39 -0
  78. package/utils/npm-run-path.d.ts +1 -1
  79. package/utils/npm-run-path.js +10 -10
  80. package/utils/package-filter.d.ts +32 -0
  81. package/utils/package-filter.js +73 -0
  82. package/utils/progress-panel.d.ts +59 -0
  83. package/utils/progress-panel.js +149 -0
  84. package/utils/workspace-range.d.ts +17 -0
  85. package/utils/workspace-range.js +28 -0
  86. package/commands/build-command.d.ts +0 -11
  87. package/commands/build-command.js +0 -26
  88. package/commands/changed-command.d.ts +0 -16
  89. package/commands/changed-command.js +0 -35
  90. package/commands/ci-command.d.ts +0 -24
  91. package/commands/ci-command.js +0 -71
  92. package/commands/execute-command.d.ts +0 -19
  93. package/commands/execute-command.js +0 -75
  94. package/commands/info-command.d.ts +0 -10
  95. package/commands/info-command.js +0 -57
  96. package/commands/list-command.d.ts +0 -38
  97. package/commands/list-command.js +0 -137
  98. package/commands/multi-task-command.d.ts +0 -22
  99. package/commands/multi-task-command.js +0 -68
  100. package/commands/publish-command.d.ts +0 -24
  101. package/commands/publish-command.js +0 -135
  102. package/commands/run-command.d.ts +0 -28
  103. package/commands/run-command.js +0 -134
  104. package/commands/version-command.d.ts +0 -25
  105. package/commands/version-command.js +0 -198
  106. package/core/command.d.ts +0 -35
  107. package/core/command.js +0 -117
  108. package/core/constants.d.ts +0 -1
  109. package/core/constants.js +0 -2
  110. package/core/logger.d.ts +0 -12
  111. package/core/logger.js +0 -2
  112. package/utils/file-utils.d.ts +0 -4
  113. package/utils/file-utils.js +0 -29
  114. package/utils/git-utils.d.ts +0 -25
  115. package/utils/git-utils.js +0 -66
  116. package/utils/npm-utils.d.ts +0 -11
  117. package/utils/npm-utils.js +0 -33
  118. package/utils/package-not-found-error.d.ts +0 -2
  119. package/utils/package-not-found-error.js +0 -2
@@ -0,0 +1,202 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://raw.githubusercontent.com/panates/rman/main/schemas/rmanrc.schema.json",
4
+ "title": "rman configuration",
5
+ "description": "Schema for rman's .rmanrc (JSON) and .rman.yml (YAML) config files, and the \"rman\" key in package.json. See docs/api.md#configuration-rmanrc-rmanyml for the full reference.",
6
+ "type": "object",
7
+ "properties": {
8
+ "$schema": {
9
+ "type": "string",
10
+ "description": "Path/URL to this schema - not read by rman itself, purely for editor tooling."
11
+ },
12
+ "packageManager": {
13
+ "type": "string",
14
+ "enum": ["npm", "yarn", "pnpm", "bun"],
15
+ "default": "npm",
16
+ "description": "Package manager used by \"ci\"/\"publish\". Root-level only - an explicit --package-manager CLI flag wins over this."
17
+ },
18
+ "logLevel": {
19
+ "$ref": "#/definitions/logLevel",
20
+ "default": "info",
21
+ "description": "Default verbosity of the classic per-step log for run/build/test/ci/clean. Root-level only. An explicit --log-level CLI flag wins over this."
22
+ },
23
+ "allowBranch": {
24
+ "$ref": "#/definitions/stringOrStringArray",
25
+ "description": "Refuse to run a state-changing command unless the current git branch matches one of these globs. Root-level only. An explicit --allow-branch CLI flag replaces this entirely (never merges)."
26
+ },
27
+ "ignoreBranch": {
28
+ "$ref": "#/definitions/stringOrStringArray",
29
+ "description": "Refuse to run a state-changing command if the current git branch matches one of these globs. Root-level only. An explicit --ignore-branch CLI flag replaces this entirely (never merges)."
30
+ },
31
+ "group": {
32
+ "description": "Release-versioning group for \"version\"/\"publish\": true (default) puts the package in the implicit repo-wide group; a string joins exactly the other packages sharing that string; false makes it a solo, fully independent group. Per-package cascaded.",
33
+ "default": true,
34
+ "oneOf": [{ "type": "boolean" }, { "type": "string", "minLength": 1 }]
35
+ },
36
+ "version": {
37
+ "type": "object",
38
+ "description": "Options for the \"version\" command/VersionService.",
39
+ "additionalProperties": false,
40
+ "properties": {
41
+ "commitMessage": {
42
+ "type": "string",
43
+ "default": "chore(release): v{version}",
44
+ "description": "Commit message for every group a \"version\" run commits. \"{version}\" is substituted when every bumped package in that commit shares one version. Root-level only."
45
+ },
46
+ "script": {
47
+ "$ref": "#/definitions/stringOrStringArray",
48
+ "description": "Command(s) to run as this package's own \"version\" npm-lifecycle step, when its package.json does not define one itself."
49
+ },
50
+ "preScript": {
51
+ "$ref": "#/definitions/stringOrStringArray",
52
+ "description": "Command(s) to run as this package's own \"preversion\" step, when its package.json does not define one itself."
53
+ },
54
+ "postScript": {
55
+ "$ref": "#/definitions/stringOrStringArray",
56
+ "description": "Command(s) to run as this package's own \"postversion\" step, when its package.json does not define one itself."
57
+ }
58
+ }
59
+ },
60
+ "changelog": {
61
+ "type": "object",
62
+ "description": "Options for the \"changelog\" command/ChangelogService, and for \"version --changelog\".",
63
+ "additionalProperties": false,
64
+ "properties": {
65
+ "ignoreTypes": {
66
+ "type": "array",
67
+ "items": { "type": "string" },
68
+ "default": [],
69
+ "description": "Conventional Commit types (e.g. \"chore\", \"ci\") dropped entirely from changelog output, instead of being folded into \"Other Changes\". Per-package cascaded."
70
+ },
71
+ "template": {
72
+ "type": "string",
73
+ "description": "Path (relative to the repository root) to a changelog template file - not the template text itself. Supports {{package}}/{{version}}/{{date}}/{{commits}} and {{features}}/{{fixes}}/{{other}}. Per-package cascaded."
74
+ },
75
+ "filePath": {
76
+ "type": "string",
77
+ "default": "CHANGELOG.md",
78
+ "description": "Where \"--write\" prepends this package's entry, relative to that package's own directory. Per-package cascaded."
79
+ },
80
+ "tagPattern": {
81
+ "type": "string",
82
+ "default": "v*",
83
+ "description": "Glob for this package's release tags. Include \"{name}\" for independent per-package tags (e.g. \"{name}@*\"); omit it for one shared repo-wide tag scheme. Per-package cascaded."
84
+ }
85
+ }
86
+ },
87
+ "clean": {
88
+ "type": "object",
89
+ "description": "Options for the \"clean\" command/CleanService.",
90
+ "additionalProperties": false,
91
+ "properties": {
92
+ "include": {
93
+ "$ref": "#/definitions/stringOrStringArray",
94
+ "description": "Extra glob(s) to remove, resolved relative to this package's own directory. Per-package cascaded (a package's own value replaces the root's, it does not merge)."
95
+ },
96
+ "exclude": {
97
+ "$ref": "#/definitions/stringOrStringArray",
98
+ "description": "Glob(s) protected from \"include\" (and from the built-in TypeScript-artifact cleanup), relative to this package's own directory. Per-package cascaded."
99
+ },
100
+ "skip": {
101
+ "type": "boolean",
102
+ "default": false,
103
+ "description": "Opts this package out of \"clean\" entirely. Per-package cascaded."
104
+ }
105
+ }
106
+ },
107
+ "run": {
108
+ "type": "object",
109
+ "description": "Per-script options for \"run\"/\"build\"/\"test\"/RunService, keyed by npm script name (e.g. \"build\", \"lint\", \"test\").",
110
+ "additionalProperties": { "$ref": "#/definitions/runScriptConfig" }
111
+ },
112
+ "packages": {
113
+ "type": "object",
114
+ "description": "Per-package overrides, keyed by the in-repo package's own name.",
115
+ "additionalProperties": {
116
+ "type": "object",
117
+ "additionalProperties": false,
118
+ "properties": {
119
+ "dependencies": {
120
+ "description": "Extra in-repo \"dependencies\" not present in this package's real package.json, purely for rman's own dependency graph (topo-sort, --deps/--dependents, run's task scheduling). An array defaults each entry's range to \"*\"; an object gives an explicit name -> range map.",
121
+ "oneOf": [
122
+ { "type": "array", "items": { "type": "string" } },
123
+ { "type": "object", "additionalProperties": { "type": "string" } }
124
+ ]
125
+ }
126
+ }
127
+ }
128
+ }
129
+ },
130
+ "definitions": {
131
+ "logLevel": {
132
+ "type": "string",
133
+ "enum": ["silent", "error", "info", "verbose"]
134
+ },
135
+ "stringOrStringArray": {
136
+ "oneOf": [
137
+ { "type": "string", "minLength": 1 },
138
+ { "type": "array", "items": { "type": "string", "minLength": 1 } }
139
+ ]
140
+ },
141
+ "runScriptConfig": {
142
+ "type": "object",
143
+ "additionalProperties": false,
144
+ "properties": {
145
+ "concurrency": {
146
+ "type": "integer",
147
+ "minimum": 1,
148
+ "description": "Max packages built at once for this script. Default: CPU count."
149
+ },
150
+ "topo": {
151
+ "type": "boolean",
152
+ "default": true,
153
+ "description": "Respect the package dependency graph: a package waits for its dependencies and is skipped if one fails. Set false for independent scripts (lint/test) - alphabetical order, no failure skipping."
154
+ },
155
+ "bail": {
156
+ "type": "boolean",
157
+ "default": true,
158
+ "description": "Stop the whole batch on this package's own failure. Unusual precedence: this package-level value outranks even an explicit CLI --bail/--no-bail flag."
159
+ },
160
+ "progress": {
161
+ "type": "boolean",
162
+ "default": true,
163
+ "description": "Show the live progress panel for this script (auto-disabled off a TTY)."
164
+ },
165
+ "logLevel": {
166
+ "$ref": "#/definitions/logLevel",
167
+ "description": "Verbosity of the classic per-step log for this package/script, when the live panel is off."
168
+ },
169
+ "changedSince": {
170
+ "type": "string",
171
+ "description": "Root-level fallback commit/hash for \"--changed-since\", used only when the CLI flag is not given."
172
+ },
173
+ "skip": {
174
+ "type": "boolean",
175
+ "default": false,
176
+ "description": "Excludes this package (or the repository root's own pre/post hooks) from running this script entirely."
177
+ },
178
+ "if": {
179
+ "type": "string",
180
+ "description": "Conditional-execution expression: atoms \"changed\"/\"dirty\"/\"committed\" (optionally \"= <hash>\" or \"= {ENV_VAR}\"), combined with and/or/not/(...). E.g. \"changed\", \"(changed or dirty) and not committed\"."
181
+ },
182
+ "script": {
183
+ "$ref": "#/definitions/stringOrStringArray",
184
+ "description": "Command(s) to run as this package's own script, when its package.json does not define one for this script name."
185
+ },
186
+ "preScript": {
187
+ "$ref": "#/definitions/stringOrStringArray",
188
+ "description": "Command(s) to run as this package's own \"pre<script>\" hook, when its package.json does not define one."
189
+ },
190
+ "postScript": {
191
+ "$ref": "#/definitions/stringOrStringArray",
192
+ "description": "Command(s) to run as this package's own \"post<script>\" hook, when its package.json does not define one."
193
+ },
194
+ "override": {
195
+ "type": "boolean",
196
+ "default": false,
197
+ "description": "When true, \"script\"/\"preScript\"/\"postScript\" above replace the package's own package.json definition even when it already has one."
198
+ }
199
+ }
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,99 @@
1
+ import type { Package } from '../core/package.js';
2
+ import type { Repository } from '../core/repository.js';
3
+ import { type PackageFilterOptions } from '../utils/package-filter.js';
4
+ export declare namespace ChangelogService {
5
+ /** Injectable dependencies shared by `getEntries`/`generate` - currently just the npm registry lookup
6
+ * `detectChangeHash` otherwise makes for itself; overridable so tests (and advanced callers)
7
+ * aren't forced through a real network call. */
8
+ interface Deps {
9
+ npmViewVersion?: (name: string, cwd: string) => Promise<string | undefined>;
10
+ }
11
+ interface Options extends PackageFilterOptions {
12
+ /** Generate the changelog since this commit/hash - applied the same way to every package.
13
+ * Default (also `"npm"` explicitly): auto-detect it per package instead, from that package's
14
+ * currently-published npm version (see `detectChangeHash`); a package this can't be resolved
15
+ * for (unpublished, no network, no matching tag) falls back to its own commits not yet
16
+ * pushed to the current branch's upstream (same reference point `--changed`/
17
+ * `--changed-since` use). */
18
+ from?: string;
19
+ /** Generate for the whole repository even when the current directory is inside a single
20
+ * package (which otherwise scopes it to just that package) - see `Repository.currentPackage`. */
21
+ root?: boolean;
22
+ /** Where a package's changelog file lives, relative to *that package's own* directory -
23
+ * default `'CHANGELOG.md'`. Applies the same way to every package; for a package that wants
24
+ * its own filename instead, use `.rmanrc changelog.filePath` (cascaded, per-package
25
+ * overridable) rather than this option - see `resolveFilePath`. Consulted even without
26
+ * `write`: when auto-detecting, if this file already exists its own last-modifying commit
27
+ * also lower-bounds the boundary, so a stale file (last updated for an older version than
28
+ * what's actually published) doesn't get changes silently skipped over - see
29
+ * `detectChangeHash`'s `catchUpFile`. */
30
+ filePath?: string;
31
+ }
32
+ /** One package's (root included) generated changelog entry - what `getEntries`/`generate`
33
+ * return. */
34
+ interface Entry {
35
+ package: Package;
36
+ /** Display name for this entry's heading - `"<repo dir name> repository"` for the root
37
+ * package, its own name otherwise (see `getEntries`'s doc comment on `{{package}}`). */
38
+ label: string;
39
+ /** Resolved from git tags, not package.json - see `resolveVersion`. */
40
+ version: string;
41
+ features: string[];
42
+ fixes: string[];
43
+ other: string[];
44
+ /** The fully rendered entry, via `.rmanrc changelog.template` (or the built-in default). */
45
+ content: string;
46
+ /** Where this entry would be (or, with `options.write`, was) written, relative to the
47
+ * package's own directory - see `GetOptions.filePath`. */
48
+ filePath: string;
49
+ }
50
+ /**
51
+ * Same as `getEntries`, and additionally - for every returned entry, when `options.write` is
52
+ * set - prepends `entry.content` into that package's own changelog file (see `Entry.filePath`).
53
+ * Still pure with respect to console output: writing a file is a real, callable-for-its-own-
54
+ * sake side effect (a "save this" request), not presentation, so it stays here rather than in
55
+ * the CLI command - printing what happened is the command's job.
56
+ */
57
+ function generateToFile(repository: Repository, options?: Options, deps?: Deps): Promise<Entry[]>;
58
+ /**
59
+ * Computes a changelog entry per package (root included) from real commits only - either
60
+ * everything since a given `--from <hash>` (applied the same way to every package), or, by
61
+ * default, auto-detected per package instead (see `detectChangeHash`). Pure: returns the
62
+ * entries, never prints or touches `CHANGELOG.md` - see `generate` for that.
63
+ *
64
+ * A commit is attributed to every package its files fall under (root included, for anything
65
+ * outside every package) - unless it's broad enough to count as a repo-wide change (see
66
+ * `BROAD_COMMIT_THRESHOLD`/`ownersOf`), in which case it goes to root alone instead of being
67
+ * repeated verbatim across most of the repository. Subject lines are grouped ✨ Features/🐛 Bug
68
+ * Fixes/🔧 Other Changes on a best-effort Conventional Commits read; anything that doesn't parse
69
+ * just lands in Other Changes as-is, so a repo that doesn't follow that convention still gets a
70
+ * usable list. `.rmanrc changelog.ignoreTypes` (cascaded, e.g. `[chore, dev]`) drops commits of
71
+ * those types entirely instead - see `ignoreTypesConfig`. A bare version-bump commit
72
+ * (`"6.0.1"`) is always dropped outright, regardless of `ignoreTypes` - see
73
+ * `VERSION_BUMP_PATTERN`.
74
+ *
75
+ * By default (or with `--from npm` explicitly), the boundary is auto-detected per package
76
+ * instead of one shared one - see `detectChangeHash`. A package that can't be resolved this way
77
+ * (unpublished, no network, no matching tag) falls back to its own commits not yet pushed to
78
+ * the current branch's upstream (the same reference point `--changed`/`--changed-since` use
79
+ * elsewhere, via `GitHelper.listCommits`) - so the command still produces something useful even
80
+ * for a repo that's never been published or tagged at all. Packages that end up resolving to
81
+ * the same hash (an explicit one, or several packages sharing one tag under fixed versioning)
82
+ * only have their commits fetched once, not once per package.
83
+ *
84
+ * Formatting comes from `.rmanrc changelog.template` - a *path* to a template file (not the
85
+ * template text itself, to keep `.rmanrc` readable), with `{{package}}`/`{{version}}`/
86
+ * `{{date}}`/`{{commits}}` (the full grouped block) and `{{features}}`/`{{fixes}}`/`{{other}}`
87
+ * (their bullet lists alone, for templates that want their own headings/order) - see
88
+ * `resolveTemplate`. `{{package}}` for the repository root is `"<repo dir name> repository"`
89
+ * (e.g. "sqb repository"), not its raw package.json name - which is often a private,
90
+ * non-published placeholder (`"sqb.v4"`) that reads like a stray version marker rather than a
91
+ * recognizable label. `{{version}}` comes from git tags, not package.json (which can drift out
92
+ * of sync with what's actually been released) - see `resolveVersion`/`.rmanrc
93
+ * changelog.tagPattern`.
94
+ *
95
+ * Run from inside a single package's own directory, it only covers that package unless
96
+ * `options.root` says otherwise (see `Repository.currentPackage`).
97
+ */
98
+ function getEntries(repository: Repository, options?: Options, deps?: Deps): Promise<Entry[]>;
99
+ }
@@ -0,0 +1,272 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { detectChangeHash, extractVersion, findLatestTag, tagPattern } from '../utils/change-hash.js';
4
+ import { parseConventionalCommit, VERSION_BUMP_PATTERN } from '../utils/conventional-commits.js';
5
+ import { GitHelper } from '../utils/git.js';
6
+ import { filterPackages } from '../utils/package-filter.js';
7
+ export var ChangelogService;
8
+ (function (ChangelogService) {
9
+ /**
10
+ * Same as `getEntries`, and additionally - for every returned entry, when `options.write` is
11
+ * set - prepends `entry.content` into that package's own changelog file (see `Entry.filePath`).
12
+ * Still pure with respect to console output: writing a file is a real, callable-for-its-own-
13
+ * sake side effect (a "save this" request), not presentation, so it stays here rather than in
14
+ * the CLI command - printing what happened is the command's job.
15
+ */
16
+ async function generateToFile(repository, options = {}, deps = {}) {
17
+ const entries = await getEntries(repository, options, deps);
18
+ for (const entry of entries)
19
+ prependToChangelogFile(entry.package, entry.filePath, entry.content);
20
+ return entries;
21
+ }
22
+ ChangelogService.generateToFile = generateToFile;
23
+ /**
24
+ * Computes a changelog entry per package (root included) from real commits only - either
25
+ * everything since a given `--from <hash>` (applied the same way to every package), or, by
26
+ * default, auto-detected per package instead (see `detectChangeHash`). Pure: returns the
27
+ * entries, never prints or touches `CHANGELOG.md` - see `generate` for that.
28
+ *
29
+ * A commit is attributed to every package its files fall under (root included, for anything
30
+ * outside every package) - unless it's broad enough to count as a repo-wide change (see
31
+ * `BROAD_COMMIT_THRESHOLD`/`ownersOf`), in which case it goes to root alone instead of being
32
+ * repeated verbatim across most of the repository. Subject lines are grouped ✨ Features/🐛 Bug
33
+ * Fixes/🔧 Other Changes on a best-effort Conventional Commits read; anything that doesn't parse
34
+ * just lands in Other Changes as-is, so a repo that doesn't follow that convention still gets a
35
+ * usable list. `.rmanrc changelog.ignoreTypes` (cascaded, e.g. `[chore, dev]`) drops commits of
36
+ * those types entirely instead - see `ignoreTypesConfig`. A bare version-bump commit
37
+ * (`"6.0.1"`) is always dropped outright, regardless of `ignoreTypes` - see
38
+ * `VERSION_BUMP_PATTERN`.
39
+ *
40
+ * By default (or with `--from npm` explicitly), the boundary is auto-detected per package
41
+ * instead of one shared one - see `detectChangeHash`. A package that can't be resolved this way
42
+ * (unpublished, no network, no matching tag) falls back to its own commits not yet pushed to
43
+ * the current branch's upstream (the same reference point `--changed`/`--changed-since` use
44
+ * elsewhere, via `GitHelper.listCommits`) - so the command still produces something useful even
45
+ * for a repo that's never been published or tagged at all. Packages that end up resolving to
46
+ * the same hash (an explicit one, or several packages sharing one tag under fixed versioning)
47
+ * only have their commits fetched once, not once per package.
48
+ *
49
+ * Formatting comes from `.rmanrc changelog.template` - a *path* to a template file (not the
50
+ * template text itself, to keep `.rmanrc` readable), with `{{package}}`/`{{version}}`/
51
+ * `{{date}}`/`{{commits}}` (the full grouped block) and `{{features}}`/`{{fixes}}`/`{{other}}`
52
+ * (their bullet lists alone, for templates that want their own headings/order) - see
53
+ * `resolveTemplate`. `{{package}}` for the repository root is `"<repo dir name> repository"`
54
+ * (e.g. "sqb repository"), not its raw package.json name - which is often a private,
55
+ * non-published placeholder (`"sqb.v4"`) that reads like a stray version marker rather than a
56
+ * recognizable label. `{{version}}` comes from git tags, not package.json (which can drift out
57
+ * of sync with what's actually been released) - see `resolveVersion`/`.rmanrc
58
+ * changelog.tagPattern`.
59
+ *
60
+ * Run from inside a single package's own directory, it only covers that package unless
61
+ * `options.root` says otherwise (see `Repository.currentPackage`).
62
+ */
63
+ async function getEntries(repository, options = {}, deps = {}) {
64
+ const cwdScope = options.root ? undefined : repository.currentPackage;
65
+ const packages = repository.getPackages().filter(p => p !== repository.rootPackage);
66
+ const targets = cwdScope ? [cwdScope] : filterPackages([repository.rootPackage, ...packages], options);
67
+ const git = new GitHelper({ cwd: repository.dirname });
68
+ // dropped up front, not just while grouping - a package whose only commits are version bumps
69
+ // should get no entry at all, rather than a heading with nothing real underneath it.
70
+ const dropVersionBumps = (commits) => commits.filter(c => !VERSION_BUMP_PATTERN.test(c.subject));
71
+ // Several packages often resolve to the identical hash (an explicit --from <hash> applies to
72
+ // all of them the same way; under fixed versioning, npm auto-detection usually does too) - so
73
+ // the git fetch for a given hash is cached, run once no matter how many packages share it.
74
+ const commitsByHash = new Map();
75
+ const listCommitsCached = (hash) => {
76
+ const key = hash ?? '';
77
+ let promise = commitsByHash.get(key);
78
+ if (!promise) {
79
+ promise = git.listCommits({ hash }).then(dropVersionBumps);
80
+ commitsByHash.set(key, promise);
81
+ }
82
+ return promise;
83
+ };
84
+ const commitsByTarget = await Promise.all(targets.map(async (pkg) => {
85
+ const catchUpFile = path.join(pkg.dirname, resolveFilePath(pkg, options.filePath));
86
+ const from = await detectChangeHash(git, pkg, {
87
+ from: options.from,
88
+ npmViewVersion: deps.npmViewVersion,
89
+ catchUpFile: fs.existsSync(catchUpFile) ? catchUpFile : undefined,
90
+ });
91
+ return listCommitsCached(from);
92
+ }));
93
+ const entries = [];
94
+ for (let i = 0; i < targets.length; i++) {
95
+ const pkg = targets[i];
96
+ const ownCommits = commitsByTarget[i].filter(c => ownersOf(repository, c).has(pkg));
97
+ if (!ownCommits.length)
98
+ continue;
99
+ const grouped = groupCommits(ownCommits.map(c => c.subject), ignoreTypesConfig(pkg));
100
+ // every commit could have been dropped by ignoreTypes - skip this package's entry entirely
101
+ // rather than rendering a heading with nothing real underneath it.
102
+ if (!grouped.features.length && !grouped.fixes.length && !grouped.other.length)
103
+ continue;
104
+ const label = pkg === repository.rootPackage ? `${path.basename(repository.dirname)} repository` : pkg.name;
105
+ const { version, content } = await renderEntry(repository, pkg, label, grouped, git);
106
+ entries.push({
107
+ package: pkg,
108
+ label,
109
+ version,
110
+ features: grouped.features,
111
+ fixes: grouped.fixes,
112
+ other: grouped.other,
113
+ content,
114
+ filePath: resolveFilePath(pkg, options.filePath),
115
+ });
116
+ }
117
+ return entries;
118
+ }
119
+ ChangelogService.getEntries = getEntries;
120
+ })(ChangelogService || (ChangelogService = {}));
121
+ /** `.rmanrc changelog.ignoreTypes` (cascaded, per-package overridable) - Conventional Commits
122
+ * `type`s to drop entirely (e.g. `[chore, dev]`), not just fold into "Other Changes". Only
123
+ * applies to commits that actually parse as `type: ...` - a non-conventional message always
124
+ * still lands in Other Changes, since it has no `type` to match against. */
125
+ function ignoreTypesConfig(pkg) {
126
+ const v = pkg.config?.changelog?.ignoreTypes;
127
+ return new Set(Array.isArray(v) ? v.map(t => String(t).toLowerCase()) : []);
128
+ }
129
+ function groupCommits(subjects, ignoreTypes = new Set()) {
130
+ const grouped = { features: [], fixes: [], other: [] };
131
+ for (const subject of subjects) {
132
+ if (VERSION_BUMP_PATTERN.test(subject))
133
+ continue;
134
+ const parsed = parseConventionalCommit(subject);
135
+ if (!parsed) {
136
+ grouped.other.push(subject);
137
+ continue;
138
+ }
139
+ const { type, scope, description } = parsed;
140
+ if (ignoreTypes.has(type))
141
+ continue;
142
+ const line = scope ? `**${scope}:** ${description}` : description;
143
+ if (type === 'feat')
144
+ grouped.features.push(line);
145
+ else if (type === 'fix')
146
+ grouped.fixes.push(line);
147
+ else
148
+ grouped.other.push(subject);
149
+ }
150
+ return grouped;
151
+ }
152
+ function bulletList(lines) {
153
+ return lines.map(l => `- ${l}`).join('\n');
154
+ }
155
+ function renderCommitsBlock(grouped) {
156
+ const sections = [];
157
+ if (grouped.features.length)
158
+ sections.push(`### ✨ Features\n\n${bulletList(grouped.features)}`);
159
+ if (grouped.fixes.length)
160
+ sections.push(`### 🐛 Bug Fixes\n\n${bulletList(grouped.fixes)}`);
161
+ if (grouped.other.length)
162
+ sections.push(`### 🔧 Other Changes\n\n${bulletList(grouped.other)}`);
163
+ return sections.join('\n\n');
164
+ }
165
+ const DEFAULT_TEMPLATE = '## {{package}} {{version}} ({{date}})\n\n{{commits}}\n';
166
+ /** `.rmanrc changelog.template` is a *path* to a template file (resolved relative to the
167
+ * repository root, regardless of which level's `.rmanrc` declared it), not the template text
168
+ * itself - keeping multi-line Markdown out of `.rmanrc` so it stays readable. Falls back to a
169
+ * small built-in template when unset. */
170
+ function resolveTemplate(repository, pkg) {
171
+ const templatePath = pkg.config?.changelog?.template;
172
+ if (typeof templatePath !== 'string' || !templatePath)
173
+ return DEFAULT_TEMPLATE;
174
+ const abs = path.resolve(repository.dirname, templatePath);
175
+ if (!fs.existsSync(abs)) {
176
+ throw new Error(`changelog.template not found for "${pkg.name}": "${templatePath}" (resolved to "${abs}")`);
177
+ }
178
+ return fs.readFileSync(abs, 'utf-8');
179
+ }
180
+ function render(template, values) {
181
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => values[key] ?? '');
182
+ }
183
+ const DEFAULT_CHANGELOG_FILE = 'CHANGELOG.md';
184
+ /** Where `write` prepends this package's entry, relative to its own directory. An explicit
185
+ * `optionsFilePath` (`Changelog.Options.filePath`, CLI `--file-path`) applies the same way to
186
+ * every package and wins over `.rmanrc changelog.filePath` (cascaded, per-package overridable),
187
+ * which in turn wins over the default `'CHANGELOG.md'`. */
188
+ function resolveFilePath(pkg, optionsFilePath) {
189
+ if (optionsFilePath)
190
+ return optionsFilePath;
191
+ const cfg = pkg.config?.changelog?.filePath;
192
+ return typeof cfg === 'string' && cfg ? cfg : DEFAULT_CHANGELOG_FILE;
193
+ }
194
+ /**
195
+ * This package's current version, from git tags rather than its (possibly stale - see the
196
+ * `{{version}}` doc on `Changelog.getEntries`) package.json. Falls back to package.json's version
197
+ * if no matching tag exists at all (never tagged, or a fresh package) - see `findLatestTag`.
198
+ */
199
+ async function resolveVersion(git, pkg) {
200
+ const tag = await findLatestTag(git, pkg);
201
+ return tag ? extractVersion(tag, tagPattern(pkg).replace('{name}', pkg.name)) : pkg.version || '';
202
+ }
203
+ /** The most specific package whose directory contains `file` - the repository root itself as the
204
+ * fallback for anything outside every package (e.g. root-level config files). Mirrors
205
+ * `Repository.currentPackage`'s longest-prefix logic, but always resolves to *something*
206
+ * (root), rather than `undefined`, since every file belongs to some changelog. */
207
+ function owningPackage(repository, file) {
208
+ let best = repository.rootPackage;
209
+ for (const pkg of repository.packages) {
210
+ const rel = path.relative(pkg.dirname, file);
211
+ const isSelfOrDescendant = rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
212
+ if (isSelfOrDescendant && pkg.dirname.length > best.dirname.length)
213
+ best = pkg;
214
+ }
215
+ return best;
216
+ }
217
+ /** A commit touching more than this fraction of all packages (a repo-wide relicense, a doc
218
+ * update stamped into every package's README, ...) is treated as a repo-wide change rather than
219
+ * attributed to each of them - see `ownersOf`. Without this, one such commit would show up
220
+ * verbatim in every single package's changelog, making them all look identical. */
221
+ const BROAD_COMMIT_THRESHOLD = 0.5;
222
+ /** ...but only once at least this many packages are actually touched - without a floor, a repo
223
+ * with only 1 or 2 packages would have *every* normal commit look "broad" (100% > 50% of a
224
+ * 1-package repo is trivially true), wrongly attributing ordinary changes to root alone. */
225
+ const BROAD_COMMIT_MIN_PACKAGES = 3;
226
+ /**
227
+ * A commit can touch more than one package at once (or a package plus root-level files) - it's
228
+ * attributed to every package its files map to, not just one. The exception is a commit broad
229
+ * enough to touch at least `BROAD_COMMIT_MIN_PACKAGES` packages *and* more than
230
+ * `BROAD_COMMIT_THRESHOLD` of all of them: that's a repo-wide maintenance change (relicensing, a
231
+ * doc pass across every package, ...), not something that belongs in each package's own release
232
+ * notes individually - it's attributed to the root alone.
233
+ */
234
+ function ownersOf(repository, commit) {
235
+ const owners = new Set();
236
+ for (const f of commit.files)
237
+ owners.add(owningPackage(repository, f));
238
+ const totalPackages = repository.packages.length;
239
+ const nonRootOwners = [...owners].filter(p => p !== repository.rootPackage).length;
240
+ if (totalPackages > 0 &&
241
+ nonRootOwners >= BROAD_COMMIT_MIN_PACKAGES &&
242
+ nonRootOwners > totalPackages * BROAD_COMMIT_THRESHOLD) {
243
+ return new Set([repository.rootPackage]);
244
+ }
245
+ return owners;
246
+ }
247
+ async function renderEntry(repository, pkg, label, grouped, git) {
248
+ const template = resolveTemplate(repository, pkg);
249
+ const version = await resolveVersion(git, pkg);
250
+ const content = render(template, {
251
+ package: label,
252
+ version,
253
+ date: new Date().toISOString().slice(0, 10),
254
+ commits: renderCommitsBlock(grouped),
255
+ features: bulletList(grouped.features),
256
+ fixes: bulletList(grouped.fixes),
257
+ other: bulletList(grouped.other),
258
+ });
259
+ return { version, content };
260
+ }
261
+ /** Prepends `content` right after the top-level "# Changelog" heading if the file already has
262
+ * one, otherwise creates the file (and any missing parent directory - `relFilePath` can nest one,
263
+ * e.g. `'docs/CHANGELOG.md'`) with one. Leaves everything already in the file untouched below it. */
264
+ function prependToChangelogFile(pkg, relFilePath, content) {
265
+ const file = path.join(pkg.dirname, relFilePath);
266
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : '';
267
+ const headerMatch = /^# Changelog\r?\n+/.exec(existing);
268
+ const header = headerMatch ? headerMatch[0] : '# Changelog\n\n';
269
+ const rest = headerMatch ? existing.slice(headerMatch[0].length) : existing;
270
+ fs.mkdirSync(path.dirname(file), { recursive: true });
271
+ fs.writeFileSync(file, header + content.trimEnd() + '\n\n' + rest);
272
+ }
@@ -0,0 +1,40 @@
1
+ import type { Repository } from '../core/repository.js';
2
+ import { type LogLevel } from '../utils/logger.js';
3
+ import { type PackageFilterOptions } from '../utils/package-filter.js';
4
+ export declare namespace CiService {
5
+ type PackageManager = (typeof PACKAGE_MANAGERS)[number];
6
+ interface Options extends PackageFilterOptions {
7
+ packageManager?: PackageManager;
8
+ /** Show the live progress panel while running. Default true, same as `run`/`build`; auto-disabled
9
+ * when stdout isn't a TTY. Doesn't affect what's printed once done - see `run`. */
10
+ progress?: boolean;
11
+ /** Verbosity of the classic per-step log (only applies when the live panel is off). Falls back to
12
+ * the root's `.rmanrc logLevel`, then 'info' - see `resolveRootLogLevel`. */
13
+ logLevel?: LogLevel;
14
+ }
15
+ /** `.rmanrc packageManager` (root only) picks the package manager used for the final install;
16
+ * explicit CLI value wins over it. Defaults to 'npm'. */
17
+ function resolvePackageManager(repository: Repository, cliValue?: PackageManager): PackageManager;
18
+ /** Deletes `node_modules` and any known lockfile directly under `dirname`. Returns the names
19
+ * that actually existed (and were removed), so the caller can log only those. */
20
+ function wipe(dirname: string): Promise<string[]>;
21
+ /**
22
+ * `ci`: a from-scratch, reproducible install for CI pipelines. For every package (root
23
+ * included), deletes `node_modules` and any lockfile - or, if the package defines its own
24
+ * `"ci"` script, runs that instead. Once every package is clean, installs once at the root
25
+ * with the configured package manager (`npm`/`yarn`/`pnpm`/`bun`).
26
+ *
27
+ * Uses the same live progress panel as `run`/`build` (see `../utils/progress-panel.ts`) while it
28
+ * runs, falling back to a plain rmdir/clean/run/install log line per step when the panel is off.
29
+ *
30
+ * Unlike `run`/`build`, it does *not* end with a per-package success tally: `ci`'s packages don't
31
+ * have independently meaningful outcomes the way a build or test run does - wiping a package is
32
+ * trivial and the one step that can genuinely fail, the install, is a single operation for the
33
+ * whole repository. Counting "N succeeded" across packages would just be noise, so only actual
34
+ * failures get called out (by name, with whatever output they produced), followed by one plain
35
+ * completed/failed line.
36
+ */
37
+ function reinstall(repository: Repository, options?: Options): Promise<void>;
38
+ }
39
+ declare const PACKAGE_MANAGERS: readonly ["npm", "yarn", "pnpm", "bun"];
40
+ export {};