@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,224 @@
|
|
|
1
|
+
import { resolveLaunchPermissionMode } from "#cli/commands/dash/launch-preferences.js";
|
|
2
|
+
import { parseMultiHostPlanProposal, parsePlanOnceDecision, } from "#cli/commands/dash/plan-once-schema.js";
|
|
3
|
+
import { buildDashLaunchCommand, } from "#cli/commands/dash/provider-capabilities.js";
|
|
4
|
+
import { updateDashRun } from "#cli/commands/dash/run-registry.js";
|
|
5
|
+
export { PLAN_ONCE_SCHEMA_VERSION, parseMultiHostPlanProposal, parsePlanOnceDecision, multiHostPlanProposalSchema, planOnceDecisionSchema, } from "#cli/commands/dash/plan-once-schema.js";
|
|
6
|
+
/** Documented fan-out concurrency — sequential because coordinator admits one pending launch. */
|
|
7
|
+
export const PLAN_ONCE_FANOUT_CONCURRENCY = "sequential";
|
|
8
|
+
/** Pure confirm-once / cancel reducer — no registry side effects. */
|
|
9
|
+
export function createPlanOnceSession(plan) {
|
|
10
|
+
return { status: "proposed", plan };
|
|
11
|
+
}
|
|
12
|
+
export function reducePlanOnceDecision(session, decision) {
|
|
13
|
+
if (decision.planId !== session.plan.planId) {
|
|
14
|
+
return { ok: false, error: `Decision planId ${decision.planId} does not match session plan` };
|
|
15
|
+
}
|
|
16
|
+
if (session.status !== "proposed") {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
error: `Cannot ${decision.kind} plan in status ${session.status} (confirm-once from proposed only)`,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (decision.kind === "confirm") {
|
|
23
|
+
return { ok: true, session: { ...session, status: "confirmed" } };
|
|
24
|
+
}
|
|
25
|
+
return { ok: true, session: { ...session, status: "cancelled" } };
|
|
26
|
+
}
|
|
27
|
+
/** Parse decision then reduce — convenience for IPC/menu handlers. */
|
|
28
|
+
export function applyPlanOnceDecision(session, rawDecision) {
|
|
29
|
+
const parsed = parsePlanOnceDecision(rawDecision);
|
|
30
|
+
if (!parsed.ok) {
|
|
31
|
+
return {
|
|
32
|
+
ok: false,
|
|
33
|
+
error: parsed.issues.map((i) => i.message).join("; ") || "Invalid plan decision",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return reducePlanOnceDecision(session, parsed.decision);
|
|
37
|
+
}
|
|
38
|
+
function defaultRegistryPort() {
|
|
39
|
+
// createRun is intentionally not used by apply — only updateRun for plan labels.
|
|
40
|
+
// Import createDashRun only when tests inject a full port; production update path:
|
|
41
|
+
return {
|
|
42
|
+
createRun: async () => {
|
|
43
|
+
throw new Error("plan-once apply must not createDashRun; selectAgent/pane-controller owns create");
|
|
44
|
+
},
|
|
45
|
+
updateRun: updateDashRun,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function defaultDeps(input) {
|
|
49
|
+
return {
|
|
50
|
+
launch: input.launch,
|
|
51
|
+
registry: input.registry ?? defaultRegistryPort(),
|
|
52
|
+
resolvePermissionMode: input.resolvePermissionMode ?? resolveLaunchPermissionMode,
|
|
53
|
+
registryCwd: input.registryCwd,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function planOnceRunLabel(planId, host) {
|
|
57
|
+
const base = host.label?.trim() || host.host;
|
|
58
|
+
return `plan-once:${planId}:${base}`;
|
|
59
|
+
}
|
|
60
|
+
function outcomeRunId(result) {
|
|
61
|
+
const outcome = result.outcome;
|
|
62
|
+
if ("runId" in outcome && typeof outcome.runId === "string" && outcome.runId.length > 0) {
|
|
63
|
+
return outcome.runId;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Apply a confirmed plan: sequential fan-out through launch coordinator.
|
|
69
|
+
*
|
|
70
|
+
* Does **not** pre-create registry rows. selectAgent (pane-controller) creates
|
|
71
|
+
* exactly one dash run per host on newRun. On success, plan-once labels that run.
|
|
72
|
+
*/
|
|
73
|
+
export async function applyConfirmedMultiHostPlan(input) {
|
|
74
|
+
if (input.session.status !== "confirmed") {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: `Cannot apply plan in status ${input.session.status} (require confirmed)`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const deps = defaultDeps(input);
|
|
81
|
+
const plan = input.session.plan;
|
|
82
|
+
const hostResults = [];
|
|
83
|
+
for (const entry of plan.hosts) {
|
|
84
|
+
const permissionMode = deps.resolvePermissionMode(entry.extraArgs ?? [], entry.host);
|
|
85
|
+
const command = buildDashLaunchCommand(entry.host, permissionMode, entry.extraArgs ?? []);
|
|
86
|
+
const label = planOnceRunLabel(plan.planId, entry);
|
|
87
|
+
let launchResult;
|
|
88
|
+
try {
|
|
89
|
+
// newRun:true without runId — selectAgent owns createDashRun.
|
|
90
|
+
// prepared:true skips prepare thrash so sequential fan-out does not
|
|
91
|
+
// contend on coordinator pending Fresh.
|
|
92
|
+
launchResult = await deps.launch.launch(command, {
|
|
93
|
+
cwd: entry.workdir,
|
|
94
|
+
newRun: true,
|
|
95
|
+
prepared: true,
|
|
96
|
+
provider: entry.host,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const message = error instanceof Error ? error.message : "Launch threw";
|
|
101
|
+
hostResults.push({
|
|
102
|
+
host: entry.host,
|
|
103
|
+
workdir: entry.workdir,
|
|
104
|
+
runId: null,
|
|
105
|
+
status: "failed",
|
|
106
|
+
permissionMode,
|
|
107
|
+
command,
|
|
108
|
+
error: message,
|
|
109
|
+
});
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (launchResult.kind === "launched") {
|
|
113
|
+
const runId = outcomeRunId(launchResult);
|
|
114
|
+
if (!runId) {
|
|
115
|
+
hostResults.push({
|
|
116
|
+
host: entry.host,
|
|
117
|
+
workdir: entry.workdir,
|
|
118
|
+
runId: null,
|
|
119
|
+
status: "failed",
|
|
120
|
+
permissionMode,
|
|
121
|
+
command,
|
|
122
|
+
error: "selectAgent launched without dash runId (cannot label or attach)",
|
|
123
|
+
});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// Label the select-created run only — never invent a second registry row.
|
|
127
|
+
// Do not write runId into providerSessionId (that field is provider session).
|
|
128
|
+
// tmuxPaneId is already persisted by pane-controller before select returns.
|
|
129
|
+
try {
|
|
130
|
+
await deps.registry.updateRun(runId, { label, error: null }, { cwd: deps.registryCwd });
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
const message = error instanceof Error ? error.message : "Failed to label dash run";
|
|
134
|
+
hostResults.push({
|
|
135
|
+
host: entry.host,
|
|
136
|
+
workdir: entry.workdir,
|
|
137
|
+
runId,
|
|
138
|
+
status: "failed",
|
|
139
|
+
permissionMode,
|
|
140
|
+
command,
|
|
141
|
+
error: message,
|
|
142
|
+
});
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
hostResults.push({
|
|
146
|
+
host: entry.host,
|
|
147
|
+
workdir: entry.workdir,
|
|
148
|
+
runId,
|
|
149
|
+
status: "launched",
|
|
150
|
+
permissionMode,
|
|
151
|
+
command,
|
|
152
|
+
error: null,
|
|
153
|
+
});
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (launchResult.kind === "discarded") {
|
|
157
|
+
hostResults.push({
|
|
158
|
+
host: entry.host,
|
|
159
|
+
workdir: entry.workdir,
|
|
160
|
+
runId: null,
|
|
161
|
+
status: "discarded",
|
|
162
|
+
permissionMode,
|
|
163
|
+
command,
|
|
164
|
+
error: "launch discarded",
|
|
165
|
+
});
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (launchResult.kind === "pending") {
|
|
169
|
+
hostResults.push({
|
|
170
|
+
host: entry.host,
|
|
171
|
+
workdir: entry.workdir,
|
|
172
|
+
runId: null,
|
|
173
|
+
status: "failed",
|
|
174
|
+
permissionMode,
|
|
175
|
+
command,
|
|
176
|
+
error: launchResult.message || "Launch still pending (unexpected in sequential fan-out)",
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
// failed — selectAgent/pane-controller already marked its own run failed if created
|
|
181
|
+
const message = `${launchResult.message}${launchResult.action ? ` · ${launchResult.action}` : ""}`;
|
|
182
|
+
hostResults.push({
|
|
183
|
+
host: entry.host,
|
|
184
|
+
workdir: entry.workdir,
|
|
185
|
+
runId: null,
|
|
186
|
+
status: "failed",
|
|
187
|
+
permissionMode,
|
|
188
|
+
command,
|
|
189
|
+
error: message,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
const launchedCount = hostResults.filter((h) => h.status === "launched").length;
|
|
193
|
+
const failedCount = hostResults.filter((h) => h.status === "failed").length;
|
|
194
|
+
const discardedCount = hostResults.filter((h) => h.status === "discarded").length;
|
|
195
|
+
let nextStatus;
|
|
196
|
+
if (launchedCount === hostResults.length && hostResults.length > 0) {
|
|
197
|
+
nextStatus = "applied";
|
|
198
|
+
}
|
|
199
|
+
else if (launchedCount === 0) {
|
|
200
|
+
nextStatus = "apply-failed";
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
nextStatus = "apply-partial";
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
ok: true,
|
|
207
|
+
result: {
|
|
208
|
+
planId: plan.planId,
|
|
209
|
+
concurrency: PLAN_ONCE_FANOUT_CONCURRENCY,
|
|
210
|
+
hosts: hostResults,
|
|
211
|
+
launchedCount,
|
|
212
|
+
failedCount,
|
|
213
|
+
discardedCount,
|
|
214
|
+
},
|
|
215
|
+
session: { ...input.session, status: nextStatus },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/** Propose → parse helper for callers that only have untrusted JSON. */
|
|
219
|
+
export function proposeMultiHostPlan(raw) {
|
|
220
|
+
const parsed = parseMultiHostPlanProposal(raw);
|
|
221
|
+
if (!parsed.ok)
|
|
222
|
+
return parsed;
|
|
223
|
+
return { ok: true, session: createPlanOnceSession(parsed.plan) };
|
|
224
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const DASH_TUI_IPC_SCHEMA_VERSION: 1;
|
|
3
|
+
/** Keys that must never appear in IPC payloads (secret / env leakage). */
|
|
4
|
+
export declare const FORBIDDEN_IPC_KEYS: readonly ["env", "argv", "secret", "secrets", "password", "token", "authorization", "apiKey", "api_key"];
|
|
5
|
+
export declare const dashTuiViewModelSchema: z.ZodObject<{
|
|
6
|
+
repoLabel: z.ZodString;
|
|
7
|
+
width: z.ZodNumber;
|
|
8
|
+
height: z.ZodNumber;
|
|
9
|
+
permissionMode: z.ZodEnum<{
|
|
10
|
+
dangerous: "dangerous";
|
|
11
|
+
safe: "safe";
|
|
12
|
+
}>;
|
|
13
|
+
hostAvailability: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
14
|
+
id: z.ZodString;
|
|
15
|
+
available: z.ZodBoolean;
|
|
16
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
17
|
+
}, z.core.$strict>>>;
|
|
18
|
+
status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
19
|
+
warnings: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
20
|
+
menuLines: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
21
|
+
overlayKind: z.ZodDefault<z.ZodString>;
|
|
22
|
+
overlayLines: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
23
|
+
paletteQuery: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
24
|
+
}, z.core.$strict>;
|
|
25
|
+
export type DashTuiViewModel = z.infer<typeof dashTuiViewModelSchema>;
|
|
26
|
+
export declare const dashTuiClientKeySchema: z.ZodObject<{
|
|
27
|
+
type: z.ZodLiteral<"key">;
|
|
28
|
+
kind: z.ZodString;
|
|
29
|
+
value: z.ZodOptional<z.ZodString>;
|
|
30
|
+
}, z.core.$strict>;
|
|
31
|
+
export declare const dashTuiClientMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
32
|
+
type: z.ZodLiteral<"hello">;
|
|
33
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
34
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
35
|
+
type: z.ZodLiteral<"ready">;
|
|
36
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
37
|
+
type: z.ZodLiteral<"key">;
|
|
38
|
+
kind: z.ZodString;
|
|
39
|
+
value: z.ZodOptional<z.ZodString>;
|
|
40
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
41
|
+
type: z.ZodLiteral<"resize">;
|
|
42
|
+
width: z.ZodNumber;
|
|
43
|
+
height: z.ZodNumber;
|
|
44
|
+
}, z.core.$strict>], "type">;
|
|
45
|
+
export type DashTuiClientMessage = z.infer<typeof dashTuiClientMessageSchema>;
|
|
46
|
+
export declare const dashTuiServerMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
47
|
+
type: z.ZodLiteral<"hello">;
|
|
48
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
49
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
50
|
+
type: z.ZodLiteral<"view">;
|
|
51
|
+
view: z.ZodObject<{
|
|
52
|
+
repoLabel: z.ZodString;
|
|
53
|
+
width: z.ZodNumber;
|
|
54
|
+
height: z.ZodNumber;
|
|
55
|
+
permissionMode: z.ZodEnum<{
|
|
56
|
+
dangerous: "dangerous";
|
|
57
|
+
safe: "safe";
|
|
58
|
+
}>;
|
|
59
|
+
hostAvailability: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
60
|
+
id: z.ZodString;
|
|
61
|
+
available: z.ZodBoolean;
|
|
62
|
+
reason: z.ZodOptional<z.ZodString>;
|
|
63
|
+
}, z.core.$strict>>>;
|
|
64
|
+
status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
65
|
+
warnings: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
66
|
+
menuLines: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
67
|
+
overlayKind: z.ZodDefault<z.ZodString>;
|
|
68
|
+
overlayLines: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
69
|
+
paletteQuery: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
70
|
+
}, z.core.$strict>;
|
|
71
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
72
|
+
type: z.ZodLiteral<"quit">;
|
|
73
|
+
}, z.core.$strict>], "type">;
|
|
74
|
+
export type DashTuiServerMessage = z.infer<typeof dashTuiServerMessageSchema>;
|
|
75
|
+
export declare function assertNoForbiddenIpcKeys(value: unknown, path?: string): void;
|
|
76
|
+
/** Encode one length-prefixed JSON frame (u32 little-endian length + UTF-8 body). */
|
|
77
|
+
export declare function encodeIpcFrame(message: unknown): Buffer;
|
|
78
|
+
/**
|
|
79
|
+
* Decode complete frames from a byte buffer. Returns messages and any incomplete tail.
|
|
80
|
+
*/
|
|
81
|
+
export declare function decodeIpcFrames(buffer: Buffer): {
|
|
82
|
+
readonly messages: readonly unknown[];
|
|
83
|
+
readonly rest: Buffer;
|
|
84
|
+
};
|
|
85
|
+
export declare function parseClientMessage(raw: unknown): DashTuiClientMessage;
|
|
86
|
+
export declare function parseServerMessage(raw: unknown): DashTuiServerMessage;
|
|
87
|
+
export declare function encodeServerView(view: DashTuiViewModel): Buffer;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// Length-prefixed JSON-RPC-over-stdio framing for the native dash shell.
|
|
2
|
+
// schemaVersion 1 — keep in sync with native/dash-tui.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
export const DASH_TUI_IPC_SCHEMA_VERSION = 1;
|
|
5
|
+
/** Keys that must never appear in IPC payloads (secret / env leakage). */
|
|
6
|
+
export const FORBIDDEN_IPC_KEYS = [
|
|
7
|
+
"env",
|
|
8
|
+
"argv",
|
|
9
|
+
"secret",
|
|
10
|
+
"secrets",
|
|
11
|
+
"password",
|
|
12
|
+
"token",
|
|
13
|
+
"authorization",
|
|
14
|
+
"apiKey",
|
|
15
|
+
"api_key",
|
|
16
|
+
];
|
|
17
|
+
const hostAvailabilitySchema = z
|
|
18
|
+
.object({
|
|
19
|
+
id: z.string().min(1),
|
|
20
|
+
available: z.boolean(),
|
|
21
|
+
reason: z.string().optional(),
|
|
22
|
+
})
|
|
23
|
+
.strict();
|
|
24
|
+
export const dashTuiViewModelSchema = z
|
|
25
|
+
.object({
|
|
26
|
+
repoLabel: z.string(),
|
|
27
|
+
width: z.number().int().positive(),
|
|
28
|
+
height: z.number().int().positive(),
|
|
29
|
+
permissionMode: z.enum(["safe", "dangerous"]),
|
|
30
|
+
hostAvailability: z.array(hostAvailabilitySchema).default([]),
|
|
31
|
+
status: z.string().nullable().optional(),
|
|
32
|
+
warnings: z.array(z.string()).default([]),
|
|
33
|
+
menuLines: z.array(z.string()).default([]),
|
|
34
|
+
overlayKind: z.string().default("none"),
|
|
35
|
+
overlayLines: z.array(z.string()).default([]),
|
|
36
|
+
paletteQuery: z.string().nullable().optional(),
|
|
37
|
+
})
|
|
38
|
+
.strict();
|
|
39
|
+
export const dashTuiClientKeySchema = z
|
|
40
|
+
.object({
|
|
41
|
+
type: z.literal("key"),
|
|
42
|
+
kind: z.string().min(1),
|
|
43
|
+
value: z.string().optional(),
|
|
44
|
+
})
|
|
45
|
+
.strict();
|
|
46
|
+
export const dashTuiClientMessageSchema = z.discriminatedUnion("type", [
|
|
47
|
+
z.object({ type: z.literal("hello"), schemaVersion: z.literal(1) }).strict(),
|
|
48
|
+
z.object({ type: z.literal("ready") }).strict(),
|
|
49
|
+
dashTuiClientKeySchema,
|
|
50
|
+
z
|
|
51
|
+
.object({
|
|
52
|
+
type: z.literal("resize"),
|
|
53
|
+
width: z.number().int().positive(),
|
|
54
|
+
height: z.number().int().positive(),
|
|
55
|
+
})
|
|
56
|
+
.strict(),
|
|
57
|
+
]);
|
|
58
|
+
export const dashTuiServerMessageSchema = z.discriminatedUnion("type", [
|
|
59
|
+
z.object({ type: z.literal("hello"), schemaVersion: z.literal(1) }).strict(),
|
|
60
|
+
z.object({ type: z.literal("view"), view: dashTuiViewModelSchema }).strict(),
|
|
61
|
+
z.object({ type: z.literal("quit") }).strict(),
|
|
62
|
+
]);
|
|
63
|
+
export function assertNoForbiddenIpcKeys(value, path = "$") {
|
|
64
|
+
if (value === null || value === undefined)
|
|
65
|
+
return;
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
value.forEach((item, index) => assertNoForbiddenIpcKeys(item, `${path}[${index}]`));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (typeof value !== "object")
|
|
71
|
+
return;
|
|
72
|
+
for (const [key, child] of Object.entries(value)) {
|
|
73
|
+
const lower = key.toLowerCase();
|
|
74
|
+
if (FORBIDDEN_IPC_KEYS.some((forbidden) => lower === forbidden.toLowerCase() || lower.includes(forbidden.toLowerCase()))) {
|
|
75
|
+
throw new Error(`Forbidden IPC key at ${path}.${key}`);
|
|
76
|
+
}
|
|
77
|
+
assertNoForbiddenIpcKeys(child, `${path}.${key}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Encode one length-prefixed JSON frame (u32 little-endian length + UTF-8 body). */
|
|
81
|
+
export function encodeIpcFrame(message) {
|
|
82
|
+
assertNoForbiddenIpcKeys(message);
|
|
83
|
+
const body = Buffer.from(JSON.stringify(message), "utf8");
|
|
84
|
+
const header = Buffer.alloc(4);
|
|
85
|
+
header.writeUInt32LE(body.length, 0);
|
|
86
|
+
return Buffer.concat([header, body]);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Decode complete frames from a byte buffer. Returns messages and any incomplete tail.
|
|
90
|
+
*/
|
|
91
|
+
export function decodeIpcFrames(buffer) {
|
|
92
|
+
const messages = [];
|
|
93
|
+
let offset = 0;
|
|
94
|
+
while (offset + 4 <= buffer.length) {
|
|
95
|
+
const len = buffer.readUInt32LE(offset);
|
|
96
|
+
if (len > 8 * 1024 * 1024) {
|
|
97
|
+
// Unrecoverable length claim — caller should drop/resync the stream.
|
|
98
|
+
throw new Error(`IPC frame too large: ${len}`);
|
|
99
|
+
}
|
|
100
|
+
if (offset + 4 + len > buffer.length)
|
|
101
|
+
break;
|
|
102
|
+
const body = buffer.subarray(offset + 4, offset + 4 + len);
|
|
103
|
+
offset += 4 + len;
|
|
104
|
+
try {
|
|
105
|
+
messages.push(JSON.parse(body.toString("utf8")));
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Skip one corrupt payload; continue scanning subsequent frames.
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { messages, rest: buffer.subarray(offset) };
|
|
113
|
+
}
|
|
114
|
+
export function parseClientMessage(raw) {
|
|
115
|
+
assertNoForbiddenIpcKeys(raw);
|
|
116
|
+
return dashTuiClientMessageSchema.parse(raw);
|
|
117
|
+
}
|
|
118
|
+
export function parseServerMessage(raw) {
|
|
119
|
+
assertNoForbiddenIpcKeys(raw);
|
|
120
|
+
return dashTuiServerMessageSchema.parse(raw);
|
|
121
|
+
}
|
|
122
|
+
export function encodeServerView(view) {
|
|
123
|
+
const message = {
|
|
124
|
+
type: "view",
|
|
125
|
+
view: dashTuiViewModelSchema.parse(view),
|
|
126
|
+
};
|
|
127
|
+
return encodeIpcFrame(message);
|
|
128
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { type TuiAppState, type TuiEffect } from "#cli/commands/dash/tui/state.js";
|
|
3
|
+
import { type DashTuiViewModel } from "#cli/commands/dash/tui/ipc-protocol.js";
|
|
4
|
+
import type { ParsedKey } from "#cli/commands/dash/tui/keys.js";
|
|
5
|
+
export declare function projectNativeViewModel(state: TuiAppState, input: {
|
|
6
|
+
readonly repoLabel: string;
|
|
7
|
+
readonly width: number;
|
|
8
|
+
readonly height: number;
|
|
9
|
+
}): DashTuiViewModel;
|
|
10
|
+
export declare function clientMessageToParsedKey(message: {
|
|
11
|
+
readonly kind: string;
|
|
12
|
+
readonly value?: string;
|
|
13
|
+
}): ParsedKey | null;
|
|
14
|
+
export type NativeHostHandlers = {
|
|
15
|
+
readonly onEffect: (effect: TuiEffect) => void | Promise<void>;
|
|
16
|
+
readonly onQuit: () => void;
|
|
17
|
+
};
|
|
18
|
+
/** Bridge so the domain runtime can push async state into the native shell. */
|
|
19
|
+
export type NativeHostBridge = {
|
|
20
|
+
readonly pushView: () => void;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Drive pure TUI state from a native binary over length-prefixed JSON frames.
|
|
24
|
+
* State is owned by the caller (runtime) so launch/HUD/browser updates paint.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runNativeTuiHost(input: {
|
|
27
|
+
readonly binaryPath: string;
|
|
28
|
+
readonly repoLabel: string;
|
|
29
|
+
readonly getState: () => TuiAppState;
|
|
30
|
+
readonly setState: (next: TuiAppState) => void;
|
|
31
|
+
readonly handlers: NativeHostHandlers;
|
|
32
|
+
readonly onBridge?: (bridge: NativeHostBridge) => void;
|
|
33
|
+
readonly spawnImpl?: typeof spawn;
|
|
34
|
+
}): Promise<number>;
|