resuml 3.0.0 → 3.1.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,781 +1,16 @@
1
- #!/usr/bin/env node
2
1
  import {
3
- KNOWN_THEMES,
4
- __export,
5
- analyzeAts,
6
- generateResumeYaml,
7
- getInstalledVersion,
8
- getRubricEntry,
9
- isThemeInstalled,
10
- listRubricMarkdown,
11
- loadConfig,
2
+ loadResumeFiles,
3
+ program,
4
+ themeRender_exports
5
+ } from "./chunk-M6JY5UDJ.js";
6
+ import {
12
7
  loadTheme,
13
8
  processResumeData
14
- } from "./chunk-R4MD5YMV.js";
15
-
16
- // src/index.ts
17
- import { Command } from "commander";
18
- import path6 from "path";
19
- import fs9 from "fs";
20
- import { fileURLToPath } from "url";
21
-
22
- // src/commands/validate.ts
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
- }
96
-
97
- // src/utils/errorHandler.ts
98
- function handleCommandError(error, command, debug = false) {
99
- const errorMessage = error instanceof Error ? error.message : String(error);
100
- console.error(`\u274C Error during ${command} command: ${errorMessage}`);
101
- const potentialValidationError = error;
102
- if (potentialValidationError instanceof Error && potentialValidationError.name === "SchemaValidationError" && Array.isArray(potentialValidationError.errors)) {
103
- const errors = potentialValidationError.errors;
104
- if (debug) {
105
- console.error("\nValidation failed with the following errors:");
106
- errors.forEach((err, index) => {
107
- const path7 = err.instancePath || "root";
108
- console.error(`${index + 1}. Path: ${path7}`);
109
- console.error(` Error: ${err.message || "Unknown validation error"}`);
110
- if (err.params) {
111
- console.error(` Params: ${JSON.stringify(err.params)}`);
112
- }
113
- });
114
- } else {
115
- console.error("\nSome validation errors were found:");
116
- const maxErrors = 5;
117
- errors.slice(0, maxErrors).forEach((err, index) => {
118
- const path7 = err.instancePath || "root";
119
- console.error(`${index + 1}. Field: ${path7}`);
120
- console.error(` Error: ${err.message || "Unknown validation error"}`);
121
- });
122
- if (errors.length > maxErrors) {
123
- console.error(`
124
- ...and ${errors.length - maxErrors} more errors.`);
125
- console.error("Use the --debug flag for complete error details.");
126
- }
127
- }
128
- }
129
- if (process.env["NODE_ENV"] !== "test") {
130
- process.exit(1);
131
- }
132
- }
133
-
134
- // src/commands/validate.ts
135
- function formatAtsReport(result, debug, chalk7) {
136
- const scoreColor = result.score >= 75 ? chalk7.green : result.score >= 60 ? chalk7.yellow : chalk7.red;
137
- console.log("");
138
- console.log(chalk7.bold("=== ATS Analysis Report ==="));
139
- console.log("");
140
- console.log(
141
- ` Score: ${scoreColor(chalk7.bold(`${result.score}/100`))} (${result.rating.replace("-", " ")})`
142
- );
143
- console.log(` ${result.summary}`);
144
- console.log("");
145
- const tierLabels = {
146
- parsing: "Parsing",
147
- recruiter: "Recruiter",
148
- match: "JD Match"
149
- };
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}]`);
157
- console.log(` ${icon} ${check.message} ${scoreText}`);
158
- for (const hint of check.hints) {
159
- console.log(chalk7.dim(` -> ${hint}`));
160
- }
161
- }
162
- console.log("");
163
- }
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}`));
169
- }
170
- console.log("");
171
- }
172
- console.log(chalk7.dim("==========================="));
173
- }
174
- async function validateAction(options) {
175
- const chalk7 = (await import("chalk")).default;
176
- console.log(chalk7.blue("Starting resuml validate..."));
177
- try {
178
- const inputPath = options.resume;
179
- const { yamlContents } = await loadResumeFiles(inputPath);
180
- console.log(chalk7.blue("Validating resume data..."));
181
- let resumeData;
182
- try {
183
- resumeData = await processResumeData(yamlContents);
184
- console.log(chalk7.green("\u2713 Resume data is valid against the schema!"));
185
- } catch (error) {
186
- handleCommandError(error, "validate", options.debug);
187
- return;
188
- }
189
- if (options.ats) {
190
- console.log(chalk7.blue("Running ATS analysis..."));
191
- let jobDescription;
192
- if (options.jd) {
193
- try {
194
- jobDescription = fs3.readFileSync(options.jd, "utf8");
195
- } catch {
196
- console.error(chalk7.red(`Failed to read job description file: ${options.jd}`));
197
- return;
198
- }
199
- }
200
- const cfg = loadConfig(options.config ? { configPath: options.config } : {});
201
- const result = analyzeAts(resumeData, {
202
- language: cfg.locale,
203
- jobDescription,
204
- config: cfg
205
- });
206
- if (options.format === "json") {
207
- console.log(JSON.stringify(result, null, 2));
208
- } else {
209
- formatAtsReport(result, !!options.debug, chalk7);
210
- }
211
- const threshold = options.atsThreshold ? parseInt(options.atsThreshold, 10) : void 0;
212
- if (threshold !== void 0 && result.score < threshold) {
213
- console.error(chalk7.red(`
214
- ATS score ${result.score} is below threshold ${threshold}.`));
215
- process.exit(1);
216
- }
217
- }
218
- } catch (error) {
219
- handleCommandError(error, "validate", options.debug);
220
- }
221
- }
222
-
223
- // src/commands/tojson.ts
224
- import fs4 from "fs";
225
- async function toJsonAction(options) {
226
- const chalk7 = (await import("chalk")).default;
227
- console.log(chalk7.blue("Starting resuml tojson..."));
228
- try {
229
- const inputPath = options.resume;
230
- const { yamlContents } = await loadResumeFiles(inputPath);
231
- console.log(chalk7.blue("Processing and validating data..."));
232
- const resumeData = await processResumeData(yamlContents);
233
- console.log(chalk7.green("Processing and validation successful!"));
234
- const jsonOutput = JSON.stringify(resumeData, null, 2);
235
- fs4.writeFileSync(options.output, jsonOutput, "utf8");
236
- console.log(chalk7.green(`Successfully wrote output to ${options.output}`));
237
- } catch (error) {
238
- handleCommandError(error, "tojson", options.debug);
239
- }
240
- }
241
-
242
- // src/commands/render.ts
243
- import fs5 from "fs";
244
- import path2 from "path";
245
- import chalk from "chalk";
246
- async function renderAction(options) {
247
- if (!options.theme) {
248
- throw new Error(
249
- "--theme option is required. Please specify a theme name (e.g., stackoverflow, react)."
250
- );
251
- }
252
- console.log(chalk.blue("Starting resuml render..."));
253
- try {
254
- const inputPath = options.resume;
255
- const { yamlContents } = await loadResumeFiles(inputPath);
256
- console.log(chalk.blue("Processing and validating resume data..."));
257
- const resumeData = await processResumeData(yamlContents);
258
- console.log(chalk.green("Resume data processing and validation successful!"));
259
- const theme = loadTheme(options.theme);
260
- const htmlOutput = await theme.render(resumeData, {
261
- locale: options.language
262
- });
263
- const defaultExtension = options.format;
264
- const defaultFilename = `resume.${defaultExtension}`;
265
- const outputPath = options.output || defaultFilename;
266
- if (options.format === "pdf") {
267
- console.log(chalk.blue(`Generating PDF output at ${outputPath}...`));
268
- const { chromium } = await import("playwright");
269
- const browser = await chromium.launch();
270
- const page = await browser.newPage();
271
- await page.setContent(htmlOutput, { waitUntil: "networkidle" });
272
- const pdfBuffer = await page.pdf({
273
- path: outputPath,
274
- format: "A4",
275
- printBackground: true,
276
- margin: {
277
- top: "1cm",
278
- right: "1cm",
279
- bottom: "1cm",
280
- left: "1cm"
281
- }
282
- });
283
- await browser.close();
284
- fs5.writeFileSync(outputPath, pdfBuffer);
285
- console.log(chalk.green(`Successfully wrote PDF output to ${outputPath}`));
286
- } else {
287
- console.log(chalk.blue(`Writing HTML output to ${outputPath}...`));
288
- fs5.mkdirSync(path2.dirname(outputPath), { recursive: true });
289
- fs5.writeFileSync(outputPath, htmlOutput, "utf8");
290
- console.log(chalk.green(`Successfully wrote HTML output to ${outputPath}`));
291
- }
292
- } catch (error) {
293
- handleCommandError(error, "render", options.debug);
294
- }
295
- }
296
-
297
- // src/commands/dev.ts
298
- import fs6 from "fs";
299
- import path3 from "path";
300
- import chalk2 from "chalk";
301
- async function devAction(options) {
302
- if (!options.theme) {
303
- throw new Error(
304
- "--theme option is required. Please specify a theme name (e.g., stackoverflow, react)."
305
- );
306
- }
307
- console.log(chalk2.blue("Starting resuml development server..."));
308
- const port = options.port || 3e3;
309
- const inputPath = options.resume;
310
- if (!inputPath) {
311
- throw new Error("Resume path is required. Use -r or --resume option.");
312
- }
313
- try {
314
- await renderResume(options);
315
- console.log(chalk2.green(`\u{1F680} Development server running at http://localhost:${port}`));
316
- console.log(chalk2.blue("Watching for file changes..."));
317
- if (fs6.existsSync(inputPath) && fs6.statSync(inputPath).isDirectory()) {
318
- watchDirectory(inputPath, () => {
319
- void renderResume(options);
320
- });
321
- } else if (fs6.existsSync(inputPath)) {
322
- watchFile(inputPath, () => {
323
- void renderResume(options);
324
- });
325
- }
326
- await startDevServer(port);
327
- } catch (error) {
328
- handleCommandError(error, "dev", options.debug);
329
- }
330
- }
331
- async function renderResume(options) {
332
- try {
333
- const inputPath = options.resume;
334
- if (!inputPath) {
335
- throw new Error("Resume path is required");
336
- }
337
- const { yamlContents } = await loadResumeFiles(inputPath);
338
- console.log(chalk2.blue("\u{1F504} Processing resume data..."));
339
- const resumeData = await processResumeData(yamlContents);
340
- const theme = loadTheme(options.theme ?? "stackoverflow");
341
- const htmlOutput = await theme.render(resumeData, {
342
- locale: options.language
343
- });
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");
347
- console.log(chalk2.green("\u2705 Resume updated!"));
348
- } catch (error) {
349
- console.error(chalk2.red("\u274C Error rendering resume:"), error.message);
350
- }
351
- }
352
- function watchDirectory(dirPath, callback) {
353
- fs6.watch(dirPath, { recursive: true }, (_eventType, filename) => {
354
- if (filename && (filename.endsWith(".yaml") || filename.endsWith(".yml"))) {
355
- console.log(chalk2.blue(`\u{1F4C1} File changed: ${filename}`));
356
- callback();
357
- }
358
- });
359
- }
360
- function watchFile(filePath, callback) {
361
- fs6.watch(filePath, (eventType) => {
362
- if (eventType === "change") {
363
- console.log(chalk2.blue(`\u{1F4C4} File changed: ${path3.basename(filePath)}`));
364
- callback();
365
- }
366
- });
367
- }
368
- async function startDevServer(port) {
369
- const http = await import("http");
370
- const url = await import("url");
371
- const server = http.createServer((req, res) => {
372
- const parsedUrl = url.parse(req.url || "", true);
373
- const pathname = parsedUrl.pathname || "/";
374
- if (pathname === "/" || pathname === "/index.html") {
375
- const htmlPath = path3.join(process.cwd(), ".resuml-dev", "index.html");
376
- if (fs6.existsSync(htmlPath)) {
377
- const html = fs6.readFileSync(htmlPath, "utf8");
378
- const liveReloadScript = `
379
- <script>
380
- setInterval(() => {
381
- fetch('/health').then(() => {
382
- location.reload();
383
- }).catch(() => {
384
- // Server might be restarting
385
- });
386
- }, 1000);
387
- </script>
388
- `;
389
- const modifiedHtml = html.replace("</body>", `${liveReloadScript}</body>`);
390
- res.writeHead(200, { "Content-Type": "text/html" });
391
- res.end(modifiedHtml);
392
- } else {
393
- res.writeHead(404, { "Content-Type": "text/plain" });
394
- res.end("Resume not found. Make sure to provide a valid resume path.");
395
- }
396
- } else if (pathname === "/health") {
397
- res.writeHead(200, { "Content-Type": "application/json" });
398
- res.end('{"status":"ok"}');
399
- } else {
400
- res.writeHead(404, { "Content-Type": "text/plain" });
401
- res.end("Not found");
402
- }
403
- });
404
- server.listen(port, () => {
405
- console.log(chalk2.green(`\u{1F310} Server listening on port ${port}`));
406
- });
407
- }
408
-
409
- // src/commands/init.ts
410
- import fs7 from "fs";
411
- import path4 from "path";
412
- import readline from "readline";
413
- import chalk3 from "chalk";
414
- function createReadlineInterface() {
415
- return readline.createInterface({
416
- input: process.stdin,
417
- output: process.stdout
418
- });
419
- }
420
- function ask(rl, question, defaultValue) {
421
- const prompt = defaultValue ? `${question} (${defaultValue}): ` : `${question}: `;
422
- return new Promise((resolve) => {
423
- rl.question(prompt, (answer) => {
424
- resolve(answer.trim() || defaultValue || "");
425
- });
426
- });
427
- }
428
- async function initAction(options) {
429
- const outputPath = options.output || "resume.yaml";
430
- const fullPath = path4.resolve(outputPath);
431
- const rl = createReadlineInterface();
432
- try {
433
- if (fs7.existsSync(fullPath)) {
434
- const overwrite = await ask(
435
- rl,
436
- `${chalk3.yellow("\u26A0")} ${outputPath} already exists. Overwrite? (y/N)`,
437
- "N"
438
- );
439
- if (overwrite.toLowerCase() !== "y") {
440
- console.log(chalk3.blue("Aborted. No files were changed."));
441
- return;
442
- }
443
- }
444
- console.log(chalk3.blue("\n\u{1F4DD} Let's set up your resume!\n"));
445
- const name = await ask(rl, "Your full name", "John Doe");
446
- const email = await ask(rl, "Email address", "john@example.com");
447
- const label = await ask(rl, "Professional title/label", "Software Engineer");
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
- }
471
- console.log(chalk3.green(`
472
- \u2705 Created ${outputPath}`));
473
- console.log(chalk3.blue("\nNext steps:"));
474
- console.log(` 1. Edit ${outputPath} to fill in your details`);
475
- console.log(" 2. Run " + chalk3.cyan("resuml validate --resume " + outputPath));
476
- console.log(
477
- " 3. Run " + chalk3.cyan("resuml render --resume " + outputPath + " --theme stackoverflow")
478
- );
479
- } finally {
480
- rl.close();
481
- }
482
- }
483
-
484
- // src/commands/pdf.ts
485
- import fs8 from "fs";
486
- import path5 from "path";
487
- import chalk4 from "chalk";
488
- async function loadPlaywright() {
489
- try {
490
- const { chromium } = await import("playwright");
491
- return chromium;
492
- } catch {
493
- throw new Error(
494
- `Playwright is required for PDF export but is not installed.
495
- Install it with: ${chalk4.cyan("npm install playwright")}`
496
- );
497
- }
498
- }
499
- function parseMargin(margin) {
500
- const defaultMargin = { top: "10mm", right: "10mm", bottom: "10mm", left: "10mm" };
501
- if (!margin) return defaultMargin;
502
- const parts = margin.split(",").map((s) => s.trim());
503
- if (parts.length === 1 && parts[0]) {
504
- return { top: parts[0], right: parts[0], bottom: parts[0], left: parts[0] };
505
- }
506
- if (parts.length === 2 && parts[0] && parts[1]) {
507
- return { top: parts[0], right: parts[1], bottom: parts[0], left: parts[1] };
508
- }
509
- if (parts.length === 4 && parts[0] && parts[1] && parts[2] && parts[3]) {
510
- return { top: parts[0], right: parts[1], bottom: parts[2], left: parts[3] };
511
- }
512
- return defaultMargin;
513
- }
514
- async function pdfAction(options) {
515
- if (!options.theme) {
516
- throw new Error(
517
- "--theme option is required. Please specify a theme name (e.g., stackoverflow, react)."
518
- );
519
- }
520
- console.log(chalk4.blue("Starting resuml PDF export..."));
521
- try {
522
- const inputPath = options.resume;
523
- const { yamlContents } = await loadResumeFiles(inputPath);
524
- console.log(chalk4.blue("Processing and validating resume data..."));
525
- const resumeData = await processResumeData(yamlContents);
526
- console.log(chalk4.green("Resume data processing and validation successful!"));
527
- const theme = loadTheme(options.theme);
528
- const htmlOutput = await theme.render(resumeData, {
529
- locale: options.language
530
- });
531
- console.log(chalk4.blue("Loading Playwright..."));
532
- const chromium = await loadPlaywright();
533
- const outputPath = options.output || "resume.pdf";
534
- const format = options.format === "Letter" ? "Letter" : "A4";
535
- const margin = parseMargin(options.margin);
536
- console.log(chalk4.blue(`Generating PDF (${format} format)...`));
537
- const browser = await chromium.launch({ headless: true });
538
- try {
539
- const page = await browser.newPage();
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
- }
565
- const pdfBuffer = await page.pdf({
566
- format,
567
- margin,
568
- printBackground: true,
569
- preferCSSPageSize: true
570
- });
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
- }
581
- console.log(chalk4.green(`\u2705 Successfully generated ${outputPath}`));
582
- } finally {
583
- await browser.close();
584
- }
585
- } catch (error) {
586
- handleCommandError(error, "pdf", options.debug);
587
- }
588
- }
589
-
590
- // src/commands/themes.ts
591
- import chalk5 from "chalk";
592
- import { execSync } from "child_process";
593
- function listThemes() {
594
- console.log(chalk5.blue("\n\u{1F4E6} Compatible JSON Resume Themes\n"));
595
- const nameWidth = 16;
596
- const pkgWidth = 38;
597
- console.log(
598
- ` ${"Status".padEnd(10)}${"Name".padEnd(nameWidth)}${"Package".padEnd(pkgWidth)}Description`
599
- );
600
- console.log(
601
- ` ${"\u2500".repeat(10)}${"\u2500".repeat(nameWidth)}${"\u2500".repeat(pkgWidth)}${"\u2500".repeat(30)}`
602
- );
603
- for (const theme of KNOWN_THEMES) {
604
- const installed = isThemeInstalled(theme.pkg);
605
- const version = installed ? getInstalledVersion(theme.pkg) : null;
606
- const status = installed ? chalk5.green(`\u2713 ${version || "yes"}`.padEnd(10)) : chalk5.yellow("not installed".substring(0, 10).padEnd(10));
607
- console.log(
608
- ` ${status}${theme.name.padEnd(nameWidth)}${chalk5.blue(theme.pkg.padEnd(pkgWidth))}${theme.description}`
609
- );
610
- }
611
- console.log(chalk5.blue("\nInstall a theme:"));
612
- console.log(` ${chalk5.cyan("resuml themes --install <name>")}`);
613
- console.log(` ${chalk5.cyan("resuml themes --install stackoverflow")}
614
- `);
615
- console.log(
616
- chalk5.blue("Browse all themes: ") + "https://www.npmjs.com/search?q=jsonresume-theme\n"
617
- );
618
- }
619
- function installTheme(name) {
620
- const known = KNOWN_THEMES.find((t) => t.name === name);
621
- const pkg = known ? known.pkg : name.startsWith("jsonresume-theme-") ? name : `jsonresume-theme-${name}`;
622
- console.log(chalk5.blue(`
623
- \u{1F4E6} Installing ${pkg}...
624
- `));
625
- try {
626
- execSync(`npm install ${pkg}`, { stdio: "inherit" });
627
- console.log(chalk5.green(`
628
- \u2705 Successfully installed ${pkg}`));
629
- console.log(
630
- chalk5.blue(`
631
- Use it with: ${chalk5.cyan(`resuml render --theme ${known?.name || name}`)}
632
- `)
633
- );
634
- } catch {
635
- console.error(chalk5.red(`
636
- \u274C Failed to install ${pkg}`));
637
- console.error(
638
- chalk5.yellow(`Make sure the package exists: https://www.npmjs.com/package/${pkg}
639
- `)
640
- );
641
- }
642
- }
643
- function themesAction(options) {
644
- if (options.install) {
645
- installTheme(options.install);
646
- } else {
647
- listThemes();
648
- }
649
- }
650
-
651
- // src/commands/mcp.ts
652
- async function mcpAction() {
653
- const { startMcpServer } = await import("./mcp/server.js");
654
- await startMcpServer();
655
- }
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
-
683
- // src/utils/themeRender.ts
684
- var themeRender_exports = {};
685
- __export(themeRender_exports, {
686
- injectCss: () => injectCss,
687
- renderTheme: () => renderTheme
688
- });
689
- async function renderTheme(themeName, resumeData, themeConfig = {}, inlineCss, language = "en") {
690
- try {
691
- if (themeName.startsWith("jsonresume-")) {
692
- return await renderJsonResumeTheme(themeName, resumeData, inlineCss);
693
- } else {
694
- return await renderRyamlTheme(themeName, resumeData, themeConfig, inlineCss, language);
695
- }
696
- } catch (error) {
697
- if (error instanceof Error) {
698
- throw new Error(`Error rendering theme: ${error.message}`);
699
- }
700
- throw new Error("Unknown error rendering theme");
701
- }
702
- }
703
- async function renderJsonResumeTheme(themeName, resumeData, inlineCss) {
704
- let themePackageName;
705
- if (themeName.startsWith("jsonresume-theme-")) {
706
- themePackageName = themeName;
707
- } else if (themeName.startsWith("jsonresume-")) {
708
- themePackageName = `jsonresume-theme-${themeName.replace("jsonresume-", "")}`;
709
- } else {
710
- themePackageName = `jsonresume-theme-${themeName}`;
711
- }
712
- const jsonResumeData = resumeData;
713
- const themePackage = await import(themePackageName);
714
- const renderedHTML = themePackage.default.render(jsonResumeData);
715
- return {
716
- htmlOutput: injectCss(renderedHTML, inlineCss)
717
- };
718
- }
719
- async function renderRyamlTheme(themeName, resumeData, themeConfig, inlineCss, language = "en") {
720
- if (themeName === "default") {
721
- throw new Error("No default theme available. Please specify a specific theme name.");
722
- }
723
- const themePackageName = `@resuml/theme-${themeName}`;
724
- const themePackage = await import(themePackageName);
725
- const renderedHTML = themePackage.default.render(resumeData, themeConfig, language);
726
- return {
727
- htmlOutput: injectCss(renderedHTML, inlineCss)
728
- };
729
- }
730
- function injectCss(html, css) {
731
- if (!css) return html;
732
- return html.replace("</head>", `<style>${css}</style></head>`);
733
- }
734
-
735
- // src/index.ts
736
- var currentDir = path6.dirname(fileURLToPath(import.meta.url));
737
- function getCliVersion() {
738
- const packageJsonPath = path6.resolve(currentDir, "../package.json");
739
- if (fs9.existsSync(packageJsonPath)) {
740
- try {
741
- const packageJson = JSON.parse(fs9.readFileSync(packageJsonPath, "utf8"));
742
- return packageJson.version ?? "0.0.0";
743
- } catch {
744
- return "0.0.0";
745
- }
746
- }
747
- return "0.0.0";
748
- }
749
- var program = new Command();
750
- program.name("resuml").description("CLI tool for managing resuml resume files.").version(getCliVersion());
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);
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);
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);
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);
758
- program.command("init").description("Scaffold a starter resume.yaml file with all sections.").option("-o, --output <file>", "Output YAML file path.", "resume.yaml").action(initAction);
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);
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);
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
- });
769
- if (process.env["NODE_ENV"] !== "test") {
770
- void (async () => {
771
- try {
772
- await program.parseAsync(process.argv);
773
- } catch (e) {
774
- console.error("Command line error:", e.message);
775
- process.exit(1);
776
- }
777
- })();
778
- }
9
+ } from "./chunk-G4AN2EMI.js";
10
+ import {
11
+ analyzeAts
12
+ } from "./chunk-N55EPZ2N.js";
13
+ import "./chunk-QR77BRMN.js";
779
14
  export {
780
15
  analyzeAts,
781
16
  loadResumeFiles,