@delorenj/pjangler 1.5.39 → 1.5.41
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/crank/tui.js +63 -6
- package/dist/index.js +1027 -62
- 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
|
|
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
|
|
@@ -20943,8 +20943,204 @@ async function loadScreen(relativeToDist, base) {
|
|
|
20943
20943
|
init_boardQuery();
|
|
20944
20944
|
init_style();
|
|
20945
20945
|
import { spawnSync as spawnSync17 } from "node:child_process";
|
|
20946
|
-
import { readFileSync as
|
|
20946
|
+
import { mkdirSync as mkdirSync12, readFileSync as readFileSync30, realpathSync as realpathSync12, renameSync as renameSync10, writeFileSync as writeFileSync15 } from "node:fs";
|
|
20947
|
+
import { homedir as homedir15 } from "node:os";
|
|
20948
|
+
import { dirname as dirname22, join as join35 } from "node:path";
|
|
20949
|
+
|
|
20950
|
+
// src/crank/engine.ts
|
|
20951
|
+
import { closeSync as closeSync9, constants as constants7, fstatSync as fstatSync5, openSync as openSync9, readFileSync as readFileSync29, readSync as readSync5 } from "node:fs";
|
|
20952
|
+
import { homedir as homedir14 } from "node:os";
|
|
20947
20953
|
import { join as join34 } from "node:path";
|
|
20954
|
+
var ENGINE_DEFAULT_JOB_ID = "777c511a009c";
|
|
20955
|
+
var WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
20956
|
+
var TICK_MS = 5 * 60 * 1e3;
|
|
20957
|
+
var STALL_MS = 3 * TICK_MS;
|
|
20958
|
+
var ENGINE_AUDIT_TAIL_BYTES = 1024 * 1024;
|
|
20959
|
+
function unknownEngineStatus(nowMs, jobId = ENGINE_DEFAULT_JOB_ID) {
|
|
20960
|
+
return { known: false, jobId, readAtMs: nowMs, outcome: "unknown", beats24h: 0, tokens24h: 0, tokenBlocked: false };
|
|
20961
|
+
}
|
|
20962
|
+
var FUTURE_SKEW_MS = 5 * 6e4;
|
|
20963
|
+
function parseAuditRows(text2, jobId) {
|
|
20964
|
+
const rows = [];
|
|
20965
|
+
for (const line of text2.split("\n")) {
|
|
20966
|
+
const trimmed2 = line.trim();
|
|
20967
|
+
if (!trimmed2) continue;
|
|
20968
|
+
let parsed;
|
|
20969
|
+
try {
|
|
20970
|
+
parsed = JSON.parse(trimmed2);
|
|
20971
|
+
} catch {
|
|
20972
|
+
continue;
|
|
20973
|
+
}
|
|
20974
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) continue;
|
|
20975
|
+
const record3 = parsed;
|
|
20976
|
+
if (String(record3.job_id ?? "") !== jobId) continue;
|
|
20977
|
+
const ts = typeof record3.ts === "string" ? Date.parse(record3.ts) : Number.NaN;
|
|
20978
|
+
if (!Number.isFinite(ts)) continue;
|
|
20979
|
+
const tokens = Number(record3.total_tokens ?? 0);
|
|
20980
|
+
const reasonRaw = record3.economics_reason ?? record3.error;
|
|
20981
|
+
rows.push({
|
|
20982
|
+
tsMs: ts,
|
|
20983
|
+
status: typeof record3.status === "string" ? record3.status : "",
|
|
20984
|
+
tokens: Number.isFinite(tokens) && tokens > 0 ? tokens : 0,
|
|
20985
|
+
reason: typeof reasonRaw === "string" && reasonRaw.trim() ? reasonRaw.trim() : void 0
|
|
20986
|
+
});
|
|
20987
|
+
}
|
|
20988
|
+
return rows.sort((a, b) => a.tsMs - b.tsMs);
|
|
20989
|
+
}
|
|
20990
|
+
function capFromReason(reason) {
|
|
20991
|
+
const match = /daily token limit reached \(([\d,]+)\)/i.exec(reason ?? "");
|
|
20992
|
+
if (!match) return void 0;
|
|
20993
|
+
const n = Number.parseInt((match[1] ?? "").replace(/,/g, ""), 10);
|
|
20994
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
20995
|
+
}
|
|
20996
|
+
function parseEngineStatus(auditText, input) {
|
|
20997
|
+
const jobId = input.jobId ?? ENGINE_DEFAULT_JOB_ID;
|
|
20998
|
+
try {
|
|
20999
|
+
const rows = parseAuditRows(auditText, jobId).filter((r) => r.tsMs <= input.nowMs + FUTURE_SKEW_MS);
|
|
21000
|
+
const last = rows.at(-1);
|
|
21001
|
+
if (!last) return unknownEngineStatus(input.nowMs, jobId);
|
|
21002
|
+
const windowStart = input.nowMs - WINDOW_MS;
|
|
21003
|
+
const counted = rows.filter((r) => (r.status === "completed" || r.status === "started") && r.tsMs >= windowStart);
|
|
21004
|
+
const tokens24h = counted.reduce((sum, r) => sum + r.tokens, 0);
|
|
21005
|
+
const ran = last.status === "completed" || last.status === "started";
|
|
21006
|
+
const refused = !ran && last.status !== "failed" && (last.status === "skipped" || last.status === "blocked" || !!last.reason);
|
|
21007
|
+
const outcome = ran ? "cranked" : refused ? "refused" : "other";
|
|
21008
|
+
const tokenBlocked = outcome === "refused" && /daily token limit/i.test(last.reason ?? "");
|
|
21009
|
+
let refusedSinceMs;
|
|
21010
|
+
if (outcome === "refused") {
|
|
21011
|
+
refusedSinceMs = last.tsMs;
|
|
21012
|
+
for (let i = rows.length - 2; i >= 0; i--) {
|
|
21013
|
+
const r = rows[i];
|
|
21014
|
+
if (!r) break;
|
|
21015
|
+
if (r.status === "completed" || r.status === "started" || r.status === "failed") break;
|
|
21016
|
+
refusedSinceMs = r.tsMs;
|
|
21017
|
+
}
|
|
21018
|
+
}
|
|
21019
|
+
const cap = input.cap && input.cap > 0 ? input.cap : capFromReason(last.reason);
|
|
21020
|
+
let resumeMs;
|
|
21021
|
+
if (tokenBlocked && cap) {
|
|
21022
|
+
let remaining = tokens24h;
|
|
21023
|
+
resumeMs = remaining < cap ? input.nowMs : void 0;
|
|
21024
|
+
for (const row of counted) {
|
|
21025
|
+
if (remaining < cap) break;
|
|
21026
|
+
remaining -= row.tokens;
|
|
21027
|
+
if (remaining < cap) {
|
|
21028
|
+
resumeMs = row.tsMs + WINDOW_MS;
|
|
21029
|
+
break;
|
|
21030
|
+
}
|
|
21031
|
+
}
|
|
21032
|
+
}
|
|
21033
|
+
return {
|
|
21034
|
+
known: true,
|
|
21035
|
+
jobId,
|
|
21036
|
+
readAtMs: input.nowMs,
|
|
21037
|
+
outcome,
|
|
21038
|
+
lastStatus: last.status || void 0,
|
|
21039
|
+
lastTickMs: last.tsMs,
|
|
21040
|
+
reason: outcome === "cranked" ? void 0 : last.reason,
|
|
21041
|
+
refusedSinceMs,
|
|
21042
|
+
beats24h: counted.length,
|
|
21043
|
+
tokens24h,
|
|
21044
|
+
cap,
|
|
21045
|
+
tokenBlocked,
|
|
21046
|
+
resumeMs
|
|
21047
|
+
};
|
|
21048
|
+
} catch {
|
|
21049
|
+
return unknownEngineStatus(input.nowMs, jobId);
|
|
21050
|
+
}
|
|
21051
|
+
}
|
|
21052
|
+
function readTail(path, maxBytes) {
|
|
21053
|
+
const fd = openSync9(path, constants7.O_RDONLY | constants7.O_NONBLOCK);
|
|
21054
|
+
try {
|
|
21055
|
+
const stat = fstatSync5(fd);
|
|
21056
|
+
if (!stat.isFile()) throw new Error(`${path} is not a regular file`);
|
|
21057
|
+
const size = stat.size;
|
|
21058
|
+
const length = Math.min(size, maxBytes);
|
|
21059
|
+
const buffer = Buffer.alloc(length);
|
|
21060
|
+
let read = 0;
|
|
21061
|
+
while (read < length) {
|
|
21062
|
+
const n = readSync5(fd, buffer, read, length - read, size - length + read);
|
|
21063
|
+
if (n <= 0) break;
|
|
21064
|
+
read += n;
|
|
21065
|
+
}
|
|
21066
|
+
const text2 = buffer.subarray(0, read).toString("utf8");
|
|
21067
|
+
if (size <= maxBytes) return text2;
|
|
21068
|
+
const newline = text2.indexOf("\n");
|
|
21069
|
+
return newline === -1 ? "" : text2.slice(newline + 1);
|
|
21070
|
+
} finally {
|
|
21071
|
+
closeSync9(fd);
|
|
21072
|
+
}
|
|
21073
|
+
}
|
|
21074
|
+
function engineSources(env2 = process.env) {
|
|
21075
|
+
const cronDir = join34(homedir14(), ".hermes", "profiles", "33god-pm", "cron");
|
|
21076
|
+
return {
|
|
21077
|
+
auditPath: env2.PJ_CRANK_USAGE_AUDIT?.trim() || join34(cronDir, "usage_audit.jsonl"),
|
|
21078
|
+
jobsPath: join34(cronDir, "jobs.json"),
|
|
21079
|
+
jobId: env2.PJ_CRANK_JOB_ID?.trim() || ENGINE_DEFAULT_JOB_ID
|
|
21080
|
+
};
|
|
21081
|
+
}
|
|
21082
|
+
function readCap(jobsPath, jobId) {
|
|
21083
|
+
try {
|
|
21084
|
+
const parsed = JSON.parse(readFileSync29(jobsPath, "utf8"));
|
|
21085
|
+
const jobs = Array.isArray(parsed.jobs) ? parsed.jobs : [];
|
|
21086
|
+
const job = jobs.find((j) => typeof j === "object" && j !== null && String(j.id ?? "") === jobId);
|
|
21087
|
+
const economics = job?.economics;
|
|
21088
|
+
if (typeof economics !== "object" || economics === null) return void 0;
|
|
21089
|
+
const cap = Number(economics.max_daily_tokens);
|
|
21090
|
+
return Number.isFinite(cap) && cap > 0 ? cap : void 0;
|
|
21091
|
+
} catch {
|
|
21092
|
+
return void 0;
|
|
21093
|
+
}
|
|
21094
|
+
}
|
|
21095
|
+
function readEngineStatus(options = {}) {
|
|
21096
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
21097
|
+
const base = engineSources(options.env);
|
|
21098
|
+
const sources = { ...base, ...options.sources };
|
|
21099
|
+
try {
|
|
21100
|
+
const text2 = readTail(sources.auditPath, options.tailBytes ?? ENGINE_AUDIT_TAIL_BYTES);
|
|
21101
|
+
return parseEngineStatus(text2, { jobId: sources.jobId, cap: readCap(sources.jobsPath, sources.jobId), nowMs });
|
|
21102
|
+
} catch {
|
|
21103
|
+
return unknownEngineStatus(nowMs, sources.jobId);
|
|
21104
|
+
}
|
|
21105
|
+
}
|
|
21106
|
+
function clockTime(ms) {
|
|
21107
|
+
const d = new Date(ms);
|
|
21108
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
21109
|
+
}
|
|
21110
|
+
function ago(thenMs, nowMs) {
|
|
21111
|
+
const s = Math.max(0, Math.round((nowMs - thenMs) / 1e3));
|
|
21112
|
+
if (s < 60) return `${s}s ago`;
|
|
21113
|
+
if (s < 3600) return `${Math.floor(s / 60)}m ago`;
|
|
21114
|
+
if (s < 86400) return `${Math.floor(s / 3600)}h ${String(Math.floor(s % 3600 / 60)).padStart(2, "0")}m ago`;
|
|
21115
|
+
return `${Math.floor(s / 86400)}d ${Math.floor(s % 86400 / 3600)}h ago`;
|
|
21116
|
+
}
|
|
21117
|
+
function compactTokens(n) {
|
|
21118
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
21119
|
+
if (n >= 1e3) return `${Math.round(n / 1e3)}k`;
|
|
21120
|
+
return String(n);
|
|
21121
|
+
}
|
|
21122
|
+
function nextTickAtOrAfter(ms) {
|
|
21123
|
+
return Math.ceil(ms / TICK_MS) * TICK_MS;
|
|
21124
|
+
}
|
|
21125
|
+
function engineStalled(status) {
|
|
21126
|
+
return status.known && status.lastTickMs !== void 0 && status.readAtMs - status.lastTickMs > STALL_MS;
|
|
21127
|
+
}
|
|
21128
|
+
function engineSummary(status) {
|
|
21129
|
+
if (!status.known || status.lastTickMs === void 0) return { text: "engine status unknown", tone: "unknown" };
|
|
21130
|
+
const now = status.readAtMs;
|
|
21131
|
+
const usage = `${status.beats24h} beat${status.beats24h === 1 ? "" : "s"} / ${compactTokens(status.tokens24h)} tokens in 24h${status.cap ? ` of ${compactTokens(status.cap)}` : ""}`;
|
|
21132
|
+
const tick = `last tick ${ago(status.lastTickMs, now)}`;
|
|
21133
|
+
const stalled = engineStalled(status) ? " - not ticking" : "";
|
|
21134
|
+
if (status.outcome === "cranked") return { text: `${tick} (cranked)${stalled} \xB7 ${usage}`, tone: stalled ? "warn" : "ok" };
|
|
21135
|
+
if (status.outcome === "refused") {
|
|
21136
|
+
const since = status.refusedSinceMs !== void 0 && status.refusedSinceMs < status.lastTickMs ? ` since ${ago(status.refusedSinceMs, now)}` : "";
|
|
21137
|
+
const resume = status.tokenBlocked && status.resumeMs !== void 0 ? ` \xB7 resumes \u2248 ${clockTime(nextTickAtOrAfter(status.resumeMs))}` : "";
|
|
21138
|
+
return { text: `REFUSING${since}: ${status.reason ?? "refused"} \xB7 ${tick}${stalled} \xB7 ${usage}${resume}`, tone: "warn" };
|
|
21139
|
+
}
|
|
21140
|
+
return { text: `${tick} (${status.lastStatus ?? "other"}${status.reason ? `: ${status.reason}` : ""})${stalled} \xB7 ${usage}`, tone: "warn" };
|
|
21141
|
+
}
|
|
21142
|
+
|
|
21143
|
+
// src/crank/index.ts
|
|
20948
21144
|
var ZCRANK_REDIS_HOST = "127.0.0.1";
|
|
20949
21145
|
var ZCRANK_REDIS_PORT = 6379;
|
|
20950
21146
|
function redisEndpoint(env2 = process.env) {
|
|
@@ -20959,7 +21155,7 @@ var ZCRANK_LOG_CAP = 500;
|
|
|
20959
21155
|
var TAB_STATUS_SUFFIX = /[\s🟢🟡🔴🔔⚫✳✻●·─]+$/u;
|
|
20960
21156
|
var defaultShell = (command, args) => {
|
|
20961
21157
|
const result2 = spawnSync17(command, args, { encoding: "utf8", timeout: 1e4 });
|
|
20962
|
-
if (result2.error) return { stdout: "", status: 127 };
|
|
21158
|
+
if (result2.error) return { stdout: "", status: result2.error.code === "ETIMEDOUT" ? 124 : 127 };
|
|
20963
21159
|
return { stdout: result2.stdout ?? "", status: result2.status ?? 1, stderr: result2.stderr ?? "" };
|
|
20964
21160
|
};
|
|
20965
21161
|
function tabDisplay(name) {
|
|
@@ -21095,6 +21291,15 @@ function redis(deps = {}) {
|
|
|
21095
21291
|
setTabEnabled: (tab, enabled2) => {
|
|
21096
21292
|
run("HSET", `zcrank:tab:${tab}`, "enabled", enabled2 ? "1" : "0");
|
|
21097
21293
|
},
|
|
21294
|
+
/** HGET zcrank:tab:<tab> root -> the owning project identity, or undefined (legacy key / absent). */
|
|
21295
|
+
tabRoot: (tab) => {
|
|
21296
|
+
const v = run("HGET", `zcrank:tab:${tab}`, "root");
|
|
21297
|
+
return v === "" || v == null ? void 0 : v;
|
|
21298
|
+
},
|
|
21299
|
+
/** HSET zcrank:tab:<tab> root <project identity> (PJAN-201). */
|
|
21300
|
+
setTabRoot: (tab, root) => {
|
|
21301
|
+
run("HSET", `zcrank:tab:${tab}`, "root", root);
|
|
21302
|
+
},
|
|
21098
21303
|
/** HGET zcrank:tab:<tab> interval -> a valid interval, else undefined (absent or garbage). */
|
|
21099
21304
|
tabInterval: (tab) => parseInterval(run("HGET", `zcrank:tab:${tab}`, "interval")),
|
|
21100
21305
|
/** HSET zcrank:tab:<tab> interval <5m|1h|6h|1d>. */
|
|
@@ -21165,11 +21370,188 @@ function liveTabs(deps = {}) {
|
|
|
21165
21370
|
try {
|
|
21166
21371
|
const parsed = JSON.parse(result2.stdout);
|
|
21167
21372
|
if (!Array.isArray(parsed)) return [];
|
|
21168
|
-
return parsed.filter((entry) => typeof entry === "object" && entry !== null).map((entry) =>
|
|
21373
|
+
return parsed.filter((entry) => typeof entry === "object" && entry !== null).map((entry) => {
|
|
21374
|
+
const id = Number(entry.tab_id);
|
|
21375
|
+
return {
|
|
21376
|
+
name: String(entry.name ?? ""),
|
|
21377
|
+
position: Number(entry.position ?? 0),
|
|
21378
|
+
...entry.tab_id !== void 0 && entry.tab_id !== null && Number.isInteger(id) ? { id } : {}
|
|
21379
|
+
};
|
|
21380
|
+
});
|
|
21169
21381
|
} catch {
|
|
21170
21382
|
return [];
|
|
21171
21383
|
}
|
|
21172
21384
|
}
|
|
21385
|
+
function parseListPanes(stdout) {
|
|
21386
|
+
if (!stdout.trim()) return null;
|
|
21387
|
+
let parsed;
|
|
21388
|
+
try {
|
|
21389
|
+
parsed = JSON.parse(stdout);
|
|
21390
|
+
} catch {
|
|
21391
|
+
return null;
|
|
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);
|
|
21422
|
+
}
|
|
21423
|
+
function projectIdentity(dir, deps = {}) {
|
|
21424
|
+
const target = (dir ?? "").trim();
|
|
21425
|
+
if (!target) return void 0;
|
|
21426
|
+
const cached = deps.cache?.get(target);
|
|
21427
|
+
if (cached !== void 0) return cached ?? void 0;
|
|
21428
|
+
if (deps.budget) {
|
|
21429
|
+
deps.budget.untilMs ??= Date.now() + deps.budget.totalMs;
|
|
21430
|
+
if (Date.now() > deps.budget.untilMs) return void 0;
|
|
21431
|
+
}
|
|
21432
|
+
const shell = deps.shell ?? defaultShell;
|
|
21433
|
+
let identity2;
|
|
21434
|
+
try {
|
|
21435
|
+
const result2 = shell("timeout", ["2s", "git", "-C", target, "worktree", "list", "--porcelain"]);
|
|
21436
|
+
if (result2.status === 0) {
|
|
21437
|
+
const first = result2.stdout.split("\n").find((line) => line.startsWith("worktree "));
|
|
21438
|
+
const path = first?.slice("worktree ".length).replace(/\r$/, "").trim();
|
|
21439
|
+
if (path) identity2 = realpathSync12(path);
|
|
21440
|
+
}
|
|
21441
|
+
} catch {
|
|
21442
|
+
identity2 = void 0;
|
|
21443
|
+
}
|
|
21444
|
+
deps.cache?.set(target, identity2 ?? null);
|
|
21445
|
+
return identity2;
|
|
21446
|
+
}
|
|
21447
|
+
function withTabOwners(tabs, panes, identify, claimed = []) {
|
|
21448
|
+
const claimedSet = new Set(claimed);
|
|
21449
|
+
return tabs.map((tab) => {
|
|
21450
|
+
const counts = /* @__PURE__ */ new Map();
|
|
21451
|
+
for (const pane of panes) {
|
|
21452
|
+
if (pane.isPlugin || !pane.cwd) continue;
|
|
21453
|
+
const inTab = tab.id !== void 0 && pane.tabId !== void 0 ? pane.tabId === tab.id : pane.tabPosition === tab.position;
|
|
21454
|
+
if (!inTab) continue;
|
|
21455
|
+
const identity2 = identify(pane.cwd);
|
|
21456
|
+
if (identity2) counts.set(identity2, (counts.get(identity2) ?? 0) + 1);
|
|
21457
|
+
}
|
|
21458
|
+
const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
21459
|
+
const tied = ranked.filter(([, n]) => n === ranked[0]?.[1]).map(([identity2]) => identity2);
|
|
21460
|
+
const claimedTied = tied.filter((identity2) => claimedSet.has(identity2));
|
|
21461
|
+
const owner = tied.length === 1 ? tied[0] : claimedTied.length === 1 ? claimedTied[0] : void 0;
|
|
21462
|
+
const enriched = { ...tab };
|
|
21463
|
+
if (owner) enriched.owner = owner;
|
|
21464
|
+
if (ranked.length) enriched.ownedPanes = Object.fromEntries(ranked);
|
|
21465
|
+
return enriched;
|
|
21466
|
+
});
|
|
21467
|
+
}
|
|
21468
|
+
var PANE_CWD_TTL_SECONDS = 6 * 3600;
|
|
21469
|
+
function paneCwdCachePath(env2 = process.env) {
|
|
21470
|
+
return env2.ZCRANK_CWD_CACHE?.trim() || join35(homedir15(), ".local/state/zcrank/pane-cwds.json");
|
|
21471
|
+
}
|
|
21472
|
+
function loadCwdCache(path) {
|
|
21473
|
+
try {
|
|
21474
|
+
const parsed = JSON.parse(readFileSync30(path, "utf8"));
|
|
21475
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
21476
|
+
} catch {
|
|
21477
|
+
return {};
|
|
21478
|
+
}
|
|
21479
|
+
}
|
|
21480
|
+
function saveCwdCache(path, cache) {
|
|
21481
|
+
try {
|
|
21482
|
+
mkdirSync12(dirname22(path), { recursive: true });
|
|
21483
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
21484
|
+
writeFileSync15(tmp, JSON.stringify(cache));
|
|
21485
|
+
renameSync10(tmp, path);
|
|
21486
|
+
} catch {
|
|
21487
|
+
}
|
|
21488
|
+
}
|
|
21489
|
+
function cwdEntryFresh(entry, nowSeconds) {
|
|
21490
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return false;
|
|
21491
|
+
const ts = Number(entry.ts ?? 0);
|
|
21492
|
+
return Number.isFinite(ts) && nowSeconds - ts < PANE_CWD_TTL_SECONDS;
|
|
21493
|
+
}
|
|
21494
|
+
function resolvePaneCwds(fetch2, opts = {}) {
|
|
21495
|
+
const now = (opts.nowMs ?? Date.now()) / 1e3;
|
|
21496
|
+
const path = opts.cachePath ?? paneCwdCachePath();
|
|
21497
|
+
const get = () => {
|
|
21498
|
+
try {
|
|
21499
|
+
const rows = fetch2();
|
|
21500
|
+
return Array.isArray(rows) ? rows : [];
|
|
21501
|
+
} catch {
|
|
21502
|
+
return [];
|
|
21503
|
+
}
|
|
21504
|
+
};
|
|
21505
|
+
const panes = get();
|
|
21506
|
+
if (!panes.length) return panes;
|
|
21507
|
+
const cache = loadCwdCache(path);
|
|
21508
|
+
const sameTab = (entry, pane) => (entry.tab_id ?? null) === (pane.tabId ?? null);
|
|
21509
|
+
const fill = () => {
|
|
21510
|
+
let unknown2 = 0;
|
|
21511
|
+
for (const pane of panes) {
|
|
21512
|
+
if (pane.isPlugin) continue;
|
|
21513
|
+
const key = String(pane.id);
|
|
21514
|
+
if (pane.cwd) {
|
|
21515
|
+
cache[key] = { cwd: pane.cwd, tab_id: pane.tabId ?? null, ts: now };
|
|
21516
|
+
continue;
|
|
21517
|
+
}
|
|
21518
|
+
const entry = cache[key];
|
|
21519
|
+
if (cwdEntryFresh(entry, now) && sameTab(entry, pane) && typeof entry.cwd === "string" && entry.cwd) {
|
|
21520
|
+
pane.cwd = entry.cwd;
|
|
21521
|
+
} else {
|
|
21522
|
+
unknown2++;
|
|
21523
|
+
}
|
|
21524
|
+
}
|
|
21525
|
+
return unknown2;
|
|
21526
|
+
};
|
|
21527
|
+
let unknown = fill();
|
|
21528
|
+
for (let attempt = 0; attempt < (opts.retries ?? 0) && unknown; attempt++) {
|
|
21529
|
+
const again = new Map(get().filter((pane) => pane.cwd).map((pane) => [pane.id, pane.cwd]));
|
|
21530
|
+
for (const pane of panes) {
|
|
21531
|
+
const cwd = again.get(pane.id);
|
|
21532
|
+
if (!pane.isPlugin && !pane.cwd && cwd) pane.cwd = cwd;
|
|
21533
|
+
}
|
|
21534
|
+
unknown = fill();
|
|
21535
|
+
}
|
|
21536
|
+
const live = new Set(panes.filter((pane) => !pane.isPlugin).map((pane) => String(pane.id)));
|
|
21537
|
+
for (const key of Object.keys(cache)) {
|
|
21538
|
+
if (!live.has(key) || !cwdEntryFresh(cache[key], now)) delete cache[key];
|
|
21539
|
+
}
|
|
21540
|
+
saveCwdCache(path, cache);
|
|
21541
|
+
return panes;
|
|
21542
|
+
}
|
|
21543
|
+
function liveTabsWithOwners(deps = {}) {
|
|
21544
|
+
const tabs = liveTabs(deps);
|
|
21545
|
+
if (!tabs.length) return tabs;
|
|
21546
|
+
const cache = /* @__PURE__ */ new Map();
|
|
21547
|
+
const budget = { totalMs: IDENTITY_BUDGET_MS };
|
|
21548
|
+
const panes = resolvePaneCwds(() => livePanes(deps), { retries: deps.retries ?? 1, nowMs: deps.nowMs, cachePath: deps.cachePath });
|
|
21549
|
+
return withTabOwners(tabs, panes, (cwd) => projectIdentity(cwd, { shell: deps.shell, cache, budget }), deps.claimed ?? []);
|
|
21550
|
+
}
|
|
21551
|
+
function claimedRoots(store, keys = storedTabKeys(store)) {
|
|
21552
|
+
return [...new Set(Object.values(storedTabRoots(store, keys)))].sort();
|
|
21553
|
+
}
|
|
21554
|
+
var IDENTITY_BUDGET_MS = 2e4;
|
|
21173
21555
|
function candidateNames(project) {
|
|
21174
21556
|
const out = [project.name.trim()].filter(Boolean);
|
|
21175
21557
|
for (const agent of project.agents) {
|
|
@@ -21182,13 +21564,41 @@ function candidateNames(project) {
|
|
|
21182
21564
|
}
|
|
21183
21565
|
return out;
|
|
21184
21566
|
}
|
|
21185
|
-
function
|
|
21567
|
+
function keyForLiveTab(tab, storedTabs) {
|
|
21568
|
+
const display = tabDisplay(tab.name);
|
|
21569
|
+
if (storedTabs.includes(display)) return display;
|
|
21570
|
+
return storedTabs.find((s) => canon(s) && canon(s) === canon(display)) ?? display;
|
|
21571
|
+
}
|
|
21572
|
+
function ownerOfLiveTab(live, liveTab) {
|
|
21573
|
+
if (liveTab === void 0) return void 0;
|
|
21574
|
+
return live.find((tab) => tabDisplay(tab.name) === liveTab)?.owner;
|
|
21575
|
+
}
|
|
21576
|
+
function resolveTabForProject(project, live, storedTabs, scope = {}) {
|
|
21577
|
+
const byCwd = resolveByCwd(project, live, storedTabs, scope);
|
|
21578
|
+
if (byCwd) return byCwd;
|
|
21579
|
+
const resolution = resolveByName(project, live, storedTabs);
|
|
21580
|
+
const tabOwner = ownerOfLiveTab(live, resolution.liveTab);
|
|
21581
|
+
return tabOwner ? { ...resolution, tabOwner } : resolution;
|
|
21582
|
+
}
|
|
21583
|
+
function resolveByCwd(project, live, storedTabs, scope) {
|
|
21584
|
+
const identity2 = scope.identity;
|
|
21585
|
+
if (!identity2) return void 0;
|
|
21586
|
+
const owned = live.filter((tab2) => tab2.owner === identity2);
|
|
21587
|
+
if (!owned.length) return void 0;
|
|
21588
|
+
const candidateCanons = new Set(candidateNames(project).map((name) => canon(name)).filter(Boolean));
|
|
21589
|
+
const ranked = [...owned].sort((a, b) => Number(candidateCanons.has(canon(b.name))) - Number(candidateCanons.has(canon(a.name))) || (b.ownedPanes?.[identity2] ?? 0) - (a.ownedPanes?.[identity2] ?? 0) || a.position - b.position);
|
|
21590
|
+
const tab = ranked[0];
|
|
21591
|
+
if (!tab) return void 0;
|
|
21592
|
+
let key = keyForLiveTab(tab, storedTabs);
|
|
21593
|
+
if (!storedTabs.includes(key)) {
|
|
21594
|
+
const rooted = storedTabs.find((stored) => scope.roots?.[stored] === identity2);
|
|
21595
|
+
if (rooted !== void 0) key = rooted;
|
|
21596
|
+
}
|
|
21597
|
+
return { tab: key, source: "live-cwd", matched: identity2, storedAsIs: false, liveTab: tabDisplay(tab.name), tabOwner: identity2 };
|
|
21598
|
+
}
|
|
21599
|
+
function resolveByName(project, live, storedTabs) {
|
|
21186
21600
|
const candidates = candidateNames(project);
|
|
21187
|
-
const keyFor = (tab) =>
|
|
21188
|
-
const display = tabDisplay(tab.name);
|
|
21189
|
-
if (storedTabs.includes(display)) return display;
|
|
21190
|
-
return storedTabs.find((s) => canon(s) && canon(s) === canon(display)) ?? display;
|
|
21191
|
-
};
|
|
21601
|
+
const keyFor = (tab) => keyForLiveTab(tab, storedTabs);
|
|
21192
21602
|
const sideSets = [
|
|
21193
21603
|
(tab) => [tab.name],
|
|
21194
21604
|
(tab) => tabDisplay(tab.name).split(":")
|
|
@@ -21224,7 +21634,7 @@ function resolveTabForProject(project, live, storedTabs) {
|
|
|
21224
21634
|
function projectNameFor(handle, registryName) {
|
|
21225
21635
|
if (registryName && registryName.trim()) return registryName.trim();
|
|
21226
21636
|
try {
|
|
21227
|
-
const manifest = JSON.parse(
|
|
21637
|
+
const manifest = JSON.parse(readFileSync30(join35(handle.root, ".project.json"), "utf8"));
|
|
21228
21638
|
const name = manifest.project_name;
|
|
21229
21639
|
if (typeof name === "string" && name.trim()) return name.trim();
|
|
21230
21640
|
} catch {
|
|
@@ -21235,7 +21645,7 @@ function projectAgentsFor(handle, registryAgents) {
|
|
|
21235
21645
|
const fromRecord = Object.entries(registryAgents ?? {}).map(([agentHandle, value]) => ({ handle: agentHandle, role: String(value?.role ?? "") }));
|
|
21236
21646
|
if (fromRecord.length) return fromRecord;
|
|
21237
21647
|
try {
|
|
21238
|
-
const manifest = JSON.parse(
|
|
21648
|
+
const manifest = JSON.parse(readFileSync30(join35(handle.root, ".project.json"), "utf8"));
|
|
21239
21649
|
const agents = manifest.agents;
|
|
21240
21650
|
if (agents && typeof agents === "object" && !Array.isArray(agents)) {
|
|
21241
21651
|
return Object.entries(agents).map(([agentHandle, value]) => ({ handle: agentHandle, role: typeof value?.role === "string" ? value.role : "" }));
|
|
@@ -21244,17 +21654,22 @@ function projectAgentsFor(handle, registryAgents) {
|
|
|
21244
21654
|
}
|
|
21245
21655
|
return [];
|
|
21246
21656
|
}
|
|
21247
|
-
function keysNamingTab(tab, storedKeys) {
|
|
21657
|
+
function keysNamingTab(tab, storedKeys, scope = {}) {
|
|
21248
21658
|
const c = canon(tab);
|
|
21249
21659
|
const out = [];
|
|
21250
21660
|
if (storedKeys.includes(tab)) out.push(tab);
|
|
21251
21661
|
for (const key of storedKeys) {
|
|
21252
21662
|
if (key !== tab && c && canon(key) === c) out.push(key);
|
|
21253
21663
|
}
|
|
21664
|
+
if (scope.owner) {
|
|
21665
|
+
for (const key of storedKeys) {
|
|
21666
|
+
if (scope.roots?.[key] === scope.owner && !out.includes(key)) out.push(key);
|
|
21667
|
+
}
|
|
21668
|
+
}
|
|
21254
21669
|
return out;
|
|
21255
21670
|
}
|
|
21256
|
-
function snapshotCrank(store, tab, storedKeys, nowMs, beatLimit = 5) {
|
|
21257
|
-
const keys = keysNamingTab(tab, storedKeys);
|
|
21671
|
+
function snapshotCrank(store, tab, storedKeys, nowMs, beatLimit = 5, scope = {}) {
|
|
21672
|
+
const keys = keysNamingTab(tab, storedKeys, scope);
|
|
21258
21673
|
const readKeys = keys.length ? keys : [tab];
|
|
21259
21674
|
const enabledValues = keys.map((key) => store.tabEnabled(key)).filter((v) => v !== void 0);
|
|
21260
21675
|
const enabled2 = enabledValues.length ? keys.length > 0 && keys.every((key) => store.tabEnabled(key) === "1") : void 0;
|
|
@@ -21280,12 +21695,13 @@ function snapshotCrank(store, tab, storedKeys, nowMs, beatLimit = 5) {
|
|
|
21280
21695
|
}
|
|
21281
21696
|
function planRenameMigration(input) {
|
|
21282
21697
|
const target = input.resolution.liveTab;
|
|
21283
|
-
if (!target || input.resolution.source !== "live-exact" && input.resolution.source !== "live-prefix") return void 0;
|
|
21698
|
+
if (!target || input.resolution.source !== "live-exact" && input.resolution.source !== "live-prefix" && input.resolution.source !== "live-cwd") return void 0;
|
|
21284
21699
|
const targetCanon = canon(target);
|
|
21285
21700
|
const wanted = new Set([targetCanon, ...input.candidates.map((name) => canon(name))].filter(Boolean));
|
|
21286
21701
|
const otherLive = new Set(input.live.map((tab) => canon(tab.name)).filter((c) => c && c !== targetCanon));
|
|
21287
21702
|
const oldKeys = [...new Set(input.storedKeys)].filter((key) => {
|
|
21288
21703
|
if (key === target) return false;
|
|
21704
|
+
if (input.identity && input.roots?.[key] === input.identity) return true;
|
|
21289
21705
|
const c = canon(key);
|
|
21290
21706
|
return c !== "" && wanted.has(c) && !otherLive.has(c);
|
|
21291
21707
|
});
|
|
@@ -21316,12 +21732,36 @@ function applyRenameMigration(store, plan) {
|
|
|
21316
21732
|
store.deleteKeys(`zcrank:tab:${old}`, ...oldPanes, `zcrank:log:${old}`);
|
|
21317
21733
|
}
|
|
21318
21734
|
}
|
|
21319
|
-
function buildCrankContext(project, live, storedKeys) {
|
|
21320
|
-
|
|
21735
|
+
function buildCrankContext(project, live, storedKeys, scope = {}) {
|
|
21736
|
+
const roots = scope.roots ?? {};
|
|
21737
|
+
return {
|
|
21738
|
+
resolution: resolveTabForProject(project, live, storedKeys, { identity: scope.identity, roots }),
|
|
21739
|
+
candidates: candidateNames(project),
|
|
21740
|
+
live,
|
|
21741
|
+
storedKeys,
|
|
21742
|
+
identity: scope.identity,
|
|
21743
|
+
roots
|
|
21744
|
+
};
|
|
21745
|
+
}
|
|
21746
|
+
function keyScopeOf(ctx) {
|
|
21747
|
+
return { owner: ctx.resolution.tabOwner, roots: ctx.roots };
|
|
21321
21748
|
}
|
|
21322
21749
|
function storedTabKeys(store) {
|
|
21323
21750
|
return store.tabKeys().map((key) => key.split(":").slice(2).join(":"));
|
|
21324
21751
|
}
|
|
21752
|
+
function storedTabRoots(store, keys) {
|
|
21753
|
+
const roots = {};
|
|
21754
|
+
for (const key of keys) {
|
|
21755
|
+
let root;
|
|
21756
|
+
try {
|
|
21757
|
+
root = store.tabRoot(key);
|
|
21758
|
+
} catch (error) {
|
|
21759
|
+
if (!(error instanceof Error) || !error.message.startsWith("Redis error reply")) throw error;
|
|
21760
|
+
}
|
|
21761
|
+
if (root) roots[key] = root;
|
|
21762
|
+
}
|
|
21763
|
+
return roots;
|
|
21764
|
+
}
|
|
21325
21765
|
function enableCrank(store, ctx, options = {}) {
|
|
21326
21766
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
21327
21767
|
const ts = crankTimestamp(now);
|
|
@@ -21331,16 +21771,20 @@ function enableCrank(store, ctx, options = {}) {
|
|
|
21331
21771
|
storedKeys: ctx.storedKeys,
|
|
21332
21772
|
live: ctx.live,
|
|
21333
21773
|
intervals: (key) => store.tabInterval(key),
|
|
21334
|
-
chosenInterval: options.interval
|
|
21774
|
+
chosenInterval: options.interval,
|
|
21775
|
+
identity: ctx.identity,
|
|
21776
|
+
roots: ctx.roots
|
|
21335
21777
|
});
|
|
21336
21778
|
const tab = plan?.target ?? ctx.resolution.tab;
|
|
21337
21779
|
store.setTabEnabled(tab, true);
|
|
21780
|
+
writeRoot(store, tab, ctx.identity);
|
|
21338
21781
|
if (options.interval) store.setTabInterval(tab, options.interval);
|
|
21339
21782
|
if (plan) applyRenameMigration(store, plan);
|
|
21340
21783
|
else {
|
|
21341
|
-
for (const sibling of keysNamingTab(tab, ctx.storedKeys)) {
|
|
21784
|
+
for (const sibling of keysNamingTab(tab, ctx.storedKeys, keyScopeOf(ctx))) {
|
|
21342
21785
|
if (sibling === tab) continue;
|
|
21343
21786
|
store.setTabEnabled(sibling, true);
|
|
21787
|
+
writeRoot(store, sibling, ctx.identity);
|
|
21344
21788
|
if (options.interval) store.clearTabInterval(sibling);
|
|
21345
21789
|
}
|
|
21346
21790
|
}
|
|
@@ -21350,42 +21794,54 @@ function enableCrank(store, ctx, options = {}) {
|
|
|
21350
21794
|
for (const { from, to } of renamed) store.pushLog(tab, "renamed", `pj crank enable: ${from} -> ${to}`, ts);
|
|
21351
21795
|
return { tab, renamed, interval: options.interval ?? plan?.carryInterval };
|
|
21352
21796
|
}
|
|
21353
|
-
function
|
|
21797
|
+
function writeRoot(store, tab, root) {
|
|
21798
|
+
if (root) store.setTabRoot(tab, root);
|
|
21799
|
+
}
|
|
21800
|
+
function disableCrank(store, tab, now = /* @__PURE__ */ new Date(), root) {
|
|
21354
21801
|
store.setTabEnabled(tab, false);
|
|
21802
|
+
writeRoot(store, tab, root);
|
|
21355
21803
|
store.pushLog(tab, "disabled", "pj crank disable", crankTimestamp(now));
|
|
21356
21804
|
}
|
|
21357
|
-
function setCrankInterval(store, tab, storedKeys, interval, now = /* @__PURE__ */ new Date()) {
|
|
21358
|
-
for (const key of keysNamingTab(tab, storedKeys)) {
|
|
21805
|
+
function setCrankInterval(store, tab, storedKeys, interval, now = /* @__PURE__ */ new Date(), scope = {}) {
|
|
21806
|
+
for (const key of keysNamingTab(tab, storedKeys, scope)) {
|
|
21359
21807
|
if (key !== tab) store.clearTabInterval(key);
|
|
21360
21808
|
}
|
|
21361
21809
|
if (interval) store.setTabInterval(tab, interval);
|
|
21362
21810
|
else store.clearTabInterval(tab);
|
|
21811
|
+
writeRoot(store, tab, scope.root);
|
|
21363
21812
|
const action = interval ? `pj crank tui: interval ${interval}` : `pj crank tui: interval reset to global (${store.globalInterval()})`;
|
|
21364
21813
|
store.pushLog(tab, "interval", action, crankTimestamp(now));
|
|
21365
21814
|
}
|
|
21366
21815
|
function buildCrankList(input) {
|
|
21367
21816
|
const global = input.globalInterval ?? ZCRANK_DEFAULT_INTERVAL;
|
|
21368
|
-
const
|
|
21369
|
-
|
|
21370
|
-
|
|
21371
|
-
|
|
21817
|
+
const roots = input.roots ?? {};
|
|
21818
|
+
const intervalOf = (tab, owner) => {
|
|
21819
|
+
const keys = keysNamingTab(tab, input.storedTabs, { owner, roots });
|
|
21820
|
+
return effectiveInterval((keys.length ? keys : [tab]).map((key) => input.tabInterval?.(key)), global);
|
|
21821
|
+
};
|
|
21372
21822
|
const rows = [];
|
|
21373
21823
|
const consumed = /* @__PURE__ */ new Set();
|
|
21824
|
+
const consumedKeys = /* @__PURE__ */ new Set();
|
|
21374
21825
|
for (const project of input.projects) {
|
|
21375
|
-
const
|
|
21826
|
+
const identity2 = project.repoPath ? input.identityOf?.(project.repoPath) : void 0;
|
|
21827
|
+
const resolution = resolveTabForProject(project, input.live, input.storedTabs, { identity: identity2, roots });
|
|
21376
21828
|
consumed.add(canon(resolution.tab));
|
|
21377
|
-
const
|
|
21829
|
+
const keys = keysNamingTab(resolution.tab, input.storedTabs, { owner: resolution.tabOwner, roots });
|
|
21830
|
+
for (const key of keys) consumedKeys.add(key);
|
|
21831
|
+
const values = (keys.length ? keys : [resolution.tab]).map((key) => input.tabEnabled(key)).filter((v) => v !== void 0);
|
|
21832
|
+
const enabled2 = values.length ? values.every((v) => v === "1") ? "1" : "0" : void 0;
|
|
21378
21833
|
rows.push({
|
|
21379
21834
|
slug: project.slug,
|
|
21380
21835
|
name: project.name,
|
|
21381
21836
|
tab: resolution.tab,
|
|
21382
21837
|
crank: enabled2 === "1" ? "ON" : enabled2 === "0" ? "OFF" : "\u2014",
|
|
21383
21838
|
source: resolution.source,
|
|
21384
|
-
...
|
|
21839
|
+
...resolution.liveTab !== void 0 ? { liveTab: resolution.liveTab } : {},
|
|
21840
|
+
...intervalOf(resolution.tab, resolution.tabOwner)
|
|
21385
21841
|
});
|
|
21386
21842
|
}
|
|
21387
|
-
const orphans = input.storedTabs.filter((stored) => !consumed.has(canon(stored))).map((stored) => ({ tab: stored, enabled: input.tabEnabled(stored) === "1", ...intervalOf(stored) }));
|
|
21388
|
-
return { rows, orphans };
|
|
21843
|
+
const orphans = input.storedTabs.filter((stored) => !consumed.has(canon(stored)) && !consumedKeys.has(stored)).map((stored) => ({ tab: stored, enabled: input.tabEnabled(stored) === "1", ...intervalOf(stored) }));
|
|
21844
|
+
return { engine: input.engine, rows, orphans };
|
|
21389
21845
|
}
|
|
21390
21846
|
function crankTimestamp(now = /* @__PURE__ */ new Date()) {
|
|
21391
21847
|
return now.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
@@ -21401,17 +21857,25 @@ function renderLogRow(raw) {
|
|
|
21401
21857
|
return null;
|
|
21402
21858
|
}
|
|
21403
21859
|
}
|
|
21860
|
+
function tabCell(row) {
|
|
21861
|
+
if (row.source !== "live-cwd" || row.liveTab === void 0) return row.tab;
|
|
21862
|
+
return canon(row.tab) === canon(row.liveTab) ? row.liveTab : `${row.liveTab} (key ${row.tab})`;
|
|
21863
|
+
}
|
|
21404
21864
|
function formatCrankList(report) {
|
|
21405
21865
|
const lines = [""];
|
|
21406
21866
|
lines.push(` ${heading("Crank by project")}`);
|
|
21867
|
+
if (report.engine) {
|
|
21868
|
+
const summary = engineSummary(report.engine);
|
|
21869
|
+
lines.push(summary.tone === "unknown" ? ` ${dim(summary.text)}` : ` ${summary.tone === "ok" ? green("Engine") : yellow("Engine")} ${summary.tone === "warn" ? yellow(summary.text) : dim(summary.text)}`);
|
|
21870
|
+
}
|
|
21407
21871
|
lines.push("");
|
|
21408
21872
|
const slugWidth = Math.max("PROJECT".length, ...report.rows.map((row) => row.slug.length));
|
|
21409
|
-
const tabWidth = Math.max("TAB".length, ...report.rows.map((row) => row.
|
|
21873
|
+
const tabWidth = Math.max("TAB".length, ...report.rows.map((row) => tabCell(row).length), ...report.orphans.map((orphan) => orphan.tab.length));
|
|
21410
21874
|
const intervalCell = (row) => row.inherited ? dim(`${row.interval} (global)`) : row.interval;
|
|
21411
21875
|
lines.push(` ${"PROJECT".padEnd(slugWidth)} ${"TAB".padEnd(tabWidth)} ${"CRANK".padEnd(5)} INTERVAL`);
|
|
21412
21876
|
for (const row of report.rows) {
|
|
21413
21877
|
const state = row.crank === "ON" ? green("ON") : row.crank === "OFF" ? dim("OFF") : dim("\u2014");
|
|
21414
|
-
lines.push(` ${cyan(row.slug.padEnd(slugWidth))} ${row.
|
|
21878
|
+
lines.push(` ${cyan(row.slug.padEnd(slugWidth))} ${tabCell(row).padEnd(tabWidth)} ${state}${" ".repeat(Math.max(0, 5 - (row.crank === "ON" ? 2 : row.crank === "OFF" ? 3 : 1)))} ${intervalCell(row)}`);
|
|
21415
21879
|
}
|
|
21416
21880
|
if (report.orphans.length) {
|
|
21417
21881
|
lines.push("");
|
|
@@ -21426,34 +21890,492 @@ function formatCrankList(report) {
|
|
|
21426
21890
|
|
|
21427
21891
|
// src/crank/controller.ts
|
|
21428
21892
|
var LIVE_TABS_TTL_MS = 3e4;
|
|
21893
|
+
var LIVE_OWNERS_TTL_MS = 3e5;
|
|
21894
|
+
function ownerMemoReader(deps = {}) {
|
|
21895
|
+
const clock = deps.clock ?? Date.now;
|
|
21896
|
+
const read = deps.read ?? ((claimed) => liveTabsWithOwners({ claimed }));
|
|
21897
|
+
const listTabs = deps.tabs ?? (() => liveTabs());
|
|
21898
|
+
const readClaimed = () => {
|
|
21899
|
+
try {
|
|
21900
|
+
return deps.claimed?.() ?? [];
|
|
21901
|
+
} catch {
|
|
21902
|
+
return [];
|
|
21903
|
+
}
|
|
21904
|
+
};
|
|
21905
|
+
let memo;
|
|
21906
|
+
return () => {
|
|
21907
|
+
const now = clock();
|
|
21908
|
+
const tabs = listTabs();
|
|
21909
|
+
const claimed = readClaimed();
|
|
21910
|
+
const claimedSig = claimed.join("\n");
|
|
21911
|
+
if (memo && memo.claimed === claimedSig && now - memo.at <= LIVE_OWNERS_TTL_MS && tabs.length && tabs.every((tab) => tab.id !== void 0 && memo?.byId.has(tab.id))) {
|
|
21912
|
+
const known = memo.byId;
|
|
21913
|
+
return tabs.map((tab) => ({ ...tab, ...known.get(tab.id) }));
|
|
21914
|
+
}
|
|
21915
|
+
const full = read(claimed);
|
|
21916
|
+
memo = full.length && full.every((tab) => tab.id !== void 0) ? { at: now, claimed: claimedSig, byId: new Map(full.map((tab) => [tab.id, { owner: tab.owner, ownedPanes: tab.ownedPanes }])) } : void 0;
|
|
21917
|
+
return full;
|
|
21918
|
+
};
|
|
21919
|
+
}
|
|
21429
21920
|
function createCrankController(deps) {
|
|
21430
21921
|
const clock = deps.clock ?? Date.now;
|
|
21431
|
-
const readLive = deps.live ?? (() =>
|
|
21922
|
+
const readLive = deps.live ?? ownerMemoReader({ clock, claimed: () => claimedRoots(deps.store) });
|
|
21923
|
+
const readEngine = (nowMs) => {
|
|
21924
|
+
try {
|
|
21925
|
+
return (deps.engine ?? ((now) => readEngineStatus({ nowMs: now })))(nowMs);
|
|
21926
|
+
} catch {
|
|
21927
|
+
return unknownEngineStatus(nowMs);
|
|
21928
|
+
}
|
|
21929
|
+
};
|
|
21432
21930
|
let cachedLive;
|
|
21433
21931
|
const live = (fresh) => {
|
|
21434
21932
|
const now = clock();
|
|
21435
21933
|
if (fresh || !cachedLive || now - cachedLive.at > LIVE_TABS_TTL_MS) cachedLive = { at: now, tabs: readLive() };
|
|
21436
21934
|
return cachedLive.tabs;
|
|
21437
21935
|
};
|
|
21438
|
-
const context = (fresh) =>
|
|
21936
|
+
const context = (fresh) => {
|
|
21937
|
+
const keys = storedTabKeys(deps.store);
|
|
21938
|
+
return buildCrankContext(deps.project, live(fresh), keys, { identity: deps.identity, roots: storedTabRoots(deps.store, keys) });
|
|
21939
|
+
};
|
|
21439
21940
|
return {
|
|
21440
21941
|
slug: deps.slug,
|
|
21441
21942
|
name: deps.project.name,
|
|
21442
21943
|
read(nowMs = clock()) {
|
|
21443
21944
|
const ctx = context(false);
|
|
21444
|
-
return {
|
|
21945
|
+
return {
|
|
21946
|
+
slug: deps.slug,
|
|
21947
|
+
name: deps.project.name,
|
|
21948
|
+
resolution: ctx.resolution,
|
|
21949
|
+
snapshot: snapshotCrank(deps.store, ctx.resolution.tab, ctx.storedKeys, nowMs, 5, keyScopeOf(ctx)),
|
|
21950
|
+
engine: readEngine(nowMs)
|
|
21951
|
+
};
|
|
21445
21952
|
},
|
|
21446
21953
|
enable: () => enableCrank(deps.store, context(true), { now: new Date(clock()) }),
|
|
21447
21954
|
disable() {
|
|
21448
|
-
|
|
21955
|
+
const ctx = context(true);
|
|
21956
|
+
disableCrank(deps.store, ctx.resolution.tab, new Date(clock()), ctx.identity);
|
|
21449
21957
|
},
|
|
21450
21958
|
setInterval(interval) {
|
|
21451
21959
|
const ctx = context(true);
|
|
21452
|
-
setCrankInterval(deps.store, ctx.resolution.tab, ctx.storedKeys, interval, new Date(clock()));
|
|
21960
|
+
setCrankInterval(deps.store, ctx.resolution.tab, ctx.storedKeys, interval, new Date(clock()), { ...keyScopeOf(ctx), root: ctx.identity });
|
|
21453
21961
|
}
|
|
21454
21962
|
};
|
|
21455
21963
|
}
|
|
21456
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
|
+
|
|
21457
22379
|
// src/crank/cli.ts
|
|
21458
22380
|
var loadCrankScreens = () => loadScreen("./crank/tui.js", import.meta.url);
|
|
21459
22381
|
var xmark = `${red(glyph.fail)}`;
|
|
@@ -21470,13 +22392,20 @@ function currentProjectNames() {
|
|
|
21470
22392
|
names: {
|
|
21471
22393
|
name: projectNameFor(handle, record3?.name),
|
|
21472
22394
|
agents: projectAgentsFor(handle, record3?.agents)
|
|
21473
|
-
}
|
|
22395
|
+
},
|
|
22396
|
+
// PJAN-201: where the project lives (its main worktree), independent of any tab name.
|
|
22397
|
+
identity: projectIdentity(handle.root)
|
|
21474
22398
|
};
|
|
21475
22399
|
}
|
|
22400
|
+
function contextFor(project, store) {
|
|
22401
|
+
const keys = storedTabKeys(store);
|
|
22402
|
+
const roots = storedTabRoots(store, keys);
|
|
22403
|
+
return buildCrankContext(project.names, liveTabsWithOwners({ claimed: Object.values(roots) }), keys, { identity: project.identity, roots });
|
|
22404
|
+
}
|
|
21476
22405
|
function resolveCurrentTab() {
|
|
21477
22406
|
const project = currentProjectNames();
|
|
21478
22407
|
const store = redis();
|
|
21479
|
-
const ctx =
|
|
22408
|
+
const ctx = contextFor(project, store);
|
|
21480
22409
|
return { slug: project.slug, tab: ctx.resolution.tab, storedAsIs: ctx.resolution.storedAsIs };
|
|
21481
22410
|
}
|
|
21482
22411
|
function registerCrankCli(program2) {
|
|
@@ -21490,6 +22419,9 @@ function registerCrankCli(program2) {
|
|
|
21490
22419
|
crankCmd.command("tui").description("Full-screen crank dashboard: toggle, interval, last runs, countdown to the next run, log").action(async () => {
|
|
21491
22420
|
await runTui();
|
|
21492
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
|
+
});
|
|
21493
22425
|
crankCmd.command("list").description("Every registry project with crank ON/OFF and its resolved tab, plus orphan zcrank keys").action(() => {
|
|
21494
22426
|
try {
|
|
21495
22427
|
const store = redis();
|
|
@@ -21507,12 +22439,16 @@ function registerCrankCli(program2) {
|
|
|
21507
22439
|
role: String(value?.role ?? "")
|
|
21508
22440
|
}))
|
|
21509
22441
|
}));
|
|
21510
|
-
const live = liveTabs();
|
|
21511
22442
|
const storedTabs = storedTabKeys(store);
|
|
22443
|
+
const live = liveTabsWithOwners({ claimed: Object.values(storedTabRoots(store, storedTabs)) });
|
|
22444
|
+
const identityCache = /* @__PURE__ */ new Map();
|
|
21512
22445
|
const report = buildCrankList({
|
|
21513
22446
|
projects,
|
|
21514
22447
|
live,
|
|
21515
22448
|
storedTabs,
|
|
22449
|
+
identityOf: (repoPath) => projectIdentity(repoPath, { cache: identityCache }),
|
|
22450
|
+
roots: storedTabRoots(store, storedTabs),
|
|
22451
|
+
engine: readEngineStatus(),
|
|
21516
22452
|
tabEnabled: (tab) => store.tabEnabled(tab),
|
|
21517
22453
|
tabInterval: (tab) => store.tabInterval(tab),
|
|
21518
22454
|
globalInterval: store.globalInterval()
|
|
@@ -21576,9 +22512,9 @@ async function runEnable(options) {
|
|
|
21576
22512
|
}
|
|
21577
22513
|
const project = currentProjectNames();
|
|
21578
22514
|
const store = liveStore();
|
|
21579
|
-
const ctx =
|
|
22515
|
+
const ctx = contextFor(project, store);
|
|
21580
22516
|
if (!chosen && isInteractive()) {
|
|
21581
|
-
const snap = snapshotCrank(store, ctx.resolution.tab, ctx.storedKeys, Date.now());
|
|
22517
|
+
const snap = snapshotCrank(store, ctx.resolution.tab, ctx.storedKeys, Date.now(), 5, keyScopeOf(ctx));
|
|
21582
22518
|
const screens = await loadCrankScreens();
|
|
21583
22519
|
const picked = await screens.pickInterval({ slug: project.slug, tab: ctx.resolution.liveTab ?? ctx.resolution.tab, effective: snap.interval, inherited: snap.inherited, global: snap.global });
|
|
21584
22520
|
if (!picked) {
|
|
@@ -21592,7 +22528,8 @@ async function runEnable(options) {
|
|
|
21592
22528
|
for (const { from, to } of result2.renamed) {
|
|
21593
22529
|
console.log(` ${yellow(glyph.warn)} tab renamed ${dim(glyph.dot)} ${from} ${dim(glyph.pointer)} ${bold(to)} ${dim("(old key merged into the new one and removed)")}`);
|
|
21594
22530
|
}
|
|
21595
|
-
const
|
|
22531
|
+
const afterKeys = storedTabKeys(store);
|
|
22532
|
+
const live = snapshotCrank(store, result2.tab, afterKeys, Date.now(), 5, { owner: ctx.resolution.tabOwner, roots: storedTabRoots(store, afterKeys) });
|
|
21596
22533
|
console.log(` ${dim("interval")} ${live.interval}${live.inherited ? dim(` (inherits global ${live.global})`) : ""}`);
|
|
21597
22534
|
if (ctx.resolution.storedAsIs) {
|
|
21598
22535
|
console.log(` ${yellow(glyph.warn)} ${dim(`no live zellij tab or zcrank key matches "${project.names.name}" \u2014 stored under the project name as-is`)}`);
|
|
@@ -21605,8 +22542,8 @@ function runDisable() {
|
|
|
21605
22542
|
try {
|
|
21606
22543
|
const project = currentProjectNames();
|
|
21607
22544
|
const store = liveStore();
|
|
21608
|
-
const ctx =
|
|
21609
|
-
disableCrank(store, ctx.resolution.tab);
|
|
22545
|
+
const ctx = contextFor(project, store);
|
|
22546
|
+
disableCrank(store, ctx.resolution.tab, /* @__PURE__ */ new Date(), ctx.identity);
|
|
21610
22547
|
console.log(` ${dim("OFF")} ${cyan(project.slug)} ${dim(glyph.pointer)} ${bold(ctx.resolution.tab)}`);
|
|
21611
22548
|
if (ctx.resolution.storedAsIs) {
|
|
21612
22549
|
console.log(` ${yellow(glyph.warn)} ${dim(`no live zellij tab or zcrank key matches "${project.names.name}" \u2014 stored under the project name as-is`)}`);
|
|
@@ -21615,6 +22552,34 @@ function runDisable() {
|
|
|
21615
22552
|
fail(error, "crank");
|
|
21616
22553
|
}
|
|
21617
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
|
+
}
|
|
21618
22583
|
async function runTui() {
|
|
21619
22584
|
try {
|
|
21620
22585
|
if (!isInteractive()) {
|
|
@@ -21623,7 +22588,7 @@ async function runTui() {
|
|
|
21623
22588
|
}
|
|
21624
22589
|
const project = currentProjectNames();
|
|
21625
22590
|
const store = liveStore();
|
|
21626
|
-
const controller = createCrankController({ slug: project.slug, project: project.names, store });
|
|
22591
|
+
const controller = createCrankController({ slug: project.slug, project: project.names, store, identity: project.identity });
|
|
21627
22592
|
const screens = await loadCrankScreens();
|
|
21628
22593
|
await screens.runCrankDashboard(controller);
|
|
21629
22594
|
} catch (error) {
|
|
@@ -21658,9 +22623,9 @@ async function promptForRuleIds(rules) {
|
|
|
21658
22623
|
return selected;
|
|
21659
22624
|
}
|
|
21660
22625
|
function readJson2(path) {
|
|
21661
|
-
if (!
|
|
22626
|
+
if (!existsSync26(path)) return void 0;
|
|
21662
22627
|
try {
|
|
21663
|
-
const parsed = JSON.parse(
|
|
22628
|
+
const parsed = JSON.parse(readFileSync32(path, "utf8"));
|
|
21664
22629
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
21665
22630
|
} catch {
|
|
21666
22631
|
return void 0;
|
|
@@ -21677,9 +22642,9 @@ function packageNameToProjectName(value) {
|
|
|
21677
22642
|
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
21678
22643
|
}
|
|
21679
22644
|
function deriveProjectDefaults(targetDir) {
|
|
21680
|
-
const manifest = readJson2(
|
|
21681
|
-
const pkg = readJson2(
|
|
21682
|
-
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";
|
|
21683
22648
|
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
21684
22649
|
return {
|
|
21685
22650
|
name,
|
|
@@ -21753,7 +22718,7 @@ function actionNeedsRun(plan, kind, syncMode) {
|
|
|
21753
22718
|
if (!action || action.kind !== "project.write-manifest") return false;
|
|
21754
22719
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
21755
22720
|
`;
|
|
21756
|
-
return !
|
|
22721
|
+
return !existsSync26(action.path) || readFileSync32(action.path, "utf8") !== next;
|
|
21757
22722
|
}
|
|
21758
22723
|
if (kind === "copier.copy.commonproject") return true;
|
|
21759
22724
|
if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
@@ -21810,28 +22775,28 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
21810
22775
|
if (!targetDir && cwdGitRoot) {
|
|
21811
22776
|
targetDir = cwdGitRoot;
|
|
21812
22777
|
}
|
|
21813
|
-
if (!targetDir && !name &&
|
|
22778
|
+
if (!targetDir && !name && existsSync26(join37(cwd, ".project.json"))) {
|
|
21814
22779
|
targetDir = cwd;
|
|
21815
22780
|
}
|
|
21816
22781
|
if (!targetDir && interactive) {
|
|
21817
|
-
const promptedName = await promptTextValue("Project name", packageNameToProjectName(
|
|
22782
|
+
const promptedName = await promptTextValue("Project name", packageNameToProjectName(basename12(cwd)));
|
|
21818
22783
|
targetDir = resolve25(await promptTextValue("Project directory", projectTargetDirUnder(promptedName, cwd)));
|
|
21819
22784
|
name = promptedName;
|
|
21820
22785
|
}
|
|
21821
22786
|
if (!targetDir) {
|
|
21822
22787
|
throw new Error("Project name or --target-dir is required when project init is not run inside a git repo or a directory carrying .project.json");
|
|
21823
22788
|
}
|
|
21824
|
-
const targetExists =
|
|
22789
|
+
const targetExists = existsSync26(targetDir);
|
|
21825
22790
|
if (targetExists && !statSync9(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
21826
22791
|
const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
|
|
21827
|
-
const alreadyScaffolded = targetExists && (
|
|
22792
|
+
const alreadyScaffolded = targetExists && (existsSync26(join37(targetDir, ".project.json")) || existsSync26(join37(targetDir, ".copier-answers.yml")));
|
|
21828
22793
|
const syncMode = Boolean(
|
|
21829
22794
|
targetGitRoot && resolve25(targetGitRoot) === resolve25(targetDir) || alreadyScaffolded
|
|
21830
22795
|
);
|
|
21831
|
-
if (targetExists && !syncMode && !options.force &&
|
|
22796
|
+
if (targetExists && !syncMode && !options.force && readdirSync14(targetDir).length > 0) {
|
|
21832
22797
|
throw new Error(`Target already exists and is not empty: ${targetDir} (pass --force to render over it)`);
|
|
21833
22798
|
}
|
|
21834
|
-
const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(
|
|
22799
|
+
const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(basename12(targetDir)) ?? "Project", description: "" };
|
|
21835
22800
|
if (!name && interactive && !syncMode) {
|
|
21836
22801
|
name = await promptTextValue("Project name", defaults.name);
|
|
21837
22802
|
}
|
|
@@ -22346,7 +23311,7 @@ program.command("reindex").argument("[project-id]", "Canonical project ID (defau
|
|
|
22346
23311
|
else if (options.all) result2 = registryRequest(location, "POST", "/v1/reindex", {});
|
|
22347
23312
|
else {
|
|
22348
23313
|
const repo = projectId ? getProject(loadProjectRegistry(location), projectId).repo_path : resolveProject(process.cwd()).root;
|
|
22349
|
-
result2 = registryRequest(location, "POST", "/v1/index", { manifest_path:
|
|
23314
|
+
result2 = registryRequest(location, "POST", "/v1/index", { manifest_path: join37(repo, ".project.json") });
|
|
22350
23315
|
}
|
|
22351
23316
|
const statuses = result2.__registry_status;
|
|
22352
23317
|
const stale = Object.entries(statuses ?? {}).filter(([, item]) => item.status !== "ok");
|