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/dist/cli.js CHANGED
@@ -69,8 +69,8 @@ const lang = (0, i18n_1.getLanguage)(workspaceRoot);
69
69
  const program = new commander_1.Command();
70
70
  program
71
71
  .name('atc')
72
- .description('AtCoder All-in-One CLI (Local-first)')
73
- .version('1.1.0-betas');
72
+ .description('AtCoder Workspace')
73
+ .version('1.1.0-beta.3');
74
74
  function handleAction(fn) {
75
75
  return async (...args) => {
76
76
  try {
@@ -237,47 +237,97 @@ program
237
237
  setupSpinner.stop((0, i18n_1.t)('newSetupSuccess', lang, tObj.label.toUpperCase(), res.sampleCount));
238
238
  }
239
239
  p.outro(picocolors_1.default.green((0, i18n_1.t)('newScaffoldingComplete', lang, selectedTasks.length)));
240
+ if (config.extractProblemStatement) {
241
+ p.note((0, i18n_1.t)('newStatementWarningBody', lang), picocolors_1.default.yellow((0, i18n_1.t)('newStatementWarningTitle', lang)));
242
+ }
240
243
  }));
241
- function resolveArgs(workspaceRoot, taskArg, fileArg) {
244
+ function resolveArgs(workspaceRoot, arg1, arg2, arg3) {
245
+ const cwd = process.cwd();
246
+ const config = (0, config_store_1.loadConfig)(workspaceRoot);
247
+ const contestDir = config.contestDir || '';
242
248
  let resolvedTaskDir = '';
243
249
  let resolvedFile;
244
- let isFile = false;
245
- let filePath = '';
246
- if (taskArg) {
247
- const pathsToCheck = [
248
- path.resolve(taskArg),
249
- path.resolve(workspaceRoot, taskArg)
250
+ // Helper function to check if relative path parts exist in cwd, workspaceRoot or contestDir
251
+ function checkPath(relativeParts) {
252
+ const pathsToTry = [
253
+ path.resolve(cwd, ...relativeParts),
254
+ path.resolve(workspaceRoot, ...relativeParts),
255
+ path.resolve(workspaceRoot, contestDir, ...relativeParts)
250
256
  ];
251
- const config = (0, config_store_1.loadConfig)(workspaceRoot);
252
- if (config.contestDir) {
253
- pathsToCheck.push(path.resolve(workspaceRoot, config.contestDir, taskArg));
254
- }
255
- for (const p of pathsToCheck) {
256
- if (fs.existsSync(p) && fs.statSync(p).isFile()) {
257
- isFile = true;
258
- filePath = p;
259
- break;
257
+ for (const p of pathsToTry) {
258
+ if (fs.existsSync(p)) {
259
+ const stat = fs.statSync(p);
260
+ return {
261
+ isFile: stat.isFile(),
262
+ isDir: stat.isDirectory(),
263
+ path: p
264
+ };
260
265
  }
261
266
  }
267
+ return null;
268
+ }
269
+ // 1. Three arguments provided: (arg1, arg2, arg3)
270
+ if (arg1 && arg2 && arg3) {
271
+ // e.g., abc300 a main.cpp -> check if arg1/arg2 is a directory
272
+ const checkDir = checkPath([arg1, arg2]);
273
+ if (checkDir && checkDir.isDir) {
274
+ resolvedTaskDir = checkDir.path;
275
+ resolvedFile = arg3;
276
+ }
262
277
  }
263
- if (isFile) {
264
- resolvedFile = path.basename(filePath);
265
- resolvedTaskDir = path.dirname(filePath);
278
+ // 2. Two arguments provided: (arg1, arg2)
279
+ if (!resolvedTaskDir && arg1 && arg2) {
280
+ // Pattern A: arg1/arg2 is a directory (e.g., abc300 a)
281
+ const checkDir = checkPath([arg1, arg2]);
282
+ if (checkDir && checkDir.isDir) {
283
+ resolvedTaskDir = checkDir.path;
284
+ resolvedFile = undefined;
285
+ }
286
+ else {
287
+ // Pattern B: arg1 is a directory, arg2 is a file (e.g., a main.cpp)
288
+ const checkArg1 = checkPath([arg1]);
289
+ if (checkArg1 && checkArg1.isDir) {
290
+ resolvedTaskDir = checkArg1.path;
291
+ resolvedFile = arg2;
292
+ }
293
+ }
266
294
  }
267
- else {
268
- resolvedTaskDir = (0, runner_1.resolveTaskDirectory)(workspaceRoot, taskArg);
269
- resolvedFile = fileArg;
295
+ // 3. One argument provided: (arg1)
296
+ if (!resolvedTaskDir && arg1) {
297
+ const checkArg1 = checkPath([arg1]);
298
+ if (checkArg1) {
299
+ if (checkArg1.isFile) {
300
+ // Pattern A: directly specified a file (e.g., main.cpp)
301
+ resolvedTaskDir = path.dirname(checkArg1.path);
302
+ resolvedFile = path.basename(checkArg1.path);
303
+ }
304
+ else if (checkArg1.isDir) {
305
+ // Pattern B: directly specified a directory (e.g., a or abc300/a)
306
+ resolvedTaskDir = checkArg1.path;
307
+ resolvedFile = undefined;
308
+ }
309
+ }
310
+ else {
311
+ // Fallback to resolveTaskDirectory (legacy behavior)
312
+ resolvedTaskDir = (0, runner_1.resolveTaskDirectory)(workspaceRoot, arg1);
313
+ resolvedFile = undefined;
314
+ }
315
+ }
316
+ // 4. No arguments provided
317
+ if (!resolvedTaskDir) {
318
+ resolvedTaskDir = (0, runner_1.resolveTaskDirectory)(workspaceRoot, undefined);
319
+ resolvedFile = undefined;
270
320
  }
271
321
  const taskLabel = path.basename(resolvedTaskDir);
272
322
  const contestId = path.basename(path.dirname(resolvedTaskDir));
273
323
  return { resolvedTaskDir, resolvedFile, taskLabel, contestId };
274
324
  }
275
325
  program
276
- .command('test [task] [file]')
326
+ .command('test [arg1] [arg2] [arg3]')
277
327
  .description((0, i18n_1.t)('descTest', lang))
278
- .action(handleAction(async (taskArg, fileArg) => {
328
+ .action(handleAction(async (arg1, arg2, arg3) => {
279
329
  const workspaceRoot = (0, finder_1.findWorkspaceRoot)();
280
- const { resolvedTaskDir, resolvedFile, taskLabel, contestId } = resolveArgs(workspaceRoot, taskArg, fileArg);
330
+ const { resolvedTaskDir, resolvedFile, taskLabel, contestId } = resolveArgs(workspaceRoot, arg1, arg2, arg3);
281
331
  p.intro(picocolors_1.default.cyan((0, i18n_1.t)('testIntro', lang, contestId, taskLabel)));
282
332
  const s = p.spinner();
283
333
  s.start((0, i18n_1.t)('testRetrievingLimits', lang));
@@ -355,11 +405,11 @@ program
355
405
  }
356
406
  }));
357
407
  program
358
- .command('submit [task] [file]')
408
+ .command('submit [arg1] [arg2] [arg3]')
359
409
  .description((0, i18n_1.t)('descSubmit', lang))
360
- .action(handleAction(async (taskArg, fileArg) => {
410
+ .action(handleAction(async (arg1, arg2, arg3) => {
361
411
  const workspaceRoot = (0, finder_1.findWorkspaceRoot)();
362
- const { resolvedTaskDir, resolvedFile, taskLabel, contestId } = resolveArgs(workspaceRoot, taskArg, fileArg);
412
+ const { resolvedTaskDir, resolvedFile, taskLabel, contestId } = resolveArgs(workspaceRoot, arg1, arg2, arg3);
363
413
  p.intro(picocolors_1.default.cyan((0, i18n_1.t)('submitPreparing', lang, contestId, taskLabel)));
364
414
  const s = p.spinner();
365
415
  s.start((0, i18n_1.t)('submitRetrievingLimits', lang));
@@ -491,11 +541,22 @@ program
491
541
  p.log.error(picocolors_1.default.red((0, i18n_1.t)('langWorkspaceRequired', lang)));
492
542
  process.exit(1);
493
543
  }
494
- if (!targetLanguage) {
495
- console.log((0, i18n_1.t)('langCommandUsage', lang));
496
- process.exit(0);
544
+ let selectedLang = targetLanguage;
545
+ if (!selectedLang) {
546
+ const choice = await p.select({
547
+ message: (0, i18n_1.t)('langSelectMessage', lang),
548
+ options: [
549
+ { value: 'en', label: 'English (en)' },
550
+ { value: 'ja', label: '日本語 (ja)' }
551
+ ]
552
+ });
553
+ if (p.isCancel(choice)) {
554
+ p.cancel((0, i18n_1.t)('langCancelled', lang));
555
+ process.exit(0);
556
+ }
557
+ selectedLang = choice;
497
558
  }
498
- const cleanLang = targetLanguage.trim().toLowerCase();
559
+ const cleanLang = selectedLang.trim().toLowerCase();
499
560
  if (cleanLang !== 'en' && cleanLang !== 'ja') {
500
561
  p.log.error(picocolors_1.default.red((0, i18n_1.t)('langInvalid', lang)));
501
562
  process.exit(1);
@@ -505,4 +566,86 @@ program
505
566
  (0, config_store_1.saveConfig)(workspaceRoot, config);
506
567
  p.log.success((0, i18n_1.t)('langSuccess', cleanLang, cleanLang));
507
568
  }));
569
+ program
570
+ .command('add-lang [langName]')
571
+ .description((0, i18n_1.t)('descAddLang', lang))
572
+ .action(handleAction(async (langName) => {
573
+ const root = (0, finder_1.findWorkspaceRoot)();
574
+ const config = (0, config_store_1.loadConfig)(root);
575
+ let targetLang = langName;
576
+ if (!targetLang) {
577
+ targetLang = await p.text({
578
+ message: (0, i18n_1.t)('addLangEnterName', lang),
579
+ validate: (val) => (!val.trim() ? (0, i18n_1.t)('addLangNameNotEmpty', lang) : undefined)
580
+ });
581
+ if (p.isCancel(targetLang)) {
582
+ p.cancel((0, i18n_1.t)('addLangCancelled', lang));
583
+ process.exit(0);
584
+ }
585
+ }
586
+ targetLang = targetLang.trim().toLowerCase();
587
+ // Check if already exists
588
+ if (config.languages[targetLang]) {
589
+ throw new errors_1.AtcError((0, i18n_1.t)('addLangAlreadyExists', lang, targetLang));
590
+ }
591
+ const preset = initializer_1.LANGUAGE_PRESETS[targetLang];
592
+ let extension = '';
593
+ let build = '';
594
+ let run = '';
595
+ let template = '';
596
+ if (preset) {
597
+ extension = preset.config.extension;
598
+ build = preset.config.build;
599
+ run = preset.config.run;
600
+ template = preset.template;
601
+ }
602
+ else {
603
+ // If not preset, prompt for parameters
604
+ const extInput = await p.text({
605
+ message: (0, i18n_1.t)('addLangEnterExtension', lang, targetLang),
606
+ placeholder: targetLang,
607
+ validate: (val) => (!val.trim() ? (0, i18n_1.t)('addLangExtNotEmpty', lang) : undefined)
608
+ });
609
+ if (p.isCancel(extInput)) {
610
+ p.cancel((0, i18n_1.t)('addLangCancelled', lang));
611
+ process.exit(0);
612
+ }
613
+ const buildInput = await p.text({
614
+ message: (0, i18n_1.t)('addLangEnterBuildCmd', lang),
615
+ placeholder: 'e.g. g++ -O2 main.cpp (leave empty if not needed)'
616
+ });
617
+ if (p.isCancel(buildInput)) {
618
+ p.cancel((0, i18n_1.t)('addLangCancelled', lang));
619
+ process.exit(0);
620
+ }
621
+ const runInput = await p.text({
622
+ message: (0, i18n_1.t)('addLangEnterRunCmd', lang),
623
+ placeholder: `e.g. python3 main.py or ./a.out`,
624
+ validate: (val) => (!val.trim() ? (0, i18n_1.t)('addLangRunCmdNotEmpty', lang) : undefined)
625
+ });
626
+ if (p.isCancel(runInput)) {
627
+ p.cancel((0, i18n_1.t)('addLangCancelled', lang));
628
+ process.exit(0);
629
+ }
630
+ extension = extInput.trim();
631
+ build = buildInput.trim();
632
+ run = runInput.trim();
633
+ template = `// Solve the problem here\n`;
634
+ }
635
+ const s = p.spinner();
636
+ s.start((0, i18n_1.t)('addLangSpinner', lang));
637
+ try {
638
+ (0, initializer_1.addLanguage)(root, targetLang, {
639
+ extension,
640
+ build,
641
+ run,
642
+ template
643
+ });
644
+ }
645
+ catch (e) {
646
+ s.stop('Failed');
647
+ throw e;
648
+ }
649
+ s.stop((0, i18n_1.t)('addLangSuccess', lang, targetLang));
650
+ }));
508
651
  program.parse(process.argv);
@@ -10,6 +10,8 @@ export interface Config {
10
10
  testDirName: string;
11
11
  contestDir?: string;
12
12
  lang?: 'en' | 'ja';
13
+ extractProblemStatement?: boolean;
14
+ problemLang?: 'en' | 'ja';
13
15
  }
14
16
  export declare const DEFAULT_CONFIG: Config;
15
17
  export declare function getConfigPath(workspaceRoot: string): string;
@@ -56,7 +56,10 @@ exports.DEFAULT_CONFIG = {
56
56
  }
57
57
  },
58
58
  testDirName: 'tests',
59
- contestDir: ''
59
+ contestDir: '',
60
+ lang: 'en',
61
+ extractProblemStatement: false,
62
+ problemLang: 'ja'
60
63
  };
61
64
  function getConfigPath(workspaceRoot) {
62
65
  return path.join(workspaceRoot, '.atcoder-cli', 'config.json');
@@ -72,10 +75,7 @@ function loadConfig(workspaceRoot) {
72
75
  return {
73
76
  ...exports.DEFAULT_CONFIG,
74
77
  ...parsed,
75
- languages: {
76
- ...exports.DEFAULT_CONFIG.languages,
77
- ...(parsed.languages || {})
78
- }
78
+ languages: parsed.languages ? parsed.languages : exports.DEFAULT_CONFIG.languages
79
79
  };
80
80
  }
81
81
  catch (e) {
@@ -46,6 +46,10 @@ export declare const MESSAGES: {
46
46
  en: string;
47
47
  ja: string;
48
48
  };
49
+ descAddLang: {
50
+ en: string;
51
+ ja: string;
52
+ };
49
53
  initIntro: {
50
54
  en: string;
51
55
  ja: string;
@@ -186,6 +190,14 @@ export declare const MESSAGES: {
186
190
  en: (count: number) => string;
187
191
  ja: (count: number) => string;
188
192
  };
193
+ newStatementWarningTitle: {
194
+ en: string;
195
+ ja: string;
196
+ };
197
+ newStatementWarningBody: {
198
+ en: string;
199
+ ja: string;
200
+ };
189
201
  testIntro: {
190
202
  en: (contestId: string, label: string) => string;
191
203
  ja: (contestId: string, label: string) => string;
@@ -330,6 +342,14 @@ export declare const MESSAGES: {
330
342
  en: string;
331
343
  ja: string;
332
344
  };
345
+ langSelectMessage: {
346
+ en: string;
347
+ ja: string;
348
+ };
349
+ langCancelled: {
350
+ en: string;
351
+ ja: string;
352
+ };
333
353
  submitSessionExpired: {
334
354
  en: string;
335
355
  ja: string;
@@ -338,6 +358,50 @@ export declare const MESSAGES: {
338
358
  en: string;
339
359
  ja: string;
340
360
  };
361
+ addLangAlreadyExists: {
362
+ en: (lang: string) => string;
363
+ ja: (lang: string) => string;
364
+ };
365
+ addLangEnterName: {
366
+ en: string;
367
+ ja: string;
368
+ };
369
+ addLangNameNotEmpty: {
370
+ en: string;
371
+ ja: string;
372
+ };
373
+ addLangCancelled: {
374
+ en: string;
375
+ ja: string;
376
+ };
377
+ addLangEnterExtension: {
378
+ en: (lang: string) => string;
379
+ ja: (lang: string) => string;
380
+ };
381
+ addLangExtNotEmpty: {
382
+ en: string;
383
+ ja: string;
384
+ };
385
+ addLangEnterBuildCmd: {
386
+ en: string;
387
+ ja: string;
388
+ };
389
+ addLangEnterRunCmd: {
390
+ en: string;
391
+ ja: string;
392
+ };
393
+ addLangRunCmdNotEmpty: {
394
+ en: string;
395
+ ja: string;
396
+ };
397
+ addLangSpinner: {
398
+ en: string;
399
+ ja: string;
400
+ };
401
+ addLangSuccess: {
402
+ en: (lang: string) => string;
403
+ ja: (lang: string) => string;
404
+ };
341
405
  };
342
406
  /**
343
407
  * Translates a key into the active language.
@@ -87,13 +87,17 @@ exports.MESSAGES = {
87
87
  ja: 'ダウンロードしたサンプルケースに対してローカルテストを実行します'
88
88
  },
89
89
  descSubmit: {
90
- en: 'Submit code to AtCoder and poll status',
91
- ja: 'コードを AtCoder に提出し、ジャッジステータスを監視します'
90
+ en: 'Submit code to AtCoder',
91
+ ja: 'コードを AtCoder に提出します'
92
92
  },
93
93
  descLang: {
94
94
  en: 'Change the display language (en or ja)',
95
95
  ja: '表示言語の切り替え (en または ja)'
96
96
  },
97
+ descAddLang: {
98
+ en: 'Add a programming language configuration and template',
99
+ ja: 'プログラミング言語の設定とテンプレートを追加します'
100
+ },
97
101
  // init
98
102
  initIntro: {
99
103
  en: 'AtCoder Workspace - Workspace Initialization',
@@ -241,6 +245,14 @@ exports.MESSAGES = {
241
245
  en: (count) => `Scaffolding complete for ${count} task(s).`,
242
246
  ja: (count) => `${count} 個の問題のセットアップが完了しました。`
243
247
  },
248
+ newStatementWarningTitle: {
249
+ en: '[WARNING] Automatic problem statement extraction is enabled',
250
+ ja: '【警告】問題文の自動抽出が有効化されています'
251
+ },
252
+ newStatementWarningBody: {
253
+ 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).',
254
+ ja: '・コンテスト中に問題文のMarkdownを生成AIに読み込ませないでください(ルール違反となります)。\n・抽出した問題文をそのままインターネット(GitHubパブリックリポジトリ等)に公開・共有しないでください。'
255
+ },
244
256
  // test
245
257
  testIntro: {
246
258
  en: (contestId, label) => `Running tests for ${contestId}/${label}`,
@@ -284,7 +296,7 @@ exports.MESSAGES = {
284
296
  },
285
297
  testOutroFailed: {
286
298
  en: 'Some tests failed. 😢',
287
- ja: '一部のテストが不合格でした。 😢'
299
+ ja: 'テストに失敗しました。 😢'
288
300
  },
289
301
  // submit
290
302
  submitPreparing: {
@@ -388,6 +400,14 @@ exports.MESSAGES = {
388
400
  en: 'Usage: atc lang <en|ja>',
389
401
  ja: '使い方: atc lang <en|ja>'
390
402
  },
403
+ langSelectMessage: {
404
+ en: 'Select display language:',
405
+ ja: '表示言語を選択してください:'
406
+ },
407
+ langCancelled: {
408
+ en: 'Language selection cancelled.',
409
+ ja: '言語選択がキャンセルされました。'
410
+ },
391
411
  submitSessionExpired: {
392
412
  en: 'Session expired or invalid. Please log in again using "atc login".',
393
413
  ja: 'セッションの期限が切れているか無効です。"atc login" を実行して再ログインしてください。'
@@ -395,6 +415,50 @@ exports.MESSAGES = {
395
415
  submitLangSelectNotFound: {
396
416
  en: 'Language selection element not found on submit page. Please make sure you are logged in and the contest has started.',
397
417
  ja: '提出ページに言語選択要素が見つかりませんでした。ログイン状態であること、およびコンテストが開始されていることを確認してください。'
418
+ },
419
+ addLangAlreadyExists: {
420
+ en: (lang) => `Language "${lang}" is already configured.`,
421
+ ja: (lang) => `言語 "${lang}" は既に設定されています。`
422
+ },
423
+ addLangEnterName: {
424
+ en: 'Enter the programming language name to add:',
425
+ ja: '追加するプログラミング言語名を入力してください:'
426
+ },
427
+ addLangNameNotEmpty: {
428
+ en: 'Language name cannot be empty.',
429
+ ja: '言語名は空にすることはできません。'
430
+ },
431
+ addLangCancelled: {
432
+ en: 'Language addition cancelled.',
433
+ ja: '言語の追加がキャンセルされました。'
434
+ },
435
+ addLangEnterExtension: {
436
+ en: (lang) => `Enter file extension for ${lang}:`,
437
+ ja: (lang) => `${lang} のファイル拡張子を入力してください:`
438
+ },
439
+ addLangExtNotEmpty: {
440
+ en: 'Extension cannot be empty.',
441
+ ja: '拡張子は空にすることはできません。'
442
+ },
443
+ addLangEnterBuildCmd: {
444
+ en: 'Enter build command (leave empty if not needed):',
445
+ ja: 'ビルドコマンドを入力してください (不要な場合は空欄のまま):'
446
+ },
447
+ addLangEnterRunCmd: {
448
+ en: 'Enter execution command:',
449
+ ja: '実行コマンドを入力してください:'
450
+ },
451
+ addLangRunCmdNotEmpty: {
452
+ en: 'Execution command cannot be empty.',
453
+ ja: '実行コマンドは空にすることはできません。'
454
+ },
455
+ addLangSpinner: {
456
+ en: 'Adding language configuration...',
457
+ ja: '言語設定を追加中...'
458
+ },
459
+ addLangSuccess: {
460
+ en: (lang) => `Language "${lang}" added successfully!`,
461
+ ja: (lang) => `言語 "${lang}" が正常に追加されました!`
398
462
  }
399
463
  };
400
464
  /**
@@ -1,4 +1,18 @@
1
+ import { LanguageConfig } from '../config/config-store';
2
+ export declare const DEFAULT_CPP_TEMPLATE = "#include <bits/stdc++.h>\n\nusing namespace std;\n\nint main() {\n // Solve the problem here\n return 0;\n}\n";
3
+ export declare const DEFAULT_PYTHON_TEMPLATE = "import sys\n\ndef main():\n # Solve the problem here\n pass\n\nif __name__ == '__main__':\n main()\n";
4
+ export declare const LANGUAGE_PRESETS: Record<string, {
5
+ config: LanguageConfig;
6
+ template: string;
7
+ filename: string;
8
+ }>;
1
9
  export declare function initWorkspace(targetDir?: string, defaultLanguage?: string): {
2
10
  alreadyInitialized: boolean;
3
11
  gitignoreUpdated: boolean;
4
12
  };
13
+ export declare function addLanguage(workspaceRoot: string, langName: string, options: {
14
+ extension: string;
15
+ build: string;
16
+ run: string;
17
+ template?: string;
18
+ }): void;