resuml 1.3.1 → 1.4.1

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/README.md CHANGED
@@ -88,6 +88,7 @@ npm install -g resuml
88
88
  | Command | Description |
89
89
  |---------|-------------|
90
90
  | `validate` | Validate resume data against the JSON Resume schema |
91
+ | `validate --ats` | Run ATS (Applicant Tracking System) compatibility analysis |
91
92
  | `tojson` | Convert YAML resume data to JSON format |
92
93
  | `render` | Render the resume using a specified theme |
93
94
  | `dev` | Start a development server with hot-reload |
@@ -102,6 +103,82 @@ npm install -g resuml
102
103
  | `--port` | `-p` | Port for dev server (default: 3000) |
103
104
  | `--language` | | Language code for localization (default: `en`) |
104
105
  | `--debug` | | Enable debug mode for detailed errors |
106
+ | `--ats` | | Run ATS compatibility analysis (with `validate`) |
107
+ | `--jd` | | Path to job description file for keyword matching (with `--ats`) |
108
+ | `--ats-threshold` | | Minimum ATS score (0-100); exits with code 1 if below |
109
+
110
+ ## ATS Analysis
111
+
112
+ Resumls built-in ATS (Applicant Tracking System) analysis helps ensure your resume passes automated screening. Fully offline and deterministic — no API keys or LLMs required.
113
+
114
+ ### Quick Start
115
+
116
+ ```bash
117
+ # Basic ATS score
118
+ resuml validate --resume resume.yaml --ats
119
+
120
+ # Match against a specific job description
121
+ resuml validate --resume resume.yaml --ats --jd job-description.txt
122
+
123
+ # CI/CD gate: fail if score is below threshold
124
+ resuml validate --resume resume.yaml --ats --ats-threshold 75
125
+
126
+ # Machine-readable JSON output
127
+ resuml validate --resume resume.yaml --ats --format json
128
+ ```
129
+
130
+ ### What It Checks
131
+
132
+ The ATS engine runs 11 deterministic checks across 3 categories:
133
+
134
+ **Contact Information**
135
+ - Complete contact details (name, email, phone, city)
136
+ - LinkedIn profile present
137
+
138
+ **Content Quality**
139
+ - Professional summary (length and presence)
140
+ - Work highlights (minimum 2 per entry)
141
+ - Action verbs (highlights start with strong verbs)
142
+ - Quantified impact (numbers, percentages, metrics in highlights)
143
+ - No first-person pronouns
144
+
145
+ **Resume Structure**
146
+ - Date consistency (no unexplained gaps > 6 months)
147
+ - Skills populated (≥ 3 categories with keywords)
148
+ - Education completeness
149
+ - Essential sections present (basics, work, education, skills)
150
+
151
+ ### Job Description Matching
152
+
153
+ Provide a job description file to get keyword matching:
154
+
155
+ ```bash
156
+ resuml validate --resume resume.yaml --ats --jd job.txt
157
+ ```
158
+
159
+ The engine extracts keywords from the job description using TF-based ranking with stem matching, then compares them against your resume. You get:
160
+ - **Match percentage** — how many JD keywords appear in your resume
161
+ - **Matched keywords** — what you already cover
162
+ - **Missing keywords** — what to consider adding
163
+
164
+ ### Scoring
165
+
166
+ | Score | Rating | Meaning |
167
+ |-------|--------|-------------|
168
+ | 90-100 | Excellent | Resume is well-optimized for ATS |
169
+ | 75-89 | Good | Minor improvements possible |
170
+ | 60-74 | Needs Work | Several issues to address |
171
+ | 0-59 | Poor | Significant improvements needed |
172
+
173
+ When a job description is provided, the final score is 60% generic checks + 40% keyword match.
174
+
175
+ ### Multi-Language Support
176
+
177
+ ATS checks support English and German, with language-specific action verb lists and pronoun detection. Use the `--language` flag:
178
+
179
+ ```bash
180
+ resuml validate --resume lebenslauf.yaml --ats --language de
181
+ ```
105
182
 
106
183
  ## Compatible Themes
107
184
 
@@ -174,6 +251,9 @@ jobs:
174
251
  - run: npm install -g resuml
175
252
  - run: npm install jsonresume-theme-stackoverflow
176
253
 
254
+ # Validate and check ATS score (fails if below 75)
255
+ - run: resuml validate --resume resume.yaml --ats --ats-threshold 75
256
+
177
257
  - run: resuml render --resume resume.yaml --theme stackoverflow --output resume.html
178
258
  - run: resuml tojson --resume resume.yaml --output resume.json
179
259
 
