terminalhire 0.1.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 +294 -0
- package/dist/bin/jpi-dispatch.js +2264 -0
- package/dist/bin/jpi-jobs.js +1506 -0
- package/dist/bin/jpi-learn.js +815 -0
- package/dist/bin/jpi-login.js +1603 -0
- package/dist/bin/jpi-profile.js +625 -0
- package/dist/bin/jpi.js +106 -0
- package/dist/src/github-auth.js +206 -0
- package/dist/src/profile.js +423 -0
- package/dist/src/signal.js +447 -0
- package/fixtures/github-sample.json +33 -0
- package/install.js +275 -0
- package/package.json +43 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
// src/signal.ts
|
|
2
|
+
import { readFileSync as readFileSync2, readdirSync } from "fs";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import { join as join2 } from "path";
|
|
5
|
+
|
|
6
|
+
// ../../packages/core/src/vocabulary.ts
|
|
7
|
+
var VOCABULARY = [
|
|
8
|
+
// Languages
|
|
9
|
+
"typescript",
|
|
10
|
+
"javascript",
|
|
11
|
+
"python",
|
|
12
|
+
"go",
|
|
13
|
+
"rust",
|
|
14
|
+
"java",
|
|
15
|
+
"ruby",
|
|
16
|
+
"elixir",
|
|
17
|
+
"scala",
|
|
18
|
+
"kotlin",
|
|
19
|
+
"swift",
|
|
20
|
+
"cpp",
|
|
21
|
+
"csharp",
|
|
22
|
+
"php",
|
|
23
|
+
"haskell",
|
|
24
|
+
"clojure",
|
|
25
|
+
"r",
|
|
26
|
+
// Frontend frameworks / libs
|
|
27
|
+
"react",
|
|
28
|
+
"nextjs",
|
|
29
|
+
"vue",
|
|
30
|
+
"nuxt",
|
|
31
|
+
"svelte",
|
|
32
|
+
"angular",
|
|
33
|
+
"solidjs",
|
|
34
|
+
"tailwind",
|
|
35
|
+
"css",
|
|
36
|
+
"html",
|
|
37
|
+
"graphql",
|
|
38
|
+
"trpc",
|
|
39
|
+
// Backend frameworks
|
|
40
|
+
"nodejs",
|
|
41
|
+
"express",
|
|
42
|
+
"fastify",
|
|
43
|
+
"nestjs",
|
|
44
|
+
"django",
|
|
45
|
+
"fastapi",
|
|
46
|
+
"flask",
|
|
47
|
+
"rails",
|
|
48
|
+
"spring",
|
|
49
|
+
"actix",
|
|
50
|
+
"gin",
|
|
51
|
+
"phoenix",
|
|
52
|
+
"laravel",
|
|
53
|
+
"dotnet",
|
|
54
|
+
// Infrastructure & DevOps
|
|
55
|
+
"kubernetes",
|
|
56
|
+
"docker",
|
|
57
|
+
"terraform",
|
|
58
|
+
"aws",
|
|
59
|
+
"gcp",
|
|
60
|
+
"azure",
|
|
61
|
+
"ci-cd",
|
|
62
|
+
"github-actions",
|
|
63
|
+
"linux",
|
|
64
|
+
"nginx",
|
|
65
|
+
"pulumi",
|
|
66
|
+
"ansible",
|
|
67
|
+
"prometheus",
|
|
68
|
+
"grafana",
|
|
69
|
+
"datadog",
|
|
70
|
+
"opentelemetry",
|
|
71
|
+
// Data & ML
|
|
72
|
+
"postgresql",
|
|
73
|
+
"mysql",
|
|
74
|
+
"sqlite",
|
|
75
|
+
"mongodb",
|
|
76
|
+
"redis",
|
|
77
|
+
"elasticsearch",
|
|
78
|
+
"kafka",
|
|
79
|
+
"rabbitmq",
|
|
80
|
+
"data-engineering",
|
|
81
|
+
"spark",
|
|
82
|
+
"airflow",
|
|
83
|
+
"dbt",
|
|
84
|
+
"ml",
|
|
85
|
+
"llm",
|
|
86
|
+
"pytorch",
|
|
87
|
+
"tensorflow",
|
|
88
|
+
"pandas",
|
|
89
|
+
"numpy",
|
|
90
|
+
// Domains / capabilities
|
|
91
|
+
"oauth",
|
|
92
|
+
"authentication",
|
|
93
|
+
"security",
|
|
94
|
+
"payments",
|
|
95
|
+
"billing",
|
|
96
|
+
"frontend",
|
|
97
|
+
"backend",
|
|
98
|
+
"devops",
|
|
99
|
+
"mobile",
|
|
100
|
+
"ios",
|
|
101
|
+
"android",
|
|
102
|
+
"api-design",
|
|
103
|
+
"microservices",
|
|
104
|
+
"websockets",
|
|
105
|
+
"testing",
|
|
106
|
+
"accessibility",
|
|
107
|
+
"seo",
|
|
108
|
+
"performance",
|
|
109
|
+
"observability",
|
|
110
|
+
"search",
|
|
111
|
+
"realtime"
|
|
112
|
+
];
|
|
113
|
+
var SYNONYMS = {
|
|
114
|
+
// Kubernetes aliases
|
|
115
|
+
"k8s": "kubernetes",
|
|
116
|
+
"kube": "kubernetes",
|
|
117
|
+
// Auth / identity
|
|
118
|
+
"passport": "authentication",
|
|
119
|
+
"oauth2": "oauth",
|
|
120
|
+
"oidc": "oauth",
|
|
121
|
+
"jwt": "authentication",
|
|
122
|
+
"saml": "authentication",
|
|
123
|
+
"auth0": "authentication",
|
|
124
|
+
"clerk": "authentication",
|
|
125
|
+
"nextauth": "authentication",
|
|
126
|
+
// Payments
|
|
127
|
+
"@stripe/stripe-js": "payments",
|
|
128
|
+
"stripe": "payments",
|
|
129
|
+
"braintree": "payments",
|
|
130
|
+
"paddle": "payments",
|
|
131
|
+
"lemonsqueezy": "payments",
|
|
132
|
+
"recurly": "billing",
|
|
133
|
+
"chargebee": "billing",
|
|
134
|
+
// Framework / lib aliases
|
|
135
|
+
"next": "nextjs",
|
|
136
|
+
"next.js": "nextjs",
|
|
137
|
+
"nuxt.js": "nuxt",
|
|
138
|
+
"vue.js": "vue",
|
|
139
|
+
"angular.js": "angular",
|
|
140
|
+
"angularjs": "angular",
|
|
141
|
+
"express.js": "express",
|
|
142
|
+
"expressjs": "express",
|
|
143
|
+
"fastapi": "fastapi",
|
|
144
|
+
"nest": "nestjs",
|
|
145
|
+
"nest.js": "nestjs",
|
|
146
|
+
"sveltekit": "svelte",
|
|
147
|
+
// Language aliases
|
|
148
|
+
"ts": "typescript",
|
|
149
|
+
"js": "javascript",
|
|
150
|
+
"py": "python",
|
|
151
|
+
"golang": "go",
|
|
152
|
+
"c++": "cpp",
|
|
153
|
+
"c#": "csharp",
|
|
154
|
+
".net": "dotnet",
|
|
155
|
+
"asp.net": "dotnet",
|
|
156
|
+
// DB aliases
|
|
157
|
+
"postgres": "postgresql",
|
|
158
|
+
"pg": "postgresql",
|
|
159
|
+
"mongo": "mongodb",
|
|
160
|
+
"elastic": "elasticsearch",
|
|
161
|
+
// Cloud aliases
|
|
162
|
+
"amazon web services": "aws",
|
|
163
|
+
"google cloud": "gcp",
|
|
164
|
+
"google cloud platform": "gcp",
|
|
165
|
+
"microsoft azure": "azure",
|
|
166
|
+
// CI/CD aliases
|
|
167
|
+
"github actions": "github-actions",
|
|
168
|
+
"circle ci": "ci-cd",
|
|
169
|
+
"circleci": "ci-cd",
|
|
170
|
+
"jenkins": "ci-cd",
|
|
171
|
+
"gitlab ci": "ci-cd",
|
|
172
|
+
"travis": "ci-cd",
|
|
173
|
+
// Mobile
|
|
174
|
+
"react native": "mobile",
|
|
175
|
+
"flutter": "mobile",
|
|
176
|
+
"expo": "mobile",
|
|
177
|
+
// AI / ML
|
|
178
|
+
"openai": "llm",
|
|
179
|
+
"anthropic": "llm",
|
|
180
|
+
"langchain": "llm",
|
|
181
|
+
"llamaindex": "llm",
|
|
182
|
+
"hugging face": "ml",
|
|
183
|
+
"huggingface": "ml",
|
|
184
|
+
"scikit-learn": "ml",
|
|
185
|
+
"sklearn": "ml",
|
|
186
|
+
// Data pipeline
|
|
187
|
+
"apache kafka": "kafka",
|
|
188
|
+
"apache spark": "spark",
|
|
189
|
+
"apache airflow": "airflow",
|
|
190
|
+
// Misc
|
|
191
|
+
"tailwindcss": "tailwind",
|
|
192
|
+
"tw": "tailwind",
|
|
193
|
+
"gql": "graphql",
|
|
194
|
+
"ws": "websockets",
|
|
195
|
+
"socket.io": "websockets",
|
|
196
|
+
"jest": "testing",
|
|
197
|
+
"vitest": "testing",
|
|
198
|
+
"playwright": "testing",
|
|
199
|
+
"cypress": "testing"
|
|
200
|
+
};
|
|
201
|
+
var VOCAB_SET = new Set(VOCABULARY);
|
|
202
|
+
function normalize(tokens) {
|
|
203
|
+
const result = /* @__PURE__ */ new Set();
|
|
204
|
+
for (const raw of tokens) {
|
|
205
|
+
const lower = raw.toLowerCase().trim();
|
|
206
|
+
if (VOCAB_SET.has(lower)) {
|
|
207
|
+
result.add(lower);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const mapped = SYNONYMS[lower];
|
|
211
|
+
if (mapped && VOCAB_SET.has(mapped)) {
|
|
212
|
+
result.add(mapped);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return Array.from(result);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ../../packages/core/src/coastal.ts
|
|
219
|
+
import { readFileSync } from "fs";
|
|
220
|
+
import { join } from "path";
|
|
221
|
+
import { fileURLToPath } from "url";
|
|
222
|
+
|
|
223
|
+
// src/signal.ts
|
|
224
|
+
var LANGUAGE_TAGS = /* @__PURE__ */ new Set([
|
|
225
|
+
"typescript",
|
|
226
|
+
"javascript",
|
|
227
|
+
"python",
|
|
228
|
+
"go",
|
|
229
|
+
"rust",
|
|
230
|
+
"java",
|
|
231
|
+
"ruby",
|
|
232
|
+
"elixir",
|
|
233
|
+
"scala",
|
|
234
|
+
"kotlin",
|
|
235
|
+
"swift",
|
|
236
|
+
"cpp",
|
|
237
|
+
"csharp",
|
|
238
|
+
"php",
|
|
239
|
+
"haskell",
|
|
240
|
+
"clojure",
|
|
241
|
+
"r"
|
|
242
|
+
]);
|
|
243
|
+
var EXT_MAP = {
|
|
244
|
+
".ts": "typescript",
|
|
245
|
+
".tsx": "typescript",
|
|
246
|
+
".js": "javascript",
|
|
247
|
+
".mjs": "javascript",
|
|
248
|
+
".cjs": "javascript",
|
|
249
|
+
".jsx": "javascript",
|
|
250
|
+
".py": "python",
|
|
251
|
+
".go": "go",
|
|
252
|
+
".rs": "rust",
|
|
253
|
+
".java": "java",
|
|
254
|
+
".rb": "ruby",
|
|
255
|
+
".ex": "elixir",
|
|
256
|
+
".exs": "elixir",
|
|
257
|
+
".scala": "scala",
|
|
258
|
+
".kt": "kotlin",
|
|
259
|
+
".swift": "swift",
|
|
260
|
+
".cpp": "cpp",
|
|
261
|
+
".cc": "cpp",
|
|
262
|
+
".cxx": "cpp",
|
|
263
|
+
".hpp": "cpp",
|
|
264
|
+
".cs": "csharp",
|
|
265
|
+
".php": "php",
|
|
266
|
+
".hs": "haskell",
|
|
267
|
+
".clj": "clojure",
|
|
268
|
+
".cljs": "clojure",
|
|
269
|
+
".r": "r",
|
|
270
|
+
".vue": "vue",
|
|
271
|
+
".svelte": "svelte"
|
|
272
|
+
};
|
|
273
|
+
var PERSONAL_GIT_HOSTS = /* @__PURE__ */ new Set([
|
|
274
|
+
"github.com",
|
|
275
|
+
"gitlab.com",
|
|
276
|
+
"bitbucket.org",
|
|
277
|
+
"codeberg.org",
|
|
278
|
+
"sr.ht"
|
|
279
|
+
]);
|
|
280
|
+
var PERSONAL_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
|
|
281
|
+
"gmail.com",
|
|
282
|
+
"googlemail.com",
|
|
283
|
+
"yahoo.com",
|
|
284
|
+
"outlook.com",
|
|
285
|
+
"hotmail.com",
|
|
286
|
+
"icloud.com",
|
|
287
|
+
"me.com",
|
|
288
|
+
"mac.com",
|
|
289
|
+
"proton.me",
|
|
290
|
+
"protonmail.com",
|
|
291
|
+
"fastmail.com",
|
|
292
|
+
"hey.com",
|
|
293
|
+
"duck.com"
|
|
294
|
+
]);
|
|
295
|
+
function safeExec(cmd) {
|
|
296
|
+
try {
|
|
297
|
+
return execSync(cmd, { timeout: 2e3, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
|
|
298
|
+
} catch {
|
|
299
|
+
return "";
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function isEmployerContext(cwd) {
|
|
303
|
+
const remote = safeExec('git -C "' + cwd + '" remote get-url origin 2>/dev/null');
|
|
304
|
+
if (remote) {
|
|
305
|
+
try {
|
|
306
|
+
const sshMatch = remote.match(/^git@([^:]+):/);
|
|
307
|
+
const httpsMatch = remote.match(/^https?:\/\/([^/]+)/);
|
|
308
|
+
const host = (sshMatch?.[1] ?? httpsMatch?.[1] ?? "").toLowerCase();
|
|
309
|
+
if (host && !PERSONAL_GIT_HOSTS.has(host)) {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const email = safeExec('git -C "' + cwd + '" config user.email 2>/dev/null');
|
|
316
|
+
if (email) {
|
|
317
|
+
const domain = email.split("@")[1]?.toLowerCase() ?? "";
|
|
318
|
+
if (domain && !PERSONAL_EMAIL_DOMAINS.has(domain)) {
|
|
319
|
+
return true;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
function readJsonSafe(path) {
|
|
325
|
+
try {
|
|
326
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
327
|
+
} catch {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function readFileSafe(path) {
|
|
332
|
+
try {
|
|
333
|
+
return readFileSync2(path, "utf8");
|
|
334
|
+
} catch {
|
|
335
|
+
return "";
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function tokensFromPackageJson(cwd) {
|
|
339
|
+
const pkg = readJsonSafe(join2(cwd, "package.json"));
|
|
340
|
+
if (!pkg || typeof pkg !== "object") return [];
|
|
341
|
+
const p = pkg;
|
|
342
|
+
const deps = {
|
|
343
|
+
...typeof p["dependencies"] === "object" ? p["dependencies"] : {},
|
|
344
|
+
...typeof p["devDependencies"] === "object" ? p["devDependencies"] : {}
|
|
345
|
+
};
|
|
346
|
+
return Object.keys(deps);
|
|
347
|
+
}
|
|
348
|
+
function tokensFromRequirementsTxt(cwd) {
|
|
349
|
+
const content = readFileSafe(join2(cwd, "requirements.txt"));
|
|
350
|
+
if (!content) return [];
|
|
351
|
+
return content.split("\n").map((l) => l.trim().split(/[>=<!\[;]/)[0].trim().toLowerCase()).filter(Boolean);
|
|
352
|
+
}
|
|
353
|
+
function tokensFromGoMod(cwd) {
|
|
354
|
+
const content = readFileSafe(join2(cwd, "go.mod"));
|
|
355
|
+
if (!content) return ["go"];
|
|
356
|
+
const requires = Array.from(content.matchAll(/^\s+([^\s]+)\s+v/gm)).map((m) => m[1].split("/").pop() ?? "").filter(Boolean);
|
|
357
|
+
return ["go", ...requires];
|
|
358
|
+
}
|
|
359
|
+
function tokensFromCargoToml(cwd) {
|
|
360
|
+
const content = readFileSafe(join2(cwd, "Cargo.toml"));
|
|
361
|
+
if (!content) return [];
|
|
362
|
+
const deps = Array.from(content.matchAll(/^([a-zA-Z0-9_-]+)\s*=/gm)).map((m) => m[1].toLowerCase());
|
|
363
|
+
return ["rust", ...deps];
|
|
364
|
+
}
|
|
365
|
+
function tokensFromFileExtensions(cwd) {
|
|
366
|
+
const tokens = [];
|
|
367
|
+
const scanDirs = [cwd];
|
|
368
|
+
try {
|
|
369
|
+
const srcDir = join2(cwd, "src");
|
|
370
|
+
readdirSync(srcDir);
|
|
371
|
+
scanDirs.push(srcDir);
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
for (const dir of scanDirs) {
|
|
375
|
+
try {
|
|
376
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
377
|
+
for (const e of entries) {
|
|
378
|
+
if (!e.isFile()) continue;
|
|
379
|
+
const dotIdx = e.name.lastIndexOf(".");
|
|
380
|
+
if (dotIdx === -1) continue;
|
|
381
|
+
const ext = e.name.slice(dotIdx).toLowerCase();
|
|
382
|
+
const tag = EXT_MAP[ext];
|
|
383
|
+
if (tag) tokens.push(tag);
|
|
384
|
+
}
|
|
385
|
+
} catch {
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return tokens;
|
|
389
|
+
}
|
|
390
|
+
function inferSeniority(rawTokens) {
|
|
391
|
+
const seniorSignals = /* @__PURE__ */ new Set([
|
|
392
|
+
"kubernetes",
|
|
393
|
+
"terraform",
|
|
394
|
+
"pulumi",
|
|
395
|
+
"kafka",
|
|
396
|
+
"spark",
|
|
397
|
+
"airflow",
|
|
398
|
+
"dbt",
|
|
399
|
+
"opentelemetry",
|
|
400
|
+
"prometheus",
|
|
401
|
+
"grafana",
|
|
402
|
+
"microservices",
|
|
403
|
+
"api-design",
|
|
404
|
+
"security",
|
|
405
|
+
"oauth",
|
|
406
|
+
"payments"
|
|
407
|
+
]);
|
|
408
|
+
const midSignals = /* @__PURE__ */ new Set([
|
|
409
|
+
"docker",
|
|
410
|
+
"ci-cd",
|
|
411
|
+
"github-actions",
|
|
412
|
+
"testing",
|
|
413
|
+
"postgresql",
|
|
414
|
+
"redis",
|
|
415
|
+
"graphql",
|
|
416
|
+
"trpc"
|
|
417
|
+
]);
|
|
418
|
+
const normalized = new Set(normalize(rawTokens));
|
|
419
|
+
const seniorHits = [...normalized].filter((t) => seniorSignals.has(t)).length;
|
|
420
|
+
const midHits = [...normalized].filter((t) => midSignals.has(t)).length;
|
|
421
|
+
if (seniorHits >= 2) return "senior";
|
|
422
|
+
if (seniorHits >= 1 || midHits >= 2) return "mid";
|
|
423
|
+
return void 0;
|
|
424
|
+
}
|
|
425
|
+
function extractFingerprint(cwd) {
|
|
426
|
+
const employer = isEmployerContext(cwd);
|
|
427
|
+
const rawTokens = [
|
|
428
|
+
...tokensFromPackageJson(cwd),
|
|
429
|
+
...tokensFromRequirementsTxt(cwd),
|
|
430
|
+
...tokensFromGoMod(cwd),
|
|
431
|
+
...tokensFromCargoToml(cwd),
|
|
432
|
+
...tokensFromFileExtensions(cwd)
|
|
433
|
+
];
|
|
434
|
+
let skillTags = normalize(rawTokens);
|
|
435
|
+
if (employer) {
|
|
436
|
+
skillTags = skillTags.filter((t) => LANGUAGE_TAGS.has(t));
|
|
437
|
+
}
|
|
438
|
+
const seniorityBand = employer ? void 0 : inferSeniority(rawTokens);
|
|
439
|
+
return {
|
|
440
|
+
skillTags,
|
|
441
|
+
seniorityBand,
|
|
442
|
+
employerContext: employer
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
export {
|
|
446
|
+
extractFingerprint
|
|
447
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"login": "janedev",
|
|
3
|
+
"name": "Jane Developer",
|
|
4
|
+
"publicEmail": "jane@example.com",
|
|
5
|
+
"avatarUrl": "https://avatars.githubusercontent.com/u/1234567?v=4",
|
|
6
|
+
"accountCreatedAt": "2016-03-14T09:22:00Z",
|
|
7
|
+
"publicRepos": 52,
|
|
8
|
+
"followers": 314,
|
|
9
|
+
"topLanguages": [
|
|
10
|
+
"typescript",
|
|
11
|
+
"python",
|
|
12
|
+
"go",
|
|
13
|
+
"rust",
|
|
14
|
+
"javascript"
|
|
15
|
+
],
|
|
16
|
+
"topics": [
|
|
17
|
+
"react",
|
|
18
|
+
"nextjs",
|
|
19
|
+
"postgresql",
|
|
20
|
+
"docker",
|
|
21
|
+
"kubernetes",
|
|
22
|
+
"graphql",
|
|
23
|
+
"testing",
|
|
24
|
+
"llm",
|
|
25
|
+
"oauth",
|
|
26
|
+
"redis"
|
|
27
|
+
],
|
|
28
|
+
"recentPRorgs": [
|
|
29
|
+
"vercel",
|
|
30
|
+
"prisma",
|
|
31
|
+
"trpc"
|
|
32
|
+
]
|
|
33
|
+
}
|