server-studio 1.2.3 → 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 +34 -0
- package/README.md +15 -27
- package/bin/cli.js +0 -3
- package/package.json +1 -1
- package/src/platform.js +10 -4
- package/src/server.js +2 -3
- package/src/telemetry.js +15 -130
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
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
|
+
|
|
30
|
+
## 1.2.4 — 2026-08-30
|
|
31
|
+
|
|
32
|
+
- **Fixed:** on Windows, stopping a server could kill an unrelated process. The port was matched
|
|
33
|
+
with `findstr :5173`, which also matches `0.0.0.0:51730`, so a longer port sharing the prefix was
|
|
34
|
+
caught too. Reported and patched by @anupamme in #1.
|
|
35
|
+
- `killPort` no longer builds a shell command at all, on any platform. The port was already
|
|
36
|
+
sanitised by `Number()`, so this is hardening rather than a fix.
|
|
37
|
+
|
|
5
38
|
## 1.2.3 — 2026-08-30
|
|
6
39
|
|
|
7
40
|
- The badge shown after an update now says **quit & reopen** rather than "restart to finish".
|
|
@@ -73,6 +106,7 @@ Notable changes to Server Studio. Dates are release dates.
|
|
|
73
106
|
- Documented in the README, with `SERVER_STUDIO_NO_TELEMETRY=1` and `DO_NOT_TRACK` both honoured.
|
|
74
107
|
- Signups go to the author's own list, so the bundled Cloudflare Worker keeps only anonymous counts
|
|
75
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.
|
|
76
110
|
|
|
77
111
|
## 1.1.1 — 2026-08-25
|
|
78
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
|
-
##
|
|
249
|
+
## What it sends
|
|
250
250
|
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
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
|
-
|
|
273
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
package/src/platform.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { execFile
|
|
7
|
+
const { execFile } = require('child_process');
|
|
8
8
|
|
|
9
9
|
const WIN = process.platform === 'win32';
|
|
10
10
|
const MAC = process.platform === 'darwin';
|
|
@@ -86,15 +86,21 @@ function killPort(port, cb) {
|
|
|
86
86
|
if (!p) return cb(new Error('bad port'));
|
|
87
87
|
if (WIN) {
|
|
88
88
|
// netstat lists the owning PID in the last column of LISTENING rows.
|
|
89
|
-
|
|
89
|
+
// execFile (no shell) avoids ever building a shell command string from `port`.
|
|
90
|
+
return execFile('netstat', ['-ano', '-p', 'tcp'], (err, stdout) => {
|
|
90
91
|
const pids = new Set(String(stdout || '').trim().split(/\r?\n/)
|
|
92
|
+
.filter(l => /LISTENING/.test(l) && l.includes(':' + p + ' '))
|
|
91
93
|
.map(l => l.trim().split(/\s+/).pop())
|
|
92
94
|
.filter(x => /^\d+$/.test(x) && x !== '0'));
|
|
93
95
|
if (!pids.size) return cb(null);
|
|
94
|
-
|
|
96
|
+
execFile('taskkill', ['/T', '/F', ...[...pids].flatMap(id => ['/PID', id])], () => cb(null));
|
|
95
97
|
});
|
|
96
98
|
}
|
|
97
|
-
return
|
|
99
|
+
return execFile('lsof', ['-nti', 'tcp:' + p], (err, stdout) => {
|
|
100
|
+
const pids = String(stdout || '').trim().split(/\r?\n/).filter(Boolean);
|
|
101
|
+
if (!pids.length) return cb(null);
|
|
102
|
+
execFile('kill', pids, () => cb(null));
|
|
103
|
+
});
|
|
98
104
|
}
|
|
99
105
|
|
|
100
106
|
/* ---------- native "choose a folder" dialog ---------- */
|
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 };
|