aislop 0.8.2 → 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}`;
@@ -1452,6 +1812,27 @@ const collectJsDeps = (rootDir, jsDeps) => {
1452
1812
  collectNestedManifests(rootDir, jsDeps);
1453
1813
  return true;
1454
1814
  };
1815
+ const TS_CONFIG_FILES = ["tsconfig.json", "jsconfig.json"];
1816
+ const buildAliasMatcher = (key) => {
1817
+ const starIdx = key.indexOf("*");
1818
+ if (starIdx === -1) return (spec) => spec === key;
1819
+ const before = key.slice(0, starIdx);
1820
+ const after = key.slice(starIdx + 1);
1821
+ return (spec) => spec.length >= before.length + after.length && spec.startsWith(before) && spec.endsWith(after);
1822
+ };
1823
+ const collectAliasMatchersFromConfig = (configPath, matchers) => {
1824
+ const opts = readJson(configPath)?.compilerOptions;
1825
+ if (!opts || typeof opts !== "object") return;
1826
+ const paths = opts.paths;
1827
+ if (!paths || typeof paths !== "object") return;
1828
+ for (const key of Object.keys(paths)) matchers.push(buildAliasMatcher(key));
1829
+ };
1830
+ const collectTsPathAliases = (rootDir) => {
1831
+ const matchers = [];
1832
+ const dirs = [rootDir, ...expandWorkspaceDirs(rootDir, readWorkspaceGlobs(rootDir, readJson(path.join(rootDir, "package.json"))))];
1833
+ for (const dir of dirs) for (const fname of TS_CONFIG_FILES) collectAliasMatchersFromConfig(path.join(dir, fname), matchers);
1834
+ return matchers;
1835
+ };
1455
1836
  const addPyDep = (pyDeps, name) => {
1456
1837
  const normalized = name.toLowerCase().replace(/_/g, "-");
1457
1838
  pyDeps.add(normalized);
@@ -1609,10 +1990,11 @@ const extractPyImports = (content) => {
1609
1990
  }
1610
1991
  return results;
1611
1992
  };
1612
- const checkJsImport = (spec, manifest) => {
1993
+ const checkJsImport = (spec, manifest, tsAliasMatchers) => {
1613
1994
  if (isJsRelativeOrAbsolute(spec)) return null;
1614
1995
  if (isJsBuiltin(spec)) return null;
1615
1996
  if (isJsVirtualModule(spec)) return null;
1997
+ if (tsAliasMatchers.some((m) => m(spec))) return null;
1616
1998
  const pkg = packageNameFromImport(spec);
1617
1999
  if (manifest.jsDeps.has(pkg)) return null;
1618
2000
  if (pkg.startsWith("@types/")) {
@@ -1633,6 +2015,7 @@ const checkPyImport = (spec, manifest) => {
1633
2015
  const detectHallucinatedImports = async (context) => {
1634
2016
  const manifest = loadManifest(context.rootDirectory);
1635
2017
  if (!manifest.hasJsManifest && !manifest.hasPyManifest) return [];
2018
+ const tsAliasMatchers = manifest.hasJsManifest ? collectTsPathAliases(context.rootDirectory) : [];
1636
2019
  const diagnostics = [];
1637
2020
  const files = getSourceFiles(context);
1638
2021
  for (const filePath of files) {
@@ -1652,7 +2035,7 @@ const detectHallucinatedImports = async (context) => {
1652
2035
  const relPath = path.relative(context.rootDirectory, filePath);
1653
2036
  const imports = isJs ? extractJsImports(content) : extractPyImports(content);
1654
2037
  for (const { spec, line } of imports) {
1655
- const hallucinated = isJs ? checkJsImport(spec, manifest) : checkPyImport(spec, manifest);
2038
+ const hallucinated = isJs ? checkJsImport(spec, manifest, tsAliasMatchers) : checkPyImport(spec, manifest);
1656
2039
  if (!hallucinated) continue;
1657
2040
  const manifestLabel = isJs ? "package.json" : "requirements.txt / pyproject.toml / Pipfile";
1658
2041
  diagnostics.push({
@@ -4613,7 +4996,7 @@ const lintEngine = {
4613
4996
  const promises = [];
4614
4997
  if (languages.includes("typescript") || languages.includes("javascript")) {
4615
4998
  promises.push(runOxlint(context));
4616
- 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)));
4617
5000
  }
4618
5001
  if (context.frameworks.includes("expo")) promises.push(Promise.resolve().then(() => expo_doctor_exports).then((mod) => mod.runExpoDoctor(context)));
4619
5002
  if (languages.includes("python") && installedTools["ruff"]) promises.push(runRuffLint(context));
@@ -5832,6 +6215,16 @@ const runClaudeHook = async (deps = {}) => {
5832
6215
  score: baseline.score,
5833
6216
  findingFingerprints: baseline.findingFingerprints
5834
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
+ });
5835
6228
  const envelope = renderClaudeOutput(JSON.stringify(feedback));
5836
6229
  write(JSON.stringify(envelope));
5837
6230
  return 0;
@@ -5963,6 +6356,15 @@ const runCursorHook = async (deps = {}) => {
5963
6356
  try {
5964
6357
  const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
5965
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
+ });
5966
6368
  const serialized = JSON.stringify(feedback);
5967
6369
  write(JSON.stringify(renderCursorOutput(serialized)));
5968
6370
  writeErr(`${serialized}\n`);
@@ -6013,6 +6415,15 @@ const runGeminiHook = async (deps = {}) => {
6013
6415
  try {
6014
6416
  const { diagnostics, score, rootDirectory } = await runScopedScan(cwd, files);
6015
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
+ });
6016
6427
  write(JSON.stringify(renderGeminiOutput(JSON.stringify(feedback))));
6017
6428
  return 0;
6018
6429
  } catch {
@@ -7027,12 +7438,18 @@ const registerInstall = (hook) => {
7027
7438
  install.action(async (positional, opts) => {
7028
7439
  const agents = await pickAgents("install", opts, positional);
7029
7440
  if (agents === null || agents.length === 0) return;
7030
- await hookInstall({
7031
- agents,
7032
- scope: resolveScope(opts),
7033
- dryRun: Boolean(opts.dryRun),
7034
- yes: Boolean(opts.yes),
7035
- 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 };
7036
7453
  });
7037
7454
  });
7038
7455
  };
@@ -7042,21 +7459,39 @@ const registerUninstall = (hook) => {
7042
7459
  uninstall.action(async (positional, opts) => {
7043
7460
  const agents = await pickAgents("uninstall", opts, positional);
7044
7461
  if (agents === null || agents.length === 0) return;
7045
- await hookUninstall({
7046
- agents,
7047
- scope: resolveScope(opts),
7048
- dryRun: Boolean(opts.dryRun),
7049
- yes: true,
7050
- 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 };
7051
7474
  });
7052
7475
  });
7053
7476
  };
7054
7477
  const registerCallbacks = (hook) => {
7055
7478
  hook.command("status").description("Show which agent hooks are installed").action(async () => {
7056
- 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
+ });
7057
7486
  });
7058
7487
  hook.command("baseline").description("Capture the current project score as the quality-gate baseline").action(async () => {
7059
- 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
+ });
7060
7495
  });
7061
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) => {
7062
7497
  await hookRun("claude", {
@@ -7569,73 +8004,6 @@ const renderCleanRun = (input, deps = {}) => {
7569
8004
  return `\n ${parts.join(` ${sep} `)}\n`;
7570
8005
  };
7571
8006
 
7572
- //#endregion
7573
- //#region src/version.ts
7574
- const APP_VERSION = "0.8.2";
7575
-
7576
- //#endregion
7577
- //#region src/utils/telemetry.ts
7578
- const POSTHOG_HOST = "https://eu.i.posthog.com";
7579
- const POSTHOG_KEY = "phc_eY2cOMFva9q24GrWeOuvuVIOhCIdjOALxeAR3ItrqbJ";
7580
- /**
7581
- * Returns true if telemetry should be disabled.
7582
- * Telemetry is opt-out: it runs unless explicitly disabled.
7583
- */
7584
- const isTelemetryDisabled = (configEnabled) => {
7585
- if (process.env.AISLOP_NO_TELEMETRY === "1" || process.env.DO_NOT_TRACK === "1") return true;
7586
- if (process.env.CI === "true" || process.env.CI === "1") return true;
7587
- if (configEnabled === false) return true;
7588
- return false;
7589
- };
7590
- const getScoreBucket = (score) => {
7591
- if (score >= 75) return "75-100";
7592
- if (score >= 50) return "50-75";
7593
- if (score >= 25) return "25-50";
7594
- return "0-25";
7595
- };
7596
- const getAnonymousId = () => {
7597
- const raw = `${os.hostname()}-${os.platform()}-${os.arch()}`;
7598
- let hash = 5381;
7599
- for (let i = 0; i < raw.length; i++) hash = hash * 33 ^ raw.charCodeAt(i);
7600
- return `aislop_${(hash >>> 0).toString(36)}`;
7601
- };
7602
- /** Pending telemetry request — kept alive so Node doesn't exit before it completes. */
7603
- let pendingRequest = null;
7604
- const trackEvent = (event) => {
7605
- const payload = {
7606
- api_key: POSTHOG_KEY,
7607
- event: `cli_${event.command}`,
7608
- distinct_id: getAnonymousId(),
7609
- properties: {
7610
- version: APP_VERSION,
7611
- node_version: process.version,
7612
- os: os.platform(),
7613
- arch: os.arch(),
7614
- languages: event.languages,
7615
- score_bucket: event.scoreBucket,
7616
- engine_issues: event.engineIssues,
7617
- engine_timings: event.engineTimings,
7618
- elapsed_ms: event.elapsedMs,
7619
- file_count: event.fileCount,
7620
- fix_steps: event.fixSteps,
7621
- fix_resolved: event.fixResolved
7622
- },
7623
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
7624
- };
7625
- pendingRequest = fetch(`${POSTHOG_HOST}/capture/`, {
7626
- method: "POST",
7627
- headers: { "Content-Type": "application/json" },
7628
- body: JSON.stringify(payload),
7629
- signal: AbortSignal.timeout(3e3)
7630
- }).then(() => {}).catch(() => {});
7631
- };
7632
- const flushTelemetry = async () => {
7633
- if (pendingRequest) {
7634
- await pendingRequest;
7635
- pendingRequest = null;
7636
- }
7637
- };
7638
-
7639
8007
  //#endregion
7640
8008
  //#region src/commands/scan.ts
7641
8009
  const shouldUseSpinner = () => Boolean(process.stderr.isTTY) && process.env.CI !== "true" && process.env.CI !== "1";
@@ -7693,7 +8061,6 @@ const buildScanRender = (input) => {
7693
8061
  }, deps)}`;
