@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.
Files changed (50) hide show
  1. package/README.md +11 -7
  2. package/bin/{palette.js → pltt.js} +1 -1
  3. package/lib/bundler.js +73 -4
  4. package/lib/cli.js +37 -12
  5. package/lib/commands/build.js +2 -0
  6. package/lib/commands/dev.js +56 -1
  7. package/lib/commands/doctor.js +143 -0
  8. package/lib/commands/init.js +45 -13
  9. package/lib/commands/logs.js +99 -0
  10. package/lib/commands/package.js +64 -0
  11. package/lib/commands/publish.js +50 -6
  12. package/lib/commands/status.js +80 -0
  13. package/lib/commands/test.js +376 -0
  14. package/lib/environments.js +1 -1
  15. package/lib/manifest.js +253 -8
  16. package/package.json +7 -6
  17. package/platform-dev/docker-compose.yml +4 -1
  18. package/template-fallback/backend/api/main.py +9 -3
  19. package/template-fallback/palette-plugin.json +24 -1
  20. package/template-fallback/pyproject.toml +1 -1
  21. package/template-fallback/templates/agent-tool/README.md +4 -0
  22. package/template-fallback/templates/agent-tool/backend/api/main.py +14 -0
  23. package/template-fallback/templates/agent-tool/backend/tools/echo.py +15 -0
  24. package/template-fallback/templates/agent-tool/package.json +5 -0
  25. package/template-fallback/templates/agent-tool/palette-plugin.json +29 -0
  26. package/template-fallback/templates/agent-tool/pyproject.toml +5 -0
  27. package/template-fallback/templates/dashboard/README.md +3 -0
  28. package/template-fallback/templates/dashboard/backend/api/main.py +23 -0
  29. package/template-fallback/templates/dashboard/frontend/src/index.tsx +46 -0
  30. package/template-fallback/templates/dashboard/package.json +9 -0
  31. package/template-fallback/templates/dashboard/palette-plugin.json +26 -0
  32. package/template-fallback/templates/dashboard/pyproject.toml +5 -0
  33. package/template-fallback/templates/database/README.md +7 -0
  34. package/template-fallback/templates/database/backend/api/main.py +38 -0
  35. package/template-fallback/templates/database/backend/api/models.py +11 -0
  36. package/template-fallback/templates/database/backend/migrations/001_init.py +26 -0
  37. package/template-fallback/templates/database/frontend/src/index.tsx +57 -0
  38. package/template-fallback/templates/database/package.json +6 -0
  39. package/template-fallback/templates/database/palette-plugin.json +26 -0
  40. package/template-fallback/templates/database/pyproject.toml +5 -0
  41. package/template-fallback/templates/external-service/README.md +4 -0
  42. package/template-fallback/templates/external-service/backend/api/main.py +28 -0
  43. package/template-fallback/templates/external-service/frontend/src/index.tsx +26 -0
  44. package/template-fallback/templates/external-service/package.json +6 -0
  45. package/template-fallback/templates/external-service/palette-plugin.json +26 -0
  46. package/template-fallback/templates/external-service/pyproject.toml +5 -0
  47. package/template-fallback/templates/frontend-only/README.md +7 -0
  48. package/template-fallback/templates/frontend-only/frontend/src/index.tsx +16 -0
  49. package/template-fallback/templates/frontend-only/package.json +9 -0
  50. 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 `palette dev`)
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
- ### `palette init <name>`
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
- ### `palette dev`
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
- ### `palette build`
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/palette.js` — entry point
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 `palette dev`
71
- - `template-fallback/` — offline fallback for `palette init` if git is unavailable
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(`[palette] ${err.message || err}`)
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 `palette init` / `palette dev` / `palette build` do
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 `palette publish`. " +
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: ["react", "react-dom", "react-dom/client", "@palettelab/sdk"],
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("palette — Palette plugin developer CLI\n")
24
- console.log("Usage: palette <command> [options]\n")
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> Target environment from ~/.palette/config.json (default: local)")
31
- console.log(" -y, --yes Skip interactive confirmation for production pushes")
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(" palette init my-app")
34
- console.log(" cd my-app && palette dev")
35
- console.log(" palette build")
36
- console.log(" palette publish # default (local)")
37
- console.log(" palette publish --env staging")
38
- console.log(" palette publish --env production # prompts for confirmation")
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(`[palette] unknown command: ${cmd}`)
74
+ console.error(`[pltt] unknown command: ${cmd}`)
50
75
  printHelp()
51
76
  process.exit(1)
52
77
  }
@@ -101,3 +101,5 @@ async function run(args, { cwd }) {
101
101
  }
102
102
 
103
103
  module.exports = run
104
+ module.exports.lintMigrationsDir = lintMigrationsDir
105
+ module.exports.lintMigrationFile = lintMigrationFile
@@ -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
- "[palette] docker is required for `palette dev`. Install Docker Desktop and make sure it is running.",
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
@@ -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
- fs.cpSync(fallback, destDir, { recursive: true })
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 name = args[0]
71
+ const positional = args.filter((a) => !a.startsWith("-"))
72
+ const name = positional[0]
52
73
  if (!name) {
53
- console.error("[palette] usage: palette init <plugin-name>")
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
- console.log(`[palette] creating plugin "${slug}" from ${TEMPLATE_REPO}@${TEMPLATE_REF}`)
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 = fetchTemplate(tmp)
70
- if (!ok) {
71
- console.warn("[palette] git clone failed, falling back to bundled template")
72
- ok = copyLocalFallback(tmp)
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