rman 0.38.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +364 -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 +6 -3
  41. package/services/changelog.service.d.ts +99 -0
  42. package/services/changelog.service.js +272 -0
  43. package/services/ci.service.d.ts +40 -0
  44. package/services/ci.service.js +204 -0
  45. package/services/clean.service.d.ts +42 -0
  46. package/services/clean.service.js +226 -0
  47. package/services/exec.service.d.ts +35 -0
  48. package/services/exec.service.js +114 -0
  49. package/services/import.service.d.ts +30 -0
  50. package/services/import.service.js +84 -0
  51. package/services/list.service.d.ts +27 -0
  52. package/services/list.service.js +25 -0
  53. package/services/publish.service.d.ts +79 -0
  54. package/services/publish.service.js +199 -0
  55. package/services/run.service.d.ts +99 -0
  56. package/services/run.service.js +432 -0
  57. package/services/system-info.d.ts +16 -0
  58. package/services/system-info.js +25 -0
  59. package/services/version.service.d.ts +95 -0
  60. package/services/version.service.js +450 -0
  61. package/services.d.ts +10 -0
  62. package/services.js +10 -0
  63. package/utils/branch-guard.d.ts +25 -0
  64. package/utils/branch-guard.js +61 -0
  65. package/utils/change-hash.d.ts +55 -0
  66. package/utils/change-hash.js +80 -0
  67. package/utils/conventional-commits.d.ts +39 -0
  68. package/utils/conventional-commits.js +43 -0
  69. package/utils/exec.d.ts +5 -6
  70. package/utils/exec.js +45 -59
  71. package/utils/git.d.ts +86 -0
  72. package/utils/git.js +276 -0
  73. package/utils/live-region.d.ts +16 -0
  74. package/utils/live-region.js +59 -0
  75. package/utils/logger.d.ts +24 -0
  76. package/utils/logger.js +39 -0
  77. package/utils/npm-run-path.d.ts +1 -1
  78. package/utils/npm-run-path.js +10 -10
  79. package/utils/package-filter.d.ts +32 -0
  80. package/utils/package-filter.js +73 -0
  81. package/utils/progress-panel.d.ts +59 -0
  82. package/utils/progress-panel.js +149 -0
  83. package/utils/workspace-range.d.ts +17 -0
  84. package/utils/workspace-range.js +28 -0
  85. package/commands/build-command.d.ts +0 -11
  86. package/commands/build-command.js +0 -26
  87. package/commands/changed-command.d.ts +0 -16
  88. package/commands/changed-command.js +0 -35
  89. package/commands/ci-command.d.ts +0 -24
  90. package/commands/ci-command.js +0 -71
  91. package/commands/execute-command.d.ts +0 -19
  92. package/commands/execute-command.js +0 -75
  93. package/commands/info-command.d.ts +0 -10
  94. package/commands/info-command.js +0 -57
  95. package/commands/list-command.d.ts +0 -38
  96. package/commands/list-command.js +0 -137
  97. package/commands/multi-task-command.d.ts +0 -22
  98. package/commands/multi-task-command.js +0 -68
  99. package/commands/publish-command.d.ts +0 -24
  100. package/commands/publish-command.js +0 -135
  101. package/commands/run-command.d.ts +0 -28
  102. package/commands/run-command.js +0 -134
  103. package/commands/version-command.d.ts +0 -25
  104. package/commands/version-command.js +0 -198
  105. package/core/command.d.ts +0 -35
  106. package/core/command.js +0 -117
  107. package/core/constants.d.ts +0 -1
  108. package/core/constants.js +0 -2
  109. package/core/logger.d.ts +0 -12
  110. package/core/logger.js +0 -2
  111. package/utils/file-utils.d.ts +0 -4
  112. package/utils/file-utils.js +0 -29
  113. package/utils/git-utils.d.ts +0 -25
  114. package/utils/git-utils.js +0 -66
  115. package/utils/npm-utils.d.ts +0 -11
  116. package/utils/npm-utils.js +0 -33
  117. package/utils/package-not-found-error.d.ts +0 -2
  118. package/utils/package-not-found-error.js +0 -2
