jobless 1.3.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 +46 -22
- package/package.json +3 -2
- package/src/cli.js +266 -52
- 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 +481 -12
- package/src/reject.js +114 -12
- package/src/screens.js +250 -50
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,61 +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, with no number
|
|
39
|
-
no-fake-salary
|
|
40
37
|
error too many interview rounds
|
|
41
38
|
max-rounds
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
L3 Six interview rounds.
|
|
40
|
+
|
|
41
|
+
RESULT REJECTED
|
|
42
|
+
28/100
|
|
43
|
+
FIT overlap Senior · Remote
|
|
44
|
+
```
|
|
45
|
+
|
|
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.
|
|
47
|
+
|
|
48
|
+
### GitHub Action
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
Copy [`examples/github-action.yml`](examples/github-action.yml):
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
- run: npx --yes jobless lint --format=github jobs/
|
|
47
54
|
```
|
|
48
55
|
|
|
49
|
-
|
|
56
|
+
### GitHub profile README
|
|
57
|
+
|
|
58
|
+
```markdown
|
|
59
|
+
[](https://www.npmjs.com/package/jobless)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx jobless
|
|
64
|
+
```
|
|
50
65
|
|
|
51
66
|
## Hire
|
|
52
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
|
+
|
|
53
70
|
- Email: [yonalem21@gmail.com](mailto:yonalem21@gmail.com?subject=npx%20jobless%20%E2%80%94%20Yonas%20Alem)
|
|
54
71
|
- Site: [yonasalem.vercel.app](https://yonasalem.vercel.app)
|
|
55
72
|
- GitHub: [github.com/Yonas21](https://github.com/Yonas21)
|
|
56
73
|
- LinkedIn: [linkedin.com/in/yonasalem21](https://www.linkedin.com/in/yonasalem21/)
|
|
57
74
|
- Source: [github.com/Yonas21/jobless](https://github.com/Yonas21/jobless)
|
|
58
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
|
+
|
|
59
78
|
## Commands
|
|
60
79
|
|
|
61
80
|
```bash
|
|
62
81
|
npx jobless # reject you, then the resume
|
|
63
|
-
npx jobless lint file.txt # lint a job post
|
|
64
|
-
npx jobless
|
|
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)
|
|
85
|
+
npx jobless lint --json # findings + rejection letter
|
|
86
|
+
npx jobless work # three case studies
|
|
65
87
|
npx jobless hire # email + links
|
|
66
88
|
npx jobless rant # the market
|
|
67
89
|
npx jobless --resume # skip the rejection
|
|
68
90
|
```
|
|
69
91
|
|
|
92
|
+
Post the command, not the punchline: `npx jobless`
|
|
93
|
+
|
|
70
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,44 +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) => {
|
|
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
|
-
printLintReport(lintJobPost(text), source);
|
|
183
|
-
blank();
|
|
184
|
-
print(dim(' more: npx jobless · npx jobless hire'));
|
|
185
|
-
};
|
|
186
|
-
|
|
187
353
|
export const run = async (argv = []) => {
|
|
188
354
|
process.stdout.on('error', (error) => {
|
|
189
355
|
if (error.code === 'EPIPE') process.exit(0);
|
|
190
356
|
});
|
|
191
357
|
|
|
192
|
-
const { flags, rest } = parse(argv);
|
|
358
|
+
const { flags, opts, rest } = parse(argv);
|
|
359
|
+
applyColorFlags(flags, opts);
|
|
360
|
+
|
|
193
361
|
const verb = rest[0] ? rest[0].toLowerCase() : '';
|
|
194
362
|
|
|
195
363
|
if (flags.has('-h') || flags.has('--help') || verb === 'help') {
|
|
@@ -202,20 +370,66 @@ export const run = async (argv = []) => {
|
|
|
202
370
|
return;
|
|
203
371
|
}
|
|
204
372
|
|
|
205
|
-
if (flags.has('--
|
|
206
|
-
|
|
373
|
+
if (verb === 'rules' || flags.has('--rules')) {
|
|
374
|
+
printRules();
|
|
207
375
|
return;
|
|
208
376
|
}
|
|
209
377
|
|
|
210
378
|
if (verb === 'lint' || verb === 'l') {
|
|
211
|
-
|
|
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'));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (flags.has('--json')) {
|
|
424
|
+
print(toJson());
|
|
212
425
|
return;
|
|
213
426
|
}
|
|
214
427
|
|
|
215
428
|
const skipTrap = flags.has('--resume') || flags.has('--skip-trap') || flags.has('--no-trap');
|
|
429
|
+
const seed = opts.seed;
|
|
216
430
|
const command = verb ? resolveCommand(verb) : null;
|
|
217
431
|
|
|
218
|
-
if (command && command !== 'quit' && command !== 'help' && command !== 'lint') {
|
|
432
|
+
if (command && command !== 'quit' && command !== 'help' && command !== 'lint' && command !== 'copy') {
|
|
219
433
|
printHeader();
|
|
220
434
|
runCommand(command);
|
|
221
435
|
blank();
|
|
@@ -224,11 +438,11 @@ export const run = async (argv = []) => {
|
|
|
224
438
|
}
|
|
225
439
|
|
|
226
440
|
if (!process.stdout.isTTY) {
|
|
227
|
-
if (!skipTrap) await printTrap({ animate: false });
|
|
441
|
+
if (!skipTrap) await printTrap({ animate: false, seed });
|
|
228
442
|
blank();
|
|
229
443
|
printHeader();
|
|
230
444
|
printSignature();
|
|
231
|
-
printHire({ copy: false });
|
|
445
|
+
printHire({ copy: false, report: lastReport });
|
|
232
446
|
return;
|
|
233
447
|
}
|
|
234
448
|
|
|
@@ -238,5 +452,5 @@ export const run = async (argv = []) => {
|
|
|
238
452
|
process.exit(0);
|
|
239
453
|
});
|
|
240
454
|
|
|
241
|
-
await interactive({ trap: !skipTrap });
|
|
455
|
+
await interactive({ trap: !skipTrap, seed });
|
|
242
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
|
+
};
|