server-studio 1.2.0 → 1.2.2

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/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  Notable changes to Server Studio. Dates are release dates.
4
4
 
5
+ ## 1.2.2 — 2026-08-30
6
+
7
+ - The version badge no longer keeps offering an update you have already installed. Once you start
8
+ one it reads **restart to finish**, because the running process keeps the old code until it is
9
+ reopened. It returns to a plain version once you restart.
10
+
11
+ ## 1.2.1 — 2026-08-30
12
+
13
+ - **Fixed:** an installed app reported its version as `0.0.0`, because the code looks for
14
+ `package.json` one level up and the app bundle does not contain one. The badge showed `v0.0.0`,
15
+ the update prompt appeared permanently even straight after updating, and telemetry recorded every
16
+ install as version 0.0.0. The installer now stamps the version beside the code.
17
+ - **Fixed:** if the local server was not responding, clicking **Update**, **Run** or **Stop** did
18
+ nothing at all. The failed request threw and was never caught, so there was no error either. Those
19
+ actions now say what went wrong.
20
+
5
21
  ## 1.2.0 — 2026-08-30
6
22
 
7
23
  ### Folders replace categories
package/bin/cli.js CHANGED
@@ -69,6 +69,14 @@ function install() {
69
69
  // cpSync does not preserve the exec bit on the launcher.
70
70
  // src/ is the single source of truth, copied into the bundle at install time.
71
71
  fs.cpSync(SRC, path.join(APP_DEST, 'Contents', 'Resources'), { recursive: true });
72
+ // The bundle has no package.json, so stamp the version beside the code. Without this
73
+ // an installed app reports 0.0.0 and therefore thinks an update is always available.
74
+ try {
75
+ fs.writeFileSync(
76
+ path.join(APP_DEST, 'Contents', 'Resources', 'version.json'),
77
+ JSON.stringify({ version: require(path.join(ROOT, 'package.json')).version }) + '\n'
78
+ );
79
+ } catch (e) {}
72
80
  fs.chmodSync(path.join(APP_DEST, 'Contents', 'MacOS', 'ServerStudio'), 0o755);
73
81
  } else skip('app skipped (--no-app)');
74
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "server-studio",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "A tiny dashboard for your local dev servers. One click to run, stop and open them, with one permanent port per project.",
5
5
  "author": "Md Mahdi Hasan",
6
6
  "license": "MIT",
package/src/index.html CHANGED
@@ -113,6 +113,7 @@
113
113
  .ver.has-update:hover { background: rgba(62,207,142,.24); }
114
114
  .ver .dot-new { width: 6px; height: 6px; border-radius: 50%; background: var(--green); flex: none; }
115
115
  .ver.checking { opacity: .55; }
