@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.
- package/assets/prompts/creators/create-statement-from-solution.tpl.txt +9 -0
- package/assets/prompts/funcs/creators/create-solution.tpl.txt +23 -0
- package/assets/prompts/funcs/creators/create-statement.tpl.txt +28 -0
- package/assets/prompts/funcs/creators/private-test-cases.tpl.txt +12 -0
- package/assets/prompts/funcs/creators/sample-test-cases.tpl.txt +16 -0
- package/assets/prompts/funcs/creators/sample.dt.tpl.txt +15 -0
- package/assets/prompts/funcs/examples/statement.tex +23 -0
- package/assets/prompts/funcs/proglangs/py.md +9 -0
- package/assets/prompts/io/examples/statement-coda.tex +7 -0
- package/dist/index.js +719 -464
- package/docs/getting-started-guide.md +6 -0
- package/docs/jutge-ai.md +1 -1
- package/package.json +16 -11
- package/toolkit/clean.ts +5 -4
- package/toolkit/convert.ts +3 -3
- package/toolkit/download.ts +16 -0
- package/toolkit/dummies.ts +219 -0
- package/toolkit/generate.ts +105 -13
- package/toolkit/index.ts +4 -0
- package/toolkit/lint.ts +4 -6
- package/toolkit/make.ts +3 -3
- package/toolkit/quiz.ts +3 -3
- package/docs/windows.md +0 -106
- /package/assets/prompts/{examples → funcs/examples}/statement-coda.tex +0 -0
- /package/assets/prompts/{creators → io/creators}/create-solution.tpl.txt +0 -0
- /package/assets/prompts/{creators → io/creators}/create-statement.tpl.txt +0 -0
- /package/assets/prompts/{creators → io/creators}/create-translation.tpl.txt +0 -0
- /package/assets/prompts/{creators → io/creators}/private-test-cases.txt +0 -0
- /package/assets/prompts/{creators → io/creators}/sample-test-cases.txt +0 -0
- /package/assets/prompts/{examples → io/examples}/statement.tex +0 -0
- /package/assets/prompts/{generators → io/generators}/efficiency.md +0 -0
- /package/assets/prompts/{generators → io/generators}/hard.md +0 -0
- /package/assets/prompts/{generators → io/generators}/random.md +0 -0
- /package/assets/prompts/{proglangs → io/proglangs}/cc.md +0 -0
- /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.
|
|
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.
|
|
65
|
-
"@dicebear/core": "^9.3.
|
|
66
|
-
"@eslint/js": "^
|
|
67
|
-
"@inquirer/prompts": "^8.
|
|
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.
|
|
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.
|
|
75
|
+
"commander": "^14.0.3",
|
|
75
76
|
"dayjs": "^1.11.19",
|
|
76
77
|
"env-paths": "^4.0.0",
|
|
77
|
-
"eslint": "^
|
|
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.
|
|
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/
|
|
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.
|
|
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, --
|
|
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 ({
|
|
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
|
|
18
|
-
const realDirectories = await findRealDirectories(
|
|
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)
|
package/toolkit/convert.ts
CHANGED
|
@@ -15,10 +15,10 @@ convertCmd
|
|
|
15
15
|
.command('transform-at-signs')
|
|
16
16
|
.description('Transform @ signs to lstinline')
|
|
17
17
|
|
|
18
|
-
.option('-d, --
|
|
18
|
+
.option('-d, --directory <directory>', 'problem directory', '.')
|
|
19
19
|
|
|
20
|
-
.action(async ({
|
|
21
|
-
const realDirectories = await findRealDirectories(
|
|
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
|
+
})
|
package/toolkit/generate.ts
CHANGED
|
@@ -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(
|
|
39
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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('[
|
|
51
|
-
.option('-d, --directory <path>', 'problem directory when no arguments given', '.')
|
|
50
|
+
.argument('[directory]', 'problem directory to lint', '.')
|
|
52
51
|
|
|
53
|
-
.action(async (
|
|
54
|
-
const
|
|
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,
|
|
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, --
|
|
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, {
|
|
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(
|
|
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, --
|
|
18
|
+
.option('-d, --directory <directory>', 'problem directory', '.')
|
|
19
19
|
|
|
20
|
-
.action(async ({
|
|
21
|
-
const realDirectories = await findRealDirectories(
|
|
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)
|