@jpetit/toolkit 3.0.0

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 (192) hide show
  1. package/.prettierignore +11 -0
  2. package/.prettierrc.json +9 -0
  3. package/.vscode/settings.json +26 -0
  4. package/README.md +22 -0
  5. package/assets/lua/fixCodeBlocks.lua +19 -0
  6. package/assets/lua/removeEnvs.lua +20 -0
  7. package/assets/lua/removeHtmlOnly.lua +11 -0
  8. package/assets/problems/graphics/japanese-flag.pbm/README.md +14 -0
  9. package/assets/problems/graphics/japanese-flag.pbm/award.png +0 -0
  10. package/assets/problems/graphics/japanese-flag.pbm/handler.yml +2 -0
  11. package/assets/problems/graphics/japanese-flag.pbm/problem.ca.tex +21 -0
  12. package/assets/problems/graphics/japanese-flag.pbm/problem.ca.yml +3 -0
  13. package/assets/problems/graphics/japanese-flag.pbm/sample-1.inp +1 -0
  14. package/assets/problems/graphics/japanese-flag.pbm/sample-2.inp +1 -0
  15. package/assets/problems/graphics/japanese-flag.pbm/solution.cc +25 -0
  16. package/assets/problems/graphics/japanese-flag.pbm/solution.py +11 -0
  17. package/assets/problems/graphics/tortuga.pbm/README.md +13 -0
  18. package/assets/problems/graphics/tortuga.pbm/award.png +0 -0
  19. package/assets/problems/graphics/tortuga.pbm/handler.yml +2 -0
  20. package/assets/problems/graphics/tortuga.pbm/problem.ca.tex +23 -0
  21. package/assets/problems/graphics/tortuga.pbm/problem.ca.yml +3 -0
  22. package/assets/problems/graphics/tortuga.pbm/sample.inp +0 -0
  23. package/assets/problems/graphics/tortuga.pbm/solution.py +11 -0
  24. package/assets/problems/standard/campanar-de-la-torrassa.pbm/README.md +15 -0
  25. package/assets/problems/standard/campanar-de-la-torrassa.pbm/award.html +1 -0
  26. package/assets/problems/standard/campanar-de-la-torrassa.pbm/award.png +0 -0
  27. package/assets/problems/standard/campanar-de-la-torrassa.pbm/campanar.eps +1113 -0
  28. package/assets/problems/standard/campanar-de-la-torrassa.pbm/campanar.png +0 -0
  29. package/assets/problems/standard/campanar-de-la-torrassa.pbm/generate.cc +10 -0
  30. package/assets/problems/standard/campanar-de-la-torrassa.pbm/handler.yml +2 -0
  31. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.ca.tex +59 -0
  32. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.ca.yml +3 -0
  33. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.en.tex +52 -0
  34. package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.en.yml +4 -0
  35. package/assets/problems/standard/campanar-de-la-torrassa.pbm/sample.inp +7 -0
  36. package/assets/problems/standard/campanar-de-la-torrassa.pbm/slow.cc +29 -0
  37. package/assets/problems/standard/campanar-de-la-torrassa.pbm/solution.cc +48 -0
  38. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-1.inp +12 -0
  39. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-2.inp +100000 -0
  40. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-2.ops +1 -0
  41. package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-b.inp +0 -0
  42. package/assets/problems/standard/maximum-of-2-integers.pbm/README.md +11 -0
  43. package/assets/problems/standard/maximum-of-2-integers.pbm/handler.yml +1 -0
  44. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.ca.tex +17 -0
  45. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.ca.yml +3 -0
  46. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.en.tex +16 -0
  47. package/assets/problems/standard/maximum-of-2-integers.pbm/problem.en.yml +4 -0
  48. package/assets/problems/standard/maximum-of-2-integers.pbm/sample-1.inp +1 -0
  49. package/assets/problems/standard/maximum-of-2-integers.pbm/sample-2.inp +1 -0
  50. package/assets/problems/standard/maximum-of-2-integers.pbm/sample-3.inp +1 -0
  51. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.c +18 -0
  52. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.cc +13 -0
  53. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.java +16 -0
  54. package/assets/problems/standard/maximum-of-2-integers.pbm/solution.py +5 -0
  55. package/assets/problems/standard/maximum-of-2-integers.pbm/test-1.inp +1 -0
  56. package/assets/problems/standard/maximum-of-2-integers.pbm/test-2.inp +1 -0
  57. package/assets/problems/standard/maximum-of-2-integers.pbm/test-3.inp +1 -0
  58. package/assets/problems/standard/maximum-of-2-integers.pbm/test-4.inp +1 -0
  59. package/assets/problems/standard/maximum-of-2-integers.pbm/test-5.inp +1 -0
  60. package/assets/problems/standard/treasures-in-a-map.pbm/README.md +12 -0
  61. package/assets/problems/standard/treasures-in-a-map.pbm/atzar.cc +85 -0
  62. package/assets/problems/standard/treasures-in-a-map.pbm/award.png +0 -0
  63. package/assets/problems/standard/treasures-in-a-map.pbm/generate-1.cc +26 -0
  64. package/assets/problems/standard/treasures-in-a-map.pbm/generate-2.cc +26 -0
  65. package/assets/problems/standard/treasures-in-a-map.pbm/generate-3.cc +26 -0
  66. package/assets/problems/standard/treasures-in-a-map.pbm/generate-4.cc +26 -0
  67. package/assets/problems/standard/treasures-in-a-map.pbm/handler.yml +1 -0
  68. package/assets/problems/standard/treasures-in-a-map.pbm/problem.ca.tex +40 -0
  69. package/assets/problems/standard/treasures-in-a-map.pbm/problem.ca.yml +3 -0
  70. package/assets/problems/standard/treasures-in-a-map.pbm/problem.en.tex +40 -0
  71. package/assets/problems/standard/treasures-in-a-map.pbm/problem.en.yml +4 -0
  72. package/assets/problems/standard/treasures-in-a-map.pbm/random-1.inp +24 -0
  73. package/assets/problems/standard/treasures-in-a-map.pbm/random-2.inp +27 -0
  74. package/assets/problems/standard/treasures-in-a-map.pbm/random-3.inp +38 -0
  75. package/assets/problems/standard/treasures-in-a-map.pbm/random-4.inp +50 -0
  76. package/assets/problems/standard/treasures-in-a-map.pbm/sample-1.inp +9 -0
  77. package/assets/problems/standard/treasures-in-a-map.pbm/sample-2.inp +6 -0
  78. package/assets/problems/standard/treasures-in-a-map.pbm/sample-3.inp +7 -0
  79. package/assets/problems/standard/treasures-in-a-map.pbm/solution.cc +38 -0
  80. package/assets/problems/standard/treasures-in-a-map.pbm/test-1.inp +5 -0
  81. package/assets/problems/standard/treasures-in-a-map.pbm/test-2.inp +6 -0
  82. package/assets/problems/standard/treasures-in-a-map.pbm/test-3.inp +6 -0
  83. package/assets/problems/standard/treasures-in-a-map.pbm/test-4.inp +9 -0
  84. package/assets/problems/standard/treasures-in-a-map.pbm/test-5.inp +10 -0
  85. package/assets/problems/standard/treasures-in-a-map.pbm/test-6.inp +9 -0
  86. package/assets/problems/standard/treasures-in-a-map.pbm/test-7.inp +12 -0
  87. package/assets/problems/standard/treasures-in-a-map.pbm/test-8.inp +3 -0
  88. package/assets/problems/standard/treasures-in-a-map.pbm/test-9.inp +37 -0
  89. package/assets/problems/standard/treasures-in-a-map.pbm/test-91.inp +52 -0
  90. package/assets/problems/standard/treasures-in-a-map.pbm/test-92.inp +25 -0
  91. package/assets/problems/standard/treasures-in-a-map.pbm/test-93.inp +3 -0
  92. package/assets/sty/judgeit.ca.sty +54 -0
  93. package/assets/sty/judgeit.de.sty +61 -0
  94. package/assets/sty/judgeit.en.sty +60 -0
  95. package/assets/sty/judgeit.es.sty +54 -0
  96. package/assets/sty/judgeit.fr.sty +59 -0
  97. package/assets/sty/judgeit.sty +307 -0
  98. package/assets/sty/picins.sty +579 -0
  99. package/assets.zip +0 -0
  100. package/eslint.config.mjs +31 -0
  101. package/lib/ai.ts +138 -0
  102. package/lib/assets.ts +31 -0
  103. package/lib/cleaner.ts +58 -0
  104. package/lib/compilers/_frompython.ts +388 -0
  105. package/lib/compilers/base.ts +97 -0
  106. package/lib/compilers/gcc.ts +47 -0
  107. package/lib/compilers/gxx.ts +47 -0
  108. package/lib/compilers/index.ts +61 -0
  109. package/lib/compilers/python3.ts +67 -0
  110. package/lib/data.ts +19 -0
  111. package/lib/doctor.ts +104 -0
  112. package/lib/generate.ts +333 -0
  113. package/lib/maker.ts +535 -0
  114. package/lib/settings.ts +42 -0
  115. package/lib/tui.ts +69 -0
  116. package/lib/utils.ts +83 -0
  117. package/package.json +56 -0
  118. package/problems/graphic.pbm/README.md +14 -0
  119. package/problems/graphic.pbm/award.png +0 -0
  120. package/problems/graphic.pbm/handler.yml +2 -0
  121. package/problems/graphic.pbm/problem.ca.html +13 -0
  122. package/problems/graphic.pbm/problem.ca.md +20 -0
  123. package/problems/graphic.pbm/problem.ca.tex +21 -0
  124. package/problems/graphic.pbm/problem.ca.txt +20 -0
  125. package/problems/graphic.pbm/problem.ca.yml +3 -0
  126. package/problems/graphic.pbm/sample-1.inp +1 -0
  127. package/problems/graphic.pbm/sample-2.inp +1 -0
  128. package/problems/graphic.pbm/solution.py +11 -0
  129. package/problems/maxim2.pbm/Main.java +13 -0
  130. package/problems/maxim2.pbm/distillation.yml +7 -0
  131. package/problems/maxim2.pbm/distilled-01.inp +1 -0
  132. package/problems/maxim2.pbm/distilled-02.inp +1 -0
  133. package/problems/maxim2.pbm/distilled-03.inp +1 -0
  134. package/problems/maxim2.pbm/distiller.yml +2 -0
  135. package/problems/maxim2.pbm/generate-inputs.py +9 -0
  136. package/problems/maxim2.pbm/handler.yml +2 -0
  137. package/problems/maxim2.pbm/ma-1.inp +1 -0
  138. package/problems/maxim2.pbm/ma-2.inp +1 -0
  139. package/problems/maxim2.pbm/ma-3.inp +1 -0
  140. package/problems/maxim2.pbm/ma-4.inp +1 -0
  141. package/problems/maxim2.pbm/ma-5.inp +1 -0
  142. package/problems/maxim2.pbm/per-doubles.inp +1 -0
  143. package/problems/maxim2.pbm/problem.ca.html +11 -0
  144. package/problems/maxim2.pbm/problem.ca.md +19 -0
  145. package/problems/maxim2.pbm/problem.ca.tex +17 -0
  146. package/problems/maxim2.pbm/problem.ca.txt +19 -0
  147. package/problems/maxim2.pbm/problem.ca.yml +3 -0
  148. package/problems/maxim2.pbm/problem.en.html +11 -0
  149. package/problems/maxim2.pbm/problem.en.md +19 -0
  150. package/problems/maxim2.pbm/problem.en.tex +16 -0
  151. package/problems/maxim2.pbm/problem.en.txt +19 -0
  152. package/problems/maxim2.pbm/problem.en.yml +4 -0
  153. package/problems/maxim2.pbm/sample-1.inp +1 -0
  154. package/problems/maxim2.pbm/sample-2.inp +1 -0
  155. package/problems/maxim2.pbm/sample-3.inp +1 -0
  156. package/problems/maxim2.pbm/solution.c +12 -0
  157. package/problems/maxim2.pbm/solution.cc +13 -0
  158. package/problems/maxim2.pbm/solution.java +13 -0
  159. package/problems/maxim2.pbm/solution.pas +9 -0
  160. package/problems/maxim2.pbm/solution.py +5 -0
  161. package/problems/maxim2.pbm/tags.yml +2 -0
  162. package/problems/maxim2.pbm/test_-1_-1.inp +1 -0
  163. package/problems/maxim2.pbm/test_-1_-2.inp +1 -0
  164. package/problems/maxim2.pbm/test_-1_0.inp +1 -0
  165. package/problems/maxim2.pbm/test_-1_1.inp +1 -0
  166. package/problems/maxim2.pbm/test_-2_-1.inp +1 -0
  167. package/problems/maxim2.pbm/test_-2_-2.inp +1 -0
  168. package/problems/maxim2.pbm/test_-2_0.inp +1 -0
  169. package/problems/maxim2.pbm/test_-2_1.inp +1 -0
  170. package/problems/maxim2.pbm/test_0_-1.inp +1 -0
  171. package/problems/maxim2.pbm/test_0_-2.inp +1 -0
  172. package/problems/maxim2.pbm/test_0_0.inp +1 -0
  173. package/problems/maxim2.pbm/test_0_1.inp +1 -0
  174. package/problems/maxim2.pbm/test_1_-1.inp +1 -0
  175. package/problems/maxim2.pbm/test_1_-2.inp +1 -0
  176. package/problems/maxim2.pbm/test_1_0.inp +1 -0
  177. package/problems/maxim2.pbm/test_1_1.inp +1 -0
  178. package/test.ts +3 -0
  179. package/toolkit/ai.ts +30 -0
  180. package/toolkit/clean.ts +19 -0
  181. package/toolkit/compilers.ts +29 -0
  182. package/toolkit/create-jutge-ai.ts +101 -0
  183. package/toolkit/create-template.ts +51 -0
  184. package/toolkit/create-wizard.ts +4 -0
  185. package/toolkit/create.ts +75 -0
  186. package/toolkit/doctor.ts +17 -0
  187. package/toolkit/index.ts +28 -0
  188. package/toolkit/init.ts +66 -0
  189. package/toolkit/make.ts +60 -0
  190. package/toolkit/verify.ts +19 -0
  191. package/tsconfig.json +38 -0
  192. package/types/zip.d.ts +4 -0
