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.
- package/LICENSE +58 -0
- package/README.md +45 -23
- package/dist/atcoder/new.js +6 -1
- package/dist/atcoder/parser/problem-page.d.ts +2 -1
- package/dist/atcoder/parser/problem-page.js +340 -6
- package/dist/cli.js +177 -34
- package/dist/config/config-store.d.ts +2 -0
- package/dist/config/config-store.js +5 -5
- package/dist/utils/i18n.d.ts +64 -0
- package/dist/utils/i18n.js +67 -3
- package/dist/workspace/initializer.d.ts +14 -0
- package/dist/workspace/initializer.js +110 -25
- package/package.json +3 -3
- package/src/atcoder/new.test.ts +140 -0
- package/src/atcoder/new.ts +7 -1
- package/src/atcoder/parser/problem-page.test.ts +125 -0
- package/src/atcoder/parser/problem-page.ts +359 -6
- package/src/cli.ts +207 -36
- package/src/config/config-store.ts +7 -5
- package/src/test-runner/runner.test.ts +2 -0
- package/src/utils/i18n.ts +67 -3
- package/src/workspace/initializer.test.ts +125 -0
- package/src/workspace/initializer.ts +128 -27
- package/THIRD_PARTY_LICENSES +0 -21
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
103
|
+
const langTemplateDir = path.join(templatesDir, defaultLanguage);
|
|
104
|
+
if (!fs.existsSync(langTemplateDir)) {
|
|
105
|
+
fs.mkdirSync(langTemplateDir, { recursive: true });
|
|
62
106
|
}
|
|
63
|
-
const
|
|
64
|
-
if (!fs.existsSync(
|
|
65
|
-
fs.writeFileSync(
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
+
|
package/THIRD_PARTY_LICENSES
DELETED
|
@@ -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!
|