@palettelab/cli 0.3.3 → 0.3.4

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 CHANGED
@@ -53,12 +53,14 @@ pltt dev --cloud --env staging
53
53
  Under the hood this runs `docker compose up` with a bundled compose file. It starts:
54
54
 
55
55
  - Postgres + Redis
56
- - The Palette frontend on http://localhost:3000
57
- - The Palette backend on http://localhost:8000
58
- - Your plugin at http://localhost:3000/apps/<your-id>
56
+ - The Palette frontend on the first available port starting at http://localhost:3000
57
+ - The Palette backend on the first available port starting at http://localhost:8000
58
+ - Your plugin at the frontend URL printed by the CLI
59
59
 
60
60
  Your plugin directory is mounted into the container at `/plugins/<your-id>`. Edits to your frontend/backend sources hot-reload.
61
61
 
62
+ `3000` and `8000` are preferred defaults, not hard requirements. If either port is already in use, `pltt dev` automatically picks the next free port and prints the actual URLs.
63
+
62
64
  `pltt dev --cloud` skips Docker and publishes a reviewable preview to a configured cloud sandbox. It defaults to `--env staging` unless `--env` or `PALETTE_ENV` is set.
63
65
 
64
66
  Environment variables:
@@ -66,8 +68,8 @@ Environment variables:
66
68
  | Name | Default | Purpose |
67
69
  |---|---|---|
68
70
  | `PALETTE_DEV_IMAGE` | `ghcr.io/palette-lab/platform-dev:latest` | Override the platform image |
69
- | `PALETTE_FRONTEND_PORT` | `3000` | Host port for the frontend |
70
- | `PALETTE_BACKEND_PORT` | `8000` | Host port for the backend |
71
+ | `PALETTE_FRONTEND_PORT` | `3000` | Preferred starting host port for the frontend |
72
+ | `PALETTE_BACKEND_PORT` | `8000` | Preferred starting host port for the backend |
71
73
 
72
74
  ### `pltt doctor`
73
75
 
@@ -5,12 +5,11 @@ const { spawn, spawnSync } = require("child_process")
5
5
  const { loadManifest } = require("../manifest")
6
6
  const { watchFrontend } = require("../bundler")
7
7
  const { parseFlags } = require("../environments")
8
+ const { resolveDevPorts } = require("../ports")
8
9
  const publish = require("./publish")
9
10
 
10
11
  const DEFAULT_IMAGE =
11
12
  process.env.PALETTE_DEV_IMAGE || "ghcr.io/palette-lab/platform-dev:latest"
12
- const FRONTEND_PORT = process.env.PALETTE_FRONTEND_PORT || "3000"
13
- const BACKEND_PORT = process.env.PALETTE_BACKEND_PORT || "8000"
14
13
  const COMPOSE_FILE = path.resolve(__dirname, "..", "..", "platform-dev", "docker-compose.yml")
15
14
 
