@palettelab/cli 0.1.0
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 +77 -0
- package/bin/palette.js +10 -0
- package/lib/bundler.js +90 -0
- package/lib/cli.js +50 -0
- package/lib/commands/build.js +103 -0
- package/lib/commands/dev.js +59 -0
- package/lib/commands/init.js +91 -0
- package/lib/commands/publish.js +114 -0
- package/lib/manifest.js +35 -0
- package/package.json +37 -0
- package/platform-dev/docker-compose.yml +52 -0
- package/template-fallback/backend/api/main.py +42 -0
- package/template-fallback/backend/tools/example_tool.py +44 -0
- package/template-fallback/frontend/src/index.tsx +67 -0
- package/template-fallback/package.json +13 -0
- package/template-fallback/palette-plugin.json +27 -0
- package/template-fallback/pyproject.toml +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @palettelab/cli
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 18+
|
|
8
|
+
- Docker Desktop (for `palette dev`)
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
You don't have to install globally — use `npx`:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx @palettelab/cli <command>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Commands
|
|
19
|
+
|
|
20
|
+
### `palette init <name>`
|
|
21
|
+
|
|
22
|
+
Scaffold a new plugin directory from the official template.
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx @palettelab/cli init data-explorer
|
|
26
|
+
cd data-explorer
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Creates `data-explorer/` with a valid `palette-plugin.json`, a frontend React entry, and a FastAPI backend entry.
|
|
30
|
+
|
|
31
|
+
### `palette dev`
|
|
32
|
+
|
|
33
|
+
Boot the platform locally with your plugin mounted live. Run this from inside your plugin directory.
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx @palettelab/cli dev
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Under the hood this runs `docker compose up` with a bundled compose file. It starts:
|
|
40
|
+
|
|
41
|
+
- Postgres + Redis
|
|
42
|
+
- The Palette frontend on http://localhost:3000
|
|
43
|
+
- The Palette backend on http://localhost:8000
|
|
44
|
+
- Your plugin at http://localhost:3000/apps/<your-id>
|
|
45
|
+
|
|
46
|
+
Your plugin directory is mounted into the container at `/plugins/<your-id>`. Edits to your frontend/backend sources hot-reload.
|
|
47
|
+
|
|
48
|
+
Environment variables:
|
|
49
|
+
|
|
50
|
+
| Name | Default | Purpose |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `PALETTE_DEV_IMAGE` | `ghcr.io/palette-lab/platform-dev:latest` | Override the platform image |
|
|
53
|
+
| `PALETTE_FRONTEND_PORT` | `3000` | Host port for the frontend |
|
|
54
|
+
| `PALETTE_BACKEND_PORT` | `8000` | Host port for the backend |
|
|
55
|
+
|
|
56
|
+
### `palette build`
|
|
57
|
+
|
|
58
|
+
Validate `palette-plugin.json` and check that all declared entry files exist. Run this before pushing a release.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx @palettelab/cli build
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## What gets shipped
|
|
65
|
+
|
|
66
|
+
`@palettelab/cli` itself contains only:
|
|
67
|
+
|
|
68
|
+
- `bin/palette.js` — entry point
|
|
69
|
+
- `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
|
|
72
|
+
|
|
73
|
+
## See also
|
|
74
|
+
|
|
75
|
+
- `@palettelab/sdk` on GitHub Packages — frontend hooks and types
|
|
76
|
+
- `palette-sdk` (git-installed from [palette-lab/virtual-organisation](https://github.com/palette-lab/virtual-organisation/tree/main/sdk/backend)) — backend `PluginRouter` + `ToolDefinition`
|
|
77
|
+
- [Developer Guide](https://github.com/palette-lab/virtual-organisation/blob/main/sdk/DEVELOPER_GUIDE.md)
|
package/bin/palette.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict"
|
|
3
|
+
|
|
4
|
+
const path = require("path")
|
|
5
|
+
const cli = require(path.join(__dirname, "..", "lib", "cli.js"))
|
|
6
|
+
|
|
7
|
+
cli.run(process.argv.slice(2)).catch((err) => {
|
|
8
|
+
console.error(`[palette] ${err.message || err}`)
|
|
9
|
+
process.exit(1)
|
|
10
|
+
})
|
package/lib/bundler.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const path = require("path")
|
|
4
|
+
const fs = require("fs")
|
|
5
|
+
const os = require("os")
|
|
6
|
+
|
|
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
|
|
9
|
+
// not pay the load cost.
|
|
10
|
+
function loadEsbuild() {
|
|
11
|
+
try {
|
|
12
|
+
return require("esbuild")
|
|
13
|
+
} catch (err) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"esbuild is required for `palette publish`. " +
|
|
16
|
+
"Run `npm install` inside your plugin directory, or install it globally.",
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Bundle the plugin's frontend entry into a single ESM file.
|
|
23
|
+
*
|
|
24
|
+
* Externalises react, react-dom, and @palettelab/sdk so the prod platform's
|
|
25
|
+
* shared instances are used — avoids duplicate React trees + hook runtime errors.
|
|
26
|
+
*
|
|
27
|
+
* Returns the bundle as a Buffer.
|
|
28
|
+
*/
|
|
29
|
+
async function bundleFrontend(pluginDir, entry) {
|
|
30
|
+
const esbuild = loadEsbuild()
|
|
31
|
+
const absEntry = path.resolve(pluginDir, entry)
|
|
32
|
+
if (!fs.existsSync(absEntry)) {
|
|
33
|
+
throw new Error(`frontend entry not found: ${entry}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result = await esbuild.build({
|
|
37
|
+
entryPoints: [absEntry],
|
|
38
|
+
bundle: true,
|
|
39
|
+
format: "esm",
|
|
40
|
+
platform: "browser",
|
|
41
|
+
target: ["es2022"],
|
|
42
|
+
write: false,
|
|
43
|
+
jsx: "automatic",
|
|
44
|
+
loader: { ".ts": "tsx", ".tsx": "tsx", ".js": "jsx", ".jsx": "jsx" },
|
|
45
|
+
external: ["react", "react-dom", "react-dom/client", "@palettelab/sdk"],
|
|
46
|
+
minify: true,
|
|
47
|
+
sourcemap: "inline",
|
|
48
|
+
logLevel: "silent",
|
|
49
|
+
absWorkingDir: pluginDir,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (!result.outputFiles || result.outputFiles.length === 0) {
|
|
53
|
+
throw new Error("esbuild produced no output")
|
|
54
|
+
}
|
|
55
|
+
return Buffer.from(result.outputFiles[0].contents)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Tar-gzip the `backend/` directory + manifest using the system `tar` command.
|
|
60
|
+
* Returns a Buffer with the gzipped tarball contents.
|
|
61
|
+
*
|
|
62
|
+
* Resulting layout inside the archive:
|
|
63
|
+
* ./backend/...
|
|
64
|
+
* ./palette-plugin.json
|
|
65
|
+
*/
|
|
66
|
+
async function bundleBackend(pluginDir) {
|
|
67
|
+
const { spawnSync } = require("child_process")
|
|
68
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "palette-bundle-"))
|
|
69
|
+
const outPath = path.join(tmp, "backend.tar.gz")
|
|
70
|
+
|
|
71
|
+
const includes = []
|
|
72
|
+
if (fs.existsSync(path.join(pluginDir, "backend"))) includes.push("backend")
|
|
73
|
+
includes.push("palette-plugin.json")
|
|
74
|
+
|
|
75
|
+
const result = spawnSync(
|
|
76
|
+
"tar",
|
|
77
|
+
["-czf", outPath, ...includes.flatMap((f) => ["--exclude=__pycache__", "--exclude=.venv"]), ...includes],
|
|
78
|
+
{ cwd: pluginDir, stdio: "pipe" },
|
|
79
|
+
)
|
|
80
|
+
if (result.status !== 0) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`tar failed (exit ${result.status}): ${result.stderr?.toString() || "unknown error"}`,
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
const buf = fs.readFileSync(outPath)
|
|
86
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
87
|
+
return buf
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { bundleFrontend, bundleBackend }
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const path = require("path")
|
|
4
|
+
const init = require("./commands/init")
|
|
5
|
+
const dev = require("./commands/dev")
|
|
6
|
+
const build = require("./commands/build")
|
|
7
|
+
const publish = require("./commands/publish")
|
|
8
|
+
|
|
9
|
+
const COMMANDS = {
|
|
10
|
+
init: { run: init, help: "Scaffold a new plugin directory from the template" },
|
|
11
|
+
dev: {
|
|
12
|
+
run: dev,
|
|
13
|
+
help: "Boot the platform-dev container and mount the current plugin for live development",
|
|
14
|
+
},
|
|
15
|
+
build: { run: build, help: "Validate palette-plugin.json and check entry points exist" },
|
|
16
|
+
publish: {
|
|
17
|
+
run: publish,
|
|
18
|
+
help: "Bundle and publish the plugin to the platform appstore",
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function printHelp() {
|
|
23
|
+
console.log("palette — Palette plugin developer CLI\n")
|
|
24
|
+
console.log("Usage: palette <command> [options]\n")
|
|
25
|
+
console.log("Commands:")
|
|
26
|
+
for (const [name, { help }] of Object.entries(COMMANDS)) {
|
|
27
|
+
console.log(` ${name.padEnd(8)} ${help}`)
|
|
28
|
+
}
|
|
29
|
+
console.log("\nExamples:")
|
|
30
|
+
console.log(" npx @palettelab/cli init my-app")
|
|
31
|
+
console.log(" cd my-app && npx @palettelab/cli dev")
|
|
32
|
+
console.log(" npx @palettelab/cli build")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function run(argv) {
|
|
36
|
+
const cmd = argv[0]
|
|
37
|
+
if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
38
|
+
printHelp()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
const entry = COMMANDS[cmd]
|
|
42
|
+
if (!entry) {
|
|
43
|
+
console.error(`[palette] unknown command: ${cmd}`)
|
|
44
|
+
printHelp()
|
|
45
|
+
process.exit(1)
|
|
46
|
+
}
|
|
47
|
+
await entry.run(argv.slice(1), { cwd: process.cwd() })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { run }
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const fs = require("fs")
|
|
4
|
+
const path = require("path")
|
|
5
|
+
const { loadManifest, validateManifest } = require("../manifest")
|
|
6
|
+
|
|
7
|
+
// Strings that must never appear in a plugin migration — they weaken the
|
|
8
|
+
// tenant isolation guarantee that org-scoped apps rely on.
|
|
9
|
+
const BANNED_PATTERNS = [
|
|
10
|
+
{ re: /DROP\s+POLICY/i, reason: "DROP POLICY weakens RLS" },
|
|
11
|
+
{ re: /DISABLE\s+ROW\s+LEVEL\s+SECURITY/i, reason: "DISABLE ROW LEVEL SECURITY removes tenant isolation" },
|
|
12
|
+
{ re: /NO\s+FORCE\s+ROW\s+LEVEL\s+SECURITY/i, reason: "NO FORCE ROW LEVEL SECURITY lets table owners bypass RLS" },
|
|
13
|
+
{ re: /\bpublic\./, reason: "plugin migrations must not reference the platform's public schema" },
|
|
14
|
+
{ re: /\bSET\s+ROLE\b/i, reason: "SET ROLE in migrations escalates out of the plugin sandbox" },
|
|
15
|
+
{ re: /\bALTER\s+ROLE\b/i, reason: "ALTER ROLE is reserved for the platform" },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
function lintMigrationFile(absPath) {
|
|
19
|
+
const issues = []
|
|
20
|
+
const src = fs.readFileSync(absPath, "utf8")
|
|
21
|
+
|
|
22
|
+
for (const { re, reason } of BANNED_PATTERNS) {
|
|
23
|
+
if (re.test(src)) {
|
|
24
|
+
issues.push(`${path.basename(absPath)}: ${reason}`)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Every op.create_table("foo", ...) in the file must have a matching
|
|
29
|
+
// ensure_org_rls(op, "foo") somewhere in the same file. Caveat: this is a
|
|
30
|
+
// cheap syntactic check, not a full AST walk. If your table name is dynamic
|
|
31
|
+
// or your migration is unusual, you can silence the check by adding the
|
|
32
|
+
// magic comment `# palette:rls-ok` on the same logical migration.
|
|
33
|
+
if (/#\s*palette:rls-ok\b/.test(src)) return issues
|
|
34
|
+
|
|
35
|
+
const tableNames = new Set()
|
|
36
|
+
const createTableRe = /op\.create_table\(\s*['"]([a-zA-Z_][a-zA-Z0-9_]*)['"]/g
|
|
37
|
+
let match
|
|
38
|
+
while ((match = createTableRe.exec(src)) !== null) {
|
|
39
|
+
tableNames.add(match[1])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const name of tableNames) {
|
|
43
|
+
const rlsRe = new RegExp(`ensure_org_rls\\(\\s*op\\s*,\\s*['"]${name}['"]`)
|
|
44
|
+
if (!rlsRe.test(src)) {
|
|
45
|
+
issues.push(
|
|
46
|
+
`${path.basename(absPath)}: create_table("${name}") is missing ensure_org_rls(op, "${name}"). ` +
|
|
47
|
+
`Inherit from OrgScopedTable and call ensure_org_rls, or mark the migration with # palette:rls-ok if the table is intentionally global.`,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return issues
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function lintMigrationsDir(migrationsDir) {
|
|
56
|
+
const errors = []
|
|
57
|
+
const versionsDir = path.join(migrationsDir, "versions")
|
|
58
|
+
if (!fs.existsSync(versionsDir)) {
|
|
59
|
+
errors.push(`migrations directory has no versions/ subfolder: ${migrationsDir}`)
|
|
60
|
+
return errors
|
|
61
|
+
}
|
|
62
|
+
for (const entry of fs.readdirSync(versionsDir)) {
|
|
63
|
+
if (!entry.endsWith(".py")) continue
|
|
64
|
+
const abs = path.join(versionsDir, entry)
|
|
65
|
+
errors.push(...lintMigrationFile(abs))
|
|
66
|
+
}
|
|
67
|
+
return errors
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function run(args, { cwd }) {
|
|
71
|
+
const manifest = loadManifest(cwd)
|
|
72
|
+
const errors = validateManifest(manifest)
|
|
73
|
+
|
|
74
|
+
const checkEntry = (label, rel) => {
|
|
75
|
+
if (!rel) return
|
|
76
|
+
const abs = path.resolve(cwd, rel)
|
|
77
|
+
if (!fs.existsSync(abs)) errors.push(`${label} entry not found: ${rel}`)
|
|
78
|
+
}
|
|
79
|
+
checkEntry("frontend", manifest.frontend?.entry)
|
|
80
|
+
checkEntry("backend", manifest.backend?.entry)
|
|
81
|
+
for (const tool of manifest.tools || []) checkEntry(`tool[${tool.name}]`, tool.entry)
|
|
82
|
+
|
|
83
|
+
if (manifest.database) {
|
|
84
|
+
const migrationsRel = manifest.database.migrations || "./backend/migrations"
|
|
85
|
+
const migrationsAbs = path.resolve(cwd, migrationsRel)
|
|
86
|
+
if (!fs.existsSync(migrationsAbs)) {
|
|
87
|
+
errors.push(`database.migrations directory not found: ${migrationsRel}`)
|
|
88
|
+
} else if (!fs.existsSync(path.join(migrationsAbs, "env.py"))) {
|
|
89
|
+
errors.push(`database.migrations directory is missing env.py: ${migrationsRel}`)
|
|
90
|
+
} else {
|
|
91
|
+
errors.push(...lintMigrationsDir(migrationsAbs))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (errors.length) {
|
|
96
|
+
console.error("[palette] validation failed:")
|
|
97
|
+
for (const e of errors) console.error(` - ${e}`)
|
|
98
|
+
process.exit(1)
|
|
99
|
+
}
|
|
100
|
+
console.log(`[palette] ok — ${manifest.id} v${manifest.version}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = run
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const path = require("path")
|
|
4
|
+
const { spawn, spawnSync } = require("child_process")
|
|
5
|
+
const { loadManifest } = require("../manifest")
|
|
6
|
+
|
|
7
|
+
const DEFAULT_IMAGE =
|
|
8
|
+
process.env.PALETTE_DEV_IMAGE || "ghcr.io/palette-lab/platform-dev:latest"
|
|
9
|
+
const FRONTEND_PORT = process.env.PALETTE_FRONTEND_PORT || "3000"
|
|
10
|
+
const BACKEND_PORT = process.env.PALETTE_BACKEND_PORT || "8000"
|
|
11
|
+
const COMPOSE_FILE = path.resolve(__dirname, "..", "..", "platform-dev", "docker-compose.yml")
|
|
12
|
+
|
|
13
|
+
function ensureDocker() {
|
|
14
|
+
const check = spawnSync("docker", ["info"], { stdio: "ignore" })
|
|
15
|
+
if (check.status !== 0) {
|
|
16
|
+
console.error(
|
|
17
|
+
"[palette] docker is required for `palette dev`. Install Docker Desktop and make sure it is running.",
|
|
18
|
+
)
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function run(args, { cwd }) {
|
|
24
|
+
const manifest = loadManifest(cwd)
|
|
25
|
+
const pluginId = manifest.id
|
|
26
|
+
|
|
27
|
+
ensureDocker()
|
|
28
|
+
|
|
29
|
+
console.log(`[palette] starting ${DEFAULT_IMAGE} via docker compose`)
|
|
30
|
+
console.log(`[palette] mounting ${cwd} → /plugins/${pluginId}`)
|
|
31
|
+
console.log(`[palette] frontend: http://localhost:${FRONTEND_PORT}/apps/${pluginId}`)
|
|
32
|
+
console.log(`[palette] backend: http://localhost:${BACKEND_PORT}/api/v1/plugins/${pluginId}`)
|
|
33
|
+
|
|
34
|
+
const env = {
|
|
35
|
+
...process.env,
|
|
36
|
+
PALETTE_DEV_IMAGE: DEFAULT_IMAGE,
|
|
37
|
+
PALETTE_ACTIVE_PLUGIN: pluginId,
|
|
38
|
+
PALETTE_PLUGIN_DIR: cwd,
|
|
39
|
+
PALETTE_FRONTEND_PORT: FRONTEND_PORT,
|
|
40
|
+
PALETTE_BACKEND_PORT: BACKEND_PORT,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const projectName = `palette-dev-${pluginId}`
|
|
44
|
+
const upArgs = ["compose", "-f", COMPOSE_FILE, "-p", projectName, "up", "--remove-orphans"]
|
|
45
|
+
const child = spawn("docker", upArgs, { stdio: "inherit", env })
|
|
46
|
+
|
|
47
|
+
const stop = () => {
|
|
48
|
+
spawnSync(
|
|
49
|
+
"docker",
|
|
50
|
+
["compose", "-f", COMPOSE_FILE, "-p", projectName, "down"],
|
|
51
|
+
{ stdio: "inherit", env },
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
process.on("SIGINT", stop)
|
|
55
|
+
process.on("SIGTERM", stop)
|
|
56
|
+
await new Promise((resolve) => child.on("close", resolve))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = run
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const fs = require("fs")
|
|
4
|
+
const path = require("path")
|
|
5
|
+
const { spawnSync } = require("child_process")
|
|
6
|
+
const os = require("os")
|
|
7
|
+
|
|
8
|
+
const DEFAULT_TEMPLATE_REPO = "palette-lab/plugin-template"
|
|
9
|
+
const TEMPLATE_REPO = process.env.PALETTE_TEMPLATE_REPO || DEFAULT_TEMPLATE_REPO
|
|
10
|
+
const TEMPLATE_REF = process.env.PALETTE_TEMPLATE_REF || "main"
|
|
11
|
+
|
|
12
|
+
function toSlug(name) {
|
|
13
|
+
return name
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
16
|
+
.replace(/-+/g, "-")
|
|
17
|
+
.replace(/^-|-$/g, "")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function fetchTemplate(destDir) {
|
|
21
|
+
// Try git first — works offline-ish if git is available.
|
|
22
|
+
const git = spawnSync(
|
|
23
|
+
"git",
|
|
24
|
+
["clone", "--depth=1", "--branch", TEMPLATE_REF, `https://github.com/${TEMPLATE_REPO}.git`, destDir],
|
|
25
|
+
{ stdio: "inherit" },
|
|
26
|
+
)
|
|
27
|
+
if (git.status === 0) {
|
|
28
|
+
fs.rmSync(path.join(destDir, ".git"), { recursive: true, force: true })
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function copyLocalFallback(destDir) {
|
|
35
|
+
const fallback = path.resolve(__dirname, "..", "..", "template-fallback")
|
|
36
|
+
if (!fs.existsSync(fallback)) return false
|
|
37
|
+
fs.cpSync(fallback, destDir, { recursive: true })
|
|
38
|
+
return true
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function rewriteManifest(destDir, slug, displayName) {
|
|
42
|
+
const manifestPath = path.join(destDir, "palette-plugin.json")
|
|
43
|
+
if (!fs.existsSync(manifestPath)) return
|
|
44
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"))
|
|
45
|
+
manifest.id = slug
|
|
46
|
+
manifest.name = displayName
|
|
47
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function run(args, { cwd }) {
|
|
51
|
+
const name = args[0]
|
|
52
|
+
if (!name) {
|
|
53
|
+
console.error("[palette] usage: palette init <plugin-name>")
|
|
54
|
+
process.exit(1)
|
|
55
|
+
}
|
|
56
|
+
const slug = toSlug(name)
|
|
57
|
+
const displayName = name
|
|
58
|
+
.replace(/[-_]+/g, " ")
|
|
59
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
60
|
+
const destDir = path.join(cwd, slug)
|
|
61
|
+
if (fs.existsSync(destDir)) {
|
|
62
|
+
console.error(`[palette] directory already exists: ${destDir}`)
|
|
63
|
+
process.exit(1)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(`[palette] creating plugin "${slug}" from ${TEMPLATE_REPO}@${TEMPLATE_REF}`)
|
|
67
|
+
|
|
68
|
+
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)
|
|
73
|
+
}
|
|
74
|
+
if (!ok) {
|
|
75
|
+
console.error(
|
|
76
|
+
"[palette] no template available — install git or ship template-fallback/ with the CLI",
|
|
77
|
+
)
|
|
78
|
+
process.exit(1)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fs.cpSync(tmp, destDir, { recursive: true })
|
|
82
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
83
|
+
rewriteManifest(destDir, slug, displayName)
|
|
84
|
+
|
|
85
|
+
console.log(`[palette] created ${destDir}`)
|
|
86
|
+
console.log("[palette] next steps:")
|
|
87
|
+
console.log(` cd ${slug}`)
|
|
88
|
+
console.log(` npx @palettelab/cli dev`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = run
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto")
|
|
4
|
+
const { loadManifest, validateManifest } = require("../manifest")
|
|
5
|
+
const { bundleFrontend, bundleBackend } = require("../bundler")
|
|
6
|
+
|
|
7
|
+
const DEFAULT_PLATFORM_URL =
|
|
8
|
+
process.env.PALETTE_PLATFORM_URL || "https://platform.palette-lab.internal"
|
|
9
|
+
|
|
10
|
+
function sha256(buf) {
|
|
11
|
+
return crypto.createHash("sha256").update(buf).digest("hex")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function token() {
|
|
15
|
+
const t = process.env.PALETTE_PUBLISH_TOKEN
|
|
16
|
+
if (!t) {
|
|
17
|
+
console.error(
|
|
18
|
+
"[palette] PALETTE_PUBLISH_TOKEN is required. Ask a superadmin to mint one for you via\n" +
|
|
19
|
+
" POST /api/superadmin/publish-tokens — then `export PALETTE_PUBLISH_TOKEN=<token>`.",
|
|
20
|
+
)
|
|
21
|
+
process.exit(1)
|
|
22
|
+
}
|
|
23
|
+
return t
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function api(pathname, { method = "GET", body, headers = {} } = {}) {
|
|
27
|
+
const url = `${DEFAULT_PLATFORM_URL}${pathname}`
|
|
28
|
+
const res = await fetch(url, {
|
|
29
|
+
method,
|
|
30
|
+
headers: {
|
|
31
|
+
Authorization: `Bearer ${token()}`,
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
...headers,
|
|
34
|
+
},
|
|
35
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
36
|
+
})
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
const text = await res.text()
|
|
39
|
+
throw new Error(`${method} ${pathname} → ${res.status}: ${text}`)
|
|
40
|
+
}
|
|
41
|
+
const ct = res.headers.get("content-type") || ""
|
|
42
|
+
return ct.includes("application/json") ? res.json() : res.text()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function put(url, buf, contentType) {
|
|
46
|
+
const res = await fetch(url, {
|
|
47
|
+
method: "PUT",
|
|
48
|
+
headers: { "Content-Type": contentType },
|
|
49
|
+
body: buf,
|
|
50
|
+
})
|
|
51
|
+
if (!res.ok) {
|
|
52
|
+
throw new Error(`PUT ${url} → ${res.status}: ${await res.text()}`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function run(args, { cwd }) {
|
|
57
|
+
const manifest = loadManifest(cwd)
|
|
58
|
+
const errors = validateManifest(manifest)
|
|
59
|
+
if (errors.length) {
|
|
60
|
+
console.error("[palette] manifest invalid:")
|
|
61
|
+
for (const e of errors) console.error(` - ${e}`)
|
|
62
|
+
process.exit(1)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`[palette] publishing ${manifest.id}@${manifest.version}`)
|
|
66
|
+
|
|
67
|
+
console.log("[palette] bundling frontend")
|
|
68
|
+
const frontend = await bundleFrontend(cwd, manifest.frontend?.entry || "./frontend/src/index.tsx")
|
|
69
|
+
console.log(`[palette] ${frontend.length} bytes`)
|
|
70
|
+
|
|
71
|
+
console.log("[palette] bundling backend")
|
|
72
|
+
const backend = await bundleBackend(cwd)
|
|
73
|
+
console.log(`[palette] ${backend.length} bytes`)
|
|
74
|
+
|
|
75
|
+
const backendSha = sha256(backend)
|
|
76
|
+
|
|
77
|
+
console.log("[palette] requesting signed URLs")
|
|
78
|
+
const signed = await api("/api/v1/appstore/sign-upload", {
|
|
79
|
+
method: "POST",
|
|
80
|
+
body: {
|
|
81
|
+
plugin_id: manifest.id,
|
|
82
|
+
version: manifest.version,
|
|
83
|
+
bundle_sha256: backendSha,
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
console.log("[palette] uploading")
|
|
88
|
+
await Promise.all([
|
|
89
|
+
put(signed.frontend_upload_url, frontend, "application/javascript"),
|
|
90
|
+
put(signed.backend_upload_url, backend, "application/gzip"),
|
|
91
|
+
put(
|
|
92
|
+
signed.manifest_upload_url,
|
|
93
|
+
Buffer.from(JSON.stringify(manifest, null, 2)),
|
|
94
|
+
"application/json",
|
|
95
|
+
),
|
|
96
|
+
])
|
|
97
|
+
|
|
98
|
+
console.log("[palette] finalizing")
|
|
99
|
+
const record = await api("/api/v1/appstore/publish", {
|
|
100
|
+
method: "POST",
|
|
101
|
+
body: {
|
|
102
|
+
plugin_id: manifest.id,
|
|
103
|
+
version: manifest.version,
|
|
104
|
+
bundle_path: signed.bundle_path,
|
|
105
|
+
bundle_sha256: backendSha,
|
|
106
|
+
manifest,
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
console.log(`[palette] published ${record.plugin_id}@${record.version}`)
|
|
111
|
+
console.log(`[palette] live at ${DEFAULT_PLATFORM_URL}${record.catalog_url}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = run
|
package/lib/manifest.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const fs = require("fs")
|
|
4
|
+
const path = require("path")
|
|
5
|
+
|
|
6
|
+
const MANIFEST_FILE = "palette-plugin.json"
|
|
7
|
+
|
|
8
|
+
function loadManifest(cwd) {
|
|
9
|
+
const manifestPath = path.join(cwd, MANIFEST_FILE)
|
|
10
|
+
if (!fs.existsSync(manifestPath)) {
|
|
11
|
+
throw new Error(`no ${MANIFEST_FILE} in ${cwd}`)
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(manifestPath, "utf8"))
|
|
15
|
+
} catch (err) {
|
|
16
|
+
throw new Error(`${MANIFEST_FILE} is not valid JSON: ${err.message}`)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Lightweight subset validator — full JSON-schema validation happens at
|
|
21
|
+
// container startup. This covers the fields the CLI itself consumes.
|
|
22
|
+
function validateManifest(m) {
|
|
23
|
+
const errors = []
|
|
24
|
+
if (!m.id || typeof m.id !== "string") errors.push("id is required (string)")
|
|
25
|
+
else if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(m.id))
|
|
26
|
+
errors.push("id must be lowercase kebab-case")
|
|
27
|
+
if (!m.name) errors.push("name is required")
|
|
28
|
+
if (!m.version) errors.push("version is required")
|
|
29
|
+
else if (!/^\d+\.\d+\.\d+/.test(m.version)) errors.push("version must be semver")
|
|
30
|
+
if (m.frontend && !m.frontend.entry) errors.push("frontend.entry is required when frontend is set")
|
|
31
|
+
if (m.backend && !m.backend.entry) errors.push("backend.entry is required when backend is set")
|
|
32
|
+
return errors
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { loadManifest, validateManifest }
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@palettelab/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Developer CLI for building Palette platform plugins — no platform source access required.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"palette": "./bin/palette.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"bin",
|
|
10
|
+
"lib",
|
|
11
|
+
"platform-dev",
|
|
12
|
+
"template-fallback",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"esbuild": "^0.24.0"
|
|
20
|
+
},
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"registry": "https://registry.npmjs.org/",
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/palette-lab/virtual-organisation.git",
|
|
28
|
+
"directory": "sdk/cli-npm"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"keywords": [
|
|
32
|
+
"palette",
|
|
33
|
+
"plugin",
|
|
34
|
+
"sdk",
|
|
35
|
+
"cli"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Bundled docker-compose for `palette dev`. The CLI references this file
|
|
2
|
+
# from the installed npm package. Third-party developers never edit it.
|
|
3
|
+
#
|
|
4
|
+
# Usage (handled by @palettelab/cli):
|
|
5
|
+
# docker compose -f docker-compose.yml up --no-build
|
|
6
|
+
#
|
|
7
|
+
# The `platform` service mounts the developer's plugin repo at /plugins/<id>.
|
|
8
|
+
|
|
9
|
+
services:
|
|
10
|
+
platform:
|
|
11
|
+
image: ${PALETTE_DEV_IMAGE:-ghcr.io/palette-lab/platform-dev:latest}
|
|
12
|
+
ports:
|
|
13
|
+
- "${PALETTE_FRONTEND_PORT:-3000}:3000"
|
|
14
|
+
- "${PALETTE_BACKEND_PORT:-8000}:8000"
|
|
15
|
+
environment:
|
|
16
|
+
PALETTE_DEV_MODE: "1"
|
|
17
|
+
NEXT_PUBLIC_PALETTE_DEV_MODE: "1"
|
|
18
|
+
PALETTE_ACTIVE_PLUGIN: "${PALETTE_ACTIVE_PLUGIN:-}"
|
|
19
|
+
DATABASE_URL: "postgresql+asyncpg://postgres:postgres@postgres:5432/app"
|
|
20
|
+
REDIS_URL: "redis://redis:6379/0"
|
|
21
|
+
JWT_SECRET: "dev-secret-do-not-use-in-prod"
|
|
22
|
+
FRONTEND_URL: "http://localhost:${PALETTE_FRONTEND_PORT:-3000}"
|
|
23
|
+
# Disable optional features that need real credentials
|
|
24
|
+
RAG_ENABLED: "false"
|
|
25
|
+
OPENAI_API_KEY: ""
|
|
26
|
+
volumes:
|
|
27
|
+
- "${PALETTE_PLUGIN_DIR}:/plugins/${PALETTE_ACTIVE_PLUGIN}"
|
|
28
|
+
depends_on:
|
|
29
|
+
postgres:
|
|
30
|
+
condition: service_healthy
|
|
31
|
+
redis:
|
|
32
|
+
condition: service_healthy
|
|
33
|
+
|
|
34
|
+
postgres:
|
|
35
|
+
image: postgres:17-alpine
|
|
36
|
+
environment:
|
|
37
|
+
POSTGRES_USER: postgres
|
|
38
|
+
POSTGRES_PASSWORD: postgres
|
|
39
|
+
POSTGRES_DB: app
|
|
40
|
+
healthcheck:
|
|
41
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
42
|
+
interval: 5s
|
|
43
|
+
timeout: 5s
|
|
44
|
+
retries: 10
|
|
45
|
+
|
|
46
|
+
redis:
|
|
47
|
+
image: redis:7-alpine
|
|
48
|
+
healthcheck:
|
|
49
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
50
|
+
interval: 5s
|
|
51
|
+
timeout: 5s
|
|
52
|
+
retries: 10
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Plugin backend API.
|
|
2
|
+
|
|
3
|
+
Define your plugin's API endpoints here. The router will be automatically
|
|
4
|
+
mounted at /api/v1/plugins/{your-plugin-id}/ by the platform.
|
|
5
|
+
|
|
6
|
+
Available dependencies:
|
|
7
|
+
from palette_sdk import PluginContext, get_plugin_context
|
|
8
|
+
ctx: PluginContext = Depends(get_plugin_context)
|
|
9
|
+
|
|
10
|
+
The PluginContext provides:
|
|
11
|
+
- ctx.db — async SQLAlchemy session
|
|
12
|
+
- ctx.user_id — authenticated user's UUID
|
|
13
|
+
- ctx.organization_id — current org ID
|
|
14
|
+
- ctx.org_role — user's role (owner/admin/member)
|
|
15
|
+
- ctx.plugin_id — this plugin's ID
|
|
16
|
+
- ctx.permissions — declared permissions from manifest
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from fastapi import Depends
|
|
20
|
+
|
|
21
|
+
from palette_sdk import PluginRouter, PluginContext, get_plugin_context, SuccessResponse
|
|
22
|
+
|
|
23
|
+
router = PluginRouter(tags=["my-plugin"])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.get("/status")
|
|
27
|
+
async def get_status(ctx: PluginContext = Depends(get_plugin_context)):
|
|
28
|
+
"""Example endpoint — returns plugin status."""
|
|
29
|
+
return SuccessResponse(
|
|
30
|
+
message="Plugin is running",
|
|
31
|
+
data={
|
|
32
|
+
"plugin_id": ctx.plugin_id,
|
|
33
|
+
"user_id": ctx.user_id,
|
|
34
|
+
"organization_id": ctx.organization_id,
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@router.get("/hello")
|
|
40
|
+
async def hello(ctx: PluginContext = Depends(get_plugin_context)):
|
|
41
|
+
"""Example endpoint — simple hello."""
|
|
42
|
+
return {"message": f"Hello from plugin! User: {ctx.user_id}"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Example custom agent tool.
|
|
2
|
+
|
|
3
|
+
To register this tool, add it to your palette-plugin.json:
|
|
4
|
+
{
|
|
5
|
+
"tools": [{
|
|
6
|
+
"name": "example_tool",
|
|
7
|
+
"description": "An example custom tool",
|
|
8
|
+
"entry": "./backend/tools/example_tool.py"
|
|
9
|
+
}]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
Then assign the tool name to an agent's tools list in the agents section:
|
|
13
|
+
{
|
|
14
|
+
"agents": [{
|
|
15
|
+
"name": "My Agent",
|
|
16
|
+
"tools": ["example_tool"]
|
|
17
|
+
}]
|
|
18
|
+
}
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from palette_sdk import ToolDefinition
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ExampleTool(ToolDefinition):
|
|
25
|
+
name = "example_tool"
|
|
26
|
+
description = "An example tool that echoes input back. Replace this with your own logic."
|
|
27
|
+
input_schema = {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"query": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "The input query to process",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
"required": ["query"],
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async def run(self, input_data: dict, context: dict) -> str:
|
|
39
|
+
query = input_data.get("query", "")
|
|
40
|
+
return f"ExampleTool received: {query}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# The plugin loader looks for a `tool` attribute or any ToolDefinition subclass
|
|
44
|
+
tool = ExampleTool
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plugin entry component.
|
|
5
|
+
*
|
|
6
|
+
* This is the root component that the platform renders when your plugin is opened.
|
|
7
|
+
* It receives a `platform` prop with access to the authenticated user, API client,
|
|
8
|
+
* navigation, and toast notifications.
|
|
9
|
+
*
|
|
10
|
+
* Available hooks from @palettelab/sdk:
|
|
11
|
+
* - usePlatform() — access user, org, apiFetch, navigate, showToast
|
|
12
|
+
* - usePluginTasks() — CRUD tasks
|
|
13
|
+
* - usePluginDataRooms() — browse data rooms and files
|
|
14
|
+
* - usePluginChat() — chat with agents
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { usePlatform, usePluginTasks } from "@palettelab/sdk"
|
|
18
|
+
import type { PluginComponentProps } from "@palettelab/sdk"
|
|
19
|
+
|
|
20
|
+
export default function MyPlugin({ platform }: PluginComponentProps) {
|
|
21
|
+
const { user, showToast } = usePlatform()
|
|
22
|
+
const { tasks, loading, createTask } = usePluginTasks()
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="p-6 space-y-6">
|
|
26
|
+
<div>
|
|
27
|
+
<h1 className="text-2xl font-bold">My Plugin</h1>
|
|
28
|
+
<p className="text-muted-foreground mt-1">
|
|
29
|
+
Hello, {user.name}! This is your plugin template.
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div className="rounded-lg border p-4 space-y-3">
|
|
34
|
+
<h2 className="font-semibold">Quick Start</h2>
|
|
35
|
+
<ul className="text-sm text-muted-foreground space-y-1 list-disc list-inside">
|
|
36
|
+
<li>Edit <code>frontend/src/index.tsx</code> to build your UI</li>
|
|
37
|
+
<li>Edit <code>backend/api/main.py</code> to add API endpoints</li>
|
|
38
|
+
<li>Use <code>usePlatform()</code> to access the authenticated user and API</li>
|
|
39
|
+
<li>Use <code>usePluginTasks()</code> to manage tasks</li>
|
|
40
|
+
<li>Use <code>usePluginDataRooms()</code> to browse files</li>
|
|
41
|
+
</ul>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div className="rounded-lg border p-4">
|
|
45
|
+
<h2 className="font-semibold mb-2">Tasks ({loading ? "..." : tasks.length})</h2>
|
|
46
|
+
{!loading && tasks.length === 0 && (
|
|
47
|
+
<p className="text-sm text-muted-foreground">No tasks yet.</p>
|
|
48
|
+
)}
|
|
49
|
+
{tasks.slice(0, 5).map(task => (
|
|
50
|
+
<div key={task.id} className="flex items-center justify-between py-1 text-sm">
|
|
51
|
+
<span>{task.title}</span>
|
|
52
|
+
<span className="text-xs text-muted-foreground">{task.status}</span>
|
|
53
|
+
</div>
|
|
54
|
+
))}
|
|
55
|
+
<button
|
|
56
|
+
className="mt-3 px-3 py-1.5 text-sm bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
|
|
57
|
+
onClick={async () => {
|
|
58
|
+
await createTask({ title: "Sample task from plugin", priority: "medium" })
|
|
59
|
+
showToast("Task created!", "success")
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
Create Sample Task
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "palette-plugin-my-plugin",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "A Palette platform plugin",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@palettelab/sdk": "^0.1.0"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"typescript": "^5.0.0",
|
|
11
|
+
"@types/react": "^19.0.0"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "my-plugin",
|
|
3
|
+
"name": "My Plugin",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"developer": "Your Team",
|
|
6
|
+
"category": "Productivity",
|
|
7
|
+
"tagline": "A short description of what this plugin does",
|
|
8
|
+
"description": "A longer description explaining the plugin's features and capabilities.",
|
|
9
|
+
"icon": "Puzzle",
|
|
10
|
+
"gradient": {
|
|
11
|
+
"bg": "linear-gradient(135deg, #6366F1, #8B5CF6)",
|
|
12
|
+
"text": "#fff"
|
|
13
|
+
},
|
|
14
|
+
"frontend": {
|
|
15
|
+
"entry": "./frontend/src/index.tsx"
|
|
16
|
+
},
|
|
17
|
+
"backend": {
|
|
18
|
+
"entry": "./backend/api/main.py"
|
|
19
|
+
},
|
|
20
|
+
"agents": [],
|
|
21
|
+
"tools": [],
|
|
22
|
+
"permissions": [
|
|
23
|
+
"data_rooms:read",
|
|
24
|
+
"tasks:read",
|
|
25
|
+
"tasks:write"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "palette-plugin-my-plugin"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "A Palette platform plugin"
|
|
5
|
+
requires-python = ">=3.12"
|
|
6
|
+
# palette-sdk is installed from the private platform repo. The
|
|
7
|
+
# platform-dev container already has it on PYTHONPATH, so you do not
|
|
8
|
+
# need to install it locally to run `palette dev`. If you want to run
|
|
9
|
+
# `pytest` on the backend outside the container, add this dependency
|
|
10
|
+
# (replace $GH_PAT with your GitHub PAT with read:packages):
|
|
11
|
+
#
|
|
12
|
+
# dependencies = [
|
|
13
|
+
# "palette-sdk @ git+https://${GH_PAT}@github.com/palette-lab/virtual-organisation.git#subdirectory=sdk/backend",
|
|
14
|
+
# ]
|
|
15
|
+
dependencies = []
|