@rodyssey/cli 0.1.9 → 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/README.md +6 -0
- package/dist/cli.js +452 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,12 @@ To run:
|
|
|
12
12
|
bun run index.ts
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
## Template Upgrade
|
|
16
|
+
|
|
17
|
+
`app upgrade-template` backfills additive template files that are missing from
|
|
18
|
+
older projects. This includes the Dynamic Worker MCP sample endpoint and the
|
|
19
|
+
shared `mcp/` helper folder; existing local MCP files are left untouched.
|
|
20
|
+
|
|
15
21
|
## Release
|
|
16
22
|
|
|
17
23
|
Release automation lives in `.github/workflows/release.yml` and uses Changesets.
|
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.
|
|
2074
|
+
version: "0.2.0",
|
|
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();
|
|
@@ -3908,13 +3981,327 @@ ${errorText}`);
|
|
|
3908
3981
|
✨ Deployment successful!`);
|
|
3909
3982
|
}
|
|
3910
3983
|
|
|
3984
|
+
// src/global-config.ts
|
|
3985
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3986
|
+
import { resolve } from "node:path";
|
|
3987
|
+
var PROD_ENV = "production";
|
|
3988
|
+
function isPlainObject(value) {
|
|
3989
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
3990
|
+
}
|
|
3991
|
+
function applyMergePatch(target, patch) {
|
|
3992
|
+
if (!isPlainObject(patch))
|
|
3993
|
+
return patch;
|
|
3994
|
+
const base = isPlainObject(target) ? { ...target } : {};
|
|
3995
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
3996
|
+
if (value === null) {
|
|
3997
|
+
delete base[key];
|
|
3998
|
+
} else {
|
|
3999
|
+
base[key] = applyMergePatch(base[key], value);
|
|
4000
|
+
}
|
|
4001
|
+
}
|
|
4002
|
+
return base;
|
|
4003
|
+
}
|
|
4004
|
+
function parseFlag(name, value) {
|
|
4005
|
+
if (value === "null")
|
|
4006
|
+
return null;
|
|
4007
|
+
const candidatePath = resolve(process.cwd(), value);
|
|
4008
|
+
const raw = existsSync5(candidatePath) ? readFileSync4(candidatePath, "utf-8") : value;
|
|
4009
|
+
try {
|
|
4010
|
+
return JSON.parse(raw);
|
|
4011
|
+
} catch (error) {
|
|
4012
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4013
|
+
throw new Error(`Invalid --${name}: ${message}`);
|
|
4014
|
+
}
|
|
4015
|
+
}
|
|
4016
|
+
function buildPayload(options) {
|
|
4017
|
+
const payload = {};
|
|
4018
|
+
if (options.config !== undefined) {
|
|
4019
|
+
payload.config = parseFlag("config", options.config);
|
|
4020
|
+
}
|
|
4021
|
+
if (options.publicConfig !== undefined) {
|
|
4022
|
+
payload.publicConfig = parseFlag("public-config", options.publicConfig);
|
|
4023
|
+
}
|
|
4024
|
+
if (payload.config === undefined && payload.publicConfig === undefined) {
|
|
4025
|
+
throw new Error("Provide at least one of --config or --public-config.");
|
|
4026
|
+
}
|
|
4027
|
+
return payload;
|
|
4028
|
+
}
|
|
4029
|
+
function resolveEndpoint(env, override, cmsUrl) {
|
|
4030
|
+
if (override)
|
|
4031
|
+
return override;
|
|
4032
|
+
return `${resolveCmsUrl(env, cmsUrl)}/api/cli/cms/global-config`;
|
|
4033
|
+
}
|
|
4034
|
+
async function prompt(question) {
|
|
4035
|
+
const readline = await import("node:readline");
|
|
4036
|
+
const rl = readline.createInterface({
|
|
4037
|
+
input: process.stdin,
|
|
4038
|
+
output: process.stdout
|
|
4039
|
+
});
|
|
4040
|
+
return new Promise((resolveAnswer) => {
|
|
4041
|
+
rl.question(question, (answer) => {
|
|
4042
|
+
rl.close();
|
|
4043
|
+
resolveAnswer(answer);
|
|
4044
|
+
});
|
|
4045
|
+
});
|
|
4046
|
+
}
|
|
4047
|
+
function isExplicitYes(answer) {
|
|
4048
|
+
const trimmed = answer.trim().toLowerCase();
|
|
4049
|
+
return trimmed === "y" || trimmed === "yes";
|
|
4050
|
+
}
|
|
4051
|
+
function pretty(value) {
|
|
4052
|
+
return JSON.stringify(value, null, 2);
|
|
4053
|
+
}
|
|
4054
|
+
function compact(value) {
|
|
4055
|
+
return JSON.stringify(value);
|
|
4056
|
+
}
|
|
4057
|
+
function useColor() {
|
|
4058
|
+
return !!process.stdout.isTTY && !process.env.NO_COLOR;
|
|
4059
|
+
}
|
|
4060
|
+
function paint(code, text) {
|
|
4061
|
+
return useColor() ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
4062
|
+
}
|
|
4063
|
+
var green = (s) => paint("32", s);
|
|
4064
|
+
var red = (s) => paint("31", s);
|
|
4065
|
+
var yellow = (s) => paint("33", s);
|
|
4066
|
+
var dim = (s) => paint("2", s);
|
|
4067
|
+
var strike = (s) => paint("9", s);
|
|
4068
|
+
var COLUMN_LABEL = {
|
|
4069
|
+
config: "Config",
|
|
4070
|
+
publicConfig: "Public Config"
|
|
4071
|
+
};
|
|
4072
|
+
function pathToDot(path3) {
|
|
4073
|
+
return path3.replace(/^\//, "").replaceAll("/", ".");
|
|
4074
|
+
}
|
|
4075
|
+
function deepEqual(a, b) {
|
|
4076
|
+
if (a === b)
|
|
4077
|
+
return true;
|
|
4078
|
+
if (typeof a !== typeof b)
|
|
4079
|
+
return false;
|
|
4080
|
+
if (a === null || b === null)
|
|
4081
|
+
return false;
|
|
4082
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
4083
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
4084
|
+
return false;
|
|
4085
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
4086
|
+
}
|
|
4087
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
4088
|
+
const ak = Object.keys(a);
|
|
4089
|
+
const bk = Object.keys(b);
|
|
4090
|
+
if (ak.length !== bk.length)
|
|
4091
|
+
return false;
|
|
4092
|
+
return ak.every((k) => deepEqual(a[k], b[k]));
|
|
4093
|
+
}
|
|
4094
|
+
return false;
|
|
4095
|
+
}
|
|
4096
|
+
function diffJson(before, after, path3 = "") {
|
|
4097
|
+
if (deepEqual(before, after))
|
|
4098
|
+
return [];
|
|
4099
|
+
if (before === undefined)
|
|
4100
|
+
return [{ path: path3, kind: "add", after }];
|
|
4101
|
+
if (after === undefined)
|
|
4102
|
+
return [{ path: path3, kind: "remove", before }];
|
|
4103
|
+
if (!isPlainObject(before) || !isPlainObject(after)) {
|
|
4104
|
+
return [{ path: path3, kind: "change", before, after }];
|
|
4105
|
+
}
|
|
4106
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
4107
|
+
const out = [];
|
|
4108
|
+
for (const key of keys) {
|
|
4109
|
+
out.push(...diffJson(before[key], after[key], `${path3}/${key}`));
|
|
4110
|
+
}
|
|
4111
|
+
return out;
|
|
4112
|
+
}
|
|
4113
|
+
function buildColumnDeltas(current, payload, method) {
|
|
4114
|
+
const out = [];
|
|
4115
|
+
for (const column of ["config", "publicConfig"]) {
|
|
4116
|
+
if (payload[column] === undefined)
|
|
4117
|
+
continue;
|
|
4118
|
+
const cur = current[column] ?? null;
|
|
4119
|
+
let projected;
|
|
4120
|
+
if (method === "PUT" || payload[column] === null) {
|
|
4121
|
+
projected = payload[column] ?? null;
|
|
4122
|
+
} else {
|
|
4123
|
+
projected = applyMergePatch(cur, payload[column]);
|
|
4124
|
+
}
|
|
4125
|
+
const deltas = diffJson(cur, projected);
|
|
4126
|
+
for (const d of deltas) {
|
|
4127
|
+
out.push({ ...d, column });
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
return out;
|
|
4131
|
+
}
|
|
4132
|
+
function deltaLine(d, showColumnTag) {
|
|
4133
|
+
const tag = showColumnTag ? `[${COLUMN_LABEL[d.column]}] ` : "";
|
|
4134
|
+
const path3 = pathToDot(d.path) || `(entire ${COLUMN_LABEL[d.column]})`;
|
|
4135
|
+
if (d.kind === "add") {
|
|
4136
|
+
return green(` ${tag}${path3}: ${compact(d.after)}`);
|
|
4137
|
+
}
|
|
4138
|
+
if (d.kind === "change") {
|
|
4139
|
+
return yellow(` ${tag}${path3}: ${strike(compact(d.before))} → ${compact(d.after)}`);
|
|
4140
|
+
}
|
|
4141
|
+
return red(strike(` ${tag}${path3}: ${compact(d.before)}`));
|
|
4142
|
+
}
|
|
4143
|
+
function formatDelta(deltas, patchedColumns, verb) {
|
|
4144
|
+
const colNames = patchedColumns.map((c) => COLUMN_LABEL[c]).join(" / ");
|
|
4145
|
+
const header = `${verb} on: ${colNames}`;
|
|
4146
|
+
if (deltas.length === 0) {
|
|
4147
|
+
return `${header}
|
|
4148
|
+
${dim(" (no changes)")}`;
|
|
4149
|
+
}
|
|
4150
|
+
const showTag = patchedColumns.length > 1;
|
|
4151
|
+
const adds = deltas.filter((d) => d.kind === "add");
|
|
4152
|
+
const changes = deltas.filter((d) => d.kind === "change");
|
|
4153
|
+
const removes = deltas.filter((d) => d.kind === "remove");
|
|
4154
|
+
const sections = [];
|
|
4155
|
+
if (adds.length > 0) {
|
|
4156
|
+
sections.push([green("New"), ...adds.map((d) => deltaLine(d, showTag))].join(`
|
|
4157
|
+
`));
|
|
4158
|
+
}
|
|
4159
|
+
if (changes.length > 0) {
|
|
4160
|
+
sections.push([yellow("Update"), ...changes.map((d) => deltaLine(d, showTag))].join(`
|
|
4161
|
+
`));
|
|
4162
|
+
}
|
|
4163
|
+
if (removes.length > 0) {
|
|
4164
|
+
sections.push([red("Delete"), ...removes.map((d) => deltaLine(d, showTag))].join(`
|
|
4165
|
+
`));
|
|
4166
|
+
}
|
|
4167
|
+
return `${header}
|
|
4168
|
+
|
|
4169
|
+
${sections.join(`
|
|
4170
|
+
|
|
4171
|
+
`)}`;
|
|
4172
|
+
}
|
|
4173
|
+
async function resolveAuth(env, cmsUrl, needsWrite) {
|
|
4174
|
+
if (env === PROD_ENV && needsWrite) {
|
|
4175
|
+
console.log(`
|
|
4176
|
+
\uD83D\uDD10 Logging in to ${PROD_ENV} CMS (ephemeral, not stored)...`);
|
|
4177
|
+
const session = await login({ env, cmsUrl, persist: false });
|
|
4178
|
+
console.log(`✅ Logged in to ${session.cmsUrl}`);
|
|
4179
|
+
return { url: session.cmsUrl, token: session.token };
|
|
4180
|
+
}
|
|
4181
|
+
return {
|
|
4182
|
+
url: resolveCmsUrl(env, cmsUrl),
|
|
4183
|
+
token: resolveSessionToken(env)
|
|
4184
|
+
};
|
|
4185
|
+
}
|
|
4186
|
+
async function getGlobalConfig(options) {
|
|
4187
|
+
const url = resolveEndpoint(options.env, options.url, options.cmsUrl);
|
|
4188
|
+
const token = resolveSessionToken(options.env);
|
|
4189
|
+
const response = await fetch(url, {
|
|
4190
|
+
method: "GET",
|
|
4191
|
+
headers: {
|
|
4192
|
+
Accept: "application/json",
|
|
4193
|
+
Authorization: `Bearer ${token}`
|
|
4194
|
+
}
|
|
4195
|
+
});
|
|
4196
|
+
const payload = await readResponsePayload(response);
|
|
4197
|
+
if (!response.ok) {
|
|
4198
|
+
throw new Error(`GET global-config failed: ${response.status} ${response.statusText}
|
|
4199
|
+
${pretty(payload)}`);
|
|
4200
|
+
}
|
|
4201
|
+
const text = pretty(payload);
|
|
4202
|
+
if (options.out) {
|
|
4203
|
+
const outPath = resolve(process.cwd(), options.out);
|
|
4204
|
+
writeFileSync3(outPath, `${text}
|
|
4205
|
+
`, "utf-8");
|
|
4206
|
+
console.log(`✅ Wrote global config to ${outPath}`);
|
|
4207
|
+
} else {
|
|
4208
|
+
console.log(text);
|
|
4209
|
+
}
|
|
4210
|
+
return payload;
|
|
4211
|
+
}
|
|
4212
|
+
async function writeGlobalConfig(method, options) {
|
|
4213
|
+
const payload = buildPayload(options);
|
|
4214
|
+
const verb = method === "PUT" ? "Replacing" : "Patching";
|
|
4215
|
+
const patchedColumns = Object.keys(payload).filter((k) => payload[k] !== undefined);
|
|
4216
|
+
try {
|
|
4217
|
+
console.log(`\uD83D\uDCE5 Fetching current global-config from [${options.env}]...
|
|
4218
|
+
`);
|
|
4219
|
+
const current = await getGlobalConfigSilent(options.env, options.cmsUrl, options.url);
|
|
4220
|
+
const deltas = buildColumnDeltas(current, payload, method);
|
|
4221
|
+
console.log(formatDelta(deltas, patchedColumns, verb));
|
|
4222
|
+
} catch (error) {
|
|
4223
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4224
|
+
console.warn(`⚠️ Could not preview changes: ${message}`);
|
|
4225
|
+
console.log(`
|
|
4226
|
+
\uD83D\uDCE6 Raw payload:`);
|
|
4227
|
+
console.log(pretty(payload));
|
|
4228
|
+
}
|
|
4229
|
+
if (options.dryRun) {
|
|
4230
|
+
console.log(dim(`
|
|
4231
|
+
↷ Dry run — no request sent.`));
|
|
4232
|
+
return;
|
|
4233
|
+
}
|
|
4234
|
+
const tty = !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
4235
|
+
if (!options.yes) {
|
|
4236
|
+
if (!tty) {
|
|
4237
|
+
throw new Error("Refusing to send write in non-interactive mode. Pass --yes to confirm or --dry-run to preview.");
|
|
4238
|
+
}
|
|
4239
|
+
const answer = await prompt(`
|
|
4240
|
+
Proceed with ${verb.toLowerCase()} on [${options.env}]? (y/N): `);
|
|
4241
|
+
if (!isExplicitYes(answer)) {
|
|
4242
|
+
console.log("✋ Aborted.");
|
|
4243
|
+
return;
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
const auth = await resolveAuth(options.env, options.cmsUrl, true);
|
|
4247
|
+
const endpoint = options.url || `${auth.url}/api/cli/cms/global-config`;
|
|
4248
|
+
console.log(`
|
|
4249
|
+
\uD83D\uDE80 ${method} ${endpoint}`);
|
|
4250
|
+
const response = await fetch(endpoint, {
|
|
4251
|
+
method,
|
|
4252
|
+
headers: {
|
|
4253
|
+
Accept: "application/json",
|
|
4254
|
+
Authorization: `Bearer ${auth.token}`,
|
|
4255
|
+
"Content-Type": "application/json"
|
|
4256
|
+
},
|
|
4257
|
+
body: JSON.stringify(payload)
|
|
4258
|
+
});
|
|
4259
|
+
const responseBody = await readResponsePayload(response);
|
|
4260
|
+
if (!response.ok) {
|
|
4261
|
+
throw new Error(`${method} global-config failed: ${response.status} ${response.statusText}
|
|
4262
|
+
${pretty(responseBody)}`);
|
|
4263
|
+
}
|
|
4264
|
+
console.log(`
|
|
4265
|
+
✅ Updated global config:`);
|
|
4266
|
+
console.log(pretty(responseBody));
|
|
4267
|
+
}
|
|
4268
|
+
async function getGlobalConfigSilent(env, cmsUrl, urlOverride) {
|
|
4269
|
+
const url = resolveEndpoint(env, urlOverride, cmsUrl);
|
|
4270
|
+
const token = resolveSessionToken(env);
|
|
4271
|
+
const response = await fetch(url, {
|
|
4272
|
+
method: "GET",
|
|
4273
|
+
headers: {
|
|
4274
|
+
Accept: "application/json",
|
|
4275
|
+
Authorization: `Bearer ${token}`
|
|
4276
|
+
}
|
|
4277
|
+
});
|
|
4278
|
+
const payload = await readResponsePayload(response);
|
|
4279
|
+
if (!response.ok) {
|
|
4280
|
+
throw new Error(`GET global-config failed: ${response.status} ${response.statusText}
|
|
4281
|
+
${pretty(payload)}`);
|
|
4282
|
+
}
|
|
4283
|
+
if (!isPlainObject(payload)) {
|
|
4284
|
+
throw new Error("Unexpected response: body is not an object.");
|
|
4285
|
+
}
|
|
4286
|
+
return {
|
|
4287
|
+
config: payload.config ?? null,
|
|
4288
|
+
publicConfig: payload.publicConfig ?? null
|
|
4289
|
+
};
|
|
4290
|
+
}
|
|
4291
|
+
async function setGlobalConfig(options) {
|
|
4292
|
+
await writeGlobalConfig("PUT", options);
|
|
4293
|
+
}
|
|
4294
|
+
async function patchGlobalConfig(options) {
|
|
4295
|
+
await writeGlobalConfig("PATCH", options);
|
|
4296
|
+
}
|
|
4297
|
+
|
|
3911
4298
|
// src/promote.ts
|
|
3912
|
-
import { existsSync as
|
|
3913
|
-
import { resolve as
|
|
4299
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
4300
|
+
import { resolve as resolve3 } from "node:path";
|
|
3914
4301
|
|
|
3915
4302
|
// src/update-webapp-config.ts
|
|
3916
|
-
import { existsSync as
|
|
3917
|
-
import { resolve } from "node:path";
|
|
4303
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4304
|
+
import { resolve as resolve2 } from "node:path";
|
|
3918
4305
|
var CONFIG_URLS = {
|
|
3919
4306
|
local: "http://localhost:5176/api/webapps/config",
|
|
3920
4307
|
development: "https://development-cms.rodyssey.ai/api/webapps/config",
|
|
@@ -3922,8 +4309,8 @@ var CONFIG_URLS = {
|
|
|
3922
4309
|
production: "https://cms.rodyssey.ai/api/webapps/config"
|
|
3923
4310
|
};
|
|
3924
4311
|
function parseJsonOption(value, optionName) {
|
|
3925
|
-
const maybePath =
|
|
3926
|
-
const raw =
|
|
4312
|
+
const maybePath = resolve2(process.cwd(), value);
|
|
4313
|
+
const raw = existsSync6(maybePath) ? readFileSync5(maybePath, "utf-8") : value;
|
|
3927
4314
|
try {
|
|
3928
4315
|
const parsed = JSON.parse(raw);
|
|
3929
4316
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -4034,7 +4421,7 @@ async function getWebappConfig(options) {
|
|
|
4034
4421
|
const config = await fetchWebappConfig(options);
|
|
4035
4422
|
const output = JSON.stringify(config, null, 2);
|
|
4036
4423
|
if (options.out) {
|
|
4037
|
-
|
|
4424
|
+
writeFileSync4(options.out, `${output}
|
|
4038
4425
|
`, "utf-8");
|
|
4039
4426
|
console.log(`✅ Webapp config written to ${options.out}`);
|
|
4040
4427
|
return;
|
|
@@ -4091,7 +4478,7 @@ ${errorText}`);
|
|
|
4091
4478
|
|
|
4092
4479
|
// src/promote.ts
|
|
4093
4480
|
var DEFAULT_SOURCE_ENV = "development";
|
|
4094
|
-
var
|
|
4481
|
+
var PROD_ENV2 = "production";
|
|
4095
4482
|
var PROD_ENV_FILE = ".env.production";
|
|
4096
4483
|
function isObject3(value) {
|
|
4097
4484
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -4118,8 +4505,8 @@ function unwrapSourceDetails(payload) {
|
|
|
4118
4505
|
return payload;
|
|
4119
4506
|
}
|
|
4120
4507
|
function parseDetailsOption(value) {
|
|
4121
|
-
const maybePath =
|
|
4122
|
-
const raw =
|
|
4508
|
+
const maybePath = resolve3(process.cwd(), value);
|
|
4509
|
+
const raw = existsSync7(maybePath) ? readFileSync6(maybePath, "utf-8") : value;
|
|
4123
4510
|
try {
|
|
4124
4511
|
const parsed = JSON.parse(raw);
|
|
4125
4512
|
if (!isObject3(parsed)) {
|
|
@@ -4131,7 +4518,7 @@ function parseDetailsOption(value) {
|
|
|
4131
4518
|
throw new Error(`Invalid --details: ${message}`);
|
|
4132
4519
|
}
|
|
4133
4520
|
}
|
|
4134
|
-
async function
|
|
4521
|
+
async function prompt2(question) {
|
|
4135
4522
|
const readline = await import("node:readline");
|
|
4136
4523
|
const rl = readline.createInterface({
|
|
4137
4524
|
input: process.stdin,
|
|
@@ -4148,7 +4535,7 @@ function isAffirmative(answer) {
|
|
|
4148
4535
|
const trimmed = answer.trim().toLowerCase();
|
|
4149
4536
|
return trimmed === "" || trimmed === "y" || trimmed === "yes";
|
|
4150
4537
|
}
|
|
4151
|
-
function
|
|
4538
|
+
function isExplicitYes2(answer) {
|
|
4152
4539
|
const trimmed = answer.trim().toLowerCase();
|
|
4153
4540
|
return trimmed === "y" || trimmed === "yes";
|
|
4154
4541
|
}
|
|
@@ -4180,9 +4567,9 @@ async function maybeDeployProduction(options, webappId, deployToken) {
|
|
|
4180
4567
|
`);
|
|
4181
4568
|
return;
|
|
4182
4569
|
}
|
|
4183
|
-
const answer = await
|
|
4570
|
+
const answer = await prompt2(`
|
|
4184
4571
|
Deploy to production now? (y/N): `);
|
|
4185
|
-
shouldDeploy =
|
|
4572
|
+
shouldDeploy = isExplicitYes2(answer);
|
|
4186
4573
|
}
|
|
4187
4574
|
if (!shouldDeploy) {
|
|
4188
4575
|
console.log(`
|
|
@@ -4194,10 +4581,10 @@ Deploy to production now? (y/N): `);
|
|
|
4194
4581
|
}
|
|
4195
4582
|
console.log(`
|
|
4196
4583
|
\uD83D\uDE80 Deploying promoted webapp to production...`);
|
|
4197
|
-
loadEnv(
|
|
4584
|
+
loadEnv(PROD_ENV2, { override: true });
|
|
4198
4585
|
process.env.WEBAPP_ID = webappId;
|
|
4199
4586
|
process.env.DEPLOY_TOKEN = deployToken;
|
|
4200
|
-
await deploy(
|
|
4587
|
+
await deploy(PROD_ENV2);
|
|
4201
4588
|
}
|
|
4202
4589
|
async function promote(options) {
|
|
4203
4590
|
assertDeployOptions(options);
|
|
@@ -4208,9 +4595,9 @@ async function promote(options) {
|
|
|
4208
4595
|
console.info("\uD83D\uDCA1 Run `ro app create --auto` first, or set WEBAPP_ID manually.");
|
|
4209
4596
|
process.exit(1);
|
|
4210
4597
|
}
|
|
4211
|
-
const prodEnvPath =
|
|
4212
|
-
if (
|
|
4213
|
-
const content =
|
|
4598
|
+
const prodEnvPath = resolve3(process.cwd(), PROD_ENV_FILE);
|
|
4599
|
+
if (existsSync7(prodEnvPath)) {
|
|
4600
|
+
const content = readFileSync6(prodEnvPath, "utf-8");
|
|
4214
4601
|
if (/^WEBAPP_ID=.+/m.test(content)) {
|
|
4215
4602
|
console.error(`❌ Error: ${PROD_ENV_FILE} already has WEBAPP_ID. The app appears to be promoted already.`);
|
|
4216
4603
|
process.exit(1);
|
|
@@ -4233,7 +4620,7 @@ async function promote(options) {
|
|
|
4233
4620
|
console.log("✓ --yes flag set, using server details.");
|
|
4234
4621
|
details = sourceDetails;
|
|
4235
4622
|
} else {
|
|
4236
|
-
const answer = await
|
|
4623
|
+
const answer = await prompt2("Pull these details from server? (Y/n): ");
|
|
4237
4624
|
if (isAffirmative(answer)) {
|
|
4238
4625
|
details = sourceDetails;
|
|
4239
4626
|
} else {
|
|
@@ -4243,18 +4630,18 @@ async function promote(options) {
|
|
|
4243
4630
|
}
|
|
4244
4631
|
}
|
|
4245
4632
|
console.log(`
|
|
4246
|
-
\uD83D\uDD10 Logging in to ${
|
|
4633
|
+
\uD83D\uDD10 Logging in to ${PROD_ENV2} CMS (ephemeral, not stored)...`);
|
|
4247
4634
|
const session = await login({
|
|
4248
|
-
env:
|
|
4635
|
+
env: PROD_ENV2,
|
|
4249
4636
|
cmsUrl: options.cmsUrl,
|
|
4250
4637
|
persist: false
|
|
4251
4638
|
});
|
|
4252
4639
|
console.log(`✅ Logged in to ${session.cmsUrl}`);
|
|
4253
|
-
const cmsUrl = resolveCmsUrl(
|
|
4640
|
+
const cmsUrl = resolveCmsUrl(PROD_ENV2, options.cmsUrl);
|
|
4254
4641
|
const promoteUrl = options.promoteUrl || `${cmsUrl}/api/cli/webapps/promote`;
|
|
4255
4642
|
const promoteBody = { webappId, details };
|
|
4256
4643
|
console.log(`
|
|
4257
|
-
\uD83D\uDE80 Promoting webapp to ${
|
|
4644
|
+
\uD83D\uDE80 Promoting webapp to ${PROD_ENV2}...`);
|
|
4258
4645
|
console.log(`\uD83D\uDCCD Promote URL: ${promoteUrl}`);
|
|
4259
4646
|
console.log(`\uD83D\uDCCD Webapp ID: ${webappId}`);
|
|
4260
4647
|
console.log("\uD83D\uDCE6 Promote body:");
|
|
@@ -4271,7 +4658,7 @@ async function promote(options) {
|
|
|
4271
4658
|
});
|
|
4272
4659
|
const payload = await readResponsePayload(response);
|
|
4273
4660
|
if (response.status === 409) {
|
|
4274
|
-
console.error(`❌ Webapp ${webappId} is already promoted to ${
|
|
4661
|
+
console.error(`❌ Webapp ${webappId} is already promoted to ${PROD_ENV2}.`);
|
|
4275
4662
|
process.exit(1);
|
|
4276
4663
|
}
|
|
4277
4664
|
if (!response.ok) {
|
|
@@ -4294,7 +4681,8 @@ ${JSON.stringify(payload, null, 2)}`);
|
|
|
4294
4681
|
|
|
4295
4682
|
// src/upgrade-template.ts
|
|
4296
4683
|
import { execSync as execSync3 } from "node:child_process";
|
|
4297
|
-
import { existsSync as
|
|
4684
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2, copyFileSync, rmSync as rmSync2 } from "node:fs";
|
|
4685
|
+
import path3 from "node:path";
|
|
4298
4686
|
var TEMPLATES = {
|
|
4299
4687
|
webapp: {
|
|
4300
4688
|
name: "webapp (SPA)",
|
|
@@ -4320,9 +4708,11 @@ var TEMPLATES = {
|
|
|
4320
4708
|
"vite-plugins/widgets-manifest.ts"
|
|
4321
4709
|
],
|
|
4322
4710
|
newFiles: [
|
|
4711
|
+
"src/api/mcp.ts",
|
|
4323
4712
|
"src/exp-engine/cli.ts",
|
|
4324
4713
|
"src/exp-engine/evaluate.ts",
|
|
4325
4714
|
"src/exp-engine/README.md",
|
|
4715
|
+
"src/mcp/server.ts",
|
|
4326
4716
|
"src/types/exp-engine.d.ts",
|
|
4327
4717
|
"src/widgets/examples.tsx",
|
|
4328
4718
|
"src/routes/widget.$id.tsx",
|
|
@@ -4335,10 +4725,12 @@ var TEMPLATES = {
|
|
|
4335
4725
|
remoteName: "template",
|
|
4336
4726
|
checkoutFiles: ["AGENTS.md", ".agent/", "app/types/webapp.d.ts", "app/types/game-sdk.d.ts"],
|
|
4337
4727
|
newFiles: [
|
|
4728
|
+
"api/mcp.ts",
|
|
4338
4729
|
"app/exp-engine/cli.ts",
|
|
4339
4730
|
"app/exp-engine/evaluate.ts",
|
|
4340
4731
|
"app/exp-engine/README.md",
|
|
4341
|
-
"app/types/exp-engine.d.ts"
|
|
4732
|
+
"app/types/exp-engine.d.ts",
|
|
4733
|
+
"mcp/server.ts"
|
|
4342
4734
|
]
|
|
4343
4735
|
}
|
|
4344
4736
|
};
|
|
@@ -4350,12 +4742,12 @@ var CLI_SCRIPTS = {
|
|
|
4350
4742
|
"upgrade-template": "bunx @rodyssey/cli@latest app upgrade-template"
|
|
4351
4743
|
};
|
|
4352
4744
|
function detectTemplate() {
|
|
4353
|
-
if (
|
|
4745
|
+
if (existsSync8("app")) {
|
|
4354
4746
|
console.log(`\uD83D\uDD0D Detected fullstack template (found app/ directory)
|
|
4355
4747
|
`);
|
|
4356
4748
|
return TEMPLATES["webapp-fullstack"];
|
|
4357
4749
|
}
|
|
4358
|
-
if (
|
|
4750
|
+
if (existsSync8("src")) {
|
|
4359
4751
|
console.log(`\uD83D\uDD0D Detected SPA template (found src/ directory)
|
|
4360
4752
|
`);
|
|
4361
4753
|
return TEMPLATES["webapp"];
|
|
@@ -4366,11 +4758,11 @@ function detectTemplate() {
|
|
|
4366
4758
|
}
|
|
4367
4759
|
function updatePackageJsonScripts() {
|
|
4368
4760
|
const pkgPath = "package.json";
|
|
4369
|
-
if (!
|
|
4761
|
+
if (!existsSync8(pkgPath)) {
|
|
4370
4762
|
console.log("⚠️ No package.json found, skipping scripts update");
|
|
4371
4763
|
return;
|
|
4372
4764
|
}
|
|
4373
|
-
const pkg = JSON.parse(
|
|
4765
|
+
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
4374
4766
|
if (!pkg.scripts) {
|
|
4375
4767
|
pkg.scripts = {};
|
|
4376
4768
|
}
|
|
@@ -4384,7 +4776,7 @@ function updatePackageJsonScripts() {
|
|
|
4384
4776
|
}
|
|
4385
4777
|
}
|
|
4386
4778
|
if (updated) {
|
|
4387
|
-
|
|
4779
|
+
writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
4388
4780
|
`, "utf-8");
|
|
4389
4781
|
console.log(`✅ package.json scripts updated
|
|
4390
4782
|
`);
|
|
@@ -4408,7 +4800,7 @@ function updateCliSkill() {
|
|
|
4408
4800
|
}
|
|
4409
4801
|
execSync3(`git fetch ${cliRemote}`, { stdio: "inherit" });
|
|
4410
4802
|
execSync3(`git checkout ${cliRemote}/main -- skills/ro-cli/SKILL.md`, { stdio: "inherit" });
|
|
4411
|
-
if (
|
|
4803
|
+
if (existsSync8("skills/ro-cli/SKILL.md")) {
|
|
4412
4804
|
mkdirSync2(".agent/skills/ro-cli", { recursive: true });
|
|
4413
4805
|
copyFileSync("skills/ro-cli/SKILL.md", ".agent/skills/ro-cli/SKILL.md");
|
|
4414
4806
|
rmSync2("skills", { recursive: true, force: true });
|
|
@@ -4441,9 +4833,10 @@ async function upgradeTemplate() {
|
|
|
4441
4833
|
const checkoutList = template.checkoutFiles.join(" ");
|
|
4442
4834
|
execSync3(`git checkout ${template.remoteName}/main -- ${checkoutList}`, { stdio: "inherit" });
|
|
4443
4835
|
for (const file of template.newFiles) {
|
|
4444
|
-
if (!
|
|
4836
|
+
if (!existsSync8(file)) {
|
|
4445
4837
|
console.log(`\uD83D\uDCC2 Checking out ${file}...`);
|
|
4446
4838
|
try {
|
|
4839
|
+
mkdirSync2(path3.dirname(file), { recursive: true });
|
|
4447
4840
|
execSync3(`git checkout ${template.remoteName}/main -- ${file}`, { stdio: "inherit" });
|
|
4448
4841
|
} catch {
|
|
4449
4842
|
console.log(`⚠️ Failed to checkout ${file}`);
|
|
@@ -4485,20 +4878,20 @@ var FILES = [
|
|
|
4485
4878
|
description: "GameSDK TypeScript definitions"
|
|
4486
4879
|
}
|
|
4487
4880
|
];
|
|
4488
|
-
async function downloadFile(url,
|
|
4881
|
+
async function downloadFile(url, path4, description) {
|
|
4489
4882
|
try {
|
|
4490
4883
|
console.log(`
|
|
4491
4884
|
\uD83D\uDCE5 Downloading ${description}...`);
|
|
4492
4885
|
console.log(` URL: ${url}`);
|
|
4493
|
-
console.log(` Path: ${
|
|
4886
|
+
console.log(` Path: ${path4}`);
|
|
4494
4887
|
const response = await fetch(url);
|
|
4495
4888
|
if (!response.ok) {
|
|
4496
4889
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
4497
4890
|
}
|
|
4498
4891
|
const content = await response.text();
|
|
4499
|
-
const dir = dirname2(
|
|
4892
|
+
const dir = dirname2(path4);
|
|
4500
4893
|
await mkdir(dir, { recursive: true });
|
|
4501
|
-
await writeFile(
|
|
4894
|
+
await writeFile(path4, content, "utf-8");
|
|
4502
4895
|
console.log(`✅ Downloaded ${description} (${content.length} bytes)
|
|
4503
4896
|
`);
|
|
4504
4897
|
return true;
|
|
@@ -4540,9 +4933,9 @@ async function downloadDocumentation(manifest) {
|
|
|
4540
4933
|
let failCount = 0;
|
|
4541
4934
|
for (const doc of manifest.documentation) {
|
|
4542
4935
|
const url = `${BASE_URL}/skills/${doc.file}`;
|
|
4543
|
-
const
|
|
4936
|
+
const path4 = join3(".agent", "skills", "game-sdk", doc.file);
|
|
4544
4937
|
const description = `${doc.title} (${doc.category})`;
|
|
4545
|
-
const success = await downloadFile(url,
|
|
4938
|
+
const success = await downloadFile(url, path4, description);
|
|
4546
4939
|
if (success) {
|
|
4547
4940
|
successCount++;
|
|
4548
4941
|
} else {
|
|
@@ -4626,15 +5019,15 @@ Available templates:
|
|
|
4626
5019
|
input: process.stdin,
|
|
4627
5020
|
output: process.stdout
|
|
4628
5021
|
});
|
|
4629
|
-
return new Promise((
|
|
5022
|
+
return new Promise((resolve4) => {
|
|
4630
5023
|
rl.question(`Select a template (1-${entries.length}): `, (answer) => {
|
|
4631
5024
|
rl.close();
|
|
4632
5025
|
const index = parseInt(answer, 10) - 1;
|
|
4633
5026
|
if (index >= 0 && index < entries.length) {
|
|
4634
|
-
|
|
5027
|
+
resolve4(entries[index].name);
|
|
4635
5028
|
} else {
|
|
4636
5029
|
console.error("Invalid selection, defaulting to 'webapp'");
|
|
4637
|
-
|
|
5030
|
+
resolve4("webapp");
|
|
4638
5031
|
}
|
|
4639
5032
|
});
|
|
4640
5033
|
});
|
|
@@ -4669,6 +5062,17 @@ function addAuthCommands(parent) {
|
|
|
4669
5062
|
return auth;
|
|
4670
5063
|
}
|
|
4671
5064
|
addAuthCommands(program);
|
|
5065
|
+
var globalConfig = program.command("global-config").description("Manage CMS-wide global configuration (game-config row)");
|
|
5066
|
+
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) => {
|
|
5067
|
+
await getGlobalConfig(options);
|
|
5068
|
+
});
|
|
5069
|
+
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");
|
|
5070
|
+
addGlobalConfigWriteOptions(globalConfig.command("set").description("Replace the `config` and/or `publicConfig` column on the global-config row")).action(async (options) => {
|
|
5071
|
+
await setGlobalConfig(options);
|
|
5072
|
+
});
|
|
5073
|
+
addGlobalConfigWriteOptions(globalConfig.command("patch").description("Merge-patch (RFC 7396) the `config` and/or `publicConfig` column. `null` values delete keys.")).action(async (options) => {
|
|
5074
|
+
await patchGlobalConfig(options);
|
|
5075
|
+
});
|
|
4672
5076
|
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) => {
|
|
4673
5077
|
let templateName;
|
|
4674
5078
|
if (options.template) {
|