@palettelab/cli 0.3.0 → 0.3.2
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 +103 -10
- package/bin/{palette.js → pltt.js} +1 -1
- package/lib/bundler.js +73 -4
- package/lib/cli.js +37 -12
- package/lib/commands/build.js +2 -0
- package/lib/commands/dev.js +37 -2
- package/lib/commands/doctor.js +143 -0
- package/lib/commands/init.js +45 -13
- package/lib/commands/logs.js +99 -0
- package/lib/commands/package.js +64 -0
- package/lib/commands/publish.js +50 -6
- package/lib/commands/status.js +80 -0
- package/lib/commands/test.js +376 -0
- package/lib/environments.js +1 -1
- package/lib/manifest.js +253 -8
- package/package.json +7 -6
- package/platform-dev/docker-compose.yml +4 -1
- package/template-fallback/backend/api/main.py +9 -3
- package/template-fallback/palette-plugin.json +24 -1
- package/template-fallback/pyproject.toml +1 -1
- package/template-fallback/templates/agent-tool/README.md +4 -0
- package/template-fallback/templates/agent-tool/backend/api/main.py +14 -0
- package/template-fallback/templates/agent-tool/backend/tools/echo.py +15 -0
- package/template-fallback/templates/agent-tool/package.json +5 -0
- package/template-fallback/templates/agent-tool/palette-plugin.json +29 -0
- package/template-fallback/templates/agent-tool/pyproject.toml +5 -0
- package/template-fallback/templates/dashboard/README.md +3 -0
- package/template-fallback/templates/dashboard/backend/api/main.py +23 -0
- package/template-fallback/templates/dashboard/frontend/src/index.tsx +46 -0
- package/template-fallback/templates/dashboard/package.json +9 -0
- package/template-fallback/templates/dashboard/palette-plugin.json +26 -0
- package/template-fallback/templates/dashboard/pyproject.toml +5 -0
- package/template-fallback/templates/database/README.md +7 -0
- package/template-fallback/templates/database/backend/api/main.py +38 -0
- package/template-fallback/templates/database/backend/api/models.py +11 -0
- package/template-fallback/templates/database/backend/migrations/001_init.py +26 -0
- package/template-fallback/templates/database/frontend/src/index.tsx +57 -0
- package/template-fallback/templates/database/package.json +6 -0
- package/template-fallback/templates/database/palette-plugin.json +26 -0
- package/template-fallback/templates/database/pyproject.toml +5 -0
- package/template-fallback/templates/external-service/README.md +4 -0
- package/template-fallback/templates/external-service/backend/api/main.py +28 -0
- package/template-fallback/templates/external-service/frontend/src/index.tsx +26 -0
- package/template-fallback/templates/external-service/package.json +6 -0
- package/template-fallback/templates/external-service/palette-plugin.json +26 -0
- package/template-fallback/templates/external-service/pyproject.toml +5 -0
- package/template-fallback/templates/frontend-only/README.md +7 -0
- package/template-fallback/templates/frontend-only/frontend/src/index.tsx +16 -0
- package/template-fallback/templates/frontend-only/package.json +9 -0
- package/template-fallback/templates/frontend-only/palette-plugin.json +25 -0
package/README.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Developer CLI for building plugins for the Palette platform. Works without any access to the platform source — your plugin repo is the only thing you own.
|
|
4
4
|
|
|
5
|
+
The installed executable is `pltt`.
|
|
6
|
+
|
|
5
7
|
## Requirements
|
|
6
8
|
|
|
7
9
|
- Node.js 18+
|
|
8
|
-
- Docker Desktop (for `
|
|
10
|
+
- Docker Desktop (for `pltt dev`)
|
|
9
11
|
|
|
10
12
|
## Install
|
|
11
13
|
|
|
@@ -13,27 +15,38 @@ You don't have to install globally — use `npx`:
|
|
|
13
15
|
|
|
14
16
|
```bash
|
|
15
17
|
npx @palettelab/cli <command>
|
|
18
|
+
# or after global install
|
|
19
|
+
pltt <command>
|
|
16
20
|
```
|
|
17
21
|
|
|
18
22
|
## Commands
|
|
19
23
|
|
|
20
|
-
### `
|
|
24
|
+
### `pltt init <name>`
|
|
21
25
|
|
|
22
26
|
Scaffold a new plugin directory from the official template.
|
|
23
27
|
|
|
24
28
|
```bash
|
|
25
|
-
|
|
29
|
+
pltt init data-explorer
|
|
30
|
+
pltt init crm-dashboard --template dashboard
|
|
26
31
|
cd data-explorer
|
|
27
32
|
```
|
|
28
33
|
|
|
29
34
|
Creates `data-explorer/` with a valid `palette-plugin.json`, a frontend React entry, and a FastAPI backend entry.
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
Templates:
|
|
37
|
+
|
|
38
|
+
- `dashboard`
|
|
39
|
+
- `agent-tool`
|
|
40
|
+
- `external-service`
|
|
41
|
+
- `database`
|
|
42
|
+
- `frontend-only`
|
|
43
|
+
|
|
44
|
+
### `pltt dev`
|
|
32
45
|
|
|
33
46
|
Boot the platform locally with your plugin mounted live. Run this from inside your plugin directory.
|
|
34
47
|
|
|
35
48
|
```bash
|
|
36
|
-
|
|
49
|
+
pltt dev
|
|
37
50
|
```
|
|
38
51
|
|
|
39
52
|
Under the hood this runs `docker compose up` with a bundled compose file. It starts:
|
|
@@ -53,22 +66,102 @@ Environment variables:
|
|
|
53
66
|
| `PALETTE_FRONTEND_PORT` | `3000` | Host port for the frontend |
|
|
54
67
|
| `PALETTE_BACKEND_PORT` | `8000` | Host port for the backend |
|
|
55
68
|
|
|
56
|
-
### `
|
|
69
|
+
### `pltt doctor`
|
|
70
|
+
|
|
71
|
+
Check local tooling and common setup problems.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pltt doctor
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Checks include Node version, Docker availability, platform image availability, port availability, manifest validity, entry files, and frontend bundling.
|
|
78
|
+
|
|
79
|
+
### `pltt build`
|
|
57
80
|
|
|
58
81
|
Validate `palette-plugin.json` and check that all declared entry files exist. Run this before pushing a release.
|
|
59
82
|
|
|
60
83
|
```bash
|
|
61
|
-
|
|
84
|
+
pltt build
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Also lints plugin migrations for unsafe RLS patterns when `database.migrations` is declared.
|
|
88
|
+
|
|
89
|
+
### `pltt test`
|
|
90
|
+
|
|
91
|
+
Run local contract checks before publishing.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pltt test
|
|
95
|
+
pltt test --json
|
|
62
96
|
```
|
|
63
97
|
|
|
98
|
+
Checks include manifest validity, frontend bundling, backend import, route permission gates, route permission declarations, migration linting, frontend sandbox policy, and dependency policy for `package.json` / `pyproject.toml`.
|
|
99
|
+
|
|
100
|
+
### `pltt package`
|
|
101
|
+
|
|
102
|
+
Bundle the plugin into `dist/<plugin-id>-<version>.tar.gz` without uploading.
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
pltt package
|
|
106
|
+
pltt package --json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Use this to inspect the publishable artifact before sending it to a Palette environment.
|
|
110
|
+
|
|
111
|
+
### `pltt publish`
|
|
112
|
+
|
|
113
|
+
Build and publish the plugin to a Palette appstore environment.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
pltt publish --env local
|
|
117
|
+
pltt publish --env staging
|
|
118
|
+
pltt publish --env production
|
|
119
|
+
pltt publish --env production -y
|
|
120
|
+
pltt publish --env staging --json
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Publishing bundles frontend/backend artifacts, uploads them, creates a `pending_review` publish record, and prints review/preview URLs when the platform returns them.
|
|
124
|
+
|
|
125
|
+
Environment config is read from `./palette.config.json`, `~/.palette/config.json`, or `PALETTE_<ENV>_URL` plus `PALETTE_<ENV>_TOKEN` / `PALETTE_PUBLISH_TOKEN`.
|
|
126
|
+
|
|
127
|
+
### `pltt status <publish-id>`
|
|
128
|
+
|
|
129
|
+
Show review status and automated risk report details for a publish.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
pltt status 123 --env staging
|
|
133
|
+
pltt status --json
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
If no publish ID is provided, the CLI uses `.palette/last-publish.json` when available.
|
|
137
|
+
|
|
138
|
+
### `pltt logs [plugin-id]`
|
|
139
|
+
|
|
140
|
+
Fetch or stream telemetry events for a plugin.
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
pltt logs hello-sdk --env staging
|
|
144
|
+
pltt logs --tail 100
|
|
145
|
+
pltt logs --follow
|
|
146
|
+
pltt logs --json
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
If no plugin ID is provided, the CLI uses the current `palette-plugin.json` or `.palette/last-publish.json`.
|
|
150
|
+
|
|
151
|
+
## Global Flags
|
|
152
|
+
|
|
153
|
+
- `--json` emits machine-readable output for `package`, `publish`, `status`, `logs`, and `test`.
|
|
154
|
+
- `--env <name>` selects a configured publish/status/logs environment.
|
|
155
|
+
- `-y, --yes` skips production publish confirmation.
|
|
156
|
+
|
|
64
157
|
## What gets shipped
|
|
65
158
|
|
|
66
159
|
`@palettelab/cli` itself contains only:
|
|
67
160
|
|
|
68
|
-
- `bin/
|
|
161
|
+
- `bin/pltt.js` — entry point
|
|
69
162
|
- `lib/` — pure-Node command implementations (no runtime dependencies)
|
|
70
|
-
- `platform-dev/docker-compose.yml` — compose file for `
|
|
71
|
-
- `template-fallback/` — offline fallback for `
|
|
163
|
+
- `platform-dev/docker-compose.yml` — compose file for `pltt dev`
|
|
164
|
+
- `template-fallback/` — offline fallback for `pltt init` if git is unavailable
|
|
72
165
|
|
|
73
166
|
## See also
|
|
74
167
|
|
|
@@ -5,6 +5,6 @@ const path = require("path")
|
|
|
5
5
|
const cli = require(path.join(__dirname, "..", "lib", "cli.js"))
|
|
6
6
|
|
|
7
7
|
cli.run(process.argv.slice(2)).catch((err) => {
|
|
8
|
-
console.error(`[
|
|
8
|
+
console.error(`[pltt] ${err.message || err}`)
|
|
9
9
|
process.exit(1)
|
|
10
10
|
})
|
package/lib/bundler.js
CHANGED
|
@@ -5,14 +5,14 @@ const fs = require("fs")
|
|
|
5
5
|
const os = require("os")
|
|
6
6
|
|
|
7
7
|
// esbuild is declared as a dependency in package.json; installed via npm install.
|
|
8
|
-
// We require it lazily so `
|
|
8
|
+
// We require it lazily so `pltt init` / `pltt dev` / `pltt build` do
|
|
9
9
|
// not pay the load cost.
|
|
10
10
|
function loadEsbuild() {
|
|
11
11
|
try {
|
|
12
12
|
return require("esbuild")
|
|
13
13
|
} catch (err) {
|
|
14
14
|
throw new Error(
|
|
15
|
-
"esbuild is required for `
|
|
15
|
+
"esbuild is required for `pltt publish`. " +
|
|
16
16
|
"Run `npm install` inside your plugin directory, or install it globally.",
|
|
17
17
|
)
|
|
18
18
|
}
|
|
@@ -27,6 +27,7 @@ function loadEsbuild() {
|
|
|
27
27
|
* Returns the bundle as a Buffer.
|
|
28
28
|
*/
|
|
29
29
|
async function bundleFrontend(pluginDir, entry) {
|
|
30
|
+
pluginDir = path.resolve(pluginDir)
|
|
30
31
|
const esbuild = loadEsbuild()
|
|
31
32
|
const absEntry = path.resolve(pluginDir, entry)
|
|
32
33
|
if (!fs.existsSync(absEntry)) {
|
|
@@ -42,7 +43,14 @@ async function bundleFrontend(pluginDir, entry) {
|
|
|
42
43
|
write: false,
|
|
43
44
|
jsx: "automatic",
|
|
44
45
|
loader: { ".ts": "tsx", ".tsx": "tsx", ".js": "jsx", ".jsx": "jsx" },
|
|
45
|
-
external: [
|
|
46
|
+
external: [
|
|
47
|
+
"react",
|
|
48
|
+
"react-dom",
|
|
49
|
+
"react-dom/client",
|
|
50
|
+
"react/jsx-runtime",
|
|
51
|
+
"react/jsx-dev-runtime",
|
|
52
|
+
"@palettelab/sdk",
|
|
53
|
+
],
|
|
46
54
|
minify: true,
|
|
47
55
|
sourcemap: "inline",
|
|
48
56
|
logLevel: "silent",
|
|
@@ -55,6 +63,63 @@ async function bundleFrontend(pluginDir, entry) {
|
|
|
55
63
|
return Buffer.from(result.outputFiles[0].contents)
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
async function watchFrontend(pluginDir, entry, outfile) {
|
|
67
|
+
pluginDir = path.resolve(pluginDir)
|
|
68
|
+
outfile = path.resolve(outfile)
|
|
69
|
+
const esbuild = loadEsbuild()
|
|
70
|
+
const absEntry = path.resolve(pluginDir, entry)
|
|
71
|
+
if (!fs.existsSync(absEntry)) {
|
|
72
|
+
throw new Error(`frontend entry not found: ${entry}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fs.mkdirSync(path.dirname(outfile), { recursive: true })
|
|
76
|
+
|
|
77
|
+
const ctx = await esbuild.context({
|
|
78
|
+
entryPoints: [absEntry],
|
|
79
|
+
bundle: true,
|
|
80
|
+
format: "esm",
|
|
81
|
+
platform: "browser",
|
|
82
|
+
target: ["es2022"],
|
|
83
|
+
outfile,
|
|
84
|
+
jsx: "automatic",
|
|
85
|
+
loader: { ".ts": "tsx", ".tsx": "tsx", ".js": "jsx", ".jsx": "jsx" },
|
|
86
|
+
external: [
|
|
87
|
+
"react",
|
|
88
|
+
"react-dom",
|
|
89
|
+
"react-dom/client",
|
|
90
|
+
"react/jsx-runtime",
|
|
91
|
+
"react/jsx-dev-runtime",
|
|
92
|
+
"@palettelab/sdk",
|
|
93
|
+
],
|
|
94
|
+
minify: false,
|
|
95
|
+
sourcemap: "inline",
|
|
96
|
+
logLevel: "silent",
|
|
97
|
+
absWorkingDir: pluginDir,
|
|
98
|
+
plugins: [
|
|
99
|
+
{
|
|
100
|
+
name: "palette-dev-watch-logger",
|
|
101
|
+
setup(build) {
|
|
102
|
+
build.onEnd((result) => {
|
|
103
|
+
if (result.errors.length) {
|
|
104
|
+
console.error("[palette] frontend bundle failed:")
|
|
105
|
+
for (const err of result.errors) {
|
|
106
|
+
console.error(` - ${err.text}`)
|
|
107
|
+
}
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
const size = fs.existsSync(outfile) ? fs.statSync(outfile).size : 0
|
|
111
|
+
console.log(`[palette] frontend bundle ready (${size} bytes)`)
|
|
112
|
+
})
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await ctx.rebuild()
|
|
119
|
+
await ctx.watch()
|
|
120
|
+
return ctx
|
|
121
|
+
}
|
|
122
|
+
|
|
58
123
|
/**
|
|
59
124
|
* Tar-gzip the `backend/` directory + manifest using the system `tar` command.
|
|
60
125
|
* Returns a Buffer with the gzipped tarball contents.
|
|
@@ -64,12 +129,16 @@ async function bundleFrontend(pluginDir, entry) {
|
|
|
64
129
|
* ./palette-plugin.json
|
|
65
130
|
*/
|
|
66
131
|
async function bundleBackend(pluginDir) {
|
|
132
|
+
pluginDir = path.resolve(pluginDir)
|
|
67
133
|
const { spawnSync } = require("child_process")
|
|
68
134
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "palette-bundle-"))
|
|
69
135
|
const outPath = path.join(tmp, "backend.tar.gz")
|
|
70
136
|
|
|
71
137
|
const includes = []
|
|
72
138
|
if (fs.existsSync(path.join(pluginDir, "backend"))) includes.push("backend")
|
|
139
|
+
for (const metadataFile of ["package.json", "pyproject.toml"]) {
|
|
140
|
+
if (fs.existsSync(path.join(pluginDir, metadataFile))) includes.push(metadataFile)
|
|
141
|
+
}
|
|
73
142
|
includes.push("palette-plugin.json")
|
|
74
143
|
|
|
75
144
|
const result = spawnSync(
|
|
@@ -87,4 +156,4 @@ async function bundleBackend(pluginDir) {
|
|
|
87
156
|
return buf
|
|
88
157
|
}
|
|
89
158
|
|
|
90
|
-
module.exports = { bundleFrontend, bundleBackend }
|
|
159
|
+
module.exports = { bundleFrontend, bundleBackend, watchFrontend }
|
package/lib/cli.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
|
|
3
|
-
const path = require("path")
|
|
4
3
|
const init = require("./commands/init")
|
|
5
4
|
const dev = require("./commands/dev")
|
|
5
|
+
const doctor = require("./commands/doctor")
|
|
6
6
|
const build = require("./commands/build")
|
|
7
|
+
const test = require("./commands/test")
|
|
7
8
|
const publish = require("./commands/publish")
|
|
9
|
+
const pkg = require("./commands/package")
|
|
10
|
+
const status = require("./commands/status")
|
|
11
|
+
const logs = require("./commands/logs")
|
|
8
12
|
|
|
9
13
|
const COMMANDS = {
|
|
10
14
|
init: { run: init, help: "Scaffold a new plugin directory from the template" },
|
|
@@ -12,30 +16,51 @@ const COMMANDS = {
|
|
|
12
16
|
run: dev,
|
|
13
17
|
help: "Boot the platform-dev container and mount the current plugin for live development",
|
|
14
18
|
},
|
|
19
|
+
doctor: {
|
|
20
|
+
run: doctor,
|
|
21
|
+
help: "Check local tooling, ports, manifest, Docker, and frontend bundling",
|
|
22
|
+
},
|
|
15
23
|
build: { run: build, help: "Validate palette-plugin.json and check entry points exist" },
|
|
24
|
+
test: { run: test, help: "Run local plugin contract checks before publishing" },
|
|
25
|
+
package: { run: pkg, help: "Bundle the plugin into a tar.gz under dist/ (no upload)" },
|
|
16
26
|
publish: {
|
|
17
27
|
run: publish,
|
|
18
28
|
help: "Bundle and publish the plugin to the platform appstore",
|
|
19
29
|
},
|
|
30
|
+
status: {
|
|
31
|
+
run: status,
|
|
32
|
+
help: "Show review status + risk report for a published version",
|
|
33
|
+
},
|
|
34
|
+
logs: {
|
|
35
|
+
run: logs,
|
|
36
|
+
help: "Tail telemetry events for a plugin (--follow to stream)",
|
|
37
|
+
},
|
|
20
38
|
}
|
|
21
39
|
|
|
22
40
|
function printHelp() {
|
|
23
|
-
console.log("
|
|
24
|
-
console.log("Usage:
|
|
41
|
+
console.log("pltt — Palette plugin developer CLI\n")
|
|
42
|
+
console.log("Usage: pltt <command> [options]\n")
|
|
25
43
|
console.log("Commands:")
|
|
26
44
|
for (const [name, { help }] of Object.entries(COMMANDS)) {
|
|
27
45
|
console.log(` ${name.padEnd(8)} ${help}`)
|
|
28
46
|
}
|
|
47
|
+
console.log("\nGlobal flags:")
|
|
48
|
+
console.log(" --json Emit machine-readable JSON output (status, logs, package, publish, test)")
|
|
29
49
|
console.log("\nPublish flags:")
|
|
30
|
-
console.log(" --env <name>
|
|
31
|
-
console.log(" -y, --yes
|
|
50
|
+
console.log(" --env <name> Target environment from ~/.palette/config.json (default: local)")
|
|
51
|
+
console.log(" -y, --yes Skip interactive confirmation for production pushes")
|
|
52
|
+
console.log("\nInit flags:")
|
|
53
|
+
console.log(" --template <name> One of: dashboard, agent-tool, external-service, database, frontend-only")
|
|
54
|
+
console.log("\nLogs flags:")
|
|
55
|
+
console.log(" --tail <n> Tail last n events (default 50)")
|
|
56
|
+
console.log(" -f, --follow Stream events (poll every 3s)")
|
|
32
57
|
console.log("\nExamples:")
|
|
33
|
-
console.log("
|
|
34
|
-
console.log(" cd my-app &&
|
|
35
|
-
console.log("
|
|
36
|
-
console.log("
|
|
37
|
-
console.log("
|
|
38
|
-
console.log("
|
|
58
|
+
console.log(" pltt init my-app --template database")
|
|
59
|
+
console.log(" cd my-app && pltt dev")
|
|
60
|
+
console.log(" pltt package")
|
|
61
|
+
console.log(" pltt publish --env staging")
|
|
62
|
+
console.log(" pltt status")
|
|
63
|
+
console.log(" pltt logs --follow")
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
async function run(argv) {
|
|
@@ -46,7 +71,7 @@ async function run(argv) {
|
|
|
46
71
|
}
|
|
47
72
|
const entry = COMMANDS[cmd]
|
|
48
73
|
if (!entry) {
|
|
49
|
-
console.error(`[
|
|
74
|
+
console.error(`[pltt] unknown command: ${cmd}`)
|
|
50
75
|
printHelp()
|
|
51
76
|
process.exit(1)
|
|
52
77
|
}
|
package/lib/commands/build.js
CHANGED
package/lib/commands/dev.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const path = require("path")
|
|
4
4
|
const { spawn, spawnSync } = require("child_process")
|
|
5
5
|
const { loadManifest } = require("../manifest")
|
|
6
|
+
const { watchFrontend } = require("../bundler")
|
|
6
7
|
|
|
7
8
|
const DEFAULT_IMAGE =
|
|
8
9
|
process.env.PALETTE_DEV_IMAGE || "ghcr.io/palette-lab/platform-dev:latest"
|
|
@@ -14,23 +15,54 @@ function ensureDocker() {
|
|
|
14
15
|
const check = spawnSync("docker", ["info"], { stdio: "ignore" })
|
|
15
16
|
if (check.status !== 0) {
|
|
16
17
|
console.error(
|
|
17
|
-
"[
|
|
18
|
+
"[pltt] docker is required for `pltt dev`. Install Docker Desktop and make sure it is running.",
|
|
18
19
|
)
|
|
19
20
|
process.exit(1)
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function imageExistsLocally(image) {
|
|
25
|
+
const res = spawnSync("docker", ["image", "inspect", image], { stdio: "ignore" })
|
|
26
|
+
return res.status === 0
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
function tryPullImage(image) {
|
|
24
30
|
const res = spawnSync("docker", ["pull", image], { stdio: "inherit" })
|
|
25
31
|
return res.status === 0
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
function ensureImageAvailable(image) {
|
|
35
|
+
// Accept a locally-built image (e.g. `platform-dev:test` from `docker build`)
|
|
36
|
+
// without attempting a registry pull — local is authoritative if present.
|
|
37
|
+
if (imageExistsLocally(image)) return true
|
|
38
|
+
return tryPullImage(image)
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
async function run(args, { cwd }) {
|
|
29
42
|
const manifest = loadManifest(cwd)
|
|
30
43
|
const pluginId = manifest.id
|
|
44
|
+
const frontendEntry = manifest.frontend?.entry || "./frontend/src/index.tsx"
|
|
45
|
+
const frontendBundle = path.join(cwd, ".palette", "dist", "frontend.mjs")
|
|
31
46
|
|
|
32
47
|
ensureDocker()
|
|
33
48
|
|
|
49
|
+
let frontendWatcher = null
|
|
50
|
+
if (manifest.frontend?.entry) {
|
|
51
|
+
console.log(`[palette] bundling frontend ${frontendEntry} → .palette/dist/frontend.mjs`)
|
|
52
|
+
try {
|
|
53
|
+
frontendWatcher = await watchFrontend(cwd, frontendEntry, frontendBundle)
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(
|
|
56
|
+
`[palette] could not start frontend bundler: ${
|
|
57
|
+
err instanceof Error ? err.message : String(err)
|
|
58
|
+
}`,
|
|
59
|
+
)
|
|
60
|
+
process.exit(1)
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
console.log("[palette] manifest has no frontend entry; skipping frontend bundler")
|
|
64
|
+
}
|
|
65
|
+
|
|
34
66
|
console.log(`[palette] starting ${DEFAULT_IMAGE} via docker compose`)
|
|
35
67
|
console.log(`[palette] mounting ${cwd} → /plugins/${pluginId}`)
|
|
36
68
|
console.log(`[palette] frontend: http://localhost:${FRONTEND_PORT}/apps/${pluginId}`)
|
|
@@ -39,7 +71,8 @@ async function run(args, { cwd }) {
|
|
|
39
71
|
// Pre-pull so we can give a useful error if the image isn't reachable
|
|
40
72
|
// (common cause: maintainer hasn't pushed it yet, or `docker login ghcr.io`
|
|
41
73
|
// missing). Without this, the `docker compose up` failure is cryptic.
|
|
42
|
-
if (!
|
|
74
|
+
if (!ensureImageAvailable(DEFAULT_IMAGE)) {
|
|
75
|
+
if (frontendWatcher) await frontendWatcher.dispose()
|
|
43
76
|
console.error(
|
|
44
77
|
`\n[palette] could not pull ${DEFAULT_IMAGE}.\n` +
|
|
45
78
|
` Most common causes:\n` +
|
|
@@ -70,10 +103,12 @@ async function run(args, { cwd }) {
|
|
|
70
103
|
["compose", "-f", COMPOSE_FILE, "-p", projectName, "down"],
|
|
71
104
|
{ stdio: "inherit", env },
|
|
72
105
|
)
|
|
106
|
+
if (frontendWatcher) frontendWatcher.dispose().catch(() => {})
|
|
73
107
|
}
|
|
74
108
|
process.on("SIGINT", stop)
|
|
75
109
|
process.on("SIGTERM", stop)
|
|
76
110
|
await new Promise((resolve) => child.on("close", resolve))
|
|
111
|
+
if (frontendWatcher) await frontendWatcher.dispose()
|
|
77
112
|
}
|
|
78
113
|
|
|
79
114
|
module.exports = run
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const fs = require("fs")
|
|
4
|
+
const net = require("net")
|
|
5
|
+
const path = require("path")
|
|
6
|
+
const { spawnSync } = require("child_process")
|
|
7
|
+
const { loadManifest, validateManifest } = require("../manifest")
|
|
8
|
+
const { bundleFrontend } = require("../bundler")
|
|
9
|
+
|
|
10
|
+
const DEFAULT_IMAGE =
|
|
11
|
+
process.env.PALETTE_DEV_IMAGE || "ghcr.io/palette-lab/platform-dev:latest"
|
|
12
|
+
const FRONTEND_PORT = Number(process.env.PALETTE_FRONTEND_PORT || "3000")
|
|
13
|
+
const BACKEND_PORT = Number(process.env.PALETTE_BACKEND_PORT || "8000")
|
|
14
|
+
|
|
15
|
+
function ok(message) {
|
|
16
|
+
console.log(`OK ${message}`)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function warn(message, fix) {
|
|
20
|
+
console.log(`WARN ${message}`)
|
|
21
|
+
if (fix) console.log(`FIX ${fix}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function fail(message, fix) {
|
|
25
|
+
console.log(`FAIL ${message}`)
|
|
26
|
+
if (fix) console.log(`FIX ${fix}`)
|
|
27
|
+
return 1
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function nodeMajor() {
|
|
31
|
+
return Number(process.versions.node.split(".")[0])
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function dockerRunning() {
|
|
35
|
+
return spawnSync("docker", ["info"], { stdio: "ignore" }).status === 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function imageExistsLocally(image) {
|
|
39
|
+
return spawnSync("docker", ["image", "inspect", image], { stdio: "ignore" }).status === 0
|
|
40
|
+
}
|
|
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
|
+
function checkEntry(cwd, label, rel) {
|
|
54
|
+
if (!rel) return 0
|
|
55
|
+
const abs = path.resolve(cwd, rel)
|
|
56
|
+
if (!fs.existsSync(abs)) {
|
|
57
|
+
return fail(`${label} entry not found: ${rel}`)
|
|
58
|
+
}
|
|
59
|
+
ok(`${label} entry exists: ${rel}`)
|
|
60
|
+
return 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function run(args, { cwd }) {
|
|
64
|
+
let failures = 0
|
|
65
|
+
|
|
66
|
+
if (nodeMajor() >= 18) ok(`Node ${process.versions.node}`)
|
|
67
|
+
else failures += fail("Node 18+ is required", "Install Node.js 18 or newer.")
|
|
68
|
+
|
|
69
|
+
if (dockerRunning()) ok("Docker is running")
|
|
70
|
+
else failures += fail(
|
|
71
|
+
"Docker is not running",
|
|
72
|
+
"Start Docker Desktop, then rerun pltt doctor.",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if (imageExistsLocally(DEFAULT_IMAGE)) {
|
|
76
|
+
ok(`platform image is available locally: ${DEFAULT_IMAGE}`)
|
|
77
|
+
} else {
|
|
78
|
+
warn(
|
|
79
|
+
`platform image is not present locally: ${DEFAULT_IMAGE}`,
|
|
80
|
+
"pltt dev will try to pull it. If that fails, run docker login ghcr.io or set PALETTE_DEV_IMAGE.",
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
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
|
+
)
|
|
97
|
+
|
|
98
|
+
let manifest
|
|
99
|
+
try {
|
|
100
|
+
manifest = loadManifest(cwd)
|
|
101
|
+
ok("palette-plugin.json found")
|
|
102
|
+
} catch (err) {
|
|
103
|
+
failures += fail(
|
|
104
|
+
err instanceof Error ? err.message : String(err),
|
|
105
|
+
"Run this command from a Palette plugin root or create one with pltt init <name>.",
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (manifest) {
|
|
110
|
+
const errors = validateManifest(manifest)
|
|
111
|
+
if (errors.length) {
|
|
112
|
+
for (const err of errors) failures += fail(`manifest invalid: ${err}`)
|
|
113
|
+
} else {
|
|
114
|
+
ok(`manifest valid: ${manifest.id}@${manifest.version}`)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
failures += checkEntry(cwd, "frontend", manifest.frontend?.entry)
|
|
118
|
+
failures += checkEntry(cwd, "backend", manifest.backend?.entry)
|
|
119
|
+
for (const tool of manifest.tools || []) {
|
|
120
|
+
failures += checkEntry(cwd, `tool[${tool.name}]`, tool.entry)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (manifest.frontend?.entry) {
|
|
124
|
+
try {
|
|
125
|
+
const bundle = await bundleFrontend(cwd, manifest.frontend.entry)
|
|
126
|
+
ok(`frontend bundles successfully (${bundle.length} bytes)`)
|
|
127
|
+
} catch (err) {
|
|
128
|
+
failures += fail(
|
|
129
|
+
`frontend bundle failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
130
|
+
"Install plugin dependencies and fix frontend compile errors, then rerun pltt doctor.",
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (failures > 0) {
|
|
137
|
+
console.log(`\n[palette] doctor found ${failures} blocking issue(s).`)
|
|
138
|
+
process.exit(1)
|
|
139
|
+
}
|
|
140
|
+
console.log("\n[palette] doctor passed.")
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = run
|