@palettelab/cli 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +103 -10
  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 +37 -2
  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
@@ -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
@@ -0,0 +1,99 @@
1
+ "use strict"
2
+
3
+ const fs = require("fs")
4
+ const path = require("path")
5
+ const { resolveEnvironment, parseFlags } = require("../environments")
6
+ const { loadManifest } = require("../manifest")
7
+
8
+ function readLastPublish(cwd) {
9
+ const p = path.join(cwd, ".palette", "last-publish.json")
10
+ if (!fs.existsSync(p)) return null
11
+ try {
12
+ return JSON.parse(fs.readFileSync(p, "utf8"))
13
+ } catch {
14
+ return null
15
+ }
16
+ }
17
+
18
+ function getOpt(argv, name, def) {
19
+ const i = argv.indexOf(name)
20
+ if (i >= 0 && argv[i + 1]) return argv[i + 1]
21
+ return def
22
+ }
23
+
24
+ async function fetchLogs(env, pluginId, tail) {
25
+ const url = `${env.url}/api/v1/appstore/plugins/${encodeURIComponent(pluginId)}/logs?tail=${tail}`
26
+ const res = await fetch(url, { headers: { Authorization: `Bearer ${env.token}` } })
27
+ if (!res.ok) {
28
+ const text = await res.text()
29
+ throw new Error(`GET ${url} → ${res.status}: ${text}`)
30
+ }
31
+ return res.json()
32
+ }
33
+
34
+ async function run(argv, { cwd }) {
35
+ const json = argv.includes("--json")
36
+ const follow = argv.includes("--follow") || argv.includes("-f")
37
+ const tail = parseInt(getOpt(argv, "--tail", "50"), 10) || 50
38
+
39
+ const positional = argv.filter((a) => !a.startsWith("-"))
40
+ let pluginId = positional[0]
41
+ const last = readLastPublish(cwd)
42
+ if (!pluginId) {
43
+ if (last && last.plugin_id) pluginId = last.plugin_id
44
+ else {
45
+ try {
46
+ pluginId = loadManifest(cwd).id
47
+ } catch {
48
+ // ignore
49
+ }
50
+ }
51
+ }
52
+ if (!pluginId) {
53
+ console.error("[palette] no plugin id provided and no manifest found.")
54
+ process.exit(1)
55
+ }
56
+
57
+ const { flags } = parseFlags(argv)
58
+ if (!flags.env && last && last.env) flags.env = last.env
59
+ let env
60
+ try {
61
+ env = resolveEnvironment({ cwd, flags })
62
+ } catch (err) {
63
+ console.error(`[palette] ${err.message}`)
64
+ process.exit(1)
65
+ }
66
+ if (!env.token) {
67
+ console.error(`[palette] no publish token for "${env.name}". set $${env.token_env}.`)
68
+ process.exit(1)
69
+ }
70
+
71
+ let lastSeenId = 0
72
+ do {
73
+ let data
74
+ try {
75
+ data = await fetchLogs(env, pluginId, tail)
76
+ } catch (err) {
77
+ console.error(`[palette] ${err.message}`)
78
+ process.exit(1)
79
+ }
80
+ const events = data.events || []
81
+ const fresh = events.filter((e) => e.id > lastSeenId)
82
+ if (fresh.length) lastSeenId = Math.max(...fresh.map((e) => e.id))
83
+ if (json) {
84
+ for (const e of fresh) console.log(JSON.stringify(e))
85
+ } else {
86
+ for (const e of fresh) {
87
+ const ts = e.created_at
88
+ const tag = e.event_type.padEnd(11)
89
+ const route = e.route || "-"
90
+ const code = e.status_code != null ? `[${e.status_code}]` : ""
91
+ const err = e.error_message ? ` err=${e.error_message}` : ""
92
+ console.log(`${ts} ${tag} ${route} ${code}${err}`)
93
+ }
94
+ }
95
+ if (follow) await new Promise((r) => setTimeout(r, 3000))
96
+ } while (follow)
97
+ }
98
+
99
+ module.exports = run
@@ -0,0 +1,64 @@
1
+ "use strict"
2
+
3
+ const fs = require("fs")
4
+ const path = require("path")
5
+ const crypto = require("crypto")
6
+ const { loadManifest, validateManifest } = require("../manifest")
7
+ const { bundleFrontend, bundleBackend } = require("../bundler")
8
+
9
+ function sha256(buf) {
10
+ return crypto.createHash("sha256").update(buf).digest("hex")
11
+ }
12
+
13
+ async function run(argv, { cwd }) {
14
+ const json = argv.includes("--json")
15
+ const manifest = loadManifest(cwd)
16
+ const errors = validateManifest(manifest)
17
+ if (errors.length) {
18
+ console.error("[palette] manifest invalid:")
19
+ for (const e of errors) console.error(` - ${e}`)
20
+ process.exit(1)
21
+ }
22
+
23
+ const distDir = path.join(cwd, "dist")
24
+ fs.mkdirSync(distDir, { recursive: true })
25
+
26
+ const frontend = manifest.frontend
27
+ ? await bundleFrontend(cwd, manifest.frontend.entry || "./frontend/src/index.tsx")
28
+ : null
29
+ const backend = manifest.backend ? await bundleBackend(cwd) : null
30
+
31
+ const out = path.join(distDir, `${manifest.id}-${manifest.version}.tar.gz`)
32
+ // Build a simple tar by piping through node-tar via spawn — avoid extra dep
33
+ // by writing files into a staging dir and shelling out to `tar`. The
34
+ // platform-dev image already ships with tar, and any developer running this
35
+ // CLI will have it too on macOS/Linux.
36
+ const stage = fs.mkdtempSync(path.join(require("os").tmpdir(), "palette-pkg-"))
37
+ fs.writeFileSync(path.join(stage, "palette-plugin.json"), JSON.stringify(manifest, null, 2))
38
+ if (frontend) fs.writeFileSync(path.join(stage, "frontend.mjs"), frontend)
39
+ if (backend) fs.writeFileSync(path.join(stage, "backend.tar.gz"), backend)
40
+
41
+ const { spawnSync } = require("child_process")
42
+ const r = spawnSync("tar", ["-czf", out, "-C", stage, "."], { stdio: "inherit" })
43
+ if (r.status !== 0) {
44
+ console.error("[palette] tar failed")
45
+ process.exit(1)
46
+ }
47
+
48
+ const buf = fs.readFileSync(out)
49
+ const result = {
50
+ id: manifest.id,
51
+ version: manifest.version,
52
+ path: out,
53
+ bytes: buf.length,
54
+ sha256: sha256(buf),
55
+ }
56
+ if (json) {
57
+ console.log(JSON.stringify(result, null, 2))
58
+ } else {
59
+ console.log(`[palette] packaged ${manifest.id}@${manifest.version} → ${out} (${buf.length} bytes)`)
60
+ console.log(`[palette] sha256: ${result.sha256}`)
61
+ }
62
+ }
63
+
64
+ module.exports = run
@@ -1,6 +1,8 @@
1
1
  "use strict"
