jobless 1.4.0 → 1.5.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 +41 -29
- package/package.json +3 -2
- package/src/cli.js +264 -58
- package/src/clip.js +18 -0
- package/src/data.js +200 -53
- package/src/diff.js +103 -0
- package/src/fit.js +41 -0
- package/src/lint.js +443 -18
- package/src/reject.js +89 -18
- package/src/screens.js +241 -43
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# jobless
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/jobless)
|
|
4
|
+
[](https://www.npmjs.com/package/jobless)
|
|
5
|
+
|
|
3
6
|
```bash
|
|
4
7
|
npx jobless
|
|
5
8
|
```
|
|
@@ -10,73 +13,82 @@ First it rejects **you**. Then it introduces an engineer.
|
|
|
10
13
|
|
|
11
14
|
## The trap
|
|
12
15
|
|
|
13
|
-
You run the command. It pretends to be Greenhouse. It reads a couple of harmless local hints (git name, email, laptop user). It writes a rejection letter addressed to you.
|
|
14
|
-
|
|
15
|
-
Then it breaks character:
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
that was not a company.
|
|
19
|
-
that was the market, in your own terminal.
|
|
20
|
-
|
|
21
|
-
anyway. here is an engineer who debugs silent production failures.
|
|
22
|
-
```
|
|
16
|
+
You run the command. It pretends to be Greenhouse, Lever, Ashby, or Workday. It reads a couple of harmless local hints (git name, email, laptop user). It writes a rejection letter addressed to you, with a fake ATS score and a pile of applicants.
|
|
23
17
|
|
|
24
|
-
Then
|
|
18
|
+
Then it breaks character and offers `l` — paste the posting that just rejected you — or `h` to hire.
|
|
25
19
|
|
|
26
|
-
Skip the theater with `npx jobless --resume`.
|
|
20
|
+
Skip the theater with `npx jobless --resume`. Record it with `--seed=42` and `--no-color`.
|
|
27
21
|
|
|
28
22
|
## Lint the posting, not the human
|
|
29
23
|
|
|
30
24
|
```bash
|
|
31
25
|
npx jobless lint posting.txt
|
|
26
|
+
npx jobless lint jobs/
|
|
32
27
|
pbpaste | npx jobless lint
|
|
28
|
+
npx jobless lint --fix posting.txt
|
|
29
|
+
npx jobless lint --format=github jobs/
|
|
30
|
+
npx jobless lint --tweet posting.txt
|
|
31
|
+
npx jobless lint --rules
|
|
33
32
|
```
|
|
34
33
|
|
|
35
|
-
Or run `npx jobless` and press `l`.
|
|
34
|
+
Or run `npx jobless` and press `l`. `c` copies the last rejection letter.
|
|
36
35
|
|
|
37
36
|
```
|
|
38
|
-
error salary described as competitive / attractive / market-rate, with no number
|
|
39
|
-
no-fake-salary
|
|
40
37
|
error too many interview rounds
|
|
41
38
|
max-rounds
|
|
42
|
-
|
|
43
|
-
means-understaffed
|
|
39
|
+
L3 Six interview rounds.
|
|
44
40
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
RESULT REJECTED
|
|
42
|
+
28/100
|
|
43
|
+
FIT overlap Senior · Remote
|
|
44
|
+
```
|
|
48
45
|
|
|
49
|
-
|
|
46
|
+
Errors fail the process (`exit 1`). `--fix` prints a unified diff and does not write files unless you pass `--write`. `--format=github` emits `::error` annotations. `jobless-disable-next-line no-fake-salary` skips a line.
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
next we will keep your posting on file. we will not.
|
|
48
|
+
### GitHub Action
|
|
49
|
+
|
|
50
|
+
Copy [`examples/github-action.yml`](examples/github-action.yml):
|
|
55
51
|
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
```yaml
|
|
53
|
+
- run: npx --yes jobless lint --format=github jobs/
|
|
58
54
|
```
|
|
59
55
|
|
|
60
|
-
|
|
56
|
+
### GitHub profile README
|
|
57
|
+
|
|
58
|
+
```markdown
|
|
59
|
+
[](https://www.npmjs.com/package/jobless)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx jobless
|
|
64
|
+
```
|
|
61
65
|
|
|
62
66
|
## Hire
|
|
63
67
|
|
|
68
|
+
The numbers have owners. Infra and performance: Mereb. Payment volume: client rails at Eaglelion, not the ledger. `npx jobless work` is three case studies. `npx jobless proof` is the 17-cron write-up. Production code is private; the walkthrough is the interview.
|
|
69
|
+
|
|
64
70
|
- Email: [yonalem21@gmail.com](mailto:yonalem21@gmail.com?subject=npx%20jobless%20%E2%80%94%20Yonas%20Alem)
|
|
65
71
|
- Site: [yonasalem.vercel.app](https://yonasalem.vercel.app)
|
|
66
72
|
- GitHub: [github.com/Yonas21](https://github.com/Yonas21)
|
|
67
73
|
- LinkedIn: [linkedin.com/in/yonasalem21](https://www.linkedin.com/in/yonasalem21/)
|
|
68
74
|
- Source: [github.com/Yonas21/jobless](https://github.com/Yonas21/jobless)
|
|
69
75
|
|
|
76
|
+
`npx jobless card` is the one-screen version. `npx jobless proof` is the 17-cron write-up. `npx jobless vcf` is a vCard. `npx jobless why` is twelve lines. `npx jobless hire --to=manager` or `--to=recruiter`. `npx jobless tweet` is 280 characters with no punchline.
|
|
77
|
+
|
|
70
78
|
## Commands
|
|
71
79
|
|
|
72
80
|
```bash
|
|
73
81
|
npx jobless # reject you, then the resume
|
|
74
82
|
npx jobless lint file.txt # lint a job post (exit 1 on errors)
|
|
83
|
+
npx jobless lint jobs/ # lint a directory of postings
|
|
84
|
+
npx jobless lint --fix # unified diff (dry-run)
|
|
75
85
|
npx jobless lint --json # findings + rejection letter
|
|
76
|
-
npx jobless work #
|
|
86
|
+
npx jobless work # three case studies
|
|
77
87
|
npx jobless hire # email + links
|
|
78
88
|
npx jobless rant # the market
|
|
79
89
|
npx jobless --resume # skip the rejection
|
|
80
90
|
```
|
|
81
91
|
|
|
92
|
+
Post the command, not the punchline: `npx jobless`
|
|
93
|
+
|
|
82
94
|
Zero dependencies. Node 18+. Local only. It does not apply to jobs or send email.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jobless",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.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",
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"work": "node bin/jobless.js work",
|
|
48
48
|
"rant": "node bin/jobless.js rant",
|
|
49
49
|
"hire": "node bin/jobless.js hire",
|
|
50
|
-
"lint": "node bin/jobless.js lint"
|
|
50
|
+
"lint": "node bin/jobless.js lint",
|
|
51
|
+
"test": "node test/lint.test.js"
|
|
51
52
|
}
|
|
52
53
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { stdin } from 'node:process';
|
|
6
6
|
|
|
7
7
|
import { menu } from './data.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { unifiedDiff } from './diff.js';
|
|
9
|
+
import { fixJobPost, githubAnnotations, lintResult } from './lint.js';
|
|
10
|
+
import { letterText, printTrap } from './reject.js';
|
|
10
11
|
import {
|
|
11
12
|
blank,
|
|
12
13
|
clear,
|
|
13
14
|
dim,
|
|
15
|
+
green,
|
|
14
16
|
print,
|
|
15
17
|
showCursor,
|
|
16
18
|
} from './style.js';
|
|
17
19
|
import {
|
|
20
|
+
copyLetter,
|
|
21
|
+
printCard,
|
|
22
|
+
printFix,
|
|
18
23
|
printHeader,
|
|
19
24
|
printHelp,
|
|
20
25
|
printHire,
|
|
@@ -22,10 +27,15 @@ import {
|
|
|
22
27
|
printLintReport,
|
|
23
28
|
printLooking,
|
|
24
29
|
printMenu,
|
|
30
|
+
printProof,
|
|
25
31
|
printRant,
|
|
32
|
+
printRules,
|
|
26
33
|
printSignature,
|
|
27
34
|
printSkills,
|
|
35
|
+
printTweet,
|
|
36
|
+
printVcf,
|
|
28
37
|
printWho,
|
|
38
|
+
printWhy,
|
|
29
39
|
printWork,
|
|
30
40
|
toJson,
|
|
31
41
|
} from './screens.js';
|
|
@@ -34,6 +44,9 @@ const pkg = JSON.parse(
|
|
|
34
44
|
readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8'),
|
|
35
45
|
);
|
|
36
46
|
|
|
47
|
+
let lastReport = null;
|
|
48
|
+
let lastTo = '';
|
|
49
|
+
|
|
37
50
|
const COMMANDS = {
|
|
38
51
|
who: printWho,
|
|
39
52
|
about: printWho,
|
|
@@ -42,15 +55,21 @@ const COMMANDS = {
|
|
|
42
55
|
rant: printRant,
|
|
43
56
|
market: printRant,
|
|
44
57
|
work: printWork,
|
|
45
|
-
proof:
|
|
58
|
+
proof: printProof,
|
|
46
59
|
experience: printWork,
|
|
47
60
|
skills: printSkills,
|
|
48
|
-
hire: printHire,
|
|
49
|
-
h: printHire,
|
|
50
|
-
links: printHire,
|
|
51
|
-
contact: printHire,
|
|
61
|
+
hire: () => printHire({ report: lastReport, to: lastTo }),
|
|
62
|
+
h: () => printHire({ report: lastReport, to: lastTo }),
|
|
63
|
+
links: () => printHire({ report: lastReport, to: lastTo }),
|
|
64
|
+
contact: () => printHire({ report: lastReport, to: lastTo }),
|
|
65
|
+
card: printCard,
|
|
66
|
+
tweet: () => printTweet(lastReport),
|
|
67
|
+
why: printWhy,
|
|
68
|
+
vcf: printVcf,
|
|
52
69
|
};
|
|
53
70
|
|
|
71
|
+
const VALUE_FLAGS = new Set(['--max-warnings', '--seed', '--off', '--format', '--to']);
|
|
72
|
+
|
|
54
73
|
const ask = (rl, prompt) =>
|
|
55
74
|
new Promise((resolve) => {
|
|
56
75
|
rl.question(prompt, resolve);
|
|
@@ -81,6 +100,12 @@ const resolveCommand = (input) => {
|
|
|
81
100
|
if (value === 'q' || value === 'quit' || value === 'exit') return 'quit';
|
|
82
101
|
if (value === 'help') return 'help';
|
|
83
102
|
if (value === 'lint' || value === 'l') return 'lint';
|
|
103
|
+
if (value === 'c' || value === 'copy') return 'copy';
|
|
104
|
+
if (value === 'tweet') return 'tweet';
|
|
105
|
+
if (value === 'card') return 'card';
|
|
106
|
+
if (value === 'proof') return 'proof';
|
|
107
|
+
if (value === 'why') return 'why';
|
|
108
|
+
if (value === 'vcf' || value === 'vcard') return 'vcf';
|
|
84
109
|
const fromMenu = menu.find((item) => item.key === value || item.command === value);
|
|
85
110
|
if (fromMenu) return fromMenu.command;
|
|
86
111
|
if (COMMANDS[value]) return value;
|
|
@@ -101,10 +126,173 @@ const showResume = () => {
|
|
|
101
126
|
printMenu();
|
|
102
127
|
};
|
|
103
128
|
|
|
104
|
-
const
|
|
129
|
+
const parse = (argv) => {
|
|
130
|
+
const flags = new Set();
|
|
131
|
+
const opts = {};
|
|
132
|
+
const rest = [];
|
|
133
|
+
|
|
134
|
+
const setOpt = (key, value) => {
|
|
135
|
+
opts[key] = opts[key] ? `${opts[key]},${value}` : value;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
139
|
+
const arg = argv[i];
|
|
140
|
+
if (arg === '--') {
|
|
141
|
+
rest.push(...argv.slice(i + 1));
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
145
|
+
const eq = arg.indexOf('=');
|
|
146
|
+
const key = arg.slice(2, eq);
|
|
147
|
+
flags.add(`--${key}`);
|
|
148
|
+
setOpt(key, arg.slice(eq + 1));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
152
|
+
flags.add(arg);
|
|
153
|
+
const next = argv[i + 1];
|
|
154
|
+
if (next && !next.startsWith('-')) {
|
|
155
|
+
setOpt(arg.slice(2), next);
|
|
156
|
+
i += 1;
|
|
157
|
+
} else if (arg === '--max-warnings') {
|
|
158
|
+
setOpt('max-warnings', '0');
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (arg.startsWith('-')) {
|
|
163
|
+
flags.add(arg);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
rest.push(arg);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { flags, opts, rest };
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const applyColorFlags = (flags, opts) => {
|
|
173
|
+
if (flags.has('--no-color') || opts.color === 'never') process.env.NO_COLOR = '1';
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const POSTING_RE = /\.(md|txt|markdown)$/i;
|
|
177
|
+
|
|
178
|
+
const walkPostings = (dir, acc = []) => {
|
|
179
|
+
for (const name of readdirSync(dir).sort()) {
|
|
180
|
+
if (name.startsWith('.')) continue;
|
|
181
|
+
const path = join(dir, name);
|
|
182
|
+
let st;
|
|
183
|
+
try {
|
|
184
|
+
st = statSync(path);
|
|
185
|
+
} catch {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (st.isDirectory()) walkPostings(path, acc);
|
|
189
|
+
else if (POSTING_RE.test(name)) acc.push(path);
|
|
190
|
+
}
|
|
191
|
+
return acc;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const collectPostings = (paths) => {
|
|
195
|
+
const files = [];
|
|
196
|
+
for (const path of paths) {
|
|
197
|
+
const st = statSync(path);
|
|
198
|
+
if (st.isDirectory()) walkPostings(path, files);
|
|
199
|
+
else files.push(path);
|
|
200
|
+
}
|
|
201
|
+
return files;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const resolveFormat = (flags, opts) => {
|
|
205
|
+
if (opts.format) return opts.format;
|
|
206
|
+
if (flags.has('--json')) return 'json';
|
|
207
|
+
return 'pretty';
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const lintOne = (text, source, { off, fix, write }) => {
|
|
211
|
+
const report = lintResult(text, source, { off });
|
|
212
|
+
if (fix || write) {
|
|
213
|
+
const fixed = fixJobPost(text, { off });
|
|
214
|
+
report.fix = {
|
|
215
|
+
applied: fixed.applied,
|
|
216
|
+
text: fixed.text,
|
|
217
|
+
path: source,
|
|
218
|
+
before: text,
|
|
219
|
+
diff: unifiedDiff(text, fixed.text, source === 'stdin' || source === 'paste' ? 'posting.txt' : source),
|
|
220
|
+
};
|
|
221
|
+
if (write && source !== 'stdin' && source !== 'paste' && fixed.applied.length) {
|
|
222
|
+
writeFileSync(source, fixed.text);
|
|
223
|
+
report.fix.wrote = true;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return report;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const runLintCli = async (files, options) => {
|
|
230
|
+
const { format = 'pretty', fix = false, write = false, copy = false, quiet = false, off = [], maxWarnings, tweet = false } = options;
|
|
231
|
+
|
|
232
|
+
const inputs = [];
|
|
233
|
+
if (files[0]) {
|
|
234
|
+
const paths = collectPostings(files);
|
|
235
|
+
if (!paths.length) {
|
|
236
|
+
print(dim(` no .md/.txt postings under ${files.join(', ')}`));
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
for (const path of paths) {
|
|
241
|
+
inputs.push({ source: path, text: readFileSync(path, 'utf8') });
|
|
242
|
+
}
|
|
243
|
+
} else if (!stdin.isTTY) {
|
|
244
|
+
inputs.push({ source: 'stdin', text: await readStdin() });
|
|
245
|
+
} else {
|
|
246
|
+
printHelp();
|
|
247
|
+
blank();
|
|
248
|
+
print(dim(' lint needs a file, a directory, a pipe, or an interactive paste: npx jobless lint jobs/'));
|
|
249
|
+
process.exitCode = 1;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const reports = inputs.map((input) => lintOne(input.text, input.source, { off, fix: fix || write, write }));
|
|
254
|
+
lastReport = reports.at(-1);
|
|
255
|
+
|
|
256
|
+
let failed = false;
|
|
257
|
+
for (const report of reports) {
|
|
258
|
+
if (!report.ok) failed = true;
|
|
259
|
+
if (maxWarnings != null && report.warnings > maxWarnings) failed = true;
|
|
260
|
+
}
|
|
261
|
+
if (failed) process.exitCode = 1;
|
|
262
|
+
|
|
263
|
+
if (format === 'json') {
|
|
264
|
+
print(JSON.stringify(reports.length === 1 ? reports[0] : { reports }, null, 2));
|
|
265
|
+
if (copy && lastReport?.letter) copyLetter(lastReport.letter);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (format === 'github') {
|
|
270
|
+
const body = reports.map((report) => githubAnnotations(report)).filter(Boolean).join('\n');
|
|
271
|
+
if (body) print(body);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const [index, report] of reports.entries()) {
|
|
276
|
+
if (index) blank();
|
|
277
|
+
const copied = copy && report.letter ? copyLetter(report.letter) : false;
|
|
278
|
+
printLintReport(report, { quiet, copied });
|
|
279
|
+
if (report.fix?.applied.length) printFix(report.fix);
|
|
280
|
+
if (report.fix?.wrote) print(` ${green('wrote')} ${report.source}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (tweet) {
|
|
284
|
+
blank();
|
|
285
|
+
printTweet(lastReport);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
blank();
|
|
289
|
+
print(dim(' more: npx jobless · npx jobless hire'));
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const interactive = async ({ trap, seed }) => {
|
|
105
293
|
if (process.stdout.isTTY) clear();
|
|
106
294
|
if (trap) {
|
|
107
|
-
await printTrap({ animate: true });
|
|
295
|
+
await printTrap({ animate: true, seed });
|
|
108
296
|
blank();
|
|
109
297
|
}
|
|
110
298
|
showResume();
|
|
@@ -116,7 +304,7 @@ const interactive = async ({ trap }) => {
|
|
|
116
304
|
const command = resolveCommand(answer);
|
|
117
305
|
|
|
118
306
|
if (!command) {
|
|
119
|
-
print(` ${dim('unknown. try 1–5, l to lint, h to hire, or q.')}`);
|
|
307
|
+
print(` ${dim('unknown. try 1–5, l to lint, c to copy, h to hire, or q.')}`);
|
|
120
308
|
continue;
|
|
121
309
|
}
|
|
122
310
|
if (command === 'help') {
|
|
@@ -125,11 +313,21 @@ const interactive = async ({ trap }) => {
|
|
|
125
313
|
printMenu();
|
|
126
314
|
continue;
|
|
127
315
|
}
|
|
316
|
+
if (command === 'copy') {
|
|
317
|
+
if (!lastReport?.letter) {
|
|
318
|
+
print(` ${dim('nothing to copy. lint a posting first (l).')}`);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (copyLetter(lastReport.letter)) print(` ${green('copied')} rejection letter`);
|
|
322
|
+
else print(letterText(lastReport.letter));
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
128
325
|
if (command === 'lint') {
|
|
129
326
|
const text = await readPaste(rl);
|
|
130
327
|
if (process.stdout.isTTY) clear();
|
|
131
328
|
printHeader();
|
|
132
|
-
|
|
329
|
+
lastReport = lintResult(text, 'paste');
|
|
330
|
+
printLintReport(lastReport);
|
|
133
331
|
printMenu();
|
|
134
332
|
continue;
|
|
135
333
|
}
|
|
@@ -152,52 +350,14 @@ const interactive = async ({ trap }) => {
|
|
|
152
350
|
}
|
|
153
351
|
};
|
|
154
352
|
|
|
155
|
-
const parse = (argv) => {
|
|
156
|
-
const flags = new Set();
|
|
157
|
-
const rest = [];
|
|
158
|
-
for (const arg of argv) {
|
|
159
|
-
if (arg.startsWith('-')) flags.add(arg);
|
|
160
|
-
else rest.push(arg);
|
|
161
|
-
}
|
|
162
|
-
return { flags, rest };
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
const runLintCli = async (files, { json = false } = {}) => {
|
|
166
|
-
let source = 'stdin';
|
|
167
|
-
let text = '';
|
|
168
|
-
|
|
169
|
-
if (files[0]) {
|
|
170
|
-
source = files[0];
|
|
171
|
-
text = readFileSync(files[0], 'utf8');
|
|
172
|
-
} else if (!stdin.isTTY) {
|
|
173
|
-
text = await readStdin();
|
|
174
|
-
} else {
|
|
175
|
-
printHelp();
|
|
176
|
-
blank();
|
|
177
|
-
print(dim(' lint needs a file, a pipe, or an interactive paste: npx jobless lint posting.txt'));
|
|
178
|
-
process.exitCode = 1;
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
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);
|
|
191
|
-
blank();
|
|
192
|
-
print(dim(' more: npx jobless · npx jobless hire'));
|
|
193
|
-
};
|
|
194
|
-
|
|
195
353
|
export const run = async (argv = []) => {
|
|
196
354
|
process.stdout.on('error', (error) => {
|
|
197
355
|
if (error.code === 'EPIPE') process.exit(0);
|
|
198
356
|
});
|
|
199
357
|
|
|
200
|
-
const { flags, rest } = parse(argv);
|
|
358
|
+
const { flags, opts, rest } = parse(argv);
|
|
359
|
+
applyColorFlags(flags, opts);
|
|
360
|
+
|
|
201
361
|
const verb = rest[0] ? rest[0].toLowerCase() : '';
|
|
202
362
|
|
|
203
363
|
if (flags.has('-h') || flags.has('--help') || verb === 'help') {
|
|
@@ -210,8 +370,53 @@ export const run = async (argv = []) => {
|
|
|
210
370
|
return;
|
|
211
371
|
}
|
|
212
372
|
|
|
373
|
+
if (verb === 'rules' || flags.has('--rules')) {
|
|
374
|
+
printRules();
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
213
378
|
if (verb === 'lint' || verb === 'l') {
|
|
214
|
-
|
|
379
|
+
const maxRaw = opts['max-warnings'];
|
|
380
|
+
const maxWarnings = maxRaw == null || maxRaw === '' ? null : Number(maxRaw);
|
|
381
|
+
await runLintCli(rest.slice(1), {
|
|
382
|
+
format: resolveFormat(flags, opts),
|
|
383
|
+
fix: flags.has('--fix') || flags.has('--diff') || flags.has('--write'),
|
|
384
|
+
write: flags.has('--write'),
|
|
385
|
+
copy: flags.has('--copy'),
|
|
386
|
+
quiet: flags.has('--quiet'),
|
|
387
|
+
tweet: flags.has('--tweet'),
|
|
388
|
+
off: String(opts.off || '')
|
|
389
|
+
.split(',')
|
|
390
|
+
.map((id) => id.trim())
|
|
391
|
+
.filter(Boolean),
|
|
392
|
+
maxWarnings: Number.isFinite(maxWarnings) ? maxWarnings : null,
|
|
393
|
+
});
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (verb === 'tweet') {
|
|
398
|
+
printTweet(lastReport);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (verb === 'why') {
|
|
403
|
+
printHeader();
|
|
404
|
+
printWhy();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (verb === 'vcf' || verb === 'vcard') {
|
|
409
|
+
printHeader();
|
|
410
|
+
printVcf();
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
lastTo = opts.to || '';
|
|
415
|
+
if ((verb === 'hire' || verb === 'h') && lastTo) {
|
|
416
|
+
printHeader();
|
|
417
|
+
printHire({ report: lastReport, to: lastTo, copy: process.stdout.isTTY });
|
|
418
|
+
blank();
|
|
419
|
+
print(dim(' more: npx jobless · npx jobless --help'));
|
|
215
420
|
return;
|
|
216
421
|
}
|
|
217
422
|
|
|
@@ -221,9 +426,10 @@ export const run = async (argv = []) => {
|
|
|
221
426
|
}
|
|
222
427
|
|
|
223
428
|
const skipTrap = flags.has('--resume') || flags.has('--skip-trap') || flags.has('--no-trap');
|
|
429
|
+
const seed = opts.seed;
|
|
224
430
|
const command = verb ? resolveCommand(verb) : null;
|
|
225
431
|
|
|
226
|
-
if (command && command !== 'quit' && command !== 'help' && command !== 'lint') {
|
|
432
|
+
if (command && command !== 'quit' && command !== 'help' && command !== 'lint' && command !== 'copy') {
|
|
227
433
|
printHeader();
|
|
228
434
|
runCommand(command);
|
|
229
435
|
blank();
|
|
@@ -232,11 +438,11 @@ export const run = async (argv = []) => {
|
|
|
232
438
|
}
|
|
233
439
|
|
|
234
440
|
if (!process.stdout.isTTY) {
|
|
235
|
-
if (!skipTrap) await printTrap({ animate: false });
|
|
441
|
+
if (!skipTrap) await printTrap({ animate: false, seed });
|
|
236
442
|
blank();
|
|
237
443
|
printHeader();
|
|
238
444
|
printSignature();
|
|
239
|
-
printHire({ copy: false });
|
|
445
|
+
printHire({ copy: false, report: lastReport });
|
|
240
446
|
return;
|
|
241
447
|
}
|
|
242
448
|
|
|
@@ -246,5 +452,5 @@ export const run = async (argv = []) => {
|
|
|
246
452
|
process.exit(0);
|
|
247
453
|
});
|
|
248
454
|
|
|
249
|
-
await interactive({ trap: !skipTrap });
|
|
455
|
+
await interactive({ trap: !skipTrap, seed });
|
|
250
456
|
};
|
package/src/clip.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
export const copyToClipboard = (text) => {
|
|
4
|
+
try {
|
|
5
|
+
if (process.platform === 'darwin') {
|
|
6
|
+
execFileSync('pbcopy', { input: text });
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
if (process.platform === 'win32') {
|
|
10
|
+
execFileSync('clip', { input: text, windowsHide: true });
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
execFileSync('xclip', ['-selection', 'clipboard'], { input: text });
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|