@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.
Files changed (3) hide show
  1. package/README.md +30 -0
  2. package/dist/cli.js +908 -311
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2071,7 +2071,7 @@ var {
2071
2071
  // package.json
2072
2072
  var package_default = {
2073
2073
  name: "@rodyssey/cli",
2074
- version: "0.4.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 existsSync5, readFileSync as readFileSync4, readdirSync, statSync, unlinkSync } from "node:fs";
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/sync-widget-manifest.ts
3861
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
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 || CONFIG_URLS[options.env];
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(CONFIG_URLS).join(", ")}, pass --url, or set WEBAPP_CONFIG_URL.`);
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
- return resolve(process.cwd(), manifest);
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
- resolve(process.cwd(), "build/client/widgets.manifest.json"),
3886
- resolve(process.cwd(), "dist/widgets.manifest.json")
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) => existsSync4(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(readFileSync3(path3, "utf-8"));
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 resolveWebappId(webappId) {
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 ensureDeployToken(env) {
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 existsSync5("app") && existsSync5("workers/app.ts") && existsSync5("wrangler.jsonc");
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 = readFileSync4(filePath, "utf-8");
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 (!existsSync5(dirPath))
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 = readFileSync4(filePath);
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 = readFileSync4(ZIP_FILE);
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 (existsSync5(ZIP_FILE)) {
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
- return;
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 existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
4357
- import { resolve as resolve2 } from "node:path";
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 = resolve2(process.cwd(), value);
4379
- const raw = existsSync6(candidatePath) ? readFileSync5(candidatePath, "utf-8") : value;
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 out;
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 deltaLine(d, showColumnTag) {
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) => deltaLine(d, showTag))].join(`
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) => deltaLine(d, showTag))].join(`
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) => deltaLine(d, showTag))].join(`
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 = resolve2(process.cwd(), options.out);
4575
- writeFileSync3(outPath, `${text}
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/promote.ts
4670
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
4671
- import { resolve as resolve4 } from "node:path";
4672
-
4673
- // src/update-webapp-config.ts
4674
- import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
4675
- import { resolve as resolve3 } from "node:path";
4676
- var CONFIG_URLS2 = {
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 coerceMaybeNull(value) {
4697
- if (value === undefined)
4698
- return;
4699
- if (value === "null")
4700
- return null;
4701
- return value;
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
- const url = new URL(configUrl);
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 buildDetailsPayload(options) {
4723
- const payload = options.details ? parseJsonOption(options.details, "--details") : {};
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 resolveWebappId2(webappId) {
4739
- const resolved = webappId || process.env.WEBAPP_ID;
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 ensureDeployToken2(env) {
4747
- if (process.env.DEPLOY_TOKEN)
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 getConfigUrl(options, required = true) {
4754
- return resolveConfigUrl(options, required);
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
- async function fetchWebappConfig(options) {
4757
- loadEnv(options.env);
4758
- const webappId = resolveWebappId2(options.webappId);
4759
- const CONFIG_URL = getConfigUrl(options);
4760
- if (!CONFIG_URL) {
4761
- throw new Error("No webapp config endpoint configured.");
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
- ensureDeployToken2(options.env);
4764
- const url = new URL(CONFIG_URL);
4765
- url.searchParams.set("webappId", webappId);
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: "GET",
5189
+ method,
4768
5190
  headers: {
4769
- Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
4770
- Accept: "application/json"
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
- const errorText = await response.text();
4775
- throw new Error(`Config fetch failed: ${response.status} ${response.statusText}
4776
- ${errorText}`);
5199
+ throw new Error(`${method} ${url} failed: ${response.status} ${response.statusText}
5200
+ ${pretty(payload)}`);
4777
5201
  }
4778
- return await response.json();
5202
+ return payload;
4779
5203
  }
4780
- async function getWebappConfig(options) {
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
- writeFileSync4(options.out, `${output}
5206
+ const outPath = resolve5(process.cwd(), options.out);
5207
+ writeFileSync6(outPath, `${pretty(payload)}
4796
5208
  `, "utf-8");
4797
- console.log(`✅ Webapp config written to ${options.out}`);
5209
+ console.log(`✅ Wrote response to ${outPath}`);
4798
5210
  return;
4799
5211
  }
4800
- console.log(output);
5212
+ console.log(options.json ? pretty(payload) : human);
4801
5213
  }
4802
- async function updateWebappConfig(options) {
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 webappId = resolveWebappId2(options.webappId);
4805
- const details = buildDetailsPayload(options);
4806
- if (Object.keys(details).length === 0) {
4807
- console.error(" Error: no detail fields provided. Use --title, --description, --cover-img, --localization, or --details.");
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("\uD83E\uDDEA Dry run payload:");
4822
- console.log(JSON.stringify(payload, null, 2));
5234
+ console.log(dim(`
5235
+ Dry run — no request sent.`));
4823
5236
  return;
4824
5237
  }
4825
- if (!CONFIG_URL)
4826
- return;
4827
- const response = await fetch(CONFIG_URL, {
4828
- method: "PATCH",
4829
- headers: {
4830
- Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
4831
- "Content-Type": "application/json"
4832
- },
4833
- body: JSON.stringify(payload)
4834
- });
4835
- if (!response.ok) {
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 result = await response.json().catch(() => {
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 PROD_ENV2 = "production";
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 = resolve4(process.cwd(), value);
4880
- const raw = existsSync8(maybePath) ? readFileSync7(maybePath, "utf-8") : value;
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(PROD_ENV2, { override: true });
5378
+ loadEnv(PROD_ENV3, { override: true });
4956
5379
  process.env.WEBAPP_ID = webappId;
4957
5380
  process.env.DEPLOY_TOKEN = deployToken;
4958
- await deploy(PROD_ENV2);
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 = resolve4(process.cwd(), PROD_ENV_FILE);
4970
- if (existsSync8(prodEnvPath)) {
4971
- const content = readFileSync7(prodEnvPath, "utf-8");
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 ${PROD_ENV2} CMS (ephemeral, not stored)...`);
5427
+ \uD83D\uDD10 Logging in to ${PROD_ENV3} CMS (ephemeral, not stored)...`);
5005
5428
  const session = await login({
5006
- env: PROD_ENV2,
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(PROD_ENV2, options.cmsUrl);
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 ${PROD_ENV2}...`);
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 ${PROD_ENV2}.`);
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 existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2, copyFileSync, rmSync as rmSync2 } from "node:fs";
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 (existsSync9("app")) {
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 (existsSync9("src")) {
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 (!existsSync9(pkgPath)) {
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(readFileSync8(pkgPath, "utf-8"));
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
- writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + `
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 (!existsSync9(pkgPath)) {
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(readFileSync8(pkgPath, "utf-8"));
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
- writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + `
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) => existsSync9(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 = readFileSync8(configPath2, "utf-8");
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 (existsSync9("skills/ro-cli/SKILL.md")) {
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 (!existsSync9(file)) {
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((resolve5) => {
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
- resolve5(entries[index].name);
6108
+ resolve7(entries[index].name);
5536
6109
  } else {
5537
6110
  console.error("Invalid selection, defaulting to 'webapp'");
5538
- resolve5("webapp");
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
  });