@palettelab/cli 0.3.15 → 0.3.17
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 +18 -10
- package/lib/cli.js +2 -1
- package/lib/commands/dev.js +16 -2
- package/lib/dev-simulator.js +380 -0
- package/lib/ports.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@ The installed executable is `pltt`.
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
9
9
|
- Node.js 18+
|
|
10
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
By default this starts:
|
|
54
56
|
|
|
55
|
-
-
|
|
56
|
-
-
|
|
57
|
-
-
|
|
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
|
|
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: "
|
|
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")
|
package/lib/commands/dev.js
CHANGED
|
@@ -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"
|
|
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
|
+
"&": "&",
|
|
279
|
+
"<": "<",
|
|
280
|
+
">": ">",
|
|
281
|
+
'"': """,
|
|
282
|
+
"'": "'",
|
|
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,
|