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/src/index.js CHANGED
@@ -1,51 +1,132 @@
1
- #!/usr/bin/env node
2
-
3
- import { program } from 'commander';
4
- import { authenticate } from './auth.js';
5
- import { createCheck, uploadToCheck, startCheck, retriveCheck, checkStatus, resultOverview, detailedResult } from './check.js';
6
-
7
- program
8
- .version('1.0.0')
9
- .description('CLI for Plagiarism Checker API');
10
-
11
- program
12
- .command('auth')
13
- .description('Authenticate API key')
14
- .action(authenticate);
15
-
16
- program
17
- .command('createCheck')
18
- .description('Create check')
19
- .action(createCheck);
20
-
21
- program
22
- .command('uploadToCheck')
23
- .description('Upload file to check')
24
- .action(uploadToCheck);
25
-
26
- program
27
- .command('startCheck')
28
- .description('Start check')
29
- .action(startCheck);
30
-
31
- program
32
- .command('retriveCheck')
33
- .description('Retrive checks')
34
- .action(retriveCheck);
35
-
36
- program
37
- .command('checkStatus')
38
- .description('Check status')
39
- .action(checkStatus);
40
-
41
- program
42
- .command('resultOverview')
43
- .description('Result Overview')
44
- .action(resultOverview);
45
-
46
- program
47
- .command('detailedResult')
48
- .description('Detailed Result')
49
- .action(detailedResult);
50
-
51
- program.parse(process.argv);
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 };