@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.
Files changed (61) hide show
  1. package/assets/prompts/creators/create-solution.tpl.txt +10 -0
  2. package/assets/prompts/creators/create-statement.tpl.txt +21 -0
  3. package/assets/prompts/creators/create-translation.tpl.txt +5 -0
  4. package/assets/prompts/creators/private-test-cases.txt +6 -0
  5. package/assets/prompts/creators/sample-test-cases.txt +6 -0
  6. package/assets/prompts/creators/system-prompt.txt +2 -0
  7. package/assets/prompts/examples/statement-coda.tex +7 -0
  8. package/assets/prompts/examples/statement.tex +19 -0
  9. package/assets/prompts/generators/efficiency.md +41 -0
  10. package/assets/prompts/generators/hard.md +47 -0
  11. package/assets/prompts/generators/random.md +39 -0
  12. package/assets/prompts/proglangs/cc.md +3 -0
  13. package/assets/prompts/proglangs/py.md +40 -0
  14. package/lib/ai.ts +60 -4
  15. package/lib/cleaner.ts +24 -13
  16. package/lib/compilers/base.ts +70 -14
  17. package/lib/compilers/clojure.ts +21 -10
  18. package/lib/compilers/gcc.ts +4 -33
  19. package/lib/compilers/ghc.ts +4 -40
  20. package/lib/compilers/gxx.ts +4 -33
  21. package/lib/compilers/index.ts +9 -0
  22. package/lib/compilers/java.ts +105 -0
  23. package/lib/compilers/python3.ts +44 -37
  24. package/lib/compilers/run-clojure.ts +101 -0
  25. package/lib/compilers/run-haskell.ts +26 -22
  26. package/lib/compilers/run-python.ts +29 -35
  27. package/lib/compilers/rust.ts +39 -0
  28. package/lib/create-with-jutgeai.ts +407 -0
  29. package/lib/create-with-template.ts +55 -0
  30. package/lib/data.ts +6 -0
  31. package/lib/doctor.ts +86 -6
  32. package/lib/generate.ts +132 -290
  33. package/lib/helpers.ts +48 -0
  34. package/lib/inspector.ts +253 -0
  35. package/lib/jutge_api_client.ts +4631 -0
  36. package/lib/maker.ts +202 -289
  37. package/lib/settings.ts +26 -17
  38. package/lib/tui.ts +25 -15
  39. package/lib/types.ts +40 -5
  40. package/lib/upload.ts +216 -0
  41. package/lib/utils.ts +82 -14
  42. package/lib/versions.ts +46 -0
  43. package/package.json +50 -11
  44. package/toolkit/about.ts +43 -0
  45. package/toolkit/ai.ts +44 -18
  46. package/toolkit/check.ts +16 -0
  47. package/toolkit/clean.ts +16 -26
  48. package/toolkit/compilers.ts +4 -4
  49. package/toolkit/config.ts +91 -0
  50. package/toolkit/create.ts +30 -58
  51. package/toolkit/doctor.ts +15 -11
  52. package/toolkit/generate.ts +195 -98
  53. package/toolkit/index.ts +32 -21
  54. package/toolkit/make.ts +12 -48
  55. package/toolkit/upgrade.ts +9 -0
  56. package/toolkit/upload.ts +19 -0
  57. package/toolkit/create-jutge-ai.ts +0 -101
  58. package/toolkit/create-template.ts +0 -55
  59. package/toolkit/create-wizard.ts +0 -6
  60. package/toolkit/init.ts +0 -56
  61. 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 { z } from 'zod'
6
- import { readJson, writeJson } from './utils'
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.json')
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
- return await writeJson(configPath(), settings)
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 readJson(configPath())
37
- return ZSettings.parse(data)
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
- // TODO: use them anywhere
60
- type CommandOptions = {
61
- italic?: boolean
59
+ function command(text: string) {
60
+ console.log(chalk.magenta(`❯ ${text}`))
62
61
  }
63
62
 
64
- function command(text: string, options: CommandOptions = {}) {
65
- if (options.italic) {
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 directory(text: string) {
73
- console.log(chalk.magenta(' ' + hyperlink(process.cwd(), text)))
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 print(text: string): void {
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 hyperlink(dir: string, path: string): string {
121
- return terminalLink(path, 'file://' + resolve(dir, path))
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 ZHandler = z.object({
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 ZHandler>
10
+ export type Handler = z.infer<typeof Handler>
11
11
 
12
- export const ZScores = z.array(
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 ZScores>
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 { readFileSync } from 'fs'
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 concatText(
39
- inputPaths: string[],
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
  }
@@ -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
+ }