server-studio 1.2.4 → 1.2.5

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,31 @@
2
2
 
3
3
  Notable changes to Server Studio. Dates are release dates.
4
4
 
5
+ ## 1.2.5 — 2026-08-30
6
+
7
+ ### The install counter is gone
8
+ - **Removed:** the anonymous install counter added in 1.2.0. It was never switched on. The
9
+ endpoint came from `SERVER_STUDIO_ANALYTICS_URL`, which had no default, so no published build
10
+ ever sent a ping and the collector was never deployed. Removed outright rather than left
11
+ dormant, along with the bundled Cloudflare Worker, both opt-out variables and the README
12
+ section describing it.
13
+ - Server Studio now makes exactly two outbound requests: a public read of
14
+ `registry.npmjs.org` to check for updates, which sends nothing about you, and the sidebar
15
+ email box, which is silent until you type an address and press send. The README says so
16
+ precisely.
17
+ - The sidebar email box is untouched. It never depended on any of this.
18
+
19
+ ### Fixed
20
+ - **Fixed:** `telemetry.js` carried a `req.unref()` call that had never run once. `ClientRequest`
21
+ has no such method, so the `typeof` guard silently skipped it on every Node version. The socket
22
+ unref beside it was doing all the work. The comments claiming it was a Node 18 workaround were
23
+ wrong on both counts.
24
+ - **Fixed:** the install-timing check in CI failed at random on macOS. It pointed at a black-hole
25
+ IP and hoped the connection would hang, which depends entirely on the network, and it inherited
26
+ `CI=true`, under which the ping did not fire at all, so it was timing two identical runs. It now
27
+ uses a local listener that accepts and never answers. That check retired with the counter it
28
+ tested.
29
+
5
30
  ## 1.2.4 — 2026-08-30
6
31
 
7
32
  - **Fixed:** on Windows, stopping a server could kill an unrelated process. The port was matched
@@ -81,6 +106,7 @@ Notable changes to Server Studio. Dates are release dates.
81
106
  - Documented in the README, with `SERVER_STUDIO_NO_TELEMETRY=1` and `DO_NOT_TRACK` both honoured.
82
107
  - Signups go to the author's own list, so the bundled Cloudflare Worker keeps only anonymous counts
83
108
  and its open write route is gone.
109
+ - **Superseded by 1.2.5:** none of this counting was ever active, and it was removed entirely.
84
110
 
85
111
  ## 1.1.1 — 2026-08-25
86
112
 
package/README.md CHANGED
@@ -246,37 +246,26 @@ The one step no machine can do is click a folder and confirm, so the path that r
246
246
  directory is exercised by hand rather than by CI. If Browse ever misbehaves on your system, type the
247
247
  path into the field instead. Nothing else depends on it.
248
248
 
249
- ## Telemetry
249
+ ## What it sends
250
250
 
251
- Server Studio sends one anonymous ping when it is installed and one when it launches, so
252
- installs can be counted. That is the whole of it.
251
+ No usage tracking, no analytics, no account, no profile. Server Studio makes exactly two
252
+ outbound requests, and neither carries anything about you that you did not type yourself.
253
253
 
254
- What is sent:
254
+ **The update check.** On launch it fetches `registry.npmjs.org/server-studio/latest` to see
255
+ whether a newer version exists. That is a plain read of a public URL, the same one `npm view`
256
+ hits. No id, no version of yours, nothing identifying is sent.
255
257
 
256
- | Field | Example | Why |
257
- |---|---|---|
258
- | A random id | `9f3c...` | Generated on your machine, stored in your data folder. Lets two launches from one machine count as one user |
259
- | Version | `1.2.0` | Which versions are still in use |
260
- | OS, arch | `darwin`, `arm64` | Which platforms to test on |
261
- | Node major | `22` | Which Node versions to keep supporting |
262
-
263
- What is never sent: your projects, folder paths, run commands, ports, notes, IP-derived
264
- location, or anything you typed into the app. There is no account and no profile.
265
-
266
- Turn it off completely, before or after installing:
267
-
268
- ```bash
269
- export SERVER_STUDIO_NO_TELEMETRY=1
270
- ```
258
+ **The email box in the sidebar.** Entirely opt-in and completely silent unless you type an
259
+ address and press send. When you do, that address and your message go to `mahdicreates.com`,
260
+ the author's own site, so he can reply and let you know when something ships. Nothing else is
261
+ attached.
271
262
 
