@jutge.org/toolkit 4.4.7 → 4.4.16

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 (35) hide show
  1. package/assets/prompts/creators/create-statement-from-solution.tpl.txt +9 -0
  2. package/assets/prompts/funcs/creators/create-solution.tpl.txt +23 -0
  3. package/assets/prompts/funcs/creators/create-statement.tpl.txt +28 -0
  4. package/assets/prompts/funcs/creators/private-test-cases.tpl.txt +12 -0
  5. package/assets/prompts/funcs/creators/sample-test-cases.tpl.txt +16 -0
  6. package/assets/prompts/funcs/creators/sample.dt.tpl.txt +15 -0
  7. package/assets/prompts/funcs/examples/statement.tex +23 -0
  8. package/assets/prompts/funcs/proglangs/py.md +9 -0
  9. package/assets/prompts/io/examples/statement-coda.tex +7 -0
  10. package/dist/index.js +719 -464
  11. package/docs/getting-started-guide.md +6 -0
  12. package/docs/jutge-ai.md +1 -1
  13. package/package.json +16 -11
  14. package/toolkit/clean.ts +5 -4
  15. package/toolkit/convert.ts +3 -3
  16. package/toolkit/download.ts +16 -0
  17. package/toolkit/dummies.ts +219 -0
  18. package/toolkit/generate.ts +105 -13
  19. package/toolkit/index.ts +4 -0
  20. package/toolkit/lint.ts +4 -6
  21. package/toolkit/make.ts +3 -3
  22. package/toolkit/quiz.ts +3 -3
  23. package/docs/windows.md +0 -106
  24. /package/assets/prompts/{examples → funcs/examples}/statement-coda.tex +0 -0
  25. /package/assets/prompts/{creators → io/creators}/create-solution.tpl.txt +0 -0
  26. /package/assets/prompts/{creators → io/creators}/create-statement.tpl.txt +0 -0
  27. /package/assets/prompts/{creators → io/creators}/create-translation.tpl.txt +0 -0
  28. /package/assets/prompts/{creators → io/creators}/private-test-cases.txt +0 -0
  29. /package/assets/prompts/{creators → io/creators}/sample-test-cases.txt +0 -0
  30. /package/assets/prompts/{examples → io/examples}/statement.tex +0 -0
  31. /package/assets/prompts/{generators → io/generators}/efficiency.md +0 -0
  32. /package/assets/prompts/{generators → io/generators}/hard.md +0 -0
  33. /package/assets/prompts/{generators → io/generators}/random.md +0 -0
  34. /package/assets/prompts/{proglangs → io/proglangs}/cc.md +0 -0
  35. /package/assets/prompts/{proglangs → io/proglangs}/py.md +0 -0
@@ -170,6 +170,12 @@ This will run the solution against all test cases and compare outputs.
170
170
 
171
171
  ### Generating Additional Content
172
172
 
173
+ **Generate a statement from a solution:**
174
+
175
+ ```bash
176
+ jtk generate statement cc en "Add a cute story about Joan."
177
+ ```
178
+
173
179
  **Add statement translations:**
174
180
 
