claude-remote-cli 1.2.0 → 1.3.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.
@@ -12,12 +12,49 @@ import * as auth from './auth.js';
12
12
  import * as sessions from './sessions.js';
13
13
  import { setupWebSocket } from './ws.js';
14
14
  import { WorktreeWatcher } from './watcher.js';
15
+ import { isInstalled as serviceIsInstalled } from './service.js';
15
16
  const __filename = fileURLToPath(import.meta.url);
16
17
  const __dirname = path.dirname(__filename);
17
18
  const execFileAsync = promisify(execFile);
18
19
  // When run via CLI bin, config lives in ~/.config/claude-remote-cli/
19
20
  // When run directly (development), fall back to local config.json
20
21
  const CONFIG_PATH = process.env.CLAUDE_REMOTE_CONFIG || path.join(__dirname, '..', '..', 'config.json');
22
+ const VERSION_CACHE_TTL = 5 * 60 * 1000;
23
+ let versionCache = null;
24
+ function getCurrentVersion() {
25
+ const pkgPath = path.join(__dirname, '..', '..', 'package.json');
26
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
27
+ return pkg.version;
28
+ }
29
+ function semverLessThan(a, b) {
30
+ const parse = (v) => v.split('.').map(Number);
31
+ const [aMaj = 0, aMin = 0, aPat = 0] = parse(a);
32
+ const [bMaj = 0, bMin = 0, bPat = 0] = parse(b);
33
+ if (aMaj !== bMaj)
34
+ return aMaj < bMaj;
35
+ if (aMin !== bMin)
36
+ return aMin < bMin;
37
+ return aPat < bPat;
38
+ }
39
+ async function getLatestVersion() {
40
+ const now = Date.now();
41
+ if (versionCache && now - versionCache.fetchedAt < VERSION_CACHE_TTL) {
42
+ return versionCache.latest;
43
+ }
44
+ try {
45
+ const res = await fetch('https://registry.npmjs.org/claude-remote-cli/latest');
46
+ if (!res.ok)
47
+ return null;
48
+ const data = await res.json();
49
+ if (!data.version)
50
+ return null;
51
+ versionCache = { latest: data.version, fetchedAt: now };
52
+ return data.version;
53
+ }
54
+ catch (_) {
55
+ return null;
56
+ }
57
+ }
21
58
  function parseTTL(ttl) {
22
59
  if (typeof ttl !== 'string')
23
60
  return 24 * 60 * 60 * 1000;
@@ -340,6 +377,28 @@ async function main() {
340
377
  res.status(404).json({ error: 'Session not found' });
341
378
  }
342
379
  });
380
+ // GET /version — check current vs latest
381
+ app.get('/version', requireAuth, async (_req, res) => {
382
+ const current = getCurrentVersion();
383
+ const latest = await getLatestVersion();
384
+ const updateAvailable = latest !== null && semverLessThan(current, latest);
385
+ res.json({ current, latest, updateAvailable });
386
+ });
387
+ // POST /update — install latest version from npm
388
+ app.post('/update', requireAuth, async (_req, res) => {
389
+ try {
390
+ await execFileAsync('npm', ['install', '-g', 'claude-remote-cli@latest']);
391
+ const restarting = serviceIsInstalled();
392
+ res.json({ ok: true, restarting });
393
+ if (restarting) {
394
+ setTimeout(() => process.exit(0), 1000);
395
+ }
396
+ }
397
+ catch (err) {
398
+ const message = err instanceof Error ? err.message : 'Update failed';
399
+ res.status(500).json({ ok: false, error: message });
400
+ }
401
+ });
343
402
  server.listen(config.port, config.host, () => {
344
403
  console.log(`claude-remote-cli listening on ${config.host}:${config.port}`);
345
404
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote-cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Remote web interface for Claude Code CLI sessions",
5
5
  "type": "module",
6
6
  "main": "dist/server/index.js",
package/public/app.js CHANGED
@@ -40,6 +40,10 @@
40
40
  var deleteWtName = document.getElementById('delete-wt-name');
41
41
  var deleteWtCancel = document.getElementById('delete-wt-cancel');
42
42
  var deleteWtConfirm = document.getElementById('delete-wt-confirm');
43
+ var updateToast = document.getElementById('update-toast');
44
+ var updateToastText = document.getElementById('update-toast-text');
45
+ var updateToastBtn = document.getElementById('update-toast-btn');
46
+ var updateToastDismiss = document.getElementById('update-toast-dismiss');
43
47
 
44
48
  // Context menu state
45
49
  var contextMenuTarget = null; // stores { worktreePath, repoPath, name }
@@ -117,6 +121,7 @@
117
121
  loadRepos();
118
122
  refreshAll();
119
123
  connectEventSocket();
124
+ checkForUpdates();
120
125
  }
121
126
 
122
127
  // ── Terminal ────────────────────────────────────────────────────────────────
@@ -851,6 +856,73 @@
851
856
  vv.addEventListener('scroll', onViewportResize);
852
857
  })();
853
858
 