272
- The standard `DO_NOT_TRACK=1` is honoured too, and nothing is sent under CI. The ping never
273
- blocks anything: it is fired without holding the process open, so a blocked or offline
274
- network costs you no time at all.
263
+ That is the whole list. Your projects, folder paths, run commands, ports and notes never leave
264
+ your machine, and there is no install counter of any kind.
275
265
 
276
- The email box in the sidebar is separate and entirely opt-in. Nothing is sent from it unless
277
- you type an address and press send. When you do, your address and your message go to
278
- `mahdicreates.com`, which is the author's site, so he can reply and let you know when
279
- something new ships. Nothing else about you is attached.
266
+ > Versions 1.2.0 to 1.2.4 shipped an anonymous install counter that was never switched on.
267
+ > The endpoint came from an environment variable with no default, so no published build ever
268
+ > sent a ping and no collector was ever deployed. It was removed outright in 1.2.5.
280
269
 
281
270
  ## Your data
282
271
 
@@ -311,7 +300,6 @@ signups arrive in someone else's inbox:
311
300
  | What | Where | Why |
312
301
  |---|---|---|
313
302
  | `SERVER_STUDIO_SUBSCRIBE_URL` | `src/telemetry.js` | Defaults to `mahdicreates.com`. Point it at your own endpoint, or set it empty to remove the sidebar box entirely |
314
- | `SERVER_STUDIO_ANALYTICS_URL` | unset by default | Install counting is inert unless you set this to your own collector |
315
303
 
316
304
  The creator credit in the sidebar is ordinary MIT-licensed source. You are free to change it,
317
305
  same as any other part.
package/bin/cli.js CHANGED
@@ -6,7 +6,6 @@ const fs = require('fs');
6
6
  const os = require('os');
7
7
  const path = require('path');
8
8
  const { execFileSync } = require('child_process');
9
- const telemetry = require('../src/telemetry');
10
9
 
11
10
  const ROOT = path.join(__dirname, '..');
12
11
  const APP_NAME = 'Server Studio.app';
@@ -118,8 +117,6 @@ function install() {
118
117
  console.log('\nUsing Claude Cowork? Install the plugin by opening this file:');
119
118
  console.log(' ' + pluginFile);
120
119
  }
121
-
122
- telemetry.ping('install'); // counts a real install (never fires on --dry-run: it returns above)
123
120
  }
124
121
 
125
122
  /* ---------- uninstall ---------- */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "server-studio",
3
- "version": "1.2.4",
3
+ "version": "1.2.5",
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/server.js CHANGED
@@ -199,8 +199,8 @@ const server = http.createServer(async (req, res) => {
199
199
  });
200
200
  }
201
201
 
