@reelout/core 0.1.1 → 0.2.0
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/dist/analyzer.js +38 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/builder.js +52 -0
- package/dist/builder.js.map +1 -0
- package/dist/changelog.js +58 -0
- package/dist/changelog.js.map +1 -0
- package/dist/formatError-BXNdyRfu.js +23 -0
- package/dist/formatError-BXNdyRfu.js.map +1 -0
- package/dist/index.js +175 -1
- package/dist/index.js.map +1 -0
- package/dist/types-la_KkjCS.js +1 -0
- package/dist/types.js +2 -0
- package/dist/workflow-BoVmbvH6.js +2342 -0
- package/dist/workflow-BoVmbvH6.js.map +1 -0
- package/package.json +27 -3
- package/src/analyzer.ts +63 -0
- package/src/builder.ts +104 -0
- package/src/cache/TTLCache.ts +100 -0
- package/src/cache/index.ts +4 -0
- package/src/changelog.ts +97 -0
- package/src/config/const.ts +97 -0
- package/src/config/index.ts +11 -0
- package/src/config/isSemVerGitTag.ts +25 -0
- package/src/config/normalizeCommitFilter.ts +37 -0
- package/src/config/normalizeReleaseRule.ts +42 -0
- package/src/config/normalizeWorkspace.ts +34 -0
- package/src/config/resolvePlugin.ts +56 -0
- package/src/config/resolveReleaseTarget.ts +71 -0
- package/src/config/sanitizePrereleaseLabel.ts +18 -0
- package/src/git/branch/getCurrentGitBranch.ts +18 -0
- package/src/git/branch/index.ts +4 -0
- package/src/git/commit/createGitCommit.ts +29 -0
- package/src/git/commit/extractMentions.ts +34 -0
- package/src/git/commit/getCurrentGitCommit.ts +14 -0
- package/src/git/commit/getParsedCommit.ts +53 -0
- package/src/git/commit/index.ts +9 -0
- package/src/git/commit/parseCommit.ts +99 -0
- package/src/git/commit/parseGitPrettyFormat.ts +76 -0
- package/src/git/history/countGitHistory.ts +28 -0
- package/src/git/history/expandGitHistory.ts +108 -0
- package/src/git/history/index.ts +9 -0
- package/src/git/history/mergeGitHistory.ts +76 -0
- package/src/git/history/restoreFullGitHistory.ts +19 -0
- package/src/git/history/showGitHistory.ts +80 -0
- package/src/git/history/showParsedGitHistory.ts +30 -0
- package/src/git/index.ts +9 -0
- package/src/git/repository/ensureGitUserConfig.ts +31 -0
- package/src/git/repository/getGitConfigValue.ts +27 -0
- package/src/git/repository/getGitRemote.ts +20 -0
- package/src/git/repository/index.ts +10 -0
- package/src/git/repository/isGitShallowRepository.ts +21 -0
- package/src/git/repository/pushGitChanges.ts +25 -0
- package/src/git/repository/resolveGitRepository.ts +40 -0
- package/src/git/repository/withShallowLock.ts +62 -0
- package/src/git/revision/detectGitRelationship.ts +43 -0
- package/src/git/revision/index.ts +7 -0
- package/src/git/revision/isGitAncestor.ts +33 -0
- package/src/git/revision/readGitFile.ts +29 -0
- package/src/git/revision/resolveGitRevision.ts +23 -0
- package/src/git/tag/createGitTag.ts +25 -0
- package/src/git/tag/deleteGitTag.ts +17 -0
- package/src/git/tag/ensureGitTag.ts +63 -0
- package/src/git/tag/generateGitTagName.ts +43 -0
- package/src/git/tag/getCurrentGitTag.ts +23 -0
- package/src/git/tag/getGitTagMessage.ts +35 -0
- package/src/git/tag/getLatestGitTag.ts +22 -0
- package/src/git/tag/index.ts +13 -0
- package/src/git/tag/isGitTagConnected.ts +33 -0
- package/src/git/tag/isGitTagPointed.ts +38 -0
- package/src/git/tag/isGitTagPresent.ts +17 -0
- package/src/index.ts +8 -2
- package/src/types.ts +217 -0
- package/src/util/buildTrailerPattern.ts +20 -0
- package/src/util/compact.ts +14 -0
- package/src/util/execCommand.ts +130 -0
- package/src/util/execGit.ts +18 -0
- package/src/util/formatError.ts +21 -0
- package/src/util/formatFile.ts +41 -0
- package/src/util/index.ts +16 -0
- package/src/util/interpolate.ts +141 -0
- package/src/util/isCI.ts +23 -0
- package/src/util/isDefined.ts +12 -0
- package/src/util/isMatch.ts +43 -0
- package/src/util/readJson.ts +47 -0
- package/src/util/unique.ts +26 -0
- package/src/util/writeJson.ts +28 -0
- package/src/workflow/analysis/analyzePackageChanges.ts +54 -0
- package/src/workflow/analysis/index.ts +9 -0
- package/src/workflow/analysis/isCommitForPackage.ts +23 -0
- package/src/workflow/analysis/promotePrereleaseVersions.ts +50 -0
- package/src/workflow/analysis/propagateDependencyUpdates.ts +71 -0
- package/src/workflow/analysis/sliceUnreleased.ts +36 -0
- package/src/workflow/analysis/streamPackageCommits.ts +34 -0
- package/src/workflow/changelog/buildChangelog.ts +79 -0
- package/src/workflow/changelog/buildReleaseNotes.ts +50 -0
- package/src/workflow/changelog/extractLatestVersion.ts +22 -0
- package/src/workflow/changelog/extractPlainText.ts +38 -0
- package/src/workflow/changelog/extractPreviousVersion.ts +77 -0
- package/src/workflow/changelog/extractSemVer.ts +33 -0
- package/src/workflow/changelog/index.ts +12 -0
- package/src/workflow/changelog/iterateChangelogVersions.ts +70 -0
- package/src/workflow/changelog/prependChangelog.ts +59 -0
- package/src/workflow/changelog/writeChangelog.ts +27 -0
- package/src/workflow/committing/generateReleaseCommitMessage.ts +96 -0
- package/src/workflow/committing/index.ts +5 -0
- package/src/workflow/committing/isReleaseCommit.ts +52 -0
- package/src/workflow/discovery/buildDependencyGraph.ts +43 -0
- package/src/workflow/discovery/discoverPackageTag.ts +98 -0
- package/src/workflow/discovery/index.ts +9 -0
- package/src/workflow/discovery/resolvePackageStableTag.ts +88 -0
- package/src/workflow/discovery/resolvePackageTag.ts +45 -0
- package/src/workflow/discovery/sortPackages.ts +73 -0
- package/src/workflow/discovery/verifyPackageTag.ts +43 -0
- package/src/workflow/index.ts +10 -0
- package/src/workflow/report/generateReleaseReport.ts +76 -0
- package/src/workflow/report/index.ts +5 -0
- package/src/workflow/report/printReleaseReport.ts +97 -0
- package/src/workflow/runner/index.ts +4 -0
- package/src/workflow/runner/runWorkflow.ts +120 -0
- package/src/workflow/versioning/bumpVersion.ts +67 -0
- package/src/workflow/versioning/deriveReleaseType.ts +33 -0
- package/src/workflow/versioning/determineReleaseType.ts +35 -0
- package/src/workflow/versioning/index.ts +8 -0
- package/src/workflow/versioning/isStableVersion.ts +31 -0
- package/src/workflow/versioning/pickPriorityReleaseType.ts +27 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import type { ConsolaInstance } from 'consola/core'
|
|
5
|
+
import type { CommitBase, CommitNote } from 'conventional-commits-parser'
|
|
6
|
+
import type { Hosts } from 'hosted-git-info'
|
|
7
|
+
|
|
8
|
+
export type CommitType =
|
|
9
|
+
| 'build'
|
|
10
|
+
| 'chore'
|
|
11
|
+
| 'ci'
|
|
12
|
+
| 'docs'
|
|
13
|
+
| 'feat'
|
|
14
|
+
| 'fix'
|
|
15
|
+
| 'perf'
|
|
16
|
+
| 'refactor'
|
|
17
|
+
| 'revert'
|
|
18
|
+
| 'style'
|
|
19
|
+
| 'test'
|
|
20
|
+
| (string & {})
|
|
21
|
+
|
|
22
|
+
export type { CommitNote }
|
|
23
|
+
|
|
24
|
+
export interface CommitTrailer {
|
|
25
|
+
key: string
|
|
26
|
+
value: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ParsedCommit extends CommitBase {
|
|
30
|
+
type: CommitType | null
|
|
31
|
+
scope: string | null
|
|
32
|
+
subject: string
|
|
33
|
+
header: string
|
|
34
|
+
body: string | null
|
|
35
|
+
footer: string | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Commit extends CommitBase {
|
|
39
|
+
hash: string
|
|
40
|
+
parent: string[]
|
|
41
|
+
authorDate: string
|
|
42
|
+
committerDate: string
|
|
43
|
+
type: CommitType | null
|
|
44
|
+
scope: string | null
|
|
45
|
+
subject: string | null
|
|
46
|
+
header: string
|
|
47
|
+
body: string | null
|
|
48
|
+
footer: string | null
|
|
49
|
+
trailers: CommitTrailer[]
|
|
50
|
+
files: readonly string[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface GitRepository {
|
|
54
|
+
host: Hosts
|
|
55
|
+
owner: string
|
|
56
|
+
repo: string
|
|
57
|
+
url: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface GitAuthor {
|
|
61
|
+
name: string
|
|
62
|
+
email: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type CommitClassifier = (
|
|
66
|
+
commit: Commit
|
|
67
|
+
) => ReleaseType | null | undefined
|
|
68
|
+
|
|
69
|
+
export type ReleaseType = 'major' | 'minor' | 'patch'
|
|
70
|
+
|
|
71
|
+
export type ReleaseRuleType = 'branch'
|
|
72
|
+
|
|
73
|
+
export interface ReleaseRule {
|
|
74
|
+
type: ReleaseRuleType
|
|
75
|
+
pattern: string
|
|
76
|
+
prereleaseLabel: string | undefined
|
|
77
|
+
releaseChannel: string | undefined
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ReleaseConfig {
|
|
81
|
+
author?: Partial<GitAuthor>
|
|
82
|
+
classifier?: CommitClassifier | Partial<Record<ReleaseType, CommitType[]>>
|
|
83
|
+
rules?: Record<string, boolean | string | Partial<ReleaseRule>>
|
|
84
|
+
plugins?: (
|
|
85
|
+
[string, unknown?] | string | ReleasePlugin | false | null | undefined
|
|
86
|
+
)[]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ResolvedReleaseConfig {
|
|
90
|
+
author: GitAuthor
|
|
91
|
+
classifier: CommitClassifier
|
|
92
|
+
rules: ReleaseRule[]
|
|
93
|
+
plugins: ReleasePlugin[]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ReleaseContext extends ResolvedReleaseConfig {
|
|
97
|
+
cwd: string
|
|
98
|
+
dryRun: boolean
|
|
99
|
+
today: string
|
|
100
|
+
logger: ConsolaInstance
|
|
101
|
+
repository: GitRepository | null
|
|
102
|
+
commit: string
|
|
103
|
+
ref: { name: string; type: ReleaseRuleType }
|
|
104
|
+
promote: boolean | undefined
|
|
105
|
+
prereleaseLabel: string | undefined
|
|
106
|
+
releaseChannel: string | undefined
|
|
107
|
+
packages: Package[]
|
|
108
|
+
plans: ReleasePlan[]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface Workspace {
|
|
112
|
+
entry: string
|
|
113
|
+
members: readonly string[] | null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface Package {
|
|
117
|
+
root: boolean
|
|
118
|
+
entry: boolean
|
|
119
|
+
dir: string
|
|
120
|
+
files?: string | string[] | undefined
|
|
121
|
+
changelogFile?: string | undefined
|
|
122
|
+
tag?: Tag | null
|
|
123
|
+
name: string | undefined
|
|
124
|
+
version: string | undefined
|
|
125
|
+
private: boolean | undefined
|
|
126
|
+
dependencies: string[]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface Tag {
|
|
130
|
+
name: string
|
|
131
|
+
message?: string | undefined
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface Release {
|
|
135
|
+
type: ReleaseType
|
|
136
|
+
version?: string | null
|
|
137
|
+
tag?: Tag | null
|
|
138
|
+
changelog?: string | null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface ReleasePlan {
|
|
142
|
+
package: Package
|
|
143
|
+
history: Commit[]
|
|
144
|
+
release: Release | null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface ReleasePlugin {
|
|
148
|
+
name: string
|
|
149
|
+
version: string
|
|
150
|
+
configure?: ReleasePluginHook
|
|
151
|
+
discover?: ReleasePluginHook
|
|
152
|
+
analyze?: ReleasePluginHook
|
|
153
|
+
build?: ReleasePluginHook
|
|
154
|
+
write?: ReleasePluginHook
|
|
155
|
+
commit?: ReleasePluginHook
|
|
156
|
+
publish?: ReleasePluginHook
|
|
157
|
+
finalize?: ReleasePluginHook
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type ReleasePluginHook =
|
|
161
|
+
ReleasePluginHookHandler | ReleasePluginHookObject
|
|
162
|
+
|
|
163
|
+
export interface ReleasePluginHookObject {
|
|
164
|
+
enforce?: ReleasePluginHookEnforce | null | undefined
|
|
165
|
+
handler: ReleasePluginHookHandler
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export type ReleasePluginHookEnforce = 'pre' | 'post'
|
|
169
|
+
|
|
170
|
+
export type ReleasePluginHookHandler = (
|
|
171
|
+
context: ReleaseContext
|
|
172
|
+
) =>
|
|
173
|
+
| undefined
|
|
174
|
+
| void
|
|
175
|
+
| ReleasePluginHookRollback
|
|
176
|
+
| Promise<undefined | void | ReleasePluginHookRollback>
|
|
177
|
+
|
|
178
|
+
export type ReleasePluginHookRollback = (
|
|
179
|
+
reason: unknown
|
|
180
|
+
) => void | Promise<void>
|
|
181
|
+
|
|
182
|
+
export type ReleaseLifecycle = Exclude<keyof ReleasePlugin, 'name' | 'version'>
|
|
183
|
+
|
|
184
|
+
export type ReleaseStatus = 'success' | 'failure'
|
|
185
|
+
|
|
186
|
+
export interface ReleaseReport {
|
|
187
|
+
prereleaseLabel: string | undefined
|
|
188
|
+
releaseChannel: string | undefined
|
|
189
|
+
phase: { plugin: string; hook: ReleaseLifecycle } | undefined
|
|
190
|
+
status: ReleaseStatus
|
|
191
|
+
reason: string | null
|
|
192
|
+
plugins: {
|
|
193
|
+
name: string
|
|
194
|
+
version: string
|
|
195
|
+
hooks: string[]
|
|
196
|
+
}[]
|
|
197
|
+
packages: {
|
|
198
|
+
dir: string
|
|
199
|
+
name: string | undefined
|
|
200
|
+
version: string | undefined
|
|
201
|
+
}[]
|
|
202
|
+
plans: {
|
|
203
|
+
package: {
|
|
204
|
+
entry: boolean
|
|
205
|
+
dir: string
|
|
206
|
+
name: string | undefined
|
|
207
|
+
version: string | undefined
|
|
208
|
+
private: boolean
|
|
209
|
+
}
|
|
210
|
+
release: {
|
|
211
|
+
type: ReleaseType
|
|
212
|
+
version: string
|
|
213
|
+
tag: string | null | undefined
|
|
214
|
+
changelog: string | null | undefined
|
|
215
|
+
}
|
|
216
|
+
}[]
|
|
217
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Build a regular expression to match trailer lines using custom markers.
|
|
6
|
+
*
|
|
7
|
+
* Combines the provided marker strings with a standard alphanumeric pattern
|
|
8
|
+
* to construct a regular expression that matches trailer definitions.
|
|
9
|
+
*
|
|
10
|
+
* @param markers Optional array of custom trailer marker strings
|
|
11
|
+
* @returns A regular expression matching trailer lines
|
|
12
|
+
*/
|
|
13
|
+
export function buildTrailerPattern(markers: readonly string[] = []) {
|
|
14
|
+
return new RegExp(
|
|
15
|
+
`^(${markers
|
|
16
|
+
.map(RegExp.escape)
|
|
17
|
+
.concat('[A-Za-z0-9_-]+')
|
|
18
|
+
.join('|')})[ \\t]*:[ \\t]*(.*)$`
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { isDefined } from './isDefined'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Remove `null` and `undefined` values from an array.
|
|
8
|
+
*
|
|
9
|
+
* @param array The array to compact
|
|
10
|
+
* @returns A new array that does not contain `null` or `undefined` values
|
|
11
|
+
*/
|
|
12
|
+
export function compact<T>(array: readonly (T | null | undefined)[]): T[] {
|
|
13
|
+
return array.filter(isDefined)
|
|
14
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
|
5
|
+
|
|
6
|
+
export interface PromiseWithChild<T> extends Promise<T> {
|
|
7
|
+
child: ChildProcessWithoutNullStreams
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let now = new Date('2026-01-01T00:00:00Z').valueOf()
|
|
11
|
+
|
|
12
|
+
const MUTATING_GIT_COMMANDS = new Set([
|
|
13
|
+
'commit',
|
|
14
|
+
'tag',
|
|
15
|
+
'cherry-pick',
|
|
16
|
+
'rebase',
|
|
17
|
+
'merge',
|
|
18
|
+
'revert',
|
|
19
|
+
'commit-tree',
|
|
20
|
+
'mktag',
|
|
21
|
+
])
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Execute a command asynchronously and return its standard output.
|
|
25
|
+
*
|
|
26
|
+
* @param cwd Working directory for the command
|
|
27
|
+
* @param command Binary to execute
|
|
28
|
+
* @param args Command-line arguments
|
|
29
|
+
* @returns Promise resolving to the stdout string
|
|
30
|
+
*/
|
|
31
|
+
export function execCommand(
|
|
32
|
+
cwd: string,
|
|
33
|
+
command: string,
|
|
34
|
+
args: readonly string[]
|
|
35
|
+
): PromiseWithChild<string> {
|
|
36
|
+
const env: NodeJS.ProcessEnv = {
|
|
37
|
+
...process.env,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (import.meta.env.MODE === 'test' && command === 'git') {
|
|
41
|
+
const primaryArg = args.find((arg) => !arg.startsWith('-'))
|
|
42
|
+
|
|
43
|
+
if (primaryArg && MUTATING_GIT_COMMANDS.has(primaryArg)) {
|
|
44
|
+
const date = new Date(now).toISOString()
|
|
45
|
+
env.GIT_AUTHOR_DATE = date
|
|
46
|
+
env.GIT_COMMITTER_DATE = date
|
|
47
|
+
now += 1000
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let childProcess!: ChildProcessWithoutNullStreams
|
|
52
|
+
|
|
53
|
+
const promise = new Promise<string>((resolve, reject) => {
|
|
54
|
+
childProcess = spawn(command, args, {
|
|
55
|
+
cwd,
|
|
56
|
+
env,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
childProcess.stdout.setEncoding('utf-8')
|
|
60
|
+
childProcess.stderr.setEncoding('utf-8')
|
|
61
|
+
|
|
62
|
+
let stdout = ''
|
|
63
|
+
let stderr = ''
|
|
64
|
+
|
|
65
|
+
childProcess.stdout.on('data', (data: string) => {
|
|
66
|
+
stdout += data
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
childProcess.stderr.on('data', (data: string) => {
|
|
70
|
+
stderr += data
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
childProcess.on('error', (err) => {
|
|
74
|
+
reject(err)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
childProcess.on('close', (code, signal) => {
|
|
78
|
+
if (code === 0) {
|
|
79
|
+
return resolve(stdout)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const quotedCommand = quoteCommandForMessage([command, ...args].join(' '))
|
|
83
|
+
|
|
84
|
+
/* istanbul ignore next */
|
|
85
|
+
const exitStatus =
|
|
86
|
+
code !== null
|
|
87
|
+
? `exit code ${code}`
|
|
88
|
+
: signal
|
|
89
|
+
? `signal ${signal}`
|
|
90
|
+
: 'unknown status'
|
|
91
|
+
|
|
92
|
+
const errorMessage = `Command ${quotedCommand} failed (${exitStatus})`
|
|
93
|
+
|
|
94
|
+
const errorDiagnostics = (stderr || stdout).trim()
|
|
95
|
+
|
|
96
|
+
const err = new Error(
|
|
97
|
+
[errorMessage, errorDiagnostics]
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.join(errorDiagnostics.includes('\n') ? '\n' : ': '),
|
|
100
|
+
{
|
|
101
|
+
cause: {
|
|
102
|
+
code,
|
|
103
|
+
signal,
|
|
104
|
+
diagnostics: errorDiagnostics,
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
Object.defineProperty(err, 'code', { value: code })
|
|
110
|
+
|
|
111
|
+
reject(err)
|
|
112
|
+
})
|
|
113
|
+
}) as PromiseWithChild<string>
|
|
114
|
+
|
|
115
|
+
promise.child = childProcess
|
|
116
|
+
|
|
117
|
+
return promise
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function quoteCommandForMessage(command: string): string {
|
|
121
|
+
if (!command.includes("'")) {
|
|
122
|
+
return `'${command}'`
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!command.includes('"')) {
|
|
126
|
+
return `"${command}"`
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return JSON.stringify(command)
|
|
130
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { execCommand, type PromiseWithChild } from './execCommand'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Run a git command in a working directory and return its standard output.
|
|
8
|
+
*
|
|
9
|
+
* @param cwd Working directory for the git command
|
|
10
|
+
* @param args Git CLI arguments
|
|
11
|
+
* @returns Promise resolving to the stdout string
|
|
12
|
+
*/
|
|
13
|
+
export function execGit(
|
|
14
|
+
cwd: string,
|
|
15
|
+
args: readonly string[]
|
|
16
|
+
): PromiseWithChild<string> {
|
|
17
|
+
return execCommand(cwd, 'git', args)
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Normalize an unknown thrown value for logging or display.
|
|
6
|
+
*
|
|
7
|
+
* - If `err` is an `Error`, returns its `message` string.
|
|
8
|
+
* - Otherwise returns the original `err` value unchanged (may be a string,
|
|
9
|
+
* object, number, `null`, or `undefined`).
|
|
10
|
+
*
|
|
11
|
+
* This function intentionally does not coerce non-Error values to strings so
|
|
12
|
+
* callers can decide how to render structured values.
|
|
13
|
+
*
|
|
14
|
+
* @param err The thrown value to normalize
|
|
15
|
+
* @returns The error message when `err` is an `Error`, otherwise the original value
|
|
16
|
+
*/
|
|
17
|
+
export function formatError<T>(err: T): T | string {
|
|
18
|
+
if (Error.isError(err)) return err.message
|
|
19
|
+
if (typeof err === 'string') return err
|
|
20
|
+
return err
|
|
21
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import prettier from 'prettier'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Format `content` for `filePath` using Prettier when possible.
|
|
8
|
+
*
|
|
9
|
+
* Resolves project/editorconfig settings and formats the provided content.
|
|
10
|
+
* Returns the original content on error or when no parser is inferred.
|
|
11
|
+
*
|
|
12
|
+
* @param filePath Path used to infer parser and config
|
|
13
|
+
* @param content Source content to format
|
|
14
|
+
* @returns Formatted content or original content on failure
|
|
15
|
+
*/
|
|
16
|
+
export async function formatFile(
|
|
17
|
+
filePath: string,
|
|
18
|
+
content: string
|
|
19
|
+
): Promise<string> {
|
|
20
|
+
try {
|
|
21
|
+
const info = await prettier.getFileInfo(filePath, {
|
|
22
|
+
resolveConfig: true,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const config = await prettier.resolveConfig(filePath, {
|
|
26
|
+
editorconfig: true,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
if (info.inferredParser == null) {
|
|
30
|
+
return content
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return await prettier.format(content, {
|
|
34
|
+
...config,
|
|
35
|
+
parser: info.inferredParser,
|
|
36
|
+
filepath: filePath,
|
|
37
|
+
})
|
|
38
|
+
} catch {
|
|
39
|
+
return content
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
export * from './buildTrailerPattern'
|
|
5
|
+
export * from './compact'
|
|
6
|
+
export * from './execCommand'
|
|
7
|
+
export * from './execGit'
|
|
8
|
+
export * from './formatError'
|
|
9
|
+
export * from './formatFile'
|
|
10
|
+
export * from './interpolate'
|
|
11
|
+
export * from './isCI'
|
|
12
|
+
export * from './isDefined'
|
|
13
|
+
export * from './isMatch'
|
|
14
|
+
export * from './readJson'
|
|
15
|
+
export * from './unique'
|
|
16
|
+
export * from './writeJson'
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
const BACKSLASH = '\\'
|
|
5
|
+
const LEFT_BRACE = '{'
|
|
6
|
+
const RIGHT_BRACE = '}'
|
|
7
|
+
const DOT = '.'
|
|
8
|
+
const ESCAPE_CHARS = [BACKSLASH, LEFT_BRACE, RIGHT_BRACE]
|
|
9
|
+
const BANNED_KEYS = ['__proto__', 'constructor', 'prototype']
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Interpolate placeholders in `template` using values from `data`.
|
|
13
|
+
*
|
|
14
|
+
* - Placeholders use `{path.to.value}` (dot notation).
|
|
15
|
+
* - Backslash escapes are supported for `{`, `}`, `\`, and `.` inside tokens.
|
|
16
|
+
* - Tokens that reference banned keys (`__proto__`, `constructor`, `prototype`)
|
|
17
|
+
* are left unexpanded to avoid prototype pollution.
|
|
18
|
+
* - Only own properties are followed when resolving paths; if any step is
|
|
19
|
+
* missing, the original token text is preserved.
|
|
20
|
+
* - Resolved non-object, non-null values are stringified and inserted.
|
|
21
|
+
* - Unclosed tokens and a trailing backslash are preserved verbatim.
|
|
22
|
+
*
|
|
23
|
+
* @param template Template string containing placeholders like `{user.name}`
|
|
24
|
+
* @param data Object used to resolve placeholder paths
|
|
25
|
+
* @returns The template with placeholders replaced by corresponding values
|
|
26
|
+
*/
|
|
27
|
+
export function interpolate(
|
|
28
|
+
template: string,
|
|
29
|
+
data: Record<string, unknown>
|
|
30
|
+
): string {
|
|
31
|
+
let result = ''
|
|
32
|
+
let i = 0
|
|
33
|
+
let char: string | undefined
|
|
34
|
+
let nextChar: string | undefined
|
|
35
|
+
|
|
36
|
+
while ((char = template[i])) {
|
|
37
|
+
// Escape handling: if a backslash precedes an escapable char, emit the
|
|
38
|
+
// escaped character; otherwise emit the backslash itself.
|
|
39
|
+
if (char === BACKSLASH) {
|
|
40
|
+
nextChar = template[i + 1]
|
|
41
|
+
|
|
42
|
+
if (nextChar !== undefined && ESCAPE_CHARS.includes(nextChar)) {
|
|
43
|
+
result += nextChar
|
|
44
|
+
i += 2
|
|
45
|
+
} else {
|
|
46
|
+
result += char
|
|
47
|
+
i += 1
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Placeholder handling: parse a token starting at '{' until a matching '}'.
|
|
52
|
+
else if (char === LEFT_BRACE) {
|
|
53
|
+
const keys: string[] = []
|
|
54
|
+
let key = ''
|
|
55
|
+
let token = ''
|
|
56
|
+
let closed = false
|
|
57
|
+
i += 1
|
|
58
|
+
|
|
59
|
+
// Parse token body, supporting escaped characters and escaped dots.
|
|
60
|
+
while ((char = template[i])) {
|
|
61
|
+
if (char === RIGHT_BRACE) {
|
|
62
|
+
closed = true
|
|
63
|
+
keys.push(key)
|
|
64
|
+
i += 1
|
|
65
|
+
break
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (char === BACKSLASH) {
|
|
69
|
+
nextChar = template[i + 1]
|
|
70
|
+
|
|
71
|
+
// Allow escaping of brace, backslash, and dot inside tokens.
|
|
72
|
+
if (
|
|
73
|
+
nextChar !== undefined &&
|
|
74
|
+
[...ESCAPE_CHARS, DOT].includes(nextChar)
|
|
75
|
+
) {
|
|
76
|
+
key += nextChar
|
|
77
|
+
token += nextChar
|
|
78
|
+
i += 2
|
|
79
|
+
} else {
|
|
80
|
+
// Treat an isolated backslash as a literal within the token.
|
|
81
|
+
key += char
|
|
82
|
+
token += char
|
|
83
|
+
i += 1
|
|
84
|
+
}
|
|
85
|
+
} else if (char === DOT) {
|
|
86
|
+
// Dot separates path segments; record the current segment.
|
|
87
|
+
keys.push(key)
|
|
88
|
+
key = ''
|
|
89
|
+
token += char
|
|
90
|
+
i += 1
|
|
91
|
+
} else {
|
|
92
|
+
key += char
|
|
93
|
+
token += char
|
|
94
|
+
i += 1
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// If token closed with '}', attempt to resolve the path against `data`.
|
|
99
|
+
if (closed) {
|
|
100
|
+
let value: unknown = data
|
|
101
|
+
|
|
102
|
+
for (const key of keys) {
|
|
103
|
+
const isSafeKey = !BANNED_KEYS.includes(key)
|
|
104
|
+
|
|
105
|
+
// Only follow own properties on objects and skip banned keys.
|
|
106
|
+
if (
|
|
107
|
+
value !== null &&
|
|
108
|
+
typeof value === 'object' &&
|
|
109
|
+
Object.hasOwn(value, key) &&
|
|
110
|
+
isSafeKey
|
|
111
|
+
) {
|
|
112
|
+
value = (value as Record<string, unknown>)[key]
|
|
113
|
+
} else {
|
|
114
|
+
value = undefined
|
|
115
|
+
break
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// If resolution failed or produced an object/null, preserve the token.
|
|
120
|
+
if (value == null || typeof value === 'object') {
|
|
121
|
+
result += LEFT_BRACE + token + RIGHT_BRACE
|
|
122
|
+
} else {
|
|
123
|
+
result += String(value)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Unclosed token: preserve the raw token text (including the opening brace).
|
|
128
|
+
else {
|
|
129
|
+
result += LEFT_BRACE + token
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Ordinary characters are appended directly.
|
|
134
|
+
else {
|
|
135
|
+
result += char
|
|
136
|
+
i += 1
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return result
|
|
141
|
+
}
|
package/src/util/isCI.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Detect whether the current process is running in a CI environment.
|
|
6
|
+
*
|
|
7
|
+
* Returns true when the standard `CI` environment variable is set to a
|
|
8
|
+
* truthy value. Explicit string values `"false"` and `"0"` are treated as
|
|
9
|
+
* false to accommodate CI systems that expose `CI` but allow disabling it.
|
|
10
|
+
*
|
|
11
|
+
* This check is intentionally minimal and does not inspect provider-specific
|
|
12
|
+
* environment variables (e.g., `GITHUB_ACTIONS`, `GITLAB_CI`). Callers that
|
|
13
|
+
* need provider-level detail should check those variables separately.
|
|
14
|
+
*
|
|
15
|
+
* @returns `true` if running in CI; otherwise `false`
|
|
16
|
+
*/
|
|
17
|
+
export function isCI(): boolean {
|
|
18
|
+
return !!(
|
|
19
|
+
process.env.CI &&
|
|
20
|
+
process.env.CI !== 'false' &&
|
|
21
|
+
process.env.CI !== '0'
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Type guard that asserts a value is neither `null` nor `undefined`.
|
|
6
|
+
*
|
|
7
|
+
* @param value Value to test
|
|
8
|
+
* @returns `true` when `value` is not `null` or `undefined`
|
|
9
|
+
*/
|
|
10
|
+
export function isDefined<T>(value: T): value is NonNullable<T> {
|
|
11
|
+
return value != null
|
|
12
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2026 Fai
|
|
3
|
+
|
|
4
|
+
import { minimatch } from 'minimatch'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Determine whether a target string matches glob patterns.
|
|
8
|
+
*
|
|
9
|
+
* Matches positive and negative glob patterns using dotfile support. Automatically
|
|
10
|
+
* expands a single negative pattern by prepending a global wildcard match.
|
|
11
|
+
*
|
|
12
|
+
* @param target Target string to evaluate against glob patterns
|
|
13
|
+
* @param patterns Single glob pattern or list of patterns to evaluate
|
|
14
|
+
* @returns True if target matches positive patterns and satisfies negative patterns
|
|
15
|
+
*/
|
|
16
|
+
export function isMatch(
|
|
17
|
+
target: string,
|
|
18
|
+
patterns: string | readonly string[]
|
|
19
|
+
): boolean {
|
|
20
|
+
if (typeof patterns === 'string') {
|
|
21
|
+
if (patterns.startsWith('!')) {
|
|
22
|
+
patterns = ['**/*', patterns]
|
|
23
|
+
} else {
|
|
24
|
+
patterns = [patterns]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const positives: string[] = []
|
|
29
|
+
const negatives: string[] = []
|
|
30
|
+
|
|
31
|
+
for (const pattern of patterns) {
|
|
32
|
+
if (pattern.startsWith('!')) {
|
|
33
|
+
negatives.push(pattern)
|
|
34
|
+
} else {
|
|
35
|
+
positives.push(pattern)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
positives.some((pattern) => minimatch(target, pattern, { dot: true })) &&
|
|
41
|
+
negatives.every((pattern) => minimatch(target, pattern, { dot: true }))
|
|
42
|
+
)
|
|
43
|
+
}
|