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 CHANGED
@@ -1,6 +1,11 @@
1
- # Codequiry CLI
1
+ # Codequiry CLI — Code Plagiarism Checker for the Terminal
2
2
 
3
- Source code similarity checker from your terminal. Detect plagiarism across 65+ programming languages.
3
+ [![npm version](https://img.shields.io/npm/v/codequiry-cli.svg)](https://www.npmjs.com/package/codequiry-cli)
4
+ [![license](https://img.shields.io/npm/l/codequiry-cli.svg)](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 Documentation](https://codequiry.com/usage/docs)
74
- - [Get API Key](https://codequiry.com/dashboard)
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.0.0",
4
- "description": "Codequiry CLI - Source code similarity checker",
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",
@@ -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 account = await getAccount();
18
+ const data = await getAccount();
16
19
  spinner.stop();
17
20
 
18
- const user = account.user || account;
19
- const plan = user.plan_name || user.plan || 'Free';
20
- const checksUsed = user.checks_used ?? user.procheck_used ?? 0;
21
- const checksLimit = user.checks_limit ?? user.procheck_limit ?? 0;
22
- const quotaPct = checksLimit > 0 ? Math.round((checksUsed / checksLimit) * 100) : 0;
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
- const filled = Math.round((quotaPct / 100) * barLen);
25
- const quotaBar = chalk.green('█'.repeat(filled)) + chalk.dim('░'.repeat(barLen - filled));
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
- chalk.bold('Account'),
42
+ accent.bold(' Account'),
29
43
  '',
30
- `${chalk.dim('Name:')} ${user.name || user.email || 'N/A'}`,
31
- `${chalk.dim('Email:')} ${user.email || 'N/A'}`,
32
- `${chalk.dim('Plan:')} ${chalk.cyan(plan)}`,
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
- chalk.bold('Usage'),
49
+ accent.bold(' Quota'),
35
50
  '',
36
- `${chalk.dim('Checks:')} ${checksUsed} / ${checksLimit}`,
37
- ` ${quotaBar} ${quotaPct}%`,
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
- boxen(content, {
43
- padding: 1,
44
- margin: 1,
45
- borderStyle: 'round',
46
- borderColor: 'cyan',
47
- }) +
48
- '\n'
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.');
@@ -13,20 +13,35 @@ async function createCommand(options) {
13
13
  let languageId = options.language;
14
14
  let testType = options.testType;
15
15
 
16
- // Fetch languages and test types
17
- const spinner = ora('Loading options...').start();
18
- let languages, testTypes;
16
+ // If all options provided via flags, skip fetching options
17
+ let langList = [];
18
+ let typeList = [];
19
19
 
20
- try {
21
- [languages, testTypes] = await Promise.all([getLanguages(), getTestTypes()]);
22
- spinner.stop();
23
- } catch (error) {
24
- spinner.fail('Failed to load options.');
25
- return;
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
- const langList = Array.isArray(languages) ? languages : languages?.languages || [];
29
- const typeList = Array.isArray(testTypes) ? testTypes : testTypes?.test_types || [];
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.name || l.language,
64
+ name: l.language || l.name,
50
65
  value: l.id,
51
66
  })),
52
67
  });
53
68
  }
54
69
 
55
70
  if (!testType) {
56
- questions.push({
57
- type: 'list',
58
- name: 'testType',
59
- message: 'Check engine:',
60
- choices: typeList.map((t) => ({
61
- name: t.name || t.type,
62
- value: t.id,
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
- createSpinner.succeed(`Check created: ${chalk.cyan(result.check?.name || name)} (ID: ${result.check?.id || 'N/A'})`);
79
- return result.check || result;
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
  }
@@ -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.status === 5 || c.status_name === 'Completed' || c.status === 'completed'
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 || s.submission_id} - ${s.filename || s.name}`,
92
- value: s.id || s.submission_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
- await open(`https://codequiry.com/results/${checkId}`);
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
  }
@@ -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
- const { getOverview } = require('../api/endpoints');
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 = ora('Loading options...').start();
50
- let languages, testTypes;
62
+ const spinner = spin('Loading options...').start();
63
+ let langList = [];
64
+ let typeList = [];
51
65
  try {
52
- [languages, testTypes] = await Promise.all([getLanguages(), getTestTypes()]);
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 answers = await inquirer.prompt([
64
- ...(!languageId
65
- ? [
66
- {
67
- type: 'list',
68
- name: 'languageId',
69
- message: 'Programming language:',
70
- choices: langList.map((l) => ({ name: l.name || l.language, value: l.id })),
71
- },
72
- ]
73
- : []),
74
- ...(!testType
75
- ? [
76
- {
77
- type: 'list',
78
- name: 'testType',
79
- message: 'Check engine:',
80
- choices: typeList.map((t) => ({ name: t.name || t.type, value: t.id })),
81
- },
82
- ]
83
- : []),
84
- ]);
85
- languageId = languageId || answers.languageId;
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 = ora('Preparing files...').start();
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 = ora('Creating check...').start();
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.check || result;
111
- createSpinner.succeed(`Check created: ${chalk.cyan(check.name || checkName)} (ID: ${check.id})`);
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 = ora('Uploading files...').start();
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
- uploadSpinner.text = `Uploading... (${Math.min(i + 50, zipFiles.length)}/${zipFiles.length})`;
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 = ora('Starting check...').start();
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(chalk.bold('\n Results'));
193
+ console.log('');
194
+ console.log(' ' + chalk.bold('Results') + dim(` · completed in ${elapsedStr}`));
165
195
  overviewTable(overview);
166
- console.log(
167
- chalk.dim(` View full results: https://codequiry.com/results/${check.id}\n`)
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 based on results for CI
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.result_score || s.similarity || 0))
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(chalk.dim(`\n View results at: https://codequiry.com/results/${check.id}\n`));
217
+ console.log(dim(`\n View results at: https://codequiry.com/results/${check.id}\n`));
184
218
  }
185
219
  }
186
220
  }
@@ -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?.status_name || status?.status || 'Unknown';
57
- const p = status?.progress ?? status?.percentage ?? '';
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.0.0');
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
- // 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);
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(' You need to authenticate first.\n'));
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
- { 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' },
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
- console.log(chalk.dim(' Goodbye!\n'));
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
  }
