@palettelab/cli 0.3.53 → 0.3.54

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 CHANGED
@@ -502,11 +502,14 @@ work without Docker or platform source. The terminal streams local frontend
502
502
  requests, frontend rebuilds, and backend process output while `pltt dev` is
503
503
  running.
504
504
 
505
- The simulator also provides the same language fields that Palette OS provides:
506
- `language`, `fallbackLanguage`, `supportedLanguages`, and `setLanguage()`.
507
- Generated frontend templates include app-owned `frontend/src/translations.ts`
508
- files wired through `usePluginTranslations()`, so apps can switch when the OS
509
- language changes without storing copy in the platform.
505
+ The simulator also provides the same OS context fields that Palette OS provides:
506
+ `language`, `fallbackLanguage`, `supportedLanguages`, `setLanguage()`,
507
+ `colorMode`, and `setColorMode()`. Generated frontend templates include
508
+ app-owned `frontend/src/translations.ts` files wired through
509
+ `usePluginTranslations()`, so apps can switch when the OS language changes
510
+ without storing copy in the platform. Apps can read `usePlatform().colorMode`
511
+ or listen for the `palette:theme-change` browser event to react when Palette OS
512
+ changes between light and dark mode.
510
513
 
511
514
  For Python apps with database tables, `ctx.db` is an async SQLAlchemy session in
512
515
  local dev. The simulator imports `backend/api/models.py` when present and creates
@@ -8,7 +8,7 @@ const { watchFrontend } = require("../bundler")
8
8
  const { parseFlags, resolveEnvironment } = require("../environments")
9
9
  const { resolveDevPorts } = require("../ports")
10
10
  const { startSimulator } = require("../dev-simulator")
11
- const { loadLocalEnv } = require("../secrets")
11
+ const { loadLocalEnvDetails } = require("../secrets")
12
12
  const buildCommand = require("./build")
13
13
  const publish = require("./publish")
14
14
  const logs = require("./logs")
@@ -98,11 +98,39 @@ function lintMigrationsForDev(cwd, manifest) {
98
98
  process.exit(1)
99
99
  }
100
100
 
