@jpetit/toolkit 3.0.23 → 3.1.2

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 (61) hide show
  1. package/assets/prompts/creators/create-solution.tpl.txt +10 -0
  2. package/assets/prompts/creators/create-statement.tpl.txt +21 -0
  3. package/assets/prompts/creators/create-translation.tpl.txt +5 -0
  4. package/assets/prompts/creators/private-test-cases.txt +6 -0
  5. package/assets/prompts/creators/sample-test-cases.txt +6 -0
  6. package/assets/prompts/creators/system-prompt.txt +2 -0
  7. package/assets/prompts/examples/statement-coda.tex +7 -0
  8. package/assets/prompts/examples/statement.tex +19 -0
  9. package/assets/prompts/generators/efficiency.md +41 -0
  10. package/assets/prompts/generators/hard.md +47 -0
  11. package/assets/prompts/generators/random.md +39 -0
  12. package/assets/prompts/proglangs/cc.md +3 -0
  13. package/assets/prompts/proglangs/py.md +40 -0
  14. package/lib/ai.ts +60 -4
  15. package/lib/cleaner.ts +24 -13
  16. package/lib/compilers/base.ts +70 -14
  17. package/lib/compilers/clojure.ts +21 -10
  18. package/lib/compilers/gcc.ts +4 -33
  19. package/lib/compilers/ghc.ts +4 -40
  20. package/lib/compilers/gxx.ts +4 -33
  21. package/lib/compilers/index.ts +9 -0
  22. package/lib/compilers/java.ts +105 -0
  23. package/lib/compilers/python3.ts +44 -37
  24. package/lib/compilers/run-clojure.ts +101 -0
  25. package/lib/compilers/run-haskell.ts +26 -22
  26. package/lib/compilers/run-python.ts +29 -35
  27. package/lib/compilers/rust.ts +39 -0
  28. package/lib/create-with-jutgeai.ts +407 -0
  29. package/lib/create-with-template.ts +55 -0
  30. package/lib/data.ts +6 -0
  31. package/lib/doctor.ts +86 -6
  32. package/lib/generate.ts +132 -290
  33. package/lib/helpers.ts +48 -0
  34. package/lib/inspector.ts +253 -0
  35. package/lib/jutge_api_client.ts +4631 -0
  36. package/lib/maker.ts +202 -289
  37. package/lib/settings.ts +26 -17
  38. package/lib/tui.ts +25 -15
  39. package/lib/types.ts +40 -5
  40. package/lib/upload.ts +216 -0
  41. package/lib/utils.ts +82 -14
  42. package/lib/versions.ts +46 -0
  43. package/package.json +50 -11
  44. package/toolkit/about.ts +43 -0
  45. package/toolkit/ai.ts +44 -18
  46. package/toolkit/check.ts +16 -0
  47. package/toolkit/clean.ts +16 -26
  48. package/toolkit/compilers.ts +4 -4
  49. package/toolkit/config.ts +91 -0
  50. package/toolkit/create.ts +30 -58
  51. package/toolkit/doctor.ts +15 -11
  52. package/toolkit/generate.ts +195 -98
  53. package/toolkit/index.ts +32 -21
  54. package/toolkit/make.ts +12 -48
  55. package/toolkit/upgrade.ts +9 -0
  56. package/toolkit/upload.ts +19 -0
  57. package/toolkit/create-jutge-ai.ts +0 -101
  58. package/toolkit/create-template.ts +0 -55
  59. package/toolkit/create-wizard.ts +0 -6
  60. package/toolkit/init.ts +0 -56
  61. package/toolkit/verify.ts +0 -19
@@ -1,10 +1,10 @@
1
- import tui from '../tui'
2
1
  import { execa } from 'execa'
3
- import { cp, exists, rm } from 'fs/promises'
4
- import { join } from 'path'
5
- import { readText, writeText } from '../utils'
6
- import { Compiler } from './base'
2
+ import { cp } from 'fs/promises'
3
+ import { join, parse } from 'path'
4
+ import tui from '../tui'
7
5
  import type { Handler } from '../types'
6
+ import { nothing, readText, toolkitPrefix, writeText } from '../utils'
7
+ import { Compiler } from './base'
8
8
 
