@exreve/exk 1.0.24 → 1.0.25
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/dist/index.js +15 -192
- package/package.json +2 -2
- package/dist/app-child.js +0 -2589
- package/dist/ttc-cli.tar.gz +0 -0
- package/dist/updater.js +0 -425
package/dist/index.js
CHANGED
|
@@ -16,7 +16,6 @@ import { startApp, stopApp, restartApp, getAppStatuses, getAppLogs } from './app
|
|
|
16
16
|
import { spawn, execSync } from 'child_process';
|
|
17
17
|
import readline from 'readline';
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
|
-
import { createHash } from 'crypto';
|
|
20
19
|
// ============ Constants ============
|
|
21
20
|
const CONFIG_DIR = path.join(os.homedir(), '.talk-to-code');
|
|
22
21
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
@@ -200,168 +199,24 @@ function getCliVersion() {
|
|
|
200
199
|
return '0.0.0';
|
|
201
200
|
}
|
|
202
201
|
}
|
|
203
|
-
|
|
204
|
-
const __dirname = path.dirname(
|
|
205
|
-
let
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return cachedCliHash;
|
|
209
|
-
try {
|
|
210
|
-
const hash = createHash('sha256').update(fsSync.readFileSync(CURRENT_FILE)).digest('hex');
|
|
211
|
-
cachedCliHash = hash;
|
|
212
|
-
return hash;
|
|
213
|
-
}
|
|
214
|
-
catch {
|
|
215
|
-
// File may be temporarily missing during update — return safe fallback
|
|
216
|
-
return 'updating';
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
async function checkForUpdate() {
|
|
202
|
+
// ============ Update Helpers ============
|
|
203
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
204
|
+
let isUpdating = false;
|
|
205
|
+
/** Check if a newer version is available on npm */
|
|
206
|
+
async function checkForNpmUpdate() {
|
|
220
207
|
try {
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
return null;
|
|
229
|
-
return await res.json();
|
|
208
|
+
const currentVersion = getCliVersion();
|
|
209
|
+
const latestVersion = execSync('npm view @exreve/exk version', { encoding: 'utf-8', timeout: 10000 }).trim();
|
|
210
|
+
return {
|
|
211
|
+
updateAvailable: currentVersion !== latestVersion,
|
|
212
|
+
currentVersion,
|
|
213
|
+
latestVersion
|
|
214
|
+
};
|
|
230
215
|
}
|
|
231
216
|
catch {
|
|
232
217
|
return null;
|
|
233
218
|
}
|
|
234
219
|
}
|
|
235
|
-
let isUpdating = false;
|
|
236
|
-
async function replaceSelf(tarballBuffer) {
|
|
237
|
-
isUpdating = true;
|
|
238
|
-
const extractDir = path.join(os.tmpdir(), `ttc-update-${Date.now()}`);
|
|
239
|
-
const stagingDir = path.join(os.tmpdir(), `ttc-stage-${Date.now()}`);
|
|
240
|
-
try {
|
|
241
|
-
await fs.mkdir(extractDir, { recursive: true });
|
|
242
|
-
const tarPath = path.join(extractDir, 'ttc-cli.tar.gz');
|
|
243
|
-
await fs.writeFile(tarPath, tarballBuffer);
|
|
244
|
-
execSync(`tar -xzf "${tarPath}" -C "${extractDir}"`);
|
|
245
|
-
await fs.unlink(tarPath);
|
|
246
|
-
// Preserve user config/token files (never overwrite on update)
|
|
247
|
-
const preserveFiles = ['device-config.json', 'device-id.json', 'config.json'];
|
|
248
|
-
const preserved = {};
|
|
249
|
-
for (const f of preserveFiles) {
|
|
250
|
-
try {
|
|
251
|
-
preserved[f] = await fs.readFile(path.join(CONFIG_DIR, f));
|
|
252
|
-
}
|
|
253
|
-
catch {
|
|
254
|
-
/* file may not exist */
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
// Build the complete new state in a staging directory first.
|
|
258
|
-
// This ensures we never delete the running app until the replacement is
|
|
259
|
-
// fully prepared and verified — no window where files are missing.
|
|
260
|
-
const cliStaging = path.join(stagingDir, 'cli');
|
|
261
|
-
const sharedStaging = path.join(stagingDir, 'shared');
|
|
262
|
-
await fs.cp(path.join(extractDir, 'cli'), cliStaging, { recursive: true });
|
|
263
|
-
await fs.cp(path.join(extractDir, 'shared'), sharedStaging, { recursive: true });
|
|
264
|
-
await fs.copyFile(path.join(extractDir, 'package.json'), path.join(stagingDir, 'package.json'));
|
|
265
|
-
// Restore preserved config into staging
|
|
266
|
-
for (const f of preserveFiles) {
|
|
267
|
-
if (preserved[f])
|
|
268
|
-
await fs.writeFile(path.join(stagingDir, f), preserved[f]);
|
|
269
|
-
}
|
|
270
|
-
// Sanity check: staging must contain a valid dist/index.js
|
|
271
|
-
const indexJs = path.join(cliStaging, 'dist', 'index.js');
|
|
272
|
-
try {
|
|
273
|
-
const stat = await fs.stat(indexJs);
|
|
274
|
-
if (stat.size === 0)
|
|
275
|
-
throw new Error('Empty index.js');
|
|
276
|
-
}
|
|
277
|
-
catch (err) {
|
|
278
|
-
throw new Error(`Staged update is invalid (missing or empty dist/index.js): ${err.message}`);
|
|
279
|
-
}
|
|
280
|
-
// Atomic swap: rename staging dirs to final locations.
|
|
281
|
-
// rename() is atomic on the same filesystem. We move the old dirs out of
|
|
282
|
-
// the way first, then rename new ones in. If anything fails mid-swap we
|
|
283
|
-
// can attempt to roll back.
|
|
284
|
-
const cliDest = path.join(CONFIG_DIR, 'cli');
|
|
285
|
-
const sharedDest = path.join(CONFIG_DIR, 'shared');
|
|
286
|
-
const cliOld = cliDest + '.old';
|
|
287
|
-
const sharedOld = sharedDest + '.old';
|
|
288
|
-
// Move current dirs to .old (must exist for this to work)
|
|
289
|
-
await fs.rm(cliOld, { recursive: true, force: true });
|
|
290
|
-
await fs.rm(sharedOld, { recursive: true, force: true });
|
|
291
|
-
if (fsSync.existsSync(cliDest)) {
|
|
292
|
-
await fs.rename(cliDest, cliOld);
|
|
293
|
-
}
|
|
294
|
-
if (fsSync.existsSync(sharedDest)) {
|
|
295
|
-
await fs.rename(sharedDest, sharedOld);
|
|
296
|
-
}
|
|
297
|
-
// Move staged dirs into place
|
|
298
|
-
try {
|
|
299
|
-
await fs.rename(cliStaging, cliDest);
|
|
300
|
-
await fs.rename(sharedStaging, sharedDest);
|
|
301
|
-
await fs.copyFile(path.join(stagingDir, 'package.json'), path.join(CONFIG_DIR, 'package.json'));
|
|
302
|
-
}
|
|
303
|
-
catch (swapErr) {
|
|
304
|
-
// Rollback: restore old dirs
|
|
305
|
-
console.error(`⚠ Swap failed: ${swapErr.message}, rolling back...`);
|
|
306
|
-
try {
|
|
307
|
-
if (fsSync.existsSync(cliOld))
|
|
308
|
-
await fs.rename(cliOld, cliDest);
|
|
309
|
-
if (fsSync.existsSync(sharedOld))
|
|
310
|
-
await fs.rename(sharedOld, sharedDest);
|
|
311
|
-
}
|
|
312
|
-
catch (rollbackErr) {
|
|
313
|
-
console.error(`⚠ Rollback also failed: ${rollbackErr.message}`);
|
|
314
|
-
}
|
|
315
|
-
throw swapErr;
|
|
316
|
-
}
|
|
317
|
-
// Clean up old dirs and temp dirs
|
|
318
|
-
await fs.rm(cliOld, { recursive: true, force: true }).catch(() => { });
|
|
319
|
-
await fs.rm(sharedOld, { recursive: true, force: true }).catch(() => { });
|
|
320
|
-
await fs.rm(extractDir, { recursive: true, force: true });
|
|
321
|
-
await fs.rm(stagingDir, { recursive: true, force: true }).catch(() => { });
|
|
322
|
-
// Invalidate cached hash since we just replaced the binary
|
|
323
|
-
cachedCliHash = null;
|
|
324
|
-
console.log('✓ CLI updated');
|
|
325
|
-
const npmPaths = [path.join(os.homedir(), '.nvm/versions/node/v22/bin/npm'), path.join(os.homedir(), '.nvm/versions/node/v20/bin/npm')];
|
|
326
|
-
const npmBin = npmPaths.find(p => fsSync.existsSync(p)) || 'npm';
|
|
327
|
-
try {
|
|
328
|
-
execSync(`"${npmBin}" install --omit=dev`, { cwd: CONFIG_DIR, stdio: 'inherit' });
|
|
329
|
-
console.log('✓ Dependencies updated');
|
|
330
|
-
}
|
|
331
|
-
catch {
|
|
332
|
-
console.warn('⚠ npm install failed');
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
finally {
|
|
336
|
-
isUpdating = false;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
async function selfUpdate(force = false) {
|
|
340
|
-
const info = await checkForUpdate();
|
|
341
|
-
if (!info || !info.updateAvailable) {
|
|
342
|
-
console.log('✓ Already up to date');
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
console.log('📦 Update available!');
|
|
346
|
-
if (!force && process.stdin.isTTY) {
|
|
347
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
348
|
-
const answer = await new Promise(r => rl.question('Update now? [Y/n] ', a => { rl.close(); r(a.trim()); }));
|
|
349
|
-
if (answer.toLowerCase().startsWith('n'))
|
|
350
|
-
return;
|
|
351
|
-
}
|
|
352
|
-
if (!info.downloadUrl || !info.hash)
|
|
353
|
-
return;
|
|
354
|
-
console.log('Downloading...');
|
|
355
|
-
const res = await fetch(info.downloadUrl);
|
|
356
|
-
if (!res.ok)
|
|
357
|
-
throw new Error('Download failed');
|
|
358
|
-
const bundle = Buffer.from(await res.arrayBuffer());
|
|
359
|
-
if (createHash('sha256').update(bundle).digest('hex') !== info.hash) {
|
|
360
|
-
throw new Error('Hash mismatch');
|
|
361
|
-
}
|
|
362
|
-
await replaceSelf(bundle);
|
|
363
|
-
process.exit(0);
|
|
364
|
-
}
|
|
365
220
|
// ============ Commands ============
|
|
366
221
|
async function registerDevice(name, email) {
|
|
367
222
|
try {
|
|
@@ -661,9 +516,9 @@ async function runDaemon(foreground = false, email) {
|
|
|
661
516
|
const ipAddress = getLocalIpAddress();
|
|
662
517
|
const hostname = getHostname();
|
|
663
518
|
// Silent update check on startup
|
|
664
|
-
|
|
519
|
+
checkForNpmUpdate().then(info => {
|
|
665
520
|
if (info?.updateAvailable)
|
|
666
|
-
console.log(
|
|
521
|
+
console.log(`📦 Update available: ${info.currentVersion} → ${info.latestVersion} (run "exk update")`);
|
|
667
522
|
}).catch(() => { });
|
|
668
523
|
if (foreground)
|
|
669
524
|
console.log('=== TalkToCode CLI (Foreground) ===');
|
|
@@ -1794,7 +1649,6 @@ async function runDaemon(foreground = false, email) {
|
|
|
1794
1649
|
callback({
|
|
1795
1650
|
success: true,
|
|
1796
1651
|
version: getCliVersion(),
|
|
1797
|
-
hash: getCliHash(),
|
|
1798
1652
|
date: new Date().toISOString(),
|
|
1799
1653
|
nodeVersion: process.version,
|
|
1800
1654
|
platform: os.platform(),
|
|
@@ -1847,37 +1701,6 @@ async function runDaemon(foreground = false, email) {
|
|
|
1847
1701
|
}
|
|
1848
1702
|
});
|
|
1849
1703
|
});
|
|
1850
|
-
// ========== update:start handler (legacy compatibility) ==========
|
|
1851
|
-
socket.on('update:start', (_data, callback) => {
|
|
1852
|
-
if (isUpdating) {
|
|
1853
|
-
callback?.({ success: false, error: 'Update already in progress' });
|
|
1854
|
-
return;
|
|
1855
|
-
}
|
|
1856
|
-
// Use npm-based self-update
|
|
1857
|
-
if (foreground) {
|
|
1858
|
-
console.log('[update:start] Starting npm self-update...');
|
|
1859
|
-
}
|
|
1860
|
-
callback?.({ success: true, message: 'Update started via npm' });
|
|
1861
|
-
const npmPaths = [
|
|
1862
|
-
path.join(os.homedir(), '.nvm/versions/node/v22/bin/npm'),
|
|
1863
|
-
path.join(os.homedir(), '.nvm/versions/node/v20/bin/npm')
|
|
1864
|
-
];
|
|
1865
|
-
const npmBin = npmPaths.find(p => fsSync.existsSync(p)) || 'npm';
|
|
1866
|
-
const updateProcess = spawn(npmBin, ['update', '-g', '@exreve/exk'], {
|
|
1867
|
-
stdio: 'pipe',
|
|
1868
|
-
detached: true
|
|
1869
|
-
});
|
|
1870
|
-
updateProcess.on('close', () => {
|
|
1871
|
-
setTimeout(() => {
|
|
1872
|
-
try {
|
|
1873
|
-
execSync('pm2 restart cli', { stdio: 'inherit' });
|
|
1874
|
-
}
|
|
1875
|
-
catch {
|
|
1876
|
-
process.exit(0);
|
|
1877
|
-
}
|
|
1878
|
-
}, 2000);
|
|
1879
|
-
});
|
|
1880
|
-
});
|
|
1881
1704
|
// Cloudflared handlers
|
|
1882
1705
|
socket.on('cloudflared:check:request', async () => {
|
|
1883
1706
|
try {
|
|
@@ -2388,7 +2211,7 @@ async function runDaemon(foreground = false, email) {
|
|
|
2388
2211
|
const { name, image, ports = [], env = {}, runAsRoot = false } = data;
|
|
2389
2212
|
// Get CLI directory path (where this script is running from)
|
|
2390
2213
|
// Use the same pattern as elsewhere in the file
|
|
2391
|
-
const cliDir =
|
|
2214
|
+
const cliDir = __dirname;
|
|
2392
2215
|
// Build docker run command
|
|
2393
2216
|
let cmd = `${runtime} run -d --name ${name}`;
|
|
2394
2217
|
// Security: Running as root INSIDE container is safe - it's still isolated from host
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exreve/exk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.25",
|
|
4
4
|
"description": "exk - Control Claude CLI with voice and programmable interfaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"container-entrypoint.sh"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "tsc
|
|
17
|
+
"build": "tsc",
|
|
18
18
|
"build:tsc": "tsc",
|
|
19
19
|
"typecheck": "tsc --noEmit",
|
|
20
20
|
"prepublishOnly": "node -e \"require('fs').chmodSync('bin/exk', 0o755)\""
|