@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 +7 -5
- package/lib/commands/dev.js +43 -18
- package/lib/commands/doctor.js +13 -25
- package/lib/ports.js +42 -0
- package/package.json +1 -1
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
|
|
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` |
|
|
70
|
-
| `PALETTE_BACKEND_PORT` | `8000` |
|
|
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
|
|
package/lib/commands/dev.js
CHANGED
|
@@ -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], {
|
|
33
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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:
|
|
111
|
-
PALETTE_BACKEND_PORT:
|
|
135
|
+
PALETTE_FRONTEND_PORT: frontendPort,
|
|
136
|
+
PALETTE_BACKEND_PORT: backendPort,
|
|
112
137
|
}
|
|
113
138
|
|
|
114
139
|
const projectName = `palette-dev-${pluginId}`
|
package/lib/commands/doctor.js
CHANGED
|
@@ -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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`frontend port ${FRONTEND_PORT} is already in use`,
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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 }
|