portless-studio 0.1.0 → 0.1.2

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.
Files changed (41) hide show
  1. package/README.md +78 -192
  2. package/dist/cli/cli-args.js +29 -3
  3. package/dist/cli/index.js +10 -15
  4. package/dist/cli/login-service.js +16 -0
  5. package/dist/cli/mcp/studio-tools.js +20 -7
  6. package/dist/cli/studio-items.js +5 -2
  7. package/dist/core/api/contract.js +19 -1
  8. package/dist/core/config/studio-config.js +6 -0
  9. package/dist/core/doctor/platform-commands.js +5 -2
  10. package/dist/core/ports/direct-url.js +7 -0
  11. package/dist/core/ports/docker-container.js +85 -0
  12. package/dist/core/routes/route.js +14 -2
  13. package/dist/core/updates/version.js +62 -0
  14. package/dist/server/application/auto-namer.js +14 -0
  15. package/dist/server/application/container-source.js +19 -0
  16. package/dist/server/application/doctor.js +225 -9
  17. package/dist/server/application/port-discovery.js +49 -6
  18. package/dist/server/application/proxy-start-check.js +83 -0
  19. package/dist/server/application/short-urls.js +136 -0
  20. package/dist/server/application/studio-runtime.js +46 -7
  21. package/dist/server/application/terminals.js +142 -15
  22. package/dist/server/application/update-check.js +66 -0
  23. package/dist/server/infrastructure/config/json-update-cache.js +35 -0
  24. package/dist/server/infrastructure/portless/portless-registry.js +31 -4
  25. package/dist/server/infrastructure/portless/state-files.js +23 -7
  26. package/dist/server/infrastructure/system/docker-containers.js +64 -0
  27. package/dist/server/infrastructure/system/doctor-facts.js +44 -1
  28. package/dist/server/infrastructure/system/npm-registry.js +20 -0
  29. package/dist/server/infrastructure/system/port-forwarder.js +78 -0
  30. package/dist/server/infrastructure/system/process-runner.js +2 -1
  31. package/dist/server/infrastructure/terminal/process-tree.js +71 -0
  32. package/dist/server/infrastructure/terminal/terminal-spawner.js +44 -3
  33. package/dist/server/interface/http/http-server.js +43 -2
  34. package/dist/server/interface/http/json-io.js +1 -0
  35. package/dist/server/main.js +62 -8
  36. package/dist/web/assets/index-BDEwEBPS.js +15 -0
  37. package/dist/web/assets/index-C9wkL1J8.css +1 -0
  38. package/dist/web/index.html +2 -2
  39. package/package.json +2 -2
  40. package/dist/web/assets/index-CA7HpYbk.css +0 -1
  41. package/dist/web/assets/index-CC6BquZf.js +0 -15
@@ -2,33 +2,48 @@ import { randomBytes } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
+ import { stopContainer } from "./application/container-source.js";
5
6
  import { createDoctorService } from "./application/doctor.js";
6
7
  import { OpenWith } from "./application/open-with.js";
7
8
  import { controlProxy } from "./application/proxy-control.js";
9
+ import { confirmProxyStarts } from "./application/proxy-start-check.js";
8
10
  import { createAlias, renameAlias } from "./application/route-use-cases.js";
11
+ import { publicUrls, SHORT_URL_PORT, ShortUrls } from "./application/short-urls.js";
9
12
  import { stopProcess } from "./application/stop-process.js";
10
13
  import { StudioRuntime } from "./application/studio-runtime.js";
11
14
  import { TerminalManager } from "./application/terminals.js";
15
+ import { UpdateChecker, updateChecksAllowed } from "./application/update-check.js";
12
16
  import { systemAppCatalog } from "./infrastructure/apps/app-catalog.js";
13
17
  import { createDetachedLauncher } from "./infrastructure/apps/detached-launcher.js";
14
18
  import { JsonCustomAppStore, JsonRouteProjectStore, openWithPathFor, routeProjectsPathFor, } from "./infrastructure/apps/json-stores.js";
15
19
  import { autoNamedPathFor, JsonAutoNamedStore, } from "./infrastructure/config/json-auto-named-store.js";
16
20
  import { defaultConfigPath, JsonConfigStore } from "./infrastructure/config/json-config-store.js";
21
+ import { JsonUpdateCache, updateCheckPathFor } from "./infrastructure/config/json-update-cache.js";
17
22
  import { proxyStartRecordPathFor, readProxyStartRecord, recordProxyStarts, } from "./infrastructure/config/proxy-start-record.js";
18
23
  import { PortlessRegistry } from "./infrastructure/portless/portless-registry.js";
