@rodyssey/cli 0.1.4 → 0.1.5

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 +534 -34
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2069,19 +2069,244 @@ var {
2069
2069
  Help
2070
2070
  } = import__.default;
2071
2071
 
2072
+ // src/auth.ts
2073
+ import { spawn } from "node:child_process";
2074
+ import { createHash, randomBytes } from "node:crypto";
2075
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2076
+ import { createServer } from "node:http";
2077
+ import { dirname, join } from "node:path";
2078
+ import { homedir } from "node:os";
2079
+ var CMS_BASE_URLS = {
2080
+ local: "http://localhost:5176",
2081
+ development: "https://development-cms.rodyssey.ai",
2082
+ staging: "https://staging-cms.rodyssey.ai",
2083
+ production: "https://cms.rodyssey.ai"
2084
+ };
2085
+ var CONFIG_FILE = join(homedir(), ".rodyssey", "config.json");
2086
+ function readConfig() {
2087
+ if (!existsSync(CONFIG_FILE))
2088
+ return {};
2089
+ try {
2090
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
2091
+ } catch {
2092
+ return {};
2093
+ }
2094
+ }
2095
+ function writeConfig(config) {
2096
+ mkdirSync(dirname(CONFIG_FILE), { recursive: true });
2097
+ writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}
2098
+ `, "utf-8");
2099
+ }
2100
+ function getAuthConfig(config) {
2101
+ const auth = config.auth;
2102
+ if (!auth || typeof auth !== "object" || Array.isArray(auth))
2103
+ return {};
2104
+ return auth;
2105
+ }
2106
+ function isObject(value) {
2107
+ return !!value && typeof value === "object" && !Array.isArray(value);
2108
+ }
2109
+ function extractToken(payload) {
2110
+ if (!isObject(payload))
2111
+ return;
2112
+ const candidates = [
2113
+ payload.accessToken,
2114
+ payload.token,
2115
+ payload.jwt,
2116
+ payload.idToken,
2117
+ isObject(payload.session) ? payload.session.token : undefined,
2118
+ isObject(payload.session) ? payload.session.accessToken : undefined,
2119
+ isObject(payload.data) ? payload.data.accessToken : undefined,
2120
+ isObject(payload.data) ? payload.data.token : undefined
2121
+ ];
2122
+ return candidates.find((candidate) => typeof candidate === "string" && candidate.length > 0);
2123
+ }
2124
+ function extractUser(payload) {
2125
+ if (!isObject(payload))
2126
+ return;
2127
+ return payload.user ?? (isObject(payload.data) ? payload.data.user : undefined);
2128
+ }
2129
+ async function readResponsePayload(response) {
2130
+ const text = await response.text();
2131
+ if (!text)
2132
+ return;
2133
+ try {
2134
+ return JSON.parse(text);
2135
+ } catch {
2136
+ return { message: text };
2137
+ }
2138
+ }
2139
+ function base64Url(buffer) {
2140
+ return buffer.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
2141
+ }
2142
+ function buildPkcePair() {
2143
+ const verifier = base64Url(randomBytes(32));
2144
+ const challenge = base64Url(createHash("sha256").update(verifier).digest());
2145
+ return { verifier, challenge };
2146
+ }
2147
+ function openBrowser(url) {
2148
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
2149
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
2150
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
2151
+ child.unref();
2152
+ }
2153
+ function resolveCmsUrl(env, cmsUrl) {
2154
+ const resolved = cmsUrl || process.env.CMS_URL || CMS_BASE_URLS[env];
2155
+ if (!resolved) {
2156
+ throw new Error(`Unknown CMS environment "${env}". Available: ${Object.keys(CMS_BASE_URLS).join(", ")}`);
2157
+ }
2158
+ return resolved.replace(/\/$/, "");
2159
+ }
2160
+ function getStoredSession(env) {
2161
+ return getAuthConfig(readConfig())[env];
2162
+ }
2163
+ function storeSession(session) {
2164
+ const config = readConfig();
2165
+ const auth = getAuthConfig(config);
2166
+ auth[session.env] = session;
2167
+ config.auth = auth;
2168
+ writeConfig(config);
2169
+ }
2170
+ function resolveSessionToken(env) {
2171
+ const token = process.env.CMS_TOKEN || getStoredSession(env)?.token;
2172
+ if (!token) {
2173
+ throw new Error(`No CMS auth token found for [${env}]. Run \`ro app auth login --env ${env}\` first.`);
2174
+ }
2175
+ return token;
2176
+ }
2177
+ async function login(options) {
2178
+ const cmsUrl = resolveCmsUrl(options.env, options.cmsUrl);
2179
+ const state = base64Url(randomBytes(24));
2180
+ const pkce = buildPkcePair();
2181
+ const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
2182
+ const callback = await new Promise((resolve, reject) => {
2183
+ const server = createServer((request, response2) => {
2184
+ const requestUrl = new URL(request.url || "/", "http://127.0.0.1");
2185
+ if (requestUrl.pathname !== "/auth/callback") {
2186
+ response2.writeHead(404).end("Not found");
2187
+ return;
2188
+ }
2189
+ const code = requestUrl.searchParams.get("code");
2190
+ const returnedState = requestUrl.searchParams.get("state");
2191
+ const error = requestUrl.searchParams.get("error");
2192
+ if (error) {
2193
+ clearTimeout(timer);
2194
+ response2.writeHead(400, { "Content-Type": "text/plain" }).end(`Login failed: ${error}`);
2195
+ server.close();
2196
+ reject(new Error(`CMS login failed: ${error}`));
2197
+ return;
2198
+ }
2199
+ if (!code || returnedState !== state) {
2200
+ clearTimeout(timer);
2201
+ response2.writeHead(400, { "Content-Type": "text/plain" }).end("Invalid CLI login callback.");
2202
+ server.close();
2203
+ reject(new Error("Invalid CMS login callback. Missing code or state mismatch."));
2204
+ return;
2205
+ }
2206
+ 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>');
2207
+ const address = server.address();
2208
+ clearTimeout(timer);
2209
+ server.close();
2210
+ resolve({ code, redirectUri: `http://127.0.0.1:${address.port}/auth/callback` });
2211
+ });
2212
+ const timer = setTimeout(() => {
2213
+ server.close();
2214
+ reject(new Error("CMS login timed out before the callback was received."));
2215
+ }, timeoutMs);
2216
+ server.on("error", (error) => {
2217
+ clearTimeout(timer);
2218
+ reject(error);
2219
+ });
2220
+ server.listen(options.callbackPort ?? 0, "127.0.0.1", () => {
2221
+ const address = server.address();
2222
+ const redirectUri = `http://127.0.0.1:${address.port}/auth/callback`;
2223
+ const authorizationUrl = new URL(options.loginUrl || `${cmsUrl}/auth/cli-login`);
2224
+ authorizationUrl.searchParams.set("redirect_uri", redirectUri);
2225
+ authorizationUrl.searchParams.set("state", state);
2226
+ authorizationUrl.searchParams.set("code_challenge", pkce.challenge);
2227
+ authorizationUrl.searchParams.set("code_challenge_method", "S256");
2228
+ console.log(`Open this URL to log in:
2229
+ ${authorizationUrl.toString()}
2230
+ `);
2231
+ if (options.open !== false) {
2232
+ openBrowser(authorizationUrl.toString());
2233
+ }
2234
+ });
2235
+ });
2236
+ const tokenUrl = options.tokenUrl || `${cmsUrl}/api/auth/cli-token`;
2237
+ const response = await fetch(tokenUrl, {
2238
+ method: "POST",
2239
+ headers: {
2240
+ Accept: "application/json",
2241
+ "Content-Type": "application/json"
2242
+ },
2243
+ body: JSON.stringify({
2244
+ code: callback.code,
2245
+ redirect_uri: callback.redirectUri,
2246
+ code_verifier: pkce.verifier
2247
+ })
2248
+ });
2249
+ const payload = await readResponsePayload(response);
2250
+ if (!response.ok) {
2251
+ throw new Error(`CMS token exchange failed: ${response.status} ${response.statusText}
2252
+ ${JSON.stringify(payload, null, 2)}`);
2253
+ }
2254
+ const token = extractToken(payload);
2255
+ if (!token) {
2256
+ throw new Error(`CMS token response did not include an auth token:
2257
+ ${JSON.stringify(payload, null, 2)}`);
2258
+ }
2259
+ const session = {
2260
+ env: options.env,
2261
+ cmsUrl,
2262
+ token,
2263
+ user: extractUser(payload)
2264
+ };
2265
+ storeSession(session);
2266
+ return session;
2267
+ }
2268
+ async function me(options) {
2269
+ const storedSession = getStoredSession(options.env);
2270
+ if (!options.remote) {
2271
+ if (!storedSession) {
2272
+ throw new Error(`No local CMS session found for [${options.env}]. Run \`ro app auth login --env ${options.env}\` first.`);
2273
+ }
2274
+ return {
2275
+ env: storedSession.env,
2276
+ cmsUrl: storedSession.cmsUrl,
2277
+ loggedIn: true,
2278
+ user: storedSession.user ?? null
2279
+ };
2280
+ }
2281
+ const cmsUrl = resolveCmsUrl(options.env, options.cmsUrl || storedSession?.cmsUrl);
2282
+ const response = await fetch(options.meUrl || `${cmsUrl}/api/auth/me`, {
2283
+ method: "GET",
2284
+ headers: {
2285
+ Accept: "application/json",
2286
+ Authorization: `Bearer ${resolveSessionToken(options.env)}`
2287
+ }
2288
+ });
2289
+ const payload = await readResponsePayload(response);
2290
+ if (!response.ok) {
2291
+ throw new Error(`CMS me failed: ${response.status} ${response.statusText}
2292
+ ${JSON.stringify(payload, null, 2)}`);
2293
+ }
2294
+ return payload;
2295
+ }
2296
+
2072
2297
  // src/create.ts
