threadlines 0.1.41 → 0.2.3
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/LICENSE +22 -0
- package/README.md +14 -43
- package/SECURITY.md +47 -0
- package/dist/commands/check.js +5 -16
- package/dist/utils/git-diff-executor.js +0 -32
- package/package.json +18 -11
- package/dist/utils/ci-detection.js +0 -52
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Threadlines
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Threadline
|
|
2
2
|
|
|
3
3
|
Threadline CLI - AI-powered linter based on your natural language documentation.
|
|
4
4
|
|
|
@@ -14,13 +14,13 @@ Getting teams to follow consistent quality standards is **hard**. Really hard.
|
|
|
14
14
|
|
|
15
15
|
### What Makes Threadline Different?
|
|
16
16
|
|
|
17
|
-
-
|
|
17
|
+
- **Focused Reviews** - Instead of one AI trying to check everything, Threadline runs multiple specialized AI reviewers in parallel. Each threadline focuses on one thing and does it well.
|
|
18
18
|
|
|
19
|
-
-
|
|
19
|
+
- **Documentation That Lives With Your Code** - Your coding standards live in your repo, in a `/threadlines` folder. They're version-controlled, reviewable, and always in sync with your codebase.
|
|
20
20
|
|
|
21
|
-
-
|
|
21
|
+
- **Fully Auditable** - Every AI review decision is logged and traceable. You can see exactly what was checked, why it passed or failed, and have confidence in the results.
|
|
22
22
|
|
|
23
|
-
-
|
|
23
|
+
- **Fast & Parallel** - Multiple threadlines run simultaneously, so you get comprehensive feedback in seconds, not minutes.
|
|
24
24
|
|
|
25
25
|
## Installation
|
|
26
26
|
|
|
@@ -48,19 +48,6 @@ npx --yes threadlines check
|
|
|
48
48
|
|
|
49
49
|
The `--yes` flag auto-confirms package installation, preventing prompts that block automation.
|
|
50
50
|
|
|
51
|
-
### Option 3: Local Project Dependency (Recommended for Teams)
|
|
52
|
-
|
|
53
|
-
```bash
|
|
54
|
-
npm install --save-dev threadlines
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Then use:
|
|
58
|
-
```bash
|
|
59
|
-
npx threadlines check
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
This ensures everyone on your team uses the same version.
|
|
63
|
-
|
|
64
51
|
## Quick Start
|
|
65
52
|
|
|
66
53
|
### 1. Initialize Your First Threadline
|
|
@@ -95,26 +82,6 @@ Edit `threadlines/example.md` with your coding standards, then rename it to some
|
|
|
95
82
|
```bash
|
|
96
83
|
npx threadlines check
|
|
97
84
|
```
|
|
98
|
-
|
|
99
|
-
## Usage
|
|
100
|
-
|
|
101
|
-
### Initialize Threadline Template
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
threadlines init
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
Creates a template threadline file to get you started. The command will:
|
|
108
|
-
- Create the `/threadlines` directory if it doesn't exist
|
|
109
|
-
- Generate `threadlines/example.md` with boilerplate content
|
|
110
|
-
- Display instructions for API key configuration
|
|
111
|
-
|
|
112
|
-
### Check Code Against Threadlines
|
|
113
|
-
|
|
114
|
-
```bash
|
|
115
|
-
threadlines check
|
|
116
|
-
```
|
|
117
|
-
|
|
118
85
|
By default, analyzes your staged/unstaged git changes against all threadlines in the `/threadlines` directory.
|
|
119
86
|
|
|
120
87
|
**Common Use Cases:**
|
|
@@ -164,11 +131,15 @@ threadlines check --full
|
|
|
164
131
|
|
|
165
132
|
### Environment Variables
|
|
166
133
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
134
|
+
| Variable | Purpose | Required |
|
|
135
|
+
|----------|---------|----------|
|
|
136
|
+
| `THREADLINE_API_KEY` | Authentication with Threadlines API | Yes |
|
|
137
|
+
| `THREADLINE_ACCOUNT` | Your Threadlines account email | Yes |
|
|
138
|
+
| `THREADLINE_API_URL` | Custom API endpoint (default: https://devthreadline.com) | No |
|
|
139
|
+
|
|
140
|
+
Both required variables can be set in a `.env.local` file (recommended for local development) or as environment variables (required for CI/CD).
|
|
141
|
+
|
|
142
|
+
The optional `THREADLINE_API_URL` allows you to point to a custom server if you want to intercept telemetry with your own endpoint. It can also be set with the `--api-url` flag.
|
|
172
143
|
|
|
173
144
|
## Threadline Files
|
|
174
145
|
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Environment Variables
|
|
4
|
+
|
|
5
|
+
The Threadlines CLI reads the following environment variables:
|
|
6
|
+
|
|
7
|
+
| Variable | Purpose | Required |
|
|
8
|
+
|----------|---------|----------|
|
|
9
|
+
| `THREADLINE_API_KEY` | Authentication with Threadlines API | Yes |
|
|
10
|
+
| `THREADLINE_ACCOUNT` | Your Threadlines account email | Yes |
|
|
11
|
+
| `THREADLINE_API_URL` | Custom API endpoint (default: https://devthreadline.com) | No |
|
|
12
|
+
|
|
13
|
+
### What We Do NOT Read
|
|
14
|
+
|
|
15
|
+
This CLI does **not** access, read, or transmit any other environment variables. We do not:
|
|
16
|
+
|
|
17
|
+
- Enumerate `process.env` to discover other secrets
|
|
18
|
+
- Read AWS, GCP, or other cloud credentials
|
|
19
|
+
- Access database connection strings
|
|
20
|
+
- Read any secrets beyond what's documented above
|
|
21
|
+
|
|
22
|
+
## Data Transmission
|
|
23
|
+
|
|
24
|
+
The CLI sends the following data to the Threadlines API:
|
|
25
|
+
|
|
26
|
+
1. **Git diff content** - The code changes being analyzed
|
|
27
|
+
2. **Threadline definitions** - Your markdown files from `/threadlines`
|
|
28
|
+
3. **Metadata** - Repository name, branch, commit SHA, context files specified in your threadline definitions
|
|
29
|
+
|
|
30
|
+
We do **not** send:
|
|
31
|
+
- Your entire codebase
|
|
32
|
+
- Environment variables or secrets
|
|
33
|
+
|
|
34
|
+
## Auditing This Package
|
|
35
|
+
|
|
36
|
+
To verify the published npm package matches this source:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Compare published package to source
|
|
40
|
+
npm pack threadlines
|
|
41
|
+
tar -xzf threadlines-*.tgz
|
|
42
|
+
diff -r package/dist dist/ # After building locally
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
All releases are published via GitHub Actions with npm provenance attestation,
|
|
46
|
+
which cryptographically links each npm package to its source commit.
|
|
47
|
+
|
package/dist/commands/check.js
CHANGED
|
@@ -46,7 +46,7 @@ const github_1 = require("../git/github");
|
|
|
46
46
|
const gitlab_1 = require("../git/gitlab");
|
|
47
47
|
const vercel_1 = require("../git/vercel");
|
|
48
48
|
const local_1 = require("../git/local");
|
|
49
|
-
const
|
|
49
|
+
const diff_1 = require("../git/diff");
|
|
50
50
|
const fs = __importStar(require("fs"));
|
|
51
51
|
const path = __importStar(require("path"));
|
|
52
52
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -122,7 +122,6 @@ async function checkCommand(options) {
|
|
|
122
122
|
}
|
|
123
123
|
// 2. Detect environment and context
|
|
124
124
|
const environment = (0, environment_1.detectEnvironment)();
|
|
125
|
-
let context;
|
|
126
125
|
let gitDiff;
|
|
127
126
|
let repoName;
|
|
128
127
|
let branchName;
|
|
@@ -138,25 +137,18 @@ async function checkCommand(options) {
|
|
|
138
137
|
if (options.file) {
|
|
139
138
|
console.log(chalk_1.default.gray(`📝 Reading file: ${options.file}...`));
|
|
140
139
|
gitDiff = await (0, file_1.getFileContent)(repoRoot, options.file);
|
|
141
|
-
context = { type: 'local' }; // File context doesn't need git context
|
|
142
|
-
// For file/folder/files, repo/branch are not available - skip them
|
|
143
140
|
}
|
|
144
141
|
else if (options.folder) {
|
|
145
142
|
console.log(chalk_1.default.gray(`📝 Reading folder: ${options.folder}...`));
|
|
146
143
|
gitDiff = await (0, file_1.getFolderContent)(repoRoot, options.folder);
|
|
147
|
-
context = { type: 'local' };
|
|
148
|
-
// For file/folder/files, repo/branch are not available - skip them
|
|
149
144
|
}
|
|
150
145
|
else if (options.files && options.files.length > 0) {
|
|
151
146
|
console.log(chalk_1.default.gray(`📝 Reading ${options.files.length} file(s)...`));
|
|
152
147
|
gitDiff = await (0, file_1.getMultipleFilesContent)(repoRoot, options.files);
|
|
153
|
-
context = { type: 'local' };
|
|
154
|
-
// For file/folder/files, repo/branch are not available - skip them
|
|
155
148
|
}
|
|
156
149
|
else if (options.branch) {
|
|
157
150
|
console.log(chalk_1.default.gray(`📝 Collecting git changes for branch: ${options.branch}...`));
|
|
158
|
-
|
|
159
|
-
gitDiff = await (0, git_diff_executor_1.getDiffForContext)(context, repoRoot, environment);
|
|
151
|
+
gitDiff = await (0, diff_1.getBranchDiff)(repoRoot, options.branch);
|
|
160
152
|
// Get repo/branch using environment-specific approach
|
|
161
153
|
if (environment === 'github') {
|
|
162
154
|
const gitContext = await (0, github_1.getGitHubContext)(repoRoot);
|
|
@@ -207,8 +199,7 @@ async function checkCommand(options) {
|
|
|
207
199
|
}
|
|
208
200
|
else if (options.commit) {
|
|
209
201
|
console.log(chalk_1.default.gray(`📝 Collecting git changes for commit: ${options.commit}...`));
|
|
210
|
-
|
|
211
|
-
gitDiff = await (0, git_diff_executor_1.getDiffForContext)(context, repoRoot, environment);
|
|
202
|
+
gitDiff = await (0, diff_1.getCommitDiff)(repoRoot, options.commit);
|
|
212
203
|
// Get repo/branch using environment-specific approach
|
|
213
204
|
if (environment === 'github') {
|
|
214
205
|
const gitContext = await (0, github_1.getGitHubContext)(repoRoot);
|
|
@@ -283,7 +274,6 @@ async function checkCommand(options) {
|
|
|
283
274
|
gitDiff = envContext.diff;
|
|
284
275
|
repoName = envContext.repoName;
|
|
285
276
|
branchName = envContext.branchName;
|
|
286
|
-
context = envContext.context;
|
|
287
277
|
// Use metadata from environment context
|
|
288
278
|
metadata = {
|
|
289
279
|
commitSha: envContext.commitSha,
|
|
@@ -294,9 +284,8 @@ async function checkCommand(options) {
|
|
|
294
284
|
};
|
|
295
285
|
}
|
|
296
286
|
if (gitDiff.changedFiles.length === 0) {
|
|
297
|
-
console.error(chalk_1.default.
|
|
298
|
-
|
|
299
|
-
process.exit(1);
|
|
287
|
+
console.error(chalk_1.default.bold('ℹ️ No changes detected.'));
|
|
288
|
+
process.exit(0);
|
|
300
289
|
}
|
|
301
290
|
// Check for zero diff (files changed but no actual code changes)
|
|
302
291
|
if (!gitDiff.diff || gitDiff.diff.trim() === '') {
|
|
@@ -7,9 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.getDiffForEnvironment = getDiffForEnvironment;
|
|
10
|
-
exports.getDiffForContext = getDiffForContext;
|
|
11
10
|
exports.getContextDescription = getContextDescription;
|
|
12
|
-
const diff_1 = require("../git/diff");
|
|
13
11
|
const vercel_diff_1 = require("../git/vercel-diff");
|
|
14
12
|
const github_diff_1 = require("../git/github-diff");
|
|
15
13
|
const local_diff_1 = require("../git/local-diff");
|
|
@@ -45,36 +43,6 @@ async function getDiffForEnvironment(environment, repoRoot, _context) {
|
|
|
45
43
|
throw new Error(`Unknown environment: ${_exhaustive}`);
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Legacy GitLab-specific diff function (deprecated).
|
|
50
|
-
*
|
|
51
|
-
* This function is kept for backward compatibility but should not be used.
|
|
52
|
-
* GitLab now uses getDiffForEnvironment() which routes to getGitLabDiff().
|
|
53
|
-
*
|
|
54
|
-
* @deprecated Use getDiffForEnvironment('gitlab', repoRoot) instead
|
|
55
|
-
*/
|
|
56
|
-
async function getDiffForContext(context, repoRoot, environment) {
|
|
57
|
-
// This should only be called for GitLab legacy code paths
|
|
58
|
-
if (environment !== 'gitlab') {
|
|
59
|
-
throw new Error(`getDiffForContext() is deprecated. Use getDiffForEnvironment('${environment}', repoRoot) instead.`);
|
|
60
|
-
}
|
|
61
|
-
switch (context.type) {
|
|
62
|
-
case 'pr':
|
|
63
|
-
return await (0, diff_1.getPRMRDiff)(repoRoot, context.sourceBranch, context.targetBranch);
|
|
64
|
-
case 'mr':
|
|
65
|
-
return await (0, diff_1.getPRMRDiff)(repoRoot, context.sourceBranch, context.targetBranch);
|
|
66
|
-
case 'branch':
|
|
67
|
-
return await (0, diff_1.getBranchDiff)(repoRoot, context.branchName);
|
|
68
|
-
case 'commit':
|
|
69
|
-
return await (0, diff_1.getCommitDiff)(repoRoot, context.commitSha);
|
|
70
|
-
case 'local':
|
|
71
|
-
throw new Error('Local context should use getDiffForEnvironment(), not getDiffForContext()');
|
|
72
|
-
default:
|
|
73
|
-
// TypeScript exhaustiveness check - should never reach here
|
|
74
|
-
const _exhaustive = context;
|
|
75
|
-
throw new Error(`Unknown context type: ${_exhaustive}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
46
|
/**
|
|
79
47
|
* Returns a human-readable description of the context for logging.
|
|
80
48
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "threadlines",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Threadlines CLI - AI-powered linter based on your natural language documentation",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"threadline": "./bin/threadline"
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
11
|
"bin",
|
|
12
|
-
"README.md"
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"SECURITY.md"
|
|
13
15
|
],
|
|
14
16
|
"keywords": [
|
|
15
17
|
"code-quality",
|
|
@@ -17,15 +19,20 @@
|
|
|
17
19
|
"ai",
|
|
18
20
|
"code-review",
|
|
19
21
|
"threadline",
|
|
20
|
-
"code-standards"
|
|
22
|
+
"code-standards",
|
|
23
|
+
"documentation",
|
|
24
|
+
"llm"
|
|
21
25
|
],
|
|
22
|
-
"author": "",
|
|
26
|
+
"author": "Threadlines",
|
|
23
27
|
"license": "MIT",
|
|
24
28
|
"repository": {
|
|
25
29
|
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/ngrootscholten/threadline.git"
|
|
27
|
-
"directory": "packages/cli"
|
|
30
|
+
"url": "git+https://github.com/ngrootscholten/threadline-cli.git"
|
|
28
31
|
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/ngrootscholten/threadline-cli/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://devthreadline.com/",
|
|
29
36
|
"scripts": {
|
|
30
37
|
"build": "npm run check && tsc",
|
|
31
38
|
"dev": "tsc --watch",
|
|
@@ -34,10 +41,10 @@
|
|
|
34
41
|
"lint": "eslint src/**/*.ts",
|
|
35
42
|
"lint:fix": "eslint src/**/*.ts --fix",
|
|
36
43
|
"typecheck": "tsc --noEmit",
|
|
37
|
-
"check": "npm run lint && npm run typecheck"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
44
|
+
"check": "npm run lint && npm run typecheck"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
41
48
|
},
|
|
42
49
|
"dependencies": {
|
|
43
50
|
"axios": "^1.7.9",
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getAutoReviewTarget = getAutoReviewTarget;
|
|
4
|
-
function getAutoReviewTarget() {
|
|
5
|
-
// 1. PR/MR context
|
|
6
|
-
if (process.env.GITHUB_EVENT_NAME === 'pull_request') {
|
|
7
|
-
const targetBranch = process.env.GITHUB_BASE_REF;
|
|
8
|
-
const sourceBranch = process.env.GITHUB_HEAD_REF;
|
|
9
|
-
const prNumber = process.env.GITHUB_EVENT_PULL_REQUEST_NUMBER || process.env.GITHUB_EVENT_NUMBER;
|
|
10
|
-
// Log PR detection for verification (remove after confirming it works)
|
|
11
|
-
console.log(`[PR Detection] GITHUB_BASE_REF=${targetBranch || 'NOT SET'}, GITHUB_HEAD_REF=${sourceBranch || 'NOT SET'}, PR_NUMBER=${prNumber || 'NOT SET'}`);
|
|
12
|
-
if (targetBranch && sourceBranch && prNumber) {
|
|
13
|
-
return {
|
|
14
|
-
type: 'pr',
|
|
15
|
-
value: prNumber,
|
|
16
|
-
sourceBranch,
|
|
17
|
-
targetBranch
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
if (process.env.CI_MERGE_REQUEST_IID) {
|
|
22
|
-
const targetBranch = process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME;
|
|
23
|
-
const sourceBranch = process.env.CI_MERGE_REQUEST_SOURCE_BRANCH_NAME;
|
|
24
|
-
const mrNumber = process.env.CI_MERGE_REQUEST_IID;
|
|
25
|
-
const mrTitle = process.env.CI_MERGE_REQUEST_TITLE;
|
|
26
|
-
if (targetBranch && sourceBranch && mrNumber) {
|
|
27
|
-
return {
|
|
28
|
-
type: 'mr',
|
|
29
|
-
value: mrNumber,
|
|
30
|
-
sourceBranch,
|
|
31
|
-
targetBranch,
|
|
32
|
-
prTitle: mrTitle || undefined
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
// 2. Branch name
|
|
37
|
-
const branch = process.env.GITHUB_REF_NAME ||
|
|
38
|
-
process.env.CI_COMMIT_REF_NAME ||
|
|
39
|
-
process.env.VERCEL_GIT_COMMIT_REF;
|
|
40
|
-
if (branch) {
|
|
41
|
-
return { type: 'branch', value: branch };
|
|
42
|
-
}
|
|
43
|
-
// 3. Commit SHA
|
|
44
|
-
const commit = process.env.GITHUB_SHA ||
|
|
45
|
-
process.env.CI_COMMIT_SHA ||
|
|
46
|
-
process.env.VERCEL_GIT_COMMIT_SHA;
|
|
47
|
-
if (commit) {
|
|
48
|
-
return { type: 'commit', value: commit };
|
|
49
|
-
}
|
|
50
|
-
// 4. Local development
|
|
51
|
-
return null;
|
|
52
|
-
}
|