@rodyssey/cli 0.4.0 → 0.5.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 +908 -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.5.0",
|
|
2075
2075
|
description: "Scaffold new projects from airconcepts templates",
|
|
2076
2076
|
repository: {
|
|
2077
2077
|
type: "git",
|
|
@@ -2671,7 +2671,7 @@ async function create(projectName, repoUrl, templateName, autoCreate) {
|
|
|
2671
2671
|
|
|
2672
2672
|
// src/deploy.ts
|
|
2673
2673
|
import { execSync as execSync2 } from "node:child_process";
|
|
2674
|
-
import { existsSync as
|
|
2674
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
2675
2675
|
import { join as join2 } from "node:path";
|
|
2676
2676
|
|
|
2677
2677
|
// node_modules/mime/dist/types/other.js
|
|
@@ -3857,8 +3857,12 @@ var Mime_default = Mime;
|
|
|
3857
3857
|
// node_modules/mime/dist/src/index.js
|
|
3858
3858
|
var src_default = new Mime_default(standard_default, other_default)._freeze();
|
|
3859
3859
|
|
|
3860
|
-
// src/
|
|
3861
|
-
import { existsSync as
|
|
3860
|
+
// src/config-file.ts
|
|
3861
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3862
|
+
import { resolve as resolve2 } from "node:path";
|
|
3863
|
+
|
|
3864
|
+
// src/update-webapp-config.ts
|
|
3865
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3862
3866
|
import { resolve } from "node:path";
|
|
3863
3867
|
var CONFIG_URLS = {
|
|
3864
3868
|
local: "http://localhost:5176/api/webapps/config",
|
|
@@ -3866,10 +3870,516 @@ var CONFIG_URLS = {
|
|
|
3866
3870
|
staging: "https://staging-cms.rodyssey.ai/api/webapps/config",
|
|
3867
3871
|
production: "https://cms.rodyssey.ai/api/webapps/config"
|
|
3868
3872
|
};
|
|
3873
|
+
function parseJsonOption(value, optionName) {
|
|
3874
|
+
const maybePath = resolve(process.cwd(), value);
|
|
3875
|
+
const raw = existsSync4(maybePath) ? readFileSync3(maybePath, "utf-8") : value;
|
|
3876
|
+
try {
|
|
3877
|
+
const parsed = JSON.parse(raw);
|
|
3878
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3879
|
+
throw new Error("value must be a JSON object");
|
|
3880
|
+
}
|
|
3881
|
+
return parsed;
|
|
3882
|
+
} catch (error) {
|
|
3883
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3884
|
+
throw new Error(`Invalid ${optionName}: ${message}`);
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
function coerceMaybeNull(value) {
|
|
3888
|
+
if (value === undefined)
|
|
3889
|
+
return;
|
|
3890
|
+
if (value === "null")
|
|
3891
|
+
return null;
|
|
3892
|
+
return value;
|
|
3893
|
+
}
|
|
3894
|
+
function resolveConfigUrl(options, required = true) {
|
|
3895
|
+
const configUrl = options.url || process.env.WEBAPP_CONFIG_URL || CONFIG_URLS[options.env];
|
|
3896
|
+
if (!configUrl) {
|
|
3897
|
+
if (!required)
|
|
3898
|
+
return;
|
|
3899
|
+
console.error("❌ Error: no webapp config endpoint configured.");
|
|
3900
|
+
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.`);
|
|
3901
|
+
process.exit(1);
|
|
3902
|
+
}
|
|
3903
|
+
const url = new URL(configUrl);
|
|
3904
|
+
if (!options.host && !options.port) {
|
|
3905
|
+
return url.toString().replace(/\/$/, "");
|
|
3906
|
+
}
|
|
3907
|
+
if (options.host)
|
|
3908
|
+
url.hostname = options.host;
|
|
3909
|
+
if (options.port)
|
|
3910
|
+
url.port = String(options.port);
|
|
3911
|
+
return url.toString().replace(/\/$/, "");
|
|
3912
|
+
}
|
|
3913
|
+
function buildDetailsPayload(options) {
|
|
3914
|
+
const payload = options.details ? parseJsonOption(options.details, "--details") : {};
|
|
3915
|
+
const title = coerceMaybeNull(options.title);
|
|
3916
|
+
const description = coerceMaybeNull(options.description);
|
|
3917
|
+
const coverImg = coerceMaybeNull(options.coverImg);
|
|
3918
|
+
if (title !== undefined)
|
|
3919
|
+
payload.title = title;
|
|
3920
|
+
if (description !== undefined)
|
|
3921
|
+
payload.description = description;
|
|
3922
|
+
if (coverImg !== undefined)
|
|
3923
|
+
payload.coverImg = coverImg;
|
|
3924
|
+
if (options.localization !== undefined) {
|
|
3925
|
+
payload.localization = options.localization === "null" ? null : parseJsonOption(options.localization, "--localization");
|
|
3926
|
+
}
|
|
3927
|
+
return payload;
|
|
3928
|
+
}
|
|
3929
|
+
function resolveWebappId(webappId) {
|
|
3930
|
+
const resolved = webappId || process.env.WEBAPP_ID;
|
|
3931
|
+
if (!resolved) {
|
|
3932
|
+
console.error("❌ Error: WEBAPP_ID is not set. Pass --webapp-id or set it in your .env file.");
|
|
3933
|
+
process.exit(1);
|
|
3934
|
+
}
|
|
3935
|
+
return resolved;
|
|
3936
|
+
}
|
|
3937
|
+
function ensureDeployToken(env) {
|
|
3938
|
+
if (process.env.DEPLOY_TOKEN)
|
|
3939
|
+
return;
|
|
3940
|
+
console.error("❌ Error: DEPLOY_TOKEN is not set in environment variables.");
|
|
3941
|
+
console.info(`\uD83D\uDCA1 Please check your .env or .env.${env} file.`);
|
|
3942
|
+
process.exit(1);
|
|
3943
|
+
}
|
|
3944
|
+
function getConfigUrl(options, required = true) {
|
|
3945
|
+
return resolveConfigUrl(options, required);
|
|
3946
|
+
}
|
|
3947
|
+
async function patchWebappConfig(options) {
|
|
3948
|
+
loadEnv(options.env);
|
|
3949
|
+
const webappId = resolveWebappId(options.webappId);
|
|
3950
|
+
const CONFIG_URL = getConfigUrl(options, false);
|
|
3951
|
+
if (!CONFIG_URL) {
|
|
3952
|
+
throw new Error("No webapp config endpoint configured.");
|
|
3953
|
+
}
|
|
3954
|
+
ensureDeployToken(options.env);
|
|
3955
|
+
const response = await fetch(CONFIG_URL, {
|
|
3956
|
+
method: "PATCH",
|
|
3957
|
+
headers: {
|
|
3958
|
+
Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
|
|
3959
|
+
"Content-Type": "application/json"
|
|
3960
|
+
},
|
|
3961
|
+
body: JSON.stringify({ webappId, details: options.details })
|
|
3962
|
+
});
|
|
3963
|
+
if (!response.ok) {
|
|
3964
|
+
const errorText = await response.text();
|
|
3965
|
+
throw new Error(`Config update failed: ${response.status} ${response.statusText}
|
|
3966
|
+
${errorText}`);
|
|
3967
|
+
}
|
|
3968
|
+
return await response.json().catch(() => {
|
|
3969
|
+
return;
|
|
3970
|
+
});
|
|
3971
|
+
}
|
|
3972
|
+
async function fetchWebappConfig(options) {
|
|
3973
|
+
loadEnv(options.env);
|
|
3974
|
+
const webappId = resolveWebappId(options.webappId);
|
|
3975
|
+
const CONFIG_URL = getConfigUrl(options, false);
|
|
3976
|
+
if (!CONFIG_URL) {
|
|
3977
|
+
throw new Error("No webapp config endpoint configured.");
|
|
3978
|
+
}
|
|
3979
|
+
ensureDeployToken(options.env);
|
|
3980
|
+
const url = new URL(CONFIG_URL);
|
|
3981
|
+
url.searchParams.set("webappId", webappId);
|
|
3982
|
+
const response = await fetch(url, {
|
|
3983
|
+
method: "GET",
|
|
3984
|
+
headers: {
|
|
3985
|
+
Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
|
|
3986
|
+
Accept: "application/json"
|
|
3987
|
+
}
|
|
3988
|
+
});
|
|
3989
|
+
if (!response.ok) {
|
|
3990
|
+
const errorText = await response.text();
|
|
3991
|
+
throw new Error(`Config fetch failed: ${response.status} ${response.statusText}
|
|
3992
|
+
${errorText}`);
|
|
3993
|
+
}
|
|
3994
|
+
return await response.json();
|
|
3995
|
+
}
|
|
3996
|
+
async function getWebappConfig(options) {
|
|
3997
|
+
loadEnv(options.env);
|
|
3998
|
+
const webappId = resolveWebappId(options.webappId);
|
|
3999
|
+
const CONFIG_URL = getConfigUrl(options);
|
|
4000
|
+
if (!CONFIG_URL)
|
|
4001
|
+
return;
|
|
4002
|
+
const url = new URL(CONFIG_URL);
|
|
4003
|
+
url.searchParams.set("webappId", webappId);
|
|
4004
|
+
console.log(`⚙️ Pulling webapp config for [${options.env}] environment...`);
|
|
4005
|
+
console.log(`\uD83D\uDCCD Config URL: ${url.toString()}`);
|
|
4006
|
+
console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
|
|
4007
|
+
`);
|
|
4008
|
+
const config = await fetchWebappConfig(options);
|
|
4009
|
+
const output = JSON.stringify(config, null, 2);
|
|
4010
|
+
if (options.out) {
|
|
4011
|
+
writeFileSync3(options.out, `${output}
|
|
4012
|
+
`, "utf-8");
|
|
4013
|
+
console.log(`✅ Webapp config written to ${options.out}`);
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
4016
|
+
console.log(output);
|
|
4017
|
+
}
|
|
4018
|
+
async function updateWebappConfig(options) {
|
|
4019
|
+
loadEnv(options.env);
|
|
4020
|
+
const webappId = resolveWebappId(options.webappId);
|
|
4021
|
+
const details = buildDetailsPayload(options);
|
|
4022
|
+
if (Object.keys(details).length === 0) {
|
|
4023
|
+
console.error("❌ Error: no detail fields provided. Use --title, --description, --cover-img, --localization, or --details.");
|
|
4024
|
+
process.exit(1);
|
|
4025
|
+
}
|
|
4026
|
+
const payload = { webappId, details };
|
|
4027
|
+
const CONFIG_URL = getConfigUrl(options, !options.dryRun);
|
|
4028
|
+
if (!options.dryRun)
|
|
4029
|
+
ensureDeployToken(options.env);
|
|
4030
|
+
console.log(`⚙️ Updating webapp config for [${options.env}] environment...`);
|
|
4031
|
+
if (CONFIG_URL) {
|
|
4032
|
+
console.log(`\uD83D\uDCCD Config URL: ${CONFIG_URL}`);
|
|
4033
|
+
}
|
|
4034
|
+
console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
|
|
4035
|
+
`);
|
|
4036
|
+
if (options.dryRun) {
|
|
4037
|
+
console.log("\uD83E\uDDEA Dry run payload:");
|
|
4038
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4041
|
+
if (!CONFIG_URL)
|
|
4042
|
+
return;
|
|
4043
|
+
const result = await patchWebappConfig({
|
|
4044
|
+
env: options.env,
|
|
4045
|
+
details,
|
|
4046
|
+
webappId,
|
|
4047
|
+
url: options.url,
|
|
4048
|
+
host: options.host,
|
|
4049
|
+
port: options.port
|
|
4050
|
+
});
|
|
4051
|
+
console.log("✅ Webapp config updated");
|
|
4052
|
+
if (result !== undefined) {
|
|
4053
|
+
console.log(`
|
|
4054
|
+
\uD83D\uDCCB Update result:`, result);
|
|
4055
|
+
}
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
// src/cli-ui.ts
|
|
4059
|
+
function isPlainObject(value) {
|
|
4060
|
+
return !!value && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
4061
|
+
}
|
|
4062
|
+
function compact(value) {
|
|
4063
|
+
return JSON.stringify(value);
|
|
4064
|
+
}
|
|
4065
|
+
function pretty(value) {
|
|
4066
|
+
return JSON.stringify(value, null, 2);
|
|
4067
|
+
}
|
|
4068
|
+
function useColor() {
|
|
4069
|
+
return !!process.stdout.isTTY && !process.env.NO_COLOR;
|
|
4070
|
+
}
|
|
4071
|
+
function paint(code, text) {
|
|
4072
|
+
return useColor() ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
4073
|
+
}
|
|
4074
|
+
var green = (s) => paint("32", s);
|
|
4075
|
+
var red = (s) => paint("31", s);
|
|
4076
|
+
var yellow = (s) => paint("33", s);
|
|
4077
|
+
var dim = (s) => paint("2", s);
|
|
4078
|
+
var strike = (s) => paint("9", s);
|
|
4079
|
+
async function prompt(question) {
|
|
4080
|
+
const readline = await import("node:readline");
|
|
4081
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4082
|
+
return new Promise((resolveAnswer) => {
|
|
4083
|
+
rl.question(question, (answer) => {
|
|
4084
|
+
rl.close();
|
|
4085
|
+
resolveAnswer(answer);
|
|
4086
|
+
});
|
|
4087
|
+
});
|
|
4088
|
+
}
|
|
4089
|
+
function isExplicitYes(answer) {
|
|
4090
|
+
const trimmed = answer.trim().toLowerCase();
|
|
4091
|
+
return trimmed === "y" || trimmed === "yes";
|
|
4092
|
+
}
|
|
4093
|
+
function pathToDot(path3) {
|
|
4094
|
+
return path3.replace(/^\//, "").replaceAll("/", ".");
|
|
4095
|
+
}
|
|
4096
|
+
function deepEqual(a, b) {
|
|
4097
|
+
if (a === b)
|
|
4098
|
+
return true;
|
|
4099
|
+
if (typeof a !== typeof b)
|
|
4100
|
+
return false;
|
|
4101
|
+
if (a === null || b === null)
|
|
4102
|
+
return false;
|
|
4103
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
4104
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
4105
|
+
return false;
|
|
4106
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
4107
|
+
}
|
|
4108
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
4109
|
+
const ak = Object.keys(a);
|
|
4110
|
+
const bk = Object.keys(b);
|
|
4111
|
+
if (ak.length !== bk.length)
|
|
4112
|
+
return false;
|
|
4113
|
+
return ak.every((k) => deepEqual(a[k], b[k]));
|
|
4114
|
+
}
|
|
4115
|
+
return false;
|
|
4116
|
+
}
|
|
4117
|
+
function diffJson(before, after, path3 = "") {
|
|
4118
|
+
if (deepEqual(before, after))
|
|
4119
|
+
return [];
|
|
4120
|
+
if (before === undefined)
|
|
4121
|
+
return [{ path: path3, kind: "add", after }];
|
|
4122
|
+
if (after === undefined)
|
|
4123
|
+
return [{ path: path3, kind: "remove", before }];
|
|
4124
|
+
if (!isPlainObject(before) || !isPlainObject(after)) {
|
|
4125
|
+
return [{ path: path3, kind: "change", before, after }];
|
|
4126
|
+
}
|
|
4127
|
+
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
4128
|
+
const out = [];
|
|
4129
|
+
for (const key of keys) {
|
|
4130
|
+
out.push(...diffJson(before[key], after[key], `${path3}/${key}`));
|
|
4131
|
+
}
|
|
4132
|
+
return out;
|
|
4133
|
+
}
|
|
4134
|
+
function deltaLine(d) {
|
|
4135
|
+
const path3 = pathToDot(d.path) || "(root)";
|
|
4136
|
+
if (d.kind === "add")
|
|
4137
|
+
return green(` ${path3}: ${compact(d.after)}`);
|
|
4138
|
+
if (d.kind === "change") {
|
|
4139
|
+
return yellow(` ${path3}: ${strike(compact(d.before))} → ${compact(d.after)}`);
|
|
4140
|
+
}
|
|
4141
|
+
return red(strike(` ${path3}: ${compact(d.before)}`));
|
|
4142
|
+
}
|
|
4143
|
+
function formatObjectDelta(deltas, header) {
|
|
4144
|
+
if (deltas.length === 0)
|
|
4145
|
+
return `${header}
|
|
4146
|
+
${dim(" (no changes)")}`;
|
|
4147
|
+
const adds = deltas.filter((d) => d.kind === "add");
|
|
4148
|
+
const changes = deltas.filter((d) => d.kind === "change");
|
|
4149
|
+
const removes = deltas.filter((d) => d.kind === "remove");
|
|
4150
|
+
const sections = [];
|
|
4151
|
+
if (adds.length)
|
|
4152
|
+
sections.push([green("New"), ...adds.map(deltaLine)].join(`
|
|
4153
|
+
`));
|
|
4154
|
+
if (changes.length)
|
|
4155
|
+
sections.push([yellow("Update"), ...changes.map(deltaLine)].join(`
|
|
4156
|
+
`));
|
|
4157
|
+
if (removes.length)
|
|
4158
|
+
sections.push([red("Delete"), ...removes.map(deltaLine)].join(`
|
|
4159
|
+
`));
|
|
4160
|
+
return `${header}
|
|
4161
|
+
|
|
4162
|
+
${sections.join(`
|
|
4163
|
+
|
|
4164
|
+
`)}`;
|
|
4165
|
+
}
|
|
4166
|
+
|
|
4167
|
+
// src/config-file.ts
|
|
4168
|
+
var DEFAULT_CONFIG_FILE = "webapp.config.json";
|
|
4169
|
+
function isClearSignalEmpty(value) {
|
|
4170
|
+
if (value === null)
|
|
4171
|
+
return true;
|
|
4172
|
+
if (Array.isArray(value) && value.length === 0)
|
|
4173
|
+
return true;
|
|
4174
|
+
if (isPlainObject(value) && Object.keys(value).length === 0)
|
|
4175
|
+
return true;
|
|
4176
|
+
return false;
|
|
4177
|
+
}
|
|
4178
|
+
function sanitizeDetails(details) {
|
|
4179
|
+
const out = {};
|
|
4180
|
+
for (const [key, value] of Object.entries(details)) {
|
|
4181
|
+
if (isClearSignalEmpty(value))
|
|
4182
|
+
continue;
|
|
4183
|
+
out[key] = value;
|
|
4184
|
+
}
|
|
4185
|
+
return out;
|
|
4186
|
+
}
|
|
4187
|
+
function unwrapDetails(payload) {
|
|
4188
|
+
if (isPlainObject(payload) && isPlainObject(payload.details)) {
|
|
4189
|
+
return payload.details;
|
|
4190
|
+
}
|
|
4191
|
+
if (isPlainObject(payload) && "details" in payload)
|
|
4192
|
+
return {};
|
|
4193
|
+
if (isPlainObject(payload))
|
|
4194
|
+
return payload;
|
|
4195
|
+
return {};
|
|
4196
|
+
}
|
|
4197
|
+
function projectDetailsPatch(current, patch) {
|
|
4198
|
+
const next = { ...current };
|
|
4199
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
4200
|
+
if (isClearSignalEmpty(value)) {
|
|
4201
|
+
delete next[key];
|
|
4202
|
+
} else {
|
|
4203
|
+
next[key] = value;
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
return next;
|
|
4207
|
+
}
|
|
4208
|
+
function computeDetailsDelta(current, fileDetails) {
|
|
4209
|
+
return diffJson(current, projectDetailsPatch(current, fileDetails));
|
|
4210
|
+
}
|
|
4211
|
+
function readConfigFile(filePath) {
|
|
4212
|
+
if (!existsSync5(filePath)) {
|
|
4213
|
+
throw new Error(`Config file not found: ${filePath}. Run \`ro app config pull\` first.`);
|
|
4214
|
+
}
|
|
4215
|
+
let raw;
|
|
4216
|
+
try {
|
|
4217
|
+
raw = readFileSync4(filePath, "utf-8");
|
|
4218
|
+
} catch (error) {
|
|
4219
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4220
|
+
throw new Error(`Could not read ${filePath}: ${message}`);
|
|
4221
|
+
}
|
|
4222
|
+
let parsed;
|
|
4223
|
+
try {
|
|
4224
|
+
parsed = JSON.parse(raw);
|
|
4225
|
+
} catch (error) {
|
|
4226
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4227
|
+
throw new Error(`Invalid JSON in ${filePath}: ${message}`);
|
|
4228
|
+
}
|
|
4229
|
+
if (!isPlainObject(parsed)) {
|
|
4230
|
+
throw new Error(`${filePath} must contain a JSON object.`);
|
|
4231
|
+
}
|
|
4232
|
+
return parsed;
|
|
4233
|
+
}
|
|
4234
|
+
async function pullWebappConfig(options) {
|
|
4235
|
+
const raw = await fetchWebappConfig({
|
|
4236
|
+
env: options.env,
|
|
4237
|
+
webappId: options.webappId,
|
|
4238
|
+
url: options.url,
|
|
4239
|
+
host: options.host,
|
|
4240
|
+
port: options.port
|
|
4241
|
+
});
|
|
4242
|
+
const details = unwrapDetails(raw);
|
|
4243
|
+
const cleaned = sanitizeDetails(details);
|
|
4244
|
+
const stripped = Object.keys(details).length - Object.keys(cleaned).length;
|
|
4245
|
+
const fileName = options.out || DEFAULT_CONFIG_FILE;
|
|
4246
|
+
const outPath = resolve2(process.cwd(), fileName);
|
|
4247
|
+
writeFileSync4(outPath, `${JSON.stringify(cleaned, null, 2)}
|
|
4248
|
+
`, "utf-8");
|
|
4249
|
+
console.log(`✅ Wrote ${fileName} (${Object.keys(cleaned).length} fields, ${stripped} empty default${stripped === 1 ? "" : "s"} stripped)`);
|
|
4250
|
+
}
|
|
4251
|
+
async function pushWebappConfig(options) {
|
|
4252
|
+
const fileName = options.file || DEFAULT_CONFIG_FILE;
|
|
4253
|
+
const filePath = resolve2(process.cwd(), fileName);
|
|
4254
|
+
const fileDetails = readConfigFile(filePath);
|
|
4255
|
+
const raw = await fetchWebappConfig({
|
|
4256
|
+
env: options.env,
|
|
4257
|
+
webappId: options.webappId,
|
|
4258
|
+
url: options.url,
|
|
4259
|
+
host: options.host,
|
|
4260
|
+
port: options.port
|
|
4261
|
+
});
|
|
4262
|
+
const current = unwrapDetails(raw);
|
|
4263
|
+
const deltas = computeDetailsDelta(current, fileDetails);
|
|
4264
|
+
console.log(formatObjectDelta(deltas, `Pushing ${fileName} → [${options.env}]`));
|
|
4265
|
+
if (deltas.length === 0) {
|
|
4266
|
+
console.log(`
|
|
4267
|
+
✓ Already in sync — nothing to push.`);
|
|
4268
|
+
return;
|
|
4269
|
+
}
|
|
4270
|
+
if (options.dryRun) {
|
|
4271
|
+
console.log(`
|
|
4272
|
+
↷ Dry run — no request sent.`);
|
|
4273
|
+
return;
|
|
4274
|
+
}
|
|
4275
|
+
const tty = !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
4276
|
+
if (!options.yes) {
|
|
4277
|
+
if (!tty) {
|
|
4278
|
+
throw new Error("Refusing to push in non-interactive mode. Pass --yes to confirm or --dry-run to preview.");
|
|
4279
|
+
}
|
|
4280
|
+
const answer = await prompt(`
|
|
4281
|
+
Proceed with push on [${options.env}]? (y/N): `);
|
|
4282
|
+
if (!isExplicitYes(answer)) {
|
|
4283
|
+
console.log("✋ Aborted.");
|
|
4284
|
+
return;
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
await patchWebappConfig({
|
|
4288
|
+
env: options.env,
|
|
4289
|
+
details: fileDetails,
|
|
4290
|
+
webappId: options.webappId,
|
|
4291
|
+
url: options.url,
|
|
4292
|
+
host: options.host,
|
|
4293
|
+
port: options.port
|
|
4294
|
+
});
|
|
4295
|
+
console.log(`
|
|
4296
|
+
✅ Webapp config pushed.`);
|
|
4297
|
+
}
|
|
4298
|
+
async function checkConfigDriftOnDeploy(options) {
|
|
4299
|
+
const fileName = options.file || DEFAULT_CONFIG_FILE;
|
|
4300
|
+
const filePath = resolve2(process.cwd(), fileName);
|
|
4301
|
+
if (!existsSync5(filePath))
|
|
4302
|
+
return;
|
|
4303
|
+
if (!process.env.WEBAPP_ID) {
|
|
4304
|
+
console.warn(`
|
|
4305
|
+
⚠️ Skipping config drift check: WEBAPP_ID is not set.`);
|
|
4306
|
+
return;
|
|
4307
|
+
}
|
|
4308
|
+
let fileDetails;
|
|
4309
|
+
try {
|
|
4310
|
+
fileDetails = readConfigFile(filePath);
|
|
4311
|
+
} catch (error) {
|
|
4312
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4313
|
+
console.warn(`
|
|
4314
|
+
⚠️ Skipping config drift check: ${message}`);
|
|
4315
|
+
return;
|
|
4316
|
+
}
|
|
4317
|
+
let current;
|
|
4318
|
+
try {
|
|
4319
|
+
const raw = await fetchWebappConfig({
|
|
4320
|
+
env: options.env,
|
|
4321
|
+
host: options.host,
|
|
4322
|
+
port: options.port
|
|
4323
|
+
});
|
|
4324
|
+
current = unwrapDetails(raw);
|
|
4325
|
+
} catch (error) {
|
|
4326
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4327
|
+
console.warn(`
|
|
4328
|
+
⚠️ Could not check config drift: ${message}`);
|
|
4329
|
+
return;
|
|
4330
|
+
}
|
|
4331
|
+
const deltas = computeDetailsDelta(current, fileDetails);
|
|
4332
|
+
if (deltas.length === 0)
|
|
4333
|
+
return;
|
|
4334
|
+
console.log(`
|
|
4335
|
+
\uD83D\uDCDD ${fileName} differs from the CMS:`);
|
|
4336
|
+
console.log(formatObjectDelta(deltas, `Config drift on [${options.env}]`));
|
|
4337
|
+
const tty = !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
4338
|
+
let shouldPush = options.pushConfig === true;
|
|
4339
|
+
if (!shouldPush) {
|
|
4340
|
+
if (!tty) {
|
|
4341
|
+
console.log(`
|
|
4342
|
+
ℹ️ ${fileName} differs from CMS — run 'ro app config push' to sync, or deploy with --push-config.`);
|
|
4343
|
+
return;
|
|
4344
|
+
}
|
|
4345
|
+
const answer = await prompt(`
|
|
4346
|
+
Push ${fileName} to the CMS now? (y/N): `);
|
|
4347
|
+
shouldPush = isExplicitYes(answer);
|
|
4348
|
+
}
|
|
4349
|
+
if (!shouldPush) {
|
|
4350
|
+
console.log(`
|
|
4351
|
+
↷ Skipped. Run 'ro app config push' later to sync.`);
|
|
4352
|
+
return;
|
|
4353
|
+
}
|
|
4354
|
+
try {
|
|
4355
|
+
await patchWebappConfig({
|
|
4356
|
+
env: options.env,
|
|
4357
|
+
details: fileDetails,
|
|
4358
|
+
host: options.host,
|
|
4359
|
+
port: options.port
|
|
4360
|
+
});
|
|
4361
|
+
console.log("✅ Webapp config pushed.");
|
|
4362
|
+
} catch (error) {
|
|
4363
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4364
|
+
console.warn(`
|
|
4365
|
+
⚠️ Config push failed (deploy still succeeded): ${message}`);
|
|
4366
|
+
console.warn(" Retry with: ro app config push");
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
// src/sync-widget-manifest.ts
|
|
4371
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
|
|
4372
|
+
import { resolve as resolve3 } from "node:path";
|
|
4373
|
+
var CONFIG_URLS2 = {
|
|
4374
|
+
local: "http://localhost:5176/api/webapps/config",
|
|
4375
|
+
development: "https://development-cms.rodyssey.ai/api/webapps/config",
|
|
4376
|
+
staging: "https://staging-cms.rodyssey.ai/api/webapps/config",
|
|
4377
|
+
production: "https://cms.rodyssey.ai/api/webapps/config"
|
|
4378
|
+
};
|
|
3869
4379
|
function resolveWidgetConfigUrl(options) {
|
|
3870
|
-
const rawUrl = options.url || process.env.WEBAPP_CONFIG_URL ||
|
|
4380
|
+
const rawUrl = options.url || process.env.WEBAPP_CONFIG_URL || CONFIG_URLS2[options.env];
|
|
3871
4381
|
if (!rawUrl) {
|
|
3872
|
-
throw new Error(`Unknown environment "${options.env}". Use one of: ${Object.keys(
|
|
4382
|
+
throw new Error(`Unknown environment "${options.env}". Use one of: ${Object.keys(CONFIG_URLS2).join(", ")}, pass --url, or set WEBAPP_CONFIG_URL.`);
|
|
3873
4383
|
}
|
|
3874
4384
|
const url = new URL(rawUrl);
|
|
3875
4385
|
if (options.host)
|
|
@@ -3879,43 +4389,49 @@ function resolveWidgetConfigUrl(options) {
|
|
|
3879
4389
|
return url.toString().replace(/\/$/, "");
|
|
3880
4390
|
}
|
|
3881
4391
|
function resolveManifestPath(manifest) {
|
|
3882
|
-
if (manifest)
|
|
3883
|
-
|
|
4392
|
+
if (manifest) {
|
|
4393
|
+
const manifestPath = resolve3(process.cwd(), manifest);
|
|
4394
|
+
if (!existsSync6(manifestPath)) {
|
|
4395
|
+
throw new Error(`Widget manifest not found: ${manifestPath}`);
|
|
4396
|
+
}
|
|
4397
|
+
return manifestPath;
|
|
4398
|
+
}
|
|
3884
4399
|
const candidates = [
|
|
3885
|
-
|
|
3886
|
-
|
|
4400
|
+
resolve3(process.cwd(), "build/client/widgets.manifest.json"),
|
|
4401
|
+
resolve3(process.cwd(), "dist/widgets.manifest.json")
|
|
3887
4402
|
];
|
|
3888
|
-
const found = candidates.find((candidate) =>
|
|
3889
|
-
if (!found) {
|
|
3890
|
-
throw new Error("No widget manifest found. Run `bun run build` first or pass --manifest <path>.");
|
|
3891
|
-
}
|
|
4403
|
+
const found = candidates.find((candidate) => existsSync6(candidate));
|
|
3892
4404
|
return found;
|
|
3893
4405
|
}
|
|
3894
4406
|
function readManifest(path3) {
|
|
3895
|
-
const parsed = JSON.parse(
|
|
4407
|
+
const parsed = JSON.parse(readFileSync5(path3, "utf-8"));
|
|
3896
4408
|
if (!Array.isArray(parsed)) {
|
|
3897
4409
|
throw new Error(`Widget manifest must be a JSON array: ${path3}`);
|
|
3898
4410
|
}
|
|
3899
4411
|
return parsed;
|
|
3900
4412
|
}
|
|
3901
|
-
function
|
|
4413
|
+
function resolveWebappId2(webappId) {
|
|
3902
4414
|
const resolved = webappId || process.env.WEBAPP_ID;
|
|
3903
4415
|
if (!resolved) {
|
|
3904
4416
|
throw new Error("WEBAPP_ID is not set. Add it to .env or pass --webapp-id.");
|
|
3905
4417
|
}
|
|
3906
4418
|
return resolved;
|
|
3907
4419
|
}
|
|
3908
|
-
function
|
|
4420
|
+
function ensureDeployToken2(env) {
|
|
3909
4421
|
if (process.env.DEPLOY_TOKEN)
|
|
3910
4422
|
return;
|
|
3911
4423
|
throw new Error(`DEPLOY_TOKEN is not set. Please check your .env or .env.${env} file.`);
|
|
3912
4424
|
}
|
|
3913
4425
|
async function syncWidgetManifest(options) {
|
|
3914
4426
|
loadEnv(options.env);
|
|
3915
|
-
const webappId = resolveWebappId(options.webappId);
|
|
3916
|
-
if (!options.dryRun)
|
|
3917
|
-
ensureDeployToken(options.env);
|
|
3918
4427
|
const manifestPath = resolveManifestPath(options.manifest);
|
|
4428
|
+
if (!manifestPath) {
|
|
4429
|
+
console.log("No widget manifest found; skipping widget manifest sync.");
|
|
4430
|
+
return;
|
|
4431
|
+
}
|
|
4432
|
+
const webappId = resolveWebappId2(options.webappId);
|
|
4433
|
+
if (!options.dryRun)
|
|
4434
|
+
ensureDeployToken2(options.env);
|
|
3919
4435
|
const manifest = readManifest(manifestPath);
|
|
3920
4436
|
const payload = { webappId, details: { widgetManifest: manifest } };
|
|
3921
4437
|
const configUrl = resolveWidgetConfigUrl(options);
|
|
@@ -3974,7 +4490,7 @@ function pickNumber(value) {
|
|
|
3974
4490
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
3975
4491
|
}
|
|
3976
4492
|
function isFullstackProject() {
|
|
3977
|
-
return
|
|
4493
|
+
return existsSync7("app") && existsSync7("workers/app.ts") && existsSync7("wrangler.jsonc");
|
|
3978
4494
|
}
|
|
3979
4495
|
function resolveDeployUrl(env, overrides) {
|
|
3980
4496
|
let deployUrl = DEPLOY_URLS[env];
|
|
@@ -4050,7 +4566,7 @@ function collectScripts(scriptFiles) {
|
|
|
4050
4566
|
const payload = { api: {}, cron: {}, cronConfig: null };
|
|
4051
4567
|
const summary = { apiEndpoints: [], cronJobs: [], mcpEndpoints: [] };
|
|
4052
4568
|
for (const filePath of scriptFiles) {
|
|
4053
|
-
const content =
|
|
4569
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
4054
4570
|
const relativePath = normalizeBuildRelativePath(filePath);
|
|
4055
4571
|
if (relativePath === "cron-jobs/cron.config.json") {
|
|
4056
4572
|
payload.cronConfig = readCronConfig(filePath, content);
|
|
@@ -4166,7 +4682,7 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4166
4682
|
return { summary, result };
|
|
4167
4683
|
}
|
|
4168
4684
|
function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
4169
|
-
if (!
|
|
4685
|
+
if (!existsSync7(dirPath))
|
|
4170
4686
|
return arrayOfFiles;
|
|
4171
4687
|
const files = readdirSync(dirPath);
|
|
4172
4688
|
files.forEach(function(f) {
|
|
@@ -4180,7 +4696,7 @@ function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
|
4180
4696
|
return arrayOfFiles;
|
|
4181
4697
|
}
|
|
4182
4698
|
function fileToBlob(filePath) {
|
|
4183
|
-
const buffer =
|
|
4699
|
+
const buffer = readFileSync6(filePath);
|
|
4184
4700
|
return new Blob([buffer], { type: src_default.getType(filePath) || "application/octet-stream" });
|
|
4185
4701
|
}
|
|
4186
4702
|
async function deployFullstack(env, overrides) {
|
|
@@ -4304,7 +4820,7 @@ ${errorText}`);
|
|
|
4304
4820
|
console.log(`✅ Created ${ZIP_FILE}
|
|
4305
4821
|
`);
|
|
4306
4822
|
console.log("☁️ Step 4: Deploying HTML zip to server...");
|
|
4307
|
-
const zipBuffer =
|
|
4823
|
+
const zipBuffer = readFileSync6(ZIP_FILE);
|
|
4308
4824
|
try {
|
|
4309
4825
|
const response = await fetch(DEPLOY_URL, {
|
|
4310
4826
|
method: "POST",
|
|
@@ -4334,7 +4850,7 @@ ${errorText}`);
|
|
|
4334
4850
|
console.error("❌ Deploy failed:", error);
|
|
4335
4851
|
throw error;
|
|
4336
4852
|
} finally {
|
|
4337
|
-
if (
|
|
4853
|
+
if (existsSync7(ZIP_FILE)) {
|
|
4338
4854
|
unlinkSync(ZIP_FILE);
|
|
4339
4855
|
console.log(`
|
|
4340
4856
|
\uD83E\uDDF9 Cleaned up ${ZIP_FILE}`);
|
|
@@ -4347,18 +4863,23 @@ async function deploy(env = "development", overrides = {}) {
|
|
|
4347
4863
|
loadEnv(env);
|
|
4348
4864
|
if (isFullstackProject()) {
|
|
4349
4865
|
await deployFullstack(env, overrides);
|
|
4350
|
-
|
|
4866
|
+
} else {
|
|
4867
|
+
await deploySpa(env, overrides);
|
|
4868
|
+
}
|
|
4869
|
+
if (!overrides.skipConfigCheck) {
|
|
4870
|
+
await checkConfigDriftOnDeploy({
|
|
4871
|
+
env,
|
|
4872
|
+
pushConfig: overrides.pushConfig,
|
|
4873
|
+
host: overrides.host,
|
|
4874
|
+
port: overrides.port
|
|
4875
|
+
});
|
|
4351
4876
|
}
|
|
4352
|
-
await deploySpa(env, overrides);
|
|
4353
4877
|
}
|
|
4354
4878
|
|
|
4355
4879
|
// src/global-config.ts
|
|
4356
|
-
import { existsSync as
|
|
4357
|
-
import { resolve as
|
|
4880
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4881
|
+
import { resolve as resolve4 } from "node:path";
|
|
4358
4882
|
var PROD_ENV = "production";
|
|
4359
|
-
function isPlainObject(value) {
|
|
4360
|
-
return !!value && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
4361
|
-
}
|
|
4362
4883
|
function applyMergePatch(target, patch) {
|
|
4363
4884
|
if (!isPlainObject(patch))
|
|
4364
4885
|
return patch;
|
|
@@ -4375,8 +4896,8 @@ function applyMergePatch(target, patch) {
|
|
|
4375
4896
|
function parseFlag(name, value) {
|
|
4376
4897
|
if (value === "null")
|
|
4377
4898
|
return null;
|
|
4378
|
-
const candidatePath =
|
|
4379
|
-
const raw =
|
|
4899
|
+
const candidatePath = resolve4(process.cwd(), value);
|
|
4900
|
+
const raw = existsSync8(candidatePath) ? readFileSync7(candidatePath, "utf-8") : value;
|
|
4380
4901
|
try {
|
|
4381
4902
|
return JSON.parse(raw);
|
|
4382
4903
|
} catch (error) {
|
|
@@ -4392,95 +4913,20 @@ function buildPayload(options) {
|
|
|
4392
4913
|
if (options.publicConfig !== undefined) {
|
|
4393
4914
|
payload.publicConfig = parseFlag("public-config", options.publicConfig);
|
|
4394
4915
|
}
|
|
4395
|
-
if (payload.config === undefined && payload.publicConfig === undefined) {
|
|
4396
|
-
throw new Error("Provide at least one of --config or --public-config.");
|
|
4397
|
-
}
|
|
4398
|
-
return payload;
|
|
4399
|
-
}
|
|
4400
|
-
function resolveEndpoint(env, override, cmsUrl) {
|
|
4401
|
-
if (override)
|
|
4402
|
-
return override;
|
|
4403
|
-
return `${resolveCmsUrl(env, cmsUrl)}/api/cli/cms/global-config`;
|
|
4404
|
-
}
|
|
4405
|
-
async function prompt(question) {
|
|
4406
|
-
const readline = await import("node:readline");
|
|
4407
|
-
const rl = readline.createInterface({
|
|
4408
|
-
input: process.stdin,
|
|
4409
|
-
output: process.stdout
|
|
4410
|
-
});
|
|
4411
|
-
return new Promise((resolveAnswer) => {
|
|
4412
|
-
rl.question(question, (answer) => {
|
|
4413
|
-
rl.close();
|
|
4414
|
-
resolveAnswer(answer);
|
|
4415
|
-
});
|
|
4416
|
-
});
|
|
4417
|
-
}
|
|
4418
|
-
function isExplicitYes(answer) {
|
|
4419
|
-
const trimmed = answer.trim().toLowerCase();
|
|
4420
|
-
return trimmed === "y" || trimmed === "yes";
|
|
4421
|
-
}
|
|
4422
|
-
function pretty(value) {
|
|
4423
|
-
return JSON.stringify(value, null, 2);
|
|
4424
|
-
}
|
|
4425
|
-
function compact(value) {
|
|
4426
|
-
return JSON.stringify(value);
|
|
4427
|
-
}
|
|
4428
|
-
function useColor() {
|
|
4429
|
-
return !!process.stdout.isTTY && !process.env.NO_COLOR;
|
|
4430
|
-
}
|
|
4431
|
-
function paint(code, text) {
|
|
4432
|
-
return useColor() ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
4433
|
-
}
|
|
4434
|
-
var green = (s) => paint("32", s);
|
|
4435
|
-
var red = (s) => paint("31", s);
|
|
4436
|
-
var yellow = (s) => paint("33", s);
|
|
4437
|
-
var dim = (s) => paint("2", s);
|
|
4438
|
-
var strike = (s) => paint("9", s);
|
|
4439
|
-
var COLUMN_LABEL = {
|
|
4440
|
-
config: "Config",
|
|
4441
|
-
publicConfig: "Public Config"
|
|
4442
|
-
};
|
|
4443
|
-
function pathToDot(path3) {
|
|
4444
|
-
return path3.replace(/^\//, "").replaceAll("/", ".");
|
|
4445
|
-
}
|
|
4446
|
-
function deepEqual(a, b) {
|
|
4447
|
-
if (a === b)
|
|
4448
|
-
return true;
|
|
4449
|
-
if (typeof a !== typeof b)
|
|
4450
|
-
return false;
|
|
4451
|
-
if (a === null || b === null)
|
|
4452
|
-
return false;
|
|
4453
|
-
if (Array.isArray(a) || Array.isArray(b)) {
|
|
4454
|
-
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length)
|
|
4455
|
-
return false;
|
|
4456
|
-
return a.every((v, i) => deepEqual(v, b[i]));
|
|
4457
|
-
}
|
|
4458
|
-
if (typeof a === "object" && typeof b === "object") {
|
|
4459
|
-
const ak = Object.keys(a);
|
|
4460
|
-
const bk = Object.keys(b);
|
|
4461
|
-
if (ak.length !== bk.length)
|
|
4462
|
-
return false;
|
|
4463
|
-
return ak.every((k) => deepEqual(a[k], b[k]));
|
|
4464
|
-
}
|
|
4465
|
-
return false;
|
|
4466
|
-
}
|
|
4467
|
-
function diffJson(before, after, path3 = "") {
|
|
4468
|
-
if (deepEqual(before, after))
|
|
4469
|
-
return [];
|
|
4470
|
-
if (before === undefined)
|
|
4471
|
-
return [{ path: path3, kind: "add", after }];
|
|
4472
|
-
if (after === undefined)
|
|
4473
|
-
return [{ path: path3, kind: "remove", before }];
|
|
4474
|
-
if (!isPlainObject(before) || !isPlainObject(after)) {
|
|
4475
|
-
return [{ path: path3, kind: "change", before, after }];
|
|
4476
|
-
}
|
|
4477
|
-
const keys = new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
4478
|
-
const out = [];
|
|
4479
|
-
for (const key of keys) {
|
|
4480
|
-
out.push(...diffJson(before[key], after[key], `${path3}/${key}`));
|
|
4916
|
+
if (payload.config === undefined && payload.publicConfig === undefined) {
|
|
4917
|
+
throw new Error("Provide at least one of --config or --public-config.");
|
|
4481
4918
|
}
|
|
4482
|
-
return
|
|
4919
|
+
return payload;
|
|
4920
|
+
}
|
|
4921
|
+
function resolveEndpoint(env, override, cmsUrl) {
|
|
4922
|
+
if (override)
|
|
4923
|
+
return override;
|
|
4924
|
+
return `${resolveCmsUrl(env, cmsUrl)}/api/cli/cms/global-config`;
|
|
4483
4925
|
}
|
|
4926
|
+
var COLUMN_LABEL = {
|
|
4927
|
+
config: "Config",
|
|
4928
|
+
publicConfig: "Public Config"
|
|
4929
|
+
};
|
|
4484
4930
|
function buildColumnDeltas(current, payload, method) {
|
|
4485
4931
|
const out = [];
|
|
4486
4932
|
for (const column of ["config", "publicConfig"]) {
|
|
@@ -4500,7 +4946,7 @@ function buildColumnDeltas(current, payload, method) {
|
|
|
4500
4946
|
}
|
|
4501
4947
|
return out;
|
|
4502
4948
|
}
|
|
4503
|
-
function
|
|
4949
|
+
function deltaLine2(d, showColumnTag) {
|
|
4504
4950
|
const tag = showColumnTag ? `[${COLUMN_LABEL[d.column]}] ` : "";
|
|
4505
4951
|
const path3 = pathToDot(d.path) || `(entire ${COLUMN_LABEL[d.column]})`;
|
|
4506
4952
|
if (d.kind === "add") {
|
|
@@ -4524,15 +4970,15 @@ ${dim(" (no changes)")}`;
|
|
|
4524
4970
|
const removes = deltas.filter((d) => d.kind === "remove");
|
|
4525
4971
|
const sections = [];
|
|
4526
4972
|
if (adds.length > 0) {
|
|
4527
|
-
sections.push([green("New"), ...adds.map((d) =>
|
|
4973
|
+
sections.push([green("New"), ...adds.map((d) => deltaLine2(d, showTag))].join(`
|
|
4528
4974
|
`));
|
|
4529
4975
|
}
|
|
4530
4976
|
if (changes.length > 0) {
|
|
4531
|
-
sections.push([yellow("Update"), ...changes.map((d) =>
|
|
4977
|
+
sections.push([yellow("Update"), ...changes.map((d) => deltaLine2(d, showTag))].join(`
|
|
4532
4978
|
`));
|
|
4533
4979
|
}
|
|
4534
4980
|
if (removes.length > 0) {
|
|
4535
|
-
sections.push([red("Delete"), ...removes.map((d) =>
|
|
4981
|
+
sections.push([red("Delete"), ...removes.map((d) => deltaLine2(d, showTag))].join(`
|
|
4536
4982
|
`));
|
|
4537
4983
|
}
|
|
4538
4984
|
return `${header}
|
|
@@ -4571,8 +5017,8 @@ ${pretty(payload)}`);
|
|
|
4571
5017
|
}
|
|
4572
5018
|
const text = pretty(payload);
|
|
4573
5019
|
if (options.out) {
|
|
4574
|
-
const outPath =
|
|
4575
|
-
|
|
5020
|
+
const outPath = resolve4(process.cwd(), options.out);
|
|
5021
|
+
writeFileSync5(outPath, `${text}
|
|
4576
5022
|
`, "utf-8");
|
|
4577
5023
|
console.log(`✅ Wrote global config to ${outPath}`);
|
|
4578
5024
|
} else {
|
|
@@ -4666,190 +5112,167 @@ async function patchGlobalConfig(options) {
|
|
|
4666
5112
|
await writeGlobalConfig("PATCH", options);
|
|
4667
5113
|
}
|
|
4668
5114
|
|
|
4669
|
-
// src/
|
|
4670
|
-
import {
|
|
4671
|
-
import { resolve as
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
local: "http://localhost:5176/api/webapps/config",
|
|
4678
|
-
development: "https://development-cms.rodyssey.ai/api/webapps/config",
|
|
4679
|
-
staging: "https://staging-cms.rodyssey.ai/api/webapps/config",
|
|
4680
|
-
production: "https://cms.rodyssey.ai/api/webapps/config"
|
|
4681
|
-
};
|
|
4682
|
-
function parseJsonOption(value, optionName) {
|
|
4683
|
-
const maybePath = resolve3(process.cwd(), value);
|
|
4684
|
-
const raw = existsSync7(maybePath) ? readFileSync6(maybePath, "utf-8") : value;
|
|
4685
|
-
try {
|
|
4686
|
-
const parsed = JSON.parse(raw);
|
|
4687
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4688
|
-
throw new Error("value must be a JSON object");
|
|
4689
|
-
}
|
|
4690
|
-
return parsed;
|
|
4691
|
-
} catch (error) {
|
|
4692
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4693
|
-
throw new Error(`Invalid ${optionName}: ${message}`);
|
|
5115
|
+
// src/group.ts
|
|
5116
|
+
import { writeFileSync as writeFileSync6 } from "node:fs";
|
|
5117
|
+
import { resolve as resolve5 } from "node:path";
|
|
5118
|
+
var PROD_ENV2 = "production";
|
|
5119
|
+
var ENTITY_TYPES = ["webapp", "character", "scene", "story"];
|
|
5120
|
+
function parseEntityType(raw) {
|
|
5121
|
+
if (raw && ENTITY_TYPES.includes(raw)) {
|
|
5122
|
+
return raw;
|
|
4694
5123
|
}
|
|
5124
|
+
throw new Error(`--type must be one of: ${ENTITY_TYPES.join(", ")}`);
|
|
4695
5125
|
}
|
|
4696
|
-
function
|
|
4697
|
-
if (
|
|
4698
|
-
return;
|
|
4699
|
-
if (
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
function resolveConfigUrl(options, required = true) {
|
|
4704
|
-
const configUrl = options.url || process.env.WEBAPP_CONFIG_URL || CONFIG_URLS2[options.env];
|
|
4705
|
-
if (!configUrl) {
|
|
4706
|
-
if (!required)
|
|
4707
|
-
return;
|
|
4708
|
-
console.error("❌ Error: no webapp config endpoint configured.");
|
|
4709
|
-
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.`);
|
|
4710
|
-
process.exit(1);
|
|
5126
|
+
function resolveEntityId(type, explicitId) {
|
|
5127
|
+
if (explicitId)
|
|
5128
|
+
return explicitId;
|
|
5129
|
+
if (type === "webapp") {
|
|
5130
|
+
if (process.env.WEBAPP_ID)
|
|
5131
|
+
return process.env.WEBAPP_ID;
|
|
5132
|
+
return missingId("(for --type webapp it defaults to WEBAPP_ID from .env, which is also missing)");
|
|
4711
5133
|
}
|
|
4712
|
-
|
|
4713
|
-
if (!options.host && !options.port) {
|
|
4714
|
-
return url.toString().replace(/\/$/, "");
|
|
4715
|
-
}
|
|
4716
|
-
if (options.host)
|
|
4717
|
-
url.hostname = options.host;
|
|
4718
|
-
if (options.port)
|
|
4719
|
-
url.port = String(options.port);
|
|
4720
|
-
return url.toString().replace(/\/$/, "");
|
|
5134
|
+
return missingId(`(no default exists for --type ${type})`);
|
|
4721
5135
|
}
|
|
4722
|
-
function
|
|
4723
|
-
|
|
4724
|
-
const title = coerceMaybeNull(options.title);
|
|
4725
|
-
const description = coerceMaybeNull(options.description);
|
|
4726
|
-
const coverImg = coerceMaybeNull(options.coverImg);
|
|
4727
|
-
if (title !== undefined)
|
|
4728
|
-
payload.title = title;
|
|
4729
|
-
if (description !== undefined)
|
|
4730
|
-
payload.description = description;
|
|
4731
|
-
if (coverImg !== undefined)
|
|
4732
|
-
payload.coverImg = coverImg;
|
|
4733
|
-
if (options.localization !== undefined) {
|
|
4734
|
-
payload.localization = options.localization === "null" ? null : parseJsonOption(options.localization, "--localization");
|
|
4735
|
-
}
|
|
4736
|
-
return payload;
|
|
5136
|
+
function missingId(detail) {
|
|
5137
|
+
throw new Error(`--id is required ${detail}`);
|
|
4737
5138
|
}
|
|
4738
|
-
function
|
|
4739
|
-
|
|
4740
|
-
if (!resolved) {
|
|
4741
|
-
console.error("❌ Error: WEBAPP_ID is not set. Pass --webapp-id or set it in your .env file.");
|
|
4742
|
-
process.exit(1);
|
|
4743
|
-
}
|
|
4744
|
-
return resolved;
|
|
5139
|
+
function orFallback(value, fallback) {
|
|
5140
|
+
return value && value.trim().length > 0 ? value : fallback;
|
|
4745
5141
|
}
|
|
4746
|
-
function
|
|
4747
|
-
|
|
4748
|
-
return;
|
|
4749
|
-
console.error("❌ Error: DEPLOY_TOKEN is not set in environment variables.");
|
|
4750
|
-
console.info(`\uD83D\uDCA1 Please check your .env or .env.${env} file.`);
|
|
4751
|
-
process.exit(1);
|
|
5142
|
+
function capitalize(s) {
|
|
5143
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
4752
5144
|
}
|
|
4753
|
-
function
|
|
4754
|
-
|
|
5145
|
+
function formatGroupList(type, env, groups) {
|
|
5146
|
+
const lines = [
|
|
5147
|
+
`\uD83D\uDCE6 ${capitalize(type)} groups on [${env}] — ${groups.length} found`
|
|
5148
|
+
];
|
|
5149
|
+
for (const g of groups) {
|
|
5150
|
+
const name = orFallback(g.name, "(unnamed)");
|
|
5151
|
+
const where = g.isSystem ? "system" : `school: ${g.schoolName ?? g.schoolId ?? "unknown"}`;
|
|
5152
|
+
const desc = orFallback(g.description, "");
|
|
5153
|
+
lines.push("");
|
|
5154
|
+
lines.push(`• ${name} (${g.itemCount} items · ${where})`);
|
|
5155
|
+
lines.push(` id: ${g.id}`);
|
|
5156
|
+
lines.push(` ${desc || dim("(no description)")}`);
|
|
5157
|
+
}
|
|
5158
|
+
return lines.join(`
|
|
5159
|
+
`);
|
|
4755
5160
|
}
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
const
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
5161
|
+
function formatItemList(type, env, group, items) {
|
|
5162
|
+
const label = orFallback(group.name, group.id);
|
|
5163
|
+
const lines = [
|
|
5164
|
+
`\uD83D\uDCE6 Items in ${type} group "${label}" on [${env}] — ${items.length} found`
|
|
5165
|
+
];
|
|
5166
|
+
for (const item of items) {
|
|
5167
|
+
lines.push("");
|
|
5168
|
+
lines.push(`• ${orFallback(item.label, "(unlabeled)")} (order: ${item.orderIndex ?? "-"})`);
|
|
5169
|
+
lines.push(` id: ${item.entityId}`);
|
|
4762
5170
|
}
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
5171
|
+
return lines.join(`
|
|
5172
|
+
`);
|
|
5173
|
+
}
|
|
5174
|
+
function groupsBaseUrl(env, cmsUrl) {
|
|
5175
|
+
return `${resolveCmsUrl(env, cmsUrl)}/api/cli/groups`;
|
|
5176
|
+
}
|
|
5177
|
+
async function resolveWriteAuth(env, cmsUrl) {
|
|
5178
|
+
if (env === PROD_ENV2) {
|
|
5179
|
+
console.log(`
|
|
5180
|
+
\uD83D\uDD10 Logging in to ${PROD_ENV2} CMS (ephemeral, not stored)...`);
|
|
5181
|
+
const session = await login({ env, cmsUrl, persist: false });
|
|
5182
|
+
console.log(`✅ Logged in to ${session.cmsUrl}`);
|
|
5183
|
+
return { baseUrl: `${session.cmsUrl}/api/cli/groups`, token: session.token };
|
|
5184
|
+
}
|
|
5185
|
+
return { baseUrl: groupsBaseUrl(env, cmsUrl), token: resolveSessionToken(env) };
|
|
5186
|
+
}
|
|
5187
|
+
async function requestJson(method, url, token, body) {
|
|
4766
5188
|
const response = await fetch(url, {
|
|
4767
|
-
method
|
|
5189
|
+
method,
|
|
4768
5190
|
headers: {
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
5191
|
+
Accept: "application/json",
|
|
5192
|
+
Authorization: `Bearer ${token}`,
|
|
5193
|
+
...body !== undefined ? { "Content-Type": "application/json" } : {}
|
|
5194
|
+
},
|
|
5195
|
+
...body !== undefined ? { body: JSON.stringify(body) } : {}
|
|
4772
5196
|
});
|
|
5197
|
+
const payload = await readResponsePayload(response);
|
|
4773
5198
|
if (!response.ok) {
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
${errorText}`);
|
|
5199
|
+
throw new Error(`${method} ${url} failed: ${response.status} ${response.statusText}
|
|
5200
|
+
${pretty(payload)}`);
|
|
4777
5201
|
}
|
|
4778
|
-
return
|
|
5202
|
+
return payload;
|
|
4779
5203
|
}
|
|
4780
|
-
|
|
4781
|
-
loadEnv(options.env);
|
|
4782
|
-
const webappId = resolveWebappId2(options.webappId);
|
|
4783
|
-
const CONFIG_URL = getConfigUrl(options);
|
|
4784
|
-
if (!CONFIG_URL)
|
|
4785
|
-
return;
|
|
4786
|
-
const url = new URL(CONFIG_URL);
|
|
4787
|
-
url.searchParams.set("webappId", webappId);
|
|
4788
|
-
console.log(`⚙️ Pulling webapp config for [${options.env}] environment...`);
|
|
4789
|
-
console.log(`\uD83D\uDCCD Config URL: ${url.toString()}`);
|
|
4790
|
-
console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
|
|
4791
|
-
`);
|
|
4792
|
-
const config = await fetchWebappConfig(options);
|
|
4793
|
-
const output = JSON.stringify(config, null, 2);
|
|
5204
|
+
function emitRead(payload, human, options) {
|
|
4794
5205
|
if (options.out) {
|
|
4795
|
-
|
|
5206
|
+
const outPath = resolve5(process.cwd(), options.out);
|
|
5207
|
+
writeFileSync6(outPath, `${pretty(payload)}
|
|
4796
5208
|
`, "utf-8");
|
|
4797
|
-
console.log(`✅
|
|
5209
|
+
console.log(`✅ Wrote response to ${outPath}`);
|
|
4798
5210
|
return;
|
|
4799
5211
|
}
|
|
4800
|
-
console.log(
|
|
5212
|
+
console.log(options.json ? pretty(payload) : human);
|
|
4801
5213
|
}
|
|
4802
|
-
async function
|
|
5214
|
+
async function listGroups(options) {
|
|
5215
|
+
const type = parseEntityType(options.type);
|
|
5216
|
+
const url = `${groupsBaseUrl(options.env, options.cmsUrl)}?type=${type}`;
|
|
5217
|
+
const payload = await requestJson("GET", url, resolveSessionToken(options.env));
|
|
5218
|
+
emitRead(payload, formatGroupList(type, options.env, payload.groups ?? []), options);
|
|
5219
|
+
}
|
|
5220
|
+
async function listGroupItems(groupId, options) {
|
|
5221
|
+
const type = parseEntityType(options.type);
|
|
5222
|
+
const url = `${groupsBaseUrl(options.env, options.cmsUrl)}/${encodeURIComponent(groupId)}/items?type=${type}`;
|
|
5223
|
+
const payload = await requestJson("GET", url, resolveSessionToken(options.env));
|
|
5224
|
+
emitRead(payload, formatItemList(type, options.env, payload.group ?? { id: groupId, name: null }, payload.items ?? []), options);
|
|
5225
|
+
}
|
|
5226
|
+
async function writeMembership(mode, groupId, options) {
|
|
5227
|
+
const type = parseEntityType(options.type);
|
|
4803
5228
|
loadEnv(options.env);
|
|
4804
|
-
const
|
|
4805
|
-
const
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
process.exit(1);
|
|
4809
|
-
}
|
|
4810
|
-
const payload = { webappId, details };
|
|
4811
|
-
const CONFIG_URL = getConfigUrl(options, !options.dryRun);
|
|
4812
|
-
if (!options.dryRun)
|
|
4813
|
-
ensureDeployToken2(options.env);
|
|
4814
|
-
console.log(`⚙️ Updating webapp config for [${options.env}] environment...`);
|
|
4815
|
-
if (CONFIG_URL) {
|
|
4816
|
-
console.log(`\uD83D\uDCCD Config URL: ${CONFIG_URL}`);
|
|
4817
|
-
}
|
|
4818
|
-
console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
|
|
4819
|
-
`);
|
|
5229
|
+
const entityId = resolveEntityId(type, options.id);
|
|
5230
|
+
const verb = mode === "assign" ? "Assign" : "Remove";
|
|
5231
|
+
const arrow = mode === "assign" ? "→" : "←";
|
|
5232
|
+
console.log(`${mode === "assign" ? "➕" : "➖"} ${verb} ${type} ${entityId} ${arrow} group ${groupId} on [${options.env}]`);
|
|
4820
5233
|
if (options.dryRun) {
|
|
4821
|
-
console.log(
|
|
4822
|
-
|
|
5234
|
+
console.log(dim(`
|
|
5235
|
+
↷ Dry run — no request sent.`));
|
|
4823
5236
|
return;
|
|
4824
5237
|
}
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
|
|
4835
|
-
|
|
4836
|
-
const errorText = await response.text();
|
|
4837
|
-
throw new Error(`Config update failed: ${response.status} ${response.statusText}
|
|
4838
|
-
${errorText}`);
|
|
5238
|
+
const tty = !!process.stdin.isTTY && !!process.stdout.isTTY;
|
|
5239
|
+
if (!options.yes) {
|
|
5240
|
+
if (!tty) {
|
|
5241
|
+
throw new Error("Refusing to send write in non-interactive mode. Pass --yes to confirm or --dry-run to preview.");
|
|
5242
|
+
}
|
|
5243
|
+
const answer = await prompt(`
|
|
5244
|
+
Proceed with ${mode} on [${options.env}]? (y/N): `);
|
|
5245
|
+
if (!isExplicitYes(answer)) {
|
|
5246
|
+
console.log("✋ Aborted.");
|
|
5247
|
+
return;
|
|
5248
|
+
}
|
|
4839
5249
|
}
|
|
4840
|
-
const
|
|
5250
|
+
const auth = await resolveWriteAuth(options.env, options.cmsUrl);
|
|
5251
|
+
const url = `${auth.baseUrl}/${encodeURIComponent(groupId)}/items`;
|
|
5252
|
+
const method = mode === "assign" ? "POST" : "DELETE";
|
|
5253
|
+
const payload = await requestJson(method, url, auth.token, { type, entityId });
|
|
5254
|
+
if (options.json) {
|
|
5255
|
+
console.log(pretty(payload));
|
|
4841
5256
|
return;
|
|
4842
|
-
});
|
|
4843
|
-
console.log("✅ Webapp config updated");
|
|
4844
|
-
if (result !== undefined) {
|
|
4845
|
-
console.log(`
|
|
4846
|
-
\uD83D\uDCCB Update result:`, result);
|
|
4847
5257
|
}
|
|
5258
|
+
if (mode === "assign") {
|
|
5259
|
+
console.log(payload.alreadyPresent ? "✅ Already in group — no change." : "✅ Added to group.");
|
|
5260
|
+
} else {
|
|
5261
|
+
console.log(payload.removed ? "✅ Removed from group." : "✅ Was not in group — no change.");
|
|
5262
|
+
}
|
|
5263
|
+
}
|
|
5264
|
+
async function assignToGroup(groupId, options) {
|
|
5265
|
+
await writeMembership("assign", groupId, options);
|
|
5266
|
+
}
|
|
5267
|
+
async function removeFromGroup(groupId, options) {
|
|
5268
|
+
await writeMembership("remove", groupId, options);
|
|
4848
5269
|
}
|
|
4849
5270
|
|
|
4850
5271
|
// src/promote.ts
|
|
5272
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "node:fs";
|
|
5273
|
+
import { resolve as resolve6 } from "node:path";
|
|
4851
5274
|
var DEFAULT_SOURCE_ENV = "development";
|
|
4852
|
-
var
|
|
5275
|
+
var PROD_ENV3 = "production";
|
|
4853
5276
|
var PROD_ENV_FILE = ".env.production";
|
|
4854
5277
|
function isObject3(value) {
|
|
4855
5278
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -4876,8 +5299,8 @@ function unwrapSourceDetails(payload) {
|
|
|
4876
5299
|
return payload;
|
|
4877
5300
|
}
|
|
4878
5301
|
function parseDetailsOption(value) {
|
|
4879
|
-
const maybePath =
|
|
4880
|
-
const raw =
|
|
5302
|
+
const maybePath = resolve6(process.cwd(), value);
|
|
5303
|
+
const raw = existsSync9(maybePath) ? readFileSync8(maybePath, "utf-8") : value;
|
|
4881
5304
|
try {
|
|
4882
5305
|
const parsed = JSON.parse(raw);
|
|
4883
5306
|
if (!isObject3(parsed)) {
|
|
@@ -4952,10 +5375,10 @@ Deploy to production now? (y/N): `);
|
|
|
4952
5375
|
}
|
|
4953
5376
|
console.log(`
|
|
4954
5377
|
\uD83D\uDE80 Deploying promoted webapp to production...`);
|
|
4955
|
-
loadEnv(
|
|
5378
|
+
loadEnv(PROD_ENV3, { override: true });
|
|
4956
5379
|
process.env.WEBAPP_ID = webappId;
|
|
4957
5380
|
process.env.DEPLOY_TOKEN = deployToken;
|
|
4958
|
-
await deploy(
|
|
5381
|
+
await deploy(PROD_ENV3, { skipConfigCheck: true });
|
|
4959
5382
|
}
|
|
4960
5383
|
async function promote(options) {
|
|
4961
5384
|
assertDeployOptions(options);
|
|
@@ -4966,9 +5389,9 @@ async function promote(options) {
|
|
|
4966
5389
|
console.info("\uD83D\uDCA1 Run `ro app create --auto` first, or set WEBAPP_ID manually.");
|
|
4967
5390
|
process.exit(1);
|
|
4968
5391
|
}
|
|
4969
|
-
const prodEnvPath =
|
|
4970
|
-
if (
|
|
4971
|
-
const content =
|
|
5392
|
+
const prodEnvPath = resolve6(process.cwd(), PROD_ENV_FILE);
|
|
5393
|
+
if (existsSync9(prodEnvPath)) {
|
|
5394
|
+
const content = readFileSync8(prodEnvPath, "utf-8");
|
|
4972
5395
|
if (/^WEBAPP_ID=.+/m.test(content)) {
|
|
4973
5396
|
console.error(`❌ Error: ${PROD_ENV_FILE} already has WEBAPP_ID. The app appears to be promoted already.`);
|
|
4974
5397
|
process.exit(1);
|
|
@@ -5001,18 +5424,18 @@ async function promote(options) {
|
|
|
5001
5424
|
}
|
|
5002
5425
|
}
|
|
5003
5426
|
console.log(`
|
|
5004
|
-
\uD83D\uDD10 Logging in to ${
|
|
5427
|
+
\uD83D\uDD10 Logging in to ${PROD_ENV3} CMS (ephemeral, not stored)...`);
|
|
5005
5428
|
const session = await login({
|
|
5006
|
-
env:
|
|
5429
|
+
env: PROD_ENV3,
|
|
5007
5430
|
cmsUrl: options.cmsUrl,
|
|
5008
5431
|
persist: false
|
|
5009
5432
|
});
|
|
5010
5433
|
console.log(`✅ Logged in to ${session.cmsUrl}`);
|
|
5011
|
-
const cmsUrl = resolveCmsUrl(
|
|
5434
|
+
const cmsUrl = resolveCmsUrl(PROD_ENV3, options.cmsUrl);
|
|
5012
5435
|
const promoteUrl = options.promoteUrl || `${cmsUrl}/api/cli/webapps/promote`;
|
|
5013
5436
|
const promoteBody = { webappId, details };
|
|
5014
5437
|
console.log(`
|
|
5015
|
-
\uD83D\uDE80 Promoting webapp to ${
|
|
5438
|
+
\uD83D\uDE80 Promoting webapp to ${PROD_ENV3}...`);
|
|
5016
5439
|
console.log(`\uD83D\uDCCD Promote URL: ${promoteUrl}`);
|
|
5017
5440
|
console.log(`\uD83D\uDCCD Webapp ID: ${webappId}`);
|
|
5018
5441
|
console.log("\uD83D\uDCE6 Promote body:");
|
|
@@ -5029,7 +5452,7 @@ async function promote(options) {
|
|
|
5029
5452
|
});
|
|
5030
5453
|
const payload = await readResponsePayload(response);
|
|
5031
5454
|
if (response.status === 409) {
|
|
5032
|
-
console.error(`❌ Webapp ${webappId} is already promoted to ${
|
|
5455
|
+
console.error(`❌ Webapp ${webappId} is already promoted to ${PROD_ENV3}.`);
|
|
5033
5456
|
process.exit(1);
|
|
5034
5457
|
}
|
|
5035
5458
|
if (!response.ok) {
|
|
@@ -5052,7 +5475,7 @@ ${JSON.stringify(payload, null, 2)}`);
|
|
|
5052
5475
|
|
|
5053
5476
|
// src/upgrade-template.ts
|
|
5054
5477
|
import { execSync as execSync3 } from "node:child_process";
|
|
5055
|
-
import { existsSync as
|
|
5478
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync2, copyFileSync, rmSync as rmSync2 } from "node:fs";
|
|
5056
5479
|
import path3 from "node:path";
|
|
5057
5480
|
var TEMPLATES = {
|
|
5058
5481
|
webapp: {
|
|
@@ -5126,12 +5549,12 @@ var FORCE_OVERWRITE_SCRIPT_NAMES = [
|
|
|
5126
5549
|
"upgrade-template"
|
|
5127
5550
|
];
|
|
5128
5551
|
function detectTemplate() {
|
|
5129
|
-
if (
|
|
5552
|
+
if (existsSync10("app")) {
|
|
5130
5553
|
console.log(`\uD83D\uDD0D Detected fullstack template (found app/ directory)
|
|
5131
5554
|
`);
|
|
5132
5555
|
return TEMPLATES["webapp-fullstack"];
|
|
5133
5556
|
}
|
|
5134
|
-
if (
|
|
5557
|
+
if (existsSync10("src")) {
|
|
5135
5558
|
console.log(`\uD83D\uDD0D Detected SPA template (found src/ directory)
|
|
5136
5559
|
`);
|
|
5137
5560
|
return TEMPLATES["webapp"];
|
|
@@ -5186,11 +5609,11 @@ function applyPackageJsonScriptUpdates(packageJson, scriptUpdates) {
|
|
|
5186
5609
|
}
|
|
5187
5610
|
function updatePackageJsonScripts(template) {
|
|
5188
5611
|
const pkgPath = "package.json";
|
|
5189
|
-
if (!
|
|
5612
|
+
if (!existsSync10(pkgPath)) {
|
|
5190
5613
|
console.log("⚠️ No package.json found, skipping scripts update");
|
|
5191
5614
|
return;
|
|
5192
5615
|
}
|
|
5193
|
-
const pkg = JSON.parse(
|
|
5616
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
|
|
5194
5617
|
const templateScripts = readTemplatePackageScripts(template);
|
|
5195
5618
|
const scriptUpdates = getForcedScriptUpdates(templateScripts);
|
|
5196
5619
|
const result = applyPackageJsonScriptUpdates(pkg, scriptUpdates);
|
|
@@ -5198,7 +5621,7 @@ function updatePackageJsonScripts(template) {
|
|
|
5198
5621
|
console.log(` \uD83D\uDCDD ${change.action} script: "${change.name}"`);
|
|
5199
5622
|
}
|
|
5200
5623
|
if (result.updated) {
|
|
5201
|
-
|
|
5624
|
+
writeFileSync7(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
5202
5625
|
`, "utf-8");
|
|
5203
5626
|
console.log(`✅ package.json scripts updated
|
|
5204
5627
|
`);
|
|
@@ -5232,11 +5655,11 @@ function viteHandlesServerScripts(viteConfigSource) {
|
|
|
5232
5655
|
}
|
|
5233
5656
|
function ensureBuildScript() {
|
|
5234
5657
|
const pkgPath = "package.json";
|
|
5235
|
-
if (!
|
|
5658
|
+
if (!existsSync10(pkgPath)) {
|
|
5236
5659
|
console.log(" ⚠️ No package.json found, skipping build-script check");
|
|
5237
5660
|
return;
|
|
5238
5661
|
}
|
|
5239
|
-
const pkg = JSON.parse(
|
|
5662
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
|
|
5240
5663
|
const buildScript = pkg.scripts?.build;
|
|
5241
5664
|
if (typeof buildScript !== "string" || buildScript.trim() === "") {
|
|
5242
5665
|
console.log(` ⚠️ No "build" script found. Add one that runs the server-scripts pass, e.g.:
|
|
@@ -5254,17 +5677,17 @@ function ensureBuildScript() {
|
|
|
5254
5677
|
return;
|
|
5255
5678
|
}
|
|
5256
5679
|
pkg.scripts.build = result.script;
|
|
5257
|
-
|
|
5680
|
+
writeFileSync7(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
5258
5681
|
`, "utf-8");
|
|
5259
5682
|
console.log(` \uD83D\uDCDD Added "vite build --mode server-scripts" to the build script`);
|
|
5260
5683
|
}
|
|
5261
5684
|
function verifyViteServerScriptsMode() {
|
|
5262
|
-
const configPath2 = VITE_CONFIG_CANDIDATES.find((p) =>
|
|
5685
|
+
const configPath2 = VITE_CONFIG_CANDIDATES.find((p) => existsSync10(p));
|
|
5263
5686
|
if (!configPath2) {
|
|
5264
5687
|
console.log(" ⚠️ No vite.config found. Server scripts in src/api and src/cron-jobs won't be compiled.");
|
|
5265
5688
|
return;
|
|
5266
5689
|
}
|
|
5267
|
-
const source =
|
|
5690
|
+
const source = readFileSync9(configPath2, "utf-8");
|
|
5268
5691
|
if (viteHandlesServerScripts(source)) {
|
|
5269
5692
|
console.log(` ✅ ${configPath2} handles the server-scripts build mode`);
|
|
5270
5693
|
return;
|
|
@@ -5289,7 +5712,7 @@ function updateCliSkill() {
|
|
|
5289
5712
|
}
|
|
5290
5713
|
execSync3(`git fetch ${cliRemote}`, { stdio: "inherit" });
|
|
5291
5714
|
execSync3(`git checkout ${cliRemote}/main -- skills/ro-cli/SKILL.md`, { stdio: "inherit" });
|
|
5292
|
-
if (
|
|
5715
|
+
if (existsSync10("skills/ro-cli/SKILL.md")) {
|
|
5293
5716
|
mkdirSync2(".agent/skills/ro-cli", { recursive: true });
|
|
5294
5717
|
copyFileSync("skills/ro-cli/SKILL.md", ".agent/skills/ro-cli/SKILL.md");
|
|
5295
5718
|
rmSync2("skills", { recursive: true, force: true });
|
|
@@ -5322,7 +5745,7 @@ async function upgradeTemplate() {
|
|
|
5322
5745
|
const checkoutList = template.checkoutFiles.join(" ");
|
|
5323
5746
|
execSync3(`git checkout ${template.remoteName}/main -- ${checkoutList}`, { stdio: "inherit" });
|
|
5324
5747
|
for (const file of template.newFiles) {
|
|
5325
|
-
if (!
|
|
5748
|
+
if (!existsSync10(file)) {
|
|
5326
5749
|
console.log(`\uD83D\uDCC2 Checking out ${file}...`);
|
|
5327
5750
|
try {
|
|
5328
5751
|
mkdirSync2(path3.dirname(file), { recursive: true });
|
|
@@ -5487,6 +5910,156 @@ async function updateGameSdk() {
|
|
|
5487
5910
|
}
|
|
5488
5911
|
}
|
|
5489
5912
|
|
|
5913
|
+
// src/assets.ts
|
|
5914
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
|
|
5915
|
+
import { basename as basename2, join as join4, relative } from "node:path";
|
|
5916
|
+
var ASSETS_URLS = {
|
|
5917
|
+
local: "http://localhost:5176/api/webapps/assets",
|
|
5918
|
+
development: "https://development-cms.rodyssey.ai/api/webapps/assets",
|
|
5919
|
+
staging: "https://staging-cms.rodyssey.ai/api/webapps/assets",
|
|
5920
|
+
production: "https://cms.rodyssey.ai/api/webapps/assets"
|
|
5921
|
+
};
|
|
5922
|
+
var MAX_FILES_PER_BATCH2 = 5;
|
|
5923
|
+
var MAX_SIZE_PER_BATCH2 = 30 * 1024 * 1024;
|
|
5924
|
+
function resolveAssetsUrl(options) {
|
|
5925
|
+
const rawUrl = options.url || process.env.WEBAPP_ASSETS_URL || ASSETS_URLS[options.env];
|
|
5926
|
+
if (!rawUrl) {
|
|
5927
|
+
throw new Error(`Unknown environment "${options.env}". Use one of: ${Object.keys(ASSETS_URLS).join(", ")}, pass --url, or set WEBAPP_ASSETS_URL.`);
|
|
5928
|
+
}
|
|
5929
|
+
const url = new URL(rawUrl);
|
|
5930
|
+
if (options.host)
|
|
5931
|
+
url.hostname = options.host;
|
|
5932
|
+
if (options.port)
|
|
5933
|
+
url.port = String(options.port);
|
|
5934
|
+
return url.toString().replace(/\/$/, "");
|
|
5935
|
+
}
|
|
5936
|
+
function normalizeRemotePath(raw) {
|
|
5937
|
+
let p = raw.replaceAll("\\", "/");
|
|
5938
|
+
while (p.startsWith("./"))
|
|
5939
|
+
p = p.slice(2);
|
|
5940
|
+
while (p.startsWith("/"))
|
|
5941
|
+
p = p.slice(1);
|
|
5942
|
+
const segments = p.split("/").filter((s) => s.length > 0);
|
|
5943
|
+
if (segments.length === 0 || segments.includes("..") || segments.includes(".")) {
|
|
5944
|
+
throw new Error(`Invalid asset path: "${raw}"`);
|
|
5945
|
+
}
|
|
5946
|
+
return segments.join("/");
|
|
5947
|
+
}
|
|
5948
|
+
function refuseManifest(remotePath) {
|
|
5949
|
+
if (basename2(remotePath) === "widgets.manifest.json") {
|
|
5950
|
+
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.");
|
|
5951
|
+
}
|
|
5952
|
+
}
|
|
5953
|
+
function walkDir(dirPath) {
|
|
5954
|
+
const out = [];
|
|
5955
|
+
for (const name of readdirSync2(dirPath)) {
|
|
5956
|
+
const full = join4(dirPath, name);
|
|
5957
|
+
if (statSync2(full).isDirectory())
|
|
5958
|
+
out.push(...walkDir(full));
|
|
5959
|
+
else
|
|
5960
|
+
out.push(full);
|
|
5961
|
+
}
|
|
5962
|
+
return out;
|
|
5963
|
+
}
|
|
5964
|
+
function collectUploads(inputs, dest) {
|
|
5965
|
+
const prefix = dest ? `${normalizeRemotePath(dest)}/` : "";
|
|
5966
|
+
const entries = [];
|
|
5967
|
+
for (const input of inputs) {
|
|
5968
|
+
if (!existsSync11(input)) {
|
|
5969
|
+
throw new Error(`Path not found: ${input}`);
|
|
5970
|
+
}
|
|
5971
|
+
if (statSync2(input).isDirectory()) {
|
|
5972
|
+
for (const file of walkDir(input)) {
|
|
5973
|
+
const remotePath = normalizeRemotePath(`${prefix}${relative(input, file)}`);
|
|
5974
|
+
refuseManifest(remotePath);
|
|
5975
|
+
entries.push({ localPath: file, remotePath });
|
|
5976
|
+
}
|
|
5977
|
+
} else {
|
|
5978
|
+
const remotePath = normalizeRemotePath(`${prefix}${basename2(input)}`);
|
|
5979
|
+
refuseManifest(remotePath);
|
|
5980
|
+
entries.push({ localPath: input, remotePath });
|
|
5981
|
+
}
|
|
5982
|
+
}
|
|
5983
|
+
if (entries.length === 0) {
|
|
5984
|
+
throw new Error("No files to upload.");
|
|
5985
|
+
}
|
|
5986
|
+
return entries;
|
|
5987
|
+
}
|
|
5988
|
+
function splitBatches(entries, sizeOf) {
|
|
5989
|
+
const batches = [];
|
|
5990
|
+
let current = [];
|
|
5991
|
+
let currentSize = 0;
|
|
5992
|
+
for (const entry of entries) {
|
|
5993
|
+
const size = sizeOf(entry);
|
|
5994
|
+
if (current.length >= MAX_FILES_PER_BATCH2 || current.length > 0 && currentSize + size > MAX_SIZE_PER_BATCH2) {
|
|
5995
|
+
batches.push(current);
|
|
5996
|
+
current = [];
|
|
5997
|
+
currentSize = 0;
|
|
5998
|
+
}
|
|
5999
|
+
current.push(entry);
|
|
6000
|
+
currentSize += size;
|
|
6001
|
+
}
|
|
6002
|
+
if (current.length > 0)
|
|
6003
|
+
batches.push(current);
|
|
6004
|
+
return batches;
|
|
6005
|
+
}
|
|
6006
|
+
function ensureDeployToken3(env) {
|
|
6007
|
+
if (process.env.DEPLOY_TOKEN)
|
|
6008
|
+
return process.env.DEPLOY_TOKEN;
|
|
6009
|
+
throw new Error(`DEPLOY_TOKEN is not set. Please check your .env or .env.${env} file.`);
|
|
6010
|
+
}
|
|
6011
|
+
function fileToBlob2(filePath) {
|
|
6012
|
+
return new Blob([readFileSync10(filePath)]);
|
|
6013
|
+
}
|
|
6014
|
+
async function pushAssets(inputs, options) {
|
|
6015
|
+
loadEnv(options.env);
|
|
6016
|
+
const entries = collectUploads(inputs, options.dest);
|
|
6017
|
+
const batches = splitBatches(entries, (e) => statSync2(e.localPath).size);
|
|
6018
|
+
const url = resolveAssetsUrl(options);
|
|
6019
|
+
console.log(`\uD83D\uDCE4 Pushing ${entries.length} asset(s) to [${options.env}] in ${batches.length} batch(es):`);
|
|
6020
|
+
for (const e of entries) {
|
|
6021
|
+
console.log(` ${e.localPath} → ${e.remotePath}`);
|
|
6022
|
+
}
|
|
6023
|
+
if (options.dryRun) {
|
|
6024
|
+
console.log(dim(`
|
|
6025
|
+
↷ Dry run — no request sent.`));
|
|
6026
|
+
return;
|
|
6027
|
+
}
|
|
6028
|
+
const token = ensureDeployToken3(options.env);
|
|
6029
|
+
const uploaded = [];
|
|
6030
|
+
for (const [index, batch] of batches.entries()) {
|
|
6031
|
+
const formData = new FormData;
|
|
6032
|
+
for (const entry of batch) {
|
|
6033
|
+
formData.append(entry.remotePath, fileToBlob2(entry.localPath), entry.remotePath);
|
|
6034
|
+
}
|
|
6035
|
+
const response = await fetch(url, {
|
|
6036
|
+
method: "POST",
|
|
6037
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
6038
|
+
body: formData
|
|
6039
|
+
});
|
|
6040
|
+
if (!response.ok) {
|
|
6041
|
+
const errorText = await response.text();
|
|
6042
|
+
throw new Error(`Asset upload failed (batch ${index + 1}/${batches.length}): ${response.status} ${response.statusText}
|
|
6043
|
+
${errorText}`);
|
|
6044
|
+
}
|
|
6045
|
+
const payload = await response.json();
|
|
6046
|
+
uploaded.push(...payload.assets ?? []);
|
|
6047
|
+
console.log(`✅ Batch ${index + 1}/${batches.length} uploaded`);
|
|
6048
|
+
}
|
|
6049
|
+
if (options.json) {
|
|
6050
|
+
console.log(pretty({ success: true, assets: uploaded }));
|
|
6051
|
+
return;
|
|
6052
|
+
}
|
|
6053
|
+
console.log(`
|
|
6054
|
+
✅ Uploaded ${uploaded.length} asset(s):`);
|
|
6055
|
+
for (const asset of uploaded) {
|
|
6056
|
+
console.log(`• ${asset.path}`);
|
|
6057
|
+
console.log(` ${asset.url}`);
|
|
6058
|
+
}
|
|
6059
|
+
console.log(dim("\nTip: set a cover image with `ro app config set --cover-img <url>`,"));
|
|
6060
|
+
console.log(dim("or put the URL in webapp.config.json and run `ro app config push`."));
|
|
6061
|
+
}
|
|
6062
|
+
|
|
5490
6063
|
// src/cli.ts
|
|
5491
6064
|
function renderError(err) {
|
|
5492
6065
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -5527,15 +6100,15 @@ Available templates:
|
|
|
5527
6100
|
input: process.stdin,
|
|
5528
6101
|
output: process.stdout
|
|
5529
6102
|
});
|
|
5530
|
-
return new Promise((
|
|
6103
|
+
return new Promise((resolve7) => {
|
|
5531
6104
|
rl.question(`Select a template (1-${entries.length}): `, (answer) => {
|
|
5532
6105
|
rl.close();
|
|
5533
6106
|
const index = parseInt(answer, 10) - 1;
|
|
5534
6107
|
if (index >= 0 && index < entries.length) {
|
|
5535
|
-
|
|
6108
|
+
resolve7(entries[index].name);
|
|
5536
6109
|
} else {
|
|
5537
6110
|
console.error("Invalid selection, defaulting to 'webapp'");
|
|
5538
|
-
|
|
6111
|
+
resolve7("webapp");
|
|
5539
6112
|
}
|
|
5540
6113
|
});
|
|
5541
6114
|
});
|
|
@@ -5580,6 +6153,20 @@ addGlobalConfigWriteOptions(globalConfig.command("set").description("Replace the
|
|
|
5580
6153
|
addGlobalConfigWriteOptions(globalConfig.command("patch").description("Merge-patch (RFC 7396) the `config` and/or `publicConfig` column. `null` values delete keys.")).action(async (options) => {
|
|
5581
6154
|
await patchGlobalConfig(options);
|
|
5582
6155
|
});
|
|
6156
|
+
var group = program.command("group").description("Manage entity groups (webapp | character | scene | story)");
|
|
6157
|
+
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");
|
|
6158
|
+
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) => {
|
|
6159
|
+
await listGroups(options);
|
|
6160
|
+
});
|
|
6161
|
+
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) => {
|
|
6162
|
+
await listGroupItems(groupId, options);
|
|
6163
|
+
});
|
|
6164
|
+
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) => {
|
|
6165
|
+
await assignToGroup(groupId, options);
|
|
6166
|
+
});
|
|
6167
|
+
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) => {
|
|
6168
|
+
await removeFromGroup(groupId, options);
|
|
6169
|
+
});
|
|
5583
6170
|
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) => {
|
|
5584
6171
|
let templateName;
|
|
5585
6172
|
if (options.template) {
|
|
@@ -5613,8 +6200,8 @@ app.command("promote").description("Promote the current webapp to production (cr
|
|
|
5613
6200
|
app.command("update-game-sdk").description("Download and update the GameSDK library, types, and documentation").action(async () => {
|
|
5614
6201
|
await updateGameSdk();
|
|
5615
6202
|
});
|
|
5616
|
-
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) => {
|
|
5617
|
-
await deploy(options.env, { host: options.host, port: options.port });
|
|
6203
|
+
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) => {
|
|
6204
|
+
await deploy(options.env, { host: options.host, port: options.port, pushConfig: options.pushConfig });
|
|
5618
6205
|
});
|
|
5619
6206
|
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) => {
|
|
5620
6207
|
await syncWidgetManifest(options);
|
|
@@ -5626,6 +6213,16 @@ addConfigTargetOptions(config.command("get").description("Pull the current webap
|
|
|
5626
6213
|
addConfigSetOptions(config.command("set").description("Update webapp metadata such as title, cover image, description, and localization")).action(async (options) => {
|
|
5627
6214
|
await updateWebappConfig(options);
|
|
5628
6215
|
});
|
|
6216
|
+
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) => {
|
|
6217
|
+
await pullWebappConfig(options);
|
|
6218
|
+
});
|
|
6219
|
+
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) => {
|
|
6220
|
+
await pushWebappConfig(options);
|
|
6221
|
+
});
|
|
6222
|
+
var assets = app.command("assets").description("Manage webapp assets (R2-hosted files)");
|
|
6223
|
+
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) => {
|
|
6224
|
+
await pushAssets(paths, options);
|
|
6225
|
+
});
|
|
5629
6226
|
app.command("upgrade-template").description("Upgrade template files and CLI scripts from the template repository").action(async () => {
|
|
5630
6227
|
await upgradeTemplate();
|
|
5631
6228
|
});
|