@@ -0,0 +1,3 @@
1
+ import type { Argv } from 'yargs';
2
+ import type { Repository } from '../core/repository.js';
3
+ export declare function initCli(repository: Repository, program: Argv): void;
@@ -0,0 +1,125 @@
1
+ import readline from 'node:readline/promises';
2
+ import colors from 'ansi-colors';
3
+ import { VersionService } from '../services/version.service.js';
4
+ import { applyBranchGuardOptions, assertAllowedBranch, readBranchGuardOptions } from '../utils/branch-guard.js';
5
+ import { applyPackageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
6
+ export function initCli(repository, program) {
7
+ program.command({
8
+ command: 'version [bump]',
9
+ describe: 'Bumps versions of changed packages (and their dependents), grouped via .rmanrc "group"',
10
+ builder: cmd => applyBranchGuardOptions(applyPackageFilterOptions(cmd))
11
+ .example('$0 version patch', '# Bump patch severity directly, applied immediately')
12
+ .example('$0 version', "# Auto-detect severity from commits, show the plan, don't write anything")
13
+ .example('$0 version --interactive', '# Show the plan either way, then ask for confirmation')
14
+ .positional('bump', {
15
+ describe: 'A release-type keyword ("patch"/"minor"/"major") or an explicit semver version. ' +
16
+ 'Omit to auto-detect from commits and only preview the plan.',
17
+ type: 'string',
18
+ })
19
+ .option('interactive', {
20
+ alias: 'i',
21
+ describe: 'Show the plan and ask for confirmation before applying (with or without an explicit bump)',
22
+ type: 'boolean',
23
+ })
24
+ .option('ignore-dirty', {
25
+ describe: 'Exclude a package with uncommitted local changes instead of aborting the whole run',
26
+ type: 'boolean',
27
+ })
28
+ .option('push', {
29
+ describe: 'Push the resulting commit(s) and tag(s) to the remote once applied',
30
+ type: 'boolean',
31
+ })
32
+ .option('message', {
33
+ alias: 'm',
34
+ describe: 'Override the commit message for every group this run commits (default: .rmanrc ' +
35
+ 'version.commitMessage, or "chore(release): v{version}") - "{version}" is substituted ' +
36
+ "when a commit's own group shares one version.",
37
+ type: 'string',
38
+ })
39
+ .option('changelog', {
40
+ describe: "Also write each bumped package's CHANGELOG.md (same as running changelog --write " +
41
+ 'separately) and fold it into the same commit as its version bump',
42
+ type: 'boolean',
43
+ })
44
+ .option('preid', {
45
+ describe: 'Make the bump a prerelease with this identifier (e.g. "beta" -> 1.2.3-beta.0). ' +
46
+ 'Running again with the same --preid increments it (-> 1.2.3-beta.1); a different ' +
47
+ 'identifier starts a fresh prerelease line. Ignored when bump is an explicit semver version.',
48
+ type: 'string',
49
+ }),
50
+ handler: async (args) => {
51
+ await assertAllowedBranch(repository, readBranchGuardOptions(args));
52
+ const bump = args.bump;
53
+ const plan = await VersionService.getPlan(repository, {
54
+ ...readPackageFilterOptions(args),
55
+ bump,
56
+ ignoreDirty: args.ignoreDirty,
57
+ preid: args.preid,
58
+ });
59
+ printPlan(plan);
60
+ const errors = plan.filter(e => e.status === 'error');
61
+ if (errors.length) {
62
+ const message = `${errors.length} package(s) have uncommitted local changes ` +
63
+ '(pass --ignore-dirty to exclude them instead of aborting)';
64
+ console.log(colors.red(message));
65
+ const err = new Error(message);
66
+ err.logged = true;
67
+ throw err;
68
+ }
69
+ if (!plan.some(e => e.status === 'bump')) {
70
+ console.log(colors.gray('Nothing to version.'));
71
+ return;
72
+ }
73
+ let apply = !!bump;
74
+ if (args.interactive) {
75
+ apply = await confirm('Apply these changes?');
76
+ }
77
+ else if (!bump) {
78
+ console.log(colors.gray('Run again with an explicit bump, or --interactive, to apply.'));
79
+ return;
80
+ }
81
+ if (!apply)
82
+ return;
83
+ const applied = await VersionService.applyPlan(repository, plan, {
84
+ push: args.push,
85
+ message: args.message,
86
+ changelog: args.changelog,
87
+ });
88
+ for (const entry of applied) {
89
+ if (entry.status === 'bump') {
90
+ console.log(colors.green('updated'), colors.cyan(entry.package.name), entry.from, '->', colors.yellow(entry.to));
91
+ }
92
+ }
93
+ },
94
+ });
95
+ }
96
+ function printPlan(entries) {
97
+ for (const e of entries) {
98
+ const name = colors.cyan(e.package.name);
99
+ const group = colors.gray(`(${e.group})`);
100
+ switch (e.status) {
101
+ case 'bump':
102
+ console.log(colors.green('bump'), name, group, e.from, '->', colors.yellow(e.to), colors.gray(e.reason ?? ''));
103
+ break;
104
+ case 'no-change':
105
+ console.log(colors.gray('no-change'), name, group, e.from);
106
+ break;
107
+ case 'skip':
108
+ console.log(colors.cyan('skip'), name, group, colors.gray(e.reason ?? ''));
109
+ break;
110
+ case 'error':
111
+ console.log(colors.red('error'), name, group, colors.red(e.reason ?? ''));
112
+ break;
113
+ }
114
+ }
115
+ }
116
+ async function confirm(question) {
117
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
118
+ try {
119
+ const answer = await rl.question(`${question} (y/N) `);
120
+ return /^y(es)?$/i.test(answer.trim());
121
+ }
122
+ finally {
123
+ rl.close();
124
+ }
125
+ }
package/constants.js CHANGED
@@ -1 +1 @@
1
- export const version = '0.38.0';
1
+ export const version = '1.0.1';
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Reads the rman configuration defined at a single directory level, merging
3
+ * (in increasing precedence): `package.json#rman`, `.rman.yml`, `.rmanrc`.
4
+ */
5
+ export declare function readDirConfig(dirname: string): any;
6
+ /**
7
+ * Resolves the effective config for `targetDir` by cascading from `rootDir`
8
+ * down to `targetDir` (inclusive), the same way tsconfig's `extends` chain
9
+ * works: each directory level overrides the ones above it. This lets a
10
+ * package (or any intermediate directory) narrow or override the repository's
11
+ * root configuration for itself and everything below it.
12
+ */
13
+ export declare function resolveConfig(rootDir: string, targetDir: string, cache?: Map<string, any>): any;
package/core/config.js ADDED
@@ -0,0 +1,61 @@
1
+ import fs from 'fs';
2
+ import * as yaml from 'js-yaml';
3
+ import path from 'path';
4
+ import merge from 'putil-merge';
5
+ /**
6
+ * Reads the rman configuration defined at a single directory level, merging
7
+ * (in increasing precedence): `package.json#rman`, `.rman.yml`, `.rmanrc`.
8
+ */
9
+ export function readDirConfig(dirname) {
10
+ const result = {};
11
+ const pkgJsonFile = path.join(dirname, 'package.json');
12
+ if (fs.existsSync(pkgJsonFile)) {
13
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonFile, 'utf-8'));
14
+ if (pkgJson && typeof pkgJson.rman === 'object')
15
+ merge(result, pkgJson.rman, { deep: true });
16
+ }
17
+ const ymlFile = path.join(dirname, '.rman.yml');
18
+ if (fs.existsSync(ymlFile)) {
19
+ const obj = yaml.load(fs.readFileSync(ymlFile, 'utf-8'));
20
+ if (obj && typeof obj === 'object')
21
+ merge(result, obj, { deep: true });
22
+ }
23
+ const rcFile = path.join(dirname, '.rmanrc');
24
+ if (fs.existsSync(rcFile)) {
25
+ const obj = JSON.parse(fs.readFileSync(rcFile, 'utf-8'));
26
+ if (obj && typeof obj === 'object')
27
+ merge(result, obj, { deep: true });
28
+ }
29
+ return result;
30
+ }
31
+ /**
32
+ * Resolves the effective config for `targetDir` by cascading from `rootDir`
33
+ * down to `targetDir` (inclusive), the same way tsconfig's `extends` chain
34
+ * works: each directory level overrides the ones above it. This lets a
35
+ * package (or any intermediate directory) narrow or override the repository's
36
+ * root configuration for itself and everything below it.
37
+ */
38
+ export function resolveConfig(rootDir, targetDir, cache = new Map()) {
39
+ const result = {};
40
+ for (const dir of dirChain(rootDir, targetDir)) {
41
+ let local = cache.get(dir);
42
+ if (!local) {
43
+ local = readDirConfig(dir);
44
+ cache.set(dir, local);
45
+ }
46
+ merge(result, local, { deep: true });
47
+ }
48
+ return result;
49
+ }
50
+ function dirChain(rootDir, targetDir) {
51
+ const rel = path.relative(rootDir, targetDir);
52
+ if (!rel || rel === '.' || rel.startsWith('..'))
53
+ return [rootDir];
54
+ const dirs = [rootDir];
55
+ let dir = rootDir;
56
+ for (const segment of rel.split(path.sep)) {
57
+ dir = path.join(dir, segment);
58
+ dirs.push(dir);
59
+ }
60
+ return dirs;
61
+ }
package/core/package.d.ts CHANGED
@@ -2,6 +2,8 @@ export declare class Package {
2
2
  readonly dirname: string;
3
3
  private _json;
4
4
  dependencies: string[];
5
+ /** Effective rman config for this package, cascaded from the repository root. */
6
+ config: any;
5
7
  constructor(dirname: string);
6
8
  get basename(): string;
7
9
  get name(): string;
package/core/package.js CHANGED
@@ -4,6 +4,8 @@ export class Package {
4
4
  dirname;
5
5
  _json;
6
6
  dependencies = [];
7
+ /** Effective rman config for this package, cascaded from the repository root. */
8
+ config = {};
7
9
  constructor(dirname) {
8
10
  this.dirname = dirname;
9
11
  this.reloadJson();
@@ -1,20 +1,53 @@
1
1
  import { Package } from './package.js';
2
2
  export declare class Repository extends Package {
3
3
  readonly dirname: string;
4
- readonly config: any;
5
4
  readonly monorepo: boolean;
6
5
  readonly packages: Package[];
6
+ /** The directory `Repository.create()` was actually invoked from - unlike `dirname` (the
7
+ * resolved repository root, possibly several levels up), this is where the user's shell
8
+ * really was. Used by `currentPackage` to scope commands to "the package I'm standing in". */
9
+ readonly cwd: string;
7
10
  readonly rootPackage: Package;
8
- protected constructor(dirname: string, config: any, monorepo: boolean, packages: Package[]);
11
+ protected constructor(dirname: string, monorepo: boolean, packages: Package[],
12
+ /** The directory `Repository.create()` was actually invoked from - unlike `dirname` (the
13
+ * resolved repository root, possibly several levels up), this is where the user's shell
14
+ * really was. Used by `currentPackage` to scope commands to "the package I'm standing in". */
15
+ cwd?: string);
16
+ /**
17
+ * The package whose own directory contains `cwd` (the deepest match, so a package nested
18
+ * inside another's directory resolves to the innermost one) - or `undefined` when `cwd` *is*
19
+ * the repository root itself, or isn't inside any known package (e.g. a non-monorepo checkout,
20
+ * or a stray directory the workspace glob doesn't cover).
21
+ */
22
+ get currentPackage(): Package | undefined;
9
23
  getPackages(options?: {
10
24
  scope?: string | string[];
11
25
  toposort?: boolean;
12
26
  }): Package[];
13
27
  getPackage(name: string): Package | undefined;
28
+ /**
29
+ * Reports each package's git change status. `dirty` (uncommitted local
30
+ * changes) is always checked first, regardless of `hash` - a file can be
31
+ * dirty no matter what it's being compared against. Once a package isn't
32
+ * dirty, the reference point decides the rest: without `hash`, `committed`
33
+ * means committed but not yet in the upstream branch; with `hash`, `changed`
34
+ * means it differs from that commit. Otherwise a package is `clean`.
35
+ */
36
+ listStatus(options?: {
37
+ hash?: string;
38
+ }): Promise<Record<string, Repository.PackageStatus>>;
39
+ /**
40
+ * Resolves the effective rman config for the repository root and every
41
+ * package, cascading root -> intermediate directories -> package directory,
42
+ * so a `.rmanrc` placed anywhere along that path overrides the levels above it.
43
+ */
44
+ protected _resolveConfigs(): void;
14
45
  protected _updateDependencies(): void;
15
46
  static create(root?: string, options?: {
16
47
  deep?: number;
17
48
  }): Repository;
18
49
  protected static _resolvePackages(dirname: string, patterns: string[]): Package[];
19
- protected static _readConfig(dirname: string): any;
50
+ }
51
+ export declare namespace Repository {
52
+ type PackageStatus = 'dirty' | 'committed' | 'changed' | 'clean';
20
53
  }
@@ -1,27 +1,55 @@
1
1
  import glob from 'fast-glob';
2
2
  import fs from 'fs';
3
- import * as yaml from 'js-yaml';
4
3
  import path from 'path';
5
- import merge from 'putil-merge';
4
+ import { GitHelper } from '../utils/git.js';
5
+ import { resolveConfig } from './config.js';
6
6
  import { Package } from './package.js';
7
7
  export class Repository extends Package {
8
8
  dirname;
9
- config;
10
9
  monorepo;
11
10
  packages;
11
+ cwd;
12
12
  rootPackage;
13
- constructor(dirname, config, monorepo, packages) {
13
+ constructor(dirname, monorepo, packages,
14
+ /** The directory `Repository.create()` was actually invoked from - unlike `dirname` (the
15
+ * resolved repository root, possibly several levels up), this is where the user's shell
16
+ * really was. Used by `currentPackage` to scope commands to "the package I'm standing in". */
17
+ cwd = dirname) {
14
18
  super(dirname);
15
19
  this.dirname = dirname;
16
- this.config = config;
17
20
  this.monorepo = monorepo;
18
21
  this.packages = packages;
22
+ this.cwd = cwd;
19
23
  this.rootPackage = new Package(dirname);
20
24
  if (!monorepo)
21
25
  this.packages = [this.rootPackage];
26
+ this._resolveConfigs();
27
+ this._updateDependencies();
28
+ }
29
+ /**
30
+ * The package whose own directory contains `cwd` (the deepest match, so a package nested
31
+ * inside another's directory resolves to the innermost one) - or `undefined` when `cwd` *is*
32
+ * the repository root itself, or isn't inside any known package (e.g. a non-monorepo checkout,
33
+ * or a stray directory the workspace glob doesn't cover).
34
+ */
35
+ get currentPackage() {
36
+ if (path.resolve(this.cwd) === path.resolve(this.dirname))
37
+ return undefined;
38
+ let best;
39
+ for (const pkg of this.packages) {
40
+ const rel = path.relative(pkg.dirname, this.cwd);
41
+ const isSelfOrDescendant = rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
42
+ if (isSelfOrDescendant && (!best || pkg.dirname.length > best.dirname.length))
43
+ best = pkg;
44
+ }
45
+ return best;
22
46
  }
23
47
  getPackages(options) {
24
- const result = [...this.packages];
48
+ let result = [...this.packages];
49
+ if (options?.scope) {
50
+ const scopes = Array.isArray(options.scope) ? options.scope : [options.scope];
51
+ result = result.filter(p => scopes.includes(p.name));
52
+ }
25
53
  if (options?.toposort)
26
54
  topoSortPackages(result);
27
55
  return result;
@@ -29,6 +57,50 @@ export class Repository extends Package {
29
57
  getPackage(name) {
30
58
  return this.packages.find(p => p.name === name);
31
59
  }
60
+ /**
61
+ * Reports each package's git change status. `dirty` (uncommitted local
62
+ * changes) is always checked first, regardless of `hash` - a file can be
63
+ * dirty no matter what it's being compared against. Once a package isn't
64
+ * dirty, the reference point decides the rest: without `hash`, `committed`
65
+ * means committed but not yet in the upstream branch; with `hash`, `changed`
66
+ * means it differs from that commit. Otherwise a package is `clean`.
67
+ */
68
+ async listStatus(options) {
69
+ const hash = options?.hash;
70
+ const git = new GitHelper({ cwd: this.dirname });
71
+ const packages = this.getPackages();
72
+ const belongsTo = (p, files) => files.some(f => !path.relative(p.dirname, f).startsWith('..'));
73
+ const [dirtyFiles, referenceFiles] = await Promise.all([
74
+ git.listDirtyFiles({ absolute: true }),
75
+ hash ? git.listChangedSince(hash, { absolute: true }) : git.listCommittedFiles({ absolute: true }),
76
+ ]);
77
+ const result = {};
78
+ for (const p of packages) {
79
+ if (belongsTo(p, dirtyFiles))
80
+ result[p.name] = 'dirty';
81
+ else if (belongsTo(p, referenceFiles))
82
+ result[p.name] = hash ? 'changed' : 'committed';
83
+ else
84
+ result[p.name] = 'clean';
85
+ }
86
+ return result;
87
+ }
88
+ /**
89
+ * Resolves the effective rman config for the repository root and every
90
+ * package, cascading root -> intermediate directories -> package directory,
91
+ * so a `.rmanrc` placed anywhere along that path overrides the levels above it.
92
+ */
93
+ _resolveConfigs() {
94
+ const cache = new Map();
95
+ const rootConfig = resolveConfig(this.dirname, this.dirname, cache);
96
+ this.config = rootConfig;
97
+ this.rootPackage.config = rootConfig;
98
+ for (const pkg of this.packages) {
99
+ if (pkg === this.rootPackage)
100
+ continue;
101
+ pkg.config = resolveConfig(this.dirname, pkg.dirname, cache);
102
+ }
103
+ }
32
104
  _updateDependencies() {
33
105
  const deps = {};
34
106
  for (const pkg of this.packages) {
@@ -38,7 +110,7 @@ export class Repository extends Package {
38
110
  ...pkg.json.peerDependencies,
39
111
  ...pkg.json.optionalDependencies,
40
112
  };
41
- const configDeps = this.config.packages?.[pkg.name]?.dependencies;
113
+ const configDeps = pkg.config.packages?.[pkg.name]?.dependencies;
42
114
  if (configDeps) {
43
115
  if (Array.isArray(configDeps))
44
116
  configDeps.forEach(x => (o[x] = o[x] || '*'));
@@ -60,13 +132,16 @@ export class Repository extends Package {
60
132
  return;
61
133
  circularCheck.push(pkg.name);
62
134
  for (const s of pkg.dependencies) {
63
- if (!target.includes(s)) {
135
+ /** `target` starts out *as* the top-level package's own `dependencies` array, so its
136
+ * direct entries are trivially "already in target" - recursing only when newly-added
137
+ * would mean a direct dependency's own transitive deps never get pulled in. Recurse
138
+ * unconditionally (guarded by circularCheck); only skip re-adding an existing entry,
139
+ * and never let the top-level package end up depending on itself via a cycle. */
140
+ if (s !== circularCheck[0] && !target.includes(s))
64
141
  target.push(s);
65
- const p = this.getPackage(s);
66
- if (p) {
67
- deepFindDependencies(p, target);
68
- }
69
- }
142
+ const p = this.getPackage(s);
143
+ if (p)
144
+ deepFindDependencies(p, target);
70
145
  }
71
146
  };
72
147
  for (const pkg of this.packages) {
@@ -84,10 +159,7 @@ export class Repository extends Package {
84
159
  const pkgJson = JSON.parse(fs.readFileSync(f, 'utf-8'));
85
160
  if (Array.isArray(pkgJson.workspaces)) {
86
161
  const packages = this._resolvePackages(pkgDirname, pkgJson.workspaces);
87
- const config = this._readConfig(pkgDirname);
88
- const repo = new Repository(pkgDirname, config, true, packages);
89
- repo._updateDependencies();
90
- return repo;
162
+ return new Repository(pkgDirname, true, packages, dirname);
91
163
  }
92
164
  /** If we reach to the root of the project */
93
165
  if (fs.existsSync(path.join(pkgDirname, '.git')))
@@ -95,11 +167,7 @@ export class Repository extends Package {
95
167
  }
96
168
  pkgDirname = path.resolve(pkgDirname, '..');
97
169
  }
98
- const config = this._readConfig(dirname);
99
- const repo = new Repository(dirname, config, false, []);
100
- repo._updateDependencies();
101
- return repo;
102
- // throw new Error('No monorepo project detected');
170
+ return new Repository(dirname, false, [], dirname);
103
171
  }
104
172
  static _resolvePackages(dirname, patterns) {
105
173
  const packages = [];
@@ -118,29 +186,6 @@ export class Repository extends Package {
118
186
  }
119
187
  return packages;
120
188
  }
121
- static _readConfig(dirname) {
122
- const result = {};
123
- const f = path.resolve(dirname, 'package.json');
124
- let pkgJson;
125
- if (fs.existsSync(f)) {
126
- pkgJson = JSON.parse(fs.readFileSync(f, 'utf-8'));
127
- }
128
- if (pkgJson && typeof pkgJson.rman === 'object')
129
- merge(result, pkgJson.rman, { deep: true });
130
- let filename = path.resolve(dirname, '.rman.yml');
131
- if (fs.existsSync(filename)) {
132
- const obj = yaml.load(fs.readFileSync(filename, 'utf-8'));
133
- if (obj && typeof obj === 'object')
134
- merge(result, obj, { deep: true });
135
- }
136
- filename = path.resolve(dirname, '.rmanrc');
137
- if (fs.existsSync(filename)) {
138
- const obj = JSON.parse(fs.readFileSync(filename, 'utf-8'));
139
- if (obj && typeof obj === 'object')
140
- merge(result, obj, { deep: true });
141
- }
142
- return result;
143
- }
144
189
  }
145
190
  function topoSortPackages(packages) {
146
191
  packages.sort((a, b) => {
package/index.d.ts CHANGED
@@ -1 +1,17 @@
1
- export * from './core/repository.js';
1
+ /**
2
+ * Programmatic API - the same logic the CLI commands run, importable directly without going
3
+ * through yargs/argv. Each domain's logic lives in a `<Name>` namespace under `./services/*.ts`
4
+ * (e.g. `ChangelogService`, `CiService`, `CleanService`, `ListService`, `RunService`,
5
+ * `SystemInfo`, `VersionService`), re-exported here via `./services.js`. Purpose-specific functions, not one
6
+ * generic `run`/`get` per domain (see e.g. `SystemInfo.getSystemInfo`/`getRepositoryInfo`, kept
7
+ * separate since they're genuinely independent capabilities, not just steps of one operation) -
8
+ * CLI-only concerns (argv parsing, `--help` text, and all console/file presentation) stay in
9
+ * `cli.ts` and the individual `commands/*.command.ts` modules, which are not exported here.
10
+ */
11
+ export { Package } from './core/package.js';
12
+ export { Repository } from './core/repository.js';
13
+ export * from './services.js';
14
+ export type { DetectChangeHashOptions } from './utils/change-hash.js';
15
+ export { detectChangeHash } from './utils/change-hash.js';
16
+ export type { LogLevel } from './utils/logger.js';
17
+ export { LOG_LEVELS, Logger, resolveRootLogLevel } from './utils/logger.js';
package/index.js CHANGED
@@ -1 +1,15 @@
1
- export * from './core/repository.js';
1
+ /**
2
+ * Programmatic API - the same logic the CLI commands run, importable directly without going
3
+ * through yargs/argv. Each domain's logic lives in a `<Name>` namespace under `./services/*.ts`
4
+ * (e.g. `ChangelogService`, `CiService`, `CleanService`, `ListService`, `RunService`,
5
+ * `SystemInfo`, `VersionService`), re-exported here via `./services.js`. Purpose-specific functions, not one
6
+ * generic `run`/`get` per domain (see e.g. `SystemInfo.getSystemInfo`/`getRepositoryInfo`, kept
7
+ * separate since they're genuinely independent capabilities, not just steps of one operation) -
8
+ * CLI-only concerns (argv parsing, `--help` text, and all console/file presentation) stay in
9
+ * `cli.ts` and the individual `commands/*.command.ts` modules, which are not exported here.
10
+ */
11
+ export { Package } from './core/package.js';
12
+ export { Repository } from './core/repository.js';
13
+ export * from './services.js';
14
+ export { detectChangeHash } from './utils/change-hash.js';
15
+ export { LOG_LEVELS, Logger, resolveRootLogLevel } from './utils/logger.js';
package/package.json CHANGED
@@ -1,19 +1,21 @@
1
1
  {
2
2
  "name": "rman",
3
3
  "description": "Repository manager",
4
- "version": "0.38.0",
4
+ "version": "1.0.1",
5
5
  "author": "Panates",
6
6
  "license": "MIT",
7
7
  "dependencies": {
8
8
  "@netlify/parse-npm-script": "^0.1.2",
9
9
  "ansi-colors": "^4.1.3",
10
- "easy-table": "^1.2.0",
11
10
  "cross-dirname": "^0.1.0",
11
+ "easy-table": "^1.2.0",
12
12
  "envinfo": "^7.21.0",
13
13
  "fast-glob": "^3.3.3",
14
+ "fast-tokenizer": "^1.10.0",
14
15
  "ini": "^7.0.0",
15
16
  "is-ci": "^4.1.0",
16
17
  "js-yaml": "^5.4.1",
18
+ "micromatch": "^4.0.8",
17
19
  "npmlog": "^7.0.1",
18
20
  "power-tasks": "^1.14.2",
19
21
  "putil-merge": "^3.13.0",
@@ -55,5 +57,6 @@
55
57
  "repository",
56
58
  "build",
57
59
  "lerna"
58
- ]
60
+ ],
61
+ "packageManager": "yarn@3.4.1+sha512.a333bf9c2557d71f5ba87dd1446cde24bb9c35301ec29dd2baba577b3f9d1ab61348b66a1324de9088c40a996c5b9203794da8bf14cab9fae6a6eccc00cb8078"
59
62
  }
@@ -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
+ }