@palettelab/cli 0.3.48 → 0.3.50

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.
@@ -8,7 +8,7 @@ const { spawn, spawnSync } = require("child_process")
8
8
  const { loadManifest } = require("./manifest")
9
9
  const { frontendBuildConfig } = require("./bundler")
10
10
  const { generatePaletteAppEntry } = require("./app-router")
11
- const { loadLocalEnv } = require("./secrets")
11
+ const { declaredSecrets, loadLocalEnv } = require("./secrets")
12
12
 
13
13
  function loadEsbuild() {
14
14
  try {
@@ -51,6 +51,13 @@ function needsDatabase(manifest) {
51
51
  return Boolean(manifest.database || manifest.capabilities?.database)
52
52
  }
53
53
 
54
+ function databaseDriverDependencies(databaseUrl) {
55
+ const url = String(databaseUrl || "")
56
+ if (/^postgres(?:ql)?(?:\+asyncpg)?:/i.test(url)) return ["asyncpg>=0.29.0"]
57
+ if (/^mysql\+aiomysql:/i.test(url)) return ["aiomysql>=0.2.0"]
58
+ return []
59
+ }
60
+
54
61
  function loadLocalConnections(cwd, manifest) {
55
62
  const declared = Array.isArray(manifest.connections) ? manifest.connections : []
56
63
  const out = {}
@@ -103,13 +110,22 @@ function loadLocalAppServiceMocks(cwd) {
103
110
  }
104
111
  }
105
112
 
113
+ function resolveDevSecrets(cwd, manifest) {
114
+ const values = loadLocalEnv(cwd, { apply: false })
115
+ for (const name of Object.keys(declaredSecrets(manifest))) {
116
+ if (process.env[name] !== undefined) values[name] = process.env[name]
117
+ }
118
+ return values
119
+ }
120
+
106
121
  function ensurePythonEnv(cwd, devDir, manifest) {
107
122
  const hostPython = process.env.PALETTE_PYTHON || "python3"
108
123
  const venvDir = path.join(devDir, "backend-venv")
109
124
  const venvPython = path.join(venvDir, "bin", "python")
110
125
  const lockPath = path.join(venvDir, ".palette-dev-deps-lock")
111
126
  const dbDeps = needsDatabase(manifest) ? ["aiosqlite>=0.20.0", "greenlet>=3.0.0"] : []
112
- const deps = Array.from(new Set([...pyprojectDependencies(cwd), ...dbDeps, "uvicorn>=0.30.0"]))
127
+ const dbDriverDeps = needsDatabase(manifest) ? databaseDriverDependencies(process.env.PALETTE_DEV_DATABASE_URL) : []
128
+ const deps = Array.from(new Set([...pyprojectDependencies(cwd), ...dbDeps, ...dbDriverDeps, "uvicorn>=0.30.0"]))
113
129
  const lock = JSON.stringify(deps)
114
130
 
115
131
  if (!fs.existsSync(venvPython)) {
@@ -149,7 +165,7 @@ function writeBackendRunner(cwd, devDir, manifest, backendEntry, backendPort) {
149
165
  const runner = path.join(devDir, "backend_runner.py")
150
166
  const sdkPath = localBackendSdkPath()
151
167
  const databasePath = path.join(devDir, `${manifest.id}.sqlite3`)
152
- const devSecrets = loadLocalEnv(cwd, { apply: false })
168
+ const devSecrets = resolveDevSecrets(cwd, manifest)
153
169
  const devConnections = loadLocalConnections(cwd, manifest)
154
170
  const devAppMocks = loadLocalAppServiceMocks(cwd)
155
171
  const content = `from __future__ import annotations
@@ -615,6 +631,7 @@ function indexHtml(manifest) {
615
631
  .palette-local-toasts { position: fixed; right: 16px; bottom: 16px; display: grid; gap: 8px; z-index: 50; }
616
632
  .palette-local-toast { background: #1d1b18; color: white; padding: 10px 12px; font-size: 13px; box-shadow: 0 10px 30px rgba(0,0,0,.15); }
617
633
  </style>
634
+ <link rel="stylesheet" href="/simulator.css" />
618
635
  </head>
619
636
  <body>
620
637
  <div id="root"></div>
@@ -642,6 +659,7 @@ async function startFrontend(cwd, devDir, manifest, frontendPort, backendPort) {
642
659
  if (!fs.existsSync(absEntry)) throw new Error(`frontend entry not found: ${entry}`)
643
660
  const generatedEntry = path.join(devDir, "simulator-entry.jsx")
644
661
  const bundlePath = path.join(devDir, "simulator.js")
662
+ const cssPath = path.join(devDir, "simulator.css")
645
663
  fs.writeFileSync(generatedEntry, simulatorEntrySource(cwd, absEntry, manifest, backendPort))
646
664
  const buildConfig = frontendBuildConfig(cwd, { ...(manifest.frontend || {}), entry })
647
665
 
@@ -658,6 +676,7 @@ async function startFrontend(cwd, devDir, manifest, frontendPort, backendPort) {
658
676
  define: buildConfig.define,
659
677
  absWorkingDir: cwd,
660
678
  sourcemap: "inline",
679
+ metafile: true,
661
680
  logLevel: "silent",
662
681
  plugins: [
663
682
  ...buildConfig.plugins,
@@ -688,6 +707,17 @@ async function startFrontend(cwd, devDir, manifest, frontendPort, backendPort) {
688
707
  res.end(fs.readFileSync(bundlePath))
689
708
  return
690
709
  }
710
+ if (url.pathname === "/simulator.css") {
711
+ console.log(`[pltt] frontend GET ${url.pathname} -> ${fs.existsSync(cssPath) ? 200 : 204}`)
712
+ if (!fs.existsSync(cssPath)) {
713
+ res.writeHead(204, { "Cache-Control": "no-store" })
714
+ res.end()
715
+ return
716
+ }
717
+ res.writeHead(200, { "Content-Type": "text/css; charset=utf-8", "Cache-Control": "no-store" })
718
+ res.end(fs.readFileSync(cssPath))
719
+ return
720
+ }
691
721
  console.log(`[pltt] frontend GET ${url.pathname} -> 200`)
692
722
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" })
693
723
  res.end(indexHtml(manifest))
@@ -736,4 +766,4 @@ async function startSimulator({ cwd, frontendPort, backendPort }) {
736
766
  await stop()
737
767
  }
738
768
 
739
- module.exports = { startSimulator }
769
+ module.exports = { databaseDriverDependencies, resolveDevSecrets, startSimulator }
package/lib/manifest.js CHANGED
@@ -53,6 +53,7 @@ const TOP_LEVEL_KEYS = new Set([
53
53
  "platform_services",
54
54
  "provides",
55
55
  "requires",
56
+ "consumes",
56
57
  ])
57
58
 
58
59
  function loadManifest(cwd) {
@@ -237,6 +238,10 @@ function isCapabilityId(value) {
237
238
  return typeof value === "string" && /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/.test(value)
238
239
  }
239
240
 
241
+ function isBrokerTarget(value) {
242
+ return typeof value === "string" && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+#[A-Za-z0-9_.-]+$/.test(value)
243
+ }
244
+
240
245
  function validateServiceRoutes(value, label, errors) {
241
246
  if (value === undefined) return
242
247
  if (!Array.isArray(value)) {
@@ -262,13 +267,63 @@ function validateServiceRoutes(value, label, errors) {
262
267
  })
263
268
  }
264
269
 
270
+ function validateServiceMethods(value, label, errors) {
271
+ if (value === undefined) return
272
+ if (!Array.isArray(value)) {
273
+ errors.push(`${label}.methods must be an array`)
274
+ return
275
+ }
276
+ const methods = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"])
277
+ const seen = new Set()
278
+ value.forEach((method, i) => {
279
+ const methodLabel = `${label}.methods[${i}]`
280
+ if (!isObject(method)) {
281
+ errors.push(`${methodLabel} must be an object`)
282
+ return
283
+ }
284
+ unknownKeys(
285
+ method,
286
+ new Set([
287
+ "name",
288
+ "scope",
289
+ "label",
290
+ "description",
291
+ "input_schema",
292
+ "input",
293
+ "output_schema",
294
+ "output",
295
+ "route_method",
296
+ "route_path",
297
+ ]),
298
+ methodLabel,
299
+ errors,
300
+ )
301
+ if (typeof method.name !== "string" || !/^[A-Za-z0-9_.-]+$/.test(method.name)) {
302
+ errors.push(`${methodLabel}.name must be a broker method name`)
303
+ } else if (seen.has(method.name)) {
304
+ errors.push(`duplicate provided method name: ${method.name}`)
305
+ }
306
+ seen.add(method.name)
307
+ for (const key of ["scope", "label", "description", "input_schema", "input", "output_schema", "output", "route_path"]) {
308
+ requireString(method, key, methodLabel, errors)
309
+ }
310
+ if (method.route_method !== undefined && !methods.has(String(method.route_method).toUpperCase())) {
311
+ errors.push(`${methodLabel}.route_method must be one of ${Array.from(methods).join(", ")}`)
312
+ }
313
+ if (method.route_path !== undefined && (typeof method.route_path !== "string" || !method.route_path.startsWith("/"))) {
314
+ errors.push(`${methodLabel}.route_path must start with '/'`)
315
+ }
316
+ })
317
+ }
318
+
265
319
  function validateProvides(value, errors) {
266
320
  if (value === undefined) return
267
321
  if (!isObject(value)) {
268
322
  errors.push("provides must be an object")
269
323
  return
270
324
  }
271
- unknownKeys(value, new Set(["services", "events"]), "provides", errors)
325
+ unknownKeys(value, new Set(["namespace", "services", "events"]), "provides", errors)
326
+ requireString(value, "namespace", "provides", errors)
272
327
  if (value.services !== undefined) {
273
328
  if (!Array.isArray(value.services)) {
274
329
  errors.push("provides.services must be an array")
@@ -280,7 +335,7 @@ function validateProvides(value, errors) {
280
335
  errors.push(`${label} must be an object`)
281
336
  return
282
337
  }
283
- unknownKeys(service, new Set(["id", "version", "label", "description", "permissions", "routes"]), label, errors)
338
+ unknownKeys(service, new Set(["id", "version", "label", "description", "permissions", "routes", "methods"]), label, errors)
284
339
  if (!isCapabilityId(service.id)) {
285
340
  errors.push(`${label}.id must be a dotted lowercase capability id`)
286
341
  } else if (seen.has(service.id)) {
@@ -302,6 +357,7 @@ function validateProvides(value, errors) {
302
357
  }
303
358
  }
304
359
  validateServiceRoutes(service.routes, label, errors)
360
+ validateServiceMethods(service.methods, label, errors)
305
361
  })
306
362
  }
307
363
  }
@@ -309,9 +365,29 @@ function validateProvides(value, errors) {
309
365
  if (!Array.isArray(value.events)) {
310
366
  errors.push("provides.events must be an array")
311
367
  } else {
312
- for (const event of value.events) {
313
- if (!isCapabilityId(event)) errors.push(`provides.events entries must be dotted lowercase topics: ${event}`)
314
- }
368
+ const seen = new Set()
369
+ value.events.forEach((event, i) => {
370
+ const label = `provides.events[${i}]`
371
+ let topic
372
+ if (typeof event === "string") {
373
+ topic = event
374
+ } else if (isObject(event)) {
375
+ unknownKeys(event, new Set(["topic", "name", "version", "payload_schema", "schema", "description"]), label, errors)
376
+ topic = event.topic || event.name
377
+ for (const key of ["version", "payload_schema", "schema", "description"]) {
378
+ requireString(event, key, label, errors)
379
+ }
380
+ } else {
381
+ errors.push(`${label} must be a topic string or object`)
382
+ return
383
+ }
384
+ if (!isCapabilityId(topic)) {
385
+ errors.push(`${label}.topic must be a dotted lowercase topic`)
386
+ } else if (seen.has(topic)) {
387
+ errors.push(`duplicate provided event topic: ${topic}`)
388
+ }
389
+ seen.add(topic)
390
+ })
315
391
  }
316
392
  }
317
393
  }
@@ -375,6 +451,44 @@ function validateRequires(value, errors) {
375
451
  }
376
452
  }
377
453
 
454
+ function validateConsumes(value, errors) {
455
+ if (value === undefined) return
456
+ if (!isObject(value)) {
457
+ errors.push("consumes must be an object")
458
+ return
459
+ }
460
+ unknownKeys(value, new Set(["services", "events"]), "consumes", errors)
461
+ for (const bucket of ["services", "events"]) {
462
+ if (value[bucket] === undefined) continue
463
+ if (!Array.isArray(value[bucket])) {
464
+ errors.push(`consumes.${bucket} must be an array`)
465
+ continue
466
+ }
467
+ const seen = new Set()
468
+ value[bucket].forEach((entry, i) => {
469
+ const label = `consumes.${bucket}[${i}]`
470
+ let target
471
+ if (typeof entry === "string") {
472
+ target = entry
473
+ } else if (isObject(entry)) {
474
+ unknownKeys(entry, new Set(["target", "required", "reason"]), label, errors)
475
+ target = entry.target
476
+ requireBoolean(entry, "required", label, errors)
477
+ requireString(entry, "reason", label, errors)
478
+ } else {
479
+ errors.push(`${label} must be a broker target string or object`)
480
+ return
481
+ }
482
+ if (!isBrokerTarget(target)) {
483
+ errors.push(`${label}.target must look like namespace/version#name`)
484
+ return
485
+ }
486
+ if (seen.has(target)) errors.push(`duplicate consumed ${bucket.slice(0, -1)} target: ${target}`)
487
+ seen.add(target)
488
+ })
489
+ }
490
+ }
491
+
378
492
  function validateManifest(m) {
379
493
  const errors = []
380
494
  if (!isObject(m)) return ["manifest must be an object"]
@@ -445,6 +559,7 @@ function validateManifest(m) {
445
559
  validatePlatformServices(m.platform_services, errors)
446
560
  validateProvides(m.provides, errors)
447
561
  validateRequires(m.requires, errors)
562
+ validateConsumes(m.consumes, errors)
448
563
 
449
564
  if (m.sdk) {
450
565
  if (!isObject(m.sdk)) errors.push("sdk must be an object")
package/lib/secrets.js CHANGED
@@ -6,6 +6,22 @@ const path = require("path")
6
6
  const LOCAL_ENV_PATH = path.join(".palette", ".env.local")
7
7
  const EXAMPLE_ENV_PATH = path.join(".palette", ".env.example")
8
8
  const SECRET_SCOPES = new Set(["dev", "plugin", "install", "platform"])
9
+ const RESERVED_AUTO_ENV_KEYS = new Set([
10
+ "CI",
11
+ "HOME",
12
+ "HOST",
13
+ "HOSTNAME",
14
+ "LOGNAME",
15
+ "NODE_ENV",
16
+ "OLDPWD",
17
+ "PATH",
18
+ "PORT",
19
+ "PWD",
20
+ "SHELL",
21
+ "TERM",
22
+ "TMPDIR",
23
+ "USER",
24
+ ])
9
25
 
10
26
  function parseDotEnv(src) {
11
27
  const values = {}
@@ -31,6 +47,50 @@ function readDotEnvFile(filePath) {
31
47
  return parseDotEnv(fs.readFileSync(filePath, "utf8"))
32
48
  }
33
49
 
50
+ function unique(items) {
51
+ return Array.from(new Set(items.filter(Boolean)))
52
+ }
53
+
54
+ function envFileNames(environment) {
55
+ return unique([
56
+ ".env",
57
+ ".env.local",
58
+ environment ? `.env.${environment}` : null,
59
+ environment ? `.env.${environment}.local` : null,
60
+ ])
61
+ }
62
+
63
+ function loadRootEnvFiles(cwd, { environment } = {}) {
64
+ const values = {}
65
+ const files = []
66
+ for (const name of envFileNames(environment || process.env.PALETTE_ENV)) {
67
+ const filePath = path.join(cwd, name)
68
+ if (!fs.existsSync(filePath)) continue
69
+ Object.assign(values, readDotEnvFile(filePath))
70
+ files.push(name)
71
+ }
72
+ return { values, files }
73
+ }
74
+
75
+ function loadLocalEnvDetails(cwd, { apply = true, environment, includePalette = true } = {}) {
76
+ const root = loadRootEnvFiles(cwd, { environment })
77
+ const values = { ...root.values }
78
+ const files = [...root.files]
79
+ if (includePalette) {
80
+ const palettePath = path.join(cwd, LOCAL_ENV_PATH)
81
+ if (fs.existsSync(palettePath)) {
82
+ Object.assign(values, readDotEnvFile(palettePath))
83
+ files.push(LOCAL_ENV_PATH)
84
+ }
85
+ }
86
+ if (apply) {
87
+ for (const [key, value] of Object.entries(values)) {
88
+ if (process.env[key] === undefined) process.env[key] = value
89
+ }
90
+ }
91
+ return { values, files }
92
+ }
93
+
34
94
  function formatDotEnvValue(value) {
35
95
  if (value === undefined || value === null) return ""
36
96
  const str = String(value)
@@ -132,14 +192,25 @@ function initLocalEnv(cwd, manifest, { overwrite = false } = {}) {
132
192
  return { localPath, examplePath, declared }
133
193
  }
134
194
 
135
- function loadLocalEnv(cwd, { apply = true } = {}) {
136
- const values = readDotEnvFile(path.join(cwd, LOCAL_ENV_PATH))
137
- if (apply) {
138
- for (const [key, value] of Object.entries(values)) {
139
- if (process.env[key] === undefined) process.env[key] = value
140
- }
141
- }
142
- return values
195
+ function loadLocalEnv(cwd, { apply = true, environment, includePalette = true } = {}) {
196
+ return loadLocalEnvDetails(cwd, { apply, environment, includePalette }).values
197
+ }
198
+
199
+ function isPublicEnvKey(key) {
200
+ return key.startsWith("NEXT_PUBLIC_")
201
+ }
202
+
203
+ function isReservedAutoEnvKey(key) {
204
+ return (
205
+ RESERVED_AUTO_ENV_KEYS.has(key) ||
206
+ key.startsWith("PALETTE_") ||
207
+ key.startsWith("npm_") ||
208
+ key.startsWith("NPM_")
209
+ )
210
+ }
211
+
212
+ function canAutoUploadEnvKey(key) {
213
+ return /^[A-Z_][A-Z0-9_]*$/.test(key) && !isPublicEnvKey(key) && !isReservedAutoEnvKey(key)
143
214
  }
144
215
 
145
216
  function redactValue(value) {
@@ -156,9 +227,13 @@ module.exports = {
156
227
  declaredSecrets,
157
228
  ensureGitignore,
158
229
  initLocalEnv,
230
+ loadLocalEnvDetails,
159
231
  loadLocalEnv,
160
232
  parseDotEnv,
161
233
  readDotEnvFile,
162
234
  redactValue,
163
235
  secretsForScope,
236
+ canAutoUploadEnvKey,
237
+ isPublicEnvKey,
238
+ isReservedAutoEnvKey,
164
239
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palettelab/cli",
3
- "version": "0.3.48",
3
+ "version": "0.3.50",
4
4
  "description": "Developer CLI for building Palette platform plugins — no platform source access required.",
5
5
  "bin": {
6
6
  "pltt": "bin/pltt.js"
@@ -23,7 +23,9 @@
23
23
  "node": ">=18"
24
24
  },
25
25
  "dependencies": {
26
- "esbuild": "^0.24.0"
26
+ "esbuild": "^0.24.0",
27
+ "postcss": "^8.5.15",
28
+ "postcss-selector-parser": "^7.1.1"
27
29
  },
28
30
  "publishConfig": {
29
31
  "registry": "https://registry.npmjs.org"