@delorenj/pjangler 1.5.40 โ 1.5.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +493 -53
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6064,8 +6064,8 @@ var init_capture = __esm({
|
|
|
6064
6064
|
|
|
6065
6065
|
// src/index.ts
|
|
6066
6066
|
import { spawnSync as spawnSync18 } from "node:child_process";
|
|
6067
|
-
import { existsSync as existsSync26, readdirSync as
|
|
6068
|
-
import { basename as
|
|
6067
|
+
import { existsSync as existsSync26, readdirSync as readdirSync14, readFileSync as readFileSync32, statSync as statSync9 } from "node:fs";
|
|
6068
|
+
import { basename as basename12, join as join37, resolve as resolve25 } from "node:path";
|
|
6069
6069
|
import { Command as Command3, CommanderError, Option } from "commander";
|
|
6070
6070
|
|
|
6071
6071
|
// src/recipes/Recipe.ts
|
|
@@ -21155,7 +21155,7 @@ var ZCRANK_LOG_CAP = 500;
|
|
|
21155
21155
|
var TAB_STATUS_SUFFIX = /[\s๐ข๐ก๐ด๐โซโณโปโยทโ]+$/u;
|
|
21156
21156
|
var defaultShell = (command, args) => {
|
|
21157
21157
|
const result2 = spawnSync17(command, args, { encoding: "utf8", timeout: 1e4 });
|
|
21158
|
-
if (result2.error) return { stdout: "", status: 127 };
|
|
21158
|
+
if (result2.error) return { stdout: "", status: result2.error.code === "ETIMEDOUT" ? 124 : 127 };
|
|
21159
21159
|
return { stdout: result2.stdout ?? "", status: result2.status ?? 1, stderr: result2.stderr ?? "" };
|
|
21160
21160
|
};
|
|
21161
21161
|
function tabDisplay(name) {
|
|
@@ -21382,48 +21382,43 @@ function liveTabs(deps = {}) {
|
|
|
21382
21382
|
return [];
|
|
21383
21383
|
}
|
|
21384
21384
|
}
|
|
21385
|
-
function
|
|
21386
|
-
|
|
21387
|
-
|
|
21388
|
-
"-u",
|
|
21389
|
-
"ZELLIJ",
|
|
21390
|
-
"-u",
|
|
21391
|
-
"ZELLIJ_SESSION_NAME",
|
|
21392
|
-
"timeout",
|
|
21393
|
-
"8s",
|
|
21394
|
-
"zellij",
|
|
21395
|
-
"-s",
|
|
21396
|
-
ZCRANK_SESSION,
|
|
21397
|
-
"action",
|
|
21398
|
-
"list-panes",
|
|
21399
|
-
"--json",
|
|
21400
|
-
"--all"
|
|
21401
|
-
]);
|
|
21402
|
-
if (result2.status !== 0 || !result2.stdout.trim()) return [];
|
|
21385
|
+
function parseListPanes(stdout) {
|
|
21386
|
+
if (!stdout.trim()) return null;
|
|
21387
|
+
let parsed;
|
|
21403
21388
|
try {
|
|
21404
|
-
|
|
21405
|
-
if (!Array.isArray(parsed)) return [];
|
|
21406
|
-
const panes = [];
|
|
21407
|
-
for (const entry of parsed) {
|
|
21408
|
-
if (typeof entry !== "object" || entry === null) continue;
|
|
21409
|
-
const e = entry;
|
|
21410
|
-
const id = Number(e.id);
|
|
21411
|
-
if (!Number.isFinite(id)) continue;
|
|
21412
|
-
const tabId = Number(e.tab_id);
|
|
21413
|
-
const tabPosition = Number(e.tab_position);
|
|
21414
|
-
panes.push({
|
|
21415
|
-
id,
|
|
21416
|
-
tabId: e.tab_id !== void 0 && e.tab_id !== null && Number.isInteger(tabId) ? tabId : void 0,
|
|
21417
|
-
tabPosition: e.tab_position !== void 0 && e.tab_position !== null && Number.isInteger(tabPosition) ? tabPosition : void 0,
|
|
21418
|
-
tabName: String(e.tab_name ?? ""),
|
|
21419
|
-
isPlugin: e.is_plugin === true,
|
|
21420
|
-
cwd: typeof e.pane_cwd === "string" && e.pane_cwd.trim() ? e.pane_cwd : void 0
|
|
21421
|
-
});
|
|
21422
|
-
}
|
|
21423
|
-
return panes;
|
|
21389
|
+
parsed = JSON.parse(stdout);
|
|
21424
21390
|
} catch {
|
|
21425
|
-
return
|
|
21391
|
+
return null;
|
|
21426
21392
|
}
|
|
21393
|
+
if (!Array.isArray(parsed)) return null;
|
|
21394
|
+
const panes = [];
|
|
21395
|
+
for (const entry of parsed) {
|
|
21396
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
21397
|
+
const e = entry;
|
|
21398
|
+
const id = Number(e.id);
|
|
21399
|
+
if (!Number.isFinite(id)) continue;
|
|
21400
|
+
const tabId = Number(e.tab_id);
|
|
21401
|
+
const tabPosition = Number(e.tab_position);
|
|
21402
|
+
panes.push({
|
|
21403
|
+
id,
|
|
21404
|
+
tabId: e.tab_id !== void 0 && e.tab_id !== null && Number.isInteger(tabId) ? tabId : void 0,
|
|
21405
|
+
tabPosition: e.tab_position !== void 0 && e.tab_position !== null && Number.isInteger(tabPosition) ? tabPosition : void 0,
|
|
21406
|
+
tabName: String(e.tab_name ?? ""),
|
|
21407
|
+
isPlugin: e.is_plugin === true,
|
|
21408
|
+
cwd: typeof e.pane_cwd === "string" && e.pane_cwd.trim() ? e.pane_cwd : void 0,
|
|
21409
|
+
title: typeof e.title === "string" ? e.title : null
|
|
21410
|
+
});
|
|
21411
|
+
}
|
|
21412
|
+
return panes;
|
|
21413
|
+
}
|
|
21414
|
+
function runListPanes(session, shell) {
|
|
21415
|
+
return shell("env", ["-u", "ZELLIJ", "-u", "ZELLIJ_SESSION_NAME", "timeout", "8s", "zellij", "-s", session, "action", "list-panes", "--json", "--all"]);
|
|
21416
|
+
}
|
|
21417
|
+
function livePanes(deps = {}) {
|
|
21418
|
+
const shell = deps.shell ?? defaultShell;
|
|
21419
|
+
const result2 = runListPanes(deps.session ?? ZCRANK_SESSION, shell);
|
|
21420
|
+
if (result2.status !== 0) return [];
|
|
21421
|
+
return (parseListPanes(result2.stdout) ?? []).map(({ title: _title, ...pane }) => pane);
|
|
21427
21422
|
}
|
|
21428
21423
|
function projectIdentity(dir, deps = {}) {
|
|
21429
21424
|
const target = (dir ?? "").trim();
|
|
@@ -21967,6 +21962,420 @@ function createCrankController(deps) {
|
|
|
21967
21962
|
};
|
|
21968
21963
|
}
|
|
21969
21964
|
|
|
21965
|
+
// src/crank/panes.ts
|
|
21966
|
+
import { readFileSync as readFileSync31, readdirSync as readdirSync13, readlinkSync as readlinkSync9 } from "node:fs";
|
|
21967
|
+
import { homedir as homedir16 } from "node:os";
|
|
21968
|
+
import { basename as basename11, join as join36 } from "node:path";
|
|
21969
|
+
var AGENT_CLIS = [
|
|
21970
|
+
"claude",
|
|
21971
|
+
"codex",
|
|
21972
|
+
"copilot",
|
|
21973
|
+
"opencode",
|
|
21974
|
+
"aider",
|
|
21975
|
+
"hermes",
|
|
21976
|
+
"agy",
|
|
21977
|
+
"kimi-code",
|
|
21978
|
+
"kimi-co",
|
|
21979
|
+
"gemini",
|
|
21980
|
+
"openclaw",
|
|
21981
|
+
// Not in Deckard's list: the Kimi binary on the operator box is `~/.kimi-code/bin/kimi` (argv[0]
|
|
21982
|
+
// basename `kimi`), which neither `kimi-code` nor `kimi-co` prefixes. Listed after them so the
|
|
21983
|
+
// reported name stays `kimi-code` where Deckard would say it.
|
|
21984
|
+
"kimi"
|
|
21985
|
+
];
|
|
21986
|
+
function agentCli(exe, argv) {
|
|
21987
|
+
if (argv.includes("mcp-server") || argv.includes(" mcp ")) return null;
|
|
21988
|
+
const byName = AGENT_CLIS.find((pat) => exe === pat || exe.startsWith(pat));
|
|
21989
|
+
if (byName) return byName;
|
|
21990
|
+
const tokens = argv.split(" ").filter(Boolean);
|
|
21991
|
+
if (INTERPRETER.test(exe)) {
|
|
21992
|
+
const program2 = interpretedProgram(exe, tokens);
|
|
21993
|
+
if (!program2) return null;
|
|
21994
|
+
if (program2.module) {
|
|
21995
|
+
const top = (program2.module.split(".")[0] ?? "").replace(/_/g, "-");
|
|
21996
|
+
return AGENT_CLIS.find((pat) => top === pat || PATH_SUFFIXES.some((suffix) => top === `${pat}-${suffix}`)) ?? null;
|
|
21997
|
+
}
|
|
21998
|
+
return PATH_PASS.find(([, re]) => re.test(program2.script))?.[0] ?? null;
|
|
21999
|
+
}
|
|
22000
|
+
const versioned = /\/([^/]+)\/versions\/[^/]+$/.exec(tokens[0] ?? "");
|
|
22001
|
+
return versioned ? AGENT_CLIS.find((pat) => versioned[1] === pat) ?? null : null;
|
|
22002
|
+
}
|
|
22003
|
+
var INTERPRETER = /^(?:node(?:js)?|bun|deno|tsx|python[\d.]*|ruby|perl|bash|sh|zsh|dash|fish|ksh)$/;
|
|
22004
|
+
var SHELL = /^(?:bash|sh|zsh|dash|fish|ksh)$/;
|
|
22005
|
+
var PYTHON = /^python[\d.]*$/;
|
|
22006
|
+
var VALUE_OPTIONS = [
|
|
22007
|
+
[/^(?:node(?:js)?|bun|deno|tsx)$/, /* @__PURE__ */ new Set(["-r", "--require", "--import", "--loader", "--experimental-loader", "-C", "--conditions", "--inspect-port", "--debug-port", "--input-type", "--title", "--env-file", "--disable-warning", "--watch-path", "--stack-size", "--max-old-space-size", "--redirect-warnings", "--openssl-config", "--icu-data-dir", "--tls-cipher-list", "--secure-heap", "--diagnostic-dir", "--report-dir", "--cpu-prof-dir", "--heap-prof-dir"])],
|
|
22008
|
+
[PYTHON, /* @__PURE__ */ new Set(["-W", "-X", "-Q", "--check-hash-based-pycs"])],
|
|
22009
|
+
[SHELL, /* @__PURE__ */ new Set(["-o", "+o", "-O", "+O", "--rcfile", "--init-file"])]
|
|
22010
|
+
];
|
|
22011
|
+
function interpretedProgram(exe, tokens) {
|
|
22012
|
+
const takesValue = VALUE_OPTIONS.find(([re]) => re.test(exe))?.[1] ?? /* @__PURE__ */ new Set();
|
|
22013
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
22014
|
+
const token = tokens[i];
|
|
22015
|
+
if (token === "-m" || PYTHON.test(exe) && /^-[BEIOPRSbdhiqsuvx]*m$/.test(token)) return tokens[i + 1] ? { module: tokens[i + 1], script: "" } : null;
|
|
22016
|
+
if (token === "-e" || token === "-p" || token === "-pe" || token === "-ep" || token === "--eval" || token === "--print") return null;
|
|
22017
|
+
if (SHELL.test(exe) && /^-[a-zA-Z]*c[a-zA-Z]*$/.test(token)) return null;
|
|
22018
|
+
if (PYTHON.test(exe) && /^-[BEIOPRSbdhiqsuvx]*c$/.test(token)) return null;
|
|
22019
|
+
if (token.startsWith("-") || token.startsWith("+")) {
|
|
22020
|
+
if (takesValue.has(token)) i++;
|
|
22021
|
+
continue;
|
|
22022
|
+
}
|
|
22023
|
+
return { script: token };
|
|
22024
|
+
}
|
|
22025
|
+
return null;
|
|
22026
|
+
}
|
|
22027
|
+
var PATH_SUFFIXES = ["code", "cli", "agent"];
|
|
22028
|
+
var PATH_PASS = AGENT_CLIS.map((pat) => {
|
|
22029
|
+
const name = pat.replace(/[-]/g, "\\-");
|
|
22030
|
+
const end = "(?=$|[ /.])";
|
|
22031
|
+
return [pat, new RegExp(`/${name}(?:${end}|-(?:${PATH_SUFFIXES.join("|")})${end})`)];
|
|
22032
|
+
});
|
|
22033
|
+
function parseProcStat(text2) {
|
|
22034
|
+
const close = text2.lastIndexOf(")");
|
|
22035
|
+
const open = text2.indexOf("(");
|
|
22036
|
+
if (close < 0 || open < 0 || open > close) return null;
|
|
22037
|
+
const comm = text2.slice(open + 1, close);
|
|
22038
|
+
const fields = text2.slice(close + 1).trim().split(/\s+/);
|
|
22039
|
+
const state = fields[0];
|
|
22040
|
+
const ppid = Number(fields[1]);
|
|
22041
|
+
const pgrp = Number(fields[2]);
|
|
22042
|
+
const ttyNr = Number(fields[4]);
|
|
22043
|
+
const tpgid = Number(fields[5]);
|
|
22044
|
+
const startTime = fields[19];
|
|
22045
|
+
if (!state || !startTime || ![ppid, pgrp, ttyNr, tpgid].every(Number.isInteger)) return null;
|
|
22046
|
+
return { comm, state, ppid, pgrp, ttyNr, tpgid, startTime };
|
|
22047
|
+
}
|
|
22048
|
+
function readProcStat(pid) {
|
|
22049
|
+
try {
|
|
22050
|
+
return parseProcStat(readFileSync31(`/proc/${pid}/stat`, "latin1"));
|
|
22051
|
+
} catch {
|
|
22052
|
+
return null;
|
|
22053
|
+
}
|
|
22054
|
+
}
|
|
22055
|
+
function readTag(pid) {
|
|
22056
|
+
let raw;
|
|
22057
|
+
try {
|
|
22058
|
+
raw = readFileSync31(`/proc/${pid}/environ`);
|
|
22059
|
+
} catch {
|
|
22060
|
+
return null;
|
|
22061
|
+
}
|
|
22062
|
+
let paneId = null;
|
|
22063
|
+
let session = null;
|
|
22064
|
+
let start = 0;
|
|
22065
|
+
while (start <= raw.length) {
|
|
22066
|
+
let end = raw.indexOf(0, start);
|
|
22067
|
+
if (end < 0) end = raw.length;
|
|
22068
|
+
const eq = raw.indexOf(61, start);
|
|
22069
|
+
if (eq >= 0 && eq < end) {
|
|
22070
|
+
const key = raw.toString("latin1", start, eq);
|
|
22071
|
+
if (key === "ZELLIJ_PANE_ID") {
|
|
22072
|
+
const value = raw.toString("utf8", eq + 1, end);
|
|
22073
|
+
paneId = /^\d+$/.test(value) ? Number(value) : null;
|
|
22074
|
+
} else if (key === "ZELLIJ_SESSION_NAME") {
|
|
22075
|
+
session = raw.toString("utf8", eq + 1, end);
|
|
22076
|
+
}
|
|
22077
|
+
}
|
|
22078
|
+
start = end + 1;
|
|
22079
|
+
}
|
|
22080
|
+
return paneId === null ? null : { paneId, session };
|
|
22081
|
+
}
|
|
22082
|
+
function readArgv(pid) {
|
|
22083
|
+
try {
|
|
22084
|
+
const parts = readFileSync31(`/proc/${pid}/cmdline`, "utf8").split("\0").filter(Boolean);
|
|
22085
|
+
return { argv: parts.join(" "), exe: parts[0] ? basename11(parts[0]) : "" };
|
|
22086
|
+
} catch {
|
|
22087
|
+
return { argv: "", exe: "" };
|
|
22088
|
+
}
|
|
22089
|
+
}
|
|
22090
|
+
var ProcScanner = class {
|
|
22091
|
+
known = /* @__PURE__ */ new Map();
|
|
22092
|
+
enumerate;
|
|
22093
|
+
/**
|
|
22094
|
+
* `pids` replaces the enumeration of /proc: the scanner then looks at exactly those pids and no
|
|
22095
|
+
* others. Production never passes it (the default reads every pid of the box, which is the
|
|
22096
|
+
* point). A test that spawns its own processes passes its own pids, so that processes OTHER
|
|
22097
|
+
* tests or agents have on the box (session-less tagged ones are visible to every scan by
|
|
22098
|
+
* design) cannot change its answer.
|
|
22099
|
+
*/
|
|
22100
|
+
constructor(options = {}) {
|
|
22101
|
+
this.enumerate = options.pids;
|
|
22102
|
+
}
|
|
22103
|
+
/** How many pids the cache holds (negatives included). For the cost report and tests. */
|
|
22104
|
+
cached() {
|
|
22105
|
+
return this.known.size;
|
|
22106
|
+
}
|
|
22107
|
+
/** Is `pid` in the cache, as a tagged process or a remembered negative? */
|
|
22108
|
+
has(pid) {
|
|
22109
|
+
return this.known.has(pid);
|
|
22110
|
+
}
|
|
22111
|
+
/**
|
|
22112
|
+
* Every process in `session` that carries a pane tag. A process whose ZELLIJ_SESSION_NAME names
|
|
22113
|
+
* a DIFFERENT session is dropped; one with no session name at all is kept (`session: null`),
|
|
22114
|
+
* because zellij has not always set it and dropping it would lose real agents (Deckard's rule).
|
|
22115
|
+
* That also keeps processes some other tool started under `env -u ZELLIJ_SESSION_NAME` (e.g.
|
|
22116
|
+
* a `zellij action list-panes` run from a pane): they carry a pane id and no session, and are
|
|
22117
|
+
* not agents, so classification discards them.
|
|
22118
|
+
*/
|
|
22119
|
+
scan(session) {
|
|
22120
|
+
let pids;
|
|
22121
|
+
if (this.enumerate) {
|
|
22122
|
+
pids = [...this.enumerate()];
|
|
22123
|
+
} else {
|
|
22124
|
+
try {
|
|
22125
|
+
pids = readdirSync13("/proc").filter((name) => /^\d+$/.test(name)).map(Number);
|
|
22126
|
+
} catch {
|
|
22127
|
+
return [];
|
|
22128
|
+
}
|
|
22129
|
+
}
|
|
22130
|
+
const alive = /* @__PURE__ */ new Set();
|
|
22131
|
+
const out = [];
|
|
22132
|
+
for (const pid of pids) {
|
|
22133
|
+
alive.add(pid);
|
|
22134
|
+
const stat = readProcStat(pid);
|
|
22135
|
+
if (!stat) continue;
|
|
22136
|
+
let slot = this.known.get(pid);
|
|
22137
|
+
if (!slot || slot.startTime !== stat.startTime || slot.comm !== stat.comm) {
|
|
22138
|
+
const tag2 = readTag(pid);
|
|
22139
|
+
slot = { startTime: stat.startTime, comm: stat.comm, tag: tag2, ...tag2 ? readArgv(pid) : { argv: "", exe: "" } };
|
|
22140
|
+
this.known.set(pid, slot);
|
|
22141
|
+
} else if (slot.tag && stat.state !== "Z") {
|
|
22142
|
+
Object.assign(slot, readArgv(pid));
|
|
22143
|
+
}
|
|
22144
|
+
const tag = slot.tag;
|
|
22145
|
+
if (!tag) continue;
|
|
22146
|
+
if (tag.session !== null && tag.session !== session) continue;
|
|
22147
|
+
let cwd = null;
|
|
22148
|
+
try {
|
|
22149
|
+
cwd = readlinkSync9(`/proc/${pid}/cwd`);
|
|
22150
|
+
} catch {
|
|
22151
|
+
}
|
|
22152
|
+
out.push({ pid, paneId: tag.paneId, session: tag.session, argv: slot.argv, exe: slot.exe, cwd, stat });
|
|
22153
|
+
}
|
|
22154
|
+
for (const pid of this.known.keys()) if (!alive.has(pid)) this.known.delete(pid);
|
|
22155
|
+
return out.sort((a, b) => a.pid - b.pid);
|
|
22156
|
+
}
|
|
22157
|
+
};
|
|
22158
|
+
var PANE_STATUSES = ["busy", "idle", "shell"];
|
|
22159
|
+
function claudeSessionsDir(env2 = process.env) {
|
|
22160
|
+
const override = env2.PJ_CLAUDE_SESSIONS_DIR?.trim();
|
|
22161
|
+
return override || join36(homedir16(), ".claude", "sessions");
|
|
22162
|
+
}
|
|
22163
|
+
function readClaudeSession(pid, stat, dir = claudeSessionsDir()) {
|
|
22164
|
+
const none = (reason) => ({ kind: null, sessionId: null, reason, status: "unknown", statusUpdatedAt: null });
|
|
22165
|
+
let raw;
|
|
22166
|
+
try {
|
|
22167
|
+
raw = readFileSync31(join36(dir, `${pid}.json`), "utf8");
|
|
22168
|
+
} catch {
|
|
22169
|
+
return none("no-session-file");
|
|
22170
|
+
}
|
|
22171
|
+
let data;
|
|
22172
|
+
try {
|
|
22173
|
+
data = JSON.parse(raw);
|
|
22174
|
+
} catch {
|
|
22175
|
+
return none("unreadable-session-file");
|
|
22176
|
+
}
|
|
22177
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) return none("unreadable-session-file");
|
|
22178
|
+
const file = data;
|
|
22179
|
+
const procStart = file.procStart === void 0 || file.procStart === null ? "" : String(file.procStart);
|
|
22180
|
+
if (!stat || !procStart || procStart !== stat.startTime) return none("stale-session-file");
|
|
22181
|
+
const sessionId = typeof file.sessionId === "string" && file.sessionId ? file.sessionId : null;
|
|
22182
|
+
const status = typeof file.status === "string" && PANE_STATUSES.includes(file.status) ? file.status : "unknown";
|
|
22183
|
+
const at = Number(file.statusUpdatedAt);
|
|
22184
|
+
return {
|
|
22185
|
+
kind: typeof file.kind === "string" ? file.kind : null,
|
|
22186
|
+
sessionId,
|
|
22187
|
+
reason: sessionId ? null : "no-session-id",
|
|
22188
|
+
status,
|
|
22189
|
+
statusUpdatedAt: file.statusUpdatedAt !== void 0 && file.statusUpdatedAt !== null && Number.isFinite(at) ? at : null
|
|
22190
|
+
};
|
|
22191
|
+
}
|
|
22192
|
+
function listPanesFailure(session, result2) {
|
|
22193
|
+
const said = (result2.stderr ?? "").trim().split("\n")[0]?.trim();
|
|
22194
|
+
if (result2.status === 127) return `zellij is not installed or not on PATH (exit 127)${said ? `: ${said}` : ""}`;
|
|
22195
|
+
if (result2.status === 124) return `zellij list-panes for session "${session}" timed out (exit 124): the zellij server may be wedged`;
|
|
22196
|
+
if (said && /not found|no active session|no session/i.test(said)) return `zellij has no session "${session}" (exit ${result2.status}): ${said}`;
|
|
22197
|
+
return `zellij list-panes for session "${session}" failed (exit ${result2.status})${said ? `: ${said}` : ""}`;
|
|
22198
|
+
}
|
|
22199
|
+
function readPaneTable(session, shell = defaultShell) {
|
|
22200
|
+
const panes = /* @__PURE__ */ new Map();
|
|
22201
|
+
const result2 = runListPanes(session, shell);
|
|
22202
|
+
if (result2.status !== 0) return { ok: false, error: listPanesFailure(session, result2), panes };
|
|
22203
|
+
const listed = parseListPanes(result2.stdout);
|
|
22204
|
+
if (!listed) return { ok: false, error: `zellij list-panes for session "${session}" printed no pane list`, panes };
|
|
22205
|
+
if (!listed.length) return { ok: false, error: `zellij list-panes for session "${session}" listed no panes at all: not a live session`, panes };
|
|
22206
|
+
for (const pane of listed) {
|
|
22207
|
+
if (pane.isPlugin || !Number.isInteger(pane.id)) continue;
|
|
22208
|
+
panes.set(pane.id, { tabId: pane.tabId ?? null, tabPosition: pane.tabPosition ?? null, tabName: pane.tabName, title: pane.title });
|
|
22209
|
+
}
|
|
22210
|
+
return { ok: true, error: null, panes };
|
|
22211
|
+
}
|
|
22212
|
+
var IDENTITY_BUDGET_MS2 = 2e4;
|
|
22213
|
+
function paneTerminal(inPane, serverParent) {
|
|
22214
|
+
const live = inPane.filter((p) => p.stat.state !== "Z");
|
|
22215
|
+
const pidsInPane = new Set(live.map((p) => p.pid));
|
|
22216
|
+
const roots = live.filter((p) => !pidsInPane.has(p.stat.ppid));
|
|
22217
|
+
const underServer = roots.filter((p) => serverParent(p.stat.ppid));
|
|
22218
|
+
const ttys = new Set((underServer.length ? underServer : roots).map((p) => p.stat.ttyNr).filter((tty) => tty > 0));
|
|
22219
|
+
return ttys.size === 1 ? [...ttys][0] : null;
|
|
22220
|
+
}
|
|
22221
|
+
function chooseCandidate(candidates) {
|
|
22222
|
+
const start = (c) => Number(c.proc.stat.startTime);
|
|
22223
|
+
const trusted = (c) => c.claude?.sessionId ? 1 : 0;
|
|
22224
|
+
const background = (c) => c.claude?.kind === "bg" ? 1 : 0;
|
|
22225
|
+
return [...candidates].sort(
|
|
22226
|
+
(a, b) => Number(b.foreground) - Number(a.foreground) || background(a) - background(b) || (a.foreground && b.foreground ? trusted(b) - trusted(a) : 0) || start(b) - start(a) || b.proc.pid - a.proc.pid
|
|
22227
|
+
)[0];
|
|
22228
|
+
}
|
|
22229
|
+
function inventoryPanes(options = {}) {
|
|
22230
|
+
const fromOption = options.session?.trim();
|
|
22231
|
+
const fromEnv = process.env.ZELLIJ_SESSION_NAME?.trim();
|
|
22232
|
+
const session = fromOption || fromEnv || ZCRANK_SESSION;
|
|
22233
|
+
const sessionSource = fromOption ? "option" : fromEnv ? "ZELLIJ_SESSION_NAME" : "default";
|
|
22234
|
+
const shell = options.shell ?? defaultShell;
|
|
22235
|
+
const sessionsDir = options.sessionsDir ?? claudeSessionsDir();
|
|
22236
|
+
const scanner = options.scanner ?? new ProcScanner();
|
|
22237
|
+
const identityCache = options.identityCache ?? /* @__PURE__ */ new Map();
|
|
22238
|
+
const budget = { totalMs: IDENTITY_BUDGET_MS2 };
|
|
22239
|
+
const procs = scanner.scan(session);
|
|
22240
|
+
const table = readPaneTable(session, shell);
|
|
22241
|
+
if (!table.ok) {
|
|
22242
|
+
return { session, sessionSource, zellijReadable: false, error: table.error, unmappedPaneIds: [], panes: [] };
|
|
22243
|
+
}
|
|
22244
|
+
const taggedByPane = /* @__PURE__ */ new Map();
|
|
22245
|
+
for (const proc of procs) {
|
|
22246
|
+
const list = taggedByPane.get(proc.paneId);
|
|
22247
|
+
if (list) list.push(proc);
|
|
22248
|
+
else taggedByPane.set(proc.paneId, [proc]);
|
|
22249
|
+
}
|
|
22250
|
+
const serverMemo = /* @__PURE__ */ new Map();
|
|
22251
|
+
const isZellijServer = (pid) => {
|
|
22252
|
+
let known = serverMemo.get(pid);
|
|
22253
|
+
if (known === void 0) {
|
|
22254
|
+
const { exe, argv } = readArgv(pid);
|
|
22255
|
+
known = exe === "zellij" && argv.includes(" --server");
|
|
22256
|
+
serverMemo.set(pid, known);
|
|
22257
|
+
}
|
|
22258
|
+
return known;
|
|
22259
|
+
};
|
|
22260
|
+
const byPane = /* @__PURE__ */ new Map();
|
|
22261
|
+
for (const [paneId, inPane] of taggedByPane) {
|
|
22262
|
+
const tty = paneTerminal(inPane, isZellijServer);
|
|
22263
|
+
for (const proc of inPane) {
|
|
22264
|
+
if (proc.stat.state === "Z") continue;
|
|
22265
|
+
const agent = agentCli(proc.exe, proc.argv);
|
|
22266
|
+
if (!agent) continue;
|
|
22267
|
+
const claude = agent === "claude" ? readClaudeSession(proc.pid, proc.stat, sessionsDir) : null;
|
|
22268
|
+
const foreground = tty !== null && proc.stat.ttyNr === tty && proc.stat.tpgid > 0 && proc.stat.pgrp === proc.stat.tpgid;
|
|
22269
|
+
const candidate = { proc, agent, foreground, claude };
|
|
22270
|
+
const list = byPane.get(paneId);
|
|
22271
|
+
if (list) list.push(candidate);
|
|
22272
|
+
else byPane.set(paneId, [candidate]);
|
|
22273
|
+
}
|
|
22274
|
+
}
|
|
22275
|
+
const rows = [];
|
|
22276
|
+
const unmappedPaneIds = [];
|
|
22277
|
+
for (const [paneId, candidates] of byPane) {
|
|
22278
|
+
if (!table.panes.has(paneId)) {
|
|
22279
|
+
unmappedPaneIds.push(paneId);
|
|
22280
|
+
continue;
|
|
22281
|
+
}
|
|
22282
|
+
const chosen = chooseCandidate(candidates);
|
|
22283
|
+
const entry = table.panes.get(paneId);
|
|
22284
|
+
const cwd = chosen.proc.cwd;
|
|
22285
|
+
const info = chosen.claude ?? { kind: null, sessionId: null, reason: "unresolved", status: "unknown", statusUpdatedAt: null };
|
|
22286
|
+
rows.push({
|
|
22287
|
+
position: entry?.tabPosition ?? Number.MAX_SAFE_INTEGER,
|
|
22288
|
+
row: {
|
|
22289
|
+
session,
|
|
22290
|
+
tabId: entry?.tabId ?? null,
|
|
22291
|
+
tabName: entry?.tabName ?? null,
|
|
22292
|
+
paneId,
|
|
22293
|
+
paneTitle: entry?.title ?? null,
|
|
22294
|
+
pid: chosen.proc.pid,
|
|
22295
|
+
agent: chosen.agent,
|
|
22296
|
+
cwd,
|
|
22297
|
+
project: (cwd ? projectIdentity(cwd, { shell, cache: identityCache, budget }) : void 0) ?? null,
|
|
22298
|
+
sessionId: info.sessionId,
|
|
22299
|
+
sessionReason: info.sessionId ? null : info.reason,
|
|
22300
|
+
status: info.status,
|
|
22301
|
+
statusUpdatedAt: info.statusUpdatedAt,
|
|
22302
|
+
nested: candidates.filter((c) => c !== chosen).map((c) => ({ pid: c.proc.pid, agent: c.agent })).sort((a, b) => a.pid - b.pid)
|
|
22303
|
+
}
|
|
22304
|
+
});
|
|
22305
|
+
}
|
|
22306
|
+
if (options.all) {
|
|
22307
|
+
for (const [paneId, entry] of table.panes) {
|
|
22308
|
+
if (byPane.has(paneId)) continue;
|
|
22309
|
+
rows.push({
|
|
22310
|
+
position: entry.tabPosition ?? Number.MAX_SAFE_INTEGER,
|
|
22311
|
+
row: {
|
|
22312
|
+
session,
|
|
22313
|
+
tabId: entry.tabId,
|
|
22314
|
+
tabName: entry.tabName,
|
|
22315
|
+
paneId,
|
|
22316
|
+
paneTitle: entry.title,
|
|
22317
|
+
pid: null,
|
|
22318
|
+
agent: null,
|
|
22319
|
+
cwd: null,
|
|
22320
|
+
project: null,
|
|
22321
|
+
sessionId: null,
|
|
22322
|
+
sessionReason: null,
|
|
22323
|
+
status: "unknown",
|
|
22324
|
+
statusUpdatedAt: null,
|
|
22325
|
+
nested: []
|
|
22326
|
+
}
|
|
22327
|
+
});
|
|
22328
|
+
}
|
|
22329
|
+
}
|
|
22330
|
+
rows.sort((a, b) => a.position - b.position || a.row.paneId - b.row.paneId);
|
|
22331
|
+
return { session, sessionSource, zellijReadable: true, error: null, unmappedPaneIds: unmappedPaneIds.sort((a, b) => a - b), panes: rows.map((r) => r.row) };
|
|
22332
|
+
}
|
|
22333
|
+
function tilde(path) {
|
|
22334
|
+
if (!path) return "-";
|
|
22335
|
+
const home = homedir16();
|
|
22336
|
+
return path === home ? "~" : path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
|
|
22337
|
+
}
|
|
22338
|
+
function displayWidth(text2) {
|
|
22339
|
+
let width = 0;
|
|
22340
|
+
for (const { segment } of new Intl.Segmenter(void 0, { granularity: "grapheme" }).segment(text2)) width += clusterWidth(segment);
|
|
22341
|
+
return width;
|
|
22342
|
+
}
|
|
22343
|
+
var EMOJI_PRESENTATION = /^\p{Emoji_Presentation}/u;
|
|
22344
|
+
var EMOJI_BASE = /^\p{Emoji}/u;
|
|
22345
|
+
var REGIONAL_INDICATOR = /^\p{Regional_Indicator}/u;
|
|
22346
|
+
var ZERO_WIDTH_ONLY = /^[\p{Mn}\p{Me}\p{Cf}\p{Cc}]+$/u;
|
|
22347
|
+
function clusterWidth(cluster) {
|
|
22348
|
+
if (ZERO_WIDTH_ONLY.test(cluster)) return 0;
|
|
22349
|
+
const cp = cluster.codePointAt(0);
|
|
22350
|
+
if (REGIONAL_INDICATOR.test(cluster)) return [...cluster].length >= 2 ? 2 : 1;
|
|
22351
|
+
if (EMOJI_PRESENTATION.test(cluster)) return 2;
|
|
22352
|
+
if (cluster.includes("\uFE0F") && EMOJI_BASE.test(cluster)) return 2;
|
|
22353
|
+
const wide = cp >= 4352 && cp <= 4447 || // Hangul Jamo
|
|
22354
|
+
cp >= 11904 && cp <= 42191 && cp !== 12351 || // CJK, kana, Yi
|
|
22355
|
+
cp >= 44032 && cp <= 55203 || // Hangul syllables
|
|
22356
|
+
cp >= 63744 && cp <= 64255 || // CJK compatibility
|
|
22357
|
+
cp >= 65072 && cp <= 65135 || // CJK compatibility forms
|
|
22358
|
+
cp >= 65280 && cp <= 65376 || // fullwidth forms
|
|
22359
|
+
cp >= 65504 && cp <= 65510 || cp >= 131072 && cp <= 262141;
|
|
22360
|
+
return wide ? 2 : 1;
|
|
22361
|
+
}
|
|
22362
|
+
function formatPaneTable(inventory) {
|
|
22363
|
+
const header = ["TAB", "PANE", "PID", "AGENT", "PROJECT", "STATUS", "SESSION", "NESTED"];
|
|
22364
|
+
const rows = inventory.panes.map((p) => [
|
|
22365
|
+
p.tabName ?? "-",
|
|
22366
|
+
String(p.paneId),
|
|
22367
|
+
p.pid === null ? "-" : String(p.pid),
|
|
22368
|
+
p.agent ?? "(no agent)",
|
|
22369
|
+
tilde(p.project ?? p.cwd),
|
|
22370
|
+
p.agent ? p.status : "-",
|
|
22371
|
+
p.sessionId ?? (p.agent ? `- (${p.sessionReason})` : "-"),
|
|
22372
|
+
p.nested.length ? p.nested.map((n) => `${n.agent}:${n.pid}`).join(",") : "-"
|
|
22373
|
+
]);
|
|
22374
|
+
const widths = header.map((h, i) => Math.max(displayWidth(h), ...rows.map((r) => displayWidth(r[i] ?? ""))));
|
|
22375
|
+
const line = (cells) => cells.map((c, i) => c + " ".repeat(Math.max(0, (widths[i] ?? 0) - displayWidth(c)))).join(" ").trimEnd();
|
|
22376
|
+
return [line(header), ...rows.map(line)].join("\n");
|
|
22377
|
+
}
|
|
22378
|
+
|
|
21970
22379
|
// src/crank/cli.ts
|
|
21971
22380
|
var loadCrankScreens = () => loadScreen("./crank/tui.js", import.meta.url);
|
|
21972
22381
|
var xmark = `${red(glyph.fail)}`;
|
|
@@ -22010,6 +22419,9 @@ function registerCrankCli(program2) {
|
|
|
22010
22419
|
crankCmd.command("tui").description("Full-screen crank dashboard: toggle, interval, last runs, countdown to the next run, log").action(async () => {
|
|
22011
22420
|
await runTui();
|
|
22012
22421
|
});
|
|
22422
|
+
crankCmd.command("panes").description("Read-only inventory of the live agent panes in this zellij session: pane -> pid -> agent CLI -> session id/status (from /proc, not tab names)").option("--json", "Emit the inventory as JSON").option("--all", "Also list terminal panes that run no agent").action((options) => {
|
|
22423
|
+
runPanes(options);
|
|
22424
|
+
});
|
|
22013
22425
|
crankCmd.command("list").description("Every registry project with crank ON/OFF and its resolved tab, plus orphan zcrank keys").action(() => {
|
|
22014
22426
|
try {
|
|
22015
22427
|
const store = redis();
|
|
@@ -22140,6 +22552,34 @@ function runDisable() {
|
|
|
22140
22552
|
fail(error, "crank");
|
|
22141
22553
|
}
|
|
22142
22554
|
}
|
|
22555
|
+
function runPanes(options) {
|
|
22556
|
+
try {
|
|
22557
|
+
const inventory = inventoryPanes({ all: options.all === true });
|
|
22558
|
+
const sessionNote = inventory.sessionSource === "default" ? `not inside zellij ($ZELLIJ_SESSION_NAME is unset): using the default session "${inventory.session}"` : inventory.sessionSource === "ZELLIJ_SESSION_NAME" ? `session "${inventory.session}" (from $ZELLIJ_SESSION_NAME)` : `session "${inventory.session}"`;
|
|
22559
|
+
if (options.json) {
|
|
22560
|
+
console.log(JSON.stringify(inventory, null, 2));
|
|
22561
|
+
}
|
|
22562
|
+
if (!inventory.zellijReadable) {
|
|
22563
|
+
console.error(`${xmark} ${inventory.error ?? "zellij is unreadable"} \u2014 ${sessionNote}`);
|
|
22564
|
+
process.exit(1);
|
|
22565
|
+
}
|
|
22566
|
+
if (options.json) {
|
|
22567
|
+
if (inventory.unmappedPaneIds.length) console.error(` ${dim(`${inventory.unmappedPaneIds.length} pane id(s) with agent processes are not listed by zellij: ${inventory.unmappedPaneIds.join(", ")}`)}`);
|
|
22568
|
+
return;
|
|
22569
|
+
}
|
|
22570
|
+
if (inventory.sessionSource === "default") console.error(` ${yellow(glyph.warn)} ${dim(sessionNote)}`);
|
|
22571
|
+
if (inventory.unmappedPaneIds.length) {
|
|
22572
|
+
console.error(` ${dim(`ignored ${inventory.unmappedPaneIds.length} pane id(s) with agent processes that zellij does not list (a closed pane, a pane that closed during the scan, or a stale daemon environment): ${inventory.unmappedPaneIds.join(", ")}`)}`);
|
|
22573
|
+
}
|
|
22574
|
+
if (!inventory.panes.length) {
|
|
22575
|
+
console.log(` ${dim(`no live agent panes in session ${inventory.session}`)}`);
|
|
22576
|
+
return;
|
|
22577
|
+
}
|
|
22578
|
+
console.log(formatPaneTable(inventory));
|
|
22579
|
+
} catch (error) {
|
|
22580
|
+
fail(error, "crank panes");
|
|
22581
|
+
}
|
|
22582
|
+
}
|
|
22143
22583
|
async function runTui() {
|
|
22144
22584
|
try {
|
|
22145
22585
|
if (!isInteractive()) {
|
|
@@ -22185,7 +22625,7 @@ async function promptForRuleIds(rules) {
|
|
|
22185
22625
|
function readJson2(path) {
|
|
22186
22626
|
if (!existsSync26(path)) return void 0;
|
|
22187
22627
|
try {
|
|
22188
|
-
const parsed = JSON.parse(
|
|
22628
|
+
const parsed = JSON.parse(readFileSync32(path, "utf8"));
|
|
22189
22629
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
22190
22630
|
} catch {
|
|
22191
22631
|
return void 0;
|
|
@@ -22202,9 +22642,9 @@ function packageNameToProjectName(value) {
|
|
|
22202
22642
|
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
22203
22643
|
}
|
|
22204
22644
|
function deriveProjectDefaults(targetDir) {
|
|
22205
|
-
const manifest = readJson2(
|
|
22206
|
-
const pkg = readJson2(
|
|
22207
|
-
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(
|
|
22645
|
+
const manifest = readJson2(join37(targetDir, ".project.json"));
|
|
22646
|
+
const pkg = readJson2(join37(targetDir, "package.json"));
|
|
22647
|
+
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename12(targetDir)) || "Project";
|
|
22208
22648
|
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
22209
22649
|
return {
|
|
22210
22650
|
name,
|
|
@@ -22278,7 +22718,7 @@ function actionNeedsRun(plan, kind, syncMode) {
|
|
|
22278
22718
|
if (!action || action.kind !== "project.write-manifest") return false;
|
|
22279
22719
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
22280
22720
|
`;
|
|
22281
|
-
return !existsSync26(action.path) ||
|
|
22721
|
+
return !existsSync26(action.path) || readFileSync32(action.path, "utf8") !== next;
|
|
22282
22722
|
}
|
|
22283
22723
|
if (kind === "copier.copy.commonproject") return true;
|
|
22284
22724
|
if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
@@ -22335,11 +22775,11 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
22335
22775
|
if (!targetDir && cwdGitRoot) {
|
|
22336
22776
|
targetDir = cwdGitRoot;
|
|
22337
22777
|
}
|
|
22338
|
-
if (!targetDir && !name && existsSync26(
|
|
22778
|
+
if (!targetDir && !name && existsSync26(join37(cwd, ".project.json"))) {
|
|
22339
22779
|
targetDir = cwd;
|
|
22340
22780
|
}
|
|
22341
22781
|
if (!targetDir && interactive) {
|
|
22342
|
-
const promptedName = await promptTextValue("Project name", packageNameToProjectName(
|
|
22782
|
+
const promptedName = await promptTextValue("Project name", packageNameToProjectName(basename12(cwd)));
|
|
22343
22783
|
targetDir = resolve25(await promptTextValue("Project directory", projectTargetDirUnder(promptedName, cwd)));
|
|
22344
22784
|
name = promptedName;
|
|
22345
22785
|
}
|
|
@@ -22349,14 +22789,14 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
22349
22789
|
const targetExists = existsSync26(targetDir);
|
|
22350
22790
|
if (targetExists && !statSync9(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
22351
22791
|
const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
|
|
22352
|
-
const alreadyScaffolded = targetExists && (existsSync26(
|
|
22792
|
+
const alreadyScaffolded = targetExists && (existsSync26(join37(targetDir, ".project.json")) || existsSync26(join37(targetDir, ".copier-answers.yml")));
|
|
22353
22793
|
const syncMode = Boolean(
|
|
22354
22794
|
targetGitRoot && resolve25(targetGitRoot) === resolve25(targetDir) || alreadyScaffolded
|
|
22355
22795
|
);
|
|
22356
|
-
if (targetExists && !syncMode && !options.force &&
|
|
22796
|
+
if (targetExists && !syncMode && !options.force && readdirSync14(targetDir).length > 0) {
|
|
22357
22797
|
throw new Error(`Target already exists and is not empty: ${targetDir} (pass --force to render over it)`);
|
|
22358
22798
|
}
|
|
22359
|
-
const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(
|
|
22799
|
+
const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(basename12(targetDir)) ?? "Project", description: "" };
|
|
22360
22800
|
if (!name && interactive && !syncMode) {
|
|
22361
22801
|
name = await promptTextValue("Project name", defaults.name);
|
|
22362
22802
|
}
|
|
@@ -22871,7 +23311,7 @@ program.command("reindex").argument("[project-id]", "Canonical project ID (defau
|
|
|
22871
23311
|
else if (options.all) result2 = registryRequest(location, "POST", "/v1/reindex", {});
|
|
22872
23312
|
else {
|
|
22873
23313
|
const repo = projectId ? getProject(loadProjectRegistry(location), projectId).repo_path : resolveProject(process.cwd()).root;
|
|
22874
|
-
result2 = registryRequest(location, "POST", "/v1/index", { manifest_path:
|
|
23314
|
+
result2 = registryRequest(location, "POST", "/v1/index", { manifest_path: join37(repo, ".project.json") });
|
|
22875
23315
|
}
|
|
22876
23316
|
const statuses = result2.__registry_status;
|
|
22877
23317
|
const stale = Object.entries(statuses ?? {}).filter(([, item]) => item.status !== "ok");
|