@palettelab/cli 0.3.15 → 0.3.16

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
@@ -7,7 +7,8 @@ The installed executable is `pltt`.
7
7
  ## Requirements
8
8
 
9
9
  - Node.js 18+
10
- - Docker Desktop (for `pltt dev`)
10
+ - Python 3.12+ for local backend simulation
11
+ - Docker Desktop only for `pltt dev --platform`
11
12
 
12
13
  ## Install
13
14
 
@@ -43,31 +44,37 @@ Templates:
43
44
 
44
45
  ### `pltt dev`
45
46
 
46
- Boot the platform locally with your plugin mounted live. Run this from inside your plugin directory.
47
+ Run a no-Docker local SDK simulator with your plugin mounted live. Run this from inside your plugin directory.
47
48
 
48
49
  ```bash
49
50
  pltt dev
51
+ pltt dev --platform
50
52
  pltt dev --cloud --env staging
51
53
  ```
52
54
 
53
- Under the hood this runs `docker compose up` with a bundled compose file. It starts:
55
+ By default this starts:
54
56
 
55
- - Postgres + Redis
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
57
+ - A small local app shell on the first available port starting at http://localhost:3000
58
+ - A local FastAPI backend runner on the first available port starting at http://localhost:8000
59
+ - A mock Palette platform context for `usePlatform()`, toasts, org/user data, and authenticated API calls
59
60
 
60
- Your plugin directory is mounted into the container at `/plugins/<your-id>`. Edits to your frontend/backend sources hot-reload.
61
+ Your frontend entry is bundled and watched. Your backend entry is loaded under
62
+ `/api/v1/plugins/<your-id>/*` with a dev `PluginContext`, so normal SDK calls
63
+ work without Docker or platform source.
61
64
 
62
65
  `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
66
 
67
+ `pltt dev --platform` runs the full Docker `platform-dev` image for deeper
68
+ integration/parity testing. It pulls `ghcr.io/palette-lab/platform-dev:latest`
69
+ and mounts your plugin at `/plugins/<your-id>`.
70
+
64
71
  `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, adds a 24-hour preview TTL by default, and prints the preview/status/log commands returned by the platform.
65
72
 
66
73
  Environment variables:
67
74
 
68
75
  | Name | Default | Purpose |
69
76
  |---|---|---|
70
- | `PALETTE_DEV_IMAGE` | `ghcr.io/palette-lab/platform-dev:latest` | Override the platform image |
77
+ | `PALETTE_DEV_IMAGE` | `ghcr.io/palette-lab/platform-dev:latest` | Override the platform image for `--platform` |
71
78
  | `PALETTE_FRONTEND_PORT` | `3000` | Preferred starting host port for the frontend |
72
79
  | `PALETTE_BACKEND_PORT` | `8000` | Preferred starting host port for the backend |
73
80
 
