@guiho/mirror 3.0.0-alpha.4

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 (55) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE.md +23 -0
  3. package/README.md +190 -0
  4. package/bin/mirror.exe +0 -0
  5. package/jsr.json +12 -0
  6. package/library/adapters.d.ts +27 -0
  7. package/library/adapters.d.ts.map +1 -0
  8. package/library/adapters.js +152 -0
  9. package/library/cli.d.ts +25 -0
  10. package/library/cli.d.ts.map +1 -0
  11. package/library/cli.js +258 -0
  12. package/library/config.d.ts +14 -0
  13. package/library/config.d.ts.map +1 -0
  14. package/library/config.js +193 -0
  15. package/library/errors.d.ts +9 -0
  16. package/library/errors.d.ts.map +1 -0
  17. package/library/errors.js +15 -0
  18. package/library/executor.d.ts +7 -0
  19. package/library/executor.d.ts.map +1 -0
  20. package/library/executor.js +34 -0
  21. package/library/flags.d.ts +6 -0
  22. package/library/flags.d.ts.map +1 -0
  23. package/library/flags.js +69 -0
  24. package/library/guiho-mirror-bin.d.ts +6 -0
  25. package/library/guiho-mirror-bin.d.ts.map +1 -0
  26. package/library/guiho-mirror-bin.js +6 -0
  27. package/library/guiho-mirror.d.ts +14 -0
  28. package/library/guiho-mirror.d.ts.map +1 -0
  29. package/library/guiho-mirror.js +12 -0
  30. package/library/plan.d.ts +10 -0
  31. package/library/plan.d.ts.map +1 -0
  32. package/library/plan.js +81 -0
  33. package/library/reporter.d.ts +12 -0
  34. package/library/reporter.d.ts.map +1 -0
  35. package/library/reporter.js +121 -0
  36. package/library/types.d.ts +123 -0
  37. package/library/types.d.ts.map +1 -0
  38. package/library/types.js +4 -0
  39. package/library/version.d.ts +10 -0
  40. package/library/version.d.ts.map +1 -0
  41. package/library/version.js +31 -0
  42. package/package.json +81 -0
  43. package/source/adapters.ts +176 -0
  44. package/source/cli.ts +285 -0
  45. package/source/config.ts +224 -0
  46. package/source/errors.ts +17 -0
  47. package/source/executor.ts +39 -0
  48. package/source/flags.ts +84 -0
  49. package/source/guiho-mirror-bin.ts +8 -0
  50. package/source/guiho-mirror.spec.ts +501 -0
  51. package/source/guiho-mirror.ts +44 -0
  52. package/source/plan.ts +98 -0
  53. package/source/reporter.ts +127 -0
  54. package/source/types.ts +128 -0
  55. package/source/version.ts +39 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * @copyright Copyright (c) 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
