threadlines 0.2.13 → 0.2.14

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.
@@ -47,7 +47,6 @@ const gitlab_1 = require("../git/gitlab");
47
47
  const bitbucket_1 = require("../git/bitbucket");
48
48
  const vercel_1 = require("../git/vercel");
49
49
  const local_1 = require("../git/local");
50
- const diff_1 = require("../git/diff");
51
50
  const config_file_1 = require("../utils/config-file");
52
51
  const logger_1 = require("../utils/logger");
53
52
  const fs = __importStar(require("fs"));
@@ -68,8 +67,11 @@ async function getContextForEnvironment(environment, repoRoot, commitSha) {
68
67
  return (0, bitbucket_1.getBitbucketContext)(repoRoot);
69
68
  case 'vercel':
70
69
  return (0, vercel_1.getVercelContext)(repoRoot);
71
- default:
70
+ case 'local':
72
71
  return (0, local_1.getLocalContext)(repoRoot, commitSha);
72
+ default:
73
+ // TypeScript exhaustiveness check - should never happen
74
+ throw new Error(`Unrecognized environment: ${environment}`);
73
75
  }
74
76
  }
75
77
  // Get CLI version from package.json
@@ -93,18 +95,20 @@ async function checkCommand(options) {
93
95
  }
94
96
  gitRoot = (await git.revparse(['--show-toplevel'])).trim();
95
97
  }
