@rodyssey/cli 0.2.1 → 0.3.1
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 +103 -3
- package/dist/cli.js +321 -78
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,23 +1,123 @@
|
|
|
1
1
|
# ro-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Rodyssey CLI (`@rodyssey/cli`) — scaffolds, deploys, configures, and ships webapp projects to the rodyssey CMS.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
The package installs three binaries: `ro` (recommended), `rocli`, and `@rodyssey/cli` (the canonical long form). Pick any.
|
|
8
|
+
|
|
9
|
+
### Global install (most users)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# via Bun
|
|
13
|
+
bun install -g @rodyssey/cli
|
|
14
|
+
|
|
15
|
+
# via npm
|
|
16
|
+
npm install -g @rodyssey/cli
|
|
17
|
+
|
|
18
|
+
# verify
|
|
19
|
+
ro --version
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Per-project (no global install)
|
|
4
23
|
|
|
5
24
|
```bash
|
|
25
|
+
# Run any time without installing
|
|
26
|
+
bunx @rodyssey/cli@latest auth login -e development
|
|
27
|
+
bunx @rodyssey/cli@latest app create my-app --auto
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Develop against this repo (live-edit the CLI itself)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git clone git@github.com:airconcepts/ro-cli.git
|
|
34
|
+
cd ro-cli
|
|
6
35
|
bun install
|
|
36
|
+
bun run build # produces dist/cli.js with the ro/rocli shebangs
|
|
37
|
+
|
|
38
|
+
# Register your local checkout as the global `ro` / `rocli` / `@rodyssey/cli`:
|
|
39
|
+
bun link
|
|
40
|
+
|
|
41
|
+
# Now `ro` everywhere points at this checkout's dist/cli.js
|
|
42
|
+
ro --version # prints whatever package.json says locally
|
|
43
|
+
|
|
44
|
+
# When done, unlink to restore the published version:
|
|
45
|
+
bun unlink # from inside ro-cli/
|
|
46
|
+
bun install -g @rodyssey/cli # reinstall the published one
|
|
7
47
|
```
|
|
8
48
|
|
|
9
|
-
|
|
49
|
+
If you want to keep **both** the published `ro` and a local-dev version side-by-side without re-linking constantly, add a shell alias for the dev tree:
|
|
10
50
|
|
|
11
51
|
```bash
|
|
12
|
-
|
|
52
|
+
# in ~/.zshrc or ~/.bashrc
|
|
53
|
+
alias rocli-dev='bun /absolute/path/to/ro-cli/src/cli.ts'
|
|
13
54
|
```
|
|
14
55
|
|
|
56
|
+
`rocli-dev` then runs your live TypeScript via Bun (no rebuild needed), while `ro` keeps pointing at the globally-installed published version. The SKILL.md recognizes both names.
|
|
57
|
+
|
|
58
|
+
## Using with AI agents
|
|
59
|
+
|
|
60
|
+
The CLI ships with a [SKILL.md](.claude/skills/rodyssey-cli/SKILL.md) that teaches an AI coding agent how to drive `ro` correctly — command map, auth/scope model, deploy-vs-session-token distinction, `app config set` PATCH delta semantics, `global-config` set-vs-patch, and the gotchas that aren't obvious from `--help`.
|
|
61
|
+
|
|
62
|
+
**To use it in your own project** (assumes Claude Code; adapt the path for other agent frameworks):
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
mkdir -p .claude/skills/rodyssey-cli
|
|
66
|
+
curl -L https://raw.githubusercontent.com/airconcepts/ro-cli/main/.claude/skills/rodyssey-cli/SKILL.md \
|
|
67
|
+
-o .claude/skills/rodyssey-cli/SKILL.md
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Or just copy [`.claude/skills/rodyssey-cli/SKILL.md`](.claude/skills/rodyssey-cli/SKILL.md) verbatim into your project's skills directory. Once loaded, your agent will recognize `ro app *` / `ro auth *` / `ro global-config *` commands, suggest the right subcommand for natural-language requests, and avoid the common footguns (e.g., echoing a `config get` response back into `config set --details` and wiping data).
|
|
71
|
+
|
|
72
|
+
## Authentication
|
|
73
|
+
|
|
74
|
+
`ro auth login -e <env>` runs a PKCE browser flow against the CMS. The consent screen
|
|
75
|
+
lets the user authorize the CLI to act as themselves or as a service account they host,
|
|
76
|
+
and pick which scopes to grant. The token is persisted at `~/.rodyssey/config.json`
|
|
77
|
+
under `auth.<env>` along with an `identity` block describing who the token represents.
|
|
78
|
+
|
|
79
|
+
`ro auth me -e <env>` reads that local session and prints which environment + identity
|
|
80
|
+
is active. Examples:
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
$ ro auth me -e development
|
|
84
|
+
Environment: development
|
|
85
|
+
CMS URL: https://development-cms.rodyssey.ai
|
|
86
|
+
Logged in as: alex@example.com
|
|
87
|
+
Granted scopes: webapps:create, webapps:deploy-token:create, cms:global-config:read
|
|
88
|
+
|
|
89
|
+
$ ro auth me -e development # after authorizing as a service account
|
|
90
|
+
Environment: development
|
|
91
|
+
CMS URL: https://development-cms.rodyssey.ai
|
|
92
|
+
Logged in as service account: deploy-bot (id: sa-abc-123)
|
|
93
|
+
Granted scopes: webapps:create, feed:post
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Add `--remote` to also call `/api/auth/me` for a freshness check.
|
|
97
|
+
|
|
98
|
+
Sessions that pre-date the identity-aware flow render as `Logged in (legacy session —
|
|
99
|
+
no identity block stored)`. Re-running `ro auth login -e <env>` refreshes them.
|
|
100
|
+
|
|
15
101
|
## Template Upgrade
|
|
16
102
|
|
|
17
103
|
`app upgrade-template` backfills additive template files that are missing from
|
|
18
104
|
older projects. This includes the Dynamic Worker MCP sample endpoint and the
|
|
19
105
|
shared `mcp/` helper folder; existing local MCP files are left untouched.
|
|
20
106
|
|
|
107
|
+
## Deployment Output
|
|
108
|
+
|
|
109
|
+
`ro app deploy` prints a post-deploy summary after the deploy succeeds:
|
|
110
|
+
|
|
111
|
+
- public Rodyssey app URL, e.g. `https://development-app.rodyssey.ai/webapp/<id>`
|
|
112
|
+
- published asset URL for SPA zip deployments
|
|
113
|
+
- registered Dynamic Worker endpoint URLs
|
|
114
|
+
- MCP server endpoint URLs for `api/mcp.ts`
|
|
115
|
+
- cron job names, schedules, enabled state, and script files
|
|
116
|
+
|
|
117
|
+
SPA deploys register built `dist/api/*` and `dist/cron-jobs/*` scripts after the
|
|
118
|
+
HTML zip has deployed. Fullstack deploys register the same Dynamic Worker
|
|
119
|
+
scripts after `wrangler deploy` and widget manifest sync.
|
|
120
|
+
|
|
21
121
|
## Release
|
|
22
122
|
|
|
23
123
|
Release automation lives in `.github/workflows/release.yml` and uses Changesets.
|
package/dist/cli.js
CHANGED
|
@@ -2071,14 +2071,16 @@ var {
|
|
|
2071
2071
|
// package.json
|
|
2072
2072
|
var package_default = {
|
|
2073
2073
|
name: "@rodyssey/cli",
|
|
2074
|
-
version: "0.
|
|
2074
|
+
version: "0.3.1",
|
|
2075
2075
|
description: "Scaffold new projects from airconcepts templates",
|
|
2076
2076
|
repository: {
|
|
2077
2077
|
type: "git",
|
|
2078
2078
|
url: "git+https://github.com/airconcepts/ro-cli.git"
|
|
2079
2079
|
},
|
|
2080
2080
|
bin: {
|
|
2081
|
-
"@rodyssey/cli": "dist/cli.js"
|
|
2081
|
+
"@rodyssey/cli": "dist/cli.js",
|
|
2082
|
+
ro: "dist/cli.js",
|
|
2083
|
+
rocli: "dist/cli.js"
|
|
2082
2084
|
},
|
|
2083
2085
|
files: [
|
|
2084
2086
|
"dist"
|
|
@@ -2123,19 +2125,47 @@ var CMS_BASE_URLS = {
|
|
|
2123
2125
|
staging: "https://staging-cms.rodyssey.ai",
|
|
2124
2126
|
production: "https://cms.rodyssey.ai"
|
|
2125
2127
|
};
|
|
2126
|
-
|
|
2128
|
+
function describeSession(session) {
|
|
2129
|
+
const base = { env: session.env, cmsUrl: session.cmsUrl };
|
|
2130
|
+
if (session.identity?.type === "service-account") {
|
|
2131
|
+
return {
|
|
2132
|
+
...base,
|
|
2133
|
+
identityLine: `Logged in as service account: ${session.identity.label} (id: ${session.identity.id})`,
|
|
2134
|
+
scopesLine: `Granted scopes: ${session.identity.scopes.join(", ")}`
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
if (session.identity?.type === "user") {
|
|
2138
|
+
return {
|
|
2139
|
+
...base,
|
|
2140
|
+
identityLine: `Logged in as: ${session.identity.label}`,
|
|
2141
|
+
scopesLine: `Granted scopes: ${session.identity.scopes.join(", ")}`
|
|
2142
|
+
};
|
|
2143
|
+
}
|
|
2144
|
+
return {
|
|
2145
|
+
...base,
|
|
2146
|
+
identityLine: "Logged in (legacy session — no identity block stored)"
|
|
2147
|
+
};
|
|
2148
|
+
}
|
|
2149
|
+
function configPath() {
|
|
2150
|
+
const override = process.env.RODYSSEY_CONFIG_DIR;
|
|
2151
|
+
if (override)
|
|
2152
|
+
return join(override, "config.json");
|
|
2153
|
+
return join(homedir(), ".rodyssey", "config.json");
|
|
2154
|
+
}
|
|
2127
2155
|
function readConfig() {
|
|
2128
|
-
|
|
2156
|
+
const file = configPath();
|
|
2157
|
+
if (!existsSync(file))
|
|
2129
2158
|
return {};
|
|
2130
2159
|
try {
|
|
2131
|
-
return JSON.parse(readFileSync(
|
|
2160
|
+
return JSON.parse(readFileSync(file, "utf-8"));
|
|
2132
2161
|
} catch {
|
|
2133
2162
|
return {};
|
|
2134
2163
|
}
|
|
2135
2164
|
}
|
|
2136
2165
|
function writeConfig(config) {
|
|
2137
|
-
|
|
2138
|
-
|
|
2166
|
+
const file = configPath();
|
|
2167
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
2168
|
+
writeFileSync(file, `${JSON.stringify(config, null, 2)}
|
|
2139
2169
|
`, "utf-8");
|
|
2140
2170
|
}
|
|
2141
2171
|
function getAuthConfig(config) {
|
|
@@ -2167,6 +2197,38 @@ function extractUser(payload) {
|
|
|
2167
2197
|
return;
|
|
2168
2198
|
return payload.user ?? (isObject(payload.data) ? payload.data.user : undefined);
|
|
2169
2199
|
}
|
|
2200
|
+
function parseIdentity(raw) {
|
|
2201
|
+
if (!isObject(raw))
|
|
2202
|
+
return;
|
|
2203
|
+
if (raw.type !== "user" && raw.type !== "service-account")
|
|
2204
|
+
return;
|
|
2205
|
+
if (typeof raw.id !== "string" || typeof raw.label !== "string")
|
|
2206
|
+
return;
|
|
2207
|
+
if (!Array.isArray(raw.scopes) || !raw.scopes.every((s) => typeof s === "string")) {
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
return {
|
|
2211
|
+
type: raw.type,
|
|
2212
|
+
id: raw.id,
|
|
2213
|
+
label: raw.label,
|
|
2214
|
+
scopes: raw.scopes
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
function extractSessionFromTokenResponse(payload, ctx) {
|
|
2218
|
+
const token = extractToken(payload);
|
|
2219
|
+
if (!token) {
|
|
2220
|
+
throw new Error(`CMS token response did not include an auth token:
|
|
2221
|
+
${JSON.stringify(payload, null, 2)}`);
|
|
2222
|
+
}
|
|
2223
|
+
const identity = isObject(payload) ? parseIdentity(payload.identity) : undefined;
|
|
2224
|
+
const user = extractUser(payload);
|
|
2225
|
+
const session = { env: ctx.env, cmsUrl: ctx.cmsUrl, token };
|
|
2226
|
+
if (user !== undefined)
|
|
2227
|
+
session.user = user;
|
|
2228
|
+
if (identity)
|
|
2229
|
+
session.identity = identity;
|
|
2230
|
+
return session;
|
|
2231
|
+
}
|
|
2170
2232
|
async function readResponsePayload(response) {
|
|
2171
2233
|
const text = await response.text();
|
|
2172
2234
|
if (!text)
|
|
@@ -2365,17 +2427,7 @@ ${authorizationUrl.toString()}
|
|
|
2365
2427
|
throw new Error(`CMS token exchange failed: ${response.status} ${response.statusText}
|
|
2366
2428
|
${JSON.stringify(payload, null, 2)}`);
|
|
2367
2429
|
}
|
|
2368
|
-
const
|
|
2369
|
-
if (!token) {
|
|
2370
|
-
throw new Error(`CMS token response did not include an auth token:
|
|
2371
|
-
${JSON.stringify(payload, null, 2)}`);
|
|
2372
|
-
}
|
|
2373
|
-
const session = {
|
|
2374
|
-
env: options.env,
|
|
2375
|
-
cmsUrl,
|
|
2376
|
-
token,
|
|
2377
|
-
user: extractUser(payload)
|
|
2378
|
-
};
|
|
2430
|
+
const session = extractSessionFromTokenResponse(payload, { env: options.env, cmsUrl });
|
|
2379
2431
|
if (options.persist !== false) {
|
|
2380
2432
|
storeSession(session);
|
|
2381
2433
|
}
|
|
@@ -2383,18 +2435,25 @@ ${JSON.stringify(payload, null, 2)}`);
|
|
|
2383
2435
|
}
|
|
2384
2436
|
async function me(options) {
|
|
2385
2437
|
const storedSession = getStoredSession(options.env);
|
|
2438
|
+
if (!storedSession) {
|
|
2439
|
+
throw new Error(`No local CMS session found for [${options.env}]. Run \`ro auth login --env ${options.env}\` first.`);
|
|
2440
|
+
}
|
|
2441
|
+
const description = describeSession(storedSession);
|
|
2442
|
+
console.log(`Environment: ${description.env}`);
|
|
2443
|
+
console.log(`CMS URL: ${description.cmsUrl}`);
|
|
2444
|
+
console.log(description.identityLine);
|
|
2445
|
+
if (description.scopesLine)
|
|
2446
|
+
console.log(description.scopesLine);
|
|
2386
2447
|
if (!options.remote) {
|
|
2387
|
-
if (!storedSession) {
|
|
2388
|
-
throw new Error(`No local CMS session found for [${options.env}]. Run \`ro auth login --env ${options.env}\` first.`);
|
|
2389
|
-
}
|
|
2390
2448
|
return {
|
|
2391
2449
|
env: storedSession.env,
|
|
2392
2450
|
cmsUrl: storedSession.cmsUrl,
|
|
2393
2451
|
loggedIn: true,
|
|
2452
|
+
identity: storedSession.identity ?? null,
|
|
2394
2453
|
user: storedSession.user ?? null
|
|
2395
2454
|
};
|
|
2396
2455
|
}
|
|
2397
|
-
const cmsUrl = resolveCmsUrl(options.env, options.cmsUrl || storedSession
|
|
2456
|
+
const cmsUrl = resolveCmsUrl(options.env, options.cmsUrl || storedSession.cmsUrl);
|
|
2398
2457
|
const response = await fetch(options.meUrl || `${cmsUrl}/api/auth/me`, {
|
|
2399
2458
|
method: "GET",
|
|
2400
2459
|
headers: {
|
|
@@ -2407,6 +2466,9 @@ async function me(options) {
|
|
|
2407
2466
|
throw new Error(`CMS me failed: ${response.status} ${response.statusText}
|
|
2408
2467
|
${JSON.stringify(payload, null, 2)}`);
|
|
2409
2468
|
}
|
|
2469
|
+
console.log(`
|
|
2470
|
+
Server view:`);
|
|
2471
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
2410
2472
|
return payload;
|
|
2411
2473
|
}
|
|
2412
2474
|
|
|
@@ -3802,6 +3864,12 @@ var DEPLOY_URLS = {
|
|
|
3802
3864
|
staging: "https://staging-cms.rodyssey.ai/api/webapps/deploy",
|
|
3803
3865
|
production: "https://cms.rodyssey.ai/api/webapps/deploy"
|
|
3804
3866
|
};
|
|
3867
|
+
var WEB_CLIENT_URLS = {
|
|
3868
|
+
local: "http://localhost:5178",
|
|
3869
|
+
development: "https://development-app.rodyssey.ai",
|
|
3870
|
+
staging: "https://staging-app.rodyssey.ai",
|
|
3871
|
+
production: "https://app.rodyssey.ai"
|
|
3872
|
+
};
|
|
3805
3873
|
var BUILD_DIR = "dist";
|
|
3806
3874
|
var ZIP_FILE = "webapp-build.zip";
|
|
3807
3875
|
var FULLSTACK_DEPLOY_ENVS = new Set(["development", "staging", "production"]);
|
|
@@ -3810,9 +3878,204 @@ var MAX_SIZE_PER_BATCH = 30 * 1024 * 1024;
|
|
|
3810
3878
|
function shellQuote(value) {
|
|
3811
3879
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
3812
3880
|
}
|
|
3881
|
+
function isRecord(value) {
|
|
3882
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3883
|
+
}
|
|
3884
|
+
function pickString2(value) {
|
|
3885
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
3886
|
+
}
|
|
3887
|
+
function pickNumber(value) {
|
|
3888
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
3889
|
+
}
|
|
3813
3890
|
function isFullstackProject() {
|
|
3814
3891
|
return existsSync4("app") && existsSync4("workers/app.ts") && existsSync4("wrangler.jsonc");
|
|
3815
3892
|
}
|
|
3893
|
+
function resolveDeployUrl(env, overrides) {
|
|
3894
|
+
let deployUrl = DEPLOY_URLS[env];
|
|
3895
|
+
if (!deployUrl) {
|
|
3896
|
+
throw new Error(`Unknown environment "${env}". Available: ${Object.keys(DEPLOY_URLS).join(", ")}`);
|
|
3897
|
+
}
|
|
3898
|
+
if (overrides.host || overrides.port) {
|
|
3899
|
+
const url = new URL(deployUrl);
|
|
3900
|
+
if (overrides.host)
|
|
3901
|
+
url.hostname = overrides.host;
|
|
3902
|
+
if (overrides.port)
|
|
3903
|
+
url.port = String(overrides.port);
|
|
3904
|
+
deployUrl = url.toString().replace(/\/$/, "");
|
|
3905
|
+
}
|
|
3906
|
+
return deployUrl;
|
|
3907
|
+
}
|
|
3908
|
+
function resolveScriptsSetupUrl(deployUrl) {
|
|
3909
|
+
return deployUrl.replace("/webapps/deploy", "/webapps/scripts-setup");
|
|
3910
|
+
}
|
|
3911
|
+
function resolveWebClientBaseUrl(env) {
|
|
3912
|
+
return WEB_CLIENT_URLS[env];
|
|
3913
|
+
}
|
|
3914
|
+
function buildPublicWebappUrl(env, webappId) {
|
|
3915
|
+
const baseUrl = resolveWebClientBaseUrl(env);
|
|
3916
|
+
if (!baseUrl)
|
|
3917
|
+
return;
|
|
3918
|
+
const url = new URL(`/webapp/${encodeURIComponent(webappId)}`, baseUrl);
|
|
3919
|
+
return url.toString();
|
|
3920
|
+
}
|
|
3921
|
+
function encodeEndpointPath(endpointPath) {
|
|
3922
|
+
return endpointPath.split("/").filter(Boolean).map((segment) => encodeURIComponent(segment)).join("/");
|
|
3923
|
+
}
|
|
3924
|
+
function buildDynamicEndpointUrl(env, webappId, endpointPath) {
|
|
3925
|
+
const baseUrl = resolveWebClientBaseUrl(env);
|
|
3926
|
+
if (!baseUrl)
|
|
3927
|
+
return;
|
|
3928
|
+
const encodedEndpoint = encodeEndpointPath(endpointPath);
|
|
3929
|
+
const url = new URL(`/api/webapps/${encodeURIComponent(webappId)}/endpoints/${encodedEndpoint}`, baseUrl);
|
|
3930
|
+
return url.toString();
|
|
3931
|
+
}
|
|
3932
|
+
function normalizeBuildRelativePath(filePath) {
|
|
3933
|
+
return filePath.substring(BUILD_DIR.length + 1).replace(/\\/g, "/");
|
|
3934
|
+
}
|
|
3935
|
+
function isScriptFile(filePath) {
|
|
3936
|
+
const relativePath = normalizeBuildRelativePath(filePath);
|
|
3937
|
+
if (relativePath === "cron-jobs/cron.config.json")
|
|
3938
|
+
return true;
|
|
3939
|
+
return (relativePath.startsWith("api/") || relativePath.startsWith("cron-jobs/")) && relativePath.endsWith(".js");
|
|
3940
|
+
}
|
|
3941
|
+
function isMcpEndpoint(endpointPath) {
|
|
3942
|
+
const segments = endpointPath.split("/").filter(Boolean);
|
|
3943
|
+
return segments[segments.length - 1]?.toLowerCase() === "mcp";
|
|
3944
|
+
}
|
|
3945
|
+
function resolveCronScriptCode(payload, scriptName) {
|
|
3946
|
+
const scriptNameKey = scriptName.replace(/\.js$/, "");
|
|
3947
|
+
return payload.cron[scriptNameKey] || payload.cron[scriptName];
|
|
3948
|
+
}
|
|
3949
|
+
function readCronConfig(filePath, content) {
|
|
3950
|
+
const parsed = JSON.parse(content);
|
|
3951
|
+
if (!isRecord(parsed)) {
|
|
3952
|
+
throw new Error(`Cron config must be a JSON object: ${filePath}`);
|
|
3953
|
+
}
|
|
3954
|
+
const jobs = parsed.jobs;
|
|
3955
|
+
if (jobs !== undefined && !Array.isArray(jobs)) {
|
|
3956
|
+
throw new Error(`Cron config jobs must be an array: ${filePath}`);
|
|
3957
|
+
}
|
|
3958
|
+
return parsed;
|
|
3959
|
+
}
|
|
3960
|
+
function collectScripts(scriptFiles) {
|
|
3961
|
+
const payload = { api: {}, cron: {}, cronConfig: null };
|
|
3962
|
+
const summary = { apiEndpoints: [], cronJobs: [], mcpEndpoints: [] };
|
|
3963
|
+
for (const filePath of scriptFiles) {
|
|
3964
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
3965
|
+
const relativePath = normalizeBuildRelativePath(filePath);
|
|
3966
|
+
if (relativePath === "cron-jobs/cron.config.json") {
|
|
3967
|
+
payload.cronConfig = readCronConfig(filePath, content);
|
|
3968
|
+
summary.cronJobs = payload.cronConfig.jobs ?? [];
|
|
3969
|
+
} else if (relativePath.startsWith("api/")) {
|
|
3970
|
+
const endpoint = relativePath.substring(4, relativePath.lastIndexOf(".js"));
|
|
3971
|
+
payload.api[endpoint] = content;
|
|
3972
|
+
summary.apiEndpoints.push(endpoint);
|
|
3973
|
+
if (isMcpEndpoint(endpoint)) {
|
|
3974
|
+
summary.mcpEndpoints.push(endpoint);
|
|
3975
|
+
}
|
|
3976
|
+
} else if (relativePath.startsWith("cron-jobs/")) {
|
|
3977
|
+
const scriptName = relativePath.substring(10);
|
|
3978
|
+
payload.cron[scriptName] = content;
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
summary.cronJobs = summary.cronJobs.filter((job) => {
|
|
3982
|
+
const hasScriptCode = !!resolveCronScriptCode(payload, job.script);
|
|
3983
|
+
if (!hasScriptCode) {
|
|
3984
|
+
console.warn(`⚠️ Cron job "${job.name}" references missing script "${job.script}" and will not be registered.`);
|
|
3985
|
+
}
|
|
3986
|
+
return hasScriptCode;
|
|
3987
|
+
});
|
|
3988
|
+
return { payload, summary };
|
|
3989
|
+
}
|
|
3990
|
+
function extractDeploymentWebapp(result) {
|
|
3991
|
+
if (!isRecord(result) || !isRecord(result.webapp))
|
|
3992
|
+
return {};
|
|
3993
|
+
return {
|
|
3994
|
+
id: pickString2(result.webapp.id),
|
|
3995
|
+
title: pickString2(result.webapp.title),
|
|
3996
|
+
appUrl: pickString2(result.webapp.appUrl)
|
|
3997
|
+
};
|
|
3998
|
+
}
|
|
3999
|
+
function printScriptsSetupCounts(result) {
|
|
4000
|
+
if (!isRecord(result))
|
|
4001
|
+
return;
|
|
4002
|
+
const parts = [
|
|
4003
|
+
["API registered", pickNumber(result.apiCreated)],
|
|
4004
|
+
["API deleted", pickNumber(result.apiDeleted)],
|
|
4005
|
+
["Cron registered", pickNumber(result.cronCreated)],
|
|
4006
|
+
["Cron deleted", pickNumber(result.cronDeleted)],
|
|
4007
|
+
["Cache invalidated", pickNumber(result.cacheInvalidated)]
|
|
4008
|
+
].filter((entry) => entry[1] !== undefined).map(([label, count]) => `${label}: ${count}`);
|
|
4009
|
+
if (parts.length > 0) {
|
|
4010
|
+
console.log(` ${parts.join(" · ")}`);
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
function printPostDeploySummary(options) {
|
|
4014
|
+
const webapp = extractDeploymentWebapp(options.deploymentResult);
|
|
4015
|
+
const webappId = webapp.id || process.env.WEBAPP_ID;
|
|
4016
|
+
console.log(`
|
|
4017
|
+
\uD83D\uDD17 Deployment URLs`);
|
|
4018
|
+
if (webappId) {
|
|
4019
|
+
const appUrl = buildPublicWebappUrl(options.env, webappId);
|
|
4020
|
+
if (appUrl) {
|
|
4021
|
+
console.log(`App URL: ${appUrl}`);
|
|
4022
|
+
}
|
|
4023
|
+
} else {
|
|
4024
|
+
console.log("App URL: unavailable (WEBAPP_ID was not found)");
|
|
4025
|
+
}
|
|
4026
|
+
if (webapp.appUrl) {
|
|
4027
|
+
console.log(`Published asset URL: ${webapp.appUrl}`);
|
|
4028
|
+
}
|
|
4029
|
+
if (options.scriptsSummary.apiEndpoints.length > 0) {
|
|
4030
|
+
console.log(`
|
|
4031
|
+
\uD83C\uDF10 Dynamic endpoints`);
|
|
4032
|
+
for (const endpoint of options.scriptsSummary.apiEndpoints) {
|
|
4033
|
+
const url = webappId ? buildDynamicEndpointUrl(options.env, webappId, endpoint) : undefined;
|
|
4034
|
+
console.log(`- ${endpoint}${url ? `: ${url}` : ""}`);
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
if (options.scriptsSummary.mcpEndpoints.length > 0) {
|
|
4038
|
+
console.log(`
|
|
4039
|
+
\uD83E\uDDE9 MCP server URLs`);
|
|
4040
|
+
for (const endpoint of options.scriptsSummary.mcpEndpoints) {
|
|
4041
|
+
const url = webappId ? buildDynamicEndpointUrl(options.env, webappId, endpoint) : undefined;
|
|
4042
|
+
console.log(`- ${endpoint}${url ? `: ${url}` : ""}`);
|
|
4043
|
+
}
|
|
4044
|
+
}
|
|
4045
|
+
if (options.scriptsSummary.cronJobs.length > 0) {
|
|
4046
|
+
console.log(`
|
|
4047
|
+
⏱️ Cron jobs`);
|
|
4048
|
+
for (const job of options.scriptsSummary.cronJobs) {
|
|
4049
|
+
const status = job.enabled === false ? "disabled" : "enabled";
|
|
4050
|
+
console.log(`- ${job.name}: ${job.schedule} (${status}, ${job.script})`);
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
async function syncScripts(deployUrl, scriptFiles, stepLabel) {
|
|
4055
|
+
const { payload, summary } = collectScripts(scriptFiles);
|
|
4056
|
+
if (scriptFiles.length === 0) {
|
|
4057
|
+
console.log(`\uD83D\uDCDC ${stepLabel}: No scripts found to sync.`);
|
|
4058
|
+
return { summary, result: undefined };
|
|
4059
|
+
}
|
|
4060
|
+
console.log(`\uD83D\uDCDC ${stepLabel}: Registering ${scriptFiles.length} scripts (APIs & Crons)...`);
|
|
4061
|
+
const scriptsUrl = resolveScriptsSetupUrl(deployUrl);
|
|
4062
|
+
const response = await fetch(scriptsUrl, {
|
|
4063
|
+
method: "POST",
|
|
4064
|
+
headers: {
|
|
4065
|
+
Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
|
|
4066
|
+
"Content-Type": "application/json"
|
|
4067
|
+
},
|
|
4068
|
+
body: JSON.stringify(payload)
|
|
4069
|
+
});
|
|
4070
|
+
const result = await readResponsePayload(response);
|
|
4071
|
+
if (!response.ok) {
|
|
4072
|
+
throw new Error(`Scripts setup failed: ${response.status} ${response.statusText}
|
|
4073
|
+
${JSON.stringify(result, null, 2)}`);
|
|
4074
|
+
}
|
|
4075
|
+
console.log("✅ Scripts registered successfully");
|
|
4076
|
+
printScriptsSetupCounts(result);
|
|
4077
|
+
return { summary, result };
|
|
4078
|
+
}
|
|
3816
4079
|
function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
3817
4080
|
if (!existsSync4(dirPath))
|
|
3818
4081
|
return arrayOfFiles;
|
|
@@ -3849,6 +4112,7 @@ async function deployFullstack(env, overrides) {
|
|
|
3849
4112
|
...process.env,
|
|
3850
4113
|
CLOUDFLARE_ENV: env
|
|
3851
4114
|
};
|
|
4115
|
+
const deployUrl = resolveDeployUrl(env, overrides);
|
|
3852
4116
|
console.log(`\uD83D\uDE80 Starting fullstack deployment process for [${env}] environment...
|
|
3853
4117
|
`);
|
|
3854
4118
|
console.log("\uD83D\uDCE6 Step 1: Building the fullstack webapp...");
|
|
@@ -3868,23 +4132,18 @@ async function deployFullstack(env, overrides) {
|
|
|
3868
4132
|
...overrides.port ? ["--port", shellQuote(String(overrides.port))] : []
|
|
3869
4133
|
];
|
|
3870
4134
|
execSync2(`bun run sync-widget-manifest -- ${syncArgs.join(" ")}`, { stdio: "inherit", env: childEnv });
|
|
4135
|
+
console.log();
|
|
4136
|
+
const scriptFiles = getAllFiles(BUILD_DIR).filter(isScriptFile);
|
|
4137
|
+
const scriptsSync = await syncScripts(deployUrl, scriptFiles, "Step 4");
|
|
4138
|
+
printPostDeploySummary({
|
|
4139
|
+
env,
|
|
4140
|
+
scriptsSummary: scriptsSync.summary
|
|
4141
|
+
});
|
|
3871
4142
|
console.log(`
|
|
3872
4143
|
✨ Fullstack deployment successful!`);
|
|
3873
4144
|
}
|
|
3874
4145
|
async function deploySpa(env, overrides) {
|
|
3875
|
-
|
|
3876
|
-
if (!DEPLOY_URL) {
|
|
3877
|
-
console.error(`❌ Unknown environment "${env}". Available: ${Object.keys(DEPLOY_URLS).join(", ")}`);
|
|
3878
|
-
process.exit(1);
|
|
3879
|
-
}
|
|
3880
|
-
if (overrides.host || overrides.port) {
|
|
3881
|
-
const url = new URL(DEPLOY_URL);
|
|
3882
|
-
if (overrides.host)
|
|
3883
|
-
url.hostname = overrides.host;
|
|
3884
|
-
if (overrides.port)
|
|
3885
|
-
url.port = String(overrides.port);
|
|
3886
|
-
DEPLOY_URL = url.toString().replace(/\/$/, "");
|
|
3887
|
-
}
|
|
4146
|
+
const DEPLOY_URL = resolveDeployUrl(env, overrides);
|
|
3888
4147
|
const ASSETS_URL = DEPLOY_URL.replace("/webapps/deploy", "/webapps/assets");
|
|
3889
4148
|
console.log(`\uD83D\uDE80 Starting deployment process for [${env}] environment...
|
|
3890
4149
|
`);
|
|
@@ -3902,7 +4161,7 @@ async function deploySpa(env, overrides) {
|
|
|
3902
4161
|
`);
|
|
3903
4162
|
const allFiles = getAllFiles(BUILD_DIR);
|
|
3904
4163
|
const htmlFiles = allFiles.filter((f) => f.endsWith(".html"));
|
|
3905
|
-
const scriptFiles = allFiles.filter(
|
|
4164
|
+
const scriptFiles = allFiles.filter(isScriptFile);
|
|
3906
4165
|
const heavyFiles = allFiles.filter((f) => !f.endsWith(".html") && !scriptFiles.includes(f));
|
|
3907
4166
|
console.log(`\uD83D\uDCE4 Step 2: Uploading ${heavyFiles.length} heavy assets...`);
|
|
3908
4167
|
if (heavyFiles.length > 0) {
|
|
@@ -3950,42 +4209,7 @@ ${errorText}`);
|
|
|
3950
4209
|
console.log("✅ No heavy assets to upload");
|
|
3951
4210
|
}
|
|
3952
4211
|
console.log();
|
|
3953
|
-
|
|
3954
|
-
console.log(`\uD83D\uDCDC Step 3: Setting up ${scriptFiles.length} scripts (APIs & Crons)...`);
|
|
3955
|
-
const scriptsPayload = { api: {}, cron: {}, cronConfig: null };
|
|
3956
|
-
for (const f of scriptFiles) {
|
|
3957
|
-
const content = readFileSync3(f, "utf-8");
|
|
3958
|
-
const relativePath = f.substring(BUILD_DIR.length + 1).replace(/\\/g, "/");
|
|
3959
|
-
if (relativePath === "cron-jobs/cron.config.json") {
|
|
3960
|
-
scriptsPayload.cronConfig = JSON.parse(content);
|
|
3961
|
-
} else if (relativePath.startsWith("api/")) {
|
|
3962
|
-
const endpoint = relativePath.substring(4, relativePath.lastIndexOf(".js"));
|
|
3963
|
-
scriptsPayload.api[endpoint] = content;
|
|
3964
|
-
} else if (relativePath.startsWith("cron-jobs/")) {
|
|
3965
|
-
const scriptName = relativePath.substring(10);
|
|
3966
|
-
scriptsPayload.cron[scriptName] = content;
|
|
3967
|
-
}
|
|
3968
|
-
}
|
|
3969
|
-
const scriptsUrl = DEPLOY_URL.replace("/webapps/deploy", "/webapps/scripts-setup");
|
|
3970
|
-
const response = await fetch(scriptsUrl, {
|
|
3971
|
-
method: "POST",
|
|
3972
|
-
headers: {
|
|
3973
|
-
Authorization: `Bearer ${process.env.DEPLOY_TOKEN}`,
|
|
3974
|
-
"Content-Type": "application/json"
|
|
3975
|
-
},
|
|
3976
|
-
body: JSON.stringify(scriptsPayload)
|
|
3977
|
-
});
|
|
3978
|
-
if (!response.ok) {
|
|
3979
|
-
const errorText = await response.text();
|
|
3980
|
-
throw new Error(`Scripts setup failed: ${response.status} ${response.statusText}
|
|
3981
|
-
${errorText}`);
|
|
3982
|
-
}
|
|
3983
|
-
console.log(`✅ Scripts synced successfully`);
|
|
3984
|
-
} else {
|
|
3985
|
-
console.log(`\uD83D\uDCDC Step 3: No scripts found to sync.`);
|
|
3986
|
-
}
|
|
3987
|
-
console.log();
|
|
3988
|
-
console.log(`\uD83D\uDDDC️ Step 4: Zipping ${htmlFiles.length} HTML files...`);
|
|
4212
|
+
console.log(`\uD83D\uDDDC️ Step 3: Zipping ${htmlFiles.length} HTML files...`);
|
|
3989
4213
|
if (htmlFiles.length === 0) {
|
|
3990
4214
|
console.warn("⚠️ No HTML files found to zip! Deployment might fail if CMS expects an HTML file.");
|
|
3991
4215
|
}
|
|
@@ -3993,7 +4217,7 @@ ${errorText}`);
|
|
|
3993
4217
|
execSync2(`cd ${BUILD_DIR} && zip ../${ZIP_FILE} ${relativeHtmlFiles.join(" ")}`, { stdio: "inherit" });
|
|
3994
4218
|
console.log(`✅ Created ${ZIP_FILE}
|
|
3995
4219
|
`);
|
|
3996
|
-
console.log("☁️ Step
|
|
4220
|
+
console.log("☁️ Step 4: Deploying HTML zip to server...");
|
|
3997
4221
|
const zipBuffer = readFileSync3(ZIP_FILE);
|
|
3998
4222
|
try {
|
|
3999
4223
|
const response = await fetch(DEPLOY_URL, {
|
|
@@ -4009,10 +4233,17 @@ ${errorText}`);
|
|
|
4009
4233
|
throw new Error(`Deploy failed: ${response.status} ${response.statusText}
|
|
4010
4234
|
${errorText}`);
|
|
4011
4235
|
}
|
|
4012
|
-
const result = await response
|
|
4236
|
+
const result = await readResponsePayload(response);
|
|
4013
4237
|
console.log("✅ Deploy completed");
|
|
4014
4238
|
console.log(`
|
|
4015
4239
|
\uD83D\uDCCB Deployment result:`, result);
|
|
4240
|
+
console.log();
|
|
4241
|
+
const scriptsSync = await syncScripts(DEPLOY_URL, scriptFiles, "Step 5");
|
|
4242
|
+
printPostDeploySummary({
|
|
4243
|
+
env,
|
|
4244
|
+
deploymentResult: result,
|
|
4245
|
+
scriptsSummary: scriptsSync.summary
|
|
4246
|
+
});
|
|
4016
4247
|
} catch (error) {
|
|
4017
4248
|
console.error("❌ Deploy failed:", error);
|
|
4018
4249
|
throw error;
|
|
@@ -4537,7 +4768,7 @@ var PROD_ENV_FILE = ".env.production";
|
|
|
4537
4768
|
function isObject3(value) {
|
|
4538
4769
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
4539
4770
|
}
|
|
4540
|
-
function
|
|
4771
|
+
function pickString3(...values) {
|
|
4541
4772
|
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
4542
4773
|
}
|
|
4543
4774
|
function nestedObject2(value, key) {
|
|
@@ -4548,7 +4779,7 @@ function nestedObject2(value, key) {
|
|
|
4548
4779
|
}
|
|
4549
4780
|
function extractDeployToken2(payload) {
|
|
4550
4781
|
const webapp = nestedObject2(payload, "webapp");
|
|
4551
|
-
return
|
|
4782
|
+
return pickString3(webapp?.deployToken, webapp?.deploymentToken);
|
|
4552
4783
|
}
|
|
4553
4784
|
function unwrapSourceDetails(payload) {
|
|
4554
4785
|
if (!isObject3(payload))
|
|
@@ -5047,6 +5278,19 @@ async function updateGameSdk() {
|
|
|
5047
5278
|
}
|
|
5048
5279
|
|
|
5049
5280
|
// src/cli.ts
|
|
5281
|
+
function renderError(err) {
|
|
5282
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5283
|
+
console.error(`
|
|
5284
|
+
Error: ${msg}`);
|
|
5285
|
+
if (process.env.RO_DEBUG && err instanceof Error && err.stack) {
|
|
5286
|
+
console.error(`
|
|
5287
|
+
${err.stack}`);
|
|
5288
|
+
}
|
|
5289
|
+
console.error();
|
|
5290
|
+
process.exit(1);
|
|
5291
|
+
}
|
|
5292
|
+
process.on("unhandledRejection", renderError);
|
|
5293
|
+
process.on("uncaughtException", renderError);
|
|
5050
5294
|
var TEMPLATES2 = {
|
|
5051
5295
|
webapp: {
|
|
5052
5296
|
name: "webapp",
|
|
@@ -5110,8 +5354,7 @@ function addAuthCommands(parent) {
|
|
|
5110
5354
|
console.log(`\uD83D\uDCCD CMS URL: ${session.cmsUrl}`);
|
|
5111
5355
|
});
|
|
5112
5356
|
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) => {
|
|
5113
|
-
|
|
5114
|
-
console.log(JSON.stringify(currentUser, null, 2));
|
|
5357
|
+
await me(options);
|
|
5115
5358
|
});
|
|
5116
5359
|
return auth;
|
|
5117
5360
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rodyssey/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Scaffold new projects from airconcepts templates",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/airconcepts/ro-cli.git"
|
|
8
8
|
},
|
|
9
9
|
"bin": {
|
|
10
|
-
"@rodyssey/cli": "dist/cli.js"
|
|
10
|
+
"@rodyssey/cli": "dist/cli.js",
|
|
11
|
+
"ro": "dist/cli.js",
|
|
12
|
+
"rocli": "dist/cli.js"
|
|
11
13
|
},
|
|
12
14
|
"files": [
|
|
13
15
|
"dist"
|