@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.
Files changed (67) hide show
  1. package/LICENSE +71 -0
  2. package/README.md +2 -2
  3. package/dist/hooks/cost-tracker.js +149 -11527
  4. package/dist/hooks/post-edit-context.js +127 -11493
  5. package/dist/hooks/post-tool-use.js +169 -11550
  6. package/dist/hooks/pre-compact.js +149 -11530
  7. package/dist/hooks/pre-delete-check.js +144 -11523
  8. package/dist/hooks/quality-event.js +149 -11527
  9. package/dist/hooks/session-end.js +188 -11570
  10. package/dist/hooks/session-start.js +159 -11534
  11. package/dist/hooks/user-prompt.js +149 -11530
  12. package/package.json +14 -19
  13. package/src/adr-generator.ts +292 -0
  14. package/src/analytics.ts +373 -0
  15. package/src/audit-trail.ts +450 -0
  16. package/src/backfill-sessions.ts +180 -0
  17. package/src/cli.ts +105 -0
  18. package/src/cloud-sync.ts +190 -0
  19. package/src/commands/doctor.ts +300 -0
  20. package/src/commands/init.ts +395 -0
  21. package/src/commands/install-hooks.ts +26 -0
  22. package/src/config.ts +357 -0
  23. package/src/cost-tracker.ts +355 -0
  24. package/src/db.ts +233 -0
  25. package/src/dependency-scorer.ts +337 -0
  26. package/src/docs-map.json +100 -0
  27. package/src/docs-tools.ts +517 -0
  28. package/src/domains.ts +181 -0
  29. package/src/hooks/cost-tracker.ts +66 -0
  30. package/src/hooks/intent-suggester.ts +131 -0
  31. package/src/hooks/post-edit-context.ts +91 -0
  32. package/src/hooks/post-tool-use.ts +175 -0
  33. package/src/hooks/pre-compact.ts +146 -0
  34. package/src/hooks/pre-delete-check.ts +153 -0
  35. package/src/hooks/quality-event.ts +127 -0
  36. package/src/hooks/security-gate.ts +121 -0
  37. package/src/hooks/session-end.ts +467 -0
  38. package/src/hooks/session-start.ts +210 -0
  39. package/src/hooks/user-prompt.ts +91 -0
  40. package/src/import-resolver.ts +224 -0
  41. package/src/memory-db.ts +1376 -0
  42. package/src/memory-tools.ts +391 -0
  43. package/src/middleware-tree.ts +70 -0
  44. package/src/observability-tools.ts +343 -0
  45. package/src/observation-extractor.ts +411 -0
  46. package/src/page-deps.ts +283 -0
  47. package/src/prompt-analyzer.ts +332 -0
  48. package/src/regression-detector.ts +319 -0
  49. package/src/rules.ts +57 -0
  50. package/src/schema-mapper.ts +232 -0
  51. package/src/security-scorer.ts +405 -0
  52. package/src/security-utils.ts +133 -0
  53. package/src/sentinel-db.ts +578 -0
  54. package/src/sentinel-scanner.ts +405 -0
  55. package/src/sentinel-tools.ts +512 -0
  56. package/src/sentinel-types.ts +140 -0
  57. package/src/server.ts +189 -0
  58. package/src/session-archiver.ts +112 -0
  59. package/src/session-state-generator.ts +174 -0
  60. package/src/team-knowledge.ts +407 -0
  61. package/src/tools.ts +847 -0
  62. package/src/transcript-parser.ts +458 -0
  63. package/src/trpc-index.ts +214 -0
  64. package/src/validate-features-runner.ts +106 -0
  65. package/src/validation-engine.ts +358 -0
  66. package/dist/cli.js +0 -7890
  67. 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
+ }