@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.
Files changed (3) hide show
  1. package/README.md +30 -0
  2. package/dist/cli.js +898 -307
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2071,7 +2071,7 @@ var {
2071
2071
  // package.json
2072
2072
  var package_default = {
2073
2073
  name: "@rodyssey/cli",
2074
- version: "0.4.1",
2074
+ version: "0.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)
@@ -3880,34 +4390,34 @@ function resolveWidgetConfigUrl(options) {
3880
4390
  }
3881
4391
  function resolveManifestPath(manifest) {
3882
4392
  if (manifest) {
3883
- const manifestPath = resolve(process.cwd(), manifest);
3884
- if (!existsSync4(manifestPath)) {
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
- resolve(process.cwd(), "build/client/widgets.manifest.json"),
3891
- resolve(process.cwd(), "dist/widgets.manifest.json")
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) => existsSync4(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(readFileSync3(path3, "utf-8"));
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 resolveWebappId(webappId) {
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 ensureDeployToken(env) {
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 = resolveWebappId(options.webappId);
4432
+ const webappId = resolveWebappId2(options.webappId);
3923
4433
  if (!options.dryRun)
3924
- ensureDeployToken(options.env);
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 existsSync5("app") && existsSync5("workers/app.ts") && existsSync5("wrangler.jsonc");
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 = readFileSync4(filePath, "utf-8");
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 (!existsSync5(dirPath))
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 = readFileSync4(filePath);
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 = readFileSync4(ZIP_FILE);
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 (existsSync5(ZIP_FILE)) {
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
- 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
+ });
4357
4876
  }
4358
- await deploySpa(env, overrides);
4359
4877
  }
4360
4878
 
4361
4879
  // src/global-config.ts
4362
- import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
4363
- import { resolve as resolve2 } from "node:path";
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 = resolve2(process.cwd(), value);
4385
- 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;
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 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`;
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 deltaLine(d, showColumnTag) {
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) => deltaLine(d, showTag))].join(`
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) => deltaLine(d, showTag))].join(`
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) => deltaLine(d, showTag))].join(`
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 = resolve2(process.cwd(), options.out);
4581
- writeFileSync3(outPath, `${text}
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/promote.ts
4676
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
4677
- import { resolve as resolve4 } from "node:path";
4678
-
4679
- // src/update-webapp-config.ts
4680
- import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
4681
- import { resolve as resolve3 } from "node:path";
4682
- var CONFIG_URLS2 = {
4683
- local: "http://localhost:5176/api/webapps/config",
4684
- development: "https://development-cms.rodyssey.ai/api/webapps/config",
4685
- staging: "https://staging-cms.rodyssey.ai/api/webapps/config",
4686
- production: "https://cms.rodyssey.ai/api/webapps/config"
4687
- };
4688
- function parseJsonOption(value, optionName) {
4689
- const maybePath = resolve3(process.cwd(), value);
4690
- const raw = existsSync7(maybePath) ? readFileSync6(maybePath, "utf-8") : value;
4691
- try {
4692
- const parsed = JSON.parse(raw);
4693
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4694
- throw new Error("value must be a JSON object");
4695
- }
4696
- return parsed;
4697
- } catch (error) {
4698
- const message = error instanceof Error ? error.message : String(error);
4699
- throw new Error(`Invalid ${optionName}: ${message}`);
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 coerceMaybeNull(value) {
4703
- if (value === undefined)
4704
- return;
4705
- if (value === "null")
4706
- return null;
4707
- return value;
4708
- }
4709
- function resolveConfigUrl(options, required = true) {
4710
- const configUrl = options.url || process.env.WEBAPP_CONFIG_URL || CONFIG_URLS2[options.env];
4711
- if (!configUrl) {
4712
- if (!required)
4713
- return;
4714
- console.error("❌ Error: no webapp config endpoint configured.");
4715
- console.error(`\uD83D\uDCA1 Use one of these environments: ${Object.keys(CONFIG_URLS2).join(", ")}, pass --url, or set WEBAPP_CONFIG_URL in your .env file.`);
4716
- process.exit(1);
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
- const url = new URL(configUrl);
4719
- if (!options.host && !options.port) {
4720
- return url.toString().replace(/\/$/, "");
4721
- }
4722
- if (options.host)
4723
- url.hostname = options.host;
4724
- if (options.port)
4725
- url.port = String(options.port);
4726
- return url.toString().replace(/\/$/, "");
5134
+ return missingId(`(no default exists for --type ${type})`);
4727
5135
  }
4728
- function buildDetailsPayload(options) {
4729
- const payload = options.details ? parseJsonOption(options.details, "--details") : {};
4730
- const title = coerceMaybeNull(options.title);
4731
- const description = coerceMaybeNull(options.description);
4732
- const coverImg = coerceMaybeNull(options.coverImg);
4733
- if (title !== undefined)
4734
- payload.title = title;
4735
- if (description !== undefined)
4736
- payload.description = description;
4737
- if (coverImg !== undefined)
4738
- payload.coverImg = coverImg;
4739
- if (options.localization !== undefined) {
4740
- payload.localization = options.localization === "null" ? null : parseJsonOption(options.localization, "--localization");
4741
- }
4742
- return payload;
5136
+ function missingId(detail) {
5137
+ throw new Error(`--id is required ${detail}`);
4743
5138
  }
4744
- function resolveWebappId2(webappId) {
4745
- const resolved = webappId || process.env.WEBAPP_ID;
4746
- if (!resolved) {
4747
- console.error("❌ Error: WEBAPP_ID is not set. Pass --webapp-id or set it in your .env file.");
4748
- process.exit(1);
4749
- }
4750
- return resolved;
5139
+ function orFallback(value, fallback) {
5140
+ return value && value.trim().length > 0 ? value : fallback;
4751
5141
  }
4752
- function ensureDeployToken2(env) {
4753
- if (process.env.DEPLOY_TOKEN)
4754
- return;
4755
- console.error("❌ Error: DEPLOY_TOKEN is not set in environment variables.");
4756
- console.info(`\uD83D\uDCA1 Please check your .env or .env.${env} file.`);
4757
- process.exit(1);
5142
+ function capitalize(s) {
5143
+ return s.charAt(0).toUpperCase() + s.slice(1);
4758
5144
  }
4759
- function getConfigUrl(options, required = true) {
4760
- 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
+ `);
4761
5160
  }
4762
- async function fetchWebappConfig(options) {
4763
- loadEnv(options.env);
4764
- const webappId = resolveWebappId2(options.webappId);
4765
- const CONFIG_URL = getConfigUrl(options);
4766
- if (!CONFIG_URL) {
4767
- throw new Error("No webapp config endpoint configured.");
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
- ensureDeployToken2(options.env);
4770
- const url = new URL(CONFIG_URL);
4771
- 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) {
4772
5188
  const response = await fetch(url, {
4773
- method: "GET",
5189
+ method,
4774
5190
  headers: {
4775
- Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
4776
- Accept: "application/json"
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
- const errorText = await response.text();
4781
- throw new Error(`Config fetch failed: ${response.status} ${response.statusText}
4782
- ${errorText}`);
5199
+ throw new Error(`${method} ${url} failed: ${response.status} ${response.statusText}
5200
+ ${pretty(payload)}`);
4783
5201
  }
4784
- return await response.json();
5202
+ return payload;
4785
5203
  }
4786
- async function getWebappConfig(options) {
4787
- loadEnv(options.env);
4788
- const webappId = resolveWebappId2(options.webappId);
4789
- const CONFIG_URL = getConfigUrl(options);
4790
- if (!CONFIG_URL)
4791
- return;
4792
- const url = new URL(CONFIG_URL);
4793
- url.searchParams.set("webappId", webappId);
4794
- console.log(`⚙️ Pulling webapp config for [${options.env}] environment...`);
4795
- console.log(`\uD83D\uDCCD Config URL: ${url.toString()}`);
4796
- console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
4797
- `);
4798
- const config = await fetchWebappConfig(options);
4799
- const output = JSON.stringify(config, null, 2);
5204
+ function emitRead(payload, human, options) {
4800
5205
  if (options.out) {
4801
- writeFileSync4(options.out, `${output}
5206
+ const outPath = resolve5(process.cwd(), options.out);
5207
+ writeFileSync6(outPath, `${pretty(payload)}
4802
5208
  `, "utf-8");
4803
- console.log(`✅ Webapp config written to ${options.out}`);
5209
+ console.log(`✅ Wrote response to ${outPath}`);
4804
5210
  return;
4805
5211
  }
4806
- console.log(output);
5212
+ console.log(options.json ? pretty(payload) : human);
4807
5213
  }
4808
- 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);
4809
5228
  loadEnv(options.env);
4810
- const webappId = resolveWebappId2(options.webappId);
4811
- const details = buildDetailsPayload(options);
4812
- if (Object.keys(details).length === 0) {
4813
- console.error(" Error: no detail fields provided. Use --title, --description, --cover-img, --localization, or --details.");
4814
- process.exit(1);
4815
- }
4816
- const payload = { webappId, details };
4817
- const CONFIG_URL = getConfigUrl(options, !options.dryRun);
4818
- if (!options.dryRun)
4819
- ensureDeployToken2(options.env);
4820
- console.log(`⚙️ Updating webapp config for [${options.env}] environment...`);
4821
- if (CONFIG_URL) {
4822
- console.log(`\uD83D\uDCCD Config URL: ${CONFIG_URL}`);
4823
- }
4824
- console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
4825
- `);
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("\uD83E\uDDEA Dry run payload:");
4828
- console.log(JSON.stringify(payload, null, 2));
5234
+ console.log(dim(`
5235
+ Dry run — no request sent.`));
4829
5236
  return;
4830
5237
  }
4831
- if (!CONFIG_URL)
4832
- return;
4833
- const response = await fetch(CONFIG_URL, {
4834
- method: "PATCH",
4835
- headers: {
4836
- Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
4837
- "Content-Type": "application/json"
4838
- },
4839
- body: JSON.stringify(payload)
4840
- });
4841
- if (!response.ok) {
4842
- const errorText = await response.text();
4843
- throw new Error(`Config update failed: ${response.status} ${response.statusText}
4844
- ${errorText}`);
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 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));
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 PROD_ENV2 = "production";
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 = resolve4(process.cwd(), value);
4886
- 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;
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(PROD_ENV2, { override: true });
5378
+ loadEnv(PROD_ENV3, { override: true });
4962
5379
  process.env.WEBAPP_ID = webappId;
4963
5380
  process.env.DEPLOY_TOKEN = deployToken;
4964
- await deploy(PROD_ENV2);
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 = resolve4(process.cwd(), PROD_ENV_FILE);
4976
- if (existsSync8(prodEnvPath)) {
4977
- 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");
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 ${PROD_ENV2} CMS (ephemeral, not stored)...`);
5427
+ \uD83D\uDD10 Logging in to ${PROD_ENV3} CMS (ephemeral, not stored)...`);
5011
5428
  const session = await login({
5012
- env: PROD_ENV2,
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(PROD_ENV2, options.cmsUrl);
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 ${PROD_ENV2}...`);
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 ${PROD_ENV2}.`);
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 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";
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 (existsSync9("app")) {
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 (existsSync9("src")) {
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 (!existsSync9(pkgPath)) {
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(readFileSync8(pkgPath, "utf-8"));
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
- writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + `
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 (!existsSync9(pkgPath)) {
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(readFileSync8(pkgPath, "utf-8"));
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
- writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + `
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) => existsSync9(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 = readFileSync8(configPath2, "utf-8");
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 (existsSync9("skills/ro-cli/SKILL.md")) {
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 (!existsSync9(file)) {
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((resolve5) => {
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
- resolve5(entries[index].name);
6108
+ resolve7(entries[index].name);
5542
6109
  } else {
5543
6110
  console.error("Invalid selection, defaulting to 'webapp'");
5544
- resolve5("webapp");
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
  });