@palettelab/cli 0.1.0 → 0.2.0
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/cli.js +9 -3
- package/lib/commands/publish.js +60 -35
- package/lib/environments.js +165 -0
- package/package.json +2 -1
- package/palette.config.example.json +21 -0
package/lib/cli.js
CHANGED
|
@@ -26,10 +26,16 @@ function printHelp() {
|
|
|
26
26
|
for (const [name, { help }] of Object.entries(COMMANDS)) {
|
|
27
27
|
console.log(` ${name.padEnd(8)} ${help}`)
|
|
28
28
|
}
|
|
29
|
+
console.log("\nPublish flags:")
|
|
30
|
+
console.log(" --env <name> Target environment from ~/.palette/config.json (default: local)")
|
|
31
|
+
console.log(" -y, --yes Skip interactive confirmation for production pushes")
|
|
29
32
|
console.log("\nExamples:")
|
|
30
|
-
console.log("
|
|
31
|
-
console.log(" cd my-app &&
|
|
32
|
-
console.log("
|
|
33
|
+
console.log(" palette init my-app")
|
|
34
|
+
console.log(" cd my-app && palette dev")
|
|
35
|
+
console.log(" palette build")
|
|
36
|
+
console.log(" palette publish # default (local)")
|
|
37
|
+
console.log(" palette publish --env staging")
|
|
38
|
+
console.log(" palette publish --env production # prompts for confirmation")
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
async function run(argv) {
|
package/lib/commands/publish.js
CHANGED
|
@@ -3,43 +3,35 @@
|
|
|
3
3
|
const crypto = require("crypto")
|
|
4
4
|
const { loadManifest, validateManifest } = require("../manifest")
|
|
5
5
|
const { bundleFrontend, bundleBackend } = require("../bundler")
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const {
|
|
7
|
+
resolveEnvironment,
|
|
8
|
+
parseFlags,
|
|
9
|
+
confirmProduction,
|
|
10
|
+
} = require("../environments")
|
|
9
11
|
|
|
10
12
|
function sha256(buf) {
|
|
11
13
|
return crypto.createHash("sha256").update(buf).digest("hex")
|
|
12
14
|
}
|
|
13
15
|
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
headers
|
|
31
|
-
|
|
32
|
-
"Content-Type": "application/json",
|
|
33
|
-
...headers,
|
|
34
|
-
},
|
|
35
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
36
|
-
})
|
|
37
|
-
if (!res.ok) {
|
|
38
|
-
const text = await res.text()
|
|
39
|
-
throw new Error(`${method} ${pathname} → ${res.status}: ${text}`)
|
|
16
|
+
function makeApi(env) {
|
|
17
|
+
return async function api(pathname, { method = "GET", body, headers = {} } = {}) {
|
|
18
|
+
const url = `${env.url}${pathname}`
|
|
19
|
+
const res = await fetch(url, {
|
|
20
|
+
method,
|
|
21
|
+
headers: {
|
|
22
|
+
Authorization: `Bearer ${env.token}`,
|
|
23
|
+
"Content-Type": "application/json",
|
|
24
|
+
...headers,
|
|
25
|
+
},
|
|
26
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
27
|
+
})
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
const text = await res.text()
|
|
30
|
+
throw new Error(`${method} ${pathname} → ${res.status}: ${text}`)
|
|
31
|
+
}
|
|
32
|
+
const ct = res.headers.get("content-type") || ""
|
|
33
|
+
return ct.includes("application/json") ? res.json() : res.text()
|
|
40
34
|
}
|
|
41
|
-
const ct = res.headers.get("content-type") || ""
|
|
42
|
-
return ct.includes("application/json") ? res.json() : res.text()
|
|
43
35
|
}
|
|
44
36
|
|
|
45
37
|
async function put(url, buf, contentType) {
|
|
@@ -53,7 +45,34 @@ async function put(url, buf, contentType) {
|
|
|
53
45
|
}
|
|
54
46
|
}
|
|
55
47
|
|
|
56
|
-
async function run(
|
|
48
|
+
async function run(argv, { cwd }) {
|
|
49
|
+
const { flags } = parseFlags(argv)
|
|
50
|
+
|
|
51
|
+
let env
|
|
52
|
+
try {
|
|
53
|
+
env = resolveEnvironment({ cwd, flags })
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(`[palette] ${err.message}`)
|
|
56
|
+
process.exit(1)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!env.token) {
|
|
60
|
+
console.error(
|
|
61
|
+
`[palette] no publish token for environment "${env.name}". ` +
|
|
62
|
+
`Set $${env.token_env} (or $PALETTE_PUBLISH_TOKEN as fallback).\n` +
|
|
63
|
+
` Ask a superadmin to mint one via POST ${env.url}/api/superadmin/publish-tokens.`,
|
|
64
|
+
)
|
|
65
|
+
process.exit(1)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (env.production_unconfirmed) {
|
|
69
|
+
const ok = await confirmProduction(env)
|
|
70
|
+
if (!ok) {
|
|
71
|
+
console.error("[palette] publish cancelled.")
|
|
72
|
+
process.exit(1)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
57
76
|
const manifest = loadManifest(cwd)
|
|
58
77
|
const errors = validateManifest(manifest)
|
|
59
78
|
if (errors.length) {
|
|
@@ -62,7 +81,9 @@ async function run(args, { cwd }) {
|
|
|
62
81
|
process.exit(1)
|
|
63
82
|
}
|
|
64
83
|
|
|
65
|
-
console.log(
|
|
84
|
+
console.log(
|
|
85
|
+
`[palette] publishing ${manifest.id}@${manifest.version} → ${env.name} (${env.url})`,
|
|
86
|
+
)
|
|
66
87
|
|
|
67
88
|
console.log("[palette] bundling frontend")
|
|
68
89
|
const frontend = await bundleFrontend(cwd, manifest.frontend?.entry || "./frontend/src/index.tsx")
|
|
@@ -73,6 +94,7 @@ async function run(args, { cwd }) {
|
|
|
73
94
|
console.log(`[palette] ${backend.length} bytes`)
|
|
74
95
|
|
|
75
96
|
const backendSha = sha256(backend)
|
|
97
|
+
const api = makeApi(env)
|
|
76
98
|
|
|
77
99
|
console.log("[palette] requesting signed URLs")
|
|
78
100
|
const signed = await api("/api/v1/appstore/sign-upload", {
|
|
@@ -107,8 +129,11 @@ async function run(args, { cwd }) {
|
|
|
107
129
|
},
|
|
108
130
|
})
|
|
109
131
|
|
|
110
|
-
console.log(
|
|
111
|
-
|
|
132
|
+
console.log(
|
|
133
|
+
`[palette] published ${record.plugin_id}@${record.version} (status=${record.status})`,
|
|
134
|
+
)
|
|
135
|
+
console.log(`[palette] awaiting superadmin review on ${env.url}`)
|
|
136
|
+
console.log(`[palette] once approved, live at ${env.url}${record.catalog_url}`)
|
|
112
137
|
}
|
|
113
138
|
|
|
114
139
|
module.exports = run
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Named environments for `palette publish`.
|
|
5
|
+
*
|
|
6
|
+
* Config is discovered in this order (first hit wins):
|
|
7
|
+
* 1. ./palette.config.json (plugin-repo-local override)
|
|
8
|
+
* 2. ~/.palette/config.json (per-user)
|
|
9
|
+
* 3. Built-in defaults (see DEFAULTS below)
|
|
10
|
+
*
|
|
11
|
+
* CLI env selection:
|
|
12
|
+
* 1. `--env <name>` flag
|
|
13
|
+
* 2. `PALETTE_ENV` env var
|
|
14
|
+
* 3. `default_environment` from config
|
|
15
|
+
* 4. "local"
|
|
16
|
+
*
|
|
17
|
+
* Each environment resolves to a platform URL and a publish token:
|
|
18
|
+
* url — absolute base URL of the target platform (no trailing slash)
|
|
19
|
+
* token_env — name of the env var holding the PALETTE_PUBLISH_TOKEN for this env
|
|
20
|
+
* production — boolean; requires `--env production` flag AND interactive
|
|
21
|
+
* confirmation (or `--yes` to skip). Keeps accidental prod
|
|
22
|
+
* pushes from happening.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const fs = require("fs")
|
|
26
|
+
const os = require("os")
|
|
27
|
+
const path = require("path")
|
|
28
|
+
|
|
29
|
+
const DEFAULTS = {
|
|
30
|
+
environments: {
|
|
31
|
+
local: {
|
|
32
|
+
url: "http://localhost:8000",
|
|
33
|
+
token_env: "PALETTE_LOCAL_TOKEN",
|
|
34
|
+
production: false,
|
|
35
|
+
},
|
|
36
|
+
staging: {
|
|
37
|
+
url: process.env.PALETTE_STAGING_URL || "",
|
|
38
|
+
token_env: "PALETTE_STAGING_TOKEN",
|
|
39
|
+
production: false,
|
|
40
|
+
},
|
|
41
|
+
production: {
|
|
42
|
+
url: process.env.PALETTE_PRODUCTION_URL || "",
|
|
43
|
+
token_env: "PALETTE_PROD_TOKEN",
|
|
44
|
+
production: true,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
default_environment: "local",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readJsonIfExists(p) {
|
|
51
|
+
try {
|
|
52
|
+
if (!fs.existsSync(p)) return null
|
|
53
|
+
return JSON.parse(fs.readFileSync(p, "utf8"))
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(`[palette] failed to parse ${p}: ${err.message}`)
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function loadConfig(cwd) {
|
|
61
|
+
const repoCfg = readJsonIfExists(path.join(cwd, "palette.config.json"))
|
|
62
|
+
if (repoCfg) return { ...DEFAULTS, ...repoCfg, environments: { ...DEFAULTS.environments, ...(repoCfg.environments || {}) } }
|
|
63
|
+
const userCfg = readJsonIfExists(path.join(os.homedir(), ".palette", "config.json"))
|
|
64
|
+
if (userCfg) return { ...DEFAULTS, ...userCfg, environments: { ...DEFAULTS.environments, ...(userCfg.environments || {}) } }
|
|
65
|
+
return DEFAULTS
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the effective environment.
|
|
70
|
+
*
|
|
71
|
+
* Returns { name, url, token, production, production_unconfirmed }:
|
|
72
|
+
* - name: resolved environment name
|
|
73
|
+
* - url: platform URL
|
|
74
|
+
* - token: publish token (may be "" if unset — caller decides whether to exit)
|
|
75
|
+
* - production: boolean from config
|
|
76
|
+
* - production_unconfirmed: true iff production AND --yes wasn't passed;
|
|
77
|
+
* caller is expected to prompt.
|
|
78
|
+
*/
|
|
79
|
+
function resolveEnvironment({ cwd, flags }) {
|
|
80
|
+
const cfg = loadConfig(cwd)
|
|
81
|
+
const requested =
|
|
82
|
+
flags.env ||
|
|
83
|
+
process.env.PALETTE_ENV ||
|
|
84
|
+
cfg.default_environment ||
|
|
85
|
+
"local"
|
|
86
|
+
|
|
87
|
+
const envCfg = cfg.environments[requested]
|
|
88
|
+
if (!envCfg) {
|
|
89
|
+
const available = Object.keys(cfg.environments).join(", ")
|
|
90
|
+
throw new Error(
|
|
91
|
+
`unknown environment "${requested}". Available: ${available}. ` +
|
|
92
|
+
`Configure via ./palette.config.json or ~/.palette/config.json.`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Backward-compat: PALETTE_PLATFORM_URL / PALETTE_PUBLISH_TOKEN override
|
|
97
|
+
// per-env resolution when the user hasn't picked an env explicitly.
|
|
98
|
+
const useLegacy =
|
|
99
|
+
!flags.env &&
|
|
100
|
+
!process.env.PALETTE_ENV &&
|
|
101
|
+
process.env.PALETTE_PLATFORM_URL
|
|
102
|
+
|
|
103
|
+
const url = useLegacy
|
|
104
|
+
? process.env.PALETTE_PLATFORM_URL
|
|
105
|
+
: envCfg.url
|
|
106
|
+
|
|
107
|
+
const token = useLegacy
|
|
108
|
+
? process.env.PALETTE_PUBLISH_TOKEN || ""
|
|
109
|
+
: process.env[envCfg.token_env] || process.env.PALETTE_PUBLISH_TOKEN || ""
|
|
110
|
+
|
|
111
|
+
if (!url) {
|
|
112
|
+
throw new Error(
|
|
113
|
+
`environment "${requested}" has no url configured. Set it in ` +
|
|
114
|
+
`~/.palette/config.json or set PALETTE_${requested.toUpperCase()}_URL.`,
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
name: requested,
|
|
120
|
+
url: url.replace(/\/+$/, ""),
|
|
121
|
+
token,
|
|
122
|
+
production: Boolean(envCfg.production),
|
|
123
|
+
production_unconfirmed: Boolean(envCfg.production) && !flags.yes,
|
|
124
|
+
token_env: envCfg.token_env,
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Minimal arg parser — extracts `--env <name>`, `--yes`, `-y` from argv.
|
|
130
|
+
* Leaves positional args in `rest`.
|
|
131
|
+
*/
|
|
132
|
+
function parseFlags(argv) {
|
|
133
|
+
const flags = { env: undefined, yes: false }
|
|
134
|
+
const rest = []
|
|
135
|
+
for (let i = 0; i < argv.length; i++) {
|
|
136
|
+
const a = argv[i]
|
|
137
|
+
if (a === "--env" || a === "-e") {
|
|
138
|
+
flags.env = argv[++i]
|
|
139
|
+
} else if (a.startsWith("--env=")) {
|
|
140
|
+
flags.env = a.slice("--env=".length)
|
|
141
|
+
} else if (a === "--yes" || a === "-y") {
|
|
142
|
+
flags.yes = true
|
|
143
|
+
} else {
|
|
144
|
+
rest.push(a)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return { flags, rest }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function confirmProduction(env) {
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
const readline = require("readline")
|
|
153
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
154
|
+
rl.question(
|
|
155
|
+
`\n[palette] You are about to publish to PRODUCTION (${env.url}).\n` +
|
|
156
|
+
` Type the environment name ("${env.name}") to confirm, anything else cancels: `,
|
|
157
|
+
(answer) => {
|
|
158
|
+
rl.close()
|
|
159
|
+
resolve(answer.trim() === env.name)
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = { resolveEnvironment, parseFlags, confirmProduction, loadConfig }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@palettelab/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Developer CLI for building Palette platform plugins — no platform source access required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"palette": "./bin/palette.js"
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"lib",
|
|
11
11
|
"platform-dev",
|
|
12
12
|
"template-fallback",
|
|
13
|
+
"palette.config.example.json",
|
|
13
14
|
"README.md"
|
|
14
15
|
],
|
|
15
16
|
"engines": {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Copy to ./palette.config.json (per-plugin) or ~/.palette/config.json (per-user). Delete this comment line and entries you don't need.",
|
|
3
|
+
"default_environment": "local",
|
|
4
|
+
"environments": {
|
|
5
|
+
"local": {
|
|
6
|
+
"url": "http://localhost:8000",
|
|
7
|
+
"token_env": "PALETTE_LOCAL_TOKEN",
|
|
8
|
+
"production": false
|
|
9
|
+
},
|
|
10
|
+
"staging": {
|
|
11
|
+
"url": "https://staging.yourcompany.example",
|
|
12
|
+
"token_env": "PALETTE_STAGING_TOKEN",
|
|
13
|
+
"production": false
|
|
14
|
+
},
|
|
15
|
+
"production": {
|
|
16
|
+
"url": "https://app.yourcompany.example",
|
|
17
|
+
"token_env": "PALETTE_PROD_TOKEN",
|
|
18
|
+
"production": true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|