@continum/cli 0.1.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 (79) hide show
  1. package/README.md +481 -0
  2. package/SETUP.md +517 -0
  3. package/dist/api/client.d.ts +17 -0
  4. package/dist/api/client.d.ts.map +1 -0
  5. package/dist/api/client.js +70 -0
  6. package/dist/api/client.js.map +1 -0
  7. package/dist/commands/init.d.ts +4 -0
  8. package/dist/commands/init.d.ts.map +1 -0
  9. package/dist/commands/init.js +104 -0
  10. package/dist/commands/init.js.map +1 -0
  11. package/dist/commands/login.d.ts +2 -0
  12. package/dist/commands/login.d.ts.map +1 -0
  13. package/dist/commands/login.js +217 -0
  14. package/dist/commands/login.js.map +1 -0
  15. package/dist/commands/patterns.d.ts +3 -0
  16. package/dist/commands/patterns.d.ts.map +1 -0
  17. package/dist/commands/patterns.js +67 -0
  18. package/dist/commands/patterns.js.map +1 -0
  19. package/dist/commands/scan.d.ts +11 -0
  20. package/dist/commands/scan.d.ts.map +1 -0
  21. package/dist/commands/scan.js +219 -0
  22. package/dist/commands/scan.js.map +1 -0
  23. package/dist/commands/status.d.ts +2 -0
  24. package/dist/commands/status.d.ts.map +1 -0
  25. package/dist/commands/status.js +61 -0
  26. package/dist/commands/status.js.map +1 -0
  27. package/dist/commands/uninstall.d.ts +2 -0
  28. package/dist/commands/uninstall.d.ts.map +1 -0
  29. package/dist/commands/uninstall.js +87 -0
  30. package/dist/commands/uninstall.js.map +1 -0
  31. package/dist/config/default-config.d.ts +3 -0
  32. package/dist/config/default-config.d.ts.map +1 -0
  33. package/dist/config/default-config.js +25 -0
  34. package/dist/config/default-config.js.map +1 -0
  35. package/dist/config/loader.d.ts +11 -0
  36. package/dist/config/loader.d.ts.map +1 -0
  37. package/dist/config/loader.js +96 -0
  38. package/dist/config/loader.js.map +1 -0
  39. package/dist/git/git-utils.d.ts +8 -0
  40. package/dist/git/git-utils.d.ts.map +1 -0
  41. package/dist/git/git-utils.js +130 -0
  42. package/dist/git/git-utils.js.map +1 -0
  43. package/dist/index.d.ts +3 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +63 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/scanner/local-scan.d.ts +15 -0
  48. package/dist/scanner/local-scan.d.ts.map +1 -0
  49. package/dist/scanner/local-scan.js +227 -0
  50. package/dist/scanner/local-scan.js.map +1 -0
  51. package/dist/scanner/pattern-updater.d.ts +12 -0
  52. package/dist/scanner/pattern-updater.d.ts.map +1 -0
  53. package/dist/scanner/pattern-updater.js +110 -0
  54. package/dist/scanner/pattern-updater.js.map +1 -0
  55. package/dist/scanner/patterns.d.ts +5 -0
  56. package/dist/scanner/patterns.d.ts.map +1 -0
  57. package/dist/scanner/patterns.js +145 -0
  58. package/dist/scanner/patterns.js.map +1 -0
  59. package/dist/types.d.ts +59 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +3 -0
  62. package/dist/types.js.map +1 -0
  63. package/package.json +40 -0
  64. package/src/api/client.ts +77 -0
  65. package/src/commands/init.ts +113 -0
  66. package/src/commands/login.ts +205 -0
  67. package/src/commands/patterns.ts +68 -0
  68. package/src/commands/scan.ts +257 -0
  69. package/src/commands/status.ts +57 -0
  70. package/src/commands/uninstall.ts +55 -0
  71. package/src/config/default-config.ts +23 -0
  72. package/src/config/loader.ts +67 -0
  73. package/src/git/git-utils.ts +95 -0
  74. package/src/index.ts +72 -0
  75. package/src/scanner/local-scan.ts +222 -0
  76. package/src/scanner/pattern-updater.ts +94 -0
  77. package/src/scanner/patterns.ts +156 -0
  78. package/src/types.ts +64 -0
  79. package/tsconfig.json +19 -0