9
9
  export class RunPython_Compiler extends Compiler {
10
10
  id(): string {
@@ -35,30 +35,34 @@ export class RunPython_Compiler extends Compiler {
35
35
  return ''
36
36
  }
37
37
 
38
+ tool(): string {
39
+ return 'python3'
40
+ }
41
+
38
42
  extension(): string {
39
43
  return 'py'
40
44
  }
41
45
 
42
- async compile(handler: Handler, directory: string, sourcePath: string): Promise<void> {
43
- const exePath = sourcePath + '.exe'
44
-
45
- if (handler.source_modifier === 'none') {
46
- tui.command(`cp ${sourcePath} ${exePath}`)
47
- await cp(join(directory, sourcePath), join(directory, exePath))
48
- } else {
49
- throw new Error(`Unknown source modifier: ${handler.source_modifier as string}`)
50
- }
51
-
52
- tui.command(`python3 -m py_compile ${exePath}`)
46
+ override async compileNormal(handler: Handler, directory: string, sourcePath: string): Promise<string> {
47
+ tui.command(`python3 -m py_compile ${sourcePath}`)
53
48
 
54
49
  const { exitCode } = await execa({
55
50
  reject: false,
56
51
  stderr: 'inherit',
57
52
  stdout: 'inherit',
58
53
  cwd: directory,
59
- })`python3 -m py_compile ${exePath}`
54
+ })`python3 -m py_compile ${sourcePath}`
60
55
 
61
- if (exitCode !== 0) throw new Error(`Compilation failed for ${sourcePath}`)
56
+ if (exitCode !== 0) {
57
+ throw new Error(`Compilation failed for ${sourcePath}`)
58
+ }
59
+
60
+ return sourcePath
61
+ }
62
+
63
+ override async compileWithMain(handler: Handler, directory: string, sourcePath: string): Promise<string> {
64
+ await nothing()
65
+ throw new Error('Method not implemented.')
62
66
  }
63
67
 
64
68
  override async execute(
@@ -68,31 +72,21 @@ export class RunPython_Compiler extends Compiler {
68
72
  inputPath: string,
69
73
  outputPath: string,
70
74
  ): Promise<void> {
71
- const exePath = 'solution.py.exe'
72
- if (!(await exists(join(directory, exePath)))) {
73
- throw new Error(`Executable file ${exePath} does not exist in directory ${directory}`)
74
- }
75
-
76
- const mergedPath = `solution-${inputPath}.py.exe`
77
-
78
- tui.command(`Merging ${exePath} and ${inputPath} into ${mergedPath}`, { italic: true })
79
-
80
- await this.mergeScripts(directory, exePath, inputPath, mergedPath)
81
-
82
- const fullOutputPath = join(directory, outputPath)
75
+ const newSourcePath = `${toolkitPrefix()}-${parse(sourcePath).name}-${parse(inputPath).name}.py`
83
76
 
84
- await rm(fullOutputPath, { force: true })
77
+ tui.command(`merge ${sourcePath} ${inputPath} > ${newSourcePath}`)
78
+ await this.mergeScripts(directory, sourcePath, inputPath, newSourcePath)
85
79
 
86
- tui.command(`python3 ${mergedPath} > ${outputPath}`)
80
+ tui.command(`python3 ${newSourcePath} > ${outputPath}`)
87
81
 
88
82
  const { exitCode } = await execa({
89
83
  reject: false,
90
- stdout: { file: fullOutputPath },
84
+ stdout: { file: join(directory, outputPath) },
91
85
  stderr: 'inherit',
92
86
  cwd: directory,
93
- })`python3 ${mergedPath}`
87
+ })`python3 ${newSourcePath}`
94
88
 
95
- if (exitCode !== 0) throw new Error(`Execution failed for ${mergedPath}`)
89
+ if (exitCode !== 0) throw new Error(`Execution failed for ${newSourcePath}`)
96
90
  }
97
91
 
