@rodyssey/cli 0.1.10 → 0.2.1
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/dist/cli.js +495 -43
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2071,7 +2071,7 @@ var {
|
|
|
2071
2071
|
// package.json
|
|
2072
2072
|
var package_default = {
|
|
2073
2073
|
name: "@rodyssey/cli",
|
|
2074
|
-
version: "0.1
|
|
2074
|
+
version: "0.2.1",
|
|
2075
2075
|
description: "Scaffold new projects from airconcepts templates",
|
|
2076
2076
|
repository: {
|
|
2077
2077
|
type: "git",
|
|
@@ -2185,6 +2185,77 @@ function buildPkcePair() {
|
|
|
2185
2185
|
const challenge = base64Url(createHash("sha256").update(verifier).digest());
|
|
2186
2186
|
return { verifier, challenge };
|
|
2187
2187
|
}
|
|
2188
|
+
function escapeHtml(value) {
|
|
2189
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
2190
|
+
}
|
|
2191
|
+
function renderCallbackPage(variant, title, message) {
|
|
2192
|
+
const accent = variant === "success" ? "#2f9e44" : "#e03131";
|
|
2193
|
+
return `<!doctype html>
|
|
2194
|
+
<html lang="en">
|
|
2195
|
+
<head>
|
|
2196
|
+
<meta charset="utf-8" />
|
|
2197
|
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
|
2198
|
+
<title>Rodyssey CLI</title>
|
|
2199
|
+
<style>
|
|
2200
|
+
:root { color-scheme: light; }
|
|
2201
|
+
* { box-sizing: border-box; }
|
|
2202
|
+
html, body { margin: 0; padding: 0; height: 100%; }
|
|
2203
|
+
body {
|
|
2204
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
2205
|
+
background: #f8f9fa;
|
|
2206
|
+
color: #1a1b1e;
|
|
2207
|
+
display: flex;
|
|
2208
|
+
align-items: center;
|
|
2209
|
+
justify-content: center;
|
|
2210
|
+
padding: 24px;
|
|
2211
|
+
min-height: 100vh;
|
|
2212
|
+
}
|
|
2213
|
+
.card {
|
|
2214
|
+
background: #ffffff;
|
|
2215
|
+
border: 1px solid #e9ecef;
|
|
2216
|
+
border-radius: 16px;
|
|
2217
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.05), 0 4px 12px rgba(0,0,0,0.06);
|
|
2218
|
+
padding: 32px;
|
|
2219
|
+
max-width: 420px;
|
|
2220
|
+
width: 100%;
|
|
2221
|
+
text-align: center;
|
|
2222
|
+
}
|
|
2223
|
+
.badge {
|
|
2224
|
+
display: inline-flex;
|
|
2225
|
+
align-items: center;
|
|
2226
|
+
justify-content: center;
|
|
2227
|
+
width: 48px;
|
|
2228
|
+
height: 48px;
|
|
2229
|
+
border-radius: 999px;
|
|
2230
|
+
background: ${accent}1a;
|
|
2231
|
+
color: ${accent};
|
|
2232
|
+
margin-bottom: 16px;
|
|
2233
|
+
font-size: 24px;
|
|
2234
|
+
line-height: 1;
|
|
2235
|
+
}
|
|
2236
|
+
h1 {
|
|
2237
|
+
font-size: 20px;
|
|
2238
|
+
font-weight: 600;
|
|
2239
|
+
margin: 0 0 8px;
|
|
2240
|
+
letter-spacing: -0.01em;
|
|
2241
|
+
}
|
|
2242
|
+
p {
|
|
2243
|
+
margin: 0;
|
|
2244
|
+
color: #495057;
|
|
2245
|
+
font-size: 14px;
|
|
2246
|
+
line-height: 1.5;
|
|
2247
|
+
}
|
|
2248
|
+
</style>
|
|
2249
|
+
</head>
|
|
2250
|
+
<body>
|
|
2251
|
+
<div class="card" role="status">
|
|
2252
|
+
<div class="badge" aria-hidden="true">${variant === "success" ? "✓" : "!"}</div>
|
|
2253
|
+
<h1>${escapeHtml(title)}</h1>
|
|
2254
|
+
<p>${escapeHtml(message)}</p>
|
|
2255
|
+
</div>
|
|
2256
|
+
</body>
|
|
2257
|
+
</html>`;
|
|
2258
|
+
}
|
|
2188
2259
|
function openBrowser(url) {
|
|
2189
2260
|
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
2190
2261
|
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
@@ -2230,21 +2301,23 @@ async function login(options) {
|
|
|
2230
2301
|
const code = requestUrl.searchParams.get("code");
|
|
2231
2302
|
const returnedState = requestUrl.searchParams.get("state");
|
|
2232
2303
|
const error = requestUrl.searchParams.get("error");
|
|
2304
|
+
const errorDescription = requestUrl.searchParams.get("error_description");
|
|
2233
2305
|
if (error) {
|
|
2306
|
+
const detail = errorDescription ? `${error}: ${errorDescription}` : error;
|
|
2234
2307
|
clearTimeout(timer);
|
|
2235
|
-
response2.writeHead(400, { "Content-Type": "text/
|
|
2308
|
+
response2.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }).end(renderCallbackPage("error", "Login failed", errorDescription || error));
|
|
2236
2309
|
server.close();
|
|
2237
|
-
reject(new Error(`CMS login failed: ${
|
|
2310
|
+
reject(new Error(`CMS login failed: ${detail}`));
|
|
2238
2311
|
return;
|
|
2239
2312
|
}
|
|
2240
2313
|
if (!code || returnedState !== state) {
|
|
2241
2314
|
clearTimeout(timer);
|
|
2242
|
-
response2.writeHead(400, { "Content-Type": "text/
|
|
2315
|
+
response2.writeHead(400, { "Content-Type": "text/html; charset=utf-8" }).end(renderCallbackPage("error", "Invalid login callback", "The login response was missing a code or had a mismatched state. Please try again from the terminal."));
|
|
2243
2316
|
server.close();
|
|
2244
2317
|
reject(new Error("Invalid CMS login callback. Missing code or state mismatch."));
|
|
2245
2318
|
return;
|
|
2246
2319
|
}
|
|
2247
|
-
response2.writeHead(200, { "Content-Type": "text/html" }).end(
|
|
2320
|
+
response2.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(renderCallbackPage("success", "Login complete", "You can close this window and return to the terminal."));
|
|
2248
2321
|
const address = server.address();
|
|
2249
2322
|
clearTimeout(timer);
|
|
2250
2323
|
server.close();
|
|
@@ -3731,8 +3804,15 @@ var DEPLOY_URLS = {
|
|
|
3731
3804
|
};
|
|
3732
3805
|
var BUILD_DIR = "dist";
|
|
3733
3806
|
var ZIP_FILE = "webapp-build.zip";
|
|
3807
|
+
var FULLSTACK_DEPLOY_ENVS = new Set(["development", "staging", "production"]);
|
|
3734
3808
|
var MAX_FILES_PER_BATCH = 5;
|
|
3735
3809
|
var MAX_SIZE_PER_BATCH = 30 * 1024 * 1024;
|
|
3810
|
+
function shellQuote(value) {
|
|
3811
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
3812
|
+
}
|
|
3813
|
+
function isFullstackProject() {
|
|
3814
|
+
return existsSync4("app") && existsSync4("workers/app.ts") && existsSync4("wrangler.jsonc");
|
|
3815
|
+
}
|
|
3736
3816
|
function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
3737
3817
|
if (!existsSync4(dirPath))
|
|
3738
3818
|
return arrayOfFiles;
|
|
@@ -3751,8 +3831,47 @@ function fileToBlob(filePath) {
|
|
|
3751
3831
|
const buffer = readFileSync3(filePath);
|
|
3752
3832
|
return new Blob([buffer], { type: src_default.getType(filePath) || "application/octet-stream" });
|
|
3753
3833
|
}
|
|
3754
|
-
async function
|
|
3755
|
-
|
|
3834
|
+
async function deployFullstack(env, overrides) {
|
|
3835
|
+
if (env === "local") {
|
|
3836
|
+
console.error("❌ Fullstack projects deploy to Cloudflare Workers and do not support the local CMS deploy target.");
|
|
3837
|
+
process.exit(1);
|
|
3838
|
+
}
|
|
3839
|
+
if (!FULLSTACK_DEPLOY_ENVS.has(env)) {
|
|
3840
|
+
console.error(`❌ Unknown fullstack environment "${env}". Available: ${Array.from(FULLSTACK_DEPLOY_ENVS).join(", ")}`);
|
|
3841
|
+
process.exit(1);
|
|
3842
|
+
}
|
|
3843
|
+
if (!process.env.DEPLOY_TOKEN) {
|
|
3844
|
+
console.error("❌ Error: DEPLOY_TOKEN is not set in environment variables.");
|
|
3845
|
+
console.info(`\uD83D\uDCA1 Please check your .env or .env.${env} file.`);
|
|
3846
|
+
process.exit(1);
|
|
3847
|
+
}
|
|
3848
|
+
const childEnv = {
|
|
3849
|
+
...process.env,
|
|
3850
|
+
CLOUDFLARE_ENV: env
|
|
3851
|
+
};
|
|
3852
|
+
console.log(`\uD83D\uDE80 Starting fullstack deployment process for [${env}] environment...
|
|
3853
|
+
`);
|
|
3854
|
+
console.log("\uD83D\uDCE6 Step 1: Building the fullstack webapp...");
|
|
3855
|
+
execSync2("bun run build", { stdio: "inherit", env: childEnv });
|
|
3856
|
+
console.log(`✅ Build completed
|
|
3857
|
+
`);
|
|
3858
|
+
console.log("☁️ Step 2: Deploying Cloudflare Worker...");
|
|
3859
|
+
const wranglerEnvArgs = env === "development" ? "" : ` --env ${shellQuote(env)}`;
|
|
3860
|
+
execSync2(`bun run wrangler deploy${wranglerEnvArgs}`, { stdio: "inherit", env: childEnv });
|
|
3861
|
+
console.log(`✅ Worker deployed
|
|
3862
|
+
`);
|
|
3863
|
+
console.log("\uD83D\uDCCB Step 3: Syncing widget manifest to CMS config...");
|
|
3864
|
+
const syncArgs = [
|
|
3865
|
+
"--env",
|
|
3866
|
+
shellQuote(env),
|
|
3867
|
+
...overrides.host ? ["--host", shellQuote(overrides.host)] : [],
|
|
3868
|
+
...overrides.port ? ["--port", shellQuote(String(overrides.port))] : []
|
|
3869
|
+
];
|
|
3870
|
+
execSync2(`bun run sync-widget-manifest -- ${syncArgs.join(" ")}`, { stdio: "inherit", env: childEnv });
|
|
3871
|
+
console.log(`
|
|
3872
|
+
✨ Fullstack deployment successful!`);
|
|
3873
|
+
}
|
|
3874
|
+
async function deploySpa(env, overrides) {
|
|
3756
3875
|
let DEPLOY_URL = DEPLOY_URLS[env];
|
|
3757
3876
|
if (!DEPLOY_URL) {
|
|
3758
3877
|
console.error(`❌ Unknown environment "${env}". Available: ${Object.keys(DEPLOY_URLS).join(", ")}`);
|
|
@@ -3907,14 +4026,336 @@ ${errorText}`);
|
|
|
3907
4026
|
console.log(`
|
|
3908
4027
|
✨ Deployment successful!`);
|
|
3909
4028
|
}
|
|
4029
|
+
async function deploy(env = "development", overrides = {}) {
|
|
4030
|
+
loadEnv(env);
|
|
4031
|
+
if (isFullstackProject()) {
|
|
4032
|
+
await deployFullstack(env, overrides);
|
|
4033
|
+
return;
|
|
4034
|
+
}
|
|
4035
|
+
await deploySpa(env, overrides);
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
// src/global-config.ts
|
|
4039
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4040
|
+
import { resolve } from "node:path";
|
|
4041
|
+
var PROD_ENV = "production";
|
|
4042
|
+
function isPlainObject(value) {
|
|
4043
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
4044
|
+
}
|
|
4045
|
+
function applyMergePatch(target, patch) {
|
|
4046
|
+
if (!isPlainObject(patch))
|
|
4047
|
+
return patch;
|
|
4048
|
+
const base = isPlainObject(target) ? { ...target } : {};
|
|
4049
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
4050
|
+
if (value === null) {
|
|
4051
|
+
delete base[key];
|
|
4052
|
+
} else {
|
|
4053
|
+
base[key] = applyMergePatch(base[key], value);
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
return base;
|
|
4057
|
+
}
|
|
4058
|
+
function parseFlag(name, value) {
|
|
4059
|
+
if (value === "null")
|
|
4060
|
+
return null;
|
|
4061
|
+
const candidatePath = resolve(process.cwd(), value);
|
|
4062
|
+
const raw = existsSync5(candidatePath) ? readFileSync4(candidatePath, "utf-8") : value;
|
|
4063
|
+
try {
|
|
4064
|
+
return JSON.parse(raw);
|
|
4065
|
+
} catch (error) {
|
|
4066
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4067
|
+
throw new Error(`Invalid --${name}: ${message}`);
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
function buildPayload(options) {
|
|
4071
|
+
const payload = {};
|
|
4072
|
+
if (options.config !== undefined) {
|
|
4073
|
+
payload.config = parseFlag("config", options.config);
|
|
4074
|
+
}
|
|
4075
|
+
if (options.publicConfig !== undefined) {
|
|
4076
|
+
payload.publicConfig = parseFlag("public-config", options.publicConfig);
|
|
4077
|
+
}
|
|
4078
|
+
if (payload.config === undefined && payload.publicConfig === undefined) {
|
|
4079
|
+
throw new Error("Provide at least one of --config or --public-config.");
|
|
4080
|
+
}
|
|
4081
|
+
return payload;
|
|
4082
|
+
}
|
|
4083
|
+
function resolveEndpoint(env, override, cmsUrl) {
|
|
4084
|
+
if (override)
|
|
4085
|
+
return override;
|
|
4086
|
+
return `${resolveCmsUrl(env, cmsUrl)}/api/cli/cms/global-config`;
|
|
4087
|
+
}
|
|
4088
|
+
async function prompt(question) {
|
|
4089
|
+
const readline = await import("node:readline");
|
|
4090
|
+
const rl = readline.createInterface({
|
|
4091
|
+
input: process.stdin,
|
|
4092
|
+
output: process.stdout
|
|
4093
|
+
});
|
|
4094
|
+
return new Promise((resolveAnswer) => {
|
|
4095
|
+
rl.question(question, (answer) => {
|
|
4096
|
+
rl.close();
|
|
4097
|
+
resolveAnswer(answer);
|
|
4098
|
+
});
|
|
4099
|
+
});
|
|
4100
|
+
}
|
|
4101
|
+
function isExplicitYes(answer) {
|
|
4102
|
+
const trimmed = answer.trim().toLowerCase();
|
|
4103
|
+
return trimmed === "y" || trimmed === "yes";
|
|
4104
|
+
}
|
|
4105
|
+
function pretty(value) {
|
|
4106
|
+
return JSON.stringify(value, null, 2);
|
|
4107
|
+
}
|
|
4108
|
+
function compact(value) {
|
|
4109
|
+
return JSON.stringify(value);
|
|
4110
|
+
}
|
|
4111
|
+
function useColor() {
|
|
4112
|
+
return !!process.stdout.isTTY && !process.env.NO_COLOR;
|
|
4113
|
+
}
|
|
4114
|
+
function paint(code, text) {
|
|
4115
|
+
return useColor() ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
4116
|
+
}
|
|
4117
|
+
var green = (s) => paint("32", s);
|
|
4118
|
+
var red = (s) => paint("31", s);
|
|
4119
|
+
var yellow = (s) => paint("33", s);
|
|
4120
|
+
var dim = (s) => paint("2", s);
|
|
4121
|
+
var strike = (s) => paint("9", s);
|
|
4122
|
+
var COLUMN_LABEL = {
|
|
4123
|
+
config: "Config",
|
|
4124
|
+
publicConfig: "Public Config"
|
|
4125
|
+
};
|
|
4126
|
+
function pathToDot(path3) {
|
|
4127
|
+
return path3.replace(/^\//, "").replaceAll("/", ".");
|
|
4128
|
+
}
|
|
4129
|
+
function deepEqual(a, b) {
|
|
4130
|
+
if (a === b)
|
|
4131
|
+
return true;
|
|
4132
|
+
if (typeof a !== typeof b)
|
|
4133
|
+
return false;
|
|
4134
|
+
if (a === null || b === null)
|
|
4135
|
+
return false;
|
|
4136
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
4137
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
4138
|
+
return false;
|
|
4139
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
4140
|
+
}
|
|
4141
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
4142
|
+
const ak = Object.keys(a);
|
|
4143
|
+
const bk = Object.keys(b);
|
|
4144
|
+
if (ak.length !== bk.length)
|
|
4145
|
+
return false;
|
|
4146
|
+
return ak.every((k) => deepEqual(a[k], b[k]));
|
|
4147
|
+
}
|
|
4148
|
+
return false;
|
|
4149
|
+
}
|
|
4150
|
+
function diffJson(before, after, path3 = "") {
|
|
4151
|
+
if (deepEqual(before, after))
|
|
4152
|
+
return [];
|
|
4153
|
+
if (before === undefined)
|
|
4154
|
+
return [{ path: path3, kind: "add", after }];
|
|
4155
|
+
if (after === undefined)
|
|
4156
|
+
return [{ path: path3, kind: "remove", before }];
|
|
4157
|
+
if (!isPlainObject(before) || !isPlainObject(after)) {
|
|
4158
|
+
return [{ path: path3, kind: "change", before, after }];
|
|
4159
|
+
}
|
|
4160
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
4161
|
+
const out = [];
|
|
4162
|
+
for (const key of keys) {
|
|
4163
|
+
out.push(...diffJson(before[key], after[key], `${path3}/${key}`));
|
|
4164
|
+
}
|
|
4165
|
+
return out;
|
|
4166
|
+
}
|
|
4167
|
+
function buildColumnDeltas(current, payload, method) {
|
|
4168
|
+
const out = [];
|
|
4169
|
+
for (const column of ["config", "publicConfig"]) {
|
|
4170
|
+
if (payload[column] === undefined)
|
|
4171
|
+
continue;
|
|
4172
|
+
const cur = current[column] ?? null;
|
|
4173
|
+
let projected;
|
|
4174
|
+
if (method === "PUT" || payload[column] === null) {
|
|
4175
|
+
projected = payload[column] ?? null;
|
|
4176
|
+
} else {
|
|
4177
|
+
projected = applyMergePatch(cur, payload[column]);
|
|
4178
|
+
}
|
|
4179
|
+
const deltas = diffJson(cur, projected);
|
|
4180
|
+
for (const d of deltas) {
|
|
4181
|
+
out.push({ ...d, column });
|
|
4182
|
+
}
|
|
4183
|
+
}
|
|
4184
|
+
return out;
|
|
4185
|
+
}
|
|
4186
|
+
function deltaLine(d, showColumnTag) {
|
|
4187
|
+
const tag = showColumnTag ? `[${COLUMN_LABEL[d.column]}] ` : "";
|
|
4188
|
+
const path3 = pathToDot(d.path) || `(entire ${COLUMN_LABEL[d.column]})`;
|
|
4189
|
+
if (d.kind === "add") {
|
|
4190
|
+
return green(` ${tag}${path3}: ${compact(d.after)}`);
|
|
4191
|
+
}
|
|
4192
|
+
if (d.kind === "change") {
|
|
4193
|
+
return yellow(` ${tag}${path3}: ${strike(compact(d.before))} → ${compact(d.after)}`);
|
|
4194
|
+
}
|
|
4195
|
+
return red(strike(` ${tag}${path3}: ${compact(d.before)}`));
|
|
4196
|
+
}
|
|
4197
|
+
function formatDelta(deltas, patchedColumns, verb) {
|
|
4198
|
+
const colNames = patchedColumns.map((c) => COLUMN_LABEL[c]).join(" / ");
|
|
4199
|
+
const header = `${verb} on: ${colNames}`;
|
|
4200
|
+
if (deltas.length === 0) {
|
|
4201
|
+
return `${header}
|
|
4202
|
+
${dim(" (no changes)")}`;
|
|
4203
|
+
}
|
|
4204
|
+
const showTag = patchedColumns.length > 1;
|
|
4205
|
+
const adds = deltas.filter((d) => d.kind === "add");
|
|
4206
|
+
const changes = deltas.filter((d) => d.kind === "change");
|
|
4207
|
+
const removes = deltas.filter((d) => d.kind === "remove");
|
|
4208
|
+
const sections = [];
|
|
4209
|
+
if (adds.length > 0) {
|
|
4210
|
+
sections.push([green("New"), ...adds.map((d) => deltaLine(d, showTag))].join(`
|
|
4211
|
+
`));
|
|
4212
|
+
}
|
|
4213
|
+
if (changes.length > 0) {
|
|
4214
|
+
sections.push([yellow("Update"), ...changes.map((d) => deltaLine(d, showTag))].join(`
|
|
4215
|
+
`));
|
|
4216
|
+
}
|
|
4217
|
+
if (removes.length > 0) {
|
|
4218
|
+
sections.push([red("Delete"), ...removes.map((d) => deltaLine(d, showTag))].join(`
|
|
4219
|
+
`));
|
|
4220
|
+
}
|
|
4221
|
+
return `${header}
|
|
4222
|
+
|
|
4223
|
+
${sections.join(`
|
|
4224
|
+
|
|
4225
|
+
`)}`;
|
|
4226
|
+
}
|
|
4227
|
+
async function resolveAuth(env, cmsUrl, needsWrite) {
|
|
4228
|
+
if (env === PROD_ENV && needsWrite) {
|
|
4229
|
+
console.log(`
|
|
4230
|
+
\uD83D\uDD10 Logging in to ${PROD_ENV} CMS (ephemeral, not stored)...`);
|
|
4231
|
+
const session = await login({ env, cmsUrl, persist: false });
|
|
4232
|
+
console.log(`✅ Logged in to ${session.cmsUrl}`);
|
|
4233
|
+
return { url: session.cmsUrl, token: session.token };
|
|
4234
|
+
}
|
|
4235
|
+
return {
|
|
4236
|
+
url: resolveCmsUrl(env, cmsUrl),
|
|
4237
|
+
token: resolveSessionToken(env)
|
|
4238
|
+
};
|
|
4239
|
+
}
|
|
4240
|
+
async function getGlobalConfig(options) {
|
|
4241
|
+
const url = resolveEndpoint(options.env, options.url, options.cmsUrl);
|
|
4242
|
+
const token = resolveSessionToken(options.env);
|
|
4243
|
+
const response = await fetch(url, {
|
|
4244
|
+
method: "GET",
|
|
4245
|
+
headers: {
|
|
4246
|
+
Accept: "application/json",
|
|
4247
|
+
Authorization: `Bearer ${token}`
|
|
4248
|
+
}
|
|
4249
|
+
});
|
|
4250
|
+
const payload = await readResponsePayload(response);
|
|
4251
|
+
if (!response.ok) {
|
|
4252
|
+
throw new Error(`GET global-config failed: ${response.status} ${response.statusText}
|
|
4253
|
+
${pretty(payload)}`);
|
|
4254
|
+
}
|
|
4255
|
+
const text = pretty(payload);
|
|
4256
|
+
if (options.out) {
|
|
4257
|
+
const outPath = resolve(process.cwd(), options.out);
|
|
4258
|
+
writeFileSync3(outPath, `${text}
|
|
4259
|
+
`, "utf-8");
|
|
4260
|
+
console.log(`✅ Wrote global config to ${outPath}`);
|
|
4261
|
+
} else {
|
|
4262
|
+
console.log(text);
|
|
4263
|
+
}
|
|
4264
|
+
return payload;
|
|
4265
|
+
}
|
|
4266
|
+
async function writeGlobalConfig(method, options) {
|
|
4267
|
+
const payload = buildPayload(options);
|
|
4268
|
+
const verb = method === "PUT" ? "Replacing" : "Patching";
|
|
4269
|
+
const patchedColumns = Object.keys(payload).filter((k) => payload[k] !== undefined);
|
|
4270
|
+
try {
|
|
4271
|
+
console.log(`\uD83D\uDCE5 Fetching current global-config from [${options.env}]...
|
|
4272
|
+
`);
|
|
4273
|
+
const current = await getGlobalConfigSilent(options.env, options.cmsUrl, options.url);
|
|
4274
|
+
const deltas = buildColumnDeltas(current, payload, method);
|
|
4275
|
+
console.log(formatDelta(deltas, patchedColumns, verb));
|
|
4276
|
+
} catch (error) {
|
|
4277
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4278
|
+
console.warn(`⚠️ Could not preview changes: ${message}`);
|
|
4279
|
+
console.log(`
|
|
4280
|
+
\uD83D\uDCE6 Raw payload:`);
|
|
4281
|
+
console.log(pretty(payload));
|
|
4282
|
+
}
|
|
4283
|
+
if (options.dryRun) {
|
|
4284
|
+
console.log(dim(`
|
|
4285
|
+
↷ Dry run — no request sent.`));
|
|
4286
|
+
return;
|
|
4287
|
+
}
|
|
4288
|
+
const tty = !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
4289
|
+
if (!options.yes) {
|
|
4290
|
+
if (!tty) {
|
|
4291
|
+
throw new Error("Refusing to send write in non-interactive mode. Pass --yes to confirm or --dry-run to preview.");
|
|
4292
|
+
}
|
|
4293
|
+
const answer = await prompt(`
|
|
4294
|
+
Proceed with ${verb.toLowerCase()} on [${options.env}]? (y/N): `);
|
|
4295
|
+
if (!isExplicitYes(answer)) {
|
|
4296
|
+
console.log("✋ Aborted.");
|
|
4297
|
+
return;
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
const auth = await resolveAuth(options.env, options.cmsUrl, true);
|
|
4301
|
+
const endpoint = options.url || `${auth.url}/api/cli/cms/global-config`;
|
|
4302
|
+
console.log(`
|
|
4303
|
+
\uD83D\uDE80 ${method} ${endpoint}`);
|
|
4304
|
+
const response = await fetch(endpoint, {
|
|
4305
|
+
method,
|
|
4306
|
+
headers: {
|
|
4307
|
+
Accept: "application/json",
|
|
4308
|
+
Authorization: `Bearer ${auth.token}`,
|
|
4309
|
+
"Content-Type": "application/json"
|
|
4310
|
+
},
|
|
4311
|
+
body: JSON.stringify(payload)
|
|
4312
|
+
});
|
|
4313
|
+
const responseBody = await readResponsePayload(response);
|
|
4314
|
+
if (!response.ok) {
|
|
4315
|
+
throw new Error(`${method} global-config failed: ${response.status} ${response.statusText}
|
|
4316
|
+
${pretty(responseBody)}`);
|
|
4317
|
+
}
|
|
4318
|
+
console.log(`
|
|
4319
|
+
✅ Updated global config:`);
|
|
4320
|
+
console.log(pretty(responseBody));
|
|
4321
|
+
}
|
|
4322
|
+
async function getGlobalConfigSilent(env, cmsUrl, urlOverride) {
|
|
4323
|
+
const url = resolveEndpoint(env, urlOverride, cmsUrl);
|
|
4324
|
+
const token = resolveSessionToken(env);
|
|
4325
|
+
const response = await fetch(url, {
|
|
4326
|
+
method: "GET",
|
|
4327
|
+
headers: {
|
|
4328
|
+
Accept: "application/json",
|
|
4329
|
+
Authorization: `Bearer ${token}`
|
|
4330
|
+
}
|
|
4331
|
+
});
|
|
4332
|
+
const payload = await readResponsePayload(response);
|
|
4333
|
+
if (!response.ok) {
|
|
4334
|
+
throw new Error(`GET global-config failed: ${response.status} ${response.statusText}
|
|
4335
|
+
${pretty(payload)}`);
|
|
4336
|
+
}
|
|
4337
|
+
if (!isPlainObject(payload)) {
|
|
4338
|
+
throw new Error("Unexpected response: body is not an object.");
|
|
4339
|
+
}
|
|
4340
|
+
return {
|
|
4341
|
+
config: payload.config ?? null,
|
|
4342
|
+
publicConfig: payload.publicConfig ?? null
|
|
4343
|
+
};
|
|
4344
|
+
}
|
|
4345
|
+
async function setGlobalConfig(options) {
|
|
4346
|
+
await writeGlobalConfig("PUT", options);
|
|
4347
|
+
}
|
|
4348
|
+
async function patchGlobalConfig(options) {
|
|
4349
|
+
await writeGlobalConfig("PATCH", options);
|
|
4350
|
+
}
|
|
3910
4351
|
|
|
3911
4352
|
// src/promote.ts
|
|
3912
|
-
import { existsSync as
|
|
3913
|
-
import { resolve as
|
|
4353
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
4354
|
+
import { resolve as resolve3 } from "node:path";
|
|
3914
4355
|
|
|
3915
4356
|
// src/update-webapp-config.ts
|
|
3916
|
-
import { existsSync as
|
|
3917
|
-
import { resolve } from "node:path";
|
|
4357
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4358
|
+
import { resolve as resolve2 } from "node:path";
|
|
3918
4359
|
var CONFIG_URLS = {
|
|
3919
4360
|
local: "http://localhost:5176/api/webapps/config",
|
|
3920
4361
|
development: "https://development-cms.rodyssey.ai/api/webapps/config",
|
|
@@ -3922,8 +4363,8 @@ var CONFIG_URLS = {
|
|
|
3922
4363
|
production: "https://cms.rodyssey.ai/api/webapps/config"
|
|
3923
4364
|
};
|
|
3924
4365
|
function parseJsonOption(value, optionName) {
|
|
3925
|
-
const maybePath =
|
|
3926
|
-
const raw =
|
|
4366
|
+
const maybePath = resolve2(process.cwd(), value);
|
|
4367
|
+
const raw = existsSync6(maybePath) ? readFileSync5(maybePath, "utf-8") : value;
|
|
3927
4368
|
try {
|
|
3928
4369
|
const parsed = JSON.parse(raw);
|
|
3929
4370
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -4034,7 +4475,7 @@ async function getWebappConfig(options) {
|
|
|
4034
4475
|
const config = await fetchWebappConfig(options);
|
|
4035
4476
|
const output = JSON.stringify(config, null, 2);
|
|
4036
4477
|
if (options.out) {
|
|
4037
|
-
|
|
4478
|
+
writeFileSync4(options.out, `${output}
|
|
4038
4479
|
`, "utf-8");
|
|
4039
4480
|
console.log(`✅ Webapp config written to ${options.out}`);
|
|
4040
4481
|
return;
|
|
@@ -4091,7 +4532,7 @@ ${errorText}`);
|
|
|
4091
4532
|
|
|
4092
4533
|
// src/promote.ts
|
|
4093
4534
|
var DEFAULT_SOURCE_ENV = "development";
|
|
4094
|
-
var
|
|
4535
|
+
var PROD_ENV2 = "production";
|
|
4095
4536
|
var PROD_ENV_FILE = ".env.production";
|
|
4096
4537
|
function isObject3(value) {
|
|
4097
4538
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -4118,8 +4559,8 @@ function unwrapSourceDetails(payload) {
|
|
|
4118
4559
|
return payload;
|
|
4119
4560
|
}
|
|
4120
4561
|
function parseDetailsOption(value) {
|
|
4121
|
-
const maybePath =
|
|
4122
|
-
const raw =
|
|
4562
|
+
const maybePath = resolve3(process.cwd(), value);
|
|
4563
|
+
const raw = existsSync7(maybePath) ? readFileSync6(maybePath, "utf-8") : value;
|
|
4123
4564
|
try {
|
|
4124
4565
|
const parsed = JSON.parse(raw);
|
|
4125
4566
|
if (!isObject3(parsed)) {
|
|
@@ -4131,7 +4572,7 @@ function parseDetailsOption(value) {
|
|
|
4131
4572
|
throw new Error(`Invalid --details: ${message}`);
|
|
4132
4573
|
}
|
|
4133
4574
|
}
|
|
4134
|
-
async function
|
|
4575
|
+
async function prompt2(question) {
|
|
4135
4576
|
const readline = await import("node:readline");
|
|
4136
4577
|
const rl = readline.createInterface({
|
|
4137
4578
|
input: process.stdin,
|
|
@@ -4148,7 +4589,7 @@ function isAffirmative(answer) {
|
|
|
4148
4589
|
const trimmed = answer.trim().toLowerCase();
|
|
4149
4590
|
return trimmed === "" || trimmed === "y" || trimmed === "yes";
|
|
4150
4591
|
}
|
|
4151
|
-
function
|
|
4592
|
+
function isExplicitYes2(answer) {
|
|
4152
4593
|
const trimmed = answer.trim().toLowerCase();
|
|
4153
4594
|
return trimmed === "y" || trimmed === "yes";
|
|
4154
4595
|
}
|
|
@@ -4180,9 +4621,9 @@ async function maybeDeployProduction(options, webappId, deployToken) {
|
|
|
4180
4621
|
`);
|
|
4181
4622
|
return;
|
|
4182
4623
|
}
|
|
4183
|
-
const answer = await
|
|
4624
|
+
const answer = await prompt2(`
|
|
4184
4625
|
Deploy to production now? (y/N): `);
|
|
4185
|
-
shouldDeploy =
|
|
4626
|
+
shouldDeploy = isExplicitYes2(answer);
|
|
4186
4627
|
}
|
|
4187
4628
|
if (!shouldDeploy) {
|
|
4188
4629
|
console.log(`
|
|
@@ -4194,10 +4635,10 @@ Deploy to production now? (y/N): `);
|
|
|
4194
4635
|
}
|
|
4195
4636
|
console.log(`
|
|
4196
4637
|
\uD83D\uDE80 Deploying promoted webapp to production...`);
|
|
4197
|
-
loadEnv(
|
|
4638
|
+
loadEnv(PROD_ENV2, { override: true });
|
|
4198
4639
|
process.env.WEBAPP_ID = webappId;
|
|
4199
4640
|
process.env.DEPLOY_TOKEN = deployToken;
|
|
4200
|
-
await deploy(
|
|
4641
|
+
await deploy(PROD_ENV2);
|
|
4201
4642
|
}
|
|
4202
4643
|
async function promote(options) {
|
|
4203
4644
|
assertDeployOptions(options);
|
|
@@ -4208,9 +4649,9 @@ async function promote(options) {
|
|
|
4208
4649
|
console.info("\uD83D\uDCA1 Run `ro app create --auto` first, or set WEBAPP_ID manually.");
|
|
4209
4650
|
process.exit(1);
|
|
4210
4651
|
}
|
|
4211
|
-
const prodEnvPath =
|
|
4212
|
-
if (
|
|
4213
|
-
const content =
|
|
4652
|
+
const prodEnvPath = resolve3(process.cwd(), PROD_ENV_FILE);
|
|
4653
|
+
if (existsSync7(prodEnvPath)) {
|
|
4654
|
+
const content = readFileSync6(prodEnvPath, "utf-8");
|
|
4214
4655
|
if (/^WEBAPP_ID=.+/m.test(content)) {
|
|
4215
4656
|
console.error(`❌ Error: ${PROD_ENV_FILE} already has WEBAPP_ID. The app appears to be promoted already.`);
|
|
4216
4657
|
process.exit(1);
|
|
@@ -4233,7 +4674,7 @@ async function promote(options) {
|
|
|
4233
4674
|
console.log("✓ --yes flag set, using server details.");
|
|
4234
4675
|
details = sourceDetails;
|
|
4235
4676
|
} else {
|
|
4236
|
-
const answer = await
|
|
4677
|
+
const answer = await prompt2("Pull these details from server? (Y/n): ");
|
|
4237
4678
|
if (isAffirmative(answer)) {
|
|
4238
4679
|
details = sourceDetails;
|
|
4239
4680
|
} else {
|
|
@@ -4243,18 +4684,18 @@ async function promote(options) {
|
|
|
4243
4684
|
}
|
|
4244
4685
|
}
|
|
4245
4686
|
console.log(`
|
|
4246
|
-
\uD83D\uDD10 Logging in to ${
|
|
4687
|
+
\uD83D\uDD10 Logging in to ${PROD_ENV2} CMS (ephemeral, not stored)...`);
|
|
4247
4688
|
const session = await login({
|
|
4248
|
-
env:
|
|
4689
|
+
env: PROD_ENV2,
|
|
4249
4690
|
cmsUrl: options.cmsUrl,
|
|
4250
4691
|
persist: false
|
|
4251
4692
|
});
|
|
4252
4693
|
console.log(`✅ Logged in to ${session.cmsUrl}`);
|
|
4253
|
-
const cmsUrl = resolveCmsUrl(
|
|
4694
|
+
const cmsUrl = resolveCmsUrl(PROD_ENV2, options.cmsUrl);
|
|
4254
4695
|
const promoteUrl = options.promoteUrl || `${cmsUrl}/api/cli/webapps/promote`;
|
|
4255
4696
|
const promoteBody = { webappId, details };
|
|
4256
4697
|
console.log(`
|
|
4257
|
-
\uD83D\uDE80 Promoting webapp to ${
|
|
4698
|
+
\uD83D\uDE80 Promoting webapp to ${PROD_ENV2}...`);
|
|
4258
4699
|
console.log(`\uD83D\uDCCD Promote URL: ${promoteUrl}`);
|
|
4259
4700
|
console.log(`\uD83D\uDCCD Webapp ID: ${webappId}`);
|
|
4260
4701
|
console.log("\uD83D\uDCE6 Promote body:");
|
|
@@ -4271,7 +4712,7 @@ async function promote(options) {
|
|
|
4271
4712
|
});
|
|
4272
4713
|
const payload = await readResponsePayload(response);
|
|
4273
4714
|
if (response.status === 409) {
|
|
4274
|
-
console.error(`❌ Webapp ${webappId} is already promoted to ${
|
|
4715
|
+
console.error(`❌ Webapp ${webappId} is already promoted to ${PROD_ENV2}.`);
|
|
4275
4716
|
process.exit(1);
|
|
4276
4717
|
}
|
|
4277
4718
|
if (!response.ok) {
|
|
@@ -4294,7 +4735,7 @@ ${JSON.stringify(payload, null, 2)}`);
|
|
|
4294
4735
|
|
|
4295
4736
|
// src/upgrade-template.ts
|
|
4296
4737
|
import { execSync as execSync3 } from "node:child_process";
|
|
4297
|
-
import { existsSync as
|
|
4738
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2, copyFileSync, rmSync as rmSync2 } from "node:fs";
|
|
4298
4739
|
import path3 from "node:path";
|
|
4299
4740
|
var TEMPLATES = {
|
|
4300
4741
|
webapp: {
|
|
@@ -4355,12 +4796,12 @@ var CLI_SCRIPTS = {
|
|
|
4355
4796
|
"upgrade-template": "bunx @rodyssey/cli@latest app upgrade-template"
|
|
4356
4797
|
};
|
|
4357
4798
|
function detectTemplate() {
|
|
4358
|
-
if (
|
|
4799
|
+
if (existsSync8("app")) {
|
|
4359
4800
|
console.log(`\uD83D\uDD0D Detected fullstack template (found app/ directory)
|
|
4360
4801
|
`);
|
|
4361
4802
|
return TEMPLATES["webapp-fullstack"];
|
|
4362
4803
|
}
|
|
4363
|
-
if (
|
|
4804
|
+
if (existsSync8("src")) {
|
|
4364
4805
|
console.log(`\uD83D\uDD0D Detected SPA template (found src/ directory)
|
|
4365
4806
|
`);
|
|
4366
4807
|
return TEMPLATES["webapp"];
|
|
@@ -4371,11 +4812,11 @@ function detectTemplate() {
|
|
|
4371
4812
|
}
|
|
4372
4813
|
function updatePackageJsonScripts() {
|
|
4373
4814
|
const pkgPath = "package.json";
|
|
4374
|
-
if (!
|
|
4815
|
+
if (!existsSync8(pkgPath)) {
|
|
4375
4816
|
console.log("⚠️ No package.json found, skipping scripts update");
|
|
4376
4817
|
return;
|
|
4377
4818
|
}
|
|
4378
|
-
const pkg = JSON.parse(
|
|
4819
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
4379
4820
|
if (!pkg.scripts) {
|
|
4380
4821
|
pkg.scripts = {};
|
|
4381
4822
|
}
|
|
@@ -4389,7 +4830,7 @@ function updatePackageJsonScripts() {
|
|
|
4389
4830
|
}
|
|
4390
4831
|
}
|
|
4391
4832
|
if (updated) {
|
|
4392
|
-
|
|
4833
|
+
writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
4393
4834
|
`, "utf-8");
|
|
4394
4835
|
console.log(`✅ package.json scripts updated
|
|
4395
4836
|
`);
|
|
@@ -4413,7 +4854,7 @@ function updateCliSkill() {
|
|
|
4413
4854
|
}
|
|
4414
4855
|
execSync3(`git fetch ${cliRemote}`, { stdio: "inherit" });
|
|
4415
4856
|
execSync3(`git checkout ${cliRemote}/main -- skills/ro-cli/SKILL.md`, { stdio: "inherit" });
|
|
4416
|
-
if (
|
|
4857
|
+
if (existsSync8("skills/ro-cli/SKILL.md")) {
|
|
4417
4858
|
mkdirSync2(".agent/skills/ro-cli", { recursive: true });
|
|
4418
4859
|
copyFileSync("skills/ro-cli/SKILL.md", ".agent/skills/ro-cli/SKILL.md");
|
|
4419
4860
|
rmSync2("skills", { recursive: true, force: true });
|
|
@@ -4446,7 +4887,7 @@ async function upgradeTemplate() {
|
|
|
4446
4887
|
const checkoutList = template.checkoutFiles.join(" ");
|
|
4447
4888
|
execSync3(`git checkout ${template.remoteName}/main -- ${checkoutList}`, { stdio: "inherit" });
|
|
4448
4889
|
for (const file of template.newFiles) {
|
|
4449
|
-
if (!
|
|
4890
|
+
if (!existsSync8(file)) {
|
|
4450
4891
|
console.log(`\uD83D\uDCC2 Checking out ${file}...`);
|
|
4451
4892
|
try {
|
|
4452
4893
|
mkdirSync2(path3.dirname(file), { recursive: true });
|
|
@@ -4632,15 +5073,15 @@ Available templates:
|
|
|
4632
5073
|
input: process.stdin,
|
|
4633
5074
|
output: process.stdout
|
|
4634
5075
|
});
|
|
4635
|
-
return new Promise((
|
|
5076
|
+
return new Promise((resolve4) => {
|
|
4636
5077
|
rl.question(`Select a template (1-${entries.length}): `, (answer) => {
|
|
4637
5078
|
rl.close();
|
|
4638
5079
|
const index = parseInt(answer, 10) - 1;
|
|
4639
5080
|
if (index >= 0 && index < entries.length) {
|
|
4640
|
-
|
|
5081
|
+
resolve4(entries[index].name);
|
|
4641
5082
|
} else {
|
|
4642
5083
|
console.error("Invalid selection, defaulting to 'webapp'");
|
|
4643
|
-
|
|
5084
|
+
resolve4("webapp");
|
|
4644
5085
|
}
|
|
4645
5086
|
});
|
|
4646
5087
|
});
|
|
@@ -4675,6 +5116,17 @@ function addAuthCommands(parent) {
|
|
|
4675
5116
|
return auth;
|
|
4676
5117
|
}
|
|
4677
5118
|
addAuthCommands(program);
|
|
5119
|
+
var globalConfig = program.command("global-config").description("Manage CMS-wide global configuration (game-config row)");
|
|
5120
|
+
globalConfig.command("get").description("Read the global config from the CMS").option("-e, --env <environment>", "CMS environment (local | development | staging | production)", "development").option("--cms-url <url>", "CMS base URL. Defaults to the selected environment").option("--url <url>", "Full endpoint URL. Defaults to <cms-url>/api/cli/cms/global-config").option("--out <file>", "Write the response JSON to a file").action(async (options) => {
|
|
5121
|
+
await getGlobalConfig(options);
|
|
5122
|
+
});
|
|
5123
|
+
var addGlobalConfigWriteOptions = (command) => command.option("-e, --env <environment>", "CMS environment (local | development | staging | production)", "development").option("--cms-url <url>", "CMS base URL. Defaults to the selected environment").option("--url <url>", "Full endpoint URL. Defaults to <cms-url>/api/cli/cms/global-config").option("--config <json-or-file>", "JSON object (or path to a JSON file) to apply to the `config` column. Pass 'null' to clear.").option("--public-config <json-or-file>", "JSON object (or path to a JSON file) to apply to the `publicConfig` column. Pass 'null' to clear.").option("--dry-run", "Print the intended payload (and merge preview) without sending").option("-y, --yes", "Skip the interactive confirmation prompt");
|
|
5124
|
+
addGlobalConfigWriteOptions(globalConfig.command("set").description("Replace the `config` and/or `publicConfig` column on the global-config row")).action(async (options) => {
|
|
5125
|
+
await setGlobalConfig(options);
|
|
5126
|
+
});
|
|
5127
|
+
addGlobalConfigWriteOptions(globalConfig.command("patch").description("Merge-patch (RFC 7396) the `config` and/or `publicConfig` column. `null` values delete keys.")).action(async (options) => {
|
|
5128
|
+
await patchGlobalConfig(options);
|
|
5129
|
+
});
|
|
4678
5130
|
app.command("create").argument("<project-name>", "Name of the project to create").option("-t, --template <template>", "Template to use (webapp | webapp-fullstack)").option("--auto", "Create a CMS webapp and write WEBAPP_ID/DEPLOY_TOKEN to .env").option("-e, --env <environment>", "CMS environment for --auto (local | development | staging | production)", "development").option("--cms-url <url>", "CMS base URL for --auto. Defaults to the selected environment").option("--create-url <url>", "Full CMS create endpoint for --auto. Defaults to <cms-url>/api/cli/webapps/create").description("Create a new project from a template").action(async (projectName, options) => {
|
|
4679
5131
|
let templateName;
|
|
4680
5132
|
if (options.template) {
|