19
- import { resolveStateDir } from "./infrastructure/portless/state-files.js";
20
- import { collectDoctorFacts, proxyPidsAmong } from "./infrastructure/system/doctor-facts.js";
24
+ import { inspectRoutesFile, resolveStateDir } from "./infrastructure/portless/state-files.js";
25
+ import { DockerContainers } from "./infrastructure/system/docker-containers.js";
26
+ import { collectDoctorFacts, proxyPidsAmong, reachesPortless, testBind, } from "./infrastructure/system/doctor-facts.js";
21
27
  import { FaviconFetcher } from "./infrastructure/system/favicon-fetcher.js";
28
+ import { fetchLatestVersion } from "./infrastructure/system/npm-registry.js";
22
29
  import { OsNotifier } from "./infrastructure/system/os-notifier.js";
30
+ import { startForwarder } from "./infrastructure/system/port-forwarder.js";
23
31
  import { ProjectInfoReader } from "./infrastructure/system/project-info.js";
24
32
  import { nodeSystemIo } from "./infrastructure/system/system-io.js";
25
33
  import { createPortScanner, listPortsFor, probeHttpResponse, } from "./infrastructure/system/system-port-scanner.js";
26
34
  import { SystemProcessControl } from "./infrastructure/system/system-process-control.js";
27
- import { createTerminalSpawner, detectTerminalMode, nodeTerminalFolders, } from "./infrastructure/terminal/terminal-spawner.js";
35
+ import { systemProcessTree } from "./infrastructure/terminal/process-tree.js";
36
+ import { createTerminalSpawner, detectTerminalMode, interactiveShellName, nodeTerminalFolders, } from "./infrastructure/terminal/terminal-spawner.js";
28
37
  import { createHttpServer } from "./interface/http/http-server.js";
29
38
  export { defaultConfigPath } from "./infrastructure/config/json-config-store.js";
30
39
  /** Only this machine can connect; see docs/architecture.md, Security. */
31
40
  const LOOPBACK = "127.0.0.1";
41
+ /** `PORTLESS_STUDIO_SHORT_URL_PORT`, when it is a valid port; port 80 otherwise. */
42
+ export function shortUrlPortFrom(env) {
43
+ const raw = env.PORTLESS_STUDIO_SHORT_URL_PORT;
44
+ const port = raw === undefined ? Number.NaN : Number(raw);
45
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : SHORT_URL_PORT;
46
+ }
32
47
  // Lets startServer reuse the runtime behind services built here, so both share one scan cache.
33
48
  const runtimes = new WeakMap();
34
49
  export function createStudioServices(options = {}) {
@@ -39,11 +54,20 @@ export function createStudioServices(options = {}) {
39
54
  bin: options.portlessBin ?? (process.env.PORTLESS_BIN || "portless"),
40
55
  ...(options.npmBin === undefined ? {} : { npmBin: options.npmBin }),
41
56
  });
42
- // Every proxy Studio starts is written down, so `uninstall` stops only Studio's own.
43
- const registry = recordProxyStarts(portless, proxyStartRecordPathFor(configPath));
44
57
  const scanner = createPortScanner();
58
+ const proxyPids = (pids) => proxyPidsAmong(process.platform, nodeSystemIo, pids);
59
+ const shortUrls = new ShortUrls({
60
+ forwarding: { listen: (port, target) => startForwarder({ port, targetPort: target }) },
61
+ port: options.shortUrlPort ?? shortUrlPortFrom(process.env),
62
+ log: (line) => console.log(line),
63
+ reachesProxy: (port) => reachesPortless(port),
64
+ });
65
+ // A start counts only once the proxy listens, and every proxy Studio starts is written down,
66
+ // so `uninstall` stops only Studio's own. Route URLs drop the port while Studio forwards it.
67
+ const registry = publicUrls(recordProxyStarts(confirmProxyStarts(portless, { scan: () => scanner.scan(), proxyPids }), proxyStartRecordPathFor(configPath)), () => shortUrls.publicPort());
45
68
  const configStore = new JsonConfigStore(configPath);
46
69
  const notifier = new OsNotifier({ platform: process.platform });
