codequiry-cli 1.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -674
- package/README.md +74 -179
- package/bin/codequiry.js +5 -0
- package/package.json +39 -26
- package/src/api/client.js +35 -0
- package/src/api/endpoints.js +148 -0
- package/src/commands/account.js +61 -0
- package/src/commands/auth.js +56 -0
- package/src/commands/checks.js +22 -0
- package/src/commands/create.js +86 -0
- package/src/commands/delete.js +72 -0
- package/src/commands/results.js +118 -0
- package/src/commands/scan.js +189 -0
- package/src/commands/start.js +76 -0
- package/src/commands/status.js +65 -0
- package/src/commands/upload.js +102 -0
- package/src/index.js +132 -51
- package/src/ui/menu.js +85 -0
- package/src/ui/prompts.js +86 -0
- package/src/utils/banner.js +20 -0
- package/src/utils/config.js +67 -0
- package/src/utils/display.js +122 -0
- package/src/utils/errors.js +63 -0
- package/src/utils/poller.js +56 -0
- package/src/utils/zipper.js +96 -0
- package/src/auth.js +0 -47
- package/src/check.js +0 -444
- package/src/const.js +0 -56
- package/src/util.js +0 -26
- package/uploads/.gitkeep +0 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const { requireAuth } = require('../utils/errors');
|
|
7
|
+
const { getLanguages, getTestTypes, createCheck } = require('../api/endpoints');
|
|
8
|
+
|
|
9
|
+
async function createCommand(options) {
|
|
10
|
+
requireAuth();
|
|
11
|
+
|
|
12
|
+
let name = options.name;
|
|
13
|
+
let languageId = options.language;
|
|
14
|
+
let testType = options.testType;
|
|
15
|
+
|
|
16
|
+
// Fetch languages and test types
|
|
17
|
+
const spinner = ora('Loading options...').start();
|
|
18
|
+
let languages, testTypes;
|
|
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
|
+
}
|
|
27
|
+
|
|
28
|
+
const langList = Array.isArray(languages) ? languages : languages?.languages || [];
|
|
29
|
+
const typeList = Array.isArray(testTypes) ? testTypes : testTypes?.test_types || [];
|
|
30
|
+
|
|
31
|
+
// Interactive prompts for missing options
|
|
32
|
+
const questions = [];
|
|
33
|
+
|
|
34
|
+
if (!name) {
|
|
35
|
+
questions.push({
|
|
36
|
+
type: 'input',
|
|
37
|
+
name: 'name',
|
|
38
|
+
message: 'Check name:',
|
|
39
|
+
validate: (input) => (input.length > 0 ? true : 'Name is required'),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!languageId) {
|
|
44
|
+
questions.push({
|
|
45
|
+
type: 'list',
|
|
46
|
+
name: 'languageId',
|
|
47
|
+
message: 'Programming language:',
|
|
48
|
+
choices: langList.map((l) => ({
|
|
49
|
+
name: l.language || l.name,
|
|
50
|
+
value: l.id,
|
|
51
|
+
})),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
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
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (questions.length > 0) {
|
|
68
|
+
const answers = await inquirer.prompt(questions);
|
|
69
|
+
name = name || answers.name;
|
|
70
|
+
languageId = languageId || answers.languageId;
|
|
71
|
+
testType = testType || answers.testType;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const createSpinner = ora('Creating check...').start();
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const result = await createCheck(name, languageId, testType);
|
|
78
|
+
// API returns the Assignment object directly
|
|
79
|
+
createSpinner.succeed(`Check created: ${chalk.cyan(result.name || name)} (ID: ${result.id || 'N/A'})`);
|
|
80
|
+
return result;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
createSpinner.fail('Failed to create check.');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = createCommand;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const { requireAuth } = require('../utils/errors');
|
|
7
|
+
const { listChecks, deleteCheck } = require('../api/endpoints');
|
|
8
|
+
|
|
9
|
+
async function deleteCommand(options) {
|
|
10
|
+
requireAuth();
|
|
11
|
+
|
|
12
|
+
let checkId = options.check;
|
|
13
|
+
|
|
14
|
+
if (!checkId) {
|
|
15
|
+
const spinner = ora('Fetching checks...').start();
|
|
16
|
+
let checks;
|
|
17
|
+
try {
|
|
18
|
+
const data = await listChecks();
|
|
19
|
+
checks = Array.isArray(data) ? data : data?.checks || [];
|
|
20
|
+
spinner.stop();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
spinner.fail('Failed to fetch checks.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (checks.length === 0) {
|
|
27
|
+
console.log(chalk.yellow('\n⚠ No checks found.\n'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const { selectedCheck } = await inquirer.prompt([
|
|
32
|
+
{
|
|
33
|
+
type: 'list',
|
|
34
|
+
name: 'selectedCheck',
|
|
35
|
+
message: 'Select a check to delete:',
|
|
36
|
+
choices: checks.map((c) => ({
|
|
37
|
+
name: `${c.id} - ${c.name}`,
|
|
38
|
+
value: c.id,
|
|
39
|
+
})),
|
|
40
|
+
},
|
|
41
|
+
]);
|
|
42
|
+
checkId = selectedCheck;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Confirm deletion
|
|
46
|
+
if (!options.force) {
|
|
47
|
+
const { confirm } = await inquirer.prompt([
|
|
48
|
+
{
|
|
49
|
+
type: 'confirm',
|
|
50
|
+
name: 'confirm',
|
|
51
|
+
message: `Are you sure you want to delete check ${checkId}?`,
|
|
52
|
+
default: false,
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
if (!confirm) {
|
|
57
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const spinner = ora('Deleting check...').start();
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await deleteCheck(checkId);
|
|
66
|
+
spinner.succeed(`Check ${chalk.cyan(checkId)} deleted.`);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
spinner.fail('Failed to delete check.');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = deleteCommand;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const open = require('open');
|
|
7
|
+
const { requireAuth } = require('../utils/errors');
|
|
8
|
+
const { listChecks, getOverview, getResults } = require('../api/endpoints');
|
|
9
|
+
const { overviewTable, resultsDetailTable } = require('../utils/display');
|
|
10
|
+
|
|
11
|
+
async function resultsCommand(options) {
|
|
12
|
+
requireAuth();
|
|
13
|
+
|
|
14
|
+
let checkId = options.check;
|
|
15
|
+
|
|
16
|
+
if (!checkId) {
|
|
17
|
+
const spinner = ora('Fetching checks...').start();
|
|
18
|
+
let checks;
|
|
19
|
+
try {
|
|
20
|
+
const data = await listChecks();
|
|
21
|
+
checks = Array.isArray(data) ? data : data?.checks || [];
|
|
22
|
+
spinner.stop();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
spinner.fail('Failed to fetch checks.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Filter to completed checks
|
|
29
|
+
const completed = checks.filter(
|
|
30
|
+
(c) => c.status_id === 4 || c.assignmentstatuses?.status === 'Completed'
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (completed.length === 0) {
|
|
34
|
+
console.log(chalk.yellow('\n⚠ No completed checks found.\n'));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const { selectedCheck } = await inquirer.prompt([
|
|
39
|
+
{
|
|
40
|
+
type: 'list',
|
|
41
|
+
name: 'selectedCheck',
|
|
42
|
+
message: 'Select a check:',
|
|
43
|
+
choices: completed.map((c) => ({
|
|
44
|
+
name: `${c.id} - ${c.name}`,
|
|
45
|
+
value: c.id,
|
|
46
|
+
})),
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
checkId = selectedCheck;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fetch overview
|
|
53
|
+
const spinner = ora('Fetching results...').start();
|
|
54
|
+
let overview;
|
|
55
|
+
try {
|
|
56
|
+
overview = await getOverview(checkId);
|
|
57
|
+
spinner.stop();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
spinner.fail('Failed to fetch results.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(chalk.bold('\n Results Overview'));
|
|
64
|
+
overviewTable(overview);
|
|
65
|
+
|
|
66
|
+
const submissions = overview?.submissions || overview || [];
|
|
67
|
+
if (!Array.isArray(submissions) || submissions.length === 0) return;
|
|
68
|
+
|
|
69
|
+
// Offer to drill into a submission
|
|
70
|
+
if (process.stdout.isTTY) {
|
|
71
|
+
const { action } = await inquirer.prompt([
|
|
72
|
+
{
|
|
73
|
+
type: 'list',
|
|
74
|
+
name: 'action',
|
|
75
|
+
message: 'What would you like to do?',
|
|
76
|
+
choices: [
|
|
77
|
+
{ name: 'View submission details', value: 'detail' },
|
|
78
|
+
{ name: 'Open in browser', value: 'browser' },
|
|
79
|
+
{ name: 'Exit', value: 'exit' },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
if (action === 'detail') {
|
|
85
|
+
const { submissionId } = await inquirer.prompt([
|
|
86
|
+
{
|
|
87
|
+
type: 'list',
|
|
88
|
+
name: 'submissionId',
|
|
89
|
+
message: 'Select a submission:',
|
|
90
|
+
choices: submissions.map((s) => ({
|
|
91
|
+
name: `${s.id} - ${s.filename || 'unknown'}`,
|
|
92
|
+
value: s.id,
|
|
93
|
+
})),
|
|
94
|
+
},
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
const detailSpinner = ora('Fetching details...').start();
|
|
98
|
+
try {
|
|
99
|
+
const details = await getResults(checkId, submissionId);
|
|
100
|
+
detailSpinner.stop();
|
|
101
|
+
resultsDetailTable(details);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
detailSpinner.fail('Failed to fetch details.');
|
|
104
|
+
}
|
|
105
|
+
} else if (action === 'browser') {
|
|
106
|
+
const url = overview?.overviewURL || `https://codequiry.com/results/${checkId}`;
|
|
107
|
+
await open(url);
|
|
108
|
+
console.log(chalk.dim(' Opened in browser.\n'));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Output JSON if requested
|
|
113
|
+
if (options.output === 'json') {
|
|
114
|
+
console.log(JSON.stringify(overview, null, 2));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = resultsCommand;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const inquirer = require('inquirer');
|
|
6
|
+
const ora = require('ora');
|
|
7
|
+
const { requireAuth } = require('../utils/errors');
|
|
8
|
+
const { getLanguages, getTestTypes, createCheck, uploadZip, uploadBatch, startCheck } = require('../api/endpoints');
|
|
9
|
+
const { prepareUploads, cleanupTempFiles } = require('../utils/zipper');
|
|
10
|
+
const { pollUntilComplete } = require('../utils/poller');
|
|
11
|
+
const { overviewTable } = require('../utils/display');
|
|
12
|
+
const { getOverview } = require('../api/endpoints');
|
|
13
|
+
|
|
14
|
+
async function scanCommand(inputPath, options) {
|
|
15
|
+
requireAuth();
|
|
16
|
+
|
|
17
|
+
const isTTY = process.stdout.isTTY;
|
|
18
|
+
const isJson = options.output === 'json';
|
|
19
|
+
|
|
20
|
+
// 1. Resolve input path
|
|
21
|
+
if (!inputPath) {
|
|
22
|
+
if (isTTY) {
|
|
23
|
+
const { filePath } = await inquirer.prompt([
|
|
24
|
+
{
|
|
25
|
+
type: 'input',
|
|
26
|
+
name: 'filePath',
|
|
27
|
+
message: 'Path to folder to scan:',
|
|
28
|
+
default: '.',
|
|
29
|
+
validate: (input) => {
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
return fs.existsSync(path.resolve(input)) ? true : 'Path does not exist';
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
]);
|
|
35
|
+
inputPath = filePath;
|
|
36
|
+
} else {
|
|
37
|
+
inputPath = '.';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const resolvedPath = path.resolve(inputPath);
|
|
42
|
+
|
|
43
|
+
// 2. Get language + test type
|
|
44
|
+
let languageId = options.language;
|
|
45
|
+
let testType = options.testType;
|
|
46
|
+
let checkName = options.name || path.basename(resolvedPath);
|
|
47
|
+
|
|
48
|
+
if (!languageId || !testType) {
|
|
49
|
+
const spinner = ora('Loading options...').start();
|
|
50
|
+
let languages, testTypes;
|
|
51
|
+
try {
|
|
52
|
+
[languages, testTypes] = await Promise.all([getLanguages(), getTestTypes()]);
|
|
53
|
+
spinner.stop();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
spinner.fail('Failed to load options.');
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const langList = Array.isArray(languages) ? languages : languages?.languages || [];
|
|
60
|
+
const typeList = Array.isArray(testTypes) ? testTypes : testTypes?.test_types || [];
|
|
61
|
+
|
|
62
|
+
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.language || l.name, 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;
|
|
87
|
+
} else {
|
|
88
|
+
// Non-TTY: use defaults
|
|
89
|
+
languageId = languageId || (langList[0] && langList[0].id);
|
|
90
|
+
testType = testType || (typeList[0] && typeList[0].id);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 3. Zip
|
|
95
|
+
const zipSpinner = ora('Preparing files...').start();
|
|
96
|
+
let zipFiles;
|
|
97
|
+
try {
|
|
98
|
+
zipFiles = await prepareUploads(resolvedPath);
|
|
99
|
+
zipSpinner.succeed(`Prepared ${zipFiles.length} file(s)`);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
zipSpinner.fail('Failed to prepare files: ' + error.message);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 4. Create check
|
|
106
|
+
const createSpinner = ora('Creating check...').start();
|
|
107
|
+
let check;
|
|
108
|
+
try {
|
|
109
|
+
const result = await createCheck(checkName, languageId, testType);
|
|
110
|
+
check = result; // API returns Assignment object directly
|
|
111
|
+
createSpinner.succeed(`Check created: ${chalk.cyan(check.name || checkName)} (ID: ${check.id})`);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
createSpinner.fail('Failed to create check.');
|
|
114
|
+
cleanupTempFiles(zipFiles);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 5. Upload
|
|
119
|
+
const uploadSpinner = ora('Uploading files...').start();
|
|
120
|
+
try {
|
|
121
|
+
if (zipFiles.length === 1) {
|
|
122
|
+
await uploadZip(check.id, zipFiles[0]);
|
|
123
|
+
} else {
|
|
124
|
+
for (let i = 0; i < zipFiles.length; i += 50) {
|
|
125
|
+
const chunk = zipFiles.slice(i, i + 50);
|
|
126
|
+
await uploadBatch(check.id, chunk);
|
|
127
|
+
uploadSpinner.text = `Uploading... (${Math.min(i + 50, zipFiles.length)}/${zipFiles.length})`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
uploadSpinner.succeed(`Uploaded ${zipFiles.length} file(s)`);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
uploadSpinner.fail('Upload failed.');
|
|
133
|
+
cleanupTempFiles(zipFiles);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
cleanupTempFiles(zipFiles);
|
|
138
|
+
|
|
139
|
+
// 6. Start check
|
|
140
|
+
const webcheck = options.web ? 1 : 0;
|
|
141
|
+
const dbcheck = options.db ? 1 : 0;
|
|
142
|
+
|
|
143
|
+
const startSpinner = ora('Starting check...').start();
|
|
144
|
+
try {
|
|
145
|
+
await startCheck(check.id, webcheck, dbcheck);
|
|
146
|
+
startSpinner.succeed('Check started!');
|
|
147
|
+
} catch (error) {
|
|
148
|
+
startSpinner.fail('Failed to start check.');
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 7. Poll until complete
|
|
153
|
+
console.log('');
|
|
154
|
+
const finalStatus = await pollUntilComplete(check.id);
|
|
155
|
+
|
|
156
|
+
// 8. Show results
|
|
157
|
+
if (finalStatus) {
|
|
158
|
+
try {
|
|
159
|
+
const overview = await getOverview(check.id);
|
|
160
|
+
|
|
161
|
+
if (isJson) {
|
|
162
|
+
console.log(JSON.stringify(overview, null, 2));
|
|
163
|
+
} else {
|
|
164
|
+
console.log(chalk.bold('\n Results'));
|
|
165
|
+
overviewTable(overview);
|
|
166
|
+
console.log(
|
|
167
|
+
chalk.dim(` View full results: https://codequiry.com/results/${check.id}\n`)
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Exit code based on results for CI
|
|
172
|
+
if (options.threshold) {
|
|
173
|
+
const submissions = overview?.submissions || overview || [];
|
|
174
|
+
const maxScore = Math.max(
|
|
175
|
+
...submissions.map((s) => parseFloat(s.result1 || s.total_result || 0))
|
|
176
|
+
);
|
|
177
|
+
if (maxScore > parseFloat(options.threshold)) {
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
} catch (error) {
|
|
182
|
+
if (!isJson) {
|
|
183
|
+
console.log(chalk.dim(`\n View results at: https://codequiry.com/results/${check.id}\n`));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = scanCommand;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const { requireAuth } = require('../utils/errors');
|
|
7
|
+
const { listChecks, startCheck } = require('../api/endpoints');
|
|
8
|
+
|
|
9
|
+
async function startCommand(options) {
|
|
10
|
+
requireAuth();
|
|
11
|
+
|
|
12
|
+
let checkId = options.checkId;
|
|
13
|
+
let webcheck = options.web ? 1 : 0;
|
|
14
|
+
let dbcheck = options.db ? 1 : 0;
|
|
15
|
+
|
|
16
|
+
// If no check ID, let user pick one
|
|
17
|
+
if (!checkId) {
|
|
18
|
+
const spinner = ora('Fetching checks...').start();
|
|
19
|
+
let checks;
|
|
20
|
+
try {
|
|
21
|
+
const data = await listChecks();
|
|
22
|
+
checks = Array.isArray(data) ? data : data?.checks || [];
|
|
23
|
+
spinner.stop();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
spinner.fail('Failed to fetch checks.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (checks.length === 0) {
|
|
30
|
+
console.log(chalk.yellow('\n⚠ No checks found. Create one first.\n'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { selectedCheck } = await inquirer.prompt([
|
|
35
|
+
{
|
|
36
|
+
type: 'list',
|
|
37
|
+
name: 'selectedCheck',
|
|
38
|
+
message: 'Select a check to start:',
|
|
39
|
+
choices: checks.map((c) => ({
|
|
40
|
+
name: `${c.id} - ${c.name} (${c.submission_count || 0} submissions)`,
|
|
41
|
+
value: c.id,
|
|
42
|
+
})),
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
checkId = selectedCheck;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Ask about check options if not set via flags
|
|
49
|
+
if (!options.web && !options.db && process.stdout.isTTY) {
|
|
50
|
+
const { checkOptions } = await inquirer.prompt([
|
|
51
|
+
{
|
|
52
|
+
type: 'checkbox',
|
|
53
|
+
name: 'checkOptions',
|
|
54
|
+
message: 'Check options (space to select):',
|
|
55
|
+
choices: [
|
|
56
|
+
{ name: 'Web check (search online sources)', value: 'web' },
|
|
57
|
+
{ name: 'Database check (search our database)', value: 'db' },
|
|
58
|
+
],
|
|
59
|
+
},
|
|
60
|
+
]);
|
|
61
|
+
webcheck = checkOptions.includes('web') ? 1 : 0;
|
|
62
|
+
dbcheck = checkOptions.includes('db') ? 1 : 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const spinner = ora('Starting check...').start();
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await startCheck(checkId, webcheck, dbcheck);
|
|
69
|
+
spinner.succeed(`Check ${chalk.cyan(checkId)} started!`);
|
|
70
|
+
console.log(chalk.dim(' Run `codequiry status --check ' + checkId + '` to monitor progress.\n'));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
spinner.fail('Failed to start check.');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = startCommand;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const { requireAuth } = require('../utils/errors');
|
|
7
|
+
const { listChecks, getCheckStatus } = require('../api/endpoints');
|
|
8
|
+
const { pollUntilComplete } = require('../utils/poller');
|
|
9
|
+
const { statusColor } = require('../utils/display');
|
|
10
|
+
|
|
11
|
+
async function statusCommand(options) {
|
|
12
|
+
requireAuth();
|
|
13
|
+
|
|
14
|
+
let checkId = options.check;
|
|
15
|
+
|
|
16
|
+
if (!checkId) {
|
|
17
|
+
const spinner = ora('Fetching checks...').start();
|
|
18
|
+
let checks;
|
|
19
|
+
try {
|
|
20
|
+
const data = await listChecks();
|
|
21
|
+
checks = Array.isArray(data) ? data : data?.checks || [];
|
|
22
|
+
spinner.stop();
|
|
23
|
+
} catch (error) {
|
|
24
|
+
spinner.fail('Failed to fetch checks.');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (checks.length === 0) {
|
|
29
|
+
console.log(chalk.yellow('\n⚠ No checks found.\n'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { selectedCheck } = await inquirer.prompt([
|
|
34
|
+
{
|
|
35
|
+
type: 'list',
|
|
36
|
+
name: 'selectedCheck',
|
|
37
|
+
message: 'Select a check:',
|
|
38
|
+
choices: checks.map((c) => ({
|
|
39
|
+
name: `${c.id} - ${c.name}`,
|
|
40
|
+
value: c.id,
|
|
41
|
+
})),
|
|
42
|
+
},
|
|
43
|
+
]);
|
|
44
|
+
checkId = selectedCheck;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (options.poll) {
|
|
48
|
+
// Live polling mode
|
|
49
|
+
await pollUntilComplete(checkId);
|
|
50
|
+
} else {
|
|
51
|
+
// One-shot status check
|
|
52
|
+
const spinner = ora('Checking status...').start();
|
|
53
|
+
try {
|
|
54
|
+
const status = await getCheckStatus(checkId);
|
|
55
|
+
spinner.stop();
|
|
56
|
+
const s = status?.status || 'Unknown';
|
|
57
|
+
const p = status?.progress ?? '';
|
|
58
|
+
console.log(`\n Check ${chalk.cyan(checkId)}: ${statusColor(s)}${p ? ` (${p}%)` : ''}\n`);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
spinner.fail('Failed to fetch status.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = statusCommand;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const inquirer = require('inquirer');
|
|
6
|
+
const ora = require('ora');
|
|
7
|
+
const { requireAuth } = require('../utils/errors');
|
|
8
|
+
const { listChecks, uploadZip, uploadBatch } = require('../api/endpoints');
|
|
9
|
+
const { prepareUploads, cleanupTempFiles } = require('../utils/zipper');
|
|
10
|
+
|
|
11
|
+
async function uploadCommand(inputPath, options) {
|
|
12
|
+
requireAuth();
|
|
13
|
+
|
|
14
|
+
let checkId = options.checkId;
|
|
15
|
+
|
|
16
|
+
// If no check ID, let user pick one
|
|
17
|
+
if (!checkId) {
|
|
18
|
+
const spinner = ora('Fetching checks...').start();
|
|
19
|
+
let checks;
|
|
20
|
+
try {
|
|
21
|
+
const data = await listChecks();
|
|
22
|
+
checks = Array.isArray(data) ? data : data?.checks || [];
|
|
23
|
+
spinner.stop();
|
|
24
|
+
} catch (error) {
|
|
25
|
+
spinner.fail('Failed to fetch checks.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (checks.length === 0) {
|
|
30
|
+
console.log(chalk.yellow('\n⚠ No checks found. Create one first with `codequiry create`.\n'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { selectedCheck } = await inquirer.prompt([
|
|
35
|
+
{
|
|
36
|
+
type: 'list',
|
|
37
|
+
name: 'selectedCheck',
|
|
38
|
+
message: 'Select a check:',
|
|
39
|
+
choices: checks.map((c) => ({
|
|
40
|
+
name: `${c.id} - ${c.name} (${c.language_name || c.language || ''})`,
|
|
41
|
+
value: c.id,
|
|
42
|
+
})),
|
|
43
|
+
},
|
|
44
|
+
]);
|
|
45
|
+
checkId = selectedCheck;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Get input path if not provided
|
|
49
|
+
if (!inputPath) {
|
|
50
|
+
const { filePath } = await inquirer.prompt([
|
|
51
|
+
{
|
|
52
|
+
type: 'input',
|
|
53
|
+
name: 'filePath',
|
|
54
|
+
message: 'Path to file or folder:',
|
|
55
|
+
default: '.',
|
|
56
|
+
validate: (input) => {
|
|
57
|
+
const resolved = path.resolve(input);
|
|
58
|
+
const fs = require('fs');
|
|
59
|
+
return fs.existsSync(resolved) ? true : 'Path does not exist';
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
inputPath = filePath;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const resolvedPath = path.resolve(inputPath);
|
|
67
|
+
|
|
68
|
+
// Zip if needed
|
|
69
|
+
const zipSpinner = ora('Preparing files...').start();
|
|
70
|
+
let zipFiles;
|
|
71
|
+
try {
|
|
72
|
+
zipFiles = await prepareUploads(resolvedPath);
|
|
73
|
+
zipSpinner.succeed(`Prepared ${zipFiles.length} file(s) for upload`);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
zipSpinner.fail('Failed to prepare files: ' + error.message);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Upload
|
|
80
|
+
const uploadSpinner = ora('Uploading...').start();
|
|
81
|
+
try {
|
|
82
|
+
if (zipFiles.length === 1) {
|
|
83
|
+
await uploadZip(checkId, zipFiles[0]);
|
|
84
|
+
} else {
|
|
85
|
+
// Batch upload in chunks of 50
|
|
86
|
+
for (let i = 0; i < zipFiles.length; i += 50) {
|
|
87
|
+
const chunk = zipFiles.slice(i, i + 50);
|
|
88
|
+
await uploadBatch(checkId, chunk);
|
|
89
|
+
if (zipFiles.length > 50) {
|
|
90
|
+
uploadSpinner.text = `Uploading... (${Math.min(i + 50, zipFiles.length)}/${zipFiles.length})`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
uploadSpinner.succeed(`Uploaded ${zipFiles.length} file(s) to check ${chalk.cyan(checkId)}`);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
uploadSpinner.fail('Upload failed.');
|
|
97
|
+
} finally {
|
|
98
|
+
cleanupTempFiles(zipFiles);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = uploadCommand;
|