server-studio 1.0.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/LICENSE +21 -0
- package/README.md +121 -0
- package/app/Server Studio.app/Contents/Info.plist +26 -0
- package/app/Server Studio.app/Contents/MacOS/ServerStudio +21 -0
- package/app/Server Studio.app/Contents/PkgInfo +1 -0
- package/app/Server Studio.app/Contents/Resources/AppIcon.icns +0 -0
- package/bin/cli.js +170 -0
- package/dist/server-studio.plugin +0 -0
- package/package.json +48 -0
- package/plugin/.claude-plugin/plugin.json +6 -0
- package/plugin/README.md +7 -0
- package/scripts/build-plugin.js +28 -0
- package/scripts/test.js +125 -0
- package/skill/SKILL.md +86 -0
- package/skill/register-server.js +126 -0
- package/src/index.html +549 -0
- package/src/platform.js +114 -0
- package/src/server.js +188 -0
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: server-studio
|
|
3
|
+
description: Save a local dev server into the Server Studio app. Use this whenever you scaffold, build, or set up a project that runs a local server (npm run dev, vite, next, php -S, wp-env, Local WP, a static server, etc.), or when the user says "save this server", "add to dashboard", "add to Server Studio", "save to Server Studio", "register the server". Adds the project's name, run command, folder, and URL/port so it shows up in the Server Studio app for one-click run.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Save Server to Dashboard
|
|
7
|
+
|
|
8
|
+
The user runs **Server Studio**, an app that lists their local servers for one-click run/stop.
|
|
9
|
+
This skill adds a server to that dashboard so nothing gets lost.
|
|
10
|
+
|
|
11
|
+
## When to use it
|
|
12
|
+
|
|
13
|
+
After you build or set up anything that runs on a local port, register it. Examples:
|
|
14
|
+
- You scaffolded a site/app and the dev command is `npm run dev` on `localhost:3000`
|
|
15
|
+
- You started a static preview, a Vite/Next/Astro server, `php -S`, `wp-env`, Storybook, etc.
|
|
16
|
+
- The user asks to "save the server", "add it to the dashboard", "register this"
|
|
17
|
+
|
|
18
|
+
If the run command or port isn't obvious yet, figure it out first (check `package.json` scripts,
|
|
19
|
+
the framework default port, or what the server printed when it started). Don't guess wildly.
|
|
20
|
+
|
|
21
|
+
## The golden rule: one permanent port per project
|
|
22
|
+
|
|
23
|
+
Every project gets ONE fixed, unique port that NEVER changes. A saved entry is only
|
|
24
|
+
useful if its port stays correct, so:
|
|
25
|
+
|
|
26
|
+
1. **Allocate once.** For a new project, let the script pick a free, never-used port with
|
|
27
|
+
`--assign` (or pass a specific `--url`). It guarantees no two projects share a port.
|
|
28
|
+
2. **Bake it into the project so the dev server ALWAYS uses that exact port** — never a random
|
|
29
|
+
one. Use the framework's strict-port option so it fails loudly instead of drifting to another port:
|
|
30
|
+
- Vite: `vite.config` → `server: { port: <n>, strictPort: true }` (or `vite --port <n> --strictPort`)
|
|
31
|
+
- Next.js: dev script → `next dev -p <n>`
|
|
32
|
+
- Astro: `astro dev --port <n>`
|
|
33
|
+
- CRA / react-scripts: `PORT=<n> react-scripts start`
|
|
34
|
+
- Plain Node/Express: read `process.env.PORT || <n>` and document the port
|
|
35
|
+
- PHP: `php -S localhost:<n>`
|
|
36
|
+
Put the exact same port in the `--command` and `--url` you register.
|
|
37
|
+
3. **Never reassign.** Re-running the skill for the same project keeps its locked port — it only
|
|
38
|
+
updates the other fields. The script enforces this; do not pass `--force` unless the user
|
|
39
|
+
explicitly wants to move a project to a new port.
|
|
40
|
+
|
|
41
|
+
## How to save it
|
|
42
|
+
|
|
43
|
+
Run the helper script with whatever fields you know. Only `--name` (or `--url`/`--command`) is required.
|
|
44
|
+
|
|
45
|
+
For a NEW project, add `--assign` to get a guaranteed-unique port, then read the `PORT <n>`
|
|
46
|
+
line it prints and bake that port into the project config (see the golden rule above).
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node "$HOME/.claude/skills/server-studio/register-server.js" \
|
|
50
|
+
--name "Project Name" \
|
|
51
|
+
--project "What it's for" \
|
|
52
|
+
--category "WordPress" \
|
|
53
|
+
--cwd "/absolute/path/to/project" \
|
|
54
|
+
--command "npm run dev -- --port 3001 --strictPort" \
|
|
55
|
+
--url "localhost:3001" \
|
|
56
|
+
--tag "Vite" \
|
|
57
|
+
--note "anything to remember, e.g. login" \
|
|
58
|
+
--assign
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Field guide:
|
|
62
|
+
- `--name` — short label, e.g. "Portfolio Site"
|
|
63
|
+
- `--project` — the project/task it belongs to
|
|
64
|
+
- `--category` — broad bucket: WordPress, Web app, Storybook, Docs, etc. (used for the filter chips)
|
|
65
|
+
- `--cwd` — absolute project folder; the run command executes here
|
|
66
|
+
- `--command` — the exact command that starts the server (this is what the Run button runs)
|
|
67
|
+
- `--url` — address or bare port (`3000` works, becomes `localhost:3000`)
|
|
68
|
+
- `--tag` — small detail: Vite, MAMP, Next, php
|
|
69
|
+
- `--note` — optional reminders (credentials, gotchas)
|
|
70
|
+
|
|
71
|
+
## Behavior
|
|
72
|
+
|
|
73
|
+
- The script matches an existing project by **folder** (`cwd`) or **name**. If found it **updates**
|
|
74
|
+
that entry but **keeps its locked port**; otherwise it **adds** a new one. Safe to run repeatedly,
|
|
75
|
+
it won't create duplicates and won't move a project's port.
|
|
76
|
+
- Ports are unique across all entries. `--assign` picks the lowest free port starting at 3001.
|
|
77
|
+
- The script prints `PORT <n>` — use that number when wiring the project's dev config.
|
|
78
|
+
- Data is written to the Server Studio data file: `~/Library/Application Support/Server Studio/data.json`
|
|
79
|
+
on macOS, `%APPDATA%\Server Studio\data.json` on Windows, `~/.config/server-studio/data.json` on Linux.
|
|
80
|
+
- The dashboard **auto-refreshes** every few seconds, so new/updated entries appear on their own.
|
|
81
|
+
|
|
82
|
+
## Default behavior for the assistant
|
|
83
|
+
|
|
84
|
+
When you finish building something that has a local server, save it automatically and tell the user
|
|
85
|
+
in one line what you saved. Keep it casual, e.g. "Saved 'X' to your Server Studio, reload the tab to run it."
|
|
86
|
+
Only skip saving if there's genuinely no local server involved.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Adds (or updates) a server entry in the Server Studio app's data file.
|
|
3
|
+
// Each project gets ONE permanent, unique port. A project's port is LOCKED:
|
|
4
|
+
// re-registering the same project never changes its port (unless --force).
|
|
5
|
+
//
|
|
6
|
+
// No npm deps. Usage:
|
|
7
|
+
// node register-server.js --name "Portfolio Site" --project "Client redesign" \
|
|
8
|
+
// --cwd "/Users/you/Sites/portfolio" --command "npm run dev" --url "localhost:3001" \
|
|
9
|
+
// --category "WordPress" --tag "Vite" --note "admin: ninja/ninja"
|
|
10
|
+
//
|
|
11
|
+
// # let it pick a fresh, never-used port for a NEW project:
|
|
12
|
+
// node register-server.js --name "New Site" --cwd "/path" --command "npm run dev" --assign
|
|
13
|
+
//
|
|
14
|
+
// On success it prints "PORT <n>" so the caller can bake that exact port into the
|
|
15
|
+
// project's dev config (with strictPort) so the server ALWAYS uses it.
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const os = require('os');
|
|
21
|
+
|
|
22
|
+
// Must match src/platform.js dataDir(); this script ships on its own, so it is inlined.
|
|
23
|
+
const DATA_DIR = (function () {
|
|
24
|
+
if (process.env.SERVER_STUDIO_DATA_DIR) return process.env.SERVER_STUDIO_DATA_DIR;
|
|
25
|
+
if (process.platform === 'darwin') return path.join(os.homedir(), 'Library', 'Application Support', 'Server Studio');
|
|
26
|
+
if (process.platform === 'win32') {
|
|
27
|
+
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Server Studio');
|
|
28
|
+
}
|
|
29
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'server-studio');
|
|
30
|
+
})();
|
|
31
|
+
const DATA_FILE = path.join(DATA_DIR, 'data.json');
|
|
32
|
+
const BASE_PORT = 3001; // assigned ports start here and go up
|
|
33
|
+
|
|
34
|
+
// ---- parse --flag "value" args ----
|
|
35
|
+
const args = {};
|
|
36
|
+
const argv = process.argv.slice(2);
|
|
37
|
+
for (let i = 0; i < argv.length; i++) {
|
|
38
|
+
if (argv[i].startsWith('--')) {
|
|
39
|
+
const key = argv[i].slice(2);
|
|
40
|
+
const val = (argv[i + 1] && !argv[i + 1].startsWith('--')) ? argv[++i] : '';
|
|
41
|
+
args[key] = val === '' ? true : val;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function portFromUrl(url) {
|
|
46
|
+
const m = String(url || '').match(/:(\d{2,5})\b/);
|
|
47
|
+
return m ? Number(m[1]) : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const entry = {
|
|
51
|
+
name: args.name || '',
|
|
52
|
+
project: args.project || '',
|
|
53
|
+
category: args.category || '',
|
|
54
|
+
cwd: args.cwd || '',
|
|
55
|
+
command: args.command || '',
|
|
56
|
+
url: typeof args.url === 'string' ? args.url : '',
|
|
57
|
+
tag: args.tag || '',
|
|
58
|
+
note: args.note || '',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
if (!entry.name && !entry.url && !entry.command) {
|
|
62
|
+
console.error('Need at least --name, --url or --command.');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- read existing ----
|
|
67
|
+
try { fs.mkdirSync(DATA_DIR, { recursive: true }); } catch (e) {}
|
|
68
|
+
let data = [];
|
|
69
|
+
try { data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8')) || []; } catch (e) { data = []; }
|
|
70
|
+
if (!Array.isArray(data)) data = [];
|
|
71
|
+
|
|
72
|
+
function uid() { return 's_' + Math.random().toString(36).slice(2, 9) + Date.now().toString(36); }
|
|
73
|
+
const norm = s => String(s || '').trim().toLowerCase();
|
|
74
|
+
function usedPorts(excludeId) {
|
|
75
|
+
return new Set(data.filter(s => s.id !== excludeId).map(s => portFromUrl(s.url)).filter(Boolean));
|
|
76
|
+
}
|
|
77
|
+
function nextFreePort(excludeId) {
|
|
78
|
+
const used = usedPorts(excludeId);
|
|
79
|
+
let p = Number(args.base) || BASE_PORT;
|
|
80
|
+
while (used.has(p)) p++;
|
|
81
|
+
return p;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---- find existing entry for this project ----
|
|
85
|
+
const match = data.find(s =>
|
|
86
|
+
(entry.cwd && norm(s.cwd) === norm(entry.cwd)) ||
|
|
87
|
+
(entry.name && norm(s.name) === norm(entry.name))
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
let finalPort = null;
|
|
91
|
+
let action;
|
|
92
|
+
|
|
93
|
+
if (match) {
|
|
94
|
+
const lockedPort = portFromUrl(match.url);
|
|
95
|
+
// Update everything EXCEPT the port, which stays locked to the project.
|
|
96
|
+
const keepUrl = match.url;
|
|
97
|
+
Object.assign(match, entry);
|
|
98
|
+
if (lockedPort && !args.force) {
|
|
99
|
+
match.url = keepUrl; // never move a project to a different port
|
|
100
|
+
finalPort = lockedPort;
|
|
101
|
+
if (entry.url && portFromUrl(entry.url) !== lockedPort) {
|
|
102
|
+
console.warn(`note: kept locked port ${lockedPort} (use --force to override)`);
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
if (!match.url || args.assign) match.url = 'localhost:' + nextFreePort(match.id);
|
|
106
|
+
finalPort = portFromUrl(match.url);
|
|
107
|
+
}
|
|
108
|
+
action = 'updated';
|
|
109
|
+
} else {
|
|
110
|
+
// New project: use given port if free, otherwise assign a fresh unique one.
|
|
111
|
+
let port = portFromUrl(entry.url);
|
|
112
|
+
const used = usedPorts();
|
|
113
|
+
if (!port || used.has(port)) {
|
|
114
|
+
if (port && used.has(port)) console.warn(`note: port ${port} already taken, assigning a free one`);
|
|
115
|
+
port = nextFreePort();
|
|
116
|
+
entry.url = 'localhost:' + port;
|
|
117
|
+
}
|
|
118
|
+
data.push({ id: uid(), pinned: false, ...entry });
|
|
119
|
+
finalPort = port;
|
|
120
|
+
action = 'added';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
|
|
124
|
+
console.log(`${action}: "${entry.name || entry.url}" -> ${data.length} server(s) total`);
|
|
125
|
+
if (finalPort) console.log(`PORT ${finalPort}`);
|
|
126
|
+
console.log('Reload not needed: the dashboard auto-refreshes within a few seconds.');
|