@jpetit/toolkit 3.0.14 → 3.0.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.
@@ -0,0 +1,18 @@
1
+ import * as doc from '@/lib/doctor'
2
+ import { Command } from '@commander-js/extra-typings'
3
+ import tui from '@/lib/tui'
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 XeLaTeX installation', doc.checkXeLaTeX)
14
+ await tui.section('Checking Pandoc installation', doc.checkPandoc)
15
+ await tui.section('Checking ImageMagick installation', doc.checkImageMagick)
16
+ await tui.section('Checking AI models', doc.checkEnvVars)
17
+ await tui.section('Checking terminal', doc.checkTerminal)
18
+ })
@@ -0,0 +1,116 @@
1
+ import tui from '@/lib/tui'
2
+ import { Maker, newMaker, type MakerOptions } from '@/lib/maker'
3
+ import { guessUserEmail, guessUserName, nothing, readText, writeText, writeYaml } from '@/lib/utils'
4
+ import { Command } from '@commander-js/extra-typings'
5
+ import { languageNames } from '@/lib/data'
6
+ import { join } from 'path'
7
+ import { cleanMardownCodeString, complete } from '@/lib/ai'
8
+ import { getTitleFromStatement } from '@/lib/generate'
9
+ import { writeFile } from 'fs/promises'
10
+ import * as jdenticon from 'jdenticon'
11
+
12
+ type Options = {
13
+ directory: string
14
+ verbose: boolean
15
+ model: string
16
+ }
17
+
18
+ export const generate = new Command('generate')
19
+ .description('Generate additional components')
20
+ // Add common options here
21
+ .option('-d, --directory <path>', 'problem directory', process.cwd()) // TODO: use '.' when bun fixes it
22
+ .option('-v, --verbose', 'verbose output (TODO)')
23
+ .option(
24
+ '-m, --model <model>',
25
+ 'the AI model to use (eg: openai/gpt-5, google/gemini-2.5-pro, ...)',
26
+ 'google/gemini-2.5-flash-lite',
27
+ )
28
+
29
+ generate
30
+ .command('translations')
31
+ .description('Generate statement translations')
32
+ .argument('<languages...>', 'languages to generate')
33
+
34
+ .action(async (languages, options, command) => {
35
+ const parentOptions = (command.parent?.opts() || {}) as any as Options
36
+ const maker = await newMaker(parentOptions)
37
+
38
+ await tui.section('Generating translations', async () => {
39
+ for (const language of languages) {
40
+ await generateTranslation(parentOptions.model, maker, language)
41
+ }
42
+ })
43
+ })
44
+
45
+ generate
46
+ .command('award')
47
+ .description('Generate random award')
48
+
49
+ .action(async (options, command) => {
50
+ const parentOptions = (command.parent?.opts() || {}) as any as Options
51
+ const maker = await newMaker(parentOptions)
52
+
53
+ await tui.section('Generating award', async () => {
54
+ const png = jdenticon.toPng(maker.directory, 200)
55
+ await writeFile(join(maker.directory, 'award.png'), png)
56
+ await writeText(join(maker.directory, 'award.html'), '<b>Well done!</b>\n')
57
+ tui.success(
58
+ 'Award generated: ' +
59
+ tui.hyperlink(maker.directory, 'award.png') +
60
+ ' and ' +
61
+ tui.hyperlink(maker.directory, 'award.html'),
62
+ )
63
+ await tui.image(join(maker.directory, 'award.png'), 6, 3)
64
+ })
65
+ })
66
+
67
+ generate
68
+ .command('solutions')
69
+ .description('Generate solutions TODO')
70
+ .argument('<proglangs...>', 'programming languages to generate')
71
+
72
+ .action(async (proglangs, options, command) => {
73
+ await nothing()
74
+ throw new Error('Not implemented yet')
75
+ })
76
+
77
+ async function generateTranslation(model: string, maker: Maker, language: string) {
78
+ const original = maker.originalLanguage
79
+ if (!original) {
80
+ throw new Error('Original language not set, cannot generate translations')
81
+ }
82
+ if (language === original) {
83
+ throw new Error(`Language ${language} is the original language`)
84
+ }
85
+ if (maker.languages.includes(language)) {
86
+ throw new Error(`Language ${language} already exists`)
87
+ }
88
+
89
+ const statememt = await readText(join(maker.directory, `problem.${original}.tex`))
90
+
91
+ await tui.section(`Translating from ${languageNames[original]} to ${languageNames[language]}`, async () => {
92
+ const prompt = `
93
+ Translate the given problem statement to ${languageNames[language]}.
94
+
95
+ The translation must be accurate and use proper technical terminology.
96
+ Maintain the LaTeX formatting and macros.
97
+ The texts that the program must read and write should not be translated.
98
+ `
99
+
100
+ tui.action(`Generating translation with ${model}`)
101
+
102
+ const answer = cleanMardownCodeString(await complete(model, prompt, statememt))
103
+
104
+ const info = {
105
+ title: getTitleFromStatement(answer) || 'Error translating title',
106
+ translator: (await guessUserName()) || 'Unknown',
107
+ translator_email: (await guessUserEmail()) || 'unknown',
108
+ model,
109
+ }
110
+ await writeText(join(maker.directory, `problem.${language}.tex`), answer)
111
+ await writeYaml(join(maker.directory, `problem.${language}.yml`), info)
112
+
113
+ tui.success(`Translation completed: files problem.${language}.tex and problem.${language}.yml created`)
114
+ tui.warning(`Please review the generated translation`)
115
+ })
116
+ }
package/toolkit/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { projectDir, readJson } from '@/lib/utils'
4
3
  import { program } from '@commander-js/extra-typings'
5
4
  import { confirm } from '@inquirer/prompts'
6
5
  import { join } from 'path'
@@ -13,6 +12,7 @@ import { init } from './init'
13
12
  import { make } from './make'
14
13
  import { verify } from './verify'
15
14
  import { generate } from './generate'
15
+ import { projectDir, readJson } from '../lib/utils'
16
16
 
17
17
  const packageJson = await readJson(join(projectDir(), 'package.json'))
18
18
 
@@ -0,0 +1,56 @@
1
+ import { initializePaths, loadSettings, paths, saveSettings, settingsExist } from '@/lib/settings'
2
+ import tui from '@/lib/tui'
3
+ import { guessUserEmail, guessUserName, nothing } from '@/lib/utils'
4
+ import { Command } from '@commander-js/extra-typings'
5
+ import { confirm, input } from '@inquirer/prompts'
6
+ import chalk from 'chalk'
7
+
8
+ async function initialize() {
9
+ await tui.section('Initialize', async () => {
10
+ let name = (await guessUserName()) || 'John Doe'
11
+ let email = (await guessUserEmail()) || 'john.doe@example.com'
12
+
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 = await confirm({ message: 'Enable notifications?', default: 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 notifications = await confirm({ message: 'Enable notifications?', default: settings.notifications })
31
+
32
+ await saveSettings({ name, email, notifications })
33
+
34
+ tui.success('Settings updated')
35
+ })
36
+ }
37
+
38
+ export const init = new Command('init')
39
+ .description('Initialize/configure the toolkit')
40
+
41
+ .action(async () => {
42
+ if (await settingsExist()) {
43
+ await configure()
44
+ } else {
45
+ await initialize()
46
+ }
47
+
48
+ await tui.section('Directories', async () => {
49
+ await nothing()
50
+ console.log('config:', chalk.magenta(paths.config))
51
+ console.log('data: ', chalk.magenta(paths.data))
52
+ console.log('cache: ', chalk.magenta(paths.cache))
53
+ console.log('log: ', chalk.magenta(paths.log))
54
+ console.log('temp: ', chalk.magenta(paths.temp))
55
+ })
56
+ })
@@ -0,0 +1,109 @@
1
+ import tui from '@/lib/tui'
2
+ import { newMaker, type MakerOptions } from '@/lib/maker'
3
+ import { nothing, projectDir } from '@/lib/utils'
4
+ import { Command } from '@commander-js/extra-typings'
5
+ import { join, resolve } from 'path'
6
+ import { languageNames } from '@/lib/data'
7
+ import { exists } from 'fs/promises'
8
+
9
+ /*
10
+ Find real problem directories from a list of directories. A real problem directory is one that contains a handler.yml file.
11
+ If a directory does not contain a handler.yml file, check if it contains subdirectories for each language that contain a handler.yml file.
12
+ If so, add those subdirectories to the list of real problem directories.
13
+ */
14
+ async function findRealDirectories(directories: string[]): Promise<string[]> {
15
+ const realDirectories: string[] = []
16
+ for (const dir of directories) {
17
+ if (await exists(join(dir, 'handler.yml'))) {
18
+ realDirectories.push(dir)
19
+ } else {
20
+ for (const language of Object.keys(languageNames).sort()) {
21
+ const child = join(dir, language, 'handler.yml')
22
+ if (await exists(child)) {
23
+ realDirectories.push(resolve(dir, language))
24
+ }
25
+ }
26
+ }
27
+ }
28
+ return realDirectories
29
+ }
30
+
31
+ type TaskType = 'all' | 'inspection' | 'executables' | 'corrects' | 'pdf' | 'text'
32
+
33
+ interface MakeOptions extends MakerOptions {
34
+ directories: string[]
35
+ tasks: TaskType[]
36
+ ignoreErrors: boolean
37
+ verbose?: boolean
38
+ }
39
+
40
+ export const make = new Command('make')
41
+ .description('Make problem components')
42
+ .argument('[directories...]', 'problem directories', ['.'])
43
+ .option('-i, --ignore-errors', 'ignore errors on a directory and continue processing', false)
44
+ .option('-t, --tasks <tasks...>', 'tasks to run: all, inspection, executables, corrects, pdf, text', ['all'])
45
+ .option('-v, --verbose', 'verbose output (TODO)', false)
46
+
47
+ .action(async (directories, options) => {
48
+ console.log()
49
+ await tui.image(join(projectDir(), 'assets', 'images', 'jutge-toolkit.png'), 8, 4)
50
+
51
+ const { tasks, ignoreErrors, ...makerOptions } = options as MakeOptions
52
+ const errors: Record<string, string> = {} // directory -> error message
53
+
54
+ const realDirectories = await findRealDirectories(directories)
55
+ // console.log(realDirectories)
56
+
57
+ for (const directory of realDirectories) {
58
+ try {
59
+ tui.title(`Making problem in directory ${tui.hyperlink(directory, resolve(directory))}`)
60
+
61
+ const maker = await newMaker({ ...makerOptions, directory })
62
+
63
+ // If tasks include 'all', run makeProblem
64
+ if (tasks.includes('all')) {
65
+ await maker.makeProblem()
66
+ } else {
67
+ // Run specific tasks
68
+ if (tasks.includes('inspection')) {
69
+ // already done in maker initialization
70
+ }
71
+ if (tasks.includes('executables')) {
72
+ await maker.makeExecutables()
73
+ }
74
+ if (tasks.includes('corrects')) {
75
+ await maker.makeCorrects()
76
+ }
77
+ if (tasks.includes('pdf')) {
78
+ await maker.makePdfs()
79
+ }
80
+ if (tasks.includes('text')) {
81
+ await maker.makeTexts()
82
+ }
83
+ }
84
+ } catch (error) {
85
+ const errorMessage = error instanceof Error ? error.message : String(error)
86
+
87
+ if (ignoreErrors) {
88
+ errors[directory] = errorMessage
89
+ tui.error(`Error: ${errorMessage}`)
90
+ } else {
91
+ throw error
92
+ }
93
+ }
94
+ console.log()
95
+ }
96
+
97
+ tui.title('Summary')
98
+ await tui.section('', async () => {
99
+ await nothing()
100
+ for (const directory of realDirectories) {
101
+ tui.directory(directory)
102
+ if (errors[directory]) {
103
+ tui.error(` ${errors[directory]}`)
104
+ } else {
105
+ tui.success(` No errors found`)
106
+ }
107
+ }
108
+ })
109
+ })
@@ -0,0 +1,19 @@
1
+ import { newMaker } from '@/lib/maker'
2
+ import { Command } from '@commander-js/extra-typings'
3
+ import { normalize } from 'path'
4
+
5
+ export const verify = new Command('verify')
6
+ .description('Verify candidate programs')
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
+ })