@rodyssey/cli 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +30 -0
  2. package/dist/cli.js +936 -311
  3. 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.4.1",
2074
+ version: "0.6.0",
2075
2075
  description: "Scaffold new projects from airconcepts templates",
2076
2076
  repository: {
2077
2077
  type: "git",
@@ -2474,7 +2474,7 @@ Server view:`);
2474
2474
 
2475
2475
  // src/create.ts
2476
2476
  import { execSync } from "node:child_process";
2477
- import { existsSync as existsSync3, rmSync } from "node:fs";
2477
+ import { existsSync as existsSync5, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
2478
2478
  import path2 from "node:path";
2479
2479
 
2480
2480
  // src/utils.ts
@@ -2548,6 +2548,516 @@ function loadEnv(envName, options = {}) {
2548
2548
  }
2549
2549
  }
2550
2550
 
2551
+ // src/config-file.ts
2552
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
2553
+ import { resolve as resolve2 } from "node:path";
2554
+
2555
+ // src/update-webapp-config.ts
2556
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
2557
+ import { resolve } from "node:path";
2558
+ var CONFIG_URLS = {
2559
+ local: "http://localhost:5176/api/webapps/config",
2560
+ development: "https://development-cms.rodyssey.ai/api/webapps/config",
2561
+ staging: "https://staging-cms.rodyssey.ai/api/webapps/config",
2562
+ production: "https://cms.rodyssey.ai/api/webapps/config"
2563
+ };
2564
+ function parseJsonOption(value, optionName) {
2565
+ const maybePath = resolve(process.cwd(), value);
2566
+ const raw = existsSync3(maybePath) ? readFileSync3(maybePath, "utf-8") : value;
2567
+ try {
2568
+ const parsed = JSON.parse(raw);
2569
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2570
+ throw new Error("value must be a JSON object");
2571
+ }
2572
+ return parsed;
2573
+ } catch (error) {
2574
+ const message = error instanceof Error ? error.message : String(error);
2575
+ throw new Error(`Invalid ${optionName}: ${message}`);
2576
+ }
2577
+ }
2578
+ function coerceMaybeNull(value) {
2579
+ if (value === undefined)
2580
+ return;
2581
+ if (value === "null")
2582
+ return null;
2583
+ return value;
2584
+ }
2585
+ function resolveConfigUrl(options, required = true) {
2586
+ const configUrl = options.url || process.env.WEBAPP_CONFIG_URL || CONFIG_URLS[options.env];
2587
+ if (!configUrl) {
2588
+ if (!required)
2589
+ return;
2590
+ console.error("❌ Error: no webapp config endpoint configured.");
2591
+ 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.`);
2592
+ process.exit(1);
2593
+ }
2594
+ const url = new URL(configUrl);
2595
+ if (!options.host && !options.port) {
2596
+ return url.toString().replace(/\/$/, "");
2597
+ }
2598
+ if (options.host)
2599
+ url.hostname = options.host;
2600
+ if (options.port)
2601
+ url.port = String(options.port);
2602
+ return url.toString().replace(/\/$/, "");
2603
+ }
2604
+ function buildDetailsPayload(options) {
2605
+ const payload = options.details ? parseJsonOption(options.details, "--details") : {};
2606
+ const title = coerceMaybeNull(options.title);
2607
+ const description = coerceMaybeNull(options.description);
2608
+ const coverImg = coerceMaybeNull(options.coverImg);
2609
+ if (title !== undefined)
2610
+ payload.title = title;
2611
+ if (description !== undefined)
2612
+ payload.description = description;
2613
+ if (coverImg !== undefined)
2614
+ payload.coverImg = coverImg;
2615
+ if (options.localization !== undefined) {
2616
+ payload.localization = options.localization === "null" ? null : parseJsonOption(options.localization, "--localization");
2617
+ }
2618
+ return payload;
2619
+ }
2620
+ function resolveWebappId(webappId) {
2621
+ const resolved = webappId || process.env.WEBAPP_ID;
2622
+ if (!resolved) {
2623
+ console.error("❌ Error: WEBAPP_ID is not set. Pass --webapp-id or set it in your .env file.");
2624
+ process.exit(1);
2625
+ }
2626
+ return resolved;
2627
+ }
2628
+ function ensureDeployToken(env) {
2629
+ if (process.env.DEPLOY_TOKEN)
2630
+ return;
2631
+ console.error("❌ Error: DEPLOY_TOKEN is not set in environment variables.");
2632
+ console.info(`\uD83D\uDCA1 Please check your .env or .env.${env} file.`);
2633
+ process.exit(1);
2634
+ }
2635
+ function getConfigUrl(options, required = true) {
2636
+ return resolveConfigUrl(options, required);
2637
+ }
2638
+ async function patchWebappConfig(options) {
2639
+ loadEnv(options.env);
2640
+ const webappId = resolveWebappId(options.webappId);
2641
+ const CONFIG_URL = getConfigUrl(options, false);
2642
+ if (!CONFIG_URL) {
2643
+ throw new Error("No webapp config endpoint configured.");
2644
+ }
2645
+ ensureDeployToken(options.env);
2646
+ const response = await fetch(CONFIG_URL, {
2647
+ method: "PATCH",
2648
+ headers: {
2649
+ Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
2650
+ "Content-Type": "application/json"
2651
+ },
2652
+ body: JSON.stringify({ webappId, details: options.details })
2653
+ });
2654
+ if (!response.ok) {
2655
+ const errorText = await response.text();
2656
+ throw new Error(`Config update failed: ${response.status} ${response.statusText}
2657
+ ${errorText}`);
2658
+ }
2659
+ return await response.json().catch(() => {
2660
+ return;
2661
+ });
2662
+ }
2663
+ async function fetchWebappConfig(options) {
2664
+ loadEnv(options.env);
2665
+ const webappId = resolveWebappId(options.webappId);
2666
+ const CONFIG_URL = getConfigUrl(options, false);
2667
+ if (!CONFIG_URL) {
2668
+ throw new Error("No webapp config endpoint configured.");
2669
+ }
2670
+ ensureDeployToken(options.env);
2671
+ const url = new URL(CONFIG_URL);
2672
+ url.searchParams.set("webappId", webappId);
2673
+ const response = await fetch(url, {
2674
+ method: "GET",
2675
+ headers: {
2676
+ Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
2677
+ Accept: "application/json"
2678
+ }
2679
+ });
2680
+ if (!response.ok) {
2681
+ const errorText = await response.text();
2682
+ throw new Error(`Config fetch failed: ${response.status} ${response.statusText}
2683
+ ${errorText}`);
2684
+ }
2685
+ return await response.json();
2686
+ }
2687
+ async function getWebappConfig(options) {
2688
+ loadEnv(options.env);
2689
+ const webappId = resolveWebappId(options.webappId);
2690
+ const CONFIG_URL = getConfigUrl(options);
2691
+ if (!CONFIG_URL)
2692
+ return;
2693
+ const url = new URL(CONFIG_URL);
2694
+ url.searchParams.set("webappId", webappId);
2695
+ console.log(`⚙️ Pulling webapp config for [${options.env}] environment...`);
2696
+ console.log(`\uD83D\uDCCD Config URL: ${url.toString()}`);
2697
+ console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
2698
+ `);
2699
+ const config = await fetchWebappConfig(options);
2700
+ const output = JSON.stringify(config, null, 2);
2701
+ if (options.out) {
2702
+ writeFileSync3(options.out, `${output}
2703
+ `, "utf-8");
2704
+ console.log(`✅ Webapp config written to ${options.out}`);
2705
+ return;
2706
+ }
2707
+ console.log(output);
2708
+ }
2709
+ async function updateWebappConfig(options) {
2710
+ loadEnv(options.env);
2711
+ const webappId = resolveWebappId(options.webappId);
2712
+ const details = buildDetailsPayload(options);
2713
+ if (Object.keys(details).length === 0) {
2714
+ console.error("❌ Error: no detail fields provided. Use --title, --description, --cover-img, --localization, or --details.");
2715
+ process.exit(1);
2716
+ }
2717
+ const payload = { webappId, details };
2718
+ const CONFIG_URL = getConfigUrl(options, !options.dryRun);
2719
+ if (!options.dryRun)
2720
+ ensureDeployToken(options.env);
2721
+ console.log(`⚙️ Updating webapp config for [${options.env}] environment...`);
2722
+ if (CONFIG_URL) {
2723
+ console.log(`\uD83D\uDCCD Config URL: ${CONFIG_URL}`);
2724
+ }
2725
+ console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
2726
+ `);
2727
+ if (options.dryRun) {
2728
+ console.log("\uD83E\uDDEA Dry run payload:");
2729
+ console.log(JSON.stringify(payload, null, 2));
2730
+ return;
2731
+ }
2732
+ if (!CONFIG_URL)
2733
+ return;
2734
+ const result = await patchWebappConfig({
2735
+ env: options.env,
2736
+ details,
2737
+ webappId,
2738
+ url: options.url,
2739
+ host: options.host,
2740
+ port: options.port
2741
+ });
2742
+ console.log("✅ Webapp config updated");
2743
+ if (result !== undefined) {
2744
+ console.log(`
2745
+ \uD83D\uDCCB Update result:`, result);
2746
+ }
2747
+ }
2748
+
2749
+ // src/cli-ui.ts
2750
+ function isPlainObject(value) {
2751
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
2752
+ }
2753
+ function compact(value) {
2754
+ return JSON.stringify(value);
2755
+ }
2756
+ function pretty(value) {
2757
+ return JSON.stringify(value, null, 2);
2758
+ }
2759
+ function useColor() {
2760
+ return !!process.stdout.isTTY && !process.env.NO_COLOR;
2761
+ }
2762
+ function paint(code, text) {
2763
+ return useColor() ? `\x1B[${code}m${text}\x1B[0m` : text;
2764
+ }
2765
+ var green = (s) => paint("32", s);
2766
+ var red = (s) => paint("31", s);
2767
+ var yellow = (s) => paint("33", s);
2768
+ var dim = (s) => paint("2", s);
2769
+ var strike = (s) => paint("9", s);
2770
+ async function prompt(question) {
2771
+ const readline = await import("node:readline");
2772
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2773
+ return new Promise((resolveAnswer) => {
2774
+ rl.question(question, (answer) => {
2775
+ rl.close();
2776
+ resolveAnswer(answer);
2777
+ });
2778
+ });
2779
+ }
2780
+ function isExplicitYes(answer) {
2781
+ const trimmed = answer.trim().toLowerCase();
2782
+ return trimmed === "y" || trimmed === "yes";
2783
+ }
2784
+ function pathToDot(path2) {
2785
+ return path2.replace(/^\//, "").replaceAll("/", ".");
2786
+ }
2787
+ function deepEqual(a, b) {
2788
+ if (a === b)
2789
+ return true;
2790
+ if (typeof a !== typeof b)
2791
+ return false;
2792
+ if (a === null || b === null)
2793
+ return false;
2794
+ if (Array.isArray(a) || Array.isArray(b)) {
2795
+ if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
2796
+ return false;
2797
+ return a.every((v, i) => deepEqual(v, b[i]));
2798
+ }
2799
+ if (typeof a === "object" && typeof b === "object") {
2800
+ const ak = Object.keys(a);
2801
+ const bk = Object.keys(b);
2802
+ if (ak.length !== bk.length)
2803
+ return false;
2804
+ return ak.every((k) => deepEqual(a[k], b[k]));
2805
+ }
2806
+ return false;
2807
+ }
2808
+ function diffJson(before, after, path2 = "") {
2809
+ if (deepEqual(before, after))
2810
+ return [];
2811
+ if (before === undefined)
2812
+ return [{ path: path2, kind: "add", after }];
2813
+ if (after === undefined)
2814
+ return [{ path: path2, kind: "remove", before }];
2815
+ if (!isPlainObject(before) || !isPlainObject(after)) {
2816
+ return [{ path: path2, kind: "change", before, after }];
2817
+ }
2818
+ const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
2819
+ const out = [];
2820
+ for (const key of keys) {
2821
+ out.push(...diffJson(before[key], after[key], `${path2}/${key}`));
2822
+ }
2823
+ return out;
2824
+ }
2825
+ function deltaLine(d) {
2826
+ const path2 = pathToDot(d.path) || "(root)";
2827
+ if (d.kind === "add")
2828
+ return green(` ${path2}: ${compact(d.after)}`);
2829
+ if (d.kind === "change") {
2830
+ return yellow(` ${path2}: ${strike(compact(d.before))} → ${compact(d.after)}`);
2831
+ }
2832
+ return red(strike(` ${path2}: ${compact(d.before)}`));
2833
+ }
2834
+ function formatObjectDelta(deltas, header) {
2835
+ if (deltas.length === 0)
2836
+ return `${header}
2837
+ ${dim(" (no changes)")}`;
2838
+ const adds = deltas.filter((d) => d.kind === "add");
2839
+ const changes = deltas.filter((d) => d.kind === "change");
2840
+ const removes = deltas.filter((d) => d.kind === "remove");
2841
+ const sections = [];
2842
+ if (adds.length)
2843
+ sections.push([green("New"), ...adds.map(deltaLine)].join(`
2844
+ `));
2845
+ if (changes.length)
2846
+ sections.push([yellow("Update"), ...changes.map(deltaLine)].join(`
2847
+ `));
2848
+ if (removes.length)
2849
+ sections.push([red("Delete"), ...removes.map(deltaLine)].join(`
2850
+ `));
2851
+ return `${header}
2852
+
2853
+ ${sections.join(`
2854
+
2855
+ `)}`;
2856
+ }
2857
+
2858
+ // src/config-file.ts
2859
+ var DEFAULT_CONFIG_FILE = "webapp.config.json";
2860
+ function isClearSignalEmpty(value) {
2861
+ if (value === null)
2862
+ return true;
2863
+ if (Array.isArray(value) && value.length === 0)
2864
+ return true;
2865
+ if (isPlainObject(value) && Object.keys(value).length === 0)
2866
+ return true;
2867
+ return false;
2868
+ }
2869
+ function sanitizeDetails(details) {
2870
+ const out = {};
2871
+ for (const [key, value] of Object.entries(details)) {
2872
+ if (isClearSignalEmpty(value))
2873
+ continue;
2874
+ out[key] = value;
2875
+ }
2876
+ return out;
2877
+ }
2878
+ function unwrapDetails(payload) {
2879
+ if (isPlainObject(payload) && isPlainObject(payload.details)) {
2880
+ return payload.details;
2881
+ }
2882
+ if (isPlainObject(payload) && "details" in payload)
2883
+ return {};
2884
+ if (isPlainObject(payload))
2885
+ return payload;
2886
+ return {};
2887
+ }
2888
+ function projectDetailsPatch(current, patch) {
2889
+ const next = { ...current };
2890
+ for (const [key, value] of Object.entries(patch)) {
2891
+ if (isClearSignalEmpty(value)) {
2892
+ delete next[key];
2893
+ } else {
2894
+ next[key] = value;
2895
+ }
2896
+ }
2897
+ return next;
2898
+ }
2899
+ function computeDetailsDelta(current, fileDetails) {
2900
+ return diffJson(current, projectDetailsPatch(current, fileDetails));
2901
+ }
2902
+ function readConfigFile(filePath) {
2903
+ if (!existsSync4(filePath)) {
2904
+ throw new Error(`Config file not found: ${filePath}. Run \`ro app config pull\` first.`);
2905
+ }
2906
+ let raw;
2907
+ try {
2908
+ raw = readFileSync4(filePath, "utf-8");
2909
+ } catch (error) {
2910
+ const message = error instanceof Error ? error.message : String(error);
2911
+ throw new Error(`Could not read ${filePath}: ${message}`);
2912
+ }
2913
+ let parsed;
2914
+ try {
2915
+ parsed = JSON.parse(raw);
2916
+ } catch (error) {
2917
+ const message = error instanceof Error ? error.message : String(error);
2918
+ throw new Error(`Invalid JSON in ${filePath}: ${message}`);
2919
+ }
2920
+ if (!isPlainObject(parsed)) {
2921
+ throw new Error(`${filePath} must contain a JSON object.`);
2922
+ }
2923
+ return parsed;
2924
+ }
2925
+ async function pullWebappConfig(options) {
2926
+ const raw = await fetchWebappConfig({
2927
+ env: options.env,
2928
+ webappId: options.webappId,
2929
+ url: options.url,
2930
+ host: options.host,
2931
+ port: options.port
2932
+ });
2933
+ const details = unwrapDetails(raw);
2934
+ const cleaned = sanitizeDetails(details);
2935
+ const stripped = Object.keys(details).length - Object.keys(cleaned).length;
2936
+ const fileName = options.out || DEFAULT_CONFIG_FILE;
2937
+ const outPath = resolve2(process.cwd(), fileName);
2938
+ writeFileSync4(outPath, `${JSON.stringify(cleaned, null, 2)}
2939
+ `, "utf-8");
2940
+ console.log(`✅ Wrote ${fileName} (${Object.keys(cleaned).length} fields, ${stripped} empty default${stripped === 1 ? "" : "s"} stripped)`);
2941
+ }
2942
+ async function pushWebappConfig(options) {
2943
+ const fileName = options.file || DEFAULT_CONFIG_FILE;
2944
+ const filePath = resolve2(process.cwd(), fileName);
2945
+ const fileDetails = readConfigFile(filePath);
2946
+ const raw = await fetchWebappConfig({
2947
+ env: options.env,
2948
+ webappId: options.webappId,
2949
+ url: options.url,
2950
+ host: options.host,
2951
+ port: options.port
2952
+ });
2953
+ const current = unwrapDetails(raw);
2954
+ const deltas = computeDetailsDelta(current, fileDetails);
2955
+ console.log(formatObjectDelta(deltas, `Pushing ${fileName} → [${options.env}]`));
2956
+ if (deltas.length === 0) {
2957
+ console.log(`
2958
+ ✓ Already in sync — nothing to push.`);
2959
+ return;
2960
+ }
2961
+ if (options.dryRun) {
2962
+ console.log(`
2963
+ ↷ Dry run — no request sent.`);
2964
+ return;
2965
+ }
2966
+ const tty = !!process.stdin.isTTY && !!process.stdout.isTTY;
2967
+ if (!options.yes) {
2968
+ if (!tty) {
2969
+ throw new Error("Refusing to push in non-interactive mode. Pass --yes to confirm or --dry-run to preview.");
2970
+ }
2971
+ const answer = await prompt(`
2972
+ Proceed with push on [${options.env}]? (y/N): `);
2973
+ if (!isExplicitYes(answer)) {
2974
+ console.log("✋ Aborted.");
2975
+ return;
2976
+ }
2977
+ }
2978
+ await patchWebappConfig({
2979
+ env: options.env,
2980
+ details: fileDetails,
2981
+ webappId: options.webappId,
2982
+ url: options.url,
2983
+ host: options.host,
2984
+ port: options.port
2985
+ });
2986
+ console.log(`
2987
+ ✅ Webapp config pushed.`);
2988
+ }
2989
+ async function checkConfigDriftOnDeploy(options) {
2990
+ const fileName = options.file || DEFAULT_CONFIG_FILE;
2991
+ const filePath = resolve2(process.cwd(), fileName);
2992
+ if (!existsSync4(filePath))
2993
+ return;
2994
+ if (!process.env.WEBAPP_ID) {
2995
+ console.warn(`
2996
+ ⚠️ Skipping config drift check: WEBAPP_ID is not set.`);
2997
+ return;
2998
+ }
2999
+ let fileDetails;
3000
+ try {
3001
+ fileDetails = readConfigFile(filePath);
3002
+ } catch (error) {
3003
+ const message = error instanceof Error ? error.message : String(error);
3004
+ console.warn(`
3005
+ ⚠️ Skipping config drift check: ${message}`);
3006
+ return;
3007
+ }
3008
+ let current;
3009
+ try {
3010
+ const raw = await fetchWebappConfig({
3011
+ env: options.env,
3012
+ host: options.host,
3013
+ port: options.port
3014
+ });
3015
+ current = unwrapDetails(raw);
3016
+ } catch (error) {
3017
+ const message = error instanceof Error ? error.message : String(error);
3018
+ console.warn(`
3019
+ ⚠️ Could not check config drift: ${message}`);
3020
+ return;
3021
+ }
3022
+ const deltas = computeDetailsDelta(current, fileDetails);
3023
+ if (deltas.length === 0)
3024
+ return;
3025
+ console.log(`
3026
+ \uD83D\uDCDD ${fileName} differs from the CMS:`);
3027
+ console.log(formatObjectDelta(deltas, `Config drift on [${options.env}]`));
3028
+ const tty = !!process.stdin.isTTY && !!process.stdout.isTTY;
3029
+ let shouldPush = options.pushConfig === true;
3030
+ if (!shouldPush) {
3031
+ if (!tty) {
3032
+ console.log(`
3033
+ ℹ️ ${fileName} differs from CMS — run 'ro app config push' to sync, or deploy with --push-config.`);
3034
+ return;
3035
+ }
3036
+ const answer = await prompt(`
3037
+ Push ${fileName} to the CMS now? (y/N): `);
3038
+ shouldPush = isExplicitYes(answer);
3039
+ }
3040
+ if (!shouldPush) {
3041
+ console.log(`
3042
+ ↷ Skipped. Run 'ro app config push' later to sync.`);
3043
+ return;
3044
+ }
3045
+ try {
3046
+ await patchWebappConfig({
3047
+ env: options.env,
3048
+ details: fileDetails,
3049
+ host: options.host,
3050
+ port: options.port
3051
+ });
3052
+ console.log("✅ Webapp config pushed.");
3053
+ } catch (error) {
3054
+ const message = error instanceof Error ? error.message : String(error);
3055
+ console.warn(`
3056
+ ⚠️ Config push failed (deploy still succeeded): ${message}`);
3057
+ console.warn(" Retry with: ro app config push");
3058
+ }
3059
+ }
3060
+
2551
3061
  // src/create.ts
2552
3062
  var PLACEHOLDER = "__PROJECT_NAME__";
2553
3063
  var PLACEHOLDER_LOWER = "__project_name__";
@@ -2614,9 +3124,30 @@ ${JSON.stringify(createPayload, null, 2)}`);
2614
3124
  deployToken
2615
3125
  };
2616
3126
  }
3127
+ async function fetchCmsConfig(cmsUrl, webappId, deployToken) {
3128
+ const url = new URL(`${cmsUrl}/api/webapps/config`);
3129
+ url.searchParams.set("webappId", webappId);
3130
+ const response = await fetch(url, {
3131
+ method: "GET",
3132
+ headers: {
3133
+ Authorization: `Bearer ${deployToken}`,
3134
+ Accept: "application/json"
3135
+ }
3136
+ });
3137
+ if (!response.ok) {
3138
+ const errorText = await response.text();
3139
+ throw new Error(`Config fetch failed: ${response.status} ${response.statusText}
3140
+ ${errorText}`);
3141
+ }
3142
+ return response.json();
3143
+ }
3144
+ function writeProjectConfig(projectDir, details) {
3145
+ writeFileSync5(path2.join(projectDir, DEFAULT_CONFIG_FILE), `${JSON.stringify(details, null, 2)}
3146
+ `, "utf-8");
3147
+ }
2617
3148
  async function create(projectName, repoUrl, templateName, autoCreate) {
2618
3149
  const targetDir = path2.resolve(process.cwd(), projectName);
2619
- if (existsSync3(targetDir)) {
3150
+ if (existsSync5(targetDir)) {
2620
3151
  console.error(`
2621
3152
  ✖ Directory "${projectName}" already exists.
2622
3153
  `);
@@ -2641,7 +3172,7 @@ async function create(projectName, repoUrl, templateName, autoCreate) {
2641
3172
  process.exit(1);
2642
3173
  }
2643
3174
  const gitDir = path2.join(targetDir, ".git");
2644
- if (existsSync3(gitDir)) {
3175
+ if (existsSync5(gitDir)) {
2645
3176
  rmSync(gitDir, { recursive: true, force: true });
2646
3177
  }
2647
3178
  const filesToReplace = REPLACEMENT_FILES[templateName] ?? [];
@@ -2657,6 +3188,19 @@ async function create(projectName, repoUrl, templateName, autoCreate) {
2657
3188
  writeProjectEnv(targetDir, provisioned);
2658
3189
  console.log(` \uD83D\uDD10 Wrote WEBAPP_ID and DEPLOY_TOKEN to .env`);
2659
3190
  console.log(` \uD83D\uDCCD Webapp ID: ${provisioned.webappId}`);
3191
+ try {
3192
+ const cmsUrl = resolveCmsUrl(autoCreate.env, autoCreate.cmsUrl);
3193
+ const raw = await fetchCmsConfig(cmsUrl, provisioned.webappId, provisioned.deployToken);
3194
+ writeProjectConfig(targetDir, sanitizeDetails(unwrapDetails(raw)));
3195
+ console.log(` \uD83D\uDCC4 Wrote webapp.config.json (pulled from CMS)`);
3196
+ } catch (err) {
3197
+ const msg = err instanceof Error ? err.message : String(err);
3198
+ console.warn(` ⚠️ Could not pull config from CMS (${msg}) — wrote empty webapp.config.json`);
3199
+ writeProjectConfig(targetDir, {});
3200
+ }
3201
+ } else {
3202
+ writeProjectConfig(targetDir, {});
3203
+ console.log(` \uD83D\uDCC4 Wrote webapp.config.json`);
2660
3204
  }
2661
3205
  console.log(`
2662
3206
  ✅ Project "${projectName}" created successfully!
@@ -2671,7 +3215,7 @@ async function create(projectName, repoUrl, templateName, autoCreate) {
2671
3215
 
2672
3216
  // src/deploy.ts
2673
3217
  import { execSync as execSync2 } from "node:child_process";
2674
- import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync, statSync, unlinkSync } from "node:fs";
3218
+ import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync, statSync, unlinkSync } from "node:fs";
2675
3219
  import { join as join2 } from "node:path";
