great-cto 2.8.6 → 2.9.2
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/adapt.js +38 -0
- package/dist/webhook-dispatch.js +70 -1
- package/package.json +2 -2
- package/dist/telemetry.js +0 -225
package/dist/adapt.js
CHANGED
|
@@ -77,6 +77,44 @@ Configure Claude Code to launch:
|
|
|
77
77
|
npx great-cto mcp
|
|
78
78
|
\`\`\`
|
|
79
79
|
|
|
80
|
+
## Subagent routing (do not default to general-purpose)
|
|
81
|
+
|
|
82
|
+
When dispatching the **Agent** tool, pick the right \`subagent_type\` based on
|
|
83
|
+
what's being changed. \`general-purpose\` is a fallback, **not** the default —
|
|
84
|
+
using it for pattern-matched work silently skips specialist review.
|
|
85
|
+
|
|
86
|
+
| Trigger (file pattern OR topic in prompt) | Use \`subagent_type:\` |
|
|
87
|
+
|---|---|
|
|
88
|
+
| \`migrations/\`, \`schema.sql\`, ORM migration files | \`db-migration-reviewer\` |
|
|
89
|
+
| \`auth/\`, OAuth/SAML/JWT, login flow, password reset | \`security-officer\` |
|
|
90
|
+
| Payment endpoints, \`stripe.\`, webhooks, refund flow | \`pci-reviewer\` |
|
|
91
|
+
| Prompts in \`prompts/\`, RAG, tool definitions, LLM-facing strings | \`ai-security-reviewer\` |
|
|
92
|
+
| Eval suites, golden citation tests, prompt regression | \`ai-eval-engineer\` |
|
|
93
|
+
| Play Store, App Store, iOS/Android release | \`mobile-store-reviewer\` |
|
|
94
|
+
| API contract: OpenAPI, GraphQL schema, webhook signatures | \`api-platform-reviewer\` |
|
|
95
|
+
| Voice/IVR/telephony, Twilio, recording-consent, TCPA | \`voice-ai-reviewer\` |
|
|
96
|
+
| Clinical / SaMD / FDA / FHIR / PHI | \`ai-clinical-reviewer\`, \`fda-reviewer\` |
|
|
97
|
+
| Lending, ECOA, FCRA, NMLS, adverse action | \`lending-credit-reviewer\` |
|
|
98
|
+
| HR-AI, hiring, AEDT, resume screening | \`hr-ai-reviewer\` |
|
|
99
|
+
| Infra-as-code: Terraform / Helm / CDK / Pulumi | \`infra-reviewer\` |
|
|
100
|
+
| Performance regression, hot path, p99 budgets | \`performance-engineer\` |
|
|
101
|
+
| Browser extension manifest, MV3 permissions | \`web-store-reviewer\` |
|
|
102
|
+
| New feature implementation (TDD) | \`senior-dev\` |
|
|
103
|
+
| Architecture decisions, ADRs, scaling questions | \`architect\` |
|
|
104
|
+
| Planning a feature into tasks (Beads), dependency graph | \`pm\` |
|
|
105
|
+
| QA report after impl, coverage + acceptance | \`qa-engineer\` |
|
|
106
|
+
| Deploy / canary / rollback / SLO | \`devops\` |
|
|
107
|
+
| Production incident triage, P0 postmortem | \`l3-support\` |
|
|
108
|
+
| Pattern extraction from session, retro to \`lessons.md\` | \`continuous-learner\` |
|
|
109
|
+
|
|
110
|
+
Rule of thumb: if a file pattern or topic matches above, use that specialist
|
|
111
|
+
**before** falling back to \`general-purpose\`. Specialist agents catch
|
|
112
|
+
domain-specific bugs (race conditions in migrations, prompt-injection vectors,
|
|
113
|
+
TCPA violations) that a generalist won't flag.
|
|
114
|
+
|
|
115
|
+
When in doubt, run two agents in parallel: the specialist + general-purpose,
|
|
116
|
+
and reconcile their outputs.
|
|
117
|
+
|
|
80
118
|
## Style + conventions
|
|
81
119
|
|
|
82
120
|
- Tests **before** implementation (RED → GREEN → REFACTOR). Coverage 80%+ default.
|
package/dist/webhook-dispatch.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// - slack: posts as Slack incoming-webhook JSON ({text, blocks?})
|
|
12
12
|
// - discord: Discord webhook JSON ({content, embeds?})
|
|
13
13
|
// - pagerduty: Events API v2 ({routing_key, event_action, payload})
|
|
14
|
+
// - resend: HTML email via Resend API ({from, to, subject, html})
|
|
14
15
|
// - generic: arbitrary JSON POST
|
|
15
16
|
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
|
|
16
17
|
import { homedir } from "node:os";
|
|
@@ -39,6 +40,62 @@ function formatDiscord(ev) {
|
|
|
39
40
|
embeds: ev.body ? [{ description: ev.body, color }] : undefined,
|
|
40
41
|
};
|
|
41
42
|
}
|
|
43
|
+
function emojiForLevel(level) {
|
|
44
|
+
return level === "critical" ? "🚨"
|
|
45
|
+
: level === "error" ? "❌"
|
|
46
|
+
: level === "warning" ? "⏸️"
|
|
47
|
+
: "ℹ️";
|
|
48
|
+
}
|
|
49
|
+
function escapeHtml(s) {
|
|
50
|
+
return s.replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the Resend API payload — POSTed to https://api.resend.com/emails
|
|
54
|
+
* with Authorization: Bearer <api_key>.
|
|
55
|
+
*
|
|
56
|
+
* Body fields from meta:
|
|
57
|
+
* - project, link (CTA URL), action (CTA label)
|
|
58
|
+
* - kv: Record<string,string> for the metric table
|
|
59
|
+
* - severity (optional override for color)
|
|
60
|
+
*/
|
|
61
|
+
function formatResend(ev, hook) {
|
|
62
|
+
const accent = ev.level === "critical" ? "#dc2626"
|
|
63
|
+
: ev.level === "error" ? "#ea580c"
|
|
64
|
+
: ev.level === "warning" ? "#d97706"
|
|
65
|
+
: "#00d97e";
|
|
66
|
+
const emoji = emojiForLevel(ev.level);
|
|
67
|
+
const meta = (ev.meta ?? {});
|
|
68
|
+
const project = typeof meta.project === "string" ? meta.project : "great_cto";
|
|
69
|
+
const link = typeof meta.link === "string" ? meta.link : "";
|
|
70
|
+
const action = typeof meta.action === "string" ? meta.action : "View in board";
|
|
71
|
+
const kv = (meta.kv ?? {});
|
|
72
|
+
const tableRows = Object.entries(kv)
|
|
73
|
+
.map(([k, v]) => `<tr><td style="padding:6px 12px;color:#6b7280;font-size:12px;font-family:ui-monospace,monospace;text-transform:uppercase;letter-spacing:.05em">${escapeHtml(k)}</td><td style="padding:6px 12px;color:#111827;font-size:14px;font-weight:500">${escapeHtml(v)}</td></tr>`)
|
|
74
|
+
.join("");
|
|
75
|
+
const html = `<!doctype html><html><body style="margin:0;background:#f9fafb;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;color:#111827">
|
|
76
|
+
<div style="max-width:560px;margin:32px auto;background:#ffffff;border:1px solid #e5e7eb;border-radius:12px;overflow:hidden">
|
|
77
|
+
<div style="padding:20px 24px;border-bottom:1px solid #e5e7eb;background:#0a0e0c;color:#ffffff">
|
|
78
|
+
<div style="font-size:11px;font-family:ui-monospace,monospace;letter-spacing:.1em;color:#9ca3af">${escapeHtml(project.toUpperCase())} · GREATCTO</div>
|
|
79
|
+
<div style="font-size:20px;font-weight:600;margin-top:6px;color:${accent}">${emoji} ${escapeHtml(ev.title)}</div>
|
|
80
|
+
</div>
|
|
81
|
+
${ev.body ? `<div style="padding:20px 24px;font-size:14px;line-height:1.55;color:#374151">${escapeHtml(ev.body).replace(/\n/g, "<br>")}</div>` : ""}
|
|
82
|
+
${tableRows ? `<table style="width:100%;border-top:1px solid #e5e7eb;border-collapse:collapse">${tableRows}</table>` : ""}
|
|
83
|
+
${link ? `<div style="padding:24px;text-align:center;border-top:1px solid #e5e7eb">
|
|
84
|
+
<a href="${escapeHtml(link)}" style="display:inline-block;background:${accent};color:#ffffff;text-decoration:none;padding:10px 22px;border-radius:8px;font-weight:600;font-size:14px">${escapeHtml(action)}</a>
|
|
85
|
+
</div>` : ""}
|
|
86
|
+
<div style="padding:14px 24px;background:#f9fafb;font-size:11px;color:#9ca3af;font-family:ui-monospace,monospace">
|
|
87
|
+
Sent by great_cto · ${escapeHtml(ev.name)} · ${new Date().toISOString()}<br>
|
|
88
|
+
Unsubscribe: edit ~/.great_cto/webhooks.json or the Notifications tab in the board
|
|
89
|
+
</div>
|
|
90
|
+
</div></body></html>`;
|
|
91
|
+
const to = (hook.to ?? "").split(",").map(s => s.trim()).filter(Boolean);
|
|
92
|
+
return {
|
|
93
|
+
from: hook.from ?? "GreatCTO <notifications@greatcto.systems>",
|
|
94
|
+
to,
|
|
95
|
+
subject: `${emoji} ${ev.title}`,
|
|
96
|
+
html,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
42
99
|
function formatPagerDuty(ev, routingKey) {
|
|
43
100
|
// PagerDuty Events API v2
|
|
44
101
|
const severity = ev.level === "critical" ? "critical"
|
|
@@ -65,6 +122,7 @@ function buildPayload(hook, ev) {
|
|
|
65
122
|
const key = hook.headers?.routing_key ?? "";
|
|
66
123
|
return formatPagerDuty(ev, key);
|
|
67
124
|
}
|
|
125
|
+
case "resend": return formatResend(ev, hook);
|
|
68
126
|
case "generic":
|
|
69
127
|
default: return { event: ev.name, ...ev };
|
|
70
128
|
}
|
|
@@ -79,7 +137,18 @@ async function deliver(hook, ev, attempt = 0) {
|
|
|
79
137
|
};
|
|
80
138
|
// Don't leak routing_key as HTTP header — PagerDuty wants it in body
|
|
81
139
|
delete headers.routing_key;
|
|
82
|
-
|
|
140
|
+
// Resend: Bearer auth + URL override (config can leave url blank).
|
|
141
|
+
let url = hook.url;
|
|
142
|
+
if (hook.format === "resend") {
|
|
143
|
+
if (!hook.apiKey)
|
|
144
|
+
throw new Error("resend: apiKey is required");
|
|
145
|
+
if (!hook.to)
|
|
146
|
+
throw new Error("resend: 'to' email is required");
|
|
147
|
+
headers["Authorization"] = `Bearer ${hook.apiKey}`;
|
|
148
|
+
if (!url)
|
|
149
|
+
url = "https://api.resend.com/emails";
|
|
150
|
+
}
|
|
151
|
+
const res = await fetch(url, { method: "POST", headers, body });
|
|
83
152
|
if (!res.ok) {
|
|
84
153
|
throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
85
154
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "great-cto",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.2",
|
|
4
4
|
"description": "One command install for the great_cto Claude Code plugin. Auto-detects your stack, picks the right archetype, bootstraps PROJECT.md.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -86,4 +86,4 @@
|
|
|
86
86
|
"engines": {
|
|
87
87
|
"node": ">=18.17.0"
|
|
88
88
|
}
|
|
89
|
-
}
|
|
89
|
+
}
|
package/dist/telemetry.js
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
// Anonymous opt-IN telemetry — default OFF.
|
|
2
|
-
//
|
|
3
|
-
// See docs/PRIVACY.md for the full policy. Short version:
|
|
4
|
-
// - Default: disabled (opt-in)
|
|
5
|
-
// - Honors DO_NOT_TRACK=1 (industry standard, https://consoledonottrack.com)
|
|
6
|
-
// - Skipped automatically in CI environments
|
|
7
|
-
// - No paths, no code, no PII — just {ts, version, command, archetype, node, os, exit, duration_ms, anon_id}
|
|
8
|
-
// - anon_id is sha256(user@hostname) truncated to 8 hex chars; not reversible
|
|
9
|
-
//
|
|
10
|
-
// Opt-in (any one):
|
|
11
|
-
// GREAT_CTO_TELEMETRY=on (env var)
|
|
12
|
-
// ~/.great_cto/telemetry.json: { "enabled": true }
|
|
13
|
-
// npx great-cto telemetry on
|
|
14
|
-
//
|
|
15
|
-
// Opt-out (overrides everything):
|
|
16
|
-
// DO_NOT_TRACK=1 (highest priority)
|
|
17
|
-
// GREAT_CTO_TELEMETRY=off
|
|
18
|
-
// GREAT_CTO_DISABLE_TELEMETRY=1 (legacy alias from v2.x)
|
|
19
|
-
// GREATCTO_NO_TELEMETRY=1 (legacy alias from v2.x)
|
|
20
|
-
// ~/.great_cto/telemetry.json: { "enabled": false }
|
|
21
|
-
//
|
|
22
|
-
// Endpoint: https://telemetry.greatcto.systems/v1/event (Cloudflare Worker → D1)
|
|
23
|
-
// Worker: workers/telemetry/index.ts
|
|
24
|
-
// Schema v1: see docs/PRIVACY.md "What we collect"
|
|
25
|
-
import * as fs from "node:fs";
|
|
26
|
-
import * as path from "node:path";
|
|
27
|
-
import * as os from "node:os";
|
|
28
|
-
import * as crypto from "node:crypto";
|
|
29
|
-
const TELEMETRY_ENDPOINT = process.env.GREAT_CTO_TELEMETRY_ENDPOINT
|
|
30
|
-
|| "https://great-cto-telemetry.alexander-velikiy.workers.dev/v1/event";
|
|
31
|
-
// Note: workers.dev URL is the temporary default until telemetry.greatcto.systems
|
|
32
|
-
// custom domain is bound. Override anytime with GREAT_CTO_TELEMETRY_ENDPOINT.
|
|
33
|
-
const TELEMETRY_TIMEOUT_MS = 1000;
|
|
34
|
-
// Allowlist — anything else is dropped client-side and server-side.
|
|
35
|
-
const ALLOWED_COMMANDS = new Set([
|
|
36
|
-
"init", "scan", "ci", "list-rules", "board", "register",
|
|
37
|
-
"adapt", "mcp", "report", "serve", "webhook",
|
|
38
|
-
"version", "help", "telemetry",
|
|
39
|
-
]);
|
|
40
|
-
// Allowlist for archetype field. Match the 25 documented + "none" + "unknown".
|
|
41
|
-
const ALLOWED_ARCHETYPES = new Set([
|
|
42
|
-
"none", "unknown", "greenfield",
|
|
43
|
-
"enterprise-saas", "agent-product", "ai-system", "mlops",
|
|
44
|
-
"cli-tool", "cli", "library", "sdk", "devtools",
|
|
45
|
-
"fintech", "regulated", "compliance",
|
|
46
|
-
"iot-embedded", "web3", "marketplace", "cms", "edtech",
|
|
47
|
-
"gov-public", "insurance", "data-platform", "streaming",
|
|
48
|
-
"mobile-app", "infra", "web-service", "agent",
|
|
49
|
-
]);
|
|
50
|
-
function configPath() {
|
|
51
|
-
return path.join(os.homedir(), ".great_cto", "telemetry.json");
|
|
52
|
-
}
|
|
53
|
-
function legacyConfigPath() {
|
|
54
|
-
return path.join(os.homedir(), ".great_cto", "config.json");
|
|
55
|
-
}
|
|
56
|
-
function readConfig() {
|
|
57
|
-
// Try new file first.
|
|
58
|
-
try {
|
|
59
|
-
return JSON.parse(fs.readFileSync(configPath(), "utf8"));
|
|
60
|
-
}
|
|
61
|
-
catch { /* fall through */ }
|
|
62
|
-
// Fall back to legacy config.json (read-only — never write to it).
|
|
63
|
-
try {
|
|
64
|
-
const legacy = JSON.parse(fs.readFileSync(legacyConfigPath(), "utf8"));
|
|
65
|
-
return { enabled: legacy.telemetry, install_id: legacy.install_id };
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return {};
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
function writeConfig(cfg) {
|
|
72
|
-
const file = configPath();
|
|
73
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
74
|
-
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
|
|
75
|
-
}
|
|
76
|
-
/** Detect CI / automation environments — never send from these. */
|
|
77
|
-
function isCI() {
|
|
78
|
-
const flags = ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "CIRCLECI", "BUILDKITE",
|
|
79
|
-
"JENKINS_URL", "TF_BUILD", "DRONE", "TRAVIS", "APPVEYOR",
|
|
80
|
-
"BITBUCKET_BUILD_NUMBER", "TEAMCITY_VERSION", "CODEBUILD_BUILD_ID"];
|
|
81
|
-
return flags.some(f => process.env[f] != null && process.env[f] !== "");
|
|
82
|
-
}
|
|
83
|
-
/** Compute anon_id deterministically per machine, never reversible. */
|
|
84
|
-
export function computeAnonId() {
|
|
85
|
-
const seed = `great_cto/${os.userInfo().username || "?"}/${os.hostname() || "?"}`;
|
|
86
|
-
return crypto.createHash("sha256").update(seed).digest("hex").slice(0, 8);
|
|
87
|
-
}
|
|
88
|
-
/** Resolve telemetry-enabled state. Pure function, no side effects. */
|
|
89
|
-
export function isTelemetryEnabled(cliFlag = false) {
|
|
90
|
-
// Opt-out wins, in priority order:
|
|
91
|
-
if (process.env.DO_NOT_TRACK === "1" || process.env.DO_NOT_TRACK === "true")
|
|
92
|
-
return false;
|
|
93
|
-
if (process.env.GREAT_CTO_TELEMETRY === "off")
|
|
94
|
-
return false;
|
|
95
|
-
if (process.env.GREAT_CTO_DISABLE_TELEMETRY === "1")
|
|
96
|
-
return false;
|
|
97
|
-
if (process.env.GREATCTO_NO_TELEMETRY === "1")
|
|
98
|
-
return false;
|
|
99
|
-
if (cliFlag)
|
|
100
|
-
return false;
|
|
101
|
-
if (isCI())
|
|
102
|
-
return false;
|
|
103
|
-
// Opt-in checks:
|
|
104
|
-
if (process.env.GREAT_CTO_TELEMETRY === "on")
|
|
105
|
-
return true;
|
|
106
|
-
const cfg = readConfig();
|
|
107
|
-
if (cfg.enabled === true)
|
|
108
|
-
return true;
|
|
109
|
-
if (cfg.telemetry === true)
|
|
110
|
-
return true; // legacy
|
|
111
|
-
// Default: opt-out.
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
function sanitize(opts) {
|
|
115
|
-
const command = opts.command.toLowerCase();
|
|
116
|
-
if (!ALLOWED_COMMANDS.has(command))
|
|
117
|
-
return null;
|
|
118
|
-
const archetypeRaw = (opts.archetype || "none").toLowerCase().trim();
|
|
119
|
-
const archetype = ALLOWED_ARCHETYPES.has(archetypeRaw) ? archetypeRaw : "unknown";
|
|
120
|
-
return {
|
|
121
|
-
ts: new Date().toISOString(),
|
|
122
|
-
version: opts.cliVersion,
|
|
123
|
-
command,
|
|
124
|
-
archetype,
|
|
125
|
-
node: process.version.replace(/^v/, ""),
|
|
126
|
-
os: process.platform,
|
|
127
|
-
exit_code: typeof opts.exitCode === "number" ? opts.exitCode : 0,
|
|
128
|
-
duration_ms: typeof opts.durationMs === "number" ? Math.max(0, Math.round(opts.durationMs)) : 0,
|
|
129
|
-
anon_id: computeAnonId(),
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
/** Fire-and-forget POST. Never blocks. Never throws. Never logs unless DRYRUN. */
|
|
133
|
-
async function send(evt) {
|
|
134
|
-
if (process.env.GREAT_CTO_TELEMETRY_DRYRUN === "1") {
|
|
135
|
-
process.stderr.write(`[telemetry] would-send: ${JSON.stringify(evt)}\n`);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
const ctrl = new AbortController();
|
|
140
|
-
const timer = setTimeout(() => ctrl.abort(), TELEMETRY_TIMEOUT_MS);
|
|
141
|
-
await fetch(TELEMETRY_ENDPOINT, {
|
|
142
|
-
method: "POST",
|
|
143
|
-
headers: { "Content-Type": "application/json" },
|
|
144
|
-
body: JSON.stringify(evt),
|
|
145
|
-
signal: ctrl.signal,
|
|
146
|
-
}).catch(() => { });
|
|
147
|
-
clearTimeout(timer);
|
|
148
|
-
}
|
|
149
|
-
catch { /* best-effort */ }
|
|
150
|
-
}
|
|
151
|
-
// --- Public API ------------------------------------------------------------
|
|
152
|
-
/** First-run/install ping. Sent only when enabled. Idempotent across runs. */
|
|
153
|
-
export async function sendInstallPing(opts) {
|
|
154
|
-
if (!opts.consent)
|
|
155
|
-
return;
|
|
156
|
-
if (!isTelemetryEnabled())
|
|
157
|
-
return;
|
|
158
|
-
const evt = sanitize({ cliVersion: opts.cliVersion, command: "init", archetype: opts.archetype });
|
|
159
|
-
if (!evt)
|
|
160
|
-
return;
|
|
161
|
-
await send(evt);
|
|
162
|
-
}
|
|
163
|
-
/** Per-command usage ping. Sent only when enabled. Fire-and-forget. */
|
|
164
|
-
export async function sendUsagePing(opts) {
|
|
165
|
-
if (!isTelemetryEnabled())
|
|
166
|
-
return;
|
|
167
|
-
const evt = sanitize({
|
|
168
|
-
cliVersion: opts.cliVersion,
|
|
169
|
-
command: opts.subcommand,
|
|
170
|
-
archetype: opts.archetype,
|
|
171
|
-
exitCode: opts.exitCode,
|
|
172
|
-
durationMs: opts.durationMs,
|
|
173
|
-
});
|
|
174
|
-
if (!evt)
|
|
175
|
-
return;
|
|
176
|
-
await send(evt);
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Legacy shim — preserved for backwards compatibility with callers in main.ts
|
|
180
|
-
* that pass `--no-telemetry`. With opt-IN default, consent resolution is
|
|
181
|
-
* trivial: enabled iff isTelemetryEnabled() returns true.
|
|
182
|
-
*/
|
|
183
|
-
export function resolveTelemetryConsent(cliFlag) {
|
|
184
|
-
return isTelemetryEnabled(cliFlag);
|
|
185
|
-
}
|
|
186
|
-
// --- `npx great-cto telemetry <on|off|status|whoami>` subcommand -----------
|
|
187
|
-
export function telemetrySubcommand(arg) {
|
|
188
|
-
const action = (arg || "status").toLowerCase();
|
|
189
|
-
switch (action) {
|
|
190
|
-
case "on": {
|
|
191
|
-
const cfg = readConfig();
|
|
192
|
-
cfg.enabled = true;
|
|
193
|
-
writeConfig(cfg);
|
|
194
|
-
return { exitCode: 0, output: `✓ telemetry enabled (config: ${configPath()})\n` +
|
|
195
|
-
` Anonymous events go to ${TELEMETRY_ENDPOINT}\n` +
|
|
196
|
-
` See docs/PRIVACY.md for the full data schema.\n` };
|
|
197
|
-
}
|
|
198
|
-
case "off": {
|
|
199
|
-
const cfg = readConfig();
|
|
200
|
-
cfg.enabled = false;
|
|
201
|
-
writeConfig(cfg);
|
|
202
|
-
return { exitCode: 0, output: `✓ telemetry disabled (config: ${configPath()})\n` };
|
|
203
|
-
}
|
|
204
|
-
case "status": {
|
|
205
|
-
const enabled = isTelemetryEnabled();
|
|
206
|
-
const reason = enabled
|
|
207
|
-
? "enabled (sending events to " + TELEMETRY_ENDPOINT + ")"
|
|
208
|
-
: isCI()
|
|
209
|
-
? "disabled (CI environment detected)"
|
|
210
|
-
: process.env.DO_NOT_TRACK === "1"
|
|
211
|
-
? "disabled (DO_NOT_TRACK=1)"
|
|
212
|
-
: "disabled (default; run 'great-cto telemetry on' to enable)";
|
|
213
|
-
return { exitCode: 0, output: `telemetry: ${reason}\n` +
|
|
214
|
-
`anon_id : ${computeAnonId()}\n` +
|
|
215
|
-
`endpoint : ${TELEMETRY_ENDPOINT}\n` +
|
|
216
|
-
`config : ${configPath()}\n` };
|
|
217
|
-
}
|
|
218
|
-
case "whoami": {
|
|
219
|
-
return { exitCode: 0, output: computeAnonId() + "\n" };
|
|
220
|
-
}
|
|
221
|
-
default: {
|
|
222
|
-
return { exitCode: 2, output: `usage: great-cto telemetry <on|off|status|whoami>\n` };
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|