jobless 1.4.0 → 1.6.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 +115 -49
- package/package.json +4 -3
- package/src/cli.js +107 -202
- package/src/clip.js +18 -0
- package/src/data.js +106 -105
- package/src/diff.js +103 -0
- package/src/fit.js +41 -0
- package/src/hiring.js +70 -0
- package/src/identity.js +5 -1
- package/src/interactive.js +73 -0
- package/src/lint-cli.js +128 -0
- package/src/lint-screens.js +114 -0
- package/src/lint.js +443 -18
- package/src/profile-screens.js +248 -0
- package/src/reject.js +89 -18
- package/src/screens.js +57 -176
- package/src/style.js +16 -8
- package/src/work-data.js +152 -0
package/src/lint-cli.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { stdin } from 'node:process';
|
|
4
|
+
import { unifiedDiff } from './diff.js';
|
|
5
|
+
import { fixJobPost, githubAnnotations, lintResult } from './lint.js';
|
|
6
|
+
import { demoPosting } from './hiring.js';
|
|
7
|
+
import { blank, dim, green, print } from './style.js';
|
|
8
|
+
import { copyLetter, printFix, printHelp, printLintReport, printTweet } from './screens.js';
|
|
9
|
+
|
|
10
|
+
const readStdin = async () => {
|
|
11
|
+
const chunks = [];
|
|
12
|
+
for await (const chunk of stdin) chunks.push(chunk);
|
|
13
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const POSTING_RE = /\.(md|txt|markdown)$/i;
|
|
17
|
+
|
|
18
|
+
const walkPostings = (dir, acc = []) => {
|
|
19
|
+
for (const name of readdirSync(dir).sort()) {
|
|
20
|
+
if (name.startsWith('.')) continue;
|
|
21
|
+
const path = join(dir, name);
|
|
22
|
+
let st;
|
|
23
|
+
try {
|
|
24
|
+
st = statSync(path);
|
|
25
|
+
} catch {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (st.isDirectory()) walkPostings(path, acc);
|
|
29
|
+
else if (POSTING_RE.test(name)) acc.push(path);
|
|
30
|
+
}
|
|
31
|
+
return acc;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const collectPostings = (paths) => {
|
|
35
|
+
const files = [];
|
|
36
|
+
for (const path of paths) {
|
|
37
|
+
const st = statSync(path);
|
|
38
|
+
if (st.isDirectory()) walkPostings(path, files);
|
|
39
|
+
else files.push(path);
|
|
40
|
+
}
|
|
41
|
+
return files;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const lintOne = (text, source, { off, fix, write }) => {
|
|
45
|
+
const report = lintResult(text, source, { off });
|
|
46
|
+
if (fix || write) {
|
|
47
|
+
const fixed = fixJobPost(text, { off });
|
|
48
|
+
report.fix = {
|
|
49
|
+
applied: fixed.applied,
|
|
50
|
+
text: fixed.text,
|
|
51
|
+
path: source,
|
|
52
|
+
before: text,
|
|
53
|
+
diff: unifiedDiff(text, fixed.text, source === 'stdin' || source === 'paste' ? 'posting.txt' : source),
|
|
54
|
+
};
|
|
55
|
+
if (write && source !== 'stdin' && source !== 'paste' && fixed.applied.length) {
|
|
56
|
+
writeFileSync(source, fixed.text);
|
|
57
|
+
report.fix.wrote = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return report;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const runLintCli = async (files, options) => {
|
|
64
|
+
const { format = 'pretty', fix = false, write = false, copy = false, quiet = false, off = [], maxWarnings, tweet = false, demo = false } = options;
|
|
65
|
+
|
|
66
|
+
const inputs = [];
|
|
67
|
+
if (demo) {
|
|
68
|
+
if (files.length || write) throw new Error('--demo cannot be combined with files or --write');
|
|
69
|
+
inputs.push({ source: 'demo', text: demoPosting });
|
|
70
|
+
} else if (files[0]) {
|
|
71
|
+
const paths = collectPostings(files);
|
|
72
|
+
if (!paths.length) {
|
|
73
|
+
print(dim(` no .md/.txt postings under ${files.join(', ')}`));
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const path of paths) {
|
|
78
|
+
inputs.push({ source: path, text: readFileSync(path, 'utf8') });
|
|
79
|
+
}
|
|
80
|
+
} else if (!stdin.isTTY) {
|
|
81
|
+
inputs.push({ source: 'stdin', text: await readStdin() });
|
|
82
|
+
} else {
|
|
83
|
+
printHelp();
|
|
84
|
+
blank();
|
|
85
|
+
print(dim(' lint needs a file, a directory, a pipe, or an interactive paste: npx jobless lint jobs/'));
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const reports = inputs.map((input) => lintOne(input.text, input.source, { off, fix: fix || write, write }));
|
|
91
|
+
const lastReport = reports.at(-1);
|
|
92
|
+
|
|
93
|
+
let failed = false;
|
|
94
|
+
for (const report of reports) {
|
|
95
|
+
if (!report.ok) failed = true;
|
|
96
|
+
if (maxWarnings != null && report.warnings > maxWarnings) failed = true;
|
|
97
|
+
}
|
|
98
|
+
if (failed) process.exitCode = 1;
|
|
99
|
+
|
|
100
|
+
if (format === 'json') {
|
|
101
|
+
print(JSON.stringify(reports.length === 1 ? reports[0] : { reports }, null, 2));
|
|
102
|
+
if (copy && lastReport?.letter) copyLetter(lastReport.letter);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (format === 'github') {
|
|
107
|
+
const body = reports.map((report) => githubAnnotations(report)).filter(Boolean).join('\n');
|
|
108
|
+
if (body) print(body);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const [index, report] of reports.entries()) {
|
|
113
|
+
if (index) blank();
|
|
114
|
+
const copied = copy && report.letter ? copyLetter(report.letter) : false;
|
|
115
|
+
printLintReport(report, { quiet, copied });
|
|
116
|
+
if (report.fix?.applied.length) printFix(report.fix);
|
|
117
|
+
if (report.fix?.wrote) print(` ${green('wrote')} ${report.source}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (tweet) {
|
|
121
|
+
blank();
|
|
122
|
+
printTweet(lastReport);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
blank();
|
|
126
|
+
print(dim(' more: npx jobless · npx jobless hire'));
|
|
127
|
+
};
|
|
128
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { unifiedDiff } from './diff.js';
|
|
2
|
+
import { rules } from './lint.js';
|
|
3
|
+
import { printCompanyReject } from './reject.js';
|
|
4
|
+
import {
|
|
5
|
+
blank,
|
|
6
|
+
bold,
|
|
7
|
+
dim,
|
|
8
|
+
green,
|
|
9
|
+
print,
|
|
10
|
+
red,
|
|
11
|
+
section,
|
|
12
|
+
yellow,
|
|
13
|
+
} from './style.js';
|
|
14
|
+
|
|
15
|
+
export const printLintReport = (report, { quiet = false, copied = false } = {}) => {
|
|
16
|
+
const { findings, source, errors, warnings, letter, ok, score } = report;
|
|
17
|
+
const visible = quiet ? findings.filter((item) => item.level === 'error') : findings;
|
|
18
|
+
|
|
19
|
+
blank();
|
|
20
|
+
section('JOBLESS LINT');
|
|
21
|
+
print(dim(' A rule-based satire score, not an employer rating.'));
|
|
22
|
+
blank();
|
|
23
|
+
print(` ${dim(source)} ${dim('·')} ${bold(`${score}/100`)}`);
|
|
24
|
+
blank();
|
|
25
|
+
|
|
26
|
+
if (ok && !findings.length) {
|
|
27
|
+
print(` ${green('clean')} this posting might be a real job.`);
|
|
28
|
+
if (report.fit) printFit(report.fit, { failed: false });
|
|
29
|
+
blank();
|
|
30
|
+
print(` ${dim('If you still want the engineer who wrote the linter:')} ${bold(green('h'))}`);
|
|
31
|
+
return report;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const finding of visible) {
|
|
35
|
+
const tag = finding.level === 'error' ? red('error') : yellow('warn ');
|
|
36
|
+
print(` ${tag} ${finding.message}`);
|
|
37
|
+
print(` ${dim(finding.id)}`);
|
|
38
|
+
if (finding.line && finding.snippet) {
|
|
39
|
+
print(` ${dim(`L${finding.line}`)} ${finding.snippet}`);
|
|
40
|
+
}
|
|
41
|
+
blank();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
printCompanyReject(letter);
|
|
45
|
+
if (report.fit) printFit(report.fit, { failed: true });
|
|
46
|
+
blank();
|
|
47
|
+
print(
|
|
48
|
+
` ${bold(`${score}/100`)} ${errors} error${errors === 1 ? '' : 's'} ${warnings} warning${warnings === 1 ? '' : 's'} ${dim('·')} ${dim('hire:')} ${bold(green('h'))}${dim(' copy letter:')} ${bold('c')}`,
|
|
49
|
+
);
|
|
50
|
+
if (copied) {
|
|
51
|
+
print(` ${green('copied')} rejection letter`);
|
|
52
|
+
}
|
|
53
|
+
return report;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const printFit = (fit, { failed = false } = {}) => {
|
|
57
|
+
if (!fit) return;
|
|
58
|
+
blank();
|
|
59
|
+
section('FIT');
|
|
60
|
+
blank();
|
|
61
|
+
if (fit.matched.length) print(` ${green('overlap')} ${fit.matched.join(dim(' · '))}`);
|
|
62
|
+
if (fit.missed.length) print(` ${yellow('miss')} ${fit.missed.join(dim(' · '))}`);
|
|
63
|
+
if (fit.extra.length) print(` ${dim('also')} ${fit.extra.join(' · ')}`);
|
|
64
|
+
blank();
|
|
65
|
+
if (fit.score == null) {
|
|
66
|
+
print(` ${dim('The posting did not name a stack. The engineer still exists.')} ${bold(green('h'))}`);
|
|
67
|
+
} else if (failed) {
|
|
68
|
+
print(
|
|
69
|
+
` ${bold(`${fit.score}/100`)} overlap ${dim('·')} ${dim('the posting failed. the engineer still matches:')} ${bold(green('h'))}`,
|
|
70
|
+
);
|
|
71
|
+
} else {
|
|
72
|
+
print(
|
|
73
|
+
` ${bold(`${fit.score}/100`)} overlap ${dim('·')} ${dim('hire the engineer who matches:')} ${bold(green('h'))}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const printFix = (fixed) => {
|
|
79
|
+
blank();
|
|
80
|
+
section('DIFF');
|
|
81
|
+
blank();
|
|
82
|
+
if (!fixed.applied.length) {
|
|
83
|
+
print(` ${dim('nothing to rewrite.')}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
print(` ${dim('applied')} ${fixed.applied.join(' · ')}`);
|
|
87
|
+
blank();
|
|
88
|
+
const diff = fixed.diff || unifiedDiff(fixed.before || '', fixed.text, fixed.path || 'posting.txt');
|
|
89
|
+
if (!diff) {
|
|
90
|
+
print(` ${dim('rewrite produced the same lines.')}`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const line of diff.split('\n')) {
|
|
94
|
+
if (line.startsWith('+') && !line.startsWith('+++')) print(` ${green(line)}`);
|
|
95
|
+
else if (line.startsWith('-') && !line.startsWith('---')) print(` ${red(line)}`);
|
|
96
|
+
else print(` ${dim(line)}`);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const printRules = () => {
|
|
101
|
+
blank();
|
|
102
|
+
section('RULES');
|
|
103
|
+
blank();
|
|
104
|
+
for (const rule of rules) {
|
|
105
|
+
const tag = rule.level === 'error' ? red('error') : yellow('warn ');
|
|
106
|
+
print(` ${tag} ${rule.id}`);
|
|
107
|
+
print(` ${dim(rule.message)}`);
|
|
108
|
+
if (rule.why) print(` ${dim(rule.why)}`);
|
|
109
|
+
blank();
|
|
110
|
+
}
|
|
111
|
+
print(` ${dim('npx jobless lint --off=rockstar-ninja posting.txt')}`);
|
|
112
|
+
print(` ${dim('jobless-disable-next-line no-fake-salary')}`);
|
|
113
|
+
};
|
|
114
|
+
|