@rodyssey/cli 0.1.10 → 0.2.1

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