@jpetit/toolkit 3.0.0

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 (192) hide show
  1. package/.prettierignore +11 -0
  2. package/.prettierrc.json +9 -0
  3. package/.vscode/settings.json +26 -0
  4. package/README.md +22 -0
  5. package/assets/lua/fixCodeBlocks.lua +19 -0
  6. package/assets/lua/removeEnvs.lua +20 -0
  7. package/assets/lua/removeHtmlOnly.lua +11 -0
  8. package/assets/problems/graphics/japanese-flag.pbm/README.md +14 -0
  9. package/assets/problems/graphics/japanese-flag.pbm/award.png +0 -0
  10. package/assets/problems/graphics/japanese-flag.pbm/handler.yml +2 -0
  11. package/assets/problems/graphics/japanese-flag.pbm/problem.ca.tex +21 -0
  12. package/assets/problems/graphics/japanese-flag.pbm/problem.ca.yml +3 -0
  13. package/assets/problems/graphics/japanese-flag.pbm/sample-1.inp +1 -0
  14. package/assets/problems/graphics/japanese-flag.pbm/sample-2.inp +1 -0
  15. package/assets/problems/graphics/japanese-flag.pbm/solution.cc +25 -0
  16. package/assets/problems/graphics/japanese-flag.pbm/solution.py +11 -0
  17. package/assets/problems/graphics/tortuga.pbm/README.md +13 -0
  18. package/assets/problems/graphics/tortuga.pbm/award.png +0 -0
  19. package/assets/problems/graphics/tortuga.pbm/handler.yml +2 -0
  20. package/assets/problems/graphics/tortuga.pbm/problem.ca.tex +23 -0
  21. package/assets/problems/graphics/tortuga.pbm/problem.ca.yml +3 -0
  22. package/assets/problems/graphics/tortuga.pbm/sample.inp +0 -0
  23. package/assets/problems/graphics/tortuga.pbm/solution.py +11 -0
  24. package/assets/problems/standard/campanar-de-la-torrassa.pbm/README.md +15 -0
  25. package/assets/problems/standard/campanar-de-la-torrassa.pbm/award.html +1 -0
  26. package/assets/problems/standard/campanar-de-la-torrassa.pbm/award.png +0 -0
  27. package/assets/problems/standard/campanar-de-la-torrassa.pbm/campanar.eps +1113 -0
  28. package/assets/problems/standard/campanar-de-la-torrassa.pbm/campanar.png +0 -0
  29. package/assets/problems/standard/campanar-de-la-torrassa.pbm/generate.cc +10 -0
  30. package/assets/problems/standard/campanar-de-la-torrassa.pbm/handler.yml +2 -0
  31. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.ca.tex +59 -0
  32. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.ca.yml +3 -0
  33. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.en.tex +52 -0
  34. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.en.yml +4 -0
  35. package/assets/problems/standard/campanar-de-la-torrassa.pbm/sample.inp +7 -0
  36. package/assets/problems/standard/campanar-de-la-torrassa.pbm/slow.cc +29 -0
  37. package/assets/problems/standard/campanar-de-la-torrassa.pbm/solution.cc +48 -0
  38. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-1.inp +12 -0
  39. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-2.inp +100000 -0
  40. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-2.ops +1 -0
  41. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-b.inp +0 -0
  42. package/assets/problems/standard/maximum-of-2-integers.pbm/README.md +11 -0
  43. package/assets/problems/standard/maximum-of-2-integers.pbm/handler.yml +1 -0
  44. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.ca.tex +17 -0
  45. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.ca.yml +3 -0
  46. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.en.tex +16 -0
  47. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.en.yml +4 -0
  48. package/assets/problems/standard/maximum-of-2-integers.pbm/sample-1.inp +1 -0
  49. package/assets/problems/standard/maximum-of-2-integers.pbm/sample-2.inp +1 -0
  50. package/assets/problems/standard/maximum-of-2-integers.pbm/sample-3.inp +1 -0
  51. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.c +18 -0
  52. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.cc +13 -0
  53. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.java +16 -0
  54. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.py +5 -0
  55. package/assets/problems/standard/maximum-of-2-integers.pbm/test-1.inp +1 -0
  56. package/assets/problems/standard/maximum-of-2-integers.pbm/test-2.inp +1 -0
  57. package/assets/problems/standard/maximum-of-2-integers.pbm/test-3.inp +1 -0
  58. package/assets/problems/standard/maximum-of-2-integers.pbm/test-4.inp +1 -0
  59. package/assets/problems/standard/maximum-of-2-integers.pbm/test-5.inp +1 -0
  60. package/assets/problems/standard/treasures-in-a-map.pbm/README.md +12 -0
  61. package/assets/problems/standard/treasures-in-a-map.pbm/atzar.cc +85 -0
  62. package/assets/problems/standard/treasures-in-a-map.pbm/award.png +0 -0
  63. package/assets/problems/standard/treasures-in-a-map.pbm/generate-1.cc +26 -0
  64. package/assets/problems/standard/treasures-in-a-map.pbm/generate-2.cc +26 -0
  65. package/assets/problems/standard/treasures-in-a-map.pbm/generate-3.cc +26 -0
  66. package/assets/problems/standard/treasures-in-a-map.pbm/generate-4.cc +26 -0
  67. package/assets/problems/standard/treasures-in-a-map.pbm/handler.yml +1 -0
  68. package/assets/problems/standard/treasures-in-a-map.pbm/problem.ca.tex +40 -0
  69. package/assets/problems/standard/treasures-in-a-map.pbm/problem.ca.yml +3 -0
  70. package/assets/problems/standard/treasures-in-a-map.pbm/problem.en.tex +40 -0
  71. package/assets/problems/standard/treasures-in-a-map.pbm/problem.en.yml +4 -0
  72. package/assets/problems/standard/treasures-in-a-map.pbm/random-1.inp +24 -0
  73. package/assets/problems/standard/treasures-in-a-map.pbm/random-2.inp +27 -0
  74. package/assets/problems/standard/treasures-in-a-map.pbm/random-3.inp +38 -0
  75. package/assets/problems/standard/treasures-in-a-map.pbm/random-4.inp +50 -0
  76. package/assets/problems/standard/treasures-in-a-map.pbm/sample-1.inp +9 -0
  77. package/assets/problems/standard/treasures-in-a-map.pbm/sample-2.inp +6 -0
  78. package/assets/problems/standard/treasures-in-a-map.pbm/sample-3.inp +7 -0
  79. package/assets/problems/standard/treasures-in-a-map.pbm/solution.cc +38 -0
  80. package/assets/problems/standard/treasures-in-a-map.pbm/test-1.inp +5 -0
  81. package/assets/problems/standard/treasures-in-a-map.pbm/test-2.inp +6 -0
  82. package/assets/problems/standard/treasures-in-a-map.pbm/test-3.inp +6 -0
  83. package/assets/problems/standard/treasures-in-a-map.pbm/test-4.inp +9 -0
  84. package/assets/problems/standard/treasures-in-a-map.pbm/test-5.inp +10 -0
  85. package/assets/problems/standard/treasures-in-a-map.pbm/test-6.inp +9 -0
  86. package/assets/problems/standard/treasures-in-a-map.pbm/test-7.inp +12 -0
  87. package/assets/problems/standard/treasures-in-a-map.pbm/test-8.inp +3 -0
  88. package/assets/problems/standard/treasures-in-a-map.pbm/test-9.inp +37 -0
  89. package/assets/problems/standard/treasures-in-a-map.pbm/test-91.inp +52 -0
  90. package/assets/problems/standard/treasures-in-a-map.pbm/test-92.inp +25 -0
  91. package/assets/problems/standard/treasures-in-a-map.pbm/test-93.inp +3 -0
  92. package/assets/sty/judgeit.ca.sty +54 -0
  93. package/assets/sty/judgeit.de.sty +61 -0
  94. package/assets/sty/judgeit.en.sty +60 -0
  95. package/assets/sty/judgeit.es.sty +54 -0
  96. package/assets/sty/judgeit.fr.sty +59 -0
  97. package/assets/sty/judgeit.sty +307 -0
  98. package/assets/sty/picins.sty +579 -0
  99. package/assets.zip +0 -0
  100. package/eslint.config.mjs +31 -0
  101. package/lib/ai.ts +138 -0
  102. package/lib/assets.ts +31 -0
  103. package/lib/cleaner.ts +58 -0
  104. package/lib/compilers/_frompython.ts +388 -0
  105. package/lib/compilers/base.ts +97 -0
  106. package/lib/compilers/gcc.ts +47 -0
  107. package/lib/compilers/gxx.ts +47 -0
  108. package/lib/compilers/index.ts +61 -0
  109. package/lib/compilers/python3.ts +67 -0
  110. package/lib/data.ts +19 -0
  111. package/lib/doctor.ts +104 -0
  112. package/lib/generate.ts +333 -0
  113. package/lib/maker.ts +535 -0
  114. package/lib/settings.ts +42 -0
  115. package/lib/tui.ts +69 -0
  116. package/lib/utils.ts +83 -0
  117. package/package.json +56 -0
  118. package/problems/graphic.pbm/README.md +14 -0
  119. package/problems/graphic.pbm/award.png +0 -0
  120. package/problems/graphic.pbm/handler.yml +2 -0
  121. package/problems/graphic.pbm/problem.ca.html +13 -0
  122. package/problems/graphic.pbm/problem.ca.md +20 -0
  123. package/problems/graphic.pbm/problem.ca.tex +21 -0
  124. package/problems/graphic.pbm/problem.ca.txt +20 -0
  125. package/problems/graphic.pbm/problem.ca.yml +3 -0
  126. package/problems/graphic.pbm/sample-1.inp +1 -0
  127. package/problems/graphic.pbm/sample-2.inp +1 -0
  128. package/problems/graphic.pbm/solution.py +11 -0
  129. package/problems/maxim2.pbm/Main.java +13 -0
  130. package/problems/maxim2.pbm/distillation.yml +7 -0
  131. package/problems/maxim2.pbm/distilled-01.inp +1 -0
  132. package/problems/maxim2.pbm/distilled-02.inp +1 -0
  133. package/problems/maxim2.pbm/distilled-03.inp +1 -0
  134. package/problems/maxim2.pbm/distiller.yml +2 -0
  135. package/problems/maxim2.pbm/generate-inputs.py +9 -0
  136. package/problems/maxim2.pbm/handler.yml +2 -0
  137. package/problems/maxim2.pbm/ma-1.inp +1 -0
  138. package/problems/maxim2.pbm/ma-2.inp +1 -0
  139. package/problems/maxim2.pbm/ma-3.inp +1 -0
  140. package/problems/maxim2.pbm/ma-4.inp +1 -0
  141. package/problems/maxim2.pbm/ma-5.inp +1 -0
  142. package/problems/maxim2.pbm/per-doubles.inp +1 -0
  143. package/problems/maxim2.pbm/problem.ca.html +11 -0
  144. package/problems/maxim2.pbm/problem.ca.md +19 -0
  145. package/problems/maxim2.pbm/problem.ca.tex +17 -0
  146. package/problems/maxim2.pbm/problem.ca.txt +19 -0
  147. package/problems/maxim2.pbm/problem.ca.yml +3 -0
  148. package/problems/maxim2.pbm/problem.en.html +11 -0
  149. package/problems/maxim2.pbm/problem.en.md +19 -0
  150. package/problems/maxim2.pbm/problem.en.tex +16 -0
  151. package/problems/maxim2.pbm/problem.en.txt +19 -0
  152. package/problems/maxim2.pbm/problem.en.yml +4 -0
  153. package/problems/maxim2.pbm/sample-1.inp +1 -0
  154. package/problems/maxim2.pbm/sample-2.inp +1 -0
  155. package/problems/maxim2.pbm/sample-3.inp +1 -0
  156. package/problems/maxim2.pbm/solution.c +12 -0
  157. package/problems/maxim2.pbm/solution.cc +13 -0
  158. package/problems/maxim2.pbm/solution.java +13 -0
  159. package/problems/maxim2.pbm/solution.pas +9 -0
  160. package/problems/maxim2.pbm/solution.py +5 -0
  161. package/problems/maxim2.pbm/tags.yml +2 -0
  162. package/problems/maxim2.pbm/test_-1_-1.inp +1 -0
  163. package/problems/maxim2.pbm/test_-1_-2.inp +1 -0
  164. package/problems/maxim2.pbm/test_-1_0.inp +1 -0
  165. package/problems/maxim2.pbm/test_-1_1.inp +1 -0
  166. package/problems/maxim2.pbm/test_-2_-1.inp +1 -0
  167. package/problems/maxim2.pbm/test_-2_-2.inp +1 -0
  168. package/problems/maxim2.pbm/test_-2_0.inp +1 -0
  169. package/problems/maxim2.pbm/test_-2_1.inp +1 -0
  170. package/problems/maxim2.pbm/test_0_-1.inp +1 -0
  171. package/problems/maxim2.pbm/test_0_-2.inp +1 -0
  172. package/problems/maxim2.pbm/test_0_0.inp +1 -0
  173. package/problems/maxim2.pbm/test_0_1.inp +1 -0
  174. package/problems/maxim2.pbm/test_1_-1.inp +1 -0
  175. package/problems/maxim2.pbm/test_1_-2.inp +1 -0
  176. package/problems/maxim2.pbm/test_1_0.inp +1 -0
  177. package/problems/maxim2.pbm/test_1_1.inp +1 -0
  178. package/test.ts +3 -0
  179. package/toolkit/ai.ts +30 -0
  180. package/toolkit/clean.ts +19 -0
  181. package/toolkit/compilers.ts +29 -0
  182. package/toolkit/create-jutge-ai.ts +101 -0
  183. package/toolkit/create-template.ts +51 -0
  184. package/toolkit/create-wizard.ts +4 -0
  185. package/toolkit/create.ts +75 -0
  186. package/toolkit/doctor.ts +17 -0
  187. package/toolkit/index.ts +28 -0
  188. package/toolkit/init.ts +66 -0
  189. package/toolkit/make.ts +60 -0
  190. package/toolkit/verify.ts +19 -0
  191. package/tsconfig.json +38 -0
  192. package/types/zip.d.ts +4 -0
