threadlines 0.2.7 → 0.2.10

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.
@@ -16,23 +16,8 @@ class ReviewAPIClient {
16
16
  });
17
17
  }
18
18
  async review(request) {
19
- try {
20
- const response = await this.client.post('/api/threadline-check', request);
21
- return response.data;
22
- }
23
- catch (error) {
24
- if (error && typeof error === 'object' && 'response' in error) {
25
- const axiosError = error;
26
- throw new Error(`API error: ${axiosError.response.status} - ${axiosError.response.data?.message || axiosError.message || 'Unknown error'}`);
27
- }
28
- else if (error && typeof error === 'object' && 'request' in error) {
29
- throw new Error(`Network error: Could not reach Threadline server at ${this.client.defaults.baseURL}`);
30
- }
31
- else {
32
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
33
- throw new Error(`Request error: ${errorMessage}`);
34
- }
35
- }
19
+ const response = await this.client.post('/api/threadline-check', request);
20
+ return response.data;
36
21
  }
37
22
  }
38
23
  exports.ReviewAPIClient = ReviewAPIClient;
@@ -48,6 +48,8 @@ const bitbucket_1 = require("../git/bitbucket");
48
48
  const vercel_1 = require("../git/vercel");
49
49
  const local_1 = require("../git/local");
50
50
  const diff_1 = require("../git/diff");
51
+ const config_file_1 = require("../utils/config-file");
52
+ const logger_1 = require("../utils/logger");
51
53
  const fs = __importStar(require("fs"));
52
54
  const path = __importStar(require("path"));
53
55
  const chalk_1 = __importDefault(require("chalk"));
