@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/README.md +56 -8
- package/backend-sdk/palette_sdk/__init__.py +12 -0
- package/backend-sdk/palette_sdk/apps.py +12 -0
- package/backend-sdk/palette_sdk/events.py +35 -1
- package/backend-sdk/palette_sdk/manifest.py +48 -3
- package/backend-sdk/palette_sdk/services.py +147 -0
- package/docs/python-backend-sdk.md +65 -35
- package/lib/bundler.js +58 -5
- package/lib/cli.js +10 -0
- package/lib/commands/dev.js +4 -1
- package/lib/commands/doctor.js +9 -5
- package/lib/commands/package.js +4 -1
- package/lib/commands/publish.js +97 -19
- package/lib/commands/services.js +426 -0
- package/lib/commands/test.js +21 -11
- package/lib/css-scope.js +124 -0
- package/lib/dev-simulator.js +34 -4
- package/lib/manifest.js +120 -5
- package/lib/secrets.js +83 -8
- package/package.json +4 -2
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* pltt services — OS broker developer tools.
|
|
5
|
+
*
|
|
6
|
+
* pltt services list — show every service/event the org's installed
|
|
7
|
+
* apps make available; useful for "what can I
|
|
8
|
+
* consume?".
|
|
9
|
+
* pltt services pull — read consumes block from palette-plugin.json,
|
|
10
|
+
* fetch JSON Schemas from the platform, and
|
|
11
|
+
* generate typed clients into:
|
|
12
|
+
* .palette/types/services.d.ts
|
|
13
|
+
* .palette/types/services.ts (proxy)
|
|
14
|
+
* .palette/python/services.py (pydantic)
|
|
15
|
+
* pltt services scaffold <method>
|
|
16
|
+
* — emit a provider stub (schemas + handler) in
|
|
17
|
+
* the current plugin under ./broker/.
|
|
18
|
+
* pltt services add <target>
|
|
19
|
+
* — add a consumed service/event target to
|
|
20
|
+
* palette-plugin.json.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fs = require("fs")
|
|
24
|
+
const path = require("path")
|
|
25
|
+
const { parseFlags, resolveEnvironment } = require("../environments")
|
|
26
|
+
|
|
27
|
+
function readManifest(cwd) {
|
|
28
|
+
const p = path.join(cwd, "palette-plugin.json")
|
|
29
|
+
if (!fs.existsSync(p)) {
|
|
30
|
+
throw new Error("no palette-plugin.json found in current directory")
|
|
31
|
+
}
|
|
32
|
+
return JSON.parse(fs.readFileSync(p, "utf8"))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeManifest(cwd, manifest) {
|
|
36
|
+
const p = path.join(cwd, "palette-plugin.json")
|
|
37
|
+
fs.writeFileSync(p, JSON.stringify(manifest, null, 2) + "\n")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function ensureDir(p) {
|
|
41
|
+
fs.mkdirSync(p, { recursive: true })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function authFetch(env, path, init = {}) {
|
|
45
|
+
const url = `${env.url}${path}`
|
|
46
|
+
const headers = { "Content-Type": "application/json", ...(init.headers || {}) }
|
|
47
|
+
if (env.token) headers.Authorization = `Bearer ${env.token}`
|
|
48
|
+
const res = await fetch(url, { ...init, headers })
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
const text = await res.text()
|
|
51
|
+
throw new Error(`${init.method || "GET"} ${url} → ${res.status}: ${text}`)
|
|
52
|
+
}
|
|
53
|
+
return res.json()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- list --------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
async function runList(argv, { cwd }) {
|
|
59
|
+
const { flags } = parseFlags(argv)
|
|
60
|
+
const env = resolveEnvironment({ cwd, flags })
|
|
61
|
+
const catalog = await authFetch(env, "/api/v1/os-broker/catalog")
|
|
62
|
+
if (argv.includes("--json")) {
|
|
63
|
+
process.stdout.write(JSON.stringify(catalog, null, 2) + "\n")
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
const byNs = {}
|
|
67
|
+
for (const svc of catalog.services || []) {
|
|
68
|
+
const key = `${svc.namespace}/${svc.version}`
|
|
69
|
+
byNs[key] = byNs[key] || { services: [], events: [], providers: new Set() }
|
|
70
|
+
byNs[key].services.push(svc)
|
|
71
|
+
byNs[key].providers.add(svc.provider_app_id)
|
|
72
|
+
}
|
|
73
|
+
for (const evt of catalog.events || []) {
|
|
74
|
+
const key = `${evt.namespace}/${evt.version}`
|
|
75
|
+
byNs[key] = byNs[key] || { services: [], events: [], providers: new Set() }
|
|
76
|
+
byNs[key].events.push(evt)
|
|
77
|
+
byNs[key].providers.add(evt.provider_app_id)
|
|
78
|
+
}
|
|
79
|
+
const keys = Object.keys(byNs).sort()
|
|
80
|
+
if (keys.length === 0) {
|
|
81
|
+
console.log("[pltt] no services or events registered for the current org.")
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
for (const key of keys) {
|
|
85
|
+
const entry = byNs[key]
|
|
86
|
+
console.log(`\n${key} (provider: ${Array.from(entry.providers).join(", ")})`)
|
|
87
|
+
for (const svc of entry.services) {
|
|
88
|
+
const scope = svc.scope ? ` [scope: ${svc.scope}]` : ""
|
|
89
|
+
console.log(` service ${key}#${svc.method}${scope}`)
|
|
90
|
+
if (svc.description) console.log(` ${svc.description}`)
|
|
91
|
+
}
|
|
92
|
+
for (const evt of entry.events) {
|
|
93
|
+
console.log(` event ${key}#${evt.topic}`)
|
|
94
|
+
if (evt.description) console.log(` ${evt.description}`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ---- pull (codegen) ----------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
function consumeTargets(manifest) {
|
|
102
|
+
const consumes = manifest.consumes || {}
|
|
103
|
+
const normalize = (entry) => {
|
|
104
|
+
if (typeof entry === "string") return entry
|
|
105
|
+
if (entry && typeof entry === "object" && typeof entry.target === "string") return entry.target
|
|
106
|
+
return null
|
|
107
|
+
}
|
|
108
|
+
const services = (consumes.services || []).map(normalize).filter(Boolean)
|
|
109
|
+
const events = (consumes.events || []).map(normalize).filter(Boolean)
|
|
110
|
+
return { services, events }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function targetFromConsumeEntry(entry) {
|
|
114
|
+
if (typeof entry === "string") return entry
|
|
115
|
+
if (entry && typeof entry === "object" && typeof entry.target === "string") return entry.target
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function brokerTargetLooksValid(target) {
|
|
120
|
+
return typeof target === "string" && /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+#[A-Za-z0-9_.-]+$/.test(target)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function valueAfter(argv, name) {
|
|
124
|
+
const index = argv.indexOf(name)
|
|
125
|
+
if (index === -1) return null
|
|
126
|
+
return argv[index + 1] || null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function jsonSchemaToTs(schema, indent = "") {
|
|
130
|
+
if (!schema || typeof schema !== "object") return "any"
|
|
131
|
+
if (schema.type === "string") return "string"
|
|
132
|
+
if (schema.type === "number" || schema.type === "integer") return "number"
|
|
133
|
+
if (schema.type === "boolean") return "boolean"
|
|
134
|
+
if (schema.type === "array") return `${jsonSchemaToTs(schema.items)}[]`
|
|
135
|
+
if (schema.type === "object" || schema.properties) {
|
|
136
|
+
const props = schema.properties || {}
|
|
137
|
+
const required = new Set(schema.required || [])
|
|
138
|
+
const lines = []
|
|
139
|
+
for (const [name, prop] of Object.entries(props)) {
|
|
140
|
+
const opt = required.has(name) ? "" : "?"
|
|
141
|
+
lines.push(`${indent} ${JSON.stringify(name)}${opt}: ${jsonSchemaToTs(prop, indent + " ")}`)
|
|
142
|
+
}
|
|
143
|
+
return `{\n${lines.join("\n")}\n${indent}}`
|
|
144
|
+
}
|
|
145
|
+
return "any"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function jsonSchemaToPydantic(name, schema) {
|
|
149
|
+
if (!schema || typeof schema !== "object") {
|
|
150
|
+
return { code: "", ref: "dict" }
|
|
151
|
+
}
|
|
152
|
+
if (schema.type === "object" || schema.properties) {
|
|
153
|
+
const props = schema.properties || {}
|
|
154
|
+
const required = new Set(schema.required || [])
|
|
155
|
+
const fieldLines = []
|
|
156
|
+
for (const [field, prop] of Object.entries(props)) {
|
|
157
|
+
const opt = required.has(field) ? "" : " | None = None"
|
|
158
|
+
fieldLines.push(` ${field}: ${pythonScalar(prop)}${opt}`)
|
|
159
|
+
}
|
|
160
|
+
const code =
|
|
161
|
+
`class ${name}(BaseModel):\n` +
|
|
162
|
+
(fieldLines.length ? fieldLines.join("\n") : " pass") +
|
|
163
|
+
"\n"
|
|
164
|
+
return { code, ref: name }
|
|
165
|
+
}
|
|
166
|
+
return { code: "", ref: pythonScalar(schema) }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function pythonScalar(schema) {
|
|
170
|
+
if (!schema) return "Any"
|
|
171
|
+
if (schema.type === "string") return "str"
|
|
172
|
+
if (schema.type === "integer") return "int"
|
|
173
|
+
if (schema.type === "number") return "float"
|
|
174
|
+
if (schema.type === "boolean") return "bool"
|
|
175
|
+
if (schema.type === "array") return `list[${pythonScalar(schema.items)}]`
|
|
176
|
+
return "Any"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function safeIdent(s) {
|
|
180
|
+
return s.replace(/[^A-Za-z0-9_]/g, "_")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function tsHeader() {
|
|
184
|
+
return (
|
|
185
|
+
"// AUTOGENERATED by `pltt services pull` — do not edit by hand.\n" +
|
|
186
|
+
"// Re-run after adding to consumes.services / consumes.events.\n\n" +
|
|
187
|
+
'import { palette } from "@palettelab/sdk"\n\n'
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function pyHeader() {
|
|
192
|
+
return (
|
|
193
|
+
'"""AUTOGENERATED by `pltt services pull` — do not edit by hand."""\n\n' +
|
|
194
|
+
"from __future__ import annotations\n\n" +
|
|
195
|
+
"from typing import Any\n\n" +
|
|
196
|
+
"from pydantic import BaseModel\n" +
|
|
197
|
+
"from palette_sdk import services as _services\n\n"
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function runPull(argv, { cwd }) {
|
|
202
|
+
const manifest = readManifest(cwd)
|
|
203
|
+
const { services, events } = consumeTargets(manifest)
|
|
204
|
+
const targets = [...new Set([...services, ...events])]
|
|
205
|
+
if (targets.length === 0) {
|
|
206
|
+
console.log("[pltt] palette-plugin.json declares no consumes.services or consumes.events — nothing to pull.")
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
const { flags } = parseFlags(argv)
|
|
210
|
+
const env = resolveEnvironment({ cwd, flags })
|
|
211
|
+
const targetsParam = encodeURIComponent(targets.join(","))
|
|
212
|
+
const schemas = await authFetch(env, `/api/v1/os-broker/schemas?targets=${targetsParam}`)
|
|
213
|
+
|
|
214
|
+
const byTarget = new Map(schemas.map((s) => [s.target, s]))
|
|
215
|
+
const missing = targets.filter((t) => !byTarget.has(t))
|
|
216
|
+
if (missing.length) {
|
|
217
|
+
console.warn(`[pltt] warning: no schema returned for: ${missing.join(", ")}`)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---- TypeScript output
|
|
221
|
+
const tsLines = [tsHeader()]
|
|
222
|
+
const tsProxies = {}
|
|
223
|
+
for (const target of targets) {
|
|
224
|
+
const entry = byTarget.get(target)
|
|
225
|
+
if (!entry) continue
|
|
226
|
+
const proxyKey = `${entry.namespace}/${entry.version}`
|
|
227
|
+
tsProxies[proxyKey] = tsProxies[proxyKey] || []
|
|
228
|
+
if (entry.kind === "service") {
|
|
229
|
+
const inT = entry.input_schema ? jsonSchemaToTs(entry.input_schema) : "Record<string, unknown>"
|
|
230
|
+
const outT = entry.output_schema ? jsonSchemaToTs(entry.output_schema) : "unknown"
|
|
231
|
+
tsProxies[proxyKey].push({ kind: "service", method: entry.method, inT, outT, scope: entry.scope })
|
|
232
|
+
} else if (entry.kind === "event") {
|
|
233
|
+
const payT = entry.payload_schema ? jsonSchemaToTs(entry.payload_schema) : "Record<string, unknown>"
|
|
234
|
+
tsProxies[proxyKey].push({ kind: "event", topic: entry.topic, payT })
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for (const [ns, items] of Object.entries(tsProxies)) {
|
|
238
|
+
const safe = safeIdent(ns)
|
|
239
|
+
tsLines.push(`// ----- ${ns} -----`)
|
|
240
|
+
const services = items.filter((i) => i.kind === "service")
|
|
241
|
+
const events = items.filter((i) => i.kind === "event")
|
|
242
|
+
if (services.length) {
|
|
243
|
+
tsLines.push(`export const ${safe} = {`)
|
|
244
|
+
for (const svc of services) {
|
|
245
|
+
const comment = svc.scope ? ` /** scope: ${svc.scope} */` : ""
|
|
246
|
+
if (comment) tsLines.push(comment)
|
|
247
|
+
tsLines.push(` async ${JSON.stringify(svc.method)}(input: ${svc.inT}): Promise<${svc.outT}> {`)
|
|
248
|
+
tsLines.push(` return (await palette.broker.call(${JSON.stringify(ns + "#" + svc.method)}, input)) as ${svc.outT}`)
|
|
249
|
+
tsLines.push(` },`)
|
|
250
|
+
}
|
|
251
|
+
tsLines.push(`}\n`)
|
|
252
|
+
}
|
|
253
|
+
if (events.length) {
|
|
254
|
+
tsLines.push(`export const ${safe}Events = {`)
|
|
255
|
+
for (const evt of events) {
|
|
256
|
+
tsLines.push(` on${safeIdent(evt.topic).replace(/^(.)/, (m) => m.toUpperCase())}(handler: (payload: ${evt.payT}) => void) {`)
|
|
257
|
+
tsLines.push(` return palette.events.on(${JSON.stringify(ns + "#" + evt.topic)}, (payload) => handler(payload as ${evt.payT}))`)
|
|
258
|
+
tsLines.push(` },`)
|
|
259
|
+
}
|
|
260
|
+
tsLines.push(`}\n`)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const tsDir = path.join(cwd, ".palette", "types")
|
|
264
|
+
ensureDir(tsDir)
|
|
265
|
+
fs.writeFileSync(path.join(tsDir, "services.ts"), tsLines.join("\n"))
|
|
266
|
+
|
|
267
|
+
// ---- Python output
|
|
268
|
+
const pyParts = [pyHeader()]
|
|
269
|
+
for (const target of targets) {
|
|
270
|
+
const entry = byTarget.get(target)
|
|
271
|
+
if (!entry) continue
|
|
272
|
+
const safeNs = safeIdent(`${entry.namespace}_${entry.version}`)
|
|
273
|
+
const safeMethod = safeIdent(entry.method || entry.topic)
|
|
274
|
+
if (entry.kind === "service") {
|
|
275
|
+
const { code: inCode, ref: inRef } = jsonSchemaToPydantic(`${safeNs}_${safeMethod}_Input`, entry.input_schema)
|
|
276
|
+
const { code: outCode, ref: outRef } = jsonSchemaToPydantic(`${safeNs}_${safeMethod}_Output`, entry.output_schema)
|
|
277
|
+
if (inCode) pyParts.push(inCode)
|
|
278
|
+
if (outCode) pyParts.push(outCode)
|
|
279
|
+
pyParts.push(
|
|
280
|
+
`async def ${safeNs}_${safeMethod}(ctx, payload: ${inRef}) -> ${outRef}:\n` +
|
|
281
|
+
` """Call ${entry.namespace}/${entry.version}#${entry.method}.\n\n` +
|
|
282
|
+
(entry.scope ? ` Scope: ${entry.scope}\n """\n` : ` """\n`) +
|
|
283
|
+
` return await _services.services(ctx).call(${JSON.stringify(target)}, payload if isinstance(payload, dict) else payload.model_dump())\n`,
|
|
284
|
+
)
|
|
285
|
+
} else if (entry.kind === "event") {
|
|
286
|
+
const { code: payCode, ref: payRef } = jsonSchemaToPydantic(`${safeNs}_${safeMethod}_Payload`, entry.payload_schema)
|
|
287
|
+
if (payCode) pyParts.push(payCode)
|
|
288
|
+
pyParts.push(
|
|
289
|
+
`def on_${safeNs}_${safeMethod}(handler):\n` +
|
|
290
|
+
` """Subscribe to ${target}. Returns the underlying SDK subscribe call."""\n` +
|
|
291
|
+
` from palette_sdk.events import subscribe_event\n` +
|
|
292
|
+
` subscribe_event(${JSON.stringify(target)}, handler)\n`,
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const pyDir = path.join(cwd, ".palette", "python")
|
|
297
|
+
ensureDir(pyDir)
|
|
298
|
+
fs.writeFileSync(path.join(pyDir, "services.py"), pyParts.join("\n"))
|
|
299
|
+
|
|
300
|
+
console.log(`[pltt] generated:`)
|
|
301
|
+
console.log(` ${path.relative(cwd, path.join(tsDir, "services.ts"))}`)
|
|
302
|
+
console.log(` ${path.relative(cwd, path.join(pyDir, "services.py"))}`)
|
|
303
|
+
if (missing.length) process.exit(2)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---- scaffold provider stub --------------------------------------------------
|
|
307
|
+
|
|
308
|
+
async function runScaffold(argv, { cwd }) {
|
|
309
|
+
const positional = argv.filter((a) => !a.startsWith("-"))
|
|
310
|
+
const method = positional[0]
|
|
311
|
+
if (!method) {
|
|
312
|
+
console.error("[pltt] usage: pltt services scaffold <method>")
|
|
313
|
+
process.exit(1)
|
|
314
|
+
}
|
|
315
|
+
const manifest = readManifest(cwd)
|
|
316
|
+
const namespace = (manifest.provides && manifest.provides.namespace) || manifest.id
|
|
317
|
+
const provides = manifest.provides || {}
|
|
318
|
+
provides.namespace = namespace
|
|
319
|
+
provides.services = provides.services || []
|
|
320
|
+
// Ensure a default service slot.
|
|
321
|
+
let svc = provides.services.find((s) => (s.id || "") === method) || null
|
|
322
|
+
if (!svc) {
|
|
323
|
+
svc = { id: method, version: "v1", methods: [] }
|
|
324
|
+
provides.services.push(svc)
|
|
325
|
+
}
|
|
326
|
+
svc.methods = svc.methods || []
|
|
327
|
+
if (!svc.methods.find((m) => m && m.name === method)) {
|
|
328
|
+
svc.methods.push({
|
|
329
|
+
name: method,
|
|
330
|
+
scope: `${namespace}.${method}`,
|
|
331
|
+
input_schema: `schemas/${method}.in.json`,
|
|
332
|
+
output_schema: `schemas/${method}.out.json`,
|
|
333
|
+
})
|
|
334
|
+
}
|
|
335
|
+
manifest.provides = provides
|
|
336
|
+
writeManifest(cwd, manifest)
|
|
337
|
+
|
|
338
|
+
const schemasDir = path.join(cwd, "schemas")
|
|
339
|
+
ensureDir(schemasDir)
|
|
340
|
+
for (const suffix of ["in", "out"]) {
|
|
341
|
+
const p = path.join(schemasDir, `${method}.${suffix}.json`)
|
|
342
|
+
if (!fs.existsSync(p)) {
|
|
343
|
+
fs.writeFileSync(
|
|
344
|
+
p,
|
|
345
|
+
JSON.stringify(
|
|
346
|
+
{
|
|
347
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
348
|
+
type: "object",
|
|
349
|
+
properties: {},
|
|
350
|
+
required: [],
|
|
351
|
+
},
|
|
352
|
+
null,
|
|
353
|
+
2,
|
|
354
|
+
) + "\n",
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const brokerDir = path.join(cwd, "broker")
|
|
360
|
+
ensureDir(brokerDir)
|
|
361
|
+
const handlerPath = path.join(brokerDir, `${safeIdent(method)}.py`)
|
|
362
|
+
if (!fs.existsSync(handlerPath)) {
|
|
363
|
+
fs.writeFileSync(
|
|
364
|
+
handlerPath,
|
|
365
|
+
`from palette_sdk import service, PluginContext\n\n` +
|
|
366
|
+
`@service(${JSON.stringify(method)}, scope=${JSON.stringify(namespace + "." + method)})\n` +
|
|
367
|
+
`async def ${safeIdent(method)}(ctx: PluginContext, payload: dict) -> dict:\n` +
|
|
368
|
+
` """Provider handler for ${namespace}/v1#${method}.\n\n` +
|
|
369
|
+
` The OS broker validates payload against schemas/${method}.in.json and the\n` +
|
|
370
|
+
` response against schemas/${method}.out.json before routing.\n """\n` +
|
|
371
|
+
` return {}\n`,
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
console.log(`[pltt] scaffolded ${namespace}/v1#${method}`)
|
|
375
|
+
console.log(` manifest: palette-plugin.json (updated)`)
|
|
376
|
+
console.log(` schemas: schemas/${method}.in.json, schemas/${method}.out.json`)
|
|
377
|
+
console.log(` handler: ${path.relative(cwd, handlerPath)}`)
|
|
378
|
+
console.log(` import the handler from your backend entry to register it.`)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ---- add consumed target -----------------------------------------------------
|
|
382
|
+
|
|
383
|
+
async function runAdd(argv, { cwd }) {
|
|
384
|
+
const target = argv.find((arg) => !arg.startsWith("-"))
|
|
385
|
+
if (!target || !brokerTargetLooksValid(target)) {
|
|
386
|
+
console.error("[pltt] usage: pltt services add <namespace/version#name> [--event] [--optional] [--reason <text>]")
|
|
387
|
+
process.exit(1)
|
|
388
|
+
}
|
|
389
|
+
const manifest = readManifest(cwd)
|
|
390
|
+
manifest.consumes = manifest.consumes || {}
|
|
391
|
+
const bucket = argv.includes("--event") || valueAfter(argv, "--type") === "event" ? "events" : "services"
|
|
392
|
+
manifest.consumes[bucket] = manifest.consumes[bucket] || []
|
|
393
|
+
const existing = new Set(manifest.consumes[bucket].map(targetFromConsumeEntry).filter(Boolean))
|
|
394
|
+
if (!existing.has(target)) {
|
|
395
|
+
const reason = valueAfter(argv, "--reason")
|
|
396
|
+
const optional = argv.includes("--optional")
|
|
397
|
+
const entry = reason || optional ? { target, required: !optional, ...(reason ? { reason } : {}) } : target
|
|
398
|
+
manifest.consumes[bucket].push(entry)
|
|
399
|
+
writeManifest(cwd, manifest)
|
|
400
|
+
console.log(`[pltt] added ${target} to consumes.${bucket}`)
|
|
401
|
+
} else {
|
|
402
|
+
console.log(`[pltt] ${target} is already declared in consumes.${bucket}`)
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ---- dispatch ----------------------------------------------------------------
|
|
407
|
+
|
|
408
|
+
async function run(argv, ctx) {
|
|
409
|
+
const sub = argv[0]
|
|
410
|
+
const rest = argv.slice(1)
|
|
411
|
+
if (sub === "list") return runList(rest, ctx)
|
|
412
|
+
if (sub === "pull") return runPull(rest, ctx)
|
|
413
|
+
if (sub === "scaffold") return runScaffold(rest, ctx)
|
|
414
|
+
if (sub === "add") return runAdd(rest, ctx)
|
|
415
|
+
console.log("pltt services <command>\n")
|
|
416
|
+
console.log(" list Show services/events available to the current org.")
|
|
417
|
+
console.log(" pull Generate typed TS+Python clients from consumes block.")
|
|
418
|
+
console.log(" scaffold Add a new provider method + schemas + handler stub.")
|
|
419
|
+
console.log(" add Add a consumed service/event target to palette-plugin.json.")
|
|
420
|
+
if (sub && !["help", "--help", "-h"].includes(sub)) {
|
|
421
|
+
console.error(`\n[pltt] unknown subcommand: ${sub}`)
|
|
422
|
+
process.exit(1)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
module.exports = run
|
package/lib/commands/test.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require("path")
|
|
|
5
5
|
const { spawnSync } = require("child_process")
|
|
6
6
|
const { loadManifest, validateManifest, KNOWN_PERMISSIONS } = require("../manifest")
|
|
7
7
|
const { bundleFrontend, bundleBackend } = require("../bundler")
|
|
8
|
-
const { declaredSecrets, loadLocalEnv } = require("../secrets")
|
|
8
|
+
const { declaredSecrets, loadLocalEnv, loadLocalEnvDetails } = require("../secrets")
|
|
9
9
|
const buildCommand = require("./build")
|
|
10
10
|
|
|
11
11
|
const DEFAULT_FRONTEND_BUNDLE_LIMIT = 15 * 1024 * 1024
|
|
@@ -664,31 +664,32 @@ function checkDeclaredSecrets(cwd, manifest, out) {
|
|
|
664
664
|
out.ok("no manifest secrets declared")
|
|
665
665
|
return 0
|
|
666
666
|
}
|
|
667
|
-
const
|
|
667
|
+
const localEnv = loadLocalEnvDetails(cwd, { apply: false })
|
|
668
|
+
const localValues = localEnv.values
|
|
668
669
|
let failures = 0
|
|
669
670
|
for (const [name, spec] of Object.entries(declared)) {
|
|
670
671
|
if (spec.scope.includes("dev") && spec.required && !localValues[name]) {
|
|
671
672
|
out.warn(
|
|
672
|
-
`required dev secret ${name} is missing from .
|
|
673
|
-
"
|
|
673
|
+
`required dev secret ${name} is missing from local .env files`,
|
|
674
|
+
"Add it to .env, .env.local, or an environment-specific .env file.",
|
|
674
675
|
{ secret: name, scope: spec.scope },
|
|
675
676
|
)
|
|
676
677
|
}
|
|
677
678
|
if (spec.scope.includes("plugin") && spec.required && !localValues[name] && !process.env[name]) {
|
|
678
679
|
out.warn(
|
|
679
680
|
`required plugin secret ${name} has no local value`,
|
|
680
|
-
"
|
|
681
|
+
"Add it to .env/.env.local, set an env var, pass --secrets-file, or run pltt secrets set.",
|
|
681
682
|
{ secret: name, scope: spec.scope },
|
|
682
683
|
)
|
|
683
684
|
}
|
|
684
685
|
}
|
|
685
|
-
if (!
|
|
686
|
+
if (!localEnv.files.length) {
|
|
686
687
|
out.warn(
|
|
687
|
-
".
|
|
688
|
-
"
|
|
688
|
+
"no local .env files found",
|
|
689
|
+
"Create .env or .env.local when your app needs local environment values.",
|
|
689
690
|
)
|
|
690
691
|
} else {
|
|
691
|
-
out.ok(
|
|
692
|
+
out.ok(`local env files present: ${localEnv.files.join(", ")}`)
|
|
692
693
|
}
|
|
693
694
|
out.ok(`manifest declares ${names.length} secret(s)`, { secrets: names })
|
|
694
695
|
return failures
|
|
@@ -757,14 +758,20 @@ async function run(args, { cwd }) {
|
|
|
757
758
|
}
|
|
758
759
|
if ((manifest.permissions || []).length) out.ok("declared permissions are known")
|
|
759
760
|
|
|
760
|
-
if (manifest.provides || manifest.requires) {
|
|
761
|
+
if (manifest.provides || manifest.requires || manifest.consumes) {
|
|
761
762
|
const providedServices = manifest.provides?.services?.map((item) => item.id).filter(Boolean) || []
|
|
762
763
|
const requiredServices = manifest.requires?.services?.map((item) => item.id).filter(Boolean) || []
|
|
763
764
|
const requiredApps = manifest.requires?.apps?.map((item) => item.id).filter(Boolean) || []
|
|
765
|
+
const consumedServices =
|
|
766
|
+
manifest.consumes?.services?.map((item) => (typeof item === "string" ? item : item.target)).filter(Boolean) || []
|
|
767
|
+
const consumedEvents =
|
|
768
|
+
manifest.consumes?.events?.map((item) => (typeof item === "string" ? item : item.target)).filter(Boolean) || []
|
|
764
769
|
out.ok("app-to-app contracts are valid", {
|
|
765
770
|
provides_services: providedServices,
|
|
766
771
|
requires_services: requiredServices,
|
|
767
772
|
requires_apps: requiredApps,
|
|
773
|
+
consumes_services: consumedServices,
|
|
774
|
+
consumes_events: consumedEvents,
|
|
768
775
|
})
|
|
769
776
|
}
|
|
770
777
|
|
|
@@ -786,7 +793,10 @@ async function run(args, { cwd }) {
|
|
|
786
793
|
|
|
787
794
|
if (manifest.frontend?.entry) {
|
|
788
795
|
try {
|
|
789
|
-
const frontend = await bundleFrontend(cwd, manifest.frontend.entry,
|
|
796
|
+
const frontend = await bundleFrontend(cwd, manifest.frontend.entry, {
|
|
797
|
+
...manifest.frontend,
|
|
798
|
+
pluginId: manifest.id,
|
|
799
|
+
})
|
|
790
800
|
out.ok(`frontend bundles successfully (${frontend.length} bytes)`, { bytes: frontend.length })
|
|
791
801
|
failures += checkBundleSize("frontend", frontend.length, out)
|
|
792
802
|
failures += checkFrontendSecretLeaks(cwd, frontend, manifest, out)
|
package/lib/css-scope.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const postcss = require("postcss")
|
|
4
|
+
const selectorParser = require("postcss-selector-parser")
|
|
5
|
+
|
|
6
|
+
function cssString(value) {
|
|
7
|
+
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"')
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function cssIdentifier(value) {
|
|
11
|
+
return String(value).replace(/[^a-zA-Z0-9_-]/g, "-")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function rootSelector(pluginId) {
|
|
15
|
+
return `[data-palette-plugin-root="${cssString(pluginId)}"]`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isKeyframesRule(rule) {
|
|
19
|
+
let parent = rule.parent
|
|
20
|
+
while (parent) {
|
|
21
|
+
if (parent.type === "atrule" && /keyframes$/i.test(parent.name)) return true
|
|
22
|
+
parent = parent.parent
|
|
23
|
+
}
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isRootLike(node) {
|
|
28
|
+
return (
|
|
29
|
+
(node.type === "tag" && /^(html|body)$/i.test(node.value)) ||
|
|
30
|
+
(node.type === "pseudo" && node.value === ":root")
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function removeLeadingCombinators(selector) {
|
|
35
|
+
while (selector.nodes[0]?.type === "combinator") {
|
|
36
|
+
selector.nodes[0].remove()
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function replaceRootLikeSelectors(selector, rootNode) {
|
|
41
|
+
let replaced = false
|
|
42
|
+
selector.walk((node) => {
|
|
43
|
+
if (!isRootLike(node)) return
|
|
44
|
+
node.replaceWith(rootNode.clone())
|
|
45
|
+
replaced = true
|
|
46
|
+
})
|
|
47
|
+
return replaced
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function prefixSelector(selector, rootNode) {
|
|
51
|
+
if (!selector.nodes.length) return
|
|
52
|
+
if (replaceRootLikeSelectors(selector, rootNode)) {
|
|
53
|
+
removeLeadingCombinators(selector)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
selector.prepend(selectorParser.combinator({ value: " " }))
|
|
57
|
+
selector.prepend(rootNode.clone())
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function scopeSelectors(selector, pluginId) {
|
|
61
|
+
const rootNode = selectorParser.attribute({
|
|
62
|
+
attribute: "data-palette-plugin-root",
|
|
63
|
+
operator: "=",
|
|
64
|
+
quoteMark: '"',
|
|
65
|
+
value: String(pluginId),
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return selectorParser((selectors) => {
|
|
69
|
+
selectors.each((sel) => prefixSelector(sel, rootNode))
|
|
70
|
+
}).processSync(selector)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function renameKeyframes(root, pluginId) {
|
|
74
|
+
const prefix = `palette-${cssIdentifier(pluginId)}-`
|
|
75
|
+
const names = new Map()
|
|
76
|
+
|
|
77
|
+
root.walkAtRules((atRule) => {
|
|
78
|
+
if (!/keyframes$/i.test(atRule.name)) return
|
|
79
|
+
const current = atRule.params.trim()
|
|
80
|
+
if (!current || /^["']/.test(current)) return
|
|
81
|
+
const next = `${prefix}${current}`
|
|
82
|
+
names.set(current, next)
|
|
83
|
+
atRule.params = next
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
if (names.size === 0) return
|
|
87
|
+
|
|
88
|
+
const namePattern = new RegExp(
|
|
89
|
+
`\\b(${Array.from(names.keys()).map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})\\b`,
|
|
90
|
+
"g",
|
|
91
|
+
)
|
|
92
|
+
root.walkDecls((decl) => {
|
|
93
|
+
if (!/^animation(-name)?$/i.test(decl.prop)) return
|
|
94
|
+
decl.value = decl.value.replace(namePattern, (match) => names.get(match) || match)
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function scopePluginCss(css, pluginId) {
|
|
99
|
+
if (!css || !String(css).trim()) return ""
|
|
100
|
+
const root = postcss.parse(css)
|
|
101
|
+
|
|
102
|
+
renameKeyframes(root, pluginId)
|
|
103
|
+
root.walkRules((rule) => {
|
|
104
|
+
if (isKeyframesRule(rule)) return
|
|
105
|
+
rule.selector = scopeSelectors(rule.selector, pluginId)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
return root.toString()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function appendCssExport(js, css) {
|
|
112
|
+
if (!css || !css.trim()) return js
|
|
113
|
+
const exportLine = `export const __palettePluginCss = ${JSON.stringify(css)};\n`
|
|
114
|
+
const sourceMapPattern = /\n?\/\/# sourceMappingURL=data:application\/json[^]*$/m
|
|
115
|
+
const match = js.match(sourceMapPattern)
|
|
116
|
+
if (!match) return `${js}\n${exportLine}`
|
|
117
|
+
return `${js.slice(0, match.index)}\n${exportLine}${match[0]}`
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
appendCssExport,
|
|
122
|
+
rootSelector,
|
|
123
|
+
scopePluginCss,
|
|
124
|
+
}
|