codequiry-cli 1.0.0 → 2.0.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/LICENSE +21 -674
- package/README.md +74 -179
- package/bin/codequiry.js +5 -0
- package/package.json +39 -26
- package/src/api/client.js +35 -0
- package/src/api/endpoints.js +148 -0
- package/src/commands/account.js +61 -0
- package/src/commands/auth.js +56 -0
- package/src/commands/checks.js +22 -0
- package/src/commands/create.js +86 -0
- package/src/commands/delete.js +72 -0
- package/src/commands/results.js +118 -0
- package/src/commands/scan.js +189 -0
- package/src/commands/start.js +76 -0
- package/src/commands/status.js +65 -0
- package/src/commands/upload.js +102 -0
- package/src/index.js +132 -51
- package/src/ui/menu.js +85 -0
- package/src/ui/prompts.js +86 -0
- package/src/utils/banner.js +20 -0
- package/src/utils/config.js +67 -0
- package/src/utils/display.js +122 -0
- package/src/utils/errors.js +63 -0
- package/src/utils/poller.js +56 -0
- package/src/utils/zipper.js +96 -0
- package/src/auth.js +0 -47
- package/src/check.js +0 -444
- package/src/const.js +0 -56
- package/src/util.js +0 -26
- package/uploads/.gitkeep +0 -0
package/src/index.js
CHANGED
|
@@ -1,51 +1,132 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
program
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Command } = require('commander');
|
|
4
|
+
const program = new Command();
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.name('codequiry')
|
|
8
|
+
.description('Codequiry CLI - Source Code Similarity Checker')
|
|
9
|
+
.version('2.0.1');
|
|
10
|
+
|
|
11
|
+
// Auth command
|
|
12
|
+
program
|
|
13
|
+
.command('auth')
|
|
14
|
+
.description('Authenticate with your Codequiry API key')
|
|
15
|
+
.option('--status', 'Show current auth status')
|
|
16
|
+
.option('--logout', 'Remove saved API key')
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
const authCommand = require('./commands/auth');
|
|
19
|
+
await authCommand(options);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Account command
|
|
23
|
+
program
|
|
24
|
+
.command('account')
|
|
25
|
+
.description('Show account info and usage quota')
|
|
26
|
+
.action(async () => {
|
|
27
|
+
const accountCommand = require('./commands/account');
|
|
28
|
+
await accountCommand();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Checks command
|
|
32
|
+
program
|
|
33
|
+
.command('checks')
|
|
34
|
+
.description('List all your checks')
|
|
35
|
+
.action(async () => {
|
|
36
|
+
const checksCommand = require('./commands/checks');
|
|
37
|
+
await checksCommand();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Create command
|
|
41
|
+
program
|
|
42
|
+
.command('create')
|
|
43
|
+
.description('Create a new check')
|
|
44
|
+
.option('-n, --name <name>', 'Check name')
|
|
45
|
+
.option('-l, --language <id>', 'Language ID')
|
|
46
|
+
.option('-t, --test-type <id>', 'Test type ID')
|
|
47
|
+
.action(async (options) => {
|
|
48
|
+
const createCommand = require('./commands/create');
|
|
49
|
+
await createCommand(options);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Upload command
|
|
53
|
+
program
|
|
54
|
+
.command('upload [path]')
|
|
55
|
+
.description('Upload files or folders to a check')
|
|
56
|
+
.option('-c, --check-id <id>', 'Check ID')
|
|
57
|
+
.action(async (inputPath, options) => {
|
|
58
|
+
const uploadCommand = require('./commands/upload');
|
|
59
|
+
await uploadCommand(inputPath, options);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Start command
|
|
63
|
+
program
|
|
64
|
+
.command('start')
|
|
65
|
+
.description('Start a check')
|
|
66
|
+
.option('-c, --check-id <id>', 'Check ID')
|
|
67
|
+
.option('--web', 'Enable web check')
|
|
68
|
+
.option('--db', 'Enable database check')
|
|
69
|
+
.action(async (options) => {
|
|
70
|
+
const startCommand = require('./commands/start');
|
|
71
|
+
await startCommand(options);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Status command
|
|
75
|
+
program
|
|
76
|
+
.command('status')
|
|
77
|
+
.description('Check the status of a check')
|
|
78
|
+
.option('-c, --check <id>', 'Check ID')
|
|
79
|
+
.option('-p, --poll', 'Live poll until complete')
|
|
80
|
+
.action(async (options) => {
|
|
81
|
+
const statusCommand = require('./commands/status');
|
|
82
|
+
await statusCommand(options);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Results command
|
|
86
|
+
program
|
|
87
|
+
.command('results')
|
|
88
|
+
.description('View check results')
|
|
89
|
+
.option('-c, --check <id>', 'Check ID')
|
|
90
|
+
.option('-o, --output <format>', 'Output format (json)')
|
|
91
|
+
.action(async (options) => {
|
|
92
|
+
const resultsCommand = require('./commands/results');
|
|
93
|
+
await resultsCommand(options);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Delete command
|
|
97
|
+
program
|
|
98
|
+
.command('delete')
|
|
99
|
+
.description('Delete a check')
|
|
100
|
+
.option('-c, --check <id>', 'Check ID')
|
|
101
|
+
.option('-f, --force', 'Skip confirmation')
|
|
102
|
+
.action(async (options) => {
|
|
103
|
+
const deleteCommand = require('./commands/delete');
|
|
104
|
+
await deleteCommand(options);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Scan command (the killer feature)
|
|
108
|
+
program
|
|
109
|
+
.command('scan [path]')
|
|
110
|
+
.description('Full scan: zip → create → upload → start → poll → results')
|
|
111
|
+
.option('-n, --name <name>', 'Check name')
|
|
112
|
+
.option('-l, --language <id>', 'Language ID')
|
|
113
|
+
.option('-t, --test-type <id>', 'Test type ID')
|
|
114
|
+
.option('--web', 'Enable web check')
|
|
115
|
+
.option('--db', 'Enable database check')
|
|
116
|
+
.option('-o, --output <format>', 'Output format (json)')
|
|
117
|
+
.option('--threshold <percent>', 'Fail if similarity exceeds threshold (for CI)')
|
|
118
|
+
.action(async (inputPath, options) => {
|
|
119
|
+
const scanCommand = require('./commands/scan');
|
|
120
|
+
await scanCommand(inputPath, options);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Default: interactive TUI when no args
|
|
124
|
+
if (process.argv.length <= 2) {
|
|
125
|
+
const { mainMenu } = require('./ui/menu');
|
|
126
|
+
mainMenu().catch((err) => {
|
|
127
|
+
console.error(err.message);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
|
130
|
+
} else {
|
|
131
|
+
program.parse(process.argv);
|
|
132
|
+
}
|
package/src/ui/menu.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const { showBanner } = require('../utils/banner');
|
|
6
|
+
const { isAuthenticated } = require('../utils/config');
|
|
7
|
+
|
|
8
|
+
async function mainMenu() {
|
|
9
|
+
showBanner();
|
|
10
|
+
|
|
11
|
+
if (!isAuthenticated()) {
|
|
12
|
+
console.log(chalk.yellow(' You need to authenticate first.\n'));
|
|
13
|
+
const authCommand = require('../commands/auth');
|
|
14
|
+
await authCommand({});
|
|
15
|
+
if (!isAuthenticated()) return;
|
|
16
|
+
console.log('');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let running = true;
|
|
20
|
+
|
|
21
|
+
while (running) {
|
|
22
|
+
const { action } = await inquirer.prompt([
|
|
23
|
+
{
|
|
24
|
+
type: 'list',
|
|
25
|
+
name: 'action',
|
|
26
|
+
message: 'What would you like to do?',
|
|
27
|
+
choices: [
|
|
28
|
+
{ name: '🔍 Scan - Analyze code similarity', value: 'scan' },
|
|
29
|
+
{ name: '📋 Checks - View all checks', value: 'checks' },
|
|
30
|
+
{ name: '➕ Create - New check', value: 'create' },
|
|
31
|
+
{ name: '📤 Upload - Upload files to a check', value: 'upload' },
|
|
32
|
+
{ name: '▶️ Start - Start a check', value: 'start' },
|
|
33
|
+
{ name: '📊 Status - Check progress', value: 'status' },
|
|
34
|
+
{ name: '📈 Results - View results', value: 'results' },
|
|
35
|
+
{ name: '👤 Account - Account info', value: 'account' },
|
|
36
|
+
{ name: '🔑 Auth - Manage API key', value: 'auth' },
|
|
37
|
+
new inquirer.Separator(),
|
|
38
|
+
{ name: '🚪 Exit', value: 'exit' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
console.log('');
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
switch (action) {
|
|
47
|
+
case 'scan':
|
|
48
|
+
await require('../commands/scan')(undefined, {});
|
|
49
|
+
break;
|
|
50
|
+
case 'checks':
|
|
51
|
+
await require('../commands/checks')();
|
|
52
|
+
break;
|
|
53
|
+
case 'create':
|
|
54
|
+
await require('../commands/create')({});
|
|
55
|
+
break;
|
|
56
|
+
case 'upload':
|
|
57
|
+
await require('../commands/upload')(undefined, {});
|
|
58
|
+
break;
|
|
59
|
+
case 'start':
|
|
60
|
+
await require('../commands/start')({});
|
|
61
|
+
break;
|
|
62
|
+
case 'status':
|
|
63
|
+
await require('../commands/status')({ poll: true });
|
|
64
|
+
break;
|
|
65
|
+
case 'results':
|
|
66
|
+
await require('../commands/results')({});
|
|
67
|
+
break;
|
|
68
|
+
case 'account':
|
|
69
|
+
await require('../commands/account')();
|
|
70
|
+
break;
|
|
71
|
+
case 'auth':
|
|
72
|
+
await require('../commands/auth')({});
|
|
73
|
+
break;
|
|
74
|
+
case 'exit':
|
|
75
|
+
running = false;
|
|
76
|
+
console.log(chalk.dim(' Goodbye!\n'));
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error(chalk.red('\n Error: ' + error.message + '\n'));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { mainMenu };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const ora = require('ora');
|
|
5
|
+
const { listChecks, getLanguages, getTestTypes } = require('../api/endpoints');
|
|
6
|
+
|
|
7
|
+
async function selectCheck(message = 'Select a check:') {
|
|
8
|
+
const spinner = ora('Fetching checks...').start();
|
|
9
|
+
const data = await listChecks();
|
|
10
|
+
const checks = Array.isArray(data) ? data : data?.checks || [];
|
|
11
|
+
spinner.stop();
|
|
12
|
+
|
|
13
|
+
if (checks.length === 0) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { checkId } = await inquirer.prompt([
|
|
18
|
+
{
|
|
19
|
+
type: 'list',
|
|
20
|
+
name: 'checkId',
|
|
21
|
+
message,
|
|
22
|
+
choices: checks.map((c) => ({
|
|
23
|
+
name: `${c.id} - ${c.name} (${c.language_name || c.language || ''})`,
|
|
24
|
+
value: c.id,
|
|
25
|
+
})),
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
return checkId;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function selectLanguage(message = 'Programming language:') {
|
|
33
|
+
const spinner = ora('Loading languages...').start();
|
|
34
|
+
const data = await getLanguages();
|
|
35
|
+
const languages = Array.isArray(data) ? data : data?.languages || [];
|
|
36
|
+
spinner.stop();
|
|
37
|
+
|
|
38
|
+
const { languageId } = await inquirer.prompt([
|
|
39
|
+
{
|
|
40
|
+
type: 'list',
|
|
41
|
+
name: 'languageId',
|
|
42
|
+
message,
|
|
43
|
+
choices: languages.map((l) => ({
|
|
44
|
+
name: l.name || l.language,
|
|
45
|
+
value: l.id,
|
|
46
|
+
})),
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
return languageId;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function selectTestType(message = 'Check engine:') {
|
|
54
|
+
const spinner = ora('Loading test types...').start();
|
|
55
|
+
const data = await getTestTypes();
|
|
56
|
+
const types = Array.isArray(data) ? data : data?.test_types || [];
|
|
57
|
+
spinner.stop();
|
|
58
|
+
|
|
59
|
+
const { testType } = await inquirer.prompt([
|
|
60
|
+
{
|
|
61
|
+
type: 'list',
|
|
62
|
+
name: 'testType',
|
|
63
|
+
message,
|
|
64
|
+
choices: types.map((t) => ({
|
|
65
|
+
name: t.name || t.type,
|
|
66
|
+
value: t.id,
|
|
67
|
+
})),
|
|
68
|
+
},
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
return testType;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function confirmAction(message, defaultVal = false) {
|
|
75
|
+
const { confirmed } = await inquirer.prompt([
|
|
76
|
+
{
|
|
77
|
+
type: 'confirm',
|
|
78
|
+
name: 'confirmed',
|
|
79
|
+
message,
|
|
80
|
+
default: defaultVal,
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
return confirmed;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = { selectCheck, selectLanguage, selectTestType, confirmAction };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const figlet = require('figlet');
|
|
4
|
+
const gradient = require('gradient-string');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
function showBanner() {
|
|
8
|
+
try {
|
|
9
|
+
const text = figlet.textSync('Codequiry', {
|
|
10
|
+
font: 'Standard',
|
|
11
|
+
horizontalLayout: 'default',
|
|
12
|
+
});
|
|
13
|
+
console.log(gradient.vice(text));
|
|
14
|
+
console.log(chalk.dim(' Source Code Similarity Checker') + ' ' + chalk.dim('v2.0.1\n'));
|
|
15
|
+
} catch {
|
|
16
|
+
console.log(gradient.vice('\n Codequiry CLI v2.0.1\n'));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { showBanner };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), '.codequiry');
|
|
8
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
9
|
+
|
|
10
|
+
function ensureConfigDir() {
|
|
11
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
12
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readConfig() {
|
|
17
|
+
ensureConfigDir();
|
|
18
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
23
|
+
} catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeConfig(config) {
|
|
29
|
+
ensureConfigDir();
|
|
30
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getApiKey() {
|
|
34
|
+
// Environment variable takes precedence
|
|
35
|
+
if (process.env.CODEQUIRY_API_KEY) {
|
|
36
|
+
return process.env.CODEQUIRY_API_KEY;
|
|
37
|
+
}
|
|
38
|
+
const config = readConfig();
|
|
39
|
+
return config.apiKey || null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function setApiKey(apiKey) {
|
|
43
|
+
const config = readConfig();
|
|
44
|
+
config.apiKey = apiKey;
|
|
45
|
+
writeConfig(config);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function clearApiKey() {
|
|
49
|
+
const config = readConfig();
|
|
50
|
+
delete config.apiKey;
|
|
51
|
+
writeConfig(config);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isAuthenticated() {
|
|
55
|
+
return !!getApiKey();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = {
|
|
59
|
+
CONFIG_DIR,
|
|
60
|
+
CONFIG_FILE,
|
|
61
|
+
readConfig,
|
|
62
|
+
writeConfig,
|
|
63
|
+
getApiKey,
|
|
64
|
+
setApiKey,
|
|
65
|
+
clearApiKey,
|
|
66
|
+
isAuthenticated,
|
|
67
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Table = require('cli-table3');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
function colorScore(score) {
|
|
7
|
+
const num = parseFloat(score);
|
|
8
|
+
if (isNaN(num)) return chalk.dim(score);
|
|
9
|
+
if (num > 70) return chalk.red.bold(num.toFixed(1) + '%');
|
|
10
|
+
if (num > 30) return chalk.yellow(num.toFixed(1) + '%');
|
|
11
|
+
return chalk.green(num.toFixed(1) + '%');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function statusColor(status) {
|
|
15
|
+
const s = String(status).toLowerCase();
|
|
16
|
+
if (s === 'completed' || s === 'done') return chalk.green(status);
|
|
17
|
+
if (s === 'checking' || s === 'waiting' || s === 'uploading') return chalk.yellow(status);
|
|
18
|
+
if (s === 'failed' || s === 'error') return chalk.red(status);
|
|
19
|
+
return chalk.dim(status);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// API returns array of Assignment objects directly from getChecks
|
|
23
|
+
function checksTable(checks) {
|
|
24
|
+
const table = new Table({
|
|
25
|
+
head: [
|
|
26
|
+
chalk.cyan('ID'),
|
|
27
|
+
chalk.cyan('Name'),
|
|
28
|
+
chalk.cyan('Language'),
|
|
29
|
+
chalk.cyan('Status'),
|
|
30
|
+
chalk.cyan('Created'),
|
|
31
|
+
],
|
|
32
|
+
style: { head: [], border: ['dim'] },
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const list = Array.isArray(checks) ? checks : [];
|
|
36
|
+
|
|
37
|
+
if (list.length === 0) {
|
|
38
|
+
console.log(chalk.dim('\n No checks found.\n'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
list.forEach((c) => {
|
|
43
|
+
// Assignment model fields from Laravel
|
|
44
|
+
const statusName = c.assignmentstatuses?.status || '';
|
|
45
|
+
table.push([
|
|
46
|
+
chalk.white(c.id),
|
|
47
|
+
c.name || '',
|
|
48
|
+
String(c.language_id || ''),
|
|
49
|
+
statusColor(statusName),
|
|
50
|
+
c.created_at ? new Date(c.created_at).toLocaleDateString() : '',
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.log('\n' + table.toString() + '\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// API overview returns {overviewURL, submissions, bardata}
|
|
58
|
+
// submissions have: id, filename, result1 (score), assignment_id, etc.
|
|
59
|
+
function overviewTable(overview) {
|
|
60
|
+
const submissions = overview?.submissions || [];
|
|
61
|
+
|
|
62
|
+
if (!Array.isArray(submissions) || submissions.length === 0) {
|
|
63
|
+
console.log(chalk.dim('\n No results available.\n'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const table = new Table({
|
|
68
|
+
head: [
|
|
69
|
+
chalk.cyan('ID'),
|
|
70
|
+
chalk.cyan('Filename'),
|
|
71
|
+
chalk.cyan('Similarity'),
|
|
72
|
+
],
|
|
73
|
+
style: { head: [], border: ['dim'] },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
submissions.forEach((s) => {
|
|
77
|
+
const score = s.result1 || s.total_result || 0;
|
|
78
|
+
table.push([
|
|
79
|
+
chalk.white(s.id || ''),
|
|
80
|
+
s.filename || '',
|
|
81
|
+
colorScore(score),
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
console.log('\n' + table.toString() + '\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// API results returns {submission, avg, max, min, other_matches, related_submissions, related_files}
|
|
89
|
+
function resultsDetailTable(results) {
|
|
90
|
+
const matches = results?.other_matches || [];
|
|
91
|
+
|
|
92
|
+
if (!Array.isArray(matches) || matches.length === 0) {
|
|
93
|
+
console.log(chalk.dim('\n No matches found.\n'));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const table = new Table({
|
|
98
|
+
head: [
|
|
99
|
+
chalk.cyan('Matched With'),
|
|
100
|
+
chalk.cyan('Tokens'),
|
|
101
|
+
chalk.cyan('Type'),
|
|
102
|
+
],
|
|
103
|
+
style: { head: [], border: ['dim'] },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
matches.forEach((m) => {
|
|
107
|
+
table.push([
|
|
108
|
+
m.filename || m.other_filename || '',
|
|
109
|
+
String(m.tokens || ''),
|
|
110
|
+
m.type || '',
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
console.log('\n' + table.toString());
|
|
115
|
+
|
|
116
|
+
// Show stats
|
|
117
|
+
if (results.avg !== undefined) {
|
|
118
|
+
console.log(chalk.dim(` Avg: ${results.avg}% Max: ${results.max}% Min: ${results.min}%\n`));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { colorScore, statusColor, checksTable, overviewTable, resultsDetailTable };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
function handleApiError(error) {
|
|
6
|
+
if (error.response) {
|
|
7
|
+
const { status, data } = error.response;
|
|
8
|
+
const message = data?.message || data?.error || 'Unknown error';
|
|
9
|
+
|
|
10
|
+
switch (status) {
|
|
11
|
+
case 401:
|
|
12
|
+
console.error(chalk.red('\n✖ Authentication failed. Check your API key.'));
|
|
13
|
+
console.error(chalk.dim(' Run `codequiry auth` to update your key.\n'));
|
|
14
|
+
break;
|
|
15
|
+
case 403:
|
|
16
|
+
console.error(chalk.red('\n✖ Access denied: ' + message));
|
|
17
|
+
break;
|
|
18
|
+
case 404:
|
|
19
|
+
console.error(chalk.red('\n✖ Not found: ' + message));
|
|
20
|
+
break;
|
|
21
|
+
case 422:
|
|
22
|
+
console.error(chalk.red('\n✖ Validation error: ' + message));
|
|
23
|
+
if (data?.errors) {
|
|
24
|
+
Object.entries(data.errors).forEach(([field, msgs]) => {
|
|
25
|
+
const msgList = Array.isArray(msgs) ? msgs : [msgs];
|
|
26
|
+
msgList.forEach((m) => console.error(chalk.yellow(` • ${field}: ${m}`)));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
case 429:
|
|
31
|
+
console.error(chalk.red('\n✖ Rate limit exceeded. Please wait and try again.'));
|
|
32
|
+
break;
|
|
33
|
+
case 500:
|
|
34
|
+
console.error(chalk.red('\n✖ Server error. Please try again later.'));
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
console.error(chalk.red(`\n✖ API error (${status}): ${message}`));
|
|
38
|
+
}
|
|
39
|
+
} else if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
|
40
|
+
console.error(chalk.red('\n✖ Could not connect to Codequiry servers.'));
|
|
41
|
+
console.error(chalk.dim(' Check your internet connection.\n'));
|
|
42
|
+
} else if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
|
43
|
+
console.error(chalk.red('\n✖ Request timed out. Please try again.'));
|
|
44
|
+
} else {
|
|
45
|
+
console.error(chalk.red('\n✖ ' + (error.message || 'An unexpected error occurred.')));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function exitWithError(message) {
|
|
50
|
+
console.error(chalk.red('\n✖ ' + message + '\n'));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function requireAuth() {
|
|
55
|
+
const { isAuthenticated } = require('../utils/config');
|
|
56
|
+
if (!isAuthenticated()) {
|
|
57
|
+
console.error(chalk.red('\n✖ Not authenticated.'));
|
|
58
|
+
console.error(chalk.dim(' Run `codequiry auth` to set your API key.\n'));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { handleApiError, exitWithError, requireAuth };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ora = require('ora');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const { getCheckStatus } = require('../api/endpoints');
|
|
6
|
+
|
|
7
|
+
async function pollUntilComplete(checkId, { silent = false } = {}) {
|
|
8
|
+
const POLL_INTERVAL = 3000;
|
|
9
|
+
let spinner;
|
|
10
|
+
let interrupted = false;
|
|
11
|
+
|
|
12
|
+
const cleanup = () => {
|
|
13
|
+
interrupted = true;
|
|
14
|
+
if (spinner) spinner.stop();
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
process.on('SIGINT', cleanup);
|
|
18
|
+
|
|
19
|
+
if (!silent) {
|
|
20
|
+
spinner = ora('Waiting for check to complete...').start();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
while (!interrupted) {
|
|
25
|
+
const status = await getCheckStatus(checkId);
|
|
26
|
+
const statusId = status?.status_id;
|
|
27
|
+
const progress = status?.progress ?? 0;
|
|
28
|
+
|
|
29
|
+
// status_id 4 = Completed
|
|
30
|
+
if (statusId === 4) {
|
|
31
|
+
if (spinner) spinner.succeed('Check completed!');
|
|
32
|
+
return status;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// status_id 5 = Error/Failed
|
|
36
|
+
if (statusId === 5 || statusId === -1) {
|
|
37
|
+
if (spinner) spinner.fail('Check failed: ' + (status?.status_message || status?.status || ''));
|
|
38
|
+
return status;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Update progress display
|
|
42
|
+
if (!silent && spinner) {
|
|
43
|
+
const statusText = status?.status || 'Processing';
|
|
44
|
+
const msg = status?.status_message || '';
|
|
45
|
+
spinner.text = `${statusText}... ${chalk.cyan(progress + '%')}${msg ? chalk.dim(' - ' + msg) : ''}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
|
|
49
|
+
}
|
|
50
|
+
} finally {
|
|
51
|
+
process.removeListener('SIGINT', cleanup);
|
|
52
|
+
if (spinner) spinner.stop();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { pollUntilComplete };
|