@rodyssey/cli 0.4.1 → 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 +898 -307
- 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)
|
|
@@ -3880,34 +4390,34 @@ function resolveWidgetConfigUrl(options) {
|
|
|
3880
4390
|
}
|
|
3881
4391
|
function resolveManifestPath(manifest) {
|
|
3882
4392
|
if (manifest) {
|
|
3883
|
-
const manifestPath =
|
|
3884
|
-
if (!
|
|
4393
|
+
const manifestPath = resolve3(process.cwd(), manifest);
|
|
4394
|
+
if (!existsSync6(manifestPath)) {
|
|
3885
4395
|
throw new Error(`Widget manifest not found: ${manifestPath}`);
|
|
3886
4396
|
}
|
|
3887
4397
|
return manifestPath;
|
|
3888
4398
|
}
|
|
3889
4399
|
const candidates = [
|
|
3890
|
-
|
|
3891
|
-
|
|
4400
|
+
resolve3(process.cwd(), "build/client/widgets.manifest.json"),
|
|
4401
|
+
resolve3(process.cwd(), "dist/widgets.manifest.json")
|
|
3892
4402
|
];
|
|
3893
|
-
const found = candidates.find((candidate) =>
|
|
4403
|
+
const found = candidates.find((candidate) => existsSync6(candidate));
|
|
3894
4404
|
return found;
|
|
3895
4405
|
}
|
|
3896
4406
|
function readManifest(path3) {
|
|
3897
|
-
const parsed = JSON.parse(
|
|
4407
|
+
const parsed = JSON.parse(readFileSync5(path3, "utf-8"));
|
|
3898
4408
|
if (!Array.isArray(parsed)) {
|
|
3899
4409
|
throw new Error(`Widget manifest must be a JSON array: ${path3}`);
|
|
3900
4410
|
}
|
|
3901
4411
|
return parsed;
|
|
3902
4412
|
}
|
|
3903
|
-
function
|
|
4413
|
+
function resolveWebappId2(webappId) {
|
|
3904
4414
|
const resolved = webappId || process.env.WEBAPP_ID;
|
|
3905
4415
|
if (!resolved) {
|
|
3906
4416
|
throw new Error("WEBAPP_ID is not set. Add it to .env or pass --webapp-id.");
|
|
3907
4417
|
}
|
|
3908
4418
|
return resolved;
|
|
3909
4419
|
}
|
|
3910
|
-
function
|
|
4420
|
+
function ensureDeployToken2(env) {
|
|
3911
4421
|
if (process.env.DEPLOY_TOKEN)
|
|
3912
4422
|
return;
|
|
3913
4423
|
throw new Error(`DEPLOY_TOKEN is not set. Please check your .env or .env.${env} file.`);
|
|
@@ -3919,9 +4429,9 @@ async function syncWidgetManifest(options) {
|
|
|
3919
4429
|
console.log("No widget manifest found; skipping widget manifest sync.");
|
|
3920
4430
|
return;
|
|
3921
4431
|
}
|
|
3922
|
-
const webappId =
|
|
4432
|
+
const webappId = resolveWebappId2(options.webappId);
|
|
3923
4433
|
if (!options.dryRun)
|
|
3924
|
-
|
|
4434
|
+
ensureDeployToken2(options.env);
|
|
3925
4435
|
const manifest = readManifest(manifestPath);
|
|
3926
4436
|
const payload = { webappId, details: { widgetManifest: manifest } };
|
|
3927
4437
|
const configUrl = resolveWidgetConfigUrl(options);
|
|
@@ -3980,7 +4490,7 @@ function pickNumber(value) {
|
|
|
3980
4490
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
3981
4491
|
}
|
|
3982
4492
|
function isFullstackProject() {
|
|
3983
|
-
return
|
|
4493
|
+
return existsSync7("app") && existsSync7("workers/app.ts") && existsSync7("wrangler.jsonc");
|
|
3984
4494
|
}
|
|
3985
4495
|
function resolveDeployUrl(env, overrides) {
|
|
3986
4496
|
let deployUrl = DEPLOY_URLS[env];
|
|
@@ -4056,7 +4566,7 @@ function collectScripts(scriptFiles) {
|
|
|
4056
4566
|
const payload = { api: {}, cron: {}, cronConfig: null };
|
|
4057
4567
|
const summary = { apiEndpoints: [], cronJobs: [], mcpEndpoints: [] };
|
|
4058
4568
|
for (const filePath of scriptFiles) {
|
|
4059
|
-
const content =
|
|
4569
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
4060
4570
|
const relativePath = normalizeBuildRelativePath(filePath);
|
|
4061
4571
|
if (relativePath === "cron-jobs/cron.config.json") {
|
|
4062
4572
|
payload.cronConfig = readCronConfig(filePath, content);
|
|
@@ -4172,7 +4682,7 @@ ${JSON.stringify(result, null, 2)}`);
|
|
|
4172
4682
|
return { summary, result };
|
|
4173
4683
|
}
|
|
4174
4684
|
function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
4175
|
-
if (!
|
|
4685
|
+
if (!existsSync7(dirPath))
|
|
4176
4686
|
return arrayOfFiles;
|
|
4177
4687
|
const files = readdirSync(dirPath);
|
|
4178
4688
|
files.forEach(function(f) {
|
|
@@ -4186,7 +4696,7 @@ function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
|
4186
4696
|
return arrayOfFiles;
|
|
4187
4697
|
}
|
|
4188
4698
|
function fileToBlob(filePath) {
|
|
4189
|
-
const buffer =
|
|
4699
|
+
const buffer = readFileSync6(filePath);
|
|
4190
4700
|
return new Blob([buffer], { type: src_default.getType(filePath) || "application/octet-stream" });
|
|
4191
4701
|
}
|
|
4192
4702
|
async function deployFullstack(env, overrides) {
|
|
@@ -4310,7 +4820,7 @@ ${errorText}`);
|
|
|
4310
4820
|
console.log(`✅ Created ${ZIP_FILE}
|
|
4311
4821
|
`);
|
|
4312
4822
|
console.log("☁️ Step 4: Deploying HTML zip to server...");
|
|
4313
|
-
const zipBuffer =
|
|
4823
|
+
const zipBuffer = readFileSync6(ZIP_FILE);
|
|
4314
4824
|
try {
|
|
4315
4825
|
const response = await fetch(DEPLOY_URL, {
|
|
4316
4826
|
method: "POST",
|
|
@@ -4340,7 +4850,7 @@ ${errorText}`);
|
|
|
4340
4850
|
console.error("❌ Deploy failed:", error);
|
|
4341
4851
|
throw error;
|
|
4342
4852
|
} finally {
|
|
4343
|
-
if (
|
|
4853
|
+
if (existsSync7(ZIP_FILE)) {
|
|
4344
4854
|
unlinkSync(ZIP_FILE);
|
|
4345
4855
|
console.log(`
|
|
4346
4856
|
\uD83E\uDDF9 Cleaned up ${ZIP_FILE}`);
|
|
@@ -4353,18 +4863,23 @@ async function deploy(env = "development", overrides = {}) {
|
|
|
4353
4863
|
loadEnv(env);
|
|
4354
4864
|
if (isFullstackProject()) {
|
|
4355
4865
|
await deployFullstack(env, overrides);
|
|
4356
|
-
|
|
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
|
+
});
|
|
4357
4876
|
}
|
|
4358
|
-
await deploySpa(env, overrides);
|
|
4359
4877
|
}
|
|
4360
4878
|
|
|
4361
4879
|
// src/global-config.ts
|
|
4362
|
-
import { existsSync as
|
|
4363
|
-
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";
|
|
4364
4882
|
var PROD_ENV = "production";
|
|
4365
|
-
function isPlainObject(value) {
|
|
4366
|
-
return !!value && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
4367
|
-
}
|
|
4368
4883
|
function applyMergePatch(target, patch) {
|
|
4369
4884
|
if (!isPlainObject(patch))
|
|
4370
4885
|
return patch;
|
|
@@ -4381,8 +4896,8 @@ function applyMergePatch(target, patch) {
|
|
|
4381
4896
|
function parseFlag(name, value) {
|
|
4382
4897
|
if (value === "null")
|
|
4383
4898
|
return null;
|
|
4384
|
-
const candidatePath =
|
|
4385
|
-
const raw =
|
|
4899
|
+
const candidatePath = resolve4(process.cwd(), value);
|
|
4900
|
+
const raw = existsSync8(candidatePath) ? readFileSync7(candidatePath, "utf-8") : value;
|
|
4386
4901
|
try {
|
|
4387
4902
|
return JSON.parse(raw);
|
|
4388
4903
|
} catch (error) {
|
|
@@ -4398,95 +4913,20 @@ function buildPayload(options) {
|
|
|
4398
4913
|
if (options.publicConfig !== undefined) {
|
|
4399
4914
|
payload.publicConfig = parseFlag("public-config", options.publicConfig);
|
|
4400
4915
|
}
|
|
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}`));
|
|
4916
|
+
if (payload.config === undefined && payload.publicConfig === undefined) {
|
|
4917
|
+
throw new Error("Provide at least one of --config or --public-config.");
|
|
4487
4918
|
}
|
|
4488
|
-
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`;
|
|
4489
4925
|
}
|
|
4926
|
+
var COLUMN_LABEL = {
|
|
4927
|
+
config: "Config",
|
|
4928
|
+
publicConfig: "Public Config"
|
|
4929
|
+
};
|
|
4490
4930
|
function buildColumnDeltas(current, payload, method) {
|
|
4491
4931
|
const out = [];
|
|
4492
4932
|
for (const column of ["config", "publicConfig"]) {
|
|
@@ -4506,7 +4946,7 @@ function buildColumnDeltas(current, payload, method) {
|
|
|
4506
4946
|
}
|
|
4507
4947
|
return out;
|
|
4508
4948
|
}
|
|
4509
|
-
function
|
|
4949
|
+
function deltaLine2(d, showColumnTag) {
|
|
4510
4950
|
const tag = showColumnTag ? `[${COLUMN_LABEL[d.column]}] ` : "";
|
|
4511
4951
|
const path3 = pathToDot(d.path) || `(entire ${COLUMN_LABEL[d.column]})`;
|
|
4512
4952
|
if (d.kind === "add") {
|
|
@@ -4530,15 +4970,15 @@ ${dim(" (no changes)")}`;
|
|
|
4530
4970
|
const removes = deltas.filter((d) => d.kind === "remove");
|
|
4531
4971
|
const sections = [];
|
|
4532
4972
|
if (adds.length > 0) {
|
|
4533
|
-
sections.push([green("New"), ...adds.map((d) =>
|
|
4973
|
+
sections.push([green("New"), ...adds.map((d) => deltaLine2(d, showTag))].join(`
|
|
4534
4974
|
`));
|
|
4535
4975
|
}
|
|
4536
4976
|
if (changes.length > 0) {
|
|
4537
|
-
sections.push([yellow("Update"), ...changes.map((d) =>
|
|
4977
|
+
sections.push([yellow("Update"), ...changes.map((d) => deltaLine2(d, showTag))].join(`
|
|
4538
4978
|
`));
|
|
4539
4979
|
}
|
|
4540
4980
|
if (removes.length > 0) {
|
|
4541
|
-
sections.push([red("Delete"), ...removes.map((d) =>
|
|
4981
|
+
sections.push([red("Delete"), ...removes.map((d) => deltaLine2(d, showTag))].join(`
|
|
4542
4982
|
`));
|
|
4543
4983
|
}
|
|
4544
4984
|
return `${header}
|
|
@@ -4577,8 +5017,8 @@ ${pretty(payload)}`);
|
|
|
4577
5017
|
}
|
|
4578
5018
|
const text = pretty(payload);
|
|
4579
5019
|
if (options.out) {
|
|
4580
|
-
const outPath =
|
|
4581
|
-
|
|
5020
|
+
const outPath = resolve4(process.cwd(), options.out);
|
|
5021
|
+
writeFileSync5(outPath, `${text}
|
|
4582
5022
|
`, "utf-8");
|
|
4583
5023
|
console.log(`✅ Wrote global config to ${outPath}`);
|
|
4584
5024
|
} else {
|
|
@@ -4672,190 +5112,167 @@ async function patchGlobalConfig(options) {
|
|
|
4672
5112
|
await writeGlobalConfig("PATCH", options);
|
|
4673
5113
|
}
|
|
4674
5114
|
|
|
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}`);
|
|
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;
|
|
4700
5123
|
}
|
|
5124
|
+
throw new Error(`--type must be one of: ${ENTITY_TYPES.join(", ")}`);
|
|
4701
5125
|
}
|
|
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);
|
|
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)");
|
|
4717
5133
|
}
|
|
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(/\/$/, "");
|
|
5134
|
+
return missingId(`(no default exists for --type ${type})`);
|
|
4727
5135
|
}
|
|
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;
|
|
5136
|
+
function missingId(detail) {
|
|
5137
|
+
throw new Error(`--id is required ${detail}`);
|
|
4743
5138
|
}
|
|
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;
|
|
5139
|
+
function orFallback(value, fallback) {
|
|
5140
|
+
return value && value.trim().length > 0 ? value : fallback;
|
|
4751
5141
|
}
|
|
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);
|
|
5142
|
+
function capitalize(s) {
|
|
5143
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
4758
5144
|
}
|
|
4759
|
-
function
|
|
4760
|
-
|
|
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
|
+
`);
|
|
4761
5160
|
}
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
const
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
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}`);
|
|
4768
5170
|
}
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
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) {
|
|
4772
5188
|
const response = await fetch(url, {
|
|
4773
|
-
method
|
|
5189
|
+
method,
|
|
4774
5190
|
headers: {
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
5191
|
+
Accept: "application/json",
|
|
5192
|
+
Authorization: `Bearer ${token}`,
|
|
5193
|
+
...body !== undefined ? { "Content-Type": "application/json" } : {}
|
|
5194
|
+
},
|
|
5195
|
+
...body !== undefined ? { body: JSON.stringify(body) } : {}
|
|
4778
5196
|
});
|
|
5197
|
+
const payload = await readResponsePayload(response);
|
|
4779
5198
|
if (!response.ok) {
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
${errorText}`);
|
|
5199
|
+
throw new Error(`${method} ${url} failed: ${response.status} ${response.statusText}
|
|
5200
|
+
${pretty(payload)}`);
|
|
4783
5201
|
}
|
|
4784
|
-
return
|
|
5202
|
+
return payload;
|
|
4785
5203
|
}
|
|
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);
|
|
5204
|
+
function emitRead(payload, human, options) {
|
|
4800
5205
|
if (options.out) {
|
|
4801
|
-
|
|
5206
|
+
const outPath = resolve5(process.cwd(), options.out);
|
|
5207
|
+
writeFileSync6(outPath, `${pretty(payload)}
|
|
4802
5208
|
`, "utf-8");
|
|
4803
|
-
console.log(`✅
|
|
5209
|
+
console.log(`✅ Wrote response to ${outPath}`);
|
|
4804
5210
|
return;
|
|
4805
5211
|
}
|
|
4806
|
-
console.log(
|
|
5212
|
+
console.log(options.json ? pretty(payload) : human);
|
|
4807
5213
|
}
|
|
4808
|
-
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);
|
|
4809
5228
|
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
|
-
`);
|
|
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}]`);
|
|
4826
5233
|
if (options.dryRun) {
|
|
4827
|
-
console.log(
|
|
4828
|
-
|
|
5234
|
+
console.log(dim(`
|
|
5235
|
+
↷ Dry run — no request sent.`));
|
|
4829
5236
|
return;
|
|
4830
5237
|
}
|
|
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}`);
|
|
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
|
+
}
|
|
4845
5249
|
}
|
|
4846
|
-
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));
|
|
4847
5256
|
return;
|
|
4848
|
-
});
|
|
4849
|
-
console.log("✅ Webapp config updated");
|
|
4850
|
-
if (result !== undefined) {
|
|
4851
|
-
console.log(`
|
|
4852
|
-
\uD83D\uDCCB Update result:`, result);
|
|
4853
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);
|
|
4854
5269
|
}
|
|
4855
5270
|
|
|
4856
5271
|
// src/promote.ts
|
|
5272
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8 } from "node:fs";
|
|
5273
|
+
import { resolve as resolve6 } from "node:path";
|
|
4857
5274
|
var DEFAULT_SOURCE_ENV = "development";
|
|
4858
|
-
var
|
|
5275
|
+
var PROD_ENV3 = "production";
|
|
4859
5276
|
var PROD_ENV_FILE = ".env.production";
|
|
4860
5277
|
function isObject3(value) {
|
|
4861
5278
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -4882,8 +5299,8 @@ function unwrapSourceDetails(payload) {
|
|
|
4882
5299
|
return payload;
|
|
4883
5300
|
}
|
|
4884
5301
|
function parseDetailsOption(value) {
|
|
4885
|
-
const maybePath =
|
|
4886
|
-
const raw =
|
|
5302
|
+
const maybePath = resolve6(process.cwd(), value);
|
|
5303
|
+
const raw = existsSync9(maybePath) ? readFileSync8(maybePath, "utf-8") : value;
|
|
4887
5304
|
try {
|
|
4888
5305
|
const parsed = JSON.parse(raw);
|
|
4889
5306
|
if (!isObject3(parsed)) {
|
|
@@ -4958,10 +5375,10 @@ Deploy to production now? (y/N): `);
|
|
|
4958
5375
|
}
|
|
4959
5376
|
console.log(`
|
|
4960
5377
|
\uD83D\uDE80 Deploying promoted webapp to production...`);
|
|
4961
|
-
loadEnv(
|
|
5378
|
+
loadEnv(PROD_ENV3, { override: true });
|
|
4962
5379
|
process.env.WEBAPP_ID = webappId;
|
|
4963
5380
|
process.env.DEPLOY_TOKEN = deployToken;
|
|
4964
|
-
await deploy(
|
|
5381
|
+
await deploy(PROD_ENV3, { skipConfigCheck: true });
|
|
4965
5382
|
}
|
|
4966
5383
|
async function promote(options) {
|
|
4967
5384
|
assertDeployOptions(options);
|
|
@@ -4972,9 +5389,9 @@ async function promote(options) {
|
|
|
4972
5389
|
console.info("\uD83D\uDCA1 Run `ro app create --auto` first, or set WEBAPP_ID manually.");
|
|
4973
5390
|
process.exit(1);
|
|
4974
5391
|
}
|
|
4975
|
-
const prodEnvPath =
|
|
4976
|
-
if (
|
|
4977
|
-
const content =
|
|
5392
|
+
const prodEnvPath = resolve6(process.cwd(), PROD_ENV_FILE);
|
|
5393
|
+
if (existsSync9(prodEnvPath)) {
|
|
5394
|
+
const content = readFileSync8(prodEnvPath, "utf-8");
|
|
4978
5395
|
if (/^WEBAPP_ID=.+/m.test(content)) {
|
|
4979
5396
|
console.error(`❌ Error: ${PROD_ENV_FILE} already has WEBAPP_ID. The app appears to be promoted already.`);
|
|
4980
5397
|
process.exit(1);
|
|
@@ -5007,18 +5424,18 @@ async function promote(options) {
|
|
|
5007
5424
|
}
|
|
5008
5425
|
}
|
|
5009
5426
|
console.log(`
|
|
5010
|
-
\uD83D\uDD10 Logging in to ${
|
|
5427
|
+
\uD83D\uDD10 Logging in to ${PROD_ENV3} CMS (ephemeral, not stored)...`);
|
|
5011
5428
|
const session = await login({
|
|
5012
|
-
env:
|
|
5429
|
+
env: PROD_ENV3,
|
|
5013
5430
|
cmsUrl: options.cmsUrl,
|
|
5014
5431
|
persist: false
|
|
5015
5432
|
});
|
|
5016
5433
|
console.log(`✅ Logged in to ${session.cmsUrl}`);
|
|
5017
|
-
const cmsUrl = resolveCmsUrl(
|
|
5434
|
+
const cmsUrl = resolveCmsUrl(PROD_ENV3, options.cmsUrl);
|
|
5018
5435
|
const promoteUrl = options.promoteUrl || `${cmsUrl}/api/cli/webapps/promote`;
|
|
5019
5436
|
const promoteBody = { webappId, details };
|
|
5020
5437
|
console.log(`
|
|
5021
|
-
\uD83D\uDE80 Promoting webapp to ${
|
|
5438
|
+
\uD83D\uDE80 Promoting webapp to ${PROD_ENV3}...`);
|
|
5022
5439
|
console.log(`\uD83D\uDCCD Promote URL: ${promoteUrl}`);
|
|
5023
5440
|
console.log(`\uD83D\uDCCD Webapp ID: ${webappId}`);
|
|
5024
5441
|
console.log("\uD83D\uDCE6 Promote body:");
|
|
@@ -5035,7 +5452,7 @@ async function promote(options) {
|
|
|
5035
5452
|
});
|
|
5036
5453
|
const payload = await readResponsePayload(response);
|
|
5037
5454
|
if (response.status === 409) {
|
|
5038
|
-
console.error(`❌ Webapp ${webappId} is already promoted to ${
|
|
5455
|
+
console.error(`❌ Webapp ${webappId} is already promoted to ${PROD_ENV3}.`);
|
|
5039
5456
|
process.exit(1);
|
|
5040
5457
|
}
|
|
5041
5458
|
if (!response.ok) {
|
|
@@ -5058,7 +5475,7 @@ ${JSON.stringify(payload, null, 2)}`);
|
|
|
5058
5475
|
|
|
5059
5476
|
// src/upgrade-template.ts
|
|
5060
5477
|
import { execSync as execSync3 } from "node:child_process";
|
|
5061
|
-
import { existsSync as
|
|
5478
|
+
import { existsSync as existsSync10, readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync2, copyFileSync, rmSync as rmSync2 } from "node:fs";
|
|
5062
5479
|
import path3 from "node:path";
|
|
5063
5480
|
var TEMPLATES = {
|
|
5064
5481
|
webapp: {
|
|
@@ -5132,12 +5549,12 @@ var FORCE_OVERWRITE_SCRIPT_NAMES = [
|
|
|
5132
5549
|
"upgrade-template"
|
|
5133
5550
|
];
|
|
5134
5551
|
function detectTemplate() {
|
|
5135
|
-
if (
|
|
5552
|
+
if (existsSync10("app")) {
|
|
5136
5553
|
console.log(`\uD83D\uDD0D Detected fullstack template (found app/ directory)
|
|
5137
5554
|
`);
|
|
5138
5555
|
return TEMPLATES["webapp-fullstack"];
|
|
5139
5556
|
}
|
|
5140
|
-
if (
|
|
5557
|
+
if (existsSync10("src")) {
|
|
5141
5558
|
console.log(`\uD83D\uDD0D Detected SPA template (found src/ directory)
|
|
5142
5559
|
`);
|
|
5143
5560
|
return TEMPLATES["webapp"];
|
|
@@ -5192,11 +5609,11 @@ function applyPackageJsonScriptUpdates(packageJson, scriptUpdates) {
|
|
|
5192
5609
|
}
|
|
5193
5610
|
function updatePackageJsonScripts(template) {
|
|
5194
5611
|
const pkgPath = "package.json";
|
|
5195
|
-
if (!
|
|
5612
|
+
if (!existsSync10(pkgPath)) {
|
|
5196
5613
|
console.log("⚠️ No package.json found, skipping scripts update");
|
|
5197
5614
|
return;
|
|
5198
5615
|
}
|
|
5199
|
-
const pkg = JSON.parse(
|
|
5616
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
|
|
5200
5617
|
const templateScripts = readTemplatePackageScripts(template);
|
|
5201
5618
|
const scriptUpdates = getForcedScriptUpdates(templateScripts);
|
|
5202
5619
|
const result = applyPackageJsonScriptUpdates(pkg, scriptUpdates);
|
|
@@ -5204,7 +5621,7 @@ function updatePackageJsonScripts(template) {
|
|
|
5204
5621
|
console.log(` \uD83D\uDCDD ${change.action} script: "${change.name}"`);
|
|
5205
5622
|
}
|
|
5206
5623
|
if (result.updated) {
|
|
5207
|
-
|
|
5624
|
+
writeFileSync7(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
5208
5625
|
`, "utf-8");
|
|
5209
5626
|
console.log(`✅ package.json scripts updated
|
|
5210
5627
|
`);
|
|
@@ -5238,11 +5655,11 @@ function viteHandlesServerScripts(viteConfigSource) {
|
|
|
5238
5655
|
}
|
|
5239
5656
|
function ensureBuildScript() {
|
|
5240
5657
|
const pkgPath = "package.json";
|
|
5241
|
-
if (!
|
|
5658
|
+
if (!existsSync10(pkgPath)) {
|
|
5242
5659
|
console.log(" ⚠️ No package.json found, skipping build-script check");
|
|
5243
5660
|
return;
|
|
5244
5661
|
}
|
|
5245
|
-
const pkg = JSON.parse(
|
|
5662
|
+
const pkg = JSON.parse(readFileSync9(pkgPath, "utf-8"));
|
|
5246
5663
|
const buildScript = pkg.scripts?.build;
|
|
5247
5664
|
if (typeof buildScript !== "string" || buildScript.trim() === "") {
|
|
5248
5665
|
console.log(` ⚠️ No "build" script found. Add one that runs the server-scripts pass, e.g.:
|
|
@@ -5260,17 +5677,17 @@ function ensureBuildScript() {
|
|
|
5260
5677
|
return;
|
|
5261
5678
|
}
|
|
5262
5679
|
pkg.scripts.build = result.script;
|
|
5263
|
-
|
|
5680
|
+
writeFileSync7(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
5264
5681
|
`, "utf-8");
|
|
5265
5682
|
console.log(` \uD83D\uDCDD Added "vite build --mode server-scripts" to the build script`);
|
|
5266
5683
|
}
|
|
5267
5684
|
function verifyViteServerScriptsMode() {
|
|
5268
|
-
const configPath2 = VITE_CONFIG_CANDIDATES.find((p) =>
|
|
5685
|
+
const configPath2 = VITE_CONFIG_CANDIDATES.find((p) => existsSync10(p));
|
|
5269
5686
|
if (!configPath2) {
|
|
5270
5687
|
console.log(" ⚠️ No vite.config found. Server scripts in src/api and src/cron-jobs won't be compiled.");
|
|
5271
5688
|
return;
|
|
5272
5689
|
}
|
|
5273
|
-
const source =
|
|
5690
|
+
const source = readFileSync9(configPath2, "utf-8");
|
|
5274
5691
|
if (viteHandlesServerScripts(source)) {
|
|
5275
5692
|
console.log(` ✅ ${configPath2} handles the server-scripts build mode`);
|
|
5276
5693
|
return;
|
|
@@ -5295,7 +5712,7 @@ function updateCliSkill() {
|
|
|
5295
5712
|
}
|
|
5296
5713
|
execSync3(`git fetch ${cliRemote}`, { stdio: "inherit" });
|
|
5297
5714
|
execSync3(`git checkout ${cliRemote}/main -- skills/ro-cli/SKILL.md`, { stdio: "inherit" });
|
|
5298
|
-
if (
|
|
5715
|
+
if (existsSync10("skills/ro-cli/SKILL.md")) {
|
|
5299
5716
|
mkdirSync2(".agent/skills/ro-cli", { recursive: true });
|
|
5300
5717
|
copyFileSync("skills/ro-cli/SKILL.md", ".agent/skills/ro-cli/SKILL.md");
|
|
5301
5718
|
rmSync2("skills", { recursive: true, force: true });
|
|
@@ -5328,7 +5745,7 @@ async function upgradeTemplate() {
|
|
|
5328
5745
|
const checkoutList = template.checkoutFiles.join(" ");
|
|
5329
5746
|
execSync3(`git checkout ${template.remoteName}/main -- ${checkoutList}`, { stdio: "inherit" });
|
|
5330
5747
|
for (const file of template.newFiles) {
|
|
5331
|
-
if (!
|
|
5748
|
+
if (!existsSync10(file)) {
|
|
5332
5749
|
console.log(`\uD83D\uDCC2 Checking out ${file}...`);
|
|
5333
5750
|
try {
|
|
5334
5751
|
mkdirSync2(path3.dirname(file), { recursive: true });
|
|
@@ -5493,6 +5910,156 @@ async function updateGameSdk() {
|
|
|
5493
5910
|
}
|
|
5494
5911
|
}
|
|
5495
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
|
+
|
|
5496
6063
|
// src/cli.ts
|
|
5497
6064
|
function renderError(err) {
|
|
5498
6065
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -5533,15 +6100,15 @@ Available templates:
|
|
|
5533
6100
|
input: process.stdin,
|
|
5534
6101
|
output: process.stdout
|
|
5535
6102
|
});
|
|
5536
|
-
return new Promise((
|
|
6103
|
+
return new Promise((resolve7) => {
|
|
5537
6104
|
rl.question(`Select a template (1-${entries.length}): `, (answer) => {
|
|
5538
6105
|
rl.close();
|
|
5539
6106
|
const index = parseInt(answer, 10) - 1;
|
|
5540
6107
|
if (index >= 0 && index < entries.length) {
|
|
5541
|
-
|
|
6108
|
+
resolve7(entries[index].name);
|
|
5542
6109
|
} else {
|
|
5543
6110
|
console.error("Invalid selection, defaulting to 'webapp'");
|
|
5544
|
-
|
|
6111
|
+
resolve7("webapp");
|
|
5545
6112
|
}
|
|
5546
6113
|
});
|
|
5547
6114
|
});
|
|
@@ -5586,6 +6153,20 @@ addGlobalConfigWriteOptions(globalConfig.command("set").description("Replace the
|
|
|
5586
6153
|
addGlobalConfigWriteOptions(globalConfig.command("patch").description("Merge-patch (RFC 7396) the `config` and/or `publicConfig` column. `null` values delete keys.")).action(async (options) => {
|
|
5587
6154
|
await patchGlobalConfig(options);
|
|
5588
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
|
+
});
|
|
5589
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) => {
|
|
5590
6171
|
let templateName;
|
|
5591
6172
|
if (options.template) {
|
|
@@ -5619,8 +6200,8 @@ app.command("promote").description("Promote the current webapp to production (cr
|
|
|
5619
6200
|
app.command("update-game-sdk").description("Download and update the GameSDK library, types, and documentation").action(async () => {
|
|
5620
6201
|
await updateGameSdk();
|
|
5621
6202
|
});
|
|
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 });
|
|
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 });
|
|
5624
6205
|
});
|
|
5625
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) => {
|
|
5626
6207
|
await syncWidgetManifest(options);
|
|
@@ -5632,6 +6213,16 @@ addConfigTargetOptions(config.command("get").description("Pull the current webap
|
|
|
5632
6213
|
addConfigSetOptions(config.command("set").description("Update webapp metadata such as title, cover image, description, and localization")).action(async (options) => {
|
|
5633
6214
|
await updateWebappConfig(options);
|
|
5634
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
|
+
});
|
|
5635
6226
|
app.command("upgrade-template").description("Upgrade template files and CLI scripts from the template repository").action(async () => {
|
|
5636
6227
|
await upgradeTemplate();
|
|
5637
6228
|
});
|