great-cto 2.5.0 → 2.5.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.
@@ -54,21 +54,46 @@ function* walk(root, exclude) {
54
54
  function fileMatchesGlobs(file, globs) {
55
55
  if (!globs || globs.length === 0)
56
56
  return true;
57
- // Tiny glob regex. Convert globs in two passes:
58
- // 1. Replace ** and * with sentinel placeholders.
59
- // 2. Escape remaining regex metachars.
60
- // 3. Replace placeholders with their regex equivalents.
57
+ // Normalize path separators for cross-platform matching
58
+ const normalized = file.replace(/\\/g, '/');
61
59
  return globs.some((g) => {
62
- const pattern = g
63
- .replace(/\*\*/g, '') // ** SOH
64
- .replace(/\*/g, '') // * → STX
65
- .replace(/\?/g, '') // ? → ETX
66
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
67
- .replace(//g, '.*')
68
- .replace(//g, '[^/]*')
69
- .replace(//g, '.');
60
+ // Token-based glob → regex conversion. Walks character-by-character to
61
+ // avoid the substitution-order pitfalls of multiple replace passes.
62
+ // Treats `**/` as "zero or more path segments" — so `**/*.ts` matches
63
+ // both `foo.ts` (root) and `src/lib/foo.ts` (nested).
64
+ let re = '';
65
+ for (let i = 0; i < g.length; i++) {
66
+ const c = g[i];
67
+ if (c === '*' && g[i + 1] === '*') {
68
+ // ** consumes the trailing /, so `**/x` becomes `(?:.*\/)?x` not `.*\/x`
69
+ if (g[i + 2] === '/') {
70
+ re += '(?:.*\\/)?';
71
+ i += 2;
72
+ }
73
+ else {
74
+ re += '.*';
75
+ i++;
76
+ }
77
+ }
78
+ else if (c === '*') {
79
+ re += '[^/]*';
80
+ }
81
+ else if (c === '?') {
82
+ re += '.';
83
+ }
84
+ else if ('.+^${}()|[]\\'.includes(c)) {
85
+ re += '\\' + c;
86
+ }
87
+ else if (c === '/') {
88
+ re += '/';
89
+ }
90
+ else {
91
+ re += c;
92
+ }
93
+ }
70
94
  try {
71
- return new RegExp(pattern).test(file);
95
+ // Match suffix — `src/foo.ts` matches `**/*.ts` regardless of cwd
96
+ return new RegExp('(?:^|/)' + re + '$').test(normalized);
72
97
  }
73
98
  catch {
74
99
  return false;
package/dist/main.js CHANGED
@@ -16,7 +16,7 @@ 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";
19
+ import { resolveTelemetryConsent, sendInstallPing, sendUsagePing } from "./telemetry.js";
20
20
  import { shouldUseLlmFallback, suggestArchetypeFromLlm } from "./llm-fallback.js";
21
21
  import { readFileSync } from "node:fs";
22
22
  import { dirname, join } from "node:path";
@@ -727,6 +727,19 @@ async function runInit(args) {
727
727
  log("");
728
728
  return 0;
729
729
  }
730
+ /**
731
+ * Exit with telemetry ping for a subcommand. Fire-and-forget — we wait up
732
+ * to 200ms for the ping to land before exiting so it doesn't get killed by
733
+ * process termination. Honours all telemetry opt-out signals.
734
+ */
735
+ async function exitWithTelemetry(subcommand, code) {
736
+ try {
737
+ const promise = sendUsagePing({ cliVersion: getCliVersion(), subcommand, exitCode: code });
738
+ await Promise.race([promise, new Promise(r => setTimeout(r, 200))]);
739
+ }
740
+ catch { /* never block exit */ }
741
+ process.exit(code);
742
+ }
730
743
  async function main() {
731
744
  const rawArgv = process.argv.slice(2);
732
745
  const args = parseArgs(rawArgv);
@@ -778,7 +791,7 @@ async function main() {
778
791
  try {
779
792
  const { runCi, parseCiArgs } = await import("./ci.js");
780
793
  const code = await runCi(parseCiArgs(rawArgv));
781
- process.exit(code);
794
+ await exitWithTelemetry("ci", code);
782
795
  }
783
796
  catch (e) {
784
797
  error(e.message);
@@ -792,7 +805,7 @@ async function main() {
792
805
  const portArg = rawArgv.indexOf("--port");
793
806
  const port = portArg >= 0 ? parseInt(rawArgv[portArg + 1] ?? "8765", 10) : 8765;
794
807
  const code = await runMcp({ mode: sse ? "sse" : "stdio", port, version: getCliVersion() });
795
- process.exit(code);
808
+ await exitWithTelemetry("mcp", code);
796
809
  }
797
810
  catch (e) {
798
811
  error(e.message);
@@ -814,7 +827,7 @@ async function main() {
814
827
  dryRun: rawArgv.includes("--dry-run"),
815
828
  cwd: args.dir,
816
829
  });
817
- process.exit(code);
830
+ await exitWithTelemetry("adapt", code);
818
831
  }
819
832
  catch (e) {
820
833
  error(e.message);
@@ -829,7 +842,7 @@ async function main() {
829
842
  noLog: rawArgv.includes("--no-log"),
830
843
  insecure: rawArgv.includes("--insecure"),
831
844
  });
832
- process.exit(code);
845
+ await exitWithTelemetry("serve", code);
833
846
  }
834
847
  catch (e) {
835
848
  error(e.message);
@@ -845,7 +858,7 @@ async function main() {
845
858
  process.exit(2);
846
859
  }
847
860
  const code = await runWebhookCli(parsed);
848
- process.exit(code);
861
+ await exitWithTelemetry("webhook", code);
849
862
  }
850
863
  catch (e) {
851
864
  error(e.message);
@@ -860,7 +873,7 @@ async function main() {
860
873
  process.exit(2);
861
874
  }
862
875
  const code = await runReport(parsed);
863
- process.exit(code);
876
+ await exitWithTelemetry("report", code);
864
877
  }
865
878
  catch (e) {
866
879
  error(e.message);
package/dist/telemetry.js CHANGED
@@ -115,3 +115,45 @@ export async function sendInstallPing(opts) {
115
115
  // never block install on telemetry failure
116
116
  }
117
117
  }
118
+ /**
119
+ * Subcommand-usage ping. Fire-and-forget. Used to track which v2.4+ commands
120
+ * (ci / mcp / adapt / serve / report / webhook) actually get used in the wild.
121
+ *
122
+ * Sends only:
123
+ * - install_id (random UUID, set on first install)
124
+ * - cli_version
125
+ * - subcommand name
126
+ * - exit code (0 / 1 / 2)
127
+ *
128
+ * No paths, no flags (since flags often contain user input), no archetype.
129
+ * Honours the same opt-out signals as install ping.
130
+ */
131
+ export async function sendUsagePing(opts) {
132
+ if (process.env.GREATCTO_NO_TELEMETRY === "1")
133
+ return;
134
+ const cfg = readConfig();
135
+ if (cfg.telemetry === false)
136
+ return;
137
+ if (!cfg.install_id)
138
+ return; // never ping without an established install_id
139
+ try {
140
+ const ctrl = new AbortController();
141
+ const timer = setTimeout(() => ctrl.abort(), TELEMETRY_TIMEOUT_MS);
142
+ await fetch(`${TELEMETRY_ENDPOINT.replace("/install", "/usage")}`, {
143
+ method: "POST",
144
+ headers: { "Content-Type": "application/json", "User-Agent": `great-cto-cli/${opts.cliVersion}` },
145
+ body: JSON.stringify({
146
+ install_id: cfg.install_id,
147
+ cli_version: opts.cliVersion,
148
+ subcommand: opts.subcommand,
149
+ exit_code: opts.exitCode,
150
+ ts: new Date().toISOString(),
151
+ }),
152
+ signal: ctrl.signal,
153
+ }).catch(() => { });
154
+ clearTimeout(timer);
155
+ }
156
+ catch {
157
+ // best-effort
158
+ }
159
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "great-cto",
3
- "version": "2.5.0",
3
+ "version": "2.5.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",