98
92
  async mergeScripts(
@@ -0,0 +1,39 @@
1
+ import { Compiler } from './base'
2
+
3
+ export class Rust_Compiler extends Compiler {
4
+ id(): string {
5
+ return 'Rust'
6
+ }
7
+
8
+ name(): string {
9
+ return 'Rust Compiler'
10
+ }
11
+
12
+ type(): string {
13
+ return 'compiler'
14
+ }
15
+
16
+ language(): string {
17
+ return 'Rust'
18
+ }
19
+
20
+ async version(): Promise<string> {
21
+ return await this.getVersion('rustc --version', 0)
22
+ }
23
+
24
+ flags1(): string {
25
+ return '-C opt-level=2 -D warnings'
26
+ }
27
+
28
+ flags2(): string {
29
+ return '-C opt-level=2 -D warnings'
30
+ }
31
+
32
+ tool(): string {
33
+ return 'rustc'
34
+ }
35
+
36
+ extension(): string {
37
+ return 'rs'
38
+ }
39
+ }
@@ -0,0 +1,407 @@
1
+ import * as prompts from '@inquirer/prompts'
2
+ import { exists, mkdir } from 'fs/promises'
3
+ import Handlebars from 'handlebars'
4
+ import checkboxPlus from 'inquirer-checkbox-plus-plus'
5
+ import { join } from 'path'
6
+ import YAML from 'yaml'
7
+ import { ChatBot, cleanMardownCodeString, estimatePowerConsumption } from './ai'
8
+ import { languageNames, proglangNames } from './data'
9
+ import { getPromptForProglang, getTitleFromStatement } from './helpers'
10
+ import { settings } from './settings'
11
+ import tui from './tui'
12
+ import { Specification } from './types'
13
+ import { nothing, projectDir, readText, readYaml, writeTextInDir, writeYaml, writeYamlInDir } from './utils'
14
+
15
+ const promptsDir = join(projectDir(), 'assets', 'prompts')
16
+
17
+ const latexExample = await readText(join(promptsDir, 'examples', 'statement.tex'))
18
+ const statementCoda = await readText(join(promptsDir, 'examples', 'statement-coda.tex'))
19
+ const systemPrompt = await readText(join(promptsDir, 'creators', 'system-prompt.txt'))
20
+ const statementPromptTemplate = await readText(join(promptsDir, 'creators', 'create-statement.tpl.txt'))
21
+ const translationPromptTemplate = await readText(join(promptsDir, 'creators', 'create-translation.tpl.txt'))
22
+ const solutionPromptTemplate = await readText(join(promptsDir, 'creators', 'create-solution.tpl.txt'))
23
+ const sampleTestCasesPrompt = await readText(join(promptsDir, 'creators', 'sample-test-cases.txt'))
24
+ const privateTestCasesPrompt = await readText(join(promptsDir, 'creators', 'private-test-cases.txt'))
25
+
26
+ export async function createProblemWithJutgeAI(
27
+ model: string,
28
+ directory: string,
29
+ inputPath: string | undefined,
30
+ outputPath: string | undefined,
31
+ doNotAsk: boolean,
32
+ ) {
33
+ if (await exists(directory)) {
34
+ throw new Error(`Directory ${directory} already exists`)
35
+ }
36
+ if (!directory.endsWith('.pbm')) {
37
+ throw new Error('The output directory must end with .pbm')
38
+ }
39
+
40
+ const spec = await getSpecification(inputPath, outputPath, doNotAsk)
41
+ const generator = new ProblemGenerator(spec, model)
42
+ await generator.run()
43
+ await generator.save(directory)
44
+ tui.success(`Created problem ${tui.hyperlink(directory)}`)
45
+ }
46
+
47
+ async function getSpecification(
48
+ inputPath: string | undefined,
49
+ outputPath: string | undefined,
50
+ doNotAsk: boolean,
51
+ ): Promise<Specification> {
52
+ let spec: Specification
53
+ if (inputPath) {
54
+ tui.action(`Reading specification from ${inputPath}`)
55
+ const data = await readYaml(inputPath)
56
+ spec = Specification.parse(data)
57
+ } else {
58
+ doNotAsk = true
59
+ spec = {
60
+ title: 'Your new problem title',
61
+ description: 'Describe the task of the problem here.',
62
+ author: settings.name || 'Your Name',
63
+ email: settings.email || 'email@example.com',
64
+ golden_proglang: 'cc',
65
+ more_proglangs: ['py'],
66
+ original_language: 'en',
67
+ more_languages: ['ca', 'es'],
68
+ generators: ['hard', 'random', 'efficiency'],
69
+ }
70
+ }
71
+
72
+ if (doNotAsk) {
73
+ return spec
74
+ }
75
+
76
+ while (true) {
77
+ spec.title = await prompts.input({ message: 'Title:', default: spec.title, prefill: 'editable' })
78
+
79
+ spec.description = await prompts.input({
80
+ message: 'Description:',
81
+ default: spec.description,
82
+ prefill: 'editable',
83
+ })
84
+
85
+ spec.author = await prompts.input({ message: 'Author:', default: spec.author, prefill: 'editable' })
86
+
87
+ spec.email = await prompts.input({ message: 'Email:', default: spec.email, prefill: 'editable' })
88
+
89
+ spec.original_language = await prompts.select({
90
+ message: `Language for original version:`,
91
+ choices: Object.entries(languageNames).map(([value, name]) => ({ value, name })),
92
+ default: spec.original_language,
93
+ })
94
+
95
+ spec.more_languages = await checkboxPlus({
96
+ message: `Other languages:`,
97
+ searchable: false,
98
+ // eslint-disable-next-line @typescript-eslint/require-await
99
+ source: async (answers, input) =>
100
+ Object.entries(languageNames)
101
+ .filter(([value]) => value !== spec.original_language)
102
+ .map(([value, name]) => ({ value, name })),
103
+ default: spec.more_languages,
104
+ loop: false,
105
+ })
106
+
107
+ spec.golden_proglang = await prompts.select({
108
+ message: `Programming language for golden solution:`,
109
+ choices: Object.entries(proglangNames).map(([value, name]) => ({ value, name })),
110
+ default: spec.golden_proglang,
111
+ })
112
+
113
+ spec.more_proglangs = await checkboxPlus({
114
+ message: `Other programming languages for solutions:`,
115
+ // eslint-disable-next-line @typescript-eslint/require-await
116
+ source: async (answers, input) =>
117
+ Object.entries(proglangNames)
118
+ .filter(([value]) => value !== spec.golden_proglang)
119
+ .map(([value, name]) => ({ value, name })),
120
+ default: spec.more_proglangs,
121
+ })
122
+
123
+ spec.generators = await checkboxPlus({
124
+ message: `Test case generators:`,
125
+ // eslint-disable-next-line @typescript-eslint/require-await
126
+ source: async (answers, input) => [
127
+ { value: 'random', name: 'Random' },
128
+ { value: 'hard', name: 'Hard' },
129
+ { value: 'efficiency', name: 'Efficiency' },
130
+ ],
131
+ default: spec.generators,
132
+ })
133
+
134
+ const action = await prompts.select({
135
+ message: 'Choose an action:',
136
+ choices: [
137
+ {
138
+ value: 'confirm',
139
+ name: 'Confirm',
140
+ description: 'Confirm specification and proceed to problem creation',
141
+ },
142
+ { value: 'edit', name: 'Edit', description: 'Edit the specification' },
143
+ { value: 'cancel', name: 'Cancel', description: 'Cancel problem creation' },
144
+ ],
145
+ })
146
+ if (action === 'confirm') break
147
+ if (action === 'cancel') {
148
+ tui.print('Problem creation cancelled')
149
+ process.exit(0)
150
+ }
151
+ }
152
+
153
+ if (outputPath) {
154
+ await writeYaml(outputPath, spec)
155
+ tui.success(`Specification file written at ${tui.hyperlink(outputPath)}`)
156
+ }
157
+
158
+ return spec
159
+ }
160
+
161
+ class ProblemGenerator {
162
+ private model: string
163
+ private spec: Specification
164
+ private bot: ChatBot
165
+
166
+ // generated problem parts
167
+ private problemStatement: string = ''
168
+ private problemMoreStatements: Record<string, string> = {}
169
+ private problemMoreSolutions: Record<string, string> = {}
170
+ private problemGenerators: Record<string, string> = {}
171
+ private problemSampleTests1: string = ''
172
+ private problemSampleTests2: string = ''
173
+ private problemPrivateTests1: string = ''
174
+ private problemPrivateTests2: string = ''
175
+ private problemReadme: string = ''
176
+
177
+ constructor(info: Specification, model: string) {
178
+ this.model = model
179
+ this.spec = info
180
+ this.bot = new ChatBot(model, systemPrompt)
181
+ }
182
+
183
+ async generateStatement(): Promise<string> {
184
+ return await tui.section(
185
+ `Generating problem statement in ${languageNames[this.spec.original_language]}`,
186
+ async () => {
187
+ const statementPrompt = Handlebars.compile(statementPromptTemplate)({
188
+ language: languageNames[this.spec.original_language],
189
+ latexExample,
190
+ title: this.spec.title,
191
+ description: this.spec.description,
192
+ })
193
+ const answer = await this.bot.complete(statementPrompt)
194
+ return cleanMardownCodeString(answer) + statementCoda
195
+ },
196
+ )
197
+ }
198
+
199
+ async generateSampleTests(): Promise<string> {
200
+ return await tui.section('Generating sample test cases', async () => {
201
+ const answer = await this.bot.complete(sampleTestCasesPrompt)
202
+ return cleanMardownCodeString(answer)
203
+ })
204
+ }
205
+
206
+ async generatePrivateTests(): Promise<string> {
207
+ return await tui.section('Generating private test cases', async () => {
208
+ const answer = await this.bot.complete(privateTestCasesPrompt)
209
+ return cleanMardownCodeString(answer)
210
+ })
211
+ }
212
+
213
+ async generateSolutions(): Promise<Record<string, string>> {
214
+ return await tui.section('Generating solutions', async () => {
215
+ const solutions: Record<string, string> = {}
216
+ for (const proglang of this.spec.more_proglangs.concat([this.spec.golden_proglang]).reverse()) {
217
+ await tui.section(`Generating solution in ${proglangNames[proglang]}`, async () => {
218
+ const proglangPrompt = await getPromptForProglang(proglang)
219
+ const solutionPrompt = Handlebars.compile(solutionPromptTemplate)({
220
+ proglang: proglangNames[proglang],
221
+ proglangPrompt,
222
+ })
223
+ const answer = await this.bot.complete(solutionPrompt)
224
+ solutions[proglang] = cleanMardownCodeString(answer)
225
+ this.bot.forgetLastInteraction()
226
+ })
227
+ }
228
+ return solutions
229
+ })
230
+ }
231
+
232
+ async translateStatements(): Promise<Record<string, string>> {
233
+ return await tui.section('Translating problem statements', async () => {
234
+ const translations: Record<string, string> = {}
235
+ for (const language of this.spec.more_languages.sort()) {
236
+ await tui.section(`Translating to ${languageNames[language]}`, async () => {
237
+ const translationPrompt = Handlebars.compile(translationPromptTemplate)({
238
+ language: languageNames[language],
239
+ })
240
+ const answer = await this.bot.complete(translationPrompt)
241
+ translations[language] = cleanMardownCodeString(answer) + statementCoda
242
+ this.bot.forgetLastInteraction()
243
+ })
244
+ }
245
+ return translations
246
+ })
247
+ }
248
+
249
+ async generateGenerators(): Promise<Record<string, string>> {
250
+ return await tui.section('Generating test cases generators', async () => {
251
+ const generators: Record<string, string> = {}
252
+ for (const type of this.spec.generators) {
253
+ await tui.section(`Generating ${type} test cases generator`, async () => {
254
+ const statement = this.problemStatement
255
+ const promptPath = join(projectDir(), 'assets', 'prompts', 'generators', `${type}.md`)
256
+ const promptTemplate = await readText(promptPath)
257
+ const prompt = Handlebars.compile(promptTemplate)({ statement })
258
+ const answer = cleanMardownCodeString(await this.bot.complete(prompt))
259
+ generators[type] = answer
260
+ })
261
+ }
262
+ return generators
263
+ })
264
+ }
265
+
266
+ async generateReadme(): Promise<string> {
267
+ return tui.section('Generating README.md', async () => {
268
+ await nothing()
269
+ const readme = `
270
+ # Problem information
271
+
272
+ This programming problem for Jutge.org was generated by Jutge<sup>AI</sup> through the Jutge.org API using ${this.model} and a prompt by ${this.spec.author}.
273
+
274
+ **Warning**: This problem may contain inaccuracies or errors. Review the problem statements, test cases, and solutions carefully before using them in a real setting. Output tests and statement PDFs have not been generated, use \`jutge-make-problem\` to generate them.
275
+
276
+ ## Author
277
+
278
+ ${this.spec.author}
279
+
280
+ ## Problem title
281
+
282
+ ${this.spec.title}
283
+
284
+ ## Problem description
285
+
286
+ ${this.spec.description}
287
+
288
+ ## Generated solutions
289
+
290
+ - ${proglangNames[this.spec.golden_proglang]} (golden solution)
291
+ ${listify(this.spec.more_proglangs.map((proglang) => proglangNames[proglang]))}
292
+
293
+ ## Generated languages
294
+
295
+ - ${languageNames[this.spec.original_language]} (original)
296
+ ${listify(this.spec.more_languages.map((language) => languageNames[language]))}
297
+
298
+ ## Generated test case generators
299
+
300
+ ${listify(this.spec.generators)}
301
+
302
+ ## Problem specification
303
+
304
+ \`\`\`yaml
305
+ ${YAML.stringify(this.spec, null, 4)}
306
+ \`\`\`
307
+
308
+ ## Model information
309
+
310
+ \`\`\`yaml
311
+ ${YAML.stringify(this.bot.modelInformation(), null, 2)}
312
+ \`\`\`
313
+
314
+ ## Estimated cost
315
+
316
+ The following information is based on estimations from token counts and do not reflect the actual costs incurred. Using GPT-5 pricing as reference.
317
+
318
+ - Total input tokens: ${this.bot.totalInputTokens}
319
+ - Total output tokens: ${this.bot.totalOutputTokens}
320
+ - Total input cost: ${this.bot.totalInputCost.toFixed(6)} USD
321
+ - Total output cost: ${this.bot.totalOutputCost.toFixed(6)} USD
322
+ - Total estimated cost: ${(this.bot.totalInputCost + this.bot.totalOutputCost).toFixed(6)} USD
323
+ - Energy: ${estimatePowerConsumption(this.bot.totalInputTokens, this.bot.totalOutputTokens).wattHours.toFixed(6)} Wh
324
+ - CO₂ emissions: ${estimatePowerConsumption(this.bot.totalInputTokens, this.bot.totalOutputTokens).co2Grams.toFixed(6)} g CO₂
325
+
326
+ `
327
+ return readme
328
+ })
329
+ }
330
+
331
+ async run() {
332
+ await tui.section('Generating problem with JutgeAI', async () => {
333
+ await this.bot.init()
334
+ this.problemStatement = await this.generateStatement()
335
+ this.problemSampleTests1 = await this.generateSampleTests()
336
+ this.bot.forgetLastInteraction() // forget first sample tests
337
+ this.problemSampleTests2 = await this.generateSampleTests()
338
+ this.problemPrivateTests1 = await this.generatePrivateTests()
339
+ this.bot.forgetLastInteraction() // forget first private tests
340
+ this.problemPrivateTests2 = await this.generatePrivateTests()
341
+ this.problemMoreSolutions = await this.generateSolutions() // these are forgotten inside
342
+ this.bot.forgetLastInteraction() // forget private tests
343
+ this.bot.forgetLastInteraction() // forget sample tests
344
+ this.problemMoreStatements = await this.translateStatements()
345
+ this.bot.forgetLastInteraction() // forget translations
346
+ this.problemGenerators = await this.generateGenerators() // these are forgotten inside
347
+ this.problemReadme = await this.generateReadme()
348
+ })
349
+ }
350
+
351
+ async save(path: string) {
352
+ await tui.section(`Saving problem to ${path}`, async () => {
353
+ await mkdir(path, { recursive: true })
354
+ await writeTextInDir(path, 'problem.en.tex', this.problemStatement)
355
+
356
+ const yml = {
357
+ title: this.spec.title,
358
+ author: this.spec.author,
359
+ email: this.spec.email,
360
+ model: this.model,
361
+ }
362
+ await writeYamlInDir(path, 'problem.en.yml', yml)
363
+
364
+ for (const [lang, translation] of Object.entries(this.problemMoreStatements)) {
365
+ await writeTextInDir(path, `problem.${lang}.tex`, translation)
366
+ const yml = {
367
+ title: getTitleFromStatement(translation) || this.spec.title,
368
+ translator: this.spec.author,
369
+ translator_email: this.spec.email,
370
+ original_language: 'en',
371
+ model: this.model,
372
+ }
373
+ await writeYamlInDir(path, `problem.${lang}.yml`, yml)
374
+ }
375
+
376
+ await writeTextInDir(path, 'sample-1.inp', this.problemSampleTests1)
377
+ await writeTextInDir(path, 'sample-2.inp', this.problemSampleTests2)
378
+
379
+ await writeTextInDir(path, 'test-1.inp', this.problemPrivateTests1)
380
+ await writeTextInDir(path, 'test-2.inp', this.problemPrivateTests2)
381
+
382
+ for (const [proglang, solution] of Object.entries(this.problemMoreSolutions)) {
383
+ const ext = proglang
384
+ await writeTextInDir(path, `solution.${ext}`, solution)
385
+ }
386
+
387
+ for (const [type, code] of Object.entries(this.problemGenerators)) {
388
+ await writeTextInDir(path, `generate-${type}.py`, code)
389
+ }
390
+
391
+ const handlerYml: any = {
392
+ handler: 'std',
393
+ solution: proglangNames[this.spec.golden_proglang],
394
+ }
395
+ await writeYamlInDir(path, 'handler.yml', handlerYml)
396
+
397
+ await writeTextInDir(path, 'README.md', this.problemReadme)
398
+ })
399
+ }
400
+ }
401
+
402
+ function listify(items: (string | undefined)[]): string {
403
+ if (items.length === 0) {
404
+ return '<none>'
405
+ }
406
+ return items.map((item) => `- ${item}`).join('\n')
407
+ }
@@ -0,0 +1,55 @@
1
+ import { confirm, select, Separator } from '@inquirer/prompts'
2
+ import { cp, exists, glob } from 'fs/promises'
3
+ import path from 'path'
4
+ import tui from './tui'
5
+ import { projectDir, readText } from './utils'
6
+ import { title } from 'radash'
7
+
8
+ const templatesDir = path.join(projectDir(), 'assets', 'problems')
9
+
10
+ export async function selectTemplate(): Promise<string> {
11
+ const choices = []
12
+
13
+ const dirs = await Array.fromAsync(glob('*', { cwd: templatesDir }))
14
+ dirs.sort()
15
+ for (const dir of dirs) {
16
+ choices.push(new Separator(title(dir)))
17
+ const templates = await Array.fromAsync(glob('*.pbm', { cwd: path.join(templatesDir, dir) }))
18
+ templates.sort()
19
+ for (const template of templates) {
20
+ choices.push({ name: template, value: path.join(dir, template) })
21
+ }
22
+ }
23
+
24
+ let template: string | undefined = undefined
25
+ while (true) {
26
+ template = await select({ message: 'Select a template:', choices, default: template })
27
+ const readme = await readText(path.join(templatesDir, template, 'README.md'))
28
+ console.log()
29
+ await tui.markdown(readme)
30
+ const confirmation = await confirm({ message: `Use template ${template}?`, default: true })
31
+ if (confirmation) return template
32
+ }
33
+ }
34
+
35
+ export async function createProblemWithTemplate(directory: string, template: string | undefined) {
36
+ if (await exists(directory)) {
37
+ throw new Error(`Directory ${directory} already exists`)
38
+ }
39
+ if (!directory.endsWith('.pbm')) {
40
+ throw new Error('The output directory must end with .pbm')
41
+ }
42
+
43
+ if (!template) {
44
+ template = await selectTemplate()
45
+ }
46
+
47
+ const templatePath = path.join(templatesDir, template)
48
+
49
+ if (!(await exists(templatePath))) {
50
+ throw new Error(`Template ${template} does not exist`)
51
+ }
52
+
53
+ await cp(templatePath, directory, { recursive: true })
54
+ tui.success(`Created problem ${tui.hyperlink(directory)} from template ${template}`)
55
+ }
package/lib/data.ts CHANGED
@@ -8,12 +8,18 @@ export const languageNames: Record<string, string> = {
8
8
  de: 'German',
9
9
  }
10
10
 
11
+ export const languageKeys = Object.keys(languageNames)
12
+
11
13
  export const proglangNames: Record<string, string> = {
12
14
  c: 'C',
13
15
  cc: 'C++',
14
16
  py: 'Python3',
15
17
  hs: 'Haskell',
16
18
  clj: 'Clojure',
19
+ java: 'Java',
20
+ rs: 'Rust',
17
21
  }
18
22
 
19
23
  export const proglangExtensions: Record<string, string> = invert(proglangNames)
24
+
25
+ export const proglangKeys = Object.keys(proglangNames)