@omnitype-code/cli 0.1.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/dist/blame.js +242 -0
- package/dist/core/ApiClient.js +234 -0
- package/dist/core/FileProvenance.js +483 -0
- package/dist/core/GitNotes.js +120 -0
- package/dist/core/Heartbeat.js +81 -0
- package/dist/core/ModelDetector.js +243 -0
- package/dist/core/ProvenanceResolver.js +424 -0
- package/dist/core/UI.js +97 -0
- package/dist/daemon.js +194 -0
- package/dist/hooks.js +220 -0
- package/dist/index.js +536 -0
- package/package.json +30 -0
- package/src/blame.ts +240 -0
- package/src/core/ApiClient.ts +197 -0
- package/src/core/FileProvenance.ts +538 -0
- package/src/core/GitNotes.ts +141 -0
- package/src/core/Heartbeat.ts +53 -0
- package/src/core/ModelDetector.ts +216 -0
- package/src/core/ProvenanceResolver.ts +433 -0
- package/src/core/UI.ts +105 -0
- package/src/daemon.ts +171 -0
- package/src/hooks.ts +195 -0
- package/src/index.ts +537 -0
- package/tsconfig.json +15 -0
package/dist/core/UI.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
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.UI = exports.COLORS = void 0;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const boxen_1 = __importDefault(require("boxen"));
|
|
9
|
+
const ora_1 = __importDefault(require("ora"));
|
|
10
|
+
const gradient_string_1 = __importDefault(require("gradient-string"));
|
|
11
|
+
exports.COLORS = {
|
|
12
|
+
primary: '#00D1FF',
|
|
13
|
+
secondary: '#7000FF',
|
|
14
|
+
success: '#00FF94',
|
|
15
|
+
warning: '#FFB800',
|
|
16
|
+
error: '#FF4D4D',
|
|
17
|
+
ai: '#BD00FF',
|
|
18
|
+
user: '#00D1FF',
|
|
19
|
+
paste: '#FFB800',
|
|
20
|
+
};
|
|
21
|
+
// Pixelated logo derived from the SVG: circle + dashed arc + center dot
|
|
22
|
+
const LOGO_PIXELS = [
|
|
23
|
+
' ▄▄▄▄▄▄▄▄▄▄▄ ',
|
|
24
|
+
' ▄█ █▄ ',
|
|
25
|
+
'██ ╌ ╌ ● ╌ ╌ ██',
|
|
26
|
+
' ▀█ █▀ ',
|
|
27
|
+
' ▀▀▀▀▀▀▀▀▀▀▀ ',
|
|
28
|
+
];
|
|
29
|
+
class UI {
|
|
30
|
+
static pixelLogo() {
|
|
31
|
+
return LOGO_PIXELS
|
|
32
|
+
.map(line => (0, gradient_string_1.default)([exports.COLORS.primary, exports.COLORS.secondary, exports.COLORS.ai])(line))
|
|
33
|
+
.join('\n');
|
|
34
|
+
}
|
|
35
|
+
static banner() {
|
|
36
|
+
const g = (s) => chalk_1.default.bold((0, gradient_string_1.default)([exports.COLORS.primary, exports.COLORS.secondary, exports.COLORS.ai])(s));
|
|
37
|
+
return [
|
|
38
|
+
'',
|
|
39
|
+
g(' ██████╗ ███╗ ███╗███╗ ██╗██╗'),
|
|
40
|
+
g('██╔═══██╗████╗ ████║████╗ ██║██║'),
|
|
41
|
+
g('██║ ██║██╔████╔██║██╔██╗ ██║██║'),
|
|
42
|
+
g('██║ ██║██║╚██╔╝██║██║╚██╗██║██║'),
|
|
43
|
+
g('╚██████╔╝██║ ╚═╝ ██║██║ ╚████║██║'),
|
|
44
|
+
g(' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝'),
|
|
45
|
+
chalk_1.default.hex(exports.COLORS.primary).dim(' code provenance · any editor · any model'),
|
|
46
|
+
'',
|
|
47
|
+
].join('\n');
|
|
48
|
+
}
|
|
49
|
+
static logo() {
|
|
50
|
+
return chalk_1.default.bold((0, gradient_string_1.default)(exports.COLORS.primary, exports.COLORS.secondary)('OmniType'));
|
|
51
|
+
}
|
|
52
|
+
static box(content, title) {
|
|
53
|
+
return (0, boxen_1.default)(content, {
|
|
54
|
+
padding: 1,
|
|
55
|
+
margin: 1,
|
|
56
|
+
borderStyle: 'round',
|
|
57
|
+
borderColor: exports.COLORS.primary,
|
|
58
|
+
title: title ? chalk_1.default.bold((0, gradient_string_1.default)(exports.COLORS.primary, exports.COLORS.secondary)(title)) : undefined,
|
|
59
|
+
titleAlignment: 'center',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
static spinner(text) {
|
|
63
|
+
return (0, ora_1.default)({
|
|
64
|
+
text,
|
|
65
|
+
color: 'cyan',
|
|
66
|
+
spinner: 'dots12',
|
|
67
|
+
}).start();
|
|
68
|
+
}
|
|
69
|
+
static error(message) {
|
|
70
|
+
console.error(chalk_1.default.hex(exports.COLORS.error)('✖ ') + message);
|
|
71
|
+
}
|
|
72
|
+
static success(message) {
|
|
73
|
+
console.log(chalk_1.default.hex(exports.COLORS.success)('✔ ') + message);
|
|
74
|
+
}
|
|
75
|
+
static info(message) {
|
|
76
|
+
console.log(chalk_1.default.hex(exports.COLORS.primary)('ℹ ') + message);
|
|
77
|
+
}
|
|
78
|
+
static dim(message) {
|
|
79
|
+
return chalk_1.default.gray(message);
|
|
80
|
+
}
|
|
81
|
+
static bold(message) {
|
|
82
|
+
return chalk_1.default.bold(message);
|
|
83
|
+
}
|
|
84
|
+
static label(text, color = exports.COLORS.primary) {
|
|
85
|
+
return chalk_1.default.bgHex(color).black(` ${text} `);
|
|
86
|
+
}
|
|
87
|
+
static bar(value, total, width = 20, color = exports.COLORS.primary) {
|
|
88
|
+
const filled = total === 0 ? 0 : Math.round((value / total) * width);
|
|
89
|
+
return chalk_1.default.hex(color)('█'.repeat(filled)) + chalk_1.default.gray('░'.repeat(width - filled));
|
|
90
|
+
}
|
|
91
|
+
static pct(value, total) {
|
|
92
|
+
const p = total === 0 ? 0 : (value / total) * 100;
|
|
93
|
+
const color = p > 60 ? exports.COLORS.ai : p > 30 ? exports.COLORS.warning : exports.COLORS.success;
|
|
94
|
+
return chalk_1.default.hex(color)(`${p.toFixed(1)}%`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
exports.UI = UI;
|
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
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.startDaemon = startDaemon;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
const chokidar_1 = __importDefault(require("chokidar"));
|
|
44
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
45
|
+
const ApiClient_1 = require("./core/ApiClient");
|
|
46
|
+
const ModelDetector_1 = require("./core/ModelDetector");
|
|
47
|
+
const FileProvenance_1 = require("./core/FileProvenance");
|
|
48
|
+
const Heartbeat_1 = require("./core/Heartbeat");
|
|
49
|
+
const UI_1 = require("./core/UI");
|
|
50
|
+
const PUSH_INTERVAL_MS = 60000;
|
|
51
|
+
const IDLE_PUSH_MS = 10000;
|
|
52
|
+
const IGNORE_DIRS = /[/\\](\.git|node_modules|\.next|dist|build|__pycache__|\.venv|venv)[/\\]/;
|
|
53
|
+
const TEXT_EXTS = new Set([
|
|
54
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
55
|
+
'.py', '.go', '.rs', '.java', '.kt', '.swift', '.c', '.cpp', '.h',
|
|
56
|
+
'.rb', '.php', '.cs', '.ex', '.exs', '.clj', '.scala', '.hs',
|
|
57
|
+
'.html', '.css', '.scss', '.less', '.vue', '.svelte',
|
|
58
|
+
'.json', '.yaml', '.yml', '.toml', '.md', '.mdx', '.txt',
|
|
59
|
+
'.sh', '.bash', '.zsh', '.fish', '.ps1',
|
|
60
|
+
]);
|
|
61
|
+
function hashLine(s) {
|
|
62
|
+
let h = 0x811c9dc5;
|
|
63
|
+
for (let i = 0; i < s.length; i++) {
|
|
64
|
+
h ^= s.charCodeAt(i);
|
|
65
|
+
h = (h * 0x01000193) >>> 0;
|
|
66
|
+
}
|
|
67
|
+
return h;
|
|
68
|
+
}
|
|
69
|
+
function toBaseline(content) {
|
|
70
|
+
return content.split('\n').map(l => ({ hash: hashLine(l), charLen: l.length + 1 }));
|
|
71
|
+
}
|
|
72
|
+
function gitBranch(repoPath) {
|
|
73
|
+
try {
|
|
74
|
+
return (0, child_process_1.execFileSync)('git', ['-C', repoPath, 'rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return 'main';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function isTextFile(filePath) {
|
|
81
|
+
return TEXT_EXTS.has(path.extname(filePath).toLowerCase());
|
|
82
|
+
}
|
|
83
|
+
function startDaemon(opts) {
|
|
84
|
+
const api = new ApiClient_1.ApiClient();
|
|
85
|
+
const detector = new ModelDetector_1.ModelDetector();
|
|
86
|
+
const baselines = new Map();
|
|
87
|
+
const dirtyFiles = new Set();
|
|
88
|
+
const provenance = new Map();
|
|
89
|
+
if (!api.isSignedIn) {
|
|
90
|
+
UI_1.UI.error('Not signed in. Run: omnitype login');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
const watchPath = path.resolve(opts.watchPath);
|
|
94
|
+
const projectName = opts.projectName;
|
|
95
|
+
const branch = opts.branch ?? gitBranch(watchPath);
|
|
96
|
+
console.log(UI_1.UI.box(`${chalk_1.default.bold('Project:')} ${chalk_1.default.cyan(projectName)}\n` +
|
|
97
|
+
`${chalk_1.default.bold('Branch:')} ${chalk_1.default.magenta(branch)}\n` +
|
|
98
|
+
`${chalk_1.default.bold('Path:')} ${UI_1.UI.dim(watchPath)}`, `${UI_1.UI.logo()} Sentinel Active`));
|
|
99
|
+
(0, Heartbeat_1.writeDaemonHeartbeat)(watchPath);
|
|
100
|
+
setInterval(() => (0, Heartbeat_1.writeDaemonHeartbeat)(watchPath), 10000);
|
|
101
|
+
function seedFile(filePath) {
|
|
102
|
+
if (!isTextFile(filePath) || IGNORE_DIRS.test(filePath))
|
|
103
|
+
return;
|
|
104
|
+
try {
|
|
105
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
106
|
+
baselines.set(filePath, toBaseline(content));
|
|
107
|
+
if (!provenance.has(filePath))
|
|
108
|
+
provenance.set(filePath, new FileProvenance_1.FileProvenance(0));
|
|
109
|
+
}
|
|
110
|
+
catch { /* unreadable */ }
|
|
111
|
+
}
|
|
112
|
+
function handleChange(filePath) {
|
|
113
|
+
if (!isTextFile(filePath) || IGNORE_DIRS.test(filePath))
|
|
114
|
+
return;
|
|
115
|
+
let content;
|
|
116
|
+
try {
|
|
117
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const newBaseline = toBaseline(content);
|
|
123
|
+
const oldBaseline = baselines.get(filePath) ?? [];
|
|
124
|
+
baselines.set(filePath, newBaseline);
|
|
125
|
+
if (oldBaseline.length === newBaseline.length &&
|
|
126
|
+
oldBaseline.every((b, i) => b.hash === newBaseline[i].hash))
|
|
127
|
+
return;
|
|
128
|
+
const detection = detector.detect(filePath);
|
|
129
|
+
const origin = detection.tool !== 'unknown' ? 'ai' : 'user';
|
|
130
|
+
let prov = provenance.get(filePath);
|
|
131
|
+
if (!prov) {
|
|
132
|
+
prov = new FileProvenance_1.FileProvenance(0);
|
|
133
|
+
provenance.set(filePath, prov);
|
|
134
|
+
}
|
|
135
|
+
const modelId = origin === 'ai' ? prov.internModel(detection.model) : undefined;
|
|
136
|
+
prov.applyLineDiff(oldBaseline, newBaseline, origin, modelId);
|
|
137
|
+
dirtyFiles.add(filePath);
|
|
138
|
+
const rel = path.relative(watchPath, filePath);
|
|
139
|
+
const added = newBaseline.length - oldBaseline.length;
|
|
140
|
+
const icon = origin === 'ai' ? chalk_1.default.hex(UI_1.COLORS.ai)('✦') : chalk_1.default.hex(UI_1.COLORS.user)('✎');
|
|
141
|
+
const modelTag = origin === 'ai' ? UI_1.UI.dim(` (${detection.model})`) : '';
|
|
142
|
+
const diffTag = added !== 0 ? ` ${added > 0 ? '+' : ''}${added} lines` : '';
|
|
143
|
+
console.log(`${icon} ${chalk_1.default.white(rel)}${diffTag}${modelTag}`);
|
|
144
|
+
}
|
|
145
|
+
let idleTimer = null;
|
|
146
|
+
async function flush() {
|
|
147
|
+
if (dirtyFiles.size === 0)
|
|
148
|
+
return;
|
|
149
|
+
if ((0, Heartbeat_1.extensionIsActiveFor)(watchPath)) {
|
|
150
|
+
UI_1.UI.info('Yielding to VS Code extension (active)');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const snapshot = {};
|
|
154
|
+
for (const f of dirtyFiles) {
|
|
155
|
+
const prov = provenance.get(f);
|
|
156
|
+
const baseline = baselines.get(f);
|
|
157
|
+
if (prov && baseline)
|
|
158
|
+
snapshot[path.relative(watchPath, f)] = prov.toStored(baseline);
|
|
159
|
+
}
|
|
160
|
+
dirtyFiles.clear();
|
|
161
|
+
try {
|
|
162
|
+
await api.pushProvenance(projectName, branch, snapshot, { source: 'cli-daemon' });
|
|
163
|
+
UI_1.UI.success(`Synced ${Object.keys(snapshot).length} file(s) to cloud`);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
UI_1.UI.error(`Sync failed: ${err}`);
|
|
167
|
+
for (const rel of Object.keys(snapshot))
|
|
168
|
+
dirtyFiles.add(path.join(watchPath, rel));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function scheduleIdleFlush() {
|
|
172
|
+
if (idleTimer)
|
|
173
|
+
clearTimeout(idleTimer);
|
|
174
|
+
idleTimer = setTimeout(() => { idleTimer = null; flush(); }, IDLE_PUSH_MS);
|
|
175
|
+
}
|
|
176
|
+
const watcher = chokidar_1.default.watch(watchPath, {
|
|
177
|
+
ignoreInitial: false,
|
|
178
|
+
ignored: (p) => IGNORE_DIRS.test(p),
|
|
179
|
+
persistent: true,
|
|
180
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 },
|
|
181
|
+
});
|
|
182
|
+
watcher
|
|
183
|
+
.on('add', (p) => seedFile(p))
|
|
184
|
+
.on('change', (p) => { handleChange(p); scheduleIdleFlush(); })
|
|
185
|
+
.on('unlink', (p) => { baselines.delete(p); provenance.delete(p); dirtyFiles.delete(p); })
|
|
186
|
+
.on('error', (err) => UI_1.UI.error(`Watcher error: ${err.message}`));
|
|
187
|
+
setInterval(flush, PUSH_INTERVAL_MS);
|
|
188
|
+
const cleanup = () => {
|
|
189
|
+
UI_1.UI.info('Shutting down...');
|
|
190
|
+
flush().finally(() => process.exit(0));
|
|
191
|
+
};
|
|
192
|
+
process.on('SIGINT', cleanup);
|
|
193
|
+
process.on('SIGTERM', cleanup);
|
|
194
|
+
}
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Git hook installer for omnitype.
|
|
4
|
+
*
|
|
5
|
+
* Installs a post-commit hook that calls `omnitype commit-scan` after each commit.
|
|
6
|
+
* Non-destructive: appends to existing hooks rather than replacing them.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.installHooks = installHooks;
|
|
43
|
+
exports.uninstallHooks = uninstallHooks;
|
|
44
|
+
exports.commitScan = commitScan;
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
const child_process_1 = require("child_process");
|
|
49
|
+
const ApiClient_1 = require("./core/ApiClient");
|
|
50
|
+
const ModelDetector_1 = require("./core/ModelDetector");
|
|
51
|
+
const FileProvenance_1 = require("./core/FileProvenance");
|
|
52
|
+
const GitNotes_1 = require("./core/GitNotes");
|
|
53
|
+
const MARKER = '# omnitype provenance — do not edit this block';
|
|
54
|
+
const MARKER_END = '# /omnitype';
|
|
55
|
+
function findGitDir(cwd) {
|
|
56
|
+
let dir = cwd;
|
|
57
|
+
for (let i = 0; i < 20; i++) {
|
|
58
|
+
const candidate = path.join(dir, '.git');
|
|
59
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory())
|
|
60
|
+
return candidate;
|
|
61
|
+
const parent = path.dirname(dir);
|
|
62
|
+
if (parent === dir)
|
|
63
|
+
return undefined;
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
function installHooks(cwd = process.cwd()) {
|
|
69
|
+
const gitDir = findGitDir(cwd);
|
|
70
|
+
if (!gitDir)
|
|
71
|
+
throw new Error('Not inside a git repository.');
|
|
72
|
+
const hooksDir = path.join(gitDir, 'hooks');
|
|
73
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
74
|
+
const hookFile = path.join(hooksDir, 'post-commit');
|
|
75
|
+
// Build hook snippet
|
|
76
|
+
const omnityeBin = process.execPath === process.argv[0]
|
|
77
|
+
? 'omnitype'
|
|
78
|
+
: `node "${process.argv[1]}"`;
|
|
79
|
+
const snippet = [
|
|
80
|
+
MARKER,
|
|
81
|
+
`${omnityeBin} commit-scan --repo "$(git rev-parse --show-toplevel)"`,
|
|
82
|
+
MARKER_END,
|
|
83
|
+
'',
|
|
84
|
+
].join('\n');
|
|
85
|
+
let existing = '';
|
|
86
|
+
try {
|
|
87
|
+
existing = fs.readFileSync(hookFile, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
catch { /* new file */ }
|
|
90
|
+
if (existing.includes(MARKER)) {
|
|
91
|
+
console.log('omnitype: git hook already installed.');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const content = existing
|
|
95
|
+
? `${existing.trimEnd()}\n\n${snippet}`
|
|
96
|
+
: `#!/bin/sh\n${snippet}`;
|
|
97
|
+
fs.writeFileSync(hookFile, content);
|
|
98
|
+
if (os.platform() !== 'win32')
|
|
99
|
+
fs.chmodSync(hookFile, 0o755);
|
|
100
|
+
console.log('omnitype: post-commit hook installed.');
|
|
101
|
+
}
|
|
102
|
+
function uninstallHooks(cwd = process.cwd()) {
|
|
103
|
+
const gitDir = findGitDir(cwd);
|
|
104
|
+
if (!gitDir)
|
|
105
|
+
throw new Error('Not inside a git repository.');
|
|
106
|
+
const hookFile = path.join(gitDir, 'hooks', 'post-commit');
|
|
107
|
+
let content;
|
|
108
|
+
try {
|
|
109
|
+
content = fs.readFileSync(hookFile, 'utf8');
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
console.log('omnitype: no hook file found.');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (!content.includes(MARKER)) {
|
|
116
|
+
console.log('omnitype: omnitype hook not found in post-commit.');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const start = content.indexOf(MARKER);
|
|
120
|
+
const end = content.indexOf(MARKER_END);
|
|
121
|
+
let updated = content.slice(0, start) + content.slice(end + MARKER_END.length);
|
|
122
|
+
updated = updated.replace(/\n{3,}/g, '\n\n').trim();
|
|
123
|
+
if (!updated || updated === '#!/bin/sh') {
|
|
124
|
+
fs.rmSync(hookFile);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
fs.writeFileSync(hookFile, updated + '\n');
|
|
128
|
+
}
|
|
129
|
+
console.log('omnitype: post-commit hook removed.');
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Called by the post-commit hook. Diffs HEAD against HEAD~1, attributing
|
|
133
|
+
* added lines as AI if a tool is currently active, otherwise as 'user'.
|
|
134
|
+
*/
|
|
135
|
+
async function commitScan(repoPath) {
|
|
136
|
+
const api = new ApiClient_1.ApiClient();
|
|
137
|
+
// Get the commit hash and message
|
|
138
|
+
const commitHash = run('git', ['-C', repoPath, 'rev-parse', 'HEAD']).trim();
|
|
139
|
+
const commitMsg = run('git', ['-C', repoPath, 'log', '-1', '--format=%s']).trim();
|
|
140
|
+
const branch = run('git', ['-C', repoPath, 'rev-parse', '--abbrev-ref', 'HEAD']).trim();
|
|
141
|
+
const projectName = path.basename(repoPath);
|
|
142
|
+
// Detect the active model
|
|
143
|
+
const detector = new ModelDetector_1.ModelDetector();
|
|
144
|
+
const detection = detector.detect();
|
|
145
|
+
// Get diff of committed files (HEAD vs HEAD~1, or HEAD vs empty if first commit)
|
|
146
|
+
let diffOutput;
|
|
147
|
+
try {
|
|
148
|
+
diffOutput = run('git', ['-C', repoPath, 'diff', '--unified=0', 'HEAD~1', 'HEAD']);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// First commit — diff against empty tree
|
|
152
|
+
diffOutput = run('git', ['-C', repoPath, 'diff', '--unified=0', '4b825dc642cb6eb9a060e54bf8d69288fbee4904', 'HEAD']);
|
|
153
|
+
}
|
|
154
|
+
const files = parseDiffToProvenance(diffOutput, detection.tool !== 'unknown' ? 'ai' : 'user', detection.model);
|
|
155
|
+
if (Object.keys(files).length === 0) {
|
|
156
|
+
process.stdout.write('omnitype: nothing to record for this commit.\n');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
// Write Git Note first (local, always works even offline)
|
|
160
|
+
try {
|
|
161
|
+
const noteFile = (0, GitNotes_1.provenanceToNoteFile)(files);
|
|
162
|
+
const note = (0, GitNotes_1.buildNote)(noteFile);
|
|
163
|
+
await (0, GitNotes_1.writeNote)(repoPath, commitHash, note);
|
|
164
|
+
process.stdout.write(`omnitype: git note written for ${commitHash.slice(0, 7)}\n`);
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
process.stderr.write(`omnitype: git note failed — ${err}\n`);
|
|
168
|
+
}
|
|
169
|
+
// Push to cloud (best-effort)
|
|
170
|
+
if (api.isSignedIn) {
|
|
171
|
+
try {
|
|
172
|
+
await api.pushProvenance(projectName, branch, files, { commitHash, commitMessage: commitMsg, source: 'cli-hook' });
|
|
173
|
+
process.stdout.write(`omnitype: pushed ${Object.keys(files).length} file(s) to cloud\n`);
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
process.stderr.write(`omnitype: cloud push failed — ${err}\n`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function run(cmd, args) {
|
|
181
|
+
return (0, child_process_1.execFileSync)(cmd, args, { encoding: 'utf8' });
|
|
182
|
+
}
|
|
183
|
+
function hashLine(s) {
|
|
184
|
+
let h = 0x811c9dc5;
|
|
185
|
+
for (let i = 0; i < s.length; i++) {
|
|
186
|
+
h ^= s.charCodeAt(i);
|
|
187
|
+
h = (h * 0x01000193) >>> 0;
|
|
188
|
+
}
|
|
189
|
+
return h;
|
|
190
|
+
}
|
|
191
|
+
function parseDiffToProvenance(diff, origin, model) {
|
|
192
|
+
// Collect added lines per file, then build provenance from scratch
|
|
193
|
+
const fileLines = {};
|
|
194
|
+
let currentFile = null;
|
|
195
|
+
for (const line of diff.split('\n')) {
|
|
196
|
+
if (line.startsWith('+++ b/')) {
|
|
197
|
+
currentFile = line.slice(6).trim();
|
|
198
|
+
if (!fileLines[currentFile])
|
|
199
|
+
fileLines[currentFile] = [];
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!currentFile)
|
|
203
|
+
continue;
|
|
204
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
205
|
+
fileLines[currentFile].push(line.slice(1));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const result = {};
|
|
209
|
+
for (const [relPath, lines] of Object.entries(fileLines)) {
|
|
210
|
+
if (lines.length === 0)
|
|
211
|
+
continue;
|
|
212
|
+
const prov = new FileProvenance_1.FileProvenance(0);
|
|
213
|
+
const modelId = origin === 'ai' ? prov.internModel(model) : undefined;
|
|
214
|
+
const baseline = lines.map(l => ({ hash: hashLine(l), charLen: l.length + 1 }));
|
|
215
|
+
// Build the baseline first, then apply a diff from empty → lines
|
|
216
|
+
prov.applyLineDiff([], baseline, origin, modelId);
|
|
217
|
+
result[relPath] = prov.toStored(baseline);
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|