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/DOCS.md +320 -0
- package/README.md +7 -2
- package/data/skills/emerging.json +59 -11
- package/data/skills/skills.json +14964 -1
- package/dist/chunk-R4MD5YMV.js +17434 -0
- package/dist/chunk-R4MD5YMV.js.map +1 -0
- package/dist/index.d.ts +452 -3
- package/dist/index.js +268 -116
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +132 -141
- package/dist/mcp/server.js.map +1 -1
- package/package.json +26 -56
- package/dist/api.d.ts +0 -9
- package/dist/api.js +0 -20
- package/dist/api.js.map +0 -1
- package/dist/chunk-4ZOTZUAW.js +0 -6666
- package/dist/chunk-4ZOTZUAW.js.map +0 -1
- package/dist/chunk-JP7UCR3P.js +0 -182
- package/dist/chunk-JP7UCR3P.js.map +0 -1
- package/dist/chunk-KRJMZ2RQ.js +0 -1621
- package/dist/chunk-KRJMZ2RQ.js.map +0 -1
- package/dist/chunk-ZLA7NFYP.js +0 -90
- package/dist/chunk-ZLA7NFYP.js.map +0 -1
- package/dist/index-yHdKpxms.d.ts +0 -422
- package/dist/themeLoader-ZGWEGYXG.js +0 -7
- package/dist/themeLoader-ZGWEGYXG.js.map +0 -1
- package/scripts/build-builder.js +0 -25
- package/scripts/build-skills-db.js +0 -314
- package/scripts/bundle-themes.js +0 -1118
- package/scripts/dev-server.js +0 -392
- package/scripts/enrich-themes-manifest.mjs +0 -156
- package/scripts/generate-types.cjs +0 -55
- package/scripts/mcp-call.mjs +0 -99
- package/scripts/quick-bundle.cjs +0 -129
- package/scripts/render-theme-thumbs.mjs +0 -117
- package/scripts/test-mcp.mjs +0 -583
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
getRubricEntry,
|
|
9
|
+
isThemeInstalled,
|
|
10
|
+
listRubricMarkdown,
|
|
11
|
+
loadConfig,
|
|
12
|
+
loadTheme,
|
|
13
13
|
processResumeData
|
|
14
|
-
} from "./chunk-
|
|
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
|
|
23
|
-
import
|
|
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
|
|
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
|
|
40
|
-
console.error(`${index + 1}. Path: ${
|
|
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
|
|
51
|
-
console.error(`${index + 1}. Field: ${
|
|
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,
|
|
68
|
-
const scoreColor = result.score >= 75 ?
|
|
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(
|
|
138
|
+
console.log(chalk7.bold("=== ATS Analysis Report ==="));
|
|
71
139
|
console.log("");
|
|
72
|
-
console.log(
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 [
|
|
91
|
-
const label =
|
|
92
|
-
console.log(
|
|
93
|
-
for (const check of checks) {
|
|
94
|
-
if (!debug && check.
|
|
95
|
-
const icon = check.
|
|
96
|
-
const scoreText =
|
|
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
|
-
|
|
99
|
-
console.log(
|
|
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.
|
|
105
|
-
console.log(
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
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(
|
|
172
|
+
console.log(chalk7.dim("==========================="));
|
|
119
173
|
}
|
|
120
174
|
async function validateAction(options) {
|
|
121
|
-
const
|
|
122
|
-
console.log(
|
|
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(
|
|
180
|
+
console.log(chalk7.blue("Validating resume data..."));
|
|
127
181
|
let resumeData;
|
|
128
182
|
try {
|
|
129
183
|
resumeData = await processResumeData(yamlContents);
|
|
130
|
-
console.log(
|
|
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(
|
|
190
|
+
console.log(chalk7.blue("Running ATS analysis..."));
|
|
137
191
|
let jobDescription;
|
|
138
192
|
if (options.jd) {
|
|
139
193
|
try {
|
|
140
|
-
jobDescription =
|
|
194
|
+
jobDescription = fs3.readFileSync(options.jd, "utf8");
|
|
141
195
|
} catch {
|
|
142
|
-
console.error(
|
|
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:
|
|
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,
|
|
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(
|
|
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
|
|
224
|
+
import fs4 from "fs";
|
|
169
225
|
async function toJsonAction(options) {
|
|
170
|
-
const
|
|
171
|
-
console.log(
|
|
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(
|
|
231
|
+
console.log(chalk7.blue("Processing and validating data..."));
|
|
176
232
|
const resumeData = await processResumeData(yamlContents);
|
|
177
|
-
console.log(
|
|
233
|
+
console.log(chalk7.green("Processing and validation successful!"));
|
|
178
234
|
const jsonOutput = JSON.stringify(resumeData, null, 2);
|
|
179
|
-
|
|
180
|
-
console.log(
|
|
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
|
|
188
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
|
243
|
-
import
|
|
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 (
|
|
317
|
+
if (fs6.existsSync(inputPath) && fs6.statSync(inputPath).isDirectory()) {
|
|
262
318
|
watchDirectory(inputPath, () => {
|
|
263
319
|
void renderResume(options);
|
|
264
320
|
});
|
|
265
|
-
} else if (
|
|
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 =
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
361
|
+
fs6.watch(filePath, (eventType) => {
|
|
306
362
|
if (eventType === "change") {
|
|
307
|
-
console.log(chalk2.blue(`\u{1F4C4} File changed: ${
|
|
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 =
|
|
320
|
-
if (
|
|
321
|
-
const html =
|
|
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
|
|
355
|
-
import
|
|
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 =
|
|
430
|
+
const fullPath = path4.resolve(outputPath);
|
|
375
431
|
const rl = createReadlineInterface();
|
|
376
432
|
try {
|
|
377
|
-
if (
|
|
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
|
|
393
|
-
|
|
394
|
-
|
|
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(
|
|
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
|
|
408
|
-
import
|
|
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
|
-
|
|
470
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
736
|
+
var currentDir = path6.dirname(fileURLToPath(import.meta.url));
|
|
595
737
|
function getCliVersion() {
|
|
596
|
-
const packageJsonPath =
|
|
597
|
-
if (
|
|
738
|
+
const packageJsonPath = path6.resolve(currentDir, "../package.json");
|
|
739
|
+
if (fs9.existsSync(packageJsonPath)) {
|
|
598
740
|
try {
|
|
599
|
-
const packageJson = JSON.parse(
|
|
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(
|
|
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 {
|