pair-mode 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -8
- package/README.md +71 -12
- package/assets/duck-appalled-banner-sprite.png +0 -0
- package/assets/duck-appalled-banner-v2-sprite.png +0 -0
- package/assets/duck-appalled-banner-v2.gif +0 -0
- package/assets/duck-appalled-sprite.png +0 -0
- package/assets/duck-appalled.gif +0 -0
- package/assets/duck-before-main.png +0 -0
- package/assets/duck.png +0 -0
- package/assets/favicon.png +0 -0
- package/assets/syntax/clojure.yaml +36 -0
- package/assets/syntax/cmake.yaml +41 -0
- package/assets/syntax/crystal.yaml +71 -0
- package/assets/syntax/csharp.yaml +51 -0
- package/assets/syntax/dart.yaml +45 -0
- package/assets/syntax/elm.yaml +38 -0
- package/assets/syntax/erb.yaml +42 -0
- package/assets/syntax/erlang.yaml +45 -0
- package/assets/syntax/fsharp.yaml +48 -0
- package/assets/syntax/graphql.yaml +47 -0
- package/assets/syntax/groovy.yaml +111 -0
- package/assets/syntax/haml.yaml +16 -0
- package/assets/syntax/haskell.yaml +52 -0
- package/assets/syntax/ini.yaml +23 -0
- package/assets/syntax/java.yaml +36 -0
- package/assets/syntax/julia.yaml +56 -0
- package/assets/syntax/kotlin.yaml +65 -0
- package/assets/syntax/makefile.yaml +37 -0
- package/assets/syntax/nginx.yaml +22 -0
- package/assets/syntax/nim.yaml +27 -0
- package/assets/syntax/nix.yaml +32 -0
- package/assets/syntax/objc.yaml +60 -0
- package/assets/syntax/ocaml.yaml +43 -0
- package/assets/syntax/perl.yaml +58 -0
- package/assets/syntax/php.yaml +60 -0
- package/assets/syntax/r.yaml +30 -0
- package/assets/syntax/scala.yaml +32 -0
- package/assets/syntax/svelte.yaml +27 -0
- package/assets/syntax/swift.yaml +102 -0
- package/assets/syntax/vue.yaml +63 -0
- package/assets/syntax/xml.yaml +37 -0
- package/assets/syntax/zig.yaml +52 -0
- package/dist/claude-code.js +463 -150
- package/dist/cli.js +1534 -459
- package/dist/codex.js +811 -394
- package/dist/opencode.js +455 -143
- package/dist/pair-tui.js +177 -27
- package/dist/pi.js +510 -156
- package/package.json +16 -17
- package/skills/toggle/SKILL.md +24 -0
- package/commands/pair.md +0 -18
- 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
|
|
7365
|
-
import { join as
|
|
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
|
|
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
|
|
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
|
|
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"] ||
|
|
7900
|
-
return
|
|
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
|
|
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
|
|
7966
|
+
return join6(stateDir(), `${digestFor(directory)}.on`);
|
|
7920
7967
|
}
|
|
7921
7968
|
function sessionsDir() {
|
|
7922
|
-
return
|
|
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
|
|
7979
|
+
return join6(sessionsDir(), `${digestFor(directory)}.sock`);
|
|
7926
7980
|
}
|
|
7927
7981
|
function sessionUrlPath(directory) {
|
|
7928
|
-
return
|
|
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
|
-
|
|
8060
|
+
unlinkSync2(path);
|
|
7942
8061
|
return true;
|
|
7943
8062
|
}
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
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
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
function
|
|
7960
|
-
|
|
7961
|
-
|
|
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/
|
|
7965
|
-
import { spawnSync as
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
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
|
-
|
|
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
|
|
8148
|
+
function isString2(value) {
|
|
7996
8149
|
return typeof value === "string";
|
|
7997
8150
|
}
|
|
7998
8151
|
function isClientKind(value) {
|
|
7999
|
-
return
|
|
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 &&
|
|
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 (!
|
|
8166
|
+
if (!isString2(raw["tool"]) || !isString2(raw["path"])) {
|
|
8014
8167
|
return null;
|
|
8015
8168
|
}
|
|
8016
|
-
if (!
|
|
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 || !
|
|
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 (!
|
|
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
|
|
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
|
|
8103
|
-
const
|
|
8104
|
-
if (
|
|
8105
|
-
return { state,
|
|
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
|
|
8108
|
-
const
|
|
8109
|
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
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
|
-
|
|
8207
|
-
|
|
8208
|
-
|
|
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
|
-
|
|
8421
|
+
agentByReview.set(id, socket);
|
|
8230
8422
|
dispatch();
|
|
8231
8423
|
}
|
|
8232
8424
|
function handleAttach(socket) {
|
|
8233
|
-
|
|
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
|
|
8238
|
-
if (
|
|
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
|
-
|
|
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 = [...
|
|
8465
|
+
const owned = [...agentByReview.entries()].filter(([, agentSocket]) => agentSocket === socket);
|
|
8466
|
+
if (owned.length === 0) {
|
|
8467
|
+
return;
|
|
8468
|
+
}
|
|
8250
8469
|
owned.forEach(([id]) => {
|
|
8251
|
-
|
|
8252
|
-
|
|
8253
|
-
|
|
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
|
-
|
|
8260
|
-
dispatch();
|
|
8261
|
-
}
|
|
8475
|
+
dispatch();
|
|
8262
8476
|
}
|
|
8263
8477
|
function dropClient(socket) {
|
|
8264
|
-
const
|
|
8265
|
-
if (
|
|
8478
|
+
const leaving = clientBySocket.get(socket);
|
|
8479
|
+
if (!leaving) {
|
|
8266
8480
|
return;
|
|
8267
8481
|
}
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
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
|
}
|
|
@@ -8309,12 +8537,18 @@ async function startSessionServer(options) {
|
|
|
8309
8537
|
}
|
|
8310
8538
|
const server = createServer(handleConnection);
|
|
8311
8539
|
await bindSocket(server, options.socketPath);
|
|
8540
|
+
if (options.record !== void 0) {
|
|
8541
|
+
writeRecord(options.socketPath, options.record);
|
|
8542
|
+
}
|
|
8312
8543
|
server.on("error", reportError);
|
|
8313
8544
|
return {
|
|
8314
8545
|
socketPath: options.socketPath,
|
|
8315
8546
|
clientCount() {
|
|
8316
8547
|
return clients.size;
|
|
8317
8548
|
},
|
|
8549
|
+
lastAttachAt() {
|
|
8550
|
+
return lastAttachAt;
|
|
8551
|
+
},
|
|
8318
8552
|
waitingDepth() {
|
|
8319
8553
|
return waitingDepth(queue);
|
|
8320
8554
|
},
|
|
@@ -8326,6 +8560,7 @@ async function startSessionServer(options) {
|
|
|
8326
8560
|
[...connections].forEach((socket) => socket.destroy());
|
|
8327
8561
|
server.close(() => {
|
|
8328
8562
|
removeQuietly(options.socketPath);
|
|
8563
|
+
removeQuietly(recordPathFor(options.socketPath));
|
|
8329
8564
|
resolve3();
|
|
8330
8565
|
});
|
|
8331
8566
|
});
|
|
@@ -8333,76 +8568,873 @@ async function startSessionServer(options) {
|
|
|
8333
8568
|
};
|
|
8334
8569
|
}
|
|
8335
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 {
|
|
8583
|
+
review: [],
|
|
8584
|
+
cancel: [],
|
|
8585
|
+
change: [],
|
|
8586
|
+
close: [],
|
|
8587
|
+
bufferedReviews: [],
|
|
8588
|
+
bufferedCancels: []
|
|
8589
|
+
};
|
|
8590
|
+
}
|
|
8591
|
+
function addReviewHandler(handlers, handler) {
|
|
8592
|
+
handlers.review.push(handler);
|
|
8593
|
+
handlers.bufferedReviews.splice(0).forEach((review) => handler(review));
|
|
8594
|
+
}
|
|
8595
|
+
function addCancelHandler(handlers, handler) {
|
|
8596
|
+
handlers.cancel.push(handler);
|
|
8597
|
+
handlers.bufferedCancels.splice(0).forEach((id) => handler(id));
|
|
8598
|
+
}
|
|
8599
|
+
async function attach(socketPath, client, handlers, onState) {
|
|
8600
|
+
const socket = await connect(socketPath);
|
|
8601
|
+
const readLines = createLineReader();
|
|
8602
|
+
socket.on("error", () => socket.destroy());
|
|
8603
|
+
socket.on("data", (chunk) => {
|
|
8604
|
+
readLines(chunk).forEach((line2) => {
|
|
8605
|
+
const message = decodeLine(line2);
|
|
8606
|
+
if (message?.type === "review") {
|
|
8607
|
+
if (handlers.review.length === 0) {
|
|
8608
|
+
handlers.bufferedReviews.push(message);
|
|
8609
|
+
return;
|
|
8610
|
+
}
|
|
8611
|
+
handlers.review.forEach((handler) => handler(message));
|
|
8612
|
+
return;
|
|
8613
|
+
}
|
|
8614
|
+
if (message?.type === "cancel") {
|
|
8615
|
+
if (handlers.cancel.length === 0) {
|
|
8616
|
+
handlers.bufferedCancels.push(message.id);
|
|
8617
|
+
return;
|
|
8618
|
+
}
|
|
8619
|
+
handlers.cancel.forEach((handler) => handler(message.id));
|
|
8620
|
+
return;
|
|
8621
|
+
}
|
|
8622
|
+
if (message?.type === "state") {
|
|
8623
|
+
onState(message);
|
|
8624
|
+
}
|
|
8625
|
+
});
|
|
8626
|
+
});
|
|
8627
|
+
socket.write(encode({ type: "attach", client }));
|
|
8628
|
+
return socket;
|
|
8629
|
+
}
|
|
8630
|
+
async function ownerHost(options) {
|
|
8631
|
+
const handlers = emptyHandlers();
|
|
8632
|
+
const server = await startSessionServer({
|
|
8633
|
+
socketPath: options.socketPath,
|
|
8634
|
+
record: options.record,
|
|
8635
|
+
onError: options.onError
|
|
8636
|
+
});
|
|
8637
|
+
server.onChange(() => handlers.change.forEach((handler) => handler()));
|
|
8638
|
+
const socket = await attach(options.socketPath, options.client, handlers, () => {
|
|
8639
|
+
});
|
|
8640
|
+
return {
|
|
8641
|
+
socketPath: options.socketPath,
|
|
8642
|
+
owns: true,
|
|
8643
|
+
counts() {
|
|
8644
|
+
return { clients: server.clientCount(), waiting: server.waitingDepth() };
|
|
8645
|
+
},
|
|
8646
|
+
// The server holds the counts in process, so nothing needs asking.
|
|
8647
|
+
refreshCounts() {
|
|
8648
|
+
},
|
|
8649
|
+
verdict(id, questions) {
|
|
8650
|
+
socket.write(encode({ type: "verdict", id, questions }));
|
|
8651
|
+
},
|
|
8652
|
+
onReview(handler) {
|
|
8653
|
+
addReviewHandler(handlers, handler);
|
|
8654
|
+
},
|
|
8655
|
+
onCancel(handler) {
|
|
8656
|
+
addCancelHandler(handlers, handler);
|
|
8657
|
+
},
|
|
8658
|
+
onChange(handler) {
|
|
8659
|
+
handlers.change.push(handler);
|
|
8660
|
+
},
|
|
8661
|
+
// The owner's socket only closes when the server it owns closes, so a handler here would never fire.
|
|
8662
|
+
onClose() {
|
|
8663
|
+
},
|
|
8664
|
+
async close() {
|
|
8665
|
+
socket.destroy();
|
|
8666
|
+
await server.close();
|
|
8667
|
+
}
|
|
8668
|
+
};
|
|
8669
|
+
}
|
|
8670
|
+
async function viewerHost(options) {
|
|
8671
|
+
const handlers = emptyHandlers();
|
|
8672
|
+
let remote = null;
|
|
8673
|
+
const socket = await attach(options.socketPath, options.client, handlers, (state) => {
|
|
8674
|
+
remote = state;
|
|
8675
|
+
handlers.change.forEach((handler) => handler());
|
|
8676
|
+
});
|
|
8677
|
+
socket.on("close", () => {
|
|
8678
|
+
remote = null;
|
|
8679
|
+
handlers.change.forEach((handler) => handler());
|
|
8680
|
+
handlers.close.forEach((handler) => handler());
|
|
8681
|
+
});
|
|
8682
|
+
return {
|
|
8683
|
+
socketPath: options.socketPath,
|
|
8684
|
+
owns: false,
|
|
8685
|
+
counts() {
|
|
8686
|
+
return { clients: remote?.clientCount ?? 0, waiting: remote?.waitingDepth ?? 0 };
|
|
8687
|
+
},
|
|
8688
|
+
refreshCounts() {
|
|
8689
|
+
if (!socket.destroyed) {
|
|
8690
|
+
socket.write(encode({ type: "status" }));
|
|
8691
|
+
}
|
|
8692
|
+
},
|
|
8693
|
+
verdict(id, questions) {
|
|
8694
|
+
if (!socket.destroyed) {
|
|
8695
|
+
socket.write(encode({ type: "verdict", id, questions }));
|
|
8696
|
+
}
|
|
8697
|
+
},
|
|
8698
|
+
onReview(handler) {
|
|
8699
|
+
addReviewHandler(handlers, handler);
|
|
8700
|
+
},
|
|
8701
|
+
onCancel(handler) {
|
|
8702
|
+
addCancelHandler(handlers, handler);
|
|
8703
|
+
},
|
|
8704
|
+
onChange(handler) {
|
|
8705
|
+
handlers.change.push(handler);
|
|
8706
|
+
},
|
|
8707
|
+
onClose(handler) {
|
|
8708
|
+
handlers.close.push(handler);
|
|
8709
|
+
},
|
|
8710
|
+
close() {
|
|
8711
|
+
socket.destroy();
|
|
8712
|
+
return Promise.resolve();
|
|
8713
|
+
}
|
|
8714
|
+
};
|
|
8715
|
+
}
|
|
8716
|
+
|
|
8717
|
+
// src/transports/session/probe.ts
|
|
8718
|
+
import { createConnection as createConnection3 } from "node:net";
|
|
8719
|
+
var STATUS_TIMEOUT_MS = 250;
|
|
8720
|
+
var ABANDONED_CODES = ["ECONNREFUSED", "ENOENT", "ENOTSOCK", "ENOTDIR"];
|
|
8721
|
+
function probeSession(socketPath) {
|
|
8722
|
+
return new Promise((resolve3) => {
|
|
8723
|
+
let settled = false;
|
|
8724
|
+
let connected = false;
|
|
8725
|
+
const settle = (probe) => {
|
|
8726
|
+
if (settled) {
|
|
8727
|
+
return;
|
|
8728
|
+
}
|
|
8729
|
+
settled = true;
|
|
8730
|
+
clearTimeout(timer);
|
|
8731
|
+
socket.destroy();
|
|
8732
|
+
resolve3(probe);
|
|
8733
|
+
};
|
|
8734
|
+
const timer = setTimeout(() => settle({ status: "silent" }), STATUS_TIMEOUT_MS);
|
|
8735
|
+
const socket = createConnection3(socketPath);
|
|
8736
|
+
socket.setEncoding("utf-8");
|
|
8737
|
+
const readLines = createLineReader();
|
|
8738
|
+
socket.on("error", (error) => {
|
|
8739
|
+
const code2 = "code" in error ? error.code : null;
|
|
8740
|
+
const abandoned = !connected && isString(code2) && ABANDONED_CODES.includes(code2);
|
|
8741
|
+
settle(abandoned ? { status: "refused" } : { status: "silent" });
|
|
8742
|
+
});
|
|
8743
|
+
socket.on("close", () => settle({ status: "silent" }));
|
|
8744
|
+
socket.on("data", (chunk) => {
|
|
8745
|
+
readLines(chunk).forEach((line2) => {
|
|
8746
|
+
const message = decodeLine(line2);
|
|
8747
|
+
if (message?.type !== "state") {
|
|
8748
|
+
return;
|
|
8749
|
+
}
|
|
8750
|
+
settle({ status: "answered", state: message });
|
|
8751
|
+
});
|
|
8752
|
+
});
|
|
8753
|
+
socket.on("connect", () => {
|
|
8754
|
+
connected = true;
|
|
8755
|
+
socket.write(encode({ type: "status" }));
|
|
8756
|
+
});
|
|
8757
|
+
});
|
|
8758
|
+
}
|
|
8759
|
+
|
|
8760
|
+
// src/cli/sessions/sessions.ts
|
|
8761
|
+
var UNKNOWN_LABEL = "unknown";
|
|
8762
|
+
var UNKNOWN_AGE = "-";
|
|
8763
|
+
var UNKNOWN_COUNT = "?";
|
|
8764
|
+
var SESSION_KINDS = ["session", "directory"];
|
|
8765
|
+
var SECOND_MS = 1e3;
|
|
8766
|
+
var MINUTE_MS = 60 * SECOND_MS;
|
|
8767
|
+
var HOUR_MS = 60 * MINUTE_MS;
|
|
8768
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
8769
|
+
function isSessionKind(value) {
|
|
8770
|
+
return isString(value) && SESSION_KINDS.includes(value);
|
|
8771
|
+
}
|
|
8772
|
+
function isSessionRecord(value) {
|
|
8773
|
+
if (!isRecord(value)) {
|
|
8774
|
+
return false;
|
|
8775
|
+
}
|
|
8776
|
+
if (!isString(value["id"]) || !isSessionKind(value["kind"])) {
|
|
8777
|
+
return false;
|
|
8778
|
+
}
|
|
8779
|
+
if (!isString(value["label"]) || !isString(value["directory"])) {
|
|
8780
|
+
return false;
|
|
8781
|
+
}
|
|
8782
|
+
if (!isNullableString(value["branch"]) || !isNullableString(value["agentSessionId"])) {
|
|
8783
|
+
return false;
|
|
8784
|
+
}
|
|
8785
|
+
if (!isNullableString(value["agentKind"]) || !isString(value["createdAt"])) {
|
|
8786
|
+
return false;
|
|
8787
|
+
}
|
|
8788
|
+
return typeof value["pid"] === "number";
|
|
8789
|
+
}
|
|
8790
|
+
function readRecord(id) {
|
|
8791
|
+
const path = join7(sessionsDir(), `${id}.json`);
|
|
8792
|
+
if (!existsSync5(path)) {
|
|
8793
|
+
return null;
|
|
8794
|
+
}
|
|
8795
|
+
try {
|
|
8796
|
+
const parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
8797
|
+
return isSessionRecord(parsed) ? parsed : null;
|
|
8798
|
+
} catch {
|
|
8799
|
+
return null;
|
|
8800
|
+
}
|
|
8801
|
+
}
|
|
8802
|
+
function sessionIds() {
|
|
8803
|
+
try {
|
|
8804
|
+
return readdirSync(sessionsDir()).filter((name) => name.endsWith(".sock")).map((name) => basename3(name, ".sock")).sort();
|
|
8805
|
+
} catch {
|
|
8806
|
+
return [];
|
|
8807
|
+
}
|
|
8808
|
+
}
|
|
8809
|
+
function removeSession(id) {
|
|
8810
|
+
[".sock", ".json", ".url"].forEach(
|
|
8811
|
+
(extension) => removeQuietly(join7(sessionsDir(), `${id}${extension}`))
|
|
8812
|
+
);
|
|
8813
|
+
}
|
|
8814
|
+
var FLAG_EXTENSIONS = [".on", ".off"];
|
|
8815
|
+
var FLAG_EXPIRY_MS = 14 * DAY_MS;
|
|
8816
|
+
function flagFiles() {
|
|
8817
|
+
try {
|
|
8818
|
+
return readdirSync(sessionsDir()).filter((name) => FLAG_EXTENSIONS.some((extension) => name.endsWith(extension))).filter((name) => name.startsWith("s-")).sort();
|
|
8819
|
+
} catch {
|
|
8820
|
+
return [];
|
|
8821
|
+
}
|
|
8822
|
+
}
|
|
8823
|
+
function ageOf(path, now) {
|
|
8824
|
+
try {
|
|
8825
|
+
return now - statSync(path).mtimeMs;
|
|
8826
|
+
} catch {
|
|
8827
|
+
return null;
|
|
8828
|
+
}
|
|
8829
|
+
}
|
|
8830
|
+
function sweepExpiredFlags(now = Date.now()) {
|
|
8831
|
+
const expired = [];
|
|
8832
|
+
flagFiles().forEach((name) => {
|
|
8833
|
+
const id = name.replace(/\.(on|off)$/, "");
|
|
8834
|
+
if (existsSync5(join7(sessionsDir(), `${id}.sock`))) {
|
|
8835
|
+
return;
|
|
8836
|
+
}
|
|
8837
|
+
const path = join7(sessionsDir(), name);
|
|
8838
|
+
const age = ageOf(path, now);
|
|
8839
|
+
if (age === null || age < FLAG_EXPIRY_MS) {
|
|
8840
|
+
return;
|
|
8841
|
+
}
|
|
8842
|
+
removeQuietly(path);
|
|
8843
|
+
expired.push(name);
|
|
8844
|
+
});
|
|
8845
|
+
return expired;
|
|
8846
|
+
}
|
|
8847
|
+
function kindOf(id, record) {
|
|
8848
|
+
if (record !== null) {
|
|
8849
|
+
return record.kind;
|
|
8850
|
+
}
|
|
8851
|
+
return id.startsWith("s-") ? "session" : "directory";
|
|
8852
|
+
}
|
|
8853
|
+
function toListing(id, probe) {
|
|
8854
|
+
const record = readRecord(id);
|
|
8855
|
+
const state = probe.status === "answered" ? probe.state : null;
|
|
8856
|
+
return {
|
|
8857
|
+
id,
|
|
8858
|
+
kind: kindOf(id, record),
|
|
8859
|
+
label: record?.label ?? UNKNOWN_LABEL,
|
|
8860
|
+
directory: record?.directory ?? "",
|
|
8861
|
+
clients: state?.clientCount ?? null,
|
|
8862
|
+
waiting: state?.waitingDepth ?? null,
|
|
8863
|
+
createdAt: record?.createdAt ?? "",
|
|
8864
|
+
alive: true
|
|
8865
|
+
};
|
|
8866
|
+
}
|
|
8867
|
+
async function scan() {
|
|
8868
|
+
const ids = sessionIds();
|
|
8869
|
+
const probes = await Promise.all(
|
|
8870
|
+
ids.map((id) => probeSession(join7(sessionsDir(), `${id}.sock`)))
|
|
8871
|
+
);
|
|
8872
|
+
const listings = [];
|
|
8873
|
+
const swept = [];
|
|
8874
|
+
ids.forEach((id, index) => {
|
|
8875
|
+
const probe = probes[index];
|
|
8876
|
+
if (probe === void 0 || probe.status === "refused") {
|
|
8877
|
+
removeSession(id);
|
|
8878
|
+
swept.push(id);
|
|
8879
|
+
return;
|
|
8880
|
+
}
|
|
8881
|
+
listings.push(toListing(id, probe));
|
|
8882
|
+
});
|
|
8883
|
+
return { listings, swept, expired: sweepExpiredFlags() };
|
|
8884
|
+
}
|
|
8885
|
+
function formatAge(createdAt, now) {
|
|
8886
|
+
const started = Date.parse(createdAt);
|
|
8887
|
+
if (Number.isNaN(started)) {
|
|
8888
|
+
return UNKNOWN_AGE;
|
|
8889
|
+
}
|
|
8890
|
+
const elapsed = Math.max(0, now - started);
|
|
8891
|
+
if (elapsed >= DAY_MS) {
|
|
8892
|
+
return `${Math.floor(elapsed / DAY_MS)}d`;
|
|
8893
|
+
}
|
|
8894
|
+
if (elapsed >= HOUR_MS) {
|
|
8895
|
+
return `${Math.floor(elapsed / HOUR_MS)}h`;
|
|
8896
|
+
}
|
|
8897
|
+
if (elapsed >= MINUTE_MS) {
|
|
8898
|
+
return `${Math.floor(elapsed / MINUTE_MS)}m`;
|
|
8899
|
+
}
|
|
8900
|
+
return `${Math.floor(elapsed / SECOND_MS)}s`;
|
|
8901
|
+
}
|
|
8902
|
+
function formatCount(count) {
|
|
8903
|
+
return count === null ? UNKNOWN_COUNT : String(count);
|
|
8904
|
+
}
|
|
8905
|
+
function formatTable(listings, now) {
|
|
8906
|
+
const header = ["ID", "LABEL", "KIND", "WATCHERS", "QUEUED", "AGE"];
|
|
8907
|
+
const rows = listings.map((entry) => [
|
|
8908
|
+
entry.id,
|
|
8909
|
+
entry.label,
|
|
8910
|
+
entry.kind,
|
|
8911
|
+
formatCount(entry.clients),
|
|
8912
|
+
formatCount(entry.waiting),
|
|
8913
|
+
formatAge(entry.createdAt, now)
|
|
8914
|
+
]);
|
|
8915
|
+
const widths = header.map(
|
|
8916
|
+
(name, column) => Math.max(name.length, ...rows.map((row) => row[column]?.length ?? 0))
|
|
8917
|
+
);
|
|
8918
|
+
const line2 = (cells) => cells.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd();
|
|
8919
|
+
return [line2(header), ...rows.map(line2)].join("\n");
|
|
8920
|
+
}
|
|
8921
|
+
function sweptLine(swept) {
|
|
8922
|
+
const noun = swept.length === 1 ? "session" : "sessions";
|
|
8923
|
+
return `swept ${swept.length} dead ${noun}`;
|
|
8924
|
+
}
|
|
8925
|
+
function expiredLine(expired) {
|
|
8926
|
+
const noun = expired.length === 1 ? "flag" : "flags";
|
|
8927
|
+
return `expired ${expired.length} stale session ${noun}`;
|
|
8928
|
+
}
|
|
8929
|
+
async function listSessions() {
|
|
8930
|
+
const { listings, swept, expired } = await scan();
|
|
8931
|
+
const table = listings.length === 0 ? "no pair-mode sessions" : formatTable(listings, Date.now());
|
|
8932
|
+
const notes = [
|
|
8933
|
+
...swept.length === 0 ? [] : [sweptLine(swept)],
|
|
8934
|
+
...expired.length === 0 ? [] : [expiredLine(expired)]
|
|
8935
|
+
];
|
|
8936
|
+
const text = notes.length === 0 ? table : `${table}
|
|
8937
|
+
|
|
8938
|
+
${notes.join("\n")}`;
|
|
8939
|
+
return { listings, swept, expired, text, exitCode: 0 };
|
|
8940
|
+
}
|
|
8941
|
+
async function sweepDeadSessions() {
|
|
8942
|
+
const { swept } = await scan();
|
|
8943
|
+
return swept;
|
|
8944
|
+
}
|
|
8945
|
+
|
|
8946
|
+
// src/cli/sessions/connect.ts
|
|
8947
|
+
var CLEAR_SCREEN = "\x1B[2J\x1B[H";
|
|
8948
|
+
var DOWN_KEYS = ["j", "\x1B[B"];
|
|
8949
|
+
var UP_KEYS = ["k", "\x1B[A"];
|
|
8950
|
+
var SELECT_KEYS = ["\r", "\n"];
|
|
8951
|
+
var QUIT_KEYS = ["q", ""];
|
|
8952
|
+
var HELP = "j/k move, Enter watches, q quits";
|
|
8953
|
+
function watchers(entry) {
|
|
8954
|
+
return entry.clients === null ? "?" : String(entry.clients);
|
|
8955
|
+
}
|
|
8956
|
+
function paint(io, listings, cursor) {
|
|
8957
|
+
const rows = listings.map((entry, index) => {
|
|
8958
|
+
const marker = index === cursor ? ">" : " ";
|
|
8959
|
+
return `${marker} ${entry.id} ${entry.label} ${watchers(entry)} watching`;
|
|
8960
|
+
});
|
|
8961
|
+
io.write(`${CLEAR_SCREEN}pair mode sessions\r
|
|
8962
|
+
\r
|
|
8963
|
+
${rows.join("\r\n")}\r
|
|
8964
|
+
\r
|
|
8965
|
+
${HELP}\r
|
|
8966
|
+
`);
|
|
8967
|
+
}
|
|
8968
|
+
function pick(io, listings) {
|
|
8969
|
+
return new Promise((resolve3) => {
|
|
8970
|
+
let cursor = 0;
|
|
8971
|
+
io.onKey((key) => {
|
|
8972
|
+
if (QUIT_KEYS.includes(key)) {
|
|
8973
|
+
resolve3({ selected: null, exitCode: 0 });
|
|
8974
|
+
return;
|
|
8975
|
+
}
|
|
8976
|
+
if (SELECT_KEYS.includes(key)) {
|
|
8977
|
+
resolve3({ selected: listings[cursor] ?? null, exitCode: 0 });
|
|
8978
|
+
return;
|
|
8979
|
+
}
|
|
8980
|
+
if (DOWN_KEYS.includes(key)) {
|
|
8981
|
+
cursor = Math.min(cursor + 1, listings.length - 1);
|
|
8982
|
+
}
|
|
8983
|
+
if (UP_KEYS.includes(key)) {
|
|
8984
|
+
cursor = Math.max(cursor - 1, 0);
|
|
8985
|
+
}
|
|
8986
|
+
paint(io, listings, cursor);
|
|
8987
|
+
});
|
|
8988
|
+
paint(io, listings, cursor);
|
|
8989
|
+
});
|
|
8990
|
+
}
|
|
8991
|
+
async function runConnect(io) {
|
|
8992
|
+
try {
|
|
8993
|
+
if (!io.isTty()) {
|
|
8994
|
+
io.write("connect needs a terminal; run pair-mode sessions instead\n");
|
|
8995
|
+
return { selected: null, exitCode: 1 };
|
|
8996
|
+
}
|
|
8997
|
+
const { listings } = await listSessions();
|
|
8998
|
+
if (listings.length === 0) {
|
|
8999
|
+
io.write("no pair-mode sessions\n");
|
|
9000
|
+
return { selected: null, exitCode: 0 };
|
|
9001
|
+
}
|
|
9002
|
+
return await pick(io, listings);
|
|
9003
|
+
} finally {
|
|
9004
|
+
io.shutdown();
|
|
9005
|
+
}
|
|
9006
|
+
}
|
|
9007
|
+
|
|
9008
|
+
// src/cli/toggle.ts
|
|
9009
|
+
import { spawn } from "node:child_process";
|
|
9010
|
+
import { existsSync as existsSync6, readFileSync as readFileSync3, unlinkSync as unlinkSync3 } from "node:fs";
|
|
9011
|
+
import { join as join8 } from "node:path";
|
|
9012
|
+
var POLL_MS = 100;
|
|
9013
|
+
var POLL_ATTEMPTS = 60;
|
|
9014
|
+
var SESSION_ENV_VARS = ["CLAUDE_CODE_SESSION_ID", "CODEX_SESSION_ID", "CODEX_THREAD_ID"];
|
|
9015
|
+
function agentSessionId(env) {
|
|
9016
|
+
const found = SESSION_ENV_VARS.map((name) => env[name]).find(
|
|
9017
|
+
(value) => typeof value === "string" && value !== ""
|
|
9018
|
+
);
|
|
9019
|
+
return found ?? null;
|
|
9020
|
+
}
|
|
9021
|
+
function currentSessionKey() {
|
|
9022
|
+
return keyFor(agentSessionId(process.env) ?? void 0);
|
|
9023
|
+
}
|
|
9024
|
+
function statusProbe(directory) {
|
|
9025
|
+
return join8(directory, ".pair-mode-status-probe");
|
|
9026
|
+
}
|
|
9027
|
+
function sleep(ms) {
|
|
9028
|
+
return new Promise((done) => setTimeout(done, ms));
|
|
9029
|
+
}
|
|
9030
|
+
function readLink(directory, key) {
|
|
9031
|
+
const path = watchUrlPath(directory, key);
|
|
9032
|
+
if (!existsSync6(path)) {
|
|
9033
|
+
return null;
|
|
9034
|
+
}
|
|
9035
|
+
try {
|
|
9036
|
+
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
9037
|
+
if (isRecord(parsed) && typeof parsed["url"] === "string" && typeof parsed["pid"] === "number") {
|
|
9038
|
+
return { url: parsed["url"], pid: parsed["pid"] };
|
|
9039
|
+
}
|
|
9040
|
+
} catch {
|
|
9041
|
+
return null;
|
|
9042
|
+
}
|
|
9043
|
+
return null;
|
|
9044
|
+
}
|
|
9045
|
+
async function waitForLink(directory, key) {
|
|
9046
|
+
const attempts = Array.from({ length: POLL_ATTEMPTS }, (_, index) => index);
|
|
9047
|
+
for (const _attempt of attempts) {
|
|
9048
|
+
const link = readLink(directory, key);
|
|
9049
|
+
if (link !== null) {
|
|
9050
|
+
return link;
|
|
9051
|
+
}
|
|
9052
|
+
await sleep(POLL_MS);
|
|
9053
|
+
}
|
|
9054
|
+
return null;
|
|
9055
|
+
}
|
|
9056
|
+
function stopLink(directory, key) {
|
|
9057
|
+
const link = readLink(directory, key);
|
|
9058
|
+
if (link === null) {
|
|
9059
|
+
return false;
|
|
9060
|
+
}
|
|
9061
|
+
try {
|
|
9062
|
+
process.kill(link.pid, "SIGTERM");
|
|
9063
|
+
} catch {
|
|
9064
|
+
}
|
|
9065
|
+
try {
|
|
9066
|
+
unlinkSync3(watchUrlPath(directory, key));
|
|
9067
|
+
} catch {
|
|
9068
|
+
}
|
|
9069
|
+
return true;
|
|
9070
|
+
}
|
|
9071
|
+
function onHeadline(directory, key) {
|
|
9072
|
+
return key ? `pair mode ON \xB7 ${key} \xB7 ${directory}` : `pair mode ON for ${directory}`;
|
|
9073
|
+
}
|
|
9074
|
+
function pairOn(directory, key) {
|
|
9075
|
+
if (key) {
|
|
9076
|
+
enableSession(key);
|
|
9077
|
+
} else {
|
|
9078
|
+
enable(directory);
|
|
9079
|
+
}
|
|
9080
|
+
return onHeadline(directory, key);
|
|
9081
|
+
}
|
|
9082
|
+
async function pairOnWeb(directory, cliPath, key) {
|
|
9083
|
+
const headline = onHeadline(directory, key);
|
|
9084
|
+
if (key) {
|
|
9085
|
+
enableSession(key);
|
|
9086
|
+
} else {
|
|
9087
|
+
enable(directory);
|
|
9088
|
+
}
|
|
9089
|
+
const existing = readLink(directory, key);
|
|
9090
|
+
if (existing !== null) {
|
|
9091
|
+
return `${headline}
|
|
9092
|
+
${existing.url}`;
|
|
9093
|
+
}
|
|
9094
|
+
const target = key ?? directory;
|
|
9095
|
+
const child = spawn(process.execPath, [cliPath, "watch", "--web", target], {
|
|
9096
|
+
cwd: directory,
|
|
9097
|
+
detached: true,
|
|
9098
|
+
stdio: "ignore"
|
|
9099
|
+
});
|
|
9100
|
+
child.unref();
|
|
9101
|
+
const link = await waitForLink(directory, key);
|
|
9102
|
+
if (link === null) {
|
|
9103
|
+
return `${headline}
|
|
9104
|
+
the web watcher did not report a link`;
|
|
9105
|
+
}
|
|
9106
|
+
return `${headline}
|
|
9107
|
+
${link.url}`;
|
|
9108
|
+
}
|
|
9109
|
+
function pairOff(directory, key) {
|
|
9110
|
+
if (key) {
|
|
9111
|
+
optOutSession(key);
|
|
9112
|
+
const stopped2 = stopLink(directory, key);
|
|
9113
|
+
return stopped2 ? `pair mode OFF \xB7 ${key} (web watcher stopped)` : `pair mode OFF \xB7 ${key}`;
|
|
9114
|
+
}
|
|
9115
|
+
disable(directory);
|
|
9116
|
+
const stopped = stopLink(directory);
|
|
9117
|
+
return stopped ? `pair mode OFF for ${directory} (web watcher stopped)` : `pair mode OFF for ${directory}`;
|
|
9118
|
+
}
|
|
9119
|
+
async function pairToggle(directory, cliPath, web, key) {
|
|
9120
|
+
const on = key ? isEnabled(statusProbe(directory), key) : existsSync6(flagPath(directory));
|
|
9121
|
+
if (on) {
|
|
9122
|
+
return pairOff(directory, key);
|
|
9123
|
+
}
|
|
9124
|
+
if (web) {
|
|
9125
|
+
return await pairOnWeb(directory, cliPath, key);
|
|
9126
|
+
}
|
|
9127
|
+
return pairOn(directory, key);
|
|
9128
|
+
}
|
|
9129
|
+
function pairStatus(directory, key) {
|
|
9130
|
+
const on = isEnabled(statusProbe(directory), key);
|
|
9131
|
+
const link = readLink(directory, key);
|
|
9132
|
+
const scope = key ? `${key} \xB7 ${directory}` : directory;
|
|
9133
|
+
const state = `pair mode ${on ? "ON" : "OFF"} for ${scope}`;
|
|
9134
|
+
return link === null ? state : `${state}
|
|
9135
|
+
${link.url}`;
|
|
9136
|
+
}
|
|
9137
|
+
|
|
8336
9138
|
// src/editors/micro.ts
|
|
8337
9139
|
var import_yaml = __toESM(require_dist(), 1);
|
|
8338
|
-
import { mkdirSync as mkdirSync4, writeFileSync as
|
|
8339
|
-
import { join as
|
|
9140
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
|
|
9141
|
+
import { join as join10 } from "node:path";
|
|
8340
9142
|
|
|
8341
9143
|
// src/editors/languages.ts
|
|
8342
|
-
import {
|
|
8343
|
-
|
|
9144
|
+
import { openSync, readSync, closeSync } from "node:fs";
|
|
9145
|
+
import { basename as basename4, extname as extname2 } from "node:path";
|
|
9146
|
+
var EXTENSIONS = {
|
|
8344
9147
|
".go": "go",
|
|
8345
9148
|
".rb": "ruby",
|
|
8346
9149
|
".rake": "ruby",
|
|
9150
|
+
".gemspec": "ruby",
|
|
9151
|
+
".ru": "ruby",
|
|
9152
|
+
".erb": "erb",
|
|
9153
|
+
".haml": "haml",
|
|
8347
9154
|
".ts": "typescript",
|
|
8348
|
-
".
|
|
9155
|
+
".mts": "typescript",
|
|
9156
|
+
".cts": "typescript",
|
|
9157
|
+
".tsx": "tsx",
|
|
8349
9158
|
".js": "javascript",
|
|
8350
|
-
".jsx": "javascript",
|
|
8351
9159
|
".mjs": "javascript",
|
|
8352
|
-
".
|
|
9160
|
+
".cjs": "javascript",
|
|
9161
|
+
".jsx": "jsx",
|
|
9162
|
+
".vue": "vue",
|
|
9163
|
+
".svelte": "svelte",
|
|
9164
|
+
".astro": "astro",
|
|
9165
|
+
".py": "python",
|
|
9166
|
+
".pyi": "python",
|
|
8353
9167
|
".ex": "elixir",
|
|
8354
9168
|
".exs": "elixir",
|
|
9169
|
+
".heex": "elixir",
|
|
9170
|
+
".erl": "erlang",
|
|
9171
|
+
".hrl": "erlang",
|
|
8355
9172
|
".rs": "rust",
|
|
8356
|
-
".sh": "
|
|
8357
|
-
".bash": "
|
|
8358
|
-
".
|
|
9173
|
+
".sh": "shellscript",
|
|
9174
|
+
".bash": "shellscript",
|
|
9175
|
+
".ksh": "shellscript",
|
|
8359
9176
|
".zsh": "zsh",
|
|
9177
|
+
".fish": "fish",
|
|
9178
|
+
".ps1": "powershell",
|
|
9179
|
+
".psm1": "powershell",
|
|
9180
|
+
".bat": "bat",
|
|
9181
|
+
".cmd": "bat",
|
|
8360
9182
|
".sql": "sql",
|
|
8361
9183
|
".json": "json",
|
|
9184
|
+
".jsonc": "jsonc",
|
|
9185
|
+
".json5": "json5",
|
|
8362
9186
|
".tf": "terraform",
|
|
9187
|
+
".tfvars": "terraform",
|
|
9188
|
+
".hcl": "hcl",
|
|
8363
9189
|
".proto": "proto",
|
|
8364
|
-
".dockerfile": "
|
|
9190
|
+
".dockerfile": "docker",
|
|
8365
9191
|
".toml": "toml",
|
|
8366
9192
|
".yaml": "yaml",
|
|
8367
9193
|
".yml": "yaml",
|
|
9194
|
+
".ini": "ini",
|
|
9195
|
+
".cfg": "ini",
|
|
9196
|
+
".properties": "properties",
|
|
9197
|
+
".env": "dotenv",
|
|
8368
9198
|
".md": "markdown",
|
|
9199
|
+
".markdown": "markdown",
|
|
9200
|
+
".mdx": "mdx",
|
|
8369
9201
|
".css": "css",
|
|
9202
|
+
".scss": "scss",
|
|
9203
|
+
".sass": "sass",
|
|
9204
|
+
".less": "less",
|
|
9205
|
+
".styl": "stylus",
|
|
8370
9206
|
".html": "html",
|
|
8371
|
-
".
|
|
9207
|
+
".htm": "html",
|
|
9208
|
+
".xml": "xml",
|
|
9209
|
+
".xsl": "xml",
|
|
9210
|
+
".svg": "xml",
|
|
8372
9211
|
".lua": "lua",
|
|
8373
9212
|
".c": "c",
|
|
8374
|
-
".h": "c"
|
|
9213
|
+
".h": "c",
|
|
9214
|
+
".cc": "cpp",
|
|
9215
|
+
".cpp": "cpp",
|
|
9216
|
+
".cxx": "cpp",
|
|
9217
|
+
".hpp": "cpp",
|
|
9218
|
+
".hh": "cpp",
|
|
9219
|
+
".cs": "csharp",
|
|
9220
|
+
".java": "java",
|
|
9221
|
+
".kt": "kotlin",
|
|
9222
|
+
".kts": "kotlin",
|
|
9223
|
+
".swift": "swift",
|
|
9224
|
+
".m": "objective-c",
|
|
9225
|
+
".mm": "objective-c",
|
|
9226
|
+
".php": "php",
|
|
9227
|
+
".pl": "perl",
|
|
9228
|
+
".pm": "perl",
|
|
9229
|
+
".scala": "scala",
|
|
9230
|
+
".sc": "scala",
|
|
9231
|
+
".clj": "clojure",
|
|
9232
|
+
".cljs": "clojure",
|
|
9233
|
+
".cljc": "clojure",
|
|
9234
|
+
".hs": "haskell",
|
|
9235
|
+
".nix": "nix",
|
|
9236
|
+
".r": "r",
|
|
9237
|
+
".jl": "julia",
|
|
9238
|
+
".zig": "zig",
|
|
9239
|
+
".nim": "nim",
|
|
9240
|
+
".dart": "dart",
|
|
9241
|
+
".groovy": "groovy",
|
|
9242
|
+
".gradle": "groovy",
|
|
9243
|
+
".cr": "crystal",
|
|
9244
|
+
".ml": "ocaml",
|
|
9245
|
+
".mli": "ocaml",
|
|
9246
|
+
".fs": "fsharp",
|
|
9247
|
+
".fsx": "fsharp",
|
|
9248
|
+
".elm": "elm",
|
|
9249
|
+
".graphql": "graphql",
|
|
9250
|
+
".gql": "graphql",
|
|
9251
|
+
".prisma": "prisma",
|
|
9252
|
+
".sol": "solidity",
|
|
9253
|
+
".vim": "viml",
|
|
9254
|
+
".diff": "diff",
|
|
9255
|
+
".patch": "diff",
|
|
9256
|
+
".tex": "latex",
|
|
9257
|
+
".wgsl": "wgsl",
|
|
9258
|
+
".glsl": "glsl",
|
|
9259
|
+
".cue": "cue"
|
|
9260
|
+
};
|
|
9261
|
+
var FILENAMES = {
|
|
9262
|
+
gemfile: "ruby",
|
|
9263
|
+
rakefile: "ruby",
|
|
9264
|
+
capfile: "ruby",
|
|
9265
|
+
vagrantfile: "ruby",
|
|
9266
|
+
guardfile: "ruby",
|
|
9267
|
+
podfile: "ruby",
|
|
9268
|
+
fastfile: "ruby",
|
|
9269
|
+
appfile: "ruby",
|
|
9270
|
+
brewfile: "ruby",
|
|
9271
|
+
"config.ru": "ruby",
|
|
9272
|
+
dockerfile: "docker",
|
|
9273
|
+
containerfile: "docker",
|
|
9274
|
+
makefile: "make",
|
|
9275
|
+
gnumakefile: "make",
|
|
9276
|
+
"cmakelists.txt": "cmake",
|
|
9277
|
+
"cargo.lock": "toml",
|
|
9278
|
+
"gemfile.lock": "toml",
|
|
9279
|
+
".babelrc": "json",
|
|
9280
|
+
".eslintrc": "json",
|
|
9281
|
+
".prettierrc": "json",
|
|
9282
|
+
".env": "dotenv",
|
|
9283
|
+
".gitconfig": "ini",
|
|
9284
|
+
".editorconfig": "ini",
|
|
9285
|
+
"nginx.conf": "nginx",
|
|
9286
|
+
"pnpm-workspace.yaml": "yaml"
|
|
8375
9287
|
};
|
|
8376
|
-
|
|
8377
|
-
|
|
8378
|
-
|
|
8379
|
-
}
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
|
|
8383
|
-
|
|
9288
|
+
var SHEBANGS = [
|
|
9289
|
+
{ pattern: /^#!.*\/(env\s+)?ruby(\s|$)/, id: "ruby" },
|
|
9290
|
+
{ pattern: /^#!.*\/(env\s+)?python[\d.]*(\s|$)/, id: "python" },
|
|
9291
|
+
{ pattern: /^#!.*\/(env\s+)?(node|bun|deno)(\s|$)/, id: "javascript" },
|
|
9292
|
+
{ pattern: /^#!.*\/(env\s+)?(bash|sh|ksh|dash)(\s|$)/, id: "shellscript" },
|
|
9293
|
+
{ pattern: /^#!.*\/(env\s+)?zsh(\s|$)/, id: "zsh" },
|
|
9294
|
+
{ pattern: /^#!.*\/(env\s+)?fish(\s|$)/, id: "fish" },
|
|
9295
|
+
{ pattern: /^#!.*\/(env\s+)?perl(\s|$)/, id: "perl" },
|
|
9296
|
+
{ pattern: /^#!.*\/(env\s+)?php(\s|$)/, id: "php" },
|
|
9297
|
+
{ pattern: /^#!.*\/(env\s+)?(elixir|iex)(\s|$)/, id: "elixir" },
|
|
9298
|
+
{ pattern: /^#!.*\/(env\s+)?lua(\s|$)/, id: "lua" },
|
|
9299
|
+
{ pattern: /^#!.*\/(env\s+)?Rscript(\s|$)/, id: "r" },
|
|
9300
|
+
{ pattern: /^#!.*\/(env\s+)?pwsh(\s|$)/, id: "powershell" }
|
|
9301
|
+
];
|
|
9302
|
+
var FIRST_LINE_BYTES = 256;
|
|
9303
|
+
function firstLine(sourcePath) {
|
|
9304
|
+
let handle = null;
|
|
9305
|
+
try {
|
|
9306
|
+
handle = openSync(sourcePath, "r");
|
|
9307
|
+
const buffer = Buffer.alloc(FIRST_LINE_BYTES);
|
|
9308
|
+
const read = readSync(handle, buffer, 0, FIRST_LINE_BYTES, 0);
|
|
9309
|
+
const text = buffer.toString("utf-8", 0, read);
|
|
9310
|
+
const newline = text.indexOf("\n");
|
|
9311
|
+
return newline === -1 ? text : text.slice(0, newline);
|
|
9312
|
+
} catch {
|
|
9313
|
+
return null;
|
|
9314
|
+
} finally {
|
|
9315
|
+
if (handle !== null) {
|
|
9316
|
+
try {
|
|
9317
|
+
closeSync(handle);
|
|
9318
|
+
} catch {
|
|
9319
|
+
}
|
|
9320
|
+
}
|
|
9321
|
+
}
|
|
9322
|
+
}
|
|
9323
|
+
function shebangLanguage(sourcePath) {
|
|
9324
|
+
const line2 = firstLine(sourcePath);
|
|
9325
|
+
if (line2 === null || !line2.startsWith("#!")) {
|
|
9326
|
+
return null;
|
|
9327
|
+
}
|
|
9328
|
+
return SHEBANGS.find((rule) => rule.pattern.test(line2))?.id ?? null;
|
|
9329
|
+
}
|
|
9330
|
+
function detectLanguage(sourcePath) {
|
|
9331
|
+
const name = basename4(sourcePath).toLowerCase();
|
|
9332
|
+
const ext = extname2(name);
|
|
9333
|
+
if (ext !== "" && EXTENSIONS[ext] !== void 0) {
|
|
9334
|
+
return EXTENSIONS[ext];
|
|
9335
|
+
}
|
|
9336
|
+
if (FILENAMES[name] !== void 0) {
|
|
9337
|
+
return FILENAMES[name];
|
|
9338
|
+
}
|
|
9339
|
+
return shebangLanguage(sourcePath);
|
|
9340
|
+
}
|
|
9341
|
+
var MICRO_SYNTAX = {
|
|
9342
|
+
c: "c",
|
|
9343
|
+
clojure: "clojure",
|
|
9344
|
+
cmake: "cmake",
|
|
9345
|
+
crystal: "crystal",
|
|
9346
|
+
csharp: "csharp",
|
|
9347
|
+
css: "css",
|
|
9348
|
+
dart: "dart",
|
|
9349
|
+
docker: "dockerfile",
|
|
9350
|
+
elixir: "elixir",
|
|
9351
|
+
elm: "elm",
|
|
9352
|
+
erb: "erb",
|
|
9353
|
+
erlang: "erlang",
|
|
8384
9354
|
fish: "fish",
|
|
9355
|
+
fsharp: "fsharp",
|
|
9356
|
+
go: "go",
|
|
9357
|
+
graphql: "graphql",
|
|
9358
|
+
groovy: "groovy",
|
|
9359
|
+
haml: "haml",
|
|
9360
|
+
haskell: "haskell",
|
|
9361
|
+
html: "html",
|
|
9362
|
+
ini: "ini",
|
|
9363
|
+
java: "java",
|
|
9364
|
+
javascript: "javascript",
|
|
9365
|
+
json: "json",
|
|
9366
|
+
julia: "julia",
|
|
9367
|
+
kotlin: "kotlin",
|
|
9368
|
+
lua: "lua",
|
|
9369
|
+
make: "makefile",
|
|
9370
|
+
markdown: "markdown",
|
|
9371
|
+
nginx: "nginx",
|
|
9372
|
+
nim: "nim",
|
|
9373
|
+
nix: "nix",
|
|
9374
|
+
"objective-c": "objc",
|
|
9375
|
+
ocaml: "ocaml",
|
|
9376
|
+
perl: "perl",
|
|
9377
|
+
php: "php",
|
|
8385
9378
|
proto: "proto",
|
|
8386
|
-
|
|
8387
|
-
|
|
9379
|
+
python: "python3",
|
|
9380
|
+
r: "r",
|
|
9381
|
+
ruby: "ruby",
|
|
9382
|
+
rust: "rust",
|
|
9383
|
+
scala: "scala",
|
|
9384
|
+
shellscript: "sh",
|
|
9385
|
+
sql: "sql",
|
|
9386
|
+
svelte: "svelte",
|
|
9387
|
+
swift: "swift",
|
|
9388
|
+
terraform: "terraform",
|
|
9389
|
+
toml: "toml",
|
|
9390
|
+
typescript: "typescript",
|
|
9391
|
+
vue: "vue",
|
|
9392
|
+
xml: "xml",
|
|
9393
|
+
yaml: "yaml",
|
|
9394
|
+
zig: "zig",
|
|
9395
|
+
zsh: "zsh"
|
|
9396
|
+
};
|
|
9397
|
+
var VIM_FILETYPES = {
|
|
9398
|
+
shellscript: "sh",
|
|
9399
|
+
docker: "dockerfile",
|
|
9400
|
+
csharp: "cs",
|
|
9401
|
+
"objective-c": "objc",
|
|
9402
|
+
bat: "dosbatch",
|
|
9403
|
+
viml: "vim",
|
|
9404
|
+
mdx: "markdown",
|
|
9405
|
+
jsx: "javascriptreact",
|
|
9406
|
+
tsx: "typescriptreact",
|
|
9407
|
+
dotenv: "sh",
|
|
9408
|
+
properties: "jproperties",
|
|
9409
|
+
stylus: "stylus"
|
|
8388
9410
|
};
|
|
8389
9411
|
function shikiLanguage(sourcePath) {
|
|
8390
|
-
|
|
8391
|
-
|
|
9412
|
+
return detectLanguage(sourcePath);
|
|
9413
|
+
}
|
|
9414
|
+
function microSyntaxName(sourcePath) {
|
|
9415
|
+
const id = detectLanguage(sourcePath);
|
|
9416
|
+
if (id === null) {
|
|
9417
|
+
return null;
|
|
9418
|
+
}
|
|
9419
|
+
return MICRO_SYNTAX[id] ?? null;
|
|
9420
|
+
}
|
|
9421
|
+
function vimFiletype(sourcePath) {
|
|
9422
|
+
const id = detectLanguage(sourcePath);
|
|
9423
|
+
if (id === null) {
|
|
8392
9424
|
return null;
|
|
8393
9425
|
}
|
|
8394
|
-
return
|
|
9426
|
+
return VIM_FILETYPES[id] ?? id;
|
|
8395
9427
|
}
|
|
8396
9428
|
|
|
8397
9429
|
// src/editors/syntax-cache.ts
|
|
8398
|
-
import { existsSync as
|
|
8399
|
-
import { dirname as dirname6, join as
|
|
9430
|
+
import { existsSync as existsSync7, readFileSync as readFileSync4 } from "node:fs";
|
|
9431
|
+
import { dirname as dirname6, join as join9 } from "node:path";
|
|
8400
9432
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
8401
9433
|
function defaultAssetsDir() {
|
|
8402
9434
|
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
8403
9435
|
while (true) {
|
|
8404
|
-
const candidate =
|
|
8405
|
-
if (
|
|
9436
|
+
const candidate = join9(dir, "assets", "syntax");
|
|
9437
|
+
if (existsSync7(candidate)) {
|
|
8406
9438
|
return candidate;
|
|
8407
9439
|
}
|
|
8408
9440
|
const parent = dirname6(dir);
|
|
@@ -8416,11 +9448,11 @@ function syntaxSource(lang, assetsDir = defaultAssetsDir()) {
|
|
|
8416
9448
|
if (assetsDir === null) {
|
|
8417
9449
|
return null;
|
|
8418
9450
|
}
|
|
8419
|
-
const path =
|
|
8420
|
-
if (!
|
|
9451
|
+
const path = join9(assetsDir, `${lang}.yaml`);
|
|
9452
|
+
if (!existsSync7(path)) {
|
|
8421
9453
|
return null;
|
|
8422
9454
|
}
|
|
8423
|
-
return
|
|
9455
|
+
return readFileSync4(path, "utf-8");
|
|
8424
9456
|
}
|
|
8425
9457
|
|
|
8426
9458
|
// src/editors/micro.ts
|
|
@@ -8453,7 +9485,7 @@ function bandRules() {
|
|
|
8453
9485
|
];
|
|
8454
9486
|
}
|
|
8455
9487
|
function writeColorScheme(configDir, theme2) {
|
|
8456
|
-
const dir =
|
|
9488
|
+
const dir = join10(configDir, "colorschemes");
|
|
8457
9489
|
mkdirSync4(dir, { recursive: true });
|
|
8458
9490
|
const text = `include "monokai"
|
|
8459
9491
|
|
|
@@ -8461,7 +9493,7 @@ color-link pairadd "#d7ffd7,${theme2.add}"
|
|
|
8461
9493
|
color-link pairdel "#ffd7d7,${theme2.del}"
|
|
8462
9494
|
color-link pairskip "#6a6a6a,${theme2.fold}"
|
|
8463
9495
|
`;
|
|
8464
|
-
|
|
9496
|
+
writeFileSync4(join10(dir, "pair.micro"), text, "utf-8");
|
|
8465
9497
|
}
|
|
8466
9498
|
function syntaxText(lang, source) {
|
|
8467
9499
|
const parsed = (0, import_yaml.parse)(source);
|
|
@@ -8477,7 +9509,7 @@ function syntaxText(lang, source) {
|
|
|
8477
9509
|
return (0, import_yaml.stringify)(file, { lineWidth: 0 });
|
|
8478
9510
|
}
|
|
8479
9511
|
function writeSyntax(configDir, sourcePath) {
|
|
8480
|
-
const lang =
|
|
9512
|
+
const lang = microSyntaxName(sourcePath);
|
|
8481
9513
|
if (lang === null) {
|
|
8482
9514
|
return;
|
|
8483
9515
|
}
|
|
@@ -8489,9 +9521,9 @@ function writeSyntax(configDir, sourcePath) {
|
|
|
8489
9521
|
if (text === null) {
|
|
8490
9522
|
return;
|
|
8491
9523
|
}
|
|
8492
|
-
const dir =
|
|
9524
|
+
const dir = join10(configDir, "syntax");
|
|
8493
9525
|
mkdirSync4(dir, { recursive: true });
|
|
8494
|
-
|
|
9526
|
+
writeFileSync4(join10(dir, `pair-${lang}.yaml`), text, "utf-8");
|
|
8495
9527
|
}
|
|
8496
9528
|
function createMicroEditor(resolvesOnPath = defaultResolvesOnPath) {
|
|
8497
9529
|
return {
|
|
@@ -8504,7 +9536,7 @@ function createMicroEditor(resolvesOnPath = defaultResolvesOnPath) {
|
|
|
8504
9536
|
return ["# F3 moves between panes. Ctrl+W or F2 sends and closes."];
|
|
8505
9537
|
},
|
|
8506
9538
|
bufferSuffix(sourcePath) {
|
|
8507
|
-
const lang =
|
|
9539
|
+
const lang = microSyntaxName(sourcePath);
|
|
8508
9540
|
if (lang === null) {
|
|
8509
9541
|
return ".diff";
|
|
8510
9542
|
}
|
|
@@ -8512,13 +9544,13 @@ function createMicroEditor(resolvesOnPath = defaultResolvesOnPath) {
|
|
|
8512
9544
|
},
|
|
8513
9545
|
prepare(context) {
|
|
8514
9546
|
mkdirSync4(context.configDir, { recursive: true });
|
|
8515
|
-
|
|
8516
|
-
|
|
9547
|
+
writeFileSync4(
|
|
9548
|
+
join10(context.configDir, "bindings.json"),
|
|
8517
9549
|
JSON.stringify(MICRO_BINDINGS, null, 2),
|
|
8518
9550
|
"utf-8"
|
|
8519
9551
|
);
|
|
8520
|
-
|
|
8521
|
-
|
|
9552
|
+
writeFileSync4(
|
|
9553
|
+
join10(context.configDir, "settings.json"),
|
|
8522
9554
|
JSON.stringify(MICRO_SETTINGS, null, 2),
|
|
8523
9555
|
"utf-8"
|
|
8524
9556
|
);
|
|
@@ -8534,16 +9566,6 @@ function createMicroEditor(resolvesOnPath = defaultResolvesOnPath) {
|
|
|
8534
9566
|
|
|
8535
9567
|
// src/editors/vim.ts
|
|
8536
9568
|
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
9569
|
function safeThemeColor(value) {
|
|
8548
9570
|
if (!isHexColor(value)) {
|
|
8549
9571
|
throw new Error(`invalid theme colour for vim highlight: ${value}`);
|
|
@@ -8597,8 +9619,8 @@ function vimEditor(name, resolvesOnPath = defaultResolvesOnPath) {
|
|
|
8597
9619
|
}
|
|
8598
9620
|
|
|
8599
9621
|
// src/editors/nano.ts
|
|
8600
|
-
import { mkdirSync as mkdirSync5, writeFileSync as
|
|
8601
|
-
import { join as
|
|
9622
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
9623
|
+
import { join as join11 } from "node:path";
|
|
8602
9624
|
function safeThemeColor2(value) {
|
|
8603
9625
|
if (!isHexColor(value)) {
|
|
8604
9626
|
throw new Error(`invalid theme colour for nano rcfile: ${value}`);
|
|
@@ -8610,8 +9632,8 @@ function writeNanorc(configDir, theme2) {
|
|
|
8610
9632
|
color ,${safeThemeColor2(theme2.del)} "^\u258C\u258C-"
|
|
8611
9633
|
color ,${safeThemeColor2(theme2.fold)} "^\u22EF"
|
|
8612
9634
|
`;
|
|
8613
|
-
const path =
|
|
8614
|
-
|
|
9635
|
+
const path = join11(configDir, "pair.nanorc");
|
|
9636
|
+
writeFileSync5(path, text, "utf-8");
|
|
8615
9637
|
return path;
|
|
8616
9638
|
}
|
|
8617
9639
|
function createNanoEditor(resolvesOnPath = defaultResolvesOnPath) {
|
|
@@ -8755,17 +9777,17 @@ function createTmuxMultiplexer(spawn2 = defaultSpawn, resolvesOnPath = defaultRe
|
|
|
8755
9777
|
}
|
|
8756
9778
|
|
|
8757
9779
|
// src/multiplexers/tty.ts
|
|
8758
|
-
import { openSync, closeSync } from "node:fs";
|
|
8759
|
-
import { spawnSync as
|
|
8760
|
-
var defaultOpen = () =>
|
|
9780
|
+
import { openSync as openSync2, closeSync as closeSync2 } from "node:fs";
|
|
9781
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
9782
|
+
var defaultOpen = () => openSync2("/dev/tty", "r+");
|
|
8761
9783
|
var defaultRunner = (command, args, fd) => {
|
|
8762
|
-
const result =
|
|
9784
|
+
const result = spawnSync4(command, args, { stdio: [fd, fd, fd] });
|
|
8763
9785
|
const detail = result.status === 0 ? "" : String(result.error?.message ?? result.stderr ?? "");
|
|
8764
9786
|
return { ok: result.status === 0, detail };
|
|
8765
9787
|
};
|
|
8766
9788
|
function closeQuietly(fd) {
|
|
8767
9789
|
try {
|
|
8768
|
-
|
|
9790
|
+
closeSync2(fd);
|
|
8769
9791
|
} catch {
|
|
8770
9792
|
}
|
|
8771
9793
|
}
|
|
@@ -8823,9 +9845,16 @@ function detect(preference, adapters = {}) {
|
|
|
8823
9845
|
}
|
|
8824
9846
|
|
|
8825
9847
|
// src/cli/register/register.ts
|
|
8826
|
-
import { existsSync as
|
|
8827
|
-
import { basename as
|
|
9848
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, copyFileSync } from "node:fs";
|
|
9849
|
+
import { basename as basename5, dirname as dirname7, join as join12, sep as sep2 } from "node:path";
|
|
8828
9850
|
var HOOK_TIMEOUT_SECONDS = 1800;
|
|
9851
|
+
function hookCommand(installRoot2, file) {
|
|
9852
|
+
const path = join12(installRoot2, "dist", file);
|
|
9853
|
+
if (/^[A-Za-z0-9_./-]+$/.test(path)) {
|
|
9854
|
+
return path;
|
|
9855
|
+
}
|
|
9856
|
+
return `'${path.replace(/'/g, "'\\''")}'`;
|
|
9857
|
+
}
|
|
8829
9858
|
function isHookEntry(value) {
|
|
8830
9859
|
if (!isRecord(value)) {
|
|
8831
9860
|
return false;
|
|
@@ -8854,10 +9883,10 @@ function isHookGroup(value) {
|
|
|
8854
9883
|
return value["hooks"].every(isHookEntry);
|
|
8855
9884
|
}
|
|
8856
9885
|
function readJsonObject(path) {
|
|
8857
|
-
if (!
|
|
9886
|
+
if (!existsSync8(path)) {
|
|
8858
9887
|
return { ok: true, root: {} };
|
|
8859
9888
|
}
|
|
8860
|
-
const text =
|
|
9889
|
+
const text = readFileSync5(path, "utf-8");
|
|
8861
9890
|
let parsed;
|
|
8862
9891
|
try {
|
|
8863
9892
|
parsed = JSON.parse(text);
|
|
@@ -8874,7 +9903,7 @@ function readJsonObject(path) {
|
|
|
8874
9903
|
}
|
|
8875
9904
|
var backedUpThisRun = /* @__PURE__ */ new Set();
|
|
8876
9905
|
function backupIfPresent(path) {
|
|
8877
|
-
if (!
|
|
9906
|
+
if (!existsSync8(path)) {
|
|
8878
9907
|
return null;
|
|
8879
9908
|
}
|
|
8880
9909
|
const backupPath = `${path}.pair-backup`;
|
|
@@ -8915,8 +9944,8 @@ function hasCommand(groups, command) {
|
|
|
8915
9944
|
});
|
|
8916
9945
|
}
|
|
8917
9946
|
function matchesOurCommand(command) {
|
|
8918
|
-
const suffix = sep2 +
|
|
8919
|
-
return (entry) => entry.command === command || entry.command.endsWith(suffix);
|
|
9947
|
+
const suffix = sep2 + join12("dist", basename5(command));
|
|
9948
|
+
return (entry) => entry.command === command || entry.command.endsWith(suffix) || entry.command.endsWith(suffix + "'");
|
|
8920
9949
|
}
|
|
8921
9950
|
function upsertHookGroup(groups, matcher, command, timeout) {
|
|
8922
9951
|
const matches = matchesOurCommand(command);
|
|
@@ -8947,7 +9976,7 @@ function upsertHookGroup(groups, matcher, command, timeout) {
|
|
|
8947
9976
|
}
|
|
8948
9977
|
function writeJsonObject(path, root) {
|
|
8949
9978
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
8950
|
-
|
|
9979
|
+
writeFileSync6(path, JSON.stringify(root, null, 2) + "\n", "utf-8");
|
|
8951
9980
|
}
|
|
8952
9981
|
function registerPreToolUseHook(path, matcher, command, timeout) {
|
|
8953
9982
|
const read = readJsonObject(path);
|
|
@@ -8971,7 +10000,7 @@ function registerPreToolUseHook(path, matcher, command, timeout) {
|
|
|
8971
10000
|
return { path, changed: true, backupPath };
|
|
8972
10001
|
}
|
|
8973
10002
|
function isPreToolUseRegistered(path, command) {
|
|
8974
|
-
if (!
|
|
10003
|
+
if (!existsSync8(path)) {
|
|
8975
10004
|
return false;
|
|
8976
10005
|
}
|
|
8977
10006
|
const read = readJsonObject(path);
|
|
@@ -8981,24 +10010,24 @@ function isPreToolUseRegistered(path, command) {
|
|
|
8981
10010
|
return hasCommand(preToolUseGroups(read.root), command);
|
|
8982
10011
|
}
|
|
8983
10012
|
function claudeCodeSettingsPath(homeDir) {
|
|
8984
|
-
return
|
|
10013
|
+
return join12(homeDir, ".claude", "settings.json");
|
|
8985
10014
|
}
|
|
8986
10015
|
function registerClaudeCode(homeDir, installRoot2) {
|
|
8987
10016
|
const path = claudeCodeSettingsPath(homeDir);
|
|
8988
|
-
const command =
|
|
10017
|
+
const command = hookCommand(installRoot2, "claude-code.js");
|
|
8989
10018
|
return registerPreToolUseHook(path, "Write|Edit|MultiEdit", command, HOOK_TIMEOUT_SECONDS);
|
|
8990
10019
|
}
|
|
8991
10020
|
function codexHooksPath(homeDir) {
|
|
8992
|
-
return
|
|
10021
|
+
return join12(homeDir, ".codex", "hooks.json");
|
|
8993
10022
|
}
|
|
8994
10023
|
function registerCodex(homeDir, installRoot2) {
|
|
8995
10024
|
const path = codexHooksPath(homeDir);
|
|
8996
|
-
const command =
|
|
10025
|
+
const command = hookCommand(installRoot2, "codex.js");
|
|
8997
10026
|
return registerPreToolUseHook(path, "apply_patch|Edit|Write", command, HOOK_TIMEOUT_SECONDS);
|
|
8998
10027
|
}
|
|
8999
10028
|
function findMultiEditMatchers(homeDir) {
|
|
9000
10029
|
const path = codexHooksPath(homeDir);
|
|
9001
|
-
if (!
|
|
10030
|
+
if (!existsSync8(path)) {
|
|
9002
10031
|
return [];
|
|
9003
10032
|
}
|
|
9004
10033
|
const read = readJsonObject(path);
|
|
@@ -9058,12 +10087,12 @@ function correctMultiEditMatchers(homeDir) {
|
|
|
9058
10087
|
return { path, changed: true, backupPath, note };
|
|
9059
10088
|
}
|
|
9060
10089
|
function writeFileIfChanged(path, content) {
|
|
9061
|
-
if (
|
|
10090
|
+
if (existsSync8(path) && readFileSync5(path, "utf-8") === content) {
|
|
9062
10091
|
return { path, changed: false, backupPath: null };
|
|
9063
10092
|
}
|
|
9064
10093
|
const backupPath = backupIfPresent(path);
|
|
9065
10094
|
mkdirSync6(dirname7(path), { recursive: true });
|
|
9066
|
-
|
|
10095
|
+
writeFileSync6(path, content, "utf-8");
|
|
9067
10096
|
return { path, changed: true, backupPath };
|
|
9068
10097
|
}
|
|
9069
10098
|
function writeReExport(path, target) {
|
|
@@ -9076,29 +10105,29 @@ export { default } from "${target}";
|
|
|
9076
10105
|
`;
|
|
9077
10106
|
}
|
|
9078
10107
|
function isReExportRegistered(path, target) {
|
|
9079
|
-
if (!
|
|
10108
|
+
if (!existsSync8(path)) {
|
|
9080
10109
|
return false;
|
|
9081
10110
|
}
|
|
9082
|
-
return
|
|
10111
|
+
return readFileSync5(path, "utf-8").includes(target);
|
|
9083
10112
|
}
|
|
9084
10113
|
function opencodePluginPath(homeDir) {
|
|
9085
|
-
return
|
|
10114
|
+
return join12(homeDir, ".config", "opencode", "plugin", "pair-mode.ts");
|
|
9086
10115
|
}
|
|
9087
10116
|
function registerOpencode(homeDir, installRoot2) {
|
|
9088
|
-
const target =
|
|
10117
|
+
const target = join12(installRoot2, "dist", "opencode.js");
|
|
9089
10118
|
return writeReExport(opencodePluginPath(homeDir), target);
|
|
9090
10119
|
}
|
|
9091
10120
|
function piExtensionPath(homeDir) {
|
|
9092
|
-
return
|
|
10121
|
+
return join12(homeDir, ".pi", "agent", "extensions", "pair-mode.ts");
|
|
9093
10122
|
}
|
|
9094
10123
|
function registerPi(homeDir, installRoot2) {
|
|
9095
|
-
const target =
|
|
10124
|
+
const target = join12(installRoot2, "dist", "pi.js");
|
|
9096
10125
|
return writeFileIfChanged(piExtensionPath(homeDir), piExtensionSource(target));
|
|
9097
10126
|
}
|
|
9098
10127
|
|
|
9099
10128
|
// src/cli/register/commands.ts
|
|
9100
|
-
import { existsSync as
|
|
9101
|
-
import { join as
|
|
10129
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6 } from "node:fs";
|
|
10130
|
+
import { join as join13 } from "node:path";
|
|
9102
10131
|
var DESCRIPTION = "Toggle pair mode. Every proposed edit opens in the pair review pane for line annotation.";
|
|
9103
10132
|
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
10133
|
var SPECS = {
|
|
@@ -9128,7 +10157,7 @@ var SPECS = {
|
|
|
9128
10157
|
}
|
|
9129
10158
|
};
|
|
9130
10159
|
function pairCommandPath(homeDir, cli) {
|
|
9131
|
-
return
|
|
10160
|
+
return join13(homeDir, ...SPECS[cli].segments);
|
|
9132
10161
|
}
|
|
9133
10162
|
function pairCommandSource(cli) {
|
|
9134
10163
|
const spec = SPECS[cli];
|
|
@@ -9155,10 +10184,10 @@ function pairCommandSource(cli) {
|
|
|
9155
10184
|
}
|
|
9156
10185
|
function isPairCommandRegistered(homeDir, cli) {
|
|
9157
10186
|
const path = pairCommandPath(homeDir, cli);
|
|
9158
|
-
if (!
|
|
10187
|
+
if (!existsSync9(path)) {
|
|
9159
10188
|
return false;
|
|
9160
10189
|
}
|
|
9161
|
-
return
|
|
10190
|
+
return readFileSync6(path, "utf-8") === pairCommandSource(cli);
|
|
9162
10191
|
}
|
|
9163
10192
|
function registerPairCommand(homeDir, cli) {
|
|
9164
10193
|
return writeFileIfChanged(pairCommandPath(homeDir, cli), pairCommandSource(cli));
|
|
@@ -9194,7 +10223,7 @@ function checkMultiplexer(config, adapters) {
|
|
|
9194
10223
|
function checkControllingTerminal(openTty) {
|
|
9195
10224
|
try {
|
|
9196
10225
|
const fd = openTty();
|
|
9197
|
-
|
|
10226
|
+
closeSync3(fd);
|
|
9198
10227
|
return { name: "controlling terminal", passed: true, detail: "/dev/tty opened" };
|
|
9199
10228
|
} catch (error) {
|
|
9200
10229
|
const name = error instanceof Error ? error.name : "Error";
|
|
@@ -9232,30 +10261,32 @@ function checkCommandOnPath(resolves) {
|
|
|
9232
10261
|
};
|
|
9233
10262
|
}
|
|
9234
10263
|
function checkClis(home, root) {
|
|
9235
|
-
const claudeCommand =
|
|
9236
|
-
const codexCommand =
|
|
9237
|
-
const
|
|
9238
|
-
const
|
|
10264
|
+
const claudeCommand = hookCommand(root, "claude-code.js");
|
|
10265
|
+
const codexCommand = hookCommand(root, "codex.js");
|
|
10266
|
+
const claudeTarget = join14(root, "dist", "claude-code.js");
|
|
10267
|
+
const codexTarget = join14(root, "dist", "codex.js");
|
|
10268
|
+
const opencodeTarget = join14(root, "dist", "opencode.js");
|
|
10269
|
+
const piTarget = join14(root, "dist", "pi.js");
|
|
9239
10270
|
const specs = [
|
|
9240
10271
|
{
|
|
9241
10272
|
cli: "claude-code",
|
|
9242
10273
|
registered: isPreToolUseRegistered(claudeCodeSettingsPath(home), claudeCommand),
|
|
9243
|
-
targetExists:
|
|
10274
|
+
targetExists: existsSync10(claudeTarget)
|
|
9244
10275
|
},
|
|
9245
10276
|
{
|
|
9246
10277
|
cli: "codex",
|
|
9247
10278
|
registered: isPreToolUseRegistered(codexHooksPath(home), codexCommand),
|
|
9248
|
-
targetExists:
|
|
10279
|
+
targetExists: existsSync10(codexTarget)
|
|
9249
10280
|
},
|
|
9250
10281
|
{
|
|
9251
10282
|
cli: "opencode",
|
|
9252
10283
|
registered: isReExportRegistered(opencodePluginPath(home), opencodeTarget),
|
|
9253
|
-
targetExists:
|
|
10284
|
+
targetExists: existsSync10(opencodeTarget)
|
|
9254
10285
|
},
|
|
9255
10286
|
{
|
|
9256
10287
|
cli: "pi",
|
|
9257
10288
|
registered: isReExportRegistered(piExtensionPath(home), piTarget),
|
|
9258
|
-
targetExists:
|
|
10289
|
+
targetExists: existsSync10(piTarget)
|
|
9259
10290
|
}
|
|
9260
10291
|
];
|
|
9261
10292
|
const shown = specs.filter((spec) => isReleased(spec.cli) || spec.registered);
|
|
@@ -9265,14 +10296,14 @@ function checkClis(home, root) {
|
|
|
9265
10296
|
var SHEBANG_LINE = "#!/usr/bin/env node\n";
|
|
9266
10297
|
function isExecutable(path) {
|
|
9267
10298
|
try {
|
|
9268
|
-
return (
|
|
10299
|
+
return (statSync2(path).mode & 73) !== 0;
|
|
9269
10300
|
} catch {
|
|
9270
10301
|
return false;
|
|
9271
10302
|
}
|
|
9272
10303
|
}
|
|
9273
10304
|
function hasShebang(path) {
|
|
9274
10305
|
try {
|
|
9275
|
-
return
|
|
10306
|
+
return readFileSync7(path, "utf-8").startsWith(SHEBANG_LINE);
|
|
9276
10307
|
} catch {
|
|
9277
10308
|
return false;
|
|
9278
10309
|
}
|
|
@@ -9286,10 +10317,10 @@ function checkEntryPoints(root) {
|
|
|
9286
10317
|
"pi.js",
|
|
9287
10318
|
"pair-tui.js"
|
|
9288
10319
|
];
|
|
9289
|
-
const missing = entryPoints.filter((entry) => !
|
|
10320
|
+
const missing = entryPoints.filter((entry) => !existsSync10(join14(root, "dist", entry)));
|
|
9290
10321
|
const present = entryPoints.filter((entry) => !missing.includes(entry));
|
|
9291
|
-
const notExecutable = present.filter((entry) => !isExecutable(
|
|
9292
|
-
const missingShebang = present.filter((entry) => !hasShebang(
|
|
10322
|
+
const notExecutable = present.filter((entry) => !isExecutable(join14(root, "dist", entry)));
|
|
10323
|
+
const missingShebang = present.filter((entry) => !hasShebang(join14(root, "dist", entry)));
|
|
9293
10324
|
const problems = [];
|
|
9294
10325
|
if (missing.length > 0) {
|
|
9295
10326
|
problems.push(`missing: ${missing.join(", ")}`);
|
|
@@ -9327,11 +10358,11 @@ function checkTrace(config) {
|
|
|
9327
10358
|
if (!config.trace) {
|
|
9328
10359
|
return null;
|
|
9329
10360
|
}
|
|
9330
|
-
const tracePath =
|
|
9331
|
-
if (!
|
|
10361
|
+
const tracePath = join14(stateDir(), "trace.log");
|
|
10362
|
+
if (!existsSync10(tracePath)) {
|
|
9332
10363
|
return { name: "trace log", passed: true, detail: "tracing is on; no log written yet" };
|
|
9333
10364
|
}
|
|
9334
|
-
const lines =
|
|
10365
|
+
const lines = readFileSync7(tracePath, "utf-8").split("\n").filter((line2) => line2 !== "");
|
|
9335
10366
|
const tail = lines.slice(-10);
|
|
9336
10367
|
return {
|
|
9337
10368
|
name: "trace log",
|
|
@@ -9339,12 +10370,14 @@ function checkTrace(config) {
|
|
|
9339
10370
|
detail: tail.length === 0 ? "empty" : tail.join(" | ")
|
|
9340
10371
|
};
|
|
9341
10372
|
}
|
|
9342
|
-
var defaultOpenTty = () =>
|
|
10373
|
+
var defaultOpenTty = () => openSync3("/dev/tty", "r+");
|
|
10374
|
+
var PROBE_NAME = ".pair-mode-doctor-probe";
|
|
9343
10375
|
async function checkSession(config, directory, probe) {
|
|
9344
|
-
const
|
|
10376
|
+
const probeFile = join14(directory, PROBE_NAME);
|
|
10377
|
+
const path = resolveSocketPath(probeFile, currentSessionKey()) ?? sessionSocketPath(directory);
|
|
9345
10378
|
const name = `session: ${path}`;
|
|
9346
10379
|
const wanted = config.transport === "session";
|
|
9347
|
-
if (!
|
|
10380
|
+
if (!existsSync10(path)) {
|
|
9348
10381
|
return {
|
|
9349
10382
|
name,
|
|
9350
10383
|
passed: !wanted,
|
|
@@ -9352,11 +10385,12 @@ async function checkSession(config, directory, probe) {
|
|
|
9352
10385
|
warnOnly: !wanted
|
|
9353
10386
|
};
|
|
9354
10387
|
}
|
|
9355
|
-
const
|
|
9356
|
-
if (
|
|
10388
|
+
const result = await (probe ?? probeSession)(path);
|
|
10389
|
+
if (result.status !== "refused") {
|
|
9357
10390
|
return { name, passed: true, detail: "a watcher is attached" };
|
|
9358
10391
|
}
|
|
9359
|
-
|
|
10392
|
+
removeSession(basename6(path, ".sock"));
|
|
10393
|
+
return { name, passed: true, detail: "removed a stale socket", warnOnly: true };
|
|
9360
10394
|
}
|
|
9361
10395
|
async function runDoctor(options = {}) {
|
|
9362
10396
|
const home = options.homeDir ?? homedir4();
|
|
@@ -9373,7 +10407,7 @@ async function runDoctor(options = {}) {
|
|
|
9373
10407
|
...checkClis(home, root),
|
|
9374
10408
|
checkEntryPoints(root),
|
|
9375
10409
|
checkShiki(options.resolvesShiki),
|
|
9376
|
-
await checkSession(config, options.directory ?? process.cwd(), options.
|
|
10410
|
+
await checkSession(config, options.directory ?? process.cwd(), options.probeSession)
|
|
9377
10411
|
];
|
|
9378
10412
|
const traceCheck = checkTrace(config);
|
|
9379
10413
|
if (traceCheck !== null) {
|
|
@@ -9593,95 +10627,6 @@ async function runSetup(options = {}) {
|
|
|
9593
10627
|
}
|
|
9594
10628
|
}
|
|
9595
10629
|
|
|
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
10630
|
// node_modules/.pnpm/diff@9.0.0/node_modules/diff/libesm/diff/base.js
|
|
9686
10631
|
var Diff = class {
|
|
9687
10632
|
diff(oldStr, newStr, options = {}) {
|
|
@@ -10167,7 +11112,7 @@ function diffArrays(oldArr, newArr, options) {
|
|
|
10167
11112
|
}
|
|
10168
11113
|
|
|
10169
11114
|
// src/tui/paint/layout.ts
|
|
10170
|
-
import { basename as
|
|
11115
|
+
import { basename as basename7 } from "node:path";
|
|
10171
11116
|
|
|
10172
11117
|
// src/core/diff/diff.ts
|
|
10173
11118
|
function mergeChangedPair(first, second, i, j) {
|
|
@@ -10227,7 +11172,7 @@ var TAB_WIDTH = 8;
|
|
|
10227
11172
|
var CONTROL_BYTE_CEILING = 32;
|
|
10228
11173
|
var CONTROL_BYTE_PLACEHOLDER = "?";
|
|
10229
11174
|
function sanitizeLine(text) {
|
|
10230
|
-
const
|
|
11175
|
+
const scan2 = text.split("").reduce(
|
|
10231
11176
|
(state, char) => {
|
|
10232
11177
|
if (char === " ") {
|
|
10233
11178
|
const width = TAB_WIDTH - state.column % TAB_WIDTH;
|
|
@@ -10243,7 +11188,7 @@ function sanitizeLine(text) {
|
|
|
10243
11188
|
},
|
|
10244
11189
|
{ output: "", column: 0 }
|
|
10245
11190
|
);
|
|
10246
|
-
return
|
|
11191
|
+
return scan2.output;
|
|
10247
11192
|
}
|
|
10248
11193
|
function buildRows(before, after) {
|
|
10249
11194
|
return opcodes(before, after).flatMap((opcode) => {
|
|
@@ -10890,7 +11835,7 @@ function paintFoldRow(fold2, width, truecolor, cursorRow) {
|
|
|
10890
11835
|
return fg(color, truecolor) + " ".repeat(leftPadding) + label + " ".repeat(rightPadding) + RESET;
|
|
10891
11836
|
}
|
|
10892
11837
|
function paintHeader(path, addCount, delCount, width, truecolor) {
|
|
10893
|
-
const prefix = `pair mode\u2502${
|
|
11838
|
+
const prefix = `pair mode\u2502${basename7(path)}\u2502`;
|
|
10894
11839
|
const addText = `+${addCount}`;
|
|
10895
11840
|
const delText = `-${delCount}`;
|
|
10896
11841
|
const used = prefix.length + addText.length + HEADER_COUNT_GAP_WIDTH + delText.length;
|
|
@@ -11412,7 +12357,7 @@ function indentWidth(line2) {
|
|
|
11412
12357
|
}
|
|
11413
12358
|
function changedSpans(before, after) {
|
|
11414
12359
|
const chunks = diffWordsWithSpace(before, after);
|
|
11415
|
-
const
|
|
12360
|
+
const scan2 = chunks.reduce(
|
|
11416
12361
|
(state, chunk) => {
|
|
11417
12362
|
const length = chunk.value.length;
|
|
11418
12363
|
if (chunk.removed === true) {
|
|
@@ -11438,7 +12383,7 @@ function changedSpans(before, after) {
|
|
|
11438
12383
|
},
|
|
11439
12384
|
{ left: [], right: [], sharedLength: 0, leftCursor: 0, rightCursor: 0 }
|
|
11440
12385
|
);
|
|
11441
|
-
const { left, right, sharedLength } =
|
|
12386
|
+
const { left, right, sharedLength } = scan2;
|
|
11442
12387
|
const indent = Math.min(indentWidth(before), indentWidth(after));
|
|
11443
12388
|
const longer = Math.max(before.length, after.length) - indent;
|
|
11444
12389
|
const sharedFraction = longer <= 0 ? 1 : (sharedLength - indent) / longer;
|
|
@@ -11461,7 +12406,7 @@ function decideLayout(options) {
|
|
|
11461
12406
|
function layoutStatusMessage(options) {
|
|
11462
12407
|
return decideLayout(options).overrideReason;
|
|
11463
12408
|
}
|
|
11464
|
-
function
|
|
12409
|
+
function paint2(options) {
|
|
11465
12410
|
const { layout } = decideLayout(options);
|
|
11466
12411
|
return layout === "unified" ? paintUnified(options) : paintSplit(options);
|
|
11467
12412
|
}
|
|
@@ -11671,7 +12616,8 @@ function splitInput(chunk) {
|
|
|
11671
12616
|
}
|
|
11672
12617
|
|
|
11673
12618
|
// src/tui/notes/notes.ts
|
|
11674
|
-
import { writeFileSync as
|
|
12619
|
+
import { writeFileSync as writeFileSync7 } from "node:fs";
|
|
12620
|
+
var OWNER_ONLY_FILE2 = 384;
|
|
11675
12621
|
function rangeOf(selection) {
|
|
11676
12622
|
const reversed = selection.anchorRow > selection.headRow || selection.anchorRow === selection.headRow && selection.anchorColumn > selection.headColumn;
|
|
11677
12623
|
if (!reversed) {
|
|
@@ -11742,7 +12688,10 @@ function toQuestions(notes) {
|
|
|
11742
12688
|
}
|
|
11743
12689
|
function writeResult(path, notes) {
|
|
11744
12690
|
try {
|
|
11745
|
-
|
|
12691
|
+
writeFileSync7(path, JSON.stringify({ questions: toQuestions(notes) }, null, 2), {
|
|
12692
|
+
encoding: "utf-8",
|
|
12693
|
+
mode: OWNER_ONLY_FILE2
|
|
12694
|
+
});
|
|
11746
12695
|
} catch {
|
|
11747
12696
|
return;
|
|
11748
12697
|
}
|
|
@@ -12039,7 +12988,7 @@ function runTui(options, io, abort) {
|
|
|
12039
12988
|
let finished = false;
|
|
12040
12989
|
const repaint = () => {
|
|
12041
12990
|
const { width, height } = io.size();
|
|
12042
|
-
const result =
|
|
12991
|
+
const result = paint2({
|
|
12043
12992
|
model: state.model,
|
|
12044
12993
|
width,
|
|
12045
12994
|
height,
|
|
@@ -12200,6 +13149,9 @@ function createWatchIo() {
|
|
|
12200
13149
|
});
|
|
12201
13150
|
process.stdout.on("resize", () => resizeHandler?.());
|
|
12202
13151
|
return {
|
|
13152
|
+
isTty() {
|
|
13153
|
+
return process.stdin.isTTY === true;
|
|
13154
|
+
},
|
|
12203
13155
|
onKey(handler) {
|
|
12204
13156
|
keyHandler = handler;
|
|
12205
13157
|
},
|
|
@@ -12232,23 +13184,15 @@ function createWatchIo() {
|
|
|
12232
13184
|
}
|
|
12233
13185
|
|
|
12234
13186
|
// src/cli/watch/watch.ts
|
|
12235
|
-
var
|
|
12236
|
-
var
|
|
13187
|
+
var CLEAR_SCREEN2 = "\x1B[2J\x1B[H";
|
|
13188
|
+
var QUIT_KEYS2 = ["q", "", ""];
|
|
12237
13189
|
function reportErrors(errors) {
|
|
12238
13190
|
errors.forEach((error) => process.stderr.write(`pair-mode: ${error.message}
|
|
12239
13191
|
`));
|
|
12240
13192
|
}
|
|
12241
13193
|
function paintIdle(io, status, truecolor) {
|
|
12242
13194
|
const { width } = io.size();
|
|
12243
|
-
io.write(
|
|
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
|
-
});
|
|
13195
|
+
io.write(CLEAR_SCREEN2 + renderIdle(status, width, truecolor).join("\r\n") + "\r\n");
|
|
12252
13196
|
}
|
|
12253
13197
|
async function optionsFor(review, config, io, resultFile) {
|
|
12254
13198
|
const truecolor = supportsTruecolor(process.env);
|
|
@@ -12275,24 +13219,29 @@ async function optionsFor(review, config, io, resultFile) {
|
|
|
12275
13219
|
};
|
|
12276
13220
|
}
|
|
12277
13221
|
async function runWatch(options, config) {
|
|
12278
|
-
const socketPath = options.
|
|
13222
|
+
const socketPath = watchSocketPath(options.directory, options.sessionKey, options.socketPath);
|
|
12279
13223
|
let errors = [];
|
|
12280
|
-
const
|
|
13224
|
+
const owns = (await probeSession(socketPath)).status === "refused";
|
|
13225
|
+
const host = owns ? await ownerHost({
|
|
12281
13226
|
socketPath,
|
|
13227
|
+
client: "tui",
|
|
13228
|
+
record: buildSessionRecord(options, socketPath),
|
|
12282
13229
|
onError: (error) => {
|
|
12283
13230
|
errors = [...errors, error];
|
|
12284
13231
|
}
|
|
12285
|
-
});
|
|
13232
|
+
}) : await viewerHost({ socketPath, client: "tui" });
|
|
13233
|
+
await sweepDeadSessions();
|
|
12286
13234
|
const truecolor = supportsTruecolor(process.env);
|
|
12287
13235
|
const io = options.io ?? createWatchIo();
|
|
12288
|
-
const status = () =>
|
|
12289
|
-
|
|
12290
|
-
|
|
12291
|
-
|
|
12292
|
-
|
|
12293
|
-
|
|
12294
|
-
|
|
12295
|
-
|
|
13236
|
+
const status = () => {
|
|
13237
|
+
const counts = host.counts();
|
|
13238
|
+
return {
|
|
13239
|
+
directory: options.directory,
|
|
13240
|
+
socketPath,
|
|
13241
|
+
clients: counts.clients,
|
|
13242
|
+
waiting: counts.waiting
|
|
13243
|
+
};
|
|
13244
|
+
};
|
|
12296
13245
|
const pending = [];
|
|
12297
13246
|
const cancelled = /* @__PURE__ */ new Set();
|
|
12298
13247
|
const aborts = /* @__PURE__ */ new Map();
|
|
@@ -12306,37 +13255,45 @@ async function runWatch(options, config) {
|
|
|
12306
13255
|
};
|
|
12307
13256
|
const listenIdle = () => {
|
|
12308
13257
|
io.onKey((chunk) => {
|
|
12309
|
-
if (
|
|
13258
|
+
if (QUIT_KEYS2.includes(chunk)) {
|
|
12310
13259
|
quitting = true;
|
|
12311
13260
|
nudge();
|
|
12312
13261
|
}
|
|
12313
13262
|
});
|
|
12314
13263
|
io.onResize(() => paintIdle(io, status(), truecolor));
|
|
12315
13264
|
};
|
|
12316
|
-
|
|
12317
|
-
|
|
12318
|
-
|
|
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
|
-
});
|
|
13265
|
+
host.onReview((review) => {
|
|
13266
|
+
pending.push(review);
|
|
13267
|
+
nudge();
|
|
12330
13268
|
});
|
|
12331
|
-
|
|
13269
|
+
host.onCancel((id) => {
|
|
13270
|
+
const queuedIndex = pending.findIndex((review) => review.id === id);
|
|
13271
|
+
if (queuedIndex !== -1) {
|
|
13272
|
+
pending.splice(queuedIndex, 1);
|
|
13273
|
+
nudge();
|
|
13274
|
+
return;
|
|
13275
|
+
}
|
|
13276
|
+
if (aborts.has(id)) {
|
|
13277
|
+
cancelled.add(id);
|
|
13278
|
+
aborts.get(id)?.abort();
|
|
13279
|
+
nudge();
|
|
13280
|
+
}
|
|
13281
|
+
});
|
|
13282
|
+
host.onChange(() => {
|
|
12332
13283
|
if (!busy && !quitting) {
|
|
12333
13284
|
paintIdle(io, status(), truecolor);
|
|
12334
13285
|
}
|
|
12335
13286
|
});
|
|
12336
|
-
|
|
13287
|
+
host.onClose(() => {
|
|
13288
|
+
quitting = true;
|
|
13289
|
+
aborts.forEach((abort) => abort.abort());
|
|
13290
|
+
errors = [...errors, new Error("the session owner exited")];
|
|
13291
|
+
nudge();
|
|
13292
|
+
});
|
|
12337
13293
|
while (!quitting) {
|
|
12338
13294
|
const review = pending.shift();
|
|
12339
13295
|
if (review === void 0) {
|
|
13296
|
+
host.refreshCounts();
|
|
12340
13297
|
listenIdle();
|
|
12341
13298
|
paintIdle(io, status(), truecolor);
|
|
12342
13299
|
await new Promise((resolve3) => {
|
|
@@ -12352,8 +13309,7 @@ async function runWatch(options, config) {
|
|
|
12352
13309
|
const tuiOptions = await optionsFor(review, config, io, resultFile);
|
|
12353
13310
|
const result = await runTui(tuiOptions, io, abort.signal);
|
|
12354
13311
|
if (!cancelled.has(review.id)) {
|
|
12355
|
-
|
|
12356
|
-
client.write(encode({ type: "verdict", id: review.id, questions }));
|
|
13312
|
+
host.verdict(review.id, result.quit === "send" ? result.questions : []);
|
|
12357
13313
|
}
|
|
12358
13314
|
} finally {
|
|
12359
13315
|
aborts.delete(review.id);
|
|
@@ -12362,10 +13318,9 @@ async function runWatch(options, config) {
|
|
|
12362
13318
|
busy = false;
|
|
12363
13319
|
}
|
|
12364
13320
|
}
|
|
12365
|
-
io.write(
|
|
13321
|
+
io.write(CLEAR_SCREEN2);
|
|
12366
13322
|
io.shutdown();
|
|
12367
|
-
|
|
12368
|
-
await server.close();
|
|
13323
|
+
await host.close();
|
|
12369
13324
|
reportErrors(errors);
|
|
12370
13325
|
return 0;
|
|
12371
13326
|
}
|
|
@@ -12601,7 +13556,7 @@ function runConfig(args, path) {
|
|
|
12601
13556
|
// src/web/server.ts
|
|
12602
13557
|
import { createServer as createServer2 } from "node:http";
|
|
12603
13558
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
12604
|
-
import { existsSync as
|
|
13559
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "node:fs";
|
|
12605
13560
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
12606
13561
|
|
|
12607
13562
|
// src/web/client/bundle.ts
|
|
@@ -13532,12 +14487,17 @@ function post(payload) {
|
|
|
13532
14487
|
.then((response) => {
|
|
13533
14488
|
// A refused verdict means another client already answered, so the notes typed here can never land.
|
|
13534
14489
|
if (!response.ok) {
|
|
13535
|
-
|
|
14490
|
+
if (review !== null && review.id === payload.id) {
|
|
14491
|
+
clearReview();
|
|
14492
|
+
}
|
|
13536
14493
|
warn("this review was already answered elsewhere - your notes were not sent");
|
|
13537
14494
|
return;
|
|
13538
14495
|
}
|
|
13539
14496
|
|
|
13540
|
-
|
|
14497
|
+
// Only clear if the server answered this review, not one that arrived via SSE meanwhile.
|
|
14498
|
+
if (review !== null && review.id === payload.id) {
|
|
14499
|
+
clearReview();
|
|
14500
|
+
}
|
|
13541
14501
|
})
|
|
13542
14502
|
.catch(() => {
|
|
13543
14503
|
sendButton.disabled = notes.length === 0;
|
|
@@ -13552,10 +14512,16 @@ approveButton.addEventListener("click", () => post({ id: review.id, notes: [] })
|
|
|
13552
14512
|
const events = new EventSource("/r/" + token + "/events");
|
|
13553
14513
|
|
|
13554
14514
|
events.addEventListener("review", (event) => {
|
|
13555
|
-
|
|
13556
|
-
|
|
13557
|
-
|
|
13558
|
-
|
|
14515
|
+
const incoming = JSON.parse(event.data);
|
|
14516
|
+
|
|
14517
|
+
// Only reset if it is a different review.
|
|
14518
|
+
if (review === null || review.id !== incoming.id) {
|
|
14519
|
+
notes = [];
|
|
14520
|
+
expanded = new Set();
|
|
14521
|
+
hidePopup();
|
|
14522
|
+
}
|
|
14523
|
+
|
|
14524
|
+
review = incoming;
|
|
13559
14525
|
render();
|
|
13560
14526
|
});
|
|
13561
14527
|
|
|
@@ -13668,7 +14634,7 @@ var MAX_BODY_BYTES = 1e6;
|
|
|
13668
14634
|
function readAsset(name) {
|
|
13669
14635
|
const bundled = fileURLToPath4(new URL(`../assets/${name}`, import.meta.url));
|
|
13670
14636
|
const source = fileURLToPath4(new URL(`../../assets/${name}`, import.meta.url));
|
|
13671
|
-
return
|
|
14637
|
+
return readFileSync8(existsSync11(bundled) ? bundled : source);
|
|
13672
14638
|
}
|
|
13673
14639
|
var IMAGES = {
|
|
13674
14640
|
"favicon.png": readAsset("favicon.png"),
|
|
@@ -13751,7 +14717,7 @@ function startWebServer(options) {
|
|
|
13751
14717
|
const token = options.token ?? defaultToken();
|
|
13752
14718
|
const base = `/r/${token}`;
|
|
13753
14719
|
const viewers = /* @__PURE__ */ new Set();
|
|
13754
|
-
let
|
|
14720
|
+
let pending = [];
|
|
13755
14721
|
function sendEvent(response, event, data) {
|
|
13756
14722
|
response.write(`event: ${event}
|
|
13757
14723
|
data: ${data}
|
|
@@ -13768,14 +14734,30 @@ data: ${data}
|
|
|
13768
14734
|
response.write(": open\n\n");
|
|
13769
14735
|
viewers.add(response);
|
|
13770
14736
|
response.on("close", () => viewers.delete(response));
|
|
13771
|
-
|
|
13772
|
-
|
|
14737
|
+
const open = pending[0];
|
|
14738
|
+
if (!open) {
|
|
14739
|
+
return;
|
|
13773
14740
|
}
|
|
14741
|
+
sendEvent(response, "review", JSON.stringify(open));
|
|
13774
14742
|
}
|
|
13775
14743
|
function broadcastCancel(id) {
|
|
13776
14744
|
const data = JSON.stringify({ id });
|
|
13777
14745
|
viewers.forEach((viewer) => sendEvent(viewer, "cancel", data));
|
|
13778
14746
|
}
|
|
14747
|
+
function broadcastReview(review) {
|
|
14748
|
+
const data = JSON.stringify(review);
|
|
14749
|
+
viewers.forEach((viewer) => sendEvent(viewer, "review", data));
|
|
14750
|
+
}
|
|
14751
|
+
function retire(id) {
|
|
14752
|
+
const wasOpen = pending[0]?.id === id;
|
|
14753
|
+
pending = pending.filter((review) => review.id !== id);
|
|
14754
|
+
broadcastCancel(id);
|
|
14755
|
+
const next = pending[0];
|
|
14756
|
+
if (!wasOpen || !next) {
|
|
14757
|
+
return;
|
|
14758
|
+
}
|
|
14759
|
+
broadcastReview(next);
|
|
14760
|
+
}
|
|
13779
14761
|
async function handleVerdict(request, response) {
|
|
13780
14762
|
const result = await readBody(request);
|
|
13781
14763
|
if (result.kind === "too-large") {
|
|
@@ -13791,13 +14773,12 @@ data: ${data}
|
|
|
13791
14773
|
response.writeHead(BAD_REQUEST).end();
|
|
13792
14774
|
return;
|
|
13793
14775
|
}
|
|
13794
|
-
const answered =
|
|
13795
|
-
if (answered
|
|
14776
|
+
const answered = pending[0];
|
|
14777
|
+
if (!answered || answered.id !== verdict.id) {
|
|
13796
14778
|
response.writeHead(CONFLICT, { "content-type": "application/json" }).end("{}");
|
|
13797
14779
|
return;
|
|
13798
14780
|
}
|
|
13799
|
-
|
|
13800
|
-
broadcastCancel(verdict.id);
|
|
14781
|
+
retire(verdict.id);
|
|
13801
14782
|
options.onVerdict(verdict.id, webNotesToQuestions(answered, verdict.notes));
|
|
13802
14783
|
response.writeHead(OK, { "content-type": "application/json" }).end("{}");
|
|
13803
14784
|
}
|
|
@@ -13809,7 +14790,7 @@ data: ${data}
|
|
|
13809
14790
|
return;
|
|
13810
14791
|
}
|
|
13811
14792
|
const image = url.startsWith(`${base}/`) ? IMAGES[url.slice(base.length + 1)] : void 0;
|
|
13812
|
-
if (image
|
|
14793
|
+
if (image && request.method === "GET") {
|
|
13813
14794
|
response.writeHead(OK, { "content-type": "image/png" });
|
|
13814
14795
|
response.end(image);
|
|
13815
14796
|
return;
|
|
@@ -13838,21 +14819,20 @@ data: ${data}
|
|
|
13838
14819
|
return viewers.size;
|
|
13839
14820
|
},
|
|
13840
14821
|
offer(review) {
|
|
13841
|
-
|
|
13842
|
-
|
|
13843
|
-
|
|
14822
|
+
pending = [...pending, review];
|
|
14823
|
+
if (pending.length === 1) {
|
|
14824
|
+
broadcastReview(review);
|
|
14825
|
+
}
|
|
13844
14826
|
},
|
|
13845
14827
|
// A withdrawal for an older review must leave the one now open alone.
|
|
13846
14828
|
withdraw(id) {
|
|
13847
|
-
|
|
13848
|
-
current = null;
|
|
13849
|
-
}
|
|
13850
|
-
broadcastCancel(id);
|
|
14829
|
+
retire(id);
|
|
13851
14830
|
},
|
|
13852
14831
|
close() {
|
|
13853
14832
|
return new Promise((done) => {
|
|
13854
14833
|
viewers.forEach((viewer) => viewer.end());
|
|
13855
14834
|
viewers.clear();
|
|
14835
|
+
pending = [];
|
|
13856
14836
|
server.close(() => done());
|
|
13857
14837
|
});
|
|
13858
14838
|
}
|
|
@@ -13862,8 +14842,7 @@ data: ${data}
|
|
|
13862
14842
|
}
|
|
13863
14843
|
|
|
13864
14844
|
// src/web/watch.ts
|
|
13865
|
-
import {
|
|
13866
|
-
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "node:fs";
|
|
14845
|
+
import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "node:fs";
|
|
13867
14846
|
import { dirname as dirname8 } from "node:path";
|
|
13868
14847
|
|
|
13869
14848
|
// src/web/review.ts
|
|
@@ -13892,73 +14871,105 @@ async function toWebReview(review, config) {
|
|
|
13892
14871
|
}
|
|
13893
14872
|
|
|
13894
14873
|
// src/web/watch.ts
|
|
14874
|
+
var OWNER_ONLY_DIR3 = 448;
|
|
14875
|
+
var OWNER_ONLY_FILE3 = 384;
|
|
13895
14876
|
function publishUrl(path, url) {
|
|
13896
|
-
mkdirSync7(dirname8(path), { recursive: true });
|
|
13897
|
-
|
|
13898
|
-
|
|
13899
|
-
|
|
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));
|
|
14877
|
+
mkdirSync7(dirname8(path), { recursive: true, mode: OWNER_ONLY_DIR3 });
|
|
14878
|
+
writeFileSync8(path, JSON.stringify({ url, pid: process.pid }) + "\n", {
|
|
14879
|
+
encoding: "utf-8",
|
|
14880
|
+
mode: OWNER_ONLY_FILE3
|
|
13905
14881
|
});
|
|
13906
14882
|
}
|
|
14883
|
+
function linkPid(path) {
|
|
14884
|
+
try {
|
|
14885
|
+
const parsed = JSON.parse(readFileSync9(path, "utf-8"));
|
|
14886
|
+
return isRecord(parsed) && typeof parsed["pid"] === "number" ? parsed["pid"] : null;
|
|
14887
|
+
} catch {
|
|
14888
|
+
return null;
|
|
14889
|
+
}
|
|
14890
|
+
}
|
|
13907
14891
|
async function startWebWatch(options, config) {
|
|
13908
|
-
const socketPath = options.
|
|
13909
|
-
const
|
|
13910
|
-
const
|
|
14892
|
+
const socketPath = watchSocketPath(options.directory, options.sessionKey, options.socketPath);
|
|
14893
|
+
const owns = (await probeSession(socketPath)).status === "refused";
|
|
14894
|
+
const host = owns ? await ownerHost({
|
|
14895
|
+
socketPath,
|
|
14896
|
+
client: "web",
|
|
14897
|
+
record: buildSessionRecord(options, socketPath)
|
|
14898
|
+
}) : await viewerHost({ socketPath, client: "web" });
|
|
14899
|
+
const ownedIds = /* @__PURE__ */ new Set();
|
|
13911
14900
|
const web = await startWebServer({
|
|
13912
14901
|
port: options.port,
|
|
13913
14902
|
token: options.token,
|
|
13914
14903
|
layout: config.layout,
|
|
13915
14904
|
onVerdict(id, questions) {
|
|
13916
|
-
|
|
14905
|
+
ownedIds.delete(id);
|
|
14906
|
+
host.verdict(id, questions);
|
|
13917
14907
|
}
|
|
13918
14908
|
});
|
|
13919
|
-
|
|
13920
|
-
|
|
13921
|
-
|
|
13922
|
-
|
|
13923
|
-
|
|
13924
|
-
|
|
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;
|
|
14909
|
+
host.onReview((message) => {
|
|
14910
|
+
const id = message.id;
|
|
14911
|
+
ownedIds.add(id);
|
|
14912
|
+
void toWebReview(message, config).then((review) => {
|
|
14913
|
+
if (ownedIds.has(id)) {
|
|
14914
|
+
web.offer(review);
|
|
13937
14915
|
}
|
|
13938
|
-
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
}
|
|
13942
|
-
web.withdraw(message.id);
|
|
14916
|
+
}).catch(() => {
|
|
14917
|
+
if (ownedIds.delete(id)) {
|
|
14918
|
+
host.verdict(id, []);
|
|
13943
14919
|
}
|
|
13944
14920
|
});
|
|
13945
14921
|
});
|
|
13946
|
-
|
|
13947
|
-
|
|
13948
|
-
|
|
14922
|
+
host.onCancel((id) => {
|
|
14923
|
+
ownedIds.delete(id);
|
|
14924
|
+
web.withdraw(id);
|
|
14925
|
+
});
|
|
14926
|
+
const urlPath = watchUrlPath(options.directory, options.sessionKey);
|
|
14927
|
+
const published = owns || !existsSync12(urlPath);
|
|
14928
|
+
if (published) {
|
|
14929
|
+
publishUrl(urlPath, web.url);
|
|
14930
|
+
}
|
|
13949
14931
|
return {
|
|
13950
14932
|
url: web.url,
|
|
13951
14933
|
socketPath,
|
|
13952
14934
|
port: web.port,
|
|
14935
|
+
owns,
|
|
13953
14936
|
async close() {
|
|
13954
|
-
|
|
13955
|
-
|
|
14937
|
+
if (published && linkPid(urlPath) === process.pid) {
|
|
14938
|
+
removeQuietly(urlPath);
|
|
14939
|
+
}
|
|
13956
14940
|
await web.close();
|
|
13957
|
-
await
|
|
14941
|
+
await host.close();
|
|
13958
14942
|
}
|
|
13959
14943
|
};
|
|
13960
14944
|
}
|
|
13961
14945
|
|
|
14946
|
+
// src/cli/watch-target.ts
|
|
14947
|
+
function usesWebWatcher(target, webEnabled) {
|
|
14948
|
+
return target.web || webEnabled;
|
|
14949
|
+
}
|
|
14950
|
+
async function watchSession(target) {
|
|
14951
|
+
const { directory, sessionKey: sessionKey2, socketPath } = target;
|
|
14952
|
+
const { config, errors } = loadConfig();
|
|
14953
|
+
errors.forEach((error) => console.error(`config ${error.path}: ${error.message}`));
|
|
14954
|
+
if (!usesWebWatcher(target, config.web.enabled)) {
|
|
14955
|
+
return await runWatch({ directory, sessionKey: sessionKey2, socketPath }, config);
|
|
14956
|
+
}
|
|
14957
|
+
const watcher = await startWebWatch(
|
|
14958
|
+
{ directory, sessionKey: sessionKey2, socketPath, port: config.web.port },
|
|
14959
|
+
config
|
|
14960
|
+
);
|
|
14961
|
+
console.log(`pair mode is watching ${directory}`);
|
|
14962
|
+
console.log(watcher.url);
|
|
14963
|
+
await new Promise((done) => {
|
|
14964
|
+
const stop = () => {
|
|
14965
|
+
void watcher.close().then(done);
|
|
14966
|
+
};
|
|
14967
|
+
process.once("SIGINT", stop);
|
|
14968
|
+
process.once("SIGTERM", stop);
|
|
14969
|
+
});
|
|
14970
|
+
return 0;
|
|
14971
|
+
}
|
|
14972
|
+
|
|
13962
14973
|
// src/cli/index.ts
|
|
13963
14974
|
var USAGE = `pair-mode <command> [directory]
|
|
13964
14975
|
|
|
@@ -13971,16 +14982,21 @@ Commands:
|
|
|
13971
14982
|
on [dir] turn pair mode on for a directory (default: cwd)
|
|
13972
14983
|
on --web [dir] turn pair mode on and serve the review in a browser
|
|
13973
14984
|
off [dir] turn pair mode off for a directory (default: cwd)
|
|
14985
|
+
toggle [dir] flip pair mode for a directory (default: cwd)
|
|
14986
|
+
toggle --web [dir] flip pair mode, and serve the review in a browser when it turns on
|
|
13974
14987
|
status [dir] report pair mode status for a directory (default: cwd)
|
|
13975
14988
|
watch [dir] review edits in this terminal (default: cwd)
|
|
13976
14989
|
watch --web [dir] serve the review in a browser and print the link
|
|
14990
|
+
watch <id> review edits for one session (see: pair-mode sessions)
|
|
14991
|
+
sessions list every live pair mode session
|
|
14992
|
+
connect pick a session from a list and watch it
|
|
13977
14993
|
--version print the installed version
|
|
13978
14994
|
--help print this message
|
|
13979
14995
|
`;
|
|
13980
14996
|
function readVersion() {
|
|
13981
|
-
const pkgPath =
|
|
14997
|
+
const pkgPath = join15(installRoot(), "package.json");
|
|
13982
14998
|
try {
|
|
13983
|
-
const raw = JSON.parse(
|
|
14999
|
+
const raw = JSON.parse(readFileSync10(pkgPath, "utf-8"));
|
|
13984
15000
|
if (isRecord(raw) && typeof raw["version"] === "string") {
|
|
13985
15001
|
return raw["version"];
|
|
13986
15002
|
}
|
|
@@ -14000,6 +15016,33 @@ function parseDirectoryArgs(args, allowedFlags) {
|
|
|
14000
15016
|
unknownFlag: flags.find((flag) => !allowedFlags.includes(flag)) ?? null
|
|
14001
15017
|
};
|
|
14002
15018
|
}
|
|
15019
|
+
var SESSION_KEY_PATTERN = /^s-[0-9a-f]{8}$/;
|
|
15020
|
+
var SESSION_KEY_PREFIX = "s-";
|
|
15021
|
+
function parseWatchArgs(args) {
|
|
15022
|
+
const flags = args.filter(isFlag);
|
|
15023
|
+
const target = args.find((entry) => !isFlag(entry));
|
|
15024
|
+
const looksLikeKey = target !== void 0 && target.startsWith(SESSION_KEY_PREFIX);
|
|
15025
|
+
const isKey = looksLikeKey && SESSION_KEY_PATTERN.test(target);
|
|
15026
|
+
return {
|
|
15027
|
+
sessionKey: isKey ? target : void 0,
|
|
15028
|
+
malformedKey: looksLikeKey && !isKey ? target : null,
|
|
15029
|
+
directory: isKey ? process.cwd() : resolve2(target ?? process.cwd()),
|
|
15030
|
+
web: flags.includes("--web"),
|
|
15031
|
+
unknownFlag: flags.find((flag) => flag !== "--web") ?? null
|
|
15032
|
+
};
|
|
15033
|
+
}
|
|
15034
|
+
function reportExtraArgs(command, args) {
|
|
15035
|
+
const extra = args[0];
|
|
15036
|
+
if (extra === void 0) {
|
|
15037
|
+
return null;
|
|
15038
|
+
}
|
|
15039
|
+
if (isFlag(extra)) {
|
|
15040
|
+
return reportUnknownFlag(command, extra);
|
|
15041
|
+
}
|
|
15042
|
+
console.error(`unexpected argument for ${command}: ${extra}`);
|
|
15043
|
+
console.error(USAGE);
|
|
15044
|
+
return 1;
|
|
15045
|
+
}
|
|
14003
15046
|
function reportUnknownFlag(command, flag) {
|
|
14004
15047
|
console.error(`unknown option for ${command}: ${flag}`);
|
|
14005
15048
|
console.error(USAGE);
|
|
@@ -14034,11 +15077,13 @@ async function main() {
|
|
|
14034
15077
|
if (parsed.unknownFlag !== null) {
|
|
14035
15078
|
return reportUnknownFlag(command, parsed.unknownFlag);
|
|
14036
15079
|
}
|
|
15080
|
+
await sweepDeadSessions();
|
|
15081
|
+
const key = currentSessionKey();
|
|
14037
15082
|
if (parsed.web) {
|
|
14038
|
-
console.log(await pairOnWeb(parsed.directory, process.argv[1] ?? ""));
|
|
15083
|
+
console.log(await pairOnWeb(parsed.directory, process.argv[1] ?? "", key));
|
|
14039
15084
|
return 0;
|
|
14040
15085
|
}
|
|
14041
|
-
console.log(pairOn(parsed.directory));
|
|
15086
|
+
console.log(pairOn(parsed.directory, key));
|
|
14042
15087
|
return 0;
|
|
14043
15088
|
}
|
|
14044
15089
|
if (command === "off") {
|
|
@@ -14046,7 +15091,17 @@ async function main() {
|
|
|
14046
15091
|
if (parsed.unknownFlag !== null) {
|
|
14047
15092
|
return reportUnknownFlag(command, parsed.unknownFlag);
|
|
14048
15093
|
}
|
|
14049
|
-
console.log(pairOff(parsed.directory));
|
|
15094
|
+
console.log(pairOff(parsed.directory, currentSessionKey()));
|
|
15095
|
+
return 0;
|
|
15096
|
+
}
|
|
15097
|
+
if (command === "toggle") {
|
|
15098
|
+
const parsed = parseDirectoryArgs(process.argv.slice(3), ["--web"]);
|
|
15099
|
+
if (parsed.unknownFlag !== null) {
|
|
15100
|
+
return reportUnknownFlag(command, parsed.unknownFlag);
|
|
15101
|
+
}
|
|
15102
|
+
console.log(
|
|
15103
|
+
await pairToggle(parsed.directory, process.argv[1] ?? "", parsed.web, currentSessionKey())
|
|
15104
|
+
);
|
|
14050
15105
|
return 0;
|
|
14051
15106
|
}
|
|
14052
15107
|
if (command === "status") {
|
|
@@ -14054,32 +15109,52 @@ async function main() {
|
|
|
14054
15109
|
if (parsed.unknownFlag !== null) {
|
|
14055
15110
|
return reportUnknownFlag(command, parsed.unknownFlag);
|
|
14056
15111
|
}
|
|
14057
|
-
console.log(pairStatus(parsed.directory));
|
|
15112
|
+
console.log(pairStatus(parsed.directory, currentSessionKey()));
|
|
14058
15113
|
return 0;
|
|
14059
15114
|
}
|
|
14060
15115
|
if (command === "watch") {
|
|
14061
|
-
const parsed =
|
|
15116
|
+
const parsed = parseWatchArgs(process.argv.slice(3));
|
|
14062
15117
|
if (parsed.unknownFlag !== null) {
|
|
14063
15118
|
return reportUnknownFlag(command, parsed.unknownFlag);
|
|
14064
15119
|
}
|
|
14065
|
-
|
|
14066
|
-
|
|
14067
|
-
|
|
14068
|
-
|
|
14069
|
-
|
|
14070
|
-
return
|
|
14071
|
-
}
|
|
14072
|
-
|
|
14073
|
-
|
|
14074
|
-
|
|
14075
|
-
|
|
14076
|
-
|
|
14077
|
-
|
|
14078
|
-
|
|
14079
|
-
|
|
14080
|
-
|
|
15120
|
+
if (parsed.malformedKey !== null) {
|
|
15121
|
+
console.error(`malformed session id: ${parsed.malformedKey}`);
|
|
15122
|
+
console.error(
|
|
15123
|
+
"an id is s- followed by eight hex characters; run pair-mode sessions to list them"
|
|
15124
|
+
);
|
|
15125
|
+
return 1;
|
|
15126
|
+
}
|
|
15127
|
+
return await watchSession({
|
|
15128
|
+
directory: parsed.directory,
|
|
15129
|
+
sessionKey: parsed.sessionKey,
|
|
15130
|
+
web: parsed.web
|
|
15131
|
+
});
|
|
15132
|
+
}
|
|
15133
|
+
if (command === "sessions") {
|
|
15134
|
+
const rejected = reportExtraArgs(command, process.argv.slice(3));
|
|
15135
|
+
if (rejected !== null) {
|
|
15136
|
+
return rejected;
|
|
15137
|
+
}
|
|
15138
|
+
const result = await listSessions();
|
|
15139
|
+
console.log(result.text);
|
|
15140
|
+
return result.exitCode;
|
|
15141
|
+
}
|
|
15142
|
+
if (command === "connect") {
|
|
15143
|
+
const rejected = reportExtraArgs(command, process.argv.slice(3));
|
|
15144
|
+
if (rejected !== null) {
|
|
15145
|
+
return rejected;
|
|
15146
|
+
}
|
|
15147
|
+
const result = await runConnect(createWatchIo());
|
|
15148
|
+
const chosen = result.selected;
|
|
15149
|
+
if (chosen === null) {
|
|
15150
|
+
return result.exitCode;
|
|
15151
|
+
}
|
|
15152
|
+
return await watchSession({
|
|
15153
|
+
directory: chosen.directory === "" ? process.cwd() : chosen.directory,
|
|
15154
|
+
sessionKey: chosen.kind === "session" ? chosen.id : void 0,
|
|
15155
|
+
socketPath: join15(sessionsDir(), `${chosen.id}.sock`),
|
|
15156
|
+
web: false
|
|
14081
15157
|
});
|
|
14082
|
-
return 0;
|
|
14083
15158
|
}
|
|
14084
15159
|
console.error(`unknown command: ${command}`);
|
|
14085
15160
|
console.error(USAGE);
|