116
+ .ver.pending { color: var(--amber); border-color: rgba(245,179,67,.4); background: rgba(245,179,67,.12); padding: 3px 9px; margin-left: -10px; }
116
117
  @keyframes verPulse {
117
118
  0%, 100% { box-shadow: 0 0 0 0 rgba(62,207,142,0); }
118
119
  50% { box-shadow: 0 0 0 4px rgba(62,207,142,.18); }
@@ -831,6 +832,15 @@ function ssPrompt(title, opts) {
831
832
  /* ---------- version + update ---------- */
832
833
  let updateInfo = null;
833
834
 
835
+ function isNewerVersion(a, b) {
836
+ const pa = String(a).split('.').map(Number), pb = String(b).split('.').map(Number);
837
+ for (let i = 0; i < 3; i++) {
838
+ const x = pa[i] || 0, y = pb[i] || 0;
839
+ if (x !== y) return x > y;
840
+ }
841
+ return false;
842
+ }
843
+
834
844
  async function checkUpdate() {
835
845
  const pill = document.getElementById('verPill');
836
846
  if (!pill) return;
@@ -840,6 +850,22 @@ async function checkUpdate() {
840
850
  updateInfo = r;
841
851
  pill.classList.remove('checking');
842
852
 
853
+ // An update was kicked off from here and this process is still the old one.
854
+ let pending = null;
855
+ try { pending = localStorage.getItem('ss_updating_to'); } catch (e) {}
856
+ if (pending && !isNewerVersion(pending, r.current)) {
857
+ // Already running it, so the restart happened. Clear the flag.
858
+ try { localStorage.removeItem('ss_updating_to'); } catch (e) {}
859
+ pending = null;
860
+ }
861
+ if (pending) {
862
+ pill.className = 'ver pending';
863
+ pill.textContent = 'restart to finish ' + pending;
864
+ pill.title = 'Version ' + pending + ' has been installed. Quit and reopen Server Studio to run it.';
865
+ pill.onclick = null;
866
+ return;
867
+ }
868
+
843
869
  if (r.newer) {
844
870
  pill.className = 'ver has-update';
845
871
  pill.innerHTML = '<span class="dot-new"></span>v' + esc(r.current) + ' &rarr; ' + esc(r.latest);
@@ -859,10 +885,19 @@ async function runUpdate() {
859
885
  'You are on ' + updateInfo.current + '. This runs npm install -g server-studio in a terminal window.\n\nYour saved servers and folders are not touched. Restart Server Studio when it finishes.',
860
886
  { okText: 'Update' });
861
887
  if (!ok) return;
862
- const r = await api('/api/run', { method: 'POST', headers: { 'Content-Type': 'application/json' },
863
- body: JSON.stringify({ cwd: '', command: 'npm install -g server-studio' }) });
864
- if (r && r.ok) toast('Updating in ' + TERM_NAME + '. Restart Server Studio when it finishes.');
865
- else toast('Could not start the update. Run: npm install -g server-studio', true);
888
+ let r = null;
889
+ try {
890
+ r = await api('/api/run', { method: 'POST', headers: { 'Content-Type': 'application/json' },
891
+ body: JSON.stringify({ cwd: '', command: 'npm install -g server-studio' }) });
892
+ } catch (e) { /* server gone: fall through to the error toast rather than doing nothing */ }
893
+ if (r && r.ok) {
894
+ // The running process keeps the old code until it restarts, so stop offering the
895
+ // update. Continuing to show it after a successful update reads as a failure.
896
+ try { localStorage.setItem('ss_updating_to', updateInfo.latest); } catch (e) {}
897
+ checkUpdate();
898
+ toast('Updating in ' + TERM_NAME + '. Restart Server Studio when it finishes.');
899
+ }
900
+ else toast('Could not start the update. Run this yourself: npm install -g server-studio', true);
866
901
  }
867
902
 
868
903
  function moveToFolder(serverId, folderId) {
@@ -988,8 +1023,11 @@ async function runServer(id) {
988
1023
  setTimeout(() => { const el = document.getElementById('f_cwd'); if (el) el.focus(); }, 120);
989
1024
  return;
990
1025
  }
991
- const r = await api('/api/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cwd: s.cwd || '', command: s.command }) });
992
- if (r.ok) {
1026
+ let r = null;
1027
+ try {
1028
+ r = await api('/api/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ cwd: s.cwd || '', command: s.command }) });
1029
+ } catch (e) {}
1030
+ if (r && r.ok) {
993
1031
  toast('Running "' + (s.name || 'server') + '" in ' + TERM_NAME);
994
1032
  if (portOf(s)) { s._status = 'checking'; render(); }
995
1033
  setTimeout(() => refreshStatus(), 3500);
@@ -1000,8 +1038,10 @@ async function stopServer(id) {
1000
1038
  const port = portOf(s);
1001
1039
  if (!port) return;
1002
1040
  s._status = 'checking'; render();
1003
- await api('/api/stop', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ port }) });
1004
- toast('Stopped whatever was on port ' + port);
1041
+ try {
1042
+ await api('/api/stop', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ port }) });
1043
+ toast('Stopped whatever was on port ' + port);
1044
+ } catch (e) { toast('Could not stop it, the app is not responding.', true); }
1005
1045
  setTimeout(() => refreshStatus(), 700);
1006
1046
  }
1007
1047
  function copyUrl(url, btn) {
package/src/server.js CHANGED
@@ -88,7 +88,12 @@ function portFromUrl(url) {
88
88
 
89
89
  /* ---------- version check ---------- */
90
90
  function appVersion() {
91
- try { return require('../package.json').version || '0.0.0'; } catch (e) { return '0.0.0'; }
91
+ // Running from a checkout, package.json is one level up. Inside the installed app
92
+ // bundle there is no package.json, so the installer stamps version.json beside the
93
+ // code. Try both before giving up.
94
+ try { return require('./version.json').version || '0.0.0'; } catch (e) {}
95
+ try { return require('../package.json').version || '0.0.0'; } catch (e) {}
96
+ return '0.0.0';
92
97
  }
93
98
  function isNewer(a, b) {
94
99
  const pa = String(a).split('.').map(Number), pb = String(b).split('.').map(Number);
package/src/telemetry.js CHANGED
@@ -28,7 +28,12 @@ function optedOut() {
28
28
  }
29
29
 
30
30
  function appVersion() {
31
- try { return require('../package.json').version; } catch (e) { return '0.0.0'; }
31
+ // Running from a checkout, package.json is one level up. Inside the installed app
32
+ // bundle there is no package.json, so the installer stamps version.json beside the
33
+ // code. Try both before giving up.
34
+ try { return require('./version.json').version || '0.0.0'; } catch (e) {}
35
+ try { return require('../package.json').version || '0.0.0'; } catch (e) {}
36
+ return '0.0.0';
32
37
  }
33
38
 
34
39
  function dataDir() {