@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.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/package.json +45 -0
- package/src/commands/init.ts +131 -0
- package/src/commands/publish.ts +151 -0
- package/src/config.ts +102 -0
- package/src/index.ts +238 -0
- package/src/lib/auth.ts +365 -0
- package/src/lib/copy-templates.ts +81 -0
- package/src/lib/env.ts +109 -0
- package/src/lib/project.ts +109 -0
- package/src/lib/ssh.ts +104 -0
- package/src/lib/store.ts +102 -0
- package/src/utils/colors.ts +25 -0
- package/src/utils/logger.ts +40 -0
- package/templates/README.md +19 -0
- package/templates/agents/genex-helper.md +16 -0
- package/templates/commands/genex-status.md +13 -0
- package/templates/skills/genex-getting-started/SKILL.md +41 -0
- package/templates/skills/genex-threejs-atmosphere-aerial-perspective/SKILL.md +30 -0
- package/templates/skills/genex-threejs-atmosphere-aerial-perspective/references/atmosphere.md +29 -0
- package/templates/skills/genex-threejs-bloom/SKILL.md +30 -0
- package/templates/skills/genex-threejs-bloom/references/bloom.md +29 -0
- package/templates/skills/genex-threejs-camera-direction/SKILL.md +36 -0
- package/templates/skills/genex-threejs-camera-direction/references/camera-rigs.md +38 -0
- package/templates/skills/genex-threejs-exposure-color-grading/SKILL.md +30 -0
- package/templates/skills/genex-threejs-exposure-color-grading/references/exposure-grading.md +30 -0
- package/templates/skills/genex-threejs-image-pipeline/SKILL.md +30 -0
- package/templates/skills/genex-threejs-image-pipeline/references/image-pipeline.md +39 -0
- package/templates/skills/genex-threejs-procedural-animation/SKILL.md +31 -0
- package/templates/skills/genex-threejs-procedural-animation/references/procedural-motion.md +33 -0
- package/templates/skills/genex-threejs-procedural-architecture/SKILL.md +30 -0
- package/templates/skills/genex-threejs-procedural-architecture/references/architecture-systems.md +31 -0
- package/templates/skills/genex-threejs-procedural-fields/SKILL.md +31 -0
- package/templates/skills/genex-threejs-procedural-fields/references/field-systems.md +35 -0
- package/templates/skills/genex-threejs-procedural-geometry/SKILL.md +30 -0
- package/templates/skills/genex-threejs-procedural-geometry/references/mesh-systems.md +36 -0
- package/templates/skills/genex-threejs-procedural-materials/SKILL.md +30 -0
- package/templates/skills/genex-threejs-procedural-materials/references/material-systems.md +31 -0
- package/templates/skills/genex-threejs-procedural-planets/SKILL.md +30 -0
- package/templates/skills/genex-threejs-procedural-planets/references/planet-systems.md +30 -0
- package/templates/skills/genex-threejs-procedural-vegetation/SKILL.md +32 -0
- package/templates/skills/genex-threejs-procedural-vegetation/references/vegetation-systems.md +37 -0
- package/templates/skills/genex-threejs-procedural-vfx/SKILL.md +31 -0
- package/templates/skills/genex-threejs-procedural-vfx/references/vfx-systems.md +30 -0
- package/templates/skills/genex-threejs-raymarched-space-effects/SKILL.md +30 -0
- package/templates/skills/genex-threejs-raymarched-space-effects/references/space-effects.md +30 -0
- package/templates/skills/genex-threejs-screen-space-ambient-occlusion/SKILL.md +29 -0
- package/templates/skills/genex-threejs-screen-space-ambient-occlusion/references/ambient-occlusion.md +29 -0
- package/templates/skills/genex-threejs-shadow-systems/SKILL.md +30 -0
- package/templates/skills/genex-threejs-shadow-systems/references/shadow-systems.md +30 -0
- package/templates/skills/genex-threejs-skill-router/SKILL.md +48 -0
- package/templates/skills/genex-threejs-skill-router/references/routing-map.md +53 -0
- package/templates/skills/genex-threejs-spectral-ocean/SKILL.md +30 -0
- package/templates/skills/genex-threejs-spectral-ocean/references/spectral-ocean.md +31 -0
- package/templates/skills/genex-threejs-temporal-surfaces/SKILL.md +30 -0
- package/templates/skills/genex-threejs-temporal-surfaces/references/temporal-surfaces.md +29 -0
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +32 -0
- package/templates/skills/genex-threejs-visual-validation/references/visual-validation.md +42 -0
- package/templates/skills/genex-threejs-volumetric-clouds/SKILL.md +29 -0
- package/templates/skills/genex-threejs-volumetric-clouds/references/volumetric-clouds.md +30 -0
- package/templates/skills/genex-threejs-water-optics/SKILL.md +30 -0
- 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
|
+
}
|