@jpetit/toolkit 3.0.23 → 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/assets/prompts/creators/create-solution.tpl.txt +10 -0
- package/assets/prompts/creators/create-statement.tpl.txt +21 -0
- package/assets/prompts/creators/create-translation.tpl.txt +5 -0
- package/assets/prompts/creators/private-test-cases.txt +6 -0
- package/assets/prompts/creators/sample-test-cases.txt +6 -0
- package/assets/prompts/creators/system-prompt.txt +2 -0
- package/assets/prompts/examples/statement-coda.tex +7 -0
- package/assets/prompts/examples/statement.tex +19 -0
- package/assets/prompts/generators/efficiency.md +41 -0
- package/assets/prompts/generators/hard.md +47 -0
- package/assets/prompts/generators/random.md +39 -0
- package/assets/prompts/proglangs/cc.md +3 -0
- package/assets/prompts/proglangs/py.md +40 -0
- package/lib/ai.ts +60 -4
- package/lib/cleaner.ts +24 -13
- package/lib/compilers/base.ts +70 -14
- package/lib/compilers/clojure.ts +21 -10
- package/lib/compilers/gcc.ts +4 -33
- package/lib/compilers/ghc.ts +4 -40
- package/lib/compilers/gxx.ts +4 -33
- package/lib/compilers/index.ts +9 -0
- package/lib/compilers/java.ts +105 -0
- package/lib/compilers/python3.ts +44 -37
- package/lib/compilers/run-clojure.ts +101 -0
- package/lib/compilers/run-haskell.ts +26 -22
- package/lib/compilers/run-python.ts +29 -35
- package/lib/compilers/rust.ts +39 -0
- package/lib/create-with-jutgeai.ts +407 -0
- package/lib/create-with-template.ts +55 -0
- package/lib/data.ts +6 -0
- package/lib/doctor.ts +86 -6
- package/lib/generate.ts +132 -290
- package/lib/helpers.ts +48 -0
- package/lib/inspector.ts +253 -0
- package/lib/jutge_api_client.ts +4631 -0
- package/lib/maker.ts +202 -289
- package/lib/settings.ts +26 -17
- package/lib/tui.ts +25 -15
- package/lib/types.ts +40 -5
- package/lib/upload.ts +216 -0
- package/lib/utils.ts +82 -14
- package/lib/versions.ts +46 -0
- package/package.json +50 -11
- package/toolkit/about.ts +43 -0
- package/toolkit/ai.ts +44 -18
- package/toolkit/check.ts +16 -0
- package/toolkit/clean.ts +16 -26
- package/toolkit/compilers.ts +4 -4
- package/toolkit/config.ts +91 -0
- package/toolkit/create.ts +30 -58
- package/toolkit/doctor.ts +15 -11
- package/toolkit/generate.ts +195 -98
- package/toolkit/index.ts +32 -21
- package/toolkit/make.ts +12 -48
- package/toolkit/upgrade.ts +9 -0
- package/toolkit/upload.ts +19 -0
- package/toolkit/create-jutge-ai.ts +0 -101
- package/toolkit/create-template.ts +0 -55
- package/toolkit/create-wizard.ts +0 -6
- package/toolkit/init.ts +0 -56
- package/toolkit/verify.ts +0 -19
package/lib/settings.ts
CHANGED
|
@@ -1,24 +1,17 @@
|
|
|
1
|
-
import { exists } from 'fs/promises'
|
|
2
1
|
import envPaths from 'env-paths'
|
|
3
|
-
import { mkdir } from 'fs/promises'
|
|
2
|
+
import { exists, mkdir } from 'fs/promises'
|
|
4
3
|
import { join } from 'path'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
const ZSettings = z.object({
|
|
9
|
-
name: z.string().min(1).default('John Doe'),
|
|
10
|
-
email: z.string().email().default('john.doe@example.com'),
|
|
11
|
-
notifications: z.boolean().default(true),
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
export type Settings = z.infer<typeof ZSettings>
|
|
4
|
+
import { guessUserEmail, guessUserName, readYaml, writeYaml } from './utils'
|
|
5
|
+
import { Settings } from './types'
|
|
15
6
|
|
|
16
7
|
export const paths = envPaths('jutge', { suffix: 'toolkit' })
|
|
17
8
|
|
|
18
|
-
function configPath() {
|
|
19
|
-
return join(paths.config, 'config.
|
|
9
|
+
export function configPath() {
|
|
10
|
+
return join(paths.config, 'config.yml')
|
|
20
11
|
}
|
|
21
12
|
|
|
13
|
+
// console.log(`Configuration path: ${configPath()}`)
|
|
14
|
+
|
|
22
15
|
export async function initializePaths() {
|
|
23
16
|
await mkdir(paths.config, { recursive: true })
|
|
24
17
|
await mkdir(paths.data, { recursive: true })
|
|
@@ -29,14 +22,30 @@ export async function initializePaths() {
|
|
|
29
22
|
|
|
30
23
|
export async function saveSettings(settings: Settings) {
|
|
31
24
|
await initializePaths()
|
|
32
|
-
|
|
25
|
+
settings = Settings.parse(settings)
|
|
26
|
+
return await writeYaml(configPath(), settings)
|
|
33
27
|
}
|
|
34
28
|
|
|
35
29
|
export async function loadSettings(): Promise<Settings> {
|
|
36
|
-
const data = await
|
|
37
|
-
return
|
|
30
|
+
const data = await readYaml(configPath())
|
|
31
|
+
return Settings.parse(data)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function loadSettingsAtStart(): Promise<Settings> {
|
|
35
|
+
if (!(await settingsExist())) {
|
|
36
|
+
const defaultSettings = Settings.parse({})
|
|
37
|
+
defaultSettings.name = (await guessUserName()) || 'John Doe'
|
|
38
|
+
defaultSettings.email = (await guessUserEmail()) || 'john.doe@example.com'
|
|
39
|
+
await saveSettings(defaultSettings)
|
|
40
|
+
return defaultSettings
|
|
41
|
+
} else {
|
|
42
|
+
return await loadSettings()
|
|
43
|
+
}
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
export async function settingsExist(): Promise<boolean> {
|
|
41
47
|
return await exists(configPath())
|
|
42
48
|
}
|
|
49
|
+
|
|
50
|
+
// settings loaded at the start of the program
|
|
51
|
+
export const settings = await loadSettingsAtStart()
|
package/lib/tui.ts
CHANGED
|
@@ -56,21 +56,20 @@ function title(text: string) {
|
|
|
56
56
|
)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
italic?: boolean
|
|
59
|
+
function command(text: string) {
|
|
60
|
+
console.log(chalk.magenta(`❯ ${text}`))
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
function
|
|
65
|
-
|
|
66
|
-
console.log(chalk.magenta.italic(`❯ ${text}`))
|
|
67
|
-
} else {
|
|
68
|
-
console.log(chalk.magenta(`❯ ${text}`))
|
|
69
|
-
}
|
|
63
|
+
function directory(text: string) {
|
|
64
|
+
console.log(chalk.magenta('◳ ' + fileLink(process.cwd(), text)))
|
|
70
65
|
}
|
|
71
66
|
|
|
72
|
-
function
|
|
73
|
-
console.log(chalk.magenta('
|
|
67
|
+
function url(dst: string) {
|
|
68
|
+
console.log(chalk.magenta('🌐 ' + terminalLink(dst, dst)))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function link(dst: string, text?: string) {
|
|
72
|
+
return terminalLink(text || dst, dst)
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
function warning(text: string) {
|
|
@@ -89,7 +88,11 @@ function action(text: string) {
|
|
|
89
88
|
console.log(chalk.blue(`${text}...`))
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
function
|
|
91
|
+
function gray(text: string) {
|
|
92
|
+
print(chalk.gray(text))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function print(text: string = ''): void {
|
|
93
96
|
const lines = text.split('\n')
|
|
94
97
|
for (const line of lines) {
|
|
95
98
|
console.log(line)
|
|
@@ -117,8 +120,12 @@ async function image(path: string, width: number, height: number): Promise<void>
|
|
|
117
120
|
console.log(await terminalImage.file(path, { width, height }))
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
function
|
|
121
|
-
|
|
123
|
+
function fileLink(dir: string, path?: string): string {
|
|
124
|
+
if (path) {
|
|
125
|
+
return terminalLink(path, 'file://' + resolve(dir, path))
|
|
126
|
+
} else {
|
|
127
|
+
return terminalLink(dir, 'file://' + resolve(dir))
|
|
128
|
+
}
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
export default {
|
|
@@ -129,8 +136,11 @@ export default {
|
|
|
129
136
|
title,
|
|
130
137
|
command,
|
|
131
138
|
directory,
|
|
139
|
+
url,
|
|
140
|
+
link,
|
|
132
141
|
warning,
|
|
133
142
|
error,
|
|
143
|
+
gray,
|
|
134
144
|
success,
|
|
135
145
|
action,
|
|
136
146
|
print,
|
|
@@ -138,5 +148,5 @@ export default {
|
|
|
138
148
|
yaml,
|
|
139
149
|
json,
|
|
140
150
|
image,
|
|
141
|
-
hyperlink,
|
|
151
|
+
hyperlink: fileLink,
|
|
142
152
|
}
|
package/lib/types.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
handler: z.enum(['std', 'graphic']).default('std'),
|
|
3
|
+
export const Handler = z.object({
|
|
4
|
+
handler: z.enum(['std', 'graphic', 'quiz']).default('std'),
|
|
5
5
|
solution: z.string().default('C++'),
|
|
6
6
|
source_modifier: z.enum(['none', 'no_main', 'structs']).default('none'),
|
|
7
7
|
compilers: z.enum(['RunPython', 'RunHaskell', 'RunClojure', 'GHC']).nullable().default(null),
|
|
8
8
|
})
|
|
9
9
|
|
|
10
|
-
export type Handler = z.infer<typeof
|
|
10
|
+
export type Handler = z.infer<typeof Handler>
|
|
11
11
|
|
|
12
|
-
export const
|
|
12
|
+
export const Scores = z.array(
|
|
13
13
|
z.object({
|
|
14
14
|
part: z.string(),
|
|
15
15
|
prefix: z.string(),
|
|
@@ -17,4 +17,39 @@ export const ZScores = z.array(
|
|
|
17
17
|
}),
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
export type Scores = z.infer<typeof
|
|
20
|
+
export type Scores = z.infer<typeof Scores>
|
|
21
|
+
|
|
22
|
+
export const Specification = z.object({
|
|
23
|
+
title: z.string(),
|
|
24
|
+
description: z.string(),
|
|
25
|
+
author: z.string(),
|
|
26
|
+
email: z.string().email(),
|
|
27
|
+
golden_proglang: z.string(),
|
|
28
|
+
more_proglangs: z.array(z.string()),
|
|
29
|
+
original_language: z.string(),
|
|
30
|
+
more_languages: z.array(z.string()),
|
|
31
|
+
generators: z.array(z.string()),
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export type Specification = z.infer<typeof Specification>
|
|
35
|
+
|
|
36
|
+
export const ProblemInfo = z.object({
|
|
37
|
+
problem_nm: z.string(),
|
|
38
|
+
passcode: z.string(),
|
|
39
|
+
created_at: z.string(),
|
|
40
|
+
updated_at: z.string(),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export type ProblemInfo = z.infer<typeof ProblemInfo>
|
|
44
|
+
|
|
45
|
+
export const Settings = z.object({
|
|
46
|
+
name: z.string().min(1).default('John Doe'),
|
|
47
|
+
email: z.string().email().default('john.doe@example.com'),
|
|
48
|
+
defaultModel: z.string().default('google/gemini-2.5-flash-lite'),
|
|
49
|
+
notifications: z.boolean().default(false),
|
|
50
|
+
showPrompts: z.boolean().default(false),
|
|
51
|
+
showAnswers: z.boolean().default(false),
|
|
52
|
+
developer: z.boolean().default(false),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
export type Settings = z.infer<typeof Settings>
|
package/lib/upload.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import archiver from 'archiver'
|
|
2
|
+
import { createWriteStream } from 'fs'
|
|
3
|
+
import { glob, mkdir } from 'fs/promises'
|
|
4
|
+
import { basename, join } from 'path'
|
|
5
|
+
import { JutgeApiClient } from './jutge_api_client'
|
|
6
|
+
import tui from './tui'
|
|
7
|
+
import { ProblemInfo } from './types'
|
|
8
|
+
import {
|
|
9
|
+
createFileFromPath,
|
|
10
|
+
existsInDir,
|
|
11
|
+
humanid,
|
|
12
|
+
nanoid12,
|
|
13
|
+
readYamlInDir,
|
|
14
|
+
toolkitPrefix,
|
|
15
|
+
writeYamlInDir,
|
|
16
|
+
} from './utils'
|
|
17
|
+
import YAML from 'yaml'
|
|
18
|
+
|
|
19
|
+
export async function uploadProblem(directory: string) {
|
|
20
|
+
await tui.section('Uploading problem', async () => {
|
|
21
|
+
await tui.section('Checking .inp/.cor files', async () => {
|
|
22
|
+
// TODO: check that each .inp has a corresponding .cor file
|
|
23
|
+
// TODO: check that date of .cor is not before .inp nor before solution/main files
|
|
24
|
+
const inps = await Array.fromAsync(glob('*.inp', { cwd: directory }))
|
|
25
|
+
const cors = await Array.fromAsync(glob('*.cor', { cwd: directory }))
|
|
26
|
+
if (inps.length !== cors.length) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Number of .inp files (${inps.length}) does not match number of .cor files (${cors.length})`,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
tui.success(`Looks good`)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const tmpDir = join(directory, toolkitPrefix() + '-zip', humanid())
|
|
35
|
+
const base = basename(process.cwd())
|
|
36
|
+
const zipFilePath = join(tmpDir, `${base}.zip`)
|
|
37
|
+
await mkdir(tmpDir, { recursive: true })
|
|
38
|
+
|
|
39
|
+
await tui.section('Creating zip file', async () => {
|
|
40
|
+
tui.directory(tmpDir)
|
|
41
|
+
await createZipFile(directory, zipFilePath, base)
|
|
42
|
+
tui.success(`Created zip file ${base}.zip at ${tui.hyperlink(tmpDir)}`)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
let info = await tui.section('Loading problem.yml', async () => {
|
|
46
|
+
const info = await readProblemInfo(directory)
|
|
47
|
+
if (info) {
|
|
48
|
+
tui.print(YAML.stringify(info, null, 4).trim())
|
|
49
|
+
} else {
|
|
50
|
+
tui.warning('problem.yml not found, will create a new problem')
|
|
51
|
+
}
|
|
52
|
+
return info
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
if (info) {
|
|
56
|
+
info = await updateProblem(directory, info, zipFilePath)
|
|
57
|
+
} else {
|
|
58
|
+
info = await createProblem(directory, zipFilePath)
|
|
59
|
+
}
|
|
60
|
+
tui.print(YAML.stringify(info, null, 4).trim())
|
|
61
|
+
tui.url(`https://jutge.org/problems/${info.problem_nm}`)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function createProblem(directory: string, zipFilePath: string): Promise<ProblemInfo> {
|
|
66
|
+
const info = {
|
|
67
|
+
problem_nm: '',
|
|
68
|
+
passcode: nanoid12(),
|
|
69
|
+
created_at: '',
|
|
70
|
+
updated_at: '',
|
|
71
|
+
}
|
|
72
|
+
const jutge = new JutgeApiClient()
|
|
73
|
+
|
|
74
|
+
await tui.section('Loging in into Jutge.org', async () => {
|
|
75
|
+
await login(jutge)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
await tui.section('Creating problem in Jutge.org', async () => {
|
|
79
|
+
const file = await createFileFromPath(zipFilePath, 'application/zip')
|
|
80
|
+
const { id } = await jutge.instructor.problems.legacyCreateWithTerminal(info.passcode, file)
|
|
81
|
+
const problem_nm = await showTerminalOutput(id)
|
|
82
|
+
if (!problem_nm) throw new Error('Failed to get problem name')
|
|
83
|
+
info.problem_nm = problem_nm
|
|
84
|
+
info.created_at = new Date().toISOString()
|
|
85
|
+
info.updated_at = new Date().toISOString()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
await tui.section('Creating problem.yml', async () => {
|
|
89
|
+
await writeProblemInfo(directory, info)
|
|
90
|
+
tui.success(`Created problem.yml`)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
tui.print('')
|
|
94
|
+
tui.success(`Problem ${info.problem_nm} created successfully`)
|
|
95
|
+
|
|
96
|
+
return info
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function updateProblem(directory: string, info: ProblemInfo, zipFilePath: string): Promise<ProblemInfo> {
|
|
100
|
+
const jutge = new JutgeApiClient()
|
|
101
|
+
|
|
102
|
+
await tui.section('Loging in into Jutge.org', async () => {
|
|
103
|
+
await login(jutge)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
await tui.section('Updating problem in Jutge.org', async () => {
|
|
107
|
+
const file = await createFileFromPath(zipFilePath, 'application/zip')
|
|
108
|
+
const { id } = await jutge.instructor.problems.legacyUpdateWithTerminal(info.problem_nm, file)
|
|
109
|
+
await showTerminalOutput(id)
|
|
110
|
+
info.updated_at = new Date().toISOString()
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
await tui.section('Updating problem.yml', async () => {
|
|
114
|
+
await writeProblemInfo(directory, info)
|
|
115
|
+
tui.success(`Updated problem.yml`)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
tui.print('')
|
|
119
|
+
tui.success(`Problem ${info.problem_nm} updated successfully`)
|
|
120
|
+
|
|
121
|
+
return info
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function createZipFile(directory: string, zipFilePath: string, base: string) {
|
|
125
|
+
const output = createWriteStream(zipFilePath)
|
|
126
|
+
const archive = archiver('zip', { zlib: { level: 9 } })
|
|
127
|
+
|
|
128
|
+
// Wrap the completion in a Promise
|
|
129
|
+
const zipPromise = new Promise<void>((resolve, reject) => {
|
|
130
|
+
output.on('close', function () {
|
|
131
|
+
// console.log(`Archive created: ${archive.pointer()} total bytes`)
|
|
132
|
+
resolve() // ← Resolve when done
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
output.on('error', reject) // ← Handle output stream errors
|
|
136
|
+
archive.on('error', reject) // ← Handle archive errors
|
|
137
|
+
archive.on('warning', function (err) {
|
|
138
|
+
if (err.code === 'ENOENT') {
|
|
139
|
+
console.warn(err)
|
|
140
|
+
} else {
|
|
141
|
+
reject(err)
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
// Pipe archive data to the file
|
|
147
|
+
archive.pipe(output)
|
|
148
|
+
|
|
149
|
+
const add = async (pattern: string) => {
|
|
150
|
+
const files = glob(pattern, { cwd: directory })
|
|
151
|
+
const list = await Array.fromAsync(files)
|
|
152
|
+
const sortedList = list.sort()
|
|
153
|
+
for (const file of sortedList) {
|
|
154
|
+
tui.print(` add ${file}`)
|
|
155
|
+
archive.file(join(directory, file), { name: join(base, file) })
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Add files according to Jutge.org problem structure
|
|
160
|
+
await add('README.{md,txt}')
|
|
161
|
+
await add('problem.yml')
|
|
162
|
+
await add('handler.yml')
|
|
163
|
+
await add('problem.[a-z][a-z].{yml,tex}')
|
|
164
|
+
await add('solution.*')
|
|
165
|
+
await add('main.*')
|
|
166
|
+
await add('*.{inp,cor}')
|
|
167
|
+
await add('award.{html,png}')
|
|
168
|
+
await add('*.png')
|
|
169
|
+
|
|
170
|
+
// Finalize the archive (this does not end immediately, thus the Promise)
|
|
171
|
+
await archive.finalize()
|
|
172
|
+
await zipPromise // ← Wait for the file to actually be written
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// returns the found problem_nm
|
|
176
|
+
async function showTerminalOutput(id: string): Promise<string | null> {
|
|
177
|
+
let problem_nm: string | undefined = undefined
|
|
178
|
+
|
|
179
|
+
const jutge = new JutgeApiClient()
|
|
180
|
+
|
|
181
|
+
const response = await fetch(`${jutge.JUTGE_API_URL}/webstreams/${id}`)
|
|
182
|
+
if (response.body === null) return null
|
|
183
|
+
|
|
184
|
+
const reader = response.body.getReader()
|
|
185
|
+
while (true) {
|
|
186
|
+
const { done, value } = await reader.read()
|
|
187
|
+
if (done) return problem_nm || null
|
|
188
|
+
const text = new TextDecoder().decode(value as Uint8Array) // TODO: check
|
|
189
|
+
tui.print(text.replaceAll(/\n/g, '\r\n'))
|
|
190
|
+
const matchCreated = text.match(/Problem ([A-Z]\d{5}) created./)
|
|
191
|
+
if (matchCreated) problem_nm = matchCreated[1]
|
|
192
|
+
const matchUpdated = text.match(/Problem ([A-Z]\d{5}) updated./)
|
|
193
|
+
if (matchUpdated) problem_nm = matchUpdated[1]
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function readProblemInfo(directory: string): Promise<ProblemInfo | null> {
|
|
198
|
+
if (!(await existsInDir(directory, 'problem.yml'))) {
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
return ProblemInfo.parse(await readYamlInDir(directory, 'problem.yml'))
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function writeProblemInfo(directory: string, problemInfo: ProblemInfo): Promise<void> {
|
|
205
|
+
await writeYamlInDir(directory, 'problem.yml', problemInfo)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// TODO: improve login mechanism
|
|
209
|
+
async function login(jutge: JutgeApiClient): Promise<void> {
|
|
210
|
+
const email = process.env.JUTGE_EMAIL
|
|
211
|
+
const password = process.env.JUTGE_PASSWORD
|
|
212
|
+
if (!email || !password) {
|
|
213
|
+
throw new Error('JUTGE_EMAIL and JUTGE_PASSWORD environment variables must be set to login')
|
|
214
|
+
}
|
|
215
|
+
await jutge.login({ email, password })
|
|
216
|
+
}
|
package/lib/utils.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { execa } from 'execa'
|
|
2
|
-
import {
|
|
3
|
-
import { readFile, stat, writeFile } from 'fs/promises'
|
|
2
|
+
import { exists, readFile, stat, writeFile } from 'fs/promises'
|
|
4
3
|
import humanId from 'human-id'
|
|
5
4
|
import { customAlphabet } from 'nanoid'
|
|
6
|
-
import { dirname, join } from 'path'
|
|
5
|
+
import { basename, dirname, join, normalize } from 'path'
|
|
7
6
|
import { fileURLToPath } from 'url'
|
|
8
7
|
import YAML from 'yaml'
|
|
9
8
|
|
|
@@ -11,40 +10,52 @@ export async function nothing(): Promise<void> {
|
|
|
11
10
|
// Do nothing: just a way to ignore lint warnings
|
|
12
11
|
}
|
|
13
12
|
|
|
13
|
+
export async function existsInDir(directory: string, path: string): Promise<boolean> {
|
|
14
|
+
return await exists(join(directory, path))
|
|
15
|
+
}
|
|
16
|
+
|
|
14
17
|
export async function readText(path: string): Promise<string> {
|
|
15
18
|
const content = await readFile(path, 'utf8')
|
|
16
19
|
return content
|
|
17
20
|
}
|
|
18
21
|
|
|
22
|
+
export async function readTextInDir(directory: string, path: string): Promise<string> {
|
|
23
|
+
return await readFile(join(directory, path), 'utf8')
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
export async function readBytes(path: string): Promise<Uint8Array> {
|
|
20
27
|
const content = await readFile(path)
|
|
21
28
|
return content
|
|
22
29
|
}
|
|
23
30
|
|
|
31
|
+
export async function readBytesInDir(directory: string, path: string): Promise<Uint8Array> {
|
|
32
|
+
return await readBytes(join(directory, path))
|
|
33
|
+
}
|
|
34
|
+
|
|
24
35
|
export async function readYaml(path: string): Promise<any> {
|
|
25
36
|
const content = await readText(path)
|
|
26
37
|
return YAML.parse(content)
|
|
27
38
|
}
|
|
28
39
|
|
|
40
|
+
export async function readYamlInDir(directory: string, path: string): Promise<any> {
|
|
41
|
+
return await readYaml(join(directory, path))
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
export async function readJson(path: string): Promise<any> {
|
|
30
45
|
const content = await readText(path)
|
|
31
46
|
return JSON.parse(content)
|
|
32
47
|
}
|
|
33
48
|
|
|
49
|
+
export async function readJsonInDir(directory: string, path: string): Promise<any> {
|
|
50
|
+
return await readJson(join(directory, path))
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
export async function writeText(path: string, content: string): Promise<void> {
|
|
35
54
|
await writeFile(path, content)
|
|
36
55
|
}
|
|
37
56
|
|
|
38
|
-
export async function
|
|
39
|
-
|
|
40
|
-
outputPath: string,
|
|
41
|
-
separator: string = '\n\n\n',
|
|
42
|
-
): Promise<void> {
|
|
43
|
-
let content = ''
|
|
44
|
-
for (const inputPath of inputPaths) {
|
|
45
|
-
content += (await readText(inputPath)) + separator
|
|
46
|
-
}
|
|
47
|
-
await writeText(outputPath, content)
|
|
57
|
+
export async function writeTextInDir(directory: string, path: string, content: string): Promise<void> {
|
|
58
|
+
await writeText(join(directory, path), content)
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
export async function writeYaml(path: string, data: any): Promise<void> {
|
|
@@ -52,22 +63,38 @@ export async function writeYaml(path: string, data: any): Promise<void> {
|
|
|
52
63
|
await writeText(path, content)
|
|
53
64
|
}
|
|
54
65
|
|
|
66
|
+
export async function writeYamlInDir(directory: string, path: string, data: any): Promise<void> {
|
|
67
|
+
await writeYaml(join(directory, path), data)
|
|
68
|
+
}
|
|
69
|
+
|
|
55
70
|
export async function writeJson(path: string, data: any): Promise<void> {
|
|
56
71
|
const content = JSON.stringify(data, null, 4)
|
|
57
72
|
await writeText(path, content)
|
|
58
73
|
}
|
|
59
74
|
|
|
75
|
+
export async function writeJsonInDir(directory: string, path: string, data: any): Promise<void> {
|
|
76
|
+
await writeJson(join(directory, path), data)
|
|
77
|
+
}
|
|
78
|
+
|
|
60
79
|
export async function fileSize(path: string): Promise<number> {
|
|
61
80
|
const stats = await stat(path)
|
|
62
81
|
return stats.size
|
|
63
82
|
}
|
|
64
83
|
|
|
84
|
+
export async function fileSizeInDir(directory: string, path: string): Promise<number> {
|
|
85
|
+
return await fileSize(join(directory, path))
|
|
86
|
+
}
|
|
87
|
+
|
|
65
88
|
export async function filesAreEqual(path1: string, path2: string): Promise<boolean> {
|
|
66
89
|
const bytes1 = await readBytes(path1)
|
|
67
90
|
const bytes2 = await readBytes(path2)
|
|
68
91
|
return bytes1.length === bytes2.length && bytes1.every((b, i) => b === bytes2[i])
|
|
69
92
|
}
|
|
70
93
|
|
|
94
|
+
export async function filesAreEqualInDir(directory: string, path1: string, path2: string): Promise<boolean> {
|
|
95
|
+
return await filesAreEqual(join(directory, path1), join(directory, path2))
|
|
96
|
+
}
|
|
97
|
+
|
|
71
98
|
export async function isDirectory(path: string): Promise<boolean> {
|
|
72
99
|
try {
|
|
73
100
|
const stats = await stat(path)
|
|
@@ -77,6 +104,10 @@ export async function isDirectory(path: string): Promise<boolean> {
|
|
|
77
104
|
}
|
|
78
105
|
}
|
|
79
106
|
|
|
107
|
+
export async function isDirectoryInDir(directory: string, path: string): Promise<boolean> {
|
|
108
|
+
return await isDirectory(join(directory, path))
|
|
109
|
+
}
|
|
110
|
+
|
|
80
111
|
// Guess user name from git config
|
|
81
112
|
export async function guessUserName(): Promise<string | null> {
|
|
82
113
|
try {
|
|
@@ -129,5 +160,42 @@ export function humanid(): string {
|
|
|
129
160
|
export function projectDir(): string {
|
|
130
161
|
const __filename = fileURLToPath(import.meta.url)
|
|
131
162
|
const __dirname = dirname(__filename)
|
|
132
|
-
return join(__dirname, '..')
|
|
163
|
+
return normalize(join(__dirname, '..'))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function toolkitPrefix(): string {
|
|
167
|
+
return 'jtk'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function createFileFromPath(path: string, type: string): Promise<File> {
|
|
171
|
+
const buffer = await readFile(path)
|
|
172
|
+
const filename = basename(path)
|
|
173
|
+
const file = new File([buffer], filename, { type })
|
|
174
|
+
return file
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Utility function to strip LaTeX commands from text
|
|
178
|
+
// Simplistic implementation; may need to be improved for complex LaTeX but works for basic cases and our needs
|
|
179
|
+
export function stripLaTeX(text: string): string {
|
|
180
|
+
return text
|
|
181
|
+
.replace(/\\[a-zA-Z]+\{([^}]*)\}/g, '$1') // Remove commands like \textbf{text}
|
|
182
|
+
.replace(/\$\$([^$]+)\$\$/g, '$1') // Remove display math $$...$$
|
|
183
|
+
.replace(/\$([^$]+)\$/g, '$1') // Remove inline math $...$
|
|
184
|
+
.replace(/\\[a-zA-Z]+/g, '') // Remove other commands
|
|
185
|
+
.replace(/[{}]/g, '') // Remove remaining braces
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function convertStringToItsType(value: string): string | number | boolean {
|
|
189
|
+
// Try parsing as boolean
|
|
190
|
+
const lower = value.toLowerCase()
|
|
191
|
+
if (lower === 'true') return true
|
|
192
|
+
if (lower === 'false') return false
|
|
193
|
+
|
|
194
|
+
// Try parsing as number
|
|
195
|
+
if (!isNaN(Number(value)) && value.trim() !== '') {
|
|
196
|
+
return Number(value)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Return as string
|
|
200
|
+
return value
|
|
133
201
|
}
|
package/lib/versions.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import semver from 'semver'
|
|
4
|
+
import tui from './tui'
|
|
5
|
+
import { nothing, projectDir, readJson } from './utils'
|
|
6
|
+
|
|
7
|
+
const packageJson = await readJson(join(projectDir(), 'package.json'))
|
|
8
|
+
|
|
9
|
+
export async function checkVersion(): Promise<void> {
|
|
10
|
+
const currentVersion = await getCurrentVersion()
|
|
11
|
+
const latestPublishedVersion = await getLatestPublishedVersion()
|
|
12
|
+
if (semver.lt(currentVersion, latestPublishedVersion)) {
|
|
13
|
+
tui.warning(
|
|
14
|
+
`A new version of ${packageJson.name} is available: ${latestPublishedVersion} (you have ${packageJson.version})`,
|
|
15
|
+
)
|
|
16
|
+
tui.warning(`Please update by running: jtk upgrade`)
|
|
17
|
+
tui.print()
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getCurrentVersion(): Promise<string> {
|
|
22
|
+
await nothing()
|
|
23
|
+
return packageJson.version as string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function getLatestPublishedVersion(): Promise<string> {
|
|
27
|
+
const response = await fetch(`https://registry.npmjs.org/${packageJson.name}/latest`)
|
|
28
|
+
const data = (await response.json()) as any
|
|
29
|
+
return data.version as string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function upgrade(): Promise<void> {
|
|
33
|
+
const currentVersion = await getCurrentVersion()
|
|
34
|
+
const latestPublishedVersion = await getLatestPublishedVersion()
|
|
35
|
+
if (semver.eq(currentVersion, latestPublishedVersion)) {
|
|
36
|
+
tui.success(`You already have the latest version (${currentVersion})`)
|
|
37
|
+
} else if (semver.gt(currentVersion, latestPublishedVersion)) {
|
|
38
|
+
tui.success(
|
|
39
|
+
`You have a newer version (${currentVersion}) than the latest published version (${latestPublishedVersion})`,
|
|
40
|
+
)
|
|
41
|
+
} else {
|
|
42
|
+
tui.action(`Upgrading from version ${currentVersion} to ${latestPublishedVersion}`)
|
|
43
|
+
execSync(`bun install --global ${packageJson.name}@latest`, { stdio: 'inherit' })
|
|
44
|
+
tui.success(`Successfully upgraded to version ${latestPublishedVersion}`)
|
|
45
|
+
}
|
|
46
|
+
}
|