package/toolkit/ai.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { Command } from '@commander-js/extra-typings'
2
+ import { complete, listModels } from '@/lib/ai.ts'
3
+
4
+ export const ai = new Command('ai').description('Query AI models')
5
+
6
+ ai.command('complete')
7
+ .description('Complete a prompt')
8
+
9
+ .argument('prompt', 'the user prompt to complete')
10
+ .option('-s, --system-prompt <system>', 'the system prompt to use', 'You are a helpful assistant.')
11
+ .option(
12
+ '-m, --model <model>',
13
+ 'the AI model to use (eg: openai/gpt-5, google/gemini-2.5-pro, ...)',
14
+ 'google/gemini-2.5-flash-lite',
15
+ )
16
+
17
+ .action(async (prompt, { model, systemPrompt }) => {
18
+ if (!prompt) prompt = 'Who are you?'
19
+ if (!systemPrompt) systemPrompt = 'You are a helpful assistant.'
20
+
21
+ const answer = await complete(model, systemPrompt, prompt)
22
+ console.log(answer)
23
+ })
24
+
25
+ ai.command('models')
26
+ .description('Show available AI models')
27
+ .action(async () => {
28
+ const models = await listModels()
29
+ console.dir(models, { depth: null })
30
+ })
@@ -0,0 +1,19 @@
1
+ import { cleanFiles } from '@/lib/cleaner'
2
+ import { Command, Option } from '@commander-js/extra-typings'
3
+ import boxen from 'boxen'
4
+ import chalk from 'chalk'
5
+
6
+ export const clean = new Command('clean')
7
+ .description('Clean generated files')
8
+
9
+ .addOption(new Option('-f, --force', 'force removal').conflicts('dryRun'))
10
+ .addOption(new Option('-n, --dry-run', 'show but do not remove files').conflicts('force'))
11
+ .option('-d, --directory <path>', 'problem directory', '.')
12
+
13
+ .action(async (options) => {
14
+ // Default to dry-run if neither option is specified
15
+ console.log(chalk.blue(boxen('Clean files', { padding: { left: 1, right: 1 }, width: process.stdout.columns })))
16
+ console.log()
17
+ const force = options.force || false
18
+ await cleanFiles(force, options.directory)
19
+ })
@@ -0,0 +1,29 @@
1
+ import { getAvailableCompilers, getCompilersInfo, getDefinedCompilerIds } from '@/lib/compilers'
2
+ import { Command } from '@commander-js/extra-typings'
3
+
4
+ export const compilers = new Command('compilers')
5
+ .description('Query compiler information')
6
+ // default action is to list all compilers
7
+
8
+ .action(async () => {
9
+ const info = await getCompilersInfo()
10
+ console.dir(info)
11
+ })
12
+
13
+ compilers
14
+ .command('list-defined')
15
+ .description('List all defined compiler names')
16
+
17
+ .action(async () => {
18
+ const items = await getDefinedCompilerIds()
19
+ console.dir(items)
20
+ })
21
+
22
+ compilers
23
+ .command('list-available')
24
+ .description('List all available compiler names')
25
+
26
+ .action(async () => {
27
+ const items = await getAvailableCompilers()
28
+ console.dir(items)
29
+ })
@@ -0,0 +1,101 @@
1
+ import { generateProblemWithJutgeAI, type ProblemData } from '@/lib/generate'
2
+ import { guessUserEmail, guessUserName } from '@/lib/utils'
3
+ import { checkbox, input, select } from '@inquirer/prompts'
4
+ import * as tui from '@/lib/tui.js'
5
+ import humanId from 'human-id'
6
+ import slug from 'slug'
7
+
8
+ const proglangsChoices = [
9
+ { name: 'C', value: 'c' },
10
+ { name: 'C++', value: 'cpp' },
11
+ { name: 'Python3', value: 'py' },
12
+ { name: 'Haskell', value: 'hs' },
13
+ { name: 'Clojure', value: 'clj' },
14
+ ]
15
+
16
+ const problemTypesChoices = [
17
+ { name: 'Standard (read input, write output)', value: 'Standard' },
18
+ { name: 'Function (use functions)', value: 'Function' },
19
+ { name: 'Graphic', value: 'Graphic' },
20
+ ]
21
+
22
+ const languagesChoices = [
23
+ { name: 'English', value: 'en' },
24
+ { name: 'Catalan', value: 'ca' },
25
+ { name: 'Spanish', value: 'es' },
26
+ { name: 'French', value: 'fr' },
27
+ { name: 'German', value: 'de' },
28
+ ]
29
+
30
+ const modelsChoices = [
31
+ { name: 'Google - Gemini 2.5 flash lite', value: 'google/gemini-2.5-flash-lite' },
32
+ { name: 'Google - Gemini 2.5 flash', value: 'google/gemini-2.5-flash' },
33
+ { name: 'Google - Gemini 2.5', value: 'google/gemini-2.5' },
34
+ { name: 'OpenAI - GPT-5 Nano', value: 'openai/gpt-5-nano' },
35
+ { name: 'OpenAI - GPT-5 Mini', value: 'openai/gpt-5-mini' },
36
+ { name: 'OpenAI - GPT-5', value: 'openai/gpt-5' },
37
+ { name: 'Ollama - GPT OSS', value: 'ollama/gpt-oss' },
38
+ ]
39
+
40
+ async function getInitialData(outputDir: string): Promise<ProblemData> {
41
+ const author = (await guessUserName()) || 'John Doe'
42
+ const email = (await guessUserEmail()) || 'john.doe@example.com'
43
+ const title = 'The ' + humanId({ separator: ' ', capitalize: false })
44
+ const folder = slug(`The ${title}.pbm`)
45
+
46
+ const data = {
47
+ title,
48
+ description: '',
49
+ author,
50
+ email,
51
+ type: 'std',
52
+ proglangs: ['cpp', 'py'],
53
+ languages: ['en', 'ca', 'es'],
54
+ model: modelsChoices[0]!.value,
55
+ outputDir,
56
+ }
57
+
58
+ return data
59
+ }
60
+
61
+ async function updateDataFromUser(data: ProblemData) {
62
+ data.author = await input({ message: 'Author:', default: data.author })
63
+ data.email = await input({ message: 'Author email:', default: data.email })
64
+ data.title = await input({ message: 'Problem title:', default: data.title })
65
+ data.description = await input({ message: 'Problem description:', default: data.description })
66
+
67
+ const checkedProglangsChoices = proglangsChoices.map((choice) => ({
68
+ ...choice,
69
+ checked: data.proglangs.includes(choice.value),
70
+ }))
71
+ data.proglangs = await checkbox({
72
+ message: 'Select programming languages for solutions:',
73
+ choices: checkedProglangsChoices,
74
+ required: true,
75
+ })
76
+
77
+ const checkedLanguagesChoices = languagesChoices.map((choice) => ({
78
+ ...choice,
79
+ checked: data.languages.includes(choice.value),
80
+ }))
81
+ data.languages = await checkbox({
82
+ message: 'Select languages for statements:',
83
+ choices: checkedLanguagesChoices,
84
+ required: true,
85
+ })
86
+
87
+ data.model = await select({
88
+ message: 'Select an AI model:',
89
+ choices: modelsChoices,
90
+ default: data.model || modelsChoices[0]!.value,
91
+ })
92
+ }
93
+
94
+ export async function createProblemWithJutgeAI(outputDir: string): Promise<void> {
95
+ tui.warning('JutgeAI can only create std problems for now.')
96
+
97
+ const data = await getInitialData(outputDir)
98
+ await updateDataFromUser(data)
99
+ console.log(data)
100
+ await generateProblemWithJutgeAI(data)
101
+ }
@@ -0,0 +1,51 @@
1
+ import { paths } from '@/lib/settings'
2
+ import * as tui from '@/lib/tui.js'
3
+ import { confirm, select, Separator } from '@inquirer/prompts'
4
+ import { cp } from 'fs/promises'
5
+ import { join } from 'path'
6
+ import { title } from 'radash'
7
+
8
+ async function chooseTemplate(): Promise<string> {
9
+ // build the choices array
10
+ const choices = []
11
+ const path = join(paths.data, 'assets', 'problems')
12
+ const dirsGlob = new Bun.Glob('*')
13
+ for await (const dir of dirsGlob.scan({ cwd: path, onlyFiles: false })) {
14
+ choices.push(new Separator(`◇ ${title(dir)}:`))
15
+ const problemsGlob = new Bun.Glob('*')
16
+ for await (const problem of problemsGlob.scan({ cwd: join(path, dir), onlyFiles: false })) {
17
+ const readme = await Bun.file(join(path, dir, problem, 'README.md')).text()
18
+ const name = // Extract title from README.md
19
+ ' ' +
20
+ readme
21
+ .split('\n')
22
+ .filter((line) => line.startsWith('#'))[0]
23
+ ?.replace('#', '')
24
+ .trim() || 'No description'
25
+ choices.push({ name, value: join(dir, problem) })
26
+ }
27
+ }
28
+
29
+ while (true) {
30
+ const template = await select({ choices, message: 'Select a problem template:', loop: false })
31
+
32
+ const readme = await Bun.file(join(path, template, 'README.md')).text()
33
+ await tui.markdown(readme)
34
+ console.log()
35
+
36
+ const confirmation = await confirm({
37
+ message: `Use this template?`,
38
+ default: true,
39
+ })
40
+ if (confirmation) return template
41
+ }
42
+ }
43
+
44
+ export async function createProblemWithTemplate(outputDir: string): Promise<void> {
45
+ const template = await chooseTemplate()
46
+
47
+ const source = join(paths.data, 'assets', 'problems', template)
48
+ tui.action(`Creating new problem from template ${template}`)
49
+ await cp(source, outputDir, { recursive: true })
50
+ tui.success(`Problem created at ${outputDir}`)
51
+ }
@@ -0,0 +1,4 @@
1
+ export async function createProblemWithWizard(outputDir: string): Promise<void> {
2
+ await Bun.sleep(0) // to ignore warning
3
+ console.error('Wizard method is not yet implemented.')
4
+ }
@@ -0,0 +1,75 @@
1
+ import * as tui from '@/lib/tui.js'
2
+ import { Command } from '@commander-js/extra-typings'
3
+ import { confirm, input, select } from '@inquirer/prompts'
4
+ import { exists, mkdir, rm } from 'fs/promises'
5
+ import { normalize } from 'path'
6
+ import { createProblemWithJutgeAI } from './create-jutge-ai'
7
+ import { createProblemWithTemplate } from './create-template'
8
+ import { createProblemWithWizard } from './create-wizard'
9
+
10
+ async function selectMethod(): Promise<'template' | 'wizard' | 'jutgeAI'> {
11
+ return await select({
12
+ message: 'Method to create a new problem:',
13
+ choices: [
14
+ { name: 'Use a template', value: 'template' },
15
+ { name: 'Use the wizard', value: 'wizard' },
16
+ { name: 'Use JutgeAI', value: 'jutgeAI' },
17
+ ],
18
+ })
19
+ }
20
+
21
+ async function selectOutputDir(): Promise<string> {
22
+ let dir = 'my-new-problem.pbm'
23
+ while (true) {
24
+ dir = await input({
25
+ message: 'Output directory for the new problem:',
26
+ default: dir,
27
+ })
28
+ dir = normalize(dir)
29
+ if (await exists(dir)) {
30
+ tui.error(`Directory ${dir} already exists.`)
31
+ const remove = await confirm({
32
+ message: 'Remove it?',
33
+ default: false,
34
+ })
35
+ if (!remove) continue
36
+ try {
37
+ tui.action(`Removing directory ${dir}`)
38
+ await rm(dir, { recursive: true, force: true })
39
+ } catch (err) {
40
+ tui.warning(`Failed to remove directory ${dir}. Please try again.`)
41
+ continue
42
+ }
43
+ tui.success(`Removed directory ${dir}`)
44
+ }
45
+ if (!dir.endsWith('.pbm')) {
46
+ tui.warning("The output directory must end with the '.pbm' extension. Please try again.")
47
+ dir += '.pbm'
48
+ continue
49
+ }
50
+ try {
51
+ await mkdir(dir, { recursive: true })
52
+ } catch (err) {
53
+ tui.warning(`Failed to create directory ${dir}. Please try again.`)
54
+ continue
55
+ }
56
+ tui.success(`Created directory ${dir}`)
57
+ return dir
58
+ }
59
+ }
60
+
61
+ export const create = new Command('create')
62
+ .description('Create a new problem')
63
+
64
+ .action(async () => {
65
+ tui.title('Create new problem')
66
+ const outputDir = await selectOutputDir()
67
+ const method = await selectMethod()
68
+ if (method === 'template') {
69
+ await createProblemWithTemplate(outputDir)
70
+ } else if (method === 'wizard') {
71
+ await createProblemWithWizard(outputDir)
72
+ } else if (method === 'jutgeAI') {
73
+ await createProblemWithJutgeAI(outputDir)
74
+ }
75
+ })
@@ -0,0 +1,17 @@
1
+ import * as doc from '@/lib/doctor'
2
+ import { Command } from '@commander-js/extra-typings'
3
+ import * as tui from '@/lib/tui.js'
4
+
5
+ export const doctor = new Command('doctor')
6
+ .description('Diagnose and fix common issues with the project setup')
7
+
8
+ .action(async () => {
9
+ tui.title('Doctor')
10
+
11
+ await tui.section('Checking Python3 installation', doc.checkPython3)
12
+ await tui.section('Checking C/C++ installation', doc.checkGCC)
13
+ await tui.section('Checking LaTeX installation', doc.checkLaTeX)
14
+ await tui.section('Checking Pandoc installation', doc.checkPandoc)
15
+ await tui.section('Checking ImageMagick installation', doc.checkImageMagick)
16
+ await tui.section('Checking environment variables', doc.checkEnvVars)
17
+ })
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { program } from '@commander-js/extra-typings'
4
+ import { ai } from './ai'
5
+ import { compilers } from './compilers'
6
+ import { make } from './make'
7
+ import { clean } from './clean'
8
+ import { create } from './create'
9
+ import { doctor } from './doctor'
10
+ import { init } from './init'
11
+ import { verify } from './verify'
12
+
13
+ program.name('jutge-toolkit')
14
+
15
+ program.description('Toolkit to prepare problems for Jutge.org')
16
+
17
+ program.version('3.0.0')
18
+
19
+ program.addCommand(init)
20
+ program.addCommand(create)
21
+ program.addCommand(make)
22
+ program.addCommand(verify)
23
+ program.addCommand(clean)
24
+ program.addCommand(compilers)
25
+ program.addCommand(doctor)
26
+ program.addCommand(ai)
27
+
28
+ await program.parseAsync()
@@ -0,0 +1,66 @@
1
+ import { decompressAssets } from '@/lib/assets'
2
+ import { initializePaths, loadSettings, paths, saveSettings, settingsExist } from '@/lib/settings'
3
+ import { guessUserEmail, guessUserName } from '@/lib/utils'
4
+ import { Command } from '@commander-js/extra-typings'
5
+ import { input, select } from '@inquirer/prompts'
6
+ import chalk from 'chalk'
7
+ import * as tui from '@/lib/tui.js'
8
+
9
+ async function initialize() {
10
+ await tui.section('Initialize', async () => {
11
+ let name = (await guessUserName()) || 'John Doe'
12
+ let email = (await guessUserEmail()) || 'john.doe@example.com'
13
+ name = await input({ message: 'What is your name?', default: name })
14
+ email = await input({ message: 'What is your email?', default: email })
15
+ const notifications = true
16
+
17
+ await initializePaths()
18
+ await saveSettings({ name, email, notifications })
19
+
20
+ tui.success('Settings created')
21
+ })
22
+ }
23
+
24
+ async function configure() {
25
+ await tui.section('Configure', async () => {
26
+ const settings = await loadSettings()
27
+
28
+ const name = await input({ message: 'What is your name?', default: settings.name })
29
+ const email = await input({ message: 'What is your email?', default: settings.email })
30
+ const notificationsInput = await select({
31
+ message: 'Enable notifications?',
32
+ choices: [
33
+ { name: 'Yes', value: 'yes' },
34
+ { name: 'No', value: 'no' },
35
+ ],
36
+ default: settings.notifications ? 'yes' : 'no',
37
+ })
38
+ const notifications = notificationsInput === 'yes'
39
+
40
+ await saveSettings({ name, email, notifications })
41
+
42
+ tui.success('Settings updated')
43
+ })
44
+ }
45
+
46
+ export const init = new Command('init')
47
+ .description('Initialize/configure the toolkit')
48
+
49
+ .action(async () => {
50
+ if (await settingsExist()) {
51
+ await configure()
52
+ } else {
53
+ await initialize()
54
+ }
55
+
56
+ await decompressAssets()
57
+
58
+ await tui.section('Directories', async () => {
59
+ await Bun.sleep(0) // hide lint warning
60
+ console.log('config:', chalk.magenta(paths.config))
61
+ console.log('data: ', chalk.magenta(paths.data))
62
+ console.log('cache: ', chalk.magenta(paths.cache))
63
+ console.log('log: ', chalk.magenta(paths.log))
64
+ console.log('temp: ', chalk.magenta(paths.temp))
65
+ })
66
+ })
@@ -0,0 +1,60 @@
1
+ import { newMaker, type MakerOptions } from '@/lib/maker'
2
+ import { Command } from '@commander-js/extra-typings'
3
+
4
+ export const make = new Command('make')
5
+ .description('Make problem components')
6
+ // Add common options here
7
+ .option('-d, --directory <path>', 'problem directory', process.cwd()) // TODO: use '.' when bun fixes it
8
+ .option('-v, --verbose', 'verbose output (TODO)')
9
+
10
+ .action(async (options) => {
11
+ const maker = await newMaker(options)
12
+ await maker.makeProblem()
13
+ })
14
+
15
+ make.command('problem')
16
+ .description('Make problem (default)')
17
+ .action(async (options, command) => {
18
+ const parentOptions = (command.parent?.opts() || {}) as any as MakerOptions
19
+ const maker = await newMaker(parentOptions)
20
+ await maker.makeProblem()
21
+ })
22
+
23
+ make.command('inspection')
24
+ .description('Make inspection')
25
+ .action(async (options, command) => {
26
+ const parentOptions = (command.parent?.opts() || {}) as any as MakerOptions
27
+ const maker = await newMaker(parentOptions)
28
+ })
29
+
30
+ make.command('executables')
31
+ .description('Make executables')
32
+ .action(async (options, command) => {
33
+ const parentOptions = (command.parent?.opts() || {}) as any as MakerOptions
34
+ const maker = await newMaker(parentOptions)
35
+ await maker.makeExecutables()
36
+ })
37
+
38
+ make.command('corrects')
39
+ .description('Make corrects')
40
+ .action(async (options, command) => {
41
+ const parentOptions = (command.parent?.opts() || {}) as any as MakerOptions
42
+ const maker = await newMaker(parentOptions)
43
+ await maker.makeCorrects()
44
+ })
45
+
46
+ make.command('pdf')
47
+ .description('Make PDF statements')
48
+ .action(async (options, command) => {
49
+ const parentOptions = (command.parent?.opts() || {}) as any as MakerOptions
50
+ const maker = await newMaker(parentOptions)
51
+ await maker.makePdfs()
52
+ })
53
+
54
+ make.command('text')
55
+ .description('Make text statements (HTML, TXT, MD)')
56
+ .action(async (options, command) => {
57
+ const parentOptions = (command.parent?.opts() || {}) as any as MakerOptions
58
+ const maker = await newMaker(parentOptions)
59
+ await maker.makeTexts()
60
+ })
@@ -0,0 +1,19 @@
1
+ import { Command } from '@commander-js/extra-typings'
2
+ import { newMaker, type MakerOptions } from '@/lib/maker'
3
+ import { normalize } from 'path'
4
+
5
+ export const verify = new Command('verify')
6
+ .description('Verify a candidate program')
7
+ // Add common options here
8
+
9
+ .argument('<programs...>', 'source programs to verify')
10
+ .option('-d, --directory <path>', 'problem directory', process.cwd()) // TODO: use '.' when bun fixes it
11
+ .option('-v, --verbose', 'verbose output (TODO)')
12
+
13
+ .action(async (programs, { directory, verbose }) => {
14
+ directory = normalize(directory)
15
+ const maker = await newMaker({ verbose, directory })
16
+ for (const program of programs) {
17
+ await maker.verifyCandidate(program)
18
+ }
19
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "compilerOptions": {
3
+ // Environment setup & latest features
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "module": "Preserve",
7
+ "moduleDetection": "force",
8
+ "jsx": "react-jsx",
9
+ "allowJs": true,
10
+
11
+ // Bundler mode
12
+ "moduleResolution": "bundler",
13
+ "allowImportingTsExtensions": true,
14
+ "verbatimModuleSyntax": true,
15
+ "noEmit": true,
16
+
17
+ // Best practices
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+ "noImplicitOverride": true,
23
+
24
+ // Some stricter flags (disabled by default)
25
+ "noUnusedLocals": false,
26
+ "noUnusedParameters": false,
27
+ "noPropertyAccessFromIndexSignature": false,
28
+
29
+ // Path aliases
30
+ "baseUrl": ".",
31
+ "paths": {
32
+ "@/*": ["*"]
33
+ },
34
+
35
+ // Types
36
+ "types": ["bun-types"]
37
+ }
38
+ }
package/types/zip.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare module '*.zip' {
2
+ const content: ArrayBuffer
3
+ export default content
4
+ }