@jpetit/toolkit 3.1.1 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/tui.ts ADDED
@@ -0,0 +1,152 @@
1
+ import boxen from 'boxen'
2
+ import chalk from 'chalk'
3
+ import { marked } from 'marked'
4
+ import { markedTerminal } from 'marked-terminal'
5
+ import { resolve } from 'path'
6
+ import terminalImage from 'terminal-image'
7
+ import terminalLink from 'terminal-link'
8
+ import YAML from 'yaml'
9
+
10
+ let indentation = 0
11
+
12
+ const symbols = ['', '▶', '◆', '●', '■']
13
+
14
+ function sectionStart(text: string) {
15
+ if (indentation === 0) {
16
+ console.log()
17
+ if (text) {
18
+ console.log(chalk.blue(boxen(text, { padding: { left: 1, right: 1 }, width: process.stdout.columns })))
19
+ }
20
+ } else {
21
+ if (text) {
22
+ console.log(chalk.blue(`${symbols[indentation]} ${text}`))
23
+ }
24
+ }
25
+ ++indentation
26
+ console.group()
27
+ }
28
+
29
+ function sectionEnd() {
30
+ --indentation
31
+ console.groupEnd()
32
+ }
33
+
34
+ function sectionReset() {
35
+ while (indentation > 0) {
36
+ --indentation
37
+ sectionEnd()
38
+ }
39
+ }
40
+
41
+ async function section<T>(text: string, fn: () => Promise<T>): Promise<T> {
42
+ try {
43
+ sectionStart(text)
44
+ const result = await fn()
45
+ return result
46
+ } finally {
47
+ sectionEnd()
48
+ }
49
+ }
50
+
51
+ function title(text: string) {
52
+ console.log(
53
+ chalk.blue.bold(
54
+ boxen(text, { padding: { left: 1, right: 1 }, width: process.stdout.columns, borderStyle: 'double' }),
55
+ ),
56
+ )
57
+ }
58
+
59
+ function command(text: string) {
60
+ console.log(chalk.magenta(`❯ ${text}`))
61
+ }
62
+
63
+ function directory(text: string) {
64
+ console.log(chalk.magenta('◳ ' + fileLink(process.cwd(), text)))
65
+ }
66
+
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)
73
+ }
74
+
75
+ function warning(text: string) {
76
+ console.log(chalk.hex('#FFA500')(text))
77
+ }
78
+
79
+ function error(text: string) {
80
+ console.log(chalk.red(text))
81
+ }
82
+
83
+ function success(text: string) {
84
+ console.log(chalk.green(text))
85
+ }
86
+
87
+ function action(text: string) {
88
+ console.log(chalk.blue(`${text}...`))
89
+ }
90
+
91
+ function gray(text: string) {
92
+ print(chalk.gray(text))
93
+ }
94
+
95
+ function print(text: string = ''): void {
96
+ const lines = text.split('\n')
97
+ for (const line of lines) {
98
+ console.log(line)
99
+ }
100
+ }
101
+
102
+ async function markdown(content: string): Promise<void> {
103
+ // @ts-expect-error: i don't know why types are not working here but seems to work at runtime
104
+ marked.use(markedTerminal())
105
+ const output = await marked.parse(content)
106
+ console.log(output)
107
+ }
108
+
109
+ function yaml(content: any): void {
110
+ const output = YAML.stringify(content, null, 2).trim()
111
+ print(output)
112
+ }
113
+
114
+ function json(content: any): void {
115
+ const output = JSON.stringify(content, null, 2)
116
+ print(output)
117
+ }
118
+
119
+ async function image(path: string, width: number, height: number): Promise<void> {
120
+ console.log(await terminalImage.file(path, { width, height }))
121
+ }
122
+
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
+ }
129
+ }
130
+
131
+ export default {
132
+ sectionStart,
133
+ sectionEnd,
134
+ sectionReset,
135
+ section,
136
+ title,
137
+ command,
138
+ directory,
139
+ url,
140
+ link,
141
+ warning,
142
+ error,
143
+ gray,
144
+ success,
145
+ action,
146
+ print,
147
+ markdown,
148
+ yaml,
149
+ json,
150
+ image,
151
+ hyperlink: fileLink,
152
+ }
package/lib/types.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { z } from 'zod'
2
+
3
+ export const Handler = z.object({
4
+ handler: z.enum(['std', 'graphic', 'quiz']).default('std'),
5
+ solution: z.string().default('C++'),
6
+ source_modifier: z.enum(['none', 'no_main', 'structs']).default('none'),
7
+ compilers: z.enum(['RunPython', 'RunHaskell', 'RunClojure', 'GHC']).nullable().default(null),
8
+ })
9
+
10
+ export type Handler = z.infer<typeof Handler>
11
+
12
+ export const Scores = z.array(
13
+ z.object({
14
+ part: z.string(),
15
+ prefix: z.string(),
16
+ points: z.number().min(0),
17
+ }),
18
+ )
19
+
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 ADDED
@@ -0,0 +1,201 @@
1
+ import { execa } from 'execa'
2
+ import { exists, readFile, stat, writeFile } from 'fs/promises'
3
+ import humanId from 'human-id'
4
+ import { customAlphabet } from 'nanoid'
5
+ import { basename, dirname, join, normalize } from 'path'
6
+ import { fileURLToPath } from 'url'
7
+ import YAML from 'yaml'
8
+
9
+ export async function nothing(): Promise<void> {
10
+ // Do nothing: just a way to ignore lint warnings
11
+ }
12
+
13
+ export async function existsInDir(directory: string, path: string): Promise<boolean> {
14
+ return await exists(join(directory, path))
15
+ }
16
+
17
+ export async function readText(path: string): Promise<string> {
18
+ const content = await readFile(path, 'utf8')
19
+ return content
20
+ }
21
+
22
+ export async function readTextInDir(directory: string, path: string): Promise<string> {
23
+ return await readFile(join(directory, path), 'utf8')
24
+ }
25
+
26
+ export async function readBytes(path: string): Promise<Uint8Array> {
27
+ const content = await readFile(path)
28
+ return content
29
+ }
30
+
31
+ export async function readBytesInDir(directory: string, path: string): Promise<Uint8Array> {
32
+ return await readBytes(join(directory, path))
33
+ }
34
+
35
+ export async function readYaml(path: string): Promise<any> {
36
+ const content = await readText(path)
37
+ return YAML.parse(content)
38
+ }
39
+
40
+ export async function readYamlInDir(directory: string, path: string): Promise<any> {
41
+ return await readYaml(join(directory, path))
42
+ }
43
+
44
+ export async function readJson(path: string): Promise<any> {
45
+ const content = await readText(path)
46
+ return JSON.parse(content)
47
+ }
48
+
49
+ export async function readJsonInDir(directory: string, path: string): Promise<any> {
50
+ return await readJson(join(directory, path))
51
+ }
52
+
53
+ export async function writeText(path: string, content: string): Promise<void> {
54
+ await writeFile(path, content)
55
+ }
56
+
57
+ export async function writeTextInDir(directory: string, path: string, content: string): Promise<void> {
58
+ await writeText(join(directory, path), content)
59
+ }
60
+
61
+ export async function writeYaml(path: string, data: any): Promise<void> {
62
+ const content = YAML.stringify(data, null, 4)
63
+ await writeText(path, content)
64
+ }
65
+
66
+ export async function writeYamlInDir(directory: string, path: string, data: any): Promise<void> {
67
+ await writeYaml(join(directory, path), data)
68
+ }
69
+
70
+ export async function writeJson(path: string, data: any): Promise<void> {
71
+ const content = JSON.stringify(data, null, 4)
72
+ await writeText(path, content)
73
+ }
74
+
75
+ export async function writeJsonInDir(directory: string, path: string, data: any): Promise<void> {
76
+ await writeJson(join(directory, path), data)
77
+ }
78
+
79
+ export async function fileSize(path: string): Promise<number> {
80
+ const stats = await stat(path)
81
+ return stats.size
82
+ }
83
+
84
+ export async function fileSizeInDir(directory: string, path: string): Promise<number> {
85
+ return await fileSize(join(directory, path))
86
+ }
87
+
88
+ export async function filesAreEqual(path1: string, path2: string): Promise<boolean> {
89
+ const bytes1 = await readBytes(path1)
90
+ const bytes2 = await readBytes(path2)
91
+ return bytes1.length === bytes2.length && bytes1.every((b, i) => b === bytes2[i])
92
+ }
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
+
98
+ export async function isDirectory(path: string): Promise<boolean> {
99
+ try {
100
+ const stats = await stat(path)
101
+ return stats.isDirectory()
102
+ } catch (e) {
103
+ return false
104
+ }
105
+ }
106
+
107
+ export async function isDirectoryInDir(directory: string, path: string): Promise<boolean> {
108
+ return await isDirectory(join(directory, path))
109
+ }
110
+
111
+ // Guess user name from git config
112
+ export async function guessUserName(): Promise<string | null> {
113
+ try {
114
+ const { stdout } = await execa`git config user.name`
115
+ const name = stdout.trim()
116
+ if (name === '') return null
117
+ return name
118
+ } catch (e) {
119
+ return null
120
+ }
121
+ }
122
+
123
+ // Guess user email from git config
124
+ export async function guessUserEmail(): Promise<string | null> {
125
+ try {
126
+ const { stdout } = await execa`git config user.email`
127
+ const email = stdout.trim()
128
+ if (email === '') return null
129
+ return email
130
+ } catch (e) {
131
+ return null
132
+ }
133
+ }
134
+
135
+ export function nanoid16(): string {
136
+ const alphabet = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
137
+ const nanoid = customAlphabet(alphabet, 16)
138
+ return nanoid()
139
+ }
140
+
141
+ export function nanoid12(): string {
142
+ const alphabet = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
143
+ const nanoid = customAlphabet(alphabet, 12)
144
+ return nanoid()
145
+ }
146
+
147
+ export function nanoid8(): string {
148
+ const alphabet = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
149
+ const nanoid = customAlphabet(alphabet, 8)
150
+ return nanoid()
151
+ }
152
+
153
+ export function humanid(): string {
154
+ return humanId({
155
+ separator: '-',
156
+ capitalize: false,
157
+ })
158
+ }
159
+
160
+ export function projectDir(): string {
161
+ const __filename = fileURLToPath(import.meta.url)
162
+ const __dirname = dirname(__filename)
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
201
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jpetit/toolkit",
3
3
  "description": "Toolkit to prepare problems for Jutge.org",
4
- "version": "3.1.1",
4
+ "version": "3.1.2",
5
5
  "homepage": "https://jutge.org",
6
6
  "author": {
7
7
  "name": "Jutge.org",
@@ -38,7 +38,9 @@
38
38
  "update-jutge-client": "(cd lib ; rm -f jutge_api_client.ts ; https --download https://api.jutge.org/clients/download/typescript)"
39
39
  },
40
40
  "files": [
41
- "assets"
41
+ "assets",
42
+ "lib",
43
+ "toolkit"
42
44
  ],
43
45
  "dependencies": {
44
46
  "@commander-js/extra-typings": "^14.0.0",