atcoder-workspace 1.1.0-beta.1 → 1.1.0-beta.3

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.
@@ -15,6 +15,8 @@ export interface Config {
15
15
  testDirName: string;
16
16
  contestDir?: string;
17
17
  lang?: 'en' | 'ja';
18
+ extractProblemStatement?: boolean;
19
+ problemLang?: 'en' | 'ja';
18
20
  }
19
21
 
20
22
  export const DEFAULT_CONFIG: Config = {
@@ -34,7 +36,10 @@ export const DEFAULT_CONFIG: Config = {
34
36
  }
35
37
  },
36
38
  testDirName: 'tests',
37
- contestDir: ''
39
+ contestDir: '',
40
+ lang: 'en',
41
+ extractProblemStatement: false,
42
+ problemLang: 'ja'
38
43
  };
39
44
 
40
45
  export function getConfigPath(workspaceRoot: string): string {
@@ -52,10 +57,7 @@ export function loadConfig(workspaceRoot: string): Config {
52
57
  return {
53
58
  ...DEFAULT_CONFIG,
54
59
  ...parsed,
55
- languages: {
56
- ...DEFAULT_CONFIG.languages,
57
- ...(parsed.languages || {})
58
- }
60
+ languages: parsed.languages ? parsed.languages : DEFAULT_CONFIG.languages
59
61
  };
60
62
  } catch (e) {
61
63
  return DEFAULT_CONFIG;
@@ -54,6 +54,7 @@ describe('runner utils', () => {
54
54
 
55
55
  describe('resolveTaskDirectory', () => {
56
56
  it('should resolve task directory under configured contestDir', () => {
57
+ const cwdSpy = vi.spyOn(process, 'cwd').mockReturnValue('/workspace');
57
58
  const loadConfigSpy = vi.spyOn(configStore, 'loadConfig').mockReturnValue({
58
59
  defaultLanguage: 'cpp',
59
60
  languages: {},
@@ -64,6 +65,7 @@ describe('runner utils', () => {
64
65
  const resolved = resolveTaskDirectory('/workspace', 'abc300/a');
65
66
  expect(resolved).toContain('my-contests/abc300/a');
66
67
 
68
+ cwdSpy.mockRestore();
67
69
  loadConfigSpy.mockRestore();
68
70
  });
69
71
  });
package/src/utils/i18n.ts CHANGED
@@ -85,13 +85,17 @@ export const MESSAGES = {
85
85
  ja: 'ダウンロードしたサンプルケースに対してローカルテストを実行します'
86
86
  },
87
87
  descSubmit: {
88
- en: 'Submit code to AtCoder and poll status',
89
- ja: 'コードを AtCoder に提出し、ジャッジステータスを監視します'
88
+ en: 'Submit code to AtCoder',
89
+ ja: 'コードを AtCoder に提出します'
90
90
  },
91
91
  descLang: {
92
92
  en: 'Change the display language (en or ja)',
93
93
  ja: '表示言語の切り替え (en または ja)'
94
94
  },
95
+ descAddLang: {
96
+ en: 'Add a programming language configuration and template',
97
+ ja: 'プログラミング言語の設定とテンプレートを追加します'
98
+ },
95
99
 
96
100
  // init
97
101
  initIntro: {
@@ -244,6 +248,14 @@ export const MESSAGES = {
244
248
  en: (count: number) => `Scaffolding complete for ${count} task(s).`,
245
249
  ja: (count: number) => `${count} 個の問題のセットアップが完了しました。`
246
250
  },
251
+ newStatementWarningTitle: {
252
+ en: '[WARNING] Automatic problem statement extraction is enabled',
253
+ ja: '【警告】問題文の自動抽出が有効化されています'
254
+ },
255
+ newStatementWarningBody: {
256
+ en: '• DO NOT feed the problem statement Markdown to Generative AI during a rated contest (violates rules).\n• DO NOT publish or share the extracted problem statement on the internet (e.g. public GitHub repos).',
257
+ ja: '・コンテスト中に問題文のMarkdownを生成AIに読み込ませないでください(ルール違反となります)。\n・抽出した問題文をそのままインターネット(GitHubパブリックリポジトリ等)に公開・共有しないでください。'
258
+ },
247
259
 
248
260
  // test
249
261
  testIntro: {
@@ -288,7 +300,7 @@ export const MESSAGES = {
288
300
  },
289
301
  testOutroFailed: {
290
302
  en: 'Some tests failed. 😢',
291
- ja: '一部のテストが不合格でした。 😢'
303
+ ja: 'テストに失敗しました。 😢'
292
304
  },
293
305
 
294
306
  // submit
@@ -394,6 +406,14 @@ export const MESSAGES = {
394
406
  en: 'Usage: atc lang <en|ja>',
395
407
  ja: '使い方: atc lang <en|ja>'
396
408
  },
409
+ langSelectMessage: {
410
+ en: 'Select display language:',
411
+ ja: '表示言語を選択してください:'
412
+ },
413
+ langCancelled: {
414
+ en: 'Language selection cancelled.',
415
+ ja: '言語選択がキャンセルされました。'
416
+ },
397
417
  submitSessionExpired: {
398
418
  en: 'Session expired or invalid. Please log in again using "atc login".',
399
419
  ja: 'セッションの期限が切れているか無効です。"atc login" を実行して再ログインしてください。'
@@ -401,6 +421,50 @@ export const MESSAGES = {
401
421
  submitLangSelectNotFound: {
402
422
  en: 'Language selection element not found on submit page. Please make sure you are logged in and the contest has started.',
403
423
  ja: '提出ページに言語選択要素が見つかりませんでした。ログイン状態であること、およびコンテストが開始されていることを確認してください。'
424
+ },
425
+ addLangAlreadyExists: {
426
+ en: (lang: string) => `Language "${lang}" is already configured.`,
427
+ ja: (lang: string) => `言語 "${lang}" は既に設定されています。`
428
+ },
429
+ addLangEnterName: {
430
+ en: 'Enter the programming language name to add:',
431
+ ja: '追加するプログラミング言語名を入力してください:'
432
+ },
433
+ addLangNameNotEmpty: {
434
+ en: 'Language name cannot be empty.',
435
+ ja: '言語名は空にすることはできません。'
436
+ },
437
+ addLangCancelled: {
438
+ en: 'Language addition cancelled.',
439
+ ja: '言語の追加がキャンセルされました。'
440
+ },
441
+ addLangEnterExtension: {
442
+ en: (lang: string) => `Enter file extension for ${lang}:`,
443
+ ja: (lang: string) => `${lang} のファイル拡張子を入力してください:`
444
+ },
445
+ addLangExtNotEmpty: {
446
+ en: 'Extension cannot be empty.',
447
+ ja: '拡張子は空にすることはできません。'
448
+ },
449
+ addLangEnterBuildCmd: {
450
+ en: 'Enter build command (leave empty if not needed):',
451
+ ja: 'ビルドコマンドを入力してください (不要な場合は空欄のまま):'
452
+ },
453
+ addLangEnterRunCmd: {
454
+ en: 'Enter execution command:',
455
+ ja: '実行コマンドを入力してください:'
456
+ },
457
+ addLangRunCmdNotEmpty: {
458
+ en: 'Execution command cannot be empty.',
459
+ ja: '実行コマンドは空にすることはできません。'
460
+ },
461
+ addLangSpinner: {
462
+ en: 'Adding language configuration...',
463
+ ja: '言語設定を追加中...'
464
+ },
465
+ addLangSuccess: {
466
+ en: (lang: string) => `Language "${lang}" added successfully!`,
467
+ ja: (lang: string) => `言語 "${lang}" が正常に追加されました!`
404
468
  }
405
469
  };
406
470
 
@@ -0,0 +1,125 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { initWorkspace, addLanguage } from './initializer';
5
+ import { loadConfig } from '../config/config-store';
6
+
7
+ describe('initializer', () => {
8
+ const tempDir = path.join(__dirname, '../../test-temp-init');
9
+
10
+ beforeEach(() => {
11
+ if (fs.existsSync(tempDir)) {
12
+ fs.rmSync(tempDir, { recursive: true, force: true });
13
+ }
14
+ fs.mkdirSync(tempDir, { recursive: true });
15
+ });
16
+
17
+ afterEach(() => {
18
+ if (fs.existsSync(tempDir)) {
19
+ fs.rmSync(tempDir, { recursive: true, force: true });
20
+ }
21
+ });
22
+
23
+ it('should initialize workspace with cpp only if cpp is selected', () => {
24
+ const { alreadyInitialized, gitignoreUpdated } = initWorkspace(tempDir, 'cpp');
25
+
26
+ expect(alreadyInitialized).toBe(false);
27
+ expect(gitignoreUpdated).toBe(true);
28
+
29
+ const atCoderCliDir = path.join(tempDir, '.atcoder-cli');
30
+ expect(fs.existsSync(atCoderCliDir)).toBe(true);
31
+
32
+ // Verify config.json contains only cpp
33
+ const config = loadConfig(tempDir);
34
+ expect(config.defaultLanguage).toBe('cpp');
35
+ expect(config.languages).toHaveProperty('cpp');
36
+ expect(config.languages).not.toHaveProperty('python');
37
+
38
+ // Verify templates contains only cpp
39
+ const templatesDir = path.join(atCoderCliDir, 'templates');
40
+ expect(fs.existsSync(path.join(templatesDir, 'cpp'))).toBe(true);
41
+ expect(fs.existsSync(path.join(templatesDir, 'cpp', 'main.cpp'))).toBe(true);
42
+ expect(fs.existsSync(path.join(templatesDir, 'python'))).toBe(false);
43
+
44
+ // Verify .gitignore contains problem.md
45
+ const gitignorePath = path.join(tempDir, '.gitignore');
46
+ expect(fs.existsSync(gitignorePath)).toBe(true);
47
+ const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
48
+ expect(gitignoreContent).toContain('problem.md');
49
+ });
50
+
51
+ it('should initialize workspace with python only if python is selected', () => {
52
+ const { alreadyInitialized, gitignoreUpdated } = initWorkspace(tempDir, 'python');
53
+
54
+ expect(alreadyInitialized).toBe(false);
55
+ expect(gitignoreUpdated).toBe(true);
56
+
57
+ const atCoderCliDir = path.join(tempDir, '.atcoder-cli');
58
+ expect(fs.existsSync(atCoderCliDir)).toBe(true);
59
+
60
+ // Verify config.json contains only python
61
+ const config = loadConfig(tempDir);
62
+ expect(config.defaultLanguage).toBe('python');
63
+ expect(config.languages).toHaveProperty('python');
64
+ expect(config.languages).not.toHaveProperty('cpp');
65
+
66
+ // Verify templates contains only python
67
+ const templatesDir = path.join(atCoderCliDir, 'templates');
68
+ expect(fs.existsSync(path.join(templatesDir, 'python'))).toBe(true);
69
+ expect(fs.existsSync(path.join(templatesDir, 'python', 'main.py'))).toBe(true);
70
+ expect(fs.existsSync(path.join(templatesDir, 'cpp'))).toBe(false);
71
+ });
72
+
73
+ describe('addLanguage', () => {
74
+ it('should throw an error if the language is already configured', () => {
75
+ initWorkspace(tempDir, 'cpp');
76
+
77
+ expect(() => {
78
+ addLanguage(tempDir, 'cpp', {
79
+ extension: 'cpp',
80
+ build: '',
81
+ run: ''
82
+ });
83
+ }).toThrowError('Language "cpp" is already configured.');
84
+ });
85
+
86
+ it('should add a new non-preset language', () => {
87
+ initWorkspace(tempDir, 'cpp');
88
+
89
+ addLanguage(tempDir, 'rust', {
90
+ extension: 'rs',
91
+ build: 'rustc main.rs',
92
+ run: './main',
93
+ template: '// Rust main\n'
94
+ });
95
+
96
+ const config = loadConfig(tempDir);
97
+ expect(config.languages).toHaveProperty('rust');
98
+ expect(config.languages.rust.extension).toBe('rs');
99
+ expect(config.languages.rust.build).toBe('rustc main.rs');
100
+ expect(config.languages.rust.run).toBe('./main');
101
+
102
+ const templatePath = path.join(tempDir, '.atcoder-cli', 'templates', 'rust', 'main.rs');
103
+ expect(fs.existsSync(templatePath)).toBe(true);
104
+ expect(fs.readFileSync(templatePath, 'utf8')).toBe('// Rust main\n');
105
+ });
106
+
107
+ it('should add a preset language (like python) when cpp is already initialized', () => {
108
+ initWorkspace(tempDir, 'cpp');
109
+
110
+ addLanguage(tempDir, 'python', {
111
+ extension: '',
112
+ build: '',
113
+ run: ''
114
+ });
115
+
116
+ const config = loadConfig(tempDir);
117
+ expect(config.languages).toHaveProperty('python');
118
+ expect(config.languages.python.extension).toBe('py');
119
+ expect(config.languages.python.run).toBe('python3 main.py');
120
+
121
+ const templatePath = path.join(tempDir, '.atcoder-cli', 'templates', 'python', 'main.py');
122
+ expect(fs.existsSync(templatePath)).toBe(true);
123
+ });
124
+ });
125
+ });
@@ -1,8 +1,9 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { DEFAULT_CONFIG, saveConfig } from '../config/config-store';
3
+ import { saveConfig, Config, LanguageConfig, loadConfig } from '../config/config-store';
4
+ import { AtcError } from '../utils/errors';
4
5
 
5
- const DEFAULT_CPP_TEMPLATE = `#include <bits/stdc++.h>
6
+ export const DEFAULT_CPP_TEMPLATE = `#include <bits/stdc++.h>
6
7
 
7
8
  using namespace std;
8
9
 
@@ -12,7 +13,7 @@ int main() {
12
13
  }
13
14
  `;
14
15
 
15
- const DEFAULT_PYTHON_TEMPLATE = `import sys
16
+ export const DEFAULT_PYTHON_TEMPLATE = `import sys
16
17
 
17
18
  def main():
18
19
  # Solve the problem here
@@ -22,6 +23,29 @@ if __name__ == '__main__':
22
23
  main()
23
24
  `;
24
25
 
26
+ export const LANGUAGE_PRESETS: Record<string, { config: LanguageConfig; template: string; filename: string }> = {
27
+ cpp: {
28
+ config: {
29
+ extension: 'cpp',
30
+ templateDir: 'templates/cpp',
31
+ build: 'g++ -O2 -std=gnu++20 -o a.out main.cpp',
32
+ run: './a.out'
33
+ },
34
+ template: DEFAULT_CPP_TEMPLATE,
35
+ filename: 'main.cpp'
36
+ },
37
+ python: {
38
+ config: {
39
+ extension: 'py',
40
+ templateDir: 'templates/python',
41
+ build: '',
42
+ run: 'python3 main.py'
43
+ },
44
+ template: DEFAULT_PYTHON_TEMPLATE,
45
+ filename: 'main.py'
46
+ }
47
+ };
48
+
25
49
  export function initWorkspace(
26
50
  targetDir: string = process.cwd(),
27
51
  defaultLanguage: string = 'cpp'
@@ -38,48 +62,125 @@ export function initWorkspace(
38
62
  // Create default config if it doesn't exist
39
63
  const configPath = path.join(atCoderCliDir, 'config.json');
40
64
  if (!fs.existsSync(configPath)) {
41
- saveConfig(targetDir, {
42
- ...DEFAULT_CONFIG,
43
- defaultLanguage
44
- });
65
+ const preset = LANGUAGE_PRESETS[defaultLanguage] || {
66
+ config: {
67
+ extension: defaultLanguage,
68
+ templateDir: `templates/${defaultLanguage}`,
69
+ build: '',
70
+ run: ''
71
+ },
72
+ template: '',
73
+ filename: `main.${defaultLanguage}`
74
+ };
75
+
76
+ const initialConfig: Config = {
77
+ defaultLanguage,
78
+ languages: {
79
+ [defaultLanguage]: preset.config
80
+ },
81
+ testDirName: 'tests',
82
+ contestDir: '',
83
+ lang: 'en',
84
+ extractProblemStatement: false,
85
+ problemLang: 'ja'
86
+ };
87
+ saveConfig(targetDir, initialConfig);
45
88
  }
46
89
 
47
90
  // Create default templates
48
91
  const templatesDir = path.join(atCoderCliDir, 'templates');
49
- const cppTemplateDir = path.join(templatesDir, 'cpp');
50
- const pythonTemplateDir = path.join(templatesDir, 'python');
51
-
52
- if (!fs.existsSync(cppTemplateDir)) {
53
- fs.mkdirSync(cppTemplateDir, { recursive: true });
54
- }
55
- const cppFile = path.join(cppTemplateDir, 'main.cpp');
56
- if (!fs.existsSync(cppFile)) {
57
- fs.writeFileSync(cppFile, DEFAULT_CPP_TEMPLATE, 'utf8');
58
- }
92
+ const preset = LANGUAGE_PRESETS[defaultLanguage] || {
93
+ config: {
94
+ extension: defaultLanguage,
95
+ templateDir: `templates/${defaultLanguage}`,
96
+ build: '',
97
+ run: ''
98
+ },
99
+ template: '',
100
+ filename: `main.${defaultLanguage}`
101
+ };
59
102
 
60
- if (!fs.existsSync(pythonTemplateDir)) {
61
- fs.mkdirSync(pythonTemplateDir, { recursive: true });
103
+ const langTemplateDir = path.join(templatesDir, defaultLanguage);
104
+ if (!fs.existsSync(langTemplateDir)) {
105
+ fs.mkdirSync(langTemplateDir, { recursive: true });
62
106
  }
63
- const pythonFile = path.join(pythonTemplateDir, 'main.py');
64
- if (!fs.existsSync(pythonFile)) {
65
- fs.writeFileSync(pythonFile, DEFAULT_PYTHON_TEMPLATE, 'utf8');
107
+ const templateFile = path.join(langTemplateDir, preset.filename);
108
+ if (!fs.existsSync(templateFile)) {
109
+ fs.writeFileSync(templateFile, preset.template, 'utf8');
66
110
  }
67
111
 
68
112
  // Update or create .gitignore
69
113
  const gitignorePath = path.join(targetDir, '.gitignore');
70
- const ignoreRule = '\n# AtCoder CLI Session\n.atcoder-cli/session.json\n';
114
+ const ignoreSession = '.atcoder-cli/session.json';
115
+ const ignoreProblem = 'problem.md';
71
116
  let gitignoreUpdated = false;
72
117
 
73
118
  if (fs.existsSync(gitignorePath)) {
74
- const content = fs.readFileSync(gitignorePath, 'utf8');
75
- if (!content.includes('.atcoder-cli/session.json')) {
76
- fs.writeFileSync(gitignorePath, content + ignoreRule, 'utf8');
119
+ let content = fs.readFileSync(gitignorePath, 'utf8');
120
+ let updated = false;
121
+ if (!content.includes(ignoreSession)) {
122
+ content += (content.endsWith('\n') ? '' : '\n') + '\n# AtCoder CLI Session\n' + ignoreSession + '\n';
123
+ updated = true;
124
+ }
125
+ if (!content.includes(ignoreProblem)) {
126
+ content += (content.endsWith('\n') ? '' : '\n') + '\n# AtCoder problem statements\n' + ignoreProblem + '\n';
127
+ updated = true;
128
+ }
129
+ if (updated) {
130
+ fs.writeFileSync(gitignorePath, content, 'utf8');
77
131
  gitignoreUpdated = true;
78
132
  }
79
133
  } else {
80
- fs.writeFileSync(gitignorePath, ignoreRule.trim() + '\n', 'utf8');
134
+ const defaultIgnore = `# AtCoder CLI Session\n${ignoreSession}\n\n# AtCoder problem statements\n${ignoreProblem}\n`;
135
+ fs.writeFileSync(gitignorePath, defaultIgnore, 'utf8');
81
136
  gitignoreUpdated = true;
82
137
  }
83
138
 
84
139
  return { alreadyInitialized, gitignoreUpdated };
85
140
  }
141
+
142
+ export function addLanguage(
143
+ workspaceRoot: string,
144
+ langName: string,
145
+ options: {
146
+ extension: string;
147
+ build: string;
148
+ run: string;
149
+ template?: string;
150
+ }
151
+ ): void {
152
+ const config = loadConfig(workspaceRoot);
153
+ const cleanLang = langName.trim().toLowerCase();
154
+
155
+ if (config.languages[cleanLang]) {
156
+ throw new AtcError(`Language "${cleanLang}" is already configured.`);
157
+ }
158
+
159
+ const preset = LANGUAGE_PRESETS[cleanLang];
160
+ const extension = options.extension || (preset ? preset.config.extension : cleanLang);
161
+ const build = options.build !== undefined ? options.build : (preset ? preset.config.build : '');
162
+ const run = options.run || (preset ? preset.config.run : '');
163
+ const template = options.template !== undefined ? options.template : (preset ? preset.template : '// Solve the problem here\n');
164
+
165
+ const templatesDir = path.join(workspaceRoot, '.atcoder-cli', 'templates');
166
+ const langTemplateDir = path.join(templatesDir, cleanLang);
167
+ if (!fs.existsSync(langTemplateDir)) {
168
+ fs.mkdirSync(langTemplateDir, { recursive: true });
169
+ }
170
+
171
+ const filename = preset ? preset.filename : `main.${extension}`;
172
+ const templateFile = path.join(langTemplateDir, filename);
173
+ if (!fs.existsSync(templateFile)) {
174
+ fs.writeFileSync(templateFile, template, 'utf8');
175
+ }
176
+
177
+ config.languages[cleanLang] = {
178
+ extension,
179
+ templateDir: `templates/${cleanLang}`,
180
+ build,
181
+ run
182
+ };
183
+
184
+ saveConfig(workspaceRoot, config);
185
+ }
186
+
@@ -1,21 +0,0 @@
1
- # Third Party Acknowledgements
2
-
3
- AtCoder Workspace is a modern, single-directory all-in-one CLI for AtCoder. We would like to express our deepest gratitude to the creators and maintainers of the following projects, whose design and features inspired this CLI:
4
-
5
- ## 1. atcoder-cli (acc)
6
- - **Author**: Tatamo
7
- - **Repository**: https://github.com/tatamo/atcoder-cli
8
- - **License**: MIT License
9
- - **Acknowledge**: We took inspiration from `atcoder-cli`'s template configurations, task structures, and workspace setups.
10
-
11
- ---
12
-
13
- ## 2. online-judge-tools (oj)
14
- - **Organization**: online-judge-tools
15
- - **Repository**: https://github.com/online-judge-tools/oj
16
- - **License**: MIT License
17
- - **Acknowledge**: We took inspiration from `online-judge-tools`'s test execution runner, diff normalization algorithms, and command interface structure.
18
-
19
- ---
20
-
21
- Thank you for paving the way for the competitive programming tooling ecosystem!