aislop 0.8.3 → 0.9.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/dist/cli.js CHANGED
@@ -4,14 +4,14 @@ import { Command } from "commander";
4
4
  import os from "node:os";
5
5
  import fs from "node:fs";
6
6
  import path from "node:path";
7
+ import crypto, { randomUUID } from "node:crypto";
8
+ import { performance } from "node:perf_hooks";
7
9
  import YAML from "yaml";
8
10
  import { z } from "zod/v4";
9
- import { performance } from "node:perf_hooks";
10
11
  import { execSync, spawn, spawnSync } from "node:child_process";
11
12
  import micromatch from "micromatch";
12
13
  import { fileURLToPath } from "node:url";
13
14
  import ts from "typescript";
14
- import crypto from "node:crypto";
15
15
  import { isCancel, multiselect, select, text } from "@clack/prompts";
16
16
  import pc from "picocolors";
17
17
  import wcwidth from "wcwidth";
@@ -32,6 +32,366 @@ var __exportAll = (all, no_symbols) => {
32
32
  return target;
33
33
  };
34
34
 
35
+ //#endregion
36
+ //#region src/version.ts
37
+ const APP_VERSION = "0.9.0";
38
+
39
+ //#endregion
40
+ //#region src/telemetry/env.ts
41
+ const detectPackageManager$1 = (env = process.env) => {
42
+ const execPath = env.npm_execpath ?? "";
43
+ if (execPath.includes("npx")) return "npx";
44
+ const userAgent = env.npm_config_user_agent ?? "";
45
+ if (userAgent.startsWith("pnpm/")) return "pnpm";
46
+ if (userAgent.startsWith("yarn/")) return "yarn";
47
+ if (userAgent.startsWith("bun/")) return "bun";
48
+ if (userAgent.startsWith("npm/")) return "npm";
49
+ if (execPath.includes("pnpm")) return "pnpm";
50
+ if (execPath.includes("yarn")) return "yarn";
51
+ if (execPath.includes("bun")) return "bun";
52
+ if (execPath.includes("npm")) return "npm";
53
+ return "unknown";
54
+ };
55
+ const CI_ENV_KEYS = [
56
+ "CI",
57
+ "GITHUB_ACTIONS",
58
+ "GITLAB_CI",
59
+ "CIRCLECI",
60
+ "TRAVIS",
61
+ "BUILDKITE",
62
+ "DRONE",
63
+ "TEAMCITY_VERSION",
64
+ "TF_BUILD"
65
+ ];
66
+ const isCiEnv = (env = process.env) => CI_ENV_KEYS.some((k) => {
67
+ const v = env[k];
68
+ return v === "true" || v === "1" || v != null && v.length > 0 && k !== "CI";
69
+ }) || env.CI === "true" || env.CI === "1";
70
+ const fileCountBucket = (count) => {
71
+ if (count < 10) return "0-10";
72
+ if (count < 50) return "10-50";
73
+ if (count < 100) return "50-100";
74
+ if (count < 500) return "100-500";
75
+ if (count < 1e3) return "500-1000";
76
+ return "1000+";
77
+ };
78
+ const scoreBucket = (score) => {
79
+ if (score >= 75) return "75-100";
80
+ if (score >= 50) return "50-75";
81
+ if (score >= 25) return "25-50";
82
+ return "0-25";
83
+ };
84
+
85
+ //#endregion
86
+ //#region src/telemetry/identity.ts
87
+ const FILE_BASENAME = "install_id";
88
+ const resolveInstallIdPath = (homedir = os.homedir(), env = process.env) => {
89
+ if (process.platform === "linux" && env.XDG_STATE_HOME) return path.join(env.XDG_STATE_HOME, "aislop", FILE_BASENAME);
90
+ return path.join(homedir, ".aislop", FILE_BASENAME);
91
+ };
92
+ const ensureInstallId = (idPath = resolveInstallIdPath()) => {
93
+ if (fs.existsSync(idPath)) {
94
+ const existing = fs.readFileSync(idPath, "utf-8").trim();
95
+ if (existing.length > 0) return {
96
+ installId: existing,
97
+ created: false
98
+ };
99
+ }
100
+ const dir = path.dirname(idPath);
101
+ fs.mkdirSync(dir, { recursive: true });
102
+ const installId = randomUUID();
103
+ const tmpPath = `${idPath}.${process.pid}.tmp`;
104
+ fs.writeFileSync(tmpPath, `${installId}\n`, { mode: 384 });
105
+ try {
106
+ fs.renameSync(tmpPath, idPath);
107
+ return {
108
+ installId,
109
+ created: true
110
+ };
111
+ } catch {
112
+ fs.rmSync(tmpPath, { force: true });
113
+ return {
114
+ installId: fs.readFileSync(idPath, "utf-8").trim(),
115
+ created: false
116
+ };
117
+ }
118
+ };
119
+
120
+ //#endregion
121
+ //#region src/telemetry/redaction.ts
122
+ const SAFE_PROPERTY_NAMES = new Set([
123
+ "aislop_version",
124
+ "node_version",
125
+ "os",
126
+ "arch",
127
+ "schema_version",
128
+ "anonymous_install_id",
129
+ "package_manager",
130
+ "is_ci",
131
+ "command",
132
+ "language_summary",
133
+ "lang_typescript",
134
+ "lang_javascript",
135
+ "lang_python",
136
+ "lang_java",
137
+ "file_count_bucket",
138
+ "exit_code",
139
+ "duration_ms",
140
+ "error_kind",
141
+ "score",
142
+ "score_bucket",
143
+ "finding_count",
144
+ "error_count",
145
+ "warning_count",
146
+ "fixable_count",
147
+ "fix_steps",
148
+ "fix_resolved",
149
+ "fix_score_delta",
150
+ "engine_format_issues",
151
+ "engine_format_ms",
152
+ "engine_lint_issues",
153
+ "engine_lint_ms",
154
+ "engine_code_quality_issues",
155
+ "engine_code_quality_ms",
156
+ "engine_ai_slop_issues",
157
+ "engine_ai_slop_ms",
158
+ "engine_architecture_issues",
159
+ "engine_architecture_ms",
160
+ "engine_security_issues",
161
+ "engine_security_ms",
162
+ "tool",
163
+ "ok",
164
+ "agent",
165
+ "score_delta"
166
+ ]);
167
+ const redactProperties = (props) => {
168
+ const clean = {};
169
+ const dropped = [];
170
+ for (const [key, value] of Object.entries(props)) {
171
+ if (value === void 0) continue;
172
+ if (SAFE_PROPERTY_NAMES.has(key)) clean[key] = value;
173
+ else dropped.push(key);
174
+ }
175
+ return {
176
+ clean,
177
+ dropped
178
+ };
179
+ };
180
+
181
+ //#endregion
182
+ //#region src/telemetry/client.ts
183
+ const POSTHOG_HOST = "https://eu.i.posthog.com";
184
+ const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
185
+ const SCHEMA_VERSION = "v2";
186
+ const REQUEST_TIMEOUT_MS = 3e3;
187
+ const isTelemetryDisabled = (config) => {
188
+ const env = process.env;
189
+ if (env.AISLOP_NO_TELEMETRY === "1" || env.DO_NOT_TRACK === "1") return true;
190
+ if (config?.enabled === false) return true;
191
+ if (config?.enabled === true) return false;
192
+ if (env.CI === "true" || env.CI === "1") return true;
193
+ return false;
194
+ };
195
+ const isDebug = () => process.env.AISLOP_TELEMETRY_DEBUG === "1";
196
+ const pendingRequests = /* @__PURE__ */ new Set();
197
+ let cachedInstallId = null;
198
+ let installCreated = false;
199
+ const baseProperties = (installId) => ({
200
+ aislop_version: APP_VERSION,
201
+ node_version: process.version,
202
+ os: os.platform(),
203
+ arch: os.arch(),
204
+ schema_version: SCHEMA_VERSION,
205
+ anonymous_install_id: installId,
206
+ package_manager: detectPackageManager$1(),
207
+ is_ci: isCiEnv()
208
+ });
209
+ const track = (input) => {
210
+ if (isTelemetryDisabled(input.config)) return { installCreated: false };
211
+ if (cachedInstallId == null) {
212
+ const ensured = ensureInstallId(resolveInstallIdPath());
213
+ cachedInstallId = ensured.installId;
214
+ installCreated = ensured.created;
215
+ }
216
+ const { clean, dropped } = redactProperties({
217
+ ...baseProperties(cachedInstallId),
218
+ ...input.properties
219
+ });
220
+ if (isDebug()) {
221
+ const compact = JSON.stringify({
222
+ event: input.event,
223
+ properties: clean
224
+ });
225
+ process.stderr.write(`[telemetry] ${compact}\n`);
226
+ if (dropped.length > 0) for (const key of dropped) process.stderr.write(`[telemetry] dropped non-allowlisted property: ${key}\n`);
227
+ }
228
+ if (process.env.AISLOP_TELEMETRY_DRY_RUN === "1") return { installCreated };
229
+ const payload = {
230
+ api_key: POSTHOG_KEY,
231
+ event: input.event,
232
+ distinct_id: cachedInstallId,
233
+ properties: clean,
234
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
235
+ };
236
+ const request = fetch(`${POSTHOG_HOST}/capture/`, {
237
+ method: "POST",
238
+ headers: { "Content-Type": "application/json" },
239
+ body: JSON.stringify(payload),
240
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
241
+ }).then(() => {}).catch(() => {}).finally(() => {
242
+ pendingRequests.delete(request);
243
+ });
244
+ pendingRequests.add(request);
245
+ return { installCreated };
246
+ };
247
+ const flushTelemetry = async () => {
248
+ if (pendingRequests.size === 0) return;
249
+ await Promise.all(pendingRequests);
250
+ };
251
+
252
+ //#endregion
253
+ //#region src/telemetry/language.ts
254
+ const ALL_LANGUAGES = [
255
+ "typescript",
256
+ "javascript",
257
+ "python",
258
+ "java"
259
+ ];
260
+ const buildLanguageProperties = (detected) => {
261
+ const present = new Set(detected);
262
+ const summary = [...present].filter((l) => ALL_LANGUAGES.includes(l));
263
+ summary.sort();
264
+ return {
265
+ language_summary: summary.join(","),
266
+ lang_typescript: present.has("typescript"),
267
+ lang_javascript: present.has("javascript"),
268
+ lang_python: present.has("python"),
269
+ lang_java: present.has("java")
270
+ };
271
+ };
272
+
273
+ //#endregion
274
+ //#region src/telemetry/events.ts
275
+ const buildCommandStartedProps = (input) => {
276
+ const props = { command: input.command };
277
+ if (input.languages) Object.assign(props, buildLanguageProperties(input.languages));
278
+ if (typeof input.fileCount === "number") props.file_count_bucket = fileCountBucket(input.fileCount);
279
+ return props;
280
+ };
281
+ const ENGINE_KEY_MAP = {
282
+ format: "engine_format",
283
+ lint: "engine_lint",
284
+ "code-quality": "engine_code_quality",
285
+ "ai-slop": "engine_ai_slop",
286
+ architecture: "engine_architecture",
287
+ security: "engine_security"
288
+ };
289
+ const flattenEngineStats = (issues, timings) => {
290
+ const out = {};
291
+ for (const [engine, count] of Object.entries(issues)) {
292
+ const key = ENGINE_KEY_MAP[engine];
293
+ if (key != null && typeof count === "number") out[`${key}_issues`] = count;
294
+ }
295
+ for (const [engine, ms] of Object.entries(timings)) {
296
+ const key = ENGINE_KEY_MAP[engine];
297
+ if (key != null && typeof ms === "number") out[`${key}_ms`] = Math.round(ms);
298
+ }
299
+ return out;
300
+ };
301
+ const buildCommandCompletedProps = (input) => {
302
+ const props = {
303
+ ...input.startProps,
304
+ exit_code: input.exitCode,
305
+ duration_ms: Math.round(input.durationMs)
306
+ };
307
+ if (input.errorKind) props.error_kind = input.errorKind;
308
+ if (typeof input.score === "number") {
309
+ props.score = input.score;
310
+ props.score_bucket = scoreBucket(input.score);
311
+ }
312
+ if (typeof input.findingCount === "number") props.finding_count = input.findingCount;
313
+ if (typeof input.errorCount === "number") props.error_count = input.errorCount;
314
+ if (typeof input.warningCount === "number") props.warning_count = input.warningCount;
315
+ if (typeof input.fixableCount === "number") props.fixable_count = input.fixableCount;
316
+ if (input.engineIssues && input.engineTimings) Object.assign(props, flattenEngineStats(input.engineIssues, input.engineTimings));
317
+ if (typeof input.fixSteps === "number") props.fix_steps = input.fixSteps;
318
+ if (typeof input.fixResolved === "number") props.fix_resolved = input.fixResolved;
319
+ if (typeof input.fixScoreDelta === "number") props.fix_score_delta = input.fixScoreDelta;
320
+ return props;
321
+ };
322
+ const buildHookScanCompletedProps = (input) => {
323
+ const props = {
324
+ agent: input.agent,
325
+ score: input.score,
326
+ score_bucket: scoreBucket(input.score),
327
+ finding_count: input.findingCount,
328
+ file_count_bucket: fileCountBucket(input.fileCount)
329
+ };
330
+ if (typeof input.scoreDelta === "number") props.score_delta = input.scoreDelta;
331
+ return props;
332
+ };
333
+ const errorKindFromException = (error) => {
334
+ const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
335
+ if (message.includes("timeout") || message.includes("timed out")) return "timeout";
336
+ if (message.includes("invalid config") || message.includes("config_invalid")) return "config_invalid";
337
+ if (message.includes("engine") && message.includes("crash")) return "engine_crash";
338
+ return "unknown";
339
+ };
340
+
341
+ //#endregion
342
+ //#region src/telemetry/lifecycle.ts
343
+ const withCommandLifecycle = async (start, run) => {
344
+ const startProps = buildCommandStartedProps({
345
+ command: start.command,
346
+ languages: start.languages,
347
+ fileCount: start.fileCount
348
+ });
349
+ track({
350
+ event: "cli_command_started",
351
+ properties: startProps,
352
+ config: start.config
353
+ });
354
+ const startedAt = performance.now();
355
+ try {
356
+ const result = await run();
357
+ const durationMs = performance.now() - startedAt;
358
+ track({
359
+ event: "cli_command_completed",
360
+ properties: buildCommandCompletedProps({
361
+ startProps,
362
+ exitCode: result.exitCode,
363
+ durationMs,
364
+ score: result.score,
365
+ findingCount: result.findingCount,
366
+ errorCount: result.errorCount,
367
+ warningCount: result.warningCount,
368
+ fixableCount: result.fixableCount,
369
+ engineIssues: result.engineIssues,
370
+ engineTimings: result.engineTimings,
371
+ fixSteps: result.fixSteps,
372
+ fixResolved: result.fixResolved,
373
+ fixScoreDelta: result.fixScoreDelta
374
+ }),
375
+ config: start.config
376
+ });
377
+ await flushTelemetry();
378
+ return result;
379
+ } catch (error) {
380
+ track({
381
+ event: "cli_command_completed",
382
+ properties: buildCommandCompletedProps({
383
+ startProps,
384
+ exitCode: 1,
385
+ durationMs: performance.now() - startedAt,
386
+ errorKind: errorKindFromException(error)
387
+ }),
388
+ config: start.config
389
+ });
390
+ await flushTelemetry();
391
+ throw error;
392
+ }
393
+ };
394
+
35
395
  //#endregion
