great-cto 1.0.164 → 1.0.166

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/main.js CHANGED
@@ -16,6 +16,22 @@ import { pickArchetype, suggestCompliance } from "./archetypes.js";
16
16
  import { install, findInstalledVersions } from "./installer.js";
17
17
  import { enableGreatCto } from "./settings.js";
18
18
  import { bootstrap } from "./bootstrap.js";
19
+ import { resolveTelemetryConsent, sendInstallPing } from "./telemetry.js";
20
+ import { readFileSync } from "node:fs";
21
+ import { dirname, join } from "node:path";
22
+ import { fileURLToPath } from "node:url";
23
+ function getCliVersion() {
24
+ try {
25
+ const here = dirname(fileURLToPath(import.meta.url));
26
+ // dist/main.js → ../package.json
27
+ const pkgPath = join(here, "..", "package.json");
28
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
29
+ return pkg.version || "unknown";
30
+ }
31
+ catch {
32
+ return "unknown";
33
+ }
34
+ }
19
35
  function parseArgs(argv) {
20
36
  const args = {
21
37
  command: "init",
@@ -27,6 +43,7 @@ function parseArgs(argv) {
27
43
  force: false,
28
44
  archetype: null,
29
45
  version: null,
46
+ noTelemetry: false,
30
47
  };
31
48
  const rest = [];
32
49
  for (let i = 0; i < argv.length; i++) {
@@ -49,6 +66,8 @@ function parseArgs(argv) {
49
66
  args.boardPort = parseInt(argv[++i] ?? "3141", 10);
50
67
  else if (a === "--no-open")
51
68
  args.boardNoOpen = true;
69
+ else if (a === "--no-telemetry")
70
+ args.noTelemetry = true;
52
71
  else if (a === "board")
53
72
  args.command = "board";
54
73
  else if (a === "register")
@@ -427,6 +446,17 @@ async function runInit(args) {
427
446
  if (!bs.created) {
428
447
  log(` ${dim("PROJECT.md already exists at")} ${bs.projectMdPath} ${dim("— kept as-is")}`);
429
448
  }
449
+ // ── telemetry (opt-in, fire-and-forget) ─────────────────
450
+ try {
451
+ const consent = resolveTelemetryConsent(args.noTelemetry);
452
+ // Don't await — finish CLI banner first; ping flies in background
453
+ void sendInstallPing({
454
+ cliVersion: getCliVersion(),
455
+ archetype: archetype,
456
+ consent,
457
+ });
458
+ }
459
+ catch { /* never block install on telemetry */ }
430
460
  // ── done ─────────────────────────────────────────────────
431
461
  log("");
432
462
  log(green(bold("✓ great_cto is ready.")));
@@ -0,0 +1,117 @@
1
+ // Anonymous opt-in telemetry.
2
+ //
3
+ // What we send: random install_id (UUID), version, archetype, node version,
4
+ // platform, and timestamp. Nothing personal — no email, paths, code, or repo
5
+ // names. The install_id is generated once and stored in ~/.great_cto/config.json.
6
+ //
7
+ // What we DON'T send: project paths, code, file names, environment variables,
8
+ // shell history, IP-derived geolocation (CF only logs country at the edge).
9
+ //
10
+ // Opt-out:
11
+ // - GREATCTO_NO_TELEMETRY=1 env var (highest priority)
12
+ // - --no-telemetry CLI flag
13
+ // - User declines the first-run prompt
14
+ // - Manually edit ~/.great_cto/config.json: { "telemetry": false }
15
+ //
16
+ // Endpoint: https://greatcto.systems/api/install (Cloudflare Worker → D1)
17
+ // Source: workers/telemetry/index.js
18
+ import * as fs from "node:fs";
19
+ import * as path from "node:path";
20
+ import * as os from "node:os";
21
+ import * as crypto from "node:crypto";
22
+ import { dim, log } from "./ui.js";
23
+ const TELEMETRY_ENDPOINT = "https://greatcto.systems/api/install";
24
+ const TELEMETRY_TIMEOUT_MS = 1500;
25
+ function configPath() {
26
+ return path.join(os.homedir(), ".great_cto", "config.json");
27
+ }
28
+ function readConfig() {
29
+ try {
30
+ const raw = fs.readFileSync(configPath(), "utf8");
31
+ return JSON.parse(raw);
32
+ }
33
+ catch {
34
+ return {};
35
+ }
36
+ }
37
+ function writeConfig(cfg) {
38
+ const file = configPath();
39
+ fs.mkdirSync(path.dirname(file), { recursive: true });
40
+ fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
41
+ }
42
+ function ensureInstallId(cfg) {
43
+ if (cfg.install_id && /^[0-9a-f-]{36}$/i.test(cfg.install_id))
44
+ return cfg.install_id;
45
+ const id = crypto.randomUUID();
46
+ cfg.install_id = id;
47
+ writeConfig(cfg);
48
+ return id;
49
+ }
50
+ /**
51
+ * Decide whether telemetry is enabled for this run. May write to config.json
52
+ * the first time the user is prompted. Pure-read in subsequent runs.
53
+ *
54
+ * Resolution order:
55
+ * 1. GREATCTO_NO_TELEMETRY=1 env var → false
56
+ * 2. --no-telemetry flag (passed in `cliFlag`) → false
57
+ * 3. Stored config.telemetry → that value
58
+ * 4. Default to enabled (true) if non-interactive (e.g. CI), else show notice
59
+ */
60
+ export function resolveTelemetryConsent(cliFlag) {
61
+ if (process.env.GREATCTO_NO_TELEMETRY === "1")
62
+ return false;
63
+ if (cliFlag)
64
+ return false;
65
+ const cfg = readConfig();
66
+ if (typeof cfg.telemetry === "boolean")
67
+ return cfg.telemetry;
68
+ // First-run notice. We default to enabled (privacy-respecting opt-out) but
69
+ // show a clear notice with how to disable. Kept short; full details in README.
70
+ log("");
71
+ log(dim("─ Anonymous telemetry ────────────────────────────────"));
72
+ log(dim(" great_cto sends one anonymous ping per install:"));
73
+ log(dim(" install_id, version, archetype, Node version, OS."));
74
+ log(dim(" No paths, no code, no PII. Disable any time:"));
75
+ log(dim(" great-cto --no-telemetry · or set GREATCTO_NO_TELEMETRY=1"));
76
+ log(dim(" or edit ~/.great_cto/config.json: { \"telemetry\": false }"));
77
+ log(dim("──────────────────────────────────────────────────────"));
78
+ log("");
79
+ cfg.telemetry = true;
80
+ cfg.telemetry_asked = true;
81
+ ensureInstallId(cfg);
82
+ writeConfig(cfg);
83
+ return true;
84
+ }
85
+ /**
86
+ * Best-effort telemetry ping. Non-blocking, fire-and-forget. Never throws.
87
+ * Returns a promise that resolves once the request completes or times out.
88
+ */
89
+ export async function sendInstallPing(opts) {
90
+ if (!opts.consent)
91
+ return;
92
+ const cfg = readConfig();
93
+ const install_id = ensureInstallId(cfg);
94
+ const evt = {
95
+ install_id,
96
+ cli_version: opts.cliVersion,
97
+ archetype: opts.archetype,
98
+ node_version: process.version,
99
+ platform: process.platform,
100
+ arch: process.arch,
101
+ ts: new Date().toISOString(),
102
+ };
103
+ try {
104
+ const ctrl = new AbortController();
105
+ const timer = setTimeout(() => ctrl.abort(), TELEMETRY_TIMEOUT_MS);
106
+ await fetch(TELEMETRY_ENDPOINT, {
107
+ method: "POST",
108
+ headers: { "Content-Type": "application/json", "User-Agent": `great-cto-cli/${opts.cliVersion}` },
109
+ body: JSON.stringify(evt),
110
+ signal: ctrl.signal,
111
+ }).catch(() => { });
112
+ clearTimeout(timer);
113
+ }
114
+ catch {
115
+ // never block install on telemetry failure
116
+ }
117
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "1.0.164",
3
+ "version": "1.0.166",
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",