server-studio 1.2.4 → 1.2.6
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 +36 -0
- package/README.md +22 -29
- package/bin/cli.js +0 -3
- package/package.json +1 -1
- package/src/server.js +2 -3
- package/src/telemetry.js +15 -130
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to Server Studio. Dates are release dates.
|
|
4
4
|
|
|
5
|
+
## 1.2.6 — 2026-08-30
|
|
6
|
+
|
|
7
|
+
- Documentation only. No runtime change; the code is identical to 1.2.5.
|
|
8
|
+
- The README now says that anyone on **1.1.1 or older** has no update button, because the version
|
|
9
|
+
badge and the update check both arrived in 1.2.0. They update once with
|
|
10
|
+
`npm install -g server-studio` and the badge takes over from there. This is published as a release
|
|
11
|
+
because npm renders the README from the package, so the note was missing on the page most people
|
|
12
|
+
land on.
|
|
13
|
+
- Removed a line that appeared twice.
|
|
14
|
+
|
|
15
|
+
## 1.2.5 — 2026-08-30
|
|
16
|
+
|
|
17
|
+
### The install counter is gone
|
|
18
|
+
- **Removed:** the anonymous install counter added in 1.2.0. It was never switched on. The
|
|
19
|
+
endpoint came from `SERVER_STUDIO_ANALYTICS_URL`, which had no default, so no published build
|
|
20
|
+
ever sent a ping and the collector was never deployed. Removed outright rather than left
|
|
21
|
+
dormant, along with the bundled Cloudflare Worker, both opt-out variables and the README
|
|
22
|
+
section describing it.
|
|
23
|
+
- Server Studio now makes exactly two outbound requests: a public read of
|
|
24
|
+
`registry.npmjs.org` to check for updates, which sends nothing about you, and the sidebar
|
|
25
|
+
email box, which is silent until you type an address and press send. The README says so
|
|
26
|
+
precisely.
|
|
27
|
+
- The sidebar email box is untouched. It never depended on any of this.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- **Fixed:** `telemetry.js` carried a `req.unref()` call that had never run once. `ClientRequest`
|
|
31
|
+
has no such method, so the `typeof` guard silently skipped it on every Node version. The socket
|
|
32
|
+
unref beside it was doing all the work. The comments claiming it was a Node 18 workaround were
|
|
33
|
+
wrong on both counts.
|
|
34
|
+
- **Fixed:** the install-timing check in CI failed at random on macOS. It pointed at a black-hole
|
|
35
|
+
IP and hoped the connection would hang, which depends entirely on the network, and it inherited
|
|
36
|
+
`CI=true`, under which the ping did not fire at all, so it was timing two identical runs. It now
|
|
37
|
+
uses a local listener that accepts and never answers. That check retired with the counter it
|
|
38
|
+
tested.
|
|
39
|
+
|
|
5
40
|
## 1.2.4 — 2026-08-30
|
|
6
41
|
|
|
7
42
|
- **Fixed:** on Windows, stopping a server could kill an unrelated process. The port was matched
|
|
@@ -81,6 +116,7 @@ Notable changes to Server Studio. Dates are release dates.
|
|
|
81
116
|
- Documented in the README, with `SERVER_STUDIO_NO_TELEMETRY=1` and `DO_NOT_TRACK` both honoured.
|
|
82
117
|
- Signups go to the author's own list, so the bundled Cloudflare Worker keeps only anonymous counts
|
|
83
118
|
and its open write route is gone.
|
|
119
|
+
- **Superseded by 1.2.5:** none of this counting was ever active, and it was removed entirely.
|
|
84
120
|
|
|
85
121
|
## 1.1.1 — 2026-08-25
|
|
86
122
|
|
package/README.md
CHANGED
|
@@ -195,14 +195,19 @@ which is what makes this safe to call from a build step.
|
|
|
195
195
|
|
|
196
196
|
Run `server-studio add` with no arguments for the full list of fields.
|
|
197
197
|
|
|
198
|
-
Run `server-studio add` with no arguments for the full list of fields.
|
|
199
|
-
|
|
200
198
|
### Staying up to date
|
|
201
199
|
|
|
202
200
|
The version sits under the app name in the sidebar. When a newer release is on npm it turns green
|
|
203
201
|
and becomes a button: click it, confirm, and the update runs in a terminal window. Restart Server
|
|
204
202
|
Studio when it finishes. Your saved servers and folders are never touched by an update.
|
|
205
203
|
|
|
204
|
+
Coming from **1.1.1 or older**? That badge did not exist yet, so there is no button in your copy.
|
|
205
|
+
Update once by hand and every update after that is a click:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
npm install -g server-studio
|
|
209
|
+
```
|
|
210
|
+
|
|
206
211
|
### Backups
|
|
207
212
|
|
|
208
213
|
**Export** saves your whole list to `server-studio-backup.json`. **Import** loads one back.
|
|
@@ -246,37 +251,26 @@ The one step no machine can do is click a folder and confirm, so the path that r
|
|
|
246
251
|
directory is exercised by hand rather than by CI. If Browse ever misbehaves on your system, type the
|
|
247
252
|
path into the field instead. Nothing else depends on it.
|
|
248
253
|
|
|
249
|
-
##
|
|
254
|
+
## What it sends
|
|
250
255
|
|
|
251
|
-
|
|
252
|
-
|
|
256
|
+
No usage tracking, no analytics, no account, no profile. Server Studio makes exactly two
|
|
257
|
+
outbound requests, and neither carries anything about you that you did not type yourself.
|
|
253
258
|
|
|
254
|
-
|
|
259
|
+
**The update check.** On launch it fetches `registry.npmjs.org/server-studio/latest` to see
|
|
260
|
+
whether a newer version exists. That is a plain read of a public URL, the same one `npm view`
|
|
261
|
+
hits. No id, no version of yours, nothing identifying is sent.
|
|
255
262
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
```
|
|
263
|
+
**The email box in the sidebar.** Entirely opt-in and completely silent unless you type an
|
|
264
|
+
address and press send. When you do, that address and your message go to `mahdicreates.com`,
|
|
265
|
+
the author's own site, so he can reply and let you know when something ships. Nothing else is
|
|
266
|
+
attached.
|
|
271
267
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
network costs you no time at all.
|
|
268
|
+
That is the whole list. Your projects, folder paths, run commands, ports and notes never leave
|
|
269
|
+
your machine, and there is no install counter of any kind.
|
|
275
270
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
something new ships. Nothing else about you is attached.
|
|
271
|
+
> Versions 1.2.0 to 1.2.4 shipped an anonymous install counter that was never switched on.
|
|
272
|
+
> The endpoint came from an environment variable with no default, so no published build ever
|
|
273
|
+
> sent a ping and no collector was ever deployed. It was removed outright in 1.2.5.
|
|
280
274
|
|
|
281
275
|
## Your data
|
|
282
276
|
|
|
@@ -311,7 +305,6 @@ signups arrive in someone else's inbox:
|
|
|
311
305
|
| What | Where | Why |
|
|
312
306
|
|---|---|---|
|
|
313
307
|
| `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
308
|
|
|
316
309
|
The creator credit in the sidebar is ordinary MIT-licensed source. You are free to change it,
|
|
317
310
|
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
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)
|
|
203
|
-
// user
|
|
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
|
-
//
|
|
1
|
+
// The email signup / "ask" box. Built-ins only, no dependencies.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
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 = {
|
|
64
|
+
module.exports = { subscribe, subscribeEnabled: !!SUBSCRIBE_URL };
|