@@ -194,7 +274,8 @@ import {
194
274
  processResumeData,
195
275
  loadResumeFiles,
196
276
  loadTheme,
197
- themeRender
277
+ themeRender,
278
+ analyzeAts
198
279
  } from 'resuml';
199
280
 
200
281
  // Load YAML files
@@ -205,6 +286,17 @@ const resume = await processResumeData(yamlContents);
205
286
  const theme = await loadTheme('stackoverflow');
206
287
  // Render HTML
207
288
  const html = await theme.render(resume, { locale: 'en' });
289
+
290
+ // ATS analysis
291
+ const atsResult = analyzeAts(resume, { language: 'en' });
292
+ console.log(`ATS Score: ${atsResult.score}/100`);
293
+
294
+ // With job description matching
295
+ const jdResult = analyzeAts(resume, {
296
+ language: 'en',
297
+ jobDescription: 'Looking for a senior TypeScript developer...'
298
+ });
299
+ console.log(`Matched keywords: ${jdResult.keywords?.matched.join(', ')}`);
208
300
  ```
209
301
 
210
302
  See the CLI and API for more details.
package/bin/resuml ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-env node */
3
+
4
+ const { existsSync } = require('fs');
5
+ const { join } = require('path');
6
+ const distPath = join(__dirname, '../dist/index.js');
7
+
8
+ function startCli() {
9
+ try {
10
+ if (existsSync(distPath)) {
11
+ require(distPath);
12
+ } else {
13
+ throw new Error('CLI not built. Please run "npm run build" first.');
14
+ }
15
+ } catch (err) {
16
+ console.error('Error starting resuml CLI:', err);
17
+ process.exit(1);
18
+ }
19
+ }
20
+
21
+ startCli();
@@ -38,7 +38,7 @@ var getImportMetaUrl, importMetaUrl;
38
38
  var init_cjs_shims = __esm({
39
39
  "node_modules/tsup/assets/cjs_shims.js"() {
40
40
  "use strict";
41
- getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
41
+ getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.src || new URL("main.js", document.baseURI).href;
42
42
  importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
43
43
  }
44
44
  });
@@ -180,7 +180,7 @@ var require_brace_expansion = __commonJS({
180
180
  var isSequence = isNumericSequence || isAlphaSequence;
181
181
  var isOptions = m.body.indexOf(",") >= 0;
182
182
  if (!isSequence && !isOptions) {
183
- if (m.post.match(/,(?!,).*\}/)) {
183
+ if (m.post.match(/,.*\}/)) {
184
184
  str = m.pre + "{" + m.body + escClose + m.post;
185
185
  return expand2(str);
186
186
  }
@@ -259,7 +259,7 @@ var themeLoader_exports = {};
259
259
  __export(themeLoader_exports, {
260
260
  loadTheme: () => loadTheme
261
261
  });
262
- async function installTheme(packageName) {
262
+ function installTheme(packageName) {
263
263
  try {
264
264
  (0, import_child_process.execFileSync)("npm", ["install", packageName], {
265
265
  stdio: ["inherit", "pipe", "pipe"],
@@ -269,7 +269,7 @@ async function installTheme(packageName) {
269
269
  throw new Error(`Failed to install ${packageName}: ${error.message}`);
270
270
  }
271
271
  }
272
- async function loadTheme(themeName, options) {
272
+ function loadTheme(themeName, options) {
273
273
  let jsonResumeThemeName;
274
274
  let nativeThemeName;
275
275
  const autoInstall = options?.autoInstall !== false;
@@ -290,7 +290,7 @@ Please install the theme package manually.`
290
290
  }
291
291
  console.log(`\u{1F4E6} Theme ${jsonResumeThemeName} not found. Installing...`);
