breakage 0.0.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -54
- package/dist/{breakage-D_nc-zEK.js → breakage-Dc4IldeW.js} +113 -23
- package/dist/breakage-Dc4IldeW.js.map +1 -0
- package/dist/breakage.$configPath.schema.json +3 -0
- package/dist/breakage.d.ts +22 -1
- package/dist/breakage.d.ts.map +1 -1
- package/dist/breakage.js +1 -1
- package/dist/breakage.main.schema.json +3 -0
- package/dist/breakage.prelude.$configPath.schema.json +31 -0
- package/dist/breakage.prelude.schema.json +31 -0
- package/dist/breakage.x.js +63 -4
- package/dist/breakage.x.js.map +1 -1
- package/package.json +50 -51
- package/src/baseline.ts +143 -0
- package/src/breakage.ts +48 -31
- package/src/breakage.x.ts +67 -4
- package/dist/breakage-D_nc-zEK.js.map +0 -1
package/dist/breakage.x.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as breakage } from "./breakage-
|
|
2
|
+
import { n as breakagePrelude, t as breakage } from "./breakage-Dc4IldeW.js";
|
|
3
3
|
import * as path$1 from "node:path";
|
|
4
4
|
import logger from "takua";
|
|
5
5
|
import { inspect } from "node:util";
|
|
@@ -8,6 +8,7 @@ import { cli, completionResponse, encapsulate, help, logWarnings, noOptions, opt
|
|
|
8
8
|
//#region src/breakage.x.ts
|
|
9
9
|
const BREAKAGE_MANUAL = options(`Check for breaking changes in a package.`, type({
|
|
10
10
|
"tagPattern?": `string`,
|
|
11
|
+
"baselineFile?": `string`,
|
|
11
12
|
testPattern: `string`,
|
|
12
13
|
testCommand: `string`,
|
|
13
14
|
certifyCommand: `string`,
|
|
@@ -22,6 +23,16 @@ const BREAKAGE_MANUAL = options(`Check for breaking changes in a package.`, type
|
|
|
22
23
|
description: `RegExp which, if found matched to a git tag, will be considered a release tag for your library.`,
|
|
23
24
|
example: `--tagPattern="my-library"`
|
|
24
25
|
},
|
|
26
|
+
baselineFile: {
|
|
27
|
+
aliases: [`baseline-file`],
|
|
28
|
+
completion: {
|
|
29
|
+
repeatable: false,
|
|
30
|
+
fileSystem: `files`
|
|
31
|
+
},
|
|
32
|
+
required: false,
|
|
33
|
+
description: `Use the exact locally fetched commit from a prelude snapshot instead of discovering remote tags.`,
|
|
34
|
+
example: `--baseline-file=.breakage/baseline.json`
|
|
35
|
+
},
|
|
25
36
|
testPattern: {
|
|
26
37
|
aliases: [`test-pattern`, `pattern`],
|
|
27
38
|
completion: { repeatable: false },
|
|
@@ -66,6 +77,39 @@ const BREAKAGE_MANUAL = options(`Check for breaking changes in a package.`, type
|
|
|
66
77
|
parse: parseBooleanOption
|
|
67
78
|
}
|
|
68
79
|
});
|
|
80
|
+
const PRELUDE_MANUAL = options(`Resolve and fetch the release baseline for a cacheable compatibility check.`, type({
|
|
81
|
+
"tagPattern?": `string`,
|
|
82
|
+
"baseDirname?": `string`,
|
|
83
|
+
"out?": `string`,
|
|
84
|
+
"testPattern?": `string`,
|
|
85
|
+
"testCommand?": `string`,
|
|
86
|
+
"certifyCommand?": `string`,
|
|
87
|
+
"baselineFile?": `string`,
|
|
88
|
+
"verbose?": `boolean`
|
|
89
|
+
}), {
|
|
90
|
+
...BREAKAGE_MANUAL.optionConfigs,
|
|
91
|
+
testPattern: {
|
|
92
|
+
...BREAKAGE_MANUAL.optionConfigs.testPattern,
|
|
93
|
+
required: false
|
|
94
|
+
},
|
|
95
|
+
testCommand: {
|
|
96
|
+
...BREAKAGE_MANUAL.optionConfigs.testCommand,
|
|
97
|
+
required: false
|
|
98
|
+
},
|
|
99
|
+
certifyCommand: {
|
|
100
|
+
...BREAKAGE_MANUAL.optionConfigs.certifyCommand,
|
|
101
|
+
required: false
|
|
102
|
+
},
|
|
103
|
+
out: {
|
|
104
|
+
completion: {
|
|
105
|
+
repeatable: false,
|
|
106
|
+
fileSystem: `files`
|
|
107
|
+
},
|
|
108
|
+
required: false,
|
|
109
|
+
description: `Gitignored snapshot path relative to the base directory (default: .breakage/baseline.json).`,
|
|
110
|
+
example: `--out=.breakage/baseline.json`
|
|
111
|
+
}
|
|
112
|
+
});
|
|
69
113
|
const SCHEMA_MANUAL = options(`Create a copy of the schema for configuring breakage.`, type({ "outdir?": `string` }), { outdir: {
|
|
70
114
|
aliases: [`out-dir`],
|
|
71
115
|
completion: {
|
|
@@ -82,18 +126,24 @@ const parse = cli({
|
|
|
82
126
|
routes: optional({
|
|
83
127
|
help: null,
|
|
84
128
|
schema: null,
|
|
129
|
+
prelude: optional({ $configPath: null }),
|
|
85
130
|
$configPath: null
|
|
86
131
|
}),
|
|
87
132
|
routeOptions: {
|
|
88
133
|
"": BREAKAGE_MANUAL,
|
|
89
134
|
$configPath: BREAKAGE_MANUAL,
|
|
90
135
|
help: noOptions(`Show usage.`),
|
|
91
|
-
schema: SCHEMA_MANUAL
|
|
136
|
+
schema: SCHEMA_MANUAL,
|
|
137
|
+
prelude: PRELUDE_MANUAL,
|
|
138
|
+
"prelude/$configPath": PRELUDE_MANUAL
|
|
139
|
+
},
|
|
140
|
+
positionalCompletions: {
|
|
141
|
+
$configPath: { fileSystem: `files` },
|
|
142
|
+
"prelude/$configPath": { fileSystem: `files` }
|
|
92
143
|
},
|
|
93
|
-
positionalCompletions: { $configPath: { fileSystem: `files` } },
|
|
94
144
|
discoverConfigPath: (args) => {
|
|
95
145
|
if (args[0] === `help` || args[0] === `schema`) return;
|
|
96
|
-
return args[0] ?? path$1.join(process.cwd(), `breakage.config.json`);
|
|
146
|
+
return (args[0] === `prelude` ? args[1] : args[0]) ?? path$1.join(process.cwd(), `breakage.config.json`);
|
|
97
147
|
}
|
|
98
148
|
}, {
|
|
99
149
|
info: (message, ...data) => logger.info(`breakage`, message, data),
|
|
@@ -115,6 +165,15 @@ else {
|
|
|
115
165
|
process.stdout.write(`📝 Wrote json.schema.`);
|
|
116
166
|
}
|
|
117
167
|
break;
|
|
168
|
+
case `prelude`:
|
|
169
|
+
case `prelude/$configPath`: {
|
|
170
|
+
const baseline = await breakagePrelude({
|
|
171
|
+
...inputs.opts,
|
|
172
|
+
out: inputs.opts.out ?? `.breakage/baseline.json`
|
|
173
|
+
});
|
|
174
|
+
process.stdout.write(`Pinned ${baseline.ref} at ${baseline.commit}.\n`);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
118
177
|
case ``:
|
|
119
178
|
case `$configPath`: {
|
|
120
179
|
const { returnValue } = await encapsulate(() => breakage(inputs.opts), {
|
package/dist/breakage.x.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"breakage.x.js","names":["path"],"sources":["../src/breakage.x.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport * as path from \"node:path\"\nimport { inspect } from \"node:util\"\n\nimport { type } from \"arktype\"\nimport type { OptionsGroup } from \"comline\"\nimport {\n\tcli,\n\tcompletionResponse,\n\tencapsulate,\n\thelp,\n\tlogWarnings,\n\tnoOptions,\n\toptional,\n\toptions,\n\tparseBooleanOption,\n} from \"comline\"\nimport logger from \"takua\"\n\nimport type { BreakageOptions } from \"./breakage.ts\"\nimport { breakage } from \"./breakage.ts\"\n\nconst BREAKAGE_MANUAL = options(\n\t`Check for breaking changes in a package.`,\n\ttype({\n\t\t\"tagPattern?\": `string`,\n\t\ttestPattern: `string`,\n\t\ttestCommand: `string`,\n\t\tcertifyCommand: `string`,\n\t\t\"baseDirname?\": `string`,\n\t\t\"verbose?\": `boolean`,\n\t}),\n\t{\n\t\ttagPattern: {\n\t\t\taliases: [`tag-pattern`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `g`,\n\t\t\trequired: false,\n\t\t\tdescription: `RegExp which, if found matched to a git tag, will be considered a release tag for your library.`,\n\t\t\texample: `--tagPattern=\"my-library\"`,\n\t\t},\n\t\ttestPattern: {\n\t\t\taliases: [`test-pattern`, `pattern`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `p`,\n\t\t\trequired: true,\n\t\t\tdescription: `The pattern to match test files that test the public API of the library.`,\n\t\t\texample: `--pattern=\"*__public.test.ts\"`,\n\t\t},\n\t\ttestCommand: {\n\t\t\taliases: [`test-command`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `t`,\n\t\t\trequired: true,\n\t\t\tdescription: `Complete bash command that runs the tests for the library's public API.`,\n\t\t\texample: `--testCommand=\"npm run test\"`,\n\t\t},\n\t\tcertifyCommand: {\n\t\t\taliases: [`certify-command`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `c`,\n\t\t\trequired: true,\n\t\t\tdescription: `Complete bash command that determines whether a major version bump for your package is indicated in the workspace. Exit code 0 indicates that a major version bump is indicated, and exit code 1 indicates that no major version bump is indicated.`,\n\t\t\texample: `--certifyCommand=\\tsx scripts/certify-major-version-bump.node`,\n\t\t},\n\t\tbaseDirname: {\n\t\t\taliases: [`base-dir`, `base-dirname`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `directories` },\n\t\t\tflag: `d`,\n\t\t\trequired: false,\n\t\t\tdescription: `The base directory to run the command from.`,\n\t\t\texample: `--baseDirname=./packages/my-package`,\n\t\t},\n\t\tverbose: {\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `v`,\n\t\t\trequired: false,\n\t\t\tdescription: `Prints out more information about the process.`,\n\t\t\texample: `--verbose`,\n\t\t\tparse: parseBooleanOption,\n\t\t},\n\t},\n) satisfies OptionsGroup<BreakageOptions>\n\nconst SCHEMA_MANUAL = options(\n\t`Create a copy of the schema for configuring breakage.`,\n\ttype({ \"outdir?\": `string` }),\n\t{\n\t\toutdir: {\n\t\t\taliases: [`out-dir`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `directories` },\n\t\t\tflag: `o`,\n\t\t\trequired: false,\n\t\t\tdescription: `Directory to write the schema to.`,\n\t\t\texample: `--outdir=./dist`,\n\t\t},\n\t},\n)\n\nconst parse = cli(\n\t{\n\t\tcliName: `breakage`,\n\t\troutes: optional({ help: null, schema: null, $configPath: null }),\n\t\trouteOptions: {\n\t\t\t\"\": BREAKAGE_MANUAL,\n\t\t\t$configPath: BREAKAGE_MANUAL,\n\t\t\thelp: noOptions(`Show usage.`),\n\t\t\tschema: SCHEMA_MANUAL,\n\t\t},\n\t\tpositionalCompletions: { $configPath: { fileSystem: `files` } },\n\t\tdiscoverConfigPath: (args: readonly string[]) => {\n\t\t\tif (args[0] === `help` || args[0] === `schema`) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst configPath =\n\t\t\t\targs[0] ?? path.join(process.cwd(), `breakage.config.json`)\n\t\t\treturn configPath\n\t\t},\n\t},\n\t{\n\t\tinfo: (message, ...data) => logger.info(`breakage`, message, data),\n\t\terror: (message, ...data) => logger.error(`breakage`, message, data),\n\t},\n)\n\nconst completion = await completionResponse(parse.definition, process.argv)\nif (completion !== undefined) {\n\tprocess.stdout.write(completion)\n} else {\n\tconst { inputs, warnings, writeJsonSchema } = parse(process.argv)\n\tlogWarnings(warnings)\n\n\tswitch (inputs.case) {\n\t\tcase `help`:\n\t\t\tconsole.log(help(parse.definition))\n\t\t\tbreak\n\t\tcase `schema`:\n\t\t\t{\n\t\t\t\tconst { outdir } = inputs.opts\n\t\t\t\twriteJsonSchema(outdir ?? `.`)\n\t\t\t\tprocess.stdout.write(`📝 Wrote json.schema.`)\n\t\t\t}\n\t\t\tbreak\n\t\tcase ``:\n\t\tcase `$configPath`: {\n\t\t\tconst { returnValue } = await encapsulate(() => breakage(inputs.opts), {\n\t\t\t\tconsole: true,\n\t\t\t\tstdout: true,\n\t\t\t})\n\t\t\tif (`testResult` in returnValue && returnValue.breakingChangesFound) {\n\t\t\t\tprocess.stdout.write(returnValue.testResult)\n\t\t\t}\n\t\t\tif (`breakingChangesFound` in returnValue) {\n\t\t\t\tif (returnValue.breakingChangesFound) {\n\t\t\t\t\tif (returnValue.breakingChangesCertified) {\n\t\t\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t\t\t`👷 Breaking changes were found and certified.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tprocess.exit(0)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t\t\t`❌ Breaking changes were found, but not certified.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tprocess.exit(1)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tprocess.stdout.write(`✅ No breaking changes were found.`)\n\t\t\t\t\tprocess.exit(0)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t`💥 Breakage failed to determine breaking changes.` +\n\t\t\t\t\t\t`\\n` +\n\t\t\t\t\t\tinspect(returnValue),\n\t\t\t\t)\n\t\t\t\tprocess.exit(2)\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"breakage.x.js","names":["path"],"sources":["../src/breakage.x.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport * as path from \"node:path\"\nimport { inspect } from \"node:util\"\n\nimport { type } from \"arktype\"\nimport type { OptionsGroup } from \"comline\"\nimport {\n\tcli,\n\tcompletionResponse,\n\tencapsulate,\n\thelp,\n\tlogWarnings,\n\tnoOptions,\n\toptional,\n\toptions,\n\tparseBooleanOption,\n} from \"comline\"\nimport logger from \"takua\"\n\nimport type { BreakageOptions } from \"./breakage.ts\"\nimport { breakage, breakagePrelude } from \"./breakage.ts\"\n\nconst BREAKAGE_MANUAL = options(\n\t`Check for breaking changes in a package.`,\n\ttype({\n\t\t\"tagPattern?\": `string`,\n\t\t\"baselineFile?\": `string`,\n\t\ttestPattern: `string`,\n\t\ttestCommand: `string`,\n\t\tcertifyCommand: `string`,\n\t\t\"baseDirname?\": `string`,\n\t\t\"verbose?\": `boolean`,\n\t}),\n\t{\n\t\ttagPattern: {\n\t\t\taliases: [`tag-pattern`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `g`,\n\t\t\trequired: false,\n\t\t\tdescription: `RegExp which, if found matched to a git tag, will be considered a release tag for your library.`,\n\t\t\texample: `--tagPattern=\"my-library\"`,\n\t\t},\n\t\tbaselineFile: {\n\t\t\taliases: [`baseline-file`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `files` },\n\t\t\trequired: false,\n\t\t\tdescription: `Use the exact locally fetched commit from a prelude snapshot instead of discovering remote tags.`,\n\t\t\texample: `--baseline-file=.breakage/baseline.json`,\n\t\t},\n\t\ttestPattern: {\n\t\t\taliases: [`test-pattern`, `pattern`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `p`,\n\t\t\trequired: true,\n\t\t\tdescription: `The pattern to match test files that test the public API of the library.`,\n\t\t\texample: `--pattern=\"*__public.test.ts\"`,\n\t\t},\n\t\ttestCommand: {\n\t\t\taliases: [`test-command`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `t`,\n\t\t\trequired: true,\n\t\t\tdescription: `Complete bash command that runs the tests for the library's public API.`,\n\t\t\texample: `--testCommand=\"npm run test\"`,\n\t\t},\n\t\tcertifyCommand: {\n\t\t\taliases: [`certify-command`],\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `c`,\n\t\t\trequired: true,\n\t\t\tdescription: `Complete bash command that determines whether a major version bump for your package is indicated in the workspace. Exit code 0 indicates that a major version bump is indicated, and exit code 1 indicates that no major version bump is indicated.`,\n\t\t\texample: `--certifyCommand=\\tsx scripts/certify-major-version-bump.node`,\n\t\t},\n\t\tbaseDirname: {\n\t\t\taliases: [`base-dir`, `base-dirname`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `directories` },\n\t\t\tflag: `d`,\n\t\t\trequired: false,\n\t\t\tdescription: `The base directory to run the command from.`,\n\t\t\texample: `--baseDirname=./packages/my-package`,\n\t\t},\n\t\tverbose: {\n\t\t\tcompletion: { repeatable: false },\n\t\t\tflag: `v`,\n\t\t\trequired: false,\n\t\t\tdescription: `Prints out more information about the process.`,\n\t\t\texample: `--verbose`,\n\t\t\tparse: parseBooleanOption,\n\t\t},\n\t},\n) satisfies OptionsGroup<BreakageOptions>\n\nconst PRELUDE_MANUAL = options(\n\t`Resolve and fetch the release baseline for a cacheable compatibility check.`,\n\ttype({\n\t\t\"tagPattern?\": `string`,\n\t\t\"baseDirname?\": `string`,\n\t\t\"out?\": `string`,\n\t\t\"testPattern?\": `string`,\n\t\t\"testCommand?\": `string`,\n\t\t\"certifyCommand?\": `string`,\n\t\t\"baselineFile?\": `string`,\n\t\t\"verbose?\": `boolean`,\n\t}),\n\t{\n\t\t...BREAKAGE_MANUAL.optionConfigs,\n\t\ttestPattern: {\n\t\t\t...BREAKAGE_MANUAL.optionConfigs.testPattern,\n\t\t\trequired: false,\n\t\t},\n\t\ttestCommand: {\n\t\t\t...BREAKAGE_MANUAL.optionConfigs.testCommand,\n\t\t\trequired: false,\n\t\t},\n\t\tcertifyCommand: {\n\t\t\t...BREAKAGE_MANUAL.optionConfigs.certifyCommand,\n\t\t\trequired: false,\n\t\t},\n\t\tout: {\n\t\t\tcompletion: { repeatable: false, fileSystem: `files` },\n\t\t\trequired: false,\n\t\t\tdescription: `Gitignored snapshot path relative to the base directory (default: .breakage/baseline.json).`,\n\t\t\texample: `--out=.breakage/baseline.json`,\n\t\t},\n\t},\n)\n\nconst SCHEMA_MANUAL = options(\n\t`Create a copy of the schema for configuring breakage.`,\n\ttype({ \"outdir?\": `string` }),\n\t{\n\t\toutdir: {\n\t\t\taliases: [`out-dir`],\n\t\t\tcompletion: { repeatable: false, fileSystem: `directories` },\n\t\t\tflag: `o`,\n\t\t\trequired: false,\n\t\t\tdescription: `Directory to write the schema to.`,\n\t\t\texample: `--outdir=./dist`,\n\t\t},\n\t},\n)\n\nconst parse = cli(\n\t{\n\t\tcliName: `breakage`,\n\t\troutes: optional({\n\t\t\thelp: null,\n\t\t\tschema: null,\n\t\t\tprelude: optional({ $configPath: null }),\n\t\t\t$configPath: null,\n\t\t}),\n\t\trouteOptions: {\n\t\t\t\"\": BREAKAGE_MANUAL,\n\t\t\t$configPath: BREAKAGE_MANUAL,\n\t\t\thelp: noOptions(`Show usage.`),\n\t\t\tschema: SCHEMA_MANUAL,\n\t\t\tprelude: PRELUDE_MANUAL,\n\t\t\t\"prelude/$configPath\": PRELUDE_MANUAL,\n\t\t},\n\t\tpositionalCompletions: {\n\t\t\t$configPath: { fileSystem: `files` },\n\t\t\t\"prelude/$configPath\": { fileSystem: `files` },\n\t\t},\n\t\tdiscoverConfigPath: (args: readonly string[]) => {\n\t\t\tif (args[0] === `help` || args[0] === `schema`) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst configPath =\n\t\t\t\t(args[0] === `prelude` ? args[1] : args[0]) ??\n\t\t\t\tpath.join(process.cwd(), `breakage.config.json`)\n\t\t\treturn configPath\n\t\t},\n\t},\n\t{\n\t\tinfo: (message, ...data) => logger.info(`breakage`, message, data),\n\t\terror: (message, ...data) => logger.error(`breakage`, message, data),\n\t},\n)\n\nconst completion = await completionResponse(parse.definition, process.argv)\nif (completion !== undefined) {\n\tprocess.stdout.write(completion)\n} else {\n\tconst { inputs, warnings, writeJsonSchema } = parse(process.argv)\n\tlogWarnings(warnings)\n\n\tswitch (inputs.case) {\n\t\tcase `help`:\n\t\t\tconsole.log(help(parse.definition))\n\t\t\tbreak\n\t\tcase `schema`:\n\t\t\t{\n\t\t\t\tconst { outdir } = inputs.opts\n\t\t\t\twriteJsonSchema(outdir ?? `.`)\n\t\t\t\tprocess.stdout.write(`📝 Wrote json.schema.`)\n\t\t\t}\n\t\t\tbreak\n\t\tcase `prelude`:\n\t\tcase `prelude/$configPath`: {\n\t\t\tconst baseline = await breakagePrelude({\n\t\t\t\t...inputs.opts,\n\t\t\t\tout: inputs.opts.out ?? `.breakage/baseline.json`,\n\t\t\t})\n\t\t\tprocess.stdout.write(`Pinned ${baseline.ref} at ${baseline.commit}.\\n`)\n\t\t\tbreak\n\t\t}\n\t\tcase ``:\n\t\tcase `$configPath`: {\n\t\t\tconst { returnValue } = await encapsulate(() => breakage(inputs.opts), {\n\t\t\t\tconsole: true,\n\t\t\t\tstdout: true,\n\t\t\t})\n\t\t\tif (`testResult` in returnValue && returnValue.breakingChangesFound) {\n\t\t\t\tprocess.stdout.write(returnValue.testResult)\n\t\t\t}\n\t\t\tif (`breakingChangesFound` in returnValue) {\n\t\t\t\tif (returnValue.breakingChangesFound) {\n\t\t\t\t\tif (returnValue.breakingChangesCertified) {\n\t\t\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t\t\t`👷 Breaking changes were found and certified.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tprocess.exit(0)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t\t\t`❌ Breaking changes were found, but not certified.`,\n\t\t\t\t\t\t)\n\t\t\t\t\t\tprocess.exit(1)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tprocess.stdout.write(`✅ No breaking changes were found.`)\n\t\t\t\t\tprocess.exit(0)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tprocess.stdout.write(\n\t\t\t\t\t`💥 Breakage failed to determine breaking changes.` +\n\t\t\t\t\t\t`\\n` +\n\t\t\t\t\t\tinspect(returnValue),\n\t\t\t\t)\n\t\t\t\tprocess.exit(2)\n\t\t\t}\n\t\t}\n\t}\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,52 +1,51 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
2
|
+
"name": "breakage",
|
|
3
|
+
"version": "0.8.2",
|
|
4
|
+
"description": "Run yesterday’s tests against tomorrow’s release.",
|
|
5
|
+
"homepage": "https://github.com/jeremybanka/breakage#readme",
|
|
6
|
+
"bugs": "https://github.com/jeremybanka/breakage/issues",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/jeremybanka/breakage.git",
|
|
10
|
+
"directory": "packages/breakage"
|
|
11
|
+
},
|
|
12
|
+
"license": "MPL-2.0",
|
|
13
|
+
"author": {
|
|
14
|
+
"name": "Jeremy Banka",
|
|
15
|
+
"email": "hello@jeremybanka.com"
|
|
16
|
+
},
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "dist/breakage.js",
|
|
19
|
+
"types": "dist/breakage.d.ts",
|
|
20
|
+
"bin": {
|
|
21
|
+
"breakage": "./dist/breakage.x.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"arktype": "2.2.8",
|
|
29
|
+
"comline": "0.9.0",
|
|
30
|
+
"minimatch": "10.2.6",
|
|
31
|
+
"simple-git": "4.0.2",
|
|
32
|
+
"takua": "0.2.3"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/bun": "npm:bun-types@1.4.3",
|
|
36
|
+
"typescript": "7.0.2"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "vp pack && pnpm run schema",
|
|
43
|
+
"check": "pnpm run check:tsc",
|
|
44
|
+
"check:tsc": "tsc --noEmit",
|
|
45
|
+
"postversion": "dprint fmt package.json",
|
|
46
|
+
"schema": "bun src/breakage.x.ts --outdir=dist -- schema",
|
|
47
|
+
"pretest": "pnpm run build",
|
|
48
|
+
"test": "bun test -- ./__tests__",
|
|
49
|
+
"test:watch": "bun test --watch -- ./__tests__"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/baseline.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto"
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
|
|
5
|
+
import type { SimpleGit } from "simple-git"
|
|
6
|
+
import { simpleGit } from "simple-git"
|
|
7
|
+
|
|
8
|
+
import { withDirectoryLock } from "./directory-lock.ts"
|
|
9
|
+
import { latestReleaseTag } from "./release-tag.ts"
|
|
10
|
+
|
|
11
|
+
export type BaselineOptions = {
|
|
12
|
+
tagPattern?: string | undefined
|
|
13
|
+
baseDirname?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type BreakagePreludeOptions = BaselineOptions & { out: string }
|
|
17
|
+
|
|
18
|
+
export type BreakageBaseline = {
|
|
19
|
+
version: 1
|
|
20
|
+
tagPattern: string | null
|
|
21
|
+
packageDirectory: string
|
|
22
|
+
ref: string
|
|
23
|
+
commit: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const objectId = /^[a-f0-9]{40}(?:[a-f0-9]{24})?$/
|
|
27
|
+
|
|
28
|
+
function isBaseline(value: unknown): value is BreakageBaseline {
|
|
29
|
+
if (typeof value !== `object` || value === null) return false
|
|
30
|
+
const record = value as Partial<BreakageBaseline>
|
|
31
|
+
return (
|
|
32
|
+
record.version === 1 &&
|
|
33
|
+
(record.tagPattern === null || typeof record.tagPattern === `string`) &&
|
|
34
|
+
typeof record.packageDirectory === `string` &&
|
|
35
|
+
typeof record.ref === `string` &&
|
|
36
|
+
record.ref.startsWith(`refs/tags/`) &&
|
|
37
|
+
typeof record.commit === `string` &&
|
|
38
|
+
objectId.test(record.commit)
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Resolve and fetch a release without running tests or changing the worktree. */
|
|
43
|
+
export async function breakagePrelude({
|
|
44
|
+
out,
|
|
45
|
+
tagPattern,
|
|
46
|
+
baseDirname = process.cwd(),
|
|
47
|
+
}: BreakagePreludeOptions): Promise<BreakageBaseline> {
|
|
48
|
+
const filename = path.resolve(baseDirname, out)
|
|
49
|
+
const temporaryPath = `${filename}.${randomUUID()}.tmp`
|
|
50
|
+
let canInvalidate = false
|
|
51
|
+
try {
|
|
52
|
+
const git = simpleGit(baseDirname)
|
|
53
|
+
// An ignored, untracked snapshot cannot make the compatibility check dirty.
|
|
54
|
+
if (!(await git.raw([`check-ignore`, `--`, filename])).trim()) {
|
|
55
|
+
throw new Error(`Baseline output must be gitignored: ${filename}.`)
|
|
56
|
+
}
|
|
57
|
+
canInvalidate = true
|
|
58
|
+
const remoteTags = await git.listRemote([`--tags`, `origin`])
|
|
59
|
+
const ref = latestReleaseTag(remoteTags, tagPattern)
|
|
60
|
+
if (!ref) {
|
|
61
|
+
throw new Error(`No tags found matching the pattern "${tagPattern}".`)
|
|
62
|
+
}
|
|
63
|
+
const refs = new Map(
|
|
64
|
+
remoteTags
|
|
65
|
+
.trim()
|
|
66
|
+
.split(`\n`)
|
|
67
|
+
.map((line) => {
|
|
68
|
+
const [sha, name] = line.split(`\t`)
|
|
69
|
+
return [name, sha]
|
|
70
|
+
}),
|
|
71
|
+
)
|
|
72
|
+
// ls-remote supplies the peeled commit for annotated tags.
|
|
73
|
+
const commit = refs.get(`${ref}^{}`) ?? refs.get(ref)
|
|
74
|
+
if (!commit || !objectId.test(commit)) {
|
|
75
|
+
throw new Error(`Could not resolve the release commit for ${ref}.`)
|
|
76
|
+
}
|
|
77
|
+
const commonGitDirectory = await git.revparse([
|
|
78
|
+
`--path-format=absolute`,
|
|
79
|
+
`--git-common-dir`,
|
|
80
|
+
])
|
|
81
|
+
await withDirectoryLock(
|
|
82
|
+
path.join(commonGitDirectory, `breakage-fetch.lock`),
|
|
83
|
+
() => git.fetch([`origin`, commit, `--no-tags`, `--no-write-fetch-head`]),
|
|
84
|
+
)
|
|
85
|
+
if ((await git.raw([`cat-file`, `-t`, commit])).trim() !== `commit`) {
|
|
86
|
+
throw new Error(`Release ${ref} does not resolve to a commit.`)
|
|
87
|
+
}
|
|
88
|
+
const baseline: BreakageBaseline = {
|
|
89
|
+
version: 1,
|
|
90
|
+
tagPattern: tagPattern ?? null,
|
|
91
|
+
packageDirectory: await git.revparse([`--show-prefix`]),
|
|
92
|
+
ref,
|
|
93
|
+
commit,
|
|
94
|
+
}
|
|
95
|
+
await mkdir(path.dirname(filename), { recursive: true })
|
|
96
|
+
await writeFile(temporaryPath, `${JSON.stringify(baseline, null, 2)}\n`, {
|
|
97
|
+
flag: `wx`,
|
|
98
|
+
})
|
|
99
|
+
await rename(temporaryPath, filename)
|
|
100
|
+
return baseline
|
|
101
|
+
} catch (cause) {
|
|
102
|
+
// A failed refresh must never leave an older snapshot available as fallback.
|
|
103
|
+
if (canInvalidate) await rm(filename, { force: true })
|
|
104
|
+
throw new Error(`Failed to prepare baseline ${filename}.`, { cause })
|
|
105
|
+
} finally {
|
|
106
|
+
await rm(temporaryPath, { force: true })
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function readBaseline(
|
|
111
|
+
git: SimpleGit,
|
|
112
|
+
filename: string,
|
|
113
|
+
{ tagPattern }: BaselineOptions,
|
|
114
|
+
): Promise<BreakageBaseline> {
|
|
115
|
+
try {
|
|
116
|
+
const baseline: unknown = JSON.parse(await readFile(filename, `utf8`))
|
|
117
|
+
if (!isBaseline(baseline)) throw new Error(`Invalid baseline fields.`)
|
|
118
|
+
if (
|
|
119
|
+
baseline.tagPattern !== (tagPattern ?? null) ||
|
|
120
|
+
baseline.packageDirectory !== (await git.revparse([`--show-prefix`]))
|
|
121
|
+
) {
|
|
122
|
+
throw new Error(`Baseline belongs to a different package or tag pattern.`)
|
|
123
|
+
}
|
|
124
|
+
if (
|
|
125
|
+
(
|
|
126
|
+
await git.raw([`check-ref-format`, `--normalize`, baseline.ref])
|
|
127
|
+
).trim() !== baseline.ref
|
|
128
|
+
) {
|
|
129
|
+
throw new Error(`Invalid release ref.`)
|
|
130
|
+
}
|
|
131
|
+
if (
|
|
132
|
+
(await git.raw([`cat-file`, `-t`, baseline.commit])).trim() !== `commit`
|
|
133
|
+
) {
|
|
134
|
+
throw new Error(`Pinned object is not a locally available commit.`)
|
|
135
|
+
}
|
|
136
|
+
return baseline
|
|
137
|
+
} catch (cause) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`Missing, invalid, or unusable baseline ${filename}. Run breakage prelude again.`,
|
|
140
|
+
{ cause },
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
package/src/breakage.ts
CHANGED
|
@@ -6,12 +6,17 @@ import { simpleGit } from "simple-git"
|
|
|
6
6
|
import type { Chronicle } from "takua"
|
|
7
7
|
import logger from "takua"
|
|
8
8
|
|
|
9
|
+
import { readBaseline } from "./baseline.ts"
|
|
9
10
|
import { withDirectoryLock } from "./directory-lock.ts"
|
|
10
11
|
import { latestReleaseTag as selectLatestReleaseTag } from "./release-tag.ts"
|
|
11
12
|
import { withTestFileState } from "./test-file-state.ts"
|
|
12
13
|
|
|
14
|
+
export type { BreakageBaseline, BreakagePreludeOptions } from "./baseline.ts"
|
|
15
|
+
export { breakagePrelude } from "./baseline.ts"
|
|
16
|
+
|
|
13
17
|
export type BreakageOptions = {
|
|
14
18
|
tagPattern?: string | undefined
|
|
19
|
+
baselineFile?: string | undefined
|
|
15
20
|
testPattern: string
|
|
16
21
|
testCommand: string
|
|
17
22
|
certifyCommand: string
|
|
@@ -54,6 +59,7 @@ export type BreakageOutcome =
|
|
|
54
59
|
|
|
55
60
|
export async function breakage({
|
|
56
61
|
tagPattern,
|
|
62
|
+
baselineFile,
|
|
57
63
|
testPattern,
|
|
58
64
|
testCommand,
|
|
59
65
|
certifyCommand,
|
|
@@ -86,39 +92,53 @@ export async function breakage({
|
|
|
86
92
|
gitWasClean: false,
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
|
-
|
|
95
|
+
let latestReleaseTag: string | undefined
|
|
96
|
+
let releaseCommit: string
|
|
97
|
+
if (baselineFile !== undefined) {
|
|
98
|
+
const baseline = await readBaseline(
|
|
99
|
+
git,
|
|
100
|
+
path.resolve(baseDirname, baselineFile),
|
|
101
|
+
{ tagPattern },
|
|
102
|
+
)
|
|
103
|
+
latestReleaseTag = baseline.ref
|
|
104
|
+
releaseCommit = baseline.commit
|
|
105
|
+
mark?.(`read pinned release baseline`)
|
|
106
|
+
} else {
|
|
107
|
+
const tagsRemote = await git.listRemote([`--tags`, `origin`])
|
|
90
108
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
109
|
+
mark?.(`list remote tags`)
|
|
110
|
+
latestReleaseTag = selectLatestReleaseTag(tagsRemote, tagPattern)
|
|
111
|
+
mark?.(`found latest release tag`)
|
|
112
|
+
if (!latestReleaseTag) {
|
|
113
|
+
return {
|
|
114
|
+
summary: `No tags found matching the pattern "${tagPattern}".`,
|
|
115
|
+
gitWasClean: true,
|
|
116
|
+
lastReleaseFound: false,
|
|
117
|
+
} as const
|
|
118
|
+
}
|
|
119
|
+
const commonGitDirectory = await git.revparse([
|
|
120
|
+
`--path-format=absolute`,
|
|
121
|
+
`--git-common-dir`,
|
|
122
|
+
])
|
|
123
|
+
await withDirectoryLock(
|
|
124
|
+
path.join(commonGitDirectory, `breakage-fetch.lock`),
|
|
125
|
+
() =>
|
|
126
|
+
git.fetch([
|
|
127
|
+
`origin`,
|
|
128
|
+
`${latestReleaseTag}:${latestReleaseTag}`,
|
|
129
|
+
`--no-tags`,
|
|
130
|
+
`--no-write-fetch-head`,
|
|
131
|
+
]),
|
|
132
|
+
)
|
|
115
133
|
|
|
116
|
-
|
|
134
|
+
releaseCommit = await git.revparse([`${latestReleaseTag}^{commit}`])
|
|
135
|
+
mark?.(`fetched latest release tag`)
|
|
136
|
+
}
|
|
117
137
|
|
|
118
138
|
let productionFiles: string[]
|
|
119
139
|
try {
|
|
120
140
|
productionFiles = (
|
|
121
|
-
await git.raw([`ls-tree`, `-r`, `--name-only`, `-z`,
|
|
141
|
+
await git.raw([`ls-tree`, `-r`, `--name-only`, `-z`, releaseCommit])
|
|
122
142
|
)
|
|
123
143
|
.split(`\0`)
|
|
124
144
|
.filter(Boolean)
|
|
@@ -164,10 +184,7 @@ export async function breakage({
|
|
|
164
184
|
testsWereFound: false,
|
|
165
185
|
} as const
|
|
166
186
|
}
|
|
167
|
-
const restore = await state.replaceTests(
|
|
168
|
-
latestReleaseTag,
|
|
169
|
-
productionTestFiles,
|
|
170
|
-
)
|
|
187
|
+
const restore = await state.replaceTests(releaseCommit, productionTestFiles)
|
|
171
188
|
mark?.(`restored public tests from latest release tag`)
|
|
172
189
|
return { latestReleaseTag, productionTestFiles, restore }
|
|
173
190
|
})
|
package/src/breakage.x.ts
CHANGED
|
@@ -19,12 +19,13 @@ import {
|
|
|
19
19
|
import logger from "takua"
|
|
20
20
|
|
|
21
21
|
import type { BreakageOptions } from "./breakage.ts"
|
|
22
|
-
import { breakage } from "./breakage.ts"
|
|
22
|
+
import { breakage, breakagePrelude } from "./breakage.ts"
|
|
23
23
|
|
|
24
24
|
const BREAKAGE_MANUAL = options(
|
|
25
25
|
`Check for breaking changes in a package.`,
|
|
26
26
|
type({
|
|
27
27
|
"tagPattern?": `string`,
|
|
28
|
+
"baselineFile?": `string`,
|
|
28
29
|
testPattern: `string`,
|
|
29
30
|
testCommand: `string`,
|
|
30
31
|
certifyCommand: `string`,
|
|
@@ -40,6 +41,13 @@ const BREAKAGE_MANUAL = options(
|
|
|
40
41
|
description: `RegExp which, if found matched to a git tag, will be considered a release tag for your library.`,
|
|
41
42
|
example: `--tagPattern="my-library"`,
|
|
42
43
|
},
|
|
44
|
+
baselineFile: {
|
|
45
|
+
aliases: [`baseline-file`],
|
|
46
|
+
completion: { repeatable: false, fileSystem: `files` },
|
|
47
|
+
required: false,
|
|
48
|
+
description: `Use the exact locally fetched commit from a prelude snapshot instead of discovering remote tags.`,
|
|
49
|
+
example: `--baseline-file=.breakage/baseline.json`,
|
|
50
|
+
},
|
|
43
51
|
testPattern: {
|
|
44
52
|
aliases: [`test-pattern`, `pattern`],
|
|
45
53
|
completion: { repeatable: false },
|
|
@@ -83,6 +91,41 @@ const BREAKAGE_MANUAL = options(
|
|
|
83
91
|
},
|
|
84
92
|
) satisfies OptionsGroup<BreakageOptions>
|
|
85
93
|
|
|
94
|
+
const PRELUDE_MANUAL = options(
|
|
95
|
+
`Resolve and fetch the release baseline for a cacheable compatibility check.`,
|
|
96
|
+
type({
|
|
97
|
+
"tagPattern?": `string`,
|
|
98
|
+
"baseDirname?": `string`,
|
|
99
|
+
"out?": `string`,
|
|
100
|
+
"testPattern?": `string`,
|
|
101
|
+
"testCommand?": `string`,
|
|
102
|
+
"certifyCommand?": `string`,
|
|
103
|
+
"baselineFile?": `string`,
|
|
104
|
+
"verbose?": `boolean`,
|
|
105
|
+
}),
|
|
106
|
+
{
|
|
107
|
+
...BREAKAGE_MANUAL.optionConfigs,
|
|
108
|
+
testPattern: {
|
|
109
|
+
...BREAKAGE_MANUAL.optionConfigs.testPattern,
|
|
110
|
+
required: false,
|
|
111
|
+
},
|
|
112
|
+
testCommand: {
|
|
113
|
+
...BREAKAGE_MANUAL.optionConfigs.testCommand,
|
|
114
|
+
required: false,
|
|
115
|
+
},
|
|
116
|
+
certifyCommand: {
|
|
117
|
+
...BREAKAGE_MANUAL.optionConfigs.certifyCommand,
|
|
118
|
+
required: false,
|
|
119
|
+
},
|
|
120
|
+
out: {
|
|
121
|
+
completion: { repeatable: false, fileSystem: `files` },
|
|
122
|
+
required: false,
|
|
123
|
+
description: `Gitignored snapshot path relative to the base directory (default: .breakage/baseline.json).`,
|
|
124
|
+
example: `--out=.breakage/baseline.json`,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
)
|
|
128
|
+
|
|
86
129
|
const SCHEMA_MANUAL = options(
|
|
87
130
|
`Create a copy of the schema for configuring breakage.`,
|
|
88
131
|
type({ "outdir?": `string` }),
|
|
@@ -101,20 +144,31 @@ const SCHEMA_MANUAL = options(
|
|
|
101
144
|
const parse = cli(
|
|
102
145
|
{
|
|
103
146
|
cliName: `breakage`,
|
|
104
|
-
routes: optional({
|
|
147
|
+
routes: optional({
|
|
148
|
+
help: null,
|
|
149
|
+
schema: null,
|
|
150
|
+
prelude: optional({ $configPath: null }),
|
|
151
|
+
$configPath: null,
|
|
152
|
+
}),
|
|
105
153
|
routeOptions: {
|
|
106
154
|
"": BREAKAGE_MANUAL,
|
|
107
155
|
$configPath: BREAKAGE_MANUAL,
|
|
108
156
|
help: noOptions(`Show usage.`),
|
|
109
157
|
schema: SCHEMA_MANUAL,
|
|
158
|
+
prelude: PRELUDE_MANUAL,
|
|
159
|
+
"prelude/$configPath": PRELUDE_MANUAL,
|
|
160
|
+
},
|
|
161
|
+
positionalCompletions: {
|
|
162
|
+
$configPath: { fileSystem: `files` },
|
|
163
|
+
"prelude/$configPath": { fileSystem: `files` },
|
|
110
164
|
},
|
|
111
|
-
positionalCompletions: { $configPath: { fileSystem: `files` } },
|
|
112
165
|
discoverConfigPath: (args: readonly string[]) => {
|
|
113
166
|
if (args[0] === `help` || args[0] === `schema`) {
|
|
114
167
|
return
|
|
115
168
|
}
|
|
116
169
|
const configPath =
|
|
117
|
-
args[0]
|
|
170
|
+
(args[0] === `prelude` ? args[1] : args[0]) ??
|
|
171
|
+
path.join(process.cwd(), `breakage.config.json`)
|
|
118
172
|
return configPath
|
|
119
173
|
},
|
|
120
174
|
},
|
|
@@ -142,6 +196,15 @@ if (completion !== undefined) {
|
|
|
142
196
|
process.stdout.write(`📝 Wrote json.schema.`)
|
|
143
197
|
}
|
|
144
198
|
break
|
|
199
|
+
case `prelude`:
|
|
200
|
+
case `prelude/$configPath`: {
|
|
201
|
+
const baseline = await breakagePrelude({
|
|
202
|
+
...inputs.opts,
|
|
203
|
+
out: inputs.opts.out ?? `.breakage/baseline.json`,
|
|
204
|
+
})
|
|
205
|
+
process.stdout.write(`Pinned ${baseline.ref} at ${baseline.commit}.\n`)
|
|
206
|
+
break
|
|
207
|
+
}
|
|
145
208
|
case ``:
|
|
146
209
|
case `$configPath`: {
|
|
147
210
|
const { returnValue } = await encapsulate(() => breakage(inputs.opts), {
|