@devquest/cli 1.0.8 → 1.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/dist/commands/submit.js
CHANGED
|
@@ -21,11 +21,8 @@ exports.submitCommand = new commander_1.Command('submit')
|
|
|
21
21
|
process.exit(1);
|
|
22
22
|
}
|
|
23
23
|
const currentDir = process.cwd();
|
|
24
|
-
if (!fs_1.default.existsSync(path_1.default.join(currentDir, '.git'))) {
|
|
25
|
-
console.log(chalk_1.default.red('Não encontrei um repositório git no diretório atual. Execute esse comando na raiz do desafio.'));
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
24
|
const git = (0, simple_git_1.default)(currentDir);
|
|
25
|
+
const hasGit = fs_1.default.existsSync(path_1.default.join(currentDir, '.git'));
|
|
29
26
|
const packageJsonPath = path_1.default.join(currentDir, 'package.json');
|
|
30
27
|
let challengeId = 'unknown';
|
|
31
28
|
if (fs_1.default.existsSync(packageJsonPath)) {
|
|
@@ -47,51 +44,81 @@ exports.submitCommand = new commander_1.Command('submit')
|
|
|
47
44
|
catch (e) {
|
|
48
45
|
console.log(chalk_1.default.yellow('⚠️ Não foi possível determinar o estágio na API. Assumindo Estágio 1.'));
|
|
49
46
|
}
|
|
50
|
-
console.log(chalk_1.default.blue(
|
|
47
|
+
console.log(chalk_1.default.blue(`\n📌 Você está no Estágio ${stageOrder} deste desafio.`));
|
|
48
|
+
// 1. RODAR TESTES LOCAIS REAIS (Tester.ts)
|
|
51
49
|
const { DevQuestTester } = require('../utils/tester');
|
|
52
50
|
const tester = new DevQuestTester(currentDir, challengeId, stageOrder);
|
|
53
51
|
const passed = await tester.execute();
|
|
54
52
|
if (!passed) {
|
|
55
|
-
console.log(chalk_1.default.red('\n🚫 Execução interrompida pela falha dos testes
|
|
53
|
+
console.log(chalk_1.default.red('\n🚫 Execução local interrompida pela falha dos testes. Corrija o código antes de submeter.'));
|
|
56
54
|
process.exit(1);
|
|
57
55
|
}
|
|
58
|
-
console.log(chalk_1.default.green('\n✅ Integração aprovada!
|
|
56
|
+
console.log(chalk_1.default.green('\n✅ Integração local aprovada! Preparando envio...'));
|
|
59
57
|
try {
|
|
60
|
-
//
|
|
61
|
-
console.log(chalk_1.default.gray('
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
58
|
+
// 2. Coletar arquivos para envio (Opção B - via API POST)
|
|
59
|
+
console.log(chalk_1.default.gray('Empacotando código-fonte para submissão remota...'));
|
|
60
|
+
const files = {};
|
|
61
|
+
const srcDir = path_1.default.join(currentDir, 'src');
|
|
62
|
+
// Helper para ler arquivos recursivamente
|
|
63
|
+
const collectFiles = (dir, baseDir) => {
|
|
64
|
+
const items = fs_1.default.readdirSync(dir);
|
|
65
|
+
for (const item of items) {
|
|
66
|
+
const fullPath = path_1.default.join(dir, item);
|
|
67
|
+
const relativePath = path_1.default.relative(baseDir, fullPath);
|
|
68
|
+
if (fs_1.default.statSync(fullPath).isDirectory()) {
|
|
69
|
+
if (item !== 'node_modules' && item !== '.git' && item !== 'dist') {
|
|
70
|
+
collectFiles(fullPath, baseDir);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Apenas arquivos de texto/código (simplificado)
|
|
75
|
+
files[relativePath] = fs_1.default.readFileSync(fullPath, 'utf-8');
|
|
76
|
+
}
|
|
78
77
|
}
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
};
|
|
79
|
+
if (fs_1.default.existsSync(srcDir))
|
|
80
|
+
collectFiles(srcDir, currentDir);
|
|
81
|
+
if (fs_1.default.existsSync(packageJsonPath))
|
|
82
|
+
files['package.json'] = fs_1.default.readFileSync(packageJsonPath, 'utf-8');
|
|
83
|
+
const tsConfigPath = path_1.default.join(currentDir, 'tsconfig.json');
|
|
84
|
+
if (fs_1.default.existsSync(tsConfigPath))
|
|
85
|
+
files['tsconfig.json'] = fs_1.default.readFileSync(tsConfigPath, 'utf-8');
|
|
86
|
+
// 3. Commit local pra manter o histórico do usuário (se houver git)
|
|
87
|
+
if (hasGit) {
|
|
88
|
+
await git.add('.');
|
|
89
|
+
const status = await git.status();
|
|
90
|
+
if (status.staged.length > 0) {
|
|
91
|
+
await git.commit(`feat: submissão estágio ${stageOrder}`);
|
|
81
92
|
}
|
|
82
93
|
}
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
// 4. Envio via API POST
|
|
95
|
+
console.log(chalk_1.default.gray('Enviando payload para o servidor DevQuest...'));
|
|
96
|
+
const subResponse = await axios_1.default.post(`${env_1.API_BASE}/challenges/${challengeId}/submit`, {
|
|
97
|
+
files,
|
|
98
|
+
stageOrder
|
|
99
|
+
}, {
|
|
100
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
101
|
+
});
|
|
102
|
+
if (!subResponse.data.success) {
|
|
103
|
+
throw new Error(subResponse.data.error || 'Erro desconhecido na API');
|
|
104
|
+
}
|
|
105
|
+
console.log(chalk_1.default.green(`🚀 Código recebido! (Submissão: ${subResponse.data.submissionId})`));
|
|
106
|
+
console.log(chalk_1.default.cyan('📡 Conectando à Sandbox remota para avaliação final...'));
|
|
107
|
+
// 5. SSE Logs para simular execução
|
|
85
108
|
startStreamingLogs(token, currentDir, challengeId, stageOrder);
|
|
86
109
|
}
|
|
87
110
|
catch (err) {
|
|
88
111
|
console.error(chalk_1.default.red('\n❌ Falha ao enviar a submissão.'));
|
|
89
|
-
|
|
112
|
+
if (err.response) {
|
|
113
|
+
console.error(chalk_1.default.red(`API: ${err.response.data.error || err.response.statusText}`));
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
console.error(chalk_1.default.red(err.message));
|
|
117
|
+
}
|
|
90
118
|
}
|
|
91
119
|
});
|
|
92
120
|
async function startStreamingLogs(token, challengeDir, challengeId, stageOrder) {
|
|
93
121
|
const url = `${env_1.SSE_BASE}?token=${encodeURIComponent(token)}&challengeId=${encodeURIComponent(challengeId)}&stageOrder=${stageOrder}`;
|
|
94
|
-
console.log(chalk_1.default.cyan('📡 Conectando à Sandbox segurizada (MicroVM Firecracker)...'));
|
|
95
122
|
try {
|
|
96
123
|
const response = await fetch(url);
|
|
97
124
|
if (!response.body) {
|
|
@@ -105,7 +132,7 @@ async function startStreamingLogs(token, challengeDir, challengeId, stageOrder)
|
|
|
105
132
|
if (done)
|
|
106
133
|
break;
|
|
107
134
|
const chunk = decoder.decode(value, { stream: true });
|
|
108
|
-
const lines = chunk.split('
|
|
135
|
+
const lines = chunk.split('\n');
|
|
109
136
|
for (const line of lines) {
|
|
110
137
|
if (line.startsWith('data: ')) {
|
|
111
138
|
const dataStr = line.slice(6);
|
|
@@ -122,20 +149,17 @@ async function startStreamingLogs(token, challengeDir, challengeId, stageOrder)
|
|
|
122
149
|
console.log(chalk_1.default.red('❌ ' + payload.message));
|
|
123
150
|
break;
|
|
124
151
|
case 'FINISHED':
|
|
125
|
-
console.log(chalk_1.default.green.bold('
|
|
152
|
+
console.log(chalk_1.default.green.bold('\n🎉 Parabéns! Você completou este estágio.'));
|
|
126
153
|
process.exit(0);
|
|
127
154
|
case 'ERROR':
|
|
128
|
-
console.log(chalk_1.default.red.bold('
|
|
155
|
+
console.log(chalk_1.default.red.bold('\n💥 Erro na execução: ' + payload.message));
|
|
129
156
|
process.exit(1);
|
|
130
157
|
}
|
|
131
158
|
}
|
|
132
|
-
catch (e) {
|
|
133
|
-
// ignores bad json fragments
|
|
134
|
-
}
|
|
159
|
+
catch (e) { }
|
|
135
160
|
}
|
|
136
161
|
}
|
|
137
162
|
}
|
|
138
|
-
console.log(chalk_1.default.gray('Conexão encerrada pelo servidor.'));
|
|
139
163
|
}
|
|
140
164
|
catch (err) {
|
|
141
165
|
console.error(chalk_1.default.red('Erro na escuta da MicroVM: ' + err.message));
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runStage = runStage;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
/**
|
|
12
|
+
* Suite de testes para o desafio Build Your Own Formatter
|
|
13
|
+
* Estágio 1: Scanner / Lexer
|
|
14
|
+
*/
|
|
15
|
+
async function runStage(stage, cwd) {
|
|
16
|
+
console.log(chalk_1.default.blue(`\n[Test Suite] Avaliando Estágio ${stage} para build-your-own-formatter...`));
|
|
17
|
+
if (stage === 1) {
|
|
18
|
+
return await testStage1(cwd);
|
|
19
|
+
}
|
|
20
|
+
console.log(chalk_1.default.yellow(`[!] Nenhum teste definido para o estágio ${stage}.`));
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
async function testStage1(cwd) {
|
|
24
|
+
console.log(chalk_1.default.gray(`[Lexer Test] Validando saída do scanner para tokens básicos...`));
|
|
25
|
+
// Cria um arquivo de teste temporário para o formatador ler
|
|
26
|
+
const testFilePath = path_1.default.join(cwd, 'test_input.ts');
|
|
27
|
+
const testContent = `const x = 10;`;
|
|
28
|
+
fs_1.default.writeFileSync(testFilePath, testContent);
|
|
29
|
+
try {
|
|
30
|
+
// Executa o código do usuário: node dist/index.js test_input.ts
|
|
31
|
+
// Assumindo que o usuário usa a estrutura padrão: ts-node ou dist/index.js
|
|
32
|
+
const entryPoint = fs_1.default.existsSync(path_1.default.join(cwd, 'dist/index.js'))
|
|
33
|
+
? 'dist/index.js'
|
|
34
|
+
: 'src/index.ts';
|
|
35
|
+
const cmd = entryPoint.endsWith('.ts') ? 'npx ts-node' : 'node';
|
|
36
|
+
const result = (0, child_process_1.spawnSync)(`${cmd} ${entryPoint} ${testFilePath}`, {
|
|
37
|
+
shell: true,
|
|
38
|
+
cwd,
|
|
39
|
+
encoding: 'utf-8'
|
|
40
|
+
});
|
|
41
|
+
if (result.status !== 0) {
|
|
42
|
+
console.log(chalk_1.default.red(`❌ Erro ao executar o código: ${result.stderr}`));
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const output = result.stdout;
|
|
46
|
+
console.log(chalk_1.default.gray(`Saída do usuário:\n${output}`));
|
|
47
|
+
// Validação rigorosa: O Estágio 1 espera que o usuário imprima uma lista de tokens em JSON
|
|
48
|
+
try {
|
|
49
|
+
const tokens = JSON.parse(output.trim());
|
|
50
|
+
const hasConst = tokens.some((t) => t.type === 'KEYWORD' && t.value === 'const');
|
|
51
|
+
const hasIdentifier = tokens.some((t) => t.type === 'IDENTIFIER' && t.value === 'x');
|
|
52
|
+
const hasLiteral = tokens.some((t) => t.type === 'LITERAL' && t.value === '10');
|
|
53
|
+
if (hasConst && hasIdentifier && hasLiteral) {
|
|
54
|
+
console.log(chalk_1.default.green(`✅ Scanner identificou os tokens (const, x, 10) corretamente no formato JSON.`));
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.log(chalk_1.default.red(`❌ Scanner produziu JSON, mas os tokens esperados (const, x, 10) não foram encontrados ou estão no formato errado.`));
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
console.log(chalk_1.default.red(`❌ Erro de Formato: O Estágio 1 exige que o Scanner imprima a lista de tokens em formato JSON válido.`));
|
|
64
|
+
console.log(chalk_1.default.yellow(`Exemplo esperado: [{"type": "KEYWORD", "value": "const"}, ...]`));
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
console.log(chalk_1.default.red(`❌ Falha técnica no teste: ${e.message}`));
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
if (fs_1.default.existsSync(testFilePath))
|
|
74
|
+
fs_1.default.unlinkSync(testFilePath);
|
|
75
|
+
}
|
|
76
|
+
}
|