@palettelab/cli 0.2.0 → 0.3.1
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 +11 -7
- 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 +56 -1
- 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,11 +15,13 @@ 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
|
|
|
@@ -28,7 +32,7 @@ cd data-explorer
|
|
|
28
32
|
|
|
29
33
|
Creates `data-explorer/` with a valid `palette-plugin.json`, a frontend React entry, and a FastAPI backend entry.
|
|
30
34
|
|
|
31
|
-
### `
|
|
35
|
+
### `pltt dev`
|
|
32
36
|
|
|
33
37
|
Boot the platform locally with your plugin mounted live. Run this from inside your plugin directory.
|
|
34
38
|
|
|
@@ -53,7 +57,7 @@ Environment variables:
|
|
|
53
57
|
| `PALETTE_FRONTEND_PORT` | `3000` | Host port for the frontend |
|
|
54
58
|
| `PALETTE_BACKEND_PORT` | `8000` | Host port for the backend |
|
|
55
59
|
|
|
56
|
-
### `
|
|
60
|
+
### `pltt build`
|
|
57
61
|
|
|
58
62
|
Validate `palette-plugin.json` and check that all declared entry files exist. Run this before pushing a release.
|
|
59
63
|
|
|
@@ -65,10 +69,10 @@ npx @palettelab/cli build
|
|
|
65
69
|
|
|
66
70
|
`@palettelab/cli` itself contains only:
|
|
67
71
|
|
|
68
|
-
- `bin/
|
|
72
|
+
- `bin/pltt.js` — entry point
|
|
69
73
|
- `lib/` — pure-Node command implementations (no runtime dependencies)
|
|
70
|
-
- `platform-dev/docker-compose.yml` — compose file for `
|
|
71
|
-
- `template-fallback/` — offline fallback for `
|
|
74
|
+
- `platform-dev/docker-compose.yml` — compose file for `pltt dev`
|
|
75
|
+
- `template-fallback/` — offline fallback for `pltt init` if git is unavailable
|
|
72
76
|
|
|
73
77
|
## See also
|
|
74
78
|
|
|
@@ -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,75 @@ 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
|
+
|
|
29
|
+
function tryPullImage(image) {
|
|
30
|
+
const res = spawnSync("docker", ["pull", image], { stdio: "inherit" })
|
|
31
|
+
return res.status === 0
|
|
32
|
+
}
|
|
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
|
+
|
|
23
41
|
async function run(args, { cwd }) {
|
|
24
42
|
const manifest = loadManifest(cwd)
|
|
25
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")
|
|
26
46
|
|
|
27
47
|
ensureDocker()
|
|
28
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
|
+
|
|
29
66
|
console.log(`[palette] starting ${DEFAULT_IMAGE} via docker compose`)
|
|
30
67
|
console.log(`[palette] mounting ${cwd} → /plugins/${pluginId}`)
|
|
31
68
|
console.log(`[palette] frontend: http://localhost:${FRONTEND_PORT}/apps/${pluginId}`)
|
|
32
69
|
console.log(`[palette] backend: http://localhost:${BACKEND_PORT}/api/v1/plugins/${pluginId}`)
|
|
33
70
|
|
|
71
|
+
// Pre-pull so we can give a useful error if the image isn't reachable
|
|
72
|
+
// (common cause: maintainer hasn't pushed it yet, or `docker login ghcr.io`
|
|
73
|
+
// missing). Without this, the `docker compose up` failure is cryptic.
|
|
74
|
+
if (!ensureImageAvailable(DEFAULT_IMAGE)) {
|
|
75
|
+
if (frontendWatcher) await frontendWatcher.dispose()
|
|
76
|
+
console.error(
|
|
77
|
+
`\n[palette] could not pull ${DEFAULT_IMAGE}.\n` +
|
|
78
|
+
` Most common causes:\n` +
|
|
79
|
+
` • The image hasn't been published to the registry yet.\n` +
|
|
80
|
+
` Platform maintainers: see docker/platform-dev/README.md for the build + push flow.\n` +
|
|
81
|
+
` • You are not logged in: docker login ghcr.io -u <github-user> -p <pat>\n` +
|
|
82
|
+
` • You are pointing at a different tag: set PALETTE_DEV_IMAGE=<your-image> to override.\n`,
|
|
83
|
+
)
|
|
84
|
+
process.exit(1)
|
|
85
|
+
}
|
|
86
|
+
|
|
34
87
|
const env = {
|
|
35
88
|
...process.env,
|
|
36
89
|
PALETTE_DEV_IMAGE: DEFAULT_IMAGE,
|
|
@@ -50,10 +103,12 @@ async function run(args, { cwd }) {
|
|
|
50
103
|
["compose", "-f", COMPOSE_FILE, "-p", projectName, "down"],
|
|
51
104
|
{ stdio: "inherit", env },
|
|
52
105
|
)
|
|
106
|
+
if (frontendWatcher) frontendWatcher.dispose().catch(() => {})
|
|
53
107
|
}
|
|
54
108
|
process.on("SIGINT", stop)
|
|
55
109
|
process.on("SIGTERM", stop)
|
|
56
110
|
await new Promise((resolve) => child.on("close", resolve))
|
|
111
|
+
if (frontendWatcher) await frontendWatcher.dispose()
|
|
57
112
|
}
|
|
58
113
|
|
|
59
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
|
package/lib/commands/init.js
CHANGED
|
@@ -9,6 +9,8 @@ const DEFAULT_TEMPLATE_REPO = "palette-lab/plugin-template"
|
|
|
9
9
|
const TEMPLATE_REPO = process.env.PALETTE_TEMPLATE_REPO || DEFAULT_TEMPLATE_REPO
|
|
10
10
|
const TEMPLATE_REF = process.env.PALETTE_TEMPLATE_REF || "main"
|
|
11
11
|
|
|
12
|
+
const KNOWN_TEMPLATES = ["frontend-only", "dashboard", "agent-tool", "external-service", "database"]
|
|
13
|
+
|
|
12
14
|
function toSlug(name) {
|
|
13
15
|
return name
|
|
14
16
|
.toLowerCase()
|
|
@@ -18,7 +20,6 @@ function toSlug(name) {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
function fetchTemplate(destDir) {
|
|
21
|
-
// Try git first — works offline-ish if git is available.
|
|
22
23
|
const git = spawnSync(
|
|
23
24
|
"git",
|
|
24
25
|
["clone", "--depth=1", "--branch", TEMPLATE_REF, `https://github.com/${TEMPLATE_REPO}.git`, destDir],
|
|
@@ -31,10 +32,23 @@ function fetchTemplate(destDir) {
|
|
|
31
32
|
return false
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
function copyLocalFallback(destDir) {
|
|
35
|
+
function copyLocalFallback(destDir, template) {
|
|
36
|
+
if (template) {
|
|
37
|
+
const tplDir = path.resolve(__dirname, "..", "..", "template-fallback", "templates", template)
|
|
38
|
+
if (!fs.existsSync(tplDir)) return false
|
|
39
|
+
fs.cpSync(tplDir, destDir, { recursive: true })
|
|
40
|
+
return true
|
|
41
|
+
}
|
|
35
42
|
const fallback = path.resolve(__dirname, "..", "..", "template-fallback")
|
|
36
43
|
if (!fs.existsSync(fallback)) return false
|
|
37
|
-
|
|
44
|
+
// Skip the templates/ directory when copying default fallback.
|
|
45
|
+
fs.cpSync(fallback, destDir, {
|
|
46
|
+
recursive: true,
|
|
47
|
+
filter: (src) => {
|
|
48
|
+
const rel = path.relative(fallback, src)
|
|
49
|
+
return !rel.startsWith("templates")
|
|
50
|
+
},
|
|
51
|
+
})
|
|
38
52
|
return true
|
|
39
53
|
}
|
|
40
54
|
|
|
@@ -47,10 +61,24 @@ function rewriteManifest(destDir, slug, displayName) {
|
|
|
47
61
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n")
|
|
48
62
|
}
|
|
49
63
|
|
|
64
|
+
function getOpt(args, name) {
|
|
65
|
+
const i = args.indexOf(name)
|
|
66
|
+
if (i >= 0 && args[i + 1]) return args[i + 1]
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
50
70
|
async function run(args, { cwd }) {
|
|
51
|
-
const
|
|
71
|
+
const positional = args.filter((a) => !a.startsWith("-"))
|
|
72
|
+
const name = positional[0]
|
|
52
73
|
if (!name) {
|
|
53
|
-
console.error("[
|
|
74
|
+
console.error("[pltt] usage: pltt init <plugin-name> [--template <name>]")
|
|
75
|
+
console.error(`[palette] templates: ${KNOWN_TEMPLATES.join(", ")}`)
|
|
76
|
+
process.exit(1)
|
|
77
|
+
}
|
|
78
|
+
const template = getOpt(args, "--template")
|
|
79
|
+
if (template && !KNOWN_TEMPLATES.includes(template)) {
|
|
80
|
+
console.error(`[palette] unknown template: ${template}`)
|
|
81
|
+
console.error(`[palette] templates: ${KNOWN_TEMPLATES.join(", ")}`)
|
|
54
82
|
process.exit(1)
|
|
55
83
|
}
|
|
56
84
|
const slug = toSlug(name)
|
|
@@ -63,18 +91,21 @@ async function run(args, { cwd }) {
|
|
|
63
91
|
process.exit(1)
|
|
64
92
|
}
|
|
65
93
|
|
|
66
|
-
|
|
94
|
+
if (template) {
|
|
95
|
+
console.log(`[palette] creating plugin "${slug}" from template "${template}"`)
|
|
96
|
+
} else {
|
|
97
|
+
console.log(`[palette] creating plugin "${slug}" from ${TEMPLATE_REPO}@${TEMPLATE_REF}`)
|
|
98
|
+
}
|
|
67
99
|
|
|
68
100
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "palette-tpl-"))
|
|
69
|
-
let ok =
|
|
70
|
-
if (!
|
|
71
|
-
|
|
72
|
-
ok
|
|
101
|
+
let ok = false
|
|
102
|
+
if (!template) {
|
|
103
|
+
ok = fetchTemplate(tmp)
|
|
104
|
+
if (!ok) console.warn("[palette] git clone failed, falling back to bundled template")
|
|
73
105
|
}
|
|
106
|
+
if (!ok) ok = copyLocalFallback(tmp, template)
|
|
74
107
|
if (!ok) {
|
|
75
|
-
console.error(
|
|
76
|
-
"[palette] no template available — install git or ship template-fallback/ with the CLI",
|
|
77
|
-
)
|
|
108
|
+
console.error("[palette] no template available")
|
|
78
109
|
process.exit(1)
|
|
79
110
|
}
|
|
80
111
|
|
|
@@ -86,6 +117,7 @@ async function run(args, { cwd }) {
|
|
|
86
117
|
console.log("[palette] next steps:")
|
|
87
118
|
console.log(` cd ${slug}`)
|
|
88
119
|
console.log(` npx @palettelab/cli dev`)
|
|
120
|
+
console.log(` # or, after global install: pltt dev`)
|
|
89
121
|
}
|
|
90
122
|
|
|
91
123
|
module.exports = run
|