175
181
  ```bash
package/docs/jutge-ai.md CHANGED
@@ -61,7 +61,7 @@ See https://platform.openai.com/docs/pricing for the pricing of the OpenAI model
61
61
 
62
62
  ### Recommendation
63
63
 
64
- Try to use `gpt-4.1-nano` or `gpt-4.1-mini` for the quickest results. If you need more reliable results, use `gpt-5-nano` or `gpt-5-mini`.
64
+ Try to use `gpt-4.1-nano` or `gpt-4.1-mini` for the quickest results. If you need more reliable results, use `gpt-5-nano` or `gpt-5-mini`. As could be expected, the larger the model, the more reliable the results and the slower the generation.
65
65
 
66
66
  # Jutge<sup>AI</sup> costs
67
67
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jutge.org/toolkit",
3
3
  "description": "Toolkit to prepare problems for Jutge.org",
4
- "version": "4.4.7",
4
+ "version": "4.4.16",
5
5
  "homepage": "https://jutge.org",
6
6
  "author": {
7
7
  "name": "Jutge.org",
@@ -61,30 +61,33 @@
61
61
  ],
62
62
  "dependencies": {
63
63
  "@commander-js/extra-typings": "^14.0.0",
64
- "@dicebear/collection": "^9.3.1",
65
- "@dicebear/core": "^9.3.1",
66
- "@eslint/js": "^9.39.2",
67
- "@inquirer/prompts": "^8.2.0",
64
+ "@dicebear/collection": "^9.3.2",
65
+ "@dicebear/core": "^9.3.2",
66
+ "@eslint/js": "^10.0.1",
67
+ "@inquirer/prompts": "^8.3.0",
68
+ "adm-zip": "^0.5.16",
68
69
  "archiver": "^7.0.1",
69
70
  "boxen": "^8.0.1",
70
- "bun-types": "^1.3.7",
71
+ "bun-types": "^1.3.9",
71
72
  "chalk": "^5.6.2",
72
73
  "chokidar": "^5.0.0",
73
74
  "cli-highlight": "^2.1.11",
74
- "commander": "^14.0.2",
75
+ "commander": "^14.0.3",
75
76
  "dayjs": "^1.11.19",
76
77
  "env-paths": "^4.0.0",
77
- "eslint": "^9.39.2",
78
+ "eslint": "^10.0.2",
78
79
  "execa": "^9.6.1",
79
80
  "handlebars": "^4.7.8",
80
81
  "image-size": "^2.0.2",
81
82
  "inquirer-checkbox-plus-plus": "^1.1.1",
83
+ "marked": "^17.0.3",
84
+ "marked-terminal": "^7.3.0",
82
85
  "nanoid": "^5.1.6",
83
86
  "open": "^11.0.0",
84
87
  "pretty-bytes": "^7.1.0",
85
88
  "pretty-ms": "^9.3.0",
86
89
  "radash": "^12.1.1",
87
- "semver": "^7.7.3",
90
+ "semver": "^7.7.4",
88
91
  "sharp": "^0.34.5",
89
92
  "terminal-link": "^5.0.0",
90
93
  "tree-node-cli": "^1.6.0",
@@ -94,12 +97,14 @@
94
97
  "zod-validation-error": "^5.0.0"
95
98
  },
96
99
  "devDependencies": {
100
+ "@types/adm-zip": "^0.5.0",
97
101
  "@types/archiver": "^7.0.0",
98
102
  "@types/image-size": "^0.8.0",
99
- "@types/node": "^25.1.0",
103
+ "@types/marked-terminal": "^6.1.1",
104
+ "@types/node": "^25.3.0",
100
105
  "@types/semver": "^7.7.1",
101
106
  "prettier": "^3.8.1",
102
- "typescript-eslint": "^8.54.0"
107
+ "typescript-eslint": "^8.56.1"
103
108
  },
104
109
  "peerDependencies": {
105
110
  "typescript": "^5.9.3"
package/toolkit/clean.ts CHANGED
@@ -2,20 +2,21 @@ import { Command, Option } from '@commander-js/extra-typings'
2
2
  import { cleanDirectory } from '../lib/cleaner'
3
3
  import tui from '../lib/tui'
4
4
  import { findRealDirectories } from '../lib/helpers'
5
+ import { resolve } from 'path'
5
6
 
6
7
  export const cleanCmd = new Command('clean')
7
8
  .description('Clean disposable files')
8
9
 
9
- .option('-d, --directories <directories...>', 'problem directories', ['.'])
10
+ .option('-d, --directory <directory>', 'problem directory', '.')
10
11
  .option('-a, --all', 'clean all disposable files (including generated statement and correct files', false)
11
12
  .addOption(new Option('-f, --force', 'force removal').conflicts('dryRun'))
12
13
  .addOption(new Option('-n, --dry-run', 'show but do not remove files').conflicts('force'))
13
14
 
14
- .action(async ({ directories, all, force, dryRun }) => {
15
+ .action(async ({ directory, all, force, dryRun }) => {
15
16
  const isForce = force || false // default to dry-run if neither option is specified
16
17
 
17
- await tui.section(`Cleaning generated files`, async () => {
18
- const realDirectories = await findRealDirectories(directories)
18
+ await tui.section(`Cleaning disposable files in ${tui.hyperlink(resolve(directory))}`, async () => {
19
+ const realDirectories = await findRealDirectories([directory])
19
20
  for (const directory of realDirectories) {
20
21
  await tui.section(`Cleaning directory ${tui.hyperlink(directory)}`, async () => {
21
22
  await cleanDirectory(isForce, all, directory)
@@ -15,10 +15,10 @@ convertCmd
15
15
  .command('transform-at-signs')
16
16
  .description('Transform @ signs to lstinline')
17
17
 
18
- .option('-d, --directories <directories...>', 'problem directories', ['.'])
18
+ .option('-d, --directory <directory>', 'problem directory', '.')
19
19
 
20
- .action(async ({ directories }) => {
21
- const realDirectories = await findRealDirectories(directories)
20
+ .action(async ({ directory }) => {
21
+ const realDirectories = await findRealDirectories([directory])
22
22
 
23
23
  for (const realDirectory of realDirectories) {
24
24
  await tui.section(`Processing ${realDirectory}`, async () => {
@@ -0,0 +1,16 @@
1
+ import { Command } from '@commander-js/extra-typings'
2
+ import { downloadProblem } from '../lib/download'
3
+
4
+ export const downloadCmd = new Command('download')
5
+ .summary('Download a problem from Jutge.org')
6
+
7
+ .argument('<problem_nm>', 'problem to download')
8
+ .option('-d, --directory <path>', 'output directory (default: <problem_nm>.pbm)')
9
+
10
+ .action(async (problem_nm, { directory }) => {
11
+ const outDir = directory ?? `${problem_nm}.pbm`
12
+ if (!outDir.endsWith('.pbm')) {
13
+ throw new Error('Output directory must end with .pbm')
14
+ }
15
+ await downloadProblem(problem_nm, outDir)
16
+ })
@@ -0,0 +1,219 @@
1
+ import { Command } from '@commander-js/extra-typings'
2
+ import type { Option, Argument } from '@commander-js/extra-typings'
3
+ import { confirm, input, select } from '@inquirer/prompts'
4
+
5
+ type CommandUnknownOpts = Command<unknown[], Record<string, unknown>, Record<string, unknown>>
6
+
7
+ function getVisibleSubcommands(cmd: CommandUnknownOpts): CommandUnknownOpts[] {
8
+ const help = cmd.createHelp()
9
+ return help.visibleCommands(cmd).filter((c) => c.name() !== 'help')
10
+ }
11
+
12
+ function getVisibleOptions(cmd: CommandUnknownOpts): Option[] {
13
+ return cmd.options.filter((o) => !o.hidden && o.long !== 'help' && o.long !== 'version' && !o.negate)
14
+ }
15
+
16
+ function getVisibleArguments(cmd: CommandUnknownOpts): Argument[] {
17
+ return [...cmd.registeredArguments]
18
+ }
19
+
20
+ function commandSummary(cmd: CommandUnknownOpts): string {
21
+ const summary = (cmd as CommandUnknownOpts & { summary?: () => string }).summary?.()
22
+ if (summary) return summary
23
+ const desc = cmd.description()
24
+ return desc ? desc.split('\n')[0]!.trim() : cmd.name()
25
+ }
26
+
27
+ async function chooseCommand(
28
+ program: CommandUnknownOpts,
29
+ path: string[],
30
+ ): Promise<{ path: string[]; cmd: CommandUnknownOpts }> {
31
+ const current = path.length === 0 ? program : resolveCommand(program, path)!
32
+ const subcommands = getVisibleSubcommands(current)
33
+
34
+ if (subcommands.length === 0) {
35
+ return { path, cmd: current }
36
+ }
37
+
38
+ const choices = subcommands.map((c) => ({
39
+ name: `${c.name()} — ${commandSummary(c)}`,
40
+ value: c.name(),
41
+ }))
42
+
43
+ const chosen = await select({
44
+ message: path.length === 0 ? 'What do you want to do?' : `Choose ${current.name()} subcommand:`,
45
+ choices: [...choices, { name: '← Back', value: '__back__' }],
46
+ })
47
+
48
+ if (chosen === '__back__') {
49
+ if (path.length === 0) return { path: [], cmd: program }
50
+ return chooseCommand(program, path.slice(0, -1))
51
+ }
52
+
53
+
54
+ const newPath = [...path, chosen]
55
+ const nextCmd = resolveCommand(program, newPath)!
56
+ const nextSubs = getVisibleSubcommands(nextCmd)
57
+ if (nextSubs.length > 0) {
58
+ return chooseCommand(program, newPath)
59
+ }
60
+ return { path: newPath, cmd: nextCmd }
61
+ }
62
+
63
+ function resolveCommand(program: CommandUnknownOpts, path: string[]): CommandUnknownOpts | null {
64
+ let current: CommandUnknownOpts = program
65
+ for (const name of path) {
66
+ const sub = current.commands.find((c) => c.name() === name)
67
+ if (!sub) return null
68
+ current = sub as CommandUnknownOpts
69
+ }
70
+ return current
71
+ }
72
+
73
+ async function promptForArgument(arg: Argument, existing: string[]): Promise<string | string[]> {
74
+ const name = arg.name()
75
+ const desc = arg.description || name
76
+ const defaultVal = arg.defaultValue
77
+ const choices = arg.argChoices
78
+ const variadic = arg.variadic
79
+ const required = arg.required
80
+
81
+ const message = desc + (required ? '' : ' (optional)')
82
+
83
+ if (choices && choices.length > 0) {
84
+ const value = await select({
85
+ message,
86
+ choices: choices.map((c) => ({ name: c, value: c })),
87
+ default: defaultVal != null ? (Array.isArray(defaultVal) ? defaultVal[0] : defaultVal) : undefined,
88
+ })
89
+ return value
90
+ }
91
+
92
+ const defaultStr =
93
+ defaultVal != null
94
+ ? (Array.isArray(defaultVal) ? defaultVal.join(' ') : String(defaultVal))
95
+ : required
96
+ ? undefined
97
+ : ''
98
+
99
+ const raw = await input({
100
+ message,
101
+ default: defaultStr,
102
+ validate: (v) => (required && !v.trim() ? 'This argument is required' : true),
103
+ })
104
+
105
+ if (variadic && raw.includes(' ')) {
106
+ return raw.trim().split(/\s+/).filter(Boolean)
107
+ }
108
+ return raw.trim() || (defaultStr as string)
109
+ }
110
+
111
+ async function promptForOption(opt: Option): Promise<{ name: string; value: unknown } | null> {
112
+ const attr = opt.attributeName()
113
+ const desc = opt.description || opt.long || opt.flags
114
+ const defaultVal = opt.defaultValue
115
+ const choices = opt.argChoices
116
+ const isBool = opt.isBoolean()
117
+
118
+ if (isBool) {
119
+ const value = await confirm({
120
+ message: desc,
121
+ default: defaultVal === true,
122
+ })
123
+ return { name: attr, value }
124
+ }
125
+
126
+ if (choices && choices.length > 0) {
127
+ const value = await select({
128
+ message: desc,
129
+ choices: choices.map((c) => ({ name: c, value: c })),
130
+ default: defaultVal != null ? String(defaultVal) : undefined,
131
+ })
132
+ return { name: attr, value }
133
+ }
134
+
135
+ const defaultStr = defaultVal != null ? String(defaultVal) : ''
136
+ const value = await input({
137
+ message: desc + (opt.required ? '' : ' (optional)'),
138
+ default: defaultStr,
139
+ })
140
+ if (value === '' && !opt.required) return null
141
+ return { name: attr, value }
142
+ }
143
+
144
+ function buildArgv(
145
+ path: string[],
146
+ cmd: CommandUnknownOpts,
147
+ argValues: (string | string[])[],
148
+ optionValues: Record<string, unknown>,
149
+ ): string[] {
150
+ const argv: string[] = [...path]
151
+
152
+ const positionals: string[] = []
153
+ for (const v of argValues) {
154
+ if (Array.isArray(v)) positionals.push(...v)
155
+ else if (v !== '') positionals.push(v)
156
+ }
157
+ argv.push(...positionals)
158
+
159
+ for (const opt of getVisibleOptions(cmd)) {
160
+ const attr = opt.attributeName()
161
+ const val = optionValues[attr]
162
+ if (val === undefined) continue
163
+ if (opt.isBoolean()) {
164
+ if (val === true) {
165
+ argv.push(opt.long!.startsWith('--') ? opt.long! : `--${opt.long}`)
166
+ } else if (val === false) {
167
+ const negateOpt = cmd.options.find((o) => o.negate && o.attributeName() === attr)
168
+ if (negateOpt?.long) {
169
+ argv.push(negateOpt.long.startsWith('--') ? negateOpt.long : `--${negateOpt.long}`)
170
+ }
171
+ }
172
+ } else {
173
+ if (val !== '' && val != null) {
174
+ const long = opt.long!.startsWith('--') ? opt.long! : `--${opt.long}`
175
+ // eslint-disable-next-line @typescript-eslint/no-base-to-string
176
+ argv.push(long, String(val))
177
+ }
178
+ }
179
+ }
180
+
181
+ return argv
182
+ }
183
+
184
+ export const dummiesCmd = new Command('for-dummies')
185
+ .alias('interactive')
186
+ .summary('Interactive menu for all toolkit tasks')
187
+ .description(
188
+ 'Run a guided flow of menus and prompts to perform any toolkit task. Help and defaults are taken from the command definitions.',
189
+ )
190
+ .action(async function (this: CommandUnknownOpts) {
191
+ const program = this.parent
192
+ if (!program) {
193
+ throw new Error('Dummies command must be run under the main program')
194
+ }
195
+
196
+ const { path, cmd } = await chooseCommand(program as CommandUnknownOpts, [])
197
+
198
+ if (path.length === 0) {
199
+ return
200
+ }
201
+
202
+ const args = getVisibleArguments(cmd)
203
+ const opts = getVisibleOptions(cmd)
204
+
205
+ const argValues: (string | string[])[] = []
206
+ for (const arg of args) {
207
+ const v = await promptForArgument(arg, argValues.flat(1))
208
+ argValues.push(v)
209
+ }
210
+
211
+ const optionValues: Record<string, unknown> = {}
212
+ for (const opt of opts) {
213
+ const result = await promptForOption(opt)
214
+ if (result) optionValues[result.name] = result.value
215
+ }
216
+
217
+ const argv = buildArgv(path, cmd, argValues, optionValues)
218
+ await program.parseAsync(argv, { from: 'user' })
219
+ })
@@ -1,20 +1,23 @@
1
- import { Argument, Command } from '@commander-js/extra-typings'
1
+ import { Argument, Command, Option } from '@commander-js/extra-typings'
2
+ import { exists } from 'fs/promises'
2
3
  import { join } from 'path'
3
4
  import sharp from 'sharp'
4
- import { createProblemWithJutgeAI } from '../lib/create-with-jutgeai'
5
5
  import { complete } from '../lib/aiclient'
6
+ import { createFuncsProblem } from '../lib/create-funcs'
7
+ import { createIOProblem } from '../lib/create-io'
6
8
  import { languageKeys, languageNames, proglangKeys, proglangNames } from '../lib/data'
7
9
  import {
8
10
  addAlternativeSolution,
9
11
  addMainFile,
10
12
  addStatementTranslation,
13
+ generateStatementFromSolution,
11
14
  generateTestCasesGenerator,
12
15
  } from '../lib/generate'
16
+ import { getLoggedInJutgeClient } from '../lib/login'
13
17
  import { newProblem } from '../lib/problem'
14
18
  import { settings } from '../lib/settings'
15
19
  import tui from '../lib/tui'
16
20
  import { writeText } from '../lib/utils'
17
- import { getLoggedInJutgeClient } from '../lib/login'
18
21
 
19
22
  export const generateCmd = new Command('generate')
20
23
  .description('Generate problem elements using JutgeAI')
@@ -27,16 +30,80 @@ generateCmd
27
30
  .command('problem')
28
31
  .description('Generate a problem with JutgeAI')
29
32
 
33
+ .summary(`Generate a problem with JutgeAI
34
+
35
+ Use this command to generate a problem with JutgeAI from a specification.
36
+
37
+ There are currently two types of problems that can be generated:
38
+
39
+ - io: The problem consists of reading input from standard input and writing output to standard output.
40
+
41
+ Current implementation supports C, C++, Python, Haskell, Java, Rust, R and Clojure programming languages.
42
+
43
+ The following items will be generated:
44
+ - problem statement in original language
45
+ - sample test cases
46
+ - private test cases
47
+ - golden solution
48
+ - translations of the problem statement into other languages
49
+ - alternative solutions in other programming languages
50
+ - test cases generators
51
+ - a README.md file describing the problem and LLM usage
52
+
53
+ - funcs: The problem consists of implementing one or more functions.
54
+
55
+ Current implementation supports Python, Haskell and Clojure programming languages (through RunPython, RunHaskell and RunClojure compilers).
56
+
57
+ The following items will be generated:
58
+ - problem statement in original language
59
+ - translations of the problem statement into other languages
60
+ - generate sample.dt for Python
61
+ - sample test cases
62
+ - private test cases for each function
63
+ - golden solution
64
+ - alternative solutions in other programming languages
65
+ - scores.yml file with the scores for each function (if there is more than one function)
66
+ - a README.md file describing the problem and LLM usage
67
+
68
+ Problem generation needs a problem specification:
69
+ - If --input is provided, the system will read the given input specification file.
70
+ - If --output is provided, the system will write the problem specification to the given output specification file.
71
+ - The system will ask interactively for the problem specification (using the values in the --input specification file if provided as defaults)
72
+ - unless the --do-not-ask flag is given.
73
+
74
+ Treat the generated problem as a starting draft. You should edit the problem directory manually after the generation.
75
+ `)
76
+
77
+ .addOption(
78
+ new Option
79
+ ('-k, --kind <kind>', 'problem kind')
80
+ .default('io')
81
+ .choices(['io', 'funcs'])
82
+ )
30
83
  .option('-d, --directory <path>', 'output directory', 'new-problem.pbm')
31
84
  .option('-i, --input <path>', 'input specification file')
32
85
  .option('-o, --output <path>', 'output specification file')
33
86
  .option('-n, --do-not-ask', 'do not ask interactively if --input given', false)
34
87
  .option('-m, --model <model>', 'AI model to use', settings.defaultModel)
35
88
 
36
- .action(async ({ input, output, directory, model, doNotAsk }) => {
89
+ .action(async ({ input, output, directory, model, doNotAsk, kind }) => {
37
90
  const jutge = await getLoggedInJutgeClient()
38
- await tui.section('Generating problem with JutgeAI', async () => {
39
- await createProblemWithJutgeAI(jutge, model, directory, input, output, doNotAsk)
91
+ await tui.section(`Generating ${kind} problem with JutgeAI`, async () => {
92
+
93
+ if (await exists(directory)) {
94
+ throw new Error(`Directory ${directory} already exists`)
95
+ }
96
+ if (!directory.endsWith('.pbm')) {
97
+ throw new Error('The output directory must end with .pbm')
98
+ }
99
+
100
+ if (kind === 'io') {
101
+ await createIOProblem(jutge, model, directory, input, output, doNotAsk)
102
+ } else if (kind === 'funcs') {
103
+ await createFuncsProblem(jutge, model, directory, input, output, doNotAsk)
104
+ } else {
105
+ throw new Error(`Invalid problem kind: ${kind as string}`)
106
+ }
40
107
  })
41
108
  })
42
109
 
@@ -51,8 +118,8 @@ The original statement will be used as the source text for translation.
51
118
 
52
119
  Provide one or more target language from the following list:
53
120
  ${Object.entries(languageNames)
54
- .map(([key, name]) => ` - ${key}: ${name}`)
55
- .join('\n')}
121
+ .map(([key, name]) => ` - ${key}: ${name}`)
122
+ .join('\n')}
56
123
 
57
124
  The added translations will be saved in the problem directory overwrite possible existing files.`,