70
+ const containers = new DockerContainers();
47
71
  const runtime = new StudioRuntime({
48
72
  registry,
49
73
  scanner,
@@ -51,11 +75,13 @@ export function createStudioServices(options = {}) {
51
75
  projects: new ProjectInfoReader(),
52
76
  studioPort: options.studioPort ?? null,
53
77
  probe: (port) => probeHttpResponse(port),
54
- proxyPids: (pids) => proxyPidsAmong(process.platform, nodeSystemIo, pids),
78
+ proxyPids,
55
79
  autoNamedStore: new JsonAutoNamedStore(autoNamedPathFor(configPath)),
56
80
  notifier,
57
81
  icons: new FaviconFetcher(),
58
82
  routeProjectStore: new JsonRouteProjectStore(routeProjectsPathFor(configPath)),
83
+ containers,
84
+ shortUrls,
59
85
  });
60
86
  const openWith = new OpenWith({
61
87
  catalog: systemAppCatalog(),
@@ -66,6 +92,8 @@ export function createStudioServices(options = {}) {
66
92
  const terminals = new TerminalManager({
67
93
  spawner: lazySpawner(),
68
94
  folders: nodeTerminalFolders(),
95
+ processTree: systemProcessTree(),
96
+ homeDir: os.homedir(),
69
97
  workspace: {
70
98
  openableFolders: () => runtime.openableFolders(),
71
99
  snapshot: () => runtime.readSnapshot(),
@@ -97,10 +125,24 @@ export function createStudioServices(options = {}) {
97
125
  probe: (port) => probeHttpResponse(port),
98
126
  ...(studioService ? { loginService: () => studioService.status() } : {}),
99
127
  systemNotifier: () => notifier.availability(),
128
+ routesFile: () => inspectRoutesFile(stateDir),
129
+ docker: () => containers.read(),
130
+ lowPort: testBind,
131
+ shortUrls: (config) => shortUrls.inspect(config),
100
132
  }),
101
133
  controlProxy: proxy,
102
134
  updateConfig: (config) => runtime.updateConfig(config),
135
+ syncShortUrls: (config) => shortUrls.sync(config),
103
136
  });
137
+ const version = options.version;
138
+ const updates = version === undefined
139
+ ? undefined
140
+ : new UpdateChecker({
141
+ current: version,
142
+ enabled: async () => updateChecksAllowed(process.env) && (await runtime.settings()).checkForUpdates,
143
+ fetchLatest: () => fetchLatestVersion(),
144
+ cache: new JsonUpdateCache(updateCheckPathFor(configPath)),
145
+ });
104
146
  const services = {
105
147
  registry,
106
148
  scanner,
@@ -113,10 +155,13 @@ export function createStudioServices(options = {}) {
113
155
  stoppablePids: () => runtime.stoppablePids(),
114
156
  selfPid: process.pid,
115
157
  }),
158
+ stopContainer: (name) => stopContainer(name, containers),
116
159
  studioService,
117
160
  openWith,
118
161
  terminals,
119
162
  openUrl: options.openUrl,
163
+ updates,
164
+ shortUrls,
120
165
  };
121
166
  runtimes.set(services, runtime);
122
167
  return services;
@@ -130,6 +175,7 @@ export function createUninstallServices(configPath = defaultConfigPath()) {
130
175
  recordPath,
131
176
  routeProjectsPathFor(configPath),
132
177
  openWithPathFor(configPath),
178
+ updateCheckPathFor(configPath),
133
179
  ];
134
180
  const processes = new SystemProcessControl({
135
181
  platform: process.platform,
@@ -160,6 +206,7 @@ export async function startServer(options) {
160
206
  scanner: services.scanner,
161
207
  configStore: services.configStore,
162
208
  projects: new ProjectInfoReader(),
209
+ ...(services.shortUrls ? { shortUrls: services.shortUrls } : {}),
163
210
  });
164
211
  const token = randomBytes(32).toString("base64url");
165
212
  const http = createHttpServer({
@@ -170,10 +217,12 @@ export async function startServer(options) {
170
217
  doctor: services.doctor,
171
218
  controlProxy: services.controlProxy,
172
219
  stopProcess: services.stopProcess,
220
+ stopContainer: services.stopContainer,
173
221
  ...(services.studioService ? { studioService: services.studioService } : {}),
174
222
  ...(services.openWith ? { openWith: services.openWith } : {}),
175
223
  ...(services.terminals ? { terminals: services.terminals } : {}),
176
224
  ...(services.openUrl ? { openUrl: services.openUrl } : {}),
225
+ ...(services.updates ? { updates: services.updates } : {}),
177
226
  });
178
227
  await new Promise((resolve, reject) => {
179
228
  http.server.once("error", reject);
@@ -185,6 +234,8 @@ export async function startServer(options) {
185
234
  const { port } = http.server.address();
186
235
  studio.setStudioPort(port);
187
236
  studio.start();
237
+ // The short-URL forwarder settles before anyone prints or registers a URL.
238
+ await studio.ready();
188
239
  return {
189
240
  url: `http://${LOOPBACK}:${port}`,
190
241
  port,
@@ -195,8 +246,10 @@ export async function startServer(options) {
195
246
  studio.stop();
196
247
  services.terminals?.closeAll();
197
248
  http.closeStreams();
198
- http.server.close(() => resolve());
199
- http.server.closeAllConnections();
249
+ void (services.shortUrls?.close() ?? Promise.resolve()).finally(() => {
250
+ http.server.close(() => resolve());
251
+ http.server.closeAllConnections();
252
+ });
200
253
  }),
201
254
  };
202
255
  }
@@ -211,6 +264,7 @@ function lazySpawner() {
211
264
  get mode() {
212
265
  return mode;
213
266
  },
267
+ shellName: interactiveShellName(),
214
268
  spawn: async (request) => {
215
269
  real ??= detectTerminalMode(process.platform).then((detected) => createTerminalSpawner({ mode: detected }));
216
270
  const spawner = await real;