36
396
  //#region src/hooks/feedback.ts
37
397
  const fingerprintFinding = (f) => `${f.file}:${f.line}:${f.ruleId}`;
@@ -4636,7 +4996,7 @@ const lintEngine = {
4636
4996
  const promises = [];
4637
4997
  if (languages.includes("typescript") || languages.includes("javascript")) {
4638
4998
  promises.push(runOxlint(context));
4639
- if (context.config.lint.typecheck) promises.push(import("./typecheck-B1MXNAy-.js").then((mod) => mod.runTypecheck(context)));
4999
+ if (context.config.lint.typecheck) promises.push(import("./typecheck-wVSohmOX.js").then((mod) => mod.runTypecheck(context)));
4640
5000
  }
4641
5001
  if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
4642
5002
  if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
@@ -5855,6 +6215,16 @@ const runClaudeHook = async (deps = {}) => {
5855
6215
  score: baseline.score,
5856
6216
  findingFingerprints: baseline.findingFingerprints
5857
6217
  } : void 0);
6218
+ track({
6219
+ event: "hook_scan_completed",
6220
+ properties: buildHookScanCompletedProps({
6221
+ agent: "claude",
6222
+ score,
6223
+ scoreDelta: baseline ? score - baseline.score : null,
6224
+ findingCount: diagnostics.length,
6225
+ fileCount: files.length
6226
+ })
6227
+ });
5858
6228
  const envelope = renderClaudeOutput(JSON.stringify(feedback));
