@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
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../src/commands.ts"],"names":[],"mappings":""}
|
package/dist/commands.js
ADDED
|
@@ -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 @@
|
|
|
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,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
|
+
}
|