@genex-ai/cli-demo 0.3.0 → 0.4.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/README.md CHANGED
@@ -21,9 +21,13 @@ genex texture "<prompt>" # generate a texture → assets/textures/
21
21
 
22
22
  `genex init` does four things:
23
23
 
24
- 1. **Scaffolds your workspace** — copies the bundled templates (skills, agents,
25
- commands) into `~/.claude`, *merging* into whatever is already there. Existing
26
- files are **never overwritten**.
24
+ 1. **Installs the skills for your coding agents** — copies the bundled templates
25
+ into every agent it detects: Claude Code (`~/.claude`, full set incl.
26
+ agents/ + commands/), Codex (`~/.codex/skills`), and Cursor
27
+ (`~/.cursor/skills`). genex-owned skills (`genex-*`) are **always refreshed**
28
+ to the package version so a stale copy can't linger; your own files are never
29
+ overwritten. Pick agents explicitly with `--agents claude,codex,cursor`, or a
30
+ single custom dir with `--dir`.
27
31
  2. **Authorizes you** — opens the Genex auth site (web) in your browser. If the
28
32
  browser can't open, it prints the URL to open manually.
29
33
  3. **Saves your token** — writes `GENEX_TOKEN` to `~/.genex/env` (per-user;
@@ -107,7 +111,8 @@ npx @genex-ai/cli-demo@latest init
107
111
  genex init [options]
108
112
 
109
113
  Options
110
- --dir <path> Destination workspace (default: ~/.claude)
114
+ --agents <list> Agents to install for: claude,codex,cursor (default: auto-detect)
115
+ --dir <path> Single destination workspace (overrides --agents)
111
116
  --env <path> Token env file (default: ~/.genex/env)
112
117
  --auth-url <url> Override the auth site (default: https://demo-web.glotech.world)
113
118
  --no-auth Only scaffold templates; skip authorization
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
9
9
  import path6 from "path";
10
10
 
11
11
  // src/config.ts
12
+ import fs from "fs";
12
13
  import os from "os";
13
14
  import path from "path";
14
15
  import { fileURLToPath } from "url";
@@ -42,45 +43,77 @@ function getTemplatesDir() {
42
43
  const here = path.dirname(fileURLToPath(import.meta.url));
43
44
  return path.resolve(here, "..", "templates");
44
45
  }
45
- function getClaudeDir(override) {
46
- if (override) return path.resolve(override);
47
- return path.join(os.homedir(), ".claude");
46
+ var KNOWN_AGENTS = {
47
+ claude: { label: "Claude Code", dirName: ".claude", full: true },
48
+ codex: { label: "Codex", dirName: ".codex", full: false },
49
+ cursor: { label: "Cursor", dirName: ".cursor", full: false }
50
+ };
51
+ var KNOWN_AGENT_IDS = Object.keys(KNOWN_AGENTS);
52
+ function resolveAgentTargets(opts = {}) {
53
+ if (opts.dir) {
54
+ return [{ id: "custom", label: "workspace", baseDir: path.resolve(opts.dir), full: true }];
55
+ }
56
+ const home = os.homedir();
57
+ let ids;
58
+ if (opts.agents && opts.agents.length > 0) {
59
+ ids = opts.agents.filter((id) => KNOWN_AGENTS[id]);
60
+ } else {
61
+ ids = KNOWN_AGENT_IDS.filter((id) => isDir(path.join(home, KNOWN_AGENTS[id].dirName)));
62
+ if (ids.length === 0) ids = ["claude"];
63
+ }
64
+ return ids.map((id) => {
65
+ const def = KNOWN_AGENTS[id];
66
+ return { id, label: def.label, baseDir: path.join(home, def.dirName), full: def.full };
67
+ });
68
+ }
69
+ function isDir(p) {
70
+ try {
71
+ return fs.statSync(p).isDirectory();
72
+ } catch {
73
+ return false;
74
+ }
48
75
  }
49
76
 
50
77
  // src/lib/copy-templates.ts
51
- import fs from "fs/promises";
78
+ import fs2 from "fs/promises";
52
79
  import path2 from "path";
80
+ function isGenexManaged(rel) {
81
+ return rel.split(path2.sep).some((seg) => seg.startsWith("genex"));
82
+ }
53
83
  async function copyTemplates(srcDir, destDir, opts = {}) {
54
- const result = { copied: [], skipped: [] };
84
+ const result = { copied: [], updated: [], skipped: [] };
55
85
  await walk(srcDir, srcDir, destDir, opts, result);
56
86
  return result;
57
87
  }
58
88
  async function walk(rootSrc, src, dest, opts, result) {
59
- const entries = await fs.readdir(src, { withFileTypes: true });
89
+ const entries = await fs2.readdir(src, { withFileTypes: true });
60
90
  for (const entry of entries) {
61
91
  const srcPath = path2.join(src, entry.name);
62
92
  const destPath = path2.join(dest, entry.name);
63
93
  const rel = path2.relative(rootSrc, srcPath);
64
94
  if (entry.isDirectory()) {
65
- await fs.mkdir(destPath, { recursive: true });
95
+ await fs2.mkdir(destPath, { recursive: true });
66
96
  await walk(rootSrc, srcPath, destPath, opts, result);
67
97
  continue;
68
98
  }
69
99
  if (!entry.isFile()) {
70
100
  continue;
71
101
  }
72
- if (!opts.force && await exists(destPath)) {
102
+ const present = await exists(destPath);
103
+ const mayOverwrite = opts.force || isGenexManaged(rel);
104
+ if (present && !mayOverwrite) {
73
105
  result.skipped.push(rel);
74
106
  continue;
75
107
  }
76
- await fs.mkdir(path2.dirname(destPath), { recursive: true });
77
- await fs.copyFile(srcPath, destPath);
108
+ await fs2.mkdir(path2.dirname(destPath), { recursive: true });
109
+ await fs2.copyFile(srcPath, destPath);
78
110
  result.copied.push(rel);
111
+ if (present) result.updated.push(rel);
79
112
  }
80
113
  }
81
114
  async function exists(p) {
82
115
  try {
83
- await fs.access(p);
116
+ await fs2.access(p);
84
117
  return true;
85
118
  } catch {
86
119
  return false;
@@ -416,7 +449,7 @@ function randomSuffix() {
416
449
  }
417
450
 
418
451
  // src/lib/ssh.ts
419
- import fs2 from "fs/promises";
452
+ import fs3 from "fs/promises";
420
453
  import path3 from "path";
421
454
  import { spawn as spawn2 } from "child_process";
422
455
  var KEY_NAME = "genex_key";
@@ -424,7 +457,7 @@ async function generateSshKeypair(dir, log) {
424
457
  const keyPath = path3.join(dir, KEY_NAME);
425
458
  const pubPath = `${keyPath}.pub`;
426
459
  try {
427
- const existing = (await fs2.readFile(pubPath, "utf8")).trim();
460
+ const existing = (await fs3.readFile(pubPath, "utf8")).trim();
428
461
  if (existing) {
429
462
  log.dim(`Reusing existing deploy key (${KEY_NAME}).`);
430
463
  return { publicKey: existing };
@@ -435,12 +468,12 @@ async function generateSshKeypair(dir, log) {
435
468
  const ok = await runSshKeygen(keyPath, log);
436
469
  if (!ok) return null;
437
470
  try {
438
- const pub = (await fs2.readFile(pubPath, "utf8")).trim();
471
+ const pub = (await fs3.readFile(pubPath, "utf8")).trim();
439
472
  if (!pub) {
440
473
  log.warn("ssh-keygen produced no public key.");
441
474
  return null;
442
475
  }
443
- await fs2.chmod(keyPath, 384).catch(() => {
476
+ await fs3.chmod(keyPath, 384).catch(() => {
444
477
  });
445
478
  return { publicKey: pub };
446
479
  } catch (err) {
@@ -473,7 +506,7 @@ async function writeGitignore(dir, log) {
473
506
  const file = path3.join(dir, ".gitignore");
474
507
  let content = "";
475
508
  try {
476
- content = await fs2.readFile(file, "utf8");
509
+ content = await fs3.readFile(file, "utf8");
477
510
  } catch {
478
511
  }
479
512
  const present = new Set(content.split("\n").map((l) => l.trim()));
@@ -483,23 +516,23 @@ async function writeGitignore(dir, log) {
483
516
  if (next.length > 0 && !next.endsWith("\n")) next += "\n";
484
517
  if (!content.trim()) next += "# genex (deploy key + local metadata \u2014 never publish)\n";
485
518
  next += toAdd.join("\n") + "\n";
486
- await fs2.writeFile(file, next);
519
+ await fs3.writeFile(file, next);
487
520
  log.dim(`Updated .gitignore (${toAdd.join(", ")}).`);
488
521
  }
489
522
 
490
523
  // src/lib/store.ts
491
- import fs4 from "fs/promises";
524
+ import fs5 from "fs/promises";
492
525
  import path5 from "path";
493
526
 
494
527
  // src/lib/env.ts
495
- import fs3 from "fs/promises";
528
+ import fs4 from "fs/promises";
496
529
  import path4 from "path";
497
530
  import { spawn as spawn3 } from "child_process";
498
531
  async function writeEnvVar(envPath, key, value) {
499
532
  let content = "";
500
533
  let existed = false;
501
534
  try {
502
- content = await fs3.readFile(envPath, "utf8");
535
+ content = await fs4.readFile(envPath, "utf8");
503
536
  existed = true;
504
537
  } catch {
505
538
  }
@@ -519,14 +552,14 @@ async function writeEnvVar(envPath, key, value) {
519
552
  next = prefix + assignment + "\n";
520
553
  mode = existed ? "appended" : "created";
521
554
  }
522
- await fs3.mkdir(path4.dirname(envPath), { recursive: true });
523
- await fs3.writeFile(envPath, next, { mode: 384 });
555
+ await fs4.mkdir(path4.dirname(envPath), { recursive: true });
556
+ await fs4.writeFile(envPath, next, { mode: 384 });
524
557
  await restrictFilePermissions(envPath);
525
558
  return { mode, path: envPath };
526
559
  }
527
560
  async function restrictFilePermissions(filePath) {
528
561
  if (process.platform !== "win32") {
529
- await fs3.chmod(filePath, 384).catch(() => {
562
+ await fs4.chmod(filePath, 384).catch(() => {
530
563
  });
531
564
  return;
532
565
  }
@@ -575,7 +608,7 @@ async function readUserToken(envPath) {
575
608
  async function readTokenFromFile(file) {
576
609
  let content;
577
610
  try {
578
- content = await fs4.readFile(file, "utf8");
611
+ content = await fs5.readFile(file, "utf8");
579
612
  } catch {
580
613
  return null;
581
614
  }
@@ -591,7 +624,7 @@ function stripQuotes(v) {
591
624
  }
592
625
  async function readProject(cwd = process.cwd()) {
593
626
  try {
594
- const raw = await fs4.readFile(getProjectMetadataPath(cwd), "utf8");
627
+ const raw = await fs5.readFile(getProjectMetadataPath(cwd), "utf8");
595
628
  return JSON.parse(raw);
596
629
  } catch {
597
630
  return null;
@@ -599,9 +632,9 @@ async function readProject(cwd = process.cwd()) {
599
632
  }
600
633
  async function writeProject(meta, cwd = process.cwd()) {
601
634
  const file = getProjectMetadataPath(cwd);
602
- await fs4.mkdir(path5.dirname(file), { recursive: true });
603
- await fs4.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
604
- await fs4.chmod(file, 384).catch(() => {
635
+ await fs5.mkdir(path5.dirname(file), { recursive: true });
636
+ await fs5.writeFile(file, JSON.stringify(meta, null, 2) + "\n", { mode: 384 });
637
+ await fs5.chmod(file, 384).catch(() => {
605
638
  });
606
639
  return { path: file };
607
640
  }
@@ -631,24 +664,24 @@ async function runInit(opts) {
631
664
  log.plain(c.bold("genex init"));
632
665
  log.plain("");
633
666
  const templatesDir = getTemplatesDir();
634
- const claudeDir = getClaudeDir(opts.dir);
635
- log.step(`Setting up your workspace at ${c.cyan(claudeDir)}`);
636
- const { copied, skipped } = await copyTemplates(templatesDir, claudeDir, {
637
- force: opts.force
638
- });
639
- if (copied.length > 0) {
640
- log.success(`Added ${copied.length} file${copied.length === 1 ? "" : "s"}.`);
641
- for (const f of copied) log.dim(` + ${f}`);
642
- }
643
- if (skipped.length > 0) {
644
- log.info(
645
- `Left ${skipped.length} existing file${skipped.length === 1 ? "" : "s"} untouched.`
646
- );
647
- for (const f of skipped) log.dim(` = ${f}`);
648
- }
649
- if (copied.length === 0 && skipped.length === 0) {
650
- log.info("No template files to install.");
651
- }
667
+ const targets = resolveAgentTargets({ dir: opts.dir, agents: opts.agents });
668
+ log.step(
669
+ `Installing Genex skills for: ${targets.map((t) => c.cyan(t.label)).join(", ")}`
670
+ );
671
+ let totalNew = 0;
672
+ let totalUpdated = 0;
673
+ for (const t of targets) {
674
+ const src = t.full ? templatesDir : path6.join(templatesDir, "skills");
675
+ const dest = t.full ? t.baseDir : path6.join(t.baseDir, "skills");
676
+ const { copied, updated } = await copyTemplates(src, dest, { force: opts.force });
677
+ const added = copied.length - updated.length;
678
+ totalNew += added;
679
+ totalUpdated += updated.length;
680
+ log.dim(` ${t.label}: ${added} added, ${updated.length} refreshed \u2192 ${dest}`);
681
+ }
682
+ log.success(
683
+ `Skills ready (${totalNew} added, ${totalUpdated} refreshed across ${targets.length} agent${targets.length === 1 ? "" : "s"}).`
684
+ );
652
685
  log.plain("");
653
686
  if (opts.noAuth) {
654
687
  log.info("Skipping authorization (--no-auth).");
@@ -703,7 +736,7 @@ async function runInit(opts) {
703
736
 
704
737
  // src/lib/deploy.ts
705
738
  import { spawn as spawn4 } from "child_process";
706
- import fs5 from "fs/promises";
739
+ import fs6 from "fs/promises";
707
740
  import os2 from "os";
708
741
  import path7 from "path";
709
742
  function run(cmd, args, env) {
@@ -737,24 +770,24 @@ async function deployGame(sshUrl, opts, log) {
737
770
  log.success("Built.");
738
771
  }
739
772
  const distDir = path7.join(cwd, "dist");
740
- const siteDir = await isDir(distDir) ? distDir : cwd;
773
+ const siteDir = await isDir2(distDir) ? distDir : cwd;
741
774
  if (siteDir === cwd) {
742
775
  await writeGitignore(cwd, log);
743
776
  }
744
777
  const rel = path7.relative(cwd, siteDir) || ".";
745
778
  try {
746
- await fs5.access(path7.join(siteDir, "index.html"));
779
+ await fs6.access(path7.join(siteDir, "index.html"));
747
780
  } catch {
748
781
  log.warn(`No index.html in ${rel} \u2014 GitHub Pages needs one to serve the game.`);
749
782
  }
750
783
  await warnIfAbsolutePaths(siteDir, log);
751
- await fs5.writeFile(path7.join(siteDir, ".nojekyll"), "");
784
+ await fs6.writeFile(path7.join(siteDir, ".nojekyll"), "");
752
785
  const keyPath = path7.resolve(cwd, KEY_NAME);
753
786
  if (!await fileExists(keyPath)) {
754
787
  log.warn(`No deploy key (${KEY_NAME}) here \u2014 run \`genex init\` in this folder first.`);
755
788
  return false;
756
789
  }
757
- const gitDir = await fs5.mkdtemp(path7.join(os2.tmpdir(), "genex-deploy-"));
790
+ const gitDir = await fs6.mkdtemp(path7.join(os2.tmpdir(), "genex-deploy-"));
758
791
  const gitEnv = { GIT_DIR: gitDir, GIT_WORK_TREE: siteDir };
759
792
  try {
760
793
  if ((await run("git", ["init", "-q"], gitEnv)).code !== 0) {
@@ -790,28 +823,28 @@ async function deployGame(sshUrl, opts, log) {
790
823
  if (tail) log.dim(` ${tail}`);
791
824
  return false;
792
825
  } finally {
793
- await fs5.rm(gitDir, { recursive: true, force: true }).catch(() => {
826
+ await fs6.rm(gitDir, { recursive: true, force: true }).catch(() => {
794
827
  });
795
828
  }
796
829
  }
797
830
  async function hasBuildScript(cwd) {
798
831
  try {
799
- const pkg = JSON.parse(await fs5.readFile(path7.join(cwd, "package.json"), "utf8"));
832
+ const pkg = JSON.parse(await fs6.readFile(path7.join(cwd, "package.json"), "utf8"));
800
833
  return Boolean(pkg.scripts?.build);
801
834
  } catch {
802
835
  return false;
803
836
  }
804
837
  }
805
- async function isDir(p) {
838
+ async function isDir2(p) {
806
839
  try {
807
- return (await fs5.stat(p)).isDirectory();
840
+ return (await fs6.stat(p)).isDirectory();
808
841
  } catch {
809
842
  return false;
810
843
  }
811
844
  }
812
845
  async function fileExists(p) {
813
846
  try {
814
- await fs5.access(p);
847
+ await fs6.access(p);
815
848
  return true;
816
849
  } catch {
817
850
  return false;
@@ -820,7 +853,7 @@ async function fileExists(p) {
820
853
  async function warnIfAbsolutePaths(siteDir, log) {
821
854
  let html;
822
855
  try {
823
- html = await fs5.readFile(path7.join(siteDir, "index.html"), "utf8");
856
+ html = await fs6.readFile(path7.join(siteDir, "index.html"), "utf8");
824
857
  } catch {
825
858
  return;
826
859
  }
@@ -918,7 +951,7 @@ async function runPreview(opts) {
918
951
  import path9 from "path";
919
952
 
920
953
  // src/lib/assets.ts
921
- import fs6 from "fs/promises";
954
+ import fs7 from "fs/promises";
922
955
  import path8 from "path";
923
956
  function slugify(input) {
924
957
  const s = input.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/g, "");
@@ -928,8 +961,8 @@ async function downloadToFile(url, dest, headers) {
928
961
  const res = await fetch(url, { headers });
929
962
  if (!res.ok) throw new Error(`download failed (HTTP ${res.status}) for ${url}`);
930
963
  const buf = Buffer.from(await res.arrayBuffer());
931
- await fs6.mkdir(path8.dirname(dest), { recursive: true });
932
- await fs6.writeFile(dest, buf);
964
+ await fs7.mkdir(path8.dirname(dest), { recursive: true });
965
+ await fs7.writeFile(dest, buf);
933
966
  return buf.byteLength;
934
967
  }
935
968
 
@@ -1099,7 +1132,8 @@ ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
1099
1132
  ${c.bold("Options for `init`")}
1100
1133
  <name> Project name (positional; default: current directory name).
1101
1134
  --name <name> Same as the positional name.
1102
- --dir <path> Destination workspace (default: ~/.claude).
1135
+ --agents <list> Agents to install skills for (claude,codex,cursor; default: auto-detect).
1136
+ --dir <path> Single destination workspace (overrides --agents).
1103
1137
  --env <path> Token env file (default: ~/.genex/env).
1104
1138
  --auth-url <url> Override the auth site (default: ${DEFAULT_AUTH_URL}).
1105
1139
  --api-url <url> Override the API base URL (default: ${DEFAULT_API_URL}).
@@ -1151,6 +1185,7 @@ function parseArgs(argv) {
1151
1185
  "--auth-url",
1152
1186
  "--api-url",
1153
1187
  "--colyseus-url",
1188
+ "--agents",
1154
1189
  "--name",
1155
1190
  "--title",
1156
1191
  "--description",
@@ -1233,6 +1268,9 @@ function applyValueFlag(options, flag, value) {
1233
1268
  case "--colyseus-url":
1234
1269
  options.colyseusUrl = value;
1235
1270
  break;
1271
+ case "--agents":
1272
+ options.agents = value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
1273
+ break;
1236
1274
  case "--name":
1237
1275
  options.name = value;
1238
1276
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Set up your ~/.claude workspace, authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,11 +11,15 @@ architecture, and team-ready workflows.
11
11
 
12
12
  ## What got installed
13
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.
14
+ `genex init` installs the Genex skills into **every coding agent it detects** —
15
+ Claude Code (`~/.claude`), Codex (`~/.codex`), and Cursor (`~/.cursor`) — so the
16
+ same skills are available whichever agent you build with. Re-running `init`
17
+ always refreshes the genex-owned skills to the latest version (your own files
18
+ are never touched).
19
+
20
+ - **skills/** - reusable Genex skills for 3D browser-game work (all agents).
21
+ - **agents/** - example subagent definitions (Claude Code).
22
+ - **commands/** - example slash commands (Claude Code).
19
23
 
20
24
  Start with `$genex-threejs-skill-router` for broad game or graphics requests.
21
25
  It routes the agent to focused skills for cameras, procedural geometry,