@rodyssey/cli 0.3.0 → 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.
- package/README.md +22 -1
- package/dist/cli.js +501 -119
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,7 +102,28 @@ 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`.
|
|
109
|
+
|
|
110
|
+
## Deployment Output
|
|
111
|
+
|
|
112
|
+
`ro app deploy` prints a post-deploy summary after the deploy succeeds:
|
|
113
|
+
|
|
114
|
+
- public Rodyssey app URL, e.g. `https://development-app.rodyssey.ai/webapp/<id>`
|
|
115
|
+
- published asset URL for SPA zip deployments
|
|
116
|
+
- registered Dynamic Worker endpoint URLs
|
|
117
|
+
- MCP server endpoint URLs for `api/mcp.ts`
|
|
118
|
+
- cron job names, schedules, enabled state, and script files
|
|
119
|
+
|
|
120
|
+
SPA deploys register built `dist/api/*` and `dist/cron-jobs/*` scripts after the
|
|
121
|
+
HTML zip has deployed. Fullstack deploys register the same Dynamic Worker
|
|
122
|
+
scripts after `wrangler deploy` and widget manifest sync.
|
|
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`.
|
|
106
127
|
|
|
107
128
|
## Release
|
|
108
129
|
|
package/dist/cli.js
CHANGED
|
@@ -2071,7 +2071,7 @@ var {
|
|
|
2071
2071
|
// package.json
|
|
2072
2072
|
var package_default = {
|
|
2073
2073
|
name: "@rodyssey/cli",
|
|
2074
|
-
version: "0.
|
|
2074
|
+
version: "0.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
|
|
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",
|
|
@@ -3864,6 +3950,12 @@ var DEPLOY_URLS = {
|
|
|
3864
3950
|
staging: "https://staging-cms.rodyssey.ai/api/webapps/deploy",
|
|
3865
3951
|
production: "https://cms.rodyssey.ai/api/webapps/deploy"
|
|
3866
3952
|
};
|
|
3953
|
+
var WEB_CLIENT_URLS = {
|
|
3954
|
+
local: "http://localhost:5178",
|
|
3955
|
+
development: "https://development-app.rodyssey.ai",
|
|
3956
|
+
staging: "https://staging-app.rodyssey.ai",
|
|
3957
|
+
production: "https://app.rodyssey.ai"
|
|
3958
|
+
};
|
|
3867
3959
|
var BUILD_DIR = "dist";
|
|
3868
3960
|
var ZIP_FILE = "webapp-build.zip";
|
|
3869
3961
|
var FULLSTACK_DEPLOY_ENVS = new Set(["development", "staging", "production"]);
|
|
@@ -3872,11 +3964,209 @@ var MAX_SIZE_PER_BATCH = 30 * 1024 * 1024;
|
|
|
3872
3964
|
function shellQuote(value) {
|
|
3873
3965
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
3874
3966
|
}
|
|
3967
|
+
function isRecord(value) {
|
|
3968
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3969
|
+
}
|
|
3970
|
+
function pickString2(value) {
|
|
3971
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
3972
|
+
}
|
|
3973
|
+
function pickNumber(value) {
|
|
3974
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
3975
|
+
}
|
|
3875
3976
|
function isFullstackProject() {
|
|
3876
|
-
return
|
|
3977
|
+
return existsSync5("app") && existsSync5("workers/app.ts") && existsSync5("wrangler.jsonc");
|
|
3978
|
+
}
|
|
3979
|
+
function resolveDeployUrl(env, overrides) {
|
|
3980
|
+
let deployUrl = DEPLOY_URLS[env];
|
|
3981
|
+
if (!deployUrl) {
|
|
3982
|
+
throw new Error(`Unknown environment "${env}". Available: ${Object.keys(DEPLOY_URLS).join(", ")}`);
|
|
3983
|
+
}
|
|
3984
|
+
if (overrides.host || overrides.port) {
|
|
3985
|
+
const url = new URL(deployUrl);
|
|
3986
|
+
if (overrides.host)
|
|
3987
|
+
url.hostname = overrides.host;
|
|
3988
|
+
if (overrides.port)
|
|
3989
|
+
url.port = String(overrides.port);
|
|
3990
|
+
deployUrl = url.toString().replace(/\/$/, "");
|
|
3991
|
+
}
|
|
3992
|
+
return deployUrl;
|
|
3993
|
+
}
|
|
3994
|
+
function resolveScriptsSetupUrl(deployUrl) {
|
|
3995
|
+
return deployUrl.replace("/webapps/deploy", "/webapps/scripts-setup");
|
|
3996
|
+
}
|
|
3997
|
+
function resolveConfigUrlFromDeployUrl(deployUrl) {
|
|
3998
|
+
return deployUrl.replace("/webapps/deploy", "/webapps/config");
|
|
3999
|
+
}
|
|
4000
|
+
function resolveWebClientBaseUrl(env) {
|
|
4001
|
+
return WEB_CLIENT_URLS[env];
|
|
4002
|
+
}
|
|
4003
|
+
function buildPublicWebappUrl(env, webappId) {
|
|
4004
|
+
const baseUrl = resolveWebClientBaseUrl(env);
|
|
4005
|
+
if (!baseUrl)
|
|
4006
|
+
return;
|
|
4007
|
+
const url = new URL(`/webapp/${encodeURIComponent(webappId)}`, baseUrl);
|
|
4008
|
+
return url.toString();
|
|
4009
|
+
}
|
|
4010
|
+
function encodeEndpointPath(endpointPath) {
|
|
4011
|
+
return endpointPath.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
|
|
4012
|
+
}
|
|
4013
|
+
function buildDynamicEndpointUrl(env, webappId, endpointPath) {
|
|
4014
|
+
const baseUrl = resolveWebClientBaseUrl(env);
|
|
4015
|
+
if (!baseUrl)
|
|
4016
|
+
return;
|
|
4017
|
+
const encodedEndpoint = encodeEndpointPath(endpointPath);
|
|
4018
|
+
const url = new URL(`/api/webapps/${encodeURIComponent(webappId)}/endpoints/${encodedEndpoint}`, baseUrl);
|
|
4019
|
+
return url.toString();
|
|
4020
|
+
}
|
|
4021
|
+
function normalizeBuildRelativePath(filePath) {
|
|
4022
|
+
return filePath.substring(BUILD_DIR.length + 1).replace(/\\/g, "/");
|
|
4023
|
+
}
|
|
4024
|
+
function isScriptFile(filePath) {
|
|
4025
|
+
const relativePath = normalizeBuildRelativePath(filePath);
|
|
4026
|
+
if (relativePath === "cron-jobs/cron.config.json")
|
|
4027
|
+
return true;
|
|
4028
|
+
return (relativePath.startsWith("api/") || relativePath.startsWith("cron-jobs/")) && relativePath.endsWith(".js");
|
|
4029
|
+
}
|
|
4030
|
+
function isMcpEndpoint(endpointPath) {
|
|
4031
|
+
const segments = endpointPath.split("/").filter(Boolean);
|
|
4032
|
+
return segments[segments.length - 1]?.toLowerCase() === "mcp";
|
|
4033
|
+
}
|
|
4034
|
+
function resolveCronScriptCode(payload, scriptName) {
|
|
4035
|
+
const scriptNameKey = scriptName.replace(/\.js$/, "");
|
|
4036
|
+
return payload.cron[scriptNameKey] || payload.cron[scriptName];
|
|
4037
|
+
}
|
|
4038
|
+
function readCronConfig(filePath, content) {
|
|
4039
|
+
const parsed = JSON.parse(content);
|
|
4040
|
+
if (!isRecord(parsed)) {
|
|
4041
|
+
throw new Error(`Cron config must be a JSON object: ${filePath}`);
|
|
4042
|
+
}
|
|
4043
|
+
const jobs = parsed.jobs;
|
|
4044
|
+
if (jobs !== undefined && !Array.isArray(jobs)) {
|
|
4045
|
+
throw new Error(`Cron config jobs must be an array: ${filePath}`);
|
|
4046
|
+
}
|
|
4047
|
+
return parsed;
|
|
4048
|
+
}
|
|
4049
|
+
function collectScripts(scriptFiles) {
|
|
4050
|
+
const payload = { api: {}, cron: {}, cronConfig: null };
|
|
4051
|
+
const summary = { apiEndpoints: [], cronJobs: [], mcpEndpoints: [] };
|
|
4052
|
+
for (const filePath of scriptFiles) {
|
|
4053
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
4054
|
+
const relativePath = normalizeBuildRelativePath(filePath);
|
|
4055
|
+
if (relativePath === "cron-jobs/cron.config.json") {
|
|
4056
|
+
payload.cronConfig = readCronConfig(filePath, content);
|
|
4057
|
+
summary.cronJobs = payload.cronConfig.jobs ?? [];
|
|
4058
|
+
} else if (relativePath.startsWith("api/")) {
|
|
4059
|
+
const endpoint = relativePath.substring(4, relativePath.lastIndexOf(".js"));
|
|
4060
|
+
payload.api[endpoint] = content;
|
|
4061
|
+
summary.apiEndpoints.push(endpoint);
|
|
4062
|
+
if (isMcpEndpoint(endpoint)) {
|
|
4063
|
+
summary.mcpEndpoints.push(endpoint);
|
|
4064
|
+
}
|
|
4065
|
+
} else if (relativePath.startsWith("cron-jobs/")) {
|
|
4066
|
+
const scriptName = relativePath.substring(10);
|
|
4067
|
+
payload.cron[scriptName] = content;
|
|
4068
|
+
}
|
|
4069
|
+
}
|
|
4070
|
+
summary.cronJobs = summary.cronJobs.filter((job) => {
|
|
4071
|
+
const hasScriptCode = !!resolveCronScriptCode(payload, job.script);
|
|
4072
|
+
if (!hasScriptCode) {
|
|
4073
|
+
console.warn(`⚠️ Cron job "${job.name}" references missing script "${job.script}" and will not be registered.`);
|
|
4074
|
+
}
|
|
4075
|
+
return hasScriptCode;
|
|
4076
|
+
});
|
|
4077
|
+
return { payload, summary };
|
|
4078
|
+
}
|
|
4079
|
+
function extractDeploymentWebapp(result) {
|
|
4080
|
+
if (!isRecord(result) || !isRecord(result.webapp))
|
|
4081
|
+
return {};
|
|
4082
|
+
return {
|
|
4083
|
+
id: pickString2(result.webapp.id),
|
|
4084
|
+
title: pickString2(result.webapp.title),
|
|
4085
|
+
appUrl: pickString2(result.webapp.appUrl)
|
|
4086
|
+
};
|
|
4087
|
+
}
|
|
4088
|
+
function printScriptsSetupCounts(result) {
|
|
4089
|
+
if (!isRecord(result))
|
|
4090
|
+
return;
|
|
4091
|
+
const parts = [
|
|
4092
|
+
["API registered", pickNumber(result.apiCreated)],
|
|
4093
|
+
["API deleted", pickNumber(result.apiDeleted)],
|
|
4094
|
+
["Cron registered", pickNumber(result.cronCreated)],
|
|
4095
|
+
["Cron deleted", pickNumber(result.cronDeleted)],
|
|
4096
|
+
["Cache invalidated", pickNumber(result.cacheInvalidated)]
|
|
4097
|
+
].filter((entry) => entry[1] !== undefined).map(([label, count]) => `${label}: ${count}`);
|
|
4098
|
+
if (parts.length > 0) {
|
|
4099
|
+
console.log(` ${parts.join(" · ")}`);
|
|
4100
|
+
}
|
|
4101
|
+
}
|
|
4102
|
+
function printPostDeploySummary(options) {
|
|
4103
|
+
const webapp = extractDeploymentWebapp(options.deploymentResult);
|
|
4104
|
+
const webappId = webapp.id || process.env.WEBAPP_ID;
|
|
4105
|
+
console.log(`
|
|
4106
|
+
\uD83D\uDD17 Deployment URLs`);
|
|
4107
|
+
if (webappId) {
|
|
4108
|
+
const appUrl = buildPublicWebappUrl(options.env, webappId);
|
|
4109
|
+
if (appUrl) {
|
|
4110
|
+
console.log(`App URL: ${appUrl}`);
|
|
4111
|
+
}
|
|
4112
|
+
} else {
|
|
4113
|
+
console.log("App URL: unavailable (WEBAPP_ID was not found)");
|
|
4114
|
+
}
|
|
4115
|
+
if (webapp.appUrl) {
|
|
4116
|
+
console.log(`Published asset URL: ${webapp.appUrl}`);
|
|
4117
|
+
}
|
|
4118
|
+
if (options.scriptsSummary.apiEndpoints.length > 0) {
|
|
4119
|
+
console.log(`
|
|
4120
|
+
\uD83C\uDF10 Dynamic endpoints`);
|
|
4121
|
+
for (const endpoint of options.scriptsSummary.apiEndpoints) {
|
|
4122
|
+
const url = webappId ? buildDynamicEndpointUrl(options.env, webappId, endpoint) : undefined;
|
|
4123
|
+
console.log(`- ${endpoint}${url ? `: ${url}` : ""}`);
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4126
|
+
if (options.scriptsSummary.mcpEndpoints.length > 0) {
|
|
4127
|
+
console.log(`
|
|
4128
|
+
\uD83E\uDDE9 MCP server URLs`);
|
|
4129
|
+
for (const endpoint of options.scriptsSummary.mcpEndpoints) {
|
|
4130
|
+
const url = webappId ? buildDynamicEndpointUrl(options.env, webappId, endpoint) : undefined;
|
|
4131
|
+
console.log(`- ${endpoint}${url ? `: ${url}` : ""}`);
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
if (options.scriptsSummary.cronJobs.length > 0) {
|
|
4135
|
+
console.log(`
|
|
4136
|
+
⏱️ Cron jobs`);
|
|
4137
|
+
for (const job of options.scriptsSummary.cronJobs) {
|
|
4138
|
+
const status = job.enabled === false ? "disabled" : "enabled";
|
|
4139
|
+
console.log(`- ${job.name}: ${job.schedule} (${status}, ${job.script})`);
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
}
|
|
4143
|
+
async function syncScripts(deployUrl, scriptFiles, stepLabel) {
|
|
4144
|
+
const { payload, summary } = collectScripts(scriptFiles);
|
|
4145
|
+
if (scriptFiles.length === 0) {
|
|
4146
|
+
console.log(`\uD83D\uDCDC ${stepLabel}: No scripts found to sync.`);
|
|
4147
|
+
return { summary, result: undefined };
|
|
4148
|
+
}
|
|
4149
|
+
console.log(`\uD83D\uDCDC ${stepLabel}: Registering ${scriptFiles.length} scripts (APIs & Crons)...`);
|
|
4150
|
+
const scriptsUrl = resolveScriptsSetupUrl(deployUrl);
|
|
4151
|
+
const response = await fetch(scriptsUrl, {
|
|
4152
|
+
method: "POST",
|
|
4153
|
+
headers: {
|
|
4154
|
+
Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
|
|
4155
|
+
"Content-Type": "application/json"
|
|
4156
|
+
},
|
|
4157
|
+
body: JSON.stringify(payload)
|
|
4158
|
+
});
|
|
4159
|
+
const result = await readResponsePayload(response);
|
|
4160
|
+
if (!response.ok) {
|
|
4161
|
+
throw new Error(`Scripts setup failed: ${response.status} ${response.statusText}
|
|
4162
|
+
${JSON.stringify(result, null, 2)}`);
|
|
4163
|
+
}
|
|
4164
|
+
console.log("✅ Scripts registered successfully");
|
|
4165
|
+
printScriptsSetupCounts(result);
|
|
4166
|
+
return { summary, result };
|
|
3877
4167
|
}
|
|
3878
4168
|
function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
3879
|
-
if (!
|
|
4169
|
+
if (!existsSync5(dirPath))
|
|
3880
4170
|
return arrayOfFiles;
|
|
3881
4171
|
const files = readdirSync(dirPath);
|
|
3882
4172
|
files.forEach(function(f) {
|
|
@@ -3890,7 +4180,7 @@ function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
|
3890
4180
|
return arrayOfFiles;
|
|
3891
4181
|
}
|
|
3892
4182
|
function fileToBlob(filePath) {
|
|
3893
|
-
const buffer =
|
|
4183
|
+
const buffer = readFileSync4(filePath);
|
|
3894
4184
|
return new Blob([buffer], { type: src_default.getType(filePath) || "application/octet-stream" });
|
|
3895
4185
|
}
|
|
3896
4186
|
async function deployFullstack(env, overrides) {
|
|
@@ -3911,6 +4201,7 @@ async function deployFullstack(env, overrides) {
|
|
|
3911
4201
|
...process.env,
|
|
3912
4202
|
CLOUDFLARE_ENV: env
|
|
3913
4203
|
};
|
|
4204
|
+
const deployUrl = resolveDeployUrl(env, overrides);
|
|
3914
4205
|
console.log(`\uD83D\uDE80 Starting fullstack deployment process for [${env}] environment...
|
|
3915
4206
|
`);
|
|
3916
4207
|
console.log("\uD83D\uDCE6 Step 1: Building the fullstack webapp...");
|
|
@@ -3923,30 +4214,22 @@ async function deployFullstack(env, overrides) {
|
|
|
3923
4214
|
console.log(`✅ Worker deployed
|
|
3924
4215
|
`);
|
|
3925
4216
|
console.log("\uD83D\uDCCB Step 3: Syncing widget manifest to CMS config...");
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
4217
|
+
await syncWidgetManifest({
|
|
4218
|
+
env,
|
|
4219
|
+
url: resolveConfigUrlFromDeployUrl(deployUrl)
|
|
4220
|
+
});
|
|
4221
|
+
console.log();
|
|
4222
|
+
const scriptFiles = getAllFiles(BUILD_DIR).filter(isScriptFile);
|
|
4223
|
+
const scriptsSync = await syncScripts(deployUrl, scriptFiles, "Step 4");
|
|
4224
|
+
printPostDeploySummary({
|
|
4225
|
+
env,
|
|
4226
|
+
scriptsSummary: scriptsSync.summary
|
|
4227
|
+
});
|
|
3933
4228
|
console.log(`
|
|
3934
4229
|
✨ Fullstack deployment successful!`);
|
|
3935
4230
|
}
|
|
3936
4231
|
async function deploySpa(env, overrides) {
|
|
3937
|
-
|
|
3938
|
-
if (!DEPLOY_URL) {
|
|
3939
|
-
console.error(`❌ Unknown environment "${env}". Available: ${Object.keys(DEPLOY_URLS).join(", ")}`);
|
|
3940
|
-
process.exit(1);
|
|
3941
|
-
}
|
|
3942
|
-
if (overrides.host || overrides.port) {
|
|
3943
|
-
const url = new URL(DEPLOY_URL);
|
|
3944
|
-
if (overrides.host)
|
|
3945
|
-
url.hostname = overrides.host;
|
|
3946
|
-
if (overrides.port)
|
|
3947
|
-
url.port = String(overrides.port);
|
|
3948
|
-
DEPLOY_URL = url.toString().replace(/\/$/, "");
|
|
3949
|
-
}
|
|
4232
|
+
const DEPLOY_URL = resolveDeployUrl(env, overrides);
|
|
3950
4233
|
const ASSETS_URL = DEPLOY_URL.replace("/webapps/deploy", "/webapps/assets");
|
|
3951
4234
|
console.log(`\uD83D\uDE80 Starting deployment process for [${env}] environment...
|
|
3952
4235
|
`);
|
|
@@ -3964,7 +4247,7 @@ async function deploySpa(env, overrides) {
|
|
|
3964
4247
|
`);
|
|
3965
4248
|
const allFiles = getAllFiles(BUILD_DIR);
|
|
3966
4249
|
const htmlFiles = allFiles.filter((f) => f.endsWith(".html"));
|
|
3967
|
-
const scriptFiles = allFiles.filter(
|
|
4250
|
+
const scriptFiles = allFiles.filter(isScriptFile);
|
|
3968
4251
|
const heavyFiles = allFiles.filter((f) => !f.endsWith(".html") && !scriptFiles.includes(f));
|
|
3969
4252
|
console.log(`\uD83D\uDCE4 Step 2: Uploading ${heavyFiles.length} heavy assets...`);
|
|
3970
4253
|
if (heavyFiles.length > 0) {
|
|
@@ -4012,42 +4295,7 @@ ${errorText}`);
|
|
|
4012
4295
|
console.log("✅ No heavy assets to upload");
|
|
4013
4296
|
}
|
|
4014
4297
|
console.log();
|
|
4015
|
-
|
|
4016
|
-
console.log(`\uD83D\uDCDC Step 3: Setting up ${scriptFiles.length} scripts (APIs & Crons)...`);
|
|
4017
|
-
const scriptsPayload = { api: {}, cron: {}, cronConfig: null };
|
|
4018
|
-
for (const f of scriptFiles) {
|
|
4019
|
-
const content = readFileSync3(f, "utf-8");
|
|
4020
|
-
const relativePath = f.substring(BUILD_DIR.length + 1).replace(/\\/g, "/");
|
|
4021
|
-
if (relativePath === "cron-jobs/cron.config.json") {
|
|
4022
|
-
scriptsPayload.cronConfig = JSON.parse(content);
|
|
4023
|
-
} else if (relativePath.startsWith("api/")) {
|
|
4024
|
-
const endpoint = relativePath.substring(4, relativePath.lastIndexOf(".js"));
|
|
4025
|
-
scriptsPayload.api[endpoint] = content;
|
|
4026
|
-
} else if (relativePath.startsWith("cron-jobs/")) {
|
|
4027
|
-
const scriptName = relativePath.substring(10);
|
|
4028
|
-
scriptsPayload.cron[scriptName] = content;
|
|
4029
|
-
}
|
|
4030
|
-
}
|
|
4031
|
-
const scriptsUrl = DEPLOY_URL.replace("/webapps/deploy", "/webapps/scripts-setup");
|
|
4032
|
-
const response = await fetch(scriptsUrl, {
|
|
4033
|
-
method: "POST",
|
|
4034
|
-
headers: {
|
|
4035
|
-
Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
|
|
4036
|
-
"Content-Type": "application/json"
|
|
4037
|
-
},
|
|
4038
|
-
body: JSON.stringify(scriptsPayload)
|
|
4039
|
-
});
|
|
4040
|
-
if (!response.ok) {
|
|
4041
|
-
const errorText = await response.text();
|
|
4042
|
-
throw new Error(`Scripts setup failed: ${response.status} ${response.statusText}
|
|
4043
|
-
${errorText}`);
|
|
4044
|
-
}
|
|
4045
|
-
console.log(`✅ Scripts synced successfully`);
|
|
4046
|
-
} else {
|
|
4047
|
-
console.log(`\uD83D\uDCDC Step 3: No scripts found to sync.`);
|
|
4048
|
-
}
|
|
4049
|
-
console.log();
|
|
4050
|
-
console.log(`\uD83D\uDDDC️ Step 4: Zipping ${htmlFiles.length} HTML files...`);
|
|
4298
|
+
console.log(`\uD83D\uDDDC️ Step 3: Zipping ${htmlFiles.length} HTML files...`);
|
|
4051
4299
|
if (htmlFiles.length === 0) {
|
|
4052
4300
|
console.warn("⚠️ No HTML files found to zip! Deployment might fail if CMS expects an HTML file.");
|
|
4053
4301
|
}
|
|
@@ -4055,8 +4303,8 @@ ${errorText}`);
|
|
|
4055
4303
|
execSync2(`cd ${BUILD_DIR} && zip ../${ZIP_FILE} ${relativeHtmlFiles.join(" ")}`, { stdio: "inherit" });
|
|
4056
4304
|
console.log(`✅ Created ${ZIP_FILE}
|
|
4057
4305
|
`);
|
|
4058
|
-
console.log("☁️ Step
|
|
4059
|
-
const zipBuffer =
|
|
4306
|
+
console.log("☁️ Step 4: Deploying HTML zip to server...");
|
|
4307
|
+
const zipBuffer = readFileSync4(ZIP_FILE);
|
|
4060
4308
|
try {
|
|
4061
4309
|
const response = await fetch(DEPLOY_URL, {
|
|
4062
4310
|
method: "POST",
|
|
@@ -4071,15 +4319,22 @@ ${errorText}`);
|
|
|
4071
4319
|
throw new Error(`Deploy failed: ${response.status} ${response.statusText}
|
|
4072
4320
|
${errorText}`);
|
|
4073
4321
|
}
|
|
4074
|
-
const result = await response
|
|
4322
|
+
const result = await readResponsePayload(response);
|
|
4075
4323
|
console.log("✅ Deploy completed");
|
|
4076
4324
|
console.log(`
|
|
4077
4325
|
\uD83D\uDCCB Deployment result:`, result);
|
|
4326
|
+
console.log();
|
|
4327
|
+
const scriptsSync = await syncScripts(DEPLOY_URL, scriptFiles, "Step 5");
|
|
4328
|
+
printPostDeploySummary({
|
|
4329
|
+
env,
|
|
4330
|
+
deploymentResult: result,
|
|
4331
|
+
scriptsSummary: scriptsSync.summary
|
|
4332
|
+
});
|
|
4078
4333
|
} catch (error) {
|
|
4079
4334
|
console.error("❌ Deploy failed:", error);
|
|
4080
4335
|
throw error;
|
|
4081
4336
|
} finally {
|
|
4082
|
-
if (
|
|
4337
|
+
if (existsSync5(ZIP_FILE)) {
|
|
4083
4338
|
unlinkSync(ZIP_FILE);
|
|
4084
4339
|
console.log(`
|
|
4085
4340
|
\uD83E\uDDF9 Cleaned up ${ZIP_FILE}`);
|
|
@@ -4098,8 +4353,8 @@ async function deploy(env = "development", overrides = {}) {
|
|
|
4098
4353
|
}
|
|
4099
4354
|
|
|
4100
4355
|
// src/global-config.ts
|
|
4101
|
-
import { existsSync as
|
|
4102
|
-
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";
|
|
4103
4358
|
var PROD_ENV = "production";
|
|
4104
4359
|
function isPlainObject(value) {
|
|
4105
4360
|
return !!value && typeof value === "object" && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
|
|
@@ -4120,8 +4375,8 @@ function applyMergePatch(target, patch) {
|
|
|
4120
4375
|
function parseFlag(name, value) {
|
|
4121
4376
|
if (value === "null")
|
|
4122
4377
|
return null;
|
|
4123
|
-
const candidatePath =
|
|
4124
|
-
const raw =
|
|
4378
|
+
const candidatePath = resolve2(process.cwd(), value);
|
|
4379
|
+
const raw = existsSync6(candidatePath) ? readFileSync5(candidatePath, "utf-8") : value;
|
|
4125
4380
|
try {
|
|
4126
4381
|
return JSON.parse(raw);
|
|
4127
4382
|
} catch (error) {
|
|
@@ -4316,7 +4571,7 @@ ${pretty(payload)}`);
|
|
|
4316
4571
|
}
|
|
4317
4572
|
const text = pretty(payload);
|
|
4318
4573
|
if (options.out) {
|
|
4319
|
-
const outPath =
|
|
4574
|
+
const outPath = resolve2(process.cwd(), options.out);
|
|
4320
4575
|
writeFileSync3(outPath, `${text}
|
|
4321
4576
|
`, "utf-8");
|
|
4322
4577
|
console.log(`✅ Wrote global config to ${outPath}`);
|
|
@@ -4412,21 +4667,21 @@ async function patchGlobalConfig(options) {
|
|
|
4412
4667
|
}
|
|
4413
4668
|
|
|
4414
4669
|
// src/promote.ts
|
|
4415
|
-
import { existsSync as
|
|
4416
|
-
import { resolve as
|
|
4670
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "node:fs";
|
|
4671
|
+
import { resolve as resolve4 } from "node:path";
|
|
4417
4672
|
|
|
4418
4673
|
// src/update-webapp-config.ts
|
|
4419
|
-
import { existsSync as
|
|
4420
|
-
import { resolve as
|
|
4421
|
-
var
|
|
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 = {
|
|
4422
4677
|
local: "http://localhost:5176/api/webapps/config",
|
|
4423
4678
|
development: "https://development-cms.rodyssey.ai/api/webapps/config",
|
|
4424
4679
|
staging: "https://staging-cms.rodyssey.ai/api/webapps/config",
|
|
4425
4680
|
production: "https://cms.rodyssey.ai/api/webapps/config"
|
|
4426
4681
|
};
|
|
4427
4682
|
function parseJsonOption(value, optionName) {
|
|
4428
|
-
const maybePath =
|
|
4429
|
-
const raw =
|
|
4683
|
+
const maybePath = resolve3(process.cwd(), value);
|
|
4684
|
+
const raw = existsSync7(maybePath) ? readFileSync6(maybePath, "utf-8") : value;
|
|
4430
4685
|
try {
|
|
4431
4686
|
const parsed = JSON.parse(raw);
|
|
4432
4687
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -4446,12 +4701,12 @@ function coerceMaybeNull(value) {
|
|
|
4446
4701
|
return value;
|
|
4447
4702
|
}
|
|
4448
4703
|
function resolveConfigUrl(options, required = true) {
|
|
4449
|
-
const configUrl = options.url || process.env.WEBAPP_CONFIG_URL ||
|
|
4704
|
+
const configUrl = options.url || process.env.WEBAPP_CONFIG_URL || CONFIG_URLS2[options.env];
|
|
4450
4705
|
if (!configUrl) {
|
|
4451
4706
|
if (!required)
|
|
4452
4707
|
return;
|
|
4453
4708
|
console.error("❌ Error: no webapp config endpoint configured.");
|
|
4454
|
-
console.error(`\uD83D\uDCA1 Use one of these environments: ${Object.keys(
|
|
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.`);
|
|
4455
4710
|
process.exit(1);
|
|
4456
4711
|
}
|
|
4457
4712
|
const url = new URL(configUrl);
|
|
@@ -4480,7 +4735,7 @@ function buildDetailsPayload(options) {
|
|
|
4480
4735
|
}
|
|
4481
4736
|
return payload;
|
|
4482
4737
|
}
|
|
4483
|
-
function
|
|
4738
|
+
function resolveWebappId2(webappId) {
|
|
4484
4739
|
const resolved = webappId || process.env.WEBAPP_ID;
|
|
4485
4740
|
if (!resolved) {
|
|
4486
4741
|
console.error("❌ Error: WEBAPP_ID is not set. Pass --webapp-id or set it in your .env file.");
|
|
@@ -4488,7 +4743,7 @@ function resolveWebappId(webappId) {
|
|
|
4488
4743
|
}
|
|
4489
4744
|
return resolved;
|
|
4490
4745
|
}
|
|
4491
|
-
function
|
|
4746
|
+
function ensureDeployToken2(env) {
|
|
4492
4747
|
if (process.env.DEPLOY_TOKEN)
|
|
4493
4748
|
return;
|
|
4494
4749
|
console.error("❌ Error: DEPLOY_TOKEN is not set in environment variables.");
|
|
@@ -4500,12 +4755,12 @@ function getConfigUrl(options, required = true) {
|
|
|
4500
4755
|
}
|
|
4501
4756
|
async function fetchWebappConfig(options) {
|
|
4502
4757
|
loadEnv(options.env);
|
|
4503
|
-
const webappId =
|
|
4758
|
+
const webappId = resolveWebappId2(options.webappId);
|
|
4504
4759
|
const CONFIG_URL = getConfigUrl(options);
|
|
4505
4760
|
if (!CONFIG_URL) {
|
|
4506
4761
|
throw new Error("No webapp config endpoint configured.");
|
|
4507
4762
|
}
|
|
4508
|
-
|
|
4763
|
+
ensureDeployToken2(options.env);
|
|
4509
4764
|
const url = new URL(CONFIG_URL);
|
|
4510
4765
|
url.searchParams.set("webappId", webappId);
|
|
4511
4766
|
const response = await fetch(url, {
|
|
@@ -4524,7 +4779,7 @@ ${errorText}`);
|
|
|
4524
4779
|
}
|
|
4525
4780
|
async function getWebappConfig(options) {
|
|
4526
4781
|
loadEnv(options.env);
|
|
4527
|
-
const webappId =
|
|
4782
|
+
const webappId = resolveWebappId2(options.webappId);
|
|
4528
4783
|
const CONFIG_URL = getConfigUrl(options);
|
|
4529
4784
|
if (!CONFIG_URL)
|
|
4530
4785
|
return;
|
|
@@ -4546,7 +4801,7 @@ async function getWebappConfig(options) {
|
|
|
4546
4801
|
}
|
|
4547
4802
|
async function updateWebappConfig(options) {
|
|
4548
4803
|
loadEnv(options.env);
|
|
4549
|
-
const webappId =
|
|
4804
|
+
const webappId = resolveWebappId2(options.webappId);
|
|
4550
4805
|
const details = buildDetailsPayload(options);
|
|
4551
4806
|
if (Object.keys(details).length === 0) {
|
|
4552
4807
|
console.error("❌ Error: no detail fields provided. Use --title, --description, --cover-img, --localization, or --details.");
|
|
@@ -4555,7 +4810,7 @@ async function updateWebappConfig(options) {
|
|
|
4555
4810
|
const payload = { webappId, details };
|
|
4556
4811
|
const CONFIG_URL = getConfigUrl(options, !options.dryRun);
|
|
4557
4812
|
if (!options.dryRun)
|
|
4558
|
-
|
|
4813
|
+
ensureDeployToken2(options.env);
|
|
4559
4814
|
console.log(`⚙️ Updating webapp config for [${options.env}] environment...`);
|
|
4560
4815
|
if (CONFIG_URL) {
|
|
4561
4816
|
console.log(`\uD83D\uDCCD Config URL: ${CONFIG_URL}`);
|
|
@@ -4599,7 +4854,7 @@ var PROD_ENV_FILE = ".env.production";
|
|
|
4599
4854
|
function isObject3(value) {
|
|
4600
4855
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
4601
4856
|
}
|
|
4602
|
-
function
|
|
4857
|
+
function pickString3(...values) {
|
|
4603
4858
|
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
4604
4859
|
}
|
|
4605
4860
|
function nestedObject2(value, key) {
|
|
@@ -4610,7 +4865,7 @@ function nestedObject2(value, key) {
|
|
|
4610
4865
|
}
|
|
4611
4866
|
function extractDeployToken2(payload) {
|
|
4612
4867
|
const webapp = nestedObject2(payload, "webapp");
|
|
4613
|
-
return
|
|
4868
|
+
return pickString3(webapp?.deployToken, webapp?.deploymentToken);
|
|
4614
4869
|
}
|
|
4615
4870
|
function unwrapSourceDetails(payload) {
|
|
4616
4871
|
if (!isObject3(payload))
|
|
@@ -4621,8 +4876,8 @@ function unwrapSourceDetails(payload) {
|
|
|
4621
4876
|
return payload;
|
|
4622
4877
|
}
|
|
4623
4878
|
function parseDetailsOption(value) {
|
|
4624
|
-
const maybePath =
|
|
4625
|
-
const raw =
|
|
4879
|
+
const maybePath = resolve4(process.cwd(), value);
|
|
4880
|
+
const raw = existsSync8(maybePath) ? readFileSync7(maybePath, "utf-8") : value;
|
|
4626
4881
|
try {
|
|
4627
4882
|
const parsed = JSON.parse(raw);
|
|
4628
4883
|
if (!isObject3(parsed)) {
|
|
@@ -4711,9 +4966,9 @@ async function promote(options) {
|
|
|
4711
4966
|
console.info("\uD83D\uDCA1 Run `ro app create --auto` first, or set WEBAPP_ID manually.");
|
|
4712
4967
|
process.exit(1);
|
|
4713
4968
|
}
|
|
4714
|
-
const prodEnvPath =
|
|
4715
|
-
if (
|
|
4716
|
-
const content =
|
|
4969
|
+
const prodEnvPath = resolve4(process.cwd(), PROD_ENV_FILE);
|
|
4970
|
+
if (existsSync8(prodEnvPath)) {
|
|
4971
|
+
const content = readFileSync7(prodEnvPath, "utf-8");
|
|
4717
4972
|
if (/^WEBAPP_ID=.+/m.test(content)) {
|
|
4718
4973
|
console.error(`❌ Error: ${PROD_ENV_FILE} already has WEBAPP_ID. The app appears to be promoted already.`);
|
|
4719
4974
|
process.exit(1);
|
|
@@ -4797,10 +5052,11 @@ ${JSON.stringify(payload, null, 2)}`);
|
|
|
4797
5052
|
|
|
4798
5053
|
// src/upgrade-template.ts
|
|
4799
5054
|
import { execSync as execSync3 } from "node:child_process";
|
|
4800
|
-
import { existsSync as
|
|
5055
|
+
import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2, copyFileSync, rmSync as rmSync2 } from "node:fs";
|
|
4801
5056
|
import path3 from "node:path";
|
|
4802
5057
|
var TEMPLATES = {
|
|
4803
5058
|
webapp: {
|
|
5059
|
+
key: "webapp",
|
|
4804
5060
|
name: "webapp (SPA)",
|
|
4805
5061
|
repo: "https://github.com/airconcepts/webapp-template.git",
|
|
4806
5062
|
remoteName: "template",
|
|
@@ -4836,6 +5092,7 @@ var TEMPLATES = {
|
|
|
4836
5092
|
]
|
|
4837
5093
|
},
|
|
4838
5094
|
"webapp-fullstack": {
|
|
5095
|
+
key: "webapp-fullstack",
|
|
4839
5096
|
name: "webapp (Fullstack)",
|
|
4840
5097
|
repo: "https://github.com/airconcepts/webapp-template-fullstack.git",
|
|
4841
5098
|
remoteName: "template",
|
|
@@ -4850,20 +5107,31 @@ var TEMPLATES = {
|
|
|
4850
5107
|
]
|
|
4851
5108
|
}
|
|
4852
5109
|
};
|
|
4853
|
-
var
|
|
5110
|
+
var CLI_SCRIPT_DEFAULTS = {
|
|
4854
5111
|
"link-game-sdk": "bunx @rodyssey/cli@latest app update-game-sdk",
|
|
4855
5112
|
deploy: "bunx @rodyssey/cli@latest app deploy",
|
|
5113
|
+
"sync-widget-manifest": "bunx @rodyssey/cli@latest app sync-widget-manifest",
|
|
4856
5114
|
"get-webapp-config": "bunx @rodyssey/cli@latest app config get",
|
|
4857
5115
|
"update-webapp-config": "bunx @rodyssey/cli@latest app config set",
|
|
4858
5116
|
"upgrade-template": "bunx @rodyssey/cli@latest app upgrade-template"
|
|
4859
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
|
+
];
|
|
4860
5128
|
function detectTemplate() {
|
|
4861
|
-
if (
|
|
5129
|
+
if (existsSync9("app")) {
|
|
4862
5130
|
console.log(`\uD83D\uDD0D Detected fullstack template (found app/ directory)
|
|
4863
5131
|
`);
|
|
4864
5132
|
return TEMPLATES["webapp-fullstack"];
|
|
4865
5133
|
}
|
|
4866
|
-
if (
|
|
5134
|
+
if (existsSync9("src")) {
|
|
4867
5135
|
console.log(`\uD83D\uDD0D Detected SPA template (found src/ directory)
|
|
4868
5136
|
`);
|
|
4869
5137
|
return TEMPLATES["webapp"];
|
|
@@ -4872,26 +5140,64 @@ function detectTemplate() {
|
|
|
4872
5140
|
`);
|
|
4873
5141
|
return TEMPLATES["webapp"];
|
|
4874
5142
|
}
|
|
4875
|
-
function
|
|
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) {
|
|
4876
5188
|
const pkgPath = "package.json";
|
|
4877
|
-
if (!
|
|
5189
|
+
if (!existsSync9(pkgPath)) {
|
|
4878
5190
|
console.log("⚠️ No package.json found, skipping scripts update");
|
|
4879
5191
|
return;
|
|
4880
5192
|
}
|
|
4881
|
-
const pkg = JSON.parse(
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
if (pkg.scripts[name] !== cmd) {
|
|
4888
|
-
const action = pkg.scripts[name] ? "Updated" : "Added";
|
|
4889
|
-
pkg.scripts[name] = cmd;
|
|
4890
|
-
console.log(` \uD83D\uDCDD ${action} script: "${name}"`);
|
|
4891
|
-
updated = true;
|
|
4892
|
-
}
|
|
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}"`);
|
|
4893
5199
|
}
|
|
4894
|
-
if (updated) {
|
|
5200
|
+
if (result.updated) {
|
|
4895
5201
|
writeFileSync5(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
4896
5202
|
`, "utf-8");
|
|
4897
5203
|
console.log(`✅ package.json scripts updated
|
|
@@ -4901,6 +5207,73 @@ function updatePackageJsonScripts() {
|
|
|
4901
5207
|
`);
|
|
4902
5208
|
}
|
|
4903
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
|
+
}
|
|
4904
5277
|
function updateCliSkill() {
|
|
4905
5278
|
const cliRemote = "ro-cli";
|
|
4906
5279
|
const cliRepo = "https://github.com/airconcepts/ro-cli.git";
|
|
@@ -4916,7 +5289,7 @@ function updateCliSkill() {
|
|
|
4916
5289
|
}
|
|
4917
5290
|
execSync3(`git fetch ${cliRemote}`, { stdio: "inherit" });
|
|
4918
5291
|
execSync3(`git checkout ${cliRemote}/main -- skills/ro-cli/SKILL.md`, { stdio: "inherit" });
|
|
4919
|
-
if (
|
|
5292
|
+
if (existsSync9("skills/ro-cli/SKILL.md")) {
|
|
4920
5293
|
mkdirSync2(".agent/skills/ro-cli", { recursive: true });
|
|
4921
5294
|
copyFileSync("skills/ro-cli/SKILL.md", ".agent/skills/ro-cli/SKILL.md");
|
|
4922
5295
|
rmSync2("skills", { recursive: true, force: true });
|
|
@@ -4949,7 +5322,7 @@ async function upgradeTemplate() {
|
|
|
4949
5322
|
const checkoutList = template.checkoutFiles.join(" ");
|
|
4950
5323
|
execSync3(`git checkout ${template.remoteName}/main -- ${checkoutList}`, { stdio: "inherit" });
|
|
4951
5324
|
for (const file of template.newFiles) {
|
|
4952
|
-
if (!
|
|
5325
|
+
if (!existsSync9(file)) {
|
|
4953
5326
|
console.log(`\uD83D\uDCC2 Checking out ${file}...`);
|
|
4954
5327
|
try {
|
|
4955
5328
|
mkdirSync2(path3.dirname(file), { recursive: true });
|
|
@@ -4965,7 +5338,13 @@ async function upgradeTemplate() {
|
|
|
4965
5338
|
\uD83D\uDD27 Updating CLI skill documentation...`);
|
|
4966
5339
|
updateCliSkill();
|
|
4967
5340
|
console.log("\uD83D\uDCE6 Updating package.json scripts...");
|
|
4968
|
-
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
|
+
}
|
|
4969
5348
|
console.log("✅ Template upgrade complete! Please check git status for changes.");
|
|
4970
5349
|
} catch (error) {
|
|
4971
5350
|
console.error("❌ Upgrade failed:", error);
|
|
@@ -5148,15 +5527,15 @@ Available templates:
|
|
|
5148
5527
|
input: process.stdin,
|
|
5149
5528
|
output: process.stdout
|
|
5150
5529
|
});
|
|
5151
|
-
return new Promise((
|
|
5530
|
+
return new Promise((resolve5) => {
|
|
5152
5531
|
rl.question(`Select a template (1-${entries.length}): `, (answer) => {
|
|
5153
5532
|
rl.close();
|
|
5154
5533
|
const index = parseInt(answer, 10) - 1;
|
|
5155
5534
|
if (index >= 0 && index < entries.length) {
|
|
5156
|
-
|
|
5535
|
+
resolve5(entries[index].name);
|
|
5157
5536
|
} else {
|
|
5158
5537
|
console.error("Invalid selection, defaulting to 'webapp'");
|
|
5159
|
-
|
|
5538
|
+
resolve5("webapp");
|
|
5160
5539
|
}
|
|
5161
5540
|
});
|
|
5162
5541
|
});
|
|
@@ -5237,6 +5616,9 @@ app.command("update-game-sdk").description("Download and update the GameSDK libr
|
|
|
5237
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) => {
|
|
5238
5617
|
await deploy(options.env, { host: options.host, port: options.port });
|
|
5239
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
|
+
});
|
|
5240
5622
|
var config = app.command("config").description("Manage webapp metadata config");
|
|
5241
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) => {
|
|
5242
5624
|
await getWebappConfig(options);
|