terminalhire 0.1.0 → 0.1.1
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/jpi-config.js +74 -0
- package/dist/bin/jpi-dispatch.js +272 -28
- package/dist/bin/jpi-init.js +126 -0
- package/dist/bin/jpi.js +66 -3
- package/dist/src/config.js +49 -0
- package/install.js +206 -98
- package/package.json +4 -2
- package/postinstall.js +35 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/jpi-config.js
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
var TERMINALHIRE_DIR = join(homedir(), ".terminalhire");
|
|
8
|
+
var CONFIG_FILE = join(TERMINALHIRE_DIR, "config.json");
|
|
9
|
+
var DEFAULT_CONFIG = { nudge: "session" };
|
|
10
|
+
function readConfig() {
|
|
11
|
+
try {
|
|
12
|
+
if (!existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
13
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync(CONFIG_FILE, "utf8")) };
|
|
14
|
+
} catch {
|
|
15
|
+
return { ...DEFAULT_CONFIG };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function writeConfig(patch) {
|
|
19
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
20
|
+
const merged = { ...readConfig(), ...patch };
|
|
21
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
22
|
+
}
|
|
23
|
+
function parseNudgeMode(raw) {
|
|
24
|
+
if (raw === "session" || raw === "always") return raw;
|
|
25
|
+
const m = /^every:(\d+)$/.exec(raw);
|
|
26
|
+
if (m && parseInt(m[1], 10) >= 1) return raw;
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
async function run() {
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
const filtered = args[0] === "config" ? args.slice(1) : args;
|
|
32
|
+
if (filtered.includes("--show") || filtered.length === 0) {
|
|
33
|
+
const cfg = readConfig();
|
|
34
|
+
const envOverride = process.env["TERMINALHIRE_NUDGE"];
|
|
35
|
+
console.log("");
|
|
36
|
+
console.log("terminalhire config");
|
|
37
|
+
console.log("");
|
|
38
|
+
console.log(` nudge: ${cfg.nudge}`);
|
|
39
|
+
if (envOverride) {
|
|
40
|
+
console.log(` (overridden by TERMINALHIRE_NUDGE=${envOverride} at runtime)`);
|
|
41
|
+
}
|
|
42
|
+
console.log(` config file: ${CONFIG_FILE}`);
|
|
43
|
+
console.log("");
|
|
44
|
+
console.log(" Valid nudge values:");
|
|
45
|
+
console.log(" session \u2014 print at most once per Claude Code session (default)");
|
|
46
|
+
console.log(" always \u2014 print every statusLine render when matches exist");
|
|
47
|
+
console.log(" every:N \u2014 print every Nth render (e.g. every:3)");
|
|
48
|
+
console.log("");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const nudgeIdx = filtered.indexOf("--nudge");
|
|
52
|
+
if (nudgeIdx !== -1) {
|
|
53
|
+
const value = filtered[nudgeIdx + 1];
|
|
54
|
+
if (!value) {
|
|
55
|
+
console.error("Error: --nudge requires a value: session | always | every:N");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
const parsed = parseNudgeMode(value);
|
|
59
|
+
if (!parsed) {
|
|
60
|
+
console.error(`Error: invalid nudge value "${value}". Valid: session | always | every:N`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
writeConfig({ nudge: parsed });
|
|
64
|
+
console.log(` nudge set to: ${parsed}`);
|
|
65
|
+
console.log(` (saved to ${CONFIG_FILE})`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
console.error("Usage: terminalhire config --nudge <session|always|every:N>");
|
|
69
|
+
console.error(" terminalhire config --show");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
export {
|
|
73
|
+
run
|
|
74
|
+
};
|
package/dist/bin/jpi-dispatch.js
CHANGED
|
@@ -202,7 +202,7 @@ async function resolveStoredLogin() {
|
|
|
202
202
|
}
|
|
203
203
|
}
|
|
204
204
|
function sleep(ms) {
|
|
205
|
-
return new Promise((
|
|
205
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
206
206
|
}
|
|
207
207
|
var TERMINALHIRE_DIR, TOKEN_FILE, KEY_FILE, ALGO, KEY_BYTES, IV_BYTES, GITHUB_SCOPE, DEVICE_CODE_URL, ACCESS_TOKEN_URL, DEV_PLACEHOLDER_CLIENT_ID, MOCK_TOKEN, MOCK_LOGIN;
|
|
208
208
|
var init_github_auth = __esm({
|
|
@@ -1529,12 +1529,12 @@ async function runLogin() {
|
|
|
1529
1529
|
let ghProfile;
|
|
1530
1530
|
if (process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["TERMINALHIRE_GITHUB_MOCK"] === "1" || process.env["JPI_GITHUB_MOCK"] === "1") {
|
|
1531
1531
|
const { createRequire: createRequire2 } = await import("module");
|
|
1532
|
-
const { fileURLToPath:
|
|
1533
|
-
const { join:
|
|
1534
|
-
const
|
|
1535
|
-
const fixturePath =
|
|
1536
|
-
const { readFileSync:
|
|
1537
|
-
ghProfile = JSON.parse(
|
|
1532
|
+
const { fileURLToPath: fileURLToPath5 } = await import("url");
|
|
1533
|
+
const { join: join9, dirname: dirname2 } = await import("path");
|
|
1534
|
+
const __dirname4 = fileURLToPath5(new URL(".", import.meta.url));
|
|
1535
|
+
const fixturePath = join9(__dirname4, "../../fixtures/github-sample.json");
|
|
1536
|
+
const { readFileSync: readFileSync8 } = await import("fs");
|
|
1537
|
+
ghProfile = JSON.parse(readFileSync8(fixturePath, "utf8"));
|
|
1538
1538
|
} else {
|
|
1539
1539
|
ghProfile = await fetchGitHubProfile2(login, token);
|
|
1540
1540
|
}
|
|
@@ -1645,10 +1645,10 @@ async function fetchIndex() {
|
|
|
1645
1645
|
}
|
|
1646
1646
|
function prompt(question) {
|
|
1647
1647
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1648
|
-
return new Promise((
|
|
1648
|
+
return new Promise((resolve2) => {
|
|
1649
1649
|
rl.question(question, (answer) => {
|
|
1650
1650
|
rl.close();
|
|
1651
|
-
|
|
1651
|
+
resolve2(answer.trim().toLowerCase());
|
|
1652
1652
|
});
|
|
1653
1653
|
});
|
|
1654
1654
|
}
|
|
@@ -1846,10 +1846,10 @@ __export(jpi_profile_exports, {
|
|
|
1846
1846
|
import { createInterface as createInterface2 } from "readline";
|
|
1847
1847
|
function prompt2(question) {
|
|
1848
1848
|
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
1849
|
-
return new Promise((
|
|
1849
|
+
return new Promise((resolve2) => {
|
|
1850
1850
|
rl.question(question, (answer) => {
|
|
1851
1851
|
rl.close();
|
|
1852
|
-
|
|
1852
|
+
resolve2(answer.trim());
|
|
1853
1853
|
});
|
|
1854
1854
|
});
|
|
1855
1855
|
}
|
|
@@ -2195,16 +2195,244 @@ var init_jpi_learn = __esm({
|
|
|
2195
2195
|
}
|
|
2196
2196
|
});
|
|
2197
2197
|
|
|
2198
|
-
// bin/jpi-
|
|
2199
|
-
|
|
2198
|
+
// bin/jpi-config.js
|
|
2199
|
+
var jpi_config_exports = {};
|
|
2200
|
+
__export(jpi_config_exports, {
|
|
2201
|
+
run: () => run5
|
|
2202
|
+
});
|
|
2203
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync4 } from "fs";
|
|
2200
2204
|
import { join as join6 } from "path";
|
|
2201
|
-
import {
|
|
2205
|
+
import { homedir as homedir4 } from "os";
|
|
2206
|
+
function readConfig() {
|
|
2207
|
+
try {
|
|
2208
|
+
if (!existsSync4(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
2209
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync6(CONFIG_FILE, "utf8")) };
|
|
2210
|
+
} catch {
|
|
2211
|
+
return { ...DEFAULT_CONFIG };
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
function writeConfig(patch) {
|
|
2215
|
+
mkdirSync4(TERMINALHIRE_DIR4, { recursive: true });
|
|
2216
|
+
const merged = { ...readConfig(), ...patch };
|
|
2217
|
+
writeFileSync4(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
2218
|
+
}
|
|
2219
|
+
function parseNudgeMode(raw) {
|
|
2220
|
+
if (raw === "session" || raw === "always") return raw;
|
|
2221
|
+
const m = /^every:(\d+)$/.exec(raw);
|
|
2222
|
+
if (m && parseInt(m[1], 10) >= 1) return raw;
|
|
2223
|
+
return null;
|
|
2224
|
+
}
|
|
2225
|
+
async function run5() {
|
|
2226
|
+
const args2 = process.argv.slice(2);
|
|
2227
|
+
const filtered = args2[0] === "config" ? args2.slice(1) : args2;
|
|
2228
|
+
if (filtered.includes("--show") || filtered.length === 0) {
|
|
2229
|
+
const cfg = readConfig();
|
|
2230
|
+
const envOverride = process.env["TERMINALHIRE_NUDGE"];
|
|
2231
|
+
console.log("");
|
|
2232
|
+
console.log("terminalhire config");
|
|
2233
|
+
console.log("");
|
|
2234
|
+
console.log(` nudge: ${cfg.nudge}`);
|
|
2235
|
+
if (envOverride) {
|
|
2236
|
+
console.log(` (overridden by TERMINALHIRE_NUDGE=${envOverride} at runtime)`);
|
|
2237
|
+
}
|
|
2238
|
+
console.log(` config file: ${CONFIG_FILE}`);
|
|
2239
|
+
console.log("");
|
|
2240
|
+
console.log(" Valid nudge values:");
|
|
2241
|
+
console.log(" session \u2014 print at most once per Claude Code session (default)");
|
|
2242
|
+
console.log(" always \u2014 print every statusLine render when matches exist");
|
|
2243
|
+
console.log(" every:N \u2014 print every Nth render (e.g. every:3)");
|
|
2244
|
+
console.log("");
|
|
2245
|
+
return;
|
|
2246
|
+
}
|
|
2247
|
+
const nudgeIdx = filtered.indexOf("--nudge");
|
|
2248
|
+
if (nudgeIdx !== -1) {
|
|
2249
|
+
const value = filtered[nudgeIdx + 1];
|
|
2250
|
+
if (!value) {
|
|
2251
|
+
console.error("Error: --nudge requires a value: session | always | every:N");
|
|
2252
|
+
process.exit(1);
|
|
2253
|
+
}
|
|
2254
|
+
const parsed = parseNudgeMode(value);
|
|
2255
|
+
if (!parsed) {
|
|
2256
|
+
console.error(`Error: invalid nudge value "${value}". Valid: session | always | every:N`);
|
|
2257
|
+
process.exit(1);
|
|
2258
|
+
}
|
|
2259
|
+
writeConfig({ nudge: parsed });
|
|
2260
|
+
console.log(` nudge set to: ${parsed}`);
|
|
2261
|
+
console.log(` (saved to ${CONFIG_FILE})`);
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
console.error("Usage: terminalhire config --nudge <session|always|every:N>");
|
|
2265
|
+
console.error(" terminalhire config --show");
|
|
2266
|
+
process.exit(1);
|
|
2267
|
+
}
|
|
2268
|
+
var TERMINALHIRE_DIR4, CONFIG_FILE, DEFAULT_CONFIG;
|
|
2269
|
+
var init_jpi_config = __esm({
|
|
2270
|
+
"bin/jpi-config.js"() {
|
|
2271
|
+
"use strict";
|
|
2272
|
+
TERMINALHIRE_DIR4 = join6(homedir4(), ".terminalhire");
|
|
2273
|
+
CONFIG_FILE = join6(TERMINALHIRE_DIR4, "config.json");
|
|
2274
|
+
DEFAULT_CONFIG = { nudge: "session" };
|
|
2275
|
+
}
|
|
2276
|
+
});
|
|
2277
|
+
|
|
2278
|
+
// bin/jpi-init.js
|
|
2279
|
+
var jpi_init_exports = {};
|
|
2280
|
+
__export(jpi_init_exports, {
|
|
2281
|
+
run: () => run6
|
|
2282
|
+
});
|
|
2283
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2284
|
+
import { join as join7, resolve } from "path";
|
|
2285
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2286
|
+
import { createInterface as createInterface3 } from "readline";
|
|
2287
|
+
import { spawnSync, spawn } from "child_process";
|
|
2288
|
+
import { homedir as homedir5 } from "os";
|
|
2289
|
+
function ask(question) {
|
|
2290
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
2291
|
+
return new Promise((resolve2) => {
|
|
2292
|
+
rl.question(question, (answer) => {
|
|
2293
|
+
rl.close();
|
|
2294
|
+
resolve2(answer.trim().toLowerCase());
|
|
2295
|
+
});
|
|
2296
|
+
});
|
|
2297
|
+
}
|
|
2298
|
+
function resolveScript(name) {
|
|
2299
|
+
const distPath = resolve(join7(__dirname2, "..", "..", "dist", "bin", `${name}.js`));
|
|
2300
|
+
const legacyPath = resolve(join7(__dirname2, `${name}.js`));
|
|
2301
|
+
return existsSync5(distPath) ? distPath : legacyPath;
|
|
2302
|
+
}
|
|
2303
|
+
function resolveInstallJs() {
|
|
2304
|
+
const fromDist = resolve(join7(__dirname2, "..", "..", "install.js"));
|
|
2305
|
+
const fromBin = resolve(join7(__dirname2, "..", "install.js"));
|
|
2306
|
+
if (existsSync5(fromDist)) return fromDist;
|
|
2307
|
+
if (existsSync5(fromBin)) return fromBin;
|
|
2308
|
+
return fromBin;
|
|
2309
|
+
}
|
|
2310
|
+
async function run6() {
|
|
2311
|
+
console.log("");
|
|
2312
|
+
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\u2500\u2500\u2500\u2500\u2510");
|
|
2313
|
+
console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
|
|
2314
|
+
console.log("\u2502 Local-first job matching for developers in Claude Code \u2502");
|
|
2315
|
+
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\u2500\u2500\u2500\u2500\u2518");
|
|
2316
|
+
console.log("");
|
|
2317
|
+
console.log("This will:");
|
|
2318
|
+
console.log(" 1. Optionally sign you in with GitHub (public profile only, read:user)");
|
|
2319
|
+
console.log(" 2. Seed your local job cache (anonymous index download)");
|
|
2320
|
+
console.log(" 3. Install the statusLine hook in ~/.claude/settings.json");
|
|
2321
|
+
console.log(" (with backup + your explicit consent before any file is touched)");
|
|
2322
|
+
console.log("");
|
|
2323
|
+
console.log('You can stop at any step. Nothing is changed until you say "yes".');
|
|
2324
|
+
console.log("");
|
|
2325
|
+
console.log("Step 1/3 \u2014 GitHub sign-in (optional but recommended)");
|
|
2326
|
+
console.log("");
|
|
2327
|
+
console.log(" Scope: read:user \u2014 public profile + public repos only.");
|
|
2328
|
+
console.log(" Your token is encrypted at ~/.terminalhire/github-token.enc.");
|
|
2329
|
+
console.log(" GitHub data enriches your local profile. Nothing leaves your machine");
|
|
2330
|
+
console.log(" until you explicitly consent to a specific lead.");
|
|
2331
|
+
console.log("");
|
|
2332
|
+
const githubAnswer = await ask("Sign in with GitHub now? [Y/n] (Enter = yes, n = stay local): ");
|
|
2333
|
+
const doGitHub = githubAnswer === "" || githubAnswer === "y" || githubAnswer === "yes";
|
|
2334
|
+
if (doGitHub) {
|
|
2335
|
+
console.log("");
|
|
2336
|
+
console.log(" Starting GitHub device flow...");
|
|
2337
|
+
const loginScript = resolveScript("jpi-login");
|
|
2338
|
+
const child = spawnSync(process.execPath, [loginScript, "login"], {
|
|
2339
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
2340
|
+
env: process.env
|
|
2341
|
+
});
|
|
2342
|
+
if (child.status !== 0) {
|
|
2343
|
+
console.log("");
|
|
2344
|
+
console.log(" GitHub sign-in did not complete. Continuing without GitHub.");
|
|
2345
|
+
console.log(" You can sign in any time with: terminalhire login");
|
|
2346
|
+
}
|
|
2347
|
+
} else {
|
|
2348
|
+
console.log("");
|
|
2349
|
+
console.log(" Staying local-only. Tags accumulate from your personal project sessions.");
|
|
2350
|
+
console.log(" Sign in any time with: terminalhire login");
|
|
2351
|
+
}
|
|
2352
|
+
console.log("");
|
|
2353
|
+
console.log("Step 2/3 \u2014 Seeding local job cache");
|
|
2354
|
+
console.log("");
|
|
2355
|
+
console.log(" Fetching anonymous job index (no dev data sent)...");
|
|
2356
|
+
const jobsScript = resolveScript("jpi-jobs");
|
|
2357
|
+
const seedChild = spawnSync(
|
|
2358
|
+
process.execPath,
|
|
2359
|
+
[jobsScript, "--limit", "0"],
|
|
2360
|
+
{
|
|
2361
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2362
|
+
env: { ...process.env, TERMINALHIRE_SEED_ONLY: "1" },
|
|
2363
|
+
timeout: 15e3
|
|
2364
|
+
}
|
|
2365
|
+
);
|
|
2366
|
+
if (seedChild.status === 0) {
|
|
2367
|
+
console.log(" Job cache seeded successfully.");
|
|
2368
|
+
} else {
|
|
2369
|
+
console.log(" Could not seed job cache right now (no profile tags yet, or offline).");
|
|
2370
|
+
console.log(" Run `terminalhire jobs` after a few Claude Code sessions to populate it.");
|
|
2371
|
+
}
|
|
2372
|
+
console.log("");
|
|
2373
|
+
console.log("Step 3/3 \u2014 Install statusLine hook in ~/.claude/settings.json");
|
|
2374
|
+
console.log("");
|
|
2375
|
+
console.log(" This is the only step that modifies a system file.");
|
|
2376
|
+
console.log(" A timestamped backup is created before any change.");
|
|
2377
|
+
console.log(" Uninstall at any time: node install.js --uninstall");
|
|
2378
|
+
console.log("");
|
|
2379
|
+
const installJs = resolveInstallJs();
|
|
2380
|
+
const installChild = spawnSync(process.execPath, [installJs], {
|
|
2381
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
2382
|
+
env: process.env
|
|
2383
|
+
});
|
|
2384
|
+
if (installChild.status !== 0) {
|
|
2385
|
+
console.log("");
|
|
2386
|
+
console.log(" Hook installation did not complete. Run manually: node install.js");
|
|
2387
|
+
}
|
|
2388
|
+
console.log("");
|
|
2389
|
+
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\u2500\u2500\u2500\u2500\u2510");
|
|
2390
|
+
console.log("\u2502 terminalhire init complete! \u2502");
|
|
2391
|
+
console.log("\u2502 \u2502");
|
|
2392
|
+
console.log("\u2502 Restart Claude Code to see the statusLine nudge. \u2502");
|
|
2393
|
+
console.log("\u2502 \u2502");
|
|
2394
|
+
console.log("\u2502 Quick reference: \u2502");
|
|
2395
|
+
console.log("\u2502 terminalhire jobs \u2014 browse matching roles \u2502");
|
|
2396
|
+
console.log("\u2502 terminalhire config --show \u2014 view nudge settings \u2502");
|
|
2397
|
+
console.log("\u2502 terminalhire login \u2014 sign in with GitHub \u2502");
|
|
2398
|
+
console.log("\u2502 terminalhire profile --show \u2014 inspect your local profile \u2502");
|
|
2399
|
+
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\u2500\u2500\u2500\u2500\u2518");
|
|
2400
|
+
console.log("");
|
|
2401
|
+
}
|
|
2402
|
+
var __dirname2;
|
|
2403
|
+
var init_jpi_init = __esm({
|
|
2404
|
+
"bin/jpi-init.js"() {
|
|
2405
|
+
"use strict";
|
|
2406
|
+
__dirname2 = fileURLToPath3(new URL(".", import.meta.url));
|
|
2407
|
+
}
|
|
2408
|
+
});
|
|
2409
|
+
|
|
2410
|
+
// bin/jpi-dispatch.js
|
|
2411
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
2412
|
+
import { join as join8, dirname } from "path";
|
|
2413
|
+
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
2202
2414
|
import { createRequire } from "module";
|
|
2203
|
-
var
|
|
2415
|
+
var __dirname3 = fileURLToPath4(new URL(".", import.meta.url));
|
|
2416
|
+
function readPackageVersion() {
|
|
2417
|
+
try {
|
|
2418
|
+
const candidates = [
|
|
2419
|
+
join8(__dirname3, "..", "..", "package.json"),
|
|
2420
|
+
join8(__dirname3, "..", "package.json")
|
|
2421
|
+
];
|
|
2422
|
+
for (const p of candidates) {
|
|
2423
|
+
if (existsSync6(p)) {
|
|
2424
|
+
const pkg = JSON.parse(readFileSync7(p, "utf8"));
|
|
2425
|
+
if (pkg.version) return pkg.version;
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
} catch {
|
|
2429
|
+
}
|
|
2430
|
+
return "0.1.1";
|
|
2431
|
+
}
|
|
2204
2432
|
var firstArg = process.argv[2];
|
|
2205
2433
|
if (!firstArg && !process.stdin.isTTY) {
|
|
2206
2434
|
const { default: childProcess } = await import("child_process");
|
|
2207
|
-
const nudgeScript =
|
|
2435
|
+
const nudgeScript = join8(__dirname3, "jpi.js");
|
|
2208
2436
|
const child = childProcess.spawnSync(process.execPath, [nudgeScript], {
|
|
2209
2437
|
stdio: ["inherit", "inherit", "inherit"]
|
|
2210
2438
|
});
|
|
@@ -2212,21 +2440,27 @@ if (!firstArg && !process.stdin.isTTY) {
|
|
|
2212
2440
|
}
|
|
2213
2441
|
if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-h") {
|
|
2214
2442
|
console.log("");
|
|
2215
|
-
console.log(
|
|
2443
|
+
console.log(`terminalhire v${readPackageVersion()} \u2014 local-first job matching for developers`);
|
|
2216
2444
|
console.log("");
|
|
2217
2445
|
console.log("Commands:");
|
|
2218
|
-
console.log(" terminalhire
|
|
2219
|
-
console.log(" terminalhire
|
|
2220
|
-
console.log(" terminalhire
|
|
2221
|
-
console.log(" terminalhire jobs
|
|
2222
|
-
console.log(" terminalhire jobs --
|
|
2223
|
-
console.log(" terminalhire
|
|
2224
|
-
console.log(" terminalhire profile --
|
|
2225
|
-
console.log(" terminalhire profile --
|
|
2446
|
+
console.log(" terminalhire init One-command onboarding (start here)");
|
|
2447
|
+
console.log(" terminalhire login Sign in with GitHub (enriches profile instantly)");
|
|
2448
|
+
console.log(" terminalhire logout Clear stored GitHub token");
|
|
2449
|
+
console.log(" terminalhire jobs Fetch job index, match locally, browse roles");
|
|
2450
|
+
console.log(" terminalhire jobs --limit N Show top N results (default: 10)");
|
|
2451
|
+
console.log(" terminalhire jobs --remote-only Filter to remote roles only");
|
|
2452
|
+
console.log(" terminalhire profile --show Display your encrypted local profile");
|
|
2453
|
+
console.log(" terminalhire profile --edit Set displayName, contactEmail, prefs");
|
|
2454
|
+
console.log(" terminalhire profile --delete Wipe profile and encryption key from disk");
|
|
2455
|
+
console.log(" terminalhire config --nudge session Nudge at most once per session (default)");
|
|
2456
|
+
console.log(" terminalhire config --nudge always Nudge every statusLine render");
|
|
2457
|
+
console.log(" terminalhire config --nudge every:N Nudge every Nth render");
|
|
2458
|
+
console.log(" terminalhire config --show Print current config");
|
|
2226
2459
|
console.log("");
|
|
2227
2460
|
console.log("Install / uninstall the Claude Code statusLine nudge:");
|
|
2228
|
-
console.log("
|
|
2229
|
-
console.log(" node install.js
|
|
2461
|
+
console.log(" terminalhire init (preferred \u2014 full guided onboarding)");
|
|
2462
|
+
console.log(" node install.js (manual install)");
|
|
2463
|
+
console.log(" node install.js --uninstall (manual uninstall)");
|
|
2230
2464
|
console.log("");
|
|
2231
2465
|
console.log("Privacy: your profile never leaves your device.");
|
|
2232
2466
|
console.log(" GET /api/index \u2014 anonymous index download (no dev data)");
|
|
@@ -2236,7 +2470,7 @@ if (!firstArg || firstArg === "help" || firstArg === "--help" || firstArg === "-
|
|
|
2236
2470
|
process.exit(0);
|
|
2237
2471
|
}
|
|
2238
2472
|
if (firstArg === "--version" || firstArg === "-v") {
|
|
2239
|
-
console.log(
|
|
2473
|
+
console.log(`terminalhire v${readPackageVersion()}`);
|
|
2240
2474
|
process.exit(0);
|
|
2241
2475
|
}
|
|
2242
2476
|
if (firstArg === "login" || firstArg === "logout") {
|
|
@@ -2260,5 +2494,15 @@ if (firstArg === "learn") {
|
|
|
2260
2494
|
await mod.run();
|
|
2261
2495
|
process.exit(0);
|
|
2262
2496
|
}
|
|
2497
|
+
if (firstArg === "config") {
|
|
2498
|
+
const mod = await Promise.resolve().then(() => (init_jpi_config(), jpi_config_exports));
|
|
2499
|
+
await mod.run();
|
|
2500
|
+
process.exit(0);
|
|
2501
|
+
}
|
|
2502
|
+
if (firstArg === "init") {
|
|
2503
|
+
const mod = await Promise.resolve().then(() => (init_jpi_init(), jpi_init_exports));
|
|
2504
|
+
await mod.run();
|
|
2505
|
+
process.exit(0);
|
|
2506
|
+
}
|
|
2263
2507
|
console.error(`terminalhire: unknown command '${firstArg}'. Run 'terminalhire help' for usage.`);
|
|
2264
2508
|
process.exit(1);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/jpi-init.js
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { join, resolve } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { createInterface } from "readline";
|
|
8
|
+
import { spawnSync, spawn } from "child_process";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
var __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
11
|
+
function ask(question) {
|
|
12
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
return new Promise((resolve2) => {
|
|
14
|
+
rl.question(question, (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
resolve2(answer.trim().toLowerCase());
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function resolveScript(name) {
|
|
21
|
+
const distPath = resolve(join(__dirname, "..", "..", "dist", "bin", `${name}.js`));
|
|
22
|
+
const legacyPath = resolve(join(__dirname, `${name}.js`));
|
|
23
|
+
return existsSync(distPath) ? distPath : legacyPath;
|
|
24
|
+
}
|
|
25
|
+
function resolveInstallJs() {
|
|
26
|
+
const fromDist = resolve(join(__dirname, "..", "..", "install.js"));
|
|
27
|
+
const fromBin = resolve(join(__dirname, "..", "install.js"));
|
|
28
|
+
if (existsSync(fromDist)) return fromDist;
|
|
29
|
+
if (existsSync(fromBin)) return fromBin;
|
|
30
|
+
return fromBin;
|
|
31
|
+
}
|
|
32
|
+
async function run() {
|
|
33
|
+
console.log("");
|
|
34
|
+
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\u2500\u2500\u2500\u2500\u2510");
|
|
35
|
+
console.log("\u2502 terminalhire init \u2014 one-command onboarding \u2502");
|
|
36
|
+
console.log("\u2502 Local-first job matching for developers in Claude Code \u2502");
|
|
37
|
+
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\u2500\u2500\u2500\u2500\u2518");
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log("This will:");
|
|
40
|
+
console.log(" 1. Optionally sign you in with GitHub (public profile only, read:user)");
|
|
41
|
+
console.log(" 2. Seed your local job cache (anonymous index download)");
|
|
42
|
+
console.log(" 3. Install the statusLine hook in ~/.claude/settings.json");
|
|
43
|
+
console.log(" (with backup + your explicit consent before any file is touched)");
|
|
44
|
+
console.log("");
|
|
45
|
+
console.log('You can stop at any step. Nothing is changed until you say "yes".');
|
|
46
|
+
console.log("");
|
|
47
|
+
console.log("Step 1/3 \u2014 GitHub sign-in (optional but recommended)");
|
|
48
|
+
console.log("");
|
|
49
|
+
console.log(" Scope: read:user \u2014 public profile + public repos only.");
|
|
50
|
+
console.log(" Your token is encrypted at ~/.terminalhire/github-token.enc.");
|
|
51
|
+
console.log(" GitHub data enriches your local profile. Nothing leaves your machine");
|
|
52
|
+
console.log(" until you explicitly consent to a specific lead.");
|
|
53
|
+
console.log("");
|
|
54
|
+
const githubAnswer = await ask("Sign in with GitHub now? [Y/n] (Enter = yes, n = stay local): ");
|
|
55
|
+
const doGitHub = githubAnswer === "" || githubAnswer === "y" || githubAnswer === "yes";
|
|
56
|
+
if (doGitHub) {
|
|
57
|
+
console.log("");
|
|
58
|
+
console.log(" Starting GitHub device flow...");
|
|
59
|
+
const loginScript = resolveScript("jpi-login");
|
|
60
|
+
const child = spawnSync(process.execPath, [loginScript, "login"], {
|
|
61
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
62
|
+
env: process.env
|
|
63
|
+
});
|
|
64
|
+
if (child.status !== 0) {
|
|
65
|
+
console.log("");
|
|
66
|
+
console.log(" GitHub sign-in did not complete. Continuing without GitHub.");
|
|
67
|
+
console.log(" You can sign in any time with: terminalhire login");
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
console.log("");
|
|
71
|
+
console.log(" Staying local-only. Tags accumulate from your personal project sessions.");
|
|
72
|
+
console.log(" Sign in any time with: terminalhire login");
|
|
73
|
+
}
|
|
74
|
+
console.log("");
|
|
75
|
+
console.log("Step 2/3 \u2014 Seeding local job cache");
|
|
76
|
+
console.log("");
|
|
77
|
+
console.log(" Fetching anonymous job index (no dev data sent)...");
|
|
78
|
+
const jobsScript = resolveScript("jpi-jobs");
|
|
79
|
+
const seedChild = spawnSync(
|
|
80
|
+
process.execPath,
|
|
81
|
+
[jobsScript, "--limit", "0"],
|
|
82
|
+
{
|
|
83
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
84
|
+
env: { ...process.env, TERMINALHIRE_SEED_ONLY: "1" },
|
|
85
|
+
timeout: 15e3
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
if (seedChild.status === 0) {
|
|
89
|
+
console.log(" Job cache seeded successfully.");
|
|
90
|
+
} else {
|
|
91
|
+
console.log(" Could not seed job cache right now (no profile tags yet, or offline).");
|
|
92
|
+
console.log(" Run `terminalhire jobs` after a few Claude Code sessions to populate it.");
|
|
93
|
+
}
|
|
94
|
+
console.log("");
|
|
95
|
+
console.log("Step 3/3 \u2014 Install statusLine hook in ~/.claude/settings.json");
|
|
96
|
+
console.log("");
|
|
97
|
+
console.log(" This is the only step that modifies a system file.");
|
|
98
|
+
console.log(" A timestamped backup is created before any change.");
|
|
99
|
+
console.log(" Uninstall at any time: node install.js --uninstall");
|
|
100
|
+
console.log("");
|
|
101
|
+
const installJs = resolveInstallJs();
|
|
102
|
+
const installChild = spawnSync(process.execPath, [installJs], {
|
|
103
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
104
|
+
env: process.env
|
|
105
|
+
});
|
|
106
|
+
if (installChild.status !== 0) {
|
|
107
|
+
console.log("");
|
|
108
|
+
console.log(" Hook installation did not complete. Run manually: node install.js");
|
|
109
|
+
}
|
|
110
|
+
console.log("");
|
|
111
|
+
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\u2500\u2500\u2500\u2500\u2510");
|
|
112
|
+
console.log("\u2502 terminalhire init complete! \u2502");
|
|
113
|
+
console.log("\u2502 \u2502");
|
|
114
|
+
console.log("\u2502 Restart Claude Code to see the statusLine nudge. \u2502");
|
|
115
|
+
console.log("\u2502 \u2502");
|
|
116
|
+
console.log("\u2502 Quick reference: \u2502");
|
|
117
|
+
console.log("\u2502 terminalhire jobs \u2014 browse matching roles \u2502");
|
|
118
|
+
console.log("\u2502 terminalhire config --show \u2014 view nudge settings \u2502");
|
|
119
|
+
console.log("\u2502 terminalhire login \u2014 sign in with GitHub \u2502");
|
|
120
|
+
console.log("\u2502 terminalhire profile --show \u2014 inspect your local profile \u2502");
|
|
121
|
+
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\u2500\u2500\u2500\u2500\u2518");
|
|
122
|
+
console.log("");
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
run
|
|
126
|
+
};
|
package/dist/bin/jpi.js
CHANGED
|
@@ -9,6 +9,7 @@ import { spawn } from "child_process";
|
|
|
9
9
|
var TERMINALHIRE_DIR = join(homedir(), ".terminalhire");
|
|
10
10
|
var INDEX_CACHE_FILE = join(TERMINALHIRE_DIR, "index-cache.json");
|
|
11
11
|
var NUDGE_FILE = join(TERMINALHIRE_DIR, "nudged.json");
|
|
12
|
+
var NUDGE_COUNTER_FILE = join(TERMINALHIRE_DIR, "nudge-counter.json");
|
|
12
13
|
var LEARNED_FILE = join(TERMINALHIRE_DIR, "learned-sessions.json");
|
|
13
14
|
var INDEX_CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
14
15
|
var __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
@@ -82,6 +83,66 @@ function getCachedMatchCount() {
|
|
|
82
83
|
return null;
|
|
83
84
|
}
|
|
84
85
|
}
|
|
86
|
+
function getNudgeMode() {
|
|
87
|
+
const envVal = process.env["TERMINALHIRE_NUDGE"];
|
|
88
|
+
if (envVal) {
|
|
89
|
+
const parsed = parseNudgeMode(envVal);
|
|
90
|
+
if (parsed) return parsed;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const configFile = join(TERMINALHIRE_DIR, "config.json");
|
|
94
|
+
if (existsSync(configFile)) {
|
|
95
|
+
const cfg = JSON.parse(readFileSync(configFile, "utf8"));
|
|
96
|
+
if (cfg.nudge) {
|
|
97
|
+
const parsed = parseNudgeMode(cfg.nudge);
|
|
98
|
+
if (parsed) return parsed;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
return "session";
|
|
104
|
+
}
|
|
105
|
+
function parseNudgeMode(raw) {
|
|
106
|
+
if (raw === "session" || raw === "always") return raw;
|
|
107
|
+
const m = /^every:(\d+)$/.exec(raw);
|
|
108
|
+
if (m) {
|
|
109
|
+
const n = parseInt(m[1], 10);
|
|
110
|
+
if (n >= 1) return `every:${n}`;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
function bumpRenderCounter() {
|
|
115
|
+
try {
|
|
116
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
117
|
+
let counter = 0;
|
|
118
|
+
if (existsSync(NUDGE_COUNTER_FILE)) {
|
|
119
|
+
const raw = JSON.parse(readFileSync(NUDGE_COUNTER_FILE, "utf8"));
|
|
120
|
+
counter = typeof raw.count === "number" ? raw.count : 0;
|
|
121
|
+
}
|
|
122
|
+
counter++;
|
|
123
|
+
writeFileSync(NUDGE_COUNTER_FILE, JSON.stringify({ count: counter }), "utf8");
|
|
124
|
+
return counter;
|
|
125
|
+
} catch {
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function shouldNudge(nudgeMode, sessionId) {
|
|
130
|
+
if (nudgeMode === "always") {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (nudgeMode === "session") {
|
|
134
|
+
const nudged2 = readNudged();
|
|
135
|
+
return !nudged2[sessionId];
|
|
136
|
+
}
|
|
137
|
+
const m = /^every:(\d+)$/.exec(nudgeMode);
|
|
138
|
+
if (m) {
|
|
139
|
+
const n = parseInt(m[1], 10);
|
|
140
|
+
const count = bumpRenderCounter();
|
|
141
|
+
return count % n === 0;
|
|
142
|
+
}
|
|
143
|
+
const nudged = readNudged();
|
|
144
|
+
return !nudged[sessionId];
|
|
145
|
+
}
|
|
85
146
|
try {
|
|
86
147
|
const input = readStdinSync();
|
|
87
148
|
const sessionId = input?.session_id;
|
|
@@ -92,14 +153,16 @@ try {
|
|
|
92
153
|
spawnLearnDetached(workDir);
|
|
93
154
|
markLearned(sessionId);
|
|
94
155
|
}
|
|
95
|
-
const nudged = readNudged();
|
|
96
|
-
if (nudged[sessionId]) process.exit(0);
|
|
97
156
|
const matchCount = getCachedMatchCount();
|
|
98
157
|
if (matchCount === null || matchCount === 0) process.exit(0);
|
|
158
|
+
const nudgeMode = getNudgeMode();
|
|
159
|
+
if (!shouldNudge(nudgeMode, sessionId)) process.exit(0);
|
|
99
160
|
const plural = matchCount === 1 ? "role" : "roles";
|
|
100
161
|
process.stdout.write(`\u2726 ${matchCount} ${plural} match your current work \u2014 run: terminalhire jobs
|
|
101
162
|
`);
|
|
102
|
-
|
|
163
|
+
if (nudgeMode === "session") {
|
|
164
|
+
markNudged(sessionId);
|
|
165
|
+
}
|
|
103
166
|
process.exit(0);
|
|
104
167
|
} catch {
|
|
105
168
|
process.exit(0);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
var TERMINALHIRE_DIR = join(homedir(), ".terminalhire");
|
|
6
|
+
var CONFIG_FILE = join(TERMINALHIRE_DIR, "config.json");
|
|
7
|
+
var DEFAULT_CONFIG = {
|
|
8
|
+
nudge: "session"
|
|
9
|
+
};
|
|
10
|
+
function readConfig() {
|
|
11
|
+
try {
|
|
12
|
+
if (!existsSync(CONFIG_FILE)) return { ...DEFAULT_CONFIG };
|
|
13
|
+
const raw = readFileSync(CONFIG_FILE, "utf8");
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
return { ...DEFAULT_CONFIG, ...parsed };
|
|
16
|
+
} catch {
|
|
17
|
+
return { ...DEFAULT_CONFIG };
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function writeConfig(config) {
|
|
21
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
22
|
+
const current = readConfig();
|
|
23
|
+
const merged = { ...current, ...config };
|
|
24
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
25
|
+
}
|
|
26
|
+
function parseNudgeMode(raw) {
|
|
27
|
+
if (raw === "session" || raw === "always") return raw;
|
|
28
|
+
const m = /^every:(\d+)$/.exec(raw);
|
|
29
|
+
if (m) {
|
|
30
|
+
const n = parseInt(m[1], 10);
|
|
31
|
+
if (n >= 1) return `every:${n}`;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
function getNudgeMode() {
|
|
36
|
+
const envVal = process.env["TERMINALHIRE_NUDGE"];
|
|
37
|
+
if (envVal) {
|
|
38
|
+
const parsed = parseNudgeMode(envVal);
|
|
39
|
+
if (parsed) return parsed;
|
|
40
|
+
}
|
|
41
|
+
const config = readConfig();
|
|
42
|
+
return config.nudge ?? "session";
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
getNudgeMode,
|
|
46
|
+
parseNudgeMode,
|
|
47
|
+
readConfig,
|
|
48
|
+
writeConfig
|
|
49
|
+
};
|
package/install.js
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* install.js — v3 installer for
|
|
3
|
+
* install.js — v3.1 installer for terminalhire
|
|
4
|
+
*
|
|
5
|
+
* ToS / safety guardrails (binding — see CONSTITUTION.md Rule 7 and Rule 12):
|
|
6
|
+
* - ONLY uses the official Claude Code `statusLine` settings.json hook.
|
|
7
|
+
* - NEVER silently modifies ~/.claude/settings.json.
|
|
8
|
+
* - Always backs up settings.json (timestamped) before any write.
|
|
9
|
+
* - Prints a clear disclosure of exactly what it changed.
|
|
10
|
+
* - Provides one-command uninstall that restores the backup.
|
|
11
|
+
* - CHAIN, never clobber: if a statusLine already exists that is NOT ours,
|
|
12
|
+
* creates a wrapper script that runs BOTH commands, then points settings.json
|
|
13
|
+
* at the wrapper. Idempotent — detects existing wrapper and does not double-wrap.
|
|
14
|
+
* - postinstall (not this file) is print-only and never calls this file.
|
|
4
15
|
*
|
|
5
16
|
* What it does:
|
|
6
|
-
* 1. Prints
|
|
17
|
+
* 1. Prints full v3 disclosure
|
|
7
18
|
* 2. Requires explicit "yes" before touching any system file
|
|
8
|
-
* 3. Backs up ~/.claude/settings.json
|
|
9
|
-
* 4.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
19
|
+
* 3. Backs up ~/.claude/settings.json (timestamped)
|
|
20
|
+
* 4. If no existing statusLine → sets terminalhire's nudge directly
|
|
21
|
+
* If existing statusLine (not ours) → builds a wrapper that chains both
|
|
22
|
+
* 5. Supports --uninstall (restores the backup / removes wrapper)
|
|
12
23
|
* 7. Idempotent: safe to run multiple times
|
|
13
24
|
*
|
|
14
25
|
* Usage:
|
|
@@ -16,7 +27,10 @@
|
|
|
16
27
|
* node install.js --uninstall
|
|
17
28
|
*/
|
|
18
29
|
|
|
19
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
readFileSync, writeFileSync, copyFileSync, existsSync,
|
|
32
|
+
mkdirSync, chmodSync, readdirSync,
|
|
33
|
+
} from 'node:fs';
|
|
20
34
|
import { homedir } from 'node:os';
|
|
21
35
|
import { join, resolve, dirname } from 'node:path';
|
|
22
36
|
import { fileURLToPath } from 'node:url';
|
|
@@ -24,15 +38,26 @@ import { createInterface } from 'node:readline';
|
|
|
24
38
|
import { spawnSync } from 'node:child_process';
|
|
25
39
|
|
|
26
40
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
41
|
+
|
|
27
42
|
// Resolve the nudge bin robustly: prefer the bundled dist output (published package),
|
|
28
43
|
// fall back to the legacy bin/ path for in-workspace / development installs.
|
|
29
44
|
const _distBin = resolve(join(__dirname, 'dist', 'bin', 'jpi.js'));
|
|
30
45
|
const _legacyBin = resolve(join(__dirname, 'bin', 'jpi.js'));
|
|
31
46
|
const BIN_PATH = existsSync(_distBin) ? _distBin : _legacyBin;
|
|
47
|
+
|
|
32
48
|
const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
|
|
33
49
|
const SETTINGS_DIR = dirname(SETTINGS_PATH);
|
|
50
|
+
const TERMINALHIRE_DIR = join(homedir(), '.terminalhire');
|
|
51
|
+
const WRAPPER_PATH = join(TERMINALHIRE_DIR, 'statusline-wrapper.sh');
|
|
52
|
+
|
|
53
|
+
// The existing statusLine command on the user's machine that we must preserve
|
|
54
|
+
const KNOWN_EXISTING_STATUSLINE = 'bash /Users/ericgang/.claude/statusline-command.sh';
|
|
55
|
+
|
|
34
56
|
const UNINSTALL = process.argv.includes('--uninstall');
|
|
35
57
|
|
|
58
|
+
// ── Sentinel comment embedded in the wrapper so we can detect our own wrappers
|
|
59
|
+
const WRAPPER_SENTINEL = '# terminalhire-wrapper-v1';
|
|
60
|
+
|
|
36
61
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
37
62
|
|
|
38
63
|
function readSettings() {
|
|
@@ -47,11 +72,12 @@ function readSettings() {
|
|
|
47
72
|
}
|
|
48
73
|
|
|
49
74
|
function backupSettings() {
|
|
50
|
-
if (!existsSync(SETTINGS_PATH)) return;
|
|
75
|
+
if (!existsSync(SETTINGS_PATH)) return null;
|
|
51
76
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
52
77
|
const backupPath = `${SETTINGS_PATH}.terminalhire-backup-${ts}`;
|
|
53
78
|
copyFileSync(SETTINGS_PATH, backupPath);
|
|
54
79
|
console.log(` Backed up settings to: ${backupPath}`);
|
|
80
|
+
return backupPath;
|
|
55
81
|
}
|
|
56
82
|
|
|
57
83
|
function writeSettings(settings) {
|
|
@@ -61,24 +87,95 @@ function writeSettings(settings) {
|
|
|
61
87
|
|
|
62
88
|
function ask(question) {
|
|
63
89
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
64
|
-
return new Promise(
|
|
90
|
+
return new Promise(res => {
|
|
65
91
|
rl.question(question, answer => {
|
|
66
92
|
rl.close();
|
|
67
|
-
|
|
93
|
+
res(answer.trim().toLowerCase());
|
|
68
94
|
});
|
|
69
95
|
});
|
|
70
96
|
}
|
|
71
97
|
|
|
72
|
-
|
|
98
|
+
/** Build the direct (no-chain) statusLine entry for terminalhire. */
|
|
99
|
+
function buildDirectEntry(binPath) {
|
|
73
100
|
return `node ${binPath}`;
|
|
74
101
|
}
|
|
75
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Returns true if the given statusLine value is terminalhire's own entry:
|
|
105
|
+
* either the direct node entry or our wrapper script.
|
|
106
|
+
*/
|
|
107
|
+
function isOurEntry(statusLine) {
|
|
108
|
+
if (!statusLine) return false;
|
|
109
|
+
// Direct entry
|
|
110
|
+
if (statusLine === buildDirectEntry(BIN_PATH)) return true;
|
|
111
|
+
// Points at our wrapper
|
|
112
|
+
if (statusLine === `bash ${WRAPPER_PATH}`) return true;
|
|
113
|
+
// Wrapper exists and contains our sentinel
|
|
114
|
+
if (existsSync(WRAPPER_PATH)) {
|
|
115
|
+
try {
|
|
116
|
+
const wrapperContent = readFileSync(WRAPPER_PATH, 'utf8');
|
|
117
|
+
return wrapperContent.includes(WRAPPER_SENTINEL);
|
|
118
|
+
} catch { /* fall through */ }
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build the chaining wrapper shell script.
|
|
125
|
+
* Both the existing command and the terminalhire nudge receive the SAME stdin JSON.
|
|
126
|
+
* Output: existing command's output first, then terminalhire's output.
|
|
127
|
+
*/
|
|
128
|
+
function buildWrapper(existingCmd) {
|
|
129
|
+
return `#!/usr/bin/env bash
|
|
130
|
+
${WRAPPER_SENTINEL}
|
|
131
|
+
# Chains the original statusLine command with terminalhire's nudge.
|
|
132
|
+
# Both receive the same stdin JSON. Original output prints first, then terminalhire.
|
|
133
|
+
# Generated by: node install.js
|
|
134
|
+
# Existing command: ${existingCmd}
|
|
135
|
+
# terminalhire nudge: node ${BIN_PATH}
|
|
136
|
+
|
|
137
|
+
# Read stdin once into a variable
|
|
138
|
+
INPUT=$(cat)
|
|
139
|
+
|
|
140
|
+
# Run the existing statusLine command
|
|
141
|
+
EXISTING_OUT=$(printf '%s' "$INPUT" | ${existingCmd} 2>/dev/null || true)
|
|
142
|
+
|
|
143
|
+
# Run terminalhire's nudge
|
|
144
|
+
TH_OUT=$(printf '%s' "$INPUT" | node ${BIN_PATH} 2>/dev/null || true)
|
|
145
|
+
|
|
146
|
+
# Print existing output (if any), then terminalhire output (if any)
|
|
147
|
+
if [ -n "$EXISTING_OUT" ]; then
|
|
148
|
+
printf '%s\\n' "$EXISTING_OUT"
|
|
149
|
+
fi
|
|
150
|
+
if [ -n "$TH_OUT" ]; then
|
|
151
|
+
printf '%s\\n' "$TH_OUT"
|
|
152
|
+
fi
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Find the most recent terminalhire backup of settings.json.
|
|
158
|
+
* Returns the path or null.
|
|
159
|
+
*/
|
|
160
|
+
function findLatestBackup() {
|
|
161
|
+
try {
|
|
162
|
+
const files = readdirSync(SETTINGS_DIR)
|
|
163
|
+
.filter(f => f.startsWith('settings.json.terminalhire-backup-'))
|
|
164
|
+
.sort()
|
|
165
|
+
.reverse();
|
|
166
|
+
if (files.length === 0) return null;
|
|
167
|
+
return join(SETTINGS_DIR, files[0]);
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
76
173
|
// ── Install ───────────────────────────────────────────────────────────────────
|
|
77
174
|
|
|
78
175
|
async function install() {
|
|
79
176
|
console.log('');
|
|
80
177
|
console.log('┌─────────────────────────────────────────────────────────────────┐');
|
|
81
|
-
console.log('│
|
|
178
|
+
console.log('│ terminalhire v0.1.1 — install statusLine hook │');
|
|
82
179
|
console.log('│ Pull your matches. Your profile stays on-device. │');
|
|
83
180
|
console.log('└─────────────────────────────────────────────────────────────────┘');
|
|
84
181
|
console.log('');
|
|
@@ -88,8 +185,10 @@ async function install() {
|
|
|
88
185
|
console.log(' 1. `terminalhire jobs` downloads an anonymous job index from the server');
|
|
89
186
|
console.log(' (GET /api/index — no dev data in the request).');
|
|
90
187
|
console.log(' 2. Matching runs LOCALLY against an encrypted profile on your device.');
|
|
91
|
-
console.log(' 3. The status bar shows a
|
|
92
|
-
console.log(' It reads
|
|
188
|
+
console.log(' 3. The status bar shows a nudge when matches exist.');
|
|
189
|
+
console.log(' It reads only local files and makes zero network calls.');
|
|
190
|
+
console.log(' 4. Nudge frequency: configurable via `terminalhire config --nudge`.');
|
|
191
|
+
console.log(' Default: once per session. Options: always | every:N.');
|
|
93
192
|
console.log('');
|
|
94
193
|
console.log('YOUR LOCAL PROFILE (~/.terminalhire/profile.enc):');
|
|
95
194
|
console.log(' • Encrypted at rest with AES-256-GCM (Node built-in crypto).');
|
|
@@ -112,33 +211,6 @@ async function install() {
|
|
|
112
211
|
console.log(' • GitHub data enriches your LOCAL profile — no data leaves the machine');
|
|
113
212
|
console.log(' unless you consent to include GitHub fields in a specific terminalhire lead.');
|
|
114
213
|
console.log('');
|
|
115
|
-
console.log('LEAD PAYLOAD (what is sent on consent — nothing else):');
|
|
116
|
-
console.log(' {');
|
|
117
|
-
console.log(' opportunityId, // the specific job id you consented for');
|
|
118
|
-
console.log(' buyerId, // "coastal"');
|
|
119
|
-
console.log(' buyerLegalName, // "Coastal Recruiting LLC"');
|
|
120
|
-
console.log(' approvedFields: {');
|
|
121
|
-
console.log(' skillTags, // your closed-vocab skill tags');
|
|
122
|
-
console.log(' seniorityBand, // optional');
|
|
123
|
-
console.log(' displayName, // optional, only if you set it in your profile');
|
|
124
|
-
console.log(' contactEmail, // optional, only if you set it in your profile');
|
|
125
|
-
console.log(' note, // optional, typed at consent time');
|
|
126
|
-
console.log(' github: { // optional, ONLY if GitHub connected AND you say "yes"');
|
|
127
|
-
console.log(' login, // your GitHub username');
|
|
128
|
-
console.log(' profileUrl, // https://github.com/<login>');
|
|
129
|
-
console.log(' topLanguages, // top public repo languages');
|
|
130
|
-
console.log(' publicRepos, // public repo count');
|
|
131
|
-
console.log(' }');
|
|
132
|
-
console.log(' },');
|
|
133
|
-
console.log(' consentText, // verbatim text you approved');
|
|
134
|
-
console.log(' createdAt // ISO timestamp');
|
|
135
|
-
console.log(' }');
|
|
136
|
-
console.log('');
|
|
137
|
-
console.log('APPLY-DIRECT vs BUYER-LEAD:');
|
|
138
|
-
console.log(' Most roles in the index are "apply-direct" — jpi opens the employer URL.');
|
|
139
|
-
console.log(' NO data is sent for apply-direct roles. Only Coastal-repped roles');
|
|
140
|
-
console.log(' offer the named-entity consent flow.');
|
|
141
|
-
console.log('');
|
|
142
214
|
console.log('HOW TO DISABLE / DELETE:');
|
|
143
215
|
console.log(' • Uninstall statusLine: node install.js --uninstall');
|
|
144
216
|
console.log(' • Delete local profile: terminalhire profile --delete');
|
|
@@ -146,75 +218,80 @@ async function install() {
|
|
|
146
218
|
console.log(' • Wipe everything: rm -rf ~/.terminalhire');
|
|
147
219
|
console.log('');
|
|
148
220
|
|
|
149
|
-
const
|
|
221
|
+
const settings = readSettings();
|
|
222
|
+
const currentStatusLine = settings.statusLine;
|
|
223
|
+
|
|
224
|
+
// Already installed?
|
|
225
|
+
if (isOurEntry(currentStatusLine)) {
|
|
226
|
+
console.log('Already installed (statusLine is already terminalhire or our wrapper).');
|
|
227
|
+
console.log('');
|
|
228
|
+
console.log('To change nudge frequency: terminalhire config --nudge <session|always|every:N>');
|
|
229
|
+
console.log('To uninstall: node install.js --uninstall');
|
|
230
|
+
console.log('');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Determine install strategy
|
|
235
|
+
let installStrategy;
|
|
236
|
+
if (!currentStatusLine) {
|
|
237
|
+
installStrategy = 'direct';
|
|
238
|
+
console.log(' No existing statusLine found.');
|
|
239
|
+
console.log(` Will set: statusLine = "node ${BIN_PATH}"`);
|
|
240
|
+
} else {
|
|
241
|
+
installStrategy = 'chain';
|
|
242
|
+
console.log(` Existing statusLine detected: ${currentStatusLine}`);
|
|
243
|
+
console.log(' Will create a wrapper at: ~/.terminalhire/statusline-wrapper.sh');
|
|
244
|
+
console.log(' The wrapper runs BOTH commands (existing first, terminalhire second).');
|
|
245
|
+
console.log(' Both receive the same stdin JSON from Claude Code.');
|
|
246
|
+
console.log(` Will set: statusLine = "bash ${WRAPPER_PATH}"`);
|
|
247
|
+
}
|
|
248
|
+
console.log('');
|
|
249
|
+
|
|
250
|
+
const installAnswer = await ask('Install terminalhire statusLine hook? Type "yes" to continue: ');
|
|
150
251
|
if (installAnswer !== 'yes') {
|
|
151
252
|
console.log('\nAborted — nothing was changed.');
|
|
152
253
|
process.exit(0);
|
|
153
254
|
}
|
|
154
255
|
|
|
155
256
|
console.log('');
|
|
156
|
-
const
|
|
257
|
+
const backupPath = backupSettings();
|
|
157
258
|
|
|
158
|
-
if (
|
|
159
|
-
|
|
259
|
+
if (installStrategy === 'direct') {
|
|
260
|
+
settings.statusLine = buildDirectEntry(BIN_PATH);
|
|
261
|
+
writeSettings(settings);
|
|
262
|
+
console.log(' Set statusLine in ~/.claude/settings.json');
|
|
263
|
+
console.log(` → node ${BIN_PATH}`);
|
|
160
264
|
} else {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
265
|
+
// Write wrapper
|
|
266
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
267
|
+
const wrapperContent = buildWrapper(currentStatusLine);
|
|
268
|
+
writeFileSync(WRAPPER_PATH, wrapperContent, 'utf8');
|
|
269
|
+
chmodSync(WRAPPER_PATH, 0o755);
|
|
270
|
+
console.log(` Created wrapper: ${WRAPPER_PATH}`);
|
|
271
|
+
|
|
272
|
+
settings.statusLine = `bash ${WRAPPER_PATH}`;
|
|
166
273
|
writeSettings(settings);
|
|
167
|
-
console.log('
|
|
274
|
+
console.log(' Updated statusLine in ~/.claude/settings.json');
|
|
275
|
+
console.log(` → bash ${WRAPPER_PATH}`);
|
|
276
|
+
console.log(' (chains existing command + terminalhire nudge)');
|
|
168
277
|
}
|
|
169
278
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
console.log('┌─────────────────────────────────────────────────────────────────┐');
|
|
173
|
-
console.log('│ RECOMMENDED: Sign in with GitHub for instant, accurate matches │');
|
|
174
|
-
console.log('│ │');
|
|
175
|
-
console.log('│ terminalhire uses your PUBLIC GitHub profile (scope: read:user) to:│');
|
|
176
|
-
console.log('│ • Infer skill tags from public repo languages + topics │');
|
|
177
|
-
console.log('│ • Estimate seniority from account age + contribution volume │');
|
|
178
|
-
console.log('│ • Pre-fill identity fields (name, public email if set) │');
|
|
179
|
-
console.log('│ │');
|
|
180
|
-
console.log('│ Your token is encrypted locally. No data leaves your machine │');
|
|
181
|
-
console.log('│ until you explicitly consent to share it in a specific lead. │');
|
|
182
|
-
console.log('│ Scope: read:user — public repos only. NEVER private repos. │');
|
|
183
|
-
console.log('└─────────────────────────────────────────────────────────────────┘');
|
|
184
|
-
console.log('');
|
|
185
|
-
|
|
186
|
-
const githubAnswer = await ask(
|
|
187
|
-
'Sign in with GitHub now? [Y/n/skip] (Enter = yes, "skip" or "n" = stay local): '
|
|
188
|
-
);
|
|
189
|
-
const doGitHub = githubAnswer === '' || githubAnswer === 'y' || githubAnswer === 'yes';
|
|
190
|
-
|
|
191
|
-
if (doGitHub) {
|
|
192
|
-
console.log('');
|
|
193
|
-
console.log(' Starting GitHub device flow...');
|
|
194
|
-
const _loginDistPath = resolve(join(__dirname, 'dist', 'bin', 'jpi-login.js'));
|
|
195
|
-
const _loginLegacyPath = resolve(join(__dirname, 'bin', 'jpi-login.js'));
|
|
196
|
-
const loginScript = existsSync(_loginDistPath) ? _loginDistPath : _loginLegacyPath;
|
|
197
|
-
const child = spawnSync(process.execPath, [loginScript, 'login'], {
|
|
198
|
-
stdio: ['inherit', 'inherit', 'inherit'],
|
|
199
|
-
env: process.env,
|
|
200
|
-
});
|
|
201
|
-
if (child.status !== 0) {
|
|
202
|
-
console.log('');
|
|
203
|
-
console.log(' GitHub sign-in did not complete. You can run `terminalhire login` any time.');
|
|
204
|
-
}
|
|
205
|
-
} else {
|
|
206
|
-
console.log('');
|
|
207
|
-
console.log(' Staying local-only. You can sign in any time with: terminalhire login');
|
|
208
|
-
console.log(' Your profile will still accumulate tags from personal project sessions.');
|
|
279
|
+
if (backupPath) {
|
|
280
|
+
console.log(` Backup: ${backupPath}`);
|
|
209
281
|
}
|
|
210
282
|
|
|
211
283
|
console.log('');
|
|
212
284
|
console.log('Done. Restart Claude Code to activate the status bar nudge.');
|
|
213
285
|
console.log('');
|
|
214
|
-
console.log(' Status bar format (once per session, only when matches exist):');
|
|
286
|
+
console.log(' Status bar format (default: once per session, only when matches exist):');
|
|
215
287
|
console.log(' ✦ N roles match your current work — run: terminalhire jobs');
|
|
216
288
|
console.log('');
|
|
217
|
-
console.log('
|
|
289
|
+
console.log(' Nudge frequency:');
|
|
290
|
+
console.log(' terminalhire config --nudge session (default — once per session)');
|
|
291
|
+
console.log(' terminalhire config --nudge always (every statusLine render)');
|
|
292
|
+
console.log(' terminalhire config --nudge every:N (every Nth render)');
|
|
293
|
+
console.log('');
|
|
294
|
+
console.log(' Other commands:');
|
|
218
295
|
console.log(' terminalhire login — sign in with GitHub (enriches profile instantly)');
|
|
219
296
|
console.log(' terminalhire logout — clear GitHub token');
|
|
220
297
|
console.log(' terminalhire jobs — fetch index, match locally, browse roles');
|
|
@@ -239,24 +316,55 @@ async function uninstall() {
|
|
|
239
316
|
}
|
|
240
317
|
|
|
241
318
|
console.log(` Current statusLine: ${settings.statusLine}`);
|
|
242
|
-
|
|
319
|
+
|
|
320
|
+
// Check if there is a backup to restore
|
|
321
|
+
const latestBackup = findLatestBackup();
|
|
322
|
+
if (latestBackup) {
|
|
323
|
+
console.log(` Backup found: ${latestBackup}`);
|
|
324
|
+
console.log(' Uninstalling will restore this backup (restoring original statusLine).');
|
|
325
|
+
} else {
|
|
326
|
+
console.log(' No backup found — statusLine entry will be deleted (set to none).');
|
|
327
|
+
}
|
|
328
|
+
console.log('');
|
|
329
|
+
|
|
330
|
+
const answer = await ask('Uninstall terminalhire hook? Type "yes" to continue: ');
|
|
243
331
|
if (answer !== 'yes') {
|
|
244
332
|
console.log('\nAborted — nothing was changed.');
|
|
245
333
|
process.exit(0);
|
|
246
334
|
}
|
|
247
335
|
|
|
248
336
|
console.log('');
|
|
249
|
-
backupSettings();
|
|
250
|
-
delete settings.statusLine;
|
|
251
|
-
writeSettings(settings);
|
|
252
337
|
|
|
253
|
-
|
|
338
|
+
if (latestBackup) {
|
|
339
|
+
// Restore the backup
|
|
340
|
+
copyFileSync(latestBackup, SETTINGS_PATH);
|
|
341
|
+
console.log(` Restored settings from backup: ${latestBackup}`);
|
|
342
|
+
} else {
|
|
343
|
+
// Just remove the statusLine key
|
|
344
|
+
backupSettings();
|
|
345
|
+
delete settings.statusLine;
|
|
346
|
+
writeSettings(settings);
|
|
347
|
+
console.log(' Removed statusLine from ~/.claude/settings.json');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Remove wrapper if it exists and is ours
|
|
351
|
+
if (existsSync(WRAPPER_PATH)) {
|
|
352
|
+
try {
|
|
353
|
+
const wrapperContent = readFileSync(WRAPPER_PATH, 'utf8');
|
|
354
|
+
if (wrapperContent.includes(WRAPPER_SENTINEL)) {
|
|
355
|
+
// We don't delete it automatically — user may have customized it
|
|
356
|
+
console.log(` Wrapper file left in place: ${WRAPPER_PATH}`);
|
|
357
|
+
console.log(' (Remove manually if desired: rm ~/.terminalhire/statusline-wrapper.sh)');
|
|
358
|
+
}
|
|
359
|
+
} catch { /* ignore */ }
|
|
360
|
+
}
|
|
361
|
+
|
|
254
362
|
console.log('');
|
|
255
363
|
console.log(' Your profile is untouched. To also delete it:');
|
|
256
364
|
console.log(' terminalhire profile --delete');
|
|
257
365
|
console.log(' rm -rf ~/.terminalhire');
|
|
258
366
|
console.log('');
|
|
259
|
-
console.log('Done.');
|
|
367
|
+
console.log('Done. Restart Claude Code to apply.');
|
|
260
368
|
console.log('');
|
|
261
369
|
}
|
|
262
370
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminalhire",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Local-first job matching for developers — Claude Code statusLine integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -14,12 +14,14 @@
|
|
|
14
14
|
"dist",
|
|
15
15
|
"fixtures",
|
|
16
16
|
"install.js",
|
|
17
|
+
"postinstall.js",
|
|
17
18
|
"README.md"
|
|
18
19
|
],
|
|
19
20
|
"scripts": {
|
|
20
21
|
"build": "tsup",
|
|
21
22
|
"prepublishOnly": "npm run build",
|
|
22
|
-
"install-hook": "node install.js"
|
|
23
|
+
"install-hook": "node install.js",
|
|
24
|
+
"postinstall": "node ./postinstall.js"
|
|
23
25
|
},
|
|
24
26
|
"publishConfig": {
|
|
25
27
|
"access": "public"
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postinstall.js — print-only notice after `npm install terminalhire`
|
|
4
|
+
*
|
|
5
|
+
* ToS / safety guarantee:
|
|
6
|
+
* - PRINT ONLY. No config edits. No network calls. No file writes.
|
|
7
|
+
* - Always exits 0 (never fails the install).
|
|
8
|
+
* - No-op if non-interactive / CI (TERM=dumb, CI=true, etc.).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Skip in CI / non-interactive environments
|
|
12
|
+
const isCI =
|
|
13
|
+
process.env['CI'] === 'true' ||
|
|
14
|
+
process.env['CI'] === '1' ||
|
|
15
|
+
process.env['CONTINUOUS_INTEGRATION'] === 'true' ||
|
|
16
|
+
process.env['TERM'] === 'dumb' ||
|
|
17
|
+
!process.stdout.isTTY;
|
|
18
|
+
|
|
19
|
+
if (!isCI) {
|
|
20
|
+
process.stdout.write('\n');
|
|
21
|
+
process.stdout.write(' ┌───────────────────────────────────────────────────────────────┐\n');
|
|
22
|
+
process.stdout.write(' │ terminalhire installed — activate the Claude Code nudge: │\n');
|
|
23
|
+
process.stdout.write(' │ │\n');
|
|
24
|
+
process.stdout.write(' │ terminalhire init │\n');
|
|
25
|
+
process.stdout.write(' │ │\n');
|
|
26
|
+
process.stdout.write(' │ This runs one-command onboarding: optional GitHub sign-in, │\n');
|
|
27
|
+
process.stdout.write(' │ seeds your local job cache, then installs the statusLine │\n');
|
|
28
|
+
process.stdout.write(' │ hook — with your explicit consent at every step. │\n');
|
|
29
|
+
process.stdout.write(' │ │\n');
|
|
30
|
+
process.stdout.write(' │ No config is changed until you run that command. │\n');
|
|
31
|
+
process.stdout.write(' └───────────────────────────────────────────────────────────────┘\n');
|
|
32
|
+
process.stdout.write('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
process.exit(0);
|