292
292
  try {
293
- await installTheme(jsonResumeThemeName);
293
+ installTheme(jsonResumeThemeName);
294
294
  console.log(`\u2705 Successfully installed ${jsonResumeThemeName}`);
295
295
  return require2(jsonResumeThemeName);
296
296
  } catch (installError) {
@@ -331,7 +331,7 @@ init_cjs_shims();
331
331
  // src/core.ts
332
332
  init_cjs_shims();
333
333
  var import_yaml = require("yaml");
334
- var import_lodash = __toESM(require("lodash.merge"));
334
+ var import_lodash = __toESM(require("lodash.merge"), 1);
335
335
  var import_schema = require("@jsonresume/schema");
336
336
  async function processResumeData(yamlContents) {
337
337
  if (yamlContents.length === 0) {
@@ -370,13 +370,13 @@ async function processResumeData(yamlContents) {
370
370
 
371
371
  // src/utils/loadResume.ts
372
372
  init_cjs_shims();
373
- var import_promises3 = __toESM(require("fs/promises"));
374
- var import_yaml2 = __toESM(require("yaml"));
373
+ var import_promises3 = __toESM(require("fs/promises"), 1);
374
+ var import_yaml2 = __toESM(require("yaml"), 1);
375
375
 
376
376
  // src/utils/fileUtils.ts
377
377
  init_cjs_shims();
378
- var import_promises2 = __toESM(require("fs/promises"));
379
- var import_path = __toESM(require("path"));
378
+ var import_promises2 = __toESM(require("fs/promises"), 1);
379
+ var import_path = __toESM(require("path"), 1);
380
380
 
381
381
  // node_modules/glob/dist/esm/index.js
382
382
  init_cjs_shims();
@@ -1066,7 +1066,7 @@ var path = {
1066
1066
  };
1067
1067
  var sep = defaultPlatform === "win32" ? path.win32.sep : path.posix.sep;
1068
1068
  minimatch.sep = sep;
1069
- var GLOBSTAR = /* @__PURE__ */ Symbol("globstar **");
1069
+ var GLOBSTAR = Symbol("globstar **");
1070
1070
  minimatch.GLOBSTAR = GLOBSTAR;
1071
1071
  var qmark2 = "[^/]";
1072
1072
  var star2 = qmark2 + "*?";
@@ -1771,6 +1771,7 @@ if (typeof AC === "undefined") {
1771
1771
  };
1772
1772
  }
1773
1773
  var shouldWarn = (code) => !warned.has(code);
1774
+ var TYPE = Symbol("type");
1774
1775
  var isPosInt = (n) => n && n === Math.floor(n) && n > 0 && isFinite(n);
1775
1776
  var getUintArray = (max) => !isPosInt(max) ? null : max <= Math.pow(2, 8) ? Uint8Array : max <= Math.pow(2, 16) ? Uint16Array : max <= Math.pow(2, 32) ? Uint32Array : max <= Number.MAX_SAFE_INTEGER ? ZeroArray : null;
1776
1777
  var ZeroArray = class extends Array {
@@ -3115,37 +3116,37 @@ var isStream = (s) => !!s && typeof s === "object" && (s instanceof Minipass ||
3115
3116
  var isReadable = (s) => !!s && typeof s === "object" && s instanceof import_node_events.EventEmitter && typeof s.pipe === "function" && // node core Writable streams have a pipe() method, but it throws
3116
3117
  s.pipe !== import_node_stream.default.Writable.prototype.pipe;
3117
3118
  var isWritable = (s) => !!s && typeof s === "object" && s instanceof import_node_events.EventEmitter && typeof s.write === "function" && typeof s.end === "function";
3118
- var EOF = /* @__PURE__ */ Symbol("EOF");
3119
- var MAYBE_EMIT_END = /* @__PURE__ */ Symbol("maybeEmitEnd");
3120
- var EMITTED_END = /* @__PURE__ */ Symbol("emittedEnd");
3121
- var EMITTING_END = /* @__PURE__ */ Symbol("emittingEnd");
3122
- var EMITTED_ERROR = /* @__PURE__ */ Symbol("emittedError");
3123
- var CLOSED = /* @__PURE__ */ Symbol("closed");
3124
- var READ = /* @__PURE__ */ Symbol("read");
3125
- var FLUSH = /* @__PURE__ */ Symbol("flush");
3126
- var FLUSHCHUNK = /* @__PURE__ */ Symbol("flushChunk");
3127
- var ENCODING = /* @__PURE__ */ Symbol("encoding");
3128
- var DECODER = /* @__PURE__ */ Symbol("decoder");
3129
- var FLOWING = /* @__PURE__ */ Symbol("flowing");
3130
- var PAUSED = /* @__PURE__ */ Symbol("paused");
3131
- var RESUME = /* @__PURE__ */ Symbol("resume");
3132
- var BUFFER = /* @__PURE__ */ Symbol("buffer");
3133
- var PIPES = /* @__PURE__ */ Symbol("pipes");
3134
- var BUFFERLENGTH = /* @__PURE__ */ Symbol("bufferLength");
3135
- var BUFFERPUSH = /* @__PURE__ */ Symbol("bufferPush");
3136
- var BUFFERSHIFT = /* @__PURE__ */ Symbol("bufferShift");
3137
- var OBJECTMODE = /* @__PURE__ */ Symbol("objectMode");
3138
- var DESTROYED = /* @__PURE__ */ Symbol("destroyed");
3139
- var ERROR = /* @__PURE__ */ Symbol("error");
3140
- var EMITDATA = /* @__PURE__ */ Symbol("emitData");
3141
- var EMITEND = /* @__PURE__ */ Symbol("emitEnd");
3142
- var EMITEND2 = /* @__PURE__ */ Symbol("emitEnd2");
3143
- var ASYNC = /* @__PURE__ */ Symbol("async");
3144
- var ABORT = /* @__PURE__ */ Symbol("abort");
3145
- var ABORTED = /* @__PURE__ */ Symbol("aborted");
3146
- var SIGNAL = /* @__PURE__ */ Symbol("signal");
3147
- var DATALISTENERS = /* @__PURE__ */ Symbol("dataListeners");
3148
- var DISCARDED = /* @__PURE__ */ Symbol("discarded");
3119
+ var EOF = Symbol("EOF");
3120
+ var MAYBE_EMIT_END = Symbol("maybeEmitEnd");
3121
+ var EMITTED_END = Symbol("emittedEnd");
3122
+ var EMITTING_END = Symbol("emittingEnd");
3123
+ var EMITTED_ERROR = Symbol("emittedError");
3124
+ var CLOSED = Symbol("closed");
3125
+ var READ = Symbol("read");
3126
+ var FLUSH = Symbol("flush");
3127
+ var FLUSHCHUNK = Symbol("flushChunk");
3128
+ var ENCODING = Symbol("encoding");
3129
+ var DECODER = Symbol("decoder");
3130
+ var FLOWING = Symbol("flowing");
3131
+ var PAUSED = Symbol("paused");
3132
+ var RESUME = Symbol("resume");
3133
+ var BUFFER = Symbol("buffer");
3134
+ var PIPES = Symbol("pipes");
3135
+ var BUFFERLENGTH = Symbol("bufferLength");
3136
+ var BUFFERPUSH = Symbol("bufferPush");
3137
+ var BUFFERSHIFT = Symbol("bufferShift");
3138
+ var OBJECTMODE = Symbol("objectMode");
3139
+ var DESTROYED = Symbol("destroyed");
3140
+ var ERROR = Symbol("error");
3141
+ var EMITDATA = Symbol("emitData");
3142
+ var EMITEND = Symbol("emitEnd");
3143
+ var EMITEND2 = Symbol("emitEnd2");
3144
+ var ASYNC = Symbol("async");
3145
+ var ABORT = Symbol("abort");
3146
+ var ABORTED = Symbol("aborted");
3147
+ var SIGNAL = Symbol("signal");
3148
+ var DATALISTENERS = Symbol("dataListeners");
3149
+ var DISCARDED = Symbol("discarded");
3149
3150
  var defer = (fn) => Promise.resolve().then(fn);
3150
3151
  var nodefer = (fn) => fn();
3151
3152
  var isEndish = (ev) => ev === "end" || ev === "finish" || ev === "prefinish";
@@ -3184,7 +3185,7 @@ var PipeProxyErrors = class extends Pipe {
3184
3185
  }
3185
3186
  constructor(src, dest, opts) {
3186
3187
  super(src, dest, opts);
3187
- this.proxyErrors = (er) => dest.emit("error", er);
3188
+ this.proxyErrors = (er) => this.dest.emit("error", er);
3188
3189
  src.on("error", this.proxyErrors);
3189
3190
  }
3190
3191
  };
@@ -3898,6 +3899,8 @@ var Minipass = class extends import_node_events.EventEmitter {
3898
3899
  return: stop,
3899
3900
  [Symbol.asyncIterator]() {
3900
3901
  return this;
3902
+ },
3903
+ [Symbol.asyncDispose]: async () => {
3901
3904
  }
3902
3905
  };
3903
3906
  }
@@ -3933,6 +3936,8 @@ var Minipass = class extends import_node_events.EventEmitter {
3933
3936
  return: stop,
3934
3937
  [Symbol.iterator]() {
3935
3938
  return this;
3939
+ },
3940
+ [Symbol.dispose]: () => {
3936
3941
  }
3937
3942
  };
3938
3943
  }
@@ -4058,7 +4063,7 @@ var ChildrenCache = class extends LRUCache {
4058
4063
  });
4059
4064
  }
4060
4065
  };
4061
- var setAsCwd = /* @__PURE__ */ Symbol("PathScurry setAsCwd");
4066
+ var setAsCwd = Symbol("PathScurry setAsCwd");
4062
4067
  var PathBase = class {
4063
4068
  /**
4064
4069
  * the basename of this path
@@ -6848,4 +6853,4 @@ async function loadTheme2(themeName) {
6848
6853
  loadTheme,
6849
6854
  processResumeData
6850
6855
  });
6851
- //# sourceMappingURL=api.js.map
6856
+ //# sourceMappingURL=api.cjs.map