2073
2298
  import { execSync } from "node:child_process";
2074
- import { existsSync as existsSync2, rmSync } from "node:fs";
2299
+ import { existsSync as existsSync3, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
2075
2300
  import path2 from "node:path";
2076
2301
 
2077
2302
  // src/utils.ts
2078
- import { readFileSync, writeFileSync, existsSync } from "node:fs";
2303
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "node:fs";
2079
2304
  import path from "node:path";
2080
2305
  function replaceInFile(filePath, search, replace) {
2081
- const content = readFileSync(filePath, "utf-8");
2306
+ const content = readFileSync2(filePath, "utf-8");
2082
2307
  const updated = content.replaceAll(search, replace);
2083
2308
  if (content !== updated) {
2084
- writeFileSync(filePath, updated, "utf-8");
2309
+ writeFileSync2(filePath, updated, "utf-8");
2085
2310
  }
2086
2311
  }
2087
2312
  function replaceInFiles(dir, filenames, search, replace) {
@@ -2099,9 +2324,9 @@ function loadEnv(envName) {
2099
2324
  }
2100
2325
  files.push(".env");
2101
2326
  for (const file of files) {
2102
- if (!existsSync(file))
2327
+ if (!existsSync2(file))
2103
2328
  continue;
2104
- const content = readFileSync(file, "utf-8");
2329
+ const content = readFileSync2(file, "utf-8");
2105
2330
  for (const line of content.split(`
2106
2331
  `)) {
2107
2332
  const trimmed = line.trim();
@@ -2129,14 +2354,88 @@ var REPLACEMENT_FILES = {
2129
2354
  webapp: ["package.json", "index.html"],
2130
2355
  "webapp-fullstack": ["package.json", "wrangler.jsonc"]
2131
2356
  };
2132
- async function create(projectName, repoUrl, templateName) {
2357
+ function isObject2(value) {
2358
+ return !!value && typeof value === "object" && !Array.isArray(value);
2359
+ }
2360
+ function pickString(...values) {
2361
+ return values.find((value) => typeof value === "string" && value.length > 0);
2362
+ }
2363
+ function nestedObject(value, key) {
2364
+ if (!isObject2(value))
2365
+ return;
2366
+ const nested = value[key];
2367
+ return isObject2(nested) ? nested : undefined;
2368
+ }
2369
+ function extractWebappId(payload) {
2370
+ const webapp = nestedObject(payload, "webapp");
2371
+ return pickString(webapp?.id, webapp?.webappId);
2372
+ }
2373
+ function extractDeployToken(payload) {
2374
+ const webapp = nestedObject(payload, "webapp");
2375
+ return pickString(webapp?.deployToken, webapp?.deploymentToken);
2376
+ }
2377
+ function upsertEnvValue(content, key, value) {
2378
+ const line = `${key}=${value}`;
2379
+ const pattern = new RegExp(`^${key}=.*$`, "m");
2380
+ if (pattern.test(content))
2381
+ return content.replace(pattern, line);
2382
+ return `${content}${content.endsWith(`
2383
+ `) || content.length === 0 ? "" : `
2384
+ `}${line}
2385
+ `;
2386
+ }
2387
+ function writeProjectEnv(projectDir, provisioned) {
2388
+ const envFile = path2.join(projectDir, ".env");
2389
+ let content = existsSync3(envFile) ? readFileSync3(envFile, "utf-8") : "";
2390
+ content = upsertEnvValue(content, "WEBAPP_ID", provisioned.webappId);
2391
+ content = upsertEnvValue(content, "DEPLOY_TOKEN", provisioned.deployToken);
2392
+ writeFileSync3(envFile, content, "utf-8");
2393
+ }
2394
+ async function provisionWebapp(projectName, options) {
2395
+ const cmsUrl = resolveCmsUrl(options.env, options.cmsUrl);
2396
+ const token = resolveSessionToken(options.env);
2397
+ const createUrl = options.createUrl || `${cmsUrl}/api/cli/webapps/create`;
2398
+ const createResponse = await fetch(createUrl, {
2399
+ method: "POST",
2400
+ headers: {
2401
+ Accept: "application/json",
2402
+ Authorization: `Bearer ${token}`,
2403
+ "Content-Type": "application/json"
2404
+ },
2405
+ body: JSON.stringify({ title: projectName })
2406
+ });
2407
+ const createPayload = await readResponsePayload(createResponse);
2408
+ if (!createResponse.ok) {
2409
+ throw new Error(`CMS webapp creation failed: ${createResponse.status} ${createResponse.statusText}
2410
+ ${JSON.stringify(createPayload, null, 2)}`);
2411
+ }
2412
+ const webappId = extractWebappId(createPayload);
2413
+ if (!webappId) {
2414
+ throw new Error(`CMS create response did not include webapp.id:
2415
+ ${JSON.stringify(createPayload, null, 2)}`);
2416
+ }
2417
+ const deployToken = extractDeployToken(createPayload);
2418
+ if (!deployToken) {
2419
+ throw new Error(`CMS create response did not include webapp.deployToken:
2420
+ ${JSON.stringify(createPayload, null, 2)}`);
2421
+ }
2422
+ return {
2423
+ webappId,
2424
+ deployToken
2425
+ };
2426
+ }
2427
+ async function create(projectName, repoUrl, templateName, autoCreate) {
2133
2428
  const targetDir = path2.resolve(process.cwd(), projectName);
2134
- if (existsSync2(targetDir)) {
2429
+ if (existsSync3(targetDir)) {
2135
2430
  console.error(`
2136
2431
  ✖ Directory "${projectName}" already exists.
2137
2432
  `);
2138
2433
  process.exit(1);
2139
2434
  }
2435
+ if (autoCreate?.enabled) {
2436
+ resolveCmsUrl(autoCreate.env, autoCreate.cmsUrl);
2437
+ resolveSessionToken(autoCreate.env);
2438
+ }
2140
2439
  console.log(`
2141
2440
  ⏳ Cloning template "${templateName}"...
2142
2441
  `);
@@ -2152,7 +2451,7 @@ async function create(projectName, repoUrl, templateName) {
2152
2451
  process.exit(1);
2153
2452
  }
2154
2453
  const gitDir = path2.join(targetDir, ".git");
2155
- if (existsSync2(gitDir)) {
2454
+ if (existsSync3(gitDir)) {
2156
2455
  rmSync(gitDir, { recursive: true, force: true });
2157
2456
  }
2158
2457
  const filesToReplace = REPLACEMENT_FILES[templateName] ?? [];
@@ -2162,6 +2461,13 @@ async function create(projectName, repoUrl, templateName) {
2162
2461
  replaceInFiles(targetDir, filesToReplace, PLACEHOLDER_LOWER, projectName.toLowerCase());
2163
2462
  }
2164
2463
  execSync("git init", { stdio: "ignore", cwd: targetDir });
2464
+ if (autoCreate?.enabled) {
2465
+ console.log(` \uD83C\uDF10 Creating CMS webapp in [${autoCreate.env}]...`);
2466
+ const provisioned = await provisionWebapp(projectName, autoCreate);
2467
+ writeProjectEnv(targetDir, provisioned);
2468
+ console.log(` \uD83D\uDD10 Wrote WEBAPP_ID and DEPLOY_TOKEN to .env`);
2469
+ console.log(` \uD83D\uDCCD Webapp ID: ${provisioned.webappId}`);
2470
+ }
2165
2471
  console.log(`
2166
2472
  ✅ Project "${projectName}" created successfully!
2167
2473
 
@@ -2175,8 +2481,8 @@ async function create(projectName, repoUrl, templateName) {
2175
2481
 
2176
2482
  // src/deploy.ts
2177
2483
  import { execSync as execSync2 } from "node:child_process";
2178
- import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync, statSync, unlinkSync } from "node:fs";
2179
- import { join } from "node:path";
2484
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync, statSync, unlinkSync } from "node:fs";
2485
+ import { join as join2 } from "node:path";
2180
2486
 
2181
2487
  // node_modules/mime/dist/types/other.js
2182
2488
  var types = {
@@ -3373,11 +3679,11 @@ var ZIP_FILE = "webapp-build.zip";
3373
3679
  var MAX_FILES_PER_BATCH = 5;
3374
3680
  var MAX_SIZE_PER_BATCH = 30 * 1024 * 1024;
3375
3681
  function getAllFiles(dirPath, arrayOfFiles = []) {
3376
- if (!existsSync3(dirPath))
3682
+ if (!existsSync4(dirPath))
3377
3683
  return arrayOfFiles;
3378
3684
  const files = readdirSync(dirPath);
3379
3685
  files.forEach(function(f) {
3380
- const fullPath = join(dirPath, f);
3686
+ const fullPath = join2(dirPath, f);
3381
3687
  if (statSync(fullPath).isDirectory()) {
3382
3688
  arrayOfFiles = getAllFiles(fullPath, arrayOfFiles);
3383
3689
  } else {
@@ -3387,7 +3693,7 @@ function getAllFiles(dirPath, arrayOfFiles = []) {
3387
3693
  return arrayOfFiles;
3388
3694
  }
3389
3695
  function fileToBlob(filePath) {
3390
- const buffer = readFileSync2(filePath);
3696
+ const buffer = readFileSync4(filePath);
3391
3697
  return new Blob([buffer], { type: src_default.getType(filePath) || "application/octet-stream" });
3392
3698
  }
3393
3699
  async function deploy(env = "development", overrides = {}) {
@@ -3474,7 +3780,7 @@ ${errorText}`);
3474
3780
  console.log(`\uD83D\uDCDC Step 3: Setting up ${scriptFiles.length} scripts (APIs & Crons)...`);
3475
3781
  const scriptsPayload = { api: {}, cron: {}, cronConfig: null };
3476
3782
  for (const f of scriptFiles) {
3477
- const content = readFileSync2(f, "utf-8");
3783
+ const content = readFileSync4(f, "utf-8");
3478
3784
  const relativePath = f.substring(BUILD_DIR.length + 1).replace(/\\/g, "/");
3479
3785
  if (relativePath === "cron-jobs/cron.config.json") {
3480
3786
  scriptsPayload.cronConfig = JSON.parse(content);
@@ -3514,7 +3820,7 @@ ${errorText}`);
3514
3820
  console.log(`✅ Created ${ZIP_FILE}
3515
3821
  `);
3516
3822
  console.log("☁️ Step 5: Deploying HTML zip to server...");
3517
- const zipBuffer = readFileSync2(ZIP_FILE);
3823
+ const zipBuffer = readFileSync4(ZIP_FILE);
3518
3824
  try {
3519
3825
  const response = await fetch(DEPLOY_URL, {
3520
3826
  method: "POST",
@@ -3537,7 +3843,7 @@ ${errorText}`);
3537
3843
  console.error("❌ Deploy failed:", error);
3538
3844
  throw error;
3539
3845
  } finally {
3540
- if (existsSync3(ZIP_FILE)) {
3846
+ if (existsSync4(ZIP_FILE)) {
3541
3847
  unlinkSync(ZIP_FILE);
3542
3848
  console.log(`
3543
3849
  \uD83E\uDDF9 Cleaned up ${ZIP_FILE}`);
@@ -3549,7 +3855,7 @@ ${errorText}`);
3549
3855
 
3550
3856
  // src/upgrade-template.ts
3551
3857
  import { execSync as execSync3 } from "node:child_process";
3552
- import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync, copyFileSync, rmSync as rmSync2 } from "node:fs";
3858
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, copyFileSync, rmSync as rmSync2 } from "node:fs";
3553
3859
  var TEMPLATES = {
3554
3860
  webapp: {
3555
3861
  name: "webapp (SPA)",
@@ -3579,15 +3885,17 @@ var TEMPLATES = {
3579
3885
  var CLI_SCRIPTS = {
3580
3886
  "link-game-sdk": "bunx @rodyssey/cli@latest app update-game-sdk",
3581
3887
  deploy: "bunx @rodyssey/cli@latest app deploy",
3888
+ "get-webapp-config": "bunx @rodyssey/cli@latest app config get",
3889
+ "update-webapp-config": "bunx @rodyssey/cli@latest app config set",
3582
3890
  "upgrade-template": "bunx @rodyssey/cli@latest app upgrade-template"
3583
3891
  };
3584
3892
  function detectTemplate() {
3585
- if (existsSync4("app")) {
3893
+ if (existsSync5("app")) {
3586
3894
  console.log(`\uD83D\uDD0D Detected fullstack template (found app/ directory)
3587
3895
  `);
3588
3896
  return TEMPLATES["webapp-fullstack"];
3589
3897
  }
3590
- if (existsSync4("src")) {
3898
+ if (existsSync5("src")) {
3591
3899
  console.log(`\uD83D\uDD0D Detected SPA template (found src/ directory)
3592
3900
  `);
3593
3901
  return TEMPLATES["webapp"];
@@ -3598,11 +3906,11 @@ function detectTemplate() {
3598
3906
  }
3599
3907
  function updatePackageJsonScripts() {
3600
3908
  const pkgPath = "package.json";
3601
- if (!existsSync4(pkgPath)) {
3909
+ if (!existsSync5(pkgPath)) {
3602
3910
  console.log("⚠️ No package.json found, skipping scripts update");
3603
3911
  return;
3604
3912
  }
3605
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
3913
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
3606
3914
  if (!pkg.scripts) {
3607
3915
  pkg.scripts = {};
3608
3916
  }
@@ -3616,7 +3924,7 @@ function updatePackageJsonScripts() {
3616
3924
  }
3617
3925
  }
3618
3926
  if (updated) {
3619
- writeFileSync2(pkgPath, JSON.stringify(pkg, null, 2) + `
3927
+ writeFileSync4(pkgPath, JSON.stringify(pkg, null, 2) + `
3620
3928
  `, "utf-8");
3621
3929
  console.log(`✅ package.json scripts updated
3622
3930
  `);
@@ -3640,8 +3948,8 @@ function updateCliSkill() {
3640
3948
  }
3641
3949
  execSync3(`git fetch ${cliRemote}`, { stdio: "inherit" });
3642
3950
  execSync3(`git checkout ${cliRemote}/main -- skills/ro-cli/SKILL.md`, { stdio: "inherit" });
3643
- if (existsSync4("skills/ro-cli/SKILL.md")) {
3644
- mkdirSync(".agent/skills/ro-cli", { recursive: true });
3951
+ if (existsSync5("skills/ro-cli/SKILL.md")) {
3952
+ mkdirSync2(".agent/skills/ro-cli", { recursive: true });
3645
3953
  copyFileSync("skills/ro-cli/SKILL.md", ".agent/skills/ro-cli/SKILL.md");
3646
3954
  rmSync2("skills", { recursive: true, force: true });
3647
3955
  console.log(` ✅ CLI skill updated
@@ -3673,7 +3981,7 @@ async function upgradeTemplate() {
3673
3981
  const checkoutList = template.checkoutFiles.join(" ");
3674
3982
  execSync3(`git checkout ${template.remoteName}/main -- ${checkoutList}`, { stdio: "inherit" });
3675
3983
  for (const file of template.newFiles) {
3676
- if (!existsSync4(file)) {
3984
+ if (!existsSync5(file)) {
3677
3985
  console.log(`\uD83D\uDCC2 Checking out ${file}...`);
3678
3986
  try {
3679
3987
  execSync3(`git checkout ${template.remoteName}/main -- ${file}`, { stdio: "inherit" });
@@ -3698,7 +4006,7 @@ async function upgradeTemplate() {
3698
4006
 
3699
4007
  // src/update-game-sdk.ts
3700
4008
  import { mkdir, writeFile } from "node:fs/promises";
3701
- import { dirname, join as join2 } from "node:path";
4009
+ import { dirname as dirname2, join as join3 } from "node:path";
3702
4010
  var BASE_URL = "https://development-app.rodyssey.ai";
3703
4011
  var FILES = [
3704
4012
  {
@@ -3728,7 +4036,7 @@ async function downloadFile(url, path3, description) {
3728
4036
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
3729
4037
  }
3730
4038
  const content = await response.text();
3731
- const dir = dirname(path3);
4039
+ const dir = dirname2(path3);
3732
4040
  await mkdir(dir, { recursive: true });
3733
4041
  await writeFile(path3, content, "utf-8");
3734
4042
  console.log(`✅ Downloaded ${description} (${content.length} bytes)
@@ -3772,7 +4080,7 @@ async function downloadDocumentation(manifest) {
3772
4080
  let failCount = 0;
3773
4081
  for (const doc of manifest.documentation) {
3774
4082
  const url = `${BASE_URL}/skills/${doc.file}`;
3775
- const path3 = join2(".agent", "skills", "game-sdk", doc.file);
4083
+ const path3 = join3(".agent", "skills", "game-sdk", doc.file);
3776
4084
  const description = `${doc.title} (${doc.category})`;
3777
4085
  const success = await downloadFile(url, path3, description);
3778
4086
  if (success) {
@@ -3831,6 +4139,162 @@ async function updateGameSdk() {
3831
4139
  }
3832
4140
  }
3833
4141
 
4142
+ // src/update-webapp-config.ts
4143
+ import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "node:fs";
4144
+ import { resolve } from "node:path";
4145
+ var CONFIG_URLS = {
4146
+ local: "http://localhost:5176/api/webapps/config",
4147
+ development: "https://development-cms.rodyssey.ai/api/webapps/config",
4148
+ staging: "https://staging-cms.rodyssey.ai/api/webapps/config",
4149
+ production: "https://cms.rodyssey.ai/api/webapps/config"
4150
+ };
4151
+ function parseJsonOption(value, optionName) {
4152
+ const maybePath = resolve(process.cwd(), value);
4153
+ const raw = existsSync6(maybePath) ? readFileSync6(maybePath, "utf-8") : value;
4154
+ try {
4155
+ const parsed = JSON.parse(raw);
4156
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4157
+ throw new Error("value must be a JSON object");
4158
+ }
4159
+ return parsed;
4160
+ } catch (error) {
4161
+ const message = error instanceof Error ? error.message : String(error);
4162
+ throw new Error(`Invalid ${optionName}: ${message}`);
4163
+ }
4164
+ }
4165
+ function resolveConfigUrl(options, required = true) {
4166
+ const configUrl = options.url || process.env.WEBAPP_CONFIG_URL || CONFIG_URLS[options.env];
4167
+ if (!configUrl) {
4168
+ if (!required)
4169
+ return;
4170
+ console.error("❌ Error: no webapp config endpoint configured.");
4171
+ console.error(`\uD83D\uDCA1 Use one of these environments: ${Object.keys(CONFIG_URLS).join(", ")}, pass --url, or set WEBAPP_CONFIG_URL in your .env file.`);
4172
+ process.exit(1);
4173
+ }
4174
+ const url = new URL(configUrl);
4175
+ if (!options.host && !options.port) {
4176
+ return url.toString().replace(/\/$/, "");
4177
+ }
4178
+ if (options.host)
4179
+ url.hostname = options.host;
4180
+ if (options.port)
4181
+ url.port = String(options.port);
4182
+ return url.toString().replace(/\/$/, "");
4183
+ }
4184
+ function buildConfigPayload(options) {
4185
+ const payload = options.config ? parseJsonOption(options.config, "--config") : {};
4186
+ if (options.title !== undefined)
4187
+ payload.title = options.title;
4188
+ if (options.description !== undefined)
4189
+ payload.description = options.description;
4190
+ if (options.coverImg !== undefined)
4191
+ payload.coverImg = options.coverImg;
4192
+ if (options.localization !== undefined) {
4193
+ payload.localization = parseJsonOption(options.localization, "--localization");
4194
+ }
4195
+ return payload;
4196
+ }
4197
+ function resolveWebappId(webappId) {
4198
+ const resolved = webappId || process.env.WEBAPP_ID;
4199
+ if (!resolved) {
4200
+ console.error("❌ Error: WEBAPP_ID is not set. Pass --webapp-id or set it in your .env file.");
4201
+ process.exit(1);
4202
+ }
4203
+ return resolved;
4204
+ }
4205
+ function ensureDeployToken(env) {
4206
+ if (process.env.DEPLOY_TOKEN)
4207
+ return;
4208
+ console.error("❌ Error: DEPLOY_TOKEN is not set in environment variables.");
4209
+ console.info(`\uD83D\uDCA1 Please check your .env or .env.${env} file.`);
4210
+ process.exit(1);
4211
+ }
4212
+ function getConfigUrl(options, required = true) {
4213
+ return resolveConfigUrl(options, required);
4214
+ }
4215
+ async function getWebappConfig(options) {
4216
+ loadEnv(options.env);
4217
+ const webappId = resolveWebappId(options.webappId);
4218
+ const CONFIG_URL = getConfigUrl(options);
4219
+ if (!CONFIG_URL)
4220
+ return;
4221
+ ensureDeployToken(options.env);
4222
+ const url = new URL(CONFIG_URL);
4223
+ url.searchParams.set("webappId", webappId);
4224
+ console.log(`⚙️ Pulling webapp config for [${options.env}] environment...`);
4225
+ console.log(`\uD83D\uDCCD Config URL: ${url.toString()}`);
4226
+ console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
4227
+ `);
4228
+ const response = await fetch(url, {
4229
+ method: "GET",
4230
+ headers: {
4231
+ Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
4232
+ Accept: "application/json"
4233
+ }
4234
+ });
4235
+ if (!response.ok) {
4236
+ const errorText = await response.text();
4237
+ throw new Error(`Config fetch failed: ${response.status} ${response.statusText}
4238
+ ${errorText}`);
4239
+ }
4240
+ const config = await response.json();
4241
+ const output = JSON.stringify(config, null, 2);
4242
+ if (options.out) {
4243
+ writeFileSync5(options.out, `${output}
4244
+ `, "utf-8");
4245
+ console.log(`✅ Webapp config written to ${options.out}`);
4246
+ return;
4247
+ }
4248
+ console.log(output);
4249
+ }
4250
+ async function updateWebappConfig(options) {
4251
+ loadEnv(options.env);
4252
+ const webappId = resolveWebappId(options.webappId);
4253
+ const config = buildConfigPayload(options);
4254
+ if (Object.keys(config).length === 0) {
4255
+ console.error("❌ Error: no config fields provided. Use --title, --cover-img, --localization, or --config.");
4256
+ process.exit(1);
4257
+ }
4258
+ const payload = { webappId, config };
4259
+ const CONFIG_URL = getConfigUrl(options, !options.dryRun);
4260
+ if (!options.dryRun)
4261
+ ensureDeployToken(options.env);
4262
+ console.log(`⚙️ Updating webapp config for [${options.env}] environment...`);
4263
+ if (CONFIG_URL) {
4264
+ console.log(`\uD83D\uDCCD Config URL: ${CONFIG_URL}`);
4265
+ }
4266
+ console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
4267
+ `);
4268
+ if (options.dryRun) {
4269
+ console.log("\uD83E\uDDEA Dry run payload:");
4270
+ console.log(JSON.stringify(payload, null, 2));
4271
+ return;
4272
+ }
4273
+ if (!CONFIG_URL)
4274
+ return;
4275
+ const response = await fetch(CONFIG_URL, {
4276
+ method: "PATCH",
4277
+ headers: {
4278
+ Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
4279
+ "Content-Type": "application/json"
4280
+ },
4281
+ body: JSON.stringify(payload)
4282
+ });
4283
+ if (!response.ok) {
4284
+ const errorText = await response.text();
4285
+ throw new Error(`Config update failed: ${response.status} ${response.statusText}
4286
+ ${errorText}`);
4287
+ }
4288
+ const result = await response.json().catch(() => {
4289
+ return;
4290
+ });
4291
+ console.log("✅ Webapp config updated");
4292
+ if (result !== undefined) {
4293
+ console.log(`
4294
+ \uD83D\uDCCB Update result:`, result);
4295
+ }
4296
+ }
4297
+
3834
4298
  // src/cli.ts
3835
4299
  var TEMPLATES2 = {
3836
4300
  webapp: {
@@ -3858,22 +4322,28 @@ Available templates:
3858
4322
  input: process.stdin,
3859
4323
  output: process.stdout
3860
4324
  });
3861
- return new Promise((resolve) => {
4325
+ return new Promise((resolve2) => {
3862
4326
  rl.question(`Select a template (1-${entries.length}): `, (answer) => {
3863
4327
  rl.close();
3864
4328
  const index = parseInt(answer, 10) - 1;
3865
4329
  if (index >= 0 && index < entries.length) {
3866
- resolve(entries[index].name);
4330
+ resolve2(entries[index].name);
3867
4331
  } else {
3868
4332
  console.error("Invalid selection, defaulting to 'webapp'");
3869
- resolve("webapp");
4333
+ resolve2("webapp");
3870
4334
  }
3871
4335
  });
3872
4336
  });
3873
4337
  }
3874
4338
  program.name("@rodyssey/cli").description("Airconcepts CLI toolkit").version("0.1.0");
3875
4339
  var app = program.command("app").description("Manage webapp projects");
3876
- app.command("create").argument("<project-name>", "Name of the project to create").option("-t, --template <template>", "Template to use (webapp | webapp-fullstack)").description("Create a new project from a template").action(async (projectName, options) => {
4340
+ function addConfigTargetOptions(command) {
4341
+ return command.option("-e, --env <environment>", "Target environment (local | development | staging | production)", "development").option("--url <url>", "Override the config endpoint URL").option("--host <host>", "Override the config endpoint host").option("--port <port>", "Override the config endpoint port", parseInt).option("--webapp-id <id>", "Webapp ID. Defaults to WEBAPP_ID from .env");
4342
+ }
4343
+ function addConfigSetOptions(command) {
4344
+ return addConfigTargetOptions(command).option("--title <title>", "Webapp title").option("--description <description>", "Webapp description").option("--cover-img <url>", "Cover image URL").option("--localization <json-or-file>", "Localization JSON object or path to a JSON file").option("--config <json-or-file>", "Full config JSON object or path to a JSON file").option("--dry-run", "Print the request payload without sending it");
4345
+ }
4346
+ 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) => {
3877
4347
  let templateName;
3878
4348
  if (options.template) {
3879
4349
  if (!(options.template in TEMPLATES2)) {
@@ -3885,7 +4355,12 @@ app.command("create").argument("<project-name>", "Name of the project to create"
3885
4355
  templateName = await selectTemplate();
3886
4356
  }
3887
4357
  const template = TEMPLATES2[templateName];
3888
- await create(projectName, template.repo, templateName);
4358
+ await create(projectName, template.repo, templateName, {
4359
+ enabled: options.auto,
4360
+ env: options.env,
4361
+ cmsUrl: options.cmsUrl,
4362
+ createUrl: options.createUrl
4363
+ });
3889
4364
  });
3890
4365
  app.command("update-game-sdk").description("Download and update the GameSDK library, types, and documentation").action(async () => {
3891
4366
  await updateGameSdk();
@@ -3893,6 +4368,31 @@ app.command("update-game-sdk").description("Download and update the GameSDK libr
3893
4368
  app.command("deploy").description("Build and deploy the webapp to the server").option("-e, --env <environment>", "Target environment (local | development | staging | production)", "development").option("--host <host>", "Override the deploy host").option("--port <port>", "Override the deploy port", parseInt).action(async (options) => {
3894
4369
  await deploy(options.env, { host: options.host, port: options.port });
3895
4370
  });
4371
+ var auth = app.command("auth").description("Authenticate with the CMS");
4372
+ auth.command("login").description("Log in to the CMS using a browser callback").option("-e, --env <environment>", "CMS environment (local | development | staging | production)", "development").option("--cms-url <url>", "CMS base URL. Defaults to the selected environment").option("--login-url <url>", "Full browser login URL. Defaults to <cms-url>/auth/cli-login").option("--token-url <url>", "Full token exchange URL. Defaults to <cms-url>/api/auth/cli-token").option("--callback-port <port>", "Local callback port. Defaults to a random free port", parseInt).option("--timeout <seconds>", "Seconds to wait for the browser callback", parseInt, 300).option("--no-open", "Print the login URL without opening a browser").action(async (options) => {
4373
+ const session = await login({
4374
+ env: options.env,
4375
+ cmsUrl: options.cmsUrl,
4376
+ loginUrl: options.loginUrl,
4377
+ tokenUrl: options.tokenUrl,
4378
+ callbackPort: options.callbackPort,
4379
+ open: options.open,
4380
+ timeoutMs: (options.timeout ?? 300) * 1000
4381
+ });
4382
+ console.log(`✅ Logged in to CMS [${session.env}]`);
4383
+ console.log(`\uD83D\uDCCD CMS URL: ${session.cmsUrl}`);
4384
+ });
4385
+ auth.command("me").description("Show the locally stored CMS login session").option("-e, --env <environment>", "CMS environment (local | development | staging | production)", "development").option("--remote", "Call the CMS /me endpoint instead of only reading the local session").option("--cms-url <url>", "CMS base URL for --remote. Defaults to the selected environment or stored session").option("--me-url <url>", "Full me endpoint URL for --remote. Defaults to <cms-url>/api/auth/me").action(async (options) => {
4386
+ const currentUser = await me(options);
4387
+ console.log(JSON.stringify(currentUser, null, 2));
4388
+ });
4389
+ var config = app.command("config").description("Manage webapp metadata config");
4390
+ addConfigTargetOptions(config.command("get").description("Pull the current webapp metadata config from the CMS").option("--out <file>", "Write the config JSON to a file")).action(async (options) => {
4391
+ await getWebappConfig(options);
4392
+ });
4393
+ addConfigSetOptions(config.command("set").description("Update webapp metadata such as title, cover image, description, and localization")).action(async (options) => {
4394
+ await updateWebappConfig(options);
4395
+ });
3896
4396
  app.command("upgrade-template").description("Upgrade template files and CLI scripts from the template repository").action(async () => {
3897
4397
  await upgradeTemplate();
3898
4398
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rodyssey/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Scaffold new projects from airconcepts templates",
5
5
  "bin": {
6
6
  "@rodyssey/cli": "dist/cli.js"