@@ -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
- 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.0\n'));
22
+ logo.forEach(line => console.log(cqGradient(line)));
15
23
  } catch {
16
- console.log(gradient.vice('\n Codequiry CLI v2.0.0\n'));
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 };
@@ -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 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) + '%');
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 === 'processing' || s === 'running' || s === 'in_progress') return chalk.yellow(status);
18
- if (s === 'failed' || s === 'error') return chalk.red(status);
19
- return chalk.dim(status);
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 table = new Table({
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(chalk.dim('\n No checks found.\n'));
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
- chalk.white(c.id),
45
- c.name || '',
46
- c.language_name || c.language || '',
47
- statusColor(c.status_name || c.status || ''),
48
- String(c.submission_count || c.submissions || 0),
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('\n' + table.toString() + '\n');
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 || overview || [];
86
+ const submissions = overview?.submissions || [];
58
87
 
59
88
  if (!Array.isArray(submissions) || submissions.length === 0) {
60
- console.log(chalk.dim('\n No results available.\n'));
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
- chalk.cyan('ID'),
67
- chalk.cyan('Filename'),
68
- chalk.cyan('Similarity'),
69
- chalk.cyan('Matches'),
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
- chalk.white(s.id || s.submission_id || ''),
77
- s.filename || s.name || '',
78
- colorScore(s.result_score || s.similarity || s.score || 0),
79
- String(s.match_count || s.matches || ''),
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('\n' + table.toString() + '\n');
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?.matches || results?.results || results || [];
134
+ const matches = results?.other_matches || [];
88
135
 
89
136
  if (!Array.isArray(matches) || matches.length === 0) {
90
- console.log(chalk.dim('\n No matches found.\n'));
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
- chalk.cyan('Matched With'),
97
- chalk.cyan('Similarity'),
98
- chalk.cyan('Lines Matched'),
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.matched_filename || m.other_filename || m.name || '',
107
- colorScore(m.score || m.similarity || 0),
108
- String(m.lines_matched || m.matched_lines || ''),
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('\n' + table.toString() + '\n');
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 };
@@ -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('Waiting for check to complete...').start();
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 statusVal = status?.status ?? status?.check_status ?? status;
30
- const progress = status?.progress ?? status?.percentage ?? 0;
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
- // Check if completed
33
- if (statusVal === 5 || statusVal === 'completed' || statusVal === 'done') {
34
- if (spinner) spinner.succeed('Check completed!');
35
- if (progressBar) progressBar.stop();
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
- // Check if failed
40
- if (statusVal === 'failed' || statusVal === 'error' || statusVal === -1) {
41
- if (spinner) spinner.fail('Check failed.');
42
- if (progressBar) progressBar.stop();
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 pct = typeof progress === 'number' ? progress : 0;
49
- spinner.text = `Processing... ${chalk.cyan(pct + '%')}`;
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 };