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/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,
@@ -14,6 +15,15 @@ import {
14
15
  yellow,
15
16
  } from './style.js';
16
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
+
17
27
  const BOOT = [
18
28
  'receiving application from the void',
19
29
  'parsing whatever npx just sent',
@@ -28,7 +38,7 @@ const reasonPool = (identity) => {
28
38
  'installed a resume instead of submitting one',
29
39
  `did not mention ${identity.company} in the first 12 words`,
30
40
  'cover letter parsed as "npx"',
31
- 'years of experience with Greenhouse: 0',
41
+ 'years of experience with this ATS: 0',
32
42
  'failed culture add (ran a CLI)',
33
43
  'resume was a person',
34
44
  ];
@@ -41,27 +51,89 @@ const reasonPool = (identity) => {
41
51
  return pool;
42
52
  };
43
53
 
44
- 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)) => {
45
60
  const pool = reasonPool(identity);
46
- const seed = seedFrom(identity);
47
61
  const picked = [];
62
+ const numeric = Number(seed) || 0;
48
63
  for (let i = 0; i < count && pool.length; i += 1) {
49
- const index = Math.abs(seed + i * 17) % pool.length;
64
+ const index = Math.abs(numeric + i * 17) % pool.length;
50
65
  picked.push(pool.splice(index, 1)[0]);
51
66
  }
52
67
  return picked;
53
68
  };
54
69
 
