@massu/core 1.10.8 → 1.12.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/CHANGELOG.md +1334 -0
- package/dist/adapter.js +6 -4
- package/dist/cli.js +581 -448
- package/dist/detect/adapters/.bundle-shasums.json +4 -7
- package/dist/detect/adapters/rails.js +6 -4
- package/dist/detect/adapters/spring.js +6 -4
- package/dist/detect/adapters/tree-sitter-loader.js +6 -4
- package/dist/hooks/auto-learning-pipeline.js +24 -1
- package/dist/hooks/classify-failure.js +32 -3
- package/dist/hooks/cost-tracker.js +24 -0
- package/dist/hooks/fix-detector.js +97 -1
- package/dist/hooks/incident-pipeline.js +30 -1
- package/dist/hooks/intent-suggester.js +6 -3
- package/dist/hooks/post-edit-context.js +24 -1
- package/dist/hooks/post-tool-use.js +67 -19
- package/dist/hooks/pre-compact.js +28 -3
- package/dist/hooks/pre-delete-check.js +53 -27
- package/dist/hooks/pre-tool-use-gate.js +962 -0
- package/dist/hooks/quality-event.js +24 -0
- package/dist/hooks/rule-enforcement-pipeline.js +24 -1
- package/dist/hooks/security-gate.js +29 -16
- package/dist/hooks/session-end.js +31 -6
- package/dist/hooks/session-start.js +8709 -8459
- package/dist/hooks/user-prompt.js +25 -1
- package/package.json +15 -18
- package/commands/README.md +0 -134
- package/src/adapter.ts +0 -121
- package/src/adr-generator.ts +0 -292
- package/src/analytics.ts +0 -373
- package/src/audit-trail.ts +0 -450
- package/src/backfill-sessions.ts +0 -182
- package/src/changelog-generator.ts +0 -178
- package/src/cli.ts +0 -246
- package/src/cloud-sync.ts +0 -234
- package/src/commands/adapters.ts +0 -824
- package/src/commands/changelog.ts +0 -165
- package/src/commands/config-check-drift.ts +0 -133
- package/src/commands/config-refresh.ts +0 -396
- package/src/commands/config-upgrade.ts +0 -127
- package/src/commands/doctor.ts +0 -485
- package/src/commands/hook-runner.ts +0 -145
- package/src/commands/init.ts +0 -1823
- package/src/commands/install-commands.ts +0 -692
- package/src/commands/install-hooks.ts +0 -27
- package/src/commands/permissions.ts +0 -150
- package/src/commands/refresh-log.ts +0 -37
- package/src/commands/show-template.ts +0 -65
- package/src/commands/template-engine.ts +0 -301
- package/src/commands/watch.ts +0 -430
- package/src/config.ts +0 -765
- package/src/cost-tracker.ts +0 -355
- package/src/db.ts +0 -370
- package/src/dependency-scorer.ts +0 -337
- package/src/detect/adapters/aspnet.ts +0 -4
- package/src/detect/adapters/discover.ts +0 -469
- package/src/detect/adapters/file-sampler.ts +0 -225
- package/src/detect/adapters/go-chi.ts +0 -4
- package/src/detect/adapters/index.ts +0 -51
- package/src/detect/adapters/nextjs-trpc.ts +0 -166
- package/src/detect/adapters/parse-guard.ts +0 -133
- package/src/detect/adapters/phoenix.ts +0 -4
- package/src/detect/adapters/python-django.ts +0 -208
- package/src/detect/adapters/python-fastapi.ts +0 -223
- package/src/detect/adapters/python-flask.ts +0 -235
- package/src/detect/adapters/query-helpers.ts +0 -170
- package/src/detect/adapters/rails.ts +0 -4
- package/src/detect/adapters/runner.ts +0 -284
- package/src/detect/adapters/spring.ts +0 -4
- package/src/detect/adapters/swift-swiftui.ts +0 -171
- package/src/detect/adapters/tree-sitter-loader.ts +0 -517
- package/src/detect/adapters/types.ts +0 -191
- package/src/detect/codebase-introspector.ts +0 -210
- package/src/detect/domain-inferrer.ts +0 -197
- package/src/detect/drift.ts +0 -199
- package/src/detect/framework-detector.ts +0 -307
- package/src/detect/index.ts +0 -209
- package/src/detect/manifest-registry.ts +0 -261
- package/src/detect/migrate.ts +0 -362
- package/src/detect/monorepo-detector.ts +0 -348
- package/src/detect/package-detector.ts +0 -828
- package/src/detect/passthrough.ts +0 -108
- package/src/detect/regex-fallback.ts +0 -449
- package/src/detect/source-dir-detector.ts +0 -297
- package/src/detect/vr-command-map.ts +0 -167
- package/src/docs-map.json +0 -100
- package/src/docs-tools.ts +0 -519
- package/src/domains.ts +0 -182
- package/src/hooks/auto-learning-pipeline.ts +0 -228
- package/src/hooks/classify-failure.ts +0 -259
- package/src/hooks/cost-tracker.ts +0 -66
- package/src/hooks/fix-detector.ts +0 -186
- package/src/hooks/incident-pipeline.ts +0 -190
- package/src/hooks/intent-suggester.ts +0 -131
- package/src/hooks/post-edit-context.ts +0 -91
- package/src/hooks/post-tool-use.ts +0 -413
- package/src/hooks/pre-compact.ts +0 -168
- package/src/hooks/pre-delete-check.ts +0 -243
- package/src/hooks/quality-event.ts +0 -127
- package/src/hooks/rule-enforcement-pipeline.ts +0 -159
- package/src/hooks/security-gate.ts +0 -153
- package/src/hooks/session-end.ts +0 -467
- package/src/hooks/session-start.ts +0 -436
- package/src/hooks/user-prompt.ts +0 -157
- package/src/import-resolver.ts +0 -226
- package/src/knowledge-db.ts +0 -176
- package/src/knowledge-indexer.ts +0 -708
- package/src/knowledge-tools.ts +0 -1414
- package/src/lib/fileLock.ts +0 -203
- package/src/lib/framework-supports.ts +0 -85
- package/src/lib/gitToplevel.ts +0 -22
- package/src/lib/hook-registry.ts +0 -43
- package/src/lib/installLock.ts +0 -66
- package/src/lib/memory-path.ts +0 -49
- package/src/lib/pidLiveness.ts +0 -67
- package/src/lib/settings-local.ts +0 -110
- package/src/lib/sql-table-names.ts +0 -71
- package/src/license.ts +0 -547
- package/src/lsp/auto-detect.ts +0 -98
- package/src/lsp/client.ts +0 -776
- package/src/lsp/enrich.ts +0 -127
- package/src/lsp/types.ts +0 -221
- package/src/memory-db.ts +0 -1614
- package/src/memory-file-ingest.ts +0 -128
- package/src/memory-tools.ts +0 -424
- package/src/middleware-tree.ts +0 -71
- package/src/observability-tools.ts +0 -343
- package/src/observation-extractor.ts +0 -418
- package/src/page-deps.ts +0 -285
- package/src/permissions.ts +0 -422
- package/src/prompt-analyzer.ts +0 -332
- package/src/python/coupling-detector.ts +0 -125
- package/src/python/domain-enforcer.ts +0 -84
- package/src/python/impact-analyzer.ts +0 -96
- package/src/python/import-parser.ts +0 -244
- package/src/python/import-resolver.ts +0 -136
- package/src/python/migration-indexer.ts +0 -116
- package/src/python/migration-parser.ts +0 -332
- package/src/python/model-indexer.ts +0 -71
- package/src/python/model-parser.ts +0 -279
- package/src/python/route-indexer.ts +0 -59
- package/src/python/route-parser.ts +0 -317
- package/src/python-tools.ts +0 -630
- package/src/regression-detector.ts +0 -319
- package/src/rules.ts +0 -57
- package/src/schema-mapper.ts +0 -232
- package/src/security/adapter-origin.ts +0 -130
- package/src/security/adapter-verifier.ts +0 -319
- package/src/security/atomic-write.ts +0 -164
- package/src/security/fetcher.ts +0 -200
- package/src/security/install-tracking.ts +0 -319
- package/src/security/license-pubkey.generated.ts +0 -30
- package/src/security/license-response-verifier.ts +0 -149
- package/src/security/local-fingerprint.ts +0 -225
- package/src/security/manifest-cache.ts +0 -333
- package/src/security/manifest-schema.ts +0 -129
- package/src/security/registry-pubkey.generated.ts +0 -35
- package/src/security/telemetry.ts +0 -320
- package/src/security-scorer.ts +0 -405
- package/src/security-utils.ts +0 -133
- package/src/sentinel-db.ts +0 -580
- package/src/sentinel-scanner.ts +0 -406
- package/src/sentinel-tools.ts +0 -513
- package/src/sentinel-types.ts +0 -140
- package/src/server-dispatch.ts +0 -225
- package/src/server.ts +0 -107
- package/src/session-archiver.ts +0 -111
- package/src/session-state-generator.ts +0 -174
- package/src/team-knowledge.ts +0 -407
- package/src/tool-db-needs.ts +0 -232
- package/src/tools.ts +0 -1206
- package/src/transcript-parser.ts +0 -458
- package/src/trpc-index.ts +0 -215
- package/src/validate-features-runner.ts +0 -107
- package/src/validation-engine.ts +0 -358
- package/src/watch/daemon.ts +0 -385
- package/src/watch/lockfile-detector.ts +0 -65
- package/src/watch/paths.ts +0 -279
- package/src/watch/state.ts +0 -178
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
-
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* `massu config upgrade` — migrate a v1 `massu.config.yaml` to schema_version=2.
|
|
6
|
-
*
|
|
7
|
-
* Flags:
|
|
8
|
-
* --rollback Restore massu.config.yaml from massu.config.yaml.bak.
|
|
9
|
-
* --ci / --yes Non-interactive; no prompts; detector wins on conflicts.
|
|
10
|
-
*
|
|
11
|
-
* Safety:
|
|
12
|
-
* - Writes .bak of the original before overwriting.
|
|
13
|
-
* - Atomic write via writeConfigAtomic (tmp + rename).
|
|
14
|
-
* - Idempotent: running on a schema_version=2 config is a no-op.
|
|
15
|
-
*
|
|
16
|
-
* Exit codes:
|
|
17
|
-
* 0 success (migrated, rolled back, or already current)
|
|
18
|
-
* 1 config missing / rollback source missing
|
|
19
|
-
* 2 parse or write failure
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import { existsSync, readFileSync, writeFileSync, copyFileSync, unlinkSync } from 'fs';
|
|
23
|
-
import { resolve } from 'path';
|
|
24
|
-
import { parse as parseYaml } from 'yaml';
|
|
25
|
-
import { runDetection } from '../detect/index.ts';
|
|
26
|
-
import { computeFingerprint } from '../detect/drift.ts';
|
|
27
|
-
import { migrateV1ToV2, type AnyConfig } from '../detect/migrate.ts';
|
|
28
|
-
import { renderConfigYaml, writeConfigAtomic } from './init.ts';
|
|
29
|
-
|
|
30
|
-
export interface ConfigUpgradeOptions {
|
|
31
|
-
rollback?: boolean;
|
|
32
|
-
ci?: boolean;
|
|
33
|
-
cwd?: string;
|
|
34
|
-
silent?: boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface ConfigUpgradeResult {
|
|
38
|
-
exitCode: 0 | 1 | 2;
|
|
39
|
-
action: 'migrated' | 'already-current' | 'rolled-back' | 'none';
|
|
40
|
-
message?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function runConfigUpgrade(opts: ConfigUpgradeOptions = {}): Promise<ConfigUpgradeResult> {
|
|
44
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
45
|
-
const configPath = resolve(cwd, 'massu.config.yaml');
|
|
46
|
-
const bakPath = `${configPath}.bak`;
|
|
47
|
-
const log = opts.silent ? () => {} : (s: string) => process.stdout.write(s);
|
|
48
|
-
const err = opts.silent ? () => {} : (s: string) => process.stderr.write(s);
|
|
49
|
-
|
|
50
|
-
if (opts.rollback) {
|
|
51
|
-
if (!existsSync(bakPath)) {
|
|
52
|
-
const message = `No backup found at ${bakPath}`;
|
|
53
|
-
err(message + '\n');
|
|
54
|
-
return { exitCode: 1, action: 'none', message };
|
|
55
|
-
}
|
|
56
|
-
try {
|
|
57
|
-
copyFileSync(bakPath, configPath);
|
|
58
|
-
unlinkSync(bakPath);
|
|
59
|
-
log('Config restored from backup.\n');
|
|
60
|
-
return { exitCode: 0, action: 'rolled-back' };
|
|
61
|
-
} catch (e) {
|
|
62
|
-
const message = `Rollback failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
63
|
-
err(message + '\n');
|
|
64
|
-
return { exitCode: 2, action: 'none', message };
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (!existsSync(configPath)) {
|
|
69
|
-
const message = 'massu.config.yaml not found. Run: npx massu init';
|
|
70
|
-
err(message + '\n');
|
|
71
|
-
return { exitCode: 1, action: 'none', message };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
let existing: AnyConfig;
|
|
75
|
-
try {
|
|
76
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
77
|
-
// pattern-scanner-allow: yaml-parse — reason: v1.1.0 config lifecycle command. Same pre-migration constraint as config-refresh: `massu config upgrade` operates on v1 configs that getConfig() Zod-rejects, so raw parse is required.
|
|
78
|
-
const parsed = parseYaml(content);
|
|
79
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
80
|
-
throw new Error('config is not a YAML object');
|
|
81
|
-
}
|
|
82
|
-
existing = parsed as AnyConfig;
|
|
83
|
-
} catch (e) {
|
|
84
|
-
const message = `Failed to parse massu.config.yaml: ${e instanceof Error ? e.message : String(e)}`;
|
|
85
|
-
err(message + '\n');
|
|
86
|
-
return { exitCode: 2, action: 'none', message };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const schemaVersion = existing.schema_version;
|
|
90
|
-
if (schemaVersion === 2) {
|
|
91
|
-
log('Config is already at schema_version=2; nothing to do.\n');
|
|
92
|
-
return { exitCode: 0, action: 'already-current' };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const detection = await runDetection(cwd);
|
|
96
|
-
const v2 = migrateV1ToV2(existing, detection);
|
|
97
|
-
v2.detection = {
|
|
98
|
-
...(v2.detection as Record<string, unknown> | undefined ?? {}),
|
|
99
|
-
fingerprint: computeFingerprint(detection),
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// Back up original before any write.
|
|
103
|
-
try {
|
|
104
|
-
const original = readFileSync(configPath, 'utf-8');
|
|
105
|
-
writeFileSync(bakPath, original, 'utf-8');
|
|
106
|
-
} catch (e) {
|
|
107
|
-
const message = `Failed to write backup: ${e instanceof Error ? e.message : String(e)}`;
|
|
108
|
-
err(message + '\n');
|
|
109
|
-
return { exitCode: 2, action: 'none', message };
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const yamlContent = renderConfigYaml(v2);
|
|
113
|
-
const writeRes = writeConfigAtomic(configPath, yamlContent);
|
|
114
|
-
if (!writeRes.validated) {
|
|
115
|
-
const message = `Failed to write upgraded config: ${writeRes.error}`;
|
|
116
|
-
err(message + '\n');
|
|
117
|
-
return { exitCode: 2, action: 'none', message };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Non-interactive mode just proceeds; interactive mode currently has no
|
|
121
|
-
// prompt on migrate (the migrator is deterministic and always user-preserving).
|
|
122
|
-
// --ci / --yes remain accepted for script-pipeline safety.
|
|
123
|
-
void opts.ci;
|
|
124
|
-
|
|
125
|
-
log(`Config upgraded to schema_version=2. Backup saved at ${bakPath}\n`);
|
|
126
|
-
return { exitCode: 0, action: 'migrated' };
|
|
127
|
-
}
|
package/src/commands/doctor.ts
DELETED
|
@@ -1,485 +0,0 @@
|
|
|
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 compiled hook files exist (count sourced from lib/hook-registry.ts SoT)
|
|
12
|
-
* 5. Knowledge DB exists (.massu/memory.db)
|
|
13
|
-
* 6. Memory directory exists (~/.claude/projects/.../memory/)
|
|
14
|
-
* 7. Shell hooks wired in settings.local.json
|
|
15
|
-
* 8. better-sqlite3 native module loads
|
|
16
|
-
* 9. Node.js version >= 18
|
|
17
|
-
* 10. Git repository detected
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
21
|
-
import { resolve, dirname } from 'path';
|
|
22
|
-
import { fileURLToPath } from 'url';
|
|
23
|
-
import { parse as parseYaml } from 'yaml';
|
|
24
|
-
import { getConfig, getResolvedPaths } from '../config.ts';
|
|
25
|
-
import { getCurrentTier, getLicenseInfo, daysUntilExpiry } from '../license.ts';
|
|
26
|
-
import { readSettingsAtPath } from '../lib/settings-local.ts';
|
|
27
|
-
import { getExpectedHookFiles } from '../lib/hook-registry.ts';
|
|
28
|
-
|
|
29
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
-
const __dirname = dirname(__filename);
|
|
31
|
-
|
|
32
|
-
// ============================================================
|
|
33
|
-
// Types
|
|
34
|
-
// ============================================================
|
|
35
|
-
|
|
36
|
-
interface CheckResult {
|
|
37
|
-
name: string;
|
|
38
|
-
status: 'pass' | 'fail' | 'warn';
|
|
39
|
-
detail: string;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ============================================================
|
|
43
|
-
// Hook Files
|
|
44
|
-
// ============================================================
|
|
45
|
-
|
|
46
|
-
// P-H001 (plan-stage-c-high-batch): drift-fix. EXPECTED_HOOKS now consumes
|
|
47
|
-
// the lib/hook-registry.ts SoT; the registry-parity drift-guard test asserts
|
|
48
|
-
// this list matches `src/hooks/*.ts` AND `buildHooksConfig()`. Previously
|
|
49
|
-
// a hand-maintained list of 11 entries silently drifted from the 16 hooks
|
|
50
|
-
// actually registered by installHooks(), letting hook files go missing
|
|
51
|
-
// without doctor noticing.
|
|
52
|
-
const EXPECTED_HOOKS: readonly string[] = getExpectedHookFiles();
|
|
53
|
-
|
|
54
|
-
// ============================================================
|
|
55
|
-
// Individual Checks
|
|
56
|
-
// ============================================================
|
|
57
|
-
|
|
58
|
-
function checkConfig(projectRoot: string): CheckResult {
|
|
59
|
-
const configPath = resolve(projectRoot, 'massu.config.yaml');
|
|
60
|
-
if (!existsSync(configPath)) {
|
|
61
|
-
return { name: 'Configuration', status: 'fail', detail: 'massu.config.yaml not found. Run: npx massu init' };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
66
|
-
// pattern-scanner-allow: yaml-parse — reason: `massu doctor` config-integrity diagnostic; runs BEFORE getConfig() to verify the file is well-formed YAML so the user sees a parse error here, not a Zod-validation surprise downstream.
|
|
67
|
-
const parsed = parseYaml(content);
|
|
68
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
69
|
-
return { name: 'Configuration', status: 'fail', detail: 'massu.config.yaml is empty or invalid YAML' };
|
|
70
|
-
}
|
|
71
|
-
return { name: 'Configuration', status: 'pass', detail: 'massu.config.yaml found and valid' };
|
|
72
|
-
} catch (err) {
|
|
73
|
-
return { name: 'Configuration', status: 'fail', detail: `massu.config.yaml parse error: ${err instanceof Error ? err.message : String(err)}` };
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function checkMcpServer(projectRoot: string): CheckResult {
|
|
78
|
-
const mcpPath = getResolvedPaths().mcpJsonPath;
|
|
79
|
-
if (!existsSync(mcpPath)) {
|
|
80
|
-
return { name: 'MCP Server', status: 'fail', detail: '.mcp.json not found. Run: npx massu init' };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const content = JSON.parse(readFileSync(mcpPath, 'utf-8'));
|
|
85
|
-
const servers = content.mcpServers ?? {};
|
|
86
|
-
if (!servers.massu) {
|
|
87
|
-
return { name: 'MCP Server', status: 'fail', detail: 'massu not registered in .mcp.json. Run: npx massu init' };
|
|
88
|
-
}
|
|
89
|
-
return { name: 'MCP Server', status: 'pass', detail: 'Registered in .mcp.json' };
|
|
90
|
-
} catch (err) {
|
|
91
|
-
return { name: 'MCP Server', status: 'fail', detail: `.mcp.json parse error: ${err instanceof Error ? err.message : String(err)}` };
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function checkHooksConfig(projectRoot: string): CheckResult {
|
|
96
|
-
const settingsPath = getResolvedPaths().settingsLocalPath;
|
|
97
|
-
if (!existsSync(settingsPath)) {
|
|
98
|
-
return { name: 'Hooks Config', status: 'fail', detail: '.claude/settings.local.json not found. Run: npx massu init' };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
const content = readSettingsAtPath(settingsPath);
|
|
103
|
-
if (!content.hooks) {
|
|
104
|
-
return { name: 'Hooks Config', status: 'fail', detail: 'No hooks configured. Run: npx massu install-hooks' };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Count configured hooks
|
|
108
|
-
let hookCount = 0;
|
|
109
|
-
for (const groups of Object.values(content.hooks)) {
|
|
110
|
-
if (Array.isArray(groups)) {
|
|
111
|
-
for (const group of groups) {
|
|
112
|
-
const g = group as { hooks?: unknown[] };
|
|
113
|
-
if (Array.isArray(g.hooks)) {
|
|
114
|
-
hookCount += g.hooks.length;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (hookCount === 0) {
|
|
121
|
-
return { name: 'Hooks Config', status: 'fail', detail: 'Hooks section exists but no hooks configured' };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return { name: 'Hooks Config', status: 'pass', detail: `${hookCount} hooks configured` };
|
|
125
|
-
} catch (err) {
|
|
126
|
-
return { name: 'Hooks Config', status: 'fail', detail: `settings.local.json parse error: ${err instanceof Error ? err.message : String(err)}` };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function checkHookFiles(projectRoot: string): CheckResult {
|
|
131
|
-
// Check node_modules path first
|
|
132
|
-
const nodeModulesHooksDir = resolve(projectRoot, 'node_modules/@massu/core/dist/hooks');
|
|
133
|
-
let hooksDir = nodeModulesHooksDir;
|
|
134
|
-
|
|
135
|
-
if (!existsSync(nodeModulesHooksDir)) {
|
|
136
|
-
// Check relative to this file (development mode)
|
|
137
|
-
const devHooksDir = resolve(__dirname, '../../dist/hooks');
|
|
138
|
-
if (existsSync(devHooksDir)) {
|
|
139
|
-
hooksDir = devHooksDir;
|
|
140
|
-
} else {
|
|
141
|
-
return { name: 'Hook Files', status: 'fail', detail: 'Compiled hooks not found. Run: npm install @massu/core' };
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const missing: string[] = [];
|
|
146
|
-
for (const hookFile of EXPECTED_HOOKS) {
|
|
147
|
-
if (!existsSync(resolve(hooksDir, hookFile))) {
|
|
148
|
-
missing.push(hookFile);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (missing.length > 0) {
|
|
153
|
-
return { name: 'Hook Files', status: 'fail', detail: `Missing hooks: ${missing.join(', ')}` };
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return { name: 'Hook Files', status: 'pass', detail: `${EXPECTED_HOOKS.length}/${EXPECTED_HOOKS.length} compiled hooks present` };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function checkNativeModules(): Promise<CheckResult> {
|
|
160
|
-
try {
|
|
161
|
-
await import('better-sqlite3');
|
|
162
|
-
return { name: 'Native Modules', status: 'pass', detail: 'better-sqlite3 loads correctly' };
|
|
163
|
-
} catch (err) {
|
|
164
|
-
return { name: 'Native Modules', status: 'fail', detail: `better-sqlite3 failed: ${err instanceof Error ? err.message : String(err)}. Try: npm rebuild better-sqlite3` };
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function checkNodeVersion(): CheckResult {
|
|
169
|
-
const version = process.versions.node;
|
|
170
|
-
const major = parseInt(version.split('.')[0], 10);
|
|
171
|
-
|
|
172
|
-
if (major >= 18) {
|
|
173
|
-
return { name: 'Node.js', status: 'pass', detail: `v${version} (>= 18 required)` };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return { name: 'Node.js', status: 'fail', detail: `v${version} — Node.js 18+ is required` };
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function checkGitRepo(projectRoot: string): Promise<CheckResult> {
|
|
180
|
-
const gitDir = resolve(projectRoot, '.git');
|
|
181
|
-
if (!existsSync(gitDir)) {
|
|
182
|
-
return { name: 'Git Repository', status: 'warn', detail: 'Not a git repository (optional but recommended)' };
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
const { spawnSync } = await import('child_process');
|
|
187
|
-
const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
188
|
-
encoding: 'utf-8',
|
|
189
|
-
timeout: 5000,
|
|
190
|
-
cwd: projectRoot,
|
|
191
|
-
});
|
|
192
|
-
const branch = result.stdout?.trim() ?? 'unknown';
|
|
193
|
-
return { name: 'Git Repository', status: 'pass', detail: `Detected (branch: ${branch})` };
|
|
194
|
-
} catch {
|
|
195
|
-
return { name: 'Git Repository', status: 'pass', detail: 'Detected' };
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function checkKnowledgeDb(projectRoot: string): CheckResult {
|
|
200
|
-
// Knowledge DB is the memory DB
|
|
201
|
-
const knowledgeDbPath = getResolvedPaths().memoryDbPath;
|
|
202
|
-
if (!existsSync(knowledgeDbPath)) {
|
|
203
|
-
return {
|
|
204
|
-
name: 'Knowledge DB',
|
|
205
|
-
status: 'warn',
|
|
206
|
-
detail: '.massu/memory.db not found (will auto-create on first session)',
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
return { name: 'Knowledge DB', status: 'pass', detail: '.massu/memory.db exists' };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function checkMemoryDir(_projectRoot: string): CheckResult {
|
|
213
|
-
// Memory dir: ~/.claude/projects/-<encoded-root>/memory/ (resolved via config)
|
|
214
|
-
const memoryDir = getResolvedPaths().memoryDir;
|
|
215
|
-
if (!existsSync(memoryDir)) {
|
|
216
|
-
return {
|
|
217
|
-
name: 'Memory Directory',
|
|
218
|
-
status: 'warn',
|
|
219
|
-
detail: 'Memory directory not found. Run: npx massu init',
|
|
220
|
-
};
|
|
221
|
-
}
|
|
222
|
-
return { name: 'Memory Directory', status: 'pass', detail: `Memory directory exists` };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function checkShellHooksWired(_projectRoot: string): CheckResult {
|
|
226
|
-
// Verify that .claude/settings.local.json has hooks configured (shell hooks are wired)
|
|
227
|
-
const settingsPath = getResolvedPaths().settingsLocalPath;
|
|
228
|
-
if (!existsSync(settingsPath)) {
|
|
229
|
-
return {
|
|
230
|
-
name: 'Shell Hooks',
|
|
231
|
-
status: 'fail',
|
|
232
|
-
detail: 'settings.local.json not found. Run: npx massu install-hooks',
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
const content = readSettingsAtPath(settingsPath);
|
|
238
|
-
const hooks = (content.hooks ?? {}) as Record<string, unknown>;
|
|
239
|
-
const hasSessionStart = Array.isArray(hooks.SessionStart) && hooks.SessionStart.length > 0;
|
|
240
|
-
const hasPreToolUse = Array.isArray(hooks.PreToolUse) && hooks.PreToolUse.length > 0;
|
|
241
|
-
if (!hasSessionStart && !hasPreToolUse) {
|
|
242
|
-
return {
|
|
243
|
-
name: 'Shell Hooks',
|
|
244
|
-
status: 'fail',
|
|
245
|
-
detail: 'No lifecycle hooks wired. Run: npx massu install-hooks',
|
|
246
|
-
};
|
|
247
|
-
}
|
|
248
|
-
return { name: 'Shell Hooks', status: 'pass', detail: 'Lifecycle hooks wired in settings.local.json' };
|
|
249
|
-
} catch (err) {
|
|
250
|
-
return {
|
|
251
|
-
name: 'Shell Hooks',
|
|
252
|
-
status: 'fail',
|
|
253
|
-
detail: `settings.local.json parse error: ${err instanceof Error ? err.message : String(err)}`,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
async function checkLicenseStatus(): Promise<CheckResult> {
|
|
259
|
-
try {
|
|
260
|
-
const tier = await getCurrentTier();
|
|
261
|
-
const info = await getLicenseInfo();
|
|
262
|
-
|
|
263
|
-
if (tier === 'free' && !info.validUntil) {
|
|
264
|
-
return { name: 'License', status: 'pass', detail: 'Free (no API key configured)' };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const days = await daysUntilExpiry();
|
|
268
|
-
if (days >= 0 && info.validUntil) {
|
|
269
|
-
return {
|
|
270
|
-
name: 'License',
|
|
271
|
-
status: 'pass',
|
|
272
|
-
detail: `${tier.charAt(0).toUpperCase() + tier.slice(1)} (valid until ${info.validUntil})`,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return {
|
|
277
|
-
name: 'License',
|
|
278
|
-
status: 'pass',
|
|
279
|
-
detail: `${tier.charAt(0).toUpperCase() + tier.slice(1)} (valid)`,
|
|
280
|
-
};
|
|
281
|
-
} catch (err) {
|
|
282
|
-
return {
|
|
283
|
-
name: 'License',
|
|
284
|
-
status: 'warn',
|
|
285
|
-
detail: `Could not check license: ${err instanceof Error ? err.message : String(err)}`,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function checkPythonHealth(projectRoot: string): CheckResult | null {
|
|
291
|
-
const config = getConfig();
|
|
292
|
-
if (!config.python?.root) return null;
|
|
293
|
-
|
|
294
|
-
const pythonRoot = resolve(projectRoot, config.python.root);
|
|
295
|
-
if (!existsSync(pythonRoot)) {
|
|
296
|
-
return {
|
|
297
|
-
name: 'Python',
|
|
298
|
-
status: 'fail',
|
|
299
|
-
detail: `Python root directory not found: ${config.python.root}`,
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Count .py files recursively (shallow scan for performance)
|
|
304
|
-
let pyFileCount = 0;
|
|
305
|
-
let routeCount = 0;
|
|
306
|
-
let modelCount = 0;
|
|
307
|
-
const initPyMissing: string[] = [];
|
|
308
|
-
|
|
309
|
-
function scanDir(dir: string, depth: number): void {
|
|
310
|
-
if (depth > 5) return;
|
|
311
|
-
try {
|
|
312
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
313
|
-
for (const entry of entries) {
|
|
314
|
-
if (entry.isDirectory()) {
|
|
315
|
-
const excludeDirs = config.python?.exclude_dirs || ['__pycache__', '.venv', 'venv', '.mypy_cache', '.pytest_cache'];
|
|
316
|
-
if (!excludeDirs.includes(entry.name)) {
|
|
317
|
-
const subdir = resolve(dir, entry.name);
|
|
318
|
-
// Check for __init__.py in package directories
|
|
319
|
-
if (depth <= 2 && !existsSync(resolve(subdir, '__init__.py'))) {
|
|
320
|
-
// Only flag directories that contain .py files
|
|
321
|
-
try {
|
|
322
|
-
const subEntries = readdirSync(subdir);
|
|
323
|
-
if (subEntries.some(f => f.endsWith('.py') && f !== '__init__.py')) {
|
|
324
|
-
initPyMissing.push(entry.name);
|
|
325
|
-
}
|
|
326
|
-
} catch { /* skip */ }
|
|
327
|
-
}
|
|
328
|
-
scanDir(subdir, depth + 1);
|
|
329
|
-
}
|
|
330
|
-
} else if (entry.name.endsWith('.py')) {
|
|
331
|
-
pyFileCount++;
|
|
332
|
-
// Rough heuristic for routes and models
|
|
333
|
-
if (entry.name === 'routes.py' || entry.name === 'router.py' || entry.name === 'endpoints.py') {
|
|
334
|
-
routeCount++;
|
|
335
|
-
}
|
|
336
|
-
if (entry.name === 'models.py' || entry.name === 'model.py' || entry.name === 'schemas.py') {
|
|
337
|
-
modelCount++;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
} catch { /* skip unreadable dirs */ }
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
scanDir(pythonRoot, 0);
|
|
345
|
-
|
|
346
|
-
if (pyFileCount === 0) {
|
|
347
|
-
return {
|
|
348
|
-
name: 'Python',
|
|
349
|
-
status: 'warn',
|
|
350
|
-
detail: `Python root ${config.python.root} exists but no .py files found`,
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const parts: string[] = [`${pyFileCount} .py files`];
|
|
355
|
-
if (routeCount > 0) parts.push(`${routeCount} route files`);
|
|
356
|
-
if (modelCount > 0) parts.push(`${modelCount} model files`);
|
|
357
|
-
if (initPyMissing.length > 0) {
|
|
358
|
-
parts.push(`missing __init__.py: ${initPyMissing.slice(0, 3).join(', ')}${initPyMissing.length > 3 ? '...' : ''}`);
|
|
359
|
-
return {
|
|
360
|
-
name: 'Python',
|
|
361
|
-
status: 'warn',
|
|
362
|
-
detail: parts.join(', '),
|
|
363
|
-
};
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
return {
|
|
367
|
-
name: 'Python',
|
|
368
|
-
status: 'pass',
|
|
369
|
-
detail: parts.join(', '),
|
|
370
|
-
};
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// ============================================================
|
|
374
|
-
// Main Doctor Flow
|
|
375
|
-
// ============================================================
|
|
376
|
-
|
|
377
|
-
export async function runDoctor(): Promise<void> {
|
|
378
|
-
const projectRoot = process.cwd();
|
|
379
|
-
|
|
380
|
-
console.log('');
|
|
381
|
-
console.log('Massu AI Health Check');
|
|
382
|
-
console.log('=====================');
|
|
383
|
-
console.log('');
|
|
384
|
-
|
|
385
|
-
const checks: CheckResult[] = [
|
|
386
|
-
checkConfig(projectRoot),
|
|
387
|
-
checkMcpServer(projectRoot),
|
|
388
|
-
checkHooksConfig(projectRoot),
|
|
389
|
-
checkHookFiles(projectRoot),
|
|
390
|
-
checkKnowledgeDb(projectRoot),
|
|
391
|
-
checkMemoryDir(projectRoot),
|
|
392
|
-
checkShellHooksWired(projectRoot),
|
|
393
|
-
await checkNativeModules(),
|
|
394
|
-
checkNodeVersion(),
|
|
395
|
-
await checkGitRepo(projectRoot),
|
|
396
|
-
await checkLicenseStatus(),
|
|
397
|
-
];
|
|
398
|
-
|
|
399
|
-
// Add Python health check if configured
|
|
400
|
-
const pythonCheck = checkPythonHealth(projectRoot);
|
|
401
|
-
if (pythonCheck) checks.push(pythonCheck);
|
|
402
|
-
|
|
403
|
-
let passed = 0;
|
|
404
|
-
let failed = 0;
|
|
405
|
-
let warned = 0;
|
|
406
|
-
|
|
407
|
-
for (const check of checks) {
|
|
408
|
-
const icon = check.status === 'pass' ? '\u2713' : check.status === 'warn' ? '!' : '\u2717';
|
|
409
|
-
const pad = check.name.padEnd(20);
|
|
410
|
-
console.log(` ${icon} ${pad} ${check.detail}`);
|
|
411
|
-
|
|
412
|
-
if (check.status === 'pass') passed++;
|
|
413
|
-
else if (check.status === 'fail') failed++;
|
|
414
|
-
else warned++;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
console.log('');
|
|
418
|
-
|
|
419
|
-
if (failed === 0) {
|
|
420
|
-
const total = passed + warned;
|
|
421
|
-
console.log(`Status: HEALTHY (${passed}/${total} checks passed${warned > 0 ? `, ${warned} warnings` : ''})`);
|
|
422
|
-
} else {
|
|
423
|
-
console.log(`Status: UNHEALTHY (${failed} check${failed > 1 ? 's' : ''} failed)`);
|
|
424
|
-
console.log('');
|
|
425
|
-
console.log('Fix issues above, then run: npx massu doctor');
|
|
426
|
-
}
|
|
427
|
-
console.log('');
|
|
428
|
-
|
|
429
|
-
process.exit(failed > 0 ? 1 : 0);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// ============================================================
|
|
433
|
-
// Validate Config Command
|
|
434
|
-
// ============================================================
|
|
435
|
-
|
|
436
|
-
export async function runValidateConfig(): Promise<void> {
|
|
437
|
-
const projectRoot = process.cwd();
|
|
438
|
-
const configPath = resolve(projectRoot, 'massu.config.yaml');
|
|
439
|
-
|
|
440
|
-
if (!existsSync(configPath)) {
|
|
441
|
-
console.error('Error: massu.config.yaml not found in current directory');
|
|
442
|
-
console.error('Run: npx massu init');
|
|
443
|
-
process.exit(1);
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
try {
|
|
448
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
449
|
-
// pattern-scanner-allow: yaml-parse — reason: `massu doctor` deep-validation pass; same diagnostic-mode reasoning as the upper checkConfig() — surfaces YAML errors with operator-friendly stderr before any getConfig() call.
|
|
450
|
-
const parsed = parseYaml(content);
|
|
451
|
-
|
|
452
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
453
|
-
console.error('Error: massu.config.yaml is empty or not a valid YAML object');
|
|
454
|
-
process.exit(1);
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Check required fields
|
|
459
|
-
const warnings: string[] = [];
|
|
460
|
-
|
|
461
|
-
if (!parsed.project?.name) {
|
|
462
|
-
warnings.push('Missing project.name (will default to "my-project")');
|
|
463
|
-
}
|
|
464
|
-
if (!parsed.toolPrefix) {
|
|
465
|
-
warnings.push('Missing toolPrefix (will default to "massu")');
|
|
466
|
-
}
|
|
467
|
-
if (!parsed.framework?.type) {
|
|
468
|
-
warnings.push('Missing framework.type (will default to "typescript")');
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
console.log('');
|
|
472
|
-
if (warnings.length === 0) {
|
|
473
|
-
console.log('massu.config.yaml is valid');
|
|
474
|
-
} else {
|
|
475
|
-
console.log('massu.config.yaml parsed successfully with warnings:');
|
|
476
|
-
for (const w of warnings) {
|
|
477
|
-
console.log(` ! ${w}`);
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
console.log('');
|
|
481
|
-
} catch (err) {
|
|
482
|
-
console.error(`Error parsing massu.config.yaml: ${err instanceof Error ? err.message : String(err)}`);
|
|
483
|
-
process.exit(1);
|
|
484
|
-
}
|
|
485
|
-
}
|