@genex-ai/cli-demo 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1006 -0
- package/package.json +8 -4
- package/src/commands/init.ts +0 -131
- package/src/commands/publish.ts +0 -151
- package/src/config.ts +0 -102
- package/src/index.ts +0 -238
- package/src/lib/auth.ts +0 -365
- package/src/lib/copy-templates.ts +0 -81
- package/src/lib/env.ts +0 -109
- package/src/lib/project.ts +0 -109
- package/src/lib/ssh.ts +0 -104
- package/src/lib/store.ts +0 -102
- package/src/utils/colors.ts +0 -25
- package/src/utils/logger.ts +0 -40
package/src/lib/ssh.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
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
|
-
}
|
package/src/utils/colors.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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
|
-
};
|
package/src/utils/logger.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
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
|
-
};
|