terminalhire 0.2.4 → 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.
@@ -2959,8 +2959,9 @@ __export(jpi_sync_exports, {
2959
2959
  });
2960
2960
  import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, existsSync as existsSync7, rmSync as rmSync2 } from "fs";
2961
2961
  import { join as join9 } from "path";
2962
- import { homedir as homedir7 } from "os";
2962
+ import { homedir as homedir7, hostname as osHostname } from "os";
2963
2963
  import { createInterface as createInterface4 } from "readline";
2964
+ import { spawn } from "child_process";
2964
2965
  function ask2(question) {
2965
2966
  const rl = createInterface4({ input: process.stdin, output: process.stdout });
2966
2967
  return new Promise((res) => {
@@ -3003,14 +3004,14 @@ function buildConsentFields(profile) {
3003
3004
  }
3004
3005
  return fields;
3005
3006
  }
3006
- function renderConsentCard(fields) {
3007
+ function renderPreview(fields) {
3007
3008
  console.log("");
3008
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");
3009
3010
  console.log("\u2502 terminalhire \u2014 sync your profile (Tier-1, opt-in) \u2502");
3010
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");
3011
3012
  console.log("");
3012
- console.log(" You are about to send the following data to");
3013
- 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:");
3014
3015
  console.log("");
3015
3016
  for (const f of fields) {
3016
3017
  const shown = Array.isArray(f.value) ? JSON.stringify(f.value) : String(f.value ?? "(not set)");
@@ -3029,6 +3030,30 @@ function renderConsentCard(fields) {
3029
3030
  console.log(" This is NOT required to use terminalhire.");
3030
3031
  console.log("");
3031
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
+ }
3032
3057
  async function runPush() {
3033
3058
  const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
3034
3059
  const profile = await readProfile2();
@@ -3040,15 +3065,91 @@ async function runPush() {
3040
3065
  process.exit(1);
3041
3066
  }
3042
3067
  const fields = buildConsentFields(profile);
3043
- renderConsentCard(fields);
3044
- let consentConfirmed = false;
3045
- const answer = await ask2(' Sync your profile to staqs (terminalhire.com)? Type "yes" to continue: ');
3046
- if (answer === "yes") {
3047
- consentConfirmed = true;
3048
- }
3049
- if (!consentConfirmed) {
3050
- console.log("\n Aborted \u2014 nothing was sent.\n");
3051
- 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);
3052
3153
  }
3053
3154
  const consentedAt = (/* @__PURE__ */ new Date()).toISOString();
3054
3155
  const consentToken = {
@@ -3067,14 +3168,14 @@ async function runPush() {
3067
3168
  };
3068
3169
  const priorMarker = readMarker();
3069
3170
  const rowToken = priorMarker && priorMarker.deleteToken ? priorMarker.deleteToken : null;
3070
- const requestBody = { consentToken, profile: payloadProfile };
3171
+ const requestBody = { consentToken, profile: payloadProfile, proofToken };
3071
3172
  if (rowToken) {
3072
3173
  requestBody.rowToken = rowToken;
3073
3174
  }
3074
- console.log("\n Sending one-time snapshot...");
3175
+ console.log("\n Verified. Sending one-time snapshot...");
3075
3176
  let res;
3076
3177
  try {
3077
- res = await fetch(`${API_URL2}/api/profile-sync`, {
3178
+ res = await fetch(`${SYNC_BASE}/api/profile-sync`, {
3078
3179
  method: "POST",
3079
3180
  headers: { "Content-Type": "application/json" },
3080
3181
  body: JSON.stringify(requestBody),
@@ -3097,7 +3198,7 @@ async function runPush() {
3097
3198
  console.error(" (Tier-1 sync is not enabled on the server yet.)");
3098
3199
  }
3099
3200
  if (res.status === 403) {
3100
- 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.)");
3101
3202
  }
3102
3203
  process.exit(1);
3103
3204
  }
@@ -3214,13 +3315,16 @@ async function run7() {
3214
3315
  console.log(" This is NOT required to use terminalhire.");
3215
3316
  console.log("");
3216
3317
  }
3217
- 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;
3218
3319
  var init_jpi_sync = __esm({
3219
3320
  "bin/jpi-sync.js"() {
3220
3321
  "use strict";
3221
3322
  TH_DIR3 = process.env["TERMINALHIRE_DIR"] || join9(homedir7(), ".terminalhire");
3222
3323
  TIER1_MARKER = join9(TH_DIR3, "tier1.json");
3223
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;
3224
3328
  CONSENT_VERSION = 1;
3225
3329
  }
3226
3330
  });
@@ -3234,7 +3338,7 @@ import { existsSync as existsSync8 } from "fs";
3234
3338
  import { join as join10, resolve } from "path";
3235
3339
  import { fileURLToPath as fileURLToPath3 } from "url";
3236
3340
  import { createInterface as createInterface5 } from "readline";
3237
- import { spawnSync, spawn } from "child_process";
3341
+ import { spawnSync, spawn as spawn2 } from "child_process";
3238
3342
  import { homedir as homedir8 } from "os";
3239
3343
  function ask3(question) {
3240
3344
  const rl = createInterface5({ input: process.stdin, output: process.stdout });
@@ -691,11 +691,15 @@ var init_profile = __esm({
691
691
  // bin/jpi-sync.js
692
692
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, rmSync } from "fs";
693
693
  import { join as join3 } from "path";
694
- import { homedir as homedir2 } from "os";
694
+ import { homedir as homedir2, hostname as osHostname } from "os";
695
695
  import { createInterface } from "readline";
696
+ import { spawn } from "child_process";
696
697
  var TH_DIR = process.env["TERMINALHIRE_DIR"] || join3(homedir2(), ".terminalhire");
697
698
  var TIER1_MARKER = join3(TH_DIR, "tier1.json");
698
699
  var API_URL = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
700
+ var SYNC_BASE = "https://www.terminalhire.com";
701
+ var POLL_INTERVAL_MS = 2e3;
702
+ var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
699
703
  var CONSENT_VERSION = 1;
700
704
  function ask(question) {
701
705
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -739,14 +743,14 @@ function buildConsentFields(profile) {
739
743
  }
740
744
  return fields;
741
745
  }
742
- function renderConsentCard(fields) {
746
+ function renderPreview(fields) {
743
747
  console.log("");
744
748
  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");
745
749
  console.log("\u2502 terminalhire \u2014 sync your profile (Tier-1, opt-in) \u2502");
746
750
  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");
747
751
  console.log("");
748
- console.log(" You are about to send the following data to");
749
- console.log(" staqs (terminalhire.com):");
752
+ console.log(" The following data will be shared with staqs (terminalhire.com)");
753
+ console.log(" AFTER you authorize + consent in the browser:");
750
754
  console.log("");
751
755
  for (const f of fields) {
752
756
  const shown = Array.isArray(f.value) ? JSON.stringify(f.value) : String(f.value ?? "(not set)");
@@ -765,6 +769,30 @@ function renderConsentCard(fields) {
765
769
  console.log(" This is NOT required to use terminalhire.");
766
770
  console.log("");
767
771
  }
772
+ function openInBrowser(url) {
773
+ let cmd;
774
+ let args;
775
+ if (process.platform === "darwin") {
776
+ cmd = "open";
777
+ args = [url];
778
+ } else if (process.platform === "win32") {
779
+ cmd = "cmd";
780
+ args = ["/c", "start", "", url];
781
+ } else {
782
+ cmd = "xdg-open";
783
+ args = [url];
784
+ }
785
+ try {
786
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
787
+ child.on("error", () => {
788
+ });
789
+ child.unref();
790
+ } catch {
791
+ }
792
+ }
793
+ function sleep(ms) {
794
+ return new Promise((res) => setTimeout(res, ms));
795
+ }
768
796
  async function runPush() {
769
797
  const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
770
798
  const profile = await readProfile2();
@@ -776,15 +804,91 @@ async function runPush() {
776
804
  process.exit(1);
777
805
  }
778
806
  const fields = buildConsentFields(profile);
779
- renderConsentCard(fields);
780
- let consentConfirmed = false;
781
- const answer = await ask(' Sync your profile to staqs (terminalhire.com)? Type "yes" to continue: ');
782
- if (answer === "yes") {
783
- consentConfirmed = true;
784
- }
785
- if (!consentConfirmed) {
786
- console.log("\n Aborted \u2014 nothing was sent.\n");
787
- process.exit(0);
807
+ renderPreview(fields);
808
+ await new Promise((resolve) => {
809
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
810
+ rl.question(
811
+ " Press Enter to open your browser to authorize + consent (or Ctrl-C to cancel)... ",
812
+ () => {
813
+ rl.close();
814
+ resolve();
815
+ }
816
+ );
817
+ });
818
+ console.log("");
819
+ console.log(" Starting browser verification...");
820
+ let begin;
821
+ try {
822
+ const r = await fetch(`${SYNC_BASE}/api/profile-sync/begin`, {
823
+ method: "POST",
824
+ headers: { "Content-Type": "application/json" },
825
+ body: JSON.stringify({ hostname: osHostname() }),
826
+ signal: AbortSignal.timeout(1e4)
827
+ });
828
+ if (!r.ok) {
829
+ let detail = "";
830
+ try {
831
+ detail = (await r.json())?.message || "";
832
+ } catch {
833
+ }
834
+ console.error(`
835
+ Could not start sync: /api/profile-sync/begin returned ${r.status}. ${detail}`);
836
+ if (r.status === 503) console.error(" (Tier-1 sync is not enabled on the server yet.)");
837
+ process.exit(1);
838
+ }
839
+ begin = await r.json();
840
+ } catch (err) {
841
+ console.error(`
842
+ Could not start sync: ${err instanceof Error ? err.message : String(err)}`);
843
+ process.exit(1);
844
+ }
845
+ const { challenge, verifyUrl } = begin || {};
846
+ if (!challenge || !verifyUrl) {
847
+ console.error("\n Could not start sync: malformed begin response.");
848
+ process.exit(1);
849
+ }
850
+ console.log("");
851
+ console.log(" Open this URL in your browser to authorize + consent:");
852
+ console.log(` ${verifyUrl}`);
853
+ console.log("");
854
+ console.log(" (Attempting to open it automatically...)");
855
+ openInBrowser(verifyUrl);
856
+ console.log(" Waiting for browser verification...");
857
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
858
+ let proofToken = null;
859
+ while (Date.now() < deadline) {
860
+ await sleep(POLL_INTERVAL_MS);
861
+ let statusRes;
862
+ try {
863
+ statusRes = await fetch(
864
+ `${SYNC_BASE}/api/profile-sync/status?challenge=${encodeURIComponent(challenge)}`,
865
+ { signal: AbortSignal.timeout(1e4) }
866
+ );
867
+ } catch {
868
+ continue;
869
+ }
870
+ if (!statusRes.ok) {
871
+ if (statusRes.status === 503) {
872
+ console.error("\n Tier-1 sync is not enabled on the server yet.\n");
873
+ process.exit(1);
874
+ }
875
+ continue;
876
+ }
877
+ let body;
878
+ try {
879
+ body = await statusRes.json();
880
+ } catch {
881
+ continue;
882
+ }
883
+ if (body && body.status === "verified" && body.proofToken) {
884
+ proofToken = body.proofToken;
885
+ break;
886
+ }
887
+ }
888
+ if (!proofToken) {
889
+ console.error("\n Timed out waiting for browser verification (10 min).");
890
+ console.error(" Re-run `terminalhire sync --push` to try again.\n");
891
+ process.exit(1);
788
892
  }
789
893
  const consentedAt = (/* @__PURE__ */ new Date()).toISOString();
790
894
  const consentToken = {
@@ -803,14 +907,14 @@ async function runPush() {
803
907
  };
804
908
  const priorMarker = readMarker();
805
909
  const rowToken = priorMarker && priorMarker.deleteToken ? priorMarker.deleteToken : null;
806
- const requestBody = { consentToken, profile: payloadProfile };
910
+ const requestBody = { consentToken, profile: payloadProfile, proofToken };
807
911
  if (rowToken) {
808
912
  requestBody.rowToken = rowToken;
809
913
  }
810
- console.log("\n Sending one-time snapshot...");
914
+ console.log("\n Verified. Sending one-time snapshot...");
811
915
  let res;
812
916
  try {
813
- res = await fetch(`${API_URL}/api/profile-sync`, {
917
+ res = await fetch(`${SYNC_BASE}/api/profile-sync`, {
814
918
  method: "POST",
815
919
  headers: { "Content-Type": "application/json" },
816
920
  body: JSON.stringify(requestBody),
@@ -833,7 +937,7 @@ async function runPush() {
833
937
  console.error(" (Tier-1 sync is not enabled on the server yet.)");
834
938
  }
835
939
  if (res.status === 403) {
836
- console.error(" (This GitHub login was already claimed by a different push.)");
940
+ console.error(" (Ownership proof rejected, expired, or already used \u2014 re-run sync --push.)");
837
941
  }
838
942
  process.exit(1);
839
943
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "terminalhire",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Local-first job matching for developers — ambient job matches in the Claude Code spinner. Matching runs on your machine; your profile never leaves it.",
5
5
  "repository": {
6
6
  "type": "git",