@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.
- package/.prettierignore +11 -0
- package/.prettierrc.json +9 -0
- package/.vscode/settings.json +26 -0
- package/README.md +22 -0
- package/assets/lua/fixCodeBlocks.lua +19 -0
- package/assets/lua/removeEnvs.lua +20 -0
- package/assets/lua/removeHtmlOnly.lua +11 -0
- package/assets/problems/graphics/japanese-flag.pbm/README.md +14 -0
- package/assets/problems/graphics/japanese-flag.pbm/award.png +0 -0
- package/assets/problems/graphics/japanese-flag.pbm/handler.yml +2 -0
- package/assets/problems/graphics/japanese-flag.pbm/problem.ca.tex +21 -0
- package/assets/problems/graphics/japanese-flag.pbm/problem.ca.yml +3 -0
- package/assets/problems/graphics/japanese-flag.pbm/sample-1.inp +1 -0
- package/assets/problems/graphics/japanese-flag.pbm/sample-2.inp +1 -0
- package/assets/problems/graphics/japanese-flag.pbm/solution.cc +25 -0
- package/assets/problems/graphics/japanese-flag.pbm/solution.py +11 -0
- package/assets/problems/graphics/tortuga.pbm/README.md +13 -0
- package/assets/problems/graphics/tortuga.pbm/award.png +0 -0
- package/assets/problems/graphics/tortuga.pbm/handler.yml +2 -0
- package/assets/problems/graphics/tortuga.pbm/problem.ca.tex +23 -0
- package/assets/problems/graphics/tortuga.pbm/problem.ca.yml +3 -0
- package/assets/problems/graphics/tortuga.pbm/sample.inp +0 -0
- package/assets/problems/graphics/tortuga.pbm/solution.py +11 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/README.md +15 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/award.html +1 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/award.png +0 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/campanar.eps +1113 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/campanar.png +0 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/generate.cc +10 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/handler.yml +2 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.ca.tex +59 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.ca.yml +3 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.en.tex +52 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/problem.en.yml +4 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/sample.inp +7 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/slow.cc +29 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/solution.cc +48 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-1.inp +12 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-2.inp +100000 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-2.ops +1 -0
- package/assets/problems/standard/campanar-de-la-torrassa.pbm/test-b.inp +0 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/README.md +11 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/handler.yml +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/problem.ca.tex +17 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/problem.ca.yml +3 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/problem.en.tex +16 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/problem.en.yml +4 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/sample-1.inp +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/sample-2.inp +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/sample-3.inp +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/solution.c +18 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/solution.cc +13 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/solution.java +16 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/solution.py +5 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/test-1.inp +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/test-2.inp +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/test-3.inp +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/test-4.inp +1 -0
- package/assets/problems/standard/maximum-of-2-integers.pbm/test-5.inp +1 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/README.md +12 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/atzar.cc +85 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/award.png +0 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/generate-1.cc +26 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/generate-2.cc +26 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/generate-3.cc +26 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/generate-4.cc +26 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/handler.yml +1 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/problem.ca.tex +40 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/problem.ca.yml +3 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/problem.en.tex +40 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/problem.en.yml +4 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/random-1.inp +24 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/random-2.inp +27 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/random-3.inp +38 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/random-4.inp +50 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/sample-1.inp +9 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/sample-2.inp +6 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/sample-3.inp +7 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/solution.cc +38 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-1.inp +5 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-2.inp +6 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-3.inp +6 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-4.inp +9 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-5.inp +10 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-6.inp +9 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-7.inp +12 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-8.inp +3 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-9.inp +37 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-91.inp +52 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-92.inp +25 -0
- package/assets/problems/standard/treasures-in-a-map.pbm/test-93.inp +3 -0
- package/assets/sty/judgeit.ca.sty +54 -0
- package/assets/sty/judgeit.de.sty +61 -0
- package/assets/sty/judgeit.en.sty +60 -0
- package/assets/sty/judgeit.es.sty +54 -0
- package/assets/sty/judgeit.fr.sty +59 -0
- package/assets/sty/judgeit.sty +307 -0
- package/assets/sty/picins.sty +579 -0
- package/assets.zip +0 -0
- package/eslint.config.mjs +31 -0
- package/lib/ai.ts +138 -0
- package/lib/assets.ts +31 -0
- package/lib/cleaner.ts +58 -0
- package/lib/compilers/_frompython.ts +388 -0
- package/lib/compilers/base.ts +97 -0
- package/lib/compilers/gcc.ts +47 -0
- package/lib/compilers/gxx.ts +47 -0
- package/lib/compilers/index.ts +61 -0
- package/lib/compilers/python3.ts +67 -0
- package/lib/data.ts +19 -0
- package/lib/doctor.ts +104 -0
- package/lib/generate.ts +333 -0
- package/lib/maker.ts +535 -0
- package/lib/settings.ts +42 -0
- package/lib/tui.ts +69 -0
- package/lib/utils.ts +83 -0
- package/package.json +56 -0
- package/problems/graphic.pbm/README.md +14 -0
- package/problems/graphic.pbm/award.png +0 -0
- package/problems/graphic.pbm/handler.yml +2 -0
- package/problems/graphic.pbm/problem.ca.html +13 -0
- package/problems/graphic.pbm/problem.ca.md +20 -0
- package/problems/graphic.pbm/problem.ca.tex +21 -0
- package/problems/graphic.pbm/problem.ca.txt +20 -0
- package/problems/graphic.pbm/problem.ca.yml +3 -0
- package/problems/graphic.pbm/sample-1.inp +1 -0
- package/problems/graphic.pbm/sample-2.inp +1 -0
- package/problems/graphic.pbm/solution.py +11 -0
- package/problems/maxim2.pbm/Main.java +13 -0
- package/problems/maxim2.pbm/distillation.yml +7 -0
- package/problems/maxim2.pbm/distilled-01.inp +1 -0
- package/problems/maxim2.pbm/distilled-02.inp +1 -0
- package/problems/maxim2.pbm/distilled-03.inp +1 -0
- package/problems/maxim2.pbm/distiller.yml +2 -0
- package/problems/maxim2.pbm/generate-inputs.py +9 -0
- package/problems/maxim2.pbm/handler.yml +2 -0
- package/problems/maxim2.pbm/ma-1.inp +1 -0
- package/problems/maxim2.pbm/ma-2.inp +1 -0
- package/problems/maxim2.pbm/ma-3.inp +1 -0
- package/problems/maxim2.pbm/ma-4.inp +1 -0
- package/problems/maxim2.pbm/ma-5.inp +1 -0
- package/problems/maxim2.pbm/per-doubles.inp +1 -0
- package/problems/maxim2.pbm/problem.ca.html +11 -0
- package/problems/maxim2.pbm/problem.ca.md +19 -0
- package/problems/maxim2.pbm/problem.ca.tex +17 -0
- package/problems/maxim2.pbm/problem.ca.txt +19 -0
- package/problems/maxim2.pbm/problem.ca.yml +3 -0
- package/problems/maxim2.pbm/problem.en.html +11 -0
- package/problems/maxim2.pbm/problem.en.md +19 -0
- package/problems/maxim2.pbm/problem.en.tex +16 -0
- package/problems/maxim2.pbm/problem.en.txt +19 -0
- package/problems/maxim2.pbm/problem.en.yml +4 -0
- package/problems/maxim2.pbm/sample-1.inp +1 -0
- package/problems/maxim2.pbm/sample-2.inp +1 -0
- package/problems/maxim2.pbm/sample-3.inp +1 -0
- package/problems/maxim2.pbm/solution.c +12 -0
- package/problems/maxim2.pbm/solution.cc +13 -0
- package/problems/maxim2.pbm/solution.java +13 -0
- package/problems/maxim2.pbm/solution.pas +9 -0
- package/problems/maxim2.pbm/solution.py +5 -0
- package/problems/maxim2.pbm/tags.yml +2 -0
- package/problems/maxim2.pbm/test_-1_-1.inp +1 -0
- package/problems/maxim2.pbm/test_-1_-2.inp +1 -0
- package/problems/maxim2.pbm/test_-1_0.inp +1 -0
- package/problems/maxim2.pbm/test_-1_1.inp +1 -0
- package/problems/maxim2.pbm/test_-2_-1.inp +1 -0
- package/problems/maxim2.pbm/test_-2_-2.inp +1 -0
- package/problems/maxim2.pbm/test_-2_0.inp +1 -0
- package/problems/maxim2.pbm/test_-2_1.inp +1 -0
- package/problems/maxim2.pbm/test_0_-1.inp +1 -0
- package/problems/maxim2.pbm/test_0_-2.inp +1 -0
- package/problems/maxim2.pbm/test_0_0.inp +1 -0
- package/problems/maxim2.pbm/test_0_1.inp +1 -0
- package/problems/maxim2.pbm/test_1_-1.inp +1 -0
- package/problems/maxim2.pbm/test_1_-2.inp +1 -0
- package/problems/maxim2.pbm/test_1_0.inp +1 -0
- package/problems/maxim2.pbm/test_1_1.inp +1 -0
- package/test.ts +3 -0
- package/toolkit/ai.ts +30 -0
- package/toolkit/clean.ts +19 -0
- package/toolkit/compilers.ts +29 -0
- package/toolkit/create-jutge-ai.ts +101 -0
- package/toolkit/create-template.ts +51 -0
- package/toolkit/create-wizard.ts +4 -0
- package/toolkit/create.ts +75 -0
- package/toolkit/doctor.ts +17 -0
- package/toolkit/index.ts +28 -0
- package/toolkit/init.ts +66 -0
- package/toolkit/make.ts +60 -0
- package/toolkit/verify.ts +19 -0
- package/tsconfig.json +38 -0
- 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
|
+
}
|