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.
- package/dist/api/client.js +2 -17
- package/dist/commands/check.js +21 -21
- package/dist/commands/init.js +10 -1
- package/dist/git/bitbucket.js +9 -4
- package/dist/git/file.js +2 -1
- package/dist/git/github.js +7 -0
- package/dist/git/gitlab.js +2 -1
- package/dist/index.js +16 -3
- package/dist/utils/config-file.js +118 -0
- package/dist/utils/config.js +98 -57
- package/dist/utils/logger.js +65 -0
- package/dist/validators/experts.js +2 -1
- package/package.json +1 -1
package/dist/api/client.js
CHANGED
|
@@ -16,23 +16,8 @@ class ReviewAPIClient {
|
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
18
|
async review(request) {
|
|
19
|
-
|
|
20
|
-
|
|
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;
|
package/dist/commands/check.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
+
logger_1.logger.error('Missing required environment variables:');
|
|
107
111
|
for (const varName of missingVars) {
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
298
|
+
logger_1.logger.error(errorMessage);
|
|
299
299
|
process.exit(1);
|
|
300
300
|
}
|
|
301
301
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
121
|
+
logger_1.logger.error(errorMessage);
|
|
113
122
|
process.exit(1);
|
|
114
123
|
}
|
|
115
124
|
}
|
package/dist/git/bitbucket.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
120
|
+
logger_1.logger.warn(`Could not read file '${filePath}': ${errorMessage}`);
|
|
120
121
|
}
|
|
121
122
|
}
|
|
122
123
|
return {
|
package/dist/git/github.js
CHANGED
|
@@ -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. ' +
|
package/dist/git/gitlab.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
.
|
|
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(
|
|
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
|
+
}
|
package/dist/utils/config.js
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
65
|
+
logger_1.logger.warn(`Skipping ${file}: ${result.errors?.join(', ')}`);
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
return threadlines;
|