2
2
 
3
3
  const crypto = require("crypto")
4
+ const fs = require("fs")
5
+ const path = require("path")
4
6
  const { loadManifest, validateManifest } = require("../manifest")
5
7
  const { bundleFrontend, bundleBackend } = require("../bundler")
6
8
  const {
@@ -85,9 +87,14 @@ async function run(argv, { cwd }) {
85
87
  `[palette] publishing ${manifest.id}@${manifest.version} → ${env.name} (${env.url})`,
86
88
  )
87
89
 
88
- console.log("[palette] bundling frontend")
89
- const frontend = await bundleFrontend(cwd, manifest.frontend?.entry || "./frontend/src/index.tsx")
90
- console.log(`[palette] ${frontend.length} bytes`)
90
+ let frontend = null
91
+ if (manifest.frontend?.entry) {
92
+ console.log("[palette] bundling frontend")
93
+ frontend = await bundleFrontend(cwd, manifest.frontend.entry)
94
+ console.log(`[palette] ${frontend.length} bytes`)
95
+ } else {
96
+ console.log("[palette] no frontend declared")
97
+ }
91
98
 
92
99
  console.log("[palette] bundling backend")
93
100
  const backend = await bundleBackend(cwd)
@@ -107,15 +114,18 @@ async function run(argv, { cwd }) {
107
114
  })
108
115
 
109
116
  console.log("[palette] uploading")
110
- await Promise.all([
111
- put(signed.frontend_upload_url, frontend, "application/javascript"),
117
+ const uploads = [
112
118
  put(signed.backend_upload_url, backend, "application/gzip"),
113
119
  put(
114
120
  signed.manifest_upload_url,
115
121
  Buffer.from(JSON.stringify(manifest, null, 2)),
116
122
  "application/json",
117
123
  ),
118
- ])
124
+ ]
125
+ if (frontend) {
126
+ uploads.push(put(signed.frontend_upload_url, frontend, "application/javascript"))
127
+ }
128
+ await Promise.all(uploads)
119
129
 
120
130
  console.log("[palette] finalizing")
121
131
  const record = await api("/api/v1/appstore/publish", {
@@ -129,10 +139,44 @@ async function run(argv, { cwd }) {
129
139
  },
130
140
  })
