delimit-cli 1.0.0 → 2.1.1
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/.github/workflows/api-governance.yml +43 -0
- package/README.md +70 -113
- package/adapters/codex-skill.js +87 -0
- package/adapters/cursor-extension.js +190 -0
- package/adapters/gemini-action.js +93 -0
- package/adapters/openai-function.js +112 -0
- package/adapters/xai-plugin.js +151 -0
- package/bin/delimit-cli.js +921 -0
- package/bin/delimit.js +237 -1
- package/delimit.yml +19 -0
- package/hooks/evidence-status.sh +12 -0
- package/hooks/git/commit-msg +4 -0
- package/hooks/git/pre-commit +4 -0
- package/hooks/git/pre-push +4 -0
- package/hooks/install-hooks.sh +583 -0
- package/hooks/message-auth-hook.js +9 -0
- package/hooks/message-governance-hook.js +9 -0
- package/hooks/models/claude-post.js +4 -0
- package/hooks/models/claude-pre.js +4 -0
- package/hooks/models/codex-post.js +4 -0
- package/hooks/models/codex-pre.js +4 -0
- package/hooks/models/cursor-post.js +4 -0
- package/hooks/models/cursor-pre.js +4 -0
- package/hooks/models/gemini-post.js +4 -0
- package/hooks/models/gemini-pre.js +4 -0
- package/hooks/models/openai-post.js +4 -0
- package/hooks/models/openai-pre.js +4 -0
- package/hooks/models/windsurf-post.js +4 -0
- package/hooks/models/windsurf-pre.js +4 -0
- package/hooks/models/xai-post.js +4 -0
- package/hooks/models/xai-pre.js +4 -0
- package/hooks/post-bash-hook.js +13 -0
- package/hooks/post-mcp-hook.js +13 -0
- package/hooks/post-response-hook.js +4 -0
- package/hooks/post-tool-hook.js +126 -0
- package/hooks/post-write-hook.js +13 -0
- package/hooks/pre-bash-hook.js +30 -0
- package/hooks/pre-mcp-hook.js +13 -0
- package/hooks/pre-read-hook.js +13 -0
- package/hooks/pre-search-hook.js +13 -0
- package/hooks/pre-submit-hook.js +4 -0
- package/hooks/pre-task-hook.js +13 -0
- package/hooks/pre-tool-hook.js +121 -0
- package/hooks/pre-web-hook.js +13 -0
- package/hooks/pre-write-hook.js +31 -0
- package/hooks/test-hooks.sh +12 -0
- package/hooks/update-delimit.sh +6 -0
- package/lib/agent.js +509 -0
- package/lib/api-engine.js +156 -0
- package/lib/auth-setup.js +891 -0
- package/lib/decision-engine.js +474 -0
- package/lib/hooks-installer.js +416 -0
- package/lib/platform-adapters.js +353 -0
- package/lib/proxy-handler.js +114 -0
- package/package.json +38 -30
- package/scripts/infect.js +128 -0
- package/test-decision-engine.js +181 -0
- package/test-hook.js +27 -0
- package/dist/commands/validate.d.ts +0 -2
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/validate.js +0 -106
- package/dist/commands/validate.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -71
- package/dist/index.js.map +0 -1
- package/dist/types/index.d.ts +0 -39
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -3
- package/dist/types/index.js.map +0 -1
- package/dist/utils/api.d.ts +0 -3
- package/dist/utils/api.d.ts.map +0 -1
- package/dist/utils/api.js +0 -64
- package/dist/utils/api.js.map +0 -1
- package/dist/utils/file.d.ts +0 -7
- package/dist/utils/file.d.ts.map +0 -1
- package/dist/utils/file.js +0 -69
- package/dist/utils/file.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -14
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -28
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/masker.d.ts +0 -14
- package/dist/utils/masker.d.ts.map +0 -1
- package/dist/utils/masker.js +0 -89
- package/dist/utils/masker.js.map +0 -1
- package/src/commands/validate.ts +0 -150
- package/src/index.ts +0 -80
- package/src/types/index.ts +0 -41
- package/src/utils/api.ts +0 -68
- package/src/utils/file.ts +0 -71
- package/src/utils/logger.ts +0 -27
- package/src/utils/masker.ts +0 -101
- package/test-sensitive.yaml +0 -109
- package/tsconfig.json +0 -23
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const axios = require('axios');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { execSync, spawn } = require('child_process');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const inquirer = require('inquirer');
|
|
10
|
+
const DelimitAuthSetup = require('../lib/auth-setup');
|
|
11
|
+
const DelimitHooksInstaller = require('../lib/hooks-installer');
|
|
12
|
+
|
|
13
|
+
const AGENT_URL = `http://127.0.0.1:${process.env.DELIMIT_AGENT_PORT || 7823}`;
|
|
14
|
+
const program = new Command();
|
|
15
|
+
|
|
16
|
+
const yaml = require('js-yaml');
|
|
17
|
+
|
|
18
|
+
// Helper to check if agent is running
|
|
19
|
+
async function checkAgent() {
|
|
20
|
+
try {
|
|
21
|
+
await axios.get(`${AGENT_URL}/status`);
|
|
22
|
+
return true;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Start agent if not running
|
|
29
|
+
async function ensureAgent() {
|
|
30
|
+
if (!(await checkAgent())) {
|
|
31
|
+
console.log(chalk.yellow('Starting Delimit Agent...'));
|
|
32
|
+
const agentPath = path.join(__dirname, '..', 'lib', 'agent.js');
|
|
33
|
+
spawn('node', [agentPath], {
|
|
34
|
+
detached: true,
|
|
35
|
+
stdio: 'ignore'
|
|
36
|
+
}).unref();
|
|
37
|
+
|
|
38
|
+
// Wait for agent to start
|
|
39
|
+
for (let i = 0; i < 10; i++) {
|
|
40
|
+
await new Promise(r => setTimeout(r, 500));
|
|
41
|
+
if (await checkAgent()) {
|
|
42
|
+
console.log(chalk.green('✓ Agent started'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw new Error('Failed to start agent');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
program
|
|
51
|
+
.name('delimit')
|
|
52
|
+
.description('Dynamic AI Governance with seamless mode switching')
|
|
53
|
+
.version('2.0.0');
|
|
54
|
+
|
|
55
|
+
// Install command with modes
|
|
56
|
+
program
|
|
57
|
+
.command('install')
|
|
58
|
+
.description('Install Delimit governance with multi-model hooks')
|
|
59
|
+
.option('--mode <mode>', 'Initial mode: advisory, guarded, enforce', 'advisory')
|
|
60
|
+
.option('--scope <scope>', 'Scope: global, repo', 'global')
|
|
61
|
+
.option('--hooks <hooks>', 'Install hooks for: all, git, ai, mcp', 'all')
|
|
62
|
+
.option('--auth', 'Setup authentication during installation')
|
|
63
|
+
.option('--dry-run', 'Preview changes without applying')
|
|
64
|
+
.action(async (options) => {
|
|
65
|
+
console.log(chalk.blue.bold('\n🔵 Delimit Installation\n'));
|
|
66
|
+
|
|
67
|
+
if (options.dryRun) {
|
|
68
|
+
console.log(chalk.yellow('DRY RUN - No changes will be made\n'));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log('This will modify:');
|
|
72
|
+
if (options.scope === 'global') {
|
|
73
|
+
console.log(' • Git global hooks');
|
|
74
|
+
console.log(' • Shell PATH (for AI tool interception)');
|
|
75
|
+
console.log(' • Create ~/.delimit configuration');
|
|
76
|
+
|
|
77
|
+
if (options.hooks === 'all' || options.hooks === 'ai') {
|
|
78
|
+
console.log(' • AI model hooks (Claude, Codex, Gemini, etc.)');
|
|
79
|
+
}
|
|
80
|
+
if (options.hooks === 'all' || options.hooks === 'mcp') {
|
|
81
|
+
console.log(' • MCP integration hooks for Claude Code');
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
console.log(' • Git hooks for current repository');
|
|
85
|
+
console.log(' • Create .delimit.yml in current directory');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
console.log(`\nInitial mode: ${chalk.bold(options.mode)}`);
|
|
89
|
+
console.log('You can change modes anytime with: delimit mode <mode>\n');
|
|
90
|
+
|
|
91
|
+
if (!options.dryRun) {
|
|
92
|
+
const { confirm } = await inquirer.prompt([{
|
|
93
|
+
type: 'confirm',
|
|
94
|
+
name: 'confirm',
|
|
95
|
+
message: 'Continue with installation?',
|
|
96
|
+
default: false
|
|
97
|
+
}]);
|
|
98
|
+
|
|
99
|
+
if (!confirm) {
|
|
100
|
+
console.log(chalk.red('Installation cancelled'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (options.dryRun) {
|
|
106
|
+
console.log(chalk.green('\n✓ Dry run complete'));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Actual installation
|
|
111
|
+
await installDelimit(options.mode, options.scope, options.hooks);
|
|
112
|
+
|
|
113
|
+
// Prompt for authentication setup
|
|
114
|
+
if (!options.dryRun) {
|
|
115
|
+
const { setupAuth } = await inquirer.prompt([{
|
|
116
|
+
type: 'confirm',
|
|
117
|
+
name: 'setupAuth',
|
|
118
|
+
message: '\nWould you like to setup authentication for GitHub, AI tools, and other services?',
|
|
119
|
+
default: true
|
|
120
|
+
}]);
|
|
121
|
+
|
|
122
|
+
if (setupAuth || options.auth) {
|
|
123
|
+
console.log(chalk.blue.bold('\n🔐 Setting up authentication...\n'));
|
|
124
|
+
const authSetup = new DelimitAuthSetup();
|
|
125
|
+
|
|
126
|
+
// Prompt for which services to configure
|
|
127
|
+
const { authCategories } = await inquirer.prompt([{
|
|
128
|
+
type: 'checkbox',
|
|
129
|
+
name: 'authCategories',
|
|
130
|
+
message: 'Select services to configure:',
|
|
131
|
+
choices: [
|
|
132
|
+
{ name: 'GitHub (recommended for governance)', value: 'github', checked: true },
|
|
133
|
+
{ name: 'AI Tools (Claude, OpenAI, Gemini)', value: 'ai', checked: true },
|
|
134
|
+
{ name: 'Cloud Providers (AWS, GCP, Azure)', value: 'cloud' },
|
|
135
|
+
{ name: 'Databases', value: 'databases' },
|
|
136
|
+
{ name: 'Container Registries', value: 'registries' },
|
|
137
|
+
{ name: 'Package Managers', value: 'packages' },
|
|
138
|
+
{ name: 'Monitoring Services', value: 'monitoring' },
|
|
139
|
+
{ name: 'Organization Settings', value: 'org' }
|
|
140
|
+
]
|
|
141
|
+
}]);
|
|
142
|
+
|
|
143
|
+
const setupOptions = {};
|
|
144
|
+
authCategories.forEach(cat => {
|
|
145
|
+
const key = cat === 'ai' ? 'setupAI' :
|
|
146
|
+
cat === 'github' ? 'setupGitHub' :
|
|
147
|
+
`setup${cat.charAt(0).toUpperCase() + cat.slice(1)}`;
|
|
148
|
+
setupOptions[key] = true;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await authSetup.setup(setupOptions);
|
|
152
|
+
console.log(chalk.green('\n✅ Authentication setup complete!'));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Mode switching command
|
|
158
|
+
program
|
|
159
|
+
.command('mode [mode]')
|
|
160
|
+
.description('Switch governance mode (advisory, guarded, enforce, auto)')
|
|
161
|
+
.action(async (mode) => {
|
|
162
|
+
await ensureAgent();
|
|
163
|
+
|
|
164
|
+
if (!mode) {
|
|
165
|
+
// Show current mode
|
|
166
|
+
const { data } = await axios.get(`${AGENT_URL}/status`);
|
|
167
|
+
console.log(chalk.blue('Current mode:'), chalk.bold(data.sessionMode));
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!['advisory', 'guarded', 'enforce', 'auto'].includes(mode)) {
|
|
172
|
+
console.error(chalk.red('Invalid mode. Choose: advisory, guarded, enforce, auto'));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const { data } = await axios.post(`${AGENT_URL}/mode`, { mode });
|
|
177
|
+
console.log(chalk.green(`✓ Mode switched to: ${chalk.bold(data.mode)}`));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Status command
|
|
181
|
+
program
|
|
182
|
+
.command('status')
|
|
183
|
+
.description('Show governance status')
|
|
184
|
+
.option('--verbose', 'Show detailed status')
|
|
185
|
+
.action(async (options) => {
|
|
186
|
+
const agentRunning = await checkAgent();
|
|
187
|
+
|
|
188
|
+
console.log(chalk.blue.bold('\nDelimit Governance Status\n'));
|
|
189
|
+
console.log('Agent:', agentRunning ? chalk.green('✓ Running') : chalk.red('✗ Not running'));
|
|
190
|
+
|
|
191
|
+
if (agentRunning) {
|
|
192
|
+
const { data } = await axios.get(`${AGENT_URL}/status`);
|
|
193
|
+
|
|
194
|
+
// Mode information
|
|
195
|
+
console.log('\n' + chalk.bold('Mode Configuration:'));
|
|
196
|
+
console.log(` Current Mode: ${chalk.bold(data.sessionMode)}`);
|
|
197
|
+
if (data.defaultMode) {
|
|
198
|
+
console.log(` Default Mode: ${data.defaultMode}`);
|
|
199
|
+
}
|
|
200
|
+
if (data.effectiveMode && data.effectiveMode !== data.sessionMode) {
|
|
201
|
+
console.log(` Effective Mode: ${chalk.yellow(data.effectiveMode)} (escalated)`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Policies
|
|
205
|
+
console.log('\n' + chalk.bold('Policies:'));
|
|
206
|
+
if (data.policiesLoaded.length > 0) {
|
|
207
|
+
data.policiesLoaded.forEach(policy => {
|
|
208
|
+
console.log(` • ${policy}`);
|
|
209
|
+
});
|
|
210
|
+
if (data.totalRules) {
|
|
211
|
+
console.log(` Total Rules: ${data.totalRules}`);
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
console.log(' No policies loaded');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Recent activity
|
|
218
|
+
console.log('\n' + chalk.bold('Activity:'));
|
|
219
|
+
console.log(` Audit Log Entries: ${data.auditLogSize}`);
|
|
220
|
+
if (data.lastDecision) {
|
|
221
|
+
const timeSince = Date.now() - new Date(data.lastDecision.timestamp);
|
|
222
|
+
const minutes = Math.floor(timeSince / 60000);
|
|
223
|
+
console.log(` Last Decision: ${minutes} minutes ago (${data.lastDecision.action})`);
|
|
224
|
+
}
|
|
225
|
+
console.log(` Uptime: ${Math.floor(data.uptime / 60)} minutes`);
|
|
226
|
+
|
|
227
|
+
// Verbose mode shows recent decisions
|
|
228
|
+
if (options.verbose && data.recentDecisions) {
|
|
229
|
+
console.log('\n' + chalk.bold('Recent Decisions:'));
|
|
230
|
+
data.recentDecisions.forEach(decision => {
|
|
231
|
+
const color = decision.action === 'block' ? chalk.red :
|
|
232
|
+
decision.action === 'prompt' ? chalk.yellow :
|
|
233
|
+
chalk.green;
|
|
234
|
+
console.log(` ${decision.timestamp} | ${color(decision.mode)} | ${decision.rule || 'no rule'}`);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// System integration
|
|
240
|
+
console.log('\n' + chalk.bold('System Integration:'));
|
|
241
|
+
|
|
242
|
+
// Git hooks
|
|
243
|
+
try {
|
|
244
|
+
const hooksPath = execSync('git config --global core.hooksPath').toString().trim();
|
|
245
|
+
const hooksActive = hooksPath.includes('.delimit');
|
|
246
|
+
console.log(` Git Hooks: ${hooksActive ? chalk.green('✓ Active') : chalk.yellow('⚠ Not configured')}`);
|
|
247
|
+
} catch (e) {
|
|
248
|
+
console.log(` Git Hooks: ${chalk.red('✗ Not configured')}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// PATH
|
|
252
|
+
if (process.env.PATH.includes('.delimit/shims')) {
|
|
253
|
+
console.log(` AI Tool Interception: ${chalk.green('✓ Active')}`);
|
|
254
|
+
} else {
|
|
255
|
+
console.log(` AI Tool Interception: ${chalk.gray('Not active')}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Policy files
|
|
259
|
+
const policyFiles = [];
|
|
260
|
+
if (fs.existsSync('delimit.yml')) {
|
|
261
|
+
policyFiles.push('project');
|
|
262
|
+
}
|
|
263
|
+
if (fs.existsSync(path.join(process.env.HOME, '.config', 'delimit', 'delimit.yml'))) {
|
|
264
|
+
policyFiles.push('user');
|
|
265
|
+
}
|
|
266
|
+
console.log(` Policy Files: ${policyFiles.length > 0 ? policyFiles.join(', ') : chalk.gray('none')}`);
|
|
267
|
+
|
|
268
|
+
if (options.verbose) {
|
|
269
|
+
console.log('\n' + chalk.gray('Run "delimit doctor" for detailed diagnostics'));
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Policy command
|
|
274
|
+
program
|
|
275
|
+
.command('policy')
|
|
276
|
+
.description('Manage governance policies')
|
|
277
|
+
.option('--init', 'Create example policy file')
|
|
278
|
+
.option('--validate', 'Validate policy syntax')
|
|
279
|
+
.action(async (options) => {
|
|
280
|
+
if (options.init) {
|
|
281
|
+
const examplePolicy = `# Delimit Policy Configuration
|
|
282
|
+
# This file defines dynamic governance rules
|
|
283
|
+
|
|
284
|
+
defaultMode: advisory
|
|
285
|
+
|
|
286
|
+
rules:
|
|
287
|
+
- name: "Production Protection"
|
|
288
|
+
mode: enforce
|
|
289
|
+
triggers:
|
|
290
|
+
- gitBranch: [main, master, production]
|
|
291
|
+
|
|
292
|
+
- name: "Payment Code Security"
|
|
293
|
+
mode: enforce
|
|
294
|
+
triggers:
|
|
295
|
+
- path: "**/payment/**"
|
|
296
|
+
- content: ["stripe", "payment", "billing"]
|
|
297
|
+
|
|
298
|
+
- name: "AI-Generated Code Review"
|
|
299
|
+
mode: guarded
|
|
300
|
+
triggers:
|
|
301
|
+
- commitMessage: "Co-authored-by"
|
|
302
|
+
|
|
303
|
+
- name: "Documentation Freedom"
|
|
304
|
+
mode: advisory
|
|
305
|
+
triggers:
|
|
306
|
+
- path: "**/*.md"
|
|
307
|
+
final: true
|
|
308
|
+
|
|
309
|
+
overrides:
|
|
310
|
+
allowEnforceOverride: false
|
|
311
|
+
requireGuardedOverrideReason: true
|
|
312
|
+
`;
|
|
313
|
+
|
|
314
|
+
fs.writeFileSync('delimit.yml', examplePolicy);
|
|
315
|
+
console.log(chalk.green('✓ Created delimit.yml'));
|
|
316
|
+
console.log('Edit this file to customize your governance rules');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (options.validate) {
|
|
320
|
+
// TODO: Implement validation
|
|
321
|
+
console.log(chalk.yellow('Policy validation coming soon'));
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Auth command - setup credentials
|
|
326
|
+
program
|
|
327
|
+
.command('auth')
|
|
328
|
+
.description('Setup authentication and credentials for services')
|
|
329
|
+
.option('--all', 'Setup all available services')
|
|
330
|
+
.option('--github', 'Setup GitHub authentication')
|
|
331
|
+
.option('--ai', 'Setup AI tools authentication')
|
|
332
|
+
.option('--cloud', 'Setup cloud provider credentials')
|
|
333
|
+
.option('--databases', 'Setup database credentials')
|
|
334
|
+
.option('--registries', 'Setup container registry credentials')
|
|
335
|
+
.option('--packages', 'Setup package manager credentials')
|
|
336
|
+
.option('--monitoring', 'Setup monitoring service credentials')
|
|
337
|
+
.option('--org', 'Setup organization settings')
|
|
338
|
+
.action(async (options) => {
|
|
339
|
+
console.log(chalk.blue.bold('\n🔐 Delimit Authentication Setup\n'));
|
|
340
|
+
|
|
341
|
+
const authSetup = new DelimitAuthSetup();
|
|
342
|
+
|
|
343
|
+
// Determine what to setup
|
|
344
|
+
const setupOptions = {
|
|
345
|
+
setupAll: options.all,
|
|
346
|
+
setupGitHub: options.github,
|
|
347
|
+
setupAI: options.ai,
|
|
348
|
+
setupCloud: options.cloud,
|
|
349
|
+
setupDatabases: options.databases,
|
|
350
|
+
setupRegistries: options.registries,
|
|
351
|
+
setupPackages: options.packages,
|
|
352
|
+
setupMonitoring: options.monitoring,
|
|
353
|
+
setupOrg: options.org
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// If no specific options, prompt for what to setup
|
|
357
|
+
if (!Object.values(setupOptions).some(v => v)) {
|
|
358
|
+
const { categories } = await inquirer.prompt([{
|
|
359
|
+
type: 'checkbox',
|
|
360
|
+
name: 'categories',
|
|
361
|
+
message: 'Which services would you like to configure?',
|
|
362
|
+
choices: [
|
|
363
|
+
{ name: 'GitHub', value: 'github' },
|
|
364
|
+
{ name: 'AI Tools (Claude, OpenAI, Gemini, etc.)', value: 'ai' },
|
|
365
|
+
{ name: 'Cloud Providers (AWS, GCP, Azure)', value: 'cloud' },
|
|
366
|
+
{ name: 'Databases', value: 'databases' },
|
|
367
|
+
{ name: 'Container Registries', value: 'registries' },
|
|
368
|
+
{ name: 'Package Managers', value: 'packages' },
|
|
369
|
+
{ name: 'Monitoring Services', value: 'monitoring' },
|
|
370
|
+
{ name: 'Organization Settings', value: 'org' }
|
|
371
|
+
]
|
|
372
|
+
}]);
|
|
373
|
+
|
|
374
|
+
categories.forEach(cat => {
|
|
375
|
+
setupOptions[`setup${cat.charAt(0).toUpperCase() + cat.slice(1)}`] = true;
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
await authSetup.setup(setupOptions);
|
|
380
|
+
|
|
381
|
+
console.log(chalk.green.bold('\n✅ Authentication setup complete!\n'));
|
|
382
|
+
console.log('Your credentials have been securely stored in ~/.delimit/credentials/');
|
|
383
|
+
console.log('Run "delimit auth" again anytime to add or update credentials');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// Audit command
|
|
387
|
+
program
|
|
388
|
+
.command('audit')
|
|
389
|
+
.description('View governance audit log')
|
|
390
|
+
.option('--tail <n>', 'Show last N entries', '10')
|
|
391
|
+
.action(async (options) => {
|
|
392
|
+
await ensureAgent();
|
|
393
|
+
|
|
394
|
+
const { data } = await axios.get(`${AGENT_URL}/audit`);
|
|
395
|
+
const entries = data.slice(-parseInt(options.tail));
|
|
396
|
+
|
|
397
|
+
if (entries.length === 0) {
|
|
398
|
+
console.log(chalk.yellow('No audit log entries'));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
console.log(chalk.blue.bold('\nRecent Governance Decisions:\n'));
|
|
403
|
+
entries.forEach(entry => {
|
|
404
|
+
const color = entry.action === 'block' ? chalk.red :
|
|
405
|
+
entry.action === 'prompt' ? chalk.yellow :
|
|
406
|
+
chalk.green;
|
|
407
|
+
|
|
408
|
+
console.log(`${entry.timestamp} | ${color(entry.mode.toUpperCase())} | ${entry.message}`);
|
|
409
|
+
if (entry.rule) {
|
|
410
|
+
console.log(` Rule: ${entry.rule}`);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
// Doctor command - diagnose issues
|
|
416
|
+
program
|
|
417
|
+
.command('doctor')
|
|
418
|
+
.description('Diagnose Delimit configuration and issues')
|
|
419
|
+
.action(async () => {
|
|
420
|
+
console.log(chalk.blue.bold('\n🩺 Delimit Doctor\n'));
|
|
421
|
+
const issues = [];
|
|
422
|
+
const warnings = [];
|
|
423
|
+
const info = [];
|
|
424
|
+
|
|
425
|
+
// Check agent status
|
|
426
|
+
const agentRunning = await checkAgent();
|
|
427
|
+
if (!agentRunning) {
|
|
428
|
+
issues.push('Agent is not running. Run "delimit status" to start it.');
|
|
429
|
+
} else {
|
|
430
|
+
info.push('Agent is running and responsive');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Check Git hooks
|
|
434
|
+
try {
|
|
435
|
+
const hooksPath = execSync('git config --global core.hooksPath').toString().trim();
|
|
436
|
+
if (hooksPath.includes('.delimit')) {
|
|
437
|
+
info.push('Git hooks are configured correctly');
|
|
438
|
+
|
|
439
|
+
// Check hook files exist
|
|
440
|
+
const hookFiles = ['pre-commit', 'pre-push'];
|
|
441
|
+
hookFiles.forEach(hook => {
|
|
442
|
+
const hookFile = path.join(hooksPath, hook);
|
|
443
|
+
if (!fs.existsSync(hookFile)) {
|
|
444
|
+
warnings.push(`Missing hook file: ${hook}`);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
} else {
|
|
448
|
+
warnings.push('Git hooks not pointing to Delimit. Run "delimit install" to fix.');
|
|
449
|
+
}
|
|
450
|
+
} catch (e) {
|
|
451
|
+
issues.push('Git hooks not configured. Run "delimit install" to set up.');
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Check PATH
|
|
455
|
+
const pathHasDelimit = process.env.PATH.includes('.delimit/shims');
|
|
456
|
+
if (pathHasDelimit) {
|
|
457
|
+
warnings.push('PATH hijacking is active (for AI tool interception)');
|
|
458
|
+
} else {
|
|
459
|
+
info.push('PATH is clean (no AI tool interception)');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Check policy files
|
|
463
|
+
const policies = [];
|
|
464
|
+
if (fs.existsSync('delimit.yml')) {
|
|
465
|
+
policies.push('project');
|
|
466
|
+
// Validate policy
|
|
467
|
+
try {
|
|
468
|
+
const policy = yaml.load(fs.readFileSync('delimit.yml', 'utf8'));
|
|
469
|
+
if (!policy.rules) {
|
|
470
|
+
warnings.push('Project policy has no rules defined');
|
|
471
|
+
}
|
|
472
|
+
} catch (e) {
|
|
473
|
+
issues.push(`Project policy is invalid: ${e.message}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const userPolicyPath = path.join(process.env.HOME, '.config', 'delimit', 'delimit.yml');
|
|
478
|
+
if (fs.existsSync(userPolicyPath)) {
|
|
479
|
+
policies.push('user');
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (policies.length === 0) {
|
|
483
|
+
warnings.push('No policy files found. Run "delimit policy --init" to create one.');
|
|
484
|
+
} else {
|
|
485
|
+
info.push(`Policy files loaded: ${policies.join(', ')}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Check audit log
|
|
489
|
+
const auditDir = path.join(process.env.HOME, '.delimit', 'audit');
|
|
490
|
+
if (fs.existsSync(auditDir)) {
|
|
491
|
+
const files = fs.readdirSync(auditDir);
|
|
492
|
+
info.push(`Audit log has ${files.length} day(s) of history`);
|
|
493
|
+
} else {
|
|
494
|
+
warnings.push('No audit logs found yet');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Display results
|
|
498
|
+
if (issues.length > 0) {
|
|
499
|
+
console.log(chalk.red.bold('❌ Issues Found:\n'));
|
|
500
|
+
issues.forEach(issue => console.log(chalk.red(` • ${issue}`)));
|
|
501
|
+
console.log();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (warnings.length > 0) {
|
|
505
|
+
console.log(chalk.yellow.bold('⚠️ Warnings:\n'));
|
|
506
|
+
warnings.forEach(warning => console.log(chalk.yellow(` • ${warning}`)));
|
|
507
|
+
console.log();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (info.length > 0) {
|
|
511
|
+
console.log(chalk.green.bold('✅ Working Correctly:\n'));
|
|
512
|
+
info.forEach(item => console.log(chalk.green(` • ${item}`)));
|
|
513
|
+
console.log();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Overall status
|
|
517
|
+
if (issues.length === 0) {
|
|
518
|
+
console.log(chalk.green.bold('🎉 Delimit is healthy!'));
|
|
519
|
+
} else {
|
|
520
|
+
console.log(chalk.red.bold('🔧 Please fix the issues above'));
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
// Explain-decision command - show governance decision reasoning
|
|
526
|
+
program
|
|
527
|
+
.command('explain-decision [decision-id]')
|
|
528
|
+
.description('Explain a governance decision')
|
|
529
|
+
.action(async (decisionId) => {
|
|
530
|
+
await ensureAgent();
|
|
531
|
+
|
|
532
|
+
try {
|
|
533
|
+
const { data } = await axios.get(`${AGENT_URL}/explain/${decisionId || 'last'}`);
|
|
534
|
+
console.log(data.explanation);
|
|
535
|
+
} catch (e) {
|
|
536
|
+
if (e.response?.status === 404) {
|
|
537
|
+
console.log(chalk.red('No decision found'));
|
|
538
|
+
} else {
|
|
539
|
+
console.log(chalk.red('Error fetching decision explanation'));
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// Uninstall command
|
|
545
|
+
program
|
|
546
|
+
.command('uninstall')
|
|
547
|
+
.description('Remove Delimit governance')
|
|
548
|
+
.action(async () => {
|
|
549
|
+
const { confirm } = await inquirer.prompt([{
|
|
550
|
+
type: 'confirm',
|
|
551
|
+
name: 'confirm',
|
|
552
|
+
message: 'This will remove all Delimit governance. Continue?',
|
|
553
|
+
default: false
|
|
554
|
+
}]);
|
|
555
|
+
|
|
556
|
+
if (!confirm) return;
|
|
557
|
+
|
|
558
|
+
// Remove Git hooks
|
|
559
|
+
try {
|
|
560
|
+
execSync('git config --global --unset core.hooksPath');
|
|
561
|
+
console.log(chalk.green('✓ Removed Git hooks'));
|
|
562
|
+
} catch (e) {}
|
|
563
|
+
|
|
564
|
+
// Remove from PATH
|
|
565
|
+
const profiles = ['.bashrc', '.zshrc', '.profile'];
|
|
566
|
+
profiles.forEach(profile => {
|
|
567
|
+
const profilePath = path.join(process.env.HOME, profile);
|
|
568
|
+
if (fs.existsSync(profilePath)) {
|
|
569
|
+
let content = fs.readFileSync(profilePath, 'utf8');
|
|
570
|
+
content = content.replace(/# Delimit Governance Layer[\s\S]*?fi\n/g, '');
|
|
571
|
+
fs.writeFileSync(profilePath, content);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
console.log(chalk.green('✓ Removed PATH modifications'));
|
|
575
|
+
|
|
576
|
+
console.log(chalk.yellow('\nRestart your terminal to complete uninstallation'));
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Helper function for installation
|
|
580
|
+
async function installDelimit(mode, scope, hooksType = 'all') {
|
|
581
|
+
const HOME = process.env.HOME;
|
|
582
|
+
const DELIMIT_HOME = path.join(HOME, '.delimit');
|
|
583
|
+
|
|
584
|
+
// Create directories
|
|
585
|
+
['bin', 'hooks', 'shims', 'config', 'audit', 'credentials'].forEach(dir => {
|
|
586
|
+
fs.mkdirSync(path.join(DELIMIT_HOME, dir), { recursive: true });
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
// Install hooks using the hooks installer
|
|
590
|
+
const hooksInstaller = new DelimitHooksInstaller();
|
|
591
|
+
|
|
592
|
+
if (hooksType === 'all' || hooksType === 'git') {
|
|
593
|
+
console.log(chalk.yellow('Installing Git hooks...'));
|
|
594
|
+
await hooksInstaller.installGitHooks();
|
|
595
|
+
console.log(chalk.green('✓ Installed Git hooks'));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (hooksType === 'all' || hooksType === 'ai') {
|
|
599
|
+
console.log(chalk.yellow('Installing AI tool hooks...'));
|
|
600
|
+
await hooksInstaller.installAIHooks();
|
|
601
|
+
console.log(chalk.green('✓ Installed AI tool hooks'));
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (hooksType === 'all' || hooksType === 'mcp') {
|
|
605
|
+
console.log(chalk.yellow('Installing MCP hooks...'));
|
|
606
|
+
await hooksInstaller.installMCPHooks();
|
|
607
|
+
console.log(chalk.green('✓ Installed MCP hooks'));
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Start agent
|
|
611
|
+
await ensureAgent();
|
|
612
|
+
|
|
613
|
+
// Set initial mode
|
|
614
|
+
await axios.post(`${AGENT_URL}/mode`, { mode });
|
|
615
|
+
|
|
616
|
+
// Create environment file
|
|
617
|
+
const envContent = `#!/bin/sh
|
|
618
|
+
# Delimit Governance Environment Variables
|
|
619
|
+
export DELIMIT_MODE="${mode}"
|
|
620
|
+
export DELIMIT_HOME="${DELIMIT_HOME}"
|
|
621
|
+
export DELIMIT_AGENT_URL="${AGENT_URL}"
|
|
622
|
+
export DELIMIT_ACTIVE=true
|
|
623
|
+
`;
|
|
624
|
+
fs.writeFileSync(path.join(DELIMIT_HOME, 'env'), envContent);
|
|
625
|
+
fs.chmodSync(path.join(DELIMIT_HOME, 'env'), '644');
|
|
626
|
+
|
|
627
|
+
console.log(chalk.green.bold('\n✅ Delimit installed successfully!\n'));
|
|
628
|
+
console.log('Next steps:');
|
|
629
|
+
console.log('1. Create policy file: delimit policy --init');
|
|
630
|
+
console.log('2. Check status: delimit status');
|
|
631
|
+
console.log('3. Switch modes: delimit mode <mode>');
|
|
632
|
+
console.log('4. Setup authentication: delimit auth');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Proxy command for AI tools
|
|
636
|
+
program
|
|
637
|
+
.command('proxy <tool>')
|
|
638
|
+
.allowUnknownOption()
|
|
639
|
+
.description('Proxy AI tool execution with governance')
|
|
640
|
+
.action(async (tool, options) => {
|
|
641
|
+
const { proxyAITool } = require('../lib/proxy-handler');
|
|
642
|
+
// Get all args after the tool name
|
|
643
|
+
const toolIndex = process.argv.indexOf(tool);
|
|
644
|
+
const args = process.argv.slice(toolIndex + 1);
|
|
645
|
+
await proxyAITool(tool, args);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// Hook handler (called by Git hooks)
|
|
649
|
+
program
|
|
650
|
+
.command('hook <type>')
|
|
651
|
+
.description('Internal hook handler')
|
|
652
|
+
.action(async (type) => {
|
|
653
|
+
await ensureAgent();
|
|
654
|
+
|
|
655
|
+
// Gather context
|
|
656
|
+
const context = {
|
|
657
|
+
command: type,
|
|
658
|
+
pwd: process.cwd(),
|
|
659
|
+
gitBranch: 'unknown',
|
|
660
|
+
files: [],
|
|
661
|
+
diff: ''
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// Try to get Git info, but don't fail if not in repo
|
|
665
|
+
try {
|
|
666
|
+
context.gitBranch = execSync('git branch --show-current 2>/dev/null').toString().trim() || 'unknown';
|
|
667
|
+
} catch (e) {
|
|
668
|
+
// Not in a Git repo or Git not available
|
|
669
|
+
context.gitBranch = 'unknown';
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (type === 'pre-commit') {
|
|
673
|
+
try {
|
|
674
|
+
context.files = execSync('git diff --cached --name-only 2>/dev/null').toString().split('\n').filter(f => f);
|
|
675
|
+
context.diff = execSync('git diff --cached 2>/dev/null').toString();
|
|
676
|
+
} catch (e) {
|
|
677
|
+
// Not in a Git repo or no staged changes
|
|
678
|
+
context.files = [];
|
|
679
|
+
context.diff = '';
|
|
680
|
+
}
|
|
681
|
+
} else if (type === 'pre-push') {
|
|
682
|
+
try {
|
|
683
|
+
// Get commits to be pushed
|
|
684
|
+
context.files = execSync('git diff --name-only @{upstream}...HEAD 2>/dev/null').toString().split('\n').filter(f => f);
|
|
685
|
+
context.diff = execSync('git diff @{upstream}...HEAD 2>/dev/null').toString();
|
|
686
|
+
} catch (e) {
|
|
687
|
+
// No upstream or not in repo
|
|
688
|
+
context.files = [];
|
|
689
|
+
context.diff = '';
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Query agent for decision
|
|
694
|
+
const { data: decision } = await axios.post(`${AGENT_URL}/evaluate`, context);
|
|
695
|
+
|
|
696
|
+
// Display decision
|
|
697
|
+
if (decision.message) {
|
|
698
|
+
const color = decision.action === 'block' ? chalk.red :
|
|
699
|
+
decision.action === 'prompt' ? chalk.yellow :
|
|
700
|
+
chalk.blue;
|
|
701
|
+
console.log(color(decision.message));
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Handle the decision
|
|
705
|
+
if (decision.action === 'block') {
|
|
706
|
+
if (decision.requiresOverride) {
|
|
707
|
+
console.log(chalk.red('Action blocked. Cannot override in enforce mode.'));
|
|
708
|
+
process.exit(1);
|
|
709
|
+
} else {
|
|
710
|
+
const { override } = await inquirer.prompt([{
|
|
711
|
+
type: 'confirm',
|
|
712
|
+
name: 'override',
|
|
713
|
+
message: 'Override and continue?',
|
|
714
|
+
default: false
|
|
715
|
+
}]);
|
|
716
|
+
|
|
717
|
+
if (!override) {
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
} else if (decision.action === 'prompt') {
|
|
722
|
+
const { proceed } = await inquirer.prompt([{
|
|
723
|
+
type: 'confirm',
|
|
724
|
+
name: 'proceed',
|
|
725
|
+
message: 'Continue with this action?',
|
|
726
|
+
default: false
|
|
727
|
+
}]);
|
|
728
|
+
|
|
729
|
+
if (!proceed) {
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Action allowed
|
|
735
|
+
process.exit(0);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
739
|
+
// V1 PUBLIC COMMANDS — API Contract Governance
|
|
740
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
741
|
+
|
|
742
|
+
const apiEngine = require('../lib/api-engine');
|
|
743
|
+
|
|
744
|
+
// Init command — scaffold .delimit/ config
|
|
745
|
+
program
|
|
746
|
+
.command('init')
|
|
747
|
+
.description('Initialize Delimit API governance in this project')
|
|
748
|
+
.action(async () => {
|
|
749
|
+
const configDir = path.join(process.cwd(), '.delimit');
|
|
750
|
+
const policyFile = path.join(configDir, 'policies.yml');
|
|
751
|
+
|
|
752
|
+
if (fs.existsSync(policyFile)) {
|
|
753
|
+
console.log(chalk.yellow('Already initialized — .delimit/policies.yml exists'));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
758
|
+
|
|
759
|
+
const template = `# Delimit API Governance Policy
|
|
760
|
+
# https://github.com/delimit-ai/delimit
|
|
761
|
+
|
|
762
|
+
# Override built-in rules (default: false)
|
|
763
|
+
override_defaults: false
|
|
764
|
+
|
|
765
|
+
rules: []
|
|
766
|
+
# Example:
|
|
767
|
+
# - id: protect_v1
|
|
768
|
+
# name: Protect V1 API
|
|
769
|
+
# change_types: [endpoint_removed, method_removed, field_removed]
|
|
770
|
+
# severity: error
|
|
771
|
+
# action: forbid
|
|
772
|
+
# conditions:
|
|
773
|
+
# path_pattern: "^/v1/.*"
|
|
774
|
+
# message: "V1 API is frozen. Make changes in V2."
|
|
775
|
+
`;
|
|
776
|
+
fs.writeFileSync(policyFile, template);
|
|
777
|
+
console.log(chalk.green('Created .delimit/policies.yml'));
|
|
778
|
+
console.log('');
|
|
779
|
+
console.log('Next steps:');
|
|
780
|
+
console.log(` ${chalk.bold('delimit lint')} old.yaml new.yaml — check for breaking changes`);
|
|
781
|
+
console.log(` ${chalk.bold('delimit diff')} old.yaml new.yaml — see all changes`);
|
|
782
|
+
console.log(` ${chalk.bold('delimit explain')} old.yaml new.yaml — human-readable summary`);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
// Lint command — diff + policy (primary command)
|
|
786
|
+
program
|
|
787
|
+
.command('lint <old_spec> <new_spec>')
|
|
788
|
+
.description('Lint API specs for breaking changes and policy violations')
|
|
789
|
+
.option('-p, --policy <file>', 'Custom policy file')
|
|
790
|
+
.option('--current-version <ver>', 'Current API version for semver bump')
|
|
791
|
+
.option('-n, --name <name>', 'API name for context')
|
|
792
|
+
.option('--json', 'Output raw JSON')
|
|
793
|
+
.action(async (oldSpec, newSpec, options) => {
|
|
794
|
+
try {
|
|
795
|
+
const result = apiEngine.lint(
|
|
796
|
+
path.resolve(oldSpec),
|
|
797
|
+
path.resolve(newSpec),
|
|
798
|
+
{ policy: options.policy, version: options.currentVersion, name: options.name }
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
if (options.json) {
|
|
802
|
+
console.log(JSON.stringify(result, null, 2));
|
|
803
|
+
process.exit(result.exit_code || 0);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Decision banner
|
|
808
|
+
const decision = result.decision;
|
|
809
|
+
const semver = result.semver;
|
|
810
|
+
const banner = decision === 'fail'
|
|
811
|
+
? chalk.red.bold('FAIL')
|
|
812
|
+
: decision === 'warn'
|
|
813
|
+
? chalk.yellow.bold('WARN')
|
|
814
|
+
: chalk.green.bold('PASS');
|
|
815
|
+
|
|
816
|
+
const bump = semver ? ` — ${chalk.bold(semver.bump.toUpperCase())}` : '';
|
|
817
|
+
const nextVer = semver && semver.next_version ? ` (${semver.next_version})` : '';
|
|
818
|
+
|
|
819
|
+
console.log(`\n${banner}${bump}${nextVer}\n`);
|
|
820
|
+
|
|
821
|
+
// Summary
|
|
822
|
+
const s = result.summary;
|
|
823
|
+
console.log(` Changes: ${s.total_changes} total, ${s.breaking_changes} breaking`);
|
|
824
|
+
if (s.violations > 0) {
|
|
825
|
+
console.log(` Violations: ${s.errors} error(s), ${s.warnings} warning(s)`);
|
|
826
|
+
}
|
|
827
|
+
console.log('');
|
|
828
|
+
|
|
829
|
+
// Violations
|
|
830
|
+
const violations = result.violations || [];
|
|
831
|
+
if (violations.length > 0) {
|
|
832
|
+
violations.forEach(v => {
|
|
833
|
+
const icon = v.severity === 'error' ? chalk.red('ERR') : chalk.yellow('WRN');
|
|
834
|
+
console.log(` ${icon} ${v.message}`);
|
|
835
|
+
if (v.path) console.log(` ${chalk.gray(v.path)}`);
|
|
836
|
+
});
|
|
837
|
+
console.log('');
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
// Non-breaking changes
|
|
841
|
+
const safe = (result.all_changes || []).filter(c => !c.is_breaking);
|
|
842
|
+
if (safe.length > 0) {
|
|
843
|
+
console.log(chalk.green(' Additions:'));
|
|
844
|
+
safe.forEach(c => console.log(` + ${c.message}`));
|
|
845
|
+
console.log('');
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
process.exit(result.exit_code || 0);
|
|
849
|
+
} catch (err) {
|
|
850
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
851
|
+
process.exit(1);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
// Diff command — pure diff, no policy
|
|
856
|
+
program
|
|
857
|
+
.command('diff <old_spec> <new_spec>')
|
|
858
|
+
.description('Show all changes between two API specs')
|
|
859
|
+
.option('--json', 'Output raw JSON')
|
|
860
|
+
.action(async (oldSpec, newSpec, options) => {
|
|
861
|
+
try {
|
|
862
|
+
const result = apiEngine.diff(
|
|
863
|
+
path.resolve(oldSpec),
|
|
864
|
+
path.resolve(newSpec)
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
if (options.json) {
|
|
868
|
+
console.log(JSON.stringify(result, null, 2));
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
console.log(`\n ${result.total_changes} change(s), ${result.breaking_changes} breaking\n`);
|
|
873
|
+
|
|
874
|
+
(result.changes || []).forEach(c => {
|
|
875
|
+
const tag = c.is_breaking ? chalk.red('[BREAKING]') : chalk.green('[safe]');
|
|
876
|
+
console.log(` ${tag} ${c.message}`);
|
|
877
|
+
});
|
|
878
|
+
console.log('');
|
|
879
|
+
} catch (err) {
|
|
880
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
881
|
+
process.exit(1);
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// Explain command — human-readable templates
|
|
886
|
+
program
|
|
887
|
+
.command('explain <old_spec> <new_spec>')
|
|
888
|
+
.description('Generate human-readable API change explanation')
|
|
889
|
+
.option('-t, --template <name>', 'Template: developer, team_lead, product, migration, changelog, pr_comment, slack', 'developer')
|
|
890
|
+
.option('--old-version <ver>', 'Old version')
|
|
891
|
+
.option('--new-version <ver>', 'New version')
|
|
892
|
+
.option('-n, --name <name>', 'API name')
|
|
893
|
+
.option('--json', 'Output raw JSON')
|
|
894
|
+
.action(async (oldSpec, newSpec, options) => {
|
|
895
|
+
try {
|
|
896
|
+
const result = apiEngine.explain(
|
|
897
|
+
path.resolve(oldSpec),
|
|
898
|
+
path.resolve(newSpec),
|
|
899
|
+
{
|
|
900
|
+
template: options.template,
|
|
901
|
+
oldVersion: options.oldVersion,
|
|
902
|
+
newVersion: options.newVersion,
|
|
903
|
+
name: options.name,
|
|
904
|
+
}
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
if (options.json) {
|
|
908
|
+
console.log(JSON.stringify(result, null, 2));
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
console.log('');
|
|
913
|
+
console.log(result.output);
|
|
914
|
+
console.log('');
|
|
915
|
+
} catch (err) {
|
|
916
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
917
|
+
process.exit(1);
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
program.parse();
|