@@ -167,8 +174,9 @@ If no plugin ID is provided, the CLI uses the current `palette-plugin.json` or `
167
174
  `@palettelab/cli` itself contains only:
168
175
 
169
176
  - `bin/pltt.js` — entry point
177
+ - `backend-sdk/` — backend SDK files used by local contract tests and simulator
170
178
  - `lib/` — pure-Node command implementations (no runtime dependencies)
171
- - `platform-dev/docker-compose.yml` — compose file for `pltt dev`
179
+ - `platform-dev/docker-compose.yml` — compose file for `pltt dev --platform`
172
180
  - `template-fallback/` — offline fallback for `pltt init` if git is unavailable
173
181
 
174
182
  ## See also
package/lib/cli.js CHANGED
@@ -14,7 +14,7 @@ const COMMANDS = {
14
14
  init: { run: init, help: "Scaffold a new plugin directory from the template" },
15
15
  dev: {
16
16
  run: dev,
17
- help: "Boot the platform-dev container and mount the current plugin for live development",
17
+ help: "Run the local SDK simulator; use --platform for Docker platform parity",
18
18
  },
19
19
  doctor: {
20
20
  run: doctor,
@@ -50,6 +50,7 @@ function printHelp() {
50
50
  console.log(" --env <name> Target environment from ~/.palette/config.json (default: local)")
51
51
  console.log(" -y, --yes Skip interactive confirmation for production pushes")
52
52
  console.log("\nDev flags:")
53
+ console.log(" --platform Run the full Docker platform-dev container")
53
54
  console.log(" --cloud Publish a reviewable preview to a configured cloud sandbox")
54
55
  console.log("\nInit flags:")
55
56
  console.log(" --template <name> One of: dashboard, agent-tool, external-service, database, frontend-only")
@@ -6,6 +6,7 @@ const { loadManifest } = require("../manifest")
6
6
  const { watchFrontend } = require("../bundler")
7
7
  const { parseFlags } = require("../environments")
8
8
  const { resolveDevPorts } = require("../ports")
9
+ const { startSimulator } = require("../dev-simulator")
9
10
  const publish = require("./publish")
10
11
 
11
12
  const DEFAULT_PLATFORM_IMAGE = "ghcr.io/palette-lab/platform-dev:latest"
@@ -16,7 +17,7 @@ function ensureDocker() {
16
17
  const check = spawnSync("docker", ["info"], { stdio: "ignore" })
17
18
  if (check.status !== 0) {
18
19
  console.error(
19
- "[pltt] docker is required for `pltt dev`. Install Docker Desktop and make sure it is running.",
20
+ "[pltt] docker is required for `pltt dev --platform`. Install Docker Desktop and make sure it is running.",
20
21
  )
21
22
  process.exit(1)
22
23
  }
@@ -76,6 +77,7 @@ function imagePullHelp(image, output) {
76
77
  async function run(args, { cwd }) {
77
78
  const { flags, rest } = parseFlags(args)
78
79
  const cloud = rest.includes("--cloud")
80
+ const platform = rest.includes("--platform")
79
81
  if (cloud) {
80
82
  const json = args.includes("--json")
81
83
  const publishArgs = []
@@ -106,9 +108,21 @@ async function run(args, { cwd }) {
106
108
 
107
109
  const manifest = loadManifest(cwd)
108
110
  const pluginId = manifest.id
111
+ const ports = await resolveDevPorts({ host: platform ? "0.0.0.0" : "127.0.0.1" })
112
+
113
+ if (!platform) {
114
+ if (ports.frontend !== ports.preferredFrontend) {
115
+ console.log(`[pltt] frontend port ${ports.preferredFrontend} is busy; using ${ports.frontend}`)
116
+ }
117
+ if (ports.backend !== ports.preferredBackend) {
118
+ console.log(`[pltt] backend port ${ports.preferredBackend} is busy; using ${ports.backend}`)
119
+ }
120
+ await startSimulator({ cwd, frontendPort: ports.frontend, backendPort: ports.backend })
121
+ return
122
+ }
123
+
109
124
  const frontendEntry = manifest.frontend?.entry || "./frontend/src/index.tsx"
110
125
  const frontendBundle = path.join(cwd, ".palette", "dist", "frontend.mjs")
111
- const ports = await resolveDevPorts()
112
126
  const frontendPort = String(ports.frontend)
113
127
  const backendPort = String(ports.backend)
114
128
 
@@ -0,0 +1,380 @@
1
+ "use strict"
2
+
3
+ const fs = require("fs")
4
+ const http = require("http")
5
+ const path = require("path")
6
+ const { spawn, spawnSync } = require("child_process")
7
+
8
+ const { loadManifest } = require("./manifest")
9
+
10
+ function loadEsbuild() {
11
+ try {
12
+ return require("esbuild")
13
+ } catch (_err) {
14
+ throw new Error("esbuild is required for `pltt dev`. Run `npm install` inside your plugin directory.")
15
+ }
16
+ }
17
+
18
+ function localBackendSdkPath() {
19
+ const candidates = [
20
+ path.resolve(__dirname, "..", "backend-sdk"),
21
+ path.resolve(__dirname, "..", "..", "..", "backend"),
22
+ ]
23
+ return candidates.find((candidate) => fs.existsSync(path.join(candidate, "palette_sdk"))) || null
24
+ }
25
+
26
+ function extractPyprojectDependencies(src) {
27
+ const deps = []
28
+ src = src
29
+ .split(/\r?\n/)
30
+ .filter((line) => !line.trimStart().startsWith("#"))
31
+ .map((line) => line.replace(/\s+#.*$/, ""))
32
+ .join("\n")
33
+ const match = src.match(/dependencies\s*=\s*\[([\s\S]*?)\]/)
34
+ if (!match) return deps
35
+ const re = /"([^"]+)"|'([^']+)'/g
36
+ let item
37
+ while ((item = re.exec(match[1])) !== null) deps.push(item[1] || item[2])
38
+ return deps
39
+ }
40
+
41
+ function pyprojectDependencies(cwd) {
42
+ const pyprojectPath = path.join(cwd, "pyproject.toml")
43
+ if (!fs.existsSync(pyprojectPath)) return []
44
+ return extractPyprojectDependencies(fs.readFileSync(pyprojectPath, "utf8"))
45
+ }
46
+
47
+ function ensurePythonEnv(cwd, devDir) {
48
+ const hostPython = process.env.PALETTE_PYTHON || "python3"
49
+ const venvDir = path.join(devDir, "backend-venv")
50
+ const venvPython = path.join(venvDir, "bin", "python")
51
+ const lockPath = path.join(venvDir, ".palette-dev-deps-lock")
52
+ const deps = Array.from(new Set([...pyprojectDependencies(cwd), "uvicorn>=0.30.0"]))
53
+ const lock = JSON.stringify(deps)
54
+
55
+ if (!fs.existsSync(venvPython)) {
56
+ const created = spawnSync(hostPython, ["-m", "venv", venvDir], {
57
+ cwd,
58
+ encoding: "utf8",
59
+ env: process.env,
60
+ })
61
+ if (created.status !== 0) {
62
+ throw new Error(
63
+ "could not create Python dev virtualenv. Install Python venv support or set PALETTE_PYTHON.\n" +
64
+ (created.stderr || created.stdout || ""),
65
+ )
66
+ }
67
+ }
68
+
69
+ if (!fs.existsSync(lockPath) || fs.readFileSync(lockPath, "utf8") !== lock) {
70
+ const installed = spawnSync(venvPython, ["-m", "pip", "install", ...deps], {
71
+ cwd,
72
+ encoding: "utf8",
73
+ env: process.env,
74
+ })
75
+ if (installed.status !== 0) {
76
+ throw new Error(
77
+ "could not install Python dependencies for local backend dev.\n" +
78
+ "Fix pyproject.toml dependencies or preinstall them in .palette/dev/backend-venv.\n" +
79
+ (installed.stderr || installed.stdout || ""),
80
+ )
81
+ }
82
+ fs.writeFileSync(lockPath, lock)
83
+ }
84
+
85
+ return venvPython
86
+ }
87
+
88
+ function writeBackendRunner(cwd, devDir, manifest, backendEntry) {
89
+ const runner = path.join(devDir, "backend_runner.py")
90
+ const sdkPath = localBackendSdkPath()
91
+ const content = `from __future__ import annotations
92
+
93
+ import importlib.util
94
+ import json
95
+ import pathlib
96
+ import sys
97
+ from types import SimpleNamespace
98
+
99
+ from fastapi import FastAPI, Request
100
+ from fastapi.middleware.cors import CORSMiddleware
101
+ from starlette.middleware.base import BaseHTTPMiddleware
102
+
103
+ ROOT = pathlib.Path(${JSON.stringify(cwd)}).resolve()
104
+ ENTRY = pathlib.Path(${JSON.stringify(path.resolve(cwd, backendEntry))}).resolve()
105
+ MANIFEST = json.loads(${JSON.stringify(JSON.stringify(manifest))})
106
+ SDK_PATH = ${JSON.stringify(sdkPath || "")}
107
+
108
+ if SDK_PATH:
109
+ sys.path.insert(0, SDK_PATH)
110
+ sys.path.insert(0, str(ROOT))
111
+ sys.path.insert(0, str(ENTRY.parent))
112
+
113
+ spec = importlib.util.spec_from_file_location("palette_local_backend", ENTRY)
114
+ module = importlib.util.module_from_spec(spec)
115
+ assert spec and spec.loader
116
+ spec.loader.exec_module(module)
117
+ router = getattr(module, "router", None)
118
+ if router is None:
119
+ raise RuntimeError(f"backend entry has no router export: {ENTRY}")
120
+
121
+ class DevPluginContextMiddleware(BaseHTTPMiddleware):
122
+ async def dispatch(self, request: Request, call_next):
123
+ request.state.db = None
124
+ request.state.user = SimpleNamespace(
125
+ id="dev-user",
126
+ email="developer@palette.local",
127
+ name="Palette Developer",
128
+ organization_id=1,
129
+ )
130
+ request.state.org_role = "owner"
131
+ request.state.plugin_id = MANIFEST.get("id", "")
132
+ request.state.plugin_permissions = MANIFEST.get("permissions", [])
133
+ request.state.plugin_config = {}
134
+ request.state.storage = None
135
+ return await call_next(request)
136
+
137
+ app = FastAPI(title=f"{MANIFEST.get('name', 'Palette Plugin')} Local Backend")
138
+ app.add_middleware(
139
+ CORSMiddleware,
140
+ allow_origins=["*"],
141
+ allow_credentials=False,
142
+ allow_methods=["*"],
143
+ allow_headers=["*"],
144
+ )
145
+ app.add_middleware(DevPluginContextMiddleware)
146
+ app.include_router(router, prefix=f"/api/v1/plugins/{MANIFEST['id']}")
147
+ `
148
+ fs.writeFileSync(runner, content)
149
+ return runner
150
+ }
151
+
152
+ function startBackend(cwd, devDir, manifest, backendPort) {
153
+ const backendEntry = manifest.backend?.entry
154
+ if (!backendEntry) return null
155
+ const absEntry = path.resolve(cwd, backendEntry)
156
+ if (!fs.existsSync(absEntry)) throw new Error(`backend entry not found: ${backendEntry}`)
157
+
158
+ const python = ensurePythonEnv(cwd, devDir)
159
+ const runner = writeBackendRunner(cwd, devDir, manifest, backendEntry)
160
+ const sdkPath = localBackendSdkPath()
161
+ const env = { ...process.env }
162
+ if (sdkPath) {
163
+ env.PYTHONPATH = [sdkPath, env.PYTHONPATH].filter(Boolean).join(path.delimiter)
164
+ }
165
+ const child = spawn(
166
+ python,
167
+ ["-m", "uvicorn", `${path.basename(runner, ".py")}:app`, "--host", "127.0.0.1", "--port", String(backendPort), "--reload", "--reload-dir", path.dirname(absEntry)],
168
+ { cwd: devDir, stdio: "inherit", env },
169
+ )
170
+ return child
171
+ }
172
+
173
+ function simulatorEntrySource(pluginEntry, manifest, backendPort) {
174
+ return `
175
+ import React from "react"
176
+ import { createRoot } from "react-dom/client"
177
+ import { PluginProvider } from "@palettelab/sdk/components"
178
+ import Plugin from ${JSON.stringify(pluginEntry)}
179
+
180
+ const backendBase = "http://127.0.0.1:${backendPort}"
181
+
182
+ async function apiFetch(path, init) {
183
+ const target = String(path || "")
184
+ if (target.startsWith("http://") || target.startsWith("https://")) {
185
+ return fetch(target, init)
186
+ }
187
+ if (target.startsWith("/api/")) {
188
+ return fetch(backendBase + target, init)
189
+ }
190
+ return fetch(target, init)
191
+ }
192
+
193
+ const platform = {
194
+ user: {
195
+ id: "dev-user",
196
+ email: "developer@palette.local",
197
+ name: "Palette Developer",
198
+ is_active: true,
199
+ created_at: new Date().toISOString(),
200
+ onboarding_completed: true,
201
+ company_name: "Palette Local",
202
+ company_type: "Development",
203
+ org_theme: null,
204
+ org_logo: null,
205
+ org_enabled_menu_items: null,
206
+ org_menu_labels: null,
207
+ org_enabled_apps: [${JSON.stringify(manifest.id)}],
208
+ org_app_store_enabled: true,
209
+ org_enabled_agent_ids: null,
210
+ organization_id: 1,
211
+ org_role: "owner",
212
+ },
213
+ organizationId: 1,
214
+ orgRole: "owner",
215
+ orgs: [{ id: 1, name: "Local Dev Org", slug: "local-dev", theme_id: "default", logo_url: null }],
216
+ agents: [],
217
+ apiFetch,
218
+ navigate: (path) => window.history.pushState(null, "", path),
219
+ showToast: (message, type = "info") => {
220
+ window.dispatchEvent(new CustomEvent("palette-toast", { detail: { message, type } }))
221
+ console.log("[palette-toast]", type, message)
222
+ },
223
+ }
224
+
225
+ function Toasts() {
226
+ const [items, setItems] = React.useState([])
227
+ React.useEffect(() => {
228
+ const onToast = (event) => {
229
+ const id = Date.now()
230
+ setItems((current) => [...current, { id, ...event.detail }])
231
+ window.setTimeout(() => setItems((current) => current.filter((item) => item.id !== id)), 2500)
232
+ }
233
+ window.addEventListener("palette-toast", onToast)
234
+ return () => window.removeEventListener("palette-toast", onToast)
235
+ }, [])
236
+ return React.createElement("div", { className: "palette-local-toasts" },
237
+ items.map((item) => React.createElement("div", { key: item.id, className: "palette-local-toast" }, item.message))
238
+ )
239
+ }
240
+
241
+ function Shell() {
242
+ return React.createElement(PluginProvider, { value: platform },
243
+ React.createElement("main", { className: "palette-local-shell" },
244
+ React.createElement(Plugin, { platform }),
245
+ React.createElement(Toasts)
246
+ )
247
+ )
248
+ }
249
+
250
+ createRoot(document.getElementById("root")).render(React.createElement(Shell))
251
+ `
252
+ }
253
+
254
+ function indexHtml(manifest) {
255
+ return `<!doctype html>
256
+ <html lang="en">
257
+ <head>
258
+ <meta charset="UTF-8" />
259
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
260
+ <title>${escapeHtml(manifest.name || manifest.id)} - Palette Local</title>
261
+ <style>
262
+ * { box-sizing: border-box; }
263
+ body { margin: 0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background: #f5f1ea; }
264
+ .palette-local-shell { min-height: 100vh; }
265
+ .palette-local-toasts { position: fixed; right: 16px; bottom: 16px; display: grid; gap: 8px; z-index: 50; }
266
+ .palette-local-toast { background: #1d1b18; color: white; padding: 10px 12px; font-size: 13px; box-shadow: 0 10px 30px rgba(0,0,0,.15); }
267
+ </style>
268
+ </head>
269
+ <body>
270
+ <div id="root"></div>
271
+ <script type="module" src="/simulator.js"></script>
272
+ </body>
273
+ </html>`
274
+ }
275
+
276
+ function escapeHtml(value) {
277
+ return String(value).replace(/[&<>"']/g, (ch) => ({
278
+ "&": "&amp;",
279
+ "<": "&lt;",
280
+ ">": "&gt;",
281
+ '"': "&quot;",
282
+ "'": "&#39;",
283
+ }[ch]))
284
+ }
285
+
286
+ async function startFrontend(cwd, devDir, manifest, frontendPort, backendPort) {
287
+ const entry = manifest.frontend?.entry || "./frontend/src/index.tsx"
288
+ const absEntry = path.resolve(cwd, entry)
289
+ if (!fs.existsSync(absEntry)) throw new Error(`frontend entry not found: ${entry}`)
290
+ const generatedEntry = path.join(devDir, "simulator-entry.jsx")
291
+ const bundlePath = path.join(devDir, "simulator.js")
292
+ fs.writeFileSync(generatedEntry, simulatorEntrySource(absEntry, manifest, backendPort))
293
+
294
+ const esbuild = loadEsbuild()
295
+ const ctx = await esbuild.context({
296
+ entryPoints: [generatedEntry],
297
+ bundle: true,
298
+ format: "esm",
299
+ platform: "browser",
300
+ target: ["es2022"],
301
+ outfile: bundlePath,
302
+ jsx: "automatic",
303
+ loader: { ".ts": "tsx", ".tsx": "tsx", ".js": "jsx", ".jsx": "jsx" },
304
+ absWorkingDir: cwd,
305
+ sourcemap: "inline",
306
+ logLevel: "silent",
307
+ plugins: [
308
+ {
309
+ name: "palette-simulator-watch",
310
+ setup(build) {
311
+ build.onEnd((result) => {
312
+ if (result.errors.length) {
313
+ console.error("[pltt] simulator bundle failed:")
314
+ for (const err of result.errors) console.error(` - ${err.text}`)
315
+ return
316
+ }
317
+ const size = fs.existsSync(bundlePath) ? fs.statSync(bundlePath).size : 0
318
+ console.log(`[pltt] simulator bundle ready (${size} bytes)`)
319
+ })
320
+ },
321
+ },
322
+ ],
323
+ })
324
+ await ctx.rebuild()
325
+ await ctx.watch()
326
+
327
+ const server = http.createServer((req, res) => {
328
+ const url = new URL(req.url || "/", `http://127.0.0.1:${frontendPort}`)
329
+ if (url.pathname === "/simulator.js") {
330
+ res.writeHead(200, { "Content-Type": "text/javascript; charset=utf-8", "Cache-Control": "no-store" })
331
+ res.end(fs.readFileSync(bundlePath))
332
+ return
333
+ }
334
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" })
335
+ res.end(indexHtml(manifest))
336
+ })
337
+
338
+ await new Promise((resolve) => server.listen(frontendPort, "127.0.0.1", resolve))
339
+ return { server, ctx }
340
+ }
341
+
342
+ async function startSimulator({ cwd, frontendPort, backendPort }) {
343
+ const manifest = loadManifest(cwd)
344
+ const devDir = path.join(cwd, ".palette", "dev")
345
+ fs.mkdirSync(devDir, { recursive: true })
346
+
347
+ const backend = startBackend(cwd, devDir, manifest, backendPort)
348
+ const frontend = await startFrontend(cwd, devDir, manifest, frontendPort, backendPort)
349
+
350
+ console.log("[pltt] running local SDK simulator (no Docker)")
351
+ console.log(`[pltt] app: http://localhost:${frontendPort}/`)
352
+ if (backend) console.log(`[pltt] backend: http://localhost:${backendPort}/api/v1/plugins/${manifest.id}`)
353
+ console.log("[pltt] use `pltt dev --platform` for full Docker platform parity")
354
+
355
+ const stop = async () => {
356
+ frontend.server.close()
357
+ await frontend.ctx.dispose()
358
+ if (backend && !backend.killed) backend.kill("SIGTERM")
359
+ }
360
+ process.on("SIGINT", () => stop().then(() => process.exit(0)))
361
+ process.on("SIGTERM", () => stop().then(() => process.exit(0)))
362
+
363
+ await new Promise((resolve) => {
364
+ const exits = []
365
+ frontend.server.on("close", () => {
366
+ exits.push("frontend")
367
+ resolve()
368
+ })
369
+ if (backend) {
370
+ backend.on("close", (code) => {
371
+ exits.push("backend")
372
+ if (code && code !== 0) process.exitCode = code
373
+ resolve()
374
+ })
375
+ }
376
+ })
377
+ await stop()
378
+ }
379
+
380
+ module.exports = { startSimulator }
package/lib/ports.js CHANGED
@@ -52,10 +52,11 @@ async function findFreePort(preferred, { host = "0.0.0.0", maxAttempts = 100 } =
52
52
  async function resolveDevPorts({
53
53
  frontend = process.env.PALETTE_FRONTEND_PORT || 3000,
54
54
  backend = process.env.PALETTE_BACKEND_PORT || 8000,
55
+ host = "0.0.0.0",
55
56
  } = {}) {
56
- const frontendPort = await findFreePort(frontend)
57
+ const frontendPort = await findFreePort(frontend, { host })
57
58
  const backendStart = Number(backend) === frontendPort ? frontendPort + 1 : backend
58
- const backendPort = await findFreePort(backendStart)
59
+ const backendPort = await findFreePort(backendStart, { host })
59
60
  return {
60
61
  frontend: frontendPort,
61
62
  backend: backendPort,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palettelab/cli",
3
- "version": "0.3.15",
3
+ "version": "0.3.16",
4
4
  "description": "Developer CLI for building Palette platform plugins — no platform source access required.",
5
5
  "bin": {
6
6
  "pltt": "bin/pltt.js"