58
125
  )
@@ -71,6 +138,31 @@ The added translations will be saved in the problem directory overwrite possible
71
138
  })
72
139
  })
73
140
 
141
+ generateCmd
142
+ .command('statement')
143
+ .summary('Generate a statement from a solution using JutgeAI')
144
+ .description(
145
+ `Generate a problem statement from a solution using JutgeAI
146
+
147
+ Use this command to create a statement file from an existing solution.
148
+ The AI infers the problem (input/output, task) from the solution code and writes a problem statement.
149
+
150
+ Provide the programming language of the solution to use and the target language for the statement.
151
+ An optional prompt can guide the statement generation (e.g. "Focus on edge cases" or "Assume the problem is for beginners").
152
+
153
+ The result is written to statement.<lang>.tex in the problem directory.`,
154
+ )
155
+ .addArgument(new Argument('<proglang>', 'solution to use (e.g. cc, py)').choices(proglangKeys))
156
+ .addArgument(new Argument('<language>', 'statement language').choices(languageKeys))
157
+ .option('-d, --directory <path>', 'problem directory', '.')
158
+ .option('-m, --model <model>', 'AI model to use', settings.defaultModel)
159
+ .argument('[prompt]', 'optional prompt to guide statement generation', '')
160
+ .action(async (proglang, language, prompt, { directory, model }) => {
161
+ const jutge = await getLoggedInJutgeClient()
162
+ const problem = await newProblem(directory)
163
+ await generateStatementFromSolution(jutge, model, problem, proglang, language, (prompt ?? '').trim() || undefined)
164
+ })
165
+
74
166
  generateCmd
