atcoder-workspace 1.1.0-beta.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 -0
- package/README.md +98 -0
- package/THIRD_PARTY_LICENSES +21 -0
- package/dist/atcoder/client.d.ts +2 -0
- package/dist/atcoder/client.js +23 -0
- package/dist/atcoder/new.d.ts +15 -0
- package/dist/atcoder/new.js +123 -0
- package/dist/atcoder/parser/contest-tasks.d.ts +6 -0
- package/dist/atcoder/parser/contest-tasks.js +86 -0
- package/dist/atcoder/parser/limits.d.ts +10 -0
- package/dist/atcoder/parser/limits.js +71 -0
- package/dist/atcoder/parser/problem-page.d.ts +12 -0
- package/dist/atcoder/parser/problem-page.js +136 -0
- package/dist/atcoder/parser/submission-status.d.ts +8 -0
- package/dist/atcoder/parser/submission-status.js +78 -0
- package/dist/atcoder/submit.d.ts +9 -0
- package/dist/atcoder/submit.js +182 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +508 -0
- package/dist/config/config-store.d.ts +17 -0
- package/dist/config/config-store.js +92 -0
- package/dist/session/auth.d.ts +8 -0
- package/dist/session/auth.js +117 -0
- package/dist/session/store.d.ts +15 -0
- package/dist/session/store.js +75 -0
- package/dist/test-runner/diff.d.ts +7 -0
- package/dist/test-runner/diff.js +32 -0
- package/dist/test-runner/runner.d.ts +46 -0
- package/dist/test-runner/runner.js +274 -0
- package/dist/utils/errors.d.ts +15 -0
- package/dist/utils/errors.js +35 -0
- package/dist/utils/format.d.ts +9 -0
- package/dist/utils/format.js +89 -0
- package/dist/utils/i18n.d.ts +345 -0
- package/dist/utils/i18n.js +413 -0
- package/dist/utils/open.d.ts +8 -0
- package/dist/utils/open.js +50 -0
- package/dist/workspace/finder.d.ts +9 -0
- package/dist/workspace/finder.js +62 -0
- package/dist/workspace/initializer.d.ts +4 -0
- package/dist/workspace/initializer.js +109 -0
- package/package.json +38 -0
- package/src/atcoder/client.ts +21 -0
- package/src/atcoder/new.ts +107 -0
- package/src/atcoder/parser/contest-tasks.test.ts +37 -0
- package/src/atcoder/parser/contest-tasks.ts +61 -0
- package/src/atcoder/parser/limits.test.ts +52 -0
- package/src/atcoder/parser/limits.ts +75 -0
- package/src/atcoder/parser/problem-page.test.ts +68 -0
- package/src/atcoder/parser/problem-page.ts +126 -0
- package/src/atcoder/parser/submission-status.test.ts +36 -0
- package/src/atcoder/parser/submission-status.ts +54 -0
- package/src/atcoder/submit.ts +170 -0
- package/src/cli.ts +554 -0
- package/src/config/config-store.ts +72 -0
- package/src/session/auth.ts +87 -0
- package/src/session/store.ts +50 -0
- package/src/test-runner/diff.test.ts +26 -0
- package/src/test-runner/diff.ts +42 -0
- package/src/test-runner/runner.test.ts +70 -0
- package/src/test-runner/runner.ts +315 -0
- package/src/utils/errors.ts +31 -0
- package/src/utils/format.test.ts +69 -0
- package/src/utils/format.ts +95 -0
- package/src/utils/i18n.test.ts +74 -0
- package/src/utils/i18n.ts +418 -0
- package/src/utils/open.ts +47 -0
- package/src/workspace/finder.ts +29 -0
- package/src/workspace/initializer.ts +85 -0
- package/tsconfig.json +16 -0
package/src/cli.ts
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import * as p from '@clack/prompts';
|
|
5
|
+
import pc from 'picocolors';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
|
|
10
|
+
import { findWorkspaceRoot } from './workspace/finder';
|
|
11
|
+
import { initWorkspace } from './workspace/initializer';
|
|
12
|
+
import { loginWithCookie, whoami } from './session/auth';
|
|
13
|
+
import { clearSession } from './session/store';
|
|
14
|
+
import { fetchContestTasks, setupTask } from './atcoder/new';
|
|
15
|
+
import { loadConfig, saveConfig } from './config/config-store';
|
|
16
|
+
import { runAllTests, resolveTaskDirectory, detectCodeFile } from './test-runner/runner';
|
|
17
|
+
import { submitTask } from './atcoder/submit';
|
|
18
|
+
import { createAtCoderClient } from './atcoder/client';
|
|
19
|
+
import { parseSubmissionStatus } from './atcoder/parser/submission-status';
|
|
20
|
+
import { parseProblemPage } from './atcoder/parser/problem-page';
|
|
21
|
+
import { AtcError, WorkspaceNotFoundError } from './utils/errors';
|
|
22
|
+
import { formatOutputLines, formatErrorOutputLines } from './utils/format';
|
|
23
|
+
import { getLanguage, t } from './utils/i18n';
|
|
24
|
+
import { openUrl, copyToClipboard } from './utils/open';
|
|
25
|
+
|
|
26
|
+
const workspaceRoot = (() => {
|
|
27
|
+
try {
|
|
28
|
+
return findWorkspaceRoot();
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
33
|
+
const lang = getLanguage(workspaceRoot);
|
|
34
|
+
|
|
35
|
+
const program = new Command();
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.name('atc')
|
|
39
|
+
.description('AtCoder All-in-One CLI (Local-first)')
|
|
40
|
+
.version('1.1.0-betas');
|
|
41
|
+
|
|
42
|
+
function handleAction(fn: (...args: any[]) => Promise<void>) {
|
|
43
|
+
return async (...args: any[]) => {
|
|
44
|
+
try {
|
|
45
|
+
await fn(...args);
|
|
46
|
+
} catch (err: any) {
|
|
47
|
+
let errMsg = err.message || 'An unexpected error occurred.';
|
|
48
|
+
if (err instanceof WorkspaceNotFoundError) {
|
|
49
|
+
errMsg = t('workspaceNotFound', lang);
|
|
50
|
+
}
|
|
51
|
+
p.log.error(pc.red(errMsg));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
program
|
|
58
|
+
.command('init')
|
|
59
|
+
.description(t('descInit', lang))
|
|
60
|
+
.action(
|
|
61
|
+
handleAction(async () => {
|
|
62
|
+
p.intro(pc.cyan(t('initIntro', lang)));
|
|
63
|
+
|
|
64
|
+
const defaultLanguage = await p.select({
|
|
65
|
+
message: t('initSelectLang', lang),
|
|
66
|
+
options: [
|
|
67
|
+
{ value: 'cpp', label: 'C++ (cpp)' },
|
|
68
|
+
{ value: 'python', label: 'Python (python)' }
|
|
69
|
+
]
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (p.isCancel(defaultLanguage)) {
|
|
73
|
+
p.cancel(t('initCancelled', lang));
|
|
74
|
+
process.exit(0);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const targetDir = process.cwd();
|
|
78
|
+
const s = p.spinner();
|
|
79
|
+
s.start(t('initSpinner', lang));
|
|
80
|
+
|
|
81
|
+
const { alreadyInitialized, gitignoreUpdated } = initWorkspace(targetDir, defaultLanguage as string);
|
|
82
|
+
|
|
83
|
+
s.stop(t('initFilesSet', lang));
|
|
84
|
+
|
|
85
|
+
if (alreadyInitialized) {
|
|
86
|
+
p.log.warn(t('initAlreadyInitialized', lang));
|
|
87
|
+
} else {
|
|
88
|
+
p.log.success(t('initCreatedConfig', lang, defaultLanguage));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (gitignoreUpdated) {
|
|
92
|
+
p.log.success(t('initGitignoreUpdated', lang));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
p.outro(pc.green(t('initOutro', lang)));
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
program
|
|
100
|
+
.command('login')
|
|
101
|
+
.description(t('descLogin', lang))
|
|
102
|
+
.action(
|
|
103
|
+
handleAction(async () => {
|
|
104
|
+
p.intro(pc.cyan(t('loginIntro', lang)));
|
|
105
|
+
const workspaceRoot = findWorkspaceRoot();
|
|
106
|
+
|
|
107
|
+
p.note(t('loginNote', lang));
|
|
108
|
+
|
|
109
|
+
const username = await promptManualCookie(workspaceRoot);
|
|
110
|
+
|
|
111
|
+
p.outro(pc.green(t('loginWelcome', lang, username)));
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
async function promptManualCookie(workspaceRoot: string): Promise<string> {
|
|
116
|
+
while (true) {
|
|
117
|
+
const sessionVal = await p.text({
|
|
118
|
+
message: t('loginEnterCookie', lang),
|
|
119
|
+
placeholder: t('loginPlaceholder', lang),
|
|
120
|
+
validate: (val) => (!val.trim() ? t('loginCookieNotEmpty', lang) : undefined)
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (p.isCancel(sessionVal)) {
|
|
124
|
+
p.cancel(t('loginCancelled', lang));
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const s = p.spinner();
|
|
129
|
+
s.start(t('loginVerifying', lang));
|
|
130
|
+
try {
|
|
131
|
+
const username = await loginWithCookie(workspaceRoot, sessionVal);
|
|
132
|
+
s.stop(t('loginVerifySuccess', lang));
|
|
133
|
+
return username;
|
|
134
|
+
} catch (err: any) {
|
|
135
|
+
s.stop(t('loginVerifyFailed', lang));
|
|
136
|
+
p.log.error(pc.red(err.message));
|
|
137
|
+
|
|
138
|
+
const retry = await p.confirm({
|
|
139
|
+
message: t('loginRetryConfirm', lang),
|
|
140
|
+
initialValue: true
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (p.isCancel(retry) || !retry) {
|
|
144
|
+
p.cancel(t('loginAborted', lang));
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
program
|
|
152
|
+
.command('logout')
|
|
153
|
+
.description(t('descLogout', lang))
|
|
154
|
+
.action(
|
|
155
|
+
handleAction(async () => {
|
|
156
|
+
const workspaceRoot = findWorkspaceRoot();
|
|
157
|
+
clearSession(workspaceRoot);
|
|
158
|
+
p.log.success(t('logoutSuccess', lang));
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
program
|
|
163
|
+
.command('whoami')
|
|
164
|
+
.description(t('descWhoami', lang))
|
|
165
|
+
.action(
|
|
166
|
+
handleAction(async () => {
|
|
167
|
+
const workspaceRoot = findWorkspaceRoot();
|
|
168
|
+
const s = p.spinner();
|
|
169
|
+
s.start('Verifying session...');
|
|
170
|
+
const username = await whoami(workspaceRoot);
|
|
171
|
+
s.stop(`Logged in as: ${pc.bold(pc.cyan(username))}`);
|
|
172
|
+
})
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
program
|
|
176
|
+
.command('new <contest> [task]')
|
|
177
|
+
.description(t('descNew', lang))
|
|
178
|
+
.option('-a, --all', 'Download all tasks for the contest')
|
|
179
|
+
.action(
|
|
180
|
+
handleAction(async (contestId: string, taskLabel: string | undefined, options: { all?: boolean }) => {
|
|
181
|
+
const workspaceRoot = findWorkspaceRoot();
|
|
182
|
+
const config = loadConfig(workspaceRoot);
|
|
183
|
+
const contestParentDir = config.contestDir ? path.join(workspaceRoot, config.contestDir) : workspaceRoot;
|
|
184
|
+
|
|
185
|
+
const contestDir = path.join(contestParentDir, contestId);
|
|
186
|
+
if (fs.existsSync(contestDir)) {
|
|
187
|
+
throw new AtcError(t('newContestDirExists', lang, contestId));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
p.intro(pc.cyan(t('newIntro', lang, contestId)));
|
|
191
|
+
|
|
192
|
+
const s = p.spinner();
|
|
193
|
+
s.start(t('newFetchingTasks', lang, contestId));
|
|
194
|
+
const tasks = await fetchContestTasks(workspaceRoot, contestId);
|
|
195
|
+
s.stop(t('newFoundTasks', lang, tasks.length));
|
|
196
|
+
|
|
197
|
+
if (tasks.length === 0) {
|
|
198
|
+
throw new AtcError(t('newNoTasksFound', lang, contestId));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let selectedTasks = tasks;
|
|
202
|
+
|
|
203
|
+
if (options.all) {
|
|
204
|
+
selectedTasks = tasks;
|
|
205
|
+
} else if (taskLabel) {
|
|
206
|
+
const matched = tasks.find(t => t.label.toLowerCase() === taskLabel.toLowerCase());
|
|
207
|
+
if (!matched) {
|
|
208
|
+
throw new AtcError(t('newLabelNotFound', lang, taskLabel, contestId, tasks.map(t => t.label).join(', ')));
|
|
209
|
+
}
|
|
210
|
+
selectedTasks = [matched];
|
|
211
|
+
} else {
|
|
212
|
+
const taskOptions = tasks.map(t => ({
|
|
213
|
+
value: t.id,
|
|
214
|
+
label: `${t.label.toUpperCase()} - ${t.id}`
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
const selection = await p.multiselect({
|
|
218
|
+
message: t('newMultiselectMessage', lang),
|
|
219
|
+
options: taskOptions,
|
|
220
|
+
initialValues: tasks.map(t => t.id),
|
|
221
|
+
required: true
|
|
222
|
+
}) as string[];
|
|
223
|
+
|
|
224
|
+
if (p.isCancel(selection)) {
|
|
225
|
+
p.cancel(t('newCancelled', lang));
|
|
226
|
+
process.exit(0);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
selectedTasks = tasks.filter(t => selection.includes(t.id));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const setupSpinner = p.spinner();
|
|
233
|
+
for (let i = 0; i < selectedTasks.length; i++) {
|
|
234
|
+
const tObj = selectedTasks[i];
|
|
235
|
+
|
|
236
|
+
if (i > 0) {
|
|
237
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
setupSpinner.start(t('newSettingUpTask', lang, tObj.label.toUpperCase(), tObj.id));
|
|
241
|
+
const res = await setupTask(workspaceRoot, contestId, tObj);
|
|
242
|
+
setupSpinner.stop(t('newSetupSuccess', lang, tObj.label.toUpperCase(), res.sampleCount));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
p.outro(pc.green(t('newScaffoldingComplete', lang, selectedTasks.length)));
|
|
246
|
+
})
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
function resolveArgs(workspaceRoot: string, taskArg: string | undefined, fileArg: string | undefined) {
|
|
250
|
+
let resolvedTaskDir = '';
|
|
251
|
+
let resolvedFile: string | undefined;
|
|
252
|
+
|
|
253
|
+
let isFile = false;
|
|
254
|
+
let filePath = '';
|
|
255
|
+
|
|
256
|
+
if (taskArg) {
|
|
257
|
+
const pathsToCheck = [
|
|
258
|
+
path.resolve(taskArg),
|
|
259
|
+
path.resolve(workspaceRoot, taskArg)
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
const config = loadConfig(workspaceRoot);
|
|
263
|
+
if (config.contestDir) {
|
|
264
|
+
pathsToCheck.push(path.resolve(workspaceRoot, config.contestDir, taskArg));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const p of pathsToCheck) {
|
|
268
|
+
if (fs.existsSync(p) && fs.statSync(p).isFile()) {
|
|
269
|
+
isFile = true;
|
|
270
|
+
filePath = p;
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (isFile) {
|
|
277
|
+
resolvedFile = path.basename(filePath);
|
|
278
|
+
resolvedTaskDir = path.dirname(filePath);
|
|
279
|
+
} else {
|
|
280
|
+
resolvedTaskDir = resolveTaskDirectory(workspaceRoot, taskArg);
|
|
281
|
+
resolvedFile = fileArg;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const taskLabel = path.basename(resolvedTaskDir);
|
|
285
|
+
const contestId = path.basename(path.dirname(resolvedTaskDir));
|
|
286
|
+
|
|
287
|
+
return { resolvedTaskDir, resolvedFile, taskLabel, contestId };
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
program
|
|
291
|
+
.command('test [task] [file]')
|
|
292
|
+
.description(t('descTest', lang))
|
|
293
|
+
.action(
|
|
294
|
+
handleAction(async (taskArg: string | undefined, fileArg: string | undefined) => {
|
|
295
|
+
const workspaceRoot = findWorkspaceRoot();
|
|
296
|
+
const { resolvedTaskDir, resolvedFile, taskLabel, contestId } = resolveArgs(workspaceRoot, taskArg, fileArg);
|
|
297
|
+
|
|
298
|
+
p.intro(pc.cyan(t('testIntro', lang, contestId, taskLabel)));
|
|
299
|
+
|
|
300
|
+
const s = p.spinner();
|
|
301
|
+
s.start(t('testRetrievingLimits', lang));
|
|
302
|
+
let timeLimitMs = 2000;
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const client = createAtCoderClient(workspaceRoot);
|
|
306
|
+
const tasks = await fetchContestTasks(workspaceRoot, contestId);
|
|
307
|
+
const taskInfo = tasks.find(t => t.label.toLowerCase() === taskLabel.toLowerCase());
|
|
308
|
+
|
|
309
|
+
if (taskInfo) {
|
|
310
|
+
const res = await client.get(`/contests/${contestId}/tasks/${taskInfo.id}`);
|
|
311
|
+
const details = parseProblemPage(res.data);
|
|
312
|
+
timeLimitMs = details.timeLimitMs;
|
|
313
|
+
s.stop(t('testLoadedLimits', lang, timeLimitMs));
|
|
314
|
+
} else {
|
|
315
|
+
s.stop(t('testDefaultLimits', lang));
|
|
316
|
+
}
|
|
317
|
+
} catch (err) {
|
|
318
|
+
s.stop(t('testDefaultLimitsError', lang));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const testSpinner = p.spinner();
|
|
322
|
+
testSpinner.start(t('testCompilingRunning', lang));
|
|
323
|
+
const testRes = await runAllTests(workspaceRoot, resolvedTaskDir, resolvedFile, timeLimitMs);
|
|
324
|
+
testSpinner.stop(t('testFinished', lang));
|
|
325
|
+
|
|
326
|
+
if (testRes.compileError) {
|
|
327
|
+
p.log.error(pc.red(t('testCompilationFailed', lang)));
|
|
328
|
+
console.log(testRes.compileError);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (testRes.results.length === 0) {
|
|
333
|
+
p.log.warn(t('testNoSamples', lang));
|
|
334
|
+
process.exit(0);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let allPassed = true;
|
|
338
|
+
for (const res of testRes.results) {
|
|
339
|
+
const label = `sample-${res.index}`;
|
|
340
|
+
const duration = `${res.durationMs.toFixed(0)} ms`;
|
|
341
|
+
|
|
342
|
+
if (res.status === 'AC') {
|
|
343
|
+
p.log.success(`${pc.green(pc.bold('[AC]'))} ${label}: Passed (${duration})`);
|
|
344
|
+
} else if (res.status === 'WA') {
|
|
345
|
+
allPassed = false;
|
|
346
|
+
p.log.error(`${pc.red(pc.bold('[WA]'))} ${label}: Failed (${duration})`);
|
|
347
|
+
console.log(` ${pc.gray('┌────────────────────────────────────────────────────────')}`);
|
|
348
|
+
console.log(` ${pc.gray('│')} ${pc.bold('Expected Output:')}`);
|
|
349
|
+
formatOutputLines(res.expectedOutput, res.firstDiffLine).forEach(l => console.log(l));
|
|
350
|
+
console.log(` ${pc.gray('├────────────────────────────────────────────────────────')}`);
|
|
351
|
+
console.log(` ${pc.gray('│')} ${pc.bold('Actual Output:')}`);
|
|
352
|
+
formatOutputLines(res.actualOutput, res.firstDiffLine).forEach(l => console.log(l));
|
|
353
|
+
if (res.firstDiffLine) {
|
|
354
|
+
console.log(` ${pc.gray('├────────────────────────────────────────────────────────')}`);
|
|
355
|
+
console.log(` ${pc.gray('│')} ${pc.yellow(`First mismatch on line ${res.firstDiffLine}`)}`);
|
|
356
|
+
}
|
|
357
|
+
console.log(` ${pc.gray('└────────────────────────────────────────────────────────')}`);
|
|
358
|
+
} else if (res.status === 'TLE') {
|
|
359
|
+
allPassed = false;
|
|
360
|
+
p.log.error(`${pc.red(pc.bold('[TLE]'))} ${label}: Time Limit Exceeded (${duration} vs Limit ${timeLimitMs} ms)`);
|
|
361
|
+
} else if (res.status === 'RE') {
|
|
362
|
+
allPassed = false;
|
|
363
|
+
p.log.error(`${pc.red(pc.bold('[RE]'))} ${label}: Runtime Error (${duration})`);
|
|
364
|
+
if (res.errorOutput) {
|
|
365
|
+
console.log(` ${pc.gray('┌────────────────────────────────────────────────────────')}`);
|
|
366
|
+
console.log(` ${pc.gray('│')} ${pc.bold('Error Output:')}`);
|
|
367
|
+
formatErrorOutputLines(res.errorOutput).forEach(l => console.log(l));
|
|
368
|
+
console.log(` ${pc.gray('└────────────────────────────────────────────────────────')}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
p.outro(allPassed ? pc.green(t('testOutroPassed', lang)) : pc.red(t('testOutroFailed', lang)));
|
|
374
|
+
if (!allPassed) {
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
program
|
|
381
|
+
.command('submit [task] [file]')
|
|
382
|
+
.description(t('descSubmit', lang))
|
|
383
|
+
.action(
|
|
384
|
+
handleAction(async (taskArg: string | undefined, fileArg: string | undefined) => {
|
|
385
|
+
const workspaceRoot = findWorkspaceRoot();
|
|
386
|
+
const { resolvedTaskDir, resolvedFile, taskLabel, contestId } = resolveArgs(workspaceRoot, taskArg, fileArg);
|
|
387
|
+
|
|
388
|
+
p.intro(pc.cyan(t('submitPreparing', lang, contestId, taskLabel)));
|
|
389
|
+
|
|
390
|
+
const s = p.spinner();
|
|
391
|
+
s.start(t('submitRetrievingLimits', lang));
|
|
392
|
+
let timeLimitMs = 2000;
|
|
393
|
+
let taskId = '';
|
|
394
|
+
|
|
395
|
+
const client = createAtCoderClient(workspaceRoot);
|
|
396
|
+
const tasks = await fetchContestTasks(workspaceRoot, contestId);
|
|
397
|
+
const taskInfo = tasks.find(t => t.label.toLowerCase() === taskLabel.toLowerCase());
|
|
398
|
+
|
|
399
|
+
if (!taskInfo) {
|
|
400
|
+
throw new AtcError(`Task label "${taskLabel}" not found in contest "${contestId}".`);
|
|
401
|
+
}
|
|
402
|
+
taskId = taskInfo.id;
|
|
403
|
+
|
|
404
|
+
try {
|
|
405
|
+
const res = await client.get(`/contests/${contestId}/tasks/${taskId}`);
|
|
406
|
+
const details = parseProblemPage(res.data);
|
|
407
|
+
timeLimitMs = details.timeLimitMs;
|
|
408
|
+
s.stop(t('testLoadedLimits', lang, timeLimitMs));
|
|
409
|
+
} catch (err) {
|
|
410
|
+
s.stop(t('testDefaultLimitsError', lang));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
p.log.step(t('submitRunningTests', lang));
|
|
414
|
+
const testRes = await runAllTests(workspaceRoot, resolvedTaskDir, resolvedFile, timeLimitMs);
|
|
415
|
+
|
|
416
|
+
if (testRes.compileError) {
|
|
417
|
+
p.log.error(pc.red(t('testCompilationFailed', lang)));
|
|
418
|
+
console.log(testRes.compileError);
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const allPassed = testRes.results.length > 0 && testRes.results.every(r => r.status === 'AC');
|
|
423
|
+
|
|
424
|
+
if (testRes.results.length === 0) {
|
|
425
|
+
p.log.warn(t('submitNoSamples', lang));
|
|
426
|
+
} else if (!allPassed) {
|
|
427
|
+
p.log.warn(pc.yellow(t('submitTestsFailed', lang)));
|
|
428
|
+
const confirmSubmit = await p.confirm({
|
|
429
|
+
message: t('submitConfirmMessage', lang),
|
|
430
|
+
initialValue: false
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
if (p.isCancel(confirmSubmit) || !confirmSubmit) {
|
|
434
|
+
p.cancel(t('submitAborted', lang));
|
|
435
|
+
process.exit(0);
|
|
436
|
+
}
|
|
437
|
+
} else {
|
|
438
|
+
p.log.success(pc.green(t('submitTestsPassed', lang)));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const subSpinner = p.spinner();
|
|
442
|
+
subSpinner.start(t('submitSubmitting', lang));
|
|
443
|
+
let subDetails;
|
|
444
|
+
try {
|
|
445
|
+
subDetails = await submitTask(workspaceRoot, contestId, taskId, taskLabel, resolvedFile);
|
|
446
|
+
subSpinner.stop(t('submitSuccess', lang, subDetails.submissionId));
|
|
447
|
+
openUrl(`https://atcoder.jp${subDetails.url}`);
|
|
448
|
+
} catch (err: any) {
|
|
449
|
+
subSpinner.stop(pc.yellow(t('submitManualSubmission', lang)));
|
|
450
|
+
|
|
451
|
+
let codeContent = '';
|
|
452
|
+
try {
|
|
453
|
+
const config = loadConfig(workspaceRoot);
|
|
454
|
+
const { codeFile } = detectCodeFile(workspaceRoot, resolvedTaskDir, config, resolvedFile);
|
|
455
|
+
codeContent = fs.readFileSync(path.join(resolvedTaskDir, codeFile), 'utf8');
|
|
456
|
+
} catch {
|
|
457
|
+
// Ignore
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const submitPageUrl = `https://atcoder.jp/contests/${contestId}/submit?taskScreenName=${taskId}`;
|
|
461
|
+
openUrl(submitPageUrl);
|
|
462
|
+
|
|
463
|
+
if (codeContent) {
|
|
464
|
+
await copyToClipboard(codeContent);
|
|
465
|
+
p.outro(pc.cyan(t('submitFallbackMessageWithClipboard', lang)));
|
|
466
|
+
} else {
|
|
467
|
+
p.outro(pc.cyan(t('submitFallbackMessage', lang)));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
process.exit(0);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const pollSpinner = p.spinner();
|
|
474
|
+
pollSpinner.start(t('submitWaitingJudge', lang));
|
|
475
|
+
|
|
476
|
+
const pollInterval = 2000;
|
|
477
|
+
const timeout = 300000; // 5 minutes
|
|
478
|
+
const startTime = Date.now();
|
|
479
|
+
let completed = false;
|
|
480
|
+
|
|
481
|
+
while (Date.now() - startTime < timeout) {
|
|
482
|
+
try {
|
|
483
|
+
const detailRes = await client.get(subDetails.url);
|
|
484
|
+
const status = parseSubmissionStatus(detailRes.data);
|
|
485
|
+
|
|
486
|
+
pollSpinner.message(`Judge Status: ${pc.yellow(pc.bold(status.status))}`);
|
|
487
|
+
|
|
488
|
+
if (status.isCompleted) {
|
|
489
|
+
completed = true;
|
|
490
|
+
pollSpinner.stop(t('submitJudgeFinished', lang, status.status));
|
|
491
|
+
|
|
492
|
+
const stats = [];
|
|
493
|
+
if (status.score) stats.push(`Score: ${status.score}`);
|
|
494
|
+
if (status.time) stats.push(`Time: ${status.time}`);
|
|
495
|
+
if (status.memory) stats.push(`Memory: ${status.memory}`);
|
|
496
|
+
const statsStr = stats.length > 0 ? ` (${stats.join(', ')})` : '';
|
|
497
|
+
|
|
498
|
+
if (status.status === 'AC') {
|
|
499
|
+
p.log.success(`${pc.green(pc.bold('[AC]'))} ${t('submitAccepted', lang)}${statsStr}`);
|
|
500
|
+
} else {
|
|
501
|
+
p.log.error(`${pc.red(pc.bold(`[${status.status}]`))} ${t('submitFailed', lang)}${statsStr}`);
|
|
502
|
+
}
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
} catch (e: any) {
|
|
506
|
+
// Ignore intermediate polling network errors
|
|
507
|
+
pollSpinner.message(`Polling status... (network retry: ${e.message})`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (!completed) {
|
|
514
|
+
pollSpinner.stop(t('submitTimeout', lang));
|
|
515
|
+
p.log.warn(t('submitTimeoutWarn', lang, subDetails.url));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
p.outro(pc.green('Done.'));
|
|
519
|
+
})
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
program
|
|
523
|
+
.command('lang [language]')
|
|
524
|
+
.description(t('descLang', lang))
|
|
525
|
+
.action(
|
|
526
|
+
handleAction(async (targetLanguage: string | undefined) => {
|
|
527
|
+
let workspaceRoot: string;
|
|
528
|
+
try {
|
|
529
|
+
workspaceRoot = findWorkspaceRoot();
|
|
530
|
+
} catch {
|
|
531
|
+
p.log.error(pc.red(t('langWorkspaceRequired', lang)));
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (!targetLanguage) {
|
|
536
|
+
console.log(t('langCommandUsage', lang));
|
|
537
|
+
process.exit(0);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const cleanLang = targetLanguage.trim().toLowerCase();
|
|
541
|
+
if (cleanLang !== 'en' && cleanLang !== 'ja') {
|
|
542
|
+
p.log.error(pc.red(t('langInvalid', lang)));
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const config = loadConfig(workspaceRoot);
|
|
547
|
+
config.lang = cleanLang as 'en' | 'ja';
|
|
548
|
+
saveConfig(workspaceRoot, config);
|
|
549
|
+
|
|
550
|
+
p.log.success(t('langSuccess', cleanLang as 'en' | 'ja', cleanLang));
|
|
551
|
+
})
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { findWorkspaceRoot } from '../workspace/finder';
|
|
4
|
+
|
|
5
|
+
export interface LanguageConfig {
|
|
6
|
+
extension: string;
|
|
7
|
+
templateDir: string;
|
|
8
|
+
build: string;
|
|
9
|
+
run: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Config {
|
|
13
|
+
defaultLanguage: string;
|
|
14
|
+
languages: Record<string, LanguageConfig>;
|
|
15
|
+
testDirName: string;
|
|
16
|
+
contestDir?: string;
|
|
17
|
+
lang?: 'en' | 'ja';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_CONFIG: Config = {
|
|
21
|
+
defaultLanguage: 'cpp',
|
|
22
|
+
languages: {
|
|
23
|
+
cpp: {
|
|
24
|
+
extension: 'cpp',
|
|
25
|
+
templateDir: 'templates/cpp',
|
|
26
|
+
build: 'g++ -O2 -std=gnu++20 -o a.out main.cpp',
|
|
27
|
+
run: './a.out'
|
|
28
|
+
},
|
|
29
|
+
python: {
|
|
30
|
+
extension: 'py',
|
|
31
|
+
templateDir: 'templates/python',
|
|
32
|
+
build: '',
|
|
33
|
+
run: 'python3 main.py'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
testDirName: 'tests',
|
|
37
|
+
contestDir: ''
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function getConfigPath(workspaceRoot: string): string {
|
|
41
|
+
return path.join(workspaceRoot, '.atcoder-cli', 'config.json');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function loadConfig(workspaceRoot: string): Config {
|
|
45
|
+
const configPath = getConfigPath(workspaceRoot);
|
|
46
|
+
if (!fs.existsSync(configPath)) {
|
|
47
|
+
return DEFAULT_CONFIG;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
return {
|
|
53
|
+
...DEFAULT_CONFIG,
|
|
54
|
+
...parsed,
|
|
55
|
+
languages: {
|
|
56
|
+
...DEFAULT_CONFIG.languages,
|
|
57
|
+
...(parsed.languages || {})
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
} catch (e) {
|
|
61
|
+
return DEFAULT_CONFIG;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function saveConfig(workspaceRoot: string, config: Config): void {
|
|
66
|
+
const configPath = getConfigPath(workspaceRoot);
|
|
67
|
+
const dir = path.dirname(configPath);
|
|
68
|
+
if (!fs.existsSync(dir)) {
|
|
69
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
72
|
+
}
|