@rodyssey/cli 0.1.10 → 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 (2) hide show
  1. package/dist/cli.js +439 -41
  2. 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.10",
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,7 @@ ${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";
4298
4685
  import path3 from "node:path";
4299
4686
  var TEMPLATES = {
4300
4687
  webapp: {
@@ -4355,12 +4742,12 @@ var CLI_SCRIPTS = {
4355
4742
  "upgrade-template": "bunx @rodyssey/cli@latest app upgrade-template"
4356
4743
  };
4357
4744
  function detectTemplate() {
4358
- if (existsSync7("app")) {
4745
+ if (existsSync8("app")) {
4359
4746
  console.log(`\uD83D\uDD0D Detected fullstack template (found app/ directory)
4360
4747
  `);
4361
4748
  return TEMPLATES["webapp-fullstack"];
4362
4749
  }
4363
- if (existsSync7("src")) {
4750
+ if (existsSync8("src")) {
4364
4751
  console.log(`\uD83D\uDD0D Detected SPA template (found src/ directory)
4365
4752
  `);
4366
4753
  return TEMPLATES["webapp"];
@@ -4371,11 +4758,11 @@ function detectTemplate() {
4371
4758
  }
4372
4759
  function updatePackageJsonScripts() {
4373
4760
  const pkgPath = "package.json";
4374
- if (!existsSync7(pkgPath)) {
4761
+ if (!existsSync8(pkgPath)) {
4375
4762
  console.log("⚠️ No package.json found, skipping scripts update");
4376
4763
  return;
4377
4764
  }
4378
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
4765
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
4379
4766
  if (!pkg.scripts) {
4380
4767
  pkg.scripts = {};
4381
4768
  }
@@ -4389,7 +4776,7 @@ function updatePackageJsonScripts() {
4389
4776
  }
4390
4777
  }
4391
4778
  if (updated) {
4392
- writeFileSync4(pkgPath, JSON.stringify(pkg, null, 2) + `
4779
+ writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + `
4393
4780
  `, "utf-8");
4394
4781
  console.log(`✅ package.json scripts updated
4395
4782
  `);
@@ -4413,7 +4800,7 @@ function updateCliSkill() {
4413
4800
  }
4414
4801
  execSync3(`git fetch ${cliRemote}`, { stdio: "inherit" });
4415
4802
  execSync3(`git checkout ${cliRemote}/main -- skills/ro-cli/SKILL.md`, { stdio: "inherit" });
4416
- if (existsSync7("skills/ro-cli/SKILL.md")) {
4803
+ if (existsSync8("skills/ro-cli/SKILL.md")) {
4417
4804
  mkdirSync2(".agent/skills/ro-cli", { recursive: true });
4418
4805
  copyFileSync("skills/ro-cli/SKILL.md", ".agent/skills/ro-cli/SKILL.md");
4419
4806
  rmSync2("skills", { recursive: true, force: true });
@@ -4446,7 +4833,7 @@ async function upgradeTemplate() {
4446
4833
  const checkoutList = template.checkoutFiles.join(" ");
4447
4834
  execSync3(`git checkout ${template.remoteName}/main -- ${checkoutList}`, { stdio: "inherit" });
4448
4835
  for (const file of template.newFiles) {
4449
- if (!existsSync7(file)) {
4836
+ if (!existsSync8(file)) {
4450
4837
  console.log(`\uD83D\uDCC2 Checking out ${file}...`);
4451
4838
  try {
4452
4839
  mkdirSync2(path3.dirname(file), { recursive: true });
@@ -4632,15 +5019,15 @@ Available templates:
4632
5019
  input: process.stdin,
4633
5020
  output: process.stdout
4634
5021
  });
4635
- return new Promise((resolve3) => {
5022
+ return new Promise((resolve4) => {
4636
5023
  rl.question(`Select a template (1-${entries.length}): `, (answer) => {
4637
5024
  rl.close();
4638
5025
  const index = parseInt(answer, 10) - 1;
4639
5026
  if (index >= 0 && index < entries.length) {
4640
- resolve3(entries[index].name);
5027
+ resolve4(entries[index].name);
4641
5028
  } else {
4642
5029
  console.error("Invalid selection, defaulting to 'webapp'");
4643
- resolve3("webapp");
5030
+ resolve4("webapp");
4644
5031
  }
4645
5032
  });
4646
5033
  });
@@ -4675,6 +5062,17 @@ function addAuthCommands(parent) {
4675
5062
  return auth;
4676
5063
  }
4677
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
+ });
4678
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) => {
4679
5077
  let templateName;
4680
5078
  if (options.template) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rodyssey/cli",
3
- "version": "0.1.10",
3
+ "version": "0.2.0",
4
4
  "description": "Scaffold new projects from airconcepts templates",
5
5
  "repository": {
6
6
  "type": "git",