@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,257 @@
1
+ import chalk from 'chalk';
2
+ import prompts from 'prompts';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { LocalScanner } from '../scanner/local-scan';
6
+ import { loadConfig, loadCredentials } from '../config/loader';
7
+ import { getStagedFiles, getStagedDiff } from '../git/git-utils';
8
+ import { ContinumApiClient } from '../api/client';
9
+ import { PatternUpdater } from '../scanner/pattern-updater';
10
+ import { UnknownPattern, RiskLevel } from '../types';
11
+
12
+ interface ScanOptions {
13
+ staged?: boolean;
14
+ hook?: boolean;
15
+ strict?: boolean;
16
+ autoApprove?: boolean;
17
+ warnOnly?: boolean;
18
+ files?: string[];
19
+ }
20
+
21
+ export async function scanCommand(options: ScanOptions): Promise<void> {
22
+ const config = loadConfig();
23
+ const credentials = loadCredentials();
24
+
25
+ // Update patterns if we have credentials
26
+ let customPatterns: any[] = [];
27
+ if (credentials.apiUrl && credentials.apiKey) {
28
+ try {
29
+ const client = new ContinumApiClient(credentials.apiUrl, credentials.apiKey);
30
+ const updater = new PatternUpdater(client);
31
+ await updater.updatePatterns();
32
+ customPatterns = updater.loadPatterns();
33
+ } catch (error) {
34
+ // Continue with built-in patterns only
35
+ }
36
+ }
37
+
38
+ // Determine which files to scan
39
+ let filesToScan: string[] = [];
40
+
41
+ if (options.staged) {
42
+ filesToScan = getStagedFiles();
43
+ } else if (options.files && options.files.length > 0) {
44
+ filesToScan = options.files;
45
+ } else {
46
+ console.log(chalk.red('✗ No files specified'));
47
+ console.log(chalk.gray(' Use --staged to scan staged files, or provide file paths'));
48
+ process.exit(1);
49
+ }
50
+
51
+ if (filesToScan.length === 0) {
52
+ console.log(chalk.yellow('⚠️ No files to scan'));
53
+ process.exit(0);
54
+ }
55
+
56
+ // Run scan
57
+ if (!options.hook) {
58
+ console.log(chalk.blue(`\nContinum — scanning ${filesToScan.length} file(s)...\n`));
59
+ }
60
+
61
+ const scanner = new LocalScanner(config, customPatterns);
62
+ const result = await scanner.scanFiles(filesToScan);
63
+
64
+ // Handle violations
65
+ if (result.violations.length > 0) {
66
+ console.log(chalk.red.bold('❌ BLOCKED\n'));
67
+
68
+ for (const violation of result.violations) {
69
+ const shouldBlock = config.block.includes(violation.severity);
70
+ const shouldWarn = config.warn.includes(violation.severity);
71
+
72
+ if (shouldBlock || shouldWarn) {
73
+ console.log(chalk.gray(`${violation.file} (line ${violation.line})`));
74
+ console.log(chalk.gray('─'.repeat(50)));
75
+ console.log(`Type: ${violation.type}`);
76
+ console.log(`Found: ${scanner.redactValue(violation.value)}`);
77
+ console.log(`Severity: ${getSeverityColor(violation.severity)(violation.severity)}`);
78
+ if (violation.message) {
79
+ console.log(`Message: ${violation.message}`);
80
+ }
81
+ console.log();
82
+ }
83
+ }
84
+
85
+ const blockedCount = result.violations.filter(v =>
86
+ config.block.includes(v.severity)
87
+ ).length;
88
+
89
+ if (blockedCount > 0 && !options.warnOnly) {
90
+ console.log(chalk.red('Fix these before committing.'));
91
+ console.log(chalk.gray('Override (not recommended): git commit --no-verify\n'));
92
+ process.exit(1);
93
+ }
94
+ }
95
+
96
+ // Handle unknown patterns
97
+ if (result.unknownPatterns.length > 0 && !options.autoApprove) {
98
+ for (const pattern of result.unknownPatterns) {
99
+ await handleUnknownPattern(pattern, scanner, credentials, options);
100
+ }
101
+ } else if (result.unknownPatterns.length > 0 && options.autoApprove) {
102
+ // Auto-approve all patterns
103
+ for (const pattern of result.unknownPatterns) {
104
+ await approvePattern(pattern, scanner, credentials, 'HIGH');
105
+ }
106
+ }
107
+
108
+ // Send background audit if we have credentials
109
+ if (credentials.apiUrl && credentials.apiKey && options.staged) {
110
+ try {
111
+ const client = new ContinumApiClient(credentials.apiUrl, credentials.apiKey);
112
+ const diff = getStagedDiff();
113
+ await client.sendSandboxAudit(diff, config.sandbox);
114
+ } catch (error) {
115
+ // Silent fail for background audit
116
+ }
117
+ }
118
+
119
+ // Success
120
+ if (result.clean) {
121
+ if (!options.hook) {
122
+ console.log(chalk.green('✓ Clean\n'));
123
+ }
124
+ process.exit(0);
125
+ }
126
+ }
127
+
128
+ async function handleUnknownPattern(
129
+ pattern: UnknownPattern,
130
+ scanner: LocalScanner,
131
+ credentials: { apiUrl?: string; apiKey?: string },
132
+ options: ScanOptions
133
+ ): Promise<void> {
134
+ console.log(chalk.yellow('\n⚠️ POSSIBLE CREDENTIAL DETECTED\n'));
135
+ console.log(chalk.gray(`${pattern.context.file} (line ${pattern.context.line})`));
136
+ console.log(chalk.gray('─'.repeat(50)));
137
+ console.log(`Type: UNKNOWN_PATTERN (${pattern.confidence} confidence)`);
138
+ console.log(`Found: ${scanner.redactValue(pattern.value)}`);
139
+ console.log(`Pattern: ${pattern.suggestedPattern}`);
140
+ console.log('\nThis looks like a credential, but it\'s not in our pattern library.\n');
141
+
142
+ if (options.strict) {
143
+ console.log(chalk.red('Commit blocked (strict mode)'));
144
+ process.exit(1);
145
+ }
146
+
147
+ const { action } = await prompts({
148
+ type: 'select',
149
+ name: 'action',
150
+ message: 'What would you like to do?',
151
+ choices: [
152
+ { title: 'Block this commit', value: 'block' },
153
+ { title: 'Approve pattern and block (will catch in future)', value: 'approve' },
154
+ { title: 'Ignore this pattern', value: 'ignore' },
155
+ { title: 'Continue anyway (not recommended)', value: 'continue' }
156
+ ]
157
+ });
158
+
159
+ switch (action) {
160
+ case 'approve':
161
+ await approvePattern(pattern, scanner, credentials);
162
+ console.log(chalk.green('✓ Pattern saved to your library'));
163
+ console.log(chalk.green('✓ This pattern will now be caught locally on future commits\n'));
164
+ process.exit(1);
165
+
166
+ case 'block':
167
+ process.exit(1);
168
+
169
+ case 'ignore':
170
+ console.log(chalk.gray('Pattern ignored\n'));
171
+ break;
172
+
173
+ case 'continue':
174
+ console.log(chalk.yellow('⚠️ Continuing without blocking\n'));
175
+ break;
176
+ }
177
+ }
178
+
179
+ async function approvePattern(
180
+ pattern: UnknownPattern,
181
+ scanner: LocalScanner,
182
+ credentials: { apiUrl?: string; apiKey?: string },
183
+ defaultSeverity?: RiskLevel
184
+ ): Promise<void> {
185
+ if (!credentials.apiUrl || !credentials.apiKey) {
186
+ console.log(chalk.red('✗ No API credentials configured'));
187
+ console.log(chalk.gray(' Run `continum init` to set up credentials'));
188
+ return;
189
+ }
190
+
191
+ let description = `Custom pattern for ${pattern.context.variableName || 'credential'}`;
192
+ let severity: RiskLevel = defaultSeverity || 'HIGH';
193
+
194
+ if (!defaultSeverity) {
195
+ const response = await prompts([
196
+ {
197
+ type: 'text',
198
+ name: 'description',
199
+ message: 'Pattern description:',
200
+ initial: description
201
+ },
202
+ {
203
+ type: 'select',
204
+ name: 'severity',
205
+ message: 'Severity level:',
206
+ choices: [
207
+ { title: 'Critical', value: 'CRITICAL' },
208
+ { title: 'High', value: 'HIGH' },
209
+ { title: 'Medium', value: 'MEDIUM' }
210
+ ],
211
+ initial: 1
212
+ }
213
+ ]);
214
+
215
+ if (response.description) description = response.description;
216
+ if (response.severity) severity = response.severity;
217
+ }
218
+
219
+ try {
220
+ const client = new ContinumApiClient(credentials.apiUrl, credentials.apiKey);
221
+ await client.approvePattern({
222
+ pattern: pattern.suggestedPattern,
223
+ patternType: 'CUSTOM',
224
+ description,
225
+ severity,
226
+ exampleValue: scanner.redactValue(pattern.value),
227
+ confidence: pattern.confidence,
228
+ context: {
229
+ file: pattern.context.file,
230
+ line: pattern.context.line,
231
+ variableName: pattern.context.variableName
232
+ }
233
+ });
234
+
235
+ // Update local cache
236
+ const updater = new PatternUpdater(client);
237
+ await updater.updatePatterns(true);
238
+ } catch (error) {
239
+ console.log(chalk.red('✗ Failed to save pattern'));
240
+ console.log(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`));
241
+ }
242
+ }
243
+
244
+ function getSeverityColor(severity: RiskLevel) {
245
+ switch (severity) {
246
+ case 'CRITICAL':
247
+ return chalk.red.bold;
248
+ case 'HIGH':
249
+ return chalk.red;
250
+ case 'MEDIUM':
251
+ return chalk.yellow;
252
+ case 'LOW':
253
+ return chalk.gray;
254
+ default:
255
+ return chalk.white;
256
+ }
257
+ }
@@ -0,0 +1,57 @@
1
+ import chalk from 'chalk';
2
+ import { loadConfig, loadCredentials } from '../config/loader';
3
+ import { ContinumApiClient } from '../api/client';
4
+ import { hasPreCommitHook, isGitRepository } from '../git/git-utils';
5
+
6
+ export async function statusCommand(): Promise<void> {
7
+ console.log(chalk.blue.bold('\n🛡️ Continum Status\n'));
8
+
9
+ // Check git repository
10
+ if (!isGitRepository()) {
11
+ console.log(chalk.red('✗ Not in a git repository'));
12
+ return;
13
+ }
14
+ console.log(chalk.green('✓ Git repository detected'));
15
+
16
+ // Check config file
17
+ const config = loadConfig();
18
+ if (config) {
19
+ console.log(chalk.green('✓ Configuration file found'));
20
+ console.log(chalk.gray(` Sandbox: ${config.sandbox}`));
21
+ console.log(chalk.gray(` Block: ${config.block.join(', ')}`));
22
+ console.log(chalk.gray(` Warn: ${config.warn.join(', ')}`));
23
+ } else {
24
+ console.log(chalk.yellow('⚠️ No configuration file'));
25
+ console.log(chalk.gray(' Run `continum init` to create one'));
26
+ }
27
+
28
+ // Check pre-commit hook
29
+ if (hasPreCommitHook()) {
30
+ console.log(chalk.green('✓ Pre-commit hook installed'));
31
+ } else {
32
+ console.log(chalk.yellow('⚠️ Pre-commit hook not installed'));
33
+ console.log(chalk.gray(' Run `continum init` to install'));
34
+ }
35
+
36
+ // Check API connection
37
+ const credentials = loadCredentials();
38
+ if (credentials.apiUrl && credentials.apiKey) {
39
+ console.log(chalk.gray('\nTesting API connection...'));
40
+
41
+ try {
42
+ const client = new ContinumApiClient(credentials.apiUrl, credentials.apiKey);
43
+ const status = await client.testConnection();
44
+ console.log(chalk.green(`✓ Connected to Continum API`));
45
+ console.log(chalk.gray(` Customer: ${status.customer}`));
46
+ console.log(chalk.gray(` Endpoint: ${credentials.apiUrl}`));
47
+ } catch (error) {
48
+ console.log(chalk.red('✗ Failed to connect to API'));
49
+ console.log(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`));
50
+ }
51
+ } else {
52
+ console.log(chalk.yellow('\n⚠️ No API credentials configured'));
53
+ console.log(chalk.gray(' Run `continum init` to set up'));
54
+ }
55
+
56
+ console.log();
57
+ }
@@ -0,0 +1,55 @@
1
+ import chalk from 'chalk';
2
+ import prompts from 'prompts';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { uninstallPreCommitHook, isGitRepository, getGitRoot } from '../git/git-utils';
6
+
7
+ export async function uninstallCommand(): Promise<void> {
8
+ console.log(chalk.blue.bold('\n🛡️ Uninstall Continum\n'));
9
+
10
+ if (!isGitRepository()) {
11
+ console.log(chalk.red('✗ Not in a git repository'));
12
+ process.exit(1);
13
+ }
14
+
15
+ const { confirm } = await prompts({
16
+ type: 'confirm',
17
+ name: 'confirm',
18
+ message: 'Remove Continum pre-commit hook from this repository?',
19
+ initial: false
20
+ });
21
+
22
+ if (!confirm) {
23
+ console.log(chalk.gray('Cancelled'));
24
+ process.exit(0);
25
+ }
26
+
27
+ try {
28
+ uninstallPreCommitHook();
29
+ console.log(chalk.green('✓ Pre-commit hook removed'));
30
+ } catch (error) {
31
+ console.log(chalk.red('✗ Failed to remove hook'));
32
+ console.log(chalk.gray(` ${error instanceof Error ? error.message : 'Unknown error'}`));
33
+ process.exit(1);
34
+ }
35
+
36
+ // Ask about config file
37
+ const gitRoot = getGitRoot();
38
+ const configPath = path.join(gitRoot, '.continum.json');
39
+
40
+ if (fs.existsSync(configPath)) {
41
+ const { removeConfig } = await prompts({
42
+ type: 'confirm',
43
+ name: 'removeConfig',
44
+ message: 'Also remove .continum.json config file?',
45
+ initial: false
46
+ });
47
+
48
+ if (removeConfig) {
49
+ fs.unlinkSync(configPath);
50
+ console.log(chalk.green('✓ Config file removed'));
51
+ }
52
+ }
53
+
54
+ console.log(chalk.blue('\n✓ Continum uninstalled\n'));
55
+ }
@@ -0,0 +1,23 @@
1
+ import { ContinumConfig } from '../types';
2
+
3
+ export const DEFAULT_CONFIG: ContinumConfig = {
4
+ scanOnCommit: true,
5
+ sandbox: 'default',
6
+ block: ['CRITICAL', 'HIGH'],
7
+ warn: ['MEDIUM'],
8
+ ignore: [
9
+ '.env.example',
10
+ '**/*.test.ts',
11
+ '**/*.test.js',
12
+ '**/*.spec.ts',
13
+ '**/*.spec.js',
14
+ '**/fixtures/**',
15
+ '**/mocks/**',
16
+ '**/test/**',
17
+ '**/tests/**',
18
+ '**/__tests__/**'
19
+ ],
20
+ patterns: {
21
+ custom: []
22
+ }
23
+ };
@@ -0,0 +1,67 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as os from 'os';
4
+ import { ContinumConfig } from '../types';
5
+ import { DEFAULT_CONFIG } from './default-config';
6
+
7
+ const CONFIG_FILE = '.continum.json';
8
+ const CACHE_DIR = path.join(os.homedir(), '.continum');
9
+ const CACHE_FILE = path.join(CACHE_DIR, 'patterns.json');
10
+ const CREDENTIALS_FILE = path.join(CACHE_DIR, 'credentials.json');
11
+
12
+ export function loadConfig(cwd: string = process.cwd()): ContinumConfig {
13
+ const configPath = path.join(cwd, CONFIG_FILE);
14
+
15
+ if (!fs.existsSync(configPath)) {
16
+ return DEFAULT_CONFIG;
17
+ }
18
+
19
+ try {
20
+ const configContent = fs.readFileSync(configPath, 'utf-8');
21
+ const config = JSON.parse(configContent);
22
+ return { ...DEFAULT_CONFIG, ...config };
23
+ } catch (error) {
24
+ console.error('Error loading config file, using defaults');
25
+ return DEFAULT_CONFIG;
26
+ }
27
+ }
28
+
29
+ export function saveConfig(config: ContinumConfig, cwd: string = process.cwd()): void {
30
+ const configPath = path.join(cwd, CONFIG_FILE);
31
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
32
+ }
33
+
34
+ export function loadCredentials(): { apiUrl?: string; apiKey?: string } {
35
+ if (!fs.existsSync(CREDENTIALS_FILE)) {
36
+ return {};
37
+ }
38
+
39
+ try {
40
+ const content = fs.readFileSync(CREDENTIALS_FILE, 'utf-8');
41
+ return JSON.parse(content);
42
+ } catch (error) {
43
+ return {};
44
+ }
45
+ }
46
+
47
+ export function saveCredentials(apiUrl: string, apiKey: string): void {
48
+ if (!fs.existsSync(CACHE_DIR)) {
49
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
50
+ }
51
+
52
+ fs.writeFileSync(
53
+ CREDENTIALS_FILE,
54
+ JSON.stringify({ apiUrl, apiKey }, null, 2)
55
+ );
56
+ }
57
+
58
+ export function getCacheDir(): string {
59
+ if (!fs.existsSync(CACHE_DIR)) {
60
+ fs.mkdirSync(CACHE_DIR, { recursive: true });
61
+ }
62
+ return CACHE_DIR;
63
+ }
64
+
65
+ export function getCacheFile(): string {
66
+ return CACHE_FILE;
67
+ }
@@ -0,0 +1,95 @@
1
+ import { execSync } from 'child_process';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+
5
+ export function getStagedFiles(): string[] {
6
+ try {
7
+ const output = execSync('git diff --cached --name-only --diff-filter=ACM', {
8
+ encoding: 'utf-8'
9
+ });
10
+
11
+ return output
12
+ .split('\n')
13
+ .filter(file => file.trim() !== '')
14
+ .map(file => file.trim());
15
+ } catch (error) {
16
+ throw new Error('Failed to get staged files. Are you in a git repository?');
17
+ }
18
+ }
19
+
20
+ export function getStagedDiff(): string {
21
+ try {
22
+ return execSync('git diff --cached', { encoding: 'utf-8' });
23
+ } catch (error) {
24
+ throw new Error('Failed to get staged diff');
25
+ }
26
+ }
27
+
28
+ export function isGitRepository(): boolean {
29
+ try {
30
+ execSync('git rev-parse --git-dir', { stdio: 'ignore' });
31
+ return true;
32
+ } catch (error) {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ export function getGitRoot(): string {
38
+ try {
39
+ const output = execSync('git rev-parse --show-toplevel', {
40
+ encoding: 'utf-8'
41
+ });
42
+ return output.trim();
43
+ } catch (error) {
44
+ throw new Error('Not in a git repository');
45
+ }
46
+ }
47
+
48
+ export function installPreCommitHook(): void {
49
+ const gitRoot = getGitRoot();
50
+ const hookPath = path.join(gitRoot, '.git', 'hooks', 'pre-commit');
51
+
52
+ const hookContent = `#!/bin/sh
53
+ # Continum pre-commit hook
54
+ # This hook runs the Continum CLI scanner before each commit
55
+
56
+ npx @continum/cli scan --staged --hook
57
+
58
+ # Exit with the same code as the scanner
59
+ exit $?
60
+ `;
61
+
62
+ fs.writeFileSync(hookPath, hookContent, { mode: 0o755 });
63
+ }
64
+
65
+ export function uninstallPreCommitHook(): void {
66
+ const gitRoot = getGitRoot();
67
+ const hookPath = path.join(gitRoot, '.git', 'hooks', 'pre-commit');
68
+
69
+ if (fs.existsSync(hookPath)) {
70
+ const content = fs.readFileSync(hookPath, 'utf-8');
71
+
72
+ // Only remove if it's our hook
73
+ if (content.includes('Continum pre-commit hook')) {
74
+ fs.unlinkSync(hookPath);
75
+ } else {
76
+ throw new Error('Pre-commit hook exists but is not a Continum hook. Remove manually.');
77
+ }
78
+ }
79
+ }
80
+
81
+ export function hasPreCommitHook(): boolean {
82
+ try {
83
+ const gitRoot = getGitRoot();
84
+ const hookPath = path.join(gitRoot, '.git', 'hooks', 'pre-commit');
85
+
86
+ if (!fs.existsSync(hookPath)) {
87
+ return false;
88
+ }
89
+
90
+ const content = fs.readFileSync(hookPath, 'utf-8');
91
+ return content.includes('Continum pre-commit hook');
92
+ } catch (error) {
93
+ return false;
94
+ }
95
+ }
package/src/index.ts ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { loginCommand } from './commands/login';
5
+ import { initCommand } from './commands/init';
6
+ import { scanCommand } from './commands/scan';
7
+ import { patternsUpdateCommand, patternsListCommand } from './commands/patterns';
8
+ import { statusCommand } from './commands/status';
9
+ import { uninstallCommand } from './commands/uninstall';
10
+
11
+ const program = new Command();
12
+
13
+ program
14
+ .name('continum')
15
+ .description('Continum CLI - Pre-commit credential scanner and pattern learning tool')
16
+ .version('0.1.0');
17
+
18
+ // Login command
19
+ program
20
+ .command('login')
21
+ .description('Authenticate with Continum (opens browser)')
22
+ .action(loginCommand);
23
+
24
+ // Init command
25
+ program
26
+ .command('init')
27
+ .description('Initialize Continum in a project (creates config, installs git hook)')
28
+ .option('--silent', 'Run without prompts (for postinstall scripts)')
29
+ .action(initCommand);
30
+
31
+ // Scan command
32
+ program
33
+ .command('scan')
34
+ .description('Scan files for credentials and sensitive data')
35
+ .option('--staged', 'Scan staged files (for pre-commit hook)')
36
+ .option('--hook', 'Running from git hook (minimal output)')
37
+ .option('--strict', 'Block on unknown patterns without prompting')
38
+ .option('--auto-approve', 'Automatically approve unknown patterns')
39
+ .option('--warn-only', 'Show warnings but don\'t block commits')
40
+ .argument('[files...]', 'Files to scan')
41
+ .action((files: string[], options: any) => {
42
+ scanCommand({ ...options, files });
43
+ });
44
+
45
+ // Patterns commands
46
+ const patterns = program
47
+ .command('patterns')
48
+ .description('Manage credential patterns');
49
+
50
+ patterns
51
+ .command('update')
52
+ .description('Update patterns from Continum API')
53
+ .action(patternsUpdateCommand);
54
+
55
+ patterns
56
+ .command('list')
57
+ .description('List all available patterns')
58
+ .action(patternsListCommand);
59
+
60
+ // Status command
61
+ program
62
+ .command('status')
63
+ .description('Check Continum configuration and connection')
64
+ .action(statusCommand);
65
+
66
+ // Uninstall command
67
+ program
68
+ .command('uninstall')
69
+ .description('Remove Continum pre-commit hook')
70
+ .action(uninstallCommand);
71
+
72
+ program.parse(process.argv);