@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
@@ -0,0 +1,21 @@
1
+ import { type PromptOptions } from '../utils/prompts.js';
2
+ import type { VersioningMode } from '@contractual/types';
3
+ /**
4
+ * Options for the init command
5
+ */
6
+ interface InitOptions extends PromptOptions {
7
+ /** Initial version for contracts */
8
+ initialVersion?: string;
9
+ /** Versioning mode */
10
+ versioning?: VersioningMode;
11
+ /** Force reinitialize */
12
+ force?: boolean;
13
+ }
14
+ /**
15
+ * Initialize Contractual in a repository
16
+ *
17
+ * Scans for spec files and generates contractual.yaml configuration
18
+ */
19
+ export declare function initCommand(options?: InitOptions): Promise<void>;
20
+ export {};
21
+ //# sourceMappingURL=init.command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.command.d.ts","sourceRoot":"","sources":["../../src/commands/init.command.ts"],"names":[],"mappings":"AAaA,OAAO,EAML,KAAK,aAAa,EACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAoC,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAY3F;;GAEG;AACH,UAAU,WAAY,SAAQ,aAAa;IACzC,oCAAoC;IACpC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,sBAAsB;IACtB,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,yBAAyB;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAiGD;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyJ1E"}
@@ -0,0 +1,294 @@
1
+ import { existsSync, writeFileSync, readFileSync } from 'node:fs';
2
+ import { join, basename } from 'node:path';
3
+ import fg from 'fast-glob';
4
+ import chalk from 'chalk';
5
+ import ora from 'ora';
6
+ import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
7
+ import { VersionManager } from '@contractual/changesets';
8
+ import { ensureContractualDir, detectSpecType, CONTRACTUAL_DIR, getSnapshotPath, } from '../utils/files.js';
9
+ import { promptSelect, promptVersion, promptConfirm, VERSION_CHOICES, VERSIONING_MODE_CHOICES, } from '../utils/prompts.js';
10
+ /**
11
+ * Default starting version for new contracts
12
+ */
13
+ const DEFAULT_VERSION = '0.0.0';
14
+ /**
15
+ * Default versioning mode
16
+ */
17
+ const DEFAULT_VERSIONING_MODE = 'independent';
18
+ /**
19
+ * Glob patterns to find spec files
20
+ */
21
+ const SPEC_PATTERNS = [
22
+ '**/*.openapi.yaml',
23
+ '**/*.openapi.yml',
24
+ '**/*.openapi.json',
25
+ '**/openapi.yaml',
26
+ '**/openapi.yml',
27
+ '**/openapi.json',
28
+ '**/*.asyncapi.yaml',
29
+ '**/*.asyncapi.yml',
30
+ '**/*.asyncapi.json',
31
+ '**/*.schema.json',
32
+ '**/*.odcs.yaml',
33
+ '**/*.odcs.yml',
34
+ ];
35
+ /**
36
+ * Directories to ignore when scanning
37
+ */
38
+ const IGNORE_PATTERNS = [
39
+ '**/node_modules/**',
40
+ '**/.git/**',
41
+ '**/dist/**',
42
+ '**/build/**',
43
+ '**/.contractual/**',
44
+ ];
45
+ /**
46
+ * Extract contract name from file path
47
+ * Uses the filename stem without extension patterns
48
+ */
49
+ function extractContractName(filePath) {
50
+ const base = basename(filePath);
51
+ // Remove known suffixes and extensions
52
+ let name = base
53
+ .replace(/\.openapi\.(ya?ml|json)$/i, '')
54
+ .replace(/\.asyncapi\.(ya?ml|json)$/i, '')
55
+ .replace(/\.schema\.json$/i, '')
56
+ .replace(/\.odcs\.ya?ml$/i, '')
57
+ .replace(/\.(ya?ml|json)$/i, '');
58
+ // Handle generic names like "openapi" -> use parent directory name
59
+ if (['openapi', 'asyncapi', 'schema', 'spec', 'api'].includes(name.toLowerCase())) {
60
+ const parts = filePath.split('/');
61
+ if (parts.length >= 2) {
62
+ name = parts[parts.length - 2];
63
+ }
64
+ }
65
+ return name;
66
+ }
67
+ /**
68
+ * Get initial version through prompts or options
69
+ */
70
+ async function getInitialVersion(options) {
71
+ // If version provided via CLI, use it
72
+ if (options.initialVersion) {
73
+ return options.initialVersion;
74
+ }
75
+ // Prompt for version
76
+ const versionChoice = await promptSelect('Initial version for contracts:', [...VERSION_CHOICES], '0.0.0', options);
77
+ if (versionChoice === 'custom') {
78
+ return promptVersion('Enter version:', DEFAULT_VERSION, options);
79
+ }
80
+ return versionChoice;
81
+ }
82
+ /**
83
+ * Get versioning mode through prompts or options
84
+ */
85
+ async function getVersioningMode(options) {
86
+ if (options.versioning) {
87
+ return options.versioning;
88
+ }
89
+ return promptSelect('Versioning mode:', [...VERSIONING_MODE_CHOICES], DEFAULT_VERSIONING_MODE, options);
90
+ }
91
+ /**
92
+ * Initialize Contractual in a repository
93
+ *
94
+ * Scans for spec files and generates contractual.yaml configuration
95
+ */
96
+ export async function initCommand(options = {}) {
97
+ const cwd = process.cwd();
98
+ const configPath = join(cwd, 'contractual.yaml');
99
+ const contractualDir = join(cwd, CONTRACTUAL_DIR);
100
+ // Check if already initialized
101
+ if (existsSync(configPath) && !options.force) {
102
+ // Try to handle existing project with uninitialized contracts
103
+ await handleExistingProject(cwd, configPath, contractualDir, options);
104
+ return;
105
+ }
106
+ const spinner = ora('Scanning for spec files...').start();
107
+ try {
108
+ // Scan for spec files
109
+ const files = await fg(SPEC_PATTERNS, {
110
+ cwd,
111
+ ignore: IGNORE_PATTERNS,
112
+ absolute: false,
113
+ onlyFiles: true,
114
+ });
115
+ spinner.succeed(`Found ${files.length} potential spec file(s)`);
116
+ // Build contract definitions
117
+ const contracts = [];
118
+ const seenNames = new Set();
119
+ for (const filePath of files) {
120
+ const absolutePath = join(cwd, filePath);
121
+ const detectedType = detectSpecType(absolutePath);
122
+ if (!detectedType) {
123
+ continue;
124
+ }
125
+ let name = extractContractName(filePath);
126
+ // Ensure unique names
127
+ if (seenNames.has(name)) {
128
+ let counter = 2;
129
+ while (seenNames.has(`${name}-${counter}`)) {
130
+ counter++;
131
+ }
132
+ name = `${name}-${counter}`;
133
+ }
134
+ seenNames.add(name);
135
+ contracts.push({
136
+ name,
137
+ type: detectedType,
138
+ path: filePath,
139
+ });
140
+ }
141
+ if (contracts.length === 0) {
142
+ console.log(chalk.yellow('\nNo spec files found'));
143
+ console.log(chalk.dim('\nSupported file patterns:'));
144
+ console.log(chalk.dim(' - *.openapi.yaml/json'));
145
+ console.log(chalk.dim(' - *.asyncapi.yaml/json'));
146
+ console.log(chalk.dim(' - *.schema.json'));
147
+ console.log(chalk.dim(' - *.odcs.yaml'));
148
+ console.log(chalk.dim('\nYou can manually create contractual.yaml to define contracts.'));
149
+ return;
150
+ }
151
+ // Show found contracts
152
+ console.log();
153
+ for (const contract of contracts) {
154
+ const typeColor = getTypeColor(contract.type);
155
+ console.log(` ${chalk.dim('Found:')} ${contract.path} ${chalk.dim('(')}${typeColor(contract.type)}${chalk.dim(')')}`);
156
+ }
157
+ console.log();
158
+ // Get version and mode through prompts
159
+ const initialVersion = await getInitialVersion(options);
160
+ const versioningMode = await getVersioningMode(options);
161
+ // Generate config
162
+ const config = {
163
+ contracts,
164
+ changeset: {
165
+ autoDetect: true,
166
+ requireOnPR: true,
167
+ },
168
+ };
169
+ // Only add versioning section if not using defaults
170
+ if (versioningMode !== 'independent') {
171
+ config.versioning = {
172
+ mode: versioningMode,
173
+ };
174
+ }
175
+ // Write contractual.yaml
176
+ const yamlContent = stringifyYaml(config, {
177
+ lineWidth: 100,
178
+ singleQuote: true,
179
+ });
180
+ writeFileSync(configPath, yamlContent, 'utf-8');
181
+ // Create .contractual directory structure
182
+ const createdDir = ensureContractualDir(cwd);
183
+ // Create snapshots and set initial versions
184
+ const versionManager = new VersionManager(createdDir);
185
+ for (const contract of contracts) {
186
+ const absolutePath = join(cwd, contract.path);
187
+ versionManager.setVersion(contract.name, initialVersion, absolutePath);
188
+ }
189
+ // Print summary
190
+ console.log();
191
+ console.log(chalk.green('✓') + ' Initialized Contractual');
192
+ console.log();
193
+ console.log(chalk.bold('Created:'));
194
+ console.log(` ${chalk.green('+')} contractual.yaml`);
195
+ console.log(` ${chalk.green('+')} .contractual/`);
196
+ console.log(` ${chalk.green('+')} .contractual/changesets/`);
197
+ console.log(` ${chalk.green('+')} .contractual/snapshots/`);
198
+ console.log(` ${chalk.green('+')} .contractual/versions.json`);
199
+ console.log();
200
+ console.log(chalk.bold(`Detected ${contracts.length} contract(s) at v${initialVersion}:`));
201
+ for (const contract of contracts) {
202
+ const typeColor = getTypeColor(contract.type);
203
+ console.log(` ${chalk.cyan(contract.name)} ${chalk.dim('(')}${typeColor(contract.type)}${chalk.dim(')')}`);
204
+ console.log(` ${chalk.dim(contract.path)}`);
205
+ }
206
+ if (versioningMode !== 'independent') {
207
+ console.log();
208
+ console.log(chalk.dim(`Versioning mode: ${versioningMode}`));
209
+ }
210
+ console.log();
211
+ console.log(chalk.dim('Next steps:'));
212
+ console.log(chalk.dim(' 1. Run `contractual lint` to validate your specs'));
213
+ console.log(chalk.dim(' 2. Make changes to your specs'));
214
+ console.log(chalk.dim(' 3. Run `contractual diff` to see changes'));
215
+ console.log(chalk.dim(' 4. Run `contractual changeset` to record changes'));
216
+ }
217
+ catch (error) {
218
+ spinner.fail('Initialization failed');
219
+ const message = error instanceof Error ? error.message : 'Unknown error';
220
+ console.error(chalk.red(message));
221
+ process.exitCode = 1;
222
+ }
223
+ }
224
+ /**
225
+ * Handle existing project - initialize uninitialized contracts
226
+ */
227
+ async function handleExistingProject(cwd, configPath, contractualDir, options) {
228
+ // Read existing config
229
+ const configContent = readFileSync(configPath, 'utf-8');
230
+ const config = parseYaml(configContent);
231
+ if (!config.contracts || config.contracts.length === 0) {
232
+ console.log(chalk.red('Already initialized:') + ' contractual.yaml exists');
233
+ console.log(chalk.dim('Use `contractual status` to see current state'));
234
+ process.exitCode = 1;
235
+ return;
236
+ }
237
+ // Ensure .contractual directory exists
238
+ ensureContractualDir(cwd);
239
+ // Find contracts without snapshots
240
+ const uninitializedContracts = [];
241
+ for (const contract of config.contracts) {
242
+ const snapshotPath = getSnapshotPath(contract.name, contractualDir);
243
+ if (!snapshotPath) {
244
+ uninitializedContracts.push(contract);
245
+ }
246
+ }
247
+ if (uninitializedContracts.length === 0) {
248
+ console.log(chalk.yellow('Already initialized:') + ' contractual.yaml exists');
249
+ console.log(chalk.dim('All contracts have snapshots.'));
250
+ console.log(chalk.dim('Use `contractual status` to see current state'));
251
+ console.log(chalk.dim('Use `--force` to reinitialize'));
252
+ return;
253
+ }
254
+ // Show uninitialized contracts
255
+ console.log(chalk.yellow(`Found ${uninitializedContracts.length} contract(s) without version history:`));
256
+ for (const contract of uninitializedContracts) {
257
+ console.log(` ${chalk.dim('-')} ${chalk.cyan(contract.name)} ${chalk.dim(`(${contract.type})`)}`);
258
+ }
259
+ console.log();
260
+ // Confirm initialization
261
+ const shouldInitialize = await promptConfirm(`Initialize with version ${DEFAULT_VERSION}?`, true, options);
262
+ if (!shouldInitialize) {
263
+ console.log(chalk.dim('Skipped initialization'));
264
+ return;
265
+ }
266
+ // Initialize uninitialized contracts
267
+ const versionManager = new VersionManager(contractualDir);
268
+ for (const contract of uninitializedContracts) {
269
+ const absolutePath = join(cwd, contract.path);
270
+ if (!existsSync(absolutePath)) {
271
+ console.log(chalk.yellow(` Skipped ${contract.name}: spec file not found at ${contract.path}`));
272
+ continue;
273
+ }
274
+ versionManager.setVersion(contract.name, DEFAULT_VERSION, absolutePath);
275
+ console.log(chalk.green('✓') + ` Initialized ${chalk.cyan(contract.name)} at v${DEFAULT_VERSION}`);
276
+ }
277
+ }
278
+ /**
279
+ * Get chalk color function for contract type
280
+ */
281
+ function getTypeColor(type) {
282
+ switch (type) {
283
+ case 'openapi':
284
+ return chalk.green;
285
+ case 'asyncapi':
286
+ return chalk.magenta;
287
+ case 'json-schema':
288
+ return chalk.blue;
289
+ case 'odcs':
290
+ return chalk.yellow;
291
+ default:
292
+ return chalk.white;
293
+ }
294
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Options for the lint command
3
+ */
4
+ export interface LintOptions {
5
+ /** Filter to specific contract name */
6
+ contract?: string;
7
+ /** Output format */
8
+ format?: 'text' | 'json';
9
+ /** Exit 1 on warnings */
10
+ failOnWarn?: boolean;
11
+ }
12
+ /**
13
+ * Run linters for all configured contracts
14
+ */
15
+ export declare function lintCommand(options?: LintOptions): Promise<void>;
16
+ //# sourceMappingURL=lint.command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lint.command.d.ts","sourceRoot":"","sources":["../../src/commands/lint.command.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oBAAoB;IACpB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,yBAAyB;IACzB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAiCD;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0K1E"}
@@ -0,0 +1,174 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { loadConfig } from '../config/index.js';
4
+ import { getLinter as getRegisteredLinter } from '../governance/index.js';
5
+ import { printSuccess, printError, printWarning } from '../utils/output.js';
6
+ /**
7
+ * Get linter for a contract using the governance registry
8
+ */
9
+ function getLinterForContract(contract) {
10
+ // Check if linting is explicitly disabled
11
+ if (contract.lint === false) {
12
+ return { status: 'disabled' };
13
+ }
14
+ // Use the governance registry to get the linter
15
+ const linter = getRegisteredLinter(contract.type, contract.lint);
16
+ if (linter === null) {
17
+ return { status: 'disabled' };
18
+ }
19
+ if (!linter) {
20
+ return { status: 'not-found', type: contract.type };
21
+ }
22
+ return { status: 'found', linter };
23
+ }
24
+ /**
25
+ * Run linters for all configured contracts
26
+ */
27
+ export async function lintCommand(options = {}) {
28
+ const { contract: filterContract, format = 'text', failOnWarn = false } = options;
29
+ try {
30
+ // Load config
31
+ const config = loadConfig();
32
+ // Filter contracts if specified
33
+ let contracts = config.contracts;
34
+ if (filterContract) {
35
+ contracts = contracts.filter((c) => c.name === filterContract);
36
+ if (contracts.length === 0) {
37
+ if (format === 'json') {
38
+ console.log(JSON.stringify({ error: `Contract "${filterContract}" not found` }));
39
+ }
40
+ else {
41
+ printError(`Contract "${filterContract}" not found`);
42
+ }
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+ }
47
+ if (contracts.length === 0) {
48
+ if (format === 'json') {
49
+ console.log(JSON.stringify({ results: [], errors: 0, warnings: 0 }));
50
+ }
51
+ else {
52
+ printWarning('No contracts configured');
53
+ }
54
+ return;
55
+ }
56
+ const results = [];
57
+ const spinner = format === 'text' ? ora('Linting contracts...').start() : null;
58
+ for (const contract of contracts) {
59
+ if (spinner) {
60
+ spinner.text = `Linting ${contract.name}...`;
61
+ }
62
+ const linterResult = getLinterForContract(contract);
63
+ // Linting explicitly disabled
64
+ if (linterResult.status === 'disabled') {
65
+ if (format === 'text') {
66
+ spinner?.stopAndPersist({
67
+ symbol: chalk.dim('-'),
68
+ text: `${contract.name}: ${chalk.dim('linting disabled')}`,
69
+ });
70
+ spinner?.start();
71
+ }
72
+ continue;
73
+ }
74
+ // No linter registered for this type
75
+ if (linterResult.status === 'not-found') {
76
+ if (format === 'text') {
77
+ spinner?.stopAndPersist({
78
+ symbol: chalk.yellow('!'),
79
+ text: `${contract.name}: ${chalk.yellow(`no linter available for ${linterResult.type}`)}`,
80
+ });
81
+ spinner?.start();
82
+ }
83
+ continue;
84
+ }
85
+ try {
86
+ const result = await linterResult.linter(contract.absolutePath);
87
+ results.push({
88
+ ...result,
89
+ contract: contract.name,
90
+ });
91
+ }
92
+ catch (error) {
93
+ const message = error instanceof Error ? error.message : 'Linter execution failed';
94
+ results.push({
95
+ contract: contract.name,
96
+ specPath: contract.absolutePath,
97
+ errors: [
98
+ {
99
+ path: '',
100
+ message,
101
+ severity: 'error',
102
+ },
103
+ ],
104
+ warnings: [],
105
+ });
106
+ }
107
+ }
108
+ spinner?.stop();
109
+ // Calculate totals
110
+ const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0);
111
+ const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0);
112
+ // Output results
113
+ if (format === 'json') {
114
+ console.log(JSON.stringify({
115
+ results,
116
+ errors: totalErrors,
117
+ warnings: totalWarnings,
118
+ }, null, 2));
119
+ }
120
+ else {
121
+ // Text format output
122
+ console.log();
123
+ for (const result of results) {
124
+ const hasErrors = result.errors.length > 0;
125
+ const hasWarnings = result.warnings.length > 0;
126
+ if (!hasErrors && !hasWarnings) {
127
+ printSuccess(`${result.contract}: No issues found`);
128
+ continue;
129
+ }
130
+ // Print contract header
131
+ const errorCount = result.errors.length;
132
+ const warningCount = result.warnings.length;
133
+ const summary = [];
134
+ if (errorCount > 0)
135
+ summary.push(chalk.red(`${errorCount} error(s)`));
136
+ if (warningCount > 0)
137
+ summary.push(chalk.yellow(`${warningCount} warning(s)`));
138
+ console.log(`${chalk.bold(result.contract)}: ${summary.join(', ')}`);
139
+ // Print errors
140
+ for (const error of result.errors) {
141
+ const location = error.path ? chalk.dim(`[${error.path}]`) : '';
142
+ const rule = error.rule ? chalk.dim(`(${error.rule})`) : '';
143
+ console.log(` ${chalk.red('error')} ${location} ${error.message} ${rule}`);
144
+ }
145
+ // Print warnings
146
+ for (const warning of result.warnings) {
147
+ const location = warning.path ? chalk.dim(`[${warning.path}]`) : '';
148
+ const rule = warning.rule ? chalk.dim(`(${warning.rule})`) : '';
149
+ console.log(` ${chalk.yellow('warn')} ${location} ${warning.message} ${rule}`);
150
+ }
151
+ console.log();
152
+ }
153
+ // Summary
154
+ if (results.length > 0) {
155
+ console.log(chalk.dim(`Checked ${results.length} contract(s): ` +
156
+ `${totalErrors} error(s), ${totalWarnings} warning(s)`));
157
+ }
158
+ }
159
+ // Exit with error code if there were errors (or warnings if --fail-on-warn)
160
+ if (totalErrors > 0 || (failOnWarn && totalWarnings > 0)) {
161
+ process.exitCode = 1;
162
+ }
163
+ }
164
+ catch (error) {
165
+ const message = error instanceof Error ? error.message : 'Unknown error';
166
+ if (options.format === 'json') {
167
+ console.log(JSON.stringify({ error: message }));
168
+ }
169
+ else {
170
+ printError(message);
171
+ }
172
+ process.exitCode = 1;
173
+ }
174
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Enter pre-release mode
3
+ * @param tag - The pre-release tag (e.g., "alpha", "beta", "rc")
4
+ */
5
+ export declare function preEnterCommand(tag: string): Promise<void>;
6
+ /**
7
+ * Exit pre-release mode
8
+ */
9
+ export declare function preExitCommand(): Promise<void>;
10
+ /**
11
+ * Show pre-release status
12
+ */
13
+ export declare function preStatusCommand(): Promise<void>;
14
+ //# sourceMappingURL=pre.command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pre.command.d.ts","sourceRoot":"","sources":["../../src/commands/pre.command.ts"],"names":[],"mappings":"AAIA;;;GAGG;AACH,wBAAsB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsChE;AAED;;GAEG;AACH,wBAAsB,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CA+DpD;AAED;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAoDtD"}
@@ -0,0 +1,141 @@
1
+ import chalk from 'chalk';
2
+ import { VersionManager, PreReleaseManager } from '@contractual/changesets';
3
+ import { findContractualDir } from '../utils/files.js';
4
+ /**
5
+ * Enter pre-release mode
6
+ * @param tag - The pre-release tag (e.g., "alpha", "beta", "rc")
7
+ */
8
+ export async function preEnterCommand(tag) {
9
+ const cwd = process.cwd();
10
+ const contractualDir = findContractualDir(cwd);
11
+ if (!contractualDir) {
12
+ console.error(chalk.red('No .contractual directory found. Run `contractual init` first.'));
13
+ process.exitCode = 1;
14
+ return;
15
+ }
16
+ try {
17
+ const versionManager = new VersionManager(contractualDir);
18
+ const preManager = new PreReleaseManager(contractualDir);
19
+ if (preManager.isActive()) {
20
+ const state = preManager.getState();
21
+ console.log(chalk.yellow('Already in pre-release mode:') + ` ${state?.tag}`);
22
+ console.log(chalk.dim('Run `contractual pre exit` to leave pre-release mode first.'));
23
+ process.exitCode = 1;
24
+ return;
25
+ }
26
+ preManager.enter(tag, versionManager);
27
+ console.log(chalk.green('✓') + ` Entered pre-release mode: ${chalk.cyan(tag)}`);
28
+ console.log();
29
+ console.log(chalk.bold('Created:'));
30
+ console.log(` ${chalk.green('+')} .contractual/pre.json`);
31
+ console.log();
32
+ console.log(chalk.dim(`Next versions will use ${tag} identifier (e.g., 2.0.0-${tag}.0)`));
33
+ console.log(chalk.dim('Run `contractual version` to apply changesets with pre-release versions.'));
34
+ }
35
+ catch (error) {
36
+ const message = error instanceof Error ? error.message : 'Unknown error';
37
+ console.error(chalk.red('Failed to enter pre-release mode:'), message);
38
+ process.exitCode = 1;
39
+ }
40
+ }
41
+ /**
42
+ * Exit pre-release mode
43
+ */
44
+ export async function preExitCommand() {
45
+ const cwd = process.cwd();
46
+ const contractualDir = findContractualDir(cwd);
47
+ if (!contractualDir) {
48
+ console.error(chalk.red('No .contractual directory found. Run `contractual init` first.'));
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+ try {
53
+ const versionManager = new VersionManager(contractualDir);
54
+ const preManager = new PreReleaseManager(contractualDir);
55
+ if (!preManager.isActive()) {
56
+ console.log(chalk.yellow('Not in pre-release mode.'));
57
+ return;
58
+ }
59
+ const state = preManager.getState();
60
+ const currentVersions = versionManager.getAllVersions();
61
+ // Show what will change
62
+ console.log(chalk.bold('Exiting pre-release mode'));
63
+ console.log();
64
+ const hasPreReleaseVersions = Object.entries(currentVersions).some(([_, version]) => version.includes('-'));
65
+ if (hasPreReleaseVersions) {
66
+ console.log(chalk.dim('Current pre-release versions:'));
67
+ for (const [contract, version] of Object.entries(currentVersions)) {
68
+ if (version.includes('-')) {
69
+ // Extract base version
70
+ const baseVersion = version.split('-')[0];
71
+ console.log(` ${chalk.cyan(contract)}: ${chalk.gray(version)} → ${chalk.green(baseVersion)}`);
72
+ }
73
+ }
74
+ console.log();
75
+ console.log(chalk.dim('Run `contractual version` after exiting to finalize versions.'));
76
+ }
77
+ preManager.exit();
78
+ console.log();
79
+ console.log(chalk.green('✓') + ' Exited pre-release mode');
80
+ console.log();
81
+ console.log(chalk.bold('Removed:'));
82
+ console.log(` ${chalk.red('-')} .contractual/pre.json`);
83
+ if (state) {
84
+ console.log();
85
+ console.log(chalk.dim(`Pre-release tag was: ${state.tag}`));
86
+ console.log(chalk.dim(`Entered at: ${new Date(state.enteredAt).toLocaleString()}`));
87
+ }
88
+ }
89
+ catch (error) {
90
+ const message = error instanceof Error ? error.message : 'Unknown error';
91
+ console.error(chalk.red('Failed to exit pre-release mode:'), message);
92
+ process.exitCode = 1;
93
+ }
94
+ }
95
+ /**
96
+ * Show pre-release status
97
+ */
98
+ export async function preStatusCommand() {
99
+ const cwd = process.cwd();
100
+ const contractualDir = findContractualDir(cwd);
101
+ if (!contractualDir) {
102
+ console.error(chalk.red('No .contractual directory found. Run `contractual init` first.'));
103
+ process.exitCode = 1;
104
+ return;
105
+ }
106
+ const preManager = new PreReleaseManager(contractualDir);
107
+ if (!preManager.isActive()) {
108
+ console.log(chalk.dim('Not in pre-release mode.'));
109
+ console.log(chalk.dim('Run `contractual pre enter <tag>` to enter pre-release mode.'));
110
+ return;
111
+ }
112
+ const state = preManager.getState();
113
+ if (!state) {
114
+ console.log(chalk.yellow('Pre-release state file is corrupted.'));
115
+ console.log(chalk.dim('Run `contractual pre exit` to reset.'));
116
+ return;
117
+ }
118
+ const versionManager = new VersionManager(contractualDir);
119
+ const currentVersions = versionManager.getAllVersions();
120
+ console.log(chalk.bold('Pre-release Status'));
121
+ console.log();
122
+ console.log(` ${chalk.dim('Tag:')} ${chalk.cyan(state.tag)}`);
123
+ console.log(` ${chalk.dim('Since:')} ${new Date(state.enteredAt).toLocaleString()}`);
124
+ // Show version changes since entering pre-release
125
+ const changedContracts = Object.entries(currentVersions).filter(([name, version]) => {
126
+ const initial = state.initialVersions[name];
127
+ return initial && initial !== version;
128
+ });
129
+ if (changedContracts.length > 0) {
130
+ console.log();
131
+ console.log(chalk.bold('Version changes since entering pre-release:'));
132
+ for (const [name, version] of changedContracts) {
133
+ const initial = state.initialVersions[name];
134
+ console.log(` ${chalk.cyan(name)}: ${chalk.gray(initial)} → ${chalk.green(version)}`);
135
+ }
136
+ }
137
+ console.log();
138
+ console.log(chalk.dim('Commands:'));
139
+ console.log(chalk.dim(' contractual version Apply changesets with pre-release versions'));
140
+ console.log(chalk.dim(' contractual pre exit Exit pre-release mode'));
141
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Show current state - versions, pending changesets, projected bumps
3
+ */
4
+ export declare function statusCommand(): Promise<void>;
5
+ //# sourceMappingURL=status.command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.command.d.ts","sourceRoot":"","sources":["../../src/commands/status.command.ts"],"names":[],"mappings":"AA0BA;;GAEG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAkHnD"}