@rodyssey/cli 0.1.4 → 0.1.6
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 +20 -0
- package/dist/cli.js +806 -37
- package/package.json +14 -2
package/README.md
CHANGED
|
@@ -12,4 +12,24 @@ To run:
|
|
|
12
12
|
bun run index.ts
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
## Release
|
|
16
|
+
|
|
17
|
+
Release automation lives in `.github/workflows/release.yml` and uses Changesets.
|
|
18
|
+
|
|
19
|
+
For a change that should publish a new CLI version:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
bun run changeset
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Choose `patch`, `minor`, or `major`, commit the generated `.changeset/*.md` file, and merge to `main`. The workflow opens or updates a release PR with the version bump and changelog. Merging that release PR publishes `@rodyssey/cli` to npm.
|
|
26
|
+
|
|
27
|
+
Configure npm trusted publishing for the package with:
|
|
28
|
+
|
|
29
|
+
- Organization/user: `airconcepts`
|
|
30
|
+
- Repository: `ro-cli`
|
|
31
|
+
- Workflow filename: `release.yml`
|
|
32
|
+
|
|
33
|
+
This repository is private, so npm provenance is disabled. If the repository becomes public later, change `publishConfig.provenance` and `NPM_CONFIG_PROVENANCE` back to `true`.
|
|
34
|
+
|
|
15
35
|
This project was created using `bun init` in bun v1.3.9. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
package/dist/cli.js
CHANGED
|
@@ -2068,20 +2068,305 @@ var {
|
|
|
2068
2068
|
Option,
|
|
2069
2069
|
Help
|
|
2070
2070
|
} = import__.default;
|
|
2071
|
+
// package.json
|
|
2072
|
+
var package_default = {
|
|
2073
|
+
name: "@rodyssey/cli",
|
|
2074
|
+
version: "0.1.6",
|
|
2075
|
+
description: "Scaffold new projects from airconcepts templates",
|
|
2076
|
+
repository: {
|
|
2077
|
+
type: "git",
|
|
2078
|
+
url: "git+https://github.com/airconcepts/ro-cli.git"
|
|
2079
|
+
},
|
|
2080
|
+
bin: {
|
|
2081
|
+
"@rodyssey/cli": "dist/cli.js"
|
|
2082
|
+
},
|
|
2083
|
+
files: [
|
|
2084
|
+
"dist"
|
|
2085
|
+
],
|
|
2086
|
+
type: "module",
|
|
2087
|
+
module: "index.ts",
|
|
2088
|
+
scripts: {
|
|
2089
|
+
build: "bun build src/cli.ts --outdir dist --target node",
|
|
2090
|
+
start: "bun dist/cli.js",
|
|
2091
|
+
changeset: "changeset",
|
|
2092
|
+
"version-packages": "changeset version",
|
|
2093
|
+
release: "bun run build && changeset publish",
|
|
2094
|
+
prepublishOnly: "bun run build"
|
|
2095
|
+
},
|
|
2096
|
+
publishConfig: {
|
|
2097
|
+
access: "public",
|
|
2098
|
+
provenance: false
|
|
2099
|
+
},
|
|
2100
|
+
dependencies: {
|
|
2101
|
+
commander: "^13.1.0",
|
|
2102
|
+
mime: "^4.1.0"
|
|
2103
|
+
},
|
|
2104
|
+
devDependencies: {
|
|
2105
|
+
"@changesets/cli": "^2.30.0",
|
|
2106
|
+
"@types/bun": "latest"
|
|
2107
|
+
},
|
|
2108
|
+
peerDependencies: {
|
|
2109
|
+
typescript: "^5"
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
|
|
2113
|
+
// src/auth.ts
|
|
2114
|
+
import { spawn } from "node:child_process";
|
|
2115
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2116
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2117
|
+
import { createServer } from "node:http";
|
|
2118
|
+
import { dirname, join } from "node:path";
|
|
2119
|
+
import { homedir } from "node:os";
|
|
2120
|
+
var CMS_BASE_URLS = {
|
|
2121
|
+
local: "http://localhost:5176",
|
|
2122
|
+
development: "https://development-cms.rodyssey.ai",
|
|
2123
|
+
staging: "https://staging-cms.rodyssey.ai",
|
|
2124
|
+
production: "https://cms.rodyssey.ai"
|
|
2125
|
+
};
|
|
2126
|
+
var CONFIG_FILE = join(homedir(), ".rodyssey", "config.json");
|
|
2127
|
+
function readConfig() {
|
|
2128
|
+
if (!existsSync(CONFIG_FILE))
|
|
2129
|
+
return {};
|
|
2130
|
+
try {
|
|
2131
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
2132
|
+
} catch {
|
|
2133
|
+
return {};
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
function writeConfig(config) {
|
|
2137
|
+
mkdirSync(dirname(CONFIG_FILE), { recursive: true });
|
|
2138
|
+
writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}
|
|
2139
|
+
`, "utf-8");
|
|
2140
|
+
}
|
|
2141
|
+
function getAuthConfig(config) {
|
|
2142
|
+
const auth = config.auth;
|
|
2143
|
+
if (!auth || typeof auth !== "object" || Array.isArray(auth))
|
|
2144
|
+
return {};
|
|
2145
|
+
return auth;
|
|
2146
|
+
}
|
|
2147
|
+
function isObject(value) {
|
|
2148
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2149
|
+
}
|
|
2150
|
+
function extractToken(payload) {
|
|
2151
|
+
if (!isObject(payload))
|
|
2152
|
+
return;
|
|
2153
|
+
const candidates = [
|
|
2154
|
+
payload.accessToken,
|
|
2155
|
+
payload.token,
|
|
2156
|
+
payload.jwt,
|
|
2157
|
+
payload.idToken,
|
|
2158
|
+
isObject(payload.session) ? payload.session.token : undefined,
|
|
2159
|
+
isObject(payload.session) ? payload.session.accessToken : undefined,
|
|
2160
|
+
isObject(payload.data) ? payload.data.accessToken : undefined,
|
|
2161
|
+
isObject(payload.data) ? payload.data.token : undefined
|
|
2162
|
+
];
|
|
2163
|
+
return candidates.find((candidate) => typeof candidate === "string" && candidate.length > 0);
|
|
2164
|
+
}
|
|
2165
|
+
function extractUser(payload) {
|
|
2166
|
+
if (!isObject(payload))
|
|
2167
|
+
return;
|
|
2168
|
+
return payload.user ?? (isObject(payload.data) ? payload.data.user : undefined);
|
|
2169
|
+
}
|
|
2170
|
+
async function readResponsePayload(response) {
|
|
2171
|
+
const text = await response.text();
|
|
2172
|
+
if (!text)
|
|
2173
|
+
return;
|
|
2174
|
+
try {
|
|
2175
|
+
return JSON.parse(text);
|
|
2176
|
+
} catch {
|
|
2177
|
+
return { message: text };
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
function base64Url(buffer) {
|
|
2181
|
+
return buffer.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
2182
|
+
}
|
|
2183
|
+
function buildPkcePair() {
|
|
2184
|
+
const verifier = base64Url(randomBytes(32));
|
|
2185
|
+
const challenge = base64Url(createHash("sha256").update(verifier).digest());
|
|
2186
|
+
return { verifier, challenge };
|
|
2187
|
+
}
|
|
2188
|
+
function openBrowser(url) {
|
|
2189
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
2190
|
+
const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
|
|
2191
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
2192
|
+
child.unref();
|
|
2193
|
+
}
|
|
2194
|
+
function resolveCmsUrl(env, cmsUrl) {
|
|
2195
|
+
const resolved = cmsUrl || process.env.CMS_URL || CMS_BASE_URLS[env];
|
|
2196
|
+
if (!resolved) {
|
|
2197
|
+
throw new Error(`Unknown CMS environment "${env}". Available: ${Object.keys(CMS_BASE_URLS).join(", ")}`);
|
|
2198
|
+
}
|
|
2199
|
+
return resolved.replace(/\/$/, "");
|
|
2200
|
+
}
|
|
2201
|
+
function getStoredSession(env) {
|
|
2202
|
+
return getAuthConfig(readConfig())[env];
|
|
2203
|
+
}
|
|
2204
|
+
function storeSession(session) {
|
|
2205
|
+
const config = readConfig();
|
|
2206
|
+
const auth = getAuthConfig(config);
|
|
2207
|
+
auth[session.env] = session;
|
|
2208
|
+
config.auth = auth;
|
|
2209
|
+
writeConfig(config);
|
|
2210
|
+
}
|
|
2211
|
+
function resolveSessionToken(env) {
|
|
2212
|
+
const token = process.env.CMS_TOKEN || getStoredSession(env)?.token;
|
|
2213
|
+
if (!token) {
|
|
2214
|
+
throw new Error(`No CMS auth token found for [${env}]. Run \`ro app auth login --env ${env}\` first.`);
|
|
2215
|
+
}
|
|
2216
|
+
return token;
|
|
2217
|
+
}
|
|
2218
|
+
async function login(options) {
|
|
2219
|
+
const cmsUrl = resolveCmsUrl(options.env, options.cmsUrl);
|
|
2220
|
+
const state = base64Url(randomBytes(24));
|
|
2221
|
+
const pkce = buildPkcePair();
|
|
2222
|
+
const timeoutMs = options.timeoutMs ?? 5 * 60 * 1000;
|
|
2223
|
+
const callback = await new Promise((resolve, reject) => {
|
|
2224
|
+
const server = createServer((request, response2) => {
|
|
2225
|
+
const requestUrl = new URL(request.url || "/", "http://127.0.0.1");
|
|
2226
|
+
if (requestUrl.pathname !== "/auth/callback") {
|
|
2227
|
+
response2.writeHead(404).end("Not found");
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
const code = requestUrl.searchParams.get("code");
|
|
2231
|
+
const returnedState = requestUrl.searchParams.get("state");
|
|
2232
|
+
const error = requestUrl.searchParams.get("error");
|
|
2233
|
+
if (error) {
|
|
2234
|
+
clearTimeout(timer);
|
|
2235
|
+
response2.writeHead(400, { "Content-Type": "text/plain" }).end(`Login failed: ${error}`);
|
|
2236
|
+
server.close();
|
|
2237
|
+
reject(new Error(`CMS login failed: ${error}`));
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
if (!code || returnedState !== state) {
|
|
2241
|
+
clearTimeout(timer);
|
|
2242
|
+
response2.writeHead(400, { "Content-Type": "text/plain" }).end("Invalid CLI login callback.");
|
|
2243
|
+
server.close();
|
|
2244
|
+
reject(new Error("Invalid CMS login callback. Missing code or state mismatch."));
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
response2.writeHead(200, { "Content-Type": "text/html" }).end('<!doctype html><meta charset="utf-8"><title>Rodyssey CLI</title><body><h1>Login complete</h1><p>You can close this window and return to the terminal.</p></body>');
|
|
2248
|
+
const address = server.address();
|
|
2249
|
+
clearTimeout(timer);
|
|
2250
|
+
server.close();
|
|
2251
|
+
resolve({ code, redirectUri: `http://127.0.0.1:${address.port}/auth/callback` });
|
|
2252
|
+
});
|
|
2253
|
+
const timer = setTimeout(() => {
|
|
2254
|
+
server.close();
|
|
2255
|
+
reject(new Error("CMS login timed out before the callback was received."));
|
|
2256
|
+
}, timeoutMs);
|
|
2257
|
+
server.on("error", (error) => {
|
|
2258
|
+
clearTimeout(timer);
|
|
2259
|
+
reject(error);
|
|
2260
|
+
});
|
|
2261
|
+
server.listen(options.callbackPort ?? 0, "127.0.0.1", () => {
|
|
2262
|
+
const address = server.address();
|
|
2263
|
+
const redirectUri = `http://127.0.0.1:${address.port}/auth/callback`;
|
|
2264
|
+
const authorizationUrl = new URL(options.loginUrl || `${cmsUrl}/auth/cli-login`);
|
|
2265
|
+
authorizationUrl.searchParams.set("redirect_uri", redirectUri);
|
|
2266
|
+
authorizationUrl.searchParams.set("state", state);
|
|
2267
|
+
authorizationUrl.searchParams.set("code_challenge", pkce.challenge);
|
|
2268
|
+
authorizationUrl.searchParams.set("code_challenge_method", "S256");
|
|
2269
|
+
console.log(`Open this URL to log in:
|
|
2270
|
+
${authorizationUrl.toString()}
|
|
2271
|
+
`);
|
|
2272
|
+
if (options.open !== false) {
|
|
2273
|
+
openBrowser(authorizationUrl.toString());
|
|
2274
|
+
}
|
|
2275
|
+
});
|
|
2276
|
+
});
|
|
2277
|
+
const tokenUrl = options.tokenUrl || `${cmsUrl}/api/auth/cli-token`;
|
|
2278
|
+
const response = await fetch(tokenUrl, {
|
|
2279
|
+
method: "POST",
|
|
2280
|
+
headers: {
|
|
2281
|
+
Accept: "application/json",
|
|
2282
|
+
"Content-Type": "application/json"
|
|
2283
|
+
},
|
|
2284
|
+
body: JSON.stringify({
|
|
2285
|
+
code: callback.code,
|
|
2286
|
+
redirect_uri: callback.redirectUri,
|
|
2287
|
+
code_verifier: pkce.verifier
|
|
2288
|
+
})
|
|
2289
|
+
});
|
|
2290
|
+
const payload = await readResponsePayload(response);
|
|
2291
|
+
if (!response.ok) {
|
|
2292
|
+
throw new Error(`CMS token exchange failed: ${response.status} ${response.statusText}
|
|
2293
|
+
${JSON.stringify(payload, null, 2)}`);
|
|
2294
|
+
}
|
|
2295
|
+
const token = extractToken(payload);
|
|
2296
|
+
if (!token) {
|
|
2297
|
+
throw new Error(`CMS token response did not include an auth token:
|
|
2298
|
+
${JSON.stringify(payload, null, 2)}`);
|
|
2299
|
+
}
|
|
2300
|
+
const session = {
|
|
2301
|
+
env: options.env,
|
|
2302
|
+
cmsUrl,
|
|
2303
|
+
token,
|
|
2304
|
+
user: extractUser(payload)
|
|
2305
|
+
};
|
|
2306
|
+
if (options.persist !== false) {
|
|
2307
|
+
storeSession(session);
|
|
2308
|
+
}
|
|
2309
|
+
return session;
|
|
2310
|
+
}
|
|
2311
|
+
async function me(options) {
|
|
2312
|
+
const storedSession = getStoredSession(options.env);
|
|
2313
|
+
if (!options.remote) {
|
|
2314
|
+
if (!storedSession) {
|
|
2315
|
+
throw new Error(`No local CMS session found for [${options.env}]. Run \`ro app auth login --env ${options.env}\` first.`);
|
|
2316
|
+
}
|
|
2317
|
+
return {
|
|
2318
|
+
env: storedSession.env,
|
|
2319
|
+
cmsUrl: storedSession.cmsUrl,
|
|
2320
|
+
loggedIn: true,
|
|
2321
|
+
user: storedSession.user ?? null
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
const cmsUrl = resolveCmsUrl(options.env, options.cmsUrl || storedSession?.cmsUrl);
|
|
2325
|
+
const response = await fetch(options.meUrl || `${cmsUrl}/api/auth/me`, {
|
|
2326
|
+
method: "GET",
|
|
2327
|
+
headers: {
|
|
2328
|
+
Accept: "application/json",
|
|
2329
|
+
Authorization: `Bearer ${resolveSessionToken(options.env)}`
|
|
2330
|
+
}
|
|
2331
|
+
});
|
|
2332
|
+
const payload = await readResponsePayload(response);
|
|
2333
|
+
if (!response.ok) {
|
|
2334
|
+
throw new Error(`CMS me failed: ${response.status} ${response.statusText}
|
|
2335
|
+
${JSON.stringify(payload, null, 2)}`);
|
|
2336
|
+
}
|
|
2337
|
+
return payload;
|
|
2338
|
+
}
|
|
2071
2339
|
|
|
2072
2340
|
// src/create.ts
|
|
2073
2341
|
import { execSync } from "node:child_process";
|
|
2074
|
-
import { existsSync as
|
|
2342
|
+
import { existsSync as existsSync3, rmSync } from "node:fs";
|
|
2075
2343
|
import path2 from "node:path";
|
|
2076
2344
|
|
|
2077
2345
|
// src/utils.ts
|
|
2078
|
-
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2346
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "node:fs";
|
|
2079
2347
|
import path from "node:path";
|
|
2348
|
+
function upsertEnvValue(content, key, value) {
|
|
2349
|
+
const line = `${key}=${value}`;
|
|
2350
|
+
const pattern = new RegExp(`^${key}=.*$`, "m");
|
|
2351
|
+
if (pattern.test(content))
|
|
2352
|
+
return content.replace(pattern, line);
|
|
2353
|
+
return `${content}${content.endsWith(`
|
|
2354
|
+
`) || content.length === 0 ? "" : `
|
|
2355
|
+
`}${line}
|
|
2356
|
+
`;
|
|
2357
|
+
}
|
|
2358
|
+
function upsertEnvFile(filePath, entries) {
|
|
2359
|
+
let content = existsSync2(filePath) ? readFileSync2(filePath, "utf-8") : "";
|
|
2360
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
2361
|
+
content = upsertEnvValue(content, key, value);
|
|
2362
|
+
}
|
|
2363
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
2364
|
+
}
|
|
2080
2365
|
function replaceInFile(filePath, search, replace) {
|
|
2081
|
-
const content =
|
|
2366
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
2082
2367
|
const updated = content.replaceAll(search, replace);
|
|
2083
2368
|
if (content !== updated) {
|
|
2084
|
-
|
|
2369
|
+
writeFileSync2(filePath, updated, "utf-8");
|
|
2085
2370
|
}
|
|
2086
2371
|
}
|
|
2087
2372
|
function replaceInFiles(dir, filenames, search, replace) {
|
|
@@ -2099,9 +2384,9 @@ function loadEnv(envName) {
|
|
|
2099
2384
|
}
|
|
2100
2385
|
files.push(".env");
|
|
2101
2386
|
for (const file of files) {
|
|
2102
|
-
if (!
|
|
2387
|
+
if (!existsSync2(file))
|
|
2103
2388
|
continue;
|
|
2104
|
-
const content =
|
|
2389
|
+
const content = readFileSync2(file, "utf-8");
|
|
2105
2390
|
for (const line of content.split(`
|
|
2106
2391
|
`)) {
|
|
2107
2392
|
const trimmed = line.trim();
|
|
@@ -2129,14 +2414,77 @@ var REPLACEMENT_FILES = {
|
|
|
2129
2414
|
webapp: ["package.json", "index.html"],
|
|
2130
2415
|
"webapp-fullstack": ["package.json", "wrangler.jsonc"]
|
|
2131
2416
|
};
|
|
2132
|
-
|
|
2417
|
+
function isObject2(value) {
|
|
2418
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2419
|
+
}
|
|
2420
|
+
function pickString(...values) {
|
|
2421
|
+
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
2422
|
+
}
|
|
2423
|
+
function nestedObject(value, key) {
|
|
2424
|
+
if (!isObject2(value))
|
|
2425
|
+
return;
|
|
2426
|
+
const nested = value[key];
|
|
2427
|
+
return isObject2(nested) ? nested : undefined;
|
|
2428
|
+
}
|
|
2429
|
+
function extractWebappId(payload) {
|
|
2430
|
+
const webapp = nestedObject(payload, "webapp");
|
|
2431
|
+
return pickString(webapp?.id, webapp?.webappId);
|
|
2432
|
+
}
|
|
2433
|
+
function extractDeployToken(payload) {
|
|
2434
|
+
const webapp = nestedObject(payload, "webapp");
|
|
2435
|
+
return pickString(webapp?.deployToken, webapp?.deploymentToken);
|
|
2436
|
+
}
|
|
2437
|
+
function writeProjectEnv(projectDir, provisioned) {
|
|
2438
|
+
upsertEnvFile(path2.join(projectDir, ".env"), {
|
|
2439
|
+
WEBAPP_ID: provisioned.webappId,
|
|
2440
|
+
DEPLOY_TOKEN: provisioned.deployToken
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2443
|
+
async function provisionWebapp(projectName, options) {
|
|
2444
|
+
const cmsUrl = resolveCmsUrl(options.env, options.cmsUrl);
|
|
2445
|
+
const token = resolveSessionToken(options.env);
|
|
2446
|
+
const createUrl = options.createUrl || `${cmsUrl}/api/cli/webapps/create`;
|
|
2447
|
+
const createResponse = await fetch(createUrl, {
|
|
2448
|
+
method: "POST",
|
|
2449
|
+
headers: {
|
|
2450
|
+
Accept: "application/json",
|
|
2451
|
+
Authorization: `Bearer ${token}`,
|
|
2452
|
+
"Content-Type": "application/json"
|
|
2453
|
+
},
|
|
2454
|
+
body: JSON.stringify({ title: projectName })
|
|
2455
|
+
});
|
|
2456
|
+
const createPayload = await readResponsePayload(createResponse);
|
|
2457
|
+
if (!createResponse.ok) {
|
|
2458
|
+
throw new Error(`CMS webapp creation failed: ${createResponse.status} ${createResponse.statusText}
|
|
2459
|
+
${JSON.stringify(createPayload, null, 2)}`);
|
|
2460
|
+
}
|
|
2461
|
+
const webappId = extractWebappId(createPayload);
|
|
2462
|
+
if (!webappId) {
|
|
2463
|
+
throw new Error(`CMS create response did not include webapp.id:
|
|
2464
|
+
${JSON.stringify(createPayload, null, 2)}`);
|
|
2465
|
+
}
|
|
2466
|
+
const deployToken = extractDeployToken(createPayload);
|
|
2467
|
+
if (!deployToken) {
|
|
2468
|
+
throw new Error(`CMS create response did not include webapp.deployToken:
|
|
2469
|
+
${JSON.stringify(createPayload, null, 2)}`);
|
|
2470
|
+
}
|
|
2471
|
+
return {
|
|
2472
|
+
webappId,
|
|
2473
|
+
deployToken
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
async function create(projectName, repoUrl, templateName, autoCreate) {
|
|
2133
2477
|
const targetDir = path2.resolve(process.cwd(), projectName);
|
|
2134
|
-
if (
|
|
2478
|
+
if (existsSync3(targetDir)) {
|
|
2135
2479
|
console.error(`
|
|
2136
2480
|
✖ Directory "${projectName}" already exists.
|
|
2137
2481
|
`);
|
|
2138
2482
|
process.exit(1);
|
|
2139
2483
|
}
|
|
2484
|
+
if (autoCreate?.enabled) {
|
|
2485
|
+
resolveCmsUrl(autoCreate.env, autoCreate.cmsUrl);
|
|
2486
|
+
resolveSessionToken(autoCreate.env);
|
|
2487
|
+
}
|
|
2140
2488
|
console.log(`
|
|
2141
2489
|
⏳ Cloning template "${templateName}"...
|
|
2142
2490
|
`);
|
|
@@ -2152,7 +2500,7 @@ async function create(projectName, repoUrl, templateName) {
|
|
|
2152
2500
|
process.exit(1);
|
|
2153
2501
|
}
|
|
2154
2502
|
const gitDir = path2.join(targetDir, ".git");
|
|
2155
|
-
if (
|
|
2503
|
+
if (existsSync3(gitDir)) {
|
|
2156
2504
|
rmSync(gitDir, { recursive: true, force: true });
|
|
2157
2505
|
}
|
|
2158
2506
|
const filesToReplace = REPLACEMENT_FILES[templateName] ?? [];
|
|
@@ -2162,6 +2510,13 @@ async function create(projectName, repoUrl, templateName) {
|
|
|
2162
2510
|
replaceInFiles(targetDir, filesToReplace, PLACEHOLDER_LOWER, projectName.toLowerCase());
|
|
2163
2511
|
}
|
|
2164
2512
|
execSync("git init", { stdio: "ignore", cwd: targetDir });
|
|
2513
|
+
if (autoCreate?.enabled) {
|
|
2514
|
+
console.log(` \uD83C\uDF10 Creating CMS webapp in [${autoCreate.env}]...`);
|
|
2515
|
+
const provisioned = await provisionWebapp(projectName, autoCreate);
|
|
2516
|
+
writeProjectEnv(targetDir, provisioned);
|
|
2517
|
+
console.log(` \uD83D\uDD10 Wrote WEBAPP_ID and DEPLOY_TOKEN to .env`);
|
|
2518
|
+
console.log(` \uD83D\uDCCD Webapp ID: ${provisioned.webappId}`);
|
|
2519
|
+
}
|
|
2165
2520
|
console.log(`
|
|
2166
2521
|
✅ Project "${projectName}" created successfully!
|
|
2167
2522
|
|
|
@@ -2175,8 +2530,8 @@ async function create(projectName, repoUrl, templateName) {
|
|
|
2175
2530
|
|
|
2176
2531
|
// src/deploy.ts
|
|
2177
2532
|
import { execSync as execSync2 } from "node:child_process";
|
|
2178
|
-
import { existsSync as
|
|
2179
|
-
import { join } from "node:path";
|
|
2533
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
2534
|
+
import { join as join2 } from "node:path";
|
|
2180
2535
|
|
|
2181
2536
|
// node_modules/mime/dist/types/other.js
|
|
2182
2537
|
var types = {
|
|
@@ -3373,11 +3728,11 @@ var ZIP_FILE = "webapp-build.zip";
|
|
|
3373
3728
|
var MAX_FILES_PER_BATCH = 5;
|
|
3374
3729
|
var MAX_SIZE_PER_BATCH = 30 * 1024 * 1024;
|
|
3375
3730
|
function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
3376
|
-
if (!
|
|
3731
|
+
if (!existsSync4(dirPath))
|
|
3377
3732
|
return arrayOfFiles;
|
|
3378
3733
|
const files = readdirSync(dirPath);
|
|
3379
3734
|
files.forEach(function(f) {
|
|
3380
|
-
const fullPath =
|
|
3735
|
+
const fullPath = join2(dirPath, f);
|
|
3381
3736
|
if (statSync(fullPath).isDirectory()) {
|
|
3382
3737
|
arrayOfFiles = getAllFiles(fullPath, arrayOfFiles);
|
|
3383
3738
|
} else {
|
|
@@ -3387,7 +3742,7 @@ function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
|
3387
3742
|
return arrayOfFiles;
|
|
3388
3743
|
}
|
|
3389
3744
|
function fileToBlob(filePath) {
|
|
3390
|
-
const buffer =
|
|
3745
|
+
const buffer = readFileSync3(filePath);
|
|
3391
3746
|
return new Blob([buffer], { type: src_default.getType(filePath) || "application/octet-stream" });
|
|
3392
3747
|
}
|
|
3393
3748
|
async function deploy(env = "development", overrides = {}) {
|
|
@@ -3474,7 +3829,7 @@ ${errorText}`);
|
|
|
3474
3829
|
console.log(`\uD83D\uDCDC Step 3: Setting up ${scriptFiles.length} scripts (APIs & Crons)...`);
|
|
3475
3830
|
const scriptsPayload = { api: {}, cron: {}, cronConfig: null };
|
|
3476
3831
|
for (const f of scriptFiles) {
|
|
3477
|
-
const content =
|
|
3832
|
+
const content = readFileSync3(f, "utf-8");
|
|
3478
3833
|
const relativePath = f.substring(BUILD_DIR.length + 1).replace(/\\/g, "/");
|
|
3479
3834
|
if (relativePath === "cron-jobs/cron.config.json") {
|
|
3480
3835
|
scriptsPayload.cronConfig = JSON.parse(content);
|
|
@@ -3514,7 +3869,7 @@ ${errorText}`);
|
|
|
3514
3869
|
console.log(`✅ Created ${ZIP_FILE}
|
|
3515
3870
|
`);
|
|
3516
3871
|
console.log("☁️ Step 5: Deploying HTML zip to server...");
|
|
3517
|
-
const zipBuffer =
|
|
3872
|
+
const zipBuffer = readFileSync3(ZIP_FILE);
|
|
3518
3873
|
try {
|
|
3519
3874
|
const response = await fetch(DEPLOY_URL, {
|
|
3520
3875
|
method: "POST",
|
|
@@ -3537,7 +3892,7 @@ ${errorText}`);
|
|
|
3537
3892
|
console.error("❌ Deploy failed:", error);
|
|
3538
3893
|
throw error;
|
|
3539
3894
|
} finally {
|
|
3540
|
-
if (
|
|
3895
|
+
if (existsSync4(ZIP_FILE)) {
|
|
3541
3896
|
unlinkSync(ZIP_FILE);
|
|
3542
3897
|
console.log(`
|
|
3543
3898
|
\uD83E\uDDF9 Cleaned up ${ZIP_FILE}`);
|
|
@@ -3547,20 +3902,387 @@ ${errorText}`);
|
|
|
3547
3902
|
✨ Deployment successful!`);
|
|
3548
3903
|
}
|
|
3549
3904
|
|
|
3905
|
+
// src/promote.ts
|
|
3906
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
|
|
3907
|
+
import { resolve as resolve2 } from "node:path";
|
|
3908
|
+
|
|
3909
|
+
// src/update-webapp-config.ts
|
|
3910
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3911
|
+
import { resolve } from "node:path";
|
|
3912
|
+
var CONFIG_URLS = {
|
|
3913
|
+
local: "http://localhost:5176/api/webapps/config",
|
|
3914
|
+
development: "https://development-cms.rodyssey.ai/api/webapps/config",
|
|
3915
|
+
staging: "https://staging-cms.rodyssey.ai/api/webapps/config",
|
|
3916
|
+
production: "https://cms.rodyssey.ai/api/webapps/config"
|
|
3917
|
+
};
|
|
3918
|
+
function parseJsonOption(value, optionName) {
|
|
3919
|
+
const maybePath = resolve(process.cwd(), value);
|
|
3920
|
+
const raw = existsSync5(maybePath) ? readFileSync4(maybePath, "utf-8") : value;
|
|
3921
|
+
try {
|
|
3922
|
+
const parsed = JSON.parse(raw);
|
|
3923
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3924
|
+
throw new Error("value must be a JSON object");
|
|
3925
|
+
}
|
|
3926
|
+
return parsed;
|
|
3927
|
+
} catch (error) {
|
|
3928
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3929
|
+
throw new Error(`Invalid ${optionName}: ${message}`);
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
function coerceMaybeNull(value) {
|
|
3933
|
+
if (value === undefined)
|
|
3934
|
+
return;
|
|
3935
|
+
if (value === "null")
|
|
3936
|
+
return null;
|
|
3937
|
+
return value;
|
|
3938
|
+
}
|
|
3939
|
+
function resolveConfigUrl(options, required = true) {
|
|
3940
|
+
const configUrl = options.url || process.env.WEBAPP_CONFIG_URL || CONFIG_URLS[options.env];
|
|
3941
|
+
if (!configUrl) {
|
|
3942
|
+
if (!required)
|
|
3943
|
+
return;
|
|
3944
|
+
console.error("❌ Error: no webapp config endpoint configured.");
|
|
3945
|
+
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.`);
|
|
3946
|
+
process.exit(1);
|
|
3947
|
+
}
|
|
3948
|
+
const url = new URL(configUrl);
|
|
3949
|
+
if (!options.host && !options.port) {
|
|
3950
|
+
return url.toString().replace(/\/$/, "");
|
|
3951
|
+
}
|
|
3952
|
+
if (options.host)
|
|
3953
|
+
url.hostname = options.host;
|
|
3954
|
+
if (options.port)
|
|
3955
|
+
url.port = String(options.port);
|
|
3956
|
+
return url.toString().replace(/\/$/, "");
|
|
3957
|
+
}
|
|
3958
|
+
function buildDetailsPayload(options) {
|
|
3959
|
+
const payload = options.details ? parseJsonOption(options.details, "--details") : {};
|
|
3960
|
+
const title = coerceMaybeNull(options.title);
|
|
3961
|
+
const description = coerceMaybeNull(options.description);
|
|
3962
|
+
const coverImg = coerceMaybeNull(options.coverImg);
|
|
3963
|
+
if (title !== undefined)
|
|
3964
|
+
payload.title = title;
|
|
3965
|
+
if (description !== undefined)
|
|
3966
|
+
payload.description = description;
|
|
3967
|
+
if (coverImg !== undefined)
|
|
3968
|
+
payload.coverImg = coverImg;
|
|
3969
|
+
if (options.localization !== undefined) {
|
|
3970
|
+
payload.localization = options.localization === "null" ? null : parseJsonOption(options.localization, "--localization");
|
|
3971
|
+
}
|
|
3972
|
+
return payload;
|
|
3973
|
+
}
|
|
3974
|
+
function resolveWebappId(webappId) {
|
|
3975
|
+
const resolved = webappId || process.env.WEBAPP_ID;
|
|
3976
|
+
if (!resolved) {
|
|
3977
|
+
console.error("❌ Error: WEBAPP_ID is not set. Pass --webapp-id or set it in your .env file.");
|
|
3978
|
+
process.exit(1);
|
|
3979
|
+
}
|
|
3980
|
+
return resolved;
|
|
3981
|
+
}
|
|
3982
|
+
function ensureDeployToken(env) {
|
|
3983
|
+
if (process.env.DEPLOY_TOKEN)
|
|
3984
|
+
return;
|
|
3985
|
+
console.error("❌ Error: DEPLOY_TOKEN is not set in environment variables.");
|
|
3986
|
+
console.info(`\uD83D\uDCA1 Please check your .env or .env.${env} file.`);
|
|
3987
|
+
process.exit(1);
|
|
3988
|
+
}
|
|
3989
|
+
function getConfigUrl(options, required = true) {
|
|
3990
|
+
return resolveConfigUrl(options, required);
|
|
3991
|
+
}
|
|
3992
|
+
async function fetchWebappConfig(options) {
|
|
3993
|
+
loadEnv(options.env);
|
|
3994
|
+
const webappId = resolveWebappId(options.webappId);
|
|
3995
|
+
const CONFIG_URL = getConfigUrl(options);
|
|
3996
|
+
if (!CONFIG_URL) {
|
|
3997
|
+
throw new Error("No webapp config endpoint configured.");
|
|
3998
|
+
}
|
|
3999
|
+
ensureDeployToken(options.env);
|
|
4000
|
+
const url = new URL(CONFIG_URL);
|
|
4001
|
+
url.searchParams.set("webappId", webappId);
|
|
4002
|
+
const response = await fetch(url, {
|
|
4003
|
+
method: "GET",
|
|
4004
|
+
headers: {
|
|
4005
|
+
Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
|
|
4006
|
+
Accept: "application/json"
|
|
4007
|
+
}
|
|
4008
|
+
});
|
|
4009
|
+
if (!response.ok) {
|
|
4010
|
+
const errorText = await response.text();
|
|
4011
|
+
throw new Error(`Config fetch failed: ${response.status} ${response.statusText}
|
|
4012
|
+
${errorText}`);
|
|
4013
|
+
}
|
|
4014
|
+
return await response.json();
|
|
4015
|
+
}
|
|
4016
|
+
async function getWebappConfig(options) {
|
|
4017
|
+
loadEnv(options.env);
|
|
4018
|
+
const webappId = resolveWebappId(options.webappId);
|
|
4019
|
+
const CONFIG_URL = getConfigUrl(options);
|
|
4020
|
+
if (!CONFIG_URL)
|
|
4021
|
+
return;
|
|
4022
|
+
ensureDeployToken(options.env);
|
|
4023
|
+
const url = new URL(CONFIG_URL);
|
|
4024
|
+
url.searchParams.set("webappId", webappId);
|
|
4025
|
+
console.log(`⚙️ Pulling webapp config for [${options.env}] environment...`);
|
|
4026
|
+
console.log(`\uD83D\uDCCD Config URL: ${url.toString()}`);
|
|
4027
|
+
console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
|
|
4028
|
+
`);
|
|
4029
|
+
const response = await fetch(url, {
|
|
4030
|
+
method: "GET",
|
|
4031
|
+
headers: {
|
|
4032
|
+
Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
|
|
4033
|
+
Accept: "application/json"
|
|
4034
|
+
}
|
|
4035
|
+
});
|
|
4036
|
+
if (!response.ok) {
|
|
4037
|
+
const errorText = await response.text();
|
|
4038
|
+
throw new Error(`Config fetch failed: ${response.status} ${response.statusText}
|
|
4039
|
+
${errorText}`);
|
|
4040
|
+
}
|
|
4041
|
+
const config = await response.json();
|
|
4042
|
+
const output = JSON.stringify(config, null, 2);
|
|
4043
|
+
if (options.out) {
|
|
4044
|
+
writeFileSync3(options.out, `${output}
|
|
4045
|
+
`, "utf-8");
|
|
4046
|
+
console.log(`✅ Webapp config written to ${options.out}`);
|
|
4047
|
+
return;
|
|
4048
|
+
}
|
|
4049
|
+
console.log(output);
|
|
4050
|
+
}
|
|
4051
|
+
async function updateWebappConfig(options) {
|
|
4052
|
+
loadEnv(options.env);
|
|
4053
|
+
const webappId = resolveWebappId(options.webappId);
|
|
4054
|
+
const details = buildDetailsPayload(options);
|
|
4055
|
+
if (Object.keys(details).length === 0) {
|
|
4056
|
+
console.error("❌ Error: no detail fields provided. Use --title, --description, --cover-img, --localization, or --details.");
|
|
4057
|
+
process.exit(1);
|
|
4058
|
+
}
|
|
4059
|
+
const payload = { webappId, details };
|
|
4060
|
+
const CONFIG_URL = getConfigUrl(options, !options.dryRun);
|
|
4061
|
+
if (!options.dryRun)
|
|
4062
|
+
ensureDeployToken(options.env);
|
|
4063
|
+
console.log(`⚙️ Updating webapp config for [${options.env}] environment...`);
|
|
4064
|
+
if (CONFIG_URL) {
|
|
4065
|
+
console.log(`\uD83D\uDCCD Config URL: ${CONFIG_URL}`);
|
|
4066
|
+
}
|
|
4067
|
+
console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
|
|
4068
|
+
`);
|
|
4069
|
+
if (options.dryRun) {
|
|
4070
|
+
console.log("\uD83E\uDDEA Dry run payload:");
|
|
4071
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
4072
|
+
return;
|
|
4073
|
+
}
|
|
4074
|
+
if (!CONFIG_URL)
|
|
4075
|
+
return;
|
|
4076
|
+
const response = await fetch(CONFIG_URL, {
|
|
4077
|
+
method: "PATCH",
|
|
4078
|
+
headers: {
|
|
4079
|
+
Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
|
|
4080
|
+
"Content-Type": "application/json"
|
|
4081
|
+
},
|
|
4082
|
+
body: JSON.stringify(payload)
|
|
4083
|
+
});
|
|
4084
|
+
if (!response.ok) {
|
|
4085
|
+
const errorText = await response.text();
|
|
4086
|
+
throw new Error(`Config update failed: ${response.status} ${response.statusText}
|
|
4087
|
+
${errorText}`);
|
|
4088
|
+
}
|
|
4089
|
+
const result = await response.json().catch(() => {
|
|
4090
|
+
return;
|
|
4091
|
+
});
|
|
4092
|
+
console.log("✅ Webapp config updated");
|
|
4093
|
+
if (result !== undefined) {
|
|
4094
|
+
console.log(`
|
|
4095
|
+
\uD83D\uDCCB Update result:`, result);
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
// src/promote.ts
|
|
4100
|
+
var DEFAULT_SOURCE_ENV = "development";
|
|
4101
|
+
var PROD_ENV = "production";
|
|
4102
|
+
var PROD_ENV_FILE = ".env.production";
|
|
4103
|
+
function isObject3(value) {
|
|
4104
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
4105
|
+
}
|
|
4106
|
+
function pickString2(...values) {
|
|
4107
|
+
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
4108
|
+
}
|
|
4109
|
+
function nestedObject2(value, key) {
|
|
4110
|
+
if (!isObject3(value))
|
|
4111
|
+
return;
|
|
4112
|
+
const nested = value[key];
|
|
4113
|
+
return isObject3(nested) ? nested : undefined;
|
|
4114
|
+
}
|
|
4115
|
+
function extractDeployToken2(payload) {
|
|
4116
|
+
const webapp = nestedObject2(payload, "webapp");
|
|
4117
|
+
return pickString2(webapp?.deployToken, webapp?.deploymentToken);
|
|
4118
|
+
}
|
|
4119
|
+
function unwrapSourceDetails(payload) {
|
|
4120
|
+
if (!isObject3(payload))
|
|
4121
|
+
return {};
|
|
4122
|
+
const inner = payload.details;
|
|
4123
|
+
if (isObject3(inner))
|
|
4124
|
+
return inner;
|
|
4125
|
+
return payload;
|
|
4126
|
+
}
|
|
4127
|
+
function parseDetailsOption(value) {
|
|
4128
|
+
const maybePath = resolve2(process.cwd(), value);
|
|
4129
|
+
const raw = existsSync6(maybePath) ? readFileSync5(maybePath, "utf-8") : value;
|
|
4130
|
+
try {
|
|
4131
|
+
const parsed = JSON.parse(raw);
|
|
4132
|
+
if (!isObject3(parsed)) {
|
|
4133
|
+
throw new Error("value must be a JSON object");
|
|
4134
|
+
}
|
|
4135
|
+
return parsed;
|
|
4136
|
+
} catch (error) {
|
|
4137
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4138
|
+
throw new Error(`Invalid --details: ${message}`);
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
async function prompt(question) {
|
|
4142
|
+
const readline = await import("node:readline");
|
|
4143
|
+
const rl = readline.createInterface({
|
|
4144
|
+
input: process.stdin,
|
|
4145
|
+
output: process.stdout
|
|
4146
|
+
});
|
|
4147
|
+
return new Promise((resolveAnswer) => {
|
|
4148
|
+
rl.question(question, (answer) => {
|
|
4149
|
+
rl.close();
|
|
4150
|
+
resolveAnswer(answer);
|
|
4151
|
+
});
|
|
4152
|
+
});
|
|
4153
|
+
}
|
|
4154
|
+
function isAffirmative(answer) {
|
|
4155
|
+
const trimmed = answer.trim().toLowerCase();
|
|
4156
|
+
return trimmed === "" || trimmed === "y" || trimmed === "yes";
|
|
4157
|
+
}
|
|
4158
|
+
async function promote(options) {
|
|
4159
|
+
loadEnv();
|
|
4160
|
+
const webappId = process.env.WEBAPP_ID;
|
|
4161
|
+
if (!webappId) {
|
|
4162
|
+
console.error("❌ Error: WEBAPP_ID is not set in .env.");
|
|
4163
|
+
console.info("\uD83D\uDCA1 Run `ro app create --auto` first, or set WEBAPP_ID manually.");
|
|
4164
|
+
process.exit(1);
|
|
4165
|
+
}
|
|
4166
|
+
const prodEnvPath = resolve2(process.cwd(), PROD_ENV_FILE);
|
|
4167
|
+
if (existsSync6(prodEnvPath)) {
|
|
4168
|
+
const content = readFileSync5(prodEnvPath, "utf-8");
|
|
4169
|
+
if (/^WEBAPP_ID=.+/m.test(content)) {
|
|
4170
|
+
console.error(`❌ Error: ${PROD_ENV_FILE} already has WEBAPP_ID. The app appears to be promoted already.`);
|
|
4171
|
+
process.exit(1);
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
const sourceEnv = options.from || DEFAULT_SOURCE_ENV;
|
|
4175
|
+
let details;
|
|
4176
|
+
if (options.details) {
|
|
4177
|
+
details = parseDetailsOption(options.details);
|
|
4178
|
+
console.log("\uD83D\uDCE6 Using details from --details flag.");
|
|
4179
|
+
} else {
|
|
4180
|
+
console.log(`\uD83D\uDCE5 Fetching latest details from [${sourceEnv}]...`);
|
|
4181
|
+
const sourceResponse = await fetchWebappConfig({ env: sourceEnv, webappId });
|
|
4182
|
+
const sourceDetails = unwrapSourceDetails(sourceResponse);
|
|
4183
|
+
console.log(`
|
|
4184
|
+
\uD83D\uDCCB Source details:`);
|
|
4185
|
+
console.log(JSON.stringify(sourceDetails, null, 2));
|
|
4186
|
+
console.log();
|
|
4187
|
+
if (options.yes) {
|
|
4188
|
+
console.log("✓ --yes flag set, using server details.");
|
|
4189
|
+
details = sourceDetails;
|
|
4190
|
+
} else {
|
|
4191
|
+
const answer = await prompt("Pull these details from server? (Y/n): ");
|
|
4192
|
+
if (isAffirmative(answer)) {
|
|
4193
|
+
details = sourceDetails;
|
|
4194
|
+
} else {
|
|
4195
|
+
console.error("❌ Aborted. Re-run with --details <json|file> to provide custom details.");
|
|
4196
|
+
process.exit(1);
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
console.log(`
|
|
4201
|
+
\uD83D\uDD10 Logging in to ${PROD_ENV} CMS (ephemeral, not stored)...`);
|
|
4202
|
+
const session = await login({
|
|
4203
|
+
env: PROD_ENV,
|
|
4204
|
+
cmsUrl: options.cmsUrl,
|
|
4205
|
+
persist: false
|
|
4206
|
+
});
|
|
4207
|
+
console.log(`✅ Logged in to ${session.cmsUrl}`);
|
|
4208
|
+
const cmsUrl = resolveCmsUrl(PROD_ENV, options.cmsUrl);
|
|
4209
|
+
const promoteUrl = options.promoteUrl || `${cmsUrl}/api/cli/webapps/promote`;
|
|
4210
|
+
console.log(`
|
|
4211
|
+
\uD83D\uDE80 Promoting webapp to ${PROD_ENV}...`);
|
|
4212
|
+
console.log(`\uD83D\uDCCD Promote URL: ${promoteUrl}`);
|
|
4213
|
+
console.log(`\uD83D\uDCCD Webapp ID: ${webappId}
|
|
4214
|
+
`);
|
|
4215
|
+
const response = await fetch(promoteUrl, {
|
|
4216
|
+
method: "POST",
|
|
4217
|
+
headers: {
|
|
4218
|
+
Accept: "application/json",
|
|
4219
|
+
Authorization: `Bearer ${session.token}`,
|
|
4220
|
+
"Content-Type": "application/json"
|
|
4221
|
+
},
|
|
4222
|
+
body: JSON.stringify({ webappId, details })
|
|
4223
|
+
});
|
|
4224
|
+
const payload = await readResponsePayload(response);
|
|
4225
|
+
if (response.status === 409) {
|
|
4226
|
+
console.error(`❌ Webapp ${webappId} is already promoted to ${PROD_ENV}.`);
|
|
4227
|
+
process.exit(1);
|
|
4228
|
+
}
|
|
4229
|
+
if (!response.ok) {
|
|
4230
|
+
throw new Error(`Promote failed: ${response.status} ${response.statusText}
|
|
4231
|
+
${JSON.stringify(payload, null, 2)}`);
|
|
4232
|
+
}
|
|
4233
|
+
const deployToken = extractDeployToken2(payload);
|
|
4234
|
+
if (!deployToken) {
|
|
4235
|
+
throw new Error(`Promote response did not include webapp.deployToken:
|
|
4236
|
+
${JSON.stringify(payload, null, 2)}`);
|
|
4237
|
+
}
|
|
4238
|
+
upsertEnvFile(prodEnvPath, {
|
|
4239
|
+
WEBAPP_ID: webappId,
|
|
4240
|
+
DEPLOY_TOKEN: deployToken
|
|
4241
|
+
});
|
|
4242
|
+
console.log(`✅ Wrote WEBAPP_ID and DEPLOY_TOKEN to ${PROD_ENV_FILE}`);
|
|
4243
|
+
console.log(`\uD83D\uDCCD Webapp ID: ${webappId}`);
|
|
4244
|
+
console.log(`
|
|
4245
|
+
✨ Promotion complete! Deploy to production with:
|
|
4246
|
+
`);
|
|
4247
|
+
console.log(` ro app deploy -e production
|
|
4248
|
+
`);
|
|
4249
|
+
}
|
|
4250
|
+
|
|
3550
4251
|
// src/upgrade-template.ts
|
|
3551
4252
|
import { execSync as execSync3 } from "node:child_process";
|
|
3552
|
-
import { existsSync as
|
|
4253
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2, copyFileSync, rmSync as rmSync2 } from "node:fs";
|
|
3553
4254
|
var TEMPLATES = {
|
|
3554
4255
|
webapp: {
|
|
3555
4256
|
name: "webapp (SPA)",
|
|
3556
4257
|
repo: "https://github.com/airconcepts/webapp-template.git",
|
|
3557
4258
|
remoteName: "template",
|
|
3558
|
-
checkoutFiles: [
|
|
4259
|
+
checkoutFiles: [
|
|
4260
|
+
"AGENTS.md",
|
|
4261
|
+
".agent/",
|
|
4262
|
+
"src/types/webapp.d.ts",
|
|
4263
|
+
"src/types/game-sdk.d.ts",
|
|
4264
|
+
"src/widgets/Widget.tsx",
|
|
4265
|
+
"src/widgets/data-source.ts",
|
|
4266
|
+
"src/widgets/index.ts",
|
|
4267
|
+
"src/widgets/manifest.ts",
|
|
4268
|
+
"src/widgets/registry.ts",
|
|
4269
|
+
"src/widgets/types.ts",
|
|
4270
|
+
"src/widgets/use-widget-data.ts",
|
|
4271
|
+
"src/widgets/dev/WidgetGrid.tsx",
|
|
4272
|
+
"src/widgets/templates/action.tsx",
|
|
4273
|
+
"src/widgets/templates/list.tsx",
|
|
4274
|
+
"src/widgets/templates/progress.tsx",
|
|
4275
|
+
"src/widgets/templates/stat.tsx",
|
|
4276
|
+
"vite-plugins/widgets-manifest.ts"
|
|
4277
|
+
],
|
|
3559
4278
|
newFiles: [
|
|
3560
4279
|
"src/exp-engine/cli.ts",
|
|
3561
4280
|
"src/exp-engine/evaluate.ts",
|
|
3562
4281
|
"src/exp-engine/README.md",
|
|
3563
|
-
"src/types/exp-engine.d.ts"
|
|
4282
|
+
"src/types/exp-engine.d.ts",
|
|
4283
|
+
"src/widgets/examples.tsx",
|
|
4284
|
+
"src/routes/widget.$id.tsx",
|
|
4285
|
+
"src/routes/dev/widgets-preview.tsx"
|
|
3564
4286
|
]
|
|
3565
4287
|
},
|
|
3566
4288
|
"webapp-fullstack": {
|
|
@@ -3579,15 +4301,17 @@ var TEMPLATES = {
|
|
|
3579
4301
|
var CLI_SCRIPTS = {
|
|
3580
4302
|
"link-game-sdk": "bunx @rodyssey/cli@latest app update-game-sdk",
|
|
3581
4303
|
deploy: "bunx @rodyssey/cli@latest app deploy",
|
|
4304
|
+
"get-webapp-config": "bunx @rodyssey/cli@latest app config get",
|
|
4305
|
+
"update-webapp-config": "bunx @rodyssey/cli@latest app config set",
|
|
3582
4306
|
"upgrade-template": "bunx @rodyssey/cli@latest app upgrade-template"
|
|
3583
4307
|
};
|
|
3584
4308
|
function detectTemplate() {
|
|
3585
|
-
if (
|
|
4309
|
+
if (existsSync7("app")) {
|
|
3586
4310
|
console.log(`\uD83D\uDD0D Detected fullstack template (found app/ directory)
|
|
3587
4311
|
`);
|
|
3588
4312
|
return TEMPLATES["webapp-fullstack"];
|
|
3589
4313
|
}
|
|
3590
|
-
if (
|
|
4314
|
+
if (existsSync7("src")) {
|
|
3591
4315
|
console.log(`\uD83D\uDD0D Detected SPA template (found src/ directory)
|
|
3592
4316
|
`);
|
|
3593
4317
|
return TEMPLATES["webapp"];
|
|
@@ -3598,11 +4322,11 @@ function detectTemplate() {
|
|
|
3598
4322
|
}
|
|
3599
4323
|
function updatePackageJsonScripts() {
|
|
3600
4324
|
const pkgPath = "package.json";
|
|
3601
|
-
if (!
|
|
4325
|
+
if (!existsSync7(pkgPath)) {
|
|
3602
4326
|
console.log("⚠️ No package.json found, skipping scripts update");
|
|
3603
4327
|
return;
|
|
3604
4328
|
}
|
|
3605
|
-
const pkg = JSON.parse(
|
|
4329
|
+
const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
|
|
3606
4330
|
if (!pkg.scripts) {
|
|
3607
4331
|
pkg.scripts = {};
|
|
3608
4332
|
}
|
|
@@ -3616,7 +4340,7 @@ function updatePackageJsonScripts() {
|
|
|
3616
4340
|
}
|
|
3617
4341
|
}
|
|
3618
4342
|
if (updated) {
|
|
3619
|
-
|
|
4343
|
+
writeFileSync4(pkgPath, JSON.stringify(pkg, null, 2) + `
|
|
3620
4344
|
`, "utf-8");
|
|
3621
4345
|
console.log(`✅ package.json scripts updated
|
|
3622
4346
|
`);
|
|
@@ -3640,8 +4364,8 @@ function updateCliSkill() {
|
|
|
3640
4364
|
}
|
|
3641
4365
|
execSync3(`git fetch ${cliRemote}`, { stdio: "inherit" });
|
|
3642
4366
|
execSync3(`git checkout ${cliRemote}/main -- skills/ro-cli/SKILL.md`, { stdio: "inherit" });
|
|
3643
|
-
if (
|
|
3644
|
-
|
|
4367
|
+
if (existsSync7("skills/ro-cli/SKILL.md")) {
|
|
4368
|
+
mkdirSync2(".agent/skills/ro-cli", { recursive: true });
|
|
3645
4369
|
copyFileSync("skills/ro-cli/SKILL.md", ".agent/skills/ro-cli/SKILL.md");
|
|
3646
4370
|
rmSync2("skills", { recursive: true, force: true });
|
|
3647
4371
|
console.log(` ✅ CLI skill updated
|
|
@@ -3673,7 +4397,7 @@ async function upgradeTemplate() {
|
|
|
3673
4397
|
const checkoutList = template.checkoutFiles.join(" ");
|
|
3674
4398
|
execSync3(`git checkout ${template.remoteName}/main -- ${checkoutList}`, { stdio: "inherit" });
|
|
3675
4399
|
for (const file of template.newFiles) {
|
|
3676
|
-
if (!
|
|
4400
|
+
if (!existsSync7(file)) {
|
|
3677
4401
|
console.log(`\uD83D\uDCC2 Checking out ${file}...`);
|
|
3678
4402
|
try {
|
|
3679
4403
|
execSync3(`git checkout ${template.remoteName}/main -- ${file}`, { stdio: "inherit" });
|
|
@@ -3698,7 +4422,7 @@ async function upgradeTemplate() {
|
|
|
3698
4422
|
|
|
3699
4423
|
// src/update-game-sdk.ts
|
|
3700
4424
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
3701
|
-
import { dirname, join as
|
|
4425
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
3702
4426
|
var BASE_URL = "https://development-app.rodyssey.ai";
|
|
3703
4427
|
var FILES = [
|
|
3704
4428
|
{
|
|
@@ -3728,7 +4452,7 @@ async function downloadFile(url, path3, description) {
|
|
|
3728
4452
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
3729
4453
|
}
|
|
3730
4454
|
const content = await response.text();
|
|
3731
|
-
const dir =
|
|
4455
|
+
const dir = dirname2(path3);
|
|
3732
4456
|
await mkdir(dir, { recursive: true });
|
|
3733
4457
|
await writeFile(path3, content, "utf-8");
|
|
3734
4458
|
console.log(`✅ Downloaded ${description} (${content.length} bytes)
|
|
@@ -3772,7 +4496,7 @@ async function downloadDocumentation(manifest) {
|
|
|
3772
4496
|
let failCount = 0;
|
|
3773
4497
|
for (const doc of manifest.documentation) {
|
|
3774
4498
|
const url = `${BASE_URL}/skills/${doc.file}`;
|
|
3775
|
-
const path3 =
|
|
4499
|
+
const path3 = join3(".agent", "skills", "game-sdk", doc.file);
|
|
3776
4500
|
const description = `${doc.title} (${doc.category})`;
|
|
3777
4501
|
const success = await downloadFile(url, path3, description);
|
|
3778
4502
|
if (success) {
|
|
@@ -3858,22 +4582,28 @@ Available templates:
|
|
|
3858
4582
|
input: process.stdin,
|
|
3859
4583
|
output: process.stdout
|
|
3860
4584
|
});
|
|
3861
|
-
return new Promise((
|
|
4585
|
+
return new Promise((resolve3) => {
|
|
3862
4586
|
rl.question(`Select a template (1-${entries.length}): `, (answer) => {
|
|
3863
4587
|
rl.close();
|
|
3864
4588
|
const index = parseInt(answer, 10) - 1;
|
|
3865
4589
|
if (index >= 0 && index < entries.length) {
|
|
3866
|
-
|
|
4590
|
+
resolve3(entries[index].name);
|
|
3867
4591
|
} else {
|
|
3868
4592
|
console.error("Invalid selection, defaulting to 'webapp'");
|
|
3869
|
-
|
|
4593
|
+
resolve3("webapp");
|
|
3870
4594
|
}
|
|
3871
4595
|
});
|
|
3872
4596
|
});
|
|
3873
4597
|
}
|
|
3874
|
-
program.name("@rodyssey/cli").description("Airconcepts CLI toolkit").version(
|
|
4598
|
+
program.name("@rodyssey/cli").description("Airconcepts CLI toolkit").version(package_default.version);
|
|
3875
4599
|
var app = program.command("app").description("Manage webapp projects");
|
|
3876
|
-
|
|
4600
|
+
function addConfigTargetOptions(command) {
|
|
4601
|
+
return command.option("-e, --env <environment>", "Target environment (local | development | staging | production)", "development").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");
|
|
4602
|
+
}
|
|
4603
|
+
function addConfigSetOptions(command) {
|
|
4604
|
+
return addConfigTargetOptions(command).option("--title <title>", "Webapp title (pass the literal 'null' to clear)").option("--description <description>", "Webapp description (pass the literal 'null' to clear)").option("--cover-img <url>", "Cover image URL (pass the literal 'null' to clear)").option("--localization <json-or-file>", "Localization JSON object, path to a JSON file, or 'null' to clear").option("--details <json-or-file>", "Partial WebappDetails JSON or path to a JSON file. Sent as a delta — only include keys you want to change. Do NOT echo a full GET response here, or empty defaults like 'tags: []' will clobber real data.").option("--dry-run", "Print the request payload without sending it");
|
|
4605
|
+
}
|
|
4606
|
+
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) => {
|
|
3877
4607
|
let templateName;
|
|
3878
4608
|
if (options.template) {
|
|
3879
4609
|
if (!(options.template in TEMPLATES2)) {
|
|
@@ -3885,7 +4615,21 @@ app.command("create").argument("<project-name>", "Name of the project to create"
|
|
|
3885
4615
|
templateName = await selectTemplate();
|
|
3886
4616
|
}
|
|
3887
4617
|
const template = TEMPLATES2[templateName];
|
|
3888
|
-
await create(projectName, template.repo, templateName
|
|
4618
|
+
await create(projectName, template.repo, templateName, {
|
|
4619
|
+
enabled: options.auto,
|
|
4620
|
+
env: options.env,
|
|
4621
|
+
cmsUrl: options.cmsUrl,
|
|
4622
|
+
createUrl: options.createUrl
|
|
4623
|
+
});
|
|
4624
|
+
});
|
|
4625
|
+
app.command("promote").description("Promote the current webapp to production (creates a prod record with the same WEBAPP_ID)").option("--details <json-or-file>", "Full WebappDetails JSON object or path to a JSON file. Skips the source-pull and confirmation prompts when provided.").option("-y, --yes", "Auto-accept pulling the latest details from development").option("--cms-url <url>", "Production CMS base URL. Defaults to the production environment").option("--promote-url <url>", "Full CMS promote endpoint. Defaults to <cms-url>/api/cli/webapps/promote").option("--from <env>", "Source environment to pull details from (testing override). Defaults to development", "development").action(async (options) => {
|
|
4626
|
+
await promote({
|
|
4627
|
+
details: options.details,
|
|
4628
|
+
yes: options.yes,
|
|
4629
|
+
cmsUrl: options.cmsUrl,
|
|
4630
|
+
promoteUrl: options.promoteUrl,
|
|
4631
|
+
from: options.from
|
|
4632
|
+
});
|
|
3889
4633
|
});
|
|
3890
4634
|
app.command("update-game-sdk").description("Download and update the GameSDK library, types, and documentation").action(async () => {
|
|
3891
4635
|
await updateGameSdk();
|
|
@@ -3893,6 +4637,31 @@ app.command("update-game-sdk").description("Download and update the GameSDK libr
|
|
|
3893
4637
|
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) => {
|
|
3894
4638
|
await deploy(options.env, { host: options.host, port: options.port });
|
|
3895
4639
|
});
|
|
4640
|
+
var auth = app.command("auth").description("Authenticate with the CMS");
|
|
4641
|
+
auth.command("login").description("Log in to the CMS using a browser callback").option("-e, --env <environment>", "CMS environment (local | development | staging | production)", "development").option("--cms-url <url>", "CMS base URL. Defaults to the selected environment").option("--login-url <url>", "Full browser login URL. Defaults to <cms-url>/auth/cli-login").option("--token-url <url>", "Full token exchange URL. Defaults to <cms-url>/api/auth/cli-token").option("--callback-port <port>", "Local callback port. Defaults to a random free port", parseInt).option("--timeout <seconds>", "Seconds to wait for the browser callback", parseInt, 300).option("--no-open", "Print the login URL without opening a browser").action(async (options) => {
|
|
4642
|
+
const session = await login({
|
|
4643
|
+
env: options.env,
|
|
4644
|
+
cmsUrl: options.cmsUrl,
|
|
4645
|
+
loginUrl: options.loginUrl,
|
|
4646
|
+
tokenUrl: options.tokenUrl,
|
|
4647
|
+
callbackPort: options.callbackPort,
|
|
4648
|
+
open: options.open,
|
|
4649
|
+
timeoutMs: (options.timeout ?? 300) * 1000
|
|
4650
|
+
});
|
|
4651
|
+
console.log(`✅ Logged in to CMS [${session.env}]`);
|
|
4652
|
+
console.log(`\uD83D\uDCCD CMS URL: ${session.cmsUrl}`);
|
|
4653
|
+
});
|
|
4654
|
+
auth.command("me").description("Show the locally stored CMS login session").option("-e, --env <environment>", "CMS environment (local | development | staging | production)", "development").option("--remote", "Call the CMS /me endpoint instead of only reading the local session").option("--cms-url <url>", "CMS base URL for --remote. Defaults to the selected environment or stored session").option("--me-url <url>", "Full me endpoint URL for --remote. Defaults to <cms-url>/api/auth/me").action(async (options) => {
|
|
4655
|
+
const currentUser = await me(options);
|
|
4656
|
+
console.log(JSON.stringify(currentUser, null, 2));
|
|
4657
|
+
});
|
|
4658
|
+
var config = app.command("config").description("Manage webapp metadata config");
|
|
4659
|
+
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) => {
|
|
4660
|
+
await getWebappConfig(options);
|
|
4661
|
+
});
|
|
4662
|
+
addConfigSetOptions(config.command("set").description("Update webapp metadata such as title, cover image, description, and localization")).action(async (options) => {
|
|
4663
|
+
await updateWebappConfig(options);
|
|
4664
|
+
});
|
|
3896
4665
|
app.command("upgrade-template").description("Upgrade template files and CLI scripts from the template repository").action(async () => {
|
|
3897
4666
|
await upgradeTemplate();
|
|
3898
4667
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rodyssey/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Scaffold new projects from airconcepts templates",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/airconcepts/ro-cli.git"
|
|
8
|
+
},
|
|
5
9
|
"bin": {
|
|
6
10
|
"@rodyssey/cli": "dist/cli.js"
|
|
7
11
|
},
|
|
@@ -13,16 +17,24 @@
|
|
|
13
17
|
"scripts": {
|
|
14
18
|
"build": "bun build src/cli.ts --outdir dist --target node",
|
|
15
19
|
"start": "bun dist/cli.js",
|
|
20
|
+
"changeset": "changeset",
|
|
21
|
+
"version-packages": "changeset version",
|
|
22
|
+
"release": "bun run build && changeset publish",
|
|
16
23
|
"prepublishOnly": "bun run build"
|
|
17
24
|
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"provenance": false
|
|
28
|
+
},
|
|
18
29
|
"dependencies": {
|
|
19
30
|
"commander": "^13.1.0",
|
|
20
31
|
"mime": "^4.1.0"
|
|
21
32
|
},
|
|
22
33
|
"devDependencies": {
|
|
34
|
+
"@changesets/cli": "^2.30.0",
|
|
23
35
|
"@types/bun": "latest"
|
|
24
36
|
},
|
|
25
37
|
"peerDependencies": {
|
|
26
38
|
"typescript": "^5"
|
|
27
39
|
}
|
|
28
|
-
}
|
|
40
|
+
}
|