@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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/bin/cli.js +2 -0
  3. package/dist/commands/breaking.command.d.ts +19 -0
  4. package/dist/commands/breaking.command.d.ts.map +1 -0
  5. package/dist/commands/breaking.command.js +138 -0
  6. package/dist/commands/changeset.command.d.ts +11 -0
  7. package/dist/commands/changeset.command.d.ts.map +1 -0
  8. package/dist/commands/changeset.command.js +65 -0
  9. package/dist/commands/contract.command.d.ts +34 -0
  10. package/dist/commands/contract.command.d.ts.map +1 -0
  11. package/dist/commands/contract.command.js +259 -0
  12. package/dist/commands/diff.command.d.ts +21 -0
  13. package/dist/commands/diff.command.d.ts.map +1 -0
  14. package/dist/commands/diff.command.js +58 -0
  15. package/dist/commands/index.d.ts +7 -0
  16. package/dist/commands/index.d.ts.map +1 -0
  17. package/dist/commands/index.js +6 -0
  18. package/dist/commands/init.command.d.ts +21 -0
  19. package/dist/commands/init.command.d.ts.map +1 -0
  20. package/dist/commands/init.command.js +294 -0
  21. package/dist/commands/lint.command.d.ts +16 -0
  22. package/dist/commands/lint.command.d.ts.map +1 -0
  23. package/dist/commands/lint.command.js +174 -0
  24. package/dist/commands/pre.command.d.ts +14 -0
  25. package/dist/commands/pre.command.d.ts.map +1 -0
  26. package/dist/commands/pre.command.js +141 -0
  27. package/dist/commands/status.command.d.ts +5 -0
  28. package/dist/commands/status.command.d.ts.map +1 -0
  29. package/dist/commands/status.command.js +120 -0
  30. package/dist/commands/version.command.d.ts +16 -0
  31. package/dist/commands/version.command.d.ts.map +1 -0
  32. package/dist/commands/version.command.js +247 -0
  33. package/dist/commands.d.ts +2 -0
  34. package/dist/commands.d.ts.map +1 -0
  35. package/dist/commands.js +84 -0
  36. package/dist/config/index.d.ts +3 -0
  37. package/dist/config/index.d.ts.map +1 -0
  38. package/dist/config/index.js +2 -0
  39. package/dist/config/loader.d.ts +28 -0
  40. package/dist/config/loader.d.ts.map +1 -0
  41. package/dist/config/loader.js +123 -0
  42. package/dist/config/schema.json +121 -0
  43. package/dist/config/validator.d.ts +28 -0
  44. package/dist/config/validator.d.ts.map +1 -0
  45. package/dist/config/validator.js +34 -0
  46. package/dist/core/diff.d.ts +26 -0
  47. package/dist/core/diff.d.ts.map +1 -0
  48. package/dist/core/diff.js +89 -0
  49. package/dist/formatters/diff.d.ts +31 -0
  50. package/dist/formatters/diff.d.ts.map +1 -0
  51. package/dist/formatters/diff.js +139 -0
  52. package/dist/governance/index.d.ts +11 -0
  53. package/dist/governance/index.d.ts.map +1 -0
  54. package/dist/governance/index.js +14 -0
  55. package/dist/index.d.ts +9 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +9 -0
  58. package/dist/utils/exec.d.ts +29 -0
  59. package/dist/utils/exec.d.ts.map +1 -0
  60. package/dist/utils/exec.js +66 -0
  61. package/dist/utils/files.d.ts +36 -0
  62. package/dist/utils/files.d.ts.map +1 -0
  63. package/dist/utils/files.js +137 -0
  64. package/dist/utils/index.d.ts +4 -0
  65. package/dist/utils/index.d.ts.map +1 -0
  66. package/dist/utils/index.js +3 -0
  67. package/dist/utils/output.d.ts +25 -0
  68. package/dist/utils/output.d.ts.map +1 -0
  69. package/dist/utils/output.js +73 -0
  70. package/dist/utils/prompts.d.ts +90 -0
  71. package/dist/utils/prompts.d.ts.map +1 -0
  72. package/dist/utils/prompts.js +119 -0
  73. package/package.json +81 -0
  74. 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,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/commands.js';
@@ -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"}
@@ -0,0 +1,6 @@
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';