@exreve/exk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -0
- package/agentLogger.ts +162 -0
- package/agentSession.ts +1176 -0
- package/app-child.ts +2769 -0
- package/appManager.ts +275 -0
- package/appRunner.ts +475 -0
- package/bin/exk +45 -0
- package/container-entrypoint.sh +177 -0
- package/index.ts +2798 -0
- package/install-service.sh +122 -0
- package/moduleMcpServer.ts +131 -0
- package/package.json +67 -0
- package/projectAnalyzer.ts +341 -0
- package/projectManager.ts +111 -0
- package/runnerGenerator.ts +218 -0
- package/shared/types.ts +488 -0
- package/skills/code-review.md +49 -0
- package/skills/front-glass.md +36 -0
- package/skills/frontend-design.md +41 -0
- package/skills/index.ts +151 -0
- package/tsconfig.json +22 -0
- package/updater.ts +512 -0
package/skills/index.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, existsSync } from 'fs'
|
|
2
|
+
import * as path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
import { dirname } from 'path'
|
|
5
|
+
|
|
6
|
+
export interface Skill {
|
|
7
|
+
name: string
|
|
8
|
+
description: string
|
|
9
|
+
content: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const SKILLS_DIR = dirname(fileURLToPath(import.meta.url)) // This file is in the skills directory
|
|
13
|
+
|
|
14
|
+
const skillCache = new Map<string, Skill>()
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parse frontmatter and content from a skill markdown file
|
|
18
|
+
*/
|
|
19
|
+
function parseSkillFile(content: string): { name: string; description: string; content: string } | null {
|
|
20
|
+
// Check for YAML frontmatter between --- markers
|
|
21
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/
|
|
22
|
+
const match = content.match(frontmatterRegex)
|
|
23
|
+
|
|
24
|
+
if (!match) {
|
|
25
|
+
// No frontmatter, use filename as name
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const frontmatter = match[1]
|
|
30
|
+
const skillContent = match[2].trim()
|
|
31
|
+
|
|
32
|
+
// Parse name and description from frontmatter
|
|
33
|
+
const nameMatch = frontmatter.match(/name:\s*(.+)/)
|
|
34
|
+
const descriptionMatch = frontmatter.match(/description:\s*(.+)/)
|
|
35
|
+
|
|
36
|
+
const name = nameMatch ? nameMatch[1].trim() : ''
|
|
37
|
+
const description = descriptionMatch ? descriptionMatch[1].trim() : ''
|
|
38
|
+
|
|
39
|
+
if (!name) {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { name, description, content: skillContent }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Load a single skill by name
|
|
48
|
+
*/
|
|
49
|
+
export function loadSkill(name: string): Skill | null {
|
|
50
|
+
// Check cache first
|
|
51
|
+
if (skillCache.has(name)) {
|
|
52
|
+
return skillCache.get(name)!
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const skillPath = path.join(SKILLS_DIR, `${name}.md`)
|
|
56
|
+
|
|
57
|
+
if (!existsSync(skillPath)) {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const content = readFileSync(skillPath, 'utf-8')
|
|
63
|
+
const parsed = parseSkillFile(content)
|
|
64
|
+
|
|
65
|
+
if (!parsed) {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const skill: Skill = {
|
|
70
|
+
name: parsed.name,
|
|
71
|
+
description: parsed.description,
|
|
72
|
+
content: parsed.content
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
skillCache.set(name, skill)
|
|
76
|
+
return skill
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error(`Failed to load skill ${name}:`, error)
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Load all available skills from the skills directory
|
|
85
|
+
*/
|
|
86
|
+
export function loadAllSkills(): Skill[] {
|
|
87
|
+
const skills: Skill[] = []
|
|
88
|
+
|
|
89
|
+
if (!existsSync(SKILLS_DIR)) {
|
|
90
|
+
return skills
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const files = readdirSync(SKILLS_DIR)
|
|
95
|
+
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
if (!file.endsWith('.md')) {
|
|
98
|
+
continue
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const name = file.replace('.md', '')
|
|
102
|
+
const skill = loadSkill(name)
|
|
103
|
+
|
|
104
|
+
if (skill) {
|
|
105
|
+
skills.push(skill)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('Failed to load skills:', error)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return skills
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get skill content for injection into prompts
|
|
117
|
+
*/
|
|
118
|
+
export function getSkillContent(names: string[]): string {
|
|
119
|
+
if (names.length === 0) {
|
|
120
|
+
return ''
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const contents: string[] = []
|
|
124
|
+
|
|
125
|
+
for (const name of names) {
|
|
126
|
+
const skill = loadSkill(name)
|
|
127
|
+
if (skill) {
|
|
128
|
+
contents.push(`# Skill: ${skill.name}\n\n${skill.content}`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (contents.length === 0) {
|
|
133
|
+
return ''
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return `## Active Skills\n\n${contents.join('\n\n---\n\n')}\n\n---\n\n`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* List all available skill names
|
|
141
|
+
*/
|
|
142
|
+
export function listSkillNames(): string[] {
|
|
143
|
+
return loadAllSkills().map(s => s.name)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get skill metadata (name, description) for all skills
|
|
148
|
+
*/
|
|
149
|
+
export function getSkillMetadata(): Array<{ name: string; description: string }> {
|
|
150
|
+
return loadAllSkills().map(s => ({ name: s.name, description: s.description }))
|
|
151
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ES2022",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"allowSyntheticDefaultImports": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true,
|
|
17
|
+
"types": ["node"],
|
|
18
|
+
"downlevelIteration": true
|
|
19
|
+
},
|
|
20
|
+
"include": ["*.ts", "shared/**/*.ts"],
|
|
21
|
+
"exclude": ["node_modules", "dist"]
|
|
22
|
+
}
|
package/updater.ts
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* TalkToCode CLI Updater - Standalone Update Process
|
|
4
|
+
*
|
|
5
|
+
* This is a minimal, robust updater that:
|
|
6
|
+
* 1. Runs as the main process
|
|
7
|
+
* 2. Spawns the app as a child process
|
|
8
|
+
* 3. Can update itself even if app is corrupted
|
|
9
|
+
* 4. Can recover from failed updates
|
|
10
|
+
*
|
|
11
|
+
* Architecture:
|
|
12
|
+
* - updater.ts (this file) -> Main process, handles updates
|
|
13
|
+
* - app-child.ts -> Child process, contains all app logic
|
|
14
|
+
*
|
|
15
|
+
* File structure:
|
|
16
|
+
* - ttc (updater) -> Main entry point
|
|
17
|
+
* - ttc-app.js -> Child bundle (updated via updates)
|
|
18
|
+
* - ttc-app.js.old -> Backup of last version
|
|
19
|
+
* - ttc-app.js.backup -> Secondary backup
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawn, ChildProcess } from 'child_process'
|
|
23
|
+
import fs from 'fs/promises'
|
|
24
|
+
import fsSync from 'fs'
|
|
25
|
+
import path from 'path'
|
|
26
|
+
import os from 'os'
|
|
27
|
+
import crypto from 'crypto'
|
|
28
|
+
import { fileURLToPath } from 'url'
|
|
29
|
+
|
|
30
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
31
|
+
const CONFIG_DIR = path.join(os.homedir(), '.talk-to-code')
|
|
32
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
|
|
33
|
+
const DEVICE_ID_FILE = path.join(CONFIG_DIR, 'device-id.json')
|
|
34
|
+
|
|
35
|
+
// File paths
|
|
36
|
+
const UPDATER_FILE = fileURLToPath(import.meta.url)
|
|
37
|
+
const APP_BUNDLE = path.join(__dirname, 'dist', 'app-child.js')
|
|
38
|
+
const APP_BUNDLE_BACKUP = APP_BUNDLE + '.backup'
|
|
39
|
+
const APP_BUNDLE_OLD = APP_BUNDLE + '.old'
|
|
40
|
+
const BUNDLE_HASHES_FILE = path.join(__dirname, 'binary-hashes.json')
|
|
41
|
+
const UPDATE_LOCK_FILE = path.join(CONFIG_DIR, '.update-lock')
|
|
42
|
+
|
|
43
|
+
// State
|
|
44
|
+
let childProcess: ChildProcess | null = null
|
|
45
|
+
let isUpdating = false
|
|
46
|
+
let restartRequested = false
|
|
47
|
+
|
|
48
|
+
// ============ Configuration ============
|
|
49
|
+
|
|
50
|
+
interface Config {
|
|
51
|
+
apiUrl: string
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function readConfig(): Promise<Config> {
|
|
55
|
+
try {
|
|
56
|
+
const data = await fs.readFile(CONFIG_FILE, 'utf-8')
|
|
57
|
+
return JSON.parse(data)
|
|
58
|
+
} catch {
|
|
59
|
+
return { apiUrl: 'https://api.talk-to-code.com' }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============ Hash Utilities ============
|
|
64
|
+
|
|
65
|
+
function calculateHash(filePath: string): string {
|
|
66
|
+
try {
|
|
67
|
+
const fileData = fsSync.readFileSync(filePath)
|
|
68
|
+
return crypto.createHash('sha256').update(fileData).digest('hex')
|
|
69
|
+
} catch {
|
|
70
|
+
return ''
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function getBundleHashes(): Promise<Record<string, any>> {
|
|
75
|
+
try {
|
|
76
|
+
const data = await fs.readFile(BUNDLE_HASHES_FILE, 'utf-8')
|
|
77
|
+
return JSON.parse(data)
|
|
78
|
+
} catch {
|
|
79
|
+
return {}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============ Update System ============
|
|
84
|
+
|
|
85
|
+
interface UpdateInfo {
|
|
86
|
+
updateAvailable: boolean
|
|
87
|
+
downloadUrl?: string
|
|
88
|
+
hash?: string
|
|
89
|
+
version?: string
|
|
90
|
+
changelog?: string
|
|
91
|
+
size?: number
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function checkForUpdates(): Promise<UpdateInfo | null> {
|
|
95
|
+
try {
|
|
96
|
+
const config = await readConfig()
|
|
97
|
+
const currentHash = calculateHash(APP_BUNDLE)
|
|
98
|
+
|
|
99
|
+
if (!currentHash) {
|
|
100
|
+
console.warn('ā ļø Cannot calculate current bundle hash')
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const response = await fetch(`${config.apiUrl}/update/check`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
hash: currentHash,
|
|
109
|
+
platform: os.platform(),
|
|
110
|
+
arch: os.arch()
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
console.warn(`ā ļø Update check failed: HTTP ${response.status}`)
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const info = await response.json() as UpdateInfo
|
|
120
|
+
return info
|
|
121
|
+
} catch (error: any) {
|
|
122
|
+
console.warn(`ā ļø Update check failed: ${error.message}`)
|
|
123
|
+
return null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function downloadUpdate(downloadUrl: string, expectedHash: string): Promise<Buffer> {
|
|
128
|
+
console.log('š„ Downloading update...')
|
|
129
|
+
|
|
130
|
+
const response = await fetch(downloadUrl)
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
throw new Error(`Download failed: HTTP ${response.status}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const buffer = Buffer.from(await response.arrayBuffer())
|
|
136
|
+
const actualHash = crypto.createHash('sha256').update(buffer).digest('hex')
|
|
137
|
+
|
|
138
|
+
if (actualHash !== expectedHash) {
|
|
139
|
+
throw new Error(`Hash mismatch: expected ${expectedHash}, got ${actualHash}`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log('ā Download verified')
|
|
143
|
+
return buffer
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function applyUpdate(newBundle: Buffer): Promise<void> {
|
|
147
|
+
console.log('š Applying update...')
|
|
148
|
+
|
|
149
|
+
// Create backup of current version if it exists
|
|
150
|
+
if (fsSync.existsSync(APP_BUNDLE)) {
|
|
151
|
+
try {
|
|
152
|
+
// Rotate backups: .old -> .backup (if exists), current -> .old
|
|
153
|
+
if (fsSync.existsSync(APP_BUNDLE_OLD)) {
|
|
154
|
+
await fs.copyFile(APP_BUNDLE_OLD, APP_BUNDLE_BACKUP)
|
|
155
|
+
}
|
|
156
|
+
await fs.copyFile(APP_BUNDLE, APP_BUNDLE_OLD)
|
|
157
|
+
console.log('ā Backup created')
|
|
158
|
+
} catch (error: any) {
|
|
159
|
+
console.warn(`ā ļø Backup creation failed: ${error.message}`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Write new bundle
|
|
164
|
+
const tempPath = path.join(os.tmpdir(), `ttc-update-${Date.now()}.js`)
|
|
165
|
+
await fs.writeFile(tempPath, newBundle, { mode: 0o755 })
|
|
166
|
+
|
|
167
|
+
// Replace current bundle
|
|
168
|
+
await fs.rename(tempPath, APP_BUNDLE)
|
|
169
|
+
|
|
170
|
+
// Make executable
|
|
171
|
+
if (process.platform !== 'win32') {
|
|
172
|
+
await fs.chmod(APP_BUNDLE, 0o755)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log('ā Update applied')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function performUpdate(info: UpdateInfo): Promise<boolean> {
|
|
179
|
+
if (!info.downloadUrl || !info.hash) {
|
|
180
|
+
console.error('ā Invalid update info')
|
|
181
|
+
return false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const newBundle = await downloadUpdate(info.downloadUrl, info.hash)
|
|
186
|
+
await applyUpdate(newBundle)
|
|
187
|
+
|
|
188
|
+
if (info.version) {
|
|
189
|
+
console.log(`ā Updated to version ${info.version}`)
|
|
190
|
+
}
|
|
191
|
+
if (info.changelog) {
|
|
192
|
+
console.log(`\nš Changelog:\n${info.changelog}`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return true
|
|
196
|
+
} catch (error: any) {
|
|
197
|
+
console.error(`ā Update failed: ${error.message}`)
|
|
198
|
+
|
|
199
|
+
// Attempt rollback
|
|
200
|
+
if (fsSync.existsSync(APP_BUNDLE_OLD)) {
|
|
201
|
+
console.log('š Rolling back to previous version...')
|
|
202
|
+
try {
|
|
203
|
+
await fs.copyFile(APP_BUNDLE_OLD, APP_BUNDLE)
|
|
204
|
+
console.log('ā Rollback complete')
|
|
205
|
+
} catch (rollbackError: any) {
|
|
206
|
+
console.error(`ā Rollback failed: ${rollbackError.message}`)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============ Child Process Management ============
|
|
215
|
+
|
|
216
|
+
function spawnChild(): ChildProcess {
|
|
217
|
+
if (!fsSync.existsSync(APP_BUNDLE)) {
|
|
218
|
+
console.error(`ā App bundle not found: ${APP_BUNDLE}`)
|
|
219
|
+
console.error('Please run: npm run build:cli')
|
|
220
|
+
process.exit(1)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.log(`š Spawning child process: ${APP_BUNDLE}`)
|
|
224
|
+
|
|
225
|
+
const child = spawn(process.execPath, [APP_BUNDLE, ...process.argv.slice(2)], {
|
|
226
|
+
stdio: 'inherit',
|
|
227
|
+
env: {
|
|
228
|
+
...process.env,
|
|
229
|
+
TTC_IS_CHILD: '1',
|
|
230
|
+
TTC_UPDATER_PID: process.pid.toString()
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
child.on('exit', (code, signal) => {
|
|
235
|
+
console.log(`\nš¦ Child process exited (code: ${code}, signal: ${signal})`)
|
|
236
|
+
childProcess = null
|
|
237
|
+
|
|
238
|
+
// If update was requested, restart with new version
|
|
239
|
+
if (restartRequested) {
|
|
240
|
+
console.log('š Restarting with updated version...')
|
|
241
|
+
restartRequested = false
|
|
242
|
+
childProcess = spawnChild()
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// If child crashed, try to recover
|
|
247
|
+
if (code !== 0 && code !== null && !isUpdating) {
|
|
248
|
+
console.log('ā ļø Child process crashed, attempting recovery...')
|
|
249
|
+
|
|
250
|
+
// Try backup version if current is corrupted
|
|
251
|
+
if (fsSync.existsSync(APP_BUNDLE_OLD)) {
|
|
252
|
+
console.log('š Attempting to recover with backup...')
|
|
253
|
+
fs.copyFile(APP_BUNDLE_OLD, APP_BUNDLE)
|
|
254
|
+
.then(() => {
|
|
255
|
+
console.log('ā Recovered, restarting...')
|
|
256
|
+
childProcess = spawnChild()
|
|
257
|
+
})
|
|
258
|
+
.catch((err) => {
|
|
259
|
+
console.error('ā Recovery failed, giving up')
|
|
260
|
+
process.exit(1)
|
|
261
|
+
})
|
|
262
|
+
} else {
|
|
263
|
+
console.error('ā No backup available, cannot recover')
|
|
264
|
+
process.exit(1)
|
|
265
|
+
}
|
|
266
|
+
} else if (signal !== 'SIGTERM' && signal !== 'SIGINT') {
|
|
267
|
+
// Normal exit or intentional signal
|
|
268
|
+
process.exit(code || 0)
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
child.on('error', (error) => {
|
|
273
|
+
console.error('ā Child process error:', error)
|
|
274
|
+
childProcess = null
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
return child
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ============ Signal Handling ============
|
|
281
|
+
|
|
282
|
+
function setupSignalHandlers() {
|
|
283
|
+
// Forward signals to child
|
|
284
|
+
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGUSR1', 'SIGUSR2']
|
|
285
|
+
|
|
286
|
+
signals.forEach(signal => {
|
|
287
|
+
process.on(signal as NodeJS.Signals, () => {
|
|
288
|
+
if (childProcess) {
|
|
289
|
+
console.log(`\nš” Forwarding ${signal} to child process...`)
|
|
290
|
+
childProcess.kill(signal as NodeJS.Signals)
|
|
291
|
+
} else {
|
|
292
|
+
process.exit(0)
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
// Handle uncaught exceptions
|
|
298
|
+
process.on('uncaughtException', (error) => {
|
|
299
|
+
console.error('ā Uncaught exception in updater:', error)
|
|
300
|
+
if (childProcess) {
|
|
301
|
+
childProcess.kill('SIGTERM')
|
|
302
|
+
}
|
|
303
|
+
process.exit(1)
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
307
|
+
console.error('ā Unhandled rejection in updater:', reason)
|
|
308
|
+
})
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ============ CLI Interface ============
|
|
312
|
+
|
|
313
|
+
async function cmdUpdate(): Promise<void> {
|
|
314
|
+
console.log('š Checking for updates...')
|
|
315
|
+
|
|
316
|
+
const info = await checkForUpdates()
|
|
317
|
+
|
|
318
|
+
if (!info) {
|
|
319
|
+
console.log('ā ļø Unable to check for updates')
|
|
320
|
+
process.exit(1)
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!info.updateAvailable) {
|
|
325
|
+
console.log('ā Already up to date')
|
|
326
|
+
process.exit(0)
|
|
327
|
+
return
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
console.log('š¦ Update available!')
|
|
331
|
+
if (info.version) {
|
|
332
|
+
console.log(` Version: ${info.version}`)
|
|
333
|
+
}
|
|
334
|
+
if (info.changelog) {
|
|
335
|
+
console.log(`\nš Changelog:\n${info.changelog}`)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Stop child process if running
|
|
339
|
+
if (childProcess) {
|
|
340
|
+
console.log('š Stopping child process...')
|
|
341
|
+
childProcess.kill('SIGTERM')
|
|
342
|
+
childProcess = null
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
isUpdating = true
|
|
346
|
+
const success = await performUpdate(info)
|
|
347
|
+
isUpdating = false
|
|
348
|
+
|
|
349
|
+
if (success) {
|
|
350
|
+
console.log('\nā Update complete! Restarting...')
|
|
351
|
+
restartRequested = true
|
|
352
|
+
childProcess = spawnChild()
|
|
353
|
+
} else {
|
|
354
|
+
console.error('\nā Update failed!')
|
|
355
|
+
process.exit(1)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function cmdUpdateCheck(): Promise<void> {
|
|
360
|
+
console.log('š Checking for updates...')
|
|
361
|
+
|
|
362
|
+
const info = await checkForUpdates()
|
|
363
|
+
|
|
364
|
+
if (!info) {
|
|
365
|
+
console.log('ā ļø Unable to check for updates')
|
|
366
|
+
process.exit(1)
|
|
367
|
+
return
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (info.updateAvailable) {
|
|
371
|
+
console.log('š¦ Update available!')
|
|
372
|
+
if (info.version) {
|
|
373
|
+
console.log(` Version: ${info.version}`)
|
|
374
|
+
}
|
|
375
|
+
if (info.size) {
|
|
376
|
+
console.log(` Size: ${(info.size / 1024 / 1024).toFixed(2)} MB`)
|
|
377
|
+
}
|
|
378
|
+
if (info.changelog) {
|
|
379
|
+
console.log(`\nš Changelog:\n${info.changelog}`)
|
|
380
|
+
}
|
|
381
|
+
console.log('\nRun "ttc update" to apply the update')
|
|
382
|
+
process.exit(0)
|
|
383
|
+
} else {
|
|
384
|
+
console.log('ā Already up to date')
|
|
385
|
+
process.exit(0)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function cmdVersion(): Promise<void> {
|
|
390
|
+
console.log(`TalkToCode CLI Updater`)
|
|
391
|
+
console.log(`Node: ${process.version}`)
|
|
392
|
+
console.log(`Platform: ${os.platform()} ${os.arch()}`)
|
|
393
|
+
|
|
394
|
+
const currentHash = calculateHash(APP_BUNDLE)
|
|
395
|
+
console.log(`Bundle hash: ${currentHash ? currentHash.substring(0, 16) + '...' : 'unknown'}`)
|
|
396
|
+
|
|
397
|
+
const hashes = await getBundleHashes()
|
|
398
|
+
if (hashes['js-bundle']) {
|
|
399
|
+
console.log(`Bundle version: ${hashes['js-bundle'].version || 'unknown'}`)
|
|
400
|
+
console.log(`Bundle date: ${hashes['js-bundle'].date || 'unknown'}`)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
process.exit(0)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ============ IPC from Child Process ============
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The child process can request updates by writing to a special file
|
|
410
|
+
* or by sending a signal to the parent
|
|
411
|
+
*/
|
|
412
|
+
function setupChildIPC() {
|
|
413
|
+
// Listen for update requests via USR1 signal
|
|
414
|
+
process.on('SIGUSR1', async () => {
|
|
415
|
+
console.log('\nšØ Update requested by child process...')
|
|
416
|
+
|
|
417
|
+
const info = await checkForUpdates()
|
|
418
|
+
|
|
419
|
+
if (!info || !info.updateAvailable) {
|
|
420
|
+
console.log('ā No updates available')
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
console.log('š¦ Update available, applying...')
|
|
425
|
+
|
|
426
|
+
// Stop child process
|
|
427
|
+
if (childProcess) {
|
|
428
|
+
childProcess.kill('SIGTERM')
|
|
429
|
+
childProcess = null
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
isUpdating = true
|
|
433
|
+
const success = await performUpdate(info)
|
|
434
|
+
isUpdating = false
|
|
435
|
+
|
|
436
|
+
if (success) {
|
|
437
|
+
console.log('ā Update complete! Restarting...')
|
|
438
|
+
restartRequested = true
|
|
439
|
+
childProcess = spawnChild()
|
|
440
|
+
}
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ============ Main Entry Point ============
|
|
445
|
+
|
|
446
|
+
async function main() {
|
|
447
|
+
const args = process.argv.slice(2)
|
|
448
|
+
const command = args[0]
|
|
449
|
+
|
|
450
|
+
// Handle updater-specific commands
|
|
451
|
+
if (command === 'update') {
|
|
452
|
+
await cmdUpdate()
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (command === 'update:check' || command === 'check-update') {
|
|
457
|
+
await cmdUpdateCheck()
|
|
458
|
+
return
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (command === 'version' || command === '--version' || command === '-v') {
|
|
462
|
+
await cmdVersion()
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
467
|
+
console.log(`
|
|
468
|
+
TalkToCode CLI Updater
|
|
469
|
+
=====================
|
|
470
|
+
|
|
471
|
+
Commands:
|
|
472
|
+
ttc <command> Run command in child process (default)
|
|
473
|
+
ttc update Check and apply updates
|
|
474
|
+
ttc update:check Check for updates without applying
|
|
475
|
+
ttc version Show version information
|
|
476
|
+
ttc help Show this help message
|
|
477
|
+
|
|
478
|
+
The updater runs as the main process and spawns the app as a child.
|
|
479
|
+
This allows the updater to update itself even if the app is corrupted.
|
|
480
|
+
`)
|
|
481
|
+
process.exit(0)
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Default: spawn child process
|
|
486
|
+
console.log('šÆ TalkToCode CLI Updater')
|
|
487
|
+
console.log('====================================\n')
|
|
488
|
+
|
|
489
|
+
// Silent update check on startup
|
|
490
|
+
checkForUpdates().then(info => {
|
|
491
|
+
if (info?.updateAvailable) {
|
|
492
|
+
console.log('š¦ Update available! Run "ttc update" to apply.\n')
|
|
493
|
+
}
|
|
494
|
+
}).catch(() => {})
|
|
495
|
+
|
|
496
|
+
setupSignalHandlers()
|
|
497
|
+
setupChildIPC()
|
|
498
|
+
|
|
499
|
+
childProcess = spawnChild()
|
|
500
|
+
|
|
501
|
+
// Keep updater alive
|
|
502
|
+
process.on('exit', () => {
|
|
503
|
+
if (childProcess) {
|
|
504
|
+
childProcess.kill('SIGTERM')
|
|
505
|
+
}
|
|
506
|
+
})
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
main().catch(error => {
|
|
510
|
+
console.error('ā Fatal error:', error)
|
|
511
|
+
process.exit(1)
|
|
512
|
+
})
|