terminalhire 0.2.3 → 0.2.5

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.
@@ -747,16 +747,102 @@ var init_ashby = __esm({
747
747
  }
748
748
  });
749
749
 
750
- // ../../packages/core/src/feeds/himalayas.ts
750
+ // ../../packages/core/src/feeds/lever.ts
751
751
  function tokenize3(text) {
752
752
  return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
753
753
  }
754
- function extractTags3(job) {
754
+ function extractTags3(p) {
755
+ const cat = p.categories ?? {};
756
+ const texts = [
757
+ p.text,
758
+ cat.team ?? "",
759
+ cat.department ?? "",
760
+ cat.location ?? "",
761
+ ...cat.allLocations ?? [],
762
+ p.descriptionPlain ?? ""
763
+ ];
764
+ return normalize(texts.flatMap(tokenize3));
765
+ }
766
+ function mapCommitment(raw) {
767
+ if (!raw) return "full_time";
768
+ const lower = raw.toLowerCase();
769
+ if (lower.includes("contract") || lower.includes("contractor")) return "contract";
770
+ if (lower.includes("freelance")) return "freelance";
771
+ return "full_time";
772
+ }
773
+ function inferRemote3(p) {
774
+ if ((p.workplaceType ?? "").toLowerCase() === "remote") return true;
775
+ const cat = p.categories ?? {};
776
+ const haystack = [cat.location ?? "", ...cat.allLocations ?? []].join(" ").toLowerCase();
777
+ return haystack.includes("remote") || haystack.includes("anywhere");
778
+ }
779
+ function toIso(ms) {
780
+ if (typeof ms !== "number" || !Number.isFinite(ms)) return void 0;
781
+ try {
782
+ return new Date(ms).toISOString();
783
+ } catch {
784
+ return void 0;
785
+ }
786
+ }
787
+ async function fetchSlug3(slug) {
788
+ const url = `https://api.lever.co/v0/postings/${slug}?mode=json`;
789
+ const res = await fetch(url, { headers: { Accept: "application/json" } });
790
+ if (!res.ok) {
791
+ throw new Error(`Lever ${slug}: HTTP ${res.status}`);
792
+ }
793
+ const data = await res.json();
794
+ const postings = Array.isArray(data) ? data : [];
795
+ if (postings.length === 0) {
796
+ console.warn(`[lever] ${slug}: 0 jobs returned (board may be private or slug invalid)`);
797
+ } else {
798
+ console.info(`[lever] ${slug}: ${postings.length} jobs`);
799
+ }
800
+ return postings.filter((p) => p && p.id && p.text).map((p) => ({
801
+ id: `lever:${p.id}`,
802
+ source: "lever",
803
+ title: p.text,
804
+ company: slug,
805
+ url: p.hostedUrl ?? p.applyUrl ?? `https://jobs.lever.co/${slug}/${p.id}`,
806
+ remote: inferRemote3(p),
807
+ location: p.categories?.location,
808
+ tags: extractTags3(p),
809
+ roleType: mapCommitment(p.categories?.commitment),
810
+ postedAt: toIso(p.createdAt),
811
+ applyMode: "direct",
812
+ raw: p
813
+ }));
814
+ }
815
+ var lever;
816
+ var init_lever = __esm({
817
+ "../../packages/core/src/feeds/lever.ts"() {
818
+ "use strict";
819
+ init_vocabulary();
820
+ lever = {
821
+ source: "lever",
822
+ async fetch(opts) {
823
+ const slugs = opts?.slugs ?? [];
824
+ const results = await Promise.allSettled(slugs.map(fetchSlug3));
825
+ const jobs = [];
826
+ for (const r of results) {
827
+ if (r.status === "fulfilled") jobs.push(...r.value);
828
+ else console.warn("[lever] slug fetch rejected:", r.reason);
829
+ }
830
+ return jobs;
831
+ }
832
+ };
833
+ }
834
+ });
835
+
836
+ // ../../packages/core/src/feeds/himalayas.ts
837
+ function tokenize4(text) {
838
+ return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
839
+ }
840
+ function extractTags4(job) {
755
841
  const texts = [
756
842
  job.title,
757
843
  ...job.tags ?? []
758
844
  ];
759
- return normalize(texts.flatMap(tokenize3));
845
+ return normalize(texts.flatMap(tokenize4));
760
846
  }
761
847
  function mapJobType(raw) {
762
848
  if (!raw) return "full_time";
@@ -802,7 +888,7 @@ var init_himalayas = __esm({
802
888
  location: (j.locationRestrictions ?? []).join(", ") || "Remote",
803
889
  compMin: j.salaryMin,
804
890
  compMax: j.salaryMax,
805
- tags: extractTags3(j),
891
+ tags: extractTags4(j),
806
892
  roleType: mapJobType(j.jobType),
807
893
  postedAt: j.pubDate ?? j.createdAt,
808
894
  applyMode: "direct",
@@ -814,7 +900,7 @@ var init_himalayas = __esm({
814
900
  });
815
901
 
816
902
  // ../../packages/core/src/feeds/wwr.ts
817
- function tokenize4(text) {
903
+ function tokenize5(text) {
818
904
  return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
819
905
  }
820
906
  function stripHtml(html) {
@@ -856,9 +942,9 @@ function parseRss(xml) {
856
942
  }
857
943
  return items;
858
944
  }
859
- function extractTags4(item) {
945
+ function extractTags5(item) {
860
946
  const text = [item.title, item.category, stripHtml(item.description)].join(" ");
861
- return normalize(tokenize4(text));
947
+ return normalize(tokenize5(text));
862
948
  }
863
949
  var WWR_RSS_URL, wwr;
864
950
  var init_wwr = __esm({
@@ -887,7 +973,7 @@ var init_wwr = __esm({
887
973
  // WWR is a remote-only board
888
974
  remote: true,
889
975
  location: "Remote",
890
- tags: extractTags4(item),
976
+ tags: extractTags5(item),
891
977
  roleType: inferRoleType(item.category),
892
978
  postedAt: item.pubDate ? new Date(item.pubDate).toISOString() : void 0,
893
979
  applyMode: "direct",
@@ -899,7 +985,7 @@ var init_wwr = __esm({
899
985
  });
900
986
 
901
987
  // ../../packages/core/src/feeds/hn.ts
902
- function tokenize5(text) {
988
+ function tokenize6(text) {
903
989
  return text.toLowerCase().replace(/[^a-z0-9.\-+#]/g, " ").split(/\s+/).filter(Boolean);
904
990
  }
905
991
  function stripHtml2(html) {
@@ -909,7 +995,7 @@ function extractUrl(text) {
909
995
  const match2 = text.match(/https?:\/\/[^\s<>"']+/);
910
996
  return match2?.[0] ?? "";
911
997
  }
912
- function inferRemote3(text) {
998
+ function inferRemote4(text) {
913
999
  const lower = text.toLowerCase();
914
1000
  return lower.includes("remote") || lower.includes("anywhere") || lower.includes("distributed");
915
1001
  }
@@ -932,7 +1018,7 @@ function parseComment(item) {
932
1018
  return null;
933
1019
  }
934
1020
  const url = extractUrl(raw) || `https://news.ycombinator.com/item?id=${item.id}`;
935
- const tags = extractTags5(raw);
1021
+ const tags = extractTags6(raw);
936
1022
  if (tags.length === 0) return null;
937
1023
  return {
938
1024
  id: `hn:${item.id}`,
@@ -940,7 +1026,7 @@ function parseComment(item) {
940
1026
  title: title.slice(0, 120),
941
1027
  company: company.slice(0, 80),
942
1028
  url,
943
- remote: inferRemote3(raw),
1029
+ remote: inferRemote4(raw),
944
1030
  location: location || void 0,
945
1031
  tags,
946
1032
  roleType: inferRoleType2(raw),
@@ -949,8 +1035,8 @@ function parseComment(item) {
949
1035
  raw: item
950
1036
  };
951
1037
  }
952
- function extractTags5(text) {
953
- return normalize(tokenize5(text));
1038
+ function extractTags6(text) {
1039
+ return normalize(tokenize6(text));
954
1040
  }
955
1041
  var ALGOLIA_SEARCH, ALGOLIA_ITEMS, hn;
956
1042
  var init_hn = __esm({
@@ -994,20 +1080,25 @@ var init_hn = __esm({
994
1080
  });
995
1081
 
996
1082
  // ../../packages/core/src/feeds/index.ts
1083
+ function flattenTiers(t) {
1084
+ return [.../* @__PURE__ */ new Set([...t.bigco, ...t.scaleup, ...t.startup])];
1085
+ }
997
1086
  async function aggregate(opts) {
998
1087
  const ghSlugs = opts?.slugs?.["greenhouse"] ?? DEFAULT_GREENHOUSE_SLUGS;
999
1088
  const ashbySlugs = opts?.slugs?.["ashby"] ?? DEFAULT_ASHBY_SLUGS;
1089
+ const leverSlugs = opts?.slugs?.["lever"] ?? DEFAULT_LEVER_SLUGS;
1000
1090
  const limit = opts?.limit ?? 150;
1001
1091
  const settled = await Promise.allSettled([
1002
1092
  greenhouse.fetch({ slugs: ghSlugs, limit }),
1003
1093
  ashby.fetch({ slugs: ashbySlugs, limit }),
1094
+ lever.fetch({ slugs: leverSlugs, limit }),
1004
1095
  himalayas.fetch({ limit }),
1005
1096
  wwr.fetch({ limit }),
1006
1097
  hn.fetch({ limit })
1007
1098
  ]);
1008
1099
  const seen = /* @__PURE__ */ new Set();
1009
1100
  const jobs = [];
1010
- const sourceNames = ["greenhouse", "ashby", "himalayas", "wwr", "hn"];
1101
+ const sourceNames = ["greenhouse", "ashby", "lever", "himalayas", "wwr", "hn"];
1011
1102
  for (let i = 0; i < settled.length; i++) {
1012
1103
  const result = settled[i];
1013
1104
  if (result.status === "rejected") {
@@ -1023,46 +1114,122 @@ async function aggregate(opts) {
1023
1114
  }
1024
1115
  return jobs;
1025
1116
  }
1026
- var FEEDS, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS;
1117
+ var FEEDS, GREENHOUSE_SLUGS_BY_TIER, ASHBY_SLUGS_BY_TIER, LEVER_SLUGS_BY_TIER, DEFAULT_GREENHOUSE_SLUGS, DEFAULT_ASHBY_SLUGS, DEFAULT_LEVER_SLUGS;
1027
1118
  var init_feeds = __esm({
1028
1119
  "../../packages/core/src/feeds/index.ts"() {
1029
1120
  "use strict";
1030
1121
  init_greenhouse();
1031
1122
  init_ashby();
1123
+ init_lever();
1032
1124
  init_himalayas();
1033
1125
  init_wwr();
1034
1126
  init_hn();
1035
- FEEDS = [greenhouse, ashby, himalayas, wwr, hn];
1036
- DEFAULT_GREENHOUSE_SLUGS = [
1037
- "stripe",
1038
- "linear",
1039
- "vercel",
1040
- "ramp",
1041
- "notion",
1042
- "airbnb",
1043
- "anthropic",
1044
- "figma",
1045
- "discord",
1046
- "brex",
1047
- "mercury",
1048
- "retool",
1049
- "vanta",
1050
- "plaid",
1051
- "gusto",
1052
- "scale",
1053
- "databricks",
1054
- "coinbase",
1055
- "robinhood",
1056
- "doordash"
1057
- ];
1058
- DEFAULT_ASHBY_SLUGS = [
1059
- "ramp",
1060
- "notion",
1061
- "linear",
1062
- "vercel",
1063
- "replit",
1064
- "posthog"
1065
- ];
1127
+ FEEDS = [greenhouse, ashby, lever, himalayas, wwr, hn];
1128
+ GREENHOUSE_SLUGS_BY_TIER = {
1129
+ bigco: [
1130
+ "stripe",
1131
+ "anthropic",
1132
+ "figma",
1133
+ "discord",
1134
+ "brex",
1135
+ "mercury",
1136
+ "plaid",
1137
+ "gusto",
1138
+ "scale",
1139
+ "databricks",
1140
+ "coinbase",
1141
+ "robinhood",
1142
+ "doordash",
1143
+ "airbnb",
1144
+ "dropbox",
1145
+ "datadog",
1146
+ "cloudflare",
1147
+ "reddit",
1148
+ "lyft",
1149
+ "instacart"
1150
+ ],
1151
+ scaleup: [
1152
+ "samsara",
1153
+ "verkada",
1154
+ "affirm",
1155
+ "gitlab",
1156
+ "asana",
1157
+ "flexport",
1158
+ "faire",
1159
+ "twitch",
1160
+ "airtable",
1161
+ "retool"
1162
+ ],
1163
+ startup: [
1164
+ "watershed"
1165
+ ]
1166
+ };
1167
+ ASHBY_SLUGS_BY_TIER = {
1168
+ bigco: [
1169
+ "openai"
1170
+ ],
1171
+ scaleup: [
1172
+ "harvey",
1173
+ "elevenlabs",
1174
+ "notion",
1175
+ "sierra",
1176
+ "cohere",
1177
+ "ramp",
1178
+ "vanta",
1179
+ "decagon",
1180
+ "cursor",
1181
+ "replit",
1182
+ "perplexity",
1183
+ "baseten",
1184
+ "drata",
1185
+ "writer",
1186
+ "temporal",
1187
+ "supabase"
1188
+ ],
1189
+ startup: [
1190
+ "suno",
1191
+ "attio",
1192
+ "modal",
1193
+ "workos",
1194
+ "linear",
1195
+ "render",
1196
+ "warp",
1197
+ "plain",
1198
+ "posthog",
1199
+ "pylon",
1200
+ "resend",
1201
+ "langfuse",
1202
+ "railway",
1203
+ "mintlify",
1204
+ "neon",
1205
+ "browserbase",
1206
+ "knock",
1207
+ "speakeasy",
1208
+ "stytch",
1209
+ "runway",
1210
+ "doppler",
1211
+ "inngest",
1212
+ "hightouch",
1213
+ "zed"
1214
+ ]
1215
+ };
1216
+ LEVER_SLUGS_BY_TIER = {
1217
+ bigco: [
1218
+ "palantir",
1219
+ "spotify"
1220
+ ],
1221
+ scaleup: [
1222
+ "mistral",
1223
+ "ro",
1224
+ "secureframe"
1225
+ ],
1226
+ startup: [
1227
+ "anyscale"
1228
+ ]
1229
+ };
1230
+ DEFAULT_GREENHOUSE_SLUGS = flattenTiers(GREENHOUSE_SLUGS_BY_TIER);
1231
+ DEFAULT_ASHBY_SLUGS = flattenTiers(ASHBY_SLUGS_BY_TIER);
1232
+ DEFAULT_LEVER_SLUGS = flattenTiers(LEVER_SLUGS_BY_TIER);
1066
1233
  }
1067
1234
  });
1068
1235
 
@@ -1249,10 +1416,14 @@ var init_github = __esm({
1249
1416
  // ../../packages/core/src/index.ts
1250
1417
  var src_exports = {};
1251
1418
  __export(src_exports, {
1419
+ ASHBY_SLUGS_BY_TIER: () => ASHBY_SLUGS_BY_TIER,
1252
1420
  COASTAL_BUYER: () => COASTAL_BUYER,
1253
1421
  DEFAULT_ASHBY_SLUGS: () => DEFAULT_ASHBY_SLUGS,
1254
1422
  DEFAULT_GREENHOUSE_SLUGS: () => DEFAULT_GREENHOUSE_SLUGS,
1423
+ DEFAULT_LEVER_SLUGS: () => DEFAULT_LEVER_SLUGS,
1255
1424
  FEEDS: () => FEEDS,
1425
+ GREENHOUSE_SLUGS_BY_TIER: () => GREENHOUSE_SLUGS_BY_TIER,
1426
+ LEVER_SLUGS_BY_TIER: () => LEVER_SLUGS_BY_TIER,
1256
1427
  SYNONYMS: () => SYNONYMS,
1257
1428
  VOCABULARY: () => VOCABULARY,
1258
1429
  aggregate: () => aggregate,
@@ -1260,10 +1431,12 @@ __export(src_exports, {
1260
1431
  buildIndex: () => buildIndex,
1261
1432
  buildReason: () => buildReason,
1262
1433
  fetchGitHubProfile: () => fetchGitHubProfile,
1434
+ flattenTiers: () => flattenTiers,
1263
1435
  githubToFingerprint: () => githubToFingerprint,
1264
1436
  greenhouse: () => greenhouse,
1265
1437
  himalayas: () => himalayas,
1266
1438
  hn: () => hn,
1439
+ lever: () => lever,
1267
1440
  loadCoastalRoles: () => loadCoastalRoles,
1268
1441
  match: () => match,
1269
1442
  matchOne: () => matchOne,
@@ -2321,6 +2494,7 @@ __export(spinner_exports, {
2321
2494
  clearSpinnerVerbs: () => clearSpinnerVerbs,
2322
2495
  ctaVerb: () => ctaVerb,
2323
2496
  formatVerbs: () => formatVerbs,
2497
+ interleaveBySource: () => interleaveBySource,
2324
2498
  rankBySessionTags: () => rankBySessionTags,
2325
2499
  readSpinnerConfig: () => readSpinnerConfig
2326
2500
  });
@@ -2474,13 +2648,40 @@ function clearSpinnerVerbs() {
2474
2648
  }
2475
2649
  return { cleared: true, keptUserVerbs };
2476
2650
  }
2651
+ function interleaveBySource(topMatches) {
2652
+ if (!Array.isArray(topMatches) || topMatches.length === 0) return topMatches;
2653
+ const buckets = /* @__PURE__ */ new Map();
2654
+ const order = [];
2655
+ for (const m of topMatches) {
2656
+ const id = m && m.id ? String(m.id) : "";
2657
+ const idx = id.indexOf(":");
2658
+ const source = idx > 0 ? id.slice(0, idx) : "_";
2659
+ if (!buckets.has(source)) {
2660
+ buckets.set(source, []);
2661
+ order.push(source);
2662
+ }
2663
+ buckets.get(source).push(m);
2664
+ }
2665
+ const out = [];
2666
+ let remaining = topMatches.length;
2667
+ while (remaining > 0) {
2668
+ for (const source of order) {
2669
+ const b = buckets.get(source);
2670
+ if (b && b.length) {
2671
+ out.push(b.shift());
2672
+ remaining--;
2673
+ }
2674
+ }
2675
+ }
2676
+ return out;
2677
+ }
2477
2678
  function buildTips(topMatches, baseUrl, max = 8) {
2478
2679
  const base = String(baseUrl || "https://terminalhire.com").replace(/\/+$/, "");
2479
2680
  const out = [];
2480
2681
  const seenRole = /* @__PURE__ */ new Set();
2481
2682
  const perCompany = /* @__PURE__ */ new Map();
2482
2683
  const COMPANY_CAP = 2;
2483
- for (const m of Array.isArray(topMatches) ? topMatches : []) {
2684
+ for (const m of interleaveBySource(Array.isArray(topMatches) ? topMatches : [])) {
2484
2685
  if (!m || !m.title || !m.company || !m.id) continue;
2485
2686
  const idx = String(m.id).indexOf(":");
2486
2687
  if (idx <= 0) continue;
@@ -2758,8 +2959,9 @@ __export(jpi_sync_exports, {
2758
2959
  });
2759
2960
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync7, rmSync as rmSync2 } from "fs";
2760
2961
  import { join as join9 } from "path";
2761
- import { homedir as homedir7 } from "os";
2962
+ import { homedir as homedir7, hostname as osHostname } from "os";
2762
2963
  import { createInterface as createInterface4 } from "readline";
2964
+ import { spawn } from "child_process";
2763
2965
  function ask2(question) {
2764
2966
  const rl = createInterface4({ input: process.stdin, output: process.stdout });
2765
2967
  return new Promise((res) => {
@@ -2802,14 +3004,14 @@ function buildConsentFields(profile) {
2802
3004
  }
2803
3005
  return fields;
2804
3006
  }
2805
- function renderConsentCard(fields) {
3007
+ function renderPreview(fields) {
2806
3008
  console.log("");
2807
3009
  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
3010
  console.log("\u2502 terminalhire \u2014 sync your profile (Tier-1, opt-in) \u2502");
2809
3011
  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
3012
  console.log("");
2811
- console.log(" You are about to send the following data to");
2812
- console.log(" staqs (terminalhire.com):");
3013
+ console.log(" The following data will be shared with staqs (terminalhire.com)");
3014
+ console.log(" AFTER you authorize + consent in the browser:");
2813
3015
  console.log("");
2814
3016
  for (const f of fields) {
2815
3017
  const shown = Array.isArray(f.value) ? JSON.stringify(f.value) : String(f.value ?? "(not set)");
@@ -2828,6 +3030,30 @@ function renderConsentCard(fields) {
2828
3030
  console.log(" This is NOT required to use terminalhire.");
2829
3031
  console.log("");
2830
3032
  }
3033
+ function openInBrowser(url) {
3034
+ let cmd;
3035
+ let args2;
3036
+ if (process.platform === "darwin") {
3037
+ cmd = "open";
3038
+ args2 = [url];
3039
+ } else if (process.platform === "win32") {
3040
+ cmd = "cmd";
3041
+ args2 = ["/c", "start", "", url];
3042
+ } else {
3043
+ cmd = "xdg-open";
3044
+ args2 = [url];
3045
+ }
3046
+ try {
3047
+ const child = spawn(cmd, args2, { stdio: "ignore", detached: true });
3048
+ child.on("error", () => {
3049
+ });
3050
+ child.unref();
3051
+ } catch {
3052
+ }
3053
+ }
3054
+ function sleep2(ms) {
3055
+ return new Promise((res) => setTimeout(res, ms));
3056
+ }
2831
3057
  async function runPush() {
2832
3058
  const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
2833
3059
  const profile = await readProfile2();
@@ -2839,15 +3065,91 @@ async function runPush() {
2839
3065
  process.exit(1);
2840
3066
  }
2841
3067
  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);
3068
+ renderPreview(fields);
3069
+ await new Promise((resolve2) => {
3070
+ const rl = createInterface4({ input: process.stdin, output: process.stdout });
3071
+ rl.question(
3072
+ " Press Enter to open your browser to authorize + consent (or Ctrl-C to cancel)... ",
3073
+ () => {
3074
+ rl.close();
3075
+ resolve2();
3076
+ }
3077
+ );
3078
+ });
3079
+ console.log("");
3080
+ console.log(" Starting browser verification...");
3081
+ let begin;
3082
+ try {
3083
+ const r = await fetch(`${SYNC_BASE}/api/profile-sync/begin`, {
3084
+ method: "POST",
3085
+ headers: { "Content-Type": "application/json" },
3086
+ body: JSON.stringify({ hostname: osHostname() }),
3087
+ signal: AbortSignal.timeout(1e4)
3088
+ });
3089
+ if (!r.ok) {
3090
+ let detail = "";
3091
+ try {
3092
+ detail = (await r.json())?.message || "";
3093
+ } catch {
3094
+ }
3095
+ console.error(`
3096
+ Could not start sync: /api/profile-sync/begin returned ${r.status}. ${detail}`);
3097
+ if (r.status === 503) console.error(" (Tier-1 sync is not enabled on the server yet.)");
3098
+ process.exit(1);
3099
+ }
3100
+ begin = await r.json();
3101
+ } catch (err) {
3102
+ console.error(`
3103
+ Could not start sync: ${err instanceof Error ? err.message : String(err)}`);
3104
+ process.exit(1);
3105
+ }
3106
+ const { challenge, verifyUrl } = begin || {};
3107
+ if (!challenge || !verifyUrl) {
3108
+ console.error("\n Could not start sync: malformed begin response.");
3109
+ process.exit(1);
3110
+ }
3111
+ console.log("");
3112
+ console.log(" Open this URL in your browser to authorize + consent:");
3113
+ console.log(` ${verifyUrl}`);
3114
+ console.log("");
3115
+ console.log(" (Attempting to open it automatically...)");
3116
+ openInBrowser(verifyUrl);
3117
+ console.log(" Waiting for browser verification...");
3118
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
3119
+ let proofToken = null;
3120
+ while (Date.now() < deadline) {
3121
+ await sleep2(POLL_INTERVAL_MS);
3122
+ let statusRes;
3123
+ try {
3124
+ statusRes = await fetch(
3125
+ `${SYNC_BASE}/api/profile-sync/status?challenge=${encodeURIComponent(challenge)}`,
3126
+ { signal: AbortSignal.timeout(1e4) }
3127
+ );
3128
+ } catch {
3129
+ continue;
3130
+ }
3131
+ if (!statusRes.ok) {
3132
+ if (statusRes.status === 503) {
3133
+ console.error("\n Tier-1 sync is not enabled on the server yet.\n");
3134
+ process.exit(1);
3135
+ }
3136
+ continue;
3137
+ }
3138
+ let body;
3139
+ try {
3140
+ body = await statusRes.json();
3141
+ } catch {
3142
+ continue;
3143
+ }
3144
+ if (body && body.status === "verified" && body.proofToken) {
3145
+ proofToken = body.proofToken;
3146
+ break;
3147
+ }
3148
+ }
3149
+ if (!proofToken) {
3150
+ console.error("\n Timed out waiting for browser verification (10 min).");
3151
+ console.error(" Re-run `terminalhire sync --push` to try again.\n");
3152
+ process.exit(1);
2851
3153
  }
2852
3154
  const consentedAt = (/* @__PURE__ */ new Date()).toISOString();
2853
3155
  const consentToken = {
@@ -2866,14 +3168,14 @@ async function runPush() {
2866
3168
  };
2867
3169
  const priorMarker = readMarker();
2868
3170
  const rowToken = priorMarker && priorMarker.deleteToken ? priorMarker.deleteToken : null;
2869
- const requestBody = { consentToken, profile: payloadProfile };
3171
+ const requestBody = { consentToken, profile: payloadProfile, proofToken };
2870
3172
  if (rowToken) {
2871
3173
  requestBody.rowToken = rowToken;
2872
3174
  }
2873
- console.log("\n Sending one-time snapshot...");
3175
+ console.log("\n Verified. Sending one-time snapshot...");
2874
3176
  let res;
2875
3177
  try {
2876
- res = await fetch(`${API_URL2}/api/profile-sync`, {
3178
+ res = await fetch(`${SYNC_BASE}/api/profile-sync`, {
2877
3179
  method: "POST",
2878
3180
  headers: { "Content-Type": "application/json" },
2879
3181
  body: JSON.stringify(requestBody),
@@ -2896,7 +3198,7 @@ async function runPush() {
2896
3198
  console.error(" (Tier-1 sync is not enabled on the server yet.)");
2897
3199
  }
2898
3200
  if (res.status === 403) {
2899
- console.error(" (This GitHub login was already claimed by a different push.)");
3201
+ console.error(" (Ownership proof rejected, expired, or already used \u2014 re-run sync --push.)");
2900
3202
  }
2901
3203
  process.exit(1);
2902
3204
  }
@@ -3013,13 +3315,16 @@ async function run7() {
3013
3315
  console.log(" This is NOT required to use terminalhire.");
3014
3316
  console.log("");
3015
3317
  }
3016
- var TH_DIR3, TIER1_MARKER, API_URL2, CONSENT_VERSION;
3318
+ var TH_DIR3, TIER1_MARKER, API_URL2, SYNC_BASE, POLL_INTERVAL_MS, POLL_TIMEOUT_MS, CONSENT_VERSION;
3017
3319
  var init_jpi_sync = __esm({
3018
3320
  "bin/jpi-sync.js"() {
3019
3321
  "use strict";
3020
3322
  TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join9(homedir7(), ".terminalhire");
3021
3323
  TIER1_MARKER = join9(TH_DIR3, "tier1.json");
3022
3324
  API_URL2 = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
3325
+ SYNC_BASE = "https://www.terminalhire.com";
3326
+ POLL_INTERVAL_MS = 2e3;
3327
+ POLL_TIMEOUT_MS = 10 * 60 * 1e3;
3023
3328
  CONSENT_VERSION = 1;
3024
3329
  }
3025
3330
  });
@@ -3033,7 +3338,7 @@ import { existsSync as existsSync8 } from "fs";
3033
3338
  import { join as join10, resolve } from "path";
3034
3339
  import { fileURLToPath as fileURLToPath3 } from "url";
3035
3340
  import { createInterface as createInterface5 } from "readline";
3036
- import { spawnSync, spawn } from "child_process";
3341
+ import { spawnSync, spawn as spawn2 } from "child_process";
3037
3342
  import { homedir as homedir8 } from "os";
3038
3343
  function ask3(question) {
3039
3344
  const rl = createInterface5({ input: process.stdin, output: process.stdout });