terminalhire 0.11.0 → 0.12.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/jpi-chat-read.js +37 -27
- package/dist/bin/jpi-chat.js +8 -27
- package/dist/bin/jpi-dispatch.js +38 -30
- package/dist/bin/jpi-init.js +30 -3
- package/dist/bin/jpi-statusline-launch.js +71 -0
- package/dist/bin/jpi-statusline.js +65 -0
- package/package.json +3 -2
- package/statusline-install.js +271 -0
|
@@ -4278,6 +4278,35 @@ async function defaultListPendingInvites(deps = {}) {
|
|
|
4278
4278
|
const invites = listed.intros.filter((it) => it && it.role === "incoming" && it.status === "pending" && it.counterpartyLogin).map((it) => ({ login: it.counterpartyLogin }));
|
|
4279
4279
|
return { status: "ok", invites };
|
|
4280
4280
|
}
|
|
4281
|
+
function relativeTime(then, now = /* @__PURE__ */ new Date()) {
|
|
4282
|
+
const t = new Date(then).getTime();
|
|
4283
|
+
if (Number.isNaN(t)) return "";
|
|
4284
|
+
const deltaMs = Math.max(0, now.getTime() - t);
|
|
4285
|
+
const sec = Math.floor(deltaMs / 1e3);
|
|
4286
|
+
if (sec < 60) return "just now";
|
|
4287
|
+
const min = Math.floor(sec / 60);
|
|
4288
|
+
if (min < 60) return `${Math.max(1, min)}m ago`;
|
|
4289
|
+
const hr = Math.floor(min / 60);
|
|
4290
|
+
if (hr < 24) return `${hr}h ago`;
|
|
4291
|
+
const day = Math.floor(hr / 24);
|
|
4292
|
+
if (day < 7) return `${day}d ago`;
|
|
4293
|
+
const wk = Math.floor(day / 7);
|
|
4294
|
+
if (wk < 5) return `${wk}w ago`;
|
|
4295
|
+
return "a while ago";
|
|
4296
|
+
}
|
|
4297
|
+
function formatPresence(presence, now = /* @__PURE__ */ new Date()) {
|
|
4298
|
+
if (!presence) return "\u25CB not on chat yet";
|
|
4299
|
+
const share = presence.shareActivity === true;
|
|
4300
|
+
const seenMs = share && presence.lastSeen ? new Date(presence.lastSeen).getTime() : NaN;
|
|
4301
|
+
const hasSeen = !Number.isNaN(seenMs);
|
|
4302
|
+
const fresh = hasSeen && now.getTime() - seenMs <= ACTIVE_WINDOW_MS;
|
|
4303
|
+
if (share && presence.optin === true && fresh) return "\u25CF active now";
|
|
4304
|
+
if (share && hasSeen) {
|
|
4305
|
+
const rel = relativeTime(presence.lastSeen, now);
|
|
4306
|
+
return rel ? `\u25D0 reachable \xB7 seen ${rel}` : "\u25D0 reachable";
|
|
4307
|
+
}
|
|
4308
|
+
return "\u25D0 reachable";
|
|
4309
|
+
}
|
|
4281
4310
|
var CHAT_BASE2, GH_SESSION_COOKIE2, ANSI_CSI, ANSI_OSC, ANSI_OTHER, C0_C1_DEL, CHAT_DISCLOSURE, CHAT_AT_REST, CHAT_CODE_OF_CONDUCT, CHAT_MIN_AGE, ACTIVE_WINDOW_MS;
|
|
4282
4311
|
var init_jpi_chat = __esm({
|
|
4283
4312
|
"bin/jpi-chat.js"() {
|
|
@@ -4404,7 +4433,7 @@ function renderInbox(items, invites = []) {
|
|
|
4404
4433
|
lines.push(" (no accepted connections yet \u2014 request one: terminalhire intro <login>)");
|
|
4405
4434
|
} else {
|
|
4406
4435
|
for (const it of items) {
|
|
4407
|
-
const dot = it.
|
|
4436
|
+
const dot = formatPresence(it.presence).charAt(0);
|
|
4408
4437
|
const unread = it.unread > 0 ? `\u2709 ${it.unread}` : "\u2014";
|
|
4409
4438
|
const login = `@${sanitizeLine(it.login)}`;
|
|
4410
4439
|
const stamp = sanitizeLine(it.lastStamp || "");
|
|
@@ -4419,13 +4448,12 @@ function renderInbox(items, invites = []) {
|
|
|
4419
4448
|
return lines.join("\n") + "\n";
|
|
4420
4449
|
}
|
|
4421
4450
|
function renderThread(state) {
|
|
4422
|
-
const { peerLogin,
|
|
4451
|
+
const { peerLogin, presence, safety, messages, total } = state;
|
|
4423
4452
|
const safePeer = sanitizeLine(peerLogin);
|
|
4424
|
-
const
|
|
4425
|
-
const presence = online ? "online" : "offline";
|
|
4453
|
+
const status = formatPresence(presence);
|
|
4426
4454
|
const lines = [];
|
|
4427
4455
|
lines.push(
|
|
4428
|
-
` @${safePeer} ${
|
|
4456
|
+
` @${safePeer} ${status}` + (safety ? ` \xB7 safety# ${sanitizeLine(safety)}` : "")
|
|
4429
4457
|
);
|
|
4430
4458
|
lines.push(" " + "\u2500".repeat(64));
|
|
4431
4459
|
if (!messages || messages.length === 0) {
|
|
@@ -4475,21 +4503,6 @@ function writeProblem(output, result, target) {
|
|
|
4475
4503
|
return "error";
|
|
4476
4504
|
}
|
|
4477
4505
|
}
|
|
4478
|
-
async function readPresence(client, peerLogin) {
|
|
4479
|
-
if (typeof client.getPeerPresence !== "function") return false;
|
|
4480
|
-
try {
|
|
4481
|
-
return await client.getPeerPresence(peerLogin) != null;
|
|
4482
|
-
} catch {
|
|
4483
|
-
return false;
|
|
4484
|
-
}
|
|
4485
|
-
}
|
|
4486
|
-
async function clearPresence(client) {
|
|
4487
|
-
if (typeof client.heartbeat !== "function") return;
|
|
4488
|
-
try {
|
|
4489
|
-
await client.heartbeat(false);
|
|
4490
|
-
} catch {
|
|
4491
|
-
}
|
|
4492
|
-
}
|
|
4493
4506
|
async function gateDisclosure(ensureDisclosure, input, output) {
|
|
4494
4507
|
const ack = await ensureDisclosure({ input, output });
|
|
4495
4508
|
if (!ack.acknowledged) {
|
|
@@ -4537,16 +4550,14 @@ async function runInbox(opts = {}) {
|
|
|
4537
4550
|
(m) => m.senderLogin === conn.peerLogin && (!cursor || m.createdAt > cursor)
|
|
4538
4551
|
).length;
|
|
4539
4552
|
const last = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
4540
|
-
const online = await readPresence(client, conn.peerLogin);
|
|
4541
4553
|
items.push({
|
|
4542
4554
|
login: conn.peerLogin,
|
|
4543
|
-
|
|
4555
|
+
presence: REACHABLE_DISPLAY,
|
|
4544
4556
|
unread,
|
|
4545
4557
|
lastStamp: last ? formatStamp(last.createdAt) : "",
|
|
4546
4558
|
preview: last ? last.plaintext : ""
|
|
4547
4559
|
});
|
|
4548
4560
|
}
|
|
4549
|
-
await clearPresence(client);
|
|
4550
4561
|
output.write(renderInbox(items, invites));
|
|
4551
4562
|
return { ok: true, count: items.length, invites: invites.length };
|
|
4552
4563
|
}
|
|
@@ -4589,7 +4600,6 @@ async function runReadThread(opts = {}) {
|
|
|
4589
4600
|
}
|
|
4590
4601
|
const total = messages.length;
|
|
4591
4602
|
const shownMessages = all ? messages : messages.slice(Math.max(0, total - limit));
|
|
4592
|
-
const online = await readPresence(client, peerLogin);
|
|
4593
4603
|
let safety = "";
|
|
4594
4604
|
if (typeof client.getSafetyNumber === "function") {
|
|
4595
4605
|
try {
|
|
@@ -4598,8 +4608,7 @@ async function runReadThread(opts = {}) {
|
|
|
4598
4608
|
safety = "";
|
|
4599
4609
|
}
|
|
4600
4610
|
}
|
|
4601
|
-
|
|
4602
|
-
output.write(renderThread({ peerLogin, online, safety, messages: shownMessages, total }));
|
|
4611
|
+
output.write(renderThread({ peerLogin, presence: REACHABLE_DISPLAY, safety, messages: shownMessages, total }));
|
|
4603
4612
|
if (total > 0) {
|
|
4604
4613
|
const newest = messages[total - 1];
|
|
4605
4614
|
if (newest && newest.createdAt) {
|
|
@@ -4672,7 +4681,7 @@ async function runSend(opts = {}) {
|
|
|
4672
4681
|
);
|
|
4673
4682
|
return { ok: true };
|
|
4674
4683
|
}
|
|
4675
|
-
var CHAT_BASE3, GH_SESSION_COOKIE3, TERMINALHIRE_DIR5, READS_FILE, INDEX_CACHE_FILE;
|
|
4684
|
+
var CHAT_BASE3, GH_SESSION_COOKIE3, TERMINALHIRE_DIR5, READS_FILE, INDEX_CACHE_FILE, REACHABLE_DISPLAY;
|
|
4676
4685
|
var init_jpi_chat_read = __esm({
|
|
4677
4686
|
"bin/jpi-chat-read.js"() {
|
|
4678
4687
|
init_chat_client();
|
|
@@ -4683,6 +4692,7 @@ var init_jpi_chat_read = __esm({
|
|
|
4683
4692
|
TERMINALHIRE_DIR5 = join8(homedir7(), ".terminalhire");
|
|
4684
4693
|
READS_FILE = join8(TERMINALHIRE_DIR5, "chat-reads.json");
|
|
4685
4694
|
INDEX_CACHE_FILE = join8(TERMINALHIRE_DIR5, "index-cache.json");
|
|
4695
|
+
REACHABLE_DISPLAY = { shareActivity: false, optin: false, lastSeen: null };
|
|
4686
4696
|
}
|
|
4687
4697
|
});
|
|
4688
4698
|
init_jpi_chat_read();
|
package/dist/bin/jpi-chat.js
CHANGED
|
@@ -4558,7 +4558,7 @@ function renderInbox(items, invites = []) {
|
|
|
4558
4558
|
lines.push(" (no accepted connections yet \u2014 request one: terminalhire intro <login>)");
|
|
4559
4559
|
} else {
|
|
4560
4560
|
for (const it of items) {
|
|
4561
|
-
const dot = it.
|
|
4561
|
+
const dot = formatPresence(it.presence).charAt(0);
|
|
4562
4562
|
const unread = it.unread > 0 ? `\u2709 ${it.unread}` : "\u2014";
|
|
4563
4563
|
const login = `@${sanitizeLine(it.login)}`;
|
|
4564
4564
|
const stamp = sanitizeLine(it.lastStamp || "");
|
|
@@ -4573,13 +4573,12 @@ function renderInbox(items, invites = []) {
|
|
|
4573
4573
|
return lines.join("\n") + "\n";
|
|
4574
4574
|
}
|
|
4575
4575
|
function renderThread(state) {
|
|
4576
|
-
const { peerLogin,
|
|
4576
|
+
const { peerLogin, presence, safety, messages, total } = state;
|
|
4577
4577
|
const safePeer = sanitizeLine(peerLogin);
|
|
4578
|
-
const
|
|
4579
|
-
const presence = online ? "online" : "offline";
|
|
4578
|
+
const status = formatPresence(presence);
|
|
4580
4579
|
const lines = [];
|
|
4581
4580
|
lines.push(
|
|
4582
|
-
` @${safePeer} ${
|
|
4581
|
+
` @${safePeer} ${status}` + (safety ? ` \xB7 safety# ${sanitizeLine(safety)}` : "")
|
|
4583
4582
|
);
|
|
4584
4583
|
lines.push(" " + "\u2500".repeat(64));
|
|
4585
4584
|
if (!messages || messages.length === 0) {
|
|
@@ -4629,21 +4628,6 @@ function writeProblem(output, result, target) {
|
|
|
4629
4628
|
return "error";
|
|
4630
4629
|
}
|
|
4631
4630
|
}
|
|
4632
|
-
async function readPresence(client, peerLogin) {
|
|
4633
|
-
if (typeof client.getPeerPresence !== "function") return false;
|
|
4634
|
-
try {
|
|
4635
|
-
return await client.getPeerPresence(peerLogin) != null;
|
|
4636
|
-
} catch {
|
|
4637
|
-
return false;
|
|
4638
|
-
}
|
|
4639
|
-
}
|
|
4640
|
-
async function clearPresence(client) {
|
|
4641
|
-
if (typeof client.heartbeat !== "function") return;
|
|
4642
|
-
try {
|
|
4643
|
-
await client.heartbeat(false);
|
|
4644
|
-
} catch {
|
|
4645
|
-
}
|
|
4646
|
-
}
|
|
4647
4631
|
async function gateDisclosure(ensureDisclosure, input, output) {
|
|
4648
4632
|
const ack = await ensureDisclosure({ input, output });
|
|
4649
4633
|
if (!ack.acknowledged) {
|
|
@@ -4691,16 +4675,14 @@ async function runInbox(opts = {}) {
|
|
|
4691
4675
|
(m) => m.senderLogin === conn.peerLogin && (!cursor || m.createdAt > cursor)
|
|
4692
4676
|
).length;
|
|
4693
4677
|
const last = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
4694
|
-
const online = await readPresence(client, conn.peerLogin);
|
|
4695
4678
|
items.push({
|
|
4696
4679
|
login: conn.peerLogin,
|
|
4697
|
-
|
|
4680
|
+
presence: REACHABLE_DISPLAY,
|
|
4698
4681
|
unread,
|
|
4699
4682
|
lastStamp: last ? formatStamp(last.createdAt) : "",
|
|
4700
4683
|
preview: last ? last.plaintext : ""
|
|
4701
4684
|
});
|
|
4702
4685
|
}
|
|
4703
|
-
await clearPresence(client);
|
|
4704
4686
|
output.write(renderInbox(items, invites));
|
|
4705
4687
|
return { ok: true, count: items.length, invites: invites.length };
|
|
4706
4688
|
}
|
|
@@ -4743,7 +4725,6 @@ async function runReadThread(opts = {}) {
|
|
|
4743
4725
|
}
|
|
4744
4726
|
const total = messages.length;
|
|
4745
4727
|
const shownMessages = all ? messages : messages.slice(Math.max(0, total - limit));
|
|
4746
|
-
const online = await readPresence(client, peerLogin);
|
|
4747
4728
|
let safety = "";
|
|
4748
4729
|
if (typeof client.getSafetyNumber === "function") {
|
|
4749
4730
|
try {
|
|
@@ -4752,8 +4733,7 @@ async function runReadThread(opts = {}) {
|
|
|
4752
4733
|
safety = "";
|
|
4753
4734
|
}
|
|
4754
4735
|
}
|
|
4755
|
-
|
|
4756
|
-
output.write(renderThread({ peerLogin, online, safety, messages: shownMessages, total }));
|
|
4736
|
+
output.write(renderThread({ peerLogin, presence: REACHABLE_DISPLAY, safety, messages: shownMessages, total }));
|
|
4757
4737
|
if (total > 0) {
|
|
4758
4738
|
const newest = messages[total - 1];
|
|
4759
4739
|
if (newest && newest.createdAt) {
|
|
@@ -4826,7 +4806,7 @@ async function runSend(opts = {}) {
|
|
|
4826
4806
|
);
|
|
4827
4807
|
return { ok: true };
|
|
4828
4808
|
}
|
|
4829
|
-
var CHAT_BASE2, GH_SESSION_COOKIE2, TERMINALHIRE_DIR6, READS_FILE, INDEX_CACHE_FILE;
|
|
4809
|
+
var CHAT_BASE2, GH_SESSION_COOKIE2, TERMINALHIRE_DIR6, READS_FILE, INDEX_CACHE_FILE, REACHABLE_DISPLAY;
|
|
4830
4810
|
var init_jpi_chat_read = __esm({
|
|
4831
4811
|
"bin/jpi-chat-read.js"() {
|
|
4832
4812
|
"use strict";
|
|
@@ -4838,6 +4818,7 @@ var init_jpi_chat_read = __esm({
|
|
|
4838
4818
|
TERMINALHIRE_DIR6 = join8(homedir7(), ".terminalhire");
|
|
4839
4819
|
READS_FILE = join8(TERMINALHIRE_DIR6, "chat-reads.json");
|
|
4840
4820
|
INDEX_CACHE_FILE = join8(TERMINALHIRE_DIR6, "index-cache.json");
|
|
4821
|
+
REACHABLE_DISPLAY = { shareActivity: false, optin: false, lastSeen: null };
|
|
4841
4822
|
}
|
|
4842
4823
|
});
|
|
4843
4824
|
|
package/dist/bin/jpi-dispatch.js
CHANGED
|
@@ -10689,7 +10689,7 @@ function renderInbox(items, invites = []) {
|
|
|
10689
10689
|
lines.push(" (no accepted connections yet \u2014 request one: terminalhire intro <login>)");
|
|
10690
10690
|
} else {
|
|
10691
10691
|
for (const it of items) {
|
|
10692
|
-
const dot = it.
|
|
10692
|
+
const dot = formatPresence(it.presence).charAt(0);
|
|
10693
10693
|
const unread = it.unread > 0 ? `\u2709 ${it.unread}` : "\u2014";
|
|
10694
10694
|
const login = `@${sanitizeLine(it.login)}`;
|
|
10695
10695
|
const stamp = sanitizeLine(it.lastStamp || "");
|
|
@@ -10704,13 +10704,12 @@ function renderInbox(items, invites = []) {
|
|
|
10704
10704
|
return lines.join("\n") + "\n";
|
|
10705
10705
|
}
|
|
10706
10706
|
function renderThread(state) {
|
|
10707
|
-
const { peerLogin,
|
|
10707
|
+
const { peerLogin, presence, safety, messages, total } = state;
|
|
10708
10708
|
const safePeer = sanitizeLine(peerLogin);
|
|
10709
|
-
const
|
|
10710
|
-
const presence = online ? "online" : "offline";
|
|
10709
|
+
const status = formatPresence(presence);
|
|
10711
10710
|
const lines = [];
|
|
10712
10711
|
lines.push(
|
|
10713
|
-
` @${safePeer} ${
|
|
10712
|
+
` @${safePeer} ${status}` + (safety ? ` \xB7 safety# ${sanitizeLine(safety)}` : "")
|
|
10714
10713
|
);
|
|
10715
10714
|
lines.push(" " + "\u2500".repeat(64));
|
|
10716
10715
|
if (!messages || messages.length === 0) {
|
|
@@ -10760,21 +10759,6 @@ function writeProblem(output, result, target) {
|
|
|
10760
10759
|
return "error";
|
|
10761
10760
|
}
|
|
10762
10761
|
}
|
|
10763
|
-
async function readPresence(client, peerLogin) {
|
|
10764
|
-
if (typeof client.getPeerPresence !== "function") return false;
|
|
10765
|
-
try {
|
|
10766
|
-
return await client.getPeerPresence(peerLogin) != null;
|
|
10767
|
-
} catch {
|
|
10768
|
-
return false;
|
|
10769
|
-
}
|
|
10770
|
-
}
|
|
10771
|
-
async function clearPresence(client) {
|
|
10772
|
-
if (typeof client.heartbeat !== "function") return;
|
|
10773
|
-
try {
|
|
10774
|
-
await client.heartbeat(false);
|
|
10775
|
-
} catch {
|
|
10776
|
-
}
|
|
10777
|
-
}
|
|
10778
10762
|
async function gateDisclosure(ensureDisclosure, input, output) {
|
|
10779
10763
|
const ack = await ensureDisclosure({ input, output });
|
|
10780
10764
|
if (!ack.acknowledged) {
|
|
@@ -10822,16 +10806,14 @@ async function runInbox(opts = {}) {
|
|
|
10822
10806
|
(m) => m.senderLogin === conn.peerLogin && (!cursor || m.createdAt > cursor)
|
|
10823
10807
|
).length;
|
|
10824
10808
|
const last = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
10825
|
-
const online = await readPresence(client, conn.peerLogin);
|
|
10826
10809
|
items.push({
|
|
10827
10810
|
login: conn.peerLogin,
|
|
10828
|
-
|
|
10811
|
+
presence: REACHABLE_DISPLAY,
|
|
10829
10812
|
unread,
|
|
10830
10813
|
lastStamp: last ? formatStamp(last.createdAt) : "",
|
|
10831
10814
|
preview: last ? last.plaintext : ""
|
|
10832
10815
|
});
|
|
10833
10816
|
}
|
|
10834
|
-
await clearPresence(client);
|
|
10835
10817
|
output.write(renderInbox(items, invites));
|
|
10836
10818
|
return { ok: true, count: items.length, invites: invites.length };
|
|
10837
10819
|
}
|
|
@@ -10874,7 +10856,6 @@ async function runReadThread(opts = {}) {
|
|
|
10874
10856
|
}
|
|
10875
10857
|
const total = messages.length;
|
|
10876
10858
|
const shownMessages = all ? messages : messages.slice(Math.max(0, total - limit));
|
|
10877
|
-
const online = await readPresence(client, peerLogin);
|
|
10878
10859
|
let safety = "";
|
|
10879
10860
|
if (typeof client.getSafetyNumber === "function") {
|
|
10880
10861
|
try {
|
|
@@ -10883,8 +10864,7 @@ async function runReadThread(opts = {}) {
|
|
|
10883
10864
|
safety = "";
|
|
10884
10865
|
}
|
|
10885
10866
|
}
|
|
10886
|
-
|
|
10887
|
-
output.write(renderThread({ peerLogin, online, safety, messages: shownMessages, total }));
|
|
10867
|
+
output.write(renderThread({ peerLogin, presence: REACHABLE_DISPLAY, safety, messages: shownMessages, total }));
|
|
10888
10868
|
if (total > 0) {
|
|
10889
10869
|
const newest = messages[total - 1];
|
|
10890
10870
|
if (newest && newest.createdAt) {
|
|
@@ -10957,7 +10937,7 @@ async function runSend(opts = {}) {
|
|
|
10957
10937
|
);
|
|
10958
10938
|
return { ok: true };
|
|
10959
10939
|
}
|
|
10960
|
-
var CHAT_BASE2, GH_SESSION_COOKIE4, TERMINALHIRE_DIR13, READS_FILE, INDEX_CACHE_FILE5;
|
|
10940
|
+
var CHAT_BASE2, GH_SESSION_COOKIE4, TERMINALHIRE_DIR13, READS_FILE, INDEX_CACHE_FILE5, REACHABLE_DISPLAY;
|
|
10961
10941
|
var init_jpi_chat_read = __esm({
|
|
10962
10942
|
"bin/jpi-chat-read.js"() {
|
|
10963
10943
|
"use strict";
|
|
@@ -10969,6 +10949,7 @@ var init_jpi_chat_read = __esm({
|
|
|
10969
10949
|
TERMINALHIRE_DIR13 = join17(homedir16(), ".terminalhire");
|
|
10970
10950
|
READS_FILE = join17(TERMINALHIRE_DIR13, "chat-reads.json");
|
|
10971
10951
|
INDEX_CACHE_FILE5 = join17(TERMINALHIRE_DIR13, "index-cache.json");
|
|
10952
|
+
REACHABLE_DISPLAY = { shareActivity: false, optin: false, lastSeen: null };
|
|
10972
10953
|
}
|
|
10973
10954
|
});
|
|
10974
10955
|
|
|
@@ -13370,6 +13351,13 @@ function resolveInstallJs() {
|
|
|
13370
13351
|
if (existsSync18(fromBin)) return fromBin;
|
|
13371
13352
|
return fromBin;
|
|
13372
13353
|
}
|
|
13354
|
+
function resolveStatuslineInstallJs() {
|
|
13355
|
+
const fromDist = resolve(join24(__dirname3, "..", "..", "statusline-install.js"));
|
|
13356
|
+
const fromBin = resolve(join24(__dirname3, "..", "statusline-install.js"));
|
|
13357
|
+
if (existsSync18(fromDist)) return fromDist;
|
|
13358
|
+
if (existsSync18(fromBin)) return fromBin;
|
|
13359
|
+
return fromBin;
|
|
13360
|
+
}
|
|
13373
13361
|
async function run17() {
|
|
13374
13362
|
console.log("");
|
|
13375
13363
|
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");
|
|
@@ -13382,10 +13370,12 @@ async function run17() {
|
|
|
13382
13370
|
console.log(" 2. Seed your local job cache (anonymous index download)");
|
|
13383
13371
|
console.log(" 3. Enable the ambient spinner job surface in ~/.claude/settings.json");
|
|
13384
13372
|
console.log(" (with backup + your explicit consent before any file is touched)");
|
|
13373
|
+
console.log(" 4. Optionally show connection notifications in your statusLine");
|
|
13374
|
+
console.log(" (\u{1F4AC} unread + intro requests only \u2014 never job ads; separate consent)");
|
|
13385
13375
|
console.log("");
|
|
13386
13376
|
console.log('You can stop at any step. Nothing is changed until you say "yes".');
|
|
13387
13377
|
console.log("");
|
|
13388
|
-
console.log("Step 1/
|
|
13378
|
+
console.log("Step 1/4 \u2014 GitHub sign-in (optional but recommended)");
|
|
13389
13379
|
console.log("");
|
|
13390
13380
|
console.log(" Scope: read:user \u2014 public profile + public repos only.");
|
|
13391
13381
|
console.log(" Your token is encrypted at ~/.terminalhire/github-token.enc.");
|
|
@@ -13413,7 +13403,7 @@ async function run17() {
|
|
|
13413
13403
|
console.log(" Sign in any time with: terminalhire login");
|
|
13414
13404
|
}
|
|
13415
13405
|
console.log("");
|
|
13416
|
-
console.log("Step 2/
|
|
13406
|
+
console.log("Step 2/4 \u2014 Seeding local job cache");
|
|
13417
13407
|
console.log("");
|
|
13418
13408
|
console.log(" Fetching anonymous job index (no dev data sent)...");
|
|
13419
13409
|
const jobsScript = resolveScript("jpi-jobs");
|
|
@@ -13433,7 +13423,7 @@ async function run17() {
|
|
|
13433
13423
|
console.log(" Run `terminalhire jobs` after a few Claude Code sessions to populate it.");
|
|
13434
13424
|
}
|
|
13435
13425
|
console.log("");
|
|
13436
|
-
console.log("Step 3/
|
|
13426
|
+
console.log("Step 3/4 \u2014 Enable the ambient spinner job surface in ~/.claude/settings.json");
|
|
13437
13427
|
console.log("");
|
|
13438
13428
|
console.log(" This is the only step that modifies a system file.");
|
|
13439
13429
|
console.log(" A timestamped backup is created before any change.");
|
|
@@ -13449,6 +13439,24 @@ async function run17() {
|
|
|
13449
13439
|
console.log(" Hook installation did not complete. Run manually: node install.js");
|
|
13450
13440
|
}
|
|
13451
13441
|
console.log("");
|
|
13442
|
+
console.log("Step 4/4 \u2014 Connection notifications in your statusLine (optional)");
|
|
13443
|
+
console.log("");
|
|
13444
|
+
console.log(" A statusLine that shows ONLY personal connection signals \u2014 \u{1F4AC} unread");
|
|
13445
|
+
console.log(" messages and inbound intro requests. Never job ads (those stay in the");
|
|
13446
|
+
console.log(" spinner). Local cache read, zero network. Separate consent + backup;");
|
|
13447
|
+
console.log(" it stays current across plugin updates and preserves any existing");
|
|
13448
|
+
console.log(" statusLine you have. Remove any time: node statusline-install.js --uninstall");
|
|
13449
|
+
console.log("");
|
|
13450
|
+
const statuslineInstallJs = resolveStatuslineInstallJs();
|
|
13451
|
+
const statuslineChild = spawnSync(process.execPath, [statuslineInstallJs], {
|
|
13452
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
13453
|
+
env: process.env
|
|
13454
|
+
});
|
|
13455
|
+
if (statuslineChild.status !== 0) {
|
|
13456
|
+
console.log("");
|
|
13457
|
+
console.log(" statusLine setup did not complete. Run manually: node statusline-install.js");
|
|
13458
|
+
}
|
|
13459
|
+
console.log("");
|
|
13452
13460
|
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");
|
|
13453
13461
|
console.log("\u2502 terminalhire init complete! \u2502");
|
|
13454
13462
|
console.log("\u2502 \u2502");
|
package/dist/bin/jpi-init.js
CHANGED
|
@@ -29,6 +29,13 @@ function resolveInstallJs() {
|
|
|
29
29
|
if (existsSync(fromBin)) return fromBin;
|
|
30
30
|
return fromBin;
|
|
31
31
|
}
|
|
32
|
+
function resolveStatuslineInstallJs() {
|
|
33
|
+
const fromDist = resolve(join(__dirname, "..", "..", "statusline-install.js"));
|
|
34
|
+
const fromBin = resolve(join(__dirname, "..", "statusline-install.js"));
|
|
35
|
+
if (existsSync(fromDist)) return fromDist;
|
|
36
|
+
if (existsSync(fromBin)) return fromBin;
|
|
37
|
+
return fromBin;
|
|
38
|
+
}
|
|
32
39
|
async function run() {
|
|
33
40
|
console.log("");
|
|
34
41
|
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");
|
|
@@ -41,10 +48,12 @@ async function run() {
|
|
|
41
48
|
console.log(" 2. Seed your local job cache (anonymous index download)");
|
|
42
49
|
console.log(" 3. Enable the ambient spinner job surface in ~/.claude/settings.json");
|
|
43
50
|
console.log(" (with backup + your explicit consent before any file is touched)");
|
|
51
|
+
console.log(" 4. Optionally show connection notifications in your statusLine");
|
|
52
|
+
console.log(" (\u{1F4AC} unread + intro requests only \u2014 never job ads; separate consent)");
|
|
44
53
|
console.log("");
|
|
45
54
|
console.log('You can stop at any step. Nothing is changed until you say "yes".');
|
|
46
55
|
console.log("");
|
|
47
|
-
console.log("Step 1/
|
|
56
|
+
console.log("Step 1/4 \u2014 GitHub sign-in (optional but recommended)");
|
|
48
57
|
console.log("");
|
|
49
58
|
console.log(" Scope: read:user \u2014 public profile + public repos only.");
|
|
50
59
|
console.log(" Your token is encrypted at ~/.terminalhire/github-token.enc.");
|
|
@@ -72,7 +81,7 @@ async function run() {
|
|
|
72
81
|
console.log(" Sign in any time with: terminalhire login");
|
|
73
82
|
}
|
|
74
83
|
console.log("");
|
|
75
|
-
console.log("Step 2/
|
|
84
|
+
console.log("Step 2/4 \u2014 Seeding local job cache");
|
|
76
85
|
console.log("");
|
|
77
86
|
console.log(" Fetching anonymous job index (no dev data sent)...");
|
|
78
87
|
const jobsScript = resolveScript("jpi-jobs");
|
|
@@ -92,7 +101,7 @@ async function run() {
|
|
|
92
101
|
console.log(" Run `terminalhire jobs` after a few Claude Code sessions to populate it.");
|
|
93
102
|
}
|
|
94
103
|
console.log("");
|
|
95
|
-
console.log("Step 3/
|
|
104
|
+
console.log("Step 3/4 \u2014 Enable the ambient spinner job surface in ~/.claude/settings.json");
|
|
96
105
|
console.log("");
|
|
97
106
|
console.log(" This is the only step that modifies a system file.");
|
|
98
107
|
console.log(" A timestamped backup is created before any change.");
|
|
@@ -108,6 +117,24 @@ async function run() {
|
|
|
108
117
|
console.log(" Hook installation did not complete. Run manually: node install.js");
|
|
109
118
|
}
|
|
110
119
|
console.log("");
|
|
120
|
+
console.log("Step 4/4 \u2014 Connection notifications in your statusLine (optional)");
|
|
121
|
+
console.log("");
|
|
122
|
+
console.log(" A statusLine that shows ONLY personal connection signals \u2014 \u{1F4AC} unread");
|
|
123
|
+
console.log(" messages and inbound intro requests. Never job ads (those stay in the");
|
|
124
|
+
console.log(" spinner). Local cache read, zero network. Separate consent + backup;");
|
|
125
|
+
console.log(" it stays current across plugin updates and preserves any existing");
|
|
126
|
+
console.log(" statusLine you have. Remove any time: node statusline-install.js --uninstall");
|
|
127
|
+
console.log("");
|
|
128
|
+
const statuslineInstallJs = resolveStatuslineInstallJs();
|
|
129
|
+
const statuslineChild = spawnSync(process.execPath, [statuslineInstallJs], {
|
|
130
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
131
|
+
env: process.env
|
|
132
|
+
});
|
|
133
|
+
if (statuslineChild.status !== 0) {
|
|
134
|
+
console.log("");
|
|
135
|
+
console.log(" statusLine setup did not complete. Run manually: node statusline-install.js");
|
|
136
|
+
}
|
|
137
|
+
console.log("");
|
|
111
138
|
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
139
|
console.log("\u2502 terminalhire init complete! \u2502");
|
|
113
140
|
console.log("\u2502 \u2502");
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/jpi-statusline-launch.js
|
|
4
|
+
import { readFileSync, existsSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { spawnSync } from "child_process";
|
|
8
|
+
import { pathToFileURL } from "url";
|
|
9
|
+
var TH_DIR = join(homedir(), ".terminalhire");
|
|
10
|
+
var POINTER = join(TH_DIR, "engine-path");
|
|
11
|
+
var FOREIGN = join(TH_DIR, "statusline-foreign.json");
|
|
12
|
+
function firstLine(buf) {
|
|
13
|
+
if (!buf || !buf.length) return "";
|
|
14
|
+
const s = Buffer.isBuffer(buf) ? buf.toString("utf8") : String(buf);
|
|
15
|
+
const nl = s.indexOf("\n");
|
|
16
|
+
return (nl === -1 ? s : s.slice(0, nl)).replace(/\s+$/u, "");
|
|
17
|
+
}
|
|
18
|
+
async function renderOurs(input) {
|
|
19
|
+
let engine = "";
|
|
20
|
+
try {
|
|
21
|
+
if (!existsSync(POINTER)) return "";
|
|
22
|
+
engine = readFileSync(POINTER, "utf8").trim();
|
|
23
|
+
} catch {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
if (!engine || !existsSync(engine)) return "";
|
|
27
|
+
try {
|
|
28
|
+
const line = await Promise.race([
|
|
29
|
+
import(pathToFileURL(engine).href).then(
|
|
30
|
+
(mod) => mod && typeof mod.render === "function" ? firstLine(mod.render()) : null
|
|
31
|
+
),
|
|
32
|
+
new Promise((resolve) => setTimeout(() => resolve(void 0), 30))
|
|
33
|
+
]);
|
|
34
|
+
if (typeof line === "string") return line;
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
const r = spawnSync(process.execPath, [engine], { input, timeout: 800 });
|
|
39
|
+
return firstLine(r.stdout);
|
|
40
|
+
} catch {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function renderForeign(input) {
|
|
45
|
+
try {
|
|
46
|
+
if (!existsSync(FOREIGN)) return "";
|
|
47
|
+
const saved = JSON.parse(readFileSync(FOREIGN, "utf8"));
|
|
48
|
+
const cmd = saved && saved.statusLine && saved.statusLine.command;
|
|
49
|
+
if (!cmd) return "";
|
|
50
|
+
const r = spawnSync(cmd, { shell: true, input, timeout: 4e3 });
|
|
51
|
+
return firstLine(r.stdout);
|
|
52
|
+
} catch {
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
let input = Buffer.alloc(0);
|
|
58
|
+
try {
|
|
59
|
+
if (!process.stdin.isTTY) input = readFileSync(0);
|
|
60
|
+
} catch {
|
|
61
|
+
}
|
|
62
|
+
const segments = [];
|
|
63
|
+
const foreign = renderForeign(input);
|
|
64
|
+
if (foreign) segments.push(foreign);
|
|
65
|
+
const ours = await renderOurs(input);
|
|
66
|
+
if (ours) segments.push(ours);
|
|
67
|
+
if (segments.length > 0) process.stdout.write(segments.join(" \xB7 ") + "\n");
|
|
68
|
+
process.exit(0);
|
|
69
|
+
} catch {
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/jpi-statusline.js
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { pathToFileURL } from "url";
|
|
8
|
+
var INDEX_CACHE_FILE = join(homedir(), ".terminalhire", "index-cache.json");
|
|
9
|
+
var INDEX_CACHE_TTL_MS = 15 * 60 * 1e3;
|
|
10
|
+
function readFreshCache() {
|
|
11
|
+
try {
|
|
12
|
+
const entry = JSON.parse(readFileSync(INDEX_CACHE_FILE, "utf8"));
|
|
13
|
+
if (typeof entry.ts !== "number" || Date.now() - entry.ts > INDEX_CACHE_TTL_MS) return null;
|
|
14
|
+
return entry;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function unreadChatCount(entry) {
|
|
20
|
+
const n = entry && entry.unreadChat && entry.unreadChat.count;
|
|
21
|
+
return typeof n === "number" && n > 0 ? n : 0;
|
|
22
|
+
}
|
|
23
|
+
function incomingCount(entry) {
|
|
24
|
+
const n = entry && entry.incomingPending && entry.incomingPending.count;
|
|
25
|
+
return typeof n === "number" && n > 0 ? n : 0;
|
|
26
|
+
}
|
|
27
|
+
function sessionStale(entry) {
|
|
28
|
+
return !!entry && entry.sessionStale === true;
|
|
29
|
+
}
|
|
30
|
+
function render() {
|
|
31
|
+
try {
|
|
32
|
+
const entry = readFreshCache();
|
|
33
|
+
if (!entry) return "";
|
|
34
|
+
const unread = unreadChatCount(entry);
|
|
35
|
+
const incoming = incomingCount(entry);
|
|
36
|
+
const stale = sessionStale(entry) && unread === 0 && incoming === 0;
|
|
37
|
+
const parts = [];
|
|
38
|
+
if (unread > 0) parts.push(`\u{1F4AC} ${unread} unread`);
|
|
39
|
+
if (incoming > 0) parts.push(`\u2709 ${incoming} waiting to connect`);
|
|
40
|
+
if (parts.length > 0) {
|
|
41
|
+
let line = parts.join(" \xB7 ");
|
|
42
|
+
line += unread > 0 ? " \u2014 run: terminalhire chat" : " \u2014 run: terminalhire intro --list";
|
|
43
|
+
return line;
|
|
44
|
+
}
|
|
45
|
+
if (stale) {
|
|
46
|
+
return "\u26A0 terminalhire session expired \u2014 run: terminalhire link";
|
|
47
|
+
}
|
|
48
|
+
return "";
|
|
49
|
+
} catch {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
55
|
+
if (isMain) {
|
|
56
|
+
const line = render();
|
|
57
|
+
if (line) process.stdout.write(line + "\n");
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
export {
|
|
64
|
+
render
|
|
65
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "terminalhire",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Local-first job matching for developers — ambient job matches in the Claude Code spinner. Matching runs on your machine; your profile never leaves it.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,13 +22,14 @@
|
|
|
22
22
|
"dist",
|
|
23
23
|
"fixtures",
|
|
24
24
|
"install.js",
|
|
25
|
+
"statusline-install.js",
|
|
25
26
|
"postinstall.js",
|
|
26
27
|
"README.md"
|
|
27
28
|
],
|
|
28
29
|
"scripts": {
|
|
29
30
|
"test": "for f in test/*.test.js; do echo \"# $f\"; node \"$f\" || exit 1; done",
|
|
30
31
|
"build": "tsup",
|
|
31
|
-
"bundle:plugin": "npm run build && rm -rf ../../plugins/terminalhire/dist && cp -R dist ../../plugins/terminalhire/dist && cp package.json ../../plugins/terminalhire/dist/package.json && cp install.js ../../plugins/terminalhire/dist/install.js && cp postinstall.js ../../plugins/terminalhire/dist/postinstall.js",
|
|
32
|
+
"bundle:plugin": "npm run build && rm -rf ../../plugins/terminalhire/dist && cp -R dist ../../plugins/terminalhire/dist && cp package.json ../../plugins/terminalhire/dist/package.json && cp install.js ../../plugins/terminalhire/dist/install.js && cp statusline-install.js ../../plugins/terminalhire/dist/statusline-install.js && cp postinstall.js ../../plugins/terminalhire/dist/postinstall.js",
|
|
32
33
|
"prepublishOnly": "npm run build",
|
|
33
34
|
"install-hook": "node install.js",
|
|
34
35
|
"postinstall": "node ./postinstall.js"
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* statusline-install.js — consent-gated installer for the CONNECTION-ONLY statusLine
|
|
4
|
+
* (feature 002 / ADR-002). Companion to install.js (which is spinner-only and, by its
|
|
5
|
+
* binding rule, NEVER writes settings.statusLine). This file is the ONLY writer of
|
|
6
|
+
* settings.statusLine, and it writes ONLY the self-updating launcher command — the
|
|
7
|
+
* statusLine it enables carries personal connection notifications, never job ads
|
|
8
|
+
* (the entire justification for re-opening the demoted statusLine surface).
|
|
9
|
+
*
|
|
10
|
+
* Guardrails (CONSTITUTION Rule 12 / ~/.claude/rules/safety.md):
|
|
11
|
+
* - Explicit typed "yes" BEFORE touching any system file (no-consent → settings.json
|
|
12
|
+
* byte-for-byte unchanged, no backup churn — V-4).
|
|
13
|
+
* - Timestamped backup of ~/.claude/settings.json before any write.
|
|
14
|
+
* - settings.statusLine.command is ZERO-VOLATILE: `node ~/.terminalhire/statusline-launch.js`
|
|
15
|
+
* — a stable, version-agnostic path (V-1). All version logic lives behind the
|
|
16
|
+
* hook-written engine-path pointer, so this artifact is written once and never
|
|
17
|
+
* re-stamped → it cannot recreate the stale-wrapper bug.
|
|
18
|
+
* - Chain-never-clobber: a pre-existing FOREIGN statusLine is saved verbatim to a
|
|
19
|
+
* sidecar and run FIRST by the launcher; uninstall restores it byte-for-byte (V-5),
|
|
20
|
+
* and it still renders when our part is empty (V-8).
|
|
21
|
+
* - Legacy re-point (Q4): an old terminalhire wrapper (npm-pinned .sh) is detected and,
|
|
22
|
+
* with a targeted yes, replaced by the self-updating launcher — never silently.
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* node statusline-install.js
|
|
26
|
+
* node statusline-install.js --uninstall
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
readFileSync, writeFileSync, copyFileSync, existsSync, mkdirSync, rmSync,
|
|
31
|
+
} from 'node:fs';
|
|
32
|
+
import { homedir } from 'node:os';
|
|
33
|
+
import { join, resolve, dirname } from 'node:path';
|
|
34
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
35
|
+
import { createInterface } from 'node:readline';
|
|
36
|
+
|
|
37
|
+
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
38
|
+
|
|
39
|
+
const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
|
|
40
|
+
const SETTINGS_DIR = dirname(SETTINGS_PATH);
|
|
41
|
+
const TERMINALHIRE_DIR = join(homedir(), '.terminalhire');
|
|
42
|
+
const STABLE_LAUNCHER = join(TERMINALHIRE_DIR, 'statusline-launch.js');
|
|
43
|
+
const FOREIGN_SIDECAR = join(TERMINALHIRE_DIR, 'statusline-foreign.json');
|
|
44
|
+
const LEGACY_WRAPPER = join(TERMINALHIRE_DIR, 'statusline-wrapper.sh');
|
|
45
|
+
|
|
46
|
+
// The exact command written to settings.statusLine — stable, version-agnostic (V-1).
|
|
47
|
+
const LAUNCHER_COMMAND = `node ${STABLE_LAUNCHER}`;
|
|
48
|
+
|
|
49
|
+
const UNINSTALL = process.argv.includes('--uninstall');
|
|
50
|
+
|
|
51
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function ask(question) {
|
|
54
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
55
|
+
return new Promise(res => {
|
|
56
|
+
rl.question(question, answer => {
|
|
57
|
+
rl.close();
|
|
58
|
+
res(answer.trim().toLowerCase());
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function backupSettings() {
|
|
64
|
+
if (!existsSync(SETTINGS_PATH)) return null;
|
|
65
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
66
|
+
const backupPath = `${SETTINGS_PATH}.terminalhire-backup-${ts}`;
|
|
67
|
+
copyFileSync(SETTINGS_PATH, backupPath);
|
|
68
|
+
console.log(` Backed up settings to: ${backupPath}`);
|
|
69
|
+
return backupPath;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readSettings() {
|
|
73
|
+
try {
|
|
74
|
+
if (existsSync(SETTINGS_PATH)) return JSON.parse(readFileSync(SETTINGS_PATH, 'utf8'));
|
|
75
|
+
} catch { /* fall through to empty */ }
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function writeSettings(settings) {
|
|
80
|
+
mkdirSync(SETTINGS_DIR, { recursive: true });
|
|
81
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Resolve the launcher SOURCE (bundled dist preferred; bin fallback for the dev workspace).
|
|
85
|
+
function resolveLauncherSource() {
|
|
86
|
+
const candidates = [
|
|
87
|
+
resolve(join(__dirname, 'dist', 'bin', 'jpi-statusline-launch.js')),
|
|
88
|
+
resolve(join(__dirname, 'bin', 'jpi-statusline-launch.js')),
|
|
89
|
+
];
|
|
90
|
+
for (const c of candidates) if (existsSync(c)) return c;
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Classify an existing statusLine command:
|
|
95
|
+
// 'ours' — already the self-updating launcher (idempotent)
|
|
96
|
+
// 'legacy' — an OLD terminalhire wrapper (npm-pinned .sh / dispatch) → offer re-point
|
|
97
|
+
// 'foreign' — someone else's statusLine → chain-wrap + preserve
|
|
98
|
+
// 'none' — no command
|
|
99
|
+
function classifyExisting(statusLine) {
|
|
100
|
+
const cmd = statusLine && typeof statusLine.command === 'string' ? statusLine.command : '';
|
|
101
|
+
if (!cmd) return 'none';
|
|
102
|
+
if (cmd.includes('statusline-launch.js')) return 'ours';
|
|
103
|
+
if (
|
|
104
|
+
cmd.includes('statusline-wrapper.sh') ||
|
|
105
|
+
existsSync(LEGACY_WRAPPER) && cmd.includes('.terminalhire') ||
|
|
106
|
+
cmd.includes('jpi-dispatch') ||
|
|
107
|
+
/terminalhire|\bjpi\b|jpi-/.test(cmd)
|
|
108
|
+
) {
|
|
109
|
+
return 'legacy';
|
|
110
|
+
}
|
|
111
|
+
return 'foreign';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Install ───────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
async function install() {
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log('┌─────────────────────────────────────────────────────────────────┐');
|
|
119
|
+
console.log('│ terminalhire — connection notifications in your statusLine │');
|
|
120
|
+
console.log('│ Personal connection signals only. Never job ads. │');
|
|
121
|
+
console.log('└─────────────────────────────────────────────────────────────────┘');
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log('DISCLOSURE — read before enabling');
|
|
124
|
+
console.log('');
|
|
125
|
+
console.log('WHAT THIS ENABLES:');
|
|
126
|
+
console.log(' A Claude Code statusLine that shows ONLY personal connection signals:');
|
|
127
|
+
console.log(' • 💬 N unread — messages from developers you accepted an intro with');
|
|
128
|
+
console.log(' • ✉ N waiting to connect — inbound intro requests awaiting your reply');
|
|
129
|
+
console.log(' • ⚠ session expired — a re-link hint when your linked session lapses');
|
|
130
|
+
console.log(' It NEVER shows job matches, role %-scores, or any advert — those stay in');
|
|
131
|
+
console.log(' the ambient spinner. The line is a LOCAL cache read: zero network calls.');
|
|
132
|
+
console.log('');
|
|
133
|
+
console.log('HOW IT STAYS CURRENT (no manual re-install ever):');
|
|
134
|
+
console.log(' settings.json gets a stable command: ' + LAUNCHER_COMMAND);
|
|
135
|
+
console.log(' On every session the plugin refreshes a pointer to its current engine, so');
|
|
136
|
+
console.log(' the statusLine follows plugin auto-updates automatically.');
|
|
137
|
+
console.log('');
|
|
138
|
+
console.log('WHAT THIS CHANGES:');
|
|
139
|
+
console.log(' • ~/.claude/settings.json — sets statusLine to the launcher above.');
|
|
140
|
+
console.log(' • A timestamped backup is created before any change.');
|
|
141
|
+
console.log(' • Any statusLine you already have is PRESERVED (run first) — never replaced.');
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log('HOW TO REMOVE:');
|
|
144
|
+
console.log(' • node statusline-install.js --uninstall (restores your prior statusLine)');
|
|
145
|
+
console.log('');
|
|
146
|
+
|
|
147
|
+
const answer = await ask('Enable the connection-notification statusLine? Type "yes" to continue: ');
|
|
148
|
+
if (answer !== 'yes') {
|
|
149
|
+
console.log('\nAborted — nothing was changed.');
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const src = resolveLauncherSource();
|
|
154
|
+
if (!src) {
|
|
155
|
+
console.log('\n Could not locate the statusLine launcher — nothing was changed.');
|
|
156
|
+
console.log(' Rebuild the package (npm run build) and try again.');
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const settings = readSettings();
|
|
161
|
+
const existing = settings.statusLine;
|
|
162
|
+
const kind = classifyExisting(existing);
|
|
163
|
+
|
|
164
|
+
// Legacy re-point (Q4) — consent-gated, never silent.
|
|
165
|
+
if (kind === 'legacy') {
|
|
166
|
+
console.log('');
|
|
167
|
+
console.log(' Found an older terminalhire statusLine (npm-pinned — can go stale).');
|
|
168
|
+
const rp = await ask(' Replace it with the self-updating launcher? Type "yes": ');
|
|
169
|
+
if (rp !== 'yes') {
|
|
170
|
+
console.log('\n Left your existing statusLine as-is — nothing was changed.');
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
// It is our own legacy artifact → do NOT preserve it as "foreign".
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Only NOW (post-consent) do we touch the filesystem — V-4.
|
|
177
|
+
console.log('');
|
|
178
|
+
mkdirSync(TERMINALHIRE_DIR, { recursive: true });
|
|
179
|
+
|
|
180
|
+
// Copy the launcher to its stable, version-agnostic home.
|
|
181
|
+
copyFileSync(src, STABLE_LAUNCHER);
|
|
182
|
+
|
|
183
|
+
// Chain-wrap a genuinely FOREIGN statusLine: save it verbatim so the launcher runs it
|
|
184
|
+
// first and uninstall can restore it byte-for-byte (V-5/V-8).
|
|
185
|
+
if (kind === 'foreign') {
|
|
186
|
+
writeFileSync(FOREIGN_SIDECAR, JSON.stringify({ statusLine: existing }, null, 2) + '\n', 'utf8');
|
|
187
|
+
console.log(' Preserved your existing statusLine — it will render alongside ours.');
|
|
188
|
+
} else if (kind === 'ours' && existsSync(FOREIGN_SIDECAR)) {
|
|
189
|
+
// Idempotent re-run: keep any previously-saved foreign statusLine sidecar.
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const backupPath = backupSettings();
|
|
193
|
+
|
|
194
|
+
settings.statusLine = { type: 'command', command: LAUNCHER_COMMAND };
|
|
195
|
+
writeSettings(settings);
|
|
196
|
+
|
|
197
|
+
console.log(' statusLine: ENABLED');
|
|
198
|
+
console.log(` Command: ${LAUNCHER_COMMAND}`);
|
|
199
|
+
if (backupPath) console.log(` Backup: ${backupPath}`);
|
|
200
|
+
console.log('');
|
|
201
|
+
console.log('Done. Restart Claude Code to see connection notifications in your statusLine.');
|
|
202
|
+
console.log(' Remove any time: node statusline-install.js --uninstall');
|
|
203
|
+
console.log('');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Uninstall ─────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
async function uninstall() {
|
|
209
|
+
console.log('');
|
|
210
|
+
console.log('terminalhire — remove the connection-notification statusLine');
|
|
211
|
+
console.log('');
|
|
212
|
+
console.log(' This restores whatever statusLine you had before (byte-for-byte), or');
|
|
213
|
+
console.log(' removes the statusLine entirely if terminalhire added it fresh.');
|
|
214
|
+
console.log('');
|
|
215
|
+
|
|
216
|
+
const answer = await ask('Remove the terminalhire statusLine? Type "yes" to continue: ');
|
|
217
|
+
if (answer !== 'yes') {
|
|
218
|
+
console.log('\nAborted — nothing was changed.');
|
|
219
|
+
process.exit(0);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log('');
|
|
223
|
+
const settings = readSettings();
|
|
224
|
+
const isOurs = settings.statusLine
|
|
225
|
+
&& typeof settings.statusLine.command === 'string'
|
|
226
|
+
&& settings.statusLine.command.includes('statusline-launch.js');
|
|
227
|
+
|
|
228
|
+
// Only mutate settings if the current statusLine is ours (never clobber a foreign one
|
|
229
|
+
// the user re-added after install).
|
|
230
|
+
if (isOurs) {
|
|
231
|
+
const backupPath = backupSettings();
|
|
232
|
+
if (existsSync(FOREIGN_SIDECAR)) {
|
|
233
|
+
try {
|
|
234
|
+
const saved = JSON.parse(readFileSync(FOREIGN_SIDECAR, 'utf8'));
|
|
235
|
+
if (saved && saved.statusLine) settings.statusLine = saved.statusLine; // byte-for-byte (V-5)
|
|
236
|
+
else delete settings.statusLine;
|
|
237
|
+
} catch {
|
|
238
|
+
delete settings.statusLine;
|
|
239
|
+
}
|
|
240
|
+
console.log(' Restored your previous statusLine.');
|
|
241
|
+
} else {
|
|
242
|
+
delete settings.statusLine;
|
|
243
|
+
console.log(' Removed the terminalhire statusLine.');
|
|
244
|
+
}
|
|
245
|
+
writeSettings(settings);
|
|
246
|
+
if (backupPath) console.log(` Backup: ${backupPath}`);
|
|
247
|
+
} else {
|
|
248
|
+
console.log(' No terminalhire statusLine is currently active — settings.json left unchanged.');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Clean up our stable artifacts (the pointer is re-written harmlessly by the hook).
|
|
252
|
+
try { if (existsSync(FOREIGN_SIDECAR)) rmSync(FOREIGN_SIDECAR); } catch { /* best-effort */ }
|
|
253
|
+
try { if (existsSync(STABLE_LAUNCHER)) rmSync(STABLE_LAUNCHER); } catch { /* best-effort */ }
|
|
254
|
+
|
|
255
|
+
console.log('');
|
|
256
|
+
console.log('Done. Restart Claude Code to apply.');
|
|
257
|
+
console.log('');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Entry ─────────────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
263
|
+
if (isMain) {
|
|
264
|
+
const run = UNINSTALL ? uninstall : install;
|
|
265
|
+
run().catch(err => {
|
|
266
|
+
console.error(`${UNINSTALL ? 'Uninstall' : 'Install'} error:`, err.message);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export { install as installStatusline, uninstall as uninstallStatusline, LAUNCHER_COMMAND };
|