@crouton-kit/crouter 0.3.81 → 0.3.82
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.
|
@@ -20,10 +20,17 @@
|
|
|
20
20
|
// over the terminal and the process never returns.
|
|
21
21
|
import { existsSync, statSync } from 'node:fs';
|
|
22
22
|
import { resolve as resolvePath } from 'node:path';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
import
|
|
23
|
+
// NOTE: the boot-path deps — ./spawn.js (bootRoot), ./resume.js (resumeRoot),
|
|
24
|
+
// ./package-health.js (missingPackageWarning + piAgentDir) — are DYNAMICALLY
|
|
25
|
+
// imported inside maybeBootRoot, NOT statically here. `cli.ts` calls
|
|
26
|
+
// maybeBootRoot on every invocation, and a static import chain that reaches the
|
|
27
|
+
// pi engine costs ~365ms of cold start that every leaf subcommand (`crtr memory
|
|
28
|
+
// list`, etc.) would pay despite never booting a root. The dynamic imports below
|
|
29
|
+
// are reached only after the early-return guards, i.e. only on a genuine
|
|
30
|
+
// front-door boot. Note we DELIBERATELY do NOT import `@earendil-works/pi-coding-agent`
|
|
31
|
+
// even on the boot path — the one thing we needed from it (getAgentDir) is
|
|
32
|
+
// reimplemented as package-health's `piAgentDir()` so the whole pi engine never
|
|
33
|
+
// loads in the FOREGROUND boot process (the broker loads it in parallel).
|
|
27
34
|
function isDir(p) {
|
|
28
35
|
try {
|
|
29
36
|
return existsSync(p) && statSync(p).isDirectory();
|
|
@@ -123,13 +130,19 @@ export async function maybeBootRoot(root, argv) {
|
|
|
123
130
|
return false;
|
|
124
131
|
}
|
|
125
132
|
// Unambiguous front-door launch → boot a resident root inline (exec pi in
|
|
126
|
-
// this terminal). Does not return.
|
|
133
|
+
// this terminal). Does not return. Load the heavy boot-path deps lazily now
|
|
134
|
+
// that we've committed to booting (see the import note at the top of file).
|
|
135
|
+
const [{ bootRoot }, { resumeRoot }, { missingPackageWarning, piAgentDir }] = await Promise.all([
|
|
136
|
+
import('./spawn.js'),
|
|
137
|
+
import('./resume.js'),
|
|
138
|
+
import('./package-health.js'),
|
|
139
|
+
]);
|
|
127
140
|
const args = parseRootArgs(tokens);
|
|
128
141
|
// Surface any stale/missing local-path pi package BEFORE the viewer takes over
|
|
129
142
|
// the terminal — pi discards a missing package silently, so this is the only
|
|
130
143
|
// moment the user sees why (e.g.) their ~/.claude commands vanished.
|
|
131
144
|
try {
|
|
132
|
-
const warn = missingPackageWarning(
|
|
145
|
+
const warn = missingPackageWarning(piAgentDir());
|
|
133
146
|
if (warn !== null)
|
|
134
147
|
process.stderr.write(`\n${warn}\n\n`);
|
|
135
148
|
}
|
|
@@ -4,6 +4,19 @@ export interface MissingPackage {
|
|
|
4
4
|
/** Absolute path it resolved to (which does not exist). */
|
|
5
5
|
resolved: string;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* The pi agent config dir (`~/.pi/agent`, or `$PI_CODING_AGENT_DIR`) — a local
|
|
9
|
+
* reimplementation of pi's `getAgentDir()`. Reimplemented (not imported) ON
|
|
10
|
+
* PURPOSE: `getAgentDir` lives in `@earendil-works/pi-coding-agent`, and
|
|
11
|
+
* importing that package pulls in the ENTIRE ~360ms pi engine module graph. The
|
|
12
|
+
* front door calls this on every bare-`crtr` boot BEFORE spawning the broker, so
|
|
13
|
+
* a static/dynamic pi import here adds ~316ms of serialized dead-time in the
|
|
14
|
+
* foreground before the broker (which loads pi itself) even starts. This mirrors
|
|
15
|
+
* pi's contract exactly: `$PI_CODING_AGENT_DIR` (tilde-expanded) else
|
|
16
|
+
* `~/.pi/agent`. Same precedent as `bearings.ts` (keep the pi engine out of the
|
|
17
|
+
* hot/foreground path). If pi ever rebrands its configDir/env, update here.
|
|
18
|
+
*/
|
|
19
|
+
export declare function piAgentDir(homeDir?: string): string;
|
|
7
20
|
/**
|
|
8
21
|
* Return every local-path package in `<agentDir>/settings.json` whose resolved
|
|
9
22
|
* path is missing. Empty when settings is absent/unreadable or all paths exist.
|
|
@@ -21,6 +21,24 @@ function expandTilde(pathValue, homeDir) {
|
|
|
21
21
|
return join(homeDir, pathValue.slice(2));
|
|
22
22
|
return pathValue;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* The pi agent config dir (`~/.pi/agent`, or `$PI_CODING_AGENT_DIR`) — a local
|
|
26
|
+
* reimplementation of pi's `getAgentDir()`. Reimplemented (not imported) ON
|
|
27
|
+
* PURPOSE: `getAgentDir` lives in `@earendil-works/pi-coding-agent`, and
|
|
28
|
+
* importing that package pulls in the ENTIRE ~360ms pi engine module graph. The
|
|
29
|
+
* front door calls this on every bare-`crtr` boot BEFORE spawning the broker, so
|
|
30
|
+
* a static/dynamic pi import here adds ~316ms of serialized dead-time in the
|
|
31
|
+
* foreground before the broker (which loads pi itself) even starts. This mirrors
|
|
32
|
+
* pi's contract exactly: `$PI_CODING_AGENT_DIR` (tilde-expanded) else
|
|
33
|
+
* `~/.pi/agent`. Same precedent as `bearings.ts` (keep the pi engine out of the
|
|
34
|
+
* hot/foreground path). If pi ever rebrands its configDir/env, update here.
|
|
35
|
+
*/
|
|
36
|
+
export function piAgentDir(homeDir = homedir()) {
|
|
37
|
+
const envDir = process.env['PI_CODING_AGENT_DIR'];
|
|
38
|
+
if (envDir !== undefined && envDir !== '')
|
|
39
|
+
return expandTilde(envDir, homeDir);
|
|
40
|
+
return join(homeDir, '.pi', 'agent');
|
|
41
|
+
}
|
|
24
42
|
/** A `packages[]` entry is a LOCAL PATH (not an npm ref) — the only kind that
|
|
25
43
|
* can go stale on a repo move. `npm:...` and bare registry names resolve
|
|
26
44
|
* through node/npm, not the filesystem, so they are out of scope here. */
|
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
import { spawn } from 'node:child_process';
|
|
16
16
|
import { emitKeypressEvents } from 'node:readline';
|
|
17
17
|
import { getNode, listResumableRoots } from '../canvas/index.js';
|
|
18
|
+
import { navLabel } from '../canvas/nav-model.js';
|
|
18
19
|
import { isPidAlive } from '../canvas/pid.js';
|
|
20
|
+
import { readGoal } from './kickoff.js';
|
|
19
21
|
import { reviveNode } from './revive.js';
|
|
20
22
|
import { installTmuxBindings } from './tmux-chrome.js';
|
|
21
23
|
import { registerViewerFocus, waitForBrokerViewSocket, viewerSplitEnv, currentTmux, inTmux, } from './placement.js';
|
|
@@ -109,17 +111,81 @@ async function attachInline(nodeId, cwd) {
|
|
|
109
111
|
const HILITE = accent('\u25b8');
|
|
110
112
|
const cursorUp = (n) => (n > 0 ? `\u001b[${n}A\r` : '');
|
|
111
113
|
const CLEAR_DOWN = '\u001b[0J';
|
|
114
|
+
const ANSI_RE = /\u001b\[[0-9;]*m/g;
|
|
115
|
+
/** Terminal rows a just-written block occupies — counts each '\n' AND the extra
|
|
116
|
+
* rows a long line WRAPS into at the current terminal width (ANSI color codes
|
|
117
|
+
* stripped so they don't inflate the width). A plain newline count undershoots
|
|
118
|
+
* in a narrow pane, so redraws stack instead of clearing. Mirrors the
|
|
119
|
+
* profile-menu blockRows. */
|
|
120
|
+
function blockRows(block) {
|
|
121
|
+
const cols = process.stdout.columns ?? 80;
|
|
122
|
+
const segs = block.split('\n');
|
|
123
|
+
const newlines = segs.length - 1; // block ends with '\n' → last seg is ''
|
|
124
|
+
let extra = 0;
|
|
125
|
+
for (let i = 0; i < newlines; i++) {
|
|
126
|
+
const w = segs[i].replace(ANSI_RE, '').length;
|
|
127
|
+
if (cols > 0 && w > cols)
|
|
128
|
+
extra += Math.ceil(w / cols) - 1;
|
|
129
|
+
}
|
|
130
|
+
return newlines + extra;
|
|
131
|
+
}
|
|
132
|
+
/** Truncate to a visible-column budget, preserving ANSI escapes (they cost no
|
|
133
|
+
* width). Appends '…' when it cuts. Keeps a styled row on one terminal line so
|
|
134
|
+
* nothing wraps. */
|
|
135
|
+
function fitWidth(s, width) {
|
|
136
|
+
if (width <= 0)
|
|
137
|
+
return s;
|
|
138
|
+
let out = '';
|
|
139
|
+
let vis = 0;
|
|
140
|
+
for (let i = 0; i < s.length;) {
|
|
141
|
+
if (s[i] === '\u001b') {
|
|
142
|
+
const m = /^\u001b\[[0-9;]*m/.exec(s.slice(i));
|
|
143
|
+
if (m !== null) {
|
|
144
|
+
out += m[0];
|
|
145
|
+
i += m[0].length;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (vis >= width - 1 && s.replace(ANSI_RE, '').length > width) {
|
|
150
|
+
out += '\u2026';
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
out += s[i];
|
|
154
|
+
vis++;
|
|
155
|
+
i++;
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
112
159
|
function label(r) {
|
|
113
160
|
// Liveness is the broker pid, not the row status: a dormant resident root
|
|
114
161
|
// keeps status='active' while its engine is down.
|
|
115
162
|
const live = isPidAlive(r.pi_pid ?? null) ? accent('\u25cf live') : dim('\u25cb dormant');
|
|
116
|
-
|
|
163
|
+
// The SAME handle the Alt-G graph shows: navLabel = name → description → kind.
|
|
164
|
+
// getNode carries `description` (NodeRow from the list query does not).
|
|
165
|
+
const node = getNode(r.node_id);
|
|
166
|
+
let name = navLabel(node, r.node_id);
|
|
167
|
+
// navLabel falls back to the bare kind for a never-named root — useless in a
|
|
168
|
+
// roster where every root is 'general'. Rescue it with the goal's first line
|
|
169
|
+
// (the spawning prompt), so unnamed convos are still tellable apart.
|
|
170
|
+
if (node !== null && name === node.kind) {
|
|
171
|
+
const goal = firstLine(readGoal(r.node_id));
|
|
172
|
+
if (goal !== '')
|
|
173
|
+
name = goal;
|
|
174
|
+
}
|
|
117
175
|
return `${name} ${live}`;
|
|
118
176
|
}
|
|
177
|
+
/** First non-empty line of a goal body, trimmed and length-capped. */
|
|
178
|
+
function firstLine(text) {
|
|
179
|
+
if (text === null)
|
|
180
|
+
return '';
|
|
181
|
+
const line = text.split('\n').map((l) => l.trim()).find((l) => l !== '') ?? '';
|
|
182
|
+
return line.length > 72 ? line.slice(0, 71) + '\u2026' : line;
|
|
183
|
+
}
|
|
119
184
|
function detail(r, cwd) {
|
|
120
185
|
const when = ago(r.created);
|
|
121
186
|
const where = r.cwd === cwd ? '' : ` ${r.cwd}`;
|
|
122
|
-
|
|
187
|
+
// Short id keeps the row on one line; the full id is rarely needed to pick.
|
|
188
|
+
return `${when}${where} ${r.node_id.slice(0, 8)}`;
|
|
123
189
|
}
|
|
124
190
|
function ago(iso) {
|
|
125
191
|
const ms = Date.now() - new Date(iso).getTime();
|
|
@@ -147,11 +213,13 @@ function pickRoot(roots, cwd) {
|
|
|
147
213
|
let sel = 0;
|
|
148
214
|
let prevLines = 0;
|
|
149
215
|
const render = () => {
|
|
216
|
+
const cols = process.stdout.columns ?? 80;
|
|
150
217
|
let s = `\n ${bold(title)}\n ${dim(subtitle)}\n\n`;
|
|
151
218
|
roots.forEach((r, i) => {
|
|
152
219
|
const mark = i === sel ? HILITE : ' ';
|
|
153
220
|
const slot = i < 9 ? key(`${i + 1}`) : ' ';
|
|
154
|
-
|
|
221
|
+
const row = ` ${mark} ${slot} ${label(r)} ${dim(detail(r, cwd))}`;
|
|
222
|
+
s += fitWidth(row, cols) + '\n';
|
|
155
223
|
});
|
|
156
224
|
s += `\n ${dim('↑/↓ move enter resume q quit')}\n`;
|
|
157
225
|
return s;
|
|
@@ -159,7 +227,7 @@ function pickRoot(roots, cwd) {
|
|
|
159
227
|
const draw = () => {
|
|
160
228
|
const block = render();
|
|
161
229
|
out.write((prevLines > 0 ? cursorUp(prevLines) + CLEAR_DOWN : '') + block);
|
|
162
|
-
prevLines = block
|
|
230
|
+
prevLines = blockRows(block);
|
|
163
231
|
};
|
|
164
232
|
emitKeypressEvents(input);
|
|
165
233
|
const wasRaw = input.isRaw ?? false;
|