@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.
- package/README.md +21 -0
- package/dist/index.js +125 -8
- 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/
|
|
4
|
-
import
|
|
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,
|
|
608
|
-
const headers = new 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, { ...
|
|
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
|
|
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(
|
|
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.
|
|
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",
|