package/lib/ai.ts ADDED
@@ -0,0 +1,138 @@
1
+ import { encode } from 'gpt-tokenizer'
2
+ import { estimateCost } from 'gpt-tokenizer/model/gpt-5'
3
+ import { igniteModel, LlmModel, loadModels, logger, Message } from 'multi-llm-ts'
4
+
5
+ // do not log anything from multi-llm-ts
6
+ logger.disable()
7
+
8
+ export async function complete(model: string, systemPrompt: string, userPrompt: string): Promise<string> {
9
+ const parts = model.split('/')
10
+ const providerName = parts[0]!
11
+ const modelName = parts[1]!
12
+
13
+ const config = { apiKey: process.env[keys[providerName]!] || '' }
14
+
15
+ const models = await loadModels(providerName, config)
16
+ // console.log(models)
17
+ const chat = models!.chat.find((m) => m.id === modelName)!
18
+
19
+ const bot = igniteModel(providerName, chat, config)
20
+ const messages = [new Message('system', systemPrompt), new Message('user', userPrompt)]
21
+ const response = await bot.complete(messages)
22
+ return response.content!
23
+ }
24
+
25
+ type ModelInfo = Record<string, Record<string, string[]>>
26
+
27
+ export async function listModels(): Promise<ModelInfo> {
28
+ const result: ModelInfo = {}
29
+ const providers = Object.keys(keys).sort()
30
+ for (const providerName of providers) {
31
+ if (providerName === 'ollama') continue // Ollama models are local, skip for now
32
+ const config = { apiKey: process.env[keys[providerName]!] || '' }
33
+ const models = await loadModels(providerName, config)
34
+ if (models === null) continue
35
+ result[providerName] = {}
36
+ for (const modelType in models) {
37
+ result[providerName][modelType] = []
38
+ for (const model of models.chat) {
39
+ result[providerName][modelType].push(model.id)
40
+ }
41
+ }
42
+ }
43
+ return result
44
+ }
45
+
46
+ export class ChatBot {
47
+ private model: string
48
+ private bot: LlmModel | null = null
49
+ private messages: Message[]
50
+ public totalInputTokens: number = 0
51
+ public totalOutputTokens: number = 0
52
+ public totalInputCost: number = 0
53
+ public totalOutputCost: number = 0
54
+
55
+ constructor(model: string, systemPrompt: string) {
56
+ this.model = model
57
+ this.messages = [new Message('system', systemPrompt)]
58
+ }
59
+
60
+ async init() {
61
+ const parts = this.model.split('/')
62
+ const providerName = parts[0]!
63
+ const modelName = parts[1]!
64
+ const config = { apiKey: process.env[keys[providerName]!] || '' }
65
+ const models = await loadModels(providerName, config)
66
+ const chat = models!.chat.find((m: any) => m.id === modelName)!
67
+ this.bot = igniteModel(providerName, chat, config)
68
+ }
69
+
70
+ async complete(userPrompt: string): Promise<string> {
71
+ if (!this.bot) {
72
+ throw new Error(`Model '${this.model}' could not be initialized`)
73
+ }
74
+
75
+ this.messages.push(new Message('user', userPrompt))
76
+ const response = await this.bot.complete(this.messages)
77
+ this.messages.push(new Message('assistant', response.content))
78
+
79
+ const inputTokens = encode(userPrompt).length
80
+ const outputTokens = encode(response.content!).length
81
+ const inputCost = estimateCost(inputTokens)
82
+ const outputCost = estimateCost(outputTokens)
83
+
84
+ this.totalInputTokens += inputTokens
85
+ this.totalOutputTokens += outputTokens
86
+ this.totalInputCost += inputCost.main!.input!
87
+ this.totalOutputCost += outputCost.main!.output!
88
+
89
+ return response.content!
90
+ }
91
+
92
+ forgetLastInteraction() {
93
+ if (this.messages.length > 2) {
94
+ this.messages.pop()
95
+ this.messages.pop()
96
+ }
97
+ }
98
+
99
+ modelInformation(): any {
100
+ if (!this.bot) {
101
+ throw new Error(`Model '${this.model}' has not been initialized`)
102
+ }
103
+ return this.bot.model
104
+ }
105
+ }
106
+
107
+ export type PowerEstimation = {
108
+ wattHours: number
109
+ joules: number
110
+ co2Grams: number
111
+ trees: number
112
+ }
113
+
114
+ export function estimatePowerConsumption(inputTokens: number, outputTokens: number): PowerEstimation {
115
+ // Written by Claude.ai
116
+
117
+ // Very rough estimates based on public data
118
+ // One tree absorbs approximately:
119
+ // - 22 kg (22,000g) of CO2 per year on average
120
+ // - 1 ton (1,000,000g) over its lifetime (~40 years)
121
+
122
+ const wattsPerToken = 0.0003 // ~0.3 milliwatts per token
123
+ const totalTokens = inputTokens + outputTokens
124
+ const co2PerTree = 22000
125
+
126
+ return {
127
+ wattHours: (totalTokens * wattsPerToken) / 3600, // Convert to Wh
128
+ joules: totalTokens * wattsPerToken,
129
+ co2Grams: ((totalTokens * wattsPerToken) / 3600) * 0.5, // Rough CO2 estimate
130
+ trees: (((totalTokens * wattsPerToken) / 3600) * 0.5) / co2PerTree,
131
+ }
132
+ }
133
+
134
+ const keys: Record<string, string> = {
135
+ google: 'GEMINI_API_KEY',
136
+ openai: 'OPENAI_API_KEY',
137
+ ollama: '',
138
+ }
package/lib/assets.ts ADDED
@@ -0,0 +1,31 @@
1
+ import zipFile from '@/assets.zip' with { type: 'file' }
2
+ import chalk from 'chalk'
3
+ import { unzipSync } from 'fflate'
4
+ import { mkdir, rm } from 'fs/promises'
5
+ import { join } from 'path'
6
+ import { paths } from './settings'
7
+ import * as tui from './tui.js'
8
+
9
+ export async function decompressAssets() {
10
+ await tui.section('Decompressing assets', async () => {
11
+ const destination = paths.data
12
+ tui.action(`Decompressing assets to ${destination}`)
13
+ const bytes = await Bun.file(zipFile).arrayBuffer()
14
+ const zipBuffer = new Uint8Array(bytes)
15
+
16
+ await rm(destination, { recursive: true, force: true })
17
+ await mkdir(destination, { recursive: true })
18
+
19
+ const unzipped = unzipSync(zipBuffer)
20
+ for (const [filePath, content] of Object.entries(unzipped)) {
21
+ // console.log(chalk.magenta(filePath))
22
+ const fullPath = join(destination, filePath)
23
+ if (filePath.endsWith('/')) {
24
+ await mkdir(fullPath, { recursive: true })
25
+ } else {
26
+ await Bun.write(fullPath, content)
27
+ }
28
+ }
29
+ tui.success('Assets decompressed')
30
+ })
31
+ }
package/lib/cleaner.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { confirm } from '@inquirer/prompts'
2
+ import * as tui from '@/lib/tui.js'
3
+ import { readdir, unlink } from 'fs/promises'
4
+ import { join } from 'path'
5
+
6
+ export async function cleanFiles(force: boolean, directory: string): Promise<void> {
7
+ const patterns = [
8
+ '\\.exe$',
9
+ '\\.cor$',
10
+ '\\.out$',
11
+ '\\.class$',
12
+ '\\.o$',
13
+ '\\.ho$',
14
+ '~$',
15
+ '^problem\\.[a-z][a-z]\\.pdf$',
16
+ '^problem\\.[a-z][a-z]\\.ps$',
17
+ '^a\\.out$',
18
+ ]
19
+ const pattern = new RegExp(patterns.join('|'))
20
+
21
+ const entries = await readdir(directory, { withFileTypes: true })
22
+
23
+ const removalList: string[] = []
24
+ for (const entry of entries) {
25
+ const fullPath = join(directory, entry.name)
26
+ if (entry.isFile() && pattern.test(entry.name)) {
27
+ removalList.push(fullPath)
28
+ }
29
+ }
30
+
31
+ if (removalList.length === 0) {
32
+ tui.success('No files to remove')
33
+ return
34
+ }
35
+
36
+ tui.warning(`The following ${removalList.length} files will be removed:`)
37
+ for (const elem of removalList.sort()) {
38
+ tui.print(elem)
39
+ }
40
+ console.log()
41
+
42
+ if (!force) {
43
+ const conformation = await confirm({ message: `Remove ${removalList.length} files?`, default: false })
44
+ if (!conformation) return
45
+ }
46
+
47
+ let removalCount = 0
48
+ for (const elem of removalList) {
49
+ try {
50
+ await unlink(elem)
51
+ removalCount++
52
+ } catch (error) {
53
+ tui.error(`Could not remove file ${elem}`)
54
+ }
55
+ }
56
+
57
+ tui.success(`Removed ${removalCount} files`)
58
+ }
@@ -0,0 +1,388 @@
1
+ import { $ } from 'bun'
2
+ import chalk from 'chalk'
3
+ import { access, copyFile, mkdir, readFile, rename, rm, unlink, writeFile } from 'fs/promises'
4
+
5
+ // Constants
6
+ const MAX_COMPILATION_TIME = 30000 // 30 seconds in milliseconds
7
+
8
+ // Custom Errors
9
+ export class CompilationTooLongError extends Error {
10
+ constructor() {
11
+ super('Compilation time exceeded')
12
+ this.name = 'CompilationTooLongError'
13
+ }
14
+ }
15
+
16
+ export class ExecutionError extends Error {
17
+ constructor(message: string) {
18
+ super(message)
19
+ this.name = 'ExecutionError'
20
+ }
21
+ }
22
+
23
+ export class CompilationError extends Error {
24
+ constructor(message: string) {
25
+ super(message)
26
+ this.name = 'CompilationError'
27
+ }
28
+ }
29
+
30
+ interface CompilerInfo {
31
+ compiler_id: string
32
+ name: string
33
+ language: string
34
+ version: string
35
+ flags1: string
36
+ flags2: string
37
+ extension: string
38
+ type: string
39
+ warning: string
40
+ }
41
+
42
+ interface Handler {
43
+ source_modifier?: string
44
+ [key: string]: any
45
+ }
46
+
47
+ const compilersList: Record<string, typeof Compiler> = {}
48
+
49
+ function compiler(baseClass: typeof Compiler) {
50
+ return function <T extends typeof Compiler>(derivedClass: T) {
51
+ compilersList[derivedClass.name] = derivedClass
52
+ return derivedClass
53
+ }
54
+ }
55
+
56
+ export abstract class Compiler {
57
+ protected handler: Handler
58
+ protected _name: string
59
+ static availableCompilers: string[] = []
60
+
61
+ constructor(handler: Handler | null, name: string | null) {
62
+ this.handler = handler || {}
63
+ this._name = name || ''
64
+ }
65
+
66
+ abstract name(): string
67
+ abstract type(): string
68
+ abstract executable(): string
69
+ abstract language(): string
70
+ abstract version(): Promise<string>
71
+ abstract flags1(): string
72
+ abstract flags2(): string
73
+ abstract extension(): string
74
+ abstract compile(): Promise<boolean>
75
+ abstract execute(tst: string, correct: boolean, iterations?: number): Promise<number>
76
+
77
+ id(): string {
78
+ return this.constructor.name.replace('Compiler_', '').replace('XX', '++')
79
+ }
80
+
81
+ warning(): string {
82
+ return ''
83
+ }
84
+
85
+ async prepareExecution(ori: string): Promise<void> {
86
+ // Override in subclasses if needed
87
+ }
88
+
89
+ protected async executeCompiler(cmd: string): Promise<void> {
90
+ console.log(chalk.dim(cmd))
91
+
92
+ return new Promise((resolve, reject) => {
93
+ const startTime = Date.now()
94
+ const proc = Bun.spawn(cmd.split(' '), {
95
+ stdout: 'pipe',
96
+ stderr: 'pipe',
97
+ })
98
+
99
+ let output = ''
100
+ let hasError = false
101
+
102
+ const timeout = setTimeout(() => {
103
+ proc.kill()
104
+ console.log(chalk.bold.red('Compilation time exceeded!'))
105
+ reject(new CompilationTooLongError())
106
+ }, MAX_COMPILATION_TIME)
107
+
108
+ ;(async () => {
109
+ try {
110
+ const [stdout, stderr] = await Promise.all([
111
+ new Response(proc.stdout).text(),
112
+ new Response(proc.stderr).text(),
113
+ ])
114
+
115
+ output = stdout + stderr
116
+ if (stderr.length > 0) {
117
+ hasError = true
118
+ }
119
+
120
+ const exitCode = await proc.exited
121
+ clearTimeout(timeout)
122
+
123
+ if (exitCode !== 0 || hasError || output.length > 0) {
124
+ if (output.length > 0) {
125
+ console.log('\n' + output.trim() + '\n')
126
+ }
127
+ console.log(
128
+ chalk.bold.red('Compilation error! ') +
129
+ chalk.normal(`Please check ${this._name}.${this.extension()} and try again.`),
130
+ )
131
+ reject(new CompilationError('Compilation failed'))
132
+ } else {
133
+ resolve()
134
+ }
135
+ } catch (error) {
136
+ clearTimeout(timeout)
137
+ reject(error)
138
+ }
139
+ })()
140
+ })
141
+ }
142
+
143
+ protected async getVersion(cmd: string, lineIndex: number): Promise<string> {
144
+ try {
145
+ const proc = Bun.spawn(cmd.split(' '), {
146
+ stdout: 'pipe',
147
+ stderr: 'pipe',
148
+ })
149
+
150
+ const stdout = await new Response(proc.stdout).text()
151
+ const lines = stdout.split('\n')
152
+ return lines[lineIndex]?.trim() || 'Unknown version'
153
+ } catch {
154
+ return 'not found'
155
+ }
156
+ }
157
+
158
+ info(): CompilerInfo {
159
+ return {
160
+ compiler_id: this.id(),
161
+ name: this._name,
162
+ language: this.language(),
163
+ version: '', // Will be populated asynchronously
164
+ flags1: this.flags1(),
165
+ flags2: this.flags2(),
166
+ extension: this.extension(),
167
+ type: this.type(),
168
+ warning: this.warning(),
169
+ }
170
+ }
171
+
172
+ protected async timeExecution(cmd: string, iterations: number = 1): Promise<number> {
173
+ const start = Date.now()
174
+ for (let i = 0; i < iterations; i++) {
175
+ await $`sh -c ${cmd}`.quiet()
176
+ }
177
+ const end = Date.now()
178
+ return (end - start) / iterations / 1000 // Return in seconds
179
+ }
180
+ }
181
+
182
+ // Python3 Compiler
183
+ export class Compiler_Python3 extends Compiler {
184
+ name(): string {
185
+ return 'Python3'
186
+ }
187
+
188
+ type(): string {
189
+ return 'interpreter'
190
+ }
191
+
192
+ executable(): string {
193
+ return `${this._name}.py`
194
+ }
195
+
196
+ language(): string {
197
+ return 'Python'
198
+ }
199
+
200
+ async version(): Promise<string> {
201
+ return await this.getVersion('python3 -V', 0)
202
+ }
203
+
204
+ flags1(): string {
205
+ return ''
206
+ }
207
+
208
+ flags2(): string {
209
+ return ''
210
+ }
211
+
212
+ extension(): string {
213
+ return 'py'
214
+ }
215
+
216
+ private async genWrapper(): Promise<void> {
217
+ await Util.writeFile(
218
+ 'py3c.py',
219
+ `#!/usr/bin/python3
220
+
221
+ import py_compile, sys
222
+
223
+ py_compile.compile(sys.argv[1])`,
224
+ )
225
+ }
226
+
227
+ private async delWrapper(): Promise<void> {
228
+ await Util.delFile('py3c.py')
229
+ }
230
+
231
+ async execute(tst: string, correct: boolean, iterations: number = 1): Promise<number> {
232
+ let exec = this.executable()
233
+
234
+ if (this.handler.source_modifier === 'no_main' || this.handler.source_modifier === 'structs') {
235
+ await Util.copyFile(`${this._name}.py`, 'modified.py')
236
+ const ori = await Util.readFile(`${this._name}.py`)
237
+ const main = await Util.readFile('main.py')
238
+ await Util.writeFile('modified.py', `${ori}\n${main}\n`)
239
+ exec = 'modified.py'
240
+ }
241
+
242
+ const ext = correct ? 'cor' : 'py.out'
243
+ const cmd = `python3 ${exec} < ${tst}.inp > ${tst}.${ext}`
244
+
245
+ if (correct) {
246
+ process.stdout.write(cmd)
247
+ }
248
+
249
+ const time = await this.timeExecution(cmd, iterations)
250
+
251
+ await Util.delFile('modified.py')
252
+ await Util.delDir('__pycache__')
253
+
254
+ return time
255
+ }
256
+
257
+ async compile(): Promise<boolean> {
258
+ if (this.handler.source_modifier === 'no_main' || this.handler.source_modifier === 'structs') {
259
+ return await this.compileNoMain()
260
+ } else {
261
+ return await this.compileNormal()
262
+ }
263
+ }
264
+
265
+ private async compileNormal(): Promise<boolean> {
266
+ try {
267
+ await this.genWrapper()
268
+ const code = await Util.readFile(`${this._name}.py`)
269
+ await Util.writeFile(`${this._name}.py`, code)
270
+ await this.executeCompiler(`python3 py3c.py ${this._name}.py 1> /dev/null`)
271
+ } catch (error) {
272
+ if (error instanceof CompilationTooLongError) {
273
+ console.log(chalk.bold.red('Compilation time exceeded!'))
274
+ }
275
+ return false
276
+ } finally {
277
+ await this.delWrapper()
278
+ }
279
+ return true
280
+ }
281
+
282
+ private async compileNoMain(): Promise<boolean> {
283
+ if (!(await this.compileNormal())) {
284
+ return false
285
+ }
286
+
287
+ await Util.copyFile(`${this._name}.py`, 'modified.py')
288
+ const ori = await Util.readFile(`${this._name}.py`)
289
+ const main = await Util.readFile('main.py')
290
+ await Util.writeFile('modified.py', `${ori}\n${main}\n`)
291
+
292
+ try {
293
+ await this.genWrapper()
294
+ await this.executeCompiler('python3 py3c.py modified.py 1> /dev/null')
295
+ } catch (error) {
296
+ if (error instanceof CompilationTooLongError) {
297
+ console.log(chalk.bold.red('Compilation time exceeded!'))
298
+ }
299
+ return false
300
+ } finally {
301
+ await this.delWrapper()
302
+ }
303
+ return true
304
+ }
305
+ }
306
+
307
+ // Compiler registry
308
+ const COMPILERS = ['GCC', 'GXX', 'Python3']
309
+
310
+ export function getCompiler(cpl: string, handler: Handler | null = null, name: string | null = null): Compiler {
311
+ const compilerClass = cpl.replace('++', 'XX')
312
+
313
+ switch (compilerClass) {
314
+ case 'GCC':
315
+ return new Compiler_GCC(handler, name)
316
+ case 'GXX':
317
+ return new Compiler_GXX(handler, name)
318
+ case 'Python3':
319
+ return new Compiler_Python3(handler, name)
320
+ default:
321
+ throw new Error(`Unknown compiler: ${cpl}`)
322
+ }
323
+ }
324
+
325
+ export async function getCompilerExtensions(handlerCompiler: string): Promise<Record<string, string>> {
326
+ const result: Record<string, string> = {}
327
+
328
+ for (const compilerName of COMPILERS) {
329
+ const compiler = getCompiler(compilerName)
330
+ const ext = compiler.extension()
331
+
332
+ if (compilerName === handlerCompiler) {
333
+ result[ext] = compilerName
334
+ } else if (!compilerName.includes('Run') && !(ext in result)) {
335
+ result[ext] = compilerName
336
+ }
337
+ }
338
+
339
+ return result
340
+ }
341
+
342
+ export async function getAvailableCompilers(): Promise<string[]> {
343
+ const available: string[] = []
344
+
345
+ for (const compilerName of COMPILERS) {
346
+ const compiler = getCompiler(compilerName)
347
+ const version = await compiler.version()
348
+
349
+ if (!version.includes('not found')) {
350
+ available.push(compilerName)
351
+ }
352
+ }
353
+
354
+ return available
355
+ }
356
+
357
+ export async function printAvailableCompilers(): Promise<void> {
358
+ const available = await getAvailableCompilers()
359
+
360
+ if (available.length > 0) {
361
+ process.stdout.write('Available compilers: ')
362
+
363
+ for (let i = 0; i < available.length; i++) {
364
+ process.stdout.write(available[i])
365
+
366
+ if (i === available.length - 2) {
367
+ process.stdout.write(' and ')
368
+ } else if (i !== available.length - 1) {
369
+ process.stdout.write(', ')
370
+ }
371
+ }
372
+
373
+ console.log()
374
+ }
375
+ }
376
+
377
+ export async function getCompilerInfo(): Promise<Record<string, CompilerInfo>> {
378
+ const result: Record<string, CompilerInfo> = {}
379
+
380
+ for (const compilerName of COMPILERS) {
381
+ const compiler = getCompiler(compilerName)
382
+ const info = compiler.info()
383
+ info.version = await compiler.version()
384
+ result[compilerName] = info
385
+ }
386
+
387
+ return result
388
+ }
@@ -0,0 +1,97 @@
1
+ import { rm, exists } from 'fs/promises'
2
+ import { join, sep } from 'path'
3
+ import * as tui from '@/lib/tui'
4
+
5
+ export type CompilerInfo = {
6
+ compiler_id: string
7
+ name: string
8
+ language: string
9
+ version: string
10
+ flags1: string
11
+ flags2: string
12
+ extension: string
13
+ type: string
14
+ warning: string
15
+ }
16
+
17
+ export abstract class Compiler {
18
+ abstract id(): string
19
+
20
+ abstract name(): string
21
+
22
+ abstract type(): string
23
+
24
+ abstract language(): string
25
+
26
+ abstract version(): Promise<string>
27
+
28
+ abstract flags1(): string
29
+
30
+ abstract flags2(): string
31
+
32
+ abstract extension(): string
33
+
34
+ warning(): string {
35
+ return ''
36
+ }
37
+
38
+ async available(): Promise<boolean> {
39
+ const version = await this.version()
40
+ return version !== 'not found'
41
+ }
42
+
43
+ async info(): Promise<CompilerInfo> {
44
+ return {
45
+ compiler_id: this.id(),
46
+ name: this.name(),
47
+ language: this.language(),
48
+ version: await this.version(),
49
+ flags1: this.flags1(),
50
+ flags2: this.flags2(),
51
+ extension: this.extension(),
52
+ type: this.type(),
53
+ warning: this.warning(),
54
+ }
55
+ }
56
+
57
+ abstract compile(directory: string, sourcePath: string): Promise<void>
58
+
59
+ // Default implementation of execute for compiled languages
60
+ async execute(directory: string, inputPath: string, outputPath: string): Promise<void> {
61
+ const executablePath = `solution.${this.extension()}.exe`
62
+ if (!(await exists(join(directory, executablePath)))) {
63
+ throw new Error(`Executable file ${executablePath} does not exist in directory ${directory}`)
64
+ }
65
+ // TODO: check in windows
66
+ const relativeExecutablePath = `.${sep}${executablePath}` // force prepending ./ to make it work
67
+ const fullInputPath = join(directory, inputPath)
68
+ const fullOutputPath = join(directory, outputPath)
69
+ await rm(fullOutputPath, { force: true })
70
+ tui.command(`${relativeExecutablePath} < ${inputPath} > ${outputPath}`)
71
+ const proc = Bun.spawn([relativeExecutablePath], {
72
+ cwd: directory,
73
+ stdin: Bun.file(fullInputPath),
74
+ stdout: Bun.file(fullOutputPath),
75
+ stderr: 'inherit',
76
+ })
77
+ await proc.exited
78
+ const exitCode = proc.exitCode
79
+
80
+ if (exitCode !== 0) throw new Error(`Execution failed for ${executablePath} with exit code ${exitCode}`)
81
+ }
82
+
83
+ protected async getVersion(cmd: string, lineIndex: number): Promise<string> {
84
+ try {
85
+ const proc = Bun.spawn(cmd.split(' '), {
86
+ stdout: 'pipe',
87
+ stderr: 'pipe',
88
+ })
89
+
90
+ const stdout = await new Response(proc.stdout).text()
91
+ const lines = stdout.split('\n')
92
+ return lines[lineIndex]?.trim() || 'Unknown version'
93
+ } catch {
94
+ return 'not found'
95
+ }
96
+ }
97
+ }