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 +18 -6
- package/package.json +1 -1
- package/src/cli.js +16 -8
- package/src/lint.js +44 -0
- package/src/reject.js +31 -0
- package/src/screens.js +14 -12
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
|
-
|
|
46
|
-
|
|
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.
|
|
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
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 {
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
206
|
-
|
|
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 (
|
|
211
|
-
|
|
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 = (
|
|
140
|
-
const { findings } =
|
|
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
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
print(
|
|
167
|
-
|
|
168
|
-
|
|
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)}`);
|