55
- export const printTrap = async ({ animate = true } = {}) => {
56
- const identity = detectIdentity();
57
- const reasons = pickReasons(identity);
58
- const to = identity.email || identity.username;
59
- 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;
60
132
 
61
133
  if (animate) {
62
134
  hideCursor();
63
135
  try {
64
- print(dim(' Greenhouse Applicant Center'));
136
+ print(dim(` ${ats.center}`));
65
137
  blank();
66
138
  for (const [index, line] of BOOT.entries()) {
67
139
  print(` ${dim(`[${index + 1}/${BOOT.length}]`)} ${line}`);
@@ -77,8 +149,8 @@ export const printTrap = async ({ animate = true } = {}) => {
77
149
 
78
150
  print(
79
151
  box([
80
- dim('FROM Greenhouse <no-reply@not-a-human.com>'),
81
- dim(`TO ${identity.name} <${to}>`),
152
+ dim(`FROM ${ats.from}`),
153
+ dim(`TO ${person.name} <${to}>`),
82
154
  dim(`SUBJECT Your application for Senior Engineer`),
83
155
  '',
84
156
  ...wrap(
@@ -89,6 +161,8 @@ export const printTrap = async ({ animate = true } = {}) => {
89
161
  `${bold(red('RESULT'))} ${bold('REJECTED')}`,
90
162
  `${dim('reason')} ${reasons[0]}`,
91
163
  ...(reasons[1] ? [`${dim('also')} ${reasons[1]}`] : []),
164
+ `${dim('score')} ${stats.score}/100`,
165
+ `${dim('in pile')} ${stats.pile} applications`,
92
166
  `${dim('elapsed')} 0.40s`,
93
167
  `${dim('next')} we will keep your resume on file. we will not.`,
94
168
  ]),
@@ -99,6 +173,8 @@ export const printTrap = async ({ animate = true } = {}) => {
99
173
  print(` ${dim('that was the market, in your own terminal.')}`);
100
174
  blank();
101
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'))}`);
102
178
  };
103
179
 
104
180
  export const printCompanyReject = (letter) => {
@@ -111,12 +187,7 @@ export const printCompanyReject = (letter) => {
111
187
  dim(`TO ${letter.to}`),
112
188
  dim(`SUBJECT ${letter.subject}`),
113
189
  '',
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
- ),
190
+ ...wrap(letterBody(letter), inner()),
120
191
  '',
121
192
  `${bold(red('RESULT'))} ${bold(letter.result === 'REJECTED' ? red(letter.result) : yellow(letter.result))}`,
122
193
  `${dim('reason')} ${letter.reasons[0] || 'failed the human compiler'}`,
package/src/screens.js CHANGED
@@ -1,20 +1,26 @@
1
- import { execFileSync } from 'node:child_process';
2
-
3
- import { printCompanyReject } from './reject.js';
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';
4
6
 
5
7
  import {
6
8
  about,
9
+ cases,
10
+ claims,
7
11
  experience,
8
12
  hireMailto,
9
13
  lookingFor,
14
+ mailtoFor,
10
15
  menu,
16
+ mentorship,
11
17
  profile,
12
18
  quotes,
13
19
  rants,
14
20
  signature,
15
21
  skills,
16
22
  stats,
17
- work,
23
+ vcard,
18
24
  } from './data.js';
19
25
  import {
20
26
  banner,
@@ -37,23 +43,6 @@ import {
37
43
  yellow,
38
44
  } from './style.js';
39
45
 
40
- const copyToClipboard = (text) => {
41
- try {
42
- if (process.platform === 'darwin') {
43
- execFileSync('pbcopy', { input: text });
44
- return true;
45
- }
46
- if (process.platform === 'win32') {
47
- execFileSync('clip', { input: text, windowsHide: true });
48
- return true;
49
- }
50
- execFileSync('xclip', ['-selection', 'clipboard'], { input: text });
51
- return true;
52
- } catch {
53
- return false;
54
- }
55
- };
56
-
57
46
  export const printHeader = () => {
58
47
  print(banner());
59
48
  blank();
@@ -86,7 +75,7 @@ export const printSignature = () => {
86
75
  export const printHints = () => {
87
76
  blank();
88
77
  print(
89
- ` ${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`,
90
79
  );
91
80
  };
92
81
 
@@ -101,6 +90,15 @@ export const printWho = () => {
101
90
  kv('Now', profile.currently);
102
91
  kv('Study', profile.study);
103
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
+ }
104
102
  };
105
103
 
106
104
  export const printLooking = () => {
@@ -138,39 +136,217 @@ export const printRant = () => {
138
136
  print(` ${dim('If you want the work, press')} ${bold('1')}${dim('. If you want to hire, press')} ${bold(green('h'))}${dim('.')}`);
139
137
  };
140
138
 
141
- export const printLintReport = (report) => {
142
- const { findings, source, errors, warnings, letter, ok } = report;
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
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
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
167
  printCompanyReject(letter);
168
+ if (report.fit) printFit(report.fit, { failed: true });
165
169
  blank();
166
170
  print(
167
- ` ${errors} error${errors === 1 ? '' : 's'} ${warnings} warning${warnings === 1 ? '' : 's'} ${dim('·')} ${dim('hire the person who wrote the compiler:')} ${bold(green('h'))}`,
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')}`,
168
172
  );
173
+ if (copied) {
174
+ print(` ${green('copied')} rejection letter`);
175
+ }
169
176
  return report;
170
177
  };
171
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
+ }
333
+ };
334
+
172
335
  export const printWork = () => {
173
- 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}`);
174
350
  blank();
175
351
  print(` ${dim('experience')}`);
176
352
  blank();
@@ -182,13 +358,6 @@ export const printWork = () => {
182
358
  print(` ${dim(job.stack.join(' · '))}`);
183
359
  blank();
184
360
  }
185
- print(` ${dim('selected work')}`);
186
- blank();
187
- for (const item of work) {
188
- print(` ${bold(item.title)} ${dim(item.company)}`);
189
- for (const line of wrap(item.blurb)) print(` ${line}`);
190
- blank();
191
- }
192
361
  print(` ${dim('people who were not bots')}`);
193
362
  blank();
194
363
  for (const quote of quotes) {
@@ -203,29 +372,35 @@ export const printSkills = () => {
203
372
  section('STACK');
204
373
  blank();
205
374
  for (const group of skills) {
206
- print(` ${bold(group.name)}`);
375
+ print(` ${bold(group.name)} ${dim(group.note)}`);
207
376
  print(` ${group.items.join(dim(' · '))}`);
208
377
  blank();
209
378
  }
210
379
  };
211
380
 
212
- export const printHire = ({ copy = true } = {}) => {
381
+ export const printHire = ({ copy = true, report, to } = {}) => {
382
+ const mailto = mailtoFor({ report, to });
213
383
  blank();
214
384
  section('HIRE ME');
215
385
  blank();
216
386
  print(` ${dim('Skip the 47-field form. This is the whole application.')}`);
217
387
  blank();
218
- kv('Email', link(profile.email, hireMailto));
388
+ kv('Email', link(profile.email, mailto));
219
389
  kv('Phone', link(profile.phone, `tel:${profile.phone.replace(/\s+/g, '')}`));
220
390
  kv('Site', link(profile.website, profile.website));
221
391
  kv('GitHub', link(profile.github, profile.github));
222
392
  kv('LinkedIn', link(profile.linkedin, profile.linkedin));
223
393
  kv('Resume', link(profile.resume, profile.resume));
224
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 });
225
400
  if (copy && process.stdout.isTTY && copyToClipboard(profile.email)) {
226
401
  print(` ${green('copied')} ${profile.email} ${dim('to clipboard')}`);
227
402
  }
228
- print(` ${dim('or')} ${link('open a prefilled email', hireMailto)}`);
403
+ print(` ${dim('or')} ${link('open a prefilled email', mailto)}`);
229
404
  blank();
230
405
  print(` ${dim('Remote senior / full-stack. I can start a conversation this week.')}`);
231
406
  };
@@ -246,24 +421,44 @@ export const printHelp = () => {
246
421
  print(banner());
247
422
  blank();
248
423
  print(` ${bold('npx jobless')} reject you, then the resume`);
249
- print(` ${bold('npx jobless lint [file]')} lint a job posting`);
424
+ print(` ${bold('npx jobless lint [file]')} lint a job posting or jobs/`);
250
425
  print(` ${bold('npx jobless lint --json')} findings + rejection letter`);
251
- print(` ${bold('npx jobless work')} proof of work`);
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`);
252
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`);
253
439
  print(` ${bold('npx jobless who')} bio`);
254
440
  print(` ${bold('npx jobless looking')} roles`);
255
441
  print(` ${bold('npx jobless skills')} stack`);
256
442
  print(` ${bold('npx jobless rant')} the market`);
257
443
  print(` ${bold('npx jobless --resume')} skip the rejection`);
444
+ print(` ${bold('npx jobless --seed=42')} same rejection every time`);
258
445
  print(` ${bold('npx jobless --json')} machine readable resume`);
446
+ print(` ${bold('npx jobless --no-color')} recording-friendly`);
259
447
  blank();
260
- 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')}`);
261
450
  print(` ${dim(profile.website)}`);
262
451
  };
263
452
 
264
453
  export const printLandingTeaser = () => {
265
454
  blank();
266
- 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);
267
462
  };
268
463
 
269
464
  export const toJson = () =>
@@ -286,6 +481,9 @@ export const toJson = () =>
286
481
  lookingFor,
287
482
  rants,
288
483
  stats,
484
+ claims,
485
+ cases,
486
+ mentorship,
289
487
  experience,
290
488
  skills,
291
489
  },