terminalhire 0.2.0 → 0.2.3

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.
@@ -0,0 +1,837 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // ../../packages/core/src/types.ts
13
+ var init_types = __esm({
14
+ "../../packages/core/src/types.ts"() {
15
+ "use strict";
16
+ }
17
+ });
18
+
19
+ // ../../packages/core/src/vocabulary.ts
20
+ function normalize(tokens) {
21
+ const result = /* @__PURE__ */ new Set();
22
+ for (const raw of tokens) {
23
+ const lower = raw.toLowerCase().trim();
24
+ if (VOCAB_SET.has(lower)) {
25
+ result.add(lower);
26
+ continue;
27
+ }
28
+ const mapped = SYNONYMS[lower];
29
+ if (mapped && VOCAB_SET.has(mapped)) {
30
+ result.add(mapped);
31
+ }
32
+ }
33
+ return Array.from(result);
34
+ }
35
+ var VOCABULARY, SYNONYMS, VOCAB_SET;
36
+ var init_vocabulary = __esm({
37
+ "../../packages/core/src/vocabulary.ts"() {
38
+ "use strict";
39
+ VOCABULARY = [
40
+ // Languages
41
+ "typescript",
42
+ "javascript",
43
+ "python",
44
+ "go",
45
+ "rust",
46
+ "java",
47
+ "ruby",
48
+ "elixir",
49
+ "scala",
50
+ "kotlin",
51
+ "swift",
52
+ "cpp",
53
+ "csharp",
54
+ "php",
55
+ "haskell",
56
+ "clojure",
57
+ "r",
58
+ // Frontend frameworks / libs
59
+ "react",
60
+ "nextjs",
61
+ "vue",
62
+ "nuxt",
63
+ "svelte",
64
+ "angular",
65
+ "solidjs",
66
+ "tailwind",
67
+ "css",
68
+ "html",
69
+ "graphql",
70
+ "trpc",
71
+ // Backend frameworks
72
+ "nodejs",
73
+ "express",
74
+ "fastify",
75
+ "nestjs",
76
+ "django",
77
+ "fastapi",
78
+ "flask",
79
+ "rails",
80
+ "spring",
81
+ "actix",
82
+ "gin",
83
+ "phoenix",
84
+ "laravel",
85
+ "dotnet",
86
+ // Infrastructure & DevOps
87
+ "kubernetes",
88
+ "docker",
89
+ "terraform",
90
+ "aws",
91
+ "gcp",
92
+ "azure",
93
+ "ci-cd",
94
+ "github-actions",
95
+ "linux",
96
+ "nginx",
97
+ "pulumi",
98
+ "ansible",
99
+ "prometheus",
100
+ "grafana",
101
+ "datadog",
102
+ "opentelemetry",
103
+ // Data & ML
104
+ "postgresql",
105
+ "mysql",
106
+ "sqlite",
107
+ "mongodb",
108
+ "redis",
109
+ "elasticsearch",
110
+ "kafka",
111
+ "rabbitmq",
112
+ "data-engineering",
113
+ "spark",
114
+ "airflow",
115
+ "dbt",
116
+ "ml",
117
+ "llm",
118
+ "pytorch",
119
+ "tensorflow",
120
+ "pandas",
121
+ "numpy",
122
+ // Domains / capabilities
123
+ "oauth",
124
+ "authentication",
125
+ "security",
126
+ "payments",
127
+ "billing",
128
+ "frontend",
129
+ "backend",
130
+ "devops",
131
+ "mobile",
132
+ "ios",
133
+ "android",
134
+ "api-design",
135
+ "microservices",
136
+ "websockets",
137
+ "testing",
138
+ "accessibility",
139
+ "seo",
140
+ "performance",
141
+ "observability",
142
+ "search",
143
+ "realtime"
144
+ ];
145
+ SYNONYMS = {
146
+ // Kubernetes aliases
147
+ "k8s": "kubernetes",
148
+ "kube": "kubernetes",
149
+ // Auth / identity
150
+ "passport": "authentication",
151
+ "oauth2": "oauth",
152
+ "oidc": "oauth",
153
+ "jwt": "authentication",
154
+ "saml": "authentication",
155
+ "auth0": "authentication",
156
+ "clerk": "authentication",
157
+ "nextauth": "authentication",
158
+ // Payments
159
+ "@stripe/stripe-js": "payments",
160
+ "stripe": "payments",
161
+ "braintree": "payments",
162
+ "paddle": "payments",
163
+ "lemonsqueezy": "payments",
164
+ "recurly": "billing",
165
+ "chargebee": "billing",
166
+ // Framework / lib aliases
167
+ "next": "nextjs",
168
+ "next.js": "nextjs",
169
+ "nuxt.js": "nuxt",
170
+ "vue.js": "vue",
171
+ "angular.js": "angular",
172
+ "angularjs": "angular",
173
+ "express.js": "express",
174
+ "expressjs": "express",
175
+ "fastapi": "fastapi",
176
+ "nest": "nestjs",
177
+ "nest.js": "nestjs",
178
+ "sveltekit": "svelte",
179
+ // Language aliases
180
+ "ts": "typescript",
181
+ "js": "javascript",
182
+ "py": "python",
183
+ "golang": "go",
184
+ "c++": "cpp",
185
+ "c#": "csharp",
186
+ ".net": "dotnet",
187
+ "asp.net": "dotnet",
188
+ // DB aliases
189
+ "postgres": "postgresql",
190
+ "pg": "postgresql",
191
+ "mongo": "mongodb",
192
+ "elastic": "elasticsearch",
193
+ // Cloud aliases
194
+ "amazon web services": "aws",
195
+ "google cloud": "gcp",
196
+ "google cloud platform": "gcp",
197
+ "microsoft azure": "azure",
198
+ // CI/CD aliases
199
+ "github actions": "github-actions",
200
+ "circle ci": "ci-cd",
201
+ "circleci": "ci-cd",
202
+ "jenkins": "ci-cd",
203
+ "gitlab ci": "ci-cd",
204
+ "travis": "ci-cd",
205
+ // Mobile
206
+ "react native": "mobile",
207
+ "flutter": "mobile",
208
+ "expo": "mobile",
209
+ // AI / ML
210
+ "openai": "llm",
211
+ "anthropic": "llm",
212
+ "langchain": "llm",
213
+ "llamaindex": "llm",
214
+ "hugging face": "ml",
215
+ "huggingface": "ml",
216
+ "scikit-learn": "ml",
217
+ "sklearn": "ml",
218
+ // Data pipeline
219
+ "apache kafka": "kafka",
220
+ "apache spark": "spark",
221
+ "apache airflow": "airflow",
222
+ // Misc
223
+ "tailwindcss": "tailwind",
224
+ "tw": "tailwind",
225
+ "gql": "graphql",
226
+ "ws": "websockets",
227
+ "socket.io": "websockets",
228
+ "jest": "testing",
229
+ "vitest": "testing",
230
+ "playwright": "testing",
231
+ "cypress": "testing"
232
+ };
233
+ VOCAB_SET = new Set(VOCABULARY);
234
+ }
235
+ });
236
+
237
+ // ../../packages/core/src/matcher.ts
238
+ var init_matcher = __esm({
239
+ "../../packages/core/src/matcher.ts"() {
240
+ "use strict";
241
+ }
242
+ });
243
+
244
+ // ../../packages/core/src/feeds/greenhouse.ts
245
+ var init_greenhouse = __esm({
246
+ "../../packages/core/src/feeds/greenhouse.ts"() {
247
+ "use strict";
248
+ init_vocabulary();
249
+ }
250
+ });
251
+
252
+ // ../../packages/core/src/feeds/ashby.ts
253
+ var init_ashby = __esm({
254
+ "../../packages/core/src/feeds/ashby.ts"() {
255
+ "use strict";
256
+ init_vocabulary();
257
+ }
258
+ });
259
+
260
+ // ../../packages/core/src/feeds/himalayas.ts
261
+ var init_himalayas = __esm({
262
+ "../../packages/core/src/feeds/himalayas.ts"() {
263
+ "use strict";
264
+ init_vocabulary();
265
+ }
266
+ });
267
+
268
+ // ../../packages/core/src/feeds/wwr.ts
269
+ var init_wwr = __esm({
270
+ "../../packages/core/src/feeds/wwr.ts"() {
271
+ "use strict";
272
+ init_vocabulary();
273
+ }
274
+ });
275
+
276
+ // ../../packages/core/src/feeds/hn.ts
277
+ var init_hn = __esm({
278
+ "../../packages/core/src/feeds/hn.ts"() {
279
+ "use strict";
280
+ init_vocabulary();
281
+ }
282
+ });
283
+
284
+ // ../../packages/core/src/feeds/index.ts
285
+ var init_feeds = __esm({
286
+ "../../packages/core/src/feeds/index.ts"() {
287
+ "use strict";
288
+ init_greenhouse();
289
+ init_ashby();
290
+ init_himalayas();
291
+ init_wwr();
292
+ init_hn();
293
+ }
294
+ });
295
+
296
+ // ../../packages/core/src/coastal.ts
297
+ import { readFileSync } from "fs";
298
+ import { join } from "path";
299
+ import { fileURLToPath } from "url";
300
+ var init_coastal = __esm({
301
+ "../../packages/core/src/coastal.ts"() {
302
+ "use strict";
303
+ }
304
+ });
305
+
306
+ // ../../packages/core/src/indexer.ts
307
+ var init_indexer = __esm({
308
+ "../../packages/core/src/indexer.ts"() {
309
+ "use strict";
310
+ init_feeds();
311
+ init_coastal();
312
+ }
313
+ });
314
+
315
+ // ../../packages/core/src/github.ts
316
+ var init_github = __esm({
317
+ "../../packages/core/src/github.ts"() {
318
+ "use strict";
319
+ init_vocabulary();
320
+ }
321
+ });
322
+
323
+ // ../../packages/core/src/index.ts
324
+ var init_src = __esm({
325
+ "../../packages/core/src/index.ts"() {
326
+ "use strict";
327
+ init_types();
328
+ init_vocabulary();
329
+ init_matcher();
330
+ init_feeds();
331
+ init_indexer();
332
+ init_coastal();
333
+ init_github();
334
+ }
335
+ });
336
+
337
+ // src/profile.ts
338
+ var profile_exports = {};
339
+ __export(profile_exports, {
340
+ accumulateGitHubTags: () => accumulateGitHubTags,
341
+ accumulateSession: () => accumulateSession,
342
+ accumulateTags: () => accumulateTags,
343
+ addSavedJob: () => addSavedJob,
344
+ deleteProfile: () => deleteProfile,
345
+ listSavedJobs: () => listSavedJobs,
346
+ profileToFingerprint: () => profileToFingerprint,
347
+ readProfile: () => readProfile,
348
+ removeSavedJob: () => removeSavedJob,
349
+ writeProfile: () => writeProfile
350
+ });
351
+ import {
352
+ createCipheriv,
353
+ createDecipheriv,
354
+ randomBytes
355
+ } from "crypto";
356
+ import {
357
+ readFileSync as readFileSync2,
358
+ writeFileSync,
359
+ mkdirSync,
360
+ existsSync
361
+ } from "fs";
362
+ import { join as join2 } from "path";
363
+ import { homedir } from "os";
364
+ async function loadKey() {
365
+ try {
366
+ const kt = await import("keytar");
367
+ const stored = await kt.getPassword("terminalhire", "profile-key");
368
+ if (stored) {
369
+ return Buffer.from(stored, "hex");
370
+ }
371
+ const key2 = randomBytes(KEY_BYTES);
372
+ await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
373
+ return key2;
374
+ } catch {
375
+ }
376
+ mkdirSync(TERMINALHIRE_DIR, { recursive: true });
377
+ if (existsSync(KEY_FILE)) {
378
+ return Buffer.from(readFileSync2(KEY_FILE, "utf8").trim(), "hex");
379
+ }
380
+ const key = randomBytes(KEY_BYTES);
381
+ writeFileSync(KEY_FILE, key.toString("hex"), { mode: 384, encoding: "utf8" });
382
+ return key;
383
+ }
384
+ function encrypt(plaintext, key) {
385
+ const iv = randomBytes(IV_BYTES);
386
+ const cipher = createCipheriv(ALGO, key, iv);
387
+ const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
388
+ const tag = cipher.getAuthTag();
389
+ return {
390
+ iv: iv.toString("hex"),
391
+ tag: tag.toString("hex"),
392
+ ciphertext: ct.toString("hex")
393
+ };
394
+ }
395
+ function decrypt(blob, key) {
396
+ const decipher = createDecipheriv(
397
+ ALGO,
398
+ key,
399
+ Buffer.from(blob.iv, "hex")
400
+ );
401
+ decipher.setAuthTag(Buffer.from(blob.tag, "hex"));
402
+ const plain = Buffer.concat([
403
+ decipher.update(Buffer.from(blob.ciphertext, "hex")),
404
+ decipher.final()
405
+ ]);
406
+ return plain.toString("utf8");
407
+ }
408
+ function blankProfile() {
409
+ return {
410
+ version: 3,
411
+ skillTags: [],
412
+ tagWeights: {},
413
+ hasEmployerSessions: false,
414
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
415
+ };
416
+ }
417
+ function recencyDecay(lastSeen) {
418
+ const ageMs = Date.now() - new Date(lastSeen).getTime();
419
+ return Math.pow(0.5, ageMs / DECAY_HALF_LIFE_MS);
420
+ }
421
+ function tagScore(w) {
422
+ return w.count * recencyDecay(w.lastSeen);
423
+ }
424
+ function deriveSkillTags(tagWeights) {
425
+ return Object.entries(tagWeights).filter(([, w]) => w.count >= 1).sort(([, a], [, b]) => tagScore(b) - tagScore(a)).map(([tag]) => tag);
426
+ }
427
+ function migrateTagWeights(profile) {
428
+ if (!profile.tagWeights) {
429
+ profile.tagWeights = {};
430
+ }
431
+ const now = (/* @__PURE__ */ new Date()).toISOString();
432
+ for (const tag of profile.skillTags) {
433
+ if (!profile.tagWeights[tag]) {
434
+ profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
435
+ }
436
+ }
437
+ }
438
+ async function readProfile() {
439
+ if (!existsSync(PROFILE_FILE)) return blankProfile();
440
+ try {
441
+ const key = await loadKey();
442
+ const raw = readFileSync2(PROFILE_FILE, "utf8");
443
+ const blob = JSON.parse(raw);
444
+ const plaintext = decrypt(blob, key);
445
+ const parsed = JSON.parse(plaintext);
446
+ migrateTagWeights(parsed);
447
+ return parsed;
448
+ } catch {
449
+ return blankProfile();
450
+ }
451
+ }
452
+ async function writeProfile(profile) {
453
+ mkdirSync(TERMINALHIRE_DIR, { recursive: true });
454
+ const key = await loadKey();
455
+ profile.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
456
+ profile.skillTags = deriveSkillTags(profile.tagWeights);
457
+ const blob = encrypt(JSON.stringify(profile), key);
458
+ writeFileSync(PROFILE_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
459
+ }
460
+ function accumulateSession(profile, tags, isEmployerContext, inferredSeniority) {
461
+ const now = (/* @__PURE__ */ new Date()).toISOString();
462
+ let filtered = normalize(tags);
463
+ if (isEmployerContext) {
464
+ filtered = filtered.filter((t) => LANGUAGE_TAGS.has(t));
465
+ profile.hasEmployerSessions = true;
466
+ }
467
+ for (const tag of filtered) {
468
+ const existing = profile.tagWeights[tag];
469
+ if (existing) {
470
+ existing.count += 1;
471
+ existing.sessions += 1;
472
+ existing.lastSeen = now;
473
+ } else {
474
+ profile.tagWeights[tag] = { count: 1, firstSeen: now, lastSeen: now, sessions: 1 };
475
+ }
476
+ }
477
+ if (inferredSeniority && !isEmployerContext) {
478
+ profile.seniority = inferredSeniority;
479
+ }
480
+ }
481
+ async function accumulateTags(rawTokens, isEmployerContext, inferredSeniority) {
482
+ const profile = await readProfile();
483
+ accumulateSession(profile, rawTokens, isEmployerContext, inferredSeniority);
484
+ await writeProfile(profile);
485
+ }
486
+ function accumulateGitHubTags(profile, tags) {
487
+ accumulateSession(
488
+ profile,
489
+ tags,
490
+ /* isEmployerContext */
491
+ false
492
+ );
493
+ }
494
+ async function listSavedJobs() {
495
+ const profile = await readProfile();
496
+ return profile.savedJobs ?? [];
497
+ }
498
+ async function addSavedJob(job) {
499
+ const profile = await readProfile();
500
+ const existing = profile.savedJobs ?? [];
501
+ const filtered = existing.filter((j) => j.id !== job.id);
502
+ profile.savedJobs = [...filtered, { ...job, savedAt: (/* @__PURE__ */ new Date()).toISOString() }];
503
+ await writeProfile(profile);
504
+ }
505
+ async function removeSavedJob(id) {
506
+ const profile = await readProfile();
507
+ const existing = profile.savedJobs ?? [];
508
+ const filtered = existing.filter((j) => j.id !== id);
509
+ if (filtered.length === existing.length) return false;
510
+ profile.savedJobs = filtered;
511
+ await writeProfile(profile);
512
+ return true;
513
+ }
514
+ async function deleteProfile() {
515
+ const { rmSync: rmSync2 } = await import("fs");
516
+ try {
517
+ rmSync2(PROFILE_FILE);
518
+ } catch {
519
+ }
520
+ try {
521
+ rmSync2(KEY_FILE);
522
+ } catch {
523
+ }
524
+ }
525
+ function profileToFingerprint(profile) {
526
+ const rankedTags = Object.entries(profile.tagWeights).map(([tag, w]) => ({ tag, score: tagScore(w) })).filter(({ score }) => score >= MIN_FINGERPRINT_SCORE).sort((a, b) => b.score - a.score).map(({ tag }) => tag);
527
+ const skillTags = rankedTags.length > 0 ? rankedTags : profile.skillTags;
528
+ return {
529
+ skillTags,
530
+ seniorityBand: profile.seniority,
531
+ prefs: {
532
+ roleTypes: profile.roleTypes,
533
+ remoteOnly: profile.remoteOnly,
534
+ compFloorUsd: profile.compFloorUsd
535
+ }
536
+ };
537
+ }
538
+ var TERMINALHIRE_DIR, PROFILE_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES, DECAY_HALF_LIFE_MS, LANGUAGE_TAGS, MIN_FINGERPRINT_SCORE;
539
+ var init_profile = __esm({
540
+ "src/profile.ts"() {
541
+ "use strict";
542
+ init_src();
543
+ TERMINALHIRE_DIR = join2(homedir(), ".terminalhire");
544
+ PROFILE_FILE = join2(TERMINALHIRE_DIR, "profile.enc");
545
+ KEY_FILE = join2(TERMINALHIRE_DIR, "key");
546
+ ALGO = "aes-256-gcm";
547
+ KEY_BYTES = 32;
548
+ IV_BYTES = 12;
549
+ DECAY_HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
550
+ LANGUAGE_TAGS = /* @__PURE__ */ new Set([
551
+ "typescript",
552
+ "javascript",
553
+ "python",
554
+ "go",
555
+ "rust",
556
+ "java",
557
+ "ruby",
558
+ "elixir",
559
+ "scala",
560
+ "kotlin",
561
+ "swift",
562
+ "cpp",
563
+ "csharp",
564
+ "php",
565
+ "haskell",
566
+ "clojure",
567
+ "r"
568
+ ]);
569
+ MIN_FINGERPRINT_SCORE = 0.05;
570
+ }
571
+ });
572
+
573
+ // bin/jpi-sync.js
574
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2, rmSync } from "fs";
575
+ import { join as join3 } from "path";
576
+ import { homedir as homedir2 } from "os";
577
+ import { createInterface } from "readline";
578
+ var TH_DIR = process.env["TERMINALHIRE_DIR"] || join3(homedir2(), ".terminalhire");
579
+ var TIER1_MARKER = join3(TH_DIR, "tier1.json");
580
+ var API_URL = process.env["TERMINALHIRE_API_URL"] || process.env["JPI_API_URL"] || "https://terminalhire.com";
581
+ var CONSENT_VERSION = 1;
582
+ function ask(question) {
583
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
584
+ return new Promise((res) => {
585
+ rl.question(question, (answer) => {
586
+ rl.close();
587
+ res(answer.trim().toLowerCase());
588
+ });
589
+ });
590
+ }
591
+ function readMarker() {
592
+ try {
593
+ return existsSync2(TIER1_MARKER) ? JSON.parse(readFileSync3(TIER1_MARKER, "utf8")) : null;
594
+ } catch {
595
+ return null;
596
+ }
597
+ }
598
+ function writeMarker(marker) {
599
+ mkdirSync2(TH_DIR, { recursive: true });
600
+ writeFileSync2(TIER1_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
601
+ }
602
+ function clearMarker() {
603
+ try {
604
+ rmSync(TIER1_MARKER);
605
+ } catch {
606
+ }
607
+ }
608
+ function buildConsentFields(profile) {
609
+ const gh = profile.github || {};
610
+ const fields = [
611
+ { key: "login", label: "GitHub login", value: gh.login },
612
+ { key: "publicEmail", label: "Public email", value: gh.publicEmail || null },
613
+ { key: "topLanguages", label: "Top languages", value: gh.topLanguages || [] },
614
+ { key: "skillTags", label: "Skill tags", value: profile.skillTags || [] }
615
+ ];
616
+ if (profile.displayName) {
617
+ fields.push({ key: "displayName", label: "Display name", value: profile.displayName });
618
+ }
619
+ if (profile.contactEmail) {
620
+ fields.push({ key: "contactEmail", label: "Contact email", value: profile.contactEmail });
621
+ }
622
+ return fields;
623
+ }
624
+ function renderConsentCard(fields) {
625
+ console.log("");
626
+ 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");
627
+ console.log("\u2502 terminalhire \u2014 sync your profile (Tier-1, opt-in) \u2502");
628
+ 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");
629
+ console.log("");
630
+ console.log(" You are about to send the following data to");
631
+ console.log(" staqs (terminalhire.com):");
632
+ console.log("");
633
+ for (const f of fields) {
634
+ const shown = Array.isArray(f.value) ? JSON.stringify(f.value) : String(f.value ?? "(not set)");
635
+ console.log(` ${f.label.padEnd(16)}: ${shown}`);
636
+ }
637
+ console.log("");
638
+ console.log(" What is NEVER sent:");
639
+ console.log(" - Private repos, employer repos, raw code");
640
+ console.log(" - Employer-repo-derived tags (filtered at source)");
641
+ console.log(" - Session history, file paths, project names");
642
+ console.log("");
643
+ console.log(" This is a ONE-TIME snapshot. Nothing is sent automatically;");
644
+ console.log(" re-run `terminalhire sync --push` to update it later.");
645
+ console.log(" Delete it any time: terminalhire sync --delete");
646
+ console.log("");
647
+ console.log(" This is NOT required to use terminalhire.");
648
+ console.log("");
649
+ }
650
+ async function runPush() {
651
+ const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
652
+ const profile = await readProfile2();
653
+ if (!profile.github || !profile.github.login) {
654
+ console.log("");
655
+ console.log(" No GitHub data in your local profile yet.");
656
+ console.log(" Run `terminalhire login` first, then re-run `terminalhire sync --push`.");
657
+ console.log("");
658
+ process.exit(1);
659
+ }
660
+ const fields = buildConsentFields(profile);
661
+ renderConsentCard(fields);
662
+ let consentConfirmed = false;
663
+ const answer = await ask(' Sync your profile to staqs (terminalhire.com)? Type "yes" to continue: ');
664
+ if (answer === "yes") {
665
+ consentConfirmed = true;
666
+ }
667
+ if (!consentConfirmed) {
668
+ console.log("\n Aborted \u2014 nothing was sent.\n");
669
+ process.exit(0);
670
+ }
671
+ const consentedAt = (/* @__PURE__ */ new Date()).toISOString();
672
+ const consentToken = {
673
+ consentedAt,
674
+ version: CONSENT_VERSION,
675
+ fields: fields.map((f) => f.key)
676
+ };
677
+ const payloadProfile = {
678
+ login: profile.github.login,
679
+ name: profile.displayName || null,
680
+ publicEmail: profile.github.publicEmail || null,
681
+ topLanguages: profile.github.topLanguages || [],
682
+ skillTags: profile.skillTags || [],
683
+ displayName: profile.displayName || null,
684
+ contactEmail: profile.contactEmail || null
685
+ };
686
+ const priorMarker = readMarker();
687
+ const rowToken = priorMarker && priorMarker.deleteToken ? priorMarker.deleteToken : null;
688
+ const requestBody = { consentToken, profile: payloadProfile };
689
+ if (rowToken) {
690
+ requestBody.rowToken = rowToken;
691
+ }
692
+ console.log("\n Sending one-time snapshot...");
693
+ let res;
694
+ try {
695
+ res = await fetch(`${API_URL}/api/profile-sync`, {
696
+ method: "POST",
697
+ headers: { "Content-Type": "application/json" },
698
+ body: JSON.stringify(requestBody),
699
+ signal: AbortSignal.timeout(1e4)
700
+ });
701
+ } catch (err) {
702
+ console.error(`
703
+ Sync failed: ${err instanceof Error ? err.message : String(err)}`);
704
+ process.exit(1);
705
+ }
706
+ if (!res.ok) {
707
+ let detail = "";
708
+ try {
709
+ detail = (await res.json())?.message || "";
710
+ } catch {
711
+ }
712
+ console.error(`
713
+ Sync failed: /api/profile-sync returned ${res.status}. ${detail}`);
714
+ if (res.status === 503) {
715
+ console.error(" (Tier-1 sync is not enabled on the server yet.)");
716
+ }
717
+ if (res.status === 403) {
718
+ console.error(" (This GitHub login was already claimed by a different push.)");
719
+ }
720
+ process.exit(1);
721
+ }
722
+ let deleteToken = null;
723
+ try {
724
+ deleteToken = (await res.json())?.deleteToken || null;
725
+ } catch {
726
+ }
727
+ writeMarker({ consentedAt, login: profile.github.login, deleteToken });
728
+ console.log("\n Profile synced. A local consent marker was written to ~/.terminalhire/tier1.json");
729
+ console.log(" Delete your synced profile any time: terminalhire sync --delete\n");
730
+ }
731
+ function runStatus() {
732
+ const marker = readMarker();
733
+ console.log("");
734
+ if (marker && marker.consentedAt) {
735
+ console.log(" Tier-1 sync: CONSENTED (local marker present)");
736
+ console.log(` login: ${marker.login || "(unknown)"}`);
737
+ console.log(` consented at: ${marker.consentedAt}`);
738
+ console.log("");
739
+ console.log(" This reflects your local marker only (no network call was made).");
740
+ console.log(" Update: terminalhire sync --push | Delete: terminalhire sync --delete");
741
+ } else {
742
+ console.log(" Tier-1 sync: NOT CONSENTED (no local marker).");
743
+ console.log(" Your profile has not been synced. Enable: terminalhire sync --push");
744
+ }
745
+ console.log("");
746
+ }
747
+ async function runDelete() {
748
+ const marker = readMarker();
749
+ console.log("");
750
+ console.log(" This will hard-delete your synced profile from staqs (terminalhire.com)");
751
+ console.log(" and remove the local consent marker. There is no soft-delete.");
752
+ console.log("");
753
+ const answer = await ask(' Delete your synced profile? Type "yes" to confirm: ');
754
+ if (answer !== "yes") {
755
+ console.log("\n Aborted \u2014 nothing was deleted.\n");
756
+ process.exit(0);
757
+ }
758
+ let login = marker && marker.login ? marker.login : null;
759
+ if (!login) {
760
+ const { readProfile: readProfile2 } = await Promise.resolve().then(() => (init_profile(), profile_exports));
761
+ const profile = await readProfile2();
762
+ login = profile.github && profile.github.login ? profile.github.login : null;
763
+ }
764
+ if (!login) {
765
+ console.log("\n No github login to delete (no marker, no GitHub profile). Clearing local marker.\n");
766
+ clearMarker();
767
+ process.exit(0);
768
+ }
769
+ const deleteToken = marker && marker.deleteToken ? marker.deleteToken : null;
770
+ if (!deleteToken) {
771
+ console.log("\n No delete token found in ~/.terminalhire/tier1.json.");
772
+ console.log(" Deletion must be run from the machine that originally pushed your profile");
773
+ console.log(" (the delete token is stored there), or re-run `terminalhire sync --push`");
774
+ console.log(" first to obtain a fresh token, then `terminalhire sync --delete`.\n");
775
+ process.exit(1);
776
+ }
777
+ const consentToken = {
778
+ consentedAt: marker && marker.consentedAt || (/* @__PURE__ */ new Date()).toISOString(),
779
+ version: CONSENT_VERSION,
780
+ fields: ["login"]
781
+ };
782
+ console.log("\n Requesting deletion...");
783
+ let res;
784
+ try {
785
+ res = await fetch(`${API_URL}/api/profile-sync`, {
786
+ method: "DELETE",
787
+ headers: { "Content-Type": "application/json" },
788
+ body: JSON.stringify({ consentToken, login, deleteToken }),
789
+ signal: AbortSignal.timeout(1e4)
790
+ });
791
+ } catch (err) {
792
+ console.error(`
793
+ Delete failed: ${err instanceof Error ? err.message : String(err)}`);
794
+ console.error(" Local marker NOT cleared (server state unknown). Re-run to retry.\n");
795
+ process.exit(1);
796
+ }
797
+ if (!res.ok) {
798
+ console.error(`
799
+ Delete failed: /api/profile-sync returned ${res.status}.`);
800
+ if (res.status === 503) {
801
+ console.error(" (Tier-1 sync is not enabled on the server yet.) Clearing local marker.");
802
+ clearMarker();
803
+ }
804
+ process.exit(1);
805
+ }
806
+ clearMarker();
807
+ console.log("\n Synced profile deleted and local marker cleared.\n");
808
+ }
809
+ async function run() {
810
+ const args = process.argv.slice(2).filter((a) => a !== "sync");
811
+ const has = (f) => args.includes(f);
812
+ if (has("--push") || has("--enable")) {
813
+ await runPush();
814
+ return;
815
+ }
816
+ if (has("--status")) {
817
+ runStatus();
818
+ return;
819
+ }
820
+ if (has("--delete")) {
821
+ await runDelete();
822
+ return;
823
+ }
824
+ console.log("");
825
+ console.log(" terminalhire sync \u2014 opt-in Tier-1 profile sync (one-time snapshot)");
826
+ console.log("");
827
+ console.log(' terminalhire sync --push Send your profile (shows a consent card, requires typed "yes")');
828
+ console.log(" terminalhire sync --status Show whether you have consented (local read only)");
829
+ console.log(" terminalhire sync --delete Hard-delete your synced profile (revocation)");
830
+ console.log("");
831
+ console.log(' Your profile is NEVER sent without an explicit typed "yes".');
832
+ console.log(" This is NOT required to use terminalhire.");
833
+ console.log("");
834
+ }
835
+ export {
836
+ run
837
+ };