@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 +8 -5
- package/lib/commands/dev.js +35 -2
- package/lib/commands/publish.js +16 -7
- package/lib/dev-simulator.js +33 -1
- package/lib/manifest.js +3 -3
- package/lib/secrets.js +4 -2
- package/package.json +1 -1
- package/platform-dev/docker-compose.yml +4 -2
- package/template-fallback/package.json +1 -1
- package/template-fallback/templates/consumer-app/package.json +1 -1
- package/template-fallback/templates/dashboard/package.json +1 -1
- package/template-fallback/templates/database/package.json +1 -1
- package/template-fallback/templates/external-service/package.json +1 -1
- package/template-fallback/templates/frontend-only/package.json +1 -1
- package/template-fallback/templates/next/package.json +1 -1
- package/template-fallback/templates/palette-app/package.json +1 -1
- package/template-fallback/templates/provider-app/package.json +1 -1
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
|
|
506
|
-
`language`, `fallbackLanguage`, `supportedLanguages`,
|
|
507
|
-
Generated frontend templates include
|
|
508
|
-
files wired through
|
|
509
|
-
|
|
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
|
package/lib/commands/dev.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
}
|
package/lib/commands/publish.js
CHANGED
|
@@ -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
|
-
|
|
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)) {
|
package/lib/dev-simulator.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
131
|
-
errors.push(`${label} must be
|
|
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 (
|
|
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
|
|
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
|
@@ -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:
|