@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.
- package/README.md +30 -0
- package/dist/cli.js +936 -311
- 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.
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
3862
|
-
import { resolve } from "node:path";
|
|
3863
|
-
var
|
|
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 ||
|
|
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(
|
|
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 =
|
|
3884
|
-
if (!
|
|
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
|
-
|
|
3891
|
-
|
|
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) =>
|
|
4437
|
+
const found = candidates.find((candidate) => existsSync6(candidate));
|
|
3894
4438
|
return found;
|
|
3895
4439
|
}
|
|
3896
4440
|
function readManifest(path3) {
|
|
3897
|
-
const parsed = JSON.parse(
|
|
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
|
|
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
|
|
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 =
|
|
4466
|
+
const webappId = resolveWebappId2(options.webappId);
|
|
3923
4467
|
if (!options.dryRun)
|
|
3924
|
-
|
|
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
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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
|
|
4363
|
-
import { resolve as
|
|
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 =
|
|
4385
|
-
const raw =
|
|
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
|
|
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
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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 =
|
|
4581
|
-
|
|
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/
|
|
4676
|
-
import {
|
|
4677
|
-
import { resolve as
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
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
|
|
4703
|
-
if (
|
|
4704
|
-
return;
|
|
4705
|
-
if (
|
|
4706
|
-
|
|
4707
|
-
|
|
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
|
-
|
|
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
|
|
4729
|
-
|
|
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
|
|
4745
|
-
|
|
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
|
|
4753
|
-
|
|
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
|
|
4760
|
-
|
|
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
|
-
|
|
4763
|
-
|
|
4764
|
-
const
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
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
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
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
|
|
5223
|
+
method,
|
|
4774
5224
|
headers: {
|
|
4775
|
-
|
|
4776
|
-
|
|
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
|
-
|
|
4781
|
-
|
|
4782
|
-
${errorText}`);
|
|
5233
|
+
throw new Error(`${method} ${url} failed: ${response.status} ${response.statusText}
|
|
5234
|
+
${pretty(payload)}`);
|
|
4783
5235
|
}
|
|
4784
|
-
return
|
|
5236
|
+
return payload;
|
|
4785
5237
|
}
|
|
4786
|
-
|
|
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
|
-
|
|
5240
|
+
const outPath = resolve5(process.cwd(), options.out);
|
|
5241
|
+
writeFileSync7(outPath, `${pretty(payload)}
|
|
4802
5242
|
`, "utf-8");
|
|
4803
|
-
console.log(`✅
|
|
5243
|
+
console.log(`✅ Wrote response to ${outPath}`);
|
|
4804
5244
|
return;
|
|
4805
5245
|
}
|
|
4806
|
-
console.log(
|
|
5246
|
+
console.log(options.json ? pretty(payload) : human);
|
|
4807
5247
|
}
|
|
4808
|
-
async function
|
|
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
|
|
4811
|
-
const
|
|
4812
|
-
|
|
4813
|
-
|
|
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(
|
|
4828
|
-
|
|
5268
|
+
console.log(dim(`
|
|
5269
|
+
↷ Dry run — no request sent.`));
|
|
4829
5270
|
return;
|
|
4830
5271
|
}
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
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
|
|
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
|
|
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 =
|
|
4886
|
-
const raw =
|
|
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(
|
|
5412
|
+
loadEnv(PROD_ENV3, { override: true });
|
|
4962
5413
|
process.env.WEBAPP_ID = webappId;
|
|
4963
5414
|
process.env.DEPLOY_TOKEN = deployToken;
|
|
4964
|
-
await deploy(
|
|
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 =
|
|
4976
|
-
if (
|
|
4977
|
-
const content =
|
|
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 ${
|
|
5461
|
+
\uD83D\uDD10 Logging in to ${PROD_ENV3} CMS (ephemeral, not stored)...`);
|
|
5011
5462
|
const session = await login({
|
|
5012
|
-
env:
|
|
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(
|
|
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 ${
|
|
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 ${
|
|
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
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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(
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
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
|
-
|
|
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) =>
|
|
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 =
|
|
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 (
|
|
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 (!
|
|
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((
|
|
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
|
-
|
|
6142
|
+
resolve7(entries[index].name);
|
|
5542
6143
|
} else {
|
|
5543
6144
|
console.error("Invalid selection, defaulting to 'webapp'");
|
|
5544
|
-
|
|
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
|
});
|