terminalhire 0.2.0 → 0.2.2
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/dist/bin/jpi-dispatch.js +351 -61
- package/dist/bin/jpi-refresh.js +18 -10
- package/dist/bin/jpi-sync.js +837 -0
- package/dist/bin/spinner.js +16 -8
- package/install.js +5 -2
- package/package.json +1 -1
package/dist/bin/jpi-dispatch.js
CHANGED
|
@@ -1461,13 +1461,13 @@ async function removeSavedJob(id) {
|
|
|
1461
1461
|
return true;
|
|
1462
1462
|
}
|
|
1463
1463
|
async function deleteProfile() {
|
|
1464
|
-
const { rmSync:
|
|
1464
|
+
const { rmSync: rmSync3 } = await import("fs");
|
|
1465
1465
|
try {
|
|
1466
|
-
|
|
1466
|
+
rmSync3(PROFILE_FILE);
|
|
1467
1467
|
} catch {
|
|
1468
1468
|
}
|
|
1469
1469
|
try {
|
|
1470
|
-
|
|
1470
|
+
rmSync3(KEY_FILE2);
|
|
1471
1471
|
} catch {
|
|
1472
1472
|
}
|
|
1473
1473
|
}
|
|
@@ -1553,11 +1553,11 @@ async function runLogin() {
|
|
|
1553
1553
|
if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
|
|
1554
1554
|
const { createRequire: createRequire2 } = await import("module");
|
|
1555
1555
|
const { fileURLToPath: fileURLToPath7 } = await import("url");
|
|
1556
|
-
const { join:
|
|
1556
|
+
const { join: join14, dirname: dirname3 } = await import("path");
|
|
1557
1557
|
const __dirname6 = fileURLToPath7(new URL(".", import.meta.url));
|
|
1558
|
-
const fixturePath =
|
|
1559
|
-
const { readFileSync:
|
|
1560
|
-
ghProfile = JSON.parse(
|
|
1558
|
+
const fixturePath = join14(__dirname6, "../../fixtures/github-sample.json");
|
|
1559
|
+
const { readFileSync: readFileSync13 } = await import("fs");
|
|
1560
|
+
ghProfile = JSON.parse(readFileSync13(fixturePath, "utf8"));
|
|
1561
1561
|
} else {
|
|
1562
1562
|
ghProfile = await fetchGitHubProfile2(login, token);
|
|
1563
1563
|
}
|
|
@@ -2474,10 +2474,12 @@ function clearSpinnerVerbs() {
|
|
|
2474
2474
|
}
|
|
2475
2475
|
return { cleared: true, keptUserVerbs };
|
|
2476
2476
|
}
|
|
2477
|
-
function buildTips(topMatches, baseUrl, max =
|
|
2477
|
+
function buildTips(topMatches, baseUrl, max = 8) {
|
|
2478
2478
|
const base = String(baseUrl || "https://terminalhire.com").replace(/\/+$/, "");
|
|
2479
2479
|
const out = [];
|
|
2480
|
-
const
|
|
2480
|
+
const seenRole = /* @__PURE__ */ new Set();
|
|
2481
|
+
const perCompany = /* @__PURE__ */ new Map();
|
|
2482
|
+
const COMPANY_CAP = 2;
|
|
2481
2483
|
for (const m of Array.isArray(topMatches) ? topMatches : []) {
|
|
2482
2484
|
if (!m || !m.title || !m.company || !m.id) continue;
|
|
2483
2485
|
const idx = String(m.id).indexOf(":");
|
|
@@ -2485,14 +2487,20 @@ function buildTips(topMatches, baseUrl, max = 3) {
|
|
|
2485
2487
|
const source = String(m.id).slice(0, idx);
|
|
2486
2488
|
const ext = String(m.id).slice(idx + 1);
|
|
2487
2489
|
if (!source || !ext) continue;
|
|
2488
|
-
const
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2490
|
+
const companyRaw = String(m.company).trim().replace(/\s+/g, " ");
|
|
2491
|
+
const titleRaw = String(m.title).trim().replace(/\s+/g, " ");
|
|
2492
|
+
const roleKey = `${titleRaw.toLowerCase()}@${companyRaw.toLowerCase()}`;
|
|
2493
|
+
const coKey = companyRaw.toLowerCase();
|
|
2494
|
+
if (seenRole.has(roleKey)) continue;
|
|
2495
|
+
if ((perCompany.get(coKey) || 0) >= COMPANY_CAP) continue;
|
|
2496
|
+
seenRole.add(roleKey);
|
|
2497
|
+
perCompany.set(coKey, (perCompany.get(coKey) || 0) + 1);
|
|
2498
|
+
let title = titleRaw;
|
|
2492
2499
|
if (title.length > 34) title = title.slice(0, 33).trimEnd() + "\u2026";
|
|
2493
|
-
const company = titleCase(
|
|
2500
|
+
const company = titleCase(companyRaw);
|
|
2494
2501
|
const pct = Math.max(1, Math.min(99, Math.round((Number(m.score) || 0) * 100)));
|
|
2495
|
-
const
|
|
2502
|
+
const token = Buffer.from(String(m.id)).toString("base64url");
|
|
2503
|
+
const url = `${base}/j/${token}`;
|
|
2496
2504
|
out.push(`\u2197 ${title} @ ${company} \xB7 ${pct}% \u2014 ${url}`);
|
|
2497
2505
|
if (out.length >= max) break;
|
|
2498
2506
|
}
|
|
@@ -2743,19 +2751,292 @@ var init_jpi_spinner = __esm({
|
|
|
2743
2751
|
}
|
|
2744
2752
|
});
|
|
2745
2753
|
|
|
2754
|
+
// bin/jpi-sync.js
|
|
2755
|
+
var jpi_sync_exports = {};
|
|
2756
|
+
__export(jpi_sync_exports, {
|
|
2757
|
+
run: () => run7
|
|
2758
|
+
});
|
|
2759
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync7, rmSync as rmSync2 } from "fs";
|
|
2760
|
+
import { join as join9 } from "path";
|
|
2761
|
+
import { homedir as homedir7 } from "os";
|
|
2762
|
+
import { createInterface as createInterface4 } from "readline";
|
|
2763
|
+
function ask2(question) {
|
|
2764
|
+
const rl = createInterface4({ input: process.stdin, output: process.stdout });
|
|
2765
|
+
return new Promise((res) => {
|
|
2766
|
+
rl.question(question, (answer) => {
|
|
2767
|
+
rl.close();
|
|
2768
|
+
res(answer.trim().toLowerCase());
|
|
2769
|
+
});
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
function readMarker() {
|
|
2773
|
+
try {
|
|
2774
|
+
return existsSync7(TIER1_MARKER) ? JSON.parse(readFileSync9(TIER1_MARKER, "utf8")) : null;
|
|
2775
|
+
} catch {
|
|
2776
|
+
return null;
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
function writeMarker(marker) {
|
|
2780
|
+
mkdirSync7(TH_DIR3, { recursive: true });
|
|
2781
|
+
writeFileSync7(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
2782
|
+
}
|
|
2783
|
+
function clearMarker() {
|
|
2784
|
+
try {
|
|
2785
|
+
rmSync2(TIER1_MARKER);
|
|
2786
|
+
} catch {
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
function buildConsentFields(profile) {
|
|
2790
|
+
const gh = profile.github || {};
|
|
2791
|
+
const fields = [
|
|
2792
|
+
{ key: "login", label: "GitHub login", value: gh.login },
|
|
2793
|
+
{ key: "publicEmail", label: "Public email", value: gh.publicEmail || null },
|
|
2794
|
+
{ key: "topLanguages", label: "Top languages", value: gh.topLanguages || [] },
|
|
2795
|
+
{ key: "skillTags", label: "Skill tags", value: profile.skillTags || [] }
|
|
2796
|
+
];
|
|
2797
|
+
if (profile.displayName) {
|
|
2798
|
+
fields.push({ key: "displayName", label: "Display name", value: profile.displayName });
|
|
2799
|
+
}
|
|
2800
|
+
if (profile.contactEmail) {
|
|
2801
|
+
fields.push({ key: "contactEmail", label: "Contact email", value: profile.contactEmail });
|
|
2802
|
+
}
|
|
2803
|
+
return fields;
|
|
2804
|
+
}
|
|
2805
|
+
function renderConsentCard(fields) {
|
|
2806
|
+
console.log("");
|
|
2807
|
+
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\u2510");
|
|
2808
|
+
console.log("\u2502 terminalhire \u2014 sync your profile (Tier-1, opt-in) \u2502");
|
|
2809
|
+
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\u2518");
|
|
2810
|
+
console.log("");
|
|
2811
|
+
console.log(" You are about to send the following data to");
|
|
2812
|
+
console.log(" staqs (terminalhire.com):");
|
|
2813
|
+
console.log("");
|
|
2814
|
+
for (const f of fields) {
|
|
2815
|
+
const shown = Array.isArray(f.value) ? JSON.stringify(f.value) : String(f.value ?? "(not set)");
|
|
2816
|
+
console.log(` ${f.label.padEnd(16)}: ${shown}`);
|
|
2817
|
+
}
|
|
2818
|
+
console.log("");
|
|
2819
|
+
console.log(" What is NEVER sent:");
|
|
2820
|
+
console.log(" - Private repos, employer repos, raw code");
|
|
2821
|
+
console.log(" - Employer-repo-derived tags (filtered at source)");
|
|
2822
|
+
console.log(" - Session history, file paths, project names");
|
|
2823
|
+
console.log("");
|
|
2824
|
+
console.log(" This is a ONE-TIME snapshot. Nothing is sent automatically;");
|
|
2825
|
+
console.log(" re-run `terminalhire sync --push` to update it later.");
|
|
2826
|
+
console.log(" Delete it any time: terminalhire sync --delete");
|
|
2827
|
+
console.log("");
|
|
2828
|
+
console.log(" This is NOT required to use terminalhire.");
|
|
2829
|
+
console.log("");
|
|
2830
|
+
}
|
|
2831
|
+
async function runPush() {
|
|
2832
|
+
const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
2833
|
+
const profile = await readProfile2();
|
|
2834
|
+
if (!profile.github || !profile.github.login) {
|
|
2835
|
+
console.log("");
|
|
2836
|
+
console.log(" No GitHub data in your local profile yet.");
|
|
2837
|
+
console.log(" Run `terminalhire login` first, then re-run `terminalhire sync --push`.");
|
|
2838
|
+
console.log("");
|
|
2839
|
+
process.exit(1);
|
|
2840
|
+
}
|
|
2841
|
+
const fields = buildConsentFields(profile);
|
|
2842
|
+
renderConsentCard(fields);
|
|
2843
|
+
let consentConfirmed = false;
|
|
2844
|
+
const answer = await ask2(' Sync your profile to staqs (terminalhire.com)? Type "yes" to continue: ');
|
|
2845
|
+
if (answer === "yes") {
|
|
2846
|
+
consentConfirmed = true;
|
|
2847
|
+
}
|
|
2848
|
+
if (!consentConfirmed) {
|
|
2849
|
+
console.log("\n Aborted \u2014 nothing was sent.\n");
|
|
2850
|
+
process.exit(0);
|
|
2851
|
+
}
|
|
2852
|
+
const consentedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2853
|
+
const consentToken = {
|
|
2854
|
+
consentedAt,
|
|
2855
|
+
version: CONSENT_VERSION,
|
|
2856
|
+
fields: fields.map((f) => f.key)
|
|
2857
|
+
};
|
|
2858
|
+
const payloadProfile = {
|
|
2859
|
+
login: profile.github.login,
|
|
2860
|
+
name: profile.displayName || null,
|
|
2861
|
+
publicEmail: profile.github.publicEmail || null,
|
|
2862
|
+
topLanguages: profile.github.topLanguages || [],
|
|
2863
|
+
skillTags: profile.skillTags || [],
|
|
2864
|
+
displayName: profile.displayName || null,
|
|
2865
|
+
contactEmail: profile.contactEmail || null
|
|
2866
|
+
};
|
|
2867
|
+
const priorMarker = readMarker();
|
|
2868
|
+
const rowToken = priorMarker && priorMarker.deleteToken ? priorMarker.deleteToken : null;
|
|
2869
|
+
const requestBody = { consentToken, profile: payloadProfile };
|
|
2870
|
+
if (rowToken) {
|
|
2871
|
+
requestBody.rowToken = rowToken;
|
|
2872
|
+
}
|
|
2873
|
+
console.log("\n Sending one-time snapshot...");
|
|
2874
|
+
let res;
|
|
2875
|
+
try {
|
|
2876
|
+
res = await fetch(`${API_URL2}/api/profile-sync`, {
|
|
2877
|
+
method: "POST",
|
|
2878
|
+
headers: { "Content-Type": "application/json" },
|
|
2879
|
+
body: JSON.stringify(requestBody),
|
|
2880
|
+
signal: AbortSignal.timeout(1e4)
|
|
2881
|
+
});
|
|
2882
|
+
} catch (err) {
|
|
2883
|
+
console.error(`
|
|
2884
|
+
Sync failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2885
|
+
process.exit(1);
|
|
2886
|
+
}
|
|
2887
|
+
if (!res.ok) {
|
|
2888
|
+
let detail = "";
|
|
2889
|
+
try {
|
|
2890
|
+
detail = (await res.json())?.message || "";
|
|
2891
|
+
} catch {
|
|
2892
|
+
}
|
|
2893
|
+
console.error(`
|
|
2894
|
+
Sync failed: /api/profile-sync returned ${res.status}. ${detail}`);
|
|
2895
|
+
if (res.status === 503) {
|
|
2896
|
+
console.error(" (Tier-1 sync is not enabled on the server yet.)");
|
|
2897
|
+
}
|
|
2898
|
+
if (res.status === 403) {
|
|
2899
|
+
console.error(" (This GitHub login was already claimed by a different push.)");
|
|
2900
|
+
}
|
|
2901
|
+
process.exit(1);
|
|
2902
|
+
}
|
|
2903
|
+
let deleteToken = null;
|
|
2904
|
+
try {
|
|
2905
|
+
deleteToken = (await res.json())?.deleteToken || null;
|
|
2906
|
+
} catch {
|
|
2907
|
+
}
|
|
2908
|
+
writeMarker({ consentedAt, login: profile.github.login, deleteToken });
|
|
2909
|
+
console.log("\n Profile synced. A local consent marker was written to ~/.terminalhire/tier1.json");
|
|
2910
|
+
console.log(" Delete your synced profile any time: terminalhire sync --delete\n");
|
|
2911
|
+
}
|
|
2912
|
+
function runStatus() {
|
|
2913
|
+
const marker = readMarker();
|
|
2914
|
+
console.log("");
|
|
2915
|
+
if (marker && marker.consentedAt) {
|
|
2916
|
+
console.log(" Tier-1 sync: CONSENTED (local marker present)");
|
|
2917
|
+
console.log(` login: ${marker.login || "(unknown)"}`);
|
|
2918
|
+
console.log(` consented at: ${marker.consentedAt}`);
|
|
2919
|
+
console.log("");
|
|
2920
|
+
console.log(" This reflects your local marker only (no network call was made).");
|
|
2921
|
+
console.log(" Update: terminalhire sync --push | Delete: terminalhire sync --delete");
|
|
2922
|
+
} else {
|
|
2923
|
+
console.log(" Tier-1 sync: NOT CONSENTED (no local marker).");
|
|
2924
|
+
console.log(" Your profile has not been synced. Enable: terminalhire sync --push");
|
|
2925
|
+
}
|
|
2926
|
+
console.log("");
|
|
2927
|
+
}
|
|
2928
|
+
async function runDelete() {
|
|
2929
|
+
const marker = readMarker();
|
|
2930
|
+
console.log("");
|
|
2931
|
+
console.log(" This will hard-delete your synced profile from staqs (terminalhire.com)");
|
|
2932
|
+
console.log(" and remove the local consent marker. There is no soft-delete.");
|
|
2933
|
+
console.log("");
|
|
2934
|
+
const answer = await ask2(' Delete your synced profile? Type "yes" to confirm: ');
|
|
2935
|
+
if (answer !== "yes") {
|
|
2936
|
+
console.log("\n Aborted \u2014 nothing was deleted.\n");
|
|
2937
|
+
process.exit(0);
|
|
2938
|
+
}
|
|
2939
|
+
let login = marker && marker.login ? marker.login : null;
|
|
2940
|
+
if (!login) {
|
|
2941
|
+
const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
|
|
2942
|
+
const profile = await readProfile2();
|
|
2943
|
+
login = profile.github && profile.github.login ? profile.github.login : null;
|
|
2944
|
+
}
|
|
2945
|
+
if (!login) {
|
|
2946
|
+
console.log("\n No github login to delete (no marker, no GitHub profile). Clearing local marker.\n");
|
|
2947
|
+
clearMarker();
|
|
2948
|
+
process.exit(0);
|
|
2949
|
+
}
|
|
2950
|
+
const deleteToken = marker && marker.deleteToken ? marker.deleteToken : null;
|
|
2951
|
+
if (!deleteToken) {
|
|
2952
|
+
console.log("\n No delete token found in ~/.terminalhire/tier1.json.");
|
|
2953
|
+
console.log(" Deletion must be run from the machine that originally pushed your profile");
|
|
2954
|
+
console.log(" (the delete token is stored there), or re-run `terminalhire sync --push`");
|
|
2955
|
+
console.log(" first to obtain a fresh token, then `terminalhire sync --delete`.\n");
|
|
2956
|
+
process.exit(1);
|
|
2957
|
+
}
|
|
2958
|
+
const consentToken = {
|
|
2959
|
+
consentedAt: marker && marker.consentedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
2960
|
+
version: CONSENT_VERSION,
|
|
2961
|
+
fields: ["login"]
|
|
2962
|
+
};
|
|
2963
|
+
console.log("\n Requesting deletion...");
|
|
2964
|
+
let res;
|
|
2965
|
+
try {
|
|
2966
|
+
res = await fetch(`${API_URL2}/api/profile-sync`, {
|
|
2967
|
+
method: "DELETE",
|
|
2968
|
+
headers: { "Content-Type": "application/json" },
|
|
2969
|
+
body: JSON.stringify({ consentToken, login, deleteToken }),
|
|
2970
|
+
signal: AbortSignal.timeout(1e4)
|
|
2971
|
+
});
|
|
2972
|
+
} catch (err) {
|
|
2973
|
+
console.error(`
|
|
2974
|
+
Delete failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2975
|
+
console.error(" Local marker NOT cleared (server state unknown). Re-run to retry.\n");
|
|
2976
|
+
process.exit(1);
|
|
2977
|
+
}
|
|
2978
|
+
if (!res.ok) {
|
|
2979
|
+
console.error(`
|
|
2980
|
+
Delete failed: /api/profile-sync returned ${res.status}.`);
|
|
2981
|
+
if (res.status === 503) {
|
|
2982
|
+
console.error(" (Tier-1 sync is not enabled on the server yet.) Clearing local marker.");
|
|
2983
|
+
clearMarker();
|
|
2984
|
+
}
|
|
2985
|
+
process.exit(1);
|
|
2986
|
+
}
|
|
2987
|
+
clearMarker();
|
|
2988
|
+
console.log("\n Synced profile deleted and local marker cleared.\n");
|
|
2989
|
+
}
|
|
2990
|
+
async function run7() {
|
|
2991
|
+
const args2 = process.argv.slice(2).filter((a) => a !== "sync");
|
|
2992
|
+
const has = (f) => args2.includes(f);
|
|
2993
|
+
if (has("--push") || has("--enable")) {
|
|
2994
|
+
await runPush();
|
|
2995
|
+
return;
|
|
2996
|
+
}
|
|
2997
|
+
if (has("--status")) {
|
|
2998
|
+
runStatus();
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
if (has("--delete")) {
|
|
3002
|
+
await runDelete();
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
console.log("");
|
|
3006
|
+
console.log(" terminalhire sync \u2014 opt-in Tier-1 profile sync (one-time snapshot)");
|
|
3007
|
+
console.log("");
|
|
3008
|
+
console.log(' terminalhire sync --push Send your profile (shows a consent card, requires typed "yes")');
|
|
3009
|
+
console.log(" terminalhire sync --status Show whether you have consented (local read only)");
|
|
3010
|
+
console.log(" terminalhire sync --delete Hard-delete your synced profile (revocation)");
|
|
3011
|
+
console.log("");
|
|
3012
|
+
console.log(' Your profile is NEVER sent without an explicit typed "yes".');
|
|
3013
|
+
console.log(" This is NOT required to use terminalhire.");
|
|
3014
|
+
console.log("");
|
|
3015
|
+
}
|
|
3016
|
+
var TH_DIR3, TIER1_MARKER, API_URL2, CONSENT_VERSION;
|
|
3017
|
+
var init_jpi_sync = __esm({
|
|
3018
|
+
"bin/jpi-sync.js"() {
|
|
3019
|
+
"use strict";
|
|
3020
|
+
TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join9(homedir7(), ".terminalhire");
|
|
3021
|
+
TIER1_MARKER = join9(TH_DIR3, "tier1.json");
|
|
3022
|
+
API_URL2 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
|
|
3023
|
+
CONSENT_VERSION = 1;
|
|
3024
|
+
}
|
|
3025
|
+
});
|
|
3026
|
+
|
|
2746
3027
|
// bin/jpi-init.js
|
|
2747
3028
|
var jpi_init_exports = {};
|
|
2748
3029
|
__export(jpi_init_exports, {
|
|
2749
|
-
run: () =>
|
|
3030
|
+
run: () => run8
|
|
2750
3031
|
});
|
|
2751
|
-
import { existsSync as
|
|
2752
|
-
import { join as
|
|
3032
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3033
|
+
import { join as join10, resolve } from "path";
|
|
2753
3034
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2754
|
-
import { createInterface as
|
|
3035
|
+
import { createInterface as createInterface5 } from "readline";
|
|
2755
3036
|
import { spawnSync, spawn } from "child_process";
|
|
2756
|
-
import { homedir as
|
|
2757
|
-
function
|
|
2758
|
-
const rl =
|
|
3037
|
+
import { homedir as homedir8 } from "os";
|
|
3038
|
+
function ask3(question) {
|
|
3039
|
+
const rl = createInterface5({ input: process.stdin, output: process.stdout });
|
|
2759
3040
|
return new Promise((resolve2) => {
|
|
2760
3041
|
rl.question(question, (answer) => {
|
|
2761
3042
|
rl.close();
|
|
@@ -2764,18 +3045,18 @@ function ask2(question) {
|
|
|
2764
3045
|
});
|
|
2765
3046
|
}
|
|
2766
3047
|
function resolveScript(name) {
|
|
2767
|
-
const distPath = resolve(
|
|
2768
|
-
const legacyPath = resolve(
|
|
2769
|
-
return
|
|
3048
|
+
const distPath = resolve(join10(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
|
|
3049
|
+
const legacyPath = resolve(join10(__dirname2, `${name}.js`));
|
|
3050
|
+
return existsSync8(distPath) ? distPath : legacyPath;
|
|
2770
3051
|
}
|
|
2771
3052
|
function resolveInstallJs() {
|
|
2772
|
-
const fromDist = resolve(
|
|
2773
|
-
const fromBin = resolve(
|
|
2774
|
-
if (
|
|
2775
|
-
if (
|
|
3053
|
+
const fromDist = resolve(join10(__dirname2, "..", "..", "install.js"));
|
|
3054
|
+
const fromBin = resolve(join10(__dirname2, "..", "install.js"));
|
|
3055
|
+
if (existsSync8(fromDist)) return fromDist;
|
|
3056
|
+
if (existsSync8(fromBin)) return fromBin;
|
|
2776
3057
|
return fromBin;
|
|
2777
3058
|
}
|
|
2778
|
-
async function
|
|
3059
|
+
async function run8() {
|
|
2779
3060
|
console.log("");
|
|
2780
3061
|
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");
|
|
2781
3062
|
console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
|
|
@@ -2797,7 +3078,7 @@ async function run7() {
|
|
|
2797
3078
|
console.log(" GitHub data enriches your local profile. Nothing leaves your machine");
|
|
2798
3079
|
console.log(" until you explicitly consent to a specific lead.");
|
|
2799
3080
|
console.log("");
|
|
2800
|
-
const githubAnswer = await
|
|
3081
|
+
const githubAnswer = await ask3("Sign in with GitHub now? [Y/n] (Enter = yes, n = stay local): ");
|
|
2801
3082
|
const doGitHub = githubAnswer === "" || githubAnswer === "y" || githubAnswer === "yes";
|
|
2802
3083
|
if (doGitHub) {
|
|
2803
3084
|
console.log("");
|
|
@@ -2878,17 +3159,17 @@ var init_jpi_init = __esm({
|
|
|
2878
3159
|
// bin/jpi-refresh.js
|
|
2879
3160
|
var jpi_refresh_exports = {};
|
|
2880
3161
|
__export(jpi_refresh_exports, {
|
|
2881
|
-
run: () =>
|
|
3162
|
+
run: () => run9
|
|
2882
3163
|
});
|
|
2883
|
-
import { readFileSync as
|
|
2884
|
-
import { join as
|
|
2885
|
-
import { homedir as
|
|
3164
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync8, existsSync as existsSync9, mkdirSync as mkdirSync8 } from "fs";
|
|
3165
|
+
import { join as join11 } from "path";
|
|
3166
|
+
import { homedir as homedir9 } from "os";
|
|
2886
3167
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2887
|
-
async function
|
|
3168
|
+
async function run9() {
|
|
2888
3169
|
try {
|
|
2889
3170
|
let index;
|
|
2890
3171
|
try {
|
|
2891
|
-
const res = await fetch(`${
|
|
3172
|
+
const res = await fetch(`${API_URL3}/api/index`, {
|
|
2892
3173
|
signal: AbortSignal.timeout(15e3),
|
|
2893
3174
|
headers: { "Accept": "application/json" }
|
|
2894
3175
|
});
|
|
@@ -2915,7 +3196,7 @@ async function run8() {
|
|
|
2915
3196
|
const fp = profileToFingerprint2(profile);
|
|
2916
3197
|
const results = match2(fp, jobs, jobs.length);
|
|
2917
3198
|
matchCount = results.length;
|
|
2918
|
-
topMatches = results.slice(0,
|
|
3199
|
+
topMatches = results.slice(0, 25).map((r) => ({
|
|
2919
3200
|
id: r.job.id,
|
|
2920
3201
|
title: r.job.title,
|
|
2921
3202
|
company: r.job.company,
|
|
@@ -2926,14 +3207,14 @@ async function run8() {
|
|
|
2926
3207
|
}
|
|
2927
3208
|
} catch {
|
|
2928
3209
|
}
|
|
2929
|
-
|
|
3210
|
+
mkdirSync8(TERMINALHIRE_DIR5, { recursive: true });
|
|
2930
3211
|
const cacheEntry = {
|
|
2931
3212
|
ts: Date.now(),
|
|
2932
3213
|
index,
|
|
2933
3214
|
matchCount,
|
|
2934
3215
|
topMatches
|
|
2935
3216
|
};
|
|
2936
|
-
|
|
3217
|
+
writeFileSync8(INDEX_CACHE_FILE2, JSON.stringify(cacheEntry), "utf8");
|
|
2937
3218
|
try {
|
|
2938
3219
|
const {
|
|
2939
3220
|
readSpinnerConfig: readSpinnerConfig2,
|
|
@@ -2960,7 +3241,7 @@ async function run8() {
|
|
|
2960
3241
|
const verbs = buildSpinnerPool2(ranked, sc.max, { sessionTags, frequency: sc.frequency });
|
|
2961
3242
|
if (verbs.length > 0) applySpinnerVerbs2(verbs, sc.mode);
|
|
2962
3243
|
else clearSpinnerVerbs2();
|
|
2963
|
-
const tips = buildTips2(ranked,
|
|
3244
|
+
const tips = buildTips2(ranked, API_URL3, 8);
|
|
2964
3245
|
if (tips.length > 0) applySpinnerTips2(tips);
|
|
2965
3246
|
else clearSpinnerTips2();
|
|
2966
3247
|
} else {
|
|
@@ -2977,30 +3258,30 @@ async function run8() {
|
|
|
2977
3258
|
process.exit(1);
|
|
2978
3259
|
}
|
|
2979
3260
|
}
|
|
2980
|
-
var __dirname3, TERMINALHIRE_DIR5, INDEX_CACHE_FILE2,
|
|
3261
|
+
var __dirname3, TERMINALHIRE_DIR5, INDEX_CACHE_FILE2, API_URL3;
|
|
2981
3262
|
var init_jpi_refresh = __esm({
|
|
2982
3263
|
"bin/jpi-refresh.js"() {
|
|
2983
3264
|
"use strict";
|
|
2984
3265
|
__dirname3 = fileURLToPath4(new URL(".", import.meta.url));
|
|
2985
|
-
TERMINALHIRE_DIR5 =
|
|
2986
|
-
INDEX_CACHE_FILE2 =
|
|
2987
|
-
|
|
3266
|
+
TERMINALHIRE_DIR5 = join11(homedir9(), ".terminalhire");
|
|
3267
|
+
INDEX_CACHE_FILE2 = join11(TERMINALHIRE_DIR5, "index-cache.json");
|
|
3268
|
+
API_URL3 = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
|
|
2988
3269
|
}
|
|
2989
3270
|
});
|
|
2990
3271
|
|
|
2991
3272
|
// bin/jpi-save.js
|
|
2992
3273
|
var jpi_save_exports = {};
|
|
2993
3274
|
__export(jpi_save_exports, {
|
|
2994
|
-
run: () =>
|
|
3275
|
+
run: () => run10
|
|
2995
3276
|
});
|
|
2996
|
-
import { readFileSync as
|
|
2997
|
-
import { join as
|
|
2998
|
-
import { homedir as
|
|
3277
|
+
import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
|
|
3278
|
+
import { join as join12 } from "path";
|
|
3279
|
+
import { homedir as homedir10 } from "os";
|
|
2999
3280
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
3000
3281
|
function findJobInCache(jobId) {
|
|
3001
3282
|
try {
|
|
3002
|
-
if (!
|
|
3003
|
-
const raw =
|
|
3283
|
+
if (!existsSync10(INDEX_CACHE_FILE3)) return null;
|
|
3284
|
+
const raw = readFileSync11(INDEX_CACHE_FILE3, "utf8");
|
|
3004
3285
|
const entry = JSON.parse(raw);
|
|
3005
3286
|
const jobs = entry?.index?.jobs ?? [];
|
|
3006
3287
|
return jobs.find((j) => j.id === jobId) ?? null;
|
|
@@ -3069,7 +3350,7 @@ async function cmdUnsave(jobId) {
|
|
|
3069
3350
|
process.exit(1);
|
|
3070
3351
|
}
|
|
3071
3352
|
}
|
|
3072
|
-
async function
|
|
3353
|
+
async function run10() {
|
|
3073
3354
|
const verb = process.argv[2];
|
|
3074
3355
|
const jobId = process.argv[3];
|
|
3075
3356
|
try {
|
|
@@ -3093,26 +3374,26 @@ var init_jpi_save = __esm({
|
|
|
3093
3374
|
"bin/jpi-save.js"() {
|
|
3094
3375
|
"use strict";
|
|
3095
3376
|
__dirname4 = fileURLToPath5(new URL(".", import.meta.url));
|
|
3096
|
-
TERMINALHIRE_DIR6 =
|
|
3097
|
-
INDEX_CACHE_FILE3 =
|
|
3377
|
+
TERMINALHIRE_DIR6 = join12(homedir10(), ".terminalhire");
|
|
3378
|
+
INDEX_CACHE_FILE3 = join12(TERMINALHIRE_DIR6, "index-cache.json");
|
|
3098
3379
|
}
|
|
3099
3380
|
});
|
|
3100
3381
|
|
|
3101
3382
|
// bin/jpi-dispatch.js
|
|
3102
3383
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
3103
|
-
import { join as
|
|
3104
|
-
import { existsSync as
|
|
3384
|
+
import { join as join13, dirname as dirname2 } from "path";
|
|
3385
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
|
|
3105
3386
|
import { createRequire } from "module";
|
|
3106
3387
|
var __dirname5 = fileURLToPath6(new URL(".", import.meta.url));
|
|
3107
3388
|
function readPackageVersion() {
|
|
3108
3389
|
try {
|
|
3109
3390
|
const candidates = [
|
|
3110
|
-
|
|
3111
|
-
|
|
3391
|
+
join13(__dirname5, "..", "..", "package.json"),
|
|
3392
|
+
join13(__dirname5, "..", "package.json")
|
|
3112
3393
|
];
|
|
3113
3394
|
for (const p of candidates) {
|
|
3114
|
-
if (
|
|
3115
|
-
const pkg = JSON.parse(
|
|
3395
|
+
if (existsSync11(p)) {
|
|
3396
|
+
const pkg = JSON.parse(readFileSync12(p, "utf8"));
|
|
3116
3397
|
if (pkg.version) return pkg.version;
|
|
3117
3398
|
}
|
|
3118
3399
|
}
|
|
@@ -3123,7 +3404,7 @@ function readPackageVersion() {
|
|
|
3123
3404
|
var firstArg = process.argv[2];
|
|
3124
3405
|
if (!firstArg && !process.stdin.isTTY) {
|
|
3125
3406
|
const { default: childProcess } = await import("child_process");
|
|
3126
|
-
const nudgeScript =
|
|
3407
|
+
const nudgeScript = join13(__dirname5, "jpi.js");
|
|
3127
3408
|
const child = childProcess.spawnSync(process.execPath, [nudgeScript], {
|
|
3128
3409
|
stdio: ["inherit", "inherit", "inherit"]
|
|
3129
3410
|
});
|
|
@@ -3154,6 +3435,9 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
|
|
|
3154
3435
|
console.log(" terminalhire save <jobId> Save a job locally (id shown in `jobs` output)");
|
|
3155
3436
|
console.log(" terminalhire saved List all locally-saved jobs");
|
|
3156
3437
|
console.log(" terminalhire unsave <jobId> Remove a saved job");
|
|
3438
|
+
console.log(" terminalhire sync --push Opt-in: send your profile to staqs (typed-yes consent)");
|
|
3439
|
+
console.log(" terminalhire sync --status Show whether you have consented (local read only)");
|
|
3440
|
+
console.log(" terminalhire sync --delete Hard-delete your synced profile (revocation)");
|
|
3157
3441
|
console.log("");
|
|
3158
3442
|
console.log("Install / uninstall the Claude Code statusLine nudge:");
|
|
3159
3443
|
console.log(" terminalhire init (preferred \u2014 full guided onboarding)");
|
|
@@ -3163,6 +3447,7 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
|
|
|
3163
3447
|
console.log("Privacy: your profile never leaves your device.");
|
|
3164
3448
|
console.log(" GET /api/index \u2014 anonymous index download (no dev data)");
|
|
3165
3449
|
console.log(" POST /api/lead \u2014 only after explicit per-role named-entity consent");
|
|
3450
|
+
console.log(" POST /api/profile-sync \u2014 only via `terminalhire sync --push` with a typed-yes consent token");
|
|
3166
3451
|
console.log(" GitHub token \u2014 encrypted at ~/.terminalhire/github-token.enc, scope: read:user");
|
|
3167
3452
|
console.log("");
|
|
3168
3453
|
process.exit(0);
|
|
@@ -3202,6 +3487,11 @@ if (firstArg === "spinner") {
|
|
|
3202
3487
|
await mod.run();
|
|
3203
3488
|
process.exit(0);
|
|
3204
3489
|
}
|
|
3490
|
+
if (firstArg === "sync") {
|
|
3491
|
+
const mod = await Promise.resolve().then(() => (init_jpi_sync(), jpi_sync_exports));
|
|
3492
|
+
await mod.run();
|
|
3493
|
+
process.exit(0);
|
|
3494
|
+
}
|
|
3205
3495
|
if (firstArg === "init") {
|
|
3206
3496
|
const mod = await Promise.resolve().then(() => (init_jpi_init(), jpi_init_exports));
|
|
3207
3497
|
await mod.run();
|
package/dist/bin/jpi-refresh.js
CHANGED
|
@@ -1471,10 +1471,12 @@ function clearSpinnerVerbs() {
|
|
|
1471
1471
|
}
|
|
1472
1472
|
return { cleared: true, keptUserVerbs };
|
|
1473
1473
|
}
|
|
1474
|
-
function buildTips(topMatches, baseUrl, max =
|
|
1474
|
+
function buildTips(topMatches, baseUrl, max = 8) {
|
|
1475
1475
|
const base = String(baseUrl || "https://terminalhire.com").replace(/\/+$/, "");
|
|
1476
1476
|
const out = [];
|
|
1477
|
-
const
|
|
1477
|
+
const seenRole = /* @__PURE__ */ new Set();
|
|
1478
|
+
const perCompany = /* @__PURE__ */ new Map();
|
|
1479
|
+
const COMPANY_CAP = 2;
|
|
1478
1480
|
for (const m of Array.isArray(topMatches) ? topMatches : []) {
|
|
1479
1481
|
if (!m || !m.title || !m.company || !m.id) continue;
|
|
1480
1482
|
const idx = String(m.id).indexOf(":");
|
|
@@ -1482,14 +1484,20 @@ function buildTips(topMatches, baseUrl, max = 3) {
|
|
|
1482
1484
|
const source = String(m.id).slice(0, idx);
|
|
1483
1485
|
const ext = String(m.id).slice(idx + 1);
|
|
1484
1486
|
if (!source || !ext) continue;
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1487
|
+
const companyRaw = String(m.company).trim().replace(/\s+/g, " ");
|
|
1488
|
+
const titleRaw = String(m.title).trim().replace(/\s+/g, " ");
|
|
1489
|
+
const roleKey = `${titleRaw.toLowerCase()}@${companyRaw.toLowerCase()}`;
|
|
1490
|
+
const coKey = companyRaw.toLowerCase();
|
|
1491
|
+
if (seenRole.has(roleKey)) continue;
|
|
1492
|
+
if ((perCompany.get(coKey) || 0) >= COMPANY_CAP) continue;
|
|
1493
|
+
seenRole.add(roleKey);
|
|
1494
|
+
perCompany.set(coKey, (perCompany.get(coKey) || 0) + 1);
|
|
1495
|
+
let title = titleRaw;
|
|
1489
1496
|
if (title.length > 34) title = title.slice(0, 33).trimEnd() + "\u2026";
|
|
1490
|
-
const company = titleCase(
|
|
1497
|
+
const company = titleCase(companyRaw);
|
|
1491
1498
|
const pct = Math.max(1, Math.min(99, Math.round((Number(m.score) || 0) * 100)));
|
|
1492
|
-
const
|
|
1499
|
+
const token = Buffer.from(String(m.id)).toString("base64url");
|
|
1500
|
+
const url = `${base}/j/${token}`;
|
|
1493
1501
|
out.push(`\u2197 ${title} @ ${company} \xB7 ${pct}% \u2014 ${url}`);
|
|
1494
1502
|
if (out.length >= max) break;
|
|
1495
1503
|
}
|
|
@@ -1822,7 +1830,7 @@ async function run() {
|
|
|
1822
1830
|
const fp = profileToFingerprint2(profile);
|
|
1823
1831
|
const results = match2(fp, jobs, jobs.length);
|
|
1824
1832
|
matchCount = results.length;
|
|
1825
|
-
topMatches = results.slice(0,
|
|
1833
|
+
topMatches = results.slice(0, 25).map((r) => ({
|
|
1826
1834
|
id: r.job.id,
|
|
1827
1835
|
title: r.job.title,
|
|
1828
1836
|
company: r.job.company,
|
|
@@ -1867,7 +1875,7 @@ async function run() {
|
|
|
1867
1875
|
const verbs = buildSpinnerPool2(ranked, sc.max, { sessionTags, frequency: sc.frequency });
|
|
1868
1876
|
if (verbs.length > 0) applySpinnerVerbs2(verbs, sc.mode);
|
|
1869
1877
|
else clearSpinnerVerbs2();
|
|
1870
|
-
const tips = buildTips2(ranked, API_URL,
|
|
1878
|
+
const tips = buildTips2(ranked, API_URL, 8);
|
|
1871
1879
|
if (tips.length > 0) applySpinnerTips2(tips);
|
|
1872
1880
|
else clearSpinnerTips2();
|
|
1873
1881
|
} else {
|