@massu/core 0.1.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/_shared-preamble.md +76 -0
- package/commands/massu-audit-deps.md +211 -0
- package/commands/massu-changelog.md +174 -0
- package/commands/massu-cleanup.md +315 -0
- package/commands/massu-commit.md +481 -0
- package/commands/massu-create-plan.md +752 -0
- package/commands/massu-dead-code.md +131 -0
- package/commands/massu-debug.md +484 -0
- package/commands/massu-deploy.md +91 -0
- package/commands/massu-deps.md +374 -0
- package/commands/massu-doc-gen.md +279 -0
- package/commands/massu-docs.md +364 -0
- package/commands/massu-estimate.md +313 -0
- package/commands/massu-golden-path.md +973 -0
- package/commands/massu-guide.md +167 -0
- package/commands/massu-hotfix.md +480 -0
- package/commands/massu-loop-playwright.md +837 -0
- package/commands/massu-loop.md +775 -0
- package/commands/massu-new-feature.md +511 -0
- package/commands/massu-parity.md +214 -0
- package/commands/massu-plan.md +456 -0
- package/commands/massu-push-light.md +207 -0
- package/commands/massu-push.md +434 -0
- package/commands/massu-refactor.md +410 -0
- package/commands/massu-release.md +363 -0
- package/commands/massu-review.md +238 -0
- package/commands/massu-simplify.md +281 -0
- package/commands/massu-status.md +278 -0
- package/commands/massu-tdd.md +201 -0
- package/commands/massu-test.md +516 -0
- package/commands/massu-verify-playwright.md +281 -0
- package/commands/massu-verify.md +667 -0
- package/dist/cli.js +7772 -3140
- package/dist/hooks/cost-tracker.js +103 -40
- package/dist/hooks/post-edit-context.js +74 -8
- package/dist/hooks/post-tool-use.js +268 -106
- package/dist/hooks/pre-compact.js +167 -43
- package/dist/hooks/pre-delete-check.js +159 -42
- package/dist/hooks/quality-event.js +103 -40
- package/dist/hooks/security-gate.js +29 -0
- package/dist/hooks/session-end.js +143 -84
- package/dist/hooks/session-start.js +186 -49
- package/dist/hooks/user-prompt.js +189 -43
- package/package.json +10 -15
- package/src/adr-generator.ts +9 -2
- package/src/analytics.ts +9 -3
- package/src/audit-trail.ts +10 -3
- package/src/backfill-sessions.ts +5 -4
- package/src/cli.ts +6 -0
- package/src/cloud-sync.ts +14 -18
- package/src/commands/doctor.ts +193 -6
- package/src/commands/init.ts +230 -5
- package/src/commands/install-commands.ts +137 -0
- package/src/config.ts +68 -2
- package/src/cost-tracker.ts +11 -6
- package/src/db.ts +115 -2
- package/src/dependency-scorer.ts +9 -2
- package/src/docs-tools.ts +21 -16
- package/src/hooks/post-edit-context.ts +4 -4
- package/src/hooks/post-tool-use.ts +130 -0
- package/src/hooks/pre-compact.ts +23 -1
- package/src/hooks/pre-delete-check.ts +92 -4
- package/src/hooks/security-gate.ts +32 -0
- package/src/hooks/session-end.ts +3 -3
- package/src/hooks/session-start.ts +99 -6
- package/src/hooks/user-prompt.ts +46 -1
- package/src/import-resolver.ts +2 -1
- package/src/knowledge-db.ts +169 -0
- package/src/knowledge-indexer.ts +704 -0
- package/src/knowledge-tools.ts +1413 -0
- package/src/license.ts +482 -0
- package/src/memory-db.ts +1364 -23
- package/src/memory-tools.ts +14 -15
- package/src/observability-tools.ts +13 -2
- package/src/observation-extractor.ts +11 -4
- package/src/page-deps.ts +3 -2
- package/src/prompt-analyzer.ts +9 -2
- package/src/python/coupling-detector.ts +124 -0
- package/src/python/domain-enforcer.ts +83 -0
- package/src/python/impact-analyzer.ts +95 -0
- package/src/python/import-parser.ts +244 -0
- package/src/python/import-resolver.ts +135 -0
- package/src/python/migration-indexer.ts +115 -0
- package/src/python/migration-parser.ts +332 -0
- package/src/python/model-indexer.ts +70 -0
- package/src/python/model-parser.ts +279 -0
- package/src/python/route-indexer.ts +58 -0
- package/src/python/route-parser.ts +317 -0
- package/src/python-tools.ts +629 -0
- package/src/regression-detector.ts +9 -3
- package/src/security-scorer.ts +9 -2
- package/src/sentinel-db.ts +45 -89
- package/src/sentinel-tools.ts +8 -11
- package/src/server.ts +29 -7
- package/src/session-archiver.ts +4 -5
- package/src/team-knowledge.ts +9 -2
- package/src/tools.ts +1032 -44
- package/src/validate-features-runner.ts +0 -1
- package/src/validation-engine.ts +9 -2
- package/README.md +0 -40
- package/dist/server.js +0 -7008
- package/src/__tests__/adr-generator.test.ts +0 -260
- package/src/__tests__/analytics.test.ts +0 -282
- package/src/__tests__/audit-trail.test.ts +0 -382
- package/src/__tests__/backfill-sessions.test.ts +0 -690
- package/src/__tests__/cli.test.ts +0 -290
- package/src/__tests__/cloud-sync.test.ts +0 -261
- package/src/__tests__/config-sections.test.ts +0 -359
- package/src/__tests__/config.test.ts +0 -732
- package/src/__tests__/cost-tracker.test.ts +0 -348
- package/src/__tests__/db.test.ts +0 -177
- package/src/__tests__/dependency-scorer.test.ts +0 -325
- package/src/__tests__/docs-integration.test.ts +0 -178
- package/src/__tests__/docs-tools.test.ts +0 -199
- package/src/__tests__/domains.test.ts +0 -236
- package/src/__tests__/hooks.test.ts +0 -221
- package/src/__tests__/import-resolver.test.ts +0 -95
- package/src/__tests__/integration/path-traversal.test.ts +0 -134
- package/src/__tests__/integration/pricing-consistency.test.ts +0 -88
- package/src/__tests__/integration/tool-registration.test.ts +0 -146
- package/src/__tests__/memory-db.test.ts +0 -404
- package/src/__tests__/memory-enhancements.test.ts +0 -316
- package/src/__tests__/memory-tools.test.ts +0 -199
- package/src/__tests__/middleware-tree.test.ts +0 -177
- package/src/__tests__/observability-tools.test.ts +0 -595
- package/src/__tests__/observability.test.ts +0 -437
- package/src/__tests__/observation-extractor.test.ts +0 -167
- package/src/__tests__/page-deps.test.ts +0 -60
- package/src/__tests__/prompt-analyzer.test.ts +0 -298
- package/src/__tests__/regression-detector.test.ts +0 -295
- package/src/__tests__/rules.test.ts +0 -87
- package/src/__tests__/schema-mapper.test.ts +0 -29
- package/src/__tests__/security-scorer.test.ts +0 -238
- package/src/__tests__/security-utils.test.ts +0 -175
- package/src/__tests__/sentinel-db.test.ts +0 -491
- package/src/__tests__/sentinel-scanner.test.ts +0 -750
- package/src/__tests__/sentinel-tools.test.ts +0 -324
- package/src/__tests__/sentinel-types.test.ts +0 -750
- package/src/__tests__/server.test.ts +0 -452
- package/src/__tests__/session-archiver.test.ts +0 -524
- package/src/__tests__/session-state-generator.test.ts +0 -900
- package/src/__tests__/team-knowledge.test.ts +0 -327
- package/src/__tests__/tools.test.ts +0 -340
- package/src/__tests__/transcript-parser.test.ts +0 -195
- package/src/__tests__/trpc-index.test.ts +0 -25
- package/src/__tests__/validate-features-runner.test.ts +0 -517
- package/src/__tests__/validation-engine.test.ts +0 -300
- package/src/core-tools.ts +0 -685
- package/src/memory-queries.ts +0 -804
- package/src/memory-schema.ts +0 -546
- package/src/tool-helpers.ts +0 -41
package/src/commands/doctor.ts
CHANGED
|
@@ -9,15 +9,20 @@
|
|
|
9
9
|
* 2. .mcp.json has massu entry
|
|
10
10
|
* 3. .claude/settings.local.json has hooks config
|
|
11
11
|
* 4. All 11 compiled hook files exist
|
|
12
|
-
* 5.
|
|
13
|
-
* 6.
|
|
14
|
-
* 7.
|
|
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
|
|
15
18
|
*/
|
|
16
19
|
|
|
17
|
-
import { existsSync, readFileSync } from 'fs';
|
|
20
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
18
21
|
import { resolve, dirname } from 'path';
|
|
19
22
|
import { fileURLToPath } from 'url';
|
|
20
23
|
import { parse as parseYaml } from 'yaml';
|
|
24
|
+
import { getConfig, getResolvedPaths } from '../config.ts';
|
|
25
|
+
import { getCurrentTier, getLicenseInfo, daysUntilExpiry } from '../license.ts';
|
|
21
26
|
|
|
22
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
23
28
|
const __dirname = dirname(__filename);
|
|
@@ -73,7 +78,7 @@ function checkConfig(projectRoot: string): CheckResult {
|
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
function checkMcpServer(projectRoot: string): CheckResult {
|
|
76
|
-
const mcpPath =
|
|
81
|
+
const mcpPath = getResolvedPaths().mcpJsonPath;
|
|
77
82
|
if (!existsSync(mcpPath)) {
|
|
78
83
|
return { name: 'MCP Server', status: 'fail', detail: '.mcp.json not found. Run: npx massu init' };
|
|
79
84
|
}
|
|
@@ -91,7 +96,7 @@ function checkMcpServer(projectRoot: string): CheckResult {
|
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
function checkHooksConfig(projectRoot: string): CheckResult {
|
|
94
|
-
const settingsPath =
|
|
99
|
+
const settingsPath = getResolvedPaths().settingsLocalPath;
|
|
95
100
|
if (!existsSync(settingsPath)) {
|
|
96
101
|
return { name: 'Hooks Config', status: 'fail', detail: '.claude/settings.local.json not found. Run: npx massu init' };
|
|
97
102
|
}
|
|
@@ -194,6 +199,180 @@ async function checkGitRepo(projectRoot: string): Promise<CheckResult> {
|
|
|
194
199
|
}
|
|
195
200
|
}
|
|
196
201
|
|
|
202
|
+
function checkKnowledgeDb(projectRoot: string): CheckResult {
|
|
203
|
+
// Knowledge DB is the memory DB
|
|
204
|
+
const knowledgeDbPath = getResolvedPaths().memoryDbPath;
|
|
205
|
+
if (!existsSync(knowledgeDbPath)) {
|
|
206
|
+
return {
|
|
207
|
+
name: 'Knowledge DB',
|
|
208
|
+
status: 'warn',
|
|
209
|
+
detail: '.massu/memory.db not found (will auto-create on first session)',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
return { name: 'Knowledge DB', status: 'pass', detail: '.massu/memory.db exists' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function checkMemoryDir(_projectRoot: string): CheckResult {
|
|
216
|
+
// Memory dir: ~/.claude/projects/-<encoded-root>/memory/ (resolved via config)
|
|
217
|
+
const memoryDir = getResolvedPaths().memoryDir;
|
|
218
|
+
if (!existsSync(memoryDir)) {
|
|
219
|
+
return {
|
|
220
|
+
name: 'Memory Directory',
|
|
221
|
+
status: 'warn',
|
|
222
|
+
detail: 'Memory directory not found. Run: npx massu init',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
return { name: 'Memory Directory', status: 'pass', detail: `Memory directory exists` };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function checkShellHooksWired(_projectRoot: string): CheckResult {
|
|
229
|
+
// Verify that .claude/settings.local.json has hooks configured (shell hooks are wired)
|
|
230
|
+
const settingsPath = getResolvedPaths().settingsLocalPath;
|
|
231
|
+
if (!existsSync(settingsPath)) {
|
|
232
|
+
return {
|
|
233
|
+
name: 'Shell Hooks',
|
|
234
|
+
status: 'fail',
|
|
235
|
+
detail: 'settings.local.json not found. Run: npx massu install-hooks',
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const content = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
241
|
+
const hooks = content.hooks ?? {};
|
|
242
|
+
const hasSessionStart = Array.isArray(hooks.SessionStart) && hooks.SessionStart.length > 0;
|
|
243
|
+
const hasPreToolUse = Array.isArray(hooks.PreToolUse) && hooks.PreToolUse.length > 0;
|
|
244
|
+
if (!hasSessionStart && !hasPreToolUse) {
|
|
245
|
+
return {
|
|
246
|
+
name: 'Shell Hooks',
|
|
247
|
+
status: 'fail',
|
|
248
|
+
detail: 'No lifecycle hooks wired. Run: npx massu install-hooks',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return { name: 'Shell Hooks', status: 'pass', detail: 'Lifecycle hooks wired in settings.local.json' };
|
|
252
|
+
} catch (err) {
|
|
253
|
+
return {
|
|
254
|
+
name: 'Shell Hooks',
|
|
255
|
+
status: 'fail',
|
|
256
|
+
detail: `settings.local.json parse error: ${err instanceof Error ? err.message : String(err)}`,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function checkLicenseStatus(): Promise<CheckResult> {
|
|
262
|
+
try {
|
|
263
|
+
const tier = await getCurrentTier();
|
|
264
|
+
const info = await getLicenseInfo();
|
|
265
|
+
|
|
266
|
+
if (tier === 'free' && !info.validUntil) {
|
|
267
|
+
return { name: 'License', status: 'pass', detail: 'Free (no API key configured)' };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const days = await daysUntilExpiry();
|
|
271
|
+
if (days >= 0 && info.validUntil) {
|
|
272
|
+
return {
|
|
273
|
+
name: 'License',
|
|
274
|
+
status: 'pass',
|
|
275
|
+
detail: `${tier.charAt(0).toUpperCase() + tier.slice(1)} (valid until ${info.validUntil})`,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
name: 'License',
|
|
281
|
+
status: 'pass',
|
|
282
|
+
detail: `${tier.charAt(0).toUpperCase() + tier.slice(1)} (valid)`,
|
|
283
|
+
};
|
|
284
|
+
} catch (err) {
|
|
285
|
+
return {
|
|
286
|
+
name: 'License',
|
|
287
|
+
status: 'warn',
|
|
288
|
+
detail: `Could not check license: ${err instanceof Error ? err.message : String(err)}`,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function checkPythonHealth(projectRoot: string): CheckResult | null {
|
|
294
|
+
const config = getConfig();
|
|
295
|
+
if (!config.python?.root) return null;
|
|
296
|
+
|
|
297
|
+
const pythonRoot = resolve(projectRoot, config.python.root);
|
|
298
|
+
if (!existsSync(pythonRoot)) {
|
|
299
|
+
return {
|
|
300
|
+
name: 'Python',
|
|
301
|
+
status: 'fail',
|
|
302
|
+
detail: `Python root directory not found: ${config.python.root}`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Count .py files recursively (shallow scan for performance)
|
|
307
|
+
let pyFileCount = 0;
|
|
308
|
+
let routeCount = 0;
|
|
309
|
+
let modelCount = 0;
|
|
310
|
+
const initPyMissing: string[] = [];
|
|
311
|
+
|
|
312
|
+
function scanDir(dir: string, depth: number): void {
|
|
313
|
+
if (depth > 5) return;
|
|
314
|
+
try {
|
|
315
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
if (entry.isDirectory()) {
|
|
318
|
+
const excludeDirs = config.python?.exclude_dirs || ['__pycache__', '.venv', 'venv', '.mypy_cache', '.pytest_cache'];
|
|
319
|
+
if (!excludeDirs.includes(entry.name)) {
|
|
320
|
+
const subdir = resolve(dir, entry.name);
|
|
321
|
+
// Check for __init__.py in package directories
|
|
322
|
+
if (depth <= 2 && !existsSync(resolve(subdir, '__init__.py'))) {
|
|
323
|
+
// Only flag directories that contain .py files
|
|
324
|
+
try {
|
|
325
|
+
const subEntries = readdirSync(subdir);
|
|
326
|
+
if (subEntries.some(f => f.endsWith('.py') && f !== '__init__.py')) {
|
|
327
|
+
initPyMissing.push(entry.name);
|
|
328
|
+
}
|
|
329
|
+
} catch { /* skip */ }
|
|
330
|
+
}
|
|
331
|
+
scanDir(subdir, depth + 1);
|
|
332
|
+
}
|
|
333
|
+
} else if (entry.name.endsWith('.py')) {
|
|
334
|
+
pyFileCount++;
|
|
335
|
+
// Rough heuristic for routes and models
|
|
336
|
+
if (entry.name === 'routes.py' || entry.name === 'router.py' || entry.name === 'endpoints.py') {
|
|
337
|
+
routeCount++;
|
|
338
|
+
}
|
|
339
|
+
if (entry.name === 'models.py' || entry.name === 'model.py' || entry.name === 'schemas.py') {
|
|
340
|
+
modelCount++;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} catch { /* skip unreadable dirs */ }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
scanDir(pythonRoot, 0);
|
|
348
|
+
|
|
349
|
+
if (pyFileCount === 0) {
|
|
350
|
+
return {
|
|
351
|
+
name: 'Python',
|
|
352
|
+
status: 'warn',
|
|
353
|
+
detail: `Python root ${config.python.root} exists but no .py files found`,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const parts: string[] = [`${pyFileCount} .py files`];
|
|
358
|
+
if (routeCount > 0) parts.push(`${routeCount} route files`);
|
|
359
|
+
if (modelCount > 0) parts.push(`${modelCount} model files`);
|
|
360
|
+
if (initPyMissing.length > 0) {
|
|
361
|
+
parts.push(`missing __init__.py: ${initPyMissing.slice(0, 3).join(', ')}${initPyMissing.length > 3 ? '...' : ''}`);
|
|
362
|
+
return {
|
|
363
|
+
name: 'Python',
|
|
364
|
+
status: 'warn',
|
|
365
|
+
detail: parts.join(', '),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
name: 'Python',
|
|
371
|
+
status: 'pass',
|
|
372
|
+
detail: parts.join(', '),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
197
376
|
// ============================================================
|
|
198
377
|
// Main Doctor Flow
|
|
199
378
|
// ============================================================
|
|
@@ -211,11 +390,19 @@ export async function runDoctor(): Promise<void> {
|
|
|
211
390
|
checkMcpServer(projectRoot),
|
|
212
391
|
checkHooksConfig(projectRoot),
|
|
213
392
|
checkHookFiles(projectRoot),
|
|
393
|
+
checkKnowledgeDb(projectRoot),
|
|
394
|
+
checkMemoryDir(projectRoot),
|
|
395
|
+
checkShellHooksWired(projectRoot),
|
|
214
396
|
await checkNativeModules(),
|
|
215
397
|
checkNodeVersion(),
|
|
216
398
|
await checkGitRepo(projectRoot),
|
|
399
|
+
await checkLicenseStatus(),
|
|
217
400
|
];
|
|
218
401
|
|
|
402
|
+
// Add Python health check if configured
|
|
403
|
+
const pythonCheck = checkPythonHealth(projectRoot);
|
|
404
|
+
if (pythonCheck) checks.push(pythonCheck);
|
|
405
|
+
|
|
219
406
|
let passed = 0;
|
|
220
407
|
let failed = 0;
|
|
221
408
|
let warned = 0;
|
package/src/commands/init.ts
CHANGED
|
@@ -8,16 +8,21 @@
|
|
|
8
8
|
* 2. Generates massu.config.yaml (or preserves existing)
|
|
9
9
|
* 3. Registers MCP server in .mcp.json (creates or merges)
|
|
10
10
|
* 4. Installs all 11 hooks in .claude/settings.local.json
|
|
11
|
-
* 5.
|
|
11
|
+
* 5. Installs slash commands into .claude/commands/
|
|
12
|
+
* 6. Initializes memory directory
|
|
13
|
+
* 7. Prints success summary
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
16
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
|
|
15
17
|
import { resolve, basename, dirname } from 'path';
|
|
16
18
|
import { fileURLToPath } from 'url';
|
|
19
|
+
import { homedir } from 'os';
|
|
17
20
|
|
|
18
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
22
|
const __dirname = dirname(__filename);
|
|
20
23
|
import { stringify as yamlStringify } from 'yaml';
|
|
24
|
+
import { getConfig } from '../config.ts';
|
|
25
|
+
import { installCommands } from './install-commands.ts';
|
|
21
26
|
|
|
22
27
|
// ============================================================
|
|
23
28
|
// Types
|
|
@@ -91,6 +96,135 @@ export function detectFramework(projectRoot: string): FrameworkDetection {
|
|
|
91
96
|
return result;
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
// ============================================================
|
|
100
|
+
// Python Project Detection
|
|
101
|
+
// ============================================================
|
|
102
|
+
|
|
103
|
+
interface PythonDetection {
|
|
104
|
+
detected: boolean;
|
|
105
|
+
root: string;
|
|
106
|
+
hasFastapi: boolean;
|
|
107
|
+
hasSqlalchemy: boolean;
|
|
108
|
+
hasAlembic: boolean;
|
|
109
|
+
alembicDir: string | null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function detectPython(projectRoot: string): PythonDetection {
|
|
113
|
+
const result: PythonDetection = {
|
|
114
|
+
detected: false,
|
|
115
|
+
root: '',
|
|
116
|
+
hasFastapi: false,
|
|
117
|
+
hasSqlalchemy: false,
|
|
118
|
+
hasAlembic: false,
|
|
119
|
+
alembicDir: null,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Check for Python project markers
|
|
123
|
+
const markers = ['pyproject.toml', 'setup.py', 'requirements.txt', 'Pipfile'];
|
|
124
|
+
const hasMarker = markers.some(m => existsSync(resolve(projectRoot, m)));
|
|
125
|
+
if (!hasMarker) return result;
|
|
126
|
+
|
|
127
|
+
result.detected = true;
|
|
128
|
+
|
|
129
|
+
// Scan dependencies for FastAPI and SQLAlchemy
|
|
130
|
+
const depFiles = [
|
|
131
|
+
{ file: 'pyproject.toml', parser: parsePyprojectDeps },
|
|
132
|
+
{ file: 'requirements.txt', parser: parseRequirementsDeps },
|
|
133
|
+
{ file: 'setup.py', parser: parseSetupPyDeps },
|
|
134
|
+
{ file: 'Pipfile', parser: parsePipfileDeps },
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
for (const { file, parser } of depFiles) {
|
|
138
|
+
const filePath = resolve(projectRoot, file);
|
|
139
|
+
if (existsSync(filePath)) {
|
|
140
|
+
try {
|
|
141
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
142
|
+
const deps = parser(content);
|
|
143
|
+
if (deps.includes('fastapi')) result.hasFastapi = true;
|
|
144
|
+
if (deps.includes('sqlalchemy')) result.hasSqlalchemy = true;
|
|
145
|
+
} catch {
|
|
146
|
+
// Best effort
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check for Alembic
|
|
152
|
+
if (existsSync(resolve(projectRoot, 'alembic.ini'))) {
|
|
153
|
+
result.hasAlembic = true;
|
|
154
|
+
// Try to find the alembic versions directory
|
|
155
|
+
if (existsSync(resolve(projectRoot, 'alembic'))) {
|
|
156
|
+
result.alembicDir = 'alembic';
|
|
157
|
+
}
|
|
158
|
+
} else if (existsSync(resolve(projectRoot, 'alembic'))) {
|
|
159
|
+
result.hasAlembic = true;
|
|
160
|
+
result.alembicDir = 'alembic';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Auto-detect Python source root
|
|
164
|
+
const candidateRoots = ['app', 'src', 'backend', 'api'];
|
|
165
|
+
for (const candidate of candidateRoots) {
|
|
166
|
+
const candidatePath = resolve(projectRoot, candidate);
|
|
167
|
+
if (existsSync(candidatePath) && existsSync(resolve(candidatePath, '__init__.py'))) {
|
|
168
|
+
result.root = candidate;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
// Also check for .py files directly (some projects use app/ without __init__.py)
|
|
172
|
+
if (existsSync(candidatePath)) {
|
|
173
|
+
try {
|
|
174
|
+
const files = readdirSync(candidatePath);
|
|
175
|
+
if (files.some(f => f.endsWith('.py'))) {
|
|
176
|
+
result.root = candidate;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// Best effort
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Fallback: use '.' if no candidate root found
|
|
186
|
+
if (!result.root) {
|
|
187
|
+
result.root = '.';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parsePyprojectDeps(content: string): string[] {
|
|
194
|
+
const deps: string[] = [];
|
|
195
|
+
const lower = content.toLowerCase();
|
|
196
|
+
if (lower.includes('fastapi')) deps.push('fastapi');
|
|
197
|
+
if (lower.includes('sqlalchemy')) deps.push('sqlalchemy');
|
|
198
|
+
return deps;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function parseRequirementsDeps(content: string): string[] {
|
|
202
|
+
const deps: string[] = [];
|
|
203
|
+
const lower = content.toLowerCase();
|
|
204
|
+
for (const line of lower.split('\n')) {
|
|
205
|
+
const trimmed = line.trim();
|
|
206
|
+
if (trimmed.startsWith('fastapi')) deps.push('fastapi');
|
|
207
|
+
if (trimmed.startsWith('sqlalchemy')) deps.push('sqlalchemy');
|
|
208
|
+
}
|
|
209
|
+
return deps;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function parseSetupPyDeps(content: string): string[] {
|
|
213
|
+
const deps: string[] = [];
|
|
214
|
+
const lower = content.toLowerCase();
|
|
215
|
+
if (lower.includes('fastapi')) deps.push('fastapi');
|
|
216
|
+
if (lower.includes('sqlalchemy')) deps.push('sqlalchemy');
|
|
217
|
+
return deps;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function parsePipfileDeps(content: string): string[] {
|
|
221
|
+
const deps: string[] = [];
|
|
222
|
+
const lower = content.toLowerCase();
|
|
223
|
+
if (lower.includes('fastapi')) deps.push('fastapi');
|
|
224
|
+
if (lower.includes('sqlalchemy')) deps.push('sqlalchemy');
|
|
225
|
+
return deps;
|
|
226
|
+
}
|
|
227
|
+
|
|
94
228
|
// ============================================================
|
|
95
229
|
// Config File Generation
|
|
96
230
|
// ============================================================
|
|
@@ -104,7 +238,7 @@ export function generateConfig(projectRoot: string, framework: FrameworkDetectio
|
|
|
104
238
|
|
|
105
239
|
const projectName = basename(projectRoot);
|
|
106
240
|
|
|
107
|
-
const config = {
|
|
241
|
+
const config: Record<string, unknown> = {
|
|
108
242
|
project: {
|
|
109
243
|
name: projectName,
|
|
110
244
|
root: 'auto',
|
|
@@ -129,6 +263,21 @@ export function generateConfig(projectRoot: string, framework: FrameworkDetectio
|
|
|
129
263
|
],
|
|
130
264
|
};
|
|
131
265
|
|
|
266
|
+
// Detect and add Python configuration
|
|
267
|
+
const python = detectPython(projectRoot);
|
|
268
|
+
if (python.detected) {
|
|
269
|
+
const pythonConfig: Record<string, unknown> = {
|
|
270
|
+
root: python.root,
|
|
271
|
+
exclude_dirs: ['__pycache__', '.venv', 'venv', '.mypy_cache', '.pytest_cache'],
|
|
272
|
+
};
|
|
273
|
+
if (python.hasFastapi) pythonConfig.framework = 'fastapi';
|
|
274
|
+
if (python.hasSqlalchemy) pythonConfig.orm = 'sqlalchemy';
|
|
275
|
+
if (python.hasAlembic && python.alembicDir) {
|
|
276
|
+
pythonConfig.alembic_dir = python.alembicDir;
|
|
277
|
+
}
|
|
278
|
+
config.python = pythonConfig;
|
|
279
|
+
}
|
|
280
|
+
|
|
132
281
|
const yamlContent = `# Massu AI Configuration
|
|
133
282
|
# Generated by: npx massu init
|
|
134
283
|
# Documentation: https://massu.ai/docs/getting-started/configuration
|
|
@@ -281,7 +430,8 @@ export function buildHooksConfig(hooksDir: string): HooksConfig {
|
|
|
281
430
|
}
|
|
282
431
|
|
|
283
432
|
export function installHooks(projectRoot: string): { installed: boolean; count: number } {
|
|
284
|
-
const
|
|
433
|
+
const claudeDirName = getConfig().conventions?.claudeDirName ?? '.claude';
|
|
434
|
+
const claudeDir = resolve(projectRoot, claudeDirName);
|
|
285
435
|
const settingsPath = resolve(claudeDir, 'settings.local.json');
|
|
286
436
|
|
|
287
437
|
// Ensure .claude directory exists
|
|
@@ -321,6 +471,51 @@ export function installHooks(projectRoot: string): { installed: boolean; count:
|
|
|
321
471
|
return { installed: true, count: hookCount };
|
|
322
472
|
}
|
|
323
473
|
|
|
474
|
+
// ============================================================
|
|
475
|
+
// Memory Directory Initialization
|
|
476
|
+
// ============================================================
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Initialize the memory directory and create an initial MEMORY.md if absent.
|
|
480
|
+
* The memory directory lives in ~/.claude/projects/<encoded-root>/memory/
|
|
481
|
+
* matching the path used by memory-db.ts / knowledge-tools.ts.
|
|
482
|
+
*/
|
|
483
|
+
export function initMemoryDir(projectRoot: string): { created: boolean; memoryMdCreated: boolean } {
|
|
484
|
+
// Encode the project root the same way as getResolvedPaths() in config.ts
|
|
485
|
+
const encodedRoot = '-' + projectRoot.replace(/\//g, '-');
|
|
486
|
+
const memoryDir = resolve(homedir(), `.claude/projects/${encodedRoot}/memory`);
|
|
487
|
+
|
|
488
|
+
let created = false;
|
|
489
|
+
if (!existsSync(memoryDir)) {
|
|
490
|
+
mkdirSync(memoryDir, { recursive: true });
|
|
491
|
+
created = true;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const memoryMdPath = resolve(memoryDir, 'MEMORY.md');
|
|
495
|
+
let memoryMdCreated = false;
|
|
496
|
+
if (!existsSync(memoryMdPath)) {
|
|
497
|
+
const projectName = basename(projectRoot);
|
|
498
|
+
const memoryContent = `# ${projectName} - Massu Memory
|
|
499
|
+
|
|
500
|
+
## Key Learnings
|
|
501
|
+
<!-- Important patterns and conventions discovered during development -->
|
|
502
|
+
|
|
503
|
+
## Common Gotchas
|
|
504
|
+
<!-- Non-obvious issues and how to avoid them -->
|
|
505
|
+
|
|
506
|
+
## Corrections
|
|
507
|
+
<!-- Wrong behaviors that were corrected and how to prevent them -->
|
|
508
|
+
|
|
509
|
+
## File Index
|
|
510
|
+
<!-- Significant files and directories -->
|
|
511
|
+
`;
|
|
512
|
+
writeFileSync(memoryMdPath, memoryContent, 'utf-8');
|
|
513
|
+
memoryMdCreated = true;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return { created, memoryMdCreated };
|
|
517
|
+
}
|
|
518
|
+
|
|
324
519
|
// ============================================================
|
|
325
520
|
// Main Init Flow
|
|
326
521
|
// ============================================================
|
|
@@ -343,6 +538,16 @@ export async function runInit(): Promise<void> {
|
|
|
343
538
|
const detected = frameworkParts.length > 0 ? frameworkParts.join(', ') : 'JavaScript';
|
|
344
539
|
console.log(` Detected: ${detected}`);
|
|
345
540
|
|
|
541
|
+
// Step 1b: Detect Python
|
|
542
|
+
const python = detectPython(projectRoot);
|
|
543
|
+
if (python.detected) {
|
|
544
|
+
const pyParts: string[] = ['Python'];
|
|
545
|
+
if (python.hasFastapi) pyParts.push('FastAPI');
|
|
546
|
+
if (python.hasSqlalchemy) pyParts.push('SQLAlchemy');
|
|
547
|
+
if (python.hasAlembic) pyParts.push('Alembic');
|
|
548
|
+
console.log(` Detected: ${pyParts.join(', ')} (root: ${python.root})`);
|
|
549
|
+
}
|
|
550
|
+
|
|
346
551
|
// Step 2: Create config
|
|
347
552
|
const configCreated = generateConfig(projectRoot, framework);
|
|
348
553
|
if (configCreated) {
|
|
@@ -363,7 +568,27 @@ export async function runInit(): Promise<void> {
|
|
|
363
568
|
const { count: hooksCount } = installHooks(projectRoot);
|
|
364
569
|
console.log(` Installed ${hooksCount} hooks in .claude/settings.local.json`);
|
|
365
570
|
|
|
366
|
-
// Step 5:
|
|
571
|
+
// Step 5: Install slash commands
|
|
572
|
+
const cmdResult = installCommands(projectRoot);
|
|
573
|
+
const cmdTotal = cmdResult.installed + cmdResult.updated + cmdResult.skipped;
|
|
574
|
+
if (cmdResult.installed > 0 || cmdResult.updated > 0) {
|
|
575
|
+
console.log(` Installed ${cmdTotal} slash commands (${cmdResult.installed} new, ${cmdResult.updated} updated)`);
|
|
576
|
+
} else {
|
|
577
|
+
console.log(` ${cmdTotal} slash commands already up to date`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Step 6: Initialize memory directory
|
|
581
|
+
const { created: memDirCreated, memoryMdCreated } = initMemoryDir(projectRoot);
|
|
582
|
+
if (memDirCreated) {
|
|
583
|
+
console.log(' Created memory directory (~/.claude/projects/.../memory/)');
|
|
584
|
+
} else {
|
|
585
|
+
console.log(' Memory directory already exists');
|
|
586
|
+
}
|
|
587
|
+
if (memoryMdCreated) {
|
|
588
|
+
console.log(' Created initial MEMORY.md');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Step 7: Databases info
|
|
367
592
|
console.log(' Databases will auto-create on first session');
|
|
368
593
|
|
|
369
594
|
// Summary
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Copyright (c) 2026 Massu. All rights reserved.
|
|
2
|
+
// Licensed under BSL 1.1 - see LICENSE file for details.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `massu install-commands` — Install massu slash commands into a project.
|
|
6
|
+
*
|
|
7
|
+
* Copies all massu command .md files from the package's commands/ directory
|
|
8
|
+
* into the project's .claude/commands/ directory. Existing massu commands
|
|
9
|
+
* are updated; non-massu commands are preserved.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from 'fs';
|
|
13
|
+
import { resolve, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import { getConfig } from '../config.ts';
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = dirname(__filename);
|
|
19
|
+
|
|
20
|
+
// ============================================================
|
|
21
|
+
// Command Installation
|
|
22
|
+
// ============================================================
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the path to the bundled commands directory.
|
|
26
|
+
* Handles both npm-installed and local development scenarios.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveCommandsDir(): string | null {
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
|
|
31
|
+
// 1. npm-installed: node_modules/@massu/core/commands
|
|
32
|
+
const nodeModulesPath = resolve(cwd, 'node_modules/@massu/core/commands');
|
|
33
|
+
if (existsSync(nodeModulesPath)) {
|
|
34
|
+
return nodeModulesPath;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Relative to compiled dist/cli.js → ../commands
|
|
38
|
+
const distRelPath = resolve(__dirname, '../commands');
|
|
39
|
+
if (existsSync(distRelPath)) {
|
|
40
|
+
return distRelPath;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 3. Relative to source src/commands/ → ../../commands
|
|
44
|
+
const srcRelPath = resolve(__dirname, '../../commands');
|
|
45
|
+
if (existsSync(srcRelPath)) {
|
|
46
|
+
return srcRelPath;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface InstallCommandsResult {
|
|
53
|
+
installed: number;
|
|
54
|
+
updated: number;
|
|
55
|
+
skipped: number;
|
|
56
|
+
commandsDir: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function installCommands(projectRoot: string): InstallCommandsResult {
|
|
60
|
+
const claudeDirName = getConfig().conventions?.claudeDirName ?? '.claude';
|
|
61
|
+
const targetDir = resolve(projectRoot, claudeDirName, 'commands');
|
|
62
|
+
|
|
63
|
+
// Ensure .claude/commands directory exists
|
|
64
|
+
if (!existsSync(targetDir)) {
|
|
65
|
+
mkdirSync(targetDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Find source commands
|
|
69
|
+
const sourceDir = resolveCommandsDir();
|
|
70
|
+
if (!sourceDir) {
|
|
71
|
+
console.error(' ERROR: Could not find massu commands directory.');
|
|
72
|
+
console.error(' Try reinstalling: npm install @massu/core');
|
|
73
|
+
return { installed: 0, updated: 0, skipped: 0, commandsDir: targetDir };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Read all command files from source
|
|
77
|
+
const sourceFiles = readdirSync(sourceDir).filter(f => f.endsWith('.md'));
|
|
78
|
+
|
|
79
|
+
let installed = 0;
|
|
80
|
+
let updated = 0;
|
|
81
|
+
let skipped = 0;
|
|
82
|
+
|
|
83
|
+
for (const file of sourceFiles) {
|
|
84
|
+
const sourcePath = resolve(sourceDir, file);
|
|
85
|
+
const targetPath = resolve(targetDir, file);
|
|
86
|
+
const sourceContent = readFileSync(sourcePath, 'utf-8');
|
|
87
|
+
|
|
88
|
+
if (existsSync(targetPath)) {
|
|
89
|
+
const existingContent = readFileSync(targetPath, 'utf-8');
|
|
90
|
+
if (existingContent === sourceContent) {
|
|
91
|
+
skipped++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// Update existing command
|
|
95
|
+
writeFileSync(targetPath, sourceContent, 'utf-8');
|
|
96
|
+
updated++;
|
|
97
|
+
} else {
|
|
98
|
+
// Install new command
|
|
99
|
+
writeFileSync(targetPath, sourceContent, 'utf-8');
|
|
100
|
+
installed++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { installed, updated, skipped, commandsDir: targetDir };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ============================================================
|
|
108
|
+
// Standalone CLI Runner
|
|
109
|
+
// ============================================================
|
|
110
|
+
|
|
111
|
+
export async function runInstallCommands(): Promise<void> {
|
|
112
|
+
const projectRoot = process.cwd();
|
|
113
|
+
|
|
114
|
+
console.log('');
|
|
115
|
+
console.log('Massu AI - Install Slash Commands');
|
|
116
|
+
console.log('==================================');
|
|
117
|
+
console.log('');
|
|
118
|
+
|
|
119
|
+
const result = installCommands(projectRoot);
|
|
120
|
+
|
|
121
|
+
if (result.installed > 0) {
|
|
122
|
+
console.log(` Installed ${result.installed} new commands`);
|
|
123
|
+
}
|
|
124
|
+
if (result.updated > 0) {
|
|
125
|
+
console.log(` Updated ${result.updated} existing commands`);
|
|
126
|
+
}
|
|
127
|
+
if (result.skipped > 0) {
|
|
128
|
+
console.log(` ${result.skipped} commands already up to date`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const total = result.installed + result.updated + result.skipped;
|
|
132
|
+
console.log('');
|
|
133
|
+
console.log(` ${total} slash commands available in ${result.commandsDir}`);
|
|
134
|
+
console.log('');
|
|
135
|
+
console.log(' Restart your Claude Code session to use them.');
|
|
136
|
+
console.log('');
|
|
137
|
+
}
|