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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/THIRD_PARTY_LICENSES +21 -0
  4. package/dist/atcoder/client.d.ts +2 -0
  5. package/dist/atcoder/client.js +23 -0
  6. package/dist/atcoder/new.d.ts +15 -0
  7. package/dist/atcoder/new.js +123 -0
  8. package/dist/atcoder/parser/contest-tasks.d.ts +6 -0
  9. package/dist/atcoder/parser/contest-tasks.js +86 -0
  10. package/dist/atcoder/parser/limits.d.ts +10 -0
  11. package/dist/atcoder/parser/limits.js +71 -0
  12. package/dist/atcoder/parser/problem-page.d.ts +12 -0
  13. package/dist/atcoder/parser/problem-page.js +136 -0
  14. package/dist/atcoder/parser/submission-status.d.ts +8 -0
  15. package/dist/atcoder/parser/submission-status.js +78 -0
  16. package/dist/atcoder/submit.d.ts +9 -0
  17. package/dist/atcoder/submit.js +182 -0
  18. package/dist/cli.d.ts +2 -0
  19. package/dist/cli.js +508 -0
  20. package/dist/config/config-store.d.ts +17 -0
  21. package/dist/config/config-store.js +92 -0
  22. package/dist/session/auth.d.ts +8 -0
  23. package/dist/session/auth.js +117 -0
  24. package/dist/session/store.d.ts +15 -0
  25. package/dist/session/store.js +75 -0
  26. package/dist/test-runner/diff.d.ts +7 -0
  27. package/dist/test-runner/diff.js +32 -0
  28. package/dist/test-runner/runner.d.ts +46 -0
  29. package/dist/test-runner/runner.js +274 -0
  30. package/dist/utils/errors.d.ts +15 -0
  31. package/dist/utils/errors.js +35 -0
  32. package/dist/utils/format.d.ts +9 -0
  33. package/dist/utils/format.js +89 -0
  34. package/dist/utils/i18n.d.ts +345 -0
  35. package/dist/utils/i18n.js +413 -0
  36. package/dist/utils/open.d.ts +8 -0
  37. package/dist/utils/open.js +50 -0
  38. package/dist/workspace/finder.d.ts +9 -0
  39. package/dist/workspace/finder.js +62 -0
  40. package/dist/workspace/initializer.d.ts +4 -0
  41. package/dist/workspace/initializer.js +109 -0
  42. package/package.json +38 -0
  43. package/src/atcoder/client.ts +21 -0
  44. package/src/atcoder/new.ts +107 -0
  45. package/src/atcoder/parser/contest-tasks.test.ts +37 -0
  46. package/src/atcoder/parser/contest-tasks.ts +61 -0
  47. package/src/atcoder/parser/limits.test.ts +52 -0
  48. package/src/atcoder/parser/limits.ts +75 -0
  49. package/src/atcoder/parser/problem-page.test.ts +68 -0
  50. package/src/atcoder/parser/problem-page.ts +126 -0
  51. package/src/atcoder/parser/submission-status.test.ts +36 -0
  52. package/src/atcoder/parser/submission-status.ts +54 -0
  53. package/src/atcoder/submit.ts +170 -0
  54. package/src/cli.ts +554 -0
  55. package/src/config/config-store.ts +72 -0
  56. package/src/session/auth.ts +87 -0
  57. package/src/session/store.ts +50 -0
  58. package/src/test-runner/diff.test.ts +26 -0
  59. package/src/test-runner/diff.ts +42 -0
  60. package/src/test-runner/runner.test.ts +70 -0
  61. package/src/test-runner/runner.ts +315 -0
  62. package/src/utils/errors.ts +31 -0
  63. package/src/utils/format.test.ts +69 -0
  64. package/src/utils/format.ts +95 -0
  65. package/src/utils/i18n.test.ts +74 -0
  66. package/src/utils/i18n.ts +418 -0
  67. package/src/utils/open.ts +47 -0
  68. package/src/workspace/finder.ts +29 -0
  69. package/src/workspace/initializer.ts +85 -0
  70. package/tsconfig.json +16 -0