7694
8062
  };
7695
8063
  const scanCommand = async (directory, config, options) => {
7696
- const startTime = performance.now();
7697
8064
  const resolvedDir = path.resolve(directory);
7698
8065
  if (!fs.existsSync(resolvedDir)) {
7699
8066
  const msg = `Path does not exist: ${resolvedDir}`;
@@ -7707,9 +8074,18 @@ const scanCommand = async (directory, config, options) => {
7707
8074
  else log.error(msg);
7708
8075
  return { exitCode: 1 };
7709
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();
7710
8087
  const showHeader = options.showHeader !== false;
7711
8088
  const useLiveProgress = !options.json && shouldUseSpinner();
7712
- const projectInfo = await discoverProject(resolvedDir);
7713
8089
  let files;
7714
8090
  if (options.staged) {
7715
8091
  files = filterProjectFiles(resolvedDir, getStagedFiles(resolvedDir), [], config.exclude);
@@ -7776,28 +8152,27 @@ const scanCommand = async (directory, config, options) => {
7776
8152
  const elapsedMs = performance.now() - startTime;
7777
8153
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
7778
8154
  const exitCode = allDiagnostics.some((d) => d.severity === "error") || scoreResult.score < config.ci.failBelow ? 1 : 0;
7779
- if (!isTelemetryDisabled(config.telemetry?.enabled)) {
7780
- const engineIssues = {};
7781
- const engineTimings = {};
7782
- for (const r of results) {
7783
- engineIssues[r.engine] = r.diagnostics.length;
7784
- engineTimings[r.engine] = Math.round(r.elapsed);
7785
- }
7786
- trackEvent({
7787
- command: options.command ?? "scan",
7788
- languages: projectInfo.languages,
7789
- scoreBucket: getScoreBucket(scoreResult.score),
7790
- engineIssues,
7791
- engineTimings,
7792
- elapsedMs: Math.round(elapsedMs),
7793
- fileCount: projectInfo.sourceFileCount
7794
- });
7795
- }
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
+ };
7796
8171
  if (options.json) {
7797
- const { buildJsonOutput } = await import("./json-BbMwrgyd.js");
8172
+ const { buildJsonOutput } = await import("./json-OIzja7OM.js");
7798
8173
  const jsonOut = buildJsonOutput(results, scoreResult, projectInfo.sourceFileCount, elapsedMs);
7799
8174
  console.log(JSON.stringify(jsonOut, null, 2));
7800
- return { exitCode };
8175
+ return completion;
7801
8176
  }
7802
8177
  const projectName = projectInfo.projectName ?? "project";
7803
8178
  const language = projectInfo.languages[0] ?? "unknown";
@@ -7814,7 +8189,7 @@ const scanCommand = async (directory, config, options) => {
7814
8189
  includeHeader: showHeader,
7815
8190
  printBrand: options.printBrand
7816
8191
  }));
7817
- return { exitCode };
8192
+ return completion;
7818
8193
  };
7819
8194
 
7820
8195
  //#endregion
@@ -9762,15 +10137,23 @@ const fixCommand = async (directory, config, options = {
9762
10137
  verbose: false,
9763
10138
  showHeader: true
9764
10139
  }) => {
9765
- const startTime = performance.now();
9766
10140
  const resolvedDir = path.resolve(directory);
9767
10141
  if (!fs.existsSync(resolvedDir) || !fs.statSync(resolvedDir).isDirectory()) {
9768
10142
  const msg = !fs.existsSync(resolvedDir) ? `Path does not exist: ${resolvedDir}` : `Not a directory: ${resolvedDir}`;
9769
10143
  log.error(msg);
9770
10144
  return;
9771
10145
  }
9772
- const showHeader = options.showHeader !== false;
9773
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;
9774
10157
  const projectName = projectInfo.projectName ?? "project";
9775
10158
  if (showHeader) process.stdout.write(renderHeader({
9776
10159
  version: APP_VERSION,
@@ -9807,12 +10190,6 @@ const fixCommand = async (directory, config, options = {
9807
10190
  await runFormattingStep(pipelineDeps);
9808
10191
  await runForceSteps(pipelineDeps);
9809
10192
  const totalResolved = steps.reduce((sum, s) => sum + s.resolvedIssues, 0);
9810
- if (!isTelemetryDisabled(config.telemetry?.enabled)) trackEvent({
9811
- command: "fix",
9812
- languages: projectInfo.languages,
9813
- fixSteps: steps.length,
9814
- fixResolved: totalResolved
9815
- });
9816
10193
  const configDir = findConfigDir(resolvedDir);
9817
10194
  const rulesPath = configDir ? path.join(configDir, RULES_FILE) : void 0;
9818
10195
  const engineConfig = {
@@ -9835,7 +10212,9 @@ const fixCommand = async (directory, config, options = {
9835
10212
  });
9836
10213
  const allDiagnostics = scanResults.flatMap((r) => r.diagnostics);
9837
10214
  const scoreResult = calculateScore(allDiagnostics, config.scoring.weights, config.scoring.thresholds, projectInfo.sourceFileCount, config.scoring.smoothing);
9838
- 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;
9839
10218
  if (steps.length === 0) rail.complete({
9840
10219
  status: "skipped",
9841
10220
  label: "No applicable auto-fixers found"
@@ -9864,12 +10243,31 @@ const fixCommand = async (directory, config, options = {
9864
10243
  }
9865
10244
  if (options.agent) {
9866
10245
  launchAgent(options.agent, resolvedDir, allDiagnostics, scoreResult.score);
9867
- return;
10246
+ return {
10247
+ exitCode: 0,
10248
+ score: scoreResult.score,
10249
+ fixSteps: steps.length,
10250
+ fixResolved: totalResolved
10251
+ };
9868
10252
  }
9869
10253
  if (options.prompt) {
9870
10254
  printPrompt(resolvedDir, allDiagnostics, scoreResult.score);
9871
- return;
10255
+ return {
10256
+ exitCode: 0,
10257
+ score: scoreResult.score,
10258
+ fixSteps: steps.length,
10259
+ fixResolved: totalResolved
10260
+ };
9872
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
+ };
9873
10271
  };
9874
10272
 
9875
10273
  //#endregion
@@ -10352,6 +10750,13 @@ const interactiveCommand = async (directory, config) => {
10352
10750
  //#region src/cli.ts
10353
10751
  process.on("SIGINT", () => process.exit(0));
10354
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
+ };
10355
10760
  const excludeParser = (value, previous = []) => {
10356
10761
  const parts = value.split(",").map((v) => v.trim()).filter(Boolean);
10357
10762
  return [...previous, ...parts];
@@ -10500,10 +10905,22 @@ fixProgram.action(async (directory = ".", _flags, command) => {
10500
10905
  });
10501
10906
  });
10502
10907
  program.command("init [directory]").description("Initialize aislop config in project").action(async (directory = ".") => {
10503
- 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
+ });
10504
10915
  });
10505
10916
  program.command("doctor [directory]").description("Check installed tools and environment").action(async (directory = ".") => {
10506
- 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
+ });
10507
10924
  });
10508
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) => {
10509
10926
  const flags = command.optsWithGlobals();
@@ -10514,16 +10931,28 @@ program.command("ci [directory]").description("CI-friendly JSON output with exit
10514
10931
  }
10515
10932
  });
10516
10933
  program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
10517
- 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
+ });
10518
10941
  });
10519
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) => {
10520
10943
  const flags = command.optsWithGlobals();
10521
10944
  try {
10522
- await badgeCommand({
10523
- directory,
10524
- owner: flags.owner,
10525
- repo: flags.repo,
10526
- 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 };
10527
10956
  });
10528
10957
  } catch (err) {
10529
10958
  process.stderr.write(`${err?.message ?? "Failed to print badge"}\n`);
@@ -10532,10 +10961,11 @@ program.command("badge [directory]").description("Print the public score badge U
10532
10961
  });
10533
10962
  registerHookCommand(program);
10534
10963
  const main = async () => {
10964
+ fireInstalledOnce();
10535
10965
  await program.parseAsync();
10536
10966
  await flushTelemetry();
10537
10967
  };
10538
10968
  main();
10539
10969
 
10540
10970
  //#endregion
10541
- 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 };