@massu/core 0.1.0 → 0.1.2
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 +71 -0
- package/README.md +2 -2
- package/dist/hooks/cost-tracker.js +149 -11527
- package/dist/hooks/post-edit-context.js +127 -11493
- package/dist/hooks/post-tool-use.js +169 -11550
- package/dist/hooks/pre-compact.js +149 -11530
- package/dist/hooks/pre-delete-check.js +144 -11523
- package/dist/hooks/quality-event.js +149 -11527
- package/dist/hooks/session-end.js +188 -11570
- package/dist/hooks/session-start.js +159 -11534
- package/dist/hooks/user-prompt.js +149 -11530
- package/package.json +14 -19
- package/src/adr-generator.ts +292 -0
- package/src/analytics.ts +373 -0
- package/src/audit-trail.ts +450 -0
- package/src/backfill-sessions.ts +180 -0
- package/src/cli.ts +105 -0
- package/src/cloud-sync.ts +190 -0
- package/src/commands/doctor.ts +300 -0
- package/src/commands/init.ts +395 -0
- package/src/commands/install-hooks.ts +26 -0
- package/src/config.ts +357 -0
- package/src/cost-tracker.ts +355 -0
- package/src/db.ts +233 -0
- package/src/dependency-scorer.ts +337 -0
- package/src/docs-map.json +100 -0
- package/src/docs-tools.ts +517 -0
- package/src/domains.ts +181 -0
- package/src/hooks/cost-tracker.ts +66 -0
- package/src/hooks/intent-suggester.ts +131 -0
- package/src/hooks/post-edit-context.ts +91 -0
- package/src/hooks/post-tool-use.ts +175 -0
- package/src/hooks/pre-compact.ts +146 -0
- package/src/hooks/pre-delete-check.ts +153 -0
- package/src/hooks/quality-event.ts +127 -0
- package/src/hooks/security-gate.ts +121 -0
- package/src/hooks/session-end.ts +467 -0
- package/src/hooks/session-start.ts +210 -0
- package/src/hooks/user-prompt.ts +91 -0
- package/src/import-resolver.ts +224 -0
- package/src/memory-db.ts +1376 -0
- package/src/memory-tools.ts +391 -0
- package/src/middleware-tree.ts +70 -0
- package/src/observability-tools.ts +343 -0
- package/src/observation-extractor.ts +411 -0
- package/src/page-deps.ts +283 -0
- package/src/prompt-analyzer.ts +332 -0
- package/src/regression-detector.ts +319 -0
- package/src/rules.ts +57 -0
- package/src/schema-mapper.ts +232 -0
- package/src/security-scorer.ts +405 -0
- package/src/security-utils.ts +133 -0
- package/src/sentinel-db.ts +578 -0
- package/src/sentinel-scanner.ts +405 -0
- package/src/sentinel-tools.ts +512 -0
- package/src/sentinel-types.ts +140 -0
- package/src/server.ts +189 -0
- package/src/session-archiver.ts +112 -0
- package/src/session-state-generator.ts +174 -0
- package/src/team-knowledge.ts +407 -0
- package/src/tools.ts +847 -0
- package/src/transcript-parser.ts +458 -0
- package/src/trpc-index.ts +214 -0
- package/src/validate-features-runner.ts +106 -0
- package/src/validation-engine.ts +358 -0
- package/dist/cli.js +0 -7890
- package/dist/server.js +0 -7008
package/src/cli.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
3
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Massu CLI Entry Point
|
|
7
|
+
*
|
|
8
|
+
* Routes subcommands to handlers, falls through to MCP server mode
|
|
9
|
+
* when no subcommand is provided (backward compatible).
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* npx massu init - Full project setup
|
|
13
|
+
* npx massu doctor - Health check
|
|
14
|
+
* npx massu install-hooks - Install hooks only
|
|
15
|
+
* npx massu validate-config - Validate configuration
|
|
16
|
+
* npx @massu/core - MCP server mode (no args)
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from 'fs';
|
|
20
|
+
import { resolve, dirname } from 'path';
|
|
21
|
+
import { fileURLToPath } from 'url';
|
|
22
|
+
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = dirname(__filename);
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const subcommand = args[0];
|
|
28
|
+
|
|
29
|
+
async function main(): Promise<void> {
|
|
30
|
+
switch (subcommand) {
|
|
31
|
+
case 'init': {
|
|
32
|
+
const { runInit } = await import('./commands/init.ts');
|
|
33
|
+
await runInit();
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case 'doctor': {
|
|
37
|
+
const { runDoctor } = await import('./commands/doctor.ts');
|
|
38
|
+
await runDoctor();
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case 'install-hooks': {
|
|
42
|
+
const { runInstallHooks } = await import('./commands/install-hooks.ts');
|
|
43
|
+
await runInstallHooks();
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
case 'validate-config': {
|
|
47
|
+
const { runValidateConfig } = await import('./commands/doctor.ts');
|
|
48
|
+
await runValidateConfig();
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case '--help':
|
|
52
|
+
case '-h': {
|
|
53
|
+
printHelp();
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
case '--version':
|
|
57
|
+
case '-v': {
|
|
58
|
+
printVersion();
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
default: {
|
|
62
|
+
// No subcommand or unknown: fall through to MCP server mode
|
|
63
|
+
// This maintains backward compatibility with `npx @massu/core`
|
|
64
|
+
await import('./server.ts');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function printHelp(): void {
|
|
70
|
+
console.log(`
|
|
71
|
+
Massu AI - Engineering Governance Platform
|
|
72
|
+
|
|
73
|
+
Usage:
|
|
74
|
+
massu <command>
|
|
75
|
+
|
|
76
|
+
Commands:
|
|
77
|
+
init Set up Massu AI in your project (one command, full setup)
|
|
78
|
+
doctor Check installation health
|
|
79
|
+
install-hooks Install/update Claude Code hooks
|
|
80
|
+
validate-config Validate massu.config.yaml
|
|
81
|
+
|
|
82
|
+
Options:
|
|
83
|
+
--help, -h Show this help message
|
|
84
|
+
--version, -v Show version
|
|
85
|
+
|
|
86
|
+
Getting started:
|
|
87
|
+
npx massu init # Full setup in one command
|
|
88
|
+
|
|
89
|
+
Documentation: https://massu.ai/docs
|
|
90
|
+
`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function printVersion(): void {
|
|
94
|
+
try {
|
|
95
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, '../package.json'), 'utf-8'));
|
|
96
|
+
console.log(`massu v${pkg.version}`);
|
|
97
|
+
} catch {
|
|
98
|
+
console.log('massu v0.1.0');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main().catch((err) => {
|
|
103
|
+
console.error(`massu: ${err instanceof Error ? err.message : String(err)}`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
import type Database from 'better-sqlite3';
|
|
5
|
+
import { getConfig } from './config.ts';
|
|
6
|
+
import {
|
|
7
|
+
enqueueSyncPayload,
|
|
8
|
+
dequeuePendingSync,
|
|
9
|
+
removePendingSync,
|
|
10
|
+
incrementRetryCount,
|
|
11
|
+
} from './memory-db.ts';
|
|
12
|
+
|
|
13
|
+
// ============================================================
|
|
14
|
+
// Cloud Sync Module
|
|
15
|
+
// Internal module — NOT an MCP tool. Called by session-end hook.
|
|
16
|
+
// ============================================================
|
|
17
|
+
|
|
18
|
+
export interface SyncPayload {
|
|
19
|
+
sessions?: Array<{
|
|
20
|
+
local_session_id: string;
|
|
21
|
+
project_name?: string;
|
|
22
|
+
summary?: string;
|
|
23
|
+
started_at?: string;
|
|
24
|
+
ended_at?: string;
|
|
25
|
+
turns?: number;
|
|
26
|
+
tokens_used?: number;
|
|
27
|
+
estimated_cost?: number;
|
|
28
|
+
tools_used?: string[];
|
|
29
|
+
}>;
|
|
30
|
+
observations?: Array<{
|
|
31
|
+
local_observation_id: string;
|
|
32
|
+
session_id?: string;
|
|
33
|
+
type: string;
|
|
34
|
+
content: string;
|
|
35
|
+
importance: number;
|
|
36
|
+
file_path?: string;
|
|
37
|
+
}>;
|
|
38
|
+
analytics?: Array<{
|
|
39
|
+
event_type: string;
|
|
40
|
+
event_data: Record<string, unknown>;
|
|
41
|
+
}>;
|
|
42
|
+
audit?: Array<{
|
|
43
|
+
action: string;
|
|
44
|
+
resource?: string;
|
|
45
|
+
details: Record<string, unknown>;
|
|
46
|
+
}>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SyncResult {
|
|
50
|
+
success: boolean;
|
|
51
|
+
synced: {
|
|
52
|
+
sessions: number;
|
|
53
|
+
observations: number;
|
|
54
|
+
analytics: number;
|
|
55
|
+
audit: number;
|
|
56
|
+
};
|
|
57
|
+
error?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const MAX_RETRIES = 3;
|
|
61
|
+
const RETRY_DELAYS = [1000, 2000, 4000]; // exponential backoff
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Sync data to the cloud endpoint.
|
|
65
|
+
* Respects config flags for selective sync.
|
|
66
|
+
* On failure after retries, enqueues payload for later retry.
|
|
67
|
+
*/
|
|
68
|
+
export async function syncToCloud(
|
|
69
|
+
db: Database.Database,
|
|
70
|
+
payload: SyncPayload
|
|
71
|
+
): Promise<SyncResult> {
|
|
72
|
+
const config = getConfig();
|
|
73
|
+
const cloud = config.cloud;
|
|
74
|
+
|
|
75
|
+
// Check if cloud sync is enabled
|
|
76
|
+
if (!cloud?.enabled) {
|
|
77
|
+
return { success: true, synced: { sessions: 0, observations: 0, analytics: 0, audit: 0 } };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check API key
|
|
81
|
+
if (!cloud.apiKey) {
|
|
82
|
+
return { success: false, synced: { sessions: 0, observations: 0, analytics: 0, audit: 0 }, error: 'No API key configured' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check endpoint
|
|
86
|
+
const endpoint = cloud.endpoint;
|
|
87
|
+
if (!endpoint) {
|
|
88
|
+
return { success: false, synced: { sessions: 0, observations: 0, analytics: 0, audit: 0 }, error: 'No sync endpoint configured' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Apply selective sync filters
|
|
92
|
+
const filteredPayload: SyncPayload = {};
|
|
93
|
+
if (cloud.sync?.memory !== false) {
|
|
94
|
+
filteredPayload.sessions = payload.sessions;
|
|
95
|
+
filteredPayload.observations = payload.observations;
|
|
96
|
+
}
|
|
97
|
+
if (cloud.sync?.analytics !== false) {
|
|
98
|
+
filteredPayload.analytics = payload.analytics;
|
|
99
|
+
}
|
|
100
|
+
if (cloud.sync?.audit !== false) {
|
|
101
|
+
filteredPayload.audit = payload.audit;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Attempt sync with retry
|
|
105
|
+
let lastError = '';
|
|
106
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch(endpoint, {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: {
|
|
111
|
+
'Content-Type': 'application/json',
|
|
112
|
+
'Authorization': `Bearer ${cloud.apiKey}`,
|
|
113
|
+
},
|
|
114
|
+
body: JSON.stringify(filteredPayload),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
lastError = `HTTP ${response.status}: ${response.statusText}`;
|
|
119
|
+
if (response.status >= 400 && response.status < 500) {
|
|
120
|
+
// Client errors are not retryable
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
// Server errors — retry
|
|
124
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
125
|
+
await sleep(RETRY_DELAYS[attempt]);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const result = await response.json() as { synced?: { sessions?: number; observations?: number; analytics?: number } };
|
|
132
|
+
return {
|
|
133
|
+
success: true,
|
|
134
|
+
synced: {
|
|
135
|
+
sessions: result.synced?.sessions ?? 0,
|
|
136
|
+
observations: result.synced?.observations ?? 0,
|
|
137
|
+
analytics: result.synced?.analytics ?? 0,
|
|
138
|
+
audit: 0,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
} catch (err) {
|
|
142
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
143
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
144
|
+
await sleep(RETRY_DELAYS[attempt]);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// All retries exhausted — enqueue for later
|
|
151
|
+
try {
|
|
152
|
+
enqueueSyncPayload(db, JSON.stringify(payload));
|
|
153
|
+
} catch (_e) {
|
|
154
|
+
// Best effort — don't crash if queue fails
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
success: false,
|
|
159
|
+
synced: { sessions: 0, observations: 0, analytics: 0, audit: 0 },
|
|
160
|
+
error: lastError,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Drain the pending sync queue. Processes items oldest-first.
|
|
166
|
+
* Successfully synced items are removed; failed items get their retry count incremented.
|
|
167
|
+
*/
|
|
168
|
+
export async function drainSyncQueue(db: Database.Database): Promise<void> {
|
|
169
|
+
const config = getConfig();
|
|
170
|
+
if (!config.cloud?.enabled || !config.cloud?.apiKey) return;
|
|
171
|
+
|
|
172
|
+
const pending = dequeuePendingSync(db, 10);
|
|
173
|
+
for (const item of pending) {
|
|
174
|
+
try {
|
|
175
|
+
const payload = JSON.parse(item.payload) as SyncPayload;
|
|
176
|
+
const result = await syncToCloud(db, payload);
|
|
177
|
+
if (result.success) {
|
|
178
|
+
removePendingSync(db, item.id);
|
|
179
|
+
} else {
|
|
180
|
+
incrementRetryCount(db, item.id, result.error ?? 'Unknown error');
|
|
181
|
+
}
|
|
182
|
+
} catch (err) {
|
|
183
|
+
incrementRetryCount(db, item.id, err instanceof Error ? err.message : String(err));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function sleep(ms: number): Promise<void> {
|
|
189
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
190
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `massu doctor` — Installation health check.
|
|
6
|
+
*
|
|
7
|
+
* Verifies all components of a Massu AI installation are working:
|
|
8
|
+
* 1. massu.config.yaml exists and parses correctly
|
|
9
|
+
* 2. .mcp.json has massu entry
|
|
10
|
+
* 3. .claude/settings.local.json has hooks config
|
|
11
|
+
* 4. All 11 compiled hook files exist
|
|
12
|
+
* 5. better-sqlite3 native module loads
|
|
13
|
+
* 6. Node.js version >= 18
|
|
14
|
+
* 7. Git repository detected
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync } from 'fs';
|
|
18
|
+
import { resolve, dirname } from 'path';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
import { parse as parseYaml } from 'yaml';
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = dirname(__filename);
|
|
24
|
+
|
|
25
|
+
// ============================================================
|
|
26
|
+
// Types
|
|
27
|
+
// ============================================================
|
|
28
|
+
|
|
29
|
+
interface CheckResult {
|
|
30
|
+
name: string;
|
|
31
|
+
status: 'pass' | 'fail' | 'warn';
|
|
32
|
+
detail: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ============================================================
|
|
36
|
+
// Hook Files
|
|
37
|
+
// ============================================================
|
|
38
|
+
|
|
39
|
+
const EXPECTED_HOOKS = [
|
|
40
|
+
'session-start.js',
|
|
41
|
+
'session-end.js',
|
|
42
|
+
'post-tool-use.js',
|
|
43
|
+
'user-prompt.js',
|
|
44
|
+
'pre-compact.js',
|
|
45
|
+
'pre-delete-check.js',
|
|
46
|
+
'post-edit-context.js',
|
|
47
|
+
'security-gate.js',
|
|
48
|
+
'cost-tracker.js',
|
|
49
|
+
'quality-event.js',
|
|
50
|
+
'intent-suggester.js',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
// ============================================================
|
|
54
|
+
// Individual Checks
|
|
55
|
+
// ============================================================
|
|
56
|
+
|
|
57
|
+
function checkConfig(projectRoot: string): CheckResult {
|
|
58
|
+
const configPath = resolve(projectRoot, 'massu.config.yaml');
|
|
59
|
+
if (!existsSync(configPath)) {
|
|
60
|
+
return { name: 'Configuration', status: 'fail', detail: 'massu.config.yaml not found. Run: npx massu init' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
65
|
+
const parsed = parseYaml(content);
|
|
66
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
67
|
+
return { name: 'Configuration', status: 'fail', detail: 'massu.config.yaml is empty or invalid YAML' };
|
|
68
|
+
}
|
|
69
|
+
return { name: 'Configuration', status: 'pass', detail: 'massu.config.yaml found and valid' };
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return { name: 'Configuration', status: 'fail', detail: `massu.config.yaml parse error: ${err instanceof Error ? err.message : String(err)}` };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function checkMcpServer(projectRoot: string): CheckResult {
|
|
76
|
+
const mcpPath = resolve(projectRoot, '.mcp.json');
|
|
77
|
+
if (!existsSync(mcpPath)) {
|
|
78
|
+
return { name: 'MCP Server', status: 'fail', detail: '.mcp.json not found. Run: npx massu init' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const content = JSON.parse(readFileSync(mcpPath, 'utf-8'));
|
|
83
|
+
const servers = content.mcpServers ?? {};
|
|
84
|
+
if (!servers.massu) {
|
|
85
|
+
return { name: 'MCP Server', status: 'fail', detail: 'massu not registered in .mcp.json. Run: npx massu init' };
|
|
86
|
+
}
|
|
87
|
+
return { name: 'MCP Server', status: 'pass', detail: 'Registered in .mcp.json' };
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return { name: 'MCP Server', status: 'fail', detail: `.mcp.json parse error: ${err instanceof Error ? err.message : String(err)}` };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function checkHooksConfig(projectRoot: string): CheckResult {
|
|
94
|
+
const settingsPath = resolve(projectRoot, '.claude/settings.local.json');
|
|
95
|
+
if (!existsSync(settingsPath)) {
|
|
96
|
+
return { name: 'Hooks Config', status: 'fail', detail: '.claude/settings.local.json not found. Run: npx massu init' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const content = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
101
|
+
if (!content.hooks) {
|
|
102
|
+
return { name: 'Hooks Config', status: 'fail', detail: 'No hooks configured. Run: npx massu install-hooks' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Count configured hooks
|
|
106
|
+
let hookCount = 0;
|
|
107
|
+
for (const groups of Object.values(content.hooks)) {
|
|
108
|
+
if (Array.isArray(groups)) {
|
|
109
|
+
for (const group of groups) {
|
|
110
|
+
const g = group as { hooks?: unknown[] };
|
|
111
|
+
if (Array.isArray(g.hooks)) {
|
|
112
|
+
hookCount += g.hooks.length;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (hookCount === 0) {
|
|
119
|
+
return { name: 'Hooks Config', status: 'fail', detail: 'Hooks section exists but no hooks configured' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { name: 'Hooks Config', status: 'pass', detail: `${hookCount} hooks configured` };
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return { name: 'Hooks Config', status: 'fail', detail: `settings.local.json parse error: ${err instanceof Error ? err.message : String(err)}` };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function checkHookFiles(projectRoot: string): CheckResult {
|
|
129
|
+
// Check node_modules path first
|
|
130
|
+
const nodeModulesHooksDir = resolve(projectRoot, 'node_modules/@massu/core/dist/hooks');
|
|
131
|
+
let hooksDir = nodeModulesHooksDir;
|
|
132
|
+
|
|
133
|
+
if (!existsSync(nodeModulesHooksDir)) {
|
|
134
|
+
// Check relative to this file (development mode)
|
|
135
|
+
const devHooksDir = resolve(__dirname, '../../dist/hooks');
|
|
136
|
+
if (existsSync(devHooksDir)) {
|
|
137
|
+
hooksDir = devHooksDir;
|
|
138
|
+
} else {
|
|
139
|
+
return { name: 'Hook Files', status: 'fail', detail: 'Compiled hooks not found. Run: npm install @massu/core' };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const missing: string[] = [];
|
|
144
|
+
for (const hookFile of EXPECTED_HOOKS) {
|
|
145
|
+
if (!existsSync(resolve(hooksDir, hookFile))) {
|
|
146
|
+
missing.push(hookFile);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (missing.length > 0) {
|
|
151
|
+
return { name: 'Hook Files', status: 'fail', detail: `Missing hooks: ${missing.join(', ')}` };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { name: 'Hook Files', status: 'pass', detail: `${EXPECTED_HOOKS.length}/${EXPECTED_HOOKS.length} compiled hooks present` };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function checkNativeModules(): Promise<CheckResult> {
|
|
158
|
+
try {
|
|
159
|
+
await import('better-sqlite3');
|
|
160
|
+
return { name: 'Native Modules', status: 'pass', detail: 'better-sqlite3 loads correctly' };
|
|
161
|
+
} catch (err) {
|
|
162
|
+
return { name: 'Native Modules', status: 'fail', detail: `better-sqlite3 failed: ${err instanceof Error ? err.message : String(err)}. Try: npm rebuild better-sqlite3` };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function checkNodeVersion(): CheckResult {
|
|
167
|
+
const version = process.versions.node;
|
|
168
|
+
const major = parseInt(version.split('.')[0], 10);
|
|
169
|
+
|
|
170
|
+
if (major >= 18) {
|
|
171
|
+
return { name: 'Node.js', status: 'pass', detail: `v${version} (>= 18 required)` };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { name: 'Node.js', status: 'fail', detail: `v${version} — Node.js 18+ is required` };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function checkGitRepo(projectRoot: string): Promise<CheckResult> {
|
|
178
|
+
const gitDir = resolve(projectRoot, '.git');
|
|
179
|
+
if (!existsSync(gitDir)) {
|
|
180
|
+
return { name: 'Git Repository', status: 'warn', detail: 'Not a git repository (optional but recommended)' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const { spawnSync } = await import('child_process');
|
|
185
|
+
const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
186
|
+
encoding: 'utf-8',
|
|
187
|
+
timeout: 5000,
|
|
188
|
+
cwd: projectRoot,
|
|
189
|
+
});
|
|
190
|
+
const branch = result.stdout?.trim() ?? 'unknown';
|
|
191
|
+
return { name: 'Git Repository', status: 'pass', detail: `Detected (branch: ${branch})` };
|
|
192
|
+
} catch {
|
|
193
|
+
return { name: 'Git Repository', status: 'pass', detail: 'Detected' };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ============================================================
|
|
198
|
+
// Main Doctor Flow
|
|
199
|
+
// ============================================================
|
|
200
|
+
|
|
201
|
+
export async function runDoctor(): Promise<void> {
|
|
202
|
+
const projectRoot = process.cwd();
|
|
203
|
+
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log('Massu AI Health Check');
|
|
206
|
+
console.log('=====================');
|
|
207
|
+
console.log('');
|
|
208
|
+
|
|
209
|
+
const checks: CheckResult[] = [
|
|
210
|
+
checkConfig(projectRoot),
|
|
211
|
+
checkMcpServer(projectRoot),
|
|
212
|
+
checkHooksConfig(projectRoot),
|
|
213
|
+
checkHookFiles(projectRoot),
|
|
214
|
+
await checkNativeModules(),
|
|
215
|
+
checkNodeVersion(),
|
|
216
|
+
await checkGitRepo(projectRoot),
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
let passed = 0;
|
|
220
|
+
let failed = 0;
|
|
221
|
+
let warned = 0;
|
|
222
|
+
|
|
223
|
+
for (const check of checks) {
|
|
224
|
+
const icon = check.status === 'pass' ? '\u2713' : check.status === 'warn' ? '!' : '\u2717';
|
|
225
|
+
const pad = check.name.padEnd(20);
|
|
226
|
+
console.log(` ${icon} ${pad} ${check.detail}`);
|
|
227
|
+
|
|
228
|
+
if (check.status === 'pass') passed++;
|
|
229
|
+
else if (check.status === 'fail') failed++;
|
|
230
|
+
else warned++;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
console.log('');
|
|
234
|
+
|
|
235
|
+
if (failed === 0) {
|
|
236
|
+
const total = passed + warned;
|
|
237
|
+
console.log(`Status: HEALTHY (${passed}/${total} checks passed${warned > 0 ? `, ${warned} warnings` : ''})`);
|
|
238
|
+
} else {
|
|
239
|
+
console.log(`Status: UNHEALTHY (${failed} check${failed > 1 ? 's' : ''} failed)`);
|
|
240
|
+
console.log('');
|
|
241
|
+
console.log('Fix issues above, then run: npx massu doctor');
|
|
242
|
+
}
|
|
243
|
+
console.log('');
|
|
244
|
+
|
|
245
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ============================================================
|
|
249
|
+
// Validate Config Command
|
|
250
|
+
// ============================================================
|
|
251
|
+
|
|
252
|
+
export async function runValidateConfig(): Promise<void> {
|
|
253
|
+
const projectRoot = process.cwd();
|
|
254
|
+
const configPath = resolve(projectRoot, 'massu.config.yaml');
|
|
255
|
+
|
|
256
|
+
if (!existsSync(configPath)) {
|
|
257
|
+
console.error('Error: massu.config.yaml not found in current directory');
|
|
258
|
+
console.error('Run: npx massu init');
|
|
259
|
+
process.exit(1);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
265
|
+
const parsed = parseYaml(content);
|
|
266
|
+
|
|
267
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
268
|
+
console.error('Error: massu.config.yaml is empty or not a valid YAML object');
|
|
269
|
+
process.exit(1);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Check required fields
|
|
274
|
+
const warnings: string[] = [];
|
|
275
|
+
|
|
276
|
+
if (!parsed.project?.name) {
|
|
277
|
+
warnings.push('Missing project.name (will default to "my-project")');
|
|
278
|
+
}
|
|
279
|
+
if (!parsed.toolPrefix) {
|
|
280
|
+
warnings.push('Missing toolPrefix (will default to "massu")');
|
|
281
|
+
}
|
|
282
|
+
if (!parsed.framework?.type) {
|
|
283
|
+
warnings.push('Missing framework.type (will default to "typescript")');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console.log('');
|
|
287
|
+
if (warnings.length === 0) {
|
|
288
|
+
console.log('massu.config.yaml is valid');
|
|
289
|
+
} else {
|
|
290
|
+
console.log('massu.config.yaml parsed successfully with warnings:');
|
|
291
|
+
for (const w of warnings) {
|
|
292
|
+
console.log(` ! ${w}`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
console.log('');
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.error(`Error parsing massu.config.yaml: ${err instanceof Error ? err.message : String(err)}`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
}
|