threadlines 0.1.32 → 0.1.33

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.
@@ -181,6 +181,14 @@ async function checkCommand(options) {
181
181
  }
182
182
  // 3. Collect metadata (commit SHA, commit message, PR title)
183
183
  const metadata = await (0, metadata_1.collectMetadata)(context, environment, repoRoot);
184
+ // Debug: Log collected metadata
185
+ console.log(`[DEBUG] Metadata after collectMetadata: ${JSON.stringify(metadata)}`);
186
+ if (metadata.commitAuthorName || metadata.commitAuthorEmail) {
187
+ console.log(chalk_1.default.gray(` Author: ${metadata.commitAuthorName} <${metadata.commitAuthorEmail}>`));
188
+ }
189
+ else {
190
+ console.log(`[DEBUG] No author info - commitAuthorName=${metadata.commitAuthorName}, commitAuthorEmail=${metadata.commitAuthorEmail}`);
191
+ }
184
192
  if (gitDiff.changedFiles.length === 0) {
185
193
  console.error(chalk_1.default.red('❌ Error: No changes detected.'));
186
194
  console.error(chalk_1.default.red(' Threadline check requires code changes to analyze.'));
@@ -225,7 +233,7 @@ async function checkCommand(options) {
225
233
  // 6. Call review API
226
234
  console.log(chalk_1.default.gray('🤖 Running threadline checks...'));
227
235
  const client = new client_1.ReviewAPIClient(apiUrl);
228
- const response = await client.review({
236
+ const reviewRequest = {
229
237
  threadlines: threadlinesWithContext,
230
238
  diff: gitDiff.diff,
231
239
  files: gitDiff.changedFiles,
@@ -235,9 +243,13 @@ async function checkCommand(options) {
235
243
  branchName: branchName,
236
244
  commitSha: metadata.commitSha,
237
245
  commitMessage: metadata.commitMessage,
246
+ commitAuthorName: metadata.commitAuthorName,
247
+ commitAuthorEmail: metadata.commitAuthorEmail,
238
248
  prTitle: metadata.prTitle,
239
249
  environment: environment
240
- });
250
+ };
251
+ console.log(`[DEBUG] Sending to API - commitAuthorName: ${reviewRequest.commitAuthorName}, commitAuthorEmail: ${reviewRequest.commitAuthorEmail}`);
252
+ const response = await client.review(reviewRequest);
241
253
  // 7. Display results (with filtering if --full not specified)
242
254
  displayResults(response, options.full || false);
243
255
  // Exit with appropriate code
@@ -260,38 +272,68 @@ function displayResults(response, showFull) {
260
272
  if (message) {
261
273
  console.log('\n' + chalk_1.default.blue('â„šī¸ ' + message));
262
274
  }
263
- console.log('\n' + chalk_1.default.bold('Results:\n'));
264
- console.log(chalk_1.default.gray(`${metadata.totalThreadlines} threadlines checked`));
265
275
  const notRelevant = results.filter((r) => r.status === 'not_relevant').length;
266
276
  const compliant = results.filter((r) => r.status === 'compliant').length;
267
277
  const attention = results.filter((r) => r.status === 'attention').length;
268
- if (showFull) {
269
- // Show all results when --full flag is used
270
- if (notRelevant > 0) {
271
- console.log(chalk_1.default.gray(` ${notRelevant} not relevant`));
272
- }
273
- if (compliant > 0) {
274
- console.log(chalk_1.default.green(` ${compliant} compliant`));
275
- }
276
- if (attention > 0) {
277
- console.log(chalk_1.default.yellow(` ${attention} attention`));
278
- }
278
+ const attentionItems = filteredResults.filter((r) => r.status === 'attention');
279
+ // Build summary parts
280
+ const summaryParts = [];
281
+ if (notRelevant > 0) {
282
+ summaryParts.push(`${notRelevant} not relevant`);
279
283
  }
280
- else {
281
- // Default: only show attention items
282
- if (attention > 0) {
283
- console.log(chalk_1.default.yellow(` ${attention} attention`));
284
- }
284
+ if (compliant > 0) {
285
+ summaryParts.push(`${compliant} compliant`);
286
+ }
287
+ if (attention > 0) {
288
+ summaryParts.push(`${attention} attention`);
285
289
  }
286
290
  if (metadata.timedOut > 0) {
287
- console.log(chalk_1.default.yellow(` ${metadata.timedOut} timed out`));
291
+ summaryParts.push(`${metadata.timedOut} timed out`);
288
292
  }
289
293
  if (metadata.errors > 0) {
290
- console.log(chalk_1.default.red(` ${metadata.errors} errors`));
294
+ summaryParts.push(`${metadata.errors} errors`);
295
+ }
296
+ // Display informational message if present (e.g., zero diffs)
297
+ if (message) {
298
+ console.log('\n' + chalk_1.default.blue('â„šī¸ ' + message));
299
+ }
300
+ // Show success message with breakdown if no issues
301
+ if (attention === 0 && metadata.timedOut === 0 && metadata.errors === 0) {
302
+ const summary = summaryParts.length > 0 ? ` (${summaryParts.join(', ')})` : '';
303
+ console.log('\n' + chalk_1.default.green(`✓ Threadline check passed${summary}`));
304
+ console.log(chalk_1.default.gray(` ${metadata.totalThreadlines} threadline${metadata.totalThreadlines !== 1 ? 's' : ''} checked\n`));
305
+ }
306
+ else {
307
+ // Show detailed breakdown when there are issues
308
+ console.log('\n' + chalk_1.default.bold('Results:\n'));
309
+ console.log(chalk_1.default.gray(`${metadata.totalThreadlines} threadlines checked`));
310
+ if (showFull) {
311
+ // Show all results when --full flag is used
312
+ if (notRelevant > 0) {
313
+ console.log(chalk_1.default.gray(` ${notRelevant} not relevant`));
314
+ }
315
+ if (compliant > 0) {
316
+ console.log(chalk_1.default.green(` ${compliant} compliant`));
317
+ }
318
+ if (attention > 0) {
319
+ console.log(chalk_1.default.yellow(` ${attention} attention`));
320
+ }
321
+ }
322
+ else {
323
+ // Default: only show attention items
324
+ if (attention > 0) {
325
+ console.log(chalk_1.default.yellow(` ${attention} attention`));
326
+ }
327
+ }
328
+ if (metadata.timedOut > 0) {
329
+ console.log(chalk_1.default.yellow(` ${metadata.timedOut} timed out`));
330
+ }
331
+ if (metadata.errors > 0) {
332
+ console.log(chalk_1.default.red(` ${metadata.errors} errors`));
333
+ }
334
+ console.log('');
291
335
  }
292
- console.log('');
293
336
  // Show attention items
294
- const attentionItems = filteredResults.filter((r) => r.status === 'attention');
295
337
  if (attentionItems.length > 0) {
296
338
  for (const item of attentionItems) {
297
339
  console.log(chalk_1.default.yellow(`âš ī¸ ${item.expertId}`));
@@ -308,15 +350,4 @@ function displayResults(response, showFull) {
308
350
  }
309
351
  console.log('');
310
352
  }
311
- // Show compliant items (only when --full flag is used)
312
- if (showFull) {
313
- const compliantItems = filteredResults.filter((r) => r.status === 'compliant');
314
- if (compliantItems.length > 0 && attentionItems.length === 0) {
315
- console.log(chalk_1.default.green('✓ All threadlines passed!\n'));
316
- }
317
- }
318
- else if (attentionItems.length === 0 && compliant > 0) {
319
- // Default: show success message if no attention items
320
- console.log(chalk_1.default.green('✓ All threadlines passed!\n'));
321
- }
322
353
  }
package/dist/git/diff.js CHANGED
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getBranchDiff = getBranchDiff;
7
7
  exports.getCommitMessage = getCommitMessage;
8
+ exports.getCommitAuthor = getCommitAuthor;
8
9
  exports.getCommitDiff = getCommitDiff;
9
10
  exports.getPRMRDiff = getPRMRDiff;
10
11
  const simple_git_1 = __importDefault(require("simple-git"));
@@ -197,6 +198,38 @@ async function getCommitMessage(repoRoot, sha) {
197
198
  return null;
198
199
  }
199
200
  }
201
+ /**
202
+ * Get commit author name and email for a specific commit SHA or HEAD.
203
+ *
204
+ * Uses git log to extract author information from the commit.
205
+ * Works in all environments where git is available.
206
+ */
207
+ async function getCommitAuthor(repoRoot, sha) {
208
+ const git = (0, simple_git_1.default)(repoRoot);
209
+ try {
210
+ let logResult;
211
+ if (sha) {
212
+ // Use git log for specific commit SHA
213
+ logResult = await git.log({ from: sha, to: sha, maxCount: 1 });
214
+ }
215
+ else {
216
+ // Use git log for HEAD (local environment only)
217
+ logResult = await git.log({ maxCount: 1 });
218
+ }
219
+ if (!logResult.latest) {
220
+ return null;
221
+ }
222
+ const name = logResult.latest.author_name?.trim();
223
+ const email = logResult.latest.author_email?.trim();
224
+ if (!name || !email) {
225
+ return null;
226
+ }
227
+ return { name, email };
228
+ }
229
+ catch {
230
+ return null;
231
+ }
232
+ }
200
233
  /**
201
234
  * Get diff for a specific commit
202
235
  */
@@ -10,9 +10,47 @@
10
10
  * Metadata collection is environment-specific because each CI platform
11
11
  * provides different environment variables.
12
12
  */
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ var __importDefault = (this && this.__importDefault) || function (mod) {
47
+ return (mod && mod.__esModule) ? mod : { "default": mod };
48
+ };
13
49
  Object.defineProperty(exports, "__esModule", { value: true });
14
50
  exports.collectMetadata = collectMetadata;
15
51
  const diff_1 = require("../git/diff");
52
+ const fs = __importStar(require("fs"));
53
+ const simple_git_1 = __importDefault(require("simple-git"));
16
54
  /**
17
55
  * Collects metadata for the given context and environment.
18
56
  *
@@ -23,12 +61,29 @@ async function collectMetadata(context, environment, repoRoot) {
23
61
  const metadata = {};
24
62
  // Collect commit SHA (environment-specific)
25
63
  metadata.commitSha = getCommitSha(context, environment);
26
- // Collect commit message (if we have a commit SHA)
64
+ // Collect commit message and author (environment-specific)
27
65
  if (metadata.commitSha) {
28
66
  const message = await (0, diff_1.getCommitMessage)(repoRoot, metadata.commitSha);
29
67
  if (message) {
30
68
  metadata.commitMessage = message;
31
69
  }
70
+ // Get commit author - environment-specific approach
71
+ const author = await getCommitAuthorForEnvironment(environment, repoRoot, metadata.commitSha);
72
+ if (author) {
73
+ metadata.commitAuthorName = author.name;
74
+ metadata.commitAuthorEmail = author.email;
75
+ }
76
+ }
77
+ else {
78
+ // For local environment without explicit commit SHA:
79
+ // Use git config (who will commit staged/unstaged changes)
80
+ // No fallbacks - if git config fails, the error propagates and fails the check
81
+ console.log('[DEBUG] Local environment - calling getGitConfigUser()');
82
+ const author = await getGitConfigUser(repoRoot);
83
+ console.log(`[DEBUG] getGitConfigUser returned: ${JSON.stringify(author)}`);
84
+ metadata.commitAuthorName = author.name;
85
+ metadata.commitAuthorEmail = author.email;
86
+ console.log(`[DEBUG] metadata after assignment: commitAuthorName=${metadata.commitAuthorName}, commitAuthorEmail=${metadata.commitAuthorEmail}`);
32
87
  }
33
88
  // Collect PR/MR title (environment-specific)
34
89
  metadata.prTitle = getPRTitle(context, environment);
@@ -69,6 +124,109 @@ function getCommitSha(context, environment) {
69
124
  }
70
125
  return undefined;
71
126
  }
127
+ /**
128
+ * Gets commit author information using environment-specific methods.
129
+ *
130
+ * For GitHub: Reads from GITHUB_EVENT_PATH JSON file (most reliable)
131
+ * For GitLab: Uses CI_COMMIT_AUTHOR environment variable (most reliable)
132
+ * For Local: Uses git config (for uncommitted changes, represents who will commit)
133
+ * For other environments: Uses git log command
134
+ */
135
+ async function getCommitAuthorForEnvironment(environment, repoRoot, commitSha) {
136
+ if (environment === 'github') {
137
+ // GitHub: Read from GITHUB_EVENT_PATH JSON file
138
+ // This is more reliable than git commands, especially in shallow clones
139
+ const eventPath = process.env.GITHUB_EVENT_PATH;
140
+ if (eventPath && fs.existsSync(eventPath)) {
141
+ try {
142
+ const eventData = JSON.parse(fs.readFileSync(eventPath, 'utf-8'));
143
+ // For push events, use head_commit.author
144
+ if (eventData.head_commit?.author) {
145
+ return {
146
+ name: eventData.head_commit.author.name,
147
+ email: eventData.head_commit.author.email
148
+ };
149
+ }
150
+ // For PR events, use commits[0].author (first commit in the PR)
151
+ if (eventData.commits && eventData.commits.length > 0 && eventData.commits[0].author) {
152
+ return {
153
+ name: eventData.commits[0].author.name,
154
+ email: eventData.commits[0].author.email
155
+ };
156
+ }
157
+ // Fallback to pull_request.head.commit.author for PR events
158
+ if (eventData.pull_request?.head?.commit?.author) {
159
+ return {
160
+ name: eventData.pull_request.head.commit.author.name,
161
+ email: eventData.pull_request.head.commit.author.email
162
+ };
163
+ }
164
+ }
165
+ catch (error) {
166
+ // If JSON parsing fails, fall through to git command
167
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
168
+ console.warn(`Warning: Failed to read GitHub event JSON: ${errorMessage}`);
169
+ }
170
+ }
171
+ }
172
+ if (environment === 'gitlab') {
173
+ // GitLab: Use CI_COMMIT_AUTHOR environment variable
174
+ // Format: "name <email>" (e.g., "ngrootscholten <niels.grootscholten@gmail.com>")
175
+ // This is more reliable than git commands, especially in shallow clones
176
+ const commitAuthor = process.env.CI_COMMIT_AUTHOR;
177
+ if (commitAuthor) {
178
+ // Parse "name <email>" format
179
+ const match = commitAuthor.match(/^(.+?)\s*<(.+?)>$/);
180
+ if (match) {
181
+ return {
182
+ name: match[1].trim(),
183
+ email: match[2].trim()
184
+ };
185
+ }
186
+ // If format doesn't match expected pattern, try to extract anyway
187
+ // Some GitLab versions might format differently
188
+ const parts = commitAuthor.trim().split(/\s+/);
189
+ if (parts.length >= 2) {
190
+ // Assume last part is email if it contains @
191
+ const emailIndex = parts.findIndex(p => p.includes('@'));
192
+ if (emailIndex >= 0) {
193
+ const email = parts[emailIndex].replace(/[<>]/g, '').trim();
194
+ const name = parts.slice(0, emailIndex).join(' ').trim();
195
+ if (name && email) {
196
+ return { name, email };
197
+ }
198
+ }
199
+ }
200
+ }
201
+ }
202
+ // Fallback to git command for all environments (including GitHub/GitLab if env vars unavailable)
203
+ return await (0, diff_1.getCommitAuthor)(repoRoot, commitSha);
204
+ }
205
+ /**
206
+ * Gets git user info from git config (for local uncommitted changes).
207
+ * This represents who is currently working on the changes and will commit them.
208
+ *
209
+ * No fallbacks - if git config is not set or fails, throws an error.
210
+ */
211
+ async function getGitConfigUser(repoRoot) {
212
+ const git = (0, simple_git_1.default)(repoRoot);
213
+ try {
214
+ const name = await git.getConfig('user.name');
215
+ const email = await git.getConfig('user.email');
216
+ if (!name.value || !email.value) {
217
+ throw new Error('Git config user.name or user.email is not set. ' +
218
+ 'Run: git config user.name "Your Name" && git config user.email "your.email@example.com"');
219
+ }
220
+ return {
221
+ name: name.value.trim(),
222
+ email: email.value.trim()
223
+ };
224
+ }
225
+ catch (error) {
226
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
227
+ throw new Error(`Failed to get git config user: ${errorMessage}`);
228
+ }
229
+ }
72
230
  /**
73
231
  * Extracts PR/MR title from context and environment.
74
232
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadlines",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "description": "Threadline CLI - AI-powered linter based on your natural language documentation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "directory": "packages/cli"
28
28
  },
29
29
  "scripts": {
30
- "build": "npm run check && (npm run threadlines || exit 0) && tsc",
30
+ "build": "npm run check && tsc",
31
31
  "dev": "tsc --watch",
32
32
  "start": "node dist/index.js",
33
33
  "prepublishOnly": "npm run build",