jobless 1.3.0 → 1.4.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/README.md CHANGED
@@ -35,18 +35,29 @@ pbpaste | npx jobless lint
35
35
  Or run `npx jobless` and press `l`.
36
36
 
37
37
  ```
38
- error salary described as competitive, with no number
38
+ error salary described as competitive / attractive / market-rate, with no number
39
39
  no-fake-salary
40
40
  error too many interview rounds
41
41
  max-rounds
42
- warn "fast-paced" / "wear many hats"
42
+ warn "fast-paced" / "wear many hats" / "do more with less"
43
43
  means-understaffed
44
44
 
45
- REJECTED 2 errors 1 warning
46
- reason failed the human compiler
45
+ FROM you <you@laptop>
46
+ TO Hiring <no-reply@not-a-human.com>
47
+ SUBJECT Your opening for this role
48
+
49
+ Thank you for your interest in my labor. After careful consideration of your job description, I will not be moving forward.
50
+
51
+ RESULT REJECTED
52
+ reason salary described as competitive / attractive / market-rate, with no number
53
+ also too many interview rounds
54
+ next we will keep your posting on file. we will not.
55
+
56
+ that was not a recruiter.
57
+ that was you, finally answering.
47
58
  ```
48
59
 
49
- Same idea as ESLint. The file is a job description. The company has to pass.
60
+ Same idea as ESLint. The file is a job description. Errors fail the process (`exit 1`). Warnings hold it for review. `npx jobless lint --json` prints the findings and the letter.
50
61
 
51
62
  ## Hire
52
63
 
@@ -60,7 +71,8 @@ Same idea as ESLint. The file is a job description. The company has to pass.
60
71
 
