codequiry-cli 2.0.0 → 2.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.
- package/README.md +17 -5
- package/install/install.ps1 +46 -0
- package/install/install.sh +46 -0
- package/package.json +27 -3
- package/src/commands/account.js +40 -24
- package/src/commands/create.js +45 -24
- package/src/commands/results.js +5 -4
- package/src/commands/scan.js +84 -50
- package/src/commands/status.js +2 -2
- package/src/index.js +22 -10
- package/src/ui/menu.js +28 -15
- package/src/utils/banner.js +40 -10
- package/src/utils/display.js +123 -52
- package/src/utils/poller.js +43 -16
- package/src/utils/update-check.js +137 -0
package/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
# Codequiry CLI
|
|
1
|
+
# Codequiry CLI — Code Plagiarism Checker for the Terminal
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/codequiry-cli)
|
|
4
|
+
[](https://github.com/cqchecker/codequiry-sdk/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
Check code for plagiarism from your terminal with [Codequiry](https://codequiry.com), the code plagiarism checker. One command zips a folder, uploads it, runs source code plagiarism detection and peer code similarity analysis against billions of web sources, and prints the results. Works across 65+ programming languages, with AI-generated code detection available on every check.
|
|
7
|
+
|
|
8
|
+
Built for professors, teaching assistants, bootcamps, hiring teams, and CI pipelines that need to verify code originality.
|
|
4
9
|
|
|
5
10
|
## Quick Install
|
|
6
11
|
|
|
@@ -69,6 +74,13 @@ codequiry scan ./src --output json
|
|
|
69
74
|
|
|
70
75
|
## Links
|
|
71
76
|
|
|
72
|
-
- [Codequiry](https://codequiry.com)
|
|
73
|
-
- [API
|
|
74
|
-
- [Get API
|
|
77
|
+
- [Codequiry: code plagiarism checker](https://codequiry.com)
|
|
78
|
+
- [API documentation](https://codequiry.com/usage/api)
|
|
79
|
+
- [Get an API key](https://codequiry.com/dashboard)
|
|
80
|
+
- [Node.js SDK](https://www.npmjs.com/package/codequiry) — the same API from your own code
|
|
81
|
+
- [MCP server](https://codequiry.com/mcp) — plagiarism checks from Claude, Cursor, and other AI agents
|
|
82
|
+
- [AI code detection](https://codequiry.com/detect-ai-written-code)
|
|
83
|
+
|
|
84
|
+
## License
|
|
85
|
+
|
|
86
|
+
MIT © [Codequiry](https://codequiry.com)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Write-Host ""
|
|
2
|
+
Write-Host " +======================================+" -ForegroundColor Cyan
|
|
3
|
+
Write-Host " | Codequiry CLI Installer |" -ForegroundColor Cyan
|
|
4
|
+
Write-Host " +======================================+" -ForegroundColor Cyan
|
|
5
|
+
Write-Host ""
|
|
6
|
+
|
|
7
|
+
# Check Node.js
|
|
8
|
+
try {
|
|
9
|
+
$nodeVersion = (node -v) -replace 'v', ''
|
|
10
|
+
$major = [int]($nodeVersion.Split('.')[0])
|
|
11
|
+
|
|
12
|
+
if ($major -lt 18) {
|
|
13
|
+
Write-Host " X Node.js 18+ required (found v$nodeVersion)" -ForegroundColor Red
|
|
14
|
+
Write-Host " Update from https://nodejs.org"
|
|
15
|
+
exit 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Write-Host " OK Node.js v$nodeVersion detected" -ForegroundColor Green
|
|
19
|
+
} catch {
|
|
20
|
+
Write-Host " X Node.js is not installed." -ForegroundColor Red
|
|
21
|
+
Write-Host " Install Node.js 18+ from https://nodejs.org"
|
|
22
|
+
exit 1
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Check npm
|
|
26
|
+
try {
|
|
27
|
+
$npmVersion = npm -v
|
|
28
|
+
Write-Host " OK npm v$npmVersion detected" -ForegroundColor Green
|
|
29
|
+
} catch {
|
|
30
|
+
Write-Host " X npm is not installed." -ForegroundColor Red
|
|
31
|
+
exit 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Write-Host ""
|
|
35
|
+
Write-Host " Installing codequiry-cli..."
|
|
36
|
+
|
|
37
|
+
npm install -g codequiry-cli
|
|
38
|
+
|
|
39
|
+
Write-Host ""
|
|
40
|
+
Write-Host " OK Codequiry CLI installed successfully!" -ForegroundColor Green
|
|
41
|
+
Write-Host ""
|
|
42
|
+
Write-Host " Get started:"
|
|
43
|
+
Write-Host " codequiry auth # Set your API key"
|
|
44
|
+
Write-Host " codequiry scan . # Scan current directory"
|
|
45
|
+
Write-Host " codequiry # Interactive mode"
|
|
46
|
+
Write-Host ""
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
echo ""
|
|
5
|
+
echo " ╔══════════════════════════════════════╗"
|
|
6
|
+
echo " ║ Codequiry CLI Installer ║"
|
|
7
|
+
echo " ╚══════════════════════════════════════╝"
|
|
8
|
+
echo ""
|
|
9
|
+
|
|
10
|
+
# Check Node.js
|
|
11
|
+
if ! command -v node &> /dev/null; then
|
|
12
|
+
echo " ✖ Node.js is not installed."
|
|
13
|
+
echo " Install Node.js 18+ from https://nodejs.org"
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
|
|
18
|
+
if [ "$NODE_VERSION" -lt 18 ]; then
|
|
19
|
+
echo " ✖ Node.js 18+ required (found v$(node -v))"
|
|
20
|
+
echo " Update from https://nodejs.org"
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
echo " ✔ Node.js $(node -v) detected"
|
|
25
|
+
|
|
26
|
+
# Check npm
|
|
27
|
+
if ! command -v npm &> /dev/null; then
|
|
28
|
+
echo " ✖ npm is not installed."
|
|
29
|
+
exit 1
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
echo " ✔ npm $(npm -v) detected"
|
|
33
|
+
echo ""
|
|
34
|
+
|
|
35
|
+
# Install
|
|
36
|
+
echo " Installing codequiry-cli..."
|
|
37
|
+
npm install -g codequiry-cli
|
|
38
|
+
|
|
39
|
+
echo ""
|
|
40
|
+
echo " ✔ Codequiry CLI installed successfully!"
|
|
41
|
+
echo ""
|
|
42
|
+
echo " Get started:"
|
|
43
|
+
echo " codequiry auth # Set your API key"
|
|
44
|
+
echo " codequiry scan . # Scan current directory"
|
|
45
|
+
echo " codequiry # Interactive mode"
|
|
46
|
+
echo ""
|
package/package.json
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codequiry-cli",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Codequiry CLI -
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Codequiry CLI - code plagiarism checker for the terminal: source code plagiarism detection, peer code similarity, and AI-generated code detection across 65+ languages",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"codequiry": "./bin/codequiry.js"
|
|
8
8
|
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"install",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE"
|
|
15
|
+
],
|
|
9
16
|
"scripts": {
|
|
10
17
|
"start": "node bin/codequiry.js",
|
|
11
18
|
"test": "echo \"No tests yet\""
|
|
@@ -13,11 +20,28 @@
|
|
|
13
20
|
"keywords": [
|
|
14
21
|
"codequiry",
|
|
15
22
|
"plagiarism",
|
|
23
|
+
"plagiarism-checker",
|
|
24
|
+
"code-plagiarism",
|
|
25
|
+
"code-plagiarism-checker",
|
|
16
26
|
"code-similarity",
|
|
27
|
+
"similarity-detection",
|
|
28
|
+
"source-code-plagiarism",
|
|
29
|
+
"ai-code-detection",
|
|
30
|
+
"academic-integrity",
|
|
31
|
+
"moss",
|
|
32
|
+
"moss-alternative",
|
|
17
33
|
"cli"
|
|
18
34
|
],
|
|
19
|
-
"author": "Codequiry",
|
|
35
|
+
"author": "Codequiry <contact@codequiry.com> (https://codequiry.com)",
|
|
20
36
|
"license": "MIT",
|
|
37
|
+
"homepage": "https://codequiry.com/sdks",
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/cqchecker/codequiry-sdk.git"
|
|
41
|
+
},
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/cqchecker/codequiry-sdk/issues"
|
|
44
|
+
},
|
|
21
45
|
"dependencies": {
|
|
22
46
|
"commander": "^12.0.0",
|
|
23
47
|
"inquirer": "^8.2.6",
|
package/src/commands/account.js
CHANGED
|
@@ -6,46 +6,62 @@ const ora = require('ora');
|
|
|
6
6
|
const { requireAuth } = require('../utils/errors');
|
|
7
7
|
const { getAccount } = require('../api/endpoints');
|
|
8
8
|
|
|
9
|
+
const accent = chalk.hex('#8b5cf6');
|
|
10
|
+
const dim = chalk.dim;
|
|
11
|
+
|
|
9
12
|
async function accountCommand() {
|
|
10
13
|
requireAuth();
|
|
11
14
|
|
|
12
|
-
const spinner = ora('Fetching account info...').start();
|
|
15
|
+
const spinner = ora({ text: 'Fetching account info...', spinner: 'dots12', color: 'magenta' }).start();
|
|
13
16
|
|
|
14
17
|
try {
|
|
15
|
-
const
|
|
18
|
+
const data = await getAccount();
|
|
16
19
|
spinner.stop();
|
|
17
20
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
21
|
+
const name = data.name || 'N/A';
|
|
22
|
+
const email = data.email || 'N/A';
|
|
23
|
+
const isPro = data.is_pro;
|
|
24
|
+
const planLabel = isPro ? accent.bold('Pro') : dim('Free');
|
|
25
|
+
const quota = data.quota || {};
|
|
26
|
+
const remaining = quota.remaining ?? 0;
|
|
27
|
+
const isUnlimited = quota.unlimited || false;
|
|
28
|
+
|
|
29
|
+
const quotaDisplay = isUnlimited
|
|
30
|
+
? chalk.green.bold('Unlimited')
|
|
31
|
+
: chalk.white.bold(String(remaining));
|
|
32
|
+
|
|
23
33
|
const barLen = 20;
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
let quotaBar = '';
|
|
35
|
+
if (!isUnlimited) {
|
|
36
|
+
const total = quota.total || Math.max(remaining, 100);
|
|
37
|
+
const filled = Math.round((remaining / total) * barLen);
|
|
38
|
+
quotaBar = chalk.green('━'.repeat(filled)) + dim('─'.repeat(barLen - filled));
|
|
39
|
+
}
|
|
26
40
|
|
|
27
41
|
const content = [
|
|
28
|
-
|
|
42
|
+
accent.bold(' Account'),
|
|
29
43
|
'',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
44
|
+
` ${dim('Name')} ${chalk.white.bold(name)}`,
|
|
45
|
+
` ${dim('Email')} ${chalk.white(email)}`,
|
|
46
|
+
` ${dim('Plan')} ${planLabel}`,
|
|
47
|
+
` ${dim('EDU')} ${data.edu_verified ? chalk.green('✓ Verified') : dim('Not verified')}`,
|
|
33
48
|
'',
|
|
34
|
-
|
|
49
|
+
accent.bold(' Quota'),
|
|
35
50
|
'',
|
|
36
|
-
|
|
37
|
-
`
|
|
38
|
-
].join('\n');
|
|
51
|
+
` ${dim('Remaining')} ${quotaDisplay}`,
|
|
52
|
+
isUnlimited ? '' : ` ${dim('Usage')} ${quotaBar}`,
|
|
53
|
+
].filter(Boolean).join('\n');
|
|
39
54
|
|
|
40
55
|
console.log(
|
|
41
56
|
'\n' +
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
boxen(content, {
|
|
58
|
+
padding: { top: 1, bottom: 1, left: 0, right: 2 },
|
|
59
|
+
margin: { top: 0, bottom: 0, left: 1, right: 0 },
|
|
60
|
+
borderStyle: 'round',
|
|
61
|
+
borderColor: '#8b5cf6',
|
|
62
|
+
dimBorder: true,
|
|
63
|
+
}) +
|
|
64
|
+
'\n'
|
|
49
65
|
);
|
|
50
66
|
} catch (error) {
|
|
51
67
|
spinner.fail('Failed to fetch account info.');
|
package/src/commands/create.js
CHANGED
|
@@ -13,20 +13,35 @@ async function createCommand(options) {
|
|
|
13
13
|
let languageId = options.language;
|
|
14
14
|
let testType = options.testType;
|
|
15
15
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
let
|
|
16
|
+
// If all options provided via flags, skip fetching options
|
|
17
|
+
let langList = [];
|
|
18
|
+
let typeList = [];
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
if (!languageId || !testType) {
|
|
21
|
+
const spinner = ora('Loading options...').start();
|
|
22
|
+
try {
|
|
23
|
+
const fetches = [];
|
|
24
|
+
fetches.push(!languageId ? getLanguages() : Promise.resolve(null));
|
|
25
|
+
fetches.push(!testType ? getTestTypes().catch(() => null) : Promise.resolve(null));
|
|
26
|
+
const [languages, testTypes] = await Promise.all(fetches);
|
|
27
|
+
spinner.stop();
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
if (languages) {
|
|
30
|
+
langList = Array.isArray(languages) ? languages : languages?.languages || [];
|
|
31
|
+
}
|
|
32
|
+
if (testTypes) {
|
|
33
|
+
typeList = Array.isArray(testTypes) ? testTypes : testTypes?.test_types || [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!languageId && langList.length === 0) {
|
|
37
|
+
spinner.fail('Failed to load languages.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
spinner.fail('Failed to load options.');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
30
45
|
|
|
31
46
|
// Interactive prompts for missing options
|
|
32
47
|
const questions = [];
|
|
@@ -46,22 +61,27 @@ async function createCommand(options) {
|
|
|
46
61
|
name: 'languageId',
|
|
47
62
|
message: 'Programming language:',
|
|
48
63
|
choices: langList.map((l) => ({
|
|
49
|
-
name: l.
|
|
64
|
+
name: l.language || l.name,
|
|
50
65
|
value: l.id,
|
|
51
66
|
})),
|
|
52
67
|
});
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
if (!testType) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
71
|
+
if (typeList.length > 0) {
|
|
72
|
+
questions.push({
|
|
73
|
+
type: 'list',
|
|
74
|
+
name: 'testType',
|
|
75
|
+
message: 'Check engine:',
|
|
76
|
+
choices: typeList.map((t) => ({
|
|
77
|
+
name: t.name || t.type,
|
|
78
|
+
value: t.id,
|
|
79
|
+
})),
|
|
80
|
+
});
|
|
81
|
+
} else {
|
|
82
|
+
// Fallback: use default engine if test types API failed
|
|
83
|
+
testType = 1;
|
|
84
|
+
}
|
|
65
85
|
}
|
|
66
86
|
|
|
67
87
|
if (questions.length > 0) {
|
|
@@ -75,8 +95,9 @@ async function createCommand(options) {
|
|
|
75
95
|
|
|
76
96
|
try {
|
|
77
97
|
const result = await createCheck(name, languageId, testType);
|
|
78
|
-
|
|
79
|
-
|
|
98
|
+
// API returns the Assignment object directly
|
|
99
|
+
createSpinner.succeed(`Check created: ${chalk.cyan(result.name || name)} (ID: ${result.id || 'N/A'})`);
|
|
100
|
+
return result;
|
|
80
101
|
} catch (error) {
|
|
81
102
|
createSpinner.fail('Failed to create check.');
|
|
82
103
|
}
|
package/src/commands/results.js
CHANGED
|
@@ -27,7 +27,7 @@ async function resultsCommand(options) {
|
|
|
27
27
|
|
|
28
28
|
// Filter to completed checks
|
|
29
29
|
const completed = checks.filter(
|
|
30
|
-
(c) => c.
|
|
30
|
+
(c) => c.status_id === 4 || c.assignmentstatuses?.status === 'Completed'
|
|
31
31
|
);
|
|
32
32
|
|
|
33
33
|
if (completed.length === 0) {
|
|
@@ -88,8 +88,8 @@ async function resultsCommand(options) {
|
|
|
88
88
|
name: 'submissionId',
|
|
89
89
|
message: 'Select a submission:',
|
|
90
90
|
choices: submissions.map((s) => ({
|
|
91
|
-
name: `${s.id
|
|
92
|
-
value: s.id
|
|
91
|
+
name: `${s.id} - ${s.filename || 'unknown'}`,
|
|
92
|
+
value: s.id,
|
|
93
93
|
})),
|
|
94
94
|
},
|
|
95
95
|
]);
|
|
@@ -103,7 +103,8 @@ async function resultsCommand(options) {
|
|
|
103
103
|
detailSpinner.fail('Failed to fetch details.');
|
|
104
104
|
}
|
|
105
105
|
} else if (action === 'browser') {
|
|
106
|
-
|
|
106
|
+
const url = overview?.overviewURL || `https://codequiry.com/results/${checkId}`;
|
|
107
|
+
await open(url);
|
|
107
108
|
console.log(chalk.dim(' Opened in browser.\n'));
|
|
108
109
|
}
|
|
109
110
|
}
|
package/src/commands/scan.js
CHANGED
|
@@ -5,11 +5,17 @@ const chalk = require('chalk');
|
|
|
5
5
|
const inquirer = require('inquirer');
|
|
6
6
|
const ora = require('ora');
|
|
7
7
|
const { requireAuth } = require('../utils/errors');
|
|
8
|
-
const { getLanguages, getTestTypes, createCheck, uploadZip, uploadBatch, startCheck } = require('../api/endpoints');
|
|
8
|
+
const { getLanguages, getTestTypes, createCheck, uploadZip, uploadBatch, startCheck, getOverview } = require('../api/endpoints');
|
|
9
9
|
const { prepareUploads, cleanupTempFiles } = require('../utils/zipper');
|
|
10
10
|
const { pollUntilComplete } = require('../utils/poller');
|
|
11
11
|
const { overviewTable } = require('../utils/display');
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
const accent = chalk.hex('#8b5cf6');
|
|
14
|
+
const dim = chalk.dim;
|
|
15
|
+
|
|
16
|
+
function spin(text) {
|
|
17
|
+
return ora({ text, spinner: 'dots12', color: 'magenta' });
|
|
18
|
+
}
|
|
13
19
|
|
|
14
20
|
async function scanCommand(inputPath, options) {
|
|
15
21
|
requireAuth();
|
|
@@ -39,6 +45,13 @@ async function scanCommand(inputPath, options) {
|
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
const resolvedPath = path.resolve(inputPath);
|
|
48
|
+
const startTime = Date.now();
|
|
49
|
+
|
|
50
|
+
if (!isJson) {
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(' ' + accent.bold('codequiry scan') + dim(' · ' + resolvedPath));
|
|
53
|
+
console.log('');
|
|
54
|
+
}
|
|
42
55
|
|
|
43
56
|
// 2. Get language + test type
|
|
44
57
|
let languageId = options.language;
|
|
@@ -46,69 +59,82 @@ async function scanCommand(inputPath, options) {
|
|
|
46
59
|
let checkName = options.name || path.basename(resolvedPath);
|
|
47
60
|
|
|
48
61
|
if (!languageId || !testType) {
|
|
49
|
-
const spinner =
|
|
50
|
-
let
|
|
62
|
+
const spinner = spin('Loading options...').start();
|
|
63
|
+
let langList = [];
|
|
64
|
+
let typeList = [];
|
|
51
65
|
try {
|
|
52
|
-
|
|
66
|
+
const fetches = [];
|
|
67
|
+
fetches.push(!languageId ? getLanguages() : Promise.resolve(null));
|
|
68
|
+
fetches.push(!testType ? getTestTypes().catch(() => null) : Promise.resolve(null));
|
|
69
|
+
const [languages, testTypes] = await Promise.all(fetches);
|
|
53
70
|
spinner.stop();
|
|
71
|
+
|
|
72
|
+
if (languages) {
|
|
73
|
+
langList = Array.isArray(languages) ? languages : languages?.languages || [];
|
|
74
|
+
}
|
|
75
|
+
if (testTypes) {
|
|
76
|
+
typeList = Array.isArray(testTypes) ? testTypes : testTypes?.test_types || [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!languageId && langList.length === 0) {
|
|
80
|
+
spinner.fail('Failed to load languages.');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
54
83
|
} catch (error) {
|
|
55
84
|
spinner.fail('Failed to load options.');
|
|
56
85
|
process.exit(1);
|
|
57
86
|
}
|
|
58
87
|
|
|
59
|
-
const langList = Array.isArray(languages) ? languages : languages?.languages || [];
|
|
60
|
-
const typeList = Array.isArray(testTypes) ? testTypes : testTypes?.test_types || [];
|
|
61
|
-
|
|
62
88
|
if (isTTY) {
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
testType = testType || answers.testType;
|
|
89
|
+
const prompts = [];
|
|
90
|
+
if (!languageId) {
|
|
91
|
+
prompts.push({
|
|
92
|
+
type: 'list',
|
|
93
|
+
name: 'languageId',
|
|
94
|
+
message: 'Programming language:',
|
|
95
|
+
pageSize: 15,
|
|
96
|
+
choices: langList.map((l) => ({ name: l.language || l.name, value: l.id })),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (!testType && typeList.length > 0) {
|
|
100
|
+
prompts.push({
|
|
101
|
+
type: 'list',
|
|
102
|
+
name: 'testType',
|
|
103
|
+
message: 'Check engine:',
|
|
104
|
+
choices: typeList.map((t) => ({ name: t.name || t.type, value: t.id })),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (prompts.length > 0) {
|
|
108
|
+
const answers = await inquirer.prompt(prompts);
|
|
109
|
+
languageId = languageId || answers.languageId;
|
|
110
|
+
testType = testType || answers.testType;
|
|
111
|
+
}
|
|
87
112
|
} else {
|
|
88
|
-
// Non-TTY: use defaults
|
|
89
113
|
languageId = languageId || (langList[0] && langList[0].id);
|
|
90
114
|
testType = testType || (typeList[0] && typeList[0].id);
|
|
91
115
|
}
|
|
116
|
+
|
|
117
|
+
if (!testType) testType = 1;
|
|
92
118
|
}
|
|
93
119
|
|
|
94
120
|
// 3. Zip
|
|
95
|
-
const zipSpinner =
|
|
121
|
+
const zipSpinner = spin('Preparing files...').start();
|
|
96
122
|
let zipFiles;
|
|
97
123
|
try {
|
|
98
124
|
zipFiles = await prepareUploads(resolvedPath);
|
|
99
|
-
zipSpinner.succeed(`Prepared ${zipFiles.length} file(s)`);
|
|
125
|
+
zipSpinner.succeed(`Prepared ${accent.bold(zipFiles.length)} file(s)`);
|
|
100
126
|
} catch (error) {
|
|
101
127
|
zipSpinner.fail('Failed to prepare files: ' + error.message);
|
|
102
128
|
process.exit(1);
|
|
103
129
|
}
|
|
104
130
|
|
|
105
131
|
// 4. Create check
|
|
106
|
-
const createSpinner =
|
|
132
|
+
const createSpinner = spin('Creating check...').start();
|
|
107
133
|
let check;
|
|
108
134
|
try {
|
|
109
135
|
const result = await createCheck(checkName, languageId, testType);
|
|
110
|
-
check = result
|
|
111
|
-
createSpinner.succeed(`Check created
|
|
136
|
+
check = result;
|
|
137
|
+
createSpinner.succeed(`Check created ${dim('·')} ${accent(check.name || checkName)} ${dim('#' + check.id)}`);
|
|
112
138
|
} catch (error) {
|
|
113
139
|
createSpinner.fail('Failed to create check.');
|
|
114
140
|
cleanupTempFiles(zipFiles);
|
|
@@ -116,7 +142,7 @@ async function scanCommand(inputPath, options) {
|
|
|
116
142
|
}
|
|
117
143
|
|
|
118
144
|
// 5. Upload
|
|
119
|
-
const uploadSpinner =
|
|
145
|
+
const uploadSpinner = spin('Uploading files...').start();
|
|
120
146
|
try {
|
|
121
147
|
if (zipFiles.length === 1) {
|
|
122
148
|
await uploadZip(check.id, zipFiles[0]);
|
|
@@ -124,10 +150,11 @@ async function scanCommand(inputPath, options) {
|
|
|
124
150
|
for (let i = 0; i < zipFiles.length; i += 50) {
|
|
125
151
|
const chunk = zipFiles.slice(i, i + 50);
|
|
126
152
|
await uploadBatch(check.id, chunk);
|
|
127
|
-
|
|
153
|
+
const done = Math.min(i + 50, zipFiles.length);
|
|
154
|
+
uploadSpinner.text = `Uploading... ${accent(done + '/' + zipFiles.length)}`;
|
|
128
155
|
}
|
|
129
156
|
}
|
|
130
|
-
uploadSpinner.succeed(`Uploaded ${zipFiles.length} file(s)`);
|
|
157
|
+
uploadSpinner.succeed(`Uploaded ${accent.bold(zipFiles.length)} file(s)`);
|
|
131
158
|
} catch (error) {
|
|
132
159
|
uploadSpinner.fail('Upload failed.');
|
|
133
160
|
cleanupTempFiles(zipFiles);
|
|
@@ -140,10 +167,10 @@ async function scanCommand(inputPath, options) {
|
|
|
140
167
|
const webcheck = options.web ? 1 : 0;
|
|
141
168
|
const dbcheck = options.db ? 1 : 0;
|
|
142
169
|
|
|
143
|
-
const startSpinner =
|
|
170
|
+
const startSpinner = spin('Starting check...').start();
|
|
144
171
|
try {
|
|
145
172
|
await startCheck(check.id, webcheck, dbcheck);
|
|
146
|
-
startSpinner.succeed('Check started
|
|
173
|
+
startSpinner.succeed('Check started');
|
|
147
174
|
} catch (error) {
|
|
148
175
|
startSpinner.fail('Failed to start check.');
|
|
149
176
|
process.exit(1);
|
|
@@ -157,30 +184,37 @@ async function scanCommand(inputPath, options) {
|
|
|
157
184
|
if (finalStatus) {
|
|
158
185
|
try {
|
|
159
186
|
const overview = await getOverview(check.id);
|
|
187
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
188
|
+
const elapsedStr = elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
|
|
160
189
|
|
|
161
190
|
if (isJson) {
|
|
162
191
|
console.log(JSON.stringify(overview, null, 2));
|
|
163
192
|
} else {
|
|
164
|
-
console.log(
|
|
193
|
+
console.log('');
|
|
194
|
+
console.log(' ' + chalk.bold('Results') + dim(` · completed in ${elapsedStr}`));
|
|
165
195
|
overviewTable(overview);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
);
|
|
196
|
+
|
|
197
|
+
const url = overview?.overviewURL || `https://codequiry.com/results/${check.id}`;
|
|
198
|
+
console.log(dim(` Full results: ${url}`));
|
|
199
|
+
console.log('');
|
|
169
200
|
}
|
|
170
201
|
|
|
171
|
-
// Exit code
|
|
202
|
+
// Exit code for CI threshold
|
|
172
203
|
if (options.threshold) {
|
|
173
204
|
const submissions = overview?.submissions || overview || [];
|
|
174
205
|
const maxScore = Math.max(
|
|
175
|
-
...submissions.map((s) => parseFloat(s.
|
|
206
|
+
...submissions.map((s) => parseFloat(s.result1 || s.total_result || 0))
|
|
176
207
|
);
|
|
177
208
|
if (maxScore > parseFloat(options.threshold)) {
|
|
209
|
+
if (!isJson) {
|
|
210
|
+
console.log(chalk.red(` Threshold exceeded: ${maxScore.toFixed(1)}% > ${options.threshold}%`));
|
|
211
|
+
}
|
|
178
212
|
process.exit(1);
|
|
179
213
|
}
|
|
180
214
|
}
|
|
181
215
|
} catch (error) {
|
|
182
216
|
if (!isJson) {
|
|
183
|
-
console.log(
|
|
217
|
+
console.log(dim(`\n View results at: https://codequiry.com/results/${check.id}\n`));
|
|
184
218
|
}
|
|
185
219
|
}
|
|
186
220
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -53,8 +53,8 @@ async function statusCommand(options) {
|
|
|
53
53
|
try {
|
|
54
54
|
const status = await getCheckStatus(checkId);
|
|
55
55
|
spinner.stop();
|
|
56
|
-
const s = status?.
|
|
57
|
-
const p = status?.progress ??
|
|
56
|
+
const s = status?.status || 'Unknown';
|
|
57
|
+
const p = status?.progress ?? '';
|
|
58
58
|
console.log(`\n Check ${chalk.cyan(checkId)}: ${statusColor(s)}${p ? ` (${p}%)` : ''}\n`);
|
|
59
59
|
} catch (error) {
|
|
60
60
|
spinner.fail('Failed to fetch status.');
|
package/src/index.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { Command } = require('commander');
|
|
4
|
+
const { checkForUpdate } = require('./utils/update-check');
|
|
4
5
|
const program = new Command();
|
|
5
6
|
|
|
7
|
+
// Start update check in background (non-blocking)
|
|
8
|
+
const updater = checkForUpdate();
|
|
9
|
+
|
|
6
10
|
program
|
|
7
11
|
.name('codequiry')
|
|
8
12
|
.description('Codequiry CLI - Source Code Similarity Checker')
|
|
9
|
-
.version('2.
|
|
13
|
+
.version('2.1.0');
|
|
10
14
|
|
|
11
15
|
// Auth command
|
|
12
16
|
program
|
|
@@ -120,13 +124,21 @@ program
|
|
|
120
124
|
await scanCommand(inputPath, options);
|
|
121
125
|
});
|
|
122
126
|
|
|
123
|
-
//
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
} else {
|
|
131
|
-
|
|
127
|
+
// Show update notice when process exits
|
|
128
|
+
process.on('exit', () => {});
|
|
129
|
+
async function run() {
|
|
130
|
+
if (process.argv.length <= 2) {
|
|
131
|
+
// Interactive TUI when no args
|
|
132
|
+
const { mainMenu } = require('./ui/menu');
|
|
133
|
+
await mainMenu();
|
|
134
|
+
} else {
|
|
135
|
+
await program.parseAsync(process.argv);
|
|
136
|
+
}
|
|
137
|
+
// Show update notice after command completes
|
|
138
|
+
await updater.notify();
|
|
132
139
|
}
|
|
140
|
+
|
|
141
|
+
run().catch((err) => {
|
|
142
|
+
console.error(err.message);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
});
|
package/src/ui/menu.js
CHANGED
|
@@ -4,12 +4,17 @@ const chalk = require('chalk');
|
|
|
4
4
|
const inquirer = require('inquirer');
|
|
5
5
|
const { showBanner } = require('../utils/banner');
|
|
6
6
|
const { isAuthenticated } = require('../utils/config');
|
|
7
|
+
const { checkForUpdate } = require('../utils/update-check');
|
|
8
|
+
|
|
9
|
+
const accent = chalk.hex('#8b5cf6');
|
|
10
|
+
const dim = chalk.dim;
|
|
7
11
|
|
|
8
12
|
async function mainMenu() {
|
|
13
|
+
const updater = checkForUpdate();
|
|
9
14
|
showBanner();
|
|
10
15
|
|
|
11
16
|
if (!isAuthenticated()) {
|
|
12
|
-
console.log(chalk.yellow('
|
|
17
|
+
console.log(chalk.yellow(' Not authenticated yet.\n'));
|
|
13
18
|
const authCommand = require('../commands/auth');
|
|
14
19
|
await authCommand({});
|
|
15
20
|
if (!isAuthenticated()) return;
|
|
@@ -23,19 +28,22 @@ async function mainMenu() {
|
|
|
23
28
|
{
|
|
24
29
|
type: 'list',
|
|
25
30
|
name: 'action',
|
|
26
|
-
message: 'What would you like to do?',
|
|
31
|
+
message: accent('What would you like to do?'),
|
|
32
|
+
pageSize: 12,
|
|
27
33
|
choices: [
|
|
28
|
-
|
|
29
|
-
{ name: '
|
|
30
|
-
{ name: '
|
|
31
|
-
{ name: '
|
|
32
|
-
{ name: '
|
|
33
|
-
{ name: '
|
|
34
|
-
{ name: '
|
|
35
|
-
{ name: '
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
{ name: '
|
|
34
|
+
new inquirer.Separator(dim('─── Commands ────────────────')),
|
|
35
|
+
{ name: accent.bold(' scan ') + dim('Full scan: zip, upload, check, results'), value: 'scan' },
|
|
36
|
+
{ name: accent.bold(' checks ') + dim('List all your checks'), value: 'checks' },
|
|
37
|
+
{ name: accent.bold(' create ') + dim('Create a new check'), value: 'create' },
|
|
38
|
+
{ name: accent.bold(' upload ') + dim('Upload files to a check'), value: 'upload' },
|
|
39
|
+
{ name: accent.bold(' start ') + dim('Start a check'), value: 'start' },
|
|
40
|
+
{ name: accent.bold(' status ') + dim('Check progress'), value: 'status' },
|
|
41
|
+
{ name: accent.bold(' results ') + dim('View similarity results'), value: 'results' },
|
|
42
|
+
new inquirer.Separator(dim('─── Settings ────────────────')),
|
|
43
|
+
{ name: accent.bold(' account ') + dim('Account info & quota'), value: 'account' },
|
|
44
|
+
{ name: accent.bold(' auth ') + dim('Manage API key'), value: 'auth' },
|
|
45
|
+
new inquirer.Separator(dim('─────────────────────────────')),
|
|
46
|
+
{ name: dim(' exit'), value: 'exit' },
|
|
39
47
|
],
|
|
40
48
|
},
|
|
41
49
|
]);
|
|
@@ -73,11 +81,16 @@ async function mainMenu() {
|
|
|
73
81
|
break;
|
|
74
82
|
case 'exit':
|
|
75
83
|
running = false;
|
|
76
|
-
|
|
84
|
+
await updater.notify();
|
|
85
|
+
console.log(dim(' See you next time.\n'));
|
|
77
86
|
break;
|
|
78
87
|
}
|
|
79
88
|
} catch (error) {
|
|
80
|
-
console.error(chalk.red('\n Error: ' + error.message + '\n')
|
|
89
|
+
console.error(chalk.red('\n Error: ') + error.message + '\n');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (running) {
|
|
93
|
+
console.log(''); // Breathing room between commands
|
|
81
94
|
}
|
|
82
95
|
}
|
|
83
96
|
}
|
package/src/utils/banner.js
CHANGED
|
@@ -1,20 +1,50 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const figlet = require('figlet');
|
|
4
|
-
const gradient = require('gradient-string');
|
|
5
3
|
const chalk = require('chalk');
|
|
4
|
+
const gradient = require('gradient-string');
|
|
5
|
+
|
|
6
|
+
// Premium gradient palette
|
|
7
|
+
const cqGradient = gradient(['#6366f1', '#8b5cf6', '#a78bfa']);
|
|
6
8
|
|
|
7
9
|
function showBanner() {
|
|
10
|
+
const logo = [
|
|
11
|
+
'',
|
|
12
|
+
' ██████╗ ██████╗ ██████╗ ███████╗ ██████╗ ██╗ ██╗██╗██████╗ ██╗ ██╗',
|
|
13
|
+
' ██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔═══██╗██║ ██║██║██╔══██╗╚██╗ ██╔╝',
|
|
14
|
+
' ██║ ██║ ██║██║ ██║█████╗ ██║ ██║██║ ██║██║██████╔╝ ╚████╔╝ ',
|
|
15
|
+
' ██║ ██║ ██║██║ ██║██╔══╝ ██║▄▄ ██║██║ ██║██║██╔══██╗ ╚██╔╝ ',
|
|
16
|
+
' ╚██████╗╚██████╔╝██████╔╝███████╗╚██████╔╝╚██████╔╝██║██║ ██║ ██║ ',
|
|
17
|
+
' ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚══▀▀═╝ ╚═════╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ',
|
|
18
|
+
'',
|
|
19
|
+
];
|
|
20
|
+
|
|
8
21
|
try {
|
|
9
|
-
|
|
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.0\n'));
|
|
22
|
+
logo.forEach(line => console.log(cqGradient(line)));
|
|
15
23
|
} catch {
|
|
16
|
-
|
|
24
|
+
// Fallback for terminals that don't support gradients
|
|
25
|
+
logo.forEach(line => console.log(chalk.hex('#8b5cf6')(line)));
|
|
17
26
|
}
|
|
27
|
+
|
|
28
|
+
console.log(
|
|
29
|
+
chalk.dim(' Code Plagiarism Detection') +
|
|
30
|
+
' ' +
|
|
31
|
+
chalk.hex('#8b5cf6').bold('v2.1.0') +
|
|
32
|
+
chalk.dim(' · ') +
|
|
33
|
+
chalk.dim('codequiry.com')
|
|
34
|
+
);
|
|
35
|
+
console.log('');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function showCompactBanner() {
|
|
39
|
+
console.log('');
|
|
40
|
+
console.log(
|
|
41
|
+
' ' +
|
|
42
|
+
cqGradient('▌') +
|
|
43
|
+
' ' +
|
|
44
|
+
chalk.bold('codequiry') +
|
|
45
|
+
chalk.dim(' v2.1.0')
|
|
46
|
+
);
|
|
47
|
+
console.log('');
|
|
18
48
|
}
|
|
19
49
|
|
|
20
|
-
module.exports = { showBanner };
|
|
50
|
+
module.exports = { showBanner, showCompactBanner };
|
package/src/utils/display.js
CHANGED
|
@@ -3,114 +3,185 @@
|
|
|
3
3
|
const Table = require('cli-table3');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
5
|
|
|
6
|
+
// Color palette matching modern CLI tools
|
|
7
|
+
const dim = chalk.dim;
|
|
8
|
+
const bold = chalk.bold;
|
|
9
|
+
const accent = chalk.hex('#8b5cf6');
|
|
10
|
+
const muted = chalk.hex('#6b7280');
|
|
11
|
+
|
|
6
12
|
function colorScore(score) {
|
|
7
13
|
const num = parseFloat(score);
|
|
8
|
-
if (isNaN(num)) return
|
|
9
|
-
|
|
10
|
-
if (num
|
|
11
|
-
|
|
14
|
+
if (isNaN(num)) return dim(score);
|
|
15
|
+
const str = num.toFixed(1) + '%';
|
|
16
|
+
if (num >= 70) return chalk.red.bold(str);
|
|
17
|
+
if (num >= 40) return chalk.yellow(str);
|
|
18
|
+
if (num >= 20) return chalk.hex('#fb923c')(str); // orange
|
|
19
|
+
return chalk.green(str);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function scoreBar(score, width = 16) {
|
|
23
|
+
const num = parseFloat(score);
|
|
24
|
+
if (isNaN(num)) return '';
|
|
25
|
+
const filled = Math.round((num / 100) * width);
|
|
26
|
+
const empty = width - filled;
|
|
27
|
+
let color = chalk.green;
|
|
28
|
+
if (num >= 70) color = chalk.red;
|
|
29
|
+
else if (num >= 40) color = chalk.yellow;
|
|
30
|
+
else if (num >= 20) color = chalk.hex('#fb923c');
|
|
31
|
+
return color('█'.repeat(filled)) + dim('░'.repeat(empty));
|
|
12
32
|
}
|
|
13
33
|
|
|
14
34
|
function statusColor(status) {
|
|
15
35
|
const s = String(status).toLowerCase();
|
|
16
|
-
if (s === 'completed' || s === 'done') return chalk.green(status);
|
|
17
|
-
if (s === '
|
|
18
|
-
if (s === 'failed' || s === 'error') return chalk.red(status);
|
|
19
|
-
return
|
|
36
|
+
if (s === 'completed' || s === 'done') return chalk.green('● ') + chalk.green(status);
|
|
37
|
+
if (s === 'checking' || s === 'waiting' || s === 'uploading') return chalk.yellow('◌ ') + chalk.yellow(status);
|
|
38
|
+
if (s === 'failed' || s === 'error') return chalk.red('✖ ') + chalk.red(status);
|
|
39
|
+
if (s === 'new check') return dim('○ ') + dim(status);
|
|
40
|
+
return dim(status);
|
|
20
41
|
}
|
|
21
42
|
|
|
22
43
|
function checksTable(checks) {
|
|
23
|
-
const
|
|
24
|
-
head: [
|
|
25
|
-
chalk.cyan('ID'),
|
|
26
|
-
chalk.cyan('Name'),
|
|
27
|
-
chalk.cyan('Language'),
|
|
28
|
-
chalk.cyan('Status'),
|
|
29
|
-
chalk.cyan('Submissions'),
|
|
30
|
-
chalk.cyan('Created'),
|
|
31
|
-
],
|
|
32
|
-
style: { head: [], border: ['dim'] },
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
const list = Array.isArray(checks) ? checks : checks?.checks || [];
|
|
44
|
+
const list = Array.isArray(checks) ? checks : [];
|
|
36
45
|
|
|
37
46
|
if (list.length === 0) {
|
|
38
|
-
console.log(
|
|
47
|
+
console.log('');
|
|
48
|
+
console.log(muted(' No checks found. Create one with ') + accent('codequiry create'));
|
|
49
|
+
console.log('');
|
|
39
50
|
return;
|
|
40
51
|
}
|
|
41
52
|
|
|
53
|
+
const table = new Table({
|
|
54
|
+
head: [
|
|
55
|
+
dim('#'),
|
|
56
|
+
dim('Name'),
|
|
57
|
+
dim('Lang'),
|
|
58
|
+
dim('Status'),
|
|
59
|
+
dim('Created'),
|
|
60
|
+
],
|
|
61
|
+
style: { head: [], border: ['dim'], compact: true },
|
|
62
|
+
colWidths: [9, 38, 8, 14, 12],
|
|
63
|
+
wordWrap: true,
|
|
64
|
+
});
|
|
65
|
+
|
|
42
66
|
list.forEach((c) => {
|
|
67
|
+
const statusName = c.assignmentstatuses?.status || 'Unknown';
|
|
43
68
|
table.push([
|
|
44
|
-
|
|
45
|
-
c.name || '',
|
|
46
|
-
c.
|
|
47
|
-
statusColor(
|
|
48
|
-
|
|
49
|
-
c.created_at ? new Date(c.created_at).toLocaleDateString() : '',
|
|
69
|
+
accent(String(c.id)),
|
|
70
|
+
c.name || dim('Untitled'),
|
|
71
|
+
dim(String(c.language_id || '–')),
|
|
72
|
+
statusColor(statusName),
|
|
73
|
+
c.created_at ? dim(new Date(c.created_at).toLocaleDateString()) : dim('–'),
|
|
50
74
|
]);
|
|
51
75
|
});
|
|
52
76
|
|
|
53
|
-
console.log('
|
|
77
|
+
console.log('');
|
|
78
|
+
console.log(
|
|
79
|
+
' ' + bold('Your Checks') +
|
|
80
|
+
dim(` (${list.length} total)`)
|
|
81
|
+
);
|
|
82
|
+
console.log(table.toString());
|
|
54
83
|
}
|
|
55
84
|
|
|
56
85
|
function overviewTable(overview) {
|
|
57
|
-
const submissions = overview?.submissions ||
|
|
86
|
+
const submissions = overview?.submissions || [];
|
|
58
87
|
|
|
59
88
|
if (!Array.isArray(submissions) || submissions.length === 0) {
|
|
60
|
-
console.log(
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(muted(' No results available yet.'));
|
|
91
|
+
console.log('');
|
|
61
92
|
return;
|
|
62
93
|
}
|
|
63
94
|
|
|
95
|
+
// Calculate stats
|
|
96
|
+
const scores = submissions.map(s => parseFloat(s.result1 || s.total_result || 0)).filter(n => !isNaN(n));
|
|
97
|
+
const maxScore = Math.max(...scores);
|
|
98
|
+
const avgScore = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
99
|
+
|
|
64
100
|
const table = new Table({
|
|
65
101
|
head: [
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
102
|
+
dim('#'),
|
|
103
|
+
dim('Submission'),
|
|
104
|
+
dim('Similarity'),
|
|
105
|
+
dim(''),
|
|
70
106
|
],
|
|
71
107
|
style: { head: [], border: ['dim'] },
|
|
108
|
+
colWidths: [9, 30, 13, 20],
|
|
72
109
|
});
|
|
73
110
|
|
|
74
111
|
submissions.forEach((s) => {
|
|
112
|
+
const score = s.result1 || s.total_result || 0;
|
|
75
113
|
table.push([
|
|
76
|
-
|
|
77
|
-
s.filename ||
|
|
78
|
-
colorScore(
|
|
79
|
-
|
|
114
|
+
accent(String(s.id || '')),
|
|
115
|
+
s.filename || dim('unknown'),
|
|
116
|
+
colorScore(score),
|
|
117
|
+
scoreBar(score),
|
|
80
118
|
]);
|
|
81
119
|
});
|
|
82
120
|
|
|
83
|
-
console.log(
|
|
121
|
+
console.log(table.toString());
|
|
122
|
+
|
|
123
|
+
// Stats line
|
|
124
|
+
console.log(
|
|
125
|
+
' ' +
|
|
126
|
+
dim('Max: ') + colorScore(maxScore) +
|
|
127
|
+
dim(' Avg: ') + colorScore(avgScore) +
|
|
128
|
+
dim(` Submissions: ${submissions.length}`)
|
|
129
|
+
);
|
|
130
|
+
console.log('');
|
|
84
131
|
}
|
|
85
132
|
|
|
86
133
|
function resultsDetailTable(results) {
|
|
87
|
-
const matches = results?.
|
|
134
|
+
const matches = results?.other_matches || [];
|
|
88
135
|
|
|
89
136
|
if (!Array.isArray(matches) || matches.length === 0) {
|
|
90
|
-
console.log(
|
|
137
|
+
console.log('');
|
|
138
|
+
console.log(muted(' No matches found for this submission.'));
|
|
139
|
+
console.log('');
|
|
91
140
|
return;
|
|
92
141
|
}
|
|
93
142
|
|
|
94
143
|
const table = new Table({
|
|
95
144
|
head: [
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
chalk.cyan('Type'),
|
|
145
|
+
dim('Matched With'),
|
|
146
|
+
dim('Tokens'),
|
|
147
|
+
dim('Type'),
|
|
100
148
|
],
|
|
101
149
|
style: { head: [], border: ['dim'] },
|
|
102
150
|
});
|
|
103
151
|
|
|
104
152
|
matches.forEach((m) => {
|
|
105
153
|
table.push([
|
|
106
|
-
m.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
m.type || m.match_type || '',
|
|
154
|
+
m.filename || m.other_filename || dim('unknown'),
|
|
155
|
+
chalk.white.bold(String(m.tokens || '–')),
|
|
156
|
+
m.type ? accent(m.type) : dim('–'),
|
|
110
157
|
]);
|
|
111
158
|
});
|
|
112
159
|
|
|
113
|
-
console.log(
|
|
160
|
+
console.log(table.toString());
|
|
161
|
+
|
|
162
|
+
// Stats summary
|
|
163
|
+
if (results.avg !== undefined) {
|
|
164
|
+
console.log(
|
|
165
|
+
' ' +
|
|
166
|
+
dim('Avg: ') + colorScore(results.avg) +
|
|
167
|
+
dim(' Max: ') + colorScore(results.max) +
|
|
168
|
+
dim(' Min: ') + colorScore(results.min)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
console.log('');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Compact single-line result for scan output
|
|
175
|
+
function scanResultLine(submission) {
|
|
176
|
+
const score = parseFloat(submission.result1 || submission.total_result || 0);
|
|
177
|
+
const name = submission.filename || 'unknown';
|
|
178
|
+
const truncName = name.length > 28 ? name.substring(0, 25) + '...' : name;
|
|
179
|
+
return (
|
|
180
|
+
' ' + accent(String(submission.id).padEnd(8)) +
|
|
181
|
+
truncName.padEnd(30) +
|
|
182
|
+
colorScore(score).padStart(8) +
|
|
183
|
+
' ' + scoreBar(score, 12)
|
|
184
|
+
);
|
|
114
185
|
}
|
|
115
186
|
|
|
116
|
-
module.exports = { colorScore, statusColor, checksTable, overviewTable, resultsDetailTable };
|
|
187
|
+
module.exports = { colorScore, scoreBar, statusColor, checksTable, overviewTable, resultsDetailTable, scanResultLine };
|
package/src/utils/poller.js
CHANGED
|
@@ -2,51 +2,78 @@
|
|
|
2
2
|
|
|
3
3
|
const ora = require('ora');
|
|
4
4
|
const chalk = require('chalk');
|
|
5
|
-
const cliProgress = require('cli-progress');
|
|
6
5
|
const { getCheckStatus } = require('../api/endpoints');
|
|
7
6
|
|
|
7
|
+
const accent = chalk.hex('#8b5cf6');
|
|
8
|
+
const dim = chalk.dim;
|
|
9
|
+
|
|
10
|
+
// Animated progress bar
|
|
11
|
+
function progressBar(percent, width = 24) {
|
|
12
|
+
const p = Math.min(100, Math.max(0, percent));
|
|
13
|
+
const filled = Math.round((p / 100) * width);
|
|
14
|
+
const empty = width - filled;
|
|
15
|
+
return accent('━'.repeat(filled)) + dim('─'.repeat(empty));
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
async function pollUntilComplete(checkId, { silent = false } = {}) {
|
|
9
19
|
const POLL_INTERVAL = 3000;
|
|
10
20
|
let spinner;
|
|
11
|
-
let progressBar;
|
|
12
21
|
let interrupted = false;
|
|
22
|
+
let startTime = Date.now();
|
|
13
23
|
|
|
14
24
|
const cleanup = () => {
|
|
15
25
|
interrupted = true;
|
|
16
26
|
if (spinner) spinner.stop();
|
|
17
|
-
if (progressBar) progressBar.stop();
|
|
18
27
|
};
|
|
19
28
|
|
|
20
29
|
process.on('SIGINT', cleanup);
|
|
21
30
|
|
|
22
31
|
if (!silent) {
|
|
23
|
-
spinner = ora(
|
|
32
|
+
spinner = ora({
|
|
33
|
+
text: 'Waiting for check to complete...',
|
|
34
|
+
spinner: 'dots12',
|
|
35
|
+
color: 'magenta',
|
|
36
|
+
}).start();
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
try {
|
|
27
40
|
while (!interrupted) {
|
|
28
41
|
const status = await getCheckStatus(checkId);
|
|
29
|
-
const
|
|
30
|
-
const progress = status?.progress ??
|
|
42
|
+
const statusId = status?.status_id;
|
|
43
|
+
const progress = status?.progress ?? 0;
|
|
44
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
45
|
+
const elapsedStr = elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
|
|
31
46
|
|
|
32
|
-
//
|
|
33
|
-
if (
|
|
34
|
-
if (spinner)
|
|
35
|
-
|
|
47
|
+
// status_id 4 = Completed
|
|
48
|
+
if (statusId === 4) {
|
|
49
|
+
if (spinner) {
|
|
50
|
+
spinner.succeed(
|
|
51
|
+
chalk.green('Check completed!') +
|
|
52
|
+
dim(` (${elapsedStr})`)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
36
55
|
return status;
|
|
37
56
|
}
|
|
38
57
|
|
|
39
|
-
//
|
|
40
|
-
if (
|
|
41
|
-
if (spinner)
|
|
42
|
-
|
|
58
|
+
// status_id 5 = Error/Failed
|
|
59
|
+
if (statusId === 5 || statusId === -1) {
|
|
60
|
+
if (spinner) {
|
|
61
|
+
spinner.fail(
|
|
62
|
+
chalk.red('Check failed') +
|
|
63
|
+
dim(': ' + (status?.status_message || status?.status || 'Unknown error'))
|
|
64
|
+
);
|
|
65
|
+
}
|
|
43
66
|
return status;
|
|
44
67
|
}
|
|
45
68
|
|
|
46
69
|
// Update progress display
|
|
47
70
|
if (!silent && spinner) {
|
|
48
|
-
const
|
|
49
|
-
spinner.text =
|
|
71
|
+
const statusText = status?.status || 'Processing';
|
|
72
|
+
spinner.text =
|
|
73
|
+
dim(statusText) + ' ' +
|
|
74
|
+
progressBar(progress) + ' ' +
|
|
75
|
+
accent.bold(progress + '%') +
|
|
76
|
+
dim(' ' + elapsedStr);
|
|
50
77
|
}
|
|
51
78
|
|
|
52
79
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const https = require('https');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
const boxen = require('boxen');
|
|
8
|
+
|
|
9
|
+
const PKG = require('../../package.json');
|
|
10
|
+
const CURRENT_VERSION = PKG.version;
|
|
11
|
+
const PACKAGE_NAME = PKG.name;
|
|
12
|
+
|
|
13
|
+
// Cache file to avoid hammering NPM on every run
|
|
14
|
+
const CACHE_DIR = path.join(require('os').homedir(), '.codequiry');
|
|
15
|
+
const CACHE_FILE = path.join(CACHE_DIR, '.update-check');
|
|
16
|
+
const CHECK_INTERVAL = 4 * 60 * 60 * 1000; // 4 hours
|
|
17
|
+
|
|
18
|
+
function fetchLatestVersion() {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const url = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
21
|
+
const req = https.get(url, { headers: { 'Accept': 'application/json' }, timeout: 3000 }, (res) => {
|
|
22
|
+
let data = '';
|
|
23
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
24
|
+
res.on('end', () => {
|
|
25
|
+
try {
|
|
26
|
+
const json = JSON.parse(data);
|
|
27
|
+
resolve(json.version || null);
|
|
28
|
+
} catch {
|
|
29
|
+
resolve(null);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
req.on('error', () => resolve(null));
|
|
34
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readCache() {
|
|
39
|
+
try {
|
|
40
|
+
const raw = fs.readFileSync(CACHE_FILE, 'utf8');
|
|
41
|
+
return JSON.parse(raw);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function writeCache(data) {
|
|
48
|
+
try {
|
|
49
|
+
if (!fs.existsSync(CACHE_DIR)) fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
50
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(data));
|
|
51
|
+
} catch {
|
|
52
|
+
// Ignore write errors
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function compareVersions(current, latest) {
|
|
57
|
+
const a = current.split('.').map(Number);
|
|
58
|
+
const b = latest.split('.').map(Number);
|
|
59
|
+
for (let i = 0; i < 3; i++) {
|
|
60
|
+
if ((b[i] || 0) > (a[i] || 0)) return true;
|
|
61
|
+
if ((b[i] || 0) < (a[i] || 0)) return false;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check for updates in the background. Shows a notification after the
|
|
68
|
+
* command finishes if a newer version is available.
|
|
69
|
+
* Returns a function you call at the end to print the notice (if any).
|
|
70
|
+
*/
|
|
71
|
+
function checkForUpdate() {
|
|
72
|
+
let updateMessage = null;
|
|
73
|
+
|
|
74
|
+
// Start the check immediately (non-blocking)
|
|
75
|
+
const promise = (async () => {
|
|
76
|
+
try {
|
|
77
|
+
// Check cache first
|
|
78
|
+
const cache = readCache();
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
|
|
81
|
+
let latest;
|
|
82
|
+
if (cache && cache.latest && (now - cache.checkedAt) < CHECK_INTERVAL) {
|
|
83
|
+
latest = cache.latest;
|
|
84
|
+
} else {
|
|
85
|
+
latest = await fetchLatestVersion();
|
|
86
|
+
if (latest) {
|
|
87
|
+
writeCache({ latest, checkedAt: now });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (latest && compareVersions(CURRENT_VERSION, latest)) {
|
|
92
|
+
const accent = chalk.hex('#8b5cf6');
|
|
93
|
+
const dim = chalk.dim;
|
|
94
|
+
const isWindows = process.platform === 'win32';
|
|
95
|
+
|
|
96
|
+
const lines = [
|
|
97
|
+
accent.bold('Update available!') + ' ' +
|
|
98
|
+
dim(CURRENT_VERSION) + dim(' → ') + chalk.green.bold(latest),
|
|
99
|
+
'',
|
|
100
|
+
dim('Update with:'),
|
|
101
|
+
' ' + chalk.cyan.bold(`npm install -g ${PACKAGE_NAME}`),
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
if (isWindows) {
|
|
105
|
+
lines.push(dim(' or: ') + chalk.cyan(`irm https://codequiry.com/install.ps1 | iex`));
|
|
106
|
+
} else {
|
|
107
|
+
lines.push(dim(' or: ') + chalk.cyan(`curl -fsSL https://codequiry.com/install.sh | bash`));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
updateMessage = boxen(
|
|
111
|
+
lines.join('\n'),
|
|
112
|
+
{
|
|
113
|
+
padding: { top: 0, bottom: 0, left: 1, right: 1 },
|
|
114
|
+
margin: { top: 1, bottom: 1, left: 1, right: 0 },
|
|
115
|
+
borderStyle: 'round',
|
|
116
|
+
borderColor: '#8b5cf6',
|
|
117
|
+
dimBorder: true,
|
|
118
|
+
}
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
// Never let update check crash the CLI
|
|
123
|
+
}
|
|
124
|
+
})();
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
/** Call this after your command completes to print the update notice */
|
|
128
|
+
async notify() {
|
|
129
|
+
await promise;
|
|
130
|
+
if (updateMessage) {
|
|
131
|
+
console.log(updateMessage);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = { checkForUpdate, CURRENT_VERSION };
|