@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.
Files changed (3) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.js +452 -48
  3. 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.1.9",
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("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
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/plain" }).end(`Login failed: ${error}`);
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: ${error}`));
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/plain" }).end("Invalid CLI login callback.");
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('<!doctype html><meta charset="utf-8"><title>Rodyssey CLI</title><body><h1>Login complete</h1><p>You can close this window and return to the terminal.</p></body>');
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 existsSync6, readFileSync as readFileSync5 } from "node:fs";
3913
- import { resolve as resolve2 } from "node:path";
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 existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
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 = resolve(process.cwd(), value);
3926
- const raw = existsSync5(maybePath) ? readFileSync4(maybePath, "utf-8") : value;
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
- writeFileSync3(options.out, `${output}
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 PROD_ENV = "production";
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 = resolve2(process.cwd(), value);
4122
- const raw = existsSync6(maybePath) ? readFileSync5(maybePath, "utf-8") : value;
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 prompt(question) {
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 isExplicitYes(answer) {
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 prompt(`
4570
+ const answer = await prompt2(`
4184
4571
  Deploy to production now? (y/N): `);
4185
- shouldDeploy = isExplicitYes(answer);
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(PROD_ENV, { override: true });
4584
+ loadEnv(PROD_ENV2, { override: true });
4198
4585
  process.env.WEBAPP_ID = webappId;
4199
4586
  process.env.DEPLOY_TOKEN = deployToken;
4200
- await deploy(PROD_ENV);
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 = resolve2(process.cwd(), PROD_ENV_FILE);
4212
- if (existsSync6(prodEnvPath)) {
4213
- const content = readFileSync5(prodEnvPath, "utf-8");
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 prompt("Pull these details from server? (Y/n): ");
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 ${PROD_ENV} CMS (ephemeral, not stored)...`);
4633
+ \uD83D\uDD10 Logging in to ${PROD_ENV2} CMS (ephemeral, not stored)...`);
4247
4634
  const session = await login({
4248
- env: PROD_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(PROD_ENV, options.cmsUrl);
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 ${PROD_ENV}...`);
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 ${PROD_ENV}.`);
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 existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, copyFileSync, rmSync as rmSync2 } from "node:fs";
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 (existsSync7("app")) {
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 (existsSync7("src")) {
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 (!existsSync7(pkgPath)) {
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(readFileSync6(pkgPath, "utf-8"));
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
- writeFileSync4(pkgPath, JSON.stringify(pkg, null, 2) + `
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 (existsSync7("skills/ro-cli/SKILL.md")) {
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 (!existsSync7(file)) {
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, path3, description) {
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: ${path3}`);
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(path3);
4892
+ const dir = dirname2(path4);
4500
4893
  await mkdir(dir, { recursive: true });
4501
- await writeFile(path3, content, "utf-8");
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 path3 = join3(".agent", "skills", "game-sdk", doc.file);
4936
+ const path4 = join3(".agent", "skills", "game-sdk", doc.file);
4544
4937
  const description = `${doc.title} (${doc.category})`;
4545
- const success = await downloadFile(url, path3, description);
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((resolve3) => {
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
- resolve3(entries[index].name);
5027
+ resolve4(entries[index].name);
4635
5028
  } else {
4636
5029
  console.error("Invalid selection, defaulting to 'webapp'");
4637
- resolve3("webapp");
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rodyssey/cli",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "description": "Scaffold new projects from airconcepts templates",
5
5
  "repository": {
6
6
  "type": "git",