@palettelab/cli 0.3.2 → 0.3.4
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 +12 -7
- package/lib/bundler.js +2 -2
- package/lib/cli.js +2 -0
- package/lib/commands/build.js +2 -2
- package/lib/commands/dev.js +38 -12
- package/lib/commands/doctor.js +15 -27
- package/lib/commands/init.js +10 -10
- package/lib/commands/logs.js +4 -4
- package/lib/commands/package.js +4 -4
- package/lib/commands/publish.js +50 -18
- package/lib/commands/status.js +5 -5
- package/lib/commands/test.js +2 -2
- package/lib/environments.js +2 -2
- package/lib/ports.js +42 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -47,24 +47,29 @@ Boot the platform locally with your plugin mounted live. Run this from inside yo
|
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
49
|
pltt dev
|
|
50
|
+
pltt dev --cloud --env staging
|
|
50
51
|
```
|
|
51
52
|
|
|
52
53
|
Under the hood this runs `docker compose up` with a bundled compose file. It starts:
|
|
53
54
|
|
|
54
55
|
- Postgres + Redis
|
|
55
|
-
- The Palette frontend on http://localhost:3000
|
|
56
|
-
- The Palette backend on http://localhost:8000
|
|
57
|
-
- Your plugin at
|
|
56
|
+
- The Palette frontend on the first available port starting at http://localhost:3000
|
|
57
|
+
- The Palette backend on the first available port starting at http://localhost:8000
|
|
58
|
+
- Your plugin at the frontend URL printed by the CLI
|
|
58
59
|
|
|
59
60
|
Your plugin directory is mounted into the container at `/plugins/<your-id>`. Edits to your frontend/backend sources hot-reload.
|
|
60
61
|
|
|
62
|
+
`3000` and `8000` are preferred defaults, not hard requirements. If either port is already in use, `pltt dev` automatically picks the next free port and prints the actual URLs.
|
|
63
|
+
|
|
64
|
+
`pltt dev --cloud` skips Docker and publishes a reviewable preview to a configured cloud sandbox. It defaults to `--env staging` unless `--env` or `PALETTE_ENV` is set.
|
|
65
|
+
|
|
61
66
|
Environment variables:
|
|
62
67
|
|
|
63
68
|
| Name | Default | Purpose |
|
|
64
69
|
|---|---|---|
|
|
65
70
|
| `PALETTE_DEV_IMAGE` | `ghcr.io/palette-lab/platform-dev:latest` | Override the platform image |
|
|
66
|
-
| `PALETTE_FRONTEND_PORT` | `3000` |
|
|
67
|
-
| `PALETTE_BACKEND_PORT` | `8000` |
|
|
71
|
+
| `PALETTE_FRONTEND_PORT` | `3000` | Preferred starting host port for the frontend |
|
|
72
|
+
| `PALETTE_BACKEND_PORT` | `8000` | Preferred starting host port for the backend |
|
|
68
73
|
|
|
69
74
|
### `pltt doctor`
|
|
70
75
|
|
|
@@ -120,7 +125,7 @@ pltt publish --env production -y
|
|
|
120
125
|
pltt publish --env staging --json
|
|
121
126
|
```
|
|
122
127
|
|
|
123
|
-
Publishing bundles frontend/backend artifacts, uploads them, creates a `pending_review` publish record, and prints review/preview URLs when the platform returns them.
|
|
128
|
+
Publishing first runs the same contract checks as `pltt test`. If preflight passes, it bundles frontend/backend artifacts, uploads them, creates a `pending_review` publish record, and prints review/preview URLs when the platform returns them.
|
|
124
129
|
|
|
125
130
|
Environment config is read from `./palette.config.json`, `~/.palette/config.json`, or `PALETTE_<ENV>_URL` plus `PALETTE_<ENV>_TOKEN` / `PALETTE_PUBLISH_TOKEN`.
|
|
126
131
|
|
|
@@ -165,6 +170,6 @@ If no plugin ID is provided, the CLI uses the current `palette-plugin.json` or `
|
|
|
165
170
|
|
|
166
171
|
## See also
|
|
167
172
|
|
|
168
|
-
- `@palettelab/sdk` on
|
|
173
|
+
- `@palettelab/sdk` on npm — frontend hooks and types
|
|
169
174
|
- `palette-sdk` (git-installed from [palette-lab/virtual-organisation](https://github.com/palette-lab/virtual-organisation/tree/main/sdk/backend)) — backend `PluginRouter` + `ToolDefinition`
|
|
170
175
|
- [Developer Guide](https://github.com/palette-lab/virtual-organisation/blob/main/sdk/DEVELOPER_GUIDE.md)
|
package/lib/bundler.js
CHANGED
|
@@ -101,14 +101,14 @@ async function watchFrontend(pluginDir, entry, outfile) {
|
|
|
101
101
|
setup(build) {
|
|
102
102
|
build.onEnd((result) => {
|
|
103
103
|
if (result.errors.length) {
|
|
104
|
-
console.error("[
|
|
104
|
+
console.error("[pltt] frontend bundle failed:")
|
|
105
105
|
for (const err of result.errors) {
|
|
106
106
|
console.error(` - ${err.text}`)
|
|
107
107
|
}
|
|
108
108
|
return
|
|
109
109
|
}
|
|
110
110
|
const size = fs.existsSync(outfile) ? fs.statSync(outfile).size : 0
|
|
111
|
-
console.log(`[
|
|
111
|
+
console.log(`[pltt] frontend bundle ready (${size} bytes)`)
|
|
112
112
|
})
|
|
113
113
|
},
|
|
114
114
|
},
|
package/lib/cli.js
CHANGED
|
@@ -49,6 +49,8 @@ function printHelp() {
|
|
|
49
49
|
console.log("\nPublish flags:")
|
|
50
50
|
console.log(" --env <name> Target environment from ~/.palette/config.json (default: local)")
|
|
51
51
|
console.log(" -y, --yes Skip interactive confirmation for production pushes")
|
|
52
|
+
console.log("\nDev flags:")
|
|
53
|
+
console.log(" --cloud Publish a reviewable preview to a configured cloud sandbox")
|
|
52
54
|
console.log("\nInit flags:")
|
|
53
55
|
console.log(" --template <name> One of: dashboard, agent-tool, external-service, database, frontend-only")
|
|
54
56
|
console.log("\nLogs flags:")
|
package/lib/commands/build.js
CHANGED
|
@@ -93,11 +93,11 @@ async function run(args, { cwd }) {
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
if (errors.length) {
|
|
96
|
-
console.error("[
|
|
96
|
+
console.error("[pltt] validation failed:")
|
|
97
97
|
for (const e of errors) console.error(` - ${e}`)
|
|
98
98
|
process.exit(1)
|
|
99
99
|
}
|
|
100
|
-
console.log(`[
|
|
100
|
+
console.log(`[pltt] ok — ${manifest.id} v${manifest.version}`)
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
module.exports = run
|
package/lib/commands/dev.js
CHANGED
|
@@ -4,11 +4,12 @@ const path = require("path")
|
|
|
4
4
|
const { spawn, spawnSync } = require("child_process")
|
|
5
5
|
const { loadManifest } = require("../manifest")
|
|
6
6
|
const { watchFrontend } = require("../bundler")
|
|
7
|
+
const { parseFlags } = require("../environments")
|
|
8
|
+
const { resolveDevPorts } = require("../ports")
|
|
9
|
+
const publish = require("./publish")
|
|
7
10
|
|
|
8
11
|
const DEFAULT_IMAGE =
|
|
9
12
|
process.env.PALETTE_DEV_IMAGE || "ghcr.io/palette-lab/platform-dev:latest"
|
|
10
|
-
const FRONTEND_PORT = process.env.PALETTE_FRONTEND_PORT || "3000"
|
|
11
|
-
const BACKEND_PORT = process.env.PALETTE_BACKEND_PORT || "8000"
|
|
12
13
|
const COMPOSE_FILE = path.resolve(__dirname, "..", "..", "platform-dev", "docker-compose.yml")
|
|
13
14
|
|
|
14
15
|
function ensureDocker() {
|
|
@@ -39,34 +40,59 @@ function ensureImageAvailable(image) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
async function run(args, { cwd }) {
|
|
43
|
+
const { flags, rest } = parseFlags(args)
|
|
44
|
+
const cloud = rest.includes("--cloud")
|
|
45
|
+
if (cloud) {
|
|
46
|
+
const publishArgs = []
|
|
47
|
+
if (flags.env) publishArgs.push("--env", flags.env)
|
|
48
|
+
if (flags.yes) publishArgs.push("--yes")
|
|
49
|
+
if (args.includes("--json")) publishArgs.push("--json")
|
|
50
|
+
if (!flags.env && !process.env.PALETTE_ENV) publishArgs.push("--env", "staging")
|
|
51
|
+
console.log(
|
|
52
|
+
"[pltt] cloud dev publishes a reviewable preview to the configured cloud sandbox.",
|
|
53
|
+
)
|
|
54
|
+
console.log("[pltt] use `pltt status <publish-id>` after upload to track review state.")
|
|
55
|
+
await publish(publishArgs, { cwd })
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
42
59
|
const manifest = loadManifest(cwd)
|
|
43
60
|
const pluginId = manifest.id
|
|
44
61
|
const frontendEntry = manifest.frontend?.entry || "./frontend/src/index.tsx"
|
|
45
62
|
const frontendBundle = path.join(cwd, ".palette", "dist", "frontend.mjs")
|
|
63
|
+
const ports = await resolveDevPorts()
|
|
64
|
+
const frontendPort = String(ports.frontend)
|
|
65
|
+
const backendPort = String(ports.backend)
|
|
46
66
|
|
|
47
67
|
ensureDocker()
|
|
48
68
|
|
|
49
69
|
let frontendWatcher = null
|
|
50
70
|
if (manifest.frontend?.entry) {
|
|
51
|
-
console.log(`[
|
|
71
|
+
console.log(`[pltt] bundling frontend ${frontendEntry} → .palette/dist/frontend.mjs`)
|
|
52
72
|
try {
|
|
53
73
|
frontendWatcher = await watchFrontend(cwd, frontendEntry, frontendBundle)
|
|
54
74
|
} catch (err) {
|
|
55
75
|
console.error(
|
|
56
|
-
`[
|
|
76
|
+
`[pltt] could not start frontend bundler: ${
|
|
57
77
|
err instanceof Error ? err.message : String(err)
|
|
58
78
|
}`,
|
|
59
79
|
)
|
|
60
80
|
process.exit(1)
|
|
61
81
|
}
|
|
62
82
|
} else {
|
|
63
|
-
console.log("[
|
|
83
|
+
console.log("[pltt] manifest has no frontend entry; skipping frontend bundler")
|
|
64
84
|
}
|
|
65
85
|
|
|
66
|
-
console.log(`[
|
|
67
|
-
console.log(`[
|
|
68
|
-
|
|
69
|
-
|
|
86
|
+
console.log(`[pltt] starting ${DEFAULT_IMAGE} via docker compose`)
|
|
87
|
+
console.log(`[pltt] mounting ${cwd} → /plugins/${pluginId}`)
|
|
88
|
+
if (ports.frontend !== ports.preferredFrontend) {
|
|
89
|
+
console.log(`[pltt] frontend port ${ports.preferredFrontend} is busy; using ${ports.frontend}`)
|
|
90
|
+
}
|
|
91
|
+
if (ports.backend !== ports.preferredBackend) {
|
|
92
|
+
console.log(`[pltt] backend port ${ports.preferredBackend} is busy; using ${ports.backend}`)
|
|
93
|
+
}
|
|
94
|
+
console.log(`[pltt] frontend: http://localhost:${frontendPort}/apps/${pluginId}`)
|
|
95
|
+
console.log(`[pltt] backend: http://localhost:${backendPort}/api/v1/plugins/${pluginId}`)
|
|
70
96
|
|
|
71
97
|
// Pre-pull so we can give a useful error if the image isn't reachable
|
|
72
98
|
// (common cause: maintainer hasn't pushed it yet, or `docker login ghcr.io`
|
|
@@ -74,7 +100,7 @@ async function run(args, { cwd }) {
|
|
|
74
100
|
if (!ensureImageAvailable(DEFAULT_IMAGE)) {
|
|
75
101
|
if (frontendWatcher) await frontendWatcher.dispose()
|
|
76
102
|
console.error(
|
|
77
|
-
`\n[
|
|
103
|
+
`\n[pltt] could not pull ${DEFAULT_IMAGE}.\n` +
|
|
78
104
|
` Most common causes:\n` +
|
|
79
105
|
` • The image hasn't been published to the registry yet.\n` +
|
|
80
106
|
` Platform maintainers: see docker/platform-dev/README.md for the build + push flow.\n` +
|
|
@@ -89,8 +115,8 @@ async function run(args, { cwd }) {
|
|
|
89
115
|
PALETTE_DEV_IMAGE: DEFAULT_IMAGE,
|
|
90
116
|
PALETTE_ACTIVE_PLUGIN: pluginId,
|
|
91
117
|
PALETTE_PLUGIN_DIR: cwd,
|
|
92
|
-
PALETTE_FRONTEND_PORT:
|
|
93
|
-
PALETTE_BACKEND_PORT:
|
|
118
|
+
PALETTE_FRONTEND_PORT: frontendPort,
|
|
119
|
+
PALETTE_BACKEND_PORT: backendPort,
|
|
94
120
|
}
|
|
95
121
|
|
|
96
122
|
const projectName = `palette-dev-${pluginId}`
|
package/lib/commands/doctor.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict"
|
|
2
2
|
|
|
3
3
|
const fs = require("fs")
|
|
4
|
-
const net = require("net")
|
|
5
4
|
const path = require("path")
|
|
6
5
|
const { spawnSync } = require("child_process")
|
|
7
6
|
const { loadManifest, validateManifest } = require("../manifest")
|
|
8
7
|
const { bundleFrontend } = require("../bundler")
|
|
8
|
+
const { resolveDevPorts } = require("../ports")
|
|
9
9
|
|
|
10
10
|
const DEFAULT_IMAGE =
|
|
11
11
|
process.env.PALETTE_DEV_IMAGE || "ghcr.io/palette-lab/platform-dev:latest"
|
|
@@ -39,17 +39,6 @@ function imageExistsLocally(image) {
|
|
|
39
39
|
return spawnSync("docker", ["image", "inspect", image], { stdio: "ignore" }).status === 0
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
function canBindPort(port) {
|
|
43
|
-
return new Promise((resolve) => {
|
|
44
|
-
const server = net.createServer()
|
|
45
|
-
server.once("error", () => resolve(false))
|
|
46
|
-
server.once("listening", () => {
|
|
47
|
-
server.close(() => resolve(true))
|
|
48
|
-
})
|
|
49
|
-
server.listen(port, "127.0.0.1")
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
42
|
function checkEntry(cwd, label, rel) {
|
|
54
43
|
if (!rel) return 0
|
|
55
44
|
const abs = path.resolve(cwd, rel)
|
|
@@ -81,19 +70,18 @@ async function run(args, { cwd }) {
|
|
|
81
70
|
)
|
|
82
71
|
}
|
|
83
72
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
`frontend port ${FRONTEND_PORT} is already in use`,
|
|
88
|
-
`
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
73
|
+
try {
|
|
74
|
+
const ports = await resolveDevPorts({ frontend: FRONTEND_PORT, backend: BACKEND_PORT })
|
|
75
|
+
if (ports.frontend === FRONTEND_PORT) ok(`frontend port ${FRONTEND_PORT} is available`)
|
|
76
|
+
else warn(`frontend port ${FRONTEND_PORT} is already in use`, `pltt dev will use ${ports.frontend}`)
|
|
77
|
+
if (ports.backend === BACKEND_PORT) ok(`backend port ${BACKEND_PORT} is available`)
|
|
78
|
+
else warn(`backend port ${BACKEND_PORT} is already in use`, `pltt dev will use ${ports.backend}`)
|
|
79
|
+
} catch (err) {
|
|
80
|
+
failures += fail(
|
|
81
|
+
`could not find free dev ports: ${err instanceof Error ? err.message : String(err)}`,
|
|
82
|
+
"Free a local port or set PALETTE_FRONTEND_PORT / PALETTE_BACKEND_PORT to a wider available range.",
|
|
83
|
+
)
|
|
84
|
+
}
|
|
97
85
|
|
|
98
86
|
let manifest
|
|
99
87
|
try {
|
|
@@ -134,10 +122,10 @@ async function run(args, { cwd }) {
|
|
|
134
122
|
}
|
|
135
123
|
|
|
136
124
|
if (failures > 0) {
|
|
137
|
-
console.log(`\n[
|
|
125
|
+
console.log(`\n[pltt] doctor found ${failures} blocking issue(s).`)
|
|
138
126
|
process.exit(1)
|
|
139
127
|
}
|
|
140
|
-
console.log("\n[
|
|
128
|
+
console.log("\n[pltt] doctor passed.")
|
|
141
129
|
}
|
|
142
130
|
|
|
143
131
|
module.exports = run
|
package/lib/commands/init.js
CHANGED
|
@@ -72,13 +72,13 @@ async function run(args, { cwd }) {
|
|
|
72
72
|
const name = positional[0]
|
|
73
73
|
if (!name) {
|
|
74
74
|
console.error("[pltt] usage: pltt init <plugin-name> [--template <name>]")
|
|
75
|
-
console.error(`[
|
|
75
|
+
console.error(`[pltt] templates: ${KNOWN_TEMPLATES.join(", ")}`)
|
|
76
76
|
process.exit(1)
|
|
77
77
|
}
|
|
78
78
|
const template = getOpt(args, "--template")
|
|
79
79
|
if (template && !KNOWN_TEMPLATES.includes(template)) {
|
|
80
|
-
console.error(`[
|
|
81
|
-
console.error(`[
|
|
80
|
+
console.error(`[pltt] unknown template: ${template}`)
|
|
81
|
+
console.error(`[pltt] templates: ${KNOWN_TEMPLATES.join(", ")}`)
|
|
82
82
|
process.exit(1)
|
|
83
83
|
}
|
|
84
84
|
const slug = toSlug(name)
|
|
@@ -87,25 +87,25 @@ async function run(args, { cwd }) {
|
|
|
87
87
|
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
88
88
|
const destDir = path.join(cwd, slug)
|
|
89
89
|
if (fs.existsSync(destDir)) {
|
|
90
|
-
console.error(`[
|
|
90
|
+
console.error(`[pltt] directory already exists: ${destDir}`)
|
|
91
91
|
process.exit(1)
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
if (template) {
|
|
95
|
-
console.log(`[
|
|
95
|
+
console.log(`[pltt] creating plugin "${slug}" from template "${template}"`)
|
|
96
96
|
} else {
|
|
97
|
-
console.log(`[
|
|
97
|
+
console.log(`[pltt] creating plugin "${slug}" from ${TEMPLATE_REPO}@${TEMPLATE_REF}`)
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "palette-tpl-"))
|
|
101
101
|
let ok = false
|
|
102
102
|
if (!template) {
|
|
103
103
|
ok = fetchTemplate(tmp)
|
|
104
|
-
if (!ok) console.warn("[
|
|
104
|
+
if (!ok) console.warn("[pltt] git clone failed, falling back to bundled template")
|
|
105
105
|
}
|
|
106
106
|
if (!ok) ok = copyLocalFallback(tmp, template)
|
|
107
107
|
if (!ok) {
|
|
108
|
-
console.error("[
|
|
108
|
+
console.error("[pltt] no template available")
|
|
109
109
|
process.exit(1)
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -113,8 +113,8 @@ async function run(args, { cwd }) {
|
|
|
113
113
|
fs.rmSync(tmp, { recursive: true, force: true })
|
|
114
114
|
rewriteManifest(destDir, slug, displayName)
|
|
115
115
|
|
|
116
|
-
console.log(`[
|
|
117
|
-
console.log("[
|
|
116
|
+
console.log(`[pltt] created ${destDir}`)
|
|
117
|
+
console.log("[pltt] next steps:")
|
|
118
118
|
console.log(` cd ${slug}`)
|
|
119
119
|
console.log(` npx @palettelab/cli dev`)
|
|
120
120
|
console.log(` # or, after global install: pltt dev`)
|
package/lib/commands/logs.js
CHANGED
|
@@ -50,7 +50,7 @@ async function run(argv, { cwd }) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
if (!pluginId) {
|
|
53
|
-
console.error("[
|
|
53
|
+
console.error("[pltt] no plugin id provided and no manifest found.")
|
|
54
54
|
process.exit(1)
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -60,11 +60,11 @@ async function run(argv, { cwd }) {
|
|
|
60
60
|
try {
|
|
61
61
|
env = resolveEnvironment({ cwd, flags })
|
|
62
62
|
} catch (err) {
|
|
63
|
-
console.error(`[
|
|
63
|
+
console.error(`[pltt] ${err.message}`)
|
|
64
64
|
process.exit(1)
|
|
65
65
|
}
|
|
66
66
|
if (!env.token) {
|
|
67
|
-
console.error(`[
|
|
67
|
+
console.error(`[pltt] no publish token for "${env.name}". set $${env.token_env}.`)
|
|
68
68
|
process.exit(1)
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -74,7 +74,7 @@ async function run(argv, { cwd }) {
|
|
|
74
74
|
try {
|
|
75
75
|
data = await fetchLogs(env, pluginId, tail)
|
|
76
76
|
} catch (err) {
|
|
77
|
-
console.error(`[
|
|
77
|
+
console.error(`[pltt] ${err.message}`)
|
|
78
78
|
process.exit(1)
|
|
79
79
|
}
|
|
80
80
|
const events = data.events || []
|
package/lib/commands/package.js
CHANGED
|
@@ -15,7 +15,7 @@ async function run(argv, { cwd }) {
|
|
|
15
15
|
const manifest = loadManifest(cwd)
|
|
16
16
|
const errors = validateManifest(manifest)
|
|
17
17
|
if (errors.length) {
|
|
18
|
-
console.error("[
|
|
18
|
+
console.error("[pltt] manifest invalid:")
|
|
19
19
|
for (const e of errors) console.error(` - ${e}`)
|
|
20
20
|
process.exit(1)
|
|
21
21
|
}
|
|
@@ -41,7 +41,7 @@ async function run(argv, { cwd }) {
|
|
|
41
41
|
const { spawnSync } = require("child_process")
|
|
42
42
|
const r = spawnSync("tar", ["-czf", out, "-C", stage, "."], { stdio: "inherit" })
|
|
43
43
|
if (r.status !== 0) {
|
|
44
|
-
console.error("[
|
|
44
|
+
console.error("[pltt] tar failed")
|
|
45
45
|
process.exit(1)
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -56,8 +56,8 @@ async function run(argv, { cwd }) {
|
|
|
56
56
|
if (json) {
|
|
57
57
|
console.log(JSON.stringify(result, null, 2))
|
|
58
58
|
} else {
|
|
59
|
-
console.log(`[
|
|
60
|
-
console.log(`[
|
|
59
|
+
console.log(`[pltt] packaged ${manifest.id}@${manifest.version} → ${out} (${buf.length} bytes)`)
|
|
60
|
+
console.log(`[pltt] sha256: ${result.sha256}`)
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
package/lib/commands/publish.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const crypto = require("crypto")
|
|
4
4
|
const fs = require("fs")
|
|
5
5
|
const path = require("path")
|
|
6
|
+
const { spawnSync } = require("child_process")
|
|
6
7
|
const { loadManifest, validateManifest } = require("../manifest")
|
|
7
8
|
const { bundleFrontend, bundleBackend } = require("../bundler")
|
|
8
9
|
const {
|
|
@@ -15,6 +16,35 @@ function sha256(buf) {
|
|
|
15
16
|
return crypto.createHash("sha256").update(buf).digest("hex")
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
function runPreflight(cwd, json) {
|
|
20
|
+
const cliBin = path.resolve(__dirname, "..", "..", "bin", "pltt.js")
|
|
21
|
+
const res = spawnSync(process.execPath, [cliBin, "test", "--json"], {
|
|
22
|
+
cwd,
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
env: process.env,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
if (res.status === 0) {
|
|
28
|
+
if (!json) console.log("[pltt] preflight contract tests passed")
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const output = `${res.stdout || ""}${res.stderr || ""}`.trim()
|
|
33
|
+
if (json) {
|
|
34
|
+
let payload = null
|
|
35
|
+
try {
|
|
36
|
+
payload = JSON.parse(res.stdout)
|
|
37
|
+
} catch {
|
|
38
|
+
payload = { ok: false, error: output || "pltt test failed" }
|
|
39
|
+
}
|
|
40
|
+
console.log(JSON.stringify({ ok: false, stage: "preflight", test: payload }, null, 2))
|
|
41
|
+
} else {
|
|
42
|
+
console.error("[pltt] preflight contract tests failed; publish cancelled.")
|
|
43
|
+
if (output) console.error(output)
|
|
44
|
+
}
|
|
45
|
+
process.exit(res.status || 1)
|
|
46
|
+
}
|
|
47
|
+
|
|
18
48
|
function makeApi(env) {
|
|
19
49
|
return async function api(pathname, { method = "GET", body, headers = {} } = {}) {
|
|
20
50
|
const url = `${env.url}${pathname}`
|
|
@@ -54,13 +84,13 @@ async function run(argv, { cwd }) {
|
|
|
54
84
|
try {
|
|
55
85
|
env = resolveEnvironment({ cwd, flags })
|
|
56
86
|
} catch (err) {
|
|
57
|
-
console.error(`[
|
|
87
|
+
console.error(`[pltt] ${err.message}`)
|
|
58
88
|
process.exit(1)
|
|
59
89
|
}
|
|
60
90
|
|
|
61
91
|
if (!env.token) {
|
|
62
92
|
console.error(
|
|
63
|
-
`[
|
|
93
|
+
`[pltt] no publish token for environment "${env.name}". ` +
|
|
64
94
|
`Set $${env.token_env} (or $PALETTE_PUBLISH_TOKEN as fallback).\n` +
|
|
65
95
|
` Ask a superadmin to mint one via POST ${env.url}/api/superadmin/publish-tokens.`,
|
|
66
96
|
)
|
|
@@ -70,7 +100,7 @@ async function run(argv, { cwd }) {
|
|
|
70
100
|
if (env.production_unconfirmed) {
|
|
71
101
|
const ok = await confirmProduction(env)
|
|
72
102
|
if (!ok) {
|
|
73
|
-
console.error("[
|
|
103
|
+
console.error("[pltt] publish cancelled.")
|
|
74
104
|
process.exit(1)
|
|
75
105
|
}
|
|
76
106
|
}
|
|
@@ -78,32 +108,34 @@ async function run(argv, { cwd }) {
|
|
|
78
108
|
const manifest = loadManifest(cwd)
|
|
79
109
|
const errors = validateManifest(manifest)
|
|
80
110
|
if (errors.length) {
|
|
81
|
-
console.error("[
|
|
111
|
+
console.error("[pltt] manifest invalid:")
|
|
82
112
|
for (const e of errors) console.error(` - ${e}`)
|
|
83
113
|
process.exit(1)
|
|
84
114
|
}
|
|
85
115
|
|
|
116
|
+
runPreflight(cwd, flags.json)
|
|
117
|
+
|
|
86
118
|
console.log(
|
|
87
|
-
`[
|
|
119
|
+
`[pltt] publishing ${manifest.id}@${manifest.version} → ${env.name} (${env.url})`,
|
|
88
120
|
)
|
|
89
121
|
|
|
90
122
|
let frontend = null
|
|
91
123
|
if (manifest.frontend?.entry) {
|
|
92
|
-
console.log("[
|
|
124
|
+
console.log("[pltt] bundling frontend")
|
|
93
125
|
frontend = await bundleFrontend(cwd, manifest.frontend.entry)
|
|
94
|
-
console.log(`[
|
|
126
|
+
console.log(`[pltt] ${frontend.length} bytes`)
|
|
95
127
|
} else {
|
|
96
|
-
console.log("[
|
|
128
|
+
console.log("[pltt] no frontend declared")
|
|
97
129
|
}
|
|
98
130
|
|
|
99
|
-
console.log("[
|
|
131
|
+
console.log("[pltt] bundling backend")
|
|
100
132
|
const backend = await bundleBackend(cwd)
|
|
101
|
-
console.log(`[
|
|
133
|
+
console.log(`[pltt] ${backend.length} bytes`)
|
|
102
134
|
|
|
103
135
|
const backendSha = sha256(backend)
|
|
104
136
|
const api = makeApi(env)
|
|
105
137
|
|
|
106
|
-
console.log("[
|
|
138
|
+
console.log("[pltt] requesting signed URLs")
|
|
107
139
|
const signed = await api("/api/v1/appstore/sign-upload", {
|
|
108
140
|
method: "POST",
|
|
109
141
|
body: {
|
|
@@ -113,7 +145,7 @@ async function run(argv, { cwd }) {
|
|
|
113
145
|
},
|
|
114
146
|
})
|
|
115
147
|
|
|
116
|
-
console.log("[
|
|
148
|
+
console.log("[pltt] uploading")
|
|
117
149
|
const uploads = [
|
|
118
150
|
put(signed.backend_upload_url, backend, "application/gzip"),
|
|
119
151
|
put(
|
|
@@ -127,7 +159,7 @@ async function run(argv, { cwd }) {
|
|
|
127
159
|
}
|
|
128
160
|
await Promise.all(uploads)
|
|
129
161
|
|
|
130
|
-
console.log("[
|
|
162
|
+
console.log("[pltt] finalizing")
|
|
131
163
|
const record = await api("/api/v1/appstore/publish", {
|
|
132
164
|
method: "POST",
|
|
133
165
|
body: {
|
|
@@ -168,16 +200,16 @@ async function run(argv, { cwd }) {
|
|
|
168
200
|
}
|
|
169
201
|
|
|
170
202
|
console.log(
|
|
171
|
-
`[
|
|
203
|
+
`[pltt] published ${record.plugin_id}@${record.version} (status=${record.status})`,
|
|
172
204
|
)
|
|
173
|
-
console.log(`[
|
|
205
|
+
console.log(`[pltt] awaiting superadmin review on ${env.url}`)
|
|
174
206
|
if (record.review_url) {
|
|
175
|
-
console.log(`[
|
|
207
|
+
console.log(`[pltt] review queue: ${record.review_url}`)
|
|
176
208
|
}
|
|
177
209
|
if (record.preview_url) {
|
|
178
|
-
console.log(`[
|
|
210
|
+
console.log(`[pltt] preview: ${record.preview_url}`)
|
|
179
211
|
}
|
|
180
|
-
console.log(`[
|
|
212
|
+
console.log(`[pltt] once approved, live at ${env.url}${record.catalog_url}`)
|
|
181
213
|
}
|
|
182
214
|
|
|
183
215
|
module.exports = run
|
package/lib/commands/status.js
CHANGED
|
@@ -22,7 +22,7 @@ async function run(argv, { cwd }) {
|
|
|
22
22
|
|
|
23
23
|
if (!publishId) {
|
|
24
24
|
console.error(
|
|
25
|
-
"[
|
|
25
|
+
"[pltt] no publish id provided and no .palette/last-publish.json found.",
|
|
26
26
|
)
|
|
27
27
|
console.error(" usage: pltt status <publish-id> [--env <name>]")
|
|
28
28
|
process.exit(1)
|
|
@@ -35,13 +35,13 @@ async function run(argv, { cwd }) {
|
|
|
35
35
|
try {
|
|
36
36
|
env = resolveEnvironment({ cwd, flags })
|
|
37
37
|
} catch (err) {
|
|
38
|
-
console.error(`[
|
|
38
|
+
console.error(`[pltt] ${err.message}`)
|
|
39
39
|
process.exit(1)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (!env.token) {
|
|
43
43
|
console.error(
|
|
44
|
-
`[
|
|
44
|
+
`[pltt] no publish token for environment "${env.name}". set $${env.token_env}.`,
|
|
45
45
|
)
|
|
46
46
|
process.exit(1)
|
|
47
47
|
}
|
|
@@ -52,7 +52,7 @@ async function run(argv, { cwd }) {
|
|
|
52
52
|
})
|
|
53
53
|
if (!res.ok) {
|
|
54
54
|
const text = await res.text()
|
|
55
|
-
console.error(`[
|
|
55
|
+
console.error(`[pltt] GET ${url} → ${res.status}: ${text}`)
|
|
56
56
|
process.exit(1)
|
|
57
57
|
}
|
|
58
58
|
const data = await res.json()
|
|
@@ -62,7 +62,7 @@ async function run(argv, { cwd }) {
|
|
|
62
62
|
return
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
console.log(`[
|
|
65
|
+
console.log(`[pltt] ${data.plugin_id}@${data.version}`)
|
|
66
66
|
console.log(` status: ${data.status}`)
|
|
67
67
|
if (data.review_decision) console.log(` decision: ${data.review_decision}`)
|
|
68
68
|
if (data.review_reason) console.log(` reason: ${data.review_reason}`)
|
package/lib/commands/test.js
CHANGED
|
@@ -365,12 +365,12 @@ async function run(args, { cwd }) {
|
|
|
365
365
|
if (json) {
|
|
366
366
|
console.log(JSON.stringify({ ok: false, failures, results }, null, 2))
|
|
367
367
|
} else {
|
|
368
|
-
console.log(`\n[
|
|
368
|
+
console.log(`\n[pltt] test failed with ${failures} issue(s).`)
|
|
369
369
|
}
|
|
370
370
|
process.exit(1)
|
|
371
371
|
}
|
|
372
372
|
if (json) console.log(JSON.stringify({ ok: true, failures: 0, results }, null, 2))
|
|
373
|
-
else console.log("\n[
|
|
373
|
+
else console.log("\n[pltt] test passed.")
|
|
374
374
|
}
|
|
375
375
|
|
|
376
376
|
module.exports = run
|
package/lib/environments.js
CHANGED
|
@@ -52,7 +52,7 @@ function readJsonIfExists(p) {
|
|
|
52
52
|
if (!fs.existsSync(p)) return null
|
|
53
53
|
return JSON.parse(fs.readFileSync(p, "utf8"))
|
|
54
54
|
} catch (err) {
|
|
55
|
-
console.error(`[
|
|
55
|
+
console.error(`[pltt] failed to parse ${p}: ${err.message}`)
|
|
56
56
|
return null
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -152,7 +152,7 @@ async function confirmProduction(env) {
|
|
|
152
152
|
const readline = require("readline")
|
|
153
153
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
154
154
|
rl.question(
|
|
155
|
-
`\n[
|
|
155
|
+
`\n[pltt] You are about to publish to PRODUCTION (${env.url}).\n` +
|
|
156
156
|
` Type the environment name ("${env.name}") to confirm, anything else cancels: `,
|
|
157
157
|
(answer) => {
|
|
158
158
|
rl.close()
|
package/lib/ports.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
const net = require("net")
|
|
4
|
+
|
|
5
|
+
function canBindPort(port, host = "127.0.0.1") {
|
|
6
|
+
return new Promise((resolve) => {
|
|
7
|
+
const server = net.createServer()
|
|
8
|
+
server.once("error", () => resolve(false))
|
|
9
|
+
server.once("listening", () => {
|
|
10
|
+
server.close(() => resolve(true))
|
|
11
|
+
})
|
|
12
|
+
server.listen(port, host)
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function findFreePort(preferred, { host = "127.0.0.1", maxAttempts = 100 } = {}) {
|
|
17
|
+
const start = Number(preferred)
|
|
18
|
+
if (!Number.isInteger(start) || start <= 0 || start > 65535) {
|
|
19
|
+
throw new Error(`invalid port: ${preferred}`)
|
|
20
|
+
}
|
|
21
|
+
for (let port = start; port < start + maxAttempts && port <= 65535; port++) {
|
|
22
|
+
if (await canBindPort(port, host)) return port
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`no free port found from ${start} to ${Math.min(start + maxAttempts - 1, 65535)}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function resolveDevPorts({
|
|
28
|
+
frontend = process.env.PALETTE_FRONTEND_PORT || 3000,
|
|
29
|
+
backend = process.env.PALETTE_BACKEND_PORT || 8000,
|
|
30
|
+
} = {}) {
|
|
31
|
+
const frontendPort = await findFreePort(frontend)
|
|
32
|
+
const backendStart = Number(backend) === frontendPort ? frontendPort + 1 : backend
|
|
33
|
+
const backendPort = await findFreePort(backendStart)
|
|
34
|
+
return {
|
|
35
|
+
frontend: frontendPort,
|
|
36
|
+
backend: backendPort,
|
|
37
|
+
preferredFrontend: Number(frontend),
|
|
38
|
+
preferredBackend: Number(backend),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { canBindPort, findFreePort, resolveDevPorts }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@palettelab/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "Developer CLI for building Palette platform plugins — no platform source access required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"pltt": "bin/pltt.js"
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"esbuild": "^0.24.0"
|
|
23
23
|
},
|
|
24
24
|
"publishConfig": {
|
|
25
|
-
"registry": "https://
|
|
25
|
+
"registry": "https://registry.npmjs.org"
|
|
26
26
|
},
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|