sentix 2.0.1
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 +627 -0
- package/bin/sentix.js +116 -0
- package/package.json +37 -0
- package/src/CLAUDE.md +26 -0
- package/src/commands/CLAUDE.md +29 -0
- package/src/commands/context.js +227 -0
- package/src/commands/doctor.js +213 -0
- package/src/commands/evolve.js +203 -0
- package/src/commands/feature.js +327 -0
- package/src/commands/init.js +467 -0
- package/src/commands/metrics.js +170 -0
- package/src/commands/plugin.js +111 -0
- package/src/commands/run.js +303 -0
- package/src/commands/safety.js +163 -0
- package/src/commands/status.js +149 -0
- package/src/commands/ticket.js +362 -0
- package/src/commands/update.js +143 -0
- package/src/commands/version.js +218 -0
- package/src/context.js +104 -0
- package/src/dev-server.js +154 -0
- package/src/lib/agent-loop.js +110 -0
- package/src/lib/api-client.js +213 -0
- package/src/lib/changelog.js +110 -0
- package/src/lib/pipeline.js +218 -0
- package/src/lib/provider.js +129 -0
- package/src/lib/safety.js +146 -0
- package/src/lib/semver.js +40 -0
- package/src/lib/similarity.js +58 -0
- package/src/lib/ticket-index.js +137 -0
- package/src/lib/tools.js +142 -0
- package/src/lib/verify-gates.js +254 -0
- package/src/plugins/auto-version.js +89 -0
- package/src/plugins/logger.js +55 -0
- package/src/registry.js +63 -0
- package/src/version.js +15 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* verify-gates.js — 하드 룰 검증 게이트
|
|
3
|
+
*
|
|
4
|
+
* 에이전트 작업 완료 후 git diff를 분석하여 하드 룰 위반 여부를 코드로 검증한다.
|
|
5
|
+
* "AI에게 부탁하는 규칙"이 아닌 "코드가 강제하는 게이트".
|
|
6
|
+
*
|
|
7
|
+
* 검증 항목:
|
|
8
|
+
* #2 SCOPE 준수 — 변경 파일이 허용 범위 안에 있는가
|
|
9
|
+
* #3 export 삭제 금지 — export 키워드가 삭제되었는가
|
|
10
|
+
* #4 테스트 삭제 금지 — 테스트 파일에서 테스트가 삭제되었는가
|
|
11
|
+
* #5 순삭제 50줄 — net deletions이 50줄을 넘는가
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { execSync } from 'node:child_process';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run all verification gates against the current git diff.
|
|
18
|
+
* @param {string} cwd - Working directory
|
|
19
|
+
* @param {object} [options]
|
|
20
|
+
* @param {string[]} [options.scope] - Allowed file patterns (glob-like). If empty, scope check is skipped.
|
|
21
|
+
* @returns {object} Gate results
|
|
22
|
+
*/
|
|
23
|
+
export function runGates(cwd, options = {}) {
|
|
24
|
+
const results = {
|
|
25
|
+
passed: true,
|
|
26
|
+
checks: [],
|
|
27
|
+
violations: [],
|
|
28
|
+
summary: '',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let diff;
|
|
32
|
+
try {
|
|
33
|
+
diff = getDiff(cwd);
|
|
34
|
+
} catch {
|
|
35
|
+
// No git repo or no changes — all gates pass trivially
|
|
36
|
+
results.summary = 'No git changes detected — gates skipped';
|
|
37
|
+
return results;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!diff.files.length) {
|
|
41
|
+
results.summary = 'No file changes — gates skipped';
|
|
42
|
+
return results;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Gate #2: SCOPE compliance
|
|
46
|
+
const scopeResult = checkScope(diff, options.scope);
|
|
47
|
+
results.checks.push(scopeResult);
|
|
48
|
+
if (!scopeResult.passed) {
|
|
49
|
+
results.passed = false;
|
|
50
|
+
results.violations.push(...scopeResult.violations);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Gate #3: No export deletion
|
|
54
|
+
const exportResult = checkExportDeletion(diff);
|
|
55
|
+
results.checks.push(exportResult);
|
|
56
|
+
if (!exportResult.passed) {
|
|
57
|
+
results.passed = false;
|
|
58
|
+
results.violations.push(...exportResult.violations);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Gate #4: No test deletion
|
|
62
|
+
const testResult = checkTestDeletion(diff);
|
|
63
|
+
results.checks.push(testResult);
|
|
64
|
+
if (!testResult.passed) {
|
|
65
|
+
results.passed = false;
|
|
66
|
+
results.violations.push(...testResult.violations);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Gate #5: Net deletion limit (50 lines)
|
|
70
|
+
const deletionResult = checkNetDeletion(diff);
|
|
71
|
+
results.checks.push(deletionResult);
|
|
72
|
+
if (!deletionResult.passed) {
|
|
73
|
+
results.passed = false;
|
|
74
|
+
results.violations.push(...deletionResult.violations);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const passCount = results.checks.filter(c => c.passed).length;
|
|
78
|
+
results.summary = `${passCount}/${results.checks.length} gates passed`;
|
|
79
|
+
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Git diff parsing ──────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function getDiff(cwd) {
|
|
86
|
+
const numstat = execSync('git diff --numstat HEAD 2>/dev/null || git diff --numstat', {
|
|
87
|
+
cwd,
|
|
88
|
+
encoding: 'utf-8',
|
|
89
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
90
|
+
timeout: 10000,
|
|
91
|
+
}).trim();
|
|
92
|
+
|
|
93
|
+
const diffContent = execSync('git diff HEAD 2>/dev/null || git diff', {
|
|
94
|
+
cwd,
|
|
95
|
+
encoding: 'utf-8',
|
|
96
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
97
|
+
timeout: 10000,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const files = [];
|
|
101
|
+
let totalAdded = 0;
|
|
102
|
+
let totalDeleted = 0;
|
|
103
|
+
|
|
104
|
+
for (const line of numstat.split('\n')) {
|
|
105
|
+
if (!line.trim()) continue;
|
|
106
|
+
const [added, deleted, file] = line.split('\t');
|
|
107
|
+
const a = parseInt(added) || 0;
|
|
108
|
+
const d = parseInt(deleted) || 0;
|
|
109
|
+
files.push({ file, added: a, deleted: d });
|
|
110
|
+
totalAdded += a;
|
|
111
|
+
totalDeleted += d;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Extract deleted lines from full diff
|
|
115
|
+
const deletedLines = [];
|
|
116
|
+
let currentFile = null;
|
|
117
|
+
for (const line of diffContent.split('\n')) {
|
|
118
|
+
if (line.startsWith('diff --git')) {
|
|
119
|
+
const match = line.match(/b\/(.+)$/);
|
|
120
|
+
currentFile = match ? match[1] : null;
|
|
121
|
+
} else if (line.startsWith('-') && !line.startsWith('---') && currentFile) {
|
|
122
|
+
deletedLines.push({ file: currentFile, line: line.slice(1) });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { files, totalAdded, totalDeleted, deletedLines };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Gate #2: SCOPE compliance ─────────────────────────
|
|
130
|
+
|
|
131
|
+
function checkScope(diff, scope) {
|
|
132
|
+
const result = { rule: 'scope', passed: true, violations: [], detail: '' };
|
|
133
|
+
|
|
134
|
+
if (!scope || scope.length === 0) {
|
|
135
|
+
result.detail = 'No scope defined — skipped';
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const { file } of diff.files) {
|
|
140
|
+
if (!matchesScope(file, scope)) {
|
|
141
|
+
result.passed = false;
|
|
142
|
+
result.violations.push({
|
|
143
|
+
rule: 'scope',
|
|
144
|
+
message: `File outside SCOPE: ${file}`,
|
|
145
|
+
file,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
result.detail = result.passed
|
|
151
|
+
? `${diff.files.length} files within scope`
|
|
152
|
+
: `${result.violations.length} file(s) outside scope`;
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function matchesScope(file, patterns) {
|
|
158
|
+
for (const pattern of patterns) {
|
|
159
|
+
if (pattern.endsWith('/**')) {
|
|
160
|
+
const dir = pattern.slice(0, -3);
|
|
161
|
+
if (file.startsWith(dir + '/') || file === dir) return true;
|
|
162
|
+
} else if (pattern.endsWith('/*')) {
|
|
163
|
+
const dir = pattern.slice(0, -2);
|
|
164
|
+
if (file.startsWith(dir + '/') && !file.slice(dir.length + 1).includes('/')) return true;
|
|
165
|
+
} else if (file === pattern) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Gate #3: No export deletion ───────────────────────
|
|
173
|
+
|
|
174
|
+
function checkExportDeletion(diff) {
|
|
175
|
+
const result = { rule: 'no-export-deletion', passed: true, violations: [], detail: '' };
|
|
176
|
+
|
|
177
|
+
const exportPattern = /^export\s+(function|const|let|var|class|default|async)/;
|
|
178
|
+
const deletedExports = diff.deletedLines.filter(
|
|
179
|
+
d => exportPattern.test(d.line.trim()) && !isTestFile(d.file)
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (deletedExports.length > 0) {
|
|
183
|
+
result.passed = false;
|
|
184
|
+
for (const d of deletedExports) {
|
|
185
|
+
result.violations.push({
|
|
186
|
+
rule: 'no-export-deletion',
|
|
187
|
+
message: `Export deleted in ${d.file}: ${d.line.trim().substring(0, 60)}`,
|
|
188
|
+
file: d.file,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
result.detail = result.passed
|
|
194
|
+
? 'No exports deleted'
|
|
195
|
+
: `${deletedExports.length} export(s) deleted`;
|
|
196
|
+
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Gate #4: No test deletion ─────────────────────────
|
|
201
|
+
|
|
202
|
+
function checkTestDeletion(diff) {
|
|
203
|
+
const result = { rule: 'no-test-deletion', passed: true, violations: [], detail: '' };
|
|
204
|
+
|
|
205
|
+
const testPattern = /\b(describe|it|test)\s*\(/;
|
|
206
|
+
const deletedTests = diff.deletedLines.filter(
|
|
207
|
+
d => isTestFile(d.file) && testPattern.test(d.line)
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
if (deletedTests.length > 0) {
|
|
211
|
+
result.passed = false;
|
|
212
|
+
for (const d of deletedTests) {
|
|
213
|
+
result.violations.push({
|
|
214
|
+
rule: 'no-test-deletion',
|
|
215
|
+
message: `Test deleted in ${d.file}: ${d.line.trim().substring(0, 60)}`,
|
|
216
|
+
file: d.file,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
result.detail = result.passed
|
|
222
|
+
? 'No tests deleted'
|
|
223
|
+
: `${deletedTests.length} test(s) deleted`;
|
|
224
|
+
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isTestFile(file) {
|
|
229
|
+
return file.includes('__tests__/') ||
|
|
230
|
+
file.includes('.test.') ||
|
|
231
|
+
file.includes('.spec.') ||
|
|
232
|
+
file.includes('test/');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Gate #5: Net deletion limit ───────────────────────
|
|
236
|
+
|
|
237
|
+
function checkNetDeletion(diff, limit = 50) {
|
|
238
|
+
const result = { rule: 'net-deletion-limit', passed: true, violations: [], detail: '' };
|
|
239
|
+
|
|
240
|
+
const netDeletions = diff.totalDeleted - diff.totalAdded;
|
|
241
|
+
|
|
242
|
+
if (netDeletions > limit) {
|
|
243
|
+
result.passed = false;
|
|
244
|
+
result.violations.push({
|
|
245
|
+
rule: 'net-deletion-limit',
|
|
246
|
+
message: `Net deletions: ${netDeletions} (limit: ${limit})`,
|
|
247
|
+
net_deletions: netDeletions,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
result.detail = `Net: +${diff.totalAdded} -${diff.totalDeleted} (net ${netDeletions > 0 ? '+' : ''}${-netDeletions})`;
|
|
252
|
+
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentix Built-in Plugin: Auto Version Bump
|
|
3
|
+
*
|
|
4
|
+
* After a successful `sentix run` pipeline, automatically bumps the project version.
|
|
5
|
+
* - feature ticket → minor bump
|
|
6
|
+
* - bug ticket → patch bump
|
|
7
|
+
*
|
|
8
|
+
* Controlled by .sentix/config.toml:
|
|
9
|
+
* [version]
|
|
10
|
+
* auto_bump = true
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { registerHook } from '../registry.js';
|
|
14
|
+
import { bumpSemver } from '../lib/semver.js';
|
|
15
|
+
import { generateForVersion, prependToChangelog } from '../lib/changelog.js';
|
|
16
|
+
|
|
17
|
+
registerHook('after:command', async ({ command, args, ctx }) => {
|
|
18
|
+
if (command !== 'run') return;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
// Check config
|
|
22
|
+
if (!await isAutoBumpEnabled(ctx)) return;
|
|
23
|
+
|
|
24
|
+
// Check governor state
|
|
25
|
+
if (!ctx.exists('tasks/governor-state.json')) return;
|
|
26
|
+
const state = await ctx.readJSON('tasks/governor-state.json');
|
|
27
|
+
if (state.status !== 'completed') return;
|
|
28
|
+
|
|
29
|
+
// Determine bump type from ticket_type
|
|
30
|
+
const ticketType = state.ticket_type;
|
|
31
|
+
let bumpType = 'patch'; // safe default
|
|
32
|
+
|
|
33
|
+
if (ticketType === 'feature') {
|
|
34
|
+
bumpType = 'minor';
|
|
35
|
+
} else if (ticketType === 'bug') {
|
|
36
|
+
bumpType = 'patch';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Read current version
|
|
40
|
+
if (!ctx.exists('package.json')) return;
|
|
41
|
+
const pkg = await ctx.readJSON('package.json');
|
|
42
|
+
const current = pkg.version;
|
|
43
|
+
if (!current) return;
|
|
44
|
+
|
|
45
|
+
const newVersion = bumpSemver(current, bumpType);
|
|
46
|
+
|
|
47
|
+
// Update package.json
|
|
48
|
+
pkg.version = newVersion;
|
|
49
|
+
await ctx.writeJSON('package.json', pkg);
|
|
50
|
+
|
|
51
|
+
// Update CHANGELOG
|
|
52
|
+
try {
|
|
53
|
+
const entry = await generateForVersion(ctx, newVersion);
|
|
54
|
+
if (entry.trim()) {
|
|
55
|
+
await prependToChangelog(ctx, entry);
|
|
56
|
+
}
|
|
57
|
+
} catch { /* non-critical */ }
|
|
58
|
+
|
|
59
|
+
// Log
|
|
60
|
+
await ctx.appendJSONL('tasks/pattern-log.jsonl', {
|
|
61
|
+
ts: new Date().toISOString(),
|
|
62
|
+
event: 'version:auto-bump',
|
|
63
|
+
from: current,
|
|
64
|
+
to: newVersion,
|
|
65
|
+
type: bumpType,
|
|
66
|
+
trigger: `pipeline:${state.cycle_id}`,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
ctx.success(`Auto version bump: ${current} → ${newVersion} (${bumpType})`);
|
|
70
|
+
} catch {
|
|
71
|
+
// Silent — auto-version should never break pipeline
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
async function isAutoBumpEnabled(ctx) {
|
|
76
|
+
if (!ctx.exists('.sentix/config.toml')) return false;
|
|
77
|
+
try {
|
|
78
|
+
const config = await ctx.readFile('.sentix/config.toml');
|
|
79
|
+
const sectionHeader = '[version]';
|
|
80
|
+
const idx = config.indexOf(sectionHeader);
|
|
81
|
+
if (idx === -1) return false;
|
|
82
|
+
const afterSection = config.slice(idx + sectionHeader.length);
|
|
83
|
+
const nextSection = afterSection.indexOf('\n[');
|
|
84
|
+
const sectionContent = nextSection === -1 ? afterSection : afterSection.slice(0, nextSection);
|
|
85
|
+
return /auto_bump\s*=\s*true/.test(sectionContent);
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentix Built-in Plugin: Logger
|
|
3
|
+
*
|
|
4
|
+
* Logs all command executions to tasks/pattern-log.jsonl.
|
|
5
|
+
* This is the primary data source for the Pattern Engine (Layer 3).
|
|
6
|
+
* Includes basic log rotation: truncates to last 10,000 entries when exceeding 20,000.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { registerHook } from '../registry.js';
|
|
10
|
+
|
|
11
|
+
const MAX_ENTRIES = 20_000;
|
|
12
|
+
const KEEP_ENTRIES = 10_000;
|
|
13
|
+
|
|
14
|
+
registerHook('before:command', async ({ command, args, ctx }) => {
|
|
15
|
+
try {
|
|
16
|
+
await ctx.appendJSONL('tasks/pattern-log.jsonl', {
|
|
17
|
+
ts: new Date().toISOString(),
|
|
18
|
+
event: 'command:start',
|
|
19
|
+
command,
|
|
20
|
+
args,
|
|
21
|
+
});
|
|
22
|
+
} catch {
|
|
23
|
+
// Silent — logging should never break command execution
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
registerHook('after:command', async ({ command, args, ctx }) => {
|
|
28
|
+
try {
|
|
29
|
+
await ctx.appendJSONL('tasks/pattern-log.jsonl', {
|
|
30
|
+
ts: new Date().toISOString(),
|
|
31
|
+
event: 'command:end',
|
|
32
|
+
command,
|
|
33
|
+
args,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ── Log rotation ────────────────────────────────
|
|
37
|
+
await rotateIfNeeded(ctx);
|
|
38
|
+
} catch {
|
|
39
|
+
// Silent
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
async function rotateIfNeeded(ctx) {
|
|
44
|
+
try {
|
|
45
|
+
if (!ctx.exists('tasks/pattern-log.jsonl')) return;
|
|
46
|
+
const content = await ctx.readFile('tasks/pattern-log.jsonl');
|
|
47
|
+
const lines = content.split('\n').filter(Boolean);
|
|
48
|
+
if (lines.length > MAX_ENTRIES) {
|
|
49
|
+
const trimmed = lines.slice(-KEEP_ENTRIES).join('\n') + '\n';
|
|
50
|
+
await ctx.writeFile('tasks/pattern-log.jsonl', trimmed);
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Silent — rotation failure is non-critical
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentix Plugin Registry
|
|
3
|
+
*
|
|
4
|
+
* Manages command registration, hook execution, and plugin loading.
|
|
5
|
+
* Loading order: src/commands/ → src/plugins/ → .sentix/plugins/ (project-local)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const commands = new Map();
|
|
9
|
+
const hooks = new Map();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Register a CLI command.
|
|
13
|
+
* @param {string} name - Command name (e.g., "init", "run")
|
|
14
|
+
* @param {{ description: string, usage: string, run: (args: string[], ctx: object) => Promise<void> }} opts
|
|
15
|
+
*/
|
|
16
|
+
export function registerCommand(name, opts) {
|
|
17
|
+
commands.set(name, opts);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register a lifecycle hook.
|
|
22
|
+
* @param {string} name - Hook name (e.g., "before:command", "after:command")
|
|
23
|
+
* @param {(info: object) => Promise<void>} fn - Hook handler
|
|
24
|
+
*/
|
|
25
|
+
export function registerHook(name, fn) {
|
|
26
|
+
if (!hooks.has(name)) hooks.set(name, []);
|
|
27
|
+
hooks.get(name).push(fn);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get a registered command by name.
|
|
32
|
+
* @param {string} name
|
|
33
|
+
* @returns {object|undefined}
|
|
34
|
+
*/
|
|
35
|
+
export function getCommand(name) {
|
|
36
|
+
return commands.get(name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get all registered commands.
|
|
41
|
+
* @returns {Map}
|
|
42
|
+
*/
|
|
43
|
+
export function getAllCommands() {
|
|
44
|
+
return commands;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Run all hooks for a given event.
|
|
49
|
+
* Errors in individual hooks are caught and logged — they never break command execution.
|
|
50
|
+
* @param {string} name - Hook event name
|
|
51
|
+
* @param {object} info - Context passed to hooks
|
|
52
|
+
*/
|
|
53
|
+
export async function runHooks(name, info) {
|
|
54
|
+
const fns = hooks.get(name) || [];
|
|
55
|
+
for (const fn of fns) {
|
|
56
|
+
try {
|
|
57
|
+
await fn(info);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
const hookName = fn.name || '(anonymous)';
|
|
60
|
+
console.error(`[sentix] Hook "${name}" (${hookName}) failed: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/version.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentix version — single source of truth.
|
|
3
|
+
* Reads from package.json at module load time.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { resolve, dirname } from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const pkgPath = resolve(__dirname, '..', 'package.json');
|
|
12
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
13
|
+
|
|
14
|
+
export const VERSION = pkg.version;
|
|
15
|
+
export const NAME = pkg.name;
|