terminalhire 0.1.0 → 0.2.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 +39 -12
- package/dist/bin/jpi-config.js +74 -0
- package/dist/bin/jpi-dispatch.js +986 -29
- package/dist/bin/jpi-init.js +126 -0
- package/dist/bin/jpi-jobs.js +34 -1
- package/dist/bin/jpi-learn.js +23 -0
- package/dist/bin/jpi-login.js +23 -0
- package/dist/bin/jpi-profile.js +23 -0
- package/dist/bin/jpi-refresh.js +1889 -0
- package/dist/bin/jpi-save.js +674 -0
- package/dist/bin/jpi-spinner.js +352 -0
- package/dist/bin/jpi.js +66 -3
- package/dist/bin/spinner.js +234 -0
- package/dist/src/config.js +49 -0
- package/dist/src/profile.js +23 -0
- package/install.js +295 -98
- package/package.json +16 -4
- package/postinstall.js +35 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/jpi-init.js
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { join, resolve } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { createInterface } from "readline";
|
|
8
|
+
import { spawnSync, spawn } from "child_process";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
var __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
11
|
+
function ask(question) {
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
return new Promise((resolve2) => {
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
resolve2(answer.trim().toLowerCase());
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function resolveScript(name) {
|
|
21
|
+
const distPath = resolve(join(__dirname, "..", "..", "dist", "bin", `${name}.js`));
|
|
22
|
+
const legacyPath = resolve(join(__dirname, `${name}.js`));
|
|
23
|
+
return existsSync(distPath) ? distPath : legacyPath;
|
|
24
|
+
}
|
|
25
|
+
function resolveInstallJs() {
|
|
26
|
+
const fromDist = resolve(join(__dirname, "..", "..", "install.js"));
|
|
27
|
+
const fromBin = resolve(join(__dirname, "..", "install.js"));
|
|
28
|
+
if (existsSync(fromDist)) return fromDist;
|
|
29
|
+
if (existsSync(fromBin)) return fromBin;
|
|
30
|
+
return fromBin;
|
|
31
|
+
}
|
|
32
|
+
async function run() {
|
|
33
|
+
console.log("");
|
|
34
|
+
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
35
|
+
console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
|
|
36
|
+
console.log("\u2502 Local-first job matching for developers in Claude Code \u2502");
|
|
37
|
+
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log("This will:");
|
|
40
|
+
console.log(" 1. Optionally sign you in with GitHub (public profile only, read:user)");
|
|
41
|
+
console.log(" 2. Seed your local job cache (anonymous index download)");
|
|
42
|
+
console.log(" 3. Install the statusLine hook in ~/.claude/settings.json");
|
|
43
|
+
console.log(" (with backup + your explicit consent before any file is touched)");
|
|
44
|
+
console.log("");
|
|
45
|
+
console.log('You can stop at any step. Nothing is changed until you say "yes".');
|
|
46
|
+
console.log("");
|
|
47
|
+
console.log("Step 1/3 \u2014 GitHub sign-in (optional but recommended)");
|
|
48
|
+
console.log("");
|
|
49
|
+
console.log(" Scope: read:user \u2014 public profile + public repos only.");
|
|
50
|
+
console.log(" Your token is encrypted at ~/.terminalhire/github-token.enc.");
|
|
51
|
+
console.log(" GitHub data enriches your local profile. Nothing leaves your machine");
|
|
52
|
+
console.log(" until you explicitly consent to a specific lead.");
|
|
53
|
+
console.log("");
|
|
54
|
+
const githubAnswer = await ask("Sign in with GitHub now? [Y/n] (Enter = yes, n = stay local): ");
|
|
55
|
+
const doGitHub = githubAnswer === "" || githubAnswer === "y" || githubAnswer === "yes";
|
|
56
|
+
if (doGitHub) {
|
|
57
|
+
console.log("");
|
|
58
|
+
console.log(" Starting GitHub device flow...");
|
|
59
|
+
const loginScript = resolveScript("jpi-login");
|
|
60
|
+
const child = spawnSync(process.execPath, [loginScript, "login"], {
|
|
61
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
62
|
+
env: process.env
|
|
63
|
+
});
|
|
64
|
+
if (child.status !== 0) {
|
|
65
|
+
console.log("");
|
|
66
|
+
console.log(" GitHub sign-in did not complete. Continuing without GitHub.");
|
|
67
|
+
console.log(" You can sign in any time with: terminalhire login");
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
console.log("");
|
|
71
|
+
console.log(" Staying local-only. Tags accumulate from your personal project sessions.");
|
|
72
|
+
console.log(" Sign in any time with: terminalhire login");
|
|
73
|
+
}
|
|
74
|
+
console.log("");
|
|
75
|
+
console.log("Step 2/3 \u2014 Seeding local job cache");
|
|
76
|
+
console.log("");
|
|
77
|
+
console.log(" Fetching anonymous job index (no dev data sent)...");
|
|
78
|
+
const jobsScript = resolveScript("jpi-jobs");
|
|
79
|
+
const seedChild = spawnSync(
|
|
80
|
+
process.execPath,
|
|
81
|
+
[jobsScript, "--limit", "0"],
|
|
82
|
+
{
|
|
83
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
84
|
+
env: { ...process.env, TERMINALHIRE_SEED_ONLY: "1" },
|
|
85
|
+
timeout: 15e3
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
if (seedChild.status === 0) {
|
|
89
|
+
console.log(" Job cache seeded successfully.");
|
|
90
|
+
} else {
|
|
91
|
+
console.log(" Could not seed job cache right now (no profile tags yet, or offline).");
|
|
92
|
+
console.log(" Run `terminalhire jobs` after a few Claude Code sessions to populate it.");
|
|
93
|
+
}
|
|
94
|
+
console.log("");
|
|
95
|
+
console.log("Step 3/3 \u2014 Install statusLine hook in ~/.claude/settings.json");
|
|
96
|
+
console.log("");
|
|
97
|
+
console.log(" This is the only step that modifies a system file.");
|
|
98
|
+
console.log(" A timestamped backup is created before any change.");
|
|
99
|
+
console.log(" Uninstall at any time: node install.js --uninstall");
|
|
100
|
+
console.log("");
|
|
101
|
+
const installJs = resolveInstallJs();
|
|
102
|
+
const installChild = spawnSync(process.execPath, [installJs], {
|
|
103
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
104
|
+
env: process.env
|
|
105
|
+
});
|
|
106
|
+
if (installChild.status !== 0) {
|
|
107
|
+
console.log("");
|
|
108
|
+
console.log(" Hook installation did not complete. Run manually: node install.js");
|
|
109
|
+
}
|
|
110
|
+
console.log("");
|
|
111
|
+
console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
112
|
+
console.log("\u2502 terminalhire init complete! \u2502");
|
|
113
|
+
console.log("\u2502 \u2502");
|
|
114
|
+
console.log("\u2502 Restart Claude Code to see the statusLine nudge. \u2502");
|
|
115
|
+
console.log("\u2502 \u2502");
|
|
116
|
+
console.log("\u2502 Quick reference: \u2502");
|
|
117
|
+
console.log("\u2502 terminalhire jobs \u2014 browse matching roles \u2502");
|
|
118
|
+
console.log("\u2502 terminalhire config --show \u2014 view nudge settings \u2502");
|
|
119
|
+
console.log("\u2502 terminalhire login \u2014 sign in with GitHub \u2502");
|
|
120
|
+
console.log("\u2502 terminalhire profile --show \u2014 inspect your local profile \u2502");
|
|
121
|
+
console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
122
|
+
console.log("");
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
run
|
|
126
|
+
};
|
package/dist/bin/jpi-jobs.js
CHANGED
|
@@ -1075,9 +1075,12 @@ __export(profile_exports, {
|
|
|
1075
1075
|
accumulateGitHubTags: () => accumulateGitHubTags,
|
|
1076
1076
|
accumulateSession: () => accumulateSession,
|
|
1077
1077
|
accumulateTags: () => accumulateTags,
|
|
1078
|
+
addSavedJob: () => addSavedJob,
|
|
1078
1079
|
deleteProfile: () => deleteProfile,
|
|
1080
|
+
listSavedJobs: () => listSavedJobs,
|
|
1079
1081
|
profileToFingerprint: () => profileToFingerprint,
|
|
1080
1082
|
readProfile: () => readProfile,
|
|
1083
|
+
removeSavedJob: () => removeSavedJob,
|
|
1081
1084
|
writeProfile: () => writeProfile
|
|
1082
1085
|
});
|
|
1083
1086
|
import {
|
|
@@ -1223,6 +1226,26 @@ function accumulateGitHubTags(profile, tags) {
|
|
|
1223
1226
|
false
|
|
1224
1227
|
);
|
|
1225
1228
|
}
|
|
1229
|
+
async function listSavedJobs() {
|
|
1230
|
+
const profile = await readProfile();
|
|
1231
|
+
return profile.savedJobs ?? [];
|
|
1232
|
+
}
|
|
1233
|
+
async function addSavedJob(job) {
|
|
1234
|
+
const profile = await readProfile();
|
|
1235
|
+
const existing = profile.savedJobs ?? [];
|
|
1236
|
+
const filtered = existing.filter((j) => j.id !== job.id);
|
|
1237
|
+
profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
1238
|
+
await writeProfile(profile);
|
|
1239
|
+
}
|
|
1240
|
+
async function removeSavedJob(id) {
|
|
1241
|
+
const profile = await readProfile();
|
|
1242
|
+
const existing = profile.savedJobs ?? [];
|
|
1243
|
+
const filtered = existing.filter((j) => j.id !== id);
|
|
1244
|
+
if (filtered.length === existing.length) return false;
|
|
1245
|
+
profile.savedJobs = filtered;
|
|
1246
|
+
await writeProfile(profile);
|
|
1247
|
+
return true;
|
|
1248
|
+
}
|
|
1226
1249
|
async function deleteProfile() {
|
|
1227
1250
|
const { rmSync } = await import("fs");
|
|
1228
1251
|
try {
|
|
@@ -1391,14 +1414,24 @@ function formatComp(job) {
|
|
|
1391
1414
|
if (job.compMin) return `$${Math.round(job.compMin / 1e3)}k+`;
|
|
1392
1415
|
return "";
|
|
1393
1416
|
}
|
|
1417
|
+
function linkTitle(title, url) {
|
|
1418
|
+
const isTTY = process.stdout.isTTY;
|
|
1419
|
+
const noColor = process.env["NO_COLOR"] !== void 0;
|
|
1420
|
+
if (isTTY && !noColor && url) {
|
|
1421
|
+
return `\x1B]8;;${url}\x1B\\${title}\x1B]8;;\x1B\\`;
|
|
1422
|
+
}
|
|
1423
|
+
return url ? `${title} (${url})` : title;
|
|
1424
|
+
}
|
|
1394
1425
|
function printResult(i, result) {
|
|
1395
1426
|
const { job, score, matchedTags, reason } = result;
|
|
1396
1427
|
const comp = formatComp(job);
|
|
1397
1428
|
const remote = job.remote ? "remote" : job.location ?? "onsite";
|
|
1398
1429
|
const compStr = comp ? ` \xB7 ${comp}` : "";
|
|
1399
1430
|
const mode = job.applyMode === "buyer-lead" ? " [COASTAL LEAD]" : "";
|
|
1431
|
+
const titleStr = linkTitle(job.title, job.url);
|
|
1400
1432
|
console.log(`
|
|
1401
|
-
${i + 1}. ${
|
|
1433
|
+
${i + 1}. ${titleStr} \u2014 ${job.company}${mode}`);
|
|
1434
|
+
console.log(` id: ${job.id}`);
|
|
1402
1435
|
console.log(` ${remote}${compStr} \xB7 ${job.roleType} \xB7 score: ${formatScore(score)}`);
|
|
1403
1436
|
console.log(` ${reason}`);
|
|
1404
1437
|
console.log(` Tags matched: ${matchedTags.slice(0, 5).join(", ")}`);
|
package/dist/bin/jpi-learn.js
CHANGED
|
@@ -577,9 +577,12 @@ __export(profile_exports, {
|
|
|
577
577
|
accumulateGitHubTags: () => accumulateGitHubTags,
|
|
578
578
|
accumulateSession: () => accumulateSession,
|
|
579
579
|
accumulateTags: () => accumulateTags,
|
|
580
|
+
addSavedJob: () => addSavedJob,
|
|
580
581
|
deleteProfile: () => deleteProfile,
|
|
582
|
+
listSavedJobs: () => listSavedJobs,
|
|
581
583
|
profileToFingerprint: () => profileToFingerprint,
|
|
582
584
|
readProfile: () => readProfile,
|
|
585
|
+
removeSavedJob: () => removeSavedJob,
|
|
583
586
|
writeProfile: () => writeProfile
|
|
584
587
|
});
|
|
585
588
|
import {
|
|
@@ -725,6 +728,26 @@ function accumulateGitHubTags(profile, tags) {
|
|
|
725
728
|
false
|
|
726
729
|
);
|
|
727
730
|
}
|
|
731
|
+
async function listSavedJobs() {
|
|
732
|
+
const profile = await readProfile();
|
|
733
|
+
return profile.savedJobs ?? [];
|
|
734
|
+
}
|
|
735
|
+
async function addSavedJob(job) {
|
|
736
|
+
const profile = await readProfile();
|
|
737
|
+
const existing = profile.savedJobs ?? [];
|
|
738
|
+
const filtered = existing.filter((j) => j.id !== job.id);
|
|
739
|
+
profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
740
|
+
await writeProfile(profile);
|
|
741
|
+
}
|
|
742
|
+
async function removeSavedJob(id) {
|
|
743
|
+
const profile = await readProfile();
|
|
744
|
+
const existing = profile.savedJobs ?? [];
|
|
745
|
+
const filtered = existing.filter((j) => j.id !== id);
|
|
746
|
+
if (filtered.length === existing.length) return false;
|
|
747
|
+
profile.savedJobs = filtered;
|
|
748
|
+
await writeProfile(profile);
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
728
751
|
async function deleteProfile() {
|
|
729
752
|
const { rmSync } = await import("fs");
|
|
730
753
|
try {
|
package/dist/bin/jpi-login.js
CHANGED
|
@@ -1289,9 +1289,12 @@ __export(profile_exports, {
|
|
|
1289
1289
|
accumulateGitHubTags: () => accumulateGitHubTags,
|
|
1290
1290
|
accumulateSession: () => accumulateSession,
|
|
1291
1291
|
accumulateTags: () => accumulateTags,
|
|
1292
|
+
addSavedJob: () => addSavedJob,
|
|
1292
1293
|
deleteProfile: () => deleteProfile,
|
|
1294
|
+
listSavedJobs: () => listSavedJobs,
|
|
1293
1295
|
profileToFingerprint: () => profileToFingerprint,
|
|
1294
1296
|
readProfile: () => readProfile,
|
|
1297
|
+
removeSavedJob: () => removeSavedJob,
|
|
1295
1298
|
writeProfile: () => writeProfile
|
|
1296
1299
|
});
|
|
1297
1300
|
import {
|
|
@@ -1437,6 +1440,26 @@ function accumulateGitHubTags(profile, tags) {
|
|
|
1437
1440
|
false
|
|
1438
1441
|
);
|
|
1439
1442
|
}
|
|
1443
|
+
async function listSavedJobs() {
|
|
1444
|
+
const profile = await readProfile();
|
|
1445
|
+
return profile.savedJobs ?? [];
|
|
1446
|
+
}
|
|
1447
|
+
async function addSavedJob(job) {
|
|
1448
|
+
const profile = await readProfile();
|
|
1449
|
+
const existing = profile.savedJobs ?? [];
|
|
1450
|
+
const filtered = existing.filter((j) => j.id !== job.id);
|
|
1451
|
+
profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
1452
|
+
await writeProfile(profile);
|
|
1453
|
+
}
|
|
1454
|
+
async function removeSavedJob(id) {
|
|
1455
|
+
const profile = await readProfile();
|
|
1456
|
+
const existing = profile.savedJobs ?? [];
|
|
1457
|
+
const filtered = existing.filter((j) => j.id !== id);
|
|
1458
|
+
if (filtered.length === existing.length) return false;
|
|
1459
|
+
profile.savedJobs = filtered;
|
|
1460
|
+
await writeProfile(profile);
|
|
1461
|
+
return true;
|
|
1462
|
+
}
|
|
1440
1463
|
async function deleteProfile() {
|
|
1441
1464
|
const { rmSync: rmSync2 } = await import("fs");
|
|
1442
1465
|
try {
|
package/dist/bin/jpi-profile.js
CHANGED
|
@@ -340,9 +340,12 @@ __export(profile_exports, {
|
|
|
340
340
|
accumulateGitHubTags: () => accumulateGitHubTags,
|
|
341
341
|
accumulateSession: () => accumulateSession,
|
|
342
342
|
accumulateTags: () => accumulateTags,
|
|
343
|
+
addSavedJob: () => addSavedJob,
|
|
343
344
|
deleteProfile: () => deleteProfile,
|
|
345
|
+
listSavedJobs: () => listSavedJobs,
|
|
344
346
|
profileToFingerprint: () => profileToFingerprint,
|
|
345
347
|
readProfile: () => readProfile,
|
|
348
|
+
removeSavedJob: () => removeSavedJob,
|
|
346
349
|
writeProfile: () => writeProfile
|
|
347
350
|
});
|
|
348
351
|
import {
|
|
@@ -488,6 +491,26 @@ function accumulateGitHubTags(profile, tags) {
|
|
|
488
491
|
false
|
|
489
492
|
);
|
|
490
493
|
}
|
|
494
|
+
async function listSavedJobs() {
|
|
495
|
+
const profile = await readProfile();
|
|
496
|
+
return profile.savedJobs ?? [];
|
|
497
|
+
}
|
|
498
|
+
async function addSavedJob(job) {
|
|
499
|
+
const profile = await readProfile();
|
|
500
|
+
const existing = profile.savedJobs ?? [];
|
|
501
|
+
const filtered = existing.filter((j) => j.id !== job.id);
|
|
502
|
+
profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
|
|
503
|
+
await writeProfile(profile);
|
|
504
|
+
}
|
|
505
|
+
async function removeSavedJob(id) {
|
|
506
|
+
const profile = await readProfile();
|
|
507
|
+
const existing = profile.savedJobs ?? [];
|
|
508
|
+
const filtered = existing.filter((j) => j.id !== id);
|
|
509
|
+
if (filtered.length === existing.length) return false;
|
|
510
|
+
profile.savedJobs = filtered;
|
|
511
|
+
await writeProfile(profile);
|
|
512
|
+
return true;
|
|
513
|
+
}
|
|
491
514
|
async function deleteProfile() {
|
|
492
515
|
const { rmSync } = await import("fs");
|
|
493
516
|
try {
|