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