@webpresso/agent-kit 3.1.25 → 3.1.28
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/catalog/AGENTS.md.tpl +6 -10
- package/catalog/agent/commands/verify.md +2 -2
- package/catalog/agent/rules/changeset-release.md +7 -1
- package/catalog/agent/rules/workflow-skills-routing.md +6 -3
- package/catalog/agent/skills/autopilot/SKILL.md +48 -11
- package/catalog/agent/skills/claude/SKILL.md +7 -0
- package/catalog/agent/skills/codex/SKILL.md +7 -0
- package/catalog/agent/skills/fix/SKILL.md +33 -8
- package/catalog/agent/skills/grok/SKILL.md +7 -0
- package/catalog/agent/skills/investigate/SKILL.md +12 -1
- package/catalog/agent/skills/pll/SKILL.md +53 -48
- package/catalog/agent/skills/team/SKILL.md +26 -9
- package/catalog/agent/skills/ultragoal/SKILL.md +147 -19
- package/catalog/agent/skills/verify/SKILL.md +71 -51
- package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +8 -0
- package/dist/esm/audit/blueprint-lifecycle-sql.js +25 -1
- package/dist/esm/ci/native-session-memory-cache.d.ts +9 -1
- package/dist/esm/ci/native-session-memory-cache.js +9 -1
- package/dist/esm/ci/vitest-ci-shards.d.ts +6 -4
- package/dist/esm/ci/vitest-ci-shards.js +6 -4
- package/dist/esm/cli/auto-update/guard-tombstone.d.ts +25 -0
- package/dist/esm/cli/auto-update/guard-tombstone.js +67 -0
- package/dist/esm/cli/auto-update/installer.d.ts +4 -17
- package/dist/esm/cli/auto-update/installer.js +6 -57
- package/dist/esm/cli/auto-update/self-invocation.d.ts +37 -0
- package/dist/esm/cli/auto-update/self-invocation.js +62 -0
- package/dist/esm/cli/commands/audit.js +1 -0
- package/dist/esm/cli/commands/dash/index.js +3 -3
- package/dist/esm/cli/commands/dash/plan-once-attention.d.ts +32 -0
- package/dist/esm/cli/commands/dash/plan-once-attention.js +50 -0
- package/dist/esm/cli/commands/dash/plan-once-schema.d.ts +73 -0
- package/dist/esm/cli/commands/dash/plan-once-schema.js +119 -0
- package/dist/esm/cli/commands/dash/plan-once.d.ts +122 -0
- package/dist/esm/cli/commands/dash/plan-once.js +224 -0
- package/dist/esm/cli/commands/dash/tui/ipc-protocol.d.ts +87 -0
- package/dist/esm/cli/commands/dash/tui/ipc-protocol.js +128 -0
- package/dist/esm/cli/commands/dash/tui/native-host.d.ts +34 -0
- package/dist/esm/cli/commands/dash/tui/native-host.js +256 -0
- package/dist/esm/cli/commands/dash/tui/native-path.d.ts +11 -0
- package/dist/esm/cli/commands/dash/tui/native-path.js +41 -0
- package/dist/esm/cli/commands/dash/tui/runtime.js +72 -0
- package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +1 -1
- package/dist/esm/cli/commands/package-manager.js +2 -1
- package/dist/esm/cli/commands/quality-log-store.d.ts +6 -0
- package/dist/esm/cli/commands/quality-log-store.js +55 -15
- package/dist/esm/cli/commands/self-install-guard.d.ts +14 -16
- package/dist/esm/cli/commands/self-install-guard.js +17 -23
- package/dist/esm/hooks/guard-switch/index.js +8 -4
- package/dist/esm/hooks/post-tool/lint-after-edit.js +6 -4
- package/dist/esm/hooks/precompact/index.js +5 -3
- package/dist/esm/hooks/sessionstart/index.d.ts +10 -0
- package/dist/esm/hooks/sessionstart/index.js +61 -4
- package/dist/esm/hooks/shared/types.d.ts +11 -0
- package/dist/esm/hooks/shared/types.js +21 -0
- package/dist/esm/paths/state-root.js +36 -3
- package/dist/esm/session-memory/current-session.js +2 -0
- package/dist/esm/session-memory/native-runtime.js +5 -3
- package/dist/esm/session-memory/native-warm-markers.d.ts +18 -0
- package/dist/esm/session-memory/native-warm-markers.js +22 -0
- package/dist/esm/session-memory/sync/types.d.ts +4 -0
- package/dist/esm/session-memory/types.d.ts +3 -1
- package/dist/esm/session-memory/types.js +2 -0
- package/dist/esm/ultragoal/runtime.js +44 -2
- package/package.json +15 -12
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// Domain host for the native Ratatui shell: maps AppViewModel ↔ IPC and keys → reduceTuiKey.
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { emitDashE2eState } from "#cli/commands/dash/e2e-state.js";
|
|
4
|
+
import { buildRows } from "#cli/commands/dash/menu-state.js";
|
|
5
|
+
import { filterPaletteCommands, groupPaletteCommands } from "#cli/commands/dash/tui/palette.js";
|
|
6
|
+
import { reduceTuiKey } from "#cli/commands/dash/tui/state.js";
|
|
7
|
+
import { decodeIpcFrames, encodeIpcFrame, parseClientMessage, } from "#cli/commands/dash/tui/ipc-protocol.js";
|
|
8
|
+
import { sanitizeTuiText } from "#cli/commands/dash/tui/sanitize.js";
|
|
9
|
+
function hostAvailabilityNote(agentKey, availability) {
|
|
10
|
+
const row = availability.find((item) => item.id === agentKey);
|
|
11
|
+
if (!row || row.available)
|
|
12
|
+
return null;
|
|
13
|
+
return row.reason ?? "unavailable";
|
|
14
|
+
}
|
|
15
|
+
export function projectNativeViewModel(state, input) {
|
|
16
|
+
const rows = buildRows(state.agents, state.menu.expandedAgent);
|
|
17
|
+
const menuLines = rows.map((row) => {
|
|
18
|
+
const mark = row.key === state.menu.selectedKey ? "▸" : " ";
|
|
19
|
+
const note = row.kind === "agent" ? hostAvailabilityNote(row.agentKey, state.hostAvailability) : null;
|
|
20
|
+
const label = note ? `${row.label} · ${note}` : row.label;
|
|
21
|
+
return sanitizeTuiText(`${mark} ${label}`);
|
|
22
|
+
});
|
|
23
|
+
let overlayKind = state.overlay.kind;
|
|
24
|
+
let overlayLines = [];
|
|
25
|
+
if (state.overlay.kind === "palette") {
|
|
26
|
+
const matches = filterPaletteCommands(state.overlay.query);
|
|
27
|
+
const groups = groupPaletteCommands(matches);
|
|
28
|
+
overlayLines = [sanitizeTuiText(`query: ${state.overlay.query || "…"}`)];
|
|
29
|
+
for (const group of groups) {
|
|
30
|
+
overlayLines.push(sanitizeTuiText(`— ${group.category}`));
|
|
31
|
+
for (const [index, command] of group.commands.entries()) {
|
|
32
|
+
// Approximate global cursor: mark the selected match row.
|
|
33
|
+
const globalIndex = matches.indexOf(command);
|
|
34
|
+
const mark = globalIndex === state.overlay.cursor ? "▸" : " ";
|
|
35
|
+
overlayLines.push(sanitizeTuiText(`${mark} ${command.title} · ${command.keys}`));
|
|
36
|
+
void index;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (matches.length === 0) {
|
|
40
|
+
overlayLines.push("(no matches)");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (state.overlay.kind === "help") {
|
|
44
|
+
overlayLines = [
|
|
45
|
+
"^p palette",
|
|
46
|
+
"^o mode",
|
|
47
|
+
"^s sessions",
|
|
48
|
+
"^x help",
|
|
49
|
+
"j/k move · h/l fold · g/G jump",
|
|
50
|
+
"q quit",
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
else if (state.overlay.kind === "wizard") {
|
|
54
|
+
overlayLines = state.overlay.lines.map((line) => sanitizeTuiText(line));
|
|
55
|
+
}
|
|
56
|
+
else if (state.overlay.kind === "browser") {
|
|
57
|
+
overlayLines = state.overlay.lines.map((line) => sanitizeTuiText(line));
|
|
58
|
+
}
|
|
59
|
+
else if (state.overlay.kind === "lifecycle") {
|
|
60
|
+
overlayLines = [
|
|
61
|
+
sanitizeTuiText(`target ${state.overlay.target}`),
|
|
62
|
+
"d detach · s stop · f force-kill · a archive",
|
|
63
|
+
"Backspace delete · y confirm · Esc cancel",
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
repoLabel: sanitizeTuiText(input.repoLabel),
|
|
68
|
+
width: input.width,
|
|
69
|
+
height: input.height,
|
|
70
|
+
permissionMode: state.permissionMode,
|
|
71
|
+
hostAvailability: state.hostAvailability.map((h) => ({
|
|
72
|
+
id: h.id,
|
|
73
|
+
available: h.available,
|
|
74
|
+
...(h.reason ? { reason: sanitizeTuiText(h.reason) } : {}),
|
|
75
|
+
})),
|
|
76
|
+
status: state.status ? sanitizeTuiText(state.status) : null,
|
|
77
|
+
warnings: state.warnings.map((w) => sanitizeTuiText(w)),
|
|
78
|
+
menuLines,
|
|
79
|
+
overlayKind,
|
|
80
|
+
overlayLines,
|
|
81
|
+
paletteQuery: state.overlay.kind === "palette" ? state.overlay.query : null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export function clientMessageToParsedKey(message) {
|
|
85
|
+
switch (message.kind) {
|
|
86
|
+
case "char":
|
|
87
|
+
return message.value ? { kind: "char", value: message.value } : null;
|
|
88
|
+
case "up":
|
|
89
|
+
case "down":
|
|
90
|
+
case "left":
|
|
91
|
+
case "right":
|
|
92
|
+
case "enter":
|
|
93
|
+
case "escape":
|
|
94
|
+
case "tab":
|
|
95
|
+
case "backspace":
|
|
96
|
+
return { kind: message.kind };
|
|
97
|
+
case "ctrl":
|
|
98
|
+
return message.value ? { kind: "ctrl", value: message.value } : null;
|
|
99
|
+
default:
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Drive pure TUI state from a native binary over length-prefixed JSON frames.
|
|
105
|
+
* State is owned by the caller (runtime) so launch/HUD/browser updates paint.
|
|
106
|
+
*/
|
|
107
|
+
export async function runNativeTuiHost(input) {
|
|
108
|
+
const spawnFn = input.spawnImpl ?? spawn;
|
|
109
|
+
const child = spawnFn(input.binaryPath, [], {
|
|
110
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
111
|
+
});
|
|
112
|
+
if (!child.stdin || !child.stdout) {
|
|
113
|
+
throw new Error("native shell spawn did not provide stdio pipes");
|
|
114
|
+
}
|
|
115
|
+
let stdinBuf = Buffer.alloc(0);
|
|
116
|
+
let width = 80;
|
|
117
|
+
let height = 24;
|
|
118
|
+
const childStdin = child.stdin;
|
|
119
|
+
const childStdout = child.stdout;
|
|
120
|
+
const pushView = () => {
|
|
121
|
+
if (!childStdin.writable)
|
|
122
|
+
return;
|
|
123
|
+
const view = projectNativeViewModel(input.getState(), {
|
|
124
|
+
repoLabel: input.repoLabel,
|
|
125
|
+
width,
|
|
126
|
+
height,
|
|
127
|
+
});
|
|
128
|
+
childStdin.write(encodeIpcFrame({ type: "view", view }));
|
|
129
|
+
};
|
|
130
|
+
input.onBridge?.({ pushView });
|
|
131
|
+
childStdin.write(encodeIpcFrame({ type: "hello", schemaVersion: 1 }));
|
|
132
|
+
pushView();
|
|
133
|
+
/**
|
|
134
|
+
* Handshake hang detection: fail closed if the shell never greets.
|
|
135
|
+
* After ready, probe process liveness so a zombie/stuck native binary
|
|
136
|
+
* cannot pin the domain host forever (runtime falls back to pure-ANSI).
|
|
137
|
+
*/
|
|
138
|
+
const HANDSHAKE_MS = 30_000;
|
|
139
|
+
const LIVENESS_MS = 5_000;
|
|
140
|
+
return await new Promise((resolve) => {
|
|
141
|
+
let settled = false;
|
|
142
|
+
let handshakeDone = false;
|
|
143
|
+
let handshakeTimer = setTimeout(() => {
|
|
144
|
+
if (handshakeDone || settled)
|
|
145
|
+
return;
|
|
146
|
+
input.handlers.onQuit();
|
|
147
|
+
child.kill("SIGTERM");
|
|
148
|
+
setTimeout(() => child.kill("SIGKILL"), 2_000);
|
|
149
|
+
finish(1);
|
|
150
|
+
}, HANDSHAKE_MS);
|
|
151
|
+
let livenessTimer;
|
|
152
|
+
const finish = (code) => {
|
|
153
|
+
if (settled)
|
|
154
|
+
return;
|
|
155
|
+
settled = true;
|
|
156
|
+
if (handshakeTimer)
|
|
157
|
+
clearTimeout(handshakeTimer);
|
|
158
|
+
if (livenessTimer)
|
|
159
|
+
clearInterval(livenessTimer);
|
|
160
|
+
resolve(code);
|
|
161
|
+
};
|
|
162
|
+
const armLiveness = () => {
|
|
163
|
+
if (livenessTimer)
|
|
164
|
+
return;
|
|
165
|
+
livenessTimer = setInterval(() => {
|
|
166
|
+
if (settled)
|
|
167
|
+
return;
|
|
168
|
+
// exitCode set when the process has already exited without us finishing.
|
|
169
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
170
|
+
finish(child.exitCode ?? 1);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const pid = child.pid;
|
|
174
|
+
if (pid === undefined) {
|
|
175
|
+
finish(1);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
process.kill(pid, 0);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
finish(1);
|
|
183
|
+
}
|
|
184
|
+
}, LIVENESS_MS);
|
|
185
|
+
};
|
|
186
|
+
childStdout.on("data", (chunk) => {
|
|
187
|
+
stdinBuf = Buffer.concat([stdinBuf, chunk]);
|
|
188
|
+
let decoded;
|
|
189
|
+
try {
|
|
190
|
+
decoded = decodeIpcFrames(stdinBuf);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// Drop a corrupted frame prefix rather than crashing the domain host.
|
|
194
|
+
// Keep a bounded tail so a subsequent well-formed frame can resync.
|
|
195
|
+
stdinBuf = stdinBuf.length > 64 * 1024 ? stdinBuf.subarray(-4096) : Buffer.alloc(0);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
stdinBuf = Buffer.from(decoded.rest);
|
|
199
|
+
for (const raw of decoded.messages) {
|
|
200
|
+
let msg;
|
|
201
|
+
try {
|
|
202
|
+
msg = parseClientMessage(raw);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (msg.type === "hello" || msg.type === "ready") {
|
|
208
|
+
handshakeDone = true;
|
|
209
|
+
if (handshakeTimer)
|
|
210
|
+
clearTimeout(handshakeTimer);
|
|
211
|
+
armLiveness();
|
|
212
|
+
pushView();
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (msg.type === "resize") {
|
|
216
|
+
width = msg.width;
|
|
217
|
+
height = msg.height;
|
|
218
|
+
pushView();
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (msg.type === "key") {
|
|
222
|
+
const key = clientMessageToParsedKey(msg);
|
|
223
|
+
if (!key)
|
|
224
|
+
continue;
|
|
225
|
+
const result = reduceTuiKey(input.getState(), key);
|
|
226
|
+
input.setState(result.state);
|
|
227
|
+
// Match pure-ANSI path: selection transitions ride the E2E channel so
|
|
228
|
+
// CI tmux harness can assert menu navigation without screen scrapes.
|
|
229
|
+
if (result.state.menu.selectedKey) {
|
|
230
|
+
emitDashE2eState({ kind: "selection", row: result.state.menu.selectedKey });
|
|
231
|
+
}
|
|
232
|
+
for (const effect of result.effects) {
|
|
233
|
+
void input.handlers.onEffect(effect);
|
|
234
|
+
if (effect.kind === "quit") {
|
|
235
|
+
input.handlers.onQuit();
|
|
236
|
+
child.kill("SIGTERM");
|
|
237
|
+
setTimeout(() => child.kill("SIGKILL"), 2_000);
|
|
238
|
+
finish(0);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (result.state.quit) {
|
|
243
|
+
input.handlers.onQuit();
|
|
244
|
+
child.kill("SIGTERM");
|
|
245
|
+
setTimeout(() => child.kill("SIGKILL"), 2_000);
|
|
246
|
+
finish(0);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
pushView();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
child.on("exit", (code) => finish(code ?? 0));
|
|
254
|
+
child.on("error", () => finish(1));
|
|
255
|
+
});
|
|
256
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Env contract:
|
|
3
|
+
* - `WP_DASH_NATIVE=0|false|off` → force pure-ANSI
|
|
4
|
+
* - `WP_DASH_NATIVE=1|true|on` → prefer native when binary present
|
|
5
|
+
* - unset → prefer native when binary present (cutover default)
|
|
6
|
+
* - `WP_DASH_TUI_BIN` → absolute path override
|
|
7
|
+
* - `WP_DASH_FORCE_ANSI=1` → runtime force pure-ANSI even if a binary resolves
|
|
8
|
+
*/
|
|
9
|
+
export declare function shouldPreferNativeDashTui(env?: NodeJS.ProcessEnv): boolean;
|
|
10
|
+
export declare function resolveNativeDashTuiBinary(env?: NodeJS.ProcessEnv, candidates?: readonly string[]): string | null;
|
|
11
|
+
export declare function defaultBinaryCandidates(cwd?: string): readonly string[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Resolve the optional native wp-dash-tui binary for the control-plane shell.
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { findPackageAsset } from "#utils/package-assets.js";
|
|
5
|
+
/**
|
|
6
|
+
* Env contract:
|
|
7
|
+
* - `WP_DASH_NATIVE=0|false|off` → force pure-ANSI
|
|
8
|
+
* - `WP_DASH_NATIVE=1|true|on` → prefer native when binary present
|
|
9
|
+
* - unset → prefer native when binary present (cutover default)
|
|
10
|
+
* - `WP_DASH_TUI_BIN` → absolute path override
|
|
11
|
+
* - `WP_DASH_FORCE_ANSI=1` → runtime force pure-ANSI even if a binary resolves
|
|
12
|
+
*/
|
|
13
|
+
export function shouldPreferNativeDashTui(env = process.env) {
|
|
14
|
+
const flag = env["WP_DASH_NATIVE"]?.trim().toLowerCase();
|
|
15
|
+
if (flag === "0" || flag === "false" || flag === "off" || flag === "no") {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
export function resolveNativeDashTuiBinary(env = process.env, candidates = defaultBinaryCandidates()) {
|
|
21
|
+
const override = env["WP_DASH_TUI_BIN"]?.trim();
|
|
22
|
+
if (override && existsSync(override))
|
|
23
|
+
return override;
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
if (existsSync(candidate))
|
|
26
|
+
return candidate;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
export function defaultBinaryCandidates(cwd = process.cwd()) {
|
|
31
|
+
const fromPackage = [
|
|
32
|
+
findPackageAsset("native/dash-tui/target/release/wp-dash-tui", { cwd }),
|
|
33
|
+
findPackageAsset("native/dash-tui/target/debug/wp-dash-tui", { cwd }),
|
|
34
|
+
].filter((path) => path !== null);
|
|
35
|
+
return [
|
|
36
|
+
join(cwd, "native/dash-tui/target/release/wp-dash-tui"),
|
|
37
|
+
join(cwd, "native/dash-tui/target/debug/wp-dash-tui"),
|
|
38
|
+
...fromPackage,
|
|
39
|
+
join(cwd, "bin/wp-dash-tui"),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
@@ -234,6 +234,8 @@ export async function runTuiMenu(input) {
|
|
|
234
234
|
let dirty = true;
|
|
235
235
|
let lastFrame = "";
|
|
236
236
|
let running = true;
|
|
237
|
+
/** When set, domain state paints via the native shell instead of pure-ANSI. */
|
|
238
|
+
let nativePushView = null;
|
|
237
239
|
/** Once true, no new launch effects may call selectAgent. */
|
|
238
240
|
let shuttingDown = false;
|
|
239
241
|
let initialLaunchConsumed = false;
|
|
@@ -261,6 +263,14 @@ export async function runTuiMenu(input) {
|
|
|
261
263
|
const paint = () => {
|
|
262
264
|
if (shuttingDown)
|
|
263
265
|
return;
|
|
266
|
+
// Native shell owns the terminal; never write pure-ANSI frames over it.
|
|
267
|
+
if (nativePushView) {
|
|
268
|
+
if (!dirty && !input.smokeOnce)
|
|
269
|
+
return;
|
|
270
|
+
nativePushView();
|
|
271
|
+
dirty = false;
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
264
274
|
if (!dirty && !input.smokeOnce)
|
|
265
275
|
return;
|
|
266
276
|
const frame = renderFrame(toViewModel(state, {
|
|
@@ -621,6 +631,68 @@ export async function runTuiMenu(input) {
|
|
|
621
631
|
stdout.write(`\n${note}\n`);
|
|
622
632
|
return 0;
|
|
623
633
|
}
|
|
634
|
+
// Primary path: native Ratatui shell when binary is available (WP_DASH_NATIVE=0 forces pure-ANSI).
|
|
635
|
+
const { shouldPreferNativeDashTui, resolveNativeDashTuiBinary } = await import("#cli/commands/dash/tui/native-path.js");
|
|
636
|
+
const nativeBinary = shouldPreferNativeDashTui() && !process.env["WP_DASH_FORCE_ANSI"]
|
|
637
|
+
? resolveNativeDashTuiBinary()
|
|
638
|
+
: null;
|
|
639
|
+
if (nativeBinary) {
|
|
640
|
+
try {
|
|
641
|
+
const { runNativeTuiHost } = await import("#cli/commands/dash/tui/native-host.js");
|
|
642
|
+
process.stderr.write(`wp dash: native control-plane shell (${nativeBinary})\n`);
|
|
643
|
+
// Ensure agent list is loaded before first native frame when possible.
|
|
644
|
+
state = { ...state, loading: false };
|
|
645
|
+
const code = await runNativeTuiHost({
|
|
646
|
+
binaryPath: nativeBinary,
|
|
647
|
+
repoLabel: input.repoLabel,
|
|
648
|
+
getState: () => state,
|
|
649
|
+
setState: (next) => {
|
|
650
|
+
state = next;
|
|
651
|
+
},
|
|
652
|
+
onBridge: (bridge) => {
|
|
653
|
+
nativePushView = bridge.pushView;
|
|
654
|
+
},
|
|
655
|
+
handlers: {
|
|
656
|
+
onEffect: (effect) => {
|
|
657
|
+
// Match pure-ANSI: repaint after async effects settle so launch /
|
|
658
|
+
// browser / status mutations reach the native shell without waiting
|
|
659
|
+
// for the next key or timer tick.
|
|
660
|
+
void enqueueEffects([effect]).then(() => {
|
|
661
|
+
if (!shuttingDown)
|
|
662
|
+
paint();
|
|
663
|
+
});
|
|
664
|
+
},
|
|
665
|
+
onQuit: () => {
|
|
666
|
+
shuttingDown = true;
|
|
667
|
+
running = false;
|
|
668
|
+
coordinator.discardPending();
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
});
|
|
672
|
+
nativePushView = null;
|
|
673
|
+
if (code !== 0) {
|
|
674
|
+
throw new Error(`native shell exited with code ${code}`);
|
|
675
|
+
}
|
|
676
|
+
shuttingDown = true;
|
|
677
|
+
running = false;
|
|
678
|
+
coordinator.discardPending();
|
|
679
|
+
clearInterval(usageTimer);
|
|
680
|
+
clearInterval(ultragoalTimer);
|
|
681
|
+
clearInterval(statusTimer);
|
|
682
|
+
stopObservingLayout?.();
|
|
683
|
+
const note = await controller?.dispose();
|
|
684
|
+
await effectQueue.catch(() => {
|
|
685
|
+
/* best-effort drain after dispose */
|
|
686
|
+
});
|
|
687
|
+
if (note)
|
|
688
|
+
stdout.write(`\n${note}\n`);
|
|
689
|
+
return code;
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
nativePushView = null;
|
|
693
|
+
process.stderr.write(`wp dash: native shell failed (${error instanceof Error ? error.message : String(error)}); falling back to pure-ANSI\n`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
624
696
|
const wasRaw = stdin.isRaw;
|
|
625
697
|
if (typeof stdin.setRawMode === "function")
|
|
626
698
|
stdin.setRawMode(true);
|
|
@@ -26,7 +26,7 @@ import { spawnSync } from "node:child_process";
|
|
|
26
26
|
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
27
27
|
import { dirname, join } from "node:path";
|
|
28
28
|
import { buildVpGlobalInstallCommand, confirmInstalledGlobally, PUBLIC_PACKAGE_NAME, resolveBundledVpCommand, } from "#cli/auto-update/detect-pm.js";
|
|
29
|
-
import { buildSelfInvocationCommand } from "#cli/
|
|
29
|
+
import { buildSelfInvocationCommand } from "#cli/auto-update/self-invocation.js";
|
|
30
30
|
import { findAgentKitPackageRoot, resolveAgentKitPackageRoot, } from "#cli/commands/init/package-root";
|
|
31
31
|
import { makeNoopSpinnerFactory, } from "#cli/commands/init/scaffolders/spinner";
|
|
32
32
|
import { UPDATE_CACHE_FILENAME, readFreshCachedLatestVersion } from "#cli/auto-update/cache.js";
|
|
@@ -7,7 +7,8 @@ import { readToolingOwnershipState, tryReadRepoKey, writeToolingOwnershipState,
|
|
|
7
7
|
import { resolveGlobalCapableVpCommand } from "#cli/global-vp.js";
|
|
8
8
|
import { UPDATE_CACHE_FILENAME, formatStaleAgentKitVersionWarning, reconcileUpdateNotifierCache, } from "#cli/auto-update/cache.js";
|
|
9
9
|
import { buildVpGlobalUpdateCommand, resolveBundledVpCommand } from "#cli/auto-update/detect-pm.js";
|
|
10
|
-
import {
|
|
10
|
+
import { buildSelfInvocationCommand } from "#cli/auto-update/self-invocation.js";
|
|
11
|
+
import { GUARD_EXIT_CHILD_FAILED, GUARD_EXIT_LEASE_HELD, GUARD_EXIT_OWNERSHIP_LOST, GUARD_EXIT_UNRESOLVABLE, } from "#cli/commands/self-install-guard.js";
|
|
11
12
|
import { readPackageVersion } from "#cli/utils";
|
|
12
13
|
import { resolveFreshAgentKitPackageRoot } from "#cli/commands/init/package-root";
|
|
13
14
|
import { DEFAULT_PLUGIN_CACHE_PRUNE_HOSTS, pruneOutdatedAgentKitPluginCaches, } from "#cli/commands/init/plugin-cache-prune.js";
|
|
@@ -10,6 +10,12 @@ export interface CliLogEntry {
|
|
|
10
10
|
readonly summary?: string;
|
|
11
11
|
}
|
|
12
12
|
export declare function isCliLogCommandName(value: string): value is CliLogCommandName;
|
|
13
|
+
/**
|
|
14
|
+
* Create a durable CLI log sink. When the preferred repo state surface is not
|
|
15
|
+
* writable (EPERM/EACCES/EROFS — common in Codex/read-only sandboxes), fall
|
|
16
|
+
* back to TMPDIR so `wp audit`/`wp test` still execute. The fallback is loud
|
|
17
|
+
* on stderr; it is not a silent swallow.
|
|
18
|
+
*/
|
|
13
19
|
export declare function createCliLogSink(command: CliLogCommandName, cwd?: string): CliLogSink;
|
|
14
20
|
export interface CliLogSink {
|
|
15
21
|
readonly command: CliLogCommandName;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { closeSync, createWriteStream, mkdirSync, openSync, readdirSync, readFileSync, statSync, renameSync, utimesSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
4
5
|
import { getSurfacePath } from "#paths/state-root.js";
|
|
5
6
|
import { readTrustedJsonFile } from "#shared-utils/read-json-file.js";
|
|
@@ -18,9 +19,17 @@ const ORPHAN_LOG_GRACE_MS = 60_000;
|
|
|
18
19
|
export function isCliLogCommandName(value) {
|
|
19
20
|
return CLI_LOG_COMMANDS.includes(value);
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
+
function isUnwritableFsError(error) {
|
|
23
|
+
if (typeof error !== "object" || error === null || !("code" in error))
|
|
24
|
+
return false;
|
|
25
|
+
const code = error.code;
|
|
26
|
+
return code === "EPERM" || code === "EACCES" || code === "EROFS";
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Open a CLI log sink in an explicit directory (preferred state surface or fallback).
|
|
30
|
+
*/
|
|
31
|
+
function openCliLogSink(commandDir, command, cwd) {
|
|
22
32
|
const id = createLogId();
|
|
23
|
-
const commandDir = getCommandLogDir(command, cwd);
|
|
24
33
|
mkdirSync(commandDir, { recursive: true });
|
|
25
34
|
const absoluteLogPath = join(commandDir, `${id}.log`);
|
|
26
35
|
const activeMarkerPath = getActiveMarkerPath(absoluteLogPath);
|
|
@@ -80,7 +89,8 @@ export function createCliLogSink(command, cwd = process.cwd()) {
|
|
|
80
89
|
};
|
|
81
90
|
try {
|
|
82
91
|
markLogRecentlyFinalized(absoluteLogPath);
|
|
83
|
-
|
|
92
|
+
// Index/lock live next to the log file so fallback dirs stay consistent.
|
|
93
|
+
writeLogEntryInDir(entry, commandDir);
|
|
84
94
|
}
|
|
85
95
|
finally {
|
|
86
96
|
rmSync(activeMarkerPath, { force: true });
|
|
@@ -89,6 +99,28 @@ export function createCliLogSink(command, cwd = process.cwd()) {
|
|
|
89
99
|
},
|
|
90
100
|
};
|
|
91
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Create a durable CLI log sink. When the preferred repo state surface is not
|
|
104
|
+
* writable (EPERM/EACCES/EROFS — common in Codex/read-only sandboxes), fall
|
|
105
|
+
* back to TMPDIR so `wp audit`/`wp test` still execute. The fallback is loud
|
|
106
|
+
* on stderr; it is not a silent swallow.
|
|
107
|
+
*/
|
|
108
|
+
export function createCliLogSink(command, cwd = process.cwd()) {
|
|
109
|
+
const preferredDir = getCommandLogDir(command, cwd);
|
|
110
|
+
try {
|
|
111
|
+
return openCliLogSink(preferredDir, command, cwd);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (!isUnwritableFsError(error))
|
|
115
|
+
throw error;
|
|
116
|
+
const fallbackDir = join(tmpdir(), "webpresso", "cli-logs", command);
|
|
117
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
118
|
+
? String(error.code)
|
|
119
|
+
: "unknown";
|
|
120
|
+
process.stderr.write(`wp: CLI log directory not writable (${preferredDir}): ${code}; using ${fallbackDir}\n`);
|
|
121
|
+
return openCliLogSink(fallbackDir, command, cwd);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
92
124
|
export function readCliLogEntries(command, cwd = process.cwd()) {
|
|
93
125
|
const indexPath = getCommandIndexPath(command, cwd);
|
|
94
126
|
try {
|
|
@@ -104,11 +136,21 @@ export function readCliLogEntry(command, ordinal = 1, cwd = process.cwd()) {
|
|
|
104
136
|
return;
|
|
105
137
|
return readCliLogEntries(command, cwd)[ordinal - 1];
|
|
106
138
|
}
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
139
|
+
function readCliLogEntriesFromDir(commandDir) {
|
|
140
|
+
const indexPath = join(commandDir, "index.json");
|
|
141
|
+
try {
|
|
142
|
+
const parsed = readTrustedJsonFile(indexPath);
|
|
143
|
+
return Array.isArray(parsed.entries) ? parsed.entries : [];
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function writeLogEntryInDir(entry, commandDir) {
|
|
150
|
+
withCommandIndexLockInDir(commandDir, () => {
|
|
151
|
+
const indexPath = join(commandDir, "index.json");
|
|
110
152
|
mkdirSync(dirname(indexPath), { recursive: true });
|
|
111
|
-
const currentEntries =
|
|
153
|
+
const currentEntries = readCliLogEntriesFromDir(commandDir).filter((current) => current.id !== entry.id);
|
|
112
154
|
const nextEntries = [entry, ...currentEntries].slice(0, 10);
|
|
113
155
|
const retainedLogPaths = new Set(nextEntries.map((item) => item.logPath));
|
|
114
156
|
for (const removed of currentEntries.slice(9)) {
|
|
@@ -116,7 +158,7 @@ function writeLogEntry(entry, cwd) {
|
|
|
116
158
|
rmSync(removed.logPath, { force: true });
|
|
117
159
|
}
|
|
118
160
|
}
|
|
119
|
-
|
|
161
|
+
pruneInactiveOrphanedLogFilesInDir(commandDir, retainedLogPaths);
|
|
120
162
|
const index = {
|
|
121
163
|
version: 1,
|
|
122
164
|
command: entry.command,
|
|
@@ -125,8 +167,7 @@ function writeLogEntry(entry, cwd) {
|
|
|
125
167
|
writeIndexAtomically(indexPath, index);
|
|
126
168
|
});
|
|
127
169
|
}
|
|
128
|
-
function
|
|
129
|
-
const commandDir = getCommandLogDir(command, cwd);
|
|
170
|
+
function withCommandIndexLockInDir(commandDir, fn) {
|
|
130
171
|
mkdirSync(commandDir, { recursive: true });
|
|
131
172
|
const lockPath = join(commandDir, "index.lock");
|
|
132
173
|
const started = Date.now();
|
|
@@ -157,13 +198,12 @@ function writeIndexAtomically(indexPath, index) {
|
|
|
157
198
|
writeJsonFile(tmpPath, index);
|
|
158
199
|
renameSync(tmpPath, indexPath);
|
|
159
200
|
}
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
for (const file of readdirSync(directory)) {
|
|
201
|
+
function pruneInactiveOrphanedLogFilesInDir(commandDir, retainedLogPaths) {
|
|
202
|
+
mkdirSync(commandDir, { recursive: true });
|
|
203
|
+
for (const file of readdirSync(commandDir)) {
|
|
164
204
|
if (!file.endsWith(".log"))
|
|
165
205
|
continue;
|
|
166
|
-
const absolutePath = join(
|
|
206
|
+
const absolutePath = join(commandDir, file);
|
|
167
207
|
if (!retainedLogPaths.has(absolutePath) && canPruneLogPath(absolutePath)) {
|
|
168
208
|
rmSync(absolutePath, { force: true });
|
|
169
209
|
}
|
|
@@ -30,13 +30,17 @@
|
|
|
30
30
|
* chain unresolvable after repair; 12 lease held; 13 ownership lost.
|
|
31
31
|
* 7. Release the lease owner-checked in a finally.
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
33
|
+
* Survival layers (in-repo SOTA bar):
|
|
34
|
+
* 1. Prefer a **stable** non-versioned launcher when re-execing the guard
|
|
35
|
+
* (`#cli/auto-update/self-invocation.js`) — primary defense.
|
|
36
|
+
* 2. Self-package helpers (probe, repair, tombstone) MUST be static imports
|
|
37
|
+
* loaded at process start — never package-local dynamic import, or Node
|
|
38
|
+
* re-resolves against a removed installId path after `vp update -g`.
|
|
39
|
+
* 3. Argument builders depend on cycle-free leaves (self-invocation,
|
|
40
|
+
* guard-tombstone), never on this command module solely for argv.
|
|
41
|
+
*
|
|
42
|
+
* Probe/repair reads the vite-plus store SSOT + PATH, not this process's
|
|
43
|
+
* dying package root.
|
|
40
44
|
*
|
|
41
45
|
* Custody boundary (stated exactly, mirrored from install-lock.ts): a stale,
|
|
42
46
|
* OS-suspended holder whose child completes a mutation AFTER the successor
|
|
@@ -45,6 +49,9 @@
|
|
|
45
49
|
* resource-side fencing.
|
|
46
50
|
*/
|
|
47
51
|
import type { CAC } from "cac";
|
|
52
|
+
import { buildSelfInvocationCommand, isVersionedAgentKitPayloadPath, resolveStableWpLauncher } from "#cli/auto-update/self-invocation.js";
|
|
53
|
+
/** Re-export leaf helpers for public ergonomics / existing imports. */
|
|
54
|
+
export { buildSelfInvocationCommand, isVersionedAgentKitPayloadPath, resolveStableWpLauncher };
|
|
48
55
|
export declare function registerSelfInstallGuardCommand(cli: CAC): void;
|
|
49
56
|
export declare const GUARD_EXIT_CHILD_FAILED = 10;
|
|
50
57
|
export declare const GUARD_EXIT_UNRESOLVABLE = 11;
|
|
@@ -85,12 +92,3 @@ export interface SelfInstallGuardDeps {
|
|
|
85
92
|
* Testable core. The CLI action wires `process.exit(await runSelfInstallGuard(...))`.
|
|
86
93
|
*/
|
|
87
94
|
export declare function runSelfInstallGuard(rawArgs: readonly string[], deps?: SelfInstallGuardDeps): Promise<number>;
|
|
88
|
-
/**
|
|
89
|
-
* How wp re-invokes itself: the bun-compiled binary IS process.execPath (its
|
|
90
|
-
* argv[1] is a virtual /$bunfs path), while the source/dev lane runs
|
|
91
|
-
* `<runtime> <script>`.
|
|
92
|
-
*/
|
|
93
|
-
export declare function buildSelfInvocationCommand(args: readonly string[], options?: {
|
|
94
|
-
readonly execPath?: string;
|
|
95
|
-
readonly argv1?: string;
|
|
96
|
-
}): string[];
|