moflo 4.9.13 → 4.9.14
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/.claude/helpers/gate.cjs +21 -5
- package/.claude/skills/fl/phases.md +18 -2
- package/.claude/skills/simplify/SKILL.md +35 -48
- package/bin/gate.cjs +21 -5
- package/bin/session-start-launcher.mjs +1 -1
- package/bin/simplify-classify.cjs +211 -0
- package/dist/src/cli/commands/doctor-checks-config.js +246 -0
- package/dist/src/cli/commands/doctor-checks-intelligence.js +197 -0
- package/dist/src/cli/commands/doctor-checks-memory.js +207 -0
- package/dist/src/cli/commands/doctor-checks-platform.js +138 -0
- package/dist/src/cli/commands/doctor-checks-runtime.js +170 -0
- package/dist/src/cli/commands/doctor-fixes.js +165 -0
- package/dist/src/cli/commands/doctor-registry.js +109 -0
- package/dist/src/cli/commands/doctor-render.js +203 -0
- package/dist/src/cli/commands/doctor-types.js +9 -0
- package/dist/src/cli/commands/doctor-version.js +134 -0
- package/dist/src/cli/commands/doctor-zombies.js +201 -0
- package/dist/src/cli/commands/doctor.js +35 -1706
- package/dist/src/cli/init/helpers-generator.js +21 -5
- package/dist/src/cli/version.js +1 -1
- package/package.json +2 -2
- package/scripts/post-install-bootstrap.mjs +1 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-level checks for `flo doctor`:
|
|
3
|
+
* spell engine integrity and OS sandbox tier.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
8
|
+
import { getMofloRoot } from './doctor-checks-deep.js';
|
|
9
|
+
// Validates core modules, built output, and step commands for the spell engine.
|
|
10
|
+
export async function checkSpellEngine() {
|
|
11
|
+
try {
|
|
12
|
+
// Resolve relative to the moflo package root (works in both dev and consumer)
|
|
13
|
+
const mofloRoot = getMofloRoot();
|
|
14
|
+
if (!mofloRoot) {
|
|
15
|
+
return { name: 'Spell Engine', status: 'warn', message: 'Could not locate moflo package root', fix: 'npm run build' };
|
|
16
|
+
}
|
|
17
|
+
// Post-#586 workspace collapse: spell engine lives at src/cli/spells/
|
|
18
|
+
// (source) and dist/src/cli/spells/ (compiled). The legacy
|
|
19
|
+
// src/modules/spells/{src,dist}/ tree was deleted.
|
|
20
|
+
const distDir = join(mofloRoot, 'dist', 'src', 'cli', 'spells');
|
|
21
|
+
const srcDir = join(mofloRoot, 'src', 'cli', 'spells');
|
|
22
|
+
const hasDistDir = existsSync(distDir);
|
|
23
|
+
const hasSrcDir = existsSync(srcDir);
|
|
24
|
+
if (!hasDistDir && !hasSrcDir) {
|
|
25
|
+
return { name: 'Spell Engine', status: 'warn', message: 'Spell engine not found', fix: 'npm run build' };
|
|
26
|
+
}
|
|
27
|
+
const coreModules = [
|
|
28
|
+
'core/runner',
|
|
29
|
+
'core/step-executor',
|
|
30
|
+
'core/step-command-registry',
|
|
31
|
+
'core/interpolation',
|
|
32
|
+
'core/credential-masker',
|
|
33
|
+
'registry/spell-registry',
|
|
34
|
+
'factory/runner-factory',
|
|
35
|
+
'schema',
|
|
36
|
+
'types',
|
|
37
|
+
'credentials',
|
|
38
|
+
'scheduler',
|
|
39
|
+
];
|
|
40
|
+
const baseDir = hasDistDir ? distDir : srcDir;
|
|
41
|
+
const ext = hasDistDir ? '.js' : '.ts';
|
|
42
|
+
const dirModules = ['schema', 'types', 'credentials', 'scheduler'];
|
|
43
|
+
const missing = coreModules.filter(m => dirModules.includes(m)
|
|
44
|
+
? !existsSync(join(baseDir, m))
|
|
45
|
+
: !existsSync(join(baseDir, m + ext)));
|
|
46
|
+
if (missing.length > 0) {
|
|
47
|
+
return {
|
|
48
|
+
name: 'Spell Engine',
|
|
49
|
+
status: 'warn',
|
|
50
|
+
message: `Missing modules: ${missing.join(', ')}`,
|
|
51
|
+
fix: 'npm run build',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const commandsDir = join(baseDir, 'commands');
|
|
55
|
+
const hasCommands = existsSync(commandsDir);
|
|
56
|
+
const loadersDir = join(baseDir, 'loaders');
|
|
57
|
+
const hasLoaders = existsSync(loadersDir);
|
|
58
|
+
const hasIndex = existsSync(join(baseDir, 'index' + ext));
|
|
59
|
+
const parts = [];
|
|
60
|
+
parts.push(`${coreModules.length} core modules`);
|
|
61
|
+
if (hasCommands)
|
|
62
|
+
parts.push('step commands');
|
|
63
|
+
if (hasLoaders)
|
|
64
|
+
parts.push('loaders');
|
|
65
|
+
if (hasIndex)
|
|
66
|
+
parts.push('index');
|
|
67
|
+
return {
|
|
68
|
+
name: 'Spell Engine',
|
|
69
|
+
status: 'pass',
|
|
70
|
+
message: parts.join(', '),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
return { name: 'Spell Engine', status: 'warn', message: `Unable to check spell engine: ${errorDetail(e)}` };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Reports OS sandbox capability AND, if the project has `sandbox.enabled: true`,
|
|
78
|
+
// whether the effective sandbox would actually start (e.g. Windows Docker image
|
|
79
|
+
// pulled and configured).
|
|
80
|
+
export async function checkSandboxTier() {
|
|
81
|
+
try {
|
|
82
|
+
const { detectSandboxCapability, loadSandboxConfigFromProject, resolveEffectiveSandbox, } = await import('../spells/index.js');
|
|
83
|
+
const cap = await detectSandboxCapability();
|
|
84
|
+
const config = await loadSandboxConfigFromProject(process.cwd());
|
|
85
|
+
if (!config.enabled) {
|
|
86
|
+
if (cap.available) {
|
|
87
|
+
return {
|
|
88
|
+
name: 'Sandbox Tier',
|
|
89
|
+
status: 'pass',
|
|
90
|
+
message: `${cap.tool} available (${cap.platform}) — sandboxing off in moflo.yaml`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const offHint = {
|
|
94
|
+
win32: 'Install Docker Desktop and set sandbox.dockerImage in moflo.yaml to enable sandboxing',
|
|
95
|
+
linux: 'Install bubblewrap: sudo apt install bubblewrap',
|
|
96
|
+
darwin: 'sandbox-exec should be available on macOS — check /usr/bin/sandbox-exec',
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
name: 'Sandbox Tier',
|
|
100
|
+
status: 'pass',
|
|
101
|
+
message: `sandboxing off (${cap.platform}, denylist active)`,
|
|
102
|
+
fix: offHint[cap.platform],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const effective = await resolveEffectiveSandbox(config);
|
|
107
|
+
if (effective.useOsSandbox) {
|
|
108
|
+
const imageHint = effective.config.dockerImage ? `, ${effective.config.dockerImage}` : '';
|
|
109
|
+
return {
|
|
110
|
+
name: 'Sandbox Tier',
|
|
111
|
+
status: 'pass',
|
|
112
|
+
message: `${cap.tool} ready (${cap.platform}${imageHint})`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
name: 'Sandbox Tier',
|
|
117
|
+
status: 'warn',
|
|
118
|
+
message: `denylist only (${cap.platform})`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
return {
|
|
123
|
+
name: 'Sandbox Tier',
|
|
124
|
+
status: 'warn',
|
|
125
|
+
message: `sandboxing enabled but not ready (${cap.platform})`,
|
|
126
|
+
fix: errorDetail(err),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
return {
|
|
132
|
+
name: 'Sandbox Tier',
|
|
133
|
+
status: 'warn',
|
|
134
|
+
message: `Unable to detect: ${err instanceof Error ? err.message : 'unknown error'}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=doctor-checks-platform.js.map
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick runtime environment checks for `flo doctor`:
|
|
3
|
+
* Node.js, npm, Claude Code CLI, git, disk space, TypeScript build tool.
|
|
4
|
+
*
|
|
5
|
+
* These checks call external binaries via the shared `runCommand` helper that
|
|
6
|
+
* inherits the full process env (critical on Windows where PATH may not be
|
|
7
|
+
* inherited properly across child shells).
|
|
8
|
+
*/
|
|
9
|
+
import { execSync, exec } from 'child_process';
|
|
10
|
+
import { promisify } from 'util';
|
|
11
|
+
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
12
|
+
import { output } from '../output.js';
|
|
13
|
+
const execAsync = promisify(exec);
|
|
14
|
+
/**
|
|
15
|
+
* Execute command asynchronously with proper environment inheritance.
|
|
16
|
+
* Critical for Windows where PATH may not be inherited properly.
|
|
17
|
+
*/
|
|
18
|
+
export async function runCommand(command, timeoutMs = 5000) {
|
|
19
|
+
const opts = {
|
|
20
|
+
encoding: 'utf8',
|
|
21
|
+
timeout: timeoutMs,
|
|
22
|
+
shell: process.platform === 'win32' ? 'cmd.exe' : '/bin/sh',
|
|
23
|
+
env: { ...process.env },
|
|
24
|
+
windowsHide: true,
|
|
25
|
+
};
|
|
26
|
+
const { stdout } = await execAsync(command, opts);
|
|
27
|
+
const out = stdout.trim();
|
|
28
|
+
// Windows parallel exec occasionally returns empty stdout under shell contention — retry once serially
|
|
29
|
+
if (!out && process.platform === 'win32') {
|
|
30
|
+
const retry = await execAsync(command, opts);
|
|
31
|
+
return retry.stdout.trim();
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
export async function checkNodeVersion() {
|
|
36
|
+
const requiredMajor = 20;
|
|
37
|
+
const version = process.version;
|
|
38
|
+
const major = parseInt(version.slice(1).split('.')[0], 10);
|
|
39
|
+
if (major >= requiredMajor) {
|
|
40
|
+
return { name: 'Node.js Version', status: 'pass', message: `${version} (>= ${requiredMajor} required)` };
|
|
41
|
+
}
|
|
42
|
+
else if (major >= 18) {
|
|
43
|
+
return { name: 'Node.js Version', status: 'warn', message: `${version} (>= ${requiredMajor} recommended)`, fix: 'nvm install 20 && nvm use 20' };
|
|
44
|
+
}
|
|
45
|
+
return { name: 'Node.js Version', status: 'fail', message: `${version} (>= ${requiredMajor} required)`, fix: 'nvm install 20 && nvm use 20' };
|
|
46
|
+
}
|
|
47
|
+
export async function checkNpmVersion() {
|
|
48
|
+
try {
|
|
49
|
+
const version = await runCommand('npm --version');
|
|
50
|
+
const major = parseInt(version.split('.')[0], 10);
|
|
51
|
+
if (major >= 9) {
|
|
52
|
+
return { name: 'npm Version', status: 'pass', message: `v${version}` };
|
|
53
|
+
}
|
|
54
|
+
return { name: 'npm Version', status: 'warn', message: `v${version} (>= 9 recommended)`, fix: 'npm install -g npm@latest' };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { name: 'npm Version', status: 'fail', message: 'npm not found', fix: 'Install Node.js from https://nodejs.org' };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function checkGit() {
|
|
61
|
+
try {
|
|
62
|
+
const version = await runCommand('git --version');
|
|
63
|
+
return { name: 'Git', status: 'pass', message: version.replace('git version ', 'v') };
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
return { name: 'Git', status: 'warn', message: `Not installed (${errorDetail(e, { firstLineOnly: true })})`, fix: 'Install git from https://git-scm.com' };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function checkGitRepo() {
|
|
70
|
+
try {
|
|
71
|
+
await runCommand('git rev-parse --git-dir');
|
|
72
|
+
return { name: 'Git Repository', status: 'pass', message: 'In a git repository' };
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
return { name: 'Git Repository', status: 'warn', message: `Not a git repository (${errorDetail(e, { firstLineOnly: true })})`, fix: 'git init' };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function checkDiskSpace() {
|
|
79
|
+
try {
|
|
80
|
+
if (process.platform === 'win32') {
|
|
81
|
+
try {
|
|
82
|
+
const driveLetter = process.cwd().match(/^([A-Z]):/i)?.[1]?.toUpperCase() || 'C';
|
|
83
|
+
const psOutput = await runCommand(`powershell -NoProfile -Command "Get-PSDrive ${driveLetter} | Select-Object -ExpandProperty Free; Get-PSDrive ${driveLetter} | Select-Object -ExpandProperty Used"`);
|
|
84
|
+
const vals = psOutput.split(/\r?\n/).filter(l => l.trim());
|
|
85
|
+
const freeBytes = parseInt(vals[0] || '0', 10);
|
|
86
|
+
const usedBytes = parseInt(vals[1] || '0', 10);
|
|
87
|
+
const totalBytes = freeBytes + usedBytes || 1;
|
|
88
|
+
const freeGB = (freeBytes / (1024 ** 3)).toFixed(1);
|
|
89
|
+
const usePercent = Math.round(((totalBytes - freeBytes) / totalBytes) * 100);
|
|
90
|
+
if (usePercent > 90) {
|
|
91
|
+
return { name: 'Disk Space', status: 'fail', message: `${freeGB}G available (${usePercent}% used)`, fix: 'Free up disk space' };
|
|
92
|
+
}
|
|
93
|
+
if (usePercent > 80) {
|
|
94
|
+
return { name: 'Disk Space', status: 'warn', message: `${freeGB}G available (${usePercent}% used)` };
|
|
95
|
+
}
|
|
96
|
+
return { name: 'Disk Space', status: 'pass', message: `${freeGB}G available` };
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { name: 'Disk Space', status: 'pass', message: 'Check skipped (PowerShell unavailable)' };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Use df -Ph for POSIX mode (guarantees single-line output even with long device names)
|
|
103
|
+
const output_str = await runCommand('df -Ph . | tail -1');
|
|
104
|
+
const parts = output_str.split(/\s+/);
|
|
105
|
+
const available = parts[3];
|
|
106
|
+
const usePercent = parseInt(parts[4]?.replace('%', '') || '0', 10);
|
|
107
|
+
if (isNaN(usePercent)) {
|
|
108
|
+
return { name: 'Disk Space', status: 'warn', message: `${available || 'unknown'} available (unable to parse usage)` };
|
|
109
|
+
}
|
|
110
|
+
if (usePercent > 90) {
|
|
111
|
+
return { name: 'Disk Space', status: 'fail', message: `${available} available (${usePercent}% used)`, fix: 'Free up disk space' };
|
|
112
|
+
}
|
|
113
|
+
if (usePercent > 80) {
|
|
114
|
+
return { name: 'Disk Space', status: 'warn', message: `${available} available (${usePercent}% used)` };
|
|
115
|
+
}
|
|
116
|
+
return { name: 'Disk Space', status: 'pass', message: `${available} available` };
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
return { name: 'Disk Space', status: 'warn', message: `Unable to check: ${errorDetail(e, { firstLineOnly: true })}` };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export async function checkBuildTools() {
|
|
123
|
+
try {
|
|
124
|
+
const tscVersion = await runCommand('npx tsc --version', 10000); // tsc can be slow
|
|
125
|
+
if (!tscVersion || tscVersion.includes('not found')) {
|
|
126
|
+
return { name: 'TypeScript', status: 'warn', message: 'Not installed locally', fix: 'npm install -D typescript' };
|
|
127
|
+
}
|
|
128
|
+
return { name: 'TypeScript', status: 'pass', message: tscVersion.replace('Version ', 'v') };
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
return { name: 'TypeScript', status: 'warn', message: `Not installed locally (${errorDetail(e, { firstLineOnly: true })})`, fix: 'npm install -D typescript' };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export async function checkClaudeCode() {
|
|
135
|
+
try {
|
|
136
|
+
const version = await runCommand('claude --version');
|
|
137
|
+
const versionMatch = version.match(/v?(\d+\.\d+\.\d+)/);
|
|
138
|
+
const versionStr = versionMatch ? `v${versionMatch[1]}` : version;
|
|
139
|
+
return { name: 'Claude Code CLI', status: 'pass', message: versionStr };
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
return {
|
|
143
|
+
name: 'Claude Code CLI',
|
|
144
|
+
status: 'warn',
|
|
145
|
+
message: `Not installed (${errorDetail(e, { firstLineOnly: true })})`,
|
|
146
|
+
fix: 'npm install -g @anthropic-ai/claude-code',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export async function installClaudeCode() {
|
|
151
|
+
try {
|
|
152
|
+
output.writeln();
|
|
153
|
+
output.writeln(output.bold('Installing Claude Code CLI...'));
|
|
154
|
+
execSync('npm install -g @anthropic-ai/claude-code', {
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
stdio: 'inherit',
|
|
157
|
+
windowsHide: true,
|
|
158
|
+
});
|
|
159
|
+
output.writeln(output.success('Claude Code CLI installed successfully!'));
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
output.writeln(output.error('Failed to install Claude Code CLI'));
|
|
164
|
+
if (error instanceof Error) {
|
|
165
|
+
output.writeln(output.dim(error.message));
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=doctor-checks-runtime.js.map
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-fix dispatch for `flo doctor --fix`.
|
|
3
|
+
*
|
|
4
|
+
* Maps each named HealthCheck to a programmatic fix function (preferred over
|
|
5
|
+
* shell-out where possible). Falls back to running the check's `fix` string
|
|
6
|
+
* if it looks like an `npx`/`npm`/`claude` command.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { output } from '../output.js';
|
|
11
|
+
import { errorDetail } from '../shared/utils/error-detail.js';
|
|
12
|
+
import { repairHookWiring } from '../services/hook-wiring.js';
|
|
13
|
+
import { findZombieProcesses } from './doctor-zombies.js';
|
|
14
|
+
import { installClaudeCode, runCommand } from './doctor-checks-runtime.js';
|
|
15
|
+
/** Run a shell command as a fix action. Returns true on exit code 0. */
|
|
16
|
+
async function runFixCommand(cmd) {
|
|
17
|
+
try {
|
|
18
|
+
await runCommand(cmd, 30_000);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Fix missing hook wiring in settings.json by patching in entries for any
|
|
27
|
+
* REQUIRED_HOOK_WIRING patterns that aren't present. Delegates to shared
|
|
28
|
+
* repairHookWiring() to stay DRY with the upgrade path.
|
|
29
|
+
*/
|
|
30
|
+
async function fixGateHealthHooks() {
|
|
31
|
+
const settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
|
32
|
+
if (!existsSync(settingsPath))
|
|
33
|
+
return false;
|
|
34
|
+
try {
|
|
35
|
+
const raw = readFileSync(settingsPath, 'utf8');
|
|
36
|
+
const settings = JSON.parse(raw);
|
|
37
|
+
const { repaired } = repairHookWiring(settings);
|
|
38
|
+
if (repaired.length === 0)
|
|
39
|
+
return true; // nothing to fix
|
|
40
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Execute the fix for a failed/warned health check.
|
|
49
|
+
* Returns true if the fix succeeded (re-check should pass).
|
|
50
|
+
*/
|
|
51
|
+
export async function autoFixCheck(check) {
|
|
52
|
+
if (!check.fix)
|
|
53
|
+
return false;
|
|
54
|
+
// Map checks to programmatic fixes (not just shell commands)
|
|
55
|
+
const fixActions = {
|
|
56
|
+
'Memory Database': async () => {
|
|
57
|
+
try {
|
|
58
|
+
const swarmDir = join(process.cwd(), '.swarm');
|
|
59
|
+
if (!existsSync(swarmDir))
|
|
60
|
+
mkdirSync(swarmDir, { recursive: true });
|
|
61
|
+
const { initializeMemoryDatabase } = await import('../memory/memory-initializer.js');
|
|
62
|
+
const result = await initializeMemoryDatabase({ force: true, verbose: false });
|
|
63
|
+
return result.success;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return runFixCommand('npx moflo memory init --force');
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
'Embeddings': async () => {
|
|
70
|
+
try {
|
|
71
|
+
const swarmDir = join(process.cwd(), '.swarm');
|
|
72
|
+
if (!existsSync(swarmDir))
|
|
73
|
+
mkdirSync(swarmDir, { recursive: true });
|
|
74
|
+
const dbPath = join(swarmDir, 'memory.db');
|
|
75
|
+
if (!existsSync(dbPath)) {
|
|
76
|
+
const { initializeMemoryDatabase } = await import('../memory/memory-initializer.js');
|
|
77
|
+
await initializeMemoryDatabase({ force: true, verbose: false });
|
|
78
|
+
}
|
|
79
|
+
return runFixCommand('npx moflo embeddings init --force');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return runFixCommand('npx moflo memory init --force');
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
'Config File': async () => {
|
|
86
|
+
try {
|
|
87
|
+
const cfDir = join(process.cwd(), '.moflo');
|
|
88
|
+
if (!existsSync(cfDir))
|
|
89
|
+
mkdirSync(cfDir, { recursive: true });
|
|
90
|
+
return runFixCommand('npx moflo config init');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
'Daemon Status': async () => {
|
|
97
|
+
const lockFile = join(process.cwd(), '.moflo', 'daemon.lock');
|
|
98
|
+
const pidFile = join(process.cwd(), '.moflo', 'daemon.pid');
|
|
99
|
+
try {
|
|
100
|
+
if (existsSync(lockFile))
|
|
101
|
+
unlinkSync(lockFile);
|
|
102
|
+
if (existsSync(pidFile))
|
|
103
|
+
unlinkSync(pidFile);
|
|
104
|
+
}
|
|
105
|
+
catch { /* best effort */ }
|
|
106
|
+
return runFixCommand('npx moflo daemon start');
|
|
107
|
+
},
|
|
108
|
+
'MCP Servers': async () => {
|
|
109
|
+
return runFixCommand('claude mcp add moflo -- npx -y moflo mcp start');
|
|
110
|
+
},
|
|
111
|
+
'Claude Code CLI': async () => {
|
|
112
|
+
return installClaudeCode();
|
|
113
|
+
},
|
|
114
|
+
'Zombie Processes': async () => {
|
|
115
|
+
const result = await findZombieProcesses(true);
|
|
116
|
+
return result.killed > 0 || result.details.length === 0;
|
|
117
|
+
},
|
|
118
|
+
'Gate Health': async () => {
|
|
119
|
+
return fixGateHealthHooks();
|
|
120
|
+
},
|
|
121
|
+
'Status Line': async () => {
|
|
122
|
+
const settingsPath = join(process.cwd(), '.claude', 'settings.json');
|
|
123
|
+
if (!existsSync(settingsPath))
|
|
124
|
+
return false;
|
|
125
|
+
try {
|
|
126
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
127
|
+
if (!settings.statusLine) {
|
|
128
|
+
settings.statusLine = {
|
|
129
|
+
type: 'command',
|
|
130
|
+
command: 'node "$CLAUDE_PROJECT_DIR/.claude/helpers/statusline.cjs" --compact',
|
|
131
|
+
};
|
|
132
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf-8');
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
const fixFn = fixActions[check.name];
|
|
142
|
+
if (fixFn) {
|
|
143
|
+
try {
|
|
144
|
+
output.writeln(output.dim(` Fixing: ${check.name}...`));
|
|
145
|
+
const success = await fixFn();
|
|
146
|
+
if (success) {
|
|
147
|
+
output.writeln(output.success(` Fixed: ${check.name}`));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
output.writeln(output.warning(` Fix attempted but may need manual action: ${check.fix}`));
|
|
151
|
+
}
|
|
152
|
+
return success;
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
output.writeln(output.warning(` Fix failed: ${errorDetail(e)}`));
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Generic: try running the fix command directly if it looks like a shell command
|
|
160
|
+
if (check.fix.startsWith('npx ') || check.fix.startsWith('npm ') || check.fix.startsWith('claude ')) {
|
|
161
|
+
return runFixCommand(check.fix);
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=doctor-fixes.js.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor check registry: ordered list and component-name lookup.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from `doctor.ts` so the orchestration file stays small and the
|
|
5
|
+
* registry can be inspected/extended without re-touching command-action code.
|
|
6
|
+
*/
|
|
7
|
+
import { checkSubagentHealth, checkSpellExecution, checkMcpToolInvocation, checkHookExecution, checkMcpSpellIntegration, checkGateHealth, checkHookBlockDrift, checkMofloDbBridge, } from './doctor-checks-deep.js';
|
|
8
|
+
import { checkEmbeddingHygiene } from './doctor-embedding-hygiene.js';
|
|
9
|
+
import { checkSwarmFunctional, checkHiveMindFunctional, } from './doctor-checks-swarm.js';
|
|
10
|
+
import { checkMemoryAccessFunctional } from './doctor-checks-memory-access.js';
|
|
11
|
+
import { checkBuildTools, checkClaudeCode, checkDiskSpace, checkGit, checkGitRepo, checkNodeVersion, checkNpmVersion, } from './doctor-checks-runtime.js';
|
|
12
|
+
import { checkConfigFile, checkDaemonStatus, checkMcpServers, checkMemoryDatabase, checkMofloYamlCompliance, checkStatusLine, checkTestDirs, } from './doctor-checks-config.js';
|
|
13
|
+
import { checkSpellEngine, checkSandboxTier } from './doctor-checks-platform.js';
|
|
14
|
+
import { checkEmbeddings, checkSemanticQuality, } from './doctor-checks-memory.js';
|
|
15
|
+
import { checkIntelligence } from './doctor-checks-intelligence.js';
|
|
16
|
+
import { checkVersionFreshness } from './doctor-version.js';
|
|
17
|
+
import { checkZombieProcesses } from './doctor-zombies.js';
|
|
18
|
+
/** Order matters — top entries surface first under the spinner. */
|
|
19
|
+
export const allChecks = [
|
|
20
|
+
checkVersionFreshness,
|
|
21
|
+
checkNodeVersion,
|
|
22
|
+
checkNpmVersion,
|
|
23
|
+
checkClaudeCode,
|
|
24
|
+
checkGit,
|
|
25
|
+
checkGitRepo,
|
|
26
|
+
checkConfigFile,
|
|
27
|
+
checkMofloYamlCompliance,
|
|
28
|
+
checkStatusLine,
|
|
29
|
+
checkDaemonStatus,
|
|
30
|
+
checkMemoryDatabase,
|
|
31
|
+
checkEmbeddings,
|
|
32
|
+
checkEmbeddingHygiene,
|
|
33
|
+
checkTestDirs,
|
|
34
|
+
checkMcpServers,
|
|
35
|
+
checkDiskSpace,
|
|
36
|
+
checkBuildTools,
|
|
37
|
+
checkSemanticQuality,
|
|
38
|
+
checkIntelligence,
|
|
39
|
+
checkSpellEngine,
|
|
40
|
+
checkZombieProcesses,
|
|
41
|
+
checkSubagentHealth,
|
|
42
|
+
checkSpellExecution,
|
|
43
|
+
checkMcpToolInvocation,
|
|
44
|
+
checkMcpSpellIntegration,
|
|
45
|
+
checkHookExecution,
|
|
46
|
+
checkGateHealth,
|
|
47
|
+
checkHookBlockDrift,
|
|
48
|
+
checkMofloDbBridge,
|
|
49
|
+
// Issue #818 / epic #798 — coordinator-path tripwires. They share the
|
|
50
|
+
// singleton coordinator with checkSubagentHealth above and assert by
|
|
51
|
+
// agent-id (not absolute counts) so they tolerate the parallel batch.
|
|
52
|
+
checkSwarmFunctional,
|
|
53
|
+
checkHiveMindFunctional,
|
|
54
|
+
// Issue #844 — memory_store + memory_search round-trip across subagent,
|
|
55
|
+
// swarm-agent, and hive-mind contexts.
|
|
56
|
+
checkMemoryAccessFunctional,
|
|
57
|
+
checkSandboxTier,
|
|
58
|
+
];
|
|
59
|
+
/** Lookup table for `flo doctor -c <name>`. */
|
|
60
|
+
export const componentMap = {
|
|
61
|
+
'version': checkVersionFreshness,
|
|
62
|
+
'freshness': checkVersionFreshness,
|
|
63
|
+
'node': checkNodeVersion,
|
|
64
|
+
'npm': checkNpmVersion,
|
|
65
|
+
'claude': checkClaudeCode,
|
|
66
|
+
'config': checkConfigFile,
|
|
67
|
+
'yaml': checkMofloYamlCompliance,
|
|
68
|
+
'moflo-yaml': checkMofloYamlCompliance,
|
|
69
|
+
'statusline': checkStatusLine,
|
|
70
|
+
'status-line': checkStatusLine,
|
|
71
|
+
'daemon': checkDaemonStatus,
|
|
72
|
+
'memory': checkMemoryDatabase,
|
|
73
|
+
'embeddings': checkEmbeddings,
|
|
74
|
+
'embedding-hygiene': checkEmbeddingHygiene,
|
|
75
|
+
'hygiene': checkEmbeddingHygiene,
|
|
76
|
+
'git': checkGit,
|
|
77
|
+
'mcp': checkMcpServers,
|
|
78
|
+
'disk': checkDiskSpace,
|
|
79
|
+
'typescript': checkBuildTools,
|
|
80
|
+
'tests': checkTestDirs,
|
|
81
|
+
'semantic': checkSemanticQuality,
|
|
82
|
+
'quality': checkSemanticQuality,
|
|
83
|
+
'intelligence': checkIntelligence,
|
|
84
|
+
'workflows': checkSpellEngine,
|
|
85
|
+
'workflow': checkSpellEngine,
|
|
86
|
+
'subagent': checkSubagentHealth,
|
|
87
|
+
'subagents': checkSubagentHealth,
|
|
88
|
+
'agents': checkSubagentHealth,
|
|
89
|
+
'spell-exec': checkSpellExecution,
|
|
90
|
+
'mcp-tools': checkMcpToolInvocation,
|
|
91
|
+
'mcp-spell': checkMcpSpellIntegration,
|
|
92
|
+
'hooks': checkHookExecution,
|
|
93
|
+
'gates': checkGateHealth,
|
|
94
|
+
'gate': checkGateHealth,
|
|
95
|
+
'hook-drift': checkHookBlockDrift,
|
|
96
|
+
'drift': checkHookBlockDrift,
|
|
97
|
+
'sandbox': checkSandboxTier,
|
|
98
|
+
'sandbox-tier': checkSandboxTier,
|
|
99
|
+
'moflodb': checkMofloDbBridge,
|
|
100
|
+
'bridge': checkMofloDbBridge,
|
|
101
|
+
'swarm': checkSwarmFunctional,
|
|
102
|
+
'swarm-functional': checkSwarmFunctional,
|
|
103
|
+
'hive': checkHiveMindFunctional,
|
|
104
|
+
'hive-mind': checkHiveMindFunctional,
|
|
105
|
+
'hive-mind-functional': checkHiveMindFunctional,
|
|
106
|
+
'memory-access': checkMemoryAccessFunctional,
|
|
107
|
+
'memory-functional': checkMemoryAccessFunctional,
|
|
108
|
+
};
|
|
109
|
+
//# sourceMappingURL=doctor-registry.js.map
|