@@ -0,0 +1,77 @@
1
+ import fetch from 'node-fetch';
2
+ import { Pattern, ApprovePatternDto } from '../types';
3
+
4
+ export class ContinumApiClient {
5
+ private apiUrl: string;
6
+ private apiKey: string;
7
+
8
+ constructor(apiUrl: string, apiKey: string) {
9
+ this.apiUrl = apiUrl.replace(/\/$/, ''); // Remove trailing slash
10
+ this.apiKey = apiKey;
11
+ }
12
+
13
+ async approvePattern(dto: ApprovePatternDto): Promise<{ success: boolean; patternId: string }> {
14
+ const response = await fetch(`${this.apiUrl}/patterns/approve`, {
15
+ method: 'POST',
16
+ headers: {
17
+ 'x-continum-key': this.apiKey,
18
+ 'Content-Type': 'application/json'
19
+ },
20
+ body: JSON.stringify(dto)
21
+ });
22
+
23
+ if (!response.ok) {
24
+ const error = await response.text();
25
+ throw new Error(`Failed to approve pattern: ${error}`);
26
+ }
27
+
28
+ return response.json() as Promise<{ success: boolean; patternId: string }>;
29
+ }
30
+
31
+ async getPatternLibrary(): Promise<Pattern[]> {
32
+ const response = await fetch(`${this.apiUrl}/patterns/library`, {
33
+ method: 'GET',
34
+ headers: {
35
+ 'x-continum-key': this.apiKey,
36
+ 'Content-Type': 'application/json'
37
+ }
38
+ });
39
+
40
+ if (!response.ok) {
41
+ const error = await response.text();
42
+ throw new Error(`Failed to fetch patterns: ${error}`);
43
+ }
44
+
45
+ return response.json() as Promise<Pattern[]>;
46
+ }
47
+
48
+ async sendSandboxAudit(diff: string, sandbox: string): Promise<void> {
49
+ // Fire and forget - don't wait for response
50
+ fetch(`${this.apiUrl}/patterns/audit`, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'x-continum-key': this.apiKey,
54
+ 'Content-Type': 'application/json'
55
+ },
56
+ body: JSON.stringify({ diff, sandbox })
57
+ }).catch(() => {
58
+ // Silent fail - this is background audit
59
+ });
60
+ }
61
+
62
+ async testConnection(): Promise<{ success: boolean; customer: string }> {
63
+ const response = await fetch(`${this.apiUrl}/patterns/status`, {
64
+ method: 'GET',
65
+ headers: {
66
+ 'x-continum-key': this.apiKey,
67
+ 'Content-Type': 'application/json'
68
+ }
69
+ });
70
+
71
+ if (!response.ok) {
72
+ throw new Error('Failed to connect to Continum API');
73
+ }
74
+
75
+ return response.json() as Promise<{ success: boolean; customer: string }>;
76
+ }
77
+ }
@@ -0,0 +1,113 @@
1
+ import chalk from 'chalk';
2
+ import prompts from 'prompts';
3
+ import { loadConfig, saveConfig, saveCredentials, loadCredentials } from '../config/loader';
4
+ import { installPreCommitHook, isGitRepository, hasPreCommitHook } from '../git/git-utils';
5
+ import { ContinumApiClient } from '../api/client';
6
+ import { DEFAULT_CONFIG } from '../config/default-config';
7
+
8
+ export async function initCommand(options: { silent?: boolean }): Promise<void> {
9
+ if (!options.silent) {
10
+ console.log(chalk.blue.bold('\n🛡️ Continum CLI Setup\n'));
11
+ }
12
+
13
+ // Check if in git repository
14
+ if (!isGitRepository()) {
15
+ console.log(chalk.red('✗ Not a git repository'));
16
+ console.log(chalk.gray(' Run this command in a git repository'));
17
+ process.exit(1);
18
+ }
19
+
20
+ // Check if already initialized
21
+ const existingConfig = loadConfig();
22
+ const hasHook = hasPreCommitHook();
23
+
24
+ if (existingConfig && hasHook && !options.silent) {
25
+ console.log(chalk.yellow('⚠️ Continum is already initialized in this repository'));
26
+ const { reinit } = await prompts({
27
+ type: 'confirm',
28
+ name: 'reinit',
29
+ message: 'Reinitialize?',
30
+ initial: false
31
+ });
32
+
33
+ if (!reinit) {
34
+ process.exit(0);
35
+ }
36
+ }
37
+
38
+ // Check for credentials - must be logged in first
39
+ const credentials = loadCredentials();
40
+
41
+ if (!credentials.apiUrl || !credentials.apiKey) {
42
+ console.log(chalk.red('✗ Not logged in to Continum\n'));
43
+ console.log(chalk.gray('Please run `continum login` first to authenticate.\n'));
44
+ console.log(chalk.blue(' $ continum login\n'));
45
+ process.exit(1);
46
+ }
47
+
48
+ // Test connection
49
+ if (!options.silent) {
50
+ console.log(chalk.gray('\nTesting connection...'));
51
+ }
52
+
53
+ try {
54
+ const client = new ContinumApiClient(credentials.apiUrl!, credentials.apiKey!);
55
+ const status = await client.testConnection();
56
+
57
+ if (!options.silent) {
58
+ console.log(chalk.green(`✓ Connected to Continum (${status.customer})`));
59
+ }
60
+ } catch (error) {
61
+ console.log(chalk.red('✗ Failed to connect to Continum API'));
62
+ console.log(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`));
63
+ process.exit(1);
64
+ }
65
+
66
+ // Get sandbox configuration
67
+ let sandbox = existingConfig?.sandbox || 'default';
68
+
69
+ if (!options.silent) {
70
+ const sandboxResponse = await prompts({
71
+ type: 'text',
72
+ name: 'sandbox',
73
+ message: 'Sandbox name:',
74
+ initial: sandbox
75
+ });
76
+
77
+ if (sandboxResponse.sandbox) {
78
+ sandbox = sandboxResponse.sandbox;
79
+ }
80
+ }
81
+
82
+ // Create config file
83
+ const config = {
84
+ ...DEFAULT_CONFIG,
85
+ sandbox,
86
+ apiUrl: credentials.apiUrl,
87
+ apiKey: credentials.apiKey
88
+ };
89
+
90
+ saveConfig(config);
91
+
92
+ if (!options.silent) {
93
+ console.log(chalk.green('✓ .continum.json created'));
94
+ }
95
+
96
+ // Install git hook
97
+ try {
98
+ installPreCommitHook();
99
+ if (!options.silent) {
100
+ console.log(chalk.green('✓ Pre-commit hook installed'));
101
+ }
102
+ } catch (error) {
103
+ console.log(chalk.red('✗ Failed to install pre-commit hook'));
104
+ console.log(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`));
105
+ process.exit(1);
106
+ }
107
+
108
+ if (!options.silent) {
109
+ console.log(chalk.blue.bold('\n✓ Continum is ready!\n'));
110
+ console.log(chalk.gray('Every commit will now be scanned locally.'));
111
+ console.log(chalk.gray('Violations are also audited by the Continum sandbox.\n'));
112
+ }
113
+ }
@@ -0,0 +1,205 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { createServer } from 'http';
4
+ import { parse } from 'url';
5
+ import { saveCredentials } from '../config/loader';
6
+ import { ContinumApiClient } from '../api/client';
7
+
8
+ const CALLBACK_PORT = 8765;
9
+ const CALLBACK_PATH = '/cli/callback';
10
+
11
+ interface AuthResponse {
12
+ apiKey: string;
13
+ apiUrl: string;
14
+ customer: string;
15
+ }
16
+
17
+ export async function loginCommand(): Promise<void> {
18
+ console.log(chalk.blue.bold('\n🛡️ Continum CLI Login\n'));
19
+
20
+ const apiUrl = process.env.CONTINUM_API_URL || 'https://api.continum.dev';
21
+ const consoleUrl = process.env.CONTINUM_CONSOLE_URL || 'https://console.continum.dev';
22
+
23
+ // Generate a random state for security
24
+ const state = generateRandomState();
25
+
26
+ // Start local server to receive callback
27
+ const authPromise = startCallbackServer(state);
28
+
29
+ // Build auth URL
30
+ const authUrl = `${consoleUrl}/cli/auth?state=${state}&port=${CALLBACK_PORT}`;
31
+
32
+ console.log(chalk.gray('Opening browser for authentication...\n'));
33
+ console.log(chalk.gray(`If browser doesn't open, visit:\n${authUrl}\n`));
34
+
35
+ // Open browser
36
+ const open = await import('open');
37
+ await open.default(authUrl);
38
+
39
+ const spinner = ora('Waiting for authentication...').start();
40
+
41
+ try {
42
+ // Wait for callback
43
+ const authResponse = await authPromise;
44
+ spinner.succeed('Authentication successful!');
45
+
46
+ // Save credentials
47
+ saveCredentials(authResponse.apiUrl || apiUrl, authResponse.apiKey);
48
+
49
+ // Test connection
50
+ console.log(chalk.gray('\nTesting connection...'));
51
+ const client = new ContinumApiClient(authResponse.apiUrl || apiUrl, authResponse.apiKey);
52
+ const status = await client.testConnection();
53
+
54
+ console.log(chalk.green(`✓ Connected to Continum (${status.customer})`));
55
+ console.log(chalk.blue.bold('\n✓ Login complete!\n'));
56
+ console.log(chalk.gray('You can now run `continum init` in your project.\n'));
57
+
58
+ process.exit(0);
59
+ } catch (error) {
60
+ spinner.fail('Authentication failed');
61
+ console.log(chalk.red(`\n✗ ${error instanceof Error ? error.message : 'Unknown error'}\n`));
62
+ process.exit(1);
63
+ }
64
+ }
65
+
66
+ function generateRandomState(): string {
67
+ return Math.random().toString(36).substring(2, 15) +
68
+ Math.random().toString(36).substring(2, 15);
69
+ }
70
+
71
+ function startCallbackServer(expectedState: string): Promise<AuthResponse> {
72
+ return new Promise((resolve, reject) => {
73
+ const timeout = setTimeout(() => {
74
+ server.close();
75
+ reject(new Error('Authentication timeout (5 minutes)'));
76
+ }, 5 * 60 * 1000); // 5 minutes
77
+
78
+ const server = createServer((req, res) => {
79
+ const parsedUrl = parse(req.url || '', true);
80
+
81
+ if (parsedUrl.pathname === CALLBACK_PATH) {
82
+ const { state, apiKey, apiUrl, customer, error } = parsedUrl.query;
83
+
84
+ // Check for errors
85
+ if (error) {
86
+ res.writeHead(400, { 'Content-Type': 'text/html' });
87
+ res.end(`
88
+ <!DOCTYPE html>
89
+ <html>
90
+ <head>
91
+ <title>Authentication Failed</title>
92
+ <style>
93
+ body { font-family: system-ui; max-width: 600px; margin: 100px auto; text-align: center; }
94
+ .error { color: #dc2626; font-size: 24px; margin-bottom: 20px; }
95
+ .message { color: #6b7280; }
96
+ </style>
97
+ </head>
98
+ <body>
99
+ <div class="error">❌ Authentication Failed</div>
100
+ <div class="message">${error}</div>
101
+ <p>You can close this window and try again.</p>
102
+ </body>
103
+ </html>
104
+ `);
105
+ clearTimeout(timeout);
106
+ server.close();
107
+ reject(new Error(error as string));
108
+ return;
109
+ }
110
+
111
+ // Verify state
112
+ if (state !== expectedState) {
113
+ res.writeHead(400, { 'Content-Type': 'text/html' });
114
+ res.end(`
115
+ <!DOCTYPE html>
116
+ <html>
117
+ <head>
118
+ <title>Authentication Failed</title>
119
+ <style>
120
+ body { font-family: system-ui; max-width: 600px; margin: 100px auto; text-align: center; }
121
+ .error { color: #dc2626; font-size: 24px; margin-bottom: 20px; }
122
+ </style>
123
+ </head>
124
+ <body>
125
+ <div class="error">❌ Invalid State</div>
126
+ <p>Security verification failed. Please try again.</p>
127
+ </body>
128
+ </html>
129
+ `);
130
+ clearTimeout(timeout);
131
+ server.close();
132
+ reject(new Error('Invalid state parameter'));
133
+ return;
134
+ }
135
+
136
+ // Success
137
+ res.writeHead(200, { 'Content-Type': 'text/html' });
138
+ res.end(`
139
+ <!DOCTYPE html>
140
+ <html>
141
+ <head>
142
+ <title>Authentication Successful</title>
143
+ <style>
144
+ body {
145
+ font-family: system-ui;
146
+ max-width: 600px;
147
+ margin: 100px auto;
148
+ text-align: center;
149
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
150
+ color: white;
151
+ padding: 40px;
152
+ }
153
+ .success { font-size: 48px; margin-bottom: 20px; }
154
+ .title { font-size: 32px; font-weight: bold; margin-bottom: 10px; }
155
+ .message { font-size: 18px; opacity: 0.9; }
156
+ .close {
157
+ margin-top: 30px;
158
+ padding: 12px 24px;
159
+ background: white;
160
+ color: #667eea;
161
+ border: none;
162
+ border-radius: 6px;
163
+ font-size: 16px;
164
+ cursor: pointer;
165
+ font-weight: 600;
166
+ }
167
+ .close:hover { background: #f3f4f6; }
168
+ </style>
169
+ </head>
170
+ <body>
171
+ <div class="success">✓</div>
172
+ <div class="title">Authentication Successful!</div>
173
+ <div class="message">You can now close this window and return to your terminal.</div>
174
+ <button class="close" onclick="window.close()">Close Window</button>
175
+ <script>
176
+ // Auto-close after 3 seconds
177
+ setTimeout(() => window.close(), 3000);
178
+ </script>
179
+ </body>
180
+ </html>
181
+ `);
182
+
183
+ clearTimeout(timeout);
184
+ server.close();
185
+ resolve({
186
+ apiKey: apiKey as string,
187
+ apiUrl: apiUrl as string,
188
+ customer: customer as string,
189
+ });
190
+ } else {
191
+ res.writeHead(404);
192
+ res.end('Not found');
193
+ }
194
+ });
195
+
196
+ server.listen(CALLBACK_PORT, () => {
197
+ // Server started
198
+ });
199
+
200
+ server.on('error', (err) => {
201
+ clearTimeout(timeout);
202
+ reject(new Error(`Failed to start callback server: ${err.message}`));
203
+ });
204
+ });
205
+ }
@@ -0,0 +1,68 @@
1
+ import chalk from 'chalk';
2
+ import { loadCredentials } from '../config/loader';
3
+ import { ContinumApiClient } from '../api/client';
4
+ import { PatternUpdater } from '../scanner/pattern-updater';
5
+
6
+ export async function patternsUpdateCommand(): Promise<void> {
7
+ const credentials = loadCredentials();
8
+
9
+ if (!credentials.apiUrl || !credentials.apiKey) {
10
+ console.log(chalk.red('✗ No API credentials configured'));
11
+ console.log(chalk.gray(' Run `continum init` to set up credentials'));
12
+ process.exit(1);
13
+ }
14
+
15
+ console.log(chalk.blue('Updating patterns...'));
16
+
17
+ try {
18
+ const client = new ContinumApiClient(credentials.apiUrl, credentials.apiKey);
19
+ const updater = new PatternUpdater(client);
20
+ await updater.updatePatterns(true);
21
+
22
+ const patterns = updater.loadPatterns();
23
+ console.log(chalk.green(`✓ Updated ${patterns.length} patterns`));
24
+ } catch (error) {
25
+ console.log(chalk.red('✗ Failed to update patterns'));
26
+ console.log(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`));
27
+ process.exit(1);
28
+ }
29
+ }
30
+
31
+ export async function patternsListCommand(): Promise<void> {
32
+ const credentials = loadCredentials();
33
+
34
+ if (!credentials.apiUrl || !credentials.apiKey) {
35
+ console.log(chalk.red('✗ No API credentials configured'));
36
+ console.log(chalk.gray(' Run `continum init` to set up credentials'));
37
+ process.exit(1);
38
+ }
39
+
40
+ try {
41
+ const client = new ContinumApiClient(credentials.apiUrl, credentials.apiKey);
42
+ const updater = new PatternUpdater(client);
43
+ const patterns = updater.loadPatterns();
44
+
45
+ console.log(chalk.blue.bold('\n🛡️ Pattern Library\n'));
46
+
47
+ const byType: { [key: string]: typeof patterns } = {};
48
+ patterns.forEach(p => {
49
+ if (!byType[p.patternType]) {
50
+ byType[p.patternType] = [];
51
+ }
52
+ byType[p.patternType].push(p);
53
+ });
54
+
55
+ for (const [type, typePatterns] of Object.entries(byType)) {
56
+ console.log(chalk.bold(`\n${type} (${typePatterns.length})`));
57
+ typePatterns.forEach(p => {
58
+ console.log(chalk.gray(` ${p.description}`));
59
+ console.log(chalk.gray(` Pattern: ${p.pattern}`));
60
+ console.log(chalk.gray(` Severity: ${p.severity}\n`));
61
+ });
62
+ }
63
+ } catch (error) {
64
+ console.log(chalk.red('✗ Failed to load patterns'));
65
+ console.log(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`));
66
+ process.exit(1);
67
+ }
68
+ }