portless-studio 0.1.1 → 0.1.3
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/README.md +60 -218
- package/dist/cli/cli-args.js +41 -12
- package/dist/cli/env-command.js +34 -0
- package/dist/cli/index.js +19 -0
- package/dist/cli/mcp/studio-tools.js +70 -7
- package/dist/cli/studio-items.js +5 -2
- package/dist/core/api/contract.js +17 -1
- package/dist/core/config/studio-config.js +6 -0
- package/dist/core/doctor/platform-commands.js +5 -2
- package/dist/core/ports/direct-url.js +7 -0
- package/dist/core/ports/docker-container.js +85 -0
- package/dist/core/ports/helper-ports.js +86 -0
- package/dist/core/routes/env-lines.js +72 -0
- package/dist/core/routes/route.js +14 -2
- package/dist/server/application/container-source.js +19 -0
- package/dist/server/application/doctor.js +199 -9
- package/dist/server/application/port-discovery.js +84 -9
- package/dist/server/application/short-urls.js +136 -0
- package/dist/server/application/studio-runtime.js +82 -24
- package/dist/server/application/terminals.js +142 -15
- package/dist/server/infrastructure/system/docker-containers.js +64 -0
- package/dist/server/infrastructure/system/doctor-facts.js +43 -1
- package/dist/server/infrastructure/system/ephemeral-ports.js +62 -0
- package/dist/server/infrastructure/system/port-forwarder.js +78 -0
- package/dist/server/infrastructure/system/process-runner.js +2 -1
- package/dist/server/infrastructure/system/system-port-scanner.js +64 -4
- package/dist/server/infrastructure/terminal/process-tree.js +71 -0
- package/dist/server/infrastructure/terminal/terminal-spawner.js +44 -3
- package/dist/server/interface/http/http-server.js +38 -2
- package/dist/server/main.js +43 -6
- package/dist/web/assets/index-9WPxv6pu.js +17 -0
- package/dist/web/assets/index-Bckc4cIr.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-B3KMqQ-l.js +0 -15
- package/dist/web/assets/index-CWujmTe4.css +0 -1
package/README.md
CHANGED
|
@@ -22,7 +22,8 @@ The screenshots use the built-in sample data (`?demo`), not a real machine.
|
|
|
22
22
|
|
|
23
23
|
## Install
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Requirements: Node 22 or later on Linux, macOS or Windows. portless is installed for you if it is
|
|
26
|
+
missing.
|
|
26
27
|
|
|
27
28
|
To try it, run it straight from npm:
|
|
28
29
|
|
|
@@ -40,18 +41,8 @@ portless-studio init
|
|
|
40
41
|
The login service starts Studio from the files npm installed. Run through `npx`, those files sit in
|
|
41
42
|
npm's cache, which npm can clear at any time, and then Studio stops starting at login.
|
|
42
43
|
|
|
43
|
-
To update, install the latest version and restart Studio
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
npm i -g portless-studio@latest
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
To remove it, undo the setup first, then remove the package:
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
portless-studio uninstall
|
|
53
|
-
npm rm -g portless-studio
|
|
54
|
-
```
|
|
44
|
+
To update, install the latest version and restart Studio with `npm i -g portless-studio@latest`.
|
|
45
|
+
To remove it, run `portless-studio uninstall`, then `npm rm -g portless-studio`.
|
|
55
46
|
|
|
56
47
|
## Quick start
|
|
57
48
|
|
|
@@ -63,61 +54,52 @@ npm rm -g portless-studio
|
|
|
63
54
|
- Starts the portless proxy on port 1355 over HTTP, which needs no password, certificate or extra
|
|
64
55
|
tool on macOS, Windows or Linux, and can set Studio to start when you log in.
|
|
65
56
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
Then `portless-studio` starts Studio at `http://127.0.0.1:4949`, registers it in portless as
|
|
58
|
+
`studio` and opens your browser. Your apps open at names like `http://web.myapp.localhost:1355`:
|
|
59
|
+
Chrome, Edge and Firefox send every `*.localhost` name to your own machine, with no hosts file and
|
|
60
|
+
no DNS setup. Turn on Short URLs in Settings to drop the `:1355` (`http://web.myapp.localhost`)
|
|
61
|
+
where your system lets you listen on port 80; see
|
|
62
|
+
[troubleshooting](docs/troubleshooting.md#names-have-1355-and-i-want-port-80).
|
|
70
63
|
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
[Getting started](docs/getting-started.md) walks through the first five minutes: naming a dev
|
|
65
|
+
server, naming a port by hand, running a command in a Studio terminal and turning on auto mode.
|
|
66
|
+
To try the page without portless, open `http://127.0.0.1:4949/?demo` for sample data.
|
|
73
67
|
|
|
74
68
|
## What it does
|
|
75
69
|
|
|
76
70
|
- **Live list** of portless routes: name, URL, port, app or alias, uptime and any Tailscale, ngrok
|
|
77
71
|
or Cloudflare tunnel. It updates as soon as a route appears or goes away.
|
|
78
72
|
- **Detected ports:** listening ports that answer HTTP but have no name yet, with the process, the
|
|
79
|
-
project folder and a suggested name. Name one, name them all, or ignore one.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
73
|
+
project folder and a suggested name. Name one, name them all, or ignore one. Ports a running
|
|
74
|
+
Docker container publishes show here too.
|
|
75
|
+
- **Auto mode** (off by default): every new dev server gets its suggested name by itself. Names
|
|
76
|
+
auto mode gave are removed 10 minutes after their port closes.
|
|
83
77
|
- **Name a port** that portless did not start, such as a database in Docker (`db` gives
|
|
84
78
|
`db.localhost`). Rename or remove those names later.
|
|
85
79
|
- **Where each server runs:** every row shows the git branch, the linked worktree and the project
|
|
86
|
-
folder.
|
|
87
|
-
- **Views:** group by project, worktree, kind or status, or into your own groups;
|
|
88
|
-
|
|
89
|
-
and pick the one that opens by default.
|
|
80
|
+
folder.
|
|
81
|
+
- **Views:** group by project, worktree, kind or status, or into your own groups; filter, sort and
|
|
82
|
+
save the combination as a named view.
|
|
90
83
|
- **Terminals:** run a command per project inside Studio and watch its output in a panel. You, the
|
|
91
|
-
CLI and your AI agents drive the same terminals.
|
|
84
|
+
CLI and your AI agents drive the same terminals.
|
|
92
85
|
- **Agents:** an MCP server lets Claude Code, Cursor and other agents list, name and stop dev
|
|
93
|
-
servers and run commands in Studio terminals.
|
|
94
|
-
- **
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
- **Health:** each route's dot says whether its port answers. The Health panel and
|
|
98
|
-
`portless-studio doctor` find a proxy left running as root, two proxies at once, an untrusted
|
|
99
|
-
certificate or a taken port, and give the fix.
|
|
100
|
-
- **Proxy and Studio control:** start, stop or restart the portless proxy, and stop or restart
|
|
101
|
-
Studio, from the page.
|
|
102
|
-
- **Stop a dev server** from its row, after a confirmation. Only your own processes on a port
|
|
103
|
-
Studio shows.
|
|
104
|
-
- **Project icons:** the favicon the server serves, else one in the project folder, else the
|
|
105
|
-
project's initials. Set an emoji or an image per project in Settings.
|
|
106
|
-
- **Notifications** in the page when a server is detected or named, and optionally from the
|
|
107
|
-
operating system.
|
|
108
|
-
- **Install portless** from the page when the `portless` command is missing.
|
|
86
|
+
servers and run commands in Studio terminals.
|
|
87
|
+
- **Health:** each route's dot says whether its port answers, and `portless-studio doctor` finds a
|
|
88
|
+
proxy left running as root, two proxies at once, an untrusted certificate or a taken port, and
|
|
89
|
+
gives the fix.
|
|
109
90
|
- **Keyboard first:** every action has a key, `?` lists them and `Ctrl+K` (`Cmd+K`) runs any of
|
|
110
91
|
them by name.
|
|
111
92
|
|
|
112
|
-
|
|
93
|
+
Also: start, stop or restart the proxy and Studio from the page, stop a dev server from its row,
|
|
94
|
+
open a project in your editor, project icons, and notifications in the page or from the operating
|
|
95
|
+
system.
|
|
96
|
+
|
|
97
|
+
## Naming
|
|
113
98
|
|
|
114
99
|
A suggested name says what the work is, then the app, then the repository. The package
|
|
115
100
|
`@acme/web` in the `acme-monorepo` folder on the branch `feat/ED-1234-add-x` gets
|
|
116
|
-
`ed-1234.web.acme`, so two worktrees of one repository get different names.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
Change it with `nameTemplate`, for example `{ticket}.{repo}` or `{package}-{port}`.
|
|
120
|
-
[docs/naming.md](docs/naming.md) lists every token and how empty or repeated parts are dropped.
|
|
101
|
+
`ed-1234.web.acme`, so two worktrees of one repository get different names. Change the pattern
|
|
102
|
+
with `nameTemplate`; [docs/naming.md](docs/naming.md) lists every token.
|
|
121
103
|
|
|
122
104
|
Anything that names a route through the portless CLI also shows up in Studio:
|
|
123
105
|
|
|
@@ -126,177 +108,42 @@ portless web.myapp pnpm dev # a dev server, at web.myapp.localhost
|
|
|
126
108
|
portless alias db.myapp 5432 # a port something else opened, at db.myapp.localhost
|
|
127
109
|
```
|
|
128
110
|
|
|
129
|
-
## Agents and
|
|
130
|
-
|
|
131
|
-
`portless-studio mcp` is an [MCP](https://modelcontextprotocol.io) server on stdio. It talks to the
|
|
132
|
-
Studio already running, so whatever an agent changes shows up in the page at once. For Claude Code:
|
|
111
|
+
## Agents and terminals
|
|
133
112
|
|
|
134
113
|
```bash
|
|
135
114
|
claude mcp add --scope user portless-studio -- npx -y portless-studio mcp
|
|
115
|
+
portless-studio run -- pnpm dev
|
|
136
116
|
```
|
|
137
117
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
118
|
+
The first line connects Claude Code to Studio's MCP server. The second runs a dev server in a
|
|
119
|
+
Studio terminal, prints its output in your shell and names the port it opens.
|
|
120
|
+
[docs/agents.md](docs/agents.md) shows how to keep agents in sync with Studio, including
|
|
121
|
+
instructions to paste and Claude Code hooks; [docs/mcp.md](docs/mcp.md) has the setup for other
|
|
122
|
+
clients and [docs/terminals.md](docs/terminals.md) covers the terminal panel.
|
|
141
123
|
|
|
142
|
-
##
|
|
124
|
+
## Documentation
|
|
143
125
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
126
|
+
- [Getting started](docs/getting-started.md) and [Troubleshooting](docs/troubleshooting.md)
|
|
127
|
+
- [Command line](docs/cli.md), [Configuration](docs/configuration.md) and
|
|
128
|
+
[Keyboard shortcuts](docs/keyboard-shortcuts.md)
|
|
129
|
+
- [Naming](docs/naming.md), [Terminals](docs/terminals.md) and
|
|
130
|
+
[Linux, macOS and Windows](docs/platforms.md)
|
|
131
|
+
- [Agents](docs/agents.md) and [MCP server](docs/mcp.md)
|
|
132
|
+
- [Every page, with one line each](docs/README.md)
|
|
133
|
+
|
|
134
|
+
## How it relates to portless
|
|
147
135
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
## Platforms
|
|
153
|
-
|
|
154
|
-
Studio runs on Linux, macOS and Windows with Node 22 or later and installs nothing beyond portless. Each
|
|
155
|
-
feature uses tools the system already has (`/proc` on Linux, `lsof` on macOS, `netstat` and
|
|
156
|
-
PowerShell on Windows). [docs/platforms.md](docs/platforms.md) says what each system uses and what
|
|
157
|
-
does not work the same everywhere.
|
|
158
|
-
|
|
159
|
-
## Commands
|
|
160
|
-
|
|
161
|
-
| Command | What it does |
|
|
162
|
-
| --- | --- |
|
|
163
|
-
| `portless-studio` | Starts Studio with the settings from the config file. |
|
|
164
|
-
| `portless-studio init [--yes]` | The setup above. Safe to run again. |
|
|
165
|
-
| `portless-studio uninstall [--yes]` | Undoes the setup: removes the login service, the names auto mode gave and the `studio` name, and stops the proxy if Studio started it. Names you or an agent gave stay. Asks before deleting the settings folder. |
|
|
166
|
-
| `portless-studio service install` | Starts Studio when you log in: a systemd user unit on Linux, a LaunchAgent on macOS, a script in the Startup folder on Windows. |
|
|
167
|
-
| `portless-studio service uninstall` | Undoes `service install`. |
|
|
168
|
-
| `portless-studio service status` | Says whether the login service is installed and running. |
|
|
169
|
-
| `portless-studio config [--edit] [--json]` | Prints the config file path and contents, or opens it in `$EDITOR`. |
|
|
170
|
-
| `portless-studio doctor [--json]` | Checks portless, the proxy, the certificate, Studio's port and the login service, and prints the fix for each problem. Exits with 1 when something fails. |
|
|
171
|
-
| `portless-studio list [--json]` | Lists named routes and ports without a name. |
|
|
172
|
-
| `portless-studio mcp` | Runs the MCP server on stdio for AI agents. |
|
|
173
|
-
| `portless-studio run [--name <name>] [--cwd <dir>] -- <command...>` | Runs a command in a Studio terminal and names the port it opens. |
|
|
174
|
-
| `portless-studio open [target]` | Brings Studio to the front showing a route, project, terminal id or `port:<n>`. |
|
|
175
|
-
|
|
176
|
-
| Flag | Effect |
|
|
177
|
-
| --- | --- |
|
|
178
|
-
| `--port <n>` | Port for the dashboard on 127.0.0.1. Default: `port` in the config, 4949. `0` picks a free one. |
|
|
179
|
-
| `--no-open` | Do not open the browser. |
|
|
180
|
-
| `--no-register` | Do not register the `studio` name in portless. |
|
|
181
|
-
| `-y`, `--yes` | With `init` and `uninstall`: accept every default. |
|
|
182
|
-
| `-h`, `--help` | Show the help. |
|
|
183
|
-
| `-v`, `--version` | Show the version. |
|
|
184
|
-
|
|
185
|
-
| Environment variable | What it does |
|
|
186
|
-
| --- | --- |
|
|
187
|
-
| `PORTLESS_STUDIO_CONFIG` | Path of the config file. |
|
|
188
|
-
| `PORTLESS_STATE_DIR` | portless state folder. Default `~/.portless`. |
|
|
189
|
-
| `PORTLESS_BIN` | portless binary used to create and remove names. Default `portless`. |
|
|
190
|
-
| `PORTLESS_STUDIO_URL` | Studio that `mcp`, `list`, `run` and `open` talk to. Default `http://127.0.0.1:<port>`. |
|
|
191
|
-
| `PORTLESS_STUDIO_NO_UPDATE_CHECK` | `1` turns the daily update check off, whatever the config says. |
|
|
192
|
-
| `EDITOR` | Editor that `config --edit` opens. |
|
|
193
|
-
|
|
194
|
-
## Config file
|
|
195
|
-
|
|
196
|
-
Settings live in one JSON file: `~/.config/portless-studio/config.json` on Linux and macOS (or
|
|
197
|
-
under `$XDG_CONFIG_HOME`), `%APPDATA%\portless-studio\config.json` on Windows. `portless-studio
|
|
198
|
-
config` prints the path. Edit it by hand or from the Settings panel; a running Studio picks up the
|
|
199
|
-
changes without a restart. Missing fields take their default.
|
|
200
|
-
|
|
201
|
-
| Field | Default | What it does |
|
|
202
|
-
| --- | --- | --- |
|
|
203
|
-
| `port` | `4949` | Port Studio listens on, on 127.0.0.1. |
|
|
204
|
-
| `autoName` | `false` | Gives every new dev server a portless name automatically. |
|
|
205
|
-
| `autoRemoveAfterMinutes` | `10` | Minutes a name auto mode gave survives after its port closes. `0` keeps them. |
|
|
206
|
-
| `openBrowser` | `true` | Opens the browser when Studio starts. |
|
|
207
|
-
| `registerStudioAlias` | `true` | Registers Studio itself in portless as `studio`. |
|
|
208
|
-
| `startProxy` | `true` | Starts the portless proxy when Studio starts and it is not running. |
|
|
209
|
-
| `proxyPort` | `1355` | Port of that proxy. 1355 needs no administrator rights; 443 removes the port from URLs. |
|
|
210
|
-
| `proxyHttps` | `false` | Serves the names over HTTPS. See the FAQ. |
|
|
211
|
-
| `scanIntervalMs` | `3000` | How often Studio looks for new listening ports, in milliseconds (at least 1000). |
|
|
212
|
-
| `ignoredPorts` | `[]` | Ports never shown as detected and never named automatically. |
|
|
213
|
-
| `ignoredProcesses` | editors, chat apps, browsers | Process names whose ports are never shown. |
|
|
214
|
-
| `views` | one view, `All`, grouped by project | Saved views: grouping, sort, filters and custom groups. |
|
|
215
|
-
| `defaultViewId` | `"all"` | The view the page opens with. |
|
|
216
|
-
| `favorites` | `[]` | Routes pinned above every group, by name or as `"port:3000"`. |
|
|
217
|
-
| `desktopNotifications` | `false` | Notifies from an open Studio tab while it is in the background. |
|
|
218
|
-
| `systemNotifications` | `false` | Asks the operating system to notify when no Studio tab is open. |
|
|
219
|
-
| `nameTemplate` | `"{work}.{package}.{repo}"` | How suggested names are built. See [docs/naming.md](docs/naming.md). |
|
|
220
|
-
| `projectIcons` | `{}` | Icon per project: an emoji, an http(s) image URL or a `data:image` URL. |
|
|
221
|
-
| `checkForUpdates` | `true` | Once a day, asks npm whether a newer portless-studio exists. See "Update notice" below. |
|
|
222
|
-
|
|
223
|
-
The `--port`, `--no-open` and `--no-register` flags win over the file for one run. Apps you add to
|
|
224
|
-
the Open with menu live in `open-with.json` next to the config file.
|
|
225
|
-
|
|
226
|
-
### Update notice
|
|
227
|
-
|
|
228
|
-
Once a day Studio asks the npm registry for the latest version of portless-studio. When a newer
|
|
229
|
-
one exists, it prints one line on start and shows a banner in the page with the command to update;
|
|
230
|
-
dismissing the banner hides it until the next version. The request is a plain
|
|
231
|
-
`GET https://registry.npmjs.org/portless-studio/latest`: Studio sends nothing about your machine,
|
|
232
|
-
your projects or your routes. It gives up after 3 seconds and stays quiet when you are offline,
|
|
233
|
-
and it keeps the time of the last check in `update-check.json` next to the config file. It never
|
|
234
|
-
runs when the `CI` variable is set. Turn it off with `checkForUpdates: false`, the Settings
|
|
235
|
-
switch, or `PORTLESS_STUDIO_NO_UPDATE_CHECK=1`.
|
|
236
|
-
|
|
237
|
-
## Keyboard shortcuts
|
|
238
|
-
|
|
239
|
-
| Keys | Action |
|
|
240
|
-
| --- | --- |
|
|
241
|
-
| `j` / `↓`, `k` / `↑` | Next and previous row |
|
|
242
|
-
| `Home`, `End` | First and last row |
|
|
243
|
-
| `/` | Search routes |
|
|
244
|
-
| `Enter` or `o` | Open the selected route |
|
|
245
|
-
| `c` | Copy its URL |
|
|
246
|
-
| `r` | Rename an alias |
|
|
247
|
-
| `Delete` or `Backspace` | Remove an alias |
|
|
248
|
-
| `f` | Pin or unpin as favorite |
|
|
249
|
-
| `.` or `Shift+F10` | Open with, copy path and more |
|
|
250
|
-
| `t` | Open a terminal in its folder |
|
|
251
|
-
| `n` | Name a port |
|
|
252
|
-
| `v` / `Shift+V` | Next view / open the view menu |
|
|
253
|
-
| `Shift+F` | Open the filters |
|
|
254
|
-
| `[`, `]` | Collapse or expand all groups |
|
|
255
|
-
| `Ctrl+K` / `Cmd+K` | Command menu |
|
|
256
|
-
| `,` | Settings |
|
|
257
|
-
| `?` | All keyboard shortcuts |
|
|
258
|
-
| `Esc` | Close a dialog or cancel |
|
|
136
|
+
Studio is an independent project, not part of portless and not made by its authors. It reads the
|
|
137
|
+
state portless keeps in `~/.portless` and names routes through the `portless` command, so anything
|
|
138
|
+
you or an agent do with portless shows up in Studio, and Studio never changes how portless works.
|
|
259
139
|
|
|
260
140
|
## Security
|
|
261
141
|
|
|
262
|
-
Studio
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
A dev server can be stopped only when it belongs to your user and listens on a port Studio shows,
|
|
268
|
-
and Studio never runs a command that needs sudo. See [SECURITY.md](SECURITY.md) to report a
|
|
269
|
-
problem.
|
|
270
|
-
|
|
271
|
-
## FAQ
|
|
272
|
-
|
|
273
|
-
**Safari does not open `*.localhost` names.** Safari does not always send `.localhost` names to
|
|
274
|
-
your machine. Open Studio at `http://127.0.0.1:4949` and each app at `http://127.0.0.1:<its port>`,
|
|
275
|
-
or use Chrome, Edge or Firefox.
|
|
276
|
-
|
|
277
|
-
**Why HTTP on port 1355, and how do I get HTTPS or port 443?** Port 1355 needs no administrator
|
|
278
|
-
rights on any system, and browsers treat plain HTTP on a `.localhost` name as a secure context, so
|
|
279
|
-
most apps work as they would on HTTPS. For URLs without a port, set `proxyPort` to `443`; on Linux
|
|
280
|
-
that needs sudo once to start the proxy. For HTTPS, set `proxyHttps` to `true` and trust the
|
|
281
|
-
portless certificate: `portless-studio doctor` prints the exact command for your system, and the
|
|
282
|
-
Health panel shows it too.
|
|
283
|
-
|
|
284
|
-
**Does it work on Windows?** Yes, with Node 22 or later and PowerShell 5 (included in Windows 10 and 11).
|
|
285
|
-
Ports come from `netstat`, process details from PowerShell, and start at login uses a hidden
|
|
286
|
-
script in the Startup folder. Windows has no service manager Studio can ask, so the page can stop
|
|
287
|
-
Studio but not restart it, and the doctor only sees the proxy portless records.
|
|
288
|
-
[docs/platforms.md](docs/platforms.md) lists every difference.
|
|
289
|
-
|
|
290
|
-
**How does it relate to portless?** Studio is an independent project, not part of portless and not
|
|
291
|
-
made by its authors. It reads the state portless keeps in `~/.portless` and names routes through
|
|
292
|
-
the `portless` command, so anything you or an agent do with portless shows up in Studio, and
|
|
293
|
-
Studio never changes how portless works.
|
|
294
|
-
|
|
295
|
-
**How do I remove it?** Run `portless-studio uninstall` (or `npx portless-studio uninstall` if you
|
|
296
|
-
never installed it globally). It removes the login service, the names auto mode gave and the
|
|
297
|
-
`studio` name, stops the proxy if Studio started it, and asks before deleting the settings folder.
|
|
298
|
-
Then remove the package with `npm rm -g portless-studio`. portless itself stays installed; remove it with
|
|
299
|
-
`npm uninstall -g portless`.
|
|
142
|
+
Studio only listens on `127.0.0.1` and rejects requests whose `Host` or `Origin` is not local, so
|
|
143
|
+
other websites open in your browser cannot read your routes or call the API. Every change also
|
|
144
|
+
needs a token that changes on every run and that only Studio's own page can read. A dev server can
|
|
145
|
+
be stopped only when it belongs to your user, and Studio never runs a command that needs sudo.
|
|
146
|
+
See [SECURITY.md](SECURITY.md) to report a problem.
|
|
300
147
|
|
|
301
148
|
## Development
|
|
302
149
|
|
|
@@ -306,16 +153,11 @@ cd portless-studio
|
|
|
306
153
|
pnpm install
|
|
307
154
|
pnpm dev # API on :4949 and the web app with hot reload on :5173
|
|
308
155
|
pnpm test # unit and component tests
|
|
309
|
-
pnpm typecheck
|
|
310
|
-
pnpm check # lint and format check
|
|
311
|
-
pnpm build # builds dist/, then `pnpm start` runs it
|
|
312
|
-
pnpm smoke # starts the built CLI on a free port and checks the API answers
|
|
313
156
|
```
|
|
314
157
|
|
|
315
|
-
|
|
316
|
-
[docs/architecture.md](docs/architecture.md)
|
|
317
|
-
[docs/conventions.md](docs/conventions.md)
|
|
318
|
-
requests.
|
|
158
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) covers the checks to run and pull requests,
|
|
159
|
+
[docs/architecture.md](docs/architecture.md) the layers and
|
|
160
|
+
[docs/conventions.md](docs/conventions.md) the code conventions.
|
|
319
161
|
|
|
320
162
|
## License
|
|
321
163
|
|
package/dist/cli/cli-args.js
CHANGED
|
@@ -17,6 +17,10 @@ Usage:
|
|
|
17
17
|
what is wrong (exit code 1 when something fails)
|
|
18
18
|
portless-studio list [--json] List named routes and ports without a name, from the running
|
|
19
19
|
Studio (or read directly when it is not running)
|
|
20
|
+
portless-studio env [--prefix P] [--strip S] [--direct] [--json] [name...]
|
|
21
|
+
Print KEY=url lines for a .env file, one per route
|
|
22
|
+
(api.acme -> API_ACME_URL); a name also picks its *.name
|
|
23
|
+
routes (docs/cli.md)
|
|
20
24
|
portless-studio mcp Run an MCP server on stdio so AI agents can list, name and
|
|
21
25
|
stop dev servers through the running Studio (docs/mcp.md)
|
|
22
26
|
portless-studio run [--name <name>] [--cwd <dir>] -- <command...>
|
|
@@ -32,7 +36,10 @@ Options:
|
|
|
32
36
|
--no-open Do not open the browser
|
|
33
37
|
--no-register Do not register the "studio" portless alias for the dashboard
|
|
34
38
|
-y, --yes With init and uninstall: accept every default without asking
|
|
35
|
-
--json With doctor, config and
|
|
39
|
+
--json With doctor, config, list and env: print JSON for scripts and agents
|
|
40
|
+
--prefix <p> With env: put p in front of every key, e.g. VITE_ or NEXT_PUBLIC_
|
|
41
|
+
--strip <s> With env: drop the .s suffix from names before they become keys
|
|
42
|
+
--direct With env: use http://localhost:<port> instead of the named URL
|
|
36
43
|
--name <name> With run: name for the port the command opens (default: the suggested one)
|
|
37
44
|
--cwd <dir> With run: folder to run in (default: the current folder)
|
|
38
45
|
-h, --help Show this help
|
|
@@ -52,6 +59,7 @@ const SUBCOMMANDS = new Set([
|
|
|
52
59
|
"config",
|
|
53
60
|
"doctor",
|
|
54
61
|
"list",
|
|
62
|
+
"env",
|
|
55
63
|
"mcp",
|
|
56
64
|
"run",
|
|
57
65
|
"open",
|
|
@@ -91,6 +99,16 @@ export function parseCliArgs(argv) {
|
|
|
91
99
|
return ok({ command: "doctor", json });
|
|
92
100
|
if (subcommand === "list")
|
|
93
101
|
return ok({ command: "list", json });
|
|
102
|
+
if (subcommand === "env") {
|
|
103
|
+
return ok({
|
|
104
|
+
command: "env",
|
|
105
|
+
json,
|
|
106
|
+
direct: values.direct === true,
|
|
107
|
+
prefix: values.prefix?.trim() || null,
|
|
108
|
+
strip: values.strip?.trim() || null,
|
|
109
|
+
names: positionals.map((name) => name.trim()).filter((name) => name !== ""),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
94
112
|
if (subcommand === "mcp")
|
|
95
113
|
return ok({ command: "mcp" });
|
|
96
114
|
if (subcommand === "run") {
|
|
@@ -152,23 +170,34 @@ function parseFlags(args, subcommand) {
|
|
|
152
170
|
? { ...common, edit: { type: "boolean" }, json }
|
|
153
171
|
: subcommand === "doctor" || subcommand === "list"
|
|
154
172
|
? { ...common, json }
|
|
155
|
-
: subcommand === "
|
|
156
|
-
?
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
: {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
: subcommand === "env"
|
|
174
|
+
? {
|
|
175
|
+
...common,
|
|
176
|
+
json,
|
|
177
|
+
direct: { type: "boolean" },
|
|
178
|
+
prefix: { type: "string" },
|
|
179
|
+
strip: { type: "string" },
|
|
180
|
+
}
|
|
181
|
+
: subcommand === "service" || subcommand === "mcp" || subcommand === "open"
|
|
182
|
+
? common
|
|
183
|
+
: subcommand === "run"
|
|
184
|
+
? { ...common, name: { type: "string" }, cwd: { type: "string" } }
|
|
185
|
+
: {
|
|
186
|
+
...common,
|
|
187
|
+
port: { type: "string" },
|
|
188
|
+
open: { type: "boolean" },
|
|
189
|
+
register: { type: "boolean" },
|
|
190
|
+
};
|
|
165
191
|
// parseArgs learned `allowNegative` in Node 22.4; reading `--no-<flag>` here keeps every Node 22.
|
|
166
192
|
const { args: positive, negated } = splitNegations(args, options);
|
|
167
193
|
const { values, positionals, tokens } = parseArgs({
|
|
168
194
|
args: positive,
|
|
169
195
|
options,
|
|
170
196
|
tokens: true,
|
|
171
|
-
allowPositionals: subcommand === "service" ||
|
|
197
|
+
allowPositionals: subcommand === "service" ||
|
|
198
|
+
subcommand === "run" ||
|
|
199
|
+
subcommand === "open" ||
|
|
200
|
+
subcommand === "env",
|
|
172
201
|
});
|
|
173
202
|
for (const token of tokens) {
|
|
174
203
|
if (token.kind === "option" && options[token.name]?.type === "boolean") {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { envEntries, formatEnvLine, selectEnvRoutes, } from "../core/routes/env-lines.js";
|
|
2
|
+
/** `portless-studio env`: `KEY=url` lines for a `.env` file, one per named route. */
|
|
3
|
+
export async function runEnvCommand(options) {
|
|
4
|
+
const fromStudio = await options.readFromStudio();
|
|
5
|
+
const routes = fromStudio.ok ? fromStudio.value : await options.readLocally();
|
|
6
|
+
const selected = selectEnvRoutes(routes, options.names);
|
|
7
|
+
if (!selected.ok)
|
|
8
|
+
return fail(options, selected.error);
|
|
9
|
+
const entries = envEntries(selected.value, {
|
|
10
|
+
direct: options.direct,
|
|
11
|
+
...(options.prefix === null ? {} : { prefix: options.prefix }),
|
|
12
|
+
...(options.strip === null ? {} : { strip: options.strip }),
|
|
13
|
+
});
|
|
14
|
+
if (!entries.ok)
|
|
15
|
+
return fail(options, entries.error);
|
|
16
|
+
if (options.json) {
|
|
17
|
+
const source = fromStudio.ok ? "studio" : "local";
|
|
18
|
+
options.print(JSON.stringify({ source, variables: entries.value }, null, 2));
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
if (!fromStudio.ok) {
|
|
22
|
+
options.printError("(Studio is not running: read directly from portless.)");
|
|
23
|
+
}
|
|
24
|
+
if (entries.value.length === 0) {
|
|
25
|
+
options.printError("No named routes yet.");
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
options.print(entries.value.map(formatEnvLine).join("\n"));
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
function fail(options, message) {
|
|
32
|
+
options.printError(`Error: ${message}`);
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
package/dist/cli/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { HELP, parseCliArgs, resolveStartSettings, } from "./cli-args.js";
|
|
|
11
11
|
import { runCommand } from "./command-runner.js";
|
|
12
12
|
import { editConfig, printConfig, printConfigJson } from "./config-command.js";
|
|
13
13
|
import { runDoctorCommand } from "./doctor-command.js";
|
|
14
|
+
import { runEnvCommand } from "./env-command.js";
|
|
14
15
|
import { runOpenCommand } from "./focus-command.js";
|
|
15
16
|
import { runInit } from "./init.js";
|
|
16
17
|
import { runListCommand } from "./list-command.js";
|
|
@@ -65,6 +66,9 @@ async function main() {
|
|
|
65
66
|
case "list":
|
|
66
67
|
await list(options.json);
|
|
67
68
|
return;
|
|
69
|
+
case "env":
|
|
70
|
+
await env(options);
|
|
71
|
+
return;
|
|
68
72
|
case "mcp":
|
|
69
73
|
await mcp();
|
|
70
74
|
return;
|
|
@@ -125,6 +129,21 @@ async function list(json) {
|
|
|
125
129
|
// The local fallback may leave scan timers behind; the list is all this command does.
|
|
126
130
|
process.exit(code);
|
|
127
131
|
}
|
|
132
|
+
async function env(options) {
|
|
133
|
+
const client = await studioClient();
|
|
134
|
+
const code = await runEnvCommand({
|
|
135
|
+
...options,
|
|
136
|
+
print,
|
|
137
|
+
printError: (line) => console.error(line),
|
|
138
|
+
readFromStudio: async () => {
|
|
139
|
+
const snapshot = await client.read(API_PATHS.snapshot);
|
|
140
|
+
return snapshot.ok ? { ok: true, value: snapshot.value.routes } : snapshot;
|
|
141
|
+
},
|
|
142
|
+
readLocally: async () => (await createStudioServices().registry.readSnapshot()).routes,
|
|
143
|
+
});
|
|
144
|
+
// The local fallback may leave watchers behind; the lines are all this command prints.
|
|
145
|
+
process.exit(code);
|
|
146
|
+
}
|
|
128
147
|
/** stdout carries only protocol messages; warnings go to stderr through `console.error`. */
|
|
129
148
|
async function mcp() {
|
|
130
149
|
const client = await studioClient();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { API_PATHS, } from "../../core/api/contract.js";
|
|
1
|
+
import { API_PATHS, INCLUDE_STUDIO_PARAM, } from "../../core/api/contract.js";
|
|
2
2
|
import { err, ok } from "../../core/result.js";
|
|
3
|
+
import { envEntries, formatEnvLine, selectEnvRoutes } from "../../core/routes/env-lines.js";
|
|
3
4
|
import { plainText } from "../../core/terminals/terminal.js";
|
|
4
5
|
import { describeProxy, detectedItem, proxyItem, routeItem } from "../studio-items.js";
|
|
5
6
|
const NAMING_HINT = "Make each name say what the work is: start it with the ticket key, PR number or a short task " +
|
|
@@ -15,6 +16,7 @@ export const AGENT_INSTRUCTIONS = [
|
|
|
15
16
|
"Start dev servers with run_in_terminal (or `portless-studio run -- <command>` in a shell) instead",
|
|
16
17
|
"of in your own shell, so the user can watch their output in Studio; read it with read_terminal.",
|
|
17
18
|
"When the user asks to see something, call focus_studio.",
|
|
19
|
+
"To point an app at another one by name in a .env file, call env_lines and write its lines.",
|
|
18
20
|
NAMING_HINT,
|
|
19
21
|
].join(" ");
|
|
20
22
|
const PROXY_ACTIONS = ["start", "stop", "restart"];
|
|
@@ -37,10 +39,20 @@ export function createStudioTools(client) {
|
|
|
37
39
|
title: "List named routes",
|
|
38
40
|
description: "Lists every portless route Studio knows: name, browser URL, local port, kind (app or alias), " +
|
|
39
41
|
"health (up, error, down), project and git worktree it runs from, and pid. Use it to find the " +
|
|
40
|
-
"URL of a running app or which ports a worktree uses."
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
"URL of a running app or which ports a worktree uses. Studio's own route is left out unless " +
|
|
43
|
+
"include_studio is true.",
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: "object",
|
|
46
|
+
properties: {
|
|
47
|
+
include_studio: {
|
|
48
|
+
type: "boolean",
|
|
49
|
+
description: "Also list Studio's own route (studio), hidden by default.",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
additionalProperties: false,
|
|
53
|
+
},
|
|
54
|
+
call: async (args) => {
|
|
55
|
+
const snapshot = await readSnapshot(client, args.include_studio === true);
|
|
44
56
|
if (!snapshot.ok)
|
|
45
57
|
return snapshot.error;
|
|
46
58
|
const { routes, proxy } = snapshot.value;
|
|
@@ -51,6 +63,54 @@ export function createStudioTools(client) {
|
|
|
51
63
|
};
|
|
52
64
|
},
|
|
53
65
|
},
|
|
66
|
+
{
|
|
67
|
+
name: "env_lines",
|
|
68
|
+
title: "Build .env lines from route names",
|
|
69
|
+
description: "Returns KEY=url lines for a .env file, one per route: api.acme becomes " +
|
|
70
|
+
"API_ACME_URL=http://api.acme.localhost:1355, a URL that keeps working when the port " +
|
|
71
|
+
"changes. names keeps only those routes (acme also keeps every *.acme route); prefix puts " +
|
|
72
|
+
"VITE_ or NEXT_PUBLIC_ in front; strip drops a suffix like the project (acme turns api.acme " +
|
|
73
|
+
"into API_URL); direct uses http://localhost:<port> instead. It only returns text: write " +
|
|
74
|
+
"the lines into the user's .env.local yourself when they ask.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
names: {
|
|
79
|
+
type: "array",
|
|
80
|
+
items: { type: "string" },
|
|
81
|
+
description: 'Route names to keep, e.g. ["acme"]; all routes when absent',
|
|
82
|
+
},
|
|
83
|
+
prefix: { type: "string", description: "Put in front of every key, e.g. VITE_" },
|
|
84
|
+
strip: { type: "string", description: "Suffix dropped from names, e.g. acme" },
|
|
85
|
+
direct: { type: "boolean", description: "Use http://localhost:<port> (default false)" },
|
|
86
|
+
},
|
|
87
|
+
additionalProperties: false,
|
|
88
|
+
},
|
|
89
|
+
call: async (args) => {
|
|
90
|
+
const names = Array.isArray(args.names)
|
|
91
|
+
? args.names.filter((name) => typeof name === "string" && name !== "")
|
|
92
|
+
: [];
|
|
93
|
+
const snapshot = await readSnapshot(client);
|
|
94
|
+
if (!snapshot.ok)
|
|
95
|
+
return snapshot.error;
|
|
96
|
+
const selected = selectEnvRoutes(snapshot.value.routes, names);
|
|
97
|
+
if (!selected.ok)
|
|
98
|
+
return { summary: selected.error, isError: true };
|
|
99
|
+
const entries = envEntries(selected.value, {
|
|
100
|
+
direct: args.direct === true,
|
|
101
|
+
...(typeof args.prefix === "string" ? { prefix: args.prefix.trim() } : {}),
|
|
102
|
+
...(typeof args.strip === "string" ? { strip: args.strip } : {}),
|
|
103
|
+
});
|
|
104
|
+
if (!entries.ok)
|
|
105
|
+
return { summary: entries.error, isError: true };
|
|
106
|
+
if (entries.value.length === 0)
|
|
107
|
+
return { summary: "No named routes yet.", data: { variables: [] } };
|
|
108
|
+
return {
|
|
109
|
+
summary: entries.value.map(formatEnvLine).join("\n"),
|
|
110
|
+
data: { variables: entries.value },
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
},
|
|
54
114
|
{
|
|
55
115
|
name: "list_detected_ports",
|
|
56
116
|
title: "List ports without a name",
|
|
@@ -553,8 +613,11 @@ function terminalItem(terminal) {
|
|
|
553
613
|
port: terminal.naming?.port ?? null,
|
|
554
614
|
};
|
|
555
615
|
}
|
|
556
|
-
async function readSnapshot(client) {
|
|
557
|
-
const
|
|
616
|
+
async function readSnapshot(client, includeStudio = false) {
|
|
617
|
+
const path = includeStudio
|
|
618
|
+
? `${API_PATHS.snapshot}?${INCLUDE_STUDIO_PARAM}=1`
|
|
619
|
+
: API_PATHS.snapshot;
|
|
620
|
+
const result = await client.read(path);
|
|
558
621
|
if (!result.ok)
|
|
559
622
|
return err(failure(result.error));
|
|
560
623
|
return ok(result.value);
|
package/dist/cli/studio-items.js
CHANGED
|
@@ -22,10 +22,13 @@ export function detectedItem(port) {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
export function proxyItem(proxy) {
|
|
25
|
-
|
|
25
|
+
const item = { running: proxy.running, port: proxy.port };
|
|
26
|
+
return proxy.publicPort == null ? item : { ...item, publicPort: proxy.publicPort };
|
|
26
27
|
}
|
|
28
|
+
/** `publicPort` is set while Studio forwards it to the proxy, for URLs without a port. */
|
|
27
29
|
export function describeProxy(proxy) {
|
|
28
30
|
if (!proxy.running)
|
|
29
31
|
return "proxy stopped";
|
|
30
|
-
|
|
32
|
+
const running = proxy.port === null ? "proxy running" : `proxy running on port ${proxy.port}`;
|
|
33
|
+
return proxy.publicPort == null ? running : `${running}, names open on port ${proxy.publicPort}`;
|
|
31
34
|
}
|