server-studio 1.1.1 → 1.2.1

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 ADDED
@@ -0,0 +1,95 @@
1
+ # Changelog
2
+
3
+ Notable changes to Server Studio. Dates are release dates.
4
+
5
+ ## 1.2.1 — 2026-08-30
6
+
7
+ - **Fixed:** an installed app reported its version as `0.0.0`, because the code looks for
8
+ `package.json` one level up and the app bundle does not contain one. The badge showed `v0.0.0`,
9
+ the update prompt appeared permanently even straight after updating, and telemetry recorded every
10
+ install as version 0.0.0. The installer now stamps the version beside the code.
11
+ - **Fixed:** if the local server was not responding, clicking **Update**, **Run** or **Stop** did
12
+ nothing at all. The failed request threw and was never caught, so there was no error either. Those
13
+ actions now say what went wrong.
14
+
15
+ ## 1.2.0 — 2026-08-30
16
+
17
+ ### Folders replace categories
18
+ - The sidebar lists your folders. Click one to filter, click **All servers** to go back.
19
+ - Create, rename and delete folders. Deleting one never deletes servers, they move to **Unfiled**.
20
+ - Move a server with the folder button on its card, by dragging the card onto a folder, or from
21
+ the Folder field in Edit.
22
+ - Existing categories become folders automatically on first launch. Nothing is lost.
23
+ - `server-studio add --folder "Name"` files a server as it registers, creating the folder if new.
24
+ `--category` still works as an alias.
25
+
26
+ ### Ports
27
+ - Two cards on one port now show a `port clash · fix` badge. Clicking it moves one to a free port.
28
+ - Where the dev script is one it recognises, it also writes the new port into that project's
29
+ `package.json`, showing the exact before and after first and keeping a backup. Anything it cannot
30
+ parse safely is refused rather than guessed at.
31
+ - Saving a server on a port another card already uses offers a free one instead.
32
+ - **Fixed:** `portOf()` only matched a port written after a colon, so a card saved as a bare `5173`
33
+ reported no port at all. That silently skipped both clash detection and the collision check.
34
+
35
+ ### Cards
36
+ - Run and Stop are one button that follows the live status: green **Run**, red **Stop** once the
37
+ port answers, and a muted **Working** while it is being rechecked.
38
+ - **Fixed:** running a server with no project folder set executed the command in your home
39
+ directory, failing with something confusing like `Missing script: "dev"`. It now refuses, says so
40
+ on the card, and opens the folder field.
41
+
42
+ ### Sidebar
43
+ - The version sits under the app name. When a newer release exists it turns green and becomes a
44
+ one-click update.
45
+ - A request box: send a message and an email address, both optional to use and easy to dismiss.
46
+ Dismissing it collapses it to a **Request a feature** button.
47
+ - A creator credit pinned to the bottom.
48
+
49
+ ### Interface
50
+ - Dialogs are in-app and match the rest of the UI, instead of the browser's own
51
+ "localhost:4587 says" boxes. Escape, click-outside and Enter all work, and destructive actions
52
+ get a red button.
53
+ - Layout verified from 320px to 1920px with no horizontal overflow. On phones the sidebar
54
+ dissolves and reorders so your servers come before the request box.
55
+
56
+ ### Telemetry and signups
57
+ - **Fixed:** an unreachable analytics endpoint added roughly 1.5 seconds to every install because
58
+ the ping held the process open. It no longer does.
59
+ - **Fixed:** on Node 18 the request was unref'd before a socket existed, which does not propagate on
60
+ that version, so the delay persisted there. The socket is unref'd too.
61
+ - Documented in the README, with `SERVER_STUDIO_NO_TELEMETRY=1` and `DO_NOT_TRACK` both honoured.
62
+ - Signups go to the author's own list, so the bundled Cloudflare Worker keeps only anonymous counts
63
+ and its open write route is gone.
64
+
65
+ ## 1.1.1 — 2026-08-25
66
+
67
+ - Documentation only. Stated precisely what CI checks about the folder picker and what it cannot.
68
+ - No runtime change; the file list is identical to 1.1.0.
69
+
70
+ ## 1.1.0 — 2026-08-25
71
+
72
+ - `npm install -g server-studio` now completes setup on its own. Installing as a project dependency
73
+ or under CI still touches nothing outside the package, and a failure prints a hint rather than
74
+ failing the install.
75
+ - Added `server-studio add`, which registers a server with no AI involved, so any editor, agent or
76
+ script can use it. The Claude Code skill wraps this command.
77
+
78
+ ## 1.0.1 — 2026-08-25
79
+
80
+ - Made clear that installing the package is not the whole setup.
81
+ - **Fixed:** the path printed for the Cowork plugin pointed inside `node_modules`, which npx deletes
82
+ as soon as it finishes. The plugin is copied somewhere durable first.
83
+
84
+ ## 1.0.0 — 2026-08-20
85
+
86
+ First public release.
87
+
88
+ - A local dashboard for your dev servers: run, stop, open, and live port status.
89
+ - One permanent port per project, with the rule documented per framework.
90
+ - Ships a Claude Code skill and a Cowork plugin.
91
+ - **Fixed before release:** the local server accepted cross-site form posts, so any page you had
92
+ open could have run shell commands through `/api/run`. Writes now require a JSON content type,
93
+ foreign origins are rejected and preflights refused.
94
+ - Cross-platform: a macOS app bundle, plus `server-studio start` on Windows and Linux. CI opens a
95
+ 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
- | Category | Groups cards into filter tabs along the top | `Web app` |
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** | Opens a terminal window, `cd`s into the folder, runs the command. Real terminal, so you see the output and can Ctrl-C it |
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
- | Stop | Kills whatever process is holding that port. Useful when something is stuck |
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
- category and notes at once, so searching `wordpress` or `5181` or `vite` all find the right card.
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. Re-running for the same project updates its fields and keeps
170
- its port locked, which is what makes this safe to call from a build step.
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';
@@ -68,6 +69,14 @@ function install() {
68
69
  // cpSync does not preserve the exec bit on the launcher.
69
70
  // src/ is the single source of truth, copied into the bundle at install time.
70
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) {}
71
80
  fs.chmodSync(path.join(APP_DEST, 'Contents', 'MacOS', 'ServerStudio'), 0o755);
72
81
  } else skip('app skipped (--no-app)');
73
82
 
@@ -109,6 +118,8 @@ function install() {
109
118
  console.log('\nUsing Claude Cowork? Install the plugin by opening this file:');
110
119
  console.log(' ' + pluginFile);
111
120
  }
121
+
122
+ telemetry.ping('install'); // counts a real install (never fires on --dry-run: it returns above)
112
123
  }
113
124
 
114
125
  /* ---------- uninstall ---------- */
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "server-studio",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
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
- --category "WordPress" \
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
- - `--category` — broad bucket: WordPress, Web app, Storybook, Docs, etc. (used for the filter chips)
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`)
@@ -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
- // --category "WordPress" --tag "Vite" --note "admin: ninja/ninja"
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
- category: args.category || '',
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));