@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,96 @@
1
+ import tui from '@/lib/tui'
2
+ import { execa } from 'execa'
3
+ import { cp, exists, rm } from 'fs/promises'
4
+ import { join } from 'path'
5
+ import { readText, writeText } from '@/lib/utils'
6
+ import { Compiler } from './base'
7
+ import type { Handler } from '@/lib/types'
8
+
9
+ export class Python3_Compiler extends Compiler {
10
+ id(): string {
11
+ return 'Python3'
12
+ }
13
+
14
+ name(): string {
15
+ return 'Python3'
16
+ }
17
+
18
+ type(): string {
19
+ return 'interpreter'
20
+ }
21
+
22
+ language(): string {
23
+ return 'Python'
24
+ }
25
+
26
+ async version(): Promise<string> {
27
+ return await this.getVersion('python3 --version', 0)
28
+ }
29
+
30
+ flags1(): string {
31
+ return ''
32
+ }
33
+
34
+ flags2(): string {
35
+ return ''
36
+ }
37
+
38
+ extension(): string {
39
+ return 'py'
40
+ }
41
+
42
+ async compile(handler: Handler, directory: string, sourcePath: string): Promise<void> {
43
+ const exePath = sourcePath + '.exe'
44
+
45
+ if (handler.source_modifier === 'none') {
46
+ tui.command(`tweak ${sourcePath} to ${exePath}`, { italic: true })
47
+ const source = await readText(join(directory, sourcePath))
48
+ const sourceModified = source
49
+ .replace('import turtle', 'import turtle_pil as turtle')
50
+ .replace('from turtle import', 'from turtle_pil import')
51
+ await writeText(join(directory, exePath), sourceModified)
52
+ } else if (handler.source_modifier === 'no_main' || handler.source_modifier === 'structs') {
53
+ tui.command(`cat ${sourcePath} main.py > ${exePath}`)
54
+ const solution = await readText(join(directory, sourcePath))
55
+ const main = await readText(join(directory, 'main.py'))
56
+ const solutionWithMain = solution + '\n\n\n' + main
57
+ await writeText(join(directory, exePath), solutionWithMain)
58
+ } else {
59
+ throw new Error(`Unknown source modifier: ${handler.source_modifier as string}`)
60
+ }
61
+
62
+ tui.command(`python3 -m py_compile ${exePath}`)
63
+
64
+ const { exitCode } = await execa({
65
+ reject: false,
66
+ stderr: 'inherit',
67
+ stdout: 'inherit',
68
+ cwd: directory,
69
+ })`python3 -m py_compile ${exePath}`
70
+
71
+ if (exitCode !== 0) throw new Error(`Compilation failed for ${sourcePath}`)
72
+ }
73
+
74
+ override async execute(handler: Handler, directory: string, inputPath: string, outputPath: string): Promise<void> {
75
+ const exePath = 'solution.py.exe'
76
+ if (!(await exists(join(directory, exePath)))) {
77
+ throw new Error(`Executable file ${exePath} does not exist in directory ${directory}`)
78
+ }
79
+ const fullInputPath = join(directory, inputPath)
80
+ const fullOutputPath = join(directory, outputPath)
81
+ await rm(fullOutputPath, { force: true })
82
+ const input = await readText(fullInputPath)
83
+
84
+ tui.command(`python3 ${exePath} < ${inputPath} > ${outputPath}`)
85
+
86
+ const { exitCode } = await execa({
87
+ reject: false,
88
+ input,
89
+ stdout: { file: fullOutputPath },
90
+ stderr: 'inherit',
91
+ cwd: directory,
92
+ })`python3 ${exePath}`
93
+
94
+ if (exitCode !== 0) throw new Error(`Execution failed for ${exePath}`)
95
+ }
96
+ }
@@ -0,0 +1,107 @@
1
+ import tui from '@/lib/tui'
2
+ import { execa } from 'execa'
3
+ import { cp, exists, rm } from 'fs/promises'
4
+ import { join } from 'path'
5
+ import { readText, writeText } from '@/lib/utils'
6
+ import { Compiler } from './base'
7
+ import type { Handler } from '@/lib/types'
8
+
9
+ export class RunHaskell_Compiler extends Compiler {
10
+ id(): string {
11
+ return 'RunHaskell'
12
+ }
13
+
14
+ name(): string {
15
+ return 'RunHaskell'
16
+ }
17
+
18
+ type(): string {
19
+ return 'compiler'
20
+ }
21
+
22
+ language(): string {
23
+ return 'Haskell'
24
+ }
25
+
26
+ async version(): Promise<string> {
27
+ return await this.getVersion('ghc --version', 0)
28
+ }
29
+
30
+ flags1(): string {
31
+ return ''
32
+ }
33
+
34
+ flags2(): string {
35
+ return ''
36
+ }
37
+
38
+ extension(): string {
39
+ return 'hs'
40
+ }
41
+
42
+ async compile(handler: Handler, directory: string, sourcePath: string): Promise<void> {
43
+ if (handler.source_modifier !== 'none') {
44
+ throw new Error(`Unknown source modifier: ${handler.source_modifier as string}`)
45
+ }
46
+
47
+ // ghci -e ':q' solution.hs
48
+ // This will load and typecheck the file, then immediately quit.
49
+ // If there are compilation errors, they'll be shown. If it loads successfully and just exits, the code compiles.
50
+ // With execa, it seems we have to remove the quotes around :q
51
+
52
+ tui.command(`ghci -e ':q' ${sourcePath}`)
53
+
54
+ const { exitCode } = await execa({
55
+ reject: false,
56
+ stderr: 'inherit',
57
+ stdout: 'inherit',
58
+ cwd: directory,
59
+ })`ghci -e :q ${sourcePath}`
60
+
61
+ if (exitCode !== 0) throw new Error(`Compilation failed for ${sourcePath}`)
62
+ }
63
+
64
+ override async execute(handler: Handler, directory: string, inputPath: string, outputPath: string): Promise<void> {
65
+ const mergedPath = `solution-${inputPath}.hs.exe`
66
+
67
+ tui.command(`Merging solution.hs and ${inputPath} into ${mergedPath}`, { italic: true })
68
+ await this.mergeScripts(directory, 'solution.hs', inputPath, mergedPath)
69
+
70
+ const fullOutputPath = join(directory, outputPath)
71
+
72
+ await rm(fullOutputPath, { force: true })
73
+
74
+ tui.command(`runhaskell ${mergedPath} > ${outputPath}`)
75
+
76
+ const { exitCode } = await execa({
77
+ reject: false,
78
+ stdout: { file: fullOutputPath },
79
+ stderr: 'inherit',
80
+ cwd: directory,
81
+ })`runhaskell ${mergedPath}`
82
+
83
+ if (exitCode !== 0) throw new Error(`Execution failed for ${mergedPath}`)
84
+ }
85
+
86
+ async mergeScripts(
87
+ directory: string,
88
+ scriptPath1: string,
89
+ scriptPath2: string,
90
+ outputScriptPath: string,
91
+ ): Promise<void> {
92
+ const script1 = await readText(join(directory, scriptPath1))
93
+ const script2 = await readText(join(directory, scriptPath2))
94
+ let mergedScript = script1
95
+ mergedScript += '\n\n\nmain = do\n'
96
+ for (const line of script2.split('\n')) {
97
+ if (line.trim() === '') {
98
+ mergedScript += '\n'
99
+ } else if (line.startsWith('let')) {
100
+ mergedScript += ` ${line}\n`
101
+ } else {
102
+ mergedScript += ` print $ ${line}\n`
103
+ }
104
+ }
105
+ await writeText(join(directory, outputScriptPath), mergedScript)
106
+ }
107
+ }
@@ -0,0 +1,103 @@
1
+ import tui from '@/lib/tui'
2
+ import { execa } from 'execa'
3
+ import { cp, exists, rm } from 'fs/promises'
4
+ import { join } from 'path'
5
+ import { readText, writeText } from '@/lib/utils'
6
+ import { Compiler } from './base'
7
+ import type { Handler } from '@/lib/types'
8
+
9
+ export class RunPython_Compiler extends Compiler {
10
+ id(): string {
11
+ return 'RunPython'
12
+ }
13
+
14
+ name(): string {
15
+ return 'RunPython'
16
+ }
17
+
18
+ type(): string {
19
+ return 'interpreter'
20
+ }
21
+
22
+ language(): string {
23
+ return 'Python'
24
+ }
25
+
26
+ async version(): Promise<string> {
27
+ return await this.getVersion('python3 --version', 0)
28
+ }
29
+
30
+ flags1(): string {
31
+ return ''
32
+ }
33
+
34
+ flags2(): string {
35
+ return ''
36
+ }
37
+
38
+ extension(): string {
39
+ return 'py'
40
+ }
41
+
42
+ async compile(handler: Handler, directory: string, sourcePath: string): Promise<void> {
43
+ const exePath = sourcePath + '.exe'
44
+
45
+ if (handler.source_modifier === 'none') {
46
+ tui.command(`cp ${sourcePath} ${exePath}`)
47
+ await cp(join(directory, sourcePath), join(directory, exePath))
48
+ } else {
49
+ throw new Error(`Unknown source modifier: ${handler.source_modifier as string}`)
50
+ }
51
+
52
+ tui.command(`python3 -m py_compile ${exePath}`)
53
+
54
+ const { exitCode } = await execa({
55
+ reject: false,
56
+ stderr: 'inherit',
57
+ stdout: 'inherit',
58
+ cwd: directory,
59
+ })`python3 -m py_compile ${exePath}`
60
+
61
+ if (exitCode !== 0) throw new Error(`Compilation failed for ${sourcePath}`)
62
+ }
63
+
64
+ override async execute(handler: Handler, directory: string, inputPath: string, outputPath: string): Promise<void> {
65
+ const exePath = 'solution.py.exe'
66
+ if (!(await exists(join(directory, exePath)))) {
67
+ throw new Error(`Executable file ${exePath} does not exist in directory ${directory}`)
68
+ }
69
+
70
+ const mergedPath = `solution-${inputPath}.py.exe`
71
+
72
+ tui.command(`Merging ${exePath} and ${inputPath} into ${mergedPath}`, { italic: true })
73
+
74
+ await this.mergeScripts(directory, exePath, inputPath, mergedPath)
75
+
76
+ const fullOutputPath = join(directory, outputPath)
77
+
78
+ await rm(fullOutputPath, { force: true })
79
+
80
+ tui.command(`python3 ${mergedPath} > ${outputPath}`)
81
+
82
+ const { exitCode } = await execa({
83
+ reject: false,
84
+ stdout: { file: fullOutputPath },
85
+ stderr: 'inherit',
86
+ cwd: directory,
87
+ })`python3 ${mergedPath}`
88
+
89
+ if (exitCode !== 0) throw new Error(`Execution failed for ${mergedPath}`)
90
+ }
91
+
92
+ async mergeScripts(
93
+ directory: string,
94
+ scriptPath1: string,
95
+ scriptPath2: string,
96
+ outputScriptPath: string,
97
+ ): Promise<void> {
98
+ const script1 = await readText(join(directory, scriptPath1))
99
+ const script2 = await readText(join(directory, scriptPath2))
100
+ const mergedScript = script1 + '\n\n\n' + script2
101
+ await writeText(join(directory, outputScriptPath), mergedScript)
102
+ }
103
+ }
package/lib/data.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { invert } from 'radash'
2
+
3
+ export const languageNames: Record<string, string> = {
4
+ en: 'English',
5
+ ca: 'Catalan',
6
+ es: 'Spanish',
7
+ fr: 'French',
8
+ de: 'German',
9
+ }
10
+
11
+ export const proglangNames: Record<string, string> = {
12
+ c: 'C',
13
+ cc: 'C++',
14
+ py: 'Python3',
15
+ hs: 'Haskell',
16
+ clj: 'Clojure',
17
+ }
18
+
19
+ export const proglangExtensions: Record<string, string> = invert(proglangNames)
package/lib/doctor.ts ADDED
@@ -0,0 +1,158 @@
1
+ import tui from '@/lib/tui'
2
+ import { execa } from 'execa'
3
+ import { nothing } from './utils'
4
+ import terminalLink from 'terminal-link'
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 probePdfLaTeX(showInfo: boolean = false): Promise<boolean> {
36
+ if (showInfo) tui.command('pdflatex --version')
37
+ const { stdout } = await execa({ reject: false })`pdflatex --version`
38
+ const version = stdout.split('\n')[0]!.trim()
39
+ if (showInfo) console.log(version)
40
+ return stdout.startsWith('pdfTeX')
41
+ }
42
+
43
+ export async function probeXeLaTeX(showInfo: boolean = false): Promise<boolean> {
44
+ if (showInfo) tui.command('xelatex --version')
45
+ const { stdout } = await execa({ reject: false })`xelatex --version`
46
+ const version = stdout.split('\n')[0]!.trim()
47
+ if (showInfo) console.log(version)
48
+ return stdout.startsWith('XeTeX')
49
+ }
50
+
51
+ export async function probePandoc(showInfo: boolean = false): Promise<boolean> {
52
+ if (showInfo) tui.command('pandoc --version')
53
+ const { stdout } = await execa({ reject: false })`pandoc --version`
54
+ const version = stdout.split('\n')[0]!.trim()
55
+ if (showInfo) console.log(version)
56
+ return stdout.startsWith('pandoc') && stdout.includes('+lua')
57
+ }
58
+
59
+ export async function probeImageMagick(showInfo: boolean = false): Promise<boolean> {
60
+ if (showInfo) tui.command('magick --version')
61
+ const { stdout } = await execa({ reject: false })`magick --version`
62
+ const version = stdout.split('\n')[0]!.trim()
63
+ if (showInfo) console.log(version)
64
+ return stdout.startsWith('Version: ImageMagick')
65
+ }
66
+
67
+ export async function checkPython3(): Promise<void> {
68
+ if (await probePython3(true)) {
69
+ tui.success('Python3 seems installed')
70
+ const modules = 'turtle-pil yogi easyinput'.split(' ')
71
+ for (const m of modules) {
72
+ await probePythonModule(true, m)
73
+ }
74
+ } else {
75
+ tui.warning('Python3 does not appear to be installed')
76
+ tui.print('This is not a problem if you do not plan to use Python solutions')
77
+ tui.print('See https://www.python.org/downloads/')
78
+ }
79
+ }
80
+
81
+ export async function checkGCC(): Promise<void> {
82
+ if (await probeGCC(true)) {
83
+ tui.success('C/C++ seems installed')
84
+ } else {
85
+ tui.warning('C/C++ does not appear to be installed')
86
+ tui.print('This is not a problem if you do not plan to use C or C++ solutions')
87
+ tui.print('Please install GCC or Clang')
88
+ }
89
+ }
90
+
91
+ export async function checkPdfLaTeX(): Promise<void> {
92
+ if (await probePdfLaTeX(true)) {
93
+ tui.success('LaTeX seems installed')
94
+ } else {
95
+ tui.warning('LaTeX does not appear to be installed')
96
+ tui.print('You will not be able to generate PDF statements')
97
+ tui.print('TODO: Provide instructions for installing LaTeX')
98
+ }
99
+ }
100
+
101
+ export async function checkXeLaTeX(): Promise<void> {
102
+ if (await probeXeLaTeX(true)) {
103
+ tui.success('XeLaTeX seems installed')
104
+ } else {
105
+ tui.warning('XeLaTeX does not appear to be installed')
106
+ tui.print('You will not be able to generate PDF statements with Unicode support')
107
+ tui.print('TODO: Provide instructions for installing XeLaTeX')
108
+ }
109
+ }
110
+
111
+ export async function checkPandoc(): Promise<void> {
112
+ if (await probePandoc(true)) {
113
+ tui.success('Pandoc with Lua support seems installed')
114
+ } else {
115
+ tui.warning('Pandoc with Lua support does not appear to be installed')
116
+ tui.print('You will not be able to generate text statements (HTML, TXT, MD)')
117
+ tui.print('See https://pandoc.org/installing.html')
118
+ }
119
+ }
120
+
121
+ export async function checkImageMagick(): Promise<void> {
122
+ if (await probeImageMagick(true)) {
123
+ tui.success('ImageMagick seems installed')
124
+ } else {
125
+ tui.warning('ImageMagick does not appear to be installed')
126
+ tui.print('You will not be able to convert images for text statements')
127
+ tui.print('See https://imagemagick.org/script/download.php')
128
+ }
129
+ }
130
+
131
+ export async function checkEnvVars(): Promise<void> {
132
+ await nothing()
133
+ const vars = ['OPENAI_API_KEY', 'GEMINI_API_KEY']
134
+ for (const v of vars) {
135
+ if (process.env[v]) {
136
+ tui.success(`${v} is set`)
137
+ } else {
138
+ tui.warning(`${v} is not set`)
139
+ tui.print(`You will not be able to use related AI models`)
140
+ }
141
+ }
142
+ }
143
+
144
+ export async function checkTerminal(): Promise<void> {
145
+ await nothing()
146
+ if (process.stdout.isTTY) {
147
+ tui.success('Terminal supports TTY')
148
+ } else {
149
+ tui.warning('Terminal does not support TTY')
150
+ tui.print('Some output features may not work as expected')
151
+ }
152
+ if (terminalLink.isSupported) {
153
+ tui.success('Terminal supports hyperlinks')
154
+ } else {
155
+ tui.warning('Terminal does not support hyperlinks')
156
+ tui.print('Links to files may not be clickable')
157
+ }
158
+ }