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/src/reject.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  bold,
5
5
  box,
6
6
  dim,
7
+ green,
7
8
  hideCursor,
8
9
  inner,
9
10
  print,
@@ -11,8 +12,18 @@ import {
11
12
  showCursor,
12
13
  sleep,
13
14
  wrap,
15
+ yellow,
14
16
  } from './style.js';
15
17
 
18
+ export const ATS = [
19
+ { name: 'Greenhouse', from: 'Greenhouse <no-reply@not-a-human.com>', center: 'Greenhouse Applicant Center' },
20
+ { name: 'Lever', from: 'Lever <no-reply@not-a-human.com>', center: 'Lever Hire' },
21
+ { name: 'Ashby', from: 'Ashby <no-reply@not-a-human.com>', center: 'Ashby Recruiting' },
22
+ { name: 'Workday', from: 'Workday <no-reply@not-a-human.com>', center: 'Workday Recruiting' },
23
+ ];
24
+
25
+ export const pickAts = (seed) => ATS[Math.abs(Number(seed) || 0) % ATS.length];
26
+
16
27
  const BOOT = [
17
28
  'receiving application from the void',
18
29
  'parsing whatever npx just sent',
@@ -27,7 +38,7 @@ const reasonPool = (identity) => {
27
38
  'installed a resume instead of submitting one',
28
39
  `did not mention ${identity.company} in the first 12 words`,
29
40
  'cover letter parsed as "npx"',
30
- 'years of experience with Greenhouse: 0',
41
+ 'years of experience with this ATS: 0',
31
42
  'failed culture add (ran a CLI)',
32
43
  'resume was a person',
33
44
  ];
@@ -40,27 +51,89 @@ const reasonPool = (identity) => {
40
51
  return pool;
41
52
  };
42
53
 
43
- const pickReasons = (identity, count = 2) => {
54
+ export const trapStats = (seed) => ({
55
+ score: 11 + (Math.abs(Number(seed) || 0) % 28),
56
+ pile: 180 + (Math.abs(Number(seed) || 0) % 820),
57
+ });
58
+
59
+ export const pickReasons = (identity, count = 2, seed = seedFrom(identity)) => {
44
60
  const pool = reasonPool(identity);
45
- const seed = seedFrom(identity);
46
61
  const picked = [];
62
+ const numeric = Number(seed) || 0;
47
63
  for (let i = 0; i < count && pool.length; i += 1) {
48
- const index = Math.abs(seed + i * 17) % pool.length;
64
+ const index = Math.abs(numeric + i * 17) % pool.length;
49
65
  picked.push(pool.splice(index, 1)[0]);
50
66
  }
51
67
  return picked;
52
68
  };
53
69
 
54
- export const printTrap = async ({ animate = true } = {}) => {
55
- const identity = detectIdentity();
56
- const reasons = pickReasons(identity);
57
- const to = identity.email || identity.username;
58
- const company = identity.company === 'your team' ? 'this role' : identity.company;
70
+ const letterBody = (letter) =>
71
+ letter.result === 'REJECTED'
72
+ ? 'Thank you for your interest in my labor. After careful consideration of your job description, I will not be moving forward.'
73
+ : 'Thank you for your interest in my labor. Your posting has been held for review. Fix the warnings and resubmit.';
74
+
75
+ export const letterText = (letter) => {
76
+ if (!letter) return '';
77
+ return [
78
+ `FROM ${letter.from}`,
79
+ `TO ${letter.to}`,
80
+ `SUBJECT ${letter.subject}`,
81
+ '',
82
+ letterBody(letter),
83
+ '',
84
+ `RESULT ${letter.result}`,
85
+ `reason ${letter.reasons[0] || 'failed the human compiler'}`,
86
+ ...(letter.reasons[1] ? [`also ${letter.reasons[1]}`] : []),
87
+ 'elapsed 0.40s',
88
+ 'next we will keep your posting on file. we will not.',
89
+ '',
90
+ 'that was not a recruiter.',
91
+ 'that was you, finally answering.',
92
+ ].join('\n');
93
+ };
94
+
95
+ export const trapModel = ({ identity, seed } = {}) => {
96
+ const person = identity || detectIdentity();
97
+ const numericSeed = seed == null || seed === '' ? seedFrom(person) : Number(seed);
98
+ const ats = pickAts(numericSeed);
99
+ const reasons = pickReasons(person, 2, numericSeed);
100
+ const stats = trapStats(numericSeed);
101
+ const to = person.email || person.username;
102
+ const company = person.company === 'your team' ? 'this role' : person.company;
103
+ return { identity: person, seed: numericSeed, ats, reasons, stats, to, company };
104
+ };
105
+
106
+ export const trapText = (model) => {
107
+ const { ats, identity, to, company, reasons, stats } = model;
108
+ return [
109
+ `FROM ${ats.from}`,
110
+ `TO ${identity.name} <${to}>`,
111
+ 'SUBJECT Your application for Senior Engineer',
112
+ '',
113
+ `Thank you for your interest in ${company}. After careful consideration of your application, we will not be moving forward.`,
114
+ '',
115
+ 'RESULT REJECTED',
116
+ `reason ${reasons[0]}`,
117
+ ...(reasons[1] ? [`also ${reasons[1]}`] : []),
118
+ `score ${stats.score}/100`,
119
+ `in pile ${stats.pile} applications`,
120
+ 'elapsed 0.40s',
121
+ 'next we will keep your resume on file. we will not.',
122
+ '',
123
+ 'that was not a company.',
124
+ 'that was the market, in your own terminal.',
125
+ ].join('\n');
126
+ };
127
+
128
+ export const printTrap = async ({ animate = true, seed, identity } = {}) => {
129
+ const model = trapModel({ identity, seed });
130
+ const { ats, reasons, stats, to, company } = model;
131
+ const person = model.identity;
59
132
 
60
133
  if (animate) {
61
134
  hideCursor();
62
135
  try {
63
- print(dim(' Greenhouse Applicant Center'));
136
+ print(dim(` ${ats.center}`));
64
137
  blank();
65
138
  for (const [index, line] of BOOT.entries()) {
66
139
  print(` ${dim(`[${index + 1}/${BOOT.length}]`)} ${line}`);
@@ -76,8 +149,8 @@ export const printTrap = async ({ animate = true } = {}) => {
76
149
 
77
150
  print(
78
151
  box([
79
- dim('FROM Greenhouse <no-reply@not-a-human.com>'),
80
- dim(`TO ${identity.name} <${to}>`),
152
+ dim(`FROM ${ats.from}`),
153
+ dim(`TO ${person.name} <${to}>`),
81
154
  dim(`SUBJECT Your application for Senior Engineer`),
82
155
  '',
83
156
  ...wrap(
@@ -88,6 +161,8 @@ export const printTrap = async ({ animate = true } = {}) => {
88
161
  `${bold(red('RESULT'))} ${bold('REJECTED')}`,
89
162
  `${dim('reason')} ${reasons[0]}`,
90
163
  ...(reasons[1] ? [`${dim('also')} ${reasons[1]}`] : []),
164
+ `${dim('score')} ${stats.score}/100`,
165
+ `${dim('in pile')} ${stats.pile} applications`,
91
166
  `${dim('elapsed')} 0.40s`,
92
167
  `${dim('next')} we will keep your resume on file. we will not.`,
93
168
  ]),
@@ -98,4 +173,31 @@ export const printTrap = async ({ animate = true } = {}) => {
98
173
  print(` ${dim('that was the market, in your own terminal.')}`);
99
174
  blank();
100
175
  print(` ${dim('anyway. here is an engineer who debugs silent production failures.')}`);
176
+ print(` ${dim('the posting that just rejected you:')} ${bold('l')}`);
177
+ print(` ${dim('or skip the theater and hire:')} ${bold(green('h'))}`);
178
+ };
179
+
180
+ export const printCompanyReject = (letter) => {
181
+ if (!letter) return;
182
+
183
+ blank();
184
+ print(
185
+ box([
186
+ dim(`FROM ${letter.from}`),
187
+ dim(`TO ${letter.to}`),
188
+ dim(`SUBJECT ${letter.subject}`),
189
+ '',
190
+ ...wrap(letterBody(letter), inner()),
191
+ '',
192
+ `${bold(red('RESULT'))} ${bold(letter.result === 'REJECTED' ? red(letter.result) : yellow(letter.result))}`,
193
+ `${dim('reason')} ${letter.reasons[0] || 'failed the human compiler'}`,
194
+ ...(letter.reasons[1] ? [`${dim('also')} ${letter.reasons[1]}`] : []),
195
+ `${dim('elapsed')} 0.40s`,
196
+ `${dim('next')} we will keep your posting on file. we will not.`,
197
+ ]),
198
+ );
199
+
200
+ blank();
201
+ print(` ${dim('that was not a recruiter.')}`);
202
+ print(` ${dim('that was you, finally answering.')}`);
101
203
  };
package/src/screens.js CHANGED
@@ -1,18 +1,26 @@
1
- import { execFileSync } from 'node:child_process';
1
+ import { copyToClipboard } from './clip.js';
2
+ import { unifiedDiff } from './diff.js';
3
+ import { tweetFromReport } from './fit.js';
4
+ import { rules } from './lint.js';
5
+ import { letterText, printCompanyReject } from './reject.js';
2
6
 
3
7
  import {
4
8
  about,
9
+ cases,
10
+ claims,
5
11
  experience,
6
12
  hireMailto,
7
13
  lookingFor,
14
+ mailtoFor,
8
15
  menu,
16
+ mentorship,
9
17
  profile,
10
18
  quotes,
11
19
  rants,
12
20
  signature,
13
21
  skills,
14
22
  stats,
15
- work,
23
+ vcard,
16
24
  } from './data.js';
17
25
  import {
18
26
  banner,
@@ -35,23 +43,6 @@ import {
35
43
  yellow,
36
44
  } from './style.js';
37
45
 
38
- const copyToClipboard = (text) => {
39
- try {
40
- if (process.platform === 'darwin') {
41
- execFileSync('pbcopy', { input: text });
42
- return true;
43
- }
44
- if (process.platform === 'win32') {
45
- execFileSync('clip', { input: text, windowsHide: true });
46
- return true;
47
- }
48
- execFileSync('xclip', ['-selection', 'clipboard'], { input: text });
49
- return true;
50
- } catch {
51
- return false;
52
- }
53
- };
54
-
55
46
  export const printHeader = () => {
56
47
  print(banner());
57
48
  blank();
@@ -84,7 +75,7 @@ export const printSignature = () => {
84
75
  export const printHints = () => {
85
76
  blank();
86
77
  print(
87
- ` ${bold(green('h'))} hire ${dim('·')} ${bold('1')} proof ${dim('·')} ${bold('l')} lint a posting ${dim('·')} ${bold('q')} quit`,
78
+ ` ${bold(green('h'))} hire ${dim('·')} ${bold('1')} proof ${dim('·')} ${bold('l')} lint a posting ${dim('·')} ${bold('c')} copy letter ${dim('·')} ${bold('q')} quit`,
88
79
  );
89
80
  };
90
81
 
@@ -99,6 +90,15 @@ export const printWho = () => {
99
90
  kv('Now', profile.currently);
100
91
  kv('Study', profile.study);
101
92
  kv('Site', link(profile.website, profile.website));
93
+ blank();
94
+ print(` ${dim('numbers, with owners')}`);
95
+ blank();
96
+ for (const claim of claims) {
97
+ print(` ${bold(claim.value)} ${dim(claim.label)}`);
98
+ print(` ${dim(claim.who)}`);
99
+ for (const line of wrap(claim.method)) print(` ${line}`);
100
+ blank();
101
+ }
102
102
  };
103
103
 
104
104
  export const printLooking = () => {
@@ -136,40 +136,217 @@ export const printRant = () => {
136
136
  print(` ${dim('If you want the work, press')} ${bold('1')}${dim('. If you want to hire, press')} ${bold(green('h'))}${dim('.')}`);
137
137
  };
138
138
 
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');
139
+ export const printLintReport = (report, { quiet = false, copied = false } = {}) => {
140
+ const { findings, source, errors, warnings, letter, ok, score } = report;
141
+ const visible = quiet ? findings.filter((item) => item.level === 'error') : findings;
143
142
 
144
143
  blank();
145
144
  section('JOBLESS LINT');
146
145
  blank();
147
- print(` ${dim(source)}`);
146
+ print(` ${dim(source)} ${dim('·')} ${bold(`${score}/100`)}`);
148
147
  blank();
149
148
 
150
- if (!findings.length) {
149
+ if (ok && !findings.length) {
151
150
  print(` ${green('clean')} this posting might be a real job.`);
151
+ if (report.fit) printFit(report.fit, { failed: false });
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
- for (const finding of findings) {
157
+ for (const finding of visible) {
158
158
  const tag = finding.level === 'error' ? red('error') : yellow('warn ');
159
159
  print(` ${tag} ${finding.message}`);
160
160
  print(` ${dim(finding.id)}`);
161
+ if (finding.line && finding.snippet) {
162
+ print(` ${dim(`L${finding.line}`)} ${finding.snippet}`);
163
+ }
161
164
  blank();
162
165
  }
163
166
 
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 };
167
+ printCompanyReject(letter);
168
+ if (report.fit) printFit(report.fit, { failed: true });
169
+ blank();
170
+ print(
171
+ ` ${bold(`${score}/100`)} ${errors} error${errors === 1 ? '' : 's'} ${warnings} warning${warnings === 1 ? '' : 's'} ${dim('·')} ${dim('hire:')} ${bold(green('h'))}${dim(' copy letter:')} ${bold('c')}`,
172
+ );
173
+ if (copied) {
174
+ print(` ${green('copied')} rejection letter`);
175
+ }
176
+ return report;
177
+ };
178
+
179
+ export const printFit = (fit, { failed = false } = {}) => {
180
+ if (!fit) return;
181
+ blank();
182
+ section('FIT');
183
+ blank();
184
+ if (fit.matched.length) print(` ${green('overlap')} ${fit.matched.join(dim(' · '))}`);
185
+ if (fit.missed.length) print(` ${yellow('miss')} ${fit.missed.join(dim(' · '))}`);
186
+ if (fit.extra.length) print(` ${dim('also')} ${fit.extra.join(' · ')}`);
187
+ blank();
188
+ if (fit.score == null) {
189
+ print(` ${dim('The posting did not name a stack. The engineer still exists.')} ${bold(green('h'))}`);
190
+ } else if (failed) {
191
+ print(
192
+ ` ${bold(`${fit.score}/100`)} overlap ${dim('·')} ${dim('the posting failed. the engineer still matches:')} ${bold(green('h'))}`,
193
+ );
194
+ } else {
195
+ print(
196
+ ` ${bold(`${fit.score}/100`)} overlap ${dim('·')} ${dim('hire the engineer who matches:')} ${bold(green('h'))}`,
197
+ );
198
+ }
199
+ };
200
+
201
+ export const printFix = (fixed) => {
202
+ blank();
203
+ section('DIFF');
204
+ blank();
205
+ if (!fixed.applied.length) {
206
+ print(` ${dim('nothing to rewrite.')}`);
207
+ return;
208
+ }
209
+ print(` ${dim('applied')} ${fixed.applied.join(' · ')}`);
210
+ blank();
211
+ const diff = fixed.diff || unifiedDiff(fixed.before || '', fixed.text, fixed.path || 'posting.txt');
212
+ if (!diff) {
213
+ print(` ${dim('rewrite produced the same lines.')}`);
214
+ return;
215
+ }
216
+ for (const line of diff.split('\n')) {
217
+ if (line.startsWith('+') && !line.startsWith('+++')) print(` ${green(line)}`);
218
+ else if (line.startsWith('-') && !line.startsWith('---')) print(` ${red(line)}`);
219
+ else print(` ${dim(line)}`);
220
+ }
221
+ };
222
+
223
+ export const printRules = () => {
224
+ blank();
225
+ section('RULES');
226
+ blank();
227
+ for (const rule of rules) {
228
+ const tag = rule.level === 'error' ? red('error') : yellow('warn ');
229
+ print(` ${tag} ${rule.id}`);
230
+ print(` ${dim(rule.message)}`);
231
+ if (rule.why) print(` ${dim(rule.why)}`);
232
+ blank();
233
+ }
234
+ print(` ${dim('npx jobless lint --off=rockstar-ninja posting.txt')}`);
235
+ print(` ${dim('jobless-disable-next-line no-fake-salary')}`);
236
+ };
237
+
238
+ export const printCard = () => {
239
+ blank();
240
+ print(` ${bold(signature.title)}`);
241
+ print(` ${dim(signature.lines.at(-1))}`);
242
+ blank();
243
+ kv('Email', link(profile.email, hireMailto));
244
+ kv('Site', link(profile.website, profile.website));
245
+ kv('GitHub', link(profile.github, profile.github));
246
+ blank();
247
+ print(` ${dim('Remote senior / full-stack. Press')} ${bold(green('h'))} ${dim('or email from here.')}`);
248
+ };
249
+
250
+ const printCase = (item, { deep = false } = {}) => {
251
+ print(` ${bold(item.title)}`);
252
+ print(` ${cyan(item.company)} ${dim(`${item.role} · ${item.kind}`)}`);
253
+ blank();
254
+ print(` ${dim('problem')}`);
255
+ for (const line of wrap(item.problem)) print(` ${line}`);
256
+ if (deep && item.trace) {
257
+ blank();
258
+ print(` ${dim('trace')}`);
259
+ for (const step of item.trace) bullet(step);
260
+ }
261
+ blank();
262
+ print(` ${dim('change')}`);
263
+ for (const line of wrap(item.change)) print(` ${line}`);
264
+ blank();
265
+ print(` ${dim('result')}`);
266
+ for (const line of wrap(item.result)) print(` ${line}`);
267
+ if (deep && item.still) {
268
+ blank();
269
+ print(` ${dim('still open')}`);
270
+ for (const line of wrap(item.still)) print(` ${line}`);
271
+ }
272
+ if (deep && item.undo) {
273
+ blank();
274
+ print(` ${dim('what I would undo')}`);
275
+ for (const line of wrap(item.undo)) print(` ${line}`);
276
+ }
277
+ if (deep && item.ask) {
278
+ blank();
279
+ print(` ${dim('ask me on the call')}`);
280
+ for (const question of item.ask) bullet(question);
281
+ }
282
+ if (item.url) {
283
+ blank();
284
+ print(` ${link(item.url.replace('https://', ''), item.url)}`);
285
+ }
286
+ print(` ${dim(item.stack.join(' · '))}`);
287
+ };
288
+
289
+ export const printProof = () => {
290
+ blank();
291
+ section('PROOF');
292
+ blank();
293
+ printCase(cases[0], { deep: true });
294
+ blank();
295
+ print(` ${dim('The other two case studies are')} ${bold('1')}${dim('. Hire is')} ${bold(green('h'))}${dim('.')}`);
296
+ };
297
+
298
+ export const printTweet = (report) => {
299
+ blank();
300
+ print(tweetFromReport(report));
301
+ blank();
302
+ print(` ${dim('Post the command, not the punchline.')}`);
303
+ };
304
+
305
+ export const printWhy = () => {
306
+ blank();
307
+ section('WHY');
308
+ blank();
309
+ for (const line of [
310
+ 'The market parses humans in 400ms and calls it hiring.',
311
+ 'jobless parses you first, then parses the job post.',
312
+ 'It is a reverse ATS: the company has to pass.',
313
+ 'Then it shows the engineer who debugs silent production failures.',
314
+ 'Local only. No apply. No email sent. No scrape.',
315
+ 'If the joke lands, press h.',
316
+ ]) {
317
+ bullet(line);
318
+ }
319
+ blank();
320
+ print(` ${dim('Hire is')} ${bold(green('h'))}${dim('.')}`);
321
+ };
322
+
323
+ export const printVcf = ({ copy = true } = {}) => {
324
+ const card = vcard();
325
+ blank();
326
+ section('VCARD');
327
+ blank();
328
+ for (const line of card.trim().split('\n')) print(` ${line}`);
329
+ blank();
330
+ if (copy && process.stdout.isTTY && copyToClipboard(card)) {
331
+ print(` ${green('copied')} vCard`);
332
+ }
169
333
  };
170
334
 
171
335
  export const printWork = () => {
172
- printSignature();
336
+ blank();
337
+ section('CASE STUDIES');
338
+ blank();
339
+ print(` ${dim('Private production systems. I will walk through them on a call.')}`);
340
+ for (const item of cases) {
341
+ blank();
342
+ printCase(item, { deep: true });
343
+ }
344
+ blank();
345
+ print(` ${dim('mentorship')}`);
346
+ blank();
347
+ print(` ${bold(mentorship.who)} ${dim(mentorship.where)}`);
348
+ print(` ${mentorship.on.join(dim(' · '))}`);
349
+ for (const line of wrap(mentorship.still)) print(` ${line}`);
173
350
  blank();
174
351
  print(` ${dim('experience')}`);
175
352
  blank();
@@ -181,13 +358,6 @@ export const printWork = () => {
181
358
  print(` ${dim(job.stack.join(' · '))}`);
182
359
  blank();
183
360
  }
184
- print(` ${dim('selected work')}`);
185
- blank();
186
- for (const item of work) {
187
- print(` ${bold(item.title)} ${dim(item.company)}`);
188
- for (const line of wrap(item.blurb)) print(` ${line}`);
189
- blank();
190
- }
191
361
  print(` ${dim('people who were not bots')}`);
192
362
  blank();
193
363
  for (const quote of quotes) {
@@ -202,29 +372,35 @@ export const printSkills = () => {
202
372
  section('STACK');
203
373
  blank();
204
374
  for (const group of skills) {
205
- print(` ${bold(group.name)}`);
375
+ print(` ${bold(group.name)} ${dim(group.note)}`);
206
376
  print(` ${group.items.join(dim(' · '))}`);
207
377
  blank();
208
378
  }
209
379
  };
210
380
 
211
- export const printHire = ({ copy = true } = {}) => {
381
+ export const printHire = ({ copy = true, report, to } = {}) => {
382
+ const mailto = mailtoFor({ report, to });
212
383
  blank();
213
384
  section('HIRE ME');
214
385
  blank();
215
386
  print(` ${dim('Skip the 47-field form. This is the whole application.')}`);
216
387
  blank();
217
- kv('Email', link(profile.email, hireMailto));
388
+ kv('Email', link(profile.email, mailto));
218
389
  kv('Phone', link(profile.phone, `tel:${profile.phone.replace(/\s+/g, '')}`));
219
390
  kv('Site', link(profile.website, profile.website));
220
391
  kv('GitHub', link(profile.github, profile.github));
221
392
  kv('LinkedIn', link(profile.linkedin, profile.linkedin));
222
393
  kv('Resume', link(profile.resume, profile.resume));
223
394
  blank();
395
+ if (report?.letter) {
396
+ print(` ${dim('last lint')} ${bold(report.letter.result)} ${dim(`${report.score}/100`)}`);
397
+ blank();
398
+ }
399
+ if (report?.fit) printFit(report.fit, { failed: !report.ok });
224
400
  if (copy && process.stdout.isTTY && copyToClipboard(profile.email)) {
225
401
  print(` ${green('copied')} ${profile.email} ${dim('to clipboard')}`);
226
402
  }
227
- print(` ${dim('or')} ${link('open a prefilled email', hireMailto)}`);
403
+ print(` ${dim('or')} ${link('open a prefilled email', mailto)}`);
228
404
  blank();
229
405
  print(` ${dim('Remote senior / full-stack. I can start a conversation this week.')}`);
230
406
  };
@@ -245,23 +421,44 @@ export const printHelp = () => {
245
421
  print(banner());
246
422
  blank();
247
423
  print(` ${bold('npx jobless')} reject you, then the resume`);
248
- print(` ${bold('npx jobless lint [file]')} lint a job posting`);
249
- print(` ${bold('npx jobless work')} proof of work`);
424
+ print(` ${bold('npx jobless lint [file]')} lint a job posting or jobs/`);
425
+ print(` ${bold('npx jobless lint --json')} findings + rejection letter`);
426
+ print(` ${bold('npx jobless lint --fix')} unified diff (dry-run)`);
427
+ print(` ${bold('npx jobless lint --write')} write the rewrite to the file`);
428
+ print(` ${bold('npx jobless lint --copy')} copy the company letter`);
429
+ print(` ${bold('npx jobless lint --rules')} print the rule table`);
430
+ print(` ${bold('npx jobless lint --format=github')} CI annotations`);
431
+ print(` ${bold('npx jobless work')} three case studies + timeline`);
432
+ print(` ${bold('npx jobless proof')} the 17-cron write-up`);
433
+ print(` ${bold('npx jobless card')} one-screen hire card`);
250
434
  print(` ${bold('npx jobless hire')} email + links`);
435
+ print(` ${bold('npx jobless hire --to=manager')} hiring-manager subject`);
436
+ print(` ${bold('npx jobless vcf')} vCard`);
437
+ print(` ${bold('npx jobless why')} why this exists`);
438
+ print(` ${bold('npx jobless tweet')} 280 characters, no punchline`);
251
439
  print(` ${bold('npx jobless who')} bio`);
252
440
  print(` ${bold('npx jobless looking')} roles`);
253
441
  print(` ${bold('npx jobless skills')} stack`);
254
442
  print(` ${bold('npx jobless rant')} the market`);
255
443
  print(` ${bold('npx jobless --resume')} skip the rejection`);
256
- print(` ${bold('npx jobless --json')} machine readable`);
444
+ print(` ${bold('npx jobless --seed=42')} same rejection every time`);
445
+ print(` ${bold('npx jobless --json')} machine readable resume`);
446
+ print(` ${bold('npx jobless --no-color')} recording-friendly`);
257
447
  blank();
258
- print(` ${dim('In the prompt:')} ${bold(green('h'))} ${dim('hires,')} ${bold('l')} ${dim('lints a posting,')} ${bold('q')} ${dim('quits.')}`);
448
+ print(` ${dim('In the prompt:')} ${bold(green('h'))} ${dim('hires,')} ${bold('l')} ${dim('lints,')} ${bold('c')} ${dim('copies the last letter,')} ${bold('q')} ${dim('quits.')}`);
449
+ print(` ${dim('Post the command, not the punchline:')} ${bold('npx jobless')}`);
259
450
  print(` ${dim(profile.website)}`);
260
451
  };
261
452
 
262
453
  export const printLandingTeaser = () => {
263
454
  blank();
264
- print(` ${dim('Paste a posting with')} ${bold('l')}${dim('. The rant is')} ${bold('5')}${dim('.')}`);
455
+ print(` ${dim('Paste the posting that just rejected you:')} ${bold('l')}${dim('.')}`);
456
+ };
457
+
458
+ export const copyLetter = (letter) => {
459
+ const text = letterText(letter);
460
+ if (!text) return false;
461
+ return copyToClipboard(text);
265
462
  };
266
463
 
267
464
  export const toJson = () =>
@@ -284,6 +481,9 @@ export const toJson = () =>
284
481
  lookingFor,
285
482
  rants,
286
483
  stats,
484
+ claims,
485
+ cases,
486
+ mentorship,
287
487
  experience,
288
488
  skills,
289
489
  },