relacher 0.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/src/types.ts ADDED
@@ -0,0 +1,114 @@
1
+ import type { VcsProvider } from './vcs';
2
+
3
+ export type BumpSize = "major" | "minor" | "patch" | "skip";
4
+
5
+ export interface Commit {
6
+ hash: string;
7
+ shortHash: string;
8
+ author: string;
9
+ date: string;
10
+ message: string; // The full commit message
11
+ type: string; // e.g., 'feat', 'fix', 'chore'
12
+ scope: string | null; // e.g., 'ui', 'api'
13
+ isBreaking: boolean; // true if contains '!' or 'BREAKING CHANGE'
14
+ description: string; // The text after the colon
15
+ }
16
+
17
+ export interface ChangelogContext {
18
+ version: string;
19
+ date: string;
20
+ commits: Commit[];
21
+ }
22
+
23
+ export interface ChangelogUpdate {
24
+ kind: "changelog";
25
+ path: string;
26
+ global?: boolean;
27
+ template?: (ctx: ChangelogContext) => string;
28
+ resolvedBlock?: string;
29
+ }
30
+
31
+ export interface TomlUpdate {
32
+ kind: "toml";
33
+ path: string;
34
+ toml: string;
35
+ }
36
+
37
+ export interface RegexUpdate {
38
+ kind: "regex";
39
+ path: string;
40
+ search: string;
41
+ replace: string;
42
+ resolvedReplace?: string;
43
+ }
44
+
45
+ export type UpdateAction = TomlUpdate | RegexUpdate | ChangelogUpdate;
46
+
47
+ export interface DependencyConfig {
48
+ name: string;
49
+ watch?: string[];
50
+ updates?: UpdateAction[];
51
+ depends?: string[];
52
+ }
53
+
54
+ export interface PatternConfig {
55
+ pattern: string;
56
+ }
57
+
58
+ export interface SizePatterns {
59
+ major: PatternConfig;
60
+ minor: PatternConfig;
61
+ patch: PatternConfig;
62
+ skip: PatternConfig;
63
+ }
64
+
65
+ export interface CascadeRules {
66
+ skip?: Record<BumpSize, BumpSize>;
67
+ patch?: Record<BumpSize, BumpSize>;
68
+ minor?: Record<BumpSize, BumpSize>;
69
+ major?: Record<BumpSize, BumpSize>;
70
+ }
71
+
72
+ export interface PrepareOptions {
73
+ sizes?: SizePatterns;
74
+ cascade?: CascadeRules;
75
+ cwd?: string;
76
+ }
77
+
78
+ export interface DependencyUpdateReport {
79
+ name: string;
80
+ currentVersion: string;
81
+ newVersion: string;
82
+ bump: BumpSize;
83
+ originalBump?: BumpSize;
84
+ commits: Commit[];
85
+ updates: UpdateAction[];
86
+ depends?: string[];
87
+ }
88
+
89
+ export interface IntermediateReport {
90
+ name: string;
91
+ currentVersion: string;
92
+ newVersion: string;
93
+ bump: BumpSize;
94
+ originalBump?: BumpSize;
95
+ commits: Commit[];
96
+ updates: UpdateAction[];
97
+ depends: string[];
98
+ }
99
+
100
+ export const defaultSizes: SizePatterns = {
101
+ major: { pattern: "^[a-z]+(?:\\([^)]+\\))?!:|BREAKING CHANGE" },
102
+ minor: { pattern: "^feat|^revert" },
103
+ patch: { pattern: "^fix|^build|^refactor|^nit|^style" },
104
+ skip: { pattern: "^release|^chore|^infra|^docs|^test|^ci|^build" },
105
+ };
106
+
107
+ export const defaultCascadeRules: Required<CascadeRules> = {
108
+ skip: { skip: 'skip', patch: 'patch', minor: 'patch', major: 'patch' },
109
+ patch: { skip: 'patch', patch: 'patch', minor: 'minor', major: 'minor' },
110
+ minor: { skip: 'minor', patch: 'minor', minor: 'minor', major: 'minor' },
111
+ major: { skip: 'major', patch: 'major', minor: 'major', major: 'major' },
112
+ };
113
+
114
+ export type { VcsProvider };
package/src/vcs/git.ts ADDED
@@ -0,0 +1,112 @@
1
+ import { execSync } from 'node:child_process';
2
+
3
+ import type { Commit, VcsProvider } from '../types';
4
+
5
+ export function runGit(cmd: string, cwd: string): string {
6
+ try {
7
+ return execSync(cmd, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
8
+ } catch {
9
+ return '';
10
+ }
11
+ }
12
+
13
+ function parseSemver(tag: string, prefix: string): number[] {
14
+ return tag.slice(prefix.length).split('.').map(Number);
15
+ }
16
+
17
+ export function findLatestTag(tags: string[], prefix: string): string | null {
18
+ const matching = tags.filter((t) => t.startsWith(prefix));
19
+ if (matching.length === 0) return null;
20
+ return (
21
+ matching.sort((a, b) => {
22
+ const verA = parseSemver(a, prefix);
23
+ const verB = parseSemver(b, prefix);
24
+ for (let i = 0; i < 3; i++) {
25
+ const diff = (verB[i] ?? 0) - (verA[i] ?? 0);
26
+ if (diff !== 0) return diff;
27
+ }
28
+ return 0;
29
+ })[0] ?? null
30
+ );
31
+ }
32
+
33
+ export function resolveGitTag(name: string, allTags: string[]): string | null {
34
+ return findLatestTag(allTags, `${name}-v`) || findLatestTag(allTags, 'v');
35
+ }
36
+
37
+ export function getGitCommits(
38
+ name: string,
39
+ watch: string[],
40
+ allTags: string[],
41
+ cwd: string,
42
+ ): Commit[] {
43
+ const lastTag = resolveGitTag(name, allTags);
44
+ const watchPaths = watch.join(' ');
45
+ const range = lastTag ? `${lastTag}..HEAD` : 'HEAD';
46
+
47
+ // Using shell-safe single quotes and disabling signature lines to prevent parsing corruption
48
+ const gitCmd = watchPaths
49
+ ? `git log ${range} --no-show-signature --format='%H|%an|%ad|%s' --date=short -- ${watchPaths}`
50
+ : `git log ${range} --no-show-signature --format='%H|%an|%ad|%s' --date=short`;
51
+
52
+ return runGit(gitCmd, cwd)
53
+ .split('\n')
54
+ .filter(Boolean)
55
+ .map((line) => {
56
+ const parts = line.split('|');
57
+ const hash = parts[0] ?? '';
58
+ const author = parts[1] ?? '';
59
+ const date = parts[2] ?? '';
60
+ const msgParts = parts.slice(3);
61
+ const message = msgParts.join('|').trim();
62
+
63
+ const ccMatch = message.match(/^([a-zA-Z]+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/);
64
+
65
+ const type = ccMatch ? (ccMatch[1] ?? 'other') : 'other';
66
+ const scope = ccMatch ? (ccMatch[2] ?? null) : null;
67
+ const isBreaking = (ccMatch && !!ccMatch[3]) || message.includes('BREAKING CHANGE');
68
+ const description = ccMatch ? (ccMatch[4] ?? message) : message;
69
+
70
+ return {
71
+ hash,
72
+ shortHash: hash.slice(0, 7),
73
+ author,
74
+ date,
75
+ message,
76
+ type,
77
+ scope,
78
+ isBreaking,
79
+ description,
80
+ };
81
+ });
82
+ }
83
+
84
+ export class GitVcsProvider implements VcsProvider {
85
+ private allTags: string[] | null = null;
86
+
87
+ constructor(private cwd: string) { }
88
+
89
+ private getAllTags(): string[] {
90
+ if (this.allTags === null) {
91
+ this.allTags = runGit('git tag', this.cwd)
92
+ .split('\n')
93
+ .map((t) => t.trim()) // Strips carriage returns (\r) and trailing whitespaces
94
+ .filter(Boolean);
95
+ }
96
+ return this.allTags;
97
+ }
98
+
99
+ async getCommits(name: string, watch: string[]): Promise<Commit[]> {
100
+ const tags = this.getAllTags();
101
+ return getGitCommits(name, watch, tags, this.cwd);
102
+ }
103
+
104
+ async commit(message: string): Promise<void> {
105
+ runGit('git add .', this.cwd);
106
+ runGit(`git commit -m "${message}"`, this.cwd);
107
+ }
108
+
109
+ async tag(tagName: string): Promise<void> {
110
+ runGit(`git tag ${tagName}`, this.cwd);
111
+ }
112
+ }
@@ -0,0 +1,10 @@
1
+ import type { Commit } from '../types';
2
+
3
+ export interface VcsProvider {
4
+ getCommits(name: string, watch: string[]): Promise<Commit[]>;
5
+ commit(message: string): Promise<void>;
6
+ tag(tagName: string): Promise<void>;
7
+ }
8
+
9
+ export * from './git';
10
+ export * from './jj';
package/src/vcs/jj.ts ADDED
@@ -0,0 +1,127 @@
1
+ import { execSync } from 'node:child_process';
2
+
3
+ import type { Commit, VcsProvider } from '../types';
4
+
5
+ export function runJj(cmd: string, cwd: string): string {
6
+ try {
7
+ return execSync(`jj --color=never ${cmd}`, {
8
+ cwd,
9
+ encoding: 'utf8',
10
+ stdio: ['ignore', 'pipe', 'inherit'],
11
+ }).trim();
12
+ } catch {
13
+ return '';
14
+ }
15
+ }
16
+
17
+ function parseSemver(tag: string, prefix: string): number[] {
18
+ return tag.slice(prefix.length).split('.').map(Number);
19
+ }
20
+
21
+ export function findLatestJjTag(tags: string[], prefix: string): string | null {
22
+ const matching = tags.filter((t) => t.startsWith(prefix));
23
+ if (matching.length === 0) return null;
24
+ return (
25
+ matching.sort((a, b) => {
26
+ const verA = parseSemver(a, prefix);
27
+ const verB = parseSemver(b, prefix);
28
+ for (let i = 0; i < 3; i++) {
29
+ const diff = (verB[i] ?? 0) - (verA[i] ?? 0);
30
+ if (diff !== 0) return diff;
31
+ }
32
+ return 0;
33
+ })[0] ?? null
34
+ );
35
+ }
36
+
37
+ export function resolveJjTag(name: string, allTags: string[]): string | null {
38
+ return findLatestJjTag(allTags, `${name}-v`) || findLatestJjTag(allTags, 'v');
39
+ }
40
+
41
+ export function getJjCommits(
42
+ name: string,
43
+ watch: string[],
44
+ allTags: string[],
45
+ cwd: string,
46
+ ): Commit[] {
47
+ const lastTag = resolveJjTag(name, allTags);
48
+
49
+ // FIXED: Double quotes explicitly escape the literal tag name for the jj revset engine
50
+ const range = lastTag ? `"${lastTag}"..@ & ~root()` : `::@ & ~root()`;
51
+
52
+ // Calling `.short(40)` to cast the CommitId into a 40-character hex String for template concatenation
53
+ const template =
54
+ 'commit_id.short(40) ++ "|" ++ author.name() ++ "|" ++ author.timestamp().format("%Y-%m-%d") ++ "|" ++ description.first_line() ++ "\\n"';
55
+
56
+ const watchPaths = watch.join(' ');
57
+ // Appending --color=never to strip ANSI escape codes so hashes can be parsed cleanly
58
+ const jjCmd = watchPaths
59
+ ? `log --no-graph -r '${range}' -T '${template}' -- ${watchPaths}`
60
+ : `log --no-graph -r '${range}' -T '${template}'`;
61
+
62
+ return runJj(jjCmd, cwd)
63
+ .split('\n')
64
+ .filter(Boolean)
65
+ .map((line) => {
66
+ const parts = line.split('|');
67
+ const hash = parts[0] ?? '';
68
+ const author = parts[1] ?? '';
69
+ const date = parts[2] ?? '';
70
+ const msgParts = parts.slice(3);
71
+ const message = msgParts.join('|').trim();
72
+
73
+ const ccMatch = message.match(/^([a-zA-Z]+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/);
74
+
75
+ const type = ccMatch ? (ccMatch[1] ?? 'other') : 'other';
76
+ const scope = ccMatch ? (ccMatch[2] ?? null) : null;
77
+ const isBreaking = (ccMatch && !!ccMatch[3]) || message.includes('BREAKING CHANGE');
78
+ const description = ccMatch ? (ccMatch[4] ?? message) : message;
79
+
80
+ return {
81
+ hash,
82
+ shortHash: hash.slice(0, 7),
83
+ author,
84
+ date,
85
+ message,
86
+ type,
87
+ scope,
88
+ isBreaking,
89
+ description,
90
+ };
91
+ });
92
+ }
93
+
94
+ export class JjVcsProvider implements VcsProvider {
95
+ private allTags: string[] | null = null;
96
+
97
+ constructor(private cwd: string) { }
98
+
99
+ private getAllTags(): string[] {
100
+ if (this.allTags === null) {
101
+ // Must enforce --color=never to prevent ANSI escape codes from failing the startsWith prefix checks
102
+ const raw = runJj('tag list', this.cwd);
103
+ this.allTags = raw
104
+ .split('\n')
105
+ .map((line) => {
106
+ const parts = line.split(':');
107
+ return (parts[0] ?? '').trim();
108
+ })
109
+ .filter(Boolean);
110
+ }
111
+ return this.allTags;
112
+ }
113
+
114
+ async getCommits(name: string, watch: string[]): Promise<Commit[]> {
115
+ const tags = this.getAllTags();
116
+ return getJjCommits(name, watch, tags, this.cwd);
117
+ }
118
+
119
+ async commit(message: string): Promise<void> {
120
+ runJj(`commit -m "${message}"`, this.cwd);
121
+ }
122
+
123
+ async tag(tagName: string): Promise<void> {
124
+ // Appending --allow-move to update the tag reference cleanly if it already exists
125
+ runJj(`tag set "${tagName}" -r @- --allow-move`, this.cwd);
126
+ }
127
+ }