@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,120 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { loadConfig } from '../config/index.js';
5
+ import { findContractualDir, VERSIONS_FILE, CHANGESETS_DIR } from '../utils/files.js';
6
+ import { aggregateBumps, incrementVersion, readChangesets } from '@contractual/changesets';
7
+ /**
8
+ * Read versions.json file
9
+ */
10
+ function readVersions(contractualDir) {
11
+ const versionsPath = join(contractualDir, VERSIONS_FILE);
12
+ if (!existsSync(versionsPath)) {
13
+ return {};
14
+ }
15
+ try {
16
+ const content = readFileSync(versionsPath, 'utf-8');
17
+ return JSON.parse(content);
18
+ }
19
+ catch {
20
+ return {};
21
+ }
22
+ }
23
+ /**
24
+ * Show current state - versions, pending changesets, projected bumps
25
+ */
26
+ export async function statusCommand() {
27
+ try {
28
+ // Load config
29
+ const config = loadConfig();
30
+ const contractualDir = findContractualDir();
31
+ if (!contractualDir) {
32
+ console.log(chalk.red('Not initialized:') + ' .contractual directory not found');
33
+ console.log(chalk.dim('Run `contractual init` to get started'));
34
+ process.exitCode = 1;
35
+ return;
36
+ }
37
+ // Read versions
38
+ const versions = readVersions(contractualDir);
39
+ // Read pending changesets
40
+ const changesetsDir = join(contractualDir, CHANGESETS_DIR);
41
+ let changesets = [];
42
+ try {
43
+ changesets = await readChangesets(changesetsDir);
44
+ }
45
+ catch (error) {
46
+ // If parsing fails, show a warning but continue
47
+ const message = error instanceof Error ? error.message : 'Unknown error';
48
+ console.warn(chalk.yellow(`Warning: ${message}`));
49
+ }
50
+ // Calculate projected bumps
51
+ const projectedBumps = aggregateBumps(changesets);
52
+ // Print header
53
+ console.log(chalk.bold('\nContractual Status\n'));
54
+ // Print contract versions
55
+ console.log(chalk.bold.underline('Contracts'));
56
+ console.log();
57
+ if (config.contracts.length === 0) {
58
+ console.log(chalk.dim(' No contracts configured'));
59
+ }
60
+ else {
61
+ for (const contract of config.contracts) {
62
+ const versionEntry = versions[contract.name];
63
+ const currentVersion = versionEntry?.version ?? '0.0.0';
64
+ const bump = projectedBumps[contract.name];
65
+ let projectedVersion = null;
66
+ if (bump) {
67
+ projectedVersion = incrementVersion(currentVersion, bump);
68
+ }
69
+ // Contract name and type
70
+ const typeLabel = chalk.dim(`(${contract.type})`);
71
+ console.log(` ${chalk.cyan(contract.name)} ${typeLabel}`);
72
+ // Version info
73
+ const versionLabel = versionEntry
74
+ ? chalk.green(`v${currentVersion}`)
75
+ : chalk.dim('v0.0.0 (unreleased)');
76
+ if (projectedVersion) {
77
+ const bumpColor = bump === 'major' ? chalk.red : bump === 'minor' ? chalk.yellow : chalk.green;
78
+ console.log(` ${versionLabel} ${chalk.dim('->')} ${bumpColor(`v${projectedVersion}`)} ${chalk.dim(`(${bump})`)}`);
79
+ }
80
+ else {
81
+ console.log(` ${versionLabel}`);
82
+ }
83
+ // Release date
84
+ if (versionEntry?.released) {
85
+ const date = new Date(versionEntry.released).toLocaleDateString();
86
+ console.log(` ${chalk.dim(`Released: ${date}`)}`);
87
+ }
88
+ console.log();
89
+ }
90
+ }
91
+ // Print pending changesets
92
+ console.log(chalk.bold.underline('Pending Changesets'));
93
+ console.log();
94
+ if (changesets.length === 0) {
95
+ console.log(chalk.dim(' No pending changesets'));
96
+ console.log(chalk.dim(' Run `contractual add` to create one'));
97
+ }
98
+ else {
99
+ console.log(` ${chalk.yellow(changesets.length.toString())} changeset(s) pending\n`);
100
+ for (const changeset of changesets) {
101
+ console.log(` ${chalk.dim('-')} ${changeset.filename}`);
102
+ for (const [contract, bump] of Object.entries(changeset.bumps)) {
103
+ const bumpColor = bump === 'major' ? chalk.red : bump === 'minor' ? chalk.yellow : chalk.green;
104
+ console.log(` ${chalk.cyan(contract)}: ${bumpColor(bump)}`);
105
+ }
106
+ }
107
+ }
108
+ console.log();
109
+ // Summary
110
+ const hasProjectedBumps = Object.keys(projectedBumps).length > 0;
111
+ if (hasProjectedBumps) {
112
+ console.log(chalk.dim('Run `contractual version` to apply pending changesets and bump versions'));
113
+ }
114
+ }
115
+ catch (error) {
116
+ const message = error instanceof Error ? error.message : 'Unknown error';
117
+ console.error(chalk.red('Error:'), message);
118
+ process.exitCode = 1;
119
+ }
120
+ }
@@ -0,0 +1,16 @@
1
+ import { type PromptOptions } from '../utils/prompts.js';
2
+ /**
3
+ * Options for the version command
4
+ */
5
+ interface VersionOptions extends PromptOptions {
6
+ /** Preview without applying */
7
+ dryRun?: boolean;
8
+ /** Output JSON (implies --yes) */
9
+ json?: boolean;
10
+ }
11
+ /**
12
+ * Consume changesets and bump versions
13
+ */
14
+ export declare function versionCommand(options?: VersionOptions): Promise<void>;
15
+ export {};
16
+ //# sourceMappingURL=version.command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.command.d.ts","sourceRoot":"","sources":["../../src/commands/version.command.ts"],"names":[],"mappings":"AAMA,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAaxE;;GAEG;AACH,UAAU,cAAe,SAAQ,aAAa;IAC5C,+BAA+B;IAC/B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,kCAAkC;IAClC,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAYD;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsOhF"}
@@ -0,0 +1,247 @@
1
+ import { existsSync, unlinkSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import chalk from 'chalk';
4
+ import ora from 'ora';
5
+ import { loadConfig } from '../config/index.js';
6
+ import { findContractualDir, CHANGESETS_DIR } from '../utils/files.js';
7
+ import { promptConfirm } from '../utils/prompts.js';
8
+ import { VersionManager, PreReleaseManager, readChangesets, aggregateBumps, extractContractChanges, appendChangelog, incrementVersion, incrementVersionWithPreRelease, } from '@contractual/changesets';
9
+ /**
10
+ * Consume changesets and bump versions
11
+ */
12
+ export async function versionCommand(options = {}) {
13
+ // JSON output implies --yes (no prompts)
14
+ if (options.json) {
15
+ options.yes = true;
16
+ }
17
+ const spinner = ora('Loading configuration...').start();
18
+ let config;
19
+ try {
20
+ config = loadConfig();
21
+ spinner.succeed('Configuration loaded');
22
+ }
23
+ catch (error) {
24
+ spinner.fail('Failed to load configuration');
25
+ const message = error instanceof Error ? error.message : 'Unknown error';
26
+ console.error(chalk.red(message));
27
+ process.exit(1);
28
+ }
29
+ const contractualDir = findContractualDir(config.configDir);
30
+ if (!contractualDir) {
31
+ console.error(chalk.red('No .contractual directory found. Run `contractual init` first.'));
32
+ process.exit(1);
33
+ }
34
+ // Read all changesets
35
+ const readSpinner = ora('Reading changesets...').start();
36
+ const changesetsDir = join(contractualDir, CHANGESETS_DIR);
37
+ const changesets = await readChangesets(changesetsDir);
38
+ if (changesets.length === 0) {
39
+ readSpinner.succeed('No pending changesets');
40
+ if (options.json) {
41
+ console.log(JSON.stringify({ bumps: [], changesets: 0 }, null, 2));
42
+ }
43
+ else {
44
+ console.log(chalk.gray('Nothing to version.'));
45
+ }
46
+ process.exit(0);
47
+ }
48
+ readSpinner.succeed(`Found ${changesets.length} changeset(s)`);
49
+ // Aggregate bumps (highest wins per contract)
50
+ const aggregatedBumps = aggregateBumps(changesets);
51
+ if (Object.keys(aggregatedBumps).length === 0) {
52
+ if (options.json) {
53
+ console.log(JSON.stringify({ bumps: [], changesets: changesets.length }, null, 2));
54
+ }
55
+ else {
56
+ console.log(chalk.gray('No version bumps required.'));
57
+ }
58
+ process.exit(0);
59
+ }
60
+ // Initialize version manager for reading current versions
61
+ const versionManager = new VersionManager(contractualDir);
62
+ const preManager = new PreReleaseManager(contractualDir);
63
+ const preReleaseTag = preManager.getTag();
64
+ // Calculate pending bumps (preview)
65
+ const pendingBumps = [];
66
+ for (const [contractName, bumpType] of Object.entries(aggregatedBumps)) {
67
+ const contract = config.contracts.find((c) => c.name === contractName);
68
+ if (!contract) {
69
+ continue;
70
+ }
71
+ const currentVersion = versionManager.getVersion(contractName) ?? '0.0.0';
72
+ const nextVersion = preReleaseTag
73
+ ? incrementVersionWithPreRelease(currentVersion, bumpType, preReleaseTag)
74
+ : incrementVersion(currentVersion, bumpType);
75
+ pendingBumps.push({
76
+ contract: contractName,
77
+ currentVersion,
78
+ nextVersion,
79
+ bumpType,
80
+ });
81
+ }
82
+ // Show pre-release mode notice
83
+ if (preReleaseTag && !options.json) {
84
+ console.log(chalk.cyan(`Pre-release mode: ${preReleaseTag}`));
85
+ }
86
+ // Show preview
87
+ if (options.json) {
88
+ if (options.dryRun) {
89
+ console.log(JSON.stringify({
90
+ dryRun: true,
91
+ bumps: pendingBumps.map((b) => ({
92
+ contract: b.contract,
93
+ current: b.currentVersion,
94
+ next: b.nextVersion,
95
+ type: b.bumpType,
96
+ })),
97
+ changesets: changesets.length,
98
+ }, null, 2));
99
+ return;
100
+ }
101
+ }
102
+ else {
103
+ printPreviewTable(pendingBumps);
104
+ if (options.dryRun) {
105
+ console.log();
106
+ console.log(chalk.dim('Dry run - no changes applied'));
107
+ return;
108
+ }
109
+ }
110
+ // Confirm before applying (unless --yes)
111
+ if (!options.json) {
112
+ const shouldApply = await promptConfirm('Apply these version bumps?', true, options);
113
+ if (!shouldApply) {
114
+ console.log(chalk.dim('Cancelled'));
115
+ return;
116
+ }
117
+ }
118
+ // Apply version bumps
119
+ const bumpSpinner = options.json ? null : ora('Applying version bumps...').start();
120
+ const bumpResults = [];
121
+ const consumedChangesetPaths = [];
122
+ for (const [contractName, bumpType] of Object.entries(aggregatedBumps)) {
123
+ const contract = config.contracts.find((c) => c.name === contractName);
124
+ if (!contract) {
125
+ if (!options.json) {
126
+ console.warn(chalk.yellow(`Warning: Contract "${contractName}" not found in config, skipping.`));
127
+ }
128
+ continue;
129
+ }
130
+ const oldVersion = versionManager.getVersion(contractName) ?? '0.0.0';
131
+ let newVersion;
132
+ if (preReleaseTag) {
133
+ // Use pre-release version increment
134
+ newVersion = incrementVersionWithPreRelease(oldVersion, bumpType, preReleaseTag);
135
+ versionManager.setVersion(contractName, newVersion, contract.absolutePath);
136
+ }
137
+ else {
138
+ // Normal bump
139
+ const result = versionManager.bump(contractName, bumpType, contract.absolutePath);
140
+ newVersion = result.newVersion;
141
+ }
142
+ // Extract changes text from changesets for this contract
143
+ const changes = extractContractChanges(changesets, contractName);
144
+ bumpResults.push({
145
+ contract: contractName,
146
+ oldVersion,
147
+ newVersion,
148
+ bumpType,
149
+ changes,
150
+ });
151
+ }
152
+ bumpSpinner?.succeed('Version bumps applied');
153
+ // Append to CHANGELOG.md
154
+ const changelogSpinner = options.json ? null : ora('Updating changelog...').start();
155
+ const changelogPath = join(config.configDir, 'CHANGELOG.md');
156
+ try {
157
+ appendChangelog(changelogPath, bumpResults);
158
+ changelogSpinner?.succeed('Changelog updated');
159
+ }
160
+ catch (error) {
161
+ const message = error instanceof Error ? error.message : 'Unknown error';
162
+ changelogSpinner?.warn(`Failed to update changelog: ${message}`);
163
+ }
164
+ // Delete consumed changeset files
165
+ const cleanupSpinner = options.json ? null : ora('Cleaning up changesets...').start();
166
+ for (const changeset of changesets) {
167
+ const changesetPath = join(changesetsDir, changeset.filename);
168
+ try {
169
+ if (existsSync(changesetPath)) {
170
+ unlinkSync(changesetPath);
171
+ consumedChangesetPaths.push(changeset.filename);
172
+ }
173
+ }
174
+ catch {
175
+ // Ignore cleanup errors
176
+ }
177
+ }
178
+ cleanupSpinner?.succeed(`Removed ${consumedChangesetPaths.length} changeset(s)`);
179
+ // Print summary
180
+ if (options.json) {
181
+ console.log(JSON.stringify({
182
+ bumps: bumpResults.map((r) => ({
183
+ contract: r.contract,
184
+ old: r.oldVersion,
185
+ new: r.newVersion,
186
+ type: r.bumpType,
187
+ })),
188
+ changesets: consumedChangesetPaths.length,
189
+ }, null, 2));
190
+ }
191
+ else {
192
+ console.log();
193
+ console.log(chalk.bold('Version Summary:'));
194
+ console.log();
195
+ for (const result of bumpResults) {
196
+ console.log(` ${chalk.cyan(result.contract)}: ` +
197
+ `${chalk.gray(result.oldVersion)} -> ${chalk.green(result.newVersion)} ` +
198
+ `(${result.bumpType})`);
199
+ }
200
+ console.log();
201
+ console.log(chalk.green('Done!'), `${bumpResults.length} contract(s) versioned.`);
202
+ }
203
+ }
204
+ /**
205
+ * Print a preview table of pending version bumps
206
+ */
207
+ function printPreviewTable(bumps) {
208
+ console.log();
209
+ console.log(chalk.bold('Pending version bumps:'));
210
+ console.log();
211
+ // Calculate column widths
212
+ const maxContractLen = Math.max(8, ...bumps.map((b) => b.contract.length));
213
+ const maxCurrentLen = Math.max(7, ...bumps.map((b) => b.currentVersion.length));
214
+ const maxNextLen = Math.max(4, ...bumps.map((b) => b.nextVersion.length));
215
+ // Header
216
+ const header = ` ${'Contract'.padEnd(maxContractLen)} ` +
217
+ `${'Current'.padEnd(maxCurrentLen)} ` +
218
+ `${'→'} ` +
219
+ `${'Next'.padEnd(maxNextLen)} ` +
220
+ `Reason`;
221
+ console.log(chalk.dim(header));
222
+ console.log(chalk.dim(' ' + '─'.repeat(header.length - 2)));
223
+ // Rows
224
+ for (const bump of bumps) {
225
+ const reason = getBumpReason(bump.bumpType);
226
+ console.log(` ${chalk.cyan(bump.contract.padEnd(maxContractLen))} ` +
227
+ `${chalk.gray(bump.currentVersion.padEnd(maxCurrentLen))} ` +
228
+ `${chalk.dim('→')} ` +
229
+ `${chalk.green(bump.nextVersion.padEnd(maxNextLen))} ` +
230
+ `${reason}`);
231
+ }
232
+ }
233
+ /**
234
+ * Get human-readable reason for bump type
235
+ */
236
+ function getBumpReason(bumpType) {
237
+ switch (bumpType) {
238
+ case 'major':
239
+ return chalk.red('major (breaking)');
240
+ case 'minor':
241
+ return chalk.yellow('minor (feature)');
242
+ case 'patch':
243
+ return chalk.dim('patch (fix)');
244
+ default:
245
+ return bumpType;
246
+ }
247
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":""}
@@ -0,0 +1,84 @@
1
+ import { Command } from 'commander';
2
+ import { initCommand } from './commands/init.command.js';
3
+ import { contractAddCommand, contractListCommand } from './commands/contract.command.js';
4
+ import { lintCommand } from './commands/lint.command.js';
5
+ import { diffCommand } from './commands/diff.command.js';
6
+ import { breakingCommand } from './commands/breaking.command.js';
7
+ import { changesetCommand } from './commands/changeset.command.js';
8
+ import { versionCommand } from './commands/version.command.js';
9
+ import { preEnterCommand, preExitCommand, preStatusCommand } from './commands/pre.command.js';
10
+ import { statusCommand } from './commands/status.command.js';
11
+ const program = new Command();
12
+ program.name('contractual').description('Schema contract lifecycle orchestrator').version('0.1.0');
13
+ program
14
+ .command('init')
15
+ .description('Initialize Contractual in this repository')
16
+ .option('-V, --initial-version <version>', 'Initial version for contracts')
17
+ .option('--versioning <mode>', 'Versioning mode: independent, fixed')
18
+ .option('-y, --yes', 'Skip prompts and use defaults')
19
+ .option('--force', 'Reinitialize existing project')
20
+ .action(initCommand);
21
+ const contractCmd = program.command('contract').description('Manage contracts');
22
+ contractCmd
23
+ .command('add')
24
+ .description('Add a new contract to the configuration')
25
+ .option('-n, --name <name>', 'Contract name')
26
+ .option('-t, --type <type>', 'Contract type: openapi, asyncapi, json-schema, odcs')
27
+ .option('-p, --path <path>', 'Path to spec file')
28
+ .option('--initial-version <version>', 'Initial version (default: 0.0.0)')
29
+ .option('--skip-validation', 'Skip spec validation')
30
+ .option('-y, --yes', 'Skip prompts and use defaults')
31
+ .action(contractAddCommand);
32
+ contractCmd
33
+ .command('list [name]')
34
+ .description('List contracts (optionally filter by name)')
35
+ .option('--json', 'Output as JSON')
36
+ .action(contractListCommand);
37
+ program
38
+ .command('lint')
39
+ .description('Lint all configured contracts')
40
+ .option('-c, --contract <name>', 'Lint specific contract')
41
+ .option('--format <format>', 'Output format: text, json', 'text')
42
+ .option('--fail-on-warn', 'Exit 1 on warnings')
43
+ .action(lintCommand);
44
+ program
45
+ .command('diff')
46
+ .description('Show all changes between current specs and last versioned snapshots')
47
+ .option('-c, --contract <name>', 'Diff specific contract')
48
+ .option('--format <format>', 'Output format: text, json', 'text')
49
+ .option('--severity <level>', 'Filter: all, breaking, non-breaking, patch', 'all')
50
+ .option('--verbose', 'Show JSON Pointer paths for each change')
51
+ .action(diffCommand);
52
+ program
53
+ .command('breaking')
54
+ .description('Detect breaking changes against last snapshot')
55
+ .option('-c, --contract <name>', 'Check specific contract')
56
+ .option('--format <format>', 'Output format: text, json', 'text')
57
+ .option('--fail-on <level>', 'Exit 1 on: breaking, non-breaking, any', 'breaking')
58
+ .action(breakingCommand);
59
+ program
60
+ .command('changeset')
61
+ .description('Create changeset from detected changes')
62
+ .action(changesetCommand);
63
+ program
64
+ .command('version')
65
+ .description('Consume changesets and bump versions')
66
+ .option('-y, --yes', 'Skip confirmation prompt')
67
+ .option('--dry-run', 'Preview without applying')
68
+ .option('--json', 'Output JSON (implies --yes)')
69
+ .action(versionCommand);
70
+ const preCmd = program.command('pre').description('Manage pre-release versions');
71
+ preCmd
72
+ .command('enter <tag>')
73
+ .description('Enter pre-release mode (e.g., alpha, beta, rc)')
74
+ .action(preEnterCommand);
75
+ preCmd.command('exit').description('Exit pre-release mode').action(preExitCommand);
76
+ preCmd.command('status').description('Show pre-release status').action(preStatusCommand);
77
+ program
78
+ .command('status')
79
+ .description('Show current versions and pending changesets')
80
+ .action(statusCommand);
81
+ program.parseAsync(process.argv).catch((err) => {
82
+ console.error(`Error: ${err.message}`);
83
+ process.exit(2);
84
+ });
@@ -0,0 +1,3 @@
1
+ export * from './loader.js';
2
+ export * from './validator.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,gBAAgB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './loader.js';
2
+ export * from './validator.js';
@@ -0,0 +1,28 @@
1
+ import type { ContractualConfig, ResolvedConfig, ResolvedContract } from '@contractual/types';
2
+ /**
3
+ * Error thrown when config cannot be loaded
4
+ */
5
+ export declare class ConfigError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ /**
9
+ * Find contractual.yaml by walking up from the given directory
10
+ */
11
+ export declare function findConfigFile(startDir?: string): string | null;
12
+ /**
13
+ * Parse YAML config file
14
+ */
15
+ export declare function parseConfigFile(configPath: string): unknown;
16
+ /**
17
+ * Resolve contract paths to absolute paths
18
+ */
19
+ export declare function resolveContractPaths(config: ContractualConfig, configDir: string): ResolvedContract[];
20
+ /**
21
+ * Load and validate config from a file path
22
+ */
23
+ export declare function loadConfigFromPath(configPath: string): ResolvedConfig;
24
+ /**
25
+ * Load config by searching from the current directory
26
+ */
27
+ export declare function loadConfig(startDir?: string): ResolvedConfig;
28
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAQ9F;;GAEG;AACH,qBAAa,WAAY,SAAQ,KAAK;gBACxB,OAAO,EAAE,MAAM;CAI5B;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,QAAQ,GAAE,MAAsB,GAAG,MAAM,GAAG,IAAI,CAyB9E;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAS3D;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,iBAAiB,EACzB,SAAS,EAAE,MAAM,GAChB,gBAAgB,EAAE,CAoCpB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,cAAc,CAoBrE;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,cAAc,CAQ5D"}
@@ -0,0 +1,123 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { parse as parseYaml } from 'yaml';
4
+ import fg from 'fast-glob';
5
+ import { validateConfig, formatValidationErrors } from './validator.js';
6
+ /**
7
+ * Config file names to search for
8
+ */
9
+ const CONFIG_FILENAMES = ['contractual.yaml', 'contractual.yml'];
10
+ /**
11
+ * Error thrown when config cannot be loaded
12
+ */
13
+ export class ConfigError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = 'ConfigError';
17
+ }
18
+ }
19
+ /**
20
+ * Find contractual.yaml by walking up from the given directory
21
+ */
22
+ export function findConfigFile(startDir = process.cwd()) {
23
+ let currentDir = resolve(startDir);
24
+ const root = dirname(currentDir);
25
+ while (currentDir !== root) {
26
+ for (const filename of CONFIG_FILENAMES) {
27
+ const configPath = join(currentDir, filename);
28
+ if (existsSync(configPath)) {
29
+ return configPath;
30
+ }
31
+ }
32
+ const parentDir = dirname(currentDir);
33
+ if (parentDir === currentDir)
34
+ break;
35
+ currentDir = parentDir;
36
+ }
37
+ // Check root directory too
38
+ for (const filename of CONFIG_FILENAMES) {
39
+ const configPath = join(currentDir, filename);
40
+ if (existsSync(configPath)) {
41
+ return configPath;
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+ /**
47
+ * Parse YAML config file
48
+ */
49
+ export function parseConfigFile(configPath) {
50
+ const content = readFileSync(configPath, 'utf-8');
51
+ try {
52
+ return parseYaml(content);
53
+ }
54
+ catch (err) {
55
+ const message = err instanceof Error ? err.message : 'Unknown parse error';
56
+ throw new ConfigError(`Failed to parse ${configPath}: ${message}`);
57
+ }
58
+ }
59
+ /**
60
+ * Resolve contract paths to absolute paths
61
+ */
62
+ export function resolveContractPaths(config, configDir) {
63
+ const resolved = [];
64
+ for (const contract of config.contracts) {
65
+ const pattern = contract.path;
66
+ const absolutePattern = resolve(configDir, pattern);
67
+ // Check if it's a glob pattern
68
+ if (pattern.includes('*')) {
69
+ const matches = fg.sync(absolutePattern, { onlyFiles: true });
70
+ if (matches.length === 0) {
71
+ console.warn(`Warning: No files matched pattern "${pattern}" for contract "${contract.name}"`);
72
+ }
73
+ // For glob patterns, use the first match as the primary path
74
+ // In the future, we might support multiple specs per contract
75
+ if (matches.length > 0) {
76
+ resolved.push({
77
+ ...contract,
78
+ absolutePath: matches[0],
79
+ });
80
+ }
81
+ }
82
+ else {
83
+ // Direct file path
84
+ if (!existsSync(absolutePattern)) {
85
+ console.warn(`Warning: File not found "${pattern}" for contract "${contract.name}"`);
86
+ }
87
+ resolved.push({
88
+ ...contract,
89
+ absolutePath: absolutePattern,
90
+ });
91
+ }
92
+ }
93
+ return resolved;
94
+ }
95
+ /**
96
+ * Load and validate config from a file path
97
+ */
98
+ export function loadConfigFromPath(configPath) {
99
+ const parsed = parseConfigFile(configPath);
100
+ const validation = validateConfig(parsed);
101
+ if (!validation.valid) {
102
+ throw new ConfigError(`Invalid configuration in ${configPath}:\n${formatValidationErrors(validation.errors)}`);
103
+ }
104
+ const config = parsed;
105
+ const configDir = dirname(configPath);
106
+ const resolvedContracts = resolveContractPaths(config, configDir);
107
+ return {
108
+ ...config,
109
+ contracts: resolvedContracts,
110
+ configDir,
111
+ configPath,
112
+ };
113
+ }
114
+ /**
115
+ * Load config by searching from the current directory
116
+ */
117
+ export function loadConfig(startDir) {
118
+ const configPath = findConfigFile(startDir);
119
+ if (!configPath) {
120
+ throw new ConfigError('No contractual.yaml found. Run `contractual init` to get started.');
121
+ }
122
+ return loadConfigFromPath(configPath);
123
+ }