96
- catch {
97
- logger_1.logger.error('Failed to get git root. Make sure you are in a git repository.');
98
+ catch (error) {
99
+ const message = error instanceof Error ? error.message : String(error);
100
+ logger_1.logger.error(`Failed to get git root: ${message}`);
98
101
  process.exit(1);
99
102
  }
100
103
  // Pre-flight check: Validate ALL required environment variables at once
101
104
  const apiKey = (0, config_1.getThreadlineApiKey)();
102
105
  const account = (0, config_1.getThreadlineAccount)();
103
106
  const missingVars = [];
104
- // Check for undefined, empty string, or literal unexpanded variable (GitLab keeps "$VAR" literal)
105
- if (!apiKey || apiKey.startsWith('$'))
107
+ // Check for undefined, empty string, or literal unexpanded variable
108
+ // GitLab CI keeps variables as literal "$VAR" if not defined in CI/CD settings
109
+ if (!apiKey || apiKey === '$THREADLINE_API_KEY')
106
110
  missingVars.push('THREADLINE_API_KEY');
107
- if (!account || account.startsWith('$'))
111
+ if (!account || account === '$THREADLINE_ACCOUNT')
108
112
  missingVars.push('THREADLINE_ACCOUNT');
109
113
  if (missingVars.length > 0) {
110
114
  logger_1.logger.error('Missing required environment variables:');
@@ -134,189 +138,171 @@ async function checkCommand(options) {
134
138
  console.log(chalk_1.default.gray('Get your credentials at: https://devthreadline.com/settings'));
135
139
  process.exit(1);
136
140
  }
137
- try {
138
- // 1. Find and validate threadlines
139
- logger_1.logger.info('Finding threadlines...');
140
- const threadlines = await (0, experts_1.findThreadlines)(cwd, gitRoot);
141
- console.log(chalk_1.default.green(`✓ Found ${threadlines.length} threadline(s)\n`));
142
- if (threadlines.length === 0) {
143
- console.log(chalk_1.default.yellow('⚠️ No valid threadlines found.'));
144
- console.log(chalk_1.default.gray(' Run `npx threadlines init` to create your first threadline.'));
145
- process.exit(0);
146
- }
147
- // 2. Detect environment and context
148
- const environment = (0, environment_1.detectEnvironment)();
149
- let gitDiff;
150
- let repoName;
151
- let branchName;
152
- let reviewContext;
153
- let metadata = {};
154
- // Check for explicit flags
155
- const explicitFlags = [options.commit, options.file, options.folder, options.files].filter(Boolean);
156
- // Validate mutually exclusive flags
157
- if (explicitFlags.length > 1) {
158
- logger_1.logger.error('Only one review option can be specified at a time');
159
- console.log(chalk_1.default.gray(' Options: --commit, --file, --folder, --files'));
160
- process.exit(1);
141
+ // 1. Find and validate threadlines
142
+ logger_1.logger.info('Finding threadlines...');
143
+ const threadlines = await (0, experts_1.findThreadlines)(cwd, gitRoot);
144
+ console.log(chalk_1.default.green(`✓ Found ${threadlines.length} threadline(s)\n`));
145
+ if (threadlines.length === 0) {
146
+ console.log(chalk_1.default.yellow('⚠️ No valid threadlines found.'));
147
+ console.log(chalk_1.default.gray(' Run `npx threadlines init` to create your first threadline.'));
148
+ process.exit(0);
149
+ }
150
+ // 2. Detect environment and context
151
+ const environment = (0, environment_1.detectEnvironment)();
152
+ let gitDiff;
153
+ let repoName;
154
+ let branchName;
155
+ let reviewContext;
156
+ let metadata = {};
157
+ // Check for explicit flags
158
+ const explicitFlags = [options.commit, options.file, options.folder, options.files].filter(Boolean);
159
+ // Validate mutually exclusive flags
160
+ if (explicitFlags.length > 1) {
161
+ logger_1.logger.error('Only one review option can be specified at a time');
162
+ console.log(chalk_1.default.gray(' Options: --commit, --file, --folder, --files'));
163
+ process.exit(1);
164
+ }
165
+ // CI environments: auto-detect only, flags are ignored with warning
166
+ // Local: full flag support for developer flexibility
167
+ if ((0, environment_1.isCIEnvironment)(environment)) {
168
+ // Warn if flags are passed in CI - they're meant for local development
169
+ if (explicitFlags.length > 0) {
170
+ const flagName = options.commit ? '--commit' :
171
+ options.file ? '--file' :
172
+ options.folder ? '--folder' : '--files';
173
+ logger_1.logger.warn(`${flagName} flag ignored in CI environment. Using auto-detection.`);
161
174
  }
162
- // CI environments: auto-detect only, flags are ignored with warning
163
- // Local: full flag support for developer flexibility
164
- if ((0, environment_1.isCIEnvironment)(environment)) {
165
- // Warn if flags are passed in CI - they're meant for local development
166
- if (explicitFlags.length > 0) {
167
- const flagName = options.commit ? '--commit' :
168
- options.file ? '--file' :
169
- options.folder ? '--folder' : '--files';
170
- logger_1.logger.warn(`${flagName} flag ignored in CI environment. Using auto-detection.`);
171
- }
172
- // CI auto-detect: use environment-specific context
173
- const envNames = {
174
- vercel: 'Vercel',
175
- github: 'GitHub Actions',
176
- gitlab: 'GitLab CI',
177
- bitbucket: 'Bitbucket Pipelines'
178
- };
179
- logger_1.logger.info(`Collecting git context for ${envNames[environment]}...`);
180
- const envContext = await getContextForEnvironment(environment, repoRoot);
181
- gitDiff = envContext.diff;
182
- repoName = envContext.repoName;
183
- branchName = envContext.branchName;
184
- reviewContext = envContext.reviewContext; // Get from CI context
185
- metadata = {
186
- commitSha: envContext.commitSha,
187
- commitMessage: envContext.commitMessage,
188
- commitAuthorName: envContext.commitAuthor.name,
189
- commitAuthorEmail: envContext.commitAuthor.email,
190
- prTitle: envContext.prTitle
191
- };
175
+ // CI auto-detect: use environment-specific context
176
+ const envNames = {
177
+ vercel: 'Vercel',
178
+ github: 'GitHub Actions',
179
+ gitlab: 'GitLab CI',
180
+ bitbucket: 'Bitbucket Pipelines'
181
+ };
182
+ logger_1.logger.info(`Collecting git context for ${envNames[environment]}...`);
183
+ const envContext = await getContextForEnvironment(environment, repoRoot);
184
+ gitDiff = envContext.diff;
185
+ repoName = envContext.repoName;
186
+ branchName = envContext.branchName;
187
+ reviewContext = envContext.reviewContext; // Get from CI context
188
+ metadata = {
189
+ commitSha: envContext.commitSha,
190
+ commitMessage: envContext.commitMessage,
191
+ commitAuthorName: envContext.commitAuthor.name,
192
+ commitAuthorEmail: envContext.commitAuthor.email,
193
+ prTitle: envContext.prTitle
194
+ };
195
+ }
196
+ else {
197
+ // Local environment: all flags share the same metadata
198
+ // 1. Get context and metadata (pass commit SHA if provided)
199
+ logger_1.logger.info('Collecting local context...');
200
+ const localContext = await (0, local_1.getLocalContext)(repoRoot, options.commit);
201
+ repoName = localContext.repoName;
202
+ branchName = localContext.branchName;
203
+ metadata = {
204
+ commitSha: localContext.commitSha,
205
+ commitMessage: localContext.commitMessage,
206
+ commitAuthorName: localContext.commitAuthor.name,
207
+ commitAuthorEmail: localContext.commitAuthor.email
208
+ };
209
+ // 2. Get diff (override with specific content if flag provided)
210
+ if (options.file) {
211
+ reviewContext = 'file';
212
+ logger_1.logger.info(`Reading file: ${options.file}...`);
213
+ gitDiff = await (0, file_1.getFileContent)(repoRoot, options.file);
192
214
  }
193
- else {
194
- // Local environment: support all flags
195
- // Detect review context from flags (priority order: file > folder > files > commit > local)
196
- if (options.file) {
197
- reviewContext = 'file';
198
- }
199
- else if (options.folder) {
200
- reviewContext = 'folder';
201
- }
202
- else if (options.files && options.files.length > 0) {
203
- reviewContext = 'files';
204
- }
205
- else if (options.commit) {
206
- reviewContext = 'commit';
207
- }
208
- else {
209
- reviewContext = 'local';
210
- }
211
- if (options.file) {
212
- logger_1.logger.info(`Reading file: ${options.file}...`);
213
- gitDiff = await (0, file_1.getFileContent)(repoRoot, options.file);
214
- }
215
- else if (options.folder) {
216
- logger_1.logger.info(`Reading folder: ${options.folder}...`);
217
- gitDiff = await (0, file_1.getFolderContent)(repoRoot, options.folder);
218
- }
219
- else if (options.files && options.files.length > 0) {
220
- logger_1.logger.info(`Reading ${options.files.length} file(s)...`);
221
- gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
222
- }
223
- else if (options.commit) {
224
- logger_1.logger.info(`Collecting git changes for commit: ${options.commit}...`);
225
- gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, options.commit);
226
- // Use local context for metadata, passing commit SHA for author lookup
227
- const localContext = await (0, local_1.getLocalContext)(repoRoot, options.commit);
228
- repoName = localContext.repoName;
229
- branchName = localContext.branchName;
230
- metadata = {
231
- commitSha: localContext.commitSha,
232
- commitMessage: localContext.commitMessage,
233
- commitAuthorName: localContext.commitAuthor.name,
234
- commitAuthorEmail: localContext.commitAuthor.email
235
- };
236
- }
237
- else {
238
- // Local auto-detect: staged/unstaged changes
239
- logger_1.logger.info('Collecting git context for Local...');
240
- const localContext = await (0, local_1.getLocalContext)(repoRoot);
241
- gitDiff = localContext.diff;
242
- repoName = localContext.repoName;
243
- branchName = localContext.branchName;
244
- metadata = {
245
- commitSha: localContext.commitSha,
246
- commitMessage: localContext.commitMessage,
247
- commitAuthorName: localContext.commitAuthor.name,
248
- commitAuthorEmail: localContext.commitAuthor.email
249
- };
250
- }
215
+ else if (options.folder) {
216
+ reviewContext = 'folder';
217
+ logger_1.logger.info(`Reading folder: ${options.folder}...`);
218
+ gitDiff = await (0, file_1.getFolderContent)(repoRoot, options.folder);
251
219
  }
252
- if (gitDiff.changedFiles.length === 0) {
253
- console.error(chalk_1.default.bold('ℹ️ No changes detected.'));
254
- process.exit(0);
220
+ else if (options.files && options.files.length > 0) {
221
+ reviewContext = 'files';
222
+ logger_1.logger.info(`Reading ${options.files.length} file(s)...`);
223
+ gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
255
224
  }
256
- // Check for zero diff (files changed but no actual code changes)
257
- if (!gitDiff.diff || gitDiff.diff.trim() === '') {
258
- console.log(chalk_1.default.blue('ℹ️ No code changes detected. Diff contains zero lines added or removed.'));
259
- console.log(chalk_1.default.gray(` ${gitDiff.changedFiles.length} file(s) changed but no content modifications detected.`));
260
- console.log('');
261
- console.log(chalk_1.default.bold('Results:\n'));
262
- console.log(chalk_1.default.gray(`${threadlines.length} threadlines checked`));
263
- console.log(chalk_1.default.gray(` ${threadlines.length} not relevant`));
264
- console.log('');
265
- process.exit(0);
225
+ else {
226
+ // Default: use diff from localContext (handles commit and staged/unstaged)
227
+ reviewContext = options.commit ? 'commit' : 'local';
228
+ gitDiff = localContext.diff;
266
229
  }
267
- console.log(chalk_1.default.green(`✓ Found ${gitDiff.changedFiles.length} changed file(s)\n`));
268
- // 4. Read context files for each threadline
269
- const threadlinesWithContext = threadlines.map(threadline => {
270
- const contextContent = {};
271
- if (threadline.contextFiles) {
272
- for (const contextFile of threadline.contextFiles) {
273
- const fullPath = path.join(repoRoot, contextFile);
274
- if (fs.existsSync(fullPath)) {
230
+ }
231
+ if (gitDiff.changedFiles.length === 0) {
232
+ console.error(chalk_1.default.bold('ℹ️ No changes detected.'));
233
+ process.exit(0);
234
+ }
235
+ // Check for zero diff (files changed but no actual code changes)
236
+ if (!gitDiff.diff || gitDiff.diff.trim() === '') {
237
+ console.log(chalk_1.default.blue('ℹ️ No code changes detected. Diff contains zero lines added or removed.'));
238
+ console.log(chalk_1.default.gray(` ${gitDiff.changedFiles.length} file(s) changed but no content modifications detected.`));
239
+ console.log('');
240
+ console.log(chalk_1.default.bold('Results:\n'));
241
+ console.log(chalk_1.default.gray(`${threadlines.length} threadlines checked`));
242
+ console.log(chalk_1.default.gray(` ${threadlines.length} not relevant`));
243
+ console.log('');
244
+ process.exit(0);
245
+ }
246
+ console.log(chalk_1.default.green(`✓ Found ${gitDiff.changedFiles.length} changed file(s)\n`));
247
+ // Log the files being sent
248
+ for (const file of gitDiff.changedFiles) {
249
+ logger_1.logger.info(` → ${file}`);
250
+ }
251
+ // 4. Read context files for each threadline
252
+ const threadlinesWithContext = threadlines.map(threadline => {
253
+ const contextContent = {};
254
+ if (threadline.contextFiles) {
255
+ for (const contextFile of threadline.contextFiles) {
256
+ const fullPath = path.join(repoRoot, contextFile);
257
+ if (fs.existsSync(fullPath)) {
258
+ try {
275
259
  contextContent[contextFile] = fs.readFileSync(fullPath, 'utf-8');
276
260
  }
261
+ catch (error) {
262
+ const message = error instanceof Error ? error.message : String(error);
263
+ throw new Error(`Failed to read context file '${contextFile}' for threadline '${threadline.id}': ${message}`);
264
+ }
265
+ }
266
+ else {
267
+ throw new Error(`Context file not found for threadline '${threadline.id}': ${contextFile}`);
277
268
  }
278
269
  }
279
- return {
280
- id: threadline.id,
281
- version: threadline.version,
282
- patterns: threadline.patterns,
283
- content: threadline.content,
284
- filePath: threadline.filePath,
285
- contextFiles: threadline.contextFiles,
286
- contextContent
287
- };
288
- });
289
- // 5. Call review API
290
- logger_1.logger.info('Running threadline checks...');
291
- const client = new client_1.ReviewAPIClient(config.api_url);
292
- const response = await client.review({
293
- threadlines: threadlinesWithContext,
294
- diff: gitDiff.diff,
295
- files: gitDiff.changedFiles,
296
- apiKey: apiKey,
297
- account: account,
298
- repoName: repoName,
299
- branchName: branchName,
300
- commitSha: metadata.commitSha,
301
- commitMessage: metadata.commitMessage,
302
- commitAuthorName: metadata.commitAuthorName,
303
- commitAuthorEmail: metadata.commitAuthorEmail,
304
- prTitle: metadata.prTitle,
305
- environment: environment,
306
- cliVersion: CLI_VERSION,
307
- reviewContext: reviewContext
308
- });
309
- // 7. Display results (with filtering if --full not specified)
310
- displayResults(response, options.full || false);
311
- // Exit with appropriate code (attention or errors = failure)
312
- const hasIssues = response.results.some(r => r.status === 'attention' || r.status === 'error');
313
- process.exit(hasIssues ? 1 : 0);
314
- }
315
- catch (error) {
316
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
317
- logger_1.logger.error(errorMessage);
318
- process.exit(1);
319
- }
270
+ }
271
+ return {
272
+ id: threadline.id,
273
+ version: threadline.version,
274
+ patterns: threadline.patterns,
275
+ content: threadline.content,
276
+ filePath: threadline.filePath,
277
+ contextFiles: threadline.contextFiles,
278
+ contextContent
279
+ };
280
+ });
281
+ // 5. Call review API
282
+ logger_1.logger.info('Running threadline checks...');
283
+ const client = new client_1.ReviewAPIClient(config.api_url);
284
+ const response = await client.review({
285
+ threadlines: threadlinesWithContext,
286
+ diff: gitDiff.diff,
287
+ files: gitDiff.changedFiles,
288
+ apiKey: apiKey,
289
+ account: account,
290
+ repoName: repoName,
291
+ branchName: branchName,
292
+ commitSha: metadata.commitSha,
293
+ commitMessage: metadata.commitMessage,
294
+ commitAuthorName: metadata.commitAuthorName,
295
+ commitAuthorEmail: metadata.commitAuthorEmail,
296
+ prTitle: metadata.prTitle,
297
+ environment: environment,
298
+ cliVersion: CLI_VERSION,
299
+ reviewContext: reviewContext
300
+ });
301
+ // 7. Display results (with filtering if --full not specified)
302
+ displayResults(response, options.full || false);
303
+ // Exit with appropriate code (attention or errors = failure)
304
+ const hasIssues = response.results.some(r => r.status === 'attention' || r.status === 'error');
305
+ process.exit(hasIssues ? 1 : 0);
320
306
  }
321
307
  function displayResults(response, showFull) {
322
308
  const { results, metadata, message } = response;
@@ -429,10 +415,6 @@ function displayResults(response, showFull) {
429
415
  console.log(chalk_1.default.gray(JSON.stringify(item.error.rawResponse, null, 2).split('\n').map(line => ' ' + line).join('\n')));
430
416
  }
431
417
  }
432
- else if (item.reasoning) {
433
- // Fallback to reasoning if no error object
434
- console.log(chalk_1.default.red(` ${item.reasoning}`));
435
- }
436
418
  console.log(''); // Empty line between errors
437
419
  }
438
420
  }
package/dist/git/file.js CHANGED
@@ -56,15 +56,17 @@ async function getFileContent(repoRoot, filePath) {
56
56
  }
57
57
  // Read file content
58
58
  const content = fs.readFileSync(fullPath, 'utf-8');
59
+ // Normalize path to forward slashes for cross-platform consistency (git uses forward slashes)
60
+ const normalizedPath = filePath.replace(/\\/g, '/');
59
61
  // Create artificial diff (all lines as additions)
60
62
  const lines = content.split('\n');
61
63
  const diff = lines.map((line) => `+${line}`).join('\n');
62
- // Add diff header
63
- const diffHeader = `--- /dev/null\n+++ ${filePath}\n@@ -0,0 +1,${lines.length} @@\n`;
64
+ // Add git diff header (matches format expected by server's filterDiffByFiles)
65
+ const diffHeader = `diff --git a/${normalizedPath} b/${normalizedPath}\n--- /dev/null\n+++ b/${normalizedPath}\n@@ -0,0 +1,${lines.length} @@\n`;
64
66
  const fullDiff = diffHeader + diff;
65
67
  return {
66
68
  diff: fullDiff,
67
- changedFiles: [filePath]
69
+ changedFiles: [normalizedPath]
68
70
  };
69
71
  }
70
72
  /**
@@ -81,8 +83,8 @@ async function getFolderContent(repoRoot, folderPath) {
81
83
  if (!stats.isDirectory()) {
82
84
  throw new Error(`Path '${folderPath}' is not a folder`);
83
85
  }
84
- // Find all files recursively
85
- const pattern = path.join(fullPath, '**', '*');
86
+ // Find all files recursively (normalize to forward slashes for glob on Windows)
87
+ const pattern = path.join(fullPath, '**', '*').replace(/\\/g, '/');
86
88
  const files = await (0, glob_1.glob)(pattern, {
87
89
  cwd: repoRoot,
88
90
  absolute: false,
@@ -108,11 +110,13 @@ async function getFolderContent(repoRoot, folderPath) {
108
110
  try {
109
111
  const content = fs.readFileSync(path.resolve(repoRoot, filePath), 'utf-8');
110
112
  const lines = content.split('\n');
111
- // Create artificial diff for this file
113
+ // Normalize path to forward slashes for cross-platform consistency
114
+ const normalizedPath = filePath.replace(/\\/g, '/');
115
+ // Create artificial diff for this file (git diff format)
112
116
  const fileDiff = lines.map((line) => `+${line}`).join('\n');
113
- const diffHeader = `--- /dev/null\n+++ ${filePath}\n@@ -0,0 +1,${lines.length} @@\n`;
117
+ const diffHeader = `diff --git a/${normalizedPath} b/${normalizedPath}\n--- /dev/null\n+++ b/${normalizedPath}\n@@ -0,0 +1,${lines.length} @@\n`;
114
118
  diffs.push(diffHeader + fileDiff);
115
- changedFiles.push(filePath);
119
+ changedFiles.push(normalizedPath);
116
120
  }
117
121
  catch (error) {
118
122
  // Skip files that can't be read (permissions, etc.)
@@ -148,11 +152,13 @@ async function getMultipleFilesContent(repoRoot, filePaths) {
148
152
  // Read file content
149
153
  const content = fs.readFileSync(fullPath, 'utf-8');
150
154
  const lines = content.split('\n');
151
- // Create artificial diff for this file
155
+ // Normalize path to forward slashes for cross-platform consistency
156
+ const normalizedPath = filePath.replace(/\\/g, '/');
157
+ // Create artificial diff for this file (git diff format)
152
158
  const fileDiff = lines.map((line) => `+${line}`).join('\n');
153
- const diffHeader = `--- /dev/null\n+++ ${filePath}\n@@ -0,0 +1,${lines.length} @@\n`;
159
+ const diffHeader = `diff --git a/${normalizedPath} b/${normalizedPath}\n--- /dev/null\n+++ b/${normalizedPath}\n@@ -0,0 +1,${lines.length} @@\n`;
154
160
  diffs.push(diffHeader + fileDiff);
155
- changedFiles.push(filePath);
161
+ changedFiles.push(normalizedPath);
156
162
  }
157
163
  return {
158
164
  diff: diffs.join('\n'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadlines",
3
- "version": "0.2.13",
3
+ "version": "0.2.14",
4
4
  "description": "Threadlines CLI - AI-powered linter based on your natural language documentation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {