Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ PRs without a corresponding issue may be closed if they don't align with the pro

```bash
# Scripts
npm run doctor # Setup validation
node verify-pipeline.mjs # Health check
node cv-sync-check.mjs # Config check

Expand Down
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,17 @@ git clone https://github.com/santifer/career-ops.git
cd career-ops && npm install
npx playwright install chromium # Required for PDF generation

# 2. Configure
# 2. Check setup
npm run doctor # Validates all prerequisites

# 3. Configure
cp config/profile.example.yml config/profile.yml # Edit with your details
cp templates/portals.example.yml portals.yml # Customize companies

# 3. Add your CV
# 4. Add your CV
# Create cv.md in the project root with your CV in markdown

# 4. Personalize with Claude
# 5. Personalize with Claude
claude # Open Claude Code in this directory

# Then ask Claude to adapt the system to you:
Expand All @@ -73,7 +76,7 @@ claude # Open Claude Code in this directory
# "Add these 5 companies to portals.yml"
# "Update my profile with this CV I'm pasting"

# 5. Start using
# 6. Start using
# Paste a job URL or run /career-ops
```

Expand Down Expand Up @@ -240,14 +243,17 @@ Construido por alguien que lo uso para evaluar 740+ ofertas, generar 100+ CVs pe
git clone https://github.com/santifer/career-ops.git
cd career-ops && npm install

# 2. Configurar
# 2. Verificar setup
npm run doctor # Valida todos los prerequisitos

# 3. Configurar
cp config/profile.example.yml config/profile.yml # Editar con tus datos
cp templates/portals.example.yml portals.yml # Personalizar empresas

# 3. Añadir tu CV
# 4. Añadir tu CV
# Crear cv.md en la raiz del proyecto con tu CV en markdown

# 4. Personalizar con Claude
# 5. Personalizar con Claude
claude # Abrir Claude Code en este directorio

# Pidele a Claude que adapte el sistema a ti:
Expand All @@ -256,7 +262,7 @@ claude # Abrir Claude Code en este directorio
# "Añade estas empresas a portals.yml"
# "Actualiza mi perfil con este CV que te pego"

# 5. Usar
# 6. Usar
# Pega una URL de oferta o ejecuta /career-ops
```

Expand Down
197 changes: 197 additions & 0 deletions doctor.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/usr/bin/env node

/**
* doctor.mjs — Setup validation for career-ops
* Checks all prerequisites and prints a pass/fail checklist.
*/

import { existsSync, mkdirSync, readdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const projectRoot = __dirname;

// ANSI colors (only on TTY)
const isTTY = process.stdout.isTTY;
const green = (s) => isTTY ? `\x1b[32m${s}\x1b[0m` : s;
const red = (s) => isTTY ? `\x1b[31m${s}\x1b[0m` : s;
const dim = (s) => isTTY ? `\x1b[2m${s}\x1b[0m` : s;

function checkNodeVersion() {
const major = parseInt(process.versions.node.split('.')[0]);
if (major >= 18) {
return { pass: true, label: `Node.js >= 18 (v${process.versions.node})` };
}
return {
pass: false,
label: `Node.js >= 18 (found v${process.versions.node})`,
fix: 'Install Node.js 18 or later from https://nodejs.org',
};
}

function checkDependencies() {
if (existsSync(join(projectRoot, 'node_modules'))) {
return { pass: true, label: 'Dependencies installed' };
}
return {
pass: false,
label: 'Dependencies not installed',
fix: 'Run: npm install',
};
}

async function checkPlaywright() {
try {
const { chromium } = await import('playwright');
const execPath = chromium.executablePath();
if (existsSync(execPath)) {
return { pass: true, label: 'Playwright chromium installed' };
}
return {
pass: false,
label: 'Playwright chromium not installed',
fix: 'Run: npx playwright install chromium',
};
} catch {
return {
pass: false,
label: 'Playwright chromium not installed',
fix: 'Run: npx playwright install chromium',
};
}
}

function checkCv() {
if (existsSync(join(projectRoot, 'cv.md'))) {
return { pass: true, label: 'cv.md found' };
}
return {
pass: false,
label: 'cv.md not found',
fix: [
'Create cv.md in the project root with your CV in markdown',
'See examples/ for reference CVs',
],
};
}

function checkProfile() {
if (existsSync(join(projectRoot, 'config', 'profile.yml'))) {
return { pass: true, label: 'config/profile.yml found' };
}
return {
pass: false,
label: 'config/profile.yml not found',
fix: [
'Run: cp config/profile.example.yml config/profile.yml',
'Then edit it with your details',
],
};
}

function checkPortals() {
if (existsSync(join(projectRoot, 'portals.yml'))) {
return { pass: true, label: 'portals.yml found' };
}
return {
pass: false,
label: 'portals.yml not found',
fix: [
'Run: cp templates/portals.example.yml portals.yml',
'Then customize with your target companies',
],
};
}

function checkFonts() {
const fontsDir = join(projectRoot, 'fonts');
if (!existsSync(fontsDir)) {
return {
pass: false,
label: 'fonts/ directory not found',
fix: 'The fonts/ directory is required for PDF generation',
};
}
try {
const files = readdirSync(fontsDir);
if (files.length === 0) {
return {
pass: false,
label: 'fonts/ directory is empty',
fix: 'The fonts/ directory must contain font files for PDF generation',
};
}
} catch {
return {
pass: false,
label: 'fonts/ directory not readable',
fix: 'Check permissions on the fonts/ directory',
};
}
return { pass: true, label: 'Fonts directory ready' };
}

function checkAutoDir(name) {
const dirPath = join(projectRoot, name);
if (existsSync(dirPath)) {
return { pass: true, label: `${name}/ directory ready` };
}
try {
mkdirSync(dirPath, { recursive: true });
return { pass: true, label: `${name}/ directory ready (auto-created)` };
} catch {
return {
pass: false,
label: `${name}/ directory could not be created`,
fix: `Run: mkdir ${name}`,
};
}
}

async function main() {
console.log('\ncareer-ops doctor');
console.log('================\n');

const checks = [
checkNodeVersion(),
checkDependencies(),
await checkPlaywright(),
checkCv(),
checkProfile(),
checkPortals(),
checkFonts(),
checkAutoDir('data'),
checkAutoDir('output'),
checkAutoDir('reports'),
];

let failures = 0;

for (const result of checks) {
if (result.pass) {
console.log(`${green('✓')} ${result.label}`);
} else {
failures++;
console.log(`${red('✗')} ${result.label}`);
const fixes = Array.isArray(result.fix) ? result.fix : [result.fix];
for (const hint of fixes) {
console.log(` ${dim('→ ' + hint)}`);
}
}
}

console.log('');
if (failures > 0) {
console.log(`Result: ${failures} issue${failures === 1 ? '' : 's'} found. Fix them and run \`npm run doctor\` again.`);
process.exit(1);
} else {
console.log('Result: All checks passed. You\'re ready to go! Run `claude` to start.');
process.exit(0);
}
}

main().catch((err) => {
console.error('doctor.mjs failed:', err.message);
process.exit(1);
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "1.0.0",
"description": "AI-powered job search pipeline built on Claude Code",
"scripts": {
"doctor": "node doctor.mjs",
"verify": "node verify-pipeline.mjs",
"normalize": "node normalize-statuses.mjs",
"dedup": "node dedup-tracker.mjs",
Expand Down