2676
3220
 
2677
3221
  // node_modules/mime/dist/types/other.js
@@ -3858,18 +4402,18 @@ var Mime_default = Mime;
3858
4402
  var src_default = new Mime_default(standard_default, other_default)._freeze();
3859
4403
 
3860
4404
  // src/sync-widget-manifest.ts
3861
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
3862
- import { resolve } from "node:path";
3863
- var CONFIG_URLS = {
4405
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
4406
+ import { resolve as resolve3 } from "node:path";
4407
+ var CONFIG_URLS2 = {
3864
4408
  local: "http://localhost:5176/api/webapps/config",
3865
4409
  development: "https://development-cms.rodyssey.ai/api/webapps/config",
3866
4410
  staging: "https://staging-cms.rodyssey.ai/api/webapps/config",
3867
4411
  production: "https://cms.rodyssey.ai/api/webapps/config"
3868
4412
  };
3869
4413
  function resolveWidgetConfigUrl(options) {
3870
- const rawUrl = options.url || process.env.WEBAPP_CONFIG_URL || CONFIG_URLS[options.env];
4414
+ const rawUrl = options.url || process.env.WEBAPP_CONFIG_URL || CONFIG_URLS2[options.env];
3871
4415
  if (!rawUrl) {
3872
- throw new Error(`Unknown environment "${options.env}". Use one of: ${Object.keys(CONFIG_URLS).join(", ")}, pass --url, or set WEBAPP_CONFIG_URL.`);
4416
+ throw new Error(`Unknown environment "${options.env}". Use one of: ${Object.keys(CONFIG_URLS2).join(", ")}, pass --url, or set WEBAPP_CONFIG_URL.`);
3873
4417
  }
3874
4418
  const url = new URL(rawUrl);
3875
4419
  if (options.host)
@@ -3880,34 +4424,34 @@ function resolveWidgetConfigUrl(options) {
3880
4424
  }
3881
4425
  function resolveManifestPath(manifest) {
3882
4426
  if (manifest) {
3883
- const manifestPath = resolve(process.cwd(), manifest);
3884
- if (!existsSync4(manifestPath)) {
4427
+ const manifestPath = resolve3(process.cwd(), manifest);
4428
+ if (!existsSync6(manifestPath)) {
3885
4429
  throw new Error(`Widget manifest not found: ${manifestPath}`);
3886
4430
  }
3887
4431
  return manifestPath;
3888
4432
  }
3889
4433
  const candidates = [
3890
- resolve(process.cwd(), "build/client/widgets.manifest.json"),
3891
- resolve(process.cwd(), "dist/widgets.manifest.json")
4434
+ resolve3(process.cwd(), "build/client/widgets.manifest.json"),
4435
+ resolve3(process.cwd(), "dist/widgets.manifest.json")
3892
4436
  ];
3893
- const found = candidates.find((candidate) => existsSync4(candidate));
4437
+ const found = candidates.find((candidate) => existsSync6(candidate));
3894
4438
  return found;
3895
4439
  }
3896
4440
  function readManifest(path3) {
3897
- const parsed = JSON.parse(readFileSync3(path3, "utf-8"));
4441
+ const parsed = JSON.parse(readFileSync5(path3, "utf-8"));
3898
4442
  if (!Array.isArray(parsed)) {
3899
4443
  throw new Error(`Widget manifest must be a JSON array: ${path3}`);
3900
4444
  }
3901
4445
  return parsed;
3902
4446
  }
3903
- function resolveWebappId(webappId) {
4447
+ function resolveWebappId2(webappId) {
3904
4448
  const resolved = webappId || process.env.WEBAPP_ID;
3905
4449
  if (!resolved) {
3906
4450
  throw new Error("WEBAPP_ID is not set. Add it to .env or pass --webapp-id.");
3907
4451
  }
3908
4452
  return resolved;
3909
4453
  }
3910
- function ensureDeployToken(env) {
4454
+ function ensureDeployToken2(env) {
3911
4455
  if (process.env.DEPLOY_TOKEN)
3912
4456
  return;
3913
4457
  throw new Error(`DEPLOY_TOKEN is not set. Please check your .env or .env.${env} file.`);
@@ -3919,9 +4463,9 @@ async function syncWidgetManifest(options) {
3919
4463
  console.log("No widget manifest found; skipping widget manifest sync.");
3920
4464
  return;
3921
4465
  }
3922
- const webappId = resolveWebappId(options.webappId);
4466
+ const webappId = resolveWebappId2(options.webappId);
3923
4467
  if (!options.dryRun)
3924
- ensureDeployToken(options.env);
4468
+ ensureDeployToken2(options.env);
3925
4469
  const manifest = readManifest(manifestPath);
3926
4470
  const payload = { webappId, details: { widgetManifest: manifest } };
3927
4471
  const configUrl = resolveWidgetConfigUrl(options);
@@ -3980,7 +4524,7 @@ function pickNumber(value) {
3980
4524
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
3981
4525
  }
3982
4526
  function isFullstackProject() {
3983
- return existsSync5("app") && existsSync5("workers/app.ts") && existsSync5("wrangler.jsonc");
4527
+ return existsSync7("app") && existsSync7("workers/app.ts") && existsSync7("wrangler.jsonc");
3984
4528
  }
3985
4529
  function resolveDeployUrl(env, overrides) {
3986
4530
  let deployUrl = DEPLOY_URLS[env];
@@ -4056,7 +4600,7 @@ function collectScripts(scriptFiles) {
4056
4600
  const payload = { api: {}, cron: {}, cronConfig: null };
4057
4601
  const summary = { apiEndpoints: [], cronJobs: [], mcpEndpoints: [] };
4058
4602
  for (const filePath of scriptFiles) {
4059
- const content = readFileSync4(filePath, "utf-8");
4603
+ const content = readFileSync6(filePath, "utf-8");
4060
4604
  const relativePath = normalizeBuildRelativePath(filePath);
4061
4605
  if (relativePath === "cron-jobs/cron.config.json") {
4062
4606
  payload.cronConfig = readCronConfig(filePath, content);
@@ -4172,7 +4716,7 @@ ${JSON.stringify(result, null, 2)}`);
4172
4716
  return { summary, result };
4173
4717
  }
4174
4718
  function getAllFiles(dirPath, arrayOfFiles = []) {
4175
- if (!existsSync5(dirPath))
4719
+ if (!existsSync7(dirPath))
4176
4720
  return arrayOfFiles;
4177
4721
  const files = readdirSync(dirPath);
4178
4722
  files.forEach(function(f) {
@@ -4186,7 +4730,7 @@ function getAllFiles(dirPath, arrayOfFiles = []) {
4186
4730
  return arrayOfFiles;
4187
4731
  }
4188
4732
  function fileToBlob(filePath) {
4189
- const buffer = readFileSync4(filePath);
4733
+ const buffer = readFileSync6(filePath);
4190
4734
  return new Blob([buffer], { type: src_default.getType(filePath) || "application/octet-stream" });
4191
4735
  }
4192
4736
  async function deployFullstack(env, overrides) {
@@ -4310,7 +4854,7 @@ ${errorText}`);
4310
4854
  console.log(`✅ Created ${ZIP_FILE}
4311
4855
  `);
4312
4856
  console.log("☁️ Step 4: Deploying HTML zip to server...");
4313
- const zipBuffer = readFileSync4(ZIP_FILE);
4857
+ const zipBuffer = readFileSync6(ZIP_FILE);
4314
4858
  try {
4315
4859
  const response = await fetch(DEPLOY_URL, {
4316
4860
  method: "POST",
@@ -4340,7 +4884,7 @@ ${errorText}`);
4340
4884
  console.error("❌ Deploy failed:", error);
4341
4885
  throw error;
4342
4886
  } finally {
4343
- if (existsSync5(ZIP_FILE)) {
4887
+ if (existsSync7(ZIP_FILE)) {
4344
4888
  unlinkSync(ZIP_FILE);
4345
4889
  console.log(`
4346
4890
  \uD83E\uDDF9 Cleaned up ${ZIP_FILE}`);
@@ -4353,18 +4897,23 @@ async function deploy(env = "development", overrides = {}) {
4353
4897
  loadEnv(env);
4354
4898
  if (isFullstackProject()) {
4355
4899
  await deployFullstack(env, overrides);
4356
- return;
4900
+ } else {
4901
+ await deploySpa(env, overrides);
4902
+ }
4903
+ if (!overrides.skipConfigCheck) {
4904
+ await checkConfigDriftOnDeploy({
4905
+ env,
4906
+ pushConfig: overrides.pushConfig,
4907
+ host: overrides.host,
4908
+ port: overrides.port
4909
+ });
4357
4910
  }
4358
- await deploySpa(env, overrides);
4359
4911
  }
4360
4912
 
4361
4913
  // src/global-config.ts
4362
- import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
4363
- import { resolve as resolve2 } from "node:path";
4914
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync6 } from "node:fs";
4915
+ import { resolve as resolve4 } from "node:path";
4364
4916
  var PROD_ENV = "production";
4365
- function isPlainObject(value) {
4366
- return !!value && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
4367
- }
4368
4917
  function applyMergePatch(target, patch) {
4369
4918
  if (!isPlainObject(patch))
4370
4919
  return patch;
@@ -4381,8 +4930,8 @@ function applyMergePatch(target, patch) {
4381
4930
  function parseFlag(name, value) {
4382
4931
  if (value === "null")
4383
4932
  return null;
4384
- const candidatePath = resolve2(process.cwd(), value);
4385
- const raw = existsSync6(candidatePath) ? readFileSync5(candidatePath, "utf-8") : value;
4933
+ const candidatePath = resolve4(process.cwd(), value);
4934
+ const raw = existsSync8(candidatePath) ? readFileSync7(candidatePath, "utf-8") : value;
4386
4935
  try {
4387
4936
  return JSON.parse(raw);
4388
4937
  } catch (error) {
@@ -4398,95 +4947,20 @@ function buildPayload(options) {
4398
4947
  if (options.publicConfig !== undefined) {
4399
4948
  payload.publicConfig = parseFlag("public-config", options.publicConfig);
4400
4949
  }
4401
- if (payload.config === undefined && payload.publicConfig === undefined) {
4402
- throw new Error("Provide at least one of --config or --public-config.");
4403
- }
4404
- return payload;
4405
- }
4406
- function resolveEndpoint(env, override, cmsUrl) {
4407
- if (override)
4408
- return override;
4409
- return `${resolveCmsUrl(env, cmsUrl)}/api/cli/cms/global-config`;
4410
- }
4411
- async function prompt(question) {
4412
- const readline = await import("node:readline");
4413
- const rl = readline.createInterface({
4414
- input: process.stdin,
4415
- output: process.stdout
4416
- });
4417
- return new Promise((resolveAnswer) => {
4418
- rl.question(question, (answer) => {
4419
- rl.close();
4420
- resolveAnswer(answer);
4421
- });
4422
- });
4423
- }
4424
- function isExplicitYes(answer) {
4425
- const trimmed = answer.trim().toLowerCase();
4426
- return trimmed === "y" || trimmed === "yes";
4427
- }
4428
- function pretty(value) {
4429
- return JSON.stringify(value, null, 2);
4430
- }
4431
- function compact(value) {
4432
- return JSON.stringify(value);
4433
- }
4434
- function useColor() {
4435
- return !!process.stdout.isTTY && !process.env.NO_COLOR;
4436
- }
4437
- function paint(code, text) {
4438
- return useColor() ? `\x1B[${code}m${text}\x1B[0m` : text;
4439
- }
4440
- var green = (s) => paint("32", s);
4441
- var red = (s) => paint("31", s);
4442
- var yellow = (s) => paint("33", s);
4443
- var dim = (s) => paint("2", s);
4444
- var strike = (s) => paint("9", s);
4445
- var COLUMN_LABEL = {
4446
- config: "Config",
4447
- publicConfig: "Public Config"
4448
- };
4449
- function pathToDot(path3) {
4450
- return path3.replace(/^\//, "").replaceAll("/", ".");
4451
- }
4452
- function deepEqual(a, b) {
4453
- if (a === b)
4454
- return true;
4455
- if (typeof a !== typeof b)
4456
- return false;
4457
- if (a === null || b === null)
4458
- return false;
4459
- if (Array.isArray(a) || Array.isArray(b)) {
4460
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
4461
- return false;
4462
- return a.every((v, i) => deepEqual(v, b[i]));
4463
- }
4464
- if (typeof a === "object" && typeof b === "object") {
4465
- const ak = Object.keys(a);
4466
- const bk = Object.keys(b);
4467
- if (ak.length !== bk.length)
4468
- return false;
4469
- return ak.every((k) => deepEqual(a[k], b[k]));
4470
- }
4471
- return false;
4472
- }
4473
- function diffJson(before, after, path3 = "") {
4474
- if (deepEqual(before, after))
4475
- return [];
4476
- if (before === undefined)
4477
- return [{ path: path3, kind: "add", after }];
4478
- if (after === undefined)
4479
- return [{ path: path3, kind: "remove", before }];
4480
- if (!isPlainObject(before) || !isPlainObject(after)) {
4481
- return [{ path: path3, kind: "change", before, after }];
4482
- }
4483
- const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
4484
- const out = [];
4485
- for (const key of keys) {
4486
- out.push(...diffJson(before[key], after[key], `${path3}/${key}`));
4950
+ if (payload.config === undefined && payload.publicConfig === undefined) {
4951
+ throw new Error("Provide at least one of --config or --public-config.");
4487
4952
  }
4488
- return out;
4953
+ return payload;
4954
+ }
4955
+ function resolveEndpoint(env, override, cmsUrl) {
4956
+ if (override)
4957
+ return override;
4958
+ return `${resolveCmsUrl(env, cmsUrl)}/api/cli/cms/global-config`;
4489
4959
  }
4960
+ var COLUMN_LABEL = {
4961
+ config: "Config",
4962
+ publicConfig: "Public Config"
4963
+ };
4490
4964
  function buildColumnDeltas(current, payload, method) {
4491
4965
  const out = [];
4492
4966
  for (const column of ["config", "publicConfig"]) {
@@ -4506,7 +4980,7 @@ function buildColumnDeltas(current, payload, method) {
4506
4980
  }
4507
4981
  return out;
4508
4982
  }
4509
- function deltaLine(d, showColumnTag) {
4983
+ function deltaLine2(d, showColumnTag) {
4510
4984
  const tag = showColumnTag ? `[${COLUMN_LABEL[d.column]}] ` : "";
4511
4985
  const path3 = pathToDot(d.path) || `(entire ${COLUMN_LABEL[d.column]})`;
4512
4986
  if (d.kind === "add") {
@@ -4530,15 +5004,15 @@ ${dim(" (no changes)")}`;
4530
5004
  const removes = deltas.filter((d) => d.kind === "remove");
4531
5005
  const sections = [];
4532
5006
  if (adds.length > 0) {
4533
- sections.push([green("New"), ...adds.map((d) => deltaLine(d, showTag))].join(`
5007
+ sections.push([green("New"), ...adds.map((d) => deltaLine2(d, showTag))].join(`
4534
5008
  `));
4535
5009
  }
4536
5010
  if (changes.length > 0) {
4537
- sections.push([yellow("Update"), ...changes.map((d) => deltaLine(d, showTag))].join(`
5011
+ sections.push([yellow("Update"), ...changes.map((d) => deltaLine2(d, showTag))].join(`
4538
5012
  `));
4539
5013
  }
4540
5014
  if (removes.length > 0) {
4541
- sections.push([red("Delete"), ...removes.map((d) => deltaLine(d, showTag))].join(`
5015
+ sections.push([red("Delete"), ...removes.map((d) => deltaLine2(d, showTag))].join(`
4542
5016
  `));
4543
5017
  }
4544
5018
  return `${header}
@@ -4577,8 +5051,8 @@ ${pretty(payload)}`);
4577
5051
  }
4578
5052
  const text = pretty(payload);
4579
5053
  if (options.out) {
4580
- const outPath = resolve2(process.cwd(), options.out);
4581
- writeFileSync3(outPath, `${text}
5054
+ const outPath = resolve4(process.cwd(), options.out);
5055
+ writeFileSync6(outPath, `${text}
4582
5056
  `, "utf-8");
4583
5057
  console.log(`✅ Wrote global config to ${outPath}`);
4584
5058
  } else {
@@ -4672,190 +5146,167 @@ async function patchGlobalConfig(options) {
4672
5146
  await writeGlobalConfig("PATCH", options);
4673
5147
  }
4674
5148
 
4675
- // src/promote.ts
4676
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
4677
- import { resolve as resolve4 } from "node:path";
4678
-
4679
- // src/update-webapp-config.ts
4680
- import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
4681
- import { resolve as resolve3 } from "node:path";
4682
- var CONFIG_URLS2 = {
4683
- local: "http://localhost:5176/api/webapps/config",
4684
- development: "https://development-cms.rodyssey.ai/api/webapps/config",
4685
- staging: "https://staging-cms.rodyssey.ai/api/webapps/config",
4686
- production: "https://cms.rodyssey.ai/api/webapps/config"
4687
- };
4688
- function parseJsonOption(value, optionName) {
4689
- const maybePath = resolve3(process.cwd(), value);
4690
- const raw = existsSync7(maybePath) ? readFileSync6(maybePath, "utf-8") : value;
4691
- try {
4692
- const parsed = JSON.parse(raw);
4693
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4694
- throw new Error("value must be a JSON object");
4695
- }
4696
- return parsed;
4697
- } catch (error) {
4698
- const message = error instanceof Error ? error.message : String(error);
4699
- throw new Error(`Invalid ${optionName}: ${message}`);
5149
+ // src/group.ts
5150
+ import { writeFileSync as writeFileSync7 } from "node:fs";
5151
+ import { resolve as resolve5 } from "node:path";
5152
+ var PROD_ENV2 = "production";
5153
+ var ENTITY_TYPES = ["webapp", "character", "scene", "story"];
5154
+ function parseEntityType(raw) {
5155
+ if (raw && ENTITY_TYPES.includes(raw)) {
5156
+ return raw;
4700
5157
  }
5158
+ throw new Error(`--type must be one of: ${ENTITY_TYPES.join(", ")}`);
4701
5159
  }
4702
- function coerceMaybeNull(value) {
4703
- if (value === undefined)
4704
- return;
4705
- if (value === "null")
4706
- return null;
4707
- return value;
4708
- }
4709
- function resolveConfigUrl(options, required = true) {
4710
- const configUrl = options.url || process.env.WEBAPP_CONFIG_URL || CONFIG_URLS2[options.env];
4711
- if (!configUrl) {
4712
- if (!required)
4713
- return;
4714
- console.error("❌ Error: no webapp config endpoint configured.");
4715
- console.error(`\uD83D\uDCA1 Use one of these environments: ${Object.keys(CONFIG_URLS2).join(", ")}, pass --url, or set WEBAPP_CONFIG_URL in your .env file.`);
4716
- process.exit(1);
5160
+ function resolveEntityId(type, explicitId) {
5161
+ if (explicitId)
5162
+ return explicitId;
5163
+ if (type === "webapp") {
5164
+ if (process.env.WEBAPP_ID)
5165
+ return process.env.WEBAPP_ID;
5166
+ return missingId("(for --type webapp it defaults to WEBAPP_ID from .env, which is also missing)");
4717
5167
  }
4718
- const url = new URL(configUrl);
4719
- if (!options.host && !options.port) {
4720
- return url.toString().replace(/\/$/, "");
4721
- }
4722
- if (options.host)
4723
- url.hostname = options.host;
4724
- if (options.port)
4725
- url.port = String(options.port);
4726
- return url.toString().replace(/\/$/, "");
5168
+ return missingId(`(no default exists for --type ${type})`);
4727
5169
  }
4728
- function buildDetailsPayload(options) {
4729
- const payload = options.details ? parseJsonOption(options.details, "--details") : {};
4730
- const title = coerceMaybeNull(options.title);
4731
- const description = coerceMaybeNull(options.description);
4732
- const coverImg = coerceMaybeNull(options.coverImg);
4733
- if (title !== undefined)
4734
- payload.title = title;
4735
- if (description !== undefined)
4736
- payload.description = description;
4737
- if (coverImg !== undefined)
4738
- payload.coverImg = coverImg;
4739
- if (options.localization !== undefined) {
4740
- payload.localization = options.localization === "null" ? null : parseJsonOption(options.localization, "--localization");
4741
- }
4742
- return payload;
5170
+ function missingId(detail) {
5171
+ throw new Error(`--id is required ${detail}`);
4743
5172
  }
4744
- function resolveWebappId2(webappId) {
4745
- const resolved = webappId || process.env.WEBAPP_ID;
4746
- if (!resolved) {
4747
- console.error("❌ Error: WEBAPP_ID is not set. Pass --webapp-id or set it in your .env file.");
4748
- process.exit(1);
4749
- }
4750
- return resolved;
5173
+ function orFallback(value, fallback) {
5174
+ return value && value.trim().length > 0 ? value : fallback;
4751
5175
  }
4752
- function ensureDeployToken2(env) {
4753
- if (process.env.DEPLOY_TOKEN)
4754
- return;
4755
- console.error("❌ Error: DEPLOY_TOKEN is not set in environment variables.");
4756
- console.info(`\uD83D\uDCA1 Please check your .env or .env.${env} file.`);
4757
- process.exit(1);
5176
+ function capitalize(s) {
5177
+ return s.charAt(0).toUpperCase() + s.slice(1);
4758
5178
  }
4759
- function getConfigUrl(options, required = true) {
4760
- return resolveConfigUrl(options, required);
5179
+ function formatGroupList(type, env, groups) {
5180
+ const lines = [
5181
+ `\uD83D\uDCE6 ${capitalize(type)} groups on [${env}] — ${groups.length} found`
5182
+ ];
5183
+ for (const g of groups) {
5184
+ const name = orFallback(g.name, "(unnamed)");
5185
+ const where = g.isSystem ? "system" : `school: ${g.schoolName ?? g.schoolId ?? "unknown"}`;
5186
+ const desc = orFallback(g.description, "");
5187
+ lines.push("");
5188
+ lines.push(`• ${name} (${g.itemCount} items · ${where})`);
5189
+ lines.push(` id: ${g.id}`);
5190
+ lines.push(` ${desc || dim("(no description)")}`);
5191
+ }
5192
+ return lines.join(`
5193
+ `);
4761
5194
  }
4762
- async function fetchWebappConfig(options) {
4763
- loadEnv(options.env);
4764
- const webappId = resolveWebappId2(options.webappId);
4765
- const CONFIG_URL = getConfigUrl(options);
4766
- if (!CONFIG_URL) {
4767
- throw new Error("No webapp config endpoint configured.");
5195
+ function formatItemList(type, env, group, items) {
5196
+ const label = orFallback(group.name, group.id);
5197
+ const lines = [
5198
+ `\uD83D\uDCE6 Items in ${type} group "${label}" on [${env}] — ${items.length} found`
5199
+ ];
5200
+ for (const item of items) {
5201
+ lines.push("");
5202
+ lines.push(`• ${orFallback(item.label, "(unlabeled)")} (order: ${item.orderIndex ?? "-"})`);
5203
+ lines.push(` id: ${item.entityId}`);
4768
5204
  }
4769
- ensureDeployToken2(options.env);
4770
- const url = new URL(CONFIG_URL);
4771
- url.searchParams.set("webappId", webappId);
5205
+ return lines.join(`
5206
+ `);
5207
+ }
5208
+ function groupsBaseUrl(env, cmsUrl) {
5209
+ return `${resolveCmsUrl(env, cmsUrl)}/api/cli/groups`;
5210
+ }
5211
+ async function resolveWriteAuth(env, cmsUrl) {
5212
+ if (env === PROD_ENV2) {
5213
+ console.log(`
5214
+ \uD83D\uDD10 Logging in to ${PROD_ENV2} CMS (ephemeral, not stored)...`);
5215
+ const session = await login({ env, cmsUrl, persist: false });
5216
+ console.log(`✅ Logged in to ${session.cmsUrl}`);
5217
+ return { baseUrl: `${session.cmsUrl}/api/cli/groups`, token: session.token };
5218
+ }
5219
+ return { baseUrl: groupsBaseUrl(env, cmsUrl), token: resolveSessionToken(env) };
5220
+ }
5221
+ async function requestJson(method, url, token, body) {
4772
5222
  const response = await fetch(url, {
4773
- method: "GET",
5223
+ method,
4774
5224
  headers: {
4775
- Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
4776
- Accept: "application/json"
4777
- }
5225
+ Accept: "application/json",
5226
+ Authorization: `Bearer ${token}`,
5227
+ ...body !== undefined ? { "Content-Type": "application/json" } : {}
5228
+ },
5229
+ ...body !== undefined ? { body: JSON.stringify(body) } : {}
4778
5230
  });
5231
+ const payload = await readResponsePayload(response);
4779
5232
  if (!response.ok) {
4780
- const errorText = await response.text();
4781
- throw new Error(`Config fetch failed: ${response.status} ${response.statusText}
4782
- ${errorText}`);
5233
+ throw new Error(`${method} ${url} failed: ${response.status} ${response.statusText}
5234
+ ${pretty(payload)}`);
4783
5235
  }
4784
- return await response.json();
5236
+ return payload;
4785
5237
  }
4786
- async function getWebappConfig(options) {
4787
- loadEnv(options.env);
4788
- const webappId = resolveWebappId2(options.webappId);
4789
- const CONFIG_URL = getConfigUrl(options);
4790
- if (!CONFIG_URL)
4791
- return;
4792
- const url = new URL(CONFIG_URL);
4793
- url.searchParams.set("webappId", webappId);
4794
- console.log(`⚙️ Pulling webapp config for [${options.env}] environment...`);
4795
- console.log(`\uD83D\uDCCD Config URL: ${url.toString()}`);
4796
- console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
4797
- `);
4798
- const config = await fetchWebappConfig(options);
4799
- const output = JSON.stringify(config, null, 2);
5238
+ function emitRead(payload, human, options) {
4800
5239
  if (options.out) {
4801
- writeFileSync4(options.out, `${output}
5240
+ const outPath = resolve5(process.cwd(), options.out);
5241
+ writeFileSync7(outPath, `${pretty(payload)}
4802
5242
  `, "utf-8");
4803
- console.log(`✅ Webapp config written to ${options.out}`);
5243
+ console.log(`✅ Wrote response to ${outPath}`);
4804
5244
  return;
4805
5245
  }
4806
- console.log(output);
5246
+ console.log(options.json ? pretty(payload) : human);
4807
5247
  }
4808
- async function updateWebappConfig(options) {
5248
+ async function listGroups(options) {
5249
+ const type = parseEntityType(options.type);
5250
+ const url = `${groupsBaseUrl(options.env, options.cmsUrl)}?type=${type}`;
5251
+ const payload = await requestJson("GET", url, resolveSessionToken(options.env));
5252
+ emitRead(payload, formatGroupList(type, options.env, payload.groups ?? []), options);
5253
+ }
5254
+ async function listGroupItems(groupId, options) {
5255
+ const type = parseEntityType(options.type);
5256
+ const url = `${groupsBaseUrl(options.env, options.cmsUrl)}/${encodeURIComponent(groupId)}/items?type=${type}`;
5257
+ const payload = await requestJson("GET", url, resolveSessionToken(options.env));
5258
+ emitRead(payload, formatItemList(type, options.env, payload.group ?? { id: groupId, name: null }, payload.items ?? []), options);
5259
+ }
5260
+ async function writeMembership(mode, groupId, options) {
5261
+ const type = parseEntityType(options.type);
4809
5262
  loadEnv(options.env);
4810
- const webappId = resolveWebappId2(options.webappId);
4811
- const details = buildDetailsPayload(options);
4812
- if (Object.keys(details).length === 0) {
4813
- console.error(" Error: no detail fields provided. Use --title, --description, --cover-img, --localization, or --details.");
4814
- process.exit(1);
4815
- }
4816
- const payload = { webappId, details };
4817
- const CONFIG_URL = getConfigUrl(options, !options.dryRun);
4818
- if (!options.dryRun)
4819
- ensureDeployToken2(options.env);
4820
- console.log(`⚙️ Updating webapp config for [${options.env}] environment...`);
4821
- if (CONFIG_URL) {
4822
- console.log(`\uD83D\uDCCD Config URL: ${CONFIG_URL}`);
4823
- }
4824
- console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
4825
- `);
5263
+ const entityId = resolveEntityId(type, options.id);
5264
+ const verb = mode === "assign" ? "Assign" : "Remove";
5265
+ const arrow = mode === "assign" ? "→" : "←";
5266
+ console.log(`${mode === "assign" ? "➕" : "➖"} ${verb} ${type} ${entityId} ${arrow} group ${groupId} on [${options.env}]`);
4826
5267
  if (options.dryRun) {
4827
- console.log("\uD83E\uDDEA Dry run payload:");
4828
- console.log(JSON.stringify(payload, null, 2));
5268
+ console.log(dim(`
5269
+ Dry run — no request sent.`));
4829
5270
  return;
4830
5271
  }
4831
- if (!CONFIG_URL)
4832
- return;
4833
- const response = await fetch(CONFIG_URL, {
4834
- method: "PATCH",
4835
- headers: {
4836
- Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
4837
- "Content-Type": "application/json"
4838
- },
4839
- body: JSON.stringify(payload)
4840
- });
4841
- if (!response.ok) {
4842
- const errorText = await response.text();
4843
- throw new Error(`Config update failed: ${response.status} ${response.statusText}
4844
- ${errorText}`);
5272
+ const tty = !!process.stdin.isTTY && !!process.stdout.isTTY;
5273
+ if (!options.yes) {
5274
+ if (!tty) {
5275
+ throw new Error("Refusing to send write in non-interactive mode. Pass --yes to confirm or --dry-run to preview.");
5276
+ }
5277
+ const answer = await prompt(`
5278
+ Proceed with ${mode} on [${options.env}]? (y/N): `);
5279
+ if (!isExplicitYes(answer)) {
5280
+ console.log("✋ Aborted.");
5281
+ return;
5282
+ }
4845
5283
  }
4846
- const result = await response.json().catch(() => {
5284
+ const auth = await resolveWriteAuth(options.env, options.cmsUrl);
5285
+ const url = `${auth.baseUrl}/${encodeURIComponent(groupId)}/items`;
5286
+ const method = mode === "assign" ? "POST" : "DELETE";
5287
+ const payload = await requestJson(method, url, auth.token, { type, entityId });
5288
+ if (options.json) {
5289
+ console.log(pretty(payload));
4847
5290
  return;
4848
- });
4849
- console.log("✅ Webapp config updated");
4850
- if (result !== undefined) {
4851
- console.log(`
4852
- \uD83D\uDCCB Update result:`, result);
4853
5291
  }
5292
+ if (mode === "assign") {
5293
+ console.log(payload.alreadyPresent ? "✅ Already in group — no change." : "✅ Added to group.");
5294
+ } else {
5295
+ console.log(payload.removed ? "✅ Removed from group." : "✅ Was not in group — no change.");
5296
+ }
5297
+ }
5298
+ async function assignToGroup(groupId, options) {
5299
+ await writeMembership("assign", groupId, options);
5300
+ }
5301
+ async function removeFromGroup(groupId, options) {
5302
+ await writeMembership("remove", groupId, options);
4854
5303
  }
4855
5304
 
4856
5305
  // src/promote.ts
5306
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "node:fs";
5307
+ import { resolve as resolve6 } from "node:path";
4857
5308
  var DEFAULT_SOURCE_ENV = "development";
4858
- var PROD_ENV2 = "production";
5309
+ var PROD_ENV3 = "production";
4859
5310
  var PROD_ENV_FILE = ".env.production";
4860
5311
  function isObject3(value) {
4861
5312
  return !!value && typeof value === "object" && !Array.isArray(value);
@@ -4882,8 +5333,8 @@ function unwrapSourceDetails(payload) {
4882
5333
  return payload;
4883
5334
  }
4884
5335
  function parseDetailsOption(value) {
4885
- const maybePath = resolve4(process.cwd(), value);
4886
- const raw = existsSync8(maybePath) ? readFileSync7(maybePath, "utf-8") : value;
5336
+ const maybePath = resolve6(process.cwd(), value);
5337
+ const raw = existsSync9(maybePath) ? readFileSync8(maybePath, "utf-8") : value;
4887
5338
  try {
4888
5339
  const parsed = JSON.parse(raw);
4889
5340
  if (!isObject3(parsed)) {
@@ -4958,10 +5409,10 @@ Deploy to production now? (y/N): `);
4958
5409
  }
4959
5410
  console.log(`
4960
5411
  \uD83D\uDE80 Deploying promoted webapp to production...`);
4961
- loadEnv(PROD_ENV2, { override: true });
5412
+ loadEnv(PROD_ENV3, { override: true });
4962
5413
  process.env.WEBAPP_ID = webappId;
4963
5414
  process.env.DEPLOY_TOKEN = deployToken;
4964
- await deploy(PROD_ENV2);
5415
+ await deploy(PROD_ENV3, { skipConfigCheck: true });
4965
5416
  }
4966
5417
  async function promote(options) {
4967
5418
  assertDeployOptions(options);
@@ -4972,9 +5423,9 @@ async function promote(options) {
4972
5423
  console.info("\uD83D\uDCA1 Run `ro app create --auto` first, or set WEBAPP_ID manually.");
4973
5424
  process.exit(1);
4974
5425
  }
4975
- const prodEnvPath = resolve4(process.cwd(), PROD_ENV_FILE);
4976
- if (existsSync8(prodEnvPath)) {
4977
- const content = readFileSync7(prodEnvPath, "utf-8");
5426
+ const prodEnvPath = resolve6(process.cwd(), PROD_ENV_FILE);
5427
+ if (existsSync9(prodEnvPath)) {
5428
+ const content = readFileSync8(prodEnvPath, "utf-8");
4978
5429
  if (/^WEBAPP_ID=.+/m.test(content)) {
4979
5430
  console.error(`❌ Error: ${PROD_ENV_FILE} already has WEBAPP_ID. The app appears to be promoted already.`);
4980
5431
  process.exit(1);
@@ -5007,18 +5458,18 @@ async function promote(options) {
5007
5458
  }
5008
5459
  }
5009
5460
  console.log(`
5010
- \uD83D\uDD10 Logging in to ${PROD_ENV2} CMS (ephemeral, not stored)...`);
5461
+ \uD83D\uDD10 Logging in to ${PROD_ENV3} CMS (ephemeral, not stored)...`);
5011
5462
  const session = await login({
5012
- env: PROD_ENV2,
5463
+ env: PROD_ENV3,
5013
5464
  cmsUrl: options.cmsUrl,
5014
5465
  persist: false
5015
5466
  });
5016
5467
  console.log(`✅ Logged in to ${session.cmsUrl}`);
5017
- const cmsUrl = resolveCmsUrl(PROD_ENV2, options.cmsUrl);
5468
+ const cmsUrl = resolveCmsUrl(PROD_ENV3, options.cmsUrl);
5018
5469
  const promoteUrl = options.promoteUrl || `${cmsUrl}/api/cli/webapps/promote`;
5019
5470
  const promoteBody = { webappId, details };
5020
5471
  console.log(`
5021
- \uD83D\uDE80 Promoting webapp to ${PROD_ENV2}...`);
5472
+ \uD83D\uDE80 Promoting webapp to ${PROD_ENV3}...`);
5022
5473
  console.log(`\uD83D\uDCCD Promote URL: ${promoteUrl}`);
5023
5474
  console.log(`\uD83D\uDCCD Webapp ID: ${webappId}`);
5024
5475
  console.log("\uD83D\uDCE6 Promote body:");
@@ -5035,7 +5486,7 @@ async function promote(options) {
5035
5486
  });
5036
5487
  const payload = await readResponsePayload(response);
5037
5488
  if (response.status === 409) {
5038
- console.error(`❌ Webapp ${webappId} is already promoted to ${PROD_ENV2}.`);
5489
+ console.error(`❌ Webapp ${webappId} is already promoted to ${PROD_ENV3}.`);
5039
5490
  process.exit(1);
5040
5491
  }
5041
5492
  if (!response.ok) {
@@ -5058,7 +5509,7 @@ ${JSON.stringify(payload, null, 2)}`);
5058
5509
 
5059
5510
  // src/upgrade-template.ts
5060
5511
  import { execSync as execSync3 } from "node:child_process";
5061
- import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2, copyFileSync, rmSync as rmSync2 } from "node:fs";
5512
+ import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync8, mkdirSync as mkdirSync2, copyFileSync, rmSync as rmSync2 } from "node:fs";
5062
5513
  import path3 from "node:path";
5063
5514
  var TEMPLATES = {
5064
5515
  webapp: {
@@ -5132,12 +5583,12 @@ var FORCE_OVERWRITE_SCRIPT_NAMES = [
5132
5583
  "upgrade-template"
5133
5584
  ];
5134
5585
  function detectTemplate() {
5135
- if (existsSync9("app")) {
5586
+ if (existsSync10("app")) {
5136
5587
  console.log(`\uD83D\uDD0D Detected fullstack template (found app/ directory)
5137
5588
  `);
5138
5589
  return TEMPLATES["webapp-fullstack"];
5139
5590
  }
5140
- if (existsSync9("src")) {
5591
+ if (existsSync10("src")) {
5141
5592
  console.log(`\uD83D\uDD0D Detected SPA template (found src/ directory)
5142
5593
  `);
5143
5594
  return TEMPLATES["webapp"];
@@ -5192,11 +5643,11 @@ function applyPackageJsonScriptUpdates(packageJson, scriptUpdates) {
5192
5643
  }
5193
5644
  function updatePackageJsonScripts(template) {
5194
5645
  const pkgPath = "package.json";
5195
- if (!existsSync9(pkgPath)) {
5646
+ if (!existsSync10(pkgPath)) {
5196
5647
  console.log("⚠️ No package.json found, skipping scripts update");
5197
5648
  return;
5198
5649
  }
5199
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
5650
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
5200
5651
  const templateScripts = readTemplatePackageScripts(template);
5201
5652
  const scriptUpdates = getForcedScriptUpdates(templateScripts);
5202
5653
  const result = applyPackageJsonScriptUpdates(pkg, scriptUpdates);
@@ -5204,7 +5655,7 @@ function updatePackageJsonScripts(template) {
5204
5655
  console.log(` \uD83D\uDCDD ${change.action} script: "${change.name}"`);
5205
5656
  }
5206
5657
  if (result.updated) {
5207
- writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + `
5658
+ writeFileSync8(pkgPath, JSON.stringify(pkg, null, 2) + `
5208
5659
  `, "utf-8");
5209
5660
  console.log(`✅ package.json scripts updated
5210
5661
  `);
@@ -5238,11 +5689,11 @@ function viteHandlesServerScripts(viteConfigSource) {
5238
5689
  }
5239
5690
  function ensureBuildScript() {
5240
5691
  const pkgPath = "package.json";
5241
- if (!existsSync9(pkgPath)) {
5692
+ if (!existsSync10(pkgPath)) {
5242
5693
  console.log(" ⚠️ No package.json found, skipping build-script check");
5243
5694
  return;
5244
5695
  }
5245
- const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
5696
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
5246
5697
  const buildScript = pkg.scripts?.build;
5247
5698
  if (typeof buildScript !== "string" || buildScript.trim() === "") {
5248
5699
  console.log(` ⚠️ No "build" script found. Add one that runs the server-scripts pass, e.g.:
@@ -5260,17 +5711,17 @@ function ensureBuildScript() {
5260
5711
  return;
5261
5712
  }
5262
5713
  pkg.scripts.build = result.script;
5263
- writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + `
5714
+ writeFileSync8(pkgPath, JSON.stringify(pkg, null, 2) + `
5264
5715
  `, "utf-8");
5265
5716
  console.log(` \uD83D\uDCDD Added "vite build --mode server-scripts" to the build script`);
5266
5717
  }
5267
5718
  function verifyViteServerScriptsMode() {
5268
- const configPath2 = VITE_CONFIG_CANDIDATES.find((p) => existsSync9(p));
5719
+ const configPath2 = VITE_CONFIG_CANDIDATES.find((p) => existsSync10(p));
5269
5720
  if (!configPath2) {
5270
5721
  console.log(" ⚠️ No vite.config found. Server scripts in src/api and src/cron-jobs won't be compiled.");
5271
5722
  return;
5272
5723
  }
5273
- const source = readFileSync8(configPath2, "utf-8");
5724
+ const source = readFileSync9(configPath2, "utf-8");
5274
5725
  if (viteHandlesServerScripts(source)) {
5275
5726
  console.log(` ✅ ${configPath2} handles the server-scripts build mode`);
5276
5727
  return;
@@ -5295,7 +5746,7 @@ function updateCliSkill() {
5295
5746
  }
5296
5747
  execSync3(`git fetch ${cliRemote}`, { stdio: "inherit" });
5297
5748
  execSync3(`git checkout ${cliRemote}/main -- skills/ro-cli/SKILL.md`, { stdio: "inherit" });
5298
- if (existsSync9("skills/ro-cli/SKILL.md")) {
5749
+ if (existsSync10("skills/ro-cli/SKILL.md")) {
5299
5750
  mkdirSync2(".agent/skills/ro-cli", { recursive: true });
5300
5751
  copyFileSync("skills/ro-cli/SKILL.md", ".agent/skills/ro-cli/SKILL.md");
5301
5752
  rmSync2("skills", { recursive: true, force: true });
@@ -5328,7 +5779,7 @@ async function upgradeTemplate() {
5328
5779
  const checkoutList = template.checkoutFiles.join(" ");
5329
5780
  execSync3(`git checkout ${template.remoteName}/main -- ${checkoutList}`, { stdio: "inherit" });
5330
5781
  for (const file of template.newFiles) {
5331
- if (!existsSync9(file)) {
5782
+ if (!existsSync10(file)) {
5332
5783
  console.log(`\uD83D\uDCC2 Checking out ${file}...`);
5333
5784
  try {
5334
5785
  mkdirSync2(path3.dirname(file), { recursive: true });
@@ -5493,6 +5944,156 @@ async function updateGameSdk() {
5493
5944
  }
5494
5945
  }
5495
5946
 
5947
+ // src/assets.ts
5948
+ import { existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
5949
+ import { basename as basename2, join as join4, relative } from "node:path";
5950
+ var ASSETS_URLS = {
5951
+ local: "http://localhost:5176/api/webapps/assets",
5952
+ development: "https://development-cms.rodyssey.ai/api/webapps/assets",
5953
+ staging: "https://staging-cms.rodyssey.ai/api/webapps/assets",
5954
+ production: "https://cms.rodyssey.ai/api/webapps/assets"
5955
+ };
5956
+ var MAX_FILES_PER_BATCH2 = 5;
5957
+ var MAX_SIZE_PER_BATCH2 = 30 * 1024 * 1024;
5958
+ function resolveAssetsUrl(options) {
5959
+ const rawUrl = options.url || process.env.WEBAPP_ASSETS_URL || ASSETS_URLS[options.env];
5960
+ if (!rawUrl) {
5961
+ throw new Error(`Unknown environment "${options.env}". Use one of: ${Object.keys(ASSETS_URLS).join(", ")}, pass --url, or set WEBAPP_ASSETS_URL.`);
5962
+ }
5963
+ const url = new URL(rawUrl);
5964
+ if (options.host)
5965
+ url.hostname = options.host;
5966
+ if (options.port)
5967
+ url.port = String(options.port);
5968
+ return url.toString().replace(/\/$/, "");
5969
+ }
5970
+ function normalizeRemotePath(raw) {
5971
+ let p = raw.replaceAll("\\", "/");
5972
+ while (p.startsWith("./"))
5973
+ p = p.slice(2);
5974
+ while (p.startsWith("/"))
5975
+ p = p.slice(1);
5976
+ const segments = p.split("/").filter((s) => s.length > 0);
5977
+ if (segments.length === 0 || segments.includes("..") || segments.includes(".")) {
5978
+ throw new Error(`Invalid asset path: "${raw}"`);
5979
+ }
5980
+ return segments.join("/");
5981
+ }
5982
+ function refuseManifest(remotePath) {
5983
+ if (basename2(remotePath) === "widgets.manifest.json") {
5984
+ throw new Error("widgets.manifest.json is webapp metadata, not an asset — the server would " + "overwrite the widget manifest. Use `ro app sync-widget-manifest` instead.");
5985
+ }
5986
+ }
5987
+ function walkDir(dirPath) {
5988
+ const out = [];
5989
+ for (const name of readdirSync2(dirPath)) {
5990
+ const full = join4(dirPath, name);
5991
+ if (statSync2(full).isDirectory())
5992
+ out.push(...walkDir(full));
5993
+ else
5994
+ out.push(full);
5995
+ }
5996
+ return out;
5997
+ }
5998
+ function collectUploads(inputs, dest) {
5999
+ const prefix = dest ? `${normalizeRemotePath(dest)}/` : "";
6000
+ const entries = [];
6001
+ for (const input of inputs) {
6002
+ if (!existsSync11(input)) {
6003
+ throw new Error(`Path not found: ${input}`);
6004
+ }
6005
+ if (statSync2(input).isDirectory()) {
6006
+ for (const file of walkDir(input)) {
6007
+ const remotePath = normalizeRemotePath(`${prefix}${relative(input, file)}`);
6008
+ refuseManifest(remotePath);
6009
+ entries.push({ localPath: file, remotePath });
6010
+ }
6011
+ } else {
6012
+ const remotePath = normalizeRemotePath(`${prefix}${basename2(input)}`);
6013
+ refuseManifest(remotePath);
6014
+ entries.push({ localPath: input, remotePath });
6015
+ }
6016
+ }
6017
+ if (entries.length === 0) {
6018
+ throw new Error("No files to upload.");
6019
+ }
6020
+ return entries;
6021
+ }
6022
+ function splitBatches(entries, sizeOf) {
6023
+ const batches = [];
6024
+ let current = [];
6025
+ let currentSize = 0;
6026
+ for (const entry of entries) {
6027
+ const size = sizeOf(entry);
6028
+ if (current.length >= MAX_FILES_PER_BATCH2 || current.length > 0 && currentSize + size > MAX_SIZE_PER_BATCH2) {
6029
+ batches.push(current);
6030
+ current = [];
6031
+ currentSize = 0;
6032
+ }
6033
+ current.push(entry);
6034
+ currentSize += size;
6035
+ }
6036
+ if (current.length > 0)
6037
+ batches.push(current);
6038
+ return batches;
6039
+ }
6040
+ function ensureDeployToken3(env) {
6041
+ if (process.env.DEPLOY_TOKEN)
6042
+ return process.env.DEPLOY_TOKEN;
6043
+ throw new Error(`DEPLOY_TOKEN is not set. Please check your .env or .env.${env} file.`);
6044
+ }
6045
+ function fileToBlob2(filePath) {
6046
+ return new Blob([readFileSync10(filePath)]);
6047
+ }
6048
+ async function pushAssets(inputs, options) {
6049
+ loadEnv(options.env);
6050
+ const entries = collectUploads(inputs, options.dest);
6051
+ const batches = splitBatches(entries, (e) => statSync2(e.localPath).size);
6052
+ const url = resolveAssetsUrl(options);
6053
+ console.log(`\uD83D\uDCE4 Pushing ${entries.length} asset(s) to [${options.env}] in ${batches.length} batch(es):`);
6054
+ for (const e of entries) {
6055
+ console.log(` ${e.localPath} → ${e.remotePath}`);
6056
+ }
6057
+ if (options.dryRun) {
6058
+ console.log(dim(`
6059
+ ↷ Dry run — no request sent.`));
6060
+ return;
6061
+ }
6062
+ const token = ensureDeployToken3(options.env);
6063
+ const uploaded = [];
6064
+ for (const [index, batch] of batches.entries()) {
6065
+ const formData = new FormData;
6066
+ for (const entry of batch) {
6067
+ formData.append(entry.remotePath, fileToBlob2(entry.localPath), entry.remotePath);
6068
+ }
6069
+ const response = await fetch(url, {
6070
+ method: "POST",
6071
+ headers: { Authorization: `Bearer ${token}` },
6072
+ body: formData
6073
+ });
6074
+ if (!response.ok) {
6075
+ const errorText = await response.text();
6076
+ throw new Error(`Asset upload failed (batch ${index + 1}/${batches.length}): ${response.status} ${response.statusText}
6077
+ ${errorText}`);
6078
+ }
6079
+ const payload = await response.json();
6080
+ uploaded.push(...payload.assets ?? []);
6081
+ console.log(`✅ Batch ${index + 1}/${batches.length} uploaded`);
6082
+ }
6083
+ if (options.json) {
6084
+ console.log(pretty({ success: true, assets: uploaded }));
6085
+ return;
6086
+ }
6087
+ console.log(`
6088
+ ✅ Uploaded ${uploaded.length} asset(s):`);
6089
+ for (const asset of uploaded) {
6090
+ console.log(`• ${asset.path}`);
6091
+ console.log(` ${asset.url}`);
6092
+ }
6093
+ console.log(dim("\nTip: set a cover image with `ro app config set --cover-img <url>`,"));
6094
+ console.log(dim("or put the URL in webapp.config.json and run `ro app config push`."));
6095
+ }
6096
+
5496
6097
  // src/cli.ts
5497
6098
  function renderError(err) {
5498
6099
  const msg = err instanceof Error ? err.message : String(err);
@@ -5533,15 +6134,15 @@ Available templates:
5533
6134
  input: process.stdin,
5534
6135
  output: process.stdout
5535
6136
  });
5536
- return new Promise((resolve5) => {
6137
+ return new Promise((resolve7) => {
5537
6138
  rl.question(`Select a template (1-${entries.length}): `, (answer) => {
5538
6139
  rl.close();
5539
6140
  const index = parseInt(answer, 10) - 1;
5540
6141
  if (index >= 0 && index < entries.length) {
5541
- resolve5(entries[index].name);
6142
+ resolve7(entries[index].name);
5542
6143
  } else {
5543
6144
  console.error("Invalid selection, defaulting to 'webapp'");
5544
- resolve5("webapp");
6145
+ resolve7("webapp");
5545
6146
  }
5546
6147
  });
5547
6148
  });
@@ -5586,6 +6187,20 @@ addGlobalConfigWriteOptions(globalConfig.command("set").description("Replace the
5586
6187
  addGlobalConfigWriteOptions(globalConfig.command("patch").description("Merge-patch (RFC 7396) the `config` and/or `publicConfig` column. `null` values delete keys.")).action(async (options) => {
5587
6188
  await patchGlobalConfig(options);
5588
6189
  });
6190
+ var group = program.command("group").description("Manage entity groups (webapp | character | scene | story)");
6191
+ var addGroupCommonOptions = (command) => command.requiredOption("--type <entity-type>", "Entity type: webapp | character | scene | story").option("-e, --env <environment>", "CMS environment (local | development | staging | production)", "development").option("--cms-url <url>", "CMS base URL. Defaults to the selected environment");
6192
+ addGroupCommonOptions(group.command("list").description("List entity groups (id, name, description, school, item count)")).option("--json", "Print the raw JSON response").option("--out <file>", "Write the response JSON to a file").action(async (options) => {
6193
+ await listGroups(options);
6194
+ });
6195
+ addGroupCommonOptions(group.command("items").description("List the entities in a group").argument("<group-id>", "Group ID (find it with `ro group list`)")).option("--json", "Print the raw JSON response").option("--out <file>", "Write the response JSON to a file").action(async (groupId, options) => {
6196
+ await listGroupItems(groupId, options);
6197
+ });
6198
+ addGroupCommonOptions(group.command("assign").description("Add an entity to a group (idempotent)").argument("<group-id>", "Group ID (find it with `ro group list`)")).option("--id <entity-id>", "Entity ID. For --type webapp defaults to WEBAPP_ID from .env").option("-y, --yes", "Skip the interactive confirmation prompt").option("--dry-run", "Print the intended request without sending it").option("--json", "Print the raw JSON response").action(async (groupId, options) => {
6199
+ await assignToGroup(groupId, options);
6200
+ });
6201
+ addGroupCommonOptions(group.command("remove").description("Remove an entity from a group").argument("<group-id>", "Group ID (find it with `ro group list`)")).option("--id <entity-id>", "Entity ID. For --type webapp defaults to WEBAPP_ID from .env").option("-y, --yes", "Skip the interactive confirmation prompt").option("--dry-run", "Print the intended request without sending it").option("--json", "Print the raw JSON response").action(async (groupId, options) => {
6202
+ await removeFromGroup(groupId, options);
6203
+ });
5589
6204
  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) => {
5590
6205
  let templateName;
5591
6206
  if (options.template) {
@@ -5619,8 +6234,8 @@ app.command("promote").description("Promote the current webapp to production (cr
5619
6234
  app.command("update-game-sdk").description("Download and update the GameSDK library, types, and documentation").action(async () => {
5620
6235
  await updateGameSdk();
5621
6236
  });
5622
- 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) => {
5623
- await deploy(options.env, { host: options.host, port: options.port });
6237
+ 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).option("--push-config", "If webapp.config.json differs from the CMS, push it without prompting").action(async (options) => {
6238
+ await deploy(options.env, { host: options.host, port: options.port, pushConfig: options.pushConfig });
5624
6239
  });
5625
6240
  app.command("sync-widget-manifest").description("Sync the built widget manifest to the CMS webapp config").option("-e, --env <environment>", "Target environment (local | development | staging | production)", "development").option("--manifest <path>", "Path to widgets.manifest.json. Defaults to build/client/widgets.manifest.json or dist/widgets.manifest.json").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").option("--dry-run", "Print the request payload without sending it").action(async (options) => {
5626
6241
  await syncWidgetManifest(options);
@@ -5632,6 +6247,16 @@ addConfigTargetOptions(config.command("get").description("Pull the current webap
5632
6247
  addConfigSetOptions(config.command("set").description("Update webapp metadata such as title, cover image, description, and localization")).action(async (options) => {
5633
6248
  await updateWebappConfig(options);
5634
6249
  });
6250
+ addConfigTargetOptions(config.command("pull").description("Pull the CMS webapp config into a committed webapp.config.json").option("--out <file>", "Output file (default: webapp.config.json)")).action(async (options) => {
6251
+ await pullWebappConfig(options);
6252
+ });
6253
+ addConfigTargetOptions(config.command("push").description("Push webapp.config.json back to the CMS (previews a diff and confirms)").option("--file <path>", "Config file to push (default: webapp.config.json)").option("--dry-run", "Preview the delta without sending").option("-y, --yes", "Skip the confirmation prompt")).action(async (options) => {
6254
+ await pushWebappConfig(options);
6255
+ });
6256
+ var assets = app.command("assets").description("Manage webapp assets (R2-hosted files)");
6257
+ assets.command("push").description("Upload or overwrite webapp assets and print their public URLs").argument("<paths...>", "Files and/or directories to upload").option("--dest <remote-dir>", "Remote directory prefix (e.g. images)").option("-e, --env <environment>", "Target environment (local | development | staging | production)", "development").option("--url <url>", "Override the assets endpoint URL").option("--host <host>", "Override the assets endpoint host").option("--port <port>", "Override the assets endpoint port", parseInt).option("--dry-run", "Preview the local→remote mapping without uploading").option("--json", "Print the raw JSON result").action(async (paths, options) => {
6258
+ await pushAssets(paths, options);
6259
+ });
5635
6260
  app.command("upgrade-template").description("Upgrade template files and CLI scripts from the template repository").action(async () => {
5636
6261
  await upgradeTemplate();
5637
6262
  });