@exreve/exk 1.0.24 → 1.0.26

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 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
- const CURRENT_FILE = fileURLToPath(import.meta.url);
204
- const __dirname = path.dirname(CURRENT_FILE);
205
- let cachedCliHash = null;
206
- function getCliHash() {
207
- if (cachedCliHash)
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 config = await readConfig();
222
- const res = await fetch(`${config.apiUrl}/update/check`, {
223
- method: 'POST',
224
- headers: { 'Content-Type': 'application/json' },
225
- body: JSON.stringify({ hash: getCliHash(), platform: os.platform() })
226
- });
227
- if (!res.ok)
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
- checkForUpdate().then(info => {
519
+ checkForNpmUpdate().then(info => {
665
520
  if (info?.updateAvailable)
666
- console.log('📦 Update available: run "ttc update"');
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(),
@@ -1830,15 +1684,9 @@ async function runDaemon(foreground = false, email) {
1830
1684
  if (output)
1831
1685
  console.log(output);
1832
1686
  }
1833
- // Restart PM2 process "cli" after a short delay
1687
+ // Exit and let PM2 restart us with the new version
1834
1688
  setTimeout(() => {
1835
- try {
1836
- execSync('pm2 restart cli', { stdio: 'inherit' });
1837
- }
1838
- catch {
1839
- // If pm2 restart fails, just exit and let pm2 restart us
1840
- process.exit(0);
1841
- }
1689
+ process.exit(0);
1842
1690
  }, 2000);
1843
1691
  });
1844
1692
  updateProcess.on('error', (err) => {
@@ -1847,37 +1695,6 @@ async function runDaemon(foreground = false, email) {
1847
1695
  }
1848
1696
  });
1849
1697
  });
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
1698
  // Cloudflared handlers
1882
1699
  socket.on('cloudflared:check:request', async () => {
1883
1700
  try {
@@ -2388,7 +2205,7 @@ async function runDaemon(foreground = false, email) {
2388
2205
  const { name, image, ports = [], env = {}, runAsRoot = false } = data;
2389
2206
  // Get CLI directory path (where this script is running from)
2390
2207
  // Use the same pattern as elsewhere in the file
2391
- const cliDir = path.dirname(CURRENT_FILE);
2208
+ const cliDir = __dirname;
2392
2209
  // Build docker run command
2393
2210
  let cmd = `${runtime} run -d --name ${name}`;
2394
2211
  // 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.24",
3
+ "version": "1.0.26",
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 && node build-cli-tarball.js",
17
+ "build": "tsc",
18
18
  "build:tsc": "tsc",
19
19
  "typecheck": "tsc --noEmit",
20
20
  "prepublishOnly": "node -e \"require('fs').chmodSync('bin/exk', 0o755)\""