resuml 1.21.0 → 3.0.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/dist/index.js CHANGED
@@ -1,30 +1,98 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- loadResumeFiles
4
- } from "./chunk-4ZOTZUAW.js";
5
2
  import {
6
3
  KNOWN_THEMES,
4
+ __export,
5
+ analyzeAts,
7
6
  generateResumeYaml,
8
7
  getInstalledVersion,
9
- isThemeInstalled
10
- } from "./chunk-JP7UCR3P.js";
11
- import {
12
- analyzeAts,
8
+ getRubricEntry,
9
+ isThemeInstalled,
10
+ listRubricMarkdown,
11
+ loadConfig,
12
+ loadTheme,
13
13
  processResumeData
14
- } from "./chunk-KRJMZ2RQ.js";
15
- import {
16
- __export,
17
- loadTheme
18
- } from "./chunk-ZLA7NFYP.js";
14
+ } from "./chunk-R4MD5YMV.js";
19
15
 
20
16
  // src/index.ts
21
17
  import { Command } from "commander";
22
- import path5 from "path";
23
- import fs7 from "fs";
18
+ import path6 from "path";
19
+ import fs9 from "fs";
24
20
  import { fileURLToPath } from "url";
25
21
 
26
22
  // src/commands/validate.ts
27
- import fs from "fs";
23
+ import fs3 from "fs";
24
+
25
+ // src/utils/loadResume.ts
26
+ import fs2 from "fs/promises";
27
+ import YAML from "yaml";
28
+
29
+ // src/utils/fileUtils.ts
30
+ import fs from "fs/promises";
31
+ import path from "path";
32
+ import { glob } from "glob";
33
+ async function findInputFiles(inputPath) {
34
+ if (!inputPath) {
35
+ return [];
36
+ }
37
+ if (inputPath.includes("*")) {
38
+ try {
39
+ const matchedFiles = await glob(inputPath);
40
+ if (matchedFiles.length === 0) {
41
+ throw new Error(`No files found matching pattern: ${inputPath}`);
42
+ }
43
+ return matchedFiles;
44
+ } catch (err) {
45
+ throw new Error(`Error matching files: ${err instanceof Error ? err.message : String(err)}`);
46
+ }
47
+ }
48
+ try {
49
+ const stat = await fs.stat(inputPath);
50
+ if (stat.isFile()) {
51
+ return [inputPath];
52
+ } else if (stat.isDirectory()) {
53
+ const pattern = path.join(inputPath, "*.{yaml,yml}");
54
+ try {
55
+ const yamlFiles = await glob(pattern);
56
+ if (yamlFiles.length === 0) {
57
+ throw new Error(`No YAML files found in directory: ${inputPath}`);
58
+ }
59
+ return yamlFiles;
60
+ } catch (_) {
61
+ throw new Error(`No YAML files found in directory: ${inputPath}`);
62
+ }
63
+ }
64
+ } catch (e) {
65
+ if (e instanceof Error && e.message.includes("ENOENT")) {
66
+ throw new Error("Input path not found");
67
+ }
68
+ throw e;
69
+ }
70
+ return [];
71
+ }
72
+
73
+ // src/utils/loadResume.ts
74
+ async function loadResumeFiles(inputPath) {
75
+ const files = await findInputFiles(inputPath);
76
+ if (files.length === 0) {
77
+ throw new Error("No resume files found");
78
+ }
79
+ const yamlContents = [];
80
+ for (const file of files) {
81
+ try {
82
+ const content = await fs2.readFile(file, "utf-8");
83
+ const parsed = YAML.parse(content);
84
+ if (parsed && typeof parsed === "object") {
85
+ yamlContents.push(content);
86
+ }
87
+ } catch (error) {
88
+ throw new Error(`Failed to parse ${file}: ${error.message}`);
89
+ }
90
+ }
91
+ if (yamlContents.length === 0) {
92
+ throw new Error("No valid data found in any of the input files");
93
+ }
94
+ return { files, yamlContents };
95
+ }
28
96
 
29
97
  // src/utils/errorHandler.ts
