@genex-ai/cli-demo 0.1.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 (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -0
  3. package/package.json +45 -0
  4. package/src/commands/init.ts +131 -0
  5. package/src/commands/publish.ts +151 -0
  6. package/src/config.ts +102 -0
  7. package/src/index.ts +238 -0
  8. package/src/lib/auth.ts +365 -0
  9. package/src/lib/copy-templates.ts +81 -0
  10. package/src/lib/env.ts +109 -0
  11. package/src/lib/project.ts +109 -0
  12. package/src/lib/ssh.ts +104 -0
  13. package/src/lib/store.ts +102 -0
  14. package/src/utils/colors.ts +25 -0
  15. package/src/utils/logger.ts +40 -0
  16. package/templates/README.md +19 -0
  17. package/templates/agents/genex-helper.md +16 -0
  18. package/templates/commands/genex-status.md +13 -0
  19. package/templates/skills/genex-getting-started/SKILL.md +41 -0
  20. package/templates/skills/genex-threejs-atmosphere-aerial-perspective/SKILL.md +30 -0
  21. package/templates/skills/genex-threejs-atmosphere-aerial-perspective/references/atmosphere.md +29 -0
  22. package/templates/skills/genex-threejs-bloom/SKILL.md +30 -0
  23. package/templates/skills/genex-threejs-bloom/references/bloom.md +29 -0
  24. package/templates/skills/genex-threejs-camera-direction/SKILL.md +36 -0
  25. package/templates/skills/genex-threejs-camera-direction/references/camera-rigs.md +38 -0
  26. package/templates/skills/genex-threejs-exposure-color-grading/SKILL.md +30 -0
  27. package/templates/skills/genex-threejs-exposure-color-grading/references/exposure-grading.md +30 -0
  28. package/templates/skills/genex-threejs-image-pipeline/SKILL.md +30 -0
  29. package/templates/skills/genex-threejs-image-pipeline/references/image-pipeline.md +39 -0
  30. package/templates/skills/genex-threejs-procedural-animation/SKILL.md +31 -0
  31. package/templates/skills/genex-threejs-procedural-animation/references/procedural-motion.md +33 -0
  32. package/templates/skills/genex-threejs-procedural-architecture/SKILL.md +30 -0
  33. package/templates/skills/genex-threejs-procedural-architecture/references/architecture-systems.md +31 -0
  34. package/templates/skills/genex-threejs-procedural-fields/SKILL.md +31 -0
  35. package/templates/skills/genex-threejs-procedural-fields/references/field-systems.md +35 -0
  36. package/templates/skills/genex-threejs-procedural-geometry/SKILL.md +30 -0
  37. package/templates/skills/genex-threejs-procedural-geometry/references/mesh-systems.md +36 -0
  38. package/templates/skills/genex-threejs-procedural-materials/SKILL.md +30 -0
  39. package/templates/skills/genex-threejs-procedural-materials/references/material-systems.md +31 -0
  40. package/templates/skills/genex-threejs-procedural-planets/SKILL.md +30 -0
  41. package/templates/skills/genex-threejs-procedural-planets/references/planet-systems.md +30 -0
  42. package/templates/skills/genex-threejs-procedural-vegetation/SKILL.md +32 -0
  43. package/templates/skills/genex-threejs-procedural-vegetation/references/vegetation-systems.md +37 -0
  44. package/templates/skills/genex-threejs-procedural-vfx/SKILL.md +31 -0
  45. package/templates/skills/genex-threejs-procedural-vfx/references/vfx-systems.md +30 -0
  46. package/templates/skills/genex-threejs-raymarched-space-effects/SKILL.md +30 -0
  47. package/templates/skills/genex-threejs-raymarched-space-effects/references/space-effects.md +30 -0
  48. package/templates/skills/genex-threejs-screen-space-ambient-occlusion/SKILL.md +29 -0
  49. package/templates/skills/genex-threejs-screen-space-ambient-occlusion/references/ambient-occlusion.md +29 -0
  50. package/templates/skills/genex-threejs-shadow-systems/SKILL.md +30 -0
  51. package/templates/skills/genex-threejs-shadow-systems/references/shadow-systems.md +30 -0
  52. package/templates/skills/genex-threejs-skill-router/SKILL.md +48 -0
  53. package/templates/skills/genex-threejs-skill-router/references/routing-map.md +53 -0
  54. package/templates/skills/genex-threejs-spectral-ocean/SKILL.md +30 -0
  55. package/templates/skills/genex-threejs-spectral-ocean/references/spectral-ocean.md +31 -0
  56. package/templates/skills/genex-threejs-temporal-surfaces/SKILL.md +30 -0
  57. package/templates/skills/genex-threejs-temporal-surfaces/references/temporal-surfaces.md +29 -0
  58. package/templates/skills/genex-threejs-visual-validation/SKILL.md +32 -0
  59. package/templates/skills/genex-threejs-visual-validation/references/visual-validation.md +42 -0
  60. package/templates/skills/genex-threejs-volumetric-clouds/SKILL.md +29 -0
  61. package/templates/skills/genex-threejs-volumetric-clouds/references/volumetric-clouds.md +30 -0
  62. package/templates/skills/genex-threejs-water-optics/SKILL.md +30 -0
  63. package/templates/skills/genex-threejs-water-optics/references/water-optics.md +29 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 me-ai-org
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,198 @@
1
+ # @genex-ai/cli-demo
2
+
3
+ Set up your `~/.claude` workspace, authorize, create a game project, and publish.
4
+ This is the `cli` app of the [genex monorepo](../../README.md).
5
+
6
+ ```bash
7
+ genex init <name> # authorize + create the draft project
8
+ genex publish # push the built game + list it in the gallery
9
+ ```
10
+
11
+ `genex init` does four things:
12
+
13
+ 1. **Scaffolds your workspace** β€” copies the bundled templates (skills, agents,
14
+ commands) into `~/.claude`, *merging* into whatever is already there. Existing
15
+ files are **never overwritten**.
16
+ 2. **Authorizes you** β€” opens the Genex auth site (web) in your browser. If the
17
+ browser can't open, it prints the URL to open manually.
18
+ 3. **Saves your token** β€” writes `GENEX_TOKEN` to `~/.genex/env` (per-user;
19
+ reused across projects).
20
+ 4. **Creates the draft project** β€” generates a per-project SSH deploy key
21
+ (`genex_key`, gitignored automatically), registers its public half with a new
22
+ repo via `POST /api/projects`, and stores the project metadata (id, slug,
23
+ `sshUrl`, urls) in `./.genex/project.json`. The game shows up in your
24
+ dashboard's **My games** immediately.
25
+
26
+ `genex publish` reads that metadata, pushes the built game to GitHub over the
27
+ deploy key (best-effort β€” `index.html` must be at the repo root), then flips the
28
+ project to **published** so it appears in the public gallery. `git push` (updates
29
+ the live Pages site) and the gallery flag are independent; `--no-push` skips the
30
+ push. Defaults: API `https://demo-api.glotech.world`, auth site
31
+ `https://demo-web.glotech.world` β€” override with `--api-url` / `--auth-url` (or
32
+ `GENEX_API_URL` / `GENEX_AUTH_URL`) for local dev.
33
+
34
+ ## Install / run
35
+
36
+ From the monorepo root (the CLI runs directly via Node β€” no build step):
37
+
38
+ ```bash
39
+ pnpm start:cli init # = node apps/cli/src/index.ts init
40
+ ```
41
+
42
+ Or from this package:
43
+
44
+ ```bash
45
+ pnpm --filter @genex-ai/cli-demo start init
46
+ ```
47
+
48
+ Once published, end users run it with `npx`:
49
+
50
+ ```bash
51
+ npx @genex-ai/cli-demo init
52
+ ```
53
+
54
+ > Requires **Node β‰₯ 24** (the CLI is executed as TypeScript via Node's native
55
+ > type stripping, matching the rest of the monorepo).
56
+
57
+ ## Usage
58
+
59
+ ```
60
+ genex init [options]
61
+
62
+ Options
63
+ --dir <path> Destination workspace (default: ~/.claude)
64
+ --env <path> Path to the .env file to write (default: ./.env)
65
+ --auth-url <url> Override the auth site (default: https://genex.dev)
66
+ --no-auth Only scaffold templates; skip authorization
67
+ --force Overwrite existing files (default: never overwrite)
68
+ --timeout <seconds> How long to wait for the auth redirect (default: 300)
69
+ --quiet Reduce output
70
+
71
+ Global
72
+ -h, --help Show help
73
+ -v, --version Show version
74
+ ```
75
+
76
+ ### Examples
77
+
78
+ ```bash
79
+ # Full setup: scaffold + authorize
80
+ genex init
81
+
82
+ # Just scaffold the workspace, no auth
83
+ genex init --no-auth
84
+
85
+ # Point at a local auth server during development
86
+ genex init --auth-url http://localhost:3000
87
+ ```
88
+
89
+ > In the monorepo, substitute `pnpm start:cli` for `genex` (e.g.
90
+ > `pnpm start:cli init --no-auth`).
91
+
92
+ ## Configuration
93
+
94
+ The auth site URL is a single configurable value:
95
+
96
+ - **Default:** `https://genex.dev` (the `DEFAULT_AUTH_URL` constant in
97
+ [`src/config.ts`](src/config.ts)).
98
+ - **Override at runtime:** set `GENEX_AUTH_URL`, or pass `--auth-url`.
99
+
100
+ ```bash
101
+ GENEX_AUTH_URL=https://staging.genex.dev genex init
102
+ ```
103
+
104
+ To control which browser is launched, set `GENEX_BROWSER` (or the conventional
105
+ `BROWSER`) to an opener command. It's parsed like a shell command, so arguments
106
+ work and paths containing spaces should be quoted:
107
+
108
+ ```bash
109
+ GENEX_BROWSER="firefox" genex init
110
+ GENEX_BROWSER="open -a Safari" genex init
111
+ GENEX_BROWSER="'/Applications/My Browser.app/Contents/MacOS/My Browser'" genex init
112
+ ```
113
+
114
+ ## How authorization works
115
+
116
+ The CLI uses a loopback-redirect flow (the same pattern as `gh auth login` and
117
+ `gcloud`). The backend only needs to honor this contract:
118
+
119
+ 1. The CLI starts a temporary HTTP server on `http://127.0.0.1:<random-port>`
120
+ and opens the browser to:
121
+
122
+ ```
123
+ {AUTH_URL}/cli/auth?redirect_uri=http://127.0.0.1:<port>/callback&state=<random>
124
+ ```
125
+
126
+ 2. After the user logs in, the site returns the token to that `redirect_uri`,
127
+ either by:
128
+
129
+ - **Redirecting (GET):**
130
+ `http://127.0.0.1:<port>/callback?token=<TOKEN>&state=<random>`, **or**
131
+ - **Posting (POST):** a request to `http://127.0.0.1:<port>/callback` with a
132
+ body of `{"token":"<TOKEN>","state":"<random>"}` (JSON) or
133
+ `token=<TOKEN>&state=<random>` (form-encoded).
134
+
135
+ The loopback server sends permissive CORS headers, so a browser `fetch()` to
136
+ the callback from the auth site's origin works.
137
+
138
+ > The `state` value is **required** on the callback β€” the site must echo back
139
+ > the exact `state` it received. A missing or mismatched `state` is rejected
140
+ > (CSRF protection).
141
+
142
+ 3. The CLI verifies `state`, writes `GENEX_TOKEN=<TOKEN>` to `.env`, and shows a
143
+ success page in the browser.
144
+
145
+ If the browser can't be opened, the URL is printed and β€” when run in an
146
+ interactive terminal β€” a pasted token is also accepted.
147
+
148
+ > **Windows note:** the `.env` file is created `0600` (owner-only) on
149
+ > macOS/Linux. On Windows, POSIX modes don't apply; the CLI makes a best-effort
150
+ > attempt to restrict the file's ACL to the current user via `icacls`, but if
151
+ > that fails the token file may inherit the directory's permissions. Keep your
152
+ > project directory out of shared/world-readable locations.
153
+
154
+ ## Development
155
+
156
+ No build step β€” like the rest of the monorepo, the CLI runs as TypeScript via
157
+ Node's native type stripping. Run these from this package (or use the root
158
+ `pnpm typecheck` / `pnpm test:cli`):
159
+
160
+ ```bash
161
+ pnpm typecheck # tsc --noEmit (extends ../../tsconfig.base.json)
162
+ pnpm test # node --test test/*.test.ts
163
+
164
+ # Try it locally:
165
+ pnpm start init --no-auth --dir ./tmp-claude
166
+ ```
167
+
168
+ Relative imports use explicit `.ts` extensions and only erasable TypeScript
169
+ syntax is allowed β€” both are monorepo-wide rules (see the root README).
170
+
171
+ ### Project layout
172
+
173
+ ```
174
+ src/
175
+ index.ts CLI entry: arg parsing + dispatch
176
+ config.ts AUTH_URL constant, path helpers
177
+ commands/init.ts the `init` command orchestration
178
+ lib/
179
+ copy-templates.ts recursive merge-copy (never overwrites)
180
+ env.ts read/modify/write .env safely
181
+ auth.ts loopback server + browser opener
182
+ utils/ logger + colors (zero runtime deps)
183
+ templates/ copied into ~/.claude by `genex init`
184
+ ```
185
+
186
+ ## Publishing
187
+
188
+ The package ships its TypeScript source (no bundle); consumers run it on
189
+ Node β‰₯ 24. The published tarball includes only `src/`, `templates/`, `README.md`,
190
+ and `LICENSE` (see the `files` field in `package.json`).
191
+
192
+ ```bash
193
+ pnpm --filter @genex-ai/cli-demo publish --access public
194
+ ```
195
+
196
+ ## License
197
+
198
+ MIT Β© me-ai-org
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@genex-ai/cli-demo",
3
+ "version": "0.1.0",
4
+ "description": "Set up your ~/.claude workspace, authorize, create a game project, and publish (genex CLI).",
5
+ "type": "module",
6
+ "bin": {
7
+ "genex": "./src/index.ts"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "templates",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=24"
17
+ },
18
+ "keywords": [
19
+ "cli",
20
+ "claude",
21
+ "genex",
22
+ "scaffold",
23
+ "init"
24
+ ],
25
+ "author": "me-ai-org",
26
+ "license": "MIT",
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/me-ai-org/genex-demo.git",
33
+ "directory": "apps/cli"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/me-ai-org/genex-demo/issues"
37
+ },
38
+ "homepage": "https://github.com/me-ai-org/genex-demo/tree/main/apps/cli#readme",
39
+ "scripts": {
40
+ "start": "node src/index.ts",
41
+ "dev": "node --watch src/index.ts",
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "node --test test/*.test.ts"
44
+ }
45
+ }
@@ -0,0 +1,131 @@
1
+ import path from "node:path";
2
+ import {
3
+ ENV_TOKEN_KEY,
4
+ getApiUrl,
5
+ getAuthUrl,
6
+ getClaudeDir,
7
+ getColyseusUrl,
8
+ getTemplatesDir,
9
+ } from "../config.ts";
10
+ import { copyTemplates } from "../lib/copy-templates.ts";
11
+ import { authorize } from "../lib/auth.ts";
12
+ import { createDraftProject } from "../lib/project.ts";
13
+ import { generateSshKeypair, writeGitignore } from "../lib/ssh.ts";
14
+ import { writeProject, writeUserToken } from "../lib/store.ts";
15
+ import { createLogger } from "../utils/logger.ts";
16
+ import { c } from "../utils/colors.ts";
17
+
18
+ export interface InitOptions {
19
+ /** Destination workspace. Defaults to `~/.claude`. */
20
+ dir?: string;
21
+ /** Override the auth site (web) URL. */
22
+ authUrl?: string;
23
+ /** Override the API base URL (where the project is created). */
24
+ apiUrl?: string;
25
+ /** Override the Colyseus relay URL (stored in project metadata). */
26
+ colyseusUrl?: string;
27
+ /** Project name. Defaults to the current directory's name. */
28
+ name?: string;
29
+ /** Path to the token env file. Defaults to `~/.genex/env`. */
30
+ envPath?: string;
31
+ /** Skip the authorization step; only scaffold templates. */
32
+ noAuth?: boolean;
33
+ /** Overwrite existing files in the destination (off by default). */
34
+ force?: boolean;
35
+ /** Auth redirect wait timeout, in seconds. */
36
+ timeoutSec?: number;
37
+ quiet?: boolean;
38
+ }
39
+
40
+ export async function runInit(opts: InitOptions): Promise<void> {
41
+ const log = createLogger({ quiet: opts.quiet });
42
+
43
+ log.plain(c.bold("genex init"));
44
+ log.plain("");
45
+
46
+ // 1. Scaffold the ~/.claude workspace from bundled templates.
47
+ const templatesDir = getTemplatesDir();
48
+ const claudeDir = getClaudeDir(opts.dir);
49
+
50
+ log.step(`Setting up your workspace at ${c.cyan(claudeDir)}`);
51
+ const { copied, skipped } = await copyTemplates(templatesDir, claudeDir, {
52
+ force: opts.force,
53
+ });
54
+
55
+ if (copied.length > 0) {
56
+ log.success(`Added ${copied.length} file${copied.length === 1 ? "" : "s"}.`);
57
+ for (const f of copied) log.dim(` + ${f}`);
58
+ }
59
+ if (skipped.length > 0) {
60
+ log.info(
61
+ `Left ${skipped.length} existing file${skipped.length === 1 ? "" : "s"} untouched.`,
62
+ );
63
+ for (const f of skipped) log.dim(` = ${f}`);
64
+ }
65
+ if (copied.length === 0 && skipped.length === 0) {
66
+ log.info("No template files to install.");
67
+ }
68
+ log.plain("");
69
+
70
+ // 2. Authorize (unless skipped).
71
+ if (opts.noAuth) {
72
+ log.info("Skipping authorization (--no-auth).");
73
+ log.success("Done.");
74
+ return;
75
+ }
76
+
77
+ const authBaseUrl = getAuthUrl(opts.authUrl);
78
+ let token: string;
79
+ try {
80
+ token = await authorize(authBaseUrl, {
81
+ log,
82
+ timeoutMs: opts.timeoutSec ? opts.timeoutSec * 1000 : undefined,
83
+ });
84
+ } catch (err) {
85
+ log.error(err instanceof Error ? err.message : String(err));
86
+ log.dim("Your workspace files were installed. Re-run `genex init` to finish authorizing.");
87
+ throw err;
88
+ }
89
+
90
+ log.success("Authorized.");
91
+
92
+ // 3. Persist the token to ~/.genex/env (per-user; reused across projects).
93
+ const { path: tokenPath } = await writeUserToken(token, opts.envPath);
94
+ log.success(`Saved your token to ${c.cyan(tokenPath)} (${ENV_TOKEN_KEY}).`);
95
+ log.plain("");
96
+
97
+ // 4. Generate a per-project deploy key, then create the draft project so it
98
+ // shows up in the dashboard's "My games" immediately. The private key stays
99
+ // local (gitignored); only its public half is registered with the repo.
100
+ const key = await generateSshKeypair(process.cwd(), log);
101
+ await writeGitignore(process.cwd(), log);
102
+ if (!key) {
103
+ log.warn(
104
+ "Skipping project creation β€” no deploy key. Install ssh-keygen (OpenSSH) and re-run `genex init`.",
105
+ );
106
+ log.plain("");
107
+ log.success("Workspace ready (no project created yet).");
108
+ return;
109
+ }
110
+
111
+ const apiUrl = getApiUrl(opts.apiUrl);
112
+ const colyseusUrl = getColyseusUrl(opts.colyseusUrl);
113
+ const projectName = opts.name?.trim() || path.basename(process.cwd());
114
+
115
+ const meta = await createDraftProject({
116
+ apiUrl,
117
+ token,
118
+ name: projectName,
119
+ deployKey: key.publicKey,
120
+ colyseusUrl,
121
+ dashboardUrl: authBaseUrl,
122
+ log,
123
+ });
124
+ if (meta) {
125
+ const { path: metaPath } = await writeProject(meta);
126
+ log.dim(` saved ${c.cyan(metaPath)}`);
127
+ }
128
+
129
+ log.plain("");
130
+ log.success("All set. πŸš€");
131
+ }
@@ -0,0 +1,151 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { getApiUrl } from "../config.ts";
5
+ import { readProject, readUserToken } from "../lib/store.ts";
6
+ import { KEY_NAME } from "../lib/ssh.ts";
7
+ import { createLogger, type Logger } from "../utils/logger.ts";
8
+ import { c } from "../utils/colors.ts";
9
+
10
+ export interface PublishOptions {
11
+ /** Override the API base URL (defaults to the project's stored apiUrl). */
12
+ apiUrl?: string;
13
+ /** Override the bearer token (defaults to ~/.genex/env). */
14
+ token?: string;
15
+ /** Path to the token env file (defaults to ~/.genex/env). */
16
+ envPath?: string;
17
+ /** Skip the git push; only flip the gallery flag. */
18
+ noPush?: boolean;
19
+ /** Optional gallery metadata. */
20
+ title?: string;
21
+ description?: string;
22
+ quiet?: boolean;
23
+ }
24
+
25
+ interface RunResult {
26
+ code: number;
27
+ out: string;
28
+ err: string;
29
+ }
30
+
31
+ /** Run a command, capturing output. Resolves with the exit code (never throws). */
32
+ function run(cmd: string, args: string[], env?: NodeJS.ProcessEnv): Promise<RunResult> {
33
+ return new Promise((resolve) => {
34
+ let child;
35
+ try {
36
+ child = spawn(cmd, args, { env: env ? { ...process.env, ...env } : process.env });
37
+ } catch {
38
+ resolve({ code: -1, out: "", err: `${cmd} not found` });
39
+ return;
40
+ }
41
+ let out = "";
42
+ let err = "";
43
+ child.stdout?.on("data", (d) => (out += String(d)));
44
+ child.stderr?.on("data", (d) => (err += String(d)));
45
+ child.on("error", () => resolve({ code: -1, out, err: `${cmd} not found` }));
46
+ child.on("close", (code) => resolve({ code: code ?? -1, out, err }));
47
+ });
48
+ }
49
+
50
+ /**
51
+ * `genex publish` β€” push the built game to GitHub over the project's deploy key
52
+ * (best-effort), then flip the project from draft to published in the gallery.
53
+ *
54
+ * push (updates the live Pages site) and publish (the gallery flag) are
55
+ * independent per CLI_INTEGRATION.md, so a failed/absent push (e.g. locally with
56
+ * the mock provider β€” no real repo) never blocks the gallery flip.
57
+ */
58
+ export async function runPublish(opts: PublishOptions): Promise<void> {
59
+ const log = createLogger({ quiet: opts.quiet });
60
+ log.plain(c.bold("genex publish"));
61
+ log.plain("");
62
+
63
+ const token = opts.token ?? (await readUserToken(opts.envPath));
64
+ if (!token) {
65
+ log.error("Not authorized. Run `genex init` first to sign in.");
66
+ process.exitCode = 1;
67
+ return;
68
+ }
69
+
70
+ const meta = await readProject();
71
+ if (!meta) {
72
+ log.error("No genex project here. Run `genex init` in this directory first.");
73
+ process.exitCode = 1;
74
+ return;
75
+ }
76
+
77
+ const apiUrl = getApiUrl(opts.apiUrl ?? meta.apiUrl);
78
+
79
+ // 1. Push the built game (best-effort).
80
+ if (opts.noPush) {
81
+ log.info("Skipping git push (--no-push).");
82
+ } else {
83
+ await pushGame(meta.sshUrl, log);
84
+ }
85
+
86
+ // 2. Flip the gallery flag (authoritative).
87
+ log.step("Publishing to the gallery…");
88
+ let res: Response;
89
+ try {
90
+ const body: Record<string, string> = {};
91
+ if (opts.title) body.title = opts.title;
92
+ if (opts.description) body.description = opts.description;
93
+ res = await fetch(`${apiUrl}/api/projects/${meta.id}/publish`, {
94
+ method: "POST",
95
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
96
+ body: JSON.stringify(body),
97
+ });
98
+ } catch (err) {
99
+ log.error(`Couldn't reach the API at ${apiUrl}: ${String(err)}`);
100
+ process.exitCode = 1;
101
+ return;
102
+ }
103
+ if (!res.ok) {
104
+ log.error(`Publish failed (HTTP ${res.status}).`);
105
+ process.exitCode = 1;
106
+ return;
107
+ }
108
+
109
+ log.plain("");
110
+ log.success("Published. πŸŽ‰");
111
+ if (meta.playUrl) {
112
+ log.dim(` play: ${meta.playUrl}`);
113
+ log.dim(" (GitHub Pages rebuilds ~30–90s after a push.)");
114
+ }
115
+ }
116
+
117
+ async function pushGame(sshUrl: string, log: Logger): Promise<void> {
118
+ // GitHub Pages serves the repo root β€” the game's entry must be index.html there.
119
+ try {
120
+ await fs.access(path.join(process.cwd(), "index.html"));
121
+ } catch {
122
+ log.warn("No index.html at the project root β€” GitHub Pages needs one to serve the game.");
123
+ }
124
+
125
+ const isRepo = (await run("git", ["rev-parse", "--git-dir"])).code === 0;
126
+ if (!isRepo) {
127
+ log.step("Initializing git…");
128
+ if ((await run("git", ["init"])).code !== 0) {
129
+ log.warn("git init failed β€” skipping push (use `genex publish --no-push` to silence).");
130
+ return;
131
+ }
132
+ }
133
+
134
+ await run("git", ["add", "-A"]);
135
+ const commit = await run("git", ["commit", "-m", "build"]);
136
+ if (commit.code !== 0 && !/nothing to commit/i.test(commit.out + commit.err)) {
137
+ log.dim(" (no new commit to push)");
138
+ }
139
+
140
+ log.step("Pushing your game over SSH…");
141
+ const push = await run("git", ["push", sshUrl, "HEAD:main"], {
142
+ GIT_SSH_COMMAND: `ssh -i ./${KEY_NAME} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new`,
143
+ });
144
+ if (push.code === 0) {
145
+ log.success("Pushed to main.");
146
+ } else {
147
+ log.warn("git push didn't succeed (expected locally β€” the mock provider has no real repo).");
148
+ const tail = push.err.trim().split("\n").slice(-1)[0];
149
+ if (tail) log.dim(` ${tail}`);
150
+ }
151
+ }
package/src/config.ts ADDED
@@ -0,0 +1,102 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ /**
6
+ * The Genex authorization SITE β€” the web app that renders `/cli/auth` + `/login`
7
+ * (NOT the API). The CLI opens `<authUrl>/cli/auth` in the browser. Override with
8
+ * `GENEX_AUTH_URL` or `--auth-url` (locally: http://localhost:5173).
9
+ */
10
+ export const DEFAULT_AUTH_URL = "https://demo-web.glotech.world";
11
+
12
+ /**
13
+ * The Genex API base URL (where projects are created + published). Distinct from
14
+ * the auth site. Override with `GENEX_API_URL` or `--api-url` (locally:
15
+ * http://localhost:3000).
16
+ */
17
+ export const DEFAULT_API_URL = "https://demo-api.glotech.world";
18
+
19
+ /**
20
+ * The Colyseus multiplayer relay URL. Stored in the project metadata + printed so
21
+ * the agent's game can `connect({ url, room: slug })`. Override with
22
+ * `GENEX_COLYSEUS_URL` or `--colyseus-url` (locally: ws://localhost:2567).
23
+ */
24
+ export const DEFAULT_COLYSEUS_URL = "wss://demo-colyseus.glotech.world";
25
+
26
+ /** Environment variable name written to `~/.genex/env` after authorizing. */
27
+ export const ENV_TOKEN_KEY = "GENEX_TOKEN";
28
+
29
+ /** Environment variable that overrides {@link DEFAULT_AUTH_URL}. */
30
+ export const AUTH_URL_ENV = "GENEX_AUTH_URL";
31
+
32
+ /** Environment variable that overrides {@link DEFAULT_API_URL}. */
33
+ export const API_URL_ENV = "GENEX_API_URL";
34
+
35
+ /** Environment variable that overrides {@link DEFAULT_COLYSEUS_URL}. */
36
+ export const COLYSEUS_URL_ENV = "GENEX_COLYSEUS_URL";
37
+
38
+ /**
39
+ * Resolve the auth site URL, honoring (in order): an explicit override
40
+ * (the `--auth-url` flag), the `GENEX_AUTH_URL` env var, then the default.
41
+ * Any trailing slashes are trimmed so we can safely append paths.
42
+ */
43
+ export function getAuthUrl(override?: string): string {
44
+ const raw = override || process.env[AUTH_URL_ENV] || DEFAULT_AUTH_URL;
45
+ return raw.replace(/\/+$/, "");
46
+ }
47
+
48
+ /**
49
+ * Resolve the API base URL, honoring (in order): an explicit override (the
50
+ * `--api-url` flag), the `GENEX_API_URL` env var, then the default. Trailing
51
+ * slashes are trimmed so we can safely append paths.
52
+ */
53
+ export function getApiUrl(override?: string): string {
54
+ const raw = override || process.env[API_URL_ENV] || DEFAULT_API_URL;
55
+ return raw.replace(/\/+$/, "");
56
+ }
57
+
58
+ /**
59
+ * Resolve the Colyseus relay URL, honoring (in order): an explicit override (the
60
+ * `--colyseus-url` flag), the `GENEX_COLYSEUS_URL` env var, then the default.
61
+ */
62
+ export function getColyseusUrl(override?: string): string {
63
+ const raw = override || process.env[COLYSEUS_URL_ENV] || DEFAULT_COLYSEUS_URL;
64
+ return raw.replace(/\/+$/, "");
65
+ }
66
+
67
+ /** The per-user Genex config dir (`~/.genex`) holding the auth token. */
68
+ export function getGenexDir(): string {
69
+ return path.join(os.homedir(), ".genex");
70
+ }
71
+
72
+ /**
73
+ * Default location of the per-user env file the token is written to
74
+ * (`~/.genex/env`), or an explicit path when `--env` is supplied.
75
+ */
76
+ export function getGenexEnvPath(override?: string): string {
77
+ if (override) return path.resolve(override);
78
+ return path.join(getGenexDir(), "env");
79
+ }
80
+
81
+ /** Absolute path to the bundled `templates/` directory shipped with the package. */
82
+ export function getTemplatesDir(): string {
83
+ // After bundling, this module lives at `dist/index.js`; templates sit at the
84
+ // package root, one level up.
85
+ const here = path.dirname(fileURLToPath(import.meta.url));
86
+ return path.resolve(here, "..", "templates");
87
+ }
88
+
89
+ /**
90
+ * The destination workspace β€” the user's global `~/.claude` directory by
91
+ * default, or an explicit path when `--dir` is supplied.
92
+ */
93
+ export function getClaudeDir(override?: string): string {
94
+ if (override) return path.resolve(override);
95
+ return path.join(os.homedir(), ".claude");
96
+ }
97
+
98
+ /** Default location of the `.env` file we write the token into. */
99
+ export function getEnvPath(override?: string): string {
100
+ if (override) return path.resolve(override);
101
+ return path.join(process.cwd(), ".env");
102
+ }