pair-mode 0.3.0 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/README.md +66 -9
  4. package/assets/syntax/clojure.yaml +36 -0
  5. package/assets/syntax/cmake.yaml +41 -0
  6. package/assets/syntax/crystal.yaml +71 -0
  7. package/assets/syntax/csharp.yaml +51 -0
  8. package/assets/syntax/dart.yaml +45 -0
  9. package/assets/syntax/elm.yaml +38 -0
  10. package/assets/syntax/erb.yaml +42 -0
  11. package/assets/syntax/erlang.yaml +45 -0
  12. package/assets/syntax/fsharp.yaml +48 -0
  13. package/assets/syntax/graphql.yaml +47 -0
  14. package/assets/syntax/groovy.yaml +111 -0
  15. package/assets/syntax/haml.yaml +16 -0
  16. package/assets/syntax/haskell.yaml +52 -0
  17. package/assets/syntax/ini.yaml +23 -0
  18. package/assets/syntax/java.yaml +36 -0
  19. package/assets/syntax/julia.yaml +56 -0
  20. package/assets/syntax/kotlin.yaml +65 -0
  21. package/assets/syntax/makefile.yaml +37 -0
  22. package/assets/syntax/nginx.yaml +22 -0
  23. package/assets/syntax/nim.yaml +27 -0
  24. package/assets/syntax/nix.yaml +32 -0
  25. package/assets/syntax/objc.yaml +60 -0
  26. package/assets/syntax/ocaml.yaml +43 -0
  27. package/assets/syntax/perl.yaml +58 -0
  28. package/assets/syntax/php.yaml +60 -0
  29. package/assets/syntax/r.yaml +30 -0
  30. package/assets/syntax/scala.yaml +32 -0
  31. package/assets/syntax/svelte.yaml +27 -0
  32. package/assets/syntax/swift.yaml +102 -0
  33. package/assets/syntax/vue.yaml +63 -0
  34. package/assets/syntax/xml.yaml +37 -0
  35. package/assets/syntax/zig.yaml +52 -0
  36. package/dist/claude-code.js +461 -149
  37. package/dist/cli.js +1490 -476
  38. package/dist/codex.js +461 -149
  39. package/dist/opencode.js +453 -142
  40. package/dist/pair-tui.js +172 -26
  41. package/dist/pi.js +508 -155
  42. package/package.json +4 -4
  43. package/skills/toggle/SKILL.md +24 -0
  44. package/commands/pair.md +0 -18
  45. package/skills/pair/SKILL.md +0 -20
package/dist/cli.js CHANGED
@@ -7361,8 +7361,8 @@ var require_dist = __commonJS({
7361
7361
  });
7362
7362
 
7363
7363
  // src/cli/index.ts
7364
- import { readFileSync as readFileSync8 } from "node:fs";
7365
- import { join as join13, resolve as resolve2 } from "node:path";
7364
+ import { readFileSync as readFileSync10 } from "node:fs";
7365
+ import { join as join15, resolve as resolve2 } from "node:path";
7366
7366
 
7367
7367
  // src/cli/setup/setup.ts
7368
7368
  import { createInterface } from "node:readline/promises";
@@ -7886,18 +7886,65 @@ function describeEphemeralRoot(root) {
7886
7886
  }
7887
7887
 
7888
7888
  // src/cli/doctor/doctor.ts
7889
- import { existsSync as existsSync8, openSync as openSync2, closeSync as closeSync2, readFileSync as readFileSync5, statSync } from "node:fs";
7889
+ import { existsSync as existsSync10, openSync as openSync3, closeSync as closeSync3, readFileSync as readFileSync7, statSync as statSync2 } from "node:fs";
7890
7890
  import { homedir as homedir4 } from "node:os";
7891
- import { join as join12 } from "node:path";
7891
+ import { basename as basename6, join as join14 } from "node:path";
7892
7892
 
7893
7893
  // src/core/state/state.ts
7894
7894
  import { createHash } from "node:crypto";
7895
7895
  import { homedir as homedir3 } from "node:os";
7896
- import { join as join5, dirname as dirname4, basename } from "node:path";
7897
- import { existsSync as existsSync4, realpathSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, unlinkSync } from "node:fs";
7896
+ import { join as join6, dirname as dirname4, basename } from "node:path";
7897
+ import { existsSync as existsSync4, realpathSync, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2 } from "node:fs";
7898
+
7899
+ // src/helpers/guards.ts
7900
+ function isString(value) {
7901
+ return typeof value === "string";
7902
+ }
7903
+ function isNullableString(value) {
7904
+ return value === null || isString(value);
7905
+ }
7906
+
7907
+ // src/helpers/removeQuietly.ts
7908
+ import { unlinkSync } from "node:fs";
7909
+ function removeQuietly(path) {
7910
+ try {
7911
+ unlinkSync(path);
7912
+ } catch {
7913
+ }
7914
+ }
7915
+
7916
+ // src/helpers/resultFilePath.ts
7917
+ import { randomBytes } from "node:crypto";
7918
+ import { tmpdir } from "node:os";
7919
+ import { join as join5 } from "node:path";
7920
+ var NAME_BYTES = 6;
7921
+ function resultFilePath() {
7922
+ const name = `pair-result-${randomBytes(NAME_BYTES).toString("hex")}.json`;
7923
+ return join5(tmpdir(), name);
7924
+ }
7925
+
7926
+ // src/helpers/spawn.ts
7927
+ import { spawnSync as spawnSync2 } from "node:child_process";
7928
+ var defaultSpawn = (command, args) => {
7929
+ const result = spawnSync2(command, args, { stdio: ["ignore", "ignore", "pipe"] });
7930
+ const stderr = result.stderr ? result.stderr.toString("utf-8") : result.error?.message ?? "";
7931
+ return { status: result.status, stderr };
7932
+ };
7933
+
7934
+ // src/helpers/splitLines.ts
7935
+ function splitLines(text) {
7936
+ const lines = text.split("\n");
7937
+ const last = lines.at(-1);
7938
+ if (last === "") {
7939
+ lines.pop();
7940
+ }
7941
+ return lines;
7942
+ }
7943
+
7944
+ // src/core/state/state.ts
7898
7945
  function stateDir() {
7899
- const base = process.env["XDG_STATE_HOME"] || join5(homedir3(), ".local", "state");
7900
- return join5(base, "pair-mode");
7946
+ const base = process.env["XDG_STATE_HOME"] || join6(homedir3(), ".local", "state");
7947
+ return join6(base, "pair-mode");
7901
7948
  }
