levelcode 1.1.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/README.md +69 -0
- package/index.js +505 -0
- package/package.json +42 -0
- package/postinstall.js +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# The most powerful coding agent
|
|
2
|
+
|
|
3
|
+
LevelCode is a CLI tool that writes code for you.
|
|
4
|
+
|
|
5
|
+
1. Run `levelcode` from your project directory
|
|
6
|
+
2. Tell it what to do
|
|
7
|
+
3. It will read and write to files and run commands to produce the code you want
|
|
8
|
+
|
|
9
|
+
Note: LevelCode will run commands in your terminal as it deems necessary to fulfill your request.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
To install LevelCode, run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g levelcode
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
(Use `sudo` if you get a permission error.)
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
After installation, you can start LevelCode by running:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
levelcode [project-directory]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
If no project directory is specified, LevelCode will use the current directory.
|
|
30
|
+
|
|
31
|
+
Once running, simply chat with LevelCode to say what coding task you want done.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- Understands your whole codebase
|
|
36
|
+
- Creates and edits multiple files based on your request
|
|
37
|
+
- Can run your tests or type checker or linter; can install packages
|
|
38
|
+
- It's powerful: ask LevelCode to keep working until it reaches a condition and it will.
|
|
39
|
+
|
|
40
|
+
Our users regularly use LevelCode to implement new features, write unit tests, refactor code,write scripts, or give advice.
|
|
41
|
+
|
|
42
|
+
## Knowledge Files
|
|
43
|
+
|
|
44
|
+
To unlock the full benefits of modern LLMs, we recommend storing knowledge alongside your code. Add a `knowledge.md` file anywhere in your project to provide helpful context, guidance, and tips for the LLM as it performs tasks for you.
|
|
45
|
+
|
|
46
|
+
LevelCode can fluently read and write files, so it will add knowledge as it goes. You don't need to write knowledge manually!
|
|
47
|
+
|
|
48
|
+
Some have said every change should be paired with a unit test. In 2024, every change should come with a knowledge update!
|
|
49
|
+
|
|
50
|
+
## Tips
|
|
51
|
+
|
|
52
|
+
1. Type '/help' or just '/' to see available commands.
|
|
53
|
+
2. Create a `knowledge.md` file and collect specific points of advice. The assistant will use this knowledge to improve its responses.
|
|
54
|
+
3. Type `undo` or `redo` to revert or reapply file changes from the conversation.
|
|
55
|
+
4. Press `Esc` or `Ctrl+C` while LevelCode is generating a response to stop it.
|
|
56
|
+
|
|
57
|
+
## Troubleshooting
|
|
58
|
+
|
|
59
|
+
If you are getting permission errors during installation, try using sudo:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
sudo npm install -g levelcode
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
If you still have errors, it's a good idea to [reinstall Node](https://nodejs.org/en/download).
|
|
66
|
+
|
|
67
|
+
## Feedback
|
|
68
|
+
|
|
69
|
+
We value your input! Please email your feedback to `founders@levelcode.vercel.app`. Thank you for using LevelCode!
|
package/index.js
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process')
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const http = require('http')
|
|
6
|
+
const https = require('https')
|
|
7
|
+
const os = require('os')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const zlib = require('zlib')
|
|
10
|
+
|
|
11
|
+
const tar = require('tar')
|
|
12
|
+
|
|
13
|
+
const packageName = 'levelcode'
|
|
14
|
+
|
|
15
|
+
function createConfig(packageName) {
|
|
16
|
+
const homeDir = os.homedir()
|
|
17
|
+
const configDir = path.join(homeDir, '.config', 'levelcode')
|
|
18
|
+
const legacyConfigDir = path.join(homeDir, '.config', 'manicode')
|
|
19
|
+
const binaryName =
|
|
20
|
+
process.platform === 'win32' ? `${packageName}.exe` : packageName
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
homeDir,
|
|
24
|
+
configDir,
|
|
25
|
+
legacyConfigDir,
|
|
26
|
+
binaryName,
|
|
27
|
+
binaryPath: path.join(configDir, binaryName),
|
|
28
|
+
metadataPath: path.join(configDir, 'levelcode-metadata.json'),
|
|
29
|
+
tempDownloadDir: path.join(configDir, '.download-temp'),
|
|
30
|
+
userAgent: `${packageName}-cli`,
|
|
31
|
+
requestTimeout: 20000,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const CONFIG = createConfig(packageName)
|
|
36
|
+
|
|
37
|
+
// Migrate credentials from legacy manicode config dir if needed
|
|
38
|
+
function migrateFromLegacyConfig() {
|
|
39
|
+
try {
|
|
40
|
+
const newCreds = path.join(CONFIG.configDir, 'credentials.json')
|
|
41
|
+
if (fs.existsSync(newCreds)) return
|
|
42
|
+
const legacyCreds = path.join(CONFIG.legacyConfigDir, 'credentials.json')
|
|
43
|
+
if (!fs.existsSync(legacyCreds)) return
|
|
44
|
+
fs.mkdirSync(CONFIG.configDir, { recursive: true })
|
|
45
|
+
fs.copyFileSync(legacyCreds, newCreds)
|
|
46
|
+
// Also migrate metadata if it exists
|
|
47
|
+
const legacyMeta = path.join(CONFIG.legacyConfigDir, 'manicode-metadata.json')
|
|
48
|
+
if (fs.existsSync(legacyMeta)) {
|
|
49
|
+
fs.copyFileSync(legacyMeta, CONFIG.metadataPath)
|
|
50
|
+
}
|
|
51
|
+
} catch { /* silently ignore migration errors */ }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
migrateFromLegacyConfig()
|
|
55
|
+
|
|
56
|
+
function getPostHogConfig() {
|
|
57
|
+
const apiKey =
|
|
58
|
+
process.env.LEVELCODE_POSTHOG_API_KEY ||
|
|
59
|
+
process.env.NEXT_PUBLIC_POSTHOG_API_KEY
|
|
60
|
+
const host =
|
|
61
|
+
process.env.LEVELCODE_POSTHOG_HOST ||
|
|
62
|
+
process.env.NEXT_PUBLIC_POSTHOG_HOST_URL
|
|
63
|
+
|
|
64
|
+
if (!apiKey || !host) {
|
|
65
|
+
return null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { apiKey, host }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Track update failure event to PostHog.
|
|
73
|
+
* Fire-and-forget - errors are silently ignored.
|
|
74
|
+
*/
|
|
75
|
+
function trackUpdateFailed(errorMessage, version, context = {}) {
|
|
76
|
+
try {
|
|
77
|
+
const posthogConfig = getPostHogConfig()
|
|
78
|
+
if (!posthogConfig) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const payload = JSON.stringify({
|
|
83
|
+
api_key: posthogConfig.apiKey,
|
|
84
|
+
event: 'cli.update_levelcode_failed',
|
|
85
|
+
properties: {
|
|
86
|
+
distinct_id: `anonymous-${CONFIG.homeDir}`,
|
|
87
|
+
error: errorMessage,
|
|
88
|
+
version: version || 'unknown',
|
|
89
|
+
platform: process.platform,
|
|
90
|
+
arch: process.arch,
|
|
91
|
+
...context,
|
|
92
|
+
},
|
|
93
|
+
timestamp: new Date().toISOString(),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
const parsedUrl = new URL(`${posthogConfig.host}/capture/`)
|
|
97
|
+
const isHttps = parsedUrl.protocol === 'https:'
|
|
98
|
+
const options = {
|
|
99
|
+
hostname: parsedUrl.hostname,
|
|
100
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
101
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: {
|
|
104
|
+
'Content-Type': 'application/json',
|
|
105
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const transport = isHttps ? https : http
|
|
110
|
+
const req = transport.request(options)
|
|
111
|
+
req.on('error', () => {}) // Silently ignore errors
|
|
112
|
+
req.write(payload)
|
|
113
|
+
req.end()
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// Silently ignore any tracking errors
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const PLATFORM_TARGETS = {
|
|
120
|
+
'linux-x64': `${packageName}-linux-x64.tar.gz`,
|
|
121
|
+
'linux-arm64': `${packageName}-linux-arm64.tar.gz`,
|
|
122
|
+
'darwin-x64': `${packageName}-darwin-x64.tar.gz`,
|
|
123
|
+
'darwin-arm64': `${packageName}-darwin-arm64.tar.gz`,
|
|
124
|
+
'win32-x64': `${packageName}-win32-x64.tar.gz`,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const term = {
|
|
128
|
+
clearLine: () => {
|
|
129
|
+
if (process.stderr.isTTY) {
|
|
130
|
+
process.stderr.write('\r\x1b[K')
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
write: (text) => {
|
|
134
|
+
term.clearLine()
|
|
135
|
+
process.stderr.write(text)
|
|
136
|
+
},
|
|
137
|
+
writeLine: (text) => {
|
|
138
|
+
term.clearLine()
|
|
139
|
+
process.stderr.write(text + '\n')
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function httpGet(url, options = {}) {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
const parsedUrl = new URL(url)
|
|
146
|
+
const reqOptions = {
|
|
147
|
+
hostname: parsedUrl.hostname,
|
|
148
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
149
|
+
headers: {
|
|
150
|
+
'User-Agent': CONFIG.userAgent,
|
|
151
|
+
...options.headers,
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const req = https.get(reqOptions, (res) => {
|
|
156
|
+
if (res.statusCode === 302 || res.statusCode === 301) {
|
|
157
|
+
return httpGet(new URL(res.headers.location, url).href, options)
|
|
158
|
+
.then(resolve)
|
|
159
|
+
.catch(reject)
|
|
160
|
+
}
|
|
161
|
+
resolve(res)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
req.on('error', reject)
|
|
165
|
+
|
|
166
|
+
const timeout = options.timeout || CONFIG.requestTimeout
|
|
167
|
+
req.setTimeout(timeout, () => {
|
|
168
|
+
req.destroy()
|
|
169
|
+
reject(new Error('Request timeout.'))
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function getLatestVersion() {
|
|
175
|
+
try {
|
|
176
|
+
const res = await httpGet(
|
|
177
|
+
`https://registry.npmjs.org/${packageName}/latest`,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if (res.statusCode !== 200) return null
|
|
181
|
+
|
|
182
|
+
const body = await streamToString(res)
|
|
183
|
+
const packageData = JSON.parse(body)
|
|
184
|
+
|
|
185
|
+
return packageData.version || null
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function streamToString(stream) {
|
|
192
|
+
return new Promise((resolve, reject) => {
|
|
193
|
+
let data = ''
|
|
194
|
+
stream.on('data', (chunk) => (data += chunk))
|
|
195
|
+
stream.on('end', () => resolve(data))
|
|
196
|
+
stream.on('error', reject)
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getCurrentVersion() {
|
|
201
|
+
try {
|
|
202
|
+
if (!fs.existsSync(CONFIG.metadataPath)) {
|
|
203
|
+
return null
|
|
204
|
+
}
|
|
205
|
+
const metadata = JSON.parse(fs.readFileSync(CONFIG.metadataPath, 'utf8'))
|
|
206
|
+
// Also verify the binary still exists
|
|
207
|
+
if (!fs.existsSync(CONFIG.binaryPath)) {
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
return metadata.version || null
|
|
211
|
+
} catch (error) {
|
|
212
|
+
return null
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function compareVersions(v1, v2) {
|
|
217
|
+
if (!v1 || !v2) return 0
|
|
218
|
+
|
|
219
|
+
// Always update if the current version is not a valid semver
|
|
220
|
+
// e.g. 1.0.420-beta.1
|
|
221
|
+
if (!v1.match(/^\d+(\.\d+)*$/)) {
|
|
222
|
+
return -1
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const parseVersion = (version) => {
|
|
226
|
+
const parts = version.split('-')
|
|
227
|
+
const mainParts = parts[0].split('.').map(Number)
|
|
228
|
+
const prereleaseParts = parts[1] ? parts[1].split('.') : []
|
|
229
|
+
return { main: mainParts, prerelease: prereleaseParts }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const p1 = parseVersion(v1)
|
|
233
|
+
const p2 = parseVersion(v2)
|
|
234
|
+
|
|
235
|
+
for (let i = 0; i < Math.max(p1.main.length, p2.main.length); i++) {
|
|
236
|
+
const n1 = p1.main[i] || 0
|
|
237
|
+
const n2 = p2.main[i] || 0
|
|
238
|
+
|
|
239
|
+
if (n1 < n2) return -1
|
|
240
|
+
if (n1 > n2) return 1
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (p1.prerelease.length === 0 && p2.prerelease.length === 0) {
|
|
244
|
+
return 0
|
|
245
|
+
} else if (p1.prerelease.length === 0) {
|
|
246
|
+
return 1
|
|
247
|
+
} else if (p2.prerelease.length === 0) {
|
|
248
|
+
return -1
|
|
249
|
+
} else {
|
|
250
|
+
for (
|
|
251
|
+
let i = 0;
|
|
252
|
+
i < Math.max(p1.prerelease.length, p2.prerelease.length);
|
|
253
|
+
i++
|
|
254
|
+
) {
|
|
255
|
+
const pr1 = p1.prerelease[i] || ''
|
|
256
|
+
const pr2 = p2.prerelease[i] || ''
|
|
257
|
+
|
|
258
|
+
const isNum1 = !isNaN(parseInt(pr1))
|
|
259
|
+
const isNum2 = !isNaN(parseInt(pr2))
|
|
260
|
+
|
|
261
|
+
if (isNum1 && isNum2) {
|
|
262
|
+
const num1 = parseInt(pr1)
|
|
263
|
+
const num2 = parseInt(pr2)
|
|
264
|
+
if (num1 < num2) return -1
|
|
265
|
+
if (num1 > num2) return 1
|
|
266
|
+
} else if (isNum1 && !isNum2) {
|
|
267
|
+
return 1
|
|
268
|
+
} else if (!isNum1 && isNum2) {
|
|
269
|
+
return -1
|
|
270
|
+
} else if (pr1 < pr2) {
|
|
271
|
+
return -1
|
|
272
|
+
} else if (pr1 > pr2) {
|
|
273
|
+
return 1
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return 0
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function formatBytes(bytes) {
|
|
281
|
+
if (bytes === 0) return '0 B'
|
|
282
|
+
const k = 1024
|
|
283
|
+
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
284
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
285
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function createProgressBar(percentage, width = 30) {
|
|
289
|
+
const filled = Math.round((width * percentage) / 100)
|
|
290
|
+
const empty = width - filled
|
|
291
|
+
return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']'
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function downloadBinary(version) {
|
|
295
|
+
const platformKey = `${process.platform}-${process.arch}`
|
|
296
|
+
const fileName = PLATFORM_TARGETS[platformKey]
|
|
297
|
+
|
|
298
|
+
if (!fileName) {
|
|
299
|
+
const error = new Error(`Unsupported platform: ${process.platform} ${process.arch}`)
|
|
300
|
+
trackUpdateFailed(error.message, version, { stage: 'platform_check' })
|
|
301
|
+
throw error
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const downloadUrl = `${
|
|
305
|
+
process.env.NEXT_PUBLIC_LEVELCODE_APP_URL || 'https://levelcode.vercel.app'
|
|
306
|
+
}/api/releases/download/${version}/${fileName}`
|
|
307
|
+
|
|
308
|
+
// Ensure config directory exists
|
|
309
|
+
fs.mkdirSync(CONFIG.configDir, { recursive: true })
|
|
310
|
+
|
|
311
|
+
// Clean up any previous temp download directory
|
|
312
|
+
if (fs.existsSync(CONFIG.tempDownloadDir)) {
|
|
313
|
+
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
|
|
314
|
+
}
|
|
315
|
+
fs.mkdirSync(CONFIG.tempDownloadDir, { recursive: true })
|
|
316
|
+
|
|
317
|
+
term.write('Downloading...')
|
|
318
|
+
|
|
319
|
+
const res = await httpGet(downloadUrl)
|
|
320
|
+
|
|
321
|
+
if (res.statusCode !== 200) {
|
|
322
|
+
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
|
|
323
|
+
const error = new Error(`Download failed: HTTP ${res.statusCode}`)
|
|
324
|
+
trackUpdateFailed(error.message, version, { stage: 'http_download', statusCode: res.statusCode })
|
|
325
|
+
throw error
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const totalSize = parseInt(res.headers['content-length'] || '0', 10)
|
|
329
|
+
let downloadedSize = 0
|
|
330
|
+
let lastProgressTime = Date.now()
|
|
331
|
+
|
|
332
|
+
res.on('data', (chunk) => {
|
|
333
|
+
downloadedSize += chunk.length
|
|
334
|
+
const now = Date.now()
|
|
335
|
+
if (now - lastProgressTime >= 100 || downloadedSize === totalSize) {
|
|
336
|
+
lastProgressTime = now
|
|
337
|
+
if (totalSize > 0) {
|
|
338
|
+
const pct = Math.round((downloadedSize / totalSize) * 100)
|
|
339
|
+
term.write(
|
|
340
|
+
`Downloading... ${createProgressBar(pct)} ${pct}% of ${formatBytes(
|
|
341
|
+
totalSize,
|
|
342
|
+
)}`,
|
|
343
|
+
)
|
|
344
|
+
} else {
|
|
345
|
+
term.write(`Downloading... ${formatBytes(downloadedSize)}`)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
// Extract to temp directory
|
|
351
|
+
await new Promise((resolve, reject) => {
|
|
352
|
+
res
|
|
353
|
+
.pipe(zlib.createGunzip())
|
|
354
|
+
.pipe(tar.x({ cwd: CONFIG.tempDownloadDir }))
|
|
355
|
+
.on('finish', resolve)
|
|
356
|
+
.on('error', reject)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
const tempBinaryPath = path.join(CONFIG.tempDownloadDir, CONFIG.binaryName)
|
|
360
|
+
|
|
361
|
+
// Verify the binary was extracted
|
|
362
|
+
if (!fs.existsSync(tempBinaryPath)) {
|
|
363
|
+
const files = fs.readdirSync(CONFIG.tempDownloadDir)
|
|
364
|
+
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
|
|
365
|
+
const error = new Error(
|
|
366
|
+
`Binary not found after extraction. Expected: ${CONFIG.binaryName}, Available files: ${files.join(', ')}`,
|
|
367
|
+
)
|
|
368
|
+
trackUpdateFailed(error.message, version, { stage: 'extraction' })
|
|
369
|
+
throw error
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Set executable permissions
|
|
373
|
+
if (process.platform !== 'win32') {
|
|
374
|
+
fs.chmodSync(tempBinaryPath, 0o755)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Move binary to final location
|
|
378
|
+
try {
|
|
379
|
+
if (fs.existsSync(CONFIG.binaryPath)) {
|
|
380
|
+
try {
|
|
381
|
+
fs.unlinkSync(CONFIG.binaryPath)
|
|
382
|
+
} catch (err) {
|
|
383
|
+
// Fallback: try renaming the locked/undeletable binary (Windows)
|
|
384
|
+
const backupPath = CONFIG.binaryPath + `.old.${Date.now()}`
|
|
385
|
+
try {
|
|
386
|
+
fs.renameSync(CONFIG.binaryPath, backupPath)
|
|
387
|
+
} catch (renameErr) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
`Failed to replace existing binary. ` +
|
|
390
|
+
`unlink error: ${err.code || err.message}, ` +
|
|
391
|
+
`rename error: ${renameErr.code || renameErr.message}`,
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
fs.renameSync(tempBinaryPath, CONFIG.binaryPath)
|
|
397
|
+
|
|
398
|
+
// Save version metadata for fast version checking
|
|
399
|
+
fs.writeFileSync(
|
|
400
|
+
CONFIG.metadataPath,
|
|
401
|
+
JSON.stringify({ version }, null, 2),
|
|
402
|
+
)
|
|
403
|
+
} finally {
|
|
404
|
+
// Clean up temp directory even if rename fails
|
|
405
|
+
if (fs.existsSync(CONFIG.tempDownloadDir)) {
|
|
406
|
+
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
term.clearLine()
|
|
411
|
+
console.log('Download complete! Starting LevelCode...')
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function ensureBinaryExists() {
|
|
415
|
+
const currentVersion = getCurrentVersion()
|
|
416
|
+
if (currentVersion !== null) {
|
|
417
|
+
return
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const version = await getLatestVersion()
|
|
421
|
+
if (!version) {
|
|
422
|
+
console.error('❌ Failed to determine latest version')
|
|
423
|
+
console.error('Please check your internet connection and try again')
|
|
424
|
+
process.exit(1)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
await downloadBinary(version)
|
|
429
|
+
} catch (error) {
|
|
430
|
+
term.clearLine()
|
|
431
|
+
console.error('❌ Failed to download levelcode:', error.message)
|
|
432
|
+
console.error('Please check your internet connection and try again')
|
|
433
|
+
process.exit(1)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function checkForUpdates(runningProcess, exitListener) {
|
|
438
|
+
try {
|
|
439
|
+
const currentVersion = getCurrentVersion()
|
|
440
|
+
|
|
441
|
+
const latestVersion = await getLatestVersion()
|
|
442
|
+
if (!latestVersion) return
|
|
443
|
+
|
|
444
|
+
if (
|
|
445
|
+
// Download new version if current version is unknown or outdated.
|
|
446
|
+
currentVersion === null ||
|
|
447
|
+
compareVersions(currentVersion, latestVersion) < 0
|
|
448
|
+
) {
|
|
449
|
+
term.clearLine()
|
|
450
|
+
|
|
451
|
+
runningProcess.removeListener('exit', exitListener)
|
|
452
|
+
runningProcess.kill('SIGTERM')
|
|
453
|
+
|
|
454
|
+
await new Promise((resolve) => {
|
|
455
|
+
runningProcess.on('exit', resolve)
|
|
456
|
+
setTimeout(() => {
|
|
457
|
+
if (!runningProcess.killed) {
|
|
458
|
+
runningProcess.kill('SIGKILL')
|
|
459
|
+
}
|
|
460
|
+
resolve()
|
|
461
|
+
}, 5000)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
console.log(`Update available: ${currentVersion} → ${latestVersion}`)
|
|
465
|
+
|
|
466
|
+
await downloadBinary(latestVersion)
|
|
467
|
+
|
|
468
|
+
const newChild = spawn(CONFIG.binaryPath, process.argv.slice(2), {
|
|
469
|
+
stdio: 'inherit',
|
|
470
|
+
detached: false,
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
newChild.on('exit', (code) => {
|
|
474
|
+
process.exit(code || 0)
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
return new Promise(() => {})
|
|
478
|
+
}
|
|
479
|
+
} catch (error) {
|
|
480
|
+
// Ignore update failures
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async function main() {
|
|
485
|
+
await ensureBinaryExists()
|
|
486
|
+
|
|
487
|
+
const child = spawn(CONFIG.binaryPath, process.argv.slice(2), {
|
|
488
|
+
stdio: 'inherit',
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
const exitListener = (code) => {
|
|
492
|
+
process.exit(code || 0)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
child.on('exit', exitListener)
|
|
496
|
+
|
|
497
|
+
setTimeout(() => {
|
|
498
|
+
checkForUpdates(child, exitListener)
|
|
499
|
+
}, 100)
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
main().catch((error) => {
|
|
503
|
+
console.error('❌ Unexpected error:', error.message)
|
|
504
|
+
process.exit(1)
|
|
505
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "levelcode",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "AI coding agent",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"levelcode": "./index.js",
|
|
8
|
+
"cb": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"postinstall": "node postinstall.js",
|
|
12
|
+
"preuninstall": "node -e \"const fs = require('fs'); const path = require('path'); const os = require('os'); const binaryPath = path.join(os.homedir(), '.config', 'levelcode', process.platform === 'win32' ? 'levelcode.exe' : 'levelcode'); try { fs.unlinkSync(binaryPath) } catch (e) { /* ignore if file doesn't exist */ }\""
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.js",
|
|
16
|
+
"postinstall.js",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"os": [
|
|
20
|
+
"darwin",
|
|
21
|
+
"linux",
|
|
22
|
+
"win32"
|
|
23
|
+
],
|
|
24
|
+
"cpu": [
|
|
25
|
+
"x64",
|
|
26
|
+
"arm64"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=16"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"tar": "^7.0.0"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/LevelCodeAI/levelcode.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://levelcode.vercel.app",
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
// Clean up old binary
|
|
8
|
+
const binaryPath = path.join(
|
|
9
|
+
os.homedir(),
|
|
10
|
+
'.config',
|
|
11
|
+
'levelcode',
|
|
12
|
+
process.platform === 'win32' ? 'levelcode.exe' : 'levelcode'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
fs.unlinkSync(binaryPath);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
/* ignore if file doesn't exist */
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Print welcome message
|
|
22
|
+
console.log('\n');
|
|
23
|
+
console.log('🎉 Welcome to LevelCode!');
|
|
24
|
+
console.log('\n');
|
|
25
|
+
console.log('To get started:');
|
|
26
|
+
console.log(' 1. cd to your project directory');
|
|
27
|
+
console.log(' 2. Run: levelcode');
|
|
28
|
+
console.log('\n');
|
|
29
|
+
console.log('Example:');
|
|
30
|
+
console.log(' $ cd ~/my-project');
|
|
31
|
+
console.log(' $ levelcode');
|
|
32
|
+
console.log('\n');
|
|
33
|
+
console.log('For more information, visit: https://levelcode.vercel.app/docs');
|
|
34
|
+
console.log('\n');
|