16
15
  function ensureDocker() {
@@ -61,6 +60,9 @@ async function run(args, { cwd }) {
61
60
  const pluginId = manifest.id
62
61
  const frontendEntry = manifest.frontend?.entry || "./frontend/src/index.tsx"
63
62
  const frontendBundle = path.join(cwd, ".palette", "dist", "frontend.mjs")
63
+ const ports = await resolveDevPorts()
64
+ const frontendPort = String(ports.frontend)
65
+ const backendPort = String(ports.backend)
64
66
 
65
67
  ensureDocker()
66
68
 
@@ -83,8 +85,14 @@ async function run(args, { cwd }) {
83
85
 
84
86
  console.log(`[pltt] starting ${DEFAULT_IMAGE} via docker compose`)
85
87
  console.log(`[pltt] mounting ${cwd} → /plugins/${pluginId}`)
86
- console.log(`[pltt] frontend: http://localhost:${FRONTEND_PORT}/apps/${pluginId}`)
87
- console.log(`[pltt] backend: http://localhost:${BACKEND_PORT}/api/v1/plugins/${pluginId}`)
88
+ if (ports.frontend !== ports.preferredFrontend) {
89
+ console.log(`[pltt] frontend port ${ports.preferredFrontend} is busy; using ${ports.frontend}`)
90
+ }
91
+ if (ports.backend !== ports.preferredBackend) {
92
+ console.log(`[pltt] backend port ${ports.preferredBackend} is busy; using ${ports.backend}`)
93
+ }
94
+ console.log(`[pltt] frontend: http://localhost:${frontendPort}/apps/${pluginId}`)
95
+ console.log(`[pltt] backend: http://localhost:${backendPort}/api/v1/plugins/${pluginId}`)
88
96
 
89
97
  // Pre-pull so we can give a useful error if the image isn't reachable
90
98
  // (common cause: maintainer hasn't pushed it yet, or `docker login ghcr.io`
@@ -107,8 +115,8 @@ async function run(args, { cwd }) {
107
115
  PALETTE_DEV_IMAGE: DEFAULT_IMAGE,
108
116
  PALETTE_ACTIVE_PLUGIN: pluginId,
109
117
  PALETTE_PLUGIN_DIR: cwd,
110
- PALETTE_FRONTEND_PORT: FRONTEND_PORT,
111
- PALETTE_BACKEND_PORT: BACKEND_PORT,
118
+ PALETTE_FRONTEND_PORT: frontendPort,
119
+ PALETTE_BACKEND_PORT: backendPort,
112
120
  }
113
121
 
114
122
  const projectName = `palette-dev-${pluginId}`
@@ -1,11 +1,11 @@
1
1
  "use strict"
2
2
 
3
3
  const fs = require("fs")
4
- const net = require("net")
5
4
  const path = require("path")
6
5
  const { spawnSync } = require("child_process")
7
6
  const { loadManifest, validateManifest } = require("../manifest")
8
7
  const { bundleFrontend } = require("../bundler")
8
+ const { resolveDevPorts } = require("../ports")
9
9
 
10
10
  const DEFAULT_IMAGE =
11
11
  process.env.PALETTE_DEV_IMAGE || "ghcr.io/palette-lab/platform-dev:latest"
@@ -39,17 +39,6 @@ function imageExistsLocally(image) {
39
39
  return spawnSync("docker", ["image", "inspect", image], { stdio: "ignore" }).status === 0
40
40
  }
41
41
 
42
- function canBindPort(port) {
43
- return new Promise((resolve) => {
44
- const server = net.createServer()
45
- server.once("error", () => resolve(false))
46
- server.once("listening", () => {
47
- server.close(() => resolve(true))
48
- })
49
- server.listen(port, "127.0.0.1")
50
- })
51
- }
52
-
53
42
  function checkEntry(cwd, label, rel) {
54
43
  if (!rel) return 0
55
44
  const abs = path.resolve(cwd, rel)
@@ -81,19 +70,18 @@ async function run(args, { cwd }) {
81
70
  )
82
71
  }
83
72
 
84
- const frontendPortFree = await canBindPort(FRONTEND_PORT)
85
- if (frontendPortFree) ok(`frontend port ${FRONTEND_PORT} is available`)
86
- else failures += fail(
87
- `frontend port ${FRONTEND_PORT} is already in use`,
88
- `Run PALETTE_FRONTEND_PORT=${FRONTEND_PORT + 1} pltt dev`,
89
- )
90
-
91
- const backendPortFree = await canBindPort(BACKEND_PORT)
92
- if (backendPortFree) ok(`backend port ${BACKEND_PORT} is available`)
93
- else failures += fail(
94
- `backend port ${BACKEND_PORT} is already in use`,
95
- `Run PALETTE_BACKEND_PORT=${BACKEND_PORT + 1} pltt dev`,
96
- )
73
+ try {
74
+ const ports = await resolveDevPorts({ frontend: FRONTEND_PORT, backend: BACKEND_PORT })
75
+ if (ports.frontend === FRONTEND_PORT) ok(`frontend port ${FRONTEND_PORT} is available`)
76
+ else warn(`frontend port ${FRONTEND_PORT} is already in use`, `pltt dev will use ${ports.frontend}`)
77
+ if (ports.backend === BACKEND_PORT) ok(`backend port ${BACKEND_PORT} is available`)
78
+ else warn(`backend port ${BACKEND_PORT} is already in use`, `pltt dev will use ${ports.backend}`)
79
+ } catch (err) {
80
+ failures += fail(
81
+ `could not find free dev ports: ${err instanceof Error ? err.message : String(err)}`,
82
+ "Free a local port or set PALETTE_FRONTEND_PORT / PALETTE_BACKEND_PORT to a wider available range.",
83
+ )
84
+ }
97
85
 
98
86
  let manifest
99
87
  try {
package/lib/ports.js ADDED
@@ -0,0 +1,42 @@
1
+ "use strict"
2
+
3
+ const net = require("net")
4
+
5
+ function canBindPort(port, host = "127.0.0.1") {
6
+ return new Promise((resolve) => {
7
+ const server = net.createServer()
8
+ server.once("error", () => resolve(false))
9
+ server.once("listening", () => {
10
+ server.close(() => resolve(true))
11
+ })
12
+ server.listen(port, host)
13
+ })
14
+ }
15
+
16
+ async function findFreePort(preferred, { host = "127.0.0.1", maxAttempts = 100 } = {}) {
17
+ const start = Number(preferred)
18
+ if (!Number.isInteger(start) || start <= 0 || start > 65535) {
19
+ throw new Error(`invalid port: ${preferred}`)
20
+ }
21
+ for (let port = start; port < start + maxAttempts && port <= 65535; port++) {
22
+ if (await canBindPort(port, host)) return port
23
+ }
24
+ throw new Error(`no free port found from ${start} to ${Math.min(start + maxAttempts - 1, 65535)}`)
25
+ }
26
+
27
+ async function resolveDevPorts({
28
+ frontend = process.env.PALETTE_FRONTEND_PORT || 3000,
29
+ backend = process.env.PALETTE_BACKEND_PORT || 8000,
30
+ } = {}) {
31
+ const frontendPort = await findFreePort(frontend)
32
+ const backendStart = Number(backend) === frontendPort ? frontendPort + 1 : backend
33
+ const backendPort = await findFreePort(backendStart)
34
+ return {
35
+ frontend: frontendPort,
36
+ backend: backendPort,
37
+ preferredFrontend: Number(frontend),
38
+ preferredBackend: Number(backend),
39
+ }
40
+ }
41
+
42
+ module.exports = { canBindPort, findFreePort, resolveDevPorts }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palettelab/cli",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Developer CLI for building Palette platform plugins — no platform source access required.",
5
5
  "bin": {
6
6
  "pltt": "bin/pltt.js"