7902
7949
  function realpathLenient(path) {
7903
7950
  try {
@@ -7907,7 +7954,7 @@ function realpathLenient(path) {
7907
7954
  if (parent === path) {
7908
7955
  return path;
7909
7956
  }
7910
- return join5(realpathLenient(parent), basename(path));
7957
+ return join6(realpathLenient(parent), basename(path));
7911
7958
  }
7912
7959
  }
7913
7960
  var DIGEST_LENGTH = 16;
@@ -7916,16 +7963,88 @@ function digestFor(directory) {
7916
7963
  return createHash("sha1").update(real).digest("hex").slice(0, DIGEST_LENGTH);
7917
7964
  }
7918
7965
  function flagPath(directory) {
7919
- return join5(stateDir(), `${digestFor(directory)}.on`);
7966
+ return join6(stateDir(), `${digestFor(directory)}.on`);
7920
7967
  }
7921
7968
  function sessionsDir() {
7922
- return join5(stateDir(), "sessions");
7969
+ return join6(stateDir(), "sessions");
7970
+ }
7971
+ var OWNER_ONLY_DIR = 448;
7972
+ var OWNER_ONLY_FILE = 384;
7973
+ function makeSessionsDir() {
7974
+ const path = sessionsDir();
7975
+ mkdirSync2(path, { recursive: true, mode: OWNER_ONLY_DIR });
7976
+ return path;
7923
7977
  }
7924
7978
  function sessionSocketPath(directory) {
7925
- return join5(sessionsDir(), `${digestFor(directory)}.sock`);
7979
+ return join6(sessionsDir(), `${digestFor(directory)}.sock`);
7926
7980
  }
7927
7981
  function sessionUrlPath(directory) {
7928
- return join5(sessionsDir(), `${digestFor(directory)}.url`);
7982
+ return join6(sessionsDir(), `${digestFor(directory)}.url`);
7983
+ }
7984
+ function findSessionSocket(filePath) {
7985
+ let current = dirname4(realpathLenient(filePath));
7986
+ while (true) {
7987
+ const candidate = sessionSocketPath(current);
7988
+ if (existsSync4(candidate)) {
7989
+ return candidate;
7990
+ }
7991
+ const parent = dirname4(current);
7992
+ if (parent === current) {
7993
+ return null;
7994
+ }
7995
+ current = parent;
7996
+ }
7997
+ }
7998
+ function resolveSocketPath(filePath, key) {
7999
+ if (key !== void 0) {
8000
+ const candidate = sessionKeySocketPath(key);
8001
+ if (existsSync4(candidate)) {
8002
+ return candidate;
8003
+ }
8004
+ }
8005
+ return findSessionSocket(filePath);
8006
+ }
8007
+ function sessionFlagState(key) {
8008
+ if (existsSync4(sessionKeyOptOutPath(key))) {
8009
+ return "off";
8010
+ }
8011
+ return existsSync4(sessionKeyFlagPath(key)) ? "on" : "unset";
8012
+ }
8013
+ function directoryEnabled(filePath) {
8014
+ let current = dirname4(realpathLenient(filePath));
8015
+ while (true) {
8016
+ if (existsSync4(flagPath(current))) {
8017
+ return true;
8018
+ }
8019
+ const parent = dirname4(current);
8020
+ if (parent === current) {
8021
+ return false;
8022
+ }
8023
+ current = parent;
8024
+ }
8025
+ }
8026
+ function isEnabled(filePath, key) {
8027
+ if (key !== void 0) {
8028
+ const state = sessionFlagState(key);
8029
+ if (state !== "unset") {
8030
+ return state === "on";
8031
+ }
8032
+ }
8033
+ return directoryEnabled(filePath);
8034
+ }
8035
+ function enableSession(key) {
8036
+ const path = sessionKeyFlagPath(key);
8037
+ makeSessionsDir();
8038
+ removeQuietly(sessionKeyOptOutPath(key));
8039
+ writeFileSync2(path, "", { mode: OWNER_ONLY_FILE });
8040
+ return path;
8041
+ }
8042
+ function optOutSession(key) {
8043
+ const path = sessionKeyOptOutPath(key);
8044
+ makeSessionsDir();
8045
+ removeQuietly(sessionKeyFlagPath(key));
8046
+ writeFileSync2(path, "", { mode: OWNER_ONLY_FILE });
8047
+ return path;
7929
8048
  }
7930
8049
  function enable(directory) {
7931
8050
  const path = flagPath(directory);
@@ -7938,46 +8057,80 @@ function disable(directory) {
7938
8057
  if (!existsSync4(path)) {
7939
8058
  return false;
7940
8059
  }
7941
- unlinkSync(path);
8060
+ unlinkSync2(path);
7942
8061
  return true;
7943
8062
  }
7944
-
7945
- // src/helpers/removeQuietly.ts
7946
- import { unlinkSync as unlinkSync2 } from "node:fs";
7947
- function removeQuietly(path) {
7948
- try {
7949
- unlinkSync2(path);
7950
- } catch {
8063
+ var SESSION_KEY_LENGTH = 8;
8064
+ function sessionKey(agentSessionId2) {
8065
+ const digest = createHash("sha1").update(agentSessionId2).digest("hex");
8066
+ return `s-${digest.slice(0, SESSION_KEY_LENGTH)}`;
8067
+ }
8068
+ function watchSocketPath(directory, key, override) {
8069
+ if (override !== void 0) {
8070
+ return override;
7951
8071
  }
8072
+ return key ? sessionKeySocketPath(key) : sessionSocketPath(directory);
7952
8073
  }
7953
-
7954
- // src/helpers/resultFilePath.ts
7955
- import { randomBytes } from "node:crypto";
7956
- import { tmpdir } from "node:os";
7957
- import { join as join6 } from "node:path";
7958
- var NAME_BYTES = 6;
7959
- function resultFilePath() {
7960
- const name = `pair-result-${randomBytes(NAME_BYTES).toString("hex")}.json`;
7961
- return join6(tmpdir(), name);
8074
+ function watchUrlPath(directory, key) {
8075
+ return key ? sessionKeyUrlPath(key) : sessionUrlPath(directory);
8076
+ }
8077
+ function keyFor(agentSessionId2) {
8078
+ return agentSessionId2 === void 0 || agentSessionId2 === "" ? void 0 : sessionKey(agentSessionId2);
8079
+ }
8080
+ function sessionKeyPath(key, extension) {
8081
+ return join6(sessionsDir(), `${key}${extension}`);
8082
+ }
8083
+ function sessionKeySocketPath(key) {
8084
+ return sessionKeyPath(key, ".sock");
8085
+ }
8086
+ function sessionKeyFlagPath(key) {
8087
+ return sessionKeyPath(key, ".on");
8088
+ }
8089
+ function sessionKeyOptOutPath(key) {
8090
+ return sessionKeyPath(key, ".off");
8091
+ }
8092
+ function sessionKeyUrlPath(key) {
8093
+ return sessionKeyPath(key, ".url");
7962
8094
  }
7963
8095
 
7964
- // src/helpers/spawn.ts
7965
- import { spawnSync as spawnSync2 } from "node:child_process";
7966
- var defaultSpawn = (command, args) => {
7967
- const result = spawnSync2(command, args, { stdio: ["ignore", "ignore", "pipe"] });
7968
- const stderr = result.stderr ? result.stderr.toString("utf-8") : result.error?.message ?? "";
7969
- return { status: result.status, stderr };
7970
- };
7971
-
7972
- // src/helpers/splitLines.ts
7973
- function splitLines(text) {
7974
- const lines = text.split("\n");
7975
- const last = lines.at(-1);
7976
- if (last === "") {
7977
- lines.pop();
8096
+ // src/core/state/record.ts
8097
+ import { spawnSync as spawnSync3 } from "node:child_process";
8098
+ import { basename as basename2 } from "node:path";
8099
+ var GIT_TIMEOUT_MS = 2e3;
8100
+ function currentBranch(directory) {
8101
+ const result = spawnSync3("git", ["-C", directory, "rev-parse", "--abbrev-ref", "HEAD"], {
8102
+ encoding: "utf-8",
8103
+ timeout: GIT_TIMEOUT_MS
8104
+ });
8105
+ if (result.status !== 0) {
8106
+ return null;
7978
8107
  }
7979
- return lines;
8108
+ const branch = result.stdout.trim();
8109
+ return branch === "" ? null : branch;
8110
+ }
8111
+ function sessionLabel(directory, branch) {
8112
+ const name = basename2(directory);
8113
+ return branch === null ? name : `${name}@${branch}`;
7980
8114
  }
8115
+ function buildSessionRecord(options, socketPath) {
8116
+ const branch = currentBranch(options.directory);
8117
+ const kind = options.sessionKey === void 0 ? "directory" : "session";
8118
+ return {
8119
+ id: options.sessionKey ?? basename2(socketPath, ".sock"),
8120
+ kind,
8121
+ label: sessionLabel(options.directory, branch),
8122
+ directory: options.directory,
8123
+ branch,
8124
+ agentSessionId: options.agentSessionId ?? null,
8125
+ agentKind: options.agentKind ?? null,
8126
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
8127
+ pid: process.pid
8128
+ };
8129
+ }
8130
+
8131
+ // src/cli/sessions/sessions.ts
8132
+ import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
8133
+ import { basename as basename3, join as join7 } from "node:path";
7981
8134
 
7982
8135
  // src/transports/session/wire.ts
7983
8136
  var CLIENT_KINDS = ["tui", "web"];
@@ -7992,11 +8145,11 @@ function createLineReader() {
7992
8145
  return parts.filter((line2) => line2.trim() !== "");
7993
8146
  };
7994
8147
  }
7995
- function isString(value) {
8148
+ function isString2(value) {
7996
8149
  return typeof value === "string";
7997
8150
  }
7998
8151
  function isClientKind(value) {
7999
- return isString(value) && CLIENT_KINDS.includes(value);
8152
+ return isString2(value) && CLIENT_KINDS.includes(value);
8000
8153
  }
8001
8154
  function isQuestion(value) {
8002
8155
  if (!isRecord(value)) {
@@ -8004,16 +8157,16 @@ function isQuestion(value) {
8004
8157
  }
8005
8158
  const line2 = value["line"];
8006
8159
  const lineOk = line2 === null || typeof line2 === "number";
8007
- return lineOk && isString(value["code"]) && isString(value["text"]);
8160
+ return lineOk && isString2(value["code"]) && isString2(value["text"]);
8008
8161
  }
8009
8162
  function isQuestionList(value) {
8010
8163
  return Array.isArray(value) && value.every(isQuestion);
8011
8164
  }
8012
8165
  function toSubmit(raw) {
8013
- if (!isString(raw["tool"]) || !isString(raw["path"])) {
8166
+ if (!isString2(raw["tool"]) || !isString2(raw["path"])) {
8014
8167
  return null;
8015
8168
  }
8016
- if (!isString(raw["before"]) || !isString(raw["after"])) {
8169
+ if (!isString2(raw["before"]) || !isString2(raw["after"])) {
8017
8170
  return null;
8018
8171
  }
8019
8172
  return {
@@ -8029,7 +8182,7 @@ function toAttach(raw) {
8029
8182
  }
8030
8183
  function toReview(raw) {
8031
8184
  const submit = toSubmit({ ...raw, type: "submit" });
8032
- if (submit === null || !isString(raw["id"])) {
8185
+ if (submit === null || !isString2(raw["id"])) {
8033
8186
  return null;
8034
8187
  }
8035
8188
  return {
@@ -8042,13 +8195,31 @@ function toReview(raw) {
8042
8195
  };
8043
8196
  }
8044
8197
  function toVerdict(raw) {
8045
- if (!isString(raw["id"]) || !isQuestionList(raw["questions"])) {
8198
+ if (!isString2(raw["id"]) || !isQuestionList(raw["questions"])) {
8046
8199
  return null;
8047
8200
  }
8048
8201
  return { type: "verdict", id: raw["id"], questions: raw["questions"] };
8049
8202
  }
8050
8203
  function toCancel(raw) {
8051
- return isString(raw["id"]) ? { type: "cancel", id: raw["id"] } : null;
8204
+ return isString2(raw["id"]) ? { type: "cancel", id: raw["id"] } : null;
8205
+ }
8206
+ function isNumber(value) {
8207
+ return typeof value === "number" && Number.isFinite(value);
8208
+ }
8209
+ function toState(raw) {
8210
+ const lastAttachAt = raw["lastAttachAt"];
8211
+ if (lastAttachAt !== null && !isString2(lastAttachAt)) {
8212
+ return null;
8213
+ }
8214
+ if (!isNumber(raw["clientCount"]) || !isNumber(raw["waitingDepth"])) {
8215
+ return null;
8216
+ }
8217
+ return {
8218
+ type: "state",
8219
+ clientCount: raw["clientCount"],
8220
+ waitingDepth: raw["waitingDepth"],
8221
+ lastAttachAt
8222
+ };
8052
8223
  }
8053
8224
  function decodeLine(line2) {
8054
8225
  let parsed;
@@ -8076,13 +8247,19 @@ function decodeLine(line2) {
8076
8247
  if (type === "cancel") {
8077
8248
  return toCancel(parsed);
8078
8249
  }
8250
+ if (type === "status") {
8251
+ return { type: "status" };
8252
+ }
8253
+ if (type === "state") {
8254
+ return toState(parsed);
8255
+ }
8079
8256
  return null;
8080
8257
  }
8081
8258
 
8082
8259
  // src/transports/session/server.ts
8083
8260
  import { createServer, createConnection } from "node:net";
8084
8261
  import { randomBytes as randomBytes2 } from "node:crypto";
8085
- import { chmodSync, mkdirSync as mkdirSync3 } from "node:fs";
8262
+ import { chmodSync, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
8086
8263
  import { dirname as dirname5 } from "node:path";
8087
8264
 
8088
8265
  // src/transports/session/queue.ts
@@ -8096,17 +8273,21 @@ function enqueue(state, id, request) {
8096
8273
  function findReview(state, id) {
8097
8274
  return state.reviews.find((review) => review.id === id) ?? null;
8098
8275
  }
8276
+ function offeredReviews(state) {
8277
+ return state.reviews.filter((review) => review.status === "offered");
8278
+ }
8099
8279
  function waitingDepth(state) {
8100
8280
  return state.reviews.filter((review) => review.status === "waiting").length;
8101
8281
  }
8102
- function takeNext(state) {
8103
- const next = state.reviews.find((review) => review.status === "waiting");
8104
- if (next === void 0) {
8105
- return { state, review: null };
8282
+ function offerAll(state) {
8283
+ const waiting = state.reviews.filter((review) => review.status === "waiting");
8284
+ if (waiting.length === 0) {
8285
+ return { state, reviews: [] };
8106
8286
  }
8107
- const taken = { ...next, status: "inFlight" };
8108
- const reviews = state.reviews.map((review) => review.id === next.id ? taken : review);
8109
- return { state: { reviews }, review: taken };
8287
+ const offered = waiting.map((review) => ({ ...review, status: "offered" }));
8288
+ const byId = new Map(offered.map((review) => [review.id, review]));
8289
+ const reviews = state.reviews.map((review) => byId.get(review.id) ?? review);
8290
+ return { state: { reviews }, reviews: offered };
8110
8291
  }
8111
8292
  function complete(state, id) {
8112
8293
  return { reviews: state.reviews.filter((review) => review.id !== id) };
@@ -8120,8 +8301,9 @@ function release(state, id) {
8120
8301
 
8121
8302
  // src/transports/session/server.ts
8122
8303
  var ID_BYTES = 8;
8123
- var OWNER_ONLY_DIR = 448;
8304
+ var OWNER_ONLY_DIR2 = 448;
8124
8305
  var OWNER_ONLY_SOCKET = 384;
8306
+ var OWNER_ONLY_RECORD = 384;
8125
8307
  var STALE_PROBE_TIMEOUT_MS = 250;
8126
8308
  function defaultGenerateId() {
8127
8309
  return randomBytes2(ID_BYTES).toString("hex");
@@ -8161,7 +8343,7 @@ function restrictToOwner(path) {
8161
8343
  }
8162
8344
  }
8163
8345
  async function bindSocket(server, path) {
8164
- mkdirSync3(dirname5(path), { recursive: true, mode: OWNER_ONLY_DIR });
8346
+ mkdirSync3(dirname5(path), { recursive: true, mode: OWNER_ONLY_DIR2 });
8165
8347
  try {
8166
8348
  await listenOn(server, path);
8167
8349
  restrictToOwner(path);
@@ -8178,14 +8360,30 @@ async function bindSocket(server, path) {
8178
8360
  await listenOn(server, path);
8179
8361
  restrictToOwner(path);
8180
8362
  }
8363
+ function recordPathFor(socketPath) {
8364
+ return socketPath.replace(/\.sock$/, ".json");
8365
+ }
8366
+ function writeRecord(socketPath, record) {
8367
+ try {
8368
+ writeFileSync3(recordPathFor(socketPath), JSON.stringify(record, null, 2) + "\n", {
8369
+ encoding: "utf-8",
8370
+ mode: OWNER_ONLY_RECORD
8371
+ });
8372
+ } catch {
8373
+ }
8374
+ }
8181
8375
  async function startSessionServer(options) {
8182
8376
  const generateId = options.generateId ?? defaultGenerateId;
8183
8377
  const reportError = options.onError ?? defaultReportError;
8184
8378
  let queue = emptyQueue();
8185
- const agents = /* @__PURE__ */ new Map();
8379
+ const agentByReview = /* @__PURE__ */ new Map();
8186
8380
  const clients = /* @__PURE__ */ new Map();
8381
+ const clientBySocket = /* @__PURE__ */ new Map();
8382
+ const clientsByReview = /* @__PURE__ */ new Map();
8187
8383
  const connections = /* @__PURE__ */ new Set();
8188
8384
  const changeHandlers = [];
8385
+ let lastAttachAt = null;
8386
+ let nextClientId = 0;
8189
8387
  function announce() {
8190
8388
  changeHandlers.forEach((handler) => handler());
8191
8389
  }
@@ -8194,81 +8392,102 @@ async function startSessionServer(options) {
8194
8392
  socket.write(encode(message));
8195
8393
  }
8196
8394
  }
8197
- function idleClient() {
8198
- const entry = [...clients.entries()].find(([, held]) => held === null);
8199
- return entry === void 0 ? null : entry[0];
8200
- }
8201
- function clientHolding(id) {
8202
- const entry = [...clients.entries()].find(([, held]) => held === id);
8203
- return entry === void 0 ? null : entry[0];
8395
+ function reviewMessage(review) {
8396
+ return {
8397
+ type: "review",
8398
+ id: review.id,
8399
+ tool: review.request.tool,
8400
+ path: review.request.filePath,
8401
+ before: review.request.before,
8402
+ after: review.request.after
8403
+ };
8204
8404
  }
8205
8405
  function dispatch() {
8206
- let client = idleClient();
8207
- while (client !== null) {
8208
- const result = takeNext(queue);
8209
- if (result.review === null) {
8210
- break;
8211
- }
8212
- queue = result.state;
8213
- clients.set(client, result.review.id);
8214
- send(client, {
8215
- type: "review",
8216
- id: result.review.id,
8217
- tool: result.review.request.tool,
8218
- path: result.review.request.filePath,
8219
- before: result.review.request.before,
8220
- after: result.review.request.after
8221
- });
8222
- client = idleClient();
8406
+ if (clients.size === 0) {
8407
+ announce();
8408
+ return;
8223
8409
  }
8410
+ const result = offerAll(queue);
8411
+ queue = result.state;
8412
+ result.reviews.forEach((review) => {
8413
+ clientsByReview.set(review.id, new Map(clients));
8414
+ clients.forEach((client) => send(client.socket, reviewMessage(review)));
8415
+ });
8224
8416
  announce();
8225
8417
  }
8226
8418
  function handleSubmit(socket, request) {
8227
8419
  const id = generateId();
8228
8420
  queue = enqueue(queue, id, request);
8229
- agents.set(id, socket);
8421
+ agentByReview.set(id, socket);
8230
8422
  dispatch();
8231
8423
  }
8232
8424
  function handleAttach(socket) {
8233
- clients.set(socket, null);
8425
+ if (clientBySocket.has(socket)) {
8426
+ return;
8427
+ }
8428
+ nextClientId += 1;
8429
+ const client = { id: `c${nextClientId}`, socket };
8430
+ clients.set(client.id, client);
8431
+ clientBySocket.set(socket, client);
8432
+ lastAttachAt = (/* @__PURE__ */ new Date()).toISOString();
8433
+ offeredReviews(queue).forEach((review) => {
8434
+ const viewers = clientsByReview.get(review.id) ?? /* @__PURE__ */ new Map();
8435
+ viewers.set(client.id, client);
8436
+ clientsByReview.set(review.id, viewers);
8437
+ send(socket, reviewMessage(review));
8438
+ });
8234
8439
  dispatch();
8235
8440
  }
8236
8441
  function handleVerdict(socket, message) {
8237
- const agent = agents.get(message.id);
8238
- if (agent !== void 0) {
8442
+ const answering = clientBySocket.get(socket);
8443
+ if (!answering) {
8444
+ return;
8445
+ }
8446
+ const viewers = clientsByReview.get(message.id);
8447
+ if (!viewers?.has(answering.id)) {
8448
+ return;
8449
+ }
8450
+ const agent = agentByReview.get(message.id);
8451
+ if (agent) {
8239
8452
  send(agent, message);
8240
- agents.delete(message.id);
8453
+ agentByReview.delete(message.id);
8241
8454
  }
8455
+ viewers.forEach((viewer) => {
8456
+ if (viewer.id !== answering.id) {
8457
+ send(viewer.socket, { type: "cancel", id: message.id });
8458
+ }
8459
+ });
8460
+ clientsByReview.delete(message.id);
8242
8461
  queue = complete(queue, message.id);
8243
- if (clients.get(socket) === message.id) {
8244
- clients.set(socket, null);
8245
- }
8246
8462
  dispatch();
8247
8463
  }
8248
8464
  function dropAgent(socket) {
8249
- const owned = [...agents.entries()].filter(([, agentSocket]) => agentSocket === socket);
8465
+ const owned = [...agentByReview.entries()].filter(([, agentSocket]) => agentSocket === socket);
8466
+ if (owned.length === 0) {
8467
+ return;
8468
+ }
8250
8469
  owned.forEach(([id]) => {
8251
- agents.delete(id);
8252
- const holder = clientHolding(id);
8253
- if (holder !== null) {
8254
- send(holder, { type: "cancel", id });
8255
- clients.set(holder, null);
8256
- }
8470
+ agentByReview.delete(id);
8471
+ clientsByReview.get(id)?.forEach((viewer) => send(viewer.socket, { type: "cancel", id }));
8472
+ clientsByReview.delete(id);
8257
8473
  queue = complete(queue, id);
8258
8474
  });
8259
- if (owned.length > 0) {
8260
- dispatch();
8261
- }
8475
+ dispatch();
8262
8476
  }
8263
8477
  function dropClient(socket) {
8264
- const held = clients.get(socket);
8265
- if (held === void 0) {
8478
+ const leaving = clientBySocket.get(socket);
8479
+ if (!leaving) {
8266
8480
  return;
8267
8481
  }
8268
- clients.delete(socket);
8269
- if (held !== null && findReview(queue, held) !== null) {
8270
- queue = release(queue, held);
8271
- }
8482
+ clientBySocket.delete(socket);
8483
+ clients.delete(leaving.id);
8484
+ clientsByReview.forEach((viewers, id) => {
8485
+ viewers.delete(leaving.id);
8486
+ if (viewers.size === 0 && findReview(queue, id) !== null) {
8487
+ queue = release(queue, id);
8488
+ clientsByReview.delete(id);
8489
+ }
8490
+ });
8272
8491
  dispatch();
8273
8492
  }
8274
8493
  function handleLine(socket, line2) {
@@ -8289,6 +8508,15 @@ async function startSessionServer(options) {
8289
8508
  handleAttach(socket);
8290
8509
  return;
8291
8510
  }
8511
+ if (message.type === "status") {
8512
+ send(socket, {
8513
+ type: "state",
8514
+ clientCount: clients.size,
8515
+ waitingDepth: waitingDepth(queue),
8516
+ lastAttachAt
8517
+ });
8518
+ return;
8519
+ }
8292
8520
  if (message.type === "verdict") {
8293
8521
  handleVerdict(socket, message);
8294
8522
  }
@@ -8307,102 +8535,883 @@ async function startSessionServer(options) {
8307
8535
  dropClient(socket);
8308
8536
  });
8309
8537
  }
8310
- const server = createServer(handleConnection);
8311
- await bindSocket(server, options.socketPath);
8312
- server.on("error", reportError);
8313
- return {
8314
- socketPath: options.socketPath,
8315
- clientCount() {
8316
- return clients.size;
8317
- },
8318
- waitingDepth() {
8319
- return waitingDepth(queue);
8320
- },
8321
- onChange(handler) {
8322
- changeHandlers.push(handler);
8323
- },
8324
- close() {
8325
- return new Promise((resolve3) => {
8326
- [...connections].forEach((socket) => socket.destroy());
8327
- server.close(() => {
8328
- removeQuietly(options.socketPath);
8329
- resolve3();
8330
- });
8331
- });
8332
- }
8333
- };
8538
+ const server = createServer(handleConnection);
8539
+ await bindSocket(server, options.socketPath);
8540
+ if (options.record !== void 0) {
8541
+ writeRecord(options.socketPath, options.record);
8542
+ }
8543
+ server.on("error", reportError);
8544
+ return {
8545
+ socketPath: options.socketPath,
8546
+ clientCount() {
8547
+ return clients.size;
8548
+ },
8549
+ lastAttachAt() {
8550
+ return lastAttachAt;
8551
+ },
8552
+ waitingDepth() {
8553
+ return waitingDepth(queue);
8554
+ },
8555
+ onChange(handler) {
8556
+ changeHandlers.push(handler);
8557
+ },
8558
+ close() {
8559
+ return new Promise((resolve3) => {
8560
+ [...connections].forEach((socket) => socket.destroy());
8561
+ server.close(() => {
8562
+ removeQuietly(options.socketPath);
8563
+ removeQuietly(recordPathFor(options.socketPath));
8564
+ resolve3();
8565
+ });
8566
+ });
8567
+ }
8568
+ };
8569
+ }
8570
+
8571
+ // src/transports/session/host.ts
8572
+ import { createConnection as createConnection2 } from "node:net";
8573
+ function connect(socketPath) {
8574
+ return new Promise((resolve3, reject) => {
8575
+ const socket = createConnection2(socketPath);
8576
+ socket.setEncoding("utf-8");
8577
+ socket.once("error", reject);
8578
+ socket.once("connect", () => resolve3(socket));
8579
+ });
8580
+ }
8581
+ function emptyHandlers() {
8582
+ return { review: [], cancel: [], change: [], bufferedReviews: [], bufferedCancels: [] };
8583
+ }
8584
+ function addReviewHandler(handlers, handler) {
8585
+ handlers.review.push(handler);
8586
+ handlers.bufferedReviews.splice(0).forEach((review) => handler(review));
8587
+ }
8588
+ function addCancelHandler(handlers, handler) {
8589
+ handlers.cancel.push(handler);
8590
+ handlers.bufferedCancels.splice(0).forEach((id) => handler(id));
8591
+ }
8592
+ async function attach(socketPath, client, handlers, onState) {
8593
+ const socket = await connect(socketPath);
8594
+ const readLines = createLineReader();
8595
+ socket.on("data", (chunk) => {
8596
+ readLines(chunk).forEach((line2) => {
8597
+ const message = decodeLine(line2);
8598
+ if (message?.type === "review") {
8599
+ if (handlers.review.length === 0) {
8600
+ handlers.bufferedReviews.push(message);
8601
+ return;
8602
+ }
8603
+ handlers.review.forEach((handler) => handler(message));
8604
+ return;
8605
+ }
8606
+ if (message?.type === "cancel") {
8607
+ if (handlers.cancel.length === 0) {
8608
+ handlers.bufferedCancels.push(message.id);
8609
+ return;
8610
+ }
8611
+ handlers.cancel.forEach((handler) => handler(message.id));
8612
+ return;
8613
+ }
8614
+ if (message?.type === "state") {
8615
+ onState(message);
8616
+ }
8617
+ });
8618
+ });
8619
+ socket.write(encode({ type: "attach", client }));
8620
+ return socket;
8621
+ }
8622
+ async function ownerHost(options) {
8623
+ const handlers = emptyHandlers();
8624
+ const server = await startSessionServer({
8625
+ socketPath: options.socketPath,
8626
+ record: options.record,
8627
+ onError: options.onError
8628
+ });
8629
+ server.onChange(() => handlers.change.forEach((handler) => handler()));
8630
+ const socket = await attach(options.socketPath, options.client, handlers, () => {
8631
+ });
8632
+ return {
8633
+ socketPath: options.socketPath,
8634
+ owns: true,
8635
+ counts() {
8636
+ return { clients: server.clientCount(), waiting: server.waitingDepth() };
8637
+ },
8638
+ // The server holds the counts in process, so nothing needs asking.
8639
+ refreshCounts() {
8640
+ },
8641
+ verdict(id, questions) {
8642
+ socket.write(encode({ type: "verdict", id, questions }));
8643
+ },
8644
+ onReview(handler) {
8645
+ addReviewHandler(handlers, handler);
8646
+ },
8647
+ onCancel(handler) {
8648
+ addCancelHandler(handlers, handler);
8649
+ },
8650
+ onChange(handler) {
8651
+ handlers.change.push(handler);
8652
+ },
8653
+ async close() {
8654
+ socket.destroy();
8655
+ await server.close();
8656
+ }
8657
+ };
8658
+ }
8659
+ async function viewerHost(options) {
8660
+ const handlers = emptyHandlers();
8661
+ let remote = null;
8662
+ const socket = await attach(options.socketPath, options.client, handlers, (state) => {
8663
+ remote = state;
8664
+ handlers.change.forEach((handler) => handler());
8665
+ });
8666
+ return {
8667
+ socketPath: options.socketPath,
8668
+ owns: false,
8669
+ counts() {
8670
+ return { clients: remote?.clientCount ?? 0, waiting: remote?.waitingDepth ?? 0 };
8671
+ },
8672
+ refreshCounts() {
8673
+ socket.write(encode({ type: "status" }));
8674
+ },
8675
+ verdict(id, questions) {
8676
+ socket.write(encode({ type: "verdict", id, questions }));
8677
+ },
8678
+ onReview(handler) {
8679
+ addReviewHandler(handlers, handler);
8680
+ },
8681
+ onCancel(handler) {
8682
+ addCancelHandler(handlers, handler);
8683
+ },
8684
+ onChange(handler) {
8685
+ handlers.change.push(handler);
8686
+ },
8687
+ close() {
8688
+ socket.destroy();
8689
+ return Promise.resolve();
8690
+ }
8691
+ };
8692
+ }
8693
+
8694
+ // src/transports/session/probe.ts
8695
+ import { createConnection as createConnection3 } from "node:net";
8696
+ var STATUS_TIMEOUT_MS = 250;
8697
+ var ABANDONED_CODES = ["ECONNREFUSED", "ENOENT", "ENOTSOCK", "ENOTDIR"];
8698
+ function probeSession(socketPath) {
8699
+ return new Promise((resolve3) => {
8700
+ let settled = false;
8701
+ let connected = false;
8702
+ const settle = (probe) => {
8703
+ if (settled) {
8704
+ return;
8705
+ }
8706
+ settled = true;
8707
+ clearTimeout(timer);
8708
+ socket.destroy();
8709
+ resolve3(probe);
8710
+ };
8711
+ const timer = setTimeout(() => settle({ status: "silent" }), STATUS_TIMEOUT_MS);
8712
+ const socket = createConnection3(socketPath);
8713
+ socket.setEncoding("utf-8");
8714
+ const readLines = createLineReader();
8715
+ socket.on("error", (error) => {
8716
+ const code2 = "code" in error ? error.code : null;
8717
+ const abandoned = !connected && isString(code2) && ABANDONED_CODES.includes(code2);
8718
+ settle(abandoned ? { status: "refused" } : { status: "silent" });
8719
+ });
8720
+ socket.on("close", () => settle({ status: "silent" }));
8721
+ socket.on("data", (chunk) => {
8722
+ readLines(chunk).forEach((line2) => {
8723
+ const message = decodeLine(line2);
8724
+ if (message?.type !== "state") {
8725
+ return;
8726
+ }
8727
+ settle({ status: "answered", state: message });
8728
+ });
8729
+ });
8730
+ socket.on("connect", () => {
8731
+ connected = true;
8732
+ socket.write(encode({ type: "status" }));
8733
+ });
8734
+ });
8735
+ }
8736
+
8737
+ // src/cli/sessions/sessions.ts
8738
+ var UNKNOWN_LABEL = "unknown";
8739
+ var UNKNOWN_AGE = "-";
8740
+ var UNKNOWN_COUNT = "?";
8741
+ var SESSION_KINDS = ["session", "directory"];
8742
+ var SECOND_MS = 1e3;
8743
+ var MINUTE_MS = 60 * SECOND_MS;
8744
+ var HOUR_MS = 60 * MINUTE_MS;
8745
+ var DAY_MS = 24 * HOUR_MS;
8746
+ function isSessionKind(value) {
8747
+ return isString(value) && SESSION_KINDS.includes(value);
8748
+ }
8749
+ function isSessionRecord(value) {
8750
+ if (!isRecord(value)) {
8751
+ return false;
8752
+ }
8753
+ if (!isString(value["id"]) || !isSessionKind(value["kind"])) {
8754
+ return false;
8755
+ }
8756
+ if (!isString(value["label"]) || !isString(value["directory"])) {
8757
+ return false;
8758
+ }
8759
+ if (!isNullableString(value["branch"]) || !isNullableString(value["agentSessionId"])) {
8760
+ return false;
8761
+ }
8762
+ if (!isNullableString(value["agentKind"]) || !isString(value["createdAt"])) {
8763
+ return false;
8764
+ }
8765
+ return typeof value["pid"] === "number";
8766
+ }
8767
+ function readRecord(id) {
8768
+ const path = join7(sessionsDir(), `${id}.json`);
8769
+ if (!existsSync5(path)) {
8770
+ return null;
8771
+ }
8772
+ try {
8773
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
8774
+ return isSessionRecord(parsed) ? parsed : null;
8775
+ } catch {
8776
+ return null;
8777
+ }
8778
+ }
8779
+ function sessionIds() {
8780
+ try {
8781
+ return readdirSync(sessionsDir()).filter((name) => name.endsWith(".sock")).map((name) => basename3(name, ".sock")).sort();
8782
+ } catch {
8783
+ return [];
8784
+ }
8785
+ }
8786
+ function removeSession(id) {
8787
+ [".sock", ".json", ".url"].forEach(
8788
+ (extension) => removeQuietly(join7(sessionsDir(), `${id}${extension}`))
8789
+ );
8790
+ }
8791
+ var FLAG_EXTENSIONS = [".on", ".off"];
8792
+ var FLAG_EXPIRY_MS = 14 * DAY_MS;
8793
+ function flagFiles() {
8794
+ try {
8795
+ return readdirSync(sessionsDir()).filter((name) => FLAG_EXTENSIONS.some((extension) => name.endsWith(extension))).filter((name) => name.startsWith("s-")).sort();
8796
+ } catch {
8797
+ return [];
8798
+ }
8799
+ }
8800
+ function ageOf(path, now) {
8801
+ try {
8802
+ return now - statSync(path).mtimeMs;
8803
+ } catch {
8804
+ return null;
8805
+ }
8806
+ }
8807
+ function sweepExpiredFlags(now = Date.now()) {
8808
+ const expired = [];
8809
+ flagFiles().forEach((name) => {
8810
+ const id = name.replace(/\.(on|off)$/, "");
8811
+ if (existsSync5(join7(sessionsDir(), `${id}.sock`))) {
8812
+ return;
8813
+ }
8814
+ const path = join7(sessionsDir(), name);
8815
+ const age = ageOf(path, now);
8816
+ if (age === null || age < FLAG_EXPIRY_MS) {
8817
+ return;
8818
+ }
8819
+ removeQuietly(path);
8820
+ expired.push(name);
8821
+ });
8822
+ return expired;
8823
+ }
8824
+ function kindOf(id, record) {
8825
+ if (record !== null) {
8826
+ return record.kind;
8827
+ }
8828
+ return id.startsWith("s-") ? "session" : "directory";
8829
+ }
8830
+ function toListing(id, probe) {
8831
+ const record = readRecord(id);
8832
+ const state = probe.status === "answered" ? probe.state : null;
8833
+ return {
8834
+ id,
8835
+ kind: kindOf(id, record),
8836
+ label: record?.label ?? UNKNOWN_LABEL,
8837
+ directory: record?.directory ?? "",
8838
+ clients: state?.clientCount ?? null,
8839
+ waiting: state?.waitingDepth ?? null,
8840
+ createdAt: record?.createdAt ?? "",
8841
+ alive: true
8842
+ };
8843
+ }
8844
+ async function scan() {
8845
+ const ids = sessionIds();
8846
+ const probes = await Promise.all(
8847
+ ids.map((id) => probeSession(join7(sessionsDir(), `${id}.sock`)))
8848
+ );
8849
+ const listings = [];
8850
+ const swept = [];
8851
+ ids.forEach((id, index) => {
8852
+ const probe = probes[index];
8853
+ if (probe === void 0 || probe.status === "refused") {
8854
+ removeSession(id);
8855
+ swept.push(id);
8856
+ return;
8857
+ }
8858
+ listings.push(toListing(id, probe));
8859
+ });
8860
+ return { listings, swept, expired: sweepExpiredFlags() };
8861
+ }
8862
+ function formatAge(createdAt, now) {
8863
+ const started = Date.parse(createdAt);
8864
+ if (Number.isNaN(started)) {
8865
+ return UNKNOWN_AGE;
8866
+ }
8867
+ const elapsed = Math.max(0, now - started);
8868
+ if (elapsed >= DAY_MS) {
8869
+ return `${Math.floor(elapsed / DAY_MS)}d`;
8870
+ }
8871
+ if (elapsed >= HOUR_MS) {
8872
+ return `${Math.floor(elapsed / HOUR_MS)}h`;
8873
+ }
8874
+ if (elapsed >= MINUTE_MS) {
8875
+ return `${Math.floor(elapsed / MINUTE_MS)}m`;
8876
+ }
8877
+ return `${Math.floor(elapsed / SECOND_MS)}s`;
8878
+ }
8879
+ function formatCount(count) {
8880
+ return count === null ? UNKNOWN_COUNT : String(count);
8881
+ }
8882
+ function formatTable(listings, now) {
8883
+ const header = ["ID", "LABEL", "KIND", "WATCHERS", "QUEUED", "AGE"];
8884
+ const rows = listings.map((entry) => [
8885
+ entry.id,
8886
+ entry.label,
8887
+ entry.kind,
8888
+ formatCount(entry.clients),
8889
+ formatCount(entry.waiting),
8890
+ formatAge(entry.createdAt, now)
8891
+ ]);
8892
+ const widths = header.map(
8893
+ (name, column) => Math.max(name.length, ...rows.map((row) => row[column]?.length ?? 0))
8894
+ );
8895
+ const line2 = (cells) => cells.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd();
8896
+ return [line2(header), ...rows.map(line2)].join("\n");
8897
+ }
8898
+ function sweptLine(swept) {
8899
+ const noun = swept.length === 1 ? "session" : "sessions";
8900
+ return `swept ${swept.length} dead ${noun}`;
8901
+ }
8902
+ function expiredLine(expired) {
8903
+ const noun = expired.length === 1 ? "flag" : "flags";
8904
+ return `expired ${expired.length} stale session ${noun}`;
8905
+ }
8906
+ async function listSessions() {
8907
+ const { listings, swept, expired } = await scan();
8908
+ const table = listings.length === 0 ? "no pair-mode sessions" : formatTable(listings, Date.now());
8909
+ const notes = [
8910
+ ...swept.length === 0 ? [] : [sweptLine(swept)],
8911
+ ...expired.length === 0 ? [] : [expiredLine(expired)]
8912
+ ];
8913
+ const text = notes.length === 0 ? table : `${table}
8914
+
8915
+ ${notes.join("\n")}`;
8916
+ return { listings, swept, expired, text, exitCode: 0 };
8917
+ }
8918
+ async function sweepDeadSessions() {
8919
+ const { swept } = await scan();
8920
+ return swept;
8921
+ }
8922
+
8923
+ // src/cli/sessions/connect.ts
8924
+ var CLEAR_SCREEN = "\x1B[2J\x1B[H";
8925
+ var DOWN_KEYS = ["j", "\x1B[B"];
8926
+ var UP_KEYS = ["k", "\x1B[A"];
8927
+ var SELECT_KEYS = ["\r", "\n"];
8928
+ var QUIT_KEYS = ["q", ""];
8929
+ var HELP = "j/k move, Enter watches, q quits";
8930
+ function watchers(entry) {
8931
+ return entry.clients === null ? "?" : String(entry.clients);
8932
+ }
8933
+ function paint(io, listings, cursor) {
8934
+ const rows = listings.map((entry, index) => {
8935
+ const marker = index === cursor ? ">" : " ";
8936
+ return `${marker} ${entry.id} ${entry.label} ${watchers(entry)} watching`;
8937
+ });
8938
+ io.write(`${CLEAR_SCREEN}pair mode sessions\r
8939
+ \r
8940
+ ${rows.join("\r\n")}\r
8941
+ \r
8942
+ ${HELP}\r
8943
+ `);
8944
+ }
8945
+ function pick(io, listings) {
8946
+ return new Promise((resolve3) => {
8947
+ let cursor = 0;
8948
+ io.onKey((key) => {
8949
+ if (QUIT_KEYS.includes(key)) {
8950
+ resolve3({ selected: null, exitCode: 0 });
8951
+ return;
8952
+ }
8953
+ if (SELECT_KEYS.includes(key)) {
8954
+ resolve3({ selected: listings[cursor] ?? null, exitCode: 0 });
8955
+ return;
8956
+ }
8957
+ if (DOWN_KEYS.includes(key)) {
8958
+ cursor = Math.min(cursor + 1, listings.length - 1);
8959
+ }
8960
+ if (UP_KEYS.includes(key)) {
8961
+ cursor = Math.max(cursor - 1, 0);
8962
+ }
8963
+ paint(io, listings, cursor);
8964
+ });
8965
+ paint(io, listings, cursor);
8966
+ });
8967
+ }
8968
+ async function runConnect(io) {
8969
+ try {
8970
+ if (!io.isTty()) {
8971
+ io.write("connect needs a terminal; run pair-mode sessions instead\n");
8972
+ return { selected: null, exitCode: 1 };
8973
+ }
8974
+ const { listings } = await listSessions();
8975
+ if (listings.length === 0) {
8976
+ io.write("no pair-mode sessions\n");
8977
+ return { selected: null, exitCode: 0 };
8978
+ }
8979
+ return await pick(io, listings);
8980
+ } finally {
8981
+ io.shutdown();
8982
+ }
8983
+ }
8984
+
8985
+ // src/cli/toggle.ts
8986
+ import { spawn } from "node:child_process";
8987
+ import { existsSync as existsSync6, readFileSync as readFileSync3, unlinkSync as unlinkSync3 } from "node:fs";
8988
+ import { join as join8 } from "node:path";
8989
+ var POLL_MS = 100;
8990
+ var POLL_ATTEMPTS = 60;
8991
+ var SESSION_ENV_VARS = ["CLAUDE_CODE_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID"];
8992
+ function agentSessionId(env) {
8993
+ const found = SESSION_ENV_VARS.map((name) => env[name]).find(
8994
+ (value) => typeof value === "string" && value !== ""
8995
+ );
8996
+ return found ?? null;
8997
+ }
8998
+ function currentSessionKey() {
8999
+ return keyFor(agentSessionId(process.env) ?? void 0);
9000
+ }
9001
+ function statusProbe(directory) {
9002
+ return join8(directory, ".pair-mode-status-probe");
9003
+ }
9004
+ function sleep(ms) {
9005
+ return new Promise((done) => setTimeout(done, ms));
9006
+ }
9007
+ function readLink(directory, key) {
9008
+ const path = watchUrlPath(directory, key);
9009
+ if (!existsSync6(path)) {
9010
+ return null;
9011
+ }
9012
+ try {
9013
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
9014
+ if (isRecord(parsed) && typeof parsed["url"] === "string" && typeof parsed["pid"] === "number") {
9015
+ return { url: parsed["url"], pid: parsed["pid"] };
9016
+ }
9017
+ } catch {
9018
+ return null;
9019
+ }
9020
+ return null;
9021
+ }
9022
+ async function waitForLink(directory, key) {
9023
+ const attempts = Array.from({ length: POLL_ATTEMPTS }, (_, index) => index);
9024
+ for (const _attempt of attempts) {
9025
+ const link = readLink(directory, key);
9026
+ if (link !== null) {
9027
+ return link;
9028
+ }
9029
+ await sleep(POLL_MS);
9030
+ }
9031
+ return null;
9032
+ }
9033
+ function stopLink(directory, key) {
9034
+ const link = readLink(directory, key);
9035
+ if (link === null) {
9036
+ return false;
9037
+ }
9038
+ try {
9039
+ process.kill(link.pid, "SIGTERM");
9040
+ } catch {
9041
+ }
9042
+ try {
9043
+ unlinkSync3(watchUrlPath(directory, key));
9044
+ } catch {
9045
+ }
9046
+ return true;
9047
+ }
9048
+ function onHeadline(directory, key) {
9049
+ return key ? `pair mode ON \xB7 ${key} \xB7 ${directory}` : `pair mode ON for ${directory}`;
9050
+ }
9051
+ function pairOn(directory, key) {
9052
+ if (key) {
9053
+ enableSession(key);
9054
+ } else {
9055
+ enable(directory);
9056
+ }
9057
+ return onHeadline(directory, key);
9058
+ }
9059
+ async function pairOnWeb(directory, cliPath, key) {
9060
+ const headline = onHeadline(directory, key);
9061
+ if (key) {
9062
+ enableSession(key);
9063
+ } else {
9064
+ enable(directory);
9065
+ }
9066
+ const existing = readLink(directory, key);
9067
+ if (existing !== null) {
9068
+ return `${headline}
9069
+ ${existing.url}`;
9070
+ }
9071
+ const target = key ?? directory;
9072
+ const child = spawn(process.execPath, [cliPath, "watch", "--web", target], {
9073
+ cwd: directory,
9074
+ detached: true,
9075
+ stdio: "ignore"
9076
+ });
9077
+ child.unref();
9078
+ const link = await waitForLink(directory, key);
9079
+ if (link === null) {
9080
+ return `${headline}
9081
+ the web watcher did not report a link`;
9082
+ }
9083
+ return `${headline}
9084
+ ${link.url}`;
9085
+ }
9086
+ function pairOff(directory, key) {
9087
+ if (key) {
9088
+ optOutSession(key);
9089
+ const stopped2 = stopLink(directory, key);
9090
+ return stopped2 ? `pair mode OFF \xB7 ${key} (web watcher stopped)` : `pair mode OFF \xB7 ${key}`;
9091
+ }
9092
+ disable(directory);
9093
+ const stopped = stopLink(directory);
9094
+ return stopped ? `pair mode OFF for ${directory} (web watcher stopped)` : `pair mode OFF for ${directory}`;
9095
+ }
9096
+ async function pairToggle(directory, cliPath, web, key) {
9097
+ const on = key ? isEnabled(statusProbe(directory), key) : existsSync6(flagPath(directory));
9098
+ if (on) {
9099
+ return pairOff(directory, key);
9100
+ }
9101
+ if (web) {
9102
+ return await pairOnWeb(directory, cliPath, key);
9103
+ }
9104
+ return pairOn(directory, key);
9105
+ }
9106
+ function pairStatus(directory, key) {
9107
+ const on = isEnabled(statusProbe(directory), key);
9108
+ const link = readLink(directory, key);
9109
+ const scope = key ? `${key} \xB7 ${directory}` : directory;
9110
+ const state = `pair mode ${on ? "ON" : "OFF"} for ${scope}`;
9111
+ return link === null ? state : `${state}
9112
+ ${link.url}`;
8334
9113
  }
8335
9114
 
8336
9115
  // src/editors/micro.ts
8337
9116
  var import_yaml = __toESM(require_dist(), 1);
8338
- import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "node:fs";
8339
- import { join as join8 } from "node:path";
9117
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
9118
+ import { join as join10 } from "node:path";
8340
9119
 
8341
9120
  // src/editors/languages.ts
8342
- import { extname as extname2 } from "node:path";
8343
- var LANGS = {
9121
+ import { openSync, readSync, closeSync } from "node:fs";
9122
+ import { basename as basename4, extname as extname2 } from "node:path";
9123
+ var EXTENSIONS = {
8344
9124
  ".go": "go",
8345
9125
  ".rb": "ruby",
8346
9126
  ".rake": "ruby",
9127
+ ".gemspec": "ruby",
9128
+ ".ru": "ruby",
9129
+ ".erb": "erb",
9130
+ ".haml": "haml",
8347
9131
  ".ts": "typescript",
8348
- ".tsx": "typescript",
9132
+ ".mts": "typescript",
9133
+ ".cts": "typescript",
9134
+ ".tsx": "tsx",
8349
9135
  ".js": "javascript",
8350
- ".jsx": "javascript",
8351
9136
  ".mjs": "javascript",
8352
- ".py": "python3",
9137
+ ".cjs": "javascript",
9138
+ ".jsx": "jsx",
9139
+ ".vue": "vue",
9140
+ ".svelte": "svelte",
9141
+ ".astro": "astro",
9142
+ ".py": "python",
9143
+ ".pyi": "python",
8353
9144
  ".ex": "elixir",
8354
9145
  ".exs": "elixir",
9146
+ ".heex": "elixir",
9147
+ ".erl": "erlang",
9148
+ ".hrl": "erlang",
8355
9149
  ".rs": "rust",
8356
- ".sh": "sh",
8357
- ".bash": "sh",
8358
- ".fish": "fish",
9150
+ ".sh": "shellscript",
9151
+ ".bash": "shellscript",
9152
+ ".ksh": "shellscript",
8359
9153
  ".zsh": "zsh",
9154
+ ".fish": "fish",
9155
+ ".ps1": "powershell",
9156
+ ".psm1": "powershell",
9157
+ ".bat": "bat",
9158
+ ".cmd": "bat",
8360
9159
  ".sql": "sql",
8361
9160
  ".json": "json",
9161
+ ".jsonc": "jsonc",
9162
+ ".json5": "json5",
8362
9163
  ".tf": "terraform",
9164
+ ".tfvars": "terraform",
9165
+ ".hcl": "hcl",
8363
9166
  ".proto": "proto",
8364
- ".dockerfile": "dockerfile",
9167
+ ".dockerfile": "docker",
8365
9168
  ".toml": "toml",
8366
9169
  ".yaml": "yaml",
8367
9170
  ".yml": "yaml",
9171
+ ".ini": "ini",
9172
+ ".cfg": "ini",
9173
+ ".properties": "properties",
9174
+ ".env": "dotenv",
8368
9175
  ".md": "markdown",
9176
+ ".markdown": "markdown",
9177
+ ".mdx": "mdx",
8369
9178
  ".css": "css",
9179
+ ".scss": "scss",
9180
+ ".sass": "sass",
9181
+ ".less": "less",
9182
+ ".styl": "stylus",
8370
9183
  ".html": "html",
8371
- ".erb": "html",
9184
+ ".htm": "html",
9185
+ ".xml": "xml",
9186
+ ".xsl": "xml",
9187
+ ".svg": "xml",
8372
9188
  ".lua": "lua",
8373
9189
  ".c": "c",
8374
- ".h": "c"
9190
+ ".h": "c",
9191
+ ".cc": "cpp",
9192
+ ".cpp": "cpp",
9193
+ ".cxx": "cpp",
9194
+ ".hpp": "cpp",
9195
+ ".hh": "cpp",
9196
+ ".cs": "csharp",
9197
+ ".java": "java",
9198
+ ".kt": "kotlin",
9199
+ ".kts": "kotlin",
9200
+ ".swift": "swift",
9201
+ ".m": "objective-c",
9202
+ ".mm": "objective-c",
9203
+ ".php": "php",
9204
+ ".pl": "perl",
9205
+ ".pm": "perl",
9206
+ ".scala": "scala",
9207
+ ".sc": "scala",
9208
+ ".clj": "clojure",
9209
+ ".cljs": "clojure",
9210
+ ".cljc": "clojure",
9211
+ ".hs": "haskell",
9212
+ ".nix": "nix",
9213
+ ".r": "r",
9214
+ ".jl": "julia",
9215
+ ".zig": "zig",
9216
+ ".nim": "nim",
9217
+ ".dart": "dart",
9218
+ ".groovy": "groovy",
9219
+ ".gradle": "groovy",
9220
+ ".cr": "crystal",
9221
+ ".ml": "ocaml",
9222
+ ".mli": "ocaml",
9223
+ ".fs": "fsharp",
9224
+ ".fsx": "fsharp",
9225
+ ".elm": "elm",
9226
+ ".graphql": "graphql",
9227
+ ".gql": "graphql",
9228
+ ".prisma": "prisma",
9229
+ ".sol": "solidity",
9230
+ ".vim": "viml",
9231
+ ".diff": "diff",
9232
+ ".patch": "diff",
9233
+ ".tex": "latex",
9234
+ ".wgsl": "wgsl",
9235
+ ".glsl": "glsl",
9236
+ ".cue": "cue"
9237
+ };
9238
+ var FILENAMES = {
9239
+ gemfile: "ruby",
9240
+ rakefile: "ruby",
9241
+ capfile: "ruby",
9242
+ vagrantfile: "ruby",
9243
+ guardfile: "ruby",
9244
+ podfile: "ruby",
9245
+ fastfile: "ruby",
9246
+ appfile: "ruby",
9247
+ brewfile: "ruby",
9248
+ "config.ru": "ruby",
9249
+ dockerfile: "docker",
9250
+ containerfile: "docker",
9251
+ makefile: "make",
9252
+ gnumakefile: "make",
9253
+ "cmakelists.txt": "cmake",
9254
+ "cargo.lock": "toml",
9255
+ "gemfile.lock": "toml",
9256
+ ".babelrc": "json",
9257
+ ".eslintrc": "json",
9258
+ ".prettierrc": "json",
9259
+ ".env": "dotenv",
9260
+ ".gitconfig": "ini",
9261
+ ".editorconfig": "ini",
9262
+ "nginx.conf": "nginx",
9263
+ "pnpm-workspace.yaml": "yaml"
8375
9264
  };
8376
- function syntaxName(sourcePath) {
8377
- const ext = extname2(sourcePath).toLowerCase();
8378
- return LANGS[ext] ?? null;
8379
- }
8380
- var SHIKI_TRANSLATIONS = {
8381
- python3: "python",
8382
- sh: "shellscript",
8383
- zsh: "shellscript",
9265
+ var SHEBANGS = [
9266
+ { pattern: /^#!.*\/(env\s+)?ruby(\s|$)/, id: "ruby" },
9267
+ { pattern: /^#!.*\/(env\s+)?python[\d.]*(\s|$)/, id: "python" },
9268
+ { pattern: /^#!.*\/(env\s+)?(node|bun|deno)(\s|$)/, id: "javascript" },
9269
+ { pattern: /^#!.*\/(env\s+)?(bash|sh|ksh|dash)(\s|$)/, id: "shellscript" },
9270
+ { pattern: /^#!.*\/(env\s+)?zsh(\s|$)/, id: "zsh" },
9271
+ { pattern: /^#!.*\/(env\s+)?fish(\s|$)/, id: "fish" },
9272
+ { pattern: /^#!.*\/(env\s+)?perl(\s|$)/, id: "perl" },
9273
+ { pattern: /^#!.*\/(env\s+)?php(\s|$)/, id: "php" },
9274
+ { pattern: /^#!.*\/(env\s+)?(elixir|iex)(\s|$)/, id: "elixir" },
9275
+ { pattern: /^#!.*\/(env\s+)?lua(\s|$)/, id: "lua" },
9276
+ { pattern: /^#!.*\/(env\s+)?Rscript(\s|$)/, id: "r" },
9277
+ { pattern: /^#!.*\/(env\s+)?pwsh(\s|$)/, id: "powershell" }
9278
+ ];
9279
+ var FIRST_LINE_BYTES = 256;
9280
+ function firstLine(sourcePath) {
9281
+ let handle = null;
9282
+ try {
9283
+ handle = openSync(sourcePath, "r");
9284
+ const buffer = Buffer.alloc(FIRST_LINE_BYTES);
9285
+ const read = readSync(handle, buffer, 0, FIRST_LINE_BYTES, 0);
9286
+ const text = buffer.toString("utf-8", 0, read);
9287
+ const newline = text.indexOf("\n");
9288
+ return newline === -1 ? text : text.slice(0, newline);
9289
+ } catch {
9290
+ return null;
9291
+ } finally {
9292
+ if (handle !== null) {
9293
+ try {
9294
+ closeSync(handle);
9295
+ } catch {
9296
+ }
9297
+ }
9298
+ }
9299
+ }
9300
+ function shebangLanguage(sourcePath) {
9301
+ const line2 = firstLine(sourcePath);
9302
+ if (line2 === null || !line2.startsWith("#!")) {
9303
+ return null;
9304
+ }
9305
+ return SHEBANGS.find((rule) => rule.pattern.test(line2))?.id ?? null;
9306
+ }
9307
+ function detectLanguage(sourcePath) {
9308
+ const name = basename4(sourcePath).toLowerCase();
9309
+ const ext = extname2(name);
9310
+ if (ext !== "" && EXTENSIONS[ext] !== void 0) {
9311
+ return EXTENSIONS[ext];
9312
+ }
9313
+ if (FILENAMES[name] !== void 0) {
9314
+ return FILENAMES[name];
9315
+ }
9316
+ return shebangLanguage(sourcePath);
9317
+ }
9318
+ var MICRO_SYNTAX = {
9319
+ c: "c",
9320
+ clojure: "clojure",
9321
+ cmake: "cmake",
9322
+ crystal: "crystal",
9323
+ csharp: "csharp",
9324
+ css: "css",
9325
+ dart: "dart",
9326
+ docker: "dockerfile",
9327
+ elixir: "elixir",
9328
+ elm: "elm",
9329
+ erb: "erb",
9330
+ erlang: "erlang",
8384
9331
  fish: "fish",
9332
+ fsharp: "fsharp",
9333
+ go: "go",
9334
+ graphql: "graphql",
9335
+ groovy: "groovy",
9336
+ haml: "haml",
9337
+ haskell: "haskell",
9338
+ html: "html",
9339
+ ini: "ini",
9340
+ java: "java",
9341
+ javascript: "javascript",
9342
+ json: "json",
9343
+ julia: "julia",
9344
+ kotlin: "kotlin",
9345
+ lua: "lua",
9346
+ make: "makefile",
9347
+ markdown: "markdown",
9348
+ nginx: "nginx",
9349
+ nim: "nim",
9350
+ nix: "nix",
9351
+ "objective-c": "objc",
9352
+ ocaml: "ocaml",
9353
+ perl: "perl",
9354
+ php: "php",
8385
9355
  proto: "proto",
8386
- dockerfile: "docker",
8387
- terraform: "terraform"
9356
+ python: "python3",
9357
+ r: "r",
9358
+ ruby: "ruby",
9359
+ rust: "rust",
9360
+ scala: "scala",
9361
+ shellscript: "sh",
9362
+ sql: "sql",
9363
+ svelte: "svelte",
9364
+ swift: "swift",
9365
+ terraform: "terraform",
9366
+ toml: "toml",
9367
+ typescript: "typescript",
9368
+ vue: "vue",
9369
+ xml: "xml",
9370
+ yaml: "yaml",
9371
+ zig: "zig",
9372
+ zsh: "zsh"
9373
+ };
9374
+ var VIM_FILETYPES = {
9375
+ shellscript: "sh",
9376
+ docker: "dockerfile",
9377
+ csharp: "cs",
9378
+ "objective-c": "objc",
9379
+ bat: "dosbatch",
9380
+ viml: "vim",
9381
+ mdx: "markdown",
9382
+ jsx: "javascriptreact",
9383
+ tsx: "typescriptreact",
9384
+ dotenv: "sh",
9385
+ properties: "jproperties",
9386
+ stylus: "stylus"
8388
9387
  };
8389
9388
  function shikiLanguage(sourcePath) {
8390
- const name = syntaxName(sourcePath);
8391
- if (name === null) {
9389
+ return detectLanguage(sourcePath);
9390
+ }
9391
+ function microSyntaxName(sourcePath) {
9392
+ const id = detectLanguage(sourcePath);
9393
+ if (id === null) {
9394
+ return null;
9395
+ }
9396
+ return MICRO_SYNTAX[id] ?? null;
9397
+ }
9398
+ function vimFiletype(sourcePath) {
9399
+ const id = detectLanguage(sourcePath);
9400
+ if (id === null) {
8392
9401
  return null;
8393
9402
  }
8394
- return SHIKI_TRANSLATIONS[name] ?? name;
9403
+ return VIM_FILETYPES[id] ?? id;
8395
9404
  }
8396
9405
 
8397
9406
  // src/editors/syntax-cache.ts
8398
- import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
8399
- import { dirname as dirname6, join as join7 } from "node:path";
9407
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
9408
+ import { dirname as dirname6, join as join9 } from "node:path";
8400
9409
  import { fileURLToPath as fileURLToPath3 } from "node:url";
8401
9410
  function defaultAssetsDir() {
8402
9411
  let dir = dirname6(fileURLToPath3(import.meta.url));
8403
9412
  while (true) {
8404
- const candidate = join7(dir, "assets", "syntax");
8405
- if (existsSync5(candidate)) {
9413
+ const candidate = join9(dir, "assets", "syntax");
9414
+ if (existsSync7(candidate)) {
8406
9415
  return candidate;
8407
9416
  }
8408
9417
  const parent = dirname6(dir);
@@ -8416,11 +9425,11 @@ function syntaxSource(lang, assetsDir = defaultAssetsDir()) {
8416
9425
  if (assetsDir === null) {
8417
9426
  return null;
8418
9427
  }
8419
- const path = join7(assetsDir, `${lang}.yaml`);
8420
- if (!existsSync5(path)) {
9428
+ const path = join9(assetsDir, `${lang}.yaml`);
9429
+ if (!existsSync7(path)) {
8421
9430
  return null;
8422
9431
  }
8423
- return readFileSync2(path, "utf-8");
9432
+ return readFileSync4(path, "utf-8");
8424
9433
  }
8425
9434
 
8426
9435
  // src/editors/micro.ts
@@ -8453,7 +9462,7 @@ function bandRules() {
8453
9462
  ];
8454
9463
  }
8455
9464
  function writeColorScheme(configDir, theme2) {
8456
- const dir = join8(configDir, "colorschemes");
9465
+ const dir = join10(configDir, "colorschemes");
8457
9466
  mkdirSync4(dir, { recursive: true });
8458
9467
  const text = `include "monokai"
8459
9468
 
@@ -8461,7 +9470,7 @@ color-link pairadd "#d7ffd7,${theme2.add}"
8461
9470
  color-link pairdel "#ffd7d7,${theme2.del}"
8462
9471
  color-link pairskip "#6a6a6a,${theme2.fold}"
8463
9472
  `;
8464
- writeFileSync3(join8(dir, "pair.micro"), text, "utf-8");
9473
+ writeFileSync4(join10(dir, "pair.micro"), text, "utf-8");
8465
9474
  }
8466
9475
  function syntaxText(lang, source) {
8467
9476
  const parsed = (0, import_yaml.parse)(source);
@@ -8477,7 +9486,7 @@ function syntaxText(lang, source) {
8477
9486
  return (0, import_yaml.stringify)(file, { lineWidth: 0 });
8478
9487
  }
8479
9488
  function writeSyntax(configDir, sourcePath) {
8480
- const lang = syntaxName(sourcePath);
9489
+ const lang = microSyntaxName(sourcePath);
8481
9490
  if (lang === null) {
8482
9491
  return;
8483
9492
  }
@@ -8489,9 +9498,9 @@ function writeSyntax(configDir, sourcePath) {
8489
9498
  if (text === null) {
8490
9499
  return;
8491
9500
  }
8492
- const dir = join8(configDir, "syntax");
9501
+ const dir = join10(configDir, "syntax");
8493
9502
  mkdirSync4(dir, { recursive: true });
8494
- writeFileSync3(join8(dir, `pair-${lang}.yaml`), text, "utf-8");
9503
+ writeFileSync4(join10(dir, `pair-${lang}.yaml`), text, "utf-8");
8495
9504
  }
8496
9505
  function createMicroEditor(resolvesOnPath = defaultResolvesOnPath) {
8497
9506
  return {
@@ -8504,7 +9513,7 @@ function createMicroEditor(resolvesOnPath = defaultResolvesOnPath) {
8504
9513
  return ["# F3 moves between panes. Ctrl+W or F2 sends and closes."];
8505
9514
  },
8506
9515
  bufferSuffix(sourcePath) {
8507
- const lang = syntaxName(sourcePath);
9516
+ const lang = microSyntaxName(sourcePath);
8508
9517
  if (lang === null) {
8509
9518
  return ".diff";
8510
9519
  }
@@ -8512,13 +9521,13 @@ function createMicroEditor(resolvesOnPath = defaultResolvesOnPath) {
8512
9521
  },
8513
9522
  prepare(context) {
8514
9523
  mkdirSync4(context.configDir, { recursive: true });
8515
- writeFileSync3(
8516
- join8(context.configDir, "bindings.json"),
9524
+ writeFileSync4(
9525
+ join10(context.configDir, "bindings.json"),
8517
9526
  JSON.stringify(MICRO_BINDINGS, null, 2),
8518
9527
  "utf-8"
8519
9528
  );
8520
- writeFileSync3(
8521
- join8(context.configDir, "settings.json"),
9529
+ writeFileSync4(
9530
+ join10(context.configDir, "settings.json"),
8522
9531
  JSON.stringify(MICRO_SETTINGS, null, 2),
8523
9532
  "utf-8"
8524
9533
  );
@@ -8534,16 +9543,6 @@ function createMicroEditor(resolvesOnPath = defaultResolvesOnPath) {
8534
9543
 
8535
9544
  // src/editors/vim.ts
8536
9545
  import { extname as extname3 } from "node:path";
8537
- var VIM_FILETYPE_OVERRIDES = {
8538
- python3: "python"
8539
- };
8540
- function vimFiletype(sourcePath) {
8541
- const lang = syntaxName(sourcePath);
8542
- if (lang === null) {
8543
- return null;
8544
- }
8545
- return VIM_FILETYPE_OVERRIDES[lang] ?? lang;
8546
- }
8547
9546
  function safeThemeColor(value) {
8548
9547
  if (!isHexColor(value)) {
8549
9548
  throw new Error(`invalid theme colour for vim highlight: ${value}`);
@@ -8597,8 +9596,8 @@ function vimEditor(name, resolvesOnPath = defaultResolvesOnPath) {
8597
9596
  }
8598
9597
 
8599
9598
  // src/editors/nano.ts
8600
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
8601
- import { join as join9 } from "node:path";
9599
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
9600
+ import { join as join11 } from "node:path";
8602
9601
  function safeThemeColor2(value) {
8603
9602
  if (!isHexColor(value)) {
8604
9603
  throw new Error(`invalid theme colour for nano rcfile: ${value}`);
@@ -8610,8 +9609,8 @@ function writeNanorc(configDir, theme2) {
8610
9609
  color ,${safeThemeColor2(theme2.del)} "^\u258C\u258C-"
8611
9610
  color ,${safeThemeColor2(theme2.fold)} "^\u22EF"
8612
9611
  `;
8613
- const path = join9(configDir, "pair.nanorc");
8614
- writeFileSync4(path, text, "utf-8");
9612
+ const path = join11(configDir, "pair.nanorc");
9613
+ writeFileSync5(path, text, "utf-8");
8615
9614
  return path;
8616
9615
  }
8617
9616
  function createNanoEditor(resolvesOnPath = defaultResolvesOnPath) {
@@ -8755,17 +9754,17 @@ function createTmuxMultiplexer(spawn2 = defaultSpawn, resolvesOnPath = defaultRe
8755
9754
  }
8756
9755
 
8757
9756
  // src/multiplexers/tty.ts
8758
- import { openSync, closeSync } from "node:fs";
8759
- import { spawnSync as spawnSync3 } from "node:child_process";
8760
- var defaultOpen = () => openSync("/dev/tty", "r+");
9757
+ import { openSync as openSync2, closeSync as closeSync2 } from "node:fs";
9758
+ import { spawnSync as spawnSync4 } from "node:child_process";
9759
+ var defaultOpen = () => openSync2("/dev/tty", "r+");
8761
9760
  var defaultRunner = (command, args, fd) => {
8762
- const result = spawnSync3(command, args, { stdio: [fd, fd, fd] });
9761
+ const result = spawnSync4(command, args, { stdio: [fd, fd, fd] });
8763
9762
  const detail = result.status === 0 ? "" : String(result.error?.message ?? result.stderr ?? "");
8764
9763
  return { ok: result.status === 0, detail };
8765
9764
  };
8766
9765
  function closeQuietly(fd) {
8767
9766
  try {
8768
- closeSync(fd);
9767
+ closeSync2(fd);
8769
9768
  } catch {
8770
9769
  }
8771
9770
  }
@@ -8823,8 +9822,8 @@ function detect(preference, adapters = {}) {
8823
9822
  }
8824
9823
 
8825
9824
  // src/cli/register/register.ts
8826
- import { existsSync as existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync5, mkdirSync as mkdirSync6, copyFileSync } from "node:fs";
8827
- import { basename as basename2, dirname as dirname7, join as join10, sep as sep2 } from "node:path";
9825
+ import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, copyFileSync } from "node:fs";
9826
+ import { basename as basename5, dirname as dirname7, join as join12, sep as sep2 } from "node:path";
8828
9827
  var HOOK_TIMEOUT_SECONDS = 1800;
8829
9828
  function isHookEntry(value) {
8830
9829
  if (!isRecord(value)) {
@@ -8854,10 +9853,10 @@ function isHookGroup(value) {
8854
9853
  return value["hooks"].every(isHookEntry);
8855
9854
  }
8856
9855
  function readJsonObject(path) {
8857
- if (!existsSync6(path)) {
9856
+ if (!existsSync8(path)) {
8858
9857
  return { ok: true, root: {} };
8859
9858
  }
8860
- const text = readFileSync3(path, "utf-8");
9859
+ const text = readFileSync5(path, "utf-8");
8861
9860
  let parsed;
8862
9861
  try {
8863
9862
  parsed = JSON.parse(text);
@@ -8874,7 +9873,7 @@ function readJsonObject(path) {
8874
9873
  }
8875
9874
  var backedUpThisRun = /* @__PURE__ */ new Set();
8876
9875
  function backupIfPresent(path) {
8877
- if (!existsSync6(path)) {
9876
+ if (!existsSync8(path)) {
8878
9877
  return null;
8879
9878
  }
8880
9879
  const backupPath = `${path}.pair-backup`;
@@ -8915,7 +9914,7 @@ function hasCommand(groups, command) {
8915
9914
  });
8916
9915
  }
8917
9916
  function matchesOurCommand(command) {
8918
- const suffix = sep2 + join10("dist", basename2(command));
9917
+ const suffix = sep2 + join12("dist", basename5(command));
8919
9918
  return (entry) => entry.command === command || entry.command.endsWith(suffix);
8920
9919
  }
8921
9920
  function upsertHookGroup(groups, matcher, command, timeout) {
@@ -8947,7 +9946,7 @@ function upsertHookGroup(groups, matcher, command, timeout) {
8947
9946
  }
8948
9947
  function writeJsonObject(path, root) {
8949
9948
  mkdirSync6(dirname7(path), { recursive: true });
8950
- writeFileSync5(path, JSON.stringify(root, null, 2) + "\n", "utf-8");
9949
+ writeFileSync6(path, JSON.stringify(root, null, 2) + "\n", "utf-8");
8951
9950
  }
8952
9951
  function registerPreToolUseHook(path, matcher, command, timeout) {
8953
9952
  const read = readJsonObject(path);
@@ -8971,7 +9970,7 @@ function registerPreToolUseHook(path, matcher, command, timeout) {
8971
9970
  return { path, changed: true, backupPath };
8972
9971
  }
8973
9972
  function isPreToolUseRegistered(path, command) {
8974
- if (!existsSync6(path)) {
9973
+ if (!existsSync8(path)) {
8975
9974
  return false;
8976
9975
  }
8977
9976
  const read = readJsonObject(path);
@@ -8981,24 +9980,24 @@ function isPreToolUseRegistered(path, command) {
8981
9980
  return hasCommand(preToolUseGroups(read.root), command);
8982
9981
  }
8983
9982
  function claudeCodeSettingsPath(homeDir) {
8984
- return join10(homeDir, ".claude", "settings.json");
9983
+ return join12(homeDir, ".claude", "settings.json");
8985
9984
  }
8986
9985
  function registerClaudeCode(homeDir, installRoot2) {
8987
9986
  const path = claudeCodeSettingsPath(homeDir);
8988
- const command = join10(installRoot2, "dist", "claude-code.js");
9987
+ const command = join12(installRoot2, "dist", "claude-code.js");
8989
9988
  return registerPreToolUseHook(path, "Write|Edit|MultiEdit", command, HOOK_TIMEOUT_SECONDS);
8990
9989
  }
8991
9990
  function codexHooksPath(homeDir) {
8992
- return join10(homeDir, ".codex", "hooks.json");
9991
+ return join12(homeDir, ".codex", "hooks.json");
8993
9992
  }
8994
9993
  function registerCodex(homeDir, installRoot2) {
8995
9994
  const path = codexHooksPath(homeDir);
8996
- const command = join10(installRoot2, "dist", "codex.js");
9995
+ const command = join12(installRoot2, "dist", "codex.js");
8997
9996
  return registerPreToolUseHook(path, "apply_patch|Edit|Write", command, HOOK_TIMEOUT_SECONDS);
8998
9997
  }
8999
9998
  function findMultiEditMatchers(homeDir) {
9000
9999
  const path = codexHooksPath(homeDir);
9001
- if (!existsSync6(path)) {
10000
+ if (!existsSync8(path)) {
9002
10001
  return [];
9003
10002
  }
9004
10003
  const read = readJsonObject(path);
@@ -9058,12 +10057,12 @@ function correctMultiEditMatchers(homeDir) {
9058
10057
  return { path, changed: true, backupPath, note };
9059
10058
  }
9060
10059
  function writeFileIfChanged(path, content) {
9061
- if (existsSync6(path) && readFileSync3(path, "utf-8") === content) {
10060
+ if (existsSync8(path) && readFileSync5(path, "utf-8") === content) {
9062
10061
  return { path, changed: false, backupPath: null };
9063
10062
  }
9064
10063
  const backupPath = backupIfPresent(path);
9065
10064
  mkdirSync6(dirname7(path), { recursive: true });
9066
- writeFileSync5(path, content, "utf-8");
10065
+ writeFileSync6(path, content, "utf-8");
9067
10066
  return { path, changed: true, backupPath };
9068
10067
  }
9069
10068
  function writeReExport(path, target) {
@@ -9076,29 +10075,29 @@ export { default } from "${target}";
9076
10075
  `;
9077
10076
  }
9078
10077
  function isReExportRegistered(path, target) {
9079
- if (!existsSync6(path)) {
10078
+ if (!existsSync8(path)) {
9080
10079
  return false;
9081
10080
  }
9082
- return readFileSync3(path, "utf-8").includes(target);
10081
+ return readFileSync5(path, "utf-8").includes(target);
9083
10082
  }
9084
10083
  function opencodePluginPath(homeDir) {
9085
- return join10(homeDir, ".config", "opencode", "plugin", "pair-mode.ts");
10084
+ return join12(homeDir, ".config", "opencode", "plugin", "pair-mode.ts");
9086
10085
  }
9087
10086
  function registerOpencode(homeDir, installRoot2) {
9088
- const target = join10(installRoot2, "dist", "opencode.js");
10087
+ const target = join12(installRoot2, "dist", "opencode.js");
9089
10088
  return writeReExport(opencodePluginPath(homeDir), target);
9090
10089
  }
9091
10090
  function piExtensionPath(homeDir) {
9092
- return join10(homeDir, ".pi", "agent", "extensions", "pair-mode.ts");
10091
+ return join12(homeDir, ".pi", "agent", "extensions", "pair-mode.ts");
9093
10092
  }
9094
10093
  function registerPi(homeDir, installRoot2) {
9095
- const target = join10(installRoot2, "dist", "pi.js");
10094
+ const target = join12(installRoot2, "dist", "pi.js");
9096
10095
  return writeFileIfChanged(piExtensionPath(homeDir), piExtensionSource(target));
9097
10096
  }
9098
10097
 
9099
10098
  // src/cli/register/commands.ts
9100
- import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
9101
- import { join as join11 } from "node:path";
10099
+ import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
10100
+ import { join as join13 } from "node:path";
9102
10101
  var DESCRIPTION = "Toggle pair mode. Every proposed edit opens in the pair review pane for line annotation.";
9103
10102
  var INFER_ACTION = "Read the action from the user's message, then run `pair-mode <action>` and report the resulting state in one line.";
9104
10103
  var SPECS = {
@@ -9128,7 +10127,7 @@ var SPECS = {
9128
10127
  }
9129
10128
  };
9130
10129
  function pairCommandPath(homeDir, cli) {
9131
- return join11(homeDir, ...SPECS[cli].segments);
10130
+ return join13(homeDir, ...SPECS[cli].segments);
9132
10131
  }
9133
10132
  function pairCommandSource(cli) {
9134
10133
  const spec = SPECS[cli];
@@ -9155,10 +10154,10 @@ function pairCommandSource(cli) {
9155
10154
  }
9156
10155
  function isPairCommandRegistered(homeDir, cli) {
9157
10156
  const path = pairCommandPath(homeDir, cli);
9158
- if (!existsSync7(path)) {
10157
+ if (!existsSync9(path)) {
9159
10158
  return false;
9160
10159
  }
9161
- return readFileSync4(path, "utf-8") === pairCommandSource(cli);
10160
+ return readFileSync6(path, "utf-8") === pairCommandSource(cli);
9162
10161
  }
9163
10162
  function registerPairCommand(homeDir, cli) {
9164
10163
  return writeFileIfChanged(pairCommandPath(homeDir, cli), pairCommandSource(cli));
@@ -9194,7 +10193,7 @@ function checkMultiplexer(config, adapters) {
9194
10193
  function checkControllingTerminal(openTty) {
9195
10194
  try {
9196
10195
  const fd = openTty();
9197
- closeSync2(fd);
10196
+ closeSync3(fd);
9198
10197
  return { name: "controlling terminal", passed: true, detail: "/dev/tty opened" };
9199
10198
  } catch (error) {
9200
10199
  const name = error instanceof Error ? error.name : "Error";
@@ -9232,30 +10231,30 @@ function checkCommandOnPath(resolves) {
9232
10231
  };
9233
10232
  }
9234
10233
  function checkClis(home, root) {
9235
- const claudeCommand = join12(root, "dist", "claude-code.js");
9236
- const codexCommand = join12(root, "dist", "codex.js");
9237
- const opencodeTarget = join12(root, "dist", "opencode.js");
9238
- const piTarget = join12(root, "dist", "pi.js");
10234
+ const claudeCommand = join14(root, "dist", "claude-code.js");
10235
+ const codexCommand = join14(root, "dist", "codex.js");
10236
+ const opencodeTarget = join14(root, "dist", "opencode.js");
10237
+ const piTarget = join14(root, "dist", "pi.js");
9239
10238
  const specs = [
9240
10239
  {
9241
10240
  cli: "claude-code",
9242
10241
  registered: isPreToolUseRegistered(claudeCodeSettingsPath(home), claudeCommand),
9243
- targetExists: existsSync8(claudeCommand)
10242
+ targetExists: existsSync10(claudeCommand)
9244
10243
  },
9245
10244
  {
9246
10245
  cli: "codex",
9247
10246
  registered: isPreToolUseRegistered(codexHooksPath(home), codexCommand),
9248
- targetExists: existsSync8(codexCommand)
10247
+ targetExists: existsSync10(codexCommand)
9249
10248
  },
9250
10249
  {
9251
10250
  cli: "opencode",
9252
10251
  registered: isReExportRegistered(opencodePluginPath(home), opencodeTarget),
9253
- targetExists: existsSync8(opencodeTarget)
10252
+ targetExists: existsSync10(opencodeTarget)
9254
10253
  },
9255
10254
  {
9256
10255
  cli: "pi",
9257
10256
  registered: isReExportRegistered(piExtensionPath(home), piTarget),
9258
- targetExists: existsSync8(piTarget)
10257
+ targetExists: existsSync10(piTarget)
9259
10258
  }
9260
10259
  ];
9261
10260
  const shown = specs.filter((spec) => isReleased(spec.cli) || spec.registered);
@@ -9265,14 +10264,14 @@ function checkClis(home, root) {
9265
10264
  var SHEBANG_LINE = "#!/usr/bin/env node\n";
9266
10265
  function isExecutable(path) {
9267
10266
  try {
9268
- return (statSync(path).mode & 73) !== 0;
10267
+ return (statSync2(path).mode & 73) !== 0;
9269
10268
  } catch {
9270
10269
  return false;
9271
10270
  }
9272
10271
  }
9273
10272
  function hasShebang(path) {
9274
10273
  try {
9275
- return readFileSync5(path, "utf-8").startsWith(SHEBANG_LINE);
10274
+ return readFileSync7(path, "utf-8").startsWith(SHEBANG_LINE);
9276
10275
  } catch {
9277
10276
  return false;
9278
10277
  }
@@ -9286,10 +10285,10 @@ function checkEntryPoints(root) {
9286
10285
  "pi.js",
9287
10286
  "pair-tui.js"
9288
10287
  ];
9289
- const missing = entryPoints.filter((entry) => !existsSync8(join12(root, "dist", entry)));
10288
+ const missing = entryPoints.filter((entry) => !existsSync10(join14(root, "dist", entry)));
9290
10289
  const present = entryPoints.filter((entry) => !missing.includes(entry));
9291
- const notExecutable = present.filter((entry) => !isExecutable(join12(root, "dist", entry)));
9292
- const missingShebang = present.filter((entry) => !hasShebang(join12(root, "dist", entry)));
10290
+ const notExecutable = present.filter((entry) => !isExecutable(join14(root, "dist", entry)));
10291
+ const missingShebang = present.filter((entry) => !hasShebang(join14(root, "dist", entry)));
9293
10292
  const problems = [];
9294
10293
  if (missing.length > 0) {
9295
10294
  problems.push(`missing: ${missing.join(", ")}`);
@@ -9327,11 +10326,11 @@ function checkTrace(config) {
9327
10326
  if (!config.trace) {
9328
10327
  return null;
9329
10328
  }
9330
- const tracePath = join12(stateDir(), "trace.log");
9331
- if (!existsSync8(tracePath)) {
10329
+ const tracePath = join14(stateDir(), "trace.log");
10330
+ if (!existsSync10(tracePath)) {
9332
10331
  return { name: "trace log", passed: true, detail: "tracing is on; no log written yet" };
9333
10332
  }
9334
- const lines = readFileSync5(tracePath, "utf-8").split("\n").filter((line2) => line2 !== "");
10333
+ const lines = readFileSync7(tracePath, "utf-8").split("\n").filter((line2) => line2 !== "");
9335
10334
  const tail = lines.slice(-10);
9336
10335
  return {
9337
10336
  name: "trace log",
@@ -9339,12 +10338,14 @@ function checkTrace(config) {
9339
10338
  detail: tail.length === 0 ? "empty" : tail.join(" | ")
9340
10339
  };
9341
10340
  }
9342
- var defaultOpenTty = () => openSync2("/dev/tty", "r+");
10341
+ var defaultOpenTty = () => openSync3("/dev/tty", "r+");
10342
+ var PROBE_NAME = ".pair-mode-doctor-probe";
9343
10343
  async function checkSession(config, directory, probe) {
9344
- const path = sessionSocketPath(directory);
10344
+ const probeFile = join14(directory, PROBE_NAME);
10345
+ const path = resolveSocketPath(probeFile, currentSessionKey()) ?? sessionSocketPath(directory);
9345
10346
  const name = `session: ${path}`;
9346
10347
  const wanted = config.transport === "session";
9347
- if (!existsSync8(path)) {
10348
+ if (!existsSync10(path)) {
9348
10349
  return {
9349
10350
  name,
9350
10351
  passed: !wanted,
@@ -9352,11 +10353,12 @@ async function checkSession(config, directory, probe) {
9352
10353
  warnOnly: !wanted
9353
10354
  };
9354
10355
  }
9355
- const alive = await (probe ?? probeSocket)(path);
9356
- if (alive) {
10356
+ const result = await (probe ?? probeSession)(path);
10357
+ if (result.status !== "refused") {
9357
10358
  return { name, passed: true, detail: "a watcher is attached" };
9358
10359
  }
9359
- return { name, passed: false, detail: `stale socket, remove it with: rm ${path}` };
10360
+ removeSession(basename6(path, ".sock"));
10361
+ return { name, passed: true, detail: "removed a stale socket", warnOnly: true };
9360
10362
  }
9361
10363
  async function runDoctor(options = {}) {
9362
10364
  const home = options.homeDir ?? homedir4();
@@ -9373,7 +10375,7 @@ async function runDoctor(options = {}) {
9373
10375
  ...checkClis(home, root),
9374
10376
  checkEntryPoints(root),
9375
10377
  checkShiki(options.resolvesShiki),
9376
- await checkSession(config, options.directory ?? process.cwd(), options.probeSocket)
10378
+ await checkSession(config, options.directory ?? process.cwd(), options.probeSession)
9377
10379
  ];
9378
10380
  const traceCheck = checkTrace(config);
9379
10381
  if (traceCheck !== null) {
@@ -9593,95 +10595,6 @@ async function runSetup(options = {}) {
9593
10595
  }
9594
10596
  }
9595
10597
 
9596
- // src/cli/toggle.ts
9597
- import { spawn } from "node:child_process";
9598
- import { existsSync as existsSync9, readFileSync as readFileSync6, unlinkSync as unlinkSync3 } from "node:fs";
9599
- var POLL_MS = 100;
9600
- var POLL_ATTEMPTS = 60;
9601
- function sleep(ms) {
9602
- return new Promise((done) => setTimeout(done, ms));
9603
- }
9604
- function readLink(directory) {
9605
- const path = sessionUrlPath(directory);
9606
- if (!existsSync9(path)) {
9607
- return null;
9608
- }
9609
- try {
9610
- const parsed = JSON.parse(readFileSync6(path, "utf-8"));
9611
- if (isRecord(parsed) && typeof parsed["url"] === "string" && typeof parsed["pid"] === "number") {
9612
- return { url: parsed["url"], pid: parsed["pid"] };
9613
- }
9614
- } catch {
9615
- return null;
9616
- }
9617
- return null;
9618
- }
9619
- async function waitForLink(directory) {
9620
- const attempts = Array.from({ length: POLL_ATTEMPTS }, (_, index) => index);
9621
- for (const _attempt of attempts) {
9622
- const link = readLink(directory);
9623
- if (link !== null) {
9624
- return link;
9625
- }
9626
- await sleep(POLL_MS);
9627
- }
9628
- return null;
9629
- }
9630
- function stopLink(directory) {
9631
- const link = readLink(directory);
9632
- if (link === null) {
9633
- return false;
9634
- }
9635
- try {
9636
- process.kill(link.pid, "SIGTERM");
9637
- } catch {
9638
- }
9639
- try {
9640
- unlinkSync3(sessionUrlPath(directory));
9641
- } catch {
9642
- }
9643
- return true;
9644
- }
9645
- function pairOn(directory) {
9646
- enable(directory);
9647
- return `pair mode ON for ${directory}`;
9648
- }
9649
- async function pairOnWeb(directory, cliPath) {
9650
- enable(directory);
9651
- const existing = readLink(directory);
9652
- if (existing !== null) {
9653
- return `pair mode ON for ${directory}
9654
- ${existing.url}`;
9655
- }
9656
- const child = spawn(process.execPath, [cliPath, "watch", "--web", directory], {
9657
- detached: true,
9658
- stdio: "ignore"
9659
- });
9660
- child.unref();
9661
- const link = await waitForLink(directory);
9662
- if (link === null) {
9663
- return `pair mode ON for ${directory}
9664
- the web watcher did not report a link`;
9665
- }
9666
- return `pair mode ON for ${directory}
9667
- ${link.url}`;
9668
- }
9669
- function pairOff(directory) {
9670
- disable(directory);
9671
- const stopped = stopLink(directory);
9672
- return stopped ? `pair mode OFF for ${directory} (web watcher stopped)` : `pair mode OFF for ${directory}`;
9673
- }
9674
- function pairStatus(directory) {
9675
- const on = existsSync9(flagPath(directory));
9676
- const link = readLink(directory);
9677
- const state = `pair mode ${on ? "ON" : "OFF"} for ${directory}`;
9678
- return link === null ? state : `${state}
9679
- ${link.url}`;
9680
- }
9681
-
9682
- // src/cli/watch/watch.ts
9683
- import { createConnection as createConnection2 } from "node:net";
9684
-
9685
10598
  // node_modules/.pnpm/diff@9.0.0/node_modules/diff/libesm/diff/base.js
9686
10599
  var Diff = class {
9687
10600
  diff(oldStr, newStr, options = {}) {
@@ -10167,7 +11080,7 @@ function diffArrays(oldArr, newArr, options) {
10167
11080
  }
10168
11081
 
10169
11082
  // src/tui/paint/layout.ts
10170
- import { basename as basename3 } from "node:path";
11083
+ import { basename as basename7 } from "node:path";
10171
11084
 
10172
11085
  // src/core/diff/diff.ts
10173
11086
  function mergeChangedPair(first, second, i, j) {
@@ -10227,7 +11140,7 @@ var TAB_WIDTH = 8;
10227
11140
  var CONTROL_BYTE_CEILING = 32;
10228
11141
  var CONTROL_BYTE_PLACEHOLDER = "?";
10229
11142
  function sanitizeLine(text) {
10230
- const scan = text.split("").reduce(
11143
+ const scan2 = text.split("").reduce(
10231
11144
  (state, char) => {
10232
11145
  if (char === " ") {
10233
11146
  const width = TAB_WIDTH - state.column % TAB_WIDTH;
@@ -10243,7 +11156,7 @@ function sanitizeLine(text) {
10243
11156
  },
10244
11157
  { output: "", column: 0 }
10245
11158
  );
10246
- return scan.output;
11159
+ return scan2.output;
10247
11160
  }
10248
11161
  function buildRows(before, after) {
10249
11162
  return opcodes(before, after).flatMap((opcode) => {
@@ -10890,7 +11803,7 @@ function paintFoldRow(fold2, width, truecolor, cursorRow) {
10890
11803
  return fg(color, truecolor) + " ".repeat(leftPadding) + label + " ".repeat(rightPadding) + RESET;
10891
11804
  }
10892
11805
  function paintHeader(path, addCount, delCount, width, truecolor) {
10893
- const prefix = `pair mode\u2502${basename3(path)}\u2502`;
11806
+ const prefix = `pair mode\u2502${basename7(path)}\u2502`;
10894
11807
  const addText = `+${addCount}`;
10895
11808
  const delText = `-${delCount}`;
10896
11809
  const used = prefix.length + addText.length + HEADER_COUNT_GAP_WIDTH + delText.length;
@@ -11412,7 +12325,7 @@ function indentWidth(line2) {
11412
12325
  }
11413
12326
  function changedSpans(before, after) {
11414
12327
  const chunks = diffWordsWithSpace(before, after);
11415
- const scan = chunks.reduce(
12328
+ const scan2 = chunks.reduce(
11416
12329
  (state, chunk) => {
11417
12330
  const length = chunk.value.length;
11418
12331
  if (chunk.removed === true) {
@@ -11438,7 +12351,7 @@ function changedSpans(before, after) {
11438
12351
  },
11439
12352
  { left: [], right: [], sharedLength: 0, leftCursor: 0, rightCursor: 0 }
11440
12353
  );
11441
- const { left, right, sharedLength } = scan;
12354
+ const { left, right, sharedLength } = scan2;
11442
12355
  const indent = Math.min(indentWidth(before), indentWidth(after));
11443
12356
  const longer = Math.max(before.length, after.length) - indent;
11444
12357
  const sharedFraction = longer <= 0 ? 1 : (sharedLength - indent) / longer;
@@ -11461,7 +12374,7 @@ function decideLayout(options) {
11461
12374
  function layoutStatusMessage(options) {
11462
12375
  return decideLayout(options).overrideReason;
11463
12376
  }
11464
- function paint(options) {
12377
+ function paint2(options) {
11465
12378
  const { layout } = decideLayout(options);
11466
12379
  return layout === "unified" ? paintUnified(options) : paintSplit(options);
11467
12380
  }
@@ -11671,7 +12584,7 @@ function splitInput(chunk) {
11671
12584
  }
11672
12585
 
11673
12586
  // src/tui/notes/notes.ts
11674
- import { writeFileSync as writeFileSync6 } from "node:fs";
12587
+ import { writeFileSync as writeFileSync7 } from "node:fs";
11675
12588
  function rangeOf(selection) {
11676
12589
  const reversed = selection.anchorRow > selection.headRow || selection.anchorRow === selection.headRow && selection.anchorColumn > selection.headColumn;
11677
12590
  if (!reversed) {
@@ -11742,7 +12655,7 @@ function toQuestions(notes) {
11742
12655
  }
11743
12656
  function writeResult(path, notes) {
11744
12657
  try {
11745
- writeFileSync6(path, JSON.stringify({ questions: toQuestions(notes) }, null, 2), "utf-8");
12658
+ writeFileSync7(path, JSON.stringify({ questions: toQuestions(notes) }, null, 2), "utf-8");
11746
12659
  } catch {
11747
12660
  return;
11748
12661
  }
@@ -12039,7 +12952,7 @@ function runTui(options, io, abort) {
12039
12952
  let finished = false;
12040
12953
  const repaint = () => {
12041
12954
  const { width, height } = io.size();
12042
- const result = paint({
12955
+ const result = paint2({
12043
12956
  model: state.model,
12044
12957
  width,
12045
12958
  height,
@@ -12200,6 +13113,9 @@ function createWatchIo() {
12200
13113
  });
12201
13114
  process.stdout.on("resize", () => resizeHandler?.());
12202
13115
  return {
13116
+ isTty() {
13117
+ return process.stdin.isTTY === true;
13118
+ },
12203
13119
  onKey(handler) {
12204
13120
  keyHandler = handler;
12205
13121
  },
@@ -12232,23 +13148,15 @@ function createWatchIo() {
12232
13148
  }
12233
13149
 
12234
13150
  // src/cli/watch/watch.ts
12235
- var CLEAR_SCREEN = "\x1B[2J\x1B[H";
12236
- var QUIT_KEYS = ["q", "", ""];
13151
+ var CLEAR_SCREEN2 = "\x1B[2J\x1B[H";
13152
+ var QUIT_KEYS2 = ["q", "", ""];
12237
13153
  function reportErrors(errors) {
12238
13154
  errors.forEach((error) => process.stderr.write(`pair-mode: ${error.message}
12239
13155
  `));
12240
13156
  }
12241
13157
  function paintIdle(io, status, truecolor) {
12242
13158
  const { width } = io.size();
12243
- io.write(CLEAR_SCREEN + renderIdle(status, width, truecolor).join("\r\n") + "\r\n");
12244
- }
12245
- function connectSelf(socketPath) {
12246
- return new Promise((resolve3, reject) => {
12247
- const socket = createConnection2(socketPath);
12248
- socket.setEncoding("utf-8");
12249
- socket.once("error", reject);
12250
- socket.once("connect", () => resolve3(socket));
12251
- });
13159
+ io.write(CLEAR_SCREEN2 + renderIdle(status, width, truecolor).join("\r\n") + "\r\n");
12252
13160
  }
12253
13161
  async function optionsFor(review, config, io, resultFile) {
12254
13162
  const truecolor = supportsTruecolor(process.env);
@@ -12275,24 +13183,29 @@ async function optionsFor(review, config, io, resultFile) {
12275
13183
  };
12276
13184
  }
12277
13185
  async function runWatch(options, config) {
12278
- const socketPath = options.socketPath ?? sessionSocketPath(options.directory);
13186
+ const socketPath = watchSocketPath(options.directory, options.sessionKey, options.socketPath);
12279
13187
  let errors = [];
12280
- const server = await startSessionServer({
13188
+ const owns = (await probeSession(socketPath)).status === "refused";
13189
+ const host = owns ? await ownerHost({
12281
13190
  socketPath,
13191
+ client: "tui",
13192
+ record: buildSessionRecord(options, socketPath),
12282
13193
  onError: (error) => {
12283
13194
  errors = [...errors, error];
12284
13195
  }
12285
- });
13196
+ }) : await viewerHost({ socketPath, client: "tui" });
13197
+ await sweepDeadSessions();
12286
13198
  const truecolor = supportsTruecolor(process.env);
12287
13199
  const io = options.io ?? createWatchIo();
12288
- const status = () => ({
12289
- directory: options.directory,
12290
- socketPath,
12291
- clients: server.clientCount(),
12292
- waiting: server.waitingDepth()
12293
- });
12294
- const client = await connectSelf(socketPath);
12295
- const readLines = createLineReader();
13200
+ const status = () => {
13201
+ const counts = host.counts();
13202
+ return {
13203
+ directory: options.directory,
13204
+ socketPath,
13205
+ clients: counts.clients,
13206
+ waiting: counts.waiting
13207
+ };
13208
+ };
12296
13209
  const pending = [];
12297
13210
  const cancelled = /* @__PURE__ */ new Set();
12298
13211
  const aborts = /* @__PURE__ */ new Map();
@@ -12306,37 +13219,31 @@ async function runWatch(options, config) {
12306
13219
  };
12307
13220
  const listenIdle = () => {
12308
13221
  io.onKey((chunk) => {
12309
- if (QUIT_KEYS.includes(chunk)) {
13222
+ if (QUIT_KEYS2.includes(chunk)) {
12310
13223
  quitting = true;
12311
13224
  nudge();
12312
13225
  }
12313
13226
  });
12314
13227
  io.onResize(() => paintIdle(io, status(), truecolor));
12315
13228
  };
12316
- client.on("data", (chunk) => {
12317
- readLines(chunk).forEach((line2) => {
12318
- const message = decodeLine(line2);
12319
- if (message?.type === "review") {
12320
- pending.push(message);
12321
- nudge();
12322
- return;
12323
- }
12324
- if (message?.type === "cancel") {
12325
- cancelled.add(message.id);
12326
- aborts.get(message.id)?.abort();
12327
- nudge();
12328
- }
12329
- });
13229
+ host.onReview((review) => {
13230
+ pending.push(review);
13231
+ nudge();
13232
+ });
13233
+ host.onCancel((id) => {
13234
+ cancelled.add(id);
13235
+ aborts.get(id)?.abort();
13236
+ nudge();
12330
13237
  });
12331
- server.onChange(() => {
13238
+ host.onChange(() => {
12332
13239
  if (!busy && !quitting) {
12333
13240
  paintIdle(io, status(), truecolor);
12334
13241
  }
12335
13242
  });
12336
- client.write(encode({ type: "attach", client: "tui" }));
12337
13243
  while (!quitting) {
12338
13244
  const review = pending.shift();
12339
13245
  if (review === void 0) {
13246
+ host.refreshCounts();
12340
13247
  listenIdle();
12341
13248
  paintIdle(io, status(), truecolor);
12342
13249
  await new Promise((resolve3) => {
@@ -12352,8 +13259,7 @@ async function runWatch(options, config) {
12352
13259
  const tuiOptions = await optionsFor(review, config, io, resultFile);
12353
13260
  const result = await runTui(tuiOptions, io, abort.signal);
12354
13261
  if (!cancelled.has(review.id)) {
12355
- const questions = result.quit === "send" ? result.questions : [];
12356
- client.write(encode({ type: "verdict", id: review.id, questions }));
13262
+ host.verdict(review.id, result.quit === "send" ? result.questions : []);
12357
13263
  }
12358
13264
  } finally {
12359
13265
  aborts.delete(review.id);
@@ -12362,10 +13268,9 @@ async function runWatch(options, config) {
12362
13268
  busy = false;
12363
13269
  }
12364
13270
  }
12365
- io.write(CLEAR_SCREEN);
13271
+ io.write(CLEAR_SCREEN2);
12366
13272
  io.shutdown();
12367
- client.destroy();
12368
- await server.close();
13273
+ await host.close();
12369
13274
  reportErrors(errors);
12370
13275
  return 0;
12371
13276
  }
@@ -12601,7 +13506,7 @@ function runConfig(args, path) {
12601
13506
  // src/web/server.ts
12602
13507
  import { createServer as createServer2 } from "node:http";
12603
13508
  import { randomBytes as randomBytes4 } from "node:crypto";
12604
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
13509
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
12605
13510
  import { fileURLToPath as fileURLToPath4 } from "node:url";
12606
13511
 
12607
13512
  // src/web/client/bundle.ts
@@ -13668,7 +14573,7 @@ var MAX_BODY_BYTES = 1e6;
13668
14573
  function readAsset(name) {
13669
14574
  const bundled = fileURLToPath4(new URL(`../assets/${name}`, import.meta.url));
13670
14575
  const source = fileURLToPath4(new URL(`../../assets/${name}`, import.meta.url));
13671
- return readFileSync7(existsSync10(bundled) ? bundled : source);
14576
+ return readFileSync8(existsSync11(bundled) ? bundled : source);
13672
14577
  }
13673
14578
  var IMAGES = {
13674
14579
  "favicon.png": readAsset("favicon.png"),
@@ -13751,7 +14656,7 @@ function startWebServer(options) {
13751
14656
  const token = options.token ?? defaultToken();
13752
14657
  const base = `/r/${token}`;
13753
14658
  const viewers = /* @__PURE__ */ new Set();
13754
- let current = null;
14659
+ let pending = [];
13755
14660
  function sendEvent(response, event, data) {
13756
14661
  response.write(`event: ${event}
13757
14662
  data: ${data}
@@ -13768,14 +14673,30 @@ data: ${data}
13768
14673
  response.write(": open\n\n");
13769
14674
  viewers.add(response);
13770
14675
  response.on("close", () => viewers.delete(response));
13771
- if (current !== null) {
13772
- sendEvent(response, "review", JSON.stringify(current));
14676
+ const open = pending[0];
14677
+ if (!open) {
14678
+ return;
13773
14679
  }
14680
+ sendEvent(response, "review", JSON.stringify(open));
13774
14681
  }
13775
14682
  function broadcastCancel(id) {
13776
14683
  const data = JSON.stringify({ id });
13777
14684
  viewers.forEach((viewer) => sendEvent(viewer, "cancel", data));
13778
14685
  }
14686
+ function broadcastReview(review) {
14687
+ const data = JSON.stringify(review);
14688
+ viewers.forEach((viewer) => sendEvent(viewer, "review", data));
14689
+ }
14690
+ function retire(id) {
14691
+ const wasOpen = pending[0]?.id === id;
14692
+ pending = pending.filter((review) => review.id !== id);
14693
+ broadcastCancel(id);
14694
+ const next = pending[0];
14695
+ if (!wasOpen || !next) {
14696
+ return;
14697
+ }
14698
+ broadcastReview(next);
14699
+ }
13779
14700
  async function handleVerdict(request, response) {
13780
14701
  const result = await readBody(request);
13781
14702
  if (result.kind === "too-large") {
@@ -13791,13 +14712,12 @@ data: ${data}
13791
14712
  response.writeHead(BAD_REQUEST).end();
13792
14713
  return;
13793
14714
  }
13794
- const answered = current;
13795
- if (answered === null || answered.id !== verdict.id) {
14715
+ const answered = pending[0];
14716
+ if (!answered || answered.id !== verdict.id) {
13796
14717
  response.writeHead(CONFLICT, { "content-type": "application/json" }).end("{}");
13797
14718
  return;
13798
14719
  }
13799
- current = null;
13800
- broadcastCancel(verdict.id);
14720
+ retire(verdict.id);
13801
14721
  options.onVerdict(verdict.id, webNotesToQuestions(answered, verdict.notes));
13802
14722
  response.writeHead(OK, { "content-type": "application/json" }).end("{}");
13803
14723
  }
@@ -13809,7 +14729,7 @@ data: ${data}
13809
14729
  return;
13810
14730
  }
13811
14731
  const image = url.startsWith(`${base}/`) ? IMAGES[url.slice(base.length + 1)] : void 0;
13812
- if (image !== void 0 && request.method === "GET") {
14732
+ if (image && request.method === "GET") {
13813
14733
  response.writeHead(OK, { "content-type": "image/png" });
13814
14734
  response.end(image);
13815
14735
  return;
@@ -13838,21 +14758,20 @@ data: ${data}
13838
14758
  return viewers.size;
13839
14759
  },
13840
14760
  offer(review) {
13841
- current = review;
13842
- const data = JSON.stringify(review);
13843
- viewers.forEach((viewer) => sendEvent(viewer, "review", data));
14761
+ pending = [...pending, review];
14762
+ if (pending.length === 1) {
14763
+ broadcastReview(review);
14764
+ }
13844
14765
  },
13845
14766
  // A withdrawal for an older review must leave the one now open alone.
13846
14767
  withdraw(id) {
13847
- if (current?.id === id) {
13848
- current = null;
13849
- }
13850
- broadcastCancel(id);
14768
+ retire(id);
13851
14769
  },
13852
14770
  close() {
13853
14771
  return new Promise((done) => {
13854
14772
  viewers.forEach((viewer) => viewer.end());
13855
14773
  viewers.clear();
14774
+ pending = [];
13856
14775
  server.close(() => done());
13857
14776
  });
13858
14777
  }
@@ -13862,8 +14781,7 @@ data: ${data}
13862
14781
  }
13863
14782
 
13864
14783
  // src/web/watch.ts
13865
- import { createConnection as createConnection3 } from "node:net";
13866
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
14784
+ import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
13867
14785
  import { dirname as dirname8 } from "node:path";
13868
14786
 
13869
14787
  // src/web/review.ts
@@ -13892,73 +14810,105 @@ async function toWebReview(review, config) {
13892
14810
  }
13893
14811
 
13894
14812
  // src/web/watch.ts
14813
+ var OWNER_ONLY_DIR3 = 448;
14814
+ var OWNER_ONLY_FILE2 = 384;
13895
14815
  function publishUrl(path, url) {
13896
- mkdirSync7(dirname8(path), { recursive: true });
13897
- writeFileSync7(path, JSON.stringify({ url, pid: process.pid }) + "\n", "utf-8");
13898
- }
13899
- function connectSelf2(socketPath) {
13900
- return new Promise((resolve3, reject) => {
13901
- const socket = createConnection3(socketPath);
13902
- socket.setEncoding("utf-8");
13903
- socket.once("error", reject);
13904
- socket.once("connect", () => resolve3(socket));
14816
+ mkdirSync7(dirname8(path), { recursive: true, mode: OWNER_ONLY_DIR3 });
14817
+ writeFileSync8(path, JSON.stringify({ url, pid: process.pid }) + "\n", {
14818
+ encoding: "utf-8",
14819
+ mode: OWNER_ONLY_FILE2
13905
14820
  });
13906
14821
  }
14822
+ function linkPid(path) {
14823
+ try {
14824
+ const parsed = JSON.parse(readFileSync9(path, "utf-8"));
14825
+ return isRecord(parsed) && typeof parsed["pid"] === "number" ? parsed["pid"] : null;
14826
+ } catch {
14827
+ return null;
14828
+ }
14829
+ }
13907
14830
  async function startWebWatch(options, config) {
13908
- const socketPath = options.socketPath ?? sessionSocketPath(options.directory);
13909
- const session = await startSessionServer({ socketPath });
13910
- const client = await connectSelf2(socketPath);
14831
+ const socketPath = watchSocketPath(options.directory, options.sessionKey, options.socketPath);
14832
+ const owns = (await probeSession(socketPath)).status === "refused";
14833
+ const host = owns ? await ownerHost({
14834
+ socketPath,
14835
+ client: "web",
14836
+ record: buildSessionRecord(options, socketPath)
14837
+ }) : await viewerHost({ socketPath, client: "web" });
14838
+ const ownedIds = /* @__PURE__ */ new Set();
13911
14839
  const web = await startWebServer({
13912
14840
  port: options.port,
13913
14841
  token: options.token,
13914
14842
  layout: config.layout,
13915
14843
  onVerdict(id, questions) {
13916
- client.write(encode({ type: "verdict", id, questions }));
14844
+ ownedIds.delete(id);
14845
+ host.verdict(id, questions);
13917
14846
  }
13918
14847
  });
13919
- const readLines = createLineReader();
13920
- let ownedId = null;
13921
- client.on("data", (chunk) => {
13922
- readLines(chunk).forEach((line2) => {
13923
- const message = decodeLine(line2);
13924
- if (message?.type === "review") {
13925
- const id = message.id;
13926
- ownedId = id;
13927
- void toWebReview(message, config).then((review) => {
13928
- if (ownedId === id) {
13929
- web.offer(review);
13930
- }
13931
- }).catch(() => {
13932
- if (ownedId === id) {
13933
- client.write(encode({ type: "verdict", id, questions: [] }));
13934
- }
13935
- });
13936
- return;
14848
+ host.onReview((message) => {
14849
+ const id = message.id;
14850
+ ownedIds.add(id);
14851
+ void toWebReview(message, config).then((review) => {
14852
+ if (ownedIds.has(id)) {
14853
+ web.offer(review);
13937
14854
  }
13938
- if (message?.type === "cancel") {
13939
- if (ownedId === message.id) {
13940
- ownedId = null;
13941
- }
13942
- web.withdraw(message.id);
14855
+ }).catch(() => {
14856
+ if (ownedIds.delete(id)) {
14857
+ host.verdict(id, []);
13943
14858
  }
13944
14859
  });
13945
14860
  });
13946
- const urlPath = sessionUrlPath(options.directory);
13947
- publishUrl(urlPath, web.url);
13948
- client.write(encode({ type: "attach", client: "web" }));
14861
+ host.onCancel((id) => {
14862
+ ownedIds.delete(id);
14863
+ web.withdraw(id);
14864
+ });
14865
+ const urlPath = watchUrlPath(options.directory, options.sessionKey);
14866
+ const published = owns || !existsSync12(urlPath);
14867
+ if (published) {
14868
+ publishUrl(urlPath, web.url);
14869
+ }
13949
14870
  return {
13950
14871
  url: web.url,
13951
14872
  socketPath,
13952
14873
  port: web.port,
14874
+ owns,
13953
14875
  async close() {
13954
- removeQuietly(urlPath);
13955
- client.destroy();
14876
+ if (published && linkPid(urlPath) === process.pid) {
14877
+ removeQuietly(urlPath);
14878
+ }
13956
14879
  await web.close();
13957
- await session.close();
14880
+ await host.close();
13958
14881
  }
13959
14882
  };
13960
14883
  }
13961
14884
 
14885
+ // src/cli/watch-target.ts
14886
+ function usesWebWatcher(target, webEnabled) {
14887
+ return target.web || webEnabled;
14888
+ }
14889
+ async function watchSession(target) {
14890
+ const { directory, sessionKey: sessionKey2, socketPath } = target;
14891
+ const { config, errors } = loadConfig();
14892
+ errors.forEach((error) => console.error(`config ${error.path}: ${error.message}`));
14893
+ if (!usesWebWatcher(target, config.web.enabled)) {
14894
+ return await runWatch({ directory, sessionKey: sessionKey2, socketPath }, config);
14895
+ }
14896
+ const watcher = await startWebWatch(
14897
+ { directory, sessionKey: sessionKey2, socketPath, port: config.web.port },
14898
+ config
14899
+ );
14900
+ console.log(`pair mode is watching ${directory}`);
14901
+ console.log(watcher.url);
14902
+ await new Promise((done) => {
14903
+ const stop = () => {
14904
+ void watcher.close().then(done);
14905
+ };
14906
+ process.once("SIGINT", stop);
14907
+ process.once("SIGTERM", stop);
14908
+ });
14909
+ return 0;
14910
+ }
14911
+
13962
14912
  // src/cli/index.ts
13963
14913
  var USAGE = `pair-mode <command> [directory]
13964
14914
 
@@ -13971,16 +14921,21 @@ Commands:
13971
14921
  on [dir] turn pair mode on for a directory (default: cwd)
13972
14922
  on --web [dir] turn pair mode on and serve the review in a browser
13973
14923
  off [dir] turn pair mode off for a directory (default: cwd)
14924
+ toggle [dir] flip pair mode for a directory (default: cwd)
14925
+ toggle --web [dir] flip pair mode, and serve the review in a browser when it turns on
13974
14926
  status [dir] report pair mode status for a directory (default: cwd)
13975
14927
  watch [dir] review edits in this terminal (default: cwd)
13976
14928
  watch --web [dir] serve the review in a browser and print the link
14929
+ watch <id> review edits for one session (see: pair-mode sessions)
14930
+ sessions list every live pair mode session
14931
+ connect pick a session from a list and watch it
13977
14932
  --version print the installed version
13978
14933
  --help print this message
13979
14934
  `;
13980
14935
  function readVersion() {
13981
- const pkgPath = join13(installRoot(), "package.json");
14936
+ const pkgPath = join15(installRoot(), "package.json");
13982
14937
  try {
13983
- const raw = JSON.parse(readFileSync8(pkgPath, "utf-8"));
14938
+ const raw = JSON.parse(readFileSync10(pkgPath, "utf-8"));
13984
14939
  if (isRecord(raw) && typeof raw["version"] === "string") {
13985
14940
  return raw["version"];
13986
14941
  }
@@ -14000,6 +14955,33 @@ function parseDirectoryArgs(args, allowedFlags) {
14000
14955
  unknownFlag: flags.find((flag) => !allowedFlags.includes(flag)) ?? null
14001
14956
  };
14002
14957
  }
14958
+ var SESSION_KEY_PATTERN = /^s-[0-9a-f]{8}$/;
14959
+ var SESSION_KEY_PREFIX = "s-";
14960
+ function parseWatchArgs(args) {
14961
+ const flags = args.filter(isFlag);
14962
+ const target = args.find((entry) => !isFlag(entry));
14963
+ const looksLikeKey = target !== void 0 && target.startsWith(SESSION_KEY_PREFIX);
14964
+ const isKey = looksLikeKey && SESSION_KEY_PATTERN.test(target);
14965
+ return {
14966
+ sessionKey: isKey ? target : void 0,
14967
+ malformedKey: looksLikeKey && !isKey ? target : null,
14968
+ directory: isKey ? process.cwd() : resolve2(target ?? process.cwd()),
14969
+ web: flags.includes("--web"),
14970
+ unknownFlag: flags.find((flag) => flag !== "--web") ?? null
14971
+ };
14972
+ }
14973
+ function reportExtraArgs(command, args) {
14974
+ const extra = args[0];
14975
+ if (extra === void 0) {
14976
+ return null;
14977
+ }
14978
+ if (isFlag(extra)) {
14979
+ return reportUnknownFlag(command, extra);
14980
+ }
14981
+ console.error(`unexpected argument for ${command}: ${extra}`);
14982
+ console.error(USAGE);
14983
+ return 1;
14984
+ }
14003
14985
  function reportUnknownFlag(command, flag) {
14004
14986
  console.error(`unknown option for ${command}: ${flag}`);
14005
14987
  console.error(USAGE);
@@ -14034,11 +15016,13 @@ async function main() {
14034
15016
  if (parsed.unknownFlag !== null) {
14035
15017
  return reportUnknownFlag(command, parsed.unknownFlag);
14036
15018
  }
15019
+ await sweepDeadSessions();
15020
+ const key = currentSessionKey();
14037
15021
  if (parsed.web) {
14038
- console.log(await pairOnWeb(parsed.directory, process.argv[1] ?? ""));
15022
+ console.log(await pairOnWeb(parsed.directory, process.argv[1] ?? "", key));
14039
15023
  return 0;
14040
15024
  }
14041
- console.log(pairOn(parsed.directory));
15025
+ console.log(pairOn(parsed.directory, key));
14042
15026
  return 0;
14043
15027
  }
14044
15028
  if (command === "off") {
@@ -14046,7 +15030,17 @@ async function main() {
14046
15030
  if (parsed.unknownFlag !== null) {
14047
15031
  return reportUnknownFlag(command, parsed.unknownFlag);
14048
15032
  }
14049
- console.log(pairOff(parsed.directory));
15033
+ console.log(pairOff(parsed.directory, currentSessionKey()));
15034
+ return 0;
15035
+ }
15036
+ if (command === "toggle") {
15037
+ const parsed = parseDirectoryArgs(process.argv.slice(3), ["--web"]);
15038
+ if (parsed.unknownFlag !== null) {
15039
+ return reportUnknownFlag(command, parsed.unknownFlag);
15040
+ }
15041
+ console.log(
15042
+ await pairToggle(parsed.directory, process.argv[1] ?? "", parsed.web, currentSessionKey())
15043
+ );
14050
15044
  return 0;
14051
15045
  }
14052
15046
  if (command === "status") {
@@ -14054,32 +15048,52 @@ async function main() {
14054
15048
  if (parsed.unknownFlag !== null) {
14055
15049
  return reportUnknownFlag(command, parsed.unknownFlag);
14056
15050
  }
14057
- console.log(pairStatus(parsed.directory));
15051
+ console.log(pairStatus(parsed.directory, currentSessionKey()));
14058
15052
  return 0;
14059
15053
  }
14060
15054
  if (command === "watch") {
14061
- const parsed = parseDirectoryArgs(process.argv.slice(3), ["--web"]);
15055
+ const parsed = parseWatchArgs(process.argv.slice(3));
14062
15056
  if (parsed.unknownFlag !== null) {
14063
15057
  return reportUnknownFlag(command, parsed.unknownFlag);
14064
15058
  }
14065
- const wantsWeb = parsed.web;
14066
- const directory = parsed.directory;
14067
- const { config, errors } = loadConfig();
14068
- errors.forEach((error) => console.error(`config ${error.path}: ${error.message}`));
14069
- if (!wantsWeb && !config.web.enabled) {
14070
- return runWatch({ directory }, config);
14071
- }
14072
- const watcher = await startWebWatch({ directory, port: config.web.port }, config);
14073
- console.log(`pair mode is watching ${directory}`);
14074
- console.log(watcher.url);
14075
- await new Promise((done) => {
14076
- const stop = () => {
14077
- void watcher.close().then(done);
14078
- };
14079
- process.once("SIGINT", stop);
14080
- process.once("SIGTERM", stop);
15059
+ if (parsed.malformedKey !== null) {
15060
+ console.error(`malformed session id: ${parsed.malformedKey}`);
15061
+ console.error(
15062
+ "an id is s- followed by eight hex characters; run pair-mode sessions to list them"
15063
+ );
15064
+ return 1;
15065
+ }
15066
+ return await watchSession({
15067
+ directory: parsed.directory,
15068
+ sessionKey: parsed.sessionKey,
15069
+ web: parsed.web
15070
+ });
15071
+ }
15072
+ if (command === "sessions") {
15073
+ const rejected = reportExtraArgs(command, process.argv.slice(3));
15074
+ if (rejected !== null) {
15075
+ return rejected;
15076
+ }
15077
+ const result = await listSessions();
15078
+ console.log(result.text);
15079
+ return result.exitCode;
15080
+ }
15081
+ if (command === "connect") {
15082
+ const rejected = reportExtraArgs(command, process.argv.slice(3));
15083
+ if (rejected !== null) {
15084
+ return rejected;
15085
+ }
15086
+ const result = await runConnect(createWatchIo());
15087
+ const chosen = result.selected;
15088
+ if (chosen === null) {
15089
+ return result.exitCode;
15090
+ }
15091
+ return await watchSession({
15092
+ directory: chosen.directory === "" ? process.cwd() : chosen.directory,
15093
+ sessionKey: chosen.kind === "session" ? chosen.id : void 0,
15094
+ socketPath: join15(sessionsDir(), `${chosen.id}.sock`),
15095
+ web: false
14081
15096
  });
14082
- return 0;
14083
15097
  }
14084
15098
  console.error(`unknown command: ${command}`);
14085
15099
  console.error(USAGE);