server-studio 1.1.1 → 1.2.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.
- package/CHANGELOG.md +85 -0
- package/README.md +88 -7
- package/bin/cli.js +3 -0
- package/dist/server-studio.plugin +0 -0
- package/package.json +2 -1
- package/skill/SKILL.md +3 -2
- package/skill/register-server.js +21 -2
- package/src/index.html +751 -45
- package/src/server.js +99 -0
- package/src/setport.js +82 -0
- package/src/telemetry.js +174 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes to Server Studio. Dates are release dates.
|
|
4
|
+
|
|
5
|
+
## 1.2.0 — 2026-08-30
|
|
6
|
+
|
|
7
|
+
### Folders replace categories
|
|
8
|
+
- The sidebar lists your folders. Click one to filter, click **All servers** to go back.
|
|
9
|
+
- Create, rename and delete folders. Deleting one never deletes servers, they move to **Unfiled**.
|
|
10
|
+
- Move a server with the folder button on its card, by dragging the card onto a folder, or from
|
|
11
|
+
the Folder field in Edit.
|
|
12
|
+
- Existing categories become folders automatically on first launch. Nothing is lost.
|
|
13
|
+
- `server-studio add --folder "Name"` files a server as it registers, creating the folder if new.
|
|
14
|
+
`--category` still works as an alias.
|
|
15
|
+
|
|
16
|
+
### Ports
|
|
17
|
+
- Two cards on one port now show a `port clash · fix` badge. Clicking it moves one to a free port.
|
|
18
|
+
- Where the dev script is one it recognises, it also writes the new port into that project's
|
|
19
|
+
`package.json`, showing the exact before and after first and keeping a backup. Anything it cannot
|
|
20
|
+
parse safely is refused rather than guessed at.
|
|
21
|
+
- Saving a server on a port another card already uses offers a free one instead.
|
|
22
|
+
- **Fixed:** `portOf()` only matched a port written after a colon, so a card saved as a bare `5173`
|
|
23
|
+
reported no port at all. That silently skipped both clash detection and the collision check.
|
|
24
|
+
|
|
25
|
+
### Cards
|
|
26
|
+
- Run and Stop are one button that follows the live status: green **Run**, red **Stop** once the
|
|
27
|
+
port answers, and a muted **Working** while it is being rechecked.
|
|
28
|
+
- **Fixed:** running a server with no project folder set executed the command in your home
|
|
29
|
+
directory, failing with something confusing like `Missing script: "dev"`. It now refuses, says so
|
|
30
|
+
on the card, and opens the folder field.
|
|
31
|
+
|
|
32
|
+
### Sidebar
|
|
33
|
+
- The version sits under the app name. When a newer release exists it turns green and becomes a
|
|
34
|
+
one-click update.
|
|
35
|
+
- A request box: send a message and an email address, both optional to use and easy to dismiss.
|
|
36
|
+
Dismissing it collapses it to a **Request a feature** button.
|
|
37
|
+
- A creator credit pinned to the bottom.
|
|
38
|
+
|
|
39
|
+
### Interface
|
|
40
|
+
- Dialogs are in-app and match the rest of the UI, instead of the browser's own
|
|
41
|
+
"localhost:4587 says" boxes. Escape, click-outside and Enter all work, and destructive actions
|
|
42
|
+
get a red button.
|
|
43
|
+
- Layout verified from 320px to 1920px with no horizontal overflow. On phones the sidebar
|
|
44
|
+
dissolves and reorders so your servers come before the request box.
|
|
45
|
+
|
|
46
|
+
### Telemetry and signups
|
|
47
|
+
- **Fixed:** an unreachable analytics endpoint added roughly 1.5 seconds to every install because
|
|
48
|
+
the ping held the process open. It no longer does.
|
|
49
|
+
- **Fixed:** on Node 18 the request was unref'd before a socket existed, which does not propagate on
|
|
50
|
+
that version, so the delay persisted there. The socket is unref'd too.
|
|
51
|
+
- Documented in the README, with `SERVER_STUDIO_NO_TELEMETRY=1` and `DO_NOT_TRACK` both honoured.
|
|
52
|
+
- Signups go to the author's own list, so the bundled Cloudflare Worker keeps only anonymous counts
|
|
53
|
+
and its open write route is gone.
|
|
54
|
+
|
|
55
|
+
## 1.1.1 — 2026-08-25
|
|
56
|
+
|
|
57
|
+
- Documentation only. Stated precisely what CI checks about the folder picker and what it cannot.
|
|
58
|
+
- No runtime change; the file list is identical to 1.1.0.
|
|
59
|
+
|
|
60
|
+
## 1.1.0 — 2026-08-25
|
|
61
|
+
|
|
62
|
+
- `npm install -g server-studio` now completes setup on its own. Installing as a project dependency
|
|
63
|
+
or under CI still touches nothing outside the package, and a failure prints a hint rather than
|
|
64
|
+
failing the install.
|
|
65
|
+
- Added `server-studio add`, which registers a server with no AI involved, so any editor, agent or
|
|
66
|
+
script can use it. The Claude Code skill wraps this command.
|
|
67
|
+
|
|
68
|
+
## 1.0.1 — 2026-08-25
|
|
69
|
+
|
|
70
|
+
- Made clear that installing the package is not the whole setup.
|
|
71
|
+
- **Fixed:** the path printed for the Cowork plugin pointed inside `node_modules`, which npx deletes
|
|
72
|
+
as soon as it finishes. The plugin is copied somewhere durable first.
|
|
73
|
+
|
|
74
|
+
## 1.0.0 — 2026-08-20
|
|
75
|
+
|
|
76
|
+
First public release.
|
|
77
|
+
|
|
78
|
+
- A local dashboard for your dev servers: run, stop, open, and live port status.
|
|
79
|
+
- One permanent port per project, with the rule documented per framework.
|
|
80
|
+
- Ships a Claude Code skill and a Cowork plugin.
|
|
81
|
+
- **Fixed before release:** the local server accepted cross-site form posts, so any page you had
|
|
82
|
+
open could have run shell commands through `/api/run`. Writes now require a JSON content type,
|
|
83
|
+
foreign origins are rejected and preflights refused.
|
|
84
|
+
- Cross-platform: a macOS app bundle, plus `server-studio start` on Windows and Linux. CI opens a
|
|
85
|
+
real terminal window and the native folder picker on each platform.
|
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ Click **Add server**. Only the name matters, everything else can be filled in la
|
|
|
85
85
|
|---|---|---|
|
|
86
86
|
| Name | Card title, and what you search by | `Acme Dashboard` |
|
|
87
87
|
| Project | A line of context for future you | `Client analytics UI` |
|
|
88
|
-
|
|
|
88
|
+
| Folder | Which folder it lives in, chosen from the sidebar list | `Web app` |
|
|
89
89
|
| Project folder | Where the run command executes. **Browse** opens a folder picker | `~/Sites/acme` |
|
|
90
90
|
| Run command | Exactly what you would type to start it | `npm run dev` |
|
|
91
91
|
| URL / Address | Where it serves. A bare port works and becomes `localhost:PORT` | `5181` |
|
|
@@ -98,19 +98,34 @@ Each card has five controls:
|
|
|
98
98
|
|
|
99
99
|
| Control | What happens |
|
|
100
100
|
|---|---|
|
|
101
|
-
| **Run** |
|
|
101
|
+
| **Run / Stop** | One button that follows the live status. Green **Run** opens a terminal, `cd`s into the folder and runs the command. Once the port answers it turns into a red **Stop**, which kills whatever holds that port |
|
|
102
102
|
| Open in browser | Opens the card's URL |
|
|
103
|
-
|
|
|
103
|
+
| Move to folder | Pick a folder, or drag the card onto one in the sidebar |
|
|
104
104
|
| Edit | Change any field |
|
|
105
105
|
| Delete | Removes the card. Does not touch the project itself |
|
|
106
106
|
|
|
107
|
+
If a card has no project folder set, **Run** refuses instead of running the command in your home
|
|
108
|
+
directory, and says so on the card.
|
|
109
|
+
|
|
107
110
|
The icon inside the URL box copies the address to your clipboard.
|
|
108
111
|
|
|
109
112
|
The dot next to the URL is live status: green means that port is currently accepting connections,
|
|
110
113
|
red means nothing is there. It re-checks every 12 seconds, and **Refresh status** forces it.
|
|
111
114
|
|
|
112
115
|
Click the star to pin a card to the top. The search box matches name, project, URL, command, tag,
|
|
113
|
-
|
|
116
|
+
folder and notes at once, so searching `wordpress` or `5181` or `vite` all find the right card.
|
|
117
|
+
|
|
118
|
+
### Folders
|
|
119
|
+
|
|
120
|
+
The sidebar lists your folders. Click one to see only its servers, click **All servers** to go
|
|
121
|
+
back. **New folder** creates one, and the pencil beside a folder renames it, or deletes it if you
|
|
122
|
+
clear the name. Deleting a folder never deletes servers, they move to **Unfiled**.
|
|
123
|
+
|
|
124
|
+
A server lives in one folder. Move it with the folder button on the card, by dragging the card onto
|
|
125
|
+
a folder in the sidebar, or from the Folder field in Edit.
|
|
126
|
+
|
|
127
|
+
Upgrading from an older version? Categories became folders automatically the first time you open
|
|
128
|
+
the dashboard, and nothing is lost.
|
|
114
129
|
|
|
115
130
|
### The one permanent port rule
|
|
116
131
|
|
|
@@ -131,6 +146,14 @@ makes a saved entry go stale. Use the strict option so it fails loudly instead:
|
|
|
131
146
|
|
|
132
147
|
Then put that same port in both the run command and the URL on the card.
|
|
133
148
|
|
|
149
|
+
**When two cards share a port**, both show a `port clash · fix` badge. Clicking it moves one of them
|
|
150
|
+
to a free port. If the project's dev script is one it recognises, it also writes the new port into
|
|
151
|
+
that project's `package.json`, showing you the exact before and after first and keeping a backup, so
|
|
152
|
+
the card and the project stay in agreement. Anything it cannot parse safely is refused rather than
|
|
153
|
+
guessed at, and only the card changes.
|
|
154
|
+
|
|
155
|
+
Adding a server on a port another card already uses offers you a free one before saving.
|
|
156
|
+
|
|
134
157
|
### Let Claude do it for you
|
|
135
158
|
|
|
136
159
|
If you installed the skill, you never have to add cards by hand. Ask Claude Code to build something
|
|
@@ -162,15 +185,24 @@ command with no AI involved, so any tool that can run a shell command can use it
|
|
|
162
185
|
Cursor, Copilot, Windsurf, a Makefile, a shell alias, or you:
|
|
163
186
|
|
|
164
187
|
```bash
|
|
165
|
-
server-studio add --name "Portfolio Site" --cwd "$PWD" --command "npm run dev" --assign
|
|
188
|
+
server-studio add --name "Portfolio Site" --cwd "$PWD" --command "npm run dev" --folder "Client work" --assign
|
|
166
189
|
```
|
|
167
190
|
|
|
168
191
|
`--assign` picks a port nothing else uses and prints `PORT <n>`, so a script can read that line and
|
|
169
|
-
write the port into the project config.
|
|
170
|
-
|
|
192
|
+
write the port into the project config. `--folder` files it under that folder, creating the folder if
|
|
193
|
+
it does not exist yet. Re-running for the same project updates its fields and keeps its port locked,
|
|
194
|
+
which is what makes this safe to call from a build step.
|
|
171
195
|
|
|
172
196
|
Run `server-studio add` with no arguments for the full list of fields.
|
|
173
197
|
|
|
198
|
+
Run `server-studio add` with no arguments for the full list of fields.
|
|
199
|
+
|
|
200
|
+
### Staying up to date
|
|
201
|
+
|
|
202
|
+
The version sits under the app name in the sidebar. When a newer release is on npm it turns green
|
|
203
|
+
and becomes a button: click it, confirm, and the update runs in a terminal window. Restart Server
|
|
204
|
+
Studio when it finishes. Your saved servers and folders are never touched by an update.
|
|
205
|
+
|
|
174
206
|
### Backups
|
|
175
207
|
|
|
176
208
|
**Export** saves your whole list to `server-studio-backup.json`. **Import** loads one back.
|
|
@@ -214,6 +246,38 @@ The one step no machine can do is click a folder and confirm, so the path that r
|
|
|
214
246
|
directory is exercised by hand rather than by CI. If Browse ever misbehaves on your system, type the
|
|
215
247
|
path into the field instead. Nothing else depends on it.
|
|
216
248
|
|
|
249
|
+
## Telemetry
|
|
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.
|
|
253
|
+
|
|
254
|
+
What is sent:
|
|
255
|
+
|
|
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
|
+
```
|
|
271
|
+
|
|
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.
|
|
275
|
+
|
|
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.
|
|
280
|
+
|
|
217
281
|
## Your data
|
|
218
282
|
|
|
219
283
|
Saved servers live in one file:
|
|
@@ -239,6 +303,19 @@ The app runs shell commands you saved, so it is deliberately locked to your own
|
|
|
239
303
|
Treat the run command on a card the way you would treat a line you are about to paste into your own
|
|
240
304
|
terminal, because that is exactly what it becomes.
|
|
241
305
|
|
|
306
|
+
## Forking this
|
|
307
|
+
|
|
308
|
+
Two values point at the author's own services. If you fork this, change them or your users'
|
|
309
|
+
signups arrive in someone else's inbox:
|
|
310
|
+
|
|
311
|
+
| What | Where | Why |
|
|
312
|
+
|---|---|---|
|
|
313
|
+
| `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
|
+
|
|
316
|
+
The creator credit in the sidebar is ordinary MIT-licensed source. You are free to change it,
|
|
317
|
+
same as any other part.
|
|
318
|
+
|
|
242
319
|
## Development
|
|
243
320
|
|
|
244
321
|
```bash
|
|
@@ -259,6 +336,10 @@ SERVER_STUDIO_DATA_DIR=/tmp/ss-test node src/server.js
|
|
|
259
336
|
code. On macOS the installer copies it into the app bundle, so the bundle never holds a stale
|
|
260
337
|
version.
|
|
261
338
|
|
|
339
|
+
## Changelog
|
|
340
|
+
|
|
341
|
+
See [CHANGELOG.md](CHANGELOG.md).
|
|
342
|
+
|
|
262
343
|
## License
|
|
263
344
|
|
|
264
345
|
MIT
|
package/bin/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ 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');
|
|
9
10
|
|
|
10
11
|
const ROOT = path.join(__dirname, '..');
|
|
11
12
|
const APP_NAME = 'Server Studio.app';
|
|
@@ -109,6 +110,8 @@ function install() {
|
|
|
109
110
|
console.log('\nUsing Claude Cowork? Install the plugin by opening this file:');
|
|
110
111
|
console.log(' ' + pluginFile);
|
|
111
112
|
}
|
|
113
|
+
|
|
114
|
+
telemetry.ping('install'); // counts a real install (never fires on --dry-run: it returns above)
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
/* ---------- uninstall ---------- */
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "server-studio",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"skill",
|
|
32
32
|
"plugin",
|
|
33
33
|
"dist",
|
|
34
|
+
"CHANGELOG.md",
|
|
34
35
|
"README.md",
|
|
35
36
|
"LICENSE"
|
|
36
37
|
],
|
package/skill/SKILL.md
CHANGED
|
@@ -49,7 +49,7 @@ line it prints and bake that port into the project config (see the golden rule a
|
|
|
49
49
|
node "$HOME/.claude/skills/server-studio/register-server.js" \
|
|
50
50
|
--name "Project Name" \
|
|
51
51
|
--project "What it's for" \
|
|
52
|
-
--
|
|
52
|
+
--folder "WordPress" \
|
|
53
53
|
--cwd "/absolute/path/to/project" \
|
|
54
54
|
--command "npm run dev -- --port 3001 --strictPort" \
|
|
55
55
|
--url "localhost:3001" \
|
|
@@ -61,7 +61,8 @@ node "$HOME/.claude/skills/server-studio/register-server.js" \
|
|
|
61
61
|
Field guide:
|
|
62
62
|
- `--name` — short label, e.g. "Portfolio Site"
|
|
63
63
|
- `--project` — the project/task it belongs to
|
|
64
|
-
- `--
|
|
64
|
+
- `--folder` — folder name to file it under, created if it does not exist yet (shown in the sidebar).
|
|
65
|
+
`--category` still works as an alias for older callers.
|
|
65
66
|
- `--cwd` — absolute project folder; the run command executes here
|
|
66
67
|
- `--command` — the exact command that starts the server (this is what the Run button runs)
|
|
67
68
|
- `--url` — address or bare port (`3000` works, becomes `localhost:3000`)
|
package/skill/register-server.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// No npm deps. Usage:
|
|
7
7
|
// node register-server.js --name "Portfolio Site" --project "Client redesign" \
|
|
8
8
|
// --cwd "/Users/you/Sites/portfolio" --command "npm run dev" --url "localhost:3001" \
|
|
9
|
-
// --
|
|
9
|
+
// --folder "WordPress" --tag "Vite" --note "admin: ninja/ninja"
|
|
10
10
|
//
|
|
11
11
|
// # let it pick a fresh, never-used port for a NEW project:
|
|
12
12
|
// node register-server.js --name "New Site" --cwd "/path" --command "npm run dev" --assign
|
|
@@ -29,6 +29,7 @@ const DATA_DIR = (function () {
|
|
|
29
29
|
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'server-studio');
|
|
30
30
|
})();
|
|
31
31
|
const DATA_FILE = path.join(DATA_DIR, 'data.json');
|
|
32
|
+
const FOLDERS_FILE = path.join(DATA_DIR, 'folders.json');
|
|
32
33
|
const BASE_PORT = 3001; // assigned ports start here and go up
|
|
33
34
|
|
|
34
35
|
// ---- parse --flag "value" args ----
|
|
@@ -50,7 +51,7 @@ function portFromUrl(url) {
|
|
|
50
51
|
const entry = {
|
|
51
52
|
name: args.name || '',
|
|
52
53
|
project: args.project || '',
|
|
53
|
-
|
|
54
|
+
folder: resolveFolder(args.folder || args.category),
|
|
54
55
|
cwd: args.cwd || '',
|
|
55
56
|
command: args.command || '',
|
|
56
57
|
url: typeof args.url === 'string' ? args.url : '',
|
|
@@ -70,6 +71,24 @@ try { data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')) || []; } catch (e) {
|
|
|
70
71
|
if (!Array.isArray(data)) data = [];
|
|
71
72
|
|
|
72
73
|
function uid() { return 's_' + Math.random().toString(36).slice(2, 9) + Date.now().toString(36); }
|
|
74
|
+
|
|
75
|
+
// The app groups by folder id. Take a folder NAME on the command line and resolve it,
|
|
76
|
+
// creating the folder if it does not exist yet, so a registered server lands in the
|
|
77
|
+
// right place instead of sitting unfiled.
|
|
78
|
+
function resolveFolder(name) {
|
|
79
|
+
const wanted = String(name || '').trim();
|
|
80
|
+
if (!wanted) return '';
|
|
81
|
+
let folders = [];
|
|
82
|
+
try { folders = JSON.parse(fs.readFileSync(FOLDERS_FILE, 'utf8')) || []; } catch (e) { folders = []; }
|
|
83
|
+
if (!Array.isArray(folders)) folders = [];
|
|
84
|
+
const hit = folders.find(f => String(f.name || '').toLowerCase() === wanted.toLowerCase());
|
|
85
|
+
if (hit) return hit.id;
|
|
86
|
+
const f = { id: 'f_' + Math.random().toString(36).slice(2, 9), name: wanted };
|
|
87
|
+
folders.push(f);
|
|
88
|
+
try { fs.mkdirSync(DATA_DIR, { recursive: true }); fs.writeFileSync(FOLDERS_FILE, JSON.stringify(folders, null, 2)); }
|
|
89
|
+
catch (e) { return ''; }
|
|
90
|
+
return f.id;
|
|
91
|
+
}
|
|
73
92
|
const norm = s => String(s || '').trim().toLowerCase();
|
|
74
93
|
function usedPorts(excludeId) {
|
|
75
94
|
return new Set(data.filter(s => s.id !== excludeId).map(s => portFromUrl(s.url)).filter(Boolean));
|