@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/src/lib/env.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export type EnvWriteMode = "created" | "updated" | "appended";
|
|
6
|
+
|
|
7
|
+
export interface EnvWriteResult {
|
|
8
|
+
mode: EnvWriteMode;
|
|
9
|
+
path: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Set `KEY=value` in a `.env` file without disturbing the rest of it.
|
|
14
|
+
*
|
|
15
|
+
* - If the file does not exist, it is created (`created`).
|
|
16
|
+
* - If the key already exists, its line is replaced in place (`updated`).
|
|
17
|
+
* - Otherwise the assignment is appended to the end (`appended`).
|
|
18
|
+
*
|
|
19
|
+
* The file is written/owned at mode 0600 since it holds a secret. Values that
|
|
20
|
+
* contain whitespace or shell-significant characters are double-quoted.
|
|
21
|
+
*/
|
|
22
|
+
export async function writeEnvVar(
|
|
23
|
+
envPath: string,
|
|
24
|
+
key: string,
|
|
25
|
+
value: string,
|
|
26
|
+
): Promise<EnvWriteResult> {
|
|
27
|
+
let content = "";
|
|
28
|
+
let existed = false;
|
|
29
|
+
try {
|
|
30
|
+
content = await fs.readFile(envPath, "utf8");
|
|
31
|
+
existed = true;
|
|
32
|
+
} catch {
|
|
33
|
+
// Missing file — we'll create it below.
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const assignment = `${key}=${formatValue(value)}`;
|
|
37
|
+
// Match `KEY=...` (optionally preceded by `export `) on any line. The `g`
|
|
38
|
+
// flag ensures *every* occurrence is rewritten, so a file that already has
|
|
39
|
+
// duplicate entries for the key never keeps a stale value.
|
|
40
|
+
const keyPattern = new RegExp(
|
|
41
|
+
`^(\\s*export\\s+)?${escapeRegExp(key)}=.*$`,
|
|
42
|
+
"gm",
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
let next: string;
|
|
46
|
+
let mode: EnvWriteMode;
|
|
47
|
+
|
|
48
|
+
if (keyPattern.test(content)) {
|
|
49
|
+
next = content.replace(keyPattern, assignment);
|
|
50
|
+
mode = "updated";
|
|
51
|
+
} else {
|
|
52
|
+
let prefix = content;
|
|
53
|
+
if (prefix.length > 0 && !prefix.endsWith("\n")) prefix += "\n";
|
|
54
|
+
next = prefix + assignment + "\n";
|
|
55
|
+
mode = existed ? "appended" : "created";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Create the parent dir if missing (e.g. ~/.genex on first run).
|
|
59
|
+
await fs.mkdir(path.dirname(envPath), { recursive: true });
|
|
60
|
+
await fs.writeFile(envPath, next, { mode: 0o600 });
|
|
61
|
+
await restrictFilePermissions(envPath);
|
|
62
|
+
|
|
63
|
+
return { mode, path: envPath };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Restrict a secret file to the current user only.
|
|
68
|
+
*
|
|
69
|
+
* On POSIX this is a `chmod 0600`. On Windows, POSIX modes are ignored, so we
|
|
70
|
+
* make a best-effort attempt to strip inherited ACLs and grant only the current
|
|
71
|
+
* user via `icacls`. Every step is non-fatal — if it fails the token is still
|
|
72
|
+
* written (see the Windows caveat in the README).
|
|
73
|
+
*/
|
|
74
|
+
async function restrictFilePermissions(filePath: string): Promise<void> {
|
|
75
|
+
if (process.platform !== "win32") {
|
|
76
|
+
// writeFile's mode only applies on creation; enforce it for existing files too.
|
|
77
|
+
await fs.chmod(filePath, 0o600).catch(() => {});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const user = process.env.USERNAME ?? process.env.USER;
|
|
82
|
+
if (!user) return;
|
|
83
|
+
|
|
84
|
+
await new Promise<void>((resolve) => {
|
|
85
|
+
try {
|
|
86
|
+
const child = spawn(
|
|
87
|
+
"icacls",
|
|
88
|
+
[filePath, "/inheritance:r", "/grant:r", `${user}:F`],
|
|
89
|
+
{ stdio: "ignore" },
|
|
90
|
+
);
|
|
91
|
+
child.on("error", () => resolve());
|
|
92
|
+
child.on("close", () => resolve());
|
|
93
|
+
} catch {
|
|
94
|
+
resolve();
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatValue(value: string): string {
|
|
100
|
+
if (/[\s#"'$`\\]/.test(value)) {
|
|
101
|
+
// Quote and escape backslashes/double-quotes for dotenv-style parsers.
|
|
102
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function escapeRegExp(s: string): string {
|
|
108
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
109
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import type { Logger } from "../utils/logger.ts";
|
|
3
|
+
import { c } from "../utils/colors.ts";
|
|
4
|
+
import type { ProjectMetadata } from "./store.ts";
|
|
5
|
+
|
|
6
|
+
export interface CreateProjectOptions {
|
|
7
|
+
/** API base URL (e.g. https://demo-api.glotech.world or http://localhost:3000). */
|
|
8
|
+
apiUrl: string;
|
|
9
|
+
/** Bearer token (from ~/.genex/env). */
|
|
10
|
+
token: string;
|
|
11
|
+
/** Desired project name (the API slugifies it). */
|
|
12
|
+
name: string;
|
|
13
|
+
/** OpenSSH PUBLIC key registered as the repo's write deploy key (required). */
|
|
14
|
+
deployKey: string;
|
|
15
|
+
/** Colyseus relay URL, stored in metadata for the game's connect(). */
|
|
16
|
+
colyseusUrl: string;
|
|
17
|
+
/** Auth/web origin, used to print the dashboard link. */
|
|
18
|
+
dashboardUrl: string;
|
|
19
|
+
log: Logger;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ProjectResponse {
|
|
23
|
+
project?: {
|
|
24
|
+
id: string;
|
|
25
|
+
slug: string;
|
|
26
|
+
title: string;
|
|
27
|
+
playUrl?: string | null;
|
|
28
|
+
};
|
|
29
|
+
sshUrl?: string;
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a draft project via `POST /api/projects { name, deployKey }`. Returns
|
|
35
|
+
* the project metadata (incl. the `sshUrl` the agent pushes to) for the caller
|
|
36
|
+
* to persist, or null on failure. Best-effort: a network/HTTP failure is logged
|
|
37
|
+
* and returns null — it never aborts `genex init` (the token is already saved).
|
|
38
|
+
*/
|
|
39
|
+
export async function createDraftProject(
|
|
40
|
+
opts: CreateProjectOptions,
|
|
41
|
+
): Promise<ProjectMetadata | null> {
|
|
42
|
+
const { apiUrl, token, deployKey, colyseusUrl, dashboardUrl, log } = opts;
|
|
43
|
+
|
|
44
|
+
log.step("Creating your project…");
|
|
45
|
+
|
|
46
|
+
// Try the requested name, then one retry with a unique suffix on a slug clash.
|
|
47
|
+
const names = [opts.name, `${opts.name}-${randomSuffix()}`];
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < names.length; i++) {
|
|
50
|
+
const name = names[i] as string;
|
|
51
|
+
let res: Response;
|
|
52
|
+
try {
|
|
53
|
+
res = await fetch(`${apiUrl}/api/projects`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: {
|
|
56
|
+
"Content-Type": "application/json",
|
|
57
|
+
Authorization: `Bearer ${token}`,
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify({ name, deployKey }),
|
|
60
|
+
});
|
|
61
|
+
} catch (err) {
|
|
62
|
+
log.warn(`Couldn't reach the API at ${apiUrl} to create the project.`);
|
|
63
|
+
log.dim(` ${String(err)}`);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (res.status === 409 && i === 0) continue; // slug taken — retry suffixed
|
|
68
|
+
if (res.status === 401) {
|
|
69
|
+
log.warn("Not authorized to create the project (token rejected).");
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
if (res.status === 400) {
|
|
73
|
+
log.warn("The API rejected the deploy key (must be an OpenSSH public key).");
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
log.warn(`Couldn't create the project (HTTP ${res.status}).`);
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const data = (await res.json().catch(() => null)) as ProjectResponse | null;
|
|
82
|
+
const project = data?.project;
|
|
83
|
+
if (!project || !data?.sshUrl) {
|
|
84
|
+
log.warn("Project created, but the API response was unexpected.");
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
log.success(`Created project ${c.cyan(project.slug)}.`);
|
|
89
|
+
if (project.playUrl) log.dim(` play (after publish): ${project.playUrl}`);
|
|
90
|
+
log.dim(` dashboard: ${dashboardUrl}/dashboard`);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
id: project.id,
|
|
94
|
+
slug: project.slug,
|
|
95
|
+
sshUrl: data.sshUrl,
|
|
96
|
+
apiUrl,
|
|
97
|
+
colyseusUrl,
|
|
98
|
+
playUrl: project.playUrl ?? undefined,
|
|
99
|
+
status: "draft",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
log.warn("Couldn't create a project with a unique name. Try `--name <unique>`.");
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function randomSuffix(): string {
|
|
108
|
+
return crypto.randomBytes(3).toString("hex");
|
|
109
|
+
}
|
package/src/lib/ssh.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Per-project SSH deploy key for `genex init` / `genex publish`.
|
|
2
|
+
//
|
|
3
|
+
// We generate an ed25519 keypair in the project dir; the PUBLIC key is sent to
|
|
4
|
+
// the API as the repo's write deploy key, the PRIVATE key (`genex_key`) stays
|
|
5
|
+
// local and is used by `genex publish` to push over SSH. The private key is
|
|
6
|
+
// gitignored so it never lands in the public game repo.
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import type { Logger } from "../utils/logger.ts";
|
|
11
|
+
|
|
12
|
+
/** The private key filename in the project dir (public key is `<name>.pub`). */
|
|
13
|
+
export const KEY_NAME = "genex_key";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Ensure a per-project ed25519 deploy keypair exists in `dir` and return its
|
|
17
|
+
* public key. Idempotent: a re-run of `genex init` reuses the existing key (it's
|
|
18
|
+
* already registered as the repo's deploy key). Returns null if `ssh-keygen`
|
|
19
|
+
* isn't available — the caller warns and skips project creation.
|
|
20
|
+
*/
|
|
21
|
+
export async function generateSshKeypair(
|
|
22
|
+
dir: string,
|
|
23
|
+
log: Logger,
|
|
24
|
+
): Promise<{ publicKey: string } | null> {
|
|
25
|
+
const keyPath = path.join(dir, KEY_NAME);
|
|
26
|
+
const pubPath = `${keyPath}.pub`;
|
|
27
|
+
|
|
28
|
+
// Reuse an existing key (idempotent re-init).
|
|
29
|
+
try {
|
|
30
|
+
const existing = (await fs.readFile(pubPath, "utf8")).trim();
|
|
31
|
+
if (existing) {
|
|
32
|
+
log.dim(`Reusing existing deploy key (${KEY_NAME}).`);
|
|
33
|
+
return { publicKey: existing };
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
/* no existing key — generate below */
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
log.step("Generating a deploy key…");
|
|
40
|
+
const ok = await runSshKeygen(keyPath, log);
|
|
41
|
+
if (!ok) return null;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const pub = (await fs.readFile(pubPath, "utf8")).trim();
|
|
45
|
+
if (!pub) {
|
|
46
|
+
log.warn("ssh-keygen produced no public key.");
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
await fs.chmod(keyPath, 0o600).catch(() => {});
|
|
50
|
+
return { publicKey: pub };
|
|
51
|
+
} catch (err) {
|
|
52
|
+
log.warn(`Couldn't read the generated public key: ${String(err)}`);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function runSshKeygen(keyPath: string, log: Logger): Promise<boolean> {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
let child;
|
|
60
|
+
try {
|
|
61
|
+
child = spawn(
|
|
62
|
+
"ssh-keygen",
|
|
63
|
+
["-t", "ed25519", "-f", keyPath, "-N", "", "-C", "genex-agent"],
|
|
64
|
+
{ stdio: "ignore" },
|
|
65
|
+
);
|
|
66
|
+
} catch {
|
|
67
|
+
log.warn("ssh-keygen not found — install OpenSSH (ssh-keygen) and re-run.");
|
|
68
|
+
resolve(false);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
child.on("error", () => {
|
|
72
|
+
log.warn("ssh-keygen not found — install OpenSSH (ssh-keygen) and re-run.");
|
|
73
|
+
resolve(false);
|
|
74
|
+
});
|
|
75
|
+
child.on("close", (code) => resolve(code === 0));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Append the genex artifacts to `<dir>/.gitignore` so `genex publish`'s
|
|
81
|
+
* `git add -A` never pushes the PRIVATE key (or local metadata) to the public
|
|
82
|
+
* game repo. Idempotent; creates the file if absent; never rewrites existing rules.
|
|
83
|
+
*/
|
|
84
|
+
export async function writeGitignore(dir: string, log: Logger): Promise<void> {
|
|
85
|
+
const file = path.join(dir, ".gitignore");
|
|
86
|
+
let content = "";
|
|
87
|
+
try {
|
|
88
|
+
content = await fs.readFile(file, "utf8");
|
|
89
|
+
} catch {
|
|
90
|
+
/* create below */
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const present = new Set(content.split("\n").map((l) => l.trim()));
|
|
94
|
+
const toAdd = [KEY_NAME, `${KEY_NAME}.pub`, ".genex/"].filter((e) => !present.has(e));
|
|
95
|
+
if (toAdd.length === 0) return;
|
|
96
|
+
|
|
97
|
+
let next = content;
|
|
98
|
+
if (next.length > 0 && !next.endsWith("\n")) next += "\n";
|
|
99
|
+
if (!content.trim()) next += "# genex (deploy key + local metadata — never publish)\n";
|
|
100
|
+
next += toAdd.join("\n") + "\n";
|
|
101
|
+
|
|
102
|
+
await fs.writeFile(file, next);
|
|
103
|
+
log.dim(`Updated .gitignore (${toAdd.join(", ")}).`);
|
|
104
|
+
}
|
package/src/lib/store.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Single source of truth for CLI credential + project storage.
|
|
2
|
+
//
|
|
3
|
+
// - The USER token lives at ~/.genex/env (per-user, reused across projects),
|
|
4
|
+
// written via the existing writeEnvVar (0600 + Windows ACL). A legacy
|
|
5
|
+
// ./.env GENEX_TOKEN is still read as a fallback for older projects.
|
|
6
|
+
// - PER-PROJECT metadata (id, slug, sshUrl, urls) lives at <cwd>/.genex/project.json
|
|
7
|
+
// so `genex publish` can run from the project dir without re-authing. The
|
|
8
|
+
// private SSH key (genex_key) is NEVER stored here — it stays a plain file in
|
|
9
|
+
// the project dir and is gitignored.
|
|
10
|
+
import fs from "node:fs/promises";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { ENV_TOKEN_KEY, getGenexEnvPath } from "../config.ts";
|
|
13
|
+
import { writeEnvVar } from "./env.ts";
|
|
14
|
+
|
|
15
|
+
export interface ProjectMetadata {
|
|
16
|
+
/** API project id (used by `POST /api/projects/:id/publish`). */
|
|
17
|
+
id: string;
|
|
18
|
+
/** URL-safe slug (used as the multiplayer room + play URL path). */
|
|
19
|
+
slug: string;
|
|
20
|
+
/** git@github.com:org/slug.git — what the agent pushes to (deploy key). */
|
|
21
|
+
sshUrl: string;
|
|
22
|
+
/** API base this project was created against. */
|
|
23
|
+
apiUrl: string;
|
|
24
|
+
/** Colyseus relay URL for the game's connect(). */
|
|
25
|
+
colyseusUrl: string;
|
|
26
|
+
/** Public play URL (GitHub Pages). */
|
|
27
|
+
playUrl?: string;
|
|
28
|
+
/** "draft" | "published". */
|
|
29
|
+
status?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Path to the per-project metadata file (`<cwd>/.genex/project.json`). */
|
|
33
|
+
export function getProjectMetadataPath(cwd: string = process.cwd()): string {
|
|
34
|
+
return path.join(cwd, ".genex", "project.json");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Persist the user token to `~/.genex/env` (or an explicit env path). */
|
|
38
|
+
export async function writeUserToken(
|
|
39
|
+
token: string,
|
|
40
|
+
envPath?: string,
|
|
41
|
+
): Promise<{ path: string }> {
|
|
42
|
+
const { path: written } = await writeEnvVar(getGenexEnvPath(envPath), ENV_TOKEN_KEY, token);
|
|
43
|
+
return { path: written };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Read the user token from `~/.genex/env`, falling back to a legacy `./.env`
|
|
48
|
+
* (read-only; never deleted). Returns null if no token is found.
|
|
49
|
+
*/
|
|
50
|
+
export async function readUserToken(envPath?: string): Promise<string | null> {
|
|
51
|
+
const fromGenex = await readTokenFromFile(getGenexEnvPath(envPath));
|
|
52
|
+
if (fromGenex) return fromGenex;
|
|
53
|
+
if (!envPath) {
|
|
54
|
+
// Backcompat: older `genex init` wrote GENEX_TOKEN into the project's ./.env.
|
|
55
|
+
return readTokenFromFile(path.join(process.cwd(), ".env"));
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readTokenFromFile(file: string): Promise<string | null> {
|
|
61
|
+
let content: string;
|
|
62
|
+
try {
|
|
63
|
+
content = await fs.readFile(file, "utf8");
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const m = content.match(/^\s*(?:export\s+)?GENEX_TOKEN=(.*)$/m);
|
|
68
|
+
if (!m) return null;
|
|
69
|
+
return stripQuotes(m[1]!.trim()) || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function stripQuotes(v: string): string {
|
|
73
|
+
if (
|
|
74
|
+
(v.startsWith('"') && v.endsWith('"')) ||
|
|
75
|
+
(v.startsWith("'") && v.endsWith("'"))
|
|
76
|
+
) {
|
|
77
|
+
return v.slice(1, -1);
|
|
78
|
+
}
|
|
79
|
+
return v;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Read `<cwd>/.genex/project.json`, or null if absent/unparseable. */
|
|
83
|
+
export async function readProject(cwd: string = process.cwd()): Promise<ProjectMetadata | null> {
|
|
84
|
+
try {
|
|
85
|
+
const raw = await fs.readFile(getProjectMetadataPath(cwd), "utf8");
|
|
86
|
+
return JSON.parse(raw) as ProjectMetadata;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Write `<cwd>/.genex/project.json` (0600 — it identifies the repo, not a secret). */
|
|
93
|
+
export async function writeProject(
|
|
94
|
+
meta: ProjectMetadata,
|
|
95
|
+
cwd: string = process.cwd(),
|
|
96
|
+
): Promise<{ path: string }> {
|
|
97
|
+
const file = getProjectMetadataPath(cwd);
|
|
98
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
99
|
+
await fs.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 0o600 });
|
|
100
|
+
await fs.chmod(file, 0o600).catch(() => {});
|
|
101
|
+
return { path: file };
|
|
102
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Tiny zero-dependency ANSI color helpers. Colors are disabled when stdout is
|
|
2
|
+
// not a TTY, when NO_COLOR is set, or for dumb terminals — so piped/CI output
|
|
3
|
+
// stays clean.
|
|
4
|
+
const useColor =
|
|
5
|
+
Boolean(process.stdout.isTTY) &&
|
|
6
|
+
process.env.NO_COLOR === undefined &&
|
|
7
|
+
process.env.TERM !== "dumb";
|
|
8
|
+
|
|
9
|
+
const ESC = String.fromCharCode(27); // ASCII escape (\x1b)
|
|
10
|
+
|
|
11
|
+
const code =
|
|
12
|
+
(open: number, close: number) =>
|
|
13
|
+
(s: string): string =>
|
|
14
|
+
useColor ? `${ESC}[${open}m${s}${ESC}[${close}m` : s;
|
|
15
|
+
|
|
16
|
+
export const c = {
|
|
17
|
+
bold: code(1, 22),
|
|
18
|
+
dim: code(2, 22),
|
|
19
|
+
red: code(31, 39),
|
|
20
|
+
green: code(32, 39),
|
|
21
|
+
yellow: code(33, 39),
|
|
22
|
+
blue: code(34, 39),
|
|
23
|
+
cyan: code(36, 39),
|
|
24
|
+
gray: code(90, 39),
|
|
25
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { c } from "./colors.ts";
|
|
2
|
+
|
|
3
|
+
export interface Logger {
|
|
4
|
+
info(msg: string): void;
|
|
5
|
+
success(msg: string): void;
|
|
6
|
+
warn(msg: string): void;
|
|
7
|
+
error(msg: string): void;
|
|
8
|
+
step(msg: string): void;
|
|
9
|
+
dim(msg: string): void;
|
|
10
|
+
plain(msg: string): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createLogger(opts: { quiet?: boolean } = {}): Logger {
|
|
14
|
+
const out = (s: string): void => {
|
|
15
|
+
if (!opts.quiet) process.stdout.write(s + "\n");
|
|
16
|
+
};
|
|
17
|
+
const err = (s: string): void => {
|
|
18
|
+
process.stderr.write(s + "\n");
|
|
19
|
+
};
|
|
20
|
+
return {
|
|
21
|
+
info: (m) => out(`${c.cyan("i")} ${m}`),
|
|
22
|
+
success: (m) => out(`${c.green("✓")} ${m}`),
|
|
23
|
+
warn: (m) => out(`${c.yellow("!")} ${m}`),
|
|
24
|
+
error: (m) => err(`${c.red("✗")} ${m}`),
|
|
25
|
+
step: (m) => out(`${c.blue("›")} ${m}`),
|
|
26
|
+
dim: (m) => out(c.dim(m)),
|
|
27
|
+
plain: (m) => out(m),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A no-op logger, handy for tests.
|
|
32
|
+
export const silentLogger: Logger = {
|
|
33
|
+
info: () => {},
|
|
34
|
+
success: () => {},
|
|
35
|
+
warn: () => {},
|
|
36
|
+
error: () => {},
|
|
37
|
+
step: () => {},
|
|
38
|
+
dim: () => {},
|
|
39
|
+
plain: () => {},
|
|
40
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Genex workspace
|
|
2
|
+
|
|
3
|
+
These files were installed into your `~/.claude` directory by `genex init`.
|
|
4
|
+
They give your coding agent a starter workspace for making 3D games in the
|
|
5
|
+
browser.
|
|
6
|
+
|
|
7
|
+
Genex is built around agent superpowers for browser games: Three.js skills,
|
|
8
|
+
one-click publishing, multiplayer-ready architecture, and team workflows. The
|
|
9
|
+
installed files are yours to edit, extend, and keep with each project.
|
|
10
|
+
|
|
11
|
+
Existing files are never overwritten. Re-running `genex init` only adds files
|
|
12
|
+
you do not already have.
|
|
13
|
+
|
|
14
|
+
- `skills/` - reusable Genex skills for 3D game creation.
|
|
15
|
+
- `agents/` - example subagent definitions.
|
|
16
|
+
- `commands/` - example slash commands.
|
|
17
|
+
|
|
18
|
+
Start with `skills/genex-threejs-skill-router/SKILL.md` when asking your agent
|
|
19
|
+
to build or improve a 3D browser game.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-helper
|
|
3
|
+
description: Example Genex subagent. Answers questions about the local Genex workspace and CLI. Replace or extend this with your own agents.
|
|
4
|
+
tools: Read, Grep, Glob
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
You are the Genex helper agent — an example subagent installed by `genex init`.
|
|
8
|
+
|
|
9
|
+
Your job is to help the user understand and navigate their `~/.claude`
|
|
10
|
+
workspace: the skills, agents, and commands installed by Genex.
|
|
11
|
+
|
|
12
|
+
Guidelines:
|
|
13
|
+
- Be concise and concrete. Point at real files with paths.
|
|
14
|
+
- When unsure what's installed, search the workspace before answering.
|
|
15
|
+
- This is a starter template — encourage the user to edit or replace it with
|
|
16
|
+
agents tailored to their own work.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Show the current Genex setup — workspace location, installed skills/agents, and whether a token is configured.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Report the user's Genex status:
|
|
6
|
+
|
|
7
|
+
1. Confirm the workspace directory (`~/.claude`) exists and list the
|
|
8
|
+
`skills/`, `agents/`, and `commands/` it contains.
|
|
9
|
+
2. Check whether a `GENEX_TOKEN` is present in the project's `.env` (do not
|
|
10
|
+
print the token value — only whether it is set).
|
|
11
|
+
3. If anything looks missing, suggest running `npx genex init`.
|
|
12
|
+
|
|
13
|
+
Keep the summary short and scannable.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-getting-started
|
|
3
|
+
description: Orient a new Genex user to the installed 3D browser-game workspace, including skills, agents, commands, authorization, and re-running setup. Use when the user asks what Genex is, how to get started, or what `genex init` installed.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Getting started with Genex
|
|
7
|
+
|
|
8
|
+
`genex init` set up a workspace that helps your coding agent build 3D games in
|
|
9
|
+
the browser. Genex focuses on Three.js game skills, publishing, multiplayer
|
|
10
|
+
architecture, and team-ready workflows.
|
|
11
|
+
|
|
12
|
+
## What got installed
|
|
13
|
+
|
|
14
|
+
Everything under `~/.claude`:
|
|
15
|
+
|
|
16
|
+
- **skills/** - reusable Genex skills for 3D browser-game work.
|
|
17
|
+
- **agents/** - example subagent definitions.
|
|
18
|
+
- **commands/** - example slash commands.
|
|
19
|
+
|
|
20
|
+
Start with `$genex-threejs-skill-router` for broad game or graphics requests.
|
|
21
|
+
It routes the agent to focused skills for cameras, procedural geometry,
|
|
22
|
+
materials, atmosphere, water, VFX, post-processing, and visual validation.
|
|
23
|
+
|
|
24
|
+
Your existing files were left untouched. `genex init` only adds what is missing.
|
|
25
|
+
|
|
26
|
+
## Re-running setup
|
|
27
|
+
|
|
28
|
+
Safe to run any time; it never overwrites your files:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx genex init
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use `--force` only if you intentionally want to overwrite installed templates
|
|
35
|
+
with the latest versions.
|
|
36
|
+
|
|
37
|
+
## Authorization
|
|
38
|
+
|
|
39
|
+
`genex init` opens the auth site, then writes a `GENEX_TOKEN` into your
|
|
40
|
+
project's `.env`. If the browser doesn't open, copy the printed URL into a
|
|
41
|
+
browser manually to finish authorizing.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-threejs-atmosphere-aerial-perspective
|
|
3
|
+
description: Implement sky and aerial perspective for Genex Three.js games. Use for planetary atmospheres, ground-to-space transitions, Rayleigh/Mie-style scattering, sun and moon discs, depth-based haze, distance color, atmospheric lighting, and scale-readable outdoor scenes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex Three.js Atmosphere And Aerial Perspective
|
|
7
|
+
|
|
8
|
+
Atmosphere should explain scale and lighting without hiding gameplay. Add it
|
|
9
|
+
after the base scene reads.
|
|
10
|
+
|
|
11
|
+
Read [references/atmosphere.md](references/atmosphere.md) for sky, haze,
|
|
12
|
+
lighting handoff, and transition checks.
|
|
13
|
+
|
|
14
|
+
## Build order
|
|
15
|
+
|
|
16
|
+
1. Define world scale, camera altitude range, sun direction, horizon behavior,
|
|
17
|
+
and target devices.
|
|
18
|
+
2. Build the sky model or sky gradient first.
|
|
19
|
+
3. Add depth-based aerial perspective for terrain, buildings, and distant props.
|
|
20
|
+
4. Connect sun color, ambient color, fog, and material response coherently.
|
|
21
|
+
5. Add planetary shell or LUT paths only when the scale needs them.
|
|
22
|
+
6. Expose debug views for transmittance, inscattering, depth, and no-atmosphere.
|
|
23
|
+
|
|
24
|
+
## Rules
|
|
25
|
+
|
|
26
|
+
- Keep gameplay silhouettes readable through haze.
|
|
27
|
+
- Use one owner for sky color and sun direction.
|
|
28
|
+
- Avoid stacking multiple fog systems with different assumptions.
|
|
29
|
+
- Keep atmosphere quality adjustable for lower-end browsers.
|
|
30
|
+
- Validate ground, mid-altitude, and high-altitude views.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Atmosphere And Aerial Perspective
|
|
2
|
+
|
|
3
|
+
Use this reference for outdoor scale and sky rendering.
|
|
4
|
+
|
|
5
|
+
## Sky
|
|
6
|
+
|
|
7
|
+
- Keep sun direction explicit and shared with lighting.
|
|
8
|
+
- Match sky color, ambient color, and direct light temperature.
|
|
9
|
+
- Include sun or moon discs only when they help composition.
|
|
10
|
+
- Keep stars or space backgrounds separated from atmospheric haze.
|
|
11
|
+
|
|
12
|
+
## Aerial perspective
|
|
13
|
+
|
|
14
|
+
- Apply haze by depth or world distance.
|
|
15
|
+
- Preserve contrast for interactable objects and traversal edges.
|
|
16
|
+
- Let distant terrain shift color before it loses all form.
|
|
17
|
+
- Provide a debug toggle to remove atmosphere.
|
|
18
|
+
|
|
19
|
+
## Planetary scale
|
|
20
|
+
|
|
21
|
+
- Use altitude-aware behavior for ground-to-space transitions.
|
|
22
|
+
- Keep horizon curvature and shell radius consistent with planet radius.
|
|
23
|
+
- Validate camera positions near ground, high altitude, and orbit.
|
|
24
|
+
|
|
25
|
+
## Performance
|
|
26
|
+
|
|
27
|
+
- Prefer simple depth haze for small scenes.
|
|
28
|
+
- Use precomputed or lower-resolution paths for expensive scattering.
|
|
29
|
+
- Expose quality tiers and render-target costs.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-threejs-bloom
|
|
3
|
+
description: Implement controlled HDR bloom for Genex Three.js games. Use for selective emission, glow hierarchy, bloom thresholds, multi-scale blur, material restoration, effect isolation, exposure coupling, and diagnostics where glow supports form without replacing it.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex Three.js Bloom
|
|
7
|
+
|
|
8
|
+
Bloom should reveal strong light and energy. It should not be the only reason an
|
|
9
|
+
object reads.
|
|
10
|
+
|
|
11
|
+
Read [references/bloom.md](references/bloom.md) for HDR signal setup,
|
|
12
|
+
selective contribution, blur hierarchy, and diagnostics.
|
|
13
|
+
|
|
14
|
+
## Build order
|
|
15
|
+
|
|
16
|
+
1. Confirm the scene uses a consistent HDR signal and tone-mapping path.
|
|
17
|
+
2. Define what is allowed to bloom and why.
|
|
18
|
+
3. Extract bright or selected contribution.
|
|
19
|
+
4. Blur at multiple scales.
|
|
20
|
+
5. Composite with exposure-aware strength.
|
|
21
|
+
6. Expose debug views for source, threshold mask, blur levels, final bloom, and
|
|
22
|
+
no-bloom baseline.
|
|
23
|
+
|
|
24
|
+
## Rules
|
|
25
|
+
|
|
26
|
+
- Keep emissive intensities scene-relative.
|
|
27
|
+
- Avoid global bloom that washes out gameplay silhouettes.
|
|
28
|
+
- Restore materials if using selective render passes.
|
|
29
|
+
- Tune bloom with exposure and grading visible.
|
|
30
|
+
- Validate readability with bloom disabled.
|