@devtrace-ai/cli 1.0.0
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 +21 -0
- package/README.md +96 -0
- package/dist/analyzer.js +163 -0
- package/dist/capture/claude.js +219 -0
- package/dist/capture/trailers.js +112 -0
- package/dist/card.js +142 -0
- package/dist/chat.js +266 -0
- package/dist/claude-correlate.js +150 -0
- package/dist/claude-hook-install.js +183 -0
- package/dist/claude-hooks.js +236 -0
- package/dist/cli.js +113 -0
- package/dist/config.js +174 -0
- package/dist/core/scan.js +265 -0
- package/dist/core/survival.js +400 -0
- package/dist/detector.js +92 -0
- package/dist/digest.js +183 -0
- package/dist/git.js +212 -0
- package/dist/hooks.js +166 -0
- package/dist/html_report.js +325 -0
- package/dist/ignore.js +126 -0
- package/dist/index.js +798 -0
- package/dist/metrics.js +133 -0
- package/dist/reporter.js +127 -0
- package/dist/storage.js +344 -0
- package/dist/test_runner.js +143 -0
- package/dist/types.js +2 -0
- package/package.json +53 -0
package/dist/git.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.GitTracker = void 0;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
class GitTracker {
|
|
40
|
+
static runGitCommand(args, cwd = process.cwd()) {
|
|
41
|
+
try {
|
|
42
|
+
const result = (0, child_process_1.spawnSync)('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
43
|
+
if (result.error) {
|
|
44
|
+
throw result.error;
|
|
45
|
+
}
|
|
46
|
+
if (result.status !== 0) {
|
|
47
|
+
throw new Error(`Exit code ${result.status}: ${result.stderr.trim()}`);
|
|
48
|
+
}
|
|
49
|
+
return result.stdout.trim();
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new Error(`Git command failed: git ${args.join(' ')}. Error: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolves the top-level repository root directory.
|
|
57
|
+
* Falls back to cwd if not inside a git repository.
|
|
58
|
+
*/
|
|
59
|
+
static getRepositoryRoot(cwd = process.cwd()) {
|
|
60
|
+
try {
|
|
61
|
+
return this.runGitCommand(['rev-parse', '--show-toplevel'], cwd);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return cwd;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolves empty tree hash dynamically using `git hash-object --stdin`.
|
|
69
|
+
* Works on both SHA-1 and SHA-256 repositories and is fully cross-platform.
|
|
70
|
+
*/
|
|
71
|
+
static getEmptyTreeHash(cwd = process.cwd()) {
|
|
72
|
+
try {
|
|
73
|
+
const result = (0, child_process_1.spawnSync)('git', ['hash-object', '-t', 'tree', '--stdin'], {
|
|
74
|
+
cwd,
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
input: '',
|
|
77
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
78
|
+
});
|
|
79
|
+
if (result.status === 0 && result.stdout) {
|
|
80
|
+
return result.stdout.trim();
|
|
81
|
+
}
|
|
82
|
+
throw new Error(result.stderr);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Standard fallback for SHA-1
|
|
86
|
+
return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Checks if the commit is a merge commit (has more than one parent).
|
|
91
|
+
*/
|
|
92
|
+
static isMergeCommit(commitHash, cwd = process.cwd()) {
|
|
93
|
+
try {
|
|
94
|
+
const parentLine = this.runGitCommand(['rev-list', '--parents', '-n', '1', commitHash], cwd);
|
|
95
|
+
const commits = parentLine.split(/\s+/);
|
|
96
|
+
return commits.length > 2;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolves a reference (e.g. "HEAD", "HEAD~1") to a commit hash.
|
|
104
|
+
* Returns null if reference cannot be resolved.
|
|
105
|
+
*/
|
|
106
|
+
static resolveRef(ref, cwd = process.cwd()) {
|
|
107
|
+
try {
|
|
108
|
+
return this.runGitCommand(['rev-parse', '--verify', ref], cwd);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Resolves a commit reference to its tree hash.
|
|
116
|
+
*/
|
|
117
|
+
static getTreeHash(ref, cwd = process.cwd()) {
|
|
118
|
+
try {
|
|
119
|
+
return this.runGitCommand(['rev-parse', `${ref}^{tree}`], cwd);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return this.getEmptyTreeHash(cwd);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Returns the current branch name.
|
|
127
|
+
*/
|
|
128
|
+
static getCurrentBranch(cwd = process.cwd()) {
|
|
129
|
+
try {
|
|
130
|
+
return this.runGitCommand(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return 'detached';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Retrieves metadata of a commit (author, timestamp, commit message).
|
|
138
|
+
*/
|
|
139
|
+
static getCommitMetadata(commitHash, cwd = process.cwd()) {
|
|
140
|
+
try {
|
|
141
|
+
const raw = this.runGitCommand(['show', '-s', '--format=%an|%aI|%B', commitHash], cwd);
|
|
142
|
+
const parts = raw.split('|');
|
|
143
|
+
const author = parts[0] || 'Unknown';
|
|
144
|
+
const timestamp = parts[1] || new Date().toISOString();
|
|
145
|
+
const message = parts.slice(2).join('|');
|
|
146
|
+
return { author, timestamp, message };
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return { author: 'Unknown', timestamp: new Date().toISOString(), message: '' };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Compares two tree/commit hashes and returns a structured DiffMetrics layout.
|
|
154
|
+
*/
|
|
155
|
+
static getDiff(beforeRef, afterRef, cwd = process.cwd()) {
|
|
156
|
+
const emptyTree = this.getEmptyTreeHash(cwd);
|
|
157
|
+
const before = beforeRef || emptyTree;
|
|
158
|
+
const after = afterRef;
|
|
159
|
+
try {
|
|
160
|
+
const numstatRaw = this.runGitCommand(['diff-tree', '-r', '--no-commit-id', '--numstat', before, after], cwd);
|
|
161
|
+
const fileMetrics = new Map();
|
|
162
|
+
if (numstatRaw) {
|
|
163
|
+
const lines = numstatRaw.split('\n');
|
|
164
|
+
for (const line of lines) {
|
|
165
|
+
const parts = line.trim().split(/\s+/);
|
|
166
|
+
if (parts.length < 3)
|
|
167
|
+
continue;
|
|
168
|
+
const insRaw = parts[0];
|
|
169
|
+
const delRaw = parts[1];
|
|
170
|
+
const filepath = parts.slice(2).join(' ');
|
|
171
|
+
const isBinary = insRaw === '-' || delRaw === '-';
|
|
172
|
+
const insertions = isBinary ? 0 : parseInt(insRaw, 10);
|
|
173
|
+
const deletions = isBinary ? 0 : parseInt(delRaw, 10);
|
|
174
|
+
fileMetrics.set(filepath, { insertions, deletions, isBinary });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const files = [];
|
|
178
|
+
let totalInsertions = 0;
|
|
179
|
+
let totalDeletions = 0;
|
|
180
|
+
for (const [filepath, metrics] of fileMetrics.entries()) {
|
|
181
|
+
const ext = path.extname(filepath).toLowerCase().replace(/^\./, '');
|
|
182
|
+
files.push({
|
|
183
|
+
path: filepath,
|
|
184
|
+
insertions: metrics.insertions,
|
|
185
|
+
deletions: metrics.deletions,
|
|
186
|
+
isBinary: metrics.isBinary,
|
|
187
|
+
language: ext || null
|
|
188
|
+
});
|
|
189
|
+
totalInsertions += metrics.insertions;
|
|
190
|
+
totalDeletions += metrics.deletions;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
filesChanged: files.length,
|
|
194
|
+
totalInsertions,
|
|
195
|
+
totalDeletions,
|
|
196
|
+
netLines: totalInsertions - totalDeletions,
|
|
197
|
+
files
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
console.error(`[DevTrace] Diff calculation error: ${error.message}`);
|
|
202
|
+
return {
|
|
203
|
+
filesChanged: 0,
|
|
204
|
+
totalInsertions: 0,
|
|
205
|
+
totalDeletions: 0,
|
|
206
|
+
netLines: 0,
|
|
207
|
+
files: []
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
exports.GitTracker = GitTracker;
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.HookManager = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
class HookManager {
|
|
41
|
+
static HOOK_START_MARKER = '### DEVTRACE-AI START ###';
|
|
42
|
+
static HOOK_END_MARKER = '### DEVTRACE-AI END ###';
|
|
43
|
+
/**
|
|
44
|
+
* Resolves an absolute invocation path for the currently-running DevTrace
|
|
45
|
+
* CLI (node binary + absolute script path). Baking this into the hook at
|
|
46
|
+
* install time avoids a per-commit `npx` resolution, which hits the
|
|
47
|
+
* network/registry and silently fails in GUI git clients without a
|
|
48
|
+
* PATH-aware shell.
|
|
49
|
+
*/
|
|
50
|
+
static resolveInvocation() {
|
|
51
|
+
const execPath = process.execPath;
|
|
52
|
+
const scriptPath = path.resolve(require.main?.filename ?? process.argv[1]);
|
|
53
|
+
return { execPath, scriptPath };
|
|
54
|
+
}
|
|
55
|
+
static getHookScript() {
|
|
56
|
+
const { execPath, scriptPath } = this.resolveInvocation();
|
|
57
|
+
return [
|
|
58
|
+
this.HOOK_START_MARKER,
|
|
59
|
+
'# DevTrace-AI automatic commit telemetry capture',
|
|
60
|
+
'# Runs asynchronously in background to ensure zero commit delay',
|
|
61
|
+
'# Invocation path is resolved to an absolute path at install time so',
|
|
62
|
+
'# this hook never has to shell out to npx (no network/registry hit,',
|
|
63
|
+
'# works in GUI git clients without PATH hacks).',
|
|
64
|
+
'(',
|
|
65
|
+
' REPO_DIR="$(git rev-parse --show-toplevel)"',
|
|
66
|
+
' cd "$REPO_DIR" || exit',
|
|
67
|
+
' mkdir -p .devtrace',
|
|
68
|
+
` if [ -x "${scriptPath}" ] || [ -f "${scriptPath}" ]; then`,
|
|
69
|
+
` "${execPath}" "${scriptPath}" post-commit >/dev/null 2>>.devtrace/hook.log`,
|
|
70
|
+
' elif [ -f "dist/index.js" ]; then',
|
|
71
|
+
' node dist/index.js post-commit >/dev/null 2>>.devtrace/hook.log',
|
|
72
|
+
' else',
|
|
73
|
+
' npx devtrace post-commit >/dev/null 2>>.devtrace/hook.log',
|
|
74
|
+
' fi',
|
|
75
|
+
') </dev/null &',
|
|
76
|
+
this.HOOK_END_MARKER
|
|
77
|
+
].join('\n');
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolves the active hooks directory dynamically via git rev-parse.
|
|
81
|
+
* Works correctly for standard repos, submodules, and git worktrees.
|
|
82
|
+
*/
|
|
83
|
+
static getHooksDirectory(projectDir = process.cwd()) {
|
|
84
|
+
const result = (0, child_process_1.spawnSync)('git', ['rev-parse', '--git-path', 'hooks'], {
|
|
85
|
+
cwd: projectDir,
|
|
86
|
+
encoding: 'utf8',
|
|
87
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
88
|
+
});
|
|
89
|
+
if (result.status !== 0 || !result.stdout.trim()) {
|
|
90
|
+
throw new Error(`Directory is not a Git repository: ${projectDir}`);
|
|
91
|
+
}
|
|
92
|
+
return path.resolve(projectDir, result.stdout.trim());
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Installs the post-commit git hook into the target project.
|
|
96
|
+
* Safe to run repeatedly (updates block if exists).
|
|
97
|
+
*/
|
|
98
|
+
static installHook(projectDir = process.cwd()) {
|
|
99
|
+
const hooksDir = this.getHooksDirectory(projectDir);
|
|
100
|
+
if (!fs.existsSync(hooksDir)) {
|
|
101
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
const hookPath = path.join(hooksDir, 'post-commit');
|
|
104
|
+
let content = '';
|
|
105
|
+
if (fs.existsSync(hookPath)) {
|
|
106
|
+
content = fs.readFileSync(hookPath, 'utf8');
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
content = '#!/bin/sh\n';
|
|
110
|
+
}
|
|
111
|
+
// Check if marker already exists
|
|
112
|
+
const startIndex = content.indexOf(this.HOOK_START_MARKER);
|
|
113
|
+
const endIndex = content.indexOf(this.HOOK_END_MARKER);
|
|
114
|
+
const hookCode = this.getHookScript();
|
|
115
|
+
if (startIndex !== -1 && endIndex !== -1) {
|
|
116
|
+
// Replace existing block
|
|
117
|
+
content =
|
|
118
|
+
content.substring(0, startIndex) +
|
|
119
|
+
hookCode +
|
|
120
|
+
content.substring(endIndex + this.HOOK_END_MARKER.length);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
// Append block
|
|
124
|
+
content = content.endsWith('\n') ? content + hookCode + '\n' : content + '\n' + hookCode + '\n';
|
|
125
|
+
}
|
|
126
|
+
fs.writeFileSync(hookPath, content, { encoding: 'utf8', mode: 0o755 });
|
|
127
|
+
return hookPath;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Removes only the DevTrace block from the post-commit hook.
|
|
131
|
+
*/
|
|
132
|
+
static uninstallHook(projectDir = process.cwd()) {
|
|
133
|
+
let hooksDir;
|
|
134
|
+
try {
|
|
135
|
+
hooksDir = this.getHooksDirectory(projectDir);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false; // Not a git repo or git not found
|
|
139
|
+
}
|
|
140
|
+
const hookPath = path.join(hooksDir, 'post-commit');
|
|
141
|
+
if (!fs.existsSync(hookPath)) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
let content = fs.readFileSync(hookPath, 'utf8');
|
|
145
|
+
const startIndex = content.indexOf(this.HOOK_START_MARKER);
|
|
146
|
+
const endIndex = content.indexOf(this.HOOK_END_MARKER);
|
|
147
|
+
if (startIndex === -1 || endIndex === -1) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
// Remove the block
|
|
151
|
+
content =
|
|
152
|
+
content.substring(0, startIndex).trim() +
|
|
153
|
+
'\n' +
|
|
154
|
+
content.substring(endIndex + this.HOOK_END_MARKER.length).trim();
|
|
155
|
+
content = content.trim();
|
|
156
|
+
if (content === '#!/bin/sh' || content === '') {
|
|
157
|
+
// If file contains only our hook and standard header, clean it up entirely
|
|
158
|
+
fs.unlinkSync(hookPath);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
fs.writeFileSync(hookPath, content + '\n', { encoding: 'utf8', mode: 0o755 });
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
exports.HookManager = HookManager;
|