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,80 @@
1
+ import { RunService } from '../services/run.service.js';
2
+ import { applyBranchGuardOptions, assertAllowedBranch, readBranchGuardOptions } from '../utils/branch-guard.js';
3
+ import { applyPackageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
4
+ export function applyRunOptions(cmd) {
5
+ return applyBranchGuardOptions(applyPackageFilterOptions(cmd))
6
+ .option('parallel', {
7
+ describe: 'Max packages to build at once: omit/true for CPU count (or .rmanrc run.<script>.concurrency), ' +
8
+ 'a number for that many, false to run serially. Packages always build in dependency order.',
9
+ coerce: (v) => {
10
+ if (v === undefined)
11
+ return undefined;
12
+ if (v === 'false' || v === false)
13
+ return false;
14
+ if (v === 'true' || v === true)
15
+ return true;
16
+ const n = Number(v);
17
+ return Number.isFinite(n) ? n : true;
18
+ },
19
+ })
20
+ .option('bail', {
21
+ describe: 'Stop on first failure (default: true, overridable per-package via .rmanrc run.<script>.bail)',
22
+ type: 'boolean',
23
+ })
24
+ .option('topo', {
25
+ describe: 'Respect package dependency order: a package waits for its dependencies and is skipped if one ' +
26
+ 'fails. Set false for independent scripts (e.g. lint/test) - order becomes alphabetical and ' +
27
+ 'failures never skip other packages. Default: true, overridable per-package via .rmanrc run.<script>.topo.',
28
+ type: 'boolean',
29
+ })
30
+ .option('progress', {
31
+ describe: 'Show a live progress panel (default: true; auto-disabled when not a TTY; .rmanrc run.<script>.progress)',
32
+ type: 'boolean',
33
+ })
34
+ .option('changed', {
35
+ alias: 'c',
36
+ describe: 'Only run in packages that have changed since the last publish',
37
+ type: 'boolean',
38
+ })
39
+ .option('changed-since', {
40
+ describe: 'Only run in packages that have changed since the given git commit/hash',
41
+ type: 'string',
42
+ })
43
+ .option('root', {
44
+ alias: 'r',
45
+ describe: 'Run across the whole repository even when the current directory is inside a single ' +
46
+ 'package (which otherwise scopes the run to just that package). No effect elsewhere.',
47
+ type: 'boolean',
48
+ })
49
+ .conflicts('changed', 'changed-since');
50
+ }
51
+ export function readRunOptions(args) {
52
+ return {
53
+ ...readPackageFilterOptions(args),
54
+ parallel: args.parallel,
55
+ topo: args.topo,
56
+ bail: args.bail,
57
+ progress: args.progress,
58
+ changed: args.changed,
59
+ changedSince: args.changedSince,
60
+ logLevel: args.logLevel,
61
+ root: args.root,
62
+ };
63
+ }
64
+ export function initCli(repository, program) {
65
+ program.command({
66
+ command: 'run <script>',
67
+ describe: 'Run an npm script in each package',
68
+ builder: cmd => applyRunOptions(cmd)
69
+ .example('$0 run build', '')
70
+ .example('$0 run build --changed', '# Only in packages changed since the last publish')
71
+ .positional('script', {
72
+ describe: 'The script to run',
73
+ type: 'string',
74
+ }),
75
+ handler: async (args) => {
76
+ await assertAllowedBranch(repository, readBranchGuardOptions(args));
77
+ await RunService.runScript(repository, args.script, { ...readRunOptions(args), commandName: 'run' });
78
+ },
79
+ });
80
+ }
@@ -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,14 @@
1
+ import { RunService } from '../services/run.service.js';
2
+ import { assertAllowedBranch, readBranchGuardOptions } from '../utils/branch-guard.js';
3
+ import { applyRunOptions, readRunOptions } from './run.command.js';
4
+ export function initCli(repository, program) {
5
+ program.command({
6
+ command: 'test',
7
+ describe: 'Alias for "run test"',
8
+ builder: cmd => applyRunOptions(cmd).example('$0 test', '# Tests packages'),
9
+ handler: async (args) => {
10
+ await assertAllowedBranch(repository, readBranchGuardOptions(args));
11
+ await RunService.runScript(repository, 'test', { ...readRunOptions(args), commandName: 'test' });
12
+ },
13
+ });
14
+ }
@@ -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.2';
@@ -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.2",
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",
@@ -33,7 +35,8 @@
33
35
  "types": "./index.d.ts",
34
36
  "default": "./index.js"
35
37
  },
36
- "./package.json": "./package.json"
38
+ "./package.json": "./package.json",
39
+ "./rmanrc.schema.json": "./rmanrc.schema.json"
37
40
  },
38
41
  "bin": {
39
42
  "rman": "cli.js"
@@ -55,5 +58,6 @@
55
58
  "repository",
56
59
  "build",
57
60
  "lerna"
58
- ]
61
+ ],
62
+ "packageManager": "yarn@3.4.1+sha512.a333bf9c2557d71f5ba87dd1446cde24bb9c35301ec29dd2baba577b3f9d1ab61348b66a1324de9088c40a996c5b9203794da8bf14cab9fae6a6eccc00cb8078"
59
63
  }