75
167
  .command('solutions')
76
168
  .summary('Generate alternative solutions using JutgeAI')
@@ -82,8 +174,8 @@ The golden solution will be used as a reference for generating the alternatives.
82
174
 
83
175
  Provide one or more target programming languages from the following list:
84
176
  ${Object.entries(languageNames)
85
- .map(([key, name]) => ` - ${key}: ${name}`)
86
- .join('\n')}
177
+ .map(([key, name]) => ` - ${key}: ${name}`)
178
+ .join('\n')}
87
179
 
88
180
  The added solutions will be saved in the problem directory overwrite possible existing files.`,
89
181
  )
@@ -117,8 +209,8 @@ The main file for the golden solution will be used as a reference for generating
117
209
 
118
210
  Provide one or more target programming languages from the following list:
119
211
  ${Object.entries(languageNames)
120
- .map(([key, name]) => ` - ${key}: ${name}`)
121
- .join('\n')}
212
+ .map(([key, name]) => ` - ${key}: ${name}`)
213
+ .join('\n')}
122
214
 
123
215
  The added main files will be saved in the problem directory overwrite possible existing files.`,
124
216
  )
@@ -185,7 +277,7 @@ The new image will be saved as award.png in the problem directory, overriding an
185
277
  .action(async (prompt, { directory, model }) => {
186
278
  const jutge = await getLoggedInJutgeClient()
187
279
  const output = join(directory, 'award.png')
188
- const problem = await newProblem(directory)
280
+ await newProblem(directory)
189
281
  let imagePrompt = prompt.trim()
190
282
  if (imagePrompt === '') {
191
283
  imagePrompt = 'A colorful award on a white background'
package/toolkit/index.ts CHANGED
@@ -11,6 +11,7 @@ import { cleanCmd } from './clean'
11
11
  import { compilersCmd } from './compilers'
12
12
  import { configCmd } from './config'
13
13
  import { cloneCmd } from './clone'
14
+ import { downloadCmd } from './download'
14
15
  import { doctorCmd } from './doctor'
15
16
  import { generateCmd } from './generate'
16
17
  import { makeCmd } from './make'
@@ -24,6 +25,7 @@ import { convertCmd } from './convert'
24
25
  import { stageCmd } from './stage'
25
26
  import { lintCmd } from './lint'
26
27
  import { completeInternalCmd, completionCmd } from './completion'
28
+ import { dummiesCmd } from './dummies'
27
29
 
28
30
  program.name(Object.keys(packageJson.bin as Record<string, string>)[0] as string)
29
31
  program.alias(Object.keys(packageJson.bin as Record<string, string>)[1] as string)
@@ -37,6 +39,7 @@ program.addCommand(uploadCmd)
37
39
  program.addCommand(cmdShare)
38
40
  program.addCommand(cleanCmd)
39
41
  program.addCommand(cloneCmd)
42
+ program.addCommand(downloadCmd)
40
43
  program.addCommand(generateCmd)
41
44
  program.addCommand(verifyCmd)
42
45
  program.addCommand(lintCmd)
@@ -54,6 +57,7 @@ program.addCommand(completionCmd)
54
57
  program.addCommand(aboutCmd)
55
58
  program.addCommand(askCmd)
56
59
  program.addCommand(completeInternalCmd, { hidden: true })
60
+ program.addCommand(dummiesCmd)
57
61
 
58
62
  try {
59
63
  await program.parseAsync()
package/toolkit/lint.ts CHANGED
@@ -47,19 +47,17 @@ export async function printLintResults(results: LintResult[], directories: strin
47
47
  export const lintCmd = new Command('lint')
48
48
  .summary('Lint a problem directory')
49
49
 
50
- .argument('[directories...]', 'problem directories to lint (default: current directory)')
51
- .option('-d, --directory <path>', 'problem directory when no arguments given', '.')
50
+ .argument('[directory]', 'problem directory to lint', '.')
52
51
 
53
- .action(async (directories: string[], { directory }) => {
54
- const dirs = directories.length > 0 ? directories : [directory]
55
- const results = await lintDirectories(dirs)
52
+ .action(async (directory: string) => {
53
+ const results = await lintDirectories([directory])
56
54
 
57
55
  if (results.length === 0) {
58
56
  tui.warning('No problem directories found (looked for handler.yml in the given path(s)).')
59
57
  return
60
58
  }
61
59
 
62
- const { hasError } = await printLintResults(results, dirs)
60
+ const { hasError } = await printLintResults(results, [directory])
63
61
  if (hasError) {
64
62
  process.exitCode = 1
65
63
  }
package/toolkit/make.ts CHANGED
@@ -16,13 +16,13 @@ export const makeCmd = new Command('make')
16
16
  .description('Make problem')
17
17
 
18
18
  .argument('[tasks...]', 'tasks to make: all|info|exe|cor|pdf|txt|md|html', ['all'])
19
- .option('-d, --directories <directories...>', 'problem directories', ['.'])
19
+ .option('-d, --directory <directory>', 'problem directory', '.')
20
20
  .option('-i, --ignore-errors', 'ignore errors on a directory and continue processing', false)
21
21
  .option('-e, --only-errors', 'only show errors at the final summary', false)
22
22
  .option('-p, --problem_nm <problem_nm>', 'problem nm', 'DRAFT')
23
23
  .option('-w, --watch', 'watch for changes and rebuild incrementally (under development)', false)
24
24
 
25
- .action(async (tasks, { directories, ignoreErrors, onlyErrors, problem_nm, watch }) => {
25
+ .action(async (tasks, { directory, ignoreErrors, onlyErrors, problem_nm, watch }) => {
26
26
  if (watch) {
27
27
  tasks = ['all']
28
28
  }
@@ -41,7 +41,7 @@ export const makeCmd = new Command('make')
41
41
 
42
42
  const errors: Record<string, string> = {} // directory -> error message
43
43
 
44
- const realDirectories = await findRealDirectories(directories)
44
+ const realDirectories = await findRealDirectories([directory])
45
45
 
46
46
  if (watch && realDirectories.length > 1) {
47
47
  tui.warning('With --watch only the first directory is watched')
package/toolkit/quiz.ts CHANGED
@@ -15,10 +15,10 @@ quizCmd
15
15
  .command('validate')
16
16
  .description('Validate a quiz problem')
17
17
 
18
- .option('-d, --directories <directories...>', 'problem directories', ['.'])
18
+ .option('-d, --directory <directory>', 'problem directory', '.')
19
19
 
20
- .action(async ({ directories }) => {
21
- const realDirectories = await findRealDirectories(directories)
20
+ .action(async ({ directory }) => {
21
+ const realDirectories = await findRealDirectories([directory])
22
22
  for (const directory of realDirectories) {
23
23
  await tui.section(`Validating quiz in directory ${tui.hyperlink(directory)}`, async () => {
24
24
  await validateQuiz(directory)