859
+ // ── Update Toast ─────────────────────────────────────────────────────────────
860
+
861
+ function checkForUpdates() {
862
+ fetch('/version')
863
+ .then(function (res) {
864
+ if (!res.ok) return;
865
+ return res.json();
866
+ })
867
+ .then(function (data) {
868
+ if (data && data.updateAvailable) {
869
+ showUpdateToast(data.current, data.latest);
870
+ }
871
+ })
872
+ .catch(function () {
873
+ // Silently ignore version check errors
874
+ });
875
+ }
876
+
877
+ function showUpdateToast(current, latest) {
878
+ updateToastText.textContent = 'Update available: v' + current + ' \u2192 v' + latest;
879
+ updateToast.hidden = false;
880
+ updateToastBtn.disabled = false;
881
+ updateToastBtn.textContent = 'Update Now';
882
+
883
+ updateToastBtn.onclick = function () {
884
+ triggerUpdate(latest);
885
+ };
886
+ }
887
+
888
+ function triggerUpdate(latest) {
889
+ updateToastBtn.disabled = true;
890
+ updateToastBtn.textContent = 'Updating\u2026';
891
+
892
+ fetch('/update', { method: 'POST' })
893
+ .then(function (res) {
894
+ return res.json().then(function (data) {
895
+ return { ok: res.ok, data: data };
896
+ });
897
+ })
898
+ .then(function (result) {
899
+ if (result.ok && result.data.restarting) {
900
+ updateToastText.textContent = 'Updated! Restarting server\u2026';
901
+ updateToastBtn.hidden = true;
902
+ updateToastDismiss.hidden = true;
903
+ setTimeout(function () {
904
+ location.reload();
905
+ }, 5000);
906
+ } else if (result.ok) {
907
+ updateToastText.textContent = 'Updated! Please restart the server manually.';
908
+ updateToastBtn.hidden = true;
909
+ } else {
910
+ updateToastText.textContent = 'Update failed: ' + (result.data.error || 'Unknown error');
911
+ updateToastBtn.disabled = false;
912
+ updateToastBtn.textContent = 'Retry';
913
+ }
914
+ })
915
+ .catch(function () {
916
+ updateToastText.textContent = 'Update failed. Please try again.';
917
+ updateToastBtn.disabled = false;
918
+ updateToastBtn.textContent = 'Retry';
919
+ });
920
+ }
921
+
922
+ updateToastDismiss.addEventListener('click', function () {
923
+ updateToast.hidden = true;
924
+ });
925
+
854
926
  // ── Auto-auth Check ─────────────────────────────────────────────────────────
855
927
 
856
928
  fetch('/sessions')
package/public/index.html CHANGED
@@ -78,6 +78,17 @@
78
78
  </div>
79
79
  </div>
80
80
 
81
+ <!-- Update Toast -->
82
+ <div id="update-toast" hidden>
83
+ <div id="update-toast-content">
84
+ <span id="update-toast-text"></span>
85
+ <div id="update-toast-actions">
86
+ <button id="update-toast-btn" class="btn-accent">Update Now</button>
87
+ <button id="update-toast-dismiss" aria-label="Dismiss">&times;</button>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
81
92
  </div>
82
93
 
83
94
  <!-- New Session Dialog -->
package/public/style.css CHANGED
@@ -818,6 +818,84 @@ dialog#delete-worktree-dialog h2 {
818
818
  opacity: 0.85;
819
819
  }
820
820
 
821
+ /* ===== Update Toast ===== */
822
+
823
+ #update-toast {
824
+ position: fixed;
825
+ bottom: 0;
826
+ left: 0;
827
+ right: 0;
828
+ z-index: 150;
829
+ display: flex;
830
+ justify-content: center;
831
+ padding: 12px 12px calc(12px + env(safe-area-inset-bottom));
832
+ pointer-events: none;
833
+ animation: toast-slide-up 0.25s ease-out;
834
+ }
835
+
836
+ @keyframes toast-slide-up {
837
+ from {
838
+ transform: translateY(100%);
839
+ opacity: 0;
840
+ }
841
+ to {
842
+ transform: translateY(0);
843
+ opacity: 1;
844
+ }
845
+ }
846
+
847
+ #update-toast-content {
848
+ display: flex;
849
+ flex-direction: row;
850
+ align-items: center;
851
+ gap: 12px;
852
+ background: var(--surface);
853
+ border: 1px solid var(--border);
854
+ border-radius: 10px;
855
+ padding: 12px 16px;
856
+ max-width: 500px;
857
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
858
+ pointer-events: auto;
859
+ }
860
+
861
+ #update-toast-text {
862
+ flex: 1;
863
+ font-size: 0.85rem;
864
+ color: var(--text);
865
+ }
866
+
867
+ #update-toast-actions {
868
+ display: flex;
869
+ gap: 8px;
870
+ flex-shrink: 0;
871
+ }
872
+
873
+ #update-toast-btn {
874
+ padding: 8px 14px;
875
+ border-radius: 6px;
876
+ font-size: 0.8rem;
877
+ border: none;
878
+ white-space: nowrap;
879
+ }
880
+
881
+ #update-toast-btn:disabled {
882
+ opacity: 0.6;
883
+ cursor: not-allowed;
884
+ }
885
+
886
+ #update-toast-dismiss {
887
+ background: none;
888
+ border: none;
889
+ color: var(--text-muted);
890
+ font-size: 1.2rem;
891
+ padding: 4px 6px;
892
+ cursor: pointer;
893
+ }
894
+
895
+ #update-toast-dismiss:hover {
896
+ color: var(--text);
897
+ }
898
+
821
899
  /* ===== Mobile Responsive ===== */
822
900
  @media (max-width: 600px) {
823
901
  #mobile-header {