3
+ */
4
+
5
+ import { $ } from 'bun'
6
+ import { existsSync } from 'node:fs'
7
+ import type { MirrorConfig, MirrorJsonObject } from './types'
8
+ import { MirrorError } from './errors'
9
+ import { assertValidSemver, sortSemverDescending } from './version'
10
+ import { resolveMirrorPath } from './config'
11
+
12
+ export const supportedGitTagTemplates = ['v{version}', '{name}@{version}', '{name}/v{version}'] as const
13
+
14
+ export const readPackageJson = async (path: string): Promise<MirrorJsonObject> => readJsonObject(path, 'package.json')
15
+ export const readJsrJson = async (path: string): Promise<MirrorJsonObject> => readJsonObject(path, 'jsr.json')
16
+
17
+ export const writeJsonObject = async (path: string, object: MirrorJsonObject) => {
18
+ await Bun.write(path, `${JSON.stringify(object, null, 2)}\n`)
19
+ }
20
+
21
+ export const readPackageVersion = async (config: MirrorConfig) => readVersionField(resolveMirrorPath(config.cwd, config.package.path), 'package.json')
22
+ export const readJsrVersion = async (config: MirrorConfig) => readVersionField(resolveMirrorPath(config.cwd, config.jsr.path), 'jsr.json')
23
+ export const readPackageName = async (config: MirrorConfig) => readNameField(resolveMirrorPath(config.cwd, config.package.path), 'package.json')
24
+ export const readJsrName = async (config: MirrorConfig) => readNameField(resolveMirrorPath(config.cwd, config.jsr.path), 'jsr.json')
25
+
26
+ export const writePackageVersion = async (config: MirrorConfig, nextVersion: string) =>
27
+ writeVersionField(resolveMirrorPath(config.cwd, config.package.path), 'package.json', nextVersion)
28
+
29
+ export const writeJsrVersion = async (config: MirrorConfig, nextVersion: string) =>
30
+ writeVersionField(resolveMirrorPath(config.cwd, config.jsr.path), 'jsr.json', nextVersion)
31
+
32
+ export const ensureAdapterFiles = async (config: MirrorConfig) => {
33
+ if (usesAdapter(config, 'package.json')) ensureFile(resolveMirrorPath(config.cwd, config.package.path), 'package.json')
34
+ if (usesAdapter(config, 'jsr.json')) ensureFile(resolveMirrorPath(config.cwd, config.jsr.path), 'jsr.json')
35
+ if (usesAdapter(config, 'git')) await ensureGitRepository(config.cwd)
36
+ }
37
+
38
+ export const resolveProjectName = async (config: MirrorConfig) => {
39
+ if (config.project.name) return config.project.name
40
+ if (config.project.nameSource === 'package.json') return readPackageName(config)
41
+ if (config.project.nameSource === 'jsr.json') return readJsrName(config)
42
+ return undefined
43
+ }
44
+
45
+ export const readCurrentVersion = async (config: MirrorConfig, projectName?: string) => {
46
+ if (config.version.source === 'package.json') return readPackageVersion(config)
47
+ if (config.version.source === 'jsr.json') return readJsrVersion(config)
48
+ return readGitVersion(config, projectName)
49
+ }
50
+
51
+ export const readGitVersion = async (config: MirrorConfig, projectName?: string) => {
52
+ await ensureGitRepository(config.cwd)
53
+
54
+ const tagsOutput = await $`git -C ${config.cwd} tag --list`.text()
55
+ const versions = tagsOutput
56
+ .split(/\r?\n/)
57
+ .map((line) => line.trim())
58
+ .filter(Boolean)
59
+ .map((tag) => versionFromTag(config.git.tagTemplate, tag, projectName))
60
+ .filter((version): version is string => Boolean(version))
61
+
62
+ if (versions.length === 0) throw new MirrorError(`No Git tags match template: ${config.git.tagTemplate}`)
63
+
64
+ return sortSemverDescending(versions)[0] ?? ''
65
+ }
66
+
67
+ export const renderGitTag = (template: string, version: string, projectName?: string) => {
68
+ assertSupportedGitTagTemplate(template)
69
+ assertValidSemver(version, 'Git tag version')
70
+
71
+ if (template.includes('{name}') && !projectName) throw new MirrorError(`Tag template requires a project name: ${template}`)
72
+
73
+ return template.replaceAll('{version}', version).replaceAll('{name}', projectName ?? '')
74
+ }
75
+
76
+ export const versionFromTag = (template: string, tag: string, projectName?: string) => {
77
+ assertSupportedGitTagTemplate(template)
78
+
79
+ if (template.includes('{name}') && !projectName) throw new MirrorError(`Tag template requires a project name: ${template}`)
80
+
81
+ const escapedTemplate = escapeRegex(template)
82
+ .replaceAll('\\{version\\}', '(?<version>.+)')
83
+ .replaceAll('\\{name\\}', escapeRegex(projectName ?? ''))
84
+ const match = new RegExp(`^${escapedTemplate}$`).exec(tag)
85
+ const version = match?.groups?.['version']
86
+
87
+ if (!version) return undefined
88
+
89
+ try {
90
+ assertValidSemver(version, 'Git tag version')
91
+ return version
92
+ } catch {
93
+ return undefined
94
+ }
95
+ }
96
+
97
+ export const assertSupportedGitTagTemplate = (template: string) => {
98
+ if (!(supportedGitTagTemplates as readonly string[]).includes(template)) {
99
+ throw new MirrorError(`Unsupported Git tag template: ${template}. Expected v{version}, {name}@{version}, or {name}/v{version}.`)
100
+ }
101
+ }
102
+
103
+ export const isGitRepository = async (cwd: string) => {
104
+ try {
105
+ await $`git -C ${cwd} rev-parse --is-inside-work-tree`.quiet()
106
+ return true
107
+ } catch {
108
+ return false
109
+ }
110
+ }
111
+
112
+ export const isGitDirty = async (cwd: string) => {
113
+ const output = await $`git -C ${cwd} status --porcelain`.quiet().text()
114
+ return output.trim().length > 0
115
+ }
116
+
117
+ export const createGitCommit = async (cwd: string, paths: string[], message: string) => {
118
+ for (const path of paths) await $`git -C ${cwd} add ${path}`.quiet()
119
+ await $`git -C ${cwd} commit -m ${message}`.quiet()
120
+ }
121
+
122
+ export const createGitTag = async (cwd: string, tag: string) => {
123
+ await $`git -C ${cwd} tag ${tag} -m ${`Release ${tag}`}`.quiet()
124
+ }
125
+
126
+ export const pushGitRefs = async (cwd: string, includeCommit: boolean, includeTags: boolean) => {
127
+ if (includeCommit) await $`git -C ${cwd} push`.quiet()
128
+ if (includeTags) await $`git -C ${cwd} push --tags`.quiet()
129
+ }
130
+
131
+ const readJsonObject = async (path: string, label: string): Promise<MirrorJsonObject> => {
132
+ ensureFile(path, label)
133
+ const json = await Bun.file(path).json()
134
+
135
+ if (typeof json !== 'object' || json === null || Array.isArray(json)) throw new MirrorError(`${label} must contain a JSON object: ${path}`)
136
+
137
+ return json as MirrorJsonObject
138
+ }
139
+
140
+ const readVersionField = async (path: string, label: string): Promise<string> => {
141
+ const json = await readJsonObject(path, label)
142
+ const version = json['version']
143
+
144
+ if (typeof version !== 'string') throw new MirrorError(`${label} must contain a string version field: ${path}`)
145
+ assertValidSemver(version, `${label} version`)
146
+
147
+ return version
148
+ }
149
+
150
+ const readNameField = async (path: string, label: string): Promise<string> => {
151
+ const json = await readJsonObject(path, label)
152
+ const name = json['name']
153
+
154
+ if (typeof name !== 'string' || name.length === 0) throw new MirrorError(`${label} must contain a string name field: ${path}`)
155
+
156
+ return name
157
+ }
158
+
159
+ const writeVersionField = async (path: string, label: string, nextVersion: string) => {
160
+ const json = await readJsonObject(path, label)
161
+ json['version'] = nextVersion
162
+ await writeJsonObject(path, json)
163
+ }
164
+
165
+ const ensureFile = (path: string, label: string) => {
166
+ if (!existsSync(path)) throw new MirrorError(`${label} file not found: ${path}`)
167
+ }
168
+
169
+ const ensureGitRepository = async (cwd: string) => {
170
+ if (!(await isGitRepository(cwd))) throw new MirrorError(`Not a Git repository: ${cwd}`)
171
+ }
172
+
173
+ const usesAdapter = (config: MirrorConfig, adapter: 'package.json' | 'jsr.json' | 'git') =>
174
+ config.version.source === adapter || config.version.output.includes(adapter)
175
+
176
+ const escapeRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
package/source/cli.ts ADDED
@@ -0,0 +1,285 @@
1
+ /**
2
+ * @copyright Copyright (c) 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
3
+ */
4
+
5
+ import { defineCommand, runMain } from 'citty'
6
+ import type { ArgsDef } from 'citty'
7
+ import { readFileSync } from 'node:fs'
8
+ import { resolve } from 'node:path'
9
+ import { MirrorError } from './errors'
10
+ import { readCurrentVersion, resolveProjectName } from './adapters'
11
+ import { configPathForDisplay, discoverMirrorConfig, loadMirrorConfig, relativeFromCwd, writeInitConfig } from './config'
12
+ import { executeVersionPlan } from './executor'
13
+ import { parseMirrorCliOptions } from './flags'
14
+ import { buildVersionPlan, validateMirrorConfig } from './plan'
15
+ import { mirrorBanner, reportConfig, reportConfigSchema, reportExecution, reportExecutionSummary, reportPlan, reportValue } from './reporter'
16
+ import type { MirrorAdapterName, MirrorCliOptions } from './types'
17
+ import { resolveNextVersion } from './version'
18
+
19
+ const mirrorVersion = readInstalledVersion()
20
+
21
+ const globalArgs = {
22
+ config: { type: 'string', description: 'Path to mirror.config.toml' },
23
+ cwd: { type: 'string', description: 'Run as if Mirror started in this directory' },
24
+ format: { type: 'enum', options: ['text', 'json'], default: 'text', description: 'Output format' },
25
+ 'no-color': { type: 'boolean', description: 'Disable color output' },
26
+ } satisfies ArgsDef
27
+
28
+ const overrideArgs = {
29
+ ...globalArgs,
30
+ source: { type: 'enum', options: ['package.json', 'jsr.json', 'git'], description: 'Override version source' },
31
+ output: { type: 'string', description: 'Override version output. Repeat or comma-separate values.' },
32
+ 'package-file': { type: 'string', description: 'Override package.json path' },
33
+ 'jsr-file': { type: 'string', description: 'Override jsr.json path' },
34
+ preid: { type: 'string', description: 'Override prerelease identifier' },
35
+ } satisfies ArgsDef
36
+
37
+ const applyArgs = {
38
+ ...overrideArgs,
39
+ 'dry-run': { type: 'boolean', alias: 'dy', description: 'Build and print the plan without applying it' },
40
+ commit: { type: 'boolean', description: 'Create a release commit when file outputs changed' },
41
+ push: { type: 'boolean', description: 'Create the release commit when needed, then push release refs' },
42
+ 'allow-dirty': { type: 'boolean', description: 'Allow release in a dirty Git worktree' },
43
+ yes: { type: 'boolean', alias: 'y', description: 'Apply without interactive confirmation' },
44
+ } satisfies ArgsDef
45
+
46
+ const targetArg = {
47
+ target: { type: 'positional', description: 'Release target or exact semantic version', required: true },
48
+ } satisfies ArgsDef
49
+
50
+ export const createMirrorCommand = () =>
51
+ defineCommand({
52
+ meta: {
53
+ name: 'mirror',
54
+ version: mirrorVersion,
55
+ description: 'Open source project versioning for Bun, npm, JSR, and Git.',
56
+ },
57
+ args: globalArgs,
58
+ subCommands: {
59
+ init: createInitCommand(),
60
+ config: createConfigCommand(),
61
+ version: createVersionCommand(),
62
+ },
63
+ })
64
+
65
+ export const runMirrorCli = async (rawArgs = process.argv.slice(2)) => {
66
+ const effectiveArgs = rawArgs.length === 0 ? ['--help'] : rawArgs
67
+ const restoreColorOutput = effectiveArgs.includes('--no-color') ? stripColorFromProcessOutput() : () => {}
68
+
69
+ try {
70
+ if (effectiveArgs.includes('--no-color')) process.env['NO_COLOR'] = '1'
71
+
72
+ if (effectiveArgs.includes('--help')) {
73
+ const parsed = parseMirrorCliOptions(effectiveArgs)
74
+ const cwd = resolve(parsed.cwd ?? process.cwd())
75
+ const discovery = await discoverMirrorConfig(cwd, parsed.config)
76
+ const configDisplay = discovery.path ? relativeFromCwd(cwd, discovery.path) : ''
77
+ process.stdout.write(mirrorBanner(configDisplay))
78
+ }
79
+
80
+ if (rawArgs.length === 0) {
81
+ process.on('exit', () => {
82
+ process.stdout.write([
83
+ 'EXAMPLES',
84
+ '',
85
+ ' mirror version current # Print the current version',
86
+ ' mirror version plan patch # Preview a patch release plan',
87
+ ' mirror version apply minor --commit # Apply a minor release with commit',
88
+ ' mirror version plan patch --output=package.json,jsr.json,git # Plan with package, jsr, and git',
89
+ ' mirror config schema # Print the configuration file reference',
90
+ '',
91
+ ].join('\n') + '\n')
92
+ })
93
+ }
94
+
95
+ await runMain(createMirrorCommand(), { rawArgs: effectiveArgs })
96
+ } catch (error) {
97
+ if (error instanceof MirrorError) {
98
+ console.error(error.message)
99
+ process.exit(error.exitCode)
100
+ }
101
+
102
+ throw error
103
+ } finally {
104
+ restoreColorOutput()
105
+ }
106
+ }
107
+
108
+ const createInitCommand = () =>
109
+ defineCommand({
110
+ meta: { name: 'init', description: 'Create a Mirror configuration file.' },
111
+ subCommands: {
112
+ 'package.json': createInitKindCommand('package.json'),
113
+ 'jsr.json': createInitKindCommand('jsr.json'),
114
+ git: createInitKindCommand('git'),
115
+ },
116
+ })
117
+
118
+ const createInitKindCommand = (kind: MirrorAdapterName) =>
119
+ defineCommand({
120
+ meta: { name: kind, description: `Create ${kind} project configuration.` },
121
+ args: {
122
+ ...globalArgs,
123
+ yes: { type: 'boolean', description: 'Overwrite existing mirror.config.toml' },
124
+ },
125
+ async run(context) {
126
+ const options = cliOptions(context.rawArgs, context.args)
127
+ const path = await writeInitConfig(kind, options.cwd ?? process.cwd(), Boolean(options.yes))
128
+ process.stdout.write(reportValue(`created ${path}`, options.format))
129
+ },
130
+ })
131
+
132
+ const createConfigCommand = () =>
133
+ defineCommand({
134
+ meta: { name: 'config', description: 'Inspect and validate Mirror configuration.' },
135
+ subCommands: {
136
+ show: defineCommand({
137
+ meta: { name: 'show', description: 'Print the resolved configuration.' },
138
+ args: overrideArgs,
139
+ async run(context) {
140
+ const options = cliOptions(context.rawArgs, context.args)
141
+ const config = await loadMirrorConfig(options)
142
+ if (options.format !== 'json') process.stdout.write(mirrorBanner(configPathForDisplay(config)))
143
+ process.stdout.write(reportConfig(config, options.format))
144
+ },
145
+ }),
146
+ check: defineCommand({
147
+ meta: { name: 'check', description: 'Validate the resolved configuration.' },
148
+ args: overrideArgs,
149
+ async run(context) {
150
+ const options = cliOptions(context.rawArgs, context.args)
151
+ await validateMirrorConfig(options)
152
+ process.stdout.write(reportValue('ok', options.format))
153
+ },
154
+ }),
155
+ schema: defineCommand({
156
+ meta: { name: 'schema', description: 'Print the configuration file reference.' },
157
+ args: globalArgs,
158
+ run(context) {
159
+ const options = cliOptions(context.rawArgs, context.args)
160
+ if (options.format !== 'json') process.stdout.write(mirrorBanner())
161
+ process.stdout.write(reportConfigSchema(options.format))
162
+ },
163
+ }),
164
+ },
165
+ })
166
+
167
+ const createVersionCommand = () =>
168
+ defineCommand({
169
+ meta: { name: 'version', description: 'Read, plan, and apply version changes.' },
170
+ subCommands: {
171
+ current: defineCommand({
172
+ meta: { name: 'current', description: 'Print the current project version.' },
173
+ args: overrideArgs,
174
+ async run(context) {
175
+ const options = cliOptions(context.rawArgs, context.args)
176
+ const config = await loadMirrorConfig(options)
177
+ const projectName = await resolveProjectName(config)
178
+ process.stdout.write(reportValue(await readCurrentVersion(config, projectName), options.format))
179
+ },
180
+ }),
181
+ next: defineCommand({
182
+ meta: { name: 'next', description: 'Print the next version without checking outputs.' },
183
+ args: { ...overrideArgs, ...targetArg },
184
+ async run(context) {
185
+ const options = cliOptions(context.rawArgs, context.args)
186
+ const config = await loadMirrorConfig(options)
187
+ const projectName = await resolveProjectName(config)
188
+ const currentVersion = await readCurrentVersion(config, projectName)
189
+ process.stdout.write(reportValue(resolveNextVersion(currentVersion, String(context.args['target']), config.version.prereleaseId), options.format))
190
+ },
191
+ }),
192
+ plan: defineCommand({
193
+ meta: { name: 'plan', description: 'Print the release plan without writing anything.' },
194
+ args: { ...overrideArgs, ...targetArg },
195
+ async run(context) {
196
+ const options = cliOptions(context.rawArgs, context.args)
197
+ const plan = await buildVersionPlan(String(context.args['target']), options)
198
+ if (options.format !== 'json') process.stdout.write(mirrorBanner(plan.configPath ? plan.configPath : ''))
199
+ process.stdout.write(reportPlan(plan, options.format))
200
+ },
201
+ }),
202
+ apply: defineCommand({
203
+ meta: { name: 'apply', description: 'Apply the release plan.' },
204
+ args: { ...applyArgs, ...targetArg },
205
+ async run(context) {
206
+ const options = cliOptions(context.rawArgs, context.args)
207
+ const plan = await buildVersionPlan(String(context.args['target']), options)
208
+
209
+ if (options.format !== 'json') process.stdout.write(mirrorBanner(plan.configPath ? plan.configPath : ''))
210
+ if (options.format !== 'json') process.stdout.write(reportPlan(plan, options.format))
211
+
212
+ const result = await executeVersionPlan(plan, options)
213
+ process.stdout.write(options.format === 'json' ? reportExecution(result, options.format) : reportExecutionSummary(result, options.format))
214
+ },
215
+ }),
216
+ },
217
+ })
218
+
219
+ const cliOptions = (rawArgs: string[], args: Record<string, unknown>): MirrorCliOptions => {
220
+ const parsed = parseMirrorCliOptions(rawArgs)
221
+
222
+ return {
223
+ ...parsed,
224
+ config: parsed.config ?? stringArg(args['config']),
225
+ cwd: parsed.cwd ?? stringArg(args['cwd']),
226
+ format: parsed.format ?? (args['format'] === 'json' ? 'json' : 'text'),
227
+ source: parsed.source ?? adapterArg(args['source']),
228
+ output: parsed.output ?? outputArg(args['output']),
229
+ packageFile: parsed.packageFile ?? stringArg(args['packageFile']),
230
+ jsrFile: parsed.jsrFile ?? stringArg(args['jsrFile']),
231
+ preid: parsed.preid ?? stringArg(args['preid']),
232
+ dryRun: parsed.dryRun || args['dryRun'] === true,
233
+ commit: parsed.commit || args['commit'] === true,
234
+ push: parsed.push || args['push'] === true,
235
+ allowDirty: parsed.allowDirty || args['allowDirty'] === true,
236
+ yes: parsed.yes || args['yes'] === true,
237
+ }
238
+ }
239
+
240
+ const stringArg = (value: unknown) => (typeof value === 'string' ? value : undefined)
241
+
242
+ const adapterArg = (value: unknown) => {
243
+ if (value === 'package.json' || value === 'jsr.json' || value === 'git') return value
244
+ return undefined
245
+ }
246
+
247
+ const outputArg = (value: unknown): MirrorCliOptions['output'] => {
248
+ if (typeof value !== 'string') return undefined
249
+
250
+ const values = value.split(',').map((item) => item.trim()).filter(Boolean)
251
+
252
+ if (values.length === 0) return undefined
253
+
254
+ return values.map((item) => {
255
+ const adapter = adapterArg(item)
256
+ if (!adapter) throw new MirrorError(`Invalid --output value: ${item}`)
257
+ return adapter
258
+ })
259
+ }
260
+
261
+ function readInstalledVersion() {
262
+ try {
263
+ const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as Record<string, unknown>
264
+ return typeof packageJson['version'] === 'string' ? packageJson['version'] : '0.0.0'
265
+ } catch {
266
+ return '0.0.0'
267
+ }
268
+ }
269
+
270
+ const stripAnsi = (value: string) => value.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, '')
271
+
272
+ const stripColorFromProcessOutput = () => {
273
+ const originalLog = console.log
274
+ const originalError = console.error
275
+
276
+ console.log = (...values: unknown[]) => originalLog(...values.map(stripAnsiValue))
277
+ console.error = (...values: unknown[]) => originalError(...values.map(stripAnsiValue))
278
+
279
+ return () => {
280
+ console.log = originalLog
281
+ console.error = originalError
282
+ }
283
+ }
284
+
285
+ const stripAnsiValue = (value: unknown) => (typeof value === 'string' ? stripAnsi(value) : value)
@@ -0,0 +1,224 @@
1
+ /**
2
+ * @copyright Copyright (c) 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
3
+ */
4
+
5
+ import { existsSync } from 'node:fs'
6
+ import { basename, isAbsolute, join, relative, resolve } from 'node:path'
7
+ import type {
8
+ MirrorAdapterName,
9
+ MirrorCliOptions,
10
+ MirrorConfig,
11
+ MirrorConfigDiscovery,
12
+ MirrorProjectNameSource,
13
+ MirrorRawConfig,
14
+ } from './types'
15
+ import { MirrorError } from './errors'
16
+
17
+ const adapters = new Set(['package.json', 'jsr.json', 'git'])
18
+ const projectNameSources = new Set(['package.json', 'jsr.json'])
19
+
20
+ export const resolveMirrorPath = (cwd: string, path: string) => (isAbsolute(path) ? path : resolve(cwd, path))
21
+
22
+ export const relativeFromCwd = (cwd: string, path: string) => {
23
+ const relativePath = relative(cwd, resolveMirrorPath(cwd, path))
24
+ return relativePath || '.'
25
+ }
26
+
27
+ export const discoverMirrorConfig = async (cwd: string, explicitPath?: string): Promise<MirrorConfigDiscovery> => {
28
+ if (explicitPath) {
29
+ const configPath = resolveMirrorPath(cwd, explicitPath)
30
+ return { path: configPath, raw: await readConfigFile(configPath) }
31
+ }
32
+
33
+ const rootConfigPath = resolve(cwd, 'mirror.config.toml')
34
+ if (existsSync(rootConfigPath)) return { path: rootConfigPath, raw: await readConfigFile(rootConfigPath) }
35
+
36
+ const nestedConfigPath = resolve(cwd, 'config', 'mirror.config.toml')
37
+ if (existsSync(nestedConfigPath)) return { path: nestedConfigPath, raw: await readConfigFile(nestedConfigPath) }
38
+
39
+ return {}
40
+ }
41
+
42
+ export const readConfigFile = async (path: string): Promise<MirrorRawConfig> => {
43
+ if (!existsSync(path)) throw new MirrorError(`Configuration file not found: ${path}`)
44
+
45
+ const parsed = Bun.TOML.parse(await Bun.file(path).text())
46
+
47
+ if (!isRecord(parsed)) throw new MirrorError(`Configuration file must contain a TOML object: ${path}`)
48
+
49
+ return parsed as MirrorRawConfig
50
+ }
51
+
52
+ export const loadMirrorConfig = async (options: MirrorCliOptions = {}): Promise<MirrorConfig> => {
53
+ const cwd = resolve(options.cwd ?? process.cwd())
54
+ const discovered = await discoverMirrorConfig(cwd, options.config)
55
+
56
+ if (!discovered.raw) throw new MirrorError('Mirror configuration not found. Run `mirror init package`, `mirror init jsr`, or `mirror init git`.')
57
+
58
+ return normalizeMirrorConfig(discovered.raw, cwd, discovered.path, options)
59
+ }
60
+
61
+ export const normalizeMirrorConfig = (
62
+ raw: MirrorRawConfig,
63
+ cwd: string,
64
+ configPath: string | undefined,
65
+ options: MirrorCliOptions = {},
66
+ ): MirrorConfig => {
67
+ if (raw.schema !== 1) throw new MirrorError('Unsupported or missing configuration schema. Expected `schema = 1`.')
68
+ if (raw.version?.scheme !== undefined && raw.version.scheme !== 'semver') throw new MirrorError('Only `version.scheme = "semver"` is supported.')
69
+
70
+ const source = options.source ?? assertAdapter(raw.version?.source, 'version.source')
71
+ const output = dedupeAdapters(options.output ?? assertOutput(raw.version?.output))
72
+ const nameSource: MirrorProjectNameSource | undefined = raw.project?.name_source
73
+ ? assertProjectNameSource(raw.project.name_source, 'project.name_source')
74
+ : undefined
75
+ const projectName = optionalString(raw.project?.name, 'project.name')
76
+ const prereleaseId = options.preid ?? optionalString(raw.version?.prerelease_id, 'version.prerelease_id') ?? ''
77
+ const packagePath = options.packageFile ?? optionalString(raw.package?.path, 'package.path') ?? 'package.json'
78
+ const jsrPath = options.jsrFile ?? optionalString(raw.jsr?.path, 'jsr.path') ?? 'jsr.json'
79
+ const tagTemplate = optionalString(raw.git?.tag_template, 'git.tag_template') ?? 'v{version}'
80
+ const gitCommit = optionalBoolean(raw.git?.commit, 'git.commit') === true
81
+ const gitPush = optionalBoolean(raw.git?.push, 'git.push') === true
82
+ const gitAllowDirty = optionalBoolean(raw.git?.allow_dirty, 'git.allow_dirty') === true
83
+
84
+ return {
85
+ schema: 1,
86
+ cwd,
87
+ configPath,
88
+ project: {
89
+ name: projectName,
90
+ nameSource,
91
+ },
92
+ version: {
93
+ scheme: 'semver',
94
+ source,
95
+ output,
96
+ prereleaseId,
97
+ },
98
+ package: {
99
+ path: packagePath,
100
+ },
101
+ jsr: {
102
+ path: jsrPath,
103
+ },
104
+ git: {
105
+ tagTemplate,
106
+ commit: options.commit === true || options.push === true || gitCommit || gitPush,
107
+ push: options.push === true || gitPush,
108
+ allowDirty: options.allowDirty === true || gitAllowDirty,
109
+ },
110
+ }
111
+ }
112
+
113
+ export const createInitConfig = (kind: MirrorAdapterName, cwd: string) => {
114
+ const projectName = basename(cwd)
115
+
116
+ if (kind === 'package.json') {
117
+ return `schema = 1
118
+
119
+ [project]
120
+ name_source = "package.json"
121
+
122
+ [version]
123
+ scheme = "semver"
124
+ source = "package.json"
125
+ output = ["package.json"]
126
+ prerelease_id = ""
127
+
128
+ [package]
129
+ path = "package.json"
130
+
131
+ [jsr]
132
+ path = "jsr.json"
133
+
134
+ [git]
135
+ tag_template = "{name}@{version}"
136
+ commit = false
137
+ push = false
138
+ allow_dirty = false
139
+ `
140
+ }
141
+
142
+ if (kind === 'jsr.json') {
143
+ return `schema = 1
144
+
145
+ [project]
146
+ name_source = "jsr.json"
147
+
148
+ [version]
149
+ scheme = "semver"
150
+ source = "jsr.json"
151
+ output = ["jsr.json"]
152
+ prerelease_id = ""
153
+
154
+ [jsr]
155
+ path = "jsr.json"
156
+
157
+ [git]
158
+ tag_template = "{name}@{version}"
159
+ commit = false
160
+ push = false
161
+ allow_dirty = false
162
+ `
163
+ }
164
+
165
+ return `schema = 1
166
+
167
+ [project]
168
+ name = "${projectName}"
169
+
170
+ [version]
171
+ scheme = "semver"
172
+ source = "git"
173
+ output = ["git"]
174
+ prerelease_id = ""
175
+
176
+ [git]
177
+ tag_template = "v{version}"
178
+ commit = false
179
+ push = false
180
+ allow_dirty = false
181
+ `
182
+ }
183
+
184
+ export const writeInitConfig = async (kind: MirrorAdapterName, cwd: string, overwrite = false) => {
185
+ const path = join(cwd, 'mirror.config.toml')
186
+
187
+ if (existsSync(path) && !overwrite) throw new MirrorError(`Configuration already exists: ${path}`)
188
+
189
+ await Bun.write(path, createInitConfig(kind, cwd))
190
+ return path
191
+ }
192
+
193
+ export const configPathForDisplay = (config: MirrorConfig) => (config.configPath ? relativeFromCwd(config.cwd, config.configPath) : '(none)')
194
+
195
+ const assertAdapter = (value: unknown, key: string): MirrorAdapterName => {
196
+ if (typeof value !== 'string' || !adapters.has(value)) throw new MirrorError(`Invalid or missing ${key}. Expected package.json, jsr.json, or git.`)
197
+ return value as MirrorAdapterName
198
+ }
199
+
200
+ const assertProjectNameSource = (value: unknown, key: string): MirrorProjectNameSource => {
201
+ if (typeof value !== 'string' || !projectNameSources.has(value)) throw new MirrorError(`Invalid ${key}. Expected package.json or jsr.json.`)
202
+ return value as MirrorProjectNameSource
203
+ }
204
+
205
+ const assertOutput = (value: unknown): MirrorAdapterName[] => {
206
+ if (!Array.isArray(value) || value.length === 0) throw new MirrorError('Invalid or missing version.output. Expected at least one output adapter.')
207
+ return value.map((item) => assertAdapter(item, 'version.output'))
208
+ }
209
+
210
+ const dedupeAdapters = (value: MirrorAdapterName[]) => [...new Set(value)]
211
+
212
+ const optionalString = (value: unknown, key: string) => {
213
+ if (value === undefined) return undefined
214
+ if (typeof value !== 'string') throw new MirrorError(`Invalid ${key}. Expected a string.`)
215
+ return value
216
+ }
217
+
218
+ const optionalBoolean = (value: unknown, key: string) => {
219
+ if (value === undefined) return undefined
220
+ if (typeof value !== 'boolean') throw new MirrorError(`Invalid ${key}. Expected true or false.`)
221
+ return value
222
+ }
223
+
224
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null && !Array.isArray(value)