@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.
package/lib/bundler.js CHANGED
@@ -4,6 +4,7 @@ const path = require("path")
4
4
  const fs = require("fs")
5
5
  const os = require("os")
6
6
  const { generatePaletteAppEntry } = require("./app-router")
7
+ const { appendCssExport, scopePluginCss } = require("./css-scope")
7
8
 
8
9
  const NEXT_CONFIG_NAMES = [
9
10
  "frontend/next.config.ts",
@@ -382,6 +383,43 @@ function mergePlugins(...pluginGroups) {
382
383
  return pluginGroups.flat().filter(Boolean)
383
384
  }
384
385
 
386
+ function pluginCssScopeId(frontend = {}) {
387
+ return frontend.pluginId || frontend.id || "plugin"
388
+ }
389
+
390
+ function cssOutputForJs(outfile) {
391
+ const ext = path.extname(outfile)
392
+ return `${outfile.slice(0, outfile.length - ext.length)}.css`
393
+ }
394
+
395
+ function outputPathFromMetafile(result, suffix) {
396
+ const outputs = Object.keys(result.metafile?.outputs || {})
397
+ return outputs.find((output) => output.endsWith(suffix)) || null
398
+ }
399
+
400
+ function writeScopedCssExport({ jsPath, cssPath, pluginId, deleteCss = true }) {
401
+ if (!cssPath || !fs.existsSync(cssPath)) return
402
+ const css = fs.readFileSync(cssPath, "utf8")
403
+ const scopedCss = scopePluginCss(css, pluginId)
404
+ if (scopedCss.trim()) {
405
+ const js = fs.readFileSync(jsPath, "utf8")
406
+ fs.writeFileSync(jsPath, appendCssExport(js, scopedCss))
407
+ }
408
+ if (deleteCss) fs.rmSync(cssPath, { force: true })
409
+ }
410
+
411
+ function bundleOutputsToFrontendBuffer(result, pluginId) {
412
+ const outputs = result.outputFiles || []
413
+ const jsOutput = outputs.find((file) => /\.(mjs|js)$/.test(file.path))
414
+ if (!jsOutput) throw new Error("esbuild produced no JavaScript output")
415
+ const css = outputs
416
+ .filter((file) => file.path.endsWith(".css"))
417
+ .map((file) => file.text)
418
+ .join("\n")
419
+ const js = jsOutput.text
420
+ return Buffer.from(appendCssExport(js, scopePluginCss(css, pluginId)))
421
+ }
422
+
385
423
  /**
386
424
  * Bundle the plugin's frontend entry into a single ESM file.
387
425
  *
@@ -393,8 +431,8 @@ function mergePlugins(...pluginGroups) {
393
431
  async function bundleFrontend(pluginDir, entry, frontend = {}) {
394
432
  pluginDir = path.resolve(pluginDir)
395
433
  const esbuild = loadEsbuild()
396
- const tmp = isPaletteApp(frontend) ? fs.mkdtempSync(path.join(os.tmpdir(), "palette-app-entry-")) : null
397
- const bundleEntry = tmp
434
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "palette-frontend-"))
435
+ const bundleEntry = isPaletteApp(frontend)
398
436
  ? generatePaletteAppEntry(pluginDir, entry || "./frontend/app", path.join(tmp, "entry.tsx"))
399
437
  : entry
400
438
  const absEntry = path.resolve(pluginDir, bundleEntry)
@@ -407,6 +445,7 @@ async function bundleFrontend(pluginDir, entry, frontend = {}) {
407
445
  const result = await esbuild.build({
408
446
  entryPoints: [absEntry],
409
447
  bundle: true,
448
+ outfile: path.join(tmp, "frontend.mjs"),
410
449
  format: "esm",
411
450
  platform: "browser",
412
451
  target: ["es2022"],
@@ -432,9 +471,9 @@ async function bundleFrontend(pluginDir, entry, frontend = {}) {
432
471
  if (!result.outputFiles || result.outputFiles.length === 0) {
433
472
  throw new Error("esbuild produced no output")
434
473
  }
435
- return Buffer.from(result.outputFiles[0].contents)
474
+ return bundleOutputsToFrontendBuffer(result, pluginCssScopeId(frontend))
436
475
  } finally {
437
- if (tmp) fs.rmSync(tmp, { recursive: true, force: true })
476
+ fs.rmSync(tmp, { recursive: true, force: true })
438
477
  }
439
478
  }
440
479
 
@@ -473,6 +512,7 @@ async function watchFrontend(pluginDir, entry, outfile, frontend = {}) {
473
512
  ],
474
513
  minify: false,
475
514
  sourcemap: "inline",
515
+ metafile: true,
476
516
  logLevel: "silent",
477
517
  absWorkingDir: pluginDir,
478
518
  plugins: [
@@ -488,6 +528,12 @@ async function watchFrontend(pluginDir, entry, outfile, frontend = {}) {
488
528
  }
489
529
  return
490
530
  }
531
+ const cssPath = outputPathFromMetafile(result, ".css") || cssOutputForJs(outfile)
532
+ writeScopedCssExport({
533
+ jsPath: outfile,
534
+ cssPath,
535
+ pluginId: pluginCssScopeId(frontend),
536
+ })
491
537
  const size = fs.existsSync(outfile) ? fs.statSync(outfile).size : 0
492
538
  console.log(`[pltt] frontend bundle ready (${size} bytes)`)
493
539
  })
@@ -509,7 +555,7 @@ async function watchFrontend(pluginDir, entry, outfile, frontend = {}) {
509
555
  * ./backend/...
510
556
  * ./palette-plugin.json
511
557
  */
512
- async function bundleBackend(pluginDir) {
558
+ async function bundleBackend(pluginDir, options = {}) {
513
559
  pluginDir = path.resolve(pluginDir)
514
560
  const { spawnSync } = require("child_process")
515
561
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "palette-bundle-"))
@@ -531,9 +577,16 @@ async function bundleBackend(pluginDir) {
531
577
  const backendDir = path.join(pluginDir, "backend")
532
578
  if (fs.existsSync(backendDir)) copy(backendDir, path.join(stage, "backend"))
533
579
  for (const metadataFile of ["package.json", "pyproject.toml", "palette-plugin.json"]) {
580
+ if (metadataFile === "palette-plugin.json" && options.manifest) continue
534
581
  const src = path.join(pluginDir, metadataFile)
535
582
  if (fs.existsSync(src)) fs.copyFileSync(src, path.join(stage, metadataFile))
536
583
  }
584
+ if (options.manifest) {
585
+ fs.writeFileSync(
586
+ path.join(stage, "palette-plugin.json"),
587
+ JSON.stringify(options.manifest, null, 2),
588
+ )
589
+ }
537
590
 
538
591
  const sdkDir = path.resolve(__dirname, "..", "backend-sdk", "palette_sdk")
539
592
  const targetSdkDir = path.join(stage, "backend", "palette_sdk")
package/lib/cli.js CHANGED
@@ -11,6 +11,7 @@ const pkg = require("./commands/package")
11
11
  const status = require("./commands/status")
12
12
  const logs = require("./commands/logs")
13
13
  const secrets = require("./commands/secrets")
14
+ const services = require("./commands/services")
14
15
 
15
16
  const COMMANDS = {
16
17
  init: { run: init, help: "Scaffold a new plugin directory from the template" },
@@ -50,6 +51,10 @@ const COMMANDS = {
50
51
  run: secrets,
51
52
  help: "Initialize local env files and manage plugin-scope secrets",
52
53
  },
54
+ services: {
55
+ run: services,
56
+ help: "Inspect / pull / scaffold OS-broker services (list, pull, scaffold)",
57
+ },
53
58
  }
54
59
 
55
60
  function printHelp() {
@@ -77,6 +82,11 @@ function printHelp() {
77
82
  console.log("\nLogs flags:")
78
83
  console.log(" --tail <n> Tail last n events (default 50)")
79
84
  console.log(" -f, --follow Stream events (poll every 3s)")
85
+ console.log("\nServices (OS broker):")
86
+ console.log(" pltt services list # show provided services/events in the org")
87
+ console.log(" pltt services add TARGET # add a consumed service/event target")
88
+ console.log(" pltt services pull # generate typed TS+Python clients from consumes")
89
+ console.log(" pltt services scaffold NAME # add a provider method (schemas + handler stub)")
80
90
  console.log("\nSecrets:")
81
91
  console.log(" pltt secrets init")
82
92
  console.log(" pltt secrets set NAME --value <secret> --env staging")
@@ -172,7 +172,10 @@ async function run(args, { cwd }) {
172
172
  if (manifest.frontend?.entry) {
173
173
  console.log(`[pltt] bundling frontend ${frontendEntry} → .palette/dist/frontend.mjs`)
174
174
  try {
175
- frontendWatcher = await watchFrontend(cwd, frontendEntry, frontendBundle, manifest.frontend)
175
+ frontendWatcher = await watchFrontend(cwd, frontendEntry, frontendBundle, {
176
+ ...manifest.frontend,
177
+ pluginId,
178
+ })
176
179
  } catch (err) {
177
180
  console.error(
178
181
  `[pltt] could not start frontend bundler: ${
@@ -6,7 +6,7 @@ const { spawnSync } = require("child_process")
6
6
  const { loadManifest, validateManifest } = require("../manifest")
7
7
  const { bundleFrontend } = require("../bundler")
8
8
  const { DEFAULT_BACKEND_DEV_PORT, DEFAULT_FRONTEND_DEV_PORT, resolveDevPorts } = require("../ports")
9
- const { declaredSecrets, loadLocalEnv } = require("../secrets")
9
+ const { declaredSecrets, loadLocalEnvDetails } = require("../secrets")
10
10
 
11
11
  const DEFAULT_IMAGE =
12
12
  process.env.PALETTE_DEV_IMAGE || "ghcr.io/palette-lab/platform-dev:latest"
@@ -122,19 +122,20 @@ async function run(args, { cwd }) {
122
122
  }
123
123
 
124
124
  const secrets = declaredSecrets(manifest)
125
- const localSecrets = loadLocalEnv(cwd, { apply: false })
125
+ const localEnv = loadLocalEnvDetails(cwd, { apply: false })
126
+ const localSecrets = localEnv.values
126
127
  if (Object.keys(secrets).length === 0) {
127
128
  ok("no manifest secrets declared")
128
129
  } else {
129
130
  ok(`manifest declares ${Object.keys(secrets).length} secret(s)`)
130
131
  for (const [name, spec] of Object.entries(secrets)) {
131
132
  if (spec.scope.includes("dev") && !localSecrets[name]) {
132
- warn(`local dev secret missing: ${name}`, "Run pltt secrets init and fill .palette/.env.local.")
133
+ warn(`local dev secret missing: ${name}`, "Add it to .env, .env.local, or an environment-specific .env file.")
133
134
  }
134
135
  if (spec.scope.includes("plugin") && spec.required && !localSecrets[name] && !process.env[name]) {
135
136
  warn(
136
137
  `plugin secret missing locally: ${name}`,
137
- "Set an env var, pass --secrets-file to publish, or run pltt secrets set after first publish.",
138
+ "Add it to .env/.env.local, set an env var, pass --secrets-file, or run pltt secrets set.",
138
139
  )
139
140
  }
140
141
  }
@@ -142,7 +143,10 @@ async function run(args, { cwd }) {
142
143
 
143
144
  if (manifest.frontend?.entry) {
144
145
  try {
145
- const bundle = await bundleFrontend(cwd, manifest.frontend.entry, manifest.frontend)
146
+ const bundle = await bundleFrontend(cwd, manifest.frontend.entry, {
147
+ ...manifest.frontend,
148
+ pluginId: manifest.id,
149
+ })
146
150
  ok(`frontend bundles successfully (${bundle.length} bytes)`)
147
151
  } catch (err) {
148
152
  failures += fail(
@@ -43,7 +43,10 @@ async function run(argv, { cwd }) {
43
43
  fs.mkdirSync(distDir, { recursive: true })
44
44
 
45
45
  const frontend = manifest.frontend
46
- ? await bundleFrontend(cwd, manifest.frontend.entry || "./frontend/src/index.tsx", manifest.frontend)
46
+ ? await bundleFrontend(cwd, manifest.frontend.entry || "./frontend/src/index.tsx", {
47
+ ...manifest.frontend,
48
+ pluginId: manifest.id,
49
+ })
47
50
  : null
48
51
  const backend = manifest.backend ? await bundleBackend(cwd) : null
49
52
 
@@ -12,10 +12,13 @@ const {
12
12
  confirmProduction,
13
13
  } = require("../environments")
14
14
  const {
15
+ canAutoUploadEnvKey,
15
16
  declaredSecrets,
17
+ isPublicEnvKey,
18
+ isReservedAutoEnvKey,
19
+ loadLocalEnvDetails,
16
20
  loadLocalEnv,
17
21
  parseDotEnv,
18
- readDotEnvFile,
19
22
  redactValue,
20
23
  } = require("../secrets")
21
24
 
@@ -138,12 +141,12 @@ function printPreflightFailure(payload, fallbackOutput) {
138
141
  console.error("[pltt] Need machine-readable details? Run `pltt test --json`.")
139
142
  }
140
143
 
141
- function runPreflight(cwd, json, publishType = "release") {
144
+ function runPreflight(cwd, json, publishType = "release", environment) {
142
145
  const cliBin = path.resolve(__dirname, "..", "..", "bin", "pltt.js")
143
146
  const res = spawnSync(process.execPath, [cliBin, "test", "--json", "--publish-type", publishType], {
144
147
  cwd,
145
148
  encoding: "utf8",
146
- env: process.env,
149
+ env: environment ? { ...process.env, PALETTE_ENV: environment } : process.env,
147
150
  })
148
151
 
149
152
  if (res.status === 0) {
@@ -218,7 +221,18 @@ async function put(url, buf, contentType) {
218
221
  }
219
222
  }
220
223
 
221
- function collectPluginSecrets(cwd, manifest, env, flags, log) {
224
+ function scopesOf(spec) {
225
+ if (!spec || typeof spec !== "object" || Array.isArray(spec)) return ["dev"]
226
+ if (Array.isArray(spec.scope)) return spec.scope
227
+ if (typeof spec.scope === "string") return [spec.scope]
228
+ return ["dev"]
229
+ }
230
+
231
+ function cloneManifest(manifest) {
232
+ return JSON.parse(JSON.stringify(manifest))
233
+ }
234
+
235
+ function collectPluginSecrets(cwd, manifest, env, flags, log, localEnv) {
222
236
  const declared = declaredSecrets(manifest)
223
237
  const pluginSecrets = Object.entries(declared).filter(([, spec]) => spec.scope.includes("plugin"))
224
238
  const devRequired = Object.entries(declared).filter(([, spec]) => spec.scope.includes("dev") && spec.required)
@@ -232,7 +246,8 @@ function collectPluginSecrets(cwd, manifest, env, flags, log) {
232
246
  if (flags.secretsFile) {
233
247
  fileValues = parseDotEnv(fs.readFileSync(path.resolve(cwd, flags.secretsFile), "utf8"))
234
248
  }
235
- const localValues = readDotEnvFile(path.join(cwd, ".palette", ".env.local"))
249
+ const localValues = localEnv?.values || loadLocalEnv(cwd, { apply: false, environment: env.name })
250
+ const candidateValues = { ...localValues, ...fileValues }
236
251
  const values = {}
237
252
  const missing = []
238
253
  for (const [name, spec] of pluginSecrets) {
@@ -249,10 +264,61 @@ function collectPluginSecrets(cwd, manifest, env, flags, log) {
249
264
  `Set env vars, pass --secrets-file, or run pltt secrets set <NAME> --env ${env.name}.`,
250
265
  )
251
266
  }
267
+ const effectiveManifest = cloneManifest(manifest)
268
+ effectiveManifest.secrets =
269
+ effectiveManifest.secrets && typeof effectiveManifest.secrets === "object" && !Array.isArray(effectiveManifest.secrets)
270
+ ? { ...effectiveManifest.secrets }
271
+ : {}
272
+ const autoUploaded = []
273
+ const publicBundled = []
274
+ const skippedReserved = []
275
+ const skippedInvalid = []
276
+ for (const [name, value] of Object.entries(candidateValues)) {
277
+ if (!value) continue
278
+ if (isPublicEnvKey(name)) {
279
+ publicBundled.push(name)
280
+ continue
281
+ }
282
+ const explicitSpec = effectiveManifest.secrets[name]
283
+ if (explicitSpec) {
284
+ if (scopesOf(explicitSpec).includes("plugin")) values[name] = fileValues[name] ?? process.env[name] ?? localValues[name]
285
+ continue
286
+ }
287
+ if (isReservedAutoEnvKey(name)) {
288
+ skippedReserved.push(name)
289
+ continue
290
+ }
291
+ if (!canAutoUploadEnvKey(name)) {
292
+ skippedInvalid.push(name)
293
+ continue
294
+ }
295
+ effectiveManifest.secrets[name] = {
296
+ scope: "plugin",
297
+ required: false,
298
+ help: "Auto-uploaded from local .env by Palette CLI.",
299
+ }
300
+ values[name] = fileValues[name] ?? process.env[name] ?? localValues[name]
301
+ autoUploaded.push(name)
302
+ }
303
+ if (localEnv?.files?.length) {
304
+ log(`[pltt] env files: ${localEnv.files.join(", ")}`)
305
+ }
306
+ if (autoUploaded.length) {
307
+ log(`[pltt] auto plugin secrets from .env: ${autoUploaded.sort().join(", ")}`)
308
+ }
309
+ if (publicBundled.length) {
310
+ log(`[pltt] public env bundled in frontend: ${Array.from(new Set(publicBundled)).sort().join(", ")}`)
311
+ }
312
+ if (skippedReserved.length) {
313
+ log(`[pltt] skipped reserved env keys: ${Array.from(new Set(skippedReserved)).sort().join(", ")}`)
314
+ }
315
+ if (skippedInvalid.length) {
316
+ log(`[pltt] skipped invalid env keys: ${Array.from(new Set(skippedInvalid)).sort().join(", ")}`)
317
+ }
252
318
  for (const [name, value] of Object.entries(values)) {
253
319
  log(`[pltt] plugin secret ${name}=${redactValue(value)} (${env.name})`)
254
320
  }
255
- return values
321
+ return { manifest: effectiveManifest, pluginSecrets: values }
256
322
  }
257
323
 
258
324
  async function run(argv, { cwd }) {
@@ -288,7 +354,7 @@ async function run(argv, { cwd }) {
288
354
 
289
355
  const manifest = loadManifest(cwd)
290
356
  const publishType = parsePublishType(argv)
291
- loadLocalEnv(cwd)
357
+ const localEnv = loadLocalEnvDetails(cwd, { environment: env.name })
292
358
  const errors = validateManifest(manifest)
293
359
  if (errors.length) {
294
360
  console.error("[pltt] manifest invalid:")
@@ -296,7 +362,23 @@ async function run(argv, { cwd }) {
296
362
  process.exit(1)
297
363
  }
298
364
 
299
- runPreflight(cwd, flags.json, publishType)
365
+ runPreflight(cwd, flags.json, publishType, env.name)
366
+ let pluginSecrets = {}
367
+ let publishManifest = manifest
368
+ try {
369
+ const collected = collectPluginSecrets(cwd, manifest, env, flags, log, localEnv)
370
+ pluginSecrets = collected.pluginSecrets
371
+ publishManifest = collected.manifest
372
+ } catch (err) {
373
+ console.error(`[pltt] ${err instanceof Error ? err.message : String(err)}`)
374
+ process.exit(1)
375
+ }
376
+ const effectiveErrors = validateManifest(publishManifest)
377
+ if (effectiveErrors.length) {
378
+ console.error("[pltt] generated manifest invalid:")
379
+ for (const e of effectiveErrors) console.error(` - ${e}`)
380
+ process.exit(1)
381
+ }
300
382
 
301
383
  log(
302
384
  `[pltt] publishing ${manifest.id}@${manifest.version} → ${env.name} (${env.url})`,
@@ -305,25 +387,21 @@ async function run(argv, { cwd }) {
305
387
  let frontend = null
306
388
  if (manifest.frontend?.entry) {
307
389
  log("[pltt] bundling frontend")
308
- frontend = await bundleFrontend(cwd, manifest.frontend.entry, manifest.frontend)
390
+ frontend = await bundleFrontend(cwd, manifest.frontend.entry, {
391
+ ...manifest.frontend,
392
+ pluginId: manifest.id,
393
+ })
309
394
  log(`[pltt] ${frontend.length} bytes`)
310
395
  } else {
311
396
  log("[pltt] no frontend declared")
312
397
  }
313
398
 
314
399
  log("[pltt] bundling backend")
315
- const backend = await bundleBackend(cwd)
400
+ const backend = await bundleBackend(cwd, { manifest: publishManifest })
316
401
  log(`[pltt] ${backend.length} bytes`)
317
402
 
318
403
  const backendSha = sha256(backend)
319
404
  const api = makeApi(env)
320
- let pluginSecrets = {}
321
- try {
322
- pluginSecrets = collectPluginSecrets(cwd, manifest, env, flags, log)
323
- } catch (err) {
324
- console.error(`[pltt] ${err instanceof Error ? err.message : String(err)}`)
325
- process.exit(1)
326
- }
327
405
 
328
406
  log("[pltt] requesting signed URLs")
329
407
  const signed = await api("/api/v1/appstore/sign-upload", {
@@ -341,7 +419,7 @@ async function run(argv, { cwd }) {
341
419
  put(signed.backend_upload_url, backend, "application/gzip"),
342
420
  put(
343
421
  signed.manifest_upload_url,
344
- Buffer.from(JSON.stringify(manifest, null, 2)),
422
+ Buffer.from(JSON.stringify(publishManifest, null, 2)),
345
423
  "application/json",
346
424
  ),
347
425
  ]
@@ -356,7 +434,7 @@ async function run(argv, { cwd }) {
356
434
  version: manifest.version,
357
435
  bundle_path: signed.bundle_path,
358
436
  bundle_sha256: backendSha,
359
- manifest,
437
+ manifest: publishManifest,
360
438
  publish_type: publishType,
361
439
  environment: env.name,
362
440
  }