@contractual/cli 0.1.0-dev.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/LICENSE +21 -0
- package/bin/cli.js +2 -0
- package/dist/commands/breaking.command.d.ts +19 -0
- package/dist/commands/breaking.command.d.ts.map +1 -0
- package/dist/commands/breaking.command.js +138 -0
- package/dist/commands/changeset.command.d.ts +11 -0
- package/dist/commands/changeset.command.d.ts.map +1 -0
- package/dist/commands/changeset.command.js +65 -0
- package/dist/commands/contract.command.d.ts +34 -0
- package/dist/commands/contract.command.d.ts.map +1 -0
- package/dist/commands/contract.command.js +259 -0
- package/dist/commands/diff.command.d.ts +21 -0
- package/dist/commands/diff.command.d.ts.map +1 -0
- package/dist/commands/diff.command.js +58 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +6 -0
- package/dist/commands/init.command.d.ts +21 -0
- package/dist/commands/init.command.d.ts.map +1 -0
- package/dist/commands/init.command.js +294 -0
- package/dist/commands/lint.command.d.ts +16 -0
- package/dist/commands/lint.command.d.ts.map +1 -0
- package/dist/commands/lint.command.js +174 -0
- package/dist/commands/pre.command.d.ts +14 -0
- package/dist/commands/pre.command.d.ts.map +1 -0
- package/dist/commands/pre.command.js +141 -0
- package/dist/commands/status.command.d.ts +5 -0
- package/dist/commands/status.command.d.ts.map +1 -0
- package/dist/commands/status.command.js +120 -0
- package/dist/commands/version.command.d.ts +16 -0
- package/dist/commands/version.command.d.ts.map +1 -0
- package/dist/commands/version.command.js +247 -0
- package/dist/commands.d.ts +2 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +84 -0
- package/dist/config/index.d.ts +3 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +2 -0
- package/dist/config/loader.d.ts +28 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +123 -0
- package/dist/config/schema.json +121 -0
- package/dist/config/validator.d.ts +28 -0
- package/dist/config/validator.d.ts.map +1 -0
- package/dist/config/validator.js +34 -0
- package/dist/core/diff.d.ts +26 -0
- package/dist/core/diff.d.ts.map +1 -0
- package/dist/core/diff.js +89 -0
- package/dist/formatters/diff.d.ts +31 -0
- package/dist/formatters/diff.d.ts.map +1 -0
- package/dist/formatters/diff.js +139 -0
- package/dist/governance/index.d.ts +11 -0
- package/dist/governance/index.d.ts.map +1 -0
- package/dist/governance/index.js +14 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/utils/exec.d.ts +29 -0
- package/dist/utils/exec.d.ts.map +1 -0
- package/dist/utils/exec.js +66 -0
- package/dist/utils/files.d.ts +36 -0
- package/dist/utils/files.d.ts.map +1 -0
- package/dist/utils/files.js +137 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/output.d.ts +25 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +73 -0
- package/dist/utils/prompts.d.ts +90 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +119 -0
- package/package.json +81 -0
- package/src/config/schema.json +121 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (C) 2025 Omer Morad
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breaking Command
|
|
3
|
+
*
|
|
4
|
+
* Detect breaking changes against snapshots.
|
|
5
|
+
* This is a CI gate — exits 1 if breaking changes are found.
|
|
6
|
+
*
|
|
7
|
+
* Uses the shared diffContracts() function internally.
|
|
8
|
+
*/
|
|
9
|
+
interface BreakingOptions {
|
|
10
|
+
contract?: string;
|
|
11
|
+
format?: 'text' | 'json';
|
|
12
|
+
failOn?: 'breaking' | 'non-breaking' | 'any';
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Detect breaking changes against snapshots
|
|
16
|
+
*/
|
|
17
|
+
export declare function breakingCommand(options: BreakingOptions): Promise<void>;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=breaking.command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"breaking.command.d.ts","sourceRoot":"","sources":["../../src/commands/breaking.command.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AASH,UAAU,eAAe;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,UAAU,GAAG,cAAc,GAAG,KAAK,CAAC;CAC9C;AAOD;;GAEG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAgE7E"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Breaking Command
|
|
3
|
+
*
|
|
4
|
+
* Detect breaking changes against snapshots.
|
|
5
|
+
* This is a CI gate — exits 1 if breaking changes are found.
|
|
6
|
+
*
|
|
7
|
+
* Uses the shared diffContracts() function internally.
|
|
8
|
+
*/
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
import { loadConfig } from '../config/index.js';
|
|
12
|
+
import { diffContracts } from '../core/diff.js';
|
|
13
|
+
import { formatSeverity } from '../utils/output.js';
|
|
14
|
+
/**
|
|
15
|
+
* Detect breaking changes against snapshots
|
|
16
|
+
*/
|
|
17
|
+
export async function breakingCommand(options) {
|
|
18
|
+
const spinner = ora('Loading configuration...').start();
|
|
19
|
+
let config;
|
|
20
|
+
try {
|
|
21
|
+
config = loadConfig();
|
|
22
|
+
spinner.succeed('Configuration loaded');
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
spinner.fail('Failed to load configuration');
|
|
26
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
27
|
+
console.error(chalk.red(message));
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const checkSpinner = ora('Checking for breaking changes...').start();
|
|
32
|
+
try {
|
|
33
|
+
const { results } = await diffContracts(config, {
|
|
34
|
+
contracts: options.contract ? [options.contract] : undefined,
|
|
35
|
+
includeEmpty: true,
|
|
36
|
+
});
|
|
37
|
+
const hasBreaking = results.some((r) => r.summary.breaking > 0);
|
|
38
|
+
if (hasBreaking) {
|
|
39
|
+
checkSpinner.fail('Breaking changes detected');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
checkSpinner.succeed('No breaking changes');
|
|
43
|
+
}
|
|
44
|
+
// Output results
|
|
45
|
+
console.log();
|
|
46
|
+
if (options.format === 'json') {
|
|
47
|
+
const output = { hasBreaking, results };
|
|
48
|
+
console.log(JSON.stringify(output, null, 2));
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
printTextResults(results);
|
|
52
|
+
}
|
|
53
|
+
// Determine exit code based on --fail-on option
|
|
54
|
+
const failOn = options.failOn ?? 'breaking';
|
|
55
|
+
let shouldFail = false;
|
|
56
|
+
if (failOn === 'any') {
|
|
57
|
+
// Fail on any detected changes
|
|
58
|
+
shouldFail = results.some((r) => r.changes.length > 0);
|
|
59
|
+
}
|
|
60
|
+
else if (failOn === 'non-breaking') {
|
|
61
|
+
// Fail on non-breaking or breaking changes
|
|
62
|
+
shouldFail = results.some((r) => r.summary.breaking > 0 || r.summary.nonBreaking > 0);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Default: fail only on breaking changes
|
|
66
|
+
shouldFail = hasBreaking;
|
|
67
|
+
}
|
|
68
|
+
if (shouldFail) {
|
|
69
|
+
process.exitCode = 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
checkSpinner.fail('Failed to check for breaking changes');
|
|
74
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
75
|
+
console.error(chalk.red('Error:'), message);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Print results in human-readable text format
|
|
81
|
+
*/
|
|
82
|
+
function printTextResults(results) {
|
|
83
|
+
if (results.length === 0) {
|
|
84
|
+
console.log(chalk.gray('No contracts were checked.'));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
for (const result of results) {
|
|
88
|
+
console.log(chalk.bold.underline(result.contract));
|
|
89
|
+
console.log();
|
|
90
|
+
if (result.changes.length === 0) {
|
|
91
|
+
console.log(chalk.gray(' No changes detected.'));
|
|
92
|
+
console.log();
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// Group changes by severity
|
|
96
|
+
const breaking = result.changes.filter((c) => c.severity === 'breaking');
|
|
97
|
+
const nonBreaking = result.changes.filter((c) => c.severity === 'non-breaking');
|
|
98
|
+
const patch = result.changes.filter((c) => c.severity === 'patch');
|
|
99
|
+
const unknown = result.changes.filter((c) => c.severity === 'unknown');
|
|
100
|
+
// Print summary
|
|
101
|
+
console.log(` Summary: ` +
|
|
102
|
+
`${chalk.red(String(result.summary.breaking))} breaking, ` +
|
|
103
|
+
`${chalk.yellow(String(result.summary.nonBreaking))} non-breaking, ` +
|
|
104
|
+
`${chalk.green(String(result.summary.patch))} patch, ` +
|
|
105
|
+
`${chalk.gray(String(result.summary.unknown))} unknown`);
|
|
106
|
+
console.log(` Suggested bump: ${chalk.cyan(result.suggestedBump)}`);
|
|
107
|
+
console.log();
|
|
108
|
+
// Print changes by severity
|
|
109
|
+
if (breaking.length > 0) {
|
|
110
|
+
console.log(` ${formatSeverity('breaking')} Changes:`);
|
|
111
|
+
for (const change of breaking) {
|
|
112
|
+
console.log(` - ${change.path}: ${change.message}`);
|
|
113
|
+
}
|
|
114
|
+
console.log();
|
|
115
|
+
}
|
|
116
|
+
if (nonBreaking.length > 0) {
|
|
117
|
+
console.log(` ${formatSeverity('non-breaking')} Changes:`);
|
|
118
|
+
for (const change of nonBreaking) {
|
|
119
|
+
console.log(` - ${change.path}: ${change.message}`);
|
|
120
|
+
}
|
|
121
|
+
console.log();
|
|
122
|
+
}
|
|
123
|
+
if (patch.length > 0) {
|
|
124
|
+
console.log(` ${formatSeverity('patch')} Changes:`);
|
|
125
|
+
for (const change of patch) {
|
|
126
|
+
console.log(` - ${change.path}: ${change.message}`);
|
|
127
|
+
}
|
|
128
|
+
console.log();
|
|
129
|
+
}
|
|
130
|
+
if (unknown.length > 0) {
|
|
131
|
+
console.log(` ${formatSeverity('unknown')} Changes:`);
|
|
132
|
+
for (const change of unknown) {
|
|
133
|
+
console.log(` - ${change.path}: ${change.message}`);
|
|
134
|
+
}
|
|
135
|
+
console.log();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Changeset Command
|
|
3
|
+
*
|
|
4
|
+
* Auto-generate changeset from detected changes.
|
|
5
|
+
* Uses the shared diffContracts() function internally.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Auto-generate changeset from detected changes
|
|
9
|
+
*/
|
|
10
|
+
export declare function changesetCommand(): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=changeset.command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"changeset.command.d.ts","sourceRoot":"","sources":["../../src/commands/changeset.command.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAeH;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAuDtD"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Changeset Command
|
|
3
|
+
*
|
|
4
|
+
* Auto-generate changeset from detected changes.
|
|
5
|
+
* Uses the shared diffContracts() function internally.
|
|
6
|
+
*/
|
|
7
|
+
import { writeFileSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
import { loadConfig } from '../config/index.js';
|
|
12
|
+
import { diffContracts } from '../core/diff.js';
|
|
13
|
+
import { CHANGESETS_DIR } from '../utils/files.js';
|
|
14
|
+
import { createChangeset, generateUniqueChangesetName, readChangesets, } from '@contractual/changesets';
|
|
15
|
+
/**
|
|
16
|
+
* Auto-generate changeset from detected changes
|
|
17
|
+
*/
|
|
18
|
+
export async function changesetCommand() {
|
|
19
|
+
const spinner = ora('Loading configuration...').start();
|
|
20
|
+
let config;
|
|
21
|
+
try {
|
|
22
|
+
config = loadConfig();
|
|
23
|
+
spinner.succeed('Configuration loaded');
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
spinner.fail('Failed to load configuration');
|
|
27
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
28
|
+
console.error(chalk.red(message));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
// Detect changes for all contracts using shared diff logic
|
|
32
|
+
const diffSpinner = ora('Detecting changes...').start();
|
|
33
|
+
try {
|
|
34
|
+
const { results: diffResults, contractualDir } = await diffContracts(config, {
|
|
35
|
+
includeEmpty: false, // Only get contracts with actual changes
|
|
36
|
+
});
|
|
37
|
+
if (diffResults.length === 0) {
|
|
38
|
+
diffSpinner.succeed('No changes detected');
|
|
39
|
+
console.log(chalk.gray('No changeset created.'));
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
diffSpinner.succeed(`Detected changes in ${diffResults.length} contract(s)`);
|
|
43
|
+
// Create changeset content
|
|
44
|
+
const { content: changesetContent } = createChangeset(diffResults);
|
|
45
|
+
// Read existing changesets to ensure unique name
|
|
46
|
+
const changesetsDir = join(contractualDir, CHANGESETS_DIR);
|
|
47
|
+
const existingChangesets = await readChangesets(changesetsDir);
|
|
48
|
+
const existingNames = existingChangesets.map((c) => c.filename.replace(/\.md$/, ''));
|
|
49
|
+
const changesetName = generateUniqueChangesetName(existingNames);
|
|
50
|
+
// Write changeset file
|
|
51
|
+
const changesetPath = join(changesetsDir, `${changesetName}.md`);
|
|
52
|
+
writeFileSync(changesetPath, changesetContent, 'utf-8');
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(chalk.green('Created changeset:'), chalk.cyan(changesetPath));
|
|
55
|
+
console.log();
|
|
56
|
+
console.log(chalk.gray('You can edit this file to add more details about the changes.'));
|
|
57
|
+
console.log(chalk.gray('Run `contractual version` to consume changesets and bump versions.'));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
diffSpinner.fail('Failed to detect changes');
|
|
61
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
62
|
+
console.error(chalk.red('Error:'), message);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type PromptOptions } from '../utils/prompts.js';
|
|
2
|
+
import type { ContractType } from '@contractual/types';
|
|
3
|
+
/**
|
|
4
|
+
* Options for the contract add command
|
|
5
|
+
*/
|
|
6
|
+
interface ContractAddOptions extends PromptOptions {
|
|
7
|
+
/** Contract name */
|
|
8
|
+
name?: string;
|
|
9
|
+
/** Contract type */
|
|
10
|
+
type?: ContractType;
|
|
11
|
+
/** Path to spec file */
|
|
12
|
+
path?: string;
|
|
13
|
+
/** Initial version */
|
|
14
|
+
initialVersion?: string;
|
|
15
|
+
/** Skip validation */
|
|
16
|
+
skipValidation?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Add a new contract to the configuration
|
|
20
|
+
*/
|
|
21
|
+
export declare function contractAddCommand(options?: ContractAddOptions): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Options for the contract list command
|
|
24
|
+
*/
|
|
25
|
+
interface ContractListOptions {
|
|
26
|
+
/** Output as JSON */
|
|
27
|
+
json?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* List contracts
|
|
31
|
+
*/
|
|
32
|
+
export declare function contractListCommand(name: string | undefined, options?: ContractListOptions): Promise<void>;
|
|
33
|
+
export {};
|
|
34
|
+
//# sourceMappingURL=contract.command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.command.d.ts","sourceRoot":"","sources":["../../src/commands/contract.command.ts"],"names":[],"mappings":"AAYA,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAsB,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAO3E;;GAEG;AACH,UAAU,kBAAmB,SAAQ,aAAa;IAChD,oBAAoB;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,wBAAwB;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sBAAsB;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sBAAsB;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmGxF;AAsHD;;GAEG;AACH,UAAU,mBAAmB;IAC3B,qBAAqB;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAYD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAoEf"}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join, resolve, extname } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
5
|
+
import { VersionManager } from '@contractual/changesets';
|
|
6
|
+
import { loadConfig } from '../config/index.js';
|
|
7
|
+
import { ensureContractualDir, detectSpecType, CONTRACTUAL_DIR, findContractualDir, } from '../utils/files.js';
|
|
8
|
+
import { promptInput, promptSelect, promptVersion, CONTRACT_TYPE_CHOICES, } from '../utils/prompts.js';
|
|
9
|
+
/**
|
|
10
|
+
* Default version for new contracts
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_VERSION = '0.0.0';
|
|
13
|
+
/**
|
|
14
|
+
* Add a new contract to the configuration
|
|
15
|
+
*/
|
|
16
|
+
export async function contractAddCommand(options = {}) {
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
const configPath = join(cwd, 'contractual.yaml');
|
|
19
|
+
// Check if initialized
|
|
20
|
+
if (!existsSync(configPath)) {
|
|
21
|
+
console.log(chalk.red('Not initialized:') + ' contractual.yaml not found');
|
|
22
|
+
console.log(chalk.dim('Run `contractual init` first'));
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Read existing config
|
|
27
|
+
const configContent = readFileSync(configPath, 'utf-8');
|
|
28
|
+
const config = parseYaml(configContent);
|
|
29
|
+
if (!config.contracts) {
|
|
30
|
+
config.contracts = [];
|
|
31
|
+
}
|
|
32
|
+
// Get contract details through prompts or options
|
|
33
|
+
const contractName = await getContractName(config.contracts, options);
|
|
34
|
+
if (!contractName)
|
|
35
|
+
return;
|
|
36
|
+
const specPath = await getSpecPath(cwd, options);
|
|
37
|
+
if (!specPath)
|
|
38
|
+
return;
|
|
39
|
+
const contractType = await getContractType(cwd, specPath, options);
|
|
40
|
+
if (!contractType)
|
|
41
|
+
return;
|
|
42
|
+
const version = await getVersion(options);
|
|
43
|
+
// Validate spec file
|
|
44
|
+
if (!options.skipValidation) {
|
|
45
|
+
const absolutePath = resolve(cwd, specPath);
|
|
46
|
+
const detectedType = detectSpecType(absolutePath);
|
|
47
|
+
if (!detectedType) {
|
|
48
|
+
console.log(chalk.red('Invalid spec file:') + ' Could not detect spec type');
|
|
49
|
+
console.log(chalk.dim(`Expected: ${contractType}`));
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (detectedType !== contractType) {
|
|
54
|
+
console.log(chalk.yellow('Type mismatch:') +
|
|
55
|
+
` Detected ${chalk.cyan(detectedType)}, specified ${chalk.cyan(contractType)}`);
|
|
56
|
+
console.log(chalk.dim('Use --skip-validation to override'));
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
console.log(chalk.green('✓') + ` Valid ${contractType} spec`);
|
|
61
|
+
}
|
|
62
|
+
// Create contract definition
|
|
63
|
+
const contract = {
|
|
64
|
+
name: contractName,
|
|
65
|
+
type: contractType,
|
|
66
|
+
path: specPath,
|
|
67
|
+
};
|
|
68
|
+
// Add to config
|
|
69
|
+
config.contracts.push(contract);
|
|
70
|
+
// Write updated config
|
|
71
|
+
const yamlContent = stringifyYaml(config, {
|
|
72
|
+
lineWidth: 100,
|
|
73
|
+
singleQuote: true,
|
|
74
|
+
});
|
|
75
|
+
writeFileSync(configPath, yamlContent, 'utf-8');
|
|
76
|
+
// Ensure .contractual directory exists and create snapshot
|
|
77
|
+
const contractualDir = findContractualDir(cwd) ?? join(cwd, CONTRACTUAL_DIR);
|
|
78
|
+
ensureContractualDir(cwd);
|
|
79
|
+
const versionManager = new VersionManager(contractualDir);
|
|
80
|
+
const absolutePath = resolve(cwd, specPath);
|
|
81
|
+
versionManager.setVersion(contractName, version, absolutePath);
|
|
82
|
+
// Print summary
|
|
83
|
+
const snapshotExt = extname(specPath) || '.yaml';
|
|
84
|
+
console.log();
|
|
85
|
+
console.log(chalk.green('✓') + ` Added ${chalk.cyan(contractName)} (${contractType}) at v${version}`);
|
|
86
|
+
console.log();
|
|
87
|
+
console.log(chalk.bold('Updated:'));
|
|
88
|
+
console.log(` ${chalk.yellow('~')} contractual.yaml`);
|
|
89
|
+
console.log(chalk.bold('Created:'));
|
|
90
|
+
console.log(` ${chalk.green('+')} .contractual/snapshots/${contractName}${snapshotExt}`);
|
|
91
|
+
console.log(` ${chalk.green('+')} .contractual/versions.json (updated)`);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get contract name through prompts or options
|
|
95
|
+
*/
|
|
96
|
+
async function getContractName(existingContracts, options) {
|
|
97
|
+
const existingNames = new Set(existingContracts.map((c) => c.name));
|
|
98
|
+
if (options.name) {
|
|
99
|
+
if (existingNames.has(options.name)) {
|
|
100
|
+
console.log(chalk.red('Contract exists:') + ` ${options.name} already defined`);
|
|
101
|
+
process.exitCode = 1;
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
return options.name;
|
|
105
|
+
}
|
|
106
|
+
const name = await promptInput('Contract name:', '', options);
|
|
107
|
+
if (!name) {
|
|
108
|
+
console.log(chalk.red('Contract name is required'));
|
|
109
|
+
process.exitCode = 1;
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
if (existingNames.has(name)) {
|
|
113
|
+
console.log(chalk.red('Contract exists:') + ` ${name} already defined`);
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
// Validate name format (alphanumeric, hyphens, underscores)
|
|
118
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name)) {
|
|
119
|
+
console.log(chalk.red('Invalid name:') +
|
|
120
|
+
' Must start with letter, contain only letters, numbers, hyphens, underscores');
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return name;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get spec path through prompts or options
|
|
128
|
+
*/
|
|
129
|
+
async function getSpecPath(cwd, options) {
|
|
130
|
+
if (options.path) {
|
|
131
|
+
const absolutePath = resolve(cwd, options.path);
|
|
132
|
+
if (!existsSync(absolutePath)) {
|
|
133
|
+
console.log(chalk.red('File not found:') + ` ${options.path}`);
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return options.path;
|
|
138
|
+
}
|
|
139
|
+
const path = await promptInput('Path to spec file:', '', options);
|
|
140
|
+
if (!path) {
|
|
141
|
+
console.log(chalk.red('Spec path is required'));
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const absolutePath = resolve(cwd, path);
|
|
146
|
+
if (!existsSync(absolutePath)) {
|
|
147
|
+
console.log(chalk.red('File not found:') + ` ${path}`);
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
return path;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Get contract type through prompts or options
|
|
155
|
+
*/
|
|
156
|
+
async function getContractType(cwd, specPath, options) {
|
|
157
|
+
if (options.type) {
|
|
158
|
+
return options.type;
|
|
159
|
+
}
|
|
160
|
+
// Try to auto-detect type
|
|
161
|
+
const absolutePath = resolve(cwd, specPath);
|
|
162
|
+
const detectedType = detectSpecType(absolutePath);
|
|
163
|
+
if (detectedType && options.yes) {
|
|
164
|
+
return detectedType;
|
|
165
|
+
}
|
|
166
|
+
const typeChoices = CONTRACT_TYPE_CHOICES.map((c) => ({
|
|
167
|
+
...c,
|
|
168
|
+
name: detectedType === c.value ? `${c.name} (detected)` : c.name,
|
|
169
|
+
}));
|
|
170
|
+
return promptSelect('Contract type:', [...typeChoices], detectedType ?? 'openapi', options);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Get version through prompts or options
|
|
174
|
+
*/
|
|
175
|
+
async function getVersion(options) {
|
|
176
|
+
if (options.initialVersion) {
|
|
177
|
+
return options.initialVersion;
|
|
178
|
+
}
|
|
179
|
+
return promptVersion('Initial version:', DEFAULT_VERSION, options);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* List contracts
|
|
183
|
+
*/
|
|
184
|
+
export async function contractListCommand(name, options = {}) {
|
|
185
|
+
let config;
|
|
186
|
+
try {
|
|
187
|
+
config = loadConfig();
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
191
|
+
console.error(chalk.red('Failed to load configuration:'), message);
|
|
192
|
+
process.exitCode = 1;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const contractualDir = findContractualDir(config.configDir);
|
|
196
|
+
const versionManager = contractualDir ? new VersionManager(contractualDir) : null;
|
|
197
|
+
// Build contract info list
|
|
198
|
+
let contracts = config.contracts.map((c) => ({
|
|
199
|
+
name: c.name,
|
|
200
|
+
type: c.type,
|
|
201
|
+
version: versionManager?.getVersion(c.name) ?? '0.0.0',
|
|
202
|
+
path: c.path,
|
|
203
|
+
}));
|
|
204
|
+
// Filter by name if provided
|
|
205
|
+
if (name) {
|
|
206
|
+
contracts = contracts.filter((c) => c.name === name);
|
|
207
|
+
if (contracts.length === 0) {
|
|
208
|
+
console.error(chalk.red(`Contract not found: ${name}`));
|
|
209
|
+
process.exitCode = 1;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Output
|
|
214
|
+
if (options.json) {
|
|
215
|
+
console.log(JSON.stringify(contracts, null, 2));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
// Table output
|
|
219
|
+
if (contracts.length === 0) {
|
|
220
|
+
console.log(chalk.dim('No contracts configured.'));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
// Calculate column widths
|
|
224
|
+
const maxNameLen = Math.max(4, ...contracts.map((c) => c.name.length));
|
|
225
|
+
const maxTypeLen = Math.max(4, ...contracts.map((c) => c.type.length));
|
|
226
|
+
const maxVersionLen = Math.max(7, ...contracts.map((c) => c.version.length));
|
|
227
|
+
// Header
|
|
228
|
+
const header = `${'Name'.padEnd(maxNameLen)} ` +
|
|
229
|
+
`${'Type'.padEnd(maxTypeLen)} ` +
|
|
230
|
+
`${'Version'.padEnd(maxVersionLen)} ` +
|
|
231
|
+
`Path`;
|
|
232
|
+
console.log(chalk.dim(header));
|
|
233
|
+
console.log(chalk.dim('─'.repeat(header.length + 10)));
|
|
234
|
+
// Rows
|
|
235
|
+
for (const contract of contracts) {
|
|
236
|
+
const typeColor = getTypeColor(contract.type);
|
|
237
|
+
console.log(`${chalk.cyan(contract.name.padEnd(maxNameLen))} ` +
|
|
238
|
+
`${typeColor(contract.type.padEnd(maxTypeLen))} ` +
|
|
239
|
+
`${chalk.green(contract.version.padEnd(maxVersionLen))} ` +
|
|
240
|
+
`${chalk.dim(contract.path)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Get chalk color function for contract type
|
|
245
|
+
*/
|
|
246
|
+
function getTypeColor(type) {
|
|
247
|
+
switch (type) {
|
|
248
|
+
case 'openapi':
|
|
249
|
+
return chalk.green;
|
|
250
|
+
case 'asyncapi':
|
|
251
|
+
return chalk.magenta;
|
|
252
|
+
case 'json-schema':
|
|
253
|
+
return chalk.blue;
|
|
254
|
+
case 'odcs':
|
|
255
|
+
return chalk.yellow;
|
|
256
|
+
default:
|
|
257
|
+
return chalk.white;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff Command
|
|
3
|
+
*
|
|
4
|
+
* Show all changes between current specs and their last versioned snapshots,
|
|
5
|
+
* classified by severity.
|
|
6
|
+
*
|
|
7
|
+
* Unlike `breaking`, which is a CI gate (exits 1 on breaking changes),
|
|
8
|
+
* `diff` is informational — it always exits 0 on success.
|
|
9
|
+
*/
|
|
10
|
+
interface DiffOptions {
|
|
11
|
+
contract?: string;
|
|
12
|
+
format?: 'text' | 'json';
|
|
13
|
+
severity?: 'all' | 'breaking' | 'non-breaking' | 'patch';
|
|
14
|
+
verbose?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Show all changes between current specs and last versioned snapshots
|
|
18
|
+
*/
|
|
19
|
+
export declare function diffCommand(options: DiffOptions): Promise<void>;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=diff.command.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.command.d.ts","sourceRoot":"","sources":["../../src/commands/diff.command.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAQH,UAAU,WAAW;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,KAAK,GAAG,UAAU,GAAG,cAAc,GAAG,OAAO,CAAC;IACzD,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CA4CrE"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff Command
|
|
3
|
+
*
|
|
4
|
+
* Show all changes between current specs and their last versioned snapshots,
|
|
5
|
+
* classified by severity.
|
|
6
|
+
*
|
|
7
|
+
* Unlike `breaking`, which is a CI gate (exits 1 on breaking changes),
|
|
8
|
+
* `diff` is informational — it always exits 0 on success.
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import ora from 'ora';
|
|
12
|
+
import { loadConfig } from '../config/index.js';
|
|
13
|
+
import { diffContracts } from '../core/diff.js';
|
|
14
|
+
import { formatDiffText, formatDiffJson, filterBySeverity } from '../formatters/diff.js';
|
|
15
|
+
/**
|
|
16
|
+
* Show all changes between current specs and last versioned snapshots
|
|
17
|
+
*/
|
|
18
|
+
export async function diffCommand(options) {
|
|
19
|
+
const spinner = ora('Loading configuration...').start();
|
|
20
|
+
let config;
|
|
21
|
+
try {
|
|
22
|
+
config = loadConfig();
|
|
23
|
+
spinner.succeed('Configuration loaded');
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
spinner.fail('Failed to load configuration');
|
|
27
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
28
|
+
console.error(chalk.red(message));
|
|
29
|
+
process.exitCode = 2;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const diffSpinner = ora('Comparing specs against snapshots...').start();
|
|
33
|
+
try {
|
|
34
|
+
const { results } = await diffContracts(config, {
|
|
35
|
+
contracts: options.contract ? [options.contract] : undefined,
|
|
36
|
+
includeEmpty: true, // Show "no changes" for contracts with no diff
|
|
37
|
+
});
|
|
38
|
+
diffSpinner.succeed('Comparison complete');
|
|
39
|
+
console.log();
|
|
40
|
+
// Apply severity filter
|
|
41
|
+
const filtered = filterBySeverity(results, options.severity ?? 'all');
|
|
42
|
+
// Output results
|
|
43
|
+
if (options.format === 'json') {
|
|
44
|
+
console.log(formatDiffJson(filtered));
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
formatDiffText(filtered, { verbose: options.verbose });
|
|
48
|
+
}
|
|
49
|
+
// diff always exits 0 on success (it's informational, not a gate)
|
|
50
|
+
process.exitCode = 0;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
diffSpinner.fail('Comparison failed');
|
|
54
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
55
|
+
console.error(chalk.red('Error:'), message);
|
|
56
|
+
process.exitCode = 3; // Tool execution error
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './init.command.js';
|
|
2
|
+
export * from './lint.command.js';
|
|
3
|
+
export * from './breaking.command.js';
|
|
4
|
+
export * from './changeset.command.js';
|
|
5
|
+
export * from './version.command.js';
|
|
6
|
+
export * from './status.command.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/commands/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,qBAAqB,CAAC"}
|