@palettelab/cli 0.3.3 → 0.3.5

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() {
@@ -29,17 +28,40 @@ function imageExistsLocally(image) {
29
28
  }
30
29
 
31
30
  function tryPullImage(image) {
32
- const res = spawnSync("docker", ["pull", image], { stdio: "inherit" })
33
- return res.status === 0
31
+ const res = spawnSync("docker", ["pull", image], { encoding: "utf8" })
32
+ if (res.stdout) process.stdout.write(res.stdout)
33
+ if (res.stderr) process.stderr.write(res.stderr)
34
+ return {
35
+ ok: res.status === 0,
36
+ output: `${res.stdout || ""}${res.stderr || ""}`,
37
+ }
34
38
  }
35
39
 
36
40
  function ensureImageAvailable(image) {
37
41
  // Accept a locally-built image (e.g. `platform-dev:test` from `docker build`)
38
42
  // without attempting a registry pull — local is authoritative if present.
39
- if (imageExistsLocally(image)) return true
43
+ if (imageExistsLocally(image)) return { ok: true, output: "" }
40
44
  return tryPullImage(image)
41
45
  }
42
46
 
47
+ function imagePullHelp(image, output) {
48
+ const arch = `${process.platform}/${process.arch}`
49
+ const archHint =
50
+ /no matching manifest|manifest.*unknown|not found/i.test(output || "")
51
+ ? ` • The image tag may not include your CPU architecture (${arch}).\n` +
52
+ " Platform maintainers: publish a multi-arch image for linux/amd64 and linux/arm64.\n"
53
+ : ""
54
+ return (
55
+ `\n[pltt] could not pull ${image}.\n` +
56
+ ` Most common causes:\n` +
57
+ archHint +
58
+ ` • The image hasn't been published to the registry yet.\n` +
59
+ ` Platform maintainers: run the platform-dev publish workflow and push the latest tag.\n` +
60
+ ` • You are not logged in: docker login ghcr.io -u <github-user> -p <pat>\n` +
61
+ ` • You are pointing at a different tag: set PALETTE_DEV_IMAGE=<your-image> to override.\n`
62
+ )
63
+ }
64
+
43
65
  async function run(args, { cwd }) {
44
66
  const { flags, rest } = parseFlags(args)
45
67
  const cloud = rest.includes("--cloud")
@@ -61,6 +83,9 @@ async function run(args, { cwd }) {
61
83
  const pluginId = manifest.id
62
84
  const frontendEntry = manifest.frontend?.entry || "./frontend/src/index.tsx"
63
85
  const frontendBundle = path.join(cwd, ".palette", "dist", "frontend.mjs")
86
+ const ports = await resolveDevPorts()
87
+ const frontendPort = String(ports.frontend)
88
+ const backendPort = String(ports.backend)
64
89
 
65
90
  ensureDocker()
66
91
 
@@ -83,22 +108,22 @@ async function run(args, { cwd }) {
83
108
 
84
109
  console.log(`[pltt] starting ${DEFAULT_IMAGE} via docker compose`)
85
110
  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}`)
111
+ if (ports.frontend !== ports.preferredFrontend) {
112
+ console.log(`[pltt] frontend port ${ports.preferredFrontend} is busy; using ${ports.frontend}`)
113
+ }
114
+ if (ports.backend !== ports.preferredBackend) {
115
+ console.log(`[pltt] backend port ${ports.preferredBackend} is busy; using ${ports.backend}`)
116
+ }
117
+ console.log(`[pltt] frontend: http://localhost:${frontendPort}/apps/${pluginId}`)
118
+ console.log(`[pltt] backend: http://localhost:${backendPort}/api/v1/plugins/${pluginId}`)
88
119
 
89
120
  // Pre-pull so we can give a useful error if the image isn't reachable
90
121
  // (common cause: maintainer hasn't pushed it yet, or `docker login ghcr.io`
91
122
  // missing). Without this, the `docker compose up` failure is cryptic.
92
- if (!ensureImageAvailable(DEFAULT_IMAGE)) {
123
+ const image = ensureImageAvailable(DEFAULT_IMAGE)
124
+ if (!image.ok) {
93
125
  if (frontendWatcher) await frontendWatcher.dispose()
94
- console.error(
95
- `\n[pltt] could not pull ${DEFAULT_IMAGE}.\n` +
96
- ` Most common causes:\n` +
97
- ` • The image hasn't been published to the registry yet.\n` +
98
- ` Platform maintainers: see docker/platform-dev/README.md for the build + push flow.\n` +
99
- ` • You are not logged in: docker login ghcr.io -u <github-user> -p <pat>\n` +
100
- ` • You are pointing at a different tag: set PALETTE_DEV_IMAGE=<your-image> to override.\n`,
101
- )
126
+ console.error(imagePullHelp(DEFAULT_IMAGE, image.output))
102
127
  process.exit(1)
103
128
  }
104
129
 
@@ -107,8 +132,8 @@ async function run(args, { cwd }) {
107
132
  PALETTE_DEV_IMAGE: DEFAULT_IMAGE,
108
133
  PALETTE_ACTIVE_PLUGIN: pluginId,
109
134
  PALETTE_PLUGIN_DIR: cwd,
110
- PALETTE_FRONTEND_PORT: FRONTEND_PORT,
111
- PALETTE_BACKEND_PORT: BACKEND_PORT,
135
+ PALETTE_FRONTEND_PORT: frontendPort,
136
+ PALETTE_BACKEND_PORT: backendPort,
112
137
  }
113
138
 
114
139
  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.5",
4
4
  "description": "Developer CLI for building Palette platform plugins — no platform source access required.",
5
5
  "bin": {
6
6
  "pltt": "bin/pltt.js"