terminalhire 0.36.1 → 0.37.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/cache-store.js +52 -2
- package/dist/bin/claim-push-bg.js +95 -23
- package/dist/bin/directory.js +151 -0
- package/dist/bin/job-status-store.js +856 -0
- package/dist/bin/jpi-bounties.js +2243 -1036
- package/dist/bin/jpi-chat-read.js +298 -95
- package/dist/bin/jpi-chat.js +311 -117
- package/dist/bin/jpi-claim.js +2716 -1153
- package/dist/bin/jpi-config.js +52 -2
- package/dist/bin/jpi-contribute.js +1549 -110
- package/dist/bin/jpi-devs.js +2207 -1027
- package/dist/bin/jpi-dispatch.js +8138 -11833
- package/dist/bin/jpi-hub.js +2304 -1071
- package/dist/bin/jpi-inbox.js +311 -117
- package/dist/bin/jpi-init.js +2103 -919
- package/dist/bin/jpi-intro.js +237 -78
- package/dist/bin/jpi-jobs.js +2940 -1073
- package/dist/bin/jpi-learn.js +235 -70
- package/dist/bin/jpi-link.js +63 -13
- package/dist/bin/jpi-login.js +2226 -1047
- package/dist/bin/jpi-mcp-chat.js +430 -5949
- package/dist/bin/jpi-mcp.js +429 -5958
- package/dist/bin/jpi-profile.js +235 -70
- package/dist/bin/jpi-project.js +2103 -919
- package/dist/bin/jpi-protocol.js +128 -17
- package/dist/bin/jpi-refresh.js +2338 -1107
- package/dist/bin/jpi-repo.js +268 -76
- package/dist/bin/jpi-save.js +235 -70
- package/dist/bin/jpi-spinner.js +208 -76
- package/dist/bin/jpi-sync.js +251 -77
- package/dist/bin/jpi-trajectory.js +270 -94
- package/dist/bin/jpi-update.js +128 -16
- package/dist/bin/jpi.js +56 -5
- package/dist/bin/peer-connect-prompt.js +52 -2
- package/dist/bin/pulse-prompt.js +53 -10
- package/dist/bin/spinner-io.js +181 -0
- package/dist/bin/spinner-render.js +1260 -0
- package/dist/bin/spinner-seen.js +154 -0
- package/dist/bin/spinner.js +197 -61
- package/dist/bin/version-nudge.js +57 -3
- package/dist/src/chat-client.js +165 -62
- package/dist/src/chat-keystore.js +149 -49
- package/dist/src/claims.js +83 -7
- package/dist/src/config.js +52 -2
- package/dist/src/crypto-store.js +62 -22
- package/dist/src/github-auth.js +67 -19
- package/dist/src/intro.js +237 -78
- package/dist/src/link.js +54 -11
- package/dist/src/policy-acks.js +112 -0
- package/dist/src/profile.js +156 -55
- package/dist/src/protocol.js +123 -17
- package/dist/src/repo-experience.js +156 -55
- package/dist/src/repo-policy.js +60 -10
- package/dist/src/reputation/audit.js +0 -1
- package/dist/src/reputation/fetch.js +201 -34
- package/dist/src/signal.js +94 -33
- package/dist/src/state-dir.js +54 -0
- package/dist/src/trajectory.js +270 -94
- package/dist/src/web-session.js +52 -9
- package/install.js +169 -27
- package/package.json +1 -1
- package/statusline-install.js +153 -20
package/dist/bin/cache-store.js
CHANGED
|
@@ -1,7 +1,57 @@
|
|
|
1
1
|
// bin/cache-store.js
|
|
2
|
-
import { readFileSync, writeFileSync,
|
|
2
|
+
import { readFileSync, writeFileSync, renameSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import { homedir } from "os";
|
|
5
|
+
|
|
6
|
+
// src/state-dir.ts
|
|
7
|
+
import { closeSync, constants, fchmodSync, fstatSync, mkdirSync, openSync } from "fs";
|
|
8
|
+
var STATE_DIR_MODE = 448;
|
|
9
|
+
var STATE_DIR_OK = "ok";
|
|
10
|
+
var STATE_DIR_SYMLINK = "symlink";
|
|
11
|
+
var STATE_DIR_UNVERIFIED = "unverified";
|
|
12
|
+
var warnedDirs = /* @__PURE__ */ new Set();
|
|
13
|
+
function warnStateDirOnce(dir, message) {
|
|
14
|
+
if (warnedDirs.has(dir)) return;
|
|
15
|
+
warnedDirs.add(dir);
|
|
16
|
+
try {
|
|
17
|
+
process.stderr.write(message);
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function ensureStateDir(dir) {
|
|
22
|
+
mkdirSync(dir, { recursive: true, mode: STATE_DIR_MODE });
|
|
23
|
+
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
24
|
+
let fd;
|
|
25
|
+
try {
|
|
26
|
+
fd = openSync(dir, constants.O_RDONLY | noFollow);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err?.code === "ELOOP") {
|
|
29
|
+
warnStateDirOnce(
|
|
30
|
+
dir,
|
|
31
|
+
`terminalhire: ${dir} is a symlink \u2014 leaving its permissions alone; the 0700 guarantee on the state directory is NOT enforced.
|
|
32
|
+
`
|
|
33
|
+
);
|
|
34
|
+
return STATE_DIR_SYMLINK;
|
|
35
|
+
}
|
|
36
|
+
return STATE_DIR_UNVERIFIED;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const currentMode = fstatSync(fd).mode & 511;
|
|
40
|
+
if ((currentMode & ~STATE_DIR_MODE) !== 0) {
|
|
41
|
+
fchmodSync(fd, currentMode & STATE_DIR_MODE);
|
|
42
|
+
}
|
|
43
|
+
return STATE_DIR_OK;
|
|
44
|
+
} catch {
|
|
45
|
+
return STATE_DIR_UNVERIFIED;
|
|
46
|
+
} finally {
|
|
47
|
+
try {
|
|
48
|
+
closeSync(fd);
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// bin/cache-store.js
|
|
5
55
|
var TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join(homedir(), ".terminalhire");
|
|
6
56
|
var INDEX_CACHE_FILE = join(TERMINALHIRE_DIR, "index-cache.json");
|
|
7
57
|
var SCHEMA_VERSION = 1;
|
|
@@ -14,7 +64,7 @@ function readCacheEntry() {
|
|
|
14
64
|
}
|
|
15
65
|
}
|
|
16
66
|
function updateIndexCache(patch) {
|
|
17
|
-
|
|
67
|
+
ensureStateDir(TERMINALHIRE_DIR);
|
|
18
68
|
const existing = readCacheEntry() ?? {};
|
|
19
69
|
const entry = {
|
|
20
70
|
...existing,
|
|
@@ -8,6 +8,60 @@ var __export = (target, all) => {
|
|
|
8
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// src/state-dir.ts
|
|
12
|
+
import { closeSync, constants, fchmodSync, fstatSync, mkdirSync, openSync } from "fs";
|
|
13
|
+
function warnStateDirOnce(dir, message) {
|
|
14
|
+
if (warnedDirs.has(dir)) return;
|
|
15
|
+
warnedDirs.add(dir);
|
|
16
|
+
try {
|
|
17
|
+
process.stderr.write(message);
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function ensureStateDir(dir) {
|
|
22
|
+
mkdirSync(dir, { recursive: true, mode: STATE_DIR_MODE });
|
|
23
|
+
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
24
|
+
let fd;
|
|
25
|
+
try {
|
|
26
|
+
fd = openSync(dir, constants.O_RDONLY | noFollow);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err?.code === "ELOOP") {
|
|
29
|
+
warnStateDirOnce(
|
|
30
|
+
dir,
|
|
31
|
+
`terminalhire: ${dir} is a symlink \u2014 leaving its permissions alone; the 0700 guarantee on the state directory is NOT enforced.
|
|
32
|
+
`
|
|
33
|
+
);
|
|
34
|
+
return STATE_DIR_SYMLINK;
|
|
35
|
+
}
|
|
36
|
+
return STATE_DIR_UNVERIFIED;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const currentMode = fstatSync(fd).mode & 511;
|
|
40
|
+
if ((currentMode & ~STATE_DIR_MODE) !== 0) {
|
|
41
|
+
fchmodSync(fd, currentMode & STATE_DIR_MODE);
|
|
42
|
+
}
|
|
43
|
+
return STATE_DIR_OK;
|
|
44
|
+
} catch {
|
|
45
|
+
return STATE_DIR_UNVERIFIED;
|
|
46
|
+
} finally {
|
|
47
|
+
try {
|
|
48
|
+
closeSync(fd);
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
var STATE_DIR_MODE, STATE_DIR_OK, STATE_DIR_SYMLINK, STATE_DIR_UNVERIFIED, warnedDirs;
|
|
54
|
+
var init_state_dir = __esm({
|
|
55
|
+
"src/state-dir.ts"() {
|
|
56
|
+
"use strict";
|
|
57
|
+
STATE_DIR_MODE = 448;
|
|
58
|
+
STATE_DIR_OK = "ok";
|
|
59
|
+
STATE_DIR_SYMLINK = "symlink";
|
|
60
|
+
STATE_DIR_UNVERIFIED = "unverified";
|
|
61
|
+
warnedDirs = /* @__PURE__ */ new Set();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
11
65
|
// src/claims.ts
|
|
12
66
|
var claims_exports = {};
|
|
13
67
|
__export(claims_exports, {
|
|
@@ -22,7 +76,15 @@ __export(claims_exports, {
|
|
|
22
76
|
toPushedClaim: () => toPushedClaim,
|
|
23
77
|
updateClaim: () => updateClaim
|
|
24
78
|
});
|
|
25
|
-
import {
|
|
79
|
+
import {
|
|
80
|
+
readFileSync as readFileSync2,
|
|
81
|
+
writeFileSync as writeFileSync2,
|
|
82
|
+
mkdirSync as mkdirSync2,
|
|
83
|
+
renameSync as renameSync2,
|
|
84
|
+
existsSync as existsSync2,
|
|
85
|
+
rmSync as rmSync2,
|
|
86
|
+
statSync
|
|
87
|
+
} from "fs";
|
|
26
88
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
27
89
|
import { join as join2 } from "path";
|
|
28
90
|
import { homedir as homedir2 } from "os";
|
|
@@ -30,7 +92,7 @@ function sleepSync(ms) {
|
|
|
30
92
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
31
93
|
}
|
|
32
94
|
function withClaimsLock(fn) {
|
|
33
|
-
|
|
95
|
+
ensureStateDir(TERMINALHIRE_DIR2);
|
|
34
96
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
35
97
|
for (; ; ) {
|
|
36
98
|
try {
|
|
@@ -89,11 +151,15 @@ function readClaims() {
|
|
|
89
151
|
}
|
|
90
152
|
}
|
|
91
153
|
function writeClaims(claims) {
|
|
92
|
-
|
|
154
|
+
ensureStateDir(TERMINALHIRE_DIR2);
|
|
93
155
|
const tmp = `${CLAIMS_FILE}.${process.pid}.${randomBytes2(6).toString("hex")}.tmp`;
|
|
94
156
|
const payload = { claims };
|
|
95
157
|
try {
|
|
96
|
-
writeFileSync2(tmp, JSON.stringify(payload, null, 2), {
|
|
158
|
+
writeFileSync2(tmp, JSON.stringify(payload, null, 2), {
|
|
159
|
+
encoding: "utf8",
|
|
160
|
+
mode: 384,
|
|
161
|
+
flag: "wx"
|
|
162
|
+
});
|
|
97
163
|
renameSync2(tmp, CLAIMS_FILE);
|
|
98
164
|
} catch (err) {
|
|
99
165
|
try {
|
|
@@ -167,6 +233,7 @@ var TERMINALHIRE_DIR2, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK
|
|
|
167
233
|
var init_claims = __esm({
|
|
168
234
|
"src/claims.ts"() {
|
|
169
235
|
"use strict";
|
|
236
|
+
init_state_dir();
|
|
170
237
|
TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join2(homedir2(), ".terminalhire");
|
|
171
238
|
CLAIMS_FILE = join2(TERMINALHIRE_DIR2, "claims.json");
|
|
172
239
|
LOCK_DIR = `${CLAIMS_FILE}.lock`;
|
|
@@ -184,8 +251,22 @@ var init_claims = __esm({
|
|
|
184
251
|
];
|
|
185
252
|
TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
|
|
186
253
|
POLL_TRANSITIONS = {
|
|
187
|
-
merged: /* @__PURE__ */ new Set([
|
|
188
|
-
|
|
254
|
+
merged: /* @__PURE__ */ new Set([
|
|
255
|
+
"claimed",
|
|
256
|
+
"working",
|
|
257
|
+
"in-review",
|
|
258
|
+
"ready",
|
|
259
|
+
"submitted",
|
|
260
|
+
"abandoned"
|
|
261
|
+
]),
|
|
262
|
+
abandoned: /* @__PURE__ */ new Set([
|
|
263
|
+
"claimed",
|
|
264
|
+
"working",
|
|
265
|
+
"in-review",
|
|
266
|
+
"ready",
|
|
267
|
+
"submitted",
|
|
268
|
+
"merged"
|
|
269
|
+
]),
|
|
189
270
|
submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
|
|
190
271
|
};
|
|
191
272
|
}
|
|
@@ -193,24 +274,14 @@ var init_claims = __esm({
|
|
|
193
274
|
|
|
194
275
|
// bin/claim-push-bg.js
|
|
195
276
|
import { createHash } from "crypto";
|
|
196
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3,
|
|
277
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, rmSync as rmSync3 } from "fs";
|
|
197
278
|
import { join as join3 } from "path";
|
|
198
279
|
import { homedir as homedir3 } from "os";
|
|
199
280
|
|
|
200
281
|
// src/github-auth.ts
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
randomBytes
|
|
205
|
-
} from "crypto";
|
|
206
|
-
import {
|
|
207
|
-
readFileSync,
|
|
208
|
-
writeFileSync,
|
|
209
|
-
mkdirSync,
|
|
210
|
-
existsSync,
|
|
211
|
-
rmSync,
|
|
212
|
-
renameSync
|
|
213
|
-
} from "fs";
|
|
282
|
+
init_state_dir();
|
|
283
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
284
|
+
import { readFileSync, writeFileSync, existsSync, rmSync, renameSync } from "fs";
|
|
214
285
|
import { join } from "path";
|
|
215
286
|
import { homedir } from "os";
|
|
216
287
|
var TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join(homedir(), ".terminalhire");
|
|
@@ -220,7 +291,7 @@ var ALGO = "aes-256-gcm";
|
|
|
220
291
|
var KEY_BYTES = 32;
|
|
221
292
|
var IV_BYTES = 12;
|
|
222
293
|
async function loadKey() {
|
|
223
|
-
|
|
294
|
+
ensureStateDir(TERMINALHIRE_DIR);
|
|
224
295
|
if (existsSync(KEY_FILE)) {
|
|
225
296
|
return Buffer.from(readFileSync(KEY_FILE, "utf8").trim(), "hex");
|
|
226
297
|
}
|
|
@@ -246,6 +317,7 @@ function decrypt(blob, key) {
|
|
|
246
317
|
}
|
|
247
318
|
|
|
248
319
|
// bin/claim-push-bg.js
|
|
320
|
+
init_state_dir();
|
|
249
321
|
var TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join3(homedir3(), ".terminalhire");
|
|
250
322
|
var CLAIM_PUSH_AUTO_MARKER = join3(TERMINALHIRE_DIR3, "claim-push-auto.json");
|
|
251
323
|
var CLAIM_PUSH_TOKEN_FILE = join3(TERMINALHIRE_DIR3, "claim-push-token.enc");
|
|
@@ -254,7 +326,7 @@ var CLAIM_SYNC_BASE = "https://terminalhire.com";
|
|
|
254
326
|
var AUTO_CONSENT_VERSION = 2;
|
|
255
327
|
var AUTO_PUSH_THROTTLE_MS = 24 * 60 * 60 * 1e3;
|
|
256
328
|
async function writePushTokenEnc(rawToken) {
|
|
257
|
-
|
|
329
|
+
ensureStateDir(TERMINALHIRE_DIR3);
|
|
258
330
|
const key = await loadKey();
|
|
259
331
|
const blob = encrypt(rawToken, key);
|
|
260
332
|
writeFileSync3(CLAIM_PUSH_TOKEN_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
|
|
@@ -283,7 +355,7 @@ function readAutoMarker() {
|
|
|
283
355
|
}
|
|
284
356
|
}
|
|
285
357
|
function writeAutoMarker(marker) {
|
|
286
|
-
|
|
358
|
+
ensureStateDir(TERMINALHIRE_DIR3);
|
|
287
359
|
writeFileSync3(CLAIM_PUSH_AUTO_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
288
360
|
}
|
|
289
361
|
function clearAutoMarker() {
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/directory.js
|
|
4
|
+
import { readFileSync, writeFileSync, renameSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
|
|
8
|
+
// src/state-dir.ts
|
|
9
|
+
import { closeSync, constants, fchmodSync, fstatSync, mkdirSync, openSync } from "fs";
|
|
10
|
+
var STATE_DIR_MODE = 448;
|
|
11
|
+
var STATE_DIR_OK = "ok";
|
|
12
|
+
var STATE_DIR_SYMLINK = "symlink";
|
|
13
|
+
var STATE_DIR_UNVERIFIED = "unverified";
|
|
14
|
+
var warnedDirs = /* @__PURE__ */ new Set();
|
|
15
|
+
function warnStateDirOnce(dir, message) {
|
|
16
|
+
if (warnedDirs.has(dir)) return;
|
|
17
|
+
warnedDirs.add(dir);
|
|
18
|
+
try {
|
|
19
|
+
process.stderr.write(message);
|
|
20
|
+
} catch {
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function ensureStateDir(dir) {
|
|
24
|
+
mkdirSync(dir, { recursive: true, mode: STATE_DIR_MODE });
|
|
25
|
+
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
26
|
+
let fd;
|
|
27
|
+
try {
|
|
28
|
+
fd = openSync(dir, constants.O_RDONLY | noFollow);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err?.code === "ELOOP") {
|
|
31
|
+
warnStateDirOnce(
|
|
32
|
+
dir,
|
|
33
|
+
`terminalhire: ${dir} is a symlink \u2014 leaving its permissions alone; the 0700 guarantee on the state directory is NOT enforced.
|
|
34
|
+
`
|
|
35
|
+
);
|
|
36
|
+
return STATE_DIR_SYMLINK;
|
|
37
|
+
}
|
|
38
|
+
return STATE_DIR_UNVERIFIED;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const currentMode = fstatSync(fd).mode & 511;
|
|
42
|
+
if ((currentMode & ~STATE_DIR_MODE) !== 0) {
|
|
43
|
+
fchmodSync(fd, currentMode & STATE_DIR_MODE);
|
|
44
|
+
}
|
|
45
|
+
return STATE_DIR_OK;
|
|
46
|
+
} catch {
|
|
47
|
+
return STATE_DIR_UNVERIFIED;
|
|
48
|
+
} finally {
|
|
49
|
+
try {
|
|
50
|
+
closeSync(fd);
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// bin/directory.js
|
|
57
|
+
var TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join(homedir(), ".terminalhire");
|
|
58
|
+
var DIRECTORY_CACHE_FILE = join(TERMINALHIRE_DIR, "directory-cache.json");
|
|
59
|
+
var PROJECT_FILE = join(TERMINALHIRE_DIR, "project.json");
|
|
60
|
+
var INDEX_TTL_MS = 15 * 60 * 1e3;
|
|
61
|
+
var API_URL = process.env["TERMINALHIRE_API_URL"] ?? process.env["JPI_API_URL"] ?? "https://terminalhire.com";
|
|
62
|
+
function readDirectoryCache() {
|
|
63
|
+
try {
|
|
64
|
+
const entry = JSON.parse(readFileSync(DIRECTORY_CACHE_FILE, "utf8"));
|
|
65
|
+
if (typeof entry.ts === "number" && Number.isFinite(entry.ts) && Date.now() - entry.ts < INDEX_TTL_MS) {
|
|
66
|
+
return { index: entry.index, ts: entry.ts };
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function writeDirectoryCache(index) {
|
|
74
|
+
ensureStateDir(TERMINALHIRE_DIR);
|
|
75
|
+
writeFileSync(DIRECTORY_CACHE_FILE, JSON.stringify({ ts: Date.now(), index }), "utf8");
|
|
76
|
+
}
|
|
77
|
+
function readProject() {
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(readFileSync(PROJECT_FILE, "utf8"));
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function writeProject(patch) {
|
|
85
|
+
const existing = readProject() ?? {};
|
|
86
|
+
const merged = { ...existing, ...patch };
|
|
87
|
+
if (!merged.createdAt) merged.createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
88
|
+
ensureStateDir(TERMINALHIRE_DIR);
|
|
89
|
+
const tmpFile = `${PROJECT_FILE}.tmp`;
|
|
90
|
+
writeFileSync(tmpFile, JSON.stringify(merged, null, 2), "utf8");
|
|
91
|
+
renameSync(tmpFile, PROJECT_FILE);
|
|
92
|
+
return merged;
|
|
93
|
+
}
|
|
94
|
+
function relativeTime(ts) {
|
|
95
|
+
const secs = Math.max(0, Math.round((Date.now() - ts) / 1e3));
|
|
96
|
+
if (secs < 60) return `${secs}s ago`;
|
|
97
|
+
const mins = Math.round(secs / 60);
|
|
98
|
+
return mins < 60 ? `${mins}m ago` : `${Math.round(mins / 60)}h ago`;
|
|
99
|
+
}
|
|
100
|
+
async function fetchDirectory({ quiet = false } = {}) {
|
|
101
|
+
const cached = readDirectoryCache();
|
|
102
|
+
if (cached) {
|
|
103
|
+
if (!quiet) console.log(`\u2713 Using cached directory (updated ${relativeTime(cached.ts)})`);
|
|
104
|
+
return cached.index;
|
|
105
|
+
}
|
|
106
|
+
if (!quiet) console.log(`\u21BB Refreshing builder directory from ${API_URL}/api/directory...`);
|
|
107
|
+
const res = await fetch(`${API_URL}/api/directory`, { signal: AbortSignal.timeout(1e4) });
|
|
108
|
+
if (!res.ok) throw new Error(`/api/directory returned ${res.status}`);
|
|
109
|
+
const index = await res.json();
|
|
110
|
+
writeDirectoryCache(index);
|
|
111
|
+
return index;
|
|
112
|
+
}
|
|
113
|
+
function reportMatched(results, fetchImpl = fetch) {
|
|
114
|
+
try {
|
|
115
|
+
const logins = [
|
|
116
|
+
...new Set(
|
|
117
|
+
results.map((r) => r?.job?.company).filter((login) => typeof login === "string" && login.length > 0)
|
|
118
|
+
)
|
|
119
|
+
];
|
|
120
|
+
if (logins.length === 0) return;
|
|
121
|
+
return Promise.resolve(
|
|
122
|
+
fetchImpl(`${API_URL}/api/directory/matched`, {
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers: { "Content-Type": "application/json" },
|
|
125
|
+
body: JSON.stringify({ matched: logins }),
|
|
126
|
+
signal: AbortSignal.timeout(3e3)
|
|
127
|
+
})
|
|
128
|
+
).catch(() => {
|
|
129
|
+
});
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function excludeOwnCard(results, ownLogin) {
|
|
134
|
+
if (!Array.isArray(results)) return results;
|
|
135
|
+
if (typeof ownLogin !== "string" || ownLogin.length === 0) return results;
|
|
136
|
+
const own = ownLogin.toLowerCase();
|
|
137
|
+
return results.filter((r) => {
|
|
138
|
+
const handle = r?.job?.company;
|
|
139
|
+
return typeof handle !== "string" || handle.toLowerCase() !== own;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
export {
|
|
143
|
+
excludeOwnCard,
|
|
144
|
+
fetchDirectory,
|
|
145
|
+
readDirectoryCache,
|
|
146
|
+
readProject,
|
|
147
|
+
relativeTime,
|
|
148
|
+
reportMatched,
|
|
149
|
+
writeDirectoryCache,
|
|
150
|
+
writeProject
|
|
151
|
+
};
|