101
+ function formatEnvValue(value) {
102
+ if (value === undefined || value === null) return ""
103
+ const str = String(value)
104
+ if (!str) return ""
105
+ if (/[\s#"'\\]/.test(str)) return JSON.stringify(str)
106
+ return str
107
+ }
108
+
109
+ function writePlatformEnvFile(cwd, localEnv) {
110
+ const dir = path.join(cwd, ".palette")
111
+ fs.mkdirSync(dir, { recursive: true })
112
+ const envPath = path.join(dir, "platform.env")
113
+ const values = {}
114
+ for (const [key, value] of Object.entries(localEnv?.values || {})) {
115
+ values[key] = process.env[key] !== undefined ? process.env[key] : value
116
+ }
117
+ const lines = [
118
+ "# Generated by pltt dev --platform from local env files.",
119
+ "# Do not commit this file.",
120
+ "",
121
+ ]
122
+ for (const [key, value] of Object.entries(values).sort(([a], [b]) => a.localeCompare(b))) {
123
+ lines.push(`${key}=${formatEnvValue(value)}`)
124
+ }
125
+ fs.writeFileSync(envPath, `${lines.join("\n")}\n`)
126
+ return envPath
127
+ }
128
+
101
129
  async function run(args, { cwd }) {
102
130
  const { flags, rest } = parseFlags(args)
103
131
  const cloud = rest.includes("--cloud") || rest.includes("--sandbox")
104
132
  const platform = rest.includes("--platform")
105
- loadLocalEnv(cwd)
133
+ const localEnv = loadLocalEnvDetails(cwd, { environment: flags.env })
106
134
  if (cloud) {
107
135
  const json = args.includes("--json")
108
136
  const publishArgs = ["--publish-type", "preview"]
@@ -199,6 +227,9 @@ async function run(args, { cwd }) {
199
227
  }
200
228
  console.log(`[pltt] frontend: http://localhost:${frontendPort}/apps/${pluginId}`)
201
229
  console.log(`[pltt] backend: http://localhost:${backendPort}/api/v1/plugins/${pluginId}`)
230
+ if (localEnv.files.length) {
231
+ console.log(`[pltt] env files: ${localEnv.files.join(", ")}`)
232
+ }
202
233
 
203
234
  // Pre-pull so we can give a useful error if the image isn't reachable
204
235
  // (common cause: maintainer hasn't pushed it yet, or `docker login ghcr.io`
@@ -210,11 +241,13 @@ async function run(args, { cwd }) {
210
241
  process.exit(1)
211
242
  }
212
243
 
244
+ const platformEnvFile = writePlatformEnvFile(cwd, localEnv)
213
245
  const env = {
214
246
  ...process.env,
215
247
  PALETTE_DEV_IMAGE: DEFAULT_IMAGE,
216
248
  PALETTE_ACTIVE_PLUGIN: pluginId,
217
249
  PALETTE_PLUGIN_DIR: cwd,
250
+ PALETTE_PLUGIN_ENV_FILE: platformEnvFile,
218
251
  PALETTE_FRONTEND_PORT: frontendPort,
219
252
  PALETTE_BACKEND_PORT: backendPort,
220
253
  }
@@ -228,6 +228,14 @@ function scopesOf(spec) {
228
228
  return ["dev"]
229
229
  }
230
230
 
231
+ function withPluginScope(spec) {
232
+ const scopes = Array.from(new Set([...scopesOf(spec), "plugin"]))
233
+ return {
234
+ ...spec,
235
+ scope: scopes.length === 1 ? scopes[0] : scopes,
236
+ }
237
+ }
238
+
231
239
  function cloneManifest(manifest) {
232
240
  return JSON.parse(JSON.stringify(manifest))
233
241
  }
@@ -235,12 +243,6 @@ function cloneManifest(manifest) {
235
243
  function collectPluginSecrets(cwd, manifest, env, flags, log, localEnv) {
236
244
  const declared = declaredSecrets(manifest)
237
245
  const pluginSecrets = Object.entries(declared).filter(([, spec]) => spec.scope.includes("plugin"))
238
- const devRequired = Object.entries(declared).filter(([, spec]) => spec.scope.includes("dev") && spec.required)
239
- if (devRequired.length) {
240
- log(
241
- `[pltt] dev-only secrets are not uploaded: ${devRequired.map(([name]) => name).join(", ")}`,
242
- )
243
- }
244
246
 
245
247
  let fileValues = {}
246
248
  if (flags.secretsFile) {
@@ -281,7 +283,14 @@ function collectPluginSecrets(cwd, manifest, env, flags, log, localEnv) {
281
283
  }
282
284
  const explicitSpec = effectiveManifest.secrets[name]
283
285
  if (explicitSpec) {
284
- if (scopesOf(explicitSpec).includes("plugin")) values[name] = fileValues[name] ?? process.env[name] ?? localValues[name]
286
+ const value = fileValues[name] ?? process.env[name] ?? localValues[name]
287
+ if (scopesOf(explicitSpec).includes("plugin")) {
288
+ values[name] = value
289
+ } else if (canAutoUploadEnvKey(name)) {
290
+ effectiveManifest.secrets[name] = withPluginScope(explicitSpec)
291
+ values[name] = value
292
+ autoUploaded.push(name)
293
+ }
285
294
  continue
286
295
  }
287
296
  if (isReservedAutoEnvKey(name)) {
@@ -576,6 +576,21 @@ function normalizePaletteLanguage(language, fallback = "en") {
576
576
  return value ? value.split("-")[0] : fallback
577
577
  }
578
578
 
579
+ function normalizePaletteColorMode(mode, fallback = "light") {
580
+ return mode === "dark" ? "dark" : fallback
581
+ }
582
+
583
+ function detectPaletteColorMode() {
584
+ return window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ? "dark" : "light"
585
+ }
586
+
587
+ function applyPaletteColorMode(mode) {
588
+ const normalized = normalizePaletteColorMode(mode)
589
+ document.documentElement.classList.toggle("dark", normalized === "dark")
590
+ document.documentElement.style.colorScheme = normalized
591
+ window.dispatchEvent(new CustomEvent("palette:theme-change", { detail: { colorMode: normalized } }))
592
+ }
593
+
579
594
  function Toasts() {
580
595
  const [items, setItems] = React.useState([])
581
596
  React.useEffect(() => {
@@ -594,6 +609,7 @@ function Toasts() {
594
609
 
595
610
  function Shell() {
596
611
  const [language, updateLanguage] = React.useState(() => normalizePaletteLanguage(navigator.language))
612
+ const [colorMode, updateColorMode] = React.useState(() => detectPaletteColorMode())
597
613
  const platform = React.useMemo(() => ({
598
614
  ...basePlatform,
599
615
  language,
@@ -605,18 +621,34 @@ function Shell() {
605
621
  document.documentElement.lang = normalized
606
622
  window.dispatchEvent(new CustomEvent("palette:language-change", { detail: { language: normalized } }))
607
623
  },
608
- }), [language])
624
+ colorMode,
625
+ setColorMode: (nextMode) => {
626
+ updateColorMode(normalizePaletteColorMode(nextMode))
627
+ },
628
+ }), [language, colorMode])
609
629
 
610
630
  React.useEffect(() => {
611
631
  document.documentElement.lang = language
612
632
  }, [language])
613
633
 
634
+ React.useEffect(() => {
635
+ applyPaletteColorMode(colorMode)
636
+ }, [colorMode])
637
+
614
638
  React.useEffect(() => {
615
639
  const onLanguageChange = () => updateLanguage(normalizePaletteLanguage(navigator.language))
616
640
  window.addEventListener("languagechange", onLanguageChange)
617
641
  return () => window.removeEventListener("languagechange", onLanguageChange)
618
642
  }, [])
619
643
 
644
+ React.useEffect(() => {
645
+ const media = window.matchMedia?.("(prefers-color-scheme: dark)")
646
+ if (!media) return
647
+ const onColorModeChange = () => updateColorMode(detectPaletteColorMode())
648
+ media.addEventListener?.("change", onColorModeChange)
649
+ return () => media.removeEventListener?.("change", onColorModeChange)
650
+ }, [])
651
+
620
652
  return React.createElement(PluginProvider, { value: platform },
621
653
  React.createElement("main", { className: "palette-local-shell" },
622
654
  React.createElement(Plugin, { platform }),
package/lib/manifest.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const fs = require("fs")
4
4
  const path = require("path")
5
- const { SECRET_SCOPES } = require("./secrets")
5
+ const { ENV_KEY_PATTERN, SECRET_SCOPES } = require("./secrets")
6
6
 
7
7
  const MANIFEST_FILE = "palette-plugin.json"
8
8
  const SUPPORTED_MANIFEST_VERSIONS = ["1"]
@@ -127,8 +127,8 @@ function validateSecrets(value, errors) {
127
127
  const allowed = new Set(["scope", "required", "label", "help", "validate"])
128
128
  for (const [name, spec] of Object.entries(value)) {
129
129
  const label = `secrets.${name}`
130
- if (!/^[A-Z_][A-Z0-9_]*$/.test(name)) {
131
- errors.push(`${label} must be an uppercase environment-style name`)
130
+ if (!ENV_KEY_PATTERN.test(name)) {
131
+ errors.push(`${label} must be a valid environment variable name`)
132
132
  }
133
133
  if (!isObject(spec)) {
134
134
  errors.push(`${label} must be an object`)
package/lib/secrets.js CHANGED
@@ -6,6 +6,7 @@ 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 ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
9
10
  const RESERVED_AUTO_ENV_KEYS = new Set([
10
11
  "CI",
11
12
  "HOME",
@@ -162,7 +163,7 @@ function declaredSecrets(manifest) {
162
163
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}
163
164
  const out = {}
164
165
  for (const [name, meta] of Object.entries(raw)) {
165
- if (!/^[A-Z_][A-Z0-9_]*$/.test(name)) continue
166
+ if (!ENV_KEY_PATTERN.test(name)) continue
166
167
  const item = meta && typeof meta === "object" && !Array.isArray(meta) ? meta : {}
167
168
  out[name] = {
168
169
  ...item,
@@ -210,7 +211,7 @@ function isReservedAutoEnvKey(key) {
210
211
  }
211
212
 
212
213
  function canAutoUploadEnvKey(key) {
213
- return /^[A-Z_][A-Z0-9_]*$/.test(key) && !isPublicEnvKey(key) && !isReservedAutoEnvKey(key)
214
+ return ENV_KEY_PATTERN.test(key) && !isPublicEnvKey(key) && !isReservedAutoEnvKey(key)
214
215
  }
215
216
 
216
217
  function redactValue(value) {
@@ -222,6 +223,7 @@ function redactValue(value) {
222
223
 
223
224
  module.exports = {
224
225
  EXAMPLE_ENV_PATH,
226
+ ENV_KEY_PATTERN,
225
227
  LOCAL_ENV_PATH,
226
228
  SECRET_SCOPES,
227
229
  declaredSecrets,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palettelab/cli",
3
- "version": "0.3.53",
3
+ "version": "0.3.54",
4
4
  "description": "Developer CLI for building Palette platform plugins — no platform source access required.",
5
5
  "bin": {
6
6
  "pltt": "bin/pltt.js"
@@ -9,6 +9,8 @@
9
9
  services:
10
10
  platform:
11
11
  image: ${PALETTE_DEV_IMAGE:-ghcr.io/palette-lab/platform-dev:latest}
12
+ env_file:
13
+ - ${PALETTE_PLUGIN_ENV_FILE:-/dev/null}
12
14
  ports:
13
15
  - "${PALETTE_FRONTEND_PORT:-7321}:3000"
14
16
  - "${PALETTE_BACKEND_PORT:-8732}:8000"
@@ -25,8 +27,8 @@ services:
25
27
  STORAGE_BACKEND: "local"
26
28
  LOCAL_STORAGE_DIR: "/srv/storage"
27
29
  # Disable optional features that need real credentials
28
- RAG_ENABLED: "false"
29
- OPENAI_API_KEY: ""
30
+ RAG_ENABLED: "${RAG_ENABLED:-false}"
31
+ OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
30
32
  volumes:
31
33
  - "${PALETTE_PLUGIN_DIR}:/plugins/${PALETTE_ACTIVE_PLUGIN}"
32
34
  depends_on:
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "description": "A Palette platform plugin",
6
6
  "dependencies": {
7
- "@palettelab/sdk": "^0.1.23"
7
+ "@palettelab/sdk": "^0.1.24"
8
8
  },
9
9
  "devDependencies": {
10
10
  "typescript": "^5.0.0",
@@ -2,7 +2,7 @@
2
2
  "private": true,
3
3
  "type": "module",
4
4
  "dependencies": {
5
- "@palettelab/sdk": "^0.1.23",
5
+ "@palettelab/sdk": "^0.1.24",
6
6
  "react": "^19.0.0",
7
7
  "react-dom": "^19.0.0"
8
8
  }
@@ -3,7 +3,7 @@
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "dependencies": {
6
- "@palettelab/sdk": "^0.1.23",
6
+ "@palettelab/sdk": "^0.1.24",
7
7
  "react": "^19.0.0"
8
8
  }
9
9
  }
@@ -2,5 +2,5 @@
2
2
  "name": "my-db-plugin",
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
- "dependencies": { "@palettelab/sdk": "^0.1.23", "react": "^19.0.0" }
5
+ "dependencies": { "@palettelab/sdk": "^0.1.24", "react": "^19.0.0" }
6
6
  }
@@ -2,5 +2,5 @@
2
2
  "name": "my-external-svc",
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
- "dependencies": { "@palettelab/sdk": "^0.1.23", "react": "^19.0.0" }
5
+ "dependencies": { "@palettelab/sdk": "^0.1.24", "react": "^19.0.0" }
6
6
  }
@@ -3,7 +3,7 @@
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "dependencies": {
6
- "@palettelab/sdk": "^0.1.23",
6
+ "@palettelab/sdk": "^0.1.24",
7
7
  "react": "^19.0.0"
8
8
  }
9
9
  }
@@ -3,7 +3,7 @@
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "dependencies": {
6
- "@palettelab/sdk": "^0.1.23",
6
+ "@palettelab/sdk": "^0.1.24",
7
7
  "react": "^19.0.0"
8
8
  },
9
9
  "devDependencies": {
@@ -3,7 +3,7 @@
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "dependencies": {
6
- "@palettelab/sdk": "^0.1.23",
6
+ "@palettelab/sdk": "^0.1.24",
7
7
  "react": "^19.0.0"
8
8
  },
9
9
  "devDependencies": {
@@ -2,7 +2,7 @@
2
2
  "private": true,
3
3
  "type": "module",
4
4
  "dependencies": {
5
- "@palettelab/sdk": "^0.1.23",
5
+ "@palettelab/sdk": "^0.1.24",
6
6
  "react": "^19.0.0"
7
7
  }
8
8
  }