@genex-ai/cli-demo 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +21 -0
  2. package/dist/index.js +125 -8
  3. package/package.json +4 -1
package/README.md CHANGED
@@ -184,6 +184,27 @@ GENEX_BROWSER="open -a Safari" genex init
184
184
  GENEX_BROWSER="'/Applications/My Browser.app/Contents/MacOS/My Browser'" genex init
185
185
  ```
186
186
 
187
+ ## Crash reporting
188
+
189
+ When a command hits an **unexpected** error, the CLI reports the crash to Sentry
190
+ so we can fix it. Expected failures (missing token, a rejected API request, bad
191
+ input) are just printed — they're not sent. Before anything leaves your machine
192
+ it's scrubbed: your `GENEX_TOKEN` and any credentials, credential-bearing URLs
193
+ (e.g. the per-push git URL), your home-directory paths and hostname, and
194
+ stack-frame locals are all stripped ([`src/lib/sentry-scrub.ts`](src/lib/sentry-scrub.ts)).
195
+ No IP address or account identity is attached. Only the crash, the command name,
196
+ and your CLI/Node/OS versions are sent.
197
+
198
+ Opt out any time:
199
+
200
+ ```bash
201
+ export GENEX_TELEMETRY=0 # our switch (on by default)
202
+ export DO_NOT_TRACK=1 # the cross-tool standard — any value disables it
203
+ ```
204
+
205
+ Reporting is also off whenever no DSN is baked into the build (the default in
206
+ local/source runs), so nothing is sent during development.
207
+
187
208
  ## How authorization works
188
209
 
189
210
  The CLI uses a loopback-redirect flow (the same pattern as `gh auth login` and
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/commands/init.ts
4
- import path8 from "path";
3
+ // src/instrument.ts
4
+ import * as Sentry from "@sentry/node";
5
5
 
6
6
  // src/config.ts
7
7
  import fs from "fs";
@@ -89,6 +89,107 @@ function isDir(p) {
89
89
  }
90
90
  }
91
91
 
92
+ // src/lib/sentry-scrub.ts
93
+ import os2 from "os";
94
+ var BEARER = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
95
+ var URL_CRED = /(https?:\/\/)[^/@\s:]+(?::[^/@\s]*)?@/gi;
96
+ var TOKENISH_KV = /((?:token|secret|password|api[_-]?key|authorization)=)[^&#\s"']+/gi;
97
+ var SENSITIVE_KEY = /token|secret|password|api[_-]?key|authorization|bearer/i;
98
+ function scrubString(s) {
99
+ return s.replace(URL_CRED, "$1[redacted]@").replace(BEARER, "Bearer [redacted]").replace(TOKENISH_KV, "$1[redacted]");
100
+ }
101
+ function redactHome(s, home = os2.homedir()) {
102
+ if (!home) return s;
103
+ return s.split(home).join("~");
104
+ }
105
+ var clean = (s) => redactHome(scrubString(s));
106
+ function scrubEvent(event) {
107
+ delete event.user;
108
+ delete event.server_name;
109
+ if (typeof event.message === "string") event.message = clean(event.message);
110
+ for (const ex of event.exception?.values ?? []) {
111
+ if (typeof ex.value === "string") ex.value = clean(ex.value);
112
+ for (const f of ex.stacktrace?.frames ?? []) {
113
+ if (typeof f.filename === "string") f.filename = redactHome(f.filename);
114
+ if (typeof f.abs_path === "string") f.abs_path = redactHome(f.abs_path);
115
+ delete f.vars;
116
+ }
117
+ }
118
+ if (event.request && typeof event.request.url === "string") {
119
+ event.request.url = clean(event.request.url);
120
+ }
121
+ for (const b of event.breadcrumbs ?? []) {
122
+ if (typeof b.message === "string") b.message = clean(b.message);
123
+ const d = b.data;
124
+ if (d) {
125
+ for (const k of Object.keys(d)) {
126
+ if (typeof d[k] === "string") d[k] = clean(d[k]);
127
+ }
128
+ }
129
+ }
130
+ for (const bag of [event.extra, event.tags]) {
131
+ if (!bag) continue;
132
+ for (const k of Object.keys(bag)) {
133
+ if (SENSITIVE_KEY.test(k)) bag[k] = "[Filtered]";
134
+ else if (typeof bag[k] === "string") bag[k] = clean(bag[k]);
135
+ }
136
+ }
137
+ return event;
138
+ }
139
+
140
+ // src/instrument.ts
141
+ var GENEX_CLI_DSN = "https://43efc6f7d16c3e67cad6c60fa8175c20@o4511115493900288.ingest.us.sentry.io/4511706579599360";
142
+ function bakedDsn() {
143
+ return import.meta.url.includes("/dist/") ? GENEX_CLI_DSN : "";
144
+ }
145
+ function resolveDsn() {
146
+ return (process.env.GENEX_SENTRY_DSN || bakedDsn()).trim();
147
+ }
148
+ function isTruthy(v) {
149
+ return v !== void 0 && v.trim() !== "" && !/^(0|false|off|no)$/i.test(v.trim());
150
+ }
151
+ function telemetryDisabled() {
152
+ if (isTruthy(process.env.DO_NOT_TRACK)) return true;
153
+ if (process.env.GENEX_TELEMETRY !== void 0 && !isTruthy(process.env.GENEX_TELEMETRY)) return true;
154
+ if (isTruthy(process.env.GENEX_DISABLE_SENTRY)) return true;
155
+ return false;
156
+ }
157
+ var dsn = resolveDsn();
158
+ var sentryEnabled = !!dsn && !telemetryDisabled();
159
+ if (sentryEnabled) {
160
+ Sentry.init({
161
+ dsn,
162
+ // release aligns with the AG-757 `x-genex-cli-version` telemetry stream so
163
+ // the two correlate; environment is prod unless pointed at a non-default API.
164
+ release: `@genex-ai/cli-demo@${getCliVersion()}`,
165
+ environment: getApiUrl() === DEFAULT_API_URL ? "production" : "development",
166
+ // A short-lived CLI has no meaningful tracing workload — errors only.
167
+ tracesSampleRate: 0,
168
+ // No IP / user auto-collection (opposite of the server apps' `userInfo: true`).
169
+ dataCollection: { userInfo: false },
170
+ // Keep @sentry/node's default onUncaughtException + onUnhandledRejection
171
+ // integrations (auto-registered) — they catch the fire-and-forget async
172
+ // paths in lib/updates.ts / lib/deploy.ts that otherwise swallow errors.
173
+ beforeSend(event) {
174
+ scrubEvent(event);
175
+ return event;
176
+ }
177
+ });
178
+ }
179
+ async function flushSentry(timeoutMs = 2e3) {
180
+ if (!sentryEnabled) return;
181
+ try {
182
+ await Sentry.flush(timeoutMs);
183
+ } catch {
184
+ }
185
+ }
186
+
187
+ // src/index.ts
188
+ import * as Sentry2 from "@sentry/node";
189
+
190
+ // src/commands/init.ts
191
+ import path8 from "path";
192
+
92
193
  // src/lib/copy-templates.ts
93
194
  import fs2 from "fs/promises";
94
195
  import path2 from "path";
@@ -604,10 +705,10 @@ var structuredPrinted = /* @__PURE__ */ new WeakSet();
604
705
  function printedStructuredError(res) {
605
706
  return structuredPrinted.has(res);
606
707
  }
607
- async function apiFetch(url, init = {}) {
608
- const headers = new Headers(init.headers);
708
+ async function apiFetch(url, init2 = {}) {
709
+ const headers = new Headers(init2.headers);
609
710
  if (!headers.has(CLI_VERSION_HEADER)) headers.set(CLI_VERSION_HEADER, getCliVersion());
610
- const res = await fetch(url, { ...init, headers });
711
+ const res = await fetch(url, { ...init2, headers });
611
712
  if (res.status === 426) {
612
713
  try {
613
714
  const body = await res.clone().json();
@@ -1157,7 +1258,7 @@ async function listOwnSlugs(apiUrl, token, log) {
1157
1258
  import { spawn as spawn3 } from "child_process";
1158
1259
  import crypto3 from "crypto";
1159
1260
  import fs9 from "fs/promises";
1160
- import os2 from "os";
1261
+ import os3 from "os";
1161
1262
  import path10 from "path";
1162
1263
  function run(cmd, args, env) {
1163
1264
  return new Promise((resolve) => {
@@ -1354,7 +1455,7 @@ async function pushWorktree(cwd, pushUrl, managed, log) {
1354
1455
  log.error("Couldn't save your game's source \u2014 please try again.");
1355
1456
  return false;
1356
1457
  };
1357
- const gitDir = await fs9.mkdtemp(path10.join(os2.tmpdir(), "genex-source-"));
1458
+ const gitDir = await fs9.mkdtemp(path10.join(os3.tmpdir(), "genex-source-"));
1358
1459
  const base = { GIT_DIR: gitDir };
1359
1460
  const ident = {
1360
1461
  GIT_AUTHOR_NAME: "genex",
@@ -2619,6 +2720,8 @@ ${c.bold("Environment")}
2619
2720
  GENEX_API_URL Overrides the default API base URL.
2620
2721
  GENEX_COLYSEUS_URL Overrides the default multiplayer URL.
2621
2722
  GENEX_BROWSER Command used to open the browser (falls back to BROWSER).
2723
+ GENEX_TELEMETRY Set to 0 to disable anonymous crash reporting (Sentry).
2724
+ DO_NOT_TRACK Standard opt-out; any value disables crash reporting.
2622
2725
 
2623
2726
  ${c.bold("Examples")}
2624
2727
  genex init my-game
@@ -2854,6 +2957,17 @@ async function main() {
2854
2957
  const updateLog = createLogger({ quiet: parsed.options.quiet });
2855
2958
  if (parsed.command !== "init") await syncSkills(updateLog);
2856
2959
  const updateCheck = startUpdateCheck();
2960
+ Sentry2.setTag("command", parsed.command);
2961
+ Sentry2.setTag("cli.version", getCliVersion());
2962
+ Sentry2.setTag("node.version", process.versions.node);
2963
+ Sentry2.setTag("os.platform", process.platform);
2964
+ Sentry2.setContext("runtime", {
2965
+ cliVersion: getCliVersion(),
2966
+ node: process.version,
2967
+ platform: process.platform,
2968
+ arch: process.arch,
2969
+ ci: !!process.env.CI
2970
+ });
2857
2971
  try {
2858
2972
  if (GEN_KINDS.has(parsed.command)) {
2859
2973
  await runGenerate(parsed.command, {
@@ -2891,10 +3005,13 @@ async function main() {
2891
3005
  }
2892
3006
  } finally {
2893
3007
  await reportUpdateNudges(updateCheck, updateLog);
3008
+ await flushSentry();
2894
3009
  }
2895
3010
  }
2896
- main().catch((err) => {
3011
+ main().catch(async (err) => {
3012
+ Sentry2.captureException(err);
2897
3013
  const log = createLogger();
2898
3014
  log.error(err instanceof Error ? err.message : String(err));
3015
+ await flushSentry();
2899
3016
  process.exitCode = 1;
2900
3017
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.38.0",
3
+ "version": "0.39.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": {
@@ -35,6 +35,9 @@
35
35
  "publishConfig": {
36
36
  "access": "public"
37
37
  },
38
+ "dependencies": {
39
+ "@sentry/node": "^10.63.0"
40
+ },
38
41
  "devDependencies": {
39
42
  "@dimforge/rapier3d-compat": "^0.19.3",
40
43
  "@pixiv/three-vrm": "^3.5.4",