terminalhire 0.36.0 → 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/README.md +1 -1
- package/dist/bin/cache-store.js +52 -2
- package/dist/bin/claim-push-bg.js +91 -73
- package/dist/bin/directory.js +151 -0
- package/dist/bin/job-status-store.js +856 -0
- package/dist/bin/jpi-bounties.js +2243 -1078
- package/dist/bin/jpi-chat-read.js +294 -146
- package/dist/bin/jpi-chat.js +314 -176
- package/dist/bin/jpi-claim.js +2820 -1313
- package/dist/bin/jpi-config.js +52 -2
- package/dist/bin/jpi-contribute.js +1549 -152
- package/dist/bin/jpi-devs.js +2211 -1073
- package/dist/bin/jpi-dispatch.js +8158 -11886
- package/dist/bin/jpi-hub.js +2309 -1132
- package/dist/bin/jpi-inbox.js +314 -176
- package/dist/bin/jpi-init.js +2103 -919
- package/dist/bin/jpi-intro.js +241 -124
- package/dist/bin/jpi-jobs.js +2940 -1115
- package/dist/bin/jpi-learn.js +235 -112
- package/dist/bin/jpi-link.js +63 -13
- package/dist/bin/jpi-login.js +2228 -1105
- package/dist/bin/jpi-mcp-chat.js +426 -5976
- package/dist/bin/jpi-mcp.js +429 -5958
- package/dist/bin/jpi-profile.js +235 -112
- package/dist/bin/jpi-project.js +2103 -919
- package/dist/bin/jpi-protocol.js +128 -17
- package/dist/bin/jpi-refresh.js +2339 -1164
- package/dist/bin/jpi-repo.js +267 -117
- package/dist/bin/jpi-save.js +235 -112
- package/dist/bin/jpi-spinner.js +208 -76
- package/dist/bin/jpi-sync.js +254 -122
- package/dist/bin/jpi-trajectory.js +274 -140
- 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 -119
- package/dist/src/chat-keystore.js +149 -106
- package/dist/src/claims.js +83 -7
- package/dist/src/config.js +52 -2
- package/dist/src/crypto-store.js +65 -70
- package/dist/src/github-auth.js +65 -74
- package/dist/src/intro.js +237 -120
- package/dist/src/link.js +54 -11
- package/dist/src/policy-acks.js +112 -0
- package/dist/src/profile.js +160 -104
- package/dist/src/protocol.js +123 -17
- package/dist/src/repo-experience.js +159 -103
- 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 -136
- 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/README.md
CHANGED
|
@@ -212,7 +212,7 @@ GitHub (optional enrichment):
|
|
|
212
212
|
## Local profile encryption
|
|
213
213
|
|
|
214
214
|
- Algorithm: **AES-256-GCM** via Node built-in `crypto` (no external deps).
|
|
215
|
-
- Key: stored at `~/.terminalhire/key` with `0600` permissions.
|
|
215
|
+
- Key: stored at `~/.terminalhire/key` with `0600` permissions (a plaintext key file). In a compiled build the OS keychain is **not** used for these stores — the cross-platform plugin ships without `node_modules` and can't reach one. See `docs/adr-002` (keychain promotion is tracked as TERM-9).
|
|
216
216
|
- Profile file: `~/.terminalhire/profile.enc` — JSON blob `{ iv, tag, ciphertext }` (all hex-encoded).
|
|
217
217
|
- GitHub token file: `~/.terminalhire/github-token.enc` — same format, same key.
|
|
218
218
|
|
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,
|
|
@@ -1,50 +1,64 @@
|
|
|
1
|
-
var __create = Object.create;
|
|
2
1
|
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
2
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
3
|
var __esm = (fn, res) => function __init() {
|
|
8
4
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
9
5
|
};
|
|
10
|
-
var __commonJS = (cb, mod) => function __require() {
|
|
11
|
-
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
12
|
-
};
|
|
13
6
|
var __export = (target, all) => {
|
|
14
7
|
for (var name in all)
|
|
15
8
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
9
|
};
|
|
17
|
-
var __copyProps = (to, from, except, desc) => {
|
|
18
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
19
|
-
for (let key of __getOwnPropNames(from))
|
|
20
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
21
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
22
|
-
}
|
|
23
|
-
return to;
|
|
24
|
-
};
|
|
25
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
26
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
27
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
28
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
29
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
30
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
31
|
-
mod
|
|
32
|
-
));
|
|
33
10
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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 {
|
|
40
50
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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();
|
|
48
62
|
}
|
|
49
63
|
});
|
|
50
64
|
|
|
@@ -62,7 +76,15 @@ __export(claims_exports, {
|
|
|
62
76
|
toPushedClaim: () => toPushedClaim,
|
|
63
77
|
updateClaim: () => updateClaim
|
|
64
78
|
});
|
|
65
|
-
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";
|
|
66
88
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
67
89
|
import { join as join2 } from "path";
|
|
68
90
|
import { homedir as homedir2 } from "os";
|
|
@@ -70,7 +92,7 @@ function sleepSync(ms) {
|
|
|
70
92
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
71
93
|
}
|
|
72
94
|
function withClaimsLock(fn) {
|
|
73
|
-
|
|
95
|
+
ensureStateDir(TERMINALHIRE_DIR2);
|
|
74
96
|
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
75
97
|
for (; ; ) {
|
|
76
98
|
try {
|
|
@@ -129,11 +151,15 @@ function readClaims() {
|
|
|
129
151
|
}
|
|
130
152
|
}
|
|
131
153
|
function writeClaims(claims) {
|
|
132
|
-
|
|
154
|
+
ensureStateDir(TERMINALHIRE_DIR2);
|
|
133
155
|
const tmp = `${CLAIMS_FILE}.${process.pid}.${randomBytes2(6).toString("hex")}.tmp`;
|
|
134
156
|
const payload = { claims };
|
|
135
157
|
try {
|
|
136
|
-
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
|
+
});
|
|
137
163
|
renameSync2(tmp, CLAIMS_FILE);
|
|
138
164
|
} catch (err) {
|
|
139
165
|
try {
|
|
@@ -207,6 +233,7 @@ var TERMINALHIRE_DIR2, CLAIMS_FILE, LOCK_DIR, LOCK_STALE_MS, LOCK_RETRY_MS, LOCK
|
|
|
207
233
|
var init_claims = __esm({
|
|
208
234
|
"src/claims.ts"() {
|
|
209
235
|
"use strict";
|
|
236
|
+
init_state_dir();
|
|
210
237
|
TERMINALHIRE_DIR2 = process.env.TERMINALHIRE_DIR || join2(homedir2(), ".terminalhire");
|
|
211
238
|
CLAIMS_FILE = join2(TERMINALHIRE_DIR2, "claims.json");
|
|
212
239
|
LOCK_DIR = `${CLAIMS_FILE}.lock`;
|
|
@@ -224,8 +251,22 @@ var init_claims = __esm({
|
|
|
224
251
|
];
|
|
225
252
|
TERMINAL_STATES = /* @__PURE__ */ new Set(["merged", "abandoned"]);
|
|
226
253
|
POLL_TRANSITIONS = {
|
|
227
|
-
merged: /* @__PURE__ */ new Set([
|
|
228
|
-
|
|
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
|
+
]),
|
|
229
270
|
submitted: /* @__PURE__ */ new Set(["claimed", "working", "in-review", "ready"])
|
|
230
271
|
};
|
|
231
272
|
}
|
|
@@ -233,24 +274,14 @@ var init_claims = __esm({
|
|
|
233
274
|
|
|
234
275
|
// bin/claim-push-bg.js
|
|
235
276
|
import { createHash } from "crypto";
|
|
236
|
-
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3,
|
|
277
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, rmSync as rmSync3 } from "fs";
|
|
237
278
|
import { join as join3 } from "path";
|
|
238
279
|
import { homedir as homedir3 } from "os";
|
|
239
280
|
|
|
240
281
|
// src/github-auth.ts
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
randomBytes
|
|
245
|
-
} from "crypto";
|
|
246
|
-
import {
|
|
247
|
-
readFileSync,
|
|
248
|
-
writeFileSync,
|
|
249
|
-
mkdirSync,
|
|
250
|
-
existsSync,
|
|
251
|
-
rmSync,
|
|
252
|
-
renameSync
|
|
253
|
-
} from "fs";
|
|
282
|
+
init_state_dir();
|
|
283
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "crypto";
|
|
284
|
+
import { readFileSync, writeFileSync, existsSync, rmSync, renameSync } from "fs";
|
|
254
285
|
import { join } from "path";
|
|
255
286
|
import { homedir } from "os";
|
|
256
287
|
var TERMINALHIRE_DIR = process.env.TERMINALHIRE_DIR || join(homedir(), ".terminalhire");
|
|
@@ -259,22 +290,8 @@ var KEY_FILE = join(TERMINALHIRE_DIR, "key");
|
|
|
259
290
|
var ALGO = "aes-256-gcm";
|
|
260
291
|
var KEY_BYTES = 32;
|
|
261
292
|
var IV_BYTES = 12;
|
|
262
|
-
function skipKeychain() {
|
|
263
|
-
return process.env.TERMINALHIRE_NO_KEYCHAIN !== void 0 || process.env.CI !== void 0 || process.env.VITEST !== void 0 || process.env.NODE_ENV === "test";
|
|
264
|
-
}
|
|
265
293
|
async function loadKey() {
|
|
266
|
-
|
|
267
|
-
try {
|
|
268
|
-
const kt = await Promise.resolve().then(() => __toESM(require_keytar(), 1));
|
|
269
|
-
const stored = await kt.getPassword("terminalhire", "profile-key");
|
|
270
|
-
if (stored) return Buffer.from(stored, "hex");
|
|
271
|
-
const key2 = randomBytes(KEY_BYTES);
|
|
272
|
-
await kt.setPassword("terminalhire", "profile-key", key2.toString("hex"));
|
|
273
|
-
return key2;
|
|
274
|
-
} catch {
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
mkdirSync(TERMINALHIRE_DIR, { recursive: true, mode: 448 });
|
|
294
|
+
ensureStateDir(TERMINALHIRE_DIR);
|
|
278
295
|
if (existsSync(KEY_FILE)) {
|
|
279
296
|
return Buffer.from(readFileSync(KEY_FILE, "utf8").trim(), "hex");
|
|
280
297
|
}
|
|
@@ -300,6 +317,7 @@ function decrypt(blob, key) {
|
|
|
300
317
|
}
|
|
301
318
|
|
|
302
319
|
// bin/claim-push-bg.js
|
|
320
|
+
init_state_dir();
|
|
303
321
|
var TERMINALHIRE_DIR3 = process.env.TERMINALHIRE_DIR || join3(homedir3(), ".terminalhire");
|
|
304
322
|
var CLAIM_PUSH_AUTO_MARKER = join3(TERMINALHIRE_DIR3, "claim-push-auto.json");
|
|
305
323
|
var CLAIM_PUSH_TOKEN_FILE = join3(TERMINALHIRE_DIR3, "claim-push-token.enc");
|
|
@@ -308,7 +326,7 @@ var CLAIM_SYNC_BASE = "https://terminalhire.com";
|
|
|
308
326
|
var AUTO_CONSENT_VERSION = 2;
|
|
309
327
|
var AUTO_PUSH_THROTTLE_MS = 24 * 60 * 60 * 1e3;
|
|
310
328
|
async function writePushTokenEnc(rawToken) {
|
|
311
|
-
|
|
329
|
+
ensureStateDir(TERMINALHIRE_DIR3);
|
|
312
330
|
const key = await loadKey();
|
|
313
331
|
const blob = encrypt(rawToken, key);
|
|
314
332
|
writeFileSync3(CLAIM_PUSH_TOKEN_FILE, JSON.stringify(blob, null, 2), { encoding: "utf8" });
|
|
@@ -337,7 +355,7 @@ function readAutoMarker() {
|
|
|
337
355
|
}
|
|
338
356
|
}
|
|
339
357
|
function writeAutoMarker(marker) {
|
|
340
|
-
|
|
358
|
+
ensureStateDir(TERMINALHIRE_DIR3);
|
|
341
359
|
writeFileSync3(CLAIM_PUSH_AUTO_MARKER, JSON.stringify(marker, null, 2) + "\n", "utf8");
|
|
342
360
|
}
|
|
343
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
|
+
};
|