jobless 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -49
- package/package.json +4 -3
- package/src/cli.js +107 -202
- package/src/clip.js +18 -0
- package/src/data.js +106 -105
- package/src/diff.js +103 -0
- package/src/fit.js +41 -0
- package/src/hiring.js +70 -0
- package/src/identity.js +5 -1
- package/src/interactive.js +73 -0
- package/src/lint-cli.js +128 -0
- package/src/lint-screens.js +114 -0
- package/src/lint.js +443 -18
- package/src/profile-screens.js +248 -0
- package/src/reject.js +89 -18
- package/src/screens.js +57 -176
- package/src/style.js +16 -8
- package/src/work-data.js +152 -0
package/README.md
CHANGED
|
@@ -1,82 +1,148 @@
|
|
|
1
1
|
# jobless
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/jobless)
|
|
4
|
+
[](https://www.npmjs.com/package/jobless)
|
|
5
|
+
|
|
6
|
+
I got invited to interview and rejected at the same time.
|
|
7
|
+
So I turned the hiring process into an npm package.
|
|
8
|
+
|
|
9
|
+
They asked how I handle rejection. Apparently: semantic versioning.
|
|
10
|
+
|
|
3
11
|
```bash
|
|
4
12
|
npx jobless
|
|
5
13
|
```
|
|
6
14
|
|
|
7
|
-
|
|
15
|
+
```text
|
|
16
|
+
✔ Congratulations! We'd love to interview you.
|
|
17
|
+
✔ Unfortunately, we're moving forward with others.
|
|
8
18
|
|
|
9
|
-
|
|
19
|
+
ERR_HIRING_RACE_CONDITION
|
|
20
|
+
Application status: yesn't
|
|
10
21
|
|
|
11
|
-
|
|
22
|
+
Interview scheduled successfully.
|
|
23
|
+
Candidate rejected successfully.
|
|
24
|
+
All systems operational.
|
|
12
25
|
|
|
13
|
-
|
|
26
|
+
I couldn't fix their hiring pipeline. So I shipped my own.
|
|
27
|
+
```
|
|
14
28
|
|
|
15
|
-
|
|
29
|
+
A hiring simulator, a reverse ATS, and an actual engineer behind the joke.
|
|
30
|
+
The simulator is satire. The contradictory interview is the origin story.
|
|
16
31
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
## Try it. Break it. Send it to a friend.
|
|
33
|
+
|
|
34
|
+
In the interactive prompt:
|
|
20
35
|
|
|
21
|
-
|
|
36
|
+
- `r` — try again. Same pipeline, different disappointment.
|
|
37
|
+
- `d` — reject a fictional job posting. No file needed.
|
|
38
|
+
- `l` — paste a real posting; finish with an empty line.
|
|
39
|
+
- `c` — copy the latest result, including the opening joke.
|
|
40
|
+
- `h` — meet the engineer.
|
|
41
|
+
- `q` — leave the hiring pipeline. Finally, a working exit.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx jobless share # a compact, anonymous result
|
|
45
|
+
npx jobless share --copy # explicitly copy it
|
|
46
|
+
npx jobless tweet # the origin story, ready to post
|
|
47
|
+
npx jobless --seed=1 # another hiring incident
|
|
48
|
+
npx jobless --record # anonymous, no animation or prompt
|
|
49
|
+
npx jobless --record --seed=42 --no-color
|
|
22
50
|
```
|
|
23
51
|
|
|
24
|
-
|
|
52
|
+
There are three hand-written scenarios. Seeds select a repeatable scenario;
|
|
53
|
+
`r` cycles through them. No AI API, account, or subscription needed.
|
|
54
|
+
|
|
55
|
+
## Reject the posting for a change
|
|
25
56
|
|
|
26
|
-
|
|
57
|
+
```bash
|
|
58
|
+
npx jobless lint --demo
|
|
59
|
+
```
|
|
27
60
|
|
|
28
|
-
|
|
61
|
+
Meet DemoCorp: competitive salary, six interview rounds, an unpaid take-home,
|
|
62
|
+
and a remote role that requires office attendance. Let it hear back from you.
|
|
29
63
|
|
|
30
64
|
```bash
|
|
31
65
|
npx jobless lint posting.txt
|
|
66
|
+
npx jobless lint jobs/
|
|
32
67
|
pbpaste | npx jobless lint
|
|
68
|
+
npx jobless lint --tweet posting.txt
|
|
69
|
+
npx jobless lint --copy posting.txt
|
|
70
|
+
npx jobless lint --json posting.txt
|
|
71
|
+
npx jobless lint --rules
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The score is rule-based satire, **not an objective employer rating**. It flags
|
|
75
|
+
phrases; it cannot judge the full context of a role or hiring process.
|
|
76
|
+
|
|
77
|
+
Errors exit with status `1`, including the intentionally bad demo. Warnings
|
|
78
|
+
alone exit with `0`, unless you set `--max-warnings=0`.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx jobless lint --fix posting.txt # proposed unified diff only
|
|
82
|
+
npx jobless lint --write posting.txt # explicitly overwrite the file
|
|
33
83
|
```
|
|
34
84
|
|
|
35
|
-
|
|
85
|
+
Review suggested rewrites: they include placeholder terms, not facts about the role.
|
|
86
|
+
`--demo` cannot be combined with file paths or `--write`.
|
|
87
|
+
Use `--off=rule-id` or `jobless-disable-next-line rule-id` to skip a rule.
|
|
88
|
+
|
|
89
|
+
For job postings in your own repository, see
|
|
90
|
+
[the example GitHub Action](examples/github-action.yml):
|
|
36
91
|
|
|
92
|
+
```yaml
|
|
93
|
+
- run: npx --yes jobless lint --format=github jobs/
|
|
37
94
|
```
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
that was not a recruiter.
|
|
57
|
-
that was you, finally answering.
|
|
95
|
+
|
|
96
|
+
## There is an engineer behind this
|
|
97
|
+
|
|
98
|
+
I'm [Yonas Alem](https://yonasalem.vercel.app), a senior software engineer in
|
|
99
|
+
Addis Ababa, open to remote roles. I debug silent production failures.
|
|
100
|
+
This one was loud enough to become a package.
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npx jobless hire # contact details; no automatic clipboard writes
|
|
104
|
+
npx jobless hire --copy # explicitly copy my email
|
|
105
|
+
npx jobless work # three case studies and experience
|
|
106
|
+
npx jobless proof # the deploy that killed 17 cron jobs
|
|
107
|
+
npx jobless card # compact contact card
|
|
108
|
+
npx jobless why # the origin story
|
|
109
|
+
npx jobless rant # five short coping mechanisms
|
|
110
|
+
npx jobless --resume # go straight to the engineer
|
|
111
|
+
npx jobless --json # machine-readable profile
|
|
112
|
+
npx jobless --help # all commands
|
|
58
113
|
```
|
|
59
114
|
|
|
60
|
-
|
|
115
|
+
Infra and performance claims belong to my work at Mereb. Payment volume
|
|
116
|
+
belongs to the products I integrated at Eaglelion, not a ledger I built.
|
|
117
|
+
Production code is private; the case studies explain the work and its limits.
|
|
118
|
+
|
|
119
|
+
- [Email](mailto:yonalem21@gmail.com)
|
|
120
|
+
- [Portfolio](https://yonasalem.vercel.app)
|
|
121
|
+
- [GitHub](https://github.com/Yonas21)
|
|
122
|
+
- [LinkedIn](https://www.linkedin.com/in/yonasalem21/)
|
|
123
|
+
|
|
124
|
+
## Local, with boundaries
|
|
125
|
+
|
|
126
|
+
Zero runtime dependencies. Node 18+. The CLI does not apply to jobs, send email,
|
|
127
|
+
scrape websites, or send telemetry. Fetching it through `npx` uses npm's network.
|
|
61
128
|
|
|
62
|
-
|
|
129
|
+
The default simulator and linter use an anonymous candidate identity. They do
|
|
130
|
+
not read your Git identity, username, or working directory for personalization.
|
|
131
|
+
`--personalize` opts into local identity lookup for the simulator's greeting;
|
|
132
|
+
it can read Git name/email and OS account details, but only shows your name.
|
|
133
|
+
`--record`, `share`, and copied simulator results always omit that local identity.
|
|
134
|
+
A pasted posting can still contain personal or company information: inspect
|
|
135
|
+
lint output before sharing it.
|
|
63
136
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
- LinkedIn: [linkedin.com/in/yonasalem21](https://www.linkedin.com/in/yonasalem21/)
|
|
68
|
-
- Source: [github.com/Yonas21/jobless](https://github.com/Yonas21/jobless)
|
|
137
|
+
Clipboard writes happen only when requested with `c`, `copy`, or `--copy`.
|
|
138
|
+
They use `pbcopy` on macOS, `clip` on Windows, or `xclip` on Linux. If copying is
|
|
139
|
+
unavailable, share commands leave the text visible for manual copying.
|
|
69
140
|
|
|
70
|
-
##
|
|
141
|
+
## Development
|
|
71
142
|
|
|
72
143
|
```bash
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
npx jobless lint --json # findings + rejection letter
|
|
76
|
-
npx jobless work # proof
|
|
77
|
-
npx jobless hire # email + links
|
|
78
|
-
npx jobless rant # the market
|
|
79
|
-
npx jobless --resume # skip the rejection
|
|
144
|
+
npm test
|
|
145
|
+
node bin/jobless.js
|
|
80
146
|
```
|
|
81
147
|
|
|
82
|
-
Zero dependencies.
|
|
148
|
+
Zero dependencies. Still dependent on someone replying.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jobless",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Invited to interview and rejected at the same time. A hiring race condition, reproduced in your terminal.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"npx",
|
|
@@ -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,99 +1,36 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline';
|
|
2
1
|
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { stdin } from 'node:process';
|
|
6
|
-
|
|
7
2
|
import { menu } from './data.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
clear,
|
|
13
|
-
dim,
|
|
14
|
-
print,
|
|
15
|
-
showCursor,
|
|
16
|
-
} from './style.js';
|
|
3
|
+
import { runLintCli } from './lint-cli.js';
|
|
4
|
+
import { interactive } from './interactive.js';
|
|
5
|
+
import { hiringResult, printHiring, printShare, shareText } from './hiring.js';
|
|
6
|
+
import { blank, dim, print, showCursor, wrap } from './style.js';
|
|
17
7
|
import {
|
|
18
|
-
printHeader,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
printLandingTeaser,
|
|
22
|
-
printLintReport,
|
|
23
|
-
printLooking,
|
|
24
|
-
printMenu,
|
|
25
|
-
printRant,
|
|
26
|
-
printSignature,
|
|
27
|
-
printSkills,
|
|
28
|
-
printWho,
|
|
29
|
-
printWork,
|
|
30
|
-
toJson,
|
|
8
|
+
printCard, printHeader, printHelp, printHire, printLandingTeaser,
|
|
9
|
+
printLooking, printMenu, printProof, printRant, printRules, printSignature,
|
|
10
|
+
printSkills, printTweet, printVcf, printWho, printWhy, printWork, toJson,
|
|
31
11
|
} from './screens.js';
|
|
32
12
|
|
|
33
|
-
const pkg = JSON.parse(
|
|
34
|
-
|
|
35
|
-
);
|
|
36
|
-
|
|
13
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
14
|
+
const VALUE_FLAGS = new Set(['--max-warnings', '--seed', '--off', '--format', '--to']);
|
|
37
15
|
const COMMANDS = {
|
|
38
|
-
who: printWho,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
rant: printRant,
|
|
43
|
-
market: printRant,
|
|
44
|
-
work: printWork,
|
|
45
|
-
proof: printWork,
|
|
46
|
-
experience: printWork,
|
|
47
|
-
skills: printSkills,
|
|
48
|
-
hire: printHire,
|
|
49
|
-
h: printHire,
|
|
50
|
-
links: printHire,
|
|
51
|
-
contact: printHire,
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const ask = (rl, prompt) =>
|
|
55
|
-
new Promise((resolve) => {
|
|
56
|
-
rl.question(prompt, resolve);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const readStdin = async () => {
|
|
60
|
-
const chunks = [];
|
|
61
|
-
for await (const chunk of stdin) chunks.push(chunk);
|
|
62
|
-
return Buffer.concat(chunks).toString('utf8');
|
|
16
|
+
who: printWho, about: printWho, looking: printLooking, jobs: printLooking,
|
|
17
|
+
rant: printRant, market: printRant, work: printWork, experience: printWork,
|
|
18
|
+
proof: printProof, skills: printSkills, card: printCard, why: printWhy,
|
|
19
|
+
help: printHelp,
|
|
63
20
|
};
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
print(dim(' paste a job description. empty line to lint.'));
|
|
68
|
-
blank();
|
|
69
|
-
const lines = [];
|
|
70
|
-
while (true) {
|
|
71
|
-
const line = await ask(rl, dim(' '));
|
|
72
|
-
if (line === '') break;
|
|
73
|
-
lines.push(line);
|
|
74
|
-
}
|
|
75
|
-
return lines.join('\n');
|
|
21
|
+
const aliases = {
|
|
22
|
+
q: 'quit', exit: 'quit', l: 'lint', c: 'copy', r: 'replay', d: 'demo',
|
|
23
|
+
h: 'hire', links: 'hire', contact: 'hire', vcard: 'vcf',
|
|
76
24
|
};
|
|
77
|
-
|
|
78
25
|
const resolveCommand = (input) => {
|
|
79
26
|
const value = String(input).trim().toLowerCase();
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
if (value
|
|
84
|
-
const fromMenu = menu.find((item) => item.key === value || item.command === value);
|
|
85
|
-
if (fromMenu) return fromMenu.command;
|
|
86
|
-
if (COMMANDS[value]) return value;
|
|
27
|
+
if (aliases[value]) return aliases[value];
|
|
28
|
+
const item = menu.find((entry) => entry.key === value);
|
|
29
|
+
if (item) return item.command;
|
|
30
|
+
if (COMMANDS[value] || ['quit', 'lint', 'copy', 'replay', 'demo', 'share', 'hire', 'vcf', 'tweet'].includes(value)) return value;
|
|
87
31
|
return null;
|
|
88
32
|
};
|
|
89
33
|
|
|
90
|
-
const runCommand = (command) => {
|
|
91
|
-
const render = COMMANDS[command];
|
|
92
|
-
if (!render) return false;
|
|
93
|
-
render();
|
|
94
|
-
return true;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
34
|
const showResume = () => {
|
|
98
35
|
printHeader();
|
|
99
36
|
printSignature();
|
|
@@ -101,150 +38,118 @@ const showResume = () => {
|
|
|
101
38
|
printMenu();
|
|
102
39
|
};
|
|
103
40
|
|
|
104
|
-
const interactive = async ({ trap }) => {
|
|
105
|
-
if (process.stdout.isTTY) clear();
|
|
106
|
-
if (trap) {
|
|
107
|
-
await printTrap({ animate: true });
|
|
108
|
-
blank();
|
|
109
|
-
}
|
|
110
|
-
showResume();
|
|
111
|
-
|
|
112
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
113
|
-
try {
|
|
114
|
-
while (true) {
|
|
115
|
-
const answer = await ask(rl, dim(' jobless> '));
|
|
116
|
-
const command = resolveCommand(answer);
|
|
117
|
-
|
|
118
|
-
if (!command) {
|
|
119
|
-
print(` ${dim('unknown. try 1–5, l to lint, h to hire, or q.')}`);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (command === 'help') {
|
|
123
|
-
if (process.stdout.isTTY) clear();
|
|
124
|
-
printHelp();
|
|
125
|
-
printMenu();
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
if (command === 'lint') {
|
|
129
|
-
const text = await readPaste(rl);
|
|
130
|
-
if (process.stdout.isTTY) clear();
|
|
131
|
-
printHeader();
|
|
132
|
-
printLintReport(lintResult(text, 'paste'));
|
|
133
|
-
printMenu();
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if (command === 'quit') {
|
|
137
|
-
blank();
|
|
138
|
-
print(dim(' still jobless. still shipping.'));
|
|
139
|
-
print(dim(' yonalem21@gmail.com'));
|
|
140
|
-
blank();
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (process.stdout.isTTY) clear();
|
|
145
|
-
printHeader();
|
|
146
|
-
runCommand(command);
|
|
147
|
-
printMenu();
|
|
148
|
-
}
|
|
149
|
-
} finally {
|
|
150
|
-
rl.close();
|
|
151
|
-
showCursor();
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
|
|
155
41
|
const parse = (argv) => {
|
|
156
42
|
const flags = new Set();
|
|
43
|
+
const opts = {};
|
|
157
44
|
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
45
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
46
|
+
const setOpt = (key, value) => {
|
|
47
|
+
opts[key] = opts[key] ? `${opts[key]},${value}` : value;
|
|
48
|
+
};
|
|
168
49
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
50
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
51
|
+
const arg = argv[i];
|
|
52
|
+
if (arg === '--') {
|
|
53
|
+
rest.push(...argv.slice(i + 1));
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (arg.startsWith('--') && arg.includes('=')) {
|
|
57
|
+
const eq = arg.indexOf('=');
|
|
58
|
+
const key = arg.slice(2, eq);
|
|
59
|
+
flags.add(`--${key}`);
|
|
60
|
+
setOpt(key, arg.slice(eq + 1));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (VALUE_FLAGS.has(arg)) {
|
|
64
|
+
flags.add(arg);
|
|
65
|
+
const next = argv[i + 1];
|
|
66
|
+
if (next && !next.startsWith('-')) {
|
|
67
|
+
setOpt(arg.slice(2), next);
|
|
68
|
+
i += 1;
|
|
69
|
+
} else if (arg === '--max-warnings') {
|
|
70
|
+
setOpt('max-warnings', '0');
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (arg.startsWith('-')) {
|
|
75
|
+
flags.add(arg);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
rest.push(arg);
|
|
180
79
|
}
|
|
181
80
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (json) {
|
|
186
|
-
print(JSON.stringify(report, null, 2));
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
81
|
+
return { flags, opts, rest };
|
|
82
|
+
};
|
|
189
83
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
print(dim(' more: npx jobless · npx jobless hire'));
|
|
84
|
+
const applyColorFlags = (flags, opts) => {
|
|
85
|
+
if (flags.has('--no-color') || opts.color === 'never') process.env.NO_COLOR = '1';
|
|
193
86
|
};
|
|
194
87
|
|
|
195
88
|
export const run = async (argv = []) => {
|
|
196
89
|
process.stdout.on('error', (error) => {
|
|
197
90
|
if (error.code === 'EPIPE') process.exit(0);
|
|
198
91
|
});
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const verb = rest[0]
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
92
|
+
const { flags, opts, rest } = parse(argv);
|
|
93
|
+
applyColorFlags(flags, opts);
|
|
94
|
+
const verb = rest[0]?.toLowerCase() || '';
|
|
95
|
+
const command = resolveCommand(verb);
|
|
96
|
+
const runCommand = (name, report = null) => {
|
|
97
|
+
if (name === 'hire') printHire({ report, to: opts.to, copy: flags.has('--copy') });
|
|
98
|
+
else if (name === 'vcf') printVcf({ copy: flags.has('--copy') });
|
|
99
|
+
else if (name === 'tweet') printTweet(report);
|
|
100
|
+
else COMMANDS[name]?.();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (flags.has('-h') || flags.has('--help') || command === 'help') return printHelp();
|
|
104
|
+
if (flags.has('-v') || flags.has('--version')) return print(pkg.version);
|
|
105
|
+
if (verb === 'rules' || flags.has('--rules')) return printRules();
|
|
106
|
+
|
|
107
|
+
if (command === 'lint' || command === 'demo') {
|
|
108
|
+
const maxRaw = opts['max-warnings'];
|
|
109
|
+
const maxWarnings = maxRaw == null || maxRaw === '' ? null : Number(maxRaw);
|
|
110
|
+
await runLintCli(rest.slice(1), {
|
|
111
|
+
format: opts.format || (flags.has('--json') ? 'json' : 'pretty'),
|
|
112
|
+
fix: flags.has('--fix') || flags.has('--diff') || flags.has('--write'),
|
|
113
|
+
write: flags.has('--write'), copy: flags.has('--copy'),
|
|
114
|
+
quiet: flags.has('--quiet'), tweet: flags.has('--tweet'),
|
|
115
|
+
demo: command === 'demo' || flags.has('--demo'),
|
|
116
|
+
off: String(opts.off || '').split(',').map((id) => id.trim()).filter(Boolean),
|
|
117
|
+
maxWarnings: Number.isFinite(maxWarnings) ? maxWarnings : null,
|
|
118
|
+
});
|
|
205
119
|
return;
|
|
206
120
|
}
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
121
|
+
if (flags.has('--json')) return print(toJson());
|
|
122
|
+
if (verb && !command) throw new Error(`Unknown command: ${verb}. Try npx jobless --help.`);
|
|
123
|
+
|
|
124
|
+
const record = flags.has('--record');
|
|
125
|
+
const seed = opts.seed ?? 0;
|
|
126
|
+
// Recording and exported results always omit local identity, even with --personalize.
|
|
127
|
+
const personalize = flags.has('--personalize') && !record;
|
|
128
|
+
if (command === 'share' || command === 'copy') {
|
|
129
|
+
printShare(shareText(hiringResult({ seed })), { copy: command === 'copy' || flags.has('--copy') });
|
|
210
130
|
return;
|
|
211
131
|
}
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (flags.has('--json')) {
|
|
219
|
-
print(toJson());
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const skipTrap = flags.has('--resume') || flags.has('--skip-trap') || flags.has('--no-trap');
|
|
224
|
-
const command = verb ? resolveCommand(verb) : null;
|
|
225
|
-
|
|
226
|
-
if (command && command !== 'quit' && command !== 'help' && command !== 'lint') {
|
|
227
|
-
printHeader();
|
|
132
|
+
if (command === 'quit') return;
|
|
133
|
+
if (command && command !== 'replay') {
|
|
134
|
+
if (!['why', 'tweet', 'rant'].includes(command)) printHeader();
|
|
228
135
|
runCommand(command);
|
|
229
|
-
blank();
|
|
230
|
-
print(dim(' more: npx jobless · npx jobless hire · npx jobless --help'));
|
|
231
136
|
return;
|
|
232
137
|
}
|
|
233
|
-
|
|
234
|
-
if (!
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
138
|
+
const skipTrap = flags.has('--resume') || flags.has('--skip-trap') || flags.has('--no-trap');
|
|
139
|
+
if (!skipTrap) hiringResult({ seed }); // Validate before starting an interactive prompt.
|
|
140
|
+
if (record || !process.stdout.isTTY || !process.stdin.isTTY) {
|
|
141
|
+
if (skipTrap) showResume();
|
|
142
|
+
else {
|
|
143
|
+
await printHiring({ seed, personalize });
|
|
144
|
+
blank();
|
|
145
|
+
for (const line of wrap('Next: npx jobless share | npx jobless lint --demo')) print(dim(` ${line}`));
|
|
146
|
+
}
|
|
240
147
|
return;
|
|
241
148
|
}
|
|
242
|
-
|
|
243
|
-
process.on('SIGINT', () => {
|
|
149
|
+
process.once('SIGINT', () => {
|
|
244
150
|
showCursor();
|
|
245
151
|
process.stdout.write('\n');
|
|
246
152
|
process.exit(0);
|
|
247
153
|
});
|
|
248
|
-
|
|
249
|
-
await interactive({ trap: !skipTrap });
|
|
154
|
+
await interactive({ trap: !skipTrap, seed, personalize, resolveCommand, runCommand, showResume });
|
|
250
155
|
};
|
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
|
+
};
|