@@ -0,0 +1,170 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import * as cheerio from 'cheerio';
4
+ import { createAtCoderClient } from './client';
5
+ import { loadConfig } from '../config/config-store';
6
+ import { detectCodeFile } from '../test-runner/runner';
7
+ import { AtcError } from '../utils/errors';
8
+ import { getLanguage, t } from '../utils/i18n';
9
+
10
+ export interface SubmissionDetails {
11
+ submissionId: string;
12
+ url: string;
13
+ }
14
+
15
+ /**
16
+ * Submits the code file for a task to AtCoder.
17
+ * Returns the submission ID and submission URL.
18
+ */
19
+ export async function submitTask(
20
+ workspaceRoot: string,
21
+ contestId: string,
22
+ taskId: string,
23
+ taskLabel: string,
24
+ fileArg?: string
25
+ ): Promise<SubmissionDetails> {
26
+ const config = loadConfig(workspaceRoot);
27
+ const lang = getLanguage(workspaceRoot);
28
+ const contestParentDir = config.contestDir ? path.join(workspaceRoot, config.contestDir) : workspaceRoot;
29
+ const taskDir = path.join(contestParentDir, contestId, taskLabel);
30
+
31
+ if (!fs.existsSync(taskDir)) {
32
+ throw new AtcError(`Task directory "${taskLabel}" not found in contest "${contestId}".`);
33
+ }
34
+
35
+ const { codeFile, langConfig } = detectCodeFile(workspaceRoot, taskDir, config, fileArg);
36
+ const codePath = path.join(taskDir, codeFile);
37
+ const codeContent = fs.readFileSync(codePath, 'utf8');
38
+
39
+ const client = createAtCoderClient(workspaceRoot);
40
+
41
+ let submitPageHtml = '';
42
+ try {
43
+ const res = await client.get(`/contests/${contestId}/submit?taskScreenName=${taskId}`);
44
+ submitPageHtml = res.data;
45
+ } catch (err: any) {
46
+ throw new AtcError(`Failed to access AtCoder submit page: ${err.message}`);
47
+ }
48
+
49
+ const $ = cheerio.load(submitPageHtml);
50
+
51
+ const csrfToken = $('input[name="csrf_token"]').val();
52
+ if (!csrfToken) {
53
+ throw new AtcError('Could not find CSRF token. Make sure you are logged in (run "atc login").');
54
+ }
55
+
56
+ let selectElement = $(`#select-lang-${taskId} select`);
57
+ if (selectElement.length === 0) {
58
+ selectElement = $('div#select-lang select');
59
+ }
60
+ if (selectElement.length === 0) {
61
+ selectElement = $('select[name="data.LanguageId"]');
62
+ }
63
+
64
+ if (selectElement.length === 0) {
65
+ if ($('input[name="username"]').length > 0 || $('input[name="password"]').length > 0 || submitPageHtml.includes('/login')) {
66
+ throw new AtcError(t('submitSessionExpired', lang));
67
+ }
68
+ throw new AtcError(t('submitLangSelectNotFound', lang));
69
+ }
70
+
71
+ const options: { id: string; name: string }[] = [];
72
+ selectElement.find('option').each((_, opt) => {
73
+ const id = $(opt).val();
74
+ const name = $(opt).text().trim();
75
+ if (id && name) {
76
+ options.push({ id: id.toString(), name });
77
+ }
78
+ });
79
+
80
+ let selectedLangId = '';
81
+ // Custom user regex in language config
82
+ const userRegexStr = (langConfig as any).atcoderLanguageIdRegex;
83
+ if (userRegexStr) {
84
+ const userRegex = new RegExp(userRegexStr, 'i');
85
+ const matched = options.find(opt => userRegex.test(opt.name));
86
+ if (matched) {
87
+ selectedLangId = matched.id;
88
+ }
89
+ }
90
+
91
+ if (!selectedLangId) {
92
+ if (langConfig.extension === 'cpp') {
93
+ const matched = options.find(opt => /C\+\+/i.test(opt.name) && !/Clang/i.test(opt.name)) ||
94
+ options.find(opt => /C\+\+/i.test(opt.name));
95
+ if (matched) selectedLangId = matched.id;
96
+ } else if (langConfig.extension === 'py') {
97
+ const matched = options.find(opt => /PyPy3/i.test(opt.name)) ||
98
+ options.find(opt => /Python3/i.test(opt.name)) ||
99
+ options.find(opt => /Python/i.test(opt.name));
100
+ if (matched) selectedLangId = matched.id;
101
+ }
102
+ }
103
+
104
+ if (!selectedLangId) {
105
+ const extRegex = new RegExp(langConfig.extension, 'i');
106
+ const matched = options.find(opt => extRegex.test(opt.name));
107
+ if (matched) {
108
+ selectedLangId = matched.id;
109
+ } else if (options.length > 0) {
110
+ selectedLangId = options[0].id;
111
+ } else {
112
+ throw new AtcError('No language options available on AtCoder submit page.');
113
+ }
114
+ }
115
+
116
+ const postData = new URLSearchParams();
117
+ postData.append('csrf_token', csrfToken.toString());
118
+ postData.append('data.TaskScreenName', taskId);
119
+ postData.append('data.LanguageId', selectedLangId);
120
+ postData.append('sourceCode', codeContent);
121
+
122
+ try {
123
+ const postRes = await client.post(`/contests/${contestId}/submit`, postData.toString(), {
124
+ headers: {
125
+ 'Content-Type': 'application/x-www-form-urlencoded',
126
+ 'Referer': `https://atcoder.jp/contests/${contestId}/submit?taskScreenName=${taskId}`
127
+ },
128
+ maxRedirects: 0,
129
+ validateStatus: (status) => status >= 200 && status < 400
130
+ });
131
+
132
+ if (postRes.status !== 302) {
133
+ const $post = cheerio.load(postRes.data);
134
+ let alertText = $post('.alert-danger, .alert-warning').text().trim();
135
+ if (!alertText) {
136
+ if ($post('.cf-challenge').length > 0) {
137
+ throw new AtcError(t('submitTurnstileDetected', lang));
138
+ }
139
+ throw new AtcError(t('submitRejected', lang));
140
+ }
141
+ alertText = alertText.replace(/^×\s*/, '').trim();
142
+ throw new AtcError(alertText);
143
+ }
144
+
145
+ const meRes = await client.get(`/contests/${contestId}/submissions/me`);
146
+ const $me = cheerio.load(meRes.data);
147
+
148
+ let submissionId = '';
149
+ $me('table tbody tr').first().find('a').each((_, a) => {
150
+ const href = $me(a).attr('href');
151
+ if (href) {
152
+ const match = href.match(/\/contests\/[^/]+\/submissions\/(\d+)/);
153
+ if (match && match[1]) {
154
+ submissionId = match[1];
155
+ }
156
+ }
157
+ });
158
+
159
+ if (!submissionId) {
160
+ throw new AtcError('Submission succeeded but could not retrieve the submission ID.');
161
+ }
162
+
163
+ return {
164
+ submissionId,
165
+ url: `/contests/${contestId}/submissions/${submissionId}`
166
+ };
167
+ } catch (err: any) {
168
+ throw new AtcError(`Failed to submit code: ${err.message}`);
169
+ }
170
+ }