202
- // Optional email signup. The browser posts here (same-origin), and we forward the
203
- // user's chosen email to the remote counter. Inert if no analytics URL is set.
202
+ // Optional email signup. The browser posts here (same-origin) and we forward the
203
+ // address the user typed to the signup endpoint. Nothing is sent until they press send.
204
204
  if (req.method === 'POST' && url === '/api/subscribe') {
205
205
  const b = await body(req);
206
206
  if (!b) return send(res, 415, { error: 'expected application/json' });
@@ -277,7 +277,6 @@ const server = http.createServer(async (req, res) => {
277
277
  ensureStore();
278
278
  server.listen(PORT, '127.0.0.1', () => {
279
279
  console.log('Server Studio running on http://localhost:' + PORT);
280
- telemetry.ping('start'); // one anonymous count per real launch (app or CLI)
281
280
  plat.openExternal('http://localhost:' + PORT + '/', () => {});
282
281
  });
283
282
  server.on('error', (e) => {
package/src/telemetry.js CHANGED
@@ -1,118 +1,17 @@
1
- // Anonymous usage counter + optional email signup. Built-ins only, no dependencies.
1
+ // The email signup / "ask" box. Built-ins only, no dependencies.
2
2
  //
3
- // Two things live here, and they are very different:
4
- // ping(event) anonymous, opt-out, fire-and-forget. A random id + OS/version.
5
- // subscribe(email) an explicit, user-typed signup. Sends the email the user chose
6
- // to give. Never automatic, never affected by the opt-out flag.
3
+ // This file used to also hold an anonymous install counter. It was never switched on:
4
+ // the endpoint came from an env var with no default, so no build ever sent a ping, and
5
+ // the Cloudflare Worker meant to receive them was never deployed. It is gone rather
6
+ // than left dormant. Server Studio now makes exactly one outbound request, and only
7
+ // when someone types a message and presses send.
7
8
  //
8
- // Both are INERT until SERVER_STUDIO_ANALYTICS_URL points at your Worker origin, e.g.
9
- // SERVER_STUDIO_ANALYTICS_URL=https://server-studio-analytics.you.workers.dev
10
- // The code appends /collect and /subscribe itself.
9
+ // subscribe(email, message) explicit, user-typed. Never automatic.
11
10
  'use strict';
12
11
 
13
- const path = require('path');
14
- const crypto = require('crypto');
15
- const fs = require('fs');
16
-
17
- const BASE = (process.env.SERVER_STUDIO_ANALYTICS_URL || '').replace(/\/+$/, '');
18
-
19
- // Signups go to the site's own subscriber list, not to the counter. They are two
20
- // separate things: one is an anonymous tally, this is a person asking to be emailed.
21
- // Overridable so a fork points somewhere else instead of at this list.
12
+ // Overridable so a fork points at its own list instead of this one.
22
13
  const SUBSCRIBE_URL = process.env.SERVER_STUDIO_SUBSCRIBE_URL || 'https://www.mahdicreates.com/api/subscribe';
23
14
 
24
- function optedOut() {
25
- if (process.env.SERVER_STUDIO_NO_TELEMETRY) return true;
26
- const dnt = String(process.env.DO_NOT_TRACK || '').toLowerCase();
27
- return dnt === '1' || dnt === 'true';
28
- }
29
-
30
- function appVersion() {
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';
37
- }
38
-
39
- function dataDir() {
40
- try { return require('./platform').dataDir(); }
41
- catch (e) {
42
- const os = require('os');
43
- if (process.env.SERVER_STUDIO_DATA_DIR) return process.env.SERVER_STUDIO_DATA_DIR;
44
- if (process.platform === 'darwin') return path.join(os.homedir(), 'Library', 'Application Support', 'Server Studio');
45
- if (process.platform === 'win32') return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Server Studio');
46
- return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'server-studio');
47
- }
48
- }
49
-
50
- // { id, fresh }. `fresh` is true the first time an id is minted.
51
- function installId() {
52
- const dir = dataDir();
53
- const file = path.join(dir, 'analytics-id');
54
- try {
55
- const existing = fs.readFileSync(file, 'utf8').trim();
56
- if (existing) return { id: existing, fresh: false };
57
- } catch (e) { /* not created yet */ }
58
- const id = crypto.randomUUID();
59
- try {
60
- fs.mkdirSync(dir, { recursive: true });
61
- fs.writeFileSync(file, id + '\n');
62
- return { id, fresh: true };
63
- } catch (e) {
64
- return { id, fresh: false }; // read-only home: still count, just cannot dedupe
65
- }
66
- }
67
-
68
- function notice() {
69
- let link = '';
70
- try { link = (require('../package.json').homepage || '').split('#')[0]; } catch (e) {}
71
- console.error(
72
- 'Server Studio sends one anonymous ping (a random id + your OS and version, no\n' +
73
- 'personal data) so installs can be counted. Turn it off any time with\n' +
74
- ' SERVER_STUDIO_NO_TELEMETRY=1' + (link ? ' · ' + link + '#telemetry' : '')
75
- );
76
- }
77
-
78
- // Low-level POST. Resolves { ok } and never rejects.
79
- function post(pathname, obj, timeout, keepAlive) {
80
- return new Promise(resolve => {
81
- try {
82
- if (!BASE) return resolve({ ok: false });
83
- const payload = JSON.stringify(obj);
84
- const u = new URL(BASE + pathname);
85
- const client = u.protocol === 'http:' ? require('http') : require('https');
86
- const req = client.request({
87
- hostname: u.hostname,
88
- port: u.port || (u.protocol === 'http:' ? 80 : 443),
89
- path: u.pathname,
90
- method: 'POST',
91
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
92
- timeout: timeout || 1500,
93
- }, res => {
94
- res.resume(); // drain
95
- resolve({ ok: res.statusCode >= 200 && res.statusCode < 300 });
96
- });
97
- req.on('error', () => resolve({ ok: false }));
98
- req.on('timeout', () => { req.destroy(); resolve({ ok: false }); });
99
- // Never keep the process alive for a ping. On a network that cannot reach the
100
- // endpoint this is the difference between exiting now and waiting out the
101
- // timeout. A ping that would not have arrived is not worth a second of anyone's
102
- // install. Subscribe passes keepAlive because a person is waiting on the answer.
103
- //
104
- // The socket has to be unref'd as well as the request. On Node 18 unref'ing the
105
- // request before a socket exists does not propagate, and the process sat there
106
- // for seconds on a blocked network.
107
- if (!keepAlive) {
108
- req.on('socket', sock => { if (sock && typeof sock.unref === 'function') sock.unref(); });
109
- if (typeof req.unref === 'function') req.unref();
110
- }
111
- req.end(payload);
112
- } catch (e) { resolve({ ok: false }); }
113
- });
114
- }
115
-
116
15
  // POST to an absolute URL and hand back the parsed body. Never rejects.
117
16
  function postTo(absUrl, obj, timeout) {
118
17
  return new Promise(resolve => {
@@ -138,28 +37,12 @@ function postTo(absUrl, obj, timeout) {
138
37
  });
139
38
  req.on('error', () => resolve({ ok: false, json: null }));
140
39
  req.on('timeout', () => { req.destroy(); resolve({ ok: false, json: null }); });
40
+ // Deliberately not unref'd: a person pressed send and is waiting on the answer.
141
41
  req.end(payload);
142
42
  } catch (e) { resolve({ ok: false, json: null }); }
143
43
  });
144
44
  }
145
45
 
146
- // Anonymous, fire-and-forget.
147
- function ping(event) {
148
- try {
149
- if (!BASE || optedOut() || process.env.CI) return;
150
- const { id, fresh } = installId();
151
- if (fresh) { try { notice(); } catch (e) {} }
152
- post('/collect', {
153
- event: String(event || 'ping'),
154
- id,
155
- v: appVersion(),
156
- os: process.platform,
157
- arch: process.arch,
158
- node: process.versions.node.split('.')[0],
159
- }, 1500);
160
- } catch (e) { /* best-effort, always */ }
161
- }
162
-
163
46
  // Explicit signup. Returns a Promise<boolean> so the UI can confirm.
164
47
  async function subscribe(email, message) {
165
48
  try {
@@ -169,11 +52,13 @@ async function subscribe(email, message) {
169
52
  const body = { email: e };
170
53
  const m = String(message || '').trim().slice(0, 2000);
171
54
  if (m) body.message = m;
172
- // Deliberately not gated on the telemetry opt-out: typing an address and pressing
173
- // send is an explicit request, not tracking, and refusing it would be wrong.
174
- const r = await postTo(SUBSCRIBE_URL, body, 6000);
55
+ // 6s was not enough. The endpoint fans out to WordPress, FluentCRM and an SMTP
56
+ // relay, and a cold function walked past it, so a first-time sender saw a silent
57
+ // failure on the one attempt they were ever going to make. Warm it answers in
58
+ // about 2s. A person is watching a spinner, so waiting is better than lying.
59
+ const r = await postTo(SUBSCRIBE_URL, body, 15000);
175
60
  return !!(r && r.ok && r.json && r.json.success);
176
61
  } catch (e) { return false; }
177
62
  }
178
63
 
179
- module.exports = { ping, subscribe, enabled: !!BASE, subscribeEnabled: !!SUBSCRIBE_URL };
64
+ module.exports = { subscribe, subscribeEnabled: !!SUBSCRIBE_URL };