@@ -77,6 +79,8 @@ const CLI_VERSION = packageJson.version;
77
79
  async function checkCommand(options) {
78
80
  const cwd = process.cwd();
79
81
  const repoRoot = cwd; // Keep for backward compatibility with rest of function
82
+ // Load configuration
83
+ const config = await (0, config_file_1.loadConfig)(cwd);
80
84
  console.log(chalk_1.default.blue(`🔍 Threadline CLI v${CLI_VERSION}: Checking code against your threadlines...\n`));
81
85
  // Get git root for consistent file paths across monorepo
82
86
  const git = (0, simple_git_1.default)(cwd);
@@ -84,13 +88,13 @@ async function checkCommand(options) {
84
88
  try {
85
89
  const isRepo = await git.checkIsRepo();
86
90
  if (!isRepo) {
87
- console.error(chalk_1.default.red('❌ Error: Not a git repository. Threadline requires a git repository.'));
91
+ logger_1.logger.error('Not a git repository. Threadline requires a git repository.');
88
92
  process.exit(1);
89
93
  }
90
94
  gitRoot = (await git.revparse(['--show-toplevel'])).trim();
91
95
  }
92
96
  catch {
93
- console.error(chalk_1.default.red('❌ Error: Failed to get git root. Make sure you are in a git repository.'));
97
+ logger_1.logger.error('Failed to get git root. Make sure you are in a git repository.');
94
98
  process.exit(1);
95
99
  }
96
100
  // Pre-flight check: Validate ALL required environment variables at once
@@ -103,9 +107,9 @@ async function checkCommand(options) {
103
107
  if (!account || account.startsWith('$'))
104
108
  missingVars.push('THREADLINE_ACCOUNT');
105
109
  if (missingVars.length > 0) {
106
- console.error(chalk_1.default.red('❌ Error: Missing required environment variables:'));
110
+ logger_1.logger.error('Missing required environment variables:');
107
111
  for (const varName of missingVars) {
108
- console.error(chalk_1.default.red(` • ${varName}`));
112
+ logger_1.logger.error(` • ${varName}`);
109
113
  }
110
114
  console.log('');
111
115
  console.log(chalk_1.default.yellow('To fix this:'));
@@ -132,7 +136,7 @@ async function checkCommand(options) {
132
136
  }
133
137
  try {
134
138
  // 1. Find and validate threadlines
135
- console.log(chalk_1.default.gray('📋 Finding threadlines...'));
139
+ logger_1.logger.info('Finding threadlines...');
136
140
  const threadlines = await (0, experts_1.findThreadlines)(cwd, gitRoot);
137
141
  console.log(chalk_1.default.green(`✓ Found ${threadlines.length} threadline(s)\n`));
138
142
  if (threadlines.length === 0) {
@@ -150,7 +154,7 @@ async function checkCommand(options) {
150
154
  const explicitFlags = [options.commit, options.file, options.folder, options.files].filter(Boolean);
151
155
  // Validate mutually exclusive flags
152
156
  if (explicitFlags.length > 1) {
153
- console.error(chalk_1.default.red('❌ Error: Only one review option can be specified at a time'));
157
+ logger_1.logger.error('Only one review option can be specified at a time');
154
158
  console.log(chalk_1.default.gray(' Options: --commit, --file, --folder, --files'));
155
159
  process.exit(1);
156
160
  }
@@ -162,7 +166,7 @@ async function checkCommand(options) {
162
166
  const flagName = options.commit ? '--commit' :
163
167
  options.file ? '--file' :
164
168
  options.folder ? '--folder' : '--files';
165
- console.log(chalk_1.default.yellow(`⚠️ Warning: ${flagName} flag ignored in CI environment. Using auto-detection.\n`));
169
+ logger_1.logger.warn(`${flagName} flag ignored in CI environment. Using auto-detection.`);
166
170
  }
167
171
  // CI auto-detect: use environment-specific context
168
172
  const envNames = {
@@ -171,7 +175,7 @@ async function checkCommand(options) {
171
175
  gitlab: 'GitLab CI',
172
176
  bitbucket: 'Bitbucket Pipelines'
173
177
  };
174
- console.log(chalk_1.default.gray(`📝 Collecting git context for ${envNames[environment]}...`));
178
+ logger_1.logger.info(`Collecting git context for ${envNames[environment]}...`);
175
179
  const envContext = await getContextForEnvironment(environment, repoRoot);
176
180
  gitDiff = envContext.diff;
177
181
  repoName = envContext.repoName;
@@ -187,19 +191,19 @@ async function checkCommand(options) {
187
191
  else {
188
192
  // Local environment: support all flags
189
193
  if (options.file) {
190
- console.log(chalk_1.default.gray(`📝 Reading file: ${options.file}...`));
194
+ logger_1.logger.info(`Reading file: ${options.file}...`);
191
195
  gitDiff = await (0, file_1.getFileContent)(repoRoot, options.file);
192
196
  }
193
197
  else if (options.folder) {
194
- console.log(chalk_1.default.gray(`📝 Reading folder: ${options.folder}...`));
198
+ logger_1.logger.info(`Reading folder: ${options.folder}...`);
195
199
  gitDiff = await (0, file_1.getFolderContent)(repoRoot, options.folder);
196
200
  }
197
201
  else if (options.files && options.files.length > 0) {
198
- console.log(chalk_1.default.gray(`📝 Reading ${options.files.length} file(s)...`));
202
+ logger_1.logger.info(`Reading ${options.files.length} file(s)...`);
199
203
  gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
200
204
  }
201
205
  else if (options.commit) {
202
- console.log(chalk_1.default.gray(`📝 Collecting git changes for commit: ${options.commit}...`));
206
+ logger_1.logger.info(`Collecting git changes for commit: ${options.commit}...`);
203
207
  gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, options.commit);
204
208
  // Use local context for metadata, passing commit SHA for author lookup
205
209
  const localContext = await (0, local_1.getLocalContext)(repoRoot, options.commit);
@@ -214,7 +218,7 @@ async function checkCommand(options) {
214
218
  }
215
219
  else {
216
220
  // Local auto-detect: staged/unstaged changes
217
- console.log(chalk_1.default.gray('📝 Collecting git context for Local...'));
221
+ logger_1.logger.info('Collecting git context for Local...');
218
222
  const localContext = await (0, local_1.getLocalContext)(repoRoot);
219
223
  gitDiff = localContext.diff;
220
224
  repoName = localContext.repoName;
@@ -264,13 +268,9 @@ async function checkCommand(options) {
264
268
  contextContent
265
269
  };
266
270
  });
267
- // 5. Get API URL
268
- const apiUrl = options.apiUrl ||
269
- process.env.THREADLINE_API_URL ||
270
- 'https://devthreadline.com';
271
- // 6. Call review API
272
- console.log(chalk_1.default.gray('🤖 Running threadline checks...'));
273
- const client = new client_1.ReviewAPIClient(apiUrl);
271
+ // 5. Call review API
272
+ logger_1.logger.info('Running threadline checks...');
273
+ const client = new client_1.ReviewAPIClient(config.api_url);
274
274
  const response = await client.review({
275
275
  threadlines: threadlinesWithContext,
276
276
  diff: gitDiff.diff,
@@ -295,7 +295,7 @@ async function checkCommand(options) {
295
295
  }
296
296
  catch (error) {
297
297
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
298
- console.error(chalk_1.default.red(`\n❌ Error: ${errorMessage}`));
298
+ logger_1.logger.error(errorMessage);
299
299
  process.exit(1);
300
300
  }
301
301
  }
@@ -40,6 +40,8 @@ exports.initCommand = initCommand;
40
40
  const fs = __importStar(require("fs"));
41
41
  const path = __importStar(require("path"));
42
42
  const chalk_1 = __importDefault(require("chalk"));
43
+ const logger_1 = require("../utils/logger");
44
+ const config_file_1 = require("../utils/config-file");
43
45
  const TEMPLATE = `---
44
46
  id: example-threadline
45
47
  version: 1.0.0
@@ -75,12 +77,19 @@ async function initCommand() {
75
77
  const repoRoot = process.cwd();
76
78
  const threadlinesDir = path.join(repoRoot, 'threadlines');
77
79
  const exampleFile = path.join(threadlinesDir, 'example.md');
80
+ const configFile = path.join(repoRoot, '.threadlinerc');
78
81
  try {
79
82
  // Create threadlines directory if it doesn't exist
80
83
  if (!fs.existsSync(threadlinesDir)) {
81
84
  fs.mkdirSync(threadlinesDir, { recursive: true });
82
85
  console.log(chalk_1.default.green(`✓ Created /threadlines directory`));
83
86
  }
87
+ // Create .threadlinerc if it doesn't exist
88
+ if (!fs.existsSync(configFile)) {
89
+ const configContent = JSON.stringify(config_file_1.DEFAULT_CONFIG, null, 2);
90
+ fs.writeFileSync(configFile, configContent, 'utf-8');
91
+ console.log(chalk_1.default.green(`✓ Created .threadlinerc`));
92
+ }
84
93
  // Check if example file already exists
85
94
  if (fs.existsSync(exampleFile)) {
86
95
  console.log(chalk_1.default.yellow(`⚠️ ${exampleFile} already exists`));
@@ -109,7 +118,7 @@ async function initCommand() {
109
118
  }
110
119
  catch (error) {
111
120
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
112
- console.error(chalk_1.default.red(`\n❌ Error: ${errorMessage}`));
121
+ logger_1.logger.error(errorMessage);
113
122
  process.exit(1);
114
123
  }
115
124
  }
@@ -25,6 +25,7 @@ exports.getBitbucketContext = getBitbucketContext;
25
25
  const simple_git_1 = __importDefault(require("simple-git"));
26
26
  const child_process_1 = require("child_process");
27
27
  const diff_1 = require("./diff");
28
+ const logger_1 = require("../utils/logger");
28
29
  /**
29
30
  * Gets all Bitbucket context in one call
30
31
  */
@@ -66,22 +67,26 @@ async function getBitbucketContext(repoRoot) {
66
67
  * Get diff for Bitbucket Pipelines environment
67
68
  *
68
69
  * Strategy:
69
- * - PR context: Compare source branch vs target branch (full PR diff)
70
+ * - PR context: Fetch destination branch on-demand, compare source vs target (full PR diff)
70
71
  * - Any push (main or feature branch): Compare last commit only (HEAD~1...HEAD)
71
72
  *
72
- * Note: Bitbucket Pipelines with depth: full has full git history available.
73
+ * Note: We fetch the destination branch on-demand so this works with shallow clones.
74
+ * Users don't need `depth: full` in their bitbucket-pipelines.yml.
73
75
  */
74
76
  async function getDiff(repoRoot) {
75
77
  const git = (0, simple_git_1.default)(repoRoot);
76
78
  const prId = process.env.BITBUCKET_PR_ID;
77
79
  const prDestinationBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
78
- // PR Context: Compare source vs target branch
80
+ // PR Context: Fetch destination branch and compare
79
81
  if (prId) {
80
82
  if (!prDestinationBranch) {
81
83
  throw new Error('Bitbucket PR context detected but BITBUCKET_PR_DESTINATION_BRANCH is not set. ' +
82
84
  'This should be automatically provided by Bitbucket Pipelines.');
83
85
  }
84
- console.log(` [Bitbucket] PR #${prId}, using origin/${prDestinationBranch}...HEAD`);
86
+ // Fetch destination branch on-demand (works with shallow clones)
87
+ logger_1.logger.debug(`Fetching destination branch: origin/${prDestinationBranch}`);
88
+ await git.fetch(['origin', `${prDestinationBranch}:refs/remotes/origin/${prDestinationBranch}`, '--depth=1']);
89
+ logger_1.logger.debug(`PR #${prId}, using origin/${prDestinationBranch}...HEAD`);
85
90
  const diff = await git.diff([`origin/${prDestinationBranch}...HEAD`, '-U200']);
86
91
  const diffSummary = await git.diffSummary([`origin/${prDestinationBranch}...HEAD`]);
87
92
  const changedFiles = diffSummary.files.map(f => f.file);
package/dist/git/file.js CHANGED
@@ -39,6 +39,7 @@ exports.getMultipleFilesContent = getMultipleFilesContent;
39
39
  const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
41
  const glob_1 = require("glob");
42
+ const logger_1 = require("../utils/logger");
42
43
  /**
43
44
  * Read content of a single file and create artificial diff (all lines as additions)
44
45
  */
@@ -116,7 +117,7 @@ async function getFolderContent(repoRoot, folderPath) {
116
117
  catch (error) {
117
118
  // Skip files that can't be read (permissions, etc.)
118
119
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
119
- console.warn(`Warning: Could not read file '${filePath}': ${errorMessage}`);
120
+ logger_1.logger.warn(`Could not read file '${filePath}': ${errorMessage}`);
120
121
  }
121
122
  }
122
123
  return {
@@ -12,47 +12,14 @@
12
12
  * - commitAuthor: { name: string; email: string }
13
13
  * - prTitle?: string
14
14
  */
15
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
16
- if (k2 === undefined) k2 = k;
17
- var desc = Object.getOwnPropertyDescriptor(m, k);
18
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
19
- desc = { enumerable: true, get: function() { return m[k]; } };
20
- }
21
- Object.defineProperty(o, k2, desc);
22
- }) : (function(o, m, k, k2) {
23
- if (k2 === undefined) k2 = k;
24
- o[k2] = m[k];
25
- }));
26
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
27
- Object.defineProperty(o, "default", { enumerable: true, value: v });
28
- }) : function(o, v) {
29
- o["default"] = v;
30
- });
31
- var __importStar = (this && this.__importStar) || (function () {
32
- var ownKeys = function(o) {
33
- ownKeys = Object.getOwnPropertyNames || function (o) {
34
- var ar = [];
35
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
36
- return ar;
37
- };
38
- return ownKeys(o);
39
- };
40
- return function (mod) {
41
- if (mod && mod.__esModule) return mod;
42
- var result = {};
43
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
44
- __setModuleDefault(result, mod);
45
- return result;
46
- };
47
- })();
48
15
  var __importDefault = (this && this.__importDefault) || function (mod) {
49
16
  return (mod && mod.__esModule) ? mod : { "default": mod };
50
17
  };
51
18
  Object.defineProperty(exports, "__esModule", { value: true });
52
19
  exports.getGitHubContext = getGitHubContext;
53
20
  const simple_git_1 = __importDefault(require("simple-git"));
54
- const fs = __importStar(require("fs"));
55
21
  const diff_1 = require("./diff");
22
+ const logger_1 = require("../utils/logger");
56
23
  /**
57
24
  * Gets all GitHub context
58
25
  */
@@ -69,8 +36,13 @@ async function getGitHubContext(repoRoot) {
69
36
  const branchName = await getBranchName();
70
37
  const context = detectContext();
71
38
  const commitSha = getCommitSha(context);
72
- // Note: commitSha parameter not needed - GitHub reads from GITHUB_EVENT_PATH JSON
73
- const commitAuthor = await getCommitAuthor();
39
+ // Validate commit SHA is available (should always be set in GitHub Actions)
40
+ if (!commitSha) {
41
+ throw new Error('GitHub Actions: GITHUB_SHA environment variable is not set. ' +
42
+ 'This should be automatically provided by GitHub Actions.');
43
+ }
44
+ // Get commit author using git commands (same approach as Bitbucket/Local)
45
+ const commitAuthor = await (0, diff_1.getCommitAuthor)(repoRoot, commitSha);
74
46
  // Get commit message if we have a SHA
75
47
  let commitMessage;
76
48
  if (commitSha) {
@@ -81,6 +53,11 @@ async function getGitHubContext(repoRoot) {
81
53
  }
82
54
  // Get PR title if in PR context
83
55
  const prTitle = getPRTitle(context);
56
+ // Validate commit author was found
57
+ if (!commitAuthor) {
58
+ throw new Error(`GitHub Actions: Failed to get commit author from git log for commit ${commitSha || 'HEAD'}. ` +
59
+ 'This should be automatically available in the git repository.');
60
+ }
84
61
  return {
85
62
  diff,
86
63
  repoName,
@@ -96,22 +73,28 @@ async function getGitHubContext(repoRoot) {
96
73
  * Gets diff for GitHub Actions CI environment
97
74
  *
98
75
  * Strategy:
99
- * - PR context: Compare source branch vs target branch (full PR diff)
76
+ * - PR context: Fetch base branch on-demand, compare base vs HEAD (full PR diff)
100
77
  * - Any push (main or feature branch): Compare last commit only (HEAD~1...HEAD)
78
+ *
79
+ * Note: GitHub Actions does shallow clones by default (fetch-depth: 1), so we fetch
80
+ * the base branch on-demand. HEAD points to the merge commit which contains all PR changes.
101
81
  */
102
82
  async function getDiff(repoRoot) {
103
83
  const git = (0, simple_git_1.default)(repoRoot);
104
84
  const eventName = process.env.GITHUB_EVENT_NAME;
105
85
  const baseRef = process.env.GITHUB_BASE_REF;
106
- const headRef = process.env.GITHUB_HEAD_REF;
107
- // PR Context: Compare source vs target branch
86
+ // PR Context: Fetch base branch and compare with HEAD (merge commit)
108
87
  if (eventName === 'pull_request') {
109
- if (!baseRef || !headRef) {
110
- throw new Error('GitHub PR context detected but GITHUB_BASE_REF or GITHUB_HEAD_REF is missing. ' +
88
+ if (!baseRef) {
89
+ throw new Error('GitHub PR context detected but GITHUB_BASE_REF is missing. ' +
111
90
  'This should be automatically provided by GitHub Actions.');
112
91
  }
113
- const diff = await git.diff([`origin/${baseRef}...origin/${headRef}`, '-U200']);
114
- const diffSummary = await git.diffSummary([`origin/${baseRef}...origin/${headRef}`]);
92
+ // Fetch base branch on-demand (works with shallow clones)
93
+ logger_1.logger.debug(`Fetching base branch: origin/${baseRef}`);
94
+ await git.fetch(['origin', `${baseRef}:refs/remotes/origin/${baseRef}`, '--depth=1']);
95
+ logger_1.logger.debug(`PR context, using origin/${baseRef}...HEAD`);
96
+ const diff = await git.diff([`origin/${baseRef}...HEAD`, '-U200']);
97
+ const diffSummary = await git.diffSummary([`origin/${baseRef}...HEAD`]);
115
98
  const changedFiles = diffSummary.files.map(f => f.file);
116
99
  return {
117
100
  diff: diff || '',
@@ -194,59 +177,6 @@ function getCommitSha(context) {
194
177
  }
195
178
  return undefined;
196
179
  }
197
- /**
198
- * Gets commit author for GitHub Actions
199
- * Reads from GITHUB_EVENT_PATH JSON file (most reliable)
200
- * Note: commitSha parameter not used - GitHub provides author info in event JSON
201
- */
202
- async function getCommitAuthor() {
203
- const eventPath = process.env.GITHUB_EVENT_PATH;
204
- if (!eventPath) {
205
- throw new Error('GitHub Actions: GITHUB_EVENT_PATH environment variable is not set. ' +
206
- 'This should be automatically provided by GitHub Actions.');
207
- }
208
- if (!fs.existsSync(eventPath)) {
209
- throw new Error(`GitHub Actions: GITHUB_EVENT_PATH file does not exist: ${eventPath}. ` +
210
- 'This should be automatically provided by GitHub Actions.');
211
- }
212
- try {
213
- const eventData = JSON.parse(fs.readFileSync(eventPath, 'utf-8'));
214
- // For push events, use head_commit.author
215
- if (eventData.head_commit?.author) {
216
- return {
217
- name: eventData.head_commit.author.name,
218
- email: eventData.head_commit.author.email
219
- };
220
- }
221
- // For PR events, use commits[0].author (first commit in the PR)
222
- if (eventData.commits && eventData.commits.length > 0 && eventData.commits[0].author) {
223
- return {
224
- name: eventData.commits[0].author.name,
225
- email: eventData.commits[0].author.email
226
- };
227
- }
228
- // Fallback to pull_request.head.commit.author for PR events
229
- if (eventData.pull_request?.head?.commit?.author) {
230
- return {
231
- name: eventData.pull_request.head.commit.author.name,
232
- email: eventData.pull_request.head.commit.author.email
233
- };
234
- }
235
- // If we get here, the event JSON doesn't contain author info
236
- throw new Error(`GitHub Actions: GITHUB_EVENT_PATH JSON does not contain commit author information. ` +
237
- `Event type: ${eventData.action || 'unknown'}. ` +
238
- `This should be automatically provided by GitHub Actions.`);
239
- }
240
- catch (error) {
241
- // If JSON parsing fails, fail loudly
242
- if (error instanceof Error && error.message.includes('GitHub Actions:')) {
243
- throw error; // Re-throw our own errors
244
- }
245
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
246
- throw new Error(`GitHub Actions: Failed to read or parse GITHUB_EVENT_PATH JSON: ${errorMessage}. ` +
247
- 'This should be automatically provided by GitHub Actions.');
248
- }
249
- }
250
180
  /**
251
181
  * Gets PR title for GitHub Actions
252
182
  * Note: GitHub Actions doesn't provide PR title as an env var by default.
@@ -19,6 +19,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
19
19
  exports.getGitLabContext = getGitLabContext;
20
20
  const simple_git_1 = __importDefault(require("simple-git"));
21
21
  const diff_1 = require("./diff");
22
+ const logger_1 = require("../utils/logger");
22
23
  /**
23
24
  * Gets all GitLab context
24
25
  */
@@ -80,7 +81,7 @@ async function getDiff(repoRoot) {
80
81
  'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME is missing. ' +
81
82
  'This should be automatically provided by GitLab CI.');
82
83
  }
83
- console.log(` [GitLab] Fetching target branch: origin/${targetBranch}`);
84
+ logger_1.logger.debug(`Fetching target branch: origin/${targetBranch}`);
84
85
  await git.fetch(['origin', `${targetBranch}:refs/remotes/origin/${targetBranch}`, '--depth=1']);
85
86
  const diff = await git.diff([`origin/${targetBranch}...origin/${sourceBranch}`, '-U200']);
86
87
  const diffSummary = await git.diffSummary([`origin/${targetBranch}...origin/${sourceBranch}`]);
package/dist/index.js CHANGED
@@ -49,6 +49,7 @@ if (fs.existsSync(envLocalPath)) {
49
49
  const commander_1 = require("commander");
50
50
  const check_1 = require("./commands/check");
51
51
  const init_1 = require("./commands/init");
52
+ const logger_1 = require("./utils/logger");
52
53
  const program = new commander_1.Command();
53
54
  program
54
55
  .name('threadlines')
@@ -57,27 +58,39 @@ program
57
58
  program
58
59
  .command('init')
59
60
  .description('Create a template threadline file to get started')
60
- .action(init_1.initCommand);
61
+ .option('--debug', 'Enable debug logging (verbose output)')
62
+ .action((options) => {
63
+ if (options.debug) {
64
+ (0, logger_1.enableDebug)();
65
+ }
66
+ (0, init_1.initCommand)();
67
+ });
61
68
  program
62
69
  .command('check')
63
70
  .description('Check code against your threadlines')
64
- .option('--api-url <url>', 'Threadline server URL', process.env.THREADLINE_API_URL || 'https://devthreadline.com')
65
71
  .option('--full', 'Show all results (compliant, attention, not_relevant). Default: only attention items')
66
72
  .option('--commit <ref>', 'Review specific commit. Accepts commit SHA or git reference (e.g., HEAD, HEAD~1, abc123). Example: --commit HEAD')
67
73
  .option('--file <path>', 'Review entire file (all lines as additions)')
68
74
  .option('--folder <path>', 'Review all files in folder recursively')
69
75
  .option('--files <paths...>', 'Review multiple specified files')
76
+ .option('--debug', 'Enable debug logging (verbose output)')
70
77
  .addHelpText('after', `
71
78
  Examples:
72
79
  $ threadlines check # Check staged/unstaged changes (local dev)
73
80
  $ threadlines check --commit HEAD # Check latest commit locally
74
81
  $ threadlines check --file src/api.ts # Check entire file
75
82
  $ threadlines check --full # Show all results (not just attention items)
83
+ $ threadlines check --debug # Enable verbose debug output
76
84
 
77
85
  Auto-detection in CI:
78
86
  - PR/MR context → reviews all changes in the PR/MR
79
87
  - Push to any branch → reviews the commit being pushed
80
88
  - Local development → reviews staged/unstaged changes
81
89
  `)
82
- .action(check_1.checkCommand);
90
+ .action((options) => {
91
+ if (options.debug) {
92
+ (0, logger_1.enableDebug)();
93
+ }
94
+ (0, check_1.checkCommand)(options);
95
+ });
83
96
  program.parse();
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.DEFAULT_CONFIG = void 0;
40
+ exports.loadConfig = loadConfig;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const simple_git_1 = __importDefault(require("simple-git"));
44
+ exports.DEFAULT_CONFIG = {
45
+ mode: 'online',
46
+ api_url: 'https://devthreadline.com',
47
+ openai_model: 'gpt-5.2',
48
+ openai_service_tier: 'Flex',
49
+ diff_context_lines: 10,
50
+ };
51
+ /**
52
+ * Finds the git root directory by walking up from startDir.
53
+ * Returns startDir if not in a git repository.
54
+ */
55
+ async function findGitRoot(startDir) {
56
+ try {
57
+ const git = (0, simple_git_1.default)(startDir);
58
+ const isRepo = await git.checkIsRepo();
59
+ if (isRepo) {
60
+ return (await git.revparse(['--show-toplevel'])).trim();
61
+ }
62
+ }
63
+ catch {
64
+ // Not a git repo or error - return startDir
65
+ }
66
+ return startDir;
67
+ }
68
+ /**
69
+ * Loads configuration from .threadlinerc file.
70
+ *
71
+ * Priority:
72
+ * 1. Built-in defaults
73
+ * 2. .threadlinerc file (if exists) - merged with defaults
74
+ *
75
+ * Searches for .threadlinerc starting from startDir, walking up to git root.
76
+ * If no file found, returns defaults.
77
+ */
78
+ async function loadConfig(startDir) {
79
+ // Start with defaults
80
+ const config = { ...exports.DEFAULT_CONFIG };
81
+ // Find git root to limit search scope
82
+ const gitRoot = await findGitRoot(startDir);
83
+ // Look for .threadlinerc starting from startDir, up to git root
84
+ let currentDir = startDir;
85
+ let configPath = null;
86
+ while (true) {
87
+ const candidatePath = path.join(currentDir, '.threadlinerc');
88
+ if (fs.existsSync(candidatePath)) {
89
+ configPath = candidatePath;
90
+ break;
91
+ }
92
+ // Stop at git root
93
+ if (path.resolve(currentDir) === path.resolve(gitRoot)) {
94
+ break;
95
+ }
96
+ // Move up one directory
97
+ const parentDir = path.dirname(currentDir);
98
+ if (parentDir === currentDir) {
99
+ // Reached filesystem root
100
+ break;
101
+ }
102
+ currentDir = parentDir;
103
+ }
104
+ // If config file found, parse and merge
105
+ if (configPath) {
106
+ try {
107
+ const configContent = fs.readFileSync(configPath, 'utf-8');
108
+ const fileConfig = JSON.parse(configContent);
109
+ // Merge file config into defaults (file overrides defaults)
110
+ Object.assign(config, fileConfig);
111
+ }
112
+ catch (error) {
113
+ // If file exists but can't be parsed, log warning but continue with defaults
114
+ console.warn(`Warning: Failed to parse .threadlinerc at ${configPath}: ${error instanceof Error ? error.message : 'Unknown error'}`);
115
+ }
116
+ }
117
+ return config;
118
+ }
@@ -1,74 +1,115 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
36
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
4
  };
38
5
  Object.defineProperty(exports, "__esModule", { value: true });
39
6
  exports.getThreadlineApiKey = getThreadlineApiKey;
40
7
  exports.getThreadlineAccount = getThreadlineAccount;
41
- const fs = __importStar(require("fs"));
42
- const path = __importStar(require("path"));
43
- const dotenv_1 = __importDefault(require("dotenv"));
44
- /**
45
- * Loads environment variables from .env.local file in the project root
46
- * (where the user runs the command, not the CLI package directory)
47
- */
48
- function loadEnvLocal() {
49
- const projectRoot = process.cwd();
50
- const envLocalPath = path.join(projectRoot, '.env.local');
51
- if (fs.existsSync(envLocalPath)) {
52
- dotenv_1.default.config({ path: envLocalPath });
53
- }
54
- }
8
+ exports.getOpenAIConfig = getOpenAIConfig;
9
+ exports.logOpenAIConfig = logOpenAIConfig;
10
+ exports.isDirectModeAvailable = isDirectModeAvailable;
11
+ const chalk_1 = __importDefault(require("chalk"));
12
+ const logger_1 = require("./logger");
13
+ // Default values for OpenAI configuration
14
+ const OPENAI_MODEL_DEFAULT = 'gpt-5.2';
15
+ const OPENAI_SERVICE_TIER_DEFAULT = 'Flex';
55
16
  /**
56
17
  * Gets THREADLINE_API_KEY from environment.
57
- * Priority: process.env.THREADLINE_API_KEY → .env.local file
18
+ *
19
+ * Note: .env.local is automatically loaded at CLI startup (see index.ts).
20
+ * In CI/CD, environment variables are injected directly into process.env.
58
21
  */
59
22
  function getThreadlineApiKey() {
60
- // Load .env.local if it exists (doesn't override existing env vars)
61
- loadEnvLocal();
62
- // Check environment variable (from shell or CI/CD)
63
- return process.env.THREADLINE_API_KEY;
23
+ const apiKey = process.env.THREADLINE_API_KEY;
24
+ if (apiKey) {
25
+ logger_1.logger.debug('THREADLINE_API_KEY: found (value hidden for security)');
26
+ }
27
+ else {
28
+ logger_1.logger.debug('THREADLINE_API_KEY: not set');
29
+ }
30
+ return apiKey;
64
31
  }
65
32
  /**
66
33
  * Gets THREADLINE_ACCOUNT from environment.
67
- * Priority: process.env.THREADLINE_ACCOUNT → .env.local file
34
+ *
35
+ * Note: .env.local is automatically loaded at CLI startup (see index.ts).
36
+ * In CI/CD, environment variables are injected directly into process.env.
68
37
  */
69
38
  function getThreadlineAccount() {
70
- // Load .env.local if it exists (doesn't override existing env vars)
71
- loadEnvLocal();
72
- // Check environment variable (from shell or CI/CD)
73
- return process.env.THREADLINE_ACCOUNT;
39
+ const account = process.env.THREADLINE_ACCOUNT;
40
+ if (account) {
41
+ logger_1.logger.debug(`THREADLINE_ACCOUNT: ${account}`);
42
+ }
43
+ else {
44
+ logger_1.logger.debug('THREADLINE_ACCOUNT: not set');
45
+ }
46
+ return account;
47
+ }
48
+ /**
49
+ * Gets OpenAI configuration from environment variables.
50
+ *
51
+ * Required:
52
+ * - OPENAI_API_KEY: Your OpenAI API key
53
+ *
54
+ * Optional (with defaults):
55
+ * - OPENAI_MODEL: Model to use (default: gpt-5.2)
56
+ * - OPENAI_SERVICE_TIER: Service tier (default: Flex)
57
+ *
58
+ * Returns undefined if OPENAI_API_KEY is not set.
59
+ *
60
+ * Note: .env.local is automatically loaded at CLI startup (see index.ts).
61
+ * In CI/CD, environment variables are injected directly into process.env.
62
+ */
63
+ function getOpenAIConfig() {
64
+ const apiKey = process.env.OPENAI_API_KEY;
65
+ if (!apiKey) {
66
+ logger_1.logger.debug('OPENAI_API_KEY: not set (direct mode unavailable)');
67
+ return undefined;
68
+ }
69
+ logger_1.logger.debug('OPENAI_API_KEY: found (value hidden for security)');
70
+ const model = process.env.OPENAI_MODEL || OPENAI_MODEL_DEFAULT;
71
+ const serviceTier = process.env.OPENAI_SERVICE_TIER || OPENAI_SERVICE_TIER_DEFAULT;
72
+ if (process.env.OPENAI_MODEL) {
73
+ logger_1.logger.debug(`OPENAI_MODEL: ${model} (from environment)`);
74
+ }
75
+ else {
76
+ logger_1.logger.debug(`OPENAI_MODEL: ${model} (using default)`);
77
+ }
78
+ if (process.env.OPENAI_SERVICE_TIER) {
79
+ logger_1.logger.debug(`OPENAI_SERVICE_TIER: ${serviceTier} (from environment)`);
80
+ }
81
+ else {
82
+ logger_1.logger.debug(`OPENAI_SERVICE_TIER: ${serviceTier} (using default)`);
83
+ }
84
+ return {
85
+ apiKey,
86
+ model,
87
+ serviceTier
88
+ };
89
+ }
90
+ /**
91
+ * Logs the OpenAI configuration being used.
92
+ * Call this when starting direct LLM mode to inform the user.
93
+ */
94
+ function logOpenAIConfig(config) {
95
+ console.log(chalk_1.default.blue('OpenAI Direct Mode:'));
96
+ console.log(chalk_1.default.gray(` Model: ${config.model}${config.model === OPENAI_MODEL_DEFAULT ? ' (default)' : ''}`));
97
+ console.log(chalk_1.default.gray(` Service Tier: ${config.serviceTier}${config.serviceTier === OPENAI_SERVICE_TIER_DEFAULT ? ' (default)' : ''}`));
98
+ console.log('');
99
+ }
100
+ /**
101
+ * Checks if direct OpenAI mode is available (OPENAI_API_KEY is set).
102
+ *
103
+ * Note: .env.local is automatically loaded at CLI startup (see index.ts).
104
+ * In CI/CD, environment variables are injected directly into process.env.
105
+ */
106
+ function isDirectModeAvailable() {
107
+ const available = !!process.env.OPENAI_API_KEY;
108
+ if (available) {
109
+ logger_1.logger.debug('Direct OpenAI mode: available (OPENAI_API_KEY found)');
110
+ }
111
+ else {
112
+ logger_1.logger.debug('Direct OpenAI mode: unavailable (OPENAI_API_KEY not set)');
113
+ }
114
+ return available;
74
115
  }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.logger = void 0;
7
+ exports.enableDebug = enableDebug;
8
+ exports.isDebugEnabled = isDebugEnabled;
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ /**
11
+ * Global debug flag - set when --debug is passed to CLI
12
+ */
13
+ let debugEnabled = false;
14
+ /**
15
+ * Enable debug logging (called when --debug flag is set)
16
+ */
17
+ function enableDebug() {
18
+ debugEnabled = true;
19
+ }
20
+ /**
21
+ * Check if debug logging is enabled
22
+ */
23
+ function isDebugEnabled() {
24
+ return debugEnabled;
25
+ }
26
+ /**
27
+ * Logger utility for CLI output
28
+ *
29
+ * - debug/info: Only shown when --debug flag is set
30
+ * - warn/error: Always shown (critical information)
31
+ */
32
+ exports.logger = {
33
+ /**
34
+ * Debug-level log (technical details, internal state)
35
+ * Only shown with --debug flag
36
+ */
37
+ debug: (message) => {
38
+ if (debugEnabled) {
39
+ console.log(chalk_1.default.gray(`[DEBUG] ${message}`));
40
+ }
41
+ },
42
+ /**
43
+ * Info-level log (what's happening, progress updates)
44
+ * Only shown with --debug flag
45
+ */
46
+ info: (message) => {
47
+ if (debugEnabled) {
48
+ console.log(chalk_1.default.blue(`[INFO] ${message}`));
49
+ }
50
+ },
51
+ /**
52
+ * Warning (non-fatal issues, recommendations)
53
+ * Always shown
54
+ */
55
+ warn: (message) => {
56
+ console.log(chalk_1.default.yellow(`⚠️ ${message}`));
57
+ },
58
+ /**
59
+ * Error (failures, problems)
60
+ * Always shown
61
+ */
62
+ error: (message) => {
63
+ console.error(chalk_1.default.red(`❌ ${message}`));
64
+ }
65
+ };
@@ -38,6 +38,7 @@ exports.validateThreadline = validateThreadline;
38
38
  const fs = __importStar(require("fs"));
39
39
  const path = __importStar(require("path"));
40
40
  const yaml = __importStar(require("js-yaml"));
41
+ const logger_1 = require("../utils/logger");
41
42
  const REQUIRED_FIELDS = ['id', 'version', 'patterns'];
42
43
  /**
43
44
  * Find and validate all threadlines in the threadlines folder.
@@ -61,7 +62,7 @@ async function findThreadlines(searchRoot, gitRoot) {
61
62
  threadlines.push(result.threadline);
62
63
  }
63
64
  else {
64
- console.warn(`⚠️ Skipping ${file}: ${result.errors?.join(', ')}`);
65
+ logger_1.logger.warn(`Skipping ${file}: ${result.errors?.join(', ')}`);
65
66
  }
66
67
  }
67
68
  return threadlines;
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "threadlines",
3
- "version": "0.2.7",
3
+ "version": "0.2.10",
4
4
  "description": "Threadlines CLI - AI-powered linter based on your natural language documentation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
+ "threadlines": "./bin/threadline",
7
8
  "threadline": "./bin/threadline"
8
9
  },
9
10
  "files": [