@jpetit/toolkit 3.1.1 → 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.
package/lib/doctor.ts ADDED
@@ -0,0 +1,238 @@
1
+ import { execa } from 'execa'
2
+ import terminalLink from 'terminal-link'
3
+ import tui from './tui'
4
+ import { nothing } from './utils'
5
+
6
+ export async function probePython3(showInfo: boolean = false): Promise<boolean> {
7
+ if (showInfo) tui.command('python3 --version')
8
+ const { stdout } = await execa({ reject: false })`python3 --version`
9
+ const version = stdout.trim()
10
+ if (showInfo) console.log(version)
11
+ return version.startsWith('Python 3')
12
+ }
13
+
14
+ export async function probePythonModule(showInfo: boolean = false, module: string): Promise<boolean> {
15
+ if (showInfo) tui.command(`python3 -m pip show ${module}`)
16
+ const { exitCode } = await execa({ reject: false })`python3 -m pip show ${module}`
17
+ if (showInfo) {
18
+ if (exitCode === 0) {
19
+ tui.success(`Module ${module} is installed`)
20
+ } else {
21
+ tui.warning(`Module ${module} is not installed`)
22
+ }
23
+ }
24
+ return exitCode === 0
25
+ }
26
+
27
+ export async function probeGCC(showInfo: boolean = false): Promise<boolean> {
28
+ if (showInfo) tui.command('g++ --version')
29
+ const { stdout } = await execa({ reject: false })`g++ --version`
30
+ const version = stdout.split('\n')[0]!.trim()
31
+ if (showInfo) console.log(version)
32
+ return stdout.startsWith('Apple clang') || stdout.startsWith('g++')
33
+ }
34
+
35
+ export async function probeHaskell(showInfo: boolean = false): Promise<boolean> {
36
+ if (showInfo) tui.command('ghc --version')
37
+ const { stdout } = await execa({ reject: false })`ghc --version`
38
+ const version = stdout.split('\n')[0]!.trim()
39
+ if (showInfo) console.log(version)
40
+ return stdout.startsWith('The Glorious Glasgow Haskell Compilation System')
41
+ }
42
+
43
+ export async function probeClojure(showInfo: boolean = false): Promise<boolean> {
44
+ if (showInfo) tui.command('clj --version')
45
+ const { stdout } = await execa({ reject: false })`clj --version`
46
+ const version = stdout.split('\n')[0]!.trim()
47
+ if (showInfo) console.log(version)
48
+ return stdout.startsWith('Clojure')
49
+ }
50
+
51
+ export async function probeJava(showInfo: boolean = false): Promise<boolean> {
52
+ if (showInfo) tui.command('javac -version')
53
+ const { stdout } = await execa({ reject: false })`javac -version`
54
+ const version = stdout.split('\n')[0]!.trim()
55
+ if (showInfo) console.log(version)
56
+ return stdout.startsWith('javac')
57
+ }
58
+
59
+ export async function probeRust(showInfo: boolean = false): Promise<boolean> {
60
+ if (showInfo) tui.command('rustc --version')
61
+ const { stdout } = await execa({ reject: false })`rustc --version`
62
+ const version = stdout.split('\n')[0]!.trim()
63
+ if (showInfo) console.log(version)
64
+ return stdout.startsWith('rustc')
65
+ }
66
+
67
+ export async function probePdfLaTeX(showInfo: boolean = false): Promise<boolean> {
68
+ if (showInfo) tui.command('pdflatex --version')
69
+ const { stdout } = await execa({ reject: false })`pdflatex --version`
70
+ const version = stdout.split('\n')[0]!.trim()
71
+ if (showInfo) console.log(version)
72
+ return stdout.startsWith('pdfTeX')
73
+ }
74
+
75
+ export async function probeXeLaTeX(showInfo: boolean = false): Promise<boolean> {
76
+ if (showInfo) tui.command('xelatex --version')
77
+ const { stdout } = await execa({ reject: false })`xelatex --version`
78
+ const version = stdout.split('\n')[0]!.trim()
79
+ if (showInfo) console.log(version)
80
+ return stdout.startsWith('XeTeX')
81
+ }
82
+
83
+ export async function probePandoc(showInfo: boolean = false): Promise<boolean> {
84
+ if (showInfo) tui.command('pandoc --version')
85
+ const { stdout } = await execa({ reject: false })`pandoc --version`
86
+ const version = stdout.split('\n')[0]!.trim()
87
+ if (showInfo) console.log(version)
88
+ return stdout.startsWith('pandoc') && stdout.includes('+lua')
89
+ }
90
+
91
+ export async function probeImageMagick(showInfo: boolean = false): Promise<boolean> {
92
+ if (showInfo) tui.command('magick --version')
93
+ const { stdout } = await execa({ reject: false })`magick --version`
94
+ const version = stdout.split('\n')[0]!.trim()
95
+ if (showInfo) console.log(version)
96
+ return stdout.startsWith('Version: ImageMagick')
97
+ }
98
+
99
+ export async function checkPython3(): Promise<void> {
100
+ if (await probePython3(true)) {
101
+ tui.success('Python3 seems installed')
102
+ const modules = 'turtle-pil yogi easyinput'.split(' ')
103
+ for (const m of modules) {
104
+ await probePythonModule(true, m)
105
+ }
106
+ } else {
107
+ tui.warning('Python3 does not appear to be installed')
108
+ tui.print('This is not a problem if you do not plan to use Python solutions')
109
+ tui.print('See https://www.python.org/downloads/')
110
+ }
111
+ }
112
+ export async function checkGCC(): Promise<void> {
113
+ if (await probeGCC(true)) {
114
+ tui.success('C/C++ seems installed')
115
+ } else {
116
+ tui.warning('C/C++ does not appear to be installed')
117
+ tui.print('This is not a problem if you do not plan to use C or C++ solutions')
118
+ tui.print('Please install GCC or Clang')
119
+ }
120
+ }
121
+
122
+ export async function checkHaskell(): Promise<void> {
123
+ if (await probeHaskell(true)) {
124
+ tui.success('Haskell seems installed')
125
+ } else {
126
+ tui.warning('Haskell does not appear to be installed')
127
+ tui.print('This is not a problem if you do not plan to use Haskell solutions')
128
+ tui.print('See https://www.haskell.org/ghc/download.html')
129
+ }
130
+ }
131
+
132
+ export async function checkClojure(): Promise<void> {
133
+ if (await probeClojure(true)) {
134
+ tui.success('Clojure seems installed')
135
+ } else {
136
+ tui.warning('Clojure does not appear to be installed')
137
+ tui.print('This is not a problem if you do not plan to use Clojure solutions')
138
+ tui.print('See https://clojure.org/guides/getting_started')
139
+ }
140
+ }
141
+
142
+ export async function checkJava(): Promise<void> {
143
+ if (await probeJava(true)) {
144
+ tui.success('Java seems installed')
145
+ } else {
146
+ tui.warning('Java does not appear to be installed')
147
+ tui.print('You will not be able to compile/execute Java solutions')
148
+ tui.print('See https://www.oracle.com/java/technologies/javase-jdk11-downloads.html')
149
+ }
150
+ }
151
+
152
+ export async function checkRust(): Promise<void> {
153
+ if (await probeRust(true)) {
154
+ tui.success('Rust seems installed')
155
+ } else {
156
+ tui.warning('Rust does not appear to be installed')
157
+ tui.print('You will not be able to compile/execute Rust solutions')
158
+ tui.print('See https://www.rust-lang.org/tools/install')
159
+ }
160
+ }
161
+
162
+ export async function checkPdfLaTeX(): Promise<void> {
163
+ if (await probePdfLaTeX(true)) {
164
+ tui.success('LaTeX seems installed')
165
+ } else {
166
+ tui.warning('LaTeX does not appear to be installed')
167
+ tui.print('You will not be able to generate PDF statements')
168
+ tui.print('TODO: Provide instructions for installing LaTeX')
169
+ }
170
+ }
171
+
172
+ export async function checkXeLaTeX(): Promise<void> {
173
+ if (await probeXeLaTeX(true)) {
174
+ tui.success('XeLaTeX seems installed')
175
+ } else {
176
+ tui.warning('XeLaTeX does not appear to be installed')
177
+ tui.print('You will not be able to generate PDF statements with Unicode support')
178
+ tui.print('TODO: Provide instructions for installing XeLaTeX')
179
+ }
180
+ }
181
+
182
+ export async function checkPandoc(): Promise<void> {
183
+ if (await probePandoc(true)) {
184
+ tui.success('Pandoc with Lua support seems installed')
185
+ } else {
186
+ tui.warning('Pandoc with Lua support does not appear to be installed')
187
+ tui.print('You will not be able to generate text statements (HTML, TXT, MD)')
188
+ tui.print('See https://pandoc.org/installing.html')
189
+ }
190
+ }
191
+
192
+ export async function checkImageMagick(): Promise<void> {
193
+ if (await probeImageMagick(true)) {
194
+ tui.success('ImageMagick seems installed')
195
+ } else {
196
+ tui.warning('ImageMagick does not appear to be installed')
197
+ tui.print('You will not be able to convert images for text statements')
198
+ tui.print('See https://imagemagick.org/script/download.php')
199
+ }
200
+ }
201
+
202
+ export async function checkAIEnvVars(): Promise<void> {
203
+ await nothing()
204
+ const vars = ['OPENAI_API_KEY', 'GEMINI_API_KEY']
205
+ for (const v of vars) {
206
+ if (process.env[v]) {
207
+ tui.success(`${v} environment variable is set`)
208
+ } else {
209
+ tui.warning(`${v} environment variable is not set`)
210
+ tui.print(`You will not be able to use related AI models`)
211
+ }
212
+ }
213
+ }
214
+
215
+ export async function checkTerminal(): Promise<void> {
216
+ await nothing()
217
+ if (process.stdout.isTTY) {
218
+ tui.success('Terminal supports TTY')
219
+ } else {
220
+ tui.warning('Terminal does not support TTY')
221
+ tui.print('Some output features may not work as expected')
222
+ }
223
+ if (terminalLink.isSupported) {
224
+ tui.success('Terminal supports hyperlinks')
225
+ } else {
226
+ tui.warning('Terminal does not support hyperlinks')
227
+ tui.print('Links to files may not be clickable')
228
+ }
229
+
230
+ const vars = ['EDITOR', 'VISUAL']
231
+ for (const v of vars) {
232
+ if (process.env[v]) {
233
+ tui.success(`${v} environment variable is set to "${process.env[v]}"`)
234
+ } else {
235
+ tui.warning(`${v} environment variable is not set`)
236
+ }
237
+ }
238
+ }
@@ -0,0 +1,171 @@
1
+ import Handlebars from 'handlebars'
2
+ import { join } from 'path'
3
+ import { cleanMardownCodeString, complete } from './ai'
4
+ import { languageKeys, languageNames, proglangExtensions, proglangKeys, proglangNames } from './data'
5
+ import { getPromptForProglang, getTitleFromStatement } from './helpers'
6
+ import type { Inspector } from './inspector'
7
+ import { settings } from './settings'
8
+ import tui from './tui'
9
+ import { projectDir, readText, readTextInDir, stripLaTeX, writeTextInDir, writeYamlInDir } from './utils'
10
+ import { exists } from 'fs/promises'
11
+
12
+ export async function addStatementTranslation(model: string, inspector: Inspector, language: string) {
13
+ const original = inspector.originalLanguage
14
+
15
+ if (!original) {
16
+ throw new Error('Original language not set, cannot generate translations')
17
+ }
18
+ if (language === original) {
19
+ throw new Error(`Language ${language} is the original language`)
20
+ }
21
+ if (inspector.languages.includes(language)) {
22
+ throw new Error(`Language ${language} already exists`)
23
+ }
24
+ if (!languageKeys.includes(language)) {
25
+ throw new Error(`Language ${language} is not supported`)
26
+ }
27
+
28
+ const statememt = await readTextInDir(inspector.directory, `problem.${original}.tex`)
29
+
30
+ await tui.section(`Translating from ${languageNames[original]} to ${languageNames[language]}`, async () => {
31
+ const prompt = `
32
+ Translate the given problem statement to ${languageNames[language]}.
33
+
34
+ The translation must be accurate and use proper technical terminology.
35
+ Maintain the LaTeX formatting and macros.
36
+ The texts that the program must read and write should not be translated.
37
+ `
38
+
39
+ const answer = cleanMardownCodeString(await complete(model, prompt, statememt))
40
+
41
+ const info = {
42
+ title: getTitleFromStatement(answer) || 'Error translating title',
43
+ translator: settings.name,
44
+ translator_email: settings.email,
45
+ model,
46
+ }
47
+ await writeTextInDir(inspector.directory, `problem.${language}.tex`, answer)
48
+ await writeYamlInDir(inspector.directory, `problem.${language}.yml`, info)
49
+
50
+ tui.success(`Translation completed: files problem.${language}.tex and problem.${language}.yml created`)
51
+ tui.warning(`Please review the generated translation`)
52
+ })
53
+ }
54
+
55
+ export async function addAlternativeSolution(model: string, inspector: Inspector, extension: string) {
56
+ if (!inspector.goldenSolution) {
57
+ throw new Error('No golden solution defined')
58
+ }
59
+ const originalExtension = inspector.goldenSolution.split('.').pop()
60
+ if (!originalExtension) {
61
+ throw new Error('Invalid golden solution filename')
62
+ }
63
+ if (!proglangKeys.includes(extension)) {
64
+ throw new Error(`Extension ${extension} is not supported`)
65
+ }
66
+ if (inspector.solutions.includes(`solution.${extension}`)) {
67
+ throw new Error(`Solution already exists`)
68
+ }
69
+ const proglang = proglangNames[extension]
70
+
71
+ if (!proglang) {
72
+ throw new Error(`Programming language for extension ${extension} not found`)
73
+ }
74
+
75
+ const originalProglang = proglangNames[originalExtension]
76
+
77
+ const goldenSource = await readTextInDir(inspector.directory, inspector.goldenSolution)
78
+
79
+ const proglangPrompt = await getPromptForProglang(proglang)
80
+
81
+ const prompt = `
82
+ Convert the given program in ${originalProglang} to ${proglang}.
83
+
84
+ The translation must be accurate and follow the structure and logic of the original program.
85
+ Each function should have a documentation comment explaining its purpose, parameters, and return values.
86
+
87
+ If some function in the original program is not defined, do not define it in the translated program either.
88
+
89
+ Your program should start with a comment line saying 'Generated by ${model}'.
90
+
91
+ ${proglangPrompt}
92
+ `
93
+
94
+ const answer = cleanMardownCodeString(await complete(model, prompt, goldenSource))
95
+
96
+ await writeTextInDir(inspector.directory, `solution.${extension}`, answer)
97
+
98
+ tui.success(`solution.${extension} created`)
99
+ tui.warning(`Please review the generated solution`)
100
+ }
101
+
102
+ // TODO: check this function
103
+ export async function addMainFile(model: string, inspector: Inspector, proglang: string) {
104
+ if (!inspector.goldenSolution) {
105
+ throw new Error('No golden solution defined')
106
+ }
107
+
108
+ const originalExtension = inspector.goldenSolution.split('.').pop()
109
+ if (!originalExtension) {
110
+ throw new Error('Invalid golden solution filename')
111
+ }
112
+ const originalProglang = proglangNames[originalExtension]
113
+
114
+ const goldenSource = await readTextInDir(inspector.directory, `main.${originalExtension}`)
115
+
116
+ await tui.section(`Converting main.${originalExtension} to ${proglang}`, async () => {
117
+ const proglangPrompt = await getPromptForProglang(proglang)
118
+
119
+ const prompt = `
120
+ Convert the given program in ${originalProglang} to ${proglang}.
121
+
122
+ The translation must be accurate and follow the structure and logic of the original program.
123
+
124
+ If some function in the original program is not defined, do not define it in the translated program either.
125
+
126
+ Your program should start with a comment line saying 'Generated by ${model}'.
127
+
128
+ ${proglangPrompt}
129
+ `
130
+
131
+ const answer = cleanMardownCodeString(await complete(model, prompt, goldenSource))
132
+
133
+ await writeTextInDir(inspector.directory, `main.${proglangExtensions[proglang]}`, answer)
134
+
135
+ tui.success(`main.${proglang} created`)
136
+ tui.warning(`Please review the generated file`)
137
+ })
138
+ }
139
+
140
+ export async function generateTestCasesGenerator(
141
+ model: string,
142
+ inspector: Inspector,
143
+ output: string, // file to create (template with {{type}})
144
+ type: 'random' | 'hard' | 'efficiency',
145
+ ) {
146
+ const outputPath = Handlebars.compile(output)({ type })
147
+ await tui.section(`Generating test cases generator ${outputPath}`, async () => {
148
+ const statement = await getStatementAsText(inspector)
149
+ const promptPath = join(projectDir(), 'assets', 'prompts', 'generators', `${type}.md`)
150
+ const promptTemplate = await readText(promptPath)
151
+ const prompt = Handlebars.compile(promptTemplate)({ statement })
152
+ const answer = cleanMardownCodeString(await complete(model, '', prompt))
153
+ await writeTextInDir(inspector.directory, outputPath, answer)
154
+ tui.success(`Test cases generator ${outputPath} generated`)
155
+ tui.warning(`Please review the generated test cases generator`)
156
+ })
157
+ }
158
+
159
+ // TODO: move into inspector?
160
+ export async function getStatementAsText(inspector: Inspector): Promise<string> {
161
+ const original = inspector.originalLanguage
162
+ if (!original) throw new Error('Original language not set')
163
+ if (await exists(join(inspector.directory, `problem.${original}.txt`))) {
164
+ const text = await readTextInDir(inspector.directory, `problem.${original}.txt`)
165
+ return text
166
+ } else {
167
+ const latex = await readTextInDir(inspector.directory, `problem.${original}.tex`)
168
+ const text = stripLaTeX(latex)
169
+ return text
170
+ }
171
+ }
package/lib/helpers.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { exists } from 'fs/promises'
2
+ import { projectDir, readText } from './utils'
3
+ import { join } from 'path'
4
+ import { normalize, resolve } from 'node:path'
5
+ import { languageNames } from './data'
6
+
7
+ export function getTitleFromStatement(statement: string): string | null {
8
+ const pattern = /\\Problem\{(.*?)\}/
9
+ const match = statement.match(pattern)
10
+ if (match) {
11
+ return match[1]!.trim()
12
+ }
13
+ return null
14
+ }
15
+
16
+ export async function getPromptForProglang(proglang: string): Promise<string> {
17
+ const location = join(projectDir(), 'assets', 'prompts', `${proglang}.md`)
18
+ if (await exists(location)) {
19
+ return await readText(location)
20
+ } else {
21
+ return ''
22
+ }
23
+ }
24
+
25
+ /*
26
+ Find real problem directories from a list of directories. A real problem directory is one that contains a handler.yml file.
27
+ If a directory does not contain a handler.yml file, check if it contains subdirectories for each language that contain a handler.yml file.
28
+ If so, add those subdirectories to the list of real problem directories.
29
+ */
30
+ export async function findRealDirectories(directories: string[]): Promise<string[]> {
31
+ const realDirectories: string[] = []
32
+ for (let dir of directories) {
33
+ if (dir === '.') {
34
+ dir = normalize(resolve(process.cwd()))
35
+ }
36
+ if (await exists(join(dir, 'handler.yml'))) {
37
+ realDirectories.push(dir)
38
+ } else {
39
+ for (const language of Object.keys(languageNames).sort()) {
40
+ const child = join(dir, language, 'handler.yml')
41
+ if (await exists(child)) {
42
+ realDirectories.push(resolve(dir, language))
43
+ }
44
+ }
45
+ }
46
+ }
47
+ return realDirectories
48
+ }