terminalhire 0.5.0 → 0.7.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/dist/bin/jpi-bounties.js +286 -1
- package/dist/bin/jpi-claim.js +249 -10
- package/dist/bin/jpi-devs.js +3309 -0
- package/dist/bin/jpi-dispatch.js +1778 -296
- package/dist/bin/jpi-intro.js +1400 -0
- package/dist/bin/jpi-jobs.js +286 -1
- package/dist/bin/jpi-learn.js +27 -0
- package/dist/bin/jpi-login.js +286 -1
- package/dist/bin/jpi-profile.js +27 -0
- package/dist/bin/jpi-project.js +2995 -0
- package/dist/bin/jpi-refresh.js +286 -1
- package/dist/bin/jpi-save.js +27 -0
- package/dist/bin/jpi-sync.js +27 -0
- package/dist/bin/jpi-trajectory.js +1339 -75
- package/dist/src/intro.js +1348 -0
- package/dist/src/profile.js +12 -0
- package/dist/src/signal.js +12 -0
- package/dist/src/trajectory.js +1341 -74
- package/package.json +1 -1
package/dist/bin/jpi-bounties.js
CHANGED
|
@@ -680,6 +680,52 @@ function githubToFingerprint(p) {
|
|
|
680
680
|
const seniorityBand = inferSeniority(p);
|
|
681
681
|
return { skillTags, seniorityBand };
|
|
682
682
|
}
|
|
683
|
+
async function fetchOwnedRepoTraction(login, token) {
|
|
684
|
+
const computedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
685
|
+
const empty = (status) => ({
|
|
686
|
+
status,
|
|
687
|
+
totalStars: 0,
|
|
688
|
+
totalForks: 0,
|
|
689
|
+
reposWithStars: 0,
|
|
690
|
+
top: [],
|
|
691
|
+
computedAt
|
|
692
|
+
});
|
|
693
|
+
let repos;
|
|
694
|
+
try {
|
|
695
|
+
repos = await ghFetch(
|
|
696
|
+
`/users/${login}/repos?sort=pushed&per_page=100`,
|
|
697
|
+
token
|
|
698
|
+
);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
701
|
+
const status = /HTTP 403|HTTP 429|rate limit/i.test(msg) ? "rate-limited" : "failed";
|
|
702
|
+
console.warn(`[github] ${login}: traction fetch failed (${status}) \u2014`, msg);
|
|
703
|
+
return empty(status);
|
|
704
|
+
}
|
|
705
|
+
if (!Array.isArray(repos)) return empty("failed");
|
|
706
|
+
const owned = repos.filter((r) => r && !r.fork && !r.archived);
|
|
707
|
+
let totalStars = 0;
|
|
708
|
+
let totalForks = 0;
|
|
709
|
+
let reposWithStars = 0;
|
|
710
|
+
const ranked = [];
|
|
711
|
+
for (const r of owned) {
|
|
712
|
+
const stars = typeof r.stargazers_count === "number" ? r.stargazers_count : 0;
|
|
713
|
+
const forks = typeof r.forks_count === "number" ? r.forks_count : 0;
|
|
714
|
+
totalStars += stars;
|
|
715
|
+
totalForks += forks;
|
|
716
|
+
if (stars >= 1) reposWithStars++;
|
|
717
|
+
ranked.push({ name: r.name, stars, forks });
|
|
718
|
+
}
|
|
719
|
+
ranked.sort((a, b) => b.stars - a.stars || b.forks - a.forks);
|
|
720
|
+
return {
|
|
721
|
+
status: "ok",
|
|
722
|
+
totalStars,
|
|
723
|
+
totalForks,
|
|
724
|
+
reposWithStars,
|
|
725
|
+
top: ranked.slice(0, TRACTION_TOP_N),
|
|
726
|
+
computedAt
|
|
727
|
+
};
|
|
728
|
+
}
|
|
683
729
|
async function ghFetchRaw(path, token) {
|
|
684
730
|
return fetch(`https://api.github.com${path}`, { headers: ghHeaders(token) });
|
|
685
731
|
}
|
|
@@ -888,11 +934,12 @@ function deriveResumeTrend(cred, repoRecency, now = Date.now()) {
|
|
|
888
934
|
}
|
|
889
935
|
return scored.sort((a, b) => b.weight - a.weight).slice(0, 12).map((s) => s.t);
|
|
890
936
|
}
|
|
891
|
-
var MIN_STARS, MIN_CONTRIBUTORS, CANDIDATE_PR_PAGE, TRIVIAL_PR_TITLE, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE;
|
|
937
|
+
var TRACTION_TOP_N, MIN_STARS, MIN_CONTRIBUTORS, CANDIDATE_PR_PAGE, TRIVIAL_PR_TITLE, RESUME_DECAY_HALF_LIFE_MS, RESUME_MIN_SCORE;
|
|
892
938
|
var init_github = __esm({
|
|
893
939
|
"../../packages/core/src/github.ts"() {
|
|
894
940
|
"use strict";
|
|
895
941
|
init_vocabulary();
|
|
942
|
+
TRACTION_TOP_N = 6;
|
|
896
943
|
MIN_STARS = 50;
|
|
897
944
|
MIN_CONTRIBUTORS = 10;
|
|
898
945
|
CANDIDATE_PR_PAGE = 50;
|
|
@@ -2221,6 +2268,51 @@ var init_workable = __esm({
|
|
|
2221
2268
|
}
|
|
2222
2269
|
});
|
|
2223
2270
|
|
|
2271
|
+
// ../../packages/core/src/feeds/directory.ts
|
|
2272
|
+
function personCardToJob(row) {
|
|
2273
|
+
const tags = [...row.skill_tags];
|
|
2274
|
+
return {
|
|
2275
|
+
id: `dev:${row.login}`,
|
|
2276
|
+
source: "person",
|
|
2277
|
+
title: row.name ?? row.login,
|
|
2278
|
+
company: row.login,
|
|
2279
|
+
url: `/r/${row.login}`,
|
|
2280
|
+
remote: true,
|
|
2281
|
+
tags,
|
|
2282
|
+
coreTags: tags.slice(0, TOP_CORE_TAGS),
|
|
2283
|
+
roleType: "full_time",
|
|
2284
|
+
applyMode: "direct"
|
|
2285
|
+
};
|
|
2286
|
+
}
|
|
2287
|
+
function buildDirectoryIndex(people, opts) {
|
|
2288
|
+
return {
|
|
2289
|
+
builtAt: opts?.builtAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2290
|
+
cards: people.map(personCardToJob)
|
|
2291
|
+
};
|
|
2292
|
+
}
|
|
2293
|
+
function projectCardToJob(row) {
|
|
2294
|
+
const tags = [...row.needed_skills];
|
|
2295
|
+
return {
|
|
2296
|
+
id: `proj:${row.id}`,
|
|
2297
|
+
source: "project",
|
|
2298
|
+
title: row.title,
|
|
2299
|
+
company: row.owner_login,
|
|
2300
|
+
url: `/r/${row.owner_login}`,
|
|
2301
|
+
remote: true,
|
|
2302
|
+
tags,
|
|
2303
|
+
coreTags: tags.slice(0, TOP_CORE_TAGS),
|
|
2304
|
+
roleType: "full_time",
|
|
2305
|
+
applyMode: "direct"
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
var TOP_CORE_TAGS;
|
|
2309
|
+
var init_directory = __esm({
|
|
2310
|
+
"../../packages/core/src/feeds/directory.ts"() {
|
|
2311
|
+
"use strict";
|
|
2312
|
+
TOP_CORE_TAGS = 4;
|
|
2313
|
+
}
|
|
2314
|
+
});
|
|
2315
|
+
|
|
2224
2316
|
// ../../packages/core/src/feeds/index.ts
|
|
2225
2317
|
async function aggregateBounties(opts) {
|
|
2226
2318
|
const [gh, op] = await Promise.all([
|
|
@@ -2337,6 +2429,7 @@ var init_feeds = __esm({
|
|
|
2337
2429
|
init_github_bounties();
|
|
2338
2430
|
init_opire();
|
|
2339
2431
|
init_workable();
|
|
2432
|
+
init_directory();
|
|
2340
2433
|
init_bounty_gate();
|
|
2341
2434
|
init_bounty_gate();
|
|
2342
2435
|
FEEDS = [greenhouse, ashby, lever, workable, himalayas, wwr, hn];
|
|
@@ -2537,6 +2630,176 @@ var init_indexer = __esm({
|
|
|
2537
2630
|
}
|
|
2538
2631
|
});
|
|
2539
2632
|
|
|
2633
|
+
// ../../packages/core/src/intro.ts
|
|
2634
|
+
function buildIntroPayload(input) {
|
|
2635
|
+
const payload = {
|
|
2636
|
+
requesterLogin: input.requesterLogin,
|
|
2637
|
+
requesterDisplayName: input.requesterDisplayName,
|
|
2638
|
+
requesterContact: input.requesterContact,
|
|
2639
|
+
targetLogin: input.targetLogin
|
|
2640
|
+
};
|
|
2641
|
+
const note = input.note?.trim();
|
|
2642
|
+
if (note) payload.note = note;
|
|
2643
|
+
return payload;
|
|
2644
|
+
}
|
|
2645
|
+
function rejectExtraIntroFields(body) {
|
|
2646
|
+
for (const key of Object.keys(body)) {
|
|
2647
|
+
if (!INTRO_ALLOWED_SET.has(key)) {
|
|
2648
|
+
return `intro payload contains disallowed field: "${key}"`;
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
return null;
|
|
2652
|
+
}
|
|
2653
|
+
function validateIntroPayload(body) {
|
|
2654
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
2655
|
+
return { ok: false, reason: "intro payload must be a JSON object" };
|
|
2656
|
+
}
|
|
2657
|
+
const b = body;
|
|
2658
|
+
const extra = rejectExtraIntroFields(b);
|
|
2659
|
+
if (extra) return { ok: false, reason: extra };
|
|
2660
|
+
const ok = (v, max) => typeof v === "string" && v.trim().length > 0 && v.length <= max;
|
|
2661
|
+
if (!ok(b.requesterLogin, MAX_SHORT)) return { ok: false, reason: "requesterLogin is required" };
|
|
2662
|
+
if (!ok(b.requesterDisplayName, MAX_SHORT)) return { ok: false, reason: "requesterDisplayName is required" };
|
|
2663
|
+
if (!ok(b.requesterContact, MAX_SHORT)) return { ok: false, reason: "requesterContact is required" };
|
|
2664
|
+
if (!ok(b.targetLogin, MAX_SHORT)) return { ok: false, reason: "targetLogin is required" };
|
|
2665
|
+
if (b.note !== void 0 && (typeof b.note !== "string" || b.note.length > MAX_NOTE)) {
|
|
2666
|
+
return { ok: false, reason: "note must be a string of at most 500 chars" };
|
|
2667
|
+
}
|
|
2668
|
+
const value = {
|
|
2669
|
+
requesterLogin: b.requesterLogin.trim(),
|
|
2670
|
+
requesterDisplayName: b.requesterDisplayName.trim(),
|
|
2671
|
+
requesterContact: b.requesterContact.trim(),
|
|
2672
|
+
targetLogin: b.targetLogin.trim()
|
|
2673
|
+
};
|
|
2674
|
+
const note = typeof b.note === "string" ? b.note.trim() : "";
|
|
2675
|
+
if (note) value.note = note;
|
|
2676
|
+
return { ok: true, value };
|
|
2677
|
+
}
|
|
2678
|
+
function introRateLimitCheck(history, now, opts) {
|
|
2679
|
+
const cutoff = now - opts.windowMs;
|
|
2680
|
+
const recent = history.filter((t) => t > cutoff);
|
|
2681
|
+
if (recent.length >= opts.max) {
|
|
2682
|
+
const oldest = recent[0] ?? now;
|
|
2683
|
+
return { allowed: false, retained: recent, retryAfterMs: Math.max(0, oldest + opts.windowMs - now) };
|
|
2684
|
+
}
|
|
2685
|
+
return { allowed: true, retained: [...recent, now], retryAfterMs: 0 };
|
|
2686
|
+
}
|
|
2687
|
+
function isOverIntroLimit(recentCount, max) {
|
|
2688
|
+
return recentCount >= max;
|
|
2689
|
+
}
|
|
2690
|
+
function composeIntroEmail(args2) {
|
|
2691
|
+
const subject = `New intro request from @${args2.requesterLogin} \xB7 terminalhire`;
|
|
2692
|
+
const text = `@${args2.requesterLogin} wants an intro to you on terminalhire.
|
|
2693
|
+
|
|
2694
|
+
Sign in to view the request and choose whether to share your contact back:
|
|
2695
|
+
${args2.dashboardUrl}
|
|
2696
|
+
|
|
2697
|
+
You control whether this connects \u2014 no contact details are shared unless you accept.
|
|
2698
|
+
|
|
2699
|
+
\u2014 Terminalhire`;
|
|
2700
|
+
return { subject, text };
|
|
2701
|
+
}
|
|
2702
|
+
function introActorRole(intro, actorLogin) {
|
|
2703
|
+
const a = actorLogin.trim().toLowerCase();
|
|
2704
|
+
if (a && a === intro.targetLogin.trim().toLowerCase()) return "target";
|
|
2705
|
+
if (a && a === intro.requesterLogin.trim().toLowerCase()) return "requester";
|
|
2706
|
+
return "other";
|
|
2707
|
+
}
|
|
2708
|
+
function authorizeIntroDecision(intro, actorLogin) {
|
|
2709
|
+
const role = introActorRole(intro, actorLogin);
|
|
2710
|
+
if (role === "target") return { ok: true };
|
|
2711
|
+
if (role === "requester") {
|
|
2712
|
+
return { ok: false, status: 403, reason: "the requester cannot accept or decline their own intro request" };
|
|
2713
|
+
}
|
|
2714
|
+
return { ok: false, status: 404, reason: "intro not found" };
|
|
2715
|
+
}
|
|
2716
|
+
function authorizeIntroDeletion(intro, actorLogin) {
|
|
2717
|
+
const role = introActorRole(intro, actorLogin);
|
|
2718
|
+
if (role === "other") return { ok: false, status: 404, reason: "intro not found" };
|
|
2719
|
+
return { ok: true };
|
|
2720
|
+
}
|
|
2721
|
+
function revealIntroContacts(intro) {
|
|
2722
|
+
if (intro.status !== "accepted") return { toRequester: null, toTarget: null };
|
|
2723
|
+
return { toRequester: intro.targetContact ?? null, toTarget: intro.requesterContact };
|
|
2724
|
+
}
|
|
2725
|
+
function validateTargetContact(v) {
|
|
2726
|
+
if (typeof v !== "string" || v.trim().length === 0) return { ok: false, reason: "targetContact is required" };
|
|
2727
|
+
if (v.length > MAX_SHORT) return { ok: false, reason: `targetContact must be at most ${MAX_SHORT} chars` };
|
|
2728
|
+
return { ok: true, value: v.trim() };
|
|
2729
|
+
}
|
|
2730
|
+
function buildIntroListItem(intro, viewerLogin) {
|
|
2731
|
+
const role = introActorRole(intro, viewerLogin);
|
|
2732
|
+
if (role === "other") return null;
|
|
2733
|
+
const reveal = revealIntroContacts(intro);
|
|
2734
|
+
if (role === "target") {
|
|
2735
|
+
return {
|
|
2736
|
+
id: intro.id,
|
|
2737
|
+
role: "incoming",
|
|
2738
|
+
counterpartyLogin: intro.requesterLogin,
|
|
2739
|
+
status: intro.status,
|
|
2740
|
+
note: intro.note ?? null,
|
|
2741
|
+
contact: reveal.toTarget
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2744
|
+
return {
|
|
2745
|
+
id: intro.id,
|
|
2746
|
+
role: "outgoing",
|
|
2747
|
+
counterpartyLogin: intro.targetLogin,
|
|
2748
|
+
status: intro.status,
|
|
2749
|
+
note: intro.note ?? null,
|
|
2750
|
+
contact: reveal.toRequester
|
|
2751
|
+
};
|
|
2752
|
+
}
|
|
2753
|
+
function composeIntroAcceptedEmail(args2) {
|
|
2754
|
+
const subject = `Intro connected with @${args2.counterpartyLogin} \xB7 terminalhire`;
|
|
2755
|
+
const lead = args2.recipientRole === "requester" ? `@${args2.counterpartyLogin} accepted your intro request on terminalhire.` : `You accepted @${args2.counterpartyLogin}'s intro request on terminalhire.`;
|
|
2756
|
+
const text = `${lead}
|
|
2757
|
+
|
|
2758
|
+
You can now reach them directly:
|
|
2759
|
+
@${args2.counterpartyLogin} \u2014 ${args2.counterpartyContact}
|
|
2760
|
+
|
|
2761
|
+
Take it from here.
|
|
2762
|
+
|
|
2763
|
+
\u2014 Terminalhire`;
|
|
2764
|
+
return { subject, text };
|
|
2765
|
+
}
|
|
2766
|
+
function introRetentionAction(row, now) {
|
|
2767
|
+
if (row.status === "pending") {
|
|
2768
|
+
const created = Date.parse(row.createdAt);
|
|
2769
|
+
if (Number.isFinite(created) && now - created > INTRO_PENDING_TTL_MS) return "purge";
|
|
2770
|
+
return "keep";
|
|
2771
|
+
}
|
|
2772
|
+
if (row.status === "declined") {
|
|
2773
|
+
return row.hasContact ? "scrub-declined" : "keep";
|
|
2774
|
+
}
|
|
2775
|
+
if (row.status === "accepted") {
|
|
2776
|
+
const updated = Date.parse(row.updatedAt);
|
|
2777
|
+
if (row.hasContact && Number.isFinite(updated) && now - updated > INTRO_ACCEPTED_TTL_MS) {
|
|
2778
|
+
return "expire-accepted";
|
|
2779
|
+
}
|
|
2780
|
+
return "keep";
|
|
2781
|
+
}
|
|
2782
|
+
return "keep";
|
|
2783
|
+
}
|
|
2784
|
+
var INTRO_ALLOWED_FIELDS, INTRO_ALLOWED_SET, MAX_SHORT, MAX_NOTE, INTRO_PENDING_TTL_MS, INTRO_ACCEPTED_TTL_MS;
|
|
2785
|
+
var init_intro = __esm({
|
|
2786
|
+
"../../packages/core/src/intro.ts"() {
|
|
2787
|
+
"use strict";
|
|
2788
|
+
INTRO_ALLOWED_FIELDS = [
|
|
2789
|
+
"requesterLogin",
|
|
2790
|
+
"requesterDisplayName",
|
|
2791
|
+
"requesterContact",
|
|
2792
|
+
"note",
|
|
2793
|
+
"targetLogin"
|
|
2794
|
+
];
|
|
2795
|
+
INTRO_ALLOWED_SET = new Set(INTRO_ALLOWED_FIELDS);
|
|
2796
|
+
MAX_SHORT = 200;
|
|
2797
|
+
MAX_NOTE = 500;
|
|
2798
|
+
INTRO_PENDING_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2799
|
+
INTRO_ACCEPTED_TTL_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
2800
|
+
}
|
|
2801
|
+
});
|
|
2802
|
+
|
|
2540
2803
|
// ../../packages/core/src/index.ts
|
|
2541
2804
|
var src_exports = {};
|
|
2542
2805
|
__export(src_exports, {
|
|
@@ -2552,6 +2815,9 @@ __export(src_exports, {
|
|
|
2552
2815
|
GRAPH: () => GRAPH,
|
|
2553
2816
|
GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
|
|
2554
2817
|
IDF_BACKGROUND: () => IDF_BACKGROUND,
|
|
2818
|
+
INTRO_ACCEPTED_TTL_MS: () => INTRO_ACCEPTED_TTL_MS,
|
|
2819
|
+
INTRO_ALLOWED_FIELDS: () => INTRO_ALLOWED_FIELDS,
|
|
2820
|
+
INTRO_PENDING_TTL_MS: () => INTRO_PENDING_TTL_MS,
|
|
2555
2821
|
LEVER_SLUGS_BY_TIER: () => LEVER_SLUGS_BY_TIER,
|
|
2556
2822
|
SYNONYMS: () => SYNONYMS,
|
|
2557
2823
|
VOCABULARY: () => VOCABULARY,
|
|
@@ -2560,10 +2826,17 @@ __export(src_exports, {
|
|
|
2560
2826
|
aggregate: () => aggregate,
|
|
2561
2827
|
aggregateBounties: () => aggregateBounties,
|
|
2562
2828
|
ashby: () => ashby,
|
|
2829
|
+
authorizeIntroDecision: () => authorizeIntroDecision,
|
|
2830
|
+
authorizeIntroDeletion: () => authorizeIntroDeletion,
|
|
2563
2831
|
bestAcceptanceDomain: () => bestAcceptanceDomain,
|
|
2832
|
+
buildDirectoryIndex: () => buildDirectoryIndex,
|
|
2564
2833
|
buildGraph: () => buildGraph,
|
|
2565
2834
|
buildIndex: () => buildIndex,
|
|
2835
|
+
buildIntroListItem: () => buildIntroListItem,
|
|
2836
|
+
buildIntroPayload: () => buildIntroPayload,
|
|
2566
2837
|
buildReason: () => buildReason,
|
|
2838
|
+
composeIntroAcceptedEmail: () => composeIntroAcceptedEmail,
|
|
2839
|
+
composeIntroEmail: () => composeIntroEmail,
|
|
2567
2840
|
computeAcceptanceCredential: () => computeAcceptanceCredential,
|
|
2568
2841
|
computeAcceptanceCredentialPublic: () => computeAcceptanceCredentialPublic,
|
|
2569
2842
|
coreTagsFromTitle: () => coreTagsFromTitle,
|
|
@@ -2571,6 +2844,7 @@ __export(src_exports, {
|
|
|
2571
2844
|
expandWeighted: () => expandWeighted,
|
|
2572
2845
|
extractSkillTags: () => extractSkillTags,
|
|
2573
2846
|
fetchGitHubProfile: () => fetchGitHubProfile,
|
|
2847
|
+
fetchOwnedRepoTraction: () => fetchOwnedRepoTraction,
|
|
2574
2848
|
fetchRepoRecency: () => fetchRepoRecency,
|
|
2575
2849
|
flattenTiers: () => flattenTiers,
|
|
2576
2850
|
getBuyer: () => getBuyer,
|
|
@@ -2579,7 +2853,11 @@ __export(src_exports, {
|
|
|
2579
2853
|
greenhouse: () => greenhouse,
|
|
2580
2854
|
himalayas: () => himalayas,
|
|
2581
2855
|
hn: () => hn,
|
|
2856
|
+
introActorRole: () => introActorRole,
|
|
2857
|
+
introRateLimitCheck: () => introRateLimitCheck,
|
|
2858
|
+
introRetentionAction: () => introRetentionAction,
|
|
2582
2859
|
isBounty: () => isBounty,
|
|
2860
|
+
isOverIntroLimit: () => isOverIntroLimit,
|
|
2583
2861
|
lever: () => lever,
|
|
2584
2862
|
loadPartnerRoles: () => loadPartnerRoles,
|
|
2585
2863
|
looksLikeEngRole: () => looksLikeEngRole,
|
|
@@ -2587,8 +2865,14 @@ __export(src_exports, {
|
|
|
2587
2865
|
normalize: () => normalize,
|
|
2588
2866
|
opire: () => opire,
|
|
2589
2867
|
passesMaturityGate: () => passesMaturityGate,
|
|
2868
|
+
personCardToJob: () => personCardToJob,
|
|
2869
|
+
projectCardToJob: () => projectCardToJob,
|
|
2870
|
+
rejectExtraIntroFields: () => rejectExtraIntroFields,
|
|
2871
|
+
revealIntroContacts: () => revealIntroContacts,
|
|
2590
2872
|
tokenize: () => tokenize,
|
|
2591
2873
|
validateGraph: () => validateGraph,
|
|
2874
|
+
validateIntroPayload: () => validateIntroPayload,
|
|
2875
|
+
validateTargetContact: () => validateTargetContact,
|
|
2592
2876
|
workable: () => workable,
|
|
2593
2877
|
wwr: () => wwr
|
|
2594
2878
|
});
|
|
@@ -2602,6 +2886,7 @@ var init_src = __esm({
|
|
|
2602
2886
|
init_indexer();
|
|
2603
2887
|
init_partners();
|
|
2604
2888
|
init_github();
|
|
2889
|
+
init_intro();
|
|
2605
2890
|
}
|
|
2606
2891
|
});
|
|
2607
2892
|
|
package/dist/bin/jpi-claim.js
CHANGED
|
@@ -106,10 +106,56 @@ var init_claims = __esm({
|
|
|
106
106
|
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
107
107
|
import { join as join2 } from "path";
|
|
108
108
|
import { homedir as homedir2 } from "os";
|
|
109
|
+
import { execFile } from "child_process";
|
|
110
|
+
import { promisify } from "util";
|
|
111
|
+
import { createInterface } from "readline";
|
|
109
112
|
var TERMINALHIRE_DIR2 = join2(homedir2(), ".terminalhire");
|
|
110
113
|
var INDEX_CACHE_FILE = join2(TERMINALHIRE_DIR2, "index-cache.json");
|
|
111
114
|
var GH_API = "https://api.github.com";
|
|
112
115
|
var GH_HEADERS = { "User-Agent": "terminalhire-claim", Accept: "application/vnd.github+json" };
|
|
116
|
+
var pExecFile = promisify(execFile);
|
|
117
|
+
async function sh(cmd, args, opts = {}) {
|
|
118
|
+
const { stdout } = await pExecFile(cmd, args, { ...opts, shell: false, maxBuffer: 16 * 1024 * 1024 });
|
|
119
|
+
return String(stdout).trim();
|
|
120
|
+
}
|
|
121
|
+
async function confirm(question) {
|
|
122
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
123
|
+
try {
|
|
124
|
+
const ans = await new Promise((resolve) => rl.question(question, resolve));
|
|
125
|
+
return /^y(es)?$/i.test(String(ans).trim());
|
|
126
|
+
} finally {
|
|
127
|
+
rl.close();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["worktree", "branch"]);
|
|
131
|
+
function parseArgs(argv) {
|
|
132
|
+
const flags = {};
|
|
133
|
+
const positional = [];
|
|
134
|
+
for (let i = 0; i < argv.length; i++) {
|
|
135
|
+
const a = argv[i];
|
|
136
|
+
if (a.startsWith("--")) {
|
|
137
|
+
const key = a.slice(2);
|
|
138
|
+
if (VALUE_FLAGS.has(key)) {
|
|
139
|
+
const val = argv[i + 1];
|
|
140
|
+
if (val === void 0 || val.startsWith("--")) {
|
|
141
|
+
console.error(`terminalhire claim: --${key} requires a value.`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
flags[key] = val;
|
|
145
|
+
i++;
|
|
146
|
+
} else {
|
|
147
|
+
flags[key] = true;
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
positional.push(a);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { flags, positional };
|
|
154
|
+
}
|
|
155
|
+
function parseRepoFromRemote(url) {
|
|
156
|
+
const m = String(url ?? "").trim().match(/github\.com[:/]+([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
157
|
+
return m ? `${m[1]}/${m[2]}` : null;
|
|
158
|
+
}
|
|
113
159
|
function findBountyInCache(bountyId) {
|
|
114
160
|
try {
|
|
115
161
|
if (!existsSync2(INDEX_CACHE_FILE)) return null;
|
|
@@ -264,7 +310,10 @@ async function cmdRecord(arg) {
|
|
|
264
310
|
console.log(" \u2022 MUST NOT `git push` or `gh pr` \u2014 pushing happens only via `terminalhire submit`");
|
|
265
311
|
console.log(" \u2022 clone + static analysis + patch only; NO test/build execution without explicit approval");
|
|
266
312
|
console.log(" \u2022 no access to ~/.terminalhire (the executor never needs your profile)");
|
|
267
|
-
console.log("\n Next:
|
|
313
|
+
console.log("\n Next:");
|
|
314
|
+
console.log(" 1. record the worktree: terminalhire claim attach " + claim.id + " --worktree <path> --branch <branch>");
|
|
315
|
+
console.log(" 2. do the work + review, then mark it cleared: terminalhire claim update " + claim.id + " ready");
|
|
316
|
+
console.log(" 3. publish (pushes to your fork + opens the PR): terminalhire claim submit " + claim.id);
|
|
268
317
|
}
|
|
269
318
|
async function cmdPreview(arg, { json } = {}) {
|
|
270
319
|
if (!arg) {
|
|
@@ -390,33 +439,223 @@ async function cmdRelease(id) {
|
|
|
390
439
|
console.log(removed ? `Released claim: ${id}` : `terminalhire claim: no claim with id '${id}'.`);
|
|
391
440
|
if (!removed) process.exit(1);
|
|
392
441
|
}
|
|
442
|
+
async function cmdAttach(id, worktree, branch) {
|
|
443
|
+
const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
|
|
444
|
+
if (!id || !worktree || !branch) {
|
|
445
|
+
console.error("Usage: terminalhire claim attach <id> --worktree <path> --branch <branchName>");
|
|
446
|
+
console.error(" Records the worktree + branch so `terminalhire claim submit` can verify identity before pushing.");
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
if (!claims.findClaim(id)) {
|
|
450
|
+
console.error(`terminalhire claim: no claim with id '${id}'.`);
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
let toplevel;
|
|
454
|
+
try {
|
|
455
|
+
toplevel = await sh("git", ["-C", worktree, "rev-parse", "--show-toplevel"]);
|
|
456
|
+
} catch {
|
|
457
|
+
console.error(`terminalhire claim: '${worktree}' is not a git work tree.`);
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
claims.updateClaim(id, { worktreePath: toplevel, branch });
|
|
461
|
+
console.log(`Attached ${id}: worktree=${toplevel} branch=${branch}`);
|
|
462
|
+
}
|
|
463
|
+
async function cmdSubmit(id, worktreeOverride) {
|
|
464
|
+
const claims = await Promise.resolve().then(() => (init_claims(), claims_exports));
|
|
465
|
+
if (!id) {
|
|
466
|
+
console.error("Usage: terminalhire claim submit <id> [--worktree <path>]");
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
const claim = claims.findClaim(id);
|
|
470
|
+
if (!claim) {
|
|
471
|
+
console.error(`terminalhire claim: no claim with id '${id}'.`);
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
if (claim.state !== "ready") {
|
|
475
|
+
console.error(
|
|
476
|
+
`terminalhire claim: ${id} is '${claim.state}', not 'ready'. Submit only runs after the review gate clears it:
|
|
477
|
+
terminalhire claim update ${id} ready`
|
|
478
|
+
);
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
if (claim.review && claim.review.verdict === "revise") {
|
|
482
|
+
console.error(`terminalhire claim: ${id} review verdict is 'revise' \u2014 the gate said do not submit. Resolve blockers and re-review first.`);
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
if (!claim.worktreePath || !claim.branch) {
|
|
486
|
+
console.error(
|
|
487
|
+
`terminalhire claim: ${id} has no recorded worktree/branch \u2014 cannot verify what to push. Run:
|
|
488
|
+
terminalhire claim attach ${id} --worktree <path> --branch <branch>`
|
|
489
|
+
);
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
const inspectDir = worktreeOverride || process.cwd();
|
|
493
|
+
let toplevel;
|
|
494
|
+
try {
|
|
495
|
+
toplevel = await sh("git", ["-C", inspectDir, "rev-parse", "--show-toplevel"]);
|
|
496
|
+
} catch {
|
|
497
|
+
console.error(
|
|
498
|
+
`terminalhire claim: '${inspectDir}' is not a git work tree.
|
|
499
|
+
Run submit from inside the claim's worktree (or pass --worktree <path>).`
|
|
500
|
+
);
|
|
501
|
+
process.exit(1);
|
|
502
|
+
}
|
|
503
|
+
if (toplevel !== claim.worktreePath) {
|
|
504
|
+
console.error(
|
|
505
|
+
`terminalhire claim: worktree mismatch \u2014 refusing to push.
|
|
506
|
+
expected: ${claim.worktreePath}
|
|
507
|
+
found: ${toplevel}
|
|
508
|
+
Run submit from inside the claim's worktree (or pass --worktree <path>).`
|
|
509
|
+
);
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
const wt = toplevel;
|
|
513
|
+
const curBranch = await sh("git", ["-C", wt, "rev-parse", "--abbrev-ref", "HEAD"]);
|
|
514
|
+
if (curBranch !== claim.branch) {
|
|
515
|
+
console.error(
|
|
516
|
+
`terminalhire claim: branch mismatch \u2014 refusing to push.
|
|
517
|
+
expected: ${claim.branch}
|
|
518
|
+
found: ${curBranch}
|
|
519
|
+
Check out the claim's branch first.`
|
|
520
|
+
);
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
let defaultBranch = null;
|
|
524
|
+
try {
|
|
525
|
+
defaultBranch = (await sh("git", ["-C", wt, "symbolic-ref", "--short", "refs/remotes/origin/HEAD"])).replace(/^origin\//, "");
|
|
526
|
+
} catch {
|
|
527
|
+
}
|
|
528
|
+
if (defaultBranch && claim.branch === defaultBranch) {
|
|
529
|
+
console.error(`terminalhire claim: '${claim.branch}' is the default branch \u2014 open the PR from a feature branch.`);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
if (defaultBranch) {
|
|
533
|
+
let ahead = "0";
|
|
534
|
+
try {
|
|
535
|
+
ahead = await sh("git", ["-C", wt, "rev-list", "--count", `origin/${defaultBranch}..HEAD`]);
|
|
536
|
+
} catch {
|
|
537
|
+
ahead = "1";
|
|
538
|
+
}
|
|
539
|
+
if (ahead === "0") {
|
|
540
|
+
console.error(`terminalhire claim: branch has no commits ahead of origin/${defaultBranch} \u2014 nothing to submit.`);
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const dirty = await sh("git", ["-C", wt, "status", "--porcelain"]);
|
|
545
|
+
if (dirty) {
|
|
546
|
+
console.error("terminalhire claim: working tree is not clean \u2014 commit or stash before submitting (submit pushes what was reviewed).");
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
let originUrl;
|
|
550
|
+
try {
|
|
551
|
+
originUrl = await sh("git", ["-C", wt, "remote", "get-url", "origin"]);
|
|
552
|
+
} catch {
|
|
553
|
+
console.error("terminalhire claim: no 'origin' remote in the worktree.");
|
|
554
|
+
process.exit(1);
|
|
555
|
+
}
|
|
556
|
+
const originRepo = parseRepoFromRemote(originUrl);
|
|
557
|
+
if (!originRepo) {
|
|
558
|
+
console.error(`terminalhire claim: could not parse owner/repo from origin (${originUrl}).`);
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
if (originRepo.toLowerCase() === claim.repoFullName.toLowerCase()) {
|
|
562
|
+
console.error(
|
|
563
|
+
`terminalhire claim: origin points at the UPSTREAM bounty repo (${claim.repoFullName}), not a fork.
|
|
564
|
+
Pushing would create a branch directly on the target repo. Fork first:
|
|
565
|
+
gh repo fork ${claim.repoFullName} --clone=false
|
|
566
|
+
set your fork as 'origin' (or push it there), then retry.`
|
|
567
|
+
);
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
let ghUser;
|
|
571
|
+
try {
|
|
572
|
+
ghUser = await sh("gh", ["api", "user", "-q", ".login"]);
|
|
573
|
+
} catch {
|
|
574
|
+
console.error("terminalhire claim: 'gh' CLI not available or not authenticated. Run 'gh auth login'.");
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
const upstream = claim.repoFullName;
|
|
578
|
+
const head = `${ghUser}:${claim.branch}`;
|
|
579
|
+
console.log(`
|
|
580
|
+
SUBMIT \xB7 ${claim.title}`);
|
|
581
|
+
console.log(` upstream: ${upstream}`);
|
|
582
|
+
console.log(` fork: ${originRepo}`);
|
|
583
|
+
console.log(` branch: ${claim.branch}`);
|
|
584
|
+
console.log(` head: ${head}`);
|
|
585
|
+
console.log(` issue: ${claim.issueUrl}`);
|
|
586
|
+
const ok = await confirm(`
|
|
587
|
+
Push '${claim.branch}' to ${originRepo} and open a PR against ${upstream}? (y/N) `);
|
|
588
|
+
if (!ok) {
|
|
589
|
+
console.log("Aborted \u2014 nothing pushed.");
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
try {
|
|
593
|
+
await sh("git", ["-C", wt, "push", "origin", claim.branch]);
|
|
594
|
+
} catch (err) {
|
|
595
|
+
console.error(`terminalhire claim: git push failed (NOT force-pushed). ${err.stderr || err.message || err}`);
|
|
596
|
+
console.error(` Resolve and retry, or open the PR manually then: terminalhire claim update ${id} submitted <prUrl>`);
|
|
597
|
+
process.exit(1);
|
|
598
|
+
}
|
|
599
|
+
let prUrl = null;
|
|
600
|
+
try {
|
|
601
|
+
const existing = await sh("gh", ["pr", "list", "--repo", upstream, "--head", head, "--state", "open", "--json", "url", "-q", ".[0].url // empty"]);
|
|
602
|
+
if (existing) prUrl = existing;
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
if (!prUrl) {
|
|
606
|
+
const issueNum = (parseGitHubUrl(claim.issueUrl) || {}).number;
|
|
607
|
+
const body = issueNum ? `Closes #${issueNum}` : "";
|
|
608
|
+
try {
|
|
609
|
+
const out = await sh("gh", ["pr", "create", "--repo", upstream, "--head", head, "--title", claim.title, "--body", body]);
|
|
610
|
+
prUrl = out.split("\n").map((l) => l.trim()).filter((l) => l.startsWith("https://github.com/")).pop() || null;
|
|
611
|
+
} catch (err) {
|
|
612
|
+
console.error(`terminalhire claim: branch pushed, but 'gh pr create' failed. ${err.stderr || err.message || err}`);
|
|
613
|
+
console.error(` Open the PR manually (gh pr create / web UI), then: terminalhire claim update ${id} submitted <prUrl>`);
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (!prUrl || !parseGitHubUrl(prUrl)) {
|
|
618
|
+
console.error(`terminalhire claim: could not determine the PR URL. Set it manually: terminalhire claim update ${id} submitted <prUrl>`);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
claims.updateClaim(id, { state: "submitted", prUrl });
|
|
622
|
+
console.log(`
|
|
623
|
+
\u2713 Submitted ${id} \u2192 ${prUrl}`);
|
|
624
|
+
console.log(` Run 'terminalhire claim status ${id}' after the maintainer acts to fold the merge into your accepted-PR rate.`);
|
|
625
|
+
}
|
|
393
626
|
async function run() {
|
|
394
627
|
const verb = process.argv[2];
|
|
395
|
-
const
|
|
396
|
-
const active =
|
|
397
|
-
const json =
|
|
628
|
+
const { flags, positional } = parseArgs(process.argv.slice(3));
|
|
629
|
+
const active = Boolean(flags.active);
|
|
630
|
+
const json = Boolean(flags.json);
|
|
398
631
|
try {
|
|
399
632
|
switch (verb) {
|
|
400
633
|
case "preview":
|
|
401
|
-
await cmdPreview(
|
|
634
|
+
await cmdPreview(positional[0], { json });
|
|
402
635
|
break;
|
|
403
636
|
case "record":
|
|
404
|
-
await cmdRecord(
|
|
637
|
+
await cmdRecord(positional[0]);
|
|
405
638
|
break;
|
|
406
639
|
case "list":
|
|
407
640
|
await cmdList(active);
|
|
408
641
|
break;
|
|
409
642
|
case "status":
|
|
410
|
-
await cmdStatus(
|
|
643
|
+
await cmdStatus(positional[0]);
|
|
411
644
|
break;
|
|
412
645
|
case "update":
|
|
413
|
-
await cmdUpdate(
|
|
646
|
+
await cmdUpdate(positional[0], positional[1], positional[2]);
|
|
647
|
+
break;
|
|
648
|
+
case "attach":
|
|
649
|
+
await cmdAttach(positional[0], flags.worktree, flags.branch);
|
|
650
|
+
break;
|
|
651
|
+
case "submit":
|
|
652
|
+
await cmdSubmit(positional[0], flags.worktree);
|
|
414
653
|
break;
|
|
415
654
|
case "release":
|
|
416
|
-
await cmdRelease(
|
|
655
|
+
await cmdRelease(positional[0]);
|
|
417
656
|
break;
|
|
418
657
|
default:
|
|
419
|
-
console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: preview | record | list | status | update | release`);
|
|
658
|
+
console.error(`terminalhire claim: unknown verb '${verb ?? ""}'. Expected: preview | record | attach | list | status | update | submit | release`);
|
|
420
659
|
process.exit(1);
|
|
421
660
|
}
|
|
422
661
|
} catch (err) {
|