61
72
  ```bash
62
73
  npx jobless # reject you, then the resume
63
- npx jobless lint file.txt # lint a job post
74
+ npx jobless lint file.txt # lint a job post (exit 1 on errors)
75
+ npx jobless lint --json # findings + rejection letter
64
76
  npx jobless work # proof
65
77
  npx jobless hire # email + links
66
78
  npx jobless rant # the market
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jobless",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "npx jobless — first it rejects you. then it lints the job post. then you meet the engineer.",
5
5
  "keywords": [
6
6
  "cli",
package/src/cli.js CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
5
5
  import { stdin } from 'node:process';
6
6
 
7
7
  import { menu } from './data.js';
8
- import { lintJobPost } from './lint.js';
8
+ import { lintResult } from './lint.js';
9
9
  import { printTrap } from './reject.js';
10
10
  import {
11
11
  blank,
@@ -129,7 +129,7 @@ const interactive = async ({ trap }) => {
129
129
  const text = await readPaste(rl);
130
130
  if (process.stdout.isTTY) clear();
131
131
  printHeader();
132
- printLintReport(lintJobPost(text), 'paste');
132
+ printLintReport(lintResult(text, 'paste'));
133
133
  printMenu();
134
134
  continue;
135
135
  }
@@ -162,7 +162,7 @@ const parse = (argv) => {
162
162
  return { flags, rest };
163
163
  };
164
164
 
165
- const runLintCli = async (files) => {
165
+ const runLintCli = async (files, { json = false } = {}) => {
166
166
  let source = 'stdin';
167
167
  let text = '';
168
168
 
@@ -179,7 +179,15 @@ const runLintCli = async (files) => {
179
179
  return;
180
180
  }
181
181
 
182
- printLintReport(lintJobPost(text), source);
182
+ const report = lintResult(text, source);
183
+ if (!report.ok) process.exitCode = 1;
184
+
185
+ if (json) {
186
+ print(JSON.stringify(report, null, 2));
187
+ return;
188
+ }
189
+
190
+ printLintReport(report);
183
191
  blank();
184
192
  print(dim(' more: npx jobless · npx jobless hire'));
185
193
  };
@@ -202,13 +210,13 @@ export const run = async (argv = []) => {
202
210
  return;
203
211
  }
204
212
 
205
- if (flags.has('--json')) {
206
- print(toJson());
213
+ if (verb === 'lint' || verb === 'l') {
214
+ await runLintCli(rest.slice(1), { json: flags.has('--json') });
207
215
  return;
208
216
  }
209
217
 
210
- if (verb === 'lint' || verb === 'l') {
211
- await runLintCli(rest.slice(1));
218
+ if (flags.has('--json')) {
219
+ print(toJson());
212
220
  return;
213
221
  }
214
222
 
package/src/lint.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { detectIdentity } from './identity.js';
2
+
1
3
  const hasMoney = (text) =>
2
4
  /\$|€|£|\bUSD\b|\bEUR\b|\bGBP\b|\b\d{2,3}\s?k\b|\b\d{1,3}(,\d{3})+\b/i.test(text);
3
5
 
@@ -101,3 +103,45 @@ export const summarizeLint = (findings) => {
101
103
  const warnings = findings.filter((item) => item.level === 'warn').length;
102
104
  return { errors, warnings };
103
105
  };
106
+
107
+ export const postingMeta = (text) => {
108
+ const roleMatch = String(text || '').match(
109
+ /\b(?:senior|staff|principal|lead|junior|entry[- ]level)?[ \t]*(?:full[- ]stack|frontend|backend|software|platform)?[ \t]*(?:engineer|developer|designer)\b/i,
110
+ );
111
+ const atMatch = String(text || '').match(
112
+ /\b(?:at|@|join)[ \t]+([A-Z][A-Za-z0-9&.'\-]{1,32}(?:[ \t]+[A-Z][A-Za-z0-9&.'\-]{1,20}){0,2})/,
113
+ );
114
+ return {
115
+ role: roleMatch ? roleMatch[0].replace(/\s+/g, ' ').trim() : 'this role',
116
+ company: atMatch ? atMatch[1].trim() : 'Hiring',
117
+ };
118
+ };
119
+
120
+ export const lintResult = (raw, source = 'stdin') => {
121
+ const { text, findings } = lintJobPost(raw);
122
+ const { errors, warnings } = summarizeLint(findings);
123
+ const identity = detectIdentity();
124
+ const meta = postingMeta(text);
125
+ const errorReasons = findings.filter((item) => item.level === 'error').map((item) => item.message);
126
+ const warnReasons = findings.filter((item) => item.level === 'warn').map((item) => item.message);
127
+ const reasons = (errorReasons.length ? errorReasons : warnReasons).slice(0, 2);
128
+
129
+ return {
130
+ source,
131
+ ok: errors === 0,
132
+ errors,
133
+ warnings,
134
+ findings,
135
+ meta,
136
+ letter:
137
+ findings.length === 0
138
+ ? null
139
+ : {
140
+ from: `${identity.name} <${identity.email}>`,
141
+ to: `${meta.company} <no-reply@not-a-human.com>`,
142
+ subject: `Your opening for ${meta.role}`,
143
+ result: errors > 0 ? 'REJECTED' : 'HELD FOR REVIEW',
144
+ reasons,
145
+ },
146
+ };
147
+ };
package/src/reject.js CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  showCursor,
12
12
  sleep,
13
13
  wrap,
14
+ yellow,
14
15
  } from './style.js';
15
16
 
16
17
  const BOOT = [
@@ -99,3 +100,33 @@ export const printTrap = async ({ animate = true } = {}) => {
99
100
  blank();
100
101
  print(` ${dim('anyway. here is an engineer who debugs silent production failures.')}`);
101
102
  };
103
+
104
+ export const printCompanyReject = (letter) => {
105
+ if (!letter) return;
106
+
107
+ blank();
108
+ print(
109
+ box([
110
+ dim(`FROM ${letter.from}`),
111
+ dim(`TO ${letter.to}`),
112
+ dim(`SUBJECT ${letter.subject}`),
113
+ '',
114
+ ...wrap(
115
+ letter.result === 'REJECTED'
116
+ ? 'Thank you for your interest in my labor. After careful consideration of your job description, I will not be moving forward.'
117
+ : 'Thank you for your interest in my labor. Your posting has been held for review. Fix the warnings and resubmit.',
118
+ inner(),
119
+ ),
120
+ '',
121
+ `${bold(red('RESULT'))} ${bold(letter.result === 'REJECTED' ? red(letter.result) : yellow(letter.result))}`,
122
+ `${dim('reason')} ${letter.reasons[0] || 'failed the human compiler'}`,
123
+ ...(letter.reasons[1] ? [`${dim('also')} ${letter.reasons[1]}`] : []),
124
+ `${dim('elapsed')} 0.40s`,
125
+ `${dim('next')} we will keep your posting on file. we will not.`,
126
+ ]),
127
+ );
128
+
129
+ blank();
130
+ print(` ${dim('that was not a recruiter.')}`);
131
+ print(` ${dim('that was you, finally answering.')}`);
132
+ };
package/src/screens.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { execFileSync } from 'node:child_process';
2
2
 
3
+ import { printCompanyReject } from './reject.js';
4
+
3
5
  import {
4
6
  about,
5
7
  experience,
@@ -136,10 +138,8 @@ export const printRant = () => {
136
138
  print(` ${dim('If you want the work, press')} ${bold('1')}${dim('. If you want to hire, press')} ${bold(green('h'))}${dim('.')}`);
137
139
  };
138
140
 
139
- export const printLintReport = (result, source = 'stdin') => {
140
- const { findings } = result;
141
- const errors = findings.filter((item) => item.level === 'error');
142
- const warnings = findings.filter((item) => item.level === 'warn');
141
+ export const printLintReport = (report) => {
142
+ const { findings, source, errors, warnings, letter, ok } = report;
143
143
 
144
144
  blank();
145
145
  section('JOBLESS LINT');
@@ -147,11 +147,11 @@ export const printLintReport = (result, source = 'stdin') => {
147
147
  print(` ${dim(source)}`);
148
148
  blank();
149
149
 
150
- if (!findings.length) {
150
+ if (ok && !findings.length) {
151
151
  print(` ${green('clean')} this posting might be a real job.`);
152
152
  blank();
153
153
  print(` ${dim('If you still want the engineer who wrote the linter:')} ${bold(green('h'))}`);
154
- return { errors: 0, warnings: 0 };
154
+ return report;
155
155
  }
156
156
 
157
157
  for (const finding of findings) {
@@ -161,11 +161,12 @@ export const printLintReport = (result, source = 'stdin') => {
161
161
  blank();
162
162
  }
163
163
 
164
- const fail = errors.length > 0;
165
- print(` ${fail ? bold(red('REJECTED')) : yellow('WARNING')} ${errors.length} error${errors.length === 1 ? '' : 's'} ${warnings.length} warning${warnings.length === 1 ? '' : 's'}`);
166
- print(` ${dim('reason')} failed the human compiler`);
167
- print(` ${dim('next')} send this report back. or hire the person who wrote it.`);
168
- return { errors: errors.length, warnings: warnings.length };
164
+ printCompanyReject(letter);
165
+ blank();
166
+ print(
167
+ ` ${errors} error${errors === 1 ? '' : 's'} ${warnings} warning${warnings === 1 ? '' : 's'} ${dim('·')} ${dim('hire the person who wrote the compiler:')} ${bold(green('h'))}`,
168
+ );
169
+ return report;
169
170
  };
170
171
 
171
172
  export const printWork = () => {
@@ -246,6 +247,7 @@ export const printHelp = () => {
246
247
  blank();
247
248
  print(` ${bold('npx jobless')} reject you, then the resume`);
248
249
  print(` ${bold('npx jobless lint [file]')} lint a job posting`);
250
+ print(` ${bold('npx jobless lint --json')} findings + rejection letter`);
249
251
  print(` ${bold('npx jobless work')} proof of work`);
250
252
  print(` ${bold('npx jobless hire')} email + links`);
251
253
  print(` ${bold('npx jobless who')} bio`);
@@ -253,7 +255,7 @@ export const printHelp = () => {
253
255
  print(` ${bold('npx jobless skills')} stack`);
254
256
  print(` ${bold('npx jobless rant')} the market`);
255
257
  print(` ${bold('npx jobless --resume')} skip the rejection`);
256
- print(` ${bold('npx jobless --json')} machine readable`);
258
+ print(` ${bold('npx jobless --json')} machine readable resume`);
257
259
  blank();
258
260
  print(` ${dim('In the prompt:')} ${bold(green('h'))} ${dim('hires,')} ${bold('l')} ${dim('lints a posting,')} ${bold('q')} ${dim('quits.')}`);
259
261
  print(` ${dim(profile.website)}`);