5859
6229
  write(JSON.stringify(envelope));
5860
6230
  return 0;
@@ -5986,6 +6356,15 @@ const runCursorHook = async (deps = {}) => {
5986
6356
  try {
5987
6357
  const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
5988
6358
  const feedback = buildFeedback(diagnostics, score, rootDirectory);
6359
+ track({
6360
+ event: "hook_scan_completed",
6361
+ properties: buildHookScanCompletedProps({
6362
+ agent: "cursor",
6363
+ score,
6364
+ findingCount: diagnostics.length,
6365
+ fileCount: files.length
6366
+ })
6367
+ });
5989
6368
  const serialized = JSON.stringify(feedback);
5990
6369
  write(JSON.stringify(renderCursorOutput(serialized)));
5991
6370
  writeErr(`${serialized}\n`);
@@ -6036,6 +6415,15 @@ const runGeminiHook = async (deps = {}) => {
6036
6415
  try {
6037
6416
  const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
6038
6417
  const feedback = buildFeedback(diagnostics, score, rootDirectory);
6418
+ track({
6419
+ event: "hook_scan_completed",
6420
+ properties: buildHookScanCompletedProps({
6421
+ agent: "gemini",
6422
+ score,
6423
+ findingCount: diagnostics.length,
6424
+ fileCount: files.length
6425
+ })
6426
+ });
6039
6427
  write(JSON.stringify(renderGeminiOutput(JSON.stringify(feedback))));
6040
6428
  return 0;
6041
6429
  } catch {
@@ -7050,12 +7438,18 @@ const registerInstall = (hook) => {
7050
7438
  install.action(async (positional, opts) => {
7051
7439
  const agents = await pickAgents("install", opts, positional);
7052
7440
  if (agents === null || agents.length === 0) return;
7053
- await hookInstall({
7054
- agents,
7055
- scope: resolveScope(opts),
7056
- dryRun: Boolean(opts.dryRun),
7057
- yes: Boolean(opts.yes),
7058
- qualityGate: Boolean(opts.qualityGate)
7441
+ await withCommandLifecycle({
7442
+ command: "hook_install",
7443
+ config: loadConfig(process.cwd()).telemetry
7444
+ }, async () => {
7445
+ await hookInstall({
7446
+ agents,
7447
+ scope: resolveScope(opts),
7448
+ dryRun: Boolean(opts.dryRun),
7449
+ yes: Boolean(opts.yes),
7450
+ qualityGate: Boolean(opts.qualityGate)
7451
+ });
7452
+ return { exitCode: 0 };
7059
7453
  });
7060
7454
  });
7061
7455
  };
@@ -7065,21 +7459,39 @@ const registerUninstall = (hook) => {
7065
7459
  uninstall.action(async (positional, opts) => {
7066
7460
  const agents = await pickAgents("uninstall", opts, positional);
7067
7461
  if (agents === null || agents.length === 0) return;
7068
- await hookUninstall({
7069
- agents,
7070
- scope: resolveScope(opts),
7071
- dryRun: Boolean(opts.dryRun),
7072
- yes: true,
7073
- qualityGate: false
7462
+ await withCommandLifecycle({
7463
+ command: "hook_uninstall",
7464
+ config: loadConfig(process.cwd()).telemetry
7465
+ }, async () => {
7466
+ await hookUninstall({
7467
+ agents,
7468
+ scope: resolveScope(opts),
7469
+ dryRun: Boolean(opts.dryRun),
7470
+ yes: true,
7471
+ qualityGate: false
7472
+ });
7473
+ return { exitCode: 0 };
7074
7474
  });
7075
7475
  });
7076
7476
  };
7077
7477
  const registerCallbacks = (hook) => {
7078
7478
  hook.command("status").description("Show which agent hooks are installed").action(async () => {
7079
- await hookStatus();
7479
+ await withCommandLifecycle({
7480
+ command: "hook_status",
7481
+ config: loadConfig(process.cwd()).telemetry
7482
+ }, async () => {
7483
+ await hookStatus();
7484
+ return { exitCode: 0 };
7485
+ });
7080
7486
  });
7081
7487
  hook.command("baseline").description("Capture the current project score as the quality-gate baseline").action(async () => {
7082
- await hookBaseline();
7488
+ await withCommandLifecycle({
7489
+ command: "hook_baseline",
7490
+ config: loadConfig(process.cwd()).telemetry
7491
+ }, async () => {
7492
+ await hookBaseline();
7493
+ return { exitCode: 0 };
7494
+ });
7083
7495
  });
7084
7496
  hook.command("claude").description("Internal: Claude Code PostToolUse / Stop / FileChanged callback (reads stdin)").option("--stop", "run in Stop-hook mode for the quality gate").option("--on-file-changed", "run in FileChanged mode (refresh baseline on watched file change)").action(async (opts) => {
7085
7497
  await hookRun("claude", {
@@ -7592,73 +8004,6 @@ const renderCleanRun = (input, deps = {}) => {
7592
8004
  return `\n ${parts.join(` ${sep} `)}\n`;
7593
8005
  };
7594
8006
 
7595
- //#endregion
7596
- //#region src/version.ts
7597
- const APP_VERSION = "0.8.3";
7598
-
7599
- //#endregion
7600
- //#region src/utils/telemetry.ts
7601
- const POSTHOG_HOST = "https://eu.i.posthog.com";
7602
- const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
7603
- /**
7604
- * Returns true if telemetry should be disabled.
7605
- * Telemetry is opt-out: it runs unless explicitly disabled.
7606
- */
7607
- const isTelemetryDisabled = (configEnabled) => {
7608
- if (process.env.AISLOP_NO_TELEMETRY === "1" || process.env.DO_NOT_TRACK === "1") return true;
7609
- if (process.env.CI === "true" || process.env.CI === "1") return true;
7610
- if (configEnabled === false) return true;
7611
- return false;
7612
- };
7613
- const getScoreBucket = (score) => {
7614
- if (score >= 75) return "75-100";
7615
- if (score >= 50) return "50-75";
7616
- if (score >= 25) return "25-50";
7617
- return "0-25";
7618
- };
7619
- const getAnonymousId = () => {
7620
- const raw = `${os.hostname()}-${os.platform()}-${os.arch()}`;
7621
- let hash = 5381;
7622
- for (let i = 0; i < raw.length; i++) hash = hash * 33 ^ raw.charCodeAt(i);
7623
- return `aislop_${(hash >>> 0).toString(36)}`;
7624
- };
7625
- /** Pending telemetry request — kept alive so Node doesn't exit before it completes. */
7626
- let pendingRequest = null;
7627
- const trackEvent = (event) => {
7628
- const payload = {
7629
- api_key: POSTHOG_KEY,
7630
- event: `cli_${event.command}`,
7631
- distinct_id: getAnonymousId(),
7632
- properties: {
7633
- version: APP_VERSION,
7634
- node_version: process.version,
7635
- os: os.platform(),
7636
- arch: os.arch(),
7637
- languages: event.languages,
7638
- score_bucket: event.scoreBucket,
7639
- engine_issues: event.engineIssues,
7640
- engine_timings: event.engineTimings,
7641
- elapsed_ms: event.elapsedMs,
7642
- file_count: event.fileCount,
7643
- fix_steps: event.fixSteps,
7644
- fix_resolved: event.fixResolved
7645
- },
7646
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
7647
- };
7648
- pendingRequest = fetch(`${POSTHOG_HOST}/capture/`, {
7649
- method: "POST",
7650
- headers: { "Content-Type": "application/json" },
7651
- body: JSON.stringify(payload),
7652
- signal: AbortSignal.timeout(3e3)
7653
- }).then(() => {}).catch(() => {});
7654
- };
7655
- const flushTelemetry = async () => {
7656
- if (pendingRequest) {
7657
- await pendingRequest;
7658
- pendingRequest = null;
7659
- }
7660
- };
7661
-
7662
8007
  //#endregion
7663
8008
  //#region src/commands/scan.ts
7664
8009
  const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
@@ -7716,7 +8061,6 @@ const buildScanRender = (input) => {
7716
8061
  }, deps)}`;
7717
8062
  };
7718
8063
  const scanCommand = async (directory, config, options) => {
7719
- const startTime = performance.now();
7720
8064
  const resolvedDir = path.resolve(directory);
7721
8065
  if (!fs.existsSync(resolvedDir)) {
7722
8066
  const msg = `Path does not exist: ${resolvedDir}`;
@@ -7730,9 +8074,18 @@ const scanCommand = async (directory, config, options) => {
7730
8074
  else log.error(msg);
7731
8075
  return { exitCode: 1 };
7732
8076
  }
8077
+ const projectInfo = await discoverProject(resolvedDir);
8078
+ return withCommandLifecycle({
8079
+ command: options.command ?? "scan",
8080
+ config: config.telemetry,
8081
+ languages: projectInfo.languages,
8082
+ fileCount: projectInfo.sourceFileCount
8083
+ }, () => runScanBody(resolvedDir, config, options, projectInfo));
8084
+ };
8085
+ const runScanBody = async (resolvedDir, config, options, projectInfo) => {
8086
+ const startTime = performance.now();
7733
8087
  const showHeader = options.showHeader !== false;
7734
8088
  const useLiveProgress = !options.json && shouldUseSpinner();
7735
- const projectInfo = await discoverProject(resolvedDir);
7736
8089
  let files;
7737
8090
  if (options.staged) {
7738
8091
  files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], config.exclude);
@@ -7799,28 +8152,27 @@ const scanCommand = async (directory, config, options) => {
7799
8152
  const elapsedMs = performance.now() - startTime;
7800
8153
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
7801
8154
  const exitCode = allDiagnostics.some((d) => d.severity === "error") || scoreResult.score < config.ci.failBelow ? 1 : 0;
7802
- if (!isTelemetryDisabled(config.telemetry?.enabled)) {
7803
- const engineIssues = {};
7804
- const engineTimings = {};
7805
- for (const r of results) {
7806
- engineIssues[r.engine] = r.diagnostics.length;
7807
- engineTimings[r.engine] = Math.round(r.elapsed);
7808
- }
7809
- trackEvent({
7810
- command: options.command ?? "scan",
7811
- languages: projectInfo.languages,
7812
- scoreBucket: getScoreBucket(scoreResult.score),
7813
- engineIssues,
7814
- engineTimings,
7815
- elapsedMs: Math.round(elapsedMs),
7816
- fileCount: projectInfo.sourceFileCount
7817
- });
7818
- }
8155
+ const engineIssues = {};
8156
+ const engineTimings = {};
8157
+ for (const r of results) {
8158
+ engineIssues[r.engine] = r.diagnostics.length;
8159
+ engineTimings[r.engine] = Math.round(r.elapsed);
8160
+ }
8161
+ const completion = {
8162
+ exitCode,
8163
+ score: scoreResult.score,
8164
+ findingCount: allDiagnostics.length,
8165
+ errorCount: allDiagnostics.filter((d) => d.severity === "error").length,
8166
+ warningCount: allDiagnostics.filter((d) => d.severity === "warning").length,
8167
+ fixableCount: allDiagnostics.filter((d) => d.fixable).length,
8168
+ engineIssues,
8169
+ engineTimings
8170
+ };
7819
8171
  if (options.json) {
7820
- const { buildJsonOutput } = await import("./json-BbMwrgyd.js");
8172
+ const { buildJsonOutput } = await import("./json-OIzja7OM.js");
7821
8173
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
7822
8174
  console.log(JSON.stringify(jsonOut, null, 2));
7823
- return { exitCode };
8175
+ return completion;
7824
8176
  }
7825
8177
  const projectName = projectInfo.projectName ?? "project";
7826
8178
  const language = projectInfo.languages[0] ?? "unknown";
@@ -7837,7 +8189,7 @@ const scanCommand = async (directory, config, options) => {
7837
8189
  includeHeader: showHeader,
7838
8190
  printBrand: options.printBrand
7839
8191
  }));
7840
- return { exitCode };
8192
+ return completion;
7841
8193
  };
7842
8194
 
7843
8195
  //#endregion
@@ -9785,15 +10137,23 @@ const fixCommand = async (directory, config, options = {
9785
10137
  verbose: false,
9786
10138
  showHeader: true
9787
10139
  }) => {
9788
- const startTime = performance.now();
9789
10140
  const resolvedDir = path.resolve(directory);
9790
10141
  if (!fs.existsSync(resolvedDir) || !fs.statSync(resolvedDir).isDirectory()) {
9791
10142
  const msg = !fs.existsSync(resolvedDir) ? `Path does not exist: ${resolvedDir}` : `Not a directory: ${resolvedDir}`;
9792
10143
  log.error(msg);
9793
10144
  return;
9794
10145
  }
9795
- const showHeader = options.showHeader !== false;
9796
10146
  const projectInfo = await discoverProject(resolvedDir);
10147
+ await withCommandLifecycle({
10148
+ command: "fix",
10149
+ config: config.telemetry,
10150
+ languages: projectInfo.languages,
10151
+ fileCount: projectInfo.sourceFileCount
10152
+ }, () => runFixBody(resolvedDir, config, options, projectInfo));
10153
+ };
10154
+ const runFixBody = async (resolvedDir, config, options, projectInfo) => {
10155
+ const startTime = performance.now();
10156
+ const showHeader = options.showHeader !== false;
9797
10157
  const projectName = projectInfo.projectName ?? "project";
9798
10158
  if (showHeader) process.stdout.write(renderHeader({
9799
10159
  version: APP_VERSION,
@@ -9830,12 +10190,6 @@ const fixCommand = async (directory, config, options = {
9830
10190
  await runFormattingStep(pipelineDeps);
9831
10191
  await runForceSteps(pipelineDeps);
9832
10192
  const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
9833
- if (!isTelemetryDisabled(config.telemetry?.enabled)) trackEvent({
9834
- command: "fix",
9835
- languages: projectInfo.languages,
9836
- fixSteps: steps.length,
9837
- fixResolved: totalResolved
9838
- });
9839
10193
  const configDir = findConfigDir(resolvedDir);
9840
10194
  const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
9841
10195
  const engineConfig = {
@@ -9858,7 +10212,9 @@ const fixCommand = async (directory, config, options = {
9858
10212
  });
9859
10213
  const allDiagnostics = scanResults.flatMap((r) => r.diagnostics);
9860
10214
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
9861
- const remaining = allDiagnostics.filter((d) => d.severity === "error").length + allDiagnostics.filter((d) => d.severity === "warning").length;
10215
+ const errors = allDiagnostics.filter((d) => d.severity === "error").length;
10216
+ const warnings = allDiagnostics.filter((d) => d.severity === "warning").length;
10217
+ const remaining = errors + warnings;
9862
10218
  if (steps.length === 0) rail.complete({
9863
10219
  status: "skipped",
9864
10220
  label: "No applicable auto-fixers found"
@@ -9887,12 +10243,31 @@ const fixCommand = async (directory, config, options = {
9887
10243
  }
9888
10244
  if (options.agent) {
9889
10245
  launchAgent(options.agent, resolvedDir, allDiagnostics, scoreResult.score);
9890
- return;
10246
+ return {
10247
+ exitCode: 0,
10248
+ score: scoreResult.score,
10249
+ fixSteps: steps.length,
10250
+ fixResolved: totalResolved
10251
+ };
9891
10252
  }
9892
10253
  if (options.prompt) {
9893
10254
  printPrompt(resolvedDir, allDiagnostics, scoreResult.score);
9894
- return;
10255
+ return {
10256
+ exitCode: 0,
10257
+ score: scoreResult.score,
10258
+ fixSteps: steps.length,
10259
+ fixResolved: totalResolved
10260
+ };
9895
10261
  }
10262
+ return {
10263
+ exitCode: 0,
10264
+ score: scoreResult.score,
10265
+ findingCount: allDiagnostics.length,
10266
+ errorCount: errors,
10267
+ warningCount: warnings,
10268
+ fixSteps: steps.length,
10269
+ fixResolved: totalResolved
10270
+ };
9896
10271
  };
9897
10272
 
9898
10273
  //#endregion
@@ -10375,6 +10750,13 @@ const interactiveCommand = async (directory, config) => {
10375
10750
  //#region src/cli.ts
10376
10751
  process.on("SIGINT", () => process.exit(0));
10377
10752
  process.on("SIGTERM", () => process.exit(0));
10753
+ const fireInstalledOnce = () => {
10754
+ if (isTelemetryDisabled(loadConfig(process.cwd()).telemetry)) return;
10755
+ if (ensureInstallId(resolveInstallIdPath()).created) track({
10756
+ event: "cli_installed",
10757
+ config: loadConfig(process.cwd()).telemetry
10758
+ });
10759
+ };
10378
10760
  const excludeParser = (value, previous = []) => {
10379
10761
  const parts = value.split(",").map((v) => v.trim()).filter(Boolean);
10380
10762
  return [...previous, ...parts];
@@ -10523,10 +10905,22 @@ fixProgram.action(async (directory = ".", _flags, command) => {
10523
10905
  });
10524
10906
  });
10525
10907
  program.command("init [directory]").description("Initialize aislop config in project").action(async (directory = ".") => {
10526
- await initCommand(directory);
10908
+ await withCommandLifecycle({
10909
+ command: "init",
10910
+ config: loadConfig(directory).telemetry
10911
+ }, async () => {
10912
+ await initCommand(directory);
10913
+ return { exitCode: 0 };
10914
+ });
10527
10915
  });
10528
10916
  program.command("doctor [directory]").description("Check installed tools and environment").action(async (directory = ".") => {
10529
- await doctorCommand(directory);
10917
+ await withCommandLifecycle({
10918
+ command: "doctor",
10919
+ config: loadConfig(directory).telemetry
10920
+ }, async () => {
10921
+ await doctorCommand(directory);
10922
+ return { exitCode: 0 };
10923
+ });
10530
10924
  });
10531
10925
  program.command("ci [directory]").description("CI-friendly JSON output with exit codes").option("--human", "render the human-friendly scan design instead of JSON").action(async (directory = ".", _flags, command) => {
10532
10926
  const flags = command.optsWithGlobals();
@@ -10537,16 +10931,28 @@ program.command("ci [directory]").description("CI-friendly JSON output with exit
10537
10931
  }
10538
10932
  });
10539
10933
  program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
10540
- await rulesCommand(directory);
10934
+ await withCommandLifecycle({
10935
+ command: "rules",
10936
+ config: loadConfig(directory).telemetry
10937
+ }, async () => {
10938
+ await rulesCommand(directory);
10939
+ return { exitCode: 0 };
10940
+ });
10541
10941
  });
10542
10942
  program.command("badge [directory]").description("Print the public score badge URL + README markdown for this repo").option("--owner <owner>", "GitHub owner (auto-detected from git remote if omitted)").option("--repo <repo>", "GitHub repo name (auto-detected from git remote if omitted)").option("--json", "emit machine-readable JSON instead of the rendered output").action(async (directory = ".", _flags, command) => {
10543
10943
  const flags = command.optsWithGlobals();
10544
10944
  try {
10545
- await badgeCommand({
10546
- directory,
10547
- owner: flags.owner,
10548
- repo: flags.repo,
10549
- json: Boolean(flags.json)
10945
+ await withCommandLifecycle({
10946
+ command: "badge",
10947
+ config: loadConfig(directory).telemetry
10948
+ }, async () => {
10949
+ await badgeCommand({
10950
+ directory,
10951
+ owner: flags.owner,
10952
+ repo: flags.repo,
10953
+ json: Boolean(flags.json)
10954
+ });
10955
+ return { exitCode: 0 };
10550
10956
  });
10551
10957
  } catch (err) {
10552
10958
  process.stderr.write(`${err?.message ?? "Failed to print badge"}\n`);
@@ -10555,10 +10961,11 @@ program.command("badge [directory]").description("Print the public score badge U
10555
10961
  });
10556
10962
  registerHookCommand(program);
10557
10963
  const main = async () => {
10964
+ fireInstalledOnce();
10558
10965
  await program.parseAsync();
10559
10966
  await flushTelemetry();
10560
10967
  };
10561
10968
  main();
10562
10969
 
10563
10970
  //#endregion
10564
- export { ENGINE_INFO as n, runSubprocess as r, APP_VERSION as t };
10971
+ export { runSubprocess as n, APP_VERSION as r, ENGINE_INFO as t };