30
98
  function handleCommandError(error, command, debug = false) {
@@ -36,8 +104,8 @@ function handleCommandError(error, command, debug = false) {
36
104
  if (debug) {
37
105
  console.error("\nValidation failed with the following errors:");
38
106
  errors.forEach((err, index) => {
39
- const path6 = err.instancePath || "root";
40
- console.error(`${index + 1}. Path: ${path6}`);
107
+ const path7 = err.instancePath || "root";
108
+ console.error(`${index + 1}. Path: ${path7}`);
41
109
  console.error(` Error: ${err.message || "Unknown validation error"}`);
42
110
  if (err.params) {
43
111
  console.error(` Params: ${JSON.stringify(err.params)}`);
@@ -47,8 +115,8 @@ function handleCommandError(error, command, debug = false) {
47
115
  console.error("\nSome validation errors were found:");
48
116
  const maxErrors = 5;
49
117
  errors.slice(0, maxErrors).forEach((err, index) => {
50
- const path6 = err.instancePath || "root";
51
- console.error(`${index + 1}. Field: ${path6}`);
118
+ const path7 = err.instancePath || "root";
119
+ console.error(`${index + 1}. Field: ${path7}`);
52
120
  console.error(` Error: ${err.message || "Unknown validation error"}`);
53
121
  });
54
122
  if (errors.length > maxErrors) {
@@ -64,97 +132,85 @@ function handleCommandError(error, command, debug = false) {
64
132
  }
65
133
 
66
134
  // src/commands/validate.ts
67
- function formatAtsReport(result, debug, chalk6) {
68
- const scoreColor = result.score >= 75 ? chalk6.green : result.score >= 60 ? chalk6.yellow : chalk6.red;
135
+ function formatAtsReport(result, debug, chalk7) {
136
+ const scoreColor = result.score >= 75 ? chalk7.green : result.score >= 60 ? chalk7.yellow : chalk7.red;
69
137
  console.log("");
70
- console.log(chalk6.bold("\u2550\u2550\u2550 ATS Analysis Report \u2550\u2550\u2550"));
138
+ console.log(chalk7.bold("=== ATS Analysis Report ==="));
71
139
  console.log("");
72
- console.log(` Score: ${scoreColor(chalk6.bold(`${result.score}/100`))} (${result.rating.replace("-", " ")})`);
140
+ console.log(
141
+ ` Score: ${scoreColor(chalk7.bold(`${result.score}/100`))} (${result.rating.replace("-", " ")})`
142
+ );
73
143
  console.log(` ${result.summary}`);
74
144
  console.log("");
75
- const categories = {};
76
- for (const check of result.checks) {
77
- const list = categories[check.category];
78
- if (!list) {
79
- categories[check.category] = [check];
80
- } else {
81
- list.push(check);
82
- }
83
- }
84
- const categoryLabels = {
85
- contact: "Contact Information",
86
- content: "Content Quality",
87
- structure: "Resume Structure",
88
- keywords: "Keywords"
145
+ const tierLabels = {
146
+ parsing: "Parsing",
147
+ recruiter: "Recruiter",
148
+ match: "JD Match"
89
149
  };
90
- for (const [cat, checks] of Object.entries(categories)) {
91
- const label = categoryLabels[cat] || cat;
92
- console.log(chalk6.bold(` ${label}`));
93
- for (const check of checks) {
94
- if (!debug && check.passed) continue;
95
- const icon = check.passed ? chalk6.green("\u2713") : chalk6.red("\u2717");
96
- const scoreText = chalk6.dim(`[${check.score}]`);
150
+ for (const [tierName, tier] of Object.entries(result.tiers)) {
151
+ const label = tierLabels[tierName] ?? tierName;
152
+ console.log(chalk7.bold(` ${label} (${tier.score}/100, grade ${tier.grade})`));
153
+ for (const check of tier.checks) {
154
+ if (!debug && (check.status === "pass" || check.status === "skipped")) continue;
155
+ const icon = check.status === "pass" ? chalk7.green("v") : check.status === "skipped" ? chalk7.dim("-") : check.status === "warn" ? chalk7.yellow("!") : chalk7.red("x");
156
+ const scoreText = chalk7.dim(`[${check.score}]`);
97
157
  console.log(` ${icon} ${check.message} ${scoreText}`);
98
- if (!check.passed && check.suggestion) {
99
- console.log(chalk6.dim(` \u2192 ${check.suggestion}`));
158
+ for (const hint of check.hints) {
159
+ console.log(chalk7.dim(` -> ${hint}`));
100
160
  }
101
161
  }
102
162
  console.log("");
103
163
  }
104
- if (result.keywords) {
105
- console.log(chalk6.bold(" Job Description Match"));
106
- const kw = result.keywords;
107
- const matchColor = kw.matchPercentage >= 70 ? chalk6.green : kw.matchPercentage >= 50 ? chalk6.yellow : chalk6.red;
108
- console.log(` Match: ${matchColor(`${kw.matchPercentage}%`)} (${kw.matched.length}/${kw.matched.length + kw.missing.length} keywords)`);
109
- if (kw.matched.length > 0) {
110
- console.log(chalk6.green(` \u2713 Matched: ${kw.matched.join(", ")}`));
111
- }
112
- if (kw.missing.length > 0) {
113
- console.log(chalk6.red(` \u2717 Missing: ${kw.missing.join(", ")}`));
114
- console.log(chalk6.dim(" \u2192 Consider incorporating these keywords into your resume where relevant."));
164
+ if (result.knockouts.length > 0) {
165
+ console.log(chalk7.bold(" Knockout Signals"));
166
+ for (const k of result.knockouts) {
167
+ console.log(chalk7.red(` ! ${k.signal}: ${k.evidence}`));
168
+ console.log(chalk7.dim(` -> ${k.recommendation}`));
115
169
  }
116
170
  console.log("");
117
171
  }
118
- console.log(chalk6.dim("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
172
+ console.log(chalk7.dim("==========================="));
119
173
  }
120
174
  async function validateAction(options) {
121
- const chalk6 = (await import("chalk")).default;
122
- console.log(chalk6.blue("Starting resuml validate..."));
175
+ const chalk7 = (await import("chalk")).default;
176
+ console.log(chalk7.blue("Starting resuml validate..."));
123
177
  try {
124
178
  const inputPath = options.resume;
125
179
  const { yamlContents } = await loadResumeFiles(inputPath);
126
- console.log(chalk6.blue("Validating resume data..."));
180
+ console.log(chalk7.blue("Validating resume data..."));
127
181
  let resumeData;
128
182
  try {
129
183
  resumeData = await processResumeData(yamlContents);
130
- console.log(chalk6.green("\u2713 Resume data is valid against the schema!"));
184
+ console.log(chalk7.green("\u2713 Resume data is valid against the schema!"));
131
185
  } catch (error) {
132
186
  handleCommandError(error, "validate", options.debug);
133
187
  return;
134
188
  }
135
189
  if (options.ats) {
136
- console.log(chalk6.blue("Running ATS analysis..."));
190
+ console.log(chalk7.blue("Running ATS analysis..."));
137
191
  let jobDescription;
138
192
  if (options.jd) {
139
193
  try {
140
- jobDescription = fs.readFileSync(options.jd, "utf8");
194
+ jobDescription = fs3.readFileSync(options.jd, "utf8");
141
195
  } catch {
142
- console.error(chalk6.red(`Failed to read job description file: ${options.jd}`));
196
+ console.error(chalk7.red(`Failed to read job description file: ${options.jd}`));
143
197
  return;
144
198
  }
145
199
  }
200
+ const cfg = loadConfig(options.config ? { configPath: options.config } : {});
146
201
  const result = analyzeAts(resumeData, {
147
- language: "en",
148
- jobDescription
202
+ language: cfg.locale,
203
+ jobDescription,
204
+ config: cfg
149
205
  });
150
206
  if (options.format === "json") {
151
207
  console.log(JSON.stringify(result, null, 2));
152
208
  } else {
153
- formatAtsReport(result, !!options.debug, chalk6);
209
+ formatAtsReport(result, !!options.debug, chalk7);
154
210
  }
155
211
  const threshold = options.atsThreshold ? parseInt(options.atsThreshold, 10) : void 0;
156
212
  if (threshold !== void 0 && result.score < threshold) {
157
- console.error(chalk6.red(`
213
+ console.error(chalk7.red(`
158
214
  ATS score ${result.score} is below threshold ${threshold}.`));
159
215
  process.exit(1);
160
216
  }
@@ -165,27 +221,27 @@ ATS score ${result.score} is below threshold ${threshold}.`));
165
221
  }
166
222
 
167
223
  // src/commands/tojson.ts
168
- import fs2 from "fs";
224
+ import fs4 from "fs";
169
225
  async function toJsonAction(options) {
170
- const chalk6 = (await import("chalk")).default;
171
- console.log(chalk6.blue("Starting resuml tojson..."));
226
+ const chalk7 = (await import("chalk")).default;
227
+ console.log(chalk7.blue("Starting resuml tojson..."));
172
228
  try {
173
229
  const inputPath = options.resume;
174
230
  const { yamlContents } = await loadResumeFiles(inputPath);
175
- console.log(chalk6.blue("Processing and validating data..."));
231
+ console.log(chalk7.blue("Processing and validating data..."));
176
232
  const resumeData = await processResumeData(yamlContents);
177
- console.log(chalk6.green("Processing and validation successful!"));
233
+ console.log(chalk7.green("Processing and validation successful!"));
178
234
  const jsonOutput = JSON.stringify(resumeData, null, 2);
179
- fs2.writeFileSync(options.output, jsonOutput, "utf8");
180
- console.log(chalk6.green(`Successfully wrote output to ${options.output}`));
235
+ fs4.writeFileSync(options.output, jsonOutput, "utf8");
236
+ console.log(chalk7.green(`Successfully wrote output to ${options.output}`));
181
237
  } catch (error) {
182
238
  handleCommandError(error, "tojson", options.debug);
183
239
  }
184
240
  }
185
241
 
186
242
  // src/commands/render.ts
187
- import fs3 from "fs";
188
- import path from "path";
243
+ import fs5 from "fs";
244
+ import path2 from "path";
189
245
  import chalk from "chalk";
190
246
  async function renderAction(options) {
191
247
  if (!options.theme) {
@@ -225,12 +281,12 @@ async function renderAction(options) {
225
281
  }
226
282
  });
227
283
  await browser.close();
228
- fs3.writeFileSync(outputPath, pdfBuffer);
284
+ fs5.writeFileSync(outputPath, pdfBuffer);
229
285
  console.log(chalk.green(`Successfully wrote PDF output to ${outputPath}`));
230
286
  } else {
231
287
  console.log(chalk.blue(`Writing HTML output to ${outputPath}...`));
232
- fs3.mkdirSync(path.dirname(outputPath), { recursive: true });
233
- fs3.writeFileSync(outputPath, htmlOutput, "utf8");
288
+ fs5.mkdirSync(path2.dirname(outputPath), { recursive: true });
289
+ fs5.writeFileSync(outputPath, htmlOutput, "utf8");
234
290
  console.log(chalk.green(`Successfully wrote HTML output to ${outputPath}`));
235
291
  }
236
292
  } catch (error) {
@@ -239,8 +295,8 @@ async function renderAction(options) {
239
295
  }
240
296
 
241
297
  // src/commands/dev.ts
242
- import fs4 from "fs";
243
- import path2 from "path";
298
+ import fs6 from "fs";
299
+ import path3 from "path";
244
300
  import chalk2 from "chalk";
245
301
  async function devAction(options) {
246
302
  if (!options.theme) {
@@ -258,11 +314,11 @@ async function devAction(options) {
258
314
  await renderResume(options);
259
315
  console.log(chalk2.green(`\u{1F680} Development server running at http://localhost:${port}`));
260
316
  console.log(chalk2.blue("Watching for file changes..."));
261
- if (fs4.existsSync(inputPath) && fs4.statSync(inputPath).isDirectory()) {
317
+ if (fs6.existsSync(inputPath) && fs6.statSync(inputPath).isDirectory()) {
262
318
  watchDirectory(inputPath, () => {
263
319
  void renderResume(options);
264
320
  });
265
- } else if (fs4.existsSync(inputPath)) {
321
+ } else if (fs6.existsSync(inputPath)) {
266
322
  watchFile(inputPath, () => {
267
323
  void renderResume(options);
268
324
  });
@@ -285,16 +341,16 @@ async function renderResume(options) {
285
341
  const htmlOutput = await theme.render(resumeData, {
286
342
  locale: options.language
287
343
  });
288
- const outputPath = path2.join(process.cwd(), ".resuml-dev", "index.html");
289
- fs4.mkdirSync(path2.dirname(outputPath), { recursive: true });
290
- fs4.writeFileSync(outputPath, htmlOutput, "utf8");
344
+ const outputPath = path3.join(process.cwd(), ".resuml-dev", "index.html");
345
+ fs6.mkdirSync(path3.dirname(outputPath), { recursive: true });
346
+ fs6.writeFileSync(outputPath, htmlOutput, "utf8");
291
347
  console.log(chalk2.green("\u2705 Resume updated!"));
292
348
  } catch (error) {
293
349
  console.error(chalk2.red("\u274C Error rendering resume:"), error.message);
294
350
  }
295
351
  }
296
352
  function watchDirectory(dirPath, callback) {
297
- fs4.watch(dirPath, { recursive: true }, (_eventType, filename) => {
353
+ fs6.watch(dirPath, { recursive: true }, (_eventType, filename) => {
298
354
  if (filename && (filename.endsWith(".yaml") || filename.endsWith(".yml"))) {
299
355
  console.log(chalk2.blue(`\u{1F4C1} File changed: ${filename}`));
300
356
  callback();
@@ -302,9 +358,9 @@ function watchDirectory(dirPath, callback) {
302
358
  });
303
359
  }
304
360
  function watchFile(filePath, callback) {
305
- fs4.watch(filePath, (eventType) => {
361
+ fs6.watch(filePath, (eventType) => {
306
362
  if (eventType === "change") {
307
- console.log(chalk2.blue(`\u{1F4C4} File changed: ${path2.basename(filePath)}`));
363
+ console.log(chalk2.blue(`\u{1F4C4} File changed: ${path3.basename(filePath)}`));
308
364
  callback();
309
365
  }
310
366
  });
@@ -316,9 +372,9 @@ async function startDevServer(port) {
316
372
  const parsedUrl = url.parse(req.url || "", true);
317
373
  const pathname = parsedUrl.pathname || "/";
318
374
  if (pathname === "/" || pathname === "/index.html") {
319
- const htmlPath = path2.join(process.cwd(), ".resuml-dev", "index.html");
320
- if (fs4.existsSync(htmlPath)) {
321
- const html = fs4.readFileSync(htmlPath, "utf8");
375
+ const htmlPath = path3.join(process.cwd(), ".resuml-dev", "index.html");
376
+ if (fs6.existsSync(htmlPath)) {
377
+ const html = fs6.readFileSync(htmlPath, "utf8");
322
378
  const liveReloadScript = `
323
379
  <script>
324
380
  setInterval(() => {
@@ -351,8 +407,8 @@ async function startDevServer(port) {
351
407
  }
352
408
 
353
409
  // src/commands/init.ts
354
- import fs5 from "fs";
355
- import path3 from "path";
410
+ import fs7 from "fs";
411
+ import path4 from "path";
356
412
  import readline from "readline";
357
413
  import chalk3 from "chalk";
358
414
  function createReadlineInterface() {
@@ -371,10 +427,10 @@ function ask(rl, question, defaultValue) {
371
427
  }
372
428
  async function initAction(options) {
373
429
  const outputPath = options.output || "resume.yaml";
374
- const fullPath = path3.resolve(outputPath);
430
+ const fullPath = path4.resolve(outputPath);
375
431
  const rl = createReadlineInterface();
376
432
  try {
377
- if (fs5.existsSync(fullPath)) {
433
+ if (fs7.existsSync(fullPath)) {
378
434
  const overwrite = await ask(
379
435
  rl,
380
436
  `${chalk3.yellow("\u26A0")} ${outputPath} already exists. Overwrite? (y/N)`,
@@ -389,23 +445,45 @@ async function initAction(options) {
389
445
  const name = await ask(rl, "Your full name", "John Doe");
390
446
  const email = await ask(rl, "Email address", "john@example.com");
391
447
  const label = await ask(rl, "Professional title/label", "Software Engineer");
392
- const yaml = generateResumeYaml(name, email, label);
393
- fs5.mkdirSync(path3.dirname(fullPath), { recursive: true });
394
- fs5.writeFileSync(fullPath, yaml, "utf8");
448
+ const yaml2 = generateResumeYaml(name, email, label);
449
+ fs7.mkdirSync(path4.dirname(fullPath), { recursive: true });
450
+ fs7.writeFileSync(fullPath, yaml2, "utf8");
451
+ const configPath = path4.join(path4.dirname(fullPath), "resuml.config.yaml");
452
+ const template = `# resuml ATS configuration
453
+ # Override defaults; everything is optional.
454
+ # Run \`resuml ats config --print\` to see the merged effective config.
455
+ ats:
456
+ weights:
457
+ tiers:
458
+ parsing: 30
459
+ match: 50
460
+ recruiter: 20
461
+ thresholds:
462
+ seniorYoeCutoff: 10
463
+ disable: []
464
+ `;
465
+ try {
466
+ fs7.writeFileSync(configPath, template, { encoding: "utf8", flag: "wx" });
467
+ console.log(chalk3.green(`Created resuml.config.yaml`));
468
+ } catch (err) {
469
+ if (err.code !== "EEXIST") throw err;
470
+ }
395
471
  console.log(chalk3.green(`
396
472
  \u2705 Created ${outputPath}`));
397
473
  console.log(chalk3.blue("\nNext steps:"));
398
474
  console.log(` 1. Edit ${outputPath} to fill in your details`);
399
475
  console.log(" 2. Run " + chalk3.cyan("resuml validate --resume " + outputPath));
400
- console.log(" 3. Run " + chalk3.cyan("resuml render --resume " + outputPath + " --theme stackoverflow"));
476
+ console.log(
477
+ " 3. Run " + chalk3.cyan("resuml render --resume " + outputPath + " --theme stackoverflow")
478
+ );
401
479
  } finally {
402
480
  rl.close();
403
481
  }
404
482
  }
405
483
 
406
484
  // src/commands/pdf.ts
407
- import fs6 from "fs";
408
- import path4 from "path";
485
+ import fs8 from "fs";
486
+ import path5 from "path";
409
487
  import chalk4 from "chalk";
410
488
  async function loadPlaywright() {
411
489
  try {
@@ -460,14 +538,46 @@ async function pdfAction(options) {
460
538
  try {
461
539
  const page = await browser.newPage();
462
540
  await page.setContent(htmlOutput, { waitUntil: "networkidle" });
541
+ const bodyText = await page.evaluate(() => document.body.innerText || "");
542
+ const bodyWords = bodyText.trim().split(/\s+/).filter(Boolean).length;
543
+ const resumeContentParts = [];
544
+ if (resumeData.basics?.summary) resumeContentParts.push(resumeData.basics.summary);
545
+ for (const w of resumeData.work || []) {
546
+ if (w.summary) resumeContentParts.push(w.summary);
547
+ resumeContentParts.push(...w.highlights || []);
548
+ }
549
+ for (const p of resumeData.projects || []) {
550
+ if (p.description) resumeContentParts.push(p.description);
551
+ resumeContentParts.push(...p.highlights || []);
552
+ }
553
+ for (const s of resumeData.skills || []) {
554
+ if (s.name) resumeContentParts.push(s.name);
555
+ resumeContentParts.push(...s.keywords || []);
556
+ }
557
+ const resumeWords = resumeContentParts.join(" ").split(/\s+/).filter(Boolean).length;
558
+ if (resumeWords > 0 && bodyWords < resumeWords * 0.7) {
559
+ console.warn(
560
+ chalk4.yellow(
561
+ `pdf-text-extractable: rendered text ${bodyWords} words vs resume ${resumeWords} (under 70%). Theme may use image-based glyphs.`
562
+ )
563
+ );
564
+ }
463
565
  const pdfBuffer = await page.pdf({
464
566
  format,
465
567
  margin,
466
568
  printBackground: true,
467
569
  preferCSSPageSize: true
468
570
  });
469
- fs6.mkdirSync(path4.dirname(path4.resolve(outputPath)), { recursive: true });
470
- fs6.writeFileSync(outputPath, pdfBuffer);
571
+ fs8.mkdirSync(path5.dirname(path5.resolve(outputPath)), { recursive: true });
572
+ fs8.writeFileSync(outputPath, pdfBuffer);
573
+ const sizeMb = pdfBuffer.length / (1024 * 1024);
574
+ if (sizeMb > 2.5) {
575
+ console.warn(
576
+ chalk4.yellow(
577
+ `pdf-size-under-2.5mb: PDF is ${sizeMb.toFixed(2)} MB (Greenhouse limit 2.5 MB).`
578
+ )
579
+ );
580
+ }
471
581
  console.log(chalk4.green(`\u2705 Successfully generated ${outputPath}`));
472
582
  } finally {
473
583
  await browser.close();
@@ -487,7 +597,9 @@ function listThemes() {
487
597
  console.log(
488
598
  ` ${"Status".padEnd(10)}${"Name".padEnd(nameWidth)}${"Package".padEnd(pkgWidth)}Description`
489
599
  );
490
- console.log(` ${"\u2500".repeat(10)}${"\u2500".repeat(nameWidth)}${"\u2500".repeat(pkgWidth)}${"\u2500".repeat(30)}`);
600
+ console.log(
601
+ ` ${"\u2500".repeat(10)}${"\u2500".repeat(nameWidth)}${"\u2500".repeat(pkgWidth)}${"\u2500".repeat(30)}`
602
+ );
491
603
  for (const theme of KNOWN_THEMES) {
492
604
  const installed = isThemeInstalled(theme.pkg);
493
605
  const version = installed ? getInstalledVersion(theme.pkg) : null;
@@ -514,14 +626,18 @@ function installTheme(name) {
514
626
  execSync(`npm install ${pkg}`, { stdio: "inherit" });
515
627
  console.log(chalk5.green(`
516
628
  \u2705 Successfully installed ${pkg}`));
517
- console.log(chalk5.blue(`
629
+ console.log(
630
+ chalk5.blue(`
518
631
  Use it with: ${chalk5.cyan(`resuml render --theme ${known?.name || name}`)}
519
- `));
632
+ `)
633
+ );
520
634
  } catch {
521
635
  console.error(chalk5.red(`
522
636
  \u274C Failed to install ${pkg}`));
523
- console.error(chalk5.yellow(`Make sure the package exists: https://www.npmjs.com/package/${pkg}
524
- `));
637
+ console.error(
638
+ chalk5.yellow(`Make sure the package exists: https://www.npmjs.com/package/${pkg}
639
+ `)
640
+ );
525
641
  }
526
642
  }
527
643
  function themesAction(options) {
@@ -538,6 +654,32 @@ async function mcpAction() {
538
654
  await startMcpServer();
539
655
  }
540
656
 
657
+ // src/commands/ats.ts
658
+ import yaml from "yaml";
659
+ import chalk6 from "chalk";
660
+ function atsExplain(id) {
661
+ if (!id) {
662
+ console.log(listRubricMarkdown());
663
+ return;
664
+ }
665
+ const entry = getRubricEntry(id);
666
+ if (!entry) {
667
+ console.error(chalk6.red(`Unknown rubric id: ${id}`));
668
+ process.exit(1);
669
+ return;
670
+ }
671
+ console.log(chalk6.bold(entry.id));
672
+ console.log(` Tier: ${entry.tier}`);
673
+ console.log(` Weight: ${entry.weight}`);
674
+ console.log(` Evidence: ${entry.evidenceLevel}`);
675
+ console.log(` Description: ${entry.description}`);
676
+ if (entry.source) console.log(` Source: ${entry.source}`);
677
+ }
678
+ function atsConfigPrint(opts) {
679
+ const cfg = loadConfig(opts.config ? { configPath: opts.config } : {});
680
+ console.log(yaml.stringify({ ats: cfg }));
681
+ }
682
+
541
683
  // src/utils/themeRender.ts
542
684
  var themeRender_exports = {};
543
685
  __export(themeRender_exports, {
@@ -591,12 +733,12 @@ function injectCss(html, css) {
591
733
  }
592
734
 
593
735
  // src/index.ts
594
- var currentDir = path5.dirname(fileURLToPath(import.meta.url));
736
+ var currentDir = path6.dirname(fileURLToPath(import.meta.url));
595
737
  function getCliVersion() {
596
- const packageJsonPath = path5.resolve(currentDir, "../package.json");
597
- if (fs7.existsSync(packageJsonPath)) {
738
+ const packageJsonPath = path6.resolve(currentDir, "../package.json");
739
+ if (fs9.existsSync(packageJsonPath)) {
598
740
  try {
599
- const packageJson = JSON.parse(fs7.readFileSync(packageJsonPath, "utf8"));
741
+ const packageJson = JSON.parse(fs9.readFileSync(packageJsonPath, "utf8"));
600
742
  return packageJson.version ?? "0.0.0";
601
743
  } catch {
602
744
  return "0.0.0";
@@ -606,7 +748,10 @@ function getCliVersion() {
606
748
  }
607
749
  var program = new Command();
608
750
  program.name("resuml").description("CLI tool for managing resuml resume files.").version(getCliVersion());
609
- program.command("validate").description("Validates resume data against the schema.").option("-r, --resume <path>", "Input YAML file, directory, or glob pattern.").option("--debug", "Show detailed validation errors.").option("--ats", "Run ATS (Applicant Tracking System) compatibility analysis.").option("--jd <path>", "Path to a job description file for keyword matching (requires --ats).").option("--ats-threshold <score>", "Minimum ATS score (0-100). Exit with code 1 if below threshold.").option("--format <type>", "Output format for ATS results (text or json).", "text").action(validateAction);
751
+ program.command("validate").description("Validates resume data against the schema.").option("-r, --resume <path>", "Input YAML file, directory, or glob pattern.").option("--debug", "Show detailed validation errors.").option("--ats", "Run ATS (Applicant Tracking System) compatibility analysis.").option("--jd <path>", "Path to a job description file for keyword matching (requires --ats).").option(
752
+ "--ats-threshold <score>",
753
+ "Minimum ATS score (0-100). Exit with code 1 if below threshold."
754
+ ).option("--format <type>", "Output format for ATS results (text or json).", "text").option("--config <path>", "Path to resuml.config.yaml (default: ./resuml.config.yaml).").action(validateAction);
610
755
  program.command("tojson").description("Converts YAML resume data to JSON format.").option("-r, --resume <path>", "Input YAML file, directory, or glob pattern.").option("-o, --output <file>", "Output JSON file path.", "resume.json").option("--debug", "Show detailed validation and processing information.").action(toJsonAction);
611
756
  program.command("render").description("Renders the resume data using a specified theme.").option("-r, --resume <path>", "Input YAML file, directory, or glob pattern.").option("-t, --theme <name>", "Theme name (e.g., stackoverflow, react).").option("-o, --output <file>", "Output file path.").option("--format <type>", "Output format (html or pdf).", "html").option("--language <code>", "Language code for localization.", "en").option("--debug", "Show detailed validation and processing information.").action(renderAction);
612
757
  program.command("dev").description("Start development server with hot-reload.").option("-r, --resume <path>", "Input YAML file, directory, or glob pattern.").option("-t, --theme <name>", "Theme name (e.g., stackoverflow, react).").option("--port <number>", "Port for development server.", "3000").option("--language <code>", "Language code for localization.", "en").option("--debug", "Show detailed validation and processing information.").action(devAction);
@@ -614,6 +759,13 @@ program.command("init").description("Scaffold a starter resume.yaml file with al
614
759
  program.command("pdf").description("Export resume as PDF using Playwright.").option("-r, --resume <path>", "Input YAML file, directory, or glob pattern.").option("-t, --theme <name>", "Theme name (e.g., stackoverflow, react).").option("-o, --output <file>", "Output PDF file path.", "resume.pdf").option("--language <code>", "Language code for localization.", "en").option("--format <size>", "Page format: A4 or Letter.", "A4").option("--margin <values>", 'Page margins (e.g., "10mm" or "10mm,15mm,10mm,15mm").').option("--debug", "Show detailed validation and processing information.").action(pdfAction);
615
760
  program.command("themes").description("List available JSON Resume themes and install them.").option("--install <name>", "Install a theme by name (e.g., stackoverflow, elegant).").action(themesAction);
616
761
  program.command("mcp").description("Start MCP server for AI agent integration (stdio transport).").action(mcpAction);
762
+ var ats = program.command("ats").description("ATS rubric utilities.");
763
+ ats.command("explain [id]").description("Print rubric entry for a check id, or full rubric if id omitted.").action((id) => {
764
+ atsExplain(id);
765
+ });
766
+ ats.command("config").description("Print the effective merged ATS config.").option("--print", "Print the merged config (default action).").option("--config <path>", "Path to resuml.config.yaml.").action((opts) => {
767
+ atsConfigPrint(opts);
768
+ });
617
769
  if (process.env["NODE_ENV"] !== "test") {
618
770
  void (async () => {
619
771
  try {