131
141
 
142
+ try {
143
+ const dir = path.join(cwd, ".palette")
144
+ fs.mkdirSync(dir, { recursive: true })
145
+ fs.writeFileSync(
146
+ path.join(dir, "last-publish.json"),
147
+ JSON.stringify(
148
+ {
149
+ id: record.id,
150
+ plugin_id: record.plugin_id,
151
+ version: record.version,
152
+ env: env.name,
153
+ url: env.url,
154
+ preview_url: record.preview_url,
155
+ published_at: new Date().toISOString(),
156
+ },
157
+ null,
158
+ 2,
159
+ ),
160
+ )
161
+ } catch (err) {
162
+ // best-effort
163
+ }
164
+
165
+ if (flags.json) {
166
+ console.log(JSON.stringify(record, null, 2))
167
+ return
168
+ }
169
+
132
170
  console.log(
133
171
  `[palette] published ${record.plugin_id}@${record.version} (status=${record.status})`,
134
172
  )
135
173
  console.log(`[palette] awaiting superadmin review on ${env.url}`)
174
+ if (record.review_url) {
175
+ console.log(`[palette] review queue: ${record.review_url}`)
176
+ }
177
+ if (record.preview_url) {
178
+ console.log(`[palette] preview: ${record.preview_url}`)
179
+ }
136
180
  console.log(`[palette] once approved, live at ${env.url}${record.catalog_url}`)
137
181
  }
138
182
 
@@ -0,0 +1,80 @@
1
+ "use strict"
2
+
3
+ const fs = require("fs")
4
+ const path = require("path")
5
+ const { resolveEnvironment, parseFlags } = require("../environments")
6
+
7
+ function readLastPublish(cwd) {
8
+ const p = path.join(cwd, ".palette", "last-publish.json")
9
+ if (!fs.existsSync(p)) return null
10
+ try {
11
+ return JSON.parse(fs.readFileSync(p, "utf8"))
12
+ } catch {
13
+ return null
14
+ }
15
+ }
16
+
17
+ async function run(argv, { cwd }) {
18
+ const json = argv.includes("--json")
19
+ const positional = argv.filter((a) => !a.startsWith("-"))
20
+ const last = readLastPublish(cwd)
21
+ const publishId = positional[0] || (last && last.id)
22
+
23
+ if (!publishId) {
24
+ console.error(
25
+ "[palette] no publish id provided and no .palette/last-publish.json found.",
26
+ )
27
+ console.error(" usage: pltt status <publish-id> [--env <name>]")
28
+ process.exit(1)
29
+ }
30
+
31
+ const { flags } = parseFlags(argv)
32
+ if (!flags.env && last && last.env) flags.env = last.env
33
+
34
+ let env
35
+ try {
36
+ env = resolveEnvironment({ cwd, flags })
37
+ } catch (err) {
38
+ console.error(`[palette] ${err.message}`)
39
+ process.exit(1)
40
+ }
41
+
42
+ if (!env.token) {
43
+ console.error(
44
+ `[palette] no publish token for environment "${env.name}". set $${env.token_env}.`,
45
+ )
46
+ process.exit(1)
47
+ }
48
+
49
+ const url = `${env.url}/api/v1/appstore/publishes/${publishId}`
50
+ const res = await fetch(url, {
51
+ headers: { Authorization: `Bearer ${env.token}` },
52
+ })
53
+ if (!res.ok) {
54
+ const text = await res.text()
55
+ console.error(`[palette] GET ${url} → ${res.status}: ${text}`)
56
+ process.exit(1)
57
+ }
58
+ const data = await res.json()
59
+
60
+ if (json) {
61
+ console.log(JSON.stringify(data, null, 2))
62
+ return
63
+ }
64
+
65
+ console.log(`[palette] ${data.plugin_id}@${data.version}`)
66
+ console.log(` status: ${data.status}`)
67
+ if (data.review_decision) console.log(` decision: ${data.review_decision}`)
68
+ if (data.review_reason) console.log(` reason: ${data.review_reason}`)
69
+ if (data.preview_url) console.log(` preview: ${data.preview_url}`)
70
+ console.log(` published: ${data.published_at}`)
71
+ if (data.report) {
72
+ console.log(` risk: ${data.report.risk_score.toUpperCase()}`)
73
+ console.log(
74
+ ` permissions=${data.report.permission_risk}, migrations=${data.report.migration_risk}, network=${data.report.external_network_risk}`,
75
+ )
76
+ console.log(` bundle=${data.report.bundle_size_bytes} bytes, sandbox=${data.report.sandbox_enabled}`)
77
+ }
78
+ }
79
+
80
+ module.exports = run