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.
- package/README.md +364 -21
- package/cli.js +51 -29
- package/commands/build.command.d.ts +3 -0
- package/commands/build.command.js +14 -0
- package/commands/changed.command.d.ts +3 -0
- package/commands/changed.command.js +29 -0
- package/commands/changelog.command.d.ts +3 -0
- package/commands/changelog.command.js +63 -0
- package/commands/ci.command.d.ts +3 -0
- package/commands/ci.command.js +30 -0
- package/commands/clean.command.d.ts +3 -0
- package/commands/clean.command.js +36 -0
- package/commands/diff.command.d.ts +3 -0
- package/commands/diff.command.js +51 -0
- package/commands/exec.command.d.ts +3 -0
- package/commands/exec.command.js +83 -0
- package/commands/import.command.d.ts +3 -0
- package/commands/import.command.js +27 -0
- package/commands/info.command.d.ts +3 -0
- package/commands/info.command.js +61 -0
- package/commands/list.command.d.ts +3 -0
- package/commands/list.command.js +113 -0
- package/commands/publish.command.d.ts +3 -0
- package/commands/publish.command.js +147 -0
- package/commands/run.command.d.ts +6 -0
- package/commands/run.command.js +80 -0
- package/commands/test.command.d.ts +3 -0
- package/commands/test.command.js +14 -0
- package/commands/version.command.d.ts +3 -0
- package/commands/version.command.js +125 -0
- package/constants.js +1 -1
- package/core/config.d.ts +13 -0
- package/core/config.js +61 -0
- package/core/package.d.ts +2 -0
- package/core/package.js +2 -0
- package/core/repository.d.ts +36 -3
- package/core/repository.js +90 -45
- package/index.d.ts +17 -1
- package/index.js +15 -1
- package/package.json +6 -3
- package/services/changelog.service.d.ts +99 -0
- package/services/changelog.service.js +272 -0
- package/services/ci.service.d.ts +40 -0
- package/services/ci.service.js +204 -0
- package/services/clean.service.d.ts +42 -0
- package/services/clean.service.js +226 -0
- package/services/exec.service.d.ts +35 -0
- package/services/exec.service.js +114 -0
- package/services/import.service.d.ts +30 -0
- package/services/import.service.js +84 -0
- package/services/list.service.d.ts +27 -0
- package/services/list.service.js +25 -0
- package/services/publish.service.d.ts +79 -0
- package/services/publish.service.js +199 -0
- package/services/run.service.d.ts +99 -0
- package/services/run.service.js +432 -0
- package/services/system-info.d.ts +16 -0
- package/services/system-info.js +25 -0
- package/services/version.service.d.ts +95 -0
- package/services/version.service.js +450 -0
- package/services.d.ts +10 -0
- package/services.js +10 -0
- package/utils/branch-guard.d.ts +25 -0
- package/utils/branch-guard.js +61 -0
- package/utils/change-hash.d.ts +55 -0
- package/utils/change-hash.js +80 -0
- package/utils/conventional-commits.d.ts +39 -0
- package/utils/conventional-commits.js +43 -0
- package/utils/exec.d.ts +5 -6
- package/utils/exec.js +45 -59
- package/utils/git.d.ts +86 -0
- package/utils/git.js +276 -0
- package/utils/live-region.d.ts +16 -0
- package/utils/live-region.js +59 -0
- package/utils/logger.d.ts +24 -0
- package/utils/logger.js +39 -0
- package/utils/npm-run-path.d.ts +1 -1
- package/utils/npm-run-path.js +10 -10
- package/utils/package-filter.d.ts +32 -0
- package/utils/package-filter.js +73 -0
- package/utils/progress-panel.d.ts +59 -0
- package/utils/progress-panel.js +149 -0
- package/utils/workspace-range.d.ts +17 -0
- package/utils/workspace-range.js +28 -0
- package/commands/build-command.d.ts +0 -11
- package/commands/build-command.js +0 -26
- package/commands/changed-command.d.ts +0 -16
- package/commands/changed-command.js +0 -35
- package/commands/ci-command.d.ts +0 -24
- package/commands/ci-command.js +0 -71
- package/commands/execute-command.d.ts +0 -19
- package/commands/execute-command.js +0 -75
- package/commands/info-command.d.ts +0 -10
- package/commands/info-command.js +0 -57
- package/commands/list-command.d.ts +0 -38
- package/commands/list-command.js +0 -137
- package/commands/multi-task-command.d.ts +0 -22
- package/commands/multi-task-command.js +0 -68
- package/commands/publish-command.d.ts +0 -24
- package/commands/publish-command.js +0 -135
- package/commands/run-command.d.ts +0 -28
- package/commands/run-command.js +0 -134
- package/commands/version-command.d.ts +0 -25
- package/commands/version-command.js +0 -198
- package/core/command.d.ts +0 -35
- package/core/command.js +0 -117
- package/core/constants.d.ts +0 -1
- package/core/constants.js +0 -2
- package/core/logger.d.ts +0 -12
- package/core/logger.js +0 -2
- package/utils/file-utils.d.ts +0 -4
- package/utils/file-utils.js +0 -29
- package/utils/git-utils.d.ts +0 -25
- package/utils/git-utils.js +0 -66
- package/utils/npm-utils.d.ts +0 -11
- package/utils/npm-utils.js +0 -33
- package/utils/package-not-found-error.d.ts +0 -2
- package/utils/package-not-found-error.js +0 -2
|
@@ -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.
|
|
1
|
+
export const version = '1.0.1';
|
package/core/config.d.ts
ADDED
|
@@ -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
package/core/repository.d.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
50
|
+
}
|
|
51
|
+
export declare namespace Repository {
|
|
52
|
+
type PackageStatus = 'dirty' | 'committed' | 'changed' | 'clean';
|
|
20
53
|
}
|
package/core/repository.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|