@rodyssey/cli 0.3.1 → 0.4.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 +8 -1
  2. package/dist/cli.js +279 -66
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -102,7 +102,10 @@ no identity block stored)`. Re-running `ro auth login -e <env>` refreshes them.
102
102
 
103
103
  `app upgrade-template` backfills additive template files that are missing from
104
104
  older projects. This includes the Dynamic Worker MCP sample endpoint and the
105
- shared `mcp/` helper folder; existing local MCP files are left untouched.
105
+ shared `mcp/` helper folder; existing local MCP files are left untouched. It
106
+ also force-overwrites a whitelist of CLI/template-owned `package.json` scripts
107
+ such as `deploy`, `deploy:staging`, `deploy:production`, and
108
+ `sync-widget-manifest`.
106
109
 
107
110
  ## Deployment Output
108
111
 
@@ -118,6 +121,10 @@ SPA deploys register built `dist/api/*` and `dist/cron-jobs/*` scripts after the
118
121
  HTML zip has deployed. Fullstack deploys register the same Dynamic Worker
119
122
  scripts after `wrangler deploy` and widget manifest sync.
120
123
 
124
+ `ro app sync-widget-manifest` can also be run directly from a fullstack project
125
+ after `bun run build`. It reads `build/client/widgets.manifest.json` by default,
126
+ then PATCHes the CMS webapp config with `details.widgetManifest`.
127
+
121
128
  ## Release
122
129
 
123
130
  Release automation lives in `.github/workflows/release.yml` and uses Changesets.
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.3.1",
2074
+ version: "0.4.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 existsSync4, readFileSync as readFileSync3, readdirSync, statSync, unlinkSync } from "node:fs";
2674
+ import { existsSync as existsSync5, readFileSync as readFileSync4, 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,6 +3857,92 @@ 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";
3862
+ import { resolve } from "node:path";
3863
+ var CONFIG_URLS = {
3864
+ local: "http://localhost:5176/api/webapps/config",
3865
+ development: "https://development-cms.rodyssey.ai/api/webapps/config",
3866
+ staging: "https://staging-cms.rodyssey.ai/api/webapps/config",
3867
+ production: "https://cms.rodyssey.ai/api/webapps/config"
3868
+ };
3869
+ function resolveWidgetConfigUrl(options) {
3870
+ const rawUrl = options.url || process.env.WEBAPP_CONFIG_URL || CONFIG_URLS[options.env];
3871
+ 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.`);
3873
+ }
3874
+ const url = new URL(rawUrl);
3875
+ if (options.host)
3876
+ url.hostname = options.host;
3877
+ if (options.port)
3878
+ url.port = String(options.port);
3879
+ return url.toString().replace(/\/$/, "");
3880
+ }
3881
+ function resolveManifestPath(manifest) {
3882
+ if (manifest)
3883
+ return resolve(process.cwd(), manifest);
3884
+ const candidates = [
3885
+ resolve(process.cwd(), "build/client/widgets.manifest.json"),
3886
+ resolve(process.cwd(), "dist/widgets.manifest.json")
3887
+ ];
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
+ }
3892
+ return found;
3893
+ }
3894
+ function readManifest(path3) {
3895
+ const parsed = JSON.parse(readFileSync3(path3, "utf-8"));
3896
+ if (!Array.isArray(parsed)) {
3897
+ throw new Error(`Widget manifest must be a JSON array: ${path3}`);
3898
+ }
3899
+ return parsed;
3900
+ }
3901
+ function resolveWebappId(webappId) {
3902
+ const resolved = webappId || process.env.WEBAPP_ID;
3903
+ if (!resolved) {
3904
+ throw new Error("WEBAPP_ID is not set. Add it to .env or pass --webapp-id.");
3905
+ }
3906
+ return resolved;
3907
+ }
3908
+ function ensureDeployToken(env) {
3909
+ if (process.env.DEPLOY_TOKEN)
3910
+ return;
3911
+ throw new Error(`DEPLOY_TOKEN is not set. Please check your .env or .env.${env} file.`);
3912
+ }
3913
+ async function syncWidgetManifest(options) {
3914
+ loadEnv(options.env);
3915
+ const webappId = resolveWebappId(options.webappId);
3916
+ if (!options.dryRun)
3917
+ ensureDeployToken(options.env);
3918
+ const manifestPath = resolveManifestPath(options.manifest);
3919
+ const manifest = readManifest(manifestPath);
3920
+ const payload = { webappId, details: { widgetManifest: manifest } };
3921
+ const configUrl = resolveWidgetConfigUrl(options);
3922
+ console.log(`Syncing ${manifest.length} widget manifest entr${manifest.length === 1 ? "y" : "ies"}`);
3923
+ console.log(`Manifest: ${manifestPath}`);
3924
+ console.log(`Config URL: ${configUrl}`);
3925
+ console.log(`Webapp ID: ${webappId}`);
3926
+ if (options.dryRun) {
3927
+ console.log(JSON.stringify(payload, null, 2));
3928
+ return;
3929
+ }
3930
+ const response = await fetch(configUrl, {
3931
+ method: "PATCH",
3932
+ headers: {
3933
+ Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
3934
+ "Content-Type": "application/json"
3935
+ },
3936
+ body: JSON.stringify(payload)
3937
+ });
3938
+ if (!response.ok) {
3939
+ const errorText = await response.text();
3940
+ throw new Error(`Widget manifest sync failed: ${response.status} ${response.statusText}
3941
+ ${errorText}`);
3942
+ }
3943
+ console.log("Widget manifest synced");
3944
+ }
3945
+
3860
3946
  // src/deploy.ts
3861
3947
  var DEPLOY_URLS = {
3862
3948
  local: "http://localhost:5176/api/webapps/deploy",
@@ -3888,7 +3974,7 @@ function pickNumber(value) {
3888
3974
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
3889
3975
  }
3890
3976
  function isFullstackProject() {
3891
- return existsSync4("app") && existsSync4("workers/app.ts") && existsSync4("wrangler.jsonc");
3977
+ return existsSync5("app") && existsSync5("workers/app.ts") && existsSync5("wrangler.jsonc");
3892
3978
  }
3893
3979
  function resolveDeployUrl(env, overrides) {
3894
3980
  let deployUrl = DEPLOY_URLS[env];
@@ -3908,6 +3994,9 @@ function resolveDeployUrl(env, overrides) {
3908
3994
  function resolveScriptsSetupUrl(deployUrl) {
3909
3995
  return deployUrl.replace("/webapps/deploy", "/webapps/scripts-setup");
3910
3996
  }
3997
+ function resolveConfigUrlFromDeployUrl(deployUrl) {
3998
+ return deployUrl.replace("/webapps/deploy", "/webapps/config");
3999
+ }
3911
4000
  function resolveWebClientBaseUrl(env) {
3912
4001
  return WEB_CLIENT_URLS[env];
3913
4002
  }
@@ -3961,7 +4050,7 @@ function collectScripts(scriptFiles) {
3961
4050
  const payload = { api: {}, cron: {}, cronConfig: null };
3962
4051
  const summary = { apiEndpoints: [], cronJobs: [], mcpEndpoints: [] };
3963
4052
  for (const filePath of scriptFiles) {
3964
- const content = readFileSync3(filePath, "utf-8");
4053
+ const content = readFileSync4(filePath, "utf-8");
3965
4054
  const relativePath = normalizeBuildRelativePath(filePath);
3966
4055
  if (relativePath === "cron-jobs/cron.config.json") {
3967
4056
  payload.cronConfig = readCronConfig(filePath, content);
@@ -4077,7 +4166,7 @@ ${JSON.stringify(result, null, 2)}`);
4077
4166
  return { summary, result };
4078
4167
  }
4079
4168
  function getAllFiles(dirPath, arrayOfFiles = []) {
4080
- if (!existsSync4(dirPath))
4169
+ if (!existsSync5(dirPath))
4081
4170
  return arrayOfFiles;
4082
4171
  const files = readdirSync(dirPath);
4083
4172
  files.forEach(function(f) {
@@ -4091,7 +4180,7 @@ function getAllFiles(dirPath, arrayOfFiles = []) {
4091
4180
  return arrayOfFiles;
4092
4181
  }
4093
4182
  function fileToBlob(filePath) {
4094
- const buffer = readFileSync3(filePath);
4183
+ const buffer = readFileSync4(filePath);
4095
4184
  return new Blob([buffer], { type: src_default.getType(filePath) || "application/octet-stream" });
4096
4185
  }
4097
4186
  async function deployFullstack(env, overrides) {
@@ -4125,13 +4214,10 @@ async function deployFullstack(env, overrides) {
4125
4214
  console.log(`✅ Worker deployed
4126
4215
  `);
4127
4216
  console.log("\uD83D\uDCCB Step 3: Syncing widget manifest to CMS config...");
4128
- const syncArgs = [
4129
- "--env",
4130
- shellQuote(env),
4131
- ...overrides.host ? ["--host", shellQuote(overrides.host)] : [],
4132
- ...overrides.port ? ["--port", shellQuote(String(overrides.port))] : []
4133
- ];
4134
- execSync2(`bun run sync-widget-manifest -- ${syncArgs.join(" ")}`, { stdio: "inherit", env: childEnv });
4217
+ await syncWidgetManifest({
4218
+ env,
4219
+ url: resolveConfigUrlFromDeployUrl(deployUrl)
4220
+ });
4135
4221
  console.log();
4136
4222
  const scriptFiles = getAllFiles(BUILD_DIR).filter(isScriptFile);
4137
4223
  const scriptsSync = await syncScripts(deployUrl, scriptFiles, "Step 4");
@@ -4218,7 +4304,7 @@ ${errorText}`);
4218
4304
  console.log(`✅ Created ${ZIP_FILE}
4219
4305
  `);
4220
4306
  console.log("☁️ Step 4: Deploying HTML zip to server...");
4221
- const zipBuffer = readFileSync3(ZIP_FILE);
4307
+ const zipBuffer = readFileSync4(ZIP_FILE);
4222
4308
  try {
4223
4309
  const response = await fetch(DEPLOY_URL, {
4224
4310
  method: "POST",
@@ -4248,7 +4334,7 @@ ${errorText}`);
4248
4334
  console.error("❌ Deploy failed:", error);
4249
4335
  throw error;
4250
4336
  } finally {
4251
- if (existsSync4(ZIP_FILE)) {
4337
+ if (existsSync5(ZIP_FILE)) {
4252
4338
  unlinkSync(ZIP_FILE);
4253
4339
  console.log(`
4254
4340
  \uD83E\uDDF9 Cleaned up ${ZIP_FILE}`);
@@ -4267,8 +4353,8 @@ async function deploy(env = "development", overrides = {}) {
4267
4353
  }
4268
4354
 
4269
4355
  // src/global-config.ts
4270
- import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
4271
- import { resolve } from "node:path";
4356
+ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
4357
+ import { resolve as resolve2 } from "node:path";
4272
4358
  var PROD_ENV = "production";
4273
4359
  function isPlainObject(value) {
4274
4360
  return !!value && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
@@ -4289,8 +4375,8 @@ function applyMergePatch(target, patch) {
4289
4375
  function parseFlag(name, value) {
4290
4376
  if (value === "null")
4291
4377
  return null;
4292
- const candidatePath = resolve(process.cwd(), value);
4293
- const raw = existsSync5(candidatePath) ? readFileSync4(candidatePath, "utf-8") : value;
4378
+ const candidatePath = resolve2(process.cwd(), value);
4379
+ const raw = existsSync6(candidatePath) ? readFileSync5(candidatePath, "utf-8") : value;
4294
4380
  try {
4295
4381
  return JSON.parse(raw);
4296
4382
  } catch (error) {
@@ -4485,7 +4571,7 @@ ${pretty(payload)}`);
4485
4571
  }
4486
4572
  const text = pretty(payload);
4487
4573
  if (options.out) {
4488
- const outPath = resolve(process.cwd(), options.out);
4574
+ const outPath = resolve2(process.cwd(), options.out);
4489
4575
  writeFileSync3(outPath, `${text}
4490
4576
  `, "utf-8");
4491
4577
  console.log(`✅ Wrote global config to ${outPath}`);
@@ -4581,21 +4667,21 @@ async function patchGlobalConfig(options) {
4581
4667
  }
4582
4668
 
4583
4669
  // src/promote.ts
4584
- import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
4585
- import { resolve as resolve3 } from "node:path";
4670
+ import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
4671
+ import { resolve as resolve4 } from "node:path";
4586
4672
 
4587
4673
  // src/update-webapp-config.ts
4588
- import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
4589
- import { resolve as resolve2 } from "node:path";
4590
- var CONFIG_URLS = {
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 = {
4591
4677
  local: "http://localhost:5176/api/webapps/config",
4592
4678
  development: "https://development-cms.rodyssey.ai/api/webapps/config",
4593
4679
  staging: "https://staging-cms.rodyssey.ai/api/webapps/config",
4594
4680
  production: "https://cms.rodyssey.ai/api/webapps/config"
4595
4681
  };
4596
4682
  function parseJsonOption(value, optionName) {
4597
- const maybePath = resolve2(process.cwd(), value);
4598
- const raw = existsSync6(maybePath) ? readFileSync5(maybePath, "utf-8") : value;
4683
+ const maybePath = resolve3(process.cwd(), value);
4684
+ const raw = existsSync7(maybePath) ? readFileSync6(maybePath, "utf-8") : value;
4599
4685
  try {
4600
4686
  const parsed = JSON.parse(raw);
4601
4687
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
@@ -4615,12 +4701,12 @@ function coerceMaybeNull(value) {
4615
4701
  return value;
4616
4702
  }
4617
4703
  function resolveConfigUrl(options, required = true) {
4618
- const configUrl = options.url || process.env.WEBAPP_CONFIG_URL || CONFIG_URLS[options.env];
4704
+ const configUrl = options.url || process.env.WEBAPP_CONFIG_URL || CONFIG_URLS2[options.env];
4619
4705
  if (!configUrl) {
4620
4706
  if (!required)
4621
4707
  return;
4622
4708
  console.error("❌ Error: no webapp config endpoint configured.");
4623
- 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.`);
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.`);
4624
4710
  process.exit(1);
4625
4711
  }
4626
4712
  const url = new URL(configUrl);
@@ -4649,7 +4735,7 @@ function buildDetailsPayload(options) {
4649
4735
  }
4650
4736
  return payload;
4651
4737
  }
4652
- function resolveWebappId(webappId) {
4738
+ function resolveWebappId2(webappId) {
4653
4739
  const resolved = webappId || process.env.WEBAPP_ID;
4654
4740
  if (!resolved) {
4655
4741
  console.error("❌ Error: WEBAPP_ID is not set. Pass --webapp-id or set it in your .env file.");
@@ -4657,7 +4743,7 @@ function resolveWebappId(webappId) {
4657
4743
  }
4658
4744
  return resolved;
4659
4745
  }
4660
- function ensureDeployToken(env) {
4746
+ function ensureDeployToken2(env) {
4661
4747
  if (process.env.DEPLOY_TOKEN)
4662
4748
  return;
4663
4749
  console.error("❌ Error: DEPLOY_TOKEN is not set in environment variables.");
@@ -4669,12 +4755,12 @@ function getConfigUrl(options, required = true) {
4669
4755
  }
4670
4756
  async function fetchWebappConfig(options) {
4671
4757
  loadEnv(options.env);
4672
- const webappId = resolveWebappId(options.webappId);
4758
+ const webappId = resolveWebappId2(options.webappId);
4673
4759
  const CONFIG_URL = getConfigUrl(options);
4674
4760
  if (!CONFIG_URL) {
4675
4761
  throw new Error("No webapp config endpoint configured.");
4676
4762
  }
4677
- ensureDeployToken(options.env);
4763
+ ensureDeployToken2(options.env);
4678
4764
  const url = new URL(CONFIG_URL);
4679
4765
  url.searchParams.set("webappId", webappId);
4680
4766
  const response = await fetch(url, {
@@ -4693,7 +4779,7 @@ ${errorText}`);
4693
4779
  }
4694
4780
  async function getWebappConfig(options) {
4695
4781
  loadEnv(options.env);
4696
- const webappId = resolveWebappId(options.webappId);
4782
+ const webappId = resolveWebappId2(options.webappId);
4697
4783
  const CONFIG_URL = getConfigUrl(options);
4698
4784
  if (!CONFIG_URL)
4699
4785
  return;
@@ -4715,7 +4801,7 @@ async function getWebappConfig(options) {
4715
4801
  }
4716
4802
  async function updateWebappConfig(options) {
4717
4803
  loadEnv(options.env);
4718
- const webappId = resolveWebappId(options.webappId);
4804
+ const webappId = resolveWebappId2(options.webappId);
4719
4805
  const details = buildDetailsPayload(options);
4720
4806
  if (Object.keys(details).length === 0) {
4721
4807
  console.error("❌ Error: no detail fields provided. Use --title, --description, --cover-img, --localization, or --details.");
@@ -4724,7 +4810,7 @@ async function updateWebappConfig(options) {
4724
4810
  const payload = { webappId, details };
4725
4811
  const CONFIG_URL = getConfigUrl(options, !options.dryRun);
4726
4812
  if (!options.dryRun)
4727
- ensureDeployToken(options.env);
4813
+ ensureDeployToken2(options.env);
4728
4814
  console.log(`⚙️ Updating webapp config for [${options.env}] environment...`);
4729
4815
  if (CONFIG_URL) {
4730
4816
  console.log(`\uD83D\uDCCD Config URL: ${CONFIG_URL}`);
@@ -4790,8 +4876,8 @@ function unwrapSourceDetails(payload) {
4790
4876
  return payload;
4791
4877
  }
4792
4878
  function parseDetailsOption(value) {
4793
- const maybePath = resolve3(process.cwd(), value);
4794
- const raw = existsSync7(maybePath) ? readFileSync6(maybePath, "utf-8") : value;
4879
+ const maybePath = resolve4(process.cwd(), value);
4880
+ const raw = existsSync8(maybePath) ? readFileSync7(maybePath, "utf-8") : value;
4795
4881
  try {
4796
4882
  const parsed = JSON.parse(raw);
4797
4883
  if (!isObject3(parsed)) {
@@ -4880,9 +4966,9 @@ async function promote(options) {
4880
4966
  console.info("\uD83D\uDCA1 Run `ro app create --auto` first, or set WEBAPP_ID manually.");
4881
4967
  process.exit(1);
4882
4968
  }
4883
- const prodEnvPath = resolve3(process.cwd(), PROD_ENV_FILE);
4884
- if (existsSync7(prodEnvPath)) {
4885
- const content = readFileSync6(prodEnvPath, "utf-8");
4969
+ const prodEnvPath = resolve4(process.cwd(), PROD_ENV_FILE);
4970
+ if (existsSync8(prodEnvPath)) {
4971
+ const content = readFileSync7(prodEnvPath, "utf-8");
4886
4972
  if (/^WEBAPP_ID=.+/m.test(content)) {
4887
4973
  console.error(`❌ Error: ${PROD_ENV_FILE} already has WEBAPP_ID. The app appears to be promoted already.`);
4888
4974
  process.exit(1);
@@ -4966,10 +5052,11 @@ ${JSON.stringify(payload, null, 2)}`);
4966
5052
 
4967
5053
  // src/upgrade-template.ts
4968
5054
  import { execSync as execSync3 } from "node:child_process";
4969
- import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2, copyFileSync, rmSync as rmSync2 } from "node:fs";
5055
+ import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2, copyFileSync, rmSync as rmSync2 } from "node:fs";
4970
5056
  import path3 from "node:path";
4971
5057
  var TEMPLATES = {
4972
5058
  webapp: {
5059
+ key: "webapp",
4973
5060
  name: "webapp (SPA)",
4974
5061
  repo: "https://github.com/airconcepts/webapp-template.git",
4975
5062
  remoteName: "template",
@@ -5005,6 +5092,7 @@ var TEMPLATES = {
5005
5092
  ]
5006
5093
  },
5007
5094
  "webapp-fullstack": {
5095
+ key: "webapp-fullstack",
5008
5096
  name: "webapp (Fullstack)",
5009
5097
  repo: "https://github.com/airconcepts/webapp-template-fullstack.git",
5010
5098
  remoteName: "template",
@@ -5019,20 +5107,31 @@ var TEMPLATES = {
5019
5107
  ]
5020
5108
  }
5021
5109
  };
5022
- var CLI_SCRIPTS = {
5110
+ var CLI_SCRIPT_DEFAULTS = {
5023
5111
  "link-game-sdk": "bunx @rodyssey/cli@latest app update-game-sdk",
5024
5112
  deploy: "bunx @rodyssey/cli@latest app deploy",
5113
+ "sync-widget-manifest": "bunx @rodyssey/cli@latest app sync-widget-manifest",
5025
5114
  "get-webapp-config": "bunx @rodyssey/cli@latest app config get",
5026
5115
  "update-webapp-config": "bunx @rodyssey/cli@latest app config set",
5027
5116
  "upgrade-template": "bunx @rodyssey/cli@latest app upgrade-template"
5028
5117
  };
5118
+ var FORCE_OVERWRITE_SCRIPT_NAMES = [
5119
+ "link-game-sdk",
5120
+ "deploy",
5121
+ "sync-widget-manifest",
5122
+ "deploy:staging",
5123
+ "deploy:production",
5124
+ "get-webapp-config",
5125
+ "update-webapp-config",
5126
+ "upgrade-template"
5127
+ ];
5029
5128
  function detectTemplate() {
5030
- if (existsSync8("app")) {
5129
+ if (existsSync9("app")) {
5031
5130
  console.log(`\uD83D\uDD0D Detected fullstack template (found app/ directory)
5032
5131
  `);
5033
5132
  return TEMPLATES["webapp-fullstack"];
5034
5133
  }
5035
- if (existsSync8("src")) {
5134
+ if (existsSync9("src")) {
5036
5135
  console.log(`\uD83D\uDD0D Detected SPA template (found src/ directory)
5037
5136
  `);
5038
5137
  return TEMPLATES["webapp"];
@@ -5041,26 +5140,64 @@ function detectTemplate() {
5041
5140
  `);
5042
5141
  return TEMPLATES["webapp"];
5043
5142
  }
5044
- function updatePackageJsonScripts() {
5143
+ function normalizeScripts(value) {
5144
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
5145
+ return {};
5146
+ }
5147
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "string"));
5148
+ }
5149
+ function readTemplatePackageScripts(template) {
5150
+ try {
5151
+ const rawPackageJson = execSync3(`git show ${template.remoteName}/main:package.json`, {
5152
+ encoding: "utf-8"
5153
+ });
5154
+ const packageJson = JSON.parse(rawPackageJson);
5155
+ return normalizeScripts(packageJson.scripts);
5156
+ } catch (error) {
5157
+ console.log(` ⚠️ Failed to read package.json scripts from template remote: ${error instanceof Error ? error.message : error}`);
5158
+ console.log(" ℹ️ Falling back to built-in CLI script defaults.");
5159
+ return {};
5160
+ }
5161
+ }
5162
+ function getForcedScriptUpdates(templateScripts) {
5163
+ const normalizedTemplateScripts = normalizeScripts(templateScripts);
5164
+ const updates = {};
5165
+ for (const name of FORCE_OVERWRITE_SCRIPT_NAMES) {
5166
+ const command = normalizedTemplateScripts[name] ?? CLI_SCRIPT_DEFAULTS[name];
5167
+ if (command) {
5168
+ updates[name] = command;
5169
+ }
5170
+ }
5171
+ return updates;
5172
+ }
5173
+ function applyPackageJsonScriptUpdates(packageJson, scriptUpdates) {
5174
+ if (!packageJson.scripts) {
5175
+ packageJson.scripts = {};
5176
+ }
5177
+ const changes = [];
5178
+ for (const [name, cmd] of Object.entries(scriptUpdates)) {
5179
+ if (packageJson.scripts[name] !== cmd) {
5180
+ const action = packageJson.scripts[name] ? "Updated" : "Added";
5181
+ packageJson.scripts[name] = cmd;
5182
+ changes.push({ name, action });
5183
+ }
5184
+ }
5185
+ return { updated: changes.length > 0, changes };
5186
+ }
5187
+ function updatePackageJsonScripts(template) {
5045
5188
  const pkgPath = "package.json";
5046
- if (!existsSync8(pkgPath)) {
5189
+ if (!existsSync9(pkgPath)) {
5047
5190
  console.log("⚠️ No package.json found, skipping scripts update");
5048
5191
  return;
5049
5192
  }
5050
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
5051
- if (!pkg.scripts) {
5052
- pkg.scripts = {};
5053
- }
5054
- let updated = false;
5055
- for (const [name, cmd] of Object.entries(CLI_SCRIPTS)) {
5056
- if (pkg.scripts[name] !== cmd) {
5057
- const action = pkg.scripts[name] ? "Updated" : "Added";
5058
- pkg.scripts[name] = cmd;
5059
- console.log(` \uD83D\uDCDD ${action} script: "${name}"`);
5060
- updated = true;
5061
- }
5193
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
5194
+ const templateScripts = readTemplatePackageScripts(template);
5195
+ const scriptUpdates = getForcedScriptUpdates(templateScripts);
5196
+ const result = applyPackageJsonScriptUpdates(pkg, scriptUpdates);
5197
+ for (const change of result.changes) {
5198
+ console.log(` \uD83D\uDCDD ${change.action} script: "${change.name}"`);
5062
5199
  }
5063
- if (updated) {
5200
+ if (result.updated) {
5064
5201
  writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + `
5065
5202
  `, "utf-8");
5066
5203
  console.log(`✅ package.json scripts updated
@@ -5070,6 +5207,73 @@ function updatePackageJsonScripts() {
5070
5207
  `);
5071
5208
  }
5072
5209
  }
5210
+ var SERVER_SCRIPTS_STEP = "vite build --mode server-scripts";
5211
+ var CANONICAL_BUILD_SCRIPT = "tsc -b && vite build && vite build --mode server-scripts && vite build --mode report";
5212
+ var VITE_CONFIG_CANDIDATES = ["vite.config.ts", "vite.config.js", "vite.config.mjs"];
5213
+ var TEMPLATE_VITE_CONFIG_URL = "https://github.com/airconcepts/webapp-template/blob/main/vite.config.ts";
5214
+ function normalizeSegment(segment) {
5215
+ return segment.trim().replace(/\s+/g, " ");
5216
+ }
5217
+ function ensureServerScriptsStep(buildScript) {
5218
+ const segments = buildScript.split("&&").map(normalizeSegment).filter(Boolean);
5219
+ if (segments.includes(SERVER_SCRIPTS_STEP)) {
5220
+ return { status: "present" };
5221
+ }
5222
+ const bareBuildIndex = segments.indexOf("vite build");
5223
+ if (bareBuildIndex === -1) {
5224
+ return { status: "unsafe" };
5225
+ }
5226
+ const next = [...segments];
5227
+ next.splice(bareBuildIndex + 1, 0, SERVER_SCRIPTS_STEP);
5228
+ return { status: "inserted", script: next.join(" && ") };
5229
+ }
5230
+ function viteHandlesServerScripts(viteConfigSource) {
5231
+ return viteConfigSource.includes("server-scripts");
5232
+ }
5233
+ function ensureBuildScript() {
5234
+ const pkgPath = "package.json";
5235
+ if (!existsSync9(pkgPath)) {
5236
+ console.log(" ⚠️ No package.json found, skipping build-script check");
5237
+ return;
5238
+ }
5239
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf-8"));
5240
+ const buildScript = pkg.scripts?.build;
5241
+ if (typeof buildScript !== "string" || buildScript.trim() === "") {
5242
+ console.log(` ⚠️ No "build" script found. Add one that runs the server-scripts pass, e.g.:
5243
+ ` + ` "build": "${CANONICAL_BUILD_SCRIPT}"`);
5244
+ return;
5245
+ }
5246
+ const result = ensureServerScriptsStep(buildScript);
5247
+ if (result.status === "present") {
5248
+ console.log(" ✅ build script already runs the server-scripts pass");
5249
+ return;
5250
+ }
5251
+ if (result.status === "unsafe") {
5252
+ console.log(` ⚠️ Could not auto-update the build script. Ensure it runs ` + `"vite build --mode server-scripts" after "vite build".
5253
+ ` + ` Current: ${buildScript}`);
5254
+ return;
5255
+ }
5256
+ pkg.scripts.build = result.script;
5257
+ writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + `
5258
+ `, "utf-8");
5259
+ console.log(` \uD83D\uDCDD Added "vite build --mode server-scripts" to the build script`);
5260
+ }
5261
+ function verifyViteServerScriptsMode() {
5262
+ const configPath2 = VITE_CONFIG_CANDIDATES.find((p) => existsSync9(p));
5263
+ if (!configPath2) {
5264
+ console.log(" ⚠️ No vite.config found. Server scripts in src/api and src/cron-jobs won't be compiled.");
5265
+ return;
5266
+ }
5267
+ const source = readFileSync8(configPath2, "utf-8");
5268
+ if (viteHandlesServerScripts(source)) {
5269
+ console.log(` ✅ ${configPath2} handles the server-scripts build mode`);
5270
+ return;
5271
+ }
5272
+ console.log(` ⚠️ ${configPath2} does not handle the "server-scripts" build mode.
5273
+ ` + ` src/api and src/cron-jobs won't compile into dist/, so "app deploy" will
5274
+ ` + ` report "No scripts found to sync." Add the server-scripts branch from the template:
5275
+ ` + ` ${TEMPLATE_VITE_CONFIG_URL}`);
5276
+ }
5073
5277
  function updateCliSkill() {
5074
5278
  const cliRemote = "ro-cli";
5075
5279
  const cliRepo = "https://github.com/airconcepts/ro-cli.git";
@@ -5085,7 +5289,7 @@ function updateCliSkill() {
5085
5289
  }
5086
5290
  execSync3(`git fetch ${cliRemote}`, { stdio: "inherit" });
5087
5291
  execSync3(`git checkout ${cliRemote}/main -- skills/ro-cli/SKILL.md`, { stdio: "inherit" });
5088
- if (existsSync8("skills/ro-cli/SKILL.md")) {
5292
+ if (existsSync9("skills/ro-cli/SKILL.md")) {
5089
5293
  mkdirSync2(".agent/skills/ro-cli", { recursive: true });
5090
5294
  copyFileSync("skills/ro-cli/SKILL.md", ".agent/skills/ro-cli/SKILL.md");
5091
5295
  rmSync2("skills", { recursive: true, force: true });
@@ -5118,7 +5322,7 @@ async function upgradeTemplate() {
5118
5322
  const checkoutList = template.checkoutFiles.join(" ");
5119
5323
  execSync3(`git checkout ${template.remoteName}/main -- ${checkoutList}`, { stdio: "inherit" });
5120
5324
  for (const file of template.newFiles) {
5121
- if (!existsSync8(file)) {
5325
+ if (!existsSync9(file)) {
5122
5326
  console.log(`\uD83D\uDCC2 Checking out ${file}...`);
5123
5327
  try {
5124
5328
  mkdirSync2(path3.dirname(file), { recursive: true });
@@ -5134,7 +5338,13 @@ async function upgradeTemplate() {
5134
5338
  \uD83D\uDD27 Updating CLI skill documentation...`);
5135
5339
  updateCliSkill();
5136
5340
  console.log("\uD83D\uDCE6 Updating package.json scripts...");
5137
- updatePackageJsonScripts();
5341
+ updatePackageJsonScripts(template);
5342
+ if (template.key === "webapp") {
5343
+ console.log("\uD83E\uDDE9 Verifying server-scripts build chain...");
5344
+ ensureBuildScript();
5345
+ verifyViteServerScriptsMode();
5346
+ console.log();
5347
+ }
5138
5348
  console.log("✅ Template upgrade complete! Please check git status for changes.");
5139
5349
  } catch (error) {
5140
5350
  console.error("❌ Upgrade failed:", error);
@@ -5317,15 +5527,15 @@ Available templates:
5317
5527
  input: process.stdin,
5318
5528
  output: process.stdout
5319
5529
  });
5320
- return new Promise((resolve4) => {
5530
+ return new Promise((resolve5) => {
5321
5531
  rl.question(`Select a template (1-${entries.length}): `, (answer) => {
5322
5532
  rl.close();
5323
5533
  const index = parseInt(answer, 10) - 1;
5324
5534
  if (index >= 0 && index < entries.length) {
5325
- resolve4(entries[index].name);
5535
+ resolve5(entries[index].name);
5326
5536
  } else {
5327
5537
  console.error("Invalid selection, defaulting to 'webapp'");
5328
- resolve4("webapp");
5538
+ resolve5("webapp");
5329
5539
  }
5330
5540
  });
5331
5541
  });
@@ -5406,6 +5616,9 @@ app.command("update-game-sdk").description("Download and update the GameSDK libr
5406
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) => {
5407
5617
  await deploy(options.env, { host: options.host, port: options.port });
5408
5618
  });
5619
+ 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
+ await syncWidgetManifest(options);
5621
+ });
5409
5622
  var config = app.command("config").description("Manage webapp metadata config");
5410
5623
  addConfigTargetOptions(config.command("get").description("Pull the current webapp metadata config from the CMS").option("--out <file>", "Write the config JSON to a file")).action(async (options) => {
5411
5624
  await getWebappConfig(options);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rodyssey/cli",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Scaffold new projects from airconcepts templates",
5
5
  "repository": {
6
6
  "type": "git",