@rodyssey/cli 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +89 -3
  2. package/dist/cli.js +98 -24
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -1,17 +1,103 @@
1
1
  # ro-cli
2
2
 
3
- To install dependencies:
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
- To run:
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
- bun run index.ts
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
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.2.1",
2074
+ version: "0.3.0",
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
- var CONFIG_FILE = join(homedir(), ".rodyssey", "config.json");
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
- if (!existsSync(CONFIG_FILE))
2156
+ const file = configPath();
2157
+ if (!existsSync(file))
2129
2158
  return {};
2130
2159
  try {
2131
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
2160
+ return JSON.parse(readFileSync(file, "utf-8"));
2132
2161
  } catch {
2133
2162
  return {};
2134
2163
  }
2135
2164
  }
2136
2165
  function writeConfig(config) {
2137
- mkdirSync(dirname(CONFIG_FILE), { recursive: true });
2138
- writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}
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 token = extractToken(payload);
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?.cmsUrl);
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
 
@@ -5047,6 +5109,19 @@ async function updateGameSdk() {
5047
5109
  }
5048
5110
 
5049
5111
  // src/cli.ts
5112
+ function renderError(err) {
5113
+ const msg = err instanceof Error ? err.message : String(err);
5114
+ console.error(`
5115
+ Error: ${msg}`);
5116
+ if (process.env.RO_DEBUG && err instanceof Error && err.stack) {
5117
+ console.error(`
5118
+ ${err.stack}`);
5119
+ }
5120
+ console.error();
5121
+ process.exit(1);
5122
+ }
5123
+ process.on("unhandledRejection", renderError);
5124
+ process.on("uncaughtException", renderError);
5050
5125
  var TEMPLATES2 = {
5051
5126
  webapp: {
5052
5127
  name: "webapp",
@@ -5110,8 +5185,7 @@ function addAuthCommands(parent) {
5110
5185
  console.log(`\uD83D\uDCCD CMS URL: ${session.cmsUrl}`);
5111
5186
  });
5112
5187
  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
- const currentUser = await me(options);
5114
- console.log(JSON.stringify(currentUser, null, 2));
5188
+ await me(options);
5115
5189
  });
5116
5190
  return auth;
5117
5191
  }
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@rodyssey/cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
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"