threadlines 0.2.7 → 0.2.9

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 {
@@ -98,6 +98,12 @@ async function getGitHubContext(repoRoot) {
98
98
  * Strategy:
99
99
  * - PR context: Compare source branch vs target branch (full PR diff)
100
100
  * - Any push (main or feature branch): Compare last commit only (HEAD~1...HEAD)
101
+ *
102
+ * Note: Unlike GitLab/Bitbucket, we don't need to fetch branches on-demand here.
103
+ * GitHub Actions' `actions/checkout` automatically fetches both base and head refs
104
+ * for pull_request events, even with the default shallow clone (fetch-depth: 1).
105
+ * The refs `origin/${GITHUB_BASE_REF}` and `origin/${GITHUB_HEAD_REF}` are available
106
+ * immediately after checkout.
101
107
  */
102
108
  async function getDiff(repoRoot) {
103
109
  const git = (0, simple_git_1.default)(repoRoot);
@@ -105,6 +111,7 @@ async function getDiff(repoRoot) {
105
111
  const baseRef = process.env.GITHUB_BASE_REF;
106
112
  const headRef = process.env.GITHUB_HEAD_REF;
107
113
  // PR Context: Compare source vs target branch
114
+ // No fetch needed - GitHub Actions provides both refs automatically
108
115
  if (eventName === 'pull_request') {
109
116
  if (!baseRef || !headRef) {
110
117
  throw new Error('GitHub PR context detected but GITHUB_BASE_REF or GITHUB_HEAD_REF is missing. ' +
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "threadlines",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Threadlines CLI - AI-powered linter based on your natural language documentation",
5
5
  "main": "dist/index.js",
6
6
  "bin": {